From 765a7af6db9ede660dbae65a665ff8896da4ba49 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 14 Dec 2022 20:23:12 +0000 Subject: [PATCH 001/921] chore: add license and readme Change-Id: I7a5a716eac371b3ab765743500bb109ea5f47de1 --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 + 2 files changed, 205 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..7a4a3ea2424 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000000..3b3fd5761d9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# BigFrames + +BigFrames supports scalable DataFrame APIs on top of BigQuery. From b6caad63cbae63123645a7c560e3242b47f2ca4c Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 20 Jul 2023 16:31:09 -0500 Subject: [PATCH 002/921] feat: prepare for initial release Includes code from all commits in internal repo up to 6a7b313e1d71c523ab039bced341518b4a6928bc Prepared in the following way: ``` git fetch github main git checkout -b prepare-0.1.0 github/main git merge --squash 6a7b313e1d71c523ab039bced341518b4a6928bc ``` --- .flake8 | 35 + .gitignore | 70 + .isort.cfg | 7 + .kokoro/build.sh | 47 + .kokoro/continuous/common.cfg | 10 + .kokoro/continuous/continuous.cfg | 1 + .kokoro/continuous/e2e.cfg | 7 + .kokoro/continuous/nightly.cfg | 8 + .kokoro/presubmit/common.cfg | 10 + .kokoro/presubmit/e2e.cfg | 7 + .kokoro/presubmit/presubmit.cfg | 1 + .kokoro/release-nightly.sh | 213 + .kokoro/requirements.in | 13 + .kokoro/requirements.txt | 567 +++ .pre-commit-config.yaml | 41 + CHANGELOG.md | 22 + CODE_OF_CONDUCT.md | 94 + CONTRIBUTING.md | 33 + LICENSE | 2 +- OWNERS | 10 + README.md | 3 - README.rst | 8 + bigframes/__init__.py | 28 + bigframes/_config/__init__.py | 49 + bigframes/_config/bigquery_options.py | 114 + bigframes/_config/display_options.py | 55 + bigframes/core/__init__.py | 1005 ++++ bigframes/core/block_transforms.py | 199 + bigframes/core/blocks.py | 917 ++++ bigframes/core/groupby/__init__.py | 315 ++ bigframes/core/guid.py | 21 + bigframes/core/indexers.py | 337 ++ bigframes/core/indexes/__init__.py | 23 + bigframes/core/indexes/index.py | 250 + bigframes/core/joins/__init__.py | 23 + bigframes/core/joins/row_identity.py | 201 + bigframes/core/joins/single_column.py | 281 ++ bigframes/core/ordering.py | 181 + bigframes/core/reshape/__init__.py | 61 + bigframes/core/scalar.py | 48 + bigframes/core/window/__init__.py | 85 + bigframes/dataframe.py | 1626 +++++++ bigframes/dtypes.py | 304 ++ bigframes/formatting_helpers.py | 120 + bigframes/ml/__init__.py | 29 + bigframes/ml/base.py | 135 + bigframes/ml/cluster.py | 97 + bigframes/ml/compose.py | 123 + bigframes/ml/core.py | 334 ++ bigframes/ml/decomposition.py | 104 + bigframes/ml/ensemble.py | 672 +++ bigframes/ml/forecasting.py | 125 + bigframes/ml/imported.py | 101 + bigframes/ml/linear_model.py | 247 + bigframes/ml/llm.py | 168 + bigframes/ml/loader.py | 74 + bigframes/ml/metrics.py | 371 ++ bigframes/ml/model_selection.py | 86 + bigframes/ml/pipeline.py | 102 + bigframes/ml/preprocessing.py | 140 + bigframes/ml/sql.py | 173 + bigframes/operations/__init__.py | 546 +++ bigframes/operations/aggregations.py | 288 ++ bigframes/operations/base.py | 198 + bigframes/operations/datetimes.py | 66 + bigframes/operations/strings.py | 81 + bigframes/pandas/__init__.py | 339 ++ bigframes/remote_function.py | 697 +++ bigframes/series.py | 964 ++++ bigframes/session.py | 1055 +++++ bigframes/version.py | 15 + docs/Makefile | 20 + docs/README.rst | 1 + docs/_static/custom.css | 15 + docs/_templates/layout.html | 45 + docs/changelog.md | 1 + docs/conf.py | 382 ++ docs/getting_started/index.rst | 27 + docs/index.rst | 18 + docs/reference/bigframes.ml/README.rst | 125 + docs/reference/bigframes.ml/cluster.rst | 5 + docs/reference/bigframes.ml/compose.rst | 5 + docs/reference/bigframes.ml/decomposition.rst | 5 + docs/reference/bigframes.ml/forecasting.rst | 5 + docs/reference/bigframes.ml/imported.rst | 5 + docs/reference/bigframes.ml/index.rst | 30 + docs/reference/bigframes.ml/linear_model.rst | 5 + docs/reference/bigframes.ml/llm.rst | 5 + docs/reference/bigframes.ml/metrics.rst | 5 + .../bigframes.ml/model_selection.rst | 5 + docs/reference/bigframes.ml/pipeline.rst | 5 + docs/reference/bigframes.ml/preprocessing.rst | 5 + docs/reference/bigframes.pandas/frame.rst | 9 + .../bigframes.pandas/general_functions.rst | 8 + docs/reference/bigframes.pandas/groupby.rst | 20 + docs/reference/bigframes.pandas/index.rst | 15 + docs/reference/bigframes.pandas/indexing.rst | 9 + docs/reference/bigframes.pandas/options.rst | 6 + docs/reference/bigframes.pandas/series.rst | 36 + docs/reference/bigframes.pandas/window.rst | 9 + docs/reference/bigframes/index.rst | 19 + docs/reference/bigframes/options.rst | 12 + docs/reference/index.rst | 12 + docs/samples | 1 + .../bigframes.pandas/remote_functions.rst | 40 + docs/user_guide/index.rst | 9 + mypy.ini | 25 + notebooks/00 - Summary.ipynb | 2060 ++++++++ notebooks/01 - Getting Started.ipynb | 965 ++++ notebooks/02 - DataFrame.ipynb | 4158 +++++++++++++++++ .../03 - Using ML - ML fundamentals.ipynb | 2250 +++++++++ ...Using ML - SKLearn linear regression.ipynb | 977 ++++ ... - Using ML - Easy linear regression.ipynb | 920 ++++ ...6 - Using ML - Large Language Models.ipynb | 201 + notebooks/50 - Remote Function.ipynb | 839 ++++ notebooks/99 - Longer ML demo.ipynb | 1925 ++++++++ noxfile.py | 636 +++ pytest.ini | 3 + samples/snippets/__init__.py | 13 + samples/snippets/quickstart.py | 89 + samples/snippets/quickstart_test.py | 31 + scripts/generate_third_party_notices.py | 332 ++ scripts/publish_api_coverage.py | 179 + scripts/test_publish_api_coverage.py | 45 + scripts/update_firebase_docs_site.sh | 105 + scripts/update_x20_docs_site.sh | 106 + setup.cfg | 33 + setup.py | 109 + testing/constraints-3.10.txt | 15 + testing/constraints-3.11.txt | 0 testing/constraints-3.9.txt | 118 + tests/__init__.py | 13 + tests/data/penguins.jsonl | 344 ++ tests/data/penguins_schema.json | 37 + tests/data/scalars.jsonl | 9 + tests/data/scalars_schema.json | 75 + tests/data/time_series.jsonl | 366 ++ tests/data/time_series_schema.json | 12 + tests/system/__init__.py | 13 + tests/system/conftest.py | 667 +++ tests/system/large/__init__.py | 13 + tests/system/large/ml/test_cluster.py | 103 + tests/system/large/ml/test_compose.py | 71 + tests/system/large/ml/test_core.py | 183 + tests/system/large/ml/test_decomposition.py | 70 + tests/system/large/ml/test_ensemble.py | 458 ++ tests/system/large/ml/test_forecasting.py | 48 + tests/system/large/ml/test_linear_model.py | 196 + tests/system/large/ml/test_pipeline.py | 524 +++ tests/system/large/test_remote_function.py | 776 +++ tests/system/large/test_session.py | 52 + tests/system/small/__init__.py | 13 + tests/system/small/ml/__init__.py | 13 + tests/system/small/ml/conftest.py | 294 ++ tests/system/small/ml/test_cluster.py | 79 + tests/system/small/ml/test_core.py | 193 + tests/system/small/ml/test_decomposition.py | 55 + tests/system/small/ml/test_ensemble.py | 427 ++ tests/system/small/ml/test_forecasting.py | 44 + tests/system/small/ml/test_imported.py | 68 + tests/system/small/ml/test_linear_model.py | 209 + tests/system/small/ml/test_llm.py | 72 + tests/system/small/ml/test_metrics.py | 615 +++ tests/system/small/ml/test_model_selection.py | 214 + tests/system/small/ml/test_preprocessing.py | 89 + tests/system/small/ml/test_register.py | 87 + tests/system/small/operations/__init__.py | 13 + .../system/small/operations/test_datetimes.py | 198 + tests/system/small/operations/test_strings.py | 230 + tests/system/small/test_dataframe.py | 1694 +++++++ tests/system/small/test_dataframe_io.py | 325 ++ tests/system/small/test_groupby.py | 129 + tests/system/small/test_index.py | 46 + tests/system/small/test_multiindex.py | 273 ++ tests/system/small/test_pandas.py | 64 + tests/system/small/test_pandas_options.py | 145 + tests/system/small/test_progress_bar.py | 74 + tests/system/small/test_remote_function.py | 278 ++ tests/system/small/test_scalar.py | 25 + tests/system/small/test_series.py | 1934 ++++++++ tests/system/small/test_session.py | 673 +++ tests/system/small/test_window.py | 66 + tests/system/utils.py | 141 + tests/unit/__init__.py | 13 + tests/unit/_config/__init__.py | 13 + tests/unit/_config/test_bigquery_options.py | 73 + tests/unit/conftest.py | 223 + tests/unit/ml/__init__.py | 13 + tests/unit/ml/test_api_primitives.py | 53 + tests/unit/ml/test_compose.py | 95 + tests/unit/ml/test_pipeline.py | 110 + tests/unit/ml/test_sql.py | 145 + tests/unit/test_core.py | 99 + tests/unit/test_dtypes.py | 184 + tests/unit/test_pandas.py | 81 + tests/unit/test_session.py | 48 + third_party/__init__.py | 0 third_party/bigframes_vendored/__init__.py | 0 .../bigframes_vendored/cpython/LICENSE | 10 + .../bigframes_vendored/cpython/__init__.py | 0 .../bigframes_vendored/cpython/_pprint.py | 470 ++ .../bigframes_vendored/pandas/AUTHORS.md | 56 + third_party/bigframes_vendored/pandas/LICENSE | 31 + .../bigframes_vendored/pandas/README.md | 174 + .../bigframes_vendored/pandas/__init__.py | 0 .../pandas/core/config_init.py | 25 + .../bigframes_vendored/pandas/core/frame.py | 1124 +++++ .../bigframes_vendored/pandas/core/generic.py | 438 ++ .../pandas/core/groupby/__init__.py | 318 ++ .../pandas/core/indexes/__init__.py | 0 .../pandas/core/indexes/accessor.py | 94 + .../pandas/core/indexes/base.py | 20 + .../pandas/core/indexing.py | 72 + .../pandas/core/reshape/concat.py | 135 + .../bigframes_vendored/pandas/core/series.py | 1553 ++++++ .../pandas/core/strings/accessor.py | 187 + .../pandas/core/window/__init__.py | 0 .../pandas/core/window/rolling.py | 37 + .../bigframes_vendored/pandas/io/__init__.py | 0 .../bigframes_vendored/pandas/io/common.py | 44 + .../bigframes_vendored/pandas/io/gbq.py | 37 + .../bigframes_vendored/pandas/io/parquet.py | 22 + .../pandas/io/parsers/__init__.py | 0 .../pandas/io/parsers/readers.py | 110 + .../pandas/pandas/_typing.py | 425 ++ .../bigframes_vendored/sklearn/COPYING | 29 + .../bigframes_vendored/sklearn/base.py | 128 + .../sklearn/cluster/_kmeans.py | 71 + .../sklearn/compose/_column_transformer.py | 61 + .../sklearn/decomposition/_pca.py | 53 + .../sklearn/ensemble/_forest.py | 191 + .../sklearn/linear_model/_base.py | 98 + .../sklearn/linear_model/_logistic.py | 83 + .../sklearn/metrics/_classification.py | 173 + .../sklearn/metrics/_ranking.py | 100 + .../sklearn/metrics/_regression.py | 53 + .../bigframes_vendored/sklearn/pipeline.py | 83 + .../sklearn/preprocessing/_data.py | 53 + .../sklearn/preprocessing/_encoder.py | 41 + .../bigframes_vendored/xgboost/LICENSE | 201 + .../bigframes_vendored/xgboost/sklearn.py | 145 + 241 files changed, 52741 insertions(+), 4 deletions(-) create mode 100644 .flake8 create mode 100644 .gitignore create mode 100644 .isort.cfg create mode 100644 .kokoro/build.sh create mode 100644 .kokoro/continuous/common.cfg create mode 100644 .kokoro/continuous/continuous.cfg create mode 100644 .kokoro/continuous/e2e.cfg create mode 100644 .kokoro/continuous/nightly.cfg create mode 100644 .kokoro/presubmit/common.cfg create mode 100644 .kokoro/presubmit/e2e.cfg create mode 100644 .kokoro/presubmit/presubmit.cfg create mode 100755 .kokoro/release-nightly.sh create mode 100644 .kokoro/requirements.in create mode 100644 .kokoro/requirements.txt create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 OWNERS delete mode 100644 README.md create mode 100644 README.rst create mode 100644 bigframes/__init__.py create mode 100644 bigframes/_config/__init__.py create mode 100644 bigframes/_config/bigquery_options.py create mode 100644 bigframes/_config/display_options.py create mode 100644 bigframes/core/__init__.py create mode 100644 bigframes/core/block_transforms.py create mode 100644 bigframes/core/blocks.py create mode 100644 bigframes/core/groupby/__init__.py create mode 100644 bigframes/core/guid.py create mode 100644 bigframes/core/indexers.py create mode 100644 bigframes/core/indexes/__init__.py create mode 100644 bigframes/core/indexes/index.py create mode 100644 bigframes/core/joins/__init__.py create mode 100644 bigframes/core/joins/row_identity.py create mode 100644 bigframes/core/joins/single_column.py create mode 100644 bigframes/core/ordering.py create mode 100644 bigframes/core/reshape/__init__.py create mode 100644 bigframes/core/scalar.py create mode 100644 bigframes/core/window/__init__.py create mode 100644 bigframes/dataframe.py create mode 100644 bigframes/dtypes.py create mode 100644 bigframes/formatting_helpers.py create mode 100644 bigframes/ml/__init__.py create mode 100644 bigframes/ml/base.py create mode 100644 bigframes/ml/cluster.py create mode 100644 bigframes/ml/compose.py create mode 100644 bigframes/ml/core.py create mode 100644 bigframes/ml/decomposition.py create mode 100644 bigframes/ml/ensemble.py create mode 100644 bigframes/ml/forecasting.py create mode 100644 bigframes/ml/imported.py create mode 100644 bigframes/ml/linear_model.py create mode 100644 bigframes/ml/llm.py create mode 100644 bigframes/ml/loader.py create mode 100644 bigframes/ml/metrics.py create mode 100644 bigframes/ml/model_selection.py create mode 100644 bigframes/ml/pipeline.py create mode 100644 bigframes/ml/preprocessing.py create mode 100644 bigframes/ml/sql.py create mode 100644 bigframes/operations/__init__.py create mode 100644 bigframes/operations/aggregations.py create mode 100644 bigframes/operations/base.py create mode 100644 bigframes/operations/datetimes.py create mode 100644 bigframes/operations/strings.py create mode 100644 bigframes/pandas/__init__.py create mode 100644 bigframes/remote_function.py create mode 100644 bigframes/series.py create mode 100644 bigframes/session.py create mode 100644 bigframes/version.py create mode 100644 docs/Makefile create mode 120000 docs/README.rst create mode 100644 docs/_static/custom.css create mode 100644 docs/_templates/layout.html create mode 120000 docs/changelog.md create mode 100644 docs/conf.py create mode 100644 docs/getting_started/index.rst create mode 100644 docs/index.rst create mode 100644 docs/reference/bigframes.ml/README.rst create mode 100644 docs/reference/bigframes.ml/cluster.rst create mode 100644 docs/reference/bigframes.ml/compose.rst create mode 100644 docs/reference/bigframes.ml/decomposition.rst create mode 100644 docs/reference/bigframes.ml/forecasting.rst create mode 100644 docs/reference/bigframes.ml/imported.rst create mode 100644 docs/reference/bigframes.ml/index.rst create mode 100644 docs/reference/bigframes.ml/linear_model.rst create mode 100644 docs/reference/bigframes.ml/llm.rst create mode 100644 docs/reference/bigframes.ml/metrics.rst create mode 100644 docs/reference/bigframes.ml/model_selection.rst create mode 100644 docs/reference/bigframes.ml/pipeline.rst create mode 100644 docs/reference/bigframes.ml/preprocessing.rst create mode 100644 docs/reference/bigframes.pandas/frame.rst create mode 100644 docs/reference/bigframes.pandas/general_functions.rst create mode 100644 docs/reference/bigframes.pandas/groupby.rst create mode 100644 docs/reference/bigframes.pandas/index.rst create mode 100644 docs/reference/bigframes.pandas/indexing.rst create mode 100644 docs/reference/bigframes.pandas/options.rst create mode 100644 docs/reference/bigframes.pandas/series.rst create mode 100644 docs/reference/bigframes.pandas/window.rst create mode 100644 docs/reference/bigframes/index.rst create mode 100644 docs/reference/bigframes/options.rst create mode 100644 docs/reference/index.rst create mode 120000 docs/samples create mode 100644 docs/user_guide/bigframes.pandas/remote_functions.rst create mode 100644 docs/user_guide/index.rst create mode 100644 mypy.ini create mode 100644 notebooks/00 - Summary.ipynb create mode 100644 notebooks/01 - Getting Started.ipynb create mode 100644 notebooks/02 - DataFrame.ipynb create mode 100644 notebooks/03 - Using ML - ML fundamentals.ipynb create mode 100644 notebooks/04 - Using ML - SKLearn linear regression.ipynb create mode 100644 notebooks/05 - Using ML - Easy linear regression.ipynb create mode 100644 notebooks/06 - Using ML - Large Language Models.ipynb create mode 100644 notebooks/50 - Remote Function.ipynb create mode 100644 notebooks/99 - Longer ML demo.ipynb create mode 100644 noxfile.py create mode 100644 pytest.ini create mode 100644 samples/snippets/__init__.py create mode 100644 samples/snippets/quickstart.py create mode 100644 samples/snippets/quickstart_test.py create mode 100644 scripts/generate_third_party_notices.py create mode 100644 scripts/publish_api_coverage.py create mode 100644 scripts/test_publish_api_coverage.py create mode 100644 scripts/update_firebase_docs_site.sh create mode 100644 scripts/update_x20_docs_site.sh create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 testing/constraints-3.10.txt create mode 100644 testing/constraints-3.11.txt create mode 100644 testing/constraints-3.9.txt create mode 100644 tests/__init__.py create mode 100644 tests/data/penguins.jsonl create mode 100644 tests/data/penguins_schema.json create mode 100644 tests/data/scalars.jsonl create mode 100644 tests/data/scalars_schema.json create mode 100644 tests/data/time_series.jsonl create mode 100644 tests/data/time_series_schema.json create mode 100644 tests/system/__init__.py create mode 100644 tests/system/conftest.py create mode 100644 tests/system/large/__init__.py create mode 100644 tests/system/large/ml/test_cluster.py create mode 100644 tests/system/large/ml/test_compose.py create mode 100644 tests/system/large/ml/test_core.py create mode 100644 tests/system/large/ml/test_decomposition.py create mode 100644 tests/system/large/ml/test_ensemble.py create mode 100644 tests/system/large/ml/test_forecasting.py create mode 100644 tests/system/large/ml/test_linear_model.py create mode 100644 tests/system/large/ml/test_pipeline.py create mode 100644 tests/system/large/test_remote_function.py create mode 100644 tests/system/large/test_session.py create mode 100644 tests/system/small/__init__.py create mode 100644 tests/system/small/ml/__init__.py create mode 100644 tests/system/small/ml/conftest.py create mode 100644 tests/system/small/ml/test_cluster.py create mode 100644 tests/system/small/ml/test_core.py create mode 100644 tests/system/small/ml/test_decomposition.py create mode 100644 tests/system/small/ml/test_ensemble.py create mode 100644 tests/system/small/ml/test_forecasting.py create mode 100644 tests/system/small/ml/test_imported.py create mode 100644 tests/system/small/ml/test_linear_model.py create mode 100644 tests/system/small/ml/test_llm.py create mode 100644 tests/system/small/ml/test_metrics.py create mode 100644 tests/system/small/ml/test_model_selection.py create mode 100644 tests/system/small/ml/test_preprocessing.py create mode 100644 tests/system/small/ml/test_register.py create mode 100644 tests/system/small/operations/__init__.py create mode 100644 tests/system/small/operations/test_datetimes.py create mode 100644 tests/system/small/operations/test_strings.py create mode 100644 tests/system/small/test_dataframe.py create mode 100644 tests/system/small/test_dataframe_io.py create mode 100644 tests/system/small/test_groupby.py create mode 100644 tests/system/small/test_index.py create mode 100644 tests/system/small/test_multiindex.py create mode 100644 tests/system/small/test_pandas.py create mode 100644 tests/system/small/test_pandas_options.py create mode 100644 tests/system/small/test_progress_bar.py create mode 100644 tests/system/small/test_remote_function.py create mode 100644 tests/system/small/test_scalar.py create mode 100644 tests/system/small/test_series.py create mode 100644 tests/system/small/test_session.py create mode 100644 tests/system/small/test_window.py create mode 100644 tests/system/utils.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/_config/__init__.py create mode 100644 tests/unit/_config/test_bigquery_options.py create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/ml/__init__.py create mode 100644 tests/unit/ml/test_api_primitives.py create mode 100644 tests/unit/ml/test_compose.py create mode 100644 tests/unit/ml/test_pipeline.py create mode 100644 tests/unit/ml/test_sql.py create mode 100644 tests/unit/test_core.py create mode 100644 tests/unit/test_dtypes.py create mode 100644 tests/unit/test_pandas.py create mode 100644 tests/unit/test_session.py create mode 100644 third_party/__init__.py create mode 100644 third_party/bigframes_vendored/__init__.py create mode 100644 third_party/bigframes_vendored/cpython/LICENSE create mode 100644 third_party/bigframes_vendored/cpython/__init__.py create mode 100644 third_party/bigframes_vendored/cpython/_pprint.py create mode 100644 third_party/bigframes_vendored/pandas/AUTHORS.md create mode 100644 third_party/bigframes_vendored/pandas/LICENSE create mode 100644 third_party/bigframes_vendored/pandas/README.md create mode 100644 third_party/bigframes_vendored/pandas/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/config_init.py create mode 100644 third_party/bigframes_vendored/pandas/core/frame.py create mode 100644 third_party/bigframes_vendored/pandas/core/generic.py create mode 100644 third_party/bigframes_vendored/pandas/core/groupby/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/indexes/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/indexes/accessor.py create mode 100644 third_party/bigframes_vendored/pandas/core/indexes/base.py create mode 100644 third_party/bigframes_vendored/pandas/core/indexing.py create mode 100644 third_party/bigframes_vendored/pandas/core/reshape/concat.py create mode 100644 third_party/bigframes_vendored/pandas/core/series.py create mode 100644 third_party/bigframes_vendored/pandas/core/strings/accessor.py create mode 100644 third_party/bigframes_vendored/pandas/core/window/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/window/rolling.py create mode 100644 third_party/bigframes_vendored/pandas/io/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/io/common.py create mode 100644 third_party/bigframes_vendored/pandas/io/gbq.py create mode 100644 third_party/bigframes_vendored/pandas/io/parquet.py create mode 100644 third_party/bigframes_vendored/pandas/io/parsers/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/io/parsers/readers.py create mode 100644 third_party/bigframes_vendored/pandas/pandas/_typing.py create mode 100644 third_party/bigframes_vendored/sklearn/COPYING create mode 100644 third_party/bigframes_vendored/sklearn/base.py create mode 100644 third_party/bigframes_vendored/sklearn/cluster/_kmeans.py create mode 100644 third_party/bigframes_vendored/sklearn/compose/_column_transformer.py create mode 100644 third_party/bigframes_vendored/sklearn/decomposition/_pca.py create mode 100644 third_party/bigframes_vendored/sklearn/ensemble/_forest.py create mode 100644 third_party/bigframes_vendored/sklearn/linear_model/_base.py create mode 100644 third_party/bigframes_vendored/sklearn/linear_model/_logistic.py create mode 100644 third_party/bigframes_vendored/sklearn/metrics/_classification.py create mode 100644 third_party/bigframes_vendored/sklearn/metrics/_ranking.py create mode 100644 third_party/bigframes_vendored/sklearn/metrics/_regression.py create mode 100644 third_party/bigframes_vendored/sklearn/pipeline.py create mode 100644 third_party/bigframes_vendored/sklearn/preprocessing/_data.py create mode 100644 third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py create mode 100644 third_party/bigframes_vendored/xgboost/LICENSE create mode 100644 third_party/bigframes_vendored/xgboost/sklearn.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000000..7826eceab6a --- /dev/null +++ b/.flake8 @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2020 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 +# +# https://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. + +# Generated by synthtool. DO NOT EDIT! +[flake8] +ignore = E203, E231, E266, E501, W503 +exclude = + third_party/** + + # Exclude generated code. + **/proto/** + **/gapic/** + **/services/** + **/types/** + *_pb2.py + + # Standard linting exemptions. + **/.nox/** + __pycache__, + .git, + *.pyc, + conf.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..2cb117c23ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +*.py[cod] +*.sw[op] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +.eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.coverage.* +.nox +.cache +.mypy_cache +.pytest_cache +.pytype + + +# Mac +.DS_Store + +# JetBrains +.idea + +# VS Code +.vscode + +# emacs +*~ + +# Built documentation +docs/_build +bigquery/docs/generated +docs.metadata + +# Virtual environment +env/ +venv/ + +# Test logs +coverage.xml +*sponge_log.xml + +# System test environment variables. +system_tests/local_test_setup + +# Make sure a generated file isn't accidentally committed. +pylintrc +pylintrc.test + +# Notebook scratch +.ipynb_checkpoints diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000000..56ce2981190 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,7 @@ +[settings] +profile=black +force_sort_within_sections=True +lexicographical=True +single_line_exclusions=('typing',) +order_by_type=False +group_by_package=True diff --git a/.kokoro/build.sh b/.kokoro/build.sh new file mode 100644 index 00000000000..f80cf6eab97 --- /dev/null +++ b/.kokoro/build.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +set -eo pipefail + +if [[ -z "${PROJECT_ROOT:-}" ]]; then + PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/git/bigframes" +fi + +cd "${PROJECT_ROOT}" + +# Disable buffering, so that the logs stream through. +export PYTHONUNBUFFERED=1 + +# Debug: show build environment +env | grep KOKORO + +# Install pip +python3 -m pip install --upgrade --quiet pip +python3 -m pip --version + +# Remove old nox +python3 -m pip uninstall --yes --quiet nox-automation + +# Install nox +python3 -m pip install --upgrade --quiet nox +python3 -m nox --version + +# If NOX_SESSION is set, it only runs the specified session, +# otherwise run all the sessions. +if [[ -n "${NOX_SESSION:-}" ]]; then + python3 -m nox --stop-on-first-error -s ${NOX_SESSION:-} +else + python3 -m nox --stop-on-first-error +fi diff --git a/.kokoro/continuous/common.cfg b/.kokoro/continuous/common.cfg new file mode 100644 index 00000000000..5d40578ac79 --- /dev/null +++ b/.kokoro/continuous/common.cfg @@ -0,0 +1,10 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +build_file: "bigframes/.kokoro/build.sh" diff --git a/.kokoro/continuous/continuous.cfg b/.kokoro/continuous/continuous.cfg new file mode 100644 index 00000000000..18a4c35325b --- /dev/null +++ b/.kokoro/continuous/continuous.cfg @@ -0,0 +1 @@ +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/continuous/e2e.cfg b/.kokoro/continuous/e2e.cfg new file mode 100644 index 00000000000..e2ca8bc78db --- /dev/null +++ b/.kokoro/continuous/e2e.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "system_prerelease system_noextras e2e notebook samples" +} diff --git a/.kokoro/continuous/nightly.cfg b/.kokoro/continuous/nightly.cfg new file mode 100644 index 00000000000..5d1ec3a8f75 --- /dev/null +++ b/.kokoro/continuous/nightly.cfg @@ -0,0 +1,8 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "NOX_SESSION" + value: "unit unit_prerelease system system_prerelease cover lint lint_setup_py mypy format docs e2e notebook" +} + +build_file: "bigframes/.kokoro/release-nightly.sh" diff --git a/.kokoro/presubmit/common.cfg b/.kokoro/presubmit/common.cfg new file mode 100644 index 00000000000..5d40578ac79 --- /dev/null +++ b/.kokoro/presubmit/common.cfg @@ -0,0 +1,10 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +build_file: "bigframes/.kokoro/build.sh" diff --git a/.kokoro/presubmit/e2e.cfg b/.kokoro/presubmit/e2e.cfg new file mode 100644 index 00000000000..e2ca8bc78db --- /dev/null +++ b/.kokoro/presubmit/e2e.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "system_prerelease system_noextras e2e notebook samples" +} diff --git a/.kokoro/presubmit/presubmit.cfg b/.kokoro/presubmit/presubmit.cfg new file mode 100644 index 00000000000..18a4c35325b --- /dev/null +++ b/.kokoro/presubmit/presubmit.cfg @@ -0,0 +1 @@ +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh new file mode 100755 index 00000000000..8742a0fc790 --- /dev/null +++ b/.kokoro/release-nightly.sh @@ -0,0 +1,213 @@ +#!/bin/bash +# Copyright 2020 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 +# +# https://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. + +# Based loosely on +# https://github.com/googleapis/python-bigquery/blob/main/.kokoro/release.sh + +set -eo pipefail +set -x + +# Parse command line arguments +DRY_RUN= +while [ $# -gt 0 ] ; do + case "$1" in + -d | --dry-run ) + DRY_RUN=true + ;; + -h | --help ) + echo -e "USAGE: `basename $0` [ -d | --dry-run ]" + exit + ;; + esac + shift 1; +done + +if [ -z "${PROJECT_ROOT:-}" ]; then + PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/git/bigframes" +fi + +# Move into the package, build the distribution and upload to shared bucket. +# See internal bug 274624240 for details. + +cd "${PROJECT_ROOT}" +rm -rf build dist + +# Workaround the fact that the repository that has been fetched before the +# build script. See: go/kokoro-native-docker-migration#known-issues and +# internal issue b/261050975. +git config --global --add safe.directory "${PROJECT_ROOT}" + +python3.10 -m pip install --require-hashes -r .kokoro/requirements.txt + +# Disable buffering, so that the logs stream through. +export PYTHONUNBUFFERED=1 + +# Install dependencies, as the following steps depend on it +python3.10 -m pip install -e .[all] + +# If NOX_SESSION is set, it only runs the specified session, +# otherwise run all the sessions. +if ! [ ${DRY_RUN} ]; then + if [ -n "${NOX_SESSION:-}" ]; then + python3.10 -m nox -s ${NOX_SESSION:-} + else + python3.10 -m nox + fi +fi + +# Generate third party notices and include it in the licenses in setup.cfg +# TODO(shobs): Don't include it in the package once vertex colab can pick it +# from elsewhere +THIRD_PARTY_NOTICES_FILE=THIRD_PARTY_NOTICES +python3.10 -m pip install pip-licenses +python3.10 scripts/generate_third_party_notices.py --output-file ${THIRD_PARTY_NOTICES_FILE} +if ! [ -s ${THIRD_PARTY_NOTICES_FILE} ]; then + echo "${THIRD_PARTY_NOTICES_FILE} was generated with zero size" + exit -1 +fi +SETUP_CFG_BKP=`mktemp` +cp -f setup.cfg ${SETUP_CFG_BKP} +cat >> setup.cfg << EOF + +[metadata] +license_files = + LICENSE + ${THIRD_PARTY_NOTICES_FILE} +EOF + +# Update version string to include git hash and date +CURRENT_DATE=$(date '+%Y%m%d') +GIT_HASH=$(git rev-parse --short HEAD) +BIGFRAMES_VERSION=$(python3.10 -c "import bigframes; print(bigframes.__version__)") +RELEASE_VERSION=${BIGFRAMES_VERSION}dev${CURRENT_DATE}+${GIT_HASH} +sed -i -e "s/$BIGFRAMES_VERSION/$RELEASE_VERSION/g" bigframes/version.py + +# Generate the package wheel +python3.10 setup.py sdist bdist_wheel + +# Make sure that the wheel file is generated +VERSION_WHEEL=`ls dist/bigframes-*.whl` +num_wheel_files=`echo $VERSION_WHEEL | wc -w` +if [ $num_wheel_files -ne 1 ] ; then + echo "Exactly one wheel file should have been generated, found $num_wheel_files: $VERSION_WHEEL" + exit -1 +fi + +# Make sure the wheel file has the third party notices included +# TODO(shobs): An utimate validation would be to create a virtual environment +# and install the wheel file, then verify that +# site-packages/bigframes-*.dist-info/ includes third party notices +python3.10 -c " +from zipfile import ZipFile +with ZipFile('$VERSION_WHEEL') as myzip: + third_party_licenses_info = [ + info + for info in myzip.infolist() + if info.filename.endswith('.dist-info/${THIRD_PARTY_NOTICES_FILE}') + ] + assert ( + len(third_party_licenses_info) == 1 + ), f'Found {len(third_party_licenses_info)} third party licenses' + assert ( + third_party_licenses_info[0].file_size > 0 + ), 'Package contains third party license of size 0' +" + +# Create a copy of the wheel with a well known, version agnostic name +LATEST_WHEEL=dist/bigframes-latest-py2.py3-none-any.whl +cp $VERSION_WHEEL $LATEST_WHEEL +cp dist/bigframes-*.tar.gz dist/bigframes-latest.tar.gz + +if ! [ ${DRY_RUN} ]; then + for gcs_path in gs://vertex_sdk_private_releases/bigframe/ \ + gs://dl-platform-colab/bigframes/ \ + gs://bigframes-wheels/; + do + gsutil cp -v dist/* ${gcs_path} + gsutil cp -v LICENSE ${gcs_path} + gsutil cp -v ${THIRD_PARTY_NOTICES_FILE} ${gcs_path} + gsutil -m cp -v "notebooks/00 - Summary.ipynb" \ + "notebooks/01 - Getting Started.ipynb" \ + "notebooks/02 - DataFrame.ipynb" \ + "notebooks/03 - Using ML - ML fundamentals.ipynb" \ + "notebooks/04 - Using ML - SKLearn linear regression.ipynb" \ + "notebooks/05 - Using ML - Easy linear regression.ipynb" \ + "notebooks/06 - Using ML - Large Language Models.ipynb" \ + "notebooks/50 - Remote Function.ipynb" \ + ${gcs_path}notebooks/ + done + + # publish API coverage information to BigQuery + # Note: only the kokoro service account has permission to write to this + # table, if you want to test this step, point it to a table you have + # write access to + COVERAGE_TABLE=bigframes-metrics.coverage_report.bigframes_coverage_nightly + python3.10 scripts/publish_api_coverage.py \ + --bigframes_version=$BIGFRAMES_VERSION \ + --release_version=$RELEASE_VERSION \ + --bigquery_table=$COVERAGE_TABLE +fi + +# Undo the file changes, in case this script is running on a +# non-temporary instance of the bigframes repo +# TODO: This doesn't work with (set -eo pipefail) if the failure happened after +# the changes were made but before this cleanup, because the script would +# terminate with the failure itself. See if we can ensure the cleanup. +sed -i -e "s/$RELEASE_VERSION/$BIGFRAMES_VERSION/g" bigframes/version.py +mv -f ${SETUP_CFG_BKP} setup.cfg +rm -f ${THIRD_PARTY_NOTICES_FILE} + +# Keep this last so as not to block the release on PDF docs build. +pdf_docs () { + sudo apt update + sudo apt install -y texlive texlive-latex-extra latexmk + + pushd "${PROJECT_ROOT}/docs" + make latexpdf + + cp "_build/latex/bigframes.pdf" "_build/latex/bigframes-${RELEASE_VERSION}.pdf" + cp "_build/latex/bigframes.pdf" "_build/latex/bigframes-latest.pdf" + + if ! [ ${DRY_RUN} ]; then + for gcs_path in gs://vertex_sdk_private_releases/bigframe/ \ + gs://dl-platform-colab/bigframes/ \ + gs://bigframes-wheels/; + do + gsutil cp -v "_build/latex/bigframes-*.pdf" ${gcs_path} + done + fi + + popd +} + +pdf_docs + +# Copy html docs to GCS from where it can be deployed to anywhere else +gcs_docs () { + docs_gcs_bucket=gs://bigframes-docs + docs_local_html_folder=docs/_build/html + if [ ! -d ${docs_local_html_folder} ]; then + python3.10 -m nox -s docs + fi + + if ! [ ${DRY_RUN} ]; then + gsutil -m cp -v -r ${docs_local_html_folder} ${docs_gcs_bucket}/${GIT_HASH} + + # Copy the script to refresh firebase docs website from GCS to GCS itself + gsutil -m cp -v scripts/update_firebase_docs_site.sh ${docs_gcs_bucket} + fi +} + +gcs_docs diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in new file mode 100644 index 00000000000..9fd9e127baf --- /dev/null +++ b/.kokoro/requirements.in @@ -0,0 +1,13 @@ +gcp-docuploader +gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x +importlib-metadata +typing-extensions +twine +wheel +setuptools +nox>=2022.11.21 # required to remove dependency on py +charset-normalizer<3 +click<8.1.0 +sphinx==4.5.0 # docs PDF see noxfile for HTML docs build +alabaster # docs PDF +recommonmark # docs PDF diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt new file mode 100644 index 00000000000..37a007667f1 --- /dev/null +++ b/.kokoro/requirements.txt @@ -0,0 +1,567 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --allow-unsafe --generate-hashes requirements.in +# +alabaster==0.7.13 \ + --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \ + --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2 + # via + # -r requirements.in + # sphinx +argcomplete==3.1.1 \ + --hash=sha256:35fa893a88deea85ea7b20d241100e64516d6af6d7b0ae2bed1d263d26f70948 \ + --hash=sha256:6c4c563f14f01440aaffa3eae13441c5db2357b5eec639abe7c0b15334627dff + # via nox +attrs==23.1.0 \ + --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ + --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 + # via gcp-releasetool +babel==2.12.1 \ + --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \ + --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455 + # via sphinx +bleach==6.0.0 \ + --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \ + --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4 + # via readme-renderer +cachetools==5.3.1 \ + --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \ + --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b + # via google-auth +certifi==2023.5.7 \ + --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ + --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 + # via requests +cffi==1.15.1 \ + --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ + --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ + --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ + --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ + --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ + --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ + --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ + --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ + --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ + --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ + --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ + --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ + --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ + --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ + --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ + --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ + --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ + --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ + --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ + --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ + --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ + --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ + --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ + --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ + --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ + --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ + --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ + --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ + --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ + --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ + --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ + --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ + --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ + --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ + --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ + --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ + --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ + --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ + --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ + --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ + --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ + --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ + --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ + --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ + --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ + --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ + --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ + --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ + --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ + --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ + --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ + --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ + --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ + --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ + --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ + --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ + --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ + --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ + --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ + --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ + --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ + --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ + --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ + --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 + # via cryptography +charset-normalizer==2.1.1 \ + --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ + --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f + # via + # -r requirements.in + # requests +click==8.0.4 \ + --hash=sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1 \ + --hash=sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb + # via + # -r requirements.in + # gcp-docuploader + # gcp-releasetool +colorlog==6.7.0 \ + --hash=sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662 \ + --hash=sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5 + # via + # gcp-docuploader + # nox +commonmark==0.9.1 \ + --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ + --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 + # via recommonmark +cryptography==41.0.1 \ + --hash=sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db \ + --hash=sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a \ + --hash=sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039 \ + --hash=sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c \ + --hash=sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3 \ + --hash=sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485 \ + --hash=sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c \ + --hash=sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca \ + --hash=sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5 \ + --hash=sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5 \ + --hash=sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3 \ + --hash=sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb \ + --hash=sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43 \ + --hash=sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31 \ + --hash=sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc \ + --hash=sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b \ + --hash=sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006 \ + --hash=sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a \ + --hash=sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699 + # via + # gcp-releasetool + # secretstorage +distlib==0.3.6 \ + --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ + --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e + # via virtualenv +docutils==0.17.1 \ + --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ + --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 + # via + # readme-renderer + # recommonmark + # sphinx +filelock==3.12.2 \ + --hash=sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81 \ + --hash=sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec + # via virtualenv +gcp-docuploader==0.6.5 \ + --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ + --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea + # via -r requirements.in +gcp-releasetool==1.14.0 \ + --hash=sha256:a39708206838477bf42804a9dd7b549131ff3fc08c6abb0dd6939e274bd6dfd4 \ + --hash=sha256:aa163ec8322045c9d46e03f8f7500974568bd5d8d322b6a2d492068a5bdf57bf + # via -r requirements.in +google-api-core==2.11.1 \ + --hash=sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a \ + --hash=sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a + # via + # google-cloud-core + # google-cloud-storage +google-auth==2.20.0 \ + --hash=sha256:030af34138909ccde0fbce611afc178f1d65d32fbff281f25738b1fe1c6f3eaa \ + --hash=sha256:23b7b0950fcda519bfb6692bf0d5289d2ea49fc143717cc7188458ec620e63fa + # via + # gcp-releasetool + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.3.2 \ + --hash=sha256:8417acf6466be2fa85123441696c4badda48db314c607cf1e5d543fa8bdc22fe \ + --hash=sha256:b9529ee7047fd8d4bf4a2182de619154240df17fbe60ead399078c1ae152af9a + # via google-cloud-storage +google-cloud-storage==2.9.0 \ + --hash=sha256:83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94 \ + --hash=sha256:9b6ae7b509fc294bdacb84d0f3ea8e20e2c54a8b4bbe39c5707635fec214eff3 + # via gcp-docuploader +google-crc32c==1.5.0 \ + --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ + --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \ + --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \ + --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \ + --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \ + --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \ + --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \ + --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \ + --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \ + --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \ + --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \ + --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \ + --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \ + --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \ + --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \ + --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \ + --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \ + --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \ + --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \ + --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \ + --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \ + --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \ + --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \ + --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \ + --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \ + --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \ + --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \ + --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \ + --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \ + --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \ + --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \ + --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \ + --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \ + --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \ + --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \ + --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \ + --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \ + --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \ + --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \ + --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \ + --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \ + --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \ + --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \ + --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \ + --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \ + --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \ + --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \ + --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \ + --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \ + --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \ + --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \ + --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \ + --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \ + --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \ + --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \ + --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \ + --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \ + --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \ + --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \ + --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \ + --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \ + --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \ + --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \ + --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \ + --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \ + --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \ + --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \ + --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4 + # via google-resumable-media +google-resumable-media==2.5.0 \ + --hash=sha256:218931e8e2b2a73a58eb354a288e03a0fd5fb1c4583261ac6e4c078666468c93 \ + --hash=sha256:da1bd943e2e114a56d85d6848497ebf9be6a14d3db23e9fc57581e7c3e8170ec + # via google-cloud-storage +googleapis-common-protos==1.59.1 \ + --hash=sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e \ + --hash=sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a + # via google-api-core +idna==3.4 \ + --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ + --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 + # via requests +imagesize==1.4.1 \ + --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ + --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a + # via sphinx +importlib-metadata==6.7.0 \ + --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ + --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 + # via + # -r requirements.in + # keyring + # twine +jaraco-classes==3.2.3 \ + --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \ + --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a + # via keyring +jeepney==0.8.0 \ + --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ + --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755 + # via + # keyring + # secretstorage +jinja2==3.1.2 \ + --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ + --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 + # via + # gcp-releasetool + # sphinx +keyring==24.0.0 \ + --hash=sha256:4e87665a19c514c7edada8b15015cf89bd99b8d7edabc5c43cca77166fa8dfad \ + --hash=sha256:770f609eed2a16c65a6349f3ba1545d00c73f9fed4254c13766c674fe6d0d22b + # via + # gcp-releasetool + # twine +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via rich +markupsafe==2.1.3 \ + --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ + --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ + --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ + --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ + --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ + --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ + --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ + --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ + --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ + --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ + --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ + --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ + --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ + --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ + --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ + --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ + --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ + --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ + --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ + --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ + --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ + --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ + --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ + --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ + --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ + --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ + --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ + --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ + --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ + --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ + --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ + --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ + --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ + --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ + --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ + --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ + --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ + --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ + --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ + --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ + --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ + --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ + --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ + --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ + --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ + --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ + --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ + --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ + --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ + --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 + # via jinja2 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +more-itertools==9.1.0 \ + --hash=sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d \ + --hash=sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3 + # via jaraco-classes +nox==2023.4.22 \ + --hash=sha256:0b1adc619c58ab4fa57d6ab2e7823fe47a32e70202f287d78474adcc7bda1891 \ + --hash=sha256:46c0560b0dc609d7d967dc99e22cb463d3c4caf54a5fda735d6c11b5177e3a9f + # via -r requirements.in +packaging==23.1 \ + --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ + --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f + # via + # gcp-releasetool + # nox + # sphinx +pkginfo==1.9.6 \ + --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \ + --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046 + # via twine +platformdirs==3.7.0 \ + --hash=sha256:87fbf6473e87c078d536980ba970a472422e94f17b752cfad17024c18876d481 \ + --hash=sha256:cfd065ba43133ff103ab3bd10aecb095c2a0035fcd1f07217c9376900d94ba07 + # via virtualenv +protobuf==3.20.3 \ + --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ + --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \ + --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \ + --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \ + --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \ + --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \ + --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \ + --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \ + --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \ + --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \ + --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \ + --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \ + --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \ + --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \ + --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \ + --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \ + --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \ + --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \ + --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \ + --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \ + --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \ + --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee + # via + # gcp-docuploader + # gcp-releasetool + # google-api-core +pyasn1==0.5.0 \ + --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \ + --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.3.0 \ + --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ + --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d + # via google-auth +pycparser==2.21 \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 + # via cffi +pygments==2.15.1 \ + --hash=sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c \ + --hash=sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1 + # via + # readme-renderer + # rich + # sphinx +pyjwt==2.7.0 \ + --hash=sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1 \ + --hash=sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074 + # via gcp-releasetool +pyperclip==1.8.2 \ + --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 + # via gcp-releasetool +python-dateutil==2.8.2 \ + --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ + --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 + # via gcp-releasetool +readme-renderer==40.0 \ + --hash=sha256:9f77b519d96d03d7d7dce44977ba543090a14397c4f60de5b6eb5b8048110aa4 \ + --hash=sha256:e18feb2a1e7706f2865b81ebb460056d93fb29d69daa10b223c00faa7bd9a00a + # via twine +recommonmark==0.7.1 \ + --hash=sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f \ + --hash=sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67 + # via -r requirements.in +requests==2.31.0 \ + --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ + --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 + # via + # gcp-releasetool + # google-api-core + # google-cloud-storage + # requests-toolbelt + # sphinx + # twine +requests-toolbelt==1.0.0 \ + --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ + --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 + # via twine +rfc3986==2.0.0 \ + --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ + --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c + # via twine +rich==13.4.2 \ + --hash=sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec \ + --hash=sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898 + # via twine +rsa==4.9 \ + --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ + --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 + # via google-auth +secretstorage==3.3.3 \ + --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ + --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 + # via keyring +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via + # bleach + # gcp-docuploader + # google-auth + # python-dateutil +snowballstemmer==2.2.0 \ + --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ + --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a + # via sphinx +sphinx==4.5.0 \ + --hash=sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6 \ + --hash=sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226 + # via + # -r requirements.in + # recommonmark +sphinxcontrib-applehelp==1.0.4 \ + --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \ + --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e + # via sphinx +sphinxcontrib-devhelp==1.0.2 \ + --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ + --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 \ + --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \ + --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903 + # via sphinx +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-qthelp==1.0.3 \ + --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ + --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 \ + --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ + --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 + # via sphinx +twine==4.0.2 \ + --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \ + --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8 + # via -r requirements.in +typing-extensions==4.6.3 \ + --hash=sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26 \ + --hash=sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5 + # via -r requirements.in +urllib3==1.26.16 \ + --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \ + --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14 + # via + # google-auth + # requests + # twine +virtualenv==20.23.1 \ + --hash=sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419 \ + --hash=sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1 + # via nox +webencodings==0.5.1 \ + --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ + --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 + # via bleach +wheel==0.40.0 \ + --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \ + --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247 + # via -r requirements.in +zipp==3.15.0 \ + --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ + --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +setuptools==68.0.0 \ + --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \ + --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235 + # via -r requirements.in diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..07d7e6b7445 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +# Copyright 2021 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. +# +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black +- repo: https://github.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.1.1 + hooks: + - id: mypy + additional_dependencies: [types-requests] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..39353a3f1ba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +[PyPI History][1] + +[1]: https://pypi.org/project/bigframes/#history + +## 0.1.0 (TBD) + +### Features + +* Add `bigframes.pandas` package with an API compatible with + [pandas](https://pandas.pydata.org/). Supported data sources include: + BigQuery SQL queries, BigQuery tables, CSV (local and GCS), Parquet (local + and GCS), and more. +* Add `bigframes.ml` package with an API inspired by + [scikit-learn](https://scikit-learn.org/stable/). Train machine learning + models and run batch predicition, powered by [BigQuery + ML](https://cloud.google.com/bigquery/docs/bqml-introduction). + +## [0.0.0](https://pypi.org/project/bigframes/0.0.0/) (2023-02-22) + +* Empty package to reserve package name. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..51d6abc4d3d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,94 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + + +Reports should be directed to *googleapis-stewards@google.com*, the +Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..b16bd944285 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# How to contribute + +We'd love to accept your patches and contributions to this project. + +## Before you begin + +### Sign our Contributor License Agreement + +Contributions to this project must be accompanied by a +[Contributor License Agreement](https://cla.developers.google.com/about) (CLA). +You (or your employer) retain the copyright to your contribution; this simply +gives us permission to use and redistribute your contributions as part of the +project. + +If you or your current employer have already signed the Google CLA (even if it +was for a different project), you probably don't need to do it again. + +Visit to see your current agreements or to +sign a new one. + +### Review our community guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). + +## Contribution process + +### Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/LICENSE b/LICENSE index 7a4a3ea2424..d6456956733 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,4 @@ 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. \ No newline at end of file + limitations under the License. diff --git a/OWNERS b/OWNERS new file mode 100644 index 00000000000..f2a0b4383d1 --- /dev/null +++ b/OWNERS @@ -0,0 +1,10 @@ +bmil@google.com +chelsealin@google.com +garrettwu@google.com +henryjsolberg@google.com +hormati@google.com +huanc@google.com +jiaxun@google.com +shobs@google.com +swast@google.com +tbergeron@google.com diff --git a/README.md b/README.md deleted file mode 100644 index 3b3fd5761d9..00000000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# BigFrames - -BigFrames supports scalable DataFrame APIs on top of BigQuery. diff --git a/README.rst b/README.rst new file mode 100644 index 00000000000..5f473e11890 --- /dev/null +++ b/README.rst @@ -0,0 +1,8 @@ +BigQuery DataFrames +=================== + +BigQuery DataFrames provides a Pythonic DataFrame and machine learning (ML) API +powered by the BigQuery engine. + +* ``bigframes.pandas`` provides a pandas-like API for analytics. +* ``bigframes.ml`` provides a Scikit-Learn-like API for ML. diff --git a/bigframes/__init__.py b/bigframes/__init__.py new file mode 100644 index 00000000000..2ee745bc52e --- /dev/null +++ b/bigframes/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BigQuery DataFrames provides a DataFrame API scaled by the BigQuery engine.""" + +from bigframes._config import options +from bigframes._config.bigquery_options import BigQueryOptions +from bigframes.session import connect, Session +from bigframes.version import __version__ + +__all__ = [ + "BigQueryOptions", + "connect", + "options", + "Session", + "__version__", +] diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py new file mode 100644 index 00000000000..54c529fb0ca --- /dev/null +++ b/bigframes/_config/__init__.py @@ -0,0 +1,49 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Configuration for BigQuery DataFrames. Do not depend on other parts of BigQuery +DataFrames from this package. +""" + +import bigframes._config.bigquery_options as bigquery_options +import bigframes._config.display_options as display_options + + +class Options: + """Global options affecting BigQuery DataFrames behavior.""" + + def __init__(self): + self._bigquery_options = bigquery_options.BigQueryOptions() + self._display_options = display_options.DisplayOptions() + + @property + def bigquery(self) -> bigquery_options.BigQueryOptions: + """Options to use with the BigQuery engine.""" + return self._bigquery_options + + @property + def display(self) -> display_options.DisplayOptions: + """Options controlling object representation.""" + return self._display_options + + +options = Options() +"""Global options for default session.""" + + +__all__ = ( + "Options", + "options", +) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py new file mode 100644 index 00000000000..5eb5ba7bc85 --- /dev/null +++ b/bigframes/_config/bigquery_options.py @@ -0,0 +1,114 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Options for BigQuery DataFrames.""" + +from __future__ import annotations + +from typing import Optional + +import google.api_core.exceptions +import google.auth.credentials + +SESSION_STARTED_MESSAGE = "Cannot change '{attribute}' once a session has started." + + +class BigQueryOptions: + """Encapsulates configuration for working with an Session.""" + + def __init__( + self, + credentials: Optional[google.auth.credentials.Credentials] = None, + project: Optional[str] = None, + location: Optional[str] = None, + remote_udf_connection: Optional[str] = None, + use_regional_endpoints: bool = False, + ): + self._credentials = credentials + self._project = project + self._location = location + self._remote_udf_connection = remote_udf_connection + self._use_regional_endpoints = use_regional_endpoints + self._session_started = False + + @property + def credentials(self) -> Optional[google.auth.credentials.Credentials]: + """The OAuth2 Credentials to use for this client.""" + return self._credentials + + @credentials.setter + def credentials(self, value: Optional[google.auth.credentials.Credentials]): + if self._session_started and self._credentials is not value: + raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="credentials")) + self._credentials = value + + @property + def location(self) -> Optional[str]: + """Default location for jobs / datasets / tables. + + See: https://cloud.google.com/bigquery/docs/locations + """ + return self._location + + @location.setter + def location(self, value: Optional[str]): + if self._session_started and self._location != value: + raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="location")) + self._location = value + + @property + def project(self) -> Optional[str]: + """Google Cloud project ID to use for billing and default data project.""" + return self._project + + @project.setter + def project(self, value: Optional[str]): + if self._session_started and self._project != value: + raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="project")) + self._project = value + + @property + def remote_udf_connection(self) -> Optional[str]: + """Name of the BigQuery connection for the purpose of remote UDFs. + + It should be either pre created in `location`, or the user should have + privilege to create one. + """ + return self._remote_udf_connection + + @remote_udf_connection.setter + def remote_udf_connection(self, value: Optional[str]): + if self._session_started and self._remote_udf_connection != value: + raise ValueError( + SESSION_STARTED_MESSAGE.format(attribute="remote_udf_connection") + ) + self._remote_udf_connection = value + + @property + def use_regional_endpoints(self) -> bool: + """In preview. Flag to connect to regional API endpoints. + + Requires ``location`` to also be set. For example, set + ``location='asia-northeast1'`` and ``use_regional_endpoints=True`` to + connect to asia-northeast1-bigquery.googleapis.com. + """ + return self._use_regional_endpoints + + @use_regional_endpoints.setter + def use_regional_endpoints(self, value: bool): + if self._session_started and self._use_regional_endpoints != value: + raise ValueError( + SESSION_STARTED_MESSAGE.format(attribute="use_regional_endpoints") + ) + self._use_regional_endpoints = value diff --git a/bigframes/_config/display_options.py b/bigframes/_config/display_options.py new file mode 100644 index 00000000000..43faad0e785 --- /dev/null +++ b/bigframes/_config/display_options.py @@ -0,0 +1,55 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Options for displaying objects.""" + +import contextlib +import dataclasses +from typing import Optional + +import pandas as pd + +import third_party.bigframes_vendored.pandas.core.config_init as vendored_pandas_config + + +@dataclasses.dataclass +class DisplayOptions: + __doc__ = vendored_pandas_config.display_options_doc + + max_columns: int = 20 + max_rows: int = 25 + progress_bar: Optional[str] = "auto" + + +@contextlib.contextmanager +def pandas_repr(display_options: DisplayOptions): + """Use this when visualizing with pandas. + + This context manager makes sure we reset the pandas options when we're done + so that we don't override pandas behavior. + """ + original_max_cols = pd.options.display.max_columns + original_max_rows = pd.options.display.max_rows + original_show_dimensions = pd.options.display.show_dimensions + + pd.options.display.max_columns = display_options.max_columns + pd.options.display.max_rows = display_options.max_rows + pd.options.display.show_dimensions = True # type: ignore + + try: + yield + finally: + pd.options.display.max_columns = original_max_cols + pd.options.display.max_rows = original_max_rows + pd.options.display.show_dimensions = original_show_dimensions diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py new file mode 100644 index 00000000000..70565fddf56 --- /dev/null +++ b/bigframes/core/__init__.py @@ -0,0 +1,1005 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from dataclasses import dataclass +import functools +import math +import typing +from typing import Collection, Dict, Iterable, Literal, Optional, Sequence, Tuple + +from google.cloud import bigquery +import ibis +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types +import pandas + +import bigframes.core.guid +from bigframes.core.ordering import ( + ExpressionOrdering, + OrderingColumnReference, + stringify_order_id, +) +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops + +if typing.TYPE_CHECKING: + from bigframes.session import Session + +ORDER_ID_COLUMN = "bigframes_ordering_id" +PREDICATE_COLUMN = "bigframes_predicate" + + +@dataclass(frozen=True) +class WindowSpec: + """ + Specifies a window over which aggregate and analytic function may be applied. + grouping_keys: set of column ids to group on + preceding: Number of preceding rows in the window + following: Number of preceding rows in the window + ordering: List of columns ids and ordering direction to override base ordering + """ + + grouping_keys: typing.Sequence[str] = tuple() + ordering: typing.Sequence[OrderingColumnReference] = tuple() + preceding: typing.Optional[int] = None + following: typing.Optional[int] = None + min_periods: int = 0 + + +# TODO(swast): We might want to move this to it's own sub-module. +class ArrayValue: + """Immutable BigQuery DataFrames expression tree. + + Note: Usage of this class is considered to be private and subject to change + at any time. + + This class is a wrapper around Ibis expressions. Its purpose is to defer + Ibis projection operations to keep generated SQL small and correct when + mixing and matching columns from different versions of a DataFrame. + + Args: + session: + A BigQuery DataFrames session to allow more flexibility in running + queries. + table: An Ibis table expression. + columns: Ibis value expressions that can be projected as columns. + hidden_ordering_columns: Ibis value expressions to store ordering. + ordering: An ordering property of the data frame. + predicates: A list of filters on the data frame. + """ + + def __init__( + self, + session: Session, + table: ibis_types.Table, + columns: Optional[Sequence[ibis_types.Value]] = None, + hidden_ordering_columns: Optional[Sequence[ibis_types.Value]] = None, + ordering: Optional[ExpressionOrdering] = None, + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + ): + self._session = session + self._table = table + self._predicates = tuple(predicates) if predicates is not None else () + # TODO: Validate ordering + self._ordering = ordering or ExpressionOrdering() + # Allow creating a DataFrame directly from an Ibis table expression. + if columns is None: + self._columns = tuple( + table[key] + for key in table.columns + if ordering is None or key != ordering.ordering_id + ) + else: + # TODO(swast): Validate that each column references the same table (or + # no table for literal values). + self._columns = tuple(columns) + + # Meta columns store ordering, or other data that doesn't correspond to dataframe columns + self._hidden_ordering_columns = ( + tuple(hidden_ordering_columns) + if hidden_ordering_columns is not None + else () + ) + + # To allow for more efficient lookup by column name, create a + # dictionary mapping names to column values. + self._column_names = {column.get_name(): column for column in self._columns} + self._hidden_ordering_column_names = { + column.get_name(): column for column in self._hidden_ordering_columns + } + + @classmethod + def mem_expr_from_pandas( + cls, + pd_df: pandas.DataFrame, + session: Optional[Session], + ) -> ArrayValue: + """ + Builds an in-memory only (SQL only) expr from a pandas dataframe. + + Caution: If session is None, only a subset of expr functionality will be available (null Session is usually not supported). + """ + # must set non-null column labels. these are not the user-facing labels + pd_df = pd_df.set_axis( + [column or bigframes.core.guid.generate_guid() for column in pd_df.columns], + axis="columns", + ) + pd_df = pd_df.assign(**{ORDER_ID_COLUMN: range(len(pd_df))}) + # ibis memtable cannot handle NA, must convert to None + pd_df = pd_df.astype("object") # type: ignore + pd_df = pd_df.where(pandas.notnull(pd_df), None) + keys_memtable = ibis.memtable(pd_df) + return cls( + session, # type: ignore # Session cannot normally be none, see "caution" above + keys_memtable, + ordering=ExpressionOrdering( + ordering_id_column=OrderingColumnReference(ORDER_ID_COLUMN) + ), + hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), + ) + + @property + def table(self) -> ibis_types.Table: + return self._table + + @property + def predicates(self) -> typing.Tuple[ibis_types.BooleanValue, ...]: + return self._predicates + + @property + def reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: + """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" + return ( + _reduce_predicate_list(self._predicates).name(PREDICATE_COLUMN) + if self._predicates + else None + ) + + @property + def columns(self) -> typing.Tuple[ibis_types.Value, ...]: + return self._columns + + @property + def column_names(self) -> Dict[str, ibis_types.Value]: + return self._column_names + + @property + def hidden_ordering_columns(self) -> typing.Tuple[ibis_types.Value, ...]: + return self._hidden_ordering_columns + + @property + def ordering(self) -> Sequence[ibis_types.Value]: + """Returns a sequence of ibis values which can be directly used to order a table expression. Has direction modifiers applied.""" + if not self._ordering: + return [] + else: + # TODO(swast): When we assign literals / scalars, we might not + # have a true Column. Do we need to check this before trying to + # sort by such a column? + return _convert_ordering_to_table_values( + {**self._column_names, **self._hidden_ordering_column_names}, + self._ordering.all_ordering_columns, + ) + + def builder(self) -> ArrayValueBuilder: + """Creates a mutable builder for expressions.""" + # Since ArrayValue is intended to be immutable (immutability offers + # potential opportunities for caching, though we might need to introduce + # more node types for that to be useful), we create a builder class. + return ArrayValueBuilder( + self._session, + self._table, + self._columns, + self._hidden_ordering_columns, + ordering=self._ordering, + predicates=self._predicates, + ) + + def insert_column(self, index: int, column: ibis_types.Value) -> ArrayValue: + expr = self.builder() + expr.columns.insert(index, column) + return expr.build() + + def drop_columns(self, columns: Iterable[str]) -> ArrayValue: + # Must generate offsets if we are dropping a column that ordering depends on + expr = self + for ordering_column in set(columns).intersection( + [col.column_id for col in self._ordering.ordering_value_columns] + ): + expr = self._hide_column(ordering_column) + + expr_builder = expr.builder() + remain_cols = [ + column for column in expr.columns if column.get_name() not in columns + ] + expr_builder.columns = remain_cols + return expr_builder.build() + + def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: + ibis_type = typing.cast(bigframes.dtypes.IbisDtype, self.get_column(key).type()) + return typing.cast( + bigframes.dtypes.Dtype, + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), + ) + + def get_column(self, key: str) -> ibis_types.Value: + """Gets the Ibis expression for a given column.""" + if key not in self._column_names.keys(): + raise ValueError( + "Column name {} not in set of values: {}".format( + key, self._column_names.keys() + ) + ) + return typing.cast(ibis_types.Value, self._column_names[key]) + + def get_any_column(self, key: str) -> ibis_types.Value: + """Gets the Ibis expression for a given column. Will also get hidden columns.""" + all_columns = {**self._column_names, **self._hidden_ordering_column_names} + if key not in all_columns.keys(): + raise ValueError( + "Column name {} not in set of values: {}".format( + key, all_columns.keys() + ) + ) + return typing.cast(ibis_types.Value, all_columns[key]) + + def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: + """Gets the Ibis expression for a given hidden column.""" + if key not in self._hidden_ordering_column_names.keys(): + raise ValueError( + "Column name {} not in set of values: {}".format( + key, self._hidden_ordering_column_names.keys() + ) + ) + return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) + + def apply_limit(self, max_results: int) -> ArrayValue: + table = self.to_ibis_expr( + ordering_mode="order_by", + expose_hidden_cols=True, + ).limit(max_results) + columns = [table[column_name] for column_name in self._column_names] + hidden_ordering_columns = [ + table[column_name] for column_name in self._hidden_ordering_column_names + ] + return ArrayValue( + self._session, + table, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=self._ordering, + ) + + def filter(self, predicate: ibis_types.BooleanValue) -> ArrayValue: + """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + expr = self.builder() + if expr.ordering: + expr.ordering = expr.ordering.with_is_sequential(False) + expr.predicates = [*self._predicates, predicate] + return expr.build() + + def order_by( + self, by: Sequence[OrderingColumnReference], stable: bool = False + ) -> ArrayValue: + expr_builder = self.builder() + expr_builder.ordering = self._ordering.with_ordering_columns(by, stable=stable) + return expr_builder.build() + + def reversed(self) -> ArrayValue: + expr_builder = self.builder() + expr_builder.ordering = self._ordering.with_reverse() + return expr_builder.build() + + @property + def offsets(self): + if not self._ordering.is_sequential: + raise ValueError( + "Expression does not have offsets. Generate them first using project_offsets." + ) + return self._get_hidden_ordering_column(self._ordering.ordering_id) + + def project_offsets(self) -> ArrayValue: + """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" + if self._ordering.is_sequential: + return self + # TODO(tbergeron): Enforce total ordering + table = self.to_ibis_expr( + ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN + ) + columns = [table[column_name] for column_name in self._column_names] + ordering = ExpressionOrdering( + ordering_id_column=OrderingColumnReference(ORDER_ID_COLUMN), + is_sequential=True, + ) + return ArrayValue( + self._session, + table, + columns=columns, + hidden_ordering_columns=[table[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def _hide_column(self, column_id) -> ArrayValue: + """Pushes columns to hidden columns list. Used to hide ordering columns that have been dropped or destructively mutated.""" + expr_builder = self.builder() + # Need to rename column as caller might be creating a new row with the same name but different values. + # Can avoid this if don't allow callers to determine ids and instead generate unique ones in this class. + new_name = bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") + expr_builder.hidden_ordering_columns = [ + *self._hidden_ordering_columns, + self.get_column(column_id).name(new_name), + ] + + ordering_columns = [ + col if col.column_id != column_id else col.with_name(new_name) + for col in self._ordering.ordering_value_columns + ] + + expr_builder.ordering = self._ordering.with_ordering_columns(ordering_columns) + return expr_builder.build() + + def promote_offsets(self) -> typing.Tuple[ArrayValue, str]: + """ + Convenience function to promote copy of column offsets to a value column. Can be used to reset index. + """ + # Special case: offsets already exist + ordering = self._ordering + + if (not ordering.is_sequential) or (not ordering.ordering_id): + return self.project_offsets().promote_offsets() + col_id = bigframes.core.guid.generate_guid() + expr_builder = self.builder() + expr_builder.columns = [ + self._get_hidden_ordering_column(ordering.ordering_id).name(col_id), + *self.columns, + ] + return expr_builder.build(), col_id + + def select_columns(self, column_ids: typing.Sequence[str]): + return self.projection([self.get_column(col_id) for col_id in column_ids]) + + def projection(self, columns: Iterable[ibis_types.Value]) -> ArrayValue: + """Creates a new expression based on this expression with new columns.""" + # TODO(swast): We might want to do validation here that columns derive + # from the same table expression instead of (in addition to?) at + # construction time. + + expr = self + for ordering_column in set(self.column_names.keys()).intersection( + [col_ref.column_id for col_ref in self._ordering.ordering_value_columns] + ): + # Need to hide ordering columns that are being dropped. Alternatively, could project offsets + expr = expr._hide_column(ordering_column) + builder = expr.builder() + builder.columns = list(columns) + new_expr = builder.build() + return new_expr + + def shape(self) -> typing.Tuple[int, int]: + """Returns dimensions as (length, width) tuple.""" + width = len(self.columns) + count_expr = self.to_ibis_expr(ordering_mode="unordered").count() + sql = self._session.ibis_client.compile(count_expr) + row_iterator, _ = self._session._start_query( + sql=sql, + max_results=1, + ) + length = next(row_iterator)[0] + return (length, width) + + def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: + """Append together multiple ArrayValue objects.""" + if len(other) == 0: + return self + tables = [] + prefix_base = 10 + prefix_size = math.ceil(math.log(len(other) + 1, prefix_base)) + # Must normalize all ids to the same encoding size + max_encoding_size = max( + self._ordering.ordering_encoding_size, + *[expression._ordering.ordering_encoding_size for expression in other], + ) + for i, expr in enumerate([self, *other]): + ordering_prefix = str(i).zfill(prefix_size) + table = expr.to_ibis_expr( + ordering_mode="ordered_col", order_col_name=ORDER_ID_COLUMN + ) + # Rename the value columns based on horizontal offset before applying union. + table = table.select( + [ + table[col].name(f"column_{i}") + if col != ORDER_ID_COLUMN + else ( + ordering_prefix + + stringify_order_id(table[ORDER_ID_COLUMN], max_encoding_size) + ).name(ORDER_ID_COLUMN) + for i, col in enumerate(table.columns) + ] + ) + tables.append(table) + combined_table = ibis.union(*tables) + ordering = ExpressionOrdering( + ordering_id_column=OrderingColumnReference(ORDER_ID_COLUMN), + ordering_encoding_size=prefix_size + max_encoding_size, + ) + return ArrayValue( + self._session, + combined_table, + columns=[ + combined_table[col] + for col in combined_table.columns + if col != ORDER_ID_COLUMN + ], + hidden_ordering_columns=[combined_table[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def project_unary_op( + self, column_name: str, op: ops.UnaryOp, output_name=None + ) -> ArrayValue: + """Creates a new expression based on this expression with unary operation applied to one column.""" + value = op._as_ibis(self.get_column(column_name)).name( + output_name or column_name + ) + return self._set_or_replace_by_id(output_name or column_name, value) + + def project_binary_op( + self, + left_column_id: str, + right_column_id: str, + op: ops.BinaryOp, + output_column_id: str, + ) -> ArrayValue: + """Creates a new expression based on this expression with binary operation applied to two columns.""" + value = op( + self.get_column(left_column_id), self.get_column(right_column_id) + ).name(output_column_id) + return self._set_or_replace_by_id(output_column_id, value) + + def project_ternary_op( + self, + col_id_1: str, + col_id_2: str, + col_id_3: str, + op: ops.TernaryOp, + output_column_id: str, + ) -> ArrayValue: + """Creates a new expression based on this expression with ternary operation applied to three columns.""" + value = op( + self.get_column(col_id_1), + self.get_column(col_id_2), + self.get_column(col_id_3), + ).name(output_column_id) + return self._set_or_replace_by_id(output_column_id, value) + + def aggregate( + self, + aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp, str]], + by_column_ids: typing.Sequence[str] = (), + dropna: bool = True, + ) -> ArrayValue: + """ + Apply aggregations to the expression. + Arguments: + by_column_id: column id of the aggregation key, this is preserved through the transform + aggregations: input_column_id, operation, output_column_id tuples + dropna: whether null keys should be dropped + """ + table = self.to_ibis_expr() + stats = { + col_out: agg_op._as_ibis(table[col_in]) + for col_in, agg_op, col_out in aggregations + } + if by_column_ids: + result = table.group_by(by_column_ids).aggregate(**stats) + # Must have deterministic ordering, so order by the unique "by" column + ordering = ExpressionOrdering( + [ + OrderingColumnReference(column_id=column_id) + for column_id in by_column_ids + ] + ) + expr = ArrayValue(self._session, result, ordering=ordering) + if dropna: + for column_id in by_column_ids: + expr = expr.filter( + ops.notnull_op._as_ibis(expr.get_column(column_id)) + ) + # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation + return expr.project_offsets() + else: + aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} + result = table.aggregate(**aggregates) + # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. + ordering = ExpressionOrdering( + ordering_id_column=OrderingColumnReference(column_id=ORDER_ID_COLUMN), + is_sequential=True, + ) + return ArrayValue( + self._session, + result, + columns=[result[col_id] for col_id in [*stats.keys()]], + hidden_ordering_columns=[result[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def project_window_op( + self, + column_name: str, + op: agg_ops.WindowOp, + window_spec: WindowSpec, + output_name=None, + *, + skip_null_groups=False, + skip_reproject_unsafe: bool = False, + ) -> ArrayValue: + """ + Creates a new expression based on this expression with unary operation applied to one column. + column_name: the id of the input column present in the expression + op: the windowable operator to apply to the input column + window_spec: a specification of the window over which to apply the operator + output_name: the id to assign to the output of the operator, by default will replace input col if distinct output id not provided + skip_null_groups: will filter out any rows where any of the grouping keys is null + skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection + """ + column = typing.cast(ibis_types.Column, self.get_column(column_name)) + window = self._ibis_window_from_spec(window_spec, allow_ties=op.handles_ties) + + window_op = op._as_ibis(column, window) + + clauses = [] + if op.skips_nulls: + clauses.append((column.isnull(), ibis.NA)) + if skip_null_groups: + for key in window_spec.grouping_keys: + clauses.append((self.get_column(key).isnull(), ibis.NA)) + if window_spec.min_periods: + clauses.append( + ( + agg_ops.count_op._as_ibis(column, window) + < ibis_types.literal(window_spec.min_periods), + ibis.NA, + ) + ) + + if clauses: + case_statement = ibis.case() + for clause in clauses: + case_statement = case_statement.when(clause[0], clause[1]) + case_statement = case_statement.else_(window_op).end() + window_op = case_statement + + result = self._set_or_replace_by_id(output_name or column_name, window_op) + # TODO(tbergeron): Automatically track analytic expression usage and defer reprojection until required for valid query generation. + return result._reproject_to_table() if not skip_reproject_unsafe else result + + def to_ibis_expr( + self, + ordering_mode: Literal[ + "order_by", "ordered_col", "offset_col", "unordered" + ] = "order_by", + order_col_name: Optional[str] = ORDER_ID_COLUMN, + expose_hidden_cols: bool = False, + ): + """ + Creates an Ibis table expression representing the DataFrame. + + ArrayValue objects are sorted, so the following options are available + to reflect this in the ibis expression. + + * "order_by" (Default): The output table will not have an ordering + column, however there will be an order_by clause applied to the ouput. + * "offset_col": Zero-based offsets are generated as a column, this will + not sort the rows however. + * "ordered_col": An ordered column is provided in output table, without + guarantee that the values are sequential + * "unordered": No ordering information will be provided in output. Only + value columns are projected. + + For offset or ordered column, order_col_name can be used to assign the + output label for the ordering column. If none is specified, the default + column name will be 'bigframes_ordering_id' + + Args: + ordering_mode: + How to construct the Ibis expression from the ArrayValue. See + above for details. + order_col_name: + If the ordering mode outputs a single ordering or offsets + column, use this as the column name. + expose_hidden_cols: + If True, include the hidden ordering columns in the results. + Only compatible with `order_by` and `unordered` + ``ordering_mode``. + Returns: + An ibis expression representing the data help by the ArrayValue object. + """ + assert ordering_mode in ( + "order_by", + "ordered_col", + "offset_col", + "unordered", + ) + if expose_hidden_cols and ordering_mode in ("ordered_col", "offset_col"): + raise ValueError( + f"Cannot expose hidden ordering columns with ordering_mode {ordering_mode}" + ) + + table = self._table + columns = list(self._columns) + hidden_ordering_columns = [ + col.column_id + for col in self._ordering.all_ordering_columns + if col.column_id not in self._column_names.keys() + ] + + if self.reduced_predicate is not None: + columns.append(self.reduced_predicate) + if ordering_mode in ("offset_col", "ordered_col"): + # Generate offsets if current ordering id semantics are not sufficiently strict + if (ordering_mode == "offset_col" and not self._ordering.is_sequential) or ( + ordering_mode == "ordered_col" and not self._ordering.order_id_defined + ): + window = ibis.window(order_by=self.ordering) + if self._predicates: + window = window.group_by(self.reduced_predicate) + columns.append(ibis.row_number().name(order_col_name).over(window)) + elif self._ordering.ordering_id: + columns.append( + self._get_hidden_ordering_column(self._ordering.ordering_id).name( + order_col_name + ) + ) + else: + # Should not be possible. + raise ValueError( + "Expression does not have ordering id and none was generated." + ) + elif ordering_mode == "order_by": + columns.extend( + [ + self._get_hidden_ordering_column(name) + for name in hidden_ordering_columns + ] + ) + + # We already need to add the hidden ordering columns for "order_by" so + # we can order by them. + if expose_hidden_cols and ordering_mode != "order_by": + columns.extend( + [ + self._get_hidden_ordering_column(name) + for name in hidden_ordering_columns + ] + ) + + # Special case for empty tables, since we can't create an empty + # projection. + if not columns: + return ibis.memtable([]) + table = table.select(columns) + # Make sure all dtypes are the "canonical" ones for BigFrames. This is + # important for operations like UNION where the schema must match. + table = bigframes.dtypes.ibis_table_to_canonical_types(table) + + if self.reduced_predicate is not None: + table = table.filter(table[PREDICATE_COLUMN]) + # Drop predicate as it is will be all TRUE after filtering + table = table.drop(PREDICATE_COLUMN) + if ordering_mode == "order_by": + # Some ordering columns are value columns, while other are used purely for ordering. + # We drop the non-value columns after the ordering + table = table.order_by( + _convert_ordering_to_table_values( + {col: table[col] for col in table.columns}, + self._ordering.all_ordering_columns, + ) # type: ignore + ) + # TODO(swast): We should be able to avoid this subquery by ordering + # by columns that don't have to be in the SELECT clause. + if not expose_hidden_cols: + table = table.drop(*hidden_ordering_columns) + + return table + + def start_query( + self, + job_config: Optional[bigquery.job.QueryJobConfig] = None, + max_results: Optional[int] = None, + ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """Execute a query and return metadata about the results.""" + # TODO(swast): Cache the job ID so we can look it up again if they ask + # for the results? We'd need a way to invalidate the cache if DataFrame + # becomes mutable, though. Or move this method to the immutable + # expression class. + # TODO(swast): We might want to move this method to Session and/or + # provide our own minimal metadata class. Tight coupling to the + # BigQuery client library isn't ideal, especially if we want to support + # a LocalSession for unit testing. + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + table = self.to_ibis_expr() + sql = self._session.ibis_client.compile(table) # type:ignore + return self._session._start_query( + sql=sql, + job_config=job_config, + max_results=max_results, + ) + + def _reproject_to_table(self) -> ArrayValue: + """ + Internal operators that projects the internal representation into a + new ibis table expression where each value column is a direct + reference to a column in that table expression. Needed after + some operations such as window operations that cannot be used + recursively in projections. + """ + table = self.to_ibis_expr( + ordering_mode="unordered", + order_col_name=self._ordering.ordering_id, + expose_hidden_cols=True, + ) + columns = [table[column_name] for column_name in self._column_names] + hidden_ordering_columns = [ + table[column_name] for column_name in self._hidden_ordering_column_names + ] + return ArrayValue( + self._session, + table, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=self._ordering, + ) + + def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = False): + group_by: typing.List[ibis_types.Value] = ( + [ + typing.cast(ibis_types.Column, _as_identity(self.get_column(column))) + for column in window_spec.grouping_keys + ] + if window_spec.grouping_keys + else [] + ) + if self.reduced_predicate is not None: + group_by.append(self.reduced_predicate) + if window_spec.ordering: + order_by = _convert_ordering_to_table_values( + {**self._column_names, **self._hidden_ordering_column_names}, + window_spec.ordering, + ) + if not allow_ties: + # Most operator need an unambiguous ordering, so the table's total ordering is appended + order_by = tuple([*order_by, *self.ordering]) + elif (window_spec.following is not None) or (window_spec.preceding is not None): + # If window spec has following or preceding bounds, we need to apply an unambiguous ordering. + order_by = tuple(self.ordering) + else: + # Unbound grouping window. Suitable for aggregations but not for analytic function application. + order_by = None + return ibis.window( + preceding=window_spec.preceding, + following=window_spec.following, + order_by=order_by, + group_by=group_by, + ) + + def transpose_single_row( + self, + labels, + *, + index_col_id: str = "index", + value_col_id: str = "values", + dtype=pandas.Float64Dtype(), + ) -> ArrayValue: + """Pivot a single row into a 3 column expression with index, values and offsets. Only works if all values can be cast to a common type.""" + table = self.to_ibis_expr(ordering_mode="unordered") + sub_expressions = [] + for i, col_id in enumerate(self._column_names.keys()): + sub_expr = table.select( + ibis_types.literal(labels[i]).name(index_col_id), + ops.AsTypeOp(dtype)._as_ibis(table[col_id]).name(value_col_id), + ibis_types.literal(i).name(ORDER_ID_COLUMN), + ) + sub_expressions.append(sub_expr) + rotated_table = ibis.union(*sub_expressions) + return ArrayValue( + session=self._session, + table=rotated_table, + columns=[rotated_table[index_col_id], rotated_table[value_col_id]], + hidden_ordering_columns=[rotated_table[ORDER_ID_COLUMN]], + ordering=ExpressionOrdering( + ordering_id_column=OrderingColumnReference(column_id=ORDER_ID_COLUMN), + ), + ) + + # TODO(b/282041134) Remove deprecate_rename_column once label/id separation in dataframe + def deprecated_rename_column(self, old_id, new_id) -> ArrayValue: + """ + Don't use this, temporary measure until dataframe supports sqlid!=dataframe col id. + In future, caller shouldn't need to control internal column id strings. + """ + if new_id == old_id: + return self + return self._set_or_replace_by_id(new_id, self.get_column(old_id)).drop_columns( + [old_id] + ) + + def assign(self, source_id: str, destination_id: str) -> ArrayValue: + return self._set_or_replace_by_id(destination_id, self.get_column(source_id)) + + def assign_constant( + self, + destination_id: str, + value: typing.Any, + dtype: typing.Optional[bigframes.dtypes.Dtype], + ) -> ArrayValue: + # TODO(b/281587571): Solve scalar constant aggregation problem w/Ibis. + ibis_value = bigframes.dtypes.literal_to_ibis_scalar(value, dtype) + if ibis_value is None: + raise NotImplementedError( + f"Type not supported as scalar value {type(value)}" + ) + expr = self._set_or_replace_by_id(destination_id, ibis_value) + return expr._reproject_to_table() + + def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value): + builder = self.builder() + if id in self.column_names: + builder.columns = [ + val if (col_id != id) else new_value.name(id) + for col_id, val in self.column_names.items() + ] + else: + builder.columns = [*self.columns, new_value.name(id)] + return builder.build() + + def slice( + self, + start: typing.Optional[int] = None, + stop: typing.Optional[int] = None, + step: typing.Optional[int] = None, + ) -> ArrayValue: + if step == 0: + raise ValueError("slice step cannot be zero") + + if not step: + step = 1 + + # Special cases for head() and tail(), where we don't need to project + # offsets. LIMIT clause is much more efficient in BigQuery than a + # filter on row_number(). + if ( + (start is None or start == 0) + and step == 1 + and stop is not None + and stop > 0 + ): + return self.apply_limit(stop) + + if start is not None and start < 0 and step == 1 and stop is None: + return self.reversed().apply_limit(abs(start)).reversed() + + expr_with_offsets = self.project_offsets() + + # start with True and reduce with start, stop, and step conditions + cond_list = [expr_with_offsets.offsets == expr_with_offsets.offsets] + + last_offset = expr_with_offsets.offsets.max() + + # Convert negative indexes to positive indexes + if start and start < 0: + start = last_offset + start + 1 + if stop and stop < 0: + stop = last_offset + stop + 1 + + if start is not None: + if step >= 1: + cond_list.append(expr_with_offsets.offsets >= start) + else: + cond_list.append(expr_with_offsets.offsets <= start) + if stop is not None: + if step >= 1: + cond_list.append(expr_with_offsets.offsets < stop) + else: + cond_list.append(expr_with_offsets.offsets > stop) + if step > 1: + start = start if (start is not None) else 0 + cond_list.append((expr_with_offsets.offsets - start) % step == 0) + if step < 0: + start = start if (start is not None) else last_offset + cond_list.append((start - expr_with_offsets.offsets) % (-step) == 0) + + sliced_expr = expr_with_offsets.filter( + functools.reduce(lambda x, y: x & y, cond_list) + ) + return sliced_expr if step > 0 else sliced_expr.reversed() + + +class ArrayValueBuilder: + """Mutable expression class. + Use ArrayValue.builder() to create from a ArrayValue object. + """ + + def __init__( + self, + session: Session, + table: ibis_types.Table, + columns: Collection[ibis_types.Value] = (), + hidden_ordering_columns: Collection[ibis_types.Value] = (), + ordering: Optional[ExpressionOrdering] = None, + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + ): + self.session = session + self.table = table + self.columns = list(columns) + self.hidden_ordering_columns = list(hidden_ordering_columns) + self.ordering = ordering + self.predicates = list(predicates) if predicates is not None else None + + def build(self) -> ArrayValue: + return ArrayValue( + session=self.session, + table=self.table, + columns=self.columns, + hidden_ordering_columns=self.hidden_ordering_columns, + ordering=self.ordering, + predicates=self.predicates, + ) + + +def _reduce_predicate_list( + predicate_list: typing.Collection[ibis_types.BooleanValue], +) -> ibis_types.BooleanValue: + """Converts a list of predicates BooleanValues into a single BooleanValue.""" + if len(predicate_list) == 0: + raise ValueError("Cannot reduce empty list of predicates") + if len(predicate_list) == 1: + (item,) = predicate_list + return item + return functools.reduce(lambda acc, pred: acc.__and__(pred), predicate_list) + + +def _convert_ordering_to_table_values( + value_lookup: typing.Mapping[str, ibis_types.Value], + ordering_columns: typing.Sequence[OrderingColumnReference], +) -> typing.Sequence[ibis_types.Value]: + column_refs = ordering_columns + ordering_values = [] + for ordering_col in column_refs: + column = typing.cast(ibis_types.Column, value_lookup[ordering_col.column_id]) + ordering_value = ( + ibis.asc(column) + if ordering_col.direction.is_ascending + else ibis.desc(column) + ) + # Bigquery SQL considers NULLS to be "smallest" values, but we need to override in these cases. + if (not ordering_col.na_last) and (not ordering_col.direction.is_ascending): + # Force nulls to be first + is_null_val = typing.cast(ibis_types.Column, column.isnull()) + ordering_values.append(ibis.desc(is_null_val)) + elif (ordering_col.na_last) and (ordering_col.direction.is_ascending): + # Force nulls to be last + is_null_val = typing.cast(ibis_types.Column, column.isnull()) + ordering_values.append(ibis.asc(is_null_val)) + ordering_values.append(ordering_value) + return ordering_values + + +def _as_identity(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(): + return value.cast(ibis_dtypes.str) + return value diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py new file mode 100644 index 00000000000..b13d7bf2d3a --- /dev/null +++ b/bigframes/core/block_transforms.py @@ -0,0 +1,199 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import typing + +import pandas as pd + +import bigframes.core as core +import bigframes.core.blocks as blocks +import bigframes.core.ordering as ordering +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops + + +def indicate_duplicates( + block: blocks.Block, columns: typing.Sequence[str], keep: str = "first" +) -> typing.Tuple[blocks.Block, str]: + """Create a boolean column where True indicates a duplicate value""" + if keep not in ["first", "last", False]: + raise ValueError("keep must be one of 'first', 'last', or False'") + + if keep == "first": + # Count how many copies occur up to current copy of value + # Discard this value if there are copies BEFORE + window_spec = core.WindowSpec( + grouping_keys=tuple(columns), + following=0, + ) + elif keep == "last": + # Count how many copies occur up to current copy of values + # Discard this value if there are copies AFTER + window_spec = core.WindowSpec( + grouping_keys=tuple(columns), + preceding=0, + ) + else: # keep == False + # Count how many copies of the value occur in entire series. + # Discard this value if there are copies ANYWHERE + window_spec = core.WindowSpec(grouping_keys=tuple(columns)) + block, dummy = block.create_constant(1) + block, val_count_col_id = block.apply_window_op( + dummy, + agg_ops.count_op, + window_spec=window_spec, + ) + block, duplicate_indicator = block.apply_unary_op( + val_count_col_id, + ops.partial_right(ops.gt_op, 1), + ) + return ( + block.drop_columns( + ( + dummy, + val_count_col_id, + ) + ), + duplicate_indicator, + ) + + +def drop_duplicates( + block: blocks.Block, columns: typing.Sequence[str], keep: str = "first" +) -> blocks.Block: + block, dupe_indicator_id = indicate_duplicates(block, columns, keep) + block, keep_indicator_id = block.apply_unary_op(dupe_indicator_id, ops.invert_op) + return block.filter(keep_indicator_id).drop_columns( + (dupe_indicator_id, keep_indicator_id) + ) + + +def value_counts( + block: blocks.Block, + columns: typing.Sequence[str], + normalize: bool = False, + sort: bool = True, + ascending: bool = False, + dropna: bool = True, +): + block, dummy = block.create_constant(1) + block, agg_ids = block.aggregate( + by_column_ids=columns, + aggregations=[(dummy, agg_ops.count_op)], + dropna=dropna, + as_index=True, + ) + count_id = agg_ids[0] + if normalize: + unbound_window = core.WindowSpec() + block, total_count_id = block.apply_window_op( + count_id, agg_ops.sum_op, unbound_window + ) + block, count_id = block.apply_binary_op(count_id, total_count_id, ops.div_op) + + if sort: + block = block.order_by( + [ + ordering.OrderingColumnReference( + count_id, + direction=ordering.OrderingDirection.ASC + if ascending + else ordering.OrderingDirection.DESC, + ) + ] + ) + return block.select_column(count_id).with_column_labels(["count"]) + + +def rank( + block: blocks.Block, + method: str = "average", + na_option: str = "keep", + ascending: bool = True, +): + if method not in ["average", "min", "max", "first", "dense"]: + raise ValueError( + "method must be one of 'average', 'min', 'max', 'first', or 'dense'" + ) + if na_option not in ["keep", "top", "bottom"]: + raise ValueError("na_option must be one of 'keep', 'top', or 'bottom'") + + columns = block.value_columns + labels = block.column_labels + # Step 1: Calculate row numbers for each row + # Identify null values to be treated according to na_option param + rownum_col_ids = [] + nullity_col_ids = [] + for col in columns: + block, nullity_col_id = block.apply_unary_op( + col, + ops.isnull_op, + ) + nullity_col_ids.append(nullity_col_id) + window = core.WindowSpec( + # BigQuery has syntax to reorder nulls with "NULLS FIRST/LAST", but that is unavailable through ibis presently, so must order on a separate nullity expression first. + ordering=( + ordering.OrderingColumnReference( + col, + ordering.OrderingDirection.ASC + if ascending + else ordering.OrderingDirection.DESC, + na_last=(na_option in ["bottom", "keep"]), + ), + ), + ) + # Count_op ignores nulls, so if na_option is "top" or "bottom", we instead count the nullity columns, where nulls have been mapped to bools + block, rownum_id = block.apply_window_op( + col if na_option == "keep" else nullity_col_id, + agg_ops.dense_rank_op if method == "dense" else agg_ops.count_op, + window_spec=window, + skip_reproject_unsafe=(col != columns[-1]), + ) + rownum_col_ids.append(rownum_id) + + # Step 2: Apply aggregate to groups of like input values. + # This step is skipped for method=='first' or 'dense' + if method in ["average", "min", "max"]: + agg_op = { + "average": agg_ops.mean_op, + "min": agg_ops.min_op, + "max": agg_ops.max_op, + }[method] + post_agg_rownum_col_ids = [] + for i in range(len(columns)): + block, result_id = block.apply_window_op( + rownum_col_ids[i], + agg_op, + window_spec=core.WindowSpec(grouping_keys=[columns[i]]), + skip_reproject_unsafe=(i < (len(columns) - 1)), + ) + post_agg_rownum_col_ids.append(result_id) + rownum_col_ids = post_agg_rownum_col_ids + + # Step 3: post processing: mask null values and cast to float + if method in ["min", "max", "first", "dense"]: + # Pandas rank always produces Float64, so must cast for aggregation types that produce ints + block = block.multi_apply_unary_op( + rownum_col_ids, ops.AsTypeOp(pd.Float64Dtype()) + ) + if na_option == "keep": + # For na_option "keep", null inputs must produce null outputs + for i in range(len(columns)): + block, null_const = block.create_constant(pd.NA, dtype=pd.Float64Dtype()) + block, rownum_col_ids[i] = block.apply_ternary_op( + null_const, nullity_col_ids[i], rownum_col_ids[i], ops.where_op + ) + + return block.select_columns(rownum_col_ids).with_column_labels(labels) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py new file mode 100644 index 00000000000..fe7261522db --- /dev/null +++ b/bigframes/core/blocks.py @@ -0,0 +1,917 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Block is a 2D data structure that supports data mutability and views. + +These data structures are shared by DataFrame and Series. This allows views to +link in both directions (DataFrame to Series and vice versa) and prevents +circular dependencies. +""" + +from __future__ import annotations + +import functools +import itertools +import typing +from typing import Iterable, List, Optional, Sequence, Tuple + +import geopandas as gpd # type: ignore +import google.cloud.bigquery as bigquery +import ibis.expr.schema as ibis_schema +import ibis.expr.types as ibis_types +import numpy +import pandas as pd +import pyarrow as pa # type: ignore + +import bigframes.core as core +import bigframes.core.guid as guid +import bigframes.core.indexes as indexes +import bigframes.core.ordering as ordering +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops + +# Type constraint for wherever column labels are used +Label = typing.Optional[str] + + +class BlockHolder(typing.Protocol): + """Interface for mutable objects with state represented by a block value object.""" + + def _set_block(self, block: Block): + """Set the underlying block value of the object""" + + def _get_block(self) -> Block: + """Get the underlying block value of the object""" + + +class Block: + """A immutable 2D data structure.""" + + def __init__( + self, + expr: core.ArrayValue, + index_columns: Iterable[str] = (), + column_labels: Optional[Sequence[Label]] = None, + index_labels: Optional[Sequence[Label]] = None, + ): + """Construct a block object, will create default index if no index columns specified.""" + if index_labels and (len(index_labels) != len(list(index_columns))): + raise ValueError( + "'index_columns' and 'index_labels' must have equal length" + ) + if len(list(index_columns)) == 0: + expr, new_index_col_id = expr.promote_offsets() + index_columns = [new_index_col_id] + self._index_columns = tuple(index_columns) + self._index_labels = ( + tuple(index_labels) + if index_labels + else tuple([None for _ in index_columns]) + ) + self._expr = self._normalize_expression(expr, self._index_columns) + # TODO(tbergeron): Force callers to provide column labels + self._column_labels = ( + tuple(column_labels) if column_labels else tuple(self.value_columns) + ) + if len(self.value_columns) != len(self._column_labels): + raise ValueError( + f"'value_columns' (size {len(self.value_columns)}) and 'column_labels' (size {len(self._column_labels)}) must have equal length" + ) + + @property + def index(self) -> indexes.IndexValue: + """Row identities for values in the Block.""" + return indexes.IndexValue(self) + + @functools.cached_property + def shape(self) -> typing.Tuple[int, int]: + """Returns dimensions as (length, width) tuple.""" + impl_length, _ = self._expr.shape() + return (impl_length, len(self.value_columns)) + + @property + def index_columns(self) -> Sequence[str]: + """Column(s) to use as row labels.""" + return self._index_columns + + @property + def index_labels(self) -> Sequence[Label]: + """Name of column(s) to use as row labels.""" + return self._index_labels + + @property + def value_columns(self) -> Sequence[str]: + """All value columns, mutually exclusive with index columns.""" + return [ + column + for column in self._expr.column_names + if column not in self.index_columns + ] + + @property + def column_labels(self) -> List[Label]: + return list(self._column_labels) + + @property + def expr(self) -> core.ArrayValue: + """Expression representing all columns, including index columns.""" + return self._expr + + @property + def dtypes( + self, + ) -> Sequence[bigframes.dtypes.Dtype]: + """Returns the dtypes of the value columns.""" + return [self.expr.get_column_type(col) for col in self.value_columns] + + @property + def index_dtypes( + self, + ) -> Sequence[bigframes.dtypes.Dtype]: + """Returns the dtypes of the index columns.""" + return [self.expr.get_column_type(col) for col in self.index_columns] + + @functools.cached_property + def col_id_to_label(self) -> typing.Mapping[str, Label]: + """Get column label for value columns, or index name for index columns""" + return { + col_id: label + for col_id, label in zip(self.value_columns, self._column_labels) + } + + @functools.cached_property + def label_to_col_id(self) -> typing.Mapping[Label, typing.Sequence[str]]: + """Get column label for value columns, or index name for index columns""" + mapping: typing.Dict[Label, typing.Sequence[str]] = {} + for id, label in self.col_id_to_label.items(): + mapping[label] = (*mapping.get(label, ()), id) + return mapping + + @functools.cached_property + def col_id_to_index_name(self) -> typing.Mapping[str, Label]: + """Get column label for value columns, or index name for index columns""" + return { + col_id: label + for col_id, label in zip(self.index_columns, self._index_labels) + } + + @functools.cached_property + def index_name_to_col_id(self) -> typing.Mapping[Label, typing.Sequence[str]]: + """Get column label for value columns, or index name for index columns""" + mapping: typing.Dict[Label, typing.Sequence[str]] = {} + for id, label in self.col_id_to_index_name.items(): + mapping[label] = (*mapping.get(label, ()), id) + return mapping + + def order_by( + self, + by: typing.Sequence[ordering.OrderingColumnReference], + stable: bool = False, + ) -> Block: + return Block( + self._expr.order_by(by, stable=stable), + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + + def reversed(self) -> Block: + return Block( + self._expr.reversed(), + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + + def reset_index(self, drop: bool = True) -> Block: + """Reset the index of the block, promoting the old index to a value column. + + Arguments: + name: this is the column id for the new value id derived from the old index + + Returns: + A new Block because dropping index columns can break references + from Index classes that point to this block. + """ + block = self + expr, new_index_col_id = self._expr.promote_offsets() + if drop: + # Even though the index might be part of the ordering, keep that + # ordering expression as reset_index shouldn't change the row + # order. + expr = expr.drop_columns(self.index_columns) + block = Block( + expr, + index_columns=[new_index_col_id], + column_labels=self.column_labels, + index_labels=[None], + ) + else: + index_labels = self.index.names + index_labels_rewritten = [] + for level, label in enumerate(index_labels): + if label is None: + if "index" not in self.column_labels: + label = "index" + else: + label = f"level_{level}" + + if label in self.column_labels: + raise ValueError(f"cannot insert {label}, already exists") + index_labels_rewritten.append(label) + + block = Block( + expr, + index_columns=[new_index_col_id], + column_labels=[*index_labels_rewritten, *self.column_labels], + index_labels=[None], + ) + return block + + def set_index( + self, col_ids: typing.Sequence[str], drop: bool = True, append: bool = False + ) -> Block: + """Set the index of the block to + + Arguments: + ids: columns to be converted to index columns + drop: whether to drop the new index columns as value columns + append: whether to discard the existing index or add on to it + + Returns: + Block with new index + """ + expr = self._expr + + new_index_columns = [] + new_index_labels = [] + for col_id in col_ids: + col_copy_id = guid.generate_guid() + expr = expr.assign(col_id, col_copy_id) + new_index_columns.append(col_copy_id) + new_index_labels.append(self.col_id_to_label[col_id]) + + if append: + new_index_columns = [*self.index_columns, *new_index_columns] + new_index_labels = [*self._index_labels, *new_index_labels] + else: + expr = expr.drop_columns(self.index_columns) + + block = Block( + expr, + index_columns=new_index_columns, + column_labels=self.column_labels, + index_labels=new_index_labels, + ) + if drop: + # These are the value columns, new index uses the copies, so this is safe + block = block.drop_columns(col_ids) + return block + + def drop_levels(self, ids: typing.Sequence[str]): + for id in ids: + if id not in self.index_columns: + raise ValueError(f"{id} is not an index column") + expr = self._expr.drop_columns(ids) + remaining_index_col_ids = [ + col_id for col_id in self.index_columns if col_id not in ids + ] + if len(remaining_index_col_ids) == 0: + raise ValueError("Cannot drop all index levels, at least 1 must remain.") + level_names = [ + self.col_id_to_index_name[index_id] for index_id in remaining_index_col_ids + ] + return Block(expr, remaining_index_col_ids, self.column_labels, level_names) + + def reorder_levels(self, ids: typing.Sequence[str]): + if sorted(self.index_columns) != sorted(ids): + raise ValueError("Cannot drop or duplicate levels using reorder_levels.") + level_names = [self.col_id_to_index_name[index_id] for index_id in ids] + return Block(self.expr, ids, self.column_labels, level_names) + + def _to_dataframe(self, result, schema: ibis_schema.Schema) -> pd.DataFrame: + """Convert BigQuery data to pandas DataFrame with specific dtypes.""" + df = result.to_dataframe( + bool_dtype=pd.BooleanDtype(), + int_dtype=pd.Int64Dtype(), + float_dtype=pd.Float64Dtype(), + string_dtype=pd.StringDtype(storage="pyarrow"), + date_dtype=pd.ArrowDtype(pa.date32()), + datetime_dtype=pd.ArrowDtype(pa.timestamp("us")), + time_dtype=pd.ArrowDtype(pa.time64("us")), + timestamp_dtype=pd.ArrowDtype(pa.timestamp("us", tz="UTC")), + ) + + # Convert Geography column from StringDType to GeometryDtype. + for column_name, ibis_dtype in schema.items(): + if ibis_dtype.is_geospatial(): + df[column_name] = gpd.GeoSeries.from_wkt( + # https://github.com/geopandas/geopandas/issues/1879 + df[column_name].replace({numpy.nan: None}), + # BigQuery geography type is based on the WGS84 reference ellipsoid. + crs="EPSG:4326", + ) + return df + + def compute( + self, value_keys: Optional[Iterable[str]] = None, max_results=None + ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: + """Run query and download results as a pandas DataFrame.""" + df, _, query_job = self._compute_and_count( + value_keys=value_keys, max_results=max_results + ) + return df, query_job + + def _compute_and_count( + self, value_keys: Optional[Iterable[str]] = None, max_results=None + ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: + """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" + # TODO(swast): Allow for dry run and timeout. + expr = self._expr + + value_column_names = value_keys or self.value_columns + if value_keys is not None: + index_columns = ( + expr.get_column(column_name) for column_name in self._index_columns + ) + value_columns = (expr.get_column(column_name) for column_name in value_keys) + expr = expr.projection(itertools.chain(index_columns, value_columns)) + + results_iterator, query_job = expr.start_query(max_results=max_results) + df = self._to_dataframe( + results_iterator, + expr.to_ibis_expr().schema(), + ) + + df = df.loc[:, [*self.index_columns, *value_column_names]] + if self.index_columns: + df = df.set_index(list(self.index_columns)) + df.index.names = self.index.names # type: ignore + + return df, results_iterator.total_rows, query_job + + def with_column_labels(self, value: typing.Iterable[Label]) -> Block: + label_list = tuple(value) + if len(label_list) != len(self.value_columns): + raise ValueError( + f"The column labels size `{len(label_list)} ` should equal to the value" + + f"columns size: {len(self.value_columns)}." + ) + return Block( + self._expr, + index_columns=self.index_columns, + column_labels=label_list, + index_labels=self.index.names, + ) + + def with_index_labels(self, value: typing.Sequence[Label]) -> Block: + if len(value) != len(self.index_columns): + raise ValueError( + f"The index labels size `{len(value)} ` should equal to the index" + + f"columns size: {len(self.value_columns)}." + ) + return Block( + self._expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=tuple(value), + ) + + def get_value_col_exprs( + self, column_names: Optional[Sequence[str]] = None + ) -> List[ibis_types.Value]: + """Retrive value column expressions.""" + column_names = self.value_columns if column_names is None else column_names + return [self._expr.get_column(column_name) for column_name in column_names] + + def apply_unary_op( + self, column: str, op: ops.UnaryOp, result_label: Label = None + ) -> typing.Tuple[Block, str]: + """ + Apply a unary op to the block. Creates a new column to store the result. + """ + # TODO(tbergeron): handle labels safely so callers don't need to + result_id = guid.generate_guid() + expr = self._expr.project_unary_op(column, op, result_id) + block = Block( + expr, + index_columns=self.index_columns, + column_labels=[*self.column_labels, result_label], + index_labels=self.index.names, + ) + return (block, result_id) + + def apply_binary_op( + self, + left_column_id: str, + right_column_id: str, + op: ops.BinaryOp, + result_label: Label = None, + ) -> typing.Tuple[Block, str]: + result_id = guid.generate_guid() + expr = self._expr.project_binary_op( + left_column_id, right_column_id, op, result_id + ) + block = Block( + expr, + index_columns=self.index_columns, + column_labels=[*self.column_labels, result_label], + index_labels=self.index.names, + ) + return (block, result_id) + + def apply_ternary_op( + self, + col_id_1: str, + col_id_2: str, + col_id_3: str, + op: ops.TernaryOp, + result_label: Label = None, + ) -> typing.Tuple[Block, str]: + result_id = guid.generate_guid() + expr = self._expr.project_ternary_op( + col_id_1, col_id_2, col_id_3, op, result_id + ) + block = Block( + expr, + index_columns=self.index_columns, + column_labels=[*self.column_labels, result_label], + index_labels=self.index.names, + ) + return (block, result_id) + + def multi_apply_window_op( + self, + columns: typing.Sequence[str], + op: agg_ops.WindowOp, + window_spec: core.WindowSpec, + *, + skip_null_groups: bool = False, + ) -> Block: + block = self + for i, col_id in enumerate(columns): + label = self.col_id_to_label[col_id] + block, result_id = block.apply_window_op( + col_id, + op, + window_spec=window_spec, + skip_reproject_unsafe=(i + 1) < len(columns), + result_label=label, + skip_null_groups=skip_null_groups, + ) + block = block.copy_values(result_id, col_id) + block = block.drop_columns([result_id]) + return block + + def multi_apply_unary_op( + self, + columns: typing.Sequence[str], + op: ops.UnaryOp, + ) -> Block: + block = self + for i, col_id in enumerate(columns): + label = self.col_id_to_label[col_id] + block, result_id = block.apply_unary_op( + col_id, + op, + result_label=label, + ) + block = block.copy_values(result_id, col_id) + block = block.drop_columns([result_id]) + return block + + def apply_window_op( + self, + column: str, + op: agg_ops.WindowOp, + window_spec: core.WindowSpec, + *, + result_label: Label = None, + skip_null_groups: bool = False, + skip_reproject_unsafe: bool = False, + ) -> typing.Tuple[Block, str]: + result_id = guid.generate_guid() + expr = self._expr.project_window_op( + column, + op, + window_spec, + result_id, + skip_null_groups=skip_null_groups, + skip_reproject_unsafe=skip_reproject_unsafe, + ) + block = Block( + expr, + index_columns=self.index_columns, + column_labels=[*self.column_labels, result_label], + index_labels=self._index_labels, + ) + return (block, result_id) + + def copy_values(self, source_column_id: str, destination_column_id: str) -> Block: + expr = self.expr.assign(source_column_id, destination_column_id) + return Block( + expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self._index_labels, + ) + + def create_constant( + self, + scalar_constant: typing.Any, + label: Label = None, + dtype: typing.Optional[bigframes.dtypes.Dtype] = None, + ) -> typing.Tuple[Block, str]: + result_id = guid.generate_guid() + expr = self.expr.assign_constant(result_id, scalar_constant, dtype=dtype) + labels = [*self.column_labels, label] + return ( + Block( + expr, + index_columns=self.index_columns, + column_labels=labels, + index_labels=self.index.names, + ), + result_id, + ) + + def assign_label(self, column_id: str, new_label: Label) -> Block: + col_index = self.value_columns.index(column_id) + new_labels = list(self.column_labels) + new_labels[col_index] = new_label + return self.with_column_labels(new_labels) + + def filter(self, column_name: str): + condition = typing.cast( + ibis_types.BooleanValue, self._expr.get_column(column_name) + ) + filtered_expr = self.expr.filter(condition) + return Block( + filtered_expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + + def aggregate_all_and_pivot( + self, + operation: agg_ops.AggregateOp, + *, + value_col_id: str = "values", + dropna: bool = True, + dtype=pd.Float64Dtype(), + ) -> Block: + aggregations = [(col_id, operation, col_id) for col_id in self.value_columns] + result_expr = self.expr.aggregate( + aggregations, dropna=dropna + ).transpose_single_row( + labels=self.column_labels, + index_col_id="index", + value_col_id=value_col_id, + dtype=dtype, + ) + return Block(result_expr, index_columns=["index"], column_labels=[None]) + + def select_column(self, id: str) -> Block: + return self.select_columns([id]) + + def select_columns(self, ids: typing.Sequence[str]) -> Block: + expr = self._expr.select_columns([*self.index_columns, *ids]) + col_labels = self._get_labels_for_columns(ids) + return Block(expr, self.index_columns, col_labels, self.index.names) + + def drop_columns(self, ids_to_drop: typing.Sequence[str]) -> Block: + """Drops columns by id. Can drop index""" + if set(ids_to_drop) & set(self.index_columns): + raise ValueError( + "Cannot directly drop index column. Use reset_index(drop=True)" + ) + expr = self._expr.drop_columns(ids_to_drop) + remaining_value_col_ids = [ + col_id for col_id in self.value_columns if (col_id not in ids_to_drop) + ] + labels = self._get_labels_for_columns(remaining_value_col_ids) + return Block(expr, self.index_columns, labels, self.index.names) + + def rename(self, *, columns: typing.Mapping[Label, Label]): + # TODO(tbergeron) Support function(Callable) as columns parameter. + col_labels = [ + (columns.get(col_label, col_label)) for col_label in self.column_labels + ] + return self.with_column_labels(col_labels) + + def aggregate( + self, + by_column_ids: typing.Sequence[str], + aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp]], + *, + as_index: bool = True, + dropna: bool = True, + ) -> typing.Tuple[Block, typing.Sequence[str]]: + """ + Apply aggregations to the block. Callers responsible for setting index column(s) after. + Arguments: + by_column_id: column id of the aggregation key, this is preserved through the transform and used as index + aggregations: input_column_id, operation tuples + as_index: if True, grouping keys will be index columns in result, otherwise they will be non-index columns. + dropna: whether null keys should be dropped + """ + agg_specs = [ + (input_id, operation, guid.generate_guid()) + for input_id, operation in aggregations + ] + output_col_ids = [agg_spec[2] for agg_spec in agg_specs] + result_expr = self.expr.aggregate(agg_specs, by_column_ids, dropna=dropna) + + aggregate_labels = self._get_labels_for_columns( + [agg[0] for agg in aggregations] + ) + if as_index: + # TODO: Generalize to multi-index + names: typing.List[Label] = [] + for by_col_id in by_column_ids: + if by_col_id in self.index_columns: + # Groupby level 0 case, keep index name + index_name = self.col_id_to_index_name[by_col_id] + else: + index_name = self.col_id_to_label[by_col_id] + names.append(index_name) + return ( + Block( + result_expr, + index_columns=by_column_ids, + column_labels=aggregate_labels, + index_labels=names, + ), + output_col_ids, + ) + else: + by_column_labels = self._get_labels_for_columns(by_column_ids) + labels = (*by_column_labels, *aggregate_labels) + return Block(result_expr, column_labels=labels), output_col_ids + + def _get_labels_for_columns(self, column_ids: typing.Sequence[str]): + """Get column label for value columns, or index name for index columns""" + lookup = self.col_id_to_label + return [lookup.get(col_id, None) for col_id in column_ids] + + def _normalize_expression( + self, + expr: core.ArrayValue, + index_columns: typing.Sequence[str], + assert_value_size: typing.Optional[int] = None, + ): + """Normalizes expression by moving index columns to left.""" + value_columns = [ + col_id for col_id in expr.column_names.keys() if col_id not in index_columns + ] + if (assert_value_size is not None) and ( + len(value_columns) != assert_value_size + ): + raise ValueError("Unexpected number of value columns.") + return expr.select_columns([*index_columns, *value_columns]) + + def slice( + self: bigframes.core.blocks.Block, + start: typing.Optional[int] = None, + stop: typing.Optional[int] = None, + step: typing.Optional[int] = None, + ) -> bigframes.core.blocks.Block: + sliced_expr = self.expr.slice(start=start, stop=stop, step=step) + # since this is slice, return a copy even if unchanged + block = Block( + sliced_expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self._index_labels, + ) + return block + + def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: + expr, result_id = self._expr.promote_offsets() + return ( + Block( + expr, + index_columns=self.index_columns, + column_labels=[label, *self.column_labels], + index_labels=self._index_labels, + ), + result_id, + ) + + def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: + axis_number = _get_axis_number(axis) + if axis_number == 0: + expr = self._expr + for index_col in self._index_columns: + expr = expr.project_unary_op(index_col, ops.AsTypeOp("string")) + prefix_op = ops.BinopPartialLeft(ops.add_op, prefix) + expr = expr.project_unary_op(index_col, prefix_op) + return Block( + expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + if axis_number == 1: + expr = self._expr + return Block( + self._expr, + index_columns=self.index_columns, + column_labels=[f"{prefix}{label}" for label in self.column_labels], + index_labels=self.index.names, + ) + + def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: + axis_number = _get_axis_number(axis) + if axis_number == 0: + expr = self._expr + for index_col in self._index_columns: + expr = expr.project_unary_op(index_col, ops.AsTypeOp("string")) + prefix_op = ops.BinopPartialRight(ops.add_op, suffix) + expr = expr.project_unary_op(index_col, prefix_op) + return Block( + expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + if axis_number == 1: + expr = self._expr + return Block( + self._expr, + index_columns=self.index_columns, + column_labels=[f"{label}{suffix}" for label in self.column_labels], + index_labels=self.index.names, + ) + + def concat( + self, + other: typing.Iterable[Block], + how: typing.Literal["inner", "outer"], + ignore_index=False, + ): + blocks: typing.List[Block] = [self, *other] + if ignore_index: + blocks = [block.reset_index() for block in blocks] + + result_labels = _align_indices(blocks) + + index_nlevels = blocks[0].index.nlevels + + aligned_schema = _align_schema(blocks, how=how) + aligned_blocks = [ + _align_block_to_schema(block, aligned_schema) for block in blocks + ] + result_expr = aligned_blocks[0]._expr.concat( + [block._expr for block in aligned_blocks[1:]] + ) + result_block = Block( + result_expr, + index_columns=list(result_expr.column_names.keys())[:index_nlevels], + column_labels=aligned_blocks[0].column_labels, + index_labels=result_labels, + ) + if ignore_index: + result_block = result_block.reset_index() + return result_block + + +def block_from_local(data, session=None, use_index=True) -> Block: + # TODO(tbergeron): Handle duplicate column labels + pd_data = pd.DataFrame(data) + + column_labels = list(pd_data.columns) + if not all((label is None) or isinstance(label, str) for label in column_labels): + raise NotImplementedError("Only string column labels supported") + + if use_index: + if pd_data.index.nlevels > 1: + raise NotImplementedError("multi-indices not supported.") + index_label = pd_data.index.name + if (index_label is not None) and (not isinstance(index_label, str)): + raise NotImplementedError("Only string index names supported") + + index_id = guid.generate_guid() + pd_data = pd_data.reset_index(names=index_id) + keys_expr = core.ArrayValue.mem_expr_from_pandas(pd_data, session) + return Block( + keys_expr, + column_labels=column_labels, + index_columns=[index_id], + index_labels=[index_label], + ) + else: + keys_expr = core.ArrayValue.mem_expr_from_pandas(pd_data, session) + # Constructor will create default range index + return Block(keys_expr, column_labels=column_labels) + + +def _align_block_to_schema( + block: Block, schema: dict[Label, bigframes.dtypes.Dtype] +) -> Block: + """For a given schema, remap block to schema by reordering columns and inserting nulls.""" + col_ids: typing.Tuple[str, ...] = () + for label, dtype in schema.items(): + # TODO: Support casting to lcd type - requires mixed type support + matching_ids: typing.Sequence[str] = block.label_to_col_id.get(label, ()) + if len(matching_ids) > 0: + col_id = matching_ids[-1] + col_ids = (*col_ids, col_id) + else: + block, null_column = block.create_constant(None, dtype=dtype) + col_ids = (*col_ids, null_column) + return block.select_columns(col_ids).with_column_labels( + [item for item in schema.keys()] + ) + + +def _align_schema( + blocks: typing.Iterable[Block], how: typing.Literal["inner", "outer"] +) -> typing.Dict[Label, bigframes.dtypes.Dtype]: + schemas = [_get_block_schema(block) for block in blocks] + reduction = _combine_schema_inner if how == "inner" else _combine_schema_outer + return functools.reduce(reduction, schemas) + + +def _align_indices(blocks: typing.Sequence[Block]) -> typing.Sequence[Label]: + """Validates that the blocks have compatible indices and returns the resulting label names.""" + names = blocks[0].index.names + types = blocks[0].index.dtypes + for block in blocks[1:]: + if len(names) != block.index.nlevels: + raise NotImplementedError( + "Cannot combine indices with different number of levels. Use 'ignore_index'=True." + ) + if block.index.dtypes != types: + raise NotImplementedError( + "Cannot combine different index dtypes. Use 'ignore_index'=True." + ) + names = [ + lname if lname == rname else None + for lname, rname in zip(names, block.index.names) + ] + return names + + +def _combine_schema_inner( + left: typing.Dict[Label, bigframes.dtypes.Dtype], + right: typing.Dict[Label, bigframes.dtypes.Dtype], +) -> typing.Dict[Label, bigframes.dtypes.Dtype]: + result = dict() + for label, type in left.items(): + if label in right: + if type != right[label]: + raise ValueError( + f"Cannot concat rows with label {label} due to mismatched types" + ) + result[label] = type + return result + + +def _combine_schema_outer( + left: typing.Dict[Label, bigframes.dtypes.Dtype], + right: typing.Dict[Label, bigframes.dtypes.Dtype], +) -> typing.Dict[Label, bigframes.dtypes.Dtype]: + result = dict() + for label, type in left.items(): + if (label in right) and (type != right[label]): + raise ValueError( + f"Cannot concat rows with label {label} due to mismatched types" + ) + result[label] = type + for label, type in right.items(): + if label not in left: + result[label] = type + return result + + +def _get_block_schema( + block: Block, +) -> typing.Dict[Label, bigframes.dtypes.Dtype]: + """Extracts the schema from the block. Where duplicate labels exist, take the last matching column.""" + result = dict() + for label, dtype in zip(block.column_labels, block.dtypes): + result[label] = typing.cast(bigframes.dtypes.Dtype, dtype) + return result + + +def _get_axis_number(axis: str | int | None) -> typing.Literal[0, 1]: + if axis in {0, "index", "rows", None}: + return 0 + elif axis in {1, "columns"}: + return 1 + else: + raise ValueError(f"Not a valid axis: {axis}") diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py new file mode 100644 index 00000000000..a8655116e93 --- /dev/null +++ b/bigframes/core/groupby/__init__.py @@ -0,0 +1,315 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing + +import bigframes.core as core +import bigframes.core.blocks as blocks +import bigframes.core.ordering as order +import bigframes.core.window as windows +import bigframes.dataframe as df +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops +import bigframes.series as series +import third_party.bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby + + +class DataFrameGroupBy(vendored_pandas_groupby.DataFrameGroupBy): + __doc__ = vendored_pandas_groupby.GroupBy.__doc__ + + def __init__( + self, + block: blocks.Block, + by_col_ids: typing.Sequence[str], + *, + dropna: bool = True, + as_index: bool = True, + ): + # TODO(tbergeron): Support more group-by expression types + self._block = block + self._col_id_labels = { + value_column: column_label + for value_column, column_label in zip( + block.value_columns, block.column_labels + ) + } + self._by_col_ids = by_col_ids + self._dropna = dropna # Applies to aggregations but not windowing + self._as_index = as_index + + def sum(self, numeric_only: bool = False, *args) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("sum") + return self._aggregate(agg_ops.sum_op, numeric_only=True) + + def mean(self, numeric_only: bool = False, *args) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("mean") + return self._aggregate(agg_ops.mean_op, numeric_only=True) + + def min(self, numeric_only: bool = False, *args) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("min") + return self._aggregate(agg_ops.min_op, numeric_only=True) + + def max(self, numeric_only: bool = False, *args) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("max") + return self._aggregate(agg_ops.max_op, numeric_only=True) + + def std( + self, + *, + numeric_only: bool = False, + ) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("std") + return self._aggregate(agg_ops.std_op, numeric_only=True) + + def var( + self, + *, + numeric_only: bool = False, + ) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("var") + return self._aggregate(agg_ops.var_op, numeric_only=True) + + def all(self) -> df.DataFrame: + return self._aggregate(agg_ops.all_op) + + def any(self) -> df.DataFrame: + return self._aggregate(agg_ops.any_op) + + def count(self) -> df.DataFrame: + return self._aggregate(agg_ops.count_op) + + def cumsum(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("cumsum") + window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) + return self._apply_window_op(agg_ops.sum_op, window, numeric_only=True) + + def cummin(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("cummin") + window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) + return self._apply_window_op(agg_ops.min_op, window, numeric_only=True) + + def cummax(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("cummax") + window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) + return self._apply_window_op(agg_ops.max_op, window, numeric_only=True) + + def cumprod(self, *args, **kwargs) -> df.DataFrame: + window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) + return self._apply_window_op(agg_ops.product_op, window, numeric_only=True) + + def _raise_on_non_numeric(self, op: str): + if not all( + dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES + for dtype in self._block.dtypes + ): + raise NotImplementedError( + f"'{op}' does not support non-numeric columns. Set 'numeric_only'=True to ignore non-numeric columns" + ) + return self + + def _aggregated_columns(self, numeric_only: bool = False): + return [ + col_id + for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) + if col_id not in self._by_col_ids + and ( + (not numeric_only) + or (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) + ) + ] + + def _aggregate( + self, aggregate_op: agg_ops.AggregateOp, numeric_only: bool = False + ) -> df.DataFrame: + aggregated_col_ids = self._aggregated_columns(numeric_only=numeric_only) + aggregations = [(col_id, aggregate_op) for col_id in aggregated_col_ids] + result_block, _ = self._block.aggregate( + self._by_col_ids, + aggregations, + as_index=self._as_index, + dropna=self._dropna, + ) + return df.DataFrame(result_block) + + def _apply_window_op( + self, + op: agg_ops.WindowOp, + window_spec: bigframes.core.WindowSpec, + numeric_only: bool = False, + ): + columns = self._aggregated_columns(numeric_only=numeric_only) + block = self._block.select_columns([*columns, *window_spec.grouping_keys]) + block = self._block.multi_apply_window_op( + columns, + op, + window_spec=window_spec, + ) + block = block.select_columns(columns) + return df.DataFrame(block) + + +class SeriesGroupBy(vendored_pandas_groupby.SeriesGroupBy): + __doc__ = vendored_pandas_groupby.GroupBy.__doc__ + + def __init__( + self, + block: blocks.Block, + value_column: str, + by_col_ids: typing.Sequence[str], + value_name: typing.Optional[str] = None, + dropna=True, + ): + # TODO(tbergeron): Support more group-by expression types + self._block = block + self._value_column = value_column + self._by_col_ids = by_col_ids + self._value_name = value_name + self._dropna = dropna # Applies to aggregations but not windowing + + @property + def value(self): + return self._block.expr.get_column(self._value_column) + + def all(self) -> series.Series: + return self._aggregate(agg_ops.all_op) + + def any(self) -> series.Series: + return self._aggregate(agg_ops.any_op) + + def count(self) -> series.Series: + return self._aggregate(agg_ops.count_op) + + def sum(self, *args) -> series.Series: + """Sums the numeric values for each group in the series. Ignores null/nan.""" + return self._aggregate(agg_ops.sum_op) + + def mean(self, *args) -> series.Series: + return self._aggregate(agg_ops.mean_op) + + def std(self, *args, **kwargs) -> series.Series: + return self._aggregate(agg_ops.std_op) + + def var(self, *args, **kwargs) -> series.Series: + return self._aggregate(agg_ops.var_op) + + def prod(self, *args) -> series.Series: + return self._aggregate(agg_ops.product_op) + + def cumsum(self, *args, **kwargs) -> series.Series: + return self._apply_window_op( + agg_ops.sum_op, + bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), + ) + + def cumprod(self, *args, **kwargs) -> series.Series: + return self._apply_window_op( + agg_ops.product_op, + bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), + ) + + def cummax(self, *args, **kwargs) -> series.Series: + return self._apply_window_op( + agg_ops.max_op, + bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), + ) + + def cummin(self, *args, **kwargs) -> series.Series: + return self._apply_window_op( + agg_ops.min_op, + bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), + ) + + def cumcount(self, *args, **kwargs) -> series.Series: + return self._apply_window_op( + agg_ops.rank_op, + bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), + discard_name=True, + )._apply_unary_op(ops.partial_right(ops.sub_op, 1)) + + def shift(self, periods=1) -> series.Series: + """Shift index by desired number of periods.""" + window = bigframes.core.WindowSpec( + grouping_keys=self._by_col_ids, + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.ShiftOp(periods), window) + + def diff(self) -> series.Series: + """Difference between each element and previous element.""" + return self._ungroup() - self.shift(1) + + 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 = core.WindowSpec( + grouping_keys=self._by_col_ids, + preceding=window - 1, + following=0, + min_periods=min_periods or window, + ) + block = self._block.order_by( + [order.OrderingColumnReference(col) for col in self._by_col_ids], + stable=True, + ) + return windows.Window(block, window_spec, self._value_column) + + def expanding(self, min_periods: int = 1) -> windows.Window: + window_spec = core.WindowSpec( + grouping_keys=self._by_col_ids, following=0, min_periods=min_periods + ) + block = self._block.order_by( + [order.OrderingColumnReference(col) for col in self._by_col_ids], + stable=True, + ) + return windows.Window(block, window_spec, self._value_column) + + def _ungroup(self) -> series.Series: + return series.Series(self._block.select_column(self._value_column)) + + def _aggregate(self, aggregate_op: agg_ops.AggregateOp) -> series.Series: + result_block, _ = self._block.aggregate( + self._by_col_ids, + ((self._value_column, aggregate_op),), + dropna=self._dropna, + ) + + return series.Series(result_block.with_column_labels([self._value_name])) + + def _apply_window_op( + self, + op: agg_ops.WindowOp, + window_spec: bigframes.core.WindowSpec, + discard_name=False, + ): + label = self._value_name if not discard_name else None + block, result_id = self._block.apply_window_op( + self._value_column, + op, + result_label=label, + window_spec=window_spec, + skip_null_groups=self._dropna, + ) + return series.Series(block.select_column(result_id)) diff --git a/bigframes/core/guid.py b/bigframes/core/guid.py new file mode 100644 index 00000000000..4eb6c7a9d62 --- /dev/null +++ b/bigframes/core/guid.py @@ -0,0 +1,21 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_GUID_COUNTER = 0 + + +def generate_guid(prefix="col_"): + global _GUID_COUNTER + _GUID_COUNTER += 1 + return prefix + str(_GUID_COUNTER) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py new file mode 100644 index 00000000000..f65cb02941e --- /dev/null +++ b/bigframes/core/indexers.py @@ -0,0 +1,337 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing +from typing import Tuple + +import ibis +import pandas as pd + +import bigframes.core as core +import bigframes.core.guid as guid +import bigframes.core.indexes as indexes +import bigframes.core.scalar +import bigframes.dataframe +import bigframes.series + +if typing.TYPE_CHECKING: + LocSingleKey = bigframes.series.Series | indexes.Index | slice + + +class LocSeriesIndexer: + def __init__(self, series: bigframes.series.Series): + self._series = series + + def __getitem__(self, key) -> bigframes.series.Series: + """ + Only indexing by a boolean bigframes.series.Series or list of index entries is currently supported + """ + return typing.cast( + bigframes.series.Series, _loc_getitem_series_or_dataframe(self._series, key) + ) + + def __setitem__(self, key, value) -> None: + # TODO(swast): support MultiIndex + if isinstance(key, slice): + # TODO(swast): Implement loc with slices. + raise NotImplementedError("loc does not yet support slices") + elif isinstance(key, list): + # TODO(tbergeron): Implement loc for index label list. + raise NotImplementedError("loc does not yet support index label lists") + + # Assume the key is for the index label. + block = self._series._block + value_column = self._series._value + index_column = block.expr.get_column(block.index_columns[0]) + new_value = ( + ibis.case() + .when( + index_column == ibis.literal(key, index_column.type()), + ibis.literal(value, value_column.type()), + ) + .else_(value_column) + .end() + .name(value_column.get_name()) + ) + all_columns = [] + for column in block.expr.columns: + if column.get_name() != value_column.get_name(): + all_columns.append(column) + else: + all_columns.append(new_value) + new_expr = block.expr.projection(all_columns) + + # TODO(tbergeron): Use block operators rather than directly building desired ibis expressions. + self._series._set_block( + core.blocks.Block( + new_expr, + self._series._block.index_columns, + self._series._block.column_labels, + self._series._block.index.names, + ) + ) + + +class IlocSeriesIndexer: + def __init__(self, series: bigframes.series.Series): + self._series = series + + def __getitem__( + self, key + ) -> bigframes.core.scalar.Scalar | bigframes.series.Series: + """ + Index series using integer offsets. Currently supports index by key type: + + slice: ex. series.iloc[2:5] returns values at index 2, 3, and 4 as a series + individual offset: ex. series.iloc[0] returns value at index 0 as a scalar + list: ex. series.iloc[1, 1, 2, 0] returns a series with the index 1 item repeated + twice, followed by the index 2 and then and 0 items in that order. + + Other key types are not yet supported. + """ + return _iloc_getitem_series_or_dataframe(self._series, key) + + +class LocDataFrameIndexer: + def __init__(self, dataframe: bigframes.dataframe.DataFrame): + self._dataframe = dataframe + + @typing.overload + def __getitem__(self, key: LocSingleKey) -> bigframes.dataframe.DataFrame: + ... + + # Technically this is wrong since we can have duplicate column labels, but + # this is expected to be rare. + @typing.overload + def __getitem__(self, key: Tuple[LocSingleKey, str]) -> bigframes.series.Series: + ... + + def __getitem__(self, key): + # TODO(swast): If the DataFrame has a MultiIndex, we'll need to + # disambiguate this from a single row selection. + if isinstance(key, tuple) and len(key) == 2: + df = typing.cast( + bigframes.dataframe.DataFrame, + _loc_getitem_series_or_dataframe(self._dataframe, key[0]), + ) + return df[key[1]] + + return typing.cast( + bigframes.dataframe.DataFrame, + _loc_getitem_series_or_dataframe(self._dataframe, key), + ) + + def __setitem__( + self, + key: Tuple[slice, str], + value: bigframes.dataframe.SingleItemValue, + ): + if ( + not isinstance(key, tuple) + or len(key) != 2 + or not isinstance(key[0], slice) + or (key[0].start is not None and key[0].start != 0) + or (key[0].step is not None and key[0].step != 1) + or key[0].stop is not None + ): + raise NotImplementedError( + "Only setting a column by DataFrame.loc[:, 'column'] is supported." + ) + + # TODO(swast): Support setting multiple columns with key[1] as a list + # of labels and value as a DataFrame. + df = self._dataframe.assign(**{key[1]: value}) + self._dataframe._set_block(df._get_block()) + + +class ILocDataFrameIndexer: + def __init__(self, dataframe: bigframes.dataframe.DataFrame): + self._dataframe = dataframe + + def __getitem__(self, key) -> bigframes.dataframe.DataFrame | pd.Series: + """ + Index dataframe using integer offsets. Currently supports index by key type: + + slice: i.e. df.iloc[2:5] returns rows at index 2, 3, and 4 as a dataframe + individual offset: i.e. df.iloc[0] returns row at index 0 as a pandas Series + + Other key types are not yet supported. + """ + return _iloc_getitem_series_or_dataframe(self._dataframe, key) + + +@typing.overload +def _loc_getitem_series_or_dataframe( + series_or_dataframe: bigframes.series.Series, key +) -> bigframes.series.Series: + ... + + +@typing.overload +def _loc_getitem_series_or_dataframe( + series_or_dataframe: bigframes.dataframe.DataFrame, key +) -> bigframes.dataframe.DataFrame: + ... + + +def _loc_getitem_series_or_dataframe( + series_or_dataframe: bigframes.dataframe.DataFrame | bigframes.series.Series, + key: LocSingleKey, +) -> bigframes.dataframe.DataFrame | bigframes.series.Series: + if isinstance(key, bigframes.series.Series) and key.dtype == "boolean": + return series_or_dataframe[key] + elif isinstance(key, bigframes.series.Series): + # TODO(henryjsolberg): support MultiIndex + temp_name = guid.generate_guid(prefix="temp_series_name_") + key = key.rename(temp_name) + keys_df = key.to_frame() + keys_df = keys_df.set_index(temp_name, drop=True) + return _perform_loc_list_join(series_or_dataframe, keys_df) + elif isinstance(key, bigframes.core.indexes.Index): + # TODO(henryjsolberg): support MultiIndex + block = key._data._get_block() + block = block.select_columns(()) + keys_df = bigframes.dataframe.DataFrame(block) + return _perform_loc_list_join(series_or_dataframe, keys_df) + elif pd.api.types.is_list_like(key): + # TODO(henryjsolberg): support MultiIndex + if len(key) == 0: # type: ignore + return typing.cast( + typing.Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + series_or_dataframe.iloc[0:0], + ) + index_name = series_or_dataframe.index.name + keys_df = bigframes.dataframe.DataFrame( + {index_name: key}, session=series_or_dataframe._get_block().expr._session + ) + keys_df = keys_df.set_index(index_name, drop=True) + return _perform_loc_list_join(series_or_dataframe, keys_df) + elif isinstance(key, slice): + return series_or_dataframe._slice(key.start, key.stop, key.step) + elif callable(key): + raise NotImplementedError("loc does not yet support indexing with a callable") + else: + raise TypeError( + "Invalid argument type. loc currently only supports indexing with a boolean bigframes Series or a list of index entries." + ) + + +@typing.overload +def _perform_loc_list_join( + series_or_dataframe: bigframes.series.Series, + keys_df: bigframes.dataframe.DataFrame, +) -> bigframes.series.Series: + ... + + +@typing.overload +def _perform_loc_list_join( + series_or_dataframe: bigframes.dataframe.DataFrame, + keys_df: bigframes.dataframe.DataFrame, +) -> bigframes.dataframe.DataFrame: + ... + + +def _perform_loc_list_join( + series_or_dataframe: bigframes.dataframe.DataFrame | bigframes.series.Series, + keys_df: bigframes.dataframe.DataFrame, +) -> bigframes.series.Series | bigframes.dataframe.DataFrame: + # right join based on the old index so that the matching rows from the user's + # original dataframe will be duplicated and reordered appropriately + original_index_names = series_or_dataframe.index.names + if isinstance(series_or_dataframe, bigframes.series.Series): + original_name = series_or_dataframe.name + name = series_or_dataframe.name if series_or_dataframe.name is not None else "0" + result = typing.cast( + bigframes.series.Series, + series_or_dataframe.to_frame().join(keys_df, how="right")[name], + ) + result = result.rename(original_name) + else: + result = series_or_dataframe.join(keys_df, how="right") # type: ignore + result = result.rename_axis(original_index_names) + return result + + +@typing.overload +def _iloc_getitem_series_or_dataframe( + series_or_dataframe: bigframes.series.Series, key +) -> bigframes.series.Series | bigframes.core.scalar.Scalar: + ... + + +@typing.overload +def _iloc_getitem_series_or_dataframe( + series_or_dataframe: bigframes.dataframe.DataFrame, key +) -> bigframes.dataframe.DataFrame | pd.Series: + ... + + +def _iloc_getitem_series_or_dataframe( + series_or_dataframe: bigframes.dataframe.DataFrame | bigframes.series.Series, key +) -> bigframes.dataframe.DataFrame | bigframes.series.Series | bigframes.core.scalar.Scalar | pd.Series: + if isinstance(key, int): + if key < 0: + raise NotImplementedError( + "iloc does not yet support negative single positional index" + ) + internal_slice_result = series_or_dataframe._slice(key, key + 1, 1) + result_pd_df = internal_slice_result.compute() + if result_pd_df.empty: + raise IndexError("single positional indexer is out-of-bounds") + return result_pd_df.iloc[0] + elif isinstance(key, slice): + return series_or_dataframe._slice(key.start, key.stop, key.step) + elif pd.api.types.is_list_like(key): + # TODO(henryjsolberg): support MultiIndex + + if len(key) == 0: + return typing.cast( + typing.Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + series_or_dataframe.iloc[0:0], + ) + df = series_or_dataframe + if isinstance(series_or_dataframe, bigframes.series.Series): + original_series_name = series_or_dataframe.name + series_name = ( + original_series_name if original_series_name is not None else "0" + ) + df = series_or_dataframe.to_frame() + original_index_name = df.index.name + temporary_index_name = guid.generate_guid(prefix="temp_iloc_index_") + df = df.rename_axis(temporary_index_name) + + # set to offset index and use regular loc, then restore index + df = df.reset_index(drop=False) + result = df.loc[key] + result = result.set_index(temporary_index_name) + result = result.rename_axis(original_index_name) + + if isinstance(series_or_dataframe, bigframes.series.Series): + result = result[series_name] + result = typing.cast(bigframes.series.Series, result) + result = result.rename(original_series_name) + + return result + + elif isinstance(key, tuple): + raise NotImplementedError( + "iloc does not yet support indexing with a (row, column) tuple" + ) + elif callable(key): + raise NotImplementedError("iloc does not yet support indexing with a callable") + else: + raise TypeError("Invalid argument type.") diff --git a/bigframes/core/indexes/__init__.py b/bigframes/core/indexes/__init__.py new file mode 100644 index 00000000000..d797c57955a --- /dev/null +++ b/bigframes/core/indexes/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes.core.indexes.index import Index, IndexValue + +INDEX_COLUMN_ID = "bigframes_index_{}" + +__all__ = [ + "Index", + "IndexValue", + "INDEX_COLUMN_ID", +] diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py new file mode 100644 index 00000000000..f7fa7ac1195 --- /dev/null +++ b/bigframes/core/indexes/index.py @@ -0,0 +1,250 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An index based on a single column.""" + +from __future__ import annotations + +import typing +from typing import Callable, Tuple + +import numpy as np +import pandas as pd + +import bigframes.core as core +import bigframes.core.blocks as blocks +import bigframes.core.joins as joins +import bigframes.dtypes as bf_dtypes +import third_party.bigframes_vendored.pandas.core.indexes.base as vendored_pandas_index + + +class Index(vendored_pandas_index.Index): + __doc__ = vendored_pandas_index.Index.__doc__ + + def __init__(self, data: blocks.BlockHolder): + self._data = data + + @property + def name(self) -> typing.Optional[str]: + return self.names[0] + + @name.setter + def name(self, value: blocks.Label): + self.names = [value] + + @property + def names(self) -> typing.Sequence[blocks.Label]: + return self._data._get_block()._index_labels + + @names.setter + def names(self, values: typing.Sequence[blocks.Label]): + return self._data._set_block(self._data._get_block().with_index_labels(values)) + + @property + def shape(self) -> typing.Tuple[int]: + return (self._data._get_block().shape[0],) + + @property + def size(self) -> int: + return self.shape[0] + + @property + def empty(self) -> bool: + return self.shape[0] == 0 + + def to_pandas(self) -> pd.Index: + """Get the Index as a pandas Index.""" + return IndexValue(self._data._get_block()).compute() + + def __len__(self): + return self.shape[0] + + compute = to_pandas + + +class IndexValue: + """An immutable index.""" + + def __init__(self, block: blocks.Block): + self._block = block + + @property + def _expr(self) -> core.ArrayValue: + return self._block.expr + + @property + def name(self) -> typing.Optional[str]: + return self._block._index_labels[0] + + @property + def names(self) -> typing.Sequence[typing.Optional[str]]: + return self._block._index_labels + + @property + def nlevels(self) -> int: + return len(self._block._index_columns) + + @property + def dtypes( + self, + ) -> typing.Sequence[typing.Union[bf_dtypes.Dtype, np.dtype[typing.Any]]]: + return self._block.index_dtypes + + def __repr__(self) -> str: + """Converts an Index to a string.""" + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + # TODO(swast): Avoid downloading the whole index by using job + # metadata, like we do with DataFrame. + preview = self.compute() + return repr(preview) + + def compute(self) -> pd.Index: + """Executes deferred operations and downloads the results.""" + # Project down to only the index column. So the query can be cached to visualize other data. + index_column = self._block.index_columns[0] + expr = self._expr.projection([self._expr.get_any_column(index_column)]) + results, _ = expr.start_query() + df = expr._session._rows_to_dataframe(results) + df.set_index(index_column) + index = df.index + index.name = self._block._index_labels[0] + return index + + def join( + self, other: IndexValue, *, how="left", sort=False + ) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: + if not isinstance(other, IndexValue): + # TODO(swast): We need to improve this error message to be more + # actionable for the user. For example, it's possible they + # could call set_index and try again to resolve this error. + raise ValueError( + "Can't mixed objects with explicit Index and ImpliedJoiner" + ) + + # TODO(swast): Support cross-joins (requires reindexing). + if how not in {"outer", "left", "right", "inner"}: + raise NotImplementedError( + "Only how='outer','left','right','inner' currently supported" + ) + if self.nlevels == other.nlevels == 1: + return join_mono_indexed(self, other, how=how, sort=sort) + else: + # Always sort mult-index join + return join_multi_indexed(self, other, how=how, sort=sort) + + def resolve_level_name(self: IndexValue, label: blocks.Label) -> str: + matches = self._block.index_name_to_col_id.get(label, []) + if len(matches) > 1: + raise ValueError(f"Ambiguous index level name {label}") + if len(matches) == 0: + raise ValueError(f"Cannot resolve index level name {label}") + return matches[0] + + def is_uniquely_named(self: IndexValue): + return len(set(self.names)) == len(self.names) + + +def join_mono_indexed( + left: IndexValue, right: IndexValue, *, how="left", sort=False +) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: + ( + combined_expr, + joined_index_col_names, + (get_column_left, get_column_right), + ) = joins.join_by_column( + left._block.expr, + left._block.index_columns, + right._block.expr, + right._block.index_columns, + how=how, + sort=sort, + ) + # Drop original indices from each side. and used the coalesced combination generated by the join. + left_indices = [get_column_left(col_id) for col_id in left._block.index_columns] + right_indices = [get_column_right(col_id) for col_id in right._block.index_columns] + combined_expr = combined_expr.drop_columns(left_indices).drop_columns(right_indices) + block = blocks.Block( + combined_expr, + index_columns=[*joined_index_col_names], + column_labels=[*left._block.column_labels, *right._block.column_labels], + index_labels=[left.name] if left.name == right.name else [None], + ) + return ( + typing.cast(IndexValue, block.index), + (get_column_left, get_column_right), + ) + + +def join_multi_indexed( + left: IndexValue, right: IndexValue, *, how="left", sort=False +) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: + if not (left.is_uniquely_named() and right.is_uniquely_named()): + raise ValueError("Joins not supported on indices with non-unique level names") + + common_names = [name for name in left.names if name in right.names] + if len(common_names) == 0: + raise ValueError("Cannot join without a index level in common.") + + left_only_names = [name for name in left.names if name not in right.names] + right_only_names = [name for name in right.names if name not in left.names] + + left_join_ids = [left.resolve_level_name(name) for name in common_names] + right_join_ids = [right.resolve_level_name(name) for name in common_names] + + ( + combined_expr, + joined_index_col_names, + (get_column_left, get_column_right), + ) = joins.join_by_column( + left._block.expr, + left_join_ids, + right._block.expr, + right_join_ids, + how=how, + sort=sort, + ) + # Drop original indices from each side. and used the coalesced combination generated by the join. + combined_expr = combined_expr.drop_columns( + [get_column_left(col) for col in left_join_ids] + ).drop_columns([get_column_right(col) for col in right_join_ids]) + + if left.nlevels == 1: + index_labels = right.names + elif right.nlevels == 1: + index_labels = left.names + else: + index_labels = [*common_names, *left_only_names, *right_only_names] + + def resolve_label_id(label: blocks.Label) -> str: + if label in common_names: + return joined_index_col_names[common_names.index(label)] + if label in left_only_names: + return get_column_left(left.resolve_level_name(label)) + if label in right_only_names: + return get_column_right(right.resolve_level_name(label)) + raise ValueError(f"Unexpected label: {label}") + + index_columns = [resolve_label_id(label) for label in index_labels] + + block = blocks.Block( + combined_expr, + index_columns=index_columns, + column_labels=[*left._block.column_labels, *right._block.column_labels], + index_labels=index_labels, + ) + return ( + typing.cast(IndexValue, block.index), + (get_column_left, get_column_right), + ) diff --git a/bigframes/core/joins/__init__.py b/bigframes/core/joins/__init__.py new file mode 100644 index 00000000000..58a1c2cfd79 --- /dev/null +++ b/bigframes/core/joins/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers to join ArrayValue objects.""" + +from bigframes.core.joins.row_identity import join_by_row_identity +from bigframes.core.joins.single_column import join_by_column + +__all__ = ( + "join_by_row_identity", + "join_by_column", +) diff --git a/bigframes/core/joins/row_identity.py b/bigframes/core/joins/row_identity.py new file mode 100644 index 00000000000..b77b5d80adc --- /dev/null +++ b/bigframes/core/joins/row_identity.py @@ -0,0 +1,201 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers to join ArrayValue objects.""" + +from __future__ import annotations + +import functools +import typing +from typing import Callable, Tuple + +import ibis +import ibis.expr.types as ibis_types + +import bigframes.core as core + +SUPPORTED_ROW_IDENTITY_HOW = {"outer", "left", "inner"} + + +def join_by_row_identity( + left: core.ArrayValue, right: core.ArrayValue, *, how: str +) -> Tuple[core.ArrayValue, Tuple[Callable[[str], str], Callable[[str], str]],]: + """Compute join when we are joining by row identity not a specific column.""" + if how not in SUPPORTED_ROW_IDENTITY_HOW: + raise NotImplementedError("Only how='outer','left','inner' currently supported") + + if not left.table.equals(right.table): + raise ValueError( + "Cannot combine objects without an explicit join/merge key. " + f"Left based on: {left.table.compile()}, but " + f"right based on: {right.table.compile()}" + ) + + left_predicates = left._predicates + right_predicates = right._predicates + # TODO(tbergeron): Skip generating these for inner part of join + ( + left_relative_predicates, + right_relative_predicates, + ) = _get_relative_predicates(left_predicates, right_predicates) + + combined_predicates = [] + if left_predicates or right_predicates: + joined_predicates = _join_predicates( + left_predicates, right_predicates, join_type=how + ) + combined_predicates = list(joined_predicates) # builder expects mutable list + + left_mask = left_relative_predicates if how in ["right", "outer"] else None + right_mask = right_relative_predicates if how in ["left", "outer"] else None + joined_columns = [ + _mask_value(left.get_column(key), left_mask).name(map_left_id(key)) + for key in left.column_names.keys() + ] + [ + _mask_value(right.get_column(key), right_mask).name(map_right_id(key)) + for key in right.column_names.keys() + ] + + hidden_ordering_columns = [] + new_ordering = core.ExpressionOrdering() + if left._ordering and right._ordering: + # These ordering columns will be present in the ArrayValue, as we + # haven't hidden any value / index column(s). Code that is aware of + # which columns are index columns / value columns columns will need to + # add the previous columns to hidden columns. + new_ordering = left._ordering.with_ordering_columns( + [ + col_ref.with_name(map_left_id(col_ref.column_id)) + for col_ref in left._ordering.ordering_value_columns + ] + + [ + col_ref.with_name(map_right_id(col_ref.column_id)) + for col_ref in right._ordering.ordering_value_columns + ] + ) + + hidden_ordering_columns = [ + left._get_hidden_ordering_column(key.column_id).name( + map_left_id(key.column_id) + ) + for key in left._ordering.ordering_value_columns + if key.column_id in left._hidden_ordering_column_names.keys() + ] + [ + right._get_hidden_ordering_column(key.column_id).name( + map_right_id(key.column_id) + ) + for key in right._ordering.ordering_value_columns + if key.column_id in right._hidden_ordering_column_names.keys() + ] + + left_ordering_id = left._ordering.ordering_id + if left_ordering_id: + new_ordering = new_ordering.with_ordering_id(map_left_id(left_ordering_id)) + if left_ordering_id in left._hidden_ordering_column_names.keys(): + hidden_ordering_columns.append( + left._get_hidden_ordering_column(left_ordering_id).name( + map_left_id(left_ordering_id) + ) + ) + + joined_expr = core.ArrayValue( + left._session, + left.table, + columns=joined_columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=new_ordering, + predicates=combined_predicates, + ) + return joined_expr, ( + lambda key: map_left_id(key), + lambda key: map_right_id(key), + ) + + +def map_left_id(left_side_id): + return f"{left_side_id}_x" + + +def map_right_id(right_side_id): + return f"{right_side_id}_y" + + +def _mask_value( + value: ibis_types.Value, + predicates: typing.Optional[typing.Sequence[ibis_types.BooleanValue]] = None, +): + if predicates: + return ( + ibis.case() + .when(_reduce_predicate_list(predicates), value) + .else_(ibis.null()) + .end() + ) + return value + + +def _join_predicates( + left_predicates: typing.Collection[ibis_types.BooleanValue], + right_predicates: typing.Collection[ibis_types.BooleanValue], + join_type: str = "outer", +) -> typing.Tuple[ibis_types.BooleanValue, ...]: + """Combines predicates lists for each side of a join.""" + if join_type == "outer": + if not left_predicates: + return () + if not right_predicates: + return () + # TODO(tbergeron): Investigate factoring out common predicates + joined_predicates = _reduce_predicate_list(left_predicates).__or__( + _reduce_predicate_list(right_predicates) + ) + return (joined_predicates,) + if join_type == "left": + return tuple(left_predicates) + if join_type == "inner": + _, right_relative_predicates = _get_relative_predicates( + left_predicates, right_predicates + ) + return (*left_predicates, *right_relative_predicates) + else: + raise ValueError("Unsupported join_type: " + join_type) + + +def _get_relative_predicates( + left_predicates: typing.Collection[ibis_types.BooleanValue], + right_predicates: typing.Collection[ibis_types.BooleanValue], +) -> tuple[ + typing.Tuple[ibis_types.BooleanValue, ...], + typing.Tuple[ibis_types.BooleanValue, ...], +]: + """Get predicates that apply to only one side of the join. Not strictly necessary but simplifies resulting query.""" + left_relative_predicates = tuple(left_predicates) or () + right_relative_predicates = tuple(right_predicates) or () + if left_predicates and right_predicates: + # Factor out common predicates needed for left/right column masking + left_relative_predicates = tuple(set(left_predicates) - set(right_predicates)) + right_relative_predicates = tuple(set(right_predicates) - set(left_predicates)) + return (left_relative_predicates, right_relative_predicates) + + +def _reduce_predicate_list( + predicate_list: typing.Collection[ibis_types.BooleanValue], +) -> ibis_types.BooleanValue: + """Converts a list of predicates BooleanValues into a single BooleanValue.""" + if len(predicate_list) == 0: + raise ValueError("Cannot reduce empty list of predicates") + if len(predicate_list) == 1: + (item,) = predicate_list + return item + return functools.reduce(lambda acc, pred: acc.__and__(pred), predicate_list) diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py new file mode 100644 index 00000000000..34873e6f7ee --- /dev/null +++ b/bigframes/core/joins/single_column.py @@ -0,0 +1,281 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers to join ArrayValue objects.""" + +from __future__ import annotations + +import typing +from typing import Callable, Literal, Tuple + +import ibis +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types + +import bigframes.core as core +import bigframes.core.guid +import bigframes.core.joins.row_identity +import bigframes.core.ordering + + +def join_by_column( + left: core.ArrayValue, + left_column_ids: typing.Sequence[str], + right: core.ArrayValue, + right_column_ids: typing.Sequence[str], + *, + how: Literal[ + "inner", + "left", + "outer", + "right", + ], + sort: bool = False, + get_both_join_key_cols: bool = False, +) -> Tuple[ + core.ArrayValue, + typing.Sequence[str], + Tuple[Callable[[str], str], Callable[[str], str]], +]: + """Join two expressions by column equality. + + Arguments: + left: Expression for left table to join. + left_column_ids: Column IDs (not label) to join by. + right: Expression for right table to join. + right_column_ids: Column IDs (not label) to join by. + how: The type of join to perform. + get_both_join_key_cols: if set to True, returned column ids will contain + both left and right join key columns. + + Returns: + The joined expression and the objects needed to interpret it. + + * ArrayValue: Joined table with all columns from left and right. + * Sequence[str]: Column IDs of the coalesced join columns. Sometimes either the + left/right table will have missing rows. This column pulls the + non-NULL value from either left/right. + If get_both_join_key_cols is True, will return uncombined left and + right key columns. + * Tuple[Callable, Callable]: For a given column ID from left or right, + respectively, return the new column id from the combined expression. + """ + + if ( + how in bigframes.core.joins.row_identity.SUPPORTED_ROW_IDENTITY_HOW + and left.table.equals(right.table) + # Compare ibis expressions for left/right columns because its possible that + # they both have the same names but were modified in different ways. + and all( + left.get_any_column(lcol).equals(right.get_any_column(rcol)) + for lcol, rcol in zip(left_column_ids, right_column_ids) + ) + ): + combined_expr, ( + get_column_left, + get_column_right, + ) = bigframes.core.joins.row_identity.join_by_row_identity(left, right, how=how) + original_ordering = combined_expr._ordering + else: + # Generate offsets if non-default ordering is applied + # Assumption, both sides are totally ordered, otherwise offsets will be nondeterministic + left_table = left.to_ibis_expr( + ordering_mode="ordered_col", order_col_name=core.ORDER_ID_COLUMN + ) + right_table = right.to_ibis_expr( + ordering_mode="ordered_col", order_col_name=core.ORDER_ID_COLUMN + ) + join_conditions = [ + value_to_join_key(left_table[left_index]) + == value_to_join_key(right_table[right_index]) + for left_index, right_index in zip(left_column_ids, right_column_ids) + ] + + combined_table = ibis.join( + left_table, + right_table, + predicates=join_conditions, + how=how, + lname="{name}_x", + rname="{name}_y", + ) + + def get_column_left(key: str) -> str: + if ( + how == "inner" + and key in left_column_ids + and key in combined_table.columns + ): + # Ibis doesn't rename the column if the values are guaranteed + # to be equal on left and right (because they're part of an + # inner join condition). See: + # https://github.com/ibis-project/ibis/pull/4651 + pass + elif key in right_table.columns: + key = f"{key}_x" + + return key + + def get_column_right(key: str) -> str: + if ( + how == "inner" + and key in right_column_ids + and key in combined_table.columns + ): + # Ibis doesn't rename the column if the values are guaranteed + # to be equal on left and right (because they're part of an + # inner join condition). See: + # https://github.com/ibis-project/ibis/pull/4651 + pass + elif key in left_table.columns: + key = f"{key}_y" + + return key + + left_ordering_encoding_size = ( + left._ordering.ordering_encoding_size + or bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH + ) + right_ordering_encoding_size = ( + right._ordering.ordering_encoding_size + or bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH + ) + + # Preserve original ordering accross joins. + left_order_id = get_column_left(core.ORDER_ID_COLUMN) + right_order_id = get_column_right(core.ORDER_ID_COLUMN) + new_order_id_col = _merge_order_ids( + combined_table[left_order_id], + left_ordering_encoding_size, + combined_table[right_order_id], + right_ordering_encoding_size, + how, + ) + new_order_id = new_order_id_col.get_name() + if new_order_id is None: + raise ValueError("new_order_id unexpectedly has no name") + hidden_columns = (new_order_id_col,) + original_ordering = core.ExpressionOrdering( + ordering_id_column=core.OrderingColumnReference(new_order_id) + if (new_order_id_col is not None) + else None, + ordering_encoding_size=left_ordering_encoding_size + + right_ordering_encoding_size, + ) + combined_expr = core.ArrayValue( + left._session, + combined_table, + hidden_ordering_columns=hidden_columns, + ) + + join_key_cols: list[ibis_types.Value] = [] + for lcol, rcol in zip(left_column_ids, right_column_ids): + if get_both_join_key_cols: + join_key_cols.append( + combined_expr.get_column(get_column_left(lcol)).name( + bigframes.core.guid.generate_guid(prefix="index_") + ) + ) + join_key_cols.append( + combined_expr.get_column(get_column_right(rcol)).name( + bigframes.core.guid.generate_guid(prefix="index_") + ) + ) + else: + if how == "left" or how == "inner": + join_key_cols.append( + combined_expr.get_column(get_column_left(lcol)).name( + bigframes.core.guid.generate_guid(prefix="index_") + ) + ) + elif how == "right": + join_key_cols.append( + combined_expr.get_column(get_column_right(rcol)).name( + bigframes.core.guid.generate_guid(prefix="index_") + ) + ) + elif how == "outer": + # The left index and the right index might contain null values, for + # example due to an outer join with different numbers of rows. Coalesce + # these to take the index value from either column. + # Use a random name in case the left index and the right index have the + # same name. In such a case, _x and _y suffixes will already be used. + join_key_cols.append( + ibis.coalesce( + combined_expr.get_column(get_column_left(lcol)), + combined_expr.get_column(get_column_right(rcol)), + ).name(bigframes.core.guid.generate_guid(prefix="index_")) + ) + else: + raise ValueError(f"Unexpected join type: {how}") + + # We could filter out the original join columns, but predicates/ordering + # might still reference them in implicit joins. + columns = ( + join_key_cols + + [ + combined_expr.get_column(get_column_left(key)) + for key in left.column_names.keys() + ] + + [ + combined_expr.get_column(get_column_right(key)) + for key in right.column_names.keys() + ] + ) + + if sort: + ordering = original_ordering.with_ordering_columns( + [ + core.OrderingColumnReference(join_key_col.get_name()) + for join_key_col in join_key_cols + ] + ) + else: + ordering = original_ordering + + combined_expr_builder = combined_expr.builder() + combined_expr_builder.columns = columns + combined_expr_builder.ordering = ordering + combined_expr = combined_expr_builder.build() + return ( + combined_expr, + [key.get_name() for key in join_key_cols], + (get_column_left, get_column_right), + ) + + +def value_to_join_key(value: ibis_types.Value): + """Converts nullable values to non-null string SQL will not match null keys together - but pandas does.""" + if not value.type().is_string(): + value = value.cast(ibis_dtypes.str) + return value.fillna(ibis_types.literal("$NULL_SENTINEL$")) + + +def _merge_order_ids( + left_id: ibis_types.Value, + left_encoding_size: int, + right_id: ibis_types.Value, + right_encoding_size: int, + how: str, +) -> ibis_types.StringValue: + if how == "right": + return _merge_order_ids( + right_id, right_encoding_size, left_id, left_encoding_size, "left" + ) + return ( + ( + bigframes.core.ordering.stringify_order_id(left_id, left_encoding_size) + + bigframes.core.ordering.stringify_order_id(right_id, right_encoding_size) + ) + ).name(bigframes.core.guid.generate_guid(prefix="bigframes_ordering_id_")) diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py new file mode 100644 index 00000000000..6f8d35e52ae --- /dev/null +++ b/bigframes/core/ordering.py @@ -0,0 +1,181 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +import math +import typing +from typing import Optional, Sequence + +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types + +# TODO(tbergeron): Encode more efficiently +ORDERING_ID_STRING_BASE: int = 10 +# Sufficient to store any value up to 2^63 +DEFAULT_ORDERING_ID_LENGTH: int = math.ceil(63 * math.log(2, ORDERING_ID_STRING_BASE)) + + +class OrderingDirection(Enum): + ASC = 1 + DESC = 2 + + def reverse(self): + if self == OrderingDirection.ASC: + return OrderingDirection.DESC + else: + return OrderingDirection.ASC + + @property + def is_ascending(self) -> bool: + return self == OrderingDirection.ASC + + +@dataclass(frozen=True) +class OrderingColumnReference: + """References a column and how to order with respect to values in that column.""" + + column_id: str + direction: OrderingDirection = OrderingDirection.ASC + na_last: bool = True + + def with_name(self, name: str): + return OrderingColumnReference(name, self.direction, self.na_last) + + def with_reverse(self): + return OrderingColumnReference( + self.column_id, self.direction.reverse(), not self.na_last + ) + + +@dataclass(frozen=True) +class ExpressionOrdering: + """Immutable object that holds information about the ordering of rows in a ArrayValue object.""" + + ordering_value_columns: Sequence[OrderingColumnReference] = () + ordering_id_column: Optional[OrderingColumnReference] = None + is_sequential: bool = False + # Encoding size must be tracked in order to know what how to combine ordering ids across tables (eg how much to pad when combining different length). + # Also will be needed to determine when length is too large and need to compact ordering id with a ROW_NUMBER operation. + ordering_encoding_size: int = DEFAULT_ORDERING_ID_LENGTH + + def with_is_sequential(self, is_sequential: bool): + """Create a copy that is marked as non-sequential. + + This is useful when filtering, but not sorting, an expression. + """ + return ExpressionOrdering( + self.ordering_value_columns, + self.ordering_id_column, + is_sequential, + ordering_encoding_size=self.ordering_encoding_size, + ) + + def with_ordering_columns( + self, + ordering_value_columns: Sequence[OrderingColumnReference] = (), + stable: bool = False, + ): + """Creates a new ordering that preserves ordering id, but replaces ordering value column list.""" + if stable: + col_ids_new = [ + ordering_ref.column_id for ordering_ref in ordering_value_columns + ] + # Only reference each column once, so discard old referenc if there is a new reference + old_ordering_keep = [ + ordering_ref + for ordering_ref in self.ordering_value_columns + if ordering_ref.column_id not in col_ids_new + ] + new_ordering = (*ordering_value_columns, *old_ordering_keep) + else: # Not stable, so discard old ordering completely + new_ordering = tuple(ordering_value_columns) + return ExpressionOrdering( + new_ordering, + self.ordering_id_column, + is_sequential=False, + ordering_encoding_size=self.ordering_encoding_size, + ) + + def with_ordering_id(self, ordering_id: str): + """Creates a new ordering that preserves other properties, but with a different ordering id. + + Useful when reprojecting ordering for implicit joins. + """ + return ExpressionOrdering( + self.ordering_value_columns, + OrderingColumnReference(ordering_id), + is_sequential=self.is_sequential, + ordering_encoding_size=self.ordering_encoding_size, + ) + + def with_reverse(self): + """Reverses the ordering.""" + return ExpressionOrdering( + tuple([col.with_reverse() for col in self.ordering_value_columns]), + self.ordering_id_column.with_reverse() + if self.ordering_id_column is not None + else None, + is_sequential=False, + ordering_encoding_size=self.ordering_encoding_size, + ) + + @property + def ordering_id(self) -> Optional[str]: + return self.ordering_id_column.column_id if self.ordering_id_column else None + + @property + def order_id_defined(self) -> bool: + """True if ordering is fully defined in ascending order by its ordering id.""" + return bool( + self.ordering_id_column + and (not self.ordering_value_columns) + and self.ordering_id_column.direction == OrderingDirection.ASC + ) + + @property + def all_ordering_columns(self) -> Sequence[OrderingColumnReference]: + return ( + list(self.ordering_value_columns) + if self.ordering_id_column is None + else [*self.ordering_value_columns, self.ordering_id_column] + ) + + +def stringify_order_id( + order_id: ibis_types.Value, length: int = DEFAULT_ORDERING_ID_LENGTH +) -> ibis_types.StringValue: + """Converts an order id value to string if it is not already a string. MUST produced fixed-length strings.""" + if order_id.type().is_int64(): + # This is very inefficient encoding base-10 string uses only 10 characters per byte(out of 256 bit combinations) + # Furthermore, if know tighter bounds on order id are known, can produce smaller strings. + # 19 characters chosen as it can represent any positive Int64 in base-10 + # For missing values, ":" * 19 is used as it is larger than any other value this function produces, so null values will be last. + string_order_id = ( + typing.cast( + ibis_types.StringValue, + typing.cast(ibis_types.IntegerValue, order_id).cast(ibis_dtypes.string), + ) + .lpad(length, "0") + .fillna(ibis_types.literal(":" * length)) + ) + else: + string_order_id = ( + typing.cast(ibis_types.StringValue, order_id) + .lpad(length, "0") + .fillna(ibis_types.literal(":" * length)) + ) + return typing.cast(ibis_types.StringValue, string_order_id) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py new file mode 100644 index 00000000000..1443a8e08fc --- /dev/null +++ b/bigframes/core/reshape/__init__.py @@ -0,0 +1,61 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import typing +from typing import Iterable, Literal, Union + +import bigframes.dataframe +import bigframes.series + + +@typing.overload +def concat( + objs: Iterable[bigframes.dataframe.DataFrame], *, join, ignore_index +) -> bigframes.dataframe.DataFrame: + ... + + +@typing.overload +def concat( + objs: Iterable[bigframes.series.Series], *, join, ignore_index +) -> bigframes.series.Series: + ... + + +def concat( + objs: Union[ + Iterable[bigframes.dataframe.DataFrame], Iterable[bigframes.series.Series] + ], + *, + join: Literal["inner", "outer"] = "outer", + ignore_index: bool = False, +) -> Union[bigframes.dataframe.DataFrame, bigframes.series.Series]: + contains_dataframes = any( + isinstance(x, bigframes.dataframe.DataFrame) for x in objs + ) + if not contains_dataframes: + # Special case, all series, so align everything into single column even if labels don't match + series = typing.cast(typing.Iterable[bigframes.series.Series], objs) + names = {s.name for s in series} + # For series case, labels are stripped if they don't all match + if len(names) > 1: + blocks = [s._block.with_column_labels([None]) for s in series] + else: + blocks = [s._block for s in series] + block = blocks[0].concat(blocks[1:], how=join, ignore_index=ignore_index) + return bigframes.series.Series(block) + blocks = [obj._block for obj in objs] + block = blocks[0].concat(blocks[1:], how=join, ignore_index=ignore_index) + return bigframes.dataframe.DataFrame(block) diff --git a/bigframes/core/scalar.py b/bigframes/core/scalar.py new file mode 100644 index 00000000000..67ced525b99 --- /dev/null +++ b/bigframes/core/scalar.py @@ -0,0 +1,48 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing +from typing import Any + +import ibis.expr.types as ibis_types + +if typing.TYPE_CHECKING: + import bigframes.session + + +class DeferredScalar: + """A deferred scalar object.""" + + def __init__(self, value: ibis_types.Scalar, session: bigframes.session.Session): + self._value = value + self._session = session + + def __repr__(self) -> str: + """Converts a Series to a string.""" + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + return repr(self.compute()) + + def compute(self) -> Any: + """Executes deferred operations and downloads the resulting scalar.""" + result, _ = self._session._start_query(self._value.compile()) + df = self._session._rows_to_dataframe(result) + return df.iloc[0, 0] + + +# All public APIs return Any at present +# Later implementation may sometimes return a lazy scalar +Scalar = Any diff --git a/bigframes/core/window/__init__.py b/bigframes/core/window/__init__.py new file mode 100644 index 00000000000..8994004e0b8 --- /dev/null +++ b/bigframes/core/window/__init__.py @@ -0,0 +1,85 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing + +import bigframes.core as core +import bigframes.core.blocks as blocks +import bigframes.operations.aggregations as agg_ops +import third_party.bigframes_vendored.pandas.core.window.rolling as vendored_pandas_rolling + +if typing.TYPE_CHECKING: + from bigframes.series import Series + + +class Window(vendored_pandas_rolling.Window): + __doc__ = vendored_pandas_rolling.Window.__doc__ + + # TODO(tbergeron): Windows with groupings should create multi-indexed results + + def __init__( + self, + block: blocks.Block, + window_spec: core.WindowSpec, + value_column_id: str, + ): + self._block = block + self._window_spec = window_spec + self._value_column_id = value_column_id + + def count(self) -> Series: + return self._apply_aggregate(agg_ops.count_op) + + def sum(self) -> Series: + return self._apply_aggregate(agg_ops.sum_op) + + def mean(self) -> Series: + return self._apply_aggregate(agg_ops.mean_op) + + def var(self) -> Series: + return self._apply_aggregate(agg_ops.var_op) + + def std(self) -> Series: + return self._apply_aggregate(agg_ops.std_op) + + def max(self) -> Series: + return self._apply_aggregate(agg_ops.max_op) + + def min(self) -> Series: + return self._apply_aggregate(agg_ops.min_op) + + def _apply_aggregate( + self, + op: agg_ops.AggregateOp, + ) -> Series: + block = self._block + label = block.col_id_to_label[self._value_column_id] + block, result_id = block.apply_window_op( + self._value_column_id, op, self._window_spec, result_label=label + ) + + if self._window_spec.grouping_keys: + original_index_ids = block.index_columns + block = block.reset_index(drop=False) + index_ids = ( + *[col for col in self._window_spec.grouping_keys], + *original_index_ids, + ) + block = block.set_index(col_ids=index_ids) + + from bigframes.series import Series + + return Series(block.select_column(result_id)) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py new file mode 100644 index 00000000000..0155aafa0b0 --- /dev/null +++ b/bigframes/dataframe.py @@ -0,0 +1,1626 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DataFrame is a two dimensional data structure.""" + +from __future__ import annotations + +import random +import re +import textwrap +import typing +from typing import ( + Callable, + Iterable, + List, + Literal, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) + +import google.cloud.bigquery as bigquery +import ibis.expr.datatypes as ibis_dtypes +import numpy +import pandas as pd +import typing_extensions + +import bigframes +import bigframes._config.display_options as display_options +import bigframes.core +import bigframes.core.block_transforms as block_ops +import bigframes.core.blocks as blocks +import bigframes.core.groupby as groupby +import bigframes.core.guid +import bigframes.core.indexers as indexers +import bigframes.core.indexes as indexes +import bigframes.core.joins as joins +import bigframes.core.ordering as order +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops +import bigframes.series +import bigframes.series as bf_series +import third_party.bigframes_vendored.pandas.core.frame as vendored_pandas_frame +import third_party.bigframes_vendored.pandas.io.common as vendored_pandas_io_common +import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing + +if typing.TYPE_CHECKING: + import bigframes.session + + +# BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. +# TODO(tbergeron): Convert to bytes-based limit +MAX_INLINE_DF_SIZE = 5000 + +LevelsType = typing.Union[str, int, typing.Sequence[typing.Union[str, int]]] +SingleItemValue = Union[bigframes.series.Series, int, float, Callable] + + +# Inherits from pandas DataFrame so that we can use the same docstrings. +class DataFrame(vendored_pandas_frame.DataFrame): + __doc__ = vendored_pandas_frame.DataFrame.__doc__ + + def __init__( + self, + data=None, + index: vendored_pandas_typing.Axes | None = None, + columns: vendored_pandas_typing.Axes | None = None, + dtype: typing.Optional[ + bigframes.dtypes.DtypeString | bigframes.dtypes.Dtype + ] = None, + copy: typing.Optional[bool] = None, + *, + session: typing.Optional[bigframes.session.Session] = None, + ): + if copy is not None and not copy: + raise ValueError("DataFrame constructor only supports copy=True") + + # Check to see if constructing from BigQuery-backed objects before + # falling back to pandas constructor + block = None + if isinstance(data, blocks.Block): + block = data + + elif isinstance(data, DataFrame): + block = data._get_block() + + # Dict of Series + elif ( + _is_dict_like(data) + and len(data) >= 1 + and any(isinstance(data[key], bf_series.Series) for key in data.keys()) + ): + if not all(isinstance(data[key], bf_series.Series) for key in data.keys()): + # TODO(tbergeron): Support local list/series data by converting to memtable. + raise NotImplementedError("Cannot mix Series with other types.") + keys = list(data.keys()) + first_label, first_series = keys[0], data[keys[0]] + block = ( + typing.cast(bf_series.Series, first_series) + ._get_block() + .with_column_labels([first_label]) + ) + + for key in keys[1:]: + other = typing.cast(bf_series.Series, data[key]) + other_block = other._block.with_column_labels([key]) + # Pandas will keep original sorting if all indices are aligned. + # We cannot detect this easily however, and so always sort on index + result_index, _ = block.index.join( # type:ignore + other_block.index, how="outer", sort=True + ) + block = result_index._block + + if block: + if index: + raise NotImplementedError( + "DataFrame 'index' constructor parameter not supported when passing BigQuery-backed objects" + ) + if columns: + block = block.select_columns(list(columns)) # type:ignore + if dtype: + block = block.multi_apply_unary_op( + block.value_columns, ops.AsTypeOp(dtype) + ) + self._block = block + + else: + import bigframes.pandas + + pd_dataframe = pd.DataFrame( + data=data, + index=index, # type:ignore + columns=columns, # type:ignore + dtype=dtype, # type:ignore + ) + if pd_dataframe.size < MAX_INLINE_DF_SIZE: + self._block = blocks.block_from_local( + pd_dataframe, session or bigframes.pandas.get_global_session() + ) + elif session: + self._block = session.read_pandas(pd_dataframe)._get_block() + else: + self._block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() + self._query_job: Optional[bigquery.QueryJob] = None + + def __dir__(self): + return dir(type(self)) + self._block.column_labels + + def _ipython_key_completions_(self) -> List[str]: + return list([label for label in self._block.column_labels if label]) + + def _find_indices( + self, + columns: Union[blocks.Label, Sequence[blocks.Label]], + tolerance: bool = False, + ) -> Sequence[int]: + """Find corresponding indices in df._block.column_labels for column name(s). + Order is kept the same as input names order. + + Args: + columns: column name(s) + tolerance: True to pass through columns not found. False to raise + ValueError. + """ + col_ids = self._sql_names(columns, tolerance) + return [self._block.value_columns.index(col_id) for col_id in col_ids] + + def _resolve_label_exact(self, label) -> str: + matches = self._block.label_to_col_id.get(label, []) + if len(matches) != 1: + raise ValueError("Index data must be 1-dimensional") + return matches[0] + + def _sql_names( + self, + columns: Union[blocks.Label, Sequence[blocks.Label], pd.Index], + tolerance: bool = False, + ) -> Sequence[str]: + """Retrieve sql name (column name in BQ schema) of column(s).""" + labels = columns if _is_list_like(columns) else [columns] # type:ignore + results: Sequence[str] = [] + for label in labels: + col_ids = self._block.label_to_col_id.get(label, []) + if not tolerance and len(col_ids) == 0: + raise ValueError(f"Column name {label} doesn't exist") + results = (*results, *col_ids) + return results + + @property + def index( + self, + ) -> indexes.Index: + return indexes.Index(self) + + @property + def loc(self) -> indexers.LocDataFrameIndexer: + return indexers.LocDataFrameIndexer(self) + + @property + def iloc(self) -> indexers.ILocDataFrameIndexer: + return indexers.ILocDataFrameIndexer(self) + + @property + def dtypes(self) -> pd.Series: + return pd.Series(data=self._block.dtypes, index=self._block.column_labels) + + @property + def columns(self) -> pd.Index: + return self.dtypes.index + + @property + def shape(self) -> Tuple[int, int]: + return self._block.shape + + @property + def size(self) -> int: + rows, cols = self.shape + return rows * cols + + @property + def ndim(self) -> int: + return 2 + + @property + def empty(self) -> bool: + return self.size == 0 + + @property + def values(self) -> numpy.ndarray: + return self.to_numpy() + + def __len__(self): + rows, _ = self.shape + return rows + + def astype( + self, + dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], + ) -> DataFrame: + return self._apply_to_rows(ops.AsTypeOp(dtype)) + + def to_sql_query( + self, always_include_index: bool + ) -> Tuple[str, List[Tuple[str, bool]]]: + """Compiles this dataframe's expression tree to SQL, optionally + including unnamed index columns + + Args: + always_include_index: whether to include unnamed index columns. + If False, only named indexes are included. + + Returns: a tuple of (sql_string, index_column_list). Each entry in the + index column list is a tuple of (column_name, named). If named is + is false, then the column name exists only in SQL""" + # Has to be unordered as it is impossible to order the sql without + # including metadata columns in selection with ibis. + ibis_expr = self._block.expr.to_ibis_expr(ordering_mode="unordered") + column_labels = self._block.column_labels + + # TODO(swast): Need to have a better way of controlling when to include + # the index or not. + index_has_names = all([name is not None for name in self.index.names]) + if index_has_names: + column_labels = column_labels + list(self.index.names) + elif always_include_index: + # In this mode include the index even if it is a nameless generated + # column like 'bigframes_index_0' + index_labels = [] + unnamed_index_count = 0 + for index_label in self._block.index_labels: + if index_label is None: + index_labels.append( + indexes.INDEX_COLUMN_ID.format(unnamed_index_count), + ) + unnamed_index_count += 1 + else: + index_labels.append(index_label) + + column_labels = column_labels + typing.cast( + List[Optional[str]], index_labels + ) + + column_labels_deduped = typing.cast( + List[str], + vendored_pandas_io_common.dedup_names( + column_labels, is_potential_multiindex=False + ), + ) + column_ids = self._block.value_columns + substitutions = {} + for column_id, column_label in zip(column_ids, column_labels_deduped): + # TODO(swast): Do we need to further escape this, or can we rely on + # the BigQuery unicode column name feature? + substitutions[column_id] = column_label + + index_cols: List[Tuple[str, bool]] = [] + first_index_offset = len(self._block.column_labels) + if index_has_names or always_include_index: + for i, index_col in enumerate(self._block.index_columns): + offset = first_index_offset + i + substitutions[index_col] = column_labels_deduped[offset] + index_cols = [ + (label, index_has_names) + for label in column_labels_deduped[first_index_offset:] + ] + else: + ibis_expr = ibis_expr.drop(*self._block.index_columns) + + ibis_expr = ibis_expr.relabel(substitutions) + return typing.cast(str, ibis_expr.compile()), index_cols + + @property + def sql(self) -> str: + """Compiles this dataframe's expression tree to SQL""" + sql, _ = self.to_sql_query(always_include_index=False) + return sql + + @property + def query_job(self) -> Optional[bigquery.QueryJob]: + """BigQuery job metadata for the most recent query.""" + return self._query_job + + @typing.overload + def __getitem__(self, key: bigframes.series.Series) -> DataFrame: + ... + + @typing.overload + def __getitem__(self, key: Sequence[blocks.Label]) -> DataFrame: # type:ignore + ... + + @typing.overload + def __getitem__(self, key: pd.Index) -> DataFrame: # type:ignore + ... + + @typing.overload + def __getitem__(self, key: blocks.Label) -> bigframes.series.Series: # type:ignore + ... + + def __getitem__( + self, + key: Union[ + blocks.Label, + Sequence[blocks.Label], + # Index of column labels can be treated the same as a sequence of column labels. + pd.Index, + bigframes.series.Series, + ], + ) -> Union[bigframes.series.Series, "DataFrame"]: + """Gets the specified column(s) from the DataFrame.""" + # NOTE: This implements the operations described in + # https://pandas.pydata.org/docs/getting_started/intro_tutorials/03_subset_data.html + + if isinstance(key, bigframes.series.Series): + return self._getitem_bool_series(key) + + sql_names = self._sql_names(key) + # Only input is a str and only find one column, returns a Series + if isinstance(key, str) and len(sql_names) == 1: + return bigframes.series.Series(self._block.select_column(sql_names[0])) + + # Select a subset of columns or re-order columns. + # In Ibis after you apply a projection, any column objects from the + # table before the projection can't be combined with column objects + # from the table after the projection. This is because the table after + # a projection is considered a totally separate table expression. + # + # This is unexpected behavior for a pandas user, who expects their old + # Series objects to still work with the new / mutated DataFrame. We + # avoid applying a projection in Ibis until it's absolutely necessary + # to provide pandas-like semantics. + # TODO(swast): Do we need to apply implicit join when doing a + # projection? + + # Select a number of columns as DF. + key = key if _is_list_like(key) else [key] # type:ignore + + selected_ids: Tuple[str, ...] = () + for label in key: + col_ids = self._block.label_to_col_id.get(label, []) + selected_ids = (*selected_ids, *col_ids) + + return DataFrame(self._block.select_columns(selected_ids)) + + # Bool Series selects rows + def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: + if not key._to_ibis_expr().type() == ibis_dtypes.bool: + raise ValueError("Only boolean series currently supported for indexing.") + # TODO: enforce stricter alignment + combined_index, ( + get_column_left, + get_column_right, + ) = self._block.index.join(key._block.index, how="left") + block = combined_index._block + filter_col_id = get_column_right(key._value_column) + block = block.filter(filter_col_id) + block = block.drop_columns([filter_col_id]) + return DataFrame(block) + + def __getattr__(self, key: str): + if key in self._block.column_labels: + return self.__getitem__(key) + elif hasattr(pd.DataFrame, key): + raise NotImplementedError( + textwrap.dedent( + f""" + BigQuery DataFrames has not yet implemented an equivalent to + 'pandas.DataFrame.{key}'. Please check + https://github.com/googleapis/python-bigquery-dataframes/issues for + existing feature requests, or file your own. + Please include information about your use case, as well as + relevant code snippets. + """ + ) + ) + else: + raise AttributeError(key) + + def __repr__(self) -> str: + """Converts a DataFrame to a string. Calls compute. + + Only represents the first ``bigframes.options.display.max_rows``. + """ + opts = bigframes.options.display + max_results = opts.max_rows + # TODO(swast): pass max_columns and get the true column count back. Maybe + # get 1 more column than we have requested so that pandas can add the + # ... for us? + pandas_df, row_count = self._retrieve_repr_request_results(max_results) + column_count = len(pandas_df.columns) + + with display_options.pandas_repr(opts): + repr_string = repr(pandas_df) + + # Modify the end of the string to reflect count. + lines = repr_string.split("\n") + pattern = re.compile("\\[[0-9]+ rows x [0-9]+ columns\\]") + if pattern.match(lines[-1]): + lines = lines[:-2] + + if row_count > len(lines) - 1: + lines.append("...") + + lines.append("") + lines.append(f"[{row_count} rows x {column_count} columns]") + return "\n".join(lines) + + def _repr_html_(self) -> str: + """ + Returns an html string primarily for use by notebooks for displaying + a representation of the DataFrame. Displays 20 rows by default since + many notebooks are not configured for large tables. + """ + opts = bigframes.options.display + max_results = bigframes.options.display.max_rows + # TODO(swast): pass max_columns and get the true column count back. Maybe + # get 1 more column than we have requested so that pandas can add the + # ... for us? + pandas_df, row_count = self._retrieve_repr_request_results(max_results) + column_count = len(pandas_df.columns) + + with display_options.pandas_repr(opts): + # _repr_html_ stub is missing so mypy thinks it's a Series. Ignore mypy. + html_string = pandas_df._repr_html_() # type:ignore + + html_string += f"[{row_count} rows x {column_count} columns in total]" + return html_string + + def _retrieve_repr_request_results( + self, max_results: int + ) -> Tuple[pd.DataFrame, int]: + """ + Retrieves a pandas dataframe containing only max_results many rows for use + with printing methods. + + Returns a tuple of the dataframe and the overall number of rows of the query. + """ + # TODO(swast): Select a subset of columns if max_columns is less than the + # number of columns in the schema. + count = self.shape[0] + if count > max_results: + head_df = self.head(n=max_results) + computed_df, query_job = head_df._block.compute(max_results=max_results) + else: + head_df = self + computed_df, query_job = head_df._block.compute() + + formatted_df = computed_df.set_axis(self._block.column_labels, axis=1) + # don't update details when the cache is hit + if self.query_job is None or not query_job.cache_hit: + self._query_job = query_job + # we reset the axis and substitute the bf index name for the default + formatted_df.index.name = self.index.name + return formatted_df, count + + def __setitem__(self, key: str, value: SingleItemValue): + """Modify or insert a column into the DataFrame. + + Note: This does **not** modify the original table the DataFrame was + derived from. + """ + df = self._assign_single_item(key, value) + self._set_block(df._get_block()) + + def _apply_binop( + self, + other: float | int | bigframes.series.Series, + op, + axis: str | int = "columns", + ): + if isinstance(other, (float, int)): + return self._apply_scalar_binop(other, op) + elif isinstance(other, bigframes.series.Series): + return self._apply_series_binop(other, op, axis=axis) + raise NotImplementedError( + f"binary operation is not implemented on the second operand of type {type(other).__name__}." + ) + + def _apply_scalar_binop(self, other: float | int, op: ops.BinaryOp) -> DataFrame: + block = self._block + partial_op = ops.BinopPartialRight(op, other) + for column_id, label in zip( + self._block.value_columns, self._block.column_labels + ): + block, _ = block.apply_unary_op(column_id, partial_op, result_label=label) + block = block.drop_columns([column_id]) + return DataFrame(block) + + def _apply_series_binop( + self, + other: bigframes.series.Series, + op: ops.BinaryOp, + axis: str | int = "columns", + ) -> DataFrame: + if axis not in ("columns", "index", 0, 1): + raise ValueError(f"Invalid input: axis {axis}.") + + if axis in ("columns", 1): + raise NotImplementedError("Row Series operations haven't been supported.") + + joined_index, (get_column_left, get_column_right) = self._block.index.join( + other._block.index, how="outer" + ) + + series_column_id = other._value.get_name() + series_col = get_column_right(series_column_id) + block = joined_index._block + for column_id, label in zip( + self._block.value_columns, self._block.column_labels + ): + block, _ = block.apply_binary_op( + get_column_left(column_id), + series_col, + op, + result_label=label, + ) + block = block.drop_columns([get_column_left(column_id)]) + + block = block.drop_columns([series_col]) + block = block.with_index_labels(self.index.names) + return DataFrame(block) + + def le(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: + return self._apply_binop(other, ops.le_op, axis=axis) + + def lt(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: + return self._apply_binop(other, ops.lt_op, axis=axis) + + def ge(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: + return self._apply_binop(other, ops.ge_op, axis=axis) + + def gt(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: + return self._apply_binop(other, ops.gt_op, axis=axis) + + __lt__ = lt + + __le__ = le + + __gt__ = gt + + __ge__ = ge + + def add( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + # TODO(swast): Support fill_value parameter. + # TODO(swast): Support level parameter with MultiIndex. + return self._apply_binop(other, ops.add_op, axis=axis) + + __radd__ = __add__ = radd = add + + def sub( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.sub_op, axis=axis) + + __sub__ = subtract = sub + + def rsub( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.reverse(ops.sub_op), axis=axis) + + __rsub__ = rsub + + def mul( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.mul_op, axis=axis) + + __rmul__ = __mul__ = rmul = multiply = mul + + def truediv( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.div_op, axis=axis) + + div = divide = __truediv__ = truediv + + def rtruediv( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.reverse(ops.div_op), axis=axis) + + __rtruediv__ = rdiv = rtruediv + + def floordiv( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.floordiv_op, axis=axis) + + __floordiv__ = floordiv + + def rfloordiv( + self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.reverse(ops.floordiv_op), axis=axis) + + __rfloordiv__ = rfloordiv + + def mod(self, other: int | bigframes.series.Series, axis: str | int = "columns") -> DataFrame: # type: ignore + return self._apply_binop(other, ops.mod_op, axis=axis) + + def rmod(self, other: int | bigframes.series.Series, axis: str | int = "columns") -> DataFrame: # type: ignore + return self._apply_binop(other, ops.reverse(ops.mod_op), axis=axis) + + __mod__ = mod + + __rmod__ = rmod + + def compute(self) -> pd.DataFrame: + """Executes deferred operations and downloads the results.""" + # TODO(orrbradford): Optimize this in future. Potentially some cases where we can return the stored query job + df, query_job = self._block.compute() + self._query_job = query_job + return df.set_axis(self._block.column_labels, axis=1) + + def copy(self) -> DataFrame: + return DataFrame(self._block) + + def head(self, n: int = 5) -> DataFrame: + return typing.cast(DataFrame, self.iloc[:n]) + + def tail(self, n: int = 5) -> DataFrame: + return typing.cast(DataFrame, self.iloc[-n:]) + + def drop(self, *, columns: Union[str, Iterable[str]]) -> DataFrame: + if not _is_list_like(columns): + columns = [columns] # type:ignore + columns = list(columns) + + block = self._block.drop_columns(self._sql_names(columns)) + return DataFrame(block) + + def droplevel(self, level: LevelsType): + resolved_level_ids = self._resolve_levels(level) + return DataFrame(self._block.drop_levels(resolved_level_ids)) + + def reorder_levels(self, order: LevelsType): + resolved_level_ids = self._resolve_levels(order) + return DataFrame(self._block.reorder_levels(resolved_level_ids)) + + def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: + if _is_list_like(level): + levels = list(level) + else: + levels = [level] + resolved_level_ids = [] + for level_ref in levels: + if isinstance(level_ref, int): + resolved_level_ids.append(self._block.index_columns[level_ref]) + elif isinstance(level_ref, str): + matching_ids = self._block.index_name_to_col_id.get(level_ref, []) + if len(matching_ids) != 1: + raise ValueError("level name cannot be found or is ambiguous") + resolved_level_ids.append(matching_ids[0]) + else: + raise ValueError(f"Unexpected level: {level_ref}") + return resolved_level_ids + + def rename(self, *, columns: Mapping[blocks.Label, blocks.Label]) -> DataFrame: + block = self._block.rename(columns=columns) + return DataFrame(block) + + def rename_axis( + self, + mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], + **kwargs, + ) -> DataFrame: + if len(kwargs) != 0: + raise NotImplementedError( + "rename_axis does not currently support any keyword arguments." + ) + # limited implementation: the new index name is simply the 'mapper' parameter + if _is_list_like(mapper): + labels = mapper + else: + labels = [mapper] + return DataFrame(self._block.with_index_labels(labels)) + + def assign(self, **kwargs) -> DataFrame: + # TODO(garrettwu) Support list-like values. Requires ordering. + # TODO(garrettwu) Support callable values. + + cur = self + for k, v in kwargs.items(): + cur = cur._assign_single_item(k, v) + + return cur + + def _assign_single_item( + self, + k: str, + v: SingleItemValue, + ) -> DataFrame: + if isinstance(v, bigframes.series.Series): + return self._assign_series_join_on_index(k, v) + elif callable(v): + copy = self.copy() + copy[k] = v(copy) + return copy + else: + return self._assign_scalar(k, v) + + def _assign_scalar(self, label: str, value: Union[int, float]) -> DataFrame: + # TODO(swast): Make sure that k is the ID / SQL name, not a label, + # which could be invalid SQL. + col_ids = self._sql_names(label, tolerance=True) + + block, constant_col_id = self._block.create_constant(value, label) + for col_id in col_ids: + block = block.copy_values(constant_col_id, col_id) + + if len(col_ids) > 0: + block = block.drop_columns([constant_col_id]) + + return DataFrame(block) + + def _assign_series_join_on_index( + self, label: str, series: bigframes.series.Series + ) -> DataFrame: + joined_index, (get_column_left, get_column_right) = self._block.index.join( + series._block.index, how="left" + ) + + column_ids = [ + get_column_left(col_id) for col_id in self._sql_names(label, tolerance=True) + ] + block = joined_index._block + source_column = get_column_right(series._value_column) + + # Replace each column matching the label + for column_id in column_ids: + block = block.copy_values(source_column, column_id).assign_label( + column_id, label + ) + + if not column_ids: + # Append case, so new column needs appropriate label + block = block.assign_label(source_column, label) + else: + # Update case, remove after copying into columns + block = block.drop_columns([source_column]) + + return DataFrame(block.with_index_labels([self.index.name])) + + def reset_index(self, *, drop: bool = False) -> DataFrame: + block = self._block.reset_index(drop) + return DataFrame(block) + + def set_index( + self, + keys: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], + append: bool = False, + drop: bool = True, + ) -> DataFrame: + if not _is_list_like(keys): + keys = typing.cast(typing.Sequence[blocks.Label], (keys,)) + else: + keys = typing.cast(typing.Sequence[blocks.Label], tuple(keys)) + col_ids = [self._resolve_label_exact(key) for key in keys] + return DataFrame(self._block.set_index(col_ids, append=append, drop=drop)) + + def sort_index(self) -> DataFrame: + index_columns = self._block.index_columns + ordering = [order.OrderingColumnReference(column) for column in index_columns] + return DataFrame(self._block.order_by(ordering)) + + def sort_values( + self, + by: str | typing.Sequence[str], + *, + ascending: bool | typing.Sequence[bool] = True, + na_position: typing.Literal["first", "last"] = "last", + ) -> DataFrame: + if na_position not in {"first", "last"}: + raise ValueError("Param na_position must be one of 'first' or 'last'") + + sort_labels = (by,) if isinstance(by, str) else tuple(by) + sort_column_ids = self._sql_names(sort_labels) + + len_by = len(sort_labels) + if not isinstance(ascending, bool): + if len(ascending) != len_by: + raise ValueError("Length of 'ascending' must equal length of 'by'") + sort_directions = ascending + else: + sort_directions = (ascending,) * len_by + + ordering = [] + for i in range(len(sort_labels)): + column_id = sort_column_ids[i] + direction = ( + order.OrderingDirection.ASC + if sort_directions[i] + else order.OrderingDirection.DESC + ) + na_last = na_position == "last" + ordering.append( + order.OrderingColumnReference( + column_id, direction=direction, na_last=na_last + ) + ) + + return DataFrame(self._block.order_by(ordering)) + + def value_counts( + self, + subset: typing.Union[blocks.Label, typing.Sequence[blocks.Label]] = None, + normalize: bool = False, + sort: bool = True, + ascending: bool = False, + dropna: bool = True, + ): + # 'sort'=False allows arbitrary sorting, so we will sort anyways and ignore the param + columns = self._sql_names(subset) if subset else self._block.value_columns + block = block_ops.value_counts( + self._block, + columns, + normalize=normalize, + sort=sort, + ascending=ascending, + dropna=dropna, + ) + return bigframes.series.Series(block) + + def add_prefix(self, prefix: str, axis: int | str | None = None) -> DataFrame: + return DataFrame(self._get_block().add_prefix(prefix, axis)) + + def add_suffix(self, suffix: str, axis: int | str | None = None) -> DataFrame: + return DataFrame(self._get_block().add_suffix(suffix, axis)) + + def dropna(self) -> DataFrame: + block = self._block + for column in self._block.value_columns: + block, result_id = block.apply_unary_op(column, ops.notnull_op) + block = block.filter(result_id) + block = block.drop_columns([result_id]) + + return DataFrame(block) + + def any( + self, + *, + bool_only: bool = False, + ) -> bigframes.series.Series: + if not bool_only: + frame = self._raise_on_non_boolean("any") + else: + frame = self._drop_non_bool() + block = frame._block.aggregate_all_and_pivot( + agg_ops.any_op, dtype=pd.BooleanDtype() + ) + return bigframes.series.Series(block.select_column("values")) + + def all(self, *, bool_only: bool = False) -> bigframes.series.Series: + if not bool_only: + frame = self._raise_on_non_boolean("all") + else: + frame = self._drop_non_bool() + block = frame._block.aggregate_all_and_pivot( + agg_ops.all_op, dtype=pd.BooleanDtype() + ) + return bigframes.series.Series(block.select_column("values")) + + def sum(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self._raise_on_non_numeric("sum") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.sum_op) + return bigframes.series.Series(block.select_column("values")) + + def mean(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self._raise_on_non_numeric("mean") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.mean_op) + return bigframes.series.Series(block.select_column("values")) + + def std(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self._raise_on_non_numeric("std") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.std_op) + return bigframes.series.Series(block.select_column("values")) + + def var(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self._raise_on_non_numeric("var") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.var_op) + return bigframes.series.Series(block.select_column("values")) + + def min(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self._raise_on_non_numeric("min") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.min_op) + return bigframes.series.Series(block.select_column("values")) + + def max(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self._raise_on_non_numeric("max") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.max_op) + return bigframes.series.Series(block.select_column("values")) + + def prod(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self._raise_on_non_numeric("prod") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.product_op) + return bigframes.series.Series(block.select_column("values")) + + product = prod + + def count(self, *, numeric_only: bool = False) -> bigframes.series.Series: + if not numeric_only: + frame = self + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.count_op) + return bigframes.series.Series(block.select_column("values")) + + def nunique(self) -> bigframes.series.Series: + block = self._block.aggregate_all_and_pivot(agg_ops.nunique_op) + return bigframes.series.Series(block.select_column("values")) + + def _drop_non_numeric(self) -> DataFrame: + non_numeric_cols = [ + col_id + for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) + if dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES + ] + return DataFrame(self._block.drop_columns(non_numeric_cols)) + + def _drop_non_bool(self) -> DataFrame: + non_bool_cols = [ + col_id + for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) + if dtype not in bigframes.dtypes.BOOL_BIGFRAMES_TYPES + ] + return DataFrame(self._block.drop_columns(non_bool_cols)) + + def _raise_on_non_numeric(self, op: str): + if not all( + dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES + for dtype in self._block.dtypes + ): + raise NotImplementedError( + f"'{op}' does not support non-numeric columns. Set 'numeric_only'=True to ignore non-numeric columns" + ) + return self + + def _raise_on_non_boolean(self, op: str): + if not all( + dtype in bigframes.dtypes.BOOL_BIGFRAMES_TYPES + for dtype in self._block.dtypes + ): + raise NotImplementedError( + f"'{op}' does not support non-bool columns. Set 'bool_only'=True to ignore non-bool columns" + ) + return self + + def merge( + self, + right: DataFrame, + how: Literal[ + "inner", + "left", + "outer", + "right", + ] = "inner", + # TODO(garrettwu): Currently can take inner, outer, left and right. To support + # cross joins + # TODO(garrettwu): Support "on" list of columns and None. Currently a single + # column must be provided + on: Optional[str] = None, + *, + left_on: Optional[str] = None, + right_on: Optional[str] = None, + sort: bool = False, + suffixes: tuple[str, str] = ("_x", "_y"), + ) -> DataFrame: + if on is None: + if left_on is None or right_on is None: + raise ValueError("Must specify either on or left_on + right_on.") + else: + if left_on is not None or right_on is not None: + raise ValueError("Can not pass both on and left_on + right_on params.") + left_on, right_on = on, on + + left = self + left_on_sql = self._sql_names(left_on) + # 0 elements already throws an exception + if len(left_on_sql) > 1: + raise ValueError(f"The column label {left_on} is not unique.") + left_on_sql = left_on_sql[0] + + right_on_sql = right._sql_names(right_on) + if len(right_on_sql) > 1: + raise ValueError(f"The column label {right_on} is not unique.") + right_on_sql = right_on_sql[0] + + ( + joined_expr, + join_key_ids, + (get_column_left, get_column_right), + ) = joins.join_by_column( + left._block.expr, + [left_on_sql], + right._block.expr, + [right_on_sql], + how=how, + sort=sort, + # In merging on the same column, it only returns 1 key column from coalesced both. + # While if 2 different columns, both will be presented in the result. + get_both_join_key_cols=(left_on != right_on), + ) + # TODO(swast): Add suffixes to the column labels instead of reusing the + # column IDs as the new labels. + # Drop the index column(s) to be consistent with pandas. + left_columns = [ + join_key_ids[0] if (col_id == left_on_sql) else get_column_left(col_id) + for col_id in left._block.value_columns + ] + + right_columns = [] + for col_id in right._block.value_columns: + if col_id == right_on_sql: + # When left_on == right_on + if len(join_key_ids) > 1: + right_columns.append(join_key_ids[1]) + else: + right_columns.append(get_column_right(col_id)) + + expr = joined_expr.select_columns([*left_columns, *right_columns]) + labels = self._get_merged_col_labels( + right, left_on=left_on, right_on=right_on, suffixes=suffixes + ) + + # Constructs default index + block = blocks.Block(expr, column_labels=labels) + return DataFrame(block) + + def _get_merged_col_labels( + self, + right: DataFrame, + left_on: str, + right_on: str, + suffixes: tuple[str, str] = ("_x", "_y"), + ) -> List[blocks.Label]: + on_col_equal = left_on == right_on + + left_col_labels: list[blocks.Label] = [] + for col_label in self._block.column_labels: + if col_label in right._block.column_labels: + if on_col_equal and col_label == left_on: + # Merging on the same column only returns 1 key column from coalesce both. + # Take the left key column. + left_col_labels.append(col_label) + else: + left_col_labels.append(str(col_label) + suffixes[0]) + else: + left_col_labels.append(col_label) + + right_col_labels: list[blocks.Label] = [] + for col_label in right._block.column_labels: + if col_label in self._block.column_labels: + if on_col_equal and col_label == left_on: + # Merging on the same column only returns 1 key column from coalesce both. + # Pass the right key column. + pass + else: + right_col_labels.append(str(col_label) + suffixes[1]) + else: + right_col_labels.append(col_label) + + return left_col_labels + right_col_labels + + def join(self, other: DataFrame, *, how: str = "left") -> DataFrame: + if not self.columns.intersection(other.columns).empty: + raise NotImplementedError("Deduping column names is not implemented") + + left = self + right = other + combined_index, (get_column_left, get_column_right) = left._block.index.join( + right._block.index, how=how + ) + return DataFrame(combined_index._block) + + def groupby( + self, + by: typing.Union[ + blocks.Label, + bigframes.series.Series, + typing.Sequence[typing.Union[blocks.Label, bigframes.series.Series]], + ] = None, + *, + level: typing.Optional[LevelsType] = None, + as_index: bool = True, + dropna: bool = True, + ) -> groupby.DataFrameGroupBy: + if (by is not None) and (level is not None): + raise ValueError("Do not specify both 'by' and 'level'") + if by is not None: + return self._groupby_series(by, as_index=as_index, dropna=dropna) + if level is not None: + return self._groupby_level(level, as_index=as_index, dropna=dropna) + else: + raise TypeError("You have to supply one of 'by' and 'level'") + + def _groupby_level( + self, + level: LevelsType, + as_index: bool = True, + dropna: bool = True, + ): + return groupby.DataFrameGroupBy( + self._block, + self._resolve_levels(level), + as_index=as_index, + dropna=dropna, + ) + + def _groupby_series( + self, + by: typing.Union[ + blocks.Label, + bigframes.series.Series, + typing.Sequence[typing.Union[blocks.Label, bigframes.series.Series]], + ], + as_index: bool = True, + dropna: bool = True, + ): + if not isinstance(by, bigframes.series.Series) and _is_list_like(by): + by = list(by) + else: + by = [typing.cast(typing.Union[blocks.Label, bigframes.series.Series], by)] + + block = self._block + col_ids: typing.Sequence[str] = [] + for key in by: + if isinstance(key, bigframes.series.Series): + combined_index, ( + get_column_left, + get_column_right, + ) = block.index.join( + key._block.index, how="inner" if dropna else "left" + ) + col_ids = [ + *[get_column_left(value) for value in col_ids], + get_column_right(key._value_column), + ] + block = combined_index._block + else: + # Interpret as index level or column name + col_matches = block.label_to_col_id.get(key, []) + level_matches = block.index_name_to_col_id.get(key, []) + matches = [*col_matches, *level_matches] + if len(matches) != 1: + raise ValueError( + f"GroupBy key {key} does not map to unambiguous column or index level" + ) + col_ids = [*col_ids, matches[0]] + + return groupby.DataFrameGroupBy( + block, + col_ids, + as_index=as_index, + dropna=dropna, + ) + + def abs(self) -> DataFrame: + return self._apply_to_rows(ops.abs_op) + + def isna(self) -> DataFrame: + return self._apply_to_rows(ops.isnull_op) + + isnull = isna + + def notna(self) -> DataFrame: + return self._apply_to_rows(ops.notnull_op) + + notnull = notna + + def cumsum(self): + is_numeric_types = [ + (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) + for _, dtype in self.dtypes.items() + ] + if not all(is_numeric_types): + raise ValueError("All values must be numeric to apply cumsum.") + return self._apply_window_op( + agg_ops.sum_op, + bigframes.core.WindowSpec(following=0), + ) + + def cumprod(self) -> DataFrame: + is_numeric_types = [ + (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) + for _, dtype in self.dtypes.items() + ] + if not all(is_numeric_types): + raise ValueError("All values must be numeric to apply cumsum.") + return self._apply_window_op( + agg_ops.product_op, + bigframes.core.WindowSpec(following=0), + ) + + def cummin(self) -> DataFrame: + return self._apply_window_op( + agg_ops.min_op, + bigframes.core.WindowSpec(following=0), + ) + + def cummax(self) -> DataFrame: + return self._apply_window_op( + agg_ops.max_op, + bigframes.core.WindowSpec(following=0), + ) + + def shift(self, periods: int = 1) -> DataFrame: + window = bigframes.core.WindowSpec( + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.ShiftOp(periods), window) + + def _apply_window_op( + self, + op: agg_ops.WindowOp, + window_spec: bigframes.core.WindowSpec, + ): + block = self._block.multi_apply_window_op( + self._block.value_columns, + op, + window_spec=window_spec, + ) + return DataFrame(block) + + def sample( + self, + n: Optional[int] = None, + frac: Optional[float] = None, + *, + random_state: Optional[int] = None, + ) -> DataFrame: + if n is not None and frac is not None: + raise ValueError("Only one of 'n' or 'frac' parameter can be specified.") + + ns = (n,) if n is not None else () + fracs = (frac,) if frac is not None else () + + return self._split(ns=ns, fracs=fracs, random_state=random_state)[0] + + def _split( + self, + ns: Iterable[int] = (), + fracs: Iterable[float] = (), + *, + random_state: Optional[int] = None, + ) -> List[DataFrame]: + """Internal function to support splitting DF to multiple parts along index axis. + + At most one of ns and fracs can be passed in. If neither, default to ns = (1,). + Return a list of sampled DataFrames. + """ + if ns and fracs: + raise ValueError("Only one of 'ns' or 'fracs' parameter must be specified.") + + block = self._block + if not ns and not fracs: + ns = (1,) + + if ns: + sample_sizes = ns + else: + total_rows = block.shape[0] + # Round to nearest integer. "round half to even" rule applies. + # At least to be 1. + sample_sizes = [round(frac * total_rows) or 1 for frac in fracs] + + # Set random_state if it is not provided + if random_state is None: + random_state = random.randint(-(2**30), 2**30) + + # Create a new column with random_state value. + block, random_state_col = block.create_constant(random_state) + + # Create an ordering col and a new sum col which is ordering+random_state. + block, ordering_col = block.promote_offsets() + block, sum_col = block.apply_binary_op( + ordering_col, random_state_col, ops.add_op + ) + + # Apply hash method to sum col and order by it. + block, string_sum_col = block.apply_unary_op( + sum_col, ops.AsTypeOp("string[pyarrow]") + ) + block, hash_string_sum_col = block.apply_unary_op(string_sum_col, ops.hash_op) + block = block.order_by([order.OrderingColumnReference(hash_string_sum_col)]) + + drop_cols = [ + random_state_col, + ordering_col, + sum_col, + string_sum_col, + hash_string_sum_col, + ] + block = block.drop_columns(drop_cols) + df = DataFrame(block) + + intervals = [] + cur = 0 + for sample_size in sample_sizes: + intervals.append((cur, cur + sample_size)) + cur += sample_size + + # DF.iloc[slice] always returns DF. + return [ + typing.cast(DataFrame, df.iloc[lower:upper]) for lower, upper in intervals + ] + + def to_pandas(self) -> pd.DataFrame: + """Writes DataFrame to Pandas DataFrame.""" + # TODO(chelsealin): Support block parameters. + # TODO(chelsealin): Add to_pandas_batches() API. + return self.compute() + + def to_csv(self, path_or_buf: str, *, index: bool = True) -> None: + # TODO(swast): Can we support partition columns argument? + # TODO(chelsealin): Support local file paths. + # TODO(swast): Some warning that wildcard is recommended for large + # query results? See: + # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size + if not path_or_buf.startswith("gs://"): + raise NotImplementedError( + "Only Google Cloud Storage (gs://...) paths are supported." + ) + + source_table = self._execute_query(index=index) + job_config = bigquery.ExtractJobConfig( + destination_format=bigquery.DestinationFormat.CSV + ) + self._block.expr._session._extract_table( + source_table, + destination_uris=[path_or_buf], + job_config=job_config, + ) + + def to_json( + self, + path_or_buf: str, + orient: Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + *, + lines: bool = False, + index: bool = True, + ) -> None: + # TODO(swast): Can we support partition columns argument? + # TODO(chelsealin): Support local file paths. + # TODO(swast): Some warning that wildcard is recommended for large + # query results? See: + # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size + if not path_or_buf.startswith("gs://"): + raise NotImplementedError( + "Only Google Cloud Storage (gs://...) paths are supported." + ) + + if lines is True and orient != "records": + raise ValueError( + "'lines' keyword is only valid when 'orient' is 'records'." + ) + + # TODO(ashleyxu) Support lines=False for small tables with arrays and TO_JSON_STRING. + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#to_json_string + if lines is False: + raise NotImplementedError( + "Only newline delimited JSON format is supported." + ) + + source_table = self._execute_query(index=index) + job_config = bigquery.ExtractJobConfig( + destination_format=bigquery.DestinationFormat.NEWLINE_DELIMITED_JSON + ) + self._block.expr._session._extract_table( + source_table, + destination_uris=[path_or_buf], + job_config=job_config, + ) + + def to_gbq( + self, + destination_table: str, + *, + if_exists: Optional[Literal["fail", "replace", "append"]] = "fail", + index: bool = True, + ) -> None: + if "." not in destination_table: + raise ValueError( + "Invalid Table Name. Should be of the form 'datasetId.tableId' or " + "'projectId.datasetId.tableId'" + ) + + dispositions = { + "fail": bigquery.WriteDisposition.WRITE_EMPTY, + "replace": bigquery.WriteDisposition.WRITE_TRUNCATE, + "append": bigquery.WriteDisposition.WRITE_APPEND, + } + if if_exists not in dispositions: + raise ValueError("'{0}' is not valid for if_exists".format(if_exists)) + + job_config = bigquery.QueryJobConfig( + write_disposition=dispositions[if_exists], + destination=bigquery.table.TableReference.from_string( + destination_table, + default_project=self._block.expr._session.bqclient.project, + ), + ) + + self._execute_query(index=index, job_config=job_config) + + def to_numpy( + self, dtype=None, copy=False, na_value=None, **kwargs + ) -> numpy.ndarray: + return self.compute().to_numpy(dtype, copy, na_value, **kwargs) + + __array__ = to_numpy + + def to_parquet(self, path: str, *, index: bool = True) -> None: + # TODO(swast): Can we support partition columns argument? + # TODO(chelsealin): Support local file paths. + # TODO(swast): Some warning that wildcard is recommended for large + # query results? See: + # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size + if not path.startswith("gs://"): + raise NotImplementedError( + "Only Google Cloud Storage (gs://...) paths are supported." + ) + + source_table = self._execute_query(index=index) + job_config = bigquery.ExtractJobConfig( + destination_format=bigquery.DestinationFormat.PARQUET + ) + + self._block.expr._session._extract_table( + source_table, + destination_uris=[path], + job_config=job_config, + ) + + def _apply_to_rows(self, operation: ops.UnaryOp): + block = self._block.multi_apply_unary_op(self._block.value_columns, operation) + return DataFrame(block) + + def _execute_query( + self, index: bool, job_config: Optional[bigquery.job.QueryJobConfig] = None + ): + """Executes a query job presenting this dataframe and returns the destination + table.""" + expr = self._block.expr + session = expr._session + columns = list(self._block.value_columns) + column_labels = list(self._block.column_labels) + # This code drops unnamed indexes to keep consistent with the behavior of + # most pandas write APIs. The exception is `pandas.to_csv`, which keeps + # unnamed indexes as `Unnamed: 0`. + # TODO(chelsealin): check if works for multiple indexes. + if index and self.index.name is not None: + columns.extend(self._block.index_columns) + column_labels.extend(self.index.names) + # TODO(chelsealin): normalize the file formats if we needs, such as arbitrary + # unicode for column labels. + value_columns = (expr.get_column(column_name) for column_name in columns) + expr = expr.projection(value_columns) + + # Make columns in SQL reflect _labels_ not _ids_. Note: This may use + # the arbitrary unicode column labels feature in BigQuery, which is + # currently (June 2023) in preview. + # TODO(swast): Handle duplicate and NULL labels. + ibis_expr = expr.to_ibis_expr() + renamed_columns = [ + ibis_expr[col_id].name(col_label) + for col_id, col_label in zip(columns, column_labels) + ] + ibis_expr = ibis_expr.select(*renamed_columns) + sql = session.ibis_client.compile(ibis_expr) # type: ignore + _, query_job = session._start_query( + sql=sql, job_config=job_config # type: ignore + ) + return query_job.destination + + def map(self, func, na_action: Optional[str] = None) -> DataFrame: + if not callable(func): + raise TypeError("the first argument must be callable") + + if na_action not in {None, "ignore"}: + raise ValueError(f"na_action={na_action} not supported") + + # TODO(shobs): Support **kwargs + + return self._apply_to_rows( + ops.RemoteFunctionOp(func, apply_on_null=(na_action is None)) + ) + + def drop_duplicates(self, subset=None, *, keep: str = "first") -> DataFrame: + if subset is None: + column_ids = self._block.value_columns + else: + column_ids = [ + id for label in subset for id in self._block.label_to_col_id[label] + ] + block = block_ops.drop_duplicates(self._block, column_ids, keep) + return DataFrame(block) + + def duplicated(self, subset=None, keep: str = "first") -> bigframes.series.Series: + if subset is None: + column_ids = self._block.value_columns + else: + column_ids = [ + id for label in subset for id in self._block.label_to_col_id[label] + ] + block, indicator = block_ops.indicate_duplicates(self._block, column_ids, keep) + return bigframes.series.Series( + block.select_column( + indicator, + ) + ) + + def rank( + self, + axis=0, + method: str = "average", + numeric_only=False, + na_option: str = "keep", + ascending=True, + ) -> DataFrame: + df = self._drop_non_numeric() if numeric_only else self + return DataFrame(block_ops.rank(df._block, method, na_option, ascending)) + + applymap = map + + def _slice( + self, + start: typing.Optional[int] = None, + stop: typing.Optional[int] = None, + step: typing.Optional[int] = None, + ) -> DataFrame: + block = self._block.slice(start=start, stop=stop, step=step) + return DataFrame(block) + + def _set_block(self, block: blocks.Block): + self._block = block + + def _get_block(self) -> blocks.Block: + return self._block + + +def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: + return pd.api.types.is_list_like(obj) + + +def _is_dict_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Mapping]: + return pd.api.types.is_dict_like(obj) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py new file mode 100644 index 00000000000..c257d6943eb --- /dev/null +++ b/bigframes/dtypes.py @@ -0,0 +1,304 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Mappings for Pandas dtypes supported by BigQuery DataFrames package""" + +import typing +from typing import Any, Dict, Iterable, Literal, Tuple, Union + +import geopandas as gpd # type: ignore +import ibis +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types +import numpy as np +import pandas as pd +import pyarrow as pa + +# Type hints for Pandas dtypes supported by BigQuery DataFrame +Dtype = Union[ + pd.BooleanDtype, + pd.Float64Dtype, + pd.Int64Dtype, + pd.StringDtype, + pd.ArrowDtype, +] + +# Corresponds to the pandas concept of numeric type (such as when 'numeric_only' is specified in an operation) +NUMERIC_BIGFRAMES_TYPES = [pd.BooleanDtype(), pd.Float64Dtype(), pd.Int64Dtype()] + +# Type hints for dtype strings supported by BigQuery DataFrame +DtypeString = Literal[ + "boolean", + "Float64", + "Int64", + "string", + "string[pyarrow]", + "timestamp[us, tz=UTC][pyarrow]", + "timestamp[us][pyarrow]", + "date32[day][pyarrow]", + "time64[us][pyarrow]", +] + +# Type hints for Ibis data types supported by BigQuery DataFrame +IbisDtype = Union[ + ibis_dtypes.Boolean, + ibis_dtypes.Float64, + ibis_dtypes.Int64, + ibis_dtypes.String, + ibis_dtypes.Date, + ibis_dtypes.Time, + ibis_dtypes.Timestamp, +] + +BOOL_BIGFRAMES_TYPES = [pd.BooleanDtype()] + +# Several operations are restricted to these types. +NUMERIC_BIGFRAMES_TYPES = [pd.BooleanDtype(), pd.Float64Dtype(), pd.Int64Dtype()] + +# Type hints for Ibis data types that can be read to Python objects by BigQuery DataFrame +ReadOnlyIbisDtype = Union[ + ibis_dtypes.Binary, + ibis_dtypes.JSON, + ibis_dtypes.Decimal, + ibis_dtypes.GeoSpatial, + ibis_dtypes.Array, + ibis_dtypes.Struct, +] + +BIDIRECTIONAL_MAPPINGS: Iterable[Tuple[IbisDtype, Dtype]] = ( + (ibis_dtypes.boolean, pd.BooleanDtype()), + (ibis_dtypes.float64, pd.Float64Dtype()), + (ibis_dtypes.int64, pd.Int64Dtype()), + (ibis_dtypes.string, pd.StringDtype(storage="pyarrow")), + (ibis_dtypes.date, pd.ArrowDtype(pa.date32())), + (ibis_dtypes.time, pd.ArrowDtype(pa.time64("us"))), + (ibis_dtypes.Timestamp(timezone=None), pd.ArrowDtype(pa.timestamp("us"))), + ( + ibis_dtypes.Timestamp(timezone="UTC"), + pd.ArrowDtype(pa.timestamp("us", tz="UTC")), + ), +) + +BIGFRAMES_TO_IBIS: Dict[Dtype, IbisDtype] = { + pandas: ibis for ibis, pandas in BIDIRECTIONAL_MAPPINGS +} + +IBIS_TO_BIGFRAMES: Dict[ + Union[IbisDtype, ReadOnlyIbisDtype], Union[Dtype, np.dtype[Any]] +] = {ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS} +# Allow REQUIRED fields to map correctly. +IBIS_TO_BIGFRAMES.update( + {ibis.copy(nullable=False): pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS} +) +IBIS_TO_BIGFRAMES.update( + { + ibis_dtypes.binary: np.dtype("O"), + ibis_dtypes.json: np.dtype("O"), + ibis_dtypes.Decimal(precision=38, scale=9, nullable=True): np.dtype("O"), + ibis_dtypes.Decimal(precision=76, scale=38, nullable=True): np.dtype("O"), + ibis_dtypes.GeoSpatial( + geotype="geography", srid=4326, nullable=True + ): gpd.array.GeometryDtype(), + # TODO: Interval + } +) + +BIGFRAMES_STRING_TO_BIGFRAMES: Dict[DtypeString, Dtype] = { + typing.cast(DtypeString, dtype.name): dtype for dtype in BIGFRAMES_TO_IBIS.keys() +} + +# special case - string[pyarrow] doesn't include the storage in its name, and both +# "string" and "string[pyarrow] are accepted" +BIGFRAMES_STRING_TO_BIGFRAMES["string[pyarrow]"] = pd.StringDtype(storage="pyarrow") + + +def ibis_dtype_to_bigframes_dtype( + ibis_dtype: Union[IbisDtype, ReadOnlyIbisDtype] +) -> Union[Dtype, np.dtype[Any]]: + """Converts an Ibis dtype to a BigQuery DataFrames dtype + + Args: + ibis_dtype: The ibis dtype used to represent this type, which + should in turn correspond to an underlying BigQuery type + + Returns: + The supported BigQuery DataFrames dtype, which may be provided by + pandas, numpy, or db_types + + Raises: + ValueError: if passed an unexpected type + """ + # Special cases: Ibis supports variations on these types, but currently + # our IO returns them as objects. Eventually, we should support them as + # ArrowDType (and update the IO accordingly) + if isinstance(ibis_dtype, ibis_dtypes.Array) or isinstance( + ibis_dtype, ibis_dtypes.Struct + ): + return np.dtype("O") + + if ibis_dtype in IBIS_TO_BIGFRAMES: + return IBIS_TO_BIGFRAMES[ibis_dtype] + else: + raise ValueError(f"Unexpected Ibis data type {type(ibis_dtype)}") + + +def ibis_value_to_canonical_type(value: ibis_types.Value) -> ibis_types.Value: + """Converts an Ibis expression to canonical type. + + This is useful in cases where multiple types correspond to the same BigFrames dtype. + """ + ibis_type = value.type() + # Allow REQUIRED fields to be joined with NULLABLE fields. + nullable_type = ibis_type.copy(nullable=True) + return value.cast(nullable_type).name(value.get_name()) + + +def ibis_table_to_canonical_types(table: ibis_types.Table) -> ibis_types.Table: + """Converts an Ibis table expression to canonical types. + + This is useful in cases where multiple types correspond to the same BigFrames dtype. + """ + casted_columns = [] + for column_name in table.columns: + column = typing.cast(ibis_types.Value, table[column_name]) + casted_columns.append(ibis_value_to_canonical_type(column)) + return table.select(*casted_columns) + + +def bigframes_dtype_to_ibis_dtype( + bigframes_dtype: Union[DtypeString, Dtype] +) -> IbisDtype: + """Converts a BigQuery DataFrames supported dtype to an Ibis dtype. + + Args: + bigframes_dtype: A dtype supported by BigQuery DataFrame + + Returns: + The corresponding Ibis type + + Raises: + ValueError: + If passed a dtype not supported by BigQuery DataFrames. + """ + type_string = str(bigframes_dtype) + if type_string in BIGFRAMES_STRING_TO_BIGFRAMES: + bigframes_dtype = BIGFRAMES_STRING_TO_BIGFRAMES[ + typing.cast(DtypeString, type_string) + ] + else: + raise ValueError(f"Unexpected data type {bigframes_dtype}") + + return BIGFRAMES_TO_IBIS[bigframes_dtype] + + +def literal_to_ibis_scalar( + literal, force_dtype: typing.Optional[Dtype] = None, validate: bool = True +): + """Accept any literal and, if possible, return an Ibis Scalar + expression with a BigQuery DataFrames compatible data type + + Args: + literal: any value accepted by Ibis + force_dtype: force the value to a specific dtype + validate: + If true, will raise ValueError if type cannot be stored in a + BigQuery DataFrames object. If used as a subexpression, this should + be disabled. + + Returns: + An ibis Scalar supported by BigQuery DataFrame + + Raises: + ValueError: if passed literal cannot be coerced to a + BigQuery DataFrames compatible scalar + """ + ibis_dtype = BIGFRAMES_TO_IBIS[force_dtype] if force_dtype else None + + if pd.api.types.is_list_like(literal): + if validate: + raise ValueError("List types can't be stored in BigQuery DataFrames") + # "correct" way would be to use ibis.array, but this produces invalid BQ SQL syntax + return tuple(literal) + if not pd.api.types.is_list_like(literal) and pd.isna(literal): + if ibis_dtype: + return ibis.null().cast(ibis_dtype) + else: + return ibis.null() + + scalar_expr = ibis.literal(literal) + if ibis_dtype: + scalar_expr = ibis.literal(literal, ibis_dtype) + elif scalar_expr.type().is_floating(): + scalar_expr = ibis.literal(literal, ibis_dtypes.float64) + elif scalar_expr.type().is_integer(): + scalar_expr = ibis.literal(literal, ibis_dtypes.int64) + + # TODO(bmil): support other literals that can be coerced to compatible types + if validate and (scalar_expr.type() not in BIGFRAMES_TO_IBIS.values()): + raise ValueError(f"Literal did not coerce to a supported data type: {literal}") + + return scalar_expr + + +def cast_ibis_value(value: ibis_types.Value, to_type: IbisDtype) -> ibis_types.Value: + """Perform compatible type casts of ibis values + + Args: + value: Ibis value, which could be a literal, scalar, or column + + to_type: The Ibis type to cast to + + Returns: + A new Ibis value of type to_type + + Raises: + TypeError: if the type cast cannot be executed""" + if value.type() == to_type: + return value + # casts that just work + # TODO(bmil): add to this as more casts are verified + good_casts = { + ibis_dtypes.bool: (ibis_dtypes.int64,), + ibis_dtypes.int64: ( + ibis_dtypes.bool, + ibis_dtypes.float64, + ibis_dtypes.string, + ), + ibis_dtypes.float64: (ibis_dtypes.string,), + ibis_dtypes.string: (), + ibis_dtypes.date: (), + ibis_dtypes.time: (), + ibis_dtypes.timestamp: (ibis_dtypes.Timestamp(timezone="UTC"),), + ibis_dtypes.Timestamp(timezone="UTC"): (ibis_dtypes.timestamp,), + } + + value = ibis_value_to_canonical_type(value) + if value.type() in good_casts: + if to_type in good_casts[value.type()]: + return value.cast(to_type) + else: + # this should never happen + raise TypeError(f"Unexpected value type {value.type()}") + + # casts that need some encouragement + + # BigQuery casts bools to lower case strings. Capitalize the result to match Pandas + # TODO(bmil): remove this workaround after fixing Ibis + if value.type() == ibis_dtypes.bool and to_type == ibis_dtypes.string: + return typing.cast(ibis_types.StringValue, value.cast(to_type)).capitalize() + + if value.type() == ibis_dtypes.bool and to_type == ibis_dtypes.float64: + return value.cast(ibis_dtypes.int64).cast(ibis_dtypes.float64) + + raise TypeError(f"Unsupported cast {value.type()} to {to_type}") diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py new file mode 100644 index 00000000000..fa0ed0c64d1 --- /dev/null +++ b/bigframes/formatting_helpers.py @@ -0,0 +1,120 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared helper functions for formatting jobs related info.""" + +import datetime +from typing import Optional + +import google.cloud.bigquery as bigquery +import humanize +import IPython +import IPython.display as display +import ipywidgets as widgets + +query_job_prop_pairs = { + "Job Id": "job_id", + "Destination Table": "destination", + "Slot Time": "slot_millis", + "Bytes Processed": "estimated_bytes_processed", + "Cache hit": "cache_hit", +} + + +def repr_query_job(query_job: Optional[bigquery.QueryJob]): + if query_job is None: + return widgets.HTML("No job information available") + table_html = "" + table_html += "" + for key, value in query_job_prop_pairs.items(): + job_val = getattr(query_job, value) + if job_val is not None: + if key == "Job Id": # add link to job + table_html += f"""""" + elif key == "Slot Time": + table_html += ( + f"""""" + ) + elif key == "Bytes Processed": + table_html += f"""""" + else: + table_html += f"""""" + table_html += "
{key}{job_val}
{key}{get_formatted_time(job_val)}
{key}{get_formatted_bytes(job_val)}
{key}{job_val}
" + return widgets.HTML(table_html) + + +def wait_for_job( + query_job: bigquery.QueryJob, + max_results: Optional[int] = None, + progress_bar: Optional[str] = None, +) -> bigquery.table.RowIterator: + """Return query results. Displays a progress bar while the query is running + Args: + query_job: + The job representing the execution of the query on the server. + max_results: + The maximum number of rows the row iterator should return. + Returns: + A row iterator over the query results. + """ + loading_bar = widgets.HTML(get_query_job_loading_html(query_job)) + if progress_bar == "auto": + progress_bar = "notebook" if in_ipython() else "terminal" + + if progress_bar == "notebook": + display.display(loading_bar) + query_result = query_job.result(max_results=max_results) + query_job.reload() + loading_bar.close() + elif progress_bar == "terminal": + print(get_query_job_loading_string(query_job)) + query_result = query_job.result(max_results=max_results) + query_job.reload() + return query_result + + +def get_job_url(query_job: bigquery.QueryJob): + if ( + query_job.project is None + or query_job.location is None + or query_job.job_id is None + ): + return None + return f"""https://console.cloud.google.com/bigquery?project={query_job.project}&j=bq:{query_job.location}:{query_job.job_id}&page=queryresults""" + + +def get_query_job_loading_html(query_job: bigquery.QueryJob): + return f"""Job {query_job.job_id} is {query_job.state}. Open Job""" + + +def get_query_job_loading_string(query_job: bigquery.QueryJob): + return ( + f"""Job {query_job.job_id} is {query_job.state}. \n{get_job_url(query_job)}""" + ) + + +def get_formatted_time(val): + try: + return humanize.naturaldelta(datetime.timedelta(milliseconds=float(val))) + except Exception: + return val + + +def get_formatted_bytes(val): + return humanize.naturalsize(val) + + +def in_ipython(): + """Return True iff we're in a colab-like IPython.""" + return hasattr(IPython.get_ipython(), "kernel") diff --git a/bigframes/ml/__init__.py b/bigframes/ml/__init__.py new file mode 100644 index 00000000000..55c8709d8d8 --- /dev/null +++ b/bigframes/ml/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BigQuery DataFrames ML provides a SKLearn-like API on the BigQuery engine.""" + +__all__ = [ + "cluster", + "compose", + "decomposition", + "linear_model", + "metrics", + "model_selection", + "pipeline", + "preprocessing", + "llm", + "forecasting", + "imported", +] diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py new file mode 100644 index 00000000000..e0a1027dfea --- /dev/null +++ b/bigframes/ml/base.py @@ -0,0 +1,135 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Wraps primitives for machine learning with BQML + +This library is an evolving attempt to +- implement BigQuery DataFrames API for BQML +- follow as close as possible the API design of SKLearn + https://arxiv.org/pdf/1309.0238.pdf +""" + +import abc +from typing import Optional, TypeVar + +from bigframes.ml.core import BqmlModel +import third_party.bigframes_vendored.sklearn.base + + +class BaseEstimator(third_party.bigframes_vendored.sklearn.base.BaseEstimator, abc.ABC): + """ + A BigQuery DataFrames machine learning component following the SKLearn API + design Ref: https://bit.ly/3NyhKjN + + The estimator is the fundamental abstraction for all learning components. This includes learning + algorithms, and also some preprocessing routines. + + This base class provides shared methods for inspecting parameters, and for building a consistent + string representation of the component. By convention, the __init__ of all descendents will be + assumed to be the list of hyperparameters. + + All descendents of this class should implement: + def __init__(self, hyperparameter_1=default_1, hyperparameter_2=default_2, hyperparameter3, ...): + '''Set hyperparameters''' + self.hyperparameter_1 = hyperparameter_1 + self.hyperparameter_2 = hyperparameter_2 + self.hyperparameter3 = hyperparameter3 + ... + Note: the object variable names must be exactly the same with parameter names. In order to utilize __repr__. + + fit(X, y) method is optional. + + The types of decendents of this class should be: + + 1) Predictors + These extend the interface with a .predict(self, x_test) method which predicts the target values + according to the parameters that were calculated in .fit() + + def predict(self, x_test: Union[DataFrame, Series]) -> Union[DataFrame, Series]: + '''Predict the target values according to the parameters that were calculated in .fit''' + ... + + 2) Transformers + These extend the interface with .transform(self, x) and .fit_transform(x_train) methods, which + apply data processing steps such as scaling that must be fitted to training data + + def transform(self, x: Union[DataFrame, Series]) -> Union[DataFrame, Series]: + '''Transform the data according to the parameters that were calculated in .fit()''' + ... + + def fit_transform(self, x_train: Union[DataFrame, Series], y_train: Union[DataFrame, Series]): + '''Perform both fit() and transform()''' + ... + """ + + def __repr__(self): + """Print the estimator's constructor with all non-default parameter values""" + + # Estimator pretty printer adapted from Sklearn's, which is in turn an adaption of + # the inbuilt pretty-printer in CPython + import third_party.bigframes_vendored.cpython._pprint as adapted_pprint + + prettyprinter = adapted_pprint._EstimatorPrettyPrinter( + compact=True, indent=1, indent_at_name=True, n_max_elements_to_show=30 + ) + + return prettyprinter.pformat(self) + + +class Predictor(BaseEstimator): + """A BigQuery DataFrames ML Model base class that can be used to predict outputs.""" + + def __init__(self): + self._bqml_model: Optional[BqmlModel] = None + + @abc.abstractmethod + def predict(self, X): + pass + + _T = TypeVar("_T", bound="Predictor") + + def register(self: _T, vertex_ai_model_id: Optional[str] = None) -> _T: + """Register the model to Vertex AI. + Args: + vertex_ai_model_id: optional string id as model id in Vertex. If not set, will by default to 'bigframes_{bq_model_id}'. + + Returns: + BigQuery DataFrames Model after register. + """ + if not self._bqml_model: + raise RuntimeError("A model must be trained before register.") + + self._bqml_model.register(vertex_ai_model_id) + return self + + +class TrainablePredictor(Predictor): + """A BigQuery DataFrame ML Model base class that can be used to fit and predict outputs. + + Also the predictor can be attached to a pipeline with transformers.""" + + @abc.abstractmethod + def fit(self, X, y, transforms): + pass + + # TODO(b/289280565): enable signatures after updating KMeans and PCA + # @abc.abstractmethod + # def score(self, X, y): + # pass + + # TODO(b/291812029): move to Predictor after implement in LLM and imported models + @abc.abstractmethod + def to_gbq(self, model_name, replace): + pass diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py new file mode 100644 index 00000000000..2dd95bfc1c9 --- /dev/null +++ b/bigframes/ml/cluster.py @@ -0,0 +1,97 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Clustering models. This module is styled after Scikit-Learn's cluster module: +https://scikit-learn.org/stable/modules/clustering.html""" + +from __future__ import annotations + +from typing import cast, Dict, List, Optional, TYPE_CHECKING + +from google.cloud import bigquery + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.base +import bigframes.ml.core +import third_party.bigframes_vendored.sklearn.cluster._kmeans + + +class KMeans( + third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans, + bigframes.ml.base.TrainablePredictor, +): + + __doc__ = third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans.__doc__ + + def __init__(self, n_clusters=8): + self.n_clusters = n_clusters + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq(session: bigframes.Session, model: bigquery.Model) -> KMeans: + assert model.model_type == "KMEANS" + + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + if "numClusters" in last_fitting: + kwargs["n_clusters"] = int(last_fitting["numClusters"]) + + new_kmeans = KMeans(**kwargs) + new_kmeans._bqml_model = bigframes.ml.core.BqmlModel(session, model) + return new_kmeans + + @property + def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: + """The model options as they will be set for BQML""" + return {"model_type": "KMEANS", "num_clusters": self.n_clusters} + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y=None, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + train_X=X, + transforms=transforms, + options=self._bqml_options, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + return cast( + bigframes.dataframe.DataFrame, self._bqml_model.predict(X)[["CENTROID_ID"]] + ) + + def to_gbq(self, model_name: str, replace: bool = False) -> KMeans: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py new file mode 100644 index 00000000000..09645d4cf8e --- /dev/null +++ b/bigframes/ml/compose.py @@ -0,0 +1,123 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Build composite transformers on heterogenous data. This module is styled +after Scikit-Learn's compose module: +https://scikit-learn.org/stable/modules/classes.html#module-sklearn.compose""" + +from __future__ import annotations + +import typing +from typing import List, Optional, Tuple, TYPE_CHECKING, Union + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.base +import bigframes.ml.compose +import bigframes.ml.core +import bigframes.ml.preprocessing +import third_party.bigframes_vendored.sklearn.compose._column_transformer + +CompilablePreprocessorType = Union[ + bigframes.ml.preprocessing.OneHotEncoder, + bigframes.ml.preprocessing.StandardScaler, +] + + +class ColumnTransformer( + third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, + bigframes.ml.base.BaseEstimator, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer.__doc__ + ) + + def __init__( + self, + transformers: List[ + Tuple[ + str, + CompilablePreprocessorType, + Union[str, List[str]], + ] + ], + ): + self.transformers = transformers + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @property + def transformers_( + self, + ) -> List[Tuple[str, CompilablePreprocessorType, str,]]: + """The collection of transformers as tuples of (name, transformer, column)""" + result: List[ + Tuple[ + str, + CompilablePreprocessorType, + str, + ] + ] = [] + for entry in self.transformers: + name, transformer, column_or_columns = entry + if isinstance(column_or_columns, str): + result.append((name, transformer, column_or_columns)) + else: + for column in column_or_columns: + result.append((name, transformer, column)) + + return result + + def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: a list of column names to transform + + Returns: a list of tuples of (sql_expression, output_name)""" + return [ + transformer._compile_to_sql([column])[0] + for column in columns + for _, transformer, target_column in self.transformers_ + if column == target_column + ] + + def fit( + self, + X: bigframes.dataframe.DataFrame, + ): + compiled_transforms = self._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + + def transform( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + df = self._bqml_model.transform(X) + return typing.cast( + bigframes.dataframe.DataFrame, + df[self._output_names], + ) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py new file mode 100644 index 00000000000..2663a8e17e5 --- /dev/null +++ b/bigframes/ml/core.py @@ -0,0 +1,334 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core operations for BQML based models""" + +from __future__ import annotations + +from typing import Callable, cast, Iterable, Mapping, Optional, Union +import uuid + +from google.cloud import bigquery + +import bigframes.dataframe +import bigframes.ml.sql +import bigframes.session + + +class BqmlModel: + """Represents an existing BQML model in BigQuery. + + Wraps the BQML API and SQL interface to expose the functionality needed for + BigQuery DataFrames ML. + """ + + def __init__(self, session: bigframes.session.Session, model: bigquery.Model): + self._session = session + self._model = model + + @property + def session(self) -> bigframes.Session: + """Get the BigQuery DataFrames session that this BQML model wrapper is tied to""" + return self._session + + @property + def model_name(self) -> str: + """Get the fully qualified name of the model, i.e. project_id.dataset_id.model_id""" + return f"{self._model.project}.{self._model.dataset_id}.{self._model.model_id}" + + @property + def model(self) -> bigquery.Model: + """Get the BQML model associated with this wrapper""" + return self._model + + @staticmethod + def _apply_sql( + session: bigframes.Session, + input_data: bigframes.dataframe.DataFrame, + func: Callable[[str], str], + ) -> bigframes.dataframe.DataFrame: + """Helper to wrap a dataframe in a SQL query, keeping the index intact. + + Args: + session: the active bigframes.Session + + input_data: the dataframe to be wrapped + + func: a function that will accept a SQL string and produce a new SQL + string from which to construct the output dataframe. It must + include the index columns of the input SQL. + """ + source_sql, tagged_index_cols = input_data.to_sql_query( + always_include_index=True + ) + + if len(tagged_index_cols) != 1: + raise NotImplementedError("Only exactly one index column is supported") + + index_col_name, is_named_index = tagged_index_cols[0] + sql = func(source_sql) + df = session.read_gbq(sql, index_col=[index_col_name]) + if not is_named_index: + df.index.name = None + + return df + + def predict( + self, input_data: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + # TODO: validate input data schema + return self._apply_sql( + self._session, + input_data, + lambda source_sql: bigframes.ml.sql.ml_predict( + model_name=self.model_name, source_sql=source_sql + ), + ) + + def transform( + self, input_data: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + # TODO: validate input data schema + return self._apply_sql( + self._session, + input_data, + lambda source_sql: bigframes.ml.sql.ml_transform( + model_name=self.model_name, source_sql=source_sql + ), + ) + + def generate_text( + self, + input_data: bigframes.dataframe.DataFrame, + options: Mapping[str, int | float], + ) -> bigframes.dataframe.DataFrame: + # TODO: validate input data schema + return self._apply_sql( + self._session, + input_data, + lambda source_sql: bigframes.ml.sql.ml_generate_text( + model_name=self.model_name, + source_sql=source_sql, + struct_options=bigframes.ml.sql.struct_options(**options), + ), + ) + + def embed_text( + self, + input_data: bigframes.dataframe.DataFrame, + options: Mapping[str, int | float], + ) -> bigframes.dataframe.DataFrame: + # TODO: validate input data schema + return self._apply_sql( + self._session, + input_data, + lambda source_sql: bigframes.ml.sql.ml_embed_text( + model_name=self.model_name, + source_sql=source_sql, + struct_options=bigframes.ml.sql.struct_options(**options), + ), + ) + + def forecast(self) -> bigframes.dataframe.DataFrame: + sql = bigframes.ml.sql.ml_forecast(self.model_name) + return self._session.read_gbq(sql) + + def evaluate(self, input_data: Union[bigframes.dataframe.DataFrame, None] = None): + # TODO: validate input data schema + # Note: don't need index as evaluate returns a new table + source_sql, _ = ( + input_data.to_sql_query(always_include_index=False) + if (input_data is not None) + else (None, None) + ) + sql = bigframes.ml.sql.ml_evaluate(self.model_name, source_sql) + + return self._session.read_gbq(sql) + + def copy(self, new_model_name, replace=False) -> BqmlModel: + job_config = bigquery.job.CopyJobConfig() + if replace: + job_config.write_disposition = "WRITE_TRUNCATE" + + self._session.bqclient.copy_table( + self.model_name, new_model_name, job_config=job_config + ).result() + + new_model = self._session.bqclient.get_model(new_model_name) + return BqmlModel(self._session, new_model) + + def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: + if vertex_ai_model_id is None: + # vertex id needs to start with letters. https://cloud.google.com/vertex-ai/docs/general/resource-naming + vertex_ai_model_id = "bigframes_" + cast(str, self._model.model_id) + + options_sql = bigframes.ml.sql.options( + **{"vertex_ai_model_id": vertex_ai_model_id} + ) + sql = bigframes.ml.sql.alter_model(self.model_name, options_sql=options_sql) + # Register the model and wait it to finish + self._session.bqclient.query(sql).result() + + self._model = self._session.bqclient.get_model(self.model_name) + return self + + +def create_bqml_model( + train_X: bigframes.dataframe.DataFrame, + train_y: Optional[bigframes.dataframe.DataFrame] = None, + transforms: Optional[Iterable[str]] = None, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, +) -> BqmlModel: + """Create a session-temporary BQML model with the CREATE MODEL statement + + Args: + train_X: features columns for training + train_y: labels columns for training, if applicable + transforms: an optional list of SQL expressions that implement preprocessing + on top of the input data. Generates a BQML TRANSFORM clause + options: a dict of options to configure the model. Generates a BQML OPTIONS + clause + + Returns: a BqmlModel, wrapping a trained model in BigQuery + """ + options = dict(options) + if train_y is None: + input_data = train_X + else: + # TODO: handle case where train_y columns are renamed in the join + input_data = train_X.join(train_y, how="outer") + options.update({"INPUT_LABEL_COLS": train_y.columns.tolist()}) + + # pickpocket session object from the dataframe + session = train_X._get_block().expr._session + + # TODO(garrettwu): add wrapper to select the feature columns + # for now, drop index to avoid including the index in feature columns + input_data = input_data.reset_index(drop=True) + + model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" + source_sql = input_data.sql + options_sql = bigframes.ml.sql.options(**options) + transform_sql = ( + bigframes.ml.sql.transform(*transforms) if transforms is not None else None + ) + sql = bigframes.ml.sql.create_model( + model_name=model_name, + source_sql=source_sql, + transform_sql=transform_sql, + options_sql=options_sql, + ) + + # fit the model, synchronously + session.bqclient.query(sql).result() + + model = session.bqclient.get_model(model_name) + return BqmlModel(session, model) + + +def create_bqml_time_series_model( + train_X: bigframes.dataframe.DataFrame, + train_y: bigframes.dataframe.DataFrame, + transforms: Optional[Iterable[str]] = None, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, +) -> BqmlModel: + + assert ( + train_X.columns.size == 1 + ), "Time series timestamp input must only contain 1 column." + assert ( + train_y.columns.size == 1 + ), "Time stamp data input must only contain 1 column." + + options = dict(options) + input_data = train_X.join(train_y, how="outer") + options.update({"TIME_SERIES_TIMESTAMP_COL": train_X.columns.tolist()[0]}) + options.update({"TIME_SERIES_DATA_COL": train_y.columns.tolist()[0]}) + # pickpocket session object from the dataframe + session = train_X._get_block().expr._session + + model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" + source_sql = input_data.sql + options_sql = bigframes.ml.sql.options(**options) + + transform_sql = ( + bigframes.ml.sql.transform(*transforms) if transforms is not None else None + ) + sql = bigframes.ml.sql.create_model( + model_name=model_name, + source_sql=source_sql, + transform_sql=transform_sql, + options_sql=options_sql, + ) + + # fit the model, synchronously + session.bqclient.query(sql).result() + + model = session.bqclient.get_model(model_name) + return BqmlModel(session, model) + + +def create_bqml_remote_model( + session: bigframes.Session, + connection_name: str, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, +) -> BqmlModel: + """Create a session-temporary BQML remote model with the CREATE MODEL statement + + Args: + connection_name: a BQ connection to talk with Vertex AI, of the format ... https://cloud.google.com/bigquery/docs/create-cloud-resource-connection + options: a dict of options to configure the model. Generates a BQML OPTIONS + clause + + Returns: a BqmlModel, wrapping a trained model in BigQuery + """ + model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" + options_sql = bigframes.ml.sql.options(**options) + sql = bigframes.ml.sql.create_remote_model( + model_name=model_name, + connection_name=connection_name, + options_sql=options_sql, + ) + + # create the model, synchronously + session.bqclient.query(sql).result() + + model = session.bqclient.get_model(model_name) + return BqmlModel(session, model) + + +def create_bqml_imported_model( + session: bigframes.Session, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, +) -> BqmlModel: + """Create a session-temporary BQML imported model with the CREATE MODEL statement + + Args: + options: a dict of options to configure the model. Generates a BQML OPTIONS + clause + + Returns: a BqmlModel, wrapping a trained model in BigQuery + """ + model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" + options_sql = bigframes.ml.sql.options(**options) + sql = bigframes.ml.sql.create_imported_model( + model_name=model_name, + options_sql=options_sql, + ) + + # create the model, synchronously + session.bqclient.query(sql).result() + + model = session.bqclient.get_model(model_name) + return BqmlModel(session, model) diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py new file mode 100644 index 00000000000..08551d1cb8c --- /dev/null +++ b/bigframes/ml/decomposition.py @@ -0,0 +1,104 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Matrix Decomposition models. This module is styled after Scikit-Learn's decomposition module: +https://scikit-learn.org/stable/modules/decomposition.html""" + +from __future__ import annotations + +from typing import cast, List, Optional, TYPE_CHECKING + +from google.cloud import bigquery + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.base +import bigframes.ml.core +import third_party.bigframes_vendored.sklearn.decomposition._pca + + +class PCA( + third_party.bigframes_vendored.sklearn.decomposition._pca.PCA, + bigframes.ml.base.TrainablePredictor, +): + __doc__ = third_party.bigframes_vendored.sklearn.decomposition._pca.PCA.__doc__ + + def __init__(self, n_components=3): + self.n_components = n_components + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq(session: bigframes.Session, model: bigquery.Model) -> PCA: + assert model.model_type == "PCA" + + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + if "numPrincipalComponents" in last_fitting: + kwargs["n_components"] = int(last_fitting["numPrincipalComponents"]) + + new_pca = PCA(**kwargs) + new_pca._bqml_model = bigframes.ml.core.BqmlModel(session, model) + return new_pca + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y=None, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + train_X=X, + transforms=transforms, + options={ + "model_type": "PCA", + "num_principal_components": self.n_components, + }, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + """Predict the closest cluster for each sample in X. + + Args: + X: a BigQuery DataFrame to predict. + y: ignored for API consistency. + + Returns: predicted BigQuery DataFrames.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + return cast( + bigframes.dataframe.DataFrame, + self._bqml_model.predict(X)[ + ["principal_component_" + str(i + 1) for i in range(self.n_components)] + ], + ) + + def to_gbq(self, model_name: str, replace: bool = False) -> PCA: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py new file mode 100644 index 00000000000..117759ca1c6 --- /dev/null +++ b/bigframes/ml/ensemble.py @@ -0,0 +1,672 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Linear models. This module is styled after Scikit-Learn's linear_model module: +https://scikit-learn.org/stable/modules/linear_model.html""" + +from __future__ import annotations + +from typing import cast, Dict, List, Literal, Optional, TYPE_CHECKING + +from google.cloud import bigquery + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.base +import bigframes.ml.core +import third_party.bigframes_vendored.sklearn.ensemble._forest +import third_party.bigframes_vendored.xgboost.sklearn + +_BQML_PARAMS_MAPPING = { + "booster": "boosterType", + "tree_method": "treeMethod", + "early_stop": "earlyStop", + "colsample_bytree": "colsampleBylevel", + "colsample_bylevel": "colsampleBytree", + "colsample_bynode": "colsampleBynode", + "gamma": "minSplitLoss", + "subsample": "subsample", + "reg_alpha": "l1Regularization", + "reg_lambda": "l2Regularization", + "learning_rate": "learnRate", + "min_rel_progress": "minRelativeProgress", + "num_parallel_tree": "numParallelTree", + "min_tree_child_weight": "minTreeChildWeight", + "max_depth": "maxTreeDepth", + "max_iterations": "maxIterations", +} + + +class XGBRegressor( + third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor, + bigframes.ml.base.TrainablePredictor, +): + __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor.__doc__ + + def __init__( + self, + num_parallel_tree: int = 1, + booster: Literal["gbtree", "dart"] = "gbtree", + dart_normalized_type: Literal["TREE", "FOREST"] = "TREE", + tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", + min_tree_child_weight: int = 1, + colsample_bytree=1.0, + colsample_bylevel=1.0, + colsample_bynode=1.0, + gamma=0.0, + max_depth: int = 6, + subsample=1.0, + reg_alpha=0.0, + reg_lambda=1.0, + early_stop=True, + learning_rate=0.3, + max_iterations: int = 20, + min_rel_progress=0.01, + enable_global_explain=False, + xgboost_version: Literal["0.9", "1.1"] = "0.9", + ): + self.num_parallel_tree = num_parallel_tree + self.booster = booster + self.dart_normalized_type = dart_normalized_type + self.tree_method = tree_method + self.min_tree_child_weight = min_tree_child_weight + self.colsample_bytree = colsample_bytree + self.colsample_bylevel = colsample_bylevel + self.colsample_bynode = colsample_bynode + self.gamma = gamma + self.max_depth = max_depth + self.subsample = subsample + self.reg_alpha = reg_alpha + self.reg_lambda = reg_lambda + self.early_stop = early_stop + self.learning_rate = learning_rate + self.max_iterations = max_iterations + self.min_rel_progress = min_rel_progress + self.enable_global_explain = enable_global_explain + self.xgboost_version = xgboost_version + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq(session: bigframes.Session, model: bigquery.Model) -> XGBRegressor: + assert model.model_type == "BOOSTED_TREE_REGRESSOR" + + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + + dummy_regressor = XGBRegressor() + for bf_param, bf_value in dummy_regressor.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param is not None: + kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + + new_xgb_regressor = XGBRegressor(**kwargs) + new_xgb_regressor._bqml_model = bigframes.ml.core.BqmlModel(session, model) + return new_xgb_regressor + + @property + def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + """The model options as they will be set for BQML""" + return { + "model_type": "BOOSTED_TREE_REGRESSOR", + "data_split_method": "NO_SPLIT", + "num_parallel_tree": self.num_parallel_tree, + "booster_type": self.booster, + "tree_method": self.tree_method, + "min_tree_child_weight": self.min_tree_child_weight, + "colsample_bytree": self.colsample_bytree, + "colsample_bylevel": self.colsample_bylevel, + "colsample_bynode": self.colsample_bynode, + "min_split_loss": self.gamma, + "max_tree_depth": self.max_depth, + "subsample": self.subsample, + "l1_reg": self.reg_alpha, + "l2_reg": self.reg_lambda, + "early_stop": self.early_stop, + "learn_rate": self.learning_rate, + "max_iterations": self.max_iterations, + "min_rel_progress": self.min_rel_progress, + "enable_global_explain": self.enable_global_explain, + "xgboost_version": self.xgboost_version, + } + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + y, + transforms=transforms, + options=self._bqml_options, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) + + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ): + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + input_data = ( + X.join(y, how="outer") if (X is not None) and (y is not None) else None + ) + return self._bqml_model.evaluate(input_data) + + def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) + + +class XGBClassifier( + third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier, + bigframes.ml.base.TrainablePredictor, +): + + __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier.__doc__ + + def __init__( + self, + num_parallel_tree: int = 1, + booster: Literal["gbtree", "dart"] = "gbtree", + dart_normalized_type: Literal["TREE", "FOREST"] = "TREE", + tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", + min_tree_child_weight: int = 1, + colsample_bytree=1.0, + colsample_bylevel=1.0, + colsample_bynode=1.0, + gamma=0.0, + max_depth: int = 6, + subsample=1.0, + reg_alpha=0.0, + reg_lambda=1.0, + early_stop=True, + learning_rate=0.3, + max_iterations: int = 20, + min_rel_progress=0.01, + enable_global_explain=False, + xgboost_version: Literal["0.9", "1.1"] = "0.9", + ): + self.num_parallel_tree = num_parallel_tree + self.booster = booster + self.dart_normalized_type = dart_normalized_type + self.tree_method = tree_method + self.min_tree_child_weight = min_tree_child_weight + self.colsample_bytree = colsample_bytree + self.colsample_bylevel = colsample_bylevel + self.colsample_bynode = colsample_bynode + self.gamma = gamma + self.max_depth = max_depth + self.subsample = subsample + self.reg_alpha = reg_alpha + self.reg_lambda = reg_lambda + self.early_stop = early_stop + self.learning_rate = learning_rate + self.max_iterations = max_iterations + self.min_rel_progress = min_rel_progress + self.enable_global_explain = enable_global_explain + self.xgboost_version = xgboost_version + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq(session: bigframes.Session, model: bigquery.Model) -> XGBClassifier: + assert model.model_type == "BOOSTED_TREE_CLASSIFIER" + + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + + dummy_classifier = XGBClassifier() + for bf_param, bf_value in dummy_classifier.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param is not None: + kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + + new_xgb_classifier = XGBClassifier(**kwargs) + new_xgb_classifier._bqml_model = bigframes.ml.core.BqmlModel(session, model) + return new_xgb_classifier + + @property + def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + """The model options as they will be set for BQML""" + return { + "model_type": "BOOSTED_TREE_CLASSIFIER", + "data_split_method": "NO_SPLIT", + "num_parallel_tree": self.num_parallel_tree, + "booster_type": self.booster, + "tree_method": self.tree_method, + "min_tree_child_weight": self.min_tree_child_weight, + "colsample_bytree": self.colsample_bytree, + "colsample_bylevel": self.colsample_bylevel, + "colsample_bynode": self.colsample_bynode, + "min_split_loss": self.gamma, + "max_tree_depth": self.max_depth, + "subsample": self.subsample, + "l1_reg": self.reg_alpha, + "l2_reg": self.reg_lambda, + "early_stop": self.early_stop, + "learn_rate": self.learning_rate, + "max_iterations": self.max_iterations, + "min_rel_progress": self.min_rel_progress, + "enable_global_explain": self.enable_global_explain, + "xgboost_version": self.xgboost_version, + } + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + y, + transforms=transforms, + options=self._bqml_options, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) + + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ): + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + input_data = ( + X.join(y, how="outer") if (X is not None) and (y is not None) else None + ) + return self._bqml_model.evaluate(input_data) + + def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) + + +class RandomForestRegressor( + third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor, + bigframes.ml.base.TrainablePredictor, +): + + __doc__ = ( + third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor.__doc__ + ) + + def __init__( + self, + num_parallel_tree: int = 100, + tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", + min_tree_child_weight: int = 1, + colsample_bytree=1.0, + colsample_bylevel=1.0, + colsample_bynode=0.8, + gamma=0.00, + max_depth: int = 15, + subsample=0.8, + reg_alpha=0.0, + reg_lambda=1.0, + early_stop=True, + min_rel_progress=0.01, + enable_global_explain=False, + xgboost_version: Literal["0.9", "1.1"] = "0.9", + ): + self.num_parallel_tree = num_parallel_tree + self.tree_method = tree_method + self.min_tree_child_weight = min_tree_child_weight + self.colsample_bytree = colsample_bytree + self.colsample_bylevel = colsample_bylevel + self.colsample_bynode = colsample_bynode + self.gamma = gamma + self.max_depth = max_depth + self.subsample = subsample + self.reg_alpha = reg_alpha + self.reg_lambda = reg_lambda + self.early_stop = early_stop + self.min_rel_progress = min_rel_progress + self.enable_global_explain = enable_global_explain + self.xgboost_version = xgboost_version + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq( + session: bigframes.Session, model: bigquery.Model + ) -> RandomForestRegressor: + assert model.model_type == "RANDOM_FOREST_REGRESSOR" + + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + + dummy_model = RandomForestRegressor() + for bf_param, bf_value in dummy_model.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param is not None: + kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + + new_random_forest_regressor = RandomForestRegressor(**kwargs) + new_random_forest_regressor._bqml_model = bigframes.ml.core.BqmlModel( + session, model + ) + return new_random_forest_regressor + + @property + def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + """The model options as they will be set for BQML""" + return { + "model_type": "RANDOM_FOREST_REGRESSOR", + "num_parallel_tree": self.num_parallel_tree, + "tree_method": self.tree_method, + "min_tree_child_weight": self.min_tree_child_weight, + "colsample_bytree": self.colsample_bytree, + "colsample_bylevel": self.colsample_bylevel, + "colsample_bynode": self.colsample_bynode, + "min_split_loss": self.gamma, + "max_tree_depth": self.max_depth, + "subsample": self.subsample, + "l1_reg": self.reg_alpha, + "l2_reg": self.reg_lambda, + "early_stop": self.early_stop, + "min_rel_progress": self.min_rel_progress, + "data_split_method": "NO_SPLIT", + "enable_global_explain": self.enable_global_explain, + "xgboost_version": self.xgboost_version, + } + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + y, + transforms=transforms, + options=self._bqml_options, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) + + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ): + """Calculate evaluation metrics of the model. + + Args: + X: a BigFrames DataFrame as evaluation data. + y: a BigFrames DataFrame as evaluation labels. + + Returns: a BigFrames DataFrame as evaluation result.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + if (X is None) != (y is None): + raise ValueError( + "Either both or neither of test_X and test_y must be specified" + ) + input_data = ( + X.join(y, how="outer") if (X is not None) and (y is not None) else None + ) + return self._bqml_model.evaluate(input_data) + + def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegressor: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) + + +class RandomForestClassifier( + third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier, + bigframes.ml.base.TrainablePredictor, +): + + __doc__ = ( + third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier.__doc__ + ) + + def __init__( + self, + num_parallel_tree: int = 100, + tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", + min_tree_child_weight: int = 1, + colsample_bytree: float = 1.0, + colsample_bylevel: float = 1.0, + colsample_bynode: float = 0.8, + gamma: float = 0.00, + max_depth: int = 15, + subsample: float = 0.8, + reg_alpha: float = 0.0, + reg_lambda: float = 1.0, + early_stop=True, + min_rel_progress: float = 0.01, + enable_global_explain=False, + xgboost_version: Literal["0.9", "1.1"] = "0.9", + ): + self.num_parallel_tree = num_parallel_tree + self.tree_method = tree_method + self.min_tree_child_weight = min_tree_child_weight + self.colsample_bytree = colsample_bytree + self.colsample_bylevel = colsample_bylevel + self.colsample_bynode = colsample_bynode + self.gamma = gamma + self.max_depth = max_depth + self.subsample = subsample + self.reg_alpha = reg_alpha + self.reg_lambda = reg_lambda + self.early_stop = early_stop + self.min_rel_progress = min_rel_progress + self.enable_global_explain = enable_global_explain + self.xgboost_version = xgboost_version + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq( + session: bigframes.Session, model: bigquery.Model + ) -> RandomForestClassifier: + assert model.model_type == "RANDOM_FOREST_CLASSIFIER" + + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + + dummy_model = RandomForestClassifier() + for bf_param, bf_value in dummy_model.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param is not None: + kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + + new_random_forest_classifier = RandomForestClassifier(**kwargs) + new_random_forest_classifier._bqml_model = bigframes.ml.core.BqmlModel( + session, model + ) + return new_random_forest_classifier + + @property + def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + """The model options as they will be set for BQML""" + return { + "model_type": "RANDOM_FOREST_CLASSIFIER", + "num_parallel_tree": self.num_parallel_tree, + "tree_method": self.tree_method, + "min_tree_child_weight": self.min_tree_child_weight, + "colsample_bytree": self.colsample_bytree, + "colsample_bylevel": self.colsample_bylevel, + "colsample_bynode": self.colsample_bynode, + "min_split_loss": self.gamma, + "max_tree_depth": self.max_depth, + "subsample": self.subsample, + "l1_reg": self.reg_alpha, + "l2_reg": self.reg_lambda, + "early_stop": self.early_stop, + "min_rel_progress": self.min_rel_progress, + "data_split_method": "NO_SPLIT", + "enable_global_explain": self.enable_global_explain, + "xgboost_version": self.xgboost_version, + } + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + y, + transforms=transforms, + options=self._bqml_options, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) + + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ): + """Calculate evaluation metrics of the model. + + Args: + X: a BigFrames DataFrame as evaluation data. + y: a BigFrames DataFrame as evaluation labels. + + Returns: a BigFrames DataFrame as evaluation result.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + if (X is None) != (y is None): + raise ValueError( + "Either both or neither of test_X and test_y must be specified" + ) + input_data = ( + X.join(y, how="outer") if (X is not None) and (y is not None) else None + ) + return self._bqml_model.evaluate(input_data) + + def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestClassifier: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py new file mode 100644 index 00000000000..24afaad7f26 --- /dev/null +++ b/bigframes/ml/forecasting.py @@ -0,0 +1,125 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import cast, Dict, List, Optional, TYPE_CHECKING + +from google.cloud import bigquery + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.base +import bigframes.ml.core + +_PREDICT_OUTPUT_COLUMNS = ["forecast_timestamp", "forecast_value"] + + +class ARIMAPlus(bigframes.ml.base.TrainablePredictor): + """Time Series ARIMA Plus model.""" + + def __init__(self): + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq(session: bigframes.Session, model: bigquery.Model) -> ARIMAPlus: + assert model.model_type == "ARIMA_PLUS" + + kwargs: Dict[str, str | int | bool | float | List[str]] = {} + + new_arima_plus = ARIMAPlus(**kwargs) + new_arima_plus._bqml_model = bigframes.ml.core.BqmlModel(session, model) + return new_arima_plus + + @property + def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + """The model options as they will be set for BQML.""" + return {"model_type": "ARIMA_PLUS"} + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + transforms: Optional[List[str]] = None, + ): + """Fit the model to training data + + Args: + X: A dataframe of training timestamp. + + y: Target values for training.""" + self._bqml_model = bigframes.ml.core.create_bqml_time_series_model( + X, + y, + transforms=transforms, + options=self._bqml_options, + ) + + def predict(self, X=None) -> bigframes.dataframe.DataFrame: + """Predict the closest cluster for each sample in X. + + Args: + X: ignored, to be compatible with other APIs. + Returns: + The predicted BigQuery DataFrames. Which contains 2 columns + "forecast_timestamp" and "forecast_value". + """ + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + return cast( + bigframes.dataframe.DataFrame, + self._bqml_model.forecast()[_PREDICT_OUTPUT_COLUMNS], + ) + + # Unlike regression models, time series forcasting can only evaluate with unseen data. X and y must be providee. + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ) -> bigframes.dataframe.DataFrame: + """Calculate evaluation metrics of the model. + + Args: + X: + A BigQuery DataFrames only contains 1 column as + evaluation timestamp. The timestamp must be within the horizon + of the model, which by default is 1000 data points. + y: + A BigQuery DataFrames only contains 1 column as + evaluation numeric values. + + Returns: + A BigQuery DataFrames as evaluation result. + """ + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + input_data = X.join(y, how="outer") + return self._bqml_model.evaluate(input_data) + + def to_gbq(self, model_name: str, replace: bool = False) -> ARIMAPlus: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py new file mode 100644 index 00000000000..581ee2b1e24 --- /dev/null +++ b/bigframes/ml/imported.py @@ -0,0 +1,101 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import cast, TYPE_CHECKING + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.base +import bigframes.ml.core + + +class TensorFlowModel(bigframes.ml.base.Predictor): + """Imported TensorFlow model. + + Args: + session: BQ session to create the model + model_path: GCS path that holds the model files.""" + + def __init__(self, session: bigframes.Session, model_path: str): + self.session = session + self.model_path = model_path + self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + options = {"model_type": "TENSORFLOW", "model_path": self.model_path} + return bigframes.ml.core.create_bqml_imported_model( + session=self.session, options=options + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + """Predict the result from input DataFrame. + + Args: + X: Input DataFrame, schema is defined by the model. + + Returns: Output DataFrame, schema is defined by the model.""" + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) + + +class OnnxModel(bigframes.ml.base.BaseEstimator): + """Imported Open Neural Network Exchange (ONNX) model. + + Args: + session: BQ session to create the model + model_path: GCS path that holds the model files.""" + + def __init__(self, session: bigframes.Session, model_path: str): + self.session = session + self.model_path = model_path + self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + options = {"model_type": "ONNX", "model_path": self.model_path} + return bigframes.ml.core.create_bqml_imported_model( + session=self.session, options=options + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + """Predict the result from input DataFrame. + + Args: + X: Input DataFrame, schema is defined by the model. + + Returns: Output DataFrame, schema is defined by the model.""" + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py new file mode 100644 index 00000000000..2b2ea5c2afc --- /dev/null +++ b/bigframes/ml/linear_model.py @@ -0,0 +1,247 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Linear models. This module is styled after Scikit-Learn's linear_model module: +https://scikit-learn.org/stable/modules/linear_model.html""" + +from __future__ import annotations + +from typing import cast, Dict, List, Optional, TYPE_CHECKING + +from google.cloud import bigquery + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.base +import bigframes.ml.core +import third_party.bigframes_vendored.sklearn.linear_model._base +import third_party.bigframes_vendored.sklearn.linear_model._logistic + + +class LinearRegression( + third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression, + bigframes.ml.base.TrainablePredictor, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression.__doc__ + ) + + def __init__( + self, + fit_intercept=True, + ): + self.fit_intercept = fit_intercept + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq(session: bigframes.Session, model: bigquery.Model) -> LinearRegression: + assert model.model_type == "LINEAR_REGRESSION" + + # TODO(bmil): construct a standard way to extract these properties + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + if "fitIntercept" in last_fitting: + kwargs["fit_intercept"] = last_fitting["fitIntercept"] + + new_linear_regression = LinearRegression(**kwargs) + new_linear_regression._bqml_model = bigframes.ml.core.BqmlModel(session, model) + return new_linear_regression + + @property + def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + """The model options as they will be set for BQML""" + return { + "model_type": "LINEAR_REG", + "data_split_method": "NO_SPLIT", + "fit_intercept": self.fit_intercept, + } + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + y, + transforms=transforms, + options=self._bqml_options, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) + + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + input_data = ( + X.join(y, how="outer") if (X is not None) and (y is not None) else None + ) + return self._bqml_model.evaluate(input_data) + + def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) + + +class LogisticRegression( + third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression, + bigframes.ml.base.TrainablePredictor, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression.__doc__ + ) + + # TODO(ashleyxu) support class_weights in the constructor. + def __init__( + self, + fit_intercept: bool = True, + auto_class_weights: bool = False, + ): + self.fit_intercept = fit_intercept + self.auto_class_weights = auto_class_weights + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + @staticmethod + def _from_bq( + session: bigframes.Session, model: bigquery.Model + ) -> LogisticRegression: + assert model.model_type == "LOGISTIC_REGRESSION" + + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = model.training_runs[-1]["trainingOptions"] + if "fitIntercept" in last_fitting: + kwargs["fit_intercept"] = last_fitting["fitIntercept"] + # TODO(ashleyxu): b/285162045 support auto_class_weights once the API is + # fixed and enable the tests. + if "autoClassWeights" in last_fitting: + kwargs["auto_class_weights"] = last_fitting["autoClassWeights"] + # TODO(ashleyxu) support class_weights in the constructor. + # if "labelClassWeights" in last_fitting: + # kwargs["class_weights"] = last_fitting["labelClassWeights"] + + new_logistic_regression = LogisticRegression(**kwargs) + new_logistic_regression._bqml_model = bigframes.ml.core.BqmlModel( + session, model + ) + return new_logistic_regression + + @property + def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: + """The model options as they will be set for BQML""" + return { + "model_type": "LOGISTIC_REG", + "data_split_method": "NO_SPLIT", + "fit_intercept": self.fit_intercept, + "auto_class_weights": self.auto_class_weights, + # TODO(ashleyxu): support class_weights (struct array) + # "class_weights": self.class_weights, + } + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + transforms: Optional[List[str]] = None, + ): + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + y, + transforms=transforms, + options=self._bqml_options, + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + df = self._bqml_model.predict(X) + return cast( + bigframes.dataframe.DataFrame, + df[ + [ + cast(str, field.name) + for field in self._bqml_model.model.label_columns + ] + ], + ) + + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + input_data = ( + X.join(y, how="outer") if (X is not None) and (y is not None) else None + ) + return self._bqml_model.evaluate(input_data) + + def to_gbq(self, model_name: str, replace: bool = False) -> LogisticRegression: + """Save the model to Google Cloud BigQuey. + + Args: + model_name: the name of the model. + replace: whether to replace if the model already exists. Default to False. + + Returns: saved model.""" + if not self._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + # TODO(ashleyxu): b/285162045 support auto_class_weights once the API is + # fixed and enable the tests. + if self.auto_class_weights is True: + raise NotImplementedError("auto_class_weight is not supported yet.") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py new file mode 100644 index 00000000000..ef4f28f1f7d --- /dev/null +++ b/bigframes/ml/llm.py @@ -0,0 +1,168 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import cast + +import bigframes +from bigframes.core import blocks +import bigframes.ml.base +import bigframes.ml.core + +_REMOTE_TEXT_GENERATOR_MODEL_CODE = "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1" +_TEXT_GENERATE_RESULT_COLUMN = "ml_generate_text_llm_result" + +_REMOTE_EMBEDDING_GENERATOR_MODEL_CODE = "CLOUD_AI_TEXT_EMBEDDING_MODEL_V1" +_EMBED_TEXT_RESULT_COLUMN = "ml_embed_text_embedding" + + +class PaLM2TextGenerator(bigframes.ml.base.Predictor): + """PaLM2 text generator LLM model. + + Args: + session: BQ session to create the model + connection_name: connection to connect with remote service. str of the format ..""" + + def __init__(self, session: bigframes.Session, connection_name: str): + self.session = session + self.connection_name = connection_name + self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + options = { + "remote_service_type": _REMOTE_TEXT_GENERATOR_MODEL_CODE, + } + + return bigframes.ml.core.create_bqml_remote_model( + session=self.session, connection_name=self.connection_name, options=options + ) + + def predict( + self, + X: bigframes.dataframe.DataFrame, + temperature: float = 0.0, + max_output_tokens: int = 128, + top_k: int = 40, + top_p: float = 0.95, + ) -> bigframes.dataframe.DataFrame: + """Predict the result from input DataFrame. + + Args: + X: Input DataFrame, which needs to contain a column with name "prompt". Only the column will be used as input. Prompts can include preamble, questions, suggestions, instructions, or examples. + + temperature: The temperature is used for sampling during the response generation, which occurs when topP and topK are applied. + Temperature controls the degree of randomness in token selection. Lower temperatures are good for prompts that expect a true or correct response, + while higher temperatures can lead to more diverse or unexpected results. A temperature of 0 is deterministic: + the highest probability token is always selected. For most use cases, try starting with a temperature of 0.2. + Default 0. + + max_output_tokens: Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. + A token may be smaller than a word. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. + Default 128. + + top_k: Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens + in the model’s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature). + For each token selection step, the top K tokens with the highest probabilities are sampled. Then tokens are further filtered based on topP with the final token selected using temperature sampling. + Specify a lower value for less random responses and a higher value for more random responses. + Default 40. + + top_p: Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value. + For example, if tokens A, B, and C have a probability of 0.3, 0.2, and 0.1 and the top-p value is 0.5, then the model will select either A or B as the next token (using temperature) + and not consider C at all. + Specify a lower value for less random responses and a higher value for more random responses. + Default 0.95. + + + Returns: Output DataFrame with only 1 column as the output text results.""" + + # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models + if temperature < 0.0 or temperature > 1.0: + raise ValueError(f"temperature must be [0.0, 1.0], but is {temperature}.") + if max_output_tokens not in range(1, 1025): + raise ValueError( + f"max_output_token must be [1, 1024], but is {max_output_tokens}." + ) + if top_k not in range(1, 41): + raise ValueError(f"top_k must be [1, 40], but is {top_k}.") + if top_p < 0.0 or top_p > 1.0: + raise ValueError(f"top_p must be [0.0, 1.0], but is {top_p}.") + if len(X.columns) != 1: + raise ValueError("Only support one column as input.") + + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "prompt"}) + + options = { + "temperature": temperature, + "max_output_tokens": max_output_tokens, + "top_k": top_k, + "top_p": top_p, + "flatten_json_output": True, + } + df = self._bqml_model.generate_text(X, options) + return cast( + bigframes.dataframe.DataFrame, + df[[_TEXT_GENERATE_RESULT_COLUMN]], + ) + + +class PaLM2EmbeddingGenerator(bigframes.ml.base.Predictor): + """PaLM2 embedding generator LLM model. + + Args: + session: BQ session to create the model + connection_name: connection to connect with remote service. str of the format ..""" + + def __init__(self, session: bigframes.Session, connection_name: str): + self.session = session + self.connection_name = connection_name + self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + options = { + "remote_service_type": _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE, + } + + return bigframes.ml.core.create_bqml_remote_model( + session=self.session, connection_name=self.connection_name, options=options + ) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + """Predict the result from input DataFrame. + + Args: + X: Input DataFrame, which needs to contain a column with name "content". Only the column will be used as input. Content can include preamble, questions, suggestions, instructions, or examples. + + Returns: Output DataFrame with only 1 column as the output embedding results.""" + + # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models + if len(X.columns) != 1: + raise ValueError("Only support one column as input.") + + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "content"}) + + options = { + "flatten_json_output": True, + } + df = self._bqml_model.embed_text(X, options) + return cast( + bigframes.dataframe.DataFrame, + df[[_EMBED_TEXT_RESULT_COLUMN]], + ) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py new file mode 100644 index 00000000000..cc43b47698e --- /dev/null +++ b/bigframes/ml/loader.py @@ -0,0 +1,74 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Union + +from google.cloud import bigquery + +if TYPE_CHECKING: + import bigframes + +import bigframes.ml.cluster +import bigframes.ml.decomposition +import bigframes.ml.ensemble +import bigframes.ml.forecasting +import bigframes.ml.linear_model + + +def from_bq( + session: bigframes.Session, model: bigquery.Model +) -> Union[ + bigframes.ml.decomposition.PCA, + bigframes.ml.cluster.KMeans, + bigframes.ml.linear_model.LinearRegression, + bigframes.ml.linear_model.LogisticRegression, + bigframes.ml.ensemble.XGBRegressor, + bigframes.ml.ensemble.XGBClassifier, + bigframes.ml.forecasting.ARIMAPlus, + bigframes.ml.ensemble.RandomForestRegressor, + bigframes.ml.ensemble.RandomForestClassifier, +]: + """Load a BQML model to BigQuery DataFrames ML. + + Args: + session: a BigQuery DataFrames session. + model: a BigQuery model. + + Returns: + A BigQuery DataFrames ML model object. + """ + if model.model_type == "LINEAR_REGRESSION": + return bigframes.ml.linear_model.LinearRegression._from_bq(session, model) + elif model.model_type == "KMEANS": + return bigframes.ml.cluster.KMeans._from_bq(session, model) + elif model.model_type == "PCA": + return bigframes.ml.decomposition.PCA._from_bq(session, model) + elif model.model_type == "LOGISTIC_REGRESSION": + return bigframes.ml.linear_model.LogisticRegression._from_bq(session, model) + elif model.model_type == "BOOSTED_TREE_REGRESSOR": + return bigframes.ml.ensemble.XGBRegressor._from_bq(session, model) + elif model.model_type == "BOOSTED_TREE_CLASSIFIER": + return bigframes.ml.ensemble.XGBClassifier._from_bq(session, model) + elif model.model_type == "ARIMA_PLUS": + return bigframes.ml.forecasting.ARIMAPlus._from_bq(session, model) + elif model.model_type == "RANDOM_FOREST_REGRESSOR": + return bigframes.ml.ensemble.RandomForestRegressor._from_bq(session, model) + elif model.model_type == "RANDOM_FOREST_CLASSIFIER": + return bigframes.ml.ensemble.RandomForestClassifier._from_bq(session, model) + else: + raise NotImplementedError( + f"Model type {model.model_type} is not yet supported by BigQuery DataFrames." + ) diff --git a/bigframes/ml/metrics.py b/bigframes/ml/metrics.py new file mode 100644 index 00000000000..861e1f02d10 --- /dev/null +++ b/bigframes/ml/metrics.py @@ -0,0 +1,371 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Metrics functions for evaluating models. This module is styled after +Scikit-Learn's metrics module: https://scikit-learn.org/stable/modules/metrics.html""" + +import inspect +import typing +from typing import Tuple + +import numpy as np +import pandas as pd +import sklearn.metrics as sklearn_metrics # type: ignore + +import bigframes.core.blocks as blocks +import bigframes.pandas as bpd +import third_party.bigframes_vendored.sklearn.metrics._classification as vendored_mertics_classification +import third_party.bigframes_vendored.sklearn.metrics._ranking as vendored_mertics_ranking +import third_party.bigframes_vendored.sklearn.metrics._regression as vendored_metrics_regression + + +def r2_score( + y_true: bpd.DataFrame, + y_pred: bpd.DataFrame, + force_finite=True, +) -> float: + # TODO(bmil): support multioutput + if len(y_true.columns) > 1 or len(y_pred.columns) > 1: + raise NotImplementedError( + "Only one labels column, one predictions column is supported" + ) + + y_true_series = typing.cast( + bpd.Series, y_true[typing.cast(str, y_true.columns.tolist()[0])] + ) + y_pred_series = typing.cast( + bpd.Series, y_pred[typing.cast(str, y_pred.columns.tolist()[0])] + ) + + # total sum of squares + # (dataframe, scalar) binops + # TODO(bmil): remove multiply by self when bigframes supports pow() + # TODO(tbergeron): These stats are eagerly evaluated. Move to lazy representation once scalar subqueries supported. + delta_from_mean = y_true_series - y_true_series.mean() + ss_total = (delta_from_mean * delta_from_mean).sum() + + # residual sum of squares + # (scalar, scalar) binops + # TODO(bmil): remove multiply by self when bigframes supports pow() + delta_from_pred = y_true_series - y_pred_series + ss_res = (delta_from_pred * delta_from_pred).sum() + + if force_finite and ss_total == 0: + return 0.0 if ss_res > 0 else 1.0 + + return 1 - (ss_res / ss_total) + + +r2_score.__doc__ = inspect.getdoc(vendored_metrics_regression.r2_score) + + +def accuracy_score( + y_true: bpd.DataFrame, + y_pred: bpd.DataFrame, + normalize=True, +) -> float: + # TODO(ashleyxu): support sample_weight as the parameter + if len(y_true.columns) != 1 or len(y_pred.columns) != 1: + raise NotImplementedError( + "Only one labels column, one predictions column is supported" + ) + + y_true_series = typing.cast( + bpd.Series, y_true[typing.cast(str, y_true.columns.tolist()[0])] + ) + y_pred_series = typing.cast( + bpd.Series, y_pred[typing.cast(str, y_pred.columns.tolist()[0])] + ) + + # Compute accuracy for each possible representation + # TODO(ashleyxu): add multilabel classification support where y_type + # starts with "multilabel" + score = (y_true_series == y_pred_series).astype(pd.Int64Dtype()) + + if normalize: + return score.mean() + else: + return score.sum() + + +accuracy_score.__doc__ = inspect.getdoc(vendored_mertics_classification.accuracy_score) + + +def roc_curve( + y_true: bpd.DataFrame, + y_score: bpd.DataFrame, + drop_intermediate: bool = True, +) -> Tuple[bpd.Series, bpd.Series, bpd.Series]: + # TODO(bmil): Add multi-class support + # TODO(bmil): Add multi-label support + if len(y_true.columns) > 1 or len(y_score.columns) > 1: + raise NotImplementedError("Only binary classification is supported") + + # TODO(bmil): Implement drop_intermediate + if drop_intermediate: + raise NotImplementedError("drop_intermediate is not yet implemented") + + # TODO(bmil): remove this once bigframes supports the necessary operations + session = y_true._block.expr._session + pd_y_true = y_true.to_pandas() + pd_y_score = y_score.to_pandas() + + # We operate on rows, so, remove the index if there is one + # TODO(bmil): check that the indexes are equivalent before removing + pd_y_true = pd_y_true.reset_index(drop=True) + pd_y_score = pd_y_score.reset_index(drop=True) + + pd_df = pd.DataFrame( + { + "y_true": pd_y_true[pd_y_true.columns[0]], + "y_score": pd_y_score[pd_y_score.columns[0]], + } + ) + + total_positives = pd_df.y_true.sum() + total_negatives = len(pd_df) - total_positives + + pd_df = pd_df.sort_values(by="y_score", ascending=False) + pd_df["cum_tp"] = pd_df.y_true.cumsum() + pd_df["cum_fp"] = (~pd_df.y_true.astype(bool)).cumsum() + + # produce just one data point per y_score + pd_df = pd_df.groupby("y_score", as_index=False).last() + pd_df = pd_df.sort_values(by="y_score", ascending=False) + + pd_df["tpr"] = pd_df.cum_tp / total_positives + pd_df["fpr"] = pd_df.cum_fp / total_negatives + pd_df["thresholds"] = pd_df.y_score + + # sklearn includes an extra datapoint for the origin with threshold np.inf + pd_origin = pd.DataFrame({"tpr": [0.0], "fpr": [0.0], "thresholds": np.inf}) + pd_df = pd.concat([pd_origin, pd_df]) + + df = session.read_pandas(pd_df) + return df.fpr, df.tpr, df.thresholds + + +roc_curve.__doc__ = inspect.getdoc(vendored_mertics_ranking.roc_curve) + + +def roc_auc_score(y_true: bpd.DataFrame, y_score: bpd.DataFrame) -> float: + # TODO(bmil): Add multi-class support + # TODO(bmil): Add multi-label support + if len(y_true.columns) > 1 or len(y_score.columns) > 1: + raise NotImplementedError("Only binary classification is supported") + + fpr, tpr, _ = roc_curve(y_true, y_score, drop_intermediate=False) + + # TODO(bmil): remove this once bigframes supports the necessary operations + pd_fpr = fpr.compute() + pd_tpr = tpr.compute() + + # Use the trapezoid rule to compute the area under the ROC curve + width_diff = pd_fpr.diff().iloc[1:].reset_index(drop=True) + height_avg = (pd_tpr.iloc[:-1] + pd_tpr.iloc[1:].reset_index(drop=True)) / 2 + return (width_diff * height_avg).sum() + + +roc_auc_score.__doc__ = inspect.getdoc(vendored_mertics_ranking.roc_auc_score) + + +def auc( + x: bpd.DataFrame, + y: bpd.DataFrame, +) -> float: + if len(x.columns) != 1 or len(y.columns) != 1: + raise ValueError("Only 1-D data structure is supported") + + # TODO(b/286410053) Support ML exceptions and error handling. + auc = sklearn_metrics.auc(x.to_pandas(), y.to_pandas()) + return auc + + +auc.__doc__ = inspect.getdoc(vendored_mertics_ranking.auc) + + +def confusion_matrix( + y_true: bpd.DataFrame, + y_pred: bpd.DataFrame, +) -> pd.DataFrame: + # TODO(ashleyxu): support labels and sample_weight parameters + # TODO(ashleyxu): support bpd.Series as input type + if len(y_true.columns) != 1 or len(y_pred.columns) != 1: + raise NotImplementedError( + "Only one labels column, one predictions column is supported" + ) + + y_true_column = typing.cast(blocks.Label, y_true.columns[0]) + y_pred_series = typing.cast( + bpd.Series, + y_pred[typing.cast(blocks.Label, y_pred.columns.tolist()[0])], + ) + confusion_df = y_true.assign(y_pred=y_pred_series) + confusion_df = confusion_df.assign(dummy=0) + groupby_count = ( + confusion_df.groupby(by=[y_true_column, "y_pred"], as_index=False) + .count() + .to_pandas() + ) + + unique_values = sorted(set(groupby_count.y_true).union(set(groupby_count.y_pred))) + + confusion_matrix = pd.DataFrame( + 0, index=pd.Index(unique_values), columns=pd.Index(unique_values), dtype=int + ) + + # Loop through the result by rows and columns + for _, row in groupby_count.iterrows(): + y_true = row["y_true"] + y_pred = row["y_pred"] + count = row["dummy"] + confusion_matrix[y_pred][y_true] = count + + return confusion_matrix + + +confusion_matrix.__doc__ = inspect.getdoc( + vendored_mertics_classification.confusion_matrix +) + + +def recall_score( + y_true: bpd.DataFrame, + y_pred: bpd.DataFrame, + average: str = "binary", +) -> pd.Series: + # TODO(ashleyxu): support more average type, default to "binary" + # TODO(ashleyxu): support bpd.Series as input type + if len(y_true.columns) != 1 or len(y_pred.columns) != 1: + raise NotImplementedError( + "Only one labels column, one predictions column is supported" + ) + + if average is not None: + raise NotImplementedError("Only average=None is supported") + + y_true_series = typing.cast( + bpd.Series, + y_true[typing.cast(blocks.Label, y_true.columns.tolist()[0])], + ) + y_pred_series = typing.cast( + bpd.Series, + y_pred[typing.cast(blocks.Label, y_pred.columns.tolist()[0])], + ) + + is_accurate = y_true_series == y_pred_series + unique_labels = ( + bpd.concat([y_true_series, y_pred_series], join="outer") + .drop_duplicates() + .sort_values() + ) + index = unique_labels.to_list() + + recall = ( + is_accurate.groupby(y_true_series).sum() + / is_accurate.groupby(y_true_series).count() + ).compute() + + recall_score = pd.Series(0, index=index) + for i in recall_score.index: + recall_score.loc[i] = recall.loc[i] + + return recall_score + + +recall_score.__doc__ = inspect.getdoc(vendored_mertics_classification.recall_score) + + +def precision_score( + y_true: bpd.DataFrame, + y_pred: bpd.DataFrame, + average: str = "binary", +) -> pd.Series: + # TODO(ashleyxu): support more average type, default to "binary" + # TODO(ashleyxu): support bpd.Series as input type + if len(y_true.columns) != 1 or len(y_pred.columns) != 1: + raise NotImplementedError( + "Only one labels column, one predictions column is supported" + ) + + if average is not None: + raise NotImplementedError("Only average=None is supported") + + y_true_series = typing.cast( + bpd.Series, + y_true[typing.cast(blocks.Label, y_true.columns.tolist()[0])], + ) + y_pred_series = typing.cast( + bpd.Series, + y_pred[typing.cast(blocks.Label, y_pred.columns.tolist()[0])], + ) + + is_accurate = y_true_series == y_pred_series + unique_labels = ( + bpd.concat([y_true_series, y_pred_series], join="outer") + .drop_duplicates() + .sort_values() + ) + index = unique_labels.to_list() + + precision = ( + is_accurate.groupby(y_pred_series).sum() + / is_accurate.groupby(y_pred_series).count() + ).compute() + + precision_score = pd.Series(0, index=index) + for i in precision.index: + precision_score.loc[i] = precision.loc[i] + + return precision_score + + +precision_score.__doc__ = inspect.getdoc( + vendored_mertics_classification.precision_score +) + + +def f1_score( + y_true: bpd.DataFrame, + y_pred: bpd.DataFrame, + average: str = "binary", +) -> pd.Series: + # TODO(ashleyxu): support more average type, default to "binary" + # TODO(ashleyxu): support bpd.Series as input type + if len(y_true.columns) != 1 or len(y_pred.columns) != 1: + raise NotImplementedError( + "Only one labels column, one predictions column is supported" + ) + + if average is not None: + raise NotImplementedError("Only average=None is supported") + + recall = recall_score(y_true, y_pred, average=None) + precision = precision_score(y_true, y_pred, average=None) + + f1_score = pd.Series(0, index=recall.index) + for index in recall.index: + if precision[index] + recall[index] != 0: + f1_score[index] = ( + 2 + * (precision[index] * recall[index]) + / (precision[index] + recall[index]) + ) + else: + f1_score[index] = 0 + + return f1_score + + +f1_score.__doc__ = inspect.getdoc(vendored_mertics_classification.f1_score) diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py new file mode 100644 index 00000000000..73a2fd76a9a --- /dev/null +++ b/bigframes/ml/model_selection.py @@ -0,0 +1,86 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for test/train split and model tuning. This module is styled after +Scikit-Learn's model_selection module: +https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection""" + + +from typing import List, Union + +import bigframes +import bigframes.dataframe + + +def train_test_split( + *dataframes: bigframes.dataframe.DataFrame, + test_size: Union[float, None] = None, + train_size: Union[float, None] = None, + random_state: Union[int, None] = None, +) -> List[bigframes.dataframe.DataFrame]: + """Splits dataframes into random train and test subsets + + Args: + *dataframes: + A sequence of BigQuery DataFrames that can be joined on + their indexes + test_size: + The proportion of the dataset to include in the test split. If + None, this will default to the complement of train_size. If both + are none, it will be set to 0.25. + train_size: + The proportion of the dataset to include in the train split. If + None, this will default to the complement of test_size. + random_state: + A seed to use for randomly choosing the rows of the split. If not + set, a random split will be generated each time. + + Returns: + A list of BigQuery DataFrames. + """ + + # TODO(garrettwu): Scikit-Learn throws an error when the dataframes don't have the same + # number of rows. We probably want to do something similar. Now the implementation is based + # on index. We'll move to based on ordering first. + + if test_size is None: + if train_size is None: + test_size = 0.25 + else: + test_size = 1.0 - train_size + if train_size is None: + train_size = 1.0 - test_size + + if train_size <= 0.0 or train_size >= 1.0: + raise ValueError(f"train_size must be within (0.0, 1.0). But is {train_size}.") + + if test_size <= 0.0 or test_size >= 1.0: + raise ValueError(f"test_size must be within (0.0, 1.0). But is {test_size}.") + + if train_size + test_size > 1.0: + raise ValueError( + f"The sum of train_size and test_size exceeds 1.0. train_size: {train_size}. test_size: {test_size}" + ) + + results = dataframes[0]._split( + fracs=(train_size, test_size), random_state=random_state + ) + train_index = results[0].index + test_index = results[1].index + + results += [ + df.loc[index] for df in dataframes[1:] for index in (train_index, test_index) + ] + + return results diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py new file mode 100644 index 00000000000..022ba0148d7 --- /dev/null +++ b/bigframes/ml/pipeline.py @@ -0,0 +1,102 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""For composing estimators together. This module is styled after Scikit-Learn's +pipeline module: https://scikit-learn.org/stable/modules/pipeline.html""" + + +from __future__ import annotations + +from typing import List, Optional, Tuple + +import bigframes +from bigframes.ml import base, cluster, compose, decomposition, preprocessing +import third_party.bigframes_vendored.sklearn.pipeline + + +class Pipeline( + third_party.bigframes_vendored.sklearn.pipeline.Pipeline, + base.BaseEstimator, +): + __doc__ = third_party.bigframes_vendored.sklearn.pipeline.Pipeline.__doc__ + + def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): + self.steps = steps + + if len(steps) != 2: + raise NotImplementedError( + "Currently only two step (transform, estimator) pipelines are supported" + ) + + transform, estimator = steps[0][1], steps[1][1] + if isinstance( + transform, + ( + compose.ColumnTransformer, + preprocessing.StandardScaler, + preprocessing.OneHotEncoder, + ), + ): + self._transform = transform + else: + raise NotImplementedError( + f"Transform {transform} is not yet supported by Pipeline" + ) + + if not isinstance( + estimator, + base.TrainablePredictor, + ): + raise NotImplementedError( + f"Estimator {estimator} is not supported by Pipeline" + ) + + self._transform = transform + self._estimator = estimator + + def fit( + self, + X: bigframes.dataframe.DataFrame, + y: Optional[bigframes.dataframe.DataFrame] = None, + ): + compiled_transforms = self._transform._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + if y is not None: + # If labels columns are present, they should pass through un-transformed + transform_sqls.extend(y.columns.tolist()) + + self._estimator.fit(X=X, y=y, transforms=transform_sqls) + + def predict( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + return self._estimator.predict(X) + + def score( + self, + X: bigframes.dataframe.DataFrame, + y: bigframes.dataframe.DataFrame, + ): + if isinstance(self._estimator, (cluster.KMeans, decomposition.PCA)): + raise NotImplementedError("KMeans/PCA haven't supported score method.") + + # TODO(b/289280565): remove type ignore after updating KMeans and PCA + return self._estimator.score(X=X, y=y) # type: ignore + + def to_gbq(self, model_name: str, replace: bool = False): + self._estimator.to_gbq(model_name, replace) + + # TODO: should instead load from GBQ, but loading pipelines is not implemented yet + return self diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py new file mode 100644 index 00000000000..59d2cc2ae9e --- /dev/null +++ b/bigframes/ml/preprocessing.py @@ -0,0 +1,140 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Transformers that prepare data for other estimators. This module is styled after +Scikit-Learn's preprocessing module: https://scikit-learn.org/stable/modules/preprocessing.html""" + + +import typing +from typing import List, Optional, Tuple + +import bigframes +import bigframes.ml +import bigframes.ml.sql +import third_party.bigframes_vendored.sklearn.preprocessing._data +import third_party.bigframes_vendored.sklearn.preprocessing._encoder + + +class StandardScaler( + third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler, + bigframes.ml.base.BaseEstimator, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler.__doc__ + ) + + def __init__(self): + self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + + def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: a list of column names to transform + + Returns: a list of tuples of (sql_expression, output_name)""" + return [ + ( + bigframes.ml.sql.ml_standard_scaler(column, f"scaled_{column}"), + f"scaled_{column}", + ) + for column in columns + ] + + def fit( + self, + X: bigframes.dataframe.DataFrame, + ): + compiled_transforms = self._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + + def transform( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + df = self._bqml_model.transform(X) + return typing.cast( + bigframes.dataframe.DataFrame, + df[self._output_names], + ) + + +class OneHotEncoder( + third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, + bigframes.ml.base.BaseEstimator, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder.__doc__ + ) + + # All estimators must implement __init__ to document their parameters, even + # if they don't have any + def __init__(self): + pass + + def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: a list of column names to transform + + Returns: a list of tuples of (sql_expression, output_name)""" + return [ + ( + bigframes.ml.sql.ml_one_hot_encoder(column, f"onehotencoded_{column}"), + f"onehotencoded_{column}", + ) + for column in columns + ] + + def fit( + self, + X: bigframes.dataframe.DataFrame, + ): + compiled_transforms = self._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = bigframes.ml.core.create_bqml_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + + def transform( + self, X: bigframes.dataframe.DataFrame + ) -> bigframes.dataframe.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + df = self._bqml_model.transform(X) + return typing.cast( + bigframes.dataframe.DataFrame, + df[self._output_names], + ) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py new file mode 100644 index 00000000000..7017b2a4cd1 --- /dev/null +++ b/bigframes/ml/sql.py @@ -0,0 +1,173 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Generates SQL queries needed for BigQuery DataFrames ML +""" + +from typing import Iterable, Optional, Union + + +def _encode_value(v: Union[str, int, float, Iterable[str]]) -> str: + """Encode a parameter value for SQL""" + if isinstance(v, str): + return f'"{v}"' + elif isinstance(v, int) or isinstance(v, float): + return f"{v}" + elif isinstance(v, Iterable): + inner = ", ".join([_encode_value(x) for x in v]) + return f"[{inner}]" + else: + raise ValueError("Unexpected value type") + + +def _build_param_Iterable(**kwargs: Union[str, int, float, Iterable[str]]) -> str: + """Encode a dict of values into a formatted Iterable of KVPs for SQL""" + indent_str = " " + param_strs = [f"{k}={_encode_value(v)}" for k, v in kwargs.items()] + return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + + +def options(**kwargs: Union[str, int, float, Iterable[str]]) -> str: + """Encode the OPTIONS clause for BQML""" + return f"OPTIONS({_build_param_Iterable(**kwargs)})" + + +def _build_struct_param_Iterable(**kwargs: Union[int, float]) -> str: + """Encode a dict of values into a formatted STRUCT items for SQL""" + indent_str = " " + param_strs = [f"{v} AS {k}" for k, v in kwargs.items()] + return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + + +def struct_options(**kwargs: Union[int, float]) -> str: + """Encode a BQ STRUCT as options.""" + return f"STRUCT({_build_struct_param_Iterable(**kwargs)})" + + +def _build_expr_Iterable(*expr_sqls: str) -> str: + """Encode a Iterable of SQL expressions into a formatted Iterable for SQL""" + indent_str = " " + return "\n" + indent_str + f",\n{indent_str}".join(expr_sqls) + + +def transform(*expr_sqls: str) -> str: + """Encode the TRANSFORM clause for BQML""" + return f"TRANSFORM({_build_expr_Iterable(*expr_sqls)})" + + +def connection(conn_name: str) -> str: + """Encode the REMOTE WITH CONNECTION clause for BQML. conn_name is of the format ...""" + return f"REMOTE WITH CONNECTION `{conn_name}`" + + +def ml_standard_scaler(numeric_expr_sql: str, name: str) -> str: + """Encode ML.STANDARD_SCALER for BQML""" + return f"""ML.STANDARD_SCALER({numeric_expr_sql}) OVER() AS {name}""" + + +def ml_one_hot_encoder(numeric_expr_sql: str, name: str) -> str: + """Encode ML.ONE_HOT_ENCODER for BQML""" + return f"""ML.ONE_HOT_ENCODER({numeric_expr_sql}) OVER() AS {name}""" + + +def create_model( + model_name: str, + source_sql: str, + transform_sql: Optional[str] = None, + options_sql: Optional[str] = None, +) -> str: + """Encode the CREATE MODEL statement for BQML""" + # TODO(garrettwu): This should be CREATE TEMP MODEL after b/145824779 is fixed + parts = [f"CREATE MODEL `{model_name}`"] + if transform_sql: + parts.append(transform_sql) + if options_sql: + parts.append(options_sql) + parts.append(f"AS {source_sql}") + return "\n".join(parts) + + +def create_remote_model( + model_name: str, + connection_name: str, + options_sql: Optional[str] = None, +) -> str: + """Encode the CREATE MODEL statement for BQML""" + # TODO(garrettwu): This should be CREATE TEMP MODEL after b/145824779 is fixed + parts = [f"CREATE MODEL `{model_name}`"] + parts.append(connection(connection_name)) + if options_sql: + parts.append(options_sql) + return "\n".join(parts) + + +def create_imported_model( + model_name: str, + options_sql: Optional[str] = None, +) -> str: + """Encode the CREATE MODEL statement for BQML""" + # TODO(garrettwu): This should be CREATE TEMP MODEL after b/145824779 is fixed + parts = [f"CREATE MODEL `{model_name}`"] + if options_sql: + parts.append(options_sql) + return "\n".join(parts) + + +def alter_model( + model_name: str, + options_sql: str, +) -> str: + """Encode the ALTER MODEL statement for BQML""" + parts = [f"ALTER MODEL `{model_name}`"] + parts.append(f"SET {options_sql}") + return "\n".join(parts) + + +def ml_evaluate(model_name: str, source_sql: Union[str, None] = None) -> str: + """Encode ML.EVALUATE for BQML""" + if source_sql is None: + return f"""SELECT * FROM ML.EVALUATE(MODEL `{model_name}`)""" + else: + return f"""SELECT * FROM ML.EVALUATE(MODEL `{model_name}`, + ({source_sql}))""" + + +def ml_predict(model_name: str, source_sql: str) -> str: + """Encode ML.PREDICT for BQML""" + return f"""SELECT * FROM ML.PREDICT(MODEL `{model_name}`, + ({source_sql}))""" + + +def ml_transform(model_name: str, source_sql: str) -> str: + """Encode ML.TRANSFORM for BQML""" + return f"""SELECT * FROM ML.TRANSFORM(MODEL `{model_name}`, + ({source_sql}))""" + + +def ml_generate_text(model_name: str, source_sql: str, struct_options: str) -> str: + """Encode ML.GENERATE_TEXT for BQML""" + return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL `{model_name}`, + ({source_sql}), {struct_options})""" + + +def ml_embed_text(model_name: str, source_sql: str, struct_options: str) -> str: + """Encode ML.EMBED_TEXT for BQML""" + return f"""SELECT * FROM ML.EMBED_TEXT(MODEL `{model_name}`, + ({source_sql}), {struct_options})""" + + +def ml_forecast(model_name: str) -> str: + """Encode ML.FORECAST for BQML""" + return f"""SELECT * FROM ML.FORECAST(MODEL `{model_name}`)""" diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py new file mode 100644 index 00000000000..781cc493397 --- /dev/null +++ b/bigframes/operations/__init__.py @@ -0,0 +1,546 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import functools +import typing + +import ibis +import ibis.common.exceptions +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.operations.generic +import ibis.expr.types as ibis_types +import numpy as np + +import bigframes.dtypes +import bigframes.dtypes as dtypes + +_ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) +_INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.inf)) + +BinaryOp = typing.Callable[[ibis_types.Value, ibis_types.Value], ibis_types.Value] +TernaryOp = typing.Callable[ + [ibis_types.Value, ibis_types.Value, ibis_types.Value], ibis_types.Value +] + + +### Unary Ops +class UnaryOp: + def _as_ibis(self, x): + raise NotImplementedError("Base class UnaryOp has no implementation.") + + @property + def is_windowed(self): + return False + + +class AbsOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).abs() + + +class InvertOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).negate() + + +class IsNullOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return x.isnull() + + +class LenOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).length() + + +class NotNullOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return x.notnull() + + +class ReverseOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).reverse() + + +class LowerOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).lower() + + +class UpperOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).upper() + + +class StripOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).strip() + + +class IsNumericOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + # catches all members of the Unicode number class, which matches pandas isnumeric + # see https://cloud.google.com/bigquery/docs/reference/standard-sql/string_functions#regexp_contains + return typing.cast(ibis_types.StringValue, x).re_search(r"^(\pN*)$") + + +class RstripOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).rstrip() + + +class LstripOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).lstrip() + + +class CapitalizeOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).capitalize() + + +class HashOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.IntegerValue, x).hash() + + +class DayOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).day() + + +class DateOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).date() + + +class DayofweekOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).day_of_week.index() + + +class HourOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).hour() + + +class MinuteOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).minute() + + +class MonthOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).month() + + +class QuarterOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).quarter() + + +class SecondOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).second() + + +class TimeOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).time() + + +class YearOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).year() + + +# Parameterized ops +class AsTypeOp(UnaryOp): + def __init__(self, to_type: dtypes.DtypeString | dtypes.Dtype): + self.to_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(to_type) + + def _as_ibis(self, x: ibis_types.Value): + if isinstance(x, ibis_types.NullScalar): + return ibis_types.null().cast(self.to_type) + + return bigframes.dtypes.cast_ibis_value(x, self.to_type) + + +class FindOp(UnaryOp): + def __init__(self, sub, start, end): + self._sub = sub + self._start = start + self._end = end + + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).find( + self._sub, self._start, self._end + ) + + +class SliceOp(UnaryOp): + def __init__(self, start, stop): + self._start = start + self._stop = stop + + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x)[self._start : self._stop] + + +class BinopPartialRight(UnaryOp): + def __init__(self, binop: BinaryOp, right_scalar: typing.Any): + self._binop = binop + self._right = dtypes.literal_to_ibis_scalar(right_scalar, validate=False) + + def _as_ibis(self, x): + return self._binop(x, self._right) + + +class BinopPartialLeft(UnaryOp): + def __init__(self, binop: BinaryOp, left_scalar: typing.Any): + self._binop = binop + self._left = dtypes.literal_to_ibis_scalar(left_scalar, validate=False) + + def _as_ibis(self, x): + return self._binop(self._left, x) + + +class RepeatOp(UnaryOp): + def __init__(self, repeats): + self._repeats = repeats + + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).repeat(self._repeats) + + +class RemoteFunctionOp(UnaryOp): + def __init__(self, func: typing.Callable, apply_on_null=True): + if not hasattr(func, "bigframes_remote_function"): + raise TypeError( + "only a bigframes remote function is supported as a callable" + ) + + self._func = func + self._apply_on_null = apply_on_null + + def _as_ibis(self, x: ibis_types.Value): + x_transformed = self._func(x) + if not self._apply_on_null: + x_transformed = where_op(x, x.isnull(), x_transformed) + return x_transformed + + +abs_op = AbsOp() +invert_op = InvertOp() +isnull_op = IsNullOp() +len_op = LenOp() +notnull_op = NotNullOp() +reverse_op = ReverseOp() +lower_op = LowerOp() +upper_op = UpperOp() +strip_op = StripOp() +isnumeric_op = IsNumericOp() +rstrip_op = RstripOp() +lstrip_op = LstripOp() +hash_op = HashOp() +day_op = DayOp() +dayofweek_op = DayofweekOp() +date_op = DateOp() +hour_op = HourOp() +minute_op = MinuteOp() +month_op = MonthOp() +quarter_op = QuarterOp() +second_op = SecondOp() +time_op = TimeOp() +year_op = YearOp() +capitalize_op = CapitalizeOp() + + +### Binary Ops +def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): + """Wraps a binary operator to generate nulls of the expected type if either input is a null scalar.""" + + def short_circuit_nulls_inner(binop): + @functools.wraps(binop) + def wrapped_binop(x: ibis_types.Value, y: ibis_types.Value): + if isinstance(x, ibis_types.NullScalar): + return ibis_types.null().cast(type_override or y.type()) + elif isinstance(y, ibis_types.NullScalar): + return ibis_types.null().cast(type_override or x.type()) + else: + return binop(x, y) + + return wrapped_binop + + return short_circuit_nulls_inner + + +def concat_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + x_string = typing.cast(ibis_types.StringValue, x) + y_string = typing.cast(ibis_types.StringValue, y) + return x_string.concat(y_string) + + +def eq_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x == y + + +def ne_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x != y + + +def and_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.BooleanValue, x) & typing.cast( + ibis_types.BooleanValue, y + ) + + +def or_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.BooleanValue, x) | typing.cast( + ibis_types.BooleanValue, y + ) + + +def isin_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x.isin(y) + + +@short_circuit_nulls() +def add_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + if isinstance(x, ibis_types.NullScalar) or isinstance(x, ibis_types.NullScalar): + return + return typing.cast(ibis_types.NumericValue, x) + typing.cast( + ibis_types.NumericValue, y + ) + + +@short_circuit_nulls() +def sub_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.NumericValue, x) - typing.cast( + ibis_types.NumericValue, y + ) + + +@short_circuit_nulls() +def mul_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.NumericValue, x) * typing.cast( + ibis_types.NumericValue, y + ) + + +@short_circuit_nulls(ibis_dtypes.float) +def div_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.NumericValue, x) / typing.cast( + ibis_types.NumericValue, y + ) + + +@short_circuit_nulls(ibis_dtypes.bool) +def lt_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x < y + + +@short_circuit_nulls(ibis_dtypes.bool) +def le_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x <= y + + +@short_circuit_nulls(ibis_dtypes.bool) +def gt_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x > y + + +@short_circuit_nulls(ibis_dtypes.bool) +def ge_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x >= y + + +@short_circuit_nulls(ibis_dtypes.int) +def floordiv_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + x_numeric = typing.cast(ibis_types.NumericValue, x) + y_numeric = typing.cast(ibis_types.NumericValue, y) + floordiv_expr = x_numeric // y_numeric + + # DIV(N, 0) will error in bigquery, but needs to return 0 for int, and inf for float in BQ so we short-circuit in this case. + # Multiplying left by zero propogates nulls. + zero_result = _INF if (x.type().is_floating() or y.type().is_floating()) else _ZERO + return ( + ibis.case() + .when(y_numeric == _ZERO, zero_result * x_numeric) + .else_(floordiv_expr) + .end() + ) + + +@short_circuit_nulls() +def mod_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + # TODO(tbergeron): fully support floats, including when mixed with integer + # Pandas has inconsitency about whether N mod 0. Most conventions have this be NAN. + # For some dtypes, the result is 0 instead. This implementation results in NA always. + x_numeric = typing.cast(ibis_types.NumericValue, x) + y_numeric = typing.cast(ibis_types.NumericValue, y) + # Hacky short-circuit to avoid passing zero-literal to sql backend, evaluate locally instead to null. + op = y.op() + if isinstance(op, ibis.expr.operations.generic.Literal) and op.value == 0: + return ibis_types.null().cast(x.type()) + + bq_mod = x_numeric % y_numeric # Bigquery will maintain x sign here + # In BigQuery returned value has the same sign as X. In pandas, the sign of y is used, so we need to flip the result if sign(x) != sign(y) + return ( + ibis.case() + .when( + y_numeric == _ZERO, _ZERO * x_numeric + ) # Dummy op to propogate nulls and type from x arg + .when( + (y_numeric < _ZERO) & (bq_mod > _ZERO), (y_numeric + bq_mod) + ) # Convert positive result to negative + .when( + (y_numeric > _ZERO) & (bq_mod < _ZERO), (y_numeric + bq_mod) + ) # Convert negative result to positive + .else_(bq_mod) + .end() + ) + + +def fillna_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x.fillna(typing.cast(ibis_types.Scalar, y)) + + +def clip_lower( + value: ibis_types.Value, + lower: ibis_types.Value, +): + return ibis.case().when(lower.isnull() | (value < lower), lower).else_(value).end() + + +def clip_upper( + value: ibis_types.Value, + upper: ibis_types.Value, +): + return ibis.case().when(upper.isnull() | (value > upper), upper).else_(value).end() + + +def reverse(op: BinaryOp) -> BinaryOp: + return lambda x, y: op(y, x) + + +def partial_left(op: BinaryOp, scalar: typing.Any) -> UnaryOp: + return BinopPartialLeft(op, scalar) + + +def partial_right(op: BinaryOp, scalar: typing.Any) -> UnaryOp: + return BinopPartialRight(op, scalar) + + +# Ternary ops +def where_op( + original: ibis_types.Value, + condition: ibis_types.Value, + replacement: ibis_types.Value, +) -> ibis_types.Value: + """Returns x if y is true, otherwise returns z.""" + return ibis.case().when(condition, original).else_(replacement).end() + + +def clip_op( + original: ibis_types.Value, + lower: ibis_types.Value, + upper: ibis_types.Value, +) -> ibis_types.Value: + """Clips value to lower and upper bounds.""" + if isinstance(lower, ibis_types.NullScalar) and ( + not isinstance(upper, ibis_types.NullScalar) + ): + return ( + ibis.case() + .when(upper.isnull() | (original > upper), upper) + .else_(original) + .end() + ) + elif (not isinstance(lower, ibis_types.NullScalar)) and isinstance( + upper, ibis_types.NullScalar + ): + return ( + ibis.case() + .when(lower.isnull() | (original < lower), lower) + .else_(original) + .end() + ) + elif isinstance(lower, ibis_types.NullScalar) and ( + isinstance(upper, ibis_types.NullScalar) + ): + return original + else: + # Note: Pandas has unchanged behavior when upper bound and lower bound are flipped. This implementation requires that lower_bound < upper_bound + return ( + ibis.case() + .when(lower.isnull() | (original < lower), lower) + .when(upper.isnull() | (original > upper), upper) + .else_(original) + .end() + ) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py new file mode 100644 index 00000000000..028ed4b6065 --- /dev/null +++ b/bigframes/operations/aggregations.py @@ -0,0 +1,288 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing + +import ibis +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types + + +class WindowOp: + def _as_ibis(self, value: ibis_types.Column, window=None): + raise NotImplementedError("Base class WindowOp has no implementaiton.") + + @property + def skips_nulls(self): + """Whether the window op skips null rows.""" + return True + + @property + def handles_ties(self): + """Whether the operator can handle ties without nondeterministic output. (eg. rank operator can handle ties but not the count operator)""" + return False + + +class AggregateOp(WindowOp): + def _as_ibis(self, value: ibis_types.Column, window=None): + raise NotImplementedError("Base class AggregateOp has no implementaiton.") + + +def numeric_op(operation): + def constrained_op(op, column: ibis_types.Column, window=None): + if column.type().is_boolean(): + column = typing.cast( + ibis_types.NumericColumn, column.cast(ibis_dtypes.int64) + ) + if column.type().is_numeric(): + return operation(op, column, window) + else: + raise ValueError( + f"Numeric operation cannot be applied to type {column.type()}" + ) + + return constrained_op + + +class SumOp(AggregateOp): + @numeric_op + def _as_ibis( + self, column: ibis_types.NumericColumn, window=None + ) -> ibis_types.NumericValue: + # Will be null if all inputs are null. Pandas defaults to zero sum though. + bq_sum = _apply_window_if_present(column.sum(), window) + return ( + ibis.case().when(bq_sum.isnull(), ibis_types.literal(0)).else_(bq_sum).end() + ) + + +class MeanOp(AggregateOp): + @numeric_op + def _as_ibis( + self, column: ibis_types.NumericColumn, window=None + ) -> ibis_types.NumericValue: + return _apply_window_if_present(column.mean(), window) + + +class ProductOp(AggregateOp): + @numeric_op + def _as_ibis( + self, column: ibis_types.NumericColumn, window=None + ) -> ibis_types.NumericValue: + # Need to short-circuit as log with zeroes is illegal sql + is_zero = typing.cast(ibis_types.BooleanColumn, (column == 0)) + + # There is no product sql aggregate function, so must implement as a sum of logs, and then + # apply power after. Note, log and power base must be equal! This impl uses base 2. + logs = typing.cast( + ibis_types.NumericColumn, + ibis.case().when(is_zero, 0).else_(column.abs().log2()).end(), + ) + logs_sum = _apply_window_if_present(logs.sum(), window) + magnitude = typing.cast(ibis_types.NumericValue, ibis_types.literal(2)).pow( + logs_sum + ) + + # Can't determine sign from logs, so have to determine parity of count of negative inputs + is_negative = typing.cast( + ibis_types.NumericColumn, + ibis.case().when(column.sign() == -1, 1).else_(0).end(), + ) + negative_count = _apply_window_if_present(is_negative.sum(), window) + negative_count_parity = negative_count % typing.cast( + ibis_types.NumericValue, ibis.literal(2) + ) # 1 if result should be negative, otherwise 0 + + any_zeroes = _apply_window_if_present(is_zero.any(), window) + float_result = ( + ibis.case() + .when(any_zeroes, ibis_types.literal(0)) + .else_(magnitude * pow(-1, negative_count_parity)) + .end() + ) + return float_result.cast(column.type()) + + +class MaxOp(AggregateOp): + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.max(), window) + + +class MinOp(AggregateOp): + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.min(), window) + + +class StdOp(AggregateOp): + @numeric_op + def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present( + typing.cast(ibis_types.NumericColumn, x).std(), window + ) + + +class VarOp(AggregateOp): + @numeric_op + def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present( + typing.cast(ibis_types.NumericColumn, x).var(), window + ) + + +class CountOp(AggregateOp): + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.IntegerValue: + return _apply_window_if_present(column.count(), window) + + @property + def skips_nulls(self): + return False + + +class NuniqueOp(AggregateOp): + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.IntegerValue: + return _apply_window_if_present(column.nunique(), window) + + @property + def skips_nulls(self): + return False + + +class RankOp(WindowOp): + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.IntegerValue: + # Ibis produces 0-based ranks, while pandas creates 1-based ranks + return _apply_window_if_present(column.rank(), window) + 1 + + @property + def skips_nulls(self): + return False + + @property + def handles_ties(self): + return True + + +class DenseRankOp(WindowOp): + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.IntegerValue: + # Ibis produces 0-based ranks, while pandas creates 1-based ranks + return _apply_window_if_present(column.dense_rank(), window) + 1 + + @property + def skips_nulls(self): + return False + + @property + def handles_ties(self): + return True + + +class FirstOp(WindowOp): + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.first(), window) + + +class ShiftOp(WindowOp): + def __init__(self, periods: int): + self._periods = periods + + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + if self._periods == 0: # No-op + return column + if self._periods > 0: + return _apply_window_if_present(column.lag(self._periods), window) + return _apply_window_if_present(column.lead(-self._periods), window) + + @property + def skips_nulls(self): + return False + + +class AllOp(AggregateOp): + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.BooleanValue: + # BQ will return null for empty column, result would be true in pandas. + result = _is_true(column).all() + return typing.cast( + ibis_types.BooleanScalar, + _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), + ) + + +class AnyOp(AggregateOp): + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.BooleanValue: + # BQ will return null for empty column, result would be false in pandas. + result = _is_true(column).any() + return typing.cast( + ibis_types.BooleanScalar, + _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), + ) + + +def _is_true(column: ibis_types.Column) -> ibis_types.BooleanColumn: + if column.type().is_boolean(): + return typing.cast(ibis_types.BooleanColumn, column) + elif column.type().is_numeric(): + result = typing.cast(ibis_types.NumericColumn, column).__ne__( + ibis_types.literal(0) + ) + return typing.cast(ibis_types.BooleanColumn, result) + elif column.type().is_string(): + result = typing.cast( + ibis_types.StringValue, column + ).length() > ibis_types.literal(0) + return typing.cast(ibis_types.BooleanColumn, result) + else: + # Time and geo values don't have a 'False' value + return typing.cast( + ibis_types.BooleanColumn, _map_to_literal(column, ibis_types.literal(True)) + ) + + +def _apply_window_if_present(value: ibis_types.Value, window): + return value.over(window) if (window is not None) else value + + +def _map_to_literal( + original: ibis_types.Value, literal: ibis_types.Scalar +) -> ibis_types.Column: + # Hack required to perform aggregations on literals in ibis, even though bigquery will let you directly aggregate literals (eg. 'SELECT COUNT(1) from table1') + return ibis.ifelse(original.isnull(), literal, literal) + + +sum_op = SumOp() +mean_op = MeanOp() +product_op = ProductOp() +max_op = MaxOp() +min_op = MinOp() +std_op = StdOp() +var_op = VarOp() +count_op = CountOp() +nunique_op = NuniqueOp() +rank_op = RankOp() +dense_rank_op = DenseRankOp() +all_op = AllOp() +any_op = AnyOp() +first_op = FirstOp() diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py new file mode 100644 index 00000000000..29017d2bbe3 --- /dev/null +++ b/bigframes/operations/base.py @@ -0,0 +1,198 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing + +import ibis.expr.types as ibis_types +import pandas as pd + +import bigframes.core.blocks as blocks +import bigframes.core.scalar as scalars +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.series as series +import bigframes.session +import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing + +# BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. +# TODO(tbergeron): Convert to bytes-based limit +MAX_INLINE_SERIES_SIZE = 5000 + + +class SeriesMethods: + def __init__( + self, + data=None, + index: vendored_pandas_typing.Axes | None = None, + dtype: typing.Optional[ + bigframes.dtypes.DtypeString | bigframes.dtypes.Dtype + ] = None, + name: str | None = None, + copy: typing.Optional[bool] = None, + *, + session: typing.Optional[bigframes.session.Session] = None, + ): + block = None + if copy is not None and not copy: + raise ValueError("Series constructor only supports copy=True") + if isinstance(data, blocks.Block): + assert len(data.value_columns) == 1 + assert len(data.column_labels) == 1 + block = data + + elif isinstance(data, SeriesMethods): + block = data._get_block() + + if block: + if name: + if not isinstance(name, str): + raise NotImplementedError( + "BigQuery DataFrames only supports string series names." + ) + block = block.with_column_labels([name]) + if index: + raise NotImplementedError( + "Series 'index' constructor parameter not supported when passing BigQuery-backed objects" + ) + if dtype: + block = block.multi_apply_unary_op( + block.value_columns, ops.AsTypeOp(dtype) + ) + self._block = block + + else: + import bigframes.pandas + + pd_series = pd.Series( + data=data, index=index, dtype=dtype, name=name # type:ignore + ) + pd_dataframe = pd_series.to_frame() + if pd_series.name is None: + # to_frame will set default numeric column label if unnamed, but we do not support int column label, so must rename + pd_dataframe = pd_dataframe.set_axis(["unnamed_col"], axis=1) + if pd_dataframe.size < MAX_INLINE_SERIES_SIZE: + self._block = blocks.block_from_local( + pd_dataframe, session or bigframes.pandas.get_global_session() + ) + elif session: + self._block = session.read_pandas(pd_dataframe)._get_block() + else: + # Uses default global session + self._block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() + if pd_series.name is None: + self._block = self._block.with_column_labels([None]) + + @property + def _value(self) -> ibis_types.Value: + """Private property to get Ibis expression for the value column.""" + return self._block.expr.get_column(self._value_column) + + @property + def _value_column(self) -> str: + return self._block.value_columns[0] + + @property + def _name(self) -> blocks.Label: + return self._block.column_labels[0] + + @property + def _dtype(self): + return self._block.dtypes[0] + + def _set_block(self, block: blocks.Block): + self._block = block + + def _get_block(self) -> blocks.Block: + return self._block + + def _apply_unary_op( + self, + op: ops.UnaryOp, + ) -> series.Series: + """Applies a unary operator to the series.""" + block, result_id = self._block.apply_unary_op( + self._value_column, op, result_label=self._name + ) + return series.Series(block.select_column(result_id)) + + def _apply_binary_op( + self, + other: typing.Any, + op: ops.BinaryOp, + alignment: typing.Literal["outer", "left"] = "outer", + ) -> series.Series: + """Applies a binary operator to the series and other.""" + if isinstance(other, pd.Series): + # TODO: Convert to BigQuery DataFrames series + raise NotImplementedError( + "Pandas series not supported supported as operand." + ) + if isinstance(other, series.Series): + (left, right, block) = self._align(other, how=alignment) + + block, result_id = block.apply_binary_op( + left, right, op, self._value_column + ) + + name = self._name + if ( + isinstance(other, series.Series) + and other.name != self._name + and alignment == "outer" + ): + name = None + + return series.Series( + block.select_column(result_id).assign_label(result_id, name) + ) + else: + partial_op = ops.BinopPartialRight(op, other) + return self._apply_unary_op(partial_op) + + def _align(self, other: series.Series, how="outer") -> tuple[str, str, blocks.Block]: # type: ignore + """Aligns the series value with another scalar or series object. Returns new left column id, right column id and joined tabled expression.""" + values, block = self._align_n( + [ + other, + ], + how, + ) + return (values[0], values[1], block) + + def _align_n( + self, + others: typing.Sequence[typing.Union[series.Series, scalars.Scalar]], + how="outer", + ) -> tuple[typing.Sequence[str], blocks.Block]: + value_ids = [self._value_column] + block = self._block + for other in others: + if isinstance(other, series.Series): + combined_index, ( + get_column_left, + get_column_right, + ) = block.index.join(other._block.index, how=how) + value_ids = [ + *[get_column_left(value) for value in value_ids], + get_column_right(other._value_column), + ] + block = combined_index._block + else: + # Will throw if can't interpret as scalar. + dtype = typing.cast(bigframes.dtypes.Dtype, self._dtype) + block, constant_col_id = block.create_constant(other, dtype=dtype) + value_ids = [*value_ids, constant_col_id] + return (value_ids, block) diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py new file mode 100644 index 00000000000..1b20c2d593d --- /dev/null +++ b/bigframes/operations/datetimes.py @@ -0,0 +1,66 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import bigframes.operations as ops +import bigframes.operations.base +import bigframes.series as series +import third_party.bigframes_vendored.pandas.core.indexes.accessor as vendordt + + +class DatetimeMethods( + bigframes.operations.base.SeriesMethods, vendordt.DatetimeProperties +): + __doc__ = vendordt.DatetimeProperties.__doc__ + + @property + def day(self) -> series.Series: + return self._apply_unary_op(ops.day_op) + + @property + def dayofweek(self) -> series.Series: + return self._apply_unary_op(ops.dayofweek_op) + + @property + def date(self) -> series.Series: + return self._apply_unary_op(ops.date_op) + + @property + def hour(self) -> series.Series: + return self._apply_unary_op(ops.hour_op) + + @property + def minute(self) -> series.Series: + return self._apply_unary_op(ops.minute_op) + + @property + def month(self) -> series.Series: + return self._apply_unary_op(ops.month_op) + + @property + def second(self) -> series.Series: + return self._apply_unary_op(ops.second_op) + + @property + def time(self) -> series.Series: + return self._apply_unary_op(ops.time_op) + + @property + def quarter(self) -> series.Series: + return self._apply_unary_op(ops.quarter_op) + + @property + def year(self) -> series.Series: + return self._apply_unary_op(ops.year_op) diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py new file mode 100644 index 00000000000..57029ede416 --- /dev/null +++ b/bigframes/operations/strings.py @@ -0,0 +1,81 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Literal, Optional, Union + +import bigframes.operations as ops +import bigframes.operations.base +import bigframes.series as series +import third_party.bigframes_vendored.pandas.core.strings.accessor as vendorstr + + +class StringMethods(bigframes.operations.base.SeriesMethods, vendorstr.StringMethods): + __doc__ = vendorstr.StringMethods.__doc__ + + def find( + self, + sub: str, + start: Optional[int] = None, + end: Optional[int] = None, + ) -> series.Series: + return self._apply_unary_op(ops.FindOp(sub, start, end)) + + def len(self) -> series.Series: + return self._apply_unary_op(ops.len_op) + + def lower(self) -> series.Series: + return self._apply_unary_op(ops.lower_op) + + def reverse(self) -> series.Series: + """Reverse strings in the Series.""" + # reverse method is in ibis, not pandas. + return self._apply_unary_op(ops.reverse_op) + + def slice( + self, + start: Optional[int] = None, + stop: Optional[int] = None, + ) -> series.Series: + return self._apply_unary_op(ops.SliceOp(start, stop)) + + def strip(self) -> series.Series: + return self._apply_unary_op(ops.strip_op) + + def upper(self) -> series.Series: + return self._apply_unary_op(ops.upper_op) + + def isnumeric(self) -> series.Series: + return self._apply_unary_op(ops.isnumeric_op) + + def rstrip(self) -> series.Series: + return self._apply_unary_op(ops.rstrip_op) + + def lstrip(self) -> series.Series: + return self._apply_unary_op(ops.lstrip_op) + + def repeat(self, repeats: int) -> series.Series: + return self._apply_unary_op(ops.RepeatOp(repeats)) + + def capitalize(self) -> series.Series: + return self._apply_unary_op(ops.capitalize_op) + + def cat( + self, + others: Union[str, series.Series], + *, + join: Literal["outer", "left"] = "left", + ) -> series.Series: + return self._apply_binary_op(others, ops.concat_op, alignment=join) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py new file mode 100644 index 00000000000..e7c14351513 --- /dev/null +++ b/bigframes/pandas/__init__.py @@ -0,0 +1,339 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BigQuery DataFrames provides a DataFrame API backed by the BigQuery engine.""" + +from __future__ import annotations + +import inspect +import threading +import typing +from typing import ( + Any, + Callable, + Dict, + IO, + Iterable, + List, + Literal, + MutableSequence, + Optional, + Sequence, + Tuple, + TypeVar, + Union, +) + +from google.cloud import bigquery +import numpy +import pandas + +import bigframes._config as config +import bigframes.core.indexes +import bigframes.core.reshape +import bigframes.dataframe +import bigframes.series +import bigframes.session +import third_party.bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat + + +# Include method definition so that the method appears in our docs for +# bigframes.pandas general functions. +@typing.overload +def concat( + objs: Iterable[bigframes.dataframe.DataFrame], *, join, ignore_index +) -> bigframes.dataframe.DataFrame: + ... + + +@typing.overload +def concat( + objs: Iterable[bigframes.series.Series], *, join, ignore_index +) -> bigframes.series.Series: + ... + + +def concat( + objs: Union[ + Iterable[bigframes.dataframe.DataFrame], Iterable[bigframes.series.Series] + ], + *, + join: Literal["inner", "outer"] = "outer", + ignore_index: bool = False, +) -> Union[bigframes.dataframe.DataFrame, bigframes.series.Series]: + return bigframes.core.reshape.concat( + objs=objs, join=join, ignore_index=ignore_index + ) + + +concat.__doc__ = vendored_pandas_concat.concat.__doc__ + + +options = config.options +"""Global :class:`~bigframes._config.Options` to configure BigQuery DataFrames.""" + +_global_session: Optional[bigframes.session.Session] = None +_global_session_lock = threading.Lock() + + +def reset_session() -> None: + """Start a fresh session next time a function requires a session. + + Closes the current session if it was already started. + """ + global _global_session + + with _global_session_lock: + if _global_session is not None: + _global_session.close() + _global_session = None + + options.bigquery._session_started = False + + +def get_global_session(): + """Gets the global session. + + Creates the global session if it does not exist. + """ + global _global_session, _global_session_lock + + with _global_session_lock: + if _global_session is None: + _global_session = bigframes.session.connect(options.bigquery) + + return _global_session + + +_T = TypeVar("_T") + + +def _with_default_session(func: Callable[..., _T], *args, **kwargs) -> _T: + return func(get_global_session(), *args, **kwargs) + + +def _set_default_session_location_if_possible(query): + # If the default session has not started yet and this is the first API user + # is calling, then set the default location as per the query. + # If query is a table name, then it would be the location of the table. + # If query is a SQL with a table, then it would be table's location. + # If query is a SQL with no table, then it would be the BQ default location. + if options.bigquery._session_started or options.bigquery.use_regional_endpoints: + return + + bqclient, _, _ = bigframes.session._create_bq_clients( + project=options.bigquery.project, + location=options.bigquery.location, + use_regional_endpoints=options.bigquery.use_regional_endpoints, + credentials=options.bigquery.credentials, + ) + + if bigframes.session._is_query(query): + job = bqclient.query(query, bigquery.QueryJobConfig(dry_run=True)) + options.bigquery.location = job.location + else: + table = bqclient.get_table(query) + options.bigquery.location = table.location + + +# Note: the following methods are duplicated from Session. This duplication +# enables the following: +# +# 1. Static type checking knows the argument and return types, which is +# difficult to do with decorators. Aside: When we require Python 3.10, we +# can use Concatenate for generic typing in decorators. See: +# https://stackoverflow.com/a/68290080/101923 +# 2. docstrings get processed by static processing tools, such as VS Code's +# autocomplete. +# 3. Positional arguments function as expected. If we were to pull in the +# methods directly from Session, a Session object would need to be the first +# argument, even if we allow a default value. +# 4. Allows to set BigQuery options for the BigFrames session based on the +# method and its arguments. + + +def read_csv( + filepath_or_buffer: str | IO["bytes"], + *, + sep: Optional[str] = ",", + header: Optional[int] = 0, + names: Optional[ + Union[MutableSequence[Any], numpy.ndarray[Any, Any], Tuple[Any, ...], range] + ] = None, + index_col: Optional[ + Union[int, str, Sequence[Union[str, int]], Literal[False]] + ] = None, + usecols: Optional[ + Union[ + MutableSequence[str], + Tuple[str, ...], + Sequence[int], + pandas.Series, + pandas.Index, + numpy.ndarray[Any, Any], + Callable[[Any], bool], + ] + ] = None, + dtype: Optional[Dict] = None, + engine: Optional[ + Literal["c", "python", "pyarrow", "python-fwf", "bigquery"] + ] = None, + encoding: Optional[str] = None, + **kwargs, +) -> bigframes.dataframe.DataFrame: + return _with_default_session( + bigframes.session.Session.read_csv, + filepath_or_buffer=filepath_or_buffer, + sep=sep, + header=header, + names=names, + index_col=index_col, + usecols=usecols, + dtype=dtype, + engine=engine, + encoding=encoding, + **kwargs, + ) + + +read_csv.__doc__ = inspect.getdoc(bigframes.session.Session.read_csv) + + +def read_gbq( + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, +) -> bigframes.dataframe.DataFrame: + _set_default_session_location_if_possible(query) + return _with_default_session( + bigframes.session.Session.read_gbq, + query, + index_col=index_col, + col_order=col_order, + max_results=max_results, + ) + + +read_gbq.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq) + + +def read_gbq_model(model_name: str): + return _with_default_session( + bigframes.session.Session.read_gbq_model, + model_name, + ) + + +read_gbq_model.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_model) + + +def read_gbq_query( + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, +) -> bigframes.dataframe.DataFrame: + _set_default_session_location_if_possible(query) + return _with_default_session( + bigframes.session.Session.read_gbq_query, + query, + index_col=index_col, + col_order=col_order, + max_results=max_results, + ) + + +read_gbq_query.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_query) + + +def read_gbq_table( + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, +) -> bigframes.dataframe.DataFrame: + _set_default_session_location_if_possible(query) + return _with_default_session( + bigframes.session.Session.read_gbq_table, + query, + index_col=index_col, + col_order=col_order, + max_results=max_results, + ) + + +read_gbq_table.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_table) + + +def read_pandas(pandas_dataframe: pandas.DataFrame) -> bigframes.dataframe.DataFrame: + return _with_default_session( + bigframes.session.Session.read_pandas, + pandas_dataframe, + ) + + +read_pandas.__doc__ = inspect.getdoc(bigframes.session.Session.read_pandas) + + +def read_parquet(path: str | IO["bytes"]) -> bigframes.dataframe.DataFrame: + return _with_default_session( + bigframes.session.Session.read_parquet, + path, + ) + + +read_parquet.__doc__ = inspect.getdoc(bigframes.session.Session.read_parquet) + + +def remote_function( + input_types: List[type], + output_type: type, + dataset: Optional[str] = None, + bigquery_connection: Optional[str] = None, + reuse: bool = True, +): + return _with_default_session( + bigframes.session.Session.remote_function, + input_types=input_types, + output_type=output_type, + dataset=dataset, + bigquery_connection=bigquery_connection, + reuse=reuse, + ) + + +remote_function.__doc__ = inspect.getdoc(bigframes.session.Session.remote_function) + + +# Other aliases +DataFrame = bigframes.dataframe.DataFrame +Index = bigframes.core.indexes.Index +Series = bigframes.series.Series + +# Use __all__ to let type checkers know what is part of the public API. +__all___ = [ + "concat", + "DataFrame", + "options", + "read_csv", + "read_gbq", + "read_gbq_model", + "read_pandas", + "remote_function", + "Series", +] diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py new file mode 100644 index 00000000000..3fe4e38d785 --- /dev/null +++ b/bigframes/remote_function.py @@ -0,0 +1,697 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import functools +import hashlib +import inspect +import logging +import os +import random +import shutil +import string +import subprocess +import sys +import tempfile +import textwrap +import time +import typing + +if typing.TYPE_CHECKING: + from bigframes.session import Session + +import cloudpickle +import google.api_core.exceptions +from google.cloud import bigquery, bigquery_connection_v1, functions_v2 +from ibis.backends.bigquery.compiler import compiles +from ibis.backends.bigquery.datatypes import BigQueryType +from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type +import ibis.expr.operations as ops +import ibis.expr.rules as rlz + +# TODO(shobs): Change the min log level to INFO after the development stabilizes +# before June 2023 +logging.basicConfig( + level=logging.INFO, format="[%(levelname)s][%(asctime)s][%(name)s] %(message)s" +) +logger = logging.getLogger(__name__) + +# Protocol version 4 is available in python version 3.4 and above +# https://docs.python.org/3/library/pickle.html#data-stream-format +_pickle_protocol_version = 4 + +# Input and output python types supported by BigQuery DataFrames remote functions. +# TODO(shobs): Extend the support to all types supported by BQ remote functions +# https://cloud.google.com/bigquery/docs/remote-functions#limitations +_supported_io_types = set((bool, float, int, str)) + + +def get_remote_function_locations(bq_location): + """Get BQ location and cloud functions region given a BQ client.""" + # TODO(shobs, b/274647164): Find the best way to determine default location. + # For now let's assume that if no BQ location is set in the client then it + # defaults to US multi region + bq_location = bq_location.lower() if bq_location else "us" + + # Cloud function should be in the same region as the bigquery remote function + cloud_function_region = bq_location + + # BigQuery has multi region but cloud functions does not. + # Any region in the multi region that supports cloud functions should work + # https://cloud.google.com/functions/docs/locations + if bq_location == "us": + cloud_function_region = "us-central1" + elif bq_location == "eu": + cloud_function_region = "europe-west1" + + return bq_location, cloud_function_region + + +def _get_hash(def_): + "Get hash of a function." + def_repr = cloudpickle.dumps(def_, protocol=_pickle_protocol_version) + return hashlib.md5(def_repr).hexdigest() + + +def _run_system_command(command): + program = subprocess.Popen( + [command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + stdout, stderr = program.communicate() + exit_code = program.wait() + if exit_code: + raise RuntimeError( + f"Command: {command}\nOutput: {stdout.decode()}\nError: {stderr.decode()}" + ) + + +def get_cloud_function_name(def_, uniq_suffix=None): + """Get the name of the cloud function.""" + cf_name = _get_hash(def_) + cf_name = f"bigframes-{cf_name}" # for identification + if uniq_suffix: + cf_name = f"{cf_name}-{uniq_suffix}" + return cf_name + + +def get_remote_function_name(def_, uniq_suffix=None): + """Get the name for the BQ remote function.""" + bq_rf_name = _get_hash(def_) + bq_rf_name = f"bigframes_{bq_rf_name}" # for identification + if uniq_suffix: + bq_rf_name = f"{bq_rf_name}_{uniq_suffix}" + return bq_rf_name + + +class RemoteFunctionClient: + # Wait time (in seconds) for an IAM binding to take effect after creation + _iam_wait_seconds = 120 + + def __init__( + self, + gcp_project_id, + cloud_function_region, + bq_location, + bq_dataset, + bq_client, + bq_connection_client, + bq_connection_id, + ): + self._gcp_project_id = gcp_project_id + self._cloud_function_region = cloud_function_region + self._bq_location = bq_location + self._bq_dataset = bq_dataset + self._bq_client = bq_client + self._bq_connection_client = bq_connection_client + self._bq_connection_id = bq_connection_id + + def create_bq_remote_function( + self, input_args, input_types, output_type, endpoint, bq_function_name + ): + """Create a BigQuery remote function given the artifacts of a user defined + function and the http endpoint of a corresponding cloud function.""" + # TODO(shobs): The below command to enable BigQuery Connection API needs + # to be automated. Disabling for now since most target users would not + # have the privilege to enable API in a project. + # log("Making sure BigQuery Connection API is enabled") + # if os.system("gcloud services enable bigqueryconnection.googleapis.com"): + # raise ValueError("Failed to enable BigQuery Connection API") + + # If the intended connection does not exist then create it + if self.check_bq_connection_exists(): + logger.info(f"Connector {self._bq_connection_id} already exists") + else: + connection_name, service_account_id = self.create_bq_connection() + logger.info( + f"Created BQ connection {connection_name} with service account id: {service_account_id}" + ) + + # Set up access on the newly created BQ connection + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + # We would explicitly wait for 60+ seconds for the IAM binding to take effect + command_iam = ( + f"gcloud projects add-iam-policy-binding {self._gcp_project_id}" + + f' --member="serviceAccount:{service_account_id}"' + + ' --role="roles/run.invoker"' + ) + logger.info(f"Setting up IAM binding on the BQ connection: {command_iam}") + _run_system_command(command_iam) + + logger.info( + f"Waiting {self._iam_wait_seconds} seconds for IAM to take effect.." + ) + time.sleep(self._iam_wait_seconds) + + # Create BQ function + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 + bq_function_args = [] + bq_function_return_type = BigQueryType.from_ibis(output_type) + # We are expecting the input type annotations to be 1:1 with the input args + for idx, name in enumerate(input_args): + bq_function_args.append( + f"{name} {BigQueryType.from_ibis(input_types[idx])}" + ) + create_function_ddl = f""" + CREATE OR REPLACE FUNCTION `{self._gcp_project_id}.{self._bq_dataset}`.{bq_function_name}({','.join(bq_function_args)}) + RETURNS {bq_function_return_type} + REMOTE WITH CONNECTION `{self._gcp_project_id}.{self._bq_location}.{self._bq_connection_id}` + OPTIONS ( + endpoint = "{endpoint}" + )""" + logger.info(f"Creating BQ remote function: {create_function_ddl}") + query_job = self._bq_client.query(create_function_ddl) # Make an API request. + query_job.result() # Wait for the job to complete. + logger.info(f"Created remote function {query_job.ddl_target_routine}") + + def get_remote_function_fully_qualified_name(self, name): + "Get the fully qualilfied name for a BQ remote function." + return "{}.{}.{}".format(self._gcp_project_id, self._bq_dataset, name) + + def get_cloud_function_fully_qualified_name(self, name): + "Get the fully qualilfied name for a cloud function." + return "projects/{}/locations/{}/functions/{}".format( + self._gcp_project_id, self._cloud_function_region, name + ) + + def get_cloud_function_endpoint(self, name): + """Get the http endpoint of a cloud function if it exists.""" + client = functions_v2.FunctionServiceClient() + fully_qualified_name = self.get_cloud_function_fully_qualified_name(name) + try: + response = client.get_function(name=fully_qualified_name) + return response.service_config.uri + except google.api_core.exceptions.NotFound: + pass + return None + + def create_bq_connection(self): + """Create the BigQuery Connection and returns corresponding service account id.""" + client = self._bq_connection_client + connection = bigquery_connection_v1.Connection( + cloud_resource=bigquery_connection_v1.CloudResourceProperties() + ) + request = bigquery_connection_v1.CreateConnectionRequest( + parent=client.common_location_path(self._gcp_project_id, self._bq_location), + connection_id=self._bq_connection_id, + connection=connection, + ) + connection = client.create_connection(request) + return connection.name, connection.cloud_resource.service_account_id + + def check_bq_connection_exists(self): + """Check if the BigQuery Connection exists.""" + client = self._bq_connection_client + request = bigquery_connection_v1.GetConnectionRequest( + name=client.connection_path( + self._gcp_project_id, self._bq_location, self._bq_connection_id + ) + ) + + try: + client.get_connection(request=request) + return True + except google.api_core.exceptions.NotFound: + pass + return False + + def generate_udf_code(self, def_, dir): + """Generate serialized bytecode using cloudpickle given a udf.""" + udf_code_file_name = "udf.py" + udf_bytecode_file_name = "udf.cloudpickle" + + # original code, only for debugging purpose + udf_code = textwrap.dedent(inspect.getsource(def_)) + udf_code_file_path = os.path.join(dir, udf_code_file_name) + with open(udf_code_file_path, "w") as f: + f.write(udf_code) + + # serialized bytecode + udf_bytecode_file_path = os.path.join(dir, udf_bytecode_file_name) + with open(udf_bytecode_file_path, "wb") as f: + cloudpickle.dump(def_, f, protocol=_pickle_protocol_version) + + return udf_code_file_name, udf_bytecode_file_name + + def generate_cloud_function_main_code(self, def_, dir): + """Get main.py code for the cloud function for the given user defined function.""" + + # Pickle the udf with all its dependencies + udf_code_file, udf_bytecode_file = self.generate_udf_code(def_, dir) + handler_func_name = "udf_http" + + # We want to build a cloud function that works for BQ remote functions, + # where we receive `calls` in json which is a batch of rows from BQ SQL. + # The number and the order of values in each row is expected to exactly + # match to the number and order of arguments in the udf , e.g. if the udf is + # def foo(x: int, y: str): + # ... + # then the http request body could look like + # { + # ... + # "calls" : [ + # [123, "hello"], + # [456, "world"] + # ] + # ... + # } + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#input_format + code_template = textwrap.dedent( + """\ + import cloudpickle + import json + + # original udf code is in {udf_code_file} + # serialized udf code is in {udf_bytecode_file} + with open("{udf_bytecode_file}", "rb") as f: + udf = cloudpickle.load(f) + + def {handler_func_name}(request): + request_json = request.get_json(silent=True) + print("[debug] received json request: " + str(request_json)) + calls = request_json["calls"] + replies = [] + for call in calls: + reply = udf(*call) + replies.append(reply) + return_json = json.dumps({{"replies" : replies}}) + return return_json + """ + ) + + code = code_template.format( + udf_code_file=udf_code_file, + udf_bytecode_file=udf_bytecode_file, + handler_func_name=handler_func_name, + ) + + main_py = os.path.join(dir, "main.py") + with open(main_py, "w") as f: + f.write(code) + logger.debug(f"Wrote {os.path.abspath(main_py)}:\n{open(main_py).read()}") + + return handler_func_name + + def generate_cloud_function_code(self, def_, dir): + """Generate the cloud function code for a given user defined function.""" + + # requirements.txt + requirements = ["cloudpickle >= 2.1.0"] + requirements_txt = os.path.join(dir, "requirements.txt") + with open(requirements_txt, "w") as f: + f.write("\n".join(requirements)) + + # main.py + entry_point = self.generate_cloud_function_main_code(def_, dir) + return entry_point + + def create_cloud_function(self, def_, cf_name): + """Create a cloud function from the given user defined function.""" + + # Build and deploy folder structure containing cloud function + with tempfile.TemporaryDirectory() as dir: + entry_point = self.generate_cloud_function_code(def_, dir) + + # We are creating cloud function source code from the currently running + # python version. Use the same version to deploy. This is necessary + # because cloudpickle serialization done in one python version and + # deserialization done in another python version doesn't work. + # TODO(shobs): Figure out how to achieve version compatibility, specially + # when pickle (internally used by cloudpickle) guarantees that: + # https://docs.python.org/3/library/pickle.html#:~:text=The%20pickle%20serialization%20format%20is,unique%20breaking%20change%20language%20boundary. + python_version = "python{}{}".format( + sys.version_info.major, sys.version_info.minor + ) + + # deploy/redeploy the cloud function + # TODO(shobs): Figure out a way to skip this step if a cloud function + # already exists with the same name and source code + command = ( + "gcloud functions deploy" + + f" {cf_name} --gen2" + + f" --runtime={python_version}" + + f" --project={self._gcp_project_id}" + + f" --region={self._cloud_function_region}" + + f" --source={dir}" + + f" --entry-point={entry_point}" + + " --trigger-http" + ) + + # If the cloud function is being created for the first time, then let's + # make it not allow unauthenticated calls. If it was previously created + # then this invocation will update it, in which case do not touch that + # aspect and let the previous policy hold. The reason we do this is to + # avoid an IAM permission needed to update the invocation policy. + # For example, when a cloud function is being created for the first + # time, i.e. + # $ gcloud functions deploy python-foo-http --gen2 --runtime=python310 + # --region=us-central1 + # --source=/source/code/dir + # --entry-point=foo_http + # --trigger-http + # --no-allow-unauthenticated + # It works. When an invocation of the same command is done for the + # second time, it may run into an error like: + # ERROR: (gcloud.functions.deploy) PERMISSION_DENIED: Permission + # 'run.services.setIamPolicy' denied on resource + # 'projects/my_project/locations/us-central1/services/python-foo-http' (or resource may not exist) + # But when --no-allow-unauthenticated is omitted then it goes through. + # It suggests that in the second invocation the command is trying to set + # the IAM policy of the service, and the user running BigQuery + # DataFrame may not have privilege to do so, so better avoid this + # if we can. + if self.get_cloud_function_endpoint(cf_name): + logger.info(f"Updating existing cloud function: {command}") + else: + command = f"{command} --no-allow-unauthenticated" + logger.info(f"Creating new cloud function: {command}") + + _run_system_command(command) + + # Fetch the endpoint of the just created function + endpoint = self.get_cloud_function_endpoint(cf_name) + if not endpoint: + raise ValueError("Couldn't fetch the http endpoint") + + logger.info( + f"Successfully created cloud function {cf_name} with uri ({endpoint})" + ) + return endpoint + + def provision_bq_remote_function( + self, def_, input_types, output_type, uniq_suffix=None + ): + """Provision a BigQuery remote function.""" + # Derive the name of the underlying cloud function and first create + # it if it does not exist + cloud_function_name = get_cloud_function_name(def_, uniq_suffix) + cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) + if not cf_endpoint: + self.check_cloud_function_tools_and_permissions() + cf_endpoint = self.create_cloud_function(def_, cloud_function_name) + else: + logger.info(f"Cloud function {cloud_function_name} already exists.") + + # Derive the name of the remote function and create/replace it if needed + remote_function_name = get_remote_function_name(def_, uniq_suffix) + rf_endpoint, rf_conn = self.get_remote_function_specs(remote_function_name) + if rf_endpoint != cf_endpoint or rf_conn != self._bq_connection_id: + input_args = inspect.getargs(def_.__code__).args + if len(input_args) != len(input_types): + raise ValueError( + "Exactly one type should be provided for every input arg." + ) + self.create_bq_remote_function( + input_args, input_types, output_type, cf_endpoint, remote_function_name + ) + else: + logger.info(f"Remote function {remote_function_name} already exists.") + + return remote_function_name, cloud_function_name + + def get_remote_function_specs(self, remote_function_name): + """Check whether a remote function already exists for the udf.""" + http_endpoint = None + bq_connection = None + routines = self._bq_client.list_routines( + f"{self._gcp_project_id}.{self._bq_dataset}" + ) + for routine in routines: + if routine.reference.routine_id == remote_function_name: + # TODO(shobs): Use first class properties when they are available + # https://github.com/googleapis/python-bigquery/issues/1552 + rf_options = routine._properties.get("remoteFunctionOptions") + if rf_options: + http_endpoint = rf_options.get("endpoint") + bq_connection = rf_options.get("connection") + if bq_connection: + bq_connection = os.path.basename(bq_connection) + break + return (http_endpoint, bq_connection) + + def check_cloud_function_tools_and_permissions(self): + """Check if the necessary tools and permissions are in place for creating remote function""" + # gcloud CLI comes with bq CLI and they are required for creating google + # cloud function and BigQuery remote function respectively + if not shutil.which("gcloud"): + raise ValueError( + "gcloud tool not installed, install it from https://cloud.google.com/sdk/docs/install" + ) + + # TODO(shobs): Check for permissions too + # I (shobs) tried the following method + # $ gcloud asset search-all-iam-policies \ + # --format=json \ + # --scope=projects/{gcp_project_id} \ + # --query='policy.role.permissions:cloudfunctions.functions.create' + # as a proxy to all the privilges necessary to create cloud function + # https://cloud.google.com/functions/docs/reference/iam/roles#cloudfunctions.developer + # but that itself required the runner to have the permission to enable + # `cloudasset.googleapis.com` + + +# Inspired by @udf decorator implemented in ibis-bigquery package +# https://github.com/ibis-project/ibis-bigquery/blob/main/ibis_bigquery/udf/__init__.py +# which has moved as @js to the ibis package +# https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py +def remote_function( + input_types: typing.Sequence[type], + output_type: type, + session: typing.Optional[Session] = None, + bigquery_client: typing.Optional[bigquery.Client] = None, + bigquery_connection_client: typing.Optional[ + bigquery_connection_v1.ConnectionServiceClient + ] = None, + dataset: typing.Optional[str] = None, + bigquery_connection: typing.Optional[str] = None, + reuse: bool = True, +): + """Decorator to turn a user defined function into a BigQuery remote function. + + .. deprecated:: 0.0.1 + Use :func:`bigframes.pandas.remote_function` instead. + + Args: + input_types : list(type). + List of input data types in the user defined function. + output_type : type. + Data type of the output in the user defined function. + session : bigframes.Session, Optional + BigQuery DataFrames session to use for getting default project, + dataset and BigQuery connection. + bigquery_client : google.cloud.bigquery.Client, Optional + Client to use for BigQuery operations. If this param is not provided + then bigquery client from the session would be used. + bigquery_connection_client : google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional + Client to use for BigQuery connection operations. If this param is + not provided then bigquery connection client from the session would + be used. + dataset : str, Optional. + Dataset to use to create a BigQuery function. It should be in + `.` or `` format. If this + param is not provided then session dataset id would be used. + bigquery_connection : str, Optional. + Name of the BigQuery connection. If this param is not provided then + the bigquery connection from the session would be used. If it is pre + created in the same location as the `bigquery_client.location` then + it would be used, otherwise it would be created dynamically using + the `bigquery_connection_client` assuming the user has necessary + priviliges. + reuse : bool, Optional. + Reuse the remote function if already exists. + `True` by default, which will result in reusing an existing remote + function (if any) that was previously created for the same udf. + Setting it to false would force creating a unique remote function. + If the required remote function does not exist then it would be + created irrespective of this param. + + Notes: + Please make sure following is setup before using this API: + + 1. Have the below APIs enabled for your project: + a. BigQuery Connection API + b. Cloud Functions API + c. Cloud Run API + d. Cloud Build API + e. Artifact Registry API + f. Cloud Resource Manager API + + This can be done from the cloud console (change PROJECT_ID to yours): + https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID + Or from the gcloud CLI: + $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com + + 2. Have following IAM roles enabled for you: + a. BigQuery Data Editor (roles/bigquery.dataEditor) + b. BigQuery Connection Admin (roles/bigquery.connectionAdmin) + c. Cloud Functions Developer (roles/cloudfunctions.developer) + d. Service Account User (roles/iam.serviceAccountUser) + e. Storage Object Viewer (roles/storage.objectViewer) + f. Project IAM Admin (roles/resourcemanager.projectIamAdmin) + (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) + + 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: + a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection + b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + Alternatively, the IAM could also be setup via the gcloud CLI: + $ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker" + + """ + + # A BigQuery client is required to perform BQ operations + if not bigquery_client: + if session: + bigquery_client = session.bqclient + if not bigquery_client: + raise ValueError( + "A bigquery client must be provided, either directly or via session" + ) + + # A BigQuery connection client is required to perform BQ connection operations + if not bigquery_connection_client: + if session: + bigquery_connection_client = session.bqconnectionclient + if not bigquery_connection_client: + raise ValueError( + "A bigquery connection client must be provided, either directly or via session" + ) + + # BQ remote function must be persisted, for which we need a dataset + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#:~:text=You%20cannot%20create%20temporary%20remote%20functions. + if dataset: + dataset_ref = bigquery.DatasetReference.from_string( + dataset, default_project=bigquery_client.project + ) + gcp_project_id = dataset_ref.project + bq_dataset = dataset_ref.dataset_id + else: + gcp_project_id = bigquery_client.project + if session: + bq_dataset = session._session_dataset_id + if not gcp_project_id: + raise ValueError("Project must be provided, either directly or via session") + if not bq_dataset: + raise ValueError("Dataset must be provided, either directly or via session") + + bq_location, cloud_function_region = get_remote_function_locations( + bigquery_client.location + ) + + # A connection is required for BQ remote function + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function + if not bigquery_connection and session: + bigquery_connection = session._remote_udf_connection # type: ignore + if not bigquery_connection: + raise ValueError( + "BigQuery connection must be provided, either directly or via session" + ) + + uniq_suffix = None + if not reuse: + uniq_suffix = "".join( + random.choices(string.ascii_lowercase + string.digits, k=8) + ) + + def wrapper(f): + if not callable(f): + raise TypeError("f must be callable, got {}".format(f)) + + signature = inspect.signature(f) + parameter_names = signature.parameters.keys() + + # Check supported python datatypes and convert to ibis datatypes + type_error_message_format = ( + "type {{}} not supported, supported types are {}.".format( + ", ".join([type_.__name__ for type_ in _supported_io_types]) + ) + ) + for type_ in input_types: + assert type_ in _supported_io_types, type_error_message_format.format(type_) + assert output_type in _supported_io_types, type_error_message_format.format( + output_type + ) + input_types_ibis = [ + python_type_to_bigquery_type(type_) for type_ in input_types + ] + output_type_ibis = python_type_to_bigquery_type(output_type) + + rf_node_fields = { + name: rlz.value(type) + for name, type in zip(parameter_names, input_types_ibis) + } + + try: + rf_node_fields["output_type"] = rlz.shape_like( + "args", dtype=output_type_ibis + ) + except TypeError: + rf_node_fields["output_dtype"] = property(lambda _: output_type_ibis) + rf_node_fields["output_shape"] = rlz.shape_like("args") + + remote_function_client = RemoteFunctionClient( + gcp_project_id, + cloud_function_region, + bq_location, + bq_dataset, + bigquery_client, + bigquery_connection_client, + bigquery_connection, + ) + rf_name, cf_name = remote_function_client.provision_bq_remote_function( + f, input_types_ibis, output_type_ibis, uniq_suffix + ) + rf_fully_qualified_name = f"`{gcp_project_id}.{bq_dataset}`.{rf_name}" + rf_node = type(rf_fully_qualified_name, (ops.ValueOp,), rf_node_fields) + + @compiles(rf_node) + def compiles_rf_node(t, op): + return "{}({})".format( + rf_node.__name__, ", ".join(map(t.translate, op.args)) + ) + + @functools.wraps(f) + def wrapped(*args, **kwargs): + node = rf_node(*args, **kwargs) + return node.to_expr() + + wrapped.__signature__ = signature + wrapped.bigframes_remote_function = ( + remote_function_client.get_remote_function_fully_qualified_name(rf_name) + ) + wrapped.bigframes_cloud_function = ( + remote_function_client.get_cloud_function_fully_qualified_name(cf_name) + ) + return wrapped + + return wrapper diff --git a/bigframes/series.py b/bigframes/series.py new file mode 100644 index 00000000000..f008e0fb060 --- /dev/null +++ b/bigframes/series.py @@ -0,0 +1,964 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Series is a 1 dimensional data structure.""" + +from __future__ import annotations + +import textwrap +import typing +from typing import Any, Optional, Union + +import google.cloud.bigquery as bigquery +import ibis.expr.types as ibis_types +import numpy +import pandas +import pandas.core.dtypes.common +import typing_extensions + +import bigframes.core +from bigframes.core import WindowSpec +import bigframes.core.block_transforms as block_ops +import bigframes.core.blocks as blocks +import bigframes.core.groupby as groupby +import bigframes.core.indexers +import bigframes.core.indexes as indexes +from bigframes.core.ordering import OrderingColumnReference, OrderingDirection +import bigframes.core.scalar as scalars +import bigframes.core.window +import bigframes.dataframe +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops +import bigframes.operations.base +import bigframes.operations.datetimes as dt +import bigframes.operations.strings as strings +import third_party.bigframes_vendored.pandas.core.series as vendored_pandas_series + +LevelsType = typing.Union[str, int, typing.Sequence[typing.Union[str, int]]] + + +class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Series): + def __init__(self, *args, **kwargs): + self._query_job: Optional[bigquery.QueryJob] = None + super().__init__(*args, **kwargs) + + @property + def dt(self) -> dt.DatetimeMethods: + return dt.DatetimeMethods(self._block) + + @property + def dtype(self): + return self._dtype + + @property + def dtypes(self): + return self._dtype + + @property + def index(self) -> indexes.Index: + return indexes.Index(self) + + @property + def loc(self) -> bigframes.core.indexers.LocSeriesIndexer: + return bigframes.core.indexers.LocSeriesIndexer(self) + + @property + def iloc(self) -> bigframes.core.indexers.IlocSeriesIndexer: + return bigframes.core.indexers.IlocSeriesIndexer(self) + + @property + def name(self) -> Optional[str]: + return self._name + + @property + def shape(self) -> typing.Tuple[int]: + return (self._block.shape[0],) + + @property + def size(self) -> int: + return self.shape[0] + + @property + def empty(self) -> bool: + return self.shape[0] == 0 + + @property + def values(self) -> numpy.ndarray: + return self.to_numpy() + + @property + def query_job(self) -> Optional[bigquery.QueryJob]: + return self._query_job + + def __len__(self): + return self.shape[0] + + def copy(self) -> Series: + return Series(self._block) + + def rename(self, index: Optional[str], **kwargs) -> Series: + if len(kwargs) != 0: + raise NotImplementedError( + "rename does not currently support any keyword arguments." + ) + block = self._block.with_column_labels([index]) + return Series(block) + + def rename_axis( + self, + mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], + **kwargs, + ) -> Series: + if len(kwargs) != 0: + raise NotImplementedError( + "rename_axis does not currently support any keyword arguments." + ) + # limited implementation: the new index name is simply the 'mapper' parameter + if _is_list_like(mapper): + labels = mapper + else: + labels = [mapper] + return Series(self._block.with_index_labels(labels)) + + def reset_index( + self, + *, + name: typing.Optional[str] = None, + drop: bool = False, + ) -> bigframes.dataframe.DataFrame | Series: + block = self._block.reset_index(drop) + if drop: + return Series(block) + else: + if name: + block = block.assign_label(self._value_column, name) + return bigframes.dataframe.DataFrame(block) + + def __repr__(self) -> str: + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + # TODO(swast): Avoid downloading the whole series by using job + # metadata, like we do with DataFrame. + preview = self.compute() + return repr(preview) + + def _to_ibis_expr(self): + """Creates an Ibis table expression representing the Series.""" + expr = self._block.expr.projection([self._value]) + ibis_expr = expr.to_ibis_expr()[self._value_column] + if self._name: + return ibis_expr.name(self._name) + return ibis_expr + + def astype( + self, + dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], + ) -> Series: + return self._apply_unary_op(bigframes.operations.AsTypeOp(dtype)) + + def compute(self) -> pandas.Series: + """Executes deferred operations and downloads the results.""" + df, query_job = self._block.compute((self._value_column,)) + self._query_job = query_job + series = df[self._value_column] + series.name = self._name + return series + + def drop(self, labels: blocks.Label | typing.Sequence[blocks.Label] = None): + block = self._block + index_column = block.index_columns[0] + + if _is_list_like(labels): + block, inverse_condition_id = block.apply_unary_op( + index_column, ops.partial_right(ops.isin_op, labels) + ) + block, condition_id = block.apply_unary_op( + inverse_condition_id, ops.invert_op + ) + + else: + block, condition_id = block.apply_unary_op( + index_column, ops.partial_right(ops.ne_op, labels) + ) + block = block.filter(condition_id) + block = block.drop_columns([condition_id]) + return Series(block.select_column(self._value_column)) + + def droplevel(self, level: LevelsType): + resolved_level_ids = self._resolve_levels(level) + return Series(self._block.drop_levels(resolved_level_ids)) + + def reorder_levels(self, order: LevelsType): + resolved_level_ids = self._resolve_levels(order) + return Series(self._block.reorder_levels(resolved_level_ids)) + + def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: + if _is_list_like(level): + levels = list(level) + else: + levels = [level] + resolved_level_ids = [] + for level_ref in levels: + if isinstance(level_ref, int): + resolved_level_ids.append(self._block.index_columns[level_ref]) + elif isinstance(level_ref, str): + matching_ids = self._block.index_name_to_col_id.get(level_ref, []) + if len(matching_ids) != 1: + raise ValueError("level name cannot be found or is ambiguous") + resolved_level_ids.append(matching_ids[0]) + else: + raise ValueError(f"Unexpected level: {level_ref}") + return resolved_level_ids + + def between(self, left, right, inclusive="both"): + if inclusive not in ["both", "neither", "left", "right"]: + raise ValueError( + "Must set 'inclusive' to one of 'both', 'neither', 'left', or 'right'" + ) + left_op = ops.ge_op if (inclusive in ["left", "both"]) else ops.gt_op + right_op = ops.le_op if (inclusive in ["right", "both"]) else ops.lt_op + return self._apply_binary_op(left, left_op).__and__( + self._apply_binary_op(right, right_op) + ) + + def cumsum(self) -> Series: + return self._apply_window_op( + agg_ops.sum_op, bigframes.core.WindowSpec(following=0) + ) + + def cummax(self) -> Series: + return self._apply_window_op( + agg_ops.max_op, bigframes.core.WindowSpec(following=0) + ) + + def cummin(self) -> Series: + return self._apply_window_op( + agg_ops.min_op, bigframes.core.WindowSpec(following=0) + ) + + def shift(self, periods: int = 1) -> Series: + window = bigframes.core.WindowSpec( + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.ShiftOp(periods), window) + + def diff(self) -> Series: + return self - self.shift(1) + + def rank( + self, + axis=0, + method: str = "average", + numeric_only=False, + na_option: str = "keep", + ascending: bool = True, + ) -> Series: + return Series(block_ops.rank(self._block, method, na_option, ascending)) + + def fillna(self, value=None) -> "Series" | None: + return self._apply_binary_op(value, ops.fillna_op) + + def head(self, n: int = 5) -> Series: + return typing.cast(Series, self.iloc[0:n]) + + def tail(self, n: int = 5) -> Series: + return typing.cast(Series, self.iloc[-n:]) + + def nlargest(self, n: int = 5, keep: str = "first") -> Series: + if keep not in ("first", "last", "all"): + raise ValueError("'keep must be one of 'first', 'last', or 'all'") + block = self._block + if keep == "last": + block = block.reversed() + ordering = ( + OrderingColumnReference( + self._value_column, direction=OrderingDirection.DESC + ), + ) + block = block.order_by(ordering, stable=True) + if keep in ("first", "last"): + return Series(block.slice(0, n)) + else: # keep == "all": + block, counter = block.apply_window_op( + self._value_column, + agg_ops.rank_op, + window_spec=WindowSpec(ordering=ordering), + ) + block, condition = block.apply_unary_op( + counter, ops.partial_right(ops.le_op, n) + ) + block = block.filter(condition) + block = block.select_column(self._value_column) + return Series(block) + + def nsmallest(self, n: int = 5, keep: str = "first") -> Series: + if keep not in ("first", "last", "all"): + raise ValueError("'keep must be one of 'first', 'last', or 'all'") + block = self._block + if keep == "last": + block = block.reversed() + ordering = (OrderingColumnReference(self._value_column),) + block = block.order_by(ordering, stable=True) + if keep in ("first", "last"): + return Series(block.slice(0, n)) + else: # keep == "all": + block, counter = block.apply_window_op( + self._value_column, + agg_ops.rank_op, + window_spec=WindowSpec(ordering=ordering), + ) + block, condition = block.apply_unary_op( + counter, ops.partial_right(ops.le_op, n) + ) + block = block.filter(condition) + block = block.select_column(self._value_column) + return Series(block) + + def isna(self) -> "Series": + return self._apply_unary_op(ops.isnull_op) + + isnull = isna + + def notna(self) -> "Series": + return self._apply_unary_op(ops.notnull_op) + + notnull = notna + + def __and__(self, other: bool | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.and_op) + + __rand__ = __and__ + + def __or__(self, other: bool | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.or_op) + + __ror__ = __or__ + + def __add__(self, other: float | int | Series | pandas.Series) -> Series: + return self.add(other) + + def __radd__(self, other: float | int | Series | pandas.Series) -> Series: + return self.radd(other) + + def add(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.add_op) + + def radd(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.reverse(ops.add_op)) + + def __sub__(self, other: float | int | Series | pandas.Series) -> Series: + return self.sub(other) + + def __rsub__(self, other: float | int | Series | pandas.Series) -> Series: + return self.rsub(other) + + def sub(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.sub_op) + + def rsub(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.reverse(ops.sub_op)) + + def __mul__(self, other: float | int | Series | pandas.Series) -> Series: + return self.mul(other) + + def __rmul__(self, other: float | int | Series | pandas.Series) -> Series: + return self.rmul(other) + + def mul(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.mul_op) + + def rmul(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.reverse(ops.mul_op)) + + multiply = mul + + def __truediv__(self, other: float | int | Series | pandas.Series) -> Series: + return self.truediv(other) + + def __rtruediv__(self, other: float | int | Series | pandas.Series) -> Series: + return self.rtruediv(other) + + def truediv(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.div_op) + + def rtruediv(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.reverse(ops.div_op)) + + div = truediv + + divide = truediv + + rdiv = rtruediv + + def __floordiv__(self, other: float | int | Series | pandas.Series) -> Series: + return self.floordiv(other) + + def __rfloordiv__(self, other: float | int | Series | pandas.Series) -> Series: + return self.rfloordiv(other) + + def floordiv(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.floordiv_op) + + def rfloordiv(self, other: float | int | Series | pandas.Series) -> Series: + return self._apply_binary_op(other, ops.reverse(ops.floordiv_op)) + + def __lt__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + return self.lt(other) + + def __le__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + return self.le(other) + + def lt(self, other) -> Series: + return self._apply_binary_op(other, ops.lt_op) + + def le(self, other) -> Series: + return self._apply_binary_op(other, ops.le_op) + + def __gt__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + return self.gt(other) + + def __ge__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + return self.ge(other) + + def gt(self, other) -> Series: + return self._apply_binary_op(other, ops.gt_op) + + def ge(self, other) -> Series: + return self._apply_binary_op(other, ops.ge_op) + + def __mod__(self, other) -> Series: # type: ignore + return self.mod(other) + + def __rmod__(self, other) -> Series: # type: ignore + return self.rmod(other) + + def mod(self, other) -> Series: # type: ignore + return self._apply_binary_op(other, ops.mod_op) + + def rmod(self, other) -> Series: # type: ignore + return self._apply_binary_op(other, ops.reverse(ops.mod_op)) + + def __matmul__(self, other): + return (self * other).sum() + + dot = __matmul__ + + def abs(self) -> Series: + return self._apply_unary_op(ops.abs_op) + + def round(self, decimals=0) -> "Series": + def round_op(x: ibis_types.Value, y: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).round( + digits=typing.cast(ibis_types.IntegerValue, y) + ) + + return self._apply_binary_op(decimals, round_op) + + def all(self) -> bool: + return typing.cast(bool, self._apply_aggregation(agg_ops.all_op)) + + def any(self) -> bool: + return typing.cast(bool, self._apply_aggregation(agg_ops.any_op)) + + def count(self) -> int: + return typing.cast(int, self._apply_aggregation(agg_ops.count_op)) + + def nunique(self) -> int: + return typing.cast(int, self._apply_aggregation(agg_ops.nunique_op)) + + def max(self) -> scalars.Scalar: + return self._apply_aggregation(agg_ops.max_op) + + def min(self) -> scalars.Scalar: + return self._apply_aggregation(agg_ops.min_op) + + def std(self) -> float: + return typing.cast(float, self._apply_aggregation(agg_ops.std_op)) + + def var(self) -> float: + return typing.cast(float, self._apply_aggregation(agg_ops.var_op)) + + def _central_moment(self, n: int) -> float: + """Useful helper for calculating central moment statistics""" + # Nth central moment is mean((x-mean(x))^n) + # See: https://en.wikipedia.org/wiki/Moment_(mathematics) + mean = self.mean() + mean_deltas = self - mean + delta_power = mean_deltas + # TODO(tbergeron): Replace with pow once implemented + for i in range(1, n): + delta_power = delta_power * mean_deltas + return delta_power.mean() + + def kurt(self) -> float: + # TODO(tbergeron): Cache intermediate count/moment/etc. statistics at block level + count = self.count() + moment4 = self._central_moment(4) + moment2 = self._central_moment(2) # AKA: Population Variance + + # Kurtosis is often defined as the second standardize moment: moment(4)/moment(2)**2 + # Pandas however uses Fisher’s estimator, implemented below + numerator = (count + 1) * (count - 1) * moment4 + denominator = (count - 2) * (count - 3) * moment2**2 + adjustment = 3 * (count - 1) ** 2 / ((count - 2) * (count - 3)) + + return (numerator / denominator) - adjustment + + kurtosis = kurt + + def mode(self) -> Series: + block = self._block + # Approach: Count each value, return each value for which count(x) == max(counts)) + block, agg_ids = block.aggregate( + [self._value_column], + ((self._value_column, agg_ops.count_op),), + as_index=False, + ) + value_count_col_id = agg_ids[0] + block, max_value_count_col_id = block.apply_window_op( + value_count_col_id, + agg_ops.max_op, + window_spec=WindowSpec(), + ) + block, is_mode_col_id = block.apply_binary_op( + value_count_col_id, + max_value_count_col_id, + ops.eq_op, + ) + block = block.filter(is_mode_col_id) + mode_values_series = Series( + block.select_column(self._value_column).assign_label( + self._value_column, self.name + ) + ) + return typing.cast( + Series, mode_values_series.sort_values().reset_index(drop=True) + ) + + def mean(self) -> float: + return typing.cast(float, self._apply_aggregation(agg_ops.mean_op)) + + def sum(self) -> float: + return typing.cast(float, self._apply_aggregation(agg_ops.sum_op)) + + def prod(self) -> float: + return typing.cast(float, self._apply_aggregation(agg_ops.product_op)) + + product = prod + + def __eq__(self, other: object) -> Series: # type: ignore + return self.eq(other) + + def __ne__(self, other: object) -> Series: # type: ignore + return self.ne(other) + + def __invert__(self) -> Series: + return self._apply_unary_op(ops.invert_op) + + def eq(self, other: object) -> Series: + # TODO: enforce stricter alignment + return self._apply_binary_op(other, ops.eq_op) + + def ne(self, other: object) -> Series: + # TODO: enforce stricter alignment + return self._apply_binary_op(other, ops.ne_op) + + def where(self, cond, other=None): + value_id, cond_id, other_id, block = self._align3(cond, other) + block, result_id = block.apply_ternary_op( + value_id, cond_id, other_id, ops.where_op + ) + return Series(block.select_column(result_id).with_column_labels([self.name])) + + def clip(self, lower, upper): + if lower is None and upper is None: + return self + if lower is None: + return self._apply_binary_op(upper, ops.clip_upper, alignment="left") + if upper is None: + return self._apply_binary_op(lower, ops.clip_lower, alignment="left") + value_id, lower_id, upper_id, block = self._align3(lower, upper) + block, result_id = block.apply_ternary_op( + value_id, lower_id, upper_id, ops.clip_op + ) + return Series(block.select_column(result_id).with_column_labels([self.name])) + + def argmax(self) -> scalars.Scalar: + block, row_nums = self._block.promote_offsets() + block = block.order_by( + [ + OrderingColumnReference( + self._value_column, direction=OrderingDirection.DESC + ), + OrderingColumnReference(row_nums), + ] + ) + return typing.cast( + scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] + ) + + def argmin(self) -> scalars.Scalar: + block, row_nums = self._block.promote_offsets() + block = block.order_by( + [ + OrderingColumnReference(self._value_column), + OrderingColumnReference(row_nums), + ] + ) + return typing.cast( + scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] + ) + + def __getitem__(self, indexer: Series): + # TODO: enforce stricter alignment, should fail if indexer is missing any keys. + (left, right, block) = self._align(indexer, "left") + block = block.filter(right) + block = block.select_column(left) + return Series(block) + + def __getattr__(self, key: str): + if hasattr(pandas.Series, key): + raise NotImplementedError( + textwrap.dedent( + f""" + BigQuery DataFrames has not yet implemented an equivalent to + 'pandas.Series.{key}'. Please check + https://github.com/googleapis/python-bigquery-dataframes/issues for + existing feature requests, or file your own. + Please include information about your use case, as well as + relevant code snippets. + """ + ) + ) + else: + raise AttributeError(key) + + def _align3(self, other1: Series | scalars.Scalar, other2: Series | scalars.Scalar, how="left") -> tuple[str, str, str, blocks.Block]: # type: ignore + """Aligns the series value with 2 other scalars or series objects. Returns new values and joined tabled expression.""" + values, index = self._align_n([other1, other2], how) + return (values[0], values[1], values[2], index) + + def _apply_aggregation(self, op: agg_ops.AggregateOp) -> Any: + aggregation_result = typing.cast( + ibis_types.Scalar, op._as_ibis(self[self.notnull()]._to_ibis_expr()) + ) + return bigframes.core.scalar.DeferredScalar( + aggregation_result, self._block._expr._session + ).compute() + + def _apply_window_op( + self, + op: agg_ops.WindowOp, + window_spec: bigframes.core.WindowSpec, + ): + block = self._block + block, result_id = block.apply_window_op( + self._value_column, op, window_spec=window_spec, result_label=self.name + ) + return Series(block.select_column(result_id)) + + def value_counts( + self, + normalize: bool = False, + sort: bool = True, + ascending: bool = False, + *, + dropna: bool = True, + ): + block = block_ops.value_counts( + self._block, + [self._value_column], + normalize=normalize, + ascending=ascending, + dropna=dropna, + ) + return Series(block) + + def sort_values(self, *, axis=0, ascending=True, na_position="last") -> Series: + if na_position not in ["first", "last"]: + raise ValueError("Param na_position must be one of 'first' or 'last'") + direction = OrderingDirection.ASC if ascending else OrderingDirection.DESC + block = self._block.order_by( + [ + OrderingColumnReference( + self._value_column, + direction=direction, + na_last=(na_position == "last"), + ) + ] + ) + return Series(block) + + def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: + # TODO(tbergeron): Support level parameter once multi-index introduced. + if na_position not in ["first", "last"]: + raise ValueError("Param na_position must be one of 'first' or 'last'") + block = self._block + direction = OrderingDirection.ASC if ascending else OrderingDirection.DESC + na_last = na_position == "last" + ordering = [ + OrderingColumnReference(column, direction=direction, na_last=na_last) + for column in block.index_columns + ] + block = block.order_by(ordering) + return Series(block) + + 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 = WindowSpec( + preceding=window - 1, following=0, min_periods=min_periods or window + ) + return bigframes.core.window.Window( + self._block, window_spec, self._value_column + ) + + def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: + window_spec = WindowSpec(following=0, min_periods=min_periods) + return bigframes.core.window.Window( + self._block, window_spec, self._value_column + ) + + def groupby( + self, + by: typing.Union[ + blocks.Label, Series, typing.Sequence[typing.Union[blocks.Label, Series]] + ] = None, + axis=0, + level: typing.Optional[ + int | str | typing.Sequence[int] | typing.Sequence[str] + ] = None, + as_index: bool = True, + *, + dropna: bool = True, + ) -> bigframes.core.groupby.SeriesGroupBy: + if (by is not None) and (level is not None): + raise ValueError("Do not specify both 'by' and 'level'") + if not as_index: + raise ValueError("as_index=False only valid with DataFrame") + if axis: + raise ValueError("No axis named {} for object type Series".format(level)) + if by is not None: + return self._groupby_values(by, dropna) + if level is not None: + return self._groupby_level(level, dropna) + else: + raise TypeError("You have to supply one of 'by' and 'level'") + + def _groupby_level( + self, + level: int | str | typing.Sequence[int] | typing.Sequence[str], + dropna: bool = True, + ) -> bigframes.core.groupby.SeriesGroupBy: + return groupby.SeriesGroupBy( + self._block, + self._value_column, + self._resolve_levels(level), + value_name=self.name, + dropna=dropna, + ) + + def _groupby_values( + self, + by: typing.Union[ + blocks.Label, Series, typing.Sequence[typing.Union[blocks.Label, Series]] + ], + dropna: bool = True, + ) -> bigframes.core.groupby.SeriesGroupBy: + if not isinstance(by, Series) and _is_list_like(by): + by = list(by) + else: + by = [typing.cast(typing.Union[blocks.Label, Series], by)] + + block = self._block + grouping_cols: typing.Sequence[str] = [] + value_col = self._value_column + for key in by: + if isinstance(key, Series): + combined_index, ( + get_column_left, + get_column_right, + ) = block.index.join( + key._block.index, how="inner" if dropna else "left" + ) + + value_col = get_column_left(self._value_column) + grouping_cols = [ + *[get_column_left(value) for value in grouping_cols], + get_column_right(key._value_column), + ] + block = combined_index._block + else: + # Interpret as index level + matches = block.index_name_to_col_id.get(key, []) + if len(matches) != 1: + raise ValueError( + f"GroupBy key {key} does not map to unambiguous index level" + ) + grouping_cols = [*grouping_cols, matches[0]] + + return groupby.SeriesGroupBy( + block, + value_col, + grouping_cols, + value_name=self.name, + dropna=dropna, + ) + + def apply(self, func) -> Series: + # TODO(shobs, b/274645634): Support convert_dtype, args, **kwargs + # is actually a ternary op + return self._apply_unary_op(ops.RemoteFunctionOp(func)) + + def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: + return Series(self._get_block().add_prefix(prefix)) + + def add_suffix(self, suffix: str, axis: int | str | None = None) -> Series: + return Series(self._get_block().add_suffix(suffix)) + + def drop_duplicates(self, *, keep: str = "first") -> Series: + block = block_ops.drop_duplicates(self._block, (self._value_column,), keep) + return Series(block) + + def unique(self) -> Series: + return self.drop_duplicates() + + def duplicated(self, keep: str = "first") -> Series: + block, indicator = block_ops.indicate_duplicates( + self._block, (self._value_column,), keep + ) + return Series( + block.select_column( + indicator, + ).with_column_labels([self.name]) + ) + + def mask(self, cond, other=None) -> Series: + if callable(cond): + cond = self.apply(cond) + + if not isinstance(cond, Series): + raise TypeError( + f"Only bigframes series condition is supported, received {type(cond).__name__}" + ) + return self.where(~cond, other) + + def to_frame(self) -> bigframes.dataframe.DataFrame: + # To be consistent with Pandas, it assigns 0 as the column name if missing. 0 is the first element of RangeIndex. + block = self._block.with_column_labels([self.name] if self.name else ["0"]) + return bigframes.dataframe.DataFrame(block) + + def to_csv(self, path_or_buf=None, **kwargs) -> typing.Optional[str]: + # TODO(b/280651142): Implement version that leverages bq export native csv support to bypass local pandas step. + return self.compute().to_csv(path_or_buf, **kwargs) + + def to_dict(self, into: type[dict] = dict) -> typing.Mapping: + return typing.cast(dict, self.compute().to_dict(into)) + + def to_excel(self, excel_writer, sheet_name="Sheet1", **kwargs) -> None: + return self.compute().to_excel(excel_writer, sheet_name, **kwargs) + + def to_json( + self, + path_or_buf=None, + orient: typing.Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + **kwargs, + ) -> typing.Optional[str]: + # TODO(b/280651142): Implement version that leverages bq export native csv support to bypass local pandas step. + return self.compute().to_json(path_or_buf, **kwargs) + + def to_latex( + self, buf=None, columns=None, header=True, index=True, **kwargs + ) -> typing.Optional[str]: + return self.compute().to_latex( + buf, columns=columns, header=header, index=index, **kwargs + ) + + def tolist(self) -> list: + return self.compute().to_list() + + to_list = tolist + + def to_markdown( + self, + buf: typing.IO[str] | None = None, + mode: str = "wt", + index: bool = True, + **kwargs, + ) -> typing.Optional[str]: + return self.compute().to_markdown(buf, mode=mode, index=index, **kwargs) # type: ignore + + def to_numpy( + self, dtype=None, copy=False, na_value=None, **kwargs + ) -> numpy.ndarray: + return self.compute().to_numpy(dtype, copy, na_value, **kwargs) + + __array__ = to_numpy + + def to_pickle(self, path, **kwargs) -> None: + return self.compute().to_pickle(path, **kwargs) + + def to_string( + self, + buf=None, + na_rep="NaN", + float_format=None, + header=True, + index=True, + length=False, + dtype=False, + name=False, + max_rows=None, + min_rows=None, + ) -> typing.Optional[str]: + return self.compute().to_string( + buf, + na_rep, + float_format, + header, + index, + length, + dtype, + name, + max_rows, + min_rows, + ) + + def to_xarray(self): + return self.compute().to_xarray() + + # Keep this at the bottom of the Series class to avoid + # confusing type checker by overriding str + @property + def str(self) -> strings.StringMethods: + return strings.StringMethods(self._block) + + def _slice( + self, + start: typing.Optional[int] = None, + stop: typing.Optional[int] = None, + step: typing.Optional[int] = None, + ) -> bigframes.series.Series: + return bigframes.series.Series( + self._block.slice(start=start, stop=stop, step=step).select_column( + self._value_column + ), + ) + + +def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: + return pandas.api.types.is_list_like(obj) diff --git a/bigframes/session.py b/bigframes/session.py new file mode 100644 index 00000000000..9590ecb8c77 --- /dev/null +++ b/bigframes/session.py @@ -0,0 +1,1055 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Session manages the connection to BigQuery.""" + +from __future__ import annotations + +import logging +import os +import re +import textwrap +import typing +from typing import ( + Any, + Callable, + Dict, + IO, + Iterable, + List, + Literal, + MutableSequence, + Optional, + Sequence, + Tuple, + Union, +) +import uuid +import warnings + +import google.api_core.client_info +import google.api_core.client_options +import google.api_core.exceptions +import google.api_core.gapic_v1.client_info +import google.auth.credentials +import google.cloud.bigquery as bigquery +import google.cloud.bigquery_connection_v1 +import google.cloud.bigquery_storage_v1 +import google.cloud.storage as storage # type: ignore +import ibis +import ibis.backends.bigquery as ibis_bigquery +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types +import numpy as np +import pandas +import pydata_google_auth + +import bigframes._config.bigquery_options as bigquery_options +import bigframes.core as core +import bigframes.core.blocks as blocks +import bigframes.core.guid as guid +from bigframes.core.ordering import OrderingColumnReference +import bigframes.dataframe as dataframe +import bigframes.formatting_helpers as formatting_helpers +import bigframes.ml.loader +from bigframes.remote_function import remote_function as bigframes_rf +import bigframes.version +import third_party.bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq +import third_party.bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet +import third_party.bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers + +_ENV_DEFAULT_PROJECT = "GOOGLE_CLOUD_PROJECT" +_APPLICATION_NAME = f"bigframes/{bigframes.version.__version__}" +_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] + +# BigQuery is a REST API, which requires the protocol as part of the URL. +_BIGQUERY_REGIONAL_ENDPOINT = "https://{location}-bigquery.googleapis.com" + +# BigQuery Connection and Storage are gRPC APIs, which don't support the +# https:// protocol in the API endpoint URL. +_BIGQUERYCONNECTION_REGIONAL_ENDPOINT = "{location}-bigqueryconnection.googleapis.com" +_BIGQUERYSTORAGE_REGIONAL_ENDPOINT = "{location}-bigquerystorage.googleapis.com" + +# TODO(swast): Need to connect to regional endpoints when performing remote +# functions operations (BQ Connection API, Cloud Run / Cloud Functions). + +# pydata-google-auth credentials in case auth credentials are not available +# otherwise +_pydata_google_auth_credentials: Optional[google.auth.credentials.Credentials] = None +_pydata_google_auth_project: Optional[str] = None + +logger = logging.getLogger(__name__) + + +def _is_query(query_or_table: str) -> bool: + """Determine if `query_or_table` is a table ID or a SQL string""" + return re.search(r"\s", query_or_table.strip(), re.MULTILINE) is not None + + +# TODO(shobs): Remove it after the same is available via pydata-google-auth +# after https://github.com/pydata/pydata-google-auth/pull/71 is merged, released +# and upgraded in the google colab image. +def _ensure_application_default_credentials_in_colab_environment(): + # This is a special handling for google colab environment where we want to + # use the colab specific authentication flow + # https://github.com/googlecolab/colabtools/blob/3c8772efd332289e1c6d1204826b0915d22b5b95/google/colab/auth.py#L209 + try: + from google.colab import auth + + auth.authenticate_user() + except Exception: + # We are catching a broad exception class here because we want to be + # agnostic to anything that could internally go wrong in the google + # colab auth. Some of the known exception we want to pass on are: + # + # ModuleNotFoundError: No module named 'google.colab' + # ImportError: cannot import name 'auth' from 'google.cloud' + # MessageError: Error: credential propagation was unsuccessful + # + # The MessageError happens on Vertex Colab when it fails to resolve auth + # from the Compute Engine Metadata server. + pass + + +pydata_google_auth.auth._ensure_application_default_credentials_in_colab_environment = ( + _ensure_application_default_credentials_in_colab_environment +) + + +def _get_default_credentials_with_project(): + global _pydata_google_auth_credentials, _pydata_google_auth_project + if not _pydata_google_auth_credentials or not _pydata_google_auth_credentials.valid: + # We want to initiate auth via a non-local web server which + # particularly helps in a cloud notebook environment where the + # machine running the notebook UI and the VM running the notebook + # runtime are not the same. + # TODO(shobs, b/278903498): Use BigQuery DataFrames's own client id + # and secret + ( + _pydata_google_auth_credentials, + _pydata_google_auth_project, + ) = pydata_google_auth.default(_SCOPES, use_local_webserver=False) + return _pydata_google_auth_credentials, _pydata_google_auth_project + + +def _create_bq_clients( + project: Optional[str], + location: Optional[str], + use_regional_endpoints: Optional[bool], + credentials: Optional[google.auth.credentials.Credentials], +) -> typing.Tuple[ + bigquery.Client, + google.cloud.bigquery_connection_v1.ConnectionServiceClient, + google.cloud.bigquery_storage_v1.BigQueryReadClient, +]: + """Create and initialize BigQuery client objects.""" + + credentials_project = None + if credentials is None: + credentials, credentials_project = _get_default_credentials_with_project() + + # Prefer the project in this order: + # 1. Project explicitly specified by the user + # 2. Project set in the environment + # 3. Project associated with the default credentials + project = ( + project + or os.getenv(_ENV_DEFAULT_PROJECT) + or typing.cast(Optional[str], credentials_project) + ) + + if not project: + raise ValueError("Project must be set to initialize BigQuery client.") + + if use_regional_endpoints: + bq_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERY_REGIONAL_ENDPOINT.format(location=location), + ) + bqstorage_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERYSTORAGE_REGIONAL_ENDPOINT.format(location=location) + ) + bqconnection_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERYCONNECTION_REGIONAL_ENDPOINT.format(location=location) + ) + else: + bq_options = None + bqstorage_options = None + bqconnection_options = None + + bq_info = google.api_core.client_info.ClientInfo(user_agent=_APPLICATION_NAME) + bqclient = bigquery.Client( + client_info=bq_info, + client_options=bq_options, + credentials=credentials, + project=project, + ) + + bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + bqconnectionclient = google.cloud.bigquery_connection_v1.ConnectionServiceClient( + client_info=bqconnection_info, + client_options=bqconnection_options, + credentials=credentials, + ) + + bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + bqstorageclient = google.cloud.bigquery_storage_v1.BigQueryReadClient( + client_info=bqstorage_info, + client_options=bqstorage_options, + credentials=credentials, + ) + + return bqclient, bqconnectionclient, bqstorageclient + + +class Session( + third_party_pandas_gbq.GBQIOMixin, + third_party_pandas_parquet.ParquetIOMixin, + third_party_pandas_readers.ReaderIOMixin, +): + """Establishes a BigQuery connection to capture a group of job activities related to + DataFrames.""" + + def __init__(self, context: Optional[bigquery_options.BigQueryOptions] = None): + if context is None: + context = bigquery_options.BigQueryOptions() + + # TODO(swast): Get location from the environment. + if context is None or context.location is None: + self._location = "US" + warnings.warn( + f"No explicit location is set, so using location {self._location} for the session.", + stacklevel=2, + ) + else: + self._location = context.location + + ( + self.bqclient, + self.bqconnectionclient, + self.bqstorageclient, + ) = _create_bq_clients( + project=context.project, + location=self._location, + use_regional_endpoints=context.use_regional_endpoints, + credentials=context.credentials, + ) + + self._create_and_bind_bq_session() + self.ibis_client = typing.cast( + ibis_bigquery.Backend, + ibis.bigquery.connect( + project_id=context.project, + client=self.bqclient, + storage_client=self.bqstorageclient, + ), + ) + + self._remote_udf_connection = context.remote_udf_connection + + # Now that we're starting the session, don't allow the options to be + # changed. + context._session_started = True + + @property + def _session_dataset_id(self): + """A dataset for storing temporary objects local to the session + This is a workaround for BQML models and remote functions that do not + yet support session-temporary instances.""" + return self._session_dataset.dataset_id + + def _create_and_bind_bq_session(self): + """Create a BQ session and bind the session id with clients to capture BQ activities: + go/bigframes-transient-data""" + job_config = bigquery.QueryJobConfig(create_session=True) + query_job = self.bqclient.query( + "SELECT 1", job_config=job_config, location=self._location + ) + query_job.result() # blocks until finished + self._session_id = query_job.session_info.session_id + + self.bqclient.default_query_job_config = bigquery.QueryJobConfig( + connection_properties=[ + bigquery.ConnectionProperty("session_id", self._session_id) + ] + ) + self.bqclient.default_load_job_config = bigquery.LoadJobConfig( + connection_properties=[ + bigquery.ConnectionProperty("session_id", self._session_id) + ] + ) + + # Dataset for storing BQML models and remote functions, which don't yet + # support proper session temporary storage yet + self._session_dataset = bigquery.Dataset( + f"{self.bqclient.project}.bigframes_temp_{self._location.lower().replace('-', '_')}" + ) + self._session_dataset.location = self._location + self._session_dataset.default_table_expiration_ms = 24 * 60 * 60 * 1000 + + # TODO: handle case when the dataset does not exist and the user does + # not have permission to create one (bigquery.datasets.create IAM) + self.bqclient.create_dataset(self._session_dataset, exists_ok=True) + + def close(self): + """Terminated the BQ session, otherwises the session will be terminated automatically after + 24 hours of inactivity or after 7 days.""" + if self._session_id is not None and self.bqclient is not None: + abort_session_query = "CALL BQ.ABORT_SESSION('{}')".format(self._session_id) + query_job = self.bqclient.query(abort_session_query) + query_job.result() # blocks until finished + self._session_id = None + + def read_gbq( + self, + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, + # Add a verify index argument that fails if the index is not unique. + ) -> dataframe.DataFrame: + # TODO(b/281571214): Generate prompt to show the progress of read_gbq. + if _is_query(query): + return self.read_gbq_query( + query, + index_col=index_col, + col_order=col_order, + max_results=max_results, + ) + else: + # TODO(swast): Query the snapshot table but mark it as a + # deterministic query so we can avoid serializing if we have a + # unique index. + return self.read_gbq_table( + query, + index_col=index_col, + col_order=col_order, + max_results=max_results, + ) + + def read_gbq_query( + self, + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, + ) -> dataframe.DataFrame: + """Turn a SQL query into a DataFrame. + + Note: Because the results are written to a temporary table, ordering by + ``ORDER BY`` is not preserved. A unique `index_col` is recommended. Use + ``row_number() over ()`` if there is no natural unique index or you + want to preserve ordering. + + See also: :meth:`Session.read_gbq`. + """ + # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so + # these docstrings are inline. + + if isinstance(index_col, str): + index_cols = [index_col] + else: + index_cols = list(index_col) + + # Make sure we cluster by the index column so that subsequent + # operations are as speedy as they can be. + if index_cols: + destination: bigquery.Table | bigquery.TableReference = ( + self._query_to_session_table(query, index_cols) + ) + else: + _, query_job = self._start_query(query) + query_job.result() # Wait for job to finish. + destination = query_job.destination + + # If there was no destination table, that means the query must have + # been DDL or DML. Return some job metadata, instead. + if not destination: + return dataframe.DataFrame( + data=pandas.DataFrame( + { + "statement_type": [query_job.statement_type], + "job_id": [query_job.job_id], + "location": [query_job.location], + } + ), + session=self, + ) + + return self.read_gbq_table( + f"{destination.project}.{destination.dataset_id}.{destination.table_id}", + index_col=index_cols, + col_order=col_order, + max_results=max_results, + ) + + def read_gbq_table( + self, + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, + ) -> dataframe.DataFrame: + """Turn a BigQuery table into a DataFrame. + + See also: :meth:`Session.read_gbq`. + """ + # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so + # these docstrings are inline. + # TODO(swast): Can we re-use the temp table from other reads in the + # session, if the original table wasn't modified? + table_ref = bigquery.table.TableReference.from_string( + query, default_project=self.bqclient.project + ) + + if table_ref.dataset_id.upper() == "_SESSION": + # _SESSION tables aren't supported by the tables.get REST API. + table_expression = self.ibis_client.sql( + f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" + ) + else: + # TODO(swast): Read from a table snapshot so that reads are consistent. + table_expression = self.ibis_client.table( + table_ref.table_id, + database=f"{table_ref.project}.{table_ref.dataset_id}", + ) + + for key in col_order: + if key not in table_expression.columns: + raise ValueError( + f"Column '{key}' of `col_order` not found in this table." + ) + + if isinstance(index_col, str): + index_cols: List[str] = [index_col] + else: + index_cols = list(index_col) + + for key in index_cols: + if key not in table_expression.columns: + raise ValueError( + f"Column `{key}` of `index_col` not found in this table." + ) + + # If the index is unique and sortable, then we don't need to generate + # an ordering column. + ordering = None + is_total_ordering = False + + if len(index_cols) != 0: + index_labels = typing.cast(List[Optional[str]], index_cols) + distinct_table = table_expression.select(*index_cols).distinct() + is_unique_sql = f"""WITH full_table AS ( + {self.ibis_client.compile(table_expression)} + ), + distinct_table AS ( + {self.ibis_client.compile(distinct_table)} + ) + + SELECT (SELECT COUNT(*) FROM full_table) AS total_count, + (SELECT COUNT(*) FROM distinct_table) AS distinct_count + """ + results, _ = self._start_query(is_unique_sql) + row = next(iter(results)) + + total_count = row["total_count"] + distinct_count = row["distinct_count"] + is_total_ordering = total_count == distinct_count + ordering = core.ExpressionOrdering( + ordering_value_columns=[ + core.OrderingColumnReference(column_id) for column_id in index_cols + ], + ) + + if not is_total_ordering: + # Make sure when we generate an ordering, the row_number() + # coresponds to the index columns. + table_expression = table_expression.order_by(index_cols) + warnings.warn( + textwrap.dedent( + f""" + Got a non-unique index. A consistent ordering is not + guaranteed. DataFrame has {total_count} rows, + but only {distinct_count} distinct index values. + """, + ) + ) + else: + table_expression, ordering = self._create_sequential_ordering( + table_expression + ) + ordering_id_column = ordering.ordering_id + assert ordering_id_column is not None + is_total_ordering = True + index_cols = [ordering_id_column] + index_labels = [None] + + if max_results is not None: + if max_results <= 0: + raise ValueError("`max_results` should be a positive number.") + table_expression = table_expression.limit(max_results) + + return self._read_gbq_with_ordering( + table_expression=table_expression, + col_order=col_order, + index_cols=index_cols, + index_labels=index_labels, + ordering=ordering, + is_total_ordering=is_total_ordering, + ) + + def _read_gbq_with_ordering( + self, + table_expression: ibis_types.Table, + *, + col_order: Iterable[str] = (), + index_cols: Sequence[str] = (), + index_labels: Sequence[Optional[str]] = (), + ordering: core.ExpressionOrdering, + is_total_ordering: bool = False, + ) -> dataframe.DataFrame: + """Internal helper method that loads DataFrame from Google BigQuery given an ordering column. + + Args: + table_expression: an ibis table expression to be executed in BigQuery. + col_order: List of BigQuery column names in the desired order for results DataFrame. + index_cols: List of column names to use as the index or multi-index. + ordering: Column name to be used for ordering. If not supplied, a default ordering is generated. + + Returns: + A DataFrame representing results of the query or table. + """ + if len(index_cols) != len(index_labels): + raise ValueError( + "Needs same number of index labels are there are index columns. " + f"Got {len(index_labels)}, expected {len(index_cols)}." + ) + + if not index_cols: + raise ValueError("Need at least 1 index column.") + + # Logic: + # no total ordering, index -> create sequential order, ordered by index, use for both ordering and index + # total ordering, index -> use ordering as ordering, index as index + + # This code block ensures the existence of a total ordering. + if not is_total_ordering: + # Rows are not ordered, we need to generate a default ordering and materialize it + table_expression, ordering = self._create_sequential_ordering( + table_expression, index_cols + ) + + index_col_values = [table_expression[index_id] for index_id in index_cols] + + column_keys = list(col_order) + if len(column_keys) == 0: + non_columns = set(index_cols) + if ordering.ordering_id is not None: + non_columns.add(ordering.ordering_id) + column_keys = [ + key for key in table_expression.columns if key not in non_columns + ] + return self._read_ibis( + table_expression, + index_col_values, + index_labels, + column_keys, + ordering=ordering, + ) + + def _read_bigquery_load_job( + self, + filepath_or_buffer: str | IO["bytes"], + table: bigquery.Table, + *, + job_config: bigquery.LoadJobConfig, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + ) -> dataframe.DataFrame: + if isinstance(index_col, str): + index_cols = [index_col] + else: + index_cols = list(index_col) + + if not job_config.clustering_fields and index_cols: + job_config.clustering_fields = index_cols + + if isinstance(filepath_or_buffer, str): + if filepath_or_buffer.startswith("gs://"): + load_job = self.bqclient.load_table_from_uri( + filepath_or_buffer, table, job_config=job_config + ) + else: + with open(filepath_or_buffer, "rb") as source_file: + load_job = self.bqclient.load_table_from_file( + source_file, table, job_config=job_config + ) + else: + load_job = self.bqclient.load_table_from_file( + filepath_or_buffer, table, job_config=job_config + ) + + load_job.result() # Wait for the job to complete + + # The BigQuery REST API for tables.get doesn't take a session ID, so we + # can't get the schema for a temp table that way. + return self.read_gbq_table( + f"{table.project}.{table.dataset_id}.{table.table_id}", + index_col=index_col, + col_order=col_order, + ) + + def _read_ibis( + self, + table_expression: ibis_types.Table, + index_cols: Sequence[ibis_types.Value], + index_labels: Sequence[Optional[str]], + column_keys: Sequence[str], + ordering: Optional[core.ExpressionOrdering] = None, + ): + """Turns a table expression (plus index column) into a DataFrame.""" + hidden_ordering_columns = None + if ordering is not None and ordering.ordering_id is not None: + hidden_ordering_columns = (table_expression[ordering.ordering_id],) + + columns = list(index_cols) + for key in column_keys: + if key not in table_expression.columns: + raise ValueError(f"Column '{key}' not found in this table.") + columns.append(table_expression[key]) + + block = blocks.Block( + core.ArrayValue( + self, table_expression, columns, hidden_ordering_columns, ordering + ), + [index_col.get_name() for index_col in index_cols], + index_labels=index_labels, + ) + + return dataframe.DataFrame(block) + + def read_gbq_model(self, model_name: str): + """Loads a BQML model from Google BigQuery. + + Args: + model_name : the model's name in BigQuery in the format + `project_id.dataset_id.model_id`, or just `dataset_id.model_id` + to load from the default project. + + Returns: + A bigframes.ml Model wrapping the model. + """ + model_ref = bigquery.ModelReference.from_string( + model_name, default_project=self.bqclient.project + ) + model = self.bqclient.get_model(model_ref) + return bigframes.ml.loader.from_bq(self, model) + + def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame: + """Loads DataFrame from a Pandas DataFrame. + + The Pandas DataFrame will be persisted as a temporary BigQuery table, which can be + automatically recycled after the Session is closed. + + Args: + pandas_dataframe: a Pandas DataFrame object to be loaded. + + Returns: + A BigQuery DataFrames. + """ + # Add order column to pandas DataFrame to preserve order in BigQuery + ordering_col = "rowid" + columns = frozenset(pandas_dataframe.columns) + suffix = 2 + while ordering_col in columns: + ordering_col = f"rowid_{suffix}" + suffix += 1 + + pandas_dataframe_copy = pandas_dataframe.copy() + pandas_dataframe_copy[ordering_col] = np.arange(pandas_dataframe_copy.shape[0]) + + # Specify the datetime dtypes, which is auto-detected as timestamp types. + schema = [] + for column, dtype in zip(pandas_dataframe.columns, pandas_dataframe.dtypes): + if dtype == "timestamp[us][pyarrow]": + schema.append( + bigquery.SchemaField(column, bigquery.enums.SqlTypeNames.DATETIME) + ) + + # Unnamed are not copied to BigQuery when load_table_from_dataframe + # executes. + index_cols = list( + filter(lambda name: name is not None, pandas_dataframe_copy.index.names) + ) + index_labels = typing.cast(List[Optional[str]], index_cols) + cluster_cols = index_cols + [ordering_col] + + if len(index_cols) == 0: + index_cols = [ordering_col] + index_labels = [None] + + job_config = bigquery.LoadJobConfig(schema=schema) + job_config.clustering_fields = cluster_cols + + # TODO(swast): Rename the unnamed index columns and restore them after + # the load job completes. + # Column values will be loaded as null if the column name has spaces. + # https://github.com/googleapis/python-bigquery/issues/1566 + load_table_destination = self._create_session_table() + load_job = self.bqclient.load_table_from_dataframe( + pandas_dataframe_copy, + load_table_destination, + job_config=job_config, + ) + load_job.result() # Wait for the job to complete + + ordering = core.ExpressionOrdering( + ordering_id_column=OrderingColumnReference(ordering_col), is_sequential=True + ) + table_expression = self.ibis_client.sql( + f"SELECT * FROM `{load_table_destination.table_id}`" + ) + + return self._read_gbq_with_ordering( + table_expression=table_expression, + index_cols=index_cols, + index_labels=index_labels, + ordering=ordering, + is_total_ordering=True, + ) + + def read_csv( + self, + filepath_or_buffer: str | IO["bytes"], + *, + sep: Optional[str] = ",", + header: Optional[int] = 0, + names: Optional[ + Union[MutableSequence[Any], np.ndarray[Any, Any], Tuple[Any, ...], range] + ] = None, + index_col: Optional[ + Union[int, str, Sequence[Union[str, int]], Literal[False]] + ] = None, + usecols: Optional[ + Union[ + MutableSequence[str], + Tuple[str, ...], + Sequence[int], + pandas.Series, + pandas.Index, + np.ndarray[Any, Any], + Callable[[Any], bool], + ] + ] = None, + dtype: Optional[Dict] = None, + engine: Optional[ + Literal["c", "python", "pyarrow", "python-fwf", "bigquery"] + ] = None, + encoding: Optional[str] = None, + **kwargs, + ) -> dataframe.DataFrame: + table = bigquery.Table(self._create_session_table()) + + if engine is not None and engine == "bigquery": + if any(param is not None for param in (dtype, names)): + not_supported = ("dtype", "names") + raise NotImplementedError( + f"BigQuery engine does not support these arguments: {not_supported}" + ) + + if index_col is not None and ( + not index_col or not isinstance(index_col, str) + ): + raise NotImplementedError( + "BigQuery engine only supports a single column name for `index_col`." + ) + + # None value for index_col cannot be passed to read_gbq + if index_col is None: + index_col = () + + # usecols should only be an iterable of strings (column names) for use as col_order in read_gbq. + col_order: Tuple[Any, ...] = tuple() + if usecols is not None: + if isinstance(usecols, Iterable) and all( + isinstance(col, str) for col in usecols + ): + col_order = tuple(col for col in usecols) + else: + raise NotImplementedError( + "BigQuery engine only supports an iterable of strings for `usecols`." + ) + + valid_encodings = {"UTF-8", "ISO-8859-1"} + if encoding is not None and encoding not in valid_encodings: + raise NotImplementedError( + f"BigQuery engine only supports the following encodings: {valid_encodings}" + ) + + job_config = bigquery.LoadJobConfig() + job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED + job_config.source_format = bigquery.SourceFormat.CSV + job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY + job_config.autodetect = True + job_config.field_delimiter = sep + job_config.encoding = encoding + + # We want to match pandas behavior. If header is 0, no rows should be skipped, so we + # do not need to set `skip_leading_rows`. If header is None, then there is no header. + # Setting skip_leading_rows to 0 does that. If header=N and N>0, we want to skip N rows. + # `skip_leading_rows` skips N-1 rows, so we set it to header+1. + if header is not None and header > 0: + job_config.skip_leading_rows = header + 1 + elif header is None: + job_config.skip_leading_rows = 0 + + return self._read_bigquery_load_job( + filepath_or_buffer, + table, + job_config=job_config, + index_col=index_col, + col_order=col_order, + ) + else: + if any(arg in kwargs for arg in ("chunksize", "iterator")): + raise NotImplementedError( + "'chunksize' and 'iterator' arguments are not supported." + ) + + if isinstance(filepath_or_buffer, str): + self._check_file_size(filepath_or_buffer) + pandas_df = pandas.read_csv( + filepath_or_buffer, + sep=sep, + header=header, + names=names, + index_col=index_col, + usecols=usecols, + dtype=dtype, + engine=engine, + encoding=encoding, + **kwargs, + ) + return self.read_pandas(pandas_df) + + def read_parquet( + self, + path: str | IO["bytes"], + ) -> dataframe.DataFrame: + # Note: "engine" is omitted because it is redundant. Loading a table + # from a pandas DataFrame will just create another parquet file + load + # job anyway. + table = bigquery.Table(self._create_session_table()) + + job_config = bigquery.LoadJobConfig() + job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED + job_config.source_format = bigquery.SourceFormat.PARQUET + job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY + + return self._read_bigquery_load_job(path, table, job_config=job_config) + + def _check_file_size(self, filepath: str): + max_size = 1024 * 1024 * 1024 # 1 GB in bytes + if filepath.startswith("gs://"): # GCS file path + client = storage.Client() + bucket_name, blob_name = filepath.split("/", 3)[2:] + bucket = client.bucket(bucket_name) + blob = bucket.blob(blob_name) + blob.reload() + file_size = blob.size + else: # local file path + file_size = os.path.getsize(filepath) + + if file_size > max_size: + # Convert to GB + file_size = round(file_size / (1024**3), 1) + max_size = int(max_size / 1024**3) + logger.warning( + f"File size {file_size}GB exceeds {max_size}GB. " + "It is recommended to use engine='bigquery' " + "for large files to avoid loading the file into local memory." + ) + + def _create_session_table(self) -> bigquery.TableReference: + table_name = f"{uuid.uuid4().hex}" + dataset = bigquery.Dataset( + bigquery.DatasetReference(self.bqclient.project, "_SESSION") + ) + return dataset.table(table_name) + + def _create_sequential_ordering( + self, table: ibis_types.Table, index_cols: Iterable[str] = () + ) -> Tuple[ibis_types.Table, core.ExpressionOrdering]: + # Since this might also be used as the index, don't use the default + # "ordering ID" name. + default_ordering_name = guid.generate_guid("bigframes_ordering_") + default_ordering_col = ( + ibis.row_number().cast(ibis_dtypes.int64).name(default_ordering_name) + ) + table = table.mutate(**{default_ordering_name: default_ordering_col}) + table_ref = self._query_to_session_table( + self.ibis_client.compile(table), + cluster_cols=list(index_cols) + [default_ordering_name], + ) + table = self.ibis_client.sql(f"SELECT * FROM `{table_ref.table_id}`") + ordering_reference = core.OrderingColumnReference(default_ordering_name) + ordering = core.ExpressionOrdering( + ordering_id_column=ordering_reference, is_sequential=True + ) + return table, ordering + + def _query_to_session_table( + self, query_text: str, cluster_cols: Iterable[str] + ) -> bigquery.TableReference: + # Can't set a table in _SESSION as destination via query job API, so we + # run DDL, instead. + table = self._create_session_table() + cluster_cols_sql = ", ".join(f"`{cluster_col}`" for cluster_col in cluster_cols) + + # TODO(swast): This might not support multi-statement SQL queries. + ddl_text = f""" + CREATE TEMP TABLE `_SESSION`.`{table.table_id}` + CLUSTER BY {cluster_cols_sql} + AS {query_text} + """ + query_job = self.bqclient.query(ddl_text) + try: + query_job.result() # Wait for the job to complete + except google.api_core.exceptions.Conflict: + # Allow query retry to succeed. + pass + return table + + def remote_function( + self, + input_types: List[type], + output_type: type, + dataset: Optional[str] = None, + bigquery_connection: Optional[str] = None, + reuse: bool = True, + ): + """Decorator to turn a user defined function into a BigQuery remote function. + + Args: + input_types (list(type)): + List of input data types in the user defined function. + output_type (type): + Data type of the output in the user defined function. + dataset (str, Optional): + Dataset to use to create a BigQuery function. It should be in + `.` or `` format. If this + param is not provided then session dataset id would be used. + bigquery_connection (str, Optional): + Name of the BigQuery connection. If it is pre created in the same + location as the `bigquery_client.location` then it would be used, + otherwise it would be created dynamically assuming the user has + necessary priviliges. If this param is not provided then the + bigquery connection from the session would be used. + reuse (bool, Optional): + Reuse the remote function if already exists. + `True` by default, which will result in reusing an existing remote + function (if any) that was previously created for the same udf. + Setting it to false would force creating a unique remote function. + If the required remote function does not exist then it would be + created irrespective of this param. + + Notes: + Please make sure following is setup before using this API: + + 1. Have the below APIs enabled for your project: + a. BigQuery Connection API + b. Cloud Functions API + c. Cloud Run API + d. Cloud Build API + e. Artifact Registry API + f. Cloud Resource Manager API + + This can be done from the cloud console (change PROJECT_ID to yours): + https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID + Or from the gcloud CLI: + $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com + + 2. Have following IAM roles enabled for you: + a. BigQuery Data Editor (roles/bigquery.dataEditor) + b. BigQuery Connection Admin (roles/bigquery.connectionAdmin) + c. Cloud Functions Developer (roles/cloudfunctions.developer) + d. Service Account User (roles/iam.serviceAccountUser) + e. Storage Object Viewer (roles/storage.objectViewer) + f. Project IAM Admin (roles/resourcemanager.projectIamAdmin) + (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) + + 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: + a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection + b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + Alternatively, the IAM could also be setup via the gcloud CLI: + $ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker" + + """ + return bigframes_rf( + input_types, + output_type, + session=self, + dataset=dataset, + bigquery_connection=bigquery_connection, + reuse=reuse, + ) + + def _start_query( + self, + sql: str, + job_config: Optional[bigquery.job.QueryJobConfig] = None, + max_results: Optional[int] = None, + ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + if job_config is not None: + query_job = self.bqclient.query(sql, job_config=job_config) + else: + query_job = self.bqclient.query(sql) + + opts = bigframes.options.display + if opts.progress_bar is not None: + results_iterator = formatting_helpers.wait_for_job( + query_job, max_results, opts.progress_bar + ) + else: + results_iterator = query_job.result(max_results=max_results) + return results_iterator, query_job + + def _extract_table(self, source_table, destination_uris, job_config): + extract_job = self.bqclient.extract_table( + source=source_table, + destination_uris=destination_uris, + job_config=job_config, + ) + extract_job.result() + return extract_job + + def _rows_to_dataframe( + self, row_iterator: bigquery.table.RowIterator + ) -> pandas.DataFrame: + return row_iterator.to_dataframe( + bool_dtype=pandas.BooleanDtype(), + int_dtype=pandas.Int64Dtype(), + float_dtype=pandas.Float64Dtype(), + string_dtype=pandas.StringDtype(storage="pyarrow"), + ) + + +def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: + return Session(context) diff --git a/bigframes/version.py b/bigframes/version.py new file mode 100644 index 00000000000..df6dc3c17c8 --- /dev/null +++ b/bigframes/version.py @@ -0,0 +1,15 @@ +# Copyright 2022 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. + +__version__ = "0.1.0" diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000000..d4bb2cbb9ed --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.rst b/docs/README.rst new file mode 120000 index 00000000000..89a0106941f --- /dev/null +++ b/docs/README.rst @@ -0,0 +1 @@ +../README.rst \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000000..c663a6ef337 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,15 @@ +/* Ensure minimum width for 'Parameters' / 'Returns' column */ +dl.field-list > dt { + min-width: 100px +} + +/* Insert space between methods for readability */ +dl.method { + padding-top: 10px; + padding-bottom: 10px +} + +/* Insert empty space between classes */ +dl.class { + padding-bottom: 50px +} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000000..bfdcc4759c5 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,45 @@ + +{% extends "!layout.html" %} +{%- block content %} +{%- if theme_fixed_sidebar|lower == 'true' %} +
+ {{ sidebar() }} + {%- block document %} +
+ {%- if render_sidebar %} +
+ {%- endif %} + + {%- block relbar_top %} + {%- if theme_show_relbar_top|tobool %} + + {%- endif %} + {% endblock %} + +
+ {% block body %} {% endblock %} +
+ + {%- block relbar_bottom %} + {%- if theme_show_relbar_bottom|tobool %} + + {%- endif %} + {% endblock %} + + {%- if render_sidebar %} +
+ {%- endif %} +
+ {%- endblock %} +
+
+{%- else %} +{{ super() }} +{%- endif %} +{%- endblock %} diff --git a/docs/changelog.md b/docs/changelog.md new file mode 120000 index 00000000000..04c99a55caa --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000000..3ab5be51035 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,382 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 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. +# +# bigframes documentation build configuration file +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys +from typing import Any, Dict + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath("..")) + +# For plugins that can not read conf.py. +# See also: https://github.com/docascode/sphinx-docfx-yaml/issues/85 +sys.path.insert(0, os.path.abspath(".")) + +__version__ = "" + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = "1.5.5" + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "recommonmark", +] + +# autodoc/autosummary flags +autoclass_content = "both" +autodoc_default_options = {"members": True} +autosummary_generate = True + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The root toctree document. +root_doc = "index" + +# General information about the project. +project = "BigQuery DataFrames" +copyright = "2022-2023 Google LLC" +author = "Google APIs" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The full version, including alpha/beta/rc tags. +release = __version__ +# The short X.Y version. +version = ".".join(release.split(".")[0:2]) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [ + "_build", + "**/.nox/**/*", + "samples/AUTHORING_GUIDE.md", + "samples/CONTRIBUTING.md", + "samples/snippets/README.rst", +] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "alabaster" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + "description": "BigQuery DataFrames provides DataFrame APIs on the BigQuery engine.", + # "github_user": "googleapis", + # "github_repo": "python-bigquery-storage", + # "github_banner": True, + "font_family": "'Roboto', Georgia, sans", + "head_font_family": "'Roboto', Georgia, serif", + "code_font_family": "'Roboto Mono', 'Consolas', monospace", +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = "bigframes-doc" + +# -- Options for warnings ------------------------------------------------------ + + +suppress_warnings = [ + # Temporarily suppress this to avoid "more than one target found for + # cross-reference" warning, which are intractable for us to avoid while in + # a mono-repo. + # See https://github.com/sphinx-doc/sphinx/blob + # /2a65ffeef5c107c19084fabdd706cdff3f52d93c/sphinx/domains/python.py#L843 + "ref.python" +] + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements: Dict[str, Any] = { + # Avoid "too deeply nested" error by using enumitem package. + # See: https://stackoverflow.com/a/28454426/101923 + "preamble": r""" +\usepackage{enumitem} +\setlistdepth{99} +""" +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + root_doc, + "bigframes.tex", + "bigframes Documentation", + author, + "manual", + ) +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + root_doc, + "bigframes", + "bigframes Documentation", + [author], + 1, + ) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + root_doc, + "bigframes", + "bigframes Documentation", + author, + "bigframes", + "bigframes Library", + "APIs", + ) +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + "python": ("https://python.readthedocs.org/en/latest/", None), + "google-auth": ("https://googleapis.dev/python/google-auth/latest/", None), + "google.api_core": ( + "https://googleapis.dev/python/google-api-core/latest/", + None, + ), + "grpc": ("https://grpc.github.io/grpc/python/", None), + "proto-plus": ("https://proto-plus-python.readthedocs.io/en/latest/", None), + "protobuf": ("https://googleapis.dev/python/protobuf/latest/", None), +} + + +# Napoleon settings +napoleon_google_docstring = True +napoleon_numpy_docstring = True +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = True +napoleon_use_admonition_for_examples = False +napoleon_use_admonition_for_notes = False +napoleon_use_admonition_for_references = False +napoleon_use_ivar = False +napoleon_use_param = True +napoleon_use_rtype = True diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst new file mode 100644 index 00000000000..cde2a70a3fb --- /dev/null +++ b/docs/getting_started/index.rst @@ -0,0 +1,27 @@ +Getting started +=============== + +Prerequisites +-------------- + +* Install the ``bigframes`` package. +* Create a GCP project and billing account. +* When running locally, authenticate with application default credentials. See + the `gcloud auth application-default login + `_ + reference. + +Code sample +----------- + +.. literalinclude:: ../samples/snippets/quickstart.py + :language: python + :dedent: 4 + :start-after: [START bigquery_bigframes_quickstart] + :end-before: [END bigquery_bigframes_quickstart] + +Learn more +---------- + +* See :ref:`user_guide` for more samples. +* See :ref:`bigframes_ml` for more ML samples. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000000..eb721fbf293 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +.. include:: README.rst + +.. toctree:: + :maxdepth: 3 + + getting_started/index + user_guide/index + reference/index + +Changelog +--------- + +For a list of all BigQuery DataFrames releases: + +.. toctree:: + :maxdepth: 2 + + changelog diff --git a/docs/reference/bigframes.ml/README.rst b/docs/reference/bigframes.ml/README.rst new file mode 100644 index 00000000000..80a1fe97b73 --- /dev/null +++ b/docs/reference/bigframes.ml/README.rst @@ -0,0 +1,125 @@ +BigQuery DataFrames ML +====================== + +As BigQuery DataFrames implements the Pandas API over top of BigQuery, BigQuery +DataFrame ML implements the SKLearn API over top of BigQuery Machine Learning. + +Tutorial +-------- + +Start a session and initialize a dataframe for a BigQuery table + +.. code-block:: python + + import bigframes.pandas + + df = bigframes.pandas.read_gbq("bigquery-public-data.ml_datasets.penguins") + df + +Clean and prepare the data + +.. code-block:: python + + # filter down to the data we want to analyze + adelie_data = df[df.species == "Adelie Penguin (Pygoscelis adeliae)"] + + # drop the columns we don't care about + adelie_data = adelie_data.drop(columns=["species"]) + + # drop rows with nulls to get our training data + training_data = adelie_data.dropna() + + # take a peek at the training data + training_data + +.. code-block:: python + + # pick feature columns and label column + X = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']] + y = training_data[['body_mass_g']] + +Use train_test_split to create train and test datasets + +.. code-block:: python + + from bigframes.ml.model_selection import train_test_split + + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.2) + +Define the model training pipeline + +.. code-block:: python + + from bigframes.ml.linear_model import LinearRegression + from bigframes.ml.pipeline import Pipeline + from bigframes.ml.compose import ColumnTransformer + from bigframes.ml.preprocessing import StandardScaler, OneHotEncoder + + preprocessing = ColumnTransformer([ + ("onehot", OneHotEncoder(), ["island", "species", "sex"]), + ("scaler", StandardScaler(), ["culmen_depth_mm", "culmen_length_mm", "flipper_length_mm"]), + ]) + + model = LinearRegression(fit_intercept=False) + + pipeline = Pipeline([ + ('preproc', preprocessing), + ('linreg', model) + ]) + + # view the pipeline + pipeline + +Train the pipeline + +.. code-block:: python + + pipeline.fit(X_train, y_train) + +Evaluate the model's performance on the test data + +.. code-block:: python + + from bigframes.ml.metrics import r2_score + + y_pred = pipeline.predict(X_test) + + r2_score(y_test, y_pred) + +Make predictions on new data + +.. code-block:: python + + import pandas + + new_penguins = bigframes.pandas.read_pandas( + pandas.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + "sex": ["MALE", "FEMALE", "FEMALE"], + } + ).set_index("tag_number") + ) + + # view the new data + new_penguins + +.. code-block:: python + + pipeline.predict(new_penguins) + +Save the trained model to BigQuery, so we can load it later + +.. code-block:: python + + pipeline.to_gbq("bqml_tutorial.penguins_model", replace=True) diff --git a/docs/reference/bigframes.ml/cluster.rst b/docs/reference/bigframes.ml/cluster.rst new file mode 100644 index 00000000000..5456b728b67 --- /dev/null +++ b/docs/reference/bigframes.ml/cluster.rst @@ -0,0 +1,5 @@ +bigframes.ml.cluster +==================== + +.. automodule:: bigframes.ml.cluster + :members: diff --git a/docs/reference/bigframes.ml/compose.rst b/docs/reference/bigframes.ml/compose.rst new file mode 100644 index 00000000000..e41f17a33b1 --- /dev/null +++ b/docs/reference/bigframes.ml/compose.rst @@ -0,0 +1,5 @@ +bigframes.ml.compose +==================== + +.. automodule:: bigframes.ml.compose + :members: diff --git a/docs/reference/bigframes.ml/decomposition.rst b/docs/reference/bigframes.ml/decomposition.rst new file mode 100644 index 00000000000..122dbc2b168 --- /dev/null +++ b/docs/reference/bigframes.ml/decomposition.rst @@ -0,0 +1,5 @@ +bigframes.ml.decomposition +========================== + +.. automodule:: bigframes.ml.decomposition + :members: diff --git a/docs/reference/bigframes.ml/forecasting.rst b/docs/reference/bigframes.ml/forecasting.rst new file mode 100644 index 00000000000..037bd7e82ef --- /dev/null +++ b/docs/reference/bigframes.ml/forecasting.rst @@ -0,0 +1,5 @@ +bigframes.ml.forecasting +======================== + +.. automodule:: bigframes.ml.forecasting + :members: diff --git a/docs/reference/bigframes.ml/imported.rst b/docs/reference/bigframes.ml/imported.rst new file mode 100644 index 00000000000..54120534c02 --- /dev/null +++ b/docs/reference/bigframes.ml/imported.rst @@ -0,0 +1,5 @@ +bigframes.ml.imported +===================== + +.. automodule:: bigframes.ml.imported + :members: diff --git a/docs/reference/bigframes.ml/index.rst b/docs/reference/bigframes.ml/index.rst new file mode 100644 index 00000000000..e17edc2ee98 --- /dev/null +++ b/docs/reference/bigframes.ml/index.rst @@ -0,0 +1,30 @@ +.. _bigframes_ml: +.. include:: README.rst + +API Reference +------------- + +.. toctree:: + :maxdepth: 3 + + cluster + + compose + + decomposition + + forecasting + + imported + + linear_model + + llm + + metrics + + model_selection + + pipeline + + preprocessing diff --git a/docs/reference/bigframes.ml/linear_model.rst b/docs/reference/bigframes.ml/linear_model.rst new file mode 100644 index 00000000000..5936858e64b --- /dev/null +++ b/docs/reference/bigframes.ml/linear_model.rst @@ -0,0 +1,5 @@ +bigframes.ml.linear_model +========================= + +.. automodule:: bigframes.ml.linear_model + :members: diff --git a/docs/reference/bigframes.ml/llm.rst b/docs/reference/bigframes.ml/llm.rst new file mode 100644 index 00000000000..4debef74ffb --- /dev/null +++ b/docs/reference/bigframes.ml/llm.rst @@ -0,0 +1,5 @@ +bigframes.ml.llm +================ + +.. automodule:: bigframes.ml.llm + :members: diff --git a/docs/reference/bigframes.ml/metrics.rst b/docs/reference/bigframes.ml/metrics.rst new file mode 100644 index 00000000000..dc951ebed42 --- /dev/null +++ b/docs/reference/bigframes.ml/metrics.rst @@ -0,0 +1,5 @@ +bigframes.ml.metrics +==================== + +.. automodule:: bigframes.ml.metrics + :members: diff --git a/docs/reference/bigframes.ml/model_selection.rst b/docs/reference/bigframes.ml/model_selection.rst new file mode 100644 index 00000000000..b0d4e5ed3ee --- /dev/null +++ b/docs/reference/bigframes.ml/model_selection.rst @@ -0,0 +1,5 @@ +bigframes.ml.model_selection +============================ + +.. automodule:: bigframes.ml.model_selection + :members: diff --git a/docs/reference/bigframes.ml/pipeline.rst b/docs/reference/bigframes.ml/pipeline.rst new file mode 100644 index 00000000000..239ee59c431 --- /dev/null +++ b/docs/reference/bigframes.ml/pipeline.rst @@ -0,0 +1,5 @@ +bigframes.ml.pipeline +===================== + +.. automodule:: bigframes.ml.pipeline + :members: diff --git a/docs/reference/bigframes.ml/preprocessing.rst b/docs/reference/bigframes.ml/preprocessing.rst new file mode 100644 index 00000000000..4d2279fc8ea --- /dev/null +++ b/docs/reference/bigframes.ml/preprocessing.rst @@ -0,0 +1,5 @@ +bigframes.ml.preprocessing +========================== + +.. automodule:: bigframes.ml.preprocessing + :members: diff --git a/docs/reference/bigframes.pandas/frame.rst b/docs/reference/bigframes.pandas/frame.rst new file mode 100644 index 00000000000..a49bcc8f7cb --- /dev/null +++ b/docs/reference/bigframes.pandas/frame.rst @@ -0,0 +1,9 @@ + +========= +DataFrame +========= + +.. autoclass:: bigframes.dataframe.DataFrame + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.pandas/general_functions.rst b/docs/reference/bigframes.pandas/general_functions.rst new file mode 100644 index 00000000000..4fff9aabf83 --- /dev/null +++ b/docs/reference/bigframes.pandas/general_functions.rst @@ -0,0 +1,8 @@ + +================= +General functions +================= + +.. automodule:: bigframes.pandas + :members: + :undoc-members: diff --git a/docs/reference/bigframes.pandas/groupby.rst b/docs/reference/bigframes.pandas/groupby.rst new file mode 100644 index 00000000000..483340f3487 --- /dev/null +++ b/docs/reference/bigframes.pandas/groupby.rst @@ -0,0 +1,20 @@ + +======= +GroupBy +======= + +DataFrameGroupBy +---------------- + +.. autoclass:: bigframes.core.groupby.DataFrameGroupBy + :members: + :inherited-members: + :undoc-members: + +SeriesGroupBy +------------- + +.. autoclass:: bigframes.core.groupby.SeriesGroupBy + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.pandas/index.rst b/docs/reference/bigframes.pandas/index.rst new file mode 100644 index 00000000000..c7ff586884e --- /dev/null +++ b/docs/reference/bigframes.pandas/index.rst @@ -0,0 +1,15 @@ + +============================ +BigQuery DataFrames (pandas) +============================ + +.. toctree:: + :maxdepth: 2 + + general_functions + series + frame + indexing + window + groupby + options diff --git a/docs/reference/bigframes.pandas/indexing.rst b/docs/reference/bigframes.pandas/indexing.rst new file mode 100644 index 00000000000..8f7f1947401 --- /dev/null +++ b/docs/reference/bigframes.pandas/indexing.rst @@ -0,0 +1,9 @@ + +============= +Index objects +============= + +.. autoclass:: bigframes.core.indexes.index.Index + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.pandas/options.rst b/docs/reference/bigframes.pandas/options.rst new file mode 100644 index 00000000000..60af8c826a4 --- /dev/null +++ b/docs/reference/bigframes.pandas/options.rst @@ -0,0 +1,6 @@ + +==================== +Options and settings +==================== + +``bigframes.pandas.options`` is an alias for :data:`bigframes.options`. diff --git a/docs/reference/bigframes.pandas/series.rst b/docs/reference/bigframes.pandas/series.rst new file mode 100644 index 00000000000..b179da9ca13 --- /dev/null +++ b/docs/reference/bigframes.pandas/series.rst @@ -0,0 +1,36 @@ + +====== +Series +====== + +.. contents:: Table of Contents + :depth: 2 + :local: + :backlinks: none + +Series +------ + +.. autoclass:: bigframes.series.Series + :members: + :inherited-members: + :undoc-members: + +Accessors +--------- + +Datetime properties +^^^^^^^^^^^^^^^^^^^ + +.. automodule:: bigframes.operations.datetimes + :members: + :inherited-members: + :undoc-members: + +String handling +^^^^^^^^^^^^^^^ + +.. automodule:: bigframes.operations.strings + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.pandas/window.rst b/docs/reference/bigframes.pandas/window.rst new file mode 100644 index 00000000000..55d911ecf4f --- /dev/null +++ b/docs/reference/bigframes.pandas/window.rst @@ -0,0 +1,9 @@ + +====== +Window +====== + +.. autoclass:: bigframes.core.window.Window + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes/index.rst b/docs/reference/bigframes/index.rst new file mode 100644 index 00000000000..76d64444faf --- /dev/null +++ b/docs/reference/bigframes/index.rst @@ -0,0 +1,19 @@ + +Core objects +============ + +.. toctree:: + :maxdepth: 2 + + options + + +Session +------- + +.. autofunction:: bigframes.connect + +.. autoclass:: bigframes.session.Session + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes/options.rst b/docs/reference/bigframes/options.rst new file mode 100644 index 00000000000..914e4976d54 --- /dev/null +++ b/docs/reference/bigframes/options.rst @@ -0,0 +1,12 @@ +Options and settings +==================== + +.. currentmodule:: bigframes + +.. autodata:: options + +.. autoclass:: bigframes._config.Options + +.. autoclass:: bigframes._config.bigquery_options.BigQueryOptions + +.. autoclass:: bigframes._config.display_options.DisplayOptions diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 00000000000..c790831db18 --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,12 @@ +API Reference +============= + +Refer to these pages for details about the public objects in the ``bigframes`` +packages. + +.. toctree:: + :maxdepth: 2 + + bigframes/index + bigframes.pandas/index + bigframes.ml/index diff --git a/docs/samples b/docs/samples new file mode 120000 index 00000000000..e804737ed3a --- /dev/null +++ b/docs/samples @@ -0,0 +1 @@ +../samples \ No newline at end of file diff --git a/docs/user_guide/bigframes.pandas/remote_functions.rst b/docs/user_guide/bigframes.pandas/remote_functions.rst new file mode 100644 index 00000000000..3a1a9d342f0 --- /dev/null +++ b/docs/user_guide/bigframes.pandas/remote_functions.rst @@ -0,0 +1,40 @@ + +Using the Remote Functions +========================== + +BigQuery DataFrames gives you the ability to turn your custom scalar functions +into a BigQuery remote function. It requires the GCP project to be set up +appropriately and the user having sufficient privileges to use them. One can +find more details on it via `help` command. + +.. code-block:: python + + import bigframes.pandas as pd + help(pd.remote_function) + +Read a table and inspect the column of interest. + +.. code-block:: python + + df = pd.read_gbq("bigquery-public-data.ml_datasets.penguins") + df["body_mass_g"].head(10) + +Define a custom function, and specify the intent to turn it into a remote +function. It requires a BigQuery connection. If the connection is not already +created, BigQuery DataFrames will attempt to create one assuming the necessary +APIs and IAM permissions are setup in the project. + +.. code-block:: python + + @pd.remote_function([float], str, bigquery_connection='bigframes-rf-conn') + def get_bucket(num): + if not num: return "NA" + boundary = 4000 + return "at_or_above_4000" if num >= boundary else "below_4000" + +Run the custom function on the column of interest to create a new column. + +.. code-block:: python + + df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket)) + df[['body_mass_g', 'body_mass_bucket']].head(10) diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst new file mode 100644 index 00000000000..915e172159c --- /dev/null +++ b/docs/user_guide/index.rst @@ -0,0 +1,9 @@ +.. _user_guide: + +User Guide +========== + +.. toctree:: + :maxdepth: 2 + + bigframes.pandas/remote_functions diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000000..ce78c4686ed --- /dev/null +++ b/mypy.ini @@ -0,0 +1,25 @@ +# https://mypy.readthedocs.io/en/stable/config_file.html#config-file + +[mypy] +exclude = ^third_party/ + +[mypy-google.auth.*] +ignore_missing_imports = True + +[mypy-cloudpickle.*] +ignore_missing_imports = True + +[mypy-pydata_google_auth] +ignore_missing_imports = True + +[mypy-google.colab] +ignore_missing_imports = True + +[mypy-pytz] +ignore_missing_imports = True + +[mypy-pyarrow] +ignore_missing_imports = True + +[mypy-ipywidgets] +ignore_missing_imports = True diff --git a/notebooks/00 - Summary.ipynb b/notebooks/00 - Summary.ipynb new file mode 100644 index 00000000000..66ac9a8de84 --- /dev/null +++ b/notebooks/00 - Summary.ipynb @@ -0,0 +1,2060 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using the BigQuery DataFrames API" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas\n", + "\n", + "bigframes.pandas.options.bigquery.project = \"bigframes-dev\"\n", + "bigframes.pandas.options.bigquery.location = \"us\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialize a dataframe for a BigQuery table" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "df = bigframes.pandas.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## View the DataFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Adelie Penguin (Pygoscelis adeliae)Dream36.618.4184.03475.0FEMALE
1Adelie Penguin (Pygoscelis adeliae)Dream39.819.1184.04650.0MALE
2Adelie Penguin (Pygoscelis adeliae)Dream40.918.9184.03900.0MALE
3Chinstrap penguin (Pygoscelis antarctica)Dream46.517.9192.03500.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Dream37.316.8192.03000.0FEMALE
5Adelie Penguin (Pygoscelis adeliae)Dream43.218.5192.04100.0MALE
6Chinstrap penguin (Pygoscelis antarctica)Dream46.916.6192.02700.0FEMALE
7Chinstrap penguin (Pygoscelis antarctica)Dream50.518.4200.03400.0FEMALE
8Chinstrap penguin (Pygoscelis antarctica)Dream49.519.0200.03800.0MALE
9Adelie Penguin (Pygoscelis adeliae)Dream40.220.1200.03975.0MALE
10Adelie Penguin (Pygoscelis adeliae)Dream40.818.9208.04300.0MALE
11Adelie Penguin (Pygoscelis adeliae)Dream39.018.7185.03650.0MALE
12Adelie Penguin (Pygoscelis adeliae)Dream37.016.9185.03000.0FEMALE
13Chinstrap penguin (Pygoscelis antarctica)Dream47.017.3185.03700.0FEMALE
14Adelie Penguin (Pygoscelis adeliae)Dream34.017.1185.03400.0FEMALE
15Adelie Penguin (Pygoscelis adeliae)Dream37.016.5185.03400.0FEMALE
16Chinstrap penguin (Pygoscelis antarctica)Dream45.717.3193.03600.0FEMALE
17Chinstrap penguin (Pygoscelis antarctica)Dream50.619.4193.03800.0MALE
18Adelie Penguin (Pygoscelis adeliae)Dream39.717.9193.04250.0MALE
19Adelie Penguin (Pygoscelis adeliae)Dream37.818.1193.03750.0MALE
\n", + "
[344 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", + "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", + "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", + "5 Adelie Penguin (Pygoscelis adeliae) Dream 43.2 \n", + "6 Chinstrap penguin (Pygoscelis antarctica) Dream 46.9 \n", + "7 Chinstrap penguin (Pygoscelis antarctica) Dream 50.5 \n", + "8 Chinstrap penguin (Pygoscelis antarctica) Dream 49.5 \n", + "9 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", + "10 Adelie Penguin (Pygoscelis adeliae) Dream 40.8 \n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream 39.0 \n", + "12 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", + "13 Chinstrap penguin (Pygoscelis antarctica) Dream 47.0 \n", + "14 Adelie Penguin (Pygoscelis adeliae) Dream 34.0 \n", + "15 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", + "16 Chinstrap penguin (Pygoscelis antarctica) Dream 45.7 \n", + "17 Chinstrap penguin (Pygoscelis antarctica) Dream 50.6 \n", + "18 Adelie Penguin (Pygoscelis adeliae) Dream 39.7 \n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.8 \n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream 46.6 \n", + "21 Chinstrap penguin (Pygoscelis antarctica) Dream 51.3 \n", + "22 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream 36.8 \n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream 49.6 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 18.4 184.0 3475.0 FEMALE \n", + "1 19.1 184.0 4650.0 MALE \n", + "2 18.9 184.0 3900.0 MALE \n", + "3 17.9 192.0 3500.0 FEMALE \n", + "4 16.8 192.0 3000.0 FEMALE \n", + "5 18.5 192.0 4100.0 MALE \n", + "6 16.6 192.0 2700.0 FEMALE \n", + "7 18.4 200.0 3400.0 FEMALE \n", + "8 19.0 200.0 3800.0 MALE \n", + "9 20.1 200.0 3975.0 MALE \n", + "10 18.9 208.0 4300.0 MALE \n", + "11 18.7 185.0 3650.0 MALE \n", + "12 16.9 185.0 3000.0 FEMALE \n", + "13 17.3 185.0 3700.0 FEMALE \n", + "14 17.1 185.0 3400.0 FEMALE \n", + "15 16.5 185.0 3400.0 FEMALE \n", + "16 17.3 193.0 3600.0 FEMALE \n", + "17 19.4 193.0 3800.0 MALE \n", + "18 17.9 193.0 4250.0 MALE \n", + "19 18.1 193.0 3750.0 MALE \n", + "20 17.8 193.0 3800.0 FEMALE \n", + "21 19.2 193.0 3650.0 MALE \n", + "22 17.1 193.0 3400.0 FEMALE \n", + "23 18.5 193.0 3500.0 FEMALE \n", + "24 18.2 193.0 3775.0 MALE \n", + "...\n", + "\n", + "[344 rows x 7 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### View the column names in the dataframe (aka columns names in the table)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['species', 'island', 'culmen_length_mm', 'culmen_depth_mm',\n", + " 'flipper_length_mm', 'body_mass_g', 'sex'],\n", + " dtype='object')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.columns" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### View the table schema" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "species string[pyarrow]\n", + "island string[pyarrow]\n", + "culmen_length_mm Float64\n", + "culmen_depth_mm Float64\n", + "flipper_length_mm Float64\n", + "body_mass_g Float64\n", + "sex string[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Select a subset of columns" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandbody_mass_g
0Adelie Penguin (Pygoscelis adeliae)Dream3475.0
1Adelie Penguin (Pygoscelis adeliae)Dream4650.0
2Adelie Penguin (Pygoscelis adeliae)Dream3900.0
3Chinstrap penguin (Pygoscelis antarctica)Dream3500.0
4Adelie Penguin (Pygoscelis adeliae)Dream3000.0
5Adelie Penguin (Pygoscelis adeliae)Dream4100.0
6Chinstrap penguin (Pygoscelis antarctica)Dream2700.0
7Chinstrap penguin (Pygoscelis antarctica)Dream3400.0
8Chinstrap penguin (Pygoscelis antarctica)Dream3800.0
9Adelie Penguin (Pygoscelis adeliae)Dream3975.0
10Adelie Penguin (Pygoscelis adeliae)Dream4300.0
11Adelie Penguin (Pygoscelis adeliae)Dream3650.0
12Adelie Penguin (Pygoscelis adeliae)Dream3000.0
13Chinstrap penguin (Pygoscelis antarctica)Dream3700.0
14Adelie Penguin (Pygoscelis adeliae)Dream3400.0
15Adelie Penguin (Pygoscelis adeliae)Dream3400.0
16Chinstrap penguin (Pygoscelis antarctica)Dream3600.0
17Chinstrap penguin (Pygoscelis antarctica)Dream3800.0
18Adelie Penguin (Pygoscelis adeliae)Dream4250.0
19Adelie Penguin (Pygoscelis adeliae)Dream3750.0
\n", + "
[344 rows x 3 columns in total]" + ], + "text/plain": [ + " species island body_mass_g\n", + "0 Adelie Penguin (Pygoscelis adeliae) Dream 3475.0\n", + "1 Adelie Penguin (Pygoscelis adeliae) Dream 4650.0\n", + "2 Adelie Penguin (Pygoscelis adeliae) Dream 3900.0\n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream 3500.0\n", + "4 Adelie Penguin (Pygoscelis adeliae) Dream 3000.0\n", + "5 Adelie Penguin (Pygoscelis adeliae) Dream 4100.0\n", + "6 Chinstrap penguin (Pygoscelis antarctica) Dream 2700.0\n", + "7 Chinstrap penguin (Pygoscelis antarctica) Dream 3400.0\n", + "8 Chinstrap penguin (Pygoscelis antarctica) Dream 3800.0\n", + "9 Adelie Penguin (Pygoscelis adeliae) Dream 3975.0\n", + "10 Adelie Penguin (Pygoscelis adeliae) Dream 4300.0\n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream 3650.0\n", + "12 Adelie Penguin (Pygoscelis adeliae) Dream 3000.0\n", + "13 Chinstrap penguin (Pygoscelis antarctica) Dream 3700.0\n", + "14 Adelie Penguin (Pygoscelis adeliae) Dream 3400.0\n", + "15 Adelie Penguin (Pygoscelis adeliae) Dream 3400.0\n", + "16 Chinstrap penguin (Pygoscelis antarctica) Dream 3600.0\n", + "17 Chinstrap penguin (Pygoscelis antarctica) Dream 3800.0\n", + "18 Adelie Penguin (Pygoscelis adeliae) Dream 4250.0\n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream 3750.0\n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream 3800.0\n", + "21 Chinstrap penguin (Pygoscelis antarctica) Dream 3650.0\n", + "22 Adelie Penguin (Pygoscelis adeliae) Dream 3400.0\n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream 3500.0\n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream 3775.0\n", + "...\n", + "\n", + "[344 rows x 3 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df[[\n", + " \"species\",\n", + " \"island\",\n", + " \"body_mass_g\",\n", + "]]\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### View the first ten values of a series" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 3475.0\n", + "1 4650.0\n", + "2 3900.0\n", + "3 3500.0\n", + "4 3000.0\n", + "5 4100.0\n", + "6 2700.0\n", + "7 3400.0\n", + "8 3800.0\n", + "9 3975.0\n", + "Name: body_mass_g, dtype: Float64" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['body_mass_g'].head(10)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compute the mean of a series" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4201.7543859649095" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['body_mass_g'].mean()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filter the DataFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandbody_mass_g
1Adelie Penguin (Pygoscelis adeliae)Dream4650.0
5Adelie Penguin (Pygoscelis adeliae)Dream4100.0
10Adelie Penguin (Pygoscelis adeliae)Dream4300.0
18Adelie Penguin (Pygoscelis adeliae)Dream4250.0
25Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
26Adelie Penguin (Pygoscelis adeliae)Dream4000.0
27Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
28Chinstrap penguin (Pygoscelis antarctica)Dream4300.0
30Chinstrap penguin (Pygoscelis antarctica)Dream4450.0
36Adelie Penguin (Pygoscelis adeliae)Dream4450.0
44Chinstrap penguin (Pygoscelis antarctica)Dream4100.0
45Chinstrap penguin (Pygoscelis antarctica)Dream4800.0
57Chinstrap penguin (Pygoscelis antarctica)Dream4400.0
61Chinstrap penguin (Pygoscelis antarctica)Dream4150.0
66Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
67Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
68Chinstrap penguin (Pygoscelis antarctica)Dream4100.0
74Adelie Penguin (Pygoscelis adeliae)Dream4350.0
77Adelie Penguin (Pygoscelis adeliae)Dream4150.0
78Adelie Penguin (Pygoscelis adeliae)Dream4400.0
\n", + "
[177 rows x 3 columns in total]" + ], + "text/plain": [ + " species island body_mass_g\n", + "1 Adelie Penguin (Pygoscelis adeliae) Dream 4650.0\n", + "5 Adelie Penguin (Pygoscelis adeliae) Dream 4100.0\n", + "10 Adelie Penguin (Pygoscelis adeliae) Dream 4300.0\n", + "18 Adelie Penguin (Pygoscelis adeliae) Dream 4250.0\n", + "25 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", + "26 Adelie Penguin (Pygoscelis adeliae) Dream 4000.0\n", + "27 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", + "28 Chinstrap penguin (Pygoscelis antarctica) Dream 4300.0\n", + "30 Chinstrap penguin (Pygoscelis antarctica) Dream 4450.0\n", + "36 Adelie Penguin (Pygoscelis adeliae) Dream 4450.0\n", + "44 Chinstrap penguin (Pygoscelis antarctica) Dream 4100.0\n", + "45 Chinstrap penguin (Pygoscelis antarctica) Dream 4800.0\n", + "57 Chinstrap penguin (Pygoscelis antarctica) Dream 4400.0\n", + "61 Chinstrap penguin (Pygoscelis antarctica) Dream 4150.0\n", + "66 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", + "67 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", + "68 Chinstrap penguin (Pygoscelis antarctica) Dream 4100.0\n", + "74 Adelie Penguin (Pygoscelis adeliae) Dream 4350.0\n", + "77 Adelie Penguin (Pygoscelis adeliae) Dream 4150.0\n", + "78 Adelie Penguin (Pygoscelis adeliae) Dream 4400.0\n", + "80 Chinstrap penguin (Pygoscelis antarctica) Dream 4300.0\n", + "90 Chinstrap penguin (Pygoscelis antarctica) Dream 4150.0\n", + "92 Chinstrap penguin (Pygoscelis antarctica) Dream 4500.0\n", + "93 Adelie Penguin (Pygoscelis adeliae) Dream 4300.0\n", + "94 Chinstrap penguin (Pygoscelis antarctica) Dream 4550.0\n", + "...\n", + "\n", + "[177 rows x 3 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[df['body_mass_g'] >= 4000.0]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using the Remote Functions" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### BigQuery DataFrames gives you the ability to turn your custom scalar functions into a BigQuery remote function.\n", + "\n", + "It requires the GCP project to be set up appropriately and the user having sufficient privileges to use them. One can find more details on it via `help` command." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function remote_function in module bigframes.pandas:\n", + "\n", + "remote_function(input_types: 'List[type]', output_type: 'type', dataset: 'Optional[str]' = None, bigquery_connection: 'Optional[str]' = None, reuse: 'bool' = True)\n", + " Decorator to turn a user defined function into a BigQuery remote function.\n", + " \n", + " Args:\n", + " input_types (list(type)):\n", + " List of input data types in the user defined function.\n", + " output_type (type):\n", + " Data type of the output in the user defined function.\n", + " dataset (str, Optional):\n", + " Dataset to use to create a BigQuery function. It should be in\n", + " `.` or `` format. If this\n", + " param is not provided then session dataset id would be used.\n", + " bigquery_connection (str, Optional):\n", + " Name of the BigQuery connection. If it is pre created in the same\n", + " location as the `bigquery_client.location` then it would be used,\n", + " otherwise it would be created dynamically assuming the user has\n", + " necessary priviliges. If this param is not provided then the\n", + " bigquery connection from the session would be used.\n", + " reuse (bool, Optional):\n", + " Reuse the remote function if already exists.\n", + " `True` by default, which will result in reusing an existing remote\n", + " function (if any) that was previously created for the same udf.\n", + " Setting it to false would force creating a unique remote function.\n", + " If the required remote function does not exist then it would be\n", + " created irrespective of this param.\n", + " \n", + " Notes:\n", + " Please make sure following is setup before using this API:\n", + " \n", + " 1. Have the below APIs enabled for your project:\n", + " a. BigQuery Connection API\n", + " b. Cloud Functions API\n", + " c. Cloud Run API\n", + " d. Cloud Build API\n", + " e. Artifact Registry API\n", + " f. Cloud Resource Manager API\n", + " \n", + " This can be done from the cloud console (change PROJECT_ID to yours):\n", + " https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID\n", + " Or from the gcloud CLI:\n", + " $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com\n", + " \n", + " 2. Have following IAM roles enabled for you:\n", + " a. BigQuery Data Editor (roles/bigquery.dataEditor)\n", + " b. BigQuery Connection Admin (roles/bigquery.connectionAdmin)\n", + " c. Cloud Functions Developer (roles/cloudfunctions.developer)\n", + " d. Service Account User (roles/iam.serviceAccountUser)\n", + " e. Storage Object Viewer (roles/storage.objectViewer)\n", + " f. Project IAM Admin (roles/resourcemanager.projectIamAdmin)\n", + " (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.)\n", + " \n", + " 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set:\n", + " a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection\n", + " b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function\n", + " Alternatively, the IAM could also be setup via the gcloud CLI:\n", + " $ gcloud projects add-iam-policy-binding PROJECT_ID --member=\"serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID\" --role=\"roles/run.invoker\"\n", + "\n" + ] + } + ], + "source": [ + "import bigframes.pandas as pd\n", + "help(pd.remote_function)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define a custom function, and specify the intent to turn it into a remote function.\n", + "\n", + "It requires a BigQuery connection. If the connection is not already created,\n", + "the BigQuery DataFrames package attempts to create one assuming the necessary\n", + "APIs and IAM permissions are setup in the project." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO][2023-06-28 23:31:49,355][bigframes.remote_function] Creating new cloud function: gcloud functions deploy bigframes-b3fab64f5997ad6a516379defe8d4202 --gen2 --runtime=python310 --project=bigframes-dev --region=us-central1 --source=/tmp/tmp9w5e89lh --entry-point=udf_http --trigger-http --no-allow-unauthenticated\n", + "Preparing function...\n", + ".done.\n", + "Deploying function...\n", + "[Build]..........................................................................................................................................................................................................................................................................................................................................................................................................................done\n", + "[Service].........................................................................................................................................................................................................done\n", + "Done.\n", + "You can view your function in the Cloud Console here: https://console.cloud.google.com/functions/details/us-central1/bigframes-b3fab64f5997ad6a516379defe8d4202?project=bigframes-dev\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "buildConfig:\n", + " build: projects/1084210331973/locations/us-central1/builds/780b1780-9b38-4515-ae60-89d05454ef83\n", + " entryPoint: udf_http\n", + " runtime: python310\n", + " source:\n", + " storageSource:\n", + " bucket: gcf-v2-sources-1084210331973-us-central1\n", + " object: bigframes-b3fab64f5997ad6a516379defe8d4202/function-source.zip\n", + " sourceProvenance:\n", + " resolvedStorageSource:\n", + " bucket: gcf-v2-sources-1084210331973-us-central1\n", + " generation: '1687995112300727'\n", + " object: bigframes-b3fab64f5997ad6a516379defe8d4202/function-source.zip\n", + "environment: GEN_2\n", + "labels:\n", + " deployment-tool: cli-gcloud\n", + "name: projects/bigframes-dev/locations/us-central1/functions/bigframes-b3fab64f5997ad6a516379defe8d4202\n", + "serviceConfig:\n", + " allTrafficOnLatestRevision: true\n", + " availableCpu: '0.1666'\n", + " availableMemory: 256M\n", + " ingressSettings: ALLOW_ALL\n", + " maxInstanceCount: 100\n", + " maxInstanceRequestConcurrency: 1\n", + " revision: bigframes-b3fab64f5997ad6a516379defe8d4202-00001-tut\n", + " service: projects/bigframes-dev/locations/us-central1/services/bigframes-b3fab64f5997ad6a516379defe8d4202\n", + " serviceAccountEmail: 1084210331973-compute@developer.gserviceaccount.com\n", + " timeoutSeconds: 60\n", + " uri: https://bigframes-b3fab64f5997ad6a516379defe8d4202-7krlje3eoq-uc.a.run.app\n", + "state: ACTIVE\n", + "updateTime: '2023-06-28T23:32:51.911131997Z'\n", + "url: https://us-central1-bigframes-dev.cloudfunctions.net/bigframes-b3fab64f5997ad6a516379defe8d4202\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO][2023-06-28 23:32:55,330][bigframes.remote_function] Successfully created cloud function bigframes-b3fab64f5997ad6a516379defe8d4202 with uri (https://bigframes-b3fab64f5997ad6a516379defe8d4202-7krlje3eoq-uc.a.run.app)\n", + "[INFO][2023-06-28 23:32:59,378][bigframes.remote_function] Connector bigframes-rf-conn already exists\n", + "[INFO][2023-06-28 23:32:59,379][bigframes.remote_function] Creating BQ remote function: \n", + " CREATE OR REPLACE FUNCTION `bigframes-dev.bigframes_temp_us`.bigframes_b3fab64f5997ad6a516379defe8d4202(num FLOAT64)\n", + " RETURNS STRING\n", + " REMOTE WITH CONNECTION `bigframes-dev.us.bigframes-rf-conn`\n", + " OPTIONS (\n", + " endpoint = \"https://bigframes-b3fab64f5997ad6a516379defe8d4202-7krlje3eoq-uc.a.run.app\"\n", + " )\n", + "[INFO][2023-06-28 23:33:00,338][bigframes.remote_function] Created remote function bigframes-dev.bigframes_temp_us.bigframes_b3fab64f5997ad6a516379defe8d4202\n" + ] + } + ], + "source": [ + "@pd.remote_function([float], str, bigquery_connection='bigframes-rf-conn')\n", + "def get_bucket(num):\n", + " if not num: return \"NA\"\n", + " boundary = 4000\n", + " return \"at_or_above_4000\" if num >= boundary else \"below_4000\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the custom function on the BigQuery-backed dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
body_mass_gbody_mass_bucket
03475.0below_4000
14650.0at_or_above_4000
23900.0below_4000
33500.0below_4000
43000.0below_4000
54100.0at_or_above_4000
62700.0below_4000
73400.0below_4000
83800.0below_4000
93975.0below_4000
\n", + "
[10 rows x 2 columns in total]" + ], + "text/plain": [ + " body_mass_g body_mass_bucket\n", + "0 3475.0 below_4000\n", + "1 4650.0 at_or_above_4000\n", + "2 3900.0 below_4000\n", + "3 3500.0 below_4000\n", + "4 3000.0 below_4000\n", + "5 4100.0 at_or_above_4000\n", + "6 2700.0 below_4000\n", + "7 3400.0 below_4000\n", + "8 3800.0 below_4000\n", + "9 3975.0 below_4000\n", + "\n", + "[10 rows x 2 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket))\n", + "df[['body_mass_g', 'body_mass_bucket']].head(10)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using the ML API" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialize a DataFrame from a BigQuery table" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Adelie Penguin (Pygoscelis adeliae)Dream36.618.4184.03475.0FEMALE
1Adelie Penguin (Pygoscelis adeliae)Dream39.819.1184.04650.0MALE
2Adelie Penguin (Pygoscelis adeliae)Dream40.918.9184.03900.0MALE
3Chinstrap penguin (Pygoscelis antarctica)Dream46.517.9192.03500.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Dream37.316.8192.03000.0FEMALE
5Adelie Penguin (Pygoscelis adeliae)Dream43.218.5192.04100.0MALE
6Chinstrap penguin (Pygoscelis antarctica)Dream46.916.6192.02700.0FEMALE
7Chinstrap penguin (Pygoscelis antarctica)Dream50.518.4200.03400.0FEMALE
8Chinstrap penguin (Pygoscelis antarctica)Dream49.519.0200.03800.0MALE
9Adelie Penguin (Pygoscelis adeliae)Dream40.220.1200.03975.0MALE
10Adelie Penguin (Pygoscelis adeliae)Dream40.818.9208.04300.0MALE
11Adelie Penguin (Pygoscelis adeliae)Dream39.018.7185.03650.0MALE
12Adelie Penguin (Pygoscelis adeliae)Dream37.016.9185.03000.0FEMALE
13Chinstrap penguin (Pygoscelis antarctica)Dream47.017.3185.03700.0FEMALE
14Adelie Penguin (Pygoscelis adeliae)Dream34.017.1185.03400.0FEMALE
15Adelie Penguin (Pygoscelis adeliae)Dream37.016.5185.03400.0FEMALE
16Chinstrap penguin (Pygoscelis antarctica)Dream45.717.3193.03600.0FEMALE
17Chinstrap penguin (Pygoscelis antarctica)Dream50.619.4193.03800.0MALE
18Adelie Penguin (Pygoscelis adeliae)Dream39.717.9193.04250.0MALE
19Adelie Penguin (Pygoscelis adeliae)Dream37.818.1193.03750.0MALE
\n", + "
[344 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", + "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", + "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", + "5 Adelie Penguin (Pygoscelis adeliae) Dream 43.2 \n", + "6 Chinstrap penguin (Pygoscelis antarctica) Dream 46.9 \n", + "7 Chinstrap penguin (Pygoscelis antarctica) Dream 50.5 \n", + "8 Chinstrap penguin (Pygoscelis antarctica) Dream 49.5 \n", + "9 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", + "10 Adelie Penguin (Pygoscelis adeliae) Dream 40.8 \n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream 39.0 \n", + "12 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", + "13 Chinstrap penguin (Pygoscelis antarctica) Dream 47.0 \n", + "14 Adelie Penguin (Pygoscelis adeliae) Dream 34.0 \n", + "15 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", + "16 Chinstrap penguin (Pygoscelis antarctica) Dream 45.7 \n", + "17 Chinstrap penguin (Pygoscelis antarctica) Dream 50.6 \n", + "18 Adelie Penguin (Pygoscelis adeliae) Dream 39.7 \n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.8 \n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream 46.6 \n", + "21 Chinstrap penguin (Pygoscelis antarctica) Dream 51.3 \n", + "22 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream 36.8 \n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream 49.6 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 18.4 184.0 3475.0 FEMALE \n", + "1 19.1 184.0 4650.0 MALE \n", + "2 18.9 184.0 3900.0 MALE \n", + "3 17.9 192.0 3500.0 FEMALE \n", + "4 16.8 192.0 3000.0 FEMALE \n", + "5 18.5 192.0 4100.0 MALE \n", + "6 16.6 192.0 2700.0 FEMALE \n", + "7 18.4 200.0 3400.0 FEMALE \n", + "8 19.0 200.0 3800.0 MALE \n", + "9 20.1 200.0 3975.0 MALE \n", + "10 18.9 208.0 4300.0 MALE \n", + "11 18.7 185.0 3650.0 MALE \n", + "12 16.9 185.0 3000.0 FEMALE \n", + "13 17.3 185.0 3700.0 FEMALE \n", + "14 17.1 185.0 3400.0 FEMALE \n", + "15 16.5 185.0 3400.0 FEMALE \n", + "16 17.3 193.0 3600.0 FEMALE \n", + "17 19.4 193.0 3800.0 MALE \n", + "18 17.9 193.0 4250.0 MALE \n", + "19 18.1 193.0 3750.0 MALE \n", + "20 17.8 193.0 3800.0 FEMALE \n", + "21 19.2 193.0 3650.0 MALE \n", + "22 17.1 193.0 3400.0 FEMALE \n", + "23 18.5 193.0 3500.0 FEMALE \n", + "24 18.2 193.0 3775.0 MALE \n", + "...\n", + "\n", + "[344 rows x 7 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bigframes.pandas.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Clean and prepare the data" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Dream36.618.4184.03475.0FEMALE
1Dream39.819.1184.04650.0MALE
2Dream40.918.9184.03900.0MALE
4Dream37.316.8192.03000.0FEMALE
5Dream43.218.5192.04100.0MALE
9Dream40.220.1200.03975.0MALE
10Dream40.818.9208.04300.0MALE
11Dream39.018.7185.03650.0MALE
12Dream37.016.9185.03000.0FEMALE
14Dream34.017.1185.03400.0FEMALE
15Dream37.016.5185.03400.0FEMALE
18Dream39.717.9193.04250.0MALE
19Dream37.818.1193.03750.0MALE
22Dream40.217.1193.03400.0FEMALE
23Dream36.818.5193.03500.0FEMALE
26Dream41.518.5201.04000.0MALE
31Dream33.116.1178.02900.0FEMALE
32Dream37.218.1178.03900.0MALE
33Dream39.516.7178.03250.0FEMALE
35Dream36.018.5186.03100.0FEMALE
\n", + "
[146 rows x 6 columns in total]" + ], + "text/plain": [ + " island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", + "0 Dream 36.6 18.4 184.0 3475.0 \n", + "1 Dream 39.8 19.1 184.0 4650.0 \n", + "2 Dream 40.9 18.9 184.0 3900.0 \n", + "4 Dream 37.3 16.8 192.0 3000.0 \n", + "5 Dream 43.2 18.5 192.0 4100.0 \n", + "9 Dream 40.2 20.1 200.0 3975.0 \n", + "10 Dream 40.8 18.9 208.0 4300.0 \n", + "11 Dream 39.0 18.7 185.0 3650.0 \n", + "12 Dream 37.0 16.9 185.0 3000.0 \n", + "14 Dream 34.0 17.1 185.0 3400.0 \n", + "15 Dream 37.0 16.5 185.0 3400.0 \n", + "18 Dream 39.7 17.9 193.0 4250.0 \n", + "19 Dream 37.8 18.1 193.0 3750.0 \n", + "22 Dream 40.2 17.1 193.0 3400.0 \n", + "23 Dream 36.8 18.5 193.0 3500.0 \n", + "26 Dream 41.5 18.5 201.0 4000.0 \n", + "31 Dream 33.1 16.1 178.0 2900.0 \n", + "32 Dream 37.2 18.1 178.0 3900.0 \n", + "33 Dream 39.5 16.7 178.0 3250.0 \n", + "35 Dream 36.0 18.5 186.0 3100.0 \n", + "36 Dream 39.6 18.1 186.0 4450.0 \n", + "38 Dream 41.3 20.3 194.0 3550.0 \n", + "41 Dream 35.7 18.0 202.0 3550.0 \n", + "51 Dream 38.1 17.6 187.0 3425.0 \n", + "53 Dream 36.0 17.1 187.0 3700.0 \n", + "\n", + " sex \n", + "0 FEMALE \n", + "1 MALE \n", + "2 MALE \n", + "4 FEMALE \n", + "5 MALE \n", + "9 MALE \n", + "10 MALE \n", + "11 MALE \n", + "12 FEMALE \n", + "14 FEMALE \n", + "15 FEMALE \n", + "18 MALE \n", + "19 MALE \n", + "22 FEMALE \n", + "23 FEMALE \n", + "26 MALE \n", + "31 FEMALE \n", + "32 MALE \n", + "33 FEMALE \n", + "35 FEMALE \n", + "36 MALE \n", + "38 MALE \n", + "41 FEMALE \n", + "51 FEMALE \n", + "53 FEMALE \n", + "...\n", + "\n", + "[146 rows x 6 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# filter down to the data we want to analyze\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# drop the columns we don't care about\n", + "adelie_data = adelie_data.drop(columns=[\"species\"])\n", + "\n", + "# drop rows with nulls to get our training data\n", + "training_data = adelie_data.dropna()\n", + "\n", + "# take a peek at the training data\n", + "training_data" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# pick feature columns and label column\n", + "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "label_columns = training_data[['body_mass_g']]\n", + "\n", + "# also get the rows that we want to make predictions for (i.e. where the feature column is null)\n", + "missing_body_mass = adelie_data[adelie_data.body_mass_g.isnull()]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Train and evaluate a linear regression model using the ML API" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", + "
[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 223.878763 78553.601634 0.005614 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 181.330911 0.623951 0.623951 \n", + "\n", + "[1 rows x 6 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "# as in scikit-learn, a newly created model is just a bundle of parameters\n", + "# default parameters are fine here\n", + "model = LinearRegression()\n", + "\n", + "# this will train a temporary model in BigQuery Machine Learning\n", + "model.fit(feature_columns, label_columns)\n", + "\n", + "# check how the model performed\n", + "model.score(feature_columns, label_columns)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Make predictions using the model" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_g
2923603.735118
\n", + "
[1 rows x 1 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g\n", + "292 3603.735118\n", + "\n", + "[1 rows x 1 columns]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.predict(missing_body_mass)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save the trained model to BigQuery, so we can load it later" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.to_gbq(\"bqml_tutorial.penguins_model\", replace=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/01 - Getting Started.ipynb b/notebooks/01 - Getting Started.ipynb new file mode 100644 index 00000000000..132cdf8db0d --- /dev/null +++ b/notebooks/01 - Getting Started.ipynb @@ -0,0 +1,965 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "d7a03de2-c0ef-4f80-9cd5-f96e87cf2d54", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# On the instance where you are running jupyter,\n", + "# authenticate with gcloud first:\n", + "#\n", + "# gcloud auth application-default login\n", + "\n", + "import bigframes.pandas as bpd\n", + "\n", + "# Change this location to the location of your datasets.\n", + "# We use \"us\" as that is the location of the sample data.\n", + "bpd.options.bigquery.location = \"us\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "941cb6c3-8c54-42ce-a945-4fa604176b2e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = bpd.read_gbq(\n", + " \"bigquery-public-data.chicago_taxi_trips.taxi_trips\",\n", + " # For large tables, a unique index or multiindex is highly recommended.\n", + " # This prevents BigQuery DataFrames from creating a default sequential index.\n", + " index_col=[\"trip_start_timestamp\", \"unique_key\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4c3686d1-8b12-4c6b-8b19-6ef40a3b8ef5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "bigframes.dataframe.DataFrame" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "586da7b2-8825-4b1e-87cd-512df60775e1", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
taxi_idtrip_end_timestamptrip_secondstrip_milespickup_census_tractdropoff_census_tractpickup_community_areadropoff_community_areafaretips...extrastrip_totalpayment_typecompanypickup_latitudepickup_longitudepickup_locationdropoff_latitudedropoff_longitudedropoff_location
trip_start_timestampunique_key
2013-01-01 00:00:00+00:00006d53094ff5b3c81140c00aa91fdb5467db08024d924c1ad8126ad7efcbe49964b63ce4e62e11223b0f41...2013-01-01 00:15:00+00:003602.3170313301001703132010033327.850.0...2.09.85Cash<NA>41.85935-87.617358POINT (-87.6173580061 41.859349715)41.884987-87.620993POINT (-87.6209929134 41.8849871918)
\n", + "

1 rows × 21 columns

\n", + "
[1 rows x 21 columns in total]" + ], + "text/plain": [ + " taxi_id \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 4d924c1ad8126ad7efcbe49964b63ce4e62e11223b0f41... \n", + "\n", + " trip_end_timestamp \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 2013-01-01 00:15:00+00:00 \n", + "\n", + " trip_seconds \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 360 \n", + "\n", + " trip_miles \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 2.3 \n", + "\n", + " pickup_census_tract \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 17031330100 \n", + "\n", + " dropoff_census_tract \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 17031320100 \n", + "\n", + " pickup_community_area \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 33 \n", + "\n", + " dropoff_community_area \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 32 \n", + "\n", + " fare \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 7.85 \n", + "\n", + " tips ... \\\n", + "trip_start_timestamp unique_key ... \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 0.0 ... \n", + "\n", + " extras \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 2.0 \n", + "\n", + " trip_total \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 9.85 \n", + "\n", + " payment_type \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 Cash \n", + "\n", + " company \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 \n", + "\n", + " pickup_latitude \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 41.85935 \n", + "\n", + " pickup_longitude \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 -87.617358 \n", + "\n", + " pickup_location \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 POINT (-87.6173580061 41.859349715) \n", + "\n", + " dropoff_latitude \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 41.884987 \n", + "\n", + " dropoff_longitude \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 -87.620993 \n", + "\n", + " dropoff_location \n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 POINT (-87.6209929134 41.8849871918) \n", + "\n", + "[1 rows x 21 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.head(n=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "75a1ce88", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
taxi_idtrip_end_timestamptrip_secondstrip_milespickup_census_tractdropoff_census_tractpickup_community_areadropoff_community_areafaretips...extrastrip_totalpayment_typecompanypickup_latitudepickup_longitudepickup_locationdropoff_latitudedropoff_longitudedropoff_location
trip_start_timestampunique_key
2023-07-01 00:00:00+00:00fb004dfe98302ccf34e5e11bf1081568a94843a275cf3a53aae5e5858361a7ca64f75d3407dc0a44d7bc42...2023-07-01 00:00:00+00:005621.82<NA><NA>22229.52.53...0.012.03MobileCity Service41.922761-87.699155POINT (-87.6991553432 41.9227606205)41.922761-87.699155POINT (-87.6991553432 41.9227606205)
\n", + "

1 rows × 21 columns

\n", + "
[1 rows x 21 columns in total]" + ], + "text/plain": [ + " taxi_id \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 75cf3a53aae5e5858361a7ca64f75d3407dc0a44d7bc42... \n", + "\n", + " trip_end_timestamp \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 2023-07-01 00:00:00+00:00 \n", + "\n", + " trip_seconds \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 562 \n", + "\n", + " trip_miles \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 1.82 \n", + "\n", + " pickup_census_tract \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 \n", + "\n", + " dropoff_census_tract \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 \n", + "\n", + " pickup_community_area \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 22 \n", + "\n", + " dropoff_community_area \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 22 \n", + "\n", + " fare \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 9.5 \n", + "\n", + " tips ... \\\n", + "trip_start_timestamp unique_key ... \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 2.53 ... \n", + "\n", + " extras \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 0.0 \n", + "\n", + " trip_total \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 12.03 \n", + "\n", + " payment_type \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 Mobile \n", + "\n", + " company \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 City Service \n", + "\n", + " pickup_latitude \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 41.922761 \n", + "\n", + " pickup_longitude \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 -87.699155 \n", + "\n", + " pickup_location \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 POINT (-87.6991553432 41.9227606205) \n", + "\n", + " dropoff_latitude \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 41.922761 \n", + "\n", + " dropoff_longitude \\\n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 -87.699155 \n", + "\n", + " dropoff_location \n", + "trip_start_timestamp unique_key \n", + "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 POINT (-87.6991553432 41.9227606205) \n", + "\n", + "[1 rows x 21 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.tail(n=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f1eee1c6-7214-440e-bc17-6839d53a6718", + "metadata": {}, + "outputs": [], + "source": [ + "df = df[[\n", + " \"company\",\n", + " \"trip_miles\",\n", + " \"fare\",\n", + " \"tips\",\n", + "]]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "66071984-e371-4161-8d7e-00d810b5cfab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
companytrip_milesfaretips
trip_start_timestampunique_key
2013-01-01 00:00:00+00:00006d53094ff5b3c81140c00aa91fdb5467db0802<NA>2.37.850.0
00a723cf08cf29c56faa8ef77be519f6590b3405<NA>1.05.650.0
01396f0debefe45ccce23e48dd471edd67bd32e8Dispatch Taxi Affiliation0.08.850.0
01e9a03fd793670ed35ef7195eeb99775895611fDispatch Taxi Affiliation1.87.450.0
024f0f183c43ad2be33f232fd8dca0a86a3e4925<NA>4.914.050.0
04373ef74229c2122ff90d92642fcd94c88302e4Dispatch Taxi Affiliation0.06.650.0
0501a22fba176594d6f974360e20737af1b6cb66<NA>2.27.650.0
05faff951f2a2703fe3eb24efac026c2a1669a92<NA>3.39.650.0
0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbeBlue Ribbon Taxi Association Inc.0.06.250.0
06b4dc6dcd6295e69284e1c9d5a98015b0588ea6Choice Taxi Association0.013.850.0
07483a0baa6df0af68696b6f2483b7f429ff1827Northwest Management LLC0.64.650.0
07585d085f65cd512b94de09684253fe0ffb31a2Taxi Affiliation Services7.04.650.0
090ea9dc5dc26374ee06cdab1b809895beb1befcDispatch Taxi Affiliation0.010.050.0
091eda4ec41520d6ba96003f05eb10744f73aa82Northwest Management LLC0.015.250.0
097f2efed248cb5900792a89bba96a64918345ceBlue Ribbon Taxi Association Inc.0.012.250.0
09ffa3f40d36dbf8d51ca903738d9bba92557ed7Taxi Affiliation Services0.06.450.0
0a3446fb4f72d6485548883cd7f6cfe068d44829Taxi Affiliation Services0.014.250.0
0a7ea707a8700555e898489200a66b546a834170<NA>5.0413.852.0
0be545d9934dd343a2d929fca2e53d3d2851137bDispatch Taxi Affiliation1.15.650.0
0c2b3705c9748a5949bf5b7d727b2d1f2e18805f<NA>12.326.250.0
0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4Taxi Affiliation Services0.015.250.0
0dafcce34426b5377fd3925418a4c24b3e45c7d9<NA>1.26.250.0
0f5f7162b289dd9743b9d2e7fc75bf69696bb7bcTaxi Affiliation Services0.09.850.0
0f63007e437d589c9139acb026f03a832da3240fNorthwest Management LLC1.56.650.0
10508745a1069e9cdaad8760a6a243dc924cc0f2<NA>5.513.250.0
\n", + "

25 rows × 4 columns

\n", + "
[208395018 rows x 4 columns in total]" + ], + "text/plain": [ + " company \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 \n", + " 00a723cf08cf29c56faa8ef77be519f6590b3405 \n", + " 01396f0debefe45ccce23e48dd471edd67bd32e8 Dispatch Taxi Affiliation \n", + " 01e9a03fd793670ed35ef7195eeb99775895611f Dispatch Taxi Affiliation \n", + " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 \n", + " 04373ef74229c2122ff90d92642fcd94c88302e4 Dispatch Taxi Affiliation \n", + " 0501a22fba176594d6f974360e20737af1b6cb66 \n", + " 05faff951f2a2703fe3eb24efac026c2a1669a92 \n", + " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe Blue Ribbon Taxi Association Inc. \n", + " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 Choice Taxi Association \n", + " 07483a0baa6df0af68696b6f2483b7f429ff1827 Northwest Management LLC \n", + " 07585d085f65cd512b94de09684253fe0ffb31a2 Taxi Affiliation Services \n", + " 090ea9dc5dc26374ee06cdab1b809895beb1befc Dispatch Taxi Affiliation \n", + " 091eda4ec41520d6ba96003f05eb10744f73aa82 Northwest Management LLC \n", + " 097f2efed248cb5900792a89bba96a64918345ce Blue Ribbon Taxi Association Inc. \n", + " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 Taxi Affiliation Services \n", + " 0a3446fb4f72d6485548883cd7f6cfe068d44829 Taxi Affiliation Services \n", + " 0a7ea707a8700555e898489200a66b546a834170 \n", + " 0be545d9934dd343a2d929fca2e53d3d2851137b Dispatch Taxi Affiliation \n", + " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f \n", + " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 Taxi Affiliation Services \n", + " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 \n", + " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc Taxi Affiliation Services \n", + " 0f63007e437d589c9139acb026f03a832da3240f Northwest Management LLC \n", + " 10508745a1069e9cdaad8760a6a243dc924cc0f2 \n", + "\n", + " trip_miles \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 2.3 \n", + " 00a723cf08cf29c56faa8ef77be519f6590b3405 1.0 \n", + " 01396f0debefe45ccce23e48dd471edd67bd32e8 0.0 \n", + " 01e9a03fd793670ed35ef7195eeb99775895611f 1.8 \n", + " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 4.9 \n", + " 04373ef74229c2122ff90d92642fcd94c88302e4 0.0 \n", + " 0501a22fba176594d6f974360e20737af1b6cb66 2.2 \n", + " 05faff951f2a2703fe3eb24efac026c2a1669a92 3.3 \n", + " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe 0.0 \n", + " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 0.0 \n", + " 07483a0baa6df0af68696b6f2483b7f429ff1827 0.6 \n", + " 07585d085f65cd512b94de09684253fe0ffb31a2 7.0 \n", + " 090ea9dc5dc26374ee06cdab1b809895beb1befc 0.0 \n", + " 091eda4ec41520d6ba96003f05eb10744f73aa82 0.0 \n", + " 097f2efed248cb5900792a89bba96a64918345ce 0.0 \n", + " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 0.0 \n", + " 0a3446fb4f72d6485548883cd7f6cfe068d44829 0.0 \n", + " 0a7ea707a8700555e898489200a66b546a834170 5.04 \n", + " 0be545d9934dd343a2d929fca2e53d3d2851137b 1.1 \n", + " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f 12.3 \n", + " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 0.0 \n", + " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 1.2 \n", + " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc 0.0 \n", + " 0f63007e437d589c9139acb026f03a832da3240f 1.5 \n", + " 10508745a1069e9cdaad8760a6a243dc924cc0f2 5.5 \n", + "\n", + " fare \\\n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 7.85 \n", + " 00a723cf08cf29c56faa8ef77be519f6590b3405 5.65 \n", + " 01396f0debefe45ccce23e48dd471edd67bd32e8 8.85 \n", + " 01e9a03fd793670ed35ef7195eeb99775895611f 7.45 \n", + " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 14.05 \n", + " 04373ef74229c2122ff90d92642fcd94c88302e4 6.65 \n", + " 0501a22fba176594d6f974360e20737af1b6cb66 7.65 \n", + " 05faff951f2a2703fe3eb24efac026c2a1669a92 9.65 \n", + " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe 6.25 \n", + " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 13.85 \n", + " 07483a0baa6df0af68696b6f2483b7f429ff1827 4.65 \n", + " 07585d085f65cd512b94de09684253fe0ffb31a2 4.65 \n", + " 090ea9dc5dc26374ee06cdab1b809895beb1befc 10.05 \n", + " 091eda4ec41520d6ba96003f05eb10744f73aa82 15.25 \n", + " 097f2efed248cb5900792a89bba96a64918345ce 12.25 \n", + " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 6.45 \n", + " 0a3446fb4f72d6485548883cd7f6cfe068d44829 14.25 \n", + " 0a7ea707a8700555e898489200a66b546a834170 13.85 \n", + " 0be545d9934dd343a2d929fca2e53d3d2851137b 5.65 \n", + " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f 26.25 \n", + " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 15.25 \n", + " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 6.25 \n", + " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc 9.85 \n", + " 0f63007e437d589c9139acb026f03a832da3240f 6.65 \n", + " 10508745a1069e9cdaad8760a6a243dc924cc0f2 13.25 \n", + "\n", + " tips \n", + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 0.0 \n", + " 00a723cf08cf29c56faa8ef77be519f6590b3405 0.0 \n", + " 01396f0debefe45ccce23e48dd471edd67bd32e8 0.0 \n", + " 01e9a03fd793670ed35ef7195eeb99775895611f 0.0 \n", + " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 0.0 \n", + " 04373ef74229c2122ff90d92642fcd94c88302e4 0.0 \n", + " 0501a22fba176594d6f974360e20737af1b6cb66 0.0 \n", + " 05faff951f2a2703fe3eb24efac026c2a1669a92 0.0 \n", + " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe 0.0 \n", + " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 0.0 \n", + " 07483a0baa6df0af68696b6f2483b7f429ff1827 0.0 \n", + " 07585d085f65cd512b94de09684253fe0ffb31a2 0.0 \n", + " 090ea9dc5dc26374ee06cdab1b809895beb1befc 0.0 \n", + " 091eda4ec41520d6ba96003f05eb10744f73aa82 0.0 \n", + " 097f2efed248cb5900792a89bba96a64918345ce 0.0 \n", + " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 0.0 \n", + " 0a3446fb4f72d6485548883cd7f6cfe068d44829 0.0 \n", + " 0a7ea707a8700555e898489200a66b546a834170 2.0 \n", + " 0be545d9934dd343a2d929fca2e53d3d2851137b 0.0 \n", + " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f 0.0 \n", + " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 0.0 \n", + " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 0.0 \n", + " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc 0.0 \n", + " 0f63007e437d589c9139acb026f03a832da3240f 0.0 \n", + " 10508745a1069e9cdaad8760a6a243dc924cc0f2 0.0 \n", + "...\n", + "\n", + "[208395018 rows x 4 columns]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e51b687e-0282-459c-8fd0-0ca22cfb153d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 7.85\n", + " 00a723cf08cf29c56faa8ef77be519f6590b3405 5.65\n", + " 01396f0debefe45ccce23e48dd471edd67bd32e8 8.85\n", + " 01e9a03fd793670ed35ef7195eeb99775895611f 7.45\n", + " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 14.05\n", + " 04373ef74229c2122ff90d92642fcd94c88302e4 6.65\n", + " 0501a22fba176594d6f974360e20737af1b6cb66 7.65\n", + " 05faff951f2a2703fe3eb24efac026c2a1669a92 9.65\n", + " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe 6.25\n", + " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 13.85\n", + "Name: fare, dtype: Float64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['fare'].head(n=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ef9d466e-1138-41a7-ad4a-51e09c05870c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "13.669183161039143" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['fare'].mean()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "eb800cbb-d7d9-4562-8c23-4dd6c06759cc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "trip_start_timestamp unique_key \n", + "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 7.85\n", + " 00a723cf08cf29c56faa8ef77be519f6590b3405 5.65\n", + " 01396f0debefe45ccce23e48dd471edd67bd32e8 8.85\n", + " 01e9a03fd793670ed35ef7195eeb99775895611f 7.45\n", + " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 14.05\n", + "dtype: Float64" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "subtotal = df['fare'] + df['tips']\n", + "subtotal.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d2ffa53f-e158-4b0b-898e-825e3bd99881", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2844629.7599999933" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "subtotal[df['company'] == 'Norshore Cab'].sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "737dca2c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/02 - DataFrame.ipynb b/notebooks/02 - DataFrame.ipynb new file mode 100644 index 00000000000..d23c5d01ee9 --- /dev/null +++ b/notebooks/02 - DataFrame.ipynb @@ -0,0 +1,4158 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "47439dbd-4e54-4954-8b16-edc4bcd4f855", + "metadata": {}, + "source": [ + "# Demo to Show DataFrame Operations" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "72ebb083-f06b-4408-b24d-f349bd0851e3", + "metadata": {}, + "outputs": [], + "source": [ + "# On the instance where you are running jupyter,\n", + "# authenticate with gcloud first:\n", + "#\n", + "# gcloud auth application-default login\n", + "\n", + "import bigframes.pandas as bpd\n", + "\n", + "bpd.options.bigquery.location = \"US\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "96757c59-fc22-420e-a42f-c6cb956110ec", + "metadata": {}, + "outputs": [], + "source": [ + "df = bpd.read_gbq(\"bigquery-public-data.baseball.schedules\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "13861abc-120c-4db6-ad0c-e414b85d3443", + "metadata": {}, + "source": [ + "### Select a subset of the DF" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "05cb36e9-bb75-4f6f-8eb6-e4219df6e1d2", + "metadata": {}, + "outputs": [], + "source": [ + "df = df[[\"gameId\", \"year\", \"homeTeamName\", \"awayTeamName\", \"duration_minutes\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f124234c-d292-4520-b9cc-a162e2b16d91", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeamNameawayTeamNameduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175
\n", + "
[2431 rows x 5 columns in total]" + ], + "text/plain": [ + " gameId year homeTeamName awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 \n", + "5 201 \n", + "6 173 \n", + "7 176 \n", + "8 180 \n", + "9 157 \n", + "10 218 \n", + "11 160 \n", + "12 178 \n", + "13 205 \n", + "14 197 \n", + "15 198 \n", + "16 188 \n", + "17 188 \n", + "18 194 \n", + "19 175 \n", + "20 257 \n", + "21 178 \n", + "22 171 \n", + "23 248 \n", + "24 174 \n", + "...\n", + "\n", + "[2431 rows x 5 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df\n", + "# Here starts real execution, may take a while" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a6b8b3ac-1df8-46ff-ac4f-d6e7657fc80c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(2431, 5)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.shape" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "efc9ccb5-4dee-4fb8-a95f-3a654421cec4", + "metadata": {}, + "source": [ + "### Retrieve properties of the DF." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "34457cc7-e734-4e3f-9f2b-34cdd4e2aba4", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "gameId string[pyarrow]\n", + "year Int64\n", + "homeTeamName string[pyarrow]\n", + "awayTeamName string[pyarrow]\n", + "duration_minutes Int64\n", + "dtype: object" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b4f4383f-f596-41d8-aad2-2fd68d261cfd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['gameId', 'year', 'homeTeamName', 'awayTeamName', 'duration_minutes'], dtype='object')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.columns" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "70e0a637-52ac-47cd-93e2-75d06d7a6170", + "metadata": {}, + "source": [ + "### Add a new column" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c7017f3d-869d-42e3-bbd8-b3fbc408c2d0", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeamNameawayTeamNameduration_minutestitle
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187Marlins vs Cubs
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189Marlins vs Cubs
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165Braves vs Cubs
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222Braves vs Cubs
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164Phillies vs Cubs
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201Diamondbacks vs Cubs
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173Athletics vs Cubs
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176Athletics vs Cubs
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180Rockies vs Cubs
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157Cardinals vs Cubs
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218Cardinals vs Cubs
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160Cardinals vs Cubs
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178Pirates vs Cubs
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205Pirates vs Cubs
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197Giants vs Cubs
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198Reds vs Cubs
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188Reds vs Cubs
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188Reds vs Cubs
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194Reds vs Cubs
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175Padres vs Cubs
\n", + "
[2431 rows x 6 columns in total]" + ], + "text/plain": [ + " gameId year homeTeamName awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes title \n", + "0 187 Marlins vs Cubs \n", + "1 189 Marlins vs Cubs \n", + "2 165 Braves vs Cubs \n", + "3 222 Braves vs Cubs \n", + "4 164 Phillies vs Cubs \n", + "5 201 Diamondbacks vs Cubs \n", + "6 173 Athletics vs Cubs \n", + "7 176 Athletics vs Cubs \n", + "8 180 Rockies vs Cubs \n", + "9 157 Cardinals vs Cubs \n", + "10 218 Cardinals vs Cubs \n", + "11 160 Cardinals vs Cubs \n", + "12 178 Pirates vs Cubs \n", + "13 205 Pirates vs Cubs \n", + "14 197 Giants vs Cubs \n", + "15 198 Reds vs Cubs \n", + "16 188 Reds vs Cubs \n", + "17 188 Reds vs Cubs \n", + "18 194 Reds vs Cubs \n", + "19 175 Padres vs Cubs \n", + "20 257 Nationals vs Cubs \n", + "21 178 Brewers vs Cubs \n", + "22 171 Brewers vs Cubs \n", + "23 248 Brewers vs Cubs \n", + "24 174 Astros vs Cubs \n", + "...\n", + "\n", + "[2431 rows x 6 columns]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.assign(title=df['homeTeamName'] + \" vs \" + df['awayTeamName'])\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a4f9c18c-865e-4057-9e9a-85564f940c82", + "metadata": {}, + "source": [ + "### Manipulate the column" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8bbe000a-36f0-4b6f-b403-b9ec28dd608b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeamNameawayTeamNameduration_minutesheadline
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187Marlins vs Cubs
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189Marlins vs Cubs
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165Braves vs Cubs
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222Braves vs Cubs
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164Phillies vs Cubs
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201Diamondbacks vs Cubs
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173Athletics vs Cubs
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176Athletics vs Cubs
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180Rockies vs Cubs
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157Cardinals vs Cubs
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218Cardinals vs Cubs
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160Cardinals vs Cubs
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178Pirates vs Cubs
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205Pirates vs Cubs
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197Giants vs Cubs
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198Reds vs Cubs
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188Reds vs Cubs
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188Reds vs Cubs
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194Reds vs Cubs
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175Padres vs Cubs
\n", + "
[2431 rows x 6 columns in total]" + ], + "text/plain": [ + " gameId year homeTeamName awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes headline \n", + "0 187 Marlins vs Cubs \n", + "1 189 Marlins vs Cubs \n", + "2 165 Braves vs Cubs \n", + "3 222 Braves vs Cubs \n", + "4 164 Phillies vs Cubs \n", + "5 201 Diamondbacks vs Cubs \n", + "6 173 Athletics vs Cubs \n", + "7 176 Athletics vs Cubs \n", + "8 180 Rockies vs Cubs \n", + "9 157 Cardinals vs Cubs \n", + "10 218 Cardinals vs Cubs \n", + "11 160 Cardinals vs Cubs \n", + "12 178 Pirates vs Cubs \n", + "13 205 Pirates vs Cubs \n", + "14 197 Giants vs Cubs \n", + "15 198 Reds vs Cubs \n", + "16 188 Reds vs Cubs \n", + "17 188 Reds vs Cubs \n", + "18 194 Reds vs Cubs \n", + "19 175 Padres vs Cubs \n", + "20 257 Nationals vs Cubs \n", + "21 178 Brewers vs Cubs \n", + "22 171 Brewers vs Cubs \n", + "23 248 Brewers vs Cubs \n", + "24 174 Astros vs Cubs \n", + "...\n", + "\n", + "[2431 rows x 6 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.rename(columns={\"title\": \"headline\"})\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "87eee643-28ac-4f4b-ac61-1f3de9c08a9d", + "metadata": {}, + "outputs": [], + "source": [ + "df = df.drop(columns=\"headline\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fad6d3da-1f40-4c5f-94ec-0bdfe21ca5b6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeamNameawayTeamNameduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175
\n", + "
[2431 rows x 5 columns in total]" + ], + "text/plain": [ + " gameId year homeTeamName awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 \n", + "5 201 \n", + "6 173 \n", + "7 176 \n", + "8 180 \n", + "9 157 \n", + "10 218 \n", + "11 160 \n", + "12 178 \n", + "13 205 \n", + "14 197 \n", + "15 198 \n", + "16 188 \n", + "17 188 \n", + "18 194 \n", + "19 175 \n", + "20 257 \n", + "21 178 \n", + "22 171 \n", + "23 248 \n", + "24 174 \n", + "...\n", + "\n", + "[2431 rows x 5 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "218b3d0f-ee0d-4a8a-8c7d-cf24825f3d3e", + "metadata": {}, + "source": [ + "### Drop Nan values" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "67a7c35f-80cf-4482-80f9-7f01c7743807", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeamNameawayTeamNameduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175
\n", + "
[2431 rows x 5 columns in total]" + ], + "text/plain": [ + " gameId year homeTeamName awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 \n", + "5 201 \n", + "6 173 \n", + "7 176 \n", + "8 180 \n", + "9 157 \n", + "10 218 \n", + "11 160 \n", + "12 178 \n", + "13 205 \n", + "14 197 \n", + "15 198 \n", + "16 188 \n", + "17 188 \n", + "18 194 \n", + "19 175 \n", + "20 257 \n", + "21 178 \n", + "22 171 \n", + "23 248 \n", + "24 174 \n", + "...\n", + "\n", + "[2431 rows x 5 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.dropna()\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "80e9a2e2-c4c9-4c17-bbd0-06882d7657fe", + "metadata": {}, + "source": [ + "### Join two DFs" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3f09ff32-ef43-4fab-a86b-8868afc34363", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamName
0e14b6493-9e7f-404f-840a-8a680cc364bfMarlins
11f32b347-cbcb-4c31-a145-0e685306d168Marlins
20c2292d1-7398-48be-bf8e-b41dad5e1a43Braves
38fbec734-a15a-42ab-8d51-60790de7750bBraves
489e514d5-fbf5-4b9d-bdac-6ca45bfd18ddPhillies
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52Diamondbacks
676ea8662-c7e6-4c38-8f2a-efe373e428ceAthletics
766fad23d-6e89-4f99-be29-d49b6e94f95dAthletics
8d977367c-cf0c-4687-95a0-eb4542efcb01Rockies
9a87070ff-1084-43ca-a7ba-69278f93ecbaCardinals
10ea6b350d-3c1d-4737-878d-4465f66999f6Cardinals
1146463c50-0f5c-4dca-a661-dd194464e791Cardinals
1259134e6d-9d13-49aa-978e-c3c2300eb90fPirates
13387630a3-a894-4327-baa1-b24ec1a654d9Pirates
145d084e13-94fd-4995-b95a-4801ea3ed556Giants
1534444c94-03ec-4d12-96af-68b8f399a22fReds
169580bffe-22e1-4975-978b-1b13e7505193Reds
17645e6a08-afd6-4677-a5c9-01ef446b0cf3Reds
1808981bd8-d1d7-48e1-8668-9098b8f7fe90Reds
19303703bb-b55f-476d-8faf-bf582169fb1dPadres
\n", + "
[2431 rows x 2 columns in total]" + ], + "text/plain": [ + " gameId homeTeamName\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins\n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins\n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 Braves\n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b Braves\n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd Phillies\n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 Diamondbacks\n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce Athletics\n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d Athletics\n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 Rockies\n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba Cardinals\n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 Cardinals\n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals\n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f Pirates\n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 Pirates\n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 Giants\n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f Reds\n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 Reds\n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 Reds\n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 Reds\n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d Padres\n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 Nationals\n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c Brewers\n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 Brewers\n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 Brewers\n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 Astros\n", + "...\n", + "\n", + "[2431 rows x 2 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df[\"gameId\", \"homeTeamName\"]\n", + "df1" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "5331d2c8-7912-4d96-8da1-f64b57374df3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdawayTeamName
0e14b6493-9e7f-404f-840a-8a680cc364bfCubs
11f32b347-cbcb-4c31-a145-0e685306d168Cubs
\n", + "
[2 rows x 2 columns in total]" + ], + "text/plain": [ + " gameId awayTeamName\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf Cubs\n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 Cubs\n", + "\n", + "[2 rows x 2 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df2 = df[\"gameId\", \"awayTeamName\"].head(2)\n", + "df2" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "a574ad3e-a219-454c-8bb5-c5ed6627f2c6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0e14b6493-9e7f-404f-840a-8a680cc364bfMarlinsCubs
11f32b347-cbcb-4c31-a145-0e685306d168MarlinsCubs
\n", + "
[2 rows x 3 columns in total]" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", + "\n", + "[2 rows x 3 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1.merge(df2, on=\"gameId\", how=\"inner\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "288e7a95-a077-46c4-8fe6-802474c01f8b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0e14b6493-9e7f-404f-840a-8a680cc364bfMarlinsCubs
11f32b347-cbcb-4c31-a145-0e685306d168MarlinsCubs
20c2292d1-7398-48be-bf8e-b41dad5e1a43Braves<NA>
38fbec734-a15a-42ab-8d51-60790de7750bBraves<NA>
489e514d5-fbf5-4b9d-bdac-6ca45bfd18ddPhillies<NA>
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52Diamondbacks<NA>
676ea8662-c7e6-4c38-8f2a-efe373e428ceAthletics<NA>
766fad23d-6e89-4f99-be29-d49b6e94f95dAthletics<NA>
8d977367c-cf0c-4687-95a0-eb4542efcb01Rockies<NA>
9a87070ff-1084-43ca-a7ba-69278f93ecbaCardinals<NA>
10ea6b350d-3c1d-4737-878d-4465f66999f6Cardinals<NA>
1146463c50-0f5c-4dca-a661-dd194464e791Cardinals<NA>
1259134e6d-9d13-49aa-978e-c3c2300eb90fPirates<NA>
13387630a3-a894-4327-baa1-b24ec1a654d9Pirates<NA>
145d084e13-94fd-4995-b95a-4801ea3ed556Giants<NA>
1534444c94-03ec-4d12-96af-68b8f399a22fReds<NA>
169580bffe-22e1-4975-978b-1b13e7505193Reds<NA>
17645e6a08-afd6-4677-a5c9-01ef446b0cf3Reds<NA>
1808981bd8-d1d7-48e1-8668-9098b8f7fe90Reds<NA>
19303703bb-b55f-476d-8faf-bf582169fb1dPadres<NA>
\n", + "
[2431 rows x 3 columns in total]" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 Braves \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b Braves \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd Phillies \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 Diamondbacks \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce Athletics \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d Athletics \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 Rockies \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba Cardinals \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 Cardinals \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f Pirates \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 Pirates \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 Giants \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f Reds \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 Reds \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 Reds \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 Reds \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d Padres \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 Nationals \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c Brewers \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 Brewers \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 Brewers \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 Astros \n", + "...\n", + "\n", + "[2431 rows x 3 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1.merge(df2, on=\"gameId\", how=\"outer\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "7ee87a01-2ff5-4021-855d-44b71cf2a225", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0e14b6493-9e7f-404f-840a-8a680cc364bfMarlinsCubs
11f32b347-cbcb-4c31-a145-0e685306d168MarlinsCubs
20c2292d1-7398-48be-bf8e-b41dad5e1a43Braves<NA>
38fbec734-a15a-42ab-8d51-60790de7750bBraves<NA>
489e514d5-fbf5-4b9d-bdac-6ca45bfd18ddPhillies<NA>
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52Diamondbacks<NA>
676ea8662-c7e6-4c38-8f2a-efe373e428ceAthletics<NA>
766fad23d-6e89-4f99-be29-d49b6e94f95dAthletics<NA>
8d977367c-cf0c-4687-95a0-eb4542efcb01Rockies<NA>
9a87070ff-1084-43ca-a7ba-69278f93ecbaCardinals<NA>
10ea6b350d-3c1d-4737-878d-4465f66999f6Cardinals<NA>
1146463c50-0f5c-4dca-a661-dd194464e791Cardinals<NA>
1259134e6d-9d13-49aa-978e-c3c2300eb90fPirates<NA>
13387630a3-a894-4327-baa1-b24ec1a654d9Pirates<NA>
145d084e13-94fd-4995-b95a-4801ea3ed556Giants<NA>
1534444c94-03ec-4d12-96af-68b8f399a22fReds<NA>
169580bffe-22e1-4975-978b-1b13e7505193Reds<NA>
17645e6a08-afd6-4677-a5c9-01ef446b0cf3Reds<NA>
1808981bd8-d1d7-48e1-8668-9098b8f7fe90Reds<NA>
19303703bb-b55f-476d-8faf-bf582169fb1dPadres<NA>
\n", + "
[2431 rows x 3 columns in total]" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 Braves \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b Braves \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd Phillies \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 Diamondbacks \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce Athletics \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d Athletics \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 Rockies \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba Cardinals \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 Cardinals \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f Pirates \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 Pirates \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 Giants \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f Reds \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 Reds \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 Reds \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 Reds \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d Padres \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 Nationals \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c Brewers \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 Brewers \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 Brewers \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 Astros \n", + "...\n", + "\n", + "[2431 rows x 3 columns]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1.merge(df2, on=\"gameId\", how=\"left\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "330ed69c-f122-4af9-bf5e-96e309d3fa0c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0e14b6493-9e7f-404f-840a-8a680cc364bfMarlinsCubs
11f32b347-cbcb-4c31-a145-0e685306d168MarlinsCubs
\n", + "
[2 rows x 3 columns in total]" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", + "\n", + "[2 rows x 3 columns]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1.merge(df2, on=\"gameId\", how=\"right\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ea42b463-f37b-4ea4-805f-a16a84b656e4", + "metadata": {}, + "source": [ + "### Concat two DFs" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "5181231e-8a2a-4ac5-a379-6aa5ad4fee89", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeamNameawayTeamNameduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175
\n", + "
[4862 rows x 5 columns in total]" + ], + "text/plain": [ + " gameId year homeTeamName awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 \n", + "5 201 \n", + "6 173 \n", + "7 176 \n", + "8 180 \n", + "9 157 \n", + "10 218 \n", + "11 160 \n", + "12 178 \n", + "13 205 \n", + "14 197 \n", + "15 198 \n", + "16 188 \n", + "17 188 \n", + "18 194 \n", + "19 175 \n", + "20 257 \n", + "21 178 \n", + "22 171 \n", + "23 248 \n", + "24 174 \n", + "...\n", + "\n", + "[4862 rows x 5 columns]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bpd.concat([df, df])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "dcddbc12-3f17-43f5-a829-84d8093c7760", + "metadata": {}, + "source": [ + "### Access column through property" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "ad1f86f1-890b-462b-b408-b94c073371ff", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 Marlins\n", + "1 Marlins\n", + "2 Braves\n", + "3 Braves\n", + "4 Phillies\n", + " ... \n", + "2426 Dodgers\n", + "2427 Dodgers\n", + "2428 Mets\n", + "2429 Mets\n", + "2430 American League\n", + "Name: homeTeamName, Length: 2431, dtype: string" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.homeTeamName" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "4ae2e978-ac96-4c6a-8be8-9df9424c3544", + "metadata": {}, + "source": [ + "### Retrieve SQL" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "2ae4924b-37a9-4327-81d4-4f2afb66cb37", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WITH t0 AS (\n", + " SELECT t17.*, t17.`bigframes_ordering_id` AS `bigframes_index_0`\n", + " FROM (\n", + " SELECT * FROM `7150072e4dc649849bc421d3a492eee1`\n", + " ) t17\n", + "),\n", + "t1 AS (\n", + " SELECT coalesce(`bigframes_ordering_id`, `bigframes_ordering_id`) AS `index_2`,\n", + " CONCAT(CONCAT(t0.`homeTeamName`, ' vs '), t0.`awayTeamName`) AS `col_3`,\n", + " t0.`bigframes_ordering_id` AS `bigframes_ordering_id`\n", + " FROM t0\n", + "),\n", + "t2 AS (\n", + " SELECT `bigframes_ordering_id` AS `bigframes_index_0`, t0.`gameId`, t0.`year`,\n", + " t0.`homeTeamName`, t0.`awayTeamName`, t0.`duration_minutes`,\n", + " t0.`bigframes_ordering_id`\n", + " FROM t0\n", + "),\n", + "t3 AS (\n", + " SELECT t2.`bigframes_index_0`, t2.`gameId`, t2.`year`, t2.`homeTeamName`,\n", + " t2.`awayTeamName`, t2.`duration_minutes`,\n", + " t2.`bigframes_ordering_id` AS `bigframes_ordering_id_x`,\n", + " t1.`index_2`, t1.`col_3`,\n", + " t1.`bigframes_ordering_id` AS `bigframes_ordering_id_y`\n", + " FROM t2\n", + " LEFT OUTER JOIN t1\n", + " ON IFNULL(CAST(t2.`bigframes_index_0` AS STRING), '$NULL_SENTINEL$') = IFNULL(CAST(t1.`index_2` AS STRING), '$NULL_SENTINEL$')\n", + "),\n", + "t4 AS (\n", + " SELECT coalesce(t3.`bigframes_index_0`, t3.`index_2`) AS `index_5`,\n", + " t3.`gameId`, t3.`awayTeamName`,\n", + " ((((t3.`gameId` IS NOT NULL) AND (t3.`year` IS NOT NULL)) AND (t3.`homeTeamName` IS NOT NULL)) AND (t3.`awayTeamName` IS NOT NULL)) AND (t3.`duration_minutes` IS NOT NULL) AS `bigframes_predicate`,\n", + " (row_number() OVER (PARTITION BY ((((t3.`gameId` IS NOT NULL) AND (t3.`year` IS NOT NULL)) AND (t3.`homeTeamName` IS NOT NULL)) AND (t3.`awayTeamName` IS NOT NULL)) AND (t3.`duration_minutes` IS NOT NULL) ORDER BY CONCAT(IFNULL(lpad(CAST(t3.`bigframes_ordering_id_x` AS STRING), 19, '0'), ':::::::::::::::::::'), IFNULL(lpad(CAST(t3.`bigframes_ordering_id_y` AS STRING), 19, '0'), ':::::::::::::::::::')) IS NULL ASC, CONCAT(IFNULL(lpad(CAST(t3.`bigframes_ordering_id_x` AS STRING), 19, '0'), ':::::::::::::::::::'), IFNULL(lpad(CAST(t3.`bigframes_ordering_id_y` AS STRING), 19, '0'), ':::::::::::::::::::')) ASC) - 1) AS `bigframes_ordering_id`\n", + " FROM t3\n", + "),\n", + "t5 AS (\n", + " SELECT t4.*\n", + " FROM t4\n", + " WHERE t4.`bigframes_predicate`\n", + "),\n", + "t6 AS (\n", + " SELECT t5.`index_5`, t5.`gameId`, t5.`awayTeamName`,\n", + " t5.`bigframes_ordering_id`\n", + " FROM t5\n", + "),\n", + "t7 AS (\n", + " SELECT t6.`index_5`, t6.`gameId`, t6.`awayTeamName`,\n", + " (t6.`bigframes_ordering_id` = t6.`bigframes_ordering_id`) AND (t6.`bigframes_ordering_id` < 2) AS `bigframes_predicate`,\n", + " t6.`bigframes_ordering_id`\n", + " FROM t6\n", + "),\n", + "t8 AS (\n", + " SELECT coalesce(t3.`bigframes_index_0`, t3.`index_2`) AS `index_5`,\n", + " t3.`gameId`, t3.`homeTeamName`,\n", + " ((((t3.`gameId` IS NOT NULL) AND (t3.`year` IS NOT NULL)) AND (t3.`homeTeamName` IS NOT NULL)) AND (t3.`awayTeamName` IS NOT NULL)) AND (t3.`duration_minutes` IS NOT NULL) AS `bigframes_predicate`,\n", + " CONCAT(IFNULL(lpad(CAST(t3.`bigframes_ordering_id_x` AS STRING), 19, '0'), ':::::::::::::::::::'), IFNULL(lpad(CAST(t3.`bigframes_ordering_id_y` AS STRING), 19, '0'), ':::::::::::::::::::')) AS `bigframes_ordering_id`\n", + " FROM t3\n", + "),\n", + "t9 AS (\n", + " SELECT t7.*\n", + " FROM t7\n", + " WHERE t7.`bigframes_predicate`\n", + "),\n", + "t10 AS (\n", + " SELECT t8.*\n", + " FROM t8\n", + " WHERE t8.`bigframes_predicate`\n", + "),\n", + "t11 AS (\n", + " SELECT t9.`index_5`, t9.`gameId`, t9.`awayTeamName`,\n", + " t9.`bigframes_ordering_id`\n", + " FROM t9\n", + "),\n", + "t12 AS (\n", + " SELECT t10.`index_5`, t10.`gameId`, t10.`homeTeamName`,\n", + " t10.`bigframes_ordering_id`\n", + " FROM t10\n", + "),\n", + "t13 AS (\n", + " SELECT t12.`index_5` AS `index_5_x`, t12.`gameId` AS `gameId_x`,\n", + " t12.`homeTeamName`,\n", + " t12.`bigframes_ordering_id` AS `bigframes_ordering_id_x`,\n", + " t11.`index_5` AS `index_5_y`, t11.`gameId` AS `gameId_y`,\n", + " t11.`awayTeamName`,\n", + " t11.`bigframes_ordering_id` AS `bigframes_ordering_id_y`\n", + " FROM t12\n", + " INNER JOIN t11\n", + " ON IFNULL(t12.`gameId`, '$NULL_SENTINEL$') = IFNULL(t11.`gameId`, '$NULL_SENTINEL$')\n", + "),\n", + "t14 AS (\n", + " SELECT coalesce(t13.`gameId_x`, t13.`gameId_y`) AS `index_24`,\n", + " t13.`homeTeamName`, t13.`awayTeamName`,\n", + " (row_number() OVER (ORDER BY CONCAT(IFNULL(lpad(t13.`bigframes_ordering_id_x`, 38, '0'), '::::::::::::::::::::::::::::::::::::::'), IFNULL(lpad(CAST(t13.`bigframes_ordering_id_y` AS STRING), 19, '0'), ':::::::::::::::::::')) IS NULL ASC, CONCAT(IFNULL(lpad(t13.`bigframes_ordering_id_x`, 38, '0'), '::::::::::::::::::::::::::::::::::::::'), IFNULL(lpad(CAST(t13.`bigframes_ordering_id_y` AS STRING), 19, '0'), ':::::::::::::::::::')) ASC) - 1) AS `bigframes_ordering_id`\n", + " FROM t13\n", + "),\n", + "t15 AS (\n", + " SELECT t14.`bigframes_ordering_id` AS `col_25`, t14.`index_24`,\n", + " t14.`homeTeamName`, t14.`awayTeamName`\n", + " FROM t14\n", + ")\n", + "SELECT t16.`index_24` AS `gameId`, t16.`homeTeamName`, t16.`awayTeamName`\n", + "FROM (\n", + " SELECT t15.`index_24`, t15.`homeTeamName`, t15.`awayTeamName`\n", + " FROM t15\n", + ") t16\n" + ] + } + ], + "source": [ + "print(df1.merge(df2, on=\"gameId\", how=\"inner\").to_sql_query(always_include_index=False)[0])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "cec555be-0773-45e0-9008-86cd3e645285", + "metadata": {}, + "source": [ + "### Special Column Names" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "5adc7bbf-2c58-4924-964c-ed1b18dc9268", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearHOME TEAMawayTeamNameduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175
\n", + "
[2431 rows x 5 columns in total]" + ], + "text/plain": [ + " gameId year HOME TEAM awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 \n", + "5 201 \n", + "6 173 \n", + "7 176 \n", + "8 180 \n", + "9 157 \n", + "10 218 \n", + "11 160 \n", + "12 178 \n", + "13 205 \n", + "14 197 \n", + "15 198 \n", + "16 188 \n", + "17 188 \n", + "18 194 \n", + "19 175 \n", + "20 257 \n", + "21 178 \n", + "22 171 \n", + "23 248 \n", + "24 174 \n", + "...\n", + "\n", + "[2431 rows x 5 columns]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.rename(columns={\"homeTeamName\": \"HOME TEAM\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "ac3ceabe-4317-453c-9418-826de5094454", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeam!@#$%colawayTeamNameduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175
\n", + "
[2431 rows x 5 columns in total]" + ], + "text/plain": [ + " gameId year homeTeam!@#$%col awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 \n", + "5 201 \n", + "6 173 \n", + "7 176 \n", + "8 180 \n", + "9 157 \n", + "10 218 \n", + "11 160 \n", + "12 178 \n", + "13 205 \n", + "14 197 \n", + "15 198 \n", + "16 188 \n", + "17 188 \n", + "18 194 \n", + "19 175 \n", + "20 257 \n", + "21 178 \n", + "22 171 \n", + "23 248 \n", + "24 174 \n", + "...\n", + "\n", + "[2431 rows x 5 columns]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.rename(columns={\"homeTeamName\": \"homeTeam!@#$%col\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "e73704c9-7aa9-4f10-b414-3417c3ad9eb8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearteamteamduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b522016DiamondbacksCubs201
676ea8662-c7e6-4c38-8f2a-efe373e428ce2016AthleticsCubs173
766fad23d-6e89-4f99-be29-d49b6e94f95d2016AthleticsCubs176
8d977367c-cf0c-4687-95a0-eb4542efcb012016RockiesCubs180
9a87070ff-1084-43ca-a7ba-69278f93ecba2016CardinalsCubs157
10ea6b350d-3c1d-4737-878d-4465f66999f62016CardinalsCubs218
1146463c50-0f5c-4dca-a661-dd194464e7912016CardinalsCubs160
1259134e6d-9d13-49aa-978e-c3c2300eb90f2016PiratesCubs178
13387630a3-a894-4327-baa1-b24ec1a654d92016PiratesCubs205
145d084e13-94fd-4995-b95a-4801ea3ed5562016GiantsCubs197
1534444c94-03ec-4d12-96af-68b8f399a22f2016RedsCubs198
169580bffe-22e1-4975-978b-1b13e75051932016RedsCubs188
17645e6a08-afd6-4677-a5c9-01ef446b0cf32016RedsCubs188
1808981bd8-d1d7-48e1-8668-9098b8f7fe902016RedsCubs194
19303703bb-b55f-476d-8faf-bf582169fb1d2016PadresCubs175
\n", + "
[2431 rows x 5 columns in total]" + ], + "text/plain": [ + " gameId year team team \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", + "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", + "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", + "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", + "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", + "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", + "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", + "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", + "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", + "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", + "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", + "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", + "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", + "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", + "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", + "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", + "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", + "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", + "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 \n", + "5 201 \n", + "6 173 \n", + "7 176 \n", + "8 180 \n", + "9 157 \n", + "10 218 \n", + "11 160 \n", + "12 178 \n", + "13 205 \n", + "14 197 \n", + "15 198 \n", + "16 188 \n", + "17 188 \n", + "18 194 \n", + "19 175 \n", + "20 257 \n", + "21 178 \n", + "22 171 \n", + "23 248 \n", + "24 174 \n", + "...\n", + "\n", + "[2431 rows x 5 columns]" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df3 = df.rename(columns={\"homeTeamName\": \"team\", \"awayTeamName\": \"team\"})\n", + "df3" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "1a80f6f8-a172-4d7d-a2f5-e10871da7224", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
teamteam
0MarlinsCubs
1MarlinsCubs
2BravesCubs
3BravesCubs
4PhilliesCubs
5DiamondbacksCubs
6AthleticsCubs
7AthleticsCubs
8RockiesCubs
9CardinalsCubs
10CardinalsCubs
11CardinalsCubs
12PiratesCubs
13PiratesCubs
14GiantsCubs
15RedsCubs
16RedsCubs
17RedsCubs
18RedsCubs
19PadresCubs
\n", + "
[2431 rows x 2 columns in total]" + ], + "text/plain": [ + " team team\n", + "0 Marlins Cubs\n", + "1 Marlins Cubs\n", + "2 Braves Cubs\n", + "3 Braves Cubs\n", + "4 Phillies Cubs\n", + "5 Diamondbacks Cubs\n", + "6 Athletics Cubs\n", + "7 Athletics Cubs\n", + "8 Rockies Cubs\n", + "9 Cardinals Cubs\n", + "10 Cardinals Cubs\n", + "11 Cardinals Cubs\n", + "12 Pirates Cubs\n", + "13 Pirates Cubs\n", + "14 Giants Cubs\n", + "15 Reds Cubs\n", + "16 Reds Cubs\n", + "17 Reds Cubs\n", + "18 Reds Cubs\n", + "19 Padres Cubs\n", + "20 Nationals Cubs\n", + "21 Brewers Cubs\n", + "22 Brewers Cubs\n", + "23 Brewers Cubs\n", + "24 Astros Cubs\n", + "...\n", + "\n", + "[2431 rows x 2 columns]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df3[\"team\"]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8e1a4dc8-de23-466e-8ead-df4af82a8085", + "metadata": {}, + "source": [ + "### Binary Operation" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "2414a095-37df-4755-b86c-2031a6cb9d4a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
yearduration_minutes
02016187
12016189
22016165
32016222
42016164
52016201
62016173
72016176
82016180
92016157
102016218
112016160
122016178
132016205
142016197
152016198
162016188
172016188
182016194
192016175
\n", + "
[2431 rows x 2 columns in total]" + ], + "text/plain": [ + " year duration_minutes\n", + "0 2016 187\n", + "1 2016 189\n", + "2 2016 165\n", + "3 2016 222\n", + "4 2016 164\n", + "5 2016 201\n", + "6 2016 173\n", + "7 2016 176\n", + "8 2016 180\n", + "9 2016 157\n", + "10 2016 218\n", + "11 2016 160\n", + "12 2016 178\n", + "13 2016 205\n", + "14 2016 197\n", + "15 2016 198\n", + "16 2016 188\n", + "17 2016 188\n", + "18 2016 194\n", + "19 2016 175\n", + "20 2016 257\n", + "21 2016 178\n", + "22 2016 171\n", + "23 2016 248\n", + "24 2016 174\n", + "...\n", + "\n", + "[2431 rows x 2 columns]" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df4 = df[[\"year\", \"duration_minutes\"]]\n", + "df4" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "7d437c7c-ae74-4f0d-a4f8-10a133f4b61e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
yearduration_minutes
02017188
12017190
22017166
32017223
42017165
52017202
62017174
72017177
82017181
92017158
102017219
112017161
122017179
132017206
142017198
152017199
162017189
172017189
182017195
192017176
\n", + "
[2431 rows x 2 columns in total]" + ], + "text/plain": [ + " year duration_minutes\n", + "0 2017 188\n", + "1 2017 190\n", + "2 2017 166\n", + "3 2017 223\n", + "4 2017 165\n", + "5 2017 202\n", + "6 2017 174\n", + "7 2017 177\n", + "8 2017 181\n", + "9 2017 158\n", + "10 2017 219\n", + "11 2017 161\n", + "12 2017 179\n", + "13 2017 206\n", + "14 2017 198\n", + "15 2017 199\n", + "16 2017 189\n", + "17 2017 189\n", + "18 2017 195\n", + "19 2017 176\n", + "20 2017 258\n", + "21 2017 179\n", + "22 2017 172\n", + "23 2017 249\n", + "24 2017 175\n", + "...\n", + "\n", + "[2431 rows x 2 columns]" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df4 + 1" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/03 - Using ML - ML fundamentals.ipynb b/notebooks/03 - Using ML - ML fundamentals.ipynb new file mode 100644 index 00000000000..c181b67e647 --- /dev/null +++ b/notebooks/03 - Using ML - ML fundamentals.ipynb @@ -0,0 +1,2250 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using ML - ML fundamentals\n", + "\n", + "The `bigframes.ml` module implements Scikit-Learn's machine learning API in\n", + "BigQuery DataFrames. It exposes BigQuery's ML capabilities in a simple, popular\n", + "API that works seamlessly with the rest of the BigQuery DataFrames API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
penguin_id
0Adelie Penguin (Pygoscelis adeliae)Dream36.618.4184.03475.0FEMALE
1Adelie Penguin (Pygoscelis adeliae)Dream39.819.1184.04650.0MALE
2Adelie Penguin (Pygoscelis adeliae)Dream40.918.9184.03900.0MALE
3Chinstrap penguin (Pygoscelis antarctica)Dream46.517.9192.03500.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Dream37.316.8192.03000.0FEMALE
5Adelie Penguin (Pygoscelis adeliae)Dream43.218.5192.04100.0MALE
6Chinstrap penguin (Pygoscelis antarctica)Dream46.916.6192.02700.0FEMALE
7Chinstrap penguin (Pygoscelis antarctica)Dream50.518.4200.03400.0FEMALE
8Chinstrap penguin (Pygoscelis antarctica)Dream49.519.0200.03800.0MALE
9Adelie Penguin (Pygoscelis adeliae)Dream40.220.1200.03975.0MALE
10Adelie Penguin (Pygoscelis adeliae)Dream40.818.9208.04300.0MALE
11Adelie Penguin (Pygoscelis adeliae)Dream39.018.7185.03650.0MALE
12Adelie Penguin (Pygoscelis adeliae)Dream37.016.9185.03000.0FEMALE
13Chinstrap penguin (Pygoscelis antarctica)Dream47.017.3185.03700.0FEMALE
14Adelie Penguin (Pygoscelis adeliae)Dream34.017.1185.03400.0FEMALE
15Adelie Penguin (Pygoscelis adeliae)Dream37.016.5185.03400.0FEMALE
16Chinstrap penguin (Pygoscelis antarctica)Dream45.717.3193.03600.0FEMALE
17Chinstrap penguin (Pygoscelis antarctica)Dream50.619.4193.03800.0MALE
18Adelie Penguin (Pygoscelis adeliae)Dream39.717.9193.04250.0MALE
19Adelie Penguin (Pygoscelis adeliae)Dream37.818.1193.03750.0MALE
\n", + "
[334 rows x 7 columns in total]" + ], + "text/plain": [ + " species island \\\n", + "penguin_id \n", + "0 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "1 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "2 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "4 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "5 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "6 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "7 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "8 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "9 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "10 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "12 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "13 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "14 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "15 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "16 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "17 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "18 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "21 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "22 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "\n", + " culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", + "penguin_id \n", + "0 36.6 18.4 184.0 3475.0 \n", + "1 39.8 19.1 184.0 4650.0 \n", + "2 40.9 18.9 184.0 3900.0 \n", + "3 46.5 17.9 192.0 3500.0 \n", + "4 37.3 16.8 192.0 3000.0 \n", + "5 43.2 18.5 192.0 4100.0 \n", + "6 46.9 16.6 192.0 2700.0 \n", + "7 50.5 18.4 200.0 3400.0 \n", + "8 49.5 19.0 200.0 3800.0 \n", + "9 40.2 20.1 200.0 3975.0 \n", + "10 40.8 18.9 208.0 4300.0 \n", + "11 39.0 18.7 185.0 3650.0 \n", + "12 37.0 16.9 185.0 3000.0 \n", + "13 47.0 17.3 185.0 3700.0 \n", + "14 34.0 17.1 185.0 3400.0 \n", + "15 37.0 16.5 185.0 3400.0 \n", + "16 45.7 17.3 193.0 3600.0 \n", + "17 50.6 19.4 193.0 3800.0 \n", + "18 39.7 17.9 193.0 4250.0 \n", + "19 37.8 18.1 193.0 3750.0 \n", + "20 46.6 17.8 193.0 3800.0 \n", + "21 51.3 19.2 193.0 3650.0 \n", + "22 40.2 17.1 193.0 3400.0 \n", + "23 36.8 18.5 193.0 3500.0 \n", + "24 49.6 18.2 193.0 3775.0 \n", + "\n", + " sex \n", + "penguin_id \n", + "0 FEMALE \n", + "1 MALE \n", + "2 MALE \n", + "3 FEMALE \n", + "4 FEMALE \n", + "5 MALE \n", + "6 FEMALE \n", + "7 FEMALE \n", + "8 MALE \n", + "9 MALE \n", + "10 MALE \n", + "11 MALE \n", + "12 FEMALE \n", + "13 FEMALE \n", + "14 FEMALE \n", + "15 FEMALE \n", + "16 FEMALE \n", + "17 MALE \n", + "18 MALE \n", + "19 MALE \n", + "20 FEMALE \n", + "21 MALE \n", + "22 FEMALE \n", + "23 FEMALE \n", + "24 MALE \n", + "...\n", + "\n", + "[334 rows x 7 columns]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Lets load some test data to use in this tutorial\n", + "import bigframes.pandas\n", + "\n", + "df = bigframes.pandas.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", + "df = df.dropna()\n", + "\n", + "# Temporary workaround: lets name our index so it isn't lost BigQuery DataFrame\n", + "# currently drops unnamed indexes when round-tripping through pandas, which\n", + "# some ML APIs do to route around missing functionality\n", + "df.index.name = \"penguin_id\"\n", + "\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data split\n", + "\n", + "Part of preparing data for a machine learning task is splitting it into subsets for training and testing, to ensure that the solution is not overfitting. Most commonly this is done with `bigframes.ml.model_selection.train_test_split` like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train_X shape: (267, 6)\n", + "test_X shape: (67, 6)\n", + "train_y shape: (267, 1)\n", + "test_y shape: (67, 1)\n" + ] + } + ], + "source": [ + "# In this example, we're doing supervised learning, where we will learn to predict\n", + "# output variable `y` from input features `X`\n", + "X = df[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex', 'species']]\n", + "y = df[['body_mass_g']] \n", + "\n", + "from bigframes.ml.model_selection import train_test_split\n", + "\n", + "# This will split X and y into test and training sets, with 20% of the rows in the test set,\n", + "# and the rest in the training set\n", + "train_X, test_X, train_y, test_y = train_test_split(\n", + " X, y, test_size=0.2)\n", + "\n", + "# Show the shape of the data after the split\n", + "print(f\"\"\"train_X shape: {train_X.shape}\n", + "test_X shape: {test_X.shape}\n", + "train_y shape: {train_y.shape}\n", + "test_y shape: {test_y.shape}\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmsexspecies
penguin_id
125Biscoe39.718.9184.0MALEAdelie Penguin (Pygoscelis adeliae)
260Biscoe50.415.7222.0MALEGentoo penguin (Pygoscelis papua)
93Dream41.118.1205.0MALEAdelie Penguin (Pygoscelis adeliae)
255Biscoe44.514.7214.0FEMALEGentoo penguin (Pygoscelis papua)
71Dream32.115.5188.0FEMALEAdelie Penguin (Pygoscelis adeliae)
\n", + "
[5 rows x 6 columns in total]" + ], + "text/plain": [ + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "penguin_id \n", + "125 Biscoe 39.7 18.9 184.0 \n", + "260 Biscoe 50.4 15.7 222.0 \n", + "93 Dream 41.1 18.1 205.0 \n", + "255 Biscoe 44.5 14.7 214.0 \n", + "71 Dream 32.1 15.5 188.0 \n", + "\n", + " sex species \n", + "penguin_id \n", + "125 MALE Adelie Penguin (Pygoscelis adeliae) \n", + "260 MALE Gentoo penguin (Pygoscelis papua) \n", + "93 MALE Adelie Penguin (Pygoscelis adeliae) \n", + "255 FEMALE Gentoo penguin (Pygoscelis papua) \n", + "71 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", + "\n", + "[5 rows x 6 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# If we look at the data, we can see that random rows were selected for\n", + "# each side of the split\n", + "test_X.head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
body_mass_g
penguin_id
1253550.0
2605750.0
934300.0
2554850.0
713050.0
\n", + "
[5 rows x 1 columns in total]" + ], + "text/plain": [ + " body_mass_g\n", + "penguin_id \n", + "125 3550.0\n", + "260 5750.0\n", + "93 4300.0\n", + "255 4850.0\n", + "71 3050.0\n", + "\n", + "[5 rows x 1 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Note that this matches the rows in test_X\n", + "test_y.head(5)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Estimators\n", + "\n", + "Following Scikit-Learn, all learning components are \"estimators\"; objects that can learn from training data and then apply themselves to new data. Estimators share the following patterns:\n", + "\n", + "- a constructor that takes a list of parameters\n", + "- a standard string representation that shows the class name and all non-default parameters, e.g. `LinearRegression(fit_intercept=False)`\n", + "- a `.fit(..)` method to fit the estimator to training data\n", + "\n", + "There estimators can be further broken down into two main subtypes:\n", + "\n", + "### Transformers\n", + "\n", + "Transformers are estimators that are used to prepare data for consumption by other estimators ('preprocessing'). In addition to `.fit(...)`, the transformer implements a `.transform(...)` method, which will apply a transformation based on what was computed during `.fit(..)`. With this pattern dynamic preprocessing steps can be applied to both training and test/production data consistently.\n", + "\n", + "An example of a transformer is `bigframes.ml.preprocessing.StandardScaler`, which rescales a dataset to have a mean of zero and a standard deviation of one:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
scaled_culmen_length_mmscaled_culmen_depth_mmscaled_flipper_length_mm
penguin_id
23-1.3102310.687253-0.568262
832.5759550.329548-1.432429
1950.816172-1.4078760.151877
2641.109469-0.4369622.096254
2370.816172-1.3056740.872017
2610.614531-0.9479691.520142
2530.321234-1.6633790.944031
1640.101261-0.7435661.160073
293-0.686975-0.079257-1.792499
1760.871166-1.5611780.655975
451.4760911.8114690.655975
1851.347773-1.5100771.232087
1411.036145-1.1012711.088059
1471.109469-0.6413651.66417
320-0.9252790.329548-1.432429
285-0.192036-2.0721851.016045
3050.0279370.431750.655975
371.4210981.607066-0.496248
98-0.5219950.176246-0.784304
10-0.5769890.8916560.511947
\n", + "
[267 rows x 3 columns in total]" + ], + "text/plain": [ + " scaled_culmen_length_mm scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "23 -1.310231 0.687253 \n", + "83 2.575955 0.329548 \n", + "195 0.816172 -1.407876 \n", + "264 1.109469 -0.436962 \n", + "237 0.816172 -1.305674 \n", + "261 0.614531 -0.947969 \n", + "253 0.321234 -1.663379 \n", + "164 0.101261 -0.743566 \n", + "293 -0.686975 -0.079257 \n", + "176 0.871166 -1.561178 \n", + "45 1.476091 1.811469 \n", + "185 1.347773 -1.510077 \n", + "141 1.036145 -1.101271 \n", + "147 1.109469 -0.641365 \n", + "320 -0.925279 0.329548 \n", + "285 -0.192036 -2.072185 \n", + "305 0.027937 0.43175 \n", + "37 1.421098 1.607066 \n", + "98 -0.521995 0.176246 \n", + "10 -0.576989 0.891656 \n", + "307 -1.420218 0.022944 \n", + "337 -0.393678 1.14716 \n", + "26 -0.448671 0.687253 \n", + "97 -1.45688 0.380649 \n", + "27 1.201125 1.249361 \n", + "\n", + " scaled_flipper_length_mm \n", + "penguin_id \n", + "23 -0.568262 \n", + "83 -1.432429 \n", + "195 0.151877 \n", + "264 2.096254 \n", + "237 0.872017 \n", + "261 1.520142 \n", + "253 0.944031 \n", + "164 1.160073 \n", + "293 -1.792499 \n", + "176 0.655975 \n", + "45 0.655975 \n", + "185 1.232087 \n", + "141 1.088059 \n", + "147 1.66417 \n", + "320 -1.432429 \n", + "285 1.016045 \n", + "305 0.655975 \n", + "37 -0.496248 \n", + "98 -0.784304 \n", + "10 0.511947 \n", + "307 -1.000346 \n", + "337 -0.208192 \n", + "26 0.00785 \n", + "97 -0.784304 \n", + "27 0.00785 \n", + "...\n", + "\n", + "[267 rows x 3 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.preprocessing import StandardScaler\n", + "\n", + "# StandardScaler will only work on numeric columns\n", + "numeric_columns = [\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]\n", + "\n", + "scaler = StandardScaler()\n", + "scaler.fit(train_X[numeric_columns])\n", + "\n", + "# Now, standardscaler should transform the numbers to have mean of zero\n", + "# and standard deviation of one:\n", + "scaler.transform(train_X[numeric_columns])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
scaled_culmen_length_mmscaled_culmen_depth_mmscaled_flipper_length_mm
penguin_id
125-0.778630.891656-1.216387
2601.182794-0.7435661.520142
93-0.5219950.4828510.295905
2550.101261-1.2545740.944031
71-2.171791-0.845768-0.928332
327-0.5219950.227347-1.360415
338-0.9802722.066973-0.71229
681.292780.8405560.151877
171.2194561.14716-0.568262
116-0.3020232.066973-0.71229
106-0.943611.453764-0.784304
2271.512753-0.0281561.952226
314-0.2103670.687253-0.424234
246-1.1269210.585052-1.936527
1381.201125-0.999071.088059
126-1.383556-0.028156-1.216387
1210.761179-0.385861-0.136178
231-0.3203541.198261-0.280206
401.1461320.8405560.079863
600.5045440.380649-0.424234
\n", + "
[67 rows x 3 columns in total]" + ], + "text/plain": [ + " scaled_culmen_length_mm scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "125 -0.77863 0.891656 \n", + "260 1.182794 -0.743566 \n", + "93 -0.521995 0.482851 \n", + "255 0.101261 -1.254574 \n", + "71 -2.171791 -0.845768 \n", + "327 -0.521995 0.227347 \n", + "338 -0.980272 2.066973 \n", + "68 1.29278 0.840556 \n", + "17 1.219456 1.14716 \n", + "116 -0.302023 2.066973 \n", + "106 -0.94361 1.453764 \n", + "227 1.512753 -0.028156 \n", + "314 -0.210367 0.687253 \n", + "246 -1.126921 0.585052 \n", + "138 1.201125 -0.99907 \n", + "126 -1.383556 -0.028156 \n", + "121 0.761179 -0.385861 \n", + "231 -0.320354 1.198261 \n", + "40 1.146132 0.840556 \n", + "60 0.504544 0.380649 \n", + "331 -1.548535 0.176246 \n", + "218 0.192916 -0.896869 \n", + "154 -1.10859 0.738354 \n", + "130 -0.357016 1.198261 \n", + "89 1.604408 1.351563 \n", + "\n", + " scaled_flipper_length_mm \n", + "penguin_id \n", + "125 -1.216387 \n", + "260 1.520142 \n", + "93 0.295905 \n", + "255 0.944031 \n", + "71 -0.928332 \n", + "327 -1.360415 \n", + "338 -0.71229 \n", + "68 0.151877 \n", + "17 -0.568262 \n", + "116 -0.71229 \n", + "106 -0.784304 \n", + "227 1.952226 \n", + "314 -0.424234 \n", + "246 -1.936527 \n", + "138 1.088059 \n", + "126 -1.216387 \n", + "121 -0.136178 \n", + "231 -0.280206 \n", + "40 0.079863 \n", + "60 -0.424234 \n", + "331 -0.784304 \n", + "218 1.376114 \n", + "154 -0.568262 \n", + "130 -0.064164 \n", + "89 -0.280206 \n", + "...\n", + "\n", + "[67 rows x 3 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can then repeat this transformation on new data\n", + "scaler.transform(test_X[numeric_columns])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Composing transformers\n", + "\n", + "To process data where different columns need different preprocessors, `bigframes.composition.ColumnTransformer` can be employed:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
onehotencoded_islandscaled_culmen_length_mmscaled_culmen_depth_mmscaled_flipper_length_mmonehotencoded_sexonehotencoded_species
penguin_id
23[{'index': 2, 'value': 1.0}]-1.3102310.687253-0.568262[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
83[{'index': 2, 'value': 1.0}]2.5759550.329548-1.432429[{'index': 1, 'value': 1.0}][{'index': 2, 'value': 1.0}]
195[{'index': 1, 'value': 1.0}]0.816172-1.4078760.151877[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
264[{'index': 1, 'value': 1.0}]1.109469-0.4369622.096254[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
237[{'index': 1, 'value': 1.0}]0.816172-1.3056740.872017[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
261[{'index': 1, 'value': 1.0}]0.614531-0.9479691.520142[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
253[{'index': 1, 'value': 1.0}]0.321234-1.6633790.944031[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
164[{'index': 1, 'value': 1.0}]0.101261-0.7435661.160073[{'index': 0, 'value': 1.0}][{'index': 3, 'value': 1.0}]
293[{'index': 3, 'value': 1.0}]-0.686975-0.079257-1.792499[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
176[{'index': 1, 'value': 1.0}]0.871166-1.5611780.655975[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
45[{'index': 2, 'value': 1.0}]1.4760911.8114690.655975[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
185[{'index': 1, 'value': 1.0}]1.347773-1.5100771.232087[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
141[{'index': 1, 'value': 1.0}]1.036145-1.1012711.088059[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
147[{'index': 1, 'value': 1.0}]1.109469-0.6413651.66417[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
320[{'index': 3, 'value': 1.0}]-0.9252790.329548-1.432429[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
285[{'index': 1, 'value': 1.0}]-0.192036-2.0721851.016045[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
305[{'index': 3, 'value': 1.0}]0.0279370.431750.655975[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
37[{'index': 2, 'value': 1.0}]1.4210981.607066-0.496248[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
98[{'index': 2, 'value': 1.0}]-0.5219950.176246-0.784304[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
10[{'index': 2, 'value': 1.0}]-0.5769890.8916560.511947[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
\n", + "
[267 rows x 6 columns in total]" + ], + "text/plain": [ + " onehotencoded_island scaled_culmen_length_mm \\\n", + "penguin_id \n", + "23 [{'index': 2, 'value': 1.0}] -1.310231 \n", + "83 [{'index': 2, 'value': 1.0}] 2.575955 \n", + "195 [{'index': 1, 'value': 1.0}] 0.816172 \n", + "264 [{'index': 1, 'value': 1.0}] 1.109469 \n", + "237 [{'index': 1, 'value': 1.0}] 0.816172 \n", + "261 [{'index': 1, 'value': 1.0}] 0.614531 \n", + "253 [{'index': 1, 'value': 1.0}] 0.321234 \n", + "164 [{'index': 1, 'value': 1.0}] 0.101261 \n", + "293 [{'index': 3, 'value': 1.0}] -0.686975 \n", + "176 [{'index': 1, 'value': 1.0}] 0.871166 \n", + "45 [{'index': 2, 'value': 1.0}] 1.476091 \n", + "185 [{'index': 1, 'value': 1.0}] 1.347773 \n", + "141 [{'index': 1, 'value': 1.0}] 1.036145 \n", + "147 [{'index': 1, 'value': 1.0}] 1.109469 \n", + "320 [{'index': 3, 'value': 1.0}] -0.925279 \n", + "285 [{'index': 1, 'value': 1.0}] -0.192036 \n", + "305 [{'index': 3, 'value': 1.0}] 0.027937 \n", + "37 [{'index': 2, 'value': 1.0}] 1.421098 \n", + "98 [{'index': 2, 'value': 1.0}] -0.521995 \n", + "10 [{'index': 2, 'value': 1.0}] -0.576989 \n", + "307 [{'index': 3, 'value': 1.0}] -1.420218 \n", + "337 [{'index': 3, 'value': 1.0}] -0.393678 \n", + "26 [{'index': 2, 'value': 1.0}] -0.448671 \n", + "97 [{'index': 2, 'value': 1.0}] -1.45688 \n", + "27 [{'index': 2, 'value': 1.0}] 1.201125 \n", + "\n", + " scaled_culmen_depth_mm scaled_flipper_length_mm \\\n", + "penguin_id \n", + "23 0.687253 -0.568262 \n", + "83 0.329548 -1.432429 \n", + "195 -1.407876 0.151877 \n", + "264 -0.436962 2.096254 \n", + "237 -1.305674 0.872017 \n", + "261 -0.947969 1.520142 \n", + "253 -1.663379 0.944031 \n", + "164 -0.743566 1.160073 \n", + "293 -0.079257 -1.792499 \n", + "176 -1.561178 0.655975 \n", + "45 1.811469 0.655975 \n", + "185 -1.510077 1.232087 \n", + "141 -1.101271 1.088059 \n", + "147 -0.641365 1.66417 \n", + "320 0.329548 -1.432429 \n", + "285 -2.072185 1.016045 \n", + "305 0.43175 0.655975 \n", + "37 1.607066 -0.496248 \n", + "98 0.176246 -0.784304 \n", + "10 0.891656 0.511947 \n", + "307 0.022944 -1.000346 \n", + "337 1.14716 -0.208192 \n", + "26 0.687253 0.00785 \n", + "97 0.380649 -0.784304 \n", + "27 1.249361 0.00785 \n", + "\n", + " onehotencoded_sex onehotencoded_species \n", + "penguin_id \n", + "23 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "83 [{'index': 1, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "195 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "264 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "237 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "261 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "253 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "164 [{'index': 0, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "293 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "176 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "45 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "185 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "141 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "147 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "320 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "285 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", + "305 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "37 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "98 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "10 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "307 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "337 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "26 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "97 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "27 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "...\n", + "\n", + "[267 rows x 6 columns]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.compose import ColumnTransformer\n", + "from bigframes.ml.preprocessing import OneHotEncoder\n", + "\n", + "# Create an aggregate transform that applies StandardScaler to the numeric columns,\n", + "# and OneHotEncoder to the string columns\n", + "preproc = ColumnTransformer([\n", + " (\"scale\", StandardScaler(), [\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]),\n", + " (\"encode\", OneHotEncoder(), [\"species\", \"sex\", \"island\"])])\n", + "\n", + "# Now we can fit all columns of the training data\n", + "preproc.fit(train_X)\n", + "\n", + "processed_train_X = preproc.transform(train_X)\n", + "processed_test_X = preproc.transform(test_X)\n", + "\n", + "processed_train_X" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Predictors\n", + "\n", + "Predictors are estimators that learn and make predictions. In addition to `.fit(...)`, the predictor implements a `.predict(...)` method, which will use what was learned during `.fit(...)` to predict some output.\n", + "\n", + "Predictors can be further broken down into two categories:\n", + "\n", + "#### Supervised predictors\n", + "\n", + "Supervised learning is when we train a model on input-output pairs, and then ask it to predict the output for new inputs. An example of such a predictor is `bigframes.ml.linear_models.LinearRegression`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_g
penguin_id
2554789.541771
1584642.382347
2303562.484258
2185321.625743
2973926.179647
934158.344927
3384093.191646
2675590.390771
2753623.482531
2005494.402339
1073450.730176
2275707.948707
1994855.771085
2605477.219436
2014965.59121
73670.281516
84072.678667
1263339.624869
393556.890093
123302.203276
\n", + "
[67 rows x 1 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g\n", + "penguin_id \n", + "255 4789.541771\n", + "158 4642.382347\n", + "230 3562.484258\n", + "218 5321.625743\n", + "297 3926.179647\n", + "93 4158.344927\n", + "338 4093.191646\n", + "267 5590.390771\n", + "275 3623.482531\n", + "200 5494.402339\n", + "107 3450.730176\n", + "227 5707.948707\n", + "199 4855.771085\n", + "260 5477.219436\n", + "201 4965.59121\n", + "7 3670.281516\n", + "8 4072.678667\n", + "126 3339.624869\n", + "39 3556.890093\n", + "12 3302.203276\n", + "34 3227.822355\n", + "206 3864.010614\n", + "53 3329.248666\n", + "282 5297.303727\n", + "225 5770.689468\n", + "...\n", + "\n", + "[67 rows x 1 columns]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "linreg = LinearRegression()\n", + "\n", + "# Learn from the training data how to predict output y\n", + "linreg.fit(processed_train_X, train_y)\n", + "\n", + "# Predict y for the test data\n", + "predicted_test_y = linreg.predict(processed_test_X)\n", + "\n", + "predicted_test_y" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Unsupervised predictors\n", + "\n", + "In unsupervised learning, there are no known outputs in the training data, instead the model learns on input data alone and predicts something else. An example of an unsupervised predictor is `bigframes.ml.cluster.KMeans`, which learns how to fit input data to a target number of clusters." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CENTROID_ID
penguin_id
171
2302
2914
1773
2062
3312
182
2184
1162
2712
1653
1942
391
2553
2254
401
81
1554
2394
1383
\n", + "
[67 rows x 1 columns in total]" + ], + "text/plain": [ + " CENTROID_ID\n", + "penguin_id \n", + "17 1\n", + "230 2\n", + "291 4\n", + "177 3\n", + "206 2\n", + "331 2\n", + "18 2\n", + "218 4\n", + "116 2\n", + "271 2\n", + "165 3\n", + "194 2\n", + "39 1\n", + "255 3\n", + "225 4\n", + "40 1\n", + "8 1\n", + "155 4\n", + "239 4\n", + "138 3\n", + "89 1\n", + "34 1\n", + "106 2\n", + "12 2\n", + "158 3\n", + "...\n", + "\n", + "[67 rows x 1 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.cluster import KMeans\n", + "\n", + "kmeans = KMeans(n_clusters=4)\n", + "\n", + "kmeans.fit(processed_train_X)\n", + "\n", + "kmeans.predict(processed_test_X)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipelines\n", + "\n", + "Transfomers and predictors can be chained into a single estimator component using `bigframes.ml.pipeline.Pipeline`:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('preproc',\n", + " ColumnTransformer(transformers=[('scale', StandardScaler(),\n", + " ['culmen_length_mm',\n", + " 'culmen_depth_mm',\n", + " 'flipper_length_mm']),\n", + " ('encode', OneHotEncoder(),\n", + " ['species', 'sex',\n", + " 'island'])])),\n", + " ('linreg', LinearRegression())])" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.pipeline import Pipeline\n", + "\n", + "pipeline = Pipeline([\n", + " ('preproc', preproc),\n", + " ('linreg', linreg)\n", + "])\n", + "\n", + "# Print our pipeline\n", + "pipeline" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The pipeline simplifies the workflow by applying each of its component steps automatically:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_g
penguin_id
1253911.489556
2605477.221344
934158.347237
2554789.543554
713153.676672
3273358.229939
3384093.194446
684133.390538
174013.257221
1164189.275197
1064019.304549
2275707.950809
3144037.311435
2463297.59755
1384971.171877
1263339.627076
1213462.463115
2314206.392245
404102.581912
603483.351408
\n", + "
[67 rows x 1 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g\n", + "penguin_id \n", + "125 3911.489556\n", + "260 5477.221344\n", + "93 4158.347237\n", + "255 4789.543554\n", + "71 3153.676672\n", + "327 3358.229939\n", + "338 4093.194446\n", + "68 4133.390538\n", + "17 4013.257221\n", + "116 4189.275197\n", + "106 4019.304549\n", + "227 5707.950809\n", + "314 4037.311435\n", + "246 3297.59755\n", + "138 4971.171877\n", + "126 3339.627076\n", + "121 3462.463115\n", + "231 4206.392245\n", + "40 4102.581912\n", + "60 3483.351408\n", + "331 3370.089387\n", + "218 5321.627618\n", + "154 3618.98119\n", + "130 4249.575399\n", + "89 4145.000048\n", + "...\n", + "\n", + "[67 rows x 1 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pipeline.fit(train_X, train_y)\n", + "\n", + "predicted_test_y = pipeline.predict(test_X)\n", + "predicted_test_y" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the backend, a pipeline will actually be compiled into a single model with an embedded TRANSFORM step." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Evaluating results\n", + "\n", + "Some models include a convenient `.score(X, y)` method for evaulation with a preset accuracy metric:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.86245773619.1653690.004765209.6072890.8883680.901838
\n", + "
[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 223.862457 73619.165369 0.004765 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 209.607289 0.888368 0.901838 \n", + "\n", + "[1 rows x 6 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# In the case of a pipeline, this will be equivalent to calling .score on the contained LinearRegression\n", + "pipeline.score(test_X, test_y)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For a more general approach, the library `bigframes.ml.metrics` is provided:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.8883676417577451" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.metrics import r2_score\n", + "\n", + "r2_score(test_y, predicted_test_y)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Save/Load to BigQuery\n", + "\n", + "Estimators can be saved to BigQuery as BQML models, and loaded again in future" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Replace with a path where you have permission to save a model\n", + "model_name = \"bigframes-dev.bqml_tutorial.penguins_model\"\n", + "\n", + "linreg.to_gbq(model_name, replace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# WARNING - until b/281709360 is fixed & pipeline is updated, pipelines will load as models,\n", + "# and details of their transform steps will be lost (the loaded model will behave the same)\n", + "bigframes.pandas.read_gbq_model(model_name)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "a850322d07d9bdc9ec5f301d307e048bcab2390ae395e1cbce9335f4e081e5e2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/04 - Using ML - SKLearn linear regression.ipynb b/notebooks/04 - Using ML - SKLearn linear regression.ipynb new file mode 100644 index 00000000000..73cf224bef4 --- /dev/null +++ b/notebooks/04 - Using ML - SKLearn linear regression.ipynb @@ -0,0 +1,977 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using ML - SKLearn linear regression\n", + "\n", + "This demo shows how we can implement a linear regression in BigQuery DataFrames ML, with API that is exactly compatible with Scikit-Learn." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Init & load data" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tag_numberspeciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
01225Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
11278Gentoo penguin (Pygoscelis papua)Biscoe42.013.5210.04150.0FEMALE
21275Gentoo penguin (Pygoscelis papua)Biscoe46.513.5210.04550.0FEMALE
31233Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
41311Gentoo penguin (Pygoscelis papua)Biscoe47.514.0212.04875.0FEMALE
51316Gentoo penguin (Pygoscelis papua)Biscoe49.114.5212.04625.0FEMALE
61313Gentoo penguin (Pygoscelis papua)Biscoe45.514.5212.04750.0FEMALE
71381Gentoo penguin (Pygoscelis papua)Biscoe47.614.5215.05400.0MALE
81377Gentoo penguin (Pygoscelis papua)Biscoe45.114.5207.05050.0FEMALE
91380Gentoo penguin (Pygoscelis papua)Biscoe45.114.5215.05000.0FEMALE
101257Gentoo penguin (Pygoscelis papua)Biscoe46.214.5209.04800.0FEMALE
111336Gentoo penguin (Pygoscelis papua)Biscoe46.514.5213.04400.0FEMALE
121237Gentoo penguin (Pygoscelis papua)Biscoe43.214.5208.04450.0FEMALE
131302Gentoo penguin (Pygoscelis papua)Biscoe48.515.0219.04850.0FEMALE
141325Gentoo penguin (Pygoscelis papua)Biscoe49.115.0228.05500.0MALE
151285Gentoo penguin (Pygoscelis papua)Biscoe47.515.0218.04950.0FEMALE
161242Gentoo penguin (Pygoscelis papua)Biscoe49.615.0216.04750.0MALE
171246Gentoo penguin (Pygoscelis papua)Biscoe47.715.0216.04750.0FEMALE
181320Gentoo penguin (Pygoscelis papua)Biscoe45.515.0220.05000.0MALE
191244Gentoo penguin (Pygoscelis papua)Biscoe46.415.0216.04700.0FEMALE
\n", + "
[347 rows x 8 columns in total]" + ], + "text/plain": [ + " tag_number species island culmen_length_mm \\\n", + "0 1225 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "1 1278 Gentoo penguin (Pygoscelis papua) Biscoe 42.0 \n", + "2 1275 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "3 1233 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", + "4 1311 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "5 1316 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "6 1313 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "7 1381 Gentoo penguin (Pygoscelis papua) Biscoe 47.6 \n", + "8 1377 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "9 1380 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "10 1257 Gentoo penguin (Pygoscelis papua) Biscoe 46.2 \n", + "11 1336 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "12 1237 Gentoo penguin (Pygoscelis papua) Biscoe 43.2 \n", + "13 1302 Gentoo penguin (Pygoscelis papua) Biscoe 48.5 \n", + "14 1325 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "15 1285 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "16 1242 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "17 1246 Gentoo penguin (Pygoscelis papua) Biscoe 47.7 \n", + "18 1320 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "19 1244 Gentoo penguin (Pygoscelis papua) Biscoe 46.4 \n", + "20 1390 Gentoo penguin (Pygoscelis papua) Biscoe 50.7 \n", + "21 1379 Gentoo penguin (Pygoscelis papua) Biscoe 47.8 \n", + "22 1267 Gentoo penguin (Pygoscelis papua) Biscoe 50.1 \n", + "23 1389 Gentoo penguin (Pygoscelis papua) Biscoe 47.2 \n", + "24 1269 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 \n", + "1 13.5 210.0 4150.0 FEMALE \n", + "2 13.5 210.0 4550.0 FEMALE \n", + "3 14.0 208.0 4575.0 FEMALE \n", + "4 14.0 212.0 4875.0 FEMALE \n", + "5 14.5 212.0 4625.0 FEMALE \n", + "6 14.5 212.0 4750.0 FEMALE \n", + "7 14.5 215.0 5400.0 MALE \n", + "8 14.5 207.0 5050.0 FEMALE \n", + "9 14.5 215.0 5000.0 FEMALE \n", + "10 14.5 209.0 4800.0 FEMALE \n", + "11 14.5 213.0 4400.0 FEMALE \n", + "12 14.5 208.0 4450.0 FEMALE \n", + "13 15.0 219.0 4850.0 FEMALE \n", + "14 15.0 228.0 5500.0 MALE \n", + "15 15.0 218.0 4950.0 FEMALE \n", + "16 15.0 216.0 4750.0 MALE \n", + "17 15.0 216.0 4750.0 FEMALE \n", + "18 15.0 220.0 5000.0 MALE \n", + "19 15.0 216.0 4700.0 FEMALE \n", + "20 15.0 223.0 5550.0 MALE \n", + "21 15.0 215.0 5650.0 MALE \n", + "22 15.0 225.0 5000.0 MALE \n", + "23 15.5 215.0 4975.0 FEMALE \n", + "24 16.0 225.0 5700.0 MALE \n", + "...\n", + "\n", + "[347 rows x 8 columns]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Initialize BigQuery DataFrame\n", + "import bigframes.pandas\n", + "\n", + "# read a BigQuery table to a BigQuery DataFrame\n", + "df = bigframes.pandas.read_gbq(\"bigframes-dev.bqml_tutorial.penguins\")\n", + "\n", + "# take a peek at the dataframe\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Data cleaning / prep" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
tag_number
1172Dream32.115.5188.03050.0FEMALE
1371Biscoe37.716.0183.03075.0FEMALE
1417Torgersen38.617.0188.02900.0FEMALE
1204Dream40.717.0190.03725.0MALE
1251Biscoe37.617.0185.03600.0FEMALE
1422Torgersen35.717.0189.03350.0FEMALE
1394Torgersen40.217.0176.03450.0FEMALE
1163Dream36.417.0195.03325.0FEMALE
1329Biscoe38.117.0181.03175.0FEMALE
1406Torgersen44.118.0210.04000.0MALE
1196Dream36.518.0182.03150.0FEMALE
1228Biscoe41.618.0192.03950.0MALE
1412Torgersen40.318.0195.03250.0FEMALE
1142Dream35.718.0202.03550.0FEMALE
1430Torgersen33.519.0190.03600.0FEMALE
1333Biscoe43.219.0197.04775.0MALE
1414Torgersen38.719.0195.03450.0FEMALE
1197Dream41.119.0182.03425.0MALE
1443Torgersen40.619.0199.04000.0MALE
1295Biscoe41.020.0203.04725.0MALE
\n", + "
[146 rows x 6 columns in total]" + ], + "text/plain": [ + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "tag_number \n", + "1172 Dream 32.1 15.5 188.0 \n", + "1371 Biscoe 37.7 16.0 183.0 \n", + "1417 Torgersen 38.6 17.0 188.0 \n", + "1204 Dream 40.7 17.0 190.0 \n", + "1251 Biscoe 37.6 17.0 185.0 \n", + "1422 Torgersen 35.7 17.0 189.0 \n", + "1394 Torgersen 40.2 17.0 176.0 \n", + "1163 Dream 36.4 17.0 195.0 \n", + "1329 Biscoe 38.1 17.0 181.0 \n", + "1406 Torgersen 44.1 18.0 210.0 \n", + "1196 Dream 36.5 18.0 182.0 \n", + "1228 Biscoe 41.6 18.0 192.0 \n", + "1412 Torgersen 40.3 18.0 195.0 \n", + "1142 Dream 35.7 18.0 202.0 \n", + "1430 Torgersen 33.5 19.0 190.0 \n", + "1333 Biscoe 43.2 19.0 197.0 \n", + "1414 Torgersen 38.7 19.0 195.0 \n", + "1197 Dream 41.1 19.0 182.0 \n", + "1443 Torgersen 40.6 19.0 199.0 \n", + "1295 Biscoe 41.0 20.0 203.0 \n", + "1207 Dream 38.8 20.0 190.0 \n", + "1349 Biscoe 38.2 20.0 190.0 \n", + "1350 Biscoe 37.8 20.0 190.0 \n", + "1351 Biscoe 38.1 16.5 198.0 \n", + "1116 Dream 37.0 16.5 185.0 \n", + "\n", + " body_mass_g sex \n", + "tag_number \n", + "1172 3050.0 FEMALE \n", + "1371 3075.0 FEMALE \n", + "1417 2900.0 FEMALE \n", + "1204 3725.0 MALE \n", + "1251 3600.0 FEMALE \n", + "1422 3350.0 FEMALE \n", + "1394 3450.0 FEMALE \n", + "1163 3325.0 FEMALE \n", + "1329 3175.0 FEMALE \n", + "1406 4000.0 MALE \n", + "1196 3150.0 FEMALE \n", + "1228 3950.0 MALE \n", + "1412 3250.0 FEMALE \n", + "1142 3550.0 FEMALE \n", + "1430 3600.0 FEMALE \n", + "1333 4775.0 MALE \n", + "1414 3450.0 FEMALE \n", + "1197 3425.0 MALE \n", + "1443 4000.0 MALE \n", + "1295 4725.0 MALE \n", + "1207 3950.0 MALE \n", + "1349 3900.0 MALE \n", + "1350 4250.0 MALE \n", + "1351 3825.0 FEMALE \n", + "1116 3400.0 FEMALE \n", + "...\n", + "\n", + "[146 rows x 6 columns]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# set a friendlier index to uniquely identify the rows\n", + "df = df.set_index(\"tag_number\")\n", + "\n", + "# filter down to the data we want to analyze\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# drop the columns we don't care about\n", + "adelie_data = adelie_data.drop(columns=[\"species\"])\n", + "\n", + "# drop rows with nulls to get our training data\n", + "training_data = adelie_data.dropna()\n", + "\n", + "# take a peek at the training data\n", + "training_data" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Use `model_selection.train_test_split` to prepare training data" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from bigframes.ml.model_selection import train_test_split\n", + "\n", + "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "label_columns = training_data[['body_mass_g']] \n", + "\n", + "train_X, test_X, train_y, test_y = train_test_split(\n", + " feature_columns, label_columns, test_size=0.2)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Configure a linear regression pipeline with preprocessing" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('preproc',\n", + " ColumnTransformer(transformers=[('onehot', OneHotEncoder(),\n", + " ['island', 'species', 'sex']),\n", + " ('scaler', StandardScaler(),\n", + " ['culmen_depth_mm',\n", + " 'culmen_length_mm',\n", + " 'flipper_length_mm'])])),\n", + " ('linreg', LinearRegression(fit_intercept=False))])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "from bigframes.ml.pipeline import Pipeline\n", + "from bigframes.ml.compose import ColumnTransformer\n", + "from bigframes.ml.preprocessing import StandardScaler, OneHotEncoder\n", + "\n", + "preprocessing = ColumnTransformer([\n", + " (\"onehot\", OneHotEncoder(), [\"island\", \"species\", \"sex\"]),\n", + " (\"scaler\", StandardScaler(), [\"culmen_depth_mm\", \"culmen_length_mm\", \"flipper_length_mm\"]),\n", + "])\n", + "\n", + "model = LinearRegression(fit_intercept=False)\n", + "\n", + "pipeline = Pipeline([\n", + " ('preproc', preprocessing),\n", + " ('linreg', model)\n", + "])\n", + "\n", + "# TODO(bmil): pretty printing for pipelines\n", + "pipeline" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Fit the pipeline to the training data\n", + "\n", + "This will create a temporary BQML model in BigQuery" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "pipeline.fit(train_X, train_y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Score the pipeline on the test data with `metrics.r2_score`" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.4597467714807377" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.metrics import r2_score\n", + "\n", + "pred_y = pipeline.predict(test_X)\n", + "\n", + "r2_score(test_y, pred_y)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Inference the model on new data" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas\n", + "\n", + "new_penguins = bigframes.pandas.read_pandas(\n", + " pandas.DataFrame(\n", + " {\n", + " \"tag_number\": [1633, 1672, 1690],\n", + " \"species\": [\n", + " \"Adelie Penguin (Pygoscelis adeliae)\",\n", + " \"Adelie Penguin (Pygoscelis adeliae)\",\n", + " \"Adelie Penguin (Pygoscelis adeliae)\",\n", + " ],\n", + " \"island\": [\"Torgersen\", \"Torgersen\", \"Dream\"],\n", + " \"culmen_length_mm\": [39.5, 38.5, 37.9],\n", + " \"culmen_depth_mm\": [18.8, 17.2, 18.1],\n", + " \"flipper_length_mm\": [196.0, 181.0, 188.0],\n", + " \"sex\": [\"MALE\", \"FEMALE\", \"FEMALE\"],\n", + " }\n", + " ).set_index(\"tag_number\")\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_g
tag_number
16334034.682043
16723267.196667
16903445.920252
\n", + "
[3 rows x 1 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g\n", + "tag_number \n", + "1633 4034.682043\n", + "1672 3267.196667\n", + "1690 3445.920252\n", + "\n", + "[3 rows x 1 columns]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pipeline.predict(new_penguins)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Save in BigQuery" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('preproc',\n", + " ColumnTransformer(transformers=[('onehot', OneHotEncoder(),\n", + " ['island', 'species', 'sex']),\n", + " ('scaler', StandardScaler(),\n", + " ['culmen_depth_mm',\n", + " 'culmen_length_mm',\n", + " 'flipper_length_mm'])])),\n", + " ('linreg', LinearRegression(fit_intercept=False))])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pipeline.to_gbq(\"bigframes-dev.bigframes_demo_us.penguin_model\", replace=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "a850322d07d9bdc9ec5f301d307e048bcab2390ae395e1cbce9335f4e081e5e2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/05 - Using ML - Easy linear regression.ipynb b/notebooks/05 - Using ML - Easy linear regression.ipynb new file mode 100644 index 00000000000..90d6f256a7b --- /dev/null +++ b/notebooks/05 - Using ML - Easy linear regression.ipynb @@ -0,0 +1,920 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using ML - Easy linear regression\n", + "\n", + "This demo shows BigQuery DataFrames ML providing an SKLearn-like experience for\n", + "training a linear regression model.\n", + "\n", + "In this \"easy\" version of linear regression, we use a couple of BQML features to simplify our code:\n", + "\n", + "- We rely on automatic preprocessing to encode string values and scale numeric values\n", + "- We rely on automatic data split & evaluation to test the model\n", + "\n", + "This example is adapted from the [BQML linear regression tutorial](https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Init & load data" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tag_numberspeciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
01225Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
11278Gentoo penguin (Pygoscelis papua)Biscoe42.013.5210.04150.0FEMALE
21275Gentoo penguin (Pygoscelis papua)Biscoe46.513.5210.04550.0FEMALE
31233Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
41311Gentoo penguin (Pygoscelis papua)Biscoe47.514.0212.04875.0FEMALE
51316Gentoo penguin (Pygoscelis papua)Biscoe49.114.5212.04625.0FEMALE
61313Gentoo penguin (Pygoscelis papua)Biscoe45.514.5212.04750.0FEMALE
71381Gentoo penguin (Pygoscelis papua)Biscoe47.614.5215.05400.0MALE
81377Gentoo penguin (Pygoscelis papua)Biscoe45.114.5207.05050.0FEMALE
91380Gentoo penguin (Pygoscelis papua)Biscoe45.114.5215.05000.0FEMALE
101257Gentoo penguin (Pygoscelis papua)Biscoe46.214.5209.04800.0FEMALE
111336Gentoo penguin (Pygoscelis papua)Biscoe46.514.5213.04400.0FEMALE
121237Gentoo penguin (Pygoscelis papua)Biscoe43.214.5208.04450.0FEMALE
131302Gentoo penguin (Pygoscelis papua)Biscoe48.515.0219.04850.0FEMALE
141325Gentoo penguin (Pygoscelis papua)Biscoe49.115.0228.05500.0MALE
151285Gentoo penguin (Pygoscelis papua)Biscoe47.515.0218.04950.0FEMALE
161242Gentoo penguin (Pygoscelis papua)Biscoe49.615.0216.04750.0MALE
171246Gentoo penguin (Pygoscelis papua)Biscoe47.715.0216.04750.0FEMALE
181320Gentoo penguin (Pygoscelis papua)Biscoe45.515.0220.05000.0MALE
191244Gentoo penguin (Pygoscelis papua)Biscoe46.415.0216.04700.0FEMALE
\n", + "
[347 rows x 8 columns in total]" + ], + "text/plain": [ + " tag_number species island culmen_length_mm \\\n", + "0 1225 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "1 1278 Gentoo penguin (Pygoscelis papua) Biscoe 42.0 \n", + "2 1275 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "3 1233 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", + "4 1311 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "5 1316 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "6 1313 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "7 1381 Gentoo penguin (Pygoscelis papua) Biscoe 47.6 \n", + "8 1377 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "9 1380 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "10 1257 Gentoo penguin (Pygoscelis papua) Biscoe 46.2 \n", + "11 1336 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "12 1237 Gentoo penguin (Pygoscelis papua) Biscoe 43.2 \n", + "13 1302 Gentoo penguin (Pygoscelis papua) Biscoe 48.5 \n", + "14 1325 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "15 1285 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "16 1242 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "17 1246 Gentoo penguin (Pygoscelis papua) Biscoe 47.7 \n", + "18 1320 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "19 1244 Gentoo penguin (Pygoscelis papua) Biscoe 46.4 \n", + "20 1390 Gentoo penguin (Pygoscelis papua) Biscoe 50.7 \n", + "21 1379 Gentoo penguin (Pygoscelis papua) Biscoe 47.8 \n", + "22 1267 Gentoo penguin (Pygoscelis papua) Biscoe 50.1 \n", + "23 1389 Gentoo penguin (Pygoscelis papua) Biscoe 47.2 \n", + "24 1269 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 \n", + "1 13.5 210.0 4150.0 FEMALE \n", + "2 13.5 210.0 4550.0 FEMALE \n", + "3 14.0 208.0 4575.0 FEMALE \n", + "4 14.0 212.0 4875.0 FEMALE \n", + "5 14.5 212.0 4625.0 FEMALE \n", + "6 14.5 212.0 4750.0 FEMALE \n", + "7 14.5 215.0 5400.0 MALE \n", + "8 14.5 207.0 5050.0 FEMALE \n", + "9 14.5 215.0 5000.0 FEMALE \n", + "10 14.5 209.0 4800.0 FEMALE \n", + "11 14.5 213.0 4400.0 FEMALE \n", + "12 14.5 208.0 4450.0 FEMALE \n", + "13 15.0 219.0 4850.0 FEMALE \n", + "14 15.0 228.0 5500.0 MALE \n", + "15 15.0 218.0 4950.0 FEMALE \n", + "16 15.0 216.0 4750.0 MALE \n", + "17 15.0 216.0 4750.0 FEMALE \n", + "18 15.0 220.0 5000.0 MALE \n", + "19 15.0 216.0 4700.0 FEMALE \n", + "20 15.0 223.0 5550.0 MALE \n", + "21 15.0 215.0 5650.0 MALE \n", + "22 15.0 225.0 5000.0 MALE \n", + "23 15.5 215.0 4975.0 FEMALE \n", + "24 16.0 225.0 5700.0 MALE \n", + "...\n", + "\n", + "[347 rows x 8 columns]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import bigframes.pandas\n", + "\n", + "# read a BigQuery table to a BigQuery DataFrame\n", + "df = bigframes.pandas.read_gbq(\"bigframes-dev.bqml_tutorial.penguins\")\n", + "\n", + "# take a peek at the dataframe\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Data cleaning / prep" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
tag_number
1172Dream32.115.5188.03050.0FEMALE
1371Biscoe37.716.0183.03075.0FEMALE
1417Torgersen38.617.0188.02900.0FEMALE
1204Dream40.717.0190.03725.0MALE
1251Biscoe37.617.0185.03600.0FEMALE
1422Torgersen35.717.0189.03350.0FEMALE
1394Torgersen40.217.0176.03450.0FEMALE
1163Dream36.417.0195.03325.0FEMALE
1329Biscoe38.117.0181.03175.0FEMALE
1406Torgersen44.118.0210.04000.0MALE
1196Dream36.518.0182.03150.0FEMALE
1228Biscoe41.618.0192.03950.0MALE
1412Torgersen40.318.0195.03250.0FEMALE
1142Dream35.718.0202.03550.0FEMALE
1430Torgersen33.519.0190.03600.0FEMALE
1333Biscoe43.219.0197.04775.0MALE
1414Torgersen38.719.0195.03450.0FEMALE
1197Dream41.119.0182.03425.0MALE
1443Torgersen40.619.0199.04000.0MALE
1295Biscoe41.020.0203.04725.0MALE
\n", + "
[146 rows x 6 columns in total]" + ], + "text/plain": [ + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "tag_number \n", + "1172 Dream 32.1 15.5 188.0 \n", + "1371 Biscoe 37.7 16.0 183.0 \n", + "1417 Torgersen 38.6 17.0 188.0 \n", + "1204 Dream 40.7 17.0 190.0 \n", + "1251 Biscoe 37.6 17.0 185.0 \n", + "1422 Torgersen 35.7 17.0 189.0 \n", + "1394 Torgersen 40.2 17.0 176.0 \n", + "1163 Dream 36.4 17.0 195.0 \n", + "1329 Biscoe 38.1 17.0 181.0 \n", + "1406 Torgersen 44.1 18.0 210.0 \n", + "1196 Dream 36.5 18.0 182.0 \n", + "1228 Biscoe 41.6 18.0 192.0 \n", + "1412 Torgersen 40.3 18.0 195.0 \n", + "1142 Dream 35.7 18.0 202.0 \n", + "1430 Torgersen 33.5 19.0 190.0 \n", + "1333 Biscoe 43.2 19.0 197.0 \n", + "1414 Torgersen 38.7 19.0 195.0 \n", + "1197 Dream 41.1 19.0 182.0 \n", + "1443 Torgersen 40.6 19.0 199.0 \n", + "1295 Biscoe 41.0 20.0 203.0 \n", + "1207 Dream 38.8 20.0 190.0 \n", + "1349 Biscoe 38.2 20.0 190.0 \n", + "1350 Biscoe 37.8 20.0 190.0 \n", + "1351 Biscoe 38.1 16.5 198.0 \n", + "1116 Dream 37.0 16.5 185.0 \n", + "\n", + " body_mass_g sex \n", + "tag_number \n", + "1172 3050.0 FEMALE \n", + "1371 3075.0 FEMALE \n", + "1417 2900.0 FEMALE \n", + "1204 3725.0 MALE \n", + "1251 3600.0 FEMALE \n", + "1422 3350.0 FEMALE \n", + "1394 3450.0 FEMALE \n", + "1163 3325.0 FEMALE \n", + "1329 3175.0 FEMALE \n", + "1406 4000.0 MALE \n", + "1196 3150.0 FEMALE \n", + "1228 3950.0 MALE \n", + "1412 3250.0 FEMALE \n", + "1142 3550.0 FEMALE \n", + "1430 3600.0 FEMALE \n", + "1333 4775.0 MALE \n", + "1414 3450.0 FEMALE \n", + "1197 3425.0 MALE \n", + "1443 4000.0 MALE \n", + "1295 4725.0 MALE \n", + "1207 3950.0 MALE \n", + "1349 3900.0 MALE \n", + "1350 4250.0 MALE \n", + "1351 3825.0 FEMALE \n", + "1116 3400.0 FEMALE \n", + "...\n", + "\n", + "[146 rows x 6 columns]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# set a friendlier index to uniquely identify the rows\n", + "df = df.set_index(\"tag_number\")\n", + "\n", + "# filter down to the data we want to analyze\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# drop the columns we don't care about\n", + "adelie_data = adelie_data.drop(columns=[\"species\"])\n", + "\n", + "# drop rows with nulls to get our training data\n", + "training_data = adelie_data.dropna()\n", + "\n", + "# take a peek at the training data\n", + "training_data" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# pick feature columns and label column\n", + "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "label_columns = training_data[['body_mass_g']] \n", + "\n", + "# also get the rows that we want to make predictions for (i.e. where the feature column is null)\n", + "missing_body_mass = adelie_data[adelie_data.body_mass_g.isnull()]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Create, score, fit, predict" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "model = LinearRegression()\n", + "\n", + "# Here we pass the feature columns without transforms - BQML will then use\n", + "# automatic preprocessing to encode these columns\n", + "model.fit(feature_columns, label_columns)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", + "
[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 223.878763 78553.601634 0.005614 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 181.330911 0.623951 0.623951 \n", + "\n", + "[1 rows x 6 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# check how the model performed\n", + "model.score(feature_columns, label_columns)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_g
tag_number
13933459.735118
15253947.881639
15244304.175638
15233471.668379
\n", + "
[4 rows x 1 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g\n", + "tag_number \n", + "1393 3459.735118\n", + "1525 3947.881639\n", + "1524 4304.175638\n", + "1523 3471.668379\n", + "\n", + "[4 rows x 1 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# use the model to predict the missing labels\n", + "model.predict(missing_body_mass)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Save in BigQuery" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# save the model to a permanent location in BigQuery, so we can use it in future sessions (and elsewhere in BQ)\n", + "model.to_gbq(\"bqml_tutorial.penguins_model\", replace=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "a850322d07d9bdc9ec5f301d307e048bcab2390ae395e1cbce9335f4e081e5e2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/06 - Using ML - Large Language Models.ipynb b/notebooks/06 - Using ML - Large Language Models.ipynb new file mode 100644 index 00000000000..c2c0b83bef2 --- /dev/null +++ b/notebooks/06 - Using ML - Large Language Models.ipynb @@ -0,0 +1,201 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas\n", + "import pandas as pd\n", + "from bigframes.ml.llm import PaLM2TextGenerator" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "Create session and define a BQ connection which we already created and allowlisted. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "session = bigframes.pandas.get_global_session()\n", + "connection = \"bigframes-dev.us.bigframes-ml\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define the model" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "model = PaLM2TextGenerator(session=session, connection_name=connection)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Sample Data\n", + "\n", + "Read as a BigQuery DataFrames." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame(\n", + " {\n", + " \"prompt\": [\"What is BigQuery?\", \"What is BQML?\", \"What is BigQuery DataFrame?\"],\n", + " })\n", + "bf_df = bigframes.pandas.read_pandas(df)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Make Predictions" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ml_generate_text_llm_result
0BigQuery is a fully managed, petabyte-scale an...
1BQML stands for BigQuery Machine Learning. It ...
2A BigQuery DataFrames is a distributed collecti...
\n", + "
" + ], + "text/plain": [ + " ml_generate_text_llm_result\n", + "0 BigQuery is a fully managed, petabyte-scale an...\n", + "1 BQML stands for BigQuery Machine Learning. It ...\n", + "2 A BigQuery DataFrames is a distributed collecti..." + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pred = model.predict(bf_df).compute()\n", + "pred" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fetch Predictions" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'BigQuery is a fully managed, petabyte-scale analytics data warehouse that enables businesses to analyze all their data very quickly. It is a cloud-based service that offers a pay-as-you-go pricing model. BigQuery is designed to handle large amounts of data and provide fast performance. It is a good choice for businesses that need to analyze large amounts of data quickly and easily.'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pred.iloc[0, 0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/50 - Remote Function.ipynb b/notebooks/50 - Remote Function.ipynb new file mode 100644 index 00000000000..76c87f8629c --- /dev/null +++ b/notebooks/50 - Remote Function.ipynb @@ -0,0 +1,839 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "3613b1cd", + "metadata": {}, + "outputs": [], + "source": [ + "TABLE='bigquery-public-data.stackoverflow.comments'\n", + "MAX_ROWS=1000000 # 1 Million" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f1175247", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 30.5 s, sys: 4.25 s, total: 34.8 s\n", + "Wall time: 1min 49s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idtextscore
010It will help if you give some details of which...6
125infact it does. Look a the first lines of your...10
227\"Currently + is implemented using StringBuffer...7
341I don't think that's the magic number he was r...18
459It's still very useful to know that magic numb...12
596This implementation is also nice if you wish t...9
6108That's not full text searching, it's searching...6
7109That's not full text searching, it's searching...6
8137In vim you can open > 1 buffer. :e filename. T...9
9154Sure, but what about a solution using O(1) mem...8
\n", + "
" + ], + "text/plain": [ + " id text score\n", + "0 10 It will help if you give some details of which... 6\n", + "1 25 infact it does. Look a the first lines of your... 10\n", + "2 27 \"Currently + is implemented using StringBuffer... 7\n", + "3 41 I don't think that's the magic number he was r... 18\n", + "4 59 It's still very useful to know that magic numb... 12\n", + "5 96 This implementation is also nice if you wish t... 9\n", + "6 108 That's not full text searching, it's searching... 6\n", + "7 109 That's not full text searching, it's searching... 6\n", + "8 137 In vim you can open > 1 buffer. :e filename. T... 9\n", + "9 154 Sure, but what about a solution using O(1) mem... 8" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "\n", + "##############################\n", + "# Pandas World #\n", + "##############################\n", + "\n", + "import pandas as pd\n", + "df = pd.read_gbq(TABLE, max_results=MAX_ROWS)[['id', 'text', 'score']]\n", + "df.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "fd8a04a3", + "metadata": {}, + "outputs": [], + "source": [ + "# User defined function\n", + "# https://www.codespeedy.com/find-nth-prime-number-in-python/\n", + "def nth_prime(n):\n", + " prime_numbers = [2,3]\n", + " i=3\n", + " if(02):\n", + " while (True):\n", + " i+=1\n", + " status = True\n", + " for j in range(2,int(i/2)+1):\n", + " if(i%j==0):\n", + " status = False\n", + " break\n", + " if(status==True):\n", + " prime_numbers.append(i)\n", + " if(len(prime_numbers)==n):\n", + " break\n", + " return prime_numbers[n-1]\n", + " else:\n", + " return -1" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2b5e4568", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 5.48 s, sys: 27.6 ms, total: 5.51 s\n", + "Wall time: 5.49 s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idtextscoren_prime
010It will help if you give some details of which...613
125infact it does. Look a the first lines of your...1029
227\"Currently + is implemented using StringBuffer...717
341I don't think that's the magic number he was r...1861
459It's still very useful to know that magic numb...1237
596This implementation is also nice if you wish t...923
6108That's not full text searching, it's searching...613
7109That's not full text searching, it's searching...613
8137In vim you can open > 1 buffer. :e filename. T...923
9154Sure, but what about a solution using O(1) mem...819
\n", + "
" + ], + "text/plain": [ + " id text score n_prime\n", + "0 10 It will help if you give some details of which... 6 13\n", + "1 25 infact it does. Look a the first lines of your... 10 29\n", + "2 27 \"Currently + is implemented using StringBuffer... 7 17\n", + "3 41 I don't think that's the magic number he was r... 18 61\n", + "4 59 It's still very useful to know that magic numb... 12 37\n", + "5 96 This implementation is also nice if you wish t... 9 23\n", + "6 108 That's not full text searching, it's searching... 6 13\n", + "7 109 That's not full text searching, it's searching... 6 13\n", + "8 137 In vim you can open > 1 buffer. :e filename. T... 9 23\n", + "9 154 Sure, but what about a solution using O(1) mem... 8 19" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "\n", + "df = df.assign(n_prime=df['score'].apply(nth_prime))\n", + "df.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b81feaef", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 835 ms, sys: 62.5 ms, total: 897 ms\n", + "Wall time: 24.1 s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idtextscore
035153602Are you trying to access a nested array?0
135156124Sorry I didn't include my timeout method befor...0
235157401As soon as I defined some sort of primary Key ...0
335158649@user3355243 I've edited it to give $values an...0
435162039I pasted my exes @Matt.0
535162396@Gene - I do have separate fields, but I also ...0
635162907could you please provide any kind of sketch ho...0
735166498We use PhoneGap. Using Angular.0
835170138`decltype((int(Foo::*)(int))(&Foo::foo))` That...0
935172348Yes, I found that one already. And I understan...0
\n", + "
[10 rows x 3 columns in total]" + ], + "text/plain": [ + " id text score\n", + "0 35153602 Are you trying to access a nested array? 0\n", + "1 35156124 Sorry I didn't include my timeout method befor... 0\n", + "2 35157401 As soon as I defined some sort of primary Key ... 0\n", + "3 35158649 @user3355243 I've edited it to give $values an... 0\n", + "4 35162039 I pasted my exes @Matt. 0\n", + "5 35162396 @Gene - I do have separate fields, but I also ... 0\n", + "6 35162907 could you please provide any kind of sketch ho... 0\n", + "7 35166498 We use PhoneGap. Using Angular. 0\n", + "8 35170138 `decltype((int(Foo::*)(int))(&Foo::foo))` That... 0\n", + "9 35172348 Yes, I found that one already. And I understan... 0\n", + "\n", + "[10 rows x 3 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "\n", + "##############################\n", + "# BigQuery DataFrames #\n", + "##############################\n", + "\n", + "import bigframes.pandas as pd\n", + "\n", + "df = pd.read_gbq(TABLE).head(MAX_ROWS)[['id', 'text', 'score']]\n", + "df.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "55ed241e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function remote_function in module bigframes.pandas:\n", + "\n", + "remote_function(input_types: 'List[type]', output_type: 'type', dataset: 'Optional[str]' = None, bigquery_connection: 'Optional[str]' = None, reuse: 'bool' = True)\n", + " Decorator to turn a user defined function into a BigQuery remote function.\n", + " \n", + " Args:\n", + " input_types : list(type)\n", + " List of input data types in the user defined function.\n", + " output_type : type\n", + " Data type of the output in the user defined function.\n", + " dataset : str, Optional\n", + " Dataset to use to create a BigQuery function. It should be in\n", + " `.` or `` format. If this\n", + " param is not provided then session dataset id would be used.\n", + " bigquery_connection : str, Optional\n", + " Name of the BigQuery connection. If it is pre created in the same\n", + " location as the `bigquery_client.location` then it would be used,\n", + " otherwise it would be created dynamically assuming the user has\n", + " necessary priviliges. If this param is not provided then the\n", + " bigquery connection from the session would be used.\n", + " reuse : bool, Optional\n", + " Reuse the remote function if already exists.\n", + " `True` by default, which will result in reusing an existing remote\n", + " function (if any) that was previously created for the same udf.\n", + " Setting it to false would force creating a unique remote function.\n", + " If the required remote function does not exist then it would be\n", + " created irrespective of this param.\n", + " \n", + " Notes:\n", + " Please make sure following is setup before using this API:\n", + " \n", + " 1. Have the below APIs enabled for your project:\n", + " a. BigQuery Connection API\n", + " b. Cloud Functions API\n", + " c. Cloud Run API\n", + " d. Cloud Build API\n", + " e. Artifact Registry API\n", + " f. Cloud Resource Manager API\n", + " \n", + " This can be done from the cloud console (change PROJECT_ID to yours):\n", + " https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID\n", + " Or from the gcloud CLI:\n", + " $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com\n", + " \n", + " 2. Have following IAM roles enabled for you:\n", + " a. BigQuery Data Editor (roles/bigquery.dataEditor)\n", + " b. BigQuery Connection Admin (roles/bigquery.connectionAdmin)\n", + " c. Cloud Functions Developer (roles/cloudfunctions.developer)\n", + " d. Service Account User (roles/iam.serviceAccountUser)\n", + " e. Storage Object Viewer (roles/storage.objectViewer)\n", + " f. Project IAM Admin (roles/resourcemanager.projectIamAdmin)\n", + " (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.)\n", + " \n", + " 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set:\n", + " a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection\n", + " b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function\n", + " Alternatively, the IAM could also be setup via the gcloud CLI:\n", + " $ gcloud projects add-iam-policy-binding PROJECT_ID --member=\"serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID\" --role=\"roles/run.invoker\"\n", + "\n" + ] + } + ], + "source": [ + "# Tell the user what needs to be done offline before using BigQuery DataFrame\n", + "# remote functions\n", + "help(pd.remote_function)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c9a8d03d", + "metadata": {}, + "outputs": [], + "source": [ + "# BigQuery DataFrames user is a data scientist and may not have privileges to\n", + "# create a BQ connector and set it up for invoking a cloud function. They\n", + "# should get such a connector created from their cloud admin and use it with\n", + "# BigQuery DataFrames remote functions. If the provided connection name does not\n", + "# exist, BigQuery DataFrames will try to create it on the fly assuming the user\n", + "# has sufficient privileges.\n", + "bq_connection_name = 'bigframes-rf-conn'" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "fbc27f81", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO][2023-06-29 01:03:30,557][bigframes.remote_function] Creating new cloud function: gcloud functions deploy bigframes-ac72c931423f68dddb4f84f6754e2b28 --gen2 --runtime=python310 --project=bigframes-dev --region=us-central1 --source=/tmp/tmposspvw3v --entry-point=udf_http --trigger-http --no-allow-unauthenticated\n", + "Preparing function...\n", + ".done.\n", + "Deploying function...\n", + "[Build]......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................done\n", + "[Service]....................................................................................................done\n", + "Done.\n", + "You can view your function in the Cloud Console here: https://console.cloud.google.com/functions/details/us-central1/bigframes-ac72c931423f68dddb4f84f6754e2b28?project=bigframes-dev\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "buildConfig:\n", + " build: projects/1084210331973/locations/us-central1/builds/57859622-4287-47bc-ab86-3cd3f4c47548\n", + " entryPoint: udf_http\n", + " runtime: python310\n", + " source:\n", + " storageSource:\n", + " bucket: gcf-v2-sources-1084210331973-us-central1\n", + " object: bigframes-ac72c931423f68dddb4f84f6754e2b28/function-source.zip\n", + " sourceProvenance:\n", + " resolvedStorageSource:\n", + " bucket: gcf-v2-sources-1084210331973-us-central1\n", + " generation: '1688000614202977'\n", + " object: bigframes-ac72c931423f68dddb4f84f6754e2b28/function-source.zip\n", + "environment: GEN_2\n", + "labels:\n", + " deployment-tool: cli-gcloud\n", + "name: projects/bigframes-dev/locations/us-central1/functions/bigframes-ac72c931423f68dddb4f84f6754e2b28\n", + "serviceConfig:\n", + " allTrafficOnLatestRevision: true\n", + " availableCpu: '0.1666'\n", + " availableMemory: 256M\n", + " ingressSettings: ALLOW_ALL\n", + " maxInstanceCount: 100\n", + " maxInstanceRequestConcurrency: 1\n", + " revision: bigframes-ac72c931423f68dddb4f84f6754e2b28-00001-faf\n", + " service: projects/bigframes-dev/locations/us-central1/services/bigframes-ac72c931423f68dddb4f84f6754e2b28\n", + " serviceAccountEmail: 1084210331973-compute@developer.gserviceaccount.com\n", + " timeoutSeconds: 60\n", + " uri: https://bigframes-ac72c931423f68dddb4f84f6754e2b28-7krlje3eoq-uc.a.run.app\n", + "state: ACTIVE\n", + "updateTime: '2023-06-29T01:04:34.306070995Z'\n", + "url: https://us-central1-bigframes-dev.cloudfunctions.net/bigframes-ac72c931423f68dddb4f84f6754e2b28\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO][2023-06-29 01:04:37,239][bigframes.remote_function] Successfully created cloud function bigframes-ac72c931423f68dddb4f84f6754e2b28 with uri (https://bigframes-ac72c931423f68dddb4f84f6754e2b28-7krlje3eoq-uc.a.run.app)\n", + "[INFO][2023-06-29 01:04:42,402][bigframes.remote_function] Connector bigframes-rf-conn already exists\n", + "[INFO][2023-06-29 01:04:42,404][bigframes.remote_function] Creating BQ remote function: \n", + " CREATE OR REPLACE FUNCTION `bigframes-dev.bigframes_temp_us`.bigframes_ac72c931423f68dddb4f84f6754e2b28(n INT64)\n", + " RETURNS INT64\n", + " REMOTE WITH CONNECTION `bigframes-dev.us.bigframes-rf-conn`\n", + " OPTIONS (\n", + " endpoint = \"https://bigframes-ac72c931423f68dddb4f84f6754e2b28-7krlje3eoq-uc.a.run.app\"\n", + " )\n", + "[INFO][2023-06-29 01:04:43,456][bigframes.remote_function] Created remote function bigframes-dev.bigframes_temp_us.bigframes_ac72c931423f68dddb4f84f6754e2b28\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Wall time: 76.2659 s\n" + ] + } + ], + "source": [ + "from time import perf_counter\n", + "start = perf_counter()\n", + "#-------------------------------------------------------------------------------------\n", + "\n", + "# User defined function\n", + "# https://www.codespeedy.com/find-nth-prime-number-in-python/\n", + "@pd.remote_function([int], int, bigquery_connection=bq_connection_name)\n", + "def nth_prime(n):\n", + " prime_numbers = [2,3]\n", + " i=3\n", + " if(02):\n", + " while (True):\n", + " i+=1\n", + " status = True\n", + " for j in range(2,int(i/2)+1):\n", + " if(i%j==0):\n", + " status = False\n", + " break\n", + " if(status==True):\n", + " prime_numbers.append(i)\n", + " if(len(prime_numbers)==n):\n", + " break\n", + " return prime_numbers[n-1]\n", + " else:\n", + " return -1\n", + "\n", + "#-------------------------------------------------------------------------------------\n", + "print(f\"\\nWall time: {(perf_counter()-start):.4f} s\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c1c9355f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 22.2 ms, sys: 0 ns, total: 22.2 ms\n", + "Wall time: 22.4 ms\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idtextscoren_prime
035153602Are you trying to access a nested array?0-1
135156124Sorry I didn't include my timeout method befor...0-1
235157401As soon as I defined some sort of primary Key ...0-1
335158649@user3355243 I've edited it to give $values an...0-1
435162039I pasted my exes @Matt.0-1
535162396@Gene - I do have separate fields, but I also ...0-1
635162907could you please provide any kind of sketch ho...0-1
735166498We use PhoneGap. Using Angular.0-1
835170138`decltype((int(Foo::*)(int))(&Foo::foo))` That...0-1
935172348Yes, I found that one already. And I understan...0-1
\n", + "
[10 rows x 4 columns in total]" + ], + "text/plain": [ + " id text score n_prime\n", + "0 35153602 Are you trying to access a nested array? 0 -1\n", + "1 35156124 Sorry I didn't include my timeout method befor... 0 -1\n", + "2 35157401 As soon as I defined some sort of primary Key ... 0 -1\n", + "3 35158649 @user3355243 I've edited it to give $values an... 0 -1\n", + "4 35162039 I pasted my exes @Matt. 0 -1\n", + "5 35162396 @Gene - I do have separate fields, but I also ... 0 -1\n", + "6 35162907 could you please provide any kind of sketch ho... 0 -1\n", + "7 35166498 We use PhoneGap. Using Angular. 0 -1\n", + "8 35170138 `decltype((int(Foo::*)(int))(&Foo::foo))` That... 0 -1\n", + "9 35172348 Yes, I found that one already. And I understan... 0 -1\n", + "\n", + "[10 rows x 4 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "\n", + "df = df.assign(n_prime=df['score'].apply(nth_prime))\n", + "df.head(10)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/99 - Longer ML demo.ipynb b/notebooks/99 - Longer ML demo.ipynb new file mode 100644 index 00000000000..c4d133421f6 --- /dev/null +++ b/notebooks/99 - Longer ML demo.ipynb @@ -0,0 +1,1925 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "71fbfc47", + "metadata": {}, + "source": [ + "**Note: this notebook requires changes not yet checked in**\n", + "\n", + "# Introduction\n", + "\n", + "This is a prototype for how a minimal SKLearn-like wrapper for BQML might work in BigQuery DataFrames.\n", + "\n", + "Disclaimer - this is not a polished design or a robust implementation, this is a quick prototype to workshop some ideas. Design will be next.\n", + "\n", + "What is BigQuery DataFrame?\n", + "- Pandas API for BigQuery\n", + "- Lets data scientists quickly iterate and prepare their data as they do in Pandas, but executed by BigQuery\n", + "\n", + "What is meant by SKLearn-like?\n", + "- Follow the API design practices from the SKLearn project\n", + " - [API design for machine learning software: experiences from the scikit-learn project](https://arxiv.org/pdf/1309.0238.pdf)\n", + "- Not a copy of, or compatible with, SKLearn\n", + "\n", + "Briefly, patterns taken from SKLearn are:\n", + "- Models and transforms are 'Estimators'\n", + " - A bundle of parameters with a consistent way to initialize/get/set\n", + " - And a .fit(..) method to fit to training data\n", + "- Models additionally have a .predict(..)\n", + "- By default, these objects are transient, making them easy to play around with. No need to give them names or decide how to persist them.\n", + "\n", + "\n", + "Design goals:\n", + "- Zero friction ML capabilities for BigQuery DataFrames users (no extra auth, configuration, etc)\n", + "- Offers first class integration with the Pandas-like BigQuery DataFrames API\n", + "- Uses SKLearn-like design patterns that feel familiar to data scientists\n", + "- Also a first class BigQuery experience\n", + " - Offers BigQuery's scalability and storage / compute management\n", + " - Works naturally with BigQuery's other interfaces, e.g. GUI and SQL\n", + " - BQML features" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "345c2163", + "metadata": {}, + "source": [ + "# Linear regression tutorial\n", + "\n", + "Adapted from the \"Penguin weight\" Linear Regression tutorial for BQML: https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "03c9e168", + "metadata": {}, + "source": [ + "## Setting the scene\n", + "\n", + "Our conservationists have sent us some measurements of penguins found in the Antarctic islands. They say that some of the body mass measurements for the Adelie penguins are missing, and ask if we can use some data science magic to estimate them. Sounds like a job for a linear regression!\n", + "\n", + "Lets take a look at the data..." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7a03de2-c0ef-4f80-9cd5-f96e87cf2d54", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tag_numberspeciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
01225Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
11278Gentoo penguin (Pygoscelis papua)Biscoe42.013.5210.04150.0FEMALE
21275Gentoo penguin (Pygoscelis papua)Biscoe46.513.5210.04550.0FEMALE
31233Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
41311Gentoo penguin (Pygoscelis papua)Biscoe47.514.0212.04875.0FEMALE
51316Gentoo penguin (Pygoscelis papua)Biscoe49.114.5212.04625.0FEMALE
61313Gentoo penguin (Pygoscelis papua)Biscoe45.514.5212.04750.0FEMALE
71381Gentoo penguin (Pygoscelis papua)Biscoe47.614.5215.05400.0MALE
81377Gentoo penguin (Pygoscelis papua)Biscoe45.114.5207.05050.0FEMALE
91380Gentoo penguin (Pygoscelis papua)Biscoe45.114.5215.05000.0FEMALE
101257Gentoo penguin (Pygoscelis papua)Biscoe46.214.5209.04800.0FEMALE
111336Gentoo penguin (Pygoscelis papua)Biscoe46.514.5213.04400.0FEMALE
121237Gentoo penguin (Pygoscelis papua)Biscoe43.214.5208.04450.0FEMALE
131302Gentoo penguin (Pygoscelis papua)Biscoe48.515.0219.04850.0FEMALE
141325Gentoo penguin (Pygoscelis papua)Biscoe49.115.0228.05500.0MALE
151285Gentoo penguin (Pygoscelis papua)Biscoe47.515.0218.04950.0FEMALE
161242Gentoo penguin (Pygoscelis papua)Biscoe49.615.0216.04750.0MALE
171246Gentoo penguin (Pygoscelis papua)Biscoe47.715.0216.04750.0FEMALE
181320Gentoo penguin (Pygoscelis papua)Biscoe45.515.0220.05000.0MALE
191244Gentoo penguin (Pygoscelis papua)Biscoe46.415.0216.04700.0FEMALE
\n", + "
[347 rows x 8 columns in total]" + ], + "text/plain": [ + " tag_number species island culmen_length_mm \\\n", + "0 1225 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "1 1278 Gentoo penguin (Pygoscelis papua) Biscoe 42.0 \n", + "2 1275 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "3 1233 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", + "4 1311 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "5 1316 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "6 1313 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "7 1381 Gentoo penguin (Pygoscelis papua) Biscoe 47.6 \n", + "8 1377 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "9 1380 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "10 1257 Gentoo penguin (Pygoscelis papua) Biscoe 46.2 \n", + "11 1336 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "12 1237 Gentoo penguin (Pygoscelis papua) Biscoe 43.2 \n", + "13 1302 Gentoo penguin (Pygoscelis papua) Biscoe 48.5 \n", + "14 1325 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "15 1285 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "16 1242 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "17 1246 Gentoo penguin (Pygoscelis papua) Biscoe 47.7 \n", + "18 1320 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "19 1244 Gentoo penguin (Pygoscelis papua) Biscoe 46.4 \n", + "20 1390 Gentoo penguin (Pygoscelis papua) Biscoe 50.7 \n", + "21 1379 Gentoo penguin (Pygoscelis papua) Biscoe 47.8 \n", + "22 1267 Gentoo penguin (Pygoscelis papua) Biscoe 50.1 \n", + "23 1389 Gentoo penguin (Pygoscelis papua) Biscoe 47.2 \n", + "24 1269 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 \n", + "1 13.5 210.0 4150.0 FEMALE \n", + "2 13.5 210.0 4550.0 FEMALE \n", + "3 14.0 208.0 4575.0 FEMALE \n", + "4 14.0 212.0 4875.0 FEMALE \n", + "5 14.5 212.0 4625.0 FEMALE \n", + "6 14.5 212.0 4750.0 FEMALE \n", + "7 14.5 215.0 5400.0 MALE \n", + "8 14.5 207.0 5050.0 FEMALE \n", + "9 14.5 215.0 5000.0 FEMALE \n", + "10 14.5 209.0 4800.0 FEMALE \n", + "11 14.5 213.0 4400.0 FEMALE \n", + "12 14.5 208.0 4450.0 FEMALE \n", + "13 15.0 219.0 4850.0 FEMALE \n", + "14 15.0 228.0 5500.0 MALE \n", + "15 15.0 218.0 4950.0 FEMALE \n", + "16 15.0 216.0 4750.0 MALE \n", + "17 15.0 216.0 4750.0 FEMALE \n", + "18 15.0 220.0 5000.0 MALE \n", + "19 15.0 216.0 4700.0 FEMALE \n", + "20 15.0 223.0 5550.0 MALE \n", + "21 15.0 215.0 5650.0 MALE \n", + "22 15.0 225.0 5000.0 MALE \n", + "23 15.5 215.0 4975.0 FEMALE \n", + "24 16.0 225.0 5700.0 MALE \n", + "...\n", + "\n", + "[347 rows x 8 columns]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import bigframes.pandas\n", + "\n", + "df = bigframes.pandas.read_gbq(\"bigframes-dev.bqml_tutorial.penguins\")\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "359524c4", + "metadata": {}, + "source": [ + "First we note that while we have a default numbered index generated by BigQuery, actually the penguins are uniquely identified by their tags.\n", + "\n", + "Lets make the data a bit friendlier to work with by setting the tag number column as the index." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "93d01411", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
tag_number
1225Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
1278Gentoo penguin (Pygoscelis papua)Biscoe42.013.5210.04150.0FEMALE
1275Gentoo penguin (Pygoscelis papua)Biscoe46.513.5210.04550.0FEMALE
1233Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
1311Gentoo penguin (Pygoscelis papua)Biscoe47.514.0212.04875.0FEMALE
1316Gentoo penguin (Pygoscelis papua)Biscoe49.114.5212.04625.0FEMALE
1313Gentoo penguin (Pygoscelis papua)Biscoe45.514.5212.04750.0FEMALE
1381Gentoo penguin (Pygoscelis papua)Biscoe47.614.5215.05400.0MALE
1377Gentoo penguin (Pygoscelis papua)Biscoe45.114.5207.05050.0FEMALE
1380Gentoo penguin (Pygoscelis papua)Biscoe45.114.5215.05000.0FEMALE
1257Gentoo penguin (Pygoscelis papua)Biscoe46.214.5209.04800.0FEMALE
1336Gentoo penguin (Pygoscelis papua)Biscoe46.514.5213.04400.0FEMALE
1237Gentoo penguin (Pygoscelis papua)Biscoe43.214.5208.04450.0FEMALE
1302Gentoo penguin (Pygoscelis papua)Biscoe48.515.0219.04850.0FEMALE
1325Gentoo penguin (Pygoscelis papua)Biscoe49.115.0228.05500.0MALE
1285Gentoo penguin (Pygoscelis papua)Biscoe47.515.0218.04950.0FEMALE
1242Gentoo penguin (Pygoscelis papua)Biscoe49.615.0216.04750.0MALE
1246Gentoo penguin (Pygoscelis papua)Biscoe47.715.0216.04750.0FEMALE
1320Gentoo penguin (Pygoscelis papua)Biscoe45.515.0220.05000.0MALE
1244Gentoo penguin (Pygoscelis papua)Biscoe46.415.0216.04700.0FEMALE
\n", + "
[347 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "tag_number \n", + "1225 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "1278 Gentoo penguin (Pygoscelis papua) Biscoe 42.0 \n", + "1275 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "1233 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", + "1311 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "1316 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "1313 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "1381 Gentoo penguin (Pygoscelis papua) Biscoe 47.6 \n", + "1377 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "1380 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "1257 Gentoo penguin (Pygoscelis papua) Biscoe 46.2 \n", + "1336 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "1237 Gentoo penguin (Pygoscelis papua) Biscoe 43.2 \n", + "1302 Gentoo penguin (Pygoscelis papua) Biscoe 48.5 \n", + "1325 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", + "1285 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", + "1242 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "1246 Gentoo penguin (Pygoscelis papua) Biscoe 47.7 \n", + "1320 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "1244 Gentoo penguin (Pygoscelis papua) Biscoe 46.4 \n", + "1390 Gentoo penguin (Pygoscelis papua) Biscoe 50.7 \n", + "1379 Gentoo penguin (Pygoscelis papua) Biscoe 47.8 \n", + "1267 Gentoo penguin (Pygoscelis papua) Biscoe 50.1 \n", + "1389 Gentoo penguin (Pygoscelis papua) Biscoe 47.2 \n", + "1269 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "tag_number \n", + "1225 \n", + "1278 13.5 210.0 4150.0 FEMALE \n", + "1275 13.5 210.0 4550.0 FEMALE \n", + "1233 14.0 208.0 4575.0 FEMALE \n", + "1311 14.0 212.0 4875.0 FEMALE \n", + "1316 14.5 212.0 4625.0 FEMALE \n", + "1313 14.5 212.0 4750.0 FEMALE \n", + "1381 14.5 215.0 5400.0 MALE \n", + "1377 14.5 207.0 5050.0 FEMALE \n", + "1380 14.5 215.0 5000.0 FEMALE \n", + "1257 14.5 209.0 4800.0 FEMALE \n", + "1336 14.5 213.0 4400.0 FEMALE \n", + "1237 14.5 208.0 4450.0 FEMALE \n", + "1302 15.0 219.0 4850.0 FEMALE \n", + "1325 15.0 228.0 5500.0 MALE \n", + "1285 15.0 218.0 4950.0 FEMALE \n", + "1242 15.0 216.0 4750.0 MALE \n", + "1246 15.0 216.0 4750.0 FEMALE \n", + "1320 15.0 220.0 5000.0 MALE \n", + "1244 15.0 216.0 4700.0 FEMALE \n", + "1390 15.0 223.0 5550.0 MALE \n", + "1379 15.0 215.0 5650.0 MALE \n", + "1267 15.0 225.0 5000.0 MALE \n", + "1389 15.5 215.0 4975.0 FEMALE \n", + "1269 16.0 225.0 5700.0 MALE \n", + "...\n", + "\n", + "[347 rows x 7 columns]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.set_index(\"tag_number\")\n", + "df" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f95fda6a", + "metadata": {}, + "source": [ + "We saw in the first view that there were some missing values. We're especially interested in observations that are missing just the body_mass_g, so lets look at those:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "941cb6c3-8c54-42ce-a945-4fa604176b2e", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
tag_number
1225Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
1393Adelie Penguin (Pygoscelis adeliae)Torgersen<NA><NA><NA><NA><NA>
1524Adelie Penguin (Pygoscelis adeliae)Dream41.620.0204.0<NA>MALE
1523Adelie Penguin (Pygoscelis adeliae)Dream38.017.5194.0<NA>FEMALE
1525Adelie Penguin (Pygoscelis adeliae)Dream36.318.5194.0<NA>MALE
\n", + "
[5 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "tag_number \n", + "1225 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "1393 Adelie Penguin (Pygoscelis adeliae) Torgersen \n", + "1524 Adelie Penguin (Pygoscelis adeliae) Dream 41.6 \n", + "1523 Adelie Penguin (Pygoscelis adeliae) Dream 38.0 \n", + "1525 Adelie Penguin (Pygoscelis adeliae) Dream 36.3 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "tag_number \n", + "1225 \n", + "1393 \n", + "1524 20.0 204.0 MALE \n", + "1523 17.5 194.0 FEMALE \n", + "1525 18.5 194.0 MALE \n", + "\n", + "[5 rows x 7 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[df.body_mass_g.isnull()]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a70c2027", + "metadata": {}, + "source": [ + "Here we see three Adelie penguins with tag numbers 1523, 1524, 1525 are missing their body_mass_g but have the other measurements. These are the ones we need to guess. We can do this by training a statistical model on the measurements that we do have, and then using it to predict the missing values.\n", + "\n", + "Our conservationists warned us that trying to generalize across species is a bad idea, so for now lets just try building a model for Adelie penguins. We can revisit it later and see if including the other observations improves the model performance." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "93ff013a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
tag_number
1172Adelie Penguin (Pygoscelis adeliae)Dream32.115.5188.03050.0FEMALE
1371Adelie Penguin (Pygoscelis adeliae)Biscoe37.716.0183.03075.0FEMALE
1417Adelie Penguin (Pygoscelis adeliae)Torgersen38.617.0188.02900.0FEMALE
1204Adelie Penguin (Pygoscelis adeliae)Dream40.717.0190.03725.0MALE
1251Adelie Penguin (Pygoscelis adeliae)Biscoe37.617.0185.03600.0FEMALE
1422Adelie Penguin (Pygoscelis adeliae)Torgersen35.717.0189.03350.0FEMALE
1394Adelie Penguin (Pygoscelis adeliae)Torgersen40.217.0176.03450.0FEMALE
1163Adelie Penguin (Pygoscelis adeliae)Dream36.417.0195.03325.0FEMALE
1329Adelie Penguin (Pygoscelis adeliae)Biscoe38.117.0181.03175.0FEMALE
1406Adelie Penguin (Pygoscelis adeliae)Torgersen44.118.0210.04000.0MALE
1196Adelie Penguin (Pygoscelis adeliae)Dream36.518.0182.03150.0FEMALE
1228Adelie Penguin (Pygoscelis adeliae)Biscoe41.618.0192.03950.0MALE
1412Adelie Penguin (Pygoscelis adeliae)Torgersen40.318.0195.03250.0FEMALE
1142Adelie Penguin (Pygoscelis adeliae)Dream35.718.0202.03550.0FEMALE
1430Adelie Penguin (Pygoscelis adeliae)Torgersen33.519.0190.03600.0FEMALE
1333Adelie Penguin (Pygoscelis adeliae)Biscoe43.219.0197.04775.0MALE
1414Adelie Penguin (Pygoscelis adeliae)Torgersen38.719.0195.03450.0FEMALE
1197Adelie Penguin (Pygoscelis adeliae)Dream41.119.0182.03425.0MALE
1443Adelie Penguin (Pygoscelis adeliae)Torgersen40.619.0199.04000.0MALE
1295Adelie Penguin (Pygoscelis adeliae)Biscoe41.020.0203.04725.0MALE
\n", + "
[146 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "tag_number \n", + "1172 Adelie Penguin (Pygoscelis adeliae) Dream 32.1 \n", + "1371 Adelie Penguin (Pygoscelis adeliae) Biscoe 37.7 \n", + "1417 Adelie Penguin (Pygoscelis adeliae) Torgersen 38.6 \n", + "1204 Adelie Penguin (Pygoscelis adeliae) Dream 40.7 \n", + "1251 Adelie Penguin (Pygoscelis adeliae) Biscoe 37.6 \n", + "1422 Adelie Penguin (Pygoscelis adeliae) Torgersen 35.7 \n", + "1394 Adelie Penguin (Pygoscelis adeliae) Torgersen 40.2 \n", + "1163 Adelie Penguin (Pygoscelis adeliae) Dream 36.4 \n", + "1329 Adelie Penguin (Pygoscelis adeliae) Biscoe 38.1 \n", + "1406 Adelie Penguin (Pygoscelis adeliae) Torgersen 44.1 \n", + "1196 Adelie Penguin (Pygoscelis adeliae) Dream 36.5 \n", + "1228 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.6 \n", + "1412 Adelie Penguin (Pygoscelis adeliae) Torgersen 40.3 \n", + "1142 Adelie Penguin (Pygoscelis adeliae) Dream 35.7 \n", + "1430 Adelie Penguin (Pygoscelis adeliae) Torgersen 33.5 \n", + "1333 Adelie Penguin (Pygoscelis adeliae) Biscoe 43.2 \n", + "1414 Adelie Penguin (Pygoscelis adeliae) Torgersen 38.7 \n", + "1197 Adelie Penguin (Pygoscelis adeliae) Dream 41.1 \n", + "1443 Adelie Penguin (Pygoscelis adeliae) Torgersen 40.6 \n", + "1295 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.0 \n", + "1207 Adelie Penguin (Pygoscelis adeliae) Dream 38.8 \n", + "1349 Adelie Penguin (Pygoscelis adeliae) Biscoe 38.2 \n", + "1350 Adelie Penguin (Pygoscelis adeliae) Biscoe 37.8 \n", + "1351 Adelie Penguin (Pygoscelis adeliae) Biscoe 38.1 \n", + "1116 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "tag_number \n", + "1172 15.5 188.0 3050.0 FEMALE \n", + "1371 16.0 183.0 3075.0 FEMALE \n", + "1417 17.0 188.0 2900.0 FEMALE \n", + "1204 17.0 190.0 3725.0 MALE \n", + "1251 17.0 185.0 3600.0 FEMALE \n", + "1422 17.0 189.0 3350.0 FEMALE \n", + "1394 17.0 176.0 3450.0 FEMALE \n", + "1163 17.0 195.0 3325.0 FEMALE \n", + "1329 17.0 181.0 3175.0 FEMALE \n", + "1406 18.0 210.0 4000.0 MALE \n", + "1196 18.0 182.0 3150.0 FEMALE \n", + "1228 18.0 192.0 3950.0 MALE \n", + "1412 18.0 195.0 3250.0 FEMALE \n", + "1142 18.0 202.0 3550.0 FEMALE \n", + "1430 19.0 190.0 3600.0 FEMALE \n", + "1333 19.0 197.0 4775.0 MALE \n", + "1414 19.0 195.0 3450.0 FEMALE \n", + "1197 19.0 182.0 3425.0 MALE \n", + "1443 19.0 199.0 4000.0 MALE \n", + "1295 20.0 203.0 4725.0 MALE \n", + "1207 20.0 190.0 3950.0 MALE \n", + "1349 20.0 190.0 3900.0 MALE \n", + "1350 20.0 190.0 4250.0 MALE \n", + "1351 16.5 198.0 3825.0 FEMALE \n", + "1116 16.5 185.0 3400.0 FEMALE \n", + "...\n", + "\n", + "[146 rows x 7 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# get all the rows with adelie penguins\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# separate out the rows that have a body mass measurement\n", + "training_data = adelie_data[adelie_data.body_mass_g.notnull()]\n", + "\n", + "# we noticed there were also some rows that were missing other values,\n", + "# lets remove these so they don't affect our results\n", + "training_data = training_data.dropna()\n", + "\n", + "# lets take a quick peek and make sure things look right:\n", + "training_data" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d55a39f9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "species string[pyarrow]\n", + "island string[pyarrow]\n", + "culmen_length_mm Float64\n", + "culmen_depth_mm Float64\n", + "flipper_length_mm Float64\n", + "body_mass_g Float64\n", + "sex string[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# we'll look at the schema too:\n", + "training_data.dtypes" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "59d374b5", + "metadata": {}, + "source": [ + "Great! Now lets configure a linear regression model to predict body mass from the other columns" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "18c4cecf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import bigframes.ml.linear_model as ml\n", + "\n", + "model = ml.LinearRegression()\n", + "model" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6e54a1a2", + "metadata": {}, + "source": [ + "As in SKLearn, an unfitted model object is just a bundle of parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a2060cf1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'fit_intercept': True}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# lets view the parameters\n", + "model.get_params()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8e25fe41", + "metadata": {}, + "source": [ + "For this task, really all the default options are fine. But just so we can see how configuration works, lets specify that we want to use gradient descent to find the solution:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "327e2232", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.optimize_strategy = \"BATCH_GRADIENT_DESCENT\"\n", + "model" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2c2e0835", + "metadata": {}, + "source": [ + "BigQuery models provide a couple of extra conveniences:\n", + "\n", + "1. By default, they will automatically perform feature engineering on the inputs - encoding our string columns and scaling our numeric columns.\n", + "2. By default, they will also automatically manage the test/training data split for us.\n", + "\n", + "So all we need to do is hook our chosen feature and label columns into the model and call .fit()!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "085c9a99", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_x = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "train_y = training_data[['body_mass_g']]\n", + "model.fit(train_x, train_y)\n", + "model" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "9e76e10c", + "metadata": {}, + "source": [ + "...and there, we've successfully trained a linear regressor model. Lets see how it performs, using the automatic data split:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c9458c02", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", + "
[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 223.878763 78553.601634 0.005614 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 181.330911 0.623951 0.623951 \n", + "\n", + "[1 rows x 6 columns]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.score(train_x, train_y)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f0b39603", + "metadata": {}, + "source": [ + "Great! The model seems useful, predicting 62% of the variance.\n", + "\n", + "We realize we made a mistake though - we're trying to predict mass using a linear model, mass will increase with the cube of the penguin's size, whereas our inputs are linear with size. Can we improve our model by cubing them?" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b94eddc7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'\\ndef cubify(penguin_df):\\n penguin_df.culmen_length_mm = train_x.culmen_length_mm.pow(3)\\n penguin_df.culmen_depth_mm = train_x.culmen_depth_mm.pow(3)\\n penguin_df.flipper_length_mm = train_x.flipper_length_mm.pow(3)\\n\\ncubify(train_x)\\ntrain_x\\n'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# SKIP THIS STEP (not yet work working in BigQuery DataFrame)\n", + "\n", + "# lets define a preprocessing step that adjust the linear measurements to use the cube\n", + "'''\n", + "def cubify(penguin_df):\n", + " penguin_df.culmen_length_mm = train_x.culmen_length_mm.pow(3)\n", + " penguin_df.culmen_depth_mm = train_x.culmen_depth_mm.pow(3)\n", + " penguin_df.flipper_length_mm = train_x.flipper_length_mm.pow(3)\n", + "\n", + "cubify(train_x)\n", + "train_x\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1b0e3f02", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'\\nmodel.fit(train_x, train_y)\\nmodel.evaluate()\\n'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# AS ABOVE, SKIP FOR NOW\n", + "'''\n", + "model.fit(train_x, train_y)\n", + "model.evaluate()\n", + "'''" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "45c5e755", + "metadata": {}, + "source": [ + "Now that we're satisfied with our model, lets see what it predicts for those Adelie penguins with no body mass measurement:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f21ebc1f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_g
tag_number
13933459.735118
15244304.175638
15233471.668379
15253947.881639
\n", + "
[4 rows x 1 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g\n", + "tag_number \n", + "1393 3459.735118\n", + "1524 4304.175638\n", + "1523 3471.668379\n", + "1525 3947.881639\n", + "\n", + "[4 rows x 1 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Lets predict the missing observations\n", + "missing_body_mass = adelie_data[adelie_data.body_mass_g.isnull()]\n", + "\n", + "model.predict(missing_body_mass)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e66bd0b0", + "metadata": {}, + "source": [ + "Because we created it without a name, it was just a temporary model that will disappear after 24 hours. \n", + "\n", + "We decide that this approach is promising, so lets tell BigQuery to save it." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c508691b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.to_gbq(\"bqml_tutorial.penguins_model\", replace=True)\n", + "model" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "46abef08", + "metadata": {}, + "source": [ + "We can now use this model anywhere in BigQuery with this name. We can also load\n", + "it again in our BigQuery DataFrames session and evaluate or inference it without\n", + "needing to retrain it:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "0c87e972", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model = bigframes.pandas.read_gbq_model(\"bqml_tutorial.penguins_model\")\n", + "model" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d6ab8def", + "metadata": {}, + "source": [ + "And of course we can retrain it if we like. Lets make another version that is based on all the penguins, so we can test that assumption we made at the beginning that it would be best to separate them:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "f4960452", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0224.71743379527.8796230.005693169.2358690.6192870.619287
\n", + "
[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 224.717433 79527.879623 0.005693 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 169.235869 0.619287 0.619287 \n", + "\n", + "[1 rows x 6 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This time we'll take all the training data, for all species\n", + "training_data = df[df.body_mass_g.notnull()]\n", + "training_data = training_data.dropna()\n", + "\n", + "# And we'll include species in our features\n", + "train_x = training_data[['species', 'island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "train_y = training_data[['body_mass_g']]\n", + "model.fit(train_x, train_y)\n", + "\n", + "# And we'll evaluate it on the Adelie penguins only\n", + "adelie_data = training_data[training_data.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "test_x = adelie_data[['species', 'island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "test_y = adelie_data[['body_mass_g']]\n", + "model.score(test_x, test_y)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7d101140", + "metadata": {}, + "source": [ + "It looks like the conservationists were right! Including other species, even though it gave us more training data, worsened prediction on the Adelie penguins." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7f3fe50d", + "metadata": {}, + "source": [ + "===============================================\n", + "\n", + "**Everything below this line not yet implemented**" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "62577c72", + "metadata": {}, + "source": [ + "We want to productionalize this model, so lets start publishing it to the vertex model registry ([prerequisites](https://cloud.google.com/bigquery-ml/docs/managing-models-vertex#prerequisites))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b82e79ee", + "metadata": {}, + "outputs": [], + "source": [ + "model.publish(\n", + " registry=\"vertex_ai\",\n", + " vertex_ai_model_version_aliases=[\"experimental\"])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "69d2482c", + "metadata": {}, + "source": [ + "Now when we fit the model, we can see it published here: https://console.cloud.google.com/vertex-ai/models" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b97d9b64", + "metadata": {}, + "source": [ + "# Custom feature engineering" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c837ace9", + "metadata": {}, + "source": [ + "So far, we've relied on BigQuery to do our feature engineering for us. What if we want to do it manually?\n", + "\n", + "BigQuery DataFrames provides a way to do this using Pipelines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "480cb12f", + "metadata": {}, + "outputs": [], + "source": [ + "from bigframes.ml.pipeline import Pipeline\n", + "from bigframes.ml.preprocessing import StandardScaler\n", + "\n", + "pipe = Pipeline([\n", + " ('scaler', StandardScaler()),\n", + " ('linreg', LinearRegression())\n", + "])\n", + "\n", + "pipe.fit(train_x, train_y)\n", + "pipe.evaluate()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "9a0e7d19", + "metadata": {}, + "source": [ + "We then can then save the entire pipeline to BigQuery, BigQuery will save this as a single model, with the pre-processing steps embedded in the TRANSFORM property:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d1831ed", + "metadata": {}, + "outputs": [], + "source": [ + "pipe.to_gbq(\"bqml_tutorial.penguins_pipeline\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f6b60898", + "metadata": {}, + "source": [ + "# Custom data split" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "60ac0174", + "metadata": {}, + "source": [ + "BigQuery has also managed splitting out our training data. What if we want to do this manually?\n", + "\n", + "*TODO: Write this section*" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "vscode": { + "interpreter": { + "hash": "a850322d07d9bdc9ec5f301d307e048bcab2390ae395e1cbce9335f4e081e5e2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000000..5e54c12fb2f --- /dev/null +++ b/noxfile.py @@ -0,0 +1,636 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import os +import pathlib +import re +import shutil +from typing import Dict, List +import warnings + +import nox + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.12.0" +SPHINX_VERSION = "sphinx==4.5.0" +LINT_PATHS = ["docs", "bigframes", "tests", "noxfile.py", "setup.py"] + +DEFAULT_PYTHON_VERSION = "3.10" + +UNIT_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] +UNIT_TEST_STANDARD_DEPENDENCIES = [ + "mock", + "asyncmock", + "pytest", + "pytest-cov", + "pytest-asyncio", +] +UNIT_TEST_EXTERNAL_DEPENDENCIES: List[str] = [] +UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [] +UNIT_TEST_DEPENDENCIES: List[str] = [] +UNIT_TEST_EXTRAS: List[str] = [] +UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} + +SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.11"] +SYSTEM_TEST_STANDARD_DEPENDENCIES = [ + "jinja2", + "mock", + "openpyxl", + "pytest", + "pytest-cov", + "pytest-retry", + "pytest-timeout", + "pytest-xdist", + "google-cloud-testutils", + "tabulate", + "xarray", +] +SYSTEM_TEST_EXTERNAL_DEPENDENCIES = [ + "google-cloud-bigquery", +] +SYSTEM_TEST_LOCAL_DEPENDENCIES: List[str] = [] +SYSTEM_TEST_DEPENDENCIES: List[str] = [] +SYSTEM_TEST_EXTRAS: List[str] = ["tests"] +SYSTEM_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} + +CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() + +# Sessions are executed in the order so putting the smaller sessions +# ahead to fail fast at presubmit running. +# 'docfx' is excluded since it only needs to run in 'docs-presubmit' +nox.options.sessions = [ + "lint", + "lint_setup_py", + "mypy", + "format", + "docs", + "docfx", + "unit", + "unit_noextras", + "unit_prerelease", + "system", + "doctest", + "cover", + "release_dry_run", +] + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def lint(session): + """Run linters. + + Returns a failure if the linters find linting errors or sufficiently + serious code quality issues. + """ + session.install("flake8", BLACK_VERSION) + session.run( + "black", + "--check", + *LINT_PATHS, + ) + session.run("flake8", "bigframes", "tests") + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def blacken(session): + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + session.run( + "black", + *LINT_PATHS, + ) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def format(session): + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run( + "isort", + *LINT_PATHS, + ) + session.run( + "black", + *LINT_PATHS, + ) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def lint_setup_py(session): + """Verify that setup.py is valid (including RST check).""" + session.install("docutils", "pygments") + session.run("python", "setup.py", "check", "--restructuredtext", "--strict") + + +def install_unittest_dependencies(session, install_test_extra, *constraints): + standard_deps = UNIT_TEST_STANDARD_DEPENDENCIES + UNIT_TEST_DEPENDENCIES + session.install(*standard_deps, *constraints) + + if UNIT_TEST_EXTERNAL_DEPENDENCIES: + warnings.warn( + "'unit_test_external_dependencies' is deprecated. Instead, please " + "use 'unit_test_dependencies' or 'unit_test_local_dependencies'.", + DeprecationWarning, + ) + session.install(*UNIT_TEST_EXTERNAL_DEPENDENCIES, *constraints) + + if UNIT_TEST_LOCAL_DEPENDENCIES: + session.install(*UNIT_TEST_LOCAL_DEPENDENCIES, *constraints) + + if install_test_extra and UNIT_TEST_EXTRAS_BY_PYTHON: + extras = UNIT_TEST_EXTRAS_BY_PYTHON.get(session.python, []) + elif install_test_extra and UNIT_TEST_EXTRAS: + extras = UNIT_TEST_EXTRAS + else: + extras = [] + + if extras: + session.install("-e", f".[{','.join(extras)}]", *constraints) + else: + session.install("-e", ".", *constraints) + + +def run_unit(session, install_test_extra): + """Run the unit test suite.""" + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + install_unittest_dependencies(session, install_test_extra, "-c", constraints_path) + + # Run py.test against the unit tests. + tests_path = os.path.join("tests", "unit") + session.run( + "py.test", + "--quiet", + f"--junitxml=unit_{session.python}_sponge_log.xml", + "--cov=bigframes", + f"--cov={tests_path}", + "--cov-append", + "--cov-config=.coveragerc", + "--cov-report=term-missing", + "--cov-fail-under=0", + tests_path, + *session.posargs, + ) + + +@nox.session(python=UNIT_TEST_PYTHON_VERSIONS) +def unit(session): + run_unit(session, install_test_extra=True) + + +@nox.session(python=UNIT_TEST_PYTHON_VERSIONS[-1]) +def unit_noextras(session): + run_unit(session, install_test_extra=False) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def mypy(session): + """Run type checks with mypy.""" + session.install("-e", ".") + + # Just install the dependencies' type info directly, since "mypy --install-types" + # might require an additional pass. + deps = ( + set( + [ + "mypy", + "pandas-stubs", + "types-protobuf", + "types-python-dateutil", + "types-requests", + "types-setuptools", + ] + ) + | set(SYSTEM_TEST_STANDARD_DEPENDENCIES) + | set(UNIT_TEST_STANDARD_DEPENDENCIES) + ) + + session.install(*deps) + shutil.rmtree(".mypy_cache", ignore_errors=True) + session.run( + "mypy", + "bigframes", + os.path.join("tests", "system"), + os.path.join("tests", "unit"), + "--explicit-package-bases", + '--exclude="^third_party"', + ) + + +def install_systemtest_dependencies(session, install_test_extra, *constraints): + # Use pre-release gRPC for system tests. + # Exclude version 1.49.0rc1 which has a known issue. + # See https://github.com/grpc/grpc/pull/30642 + session.install("--pre", "grpcio!=1.49.0rc1") + + session.install(*SYSTEM_TEST_STANDARD_DEPENDENCIES, *constraints) + + if SYSTEM_TEST_EXTERNAL_DEPENDENCIES: + session.install(*SYSTEM_TEST_EXTERNAL_DEPENDENCIES, *constraints) + + if SYSTEM_TEST_LOCAL_DEPENDENCIES: + session.install("-e", *SYSTEM_TEST_LOCAL_DEPENDENCIES, *constraints) + + if SYSTEM_TEST_DEPENDENCIES: + session.install("-e", *SYSTEM_TEST_DEPENDENCIES, *constraints) + + if install_test_extra and SYSTEM_TEST_EXTRAS_BY_PYTHON: + extras = SYSTEM_TEST_EXTRAS_BY_PYTHON.get(session.python, []) + elif install_test_extra and SYSTEM_TEST_EXTRAS: + extras = SYSTEM_TEST_EXTRAS + else: + extras = [] + + if extras: + session.install("-e", f".[{','.join(extras)}]", *constraints) + else: + session.install("-e", ".", *constraints) + + +def run_system( + session, + prefix_name, + test_folder, + *, + check_cov=False, + install_test_extra=True, + print_duration=False, + extra_pytest_options=(), +): + """Run the system test suite.""" + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + + # Check the value of `RUN_SYSTEM_TESTS` env var. It defaults to true. + if os.environ.get("RUN_SYSTEM_TESTS", "true") == "false": + session.skip("RUN_SYSTEM_TESTS is set to false, skipping") + # Install pyopenssl for mTLS testing. + if os.environ.get("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true": + session.install("pyopenssl") + + install_systemtest_dependencies(session, install_test_extra, "-c", constraints_path) + + # Run py.test against the system tests. + pytest_cmd = [ + "py.test", + "--quiet", + "-n 20", + # Any indivisual test taking longer than 10 mins will be terminated. + "--timeout=900", + f"--junitxml={prefix_name}_{session.python}_sponge_log.xml", + ] + if print_duration: + pytest_cmd.extend( + [ + "--durations=0", + ] + ) + if check_cov: + pytest_cmd.extend( + [ + "--cov=bigframes", + f"--cov={test_folder}", + "--cov-append", + "--cov-config=.coveragerc", + "--cov-report=term-missing", + "--cov-fail-under=0", + ] + ) + + pytest_cmd.extend(extra_pytest_options) + session.run( + *pytest_cmd, + test_folder, + *session.posargs, + ) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) +def system(session): + """Run the system test suite.""" + run_system( + session=session, + prefix_name="system", + test_folder=os.path.join("tests", "system", "small"), + check_cov=True, + ) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS[-1]) +def system_noextras(session): + """Run the system test suite.""" + run_system( + session=session, + prefix_name="system_noextras", + test_folder=os.path.join("tests", "system", "small"), + install_test_extra=False, + ) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS[-1]) +def doctest(session): + """Run the system test suite.""" + run_system( + session=session, + prefix_name="doctest", + extra_pytest_options=("--doctest-modules",), + test_folder="bigframes", + check_cov=True, + ) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS[-1]) +def e2e(session): + """Run the large tests in system test suite.""" + run_system( + session=session, + prefix_name="e2e", + test_folder=os.path.join("tests", "system", "large"), + print_duration=True, + ) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) +def samples(session): + """Run the samples test suite.""" + + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + + # TODO(swast): Use `requirements.txt` files from the samples directories to + # test samples. + install_test_extra = True + install_systemtest_dependencies(session, install_test_extra, "-c", constraints_path) + + session.run( + "py.test", + "samples", + *session.posargs, + ) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def cover(session): + """Run the final coverage report. + + This outputs the coverage report aggregating coverage from the test runs + (including system test runs), and then erases coverage data. + """ + session.install("coverage", "pytest-cov") + session.run("coverage", "report", "--show-missing", "--fail-under=90") + + # Make sure there is no dead code in our test directories. + # TODO(swast): Cleanup dead code in the system tests directory. + session.run( + "coverage", + "report", + "--show-missing", + "--include=tests/unit/*", + "--fail-under=100", + ) + + session.run("coverage", "erase") + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def docs(session): + """Build the docs for this library.""" + + session.install("-e", ".") + session.install( + SPHINX_VERSION, + "alabaster", + "recommonmark", + ) + + shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) + session.run( + "sphinx-build", + "-W", # warnings as errors + "-T", # show full traceback on exception + "-N", # no colors + "-b", + "html", + "-d", + os.path.join("docs", "_build", "doctrees", ""), + os.path.join("docs", ""), + os.path.join("docs", "_build", "html", ""), + ) + + +# docfx doesn't yet support Python 3.10. +# https://github.com/googleapis/sphinx-docfx-yaml/issues/305 +@nox.session(python="3.9") +def docfx(session): + """Build the docfx yaml files for this library.""" + + session.install("-e", ".") + session.install( + SPHINX_VERSION, + "alabaster", + "recommonmark", + "gcp-sphinx-docfx-yaml", + ) + + shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) + session.run( + "sphinx-build", + "-T", # show full traceback on exception + "-N", # no colors + "-D", + ( + "extensions=sphinx.ext.autodoc," + "sphinx.ext.autosummary," + "docfx_yaml.extension," + "sphinx.ext.intersphinx," + "sphinx.ext.coverage," + "sphinx.ext.napoleon," + "sphinx.ext.todo," + "sphinx.ext.viewcode," + "recommonmark" + ), + "-b", + "html", + "-d", + os.path.join("docs", "_build", "doctrees", ""), + os.path.join("docs", ""), + os.path.join("docs", "_build", "html", ""), + ) + + +def prerelease(session, tests_path): + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + + # PyArrow prerelease packages are published to an alternative PyPI host. + # https://arrow.apache.org/docs/python/install.html#installing-nightly-packages + session.install( + "--extra-index-url", + "https://pypi.fury.io/arrow-nightlies/", + "--prefer-binary", + "--pre", + "--upgrade", + "pyarrow", + ) + session.install( + "--extra-index-url", + "https://pypi.anaconda.org/scipy-wheels-nightly/simple", + "--prefer-binary", + "--pre", + "--upgrade", + "pandas", + ) + session.install( + "--upgrade", + "-e", # Use -e so that py.typed file is included. + "git+https://github.com/ibis-project/ibis.git#egg=ibis-framework", + ) + # Workaround https://github.com/googleapis/python-db-dtypes-pandas/issues/178 + session.install("--no-deps", "db-dtypes") + + # Workaround to install pandas-gbq >=0.15.0, which is required by test only. + session.install("--no-deps", "pandas-gbq") + + session.install( + *set(UNIT_TEST_STANDARD_DEPENDENCIES + SYSTEM_TEST_STANDARD_DEPENDENCIES), + "-c", + constraints_path, + ) + + # Because we test minimum dependency versions on the minimum Python + # version, the first version we test with in the unit tests sessions has a + # constraints file containing all dependencies and extras. + with open( + CURRENT_DIRECTORY + / "testing" + / f"constraints-{UNIT_TEST_PYTHON_VERSIONS[0]}.txt", + encoding="utf-8", + ) as constraints_file: + constraints_text = constraints_file.read() + + # Ignore leading whitespace and comment lines. + already_installed = frozenset( + ("db-dtypes", "pandas", "pyarrow", "ibis-framework", "pandas-gbq") + ) + deps = [ + match.group(1) + for match in re.finditer( + r"^\s*(\S+)(?===\S+)", constraints_text, flags=re.MULTILINE + ) + if match.group(1) not in already_installed + ] + + # We use --no-deps to ensure that pre-release versions aren't overwritten + # by the version ranges in setup.py. + session.install(*deps) + session.install("--no-deps", "-e", ".") + + # Print out prerelease package versions. + session.run("python", "-m", "pip", "freeze") + + # Run py.test against the tests. + session.run( + "py.test", + "--quiet", + "-n 20", + # Any indivisual test taking longer than 10 mins will be terminated. + "--timeout=600", + f"--junitxml={os.path.split(tests_path)[-1]}_prerelease_{session.python}_sponge_log.xml", + "--cov=bigframes", + f"--cov={tests_path}", + "--cov-append", + "--cov-config=.coveragerc", + "--cov-report=term-missing", + "--cov-fail-under=0", + tests_path, + *session.posargs, + ) + + +@nox.session(python=UNIT_TEST_PYTHON_VERSIONS[-1]) +def unit_prerelease(session): + """Run the unit test suite with prerelease dependencies.""" + prerelease(session, os.path.join("tests", "unit")) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS[-1]) +def system_prerelease(session): + """Run the system test suite with prerelease dependencies.""" + prerelease(session, os.path.join("tests", "system", "small")) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) +def notebook(session): + session.install("-e", ".[all]") + session.install("pytest", "pytest-xdist", "pytest-retry", "nbmake") + + notebooks = [ + "00 - Summary.ipynb", + "01 - Getting Started.ipynb", + "02 - DataFrame.ipynb", + "03 - Using ML - ML fundamentals.ipynb", + "04 - Using ML - SKLearn linear regression.ipynb", + "05 - Using ML - Easy linear regression.ipynb", + "06 - Using ML - Large Language Models.ipynb", + "50 - Remote Function.ipynb", + ] + notebooks = [os.path.join("notebooks", nb) for nb in notebooks] + + # For some reason nbmake exits silently with "no tests ran" message if + # one of the notebook paths supplied does not exist. Let's make sure that + # each path exists + for nb in notebooks: + assert os.path.exists(nb), nb + + # Use retries because sometimes parallel runs of the same notebook can try + # to create the same artifacts and may run into resoure conflict at the GCP + # level. + session.run( + "py.test", + "-nauto", + "--nbmake", + "--nbmake-timeout=600", + "--retries=3", + *notebooks, + ) + + +@nox.session(python="3.10") +def release_dry_run(session): + env = {} + + # If the project root is not set, then take current directory as the project + # root. See the release script for how the project root is set/used. This is + # specially useful when the developer runs the nox session on local machine. + if not os.environ.get("PROJECT_ROOT") and not os.environ.get( + "KOKORO_ARTIFACTS_DIR" + ): + env["PROJECT_ROOT"] = "." + session.run(".kokoro/release-nightly.sh", "--dry-run", env=env) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000000..693439f47ca --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore::pandas.errors.SettingWithCopyWarning diff --git a/samples/snippets/__init__.py b/samples/snippets/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/samples/snippets/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/samples/snippets/quickstart.py b/samples/snippets/quickstart.py new file mode 100644 index 00000000000..dc8ab7ba2d0 --- /dev/null +++ b/samples/snippets/quickstart.py @@ -0,0 +1,89 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def run_quickstart(project_id: str): + # [START bigquery_bigframes_quickstart] + import bigframes.pandas as pd + + # TODO: (Optional) Setup your session with the configuration. Some of these + # settings cannot be changed once a session has started. + pd.options.bigquery.project = "your-gcp-project-id" + pd.options.bigquery.location = "us" + + # [START_EXCLUDE silent] + # Ignore the lines between "EXCLUDE" comments. They replace values so this + # sample runs in our test suite. + pd.options.bigquery.project = project_id + # [END_EXCLUDE] + + query_or_table = "bigquery-public-data.ml_datasets.penguins" + df = pd.read_gbq(query_or_table) + + # Use the DataFrame just as you would a pandas DataFrame, but calculations + # happen in the BigQuery query engine instead of the local system. + average_body_mass = df["body_mass_g"].mean() + + print(f"average_body_mass: {average_body_mass}") + + # IMPORTANT: The `bigframes.pandas` package creates a BigQuery session for + # queries and temporary tables. A BigQuery session has a limited lifetime + # (https://cloud.google.com/bigquery/docs/sessions-intro#limitations) and + # does not support concurrent queries. For long lived applications, create + # session objects as needed, instead. + + import bigframes + + session_options = bigframes.BigQueryOptions() + session_options.project = "your-gcp-project-id" + session_options.location = "us" + + # [START_EXCLUDE silent] + # Ignore the lines between "EXCLUDE" comments. They replace values so this + # sample runs in our test suite. + session_options.project = project_id + # [END_EXCLUDE] + + session = bigframes.connect(session_options) + df_session = session.read_gbq(query_or_table) + average_body_mass = df_session["body_mass_g"].mean() + print(f"average_body_mass (df_session): {average_body_mass}") + + from bigframes.ml.linear_model import LinearRegression + + # filter down to the data we want to analyze + adelie_data = df[df.species == "Adelie Penguin (Pygoscelis adeliae)"] + + # drop the columns we don't care about + adelie_data = adelie_data.drop(columns=["species"]) + + # drop rows with nulls to get our training data + training_data = adelie_data.dropna() + + # pick feature columns and label column + X = training_data[ + [ + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + y = training_data[["body_mass_g"]] + + model = LinearRegression(fit_intercept=False) + model.fit(X, y) + model.score(X, y) + # [END bigquery_bigframes_quickstart] diff --git a/samples/snippets/quickstart_test.py b/samples/snippets/quickstart_test.py new file mode 100644 index 00000000000..6b0c69de999 --- /dev/null +++ b/samples/snippets/quickstart_test.py @@ -0,0 +1,31 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.pandas + +from . import quickstart + + +def test_quickstart( + capsys: pytest.CaptureFixture[str], +) -> None: + # We need a fresh session since we're modifying connection options. + bigframes.pandas.reset_session() + + # TODO(swast): Get project from environment so contributors can run tests. + quickstart.run_quickstart("bigframes-dev") + out, _ = capsys.readouterr() + assert "average_body_mass (df_session):" in out diff --git a/scripts/generate_third_party_notices.py b/scripts/generate_third_party_notices.py new file mode 100644 index 00000000000..7040bb2e5f8 --- /dev/null +++ b/scripts/generate_third_party_notices.py @@ -0,0 +1,332 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import glob +import importlib.metadata +import json +import os.path +import re +import sys + +import piplicenses +import requests + +DEPENDENCY_INFO_SEPARATOR = "*" * 80 + "\n" +PACKAGE_NAME_EXTRACTOR = re.compile("^[a-zA-Z0-9._-]+") + +# These packages don't have LICENSE files distributed in their packages, +# but we have manually confirmed they have a compatible license and +# included it manually in our `third_party` directory. +# +# TODO(swast): We can remove this workaround once these packages bundle the +# license file. +# +# ipython-genutils and recommonmark are both in an archived state with no likely updates in the future +# +# Tracking issues: +# * https://github.com/grpc/grpc/issues/33557 +# * https://github.com/gsnedders/python-webencodings/issues/33 +# * https://github.com/pickleshare/pickleshare/issues/34 +DIRECT_LICENSE_MAPPINGS = { + "grpcio-status": "https://raw.githubusercontent.com/grpc/grpc/master/LICENSE", + "webencodings": "https://raw.githubusercontent.com/gsnedders/python-webencodings/master/LICENSE", + "ipython-genutils": "https://raw.githubusercontent.com/ipython/ipython_genutils/master/COPYING.md", + "pickleshare": "https://raw.githubusercontent.com/pickleshare/pickleshare/master/LICENSE", + "recommonmark": "https://raw.githubusercontent.com/readthedocs/recommonmark/master/license.md", +} + + +def get_package_dependencies(pkg_name): + """Get all package dependencies for a given package, both required and optional.""" + packages = set() + requirements = importlib.metadata.requires(pkg_name) + if requirements: + for req in requirements: + match = PACKAGE_NAME_EXTRACTOR.match(req) + assert match, f"Could not parse {req} for package name" + packages.add(match.group(0)) + return packages + + +# Inspired by third_party/colab/cleanup_filesets.py +def find_dependencies( + roots: set[str], ignore_missing_metadata=False +) -> dict[str, dict[str, set[str]]]: + """Return the transitive dependencies of a set of packages. + Args: + roots: List of package names, e.g. ["pkg1", "pkg2"] + Returns: + A dictionary of dependencies, e.g. + { + "pkg3" : { + "Requires" : set(["pkg4", "pkg5", "pkg6"]), + "RequiredBy": set(["pkg1"]) + }, + "pkg4" : { + "Requires" : set([]), + "RequiredBy": set(["pkg3"]) + }, + ... + } + """ + hops = set() + visited = set() + deps: dict[str, dict[str, set[str]]] = dict() + + # Initialize the start of the graph walk + for root in roots: + # Get the normalized package name + try: + pkg = importlib.metadata.metadata(root) + except importlib.metadata.PackageNotFoundError: + if not ignore_missing_metadata: + raise + continue + hops.add(pkg["Name"]) + + # Start the graph walk + while True: + if not hops: + break + hop = hops.pop() + if hop in visited: + continue + visited.add(hop) + + for dep in get_package_dependencies(hop): + # Get the normalized package name + try: + req_pkg = importlib.metadata.metadata(dep) + except importlib.metadata.PackageNotFoundError: + if not ignore_missing_metadata: + raise + continue + dep = req_pkg["Name"] + + # Create outgoing edge only for non root packages, for which an + # entry must have been created in the deps dictionary when we + # saw the package for the first time during the graph walk + if hop in deps: + deps[hop]["Requires"].add(dep) + + if dep in deps: + # We have already seen this requirement in the graph walk. + # Just update the incoming dependency and carry on. + deps[dep]["RequiredBy"].add(hop) + else: + # This is the first time we came across this requirement. + # Create a new entry with the incoming dependency. + deps[dep] = {"RequiredBy": {hop}, "Requires": set()} + + # Put it in the next hops for further graph traversal + hops.add(dep) + + return deps + + +def get_metadata_and_filename( + package_name: str, + metadata_name: str, + metadata_file: str, + metadata_text: str, + ignore_missing=True, +) -> tuple[str, str] | None: + """Get package metadata and corresponsing file name.""" + + # Check metadata file + metadata_filepath_known = metadata_file != piplicenses.LICENSE_UNKNOWN + if not metadata_filepath_known and not ignore_missing: + raise ValueError(f"No {metadata_name} file found for {package_name}") + + # Check metadata text + if metadata_text != piplicenses.LICENSE_UNKNOWN: + output_filename = metadata_name + if metadata_filepath_known: + output_filename = os.path.basename(metadata_file) + if not output_filename: + raise ValueError( + f"Need a file name to write {metadata_name} text for {package_name}." + ) + return metadata_text, output_filename + elif not ignore_missing: + raise ValueError(f"No {metadata_name} text found for {package_name}") + + return None + + +def fetch_license_and_notice_metadata(packages: list[str]): + """Fetch metadata including license and notice for given packages. + Returns a json object. + """ + parser = piplicenses.create_parser() + args = parser.parse_args( + [ + "--format", + "json", + "--with-license-file", + "--with-notice-file", + "--with-urls", + "--with-description", + "--packages", + *packages, + ] + ) + output_str = piplicenses.create_output_string(args) + metadatas = json.loads(output_str) + return metadatas + + +def write_lines_without_trailing_spaces(file, text: str, key: str): + """Write text lines to a file without the trailing spaces. + This will stop complaints by the trailing-whitespace pre-commit hook.""" + text = "\n".join([line.rstrip() for line in text.split("\n")]) + file.write(f"{key}:\n{text}\n") + + +def write_metadata_to_file( + file, metadata, with_version=False, requires_packages=[], packages_required_by=[] +): + """Write package metadata to a file object.""" + file.write(DEPENDENCY_INFO_SEPARATOR) + + info_keys = ["Name"] + if with_version: + info_keys.append("Version") + info_keys.extend(["License", "URL"]) + file.writelines([f"{key}: {metadata[key]}\n" for key in info_keys]) + + if requires_packages: + file.write(f"Requires: {', '.join(sorted(requires_packages))}\n") + + if packages_required_by: + file.write(f"Required By: {', '.join(sorted(packages_required_by))}\n") + + # Try to generate third party license + + license_info = get_metadata_and_filename( + metadata["Name"], + "LICENSE", + metadata["LicenseFile"], + metadata["LicenseText"], + ignore_missing=metadata["Name"] in DIRECT_LICENSE_MAPPINGS, + ) + + license_text = "" + if license_info: + license_text = license_info[0] + else: + license_text_response = requests.get(DIRECT_LICENSE_MAPPINGS[metadata["Name"]]) + license_text = license_text_response.text + + write_lines_without_trailing_spaces(file, license_text, "License") + + # Try to generate third party notice + notice_info = get_metadata_and_filename( + metadata["Name"], + "NOTICE", + metadata["NoticeFile"], + metadata["NoticeText"], + ignore_missing=True, + ) + + if notice_info: + write_lines_without_trailing_spaces(file, notice_info[0], "Notice") + + file.write(DEPENDENCY_INFO_SEPARATOR) + + +def write_third_party_vendored_license(file, path): + """Write license of a vendored third party library to notices file.""" + file.write(DEPENDENCY_INFO_SEPARATOR) + file.write(f"Vendored Code: {os.path.dirname(path)}\n") + notice_key = f"Notice ({os.path.basename(path)})" + write_lines_without_trailing_spaces(file, open(path).read(), notice_key) + file.write(DEPENDENCY_INFO_SEPARATOR) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generate third party notices for bigframes dependencies." + ) + parser.add_argument( + "--with-version", + action="store_true", + default=False, + help="Include the version information for each package.", + ) + parser.add_argument( + "--with-requires", + action="store_true", + default=False, + help="Include for each package the packages it requires.", + ) + parser.add_argument( + "--with-required-by", + action="store_true", + default=False, + help="Include for each package the packages that require it.", + ) + parser.add_argument( + "--output-file", + action="store", + default="THIRD_PARTY_NOTICES", + help="The output file to write third party notices in.", + ) + args = parser.parse_args(sys.argv[1:]) + + # Initialize the root package + roots = {"bigframes"} + + # Find dependencies + # Let's ignore the packages that are not installed assuming they are + # just the optional dependencies that bigframes does not require. + # One example is the dependency path bigframes -> SQLAlchemy -> pg8000, + # where pg8000 is only an optional dependency for SQLAlchemy which bigframes + # is not depending on + # https://github.com/sqlalchemy/sqlalchemy/blob/7bc81947e22dc32368b0c49a41c398cd251d94af/setup.cfg#LL62C21-L62C27 + deps = find_dependencies(roots, ignore_missing_metadata=True) + + # Use third party solution to fetch dependency metadata + deps_metadata = fetch_license_and_notice_metadata(list(deps)) + deps_metadata = sorted(deps_metadata, key=lambda m: m["Name"]) + + # Write the file + with open(args.output_file, "w") as f: + # Generate third party metadata for each dependency + for metadata in deps_metadata: + dep = deps[metadata["Name"]] + write_metadata_to_file( + f, + metadata, + args.with_version, + dep["Requires"] if args.with_requires else [], + dep["RequiredBy"] if args.with_required_by else [], + ) + + # Generate third party vendored notices + notices = set() + for filename in [ + "LICENCE", + "LICENCE.txt", + "LICENSE", + "LICENSE.txt", + "NOTICE", + "NOTICE.txt", + "COPYING", + "COPYING.txt", + ]: + notices.update(glob.glob(f"third_party/bigframes_vendored/*/{filename}")) + for path in sorted(notices): + write_third_party_vendored_license(f, path) diff --git a/scripts/publish_api_coverage.py b/scripts/publish_api_coverage.py new file mode 100644 index 00000000000..856307e4407 --- /dev/null +++ b/scripts/publish_api_coverage.py @@ -0,0 +1,179 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This script runs with each release and publishes information about our pandas +and sklearn API coverage to BigQuery, where it can be used in our dashboards.""" + +import argparse +import inspect + +import pandas as pd + +import bigframes.pandas as bpd + + +def generate_pandas_api_coverage(): + """Inspect all our pandas objects, and compare with the real pandas objects, to see + which methods we implement. For each, generate a regex that can be used to check if + its present in a notebook""" + header = ["api", "pattern", "kind", "is_in_bigframes"] + api_patterns = [] + targets = [ + ("pandas", pd, bpd), + ("dataframe", pd.DataFrame, bpd.DataFrame), + ("series", pd.Series, bpd.Series), + ("index", pd.Index, bpd.Index), + ] + indexers = ["loc", "iloc", "iat", "ix", "at"] + for name, pandas_obj, bigframes_obj in targets: + for member in dir(pandas_obj): + # skip private functions and properties + if member[0] == "_" and member[1] != "_": + continue + + # skip members that are also common python methods + if member in {"append", "tolist", "format", "items", "keys"}: + continue + + if inspect.isfunction(getattr(pandas_obj, member)): + # Function, match .member( + token = f"\\.{member}\\(" + token_type = "function" + elif member in indexers: + # Indexer, match .indexer[ + token = f"\\.{member}\\[" + token_type = "indexer" + else: + # Property + token = f"\\.{member}\\b" + token_type = "property" + + is_in_bigframes = hasattr(bigframes_obj, member) + + api_patterns.append( + [f"{name}.{member}", token, token_type, is_in_bigframes] + ) + + return pd.DataFrame(api_patterns, columns=header) + + +def generate_sklearn_api_coverage(): + """Explore all SKLearn modules, and for each item contained generate a + regex to detect it being imported, and record whether we implement it""" + sklearn_modules = [ + "sklearn", + "sklearn.model_selection", + "sklearn.preprocessing", + "sklearn.metrics", + "sklearn.linear_model", + "sklearn.ensemble", + "sklearn.tree", + "sklearn.neighbors", + "sklearn.svm", + "sklearn.naive_bayes", + "sklearn.pipeline", + "sklearn.decomposition", + "sklearn.impute", + "sklearn.cluster", + "sklearn.feature_selection", + "sklearn.utils", + "sklearn.compose", + "sklearn.neural_network", + "sklearn.datasets", + "sklearn.base", + "sklearn.manifold", + "sklearn.discriminant_analysis", + "sklearn.experimental", + "sklearn.multiclass", + "sklearn.kernel_ridge", + "sklearn.feature_extraction", + "sklearn.dummy", + "sklearn.mixture", + "sklearn.gaussian_process", + "sklearn.calibration", + "sklearn.multioutput", + "sklearn.inspection", + "sklearn.exceptions", + "sklearn.cross_decomposition", + "sklearn.random_projection", + "sklearn.covariance", + "sklearn.semi_supervised", + "sklearn.isotonic", + "sklearn.kernel_approximation", + ] + + header = ["api", "pattern", "kind", "is_in_bigframes"] + api_patterns = [] + for module in sklearn_modules: + exec(f"import {module}") + members = eval(f"dir({module})") + bigframes_has_module = False + bigframes_members = [] + try: + bigframes_module = module.replace("sklearn", "bigframes.ml") + exec(f"import {bigframes_module}") + bigframes_has_module = True + bigframes_members = eval(f"dir({bigframes_module})") + except ImportError: + pass + + api_patterns.append( + [ + module, + f"from {module} import ", + "module", + bigframes_has_module, + ] + ) + for member in members: + # skip private functions and properties + if member[0] == "_": + continue + + api_patterns.append( + [ + f"{module}.{member}", + rf"from {module} import [^\n]*\b{member}\b", + "api", + member in bigframes_members, + ] + ) + + return pd.DataFrame(api_patterns, columns=header) + + +def build_api_coverage_table(bigframes_version: str, release_version: str): + pandas_cov_df = generate_pandas_api_coverage() + pandas_cov_df["module"] = "bigframes" + sklearn_cov_df = generate_sklearn_api_coverage() + sklearn_cov_df["module"] = "bigframes.ml" + combined_df = pd.concat([pandas_cov_df, sklearn_cov_df]) + combined_df["timestamp"] = pd.Timestamp.now() + combined_df["bigframes_version"] = bigframes_version + combined_df["release_version"] = release_version + return combined_df.infer_objects().convert_dtypes() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--bigframes_version") + parser.add_argument("--release_version") + parser.add_argument("--bigquery_table_name") + args = parser.parse_args() + df = build_api_coverage_table(args.bigframes_version, args.release_version) + df.to_gbq(args.bigquery_table_name, if_exists="append") + + +if __name__ == "__main__": + main() diff --git a/scripts/test_publish_api_coverage.py b/scripts/test_publish_api_coverage.py new file mode 100644 index 00000000000..96b2d1bb48d --- /dev/null +++ b/scripts/test_publish_api_coverage.py @@ -0,0 +1,45 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas +import publish_api_coverage + + +def test_api_coverage_produces_expected_schema(): + df = publish_api_coverage.build_api_coverage_table("my_bf_ver", "my_release_ver") + pandas.testing.assert_series_equal( + df.dtypes, + pandas.Series( + data=[ + "string", + "string", + "string", + "boolean", + "string", + "datetime64[ns]", + "string", + "string", + ], + index=[ + "api", + "pattern", + "kind", + "is_in_bigframes", + "module", + "timestamp", + "bigframes_version", + "release_version", + ], + ), + ) diff --git a/scripts/update_firebase_docs_site.sh b/scripts/update_firebase_docs_site.sh new file mode 100644 index 00000000000..f0ef866c90b --- /dev/null +++ b/scripts/update_firebase_docs_site.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e -o pipefail +set -x + +# Replace the docs version by the desired commit +BIGFRAMES_DOCS_VERSION=8ce2a3e + +BIGFRAMES_FIREBASE_PROJECT=bigframes-dev-d4d9a +BIGFRAMES_DOCS_GCS_BUCKET=bigframes-docs + +BIGFRAMES_DOCS_DIR=`mktemp -d` + + +# Install firebase if not already installed +if ! which firebase; then + npm install -g firebase-tools +fi + +# Prepare a working directory for firebase +mkdir -p ${BIGFRAMES_DOCS_DIR} +pushd ${BIGFRAMES_DOCS_DIR} + +# Copy the bigframes version +if [ ! -d ${BIGFRAMES_DOCS_VERSION} ]; then + gsutil -m cp -r gs://${BIGFRAMES_DOCS_GCS_BUCKET}/${BIGFRAMES_DOCS_VERSION} . +fi + +rm -f latest +ln -s ${BIGFRAMES_DOCS_VERSION} latest + +# Set up firebase +firebase login --no-localhost +firebase init hosting + +versions="${BIGFRAMES_DOCS_VERSION} latest" +for version in ${versions}; do + site_name=bigframes-docs-${version} + if ! firebase hosting:sites:list | grep ${site_name}; then + firebase hosting:sites:create ${site_name} + fi + + firebase target:apply hosting ${version} ${site_name} +done + +# Make sure the firebase json config is consistent with ${versions} +# TODO(shobs): Come up with a better way of updating the config than +# a hard overwrite +cat > firebase.json << EOF +{ + "hosting": [ + { + "target": "latest", + "public": "latest", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } + , + { + "target": "${BIGFRAMES_DOCS_VERSION}", + "public": "${BIGFRAMES_DOCS_VERSION}", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } + ] +} +EOF + +# Verify that the intended sites look good +for version in ${versions}; do + echo "Preview the local hosting of the docs site \"${version}\" before actually deploying (Press Ctrl+C to stop)" + firebase serve --only hosting:${version} +done + +echo -n "Go ahead and deploy? [y/N]: " +read deploy_consent + +# Deploy the sites +if [ "$deploy_consent" = y ]; then + echo "Deploying ..." + firebase deploy --only hosting +else + echo "Not Deploying anything." +fi + +popd diff --git a/scripts/update_x20_docs_site.sh b/scripts/update_x20_docs_site.sh new file mode 100644 index 00000000000..31da116bdd4 --- /dev/null +++ b/scripts/update_x20_docs_site.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Copyright 2020 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 +# +# https://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. + +################################################################################ +# This script uses corp credentials to sync the files to x20. +# Make sure you: +# 1. Have write permission to /x20/teams/bigframes-swe +# 2. Have run `gcert` before running the script +################################################################################ + +set -e -o pipefail +set -x + +GIT_DOCS_DIR=docs/_build/html/ +X20_BIGFRAMES_DIR=/x20/teams/bigframes-swe/bigframes/docs +MAX_BACKFILL=10 +REQ_PYTHON_VERSION=3.9 + +# Create a temporary directory +tempdir=`mktemp --directory --tmpdir bigframes.XXXXXXXXXX` + +# Clone the repository +git clone sso://team/bigquery-query-swe/bigframes ${tempdir} + +# Enter the temporary bigframes directory +pushd ${tempdir} + +# Python version 3.9 is required to build bigframes docs, install if not present +if ! python3 --version | grep ${REQ_PYTHON_VERSION}; then + # Install pyenv to install the required python version + ## https://github.com/pyenv/pyenv#basic-github-checkout + git clone https://github.com/pyenv/pyenv.git .pyenv + pushd .pyenv && src/configure && make -C src && popd + + ## https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv + export PYENV_ROOT=${PWD}/.pyenv + PATH=${PYENV_ROOT}/bin:${PATH} + eval "$(pyenv init -)" + + ## Install the required python version + pyenv install ${REQ_PYTHON_VERSION} + + ## Make the required python version available + pyenv global ${REQ_PYTHON_VERSION} +fi + +# Create a virtual environment with nox installed +python3 -m venv venv +source venv/bin/activate +pip install nox + +# i = 0 means docs for the latest version, and i = 1 onwards means backfill +for i in `seq 0 ${MAX_BACKFILL}`; do + # If it is backfill turn, back off the version by 1 + if [ ${i} -ne 0 ]; then + git reset --hard HEAD~1 + + # Clean up any old docs + rm -rf ${GIT_DOCS_DIR} + fi + + # Construct a docs path in x20 + commit_hash=`git rev-parse --short HEAD` + x20_docs_dir_commit=${X20_BIGFRAMES_DIR}/${commit_hash} + + # If the x20 docs path already exists, let's assume that it was created + # properly in the previous attempt + if fileutil test -d ${x20_docs_dir_commit}; then + echo ${x20_docs_dir_commit} exists, skipping rebuilding it.. + continue + fi + + # Build the docs + echo Building docs for commit ${commit_hash}.. + nox -s docs + + # TODO(shobs): Check if a symlink can be created instead of another copy of + # the latest commit's docs, using fileutil CLI or otherwise + x20_docs_dirs=${x20_docs_dir_commit} + if [ ${i} -eq 0 ]; then + x20_docs_dirs="${x20_docs_dirs} ${X20_BIGFRAMES_DIR}/latest" + fi + + for x20_docs_dir in ${x20_docs_dirs}; do + fileutil mirror -parallelism=4 -force ${GIT_DOCS_DIR} ${x20_docs_dir} + x20_own request_change --recursive --path=${x20_docs_dir} --new_mode=a+r + done +done + +# Exit the temporary bigframes directory +popd + +# Clean up the temporary bigframes directory +rm -rf ${tempdir} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000000..8bd749387ed --- /dev/null +++ b/setup.cfg @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2020 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 +# +# https://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. + +# Generated by synthtool. DO NOT EDIT! +[bdist_wheel] +universal = 1 + +[pytype] +python_version = 3.9 +inputs = + google/cloud/ +exclude = + tests/ + google/cloud/bigquery_v2/ # Legacy proto-based types. +output = .pytype/ +disable = + # There's some issue with finding some pyi files, thus disabling. + # The issue https://github.com/google/pytype/issues/150 is closed, but the + # error still occurs for some reason. + pyi-error diff --git a/setup.py b/setup.py new file mode 100644 index 00000000000..311471f2627 --- /dev/null +++ b/setup.py @@ -0,0 +1,109 @@ +# Copyright 2022 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 io +import itertools +import os +from typing import Dict + +import setuptools + +# Package metadata. + +name = "bigframes" +description = "Scalable DataFrames with BigQuery" + +# Should be one of: +# 'Development Status :: 3 - Alpha' +# 'Development Status :: 4 - Beta' +# 'Development Status :: 5 - Production/Stable' +release_status = "Development Status :: 3 - Alpha" +dependencies = [ + "cloudpickle >= 2.2.1", + "fsspec >=2023.3.0", + "gcsfs >=2023.3.0", + "geopandas >=0.12.2", + "google-auth >2.14.1,<3.0dev", + "google-cloud-bigquery[bqstorage,pandas] >=3.10.0", + "google-cloud-functions >=1.10.1", + "google-cloud-bigquery-connection >=1.12.0", + "google-cloud-storage >=2.0.0", + "ibis-framework[bigquery] >=6.0.0", + "pandas >=1.5.0", + "pydata-google-auth >=1.5.0", + "scikit-learn >=1.2.2", + "sqlalchemy >=1.4,<3.0", + "ipywidgets >=7.7.1", + "humanize >= 4.6.0", +] +extras = { + # Optional test dependencies packages. If they're missed, may skip some tests. + "tests": [ + "pandas-gbq >=0.19.0", + ], + # Packages required for basic development flow. + "dev": ["pytest", "pre-commit", "nox", "google-cloud-testutils"], +} +extras["all"] = list(sorted(frozenset(itertools.chain.from_iterable(extras.values())))) + +# Setup boilerplate below this line. + +package_root = os.path.abspath(os.path.dirname(__file__)) + +readme_filename = os.path.join(package_root, "README.rst") +with io.open(readme_filename, encoding="utf-8") as readme_file: + readme = readme_file.read() + +version: Dict[str, str] = {} +with open(os.path.join(package_root, "bigframes/version.py")) as fp: + exec(fp.read(), version) +version_id = version["__version__"] + +# Only include packages under the 'bigframes' namespace. Do not include tests, +# benchmarks, etc. +packages = [ + package + for package in setuptools.PEP420PackageFinder.find() + if package.startswith("bigframes") or package.startswith("third_party") +] + +setuptools.setup( + name=name, + version=version_id, + description=description, + long_description=readme, + author="Google LLC", + author_email="googleapis-packages@google.com", + license="Apache 2.0", + url="https://github.com/googleapis/python-bigquery", + classifiers=[ + release_status, + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Operating System :: OS Independent", + "Topic :: Internet", + ], + install_requires=dependencies, + extras_require=extras, + platforms="Posix; MacOS X; Windows", + packages=packages, + python_requires=">=3.9", + include_package_data=True, + zip_safe=False, +) diff --git a/testing/constraints-3.10.txt b/testing/constraints-3.10.txt new file mode 100644 index 00000000000..9f0786f47e6 --- /dev/null +++ b/testing/constraints-3.10.txt @@ -0,0 +1,15 @@ +# Keep in sync with colab/containers/requirements.core.in image +google-auth==2.17.3 +ipykernel==5.5.6 +ipython==7.34.0 +notebook==6.4.8 +pandas==1.5.3 +portpicker==1.3.9 +requests==2.27.1 +tornado==6.3.1 +absl-py==1.4.0 +debugpy==1.6.6 +ipywidgets==7.7.1 +matplotlib==3.7.1 +psutil==5.9.5 +traitlets==5.7.1 diff --git a/testing/constraints-3.11.txt b/testing/constraints-3.11.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt new file mode 100644 index 00000000000..aa72e5426b7 --- /dev/null +++ b/testing/constraints-3.9.txt @@ -0,0 +1,118 @@ +argcomplete==2.1.2 +asyncmock==0.4.2 +atpublic==3.1.1 +attrs==22.2.0 +bidict==0.22.1 +black==23.3.0 +bleach==6.0.0 +cachetools==5.3.0 +certifi==2022.12.7 +cffi==1.15.1 +cfgv==3.3.1 +charset-normalizer==3.1.0 +click==8.1.3 +cloudpickle==2.2.1 +colorlog==6.7.0 +coverage==7.2.2 +cryptography==40.0.1 +distlib==0.3.6 +docstring-inheritance==2.0.0 +docutils==0.19 +exceptiongroup==1.1.1 +execnet==1.9.0 +filelock==3.10.7 +fsspec==2023.3.0 +gcp-docuploader==0.6.5 +gcp-releasetool==1.11.0 +gcsfs==2023.3.0 +geopandas==0.12.2 +google-api-core==2.11.0 +google-auth==2.17.0 +google-auth-oauthlib==1.0.0 +google-cloud-bigquery==3.10.0 +google-cloud-bigquery-connection==1.12.0 +google-cloud-bigquery-storage==2.19.1 +google-cloud-core==2.3.2 +google-cloud-functions==1.10.1 +google-cloud-storage==2.0.0 +google-cloud-testutils==1.3.3 +google-crc32c==1.5.0 +google-resumable-media==2.4.1 +googleapis-common-protos==1.59.0 +greenlet==2.0.2 +grpc-google-iam-v1==0.12.6 +grpcio==1.53.0 +grpcio-status==1.48.2 +ibis-framework==6.0.0 +humanize==4.6.0 +identify==2.5.22 +idna==3.4 +importlib-metadata==6.1.0 +iniconfig==2.0.0 +ipywidgets==7.7.1 +jaraco.classes==3.2.3 +jeepney==0.8.0 +Jinja2==3.1.2 +keyring==23.13.1 +markdown-it-py==2.2.0 +MarkupSafe==2.1.2 +mdurl==0.1.2 +mock==5.0.1 +more-itertools==9.1.0 +multipledispatch==0.6.0 +mypy-extensions==1.0.0 +nodeenv==1.7.0 +nox==2022.11.21 +numpy==1.24.2 +oauthlib==3.2.2 +packaging==23.0 +pandas==1.5.0 +pandas-gbq==0.19.0 +parsy==2.1 +pathspec==0.11.1 +pkginfo==1.9.6 +platformdirs==3.2.0 +pluggy==1.0.0 +pooch==1.7.0 +pre-commit==3.2.1 +proto-plus==1.22.2 +protobuf==3.20.3 +pyarrow==11.0.0 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycparser==2.21 +pydata-google-auth==1.5.0 +Pygments==2.14.0 +PyJWT==2.6.0 +pyperclip==1.8.2 +pytest==7.2.2 +pytest-asyncio==0.21.0 +pytest-cov==4.0.0 +pytest-retry==1.1.0 +pytest-xdist==3.2.1 +python-dateutil==2.8.2 +pytz==2023.3 +PyYAML==6.0 +readme-renderer==37.3 +requests==2.28.2 +requests-oauthlib==1.3.1 +requests-toolbelt==0.10.1 +rfc3986==2.0.0 +rich==13.3.3 +rsa==4.9 +scikit-learn==1.2.2 +SecretStorage==3.3.3 +six==1.16.0 +SQLAlchemy==1.4.0 +sqlglot==10.6.4 +tomli==2.0.1 +toolz==0.12.0 +tqdm==4.65.0 +twine==4.0.2 +typing_extensions==4.5.0 +tzdata==2023.3 +urllib3==1.26.15 +virtualenv==20.21.0 +webencodings==0.5.1 +xxhash==3.2.0 +zipp==3.15.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/data/penguins.jsonl b/tests/data/penguins.jsonl new file mode 100644 index 00000000000..d3aa06e3339 --- /dev/null +++ b/tests/data/penguins.jsonl @@ -0,0 +1,344 @@ +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36.6,"culmen_depth_mm":18.4,"flipper_length_mm":184,"body_mass_g":3475,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.8,"culmen_depth_mm":19.1,"flipper_length_mm":184,"body_mass_g":4650,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.9,"culmen_depth_mm":18.9,"flipper_length_mm":184,"body_mass_g":3900,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.5,"culmen_depth_mm":17.9,"flipper_length_mm":192,"body_mass_g":3500,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37.3,"culmen_depth_mm":16.8,"flipper_length_mm":192,"body_mass_g":3000,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":43.2,"culmen_depth_mm":18.5,"flipper_length_mm":192,"body_mass_g":4100,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.9,"culmen_depth_mm":16.6,"flipper_length_mm":192,"body_mass_g":2700,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.5,"culmen_depth_mm":18.4,"flipper_length_mm":200,"body_mass_g":3400,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49.5,"culmen_depth_mm":19,"flipper_length_mm":200,"body_mass_g":3800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.2,"culmen_depth_mm":20.1,"flipper_length_mm":200,"body_mass_g":3975,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.8,"culmen_depth_mm":18.9,"flipper_length_mm":208,"body_mass_g":4300,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39,"culmen_depth_mm":18.7,"flipper_length_mm":185,"body_mass_g":3650,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37,"culmen_depth_mm":16.9,"flipper_length_mm":185,"body_mass_g":3000,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":47,"culmen_depth_mm":17.3,"flipper_length_mm":185,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":34,"culmen_depth_mm":17.1,"flipper_length_mm":185,"body_mass_g":3400,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37,"culmen_depth_mm":16.5,"flipper_length_mm":185,"body_mass_g":3400,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.7,"culmen_depth_mm":17.3,"flipper_length_mm":193,"body_mass_g":3600,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.6,"culmen_depth_mm":19.4,"flipper_length_mm":193,"body_mass_g":3800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.7,"culmen_depth_mm":17.9,"flipper_length_mm":193,"body_mass_g":4250,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37.8,"culmen_depth_mm":18.1,"flipper_length_mm":193,"body_mass_g":3750,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.6,"culmen_depth_mm":17.8,"flipper_length_mm":193,"body_mass_g":3800,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51.3,"culmen_depth_mm":19.2,"flipper_length_mm":193,"body_mass_g":3650,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.2,"culmen_depth_mm":17.1,"flipper_length_mm":193,"body_mass_g":3400,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36.8,"culmen_depth_mm":18.5,"flipper_length_mm":193,"body_mass_g":3500,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49.6,"culmen_depth_mm":18.2,"flipper_length_mm":193,"body_mass_g":3775,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":52,"culmen_depth_mm":18.1,"flipper_length_mm":201,"body_mass_g":4050,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":41.5,"culmen_depth_mm":18.5,"flipper_length_mm":201,"body_mass_g":4000,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.5,"culmen_depth_mm":19.6,"flipper_length_mm":201,"body_mass_g":4050,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":54.2,"culmen_depth_mm":20.8,"flipper_length_mm":201,"body_mass_g":4300,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51.4,"culmen_depth_mm":19,"flipper_length_mm":201,"body_mass_g":3950,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.8,"culmen_depth_mm":18.5,"flipper_length_mm":201,"body_mass_g":4450,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":33.1,"culmen_depth_mm":16.1,"flipper_length_mm":178,"body_mass_g":2900,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37.2,"culmen_depth_mm":18.1,"flipper_length_mm":178,"body_mass_g":3900,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.5,"culmen_depth_mm":16.7,"flipper_length_mm":178,"body_mass_g":3250,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.1,"culmen_depth_mm":18.2,"flipper_length_mm":178,"body_mass_g":3250,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36,"culmen_depth_mm":18.5,"flipper_length_mm":186,"body_mass_g":3100,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.6,"culmen_depth_mm":18.1,"flipper_length_mm":186,"body_mass_g":4450,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51.7,"culmen_depth_mm":20.3,"flipper_length_mm":194,"body_mass_g":3775,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":41.3,"culmen_depth_mm":20.3,"flipper_length_mm":194,"body_mass_g":3550,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.6,"culmen_depth_mm":19.4,"flipper_length_mm":194,"body_mass_g":3525,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.2,"culmen_depth_mm":18.8,"flipper_length_mm":202,"body_mass_g":3800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":35.7,"culmen_depth_mm":18,"flipper_length_mm":202,"body_mass_g":3550,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":43.5,"culmen_depth_mm":18.1,"flipper_length_mm":202,"body_mass_g":3400,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49,"culmen_depth_mm":19.5,"flipper_length_mm":210,"body_mass_g":3950,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.8,"culmen_depth_mm":19,"flipper_length_mm":210,"body_mass_g":4100,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":52,"culmen_depth_mm":20.7,"flipper_length_mm":210,"body_mass_g":4800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37.5,"culmen_depth_mm":18.9,"flipper_length_mm":179,"body_mass_g":2975} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.2,"culmen_depth_mm":17.5,"flipper_length_mm":187,"body_mass_g":3650,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51.5,"culmen_depth_mm":18.7,"flipper_length_mm":187,"body_mass_g":3250,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":40.9,"culmen_depth_mm":16.6,"flipper_length_mm":187,"body_mass_g":3200,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":42.5,"culmen_depth_mm":16.7,"flipper_length_mm":187,"body_mass_g":3350,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":38.1,"culmen_depth_mm":17.6,"flipper_length_mm":187,"body_mass_g":3425,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":42.5,"culmen_depth_mm":17.3,"flipper_length_mm":187,"body_mass_g":3350,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36,"culmen_depth_mm":17.1,"flipper_length_mm":187,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.6,"culmen_depth_mm":17.2,"flipper_length_mm":187,"body_mass_g":3475,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36.2,"culmen_depth_mm":17.3,"flipper_length_mm":187,"body_mass_g":3300,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":43.2,"culmen_depth_mm":16.6,"flipper_length_mm":187,"body_mass_g":2900,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49.2,"culmen_depth_mm":18.2,"flipper_length_mm":195,"body_mass_g":4400,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":47.6,"culmen_depth_mm":18.3,"flipper_length_mm":195,"body_mass_g":3850,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.7,"culmen_depth_mm":17,"flipper_length_mm":195,"body_mass_g":3650,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.7,"culmen_depth_mm":17.9,"flipper_length_mm":195,"body_mass_g":3300,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46,"culmen_depth_mm":18.9,"flipper_length_mm":195,"body_mass_g":4150,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36.4,"culmen_depth_mm":17,"flipper_length_mm":195,"body_mass_g":3325,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49.7,"culmen_depth_mm":18.6,"flipper_length_mm":195,"body_mass_g":3600,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36,"culmen_depth_mm":17.8,"flipper_length_mm":195,"body_mass_g":3450,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.8,"culmen_depth_mm":18.4,"flipper_length_mm":195,"body_mass_g":3900,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49.3,"culmen_depth_mm":19.9,"flipper_length_mm":203,"body_mass_g":4050,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.7,"culmen_depth_mm":19.7,"flipper_length_mm":203,"body_mass_g":4050,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51,"culmen_depth_mm":18.8,"flipper_length_mm":203,"body_mass_g":4100,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":42.2,"culmen_depth_mm":18.5,"flipper_length_mm":180,"body_mass_g":3550,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.5,"culmen_depth_mm":17.8,"flipper_length_mm":188,"body_mass_g":3300,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":32.1,"culmen_depth_mm":15.5,"flipper_length_mm":188,"body_mass_g":3050,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.4,"culmen_depth_mm":18.7,"flipper_length_mm":188,"body_mass_g":3525,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.9,"culmen_depth_mm":19.1,"flipper_length_mm":196,"body_mass_g":3550,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.3,"culmen_depth_mm":18.5,"flipper_length_mm":196,"body_mass_g":4350,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.5,"culmen_depth_mm":17,"flipper_length_mm":196,"body_mass_g":3500,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50,"culmen_depth_mm":19.5,"flipper_length_mm":196,"body_mass_g":3900,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.2,"culmen_depth_mm":21.1,"flipper_length_mm":196,"body_mass_g":4150,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":44.1,"culmen_depth_mm":19.7,"flipper_length_mm":196,"body_mass_g":4400,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.9,"culmen_depth_mm":17.9,"flipper_length_mm":196,"body_mass_g":3675,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49,"culmen_depth_mm":19.6,"flipper_length_mm":212,"body_mass_g":4300,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37.6,"culmen_depth_mm":19.3,"flipper_length_mm":181,"body_mass_g":3300,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":42.4,"culmen_depth_mm":17.3,"flipper_length_mm":181,"body_mass_g":3600,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":58,"culmen_depth_mm":17.8,"flipper_length_mm":181,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":38.3,"culmen_depth_mm":19.2,"flipper_length_mm":189,"body_mass_g":3950,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36.9,"culmen_depth_mm":18.6,"flipper_length_mm":189,"body_mass_g":3500,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.8,"culmen_depth_mm":16.5,"flipper_length_mm":189,"body_mass_g":3650,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":52.2,"culmen_depth_mm":18.8,"flipper_length_mm":197,"body_mass_g":3450,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51.3,"culmen_depth_mm":18.2,"flipper_length_mm":197,"body_mass_g":3750,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":52.7,"culmen_depth_mm":19.8,"flipper_length_mm":197,"body_mass_g":3725,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":52,"culmen_depth_mm":19,"flipper_length_mm":197,"body_mass_g":4150,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.3,"culmen_depth_mm":20,"flipper_length_mm":197,"body_mass_g":3300,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":53.5,"culmen_depth_mm":19.9,"flipper_length_mm":205,"body_mass_g":4500,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":41.1,"culmen_depth_mm":18.1,"flipper_length_mm":205,"body_mass_g":4300,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":52.8,"culmen_depth_mm":20,"flipper_length_mm":205,"body_mass_g":4550,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36.5,"culmen_depth_mm":18,"flipper_length_mm":182,"body_mass_g":3150,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":41.1,"culmen_depth_mm":19,"flipper_length_mm":182,"body_mass_g":3425,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36,"culmen_depth_mm":17.9,"flipper_length_mm":190,"body_mass_g":3450,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":41.1,"culmen_depth_mm":17.5,"flipper_length_mm":190,"body_mass_g":3900,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":36.3,"culmen_depth_mm":19.5,"flipper_length_mm":190,"body_mass_g":3800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.2,"culmen_depth_mm":18.6,"flipper_length_mm":190,"body_mass_g":4250,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":38.9,"culmen_depth_mm":18.8,"flipper_length_mm":190,"body_mass_g":3600,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":39.6,"culmen_depth_mm":18.8,"flipper_length_mm":190,"body_mass_g":4600,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":40.7,"culmen_depth_mm":17,"flipper_length_mm":190,"body_mass_g":3725,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.9,"culmen_depth_mm":17.1,"flipper_length_mm":190,"body_mass_g":3575,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":38.1,"culmen_depth_mm":18.6,"flipper_length_mm":190,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":38.8,"culmen_depth_mm":20,"flipper_length_mm":190,"body_mass_g":3950,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.4,"culmen_depth_mm":18.6,"flipper_length_mm":190,"body_mass_g":3450,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.1,"culmen_depth_mm":17.9,"flipper_length_mm":190,"body_mass_g":3400,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.2,"culmen_depth_mm":17.8,"flipper_length_mm":198,"body_mass_g":3950,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51.3,"culmen_depth_mm":19.9,"flipper_length_mm":198,"body_mass_g":3700,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":49.8,"culmen_depth_mm":17.3,"flipper_length_mm":198,"body_mass_g":3675,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":50.2,"culmen_depth_mm":18.7,"flipper_length_mm":198,"body_mass_g":3775,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":51.9,"culmen_depth_mm":19.5,"flipper_length_mm":206,"body_mass_g":3950,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":45.2,"culmen_depth_mm":16.6,"flipper_length_mm":191,"body_mass_g":3250,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":48.5,"culmen_depth_mm":17.5,"flipper_length_mm":191,"body_mass_g":3400,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":42.3,"culmen_depth_mm":21.2,"flipper_length_mm":191,"body_mass_g":4150,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37.3,"culmen_depth_mm":17.8,"flipper_length_mm":191,"body_mass_g":3350,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":35.6,"culmen_depth_mm":17.5,"flipper_length_mm":191,"body_mass_g":3175,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":46.4,"culmen_depth_mm":17.8,"flipper_length_mm":191,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":47.5,"culmen_depth_mm":16.8,"flipper_length_mm":199,"body_mass_g":3900,"sex":"FEMALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":48.1,"culmen_depth_mm":16.4,"flipper_length_mm":199,"body_mass_g":3325,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Dream","culmen_length_mm":37.5,"culmen_depth_mm":18.5,"flipper_length_mm":199,"body_mass_g":4475,"sex":"MALE"} +{"species":"Chinstrap penguin (Pygoscelis antarctica)","island":"Dream","culmen_length_mm":55.8,"culmen_depth_mm":19.8,"flipper_length_mm":207,"body_mass_g":4000,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":39.7,"culmen_depth_mm":18.9,"flipper_length_mm":184,"body_mass_g":3550,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":36.4,"culmen_depth_mm":17.1,"flipper_length_mm":184,"body_mass_g":2850,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":41.6,"culmen_depth_mm":18,"flipper_length_mm":192,"body_mass_g":3950,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":35,"culmen_depth_mm":17.9,"flipper_length_mm":192,"body_mass_g":3725,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":41.1,"culmen_depth_mm":18.2,"flipper_length_mm":192,"body_mass_g":4050,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":42,"culmen_depth_mm":19.5,"flipper_length_mm":200,"body_mass_g":4050,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.8,"culmen_depth_mm":13.9,"flipper_length_mm":208,"body_mass_g":4300,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.3,"culmen_depth_mm":14,"flipper_length_mm":208,"body_mass_g":4575,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":44,"culmen_depth_mm":13.6,"flipper_length_mm":208,"body_mass_g":4350,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":42.7,"culmen_depth_mm":13.7,"flipper_length_mm":208,"body_mass_g":3950,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.3,"culmen_depth_mm":13.8,"flipper_length_mm":208,"body_mass_g":4200,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.2,"culmen_depth_mm":14.5,"flipper_length_mm":208,"body_mass_g":4450,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.7,"culmen_depth_mm":15.7,"flipper_length_mm":208,"body_mass_g":5350,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.5,"culmen_depth_mm":15.2,"flipper_length_mm":216,"body_mass_g":5000,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.4,"culmen_depth_mm":15.8,"flipper_length_mm":216,"body_mass_g":4925,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.3,"culmen_depth_mm":13.8,"flipper_length_mm":216,"body_mass_g":4725} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.6,"culmen_depth_mm":15,"flipper_length_mm":216,"body_mass_g":4750,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":44.5,"culmen_depth_mm":14.3,"flipper_length_mm":216,"body_mass_g":4100} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.4,"culmen_depth_mm":15,"flipper_length_mm":216,"body_mass_g":4700,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49,"culmen_depth_mm":16.1,"flipper_length_mm":216,"body_mass_g":5550,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.7,"culmen_depth_mm":15,"flipper_length_mm":216,"body_mass_g":4750,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.5,"culmen_depth_mm":16.1,"flipper_length_mm":224,"body_mass_g":5650,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50,"culmen_depth_mm":15.9,"flipper_length_mm":224,"body_mass_g":5350,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.4,"culmen_depth_mm":15.3,"flipper_length_mm":224,"body_mass_g":5550,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":35.7,"culmen_depth_mm":16.9,"flipper_length_mm":185,"body_mass_g":3150,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.6,"culmen_depth_mm":17,"flipper_length_mm":185,"body_mass_g":3600,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":38.2,"culmen_depth_mm":18.1,"flipper_length_mm":185,"body_mass_g":3950,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":40.6,"culmen_depth_mm":18.8,"flipper_length_mm":193,"body_mass_g":3800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":39.7,"culmen_depth_mm":17.7,"flipper_length_mm":193,"body_mass_g":3200,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.9,"culmen_depth_mm":18.6,"flipper_length_mm":193,"body_mass_g":2925,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.1,"culmen_depth_mm":15.1,"flipper_length_mm":209,"body_mass_g":5500,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.2,"culmen_depth_mm":14.5,"flipper_length_mm":209,"body_mass_g":4800,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.3,"culmen_depth_mm":13.4,"flipper_length_mm":209,"body_mass_g":4400,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":42.8,"culmen_depth_mm":14.2,"flipper_length_mm":209,"body_mass_g":4700,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.5,"culmen_depth_mm":14.2,"flipper_length_mm":209,"body_mass_g":4600,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.6,"culmen_depth_mm":13.9,"flipper_length_mm":217,"body_mass_g":4900,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.3,"culmen_depth_mm":15.7,"flipper_length_mm":217,"body_mass_g":5850,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.5,"culmen_depth_mm":14.8,"flipper_length_mm":217,"body_mass_g":5200,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.5,"culmen_depth_mm":14.4,"flipper_length_mm":217,"body_mass_g":4900,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":44.5,"culmen_depth_mm":15.7,"flipper_length_mm":217,"body_mass_g":4875,"sex":"."} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.2,"culmen_depth_mm":14.1,"flipper_length_mm":217,"body_mass_g":4375,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.1,"culmen_depth_mm":15,"flipper_length_mm":225,"body_mass_g":5000,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":51.1,"culmen_depth_mm":16.5,"flipper_length_mm":225,"body_mass_g":5250,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.6,"culmen_depth_mm":16,"flipper_length_mm":225,"body_mass_g":5700,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.5,"culmen_depth_mm":15.9,"flipper_length_mm":225,"body_mass_g":5400,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":39,"culmen_depth_mm":17.5,"flipper_length_mm":186,"body_mass_g":3550,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":39.6,"culmen_depth_mm":17.7,"flipper_length_mm":186,"body_mass_g":3500,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.6,"culmen_depth_mm":19.1,"flipper_length_mm":194,"body_mass_g":3750,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.8,"culmen_depth_mm":14.6,"flipper_length_mm":210,"body_mass_g":4200,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.5,"culmen_depth_mm":13.5,"flipper_length_mm":210,"body_mass_g":4550,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.3,"culmen_depth_mm":13.7,"flipper_length_mm":210,"body_mass_g":4300,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.7,"culmen_depth_mm":14.1,"flipper_length_mm":210,"body_mass_g":4450,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":42,"culmen_depth_mm":13.5,"flipper_length_mm":210,"body_mass_g":4150,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.1,"culmen_depth_mm":14.4,"flipper_length_mm":210,"body_mass_g":4400,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.5,"culmen_depth_mm":13.9,"flipper_length_mm":210,"body_mass_g":4200,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.2,"culmen_depth_mm":14.3,"flipper_length_mm":210,"body_mass_g":4600,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.6,"culmen_depth_mm":14.2,"flipper_length_mm":210,"body_mass_g":4850,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":41.7,"culmen_depth_mm":14.7,"flipper_length_mm":210,"body_mass_g":4700,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.2,"culmen_depth_mm":14.3,"flipper_length_mm":218,"body_mass_g":5700,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.5,"culmen_depth_mm":15,"flipper_length_mm":218,"body_mass_g":4950,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":51.3,"culmen_depth_mm":14.2,"flipper_length_mm":218,"body_mass_g":5300,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50,"culmen_depth_mm":15.2,"flipper_length_mm":218,"body_mass_g":5700,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.4,"culmen_depth_mm":14.4,"flipper_length_mm":218,"body_mass_g":4600,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.8,"culmen_depth_mm":15.7,"flipper_length_mm":226,"body_mass_g":5200,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":35.3,"culmen_depth_mm":18.9,"flipper_length_mm":187,"body_mass_g":3800,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":40.5,"culmen_depth_mm":17.9,"flipper_length_mm":187,"body_mass_g":3200,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":34.5,"culmen_depth_mm":18.1,"flipper_length_mm":187,"body_mass_g":2900,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":41.3,"culmen_depth_mm":21.1,"flipper_length_mm":195,"body_mass_g":4400,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":35.5,"culmen_depth_mm":16.2,"flipper_length_mm":195,"body_mass_g":3350,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":41,"culmen_depth_mm":20,"flipper_length_mm":203,"body_mass_g":4725,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.4,"culmen_depth_mm":14.4,"flipper_length_mm":203,"body_mass_g":4625,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.4,"culmen_depth_mm":14.6,"flipper_length_mm":211,"body_mass_g":4800,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.1,"culmen_depth_mm":13.2,"flipper_length_mm":211,"body_mass_g":4500,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":44.4,"culmen_depth_mm":17.3,"flipper_length_mm":219,"body_mass_g":5250,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.8,"culmen_depth_mm":14.2,"flipper_length_mm":219,"body_mass_g":4700,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":53.4,"culmen_depth_mm":15.8,"flipper_length_mm":219,"body_mass_g":5500,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.5,"culmen_depth_mm":15,"flipper_length_mm":219,"body_mass_g":4850,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.7,"culmen_depth_mm":15.3,"flipper_length_mm":219,"body_mass_g":5200,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.9,"culmen_depth_mm":18.6,"flipper_length_mm":172,"body_mass_g":3150,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":38.8,"culmen_depth_mm":17.2,"flipper_length_mm":180,"body_mass_g":3800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.7,"culmen_depth_mm":18.7,"flipper_length_mm":180,"body_mass_g":3600,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":40.5,"culmen_depth_mm":18.9,"flipper_length_mm":180,"body_mass_g":3950,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":41.1,"culmen_depth_mm":19.1,"flipper_length_mm":188,"body_mass_g":4100,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":40.1,"culmen_depth_mm":18.9,"flipper_length_mm":188,"body_mass_g":4300,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":42.7,"culmen_depth_mm":18.3,"flipper_length_mm":196,"body_mass_g":4075,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.5,"culmen_depth_mm":14,"flipper_length_mm":212,"body_mass_g":4875,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.4,"culmen_depth_mm":14.6,"flipper_length_mm":212,"body_mass_g":4725,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.5,"culmen_depth_mm":14.5,"flipper_length_mm":212,"body_mass_g":4750,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.2,"culmen_depth_mm":14.8,"flipper_length_mm":212,"body_mass_g":5200,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":44.9,"culmen_depth_mm":13.8,"flipper_length_mm":212,"body_mass_g":4750,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.1,"culmen_depth_mm":14.5,"flipper_length_mm":212,"body_mass_g":4625,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.4,"culmen_depth_mm":16.3,"flipper_length_mm":220,"body_mass_g":5400,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50,"culmen_depth_mm":15.3,"flipper_length_mm":220,"body_mass_g":5550,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45,"culmen_depth_mm":15.4,"flipper_length_mm":220,"body_mass_g":5050,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.5,"culmen_depth_mm":15,"flipper_length_mm":220,"body_mass_g":5000,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.5,"culmen_depth_mm":14.1,"flipper_length_mm":220,"body_mass_g":5300,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":51.1,"culmen_depth_mm":16.3,"flipper_length_mm":220,"body_mass_g":6000,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.1,"culmen_depth_mm":14.8,"flipper_length_mm":220,"body_mass_g":5150,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.5,"culmen_depth_mm":14.2,"flipper_length_mm":220,"body_mass_g":4700,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.1,"culmen_depth_mm":15,"flipper_length_mm":228,"body_mass_g":5500,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":55.9,"culmen_depth_mm":17,"flipper_length_mm":228,"body_mass_g":5600,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.8,"culmen_depth_mm":17.3,"flipper_length_mm":228,"body_mass_g":5600,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":52.2,"culmen_depth_mm":17.1,"flipper_length_mm":228,"body_mass_g":5400,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":38.1,"culmen_depth_mm":17,"flipper_length_mm":181,"body_mass_g":3175,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":36.5,"culmen_depth_mm":16.6,"flipper_length_mm":181,"body_mass_g":2850,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":35.9,"culmen_depth_mm":19.2,"flipper_length_mm":189,"body_mass_g":3800,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":42.2,"culmen_depth_mm":19.5,"flipper_length_mm":197,"body_mass_g":4275,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":43.2,"culmen_depth_mm":19,"flipper_length_mm":197,"body_mass_g":4775,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":42.6,"culmen_depth_mm":13.7,"flipper_length_mm":213,"body_mass_g":4950,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":44.9,"culmen_depth_mm":13.3,"flipper_length_mm":213,"body_mass_g":5100,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.5,"culmen_depth_mm":14.5,"flipper_length_mm":213,"body_mass_g":4400,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":43.5,"culmen_depth_mm":15.2,"flipper_length_mm":213,"body_mass_g":4650,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.4,"culmen_depth_mm":14.6,"flipper_length_mm":213,"body_mass_g":5850,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.9,"culmen_depth_mm":16.1,"flipper_length_mm":213,"body_mass_g":5400,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.4,"culmen_depth_mm":15.6,"flipper_length_mm":221,"body_mass_g":5000,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.2,"culmen_depth_mm":15.2,"flipper_length_mm":221,"body_mass_g":6300,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.2,"culmen_depth_mm":14.9,"flipper_length_mm":221,"body_mass_g":5300,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":52.5,"culmen_depth_mm":15.6,"flipper_length_mm":221,"body_mass_g":5450,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.2,"culmen_depth_mm":15.6,"flipper_length_mm":221,"body_mass_g":5100,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.8,"culmen_depth_mm":15.9,"flipper_length_mm":229,"body_mass_g":5950,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.5,"culmen_depth_mm":16.2,"flipper_length_mm":229,"body_mass_g":5800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.8,"culmen_depth_mm":18.3,"flipper_length_mm":174,"body_mass_g":3400,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":35,"culmen_depth_mm":17.9,"flipper_length_mm":190,"body_mass_g":3450,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":38.2,"culmen_depth_mm":20,"flipper_length_mm":190,"body_mass_g":3900,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.8,"culmen_depth_mm":20,"flipper_length_mm":190,"body_mass_g":4250,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":38.1,"culmen_depth_mm":16.5,"flipper_length_mm":198,"body_mass_g":3825,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.5,"culmen_depth_mm":13.7,"flipper_length_mm":214,"body_mass_g":4650,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.2,"culmen_depth_mm":14.4,"flipper_length_mm":214,"body_mass_g":4650} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.7,"culmen_depth_mm":13.9,"flipper_length_mm":214,"body_mass_g":4400,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":40.9,"culmen_depth_mm":13.7,"flipper_length_mm":214,"body_mass_g":4650,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":44.5,"culmen_depth_mm":14.7,"flipper_length_mm":214,"body_mass_g":4850,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.2,"culmen_depth_mm":13.7,"flipper_length_mm":214,"body_mass_g":4925,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.8,"culmen_depth_mm":16.2,"flipper_length_mm":222,"body_mass_g":6000,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.9,"culmen_depth_mm":14.6,"flipper_length_mm":222,"body_mass_g":4875,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.7,"culmen_depth_mm":15.1,"flipper_length_mm":222,"body_mass_g":5350,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.4,"culmen_depth_mm":15.7,"flipper_length_mm":222,"body_mass_g":5750,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.3,"culmen_depth_mm":15.3,"flipper_length_mm":222,"body_mass_g":5250,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.5,"culmen_depth_mm":15.9,"flipper_length_mm":222,"body_mass_g":5550,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":52.1,"culmen_depth_mm":17,"flipper_length_mm":230,"body_mass_g":5550,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50,"culmen_depth_mm":16.3,"flipper_length_mm":230,"body_mass_g":5700,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":51.5,"culmen_depth_mm":16.3,"flipper_length_mm":230,"body_mass_g":5500,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":59.6,"culmen_depth_mm":17,"flipper_length_mm":230,"body_mass_g":6050,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":48.6,"culmen_depth_mm":16,"flipper_length_mm":230,"body_mass_g":5800,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":49.8,"culmen_depth_mm":16.8,"flipper_length_mm":230,"body_mass_g":5700,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":55.1,"culmen_depth_mm":16,"flipper_length_mm":230,"body_mass_g":5850,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":37.7,"culmen_depth_mm":16,"flipper_length_mm":183,"body_mass_g":3075,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":40.6,"culmen_depth_mm":18.6,"flipper_length_mm":183,"body_mass_g":3550,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":41.4,"culmen_depth_mm":18.6,"flipper_length_mm":191,"body_mass_g":3700,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":45.6,"culmen_depth_mm":20.3,"flipper_length_mm":191,"body_mass_g":4600,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":39.6,"culmen_depth_mm":20.7,"flipper_length_mm":191,"body_mass_g":3900,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Biscoe","culmen_length_mm":38.6,"culmen_depth_mm":17.2,"flipper_length_mm":199,"body_mass_g":3750,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.1,"culmen_depth_mm":14.5,"flipper_length_mm":207,"body_mass_g":5050,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.8,"culmen_depth_mm":15.4,"flipper_length_mm":215,"body_mass_g":5150,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.8,"culmen_depth_mm":15,"flipper_length_mm":215,"body_mass_g":5650,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.1,"culmen_depth_mm":14.5,"flipper_length_mm":215,"body_mass_g":5000,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.6,"culmen_depth_mm":14.5,"flipper_length_mm":215,"body_mass_g":5400,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.8,"culmen_depth_mm":16.1,"flipper_length_mm":215,"body_mass_g":5500,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.3,"culmen_depth_mm":15.8,"flipper_length_mm":215,"body_mass_g":5050,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.2,"culmen_depth_mm":15.8,"flipper_length_mm":215,"body_mass_g":5300,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.2,"culmen_depth_mm":13.8,"flipper_length_mm":215,"body_mass_g":4750,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":42.9,"culmen_depth_mm":13.1,"flipper_length_mm":215,"body_mass_g":5000,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.1,"culmen_depth_mm":15.1,"flipper_length_mm":215,"body_mass_g":5100,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":46.8,"culmen_depth_mm":14.3,"flipper_length_mm":215,"body_mass_g":4850,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":47.2,"culmen_depth_mm":15.5,"flipper_length_mm":215,"body_mass_g":4975,"sex":"FEMALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":50.7,"culmen_depth_mm":15,"flipper_length_mm":223,"body_mass_g":5550,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":45.2,"culmen_depth_mm":16.4,"flipper_length_mm":223,"body_mass_g":5950,"sex":"MALE"} +{"species":"Gentoo penguin (Pygoscelis papua)","island":"Biscoe","culmen_length_mm":54.3,"culmen_depth_mm":15.7,"flipper_length_mm":231,"body_mass_g":5650,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":40.2,"culmen_depth_mm":17,"flipper_length_mm":176,"body_mass_g":3450,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":34.4,"culmen_depth_mm":18.4,"flipper_length_mm":184,"body_mass_g":3325,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":37.2,"culmen_depth_mm":19.4,"flipper_length_mm":184,"body_mass_g":3900,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":36.6,"culmen_depth_mm":17.8,"flipper_length_mm":185,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":35.1,"culmen_depth_mm":19.4,"flipper_length_mm":193,"body_mass_g":4200,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":36.7,"culmen_depth_mm":19.3,"flipper_length_mm":193,"body_mass_g":3450,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":34.1,"culmen_depth_mm":18.1,"flipper_length_mm":193,"body_mass_g":3475} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":37.8,"culmen_depth_mm":17.1,"flipper_length_mm":186,"body_mass_g":3300} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":35.2,"culmen_depth_mm":15.9,"flipper_length_mm":186,"body_mass_g":3050,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":39.5,"culmen_depth_mm":17.4,"flipper_length_mm":186,"body_mass_g":3800,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":46,"culmen_depth_mm":21.5,"flipper_length_mm":194,"body_mass_g":4200,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":41.4,"culmen_depth_mm":18.5,"flipper_length_mm":202,"body_mass_g":3875,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":44.1,"culmen_depth_mm":18,"flipper_length_mm":210,"body_mass_g":4000,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":36.2,"culmen_depth_mm":16.1,"flipper_length_mm":187,"body_mass_g":3550,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":36.2,"culmen_depth_mm":17.2,"flipper_length_mm":187,"body_mass_g":3150,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":36.7,"culmen_depth_mm":18.8,"flipper_length_mm":187,"body_mass_g":3800,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":41.5,"culmen_depth_mm":18.3,"flipper_length_mm":195,"body_mass_g":4300,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":42.1,"culmen_depth_mm":19.1,"flipper_length_mm":195,"body_mass_g":4000,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":40.3,"culmen_depth_mm":18,"flipper_length_mm":195,"body_mass_g":3250,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":39.2,"culmen_depth_mm":19.6,"flipper_length_mm":195,"body_mass_g":4675,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":38.7,"culmen_depth_mm":19,"flipper_length_mm":195,"body_mass_g":3450,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":42.8,"culmen_depth_mm":18.5,"flipper_length_mm":195,"body_mass_g":4250,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":37.8,"culmen_depth_mm":17.3,"flipper_length_mm":180,"body_mass_g":3700} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":38.6,"culmen_depth_mm":17,"flipper_length_mm":188,"body_mass_g":2900,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":42.9,"culmen_depth_mm":17.6,"flipper_length_mm":196,"body_mass_g":4700,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":39.6,"culmen_depth_mm":17.2,"flipper_length_mm":196,"body_mass_g":3550,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":39.1,"culmen_depth_mm":18.7,"flipper_length_mm":181,"body_mass_g":3750,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":38.9,"culmen_depth_mm":17.8,"flipper_length_mm":181,"body_mass_g":3625,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":35.7,"culmen_depth_mm":17,"flipper_length_mm":189,"body_mass_g":3350,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":41.1,"culmen_depth_mm":18.6,"flipper_length_mm":189,"body_mass_g":3325,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":34.6,"culmen_depth_mm":17.2,"flipper_length_mm":189,"body_mass_g":3200,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":42.5,"culmen_depth_mm":20.7,"flipper_length_mm":197,"body_mass_g":4500,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":43.1,"culmen_depth_mm":19.2,"flipper_length_mm":197,"body_mass_g":3500,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":45.8,"culmen_depth_mm":18.9,"flipper_length_mm":197,"body_mass_g":4150,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":41.1,"culmen_depth_mm":17.6,"flipper_length_mm":182,"body_mass_g":3200,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":35.9,"culmen_depth_mm":16.6,"flipper_length_mm":190,"body_mass_g":3050,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":33.5,"culmen_depth_mm":19,"flipper_length_mm":190,"body_mass_g":3600,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":39.7,"culmen_depth_mm":18.4,"flipper_length_mm":190,"body_mass_g":3900,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":35.5,"culmen_depth_mm":17.5,"flipper_length_mm":190,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":42,"culmen_depth_mm":20.2,"flipper_length_mm":190,"body_mass_g":4250} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":39.3,"culmen_depth_mm":20.6,"flipper_length_mm":190,"body_mass_g":3650,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":38.5,"culmen_depth_mm":17.9,"flipper_length_mm":190,"body_mass_g":3325,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":37.7,"culmen_depth_mm":19.8,"flipper_length_mm":198,"body_mass_g":3500,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":34.6,"culmen_depth_mm":21.1,"flipper_length_mm":198,"body_mass_g":4400,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":41.8,"culmen_depth_mm":19.4,"flipper_length_mm":198,"body_mass_g":4450,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":38.6,"culmen_depth_mm":21.2,"flipper_length_mm":191,"body_mass_g":3800,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":38.8,"culmen_depth_mm":17.6,"flipper_length_mm":191,"body_mass_g":3275,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":40.9,"culmen_depth_mm":16.8,"flipper_length_mm":191,"body_mass_g":3700,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":39,"culmen_depth_mm":17.1,"flipper_length_mm":191,"body_mass_g":3050,"sex":"FEMALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":40.6,"culmen_depth_mm":19,"flipper_length_mm":199,"body_mass_g":4000,"sex":"MALE"} +{"species":"Adelie Penguin (Pygoscelis adeliae)","island":"Torgersen","culmen_length_mm":37.3,"culmen_depth_mm":20.5,"flipper_length_mm":199,"body_mass_g":3775,"sex":"MALE"} diff --git a/tests/data/penguins_schema.json b/tests/data/penguins_schema.json new file mode 100644 index 00000000000..104323547fc --- /dev/null +++ b/tests/data/penguins_schema.json @@ -0,0 +1,37 @@ +[ + { + "mode": "NULLABLE", + "name": "species", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "island", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "culmen_length_mm", + "type": "FLOAT" + }, + { + "mode": "NULLABLE", + "name": "culmen_depth_mm", + "type": "FLOAT" + }, + { + "mode": "NULLABLE", + "name": "flipper_length_mm", + "type": "FLOAT" + }, + { + "mode": "NULLABLE", + "name": "body_mass_g", + "type": "FLOAT" + }, + { + "mode": "NULLABLE", + "name": "sex", + "type": "STRING" + } + ] diff --git a/tests/data/scalars.jsonl b/tests/data/scalars.jsonl new file mode 100644 index 00000000000..172a55ec11a --- /dev/null +++ b/tests/data/scalars.jsonl @@ -0,0 +1,9 @@ +{"bool_col": true, "bytes_col": "SGVsbG8sIFdvcmxkIQ==", "date_col": "2021-07-21", "datetime_col": "2021-07-21 11:39:45", "geography_col": "POINT(-122.0838511 37.3860517)", "int64_col": "123456789", "int64_too": "0", "numeric_col": "1.23456789", "float64_col": "1.25", "rowindex": 0, "rowindex_2": 0, "string_col": "Hello, World!", "time_col": "11:41:43.076160", "timestamp_col": "2021-07-21T17:43:43.945289Z"} +{"bool_col": false, "bytes_col": "44GT44KT44Gr44Gh44Gv", "date_col": "1991-02-03", "datetime_col": "1991-01-02 03:45:06", "geography_col": "POINT(-71.104 42.315)", "int64_col": "-987654321", "int64_too": "1", "numeric_col": "1.23456789", "float64_col": "2.51", "rowindex": 1, "rowindex_2": 1, "string_col": "こんにちは", "time_col": "11:14:34.701606", "timestamp_col": "2021-07-21T17:43:43.945289Z"} +{"bool_col": true, "bytes_col": "wqFIb2xhIE11bmRvIQ==", "date_col": "2023-03-01", "datetime_col": "2023-03-01 10:55:13", "geography_col": "POINT(-0.124474760143016 51.5007826749545)", "int64_col": "314159", "int64_too": "0", "numeric_col": "101.1010101", "float64_col": "2.5e10", "rowindex": 2, "rowindex_2": 2, "string_col": " ¡Hola Mundo! ", "time_col": "23:59:59.999999", "timestamp_col": "2023-03-01T10:55:13.250125Z"} +{"bool_col": null, "bytes_col": null, "date_col": null, "datetime_col": null, "geography_col": null, "int64_col": null, "int64_too": "1", "numeric_col": null, "float64_col": null, "rowindex": 3, "rowindex_2": 3, "string_col": null, "time_col": null, "timestamp_col": null} +{"bool_col": false, "bytes_col": "44GT44KT44Gr44Gh44Gv", "date_col": "2021-07-21", "datetime_col": null, "geography_col": null, "int64_col": "-234892", "int64_too": "-2345", "numeric_col": null, "float64_col": null, "rowindex": 4, "rowindex_2": 4, "string_col": "Hello, World!", "time_col": null, "timestamp_col": null} +{"bool_col": false, "bytes_col": "R8O8dGVuIFRhZw==", "date_col": "1980-03-14", "datetime_col": "1980-03-14 15:16:17", "geography_col": null, "int64_col": "55555", "int64_too": "0", "numeric_col": "5.555555", "float64_col": "555.555", "rowindex": 5, "rowindex_2": 5, "string_col": "Güten Tag!", "time_col": "15:16:17.181921", "timestamp_col": "1980-03-14T15:16:17.181921Z"} +{"bool_col": true, "bytes_col": "SGVsbG8JQmlnRnJhbWVzIQc=", "date_col": "2023-05-23", "datetime_col": "2023-05-23 11:37:01", "geography_col": "MULTIPOINT (20 20, 10 40, 40 30, 30 10)", "int64_col": "101202303", "int64_too": "2", "numeric_col": "-10.090807", "float64_col": "-123.456", "rowindex": 6, "rowindex_2": 6, "string_col": "capitalize, This ", "time_col": "01:02:03.456789", "timestamp_col": "2023-05-23T11:42:55.000001Z"} +{"bool_col": true, "bytes_col": null, "date_col": "2038-01-20", "datetime_col": "2038-01-19 03:14:08", "geography_col": null, "int64_col": "-214748367", "int64_too": "2", "numeric_col": "11111111.1", "float64_col": "42.42", "rowindex": 7, "rowindex_2": 7, "string_col": " سلام", "time_col": "12:00:00.000001", "timestamp_col": "2038-01-19T03:14:17.999999Z"} +{"bool_col": false, "bytes_col": null, "date_col": null, "datetime_col": null, "geography_col": null, "int64_col": "2", "int64_too": "1", "numeric_col": null, "float64_col": "6.87", "rowindex": 8, "rowindex_2": 8, "string_col": "T", "time_col": null, "timestamp_col": null} diff --git a/tests/data/scalars_schema.json b/tests/data/scalars_schema.json new file mode 100644 index 00000000000..1f5d8cdb650 --- /dev/null +++ b/tests/data/scalars_schema.json @@ -0,0 +1,75 @@ +[ + { + "mode": "NULLABLE", + "name": "bool_col", + "type": "BOOLEAN" + }, + { + "mode": "NULLABLE", + "name": "bytes_col", + "type": "BYTES" + }, + { + "mode": "NULLABLE", + "name": "date_col", + "type": "DATE" + }, + { + "mode": "NULLABLE", + "name": "datetime_col", + "type": "DATETIME" + }, + { + "mode": "NULLABLE", + "name": "geography_col", + "type": "GEOGRAPHY" + }, + { + "mode": "NULLABLE", + "name": "int64_col", + "type": "INTEGER", + "description": "Assumed to be have unique values." + }, + { + "mode": "REQUIRED", + "name": "int64_too", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "numeric_col", + "type": "NUMERIC" + }, + { + "mode": "NULLABLE", + "name": "float64_col", + "type": "FLOAT" + }, + { + "mode": "REQUIRED", + "name": "rowindex", + "type": "INTEGER", + "description": "Assumed to be sequential integers." + }, + { + "mode": "REQUIRED", + "name": "rowindex_2", + "type": "INTEGER", + "description": "Assumed to be a copy of the rowindex column." + }, + { + "mode": "NULLABLE", + "name": "string_col", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "time_col", + "type": "TIME" + }, + { + "mode": "NULLABLE", + "name": "timestamp_col", + "type": "TIMESTAMP" + } + ] diff --git a/tests/data/time_series.jsonl b/tests/data/time_series.jsonl new file mode 100644 index 00000000000..e0f9ca7ae25 --- /dev/null +++ b/tests/data/time_series.jsonl @@ -0,0 +1,366 @@ +{"parsed_date":"2017-07-01 00:00:00 UTC","total_visits":"2048"} +{"parsed_date":"2016-09-07 00:00:00 UTC","total_visits":"2562"} +{"parsed_date":"2016-10-25 00:00:00 UTC","total_visits":"3842"} +{"parsed_date":"2017-04-10 00:00:00 UTC","total_visits":"2563"} +{"parsed_date":"2017-01-09 00:00:00 UTC","total_visits":"2308"} +{"parsed_date":"2017-05-02 00:00:00 UTC","total_visits":"2564"} +{"parsed_date":"2016-11-11 00:00:00 UTC","total_visits":"3588"} +{"parsed_date":"2017-07-30 00:00:00 UTC","total_visits":"1799"} +{"parsed_date":"2017-06-10 00:00:00 UTC","total_visits":"1545"} +{"parsed_date":"2016-08-14 00:00:00 UTC","total_visits":"1801"} +{"parsed_date":"2017-05-14 00:00:00 UTC","total_visits":"1290"} +{"parsed_date":"2017-02-08 00:00:00 UTC","total_visits":"2570"} +{"parsed_date":"2017-06-01 00:00:00 UTC","total_visits":"2826"} +{"parsed_date":"2017-04-23 00:00:00 UTC","total_visits":"1548"} +{"parsed_date":"2016-11-04 00:00:00 UTC","total_visits":"3596"} +{"parsed_date":"2017-02-04 00:00:00 UTC","total_visits":"1549"} +{"parsed_date":"2016-12-09 00:00:00 UTC","total_visits":"2830"} +{"parsed_date":"2016-10-30 00:00:00 UTC","total_visits":"3086"} +{"parsed_date":"2017-03-28 00:00:00 UTC","total_visits":"2577"} +{"parsed_date":"2017-06-11 00:00:00 UTC","total_visits":"1555"} +{"parsed_date":"2016-12-17 00:00:00 UTC","total_visits":"2324"} +{"parsed_date":"2016-09-22 00:00:00 UTC","total_visits":"2581"} +{"parsed_date":"2017-01-29 00:00:00 UTC","total_visits":"1814"} +{"parsed_date":"2017-03-22 00:00:00 UTC","total_visits":"2582"} +{"parsed_date":"2017-02-21 00:00:00 UTC","total_visits":"2582"} +{"parsed_date":"2016-10-14 00:00:00 UTC","total_visits":"2838"} +{"parsed_date":"2017-04-27 00:00:00 UTC","total_visits":"2838"} +{"parsed_date":"2016-10-26 00:00:00 UTC","total_visits":"4375"} +{"parsed_date":"2016-08-22 00:00:00 UTC","total_visits":"2584"} +{"parsed_date":"2016-12-07 00:00:00 UTC","total_visits":"2840"} +{"parsed_date":"2017-01-20 00:00:00 UTC","total_visits":"2074"} +{"parsed_date":"2017-03-07 00:00:00 UTC","total_visits":"2586"} +{"parsed_date":"2017-05-16 00:00:00 UTC","total_visits":"3098"} +{"parsed_date":"2017-05-03 00:00:00 UTC","total_visits":"2588"} +{"parsed_date":"2017-05-01 00:00:00 UTC","total_visits":"2588"} +{"parsed_date":"2016-11-27 00:00:00 UTC","total_visits":"3356"} +{"parsed_date":"2017-04-29 00:00:00 UTC","total_visits":"1566"} +{"parsed_date":"2016-09-18 00:00:00 UTC","total_visits":"1822"} +{"parsed_date":"2017-03-23 00:00:00 UTC","total_visits":"2847"} +{"parsed_date":"2017-03-14 00:00:00 UTC","total_visits":"2338"} +{"parsed_date":"2016-12-21 00:00:00 UTC","total_visits":"2594"} +{"parsed_date":"2016-10-11 00:00:00 UTC","total_visits":"2850"} +{"parsed_date":"2017-01-24 00:00:00 UTC","total_visits":"3618"} +{"parsed_date":"2017-03-05 00:00:00 UTC","total_visits":"1827"} +{"parsed_date":"2017-01-19 00:00:00 UTC","total_visits":"2083"} +{"parsed_date":"2016-08-09 00:00:00 UTC","total_visits":"2851"} +{"parsed_date":"2017-04-08 00:00:00 UTC","total_visits":"1829"} +{"parsed_date":"2017-04-12 00:00:00 UTC","total_visits":"2341"} +{"parsed_date":"2016-09-29 00:00:00 UTC","total_visits":"2597"} +{"parsed_date":"2016-12-20 00:00:00 UTC","total_visits":"3110"} +{"parsed_date":"2017-01-15 00:00:00 UTC","total_visits":"1576"} +{"parsed_date":"2017-04-14 00:00:00 UTC","total_visits":"1834"} +{"parsed_date":"2017-02-28 00:00:00 UTC","total_visits":"2347"} +{"parsed_date":"2016-09-16 00:00:00 UTC","total_visits":"2603"} +{"parsed_date":"2016-10-18 00:00:00 UTC","total_visits":"3628"} +{"parsed_date":"2017-02-24 00:00:00 UTC","total_visits":"2093"} +{"parsed_date":"2017-05-17 00:00:00 UTC","total_visits":"3117"} +{"parsed_date":"2017-06-23 00:00:00 UTC","total_visits":"2095"} +{"parsed_date":"2016-11-12 00:00:00 UTC","total_visits":"3119"} +{"parsed_date":"2016-11-21 00:00:00 UTC","total_visits":"4143"} +{"parsed_date":"2017-02-27 00:00:00 UTC","total_visits":"2352"} +{"parsed_date":"2016-12-26 00:00:00 UTC","total_visits":"1586"} +{"parsed_date":"2017-04-25 00:00:00 UTC","total_visits":"2354"} +{"parsed_date":"2017-03-21 00:00:00 UTC","total_visits":"2611"} +{"parsed_date":"2016-12-22 00:00:00 UTC","total_visits":"2100"} +{"parsed_date":"2016-10-01 00:00:00 UTC","total_visits":"1589"} +{"parsed_date":"2016-09-24 00:00:00 UTC","total_visits":"1845"} +{"parsed_date":"2017-06-21 00:00:00 UTC","total_visits":"2357"} +{"parsed_date":"2016-09-02 00:00:00 UTC","total_visits":"2613"} +{"parsed_date":"2016-08-26 00:00:00 UTC","total_visits":"2359"} +{"parsed_date":"2016-10-12 00:00:00 UTC","total_visits":"2871"} +{"parsed_date":"2017-05-15 00:00:00 UTC","total_visits":"2360"} +{"parsed_date":"2017-06-12 00:00:00 UTC","total_visits":"2361"} +{"parsed_date":"2016-08-16 00:00:00 UTC","total_visits":"2873"} +{"parsed_date":"2017-04-30 00:00:00 UTC","total_visits":"1594"} +{"parsed_date":"2017-04-05 00:00:00 UTC","total_visits":"2619"} +{"parsed_date":"2016-08-12 00:00:00 UTC","total_visits":"2619"} +{"parsed_date":"2016-11-08 00:00:00 UTC","total_visits":"3899"} +{"parsed_date":"2016-08-13 00:00:00 UTC","total_visits":"1596"} +{"parsed_date":"2017-05-09 00:00:00 UTC","total_visits":"2108"} +{"parsed_date":"2017-02-23 00:00:00 UTC","total_visits":"2364"} +{"parsed_date":"2017-07-31 00:00:00 UTC","total_visits":"2620"} +{"parsed_date":"2017-06-25 00:00:00 UTC","total_visits":"1597"} +{"parsed_date":"2017-07-29 00:00:00 UTC","total_visits":"1597"} +{"parsed_date":"2016-09-17 00:00:00 UTC","total_visits":"1853"} +{"parsed_date":"2016-12-27 00:00:00 UTC","total_visits":"1855"} +{"parsed_date":"2017-05-20 00:00:00 UTC","total_visits":"1855"} +{"parsed_date":"2016-10-08 00:00:00 UTC","total_visits":"2114"} +{"parsed_date":"2016-10-27 00:00:00 UTC","total_visits":"4162"} +{"parsed_date":"2017-07-08 00:00:00 UTC","total_visits":"1859"} +{"parsed_date":"2016-08-24 00:00:00 UTC","total_visits":"2627"} +{"parsed_date":"2016-12-23 00:00:00 UTC","total_visits":"1604"} +{"parsed_date":"2017-02-02 00:00:00 UTC","total_visits":"2372"} +{"parsed_date":"2016-09-08 00:00:00 UTC","total_visits":"2628"} +{"parsed_date":"2017-04-02 00:00:00 UTC","total_visits":"1861"} +{"parsed_date":"2017-02-15 00:00:00 UTC","total_visits":"2629"} +{"parsed_date":"2017-07-05 00:00:00 UTC","total_visits":"2885"} +{"parsed_date":"2016-10-17 00:00:00 UTC","total_visits":"3397"} +{"parsed_date":"2017-02-20 00:00:00 UTC","total_visits":"2374"} +{"parsed_date":"2017-03-24 00:00:00 UTC","total_visits":"2374"} +{"parsed_date":"2017-04-20 00:00:00 UTC","total_visits":"2374"} +{"parsed_date":"2016-11-18 00:00:00 UTC","total_visits":"3654"} +{"parsed_date":"2017-07-25 00:00:00 UTC","total_visits":"2631"} +{"parsed_date":"2016-11-13 00:00:00 UTC","total_visits":"3144"} +{"parsed_date":"2017-03-18 00:00:00 UTC","total_visits":"1610"} +{"parsed_date":"2016-08-03 00:00:00 UTC","total_visits":"2890"} +{"parsed_date":"2016-08-19 00:00:00 UTC","total_visits":"2379"} +{"parsed_date":"2017-02-14 00:00:00 UTC","total_visits":"2379"} +{"parsed_date":"2017-07-11 00:00:00 UTC","total_visits":"2635"} +{"parsed_date":"2017-04-22 00:00:00 UTC","total_visits":"1612"} +{"parsed_date":"2016-10-07 00:00:00 UTC","total_visits":"2892"} +{"parsed_date":"2016-09-05 00:00:00 UTC","total_visits":"2125"} +{"parsed_date":"2016-09-23 00:00:00 UTC","total_visits":"2381"} +{"parsed_date":"2016-11-15 00:00:00 UTC","total_visits":"4685"} +{"parsed_date":"2017-01-28 00:00:00 UTC","total_visits":"1614"} +{"parsed_date":"2017-07-14 00:00:00 UTC","total_visits":"2382"} +{"parsed_date":"2017-01-07 00:00:00 UTC","total_visits":"1615"} +{"parsed_date":"2017-04-03 00:00:00 UTC","total_visits":"2383"} +{"parsed_date":"2017-03-20 00:00:00 UTC","total_visits":"2383"} +{"parsed_date":"2016-12-18 00:00:00 UTC","total_visits":"2128"} +{"parsed_date":"2017-03-17 00:00:00 UTC","total_visits":"2129"} +{"parsed_date":"2017-05-23 00:00:00 UTC","total_visits":"2129"} +{"parsed_date":"2016-11-30 00:00:00 UTC","total_visits":"4435"} +{"parsed_date":"2017-01-01 00:00:00 UTC","total_visits":"1364"} +{"parsed_date":"2017-01-02 00:00:00 UTC","total_visits":"1620"} +{"parsed_date":"2016-09-25 00:00:00 UTC","total_visits":"1877"} +{"parsed_date":"2016-08-07 00:00:00 UTC","total_visits":"1622"} +{"parsed_date":"2016-10-09 00:00:00 UTC","total_visits":"2134"} +{"parsed_date":"2017-03-01 00:00:00 UTC","total_visits":"2390"} +{"parsed_date":"2017-01-04 00:00:00 UTC","total_visits":"2390"} +{"parsed_date":"2017-06-06 00:00:00 UTC","total_visits":"2391"} +{"parsed_date":"2017-04-18 00:00:00 UTC","total_visits":"2391"} +{"parsed_date":"2017-04-06 00:00:00 UTC","total_visits":"2647"} +{"parsed_date":"2017-01-30 00:00:00 UTC","total_visits":"2392"} +{"parsed_date":"2016-10-16 00:00:00 UTC","total_visits":"2649"} +{"parsed_date":"2016-08-04 00:00:00 UTC","total_visits":"3161"} +{"parsed_date":"2016-10-21 00:00:00 UTC","total_visits":"3419"} +{"parsed_date":"2016-08-02 00:00:00 UTC","total_visits":"2140"} +{"parsed_date":"2017-03-06 00:00:00 UTC","total_visits":"2396"} +{"parsed_date":"2016-09-13 00:00:00 UTC","total_visits":"2396"} +{"parsed_date":"2016-09-14 00:00:00 UTC","total_visits":"2652"} +{"parsed_date":"2017-04-19 00:00:00 UTC","total_visits":"2397"} +{"parsed_date":"2017-06-19 00:00:00 UTC","total_visits":"2142"} +{"parsed_date":"2016-12-13 00:00:00 UTC","total_visits":"3166"} +{"parsed_date":"2017-06-20 00:00:00 UTC","total_visits":"2143"} +{"parsed_date":"2016-10-10 00:00:00 UTC","total_visits":"2911"} +{"parsed_date":"2017-07-06 00:00:00 UTC","total_visits":"2658"} +{"parsed_date":"2017-01-03 00:00:00 UTC","total_visits":"2403"} +{"parsed_date":"2017-01-08 00:00:00 UTC","total_visits":"1637"} +{"parsed_date":"2017-02-25 00:00:00 UTC","total_visits":"1638"} +{"parsed_date":"2017-05-24 00:00:00 UTC","total_visits":"2406"} +{"parsed_date":"2016-11-22 00:00:00 UTC","total_visits":"3942"} +{"parsed_date":"2017-05-06 00:00:00 UTC","total_visits":"1383"} +{"parsed_date":"2017-07-02 00:00:00 UTC","total_visits":"1895"} +{"parsed_date":"2016-12-01 00:00:00 UTC","total_visits":"4200"} +{"parsed_date":"2017-03-16 00:00:00 UTC","total_visits":"2409"} +{"parsed_date":"2016-12-12 00:00:00 UTC","total_visits":"3433"} +{"parsed_date":"2016-12-25 00:00:00 UTC","total_visits":"1386"} +{"parsed_date":"2017-02-26 00:00:00 UTC","total_visits":"1643"} +{"parsed_date":"2017-04-28 00:00:00 UTC","total_visits":"2411"} +{"parsed_date":"2016-08-11 00:00:00 UTC","total_visits":"2667"} +{"parsed_date":"2017-07-20 00:00:00 UTC","total_visits":"2668"} +{"parsed_date":"2017-05-21 00:00:00 UTC","total_visits":"1645"} +{"parsed_date":"2017-06-17 00:00:00 UTC","total_visits":"1391"} +{"parsed_date":"2016-12-29 00:00:00 UTC","total_visits":"1647"} +{"parsed_date":"2017-07-17 00:00:00 UTC","total_visits":"2671"} +{"parsed_date":"2017-01-16 00:00:00 UTC","total_visits":"1906"} +{"parsed_date":"2017-03-03 00:00:00 UTC","total_visits":"2162"} +{"parsed_date":"2016-11-14 00:00:00 UTC","total_visits":"4466"} +{"parsed_date":"2016-08-30 00:00:00 UTC","total_visits":"2675"} +{"parsed_date":"2016-08-27 00:00:00 UTC","total_visits":"1654"} +{"parsed_date":"2017-02-09 00:00:00 UTC","total_visits":"2678"} +{"parsed_date":"2017-06-03 00:00:00 UTC","total_visits":"1399"} +{"parsed_date":"2017-05-07 00:00:00 UTC","total_visits":"1400"} +{"parsed_date":"2016-11-02 00:00:00 UTC","total_visits":"3960"} +{"parsed_date":"2016-12-15 00:00:00 UTC","total_visits":"2937"} +{"parsed_date":"2017-04-01 00:00:00 UTC","total_visits":"2170"} +{"parsed_date":"2017-07-21 00:00:00 UTC","total_visits":"2427"} +{"parsed_date":"2016-08-06 00:00:00 UTC","total_visits":"1663"} +{"parsed_date":"2016-09-01 00:00:00 UTC","total_visits":"2687"} +{"parsed_date":"2017-06-28 00:00:00 UTC","total_visits":"2687"} +{"parsed_date":"2016-08-20 00:00:00 UTC","total_visits":"1664"} +{"parsed_date":"2017-04-26 00:00:00 UTC","total_visits":"4224"} +{"parsed_date":"2017-07-09 00:00:00 UTC","total_visits":"1921"} +{"parsed_date":"2017-07-28 00:00:00 UTC","total_visits":"2433"} +{"parsed_date":"2016-09-19 00:00:00 UTC","total_visits":"2689"} +{"parsed_date":"2017-07-24 00:00:00 UTC","total_visits":"2436"} +{"parsed_date":"2017-06-13 00:00:00 UTC","total_visits":"2181"} +{"parsed_date":"2016-09-15 00:00:00 UTC","total_visits":"2949"} +{"parsed_date":"2017-02-03 00:00:00 UTC","total_visits":"2182"} +{"parsed_date":"2016-09-10 00:00:00 UTC","total_visits":"1671"} +{"parsed_date":"2017-06-09 00:00:00 UTC","total_visits":"1927"} +{"parsed_date":"2017-01-11 00:00:00 UTC","total_visits":"2185"} +{"parsed_date":"2017-02-19 00:00:00 UTC","total_visits":"2187"} +{"parsed_date":"2017-01-17 00:00:00 UTC","total_visits":"2443"} +{"parsed_date":"2017-05-12 00:00:00 UTC","total_visits":"1932"} +{"parsed_date":"2016-12-16 00:00:00 UTC","total_visits":"2956"} +{"parsed_date":"2017-02-01 00:00:00 UTC","total_visits":"2445"} +{"parsed_date":"2016-11-26 00:00:00 UTC","total_visits":"3213"} +{"parsed_date":"2017-06-02 00:00:00 UTC","total_visits":"2190"} +{"parsed_date":"2016-08-05 00:00:00 UTC","total_visits":"2702"} +{"parsed_date":"2016-11-01 00:00:00 UTC","total_visits":"3728"} +{"parsed_date":"2017-01-05 00:00:00 UTC","total_visits":"2193"} +{"parsed_date":"2017-03-08 00:00:00 UTC","total_visits":"2449"} +{"parsed_date":"2016-08-28 00:00:00 UTC","total_visits":"1682"} +{"parsed_date":"2017-07-04 00:00:00 UTC","total_visits":"1938"} +{"parsed_date":"2017-03-10 00:00:00 UTC","total_visits":"2194"} +{"parsed_date":"2017-07-07 00:00:00 UTC","total_visits":"2450"} +{"parsed_date":"2016-10-29 00:00:00 UTC","total_visits":"2964"} +{"parsed_date":"2016-10-13 00:00:00 UTC","total_visits":"2964"} +{"parsed_date":"2016-12-04 00:00:00 UTC","total_visits":"3220"} +{"parsed_date":"2017-01-21 00:00:00 UTC","total_visits":"1685"} +{"parsed_date":"2017-06-29 00:00:00 UTC","total_visits":"2709"} +{"parsed_date":"2016-08-29 00:00:00 UTC","total_visits":"2454"} +{"parsed_date":"2016-12-19 00:00:00 UTC","total_visits":"3222"} +{"parsed_date":"2017-05-30 00:00:00 UTC","total_visits":"2199"} +{"parsed_date":"2017-02-10 00:00:00 UTC","total_visits":"2199"} +{"parsed_date":"2016-08-31 00:00:00 UTC","total_visits":"3223"} +{"parsed_date":"2017-06-18 00:00:00 UTC","total_visits":"1432"} +{"parsed_date":"2017-01-12 00:00:00 UTC","total_visits":"2203"} +{"parsed_date":"2017-05-18 00:00:00 UTC","total_visits":"2715"} +{"parsed_date":"2016-10-23 00:00:00 UTC","total_visits":"2971"} +{"parsed_date":"2016-09-04 00:00:00 UTC","total_visits":"1692"} +{"parsed_date":"2016-12-10 00:00:00 UTC","total_visits":"2207"} +{"parsed_date":"2016-12-11 00:00:00 UTC","total_visits":"2208"} +{"parsed_date":"2017-04-11 00:00:00 UTC","total_visits":"2464"} +{"parsed_date":"2016-09-21 00:00:00 UTC","total_visits":"2720"} +{"parsed_date":"2016-11-06 00:00:00 UTC","total_visits":"3232"} +{"parsed_date":"2017-01-26 00:00:00 UTC","total_visits":"2209"} +{"parsed_date":"2016-09-12 00:00:00 UTC","total_visits":"2465"} +{"parsed_date":"2017-04-21 00:00:00 UTC","total_visits":"2210"} +{"parsed_date":"2017-01-06 00:00:00 UTC","total_visits":"2210"} +{"parsed_date":"2017-04-04 00:00:00 UTC","total_visits":"2978"} +{"parsed_date":"2017-01-22 00:00:00 UTC","total_visits":"1700"} +{"parsed_date":"2017-07-26 00:00:00 UTC","total_visits":"2725"} +{"parsed_date":"2016-08-18 00:00:00 UTC","total_visits":"2725"} +{"parsed_date":"2016-09-27 00:00:00 UTC","total_visits":"2727"} +{"parsed_date":"2016-12-02 00:00:00 UTC","total_visits":"3751"} +{"parsed_date":"2017-05-05 00:00:00 UTC","total_visits":"1960"} +{"parsed_date":"2016-11-19 00:00:00 UTC","total_visits":"2984"} +{"parsed_date":"2016-11-09 00:00:00 UTC","total_visits":"3752"} +{"parsed_date":"2016-12-05 00:00:00 UTC","total_visits":"4265"} +{"parsed_date":"2017-05-11 00:00:00 UTC","total_visits":"2218"} +{"parsed_date":"2017-01-25 00:00:00 UTC","total_visits":"2986"} +{"parsed_date":"2017-03-11 00:00:00 UTC","total_visits":"1707"} +{"parsed_date":"2017-03-30 00:00:00 UTC","total_visits":"2731"} +{"parsed_date":"2016-10-20 00:00:00 UTC","total_visits":"3755"} +{"parsed_date":"2017-02-07 00:00:00 UTC","total_visits":"2476"} +{"parsed_date":"2017-02-22 00:00:00 UTC","total_visits":"2477"} +{"parsed_date":"2017-07-23 00:00:00 UTC","total_visits":"1966"} +{"parsed_date":"2016-11-03 00:00:00 UTC","total_visits":"4014"} +{"parsed_date":"2016-08-01 00:00:00 UTC","total_visits":"1711"} +{"parsed_date":"2017-01-13 00:00:00 UTC","total_visits":"1967"} +{"parsed_date":"2017-05-19 00:00:00 UTC","total_visits":"2223"} +{"parsed_date":"2016-11-20 00:00:00 UTC","total_visits":"3247"} +{"parsed_date":"2016-11-25 00:00:00 UTC","total_visits":"3759"} +{"parsed_date":"2017-03-25 00:00:00 UTC","total_visits":"1712"} +{"parsed_date":"2017-01-27 00:00:00 UTC","total_visits":"1969"} +{"parsed_date":"2017-06-26 00:00:00 UTC","total_visits":"2226"} +{"parsed_date":"2017-05-25 00:00:00 UTC","total_visits":"2228"} +{"parsed_date":"2017-01-31 00:00:00 UTC","total_visits":"2229"} +{"parsed_date":"2017-07-13 00:00:00 UTC","total_visits":"2741"} +{"parsed_date":"2017-03-15 00:00:00 UTC","total_visits":"2486"} +{"parsed_date":"2017-05-28 00:00:00 UTC","total_visits":"1463"} +{"parsed_date":"2017-03-09 00:00:00 UTC","total_visits":"2231"} +{"parsed_date":"2017-07-15 00:00:00 UTC","total_visits":"1721"} +{"parsed_date":"2016-11-24 00:00:00 UTC","total_visits":"3770"} +{"parsed_date":"2016-10-05 00:00:00 UTC","total_visits":"3770"} +{"parsed_date":"2016-12-31 00:00:00 UTC","total_visits":"1211"} +{"parsed_date":"2016-10-02 00:00:00 UTC","total_visits":"1724"} +{"parsed_date":"2017-07-22 00:00:00 UTC","total_visits":"1724"} +{"parsed_date":"2016-09-11 00:00:00 UTC","total_visits":"1725"} +{"parsed_date":"2017-06-15 00:00:00 UTC","total_visits":"2237"} +{"parsed_date":"2017-06-05 00:00:00 UTC","total_visits":"2493"} +{"parsed_date":"2017-02-06 00:00:00 UTC","total_visits":"2238"} +{"parsed_date":"2016-10-15 00:00:00 UTC","total_visits":"2495"} +{"parsed_date":"2016-08-21 00:00:00 UTC","total_visits":"1730"} +{"parsed_date":"2016-08-23 00:00:00 UTC","total_visits":"2754"} +{"parsed_date":"2017-06-30 00:00:00 UTC","total_visits":"2499"} +{"parsed_date":"2017-01-18 00:00:00 UTC","total_visits":"2245"} +{"parsed_date":"2016-08-10 00:00:00 UTC","total_visits":"2757"} +{"parsed_date":"2016-12-08 00:00:00 UTC","total_visits":"3013"} +{"parsed_date":"2016-11-28 00:00:00 UTC","total_visits":"4807"} +{"parsed_date":"2017-05-22 00:00:00 UTC","total_visits":"2248"} +{"parsed_date":"2016-09-20 00:00:00 UTC","total_visits":"2760"} +{"parsed_date":"2016-10-06 00:00:00 UTC","total_visits":"3016"} +{"parsed_date":"2016-09-06 00:00:00 UTC","total_visits":"2508"} +{"parsed_date":"2016-09-03 00:00:00 UTC","total_visits":"1741"} +{"parsed_date":"2016-12-06 00:00:00 UTC","total_visits":"3021"} +{"parsed_date":"2016-12-24 00:00:00 UTC","total_visits":"1231"} +{"parsed_date":"2016-10-28 00:00:00 UTC","total_visits":"3791"} +{"parsed_date":"2016-12-30 00:00:00 UTC","total_visits":"1232"} +{"parsed_date":"2017-05-29 00:00:00 UTC","total_visits":"1745"} +{"parsed_date":"2017-07-10 00:00:00 UTC","total_visits":"2769"} +{"parsed_date":"2017-06-22 00:00:00 UTC","total_visits":"2258"} +{"parsed_date":"2017-07-19 00:00:00 UTC","total_visits":"2514"} +{"parsed_date":"2016-10-03 00:00:00 UTC","total_visits":"2514"} +{"parsed_date":"2017-06-14 00:00:00 UTC","total_visits":"2517"} +{"parsed_date":"2016-10-22 00:00:00 UTC","total_visits":"3029"} +{"parsed_date":"2017-01-23 00:00:00 UTC","total_visits":"2262"} +{"parsed_date":"2017-04-24 00:00:00 UTC","total_visits":"2263"} +{"parsed_date":"2016-11-10 00:00:00 UTC","total_visits":"4055"} +{"parsed_date":"2016-09-26 00:00:00 UTC","total_visits":"2776"} +{"parsed_date":"2016-10-19 00:00:00 UTC","total_visits":"3544"} +{"parsed_date":"2017-03-04 00:00:00 UTC","total_visits":"1753"} +{"parsed_date":"2017-05-26 00:00:00 UTC","total_visits":"2009"} +{"parsed_date":"2017-02-13 00:00:00 UTC","total_visits":"2266"} +{"parsed_date":"2017-02-18 00:00:00 UTC","total_visits":"1755"} +{"parsed_date":"2017-03-02 00:00:00 UTC","total_visits":"2267"} +{"parsed_date":"2017-03-31 00:00:00 UTC","total_visits":"2268"} +{"parsed_date":"2017-01-10 00:00:00 UTC","total_visits":"2268"} +{"parsed_date":"2017-03-29 00:00:00 UTC","total_visits":"2525"} +{"parsed_date":"2017-03-27 00:00:00 UTC","total_visits":"2525"} +{"parsed_date":"2016-11-23 00:00:00 UTC","total_visits":"3805"} +{"parsed_date":"2017-05-27 00:00:00 UTC","total_visits":"1502"} +{"parsed_date":"2016-10-24 00:00:00 UTC","total_visits":"4063"} +{"parsed_date":"2016-12-14 00:00:00 UTC","total_visits":"3040"} +{"parsed_date":"2017-02-11 00:00:00 UTC","total_visits":"1761"} +{"parsed_date":"2017-07-27 00:00:00 UTC","total_visits":"2529"} +{"parsed_date":"2017-02-17 00:00:00 UTC","total_visits":"2785"} +{"parsed_date":"2017-04-15 00:00:00 UTC","total_visits":"1506"} +{"parsed_date":"2016-11-05 00:00:00 UTC","total_visits":"3042"} +{"parsed_date":"2016-10-04 00:00:00 UTC","total_visits":"4322"} +{"parsed_date":"2017-05-13 00:00:00 UTC","total_visits":"1251"} +{"parsed_date":"2017-04-16 00:00:00 UTC","total_visits":"1507"} +{"parsed_date":"2016-12-28 00:00:00 UTC","total_visits":"1763"} +{"parsed_date":"2016-08-15 00:00:00 UTC","total_visits":"3043"} +{"parsed_date":"2016-12-03 00:00:00 UTC","total_visits":"3044"} +{"parsed_date":"2017-06-27 00:00:00 UTC","total_visits":"2789"} +{"parsed_date":"2017-06-24 00:00:00 UTC","total_visits":"1510"} +{"parsed_date":"2017-07-16 00:00:00 UTC","total_visits":"1766"} +{"parsed_date":"2017-04-09 00:00:00 UTC","total_visits":"1766"} +{"parsed_date":"2017-06-07 00:00:00 UTC","total_visits":"2279"} +{"parsed_date":"2017-04-17 00:00:00 UTC","total_visits":"2279"} +{"parsed_date":"2016-09-28 00:00:00 UTC","total_visits":"2535"} +{"parsed_date":"2017-03-26 00:00:00 UTC","total_visits":"1768"} +{"parsed_date":"2017-05-10 00:00:00 UTC","total_visits":"2024"} +{"parsed_date":"2017-06-08 00:00:00 UTC","total_visits":"2280"} +{"parsed_date":"2017-05-08 00:00:00 UTC","total_visits":"2025"} +{"parsed_date":"2017-03-13 00:00:00 UTC","total_visits":"2537"} +{"parsed_date":"2016-11-17 00:00:00 UTC","total_visits":"4074"} +{"parsed_date":"2016-08-25 00:00:00 UTC","total_visits":"2539"} +{"parsed_date":"2017-02-16 00:00:00 UTC","total_visits":"2539"} +{"parsed_date":"2017-06-16 00:00:00 UTC","total_visits":"2028"} +{"parsed_date":"2016-11-16 00:00:00 UTC","total_visits":"4334"} +{"parsed_date":"2016-08-17 00:00:00 UTC","total_visits":"2799"} +{"parsed_date":"2017-03-19 00:00:00 UTC","total_visits":"1776"} +{"parsed_date":"2016-11-29 00:00:00 UTC","total_visits":"4337"} +{"parsed_date":"2017-02-05 00:00:00 UTC","total_visits":"1522"} +{"parsed_date":"2016-10-31 00:00:00 UTC","total_visits":"3827"} +{"parsed_date":"2017-05-31 00:00:00 UTC","total_visits":"2292"} +{"parsed_date":"2017-07-18 00:00:00 UTC","total_visits":"2804"} +{"parsed_date":"2017-03-12 00:00:00 UTC","total_visits":"1781"} +{"parsed_date":"2016-09-09 00:00:00 UTC","total_visits":"2549"} +{"parsed_date":"2017-01-14 00:00:00 UTC","total_visits":"1526"} +{"parsed_date":"2017-05-04 00:00:00 UTC","total_visits":"2806"} +{"parsed_date":"2016-11-07 00:00:00 UTC","total_visits":"3832"} +{"parsed_date":"2017-04-07 00:00:00 UTC","total_visits":"2297"} +{"parsed_date":"2017-07-12 00:00:00 UTC","total_visits":"2554"} +{"parsed_date":"2017-04-13 00:00:00 UTC","total_visits":"2300"} +{"parsed_date":"2017-08-01 00:00:00 UTC","total_visits":"2556"} +{"parsed_date":"2017-06-04 00:00:00 UTC","total_visits":"1534"} +{"parsed_date":"2017-02-12 00:00:00 UTC","total_visits":"1790"} +{"parsed_date":"2017-07-03 00:00:00 UTC","total_visits":"2046"} +{"parsed_date":"2016-09-30 00:00:00 UTC","total_visits":"2303"} +{"parsed_date":"2016-08-08 00:00:00 UTC","total_visits":"2815"} diff --git a/tests/data/time_series_schema.json b/tests/data/time_series_schema.json new file mode 100644 index 00000000000..857595b9e64 --- /dev/null +++ b/tests/data/time_series_schema.json @@ -0,0 +1,12 @@ +[ + { + "mode": "NULLABLE", + "name": "parsed_date", + "type": "TIMESTAMP" + }, + { + "mode": "NULLABLE", + "name": "total_visits", + "type": "INTEGER" + } + ] diff --git a/tests/system/__init__.py b/tests/system/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/system/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/system/conftest.py b/tests/system/conftest.py new file mode 100644 index 00000000000..03638698966 --- /dev/null +++ b/tests/system/conftest.py @@ -0,0 +1,667 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +import hashlib +import logging +import pathlib +import typing +from typing import Dict, Optional + +import google.cloud.bigquery as bigquery +import google.cloud.bigquery_connection_v1 as bigquery_connection_v1 +import google.cloud.exceptions +import google.cloud.storage as storage # type: ignore +import ibis.backends.base +import pandas as pd +import pytest +import pytz +import test_utils.prefixer + +import bigframes +from tests.system.utils import convert_pandas_dtypes + +CURRENT_DIR = pathlib.Path(__file__).parent +DATA_DIR = CURRENT_DIR.parent / "data" +PERMANENT_DATASET = "bigframes_testing" +PERMANENT_DATASET_TOKYO = "bigframes_testing_tokyo" +TOKYO_LOCATION = "asia-northeast1" +prefixer = test_utils.prefixer.Prefixer("bigframes", "tests/system") + + +def _hash_digest_file(hasher, filepath): + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hasher.update(chunk) + + +@pytest.fixture(scope="session") +def tokyo_location() -> str: + return TOKYO_LOCATION + + +@pytest.fixture(scope="session") +def gcs_client() -> storage.Client: + # TODO(swast): Ensure same credentials and project are used as in the rest + # of our tests. + return storage.Client() + + +@pytest.fixture(scope="session") +def gcs_folder(gcs_client: storage.Client): + # TODO(swast): Allow bucket name from environment variable for testing by + # non-Googlers. + bucket = "bigframes-dev-testing" + prefix = prefixer.create_prefix() + path = f"gs://{bucket}/{prefix}/" + yield path + for blob in gcs_client.list_blobs(bucket, prefix=prefix): + blob = typing.cast(storage.Blob, blob) + blob.delete() + + +@pytest.fixture(scope="session") +def bigquery_client(session: bigframes.Session) -> bigquery.Client: + return session.bqclient + + +@pytest.fixture(scope="session") +def bigquery_client_tokyo(session_tokyo: bigframes.Session) -> bigquery.Client: + return session_tokyo.bqclient + + +@pytest.fixture(scope="session") +def ibis_client(session: bigframes.Session) -> ibis.backends.base.BaseBackend: + return session.ibis_client + + +@pytest.fixture(scope="session") +def bigqueryconnection_client( + session: bigframes.Session, +) -> bigquery_connection_v1.ConnectionServiceClient: + return session.bqconnectionclient + + +@pytest.fixture(scope="session") +def session() -> bigframes.Session: + return bigframes.Session() + + +@pytest.fixture(scope="session") +def session_tokyo(tokyo_location: str) -> bigframes.Session: + context = bigframes.BigQueryOptions( + location=tokyo_location, + use_regional_endpoints=True, + ) + return bigframes.Session(context=context) + + +@pytest.fixture(scope="session", autouse=True) +def cleanup_datasets(bigquery_client: bigquery.Client) -> None: + """Cleanup any datasets that were created but not cleaned up.""" + for dataset in bigquery_client.list_datasets(): + if prefixer.should_cleanup(dataset.dataset_id): + bigquery_client.delete_dataset( + dataset, delete_contents=True, not_found_ok=True + ) + + +@pytest.fixture(scope="session") +def dataset_id(bigquery_client: bigquery.Client): + """Create (and cleanup) a temporary dataset.""" + project_id = bigquery_client.project + dataset_id = f"{project_id}.{prefixer.create_prefix()}_dataset_id" + dataset = bigquery.Dataset(dataset_id) + bigquery_client.create_dataset(dataset) + yield dataset_id + bigquery_client.delete_dataset(dataset, delete_contents=True) + + +@pytest.fixture(scope="session") +def dataset_id_permanent(bigquery_client: bigquery.Client) -> str: + """Create a dataset if it doesn't exist.""" + project_id = bigquery_client.project + dataset_id = f"{project_id}.{PERMANENT_DATASET}" + dataset = bigquery.Dataset(dataset_id) + bigquery_client.create_dataset(dataset, exists_ok=True) + return dataset_id + + +@pytest.fixture(scope="session") +def dataset_id_permanent_tokyo( + bigquery_client_tokyo: bigquery.Client, tokyo_location: str +) -> str: + """Create a dataset in Tokyo if it doesn't exist.""" + project_id = bigquery_client_tokyo.project + dataset_id = f"{project_id}.{PERMANENT_DATASET_TOKYO}" + dataset = bigquery.Dataset(dataset_id) + dataset.location = tokyo_location + dataset = bigquery_client_tokyo.create_dataset(dataset, exists_ok=True) + assert dataset.location == tokyo_location + return dataset_id + + +@pytest.fixture(scope="session") +def scalars_schema(bigquery_client: bigquery.Client): + # TODO(swast): Add missing scalar data types such as BIGNUMERIC. + # See also: https://github.com/ibis-project/ibis-bigquery/pull/67 + schema = bigquery_client.schema_from_json(DATA_DIR / "scalars_schema.json") + return tuple(schema) + + +def load_test_data( + table_id: str, + bigquery_client: bigquery.Client, + schema_filename: str, + data_filename: str, + location: Optional[str], +) -> bigquery.LoadJob: + """Create a temporary table with test data""" + job_config = bigquery.LoadJobConfig() + job_config.source_format = bigquery.SourceFormat.NEWLINE_DELIMITED_JSON + job_config.schema = tuple( + bigquery_client.schema_from_json(DATA_DIR / schema_filename) + ) + job_config.write_disposition = bigquery.WriteDisposition.WRITE_TRUNCATE + with open(DATA_DIR / data_filename, "rb") as input_file: + # TODO(swast): Location is allowed to be None in BigQuery Client. + # Can remove after + # https://github.com/googleapis/python-bigquery/pull/1554 is released. + location = "US" if location is None else location + job = bigquery_client.load_table_from_file( + input_file, + table_id, + job_config=job_config, + location=location, + ) + # No cleanup necessary, as the surrounding dataset will delete contents. + return typing.cast(bigquery.LoadJob, job.result()) + + +def load_test_data_tables( + session: bigframes.Session, dataset_id_permanent: str +) -> Dict[str, str]: + """Returns cached references to the test data tables in BigQuery. If no matching table is found + for the hash of the data and schema, the table will be uploaded.""" + existing_table_ids = [ + table.table_id for table in session.bqclient.list_tables(dataset_id_permanent) + ] + table_mapping: Dict[str, str] = {} + for table_name, schema_filename, data_filename in [ + ("scalars", "scalars_schema.json", "scalars.jsonl"), + ("scalars_too", "scalars_schema.json", "scalars.jsonl"), + ("penguins", "penguins_schema.json", "penguins.jsonl"), + ("time_series", "time_series_schema.json", "time_series.jsonl"), + ]: + test_data_hash = hashlib.md5() + _hash_digest_file(test_data_hash, DATA_DIR / schema_filename) + _hash_digest_file(test_data_hash, DATA_DIR / data_filename) + test_data_hash.update(table_name.encode()) + target_table_id = f"{table_name}_{test_data_hash.hexdigest()}" + target_table_id_full = f"{dataset_id_permanent}.{target_table_id}" + if target_table_id not in existing_table_ids: + # matching table wasn't found in the permanent dataset - we need to upload it + logging.info( + f"Test data table {table_name} was not found in the permanent dataset, regenerating it..." + ) + load_test_data( + target_table_id_full, + session.bqclient, + schema_filename, + data_filename, + location=session._location, + ) + + table_mapping[table_name] = target_table_id_full + + return table_mapping + + +@pytest.fixture(scope="session") +def test_data_tables( + session: bigframes.Session, dataset_id_permanent: str +) -> Dict[str, str]: + return load_test_data_tables(session, dataset_id_permanent) + + +@pytest.fixture(scope="session") +def test_data_tables_tokyo( + session_tokyo: bigframes.Session, dataset_id_permanent_tokyo: str +) -> Dict[str, str]: + return load_test_data_tables(session_tokyo, dataset_id_permanent_tokyo) + + +@pytest.fixture(scope="session") +def scalars_table_id(test_data_tables) -> str: + return test_data_tables["scalars"] + + +@pytest.fixture(scope="session") +def scalars_table_id_2(test_data_tables) -> str: + return test_data_tables["scalars_too"] + + +@pytest.fixture(scope="session") +def scalars_table_tokyo(test_data_tables_tokyo) -> str: + return test_data_tables_tokyo["scalars"] + + +@pytest.fixture(scope="session") +def penguins_table_id(test_data_tables) -> str: + return test_data_tables["penguins"] + + +@pytest.fixture(scope="session") +def time_series_table_id(test_data_tables) -> str: + return test_data_tables["time_series"] + + +@pytest.fixture(scope="session") +def scalars_df_default_index( + scalars_df_index: bigframes.dataframe.DataFrame, + scalars_pandas_df_default_index: pd.DataFrame, +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + df = scalars_df_index.reset_index(drop=False) + # Ensure the order of the columns is the same. + df = typing.cast( + bigframes.dataframe.DataFrame, df[scalars_pandas_df_default_index.columns] + ) + return df + + +@pytest.fixture(scope="session") +def scalars_df_index( + scalars_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq(scalars_table_id, index_col="rowindex") + + +@pytest.fixture(scope="session") +def scalars_df_2_default_index( + scalars_df_2_index: bigframes.dataframe.DataFrame, +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return scalars_df_2_index.reset_index(drop=False) + + +@pytest.fixture(scope="session") +def scalars_df_2_index( + scalars_table_id_2: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq(scalars_table_id_2, index_col="rowindex") + + +@pytest.fixture(scope="session") +def scalars_pandas_df_default_index() -> pd.DataFrame: + """pd.DataFrame pointing at test data.""" + + df = pd.read_json( + DATA_DIR / "scalars.jsonl", + lines=True, + ) + convert_pandas_dtypes(df, bytes_col=True) + + df = df.set_index("rowindex", drop=False) + df.index.name = None + return df + + +@pytest.fixture(scope="session") +def scalars_pandas_df_index( + scalars_pandas_df_default_index: pd.DataFrame, +) -> pd.DataFrame: + """pd.DataFrame pointing at test data.""" + return scalars_pandas_df_default_index.set_index("rowindex").sort_index() + + +@pytest.fixture(scope="session") +def scalars_pandas_df_multi_index( + scalars_pandas_df_default_index: pd.DataFrame, +) -> pd.DataFrame: + """pd.DataFrame pointing at test data.""" + return scalars_pandas_df_default_index.set_index( + ["rowindex", "timestamp_col"] + ).sort_index() + + +@pytest.fixture(scope="session") +def scalars_dfs( + scalars_df_index, + scalars_pandas_df_index, +): + return scalars_df_index, scalars_pandas_df_index + + +@pytest.fixture(scope="session") +def penguins_df_default_index( + penguins_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq(penguins_table_id) + + +@pytest.fixture(scope="session") +def time_series_df_default_index( + time_series_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq(time_series_table_id) + + +@pytest.fixture(scope="session") +def new_time_series_pandas_df(): + """Additional data matching the time series dataset. The values are dummy ones used to basically check the prediction scores.""" + utc = pytz.utc + return pd.DataFrame( + { + "parsed_date": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "total_visits": [2500, 2500, 2500], + } + ) + + +@pytest.fixture(scope="session") +def new_time_series_df(session, new_time_series_pandas_df): + return session.read_pandas(new_time_series_pandas_df) + + +@pytest.fixture(scope="session") +def penguins_pandas_df_default_index() -> pd.DataFrame: + """Consistently ordered pandas dataframe for penguins test data""" + df = pd.read_json( + f"{DATA_DIR}/penguins.jsonl", + lines=True, + dtype={ + "species": pd.StringDtype(storage="pyarrow"), + "island": pd.StringDtype(storage="pyarrow"), + "culmen_length_mm": pd.Float64Dtype(), + "culmen_depth_mm": pd.Float64Dtype(), + "flipper_length_mm": pd.Float64Dtype(), + "sex": pd.StringDtype(storage="pyarrow"), + "body_mass_g": pd.Float64Dtype(), + }, + ) + df.index = df.index.astype("Int64") + return df + + +@pytest.fixture(scope="session") +def new_penguins_pandas_df(): + """Additional data matching the penguins dataset, with a new index""" + return pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + "body_mass_g": [3750.0, 5200.0, 3325.0], + "sex": ["MALE", "FEMALE", "FEMALE"], + } + ).set_index("tag_number") + + +@pytest.fixture(scope="session") +def new_penguins_df(session, new_penguins_pandas_df): + return session.read_pandas(new_penguins_pandas_df) + + +@pytest.fixture(scope="session") +def penguins_linear_model_name( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='linear_reg', + input_label_cols=['body_mass_g'], + data_split_method='NO_SPLIT' +) AS +SELECT + * +FROM + `{penguins_table_id}` +WHERE + body_mass_g IS NOT NULL""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_linear_reg_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_linear_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + +@pytest.fixture(scope="session") +def penguins_logistic_model_name( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='logistic_reg', + input_label_cols=['sex'], + data_split_method='NO_SPLIT' +) AS SELECT + * +FROM `{penguins_table_id}` +WHERE + sex IS NOT NULL""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_logistic_reg_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_logistic_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + +@pytest.fixture(scope="session") +def penguins_xgbregressor_model_name( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='BOOSTED_TREE_REGRESSOR', + num_parallel_tree=1, + booster_type='GBTREE', + early_stop=True, + data_split_method='NO_SPLIT', + subsample=1.0, + input_label_cols=['body_mass_g'] +) AS SELECT + * +FROM `{penguins_table_id}` +WHERE + body_mass_g IS NOT NULL""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_xgbregressor_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_xgbregressor_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + +@pytest.fixture(scope="session") +def time_series_arima_plus_model_name( + session: bigframes.Session, dataset_id_permanent, time_series_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='ARIMA_PLUS', + time_series_timestamp_col = 'parsed_date', + time_series_data_col = 'total_visits' +) AS SELECT + * +FROM `{time_series_table_id}`""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.time_series_arima_plus_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "time_series_arima_plus_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + +@pytest.fixture(scope="session") +def penguins_xgbclassifier_model_name( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type="BOOSTED_TREE_CLASSIFIER", + num_parallel_tree=1, + booster_type='GBTREE', + early_stop=True, + data_split_method='NO_SPLIT', + subsample=1.0, + input_label_cols=['sex'] +) AS SELECT + * +FROM `{penguins_table_id}` +WHERE + sex IS NOT NULL""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_classifier_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_classifier_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + +@pytest.fixture(scope="session") +def penguins_randomforest_regressor_model_name( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='RANDOM_FOREST_REGRESSOR', + num_parallel_tree=100, + early_stop=True, + data_split_method='NO_SPLIT', + subsample=0.8, + input_label_cols=['body_mass_g'] +) AS SELECT + * +FROM `{penguins_table_id}` +WHERE + body_mass_g IS NOT NULL""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_randomforest_regressor_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_randomforest_regressor_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + +@pytest.fixture(scope="session") +def penguins_randomforest_classifier_model_name( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type="RANDOM_FOREST_CLASSIFIER", + num_parallel_tree=100, + early_stop=True, + data_split_method='NO_SPLIT', + subsample=0.8, + input_label_cols=['sex'] +) AS SELECT + * +FROM `{penguins_table_id}` +WHERE + sex IS NOT NULL""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_randomforest_classifier_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_randomforest_classifier_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name diff --git a/tests/system/large/__init__.py b/tests/system/large/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/system/large/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py new file mode 100644 index 00000000000..bb64b19076f --- /dev/null +++ b/tests/system/large/ml/test_cluster.py @@ -0,0 +1,103 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas + +import bigframes.ml.cluster +from tests.system.utils import assert_pandas_df_equal_ignore_ordering + + +def test_cluster_configure_fit_predict(session, penguins_df_default_index, dataset_id): + model = bigframes.ml.cluster.KMeans(n_clusters=3) + + df = penguins_df_default_index.dropna()[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + + # TODO(swast): How should we handle the default index? Currently, we get: + # "Column bigframes_index_0_z is not found in the input data to the + # EVALUATE function." + df = df.reset_index(drop=True) + + model.fit(df) + + pd_new_penguins = pandas.DataFrame.from_dict( + { + "test1": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Dream", + "culmen_length_mm": 37.5, + "culmen_depth_mm": 18.5, + "flipper_length_mm": 199, + "body_mass_g": 4475, + "sex": "MALE", + }, + "test2": { + "species": "Chinstrap penguin (Pygoscelis antarctica)", + "island": "Dream", + "culmen_length_mm": 55.8, + "culmen_depth_mm": 19.8, + "flipper_length_mm": 207, + "body_mass_g": 4000, + "sex": "MALE", + }, + "test3": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Biscoe", + "culmen_length_mm": 39.7, + "culmen_depth_mm": 18.9, + "flipper_length_mm": 184, + "body_mass_g": 3550, + "sex": "MALE", + }, + "test4": { + "species": "Gentoo penguin (Pygoscelis papua)", + "island": "Biscoe", + "culmen_length_mm": 43.8, + "culmen_depth_mm": 13.9, + "flipper_length_mm": 208, + "body_mass_g": 4300, + "sex": "FEMALE", + }, + }, + orient="index", + ) + pd_new_penguins.index.name = "observation" + + new_penguins = session.read_pandas(pd_new_penguins) + result = model.predict(new_penguins).compute() + expected = pandas.DataFrame( + {"CENTROID_ID": [2, 3, 1, 2]}, + dtype="Int64", + index=pandas.Index( + ["test1", "test2", "test3", "test4"], dtype="string[pyarrow]" + ), + ) + expected.index.name = "observation" + assert_pandas_df_equal_ignore_ordering(result, expected) + + # save, load, check n_clusters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_cluster_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_cluster_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.n_clusters == 3 diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py new file mode 100644 index 00000000000..52f347151ed --- /dev/null +++ b/tests/system/large/ml/test_compose.py @@ -0,0 +1,71 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas + +import bigframes.ml.cluster +import bigframes.ml.compose +import bigframes.ml.linear_model +import bigframes.ml.pipeline +import bigframes.ml.preprocessing + + +def test_columntransformer_standalone_fit_transform( + penguins_df_default_index, new_penguins_df +): + transformer = bigframes.ml.compose.ColumnTransformer( + [ + ( + "onehot", + bigframes.ml.preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + bigframes.ml.preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ) + + transformer.fit( + penguins_df_default_index[["species", "culmen_length_mm", "flipper_length_mm"]] + ) + result = transformer.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pandas.DataFrame( + { + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + "scaled_culmen_length_mm": [-0.8099, -0.9931, -1.103], + "scaled_flipper_length_mm": [-0.3495, -1.416, -0.9185], + }, + index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), + ) + expected.scaled_culmen_length_mm = expected.scaled_culmen_length_mm.astype( + "Float64" + ) + expected.scaled_flipper_length_mm = expected.scaled_flipper_length_mm.astype( + "Float64" + ) + + pandas.testing.assert_frame_equal(result, expected, rtol=1e-3) diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py new file mode 100644 index 00000000000..cd829f0ae2d --- /dev/null +++ b/tests/system/large/ml/test_core.py @@ -0,0 +1,183 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas + +import bigframes.ml.core +import bigframes.ml.sql + + +def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_df): + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + train_y = df[["body_mass_g"]] + + model = bigframes.ml.core.create_bqml_model( + train_X, train_y, options={"model_type": "linear_reg"} + ) + + # no data - report evaluation from the automatic data split + evaluate_result = model.evaluate().compute() + evaluate_expected = pandas.DataFrame( + { + "mean_absolute_error": [225.817334], + "mean_squared_error": [80540.705944], + "mean_squared_log_error": [0.004972], + "median_absolute_error": [173.080816], + "r2_score": [0.87529], + "explained_variance": [0.87529], + }, + dtype="Float64", + ) + evaluate_expected = evaluate_expected.reindex( + index=evaluate_expected.index.astype("Int64") + ) + pandas.testing.assert_frame_equal( + evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + ) + + # evaluate on all training data + evaluate_result = model.evaluate(df).compute() + pandas.testing.assert_frame_equal( + evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + ) + + # predict new labels + predictions = model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, + dtype="Float64", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + ) + + new_name = f"{dataset_id}.my_model" + new_model = model.copy(new_name, True) + assert new_model.model_name == new_name + + fetch_result = session.bqclient.get_model(new_name) + assert fetch_result.model_type == "LINEAR_REGRESSION" + + +def test_bqml_manual_preprocessing_e2e( + session, dataset_id, penguins_df_default_index, new_penguins_df +): + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["body_mass_g"]] + transforms = [ + bigframes.ml.sql.ml_standard_scaler(column, column) + for column in train_X.columns.tolist() + ] + transforms.extend(train_y.columns.tolist()) + options = {"model_type": "linear_reg"} + model = bigframes.ml.core.create_bqml_model( + train_X, train_y, transforms=transforms, options=options + ) + + # no data - report evaluation from the automatic data split + evaluate_result = model.evaluate().compute() + evaluate_expected = pandas.DataFrame( + { + "mean_absolute_error": [309.477334], + "mean_squared_error": [152184.227218], + "mean_squared_log_error": [0.009524], + "median_absolute_error": [257.727777], + "r2_score": [0.764356], + "explained_variance": [0.764356], + }, + dtype="Float64", + ) + evaluate_expected = evaluate_expected.reindex( + index=evaluate_expected.index.astype("Int64") + ) + + pandas.testing.assert_frame_equal( + evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + ) + + # evaluate on all training data + evaluate_result = model.evaluate(df).compute() + pandas.testing.assert_frame_equal( + evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + ) + + # predict new labels + predictions = model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_body_mass_g": [3968.8, 3176.3, 3545.2]}, + dtype="Float64", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + ) + + new_name = f"{dataset_id}.my_model" + new_model = model.copy(new_name, True) + assert new_model.model_name == new_name + + fetch_result = session.bqclient.get_model(new_name) + assert fetch_result.model_type == "LINEAR_REGRESSION" + + +def test_bqml_standalone_transform(penguins_df_default_index, new_penguins_df): + X = penguins_df_default_index[["culmen_length_mm", "species"]] + model = bigframes.ml.core.create_bqml_model( + X, + options={"model_type": "transform_only"}, + transforms=[ + "ML.STANDARD_SCALER(culmen_length_mm) OVER() AS scaled_culmen_length_mm", + "ML.ONE_HOT_ENCODER(species) OVER() AS onehotencoded_species", + ], + ) + + transformed = model.transform(new_penguins_df).compute() + expected = pandas.DataFrame( + { + "scaled_culmen_length_mm": [-0.8099, -0.9931, -1.103], + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + }, + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + expected["scaled_culmen_length_mm"] = expected["scaled_culmen_length_mm"].astype( + "Float64" + ) + pandas.testing.assert_frame_equal( + transformed[["scaled_culmen_length_mm", "onehotencoded_species"]], + expected, + check_exact=False, + rtol=0.1, + ) diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py new file mode 100644 index 00000000000..57507c11342 --- /dev/null +++ b/tests/system/large/ml/test_decomposition.py @@ -0,0 +1,70 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas + +import bigframes.ml.decomposition + + +def test_decomposition_configure_fit_predict( + session, penguins_df_default_index, dataset_id +): + model = bigframes.ml.decomposition.PCA(n_components=3) + model.fit(penguins_df_default_index) + + pd_new_penguins = session.read_pandas( + pandas.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Gentoo penguin (Pygoscelis papua)", + "Adelie Penguin (Pygoscelis adeliae)", + ], + "island": ["Dream", "Biscoe", "Torgersen"], + "culmen_length_mm": [37.8, 46.5, 41.1], + "culmen_depth_mm": [18.1, 14.8, 18.6], + "flipper_length_mm": [193.0, 217.0, 189.0], + "body_mass_g": [3750.0, 5200.0, 3325.0], + "sex": ["MALE", "FEMALE", "MALE"], + } + ).set_index("tag_number") + ) + + result = model.predict(pd_new_penguins).compute() + expected = pandas.DataFrame( + { + "principal_component_1": [-1.459, 2.258, -1.685], + "principal_component_2": [-1.120, -1.351, -0.874], + "principal_component_3": [-0.646, 0.443, -0.704], + }, + dtype="Float64", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + result.sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) + + # save, load, check n_components to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_pca_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_pca_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.n_components == 3 diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py new file mode 100644 index 00000000000..0d5da380d1e --- /dev/null +++ b/tests/system/large/ml/test_ensemble.py @@ -0,0 +1,458 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +import pandas + +import bigframes.ml.ensemble + + +def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): + model = bigframes.ml.ensemble.XGBRegressor() + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + train_y = df[["body_mass_g"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + expected = pandas.DataFrame( + { + "mean_absolute_error": [97.368139], + "mean_squared_error": [16284.877027], + "mean_squared_log_error": [0.0010189], + "median_absolute_error": [72.158691], + "r2_score": [0.974784], + "explained_variance": [0.974845], + }, + dtype="Float64", + ) + expected = expected.reindex(index=expected.index.astype("Int64")) + pandas.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_xgbregressor_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_xgbregressor_model" + in reloaded_model._bqml_model.model_name + ) + + +def test_xgbregressor_dart_booster_multiple_params( + penguins_df_default_index, dataset_id +): + model = bigframes.ml.ensemble.XGBRegressor( + booster="dart", + tree_method="AUTO", + min_tree_child_weight=2, + colsample_bytree=0.95, + colsample_bylevel=0.95, + colsample_bynode=0.95, + num_parallel_tree=2, + max_depth=4, + subsample=0.95, + reg_alpha=0.0001, + reg_lambda=0.0001, + learning_rate=0.015, + max_iterations=4, + min_rel_progress=0.02, + ) + + df = penguins_df_default_index.dropna().sample(n=70) + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + train_y = df[["body_mass_g"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "mean_absolute_error", + "mean_squared_error", + "mean_squared_log_error", + "median_absolute_error", + "r2_score", + "explained_variance", + ]: + assert col_name in result.columns + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_xgbregressor_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_xgbregressor_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.booster == "DART" + assert reloaded_model.dart_normalized_type == "TREE" + assert reloaded_model.tree_method == "AUTO" + assert reloaded_model.colsample_bytree == 0.95 + assert reloaded_model.colsample_bylevel == 0.95 + assert reloaded_model.colsample_bynode == 0.95 + assert reloaded_model.early_stop is True + assert reloaded_model.subsample == 0.95 + assert reloaded_model.reg_alpha == 0.0001 + assert reloaded_model.reg_lambda == 0.0001 + assert reloaded_model.learning_rate == 0.015 + assert reloaded_model.max_iterations == 4 + assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.gamma == 0.0 + assert reloaded_model.max_depth == 4 + assert reloaded_model.min_tree_child_weight == 2 + assert reloaded_model.num_parallel_tree == 2 + + +def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): + model = bigframes.ml.ensemble.XGBClassifier() + + df = penguins_df_default_index.dropna().sample(n=70) + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["sex"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_xgbclassifier_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_xgbclassifier_model" + in reloaded_model._bqml_model.model_name + ) + + +def test_xgbclassifier_dart_booster_multiple_params( + penguins_df_default_index, dataset_id +): + model = bigframes.ml.ensemble.XGBClassifier( + booster="dart", + tree_method="AUTO", + min_tree_child_weight=2, + colsample_bytree=0.95, + colsample_bylevel=0.95, + colsample_bynode=0.95, + num_parallel_tree=2, + max_depth=4, + subsample=0.95, + reg_alpha=0.0001, + reg_lambda=0.0001, + learning_rate=0.015, + max_iterations=4, + min_rel_progress=0.02, + ) + + df = penguins_df_default_index.dropna().sample(n=70) + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["sex"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_xgbclassifier_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_xgbclassifier_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.booster == "DART" + assert reloaded_model.dart_normalized_type == "TREE" + assert reloaded_model.tree_method == "AUTO" + assert reloaded_model.colsample_bytree == 0.95 + assert reloaded_model.colsample_bylevel == 0.95 + assert reloaded_model.colsample_bynode == 0.95 + assert reloaded_model.early_stop is True + assert reloaded_model.subsample == 0.95 + assert reloaded_model.reg_alpha == 0.0001 + assert reloaded_model.reg_lambda == 0.0001 + assert reloaded_model.learning_rate == 0.015 + assert reloaded_model.max_iterations == 4 + assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.gamma == 0.0 + assert reloaded_model.max_depth == 4 + assert reloaded_model.min_tree_child_weight == 2 + assert reloaded_model.num_parallel_tree == 2 + + +def test_randomforestregressor_default_params(penguins_df_default_index, dataset_id): + model = bigframes.ml.ensemble.RandomForestRegressor() + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + train_y = df[["body_mass_g"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "mean_absolute_error", + "mean_squared_error", + "mean_squared_log_error", + "median_absolute_error", + "r2_score", + "explained_variance", + ]: + assert col_name in result.columns + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_randomforestregressor_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_randomforestregressor_model" + in reloaded_model._bqml_model.model_name + ) + + +def test_randomforestregressor_multiple_params(penguins_df_default_index, dataset_id): + model = bigframes.ml.ensemble.RandomForestRegressor( + tree_method="AUTO", + min_tree_child_weight=2, + colsample_bytree=0.95, + colsample_bylevel=0.95, + colsample_bynode=0.95, + num_parallel_tree=90, + max_depth=14, + subsample=0.95, + reg_alpha=0.0001, + reg_lambda=0.0001, + min_rel_progress=0.02, + ) + + df = penguins_df_default_index.dropna().sample(n=70) + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + train_y = df[["body_mass_g"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "mean_absolute_error", + "mean_squared_error", + "mean_squared_log_error", + "median_absolute_error", + "r2_score", + "explained_variance", + ]: + assert col_name in result.columns + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_randomforestregressor_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_randomforestregressor_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.tree_method == "AUTO" + assert reloaded_model.colsample_bytree == 0.95 + assert reloaded_model.colsample_bylevel == 0.95 + assert reloaded_model.colsample_bynode == 0.95 + assert reloaded_model.early_stop is True + assert reloaded_model.subsample == 0.95 + assert reloaded_model.reg_alpha == 0.0001 + assert reloaded_model.reg_lambda == 0.0001 + assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.gamma == 0.0 + assert reloaded_model.max_depth == 14 + assert reloaded_model.min_tree_child_weight == 2 + assert reloaded_model.num_parallel_tree == 90 + assert reloaded_model.enable_global_explain is False + + +def test_randomforestclassifier_default_params(penguins_df_default_index, dataset_id): + model = bigframes.ml.ensemble.RandomForestClassifier() + + df = penguins_df_default_index.dropna().sample(n=70) + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["sex"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_randomforestclassifier_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_randomforestclassifier_model" + in reloaded_model._bqml_model.model_name + ) + + +def test_randomforestclassifier_multiple_params(penguins_df_default_index, dataset_id): + model = bigframes.ml.ensemble.RandomForestClassifier( + tree_method="AUTO", + min_tree_child_weight=2, + colsample_bytree=0.95, + colsample_bylevel=0.95, + colsample_bynode=0.95, + num_parallel_tree=90, + max_depth=14, + subsample=0.95, + reg_alpha=0.0001, + reg_lambda=0.0001, + min_rel_progress=0.02, + ) + + df = penguins_df_default_index.dropna().sample(n=70) + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["sex"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_randomforestclassifier_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_randomforestclassifier_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.tree_method == "AUTO" + assert reloaded_model.colsample_bytree == 0.95 + assert reloaded_model.colsample_bylevel == 0.95 + assert reloaded_model.colsample_bynode == 0.95 + assert reloaded_model.early_stop is True + assert reloaded_model.subsample == 0.95 + assert reloaded_model.reg_alpha == 0.0001 + assert reloaded_model.reg_lambda == 0.0001 + assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.gamma == 0.0 + assert reloaded_model.max_depth == 14 + assert reloaded_model.min_tree_child_weight == 2 + assert reloaded_model.num_parallel_tree == 90 + assert reloaded_model.enable_global_explain is False diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py new file mode 100644 index 00000000000..98726eb289d --- /dev/null +++ b/tests/system/large/ml/test_forecasting.py @@ -0,0 +1,48 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +from bigframes.ml import forecasting + + +def test_arima_plus_model_fit_score( + time_series_df_default_index, dataset_id, new_time_series_df +): + model = forecasting.ARIMAPlus() + train_X = time_series_df_default_index[["parsed_date"]] + train_y = time_series_df_default_index[["total_visits"]] + model.fit(train_X, train_y) + + result = model.score( + new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] + ).compute() + expected = pd.DataFrame( + { + "mean_absolute_error": [154.742547], + "mean_squared_error": [26844.868855], + "root_mean_squared_error": [163.844038], + "mean_absolute_percentage_error": [6.189702], + "symmetric_mean_absolute_percentage_error": [6.097155], + }, + dtype="Float64", + ) + expected = expected.reindex(index=expected.index.astype("Int64")) + pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) + + # save, load to ensure configuration was kept + reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) + assert ( + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name + ) diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py new file mode 100644 index 00000000000..33010a005cb --- /dev/null +++ b/tests/system/large/ml/test_linear_model.py @@ -0,0 +1,196 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +import bigframes.ml.linear_model + + +def test_linear_regression_configure_fit_score(penguins_df_default_index, dataset_id): + model = bigframes.ml.linear_model.LinearRegression(fit_intercept=False) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + train_y = df[["body_mass_g"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + expected = pd.DataFrame( + { + "mean_absolute_error": [225.735767], + "mean_squared_error": [80417.461828], + "mean_squared_log_error": [0.004967], + "median_absolute_error": [172.543702], + "r2_score": [0.87548], + "explained_variance": [0.87548], + }, + dtype="Float64", + ) + expected = expected.reindex(index=expected.index.astype("Int64")) + pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) + assert ( + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name + ) + + # TODO(yunmengxie): enable this once b/277242951 (fit_intercept missing from API) is fixed + # assert reloaded_model.fit_intercept == False + + +def test_linear_regression_manual_split_configure_fit_score( + penguins_df_default_index, dataset_id +): + model = bigframes.ml.linear_model.LinearRegression(fit_intercept=True) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + train_y = df[["body_mass_g"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + expected = pd.DataFrame( + { + "mean_absolute_error": [225.735767], + "mean_squared_error": [80417.461828], + "mean_squared_log_error": [0.004967], + "median_absolute_error": [172.543702], + "r2_score": [0.87548], + "explained_variance": [0.87548], + }, + dtype="Float64", + ) + expected = expected.reindex(index=expected.index.astype("Int64")) + pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) + assert ( + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.fit_intercept is True + + +def test_logistic_regression_auto_class_weights_configure_fit_score( + penguins_df_default_index, dataset_id +): + model = bigframes.ml.linear_model.LogisticRegression() + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["sex"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + expected = pd.DataFrame( + { + "precision": [0.58085], + "recall": [0.582576], + "accuracy": [0.871257], + "f1_score": [0.58171], + "log_loss": [1.59285], + "roc_auc": [0.9602], + }, + dtype="Float64", + ) + expected = expected.reindex(index=expected.index.astype("Int64")) + pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_logistic_reg_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_logistic_reg_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.fit_intercept is True + # TODO(gaotianxiang): enable this once (auto_class_weights missing from API) is fixed + # assert reloaded_model.auto_class_weights is True + + +def test_logistic_regression_manual_split_configure_fit_score( + penguins_df_default_index, dataset_id +): + model = bigframes.ml.linear_model.LogisticRegression(fit_intercept=True) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + train_y = df[["sex"]] + model.fit(train_X, train_y) + + # Check score to ensure the model was fitted + result = model.score(train_X, train_y).compute() + expected = pd.DataFrame( + { + "precision": [0.616753], + "recall": [0.618615], + "accuracy": [0.92515], + "f1_score": [0.617681], + "log_loss": [1.498832], + "roc_auc": [0.975807], + }, + dtype="Float64", + ) + expected = expected.reindex(index=expected.index.astype("Int64")) + pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_logistic_reg_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_logistic_reg_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.fit_intercept is True + assert reloaded_model.auto_class_weights is False diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py new file mode 100644 index 00000000000..9700ba2bf65 --- /dev/null +++ b/tests/system/large/ml/test_pipeline.py @@ -0,0 +1,524 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +from bigframes.ml import ( + cluster, + compose, + decomposition, + ensemble, + linear_model, + pipeline, + preprocessing, +) +from tests.system.utils import assert_pandas_df_equal_ignore_ordering + + +def test_pipeline_linear_regression_fit_score_predict( + session, penguins_df_default_index +): + """Test a supervised model with a minimal preprocessing step""" + pl = pipeline.Pipeline( + [ + ("scale", preprocessing.StandardScaler()), + ("linreg", linear_model.LinearRegression()), + ] + ) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["body_mass_g"]] + pl.fit(train_X, train_y) + + # Check score to ensure the model was fitted + score_result = pl.score(train_X, train_y).compute() + score_expected = pd.DataFrame( + { + "mean_absolute_error": [309.477334], + "mean_squared_error": [152184.227218], + "mean_squared_log_error": [0.009524], + "median_absolute_error": [257.727777], + "r2_score": [0.764356], + "explained_variance": [0.764356], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + # predict new labels + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + "sex": ["MALE", "FEMALE", "FEMALE"], + } + ).set_index("tag_number") + ) + predictions = pl.predict(new_penguins).to_pandas() + expected = pd.DataFrame( + {"predicted_body_mass_g": [3968.8, 3176.3, 3545.2]}, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + ) + + +def test_pipeline_logistic_regression_fit_score_predict( + session, penguins_df_default_index +): + """Test a supervised model with a minimal preprocessing step""" + pl = pipeline.Pipeline( + [ + ("scale", preprocessing.StandardScaler()), + ("logreg", linear_model.LogisticRegression()), + ] + ) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["sex"]] + pl.fit(train_X, train_y) + + # Check score to ensure the model was fitted + score_result = pl.score(train_X, train_y).compute() + score_expected = pd.DataFrame( + { + "precision": [0.537091], + "recall": [0.538636], + "accuracy": [0.805389], + "f1_score": [0.537716], + "log_loss": [1.445433], + "roc_auc": [0.917818], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + # predict new labels + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + } + ).set_index("tag_number") + ) + predictions = pl.predict(new_penguins).to_pandas() + expected = pd.DataFrame( + {"predicted_sex": ["MALE", "FEMALE", "FEMALE"]}, + dtype=pd.StringDtype(storage="pyarrow"), + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_sex"]], + expected, + ) + + +def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_index): + """Test a supervised model with a minimal preprocessing step""" + pl = pipeline.Pipeline( + [ + ("scale", preprocessing.StandardScaler()), + ("xgbreg", ensemble.XGBRegressor()), + ] + ) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["body_mass_g"]] + pl.fit(train_X, train_y) + + # Check score to ensure the model was fitted + score_result = pl.score(train_X, train_y).compute() + score_expected = pd.DataFrame( + { + "mean_absolute_error": [203.4001727989334], + "mean_squared_error": [74898.80551717622], + "mean_squared_log_error": [0.004394266810531861], + "median_absolute_error": [152.01806640625], + "r2_score": [0.8840255831308607], + "explained_variance": [0.8858505311591299], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + # predict new labels + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + "sex": ["MALE", "FEMALE", "FEMALE"], + } + ).set_index("tag_number") + ) + predictions = pl.predict(new_penguins).to_pandas() + expected = pd.DataFrame( + { + "predicted_body_mass_g": [ + 4287.34521484375, + 3198.351806640625, + 3385.34130859375, + ] + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + ) + + +def test_pipeline_random_forest_classifier_fit_score_predict( + session, penguins_df_default_index +): + """Test a supervised model with a minimal preprocessing step""" + pl = pipeline.Pipeline( + [ + ("scale", preprocessing.StandardScaler()), + ("rfcls", ensemble.RandomForestClassifier()), + ] + ) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + train_y = df[["sex"]] + pl.fit(train_X, train_y) + + # Check score to ensure the model was fitted + score_result = pl.score(train_X, train_y).compute() + score_expected = pd.DataFrame( + { + "precision": [0.587673], + "recall": [0.588781], + "accuracy": [0.88024], + "f1_score": [0.587644], + "log_loss": [0.859459], + "roc_auc": [0.971737], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + # predict new labels + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + } + ).set_index("tag_number") + ) + predictions = pl.predict(new_penguins).to_pandas() + expected = pd.DataFrame( + {"predicted_sex": ["MALE", "FEMALE", "FEMALE"]}, + dtype=pd.StringDtype(storage="pyarrow"), + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_sex"]], + expected, + ) + + +def test_pipeline_PCA_fit_predict(session, penguins_df_default_index): + """Test a supervised model with a minimal preprocessing step""" + pl = pipeline.Pipeline( + [ + ("scale", preprocessing.StandardScaler()), + ("pca", decomposition.PCA()), + ] + ) + + df = penguins_df_default_index.dropna() + train_X = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + pl.fit(train_X) + + # predict new labels + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + "sex": ["MALE", "FEMALE", "FEMALE"], + } + ).set_index("tag_number") + ) + predictions = pl.predict(new_penguins).to_pandas() + expected = pd.DataFrame( + { + "principal_component_1": [-1.115259, -1.506141, -1.471174], + "principal_component_2": [-0.074824, 0.69664, 0.406104], + "principal_component_3": [0.500012, -0.544479, 0.075849], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[ + ["principal_component_1", "principal_component_2", "principal_component_3"] + ], + expected, + check_exact=False, + rtol=0.1, + ) + + +def test_pipeline_standard_scaler_kmeans_fit_predict( + session, penguins_pandas_df_default_index +): + """Test an unsupervised model with a non-BQML implementation of StandardScaler""" + pl = pipeline.Pipeline( + [ + ("scale", preprocessing.StandardScaler()), + ("kmeans", cluster.KMeans(n_clusters=2)), + ] + ) + + # kmeans is sensitive to the order with this configuration, so use ordered source data + df = session.read_pandas(penguins_pandas_df_default_index).dropna() + train_X = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + pl.fit(train_X) + + # predict new labels + pd_new_penguins = pd.DataFrame.from_dict( + { + "test1": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Dream", + "culmen_length_mm": 27.5, + "culmen_depth_mm": 8.5, + "flipper_length_mm": 99, + "body_mass_g": 4475, + "sex": "MALE", + }, + "test2": { + "species": "Chinstrap penguin (Pygoscelis antarctica)", + "island": "Dream", + "culmen_length_mm": 55.8, + "culmen_depth_mm": 29.8, + "flipper_length_mm": 307, + "body_mass_g": 4000, + "sex": "MALE", + }, + "test3": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Biscoe", + "culmen_length_mm": 19.7, + "culmen_depth_mm": 8.9, + "flipper_length_mm": 84, + "body_mass_g": 3550, + "sex": "MALE", + }, + "test4": { + "species": "Gentoo penguin (Pygoscelis papua)", + "island": "Biscoe", + "culmen_length_mm": 63.8, + "culmen_depth_mm": 33.9, + "flipper_length_mm": 298, + "body_mass_g": 4300, + "sex": "FEMALE", + }, + "test5": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Dream", + "culmen_length_mm": 27.5, + "culmen_depth_mm": 8.5, + "flipper_length_mm": 99, + "body_mass_g": 4475, + "sex": "MALE", + }, + "test6": { + "species": "Chinstrap penguin (Pygoscelis antarctica)", + "island": "Dream", + "culmen_length_mm": 55.8, + "culmen_depth_mm": 29.8, + "flipper_length_mm": 307, + "body_mass_g": 4000, + "sex": "MALE", + }, + }, + orient="index", + ) + pd_new_penguins.index.name = "observation" + + new_penguins = session.read_pandas(pd_new_penguins) + result = pl.predict(new_penguins).to_pandas().sort_index() + expected = pd.DataFrame( + {"CENTROID_ID": [1, 2, 1, 2, 1, 2]}, + dtype="Int64", + index=pd.Index( + ["test1", "test2", "test3", "test4", "test5", "test6"], + dtype="string[pyarrow]", + ), + ) + expected.index.name = "observation" + assert_pandas_df_equal_ignore_ordering(result, expected) + + +def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_index): + """Test a preprocessing step that manages heterogenous data with ColumnTransformer""" + pl = pipeline.Pipeline( + [ + ( + "preproc", + compose.ColumnTransformer( + [ + ( + "onehot", + preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ), + ), + ("linreg", linear_model.LinearRegression()), + ] + ) + + df = penguins_df_default_index.dropna() + train_X = df[["species", "culmen_length_mm", "flipper_length_mm"]] + train_y = df[["body_mass_g"]] + pl.fit(train_X, train_y) + + # predict new labels + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Adelie Penguin (Pygoscelis adeliae)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + "island": ["Torgersen", "Torgersen", "Dream"], + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [18.8, 17.2, 18.1], + "flipper_length_mm": [196.0, 181.0, 188.0], + "sex": ["MALE", "FEMALE", "FEMALE"], + } + ).set_index("tag_number") + ) + predictions = pl.predict(new_penguins).to_pandas() + expected = pd.DataFrame( + {"predicted_body_mass_g": [3909.2, 3436.0, 2860.0]}, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + ) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py new file mode 100644 index 00000000000..714252c9e00 --- /dev/null +++ b/tests/system/large/test_remote_function.py @@ -0,0 +1,776 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +import importlib.util +import inspect +import math # must keep this at top level to test udf referring global import +import os.path +import shutil +import tempfile +import textwrap + +from google.api_core.exceptions import NotFound, ResourceExhausted +from google.cloud import functions_v2 +import pandas +import pytest +import test_utils.prefixer + +from bigframes.remote_function import ( + get_cloud_function_name, + get_remote_function_locations, +) +from tests.system.utils import assert_pandas_df_equal_ignore_ordering + +# Use this to control the number of cloud functions being deleted in a single +# test session. This should help soften the spike of the number of mutations per +# minute tracked against a quota limit (default 60, increased to 120 for +# bigframes-dev project) by the Cloud Functions API +# We are running pytest with "-n 20". Let's say each session lasts about a +# minute, so we are setting a limit of 120/20 = 6 deletions per session. +_MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION = 6 + +# NOTE: Keep this import at the top level to test global var behavior with +# remote functions +_team_pi = "Team Pi" +_team_euler = "Team Euler" + + +def get_remote_function_endpoints(bigquery_client, dataset_id): + """Get endpoints used by the remote functions in a datset""" + endpoints = set() + routines = bigquery_client.list_routines(dataset=dataset_id) + for routine in routines: + rf_options = routine._properties.get("remoteFunctionOptions") + if not rf_options: + continue + rf_endpoint = rf_options.get("endpoint") + if rf_endpoint: + endpoints.add(rf_endpoint) + return endpoints + + +def get_cloud_functions(functions_client, project, location, name_prefix="bigframes-"): + """Get the cloud functions in the given project and location.""" + _, location = get_remote_function_locations(location) + parent = f"projects/{project}/locations/{location}" + request = functions_v2.ListFunctionsRequest(parent=parent) + page_result = functions_client.list_functions(request=request) + full_name_prefix = parent + f"/functions/{name_prefix}" + for response in page_result: + if not name_prefix or response.name.startswith(full_name_prefix): + yield response + + +def delete_cloud_function(functions_client, full_name): + """Delete a cloud function with the given fully qualified name.""" + request = functions_v2.DeleteFunctionRequest(name=full_name) + operation = functions_client.delete_function(request=request) + return operation + + +def cleanup_remote_function_assets( + bigquery_client, functions_client, remote_udf, ignore_failures=True +): + """Clean up the GCP assets behind a bigframes remote function.""" + try: + bigquery_client.delete_routine(remote_udf.bigframes_remote_function) + delete_cloud_function(functions_client, remote_udf.bigframes_cloud_function) + except Exception: + # By default don't raise exception in cleanup + if not ignore_failures: + raise + + +def make_uniq_udf(udf): + """Transform a udf to another with same behavior but a unique name.""" + prefixer = test_utils.prefixer.Prefixer(udf.__name__, "") + udf_uniq_name = prefixer.create_prefix() + udf_file_name = f"{udf_uniq_name}.py" + + # We are not using `tempfile.TemporaryDirectory()` because we want to keep + # the temp code around, otherwise `inspect.getsource()` complains. + tmpdir = tempfile.mkdtemp() + udf_file_path = os.path.join(tmpdir, udf_file_name) + with open(udf_file_path, "w") as f: + # TODO(shobs): Find a better way of modifying the udf, maybe regex? + source_key = f"def {udf.__name__}" + target_key = f"def {udf_uniq_name}" + source_code = textwrap.dedent(inspect.getsource(udf)) + target_code = source_code.replace(source_key, target_key, 1) + f.write(target_code) + spec = importlib.util.spec_from_file_location(udf_file_name, udf_file_path) + return getattr(spec.loader.load_module(), udf_uniq_name), tmpdir + + +@pytest.fixture(scope="module") +def bq_cf_connection() -> str: + """Pre-created BQ connection to invoke cloud function for bigframes-dev + $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn + """ + return "bigframes-rf-conn" + + +@pytest.fixture(scope="module") +def functions_client() -> functions_v2.FunctionServiceClient: + """Cloud Functions client""" + return functions_v2.FunctionServiceClient() + + +@pytest.fixture(scope="module", autouse=True) +def cleanup_cloud_functions(session, functions_client, dataset_id_permanent): + """Clean up stale cloud functions.""" + permanent_endpoints = get_remote_function_endpoints( + session.bqclient, dataset_id_permanent + ) + delete_count = 0 + for cloud_function in get_cloud_functions( + functions_client, session.bqclient.project, session.bqclient.location + ): + # Ignore bigframes cloud functions referred by the remote functions in + # the permanent dataset + if cloud_function.service_config.uri in permanent_endpoints: + continue + + # Ignore the functions less than one day old + age = datetime.now() - datetime.fromtimestamp( + cloud_function.update_time.timestamp() + ) + if age.days <= 0: + continue + + # Go ahead and delete + try: + delete_cloud_function(functions_client, cloud_function.name) + delete_count += 1 + if delete_count >= _MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION: + break + except NotFound: + # This can happen when multiple pytest sessions are running in + # parallel. Two or more sessions may discover the same cloud + # function, but only one of them would be able to delete it + # successfully, while the other instance will run into this + # exception. Ignore this exception. + pass + except ResourceExhausted: + # This can happen if we are hitting GCP limits, e.g. + # google.api_core.exceptions.ResourceExhausted: 429 Quota exceeded + # for quota metric 'Per project mutation requests' and limit + # 'Per project mutation requests per minute per region' of service + # 'cloudfunctions.googleapis.com' for consumer + # 'project_number:1084210331973'. + # [reason: "RATE_LIMIT_EXCEEDED" domain: "googleapis.com" ... + # Let's stop further clean up and leave it to later. + break + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_multiply_with_ibis( + session, + scalars_table_id, + ibis_client, + dataset_id, + bq_cf_connection, + functions_client, +): + try: + + @session.remote_function( + [int, int], + int, + dataset_id, + bq_cf_connection, + reuse=False, + ) + def multiply(x, y): + return x * y + + project_id, dataset_name, table_name = scalars_table_id.split(".") + if not ibis_client.dataset: + ibis_client.dataset = dataset_name + + col_name = "int64_col" + table = ibis_client.tables[table_name] + table = table.filter(table[col_name].notnull()).order_by("rowindex").head(10) + pandas_df_orig = table.execute() + + col = table[col_name] + col_2x = multiply(col, 2).name("int64_col_2x") + col_square = multiply(col, col).name("int64_col_square") + table = table.mutate([col_2x, col_square]) + pandas_df_new = table.execute() + + pandas.testing.assert_series_equal( + pandas_df_orig[col_name] * 2, + pandas_df_new["int64_col_2x"], + check_names=False, + ) + + pandas.testing.assert_series_equal( + pandas_df_orig[col_name] * pandas_df_orig[col_name], + pandas_df_new["int64_col_square"], + check_names=False, + ) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets(session.bqclient, functions_client, multiply) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_stringify_with_ibis( + session, + scalars_table_id, + ibis_client, + dataset_id, + bq_cf_connection, + functions_client, +): + try: + + @session.remote_function( + [int], + str, + dataset_id, + bq_cf_connection, + reuse=False, + ) + def stringify(x): + return f"I got {x}" + + project_id, dataset_name, table_name = scalars_table_id.split(".") + if not ibis_client.dataset: + ibis_client.dataset = dataset_name + + col_name = "int64_col" + table = ibis_client.tables[table_name] + table = table.filter(table[col_name].notnull()).order_by("rowindex").head(10) + pandas_df_orig = table.execute() + + col = table[col_name] + col_2x = stringify(col).name("int64_str_col") + table = table.mutate([col_2x]) + pandas_df_new = table.execute() + + pandas.testing.assert_series_equal( + pandas_df_orig[col_name].apply(lambda x: f"I got {x}"), + pandas_df_new["int64_str_col"], + check_names=False, + ) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets(session.bqclient, functions_client, stringify) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_decorator_with_bigframes_series( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + @session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + reuse=False, + ) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets(session.bqclient, functions_client, square) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_explicit_with_bigframes_series( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + def add_one(x): + return x + 1 + + remote_add_one = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + reuse=False, + )(add_one) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(remote_add_one) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(add_one) + # TODO(shobs): Figure why pandas .apply() changes the dtype, e.g. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, remote_add_one + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_udf_referring_outside_var( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + POSITIVE_SIGN = 1 + NEGATIVE_SIGN = -1 + NO_SIGN = 0 + + def sign(num): + if num > 0: + return POSITIVE_SIGN + elif num < 0: + return NEGATIVE_SIGN + return NO_SIGN + + remote_sign = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + reuse=False, + )(sign) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(remote_sign) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(sign) + # TODO(shobs): Figure why pandas .apply() changes the dtype, e.g. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets(session.bqclient, functions_client, remote_sign) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_udf_referring_outside_import( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + import math as mymath + + def circumference(radius): + return 2 * mymath.pi * radius + + remote_circumference = session.remote_function( + [float], + float, + dataset_id, + bq_cf_connection, + reuse=False, + )(circumference) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_float64_col = scalars_df["float64_col"] + bf_float64_col_filter = bf_float64_col.notnull() + bf_float64_col_filtered = bf_float64_col[bf_float64_col_filter] + bf_result_col = bf_float64_col_filtered.apply(remote_circumference) + bf_result = ( + bf_float64_col_filtered.to_frame().assign(result=bf_result_col).compute() + ) + + pd_float64_col = scalars_pandas_df["float64_col"] + pd_float64_col_filter = pd_float64_col.notnull() + pd_float64_col_filtered = pd_float64_col[pd_float64_col_filter] + pd_result_col = pd_float64_col_filtered.apply(circumference) + # TODO(shobs): Figure why pandas .apply() changes the dtype, e.g. + # pd_float64_col_filtered.dtype is Float64Dtype() + # pd_float64_col_filtered.apply(lambda x: x).dtype is float64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Float64Dtype()) + pd_result = pd_float64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, remote_circumference + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_udf_referring_global_var_and_import( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + def find_team(num): + boundary = (math.pi + math.e) / 2 + if num >= boundary: + return _team_euler + return _team_pi + + remote_find_team = session.remote_function( + [float], + str, + dataset_id, + bq_cf_connection, + reuse=False, + )(find_team) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_float64_col = scalars_df["float64_col"] + bf_float64_col_filter = bf_float64_col.notnull() + bf_float64_col_filtered = bf_float64_col[bf_float64_col_filter] + bf_result_col = bf_float64_col_filtered.apply(remote_find_team) + bf_result = ( + bf_float64_col_filtered.to_frame().assign(result=bf_result_col).compute() + ) + + pd_float64_col = scalars_pandas_df["float64_col"] + pd_float64_col_filter = pd_float64_col.notnull() + pd_float64_col_filtered = pd_float64_col[pd_float64_col_filter] + pd_result_col = pd_float64_col_filtered.apply(find_team) + # TODO(shobs): Figure if the dtype mismatch is by design: + # bf_result.dtype: string[pyarrow] + # pd_result.dtype: dtype('O'). + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.StringDtype(storage="pyarrow")) + pd_result = pd_float64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, remote_find_team + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_restore_with_bigframes_series( + session, + scalars_dfs, + dataset_id, + bq_cf_connection, + functions_client, +): + try: + + def add_one(x): + return x + 1 + + # Make a unique udf + add_one_uniq, add_one_uniq_dir = make_uniq_udf(add_one) + + # This is a bit of a hack but we need to remove the reference to a foreign + # module, otherwise the serialization would keep the foreign module + # reference and deserialization would fail with error like following: + # ModuleNotFoundError: No module named 'add_one_2nxcmd9j' + # TODO(shobs): Figure out if there is a better way of generating the unique + # function object, but for now let's just set it to same module as the + # original udf. + add_one_uniq.__module__ = add_one.__module__ + + # Expected cloud function name for the unique udf + add_one_uniq_cf_name = get_cloud_function_name(add_one_uniq) + + # There should be no cloud function yet for the unique udf + cloud_functions = list( + get_cloud_functions( + functions_client, + session.bqclient.project, + session.bqclient.location, + name_prefix=add_one_uniq_cf_name, + ) + ) + assert len(cloud_functions) == 0 + + # The first time both the cloud function and the bq remote function don't + # exist and would be created + remote_add_one = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + reuse=True, + )(add_one_uniq) + + # There should have been excactly one cloud function created at this point + cloud_functions = list( + get_cloud_functions( + functions_client, + session.bqclient.project, + session.bqclient.location, + name_prefix=add_one_uniq_cf_name, + ) + ) + assert len(cloud_functions) == 1 + + # We will test this twice + def inner_test(): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(remote_add_one) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(add_one_uniq) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + # Test that the remote function works as expected + inner_test() + + # Let's delete the cloud function while not touching the bq remote function + delete_operation = delete_cloud_function( + functions_client, cloud_functions[0].name + ) + delete_operation.result() + assert delete_operation.done() + + # There should be no cloud functions at this point for the uniq udf + cloud_functions = list( + get_cloud_functions( + functions_client, + session.bqclient.project, + session.bqclient.location, + name_prefix=add_one_uniq_cf_name, + ) + ) + assert len(cloud_functions) == 0 + + # The second time bigframes detects that the required cloud function doesn't + # exist even though the remote function exists, and goes ahead and recreates + # the cloud function + remote_add_one = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + reuse=True, + )(add_one_uniq) + + # There should be excactly one cloud function again + cloud_functions = list( + get_cloud_functions( + functions_client, + session.bqclient.project, + session.bqclient.location, + name_prefix=add_one_uniq_cf_name, + ) + ) + assert len(cloud_functions) == 1 + + # Test again after the cloud function is restored that the remote function + # works as expected + inner_test() + + # clean up the temp code + shutil.rmtree(add_one_uniq_dir) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, remote_add_one + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_udf_mask_default_value( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + def is_odd(num): + flag = False + try: + flag = num % 2 == 1 + except TypeError: + pass + return flag + + is_odd_remote = session.remote_function( + [int], + bool, + dataset_id, + bq_cf_connection, + reuse=False, + )(is_odd) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_result_col = bf_int64_col.mask(is_odd_remote) + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_result_col = pd_int64_col.mask(is_odd) + pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, is_odd_remote + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_udf_mask_custom_value( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + def is_odd(num): + flag = False + try: + flag = num % 2 == 1 + except TypeError: + pass + return flag + + is_odd_remote = session.remote_function( + [int], + bool, + dataset_id, + bq_cf_connection, + reuse=False, + )(is_odd) + + scalars_df, scalars_pandas_df = scalars_dfs + + # TODO(shobs): Revisit this test when NA handling of pandas' Series.mask is + # fixed https://github.com/pandas-dev/pandas/issues/52955, + # for now filter out the nulls and test the rest + bf_int64_col = scalars_df["int64_col"] + bf_result_col = bf_int64_col[bf_int64_col.notnull()].mask(is_odd_remote, -1) + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_result_col = pd_int64_col[pd_int64_col.notnull()].mask(is_odd, -1) + pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, is_odd_remote + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_udf_lambda( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + add_one_lambda = lambda x: x + 1 # noqa: E731 + + add_one_lambda_remote = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + reuse=False, + )(add_one_lambda) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(add_one_lambda_remote) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(add_one_lambda) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, add_one_lambda_remote + ) diff --git a/tests/system/large/test_session.py b/tests/system/large/test_session.py new file mode 100644 index 00000000000..62fa5a83d33 --- /dev/null +++ b/tests/system/large/test_session.py @@ -0,0 +1,52 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from bigframes import Session + + +@pytest.mark.parametrize( + ("query_or_table", "index_col"), + [ + pytest.param( + "bigquery-public-data.patents_view.ipcr_201708", + (), + id="1g_table_w_default_index", + ), + pytest.param( + "bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2011", + (), + id="30g_table_w_default_index", + ), + # TODO(chelsealin): Disable the long run tests until we have propertily + # ordering support to avoid materializating any data. + # # Adding default index to large tables would take much longer time, + # # e.g. ~5 mins for a 100G table, ~20 mins for a 1T table. + # pytest.param( + # "bigquery-public-data.stackoverflow.post_history", + # ["id"], + # id="100g_table_w_unique_column_index", + # ), + # pytest.param( + # "bigquery-public-data.wise_all_sky_data_release.all_wise", + # ["cntr"], + # id="1t_table_w_unique_column_index", + # ), + ], +) +def test_read_gbq_for_large_tables(session: Session, query_or_table, index_col): + """Verify read_gbq() is able to read large tables.""" + df = session.read_gbq(query_or_table, index_col=index_col) + assert len(df.columns) != 0 diff --git a/tests/system/small/__init__.py b/tests/system/small/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/system/small/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/system/small/ml/__init__.py b/tests/system/small/ml/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/system/small/ml/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py new file mode 100644 index 00000000000..5c3abf25dcc --- /dev/null +++ b/tests/system/small/ml/conftest.py @@ -0,0 +1,294 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +import logging +from typing import cast +import uuid + +import google.cloud.exceptions +import pandas as pd +import pytest + +from bigframes.ml import core, forecasting, imported, llm +import bigframes.ml.cluster +import bigframes.ml.core +import bigframes.ml.ensemble +import bigframes.ml.linear_model + + +@pytest.fixture(scope="session") +def ml_connection() -> str: + return "bigframes-dev.us.bigframes-ml" + + +@pytest.fixture(scope="session") +def penguins_bqml_linear_model( + session, penguins_linear_model_name +) -> bigframes.ml.core.BqmlModel: + model = session.bqclient.get_model(penguins_linear_model_name) + return bigframes.ml.core.BqmlModel(session, model) + + +@pytest.fixture(scope="function") +def ephemera_penguins_bqml_linear_model( + penguins_bqml_linear_model, +) -> bigframes.ml.linear_model.LinearRegression: + model = penguins_bqml_linear_model + return model.copy( + f"{model._model.project}.{model._model.dataset_id}.{uuid.uuid4().hex}" + ) + + +@pytest.fixture(scope="session") +def penguins_linear_model( + session, penguins_linear_model_name: str +) -> bigframes.ml.linear_model.LinearRegression: + return cast( + bigframes.ml.linear_model.LinearRegression, + session.read_gbq_model(penguins_linear_model_name), + ) + + +@pytest.fixture(scope="function") +def ephemera_penguins_linear_model( + ephemera_penguins_bqml_linear_model: bigframes.ml.core.BqmlModel, +) -> bigframes.ml.linear_model.LinearRegression: + bf_model = bigframes.ml.linear_model.LinearRegression() + bf_model._bqml_model = ephemera_penguins_bqml_linear_model + return bf_model + + +@pytest.fixture(scope="session") +def penguins_logistic_model( + session, penguins_logistic_model_name +) -> bigframes.ml.linear_model.LogisticRegression: + return cast( + bigframes.ml.linear_model.LogisticRegression, + session.read_gbq_model(penguins_logistic_model_name), + ) + + +@pytest.fixture(scope="session") +def penguins_xgbregressor_model( + session, penguins_xgbregressor_model_name +) -> bigframes.ml.ensemble.XGBRegressor: + return cast( + bigframes.ml.ensemble.XGBRegressor, + session.read_gbq_model(penguins_xgbregressor_model_name), + ) + + +@pytest.fixture(scope="session") +def penguins_xgbclassifier_model( + session, penguins_xgbclassifier_model_name +) -> bigframes.ml.ensemble.XGBClassifier: + return cast( + bigframes.ml.ensemble.XGBClassifier, + session.read_gbq_model(penguins_xgbclassifier_model_name), + ) + + +@pytest.fixture(scope="session") +def penguins_randomforest_regressor_model( + session, penguins_randomforest_regressor_model_name +) -> bigframes.ml.ensemble.RandomForestRegressor: + return cast( + bigframes.ml.ensemble.RandomForestRegressor, + session.read_gbq_model(penguins_randomforest_regressor_model_name), + ) + + +@pytest.fixture(scope="session") +def penguins_randomforest_classifier_model( + session, penguins_randomforest_classifier_model_name +) -> bigframes.ml.ensemble.RandomForestClassifier: + return cast( + bigframes.ml.ensemble.RandomForestClassifier, + session.read_gbq_model(penguins_randomforest_classifier_model_name), + ) + + +@pytest.fixture(scope="session") +def penguins_kmeans_model( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> bigframes.ml.cluster.KMeans: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='kmeans', + num_clusters=3 +) AS SELECT + culmen_length_mm, + culmen_depth_mm, + flipper_length_mm, + sex +FROM `{penguins_table_id}`""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_cluster_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + return session.read_gbq_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_kmeans_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + return session.read_gbq_model(model_name) + + +@pytest.fixture(scope="session") +def penguins_pca_model( + session: bigframes.Session, dataset_id_permanent, penguins_table_id +) -> bigframes.ml.decomposition.PCA: + + # TODO(yunmengxie): Create a shared method to get different types of pretrained models. + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='pca', + num_principal_components=3 +) AS SELECT + * +FROM `{penguins_table_id}`""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = ( + f"{dataset_id_permanent}.penguins_pca_{hashlib.md5(sql.encode()).hexdigest()}" + ) + sql = sql.replace("$model_name", model_name) + + try: + return session.read_gbq_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_pca_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + return session.read_gbq_model(model_name) + + +@pytest.fixture(scope="session") +def llm_text_pandas_df(): + """Additional data matching the penguins dataset, with a new index""" + return pd.DataFrame( + { + "prompt": [ + "What is BigQuery?", + "What is BQML?", + "What is BigQuery DataFrame?", + ], + } + ) + + +@pytest.fixture(scope="session") +def onnx_iris_pandas_df(): + """Data matching the iris dataset.""" + return pd.DataFrame( + { + "sepal_length": [4.9, 5.1, 34.7], + "sepal_width": [3.0, 5.1, 24.7], + "petal_length": [1.4, 1.5, 13.3], + "petal_width": [0.4, 0.2, 18.3], + "species": [ + "setosa", + "setosa", + "virginica", + ], + } + ) + + +@pytest.fixture(scope="session") +def onnx_iris_df(session, onnx_iris_pandas_df): + return session.read_pandas(onnx_iris_pandas_df) + + +@pytest.fixture(scope="session") +def llm_text_df(session, llm_text_pandas_df): + return session.read_pandas(llm_text_pandas_df) + + +@pytest.fixture(scope="session") +def bqml_palm2_text_generator_model(session, ml_connection) -> core.BqmlModel: + options = { + "remote_service_type": "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1", + } + return core.create_bqml_remote_model( + session=session, connection_name=ml_connection, options=options + ) + + +@pytest.fixture(scope="session") +def palm2_text_generator_model(session, ml_connection) -> llm.PaLM2TextGenerator: + return llm.PaLM2TextGenerator(session=session, connection_name=ml_connection) + + +@pytest.fixture(scope="function") +def ephemera_palm2_text_generator_model( + session, ml_connection +) -> llm.PaLM2TextGenerator: + return llm.PaLM2TextGenerator(session=session, connection_name=ml_connection) + + +@pytest.fixture(scope="session") +def palm2_embedding_generator_model( + session, ml_connection +) -> llm.PaLM2EmbeddingGenerator: + return llm.PaLM2EmbeddingGenerator(session=session, connection_name=ml_connection) + + +@pytest.fixture(scope="session") +def time_series_bqml_arima_plus_model( + session, time_series_arima_plus_model_name +) -> core.BqmlModel: + model = session.bqclient.get_model(time_series_arima_plus_model_name) + return bigframes.ml.core.BqmlModel(session, model) + + +@pytest.fixture(scope="session") +def time_series_arima_plus_model( + session, time_series_arima_plus_model_name +) -> forecasting.ARIMAPlus: + return cast( + forecasting.ARIMAPlus, + session.read_gbq_model(time_series_arima_plus_model_name), + ) + + +@pytest.fixture(scope="session") +def imported_tensorflow_model(session) -> imported.TensorFlowModel: + return imported.TensorFlowModel( + session=session, + model_path="gs://cloud-training-demos/txtclass/export/exporter/1549825580/*", + ) + + +@pytest.fixture(scope="function") +def ephemera_imported_tensorflow_model(session) -> imported.TensorFlowModel: + return imported.TensorFlowModel( + session=session, + model_path="gs://cloud-training-demos/txtclass/export/exporter/1549825580/*", + ) + + +@pytest.fixture(scope="session") +def imported_onnx_model(session) -> imported.OnnxModel: + return imported.OnnxModel( + session=session, + model_path="gs://cloud-samples-data/bigquery/ml/onnx/pipeline_rf.onnx", + ) diff --git a/tests/system/small/ml/test_cluster.py b/tests/system/small/ml/test_cluster.py new file mode 100644 index 00000000000..4aefc5fa697 --- /dev/null +++ b/tests/system/small/ml/test_cluster.py @@ -0,0 +1,79 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas + +import bigframes.ml.cluster +from tests.system.utils import assert_pandas_df_equal_ignore_ordering + + +def test_model_predict(session, penguins_kmeans_model: bigframes.ml.cluster.KMeans): + pd_new_penguins = pandas.DataFrame.from_dict( + { + "test1": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Dream", + "culmen_length_mm": 37.5, + "culmen_depth_mm": 18.5, + "flipper_length_mm": 199, + "body_mass_g": 4475, + "sex": "MALE", + }, + "test2": { + "species": "Chinstrap penguin (Pygoscelis antarctica)", + "island": "Dream", + "culmen_length_mm": 55.8, + "culmen_depth_mm": 19.8, + "flipper_length_mm": 207, + "body_mass_g": 4000, + "sex": "MALE", + }, + "test3": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Biscoe", + "culmen_length_mm": 39.7, + "culmen_depth_mm": 18.9, + "flipper_length_mm": 184, + "body_mass_g": 3550, + "sex": "MALE", + }, + "test4": { + "species": "Gentoo penguin (Pygoscelis papua)", + "island": "Biscoe", + "culmen_length_mm": 43.8, + "culmen_depth_mm": 13.9, + "flipper_length_mm": 208, + "body_mass_g": 4300, + "sex": "FEMALE", + }, + }, + orient="index", + ) + pd_new_penguins.index.name = "observation" + + new_penguins = session.read_pandas(pd_new_penguins) + result = penguins_kmeans_model.predict(new_penguins).compute() + expected = pandas.DataFrame( + {"CENTROID_ID": [2, 3, 1, 2]}, + dtype="Int64", + index=pandas.Index( + ["test1", "test2", "test3", "test4"], dtype="string[pyarrow]" + ), + ) + expected.index.name = "observation" + assert_pandas_df_equal_ignore_ordering(result, expected) + + +def test_loaded_config(penguins_kmeans_model): + assert penguins_kmeans_model.n_clusters == 3 diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py new file mode 100644 index 00000000000..d63e401dd7a --- /dev/null +++ b/tests/system/small/ml/test_core.py @@ -0,0 +1,193 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +import typing +from unittest import TestCase + +import pandas as pd +import pyarrow as pa +import pytz + +import bigframes +import bigframes.ml.core + + +def test_model_eval( + penguins_bqml_linear_model, +): + result = penguins_bqml_linear_model.evaluate().compute() + expected = pd.DataFrame( + { + "mean_absolute_error": [227.01223], + "mean_squared_error": [81838.159892], + "mean_squared_log_error": [0.00507], + "median_absolute_error": [173.080816], + "r2_score": [0.872377], + "explained_variance": [0.872377], + }, + dtype="Float64", + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_model_eval_with_data(penguins_bqml_linear_model, penguins_df_default_index): + result = penguins_bqml_linear_model.evaluate( + penguins_df_default_index.dropna() + ).compute() + expected = pd.DataFrame( + { + "mean_absolute_error": [225.817334], + "mean_squared_error": [80540.705944], + "mean_squared_log_error": [0.004972], + "median_absolute_error": [173.080816], + "r2_score": [0.87529], + "explained_variance": [0.87529], + }, + dtype="Float64", + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_model_predict( + penguins_bqml_linear_model: bigframes.ml.core.BqmlModel, new_penguins_df +): + predictions = penguins_bqml_linear_model.predict(new_penguins_df).compute() + expected = pd.DataFrame( + {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]].sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) + + +def test_model_predict_with_unnamed_index( + penguins_bqml_linear_model: bigframes.ml.core.BqmlModel, new_penguins_df +): + + # This will result in an index that lacks a name, which the ML library will + # need to persist through the call to ML.PREDICT + new_penguins_df = new_penguins_df.reset_index() + + # remove the middle tag number to ensure we're really keeping the unnamed index + new_penguins_df = typing.cast( + bigframes.dataframe.DataFrame, + new_penguins_df[new_penguins_df.tag_number != 1672], + ) + + predictions = penguins_bqml_linear_model.predict(new_penguins_df).compute() + + expected = pd.DataFrame( + {"predicted_body_mass_g": [4030.1, 3177.9]}, + dtype="Float64", + index=pd.Index([0, 2], dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]].sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) + + +def test_model_generate_text( + bqml_palm2_text_generator_model: bigframes.ml.core.BqmlModel, llm_text_df +): + options = { + "temperature": 0.5, + "max_output_tokens": 100, + "top_k": 20, + "top_p": 0.5, + "flatten_json_output": True, + } + df = bqml_palm2_text_generator_model.generate_text( + llm_text_df, options=options + ).compute() + + TestCase().assertSequenceEqual(df.shape, (3, 4)) + TestCase().assertSequenceEqual( + [ + "ml_generate_text_llm_result", + "ml_generate_text_rai_result", + "ml_generate_text_status", + "prompt", + ], + df.columns.to_list(), + ) + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +def test_model_forecast(time_series_bqml_arima_plus_model: bigframes.ml.core.BqmlModel): + utc = pytz.utc + forecast = time_series_bqml_arima_plus_model.forecast().compute()[ + ["forecast_timestamp", "forecast_value"] + ] + expected = pd.DataFrame( + { + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "forecast_value": [2724.472284, 2593.368389, 2353.613034], + } + ) + expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) + expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( + pd.ArrowDtype(pa.timestamp("us", tz="UTC")) + ) + pd.testing.assert_frame_equal( + forecast, + expected, + rtol=0.1, + check_index_type=False, + ) + + +def test_model_register(ephemera_penguins_bqml_linear_model): + model = ephemera_penguins_bqml_linear_model + model.register() + + model_name = "bigframes_" + model.model.model_id + # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. + assert model_name in model.model.training_runs[-1]["vertexAiModelId"] + + +def test_model_register_with_params(ephemera_penguins_bqml_linear_model): + model_name = "bigframes_system_test_model" + model = ephemera_penguins_bqml_linear_model + model.register(model_name) + + # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. + assert model_name in model.model.training_runs[-1]["vertexAiModelId"] diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py new file mode 100644 index 00000000000..7e0421129eb --- /dev/null +++ b/tests/system/small/ml/test_decomposition.py @@ -0,0 +1,55 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas + +import bigframes.ml.decomposition + + +def test_model_predict(session, penguins_pca_model: bigframes.ml.decomposition.PCA): + new_penguins = session.read_pandas( + pandas.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Gentoo penguin (Pygoscelis papua)", + "Adelie Penguin (Pygoscelis adeliae)", + ], + "island": ["Dream", "Biscoe", "Torgersen"], + "culmen_length_mm": [37.8, 46.5, 41.1], + "culmen_depth_mm": [18.1, 14.8, 18.6], + "flipper_length_mm": [193.0, 217.0, 189.0], + "body_mass_g": [3750.0, 5200.0, 3325.0], + "sex": ["MALE", "FEMALE", "MALE"], + } + ).set_index("tag_number") + ) + + predictions = penguins_pca_model.predict(new_penguins).compute() + expected = pandas.DataFrame( + { + "principal_component_1": [-1.459, 2.258, -1.685], + "principal_component_2": [-1.120, -1.351, -0.874], + "principal_component_3": [-0.646, 0.443, -0.704], + }, + dtype="Float64", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + predictions.sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) diff --git a/tests/system/small/ml/test_ensemble.py b/tests/system/small/ml/test_ensemble.py new file mode 100644 index 00000000000..61c60c249f2 --- /dev/null +++ b/tests/system/small/ml/test_ensemble.py @@ -0,0 +1,427 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +import google.api_core.exceptions +import pandas +import pytest + +import bigframes.ml.ensemble + + +def test_xgbregressor_model_score( + penguins_xgbregressor_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = penguins_xgbregressor_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "mean_absolute_error": [108.77582], + "mean_squared_error": [20943.272738], + "mean_squared_log_error": [0.00135], + "median_absolute_error": [86.313477], + "r2_score": [0.967571], + "explained_variance": [0.967609], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_xgbregressor_model_predict( + penguins_xgbregressor_model: bigframes.ml.ensemble.XGBRegressor, new_penguins_df +): + result = penguins_xgbregressor_model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_body_mass_g": ["4293.1538089", "3410.0271", "3357.944"]}, + dtype="Float64", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + result.sort_index(), + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + ) + + +def test_to_gbq_saved_xgbregressor_model_scores( + penguins_xgbregressor_model, dataset_id, penguins_df_default_index +): + saved_model = penguins_xgbregressor_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = saved_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "mean_absolute_error": [109.016973], + "mean_squared_error": [20867.299758], + "mean_squared_log_error": [0.00135], + "median_absolute_error": [86.490234], + "r2_score": [0.967458], + "explained_variance": [0.967504], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_to_xgbregressor_model_gbq_replace(penguins_xgbregressor_model, dataset_id): + penguins_xgbregressor_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + with pytest.raises(google.api_core.exceptions.Conflict): + penguins_xgbregressor_model.to_gbq(f"{dataset_id}.test_penguins_model") + + +def test_xgbclassifier_model_score( + penguins_xgbclassifier_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = penguins_xgbclassifier_model.score(test_X, test_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + +def test_xgbclassifier_model_predict( + penguins_xgbclassifier_model: bigframes.ml.ensemble.XGBClassifier, new_penguins_df +): + result = penguins_xgbclassifier_model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, + dtype="string[pyarrow]", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + result.sort_index(), + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + ) + + +def test_to_gbq_saved_xgbclassifier_model_scores( + penguins_xgbclassifier_model, dataset_id, penguins_df_default_index +): + saved_model = penguins_xgbclassifier_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = saved_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "precision": [1.0], + "recall": [1.0], + "accuracy": [1.0], + "f1_score": [1.0], + "log_loss": [0.331442], + "roc_auc": [1.0], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + assert saved_model.max_depth == 6 + assert saved_model.max_iterations == 20 + + +def test_to_xgbclassifier_model_gbq_replace(penguins_xgbclassifier_model, dataset_id): + penguins_xgbclassifier_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + with pytest.raises(google.api_core.exceptions.Conflict): + penguins_xgbclassifier_model.to_gbq(f"{dataset_id}.test_penguins_model") + + +def test_randomforestregressor_model_score( + penguins_randomforest_regressor_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = penguins_randomforest_regressor_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "mean_absolute_error": [317.031042], + "mean_squared_error": [159713.053504], + "mean_squared_log_error": [0.008449], + "median_absolute_error": [258.385742], + "r2_score": [0.752698], + "explained_variance": [0.756173], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigFramese + check_index_type=False, + ) + + +def test_randomforestregressor_model_predict( + penguins_randomforest_regressor_model: bigframes.ml.ensemble.RandomForestRegressor, + new_penguins_df, +): + result = penguins_randomforest_regressor_model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_body_mass_g": ["3897.341797", "3458.385742", "3458.385742"]}, + dtype="Float64", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + result.sort_index(), + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + ) + + +def test_to_gbq_saved_randomforestregressor_model_scores( + penguins_randomforest_regressor_model, dataset_id, penguins_df_default_index +): + saved_model = penguins_randomforest_regressor_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = saved_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "mean_absolute_error": [319.239235], + "mean_squared_error": [161913.126651], + "mean_squared_log_error": [0.008611], + "median_absolute_error": [266.614258], + "r2_score": [0.747504], + "explained_variance": [0.750358], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigFramese + check_index_type=False, + ) + + +def test_to_randomforestregressor_model_gbq_replace( + penguins_randomforest_regressor_model, dataset_id +): + penguins_randomforest_regressor_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + with pytest.raises(google.api_core.exceptions.Conflict): + penguins_randomforest_regressor_model.to_gbq( + f"{dataset_id}.test_penguins_model" + ) + + +def test_randomforestclassifier_model_score( + penguins_randomforest_classifier_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = penguins_randomforest_classifier_model.score(test_X, test_y).compute() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + +def test_randomforestclassifier_model_predict( + penguins_randomforest_classifier_model: bigframes.ml.ensemble.RandomForestClassifier, + new_penguins_df, +): + result = penguins_randomforest_classifier_model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, + dtype="string[pyarrow]", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + result.sort_index(), + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + ) + + +def test_to_gbq_saved_randomforestclassifier_model_scores( + penguins_randomforest_classifier_model, dataset_id, penguins_df_default_index +): + saved_model = penguins_randomforest_classifier_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = saved_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "precision": [0.636746], + "recall": [0.638636], + "accuracy": [0.95509], + "f1_score": [0.637688], + "log_loss": [0.886307], + "roc_auc": [0.966543], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_to_randomforestclassifier_model_gbq_replace( + penguins_randomforest_classifier_model, dataset_id +): + penguins_randomforest_classifier_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + with pytest.raises(google.api_core.exceptions.Conflict): + penguins_randomforest_classifier_model.to_gbq( + f"{dataset_id}.test_penguins_model" + ) diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py new file mode 100644 index 00000000000..153299f5989 --- /dev/null +++ b/tests/system/small/ml/test_forecasting.py @@ -0,0 +1,44 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime + +import pandas as pd +import pyarrow as pa +import pytz + + +def test_model_predict(time_series_arima_plus_model): + utc = pytz.utc + predictions = time_series_arima_plus_model.predict().compute() + expected = pd.DataFrame( + { + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "forecast_value": [2724.472284, 2593.368389, 2353.613034], + } + ) + expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) + expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( + pd.ArrowDtype(pa.timestamp("us", tz="UTC")) + ) + pd.testing.assert_frame_equal( + predictions, + expected, + rtol=0.1, + check_index_type=False, + ) diff --git a/tests/system/small/ml/test_imported.py b/tests/system/small/ml/test_imported.py new file mode 100644 index 00000000000..b2a9abc46e7 --- /dev/null +++ b/tests/system/small/ml/test_imported.py @@ -0,0 +1,68 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd + + +def test_tensorflow_create_model(imported_tensorflow_model): + # Model creation doesn't return error + assert imported_tensorflow_model is not None + + +def test_tensorflow_model_predict(imported_tensorflow_model, llm_text_df): + df = llm_text_df.rename(columns={"prompt": "input"}) + result = imported_tensorflow_model.predict(df).compute() + # The values are non-human-readable. As they are a dense layer of Neural Network. + # And since it is pretrained and imported, the model is a opaque-box. + # We may want to switch to better test model and cases. + value = np.array( + [9.375373792863684e-07, 0.00015779426030348986, 0.9998412132263184] + ) + expected = pd.DataFrame( + { + "dense_1": [value, value, value], + }, + ) + expected.set_index(expected.index.astype("Int64"), inplace=True) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + atol=0.1, + ) + + +def test_onnx_create_model(imported_onnx_model): + # Model creation doesn't return error + assert imported_onnx_model is not None + + +def test_onnx_model_predict(imported_onnx_model, onnx_iris_df): + result = imported_onnx_model.predict(onnx_iris_df).compute() + value1 = np.array([0.9999993443489075, 0.0, 0.0]) + value2 = np.array([0.0, 0.0, 0.9999993443489075]) + expected = pd.DataFrame( + { + "label": pd.array([0, 0, 2]).astype("Int64"), + "probabilities": [value1, value1, value2], + }, + index=pd.Index([0, 1, 2], dtype="Int64"), + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + atol=0.1, + ) diff --git a/tests/system/small/ml/test_linear_model.py b/tests/system/small/ml/test_linear_model.py new file mode 100644 index 00000000000..d5d6957eec3 --- /dev/null +++ b/tests/system/small/ml/test_linear_model.py @@ -0,0 +1,209 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import google.api_core.exceptions +import pandas +import pytest + + +def test_linear_reg_model_score(penguins_linear_model, penguins_df_default_index): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + test_y = df[["body_mass_g"]] + result = penguins_linear_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "mean_absolute_error": [225.817334], + "mean_squared_error": [80540.705944], + "mean_squared_log_error": [0.004972], + "median_absolute_error": [173.080816], + "r2_score": [0.87529], + "explained_variance": [0.87529], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_linear_reg_model_predict(penguins_linear_model, new_penguins_df): + predictions = penguins_linear_model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, + dtype="Float64", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + predictions.sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) + + +def test_to_gbq_saved_linear_reg_model_scores( + penguins_linear_model, dataset_id, penguins_df_default_index +): + saved_model = penguins_linear_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + test_y = df[["body_mass_g"]] + result = saved_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "mean_absolute_error": [227.01223], + "mean_squared_error": [81838.159892], + "mean_squared_log_error": [0.00507], + "median_absolute_error": [173.080816], + "r2_score": [0.872377], + "explained_variance": [0.872377], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_to_gbq_replace(penguins_linear_model, dataset_id): + penguins_linear_model.to_gbq(f"{dataset_id}.test_penguins_model", replace=True) + with pytest.raises(google.api_core.exceptions.Conflict): + penguins_linear_model.to_gbq(f"{dataset_id}.test_penguins_model") + + +def test_logistic_model_score(penguins_logistic_model, penguins_df_default_index): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = penguins_logistic_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "precision": [0.616753], + "recall": [0.618615], + "accuracy": [0.92515], + "f1_score": [0.617681], + "log_loss": [1.498832], + "roc_auc": [0.975807], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_logsitic_model_predict(penguins_logistic_model, new_penguins_df): + predictions = penguins_logistic_model.predict(new_penguins_df).compute() + expected = pandas.DataFrame( + {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, + dtype="string[pyarrow]", + index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pandas.testing.assert_frame_equal( + predictions.sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) + + +def test_to_gbq_saved_logsitic_model_score( + penguins_logistic_model, dataset_id, penguins_df_default_index +): + saved_model = penguins_logistic_model.to_gbq( + f"{dataset_id}.test_penguins_model", replace=True + ) + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df[["sex"]] + result = saved_model.score(test_X, test_y).compute() + expected = pandas.DataFrame( + { + "precision": [0.616753], + "recall": [0.618615], + "accuracy": [0.92515], + "f1_score": [0.617681], + "log_loss": [1.498832], + "roc_auc": [0.975807], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_to_logistic_model_gbq_replace(penguins_logistic_model, dataset_id): + penguins_logistic_model.to_gbq(f"{dataset_id}.test_penguins_model", replace=True) + with pytest.raises(google.api_core.exceptions.Conflict): + penguins_logistic_model.to_gbq(f"{dataset_id}.test_penguins_model") diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py new file mode 100644 index 00000000000..b579e754e5a --- /dev/null +++ b/tests/system/small/ml/test_llm.py @@ -0,0 +1,72 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +import numpy as np + + +def test_create_text_generator_model(palm2_text_generator_model): + # Model creation doesn't return error + assert palm2_text_generator_model is not None + + +def test_text_generator_predict_default_params_success( + palm2_text_generator_model, llm_text_df +): + df = palm2_text_generator_model.predict(llm_text_df).compute() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +def test_text_generator_predict_arbitrary_col_label_success( + palm2_text_generator_model, llm_text_df +): + llm_text_df = llm_text_df.rename(columns={"prompt": "arbitrary"}) + df = palm2_text_generator_model.predict(llm_text_df).compute() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +def test_text_generator_predict_with_params_success( + palm2_text_generator_model, llm_text_df +): + df = palm2_text_generator_model.predict( + llm_text_df, temperature=0.5, max_output_tokens=100, top_k=20, top_p=0.5 + ).compute() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +def test_create_embedding_generator_model(palm2_embedding_generator_model): + # Model creation doesn't return error + assert palm2_embedding_generator_model is not None + + +def test_embedding_generator_predict_success( + palm2_embedding_generator_model, llm_text_df +): + df = palm2_embedding_generator_model.predict(llm_text_df).compute() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_embed_text_embedding" in df.columns + series = df["ml_embed_text_embedding"] + value = series[0] + assert isinstance(value, np.ndarray) + assert value.size == 768 diff --git a/tests/system/small/ml/test_metrics.py b/tests/system/small/ml/test_metrics.py new file mode 100644 index 00000000000..5b4c486b570 --- /dev/null +++ b/tests/system/small/ml/test_metrics.py @@ -0,0 +1,615 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +import numpy as np +import pandas as pd +import pytest +import sklearn.metrics as sklearn_metrics # type: ignore + +import bigframes.ml.metrics + + +def test_r2_score_perfect_fit(session): + pd_df = pd.DataFrame({"y_true": [1, 7, 3, 2, 5], "y_pred": [1, 7, 3, 2, 5]}) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred"]]) == 1.0 + + +def test_r2_score_bad_fit(session): + pd_df = pd.DataFrame({"y_true": [1, 2, 3, 4, 5], "y_pred": [5, 4, 3, 2, 1]}) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred"]]) == -3.0 + + +def test_r2_score_force_finite(session): + pd_df = pd.DataFrame( + { + "y_true": [1, 1, 1, 1, 1], + "y_pred_1": [5, 4, 3, 2, 1], + "y_pred_2": [1, 1, 1, 1, 1], + } + ) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.r2_score( + df[["y_true"]], df[["y_pred_1"]], force_finite=False + ) == float("-inf") + assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred_1"]]) == 0.0 + assert math.isnan( + bigframes.ml.metrics.r2_score( + df[["y_true"]], df[["y_pred_2"]], force_finite=False + ) + ) + assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred_2"]]) == 1.0 + + +def test_r2_score_ok_fit_matches_sklearn(session): + pd_df = pd.DataFrame({"y_true": [1, 2, 3, 4, 5], "y_pred": [2, 3, 4, 3, 6]}) + + df = session.read_pandas(pd_df) + bf_result = bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred"]]) + sklearn_result = sklearn_metrics.r2_score(pd_df[["y_true"]], pd_df[["y_pred"]]) + assert math.isclose(bf_result, sklearn_result) + + +def test_accuracy_score_perfect_fit(session): + pd_df = pd.DataFrame({"y_true": [1, 7, 3, 2, 5], "y_pred": [1, 7, 3, 2, 5]}) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) == 1.0 + + +def test_accuracy_score_bad_fit(session): + pd_df = pd.DataFrame({"y_true": [0, 2, 1, 3, 4], "y_pred": [0, 1, 2, 3, 4]}) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) == 0.6 + + +def test_accuracy_score_not_normailze(session): + pd_df = pd.DataFrame({"y_true": [0, 2, 1, 3, 4], "y_pred": [0, 1, 2, 3, 4]}) + + df = session.read_pandas(pd_df) + assert ( + bigframes.ml.metrics.accuracy_score( + df[["y_true"]], df[["y_pred"]], normalize=False + ) + == 3 + ) + + +@pytest.mark.skipif(sklearn_metrics is None, reason="requires sklearn") +def test_accuracy_score_fit_matches_sklearn(session): + pd_df = pd.DataFrame({"y_true": [1, 2, 3, 4, 5], "y_pred": [2, 3, 4, 3, 6]}) + + df = session.read_pandas(pd_df) + bf_result = bigframes.ml.metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) + sklearn_result = sklearn_metrics.accuracy_score( + pd_df[["y_true"]], pd_df[["y_pred"]] + ) + assert math.isclose(bf_result, sklearn_result) + + +def test_roc_curve_binary_classification_prediction_returns_expected(session): + pd_df = pd.DataFrame( + { + "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score": [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45], + } + ) + + df = session.read_pandas(pd_df) + fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + df[["y_true"]], df[["y_score"]], drop_intermediate=False + ) + + pd_fpr = fpr.compute() + pd_tpr = tpr.compute() + pd_thresholds = thresholds.compute() + + pd.testing.assert_series_equal( + # skip testing the first value, as it is redundant and inconsistent across sklearn versions + pd_thresholds[1:], + pd.Series( + [0.9, 0.8, 0.65, 0.6, 0.5, 0.45, 0.4, 0.35, 0.3, 0.1], + dtype="Float64", + name="thresholds", + ), + check_index=False, + ) + pd.testing.assert_series_equal( + pd_fpr, + pd.Series( + [0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 0.75, 1.0], + dtype="Float64", + name="fpr", + ), + check_index_type=False, + ) + pd.testing.assert_series_equal( + pd_tpr, + pd.Series( + [ + 0.0, + 0.16666667, + 0.33333333, + 0.33333333, + 0.5, + 0.5, + 0.66666667, + 0.66666667, + 0.83333333, + 1.0, + 1.0, + ], + dtype="Float64", + name="tpr", + ), + check_index_type=False, + ) + + +def test_roc_curve_binary_classification_prediction_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score": [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45], + } + ) + + df = session.read_pandas(pd_df) + fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + df[["y_true"]], df[["y_score"]], drop_intermediate=False + ) + expected_fpr, expected_tpr, expected_thresholds = sklearn_metrics.roc_curve( + pd_df[["y_true"]], pd_df[["y_score"]], drop_intermediate=False + ) + + # sklearn returns float64 np arrays + np_fpr = fpr.compute().astype("float64").array + np_tpr = tpr.compute().astype("float64").array + np_thresholds = thresholds.compute().astype("float64").array + + np.testing.assert_array_equal( + # skip testing the first value, as it is redundant and inconsistent across sklearn versions + np_thresholds[1:], + expected_thresholds[1:], + ) + np.testing.assert_array_equal( + np_fpr, + expected_fpr, + ) + np.testing.assert_array_equal( + np_tpr, + expected_tpr, + ) + + +def test_roc_curve_binary_classification_decision_returns_expected(session): + # Instead of operating on probabilities, assume a 70% decision threshold + # has been applied, and operate on the final output + y_score = [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45] + decisions_70pct = [1 if s > 0.7 else 0 for s in y_score] + pd_df = pd.DataFrame( + { + "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score": decisions_70pct, + } + ) + + df = session.read_pandas(pd_df) + fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + df[["y_true"]], df[["y_score"]], drop_intermediate=False + ) + + pd_fpr = fpr.compute() + pd_tpr = tpr.compute() + pd_thresholds = thresholds.compute() + + pd.testing.assert_series_equal( + # skip testing the first value, as it is redundant and inconsistent across sklearn versions + pd_thresholds[1:], + pd.Series( + [1.0, 0.0], + dtype="Float64", + name="thresholds", + ), + check_index=False, + ) + pd.testing.assert_series_equal( + pd_fpr, + pd.Series( + [0.0, 0.0, 1.0], + dtype="Float64", + name="fpr", + ), + check_index_type=False, + ) + pd.testing.assert_series_equal( + pd_tpr, + pd.Series( + [ + 0.0, + 0.33333333, + 1.0, + ], + dtype="Float64", + name="tpr", + ), + check_index_type=False, + ) + + +def test_roc_curve_binary_classification_decision_matches_sklearn(session): + # Instead of operating on probabilities, assume a 70% decision threshold + # has been applied, and operate on the final output + y_score = [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45] + decisions_70pct = [1 if s > 0.7 else 0 for s in y_score] + pd_df = pd.DataFrame( + { + "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score": decisions_70pct, + } + ) + + df = session.read_pandas(pd_df) + fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + df[["y_true"]], df[["y_score"]], drop_intermediate=False + ) + expected_fpr, expected_tpr, expected_thresholds = sklearn_metrics.roc_curve( + pd_df[["y_true"]], pd_df[["y_score"]], drop_intermediate=False + ) + + # sklearn returns float64 np arrays + np_fpr = fpr.compute().astype("float64").array + np_tpr = tpr.compute().astype("float64").array + np_thresholds = thresholds.compute().astype("float64").array + + np.testing.assert_array_equal( + # skip testing the first value, as it is redundant and inconsistent across sklearn versions + np_thresholds[1:], + expected_thresholds[1:], + ) + np.testing.assert_array_equal( + np_fpr, + expected_fpr, + ) + np.testing.assert_array_equal( + np_tpr, + expected_tpr, + ) + + +def test_roc_auc_score_returns_expected(session): + pd_df = pd.DataFrame( + { + "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score": [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45], + } + ) + + df = session.read_pandas(pd_df) + score = bigframes.ml.metrics.roc_auc_score(df[["y_true"]], df[["y_score"]]) + + assert score == 0.625 + + +def test_roc_auc_score_returns_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score": [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45], + } + ) + + df = session.read_pandas(pd_df) + score = bigframes.ml.metrics.roc_auc_score(df[["y_true"]], df[["y_score"]]) + expected_score = sklearn_metrics.roc_auc_score( + pd_df[["y_true"]], pd_df[["y_score"]] + ) + + assert score == expected_score + + +def test_auc_invalid_x_size(session): + pd_df = pd.DataFrame({"x": [0], "y": [0]}) + df = session.read_pandas(pd_df) + with pytest.raises(ValueError): + bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) + + +def test_auc_nondecreasing_x(session): + pd_df = pd.DataFrame({"x": [0, 0, 0.5, 0.5, 1], "y": [0, 0.5, 0.5, 1, 1]}) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) == 0.75 + + +def test_auc_nonincreasing_x(session): + pd_df = pd.DataFrame({"x": [0, 0, -0.5, -0.5, -1], "y": [0, 0.5, 0.5, 1, 1]}) + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) == 0.75 + + +def test_auc_nonincreasing_x_negative(session): + pd_df = pd.DataFrame({"x": [0, 0, -0.5, -0.5, -1], "y": [0, -0.5, -0.5, -1, -1]}) + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) == -0.75 + + +def test_confusion_matrix(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + confusion_matrix = bigframes.ml.metrics.confusion_matrix( + df[["y_true"]], df[["y_pred"]] + ) + expected_pd_df = pd.DataFrame( + { + 0: [2, 0, 1], + 1: [0, 0, 0], + 2: [0, 1, 2], + } + ).astype("int64") + pd.testing.assert_frame_equal( + confusion_matrix, expected_pd_df, check_index_type=False + ) + + +def test_confusion_matrix_column_index(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 3, 3, 3, 4, 1], + "y_pred": [4, 1, 2, 2, 4, 1], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + confusion_matrix = bigframes.ml.metrics.confusion_matrix( + df[["y_true"]], df[["y_pred"]] + ) + expected_pd_df = ( + pd.DataFrame( + {1: [1, 0, 1, 0], 2: [0, 0, 2, 0], 3: [0, 0, 0, 0], 4: [0, 1, 0, 1]} + ) + .astype("int64") + .set_index([pd.Index([1, 2, 3, 4])]) + ) + pd.testing.assert_frame_equal( + confusion_matrix, expected_pd_df, check_index_type=False + ) + + +def test_confusion_matrix_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 3, 3, 3, 4, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + confusion_matrix = bigframes.ml.metrics.confusion_matrix( + df[["y_true"]], df[["y_pred"]] + ) + expected_confusion_matrix = sklearn_metrics.confusion_matrix( + pd_df[["y_true"]], pd_df[["y_pred"]] + ) + expected_pd_df = pd.DataFrame(expected_confusion_matrix) + pd.testing.assert_frame_equal( + confusion_matrix, expected_pd_df, check_index_type=False + ) + + +def test_confusion_matrix_str_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": ["cat", "ant", "cat", "cat", "ant", "bird"], + "y_pred": ["ant", "ant", "cat", "cat", "ant", "cat"], + } + ).astype("str") + df = session.read_pandas(pd_df) + confusion_matrix = bigframes.ml.metrics.confusion_matrix( + df[["y_true"]], df[["y_pred"]] + ) + expected_confusion_matrix = sklearn_metrics.confusion_matrix( + pd_df[["y_true"]], pd_df[["y_pred"]] + ) + expected_pd_df = pd.DataFrame(expected_confusion_matrix).set_index( + [pd.Index(["ant", "bird", "cat"])] + ) + expected_pd_df.columns = pd.Index(["ant", "bird", "cat"]) + pd.testing.assert_frame_equal( + confusion_matrix, expected_pd_df, check_index_type=False + ) + + +def test_recall_score(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + recall = bigframes.ml.metrics.recall_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = [1.000000, 0.000000, 0.666667] + expected_index = [0, 1, 2] + expected_recall = pd.Series(expected_values, index=expected_index) + + pd.testing.assert_series_equal(recall, expected_recall, check_index_type=False) + + +def test_recall_score_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + recall = bigframes.ml.metrics.recall_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = sklearn_metrics.recall_score( + pd_df[["y_true"]], pd_df[["y_pred"]], average=None + ) + expected_index = [0, 1, 2] + expected_recall = pd.Series(expected_values, index=expected_index) + pd.testing.assert_series_equal(recall, expected_recall, check_index_type=False) + + +def test_recall_score_str_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": ["cat", "ant", "cat", "cat", "ant", "bird"], + "y_pred": ["ant", "ant", "cat", "cat", "ant", "cat"], + } + ).astype("str") + df = session.read_pandas(pd_df) + recall = bigframes.ml.metrics.recall_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = sklearn_metrics.recall_score( + pd_df[["y_true"]], pd_df[["y_pred"]], average=None + ) + expected_index = ["ant", "bird", "cat"] + expected_recall = pd.Series(expected_values, index=expected_index) + pd.testing.assert_series_equal(recall, expected_recall, check_index_type=False) + + +def test_precision_score(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + precision_score = bigframes.ml.metrics.precision_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = [0.666667, 0.000000, 0.666667] + expected_index = [0, 1, 2] + expected_precision = pd.Series(expected_values, index=expected_index) + + pd.testing.assert_series_equal( + precision_score, expected_precision, check_index_type=False + ) + + +def test_precision_score_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + precision_score = bigframes.ml.metrics.precision_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = sklearn_metrics.precision_score( + pd_df[["y_true"]], pd_df[["y_pred"]], average=None + ) + expected_index = [0, 1, 2] + expected_precision = pd.Series(expected_values, index=expected_index) + pd.testing.assert_series_equal( + precision_score, expected_precision, check_index_type=False + ) + + +def test_precision_score_str_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": ["cat", "ant", "cat", "cat", "ant", "bird"], + "y_pred": ["ant", "ant", "cat", "cat", "ant", "cat"], + } + ).astype("str") + df = session.read_pandas(pd_df) + precision_score = bigframes.ml.metrics.precision_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = sklearn_metrics.precision_score( + pd_df[["y_true"]], pd_df[["y_pred"]], average=None + ) + expected_index = ["ant", "bird", "cat"] + expected_precision = pd.Series(expected_values, index=expected_index) + pd.testing.assert_series_equal( + precision_score, expected_precision, check_index_type=False + ) + + +def test_f1_score(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + f1_score = bigframes.ml.metrics.f1_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = [0.8, 0.000000, 0.666667] + expected_index = [0, 1, 2] + expected_f1 = pd.Series(expected_values, index=expected_index) + + pd.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + + +def test_f1_score_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + f1_score = bigframes.ml.metrics.f1_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = sklearn_metrics.f1_score( + pd_df[["y_true"]], pd_df[["y_pred"]], average=None + ) + expected_index = [0, 1, 2] + expected_f1 = pd.Series(expected_values, index=expected_index) + pd.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + + +def test_f1_score_str_matches_sklearn(session): + pd_df = pd.DataFrame( + { + "y_true": ["cat", "ant", "cat", "cat", "ant", "bird"], + "y_pred": ["ant", "ant", "cat", "cat", "ant", "cat"], + } + ).astype("str") + df = session.read_pandas(pd_df) + f1_score = bigframes.ml.metrics.f1_score( + df[["y_true"]], df[["y_pred"]], average=None + ) + expected_values = sklearn_metrics.f1_score( + pd_df[["y_true"]], pd_df[["y_pred"]], average=None + ) + expected_index = ["ant", "bird", "cat"] + expected_f1 = pd.Series(expected_values, index=expected_index) + pd.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) diff --git a/tests/system/small/ml/test_model_selection.py b/tests/system/small/ml/test_model_selection.py new file mode 100644 index 00000000000..add455ab9ac --- /dev/null +++ b/tests/system/small/ml/test_model_selection.py @@ -0,0 +1,214 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +import bigframes.ml.model_selection + + +def test_train_test_split_default_correct_shape(penguins_df_default_index): + X = penguins_df_default_index[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = penguins_df_default_index[["body_mass_g"]] + X_train, X_test, y_train, y_test = bigframes.ml.model_selection.train_test_split( + X, y + ) + + # even though the default seed is random, it should always result in this shape + assert X_train.shape == (258, 3) + assert X_test.shape == (86, 3) + assert y_train.shape == (258, 1) + assert y_test.shape == (86, 1) + + +def test_train_test_double_split_correct_shape(penguins_df_default_index): + X = penguins_df_default_index[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = penguins_df_default_index[["body_mass_g"]] + X_train, X_test, y_train, y_test = bigframes.ml.model_selection.train_test_split( + X, y, test_size=0.2, train_size=0.4 + ) + + # should have 20% in test, 40% in train, 40% dropped + assert X_train.shape == (138, 3) + assert X_test.shape == (69, 3) + assert y_train.shape == (138, 1) + assert y_test.shape == (69, 1) + + +def test_train_test_three_dataframes_correct_shape(penguins_df_default_index): + A = penguins_df_default_index[ + [ + "species", + "culmen_length_mm", + ] + ] + B = penguins_df_default_index[ + [ + "island", + ] + ] + C = penguins_df_default_index[["culmen_depth_mm", "body_mass_g"]] + ( + A_train, + A_test, + B_train, + B_test, + C_train, + C_test, + ) = bigframes.ml.model_selection.train_test_split(A, B, C) + + assert A_train.shape == (258, 2) + assert A_test.shape == (86, 2) + assert B_train.shape == (258, 1) + assert B_test.shape == (86, 1) + assert C_train.shape == (258, 2) + assert C_test.shape == (86, 2) + + +def test_train_test_split_seeded_correct_rows( + session, penguins_pandas_df_default_index +): + # Note that we're using `penguins_pandas_df_default_index` as this test depends + # on a stable row order being present end to end + # filter down to the chunkiest penguins, to keep our test code a reasonable size + all_data = penguins_pandas_df_default_index[ + penguins_pandas_df_default_index.body_mass_g > 5500 + ] + + # Note that bigframes loses the index if it doesn't have a name + all_data.index.name = "rowindex" + + df = session.read_pandas(all_data) + + X = df[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = df[["body_mass_g"]] + X_train, X_test, y_train, y_test = bigframes.ml.model_selection.train_test_split( + X, y, random_state=42 + ) + + X_train = X_train.to_pandas().sort_index() + X_test = X_test.to_pandas().sort_index() + y_train = y_train.to_pandas().sort_index() + y_test = y_test.to_pandas().sort_index() + + train_index = pd.Index( + [ + 144, + 146, + 148, + 168, + 183, + 186, + 217, + 225, + 226, + 237, + 244, + 245, + 260, + 262, + 263, + 266, + 268, + 269, + 289, + 290, + 291, + ], + dtype="Int64", + name="rowindex", + ) + test_index = pd.Index( + [161, 221, 240, 257, 264, 267, 278], dtype="Int64", name="rowindex" + ) + + all_data.index.name = "_" + pd.testing.assert_frame_equal( + X_train, + all_data[ + [ + "species", + "island", + "culmen_length_mm", + ] + ].loc[train_index], + ) + pd.testing.assert_frame_equal( + X_test, + all_data[ + [ + "species", + "island", + "culmen_length_mm", + ] + ].loc[test_index], + ) + pd.testing.assert_frame_equal( + y_train, + all_data[ + [ + "body_mass_g", + ] + ].loc[train_index], + ) + pd.testing.assert_frame_equal( + y_test, + all_data[ + [ + "body_mass_g", + ] + ].loc[test_index], + ) + + +@pytest.mark.parametrize( + ("train_size", "test_size"), + [ + (0.0, 0.5), + (-0.5, 0.7), + (0.5, 1.2), + (0.6, 0.6), + ], +) +def test_train_test_split_value_error(penguins_df_default_index, train_size, test_size): + X = penguins_df_default_index[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = penguins_df_default_index[["body_mass_g"]] + with pytest.raises(ValueError): + bigframes.ml.model_selection.train_test_split( + X, y, train_size=train_size, test_size=test_size + ) diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py new file mode 100644 index 00000000000..f31b93b4cc2 --- /dev/null +++ b/tests/system/small/ml/test_preprocessing.py @@ -0,0 +1,89 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +import pandas as pd + +import bigframes.ml.preprocessing + + +def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): + # TODO(bmil): add a second test that compares output to sklearn.preprocessing.StandardScaler + scaler = bigframes.ml.preprocessing.StandardScaler() + scaler.fit( + penguins_df_default_index[ + "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm" + ] + ) + + result = scaler.transform( + penguins_df_default_index[ + "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm" + ] + ).to_pandas() + + # If standard-scaled correctly, mean should be 0.0 and standard deviation 1.0 + for column in result.columns: + assert math.isclose(result[column].mean(), 0.0, abs_tol=1e-3) + assert math.isclose(result[column].std(), 1.0, abs_tol=1e-3) + + result = scaler.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "scaled_culmen_depth_mm": [0.8349, 0.02473, 0.4805], + "scaled_culmen_length_mm": [-0.8099, -0.9931, -1.103], + "scaled_flipper_length_mm": [-0.3495, -1.4163, -0.9185], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_one_hot_encoder_encodes(penguins_df_default_index, new_penguins_df): + encoder = bigframes.ml.preprocessing.OneHotEncoder() + encoder.fit(penguins_df_default_index["species", "sex"]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "onehotencoded_sex": [ + [{"index": 2, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + ], + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) diff --git a/tests/system/small/ml/test_register.py b/tests/system/small/ml/test_register.py new file mode 100644 index 00000000000..ad378d35084 --- /dev/null +++ b/tests/system/small/ml/test_register.py @@ -0,0 +1,87 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import cast + +from bigframes.ml import core, imported, linear_model, llm + + +def test_linear_reg_register( + ephemera_penguins_linear_model: linear_model.LinearRegression, +): + model = ephemera_penguins_linear_model + model.register() + + model_name = "bigframes_" + cast( + str, cast(core.BqmlModel, model._bqml_model).model.model_id + ) + # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. + assert ( + model_name + in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ + "vertexAiModelId" + ] + ) + + +def test_linear_reg_register_with_params( + ephemera_penguins_linear_model: linear_model.LinearRegression, +): + model_name = "bigframes_system_test_linear_reg_model" + model = ephemera_penguins_linear_model + model.register(model_name) + + # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. + assert ( + model_name + in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ + "vertexAiModelId" + ] + ) + + +def test_palm2_text_generator_register( + ephemera_palm2_text_generator_model: llm.PaLM2TextGenerator, +): + model = ephemera_palm2_text_generator_model + model.register() + + model_name = "bigframes_" + cast( + str, cast(core.BqmlModel, model._bqml_model).model.model_id + ) + # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. + assert ( + model_name + in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ + "vertexAiModelId" + ] + ) + + +def test_imported_tensorflow_register( + ephemera_imported_tensorflow_model: imported.TensorFlowModel, +): + model = ephemera_imported_tensorflow_model + model.register() + + model_name = "bigframes_" + cast( + str, cast(core.BqmlModel, model._bqml_model).model.model_id + ) + # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. + assert ( + model_name + in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ + "vertexAiModelId" + ] + ) diff --git a/tests/system/small/operations/__init__.py b/tests/system/small/operations/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/system/small/operations/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py new file mode 100644 index 00000000000..25d3eb69adb --- /dev/null +++ b/tests/system/small/operations/test_datetimes.py @@ -0,0 +1,198 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +import bigframes.series +from tests.system.utils import assert_series_equal_ignoring_order + +DATETIME_COL_NAMES = [("datetime_col",), ("timestamp_col",)] + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_day(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.day.compute() + pd_result = scalars_pandas_df[col_name].dt.day + + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_date(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.date.compute() + pd_result = scalars_pandas_df[col_name].dt.date + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_dayofweek(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.dayofweek.compute() + pd_result = scalars_pandas_df[col_name].dt.dayofweek + + assert_series_equal_ignoring_order(pd_result, bf_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_hour(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.hour.compute() + pd_result = scalars_pandas_df[col_name].dt.hour + + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_minute(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.minute.compute() + pd_result = scalars_pandas_df[col_name].dt.minute + + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_month(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.month.compute() + pd_result = scalars_pandas_df[col_name].dt.month + + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_quarter(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.quarter.compute() + pd_result = scalars_pandas_df[col_name].dt.quarter + + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_second(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.second.compute() + pd_result = scalars_pandas_df[col_name].dt.second + + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_time(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.time.compute() + pd_result = scalars_pandas_df[col_name].dt.time + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +def test_year(scalars_dfs, col_name): + if pd.__version__.startswith("1."): + pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.year.compute() + pd_result = scalars_pandas_df[col_name].dt.year + + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py new file mode 100644 index 00000000000..76736e3cddd --- /dev/null +++ b/tests/system/small/operations/test_strings.py @@ -0,0 +1,230 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +import bigframes.series + +from ...utils import assert_series_equal_ignoring_order + + +def test_find(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.find("W").compute() + pd_result = scalars_pandas_df[col_name].str.find("W") + + # One of type mismatches to be documented. Here, the `bf_result.dtype` is `Int64` but + # the `pd_result.dtype` is `float64`: https://github.com/pandas-dev/pandas/issues/51948 + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +def test_len(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.len().compute() + pd_result = scalars_pandas_df[col_name].str.len() + + # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is `Int64` but + # the `pd_result.dtype` is `float64`: https://github.com/pandas-dev/pandas/issues/51948 + assert_series_equal_ignoring_order( + pd_result.astype(pd.Int64Dtype()), + bf_result, + ) + + +def test_lower(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.lower().compute() + pd_result = scalars_pandas_df[col_name].str.lower() + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_reverse(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.reverse().compute() + pd_result = scalars_pandas_df[col_name].copy() + for i in pd_result.index: + cell = pd_result.loc[i] + if pd.isna(cell): + pd_result.loc[i] = None + else: + pd_result.loc[i] = cell[::-1] + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ["start", "stop"], [(0, 1), (3, 5), (100, 101), (None, 1), (0, 12), (0, None)] +) +def test_slice(scalars_dfs, start, stop): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.slice(start, stop).compute() + pd_series = scalars_pandas_df[col_name] + pd_result = pd_series.str.slice(start, stop) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_strip(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.strip().compute() + pd_result = scalars_pandas_df[col_name].str.strip() + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_upper(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.upper().compute() + pd_result = scalars_pandas_df[col_name].str.upper() + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_isnumeric(session): + pandas_df = pd.DataFrame( + { + "numeric_string_col": [ + "٠١٢٣٤٥٦٧٨٩", + "", + "0", + "字", + "五", + "0123456789", + pd.NA, + "abc 123 mixed letters and numbers", + "no numbers here", + "123a", + "23!", + " 45", + "a45", + ] + } + ) + + df = session.read_pandas(pandas_df) + + pd_result = pandas_df.numeric_string_col.str.isnumeric() + bf_result = df.numeric_string_col.str.isnumeric().compute() + + assert_series_equal_ignoring_order( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_rstrip(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.rstrip().compute() + pd_result = scalars_pandas_df[col_name].str.rstrip() + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_lstrip(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.lstrip().compute() + pd_result = scalars_pandas_df[col_name].str.lstrip() + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize(["repeats"], [(5,), (0,), (1,)]) +def test_repeat(scalars_dfs, repeats): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.repeat(repeats).compute() + pd_result = scalars_pandas_df[col_name].str.repeat(repeats) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_capitalize(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.capitalize().compute() + pd_result = scalars_pandas_df[col_name].str.capitalize() + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_cat_with_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_filter: bigframes.series.Series = scalars_df["bool_col"] + bf_left: bigframes.series.Series = scalars_df[col_name][bf_filter] + bf_right: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_left.str.cat(others=bf_right).compute() + pd_filter = scalars_pandas_df["bool_col"] + pd_left = scalars_pandas_df[col_name][pd_filter] + pd_right = scalars_pandas_df[col_name] + pd_result = pd_left.str.cat(others=pd_right) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py new file mode 100644 index 00000000000..1833c674fdd --- /dev/null +++ b/tests/system/small/test_dataframe.py @@ -0,0 +1,1694 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import operator + +import geopandas as gpd # type: ignore +import numpy as np +import pandas as pd +import pandas.testing +import pyarrow as pa # type: ignore +import pytest + +import bigframes +import bigframes._config.display_options as display_options +import bigframes.dataframe as dataframe +from tests.system.utils import ( + assert_pandas_df_equal_ignore_ordering, + assert_series_equal_ignoring_order, +) + + +def test_df_construct_copy(scalars_dfs): + columns = ["int64_col", "string_col", "float64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = dataframe.DataFrame(scalars_df, columns=columns).compute() + pd_result = pd.DataFrame(scalars_pandas_df, columns=columns) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_construct_pandas(scalars_dfs): + columns = ["int64_too", "int64_col", "float64_col", "bool_col", "string_col"] + _, scalars_pandas_df = scalars_dfs + bf_result = dataframe.DataFrame(scalars_pandas_df, columns=columns).compute() + pd_result = pd.DataFrame(scalars_pandas_df, columns=columns) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_construct_pandas_set_dtype(scalars_dfs): + columns = [ + "int64_too", + "int64_col", + "float64_col", + "bool_col", + ] + _, scalars_pandas_df = scalars_dfs + bf_result = dataframe.DataFrame( + scalars_pandas_df, columns=columns, dtype="Float64" + ).compute() + pd_result = pd.DataFrame(scalars_pandas_df, columns=columns, dtype="Float64") + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_construct_from_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = dataframe.DataFrame( + {"a": scalars_df["int64_col"], "b": scalars_df["string_col"]}, + dtype="string[pyarrow]", + ).compute() + pd_result = pd.DataFrame( + {"a": scalars_pandas_df["int64_col"], "b": scalars_pandas_df["string_col"]}, + dtype="string[pyarrow]", + ) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_get_column(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + series = scalars_df[col_name] + bf_result = series.compute() + pd_result = scalars_pandas_df[col_name] + assert_series_equal_ignoring_order(bf_result, pd_result) + + +def test_hasattr(scalars_dfs): + scalars_df, _ = scalars_dfs + assert hasattr(scalars_df, "int64_col") + assert hasattr(scalars_df, "head") + assert not hasattr(scalars_df, "not_exist") + + +def test_head_with_custom_column_labels(scalars_df_index, scalars_pandas_df_index): + rename_mapping = { + "int64_col": "Integer Column", + "string_col": "言語列", + } + bf_df = scalars_df_index.rename(columns=rename_mapping).head(3) + bf_result = bf_df.compute() + pd_result = scalars_pandas_df_index.rename(columns=rename_mapping).head(3) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_tail_with_custom_column_labels(scalars_df_index, scalars_pandas_df_index): + rename_mapping = { + "int64_col": "Integer Column", + "string_col": "言語列", + } + bf_df = scalars_df_index.rename(columns=rename_mapping).tail(3) + bf_result = bf_df.compute() + pd_result = scalars_pandas_df_index.rename(columns=rename_mapping).tail(3) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_get_column_by_attr(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + series = scalars_df.int64_col + bf_result = series.compute() + pd_result = scalars_pandas_df.int64_col + assert_series_equal_ignoring_order(bf_result, pd_result) + + +def test_get_columns(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_names = ["bool_col", "float64_col", "int64_col"] + df_subset = scalars_df.get(col_names) + df_pandas = df_subset.compute() + pd.testing.assert_index_equal( + df_pandas.columns, scalars_pandas_df[col_names].columns + ) + + +def test_get_columns_default(scalars_dfs): + scalars_df, _ = scalars_dfs + col_names = ["not", "column", "names"] + result = scalars_df.get(col_names, "default_val") + assert result == "default_val" + + +def test_drop_column(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + df_pandas = scalars_df.drop(columns=col_name).compute() + pd.testing.assert_index_equal( + df_pandas.columns, scalars_pandas_df.drop(columns=col_name).columns + ) + + +def test_drop_columns(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_names = ["int64_col", "geography_col", "time_col"] + df_pandas = scalars_df.drop(columns=col_names).compute() + pd.testing.assert_index_equal( + df_pandas.columns, scalars_pandas_df.drop(columns=col_names).columns + ) + + +def test_drop_with_custom_column_labels(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + rename_mapping = { + "int64_col": "Integer Column", + "string_col": "言語列", + } + dropped_columns = [ + "言語列", + "timestamp_col", + ] + bf_df = scalars_df.rename(columns=rename_mapping).drop(columns=dropped_columns) + bf_result = bf_df.compute() + pd_result = scalars_pandas_df.rename(columns=rename_mapping).drop( + columns=dropped_columns + ) + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_rename(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name_dict = {"bool_col": "boolean_col"} + df_pandas = scalars_df.rename(columns=col_name_dict).compute() + pd.testing.assert_index_equal( + df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns + ) + + +def test_repr_w_all_rows(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + # Remove columns with flaky formatting, like NUMERIC columns (which use the + # object dtype). Also makes a copy so that mutating the index name doesn't + # break other tests. + scalars_df = scalars_df.drop(columns=["numeric_col"]) + scalars_pandas_df = scalars_pandas_df.drop(columns=["numeric_col"]) + + if scalars_pandas_df.index.name is None: + # Note: Not quite the same as no index / default index, but hopefully + # simulates it well enough while being consistent enough for string + # comparison to work. + scalars_df = scalars_df.set_index("rowindex", drop=False).sort_index() + scalars_df.index.name = None + + # When there are 10 or fewer rows, the outputs should be identical. + actual = repr(scalars_df.head(10)) + + with display_options.pandas_repr(bigframes.options.display): + expected = repr(scalars_pandas_df.head(10)) + + assert actual == expected + + +def test_repr_html_w_all_rows(scalars_dfs): + scalars_df, _ = scalars_dfs + # get a pandas df of the expected format + df, _ = scalars_df._block.compute() + pandas_df = df.set_axis(scalars_df._block.column_labels, axis=1) + pandas_df.index.name = scalars_df.index.name + + # When there are 10 or fewer rows, the outputs should be identical except for the extra note. + actual = scalars_df.head(10)._repr_html_() + with display_options.pandas_repr(bigframes.options.display): + pandas_repr = pandas_df.head(10)._repr_html_() + + expected = ( + pandas_repr + + f"[{len(pandas_df.index)} rows x {len(pandas_df.columns)} columns in total]" + ) + assert actual == expected + + +def test_df_column_name_with_space(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name_dict = {"bool_col": "bool col"} + df_pandas = scalars_df.rename(columns=col_name_dict).compute() + pd.testing.assert_index_equal( + df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns + ) + + +def test_df_column_name_duplicate(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name_dict = {"int64_too": "int64_col"} + df_pandas = scalars_df.rename(columns=col_name_dict).compute() + pd.testing.assert_index_equal( + df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns + ) + + +def test_get_df_column_name_duplicate(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name_dict = {"int64_too": "int64_col"} + + bf_result = scalars_df.rename(columns=col_name_dict)["int64_col"].compute() + pd_result = scalars_pandas_df.rename(columns=col_name_dict)["int64_col"] + pd.testing.assert_index_equal(bf_result.columns, pd_result.columns) + + +def test_filter_df(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_bool_series = scalars_df["bool_col"] + bf_result = scalars_df[bf_bool_series].compute() + + pd_bool_series = scalars_pandas_df["bool_col"] + pd_result = scalars_pandas_df[pd_bool_series] + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_assign_new_column(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + kwargs = {"new_col": 2} + df = scalars_df.assign(**kwargs) + bf_result = df.compute() + pd_result = scalars_pandas_df.assign(**kwargs) + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_assign_new_column_w_loc(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df.loc[:, "new_col"] = 2 + pd_df.loc[:, "new_col"] = 2 + bf_result = bf_df.compute() + pd_result = pd_df + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_assign_new_column_w_setitem(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df["new_col"] = 2 + pd_df["new_col"] = 2 + bf_result = bf_df.compute() + pd_result = pd_df + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_assign_existing_column(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + kwargs = {"int64_col": 2} + df = scalars_df.assign(**kwargs) + bf_result = df.compute() + pd_result = scalars_pandas_df.assign(**kwargs) + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["int64_col"] = pd_result["int64_col"].astype("Int64") + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_assign_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + column_name = "int64_col" + df = scalars_df.assign(new_col=scalars_df[column_name]) + bf_result = df.compute() + pd_result = scalars_pandas_df.assign(new_col=scalars_pandas_df[column_name]) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_assign_series_overwrite(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + column_name = "int64_col" + df = scalars_df.assign(**{column_name: scalars_df[column_name] + 3}) + bf_result = df.compute() + pd_result = scalars_pandas_df.assign( + **{column_name: scalars_pandas_df[column_name] + 3} + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_assign_sequential(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + kwargs = {"int64_col": 2, "new_col": 3, "new_col2": 4} + df = scalars_df.assign(**kwargs) + bf_result = df.compute() + pd_result = scalars_pandas_df.assign(**kwargs) + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["int64_col"] = pd_result["int64_col"].astype("Int64") + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + pd_result["new_col2"] = pd_result["new_col2"].astype("Int64") + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +# Require an index so that the self-join is consistent each time. +def test_assign_same_table_different_index_performs_self_join( + scalars_df_index, scalars_pandas_df_index +): + column_name = "int64_col" + bf_df = scalars_df_index.assign( + alternative_index=scalars_df_index["rowindex_2"] + 2 + ) + pd_df = scalars_pandas_df_index.assign( + alternative_index=scalars_pandas_df_index["rowindex_2"] + 2 + ) + bf_df_2 = bf_df.set_index("alternative_index") + pd_df_2 = pd_df.set_index("alternative_index") + bf_result = bf_df.assign(new_col=bf_df_2[column_name] * 10).compute() + pd_result = pd_df.assign(new_col=pd_df_2[column_name] * 10) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +# Different table expression must have Index +def test_assign_different_df( + scalars_df_index, scalars_df_2_index, scalars_pandas_df_index +): + column_name = "int64_col" + df = scalars_df_index.assign(new_col=scalars_df_2_index[column_name]) + bf_result = df.compute() + # Doesn't matter to pandas if it comes from the same DF or a different DF. + pd_result = scalars_pandas_df_index.assign( + new_col=scalars_pandas_df_index[column_name] + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_assign_different_df_w_loc( + scalars_df_index, scalars_df_2_index, scalars_pandas_df_index +): + bf_df = scalars_df_index.copy() + bf_df2 = scalars_df_2_index.copy() + pd_df = scalars_pandas_df_index.copy() + assert "int64_col" in bf_df.columns + assert "int64_col" in pd_df.columns + bf_df.loc[:, "int64_col"] = bf_df2.loc[:, "int64_col"] + 1 + pd_df.loc[:, "int64_col"] = pd_df.loc[:, "int64_col"] + 1 + bf_result = bf_df.compute() + pd_result = pd_df + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["int64_col"] = pd_result["int64_col"].astype("Int64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_assign_different_df_w_setitem( + scalars_df_index, scalars_df_2_index, scalars_pandas_df_index +): + bf_df = scalars_df_index.copy() + bf_df2 = scalars_df_2_index.copy() + pd_df = scalars_pandas_df_index.copy() + assert "int64_col" in bf_df.columns + assert "int64_col" in pd_df.columns + bf_df["int64_col"] = bf_df2["int64_col"] + 1 + pd_df["int64_col"] = pd_df["int64_col"] + 1 + bf_result = bf_df.compute() + pd_result = pd_df + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["int64_col"] = pd_result["int64_col"].astype("Int64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_assign_callable_lambda(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + kwargs = {"new_col": lambda x: x["int64_col"] + x["int64_too"]} + df = scalars_df.assign(**kwargs) + bf_result = df.compute() + pd_result = scalars_pandas_df.assign(**kwargs) + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_dropna(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + df = scalars_df.dropna() + bf_result = df.compute() + pd_result = scalars_pandas_df.dropna() + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("merge_how",), + [ + ("inner",), + ("outer",), + ("left",), + ("right",), + ], +) +def test_merge(scalars_dfs, merge_how): + scalars_df, scalars_pandas_df = scalars_dfs + on = "rowindex_2" + left_columns = ["int64_col", "float64_col", "rowindex_2"] + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_columns] + # Offset the rows somewhat so that outer join can have an effect. + right = scalars_df[right_columns].assign(rowindex_2=scalars_df["rowindex_2"] + 2) + + df = left.merge(right, merge_how, on, sort=True) + bf_result = df.compute() + + pd_result = scalars_pandas_df[left_columns].merge( + scalars_pandas_df[right_columns].assign( + rowindex_2=scalars_pandas_df["rowindex_2"] + 2 + ), + merge_how, + on, + sort=True, + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("merge_how",), + [ + ("inner",), + ("outer",), + ("left",), + ("right",), + ], +) +def test_merge_custom_col_name(scalars_dfs, merge_how): + scalars_df, scalars_pandas_df = scalars_dfs + left_columns = ["int64_col", "float64_col"] + right_columns = ["int64_col", "bool_col", "string_col"] + on = "int64_col" + rename_columns = {"float64_col": "f64_col"} + + left = scalars_df[left_columns] + left = left.rename(columns=rename_columns) + right = scalars_df[right_columns] + df = left.merge(right, merge_how, on, sort=True) + bf_result = df.compute() + + pandas_left_df = scalars_pandas_df[left_columns] + pandas_left_df = pandas_left_df.rename(columns=rename_columns) + pandas_right_df = scalars_pandas_df[right_columns] + pd_result = pandas_left_df.merge(pandas_right_df, merge_how, on, sort=True) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("merge_how",), + [ + ("inner",), + ("outer",), + ("left",), + ("right",), + ], +) +def test_merge_left_on_right_on(scalars_dfs, merge_how): + scalars_df, scalars_pandas_df = scalars_dfs + left_columns = ["int64_col", "float64_col", "int64_too"] + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_columns] + right = scalars_df[right_columns] + + df = left.merge( + right, merge_how, left_on="int64_too", right_on="rowindex_2", sort=True + ) + bf_result = df.compute() + + pd_result = scalars_pandas_df[left_columns].merge( + scalars_pandas_df[right_columns], + merge_how, + left_on="int64_too", + right_on="rowindex_2", + sort=True, + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_get_dtypes(scalars_df_default_index): + dtypes = scalars_df_default_index.dtypes + pd.testing.assert_series_equal( + dtypes, + pd.Series( + { + "bool_col": pd.BooleanDtype(), + "bytes_col": np.dtype("O"), + "date_col": pd.ArrowDtype(pa.date32()), + "datetime_col": pd.ArrowDtype(pa.timestamp("us")), + "geography_col": gpd.array.GeometryDtype(), + "int64_col": pd.Int64Dtype(), + "int64_too": pd.Int64Dtype(), + "numeric_col": np.dtype("O"), + "float64_col": pd.Float64Dtype(), + "rowindex": pd.Int64Dtype(), + "rowindex_2": pd.Int64Dtype(), + "string_col": pd.StringDtype(storage="pyarrow"), + "time_col": pd.ArrowDtype(pa.time64("us")), + "timestamp_col": pd.ArrowDtype(pa.timestamp("us", tz="UTC")), + } + ), + ) + + +def test_get_dtypes_array_struct(session): + """We may upgrade struct and array to proper arrow dtype support in future. For now, + we return python objects""" + df = session.read_gbq( + """SELECT + [1, 3, 2] AS array_column, + STRUCT( + "a" AS string_field, + 1.2 AS float_field) AS struct_column""" + ) + + dtypes = df.dtypes + pd.testing.assert_series_equal( + dtypes, + pd.Series({"array_column": np.dtype("O"), "struct_column": np.dtype("O")}), + ) + + +def test_shape(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df.shape + pd_result = scalars_pandas_df.shape + + assert bf_result == pd_result + + +def test_len(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = len(scalars_df) + pd_result = len(scalars_pandas_df) + + assert bf_result == pd_result + + +def test_size(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df.size + pd_result = scalars_pandas_df.size + + assert bf_result == pd_result + + +def test_ndim(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df.ndim + pd_result = scalars_pandas_df.ndim + + assert bf_result == pd_result + + +def test_empty_false(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.empty + pd_result = scalars_pandas_df.empty + + assert bf_result == pd_result + + +def test_empty_true(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df[[]].empty + pd_result = scalars_pandas_df[[]].empty + + assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("drop",), + ((True,), (False,)), +) +def test_reset_index(scalars_df_index, scalars_pandas_df_index, drop): + df = scalars_df_index.reset_index(drop=drop) + assert df.index.name is None + + bf_result = df.compute() + pd_result = scalars_pandas_df_index.reset_index(drop=drop) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + + # reset_index should maintain the original ordering. + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_reset_index_then_filter( + scalars_df_index, + scalars_pandas_df_index, +): + bf_filter = scalars_df_index["bool_col"].fillna(True) + bf_df = scalars_df_index.reset_index()[bf_filter] + bf_result = bf_df.compute() + pd_filter = scalars_pandas_df_index["bool_col"].fillna(True) + pd_result = scalars_pandas_df_index.reset_index()[pd_filter] + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + + # reset_index should maintain the original ordering and index keys + # post-filter will have gaps. + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_reset_index_with_unnamed_index( + scalars_df_index, + scalars_pandas_df_index, +): + scalars_df_index = scalars_df_index.copy() + scalars_pandas_df_index = scalars_pandas_df_index.copy() + + scalars_df_index.index.name = None + scalars_pandas_df_index.index.name = None + df = scalars_df_index.reset_index(drop=False) + assert df.index.name is None + + # reset_index(drop=False) creates a new column "index". + assert df.columns[0] == "index" + + bf_result = df.compute() + pd_result = scalars_pandas_df_index.reset_index(drop=False) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + + # reset_index should maintain the original ordering. + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_reset_index_with_unnamed_index_and_index_column( + scalars_df_index, + scalars_pandas_df_index, +): + scalars_df_index = scalars_df_index.copy() + scalars_pandas_df_index = scalars_pandas_df_index.copy() + + scalars_df_index.index.name = None + scalars_pandas_df_index.index.name = None + df = scalars_df_index.assign(index=scalars_df_index["int64_col"]).reset_index( + drop=False + ) + assert df.index.name is None + + # reset_index(drop=False) creates a new column "level_0" if the "index" column already exists. + assert df.columns[0] == "level_0" + + bf_result = df.compute() + pd_result = scalars_pandas_df_index.assign( + index=scalars_pandas_df_index["int64_col"] + ).reset_index(drop=False) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + + # reset_index should maintain the original ordering. + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("drop",), + ( + (True,), + (False,), + ), +) +@pytest.mark.parametrize( + ("append",), + ( + (True,), + (False,), + ), +) +@pytest.mark.parametrize( + ("index_column",), + (("int64_too",), ("string_col",), ("timestamp_col",)), +) +def test_set_index(scalars_dfs, index_column, drop, append): + scalars_df, scalars_pandas_df = scalars_dfs + df = scalars_df.set_index(index_column, append=append, drop=drop) + bf_result = df.compute() + pd_result = scalars_pandas_df.set_index(index_column, append=append, drop=drop) + + # Sort to disambiguate when there are duplicate index labels. + # Note: Doesn't use assert_pandas_df_equal_ignore_ordering because we get + # "ValueError: 'timestamp_col' is both an index level and a column label, + # which is ambiguous" when trying to sort by a column with the same name as + # the index. + bf_result = bf_result.sort_values("rowindex_2") + pd_result = pd_result.sort_values("rowindex_2") + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_abs(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + columns = ["int64_col", "int64_too", "float64_col"] + + bf_result = scalars_df[columns].abs().compute() + pd_result = scalars_pandas_df[columns].abs() + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_df_isnull(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + columns = ["int64_col", "int64_too", "string_col", "bool_col"] + bf_result = scalars_df[columns].isnull().compute() + pd_result = scalars_pandas_df[columns].isnull() + + # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is + # `BooleanDtype` but the `pd_result.dtype` is `bool`. + pd_result["int64_col"] = pd_result["int64_col"].astype(pd.BooleanDtype()) + pd_result["int64_too"] = pd_result["int64_too"].astype(pd.BooleanDtype()) + pd_result["string_col"] = pd_result["string_col"].astype(pd.BooleanDtype()) + pd_result["bool_col"] = pd_result["bool_col"].astype(pd.BooleanDtype()) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_df_notnull(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + columns = ["int64_col", "int64_too", "string_col", "bool_col"] + bf_result = scalars_df[columns].notnull().compute() + pd_result = scalars_pandas_df[columns].notnull() + + # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is + # `BooleanDtype` but the `pd_result.dtype` is `bool`. + pd_result["int64_col"] = pd_result["int64_col"].astype(pd.BooleanDtype()) + pd_result["int64_too"] = pd_result["int64_too"].astype(pd.BooleanDtype()) + pd_result["string_col"] = pd_result["string_col"].astype(pd.BooleanDtype()) + pd_result["bool_col"] = pd_result["bool_col"].astype(pd.BooleanDtype()) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("op"), + [ + operator.add, + operator.sub, + operator.mul, + operator.truediv, + operator.floordiv, + operator.gt, + operator.ge, + operator.lt, + operator.le, + ], + ids=[ + "add", + "subtract", + "multiply", + "true_divide", + "floor_divide", + "gt", + "ge", + "lt", + "le", + ], +) +# TODO(garrettwu): deal with NA values +@pytest.mark.parametrize(("other_scalar"), [1, 2.5, 0, 0.0]) +@pytest.mark.parametrize(("reverse_operands"), [True, False]) +def test_scalar_binop(scalars_dfs, op, other_scalar, reverse_operands): + scalars_df, scalars_pandas_df = scalars_dfs + columns = ["int64_col", "float64_col"] + + maybe_reversed_op = (lambda x, y: op(y, x)) if reverse_operands else op + + bf_result = maybe_reversed_op(scalars_df[columns], other_scalar).compute() + pd_result = maybe_reversed_op(scalars_pandas_df[columns], other_scalar) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize(("other_scalar"), [1, -2]) +def test_mod(scalars_dfs, other_scalar): + # Zero case excluded as pandas produces 0 result for Int64 inputs rather than NA/NaN. + # This is likely a pandas bug as mod 0 is undefined in other dtypes, and most programming languages. + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = (scalars_df[["int64_col", "int64_too"]] % other_scalar).compute() + pd_result = scalars_pandas_df[["int64_col", "int64_too"]] % other_scalar + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_scalar_binop_str_exception(scalars_dfs): + scalars_df, _ = scalars_dfs + columns = ["string_col"] + with pytest.raises(TypeError): + (scalars_df[columns] + 1).compute() + + +@pytest.mark.parametrize( + ("op"), + [ + (lambda x, y: x.add(y, axis="index")), + (lambda x, y: x.radd(y, axis="index")), + (lambda x, y: x.sub(y, axis="index")), + (lambda x, y: x.rsub(y, axis="index")), + (lambda x, y: x.mul(y, axis="index")), + (lambda x, y: x.rmul(y, axis="index")), + (lambda x, y: x.truediv(y, axis="index")), + (lambda x, y: x.rtruediv(y, axis="index")), + (lambda x, y: x.floordiv(y, axis="index")), + (lambda x, y: x.floordiv(y, axis="index")), + (lambda x, y: x.gt(y, axis="index")), + (lambda x, y: x.ge(y, axis="index")), + (lambda x, y: x.lt(y, axis="index")), + (lambda x, y: x.le(y, axis="index")), + ], + ids=[ + "add", + "radd", + "sub", + "rsub", + "mul", + "rmul", + "truediv", + "rtruediv", + "floordiv", + "rfloordiv", + "gt", + "ge", + "lt", + "le", + ], +) +def test_series_binop_axis_index( + scalars_dfs, + op, +): + scalars_df, scalars_pandas_df = scalars_dfs + df_columns = ["int64_col", "float64_col"] + series_column = "int64_too" + + bf_result = op(scalars_df[df_columns], scalars_df[series_column]).compute() + pd_result = op(scalars_pandas_df[df_columns], scalars_pandas_df[series_column]) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("op"), + [ + (lambda x, y: x.add(y, axis="index")), + (lambda x, y: x.radd(y, axis="index")), + (lambda x, y: x.sub(y, axis="index")), + (lambda x, y: x.rsub(y, axis="index")), + (lambda x, y: x.mul(y, axis="index")), + (lambda x, y: x.rmul(y, axis="index")), + (lambda x, y: x.truediv(y, axis="index")), + (lambda x, y: x.rtruediv(y, axis="index")), + (lambda x, y: x.floordiv(y, axis="index")), + (lambda x, y: x.floordiv(y, axis="index")), + (lambda x, y: x.gt(y, axis="index")), + (lambda x, y: x.ge(y, axis="index")), + (lambda x, y: x.lt(y, axis="index")), + (lambda x, y: x.le(y, axis="index")), + ], + ids=[ + "add", + "radd", + "sub", + "rsub", + "mul", + "rmul", + "truediv", + "rtruediv", + "floordiv", + "rfloordiv", + "gt", + "ge", + "lt", + "le", + ], +) +def test_dataframe_binop_axis_index_throws_not_implemented( + scalars_dfs, + op, +): + scalars_df, scalars_pandas_df = scalars_dfs + df_columns = ["int64_col", "float64_col"] + other_df_columns = ["int64_too"] + + with pytest.raises(NotImplementedError): + op(scalars_df[df_columns], scalars_df[other_df_columns]).compute() + + +# Differnt table will only work for explicit index, since default index orders are arbitrary. +def test_series_binop_add_different_table( + scalars_df_index, scalars_pandas_df_index, scalars_df_2_index +): + df_columns = ["int64_col", "float64_col"] + series_column = "int64_too" + + bf_result = ( + scalars_df_index[df_columns] + .add(scalars_df_2_index[series_column], axis="index") + .compute() + ) + pd_result = scalars_pandas_df_index[df_columns].add( + scalars_pandas_df_index[series_column], axis="index" + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +# TODO(garrettwu): Test series binop with different index + +all_joins = pytest.mark.parametrize( + ("how",), + ( + ("outer",), + ("left",), + ("right",), + ("inner",), + ), +) + + +@all_joins +def test_join_same_table(scalars_dfs, how): + bf_df, pd_df = scalars_dfs + if how == "right" and pd_df.index.name != "rowindex": + pytest.skip("right join not supported without an index") + + bf_df_a = bf_df[["string_col", "int64_col"]] + bf_df_b = bf_df[["float64_col"]] + bf_result = bf_df_a.join(bf_df_b, how=how).compute() + pd_df_a = pd_df[["string_col", "int64_col"]] + pd_df_b = pd_df[["float64_col"]] + pd_result = pd_df_a.join(pd_df_b, how=how) + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@all_joins +def test_join_different_table( + scalars_df_index, scalars_df_2_index, scalars_pandas_df_index, how +): + bf_df_a = scalars_df_index[["string_col", "int64_col"]] + bf_df_b = scalars_df_2_index.dropna()[["float64_col"]] + bf_result = bf_df_a.join(bf_df_b, how=how).compute() + pd_df_a = scalars_pandas_df_index[["string_col", "int64_col"]] + pd_df_b = scalars_pandas_df_index.dropna()[["float64_col"]] + pd_result = pd_df_a.join(pd_df_b, how=how) + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_join_duplicate_columns_raises_not_implemented(scalars_dfs): + scalars_df, _ = scalars_dfs + df_a = scalars_df[["string_col", "float64_col"]] + df_b = scalars_df[["float64_col"]] + with pytest.raises(NotImplementedError): + df_a.join(df_b, how="outer").compute() + + +@pytest.mark.parametrize( + ("by", "ascending", "na_position"), + [ + ("int64_col", True, "first"), + (["bool_col", "int64_col"], True, "last"), + ("int64_col", False, "first"), + (["bool_col", "int64_col"], [False, True], "last"), + (["bool_col", "int64_col"], [True, False], "first"), + ], +) +def test_dataframe_sort_values( + scalars_df_index, scalars_pandas_df_index, by, ascending, na_position +): + # Test needs values to be unique + bf_result = scalars_df_index.sort_values( + by, ascending=ascending, na_position=na_position + ).compute() + pd_result = scalars_pandas_df_index.sort_values( + by, ascending=ascending, na_position=na_position + ) + + pandas.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("operator", "columns"), + [ + pytest.param(lambda x: x.cumsum(), ["float64_col", "int64_too"]), + pytest.param(lambda x: x.cumprod(), ["float64_col", "int64_too"]), + pytest.param( + lambda x: x.cumprod(), + ["string_col"], + marks=pytest.mark.xfail( + raises=ValueError, + ), + ), + ], + ids=[ + "cumsum", + "cumprod", + "non-numeric", + ], +) +def test_dataframe_numeric_analytic_op( + scalars_df_index, scalars_pandas_df_index, operator, columns +): + # TODO: Add nullable ints (pandas 1.x has poor behavior on these) + bf_series = operator(scalars_df_index[columns]) + pd_series = operator(scalars_pandas_df_index[columns]) + bf_result = bf_series.compute() + pd.testing.assert_frame_equal(pd_series, bf_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x: x.cummin()), + (lambda x: x.cummax()), + (lambda x: x.shift(2)), + (lambda x: x.shift(-2)), + ], + ids=[ + "cummin", + "cummax", + "shiftpostive", + "shiftnegative", + ], +) +def test_dataframe_general_analytic_op( + scalars_df_index, scalars_pandas_df_index, operator +): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col"] + bf_series = operator(scalars_df_index[col_names]) + pd_series = operator(scalars_pandas_df_index[col_names]) + bf_result = bf_series.compute() + pd.testing.assert_frame_equal( + pd_series, + bf_result, + ) + + +def test_ipython_key_completions_with_drop(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_names = "string_col" + bf_dataframe = scalars_df.drop(columns=col_names) + pd_dataframe = scalars_pandas_df.drop(columns=col_names) + expected = pd_dataframe.columns.tolist() + + results = bf_dataframe._ipython_key_completions_() + + assert col_names not in results + assert results == expected + # _ipython_key_completions_ is called with square brackets + # so only column names are relevant with tab completion + assert "to_gbq" not in results + assert "merge" not in results + assert "drop" not in results + + +def test_ipython_key_completions_with_rename(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name_dict = {"string_col": "a_renamed_column"} + bf_dataframe = scalars_df.rename(columns=col_name_dict) + pd_dataframe = scalars_pandas_df.rename(columns=col_name_dict) + expected = pd_dataframe.columns.tolist() + + results = bf_dataframe._ipython_key_completions_() + + assert "string_col" not in results + assert "a_renamed_column" in results + assert results == expected + # _ipython_key_completions_ is called with square brackets + # so only column names are relevant with tab completion + assert "to_gbq" not in results + assert "merge" not in results + assert "drop" not in results + + +def test__dir__with_drop(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_names = "string_col" + bf_dataframe = scalars_df.drop(columns=col_names) + pd_dataframe = scalars_pandas_df.drop(columns=col_names) + expected = pd_dataframe.columns.tolist() + + results = dir(bf_dataframe) + + assert col_names not in results + assert frozenset(expected) <= frozenset(results) + # __dir__ is called with a '.' and displays all methods, columns names, etc. + assert "to_gbq" in results + assert "merge" in results + assert "drop" in results + + +def test__dir__with_rename(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name_dict = {"string_col": "a_renamed_column"} + bf_dataframe = scalars_df.rename(columns=col_name_dict) + pd_dataframe = scalars_pandas_df.rename(columns=col_name_dict) + expected = pd_dataframe.columns.tolist() + + results = dir(bf_dataframe) + + assert "string_col" not in results + assert "a_renamed_column" in results + assert frozenset(expected) <= frozenset(results) + # __dir__ is called with a '.' and displays all methods, columns names, etc. + assert "to_gbq" in results + assert "merge" in results + assert "drop" in results + + +@pytest.mark.parametrize( + ("start", "stop", "step"), + [ + (0, 0, None), + (None, None, None), + (1, None, None), + (None, 4, None), + (None, None, 2), + (None, 50000000000, 1), + (5, 4, None), + (3, None, 2), + (1, 7, 2), + (1, 7, 50000000000), + ], +) +def test_iloc_slice(scalars_df_index, scalars_pandas_df_index, start, stop, step): + bf_result = scalars_df_index.iloc[start:stop:step].compute() + pd_result = scalars_pandas_df_index.iloc[start:stop:step] + + # Pandas may assign non-object dtype to empty series and series index + # dtypes of empty columns are a known area of divergence from pandas + for column in pd_result.columns: + if ( + pd_result[column].empty and column != "geography_col" + ): # for empty geography_col, bigframes assigns non-object dtype + pd_result[column] = pd_result[column].astype("object") + pd_result.index = pd_result.index.astype("object") + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_iloc_slice_zero_step(scalars_df_index): + with pytest.raises(ValueError): + scalars_df_index.iloc[0:0:0] + + +def test_iloc_slice_nested(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.iloc[1:].iloc[1:].compute() + pd_result = scalars_pandas_df_index.iloc[1:].iloc[1:] + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + "index", + [0, 5], +) +def test_iloc_single_integer(scalars_df_index, scalars_pandas_df_index, index): + bf_result = scalars_df_index.iloc[index] + pd_result = scalars_pandas_df_index.iloc[index] + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_iloc_single_integer_out_of_bound_error( + scalars_df_index, scalars_pandas_df_index +): + with pytest.raises(IndexError, match="single positional indexer is out-of-bounds"): + scalars_df_index.iloc[99] + + +def test_loc_bool_series(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.loc[scalars_df_index.bool_col].compute() + pd_result = scalars_pandas_df_index.loc[scalars_pandas_df_index.bool_col] + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_loc_select_column(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.loc[:, "int64_col"].compute() + pd_result = scalars_pandas_df_index.loc[:, "int64_col"] + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("op"), + [ + (lambda x: x.sum(numeric_only=True)), + (lambda x: x.mean(numeric_only=True)), + (lambda x: x.min(numeric_only=True)), + (lambda x: x.max(numeric_only=True)), + (lambda x: x.std(numeric_only=True)), + (lambda x: x.var(numeric_only=True)), + (lambda x: x.count(numeric_only=False)), + (lambda x: x.nunique()), + ], + ids=["sum", "mean", "min", "max", "std", "var", "count", "nunique"], +) +def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op): + col_names = ["int64_too", "float64_col", "string_col", "int64_col", "bool_col"] + bf_series = op(scalars_df_index[col_names]) + pd_series = op(scalars_pandas_df_index[col_names]) + bf_result = bf_series.compute() + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_series = pd_series.astype("Float64") + # Pandas has object index type + pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) + + +@pytest.mark.parametrize( + ("op"), + [ + (lambda x: x.all(bool_only=True)), + (lambda x: x.any(bool_only=True)), + ], + ids=["all", "any"], +) +def test_dataframe_bool_aggregates(scalars_df_index, scalars_pandas_df_index, op): + # Pandas will drop nullable 'boolean' dtype so we convert first to bool, then cast back later + scalars_pandas_df_index = scalars_pandas_df_index.assign( + bool_col=scalars_pandas_df_index.bool_col.fillna(False).astype("bool") + ) + bf_series = op(scalars_df_index) + pd_series = op(scalars_pandas_df_index).astype("boolean") + bf_result = bf_series.compute() + + # Pandas has object index type + pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) + + +def test_dataframe_prod(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col"] + bf_series = scalars_df_index[col_names].prod() + pd_series = scalars_pandas_df_index[col_names].prod() + bf_result = bf_series.compute() + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_series = pd_series.astype("Float64") + # Pandas has object index type + pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) + + +@pytest.mark.parametrize( + ("frac", "n", "random_state"), + [ + (None, 4, None), + (0.5, None, None), + (None, 4, 10), + (0.5, None, 10), + (None, None, None), + ], + ids=[ + "n_wo_random_state", + "frac_wo_random_state", + "n_w_random_state", + "frac_w_random_state", + "n_default", + ], +) +def test_sample(scalars_dfs, frac, n, random_state): + scalars_df, _ = scalars_dfs + df = scalars_df.sample(frac=frac, n=n, random_state=random_state) + bf_result = df.compute() + + n = 1 if n is None else n + expected_sample_size = round(frac * scalars_df.shape[0]) if frac is not None else n + assert bf_result.shape[0] == expected_sample_size + assert bf_result.shape[1] == scalars_df.shape[1] + + +def test_sample_raises_value_error(scalars_dfs): + scalars_df, _ = scalars_dfs + with pytest.raises( + ValueError, match="Only one of 'n' or 'frac' parameter can be specified." + ): + scalars_df.sample(frac=0.5, n=4) + + +@pytest.mark.parametrize( + ("axis",), + [ + (0,), + (1,), + ], +) +def test_df_add_prefix(scalars_df_index, scalars_pandas_df_index, axis): + if pd.__version__.startswith("1."): + pytest.skip("add_prefix axis parameter not supported in pandas 1.x.") + bf_result = scalars_df_index.add_prefix("prefix_", axis).compute() + + pd_result = scalars_pandas_df_index.add_prefix("prefix_", axis) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + check_index_type=False, + ) + + +@pytest.mark.parametrize( + ("axis",), + [ + (0,), + (1,), + ], +) +def test_df_add_suffix(scalars_df_index, scalars_pandas_df_index, axis): + if pd.__version__.startswith("1."): + pytest.skip("add_prefix axis parameter not supported in pandas 1.x.") + bf_result = scalars_df_index.add_suffix("_suffix", axis).compute() + + pd_result = scalars_pandas_df_index.add_suffix("_suffix", axis) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + check_index_type=False, + ) + + +def test_df_values(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.values + + pd_result = scalars_pandas_df_index.values + # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe + pd.testing.assert_frame_equal( + pd.DataFrame(bf_result), pd.DataFrame(pd_result), check_dtype=False + ) + + +def test_df_to_numpy(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.to_numpy() + + pd_result = scalars_pandas_df_index.to_numpy() + # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe + pd.testing.assert_frame_equal( + pd.DataFrame(bf_result), pd.DataFrame(pd_result), check_dtype=False + ) + + +def test_df___array__(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.__array__() + + pd_result = scalars_pandas_df_index.__array__() + # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe + pd.testing.assert_frame_equal( + pd.DataFrame(bf_result), pd.DataFrame(pd_result), check_dtype=False + ) + + +def test_getattr_not_implemented(scalars_df_index): + with pytest.raises(NotImplementedError): + scalars_df_index.asof() + + +def test_getattr_attribute_error(scalars_df_index): + with pytest.raises(AttributeError): + scalars_df_index.not_a_method() + + +def test_loc_list_string_index(scalars_df_index, scalars_pandas_df_index): + index_list = scalars_pandas_df_index.string_col.iloc[[0, 1, 1, 5]].values + + scalars_df_index = scalars_df_index.set_index("string_col") + scalars_pandas_df_index = scalars_pandas_df_index.set_index("string_col") + + bf_result = scalars_df_index.loc[index_list] + pd_result = scalars_pandas_df_index.loc[index_list] + + pd.testing.assert_frame_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): + index_list = [3, 2, 1, 3, 2, 1] + + bf_result = scalars_df_index.loc[index_list] + pd_result = scalars_pandas_df_index.loc[index_list] + + pd.testing.assert_frame_equal( + bf_result.compute(), + pd_result, + ) + + +def test_iloc_list(scalars_df_index, scalars_pandas_df_index): + index_list = [0, 0, 0, 5, 4, 7] + + bf_result = scalars_df_index.iloc[index_list] + pd_result = scalars_pandas_df_index.iloc[index_list] + + pd.testing.assert_frame_equal( + bf_result.compute(), + pd_result, + ) + + +def test_iloc_empty_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] + + bf_result = bf_result.compute() + assert bf_result.shape == pd_result.shape # types are known to be different + + +def test_rename_axis(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.rename_axis("newindexname") + pd_result = scalars_pandas_df_index.rename_axis("newindexname") + + pd.testing.assert_frame_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): + pd_string_series = scalars_pandas_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + bf_string_series = scalars_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + + scalars_df_index = scalars_df_index.set_index("string_col") + scalars_pandas_df_index = scalars_pandas_df_index.set_index("string_col") + + bf_result = scalars_df_index.loc[bf_string_series] + pd_result = scalars_pandas_df_index.loc[pd_string_series] + + pd.testing.assert_frame_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): + pd_index = scalars_pandas_df_index.iloc[[0, 5, 1, 1, 5]].index + bf_index = scalars_df_index.iloc[[0, 5, 1, 1, 5]].index + + bf_result = scalars_df_index.loc[bf_index] + pd_result = scalars_pandas_df_index.loc[pd_index] + + pd.testing.assert_frame_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_bf_index_integer_index_renamed_col( + scalars_df_index, scalars_pandas_df_index +): + scalars_df_index = scalars_df_index.rename(columns={"int64_col": "rename"}) + scalars_pandas_df_index = scalars_pandas_df_index.rename( + columns={"int64_col": "rename"} + ) + + pd_index = scalars_pandas_df_index.iloc[[0, 5, 1, 1, 5]].index + bf_index = scalars_df_index.iloc[[0, 5, 1, 1, 5]].index + + bf_result = scalars_df_index.loc[bf_index] + pd_result = scalars_pandas_df_index.loc[pd_index] + + pd.testing.assert_frame_equal( + bf_result.compute(), + pd_result, + ) + + +@pytest.mark.parametrize( + ("subset"), + [ + None, + ["bool_col", "int64_too"], + ], +) +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + (False,), + ], +) +def test_df_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep, subset): + columns = ["bool_col", "int64_too", "int64_col"] + bf_series = scalars_df_index[columns].drop_duplicates(subset, keep=keep).compute() + pd_series = scalars_pandas_df_index[columns].drop_duplicates(subset, keep=keep) + pd.testing.assert_frame_equal( + pd_series, + bf_series, + ) + + +@pytest.mark.parametrize( + ("subset"), + [ + None, + ["bool_col"], + ], +) +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + (False,), + ], +) +def test_df_duplicated(scalars_df_index, scalars_pandas_df_index, keep, subset): + columns = ["bool_col", "int64_too", "int64_col"] + bf_series = scalars_df_index[columns].duplicated(subset, keep=keep).compute() + pd_series = scalars_pandas_df_index[columns].duplicated(subset, keep=keep) + pd.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) + + +@pytest.mark.parametrize( + ("subset", "normalize", "ascending", "dropna"), + [ + (None, False, False, False), + (None, True, True, True), + ("bool_col", True, False, True), + ], +) +def test_df_value_counts(scalars_dfs, subset, normalize, ascending, dropna): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = ( + scalars_df[["string_col", "bool_col"]] + .value_counts(subset, normalize=normalize, ascending=ascending, dropna=dropna) + .compute() + ) + pd_result = scalars_pandas_df[["string_col", "bool_col"]].value_counts( + subset, normalize=normalize, ascending=ascending, dropna=dropna + ) + + # Older pandas version may not have these values, bigframes tries to emulate 2.0+ + pd_result.name = "count" + pd_result.index.names = bf_result.index.names + + pd.testing.assert_series_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + +@pytest.mark.parametrize( + ("na_option", "method", "ascending", "numeric_only"), + [ + ("keep", "average", True, True), + ("top", "min", False, False), + ("bottom", "max", False, False), + ("top", "first", False, False), + ("bottom", "dense", False, False), + ], +) +@pytest.mark.skipif( + True, reason="Blocked by possible pandas rank() regression (b/283278923)" +) +def test_df_rank_with_nulls( + scalars_df_index, + scalars_pandas_df_index, + na_option, + method, + ascending, + numeric_only, +): + unsupported_columns = ["geography_col"] + bf_result = ( + scalars_df_index.drop(columns=unsupported_columns) + .rank( + na_option=na_option, + method=method, + ascending=ascending, + numeric_only=numeric_only, + ) + .compute() + ) + pd_result = ( + scalars_pandas_df_index.drop(columns=unsupported_columns) + .rank( + na_option=na_option, + method=method, + ascending=ascending, + numeric_only=numeric_only, + ) + .astype(pd.Float64Dtype()) + ) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_bool_interpretation_error(scalars_df_index): + with pytest.raises(ValueError): + True if scalars_df_index else False diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py new file mode 100644 index 00000000000..a4318a8658f --- /dev/null +++ b/tests/system/small/test_dataframe_io.py @@ -0,0 +1,325 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Tuple + +import google.api_core.exceptions +import pandas as pd +import pytest + +from tests.system.utils import ( + assert_pandas_df_equal_ignore_ordering, + convert_pandas_dtypes, +) + +try: + import pandas_gbq # type: ignore +except ImportError: + pandas_gbq = None + +import bigframes +import bigframes.dataframe + + +def test_to_pandas_w_correct_dtypes(scalars_df_default_index): + """Verify to_pandas() APIs returns the expected dtypes.""" + actual = scalars_df_default_index.to_pandas().dtypes + expected = scalars_df_default_index.dtypes + + pd.testing.assert_series_equal(actual, expected) + + +def test_to_pandas_array_struct_correct_result(session): + """In future, we should support arrays and structs with arrow types. + For now we fall back to the current connector behavior of converting + to Python objects""" + df = session.read_gbq( + """SELECT + [1, 3, 2] AS array_column, + STRUCT( + "a" AS string_field, + 1.2 AS float_field) AS struct_column""" + ) + + result = df.to_pandas() + expected = pd.DataFrame( + { + "array_column": [[1, 3, 2]], + "struct_column": [{"string_field": "a", "float_field": 1.2}], + } + ) + expected.index = expected.index.astype("Int64") + pd.testing.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + ("index"), + [True, False], +) +def test_to_csv_index( + scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], + gcs_folder: str, + index: bool, +): + """Test the `to_csv` API with the `index` parameter.""" + scalars_df, scalars_pandas_df = scalars_dfs + index_col = None + if scalars_df.index.name is not None: + path = gcs_folder + f"test_index_df_to_csv_index_{index}" + if index: + index_col = scalars_df.index.name + else: + path = gcs_folder + f"test_default_index_df_to_csv_index_{index}" + + # TODO(swast): Support "date_format" parameter and make sure our + # DATETIME/TIMESTAMP column export is the same format as pandas by default. + scalars_df.to_csv(path, index=index) + + # Pandas dataframes dtypes from read_csv are not fully compatible with + # BigQuery-backed dataframes, so manually convert the dtypes specifically + # here. + dtype = scalars_df.reset_index().dtypes.to_dict() + dtype.pop("timestamp_col") + dtype.pop("geography_col") + gcs_df = pd.read_csv( + path, dtype=dtype, parse_dates=["timestamp_col"], index_col=index_col + ) + convert_pandas_dtypes(gcs_df, bytes_col=True) + + assert_pandas_df_equal_ignore_ordering(gcs_df, scalars_pandas_df) + + +@pytest.mark.parametrize( + ("index"), + [True, False], +) +@pytest.mark.skipif(pandas_gbq is None, reason="required by pd.read_gbq") +def test_to_gbq_index(scalars_dfs, dataset_id, index): + """Test the `to_gbq` API with the `index` parameter.""" + scalars_df, scalars_pandas_df = scalars_dfs + destination_table = f"{dataset_id}.test_index_df_to_gbq_{index}" + df_in = scalars_df.copy() + if index: + index_col = "index" + df_in.index.name = index_col + else: + index_col = None + + df_in.to_gbq(destination_table, if_exists="replace", index=index) + df_out = pd.read_gbq(destination_table, index_col=index_col) + + if index: + df_out = df_out.sort_index() + else: + df_out = df_out.sort_values("rowindex_2").reset_index(drop=True) + + convert_pandas_dtypes(df_out, bytes_col=False) + expected = scalars_pandas_df.copy() + expected.index.name = index_col + pd.testing.assert_frame_equal(df_out, expected, check_index_type=False) + + +@pytest.mark.parametrize( + ("if_exists", "expected_index"), + [ + pytest.param("replace", 1), + pytest.param("append", 2), + pytest.param( + "fail", + 0, + marks=pytest.mark.xfail( + raises=google.api_core.exceptions.Conflict, + ), + ), + pytest.param( + "unknown", + 0, + marks=pytest.mark.xfail( + raises=ValueError, + ), + ), + ], +) +@pytest.mark.skipif(pandas_gbq is None, reason="required by pd.read_gbq") +def test_to_gbq_if_exists( + scalars_df_default_index, + scalars_pandas_df_default_index, + dataset_id, + if_exists, + expected_index, +): + """Test the `to_gbq` API with the `if_exists` parameter.""" + destination_table = f"{dataset_id}.test_to_gbq_if_exists_{if_exists}" + + scalars_df_default_index.to_gbq(destination_table) + scalars_df_default_index.to_gbq(destination_table, if_exists=if_exists) + + gcs_df = pd.read_gbq(destination_table) + assert len(gcs_df.index) == expected_index * len( + scalars_pandas_df_default_index.index + ) + pd.testing.assert_index_equal( + gcs_df.columns, scalars_pandas_df_default_index.columns + ) + + +def test_to_gbq_w_invalid_destination_table(scalars_df_index): + with pytest.raises(ValueError): + scalars_df_index.to_gbq("table_id") + + +@pytest.mark.parametrize( + ("index"), + [True, False], +) +def test_to_json_index_invalid_orient( + scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], + gcs_folder: str, + index: bool, +): + scalars_df, scalars_pandas_df = scalars_dfs + if scalars_df.index.name is not None: + path = gcs_folder + f"test_index_df_to_json_index_{index}" + else: + path = gcs_folder + f"test_default_index_df_to_json_index_{index}" + with pytest.raises(ValueError): + scalars_df.to_json(path, index=index, lines=True) + + +@pytest.mark.parametrize( + ("index"), + [True, False], +) +def test_to_json_index_invalid_lines( + scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], + gcs_folder: str, + index: bool, +): + scalars_df, scalars_pandas_df = scalars_dfs + if scalars_df.index.name is not None: + path = gcs_folder + f"test_index_df_to_json_index_{index}" + else: + path = gcs_folder + f"test_default_index_df_to_json_index_{index}" + with pytest.raises(NotImplementedError): + scalars_df.to_json(path, index=index) + + +@pytest.mark.parametrize( + ("index"), + [True, False], +) +def test_to_json_index_records_orient( + scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], + gcs_folder: str, + index: bool, +): + """Test the `to_json` API with the `index` parameter.""" + scalars_df, scalars_pandas_df = scalars_dfs + if scalars_df.index.name is not None: + path = gcs_folder + f"test_index_df_to_json_index_{index}" + else: + path = gcs_folder + f"test_default_index_df_to_json_index_{index}" + + """ Test the `to_json` API with `orient` is `records` and `lines` is True""" + scalars_df.to_json(path, index=index, orient="records", lines=True) + + gcs_df = pd.read_json(path, lines=True, convert_dates=["datetime_col"]) + convert_pandas_dtypes(gcs_df, bytes_col=True) + if index and scalars_df.index.name is not None: + gcs_df = gcs_df.set_index(scalars_df.index.name) + + assert len(gcs_df.index) == len(scalars_pandas_df.index) + pd.testing.assert_index_equal(gcs_df.columns, scalars_pandas_df.columns) + assert_pandas_df_equal_ignore_ordering(gcs_df, scalars_pandas_df) + + +@pytest.mark.parametrize( + ("index"), + [True, False], +) +def test_to_parquet_index(scalars_dfs, gcs_folder, index): + """Test the `to_parquet` API with the `index` parameter.""" + scalars_df, scalars_pandas_df = scalars_dfs + if scalars_df.index.name is not None: + path = gcs_folder + f"test_index_df_to_parquet_{index}" + else: + path = gcs_folder + f"test_default_index_df_to_parquet_{index}" + + # TODO(b/268693993): Type GEOGRAPHY is not currently supported for parquet. + scalars_df = scalars_df.drop(columns="geography_col") + scalars_pandas_df = scalars_pandas_df.drop(columns="geography_col") + + # TODO(swast): Do a bit more processing on the input DataFrame to ensure + # the exported results are from the generated query, not just the source + # table. + scalars_df.to_parquet(path, index=index) + + gcs_df = pd.read_parquet(path) + convert_pandas_dtypes(gcs_df, bytes_col=False) + if index and scalars_df.index.name is not None: + gcs_df = gcs_df.set_index(scalars_df.index.name) + + assert len(gcs_df.index) == len(scalars_pandas_df.index) + pd.testing.assert_index_equal(gcs_df.columns, scalars_pandas_df.columns) + assert_pandas_df_equal_ignore_ordering(gcs_df, scalars_pandas_df) + + +def test_to_sql_query_named_index_included( + session, scalars_df_index, scalars_pandas_df_index +): + sql, index_columns = scalars_df_index.to_sql_query(always_include_index=True) + assert len(index_columns) == 1 + index_column, is_named = index_columns[0] + assert index_column == "rowindex" + assert is_named + + roundtrip = session.read_gbq(sql, index_col=[index_column]) + assert_pandas_df_equal_ignore_ordering( + roundtrip.to_pandas(), scalars_pandas_df_index + ) + + +def test_to_sql_query_unnamed_index_excluded( + session, scalars_df_default_index, scalars_pandas_df_default_index +): + # The .sql property should return SQL without the unnamed indexes + sql, index_columns = scalars_df_default_index.to_sql_query( + always_include_index=False + ) + assert len(index_columns) == 0 + + roundtrip = session.read_gbq(sql) + assert_pandas_df_equal_ignore_ordering( + roundtrip.to_pandas(), scalars_pandas_df_default_index + ) + + +def test_to_sql_query_unnamed_index_always_include( + session, + scalars_df_default_index: bigframes.dataframe.DataFrame, + scalars_pandas_df_default_index, +): + sql, index_columns = scalars_df_default_index.to_sql_query( + always_include_index=True + ) + assert len(index_columns) == 1 + index_column, is_named = index_columns[0] + assert index_column == "bigframes_index_0" + assert not is_named + + roundtrip = session.read_gbq(sql, index_col=[index_column]) + roundtrip.index.name = None + assert_pandas_df_equal_ignore_ordering( + roundtrip.to_pandas(), scalars_pandas_df_default_index + ) diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py new file mode 100644 index 00000000000..066e20bb12c --- /dev/null +++ b/tests/system/small/test_groupby.py @@ -0,0 +1,129 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x: x.sum(numeric_only=True)), + (lambda x: x.mean(numeric_only=True)), + (lambda x: x.min(numeric_only=True)), + (lambda x: x.max(numeric_only=True)), + (lambda x: x.std(numeric_only=True)), + (lambda x: x.var(numeric_only=True)), + ], + ids=[ + "sum", + "mean", + "min", + "max", + "std", + "var", + ], +) +def test_dataframe_groupby_numeric_aggregate( + scalars_df_index, scalars_pandas_df_index, operator +): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = operator(scalars_df_index[col_names].groupby("string_col")) + pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) + bf_result_computed = bf_result.compute() + # Pandas std function produces float64, not matching Float64 from bigframes + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x: x.count()), + (lambda x: x.any()), + (lambda x: x.all()), + ], + ids=[ + "count", + "any", + "all", + ], +) +def test_dataframe_groupby_aggregate( + scalars_df_index, scalars_pandas_df_index, operator +): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = operator(scalars_df_index[col_names].groupby("string_col")) + pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) + bf_result_computed = bf_result.compute() + + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + +@pytest.mark.parametrize( + ("as_index"), + [ + (True), + (False), + ], +) +def test_dataframe_groupby_multi_sum( + scalars_df_index, scalars_pandas_df_index, as_index +): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_series = ( + scalars_df_index[col_names] + .groupby(["bool_col", "int64_col"], as_index=as_index) + .sum(numeric_only=True) + ) + pd_series = ( + scalars_pandas_df_index[col_names] + .groupby(["bool_col", "int64_col"], as_index=as_index) + .sum(numeric_only=True) + ) + bf_result = bf_series.compute() + + if not as_index: + # BigQuery DataFrames default indices use nullable Int64 always + pd_series.index = pd_series.index.astype("Int64") + + pd.testing.assert_frame_equal( + pd_series, + bf_result, + ) + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x: x.cumsum(numeric_only=True)), + (lambda x: x.cummax(numeric_only=True)), + (lambda x: x.cummin(numeric_only=True)), + (lambda x: x.cumprod()), + ], + ids=[ + "cumsum", + "cummax", + "cummin", + "cumprod", + ], +) +def test_dataframe_groupby_analytic( + scalars_df_index, scalars_pandas_df_index, operator +): + col_names = ["float64_col", "int64_col", "bool_col", "string_col"] + bf_result = operator(scalars_df_index[col_names].groupby("string_col")) + pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) + bf_result_computed = bf_result.compute() + + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py new file mode 100644 index 00000000000..312e86d7f23 --- /dev/null +++ b/tests/system/small/test_index.py @@ -0,0 +1,46 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy + +from tests.system.utils import assert_pandas_index_equal_ignore_index_type + + +def test_get_index(scalars_df_index, scalars_pandas_df_index): + index = scalars_df_index.index + bf_result = index.compute() + pd_result = scalars_pandas_df_index.index + + assert_pandas_index_equal_ignore_index_type(bf_result, pd_result) + + +def test_index_shape(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.index.shape + pd_result = scalars_pandas_df_index.index.shape + + assert bf_result == pd_result + + +def test_index_len(scalars_df_index, scalars_pandas_df_index): + bf_result = len(scalars_df_index.index) + pd_result = len(scalars_pandas_df_index.index) + + assert bf_result == pd_result + + +def test_index_array(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.__array__() + pd_result = scalars_pandas_df_index.__array__() + + numpy.array_equal(bf_result, pd_result) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py new file mode 100644 index 00000000000..bf2a2080de3 --- /dev/null +++ b/tests/system/small/test_multiindex.py @@ -0,0 +1,273 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas +import pytest + +import bigframes.pandas as bpd + + +def test_set_multi_index(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index(["bool_col", "int64_too"]).compute() + pd_result = scalars_pandas_df_index.set_index(["bool_col", "int64_too"]) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_reset_multi_index(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index.set_index(["bool_col", "int64_too"]).reset_index().compute() + ) + pd_result = scalars_pandas_df_index.set_index( + ["bool_col", "int64_too"] + ).reset_index() + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pandas.Int64Dtype()) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_binop_series_series_matching_multi_indices( + scalars_df_index, scalars_pandas_df_index +): + bf_left = scalars_df_index.set_index(["bool_col", "string_col"]) + bf_right = scalars_df_index.set_index(["bool_col", "string_col"]) + pd_left = scalars_pandas_df_index.set_index(["bool_col", "string_col"]) + pd_right = scalars_pandas_df_index.set_index(["bool_col", "string_col"]) + + bf_result = bf_left["int64_col"] + bf_right["int64_too"] + pd_result = pd_left["int64_col"] + pd_right["int64_too"] + + pandas.testing.assert_series_equal( + bf_result.sort_index().compute(), pd_result.sort_index() + ) + + +def test_binop_df_series_matching_multi_indices( + scalars_df_index, scalars_pandas_df_index +): + bf_left = scalars_df_index.set_index(["bool_col", "string_col"]) + bf_right = scalars_df_index.set_index(["bool_col", "string_col"]) + pd_left = scalars_pandas_df_index.set_index(["bool_col", "string_col"]) + pd_right = scalars_pandas_df_index.set_index(["bool_col", "string_col"]) + + bf_result = bf_left[["int64_col", "int64_too"]].add(bf_right["int64_too"], axis=0) + pd_result = pd_left[["int64_col", "int64_too"]].add(pd_right["int64_too"], axis=0) + + pandas.testing.assert_frame_equal( + bf_result.sort_index().compute(), pd_result.sort_index() + ) + + +def test_binop_multi_index_mono_index(scalars_df_index, scalars_pandas_df_index): + bf_left = scalars_df_index.set_index(["bool_col", "rowindex_2"]) + bf_right = scalars_df_index.set_index("rowindex_2") + pd_left = scalars_pandas_df_index.set_index(["bool_col", "rowindex_2"]) + pd_right = scalars_pandas_df_index.set_index("rowindex_2") + + bf_result = bf_left["int64_col"] + bf_right["int64_too"] + pd_result = pd_left["int64_col"] + pd_right["int64_too"] + + pandas.testing.assert_series_equal(bf_result.compute(), pd_result) + + +def test_binop_overlapping_multi_indices(scalars_df_index, scalars_pandas_df_index): + bf_left = scalars_df_index.set_index(["bool_col", "int64_too"]) + bf_right = scalars_df_index.set_index(["bool_col", "int64_col"]) + pd_left = scalars_pandas_df_index.set_index(["bool_col", "int64_too"]) + pd_right = scalars_pandas_df_index.set_index(["bool_col", "int64_col"]) + + bf_result = bf_left["int64_col"] + bf_right["int64_too"] + pd_result = pd_left["int64_col"] + pd_right["int64_too"] + + pandas.testing.assert_series_equal( + bf_result.sort_index().compute(), pd_result.sort_index() + ) + + +def test_concat_compatible_multi_indices(scalars_df_index, scalars_pandas_df_index): + if pandas.__version__.startswith("1."): + pytest.skip("Labels not preserved in pandas 1.x.") + bf_left = scalars_df_index.set_index(["bool_col", "int64_col"]) + bf_right = scalars_df_index.set_index(["bool_col", "int64_too"]) + pd_left = scalars_pandas_df_index.set_index(["bool_col", "int64_col"]) + pd_right = scalars_pandas_df_index.set_index(["bool_col", "int64_too"]) + + bf_result = bpd.concat([bf_left, bf_right]) + pd_result = pandas.concat([pd_left, pd_right]) + + pandas.testing.assert_frame_equal(bf_result.compute(), pd_result) + + +def test_concat_multi_indices_ignore_index(scalars_df_index, scalars_pandas_df_index): + bf_left = scalars_df_index.set_index(["bool_col", "int64_too"]) + bf_right = scalars_df_index.set_index(["bool_col", "int64_col"]) + pd_left = scalars_pandas_df_index.set_index(["bool_col", "int64_too"]) + pd_right = scalars_pandas_df_index.set_index(["bool_col", "int64_col"]) + + bf_result = bpd.concat([bf_left, bf_right], ignore_index=True) + pd_result = pandas.concat([pd_left, pd_right], ignore_index=True) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pandas.Int64Dtype()) + + pandas.testing.assert_frame_equal(bf_result.compute(), pd_result) + + +def test_multi_index_loc(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index.set_index(["int64_too", "bool_col"]).loc[[2, 0]].compute() + ) + pd_result = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]).loc[[2, 0]] + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_multi_index_getitem_bool(scalars_df_index, scalars_pandas_df_index): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) + + bf_result = bf_frame[bf_frame["int64_col"] > 0].compute() + pd_result = pd_frame[pd_frame["int64_col"] > 0] + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("level"), + [ + (1), + ("int64_too"), + ([0, 2]), + ([2, "bool_col"]), + ], + ids=["level_num", "level_name", "list", "mixed_list"], +) +def test_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, level): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + + bf_result = bf_frame.droplevel(level).compute() + pd_result = pd_frame.droplevel(level) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("order"), + [ + (1, 0, 2), + (["int64_col", "bool_col", "int64_too"]), + (["int64_col", "bool_col", 0]), + ], + ids=[ + "level_nums", + "level_names", + "num_names_mixed", + ], +) +def test_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_index, order): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + + bf_result = bf_frame.reorder_levels(order).compute() + pd_result = pd_frame.reorder_levels(order) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_multi_index_series_groupby(scalars_df_index, scalars_pandas_df_index): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col"]) + bf_result = ( + bf_frame["float64_col"] + .groupby([bf_frame.int64_col % 2, "bool_col"]) + .mean() + .compute() + ) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) + pd_result = ( + pd_frame["float64_col"].groupby([pd_frame.int64_col % 2, "bool_col"]).mean() + ) + + pandas.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("level"), + [ + (1), + ([0]), + (["bool_col"]), + (["bool_col", "int64_too"]), + ], +) +def test_multi_index_series_groupby_level( + scalars_df_index, scalars_pandas_df_index, level +): + bf_result = ( + scalars_df_index.set_index(["int64_too", "bool_col"])["float64_col"] + .groupby(level=level) + .mean() + .compute() + ) + pd_result = ( + scalars_pandas_df_index.set_index(["int64_too", "bool_col"])["float64_col"] + .groupby(level=level) + .mean() + ) + + pandas.testing.assert_series_equal(bf_result, pd_result) + + +def test_multi_index_dataframe_groupby(scalars_df_index, scalars_pandas_df_index): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col"]) + bf_result = ( + bf_frame.groupby([bf_frame.int64_col % 2, "bool_col"]) + .mean(numeric_only=True) + .compute() + ) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) + pd_result = pd_frame.groupby([pd_frame.int64_col % 2, "bool_col"]).mean( + numeric_only=True + ) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("level"), + [ + (1), + ([0]), + (["bool_col"]), + (["bool_col", "int64_too"]), + ], +) +def test_multi_index_dataframe_groupby_level( + scalars_df_index, scalars_pandas_df_index, level +): + bf_result = ( + scalars_df_index.set_index(["int64_too", "bool_col"]) + .groupby(level=level) + .mean(numeric_only=True) + .compute() + ) + pd_result = ( + scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) + .groupby(level=level) + .mean(numeric_only=True) + ) + + pandas.testing.assert_frame_equal(bf_result, pd_result) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py new file mode 100644 index 00000000000..a612ad946fa --- /dev/null +++ b/tests/system/small/test_pandas.py @@ -0,0 +1,64 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +import bigframes.pandas as bpd + + +def test_concat_dataframe(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = bpd.concat(11 * [scalars_df]) + bf_result = bf_result.compute() + pd_result = pd.concat(11 * [scalars_pandas_df]) + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_concat_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = bpd.concat( + [scalars_df.int64_col, scalars_df.int64_too, scalars_df.int64_col] + ) + bf_result = bf_result.compute() + pd_result = pd.concat( + [ + scalars_pandas_df.int64_col, + scalars_pandas_df.int64_too, + scalars_pandas_df.int64_col, + ] + ) + + pd.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("how",), + [ + ("inner",), + ("outer",), + ], +) +def test_concat_dataframe_mismatched_columns(scalars_dfs, how): + cols1 = ["int64_too", "int64_col", "float64_col"] + cols2 = ["int64_col", "string_col", "int64_too"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = bpd.concat([scalars_df[cols1], scalars_df[cols2]], join=how) + bf_result = bf_result.compute() + pd_result = pd.concat( + [scalars_pandas_df[cols1], scalars_pandas_df[cols2]], join=how + ) + + pd.testing.assert_frame_equal(bf_result, pd_result) diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py new file mode 100644 index 00000000000..261e0d5b14e --- /dev/null +++ b/tests/system/small/test_pandas_options.py @@ -0,0 +1,145 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import google.api_core.exceptions +import pytest + +import bigframes.pandas as bpd + + +@pytest.fixture(autouse=True) +def reset_default_session_and_location(): + bpd.reset_session() + bpd.options.bigquery.location = None + + +@pytest.mark.parametrize( + ("read_method", "query_prefix"), + [ + (bpd.read_gbq, None), + (bpd.read_gbq, "SELECT COUNT(1) FROM "), + (bpd.read_gbq_table, None), + (bpd.read_gbq_query, "SELECT COUNT(1) FROM "), + ], + ids=[ + "read_gbq-on-table-name", + "read_gbq-on-sql", + "read_gbq_table-on-table-name", + "read_gbq_query-on-sql", + ], +) +def test_read_gbq_start_sets_session_location( + test_data_tables_tokyo, + dataset_id_permanent_tokyo, + tokyo_location, + test_data_tables, + dataset_id_permanent, + read_method, + query_prefix, +): + # Form query as a table name or a SQL depending on the test scenario + query_tokyo = test_data_tables_tokyo["scalars"] + query = test_data_tables["scalars"] + if query_prefix: + query_tokyo = f"{query_prefix} {query_tokyo}" + query = f"{query_prefix} {query}" + + # Initially there is no location set in the bigquery options + assert not bpd.options.bigquery.location + + # Starting user journey with read_gbq* should work for a table in any + # location, in this case tokyo + df = read_method(query_tokyo) + assert df is not None + + # Now bigquery options location should be set to tokyo + assert bpd.options.bigquery.location == tokyo_location + + # Now read_gbq* from another location should fail + with pytest.raises( + google.api_core.exceptions.NotFound, + match=f"404 Not found: Dataset {dataset_id_permanent} was not found in location {tokyo_location}", + ): + read_method(query) + + # Reset global session to start over + bpd.reset_session() + + # There should still be the previous location set in the bigquery options + assert bpd.options.bigquery.location == tokyo_location + + # Starting over the user journey with read_gbq* should work for a table + # in another location, in this case US + df = read_method(query) + assert df is not None + + # Now bigquery options location should be set to US + assert bpd.options.bigquery.location == "US" + + # Now read_gbq* from another location should fail + with pytest.raises( + google.api_core.exceptions.NotFound, + match=f"404 Not found: Dataset {dataset_id_permanent_tokyo} was not found in location US", + ): + read_method(query_tokyo) + + +@pytest.mark.parametrize( + ("read_method", "query_prefix"), + [ + (bpd.read_gbq, None), + (bpd.read_gbq, "SELECT COUNT(1) FROM "), + (bpd.read_gbq_table, None), + (bpd.read_gbq_query, "SELECT COUNT(1) FROM "), + ], + ids=[ + "read_gbq-on-table-name", + "read_gbq-on-sql", + "read_gbq_table-on-table-name", + "read_gbq_query-on-sql", + ], +) +def test_read_gbq_after_session_start_must_comply_with_default_location( + scalars_pandas_df_index, + test_data_tables, + test_data_tables_tokyo, + dataset_id_permanent_tokyo, + read_method, + query_prefix, +): + # Form query as a table name or a SQL depending on the test scenario + query_tokyo = test_data_tables_tokyo["scalars"] + query = test_data_tables["scalars"] + if query_prefix: + query_tokyo = f"{query_prefix} {query_tokyo}" + query = f"{query_prefix} {query}" + + # Initially there is no location set in the bigquery options + assert not bpd.options.bigquery.location + + # Starting user journey with anything other than read_gbq*, such as + # read_pandas would bind the session to default location US + df = bpd.read_pandas(scalars_pandas_df_index) + assert df is not None + + # Doing read_gbq* from a table in another location should fail + with pytest.raises( + google.api_core.exceptions.NotFound, + match=f"404 Not found: Dataset {dataset_id_permanent_tokyo} was not found in location US", + ): + read_method(query_tokyo) + + # read_gbq* from a table in the default location should work + df = read_method(query) + assert df is not None diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py new file mode 100644 index 00000000000..6eeee3a3a43 --- /dev/null +++ b/tests/system/small/test_progress_bar.py @@ -0,0 +1,74 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes as bf +import bigframes.formatting_helpers as formatting_helpers + + +def test_progress_bar_dataframe( + penguins_df_default_index: bf.dataframe.DataFrame, capsys +): + bf.options.display.progress_bar = "notebook" + penguins_df_default_index.compute() + html_check = "HTML(value=" + open_job_check = "Open Job" + lines = capsys.readouterr().out.split("\n") + lines = filter(None, lines) + assert penguins_df_default_index.query_job is not None + for line in lines: + assert html_check in line and open_job_check in line + + +def test_progress_bar_series(penguins_df_default_index: bf.dataframe.DataFrame, capsys): + bf.options.display.progress_bar = "notebook" + series = penguins_df_default_index["body_mass_g"].head(10) + series.compute() + html_check = "HTML(value=" + open_job_check = "Open Job" + lines = capsys.readouterr().out.split("\n") + lines = filter(None, lines) + assert series.query_job is not None + for line in lines: + assert html_check in line and open_job_check in line + + +def test_progress_bar_scalar(penguins_df_default_index: bf.dataframe.DataFrame, capsys): + bf.options.display.progress_bar = "notebook" + penguins_df_default_index["body_mass_g"].head(10).mean() + html_check = "HTML(value=" + open_job_check = "Open Job" + lines = capsys.readouterr().out.split("\n") + lines = filter(None, lines) + for line in lines: + assert html_check in line and open_job_check in line + + +def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): + bf.options.display.progress_bar = "notebook" + penguins_df_default_index._block._expr._session.bqclient.default_query_job_config.use_query_cache = ( + False + ) + penguins_df_default_index.compute() + query_job_repr = formatting_helpers.repr_query_job( + penguins_df_default_index.query_job + ).value + string_checks = [ + "Job Id", + "Destination Table", + "Slot Time", + "Bytes Processed", + "Cache hit", + ] + for string in string_checks: + assert string in query_job_repr diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py new file mode 100644 index 00000000000..4c75b5d3d27 --- /dev/null +++ b/tests/system/small/test_remote_function.py @@ -0,0 +1,278 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +import bigframes +from bigframes.remote_function import remote_function +from tests.system.utils import assert_pandas_df_equal_ignore_ordering + + +@pytest.fixture(scope="module") +def bq_cf_connection() -> str: + """Pre-created BQ connection to invoke cloud function for bigframes-dev + $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn + """ + return "bigframes-rf-conn" + + +@pytest.fixture(scope="module") +def session_with_bq_connection(bq_cf_connection) -> bigframes.Session: + return bigframes.Session( + bigframes.BigQueryOptions(remote_udf_connection=bq_cf_connection) + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_direct_no_session_param( + bigquery_client, + bigqueryconnection_client, + scalars_dfs, + dataset_id_permanent, + bq_cf_connection, +): + @remote_function( + [int], + int, + bigquery_client=bigquery_client, + bigquery_connection_client=bigqueryconnection_client, + dataset=dataset_id_permanent, + bigquery_connection=bq_cf_connection, + # See e2e tests for tests that actually deploy the Cloud Function. + reuse=True, + ) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_direct_session_param(session_with_bq_connection, scalars_dfs): + @remote_function( + [int], + int, + session=session_with_bq_connection, + ) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_via_session_default(session_with_bq_connection, scalars_dfs): + # Session has bigquery connection initialized via context. Without an + # explicit dataset the default dataset from the session would be used. + # Without an explicit bigquery connection, the one present in Session set + # through the explicit BigQueryOptions would be used. Without an explicit `reuse` + # the default behavior of reuse=True will take effect. Please note that the + # udf is same as the one used in other tests in this file so the underlying + # cloud function would be common and quickly reused. + @session_with_bq_connection.remote_function([int], int) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_via_session_with_overrides( + session, scalars_dfs, dataset_id_permanent, bq_cf_connection +): + @session.remote_function( + [int], + int, + dataset_id_permanent, + bq_cf_connection, + # See e2e tests for tests that actually deploy the Cloud Function. + reuse=True, + ) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_via_session_context_connection_setter( + scalars_dfs, dataset_id, bq_cf_connection +): + # Creating a session scoped only to this test as we would be setting a + # property in it + context = bigframes.BigQueryOptions() + context.remote_udf_connection = bq_cf_connection + session = bigframes.connect(context) + + # Without an explicit bigquery connection, the one present in Session, + # set via context setter would be used. Without an explicit `reuse` the + # default behavior of reuse=True will take effect. Please note that the + # udf is same as the one used in other tests in this file so the underlying + # cloud function would be common with reuse=True. Since we are using a + # unique dataset_id, even though the cloud function would be reused, the bq + # remote function would still be created, making use of the bq connection + # set in the BigQueryOptions above. + @session.remote_function([int], int, dataset=dataset_id) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_dataframe_applymap(session_with_bq_connection, scalars_dfs): + def add_one(x): + return x + 1 + + remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + + scalars_df, scalars_pandas_df = scalars_dfs + int64_cols = ["int64_col", "int64_too"] + + bf_int64_df = scalars_df[int64_cols] + bf_int64_df_filtered = bf_int64_df.dropna() + bf_result = bf_int64_df_filtered.applymap(remote_add_one).compute() + + pd_int64_df = scalars_pandas_df[int64_cols] + pd_int64_df_filtered = pd_int64_df.dropna() + pd_result = pd_int64_df_filtered.applymap(add_one) + # TODO(shobs): Figure why pandas .applymap() changes the dtype, i.e. + # pd_int64_df_filtered.dtype is Int64Dtype() + # pd_int64_df_filtered.applymap(lambda x: x).dtype is int64. + # For this test let's force the pandas dtype to be same as input. + for col in pd_result: + pd_result[col] = pd_result[col].astype(pd_int64_df_filtered[col].dtype) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_dataframe_applymap_na_ignore(session_with_bq_connection, scalars_dfs): + def add_one(x): + return x + 1 + + remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + + scalars_df, scalars_pandas_df = scalars_dfs + int64_cols = ["int64_col", "int64_too"] + + bf_int64_df = scalars_df[int64_cols] + bf_result = bf_int64_df.applymap(remote_add_one, na_action="ignore").compute() + + pd_int64_df = scalars_pandas_df[int64_cols] + pd_result = pd_int64_df.applymap(add_one, na_action="ignore") + # TODO(shobs): Figure why pandas .applymap() changes the dtype, i.e. + # pd_int64_df_filtered.dtype is Int64Dtype() + # pd_int64_df_filtered.applymap(lambda x: x).dtype is int64. + # For this test let's force the pandas dtype to be same as input. + for col in pd_result: + pd_result[col] = pd_result[col].astype(pd_int64_df[col].dtype) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) diff --git a/tests/system/small/test_scalar.py b/tests/system/small/test_scalar.py new file mode 100644 index 00000000000..8b9e1fd95a9 --- /dev/null +++ b/tests/system/small/test_scalar.py @@ -0,0 +1,25 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas + + +def test_repr(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_series = scalars_df[col_name] + pd_series = scalars_pandas_df[col_name].astype(pandas.Int64Dtype()) + bf_scalar = bf_series.sum() + pd_scalar = pd_series.sum() + assert repr(bf_scalar) == repr(pd_scalar) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py new file mode 100644 index 00000000000..751521bd754 --- /dev/null +++ b/tests/system/small/test_series.py @@ -0,0 +1,1934 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import tempfile + +import geopandas as gpd # type: ignore +import numpy +import pandas as pd +import pyarrow as pa # type: ignore +import pytest + +import bigframes.series as series +from tests.system.utils import ( + assert_pandas_df_equal_ignore_ordering, + assert_series_equal_ignoring_order, +) + + +def test_series_construct_copy(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = series.Series( + scalars_df["int64_col"], name="test_series", dtype="Float64" + ).compute() + pd_result = pd.Series( + scalars_pandas_df["int64_col"], name="test_series", dtype="Float64" + ) + pd.testing.assert_series_equal(bf_result, pd_result) + + +def test_series_construct_pandas(scalars_dfs): + _, scalars_pandas_df = scalars_dfs + bf_result = series.Series( + scalars_pandas_df["int64_col"], name="test_series", dtype="Float64" + ) + pd_result = pd.Series( + scalars_pandas_df["int64_col"], name="test_series", dtype="Float64" + ) + assert bf_result.shape == pd_result.shape + pd.testing.assert_series_equal(bf_result.compute(), pd_result) + + +def test_series_construct_from_list(): + bf_result = series.Series([1, 1, 2, 3, 5, 8, 13], dtype="Int64").compute() + pd_result = pd.Series([1, 1, 2, 3, 5, 8, 13], dtype="Int64") + + # BigQuery DataFrame default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ["col_name", "expected_dtype"], + [ + ("bool_col", pd.BooleanDtype()), + # TODO(swast): Use a more efficient type. + ("bytes_col", numpy.dtype("object")), + ("date_col", pd.ArrowDtype(pa.date32())), + ("datetime_col", pd.ArrowDtype(pa.timestamp("us"))), + ("float64_col", pd.Float64Dtype()), + ("geography_col", gpd.array.GeometryDtype()), + ("int64_col", pd.Int64Dtype()), + # TODO(swast): Use a more efficient type. + ("numeric_col", numpy.dtype("object")), + ("int64_too", pd.Int64Dtype()), + ("string_col", pd.StringDtype(storage="pyarrow")), + ("time_col", pd.ArrowDtype(pa.time64("us"))), + ("timestamp_col", pd.ArrowDtype(pa.timestamp("us", tz="UTC"))), + ], +) +def test_get_column(scalars_dfs, col_name, expected_dtype): + scalars_df, scalars_pandas_df = scalars_dfs + series = scalars_df[col_name] + series_pandas = series.compute() + assert series_pandas.dtype == expected_dtype + assert series_pandas.shape[0] == scalars_pandas_df.shape[0] + + +def test_series_get_column_default(scalars_dfs): + scalars_df, _ = scalars_dfs + result = scalars_df.get(123123123123123, "default_val") + assert result == "default_val" + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("float64_col",), + ("int64_too",), + ), +) +def test_abs(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].abs().compute() + pd_result = scalars_pandas_df[col_name].abs() + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +def test_fillna(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = scalars_df[col_name].fillna("Missing").compute() + pd_result = scalars_pandas_df[col_name].fillna("Missing") + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("string_col",), + ("int64_col",), + ), +) +def test_max(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].max() + pd_result = scalars_pandas_df[col_name].max() + assert pd_result == bf_result + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("string_col",), + ("int64_col",), + ), +) +def test_min(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].min() + pd_result = scalars_pandas_df[col_name].min() + assert pd_result == bf_result + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("float64_col",), + ("int64_col",), + ), +) +def test_std(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].std() + pd_result = scalars_pandas_df[col_name].std() + assert math.isclose(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("float64_col",), + ("int64_col",), + ), +) +def test_kurt(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].kurt() + pd_result = scalars_pandas_df[col_name].kurt() + assert math.isclose(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("float64_col",), + ("int64_col",), + ), +) +def test_var(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].var() + pd_result = scalars_pandas_df[col_name].var() + assert math.isclose(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("bool_col",), + ("int64_col",), + ), +) +def test_mode_stat(scalars_df_index, scalars_pandas_df_index, col_name): + bf_result = scalars_df_index[col_name].mode().compute() + pd_result = scalars_pandas_df_index[col_name].mode() + + ## Mode implicitly resets index, and bigframes default indices use nullable Int64 + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x, y: x + y), + (lambda x, y: x - y), + (lambda x, y: x * y), + (lambda x, y: x / y), + (lambda x, y: x // y), + (lambda x, y: x < y), + (lambda x, y: x > y), + (lambda x, y: x <= y), + (lambda x, y: x >= y), + ], + ids=[ + "add", + "subtract", + "multiply", + "divide", + "floordivide", + "less_than", + "greater_than", + "less_than_equal", + "greater_than_equal", + ], +) +@pytest.mark.parametrize(("other_scalar"), [-1, 0, 14, pd.NA]) +@pytest.mark.parametrize(("reverse_operands"), [True, False]) +def test_series_int_int_operators_scalar( + scalars_dfs, operator, other_scalar, reverse_operands +): + scalars_df, scalars_pandas_df = scalars_dfs + + maybe_reversed_op = (lambda x, y: operator(y, x)) if reverse_operands else operator + + bf_result = maybe_reversed_op(scalars_df["int64_col"], other_scalar).compute() + pd_result = maybe_reversed_op(scalars_pandas_df["int64_col"], other_scalar) + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x, y: x & y), + (lambda x, y: x | y), + ], + ids=[ + "and", + "or", + ], +) +@pytest.mark.parametrize(("other_scalar"), [True, False, pd.NA]) +@pytest.mark.parametrize(("reverse_operands"), [True, False]) +def test_series_bool_bool_operators_scalar( + scalars_dfs, operator, other_scalar, reverse_operands +): + scalars_df, scalars_pandas_df = scalars_dfs + + maybe_reversed_op = (lambda x, y: operator(y, x)) if reverse_operands else operator + + bf_result = maybe_reversed_op(scalars_df["bool_col"], other_scalar).compute() + pd_result = maybe_reversed_op(scalars_pandas_df["bool_col"], other_scalar) + + assert_series_equal_ignoring_order(pd_result.astype(pd.BooleanDtype()), bf_result) + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x, y: x + y), + (lambda x, y: x - y), + (lambda x, y: x * y), + (lambda x, y: x / y), + (lambda x, y: x < y), + (lambda x, y: x > y), + (lambda x, y: x <= y), + (lambda x, y: x >= y), + (lambda x, y: x % y), + (lambda x, y: x // y), + (lambda x, y: x & y), + (lambda x, y: x | y), + ], + ids=[ + "add", + "subtract", + "multiply", + "divide", + "less_than", + "greater_than", + "less_than_equal", + "greater_than_equal", + "modulo", + "floordivide", + "bitwise_and", + "bitwise_or", + ], +) +def test_series_int_int_operators_series(scalars_dfs, operator): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = operator(scalars_df["int64_col"], scalars_df["int64_too"]).compute() + pd_result = operator(scalars_pandas_df["int64_col"], scalars_pandas_df["int64_too"]) + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("other",), + [ + (3,), + (-6.2,), + ], +) +def test_series_add_scalar(scalars_dfs, other): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = (scalars_df["float64_col"] + other).compute() + pd_result = scalars_pandas_df["float64_col"] + other + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("left_col", "right_col"), + [ + ("float64_col", "float64_col"), + ("int64_col", "float64_col"), + ("int64_col", "int64_too"), + ], +) +def test_series_add_bigframes_series(scalars_dfs, left_col, right_col): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = (scalars_df[left_col] + scalars_df[right_col]).compute() + pd_result = scalars_pandas_df[left_col] + scalars_pandas_df[right_col] + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("left_col", "right_col", "righter_col"), + [ + ("float64_col", "float64_col", "float64_col"), + ("int64_col", "int64_col", "int64_col"), + ], +) +def test_series_add_bigframes_series_nested( + scalars_dfs, left_col, right_col, righter_col +): + """Test that we can correctly add multiple times.""" + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = ( + (scalars_df[left_col] + scalars_df[right_col]) + scalars_df[righter_col] + ).compute() + pd_result = ( + scalars_pandas_df[left_col] + scalars_pandas_df[right_col] + ) + scalars_pandas_df[righter_col] + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +def test_series_add_different_table_default_index( + scalars_df_default_index, + scalars_df_2_default_index, +): + bf_result = ( + scalars_df_default_index["float64_col"] + + scalars_df_2_default_index["float64_col"] + ).compute() + pd_result = ( + # Default index may not have a well defined order, but it should at + # least be consistent across compute() calls. + scalars_df_default_index["float64_col"].compute() + + scalars_df_2_default_index["float64_col"].compute() + ) + # TODO(swast): Can remove sort_index() when there's default ordering. + pd.testing.assert_series_equal(bf_result.sort_index(), pd_result.sort_index()) + + +def test_series_add_different_table_with_index( + scalars_df_index, scalars_df_2_index, scalars_pandas_df_index +): + scalars_pandas_df = scalars_pandas_df_index + bf_result = scalars_df_index["float64_col"] + scalars_df_2_index["int64_col"] + # When index values are unique, we can emulate with values from the same + # DataFrame. + pd_result = scalars_pandas_df["float64_col"] + scalars_pandas_df["int64_col"] + pd.testing.assert_series_equal(bf_result.compute(), pd_result) + + +def test_reset_index_drop(scalars_df_index, scalars_pandas_df_index): + scalars_pandas_df = scalars_pandas_df_index + bf_result = ( + scalars_df_index["float64_col"] + .sort_index(ascending=False) + .reset_index(drop=True) + ).iloc[::2] + pd_result = ( + scalars_pandas_df["float64_col"] + .sort_index(ascending=False) + .reset_index(drop=True) + ).iloc[::2] + + # BigQuery DataFrames default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal(bf_result.compute(), pd_result) + + +@pytest.mark.parametrize( + ("name",), + [ + ("some_name",), + (None,), + ], +) +def test_reset_index_no_drop(scalars_df_index, scalars_pandas_df_index, name): + scalars_pandas_df = scalars_pandas_df_index + kw_args = {"name": name} if name else {} + bf_result = ( + scalars_df_index["float64_col"] + .sort_index(ascending=False) + .reset_index(drop=False, **kw_args) + ) + pd_result = ( + scalars_pandas_df["float64_col"] + .sort_index(ascending=False) + .reset_index(drop=False, **kw_args) + ) + + # BigQuery DataFrames default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_frame_equal(bf_result.compute(), pd_result) + + +def test_series_add_pandas_series_not_implemented(scalars_dfs): + scalars_df, _ = scalars_dfs + with pytest.raises(NotImplementedError): + ( + scalars_df["float64_col"] + + pd.Series( + [1, 1, 1, 1], + ) + ).compute() + + +def test_copy(scalars_df_index, scalars_pandas_df_index): + col_name = "float64_col" + # Expect mutation on original not to effect_copy + bf_series = scalars_df_index[col_name].copy() + bf_copy = bf_series.copy() + bf_copy.loc[0] = 5.6 + bf_series.loc[0] = 3.4 + + pd_series = scalars_pandas_df_index[col_name].copy() + pd_copy = pd_series.copy() + pd_copy.loc[0] = 5.6 + pd_series.loc[0] = 3.4 + + assert bf_copy.compute().loc[0] != bf_series.compute().loc[0] + pd.testing.assert_series_equal(bf_copy.compute(), pd_copy) + + +def test_isnull(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "float64_col" + bf_series = scalars_df[col_name].isnull().compute() + pd_series = scalars_pandas_df[col_name].isnull() + + # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but + # the `pd_series.dtype` is `bool`. + assert_series_equal_ignoring_order(pd_series.astype(pd.BooleanDtype()), bf_series) + + +def test_notnull(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series = scalars_df[col_name].notnull().compute() + pd_series = scalars_pandas_df[col_name].notnull() + + # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but + # the `pd_series.dtype` is `bool`. + assert_series_equal_ignoring_order(pd_series.astype(pd.BooleanDtype()), bf_series) + + +def test_round(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "float64_col" + bf_result = scalars_df[col_name].round().compute() + pd_result = scalars_pandas_df[col_name].round() + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +def test_eq_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_result = scalars_df[col_name].eq(0).compute() + pd_result = scalars_pandas_df[col_name].eq(0) + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +def test_eq_wider_type_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_result = scalars_df[col_name].eq(1.0).compute() + pd_result = scalars_pandas_df[col_name].eq(1.0) + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +def test_ne_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_result = (scalars_df[col_name] != 0).compute() + pd_result = scalars_pandas_df[col_name] != 0 + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +def test_eq_int_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_result = (scalars_df[col_name] == 0).compute() + pd_result = scalars_pandas_df[col_name] == 0 + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("string_col",), + ("float64_col",), + ("int64_too",), + ), +) +def test_eq_same_type_series(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = (scalars_df[col_name] == scalars_df[col_name]).compute() + pd_result = scalars_pandas_df[col_name] == scalars_pandas_df[col_name] + + # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but + # the `pd_series.dtype` is `bool`. + assert_series_equal_ignoring_order(pd_result.astype(pd.BooleanDtype()), bf_result) + + +def test_loc_setitem_cell(scalars_df_index, scalars_pandas_df_index): + bf_original = scalars_df_index["string_col"] + bf_series = scalars_df_index["string_col"] + pd_original = scalars_pandas_df_index["string_col"] + pd_series = scalars_pandas_df_index["string_col"].copy() + bf_series.loc[2] = "This value isn't in the test data." + pd_series.loc[2] = "This value isn't in the test data." + bf_result = bf_series.compute() + pd_result = pd_series + pd.testing.assert_series_equal(bf_result, pd_result) + # Per Copy-on-Write semantics, other references to the original DataFrame + # should remain unchanged. + pd.testing.assert_series_equal(bf_original.compute(), pd_original) + + +def test_ne_obj_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = (scalars_df[col_name] != scalars_df[col_name]).compute() + pd_result = scalars_pandas_df[col_name] != scalars_pandas_df[col_name] + + # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but + # the `pd_series.dtype` is `bool`. + assert_series_equal_ignoring_order(pd_result.astype(pd.BooleanDtype()), bf_result) + + +def test_indexing_using_unselected_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = scalars_df[col_name][scalars_df["int64_too"].eq(0)].compute() + pd_result = scalars_pandas_df[col_name][scalars_pandas_df["int64_too"].eq(0)] + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_indexing_using_selected_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = scalars_df[col_name][ + scalars_df["string_col"].eq("Hello, World!") + ].compute() + pd_result = scalars_pandas_df[col_name][ + scalars_pandas_df["string_col"].eq("Hello, World!") + ] + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_nested_filter(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + string_col = scalars_df["string_col"] + int64_too = scalars_df["int64_too"] + bool_col = scalars_df["bool_col"] == bool( + True + ) # Convert from nullable bool to nonnullable bool usable as indexer + bf_result = string_col[int64_too == 0][~bool_col].compute() + + pd_string_col = scalars_pandas_df["string_col"] + pd_int64_too = scalars_pandas_df["int64_too"] + pd_bool_col = scalars_pandas_df["bool_col"] == bool( + True + ) # Convert from nullable bool to nonnullable bool usable as indexer + pd_result = pd_string_col[pd_int64_too == 0][~pd_bool_col] + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_binop_opposite_filters(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + int64_col1 = scalars_df["int64_col"] + int64_col2 = scalars_df["int64_col"] + bool_col = scalars_df["bool_col"] + bf_result = (int64_col1[bool_col] + int64_col2[bool_col.__invert__()]).compute() + + pd_int64_col1 = scalars_pandas_df["int64_col"] + pd_int64_col2 = scalars_pandas_df["int64_col"] + pd_bool_col = scalars_pandas_df["bool_col"] + pd_result = pd_int64_col1[pd_bool_col] + pd_int64_col2[pd_bool_col.__invert__()] + + assert_series_equal_ignoring_order( + bf_result, + pd_result, + ) + + +def test_binop_left_filtered(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + int64_col = scalars_df["int64_col"] + float64_col = scalars_df["float64_col"] + bool_col = scalars_df["bool_col"] + bf_result = (int64_col[bool_col] + float64_col).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_float64_col = scalars_pandas_df["float64_col"] + pd_bool_col = scalars_pandas_df["bool_col"] + pd_result = pd_int64_col[pd_bool_col] + pd_float64_col + + assert_series_equal_ignoring_order( + bf_result, + pd_result, + ) + + +def test_binop_right_filtered(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + int64_col = scalars_df["int64_col"] + float64_col = scalars_df["float64_col"] + bool_col = scalars_df["bool_col"] + bf_result = (float64_col + int64_col[bool_col]).compute() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_float64_col = scalars_pandas_df["float64_col"] + pd_bool_col = scalars_pandas_df["bool_col"] + pd_result = pd_float64_col + pd_int64_col[pd_bool_col] + + assert_series_equal_ignoring_order( + bf_result, + pd_result, + ) + + +def test_mean(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = scalars_df[col_name].mean() + pd_result = scalars_pandas_df[col_name].mean() + assert math.isclose(pd_result, bf_result) + + +def test_repr(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + if scalars_pandas_df.index.name != "rowindex": + pytest.skip("Require index & ordering for consistent repr.") + + col_name = "int64_col" + bf_series = scalars_df[col_name] + pd_series = scalars_pandas_df[col_name] + assert repr(bf_series) == repr(pd_series) + + +def test_sum(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = scalars_df[col_name].sum() + pd_result = scalars_pandas_df[col_name].sum() + assert pd_result == bf_result + + +def test_product(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "float64_col" + bf_result = scalars_df[col_name].product() + pd_result = scalars_pandas_df[col_name].product() + assert math.isclose(pd_result, bf_result) + + +def test_count(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = scalars_df[col_name].count() + pd_result = scalars_pandas_df[col_name].count() + assert pd_result == bf_result + + +def test_nunique(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = (scalars_df[col_name] % 3).nunique() + pd_result = (scalars_pandas_df[col_name] % 3).nunique() + assert pd_result == bf_result + + +def test_all(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = scalars_df[col_name].all() + pd_result = scalars_pandas_df[col_name].all() + assert pd_result == bf_result + + +def test_any(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = scalars_df[col_name].any() + pd_result = scalars_pandas_df[col_name].any() + assert pd_result == bf_result + + +def test_groupby_sum(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_series = scalars_df[col_name].groupby(scalars_df["string_col"]).sum() + pd_series = ( + scalars_pandas_df[col_name].groupby(scalars_pandas_df["string_col"]).sum() + ) + # TODO(swast): Update groupby to use index based on group by key(s). + bf_result = bf_series.compute() + assert_series_equal_ignoring_order( + pd_series, + bf_result, + check_exact=False, + ) + + +def test_groupby_std(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_series = scalars_df[col_name].groupby(scalars_df["string_col"]).std() + pd_series = ( + scalars_pandas_df[col_name] + .groupby(scalars_pandas_df["string_col"]) + .std() + .astype(pd.Float64Dtype()) + ) + bf_result = bf_series.compute() + assert_series_equal_ignoring_order( + pd_series, + bf_result, + check_exact=False, + ) + + +def test_groupby_var(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_series = scalars_df[col_name].groupby(scalars_df["string_col"]).var() + pd_series = ( + scalars_pandas_df[col_name].groupby(scalars_pandas_df["string_col"]).var() + ) + bf_result = bf_series.compute() + assert_series_equal_ignoring_order( + pd_series, + bf_result, + check_exact=False, + ) + + +def test_groupby_level_sum(scalars_dfs): + # TODO(tbergeron): Use a non-unique index once that becomes possible in tests + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + if scalars_pandas_df.index.name != "rowindex": + pytest.skip("Require index for groupby level.") + + bf_series = scalars_df[col_name].groupby(level=0).sum() + pd_series = scalars_pandas_df[col_name].groupby(level=0).sum() + # TODO(swast): Update groupby to use index based on group by key(s). + pd.testing.assert_series_equal( + pd_series.sort_index(), + bf_series.compute().sort_index(), + ) + + +def test_groupby_level_list_sum(scalars_dfs): + # TODO(tbergeron): Use a non-unique index once that becomes possible in tests + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + if scalars_pandas_df.index.name != "rowindex": + pytest.skip("Require index for groupby level.") + + bf_series = scalars_df[col_name].groupby(level=["rowindex"]).sum() + pd_series = scalars_pandas_df[col_name].groupby(level=["rowindex"]).sum() + # TODO(swast): Update groupby to use index based on group by key(s). + pd.testing.assert_series_equal( + pd_series.sort_index(), + bf_series.compute().sort_index(), + ) + + +def test_groupby_mean(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_series = ( + scalars_df[col_name].groupby(scalars_df["string_col"], dropna=False).mean() + ) + pd_series = ( + scalars_pandas_df[col_name] + .groupby(scalars_pandas_df["string_col"], dropna=False) + .mean() + ) + # TODO(swast): Update groupby to use index based on group by key(s). + bf_result = bf_series.compute() + assert_series_equal_ignoring_order( + pd_series, + bf_result, + ) + + +def test_groupby_prod(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_series = scalars_df[col_name].groupby(scalars_df["int64_col"]).prod() + pd_series = ( + scalars_pandas_df[col_name].groupby(scalars_pandas_df["int64_col"]).prod() + ) + # TODO(swast): Update groupby to use index based on group by key(s). + bf_result = bf_series.compute() + assert_series_equal_ignoring_order( + pd_series, + bf_result, + ) + + +@pytest.mark.parametrize( + ("operator"), + [ + (lambda x: x.cumsum()), + (lambda x: x.cumcount()), + (lambda x: x.cummin()), + (lambda x: x.cummax()), + (lambda x: x.cumprod()), + (lambda x: x.diff()), + (lambda x: x.shift(2)), + (lambda x: x.shift(-2)), + ], + ids=[ + "cumsum", + "cumcount", + "cummin", + "cummax", + "cumprod", + "diff", + "shiftpostive", + "shiftnegative", + ], +) +def test_groupby_window_ops(scalars_df_index, scalars_pandas_df_index, operator): + col_name = "int64_col" + group_key = "int64_too" # has some duplicates values, good for grouping + bf_series = ( + operator(scalars_df_index[col_name].groupby(scalars_df_index[group_key])) + ).compute() + pd_series = operator( + scalars_pandas_df_index[col_name].groupby(scalars_pandas_df_index[group_key]) + ).astype(pd.Int64Dtype()) + pd.testing.assert_series_equal( + pd_series, + bf_series, + ) + + +def test_drop_label(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_col" + bf_series = scalars_df_index[col_name].drop(1).compute() + pd_series = scalars_pandas_df_index[col_name].drop(1) + pd.testing.assert_series_equal( + pd_series, + bf_series, + ) + + +def test_drop_label_list(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_col" + bf_series = scalars_df_index[col_name].drop([1, 3]).compute() + pd_series = scalars_pandas_df_index[col_name].drop([1, 3]) + pd.testing.assert_series_equal( + pd_series, + bf_series, + ) + + +@pytest.mark.parametrize( + ("col_name",), + [ + ("bool_col",), + ("int64_too",), + ], +) +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + (False,), + ], +) +def test_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep, col_name): + bf_series = scalars_df_index[col_name].drop_duplicates(keep=keep).compute() + pd_series = scalars_pandas_df_index[col_name].drop_duplicates(keep=keep) + pd.testing.assert_series_equal( + pd_series, + bf_series, + ) + + +@pytest.mark.parametrize( + ("col_name",), + [ + ("bool_col",), + ("int64_too",), + ], +) +def test_unique(scalars_df_index, scalars_pandas_df_index, col_name): + bf_uniq = scalars_df_index[col_name].unique().to_numpy() + pd_uniq = scalars_pandas_df_index[col_name].unique() + numpy.array_equal(pd_uniq, bf_uniq) + + +@pytest.mark.parametrize( + ("col_name",), + [ + ("bool_col",), + ("int64_too",), + ], +) +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + (False,), + ], +) +def test_duplicated(scalars_df_index, scalars_pandas_df_index, keep, col_name): + bf_series = scalars_df_index[col_name].duplicated(keep=keep).compute() + pd_series = scalars_pandas_df_index[col_name].duplicated(keep=keep) + pd.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) + + +def test_shape(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"].shape + pd_result = scalars_pandas_df["string_col"].shape + + assert pd_result == bf_result + + +def test_len(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = len(scalars_df["string_col"]) + pd_result = len(scalars_pandas_df["string_col"]) + + assert pd_result == bf_result + + +def test_size(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"].size + pd_result = scalars_pandas_df["string_col"].size + + assert pd_result == bf_result + + +def test_empty_false(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"].empty + pd_result = scalars_pandas_df["string_col"].empty + + assert pd_result == bf_result + + +def test_empty_true(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"][ + scalars_df["string_col"] == "won't find this" + ].empty + pd_result = scalars_pandas_df["string_col"][ + scalars_pandas_df["string_col"] == "won't find this" + ].empty + + assert pd_result == bf_result + + +def test_dtype(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"].dtype + pd_result = scalars_pandas_df["string_col"].dtype + + assert pd_result == bf_result + + +def test_dtypes(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["int64_col"].dtypes + pd_result = scalars_pandas_df["int64_col"].dtypes + + assert pd_result == bf_result + + +def test_head(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + if scalars_df.index.name is None: + pytest.skip("Require explicit index for offset ops.") + + bf_result = scalars_df["string_col"].head(2).compute() + pd_result = scalars_pandas_df["string_col"].head(2) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_tail(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + if scalars_df.index.name is None: + pytest.skip("Require explicit index for offset ops.") + + bf_result = scalars_df["string_col"].tail(2).compute() + pd_result = scalars_pandas_df["string_col"].tail(2) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_head_then_scalar_operation(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + if scalars_df.index.name is None: + pytest.skip("Require explicit index for offset ops.") + + bf_result = (scalars_df["float64_col"].head(1) + 4).compute() + pd_result = scalars_pandas_df["float64_col"].head(1) + 4 + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_head_then_series_operation(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + if scalars_df.index.name is None: + pytest.skip("Require explicit index for offset ops.") + + bf_result = ( + scalars_df["float64_col"].head(4) + scalars_df["float64_col"].head(2) + ).compute() + pd_result = scalars_pandas_df["float64_col"].head(4) + scalars_pandas_df[ + "float64_col" + ].head(2) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_shift(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_col" + bf_result = scalars_df_index[col_name].shift().compute() + # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA + pd_result = scalars_pandas_df_index[col_name].shift().astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_cumsum_int(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("1."): + pytest.skip("Series.cumsum NA mask are different in pandas 1.x.") + + col_name = "int64_col" + bf_result = scalars_df_index[col_name].cumsum().compute() + # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA + pd_result = scalars_pandas_df_index[col_name].cumsum().astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("na_option",), + [ + ("keep",), + ("top",), + ("bottom",), + ], +) +@pytest.mark.parametrize( + ("method",), + [ + ("average",), + ("min",), + ("max",), + ("first",), + ("dense",), + ], +) +@pytest.mark.skipif( + True, reason="Blocked by possible pandas rank() regression (b/283278923)" +) +def test_rank_with_nulls(scalars_df_index, scalars_pandas_df_index, na_option, method): + col_name = "bool_col" + bf_result = ( + scalars_df_index[col_name].rank(na_option=na_option, method=method).compute() + ) + pd_result = ( + scalars_pandas_df_index[col_name] + .rank(na_option=na_option, method=method) + .astype(pd.Float64Dtype()) + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + ("all",), + ], +) +def test_nlargest(scalars_df_index, scalars_pandas_df_index, keep): + col_name = "bool_col" + bf_result = scalars_df_index[col_name].nlargest(4, keep=keep).compute() + pd_result = scalars_pandas_df_index[col_name].nlargest(4, keep=keep) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + ("all",), + ], +) +def test_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): + col_name = "bool_col" + bf_result = scalars_df_index[col_name].nsmallest(2, keep=keep).compute() + pd_result = scalars_pandas_df_index[col_name].nsmallest(2, keep=keep) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_rank_ints(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_too" + bf_result = scalars_df_index[col_name].rank().compute() + pd_result = scalars_pandas_df_index[col_name].rank().astype(pd.Float64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_cumsum_nested(scalars_df_index, scalars_pandas_df_index): + col_name = "float64_col" + bf_result = scalars_df_index[col_name].cumsum().cumsum().cumsum().compute() + # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA + pd_result = ( + scalars_pandas_df_index[col_name] + .cumsum() + .cumsum() + .cumsum() + .astype(pd.Float64Dtype()) + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_cumsum_int_filtered(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_col" + + bf_col = scalars_df_index[col_name] + bf_result = bf_col[bf_col > -2].cumsum().compute() + + pd_col = scalars_pandas_df_index[col_name] + # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA + pd_result = pd_col[pd_col > -2].cumsum().astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_cumsum_float(scalars_df_index, scalars_pandas_df_index): + col_name = "float64_col" + bf_result = scalars_df_index[col_name].cumsum().compute() + # cumsum does not behave well on nullable floats in pandas, produces object type and never ignores NA + pd_result = scalars_pandas_df_index[col_name].cumsum().astype(pd.Float64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_cummin_int(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_col" + bf_result = scalars_df_index[col_name].cummin().compute() + pd_result = scalars_pandas_df_index[col_name].cummin() + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_cummax_int(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_col" + bf_result = scalars_df_index[col_name].cummax().compute() + pd_result = scalars_pandas_df_index[col_name].cummax() + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_value_counts(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + + bf_result = scalars_df[col_name].value_counts().compute() + pd_result = scalars_pandas_df[col_name].value_counts() + + # Older pandas version may not have these values, bigframes tries to emulate 2.0+ + pd_result.name = "count" + pd_result.index.name = col_name + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_iloc_nested(scalars_df_index, scalars_pandas_df_index): + + bf_result = scalars_df_index["string_col"].iloc[1:].iloc[1:].compute() + pd_result = scalars_pandas_df_index["string_col"].iloc[1:].iloc[1:] + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("start", "stop", "step"), + [ + (1, None, None), + (None, 4, None), + (None, None, 2), + (None, 50000000000, 1), + (5, 4, None), + (3, None, 2), + (1, 7, 2), + (1, 7, 50000000000), + (-1, -7, -2), + (None, -7, -2), + (-1, None, -2), + (-7, -1, 2), + (-7, -1, None), + (-7, 7, None), + (7, -7, -2), + ], +) +def test_series_iloc(scalars_df_index, scalars_pandas_df_index, start, stop, step): + bf_result = scalars_df_index["string_col"].iloc[start:stop:step].compute() + pd_result = scalars_pandas_df_index["string_col"].iloc[start:stop:step] + + # Pandas may assign non-object dtype to empty series and series index + if pd_result.empty: + pd_result = pd_result.astype("object") + pd_result.index = pd_result.index.astype("object") + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_series_add_prefix(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].add_prefix("prefix_").compute() + + pd_result = scalars_pandas_df_index["int64_too"].add_prefix("prefix_") + + # Index will be object type in pandas, string type in bigframes, but same values + pd.testing.assert_series_equal( + bf_result, + pd_result, + check_index_type=False, + ) + + +def test_series_add_suffix(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].add_suffix("_suffix").compute() + + pd_result = scalars_pandas_df_index["int64_too"].add_suffix("_suffix") + + # Index will be object type in pandas, string type in bigframes, but same values + pd.testing.assert_series_equal( + bf_result, + pd_result, + check_index_type=False, + ) + + +def test_where_with_series(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_col"] + .where(scalars_df_index["bool_col"], scalars_df_index["int64_too"]) + .compute() + ) + pd_result = scalars_pandas_df_index["int64_col"].where( + scalars_pandas_df_index["bool_col"], scalars_pandas_df_index["int64_too"] + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_where_with_different_indices(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_col"] + .iloc[::2] + .where( + scalars_df_index["bool_col"].iloc[2:], + scalars_df_index["int64_too"].iloc[:5], + ) + .compute() + ) + pd_result = ( + scalars_pandas_df_index["int64_col"] + .iloc[::2] + .where( + scalars_pandas_df_index["bool_col"].iloc[2:], + scalars_pandas_df_index["int64_too"].iloc[:5], + ) + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_where_with_default(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_col"].where(scalars_df_index["bool_col"]).compute() + ) + pd_result = scalars_pandas_df_index["int64_col"].where( + scalars_pandas_df_index["bool_col"] + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_clip(scalars_df_index, scalars_pandas_df_index): + col_bf = scalars_df_index["int64_col"] + lower_bf = scalars_df_index["int64_too"] - 1 + upper_bf = scalars_df_index["int64_too"] + 1 + bf_result = col_bf.clip(lower_bf, upper_bf).compute() + + col_pd = scalars_pandas_df_index["int64_col"] + lower_pd = scalars_pandas_df_index["int64_too"] - 1 + upper_pd = scalars_pandas_df_index["int64_too"] + 1 + pd_result = col_pd.clip(lower_pd, upper_pd) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_clip_filtered_two_sided(scalars_df_index, scalars_pandas_df_index): + col_bf = scalars_df_index["int64_col"].iloc[::2] + lower_bf = scalars_df_index["int64_too"].iloc[2:] - 1 + upper_bf = scalars_df_index["int64_too"].iloc[:5] + 1 + bf_result = col_bf.clip(lower_bf, upper_bf).compute() + + col_pd = scalars_pandas_df_index["int64_col"].iloc[::2] + lower_pd = scalars_pandas_df_index["int64_too"].iloc[2:] - 1 + upper_pd = scalars_pandas_df_index["int64_too"].iloc[:5] + 1 + pd_result = col_pd.clip(lower_pd, upper_pd) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_clip_filtered_one_sided(scalars_df_index, scalars_pandas_df_index): + col_bf = scalars_df_index["int64_col"].iloc[::2] + lower_bf = scalars_df_index["int64_too"].iloc[2:] - 1 + bf_result = col_bf.clip(lower_bf, None).compute() + + col_pd = scalars_pandas_df_index["int64_col"].iloc[::2] + lower_pd = scalars_pandas_df_index["int64_too"].iloc[2:] - 1 + pd_result = col_pd.clip(lower_pd, None) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_dot(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["int64_too"] @ scalars_df["int64_too"] + + pd_result = scalars_pandas_df["int64_too"] @ scalars_pandas_df["int64_too"] + + assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("left", "right", "inclusive"), + [ + (-234892, 55555, "left"), + (-234892, 55555, "both"), + (-234892, 55555, "neither"), + (-234892, 55555, "right"), + ], +) +def test_between(scalars_df_index, scalars_pandas_df_index, left, right, inclusive): + bf_result = scalars_df_index["int64_col"].between(left, right, inclusive).compute() + pd_result = scalars_pandas_df_index["int64_col"].between(left, right, inclusive) + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.BooleanDtype()), + ) + + +def test_to_frame(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["int64_col"].to_frame().compute() + pd_result = scalars_pandas_df["int64_col"].to_frame() + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_to_json(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_col"].to_json() + pd_result = scalars_pandas_df_index["int64_col"].to_json() + + assert bf_result == pd_result + + +def test_to_csv(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_col"].to_csv() + pd_result = scalars_pandas_df_index["int64_col"].to_csv() + + assert bf_result == pd_result + + +def test_to_latex(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_col"].to_latex() + pd_result = scalars_pandas_df_index["int64_col"].to_latex() + + assert bf_result == pd_result + + +def test_to_dict(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].to_dict() + + pd_result = scalars_pandas_df_index["int64_too"].to_dict() + + assert bf_result == pd_result + + +def test_to_excel(scalars_df_index, scalars_pandas_df_index): + bf_result_file = tempfile.TemporaryFile() + pd_result_file = tempfile.TemporaryFile() + scalars_df_index["int64_too"].to_excel(bf_result_file) + scalars_pandas_df_index["int64_too"].to_excel(pd_result_file) + bf_result = bf_result_file.read() + pd_result = bf_result_file.read() + + assert bf_result == pd_result + + +def test_to_pickle(scalars_df_index, scalars_pandas_df_index): + bf_result_file = tempfile.TemporaryFile() + pd_result_file = tempfile.TemporaryFile() + scalars_df_index["int64_too"].to_pickle(bf_result_file) + scalars_pandas_df_index["int64_too"].to_pickle(pd_result_file) + bf_result = bf_result_file.read() + pd_result = bf_result_file.read() + + assert bf_result == pd_result + + +def test_to_string(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].to_string() + + pd_result = scalars_pandas_df_index["int64_too"].to_string() + + assert bf_result == pd_result + + +def test_to_list(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].to_list() + + pd_result = scalars_pandas_df_index["int64_too"].to_list() + + assert bf_result == pd_result + + +def test_to_numpy(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].to_numpy() + + pd_result = scalars_pandas_df_index["int64_too"].to_numpy() + + assert (bf_result == pd_result).all() + + +def test_to_xarray(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].to_xarray() + + pd_result = scalars_pandas_df_index["int64_too"].to_xarray() + + assert bf_result.equals(pd_result) + + +def test_to_markdown(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].to_markdown() + + pd_result = scalars_pandas_df_index["int64_too"].to_markdown() + + assert bf_result == pd_result + + +def test_series_values(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].values + + pd_result = scalars_pandas_df_index["int64_too"].values + # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe + pd.testing.assert_series_equal( + pd.Series(bf_result), pd.Series(pd_result), check_dtype=False + ) + + +def test_series___array__(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["float64_col"].__array__() + + pd_result = scalars_pandas_df_index["float64_col"].__array__() + # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe + numpy.array_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("ascending", "na_position"), + [ + (True, "first"), + (True, "last"), + (False, "first"), + (False, "last"), + ], +) +def test_sort_values(scalars_df_index, scalars_pandas_df_index, ascending, na_position): + # Test needs values to be unique + bf_result = ( + scalars_df_index["int64_col"] + .sort_values(ascending=ascending, na_position=na_position) + .compute() + ) + pd_result = scalars_pandas_df_index["int64_col"].sort_values( + ascending=ascending, na_position=na_position + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("ascending"), + [ + (True,), + (False,), + ], +) +def test_sort_index(scalars_df_index, scalars_pandas_df_index, ascending): + bf_result = scalars_df_index["int64_too"].sort_index(ascending=ascending).compute() + pd_result = scalars_pandas_df_index["int64_too"].sort_index(ascending=ascending) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_mask_default_value(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_col = scalars_df["int64_col"] + bf_col_masked = bf_col.mask(bf_col % 2 == 1) + bf_result = bf_col.to_frame().assign(int64_col_masked=bf_col_masked).compute() + + pd_col = scalars_pandas_df["int64_col"] + pd_col_masked = pd_col.mask(pd_col % 2 == 1) + pd_result = pd_col.to_frame().assign(int64_col_masked=pd_col_masked) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_mask_custom_value(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_col = scalars_df["int64_col"] + bf_col_masked = bf_col.mask(bf_col % 2 == 1, -1) + bf_result = bf_col.to_frame().assign(int64_col_masked=bf_col_masked).compute() + + pd_col = scalars_pandas_df["int64_col"] + pd_col_masked = pd_col.mask(pd_col % 2 == 1, -1) + pd_result = pd_col.to_frame().assign(int64_col_masked=pd_col_masked) + + # TODO(shobs): There is a pd.NA value in the original series, which is not + # odd so should be left as is, but it is being masked in pandas. + # Accidentally the bigframes bahavior matches, but it should be updated + # after the resolution of https://github.com/pandas-dev/pandas/issues/52955 + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("column", "to_type"), + [ + ("int64_col", "Float64"), + ("int64_col", "Int64"), # No-op + ("int64_col", pd.Float64Dtype()), + ("int64_col", "string[pyarrow]"), + ("int64_col", "boolean"), + ("bool_col", "Int64"), + ("bool_col", "string[pyarrow]"), + # pandas actually doesn't let folks convert to/from naive timestamp and + # raises a deprecation warning to use tz_localize/tz_convert instead, + # but BigQuery always stores values as UTC and doesn't have to deal + # with timezone conversions, so we'll allow it. + ("timestamp_col", pd.ArrowDtype(pa.timestamp("us"))), + ("datetime_col", pd.ArrowDtype(pa.timestamp("us", tz="UTC"))), + # TODO(bmil): fix Ibis bug: BigQuery backend rounds to nearest int + # ("float64_col", "Int64"), + # TODO(bmil): decide whether to fix Ibis bug: BigQuery backend + # formats floats with no decimal places if they have no fractional + # part, and does not switch to scientific notation for > 10^15 + # ("float64_col", "string[pyarrow]") + # TODO(bmil): add any other compatible conversions per + # https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions + ], +) +def test_astype(scalars_df_index, scalars_pandas_df_index, column, to_type): + bf_result = scalars_df_index[column].astype(to_type).compute() + pd_result = scalars_pandas_df_index[column].astype(to_type) + pd.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + "index", + [0, 5], +) +def test_iloc_single_integer(scalars_df_index, scalars_pandas_df_index, index): + bf_result = scalars_df_index.string_col.iloc[index] + pd_result = scalars_pandas_df_index.string_col.iloc[index] + + assert bf_result == pd_result + + +def test_iloc_single_integer_out_of_bound_error( + scalars_df_index, scalars_pandas_df_index +): + with pytest.raises(IndexError, match="single positional indexer is out-of-bounds"): + scalars_df_index.string_col.iloc[99] + + +def test_loc_bool_series_explicit_index(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.string_col.loc[scalars_df_index.bool_col].compute() + pd_result = scalars_pandas_df_index.string_col.loc[scalars_pandas_df_index.bool_col] + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_loc_bool_series_default_index( + scalars_df_default_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_default_index.string_col.loc[ + scalars_df_default_index.bool_col + ].compute() + pd_result = scalars_pandas_df_default_index.string_col.loc[ + scalars_pandas_df_default_index.bool_col + ] + + assert_pandas_df_equal_ignore_ordering( + bf_result.to_frame(), + pd_result.to_frame(), + ) + + +def test_argmin(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.string_col.argmin() + pd_result = scalars_pandas_df_index.string_col.argmin() + assert bf_result == pd_result + + +def test_argmax(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.int64_too.argmax() + pd_result = scalars_pandas_df_index.int64_too.argmax() + assert bf_result == pd_result + + +def test_getattr_not_implemented(scalars_df_index): + with pytest.raises(NotImplementedError): + scalars_df_index.string_col.asof() + + +def test_getattr_attribute_error(scalars_df_index): + with pytest.raises(AttributeError): + scalars_df_index.string_col.not_a_method() + + +def test_rename(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.string_col.rename("newname") + pd_result = scalars_pandas_df_index.string_col.rename("newname") + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_rename_axis(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.string_col.rename_axis("newindexname") + pd_result = scalars_pandas_df_index.string_col.rename_axis("newindexname") + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_list_string_index(scalars_df_index, scalars_pandas_df_index): + index_list = scalars_pandas_df_index.string_col.iloc[[0, 1, 1, 5]].values + + scalars_df_index = scalars_df_index.set_index("string_col", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index( + "string_col", drop=False + ) + + bf_result = scalars_df_index.string_col.loc[index_list] + pd_result = scalars_pandas_df_index.string_col.loc[index_list] + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): + index_list = [3, 2, 1, 3, 2, 1] + + bf_result = scalars_df_index.bool_col.loc[index_list] + pd_result = scalars_pandas_df_index.bool_col.loc[index_list] + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_iloc_list(scalars_df_index, scalars_pandas_df_index): + index_list = [0, 0, 0, 5, 4, 7] + + bf_result = scalars_df_index.string_col.iloc[index_list] + pd_result = scalars_pandas_df_index.string_col.iloc[index_list] + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_iloc_list_nameless(scalars_df_index, scalars_pandas_df_index): + index_list = [0, 0, 0, 5, 4, 7] + + bf_series = scalars_df_index.string_col.rename(None) + bf_result = bf_series.iloc[index_list] + pd_series = scalars_pandas_df_index.string_col.rename(None) + pd_result = pd_series.iloc[index_list] + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_list_nameless(scalars_df_index, scalars_pandas_df_index): + index_list = [0, 0, 0, 5, 4, 7] + + bf_series = scalars_df_index.string_col.rename(None) + bf_result = bf_series.loc[index_list] + + pd_series = scalars_pandas_df_index.string_col.rename(None) + pd_result = pd_series.loc[index_list] + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): + pd_string_series = scalars_pandas_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + bf_string_series = scalars_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + + scalars_df_index = scalars_df_index.set_index("string_col") + scalars_pandas_df_index = scalars_pandas_df_index.set_index("string_col") + + bf_result = scalars_df_index.date_col.loc[bf_string_series] + pd_result = scalars_pandas_df_index.date_col.loc[pd_string_series] + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): + pd_index = scalars_pandas_df_index.iloc[[0, 5, 1, 1, 5]].index + bf_index = scalars_df_index.iloc[[0, 5, 1, 1, 5]].index + + bf_result = scalars_df_index.date_col.loc[bf_index] + pd_result = scalars_pandas_df_index.date_col.loc[pd_index] + + pd.testing.assert_series_equal( + bf_result.compute(), + pd_result, + ) + + +def test_series_bool_interpretation_error(scalars_df_index): + with pytest.raises(ValueError): + True if scalars_df_index["string_col"] else False diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py new file mode 100644 index 00000000000..31ab4dee121 --- /dev/null +++ b/tests/system/small/test_session.py @@ -0,0 +1,673 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random +import tempfile +import typing +from typing import List + +import google.api_core.exceptions +import numpy as np +import pandas as pd +import pytest + +import bigframes +import bigframes.core.indexes.index +import bigframes.dataframe +import bigframes.dtypes +import bigframes.ml.linear_model + + +def test_read_gbq_tokyo( + session_tokyo: bigframes.Session, + scalars_table_tokyo: str, + scalars_pandas_df_index: pd.DataFrame, + tokyo_location: str, +): + df = session_tokyo.read_gbq(scalars_table_tokyo, index_col=["rowindex"]) + result = df.sort_index().compute() + expected = scalars_pandas_df_index + + _, query_job = df._block.expr.start_query() + assert query_job.location == tokyo_location + + pd.testing.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + ("query_or_table", "col_order"), + [ + pytest.param( + "{scalars_table_id}", ["bool_col", "int64_col"], id="two_cols_in_table" + ), + pytest.param( + """SELECT + t.float64_col * 2 AS my_floats, + CONCAT(t.string_col, "_2") AS my_strings, + t.int64_col > 0 AS my_bools, + FROM `{scalars_table_id}` AS t + """, + ["my_strings"], + id="one_cols_in_query", + ), + pytest.param( + "{scalars_table_id}", + ["unknown"], + marks=pytest.mark.xfail( + raises=ValueError, + reason="Column `unknown` not found in this table.", + ), + id="unknown_col", + ), + ], +) +def test_read_gbq_w_col_order( + session: bigframes.Session, + scalars_table_id: str, + query_or_table: str, + col_order: List[str], +): + df = session.read_gbq( + query_or_table.format(scalars_table_id=scalars_table_id), col_order=col_order + ) + assert df.columns.tolist() == col_order + + +@pytest.mark.parametrize( + ("query_or_table", "index_col"), + [ + pytest.param("{scalars_table_id}", ["bool_col", "int64_col"], id="multiindex"), + pytest.param( + """SELECT + t.float64_col * 2 AS my_floats, + CONCAT(t.string_col, "_2") AS my_strings, + t.int64_col > 0 AS my_bools, + FROM `{scalars_table_id}` AS t + """, + ["my_strings"], + id="string_index", + ), + pytest.param( + "{scalars_table_id}", + ["bool_col"], + id="non_unique_index", + ), + ], +) +def test_read_gbq_w_index_col( + session: bigframes.Session, + scalars_table_id: str, + query_or_table: str, + index_col: List[str], +): + df = session.read_gbq( + query_or_table.format(scalars_table_id=scalars_table_id), + index_col=index_col, + ) + assert list(df.index.names) == index_col + + +@pytest.mark.parametrize( + ("query_or_table", "max_results"), + [ + pytest.param("{scalars_table_id}", 2, id="two_rows_in_table"), + pytest.param( + """SELECT + t.float64_col * 2 AS my_floats, + CONCAT(t.string_col, "_2") AS my_strings, + t.int64_col > 0 AS my_bools, + FROM `{scalars_table_id}` AS t + """, + 2, + id="three_rows_in_query", + ), + pytest.param( + "{scalars_table_id}", + -1, + marks=pytest.mark.xfail( + raises=ValueError, + reason="`max_results` should be a positive number.", + ), + id="neg_rows", + ), + ], +) +def test_read_gbq_w_max_results( + session: bigframes.Session, + scalars_table_id: str, + query_or_table: str, + max_results: int, +): + df = session.read_gbq( + query_or_table.format(scalars_table_id=scalars_table_id), + max_results=max_results, + ) + bf_result = df.compute() + assert bf_result.shape[0] == max_results + + +def test_read_gbq_w_script(session, dataset_id: str): + ddl = f""" + CREATE TABLE `{dataset_id}.test_read_gbq_w_ddl` ( + `col_a` INT64, + `col_b` STRING + ); + + INSERT INTO `{dataset_id}.test_read_gbq_w_ddl` + VALUES (123, 'hello world'); + """ + df = session.read_gbq(ddl).to_pandas() + assert df["statement_type"][0] == "SCRIPT" + + +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) + + +def test_read_pandas(session, scalars_dfs): + _, scalars_pandas_df = scalars_dfs + + df = session.read_pandas(scalars_pandas_df) + assert df._block._expr._ordering is not None + + result = df.compute() + expected = scalars_pandas_df + + pd.testing.assert_frame_equal(result, expected) + + +def test_read_pandas_multi_index(session, scalars_pandas_df_multi_index): + df = session.read_pandas(scalars_pandas_df_multi_index) + result = df.compute() + pd.testing.assert_frame_equal(result, scalars_pandas_df_multi_index) + + +def test_read_pandas_rowid_exists_adds_suffix(session, scalars_pandas_df_default_index): + scalars_pandas_df_default_index["rowid"] = np.arange( + scalars_pandas_df_default_index.shape[0] + ) + + df = session.read_pandas(scalars_pandas_df_default_index) + assert df._block._expr._ordering.ordering_id == "rowid_2" + + +def test_read_pandas_tokyo( + session_tokyo: bigframes.Session, + scalars_pandas_df_index: pd.DataFrame, + tokyo_location: str, +): + df = session_tokyo.read_pandas(scalars_pandas_df_index) + result = df.compute() + expected = scalars_pandas_df_index + + _, query_job = df._block.expr.start_query() + assert query_job.location == tokyo_location + + pd.testing.assert_frame_equal(result, expected) + + +def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): + scalars_df, _ = scalars_dfs + if scalars_df.index.name is not None: + path = gcs_folder + "test_read_csv_gcs_default_engine_w_index.csv" + else: + path = gcs_folder + "test_read_csv_gcs_default_engine_wo_index.csv" + scalars_df.to_csv(path, index=False) + dtype = scalars_df.dtypes.to_dict() + dtype.pop("geography_col") + df = session.read_csv( + path, + # Convert default pandas dtypes to match BigQuery DataFrames dtypes. + dtype=dtype, + ) + assert df._block._expr._ordering is not None + + # TODO(chelsealin): If we serialize the index, can more easily compare values. + pd.testing.assert_index_equal(df.columns, scalars_df.columns) + + # The auto detects of BigQuery load job have restrictions to detect the bytes, + # numeric and geometry types, so they're skipped here. + df = df.drop(columns=["bytes_col", "numeric_col", "geography_col"]) + scalars_df = scalars_df.drop(columns=["bytes_col", "numeric_col", "geography_col"]) + assert df.shape[0] == scalars_df.shape[0] + pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) + + +def test_read_csv_gcs_bq_engine(session, scalars_dfs, gcs_folder): + scalars_df, _ = scalars_dfs + if scalars_df.index.name is not None: + path = gcs_folder + "test_read_csv_gcs_bq_engine_w_index.csv" + else: + path = gcs_folder + "test_read_csv_gcs_bq_engine_wo_index.csv" + scalars_df.to_csv(path, index=False) + df = session.read_csv(path, engine="bigquery") + + # TODO(chelsealin): If we serialize the index, can more easily compare values. + pd.testing.assert_index_equal(df.columns, scalars_df.columns) + + # The auto detects of BigQuery load job have restrictions to detect the bytes, + # datetime, numeric and geometry types, so they're skipped here. + df = df.drop(columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"]) + scalars_df = scalars_df.drop( + columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"] + ) + assert df.shape[0] == scalars_df.shape[0] + pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) + + +@pytest.mark.parametrize( + "sep", + [ + pytest.param(",", id="default_sep"), + pytest.param("\t", id="custom_sep"), + ], +) +def test_read_csv_local_default_engine(session, scalars_dfs, sep): + scalars_df, scalars_pandas_df = scalars_dfs + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_default_engine.csv" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df.to_csv(path, index=False, sep=sep) + dtype = scalars_df.dtypes.to_dict() + dtype.pop("geography_col") + df = session.read_csv( + path, + sep=sep, + # Convert default pandas dtypes to match BigQuery DataFrames dtypes. + dtype=dtype, + ) + assert df._block._expr._ordering is not None + + # TODO(chelsealin): If we serialize the index, can more easily compare values. + pd.testing.assert_index_equal(df.columns, scalars_df.columns) + + # The auto detects of BigQuery load job have restrictions to detect the bytes, + # numeric and geometry types, so they're skipped here. + df = df.drop(columns=["bytes_col", "numeric_col", "geography_col"]) + scalars_df = scalars_df.drop( + columns=["bytes_col", "numeric_col", "geography_col"] + ) + assert df.shape[0] == scalars_df.shape[0] + pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) + + +@pytest.mark.parametrize( + "sep", + [ + pytest.param(",", id="default_sep"), + pytest.param("\t", id="custom_sep"), + ], +) +def test_read_csv_local_bq_engine(session, scalars_dfs, sep): + scalars_df, scalars_pandas_df = scalars_dfs + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_bq_engine.csv" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df.to_csv(path, index=False, sep=sep) + df = session.read_csv(path, engine="bigquery", sep=sep) + + # TODO(chelsealin): If we serialize the index, can more easily compare values. + pd.testing.assert_index_equal(df.columns, scalars_df.columns) + + # The auto detects of BigQuery load job have restrictions to detect the bytes, + # datetime, numeric and geometry types, so they're skipped here. + df = df.drop( + columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"] + ) + scalars_df = scalars_df.drop( + columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"] + ) + assert df.shape[0] == scalars_df.shape[0] + pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) + + +def test_read_csv_localbuffer_bq_engine(session, scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_bq_engine.csv" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df.to_csv(path, index=False) + with open(path, "rb") as buffer: + df = session.read_csv(buffer, engine="bigquery") + + # TODO(chelsealin): If we serialize the index, can more easily compare values. + pd.testing.assert_index_equal(df.columns, scalars_df.columns) + + # The auto detects of BigQuery load job have restrictions to detect the bytes, + # datetime, numeric and geometry types, so they're skipped here. + df = df.drop( + columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"] + ) + scalars_df = scalars_df.drop( + columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"] + ) + assert df.shape[0] == scalars_df.shape[0] + pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) + + +@pytest.mark.parametrize( + ("kwargs", "match"), + [ + pytest.param( + {"engine": "bigquery", "names": []}, + "BigQuery engine does not support these arguments", + id="with_names", + ), + pytest.param( + {"engine": "bigquery", "dtype": {}}, + "BigQuery engine does not support these arguments", + id="with_dtype", + ), + pytest.param( + {"engine": "bigquery", "index_col": False}, + "BigQuery engine only supports a single column name for `index_col`.", + id="with_index_col_false", + ), + pytest.param( + {"engine": "bigquery", "index_col": 5}, + "BigQuery engine only supports a single column name for `index_col`.", + id="with_index_col_not_str", + ), + pytest.param( + {"engine": "bigquery", "usecols": [1, 2]}, + "BigQuery engine only supports an iterable of strings for `usecols`.", + id="with_usecols_invalid", + ), + pytest.param( + {"engine": "bigquery", "encoding": "ASCII"}, + "BigQuery engine only supports the following encodings", + id="with_encoding_invalid", + ), + ], +) +def test_read_csv_bq_engine_throws_not_implemented_error(session, kwargs, match): + with pytest.raises(NotImplementedError, match=match): + session.read_csv("", **kwargs) + + +@pytest.mark.parametrize( + ("kwargs", "match"), + [ + pytest.param( + {"chunksize": 5}, + "'chunksize' and 'iterator' arguments are not supported.", + id="with_chunksize", + ), + pytest.param( + {"iterator": True}, + "'chunksize' and 'iterator' arguments are not supported.", + id="with_iterator", + ), + ], +) +def test_read_csv_default_engine_throws_not_implemented_error( + session, + scalars_df_index, + gcs_folder, + kwargs, + match, +): + path = ( + gcs_folder + "test_read_csv_gcs_default_engine_throws_not_implemented_error.csv" + ) + scalars_df_index.to_csv(path) + with pytest.raises(NotImplementedError, match=match): + session.read_csv(path, **kwargs) + + +def test_read_csv_gcs_default_engine_w_header(session, scalars_df_index, gcs_folder): + path = gcs_folder + "test_read_csv_gcs_default_engine_w_header.csv" + scalars_df_index.to_csv(path) + + # Skips header=N rows, normally considers the N+1th row as the header, but overridden by + # passing the `names` argument. In this case, pandas will skip the N+1th row too, take + # the column names from `names`, and begin reading data from the N+2th row. + df = session.read_csv( + path, + header=2, + names=scalars_df_index.columns.to_list(), + ) + assert df.shape[0] == scalars_df_index.shape[0] - 2 + assert len(df.columns) == len(scalars_df_index.columns) + + +def test_read_csv_gcs_bq_engine_w_header(session, scalars_df_index, gcs_folder): + path = gcs_folder + "test_read_csv_gcs_bq_engine_w_header.csv" + scalars_df_index.to_csv(path, index=False) + + # Skip the header and the first 2 data rows. Without provided schema, the column names + # would be like `bool_field_0`, `string_field_1` and etc. + df = session.read_csv(path, header=2, engine="bigquery") + assert df.shape[0] == scalars_df_index.shape[0] - 2 + assert len(df.columns) == len(scalars_df_index.columns) + + +def test_read_csv_local_default_engine_w_header(session, scalars_pandas_df_index): + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_default_engine_w_header.csv" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df_index.to_csv(path, index=False) + + # Skips header=N rows. Normally row N+1 would be the header now, but overridden by + # passing the `names` argument. In this case, pandas will skip row N+1 too, infer + # the column names from `names`, and begin reading data from row N+2. + df = session.read_csv( + path, + header=2, + names=scalars_pandas_df_index.columns.to_list(), + ) + assert df.shape[0] == scalars_pandas_df_index.shape[0] - 2 + assert len(df.columns) == len(scalars_pandas_df_index.columns) + + +def test_read_csv_local_bq_engine_w_header(session, scalars_pandas_df_index): + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_bq_engine_w_header.csv" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df_index.to_csv(path, index=False) + + # Skip the header and the first 2 data rows. Without provided schema, the column names + # would be like `bool_field_0`, `string_field_1` and etc. + df = session.read_csv(path, header=2, engine="bigquery") + assert df.shape[0] == scalars_pandas_df_index.shape[0] - 2 + assert len(df.columns) == len(scalars_pandas_df_index.columns) + + +def test_read_csv_gcs_default_engine_w_index_col_name( + session, scalars_df_default_index, gcs_folder +): + path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_name.csv" + scalars_df_default_index.to_csv(path) + + df = session.read_csv(path, index_col="rowindex") + scalars_df_default_index = scalars_df_default_index.set_index( + "rowindex" + ).sort_index() + pd.testing.assert_index_equal(df.columns, scalars_df_default_index.columns) + assert df.index.name == "rowindex" + + +def test_read_csv_gcs_default_engine_w_index_col_index( + session, scalars_df_default_index, gcs_folder +): + path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_index.csv" + scalars_df_default_index.to_csv(path) + + index_col = scalars_df_default_index.columns.to_list().index("rowindex") + df = session.read_csv(path, index_col=index_col) + scalars_df_default_index = scalars_df_default_index.set_index( + "rowindex" + ).sort_index() + pd.testing.assert_index_equal(df.columns, scalars_df_default_index.columns) + assert df.index.name == "rowindex" + + +def test_read_csv_local_default_engine_w_index_col_name( + session, scalars_pandas_df_default_index +): + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_default_engine_w_index_col_name" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df_default_index.to_csv(path, index=False) + + df = session.read_csv(path, index_col="rowindex") + scalars_pandas_df_default_index = scalars_pandas_df_default_index.set_index( + "rowindex" + ).sort_index() + pd.testing.assert_index_equal( + df.columns, scalars_pandas_df_default_index.columns + ) + assert df.index.name == "rowindex" + + +def test_read_csv_local_default_engine_w_index_col_index( + session, scalars_pandas_df_default_index +): + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_default_engine_w_index_col_index" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df_default_index.to_csv(path, index=False) + + index_col = scalars_pandas_df_default_index.columns.to_list().index("rowindex") + df = session.read_csv(path, index_col=index_col) + scalars_pandas_df_default_index = scalars_pandas_df_default_index.set_index( + "rowindex" + ).sort_index() + pd.testing.assert_index_equal( + df.columns, scalars_pandas_df_default_index.columns + ) + assert df.index.name == "rowindex" + + +@pytest.mark.parametrize( + "engine", + [ + pytest.param("bigquery", id="bq_engine"), + pytest.param(None, id="default_engine"), + ], +) +def test_read_csv_gcs_w_usecols(session, scalars_df_index, gcs_folder, engine): + path = gcs_folder + "test_read_csv_gcs_w_usecols" + path = path + "_default_engine.csv" if engine is None else path + "_bq_engine.csv" + scalars_df_index.to_csv(path) + + # df should only have 1 column which is bool_col. + df = session.read_csv(path, usecols=["bool_col"], engine=engine) + assert len(df.columns) == 1 + + +@pytest.mark.parametrize( + "engine", + [ + pytest.param("bigquery", id="bq_engine"), + pytest.param(None, id="default_engine"), + ], +) +def test_read_csv_local_w_usecols(session, scalars_pandas_df_index, engine): + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_w_usecols.csv" + # Using the pandas to_csv method because the BQ one does not support local write. + scalars_pandas_df_index.to_csv(path, index=False) + + # df should only have 1 column which is bool_col. + df = session.read_csv(path, usecols=["bool_col"], engine=engine) + assert len(df.columns) == 1 + + +@pytest.mark.parametrize( + "engine", + [ + pytest.param("bigquery", id="bq_engine"), + pytest.param(None, id="default_engine"), + ], +) +def test_read_csv_local_w_encoding(session, penguins_pandas_df_default_index, engine): + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_local_w_encoding.csv" + # Using the pandas to_csv method because the BQ one does not support local write. + penguins_pandas_df_default_index.to_csv( + path, index=False, encoding="ISO-8859-1" + ) + + # File can only be read using the same character encoding as when written. + df = session.read_csv(path, engine=engine, encoding="ISO-8859-1") + + # TODO(chelsealin): If we serialize the index, can more easily compare values. + pd.testing.assert_index_equal( + df.columns, penguins_pandas_df_default_index.columns + ) + + assert df.shape[0] == penguins_pandas_df_default_index.shape[0] + + +def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): + scalars_df, _ = scalars_dfs + # Include wildcard so that multiple files can be written/read if > 1 GB. + # https://cloud.google.com/bigquery/docs/exporting-data#exporting_data_into_one_or_more_files + path = gcs_folder + "test_read_parquet_gcs*.parquet" + df_in: bigframes.dataframe.DataFrame = scalars_df.copy() + # GEOGRAPHY not supported in parquet export. + df_in = df_in.drop(columns="geography_col") + # Make sure we can also serialize the order. + df_write = df_in.reset_index(drop=False) + df_write.index.name = f"ordering_id_{random.randrange(1_000_000)}" + df_write.to_parquet(path, index=True) + + df_out = ( + session.read_parquet(path) + # Restore order. + .set_index(df_write.index.name).sort_index() + # Restore index. + .set_index(typing.cast(str, df_in.index.name)) + ) + + # DATETIME gets loaded as TIMESTAMP in parquet. See: + # https://cloud.google.com/bigquery/docs/exporting-data#parquet_export_details + df_out = df_out.assign( + datetime_col=df_out["datetime_col"].astype("timestamp[us][pyarrow]") + ) + + # Make sure we actually have at least some values before comparing. + assert df_out.size != 0 + pd_df_in = df_in.to_pandas() + pd_df_out = df_out.to_pandas() + pd.testing.assert_frame_equal(pd_df_in, pd_df_out) + + +def test_session_id(session): + assert session._session_id is not None + + # BQ client always runs query within the opened session. + query_job = session.bqclient.query("SELECT 1") + assert query_job.session_info.session_id == session._session_id + + # TODO(chelsealin): Verify the session id can be binded with a load job. + + +def test_session_dataset_exists_and_configured(session: bigframes.Session): + dataset = session.bqclient.get_dataset(session._session_dataset_id) + assert dataset.default_table_expiration_ms == 24 * 60 * 60 * 1000 + + +@pytest.mark.flaky(retries=2) +def test_to_close_session(): + session = bigframes.Session() + assert session._session_id is not None + session.close() + assert session._session_id is None + + # Session has expired and is no longer available. + with pytest.raises(google.api_core.exceptions.BadRequest): + query_job = session.bqclient.query("SELECT 1") + query_job.result() # blocks until finished diff --git a/tests/system/small/test_window.py b/tests/system/small/test_window.py new file mode 100644 index 00000000000..34b56fcd730 --- /dev/null +++ b/tests/system/small/test_window.py @@ -0,0 +1,66 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + + +@pytest.mark.parametrize( + ("windowing"), + [ + (lambda x: x.expanding()), + (lambda x: x.rolling(3, min_periods=3)), + (lambda x: x.groupby(x % 2).rolling(3, min_periods=3)), + (lambda x: x.groupby(x % 3).expanding(min_periods=2)), + ], + ids=[ + "expanding", + "rolling", + "rollinggroupby", + "expandinggroupby", + ], +) +@pytest.mark.parametrize( + ("agg_op"), + [ + (lambda x: x.sum()), + (lambda x: x.min()), + (lambda x: x.max()), + (lambda x: x.mean()), + (lambda x: x.count()), + (lambda x: x.std()), + (lambda x: x.var()), + ], + ids=[ + "sum", + "min", + "max", + "mean", + "count", + "std", + "var", + ], +) +def test_window_agg_ops(scalars_df_index, scalars_pandas_df_index, windowing, agg_op): + col_name = "int64_too" + bf_series = agg_op(windowing(scalars_df_index[col_name])).compute() + pd_series = agg_op(windowing(scalars_pandas_df_index[col_name])) + + # Pandas always converts to float64, even for min/max/count, which is not desired + pd_series = pd_series.astype(bf_series.dtype) + + pd.testing.assert_series_equal( + pd_series, + bf_series, + ) diff --git a/tests/system/utils.py b/tests/system/utils.py new file mode 100644 index 00000000000..e2daf3b8bf0 --- /dev/null +++ b/tests/system/utils.py @@ -0,0 +1,141 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import decimal + +import geopandas as gpd # type: ignore +import numpy as np +import pandas as pd +import pyarrow as pa # type: ignore + + +def assert_pandas_df_equal_ignore_ordering(df0, df1, **kwargs): + # Sort by a column to get consistent results. + if df0.index.name != "rowindex": + df0 = df0.sort_values( + list(df0.columns.drop("geography_col", errors="ignore")) + ).reset_index(drop=True) + df1 = df1.sort_values( + list(df1.columns.drop("geography_col", errors="ignore")) + ).reset_index(drop=True) + else: + df0 = df0.sort_index() + df1 = df1.sort_index() + + pd.testing.assert_frame_equal(df0, df1, **kwargs) + + +def assert_series_equal_ignoring_order(left: pd.Series, right: pd.Series, **kwargs): + if left.index.name is None: + left = left.sort_values().reset_index(drop=True) + right = right.sort_values().reset_index(drop=True) + else: + left = left.sort_index() + right = right.sort_index() + + pd.testing.assert_series_equal(left, right, **kwargs) + + +def _standardize_index(idx): + return pd.Index(list(idx), name=idx.name) + + +def assert_pandas_index_equal_ignore_index_type(idx0, idx1): + idx0 = _standardize_index(idx0) + idx1 = _standardize_index(idx1) + + pd.testing.assert_index_equal(idx0, idx1) + + +def convert_pandas_dtypes(df: pd.DataFrame, bytes_col: bool): + """Convert pandas dataframe dtypes compatible with bigframes dataframe.""" + + # TODO(chelsealin): updates the function to accept dtypes as input rather than + # hard-code the column names here. + + # Convert basic types columns + df["bool_col"] = df["bool_col"].astype(pd.BooleanDtype()) + df["int64_col"] = df["int64_col"].astype(pd.Int64Dtype()) + df["int64_too"] = df["int64_too"].astype(pd.Int64Dtype()) + df["float64_col"] = df["float64_col"].astype(pd.Float64Dtype()) + df["string_col"] = df["string_col"].astype(pd.StringDtype(storage="pyarrow")) + + if "rowindex" in df.columns: + df["rowindex"] = df["rowindex"].astype(pd.Int64Dtype()) + if "rowindex_2" in df.columns: + df["rowindex_2"] = df["rowindex_2"].astype(pd.Int64Dtype()) + + # Convert time types columns. The `astype` works for Pandas 2.0 but hits an assert + # error at Pandas 1.5. Hence, we have to convert to arrow table and convert back + # to pandas dataframe. + if not isinstance(df["date_col"].dtype, pd.ArrowDtype): + df["date_col"] = pd.to_datetime(df["date_col"], format="%Y-%m-%d") + arrow_table = pa.Table.from_pandas( + pd.DataFrame(df, columns=["date_col"]), + schema=pa.schema([("date_col", pa.date32())]), + ) + df["date_col"] = arrow_table.to_pandas(types_mapper=pd.ArrowDtype)["date_col"] + + if not isinstance(df["datetime_col"].dtype, pd.ArrowDtype): + df["datetime_col"] = pd.to_datetime( + df["datetime_col"], format="%Y-%m-%d %H:%M:%S" + ) + arrow_table = pa.Table.from_pandas( + pd.DataFrame(df, columns=["datetime_col"]), + schema=pa.schema([("datetime_col", pa.timestamp("us"))]), + ) + df["datetime_col"] = arrow_table.to_pandas(types_mapper=pd.ArrowDtype)[ + "datetime_col" + ] + + if not isinstance(df["time_col"].dtype, pd.ArrowDtype): + df["time_col"] = pd.to_datetime(df["time_col"], format="%H:%M:%S.%f") + arrow_table = pa.Table.from_pandas( + pd.DataFrame(df, columns=["time_col"]), + schema=pa.schema([("time_col", pa.time64("us"))]), + ) + df["time_col"] = arrow_table.to_pandas(types_mapper=pd.ArrowDtype)["time_col"] + + if not isinstance(df["timestamp_col"].dtype, pd.ArrowDtype): + df["timestamp_col"] = pd.to_datetime( + df["timestamp_col"], format="%Y-%m-%d %H:%M:%S.%f%Z" + ) + arrow_table = pa.Table.from_pandas( + pd.DataFrame(df, columns=["timestamp_col"]), + schema=pa.schema([("timestamp_col", pa.timestamp("us", tz="UTC"))]), + ) + df["timestamp_col"] = arrow_table.to_pandas(types_mapper=pd.ArrowDtype)[ + "timestamp_col" + ] + + # Convert geography types columns. + if "geography_col" in df.columns: + df["geography_col"] = df["geography_col"].astype( + pd.StringDtype(storage="pyarrow") + ) + df["geography_col"] = gpd.GeoSeries.from_wkt( + df["geography_col"].replace({np.nan: None}) + ) + + # Convert bytes types column. + if bytes_col: + df["bytes_col"] = df["bytes_col"].apply( + lambda value: base64.b64decode(value) if not pd.isnull(value) else value + ) + + # Convert numeric types column. + df["numeric_col"] = df["numeric_col"].apply( + lambda value: decimal.Decimal(str(value)) if value else None # type: ignore + ) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/_config/__init__.py b/tests/unit/_config/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/unit/_config/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py new file mode 100644 index 00000000000..43b5663bf75 --- /dev/null +++ b/tests/unit/_config/test_bigquery_options.py @@ -0,0 +1,73 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +import pytest + +import bigframes._config.bigquery_options as bigquery_options + + +@pytest.mark.parametrize( + ["attribute", "original_value", "new_value"], + [ + # For credentials, the match is by reference. + ("credentials", object(), object()), + ("location", "us-east1", "us-central1"), + ("project", "my-project", "my-other-project"), + ("remote_udf_connection", "path/to/connection/1", "path/to/connection/2"), + ], +) +def test_setter_raises_if_session_started(attribute, original_value, new_value): + options = bigquery_options.BigQueryOptions() + setattr(options, attribute, original_value) + assert getattr(options, attribute) is original_value + assert getattr(options, attribute) is not new_value + + options._session_started = True + expected_message = re.escape( + bigquery_options.SESSION_STARTED_MESSAGE.format(attribute=attribute) + ) + with pytest.raises(ValueError, match=expected_message): + setattr(options, attribute, new_value) + + assert getattr(options, attribute) is original_value + assert getattr(options, attribute) is not new_value + + +@pytest.mark.parametrize( + [ + "attribute", + ], + [ + (attribute,) + for attribute in [ + "credentials", + "location", + "project", + "remote_udf_connection", + ] + ], +) +def test_setter_if_session_started_but_setting_the_same_value(attribute): + options = bigquery_options.BigQueryOptions() + original_object = object() + setattr(options, attribute, original_object) + assert getattr(options, attribute) is original_object + + # This should work fine since we're setting the same value as before. + options._session_started = True + setattr(options, attribute, original_object) + + assert getattr(options, attribute) is original_object diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 00000000000..dcf2d918a5e --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,223 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Callable, Optional, Tuple, Union +from unittest import mock + +import google.api_core.exceptions +import google.auth +import google.cloud.bigquery as bigquery +import google.cloud.bigquery.table +import google.oauth2.credentials # type: ignore +import ibis.expr.types as ibis_types +import pandas +import pytest + +import bigframes +import bigframes.core +import bigframes.dataframe + +SCALARS_TABLE_ID = "project.dataset.scalars_table" + + +@pytest.fixture +def scalars_pandas_df_default_index() -> pandas.DataFrame: + # Note: as of 2023-02-07, using nullable dtypes with the ibis pandas + # backend requires running ibis at HEAD. See: + # https://github.com/ibis-project/ibis/pull/5345 + return pandas.DataFrame( + { + "rowindex": pandas.Series( + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + ], + dtype="Int64", + ), + "bool_col": pandas.Series( + [ + True, + None, + False, + True, + None, + False, + True, + None, + False, + True, + ], + dtype="boolean", + ), + "int64_col": pandas.Series( + [ + 1, + 2, + 3, + None, + 0, + -1, + -2, + 2**63 - 1, + -(2**63), + None, + ], + dtype="Int64", + ), + "float64_col": pandas.Series( + [ + None, + 1, + math.pi, + math.e * 1e10, + 0, + float("nan"), + float("inf"), + float("-inf"), + -2.23e-308, + 1.8e308, + ], + dtype="Float64", + ), + "string_col": pandas.Series( + [ + "abc", + "XYZ", + "aBcDeFgHiJkLmNoPqRsTuVwXyZ", + "1_2-3+4=5~6*7/8&9%10#11@12$" "", + None, + "こんにちは", + "你好", + "வணக்கம்", + "שלום", + ], + dtype="string[pyarrow]", + ), + } + ) + + +# We parameterize the fixtures at this point with the real pandas +# dataframes and deferred bigframes dataframes as we have the following +# chain of dependencies: +# -> index/default_index parameterization +# -> pandas dataframe +# -> bqclient mock +# -> session +# -> bigframes dataframe +@pytest.fixture +def scalars_testdata_setup( + scalars_pandas_df_default_index, +) -> Tuple[ + pandas.DataFrame, Callable[[bigframes.Session], bigframes.dataframe.DataFrame] +]: + return ( + scalars_pandas_df_default_index.set_index("rowindex"), + lambda session: session.read_gbq(SCALARS_TABLE_ID, index_col=["rowindex"]), + ) + + +@pytest.fixture(autouse=True) +def mock_bigquery_client(monkeypatch, scalars_testdata_setup) -> bigquery.Client: + scalars_pandas_df, _ = scalars_testdata_setup + mock_client = mock.create_autospec(bigquery.Client) + # Constructor returns the mock itself, so this mock can be treated as the + # constructor or the instance. + mock_client.return_value = mock_client + mock_client.project = "default-project" + most_recent_table = None + + def mock_bigquery_client_get_table( + table_ref: Union[google.cloud.bigquery.table.TableReference, str] + ): + global most_recent_table + + if isinstance(table_ref, google.cloud.bigquery.table.TableReference): + table_name = table_ref.__str__() + else: + table_name = table_ref + + schema = [ + {"mode": "NULLABLE", "name": "rowindex", "type": "INTEGER"}, + { + "mode": "NULLABLE", + "name": "bigframes_ordering_id", + "type": "INTEGER", + }, + ] + + if table_name == SCALARS_TABLE_ID: + schema += [ + {"mode": "NULLABLE", "name": "bool_col", "type": "BOOL"}, + {"mode": "NULLABLE", "name": "int64_col", "type": "INTEGER"}, + {"mode": "NULLABLE", "name": "float64_col", "type": "FLOAT"}, + {"mode": "NULLABLE", "name": "string_col", "type": "STRING"}, + ] + else: + raise google.api_core.exceptions.NotFound("Not Found Table") + + most_recent_table = bigquery.Table(table_name, schema) # type: ignore + return most_recent_table # type: ignore + + def mock_query( + sql: str, + job_config: Optional[bigquery.QueryJobConfig] = None, + location: str = "US", + ) -> bigquery.QueryJob: + global most_recent_table + + def mock_result(max_results=None): + mock_rows = mock.create_autospec(google.cloud.bigquery.table.RowIterator) + mock_rows.total_rows = len(scalars_pandas_df.index) + mock_rows.schema = [ + bigquery.SchemaField(name=name, field_type="INT64") + for name in scalars_pandas_df.columns + ] + # Use scalars_pandas_df instead of ibis_expr.execute() to preserve dtypes. + mock_rows.to_dataframe.return_value = scalars_pandas_df.head(n=max_results) + return mock_rows + + mock_job = mock.create_autospec(bigquery.QueryJob) + mock_job.result = mock_result + return mock_job + + mock_client.get_table = mock_bigquery_client_get_table + mock_client.query.side_effect = mock_query + monkeypatch.setattr(bigquery, "Client", mock_client) + mock_client.reset_mock() + return mock_client + + +@pytest.fixture +def session() -> bigframes.Session: + return bigframes.Session( + context=bigframes.BigQueryOptions( + credentials=mock.create_autospec(google.oauth2.credentials.Credentials), + project="unit-test-project", + ) + ) + + +@pytest.fixture +def scalars_ibis_table(session) -> ibis_types.Table: + return session.ibis_client.table(SCALARS_TABLE_ID) diff --git a/tests/unit/ml/__init__.py b/tests/unit/ml/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/unit/ml/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/ml/test_api_primitives.py b/tests/unit/ml/test_api_primitives.py new file mode 100644 index 00000000000..da77a180a8d --- /dev/null +++ b/tests/unit/ml/test_api_primitives.py @@ -0,0 +1,53 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import sklearn.decomposition as sklearn_decomposition # type: ignore +import sklearn.linear_model as sklearn_linear_model # type: ignore + +import bigframes.ml.decomposition +import bigframes.ml.linear_model + + +def test_base_estimator_repr(): + estimator = bigframes.ml.linear_model.LinearRegression() + assert estimator.__repr__() == "LinearRegression()" + + estimator = bigframes.ml.linear_model.LinearRegression(fit_intercept=False) + assert estimator.__repr__() == "LinearRegression(fit_intercept=False)" + + estimator = bigframes.ml.linear_model.LinearRegression(fit_intercept=True) + assert estimator.__repr__() == "LinearRegression()" + + estimator = bigframes.ml.decomposition.PCA(n_components=7) + assert estimator.__repr__() == "PCA(n_components=7)" + + +@pytest.mark.skipif(sklearn_linear_model is None, reason="requires sklearn") +def test_base_estimator_repr_matches_sklearn(): + estimator = bigframes.ml.linear_model.LinearRegression() + sklearn_estimator = sklearn_linear_model.LinearRegression() + assert estimator.__repr__() == sklearn_estimator.__repr__() + + estimator = bigframes.ml.linear_model.LinearRegression(fit_intercept=False) + sklearn_estimator = sklearn_linear_model.LinearRegression(fit_intercept=False) + assert estimator.__repr__() == sklearn_estimator.__repr__() + + estimator = bigframes.ml.linear_model.LinearRegression(fit_intercept=True) + sklearn_estimator = sklearn_linear_model.LinearRegression(fit_intercept=True) + assert estimator.__repr__() == sklearn_estimator.__repr__() + + estimator = bigframes.ml.decomposition.PCA(n_components=7) + sklearn_estimator = sklearn_decomposition.PCA(n_components=7) + assert estimator.__repr__() == sklearn_estimator.__repr__() diff --git a/tests/unit/ml/test_compose.py b/tests/unit/ml/test_compose.py new file mode 100644 index 00000000000..772a148c95f --- /dev/null +++ b/tests/unit/ml/test_compose.py @@ -0,0 +1,95 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import sklearn.compose as sklearn_compose # type: ignore +import sklearn.preprocessing as sklearn_preprocessing # type: ignore + +import bigframes.ml.compose +import bigframes.ml.preprocessing + + +def test_columntransformer_init_expectedtransforms(): + onehot_transformer = bigframes.ml.preprocessing.OneHotEncoder() + scaler_transformer = bigframes.ml.preprocessing.StandardScaler() + column_transformer = bigframes.ml.compose.ColumnTransformer( + [ + ("onehot", onehot_transformer, "species"), + ("scale", scaler_transformer, ["culmen_length_mm", "flipper_length_mm"]), + ] + ) + + assert column_transformer.transformers_ == [ + ("onehot", onehot_transformer, "species"), + ("scale", scaler_transformer, "culmen_length_mm"), + ("scale", scaler_transformer, "flipper_length_mm"), + ] + + +def test_columntransformer_repr(): + column_transformer = bigframes.ml.compose.ColumnTransformer( + [ + ( + "onehot", + bigframes.ml.preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + bigframes.ml.preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ) + + assert ( + column_transformer.__repr__() + == """ColumnTransformer(transformers=[('onehot', OneHotEncoder(), 'species'), + ('scale', StandardScaler(), + ['culmen_length_mm', 'flipper_length_mm'])])""" + ) + + +@pytest.mark.skipif(sklearn_compose is None, reason="requires sklearn") +def test_columntransformer_repr_matches_sklearn(): + bf_column_transformer = bigframes.ml.compose.ColumnTransformer( + [ + ( + "onehot", + bigframes.ml.preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + bigframes.ml.preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ) + sk_column_transformer = sklearn_compose.ColumnTransformer( + [ + ( + "onehot", + sklearn_preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + sklearn_preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ) + + assert bf_column_transformer.__repr__() == sk_column_transformer.__repr__() diff --git a/tests/unit/ml/test_pipeline.py b/tests/unit/ml/test_pipeline.py new file mode 100644 index 00000000000..27706a1a07f --- /dev/null +++ b/tests/unit/ml/test_pipeline.py @@ -0,0 +1,110 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import sklearn.compose as sklearn_compose # type: ignore +import sklearn.linear_model as sklearn_linear_model # type: ignore +import sklearn.pipeline as sklearn_pipeline # type: ignore +import sklearn.preprocessing as sklearn_preprocessing # type: ignore + +import bigframes.ml.compose +import bigframes.ml.linear_model +import bigframes.ml.pipeline +import bigframes.ml.preprocessing + + +def test_pipeline_repr(): + pipeline = bigframes.ml.pipeline.Pipeline( + [ + ( + "preproc", + bigframes.ml.compose.ColumnTransformer( + [ + ( + "onehot", + bigframes.ml.preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + bigframes.ml.preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ), + ), + ("linreg", bigframes.ml.linear_model.LinearRegression()), + ] + ) + + assert ( + pipeline.__repr__() + == """Pipeline(steps=[('preproc', + ColumnTransformer(transformers=[('onehot', OneHotEncoder(), + 'species'), + ('scale', StandardScaler(), + ['culmen_length_mm', + 'flipper_length_mm'])])), + ('linreg', LinearRegression())])""" + ) + + +@pytest.mark.skipif(sklearn_pipeline is None, reason="requires sklearn") +def test_pipeline_repr_matches_sklearn(): + bf_pipeline = bigframes.ml.pipeline.Pipeline( + [ + ( + "preproc", + bigframes.ml.compose.ColumnTransformer( + [ + ( + "onehot", + bigframes.ml.preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + bigframes.ml.preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ), + ), + ("linreg", bigframes.ml.linear_model.LinearRegression()), + ] + ) + sk_pipeline = sklearn_pipeline.Pipeline( + [ + ( + "preproc", + sklearn_compose.ColumnTransformer( + [ + ( + "onehot", + sklearn_preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + sklearn_preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ), + ), + ("linreg", sklearn_linear_model.LinearRegression()), + ] + ) + + assert bf_pipeline.__repr__() == sk_pipeline.__repr__() diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py new file mode 100644 index 00000000000..92cb52a6815 --- /dev/null +++ b/tests/unit/ml/test_sql.py @@ -0,0 +1,145 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.ml.sql as ml_sql + + +def test_options_produces_correct_sql(): + sql = ml_sql.options(model_type="lin_reg", input_label_cols=["col_a"], l1_reg=0.6) + assert ( + sql + == """OPTIONS( + model_type="lin_reg", + input_label_cols=["col_a"], + l1_reg=0.6)""" + ) + + +def test_transform_produces_correct_sql(): + sql = ml_sql.transform( + "ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a", + "ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b", + ) + assert ( + sql + == """TRANSFORM( + ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a, + ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b)""" + ) + + +def test_standard_scaler_produces_correct_sql(): + sql = ml_sql.ml_standard_scaler("col_a", "scaled_col_a") + assert sql == "ML.STANDARD_SCALER(col_a) OVER() AS scaled_col_a" + + +def test_one_hot_encoder_produces_correct_sql(): + sql = ml_sql.ml_one_hot_encoder("col_a", "encoded_col_a") + assert sql == "ML.ONE_HOT_ENCODER(col_a) OVER() AS encoded_col_a" + + +def test_create_model_produces_correct_sql(): + sql = ml_sql.create_model( + model_name="my_dataset.my_model", + source_sql="my_source_sql", + options_sql="my_options_sql", + ) + assert ( + sql + == """CREATE MODEL `my_dataset.my_model` +my_options_sql +AS my_source_sql""" + ) + + +def test_create_model_transform_produces_correct_sql(): + sql = ml_sql.create_model( + model_name="my_dataset.my_model", + source_sql="my_source_sql", + options_sql="my_options_sql", + transform_sql="my_transform_sql", + ) + assert ( + sql + == """CREATE MODEL `my_dataset.my_model` +my_transform_sql +my_options_sql +AS my_source_sql""" + ) + + +def test_create_remote_model_produces_correct_sql(): + sql = ml_sql.create_remote_model( + model_name="my_dataset.my_model", + connection_name="my_project.us.my_connection", + options_sql="my_options_sql", + ) + assert ( + sql + == """CREATE MODEL `my_dataset.my_model` +REMOTE WITH CONNECTION `my_project.us.my_connection` +my_options_sql""" + ) + + +def test_alter_model_correct_sql(): + sql = ml_sql.alter_model( + model_name="my_dataset.my_model", + options_sql="my_options_sql", + ) + assert ( + sql + == """ALTER MODEL `my_dataset.my_model` +SET my_options_sql""" + ) + + +def test_ml_predict_produces_correct_sql(): + sql = ml_sql.ml_predict( + model_name="my_dataset.my_model", source_sql="SELECT * FROM my_table" + ) + assert ( + sql + == """SELECT * FROM ML.PREDICT(MODEL `my_dataset.my_model`, + (SELECT * FROM my_table))""" + ) + + +def test_ml_evaluate_produces_correct_sql(): + sql = ml_sql.ml_evaluate( + model_name="my_dataset.my_model", source_sql="SELECT * FROM my_table" + ) + assert ( + sql + == """SELECT * FROM ML.EVALUATE(MODEL `my_dataset.my_model`, + (SELECT * FROM my_table))""" + ) + + +def test_ml_evaluate_no_source_produces_correct_sql(): + sql = ml_sql.ml_evaluate(model_name="my_dataset.my_model") + assert sql == """SELECT * FROM ML.EVALUATE(MODEL `my_dataset.my_model`)""" + + +def test_ml_generate_text_produces_correct_sql(): + sql = ml_sql.ml_generate_text( + model_name="my_dataset.my_model", + source_sql="SELECT * FROM my_table", + struct_options="STRUCT(value AS item)", + ) + assert ( + sql + == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_dataset.my_model`, + (SELECT * FROM my_table), STRUCT(value AS item))""" + ) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py new file mode 100644 index 00000000000..25286189637 --- /dev/null +++ b/tests/unit/test_core.py @@ -0,0 +1,99 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +import ibis +from ibis.expr.types import Column, Table + +from bigframes import core + + +def test_constructor_from_ibis_table_adds_all_columns( + session, scalars_ibis_table: Table +): + actual = core.ArrayValue(session=session, table=scalars_ibis_table) + assert actual._table is scalars_ibis_table + assert len(actual._columns) == len(scalars_ibis_table.columns) + + +def test_builder_doesnt_change_original(session): + mock_table = mock.create_autospec(Table) + mock_column = mock.create_autospec(Column) + original = core.ArrayValue(session=session, table=mock_table, columns=[mock_column]) + assert original._table is mock_table + assert len(original._columns) == 1 + assert original._columns[0] is mock_column + + # Create a new expression from a builder. + builder = original.builder() + new_table = mock.create_autospec(Table) + assert new_table is not mock_table + builder.table = new_table + new_column = mock.create_autospec(Column) + assert new_column is not mock_column + builder.columns.append(new_column) + actual = builder.build() + + # Expected values are present. + assert actual._table is new_table + assert len(actual._columns) == 2 + assert actual._columns[0] is mock_column + assert actual._columns[1] is new_column + # Don't modify the original. + assert original._table is mock_table + assert len(original._columns) == 1 + assert original._columns[0] is mock_column + + +def test_projection_doesnt_change_original(session): + mock_table = mock.create_autospec(Table) + mock_column = mock.create_autospec(Column) + original = core.ArrayValue(session=session, table=mock_table, columns=[mock_column]) + assert original._table is mock_table + assert len(original._columns) == 1 + assert original._columns[0] is mock_column + + # Create a new expression from a projection. + new_column_1 = mock.create_autospec(Column) + new_column_2 = mock.create_autospec(Column) + assert new_column_1 is not mock_column + assert new_column_2 is not mock_column + actual = original.projection([new_column_1, mock_column, new_column_2]) + + # Expected values are present. + assert actual._table is mock_table + assert len(actual._columns) == 3 + assert actual._columns[0] is new_column_1 + assert actual._columns[1] is mock_column + assert actual._columns[2] is new_column_2 + # Don't modify the original. + assert original._table is mock_table + assert len(original._columns) == 1 + assert original._columns[0] is mock_column + + +def test_to_ibis_expr_with_projection(session, scalars_ibis_table: Table): + expr = core.ArrayValue(session=session, table=scalars_ibis_table).projection( + [ + scalars_ibis_table["int64_col"], + ibis.literal(123456789).name("literals"), + scalars_ibis_table["string_col"], + ] + ) + actual = expr.to_ibis_expr() + assert len(actual.columns) == 3 + assert actual.columns[0] == "int64_col" + assert actual.columns[1] == "literals" + assert actual.columns[2] == "string_col" diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py new file mode 100644 index 00000000000..ec49315f0fe --- /dev/null +++ b/tests/unit/test_dtypes.py @@ -0,0 +1,184 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import geopandas as gpd # type: ignore +import ibis +import ibis.expr.datatypes as ibis_dtypes +import numpy as np +import pandas as pd +import pyarrow as pa # type: ignore +import pytest + +import bigframes.dtypes + + +@pytest.mark.parametrize( + ["ibis_dtype", "bigframes_dtype"], + [ + # TODO(bmil): Add ARRAY, INTERVAL, STRUCT to cover all the standard + # BigQuery data types as they appear in Ibis: + # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types + (ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), np.dtype("O")), + (ibis_dtypes.boolean, pd.BooleanDtype()), + (ibis_dtypes.binary, np.dtype("O")), + (ibis_dtypes.date, pd.ArrowDtype(pa.date32())), + (ibis_dtypes.Timestamp(), pd.ArrowDtype(pa.timestamp("us"))), + (ibis_dtypes.float64, pd.Float64Dtype()), + ( + ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True), + gpd.array.GeometryDtype(), + ), + (ibis_dtypes.int64, pd.Int64Dtype()), + (ibis_dtypes.json, np.dtype("O")), + (ibis_dtypes.Decimal(precision=38, scale=9, nullable=True), np.dtype("O")), + (ibis_dtypes.string, pd.StringDtype(storage="pyarrow")), + (ibis_dtypes.time, pd.ArrowDtype(pa.time64("us"))), + ( + ibis_dtypes.Timestamp(timezone="UTC"), + pd.ArrowDtype(pa.timestamp("us", tz="UTC")), # type: ignore + ), + ], + ids=[ + "bignumeric", + "bool", + "bytes", + "date", + "datetime", + "float", + "geography", + "int64", + "json", + "numeric", + "string", + "time", + "timestamp", + ], +) +def test_ibis_dtype_converts(ibis_dtype, bigframes_dtype): + """Test all the Ibis data types needed to read BigQuery tables""" + result = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_dtype) + assert result == bigframes_dtype + + +def test_ibis_timestamp_pst_raises_unexpected_datatype(): + """BigQuery timestamp only supports UTC time""" + with pytest.raises(ValueError, match="Unexpected Ibis data type"): + bigframes.dtypes.ibis_dtype_to_bigframes_dtype( + ibis_dtypes.Timestamp(timezone="PST") + ) + + +def test_ibis_float32_raises_unexpected_datatype(): + """Other Ibis types not read from BigQuery are not expected""" + with pytest.raises(ValueError, match="Unexpected Ibis data type"): + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_dtypes.float32) + + +@pytest.mark.parametrize( + ["bigframes_dtype", "ibis_dtype"], + [ + # This test covers all dtypes that BigQuery DataFrames can exactly map to Ibis + (pd.BooleanDtype(), ibis_dtypes.boolean), + (pd.ArrowDtype(pa.date32()), ibis_dtypes.date), + (pd.ArrowDtype(pa.timestamp("us")), ibis_dtypes.Timestamp()), + (pd.Float64Dtype(), ibis_dtypes.float64), + (pd.Int64Dtype(), ibis_dtypes.int64), + (pd.StringDtype(storage="pyarrow"), ibis_dtypes.string), + (pd.ArrowDtype(pa.time64("us")), ibis_dtypes.time), + ( + pd.ArrowDtype(pa.timestamp("us", tz="UTC")), # type: ignore + ibis_dtypes.Timestamp(timezone="UTC"), + ), + ], + ids=[ + "boolean", + "date", + "datetime", + "float", + "int", + "string", + "time", + "timestamp", + ], +) +def test_bigframes_dtype_converts(ibis_dtype, bigframes_dtype): + """Test all the Ibis data types needed to read BigQuery tables""" + result = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(bigframes_dtype) + assert result == ibis_dtype + + +@pytest.mark.parametrize( + ["bigframes_dtype_str", "ibis_dtype"], + [ + # This test covers all dtypes that BigQuery DataFrames can exactly map to Ibis + ("boolean", ibis_dtypes.boolean), + ("date32[day][pyarrow]", ibis_dtypes.date), + ("timestamp[us][pyarrow]", ibis_dtypes.Timestamp()), + ("Float64", ibis_dtypes.float64), + ("Int64", ibis_dtypes.int64), + ("string[pyarrow]", ibis_dtypes.string), + ("time64[us][pyarrow]", ibis_dtypes.time), + ( + "timestamp[us, tz=UTC][pyarrow]", + ibis_dtypes.Timestamp(timezone="UTC"), + ), + # Special case - "string" is acceptable for "string[pyarrow]" + ("string", ibis_dtypes.string), + ], +) +def test_bigframes_string_dtype_converts(ibis_dtype, bigframes_dtype_str): + """Test all the Ibis data types needed to read BigQuery tables""" + result = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(bigframes_dtype_str) + assert result == ibis_dtype + + +def test_unsupported_dtype_raises_unexpected_datatype(): + """Incompatible dtypes should fail when passed into BigQuery DataFrames""" + with pytest.raises(ValueError, match="Unexpected data type"): + bigframes.dtypes.bigframes_dtype_to_ibis_dtype(np.float32) + + +def test_unsupported_dtype_str_raises_unexpected_datatype(): + """Incompatible dtypes should fail when passed into BigQuery DataFrames""" + with pytest.raises(ValueError, match="Unexpected data type"): + bigframes.dtypes.bigframes_dtype_to_ibis_dtype("int64") + + +@pytest.mark.parametrize( + ["literal", "ibis_scalar"], + [ + (True, ibis.literal(True, ibis_dtypes.boolean)), + (5, ibis.literal(5, ibis_dtypes.int64)), + (-33.2, ibis.literal(-33.2, ibis_dtypes.float64)), + ], +) +def test_literal_to_ibis_scalar_converts(literal, ibis_scalar): + assert bigframes.dtypes.literal_to_ibis_scalar(literal).equals(ibis_scalar) + + +def test_literal_to_ibis_scalar_throws_on_incompatible_literal(): + with pytest.raises( + ValueError, + ): + bigframes.dtypes.literal_to_ibis_scalar({"mykey": "myval"}) + + +def test_remote_function_io_types_are_supported_bigframes_types(): + from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type + + from bigframes.remote_function import _supported_io_types as rf_supported_io_types + + for python_type in rf_supported_io_types: + ibis_type = python_type_to_bigquery_type(python_type) + assert ibis_type in bigframes.dtypes.IBIS_TO_BIGFRAMES diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py new file mode 100644 index 00000000000..3e8e16ee6ab --- /dev/null +++ b/tests/unit/test_pandas.py @@ -0,0 +1,81 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import re +import sys + +import pytest + +import bigframes.pandas +import bigframes.session + +leading_whitespace = re.compile(r"^\s+", flags=re.MULTILINE) + + +def all_session_methods(): + session_attributes = set( + attribute + for attribute in dir(bigframes.session.Session) + if not attribute.startswith("_") + ) + session_attributes.remove("close") + + for attribute in sorted(session_attributes): + session_method = getattr(bigframes.session.Session, attribute) + if not callable(session_method): + continue # pragma: no cover + yield attribute + + +@pytest.mark.parametrize( + [ + "method_name", + ], + [(method_name,) for method_name in all_session_methods()], +) +def test_method_matches_session(method_name: str): + if sys.version_info <= (3, 10): + pytest.skip( + "Need Python 3.10 to reconcile deferred annotations." + ) # pragma: no cover + + session_method = getattr(bigframes.session.Session, method_name) + session_doc = inspect.getdoc(session_method) + assert session_doc is not None, "docstrings are required" + + pandas_method = getattr(bigframes.pandas, method_name) + pandas_doc = inspect.getdoc(pandas_method) + assert pandas_doc is not None, "docstrings are required" + assert re.sub(leading_whitespace, "", pandas_doc) == re.sub( + leading_whitespace, "", session_doc + ) + + # Add `eval_str = True` so that deferred annotations are turned into their + # corresponding type objects. Need Python 3.10 for eval_str parameter. + session_signature = inspect.signature(session_method, eval_str=True) + pandas_signature = inspect.signature(pandas_method, eval_str=True) + assert [ + # Kind includes position, which will be an offset. + parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY) + for parameter in pandas_signature.parameters.values() + ] == [ + # Kind includes position, which will be an offset. + parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY) + for parameter in session_signature.parameters.values() + # Don't include the first parameter, which is `self: Session` + ][ + 1: + ] + assert pandas_signature.return_annotation == session_signature.return_annotation diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py new file mode 100644 index 00000000000..ab573c4c11f --- /dev/null +++ b/tests/unit/test_session.py @@ -0,0 +1,48 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from unittest import mock + +import google.api_core.exceptions +import pytest + +import bigframes + + +@pytest.mark.parametrize("missing_parts_table_id", [(""), ("table")]) +def test_read_gbq_missing_parts(session, missing_parts_table_id): + with pytest.raises(ValueError): + session.read_gbq(missing_parts_table_id) + + +@pytest.mark.parametrize( + "not_found_table_id", + [("unknown.dataset.table"), ("project.unknown.table"), ("project.dataset.unknown")], +) +def test_read_gdb_not_found_tables(session, not_found_table_id): + with pytest.raises(google.api_core.exceptions.NotFound): + session.read_gbq(not_found_table_id) + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_session_init_fails_with_no_project(): + with pytest.raises( + ValueError, match="Project must be set to initialize BigQuery client." + ): + bigframes.Session( + bigframes.BigQueryOptions( + credentials=mock.Mock(spec=google.auth.credentials.Credentials) + ) + ) diff --git a/third_party/__init__.py b/third_party/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/__init__.py b/third_party/bigframes_vendored/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/cpython/LICENSE b/third_party/bigframes_vendored/cpython/LICENSE new file mode 100644 index 00000000000..73935ad7468 --- /dev/null +++ b/third_party/bigframes_vendored/cpython/LICENSE @@ -0,0 +1,10 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 + +1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. +2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright , i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. +3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. +4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. +7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. +8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. diff --git a/third_party/bigframes_vendored/cpython/__init__.py b/third_party/bigframes_vendored/cpython/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/cpython/_pprint.py b/third_party/bigframes_vendored/cpython/_pprint.py new file mode 100644 index 00000000000..617c14df0d9 --- /dev/null +++ b/third_party/bigframes_vendored/cpython/_pprint.py @@ -0,0 +1,470 @@ +"""This module contains the _EstimatorPrettyPrinter class used in +BaseEstimator.__repr__ for pretty-printing estimators""" + +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; +# All Rights Reserved + +# Authors: Fred L. Drake, Jr. (built-in CPython pprint module) +# Nicolas Hug (scikit-learn specific changes) +# Brenton Milne (bigframes specific changes) +# This version is based on the version at sklearn/utils/_pprint.py at 65d42c9 + +# License: PSF License version 2 (see below) + +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- + +# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), +# and the Individual or Organization ("Licensee") accessing and otherwise +# using this software ("Python") in source or binary form and its associated +# documentation. + +# 2. Subject to the terms and conditions of this License Agreement, PSF hereby +# grants Licensee a nonexclusive, royalty-free, world-wide license to +# reproduce, analyze, test, perform and/or display publicly, prepare +# derivative works, distribute, and otherwise use Python alone or in any +# derivative version, provided, however, that PSF's License Agreement and +# PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, +# 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, +# 2017, 2018 Python Software Foundation; All Rights Reserved" are retained in +# Python alone or in any derivative version prepared by Licensee. + +# 3. In the event Licensee prepares a derivative work that is based on or +# incorporates Python or any part thereof, and wants to make the derivative +# work available to others as provided herein, then Licensee hereby agrees to +# include in any such work a brief summary of the changes made to Python. + +# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES +# NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT +# NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF +# MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF +# PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY +# INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF +# MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE +# THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. + +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote products +# or services of Licensee, or any third party. + +# 8. By copying, installing or otherwise using Python, Licensee agrees to be +# bound by the terms and conditions of this License Agreement. + + +# Brief summary of changes to original code: +# - "compact" parameter is supported for dicts, not just lists or tuples +# - estimators have a custom handler, they're not just treated as objects +# - long sequences (lists, tuples, dict items) with more than N elements are +# shortened using ellipsis (', ...') at the end. +# +# Additional changes to adapt this to BigQuery DataFrame: +# - sklearn BaseEstimator replaced with BigQuery DataFrames BaseEstimator +# - removed global get_config, set _changed_only=True +# - replace is_scalar_nan with isinstance(x, numbers.Real) and math.isnan + +from collections import OrderedDict +import inspect +import math +import numbers +import pprint + +from bigframes.ml.base import BaseEstimator + + +class KeyValTuple(tuple): + """Dummy class for correctly rendering key-value tuples from dicts.""" + + def __repr__(self): + # needed for _dispatch[tuple.__repr__] not to be overridden + return super().__repr__() + + +class KeyValTupleParam(KeyValTuple): + """Dummy class for correctly rendering key-value tuples from parameters.""" + + pass + + +def _changed_params(estimator): + """Return dict (param_name: value) of parameters that were given to + estimator with non-default values.""" + + params = estimator.get_params(deep=False) + init_func = getattr(estimator.__init__, "deprecated_original", estimator.__init__) + init_params = inspect.signature(init_func).parameters + init_params = {name: param.default for name, param in init_params.items()} + + def has_changed(k, v): + if k not in init_params: # happens if k is part of a **kwargs + return True + if init_params[k] == inspect._empty: # k has no default value + return True + # try to avoid calling repr on nested estimators + if isinstance(v, BaseEstimator) and v.__class__ != init_params[k].__class__: + return True + # Use repr as a last resort. It may be expensive. + def is_scalar_nan(x): + return isinstance(x, numbers.Real) and math.isnan(x) + + if repr(v) != repr(init_params[k]) and not ( + is_scalar_nan(init_params[k]) and is_scalar_nan(v) + ): + return True + return False + + return {k: v for k, v in params.items() if has_changed(k, v)} + + +class _EstimatorPrettyPrinter(pprint.PrettyPrinter): + """Pretty Printer class for estimator objects. + + This extends the pprint.PrettyPrinter class, because: + - we need estimators to be printed with their parameters, e.g. + Estimator(param1=value1, ...) which is not supported by default. + - the 'compact' parameter of PrettyPrinter is ignored for dicts, which + may lead to very long representations that we want to avoid. + + Quick overview of pprint.PrettyPrinter (see also + https://stackoverflow.com/questions/49565047/pprint-with-hex-numbers): + + - the entry point is the _format() method which calls format() (overridden + here) + - format() directly calls _safe_repr() for a first try at rendering the + object + - _safe_repr formats the whole object recursively, only calling itself, + not caring about line length or anything + - back to _format(), if the output string is too long, _format() then calls + the appropriate _pprint_TYPE() method (e.g. _pprint_list()) depending on + the type of the object. This where the line length and the compact + parameters are taken into account. + - those _pprint_TYPE() methods will internally use the format() method for + rendering the nested objects of an object (e.g. the elements of a list) + + In the end, everything has to be implemented twice: in _safe_repr and in + the custom _pprint_TYPE methods. Unfortunately PrettyPrinter is really not + straightforward to extend (especially when we want a compact output), so + the code is a bit convoluted. + + This class overrides: + - format() to support the changed_only parameter + - _safe_repr to support printing of estimators (for when they fit on a + single line) + - _format_dict_items so that dict are correctly 'compacted' + - _format_items so that ellipsis is used on long lists and tuples + + When estimators cannot be printed on a single line, the builtin _format() + will call _pprint_estimator() because it was registered to do so (see + _dispatch[BaseEstimator.__repr__] = _pprint_estimator). + + both _format_dict_items() and _pprint_estimator() use the + _format_params_or_dict_items() method that will format parameters and + key-value pairs respecting the compact parameter. This method needs another + subroutine _pprint_key_val_tuple() used when a parameter or a key-value + pair is too long to fit on a single line. This subroutine is called in + _format() and is registered as well in the _dispatch dict (just like + _pprint_estimator). We had to create the two classes KeyValTuple and + KeyValTupleParam for this. + """ + + def __init__( + self, + indent=1, + width=80, + depth=None, + stream=None, + *, + compact=False, + indent_at_name=True, + n_max_elements_to_show=None, + ): + super().__init__(indent, width, depth, stream, compact=compact) + self._indent_at_name = indent_at_name + if self._indent_at_name: + self._indent_per_level = 1 # ignore indent param + # Max number of elements in a list, dict, tuple until we start using + # ellipsis. This also affects the number of arguments of an estimators + # (they are treated as dicts) + self.n_max_elements_to_show = n_max_elements_to_show + self._changed_only = True + + def format(self, object, context, maxlevels, level): + return _safe_repr( + object, context, maxlevels, level, changed_only=self._changed_only + ) + + def _pprint_estimator(self, object, stream, indent, allowance, context, level): + stream.write(object.__class__.__name__ + "(") + if self._indent_at_name: + indent += len(object.__class__.__name__) + + params = _changed_params(object) + + params = OrderedDict((name, val) for (name, val) in sorted(params.items())) + + self._format_params( + params.items(), stream, indent, allowance + 1, context, level + ) + stream.write(")") + + def _format_dict_items(self, items, stream, indent, allowance, context, level): + return self._format_params_or_dict_items( + items, stream, indent, allowance, context, level, is_dict=True + ) + + def _format_params(self, items, stream, indent, allowance, context, level): + return self._format_params_or_dict_items( + items, stream, indent, allowance, context, level, is_dict=False + ) + + def _format_params_or_dict_items( + self, object, stream, indent, allowance, context, level, is_dict + ): + """Format dict items or parameters respecting the compact=True + parameter. For some reason, the builtin rendering of dict items doesn't + respect compact=True and will use one line per key-value if all cannot + fit in a single line. + Dict items will be rendered as <'key': value> while params will be + rendered as . The implementation is mostly copy/pasting from + the builtin _format_items(). + This also adds ellipsis if the number of items is greater than + self.n_max_elements_to_show. + """ + write = stream.write + indent += self._indent_per_level + delimnl = ",\n" + " " * indent + delim = "" + width = max_width = self._width - indent + 1 + it = iter(object) + try: + next_ent = next(it) + except StopIteration: + return + last = False + n_items = 0 + while not last: + if n_items == self.n_max_elements_to_show: + write(", ...") + break + n_items += 1 + ent = next_ent + try: + next_ent = next(it) + except StopIteration: + last = True + max_width -= allowance + width -= allowance + if self._compact: + k, v = ent + krepr = self._repr(k, context, level) + vrepr = self._repr(v, context, level) + if not is_dict: + krepr = krepr.strip("'") + middle = ": " if is_dict else "=" + rep = krepr + middle + vrepr + w = len(rep) + 2 + if width < w: + width = max_width + if delim: + delim = delimnl + if width >= w: + width -= w + write(delim) + delim = ", " + write(rep) + continue + write(delim) + delim = delimnl + class_ = KeyValTuple if is_dict else KeyValTupleParam + self._format( + class_(ent), stream, indent, allowance if last else 1, context, level + ) + + def _format_items(self, items, stream, indent, allowance, context, level): + """Format the items of an iterable (list, tuple...). Same as the + built-in _format_items, with support for ellipsis if the number of + elements is greater than self.n_max_elements_to_show. + """ + write = stream.write + indent += self._indent_per_level + if self._indent_per_level > 1: + write((self._indent_per_level - 1) * " ") + delimnl = ",\n" + " " * indent + delim = "" + width = max_width = self._width - indent + 1 + it = iter(items) + try: + next_ent = next(it) + except StopIteration: + return + last = False + n_items = 0 + while not last: + if n_items == self.n_max_elements_to_show: + write(", ...") + break + n_items += 1 + ent = next_ent + try: + next_ent = next(it) + except StopIteration: + last = True + max_width -= allowance + width -= allowance + if self._compact: + rep = self._repr(ent, context, level) + w = len(rep) + 2 + if width < w: + width = max_width + if delim: + delim = delimnl + if width >= w: + width -= w + write(delim) + delim = ", " + write(rep) + continue + write(delim) + delim = delimnl + self._format(ent, stream, indent, allowance if last else 1, context, level) + + def _pprint_key_val_tuple(self, object, stream, indent, allowance, context, level): + """Pretty printing for key-value tuples from dict or parameters.""" + k, v = object + rep = self._repr(k, context, level) + if isinstance(object, KeyValTupleParam): + rep = rep.strip("'") + middle = "=" + else: + middle = ": " + stream.write(rep) + stream.write(middle) + self._format( + v, stream, indent + len(rep) + len(middle), allowance, context, level + ) + + # Note: need to copy _dispatch to prevent instances of the builtin + # PrettyPrinter class to call methods of _EstimatorPrettyPrinter (see issue + # 12906) + # mypy error: "Type[PrettyPrinter]" has no attribute "_dispatch" + _dispatch = pprint.PrettyPrinter._dispatch.copy() # type: ignore + _dispatch[BaseEstimator.__repr__] = _pprint_estimator + _dispatch[KeyValTuple.__repr__] = _pprint_key_val_tuple + + +def _safe_repr(object, context, maxlevels, level, changed_only=False): + """Same as the builtin _safe_repr, with added support for Estimator + objects.""" + typ = type(object) + + if typ in pprint._builtin_scalars: + return repr(object), True, False + + r = getattr(typ, "__repr__", None) + if issubclass(typ, dict) and r is dict.__repr__: + if not object: + return "{}", True, False + objid = id(object) + if maxlevels and level >= maxlevels: + return "{...}", False, objid in context + if objid in context: + return pprint._recursion(object), False, True + context[objid] = 1 + readable = True + recursive = False + components = [] + append = components.append + level += 1 + saferepr = _safe_repr + items = sorted(object.items(), key=pprint._safe_tuple) + for k, v in items: + krepr, kreadable, krecur = saferepr( + k, context, maxlevels, level, changed_only=changed_only + ) + vrepr, vreadable, vrecur = saferepr( + v, context, maxlevels, level, changed_only=changed_only + ) + append("%s: %s" % (krepr, vrepr)) + readable = readable and kreadable and vreadable + if krecur or vrecur: + recursive = True + del context[objid] + return "{%s}" % ", ".join(components), readable, recursive + + if (issubclass(typ, list) and r is list.__repr__) or ( + issubclass(typ, tuple) and r is tuple.__repr__ + ): + if issubclass(typ, list): + if not object: + return "[]", True, False + format = "[%s]" + elif len(object) == 1: + format = "(%s,)" + else: + if not object: + return "()", True, False + format = "(%s)" + objid = id(object) + if maxlevels and level >= maxlevels: + return format % "...", False, objid in context + if objid in context: + return pprint._recursion(object), False, True + context[objid] = 1 + readable = True + recursive = False + components = [] + append = components.append + level += 1 + for o in object: + orepr, oreadable, orecur = _safe_repr( + o, context, maxlevels, level, changed_only=changed_only + ) + append(orepr) + if not oreadable: + readable = False + if orecur: + recursive = True + del context[objid] + return format % ", ".join(components), readable, recursive + + if issubclass(typ, BaseEstimator): + objid = id(object) + if maxlevels and level >= maxlevels: + return "{...}", False, objid in context + if objid in context: + return pprint._recursion(object), False, True + context[objid] = 1 + readable = True + recursive = False + if changed_only: + params = _changed_params(object) + else: + params = object.get_params(deep=False) + components = [] + append = components.append + level += 1 + saferepr = _safe_repr + items = sorted(params.items(), key=pprint._safe_tuple) + for k, v in items: + krepr, kreadable, krecur = saferepr( + k, context, maxlevels, level, changed_only=changed_only + ) + vrepr, vreadable, vrecur = saferepr( + v, context, maxlevels, level, changed_only=changed_only + ) + append("%s=%s" % (krepr.strip("'"), vrepr)) + readable = readable and kreadable and vreadable + if krecur or vrecur: + recursive = True + del context[objid] + return ("%s(%s)" % (typ.__name__, ", ".join(components)), readable, recursive) + + rep = repr(object) + return rep, (rep and not rep.startswith("<")), False diff --git a/third_party/bigframes_vendored/pandas/AUTHORS.md b/third_party/bigframes_vendored/pandas/AUTHORS.md new file mode 100644 index 00000000000..84fcfe05e30 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/AUTHORS.md @@ -0,0 +1,56 @@ +About the Copyright Holders +=========================== + +* Copyright (c) 2008-2011 AQR Capital Management, LLC + + AQR Capital Management began pandas development in 2008. Development was + led by Wes McKinney. AQR released the source under this license in 2009. +* Copyright (c) 2011-2012, Lambda Foundry, Inc. + + Wes is now an employee of Lambda Foundry, and remains the pandas project + lead. +* Copyright (c) 2011-2012, PyData Development Team + + The PyData Development Team is the collection of developers of the PyData + project. This includes all of the PyData sub-projects, including pandas. The + core team that coordinates development on GitHub can be found here: + https://github.com/pydata. + +Full credits for pandas contributors can be found in the documentation. + +Our Copyright Policy +==================== + +PyData uses a shared copyright model. Each contributor maintains copyright +over their contributions to PyData. However, it is important to note that +these contributions are typically only changes to the repositories. Thus, +the PyData source code, in its entirety, is not the copyright of any single +person or institution. Instead, it is the collective copyright of the +entire PyData Development Team. If individual contributors want to maintain +a record of what changes/contributions they have specific copyright on, +they should indicate their copyright in the commit message of the change +when they commit the change to one of the PyData repositories. + +With this in mind, the following banner should be used in any source code +file to indicate the copyright and license terms: + +``` +#----------------------------------------------------------------------------- +# Copyright (c) 2012, PyData Development Team +# All rights reserved. +# +# Distributed under the terms of the BSD Simplified License. +# +# The full license is in the LICENSE file, distributed with this software. +#----------------------------------------------------------------------------- +``` + +Other licenses can be found in the LICENSES directory. + +License +======= + +pandas is distributed under a 3-clause ("Simplified" or "New") BSD +license. Parts of NumPy, SciPy, numpydoc, bottleneck, which all have +BSD-compatible licenses, are included. Their licenses follow the pandas +license. diff --git a/third_party/bigframes_vendored/pandas/LICENSE b/third_party/bigframes_vendored/pandas/LICENSE new file mode 100644 index 00000000000..cdfa749dc34 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/LICENSE @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team +All rights reserved. + +Copyright (c) 2011-2023, Open source contributors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/bigframes_vendored/pandas/README.md b/third_party/bigframes_vendored/pandas/README.md new file mode 100644 index 00000000000..9f2bc800e84 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/README.md @@ -0,0 +1,174 @@ +
+
+
+ +----------------- + +# pandas: powerful Python data analysis toolkit +[![PyPI Latest Release](https://img.shields.io/pypi/v/pandas.svg)](https://pypi.org/project/pandas/) +[![Conda Latest Release](https://anaconda.org/conda-forge/pandas/badges/version.svg)](https://anaconda.org/anaconda/pandas/) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3509134.svg)](https://doi.org/10.5281/zenodo.3509134) +[![Package Status](https://img.shields.io/pypi/status/pandas.svg)](https://pypi.org/project/pandas/) +[![License](https://img.shields.io/pypi/l/pandas.svg)](https://github.com/pandas-dev/pandas/blob/main/LICENSE) +[![Coverage](https://codecov.io/github/pandas-dev/pandas/coverage.svg?branch=main)](https://codecov.io/gh/pandas-dev/pandas) +[![Downloads](https://static.pepy.tech/personalized-badge/pandas?period=month&units=international_system&left_color=black&right_color=orange&left_text=PyPI%20downloads%20per%20month)](https://pepy.tech/project/pandas) +[![Slack](https://img.shields.io/badge/join_Slack-information-brightgreen.svg?logo=slack)](https://pandas.pydata.org/docs/dev/development/community.html?highlight=slack#community-slack) +[![Powered by NumFOCUS](https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A)](https://numfocus.org) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) + +## What is it? + +**pandas** is a Python package that provides fast, flexible, and expressive data +structures designed to make working with "relational" or "labeled" data both +easy and intuitive. It aims to be the fundamental high-level building block for +doing practical, **real world** data analysis in Python. Additionally, it has +the broader goal of becoming **the most powerful and flexible open source data +analysis / manipulation tool available in any language**. It is already well on +its way towards this goal. + +## Main Features +Here are just a few of the things that pandas does well: + + - Easy handling of [**missing data**][missing-data] (represented as + `NaN`, `NA`, or `NaT`) in floating point as well as non-floating point data + - Size mutability: columns can be [**inserted and + deleted**][insertion-deletion] from DataFrame and higher dimensional + objects + - Automatic and explicit [**data alignment**][alignment]: objects can + be explicitly aligned to a set of labels, or the user can simply + ignore the labels and let `Series`, `DataFrame`, etc. automatically + align the data for you in computations + - Powerful, flexible [**group by**][groupby] functionality to perform + split-apply-combine operations on data sets, for both aggregating + and transforming data + - Make it [**easy to convert**][conversion] ragged, + differently-indexed data in other Python and NumPy data structures + into DataFrame objects + - Intelligent label-based [**slicing**][slicing], [**fancy + indexing**][fancy-indexing], and [**subsetting**][subsetting] of + large data sets + - Intuitive [**merging**][merging] and [**joining**][joining] data + sets + - Flexible [**reshaping**][reshape] and [**pivoting**][pivot-table] of + data sets + - [**Hierarchical**][mi] labeling of axes (possible to have multiple + labels per tick) + - Robust IO tools for loading data from [**flat files**][flat-files] + (CSV and delimited), [**Excel files**][excel], [**databases**][db], + and saving/loading data from the ultrafast [**HDF5 format**][hdfstore] + - [**Time series**][timeseries]-specific functionality: date range + generation and frequency conversion, moving window statistics, + date shifting and lagging + + + [missing-data]: https://pandas.pydata.org/pandas-docs/stable/user_guide/missing_data.html + [insertion-deletion]: https://pandas.pydata.org/pandas-docs/stable/user_guide/dsintro.html#column-selection-addition-deletion + [alignment]: https://pandas.pydata.org/pandas-docs/stable/user_guide/dsintro.html?highlight=alignment#intro-to-data-structures + [groupby]: https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html#group-by-split-apply-combine + [conversion]: https://pandas.pydata.org/pandas-docs/stable/user_guide/dsintro.html#dataframe + [slicing]: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#slicing-ranges + [fancy-indexing]: https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html#advanced + [subsetting]: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#boolean-indexing + [merging]: https://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html#database-style-dataframe-or-named-series-joining-merging + [joining]: https://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html#joining-on-index + [reshape]: https://pandas.pydata.org/pandas-docs/stable/user_guide/reshaping.html + [pivot-table]: https://pandas.pydata.org/pandas-docs/stable/user_guide/reshaping.html + [mi]: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#hierarchical-indexing-multiindex + [flat-files]: https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#csv-text-files + [excel]: https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#excel-files + [db]: https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#sql-queries + [hdfstore]: https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#hdf5-pytables + [timeseries]: https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-series-date-functionality + +## Where to get it +The source code is currently hosted on GitHub at: +https://github.com/pandas-dev/pandas + +Binary installers for the latest released version are available at the [Python +Package Index (PyPI)](https://pypi.org/project/pandas) and on [Conda](https://docs.conda.io/en/latest/). + +```sh +# conda +conda install -c conda-forge pandas +``` + +```sh +# or PyPI +pip install pandas +``` + +The list of changes to pandas between each release can be found +[here](https://pandas.pydata.org/pandas-docs/stable/whatsnew/index.html). For full +details, see the commit logs at https://github.com/pandas-dev/pandas. + +## Dependencies +- [NumPy - Adds support for large, multi-dimensional arrays, matrices and high-level mathematical functions to operate on these arrays](https://www.numpy.org) +- [python-dateutil - Provides powerful extensions to the standard datetime module](https://dateutil.readthedocs.io/en/stable/index.html) +- [pytz - Brings the Olson tz database into Python which allows accurate and cross platform timezone calculations](https://github.com/stub42/pytz) + +See the [full installation instructions](https://pandas.pydata.org/pandas-docs/stable/install.html#dependencies) for minimum supported versions of required, recommended and optional dependencies. + +## Installation from sources +To install pandas from source you need [Cython](https://cython.org/) in addition to the normal +dependencies above. Cython can be installed from PyPI: + +```sh +pip install cython +``` + +In the `pandas` directory (same one where you found this file after +cloning the git repo), execute: + +```sh +python setup.py install +``` + +or for installing in [development mode](https://pip.pypa.io/en/latest/cli/pip_install/#install-editable): + + +```sh +python -m pip install -e . --no-build-isolation --no-use-pep517 +``` + +or alternatively + +```sh +python setup.py develop +``` + +See the full instructions for [installing from source](https://pandas.pydata.org/pandas-docs/stable/getting_started/install.html#installing-from-source). + +## License +[BSD 3](LICENSE) + +## Documentation +The official documentation is hosted on PyData.org: https://pandas.pydata.org/pandas-docs/stable + +## Background +Work on ``pandas`` started at [AQR](https://www.aqr.com/) (a quantitative hedge fund) in 2008 and +has been under active development since then. + +## Getting Help + +For usage questions, the best place to go to is [StackOverflow](https://stackoverflow.com/questions/tagged/pandas). +Further, general questions and discussions can also take place on the [pydata mailing list](https://groups.google.com/forum/?fromgroups#!forum/pydata). + +## Discussion and Development +Most development discussions take place on GitHub in this repo. Further, the [pandas-dev mailing list](https://mail.python.org/mailman/listinfo/pandas-dev) can also be used for specialized discussions or design issues, and a [Slack channel](https://pandas.pydata.org/docs/dev/development/community.html?highlight=slack#community-slack) is available for quick development related questions. + +## Contributing to pandas [![Open Source Helpers](https://www.codetriage.com/pandas-dev/pandas/badges/users.svg)](https://www.codetriage.com/pandas-dev/pandas) + +All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. + +A detailed overview on how to contribute can be found in the **[contributing guide](https://pandas.pydata.org/docs/dev/development/contributing.html)**. + +If you are simply looking to start working with the pandas codebase, navigate to the [GitHub "issues" tab](https://github.com/pandas-dev/pandas/issues) and start looking through interesting issues. There are a number of issues listed under [Docs](https://github.com/pandas-dev/pandas/issues?labels=Docs&sort=updated&state=open) and [good first issue](https://github.com/pandas-dev/pandas/issues?labels=good+first+issue&sort=updated&state=open) where you could start out. + +You can also triage issues which may include reproducing bug reports, or asking for vital information such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to pandas on CodeTriage](https://www.codetriage.com/pandas-dev/pandas). + +Or maybe through using pandas you have an idea of your own or are looking for something in the documentation and thinking ‘this can be improved’...you can do something about it! + +Feel free to ask questions on the [mailing list](https://groups.google.com/forum/?fromgroups#!forum/pydata) or on [Slack](https://pandas.pydata.org/docs/dev/development/community.html?highlight=slack#community-slack). + +As contributors and maintainers to this project, you are expected to abide by pandas' code of conduct. More information can be found at: [Contributor Code of Conduct](https://github.com/pandas-dev/.github/blob/master/CODE_OF_CONDUCT.md) diff --git a/third_party/bigframes_vendored/pandas/__init__.py b/third_party/bigframes_vendored/pandas/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py new file mode 100644 index 00000000000..10f5546f108 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -0,0 +1,25 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/config_init.py +""" +This module is imported from the pandas package __init__.py file +in order to ensure that the core.config options registered here will +be available as soon as the user loads the package. if register_option +is invoked inside specific modules, they will not be registered until that +module is imported, which may or may not be a problem. + +If you need to make sure options are available even before a certain +module is imported, register them here rather than in the module. + +""" +from __future__ import annotations + +display_options_doc = """ +Encapsulates configuration for displaying objects. + +Attributes: + max_columns (int): + If max_columns is exceeded, switch to truncate view. + max_rows (int): + If max_rows is exceeded, switch to truncate view. + progress_bar Optional(str): + Determines if progress bars are shown during job runs. Use auto | notebook | terminal. Set to 'None' to remove progress bars +""" diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py new file mode 100644 index 00000000000..93b7c7683cf --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -0,0 +1,1124 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/frame.py +""" +DataFrame +--------- +An efficient 2D container for potentially mixed-type time series or other +labeled data series. + +Similar to its R counterpart, data.frame, except providing automatic data +alignment and a host of useful data manipulation methods having to do with the +labeling information +""" +from __future__ import annotations + +from typing import Iterable, Literal, Mapping, Optional, Sequence, Union + +from third_party.bigframes_vendored.pandas.core.generic import NDFrame + +# ----------------------------------------------------------------------- +# DataFrame class + + +class DataFrame(NDFrame): + """Two-dimensional, size-mutable, potentially heterogeneous tabular data. + + Data structure also contains labeled axes (rows and columns). + Arithmetic operations align on both row and column labels. Can be + thought of as a dict-like container for Series objects. The primary + pandas data structure. + """ + + @property + def shape(self) -> tuple[int, int]: + """Return a tuple representing the dimensionality of the DataFrame.""" + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # IO methods (to / from other formats) + + def to_gbq( + self, + destination_table: str, + *, + if_exists: Optional[Literal["fail", "replace", "append"]] = "fail", + index: bool = True, + ) -> None: + """Write a DataFrame to a Google BigQuery table. + + Args: + destination_table : str + Name of table to be written, in the form ``dataset.tablename`` + or ``project.dataset.tablename``. + + if_exists : str, default 'fail' + Behavior when the destination table exists. Value can be one of: + + ``'fail'`` + If table exists raise pandas_gbq.gbq.TableCreationError. + ``'replace'`` + If table exists, drop it, recreate it, and insert data. + ``'append'`` + If table exists, insert data. Create if does not exist. + + index: whether write row names (index) or not. + """ + raise NotImplementedError("abstract method") + + def to_parquet( + self, + path: str, + *, + index: bool = True, + ) -> None: + """Write a DataFrame to the binary parquet format. + + This function writes the dataframe as a `parquet file + `_ to Google Cloud Storage. + + Args: + path: + Destination URI(s) of GCS files(s) to store the extracted dataframe + in format of ``gs:///``. + If the data size is more than 1GB, you must use a wildcard to export + the data into multiple files and the size of the files varies. + + index : bool, default None + If ``True``, include the dataframe's index(es) in the file output. + If ``False``, they will not be written to the file. + + Returns: + None. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Unsorted + + def assign(self, **kwargs) -> DataFrame: + r""" + Assign new columns to a DataFrame. + + Returns a new object with all original columns in addition to new ones. + Existing columns that are re-assigned will be overwritten. + + Args: + kwargs: + A dictionary of ``{str: values}``. The column names are + keywords. If the values (e.g. a Series, scalar, or array), they + are simply assigned to the column. + + Returns: + A new DataFrame with the new columns in addition to + all the existing columns. + + Notes: + Assigning multiple columns within the same ``assign`` is possible. + Later items in '\*\*kwargs' may refer to newly created or modified + columns in 'df'; items are computed and assigned into 'df' in + order. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Reindexing and alignment + + def drop( + self, + *, + columns: Union[str, Iterable[str]], + ) -> DataFrame | None: + """Drop specified labels from columns. + + Remove columns by directly specifying column names. + + Args: + columns: + Single label or list-like. + + Returns: + DataFrame without the removed column labels. + + Raises: + KeyError: + If any of the labels is not found in the selected axis. + """ + raise NotImplementedError("abstract method") + + def rename( + self, + *, + columns: Mapping, + ) -> DataFrame: + """Rename columns. + + Dict values must be unique (1-to-1). Labels not contained in a dict + will be left as-is. Extra labels listed don't throw an error. + + Args: + columns: + Dict-like from old column labels to new column labels. + + Returns: + DataFrame with the renamed axis labels. + + Raises: + KeyError: + If any of the labels is not found. + """ + raise NotImplementedError("abstract method") + + def rename_axis(self, mapper: Optional[str], **kwargs) -> DataFrame: + """ + Set the name of the axis for the index. + + Note: currently only accepts a single string parameter (the new name of the index) + + Args: + mapper : str + Value to set the axis name attribute. + + Returns: + DataFrame with the new index name + """ + raise NotImplementedError("abstract method") + + def set_index( + self, + keys, + *, + drop: bool = True, + ) -> DataFrame | None: + """ + Set the DataFrame index using existing columns. + + Set the DataFrame index (row labels) using one existing column. The + index can replace the existing index. + + Args: + keys: + A label. This parameter can be a single column key. + drop: + Delete columns to be used as the new index. + + Returns: + DataFrame: + Changed row labels. + """ + raise NotImplementedError("abstract method") + + def reorder_levels(self, order: Sequence[int | str]) -> DataFrame: + """ + Rearrange index levels using input order. May not drop or duplicate levels. + + Args: + order: list of int or list of str + List representing new level order. Reference level by number + (position) or by key (label). + + Returns: + DataFrame + """ + raise NotImplementedError("abstract method") + + def droplevel(self, level): + """ + Return DataFrame with requested index / column level(s) removed. + + Args: + level: int, str, or list-like + If a string is given, must be the name of a level + If list-like, elements must be names or positional indexes + of levels. + Returns: + DataFrame with requested index / column level(s) removed. + """ + raise NotImplementedError("abstract method") + + def reset_index( + self, + *, + drop: bool = False, + ) -> DataFrame | None: + """Reset the index. + + Reset the index of the DataFrame, and use the default one instead. + + Args: + drop: + Do not try to insert index into dataframe columns. This resets + the index to the default integer index. + + Returns: + DataFrame with the new index. + """ + raise NotImplementedError("abstract method") + + def drop_duplicates( + self, + *, + keep="first", + ) -> DataFrame: + """ + Return DataFrame with duplicate rows removed. + + Considering certain columns is optional. Indexes, including time indexes + are ignored. + + Args: + subset : column label or sequence of labels, optional + Only consider certain columns for identifying duplicates, by + default use all of the columns. + keep : {'first', 'last', ``False``}, default 'first' + Determines which duplicates (if any) to keep. + + - 'first' : Drop duplicates except for the first occurrence. + - 'last' : Drop duplicates except for the last occurrence. + - ``False`` : Drop all duplicates. + + Returns: + DataFrame with duplicates removed + """ + raise NotImplementedError("abstract method") + + def duplicated(self, subset=None, keep="first"): + """ + Return boolean Series denoting duplicate rows. + + Considering certain columns is optional. + + Args: + subset : column label or sequence of labels, optional + Only consider certain columns for identifying duplicates, by + default use all of the columns. + keep : {'first', 'last', False}, default 'first' + Determines which duplicates (if any) to mark. + + - ``first`` : Mark duplicates as ``True`` except for the first occurrence. + - ``last`` : Mark duplicates as ``True`` except for the last occurrence. + - False : Mark all duplicates as ``True``. + + Returns: + Boolean series for each duplicated rows. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Reindex-based selection methods + + def dropna( + self, + ) -> DataFrame: + """Remove missing values. + + Returns: + DataFrame with NA entries dropped from it. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Sorting + + def sort_values( + self, + by: str | Sequence[str], + *, + ascending: bool | Sequence[bool] = True, + na_position="last", + ) -> DataFrame: + """Sort by the values along row axis. + + Args: + by: + Name or list of names to sort by. + ascending: + Sort ascending vs. descending. Specify list for multiple sort + orders. If this is a list of bools, must match the length of + the by. + na_position: + ``{'first', 'last'}``, default 'last' Puts NaNs at the beginning + if `first`; `last` puts NaNs at the end. + + Returns: + DataFrame with sorted values. + """ + raise NotImplementedError("abstract method") + + def sort_index( + self, + ) -> DataFrame: + """Sort object by labels (along an axis). + + Returns: + The original DataFrame sorted by the labels. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Arithmetic Methods + + def le(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'less than or equal to' of dataframe and other, element-wise (binary operator `<=`). + + Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison + operators. + + Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis + (rows or columns) and level for comparison. + + Args: + other: scalar, sequence, Series, or DataFrame + Any single or multiple element data structure, or list-like object. + axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + Whether to compare by the index (0 or 'index') or columns + (1 or 'columns'). + + Returns: + DataFrame of bool. The result of the comparison. + + Notes: + Mismatched indices will be unioned together. `NaN` values in + floating point columns are considered different + (i.e. `NaN` != `NaN`). + """ + raise NotImplementedError("abstract method") + + def lt(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'less than' of dataframe and other, element-wise (binary operator `<`). + + Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison + operators. + + Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis + (rows or columns) and level for comparison. + + Args: + other: scalar, sequence, Series, or DataFrame + Any single or multiple element data structure, or list-like object. + axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + Whether to compare by the index (0 or 'index') or columns + (1 or 'columns'). + + Returns: + DataFrame of bool. The result of the comparison. + + Notes: + Mismatched indices will be unioned together. `NaN` values in + floating point columns are considered different + (i.e. `NaN` != `NaN`). + """ + raise NotImplementedError("abstract method") + + def ge(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'greater than or equal to' of dataframe and other, element-wise (binary operator `>=`). + + Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison + operators. + + Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis + (rows or columns) and level for comparison. + + Args: + other: scalar, sequence, Series, or DataFrame + Any single or multiple element data structure, or list-like object. + axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + Whether to compare by the index (0 or 'index') or columns + (1 or 'columns'). + + Returns: + DataFrame of bool. The result of the comparison. + + Notes: + Mismatched indices will be unioned together. `NaN` values in + floating point columns are considered different + (i.e. `NaN` != `NaN`). + """ + raise NotImplementedError("abstract method") + + def gt(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'greater than' of dataframe and other, element-wise (binary operator `>`). + + Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison + operators. + + Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis + (rows or columns) and level for comparison. + + Args: + other: scalar, sequence, Series, or DataFrame + Any single or multiple element data structure, or list-like object. + axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + Whether to compare by the index (0 or 'index') or columns + (1 or 'columns'). + + Returns: + DataFrame of bool. The result of the comparison. + + Notes: + Mismatched indices will be unioned together. `NaN` values in + floating point columns are considered different + (i.e. `NaN` != `NaN`). + """ + raise NotImplementedError("abstract method") + + def add(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'addition' of dataframe and other, element-wise (binary operator `+`). + + Equivalent to ``dataframe + other``. With reverse version, `radd`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def sub(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'subtraction' of dataframe and other, element-wise (binary operator `-`). + + Equivalent to ``dataframe - other``. With reverse version, `rsub`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def rsub(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'subtraction' of dataframe and other, element-wise (binary operator `-`). + + Equivalent to ``other - dataframe``. With reverse version, `sub`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def mul(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'multiplication' of dataframe and other, element-wise (binary operator `*`). + + Equivalent to ``dataframe * other``. With reverse version, `rmul`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def truediv(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'floating division' of dataframe and other, element-wise (binary operator `/`). + + Equivalent to ``dataframe / other``. With reverse version, `rtruediv`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def rtruediv(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'floating division' of dataframe and other, element-wise (binary operator `/`). + + Equivalent to ``other / dataframe``. With reverse version, `truediv`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def floordiv(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'integer division' of dataframe and other, element-wise (binary operator `//`). + + Equivalent to ``dataframe // other``. With reverse version, `rfloordiv`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def rfloordiv(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'integer division' of dataframe and other, element-wise (binary operator `//`). + + Equivalent to ``other // dataframe``. With reverse version, `rfloordiv`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def mod(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'modulo' of dataframe and other, element-wise (binary operator `%`). + + Equivalent to ``dataframe % other``. With reverse version, `rmod`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + def rmod(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'modulo' of dataframe and other, element-wise (binary operator `%`). + + Equivalent to ``other % dataframe``. With reverse version, `mod`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + Args: + other: + Any single or multiple element data structure, or list-like object. + axis: + ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the + index (0 or 'index') or columns. (1 or 'columns'). For Series + input, axis to match Series index on. + + Returns: + DataFrame result of the arithmetic operation. + + Notes: + Mismatched indices will be unioned together. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Data reshaping + + def groupby( + self, + by: Union[str, Sequence[str]], + *, + level=None, + as_index: bool = True, + dropna: bool = True, + ): + """Group DataFrame by columns. + + A groupby operation involves some combination of splitting the + object, applying a function, and combining the results. This can be + used to group large amounts of data and compute operations on these + groups. + + Args: + by: + A label or list of labels may be passed to group by the columns + in ``self``. Notice that a tuple is interpreted as a (single) + key. + level : int, level name, or sequence of such, default None + If the axis is a MultiIndex (hierarchical), group by a particular + level or levels. Do not specify both ``by`` and ``level``. + as_index: + Default True. Return object with group labels as the index. + Only relevant for DataFrame input. ``as_index=False`` is + effectively "SQL-style" grouped output. This argument has no + effect on filtrations such as ``head()``, ``tail()``, ``nth()`` + and in transformations. + dropna: + Default True. If True, and if group keys contain NA values, NA + values together with row/column will be dropped. If False, NA + values will also be treated as the key in groups. + + Returns: + A groupby object that contains information about the groups. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Function application + + def map(self, func, na_action: Optional[str] = None) -> DataFrame: + """Apply a function to a Dataframe elementwise. + + .. note:: + + In pandas 2.1.0, DataFrame.applymap is deprecated and renamed to + DataFrame.map. + + This method applies a function that accepts and returns a scalar + to every element of a DataFrame. + + Args: + func: + Python function wrapped by ``remote_function`` decorator, + returns a single value from a single value. + na_action: + ``{None, 'ignore'}``, default None. If ‘ignore’, propagate NaN + values, without passing them to func. + + Returns: + Transformed DataFrame. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Merging / joining methods + + def join(self, other, *, how: str) -> DataFrame: + """Join columns of another DataFrame. + + Join columns with `other` DataFrame on index + + Args: + other: + DataFrame with an Index similar to the Index of this one. + how: + ``{'left', 'right', 'outer', 'inner'}, default 'left'`` + + How to handle the operation of the two objects. + + * left: use calling frame's index (or column if on is specified) + * right: use `other`'s index. + * outer: form union of calling frame's index (or column if on is + specified) with `other`'s index, and sort it. + lexicographically. + * inner: form intersection of calling frame's index (or column if + on is specified) with `other`'s index, preserving the order + of the calling's one. + + Returns: + A dataframe containing columns from both the caller and `other`. + """ + raise NotImplementedError("abstract method") + + def merge( + self, + right, + how: Literal[ + "inner", + "left", + "outer", + "right", + ] = "inner", + on: Optional[str] = None, + *, + left_on: Optional[str] = None, + right_on: Optional[str] = None, + sort: bool = False, + suffixes: tuple[str, str] = ("_x", "_y"), + ) -> DataFrame: + """Merge DataFrame objects with a database-style join. + + The join is done on columns or indexes. If joining columns on + columns, the DataFrame indexes *will be ignored*. Otherwise if joining indexes + on indexes or indexes on a column or columns, the index will be passed on. + When performing a cross merge, no column specifications to merge on are + allowed. + + .. warning:: + + If both key columns contain rows where the key is a null value, those + rows will be matched against each other. This is different from usual SQL + join behaviour and can lead to unexpected results. + + Args: + right: + Object to merge with. + how: + ``{'left', 'right', 'outer', 'inner'}, default 'inner'`` + Type of merge to be performed. + + * left: use only keys from left frame, similar to a SQL left outer join; + preserve key order. + * right: use only keys from right frame, similar to a SQL right outer join; + preserve key order. + * outer: use union of keys from both frames, similar to a SQL full outer + join; sort keys lexicographically. + * inner: use intersection of keys from both frames, similar to a SQL inner + join; preserve the order of the left keys. + + on: + Column join on. It must be found in both DataFrames. Either on or left_on + right_on + must be passed in. + left_on: + Column join on in the left DataFrame. Either on or left_on + right_on + must be passed in. + right_on: + Column join on in the right DataFrame. Either on or left_on + right_on + must be passed in. + sort: + Default False. Sort the join keys lexicographically in the + result DataFrame. If False, the order of the join keys depends + on the join type (how keyword). + suffixes: + Default ``("_x", "_y")``. A length-2 sequence where each + element is optionally a string indicating the suffix to add to + overlapping column names in `left` and `right` respectively. + Pass a value of `None` instead of a string to indicate that the + column name from `left` or `right` should be left as-is, with + no suffix. At least one of the values must not be None. + + Returns: + A DataFrame of the two merged objects. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # ndarray-like stats methods + + def any(self, *, bool_only: bool = False): + """ + Return whether any element is True, potentially over an axis. + + Returns False unless there is at least one element within a series or + along a Dataframe axis that is True or equivalent (e.g. non-zero or + non-empty). + + Args: + bool_only: + Include only boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def all(self, *, bool_only: bool = False): + """ + Return whether all elements are True, potentially over an axis. + + Returns True unless there at least one element within a series or + along a Dataframe axis that is False or equivalent (e.g. zero or + empty). + + Args: + bool_only: + Include only boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def prod(self, *, numeric_only: bool = False): + """ + Return the product of the values over the requested axis. + + Args: + numeric_only: + Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def min(self, *, numeric_only: bool = False): + """Return the minimum of the values over the requested axis. + + If you want the *index* of the minimum, use ``idxmin``. This is the + equivalent of the ``numpy.ndarray`` method ``argmin``. + + Args: + numeric_only: + Default False. Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def max(self, *, numeric_only: bool = False): + """Return the maximum of the values over the requested axis. + + If you want the *index* of the maximum, use ``idxmax``. This is + the equivalent of the ``numpy.ndarray`` method ``argmax``. + + Args: + numeric_only: + Default False. Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def sum(self, *, numeric_only: bool = False): + """Return the sum of the values over the requested axis. + + This is equivalent to the method ``numpy.sum``. + + Args: + numeric_only: + Default False. Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def mean(self, *, numeric_only: bool = False): + """Return the mean of the values over the requested axis. + + Args: + numeric_only: + Default False. Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def var(self, *, numeric_only: bool = False): + """Return unbiased variance over requested axis. + + Normalized by N-1 by default. + + Args: + numeric_only: + Default False. Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def std(self, *, numeric_only: bool = False): + """Return sample standard deviation over requested axis. + + Normalized by N-1 by default. + + Args: + numeric_only: + Default False. Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def count(self, *, numeric_only: bool = False): + """ + Count non-NA cells for each column or row. + + The values `None`, `NaN`, `NaT`, and optionally `numpy.inf` (depending + on `pandas.options.mode.use_inf_as_na`) are considered NA. + + Args: + numeric_only : bool, default False + Include only `float`, `int` or `boolean` data. + + Returns: + For each column/row the number of non-NA/null entries. + If `level` is specified returns a `DataFrame`. + """ + raise NotImplementedError("abstract method") + + def nunique(self): + """ + Count number of distinct elements in specified axis. + + Return Series with number of distinct elements. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + def cummin(self) -> DataFrame: + """Return cumulative minimum over a DataFrame axis. + + Returns a DataFrame of the same size containing the cumulative minimum. + + Returns: + Return cumulative minimum of DataFrame. + """ + raise NotImplementedError("abstract method") + + def cummax(self) -> DataFrame: + """Return cumulative maximum over a DataFrame axis. + + Returns a DataFrame of the same size containing the cumulative maximum. + + Returns: + Return cumulative maximum of DataFrame. + """ + raise NotImplementedError("abstract method") + + def cumsum(self) -> DataFrame: + """Return cumulative sum over a DataFrame axis. + + Returns a DataFrame of the same size containing the cumulative sum. + + Returns: + Return cumulative sum of DataFrame. + """ + raise NotImplementedError("abstract method") + + def cumprod(self) -> DataFrame: + """Return cumulative product over a DataFrame axis. + + Returns a DataFrame of the same size containing the cumulative product. + + Returns: + Return cumulative product of DataFrame. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Add index and columns + + @property + def index(self): + """The index (row labels) of the DataFrame. + + The index of a DataFrame is a series of labels that identify each row. + The labels can be integers, strings, or any other hashable type. The + index is used for label-based access and alignment, and can be accessed + or modified using this attribute. + + Returns: + The index labels of the DataFrame. + """ + raise NotImplementedError("abstract method") + + @property + def columns(self): + "The column labels of the DataFrame." + raise NotImplementedError("abstract method") + + def value_counts( + self, + subset=None, + normalize: bool = False, + sort: bool = True, + ascending: bool = False, + dropna: bool = True, + ): + """ + Return a Series containing counts of unique rows in the DataFrame. + + Args: + subset : label or list of labels, optional + Columns to use when counting unique combinations. + normalize : bool, default False + Return proportions rather than frequencies. + sort : bool, default True + Sort by frequencies. + ascending : bool, default False + Sort in ascending order. + dropna : bool, default True + Don’t include counts of rows that contain NA values. + + Returns: + Series + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py new file mode 100644 index 00000000000..b6ee19b46ac --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -0,0 +1,438 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/generic.py +from __future__ import annotations + +from typing import Literal, Optional + +from third_party.bigframes_vendored.pandas.core import indexing + + +class NDFrame(indexing.IndexingMixin): + """ + N-dimensional analogue of DataFrame. Store multi-dimensional in a + size-mutable, labeled data structure + """ + + # ---------------------------------------------------------------------- + # Axis + + @property + def ndim(self) -> int: + """Return an int representing the number of axes / array dimensions. + + Return 1 if Series. Otherwise return 2 if DataFrame. + """ + raise NotImplementedError("abstract method") + + @property + def size(self) -> int: + """Return an int representing the number of elements in this object. + + Return the number of rows if Series. Otherwise return the number of + rows times number of columns if DataFrame. + """ + raise NotImplementedError("abstract method") + + # ------------------------------------------------------------------------- + # Unary Methods + + def abs(self): + """Return a Series/DataFrame with absolute numeric value of each element. + + This function only applies to elements that are all numeric. + + Returns: + Series/DataFrame containing the absolute value of each element. + """ + raise NotImplementedError("abstract method") + + def astype(self, dtype): + """ + Cast a pandas object to a specified dtype ``dtype``. + + Parameters + ---------- + dtype : str, data type, Series or Mapping of column name -> data type + Use a str, numpy.dtype, pandas.ExtensionDtype or Python type to + cast entire pandas object to the same type. Alternatively, use a + mapping, e.g. {col: dtype, ...}, where col is a column label and dtype is + a numpy.dtype or Python type to cast one or more of the DataFrame's + columns to column-specific types. + + Returns + ------- + same type as caller + + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Iteration + + @property + def empty(self) -> bool: + """Indicator whether Series/DataFrame is empty. + + True if Series/DataFrame is entirely empty (no items), meaning any of the + axes are of length 0. + + Returns: + If Series/DataFrame is empty, return True, if not return False. + + Note: + If Series/DataFrame contains only NA values, it is still not + considered empty. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # I/O Methods + + def to_json( + self, + path_or_buf: str, + orient: Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + *, + index: bool = True, + lines: bool = False, + ) -> str | None: + """Convert the object to a JSON string, written to GCS. + + Note NaN's and None will be converted to null and datetime objects + will be converted to UNIX timestamps. + + Args: + path_or_buf: + A destination URI of GCS files(s) to store the extracted dataframe + in format of ``gs:///``. + + If the data size is more than 1GB, you must use a wildcard to + export the data into multiple files and the size of the files + varies. + + None, file-like objects or local file paths not yet supported. + orient: + Indication of expected JSON string format. + + .. note:: + + In BigQuery DataFrame, only `orient='records'` is supported so far. + + * Series: + + - default is 'index' + - allowed values are: {{'split', 'records', 'index', 'table'}}. + + * DataFrame: + + - default is 'columns' + - allowed values are: {{'split', 'records', 'index', 'columns', + 'values', 'table'}}. + + * The format of the JSON string: + + - 'split' : dict like {{'index' -> [index], 'columns' -> [columns], + 'data' -> [values]}} + - 'records' : list like [{{column -> value}}, ... , {{column -> value}}] + - 'index' : dict like {{index -> {{column -> value}}}} + - 'columns' : dict like {{column -> {{index -> value}}}} + - 'values' : just the values array + - 'table' : dict like {{'schema': {{schema}}, 'data': {{data}}}} + + Describing the data, where data component is like ``orient='records'``. + + lines: + If 'orient' is 'records' write out line-delimited json format. Will + throw ValueError if incorrect 'orient' since others are not + list-like. + + .. note:: + + BigQuery DataFrames only supports ``lines=True`` so far. + + index: + If True, write row names (index). + + Returns: + None. String output not yet supported. + """ + raise NotImplementedError("abstract method") + + def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: + """Write object to a comma-separated values (csv) file on GCS. + + Args: + path_or_buf: + A destination URI of GCS files(s) to store the extracted dataframe + in format of ``gs:///``. + + If the data size is more than 1GB, you must use a wildcard to + export the data into multiple files and the size of the files + varies. + + None, file-like objects or local file paths not yet supported. + + index: + If True, write row names (index). + + Returns: + None. String output not yet supported. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Unsorted + + def get(self, key, default=None): + """ + Get item from object for given key (ex: DataFrame column). + + Returns default value if not found. + + Args: + key: object + + Returns: + same type as items contained in object + """ + try: + return self[key] + except (KeyError, ValueError, IndexError): + return default + + def add_prefix(self, prefix: str, axis: int | str | None = None): + """Prefix labels with string `prefix`. + + For Series, the row labels are prefixed. + For DataFrame, the column labels are prefixed. + + Args: + prefix: + The string to add before each label. + axis: + ``{{0 or 'index', 1 or 'columns', None}}``, default None. Axis + to add prefix on + + Returns: + New Series or DataFrame with updated labels. + """ + raise NotImplementedError("abstract method") + + def add_suffix(self, suffix: str, axis: int | str | None = None): + """Suffix labels with string `suffix`. + + For Series, the row labels are suffixed. + For DataFrame, the column labels are suffixed. + + Args: + suffix: + The string to add after each label. + axis: + ``{{0 or 'index', 1 or 'columns', None}}``, default None. Axis + to add suffix on + + Returns: + New Series or DataFrame with updated labels. + """ + raise NotImplementedError("abstract method") + + def head(self, n: int = 5): + """Return the first `n` rows. + + This function returns the first `n` rows for the object based + on position. It is useful for quickly testing if your object + has the right type of data in it. + + **Not yet supported** For negative values of `n`, this function returns + all rows except the last `|n|` rows, equivalent to ``df[:n]``. + + If n is larger than the number of rows, this function returns all rows. + + Args: + n: + Default 5. Number of rows to select. + + Returns: + The first `n` rows of the caller object. + """ + raise NotImplementedError("abstract method") + + def tail(self, n: int = 5): + """Return the last `n` rows. + + This function returns last `n` rows from the object based on + position. It is useful for quickly verifying data, for example, + after sorting or appending rows. + + For negative values of `n`, this function returns all rows except + the first `|n|` rows, equivalent to ``df[|n|:]``. + + If n is larger than the number of rows, this function returns all rows. + + Args: + n: int, default 5. Number of rows to select. + + Returns: + The last `n` rows of the caller object. + """ + raise NotImplementedError("abstract method") + + def sample( + self, + n: Optional[int] = None, + frac: Optional[float] = None, + *, + random_state: Optional[int] = None, + ): + """Return a random sample of items from an axis of object. + + You can use `random_state` for reproducibility. + + Args: + n: + Number of items from axis to return. Cannot be used with `frac`. + Default = 1 if `frac` = None. + frac: + Fraction of axis items to return. Cannot be used with `n`. + random_state: + Seed for random number generator. + + Returns: + A new object of same type as caller containing `n` items randomly + sampled from the caller object. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Internal Interface Methods + + @property + def dtypes(self): + """Return the dtypes in the DataFrame. + + This returns a Series with the data type of each column. + The result's index is the original DataFrame's columns. Columns + with mixed types aren't supported yet in BigQuery DataFrames. + + Returns: + A *pandas* Series with the data type of each column. + """ + raise NotImplementedError("abstract method") + + def copy(self): + """Make a copy of this object's indices and data. + + A new object will be created with a copy of the calling object's data + and indices. Modifications to the data or indices of the copy will not + be reflected in the original object. + + Returns: + Object type matches caller. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # Action Methods + + def isna(self) -> NDFrame: + """Detect missing values. + + Return a boolean same-sized object indicating if the values are NA. + NA values get mapped to True values. Everything else gets mapped to + False values. Characters such as empty strings ``''`` or + :attr:`numpy.inf` are not considered NA values. + + Returns: + Mask of bool values for each element that indicates whether an + element is an NA value. + """ + raise NotImplementedError("abstract method") + + isnull = isna + + def notna(self) -> NDFrame: + """Detect existing (non-missing) values. + + Return a boolean same-sized object indicating if the values are not NA. + Non-missing values get mapped to True. Characters such as empty + strings ``''`` or :attr:`numpy.inf` are not considered NA values. + NA values get mapped to False values. + + Returns: + Mask of bool values for each element that indicates whether an + element is not an NA value. + """ + raise NotImplementedError("abstract method") + + notnull = notna + + def shift( + self, + periods: int = 1, + ) -> NDFrame: + """Shift index by desired number of periods. + + Shifts the index without realigning the data. + + Args: + periods: + Number of periods to shift. Can be positive or negative. + + Returns: + Copy of input object, shifted. + """ + raise NotImplementedError("abstract method") + + def rank( + self, + axis=0, + method: str = "average", + numeric_only: bool = False, + na_option: str = "keep", + ascending: bool = True, + ): + """ + Compute numerical data ranks (1 through n) along axis. + + By default, equal values are assigned a rank that is the average of the + ranks of those values. + + Parameters + ---------- + method : {'average', 'min', 'max', 'first', 'dense'}, default 'average' + How to rank the group of records that have the same value (i.e. ties): + + * average: average rank of the group + * min: lowest rank in the group + * max: highest rank in the group + * first: ranks assigned in order they appear in the array + * dense: like 'min', but rank always increases by 1 between groups. + + numeric_only : bool, default False + For DataFrame objects, rank only numeric columns if set to True. + + na_option : {'keep', 'top', 'bottom'}, default 'keep' + How to rank NaN values: + + * keep: assign NaN rank to NaN values + * top: assign lowest rank to NaN values + * bottom: assign highest rank to NaN values + + ascending : bool, default True + Whether or not the elements should be ranked in ascending order. + + Returns + ------- + same type as caller + Return a Series or DataFrame with data ranks as values. + """ + raise NotImplementedError("abstract method") + + def __nonzero__(self): + raise ValueError( + f"The truth value of a {type(self).__name__} is ambiguous. " + "Use a.empty, a.bool(), a.item(), a.any() or a.all()." + ) + + __bool__ = __nonzero__ diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py new file mode 100644 index 00000000000..5b5a9f206b3 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -0,0 +1,318 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/groupby/groupby.py +""" +Provide the groupby split-apply-combine paradigm. Define the GroupBy +class providing the base-class of operations. + +The SeriesGroupBy and DataFrameGroupBy sub-class +(defined in pandas.core.groupby.generic) +expose these user-facing objects to provide specific functionality. +""" +from __future__ import annotations + + +class GroupBy: + """ + Class for grouping and aggregating relational data. + """ + + def any(self): + """ + Return True if any value in the group is truthful, else False. + + + Returns + ------- + Series or DataFrame + DataFrame or Series of boolean values, where a value is True if any element + is True within its respective group, False otherwise. + """ + raise NotImplementedError("abstract property") + + def all(self): + """ + Return True if all values in the group are truthful, else False. + + Returns + ------- + Series or DataFrame + DataFrame or Series of boolean values, where a value is True if all elements + are True within its respective group, False otherwise. + """ + raise NotImplementedError("abstract property") + + def count(self): + """ + Compute count of group, excluding missing values. + + Returns + ------- + Series or DataFrame + Count of values within each group. + """ + raise NotImplementedError("abstract property") + + def mean( + self, + numeric_only: bool = False, + ): + """ + Compute mean of groups, excluding missing values. + + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + + Returns + ------- + pandas.Series or pandas.DataFrame + """ + raise NotImplementedError("abstract property") + + def std( + self, + *, + numeric_only: bool = False, + ): + """ + Compute standard deviation of groups, excluding missing values. + + For multiple groupings, the result index will be a MultiIndex. + + Parameters + ---------- + numeric_only : bool, default False + Include only `float`, `int` or `boolean` data. + + Returns + ------- + Series or DataFrame + Standard deviation of values within each group. + """ + raise NotImplementedError("abstract property") + + def var( + self, + *, + numeric_only: bool = False, + ): + """ + Compute variance of groups, excluding missing values. + + For multiple groupings, the result index will be a MultiIndex. + + Parameters + ---------- + numeric_only : bool, default False + Include only `float`, `int` or `boolean` data. + + Returns + ------- + Series or DataFrame + Variance of values within each group. + """ + raise NotImplementedError("abstract property") + + def sum( + self, + numeric_only: bool = False, + min_count: int = 0, + ): + """ + Compute sum of group values. + + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + min_count : int, default 0 + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + Returns + ------- + Series or DataFrame + Computed sum of values within each group. + """ + raise NotImplementedError("abstract property") + + def prod(self, numeric_only: bool = False, min_count: int = 0): + """ + Compute prod of group values. + + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + min_count : int, default 0 + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + Returns + ------- + Series or DataFrame + Computed prod of values within each group. + """ + raise NotImplementedError("abstract property") + + def min( + self, + numeric_only: bool = False, + min_count: int = -1, + ): + """ + Compute min of group values. + + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + min_count : int, default 0 + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + Returns + ------- + Series or DataFrame + Computed min of values within each group. + """ + raise NotImplementedError("abstract property") + + def max( + self, + numeric_only: bool = False, + min_count: int = -1, + ): + """ + Compute max of group values. + + Parameters + ---------- + numeric_only : bool, default False + Include only float, int, boolean columns. + min_count : int, default 0 + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + Returns + ------- + Series or DataFrame + Computed max of values within each group. + """ + raise NotImplementedError("abstract property") + + def cumcount(self, ascending: bool = True): + """ + Number each item in each group from 0 to the length of that group - 1. + + Parameters + ---------- + ascending : bool, default True + If False, number in reverse, from length of group - 1 to 0. + + Returns + ------- + Series + Sequence number of each element within each group. + """ + raise NotImplementedError("abstract property") + + def cumprod(self, *args, **kwargs): + """ + Cumulative product for each group. + + Returns + ------- + Series or DataFrame + """ + raise NotImplementedError("abstract property") + + def cumsum(self, *args, **kwargs): + """ + Cumulative sum for each group. + + Returns + ------- + Series or DataFrame + """ + raise NotImplementedError("abstract property") + + def cummin(self, *args, numeric_only: bool = False, **kwargs): + """ + Cumulative min for each group. + + Returns + ------- + Series or DataFrame + """ + raise NotImplementedError("abstract property") + + def cummax(self, *args, numeric_only: bool = False, **kwargs): + """ + Cumulative max for each group. + + Returns + ------- + Series or DataFrame + """ + raise NotImplementedError("abstract property") + + def diff(self): + """ + First discrete difference of element. + Calculates the difference of each element compared with another + element in the group (default is element in previous row). + + Returns + ------- + Series or DataFrame + First differences. + """ + raise NotImplementedError("abstract property") + + def shift(self, periods: int = 1): + """ + Shift each group by periods observations. + If freq is passed, the index will be increased using the periods and the freq. + + Parameters + ---------- + periods : int, default 1 + Number of periods to shift. + Returns + ------- + Series or DataFrame + Object shifted within each group. + """ + raise NotImplementedError("abstract property") + + def rolling(self, *args, **kwargs): + """ + Return a rolling grouper, providing rolling functionality per group. + + Args: + min_periods : int, default None + Minimum number of observations in window required to have a value; + otherwise, result is ``np.nan``. + + For a window that is specified by an offset, + ``min_periods`` will default to 1. + + For a window that is specified by an integer, ``min_periods`` will default + to the size of the window. + + Returns: + Return a new grouper with our rolling appended. + """ + raise NotImplementedError("abstract property") + + def expanding(self, *args, **kwargs): + """ + Return an expanding grouper, providing expanding + functionality per group. + """ + raise NotImplementedError("abstract property") + + +class SeriesGroupBy(GroupBy): + pass + + +class DataFrameGroupBy(GroupBy): + pass diff --git a/third_party/bigframes_vendored/pandas/core/indexes/__init__.py b/third_party/bigframes_vendored/pandas/core/indexes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py new file mode 100644 index 00000000000..5d431a8a949 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py @@ -0,0 +1,94 @@ +class DatetimeProperties: + """ + Accessor object for datetimelike properties of the Series values. + """ + + @property + def day(self): + """The day of the datetime.""" + + raise NotImplementedError("abstract method") + + @property + def dayofweek(self): + """The day of the week with Monday=0, Sunday=6. + + Return the day of the week. It is assumed the week starts on + Monday, which is denoted by 0 and ends on Sunday which is denoted + by 6. This method is available on both Series with datetime + values (using the `dt` accessor) or DatetimeIndex. + + Returns: + Series or Index + Containing integers indicating the day number. + """ + + raise NotImplementedError("abstract method") + + @property + def date(self): + """Returns numpy array of python :class:`datetime.date` objects. + + Namely, the date part of Timestamps without time and + timezone information. + + warning: + This method returns a Series whereas pandas returns + a numpy array. + """ + + raise NotImplementedError("abstract method") + + @property + def hour(self): + """The hours of the datetime.""" + + raise NotImplementedError("abstract method") + + @property + def minute(self): + """The minutes of the datetime.""" + + raise NotImplementedError("abstract method") + + @property + def month(self): + """The month as January=1, December=12.""" + + raise NotImplementedError("abstract method") + + @property + def second(self): + """The seconds of the datetime.""" + + raise NotImplementedError("abstract method") + + @property + def time(self): + """Returns numpy array of :class:`datetime.time` objects. + + The time part of the Timestamps. + + warning: + This method returns a Series whereas pandas returns + a numpy array. + """ + + raise NotImplementedError("abstract method") + + @property + def quarter(self): + """The quarter of the date. + + warning: + This method returns a Series whereas pandas returns + a numpy array. + """ + + raise NotImplementedError("abstract method") + + @property + def year(self): + """The year of the datetime.""" + + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py new file mode 100644 index 00000000000..bdd05a01007 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -0,0 +1,20 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexes/base.py + + +class Index: + """Immutable sequence used for indexing and alignment. + + The basic object storing axis labels for all objects. + """ + + @property + def name(self): + """Return Index name.""" + raise NotImplementedError("abstract method") + + @property + def shape(self): + """ + Return a tuple of the shape of the underlying data. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/indexing.py b/third_party/bigframes_vendored/pandas/core/indexing.py new file mode 100644 index 00000000000..87fb34b32a3 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/indexing.py @@ -0,0 +1,72 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexing.py + + +class IndexingMixin: + """ + Mixin for adding .loc/.iloc/.at/.iat to Dataframes and Series. + """ + + @property + def iloc(self): + """Purely integer-location based indexing for selection by position. + + ``.iloc[]`` is primarily integer position based (from ``0`` to + ``length-1`` of the axis), but may also be used with a boolean + array. + + Allowed inputs are: + + - **Not supported yet** An integer, e.g. ``5``. + - **Not supported yet** A list or array of integers, e.g. ``[4, 3, 0]``. + - A slice object with ints, e.g. ``1:7``. + - **Not supported yet** A boolean array. + - **Not supported yet** A ``callable`` function with one argument (the + calling Series or DataFrame) and that returns valid output for + indexing (one of the above). This is useful in method chains, when you + don't have a reference to the calling object, but would like to base + your selection on some value. + - **Not supported yet** A tuple of row and column indexes. The tuple + elements consist of one of the above inputs, e.g. ``(0, 1)``. + + ``.iloc`` will raise ``IndexError`` if a requested indexer is + out-of-bounds, except *slice* indexers which allow out-of-bounds + indexing (this conforms with python/numpy *slice* semantics). + """ + raise NotImplementedError("abstract methdod") + + @property + def loc(self): + """Access a group of rows and columns by label(s) or a boolean array. + + ``.loc[]`` is primarily label based, but may also be used with a + boolean array. + + Allowed inputs are: + + - **Not supported yet** A single label, e.g. ``5`` or ``'a'``, (note + that ``5`` is interpreted as a *label* of the index, and **never** as + an integer position along the index). + - **Not supported yet** A list or array of labels, e.g. ``['a', 'b', 'c']``. + - **Not supported yet** A slice object with labels, e.g. ``'a':'f'``. + + .. warning:: Note that contrary to usual python slices, **both** the + start and the stop are included + + - **Not supported yet** A boolean array of the same length as the axis being sliced, + e.g. ``[True, False, True]``. + - An alignable boolean Series. The index of the key will be aligned before + masking. + - **Not supported yet** An alignable Index. The Index of the returned + selection will be the input. + - **Not supported yet** A ``callable`` function with one argument (the + calling Series or DataFrame) + and that returns valid output for indexing (one of the above) + + Raises: + KeyError: If any items are not found. + IndexingError: + If an indexed key is passed and its index is unalignable to the + frame index. + + """ + raise NotImplementedError("abstract methdod") diff --git a/third_party/bigframes_vendored/pandas/core/reshape/concat.py b/third_party/bigframes_vendored/pandas/core/reshape/concat.py new file mode 100644 index 00000000000..98b43c7dfba --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/reshape/concat.py @@ -0,0 +1,135 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/reshape/concat.py +""" +Concat routines. +""" +from __future__ import annotations + + +def concat( + objs, + *, + join: str = "outer", + ignore_index: bool = False, +): + """ + Concatenate BigQuery DataFrames objects along a particular axis. + + Allows optional set logic along the other axes. + + Can also add a layer of hierarchical indexing on the concatenation axis, + which may be useful if the labels are the same (or overlapping) on + the passed axis number. + + Parameters + ---------- + objs: + Objects to concatenate. Any None objects will be dropped silently unless + they are all None in which case a ValueError will be raised. + join: {'inner', 'outer'}, default 'outer' + How to handle indexes on other axis (or axes). + ignore_index : bool, default False + If True, do not use the index values along the concatenation axis. The + resulting axis will be labeled 0, ..., n - 1. This is useful if you are + concatenating objects where the concatenation axis does not have + meaningful indexing information. Note the index values on the other + axes are still respected in the join. + + Returns + ------- + object, type of objs + When concatenating all ``Series`` along the index (axis=0), a + ``Series`` is returned. When ``objs`` contains at least one + ``DataFrame``, a ``DataFrame`` is returned. + + Notes + ----- + It is not recommended to build DataFrames by adding single rows in a + for loop. Build a list of rows and make a DataFrame in a single concat. + + Examples + -------- + Combine two ``Series``. + + >>> import bigframes.pandas as pd + >>> pd.options.display.progress_bar = None + >>> s1 = pd.Series(['a', 'b']) + >>> s2 = pd.Series(['c', 'd']) + >>> pd.concat([s1, s2]) + 0 a + 1 b + 0 c + 1 d + dtype: string + + Clear the existing index and reset it in the result + by setting the ``ignore_index`` option to ``True``. + + >>> pd.concat([s1, s2], ignore_index=True) + 0 a + 1 b + 2 c + 3 d + dtype: string + + Combine two ``DataFrame`` objects with identical columns. + + >>> df1 = pd.DataFrame([['a', 1], ['b', 2]], + ... columns=['letter', 'number']) + >>> df1 + letter number + 0 a 1 + 1 b 2 + + [2 rows x 2 columns] + >>> df2 = pd.DataFrame([['c', 3], ['d', 4]], + ... columns=['letter', 'number']) + >>> df2 + letter number + 0 c 3 + 1 d 4 + + [2 rows x 2 columns] + >>> pd.concat([df1, df2]) + letter number + 0 a 1 + 1 b 2 + 0 c 3 + 1 d 4 + + [4 rows x 2 columns] + + Combine ``DataFrame`` objects with overlapping columns + and return everything. Columns outside the intersection will + be filled with ``NaN`` values. + + >>> df3 = pd.DataFrame([['c', 3, 'cat'], ['d', 4, 'dog']], + ... columns=['letter', 'number', 'animal']) + >>> df3 + letter number animal + 0 c 3 cat + 1 d 4 dog + + [2 rows x 3 columns] + >>> pd.concat([df1, df3]) + letter number animal + 0 a 1 + 1 b 2 + 0 c 3 cat + 1 d 4 dog + + [4 rows x 3 columns] + + Combine ``DataFrame`` objects with overlapping columns + and return only those that are shared by passing ``inner`` to + the ``join`` keyword argument. + + >>> pd.concat([df1, df3], join="inner") + letter number + 0 a 1 + 1 b 2 + 0 c 3 + 1 d 4 + + [4 rows x 2 columns] + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py new file mode 100644 index 00000000000..a254fcf842d --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -0,0 +1,1553 @@ +""" +Data structure for 1-dimensional cross-sectional and time series data +""" +from __future__ import annotations + +from typing import Hashable, IO, Literal, Mapping, Sequence, TYPE_CHECKING + +import numpy as np +from pandas._libs import lib +from pandas._typing import Axis, FilePath, NaPosition, WriteBuffer +import pandas.io.formats.format as fmt + +from third_party.bigframes_vendored.pandas.core.generic import NDFrame + +if TYPE_CHECKING: + from third_party.bigframes_vendored.pandas.core.frame import DataFrame + from third_party.bigframes_vendored.pandas.core.groupby import SeriesGroupBy + + +class Series(NDFrame): # type: ignore[misc] + @property + def dt(self): + """ + Accessor object for datetimelike properties of the Series values. + """ + raise NotImplementedError("abstract property") + + @property + def index(self): + """The index (axis labels) of the Series.""" + raise NotImplementedError("abstract property") + + @property + def shape(self): + """Return a tuple of the shape of the underlying data.""" + raise NotImplementedError("abstract property") + + @property + def dtype(self): + """ + Return the dtype object of the underlying data. + """ + raise NotImplementedError("abstract property") + + @property + def dtypes(self): + """ + Return the dtype object of the underlying data. + """ + raise NotImplementedError("abstract property") + + @property + def name(self) -> Hashable: + """ + Return the name of the Series. + + The name of a Series becomes its index or column name if it is used + to form a DataFrame. It is also used whenever displaying the Series + using the interpreter. + + Returns + ------- + label (hashable object) + The name of the Series, also the column name if part of a DataFrame. + """ + raise NotImplementedError("abstract property") + + def reset_index( + self, + *, + drop: bool = False, + name=lib.no_default, + ) -> DataFrame | Series | None: + """ + Generate a new DataFrame or Series with the index reset. + + This is useful when the index needs to be treated as a column, or + when the index is meaningless and needs to be reset to the default + before another operation. + + Parameters + ---------- + drop : bool, default False + Just reset the index, without inserting it as a column in + the new DataFrame. + name : object, optional + The name to use for the column containing the original Series + values. Uses ``self.name`` by default. This argument is ignored + when `drop` is True. + + Returns + ------- + Series or DataFrame or None + When `drop` is False (the default), a DataFrame is returned. + The newly created columns will come first in the DataFrame, + followed by the original Series values. + When `drop` is True, a `Series` is returned. + In either case, if ``inplace=True``, no value is returned. + + """ + raise NotImplementedError("abstract method") + + def __repr__(self) -> str: + """ + Return a string representation for a particular Series. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # IO methods (to / from other formats) + + def to_string( + self, + buf: FilePath | WriteBuffer[str] | None = None, + na_rep: str = "NaN", + float_format: str | None = None, + header: bool = True, + index: bool = True, + length: bool = False, + dtype: bool = False, + name: bool = False, + max_rows: int | None = None, + min_rows: int | None = None, + ) -> str | None: + """ + Render a string representation of the Series. + + Parameters + ---------- + buf : StringIO-like, optional + Buffer to write to. + na_rep : str, optional + String representation of NaN to use, default 'NaN'. + float_format : one-parameter function, optional + Formatter function to apply to columns' elements if they are + floats, default None. + header : bool, default True + Add the Series header (index name). + index : bool, optional + Add index (row) labels, default True. + length : bool, default False + Add the Series length. + dtype : bool, default False + Add the Series dtype. + name : bool, default False + Add the Series name if not None. + max_rows : int, optional + Maximum number of rows to show before truncating. If None, show + all. + min_rows : int, optional + The number of rows to display in a truncated repr (when number + of rows is above `max_rows`). + + Returns + ------- + str or None + String representation of Series if ``buf=None``, otherwise None. + """ + formatter = fmt.SeriesFormatter( + self, + name=name, + length=length, + header=header, + index=index, + dtype=dtype, + na_rep=na_rep, + float_format=float_format, + min_rows=min_rows, + max_rows=max_rows, + ) + result = formatter.to_string() + + # catch contract violations + raise NotImplementedError("abstract method") + + def to_markdown( + self, + buf: IO[str] | None = None, + mode: str = "wt", + index: bool = True, + **kwargs, + ) -> str | None: + """ + Print {klass} in Markdown-friendly format. + + Parameters + ---------- + buf : str, Path or StringIO-like, optional, default None + Buffer to write to. If None, the output is returned as a string. + mode : str, optional + Mode in which file is opened, "wt" by default. + index : bool, optional, default True + Add index (row) labels. + Returns + ------- + str + {klass} in Markdown-friendly format. + """ + raise NotImplementedError("abstract method") + + def to_dict(self, into: type[dict] = dict) -> Mapping: + """ + Convert Series to {label -> value} dict or dict-like object. + + Parameters + ---------- + into : class, default dict + The collections.abc.Mapping subclass to use as the return + object. Can be the actual class or an empty + instance of the mapping type you want. If you want a + collections.defaultdict, you must pass it initialized. + + Returns + ------- + collections.abc.Mapping + Key-value representation of Series. + """ + raise NotImplementedError("abstract method") + + def to_frame(self) -> DataFrame: + """ + Convert Series to DataFrame. + + Returns + ------- + DataFrame + DataFrame representation of Series. + """ + raise NotImplementedError("abstract method") + + def to_excel(self, excel_writer, sheet_name): + """ + Write Series to an Excel sheet. + + To write a single Series to an Excel .xlsx file it is only necessary to + specify a target file name. To write to multiple sheets it is necessary to + create an `ExcelWriter` object with a target file name, and specify a sheet + in the file to write to. + + Multiple sheets may be written to by specifying unique `sheet_name`. + With all data written to the file it is necessary to save the changes. + Note that creating an `ExcelWriter` object with a file name that already + exists will result in the contents of the existing file being erased. + + Parameters + ---------- + excel_writer : path-like, file-like, or ExcelWriter object + File path or existing ExcelWriter. + sheet_name : str, default 'Sheet1' + Name of sheet which will contain DataFrame. + """ + raise NotImplementedError("abstract method") + + def to_latex(self, buf=None, columns=None, header=True, index=True, **kwargs): + """ + Render object to a LaTeX tabular, longtable, or nested table. + + Parameters + ---------- + buf : str, Path or StringIO-like, optional, default None + Buffer to write to. If None, the output is returned as a string. + columns : list of label, optional + The subset of columns to write. Writes all columns by default. + header : bool or list of str, default True + Write out the column names. If a list of strings is given, + it is assumed to be aliases for the column names. + index : bool, default True + Write row names (index). + + + Returns + ------- + str or None + If buf is None, returns the result as a string. Otherwise returns None. + """ + raise NotImplementedError("abstract method") + + def tolist(self) -> list: + """ + Return a list of the values. + + These are each a scalar type, which is a Python scalar + (for str, int, float) or a pandas scalar + (for Timestamp/Timedelta/Interval/Period) + + Returns + ------- + list + """ + raise NotImplementedError("abstract method") + + to_list = tolist + + def to_numpy(self, dtype, copy=False, na_value=None): + """ + A NumPy ndarray representing the values in this Series or Index. + + Parameters + ---------- + dtype : str or numpy.dtype, optional + The dtype to pass to :meth:`numpy.asarray`. + copy : bool, default False + Whether to ensure that the returned value is not a view on + another array. Note that ``copy=False`` does not *ensure* that + ``to_numpy()`` is no-copy. Rather, ``copy=True`` ensure that + a copy is made, even if not strictly necessary. + na_value : Any, optional + The value to use for missing values. The default value depends + on `dtype` and the type of the array. + **kwargs + Additional keywords passed through to the ``to_numpy`` method + of the underlying array (for extension arrays). + + Returns + ------- + numpy.ndarray + """ + raise NotImplementedError("abstract method") + + def to_pickle(self, path, **kwargs): + """ + Pickle (serialize) object to file. + + Parameters + ---------- + path : str, path object, or file-like object + String, path object (implementing ``os.PathLike[str]``), or file-like + object implementing a binary ``write()`` function. File path where + the pickled object will be stored. + """ + raise NotImplementedError("abstract method") + + def to_xarray(self): + """ + Return an xarray object from the pandas object. + + Returns + ------- + xarray.DataArray or xarray.Dataset + Data in the pandas structure converted to Dataset if the object is + a DataFrame, or a DataArray if the object is a Series. + """ + raise NotImplementedError("abstract method") + + def to_json( + self, + path_or_buf=None, + orient: Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + **kwarg, + ) -> str | None: + """ + Convert the object to a JSON string. + + Note NaN's and None will be converted to null and datetime objects + will be converted to UNIX timestamps. + + Parameters + ---------- + path_or_buf : str, path object, file-like object, or None, default None + String, path object (implementing os.PathLike[str]), or file-like + object implementing a write() function. If None, the result is + returned as a string. + orient: + Indication of expected JSON string format. + + * Series: + + - default is 'index' + - allowed values are: {{'split', 'records', 'index', 'table'}}. + + * DataFrame: + + - default is 'columns' + - allowed values are: {{'split', 'records', 'index', 'columns', + 'values', 'table'}}. + + * The format of the JSON string: + + - 'split' : dict like {{'index' -> [index], 'columns' -> [columns], + 'data' -> [values]}} + - 'records' : list like [{{column -> value}}, ... , {{column -> value}}] + - 'index' : dict like {{index -> {{column -> value}}}} + - 'columns' : dict like {{column -> {{index -> value}}}} + - 'values' : just the values array + - 'table' : dict like {{'schema': {{schema}}, 'data': {{data}}}} + + Describing the data, where data component is like ``orient='records'``. + + Returns + ------- + None or str + If path_or_buf is None, returns the resulting json format as a + string. Otherwise returns None. + """ + raise NotImplementedError("abstract method") + + def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: + """ + Write object to a comma-separated values (csv) file. + + Parameters + ---------- + path_or_buf : str, path object, file-like object, or None, default None + String, path object (implementing os.PathLike[str]), or file-like + object implementing a write() function. If None, the result is + returned as a string. If a non-binary file object is passed, it should + be opened with `newline=''`, disabling universal newlines. If a binary + file object is passed, `mode` might need to contain a `'b'`. + + Returns + ------- + None or str + If path_or_buf is None, returns the resulting csv format as a + string. Otherwise returns None. + """ + raise NotImplementedError("abstract method") + + def count(self): + """ + Return number of non-NA/null observations in the Series. + + Returns + ------- + int or Series (if level specified) + Number of non-null values in the Series. + """ + raise NotImplementedError("abstract method") + + def nunique(self) -> int: + """ + Return number of unique elements in the object. + + Excludes NA values by default. + + Returns + ------- + int + """ + raise NotImplementedError("abstract method") + + def mode(self) -> Series: + """ + Return the mode(s) of the Series. + + The mode is the value that appears most often. There can be multiple modes. + + Always returns Series even if only one value is returned. + + Returns + ------- + Series + Modes of the Series in sorted order. + """ + raise NotImplementedError("abstract method") + + def drop_duplicates( + self, + *, + keep="first", + ) -> Series | None: + """ + Return Series with duplicate values removed. + + Args: + keep : {'first', 'last', ``False``}, default 'first' + Method to handle dropping duplicates: + + - 'first' : Drop duplicates except for the first occurrence. + - 'last' : Drop duplicates except for the last occurrence. + - ``False`` : Drop all duplicates. + + Returns: + Series with duplicates dropped or None if ``inplace=True``. + """ + raise NotImplementedError("abstract method") + + def duplicated(self, keep="first") -> Series: + """ + Indicate duplicate Series values. + + Duplicated values are indicated as ``True`` values in the resulting + Series. Either all duplicates, all except the first or all except the + last occurrence of duplicates can be indicated. + + Args: + keep : {'first', 'last', False}, default 'first' + Method to handle dropping duplicates: + + - 'first' : Mark duplicates as ``True`` except for the first + occurrence. + - 'last' : Mark duplicates as ``True`` except for the last + occurrence. + - ``False`` : Mark all duplicates as ``True``. + + Returns: + Series indicating whether each value has occurred in the + preceding values. + """ + raise NotImplementedError("abstract method") + + def round(self, decimals: int = 0) -> Series: + """ + Round each value in a Series to the given number of decimals. + + Parameters + ---------- + decimals : int, default 0 + Number of decimal places to round to. If decimals is negative, + it specifies the number of positions to the left of the decimal point. + + Returns + ------- + Series + Rounded values of the Series. + """ + raise NotImplementedError("abstract method") + + def diff(self) -> Series: + """ + First discrete difference of element. + + Calculates the difference of a {klass} element compared with another + element in the {klass} (default is element in previous row). + + Returns + ------- + {klass} + First differences of the Series. + """ + raise NotImplementedError("abstract method") + + def dot(self, other) -> Series | np.ndarray: + """ + Compute the dot product between the Series and the columns of other. + + This method computes the dot product between the Series and another + one, or the Series and each columns of a DataFrame, or the Series and + each columns of an array. + + It can also be called using `self @ other` in Python >= 3.5. + + Parameters + ---------- + other : Series, DataFrame or array-like + The other object to compute the dot product with its columns. + + Returns + ------- + scalar, Series or numpy.ndarray + Return the dot product of the Series and other if other is a + Series, the Series of the dot product of Series and each rows of + other if other is a DataFrame or a numpy.ndarray between the Series + and each columns of the numpy array. + + Notes + ----- + The Series and other has to share the same index if other is a Series + or a DataFrame. + + """ + raise NotImplementedError("abstract method") + + def __matmul__(self, other): + """ + Matrix multiplication using binary `@` operator in Python>=3.5. + """ + raise NotImplementedError("abstract method") + + def __rmatmul__(self, other): + """ + Matrix multiplication using binary `@` operator in Python>=3.5. + """ + raise NotImplementedError("abstract method") + + def sort_values( + self, + *, + axis: Axis = 0, + ascending: bool | int | Sequence[bool] | Sequence[int] = True, + na_position: str = "last", + ) -> Series | None: + """ + Sort by the values. + + Sort a Series in ascending or descending order by some + criterion. + + Parameters + ---------- + axis : {0 or 'index'} + Unused. Parameter needed for compatibility with DataFrame. + ascending : bool or list of bools, default True + If True, sort values in ascending order, otherwise descending. + na_position : {'first' or 'last'}, default 'last' + Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at + the end. + + Returns + ------- + Series or None + Series ordered by values or None if ``inplace=True``. + """ + raise NotImplementedError("abstract method") + + def sort_index( + self, + *, + axis: Axis = 0, + ascending: bool | Sequence[bool] = True, + na_position: NaPosition = "last", + ) -> Series | None: + """ + Sort Series by index labels. + + Returns a new Series sorted by label if `inplace` argument is + ``False``, otherwise updates the original series and returns None. + + Parameters + ---------- + axis : {0 or 'index'} + Unused. Parameter needed for compatibility with DataFrame. + ascending : bool or list-like of bools, default True + Sort ascending vs. descending. When the index is a MultiIndex the + sort direction can be controlled for each level individually. + na_position : {'first', 'last'}, default 'last' + If 'first' puts NaNs at the beginning, 'last' puts NaNs at the end. + Not implemented for MultiIndex. + + Returns + ------- + Series or None + The original Series sorted by the labels or None if ``inplace=True``. + + """ + + raise NotImplementedError("abstract method") + + def nlargest( + self, n: int = 5, keep: Literal["first", "last", "all"] = "first" + ) -> Series: + """ + Return the largest `n` elements. + + Parameters + ---------- + n : int, default 5 + Return this many descending sorted values. + keep : {'first', 'last', 'all'}, default 'first' + When there are duplicate values that cannot all fit in a + Series of `n` elements: + + - ``first`` : return the first `n` occurrences in order + of appearance. + - ``last`` : return the last `n` occurrences in reverse + order of appearance. + - ``all`` : keep all occurrences. This can result in a Series of + size larger than `n`. + + Returns + ------- + Series + The `n` largest values in the Series, sorted in decreasing order. + """ + raise NotImplementedError("abstract method") + + def nsmallest(self, n: int = 5, keep: str = "first") -> Series: + """ + Return the smallest `n` elements. + + Parameters + ---------- + n : int, default 5 + Return this many ascending sorted values. + keep : {'first', 'last', 'all'}, default 'first' + When there are duplicate values that cannot all fit in a + Series of `n` elements: + + - ``first`` : return the first `n` occurrences in order + of appearance. + - ``last`` : return the last `n` occurrences in reverse + order of appearance. + - ``all`` : keep all occurrences. This can result in a Series of + size larger than `n`. + + Returns + ------- + Series + The `n` smallest values in the Series, sorted in increasing order. + """ + raise NotImplementedError("abstract method") + + # ---------------------------------------------------------------------- + # function application + + def apply( + self, + func, + ) -> DataFrame | Series: + """ + Invoke function on values of Series. + + Can be ufunc (a NumPy function that applies to the entire Series) + or a Python function that only works on single values. + + Parameters + ---------- + func : function + Python function or NumPy ufunc to apply. + Returns + ------- + Series or DataFrame + If func returns a Series object the result will be a DataFrame. + """ + raise NotImplementedError("abstract method") + + def groupby( + self, + by=None, + axis: Axis = 0, + level=None, + as_index: bool = True, + *, + dropna: bool = True, + ) -> SeriesGroupBy: + """Group Series using a mapper or by a Series of columns. + + A groupby operation involves some combination of splitting the + object, applying a function, and combining the results. This can be + used to group large amounts of data and compute operations on these + groups. + + Parameters + ---------- + by : mapping, function, label, pd.Grouper or list of such + Used to determine the groups for the groupby. + If ``by`` is a function, it's called on each value of the object's + index. If a dict or Series is passed, the Series or dict VALUES + will be used to determine the groups (the Series' values are first + aligned; see ``.align()`` method). If a list or ndarray of length + equal to the selected axis is passed (see the `groupby user guide + `_), + the values are used as-is to determine the groups. A label or list + of labels may be passed to group by the columns in ``self``. + Notice that a tuple is interpreted as a (single) key. + axis : {0 or 'index', 1 or 'columns'}, default 0 + Split along rows (0) or columns (1). For `Series` this parameter + is unused and defaults to 0. + level : int, level name, or sequence of such, default None + If the axis is a MultiIndex (hierarchical), group by a particular + level or levels. Do not specify both ``by`` and ``level``. + as_index : bool, default True + Return object with group labels as the + index. Only relevant for DataFrame input. as_index=False is + effectively "SQL-style" grouped output. This argument has no effect + on filtrations (see the `filtrations in the user guide + `_), + such as ``head()``, ``tail()``, ``nth()`` and in transformations + (see the `transformations in the user guide + `_). + dropna : bool, default True + If True, and if group keys contain NA values, NA values together + with row/column will be dropped. + If False, NA values will also be treated as the key in groups. + + Returns + ------- + SeriesGroupBy + Returns a groupby object that contains information about the groups. + """ + raise NotImplementedError("abstract method") + + def drop( + self, + labels=None, + ) -> Series | None: + """ + Return Series with specified index labels removed. + + Remove elements of a Series based on specifying the index labels. + When using a multi-index, labels on different levels can be removed + by specifying the level. + + Parameters + ---------- + labels : single label or list-like + Index labels to drop. + + Returns + ------- + Series or None + Series with specified index labels removed or None if ``inplace=True``. + + Raises + ------ + KeyError + If none of the labels are found in the index. + """ + raise NotImplementedError("abstract method") + + def reorder_levels(self, order: Sequence) -> Series: + """ + Rearrange index levels using input order. + + May not drop or duplicate levels. + + Args: + order: list of int representing new level order + Reference level by number or key. + + Returns: + type of caller (new object) + """ + raise NotImplementedError("abstract method") + + def droplevel(self, level): + """ + Return Series with requested index / column level(s) removed. + + Args: + level: int, str, or list-like + If a string is given, must be the name of a level + If list-like, elements must be names or positional indexes + of levels. + + Returns: + Series with requested index / column level(s) removed. + """ + raise NotImplementedError("abstract method") + + def fillna( + self, + value=None, + ) -> Series | None: + raise NotImplementedError("abstract method") + + def between( + self, + left, + right, + inclusive: Literal["both", "neither", "left", "right"] = "both", + ) -> Series: + """ + Return boolean Series equivalent to left <= series <= right. + + This function returns a boolean vector containing `True` wherever the + corresponding Series element is between the boundary values `left` and + `right`. NA values are treated as `False`. + + Parameters + ---------- + left : scalar or list-like + Left boundary. + right : scalar or list-like + Right boundary. + inclusive : {"both", "neither", "left", "right"} + Include boundaries. Whether to set each bound as closed or open. + + Returns + ------- + Series + Series representing whether each element is between left and + right (inclusive). + + """ + raise NotImplementedError("abstract method") + + def cumsum(self): + """ + Return cumulative sum over a DataFrame or Series axis. + + Returns a DataFrame or Series of the same size containing the cumulative + sum. + + Parameters + ---------- + axis : {{0 or 'index', 1 or 'columns'}}, default 0 + The index or the name of the axis. 0 is equivalent to None or 'index'. + For `Series` this parameter is unused and defaults to 0. + + Returns + ------- + scalar or Series + Return cumulative sum of scalar or Series. + """ + raise NotImplementedError("abstract method") + + def cummax(self): + """ + Return cumulative maximum over a DataFrame or Series axis. + + Returns a DataFrame or Series of the same size containing the cumulative + maximum. + + Parameters + ---------- + axis : {{0 or 'index', 1 or 'columns'}}, default 0 + The index or the name of the axis. 0 is equivalent to None or 'index'. + For `Series` this parameter is unused and defaults to 0. + + Returns + ------- + scalar or Series + Return cumulative maximum of scalar or Series. + """ + raise NotImplementedError("abstract method") + + def cummin(self): + """ + Return cumulative minimum over a DataFrame or Series axis. + + Returns a DataFrame or Series of the same size containing the cumulative + minimum. + + Parameters + ---------- + axis : {{0 or 'index', 1 or 'columns'}}, default 0 + The index or the name of the axis. 0 is equivalent to None or 'index'. + For `Series` this parameter is unused and defaults to 0. + skipna : bool, default True + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + *args, **kwargs + Additional keywords have no effect but might be accepted for + compatibility with NumPy. + + Returns + ------- + scalar or Series + Return cumulative minimum of scalar or Series. + """ + raise NotImplementedError("abstract method") + + def eq(self, other) -> Series: + """Return Equal of series and other, element-wise (binary operator eq). + + Equivalent to ``other == series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def ne(self, other) -> Series: + """Return Not equal of series and other, element-wise (binary operator ne). + + Equivalent to ``other != series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the comparison. + + """ + raise NotImplementedError("abstract method") + + def le(self, other) -> Series: + """Get 'less than or equal to' of series and other, element-wise (binary operator `<=`). + + Equivalent to ``series <= other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the comparison. + + """ + raise NotImplementedError("abstract method") + + def lt(self, other) -> Series: + """Get 'less than' of series and other, element-wise (binary operator `<`). + + Equivalent to ``series < other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the comparison. + + """ + raise NotImplementedError("abstract method") + + def ge(self, other) -> Series: + """Get 'greater than or equal to' of series and other, element-wise (binary operator `>=`). + + Equivalent to ``series >= other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the comparison. + + """ + raise NotImplementedError("abstract method") + + def gt(self, other) -> Series: + """Get 'less than or equal to' of series and other, element-wise (binary operator `<=`). + + Equivalent to ``series <= other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def add(self, other) -> Series: + """Return Addition of series and other, element-wise (binary operator add). + + Equivalent to ``series + other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def sub( + self, + other, + ) -> Series: + """Return Subtraction of series and other, element-wise (binary operator sub). + + Equivalent to ``series - other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def rsub(self, other) -> Series: + """Return Subtraction of series and other, element-wise (binary operator rsub). + + Equivalent to ``other - series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def mul(self, other) -> Series: + """Return Multiplication of series and other, element-wise (binary operator mul). + + Equivalent to ``other * series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def truediv(self, other) -> Series: + """Return Floating division of series and other, element-wise (binary operator truediv). + + Equivalent to ``series / other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def rtruediv(self, other) -> Series: + """Return Floating division of series and other, element-wise (binary operator rtruediv). + + Equivalent to ``other / series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def floordiv(self, other) -> Series: + """Return Integer division of series and other, element-wise (binary operator floordiv). + + Equivalent to ``series // other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def rfloordiv(self, other) -> Series: + """Return Integer division of series and other, element-wise (binary operator rfloordiv). + + Equivalent to ``other // series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def mod(self, other) -> Series: + """Return Modulo of series and other, element-wise (binary operator mod). + + Equivalent to ``series % other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def rmod(self, other) -> Series: + """Get Modulo of series and other, element-wise (binary operator `rmod`). + + Equivalent to ``other % series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other: Series, or scalar value + + Returns: + Series. The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def all( + self, + ): + """ + Return whether all elements are True, potentially over an axis. + + Returns True unless there at least one element within a series or along a + Dataframe axis that is False or equivalent (e.g. zero or empty). + + + Returns + ------- + scalar or Series + If level is specified, then, Series is returned; otherwise, scalar + is returned. + """ + raise NotImplementedError("abstract method") + + def any( + self, + ): + """ + Return whether any element is True, potentially over an axis. + + Returns False unless there is at least one element within a series or along + a Dataframe axis that is True or equivalent (e.g. non-zero or non-empty). + + + Returns + ------- + scalar or Series + If level is specified, then, Series is returned; otherwise, scalar + is returned. + """ + raise NotImplementedError("abstract method") + + def max( + self, + ): + """ + Return the maximum of the values over the requested axis. + + If you want the index of the maximum, use ``idxmax``. This is the equivalent + of the ``numpy.ndarray`` method ``argmax``. + + + Returns + ------- + scalar or scalar + """ + raise NotImplementedError("abstract method") + + def min( + self, + ): + """ + Return the maximum of the values over the requested axis. + + If you want the index of the minimum, use ``idxmin``. This is the equivalent + of the ``numpy.ndarray`` method ``argmin``. + + + Returns + ------- + scalar or scalar + """ + raise NotImplementedError("abstract method") + + def std( + self, + ): + """ + Return sample standard deviation over requested axis. + + Normalized by N-1 by default. This can be changed using the ddof argument. + + + Returns + ------- + scalar or Series (if level specified) + """ + raise NotImplementedError("abstract method") + + def var( + self, + ): + """ + Return unbiased variance over requested axis. + + Normalized by N-1 by default. This can be changed using the ddof argument. + + + Returns + ------- + scalar or Series (if level specified) + """ + raise NotImplementedError("abstract method") + + def sum(self): + """Return the sum of the values over the requested axis. + + This is equivalent to the method ``numpy.sum``. + + Returns: + scalar + """ + raise NotImplementedError("abstract method") + + def mean(self): + """Return the mean of the values over the requested axis. + + Returns: + scalar + """ + raise NotImplementedError("abstract method") + + def prod(self): + """Return the product of the values over the requested axis. + + Returns: + scalar + """ + raise NotImplementedError("abstract method") + + def kurt(self): + """Return unbiased kurtosis over requested axis. + + Kurtosis obtained using Fisher’s definition of kurtosis (kurtosis of normal == 0.0). Normalized by N-1. + + Returns + ------- + scalar or scalar + """ + raise NotImplementedError("abstract method") + + def where(self, cond, other): + """Replace values where the condition is False. + + Parameters + ---------- + cond: bool Series/DataFrame, array-like, or callable + Where cond is True, keep the original value. Where False, replace + with corresponding value from other. If cond is callable, it is + computed on the Series/DataFrame and should return boolean + Series/DataFrame or array. The callable must not change input + Series/DataFrame (though pandas doesn’t check it). + other: scalar, Series/DataFrame, or callable + Entries where cond is False are replaced with corresponding value + from other. If other is callable, it is computed on the + Series/DataFrame and should return scalar or Series/DataFrame. + The callable must not change input Series/DataFrame (though pandas + doesn’t check it). If not specified, entries will be filled with + the corresponding NULL value (np.nan for numpy dtypes, pd.NA for + extension dtypes). + + Returns + ------- + Series + """ + raise NotImplementedError("abstract method") + + def mask(self, cond, other): + """Replace values where the condition is True. + + Parameters + ---------- + cond: bool Series/DataFrame, array-like, or callable + Where cond is False, keep the original value. Where True, replace + with corresponding value from other. If cond is callable, it is + computed on the Series/DataFrame and should return boolean + Series/DataFrame or array. The callable must not change input + Series/DataFrame (though pandas doesn’t check it). + other: scalar, Series/DataFrame, or callable + Entries where cond is True are replaced with corresponding value + from other. If other is callable, it is computed on the + Series/DataFrame and should return scalar or Series/DataFrame. + The callable must not change input Series/DataFrame (though pandas + doesn’t check it). If not specified, entries will be filled with + the corresponding NULL value (np.nan for numpy dtypes, pd.NA for + extension dtypes). + + Returns + ------- + Series + """ + raise NotImplementedError("abstract method") + + def clip(self): + """Trim values at input threshold(s). + + Assigns values outside boundary to boundary values. Thresholds can be + singular values or array like, and in the latter case the clipping is + performed element-wise in the specified axis. + + Parameters + ---------- + + lower: float or array-like, default None + Minimum threshold value. All values below this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. + + upper: float or array-like, default None + Maximum threshold value. All values above this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. + + Returns + ------- + Series + """ + raise NotImplementedError("abstract method") + + def argmax(self): + """ + Return int position of the smallest value in the Series. + + If the minimum is achieved in multiple locations, the first row position is returned. + + Returns + ------- + Series + Row position of the maximum value. + """ + raise NotImplementedError("abstract method") + + def argmin(self): + """ + Return int position of the largest value in the Series. + + If the maximum is achieved in multiple locations, the first row position is returned. + + Returns + ------- + Series + Row position of the minimum value. + """ + raise NotImplementedError("abstract method") + + def rename(self, index, **kwargs) -> Series | None: + """ + Alter Series index labels or name. + + Function / dict values must be unique (1-to-1). Labels not contained in + a dict / Series will be left as-is. Extra labels listed don't throw an + error. + + Alternatively, change ``Series.name`` with a scalar value. + + Parameters + ---------- + index : scalar, hashable sequence, dict-like or function optional + Functions or dict-like are transformations to apply to + the index. + Scalar or hashable sequence-like will alter the ``Series.name`` + attribute. + + Returns + ------- + Series + Series with index labels + + """ + raise NotImplementedError("abstract method") + + def rename_axis(self, mapper, **kwargs): + """ + Set the name of the axis for the index or columns. + + Parameters + ---------- + mapper : scalar, list-like, optional + Value to set the axis name attribute. + + Returns + ------- + Series + """ + raise NotImplementedError("abstract method") + + def rolling( + self, + window, + min_periods: int | None = None, + ): + """ + Provide rolling window calculations. + + Parameters + ---------- + window : int, timedelta, str, offset, or BaseIndexer subclass + Size of the moving window. + + If an integer, the fixed number of observations used for + each window. + + If a timedelta, str, or offset, the time period of each window. Each + window will be a variable sized based on the observations included in + the time-period. This is only valid for datetimelike indexes. + To learn more about the offsets & frequency strings, please see `this link + `__. + + If a BaseIndexer subclass, the window boundaries + based on the defined ``get_window_bounds`` method. Additional rolling + keyword arguments, namely ``min_periods``, ``center``, ``closed`` and + ``step`` will be passed to ``get_window_bounds``. + + min_periods : int, default None + Minimum number of observations in window required to have a value; + otherwise, result is ``np.nan``. + + For a window that is specified by an offset, ``min_periods`` will default to 1. + + For a window that is specified by an integer, ``min_periods`` will default + to the size of the window. + + Returns + ------- + ``Window`` subclass if a ``win_type`` is passed + + ``Rolling`` subclass if ``win_type`` is not passed + """ + raise NotImplementedError("abstract method") + + def expanding(self, min_periods=1): + """ + Provide expanding window calculations. + + Parameters + ---------- + min_periods : int, default 1 + Minimum number of observations in window required to have a value; + otherwise, result is ``np.nan``. + + Returns + ------- + ``Expanding`` subclass + """ + raise NotImplementedError("abstract method") + + def value_counts( + self, + normalize: bool = False, + sort: bool = True, + ascending: bool = False, + *, + dropna: bool = True, + ) -> Series: + """ + Return a Series containing counts of unique values. + + The resulting object will be in descending order so that the + first element is the most frequently-occurring element. + Excludes NA values by default. + + Args: + normalize : bool, default False + If True then the object returned will contain the relative + frequencies of the unique values. + sort : bool, default True + Sort by frequencies. + ascending : bool, default False + Sort in ascending order. + dropna : bool, default True + Don't include counts of NaN. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + + @property + def str(self): + """ + Vectorized string functions for Series and Index. + + NAs stay NA unless handled otherwise by a particular method. Patterned + after Python’s string methods, with some inspiration from R’s stringr package. + """ + raise NotImplementedError("abstract property") diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py new file mode 100644 index 00000000000..81b9bd3d146 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -0,0 +1,187 @@ +class StringMethods: + """ + Vectorized string functions for Series and Index. + + NAs stay NA unless handled otherwise by a particular method. + Patterned after Python's string methods, with some inspiration from + R's stringr package. + """ + + def find(self, sub, start: int = 0, end=None): + """Return lowest indexes in each strings in the Series/Index. + + Each of returned indexes corresponds to the position where the + substring is fully contained between [start:end]. Return -1 on + failure. Equivalent to standard :meth:`str.find`. + + Args: + sub: + Substring being searched. + start: + Left edge index. + end: + Right edge index. + + Returns: + Series or Index of int. + """ + + raise NotImplementedError("abstract method") + + def len(self): + """Compute the length of each element in the Series/Index. + + The element may be a sequence (such as a string, tuple or list) or a collection + (such as a dictionary). + + Returns: + Series or Index of int + A Series or Index of integer values indicating the length of each + element in the Series or Index. + """ + + raise NotImplementedError("abstract method") + + def lower(self): + """Convert strings in the Series/Index to lowercase. + + Equivalent to :meth:`str.lower`. + + Returns: + Series or Index of object + """ + + raise NotImplementedError("abstract method") + + def slice(self, start=None, stop=None): + """Slice substrings from each element in the Series or Index. + + Args: + start : int, optional + Start position for slice operation. + stop : int, optional + Stop position for slice operation. + step : int, optional + Step size for slice operation. + + Returns: + Series or Index of object + Series or Index from sliced substring from original string object. + """ + + raise NotImplementedError("abstract method") + + def strip(self): + """Remove leading and trailing characters. + + Strip whitespaces (including newlines) or a set of specified characters + from each string in the Series/Index from left and right sides. + Replaces any non-strings in Series with NaNs. + Equivalent to :meth:`str.strip`. + + Returns: + Series or Index of object + """ + + raise NotImplementedError("abstract method") + + def upper(self): + """Convert strings in the Series/Index to uppercase. + + Equivalent to :meth:`str.upper`. + + Returns: + Series or Index of object + """ + + raise NotImplementedError("abstract method") + + def isnumeric(self): + """Check whether all characters in each string are numeric. + + This is equivalent to running the Python string method + :meth:`str.isnumeric` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + Series or Index of bool + Series or Index of boolean values with the same length as the original + Series/Index. + """ + + raise NotImplementedError("abstract method") + + def rstrip(self): + """Remove trailing characters. + + Strip whitespaces (including newlines) or a set of specified characters + from each string in the Series/Index from right side. + Replaces any non-strings in Series with NaNs. + Equivalent to :meth:`str.rstrip`. + + Returns: + Series or Index of object + """ + + raise NotImplementedError("abstract method") + + def lstrip(self): + """Remove leading characters. + + Strip whitespaces (including newlines) or a set of specified characters + from each string in the Series/Index from left side. + Replaces any non-strings in Series with NaNs. + Equivalent to :meth:`str.lstrip`. + + Returns: + Series or Index of object` + """ + + raise NotImplementedError("abstract method") + + def repeat(self, repeats: int): + """Duplicate each string in the Series or Index. + + Args: + repeats : int or sequence of int + Same value for all (int) or different value per (sequence). + + Returns: + Series or pandas.Index + Series or Index of repeated string objects specified by + input parameter repeats. + """ + + raise NotImplementedError("abstract method") + + def capitalize(self): + """Convert strings in the Series/Index to be capitalized. + + Equivalent to :meth:`str.capitalize`. + + Returns: + Series or Index of object + """ + + raise NotImplementedError("abstract method") + + def cat(self, others, *, join): + """Concatenate strings in the Series/Index with given separator. + + If `others` is specified, this function concatenates the Series/Index + and elements of `others` element-wise. + + Args: + others : Series + + join : {'left', 'outer'}, default 'left' + Determines the join-style between the calling Series and any + Series in `others` (objects without an index need + to match the length of the calling Series). To disable + alignment, use `.values` on any Series/Index/DataFrame in `others`. + + Returns: + Series + """ + + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/window/__init__.py b/third_party/bigframes_vendored/pandas/core/window/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/core/window/rolling.py b/third_party/bigframes_vendored/pandas/core/window/rolling.py new file mode 100644 index 00000000000..7a9239b70cf --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/window/rolling.py @@ -0,0 +1,37 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/window/rolling.py +""" +Provide a generic structure to support window functions, +similar to how we have a Groupby object. +""" + + +class Window: + """Provide window calculations.""" + + def count(self): + """Calculate the window count of non-NULL observations.""" + raise NotImplementedError("abstract method") + + def sum(self): + """Calculate the weighted window sum.""" + raise NotImplementedError("abstract method") + + def mean(self): + """Calculate the weighted window mean.""" + raise NotImplementedError("abstract method") + + def var(self): + """Calculate the weighted window variance.""" + raise NotImplementedError("abstract method") + + def std(self): + """Calculate the weighted window standard deviation.""" + raise NotImplementedError("abstract method") + + def max(self): + """Calculate the weighted window maximum.""" + raise NotImplementedError("abstract method") + + def min(self): + """Calculate the weighted window minimum.""" + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/io/__init__.py b/third_party/bigframes_vendored/pandas/io/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/io/common.py b/third_party/bigframes_vendored/pandas/io/common.py new file mode 100644 index 00000000000..057ed96721d --- /dev/null +++ b/third_party/bigframes_vendored/pandas/io/common.py @@ -0,0 +1,44 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/io/common.py +"""Common IO api utilities""" +from __future__ import annotations + +from collections import defaultdict +from typing import DefaultDict, Hashable, Sequence + + +def dedup_names( + names: Sequence[Hashable], + is_potential_multiindex: bool, +) -> Sequence[Hashable]: + """ + Rename column names if duplicates exist. + + Currently the renaming is done by appending a period and an autonumeric, + but a custom pattern may be supported in the future. + + Examples + -------- + >>> dedup_names(["x", "y", "x", "x"], is_potential_multiindex=False) + ['x', 'y', 'x.1', 'x.2'] + """ + names = list(names) # so we can index + counts: DefaultDict[Hashable, int] = defaultdict(int) + + for i, col in enumerate(names): + cur_count = counts[col] + + while cur_count > 0: + counts[col] = cur_count + 1 + + if is_potential_multiindex: + # for mypy + assert isinstance(col, tuple) + col = col[:-1] + (f"{col[-1]}.{cur_count}",) + else: + col = f"{col}.{cur_count}" + cur_count = counts[col] + + names[i] = col + counts[col] = cur_count + 1 + + return names diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py new file mode 100644 index 00000000000..4f4b1b8199f --- /dev/null +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -0,0 +1,37 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/io/gbq.py +""" Google BigQuery support """ + +from __future__ import annotations + +from typing import Iterable, Optional + + +class GBQIOMixin: + def read_gbq( + self, + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, + ): + """Loads DataFrame from Google BigQuery. + + Args: + query: + A SQL string to be executed or a BigQuery table to be read. The + table must be specified in the format of + `project.dataset.tablename` or `dataset.tablename`. + index_col: + Name of result column(s) to use for index in results DataFrame. + col_order: + List of BigQuery column names in the desired order for results + DataFrame. + max_results: + If set, limit the maximum number of rows to fetch from the + query results. + + Returns: + A DataFrame representing results of the query or table. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/io/parquet.py b/third_party/bigframes_vendored/pandas/io/parquet.py new file mode 100644 index 00000000000..ee2e9a65f22 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/io/parquet.py @@ -0,0 +1,22 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/io/parquet.py +""" parquet compat """ +from __future__ import annotations + + +class ParquetIOMixin: + def read_parquet( + self, + path: str, + ): + r"""Load a parquet object from the file path (local or GCS), returning a DataFrame. + + Args: + path: + Local or GCS path to parquet file. + + Note: + This method will not guarantee the same ordering as the file. + Instead, set a serialized index column as the index and sort by + that in the resulting DataFrame. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/io/parsers/__init__.py b/third_party/bigframes_vendored/pandas/io/parsers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py new file mode 100644 index 00000000000..e5ed39b54aa --- /dev/null +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -0,0 +1,110 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/io/parsers/readers.py +""" +Module contains tools for processing files into DataFrames or other objects + +GH#48849 provides a convenient way of deprecating keyword arguments +""" +from __future__ import annotations + +from typing import Any, Dict, Literal, MutableSequence, Optional, Sequence, Tuple, Union + +import numpy as np + + +class ReaderIOMixin: + def read_csv( + self, + filepath_or_buffer: str, + *, + sep: Optional[str] = ",", + header: Optional[int] = 0, + names: Optional[ + Union[MutableSequence[Any], np.ndarray[Any, Any], Tuple[Any, ...], range] + ] = None, + index_col: Optional[ + Union[int, str, Sequence[Union[str, int]], Literal[False]] + ] = None, + usecols=None, + dtype: Optional[Dict] = None, + engine: Optional[ + Literal["c", "python", "pyarrow", "python-fwf", "bigquery"] + ] = None, + encoding: Optional[str] = None, + **kwargs, + ): + r"""Loads DataFrame from comma-separated values (csv) file locally or from GCS. + + The CSV file data will be persisted as a temporary BigQuery table, which can be + automatically recycled after the Session is closed. + + Note: using `engine="bigquery"` will not guarantee the same ordering as the + file. Instead, set a serialized index column as the index and sort by + that in the resulting DataFrame. + + Args: + filepath_or_buffer: a string path including GCS and local file. + + sep: the separator for fields in a CSV file. For the BigQuery engine, the separator + can be any ISO-8859-1 single-byte character. To use a character in the range + 128-255, you must encode the character as UTF-8. Both engines support + `sep="\t"` to specify tab character as separator. Default engine supports + having any number of spaces as separator by specifying `sep="\s+"`. Separators + longer than 1 character are interpreted as regular expressions by the default + engine. BigQuery engine only supports single character separators. + + header: row number to use as the column names. + - ``None``: Instructs autodetect that there are no headers and data should be + read starting from the first row. + - ``0``: If using `engine="bigquery"`, Autodetect tries to detect headers in the + first row. If they are not detected, the row is read as data. Otherwise data + is read starting from the second row. When using default engine, pandas assumes + the first row contains column names unless the `names` argument is specified. + If `names` is provided, then the first row is ignored, second row is read as + data, and column names are inferred from `names`. + - ``N > 0``: If using `engine="bigquery"`, Autodetect skips N rows and tries + to detect headers in row N+1. If headers are not detected, row N+1 is just + skipped. Otherwise row N+1 is used to extract column names for the detected + schema. When using default engine, pandas will skip N rows and assumes row N+1 + contains column names unless the `names` argument is specified. If `names` is + provided, row N+1 will be ignored, row N+2 will be read as data, and column + names are inferred from `names`. + + names: a list of column names to use. If the file contains a header row and you + want to pass this parameter, then `header=0` should be passed as well so the + first (header) row is ignored. Only to be used with default engine. + + index_col: column(s) to use as the row labels of the DataFrame, either given as + string name or column index. `index_col=False` can be used with the default + engine only to enforce that the first column is not used as the index. Using + column index instead of column name is only supported with the default engine. + The BigQuery engine only supports having a single column name as the `index_col`. + Neither engine supports having a multi-column index. + + usecols: list of column names to use. The BigQuery engine only supports having a list + of string column names. Column indices and callable functions are only supported + with the default engine. Using the default engine, the column names in `usecols` + can be defined to correspond to column names provided with the `names` parameter + (ignoring the document's header row of column names). The order of the column + indices/names in `usecols` is ignored with the default engine. The order of the + column names provided with the BigQuery engine will be consistent in the resulting + dataframe. If using a callable function with the default engine, only column names + that evaluate to True by the callable function will be in the resulting dataframe. + + dtype: data type for data or columns. Only to be used with default engine. + + engine: type of engine to use. If `engine="bigquery"` is specified, then BigQuery's + load API will be used. Otherwise, the engine will be passed to `pandas.read_csv`. + + encoding: the character encoding of the data. The default encoding is `UTF-8` for both + engines. The default engine acceps a wide range of encodings. Refer to Python + documentation for a comprehensive list, + https://docs.python.org/3/library/codecs.html#standard-encodings + The BigQuery engine only supports `UTF-8` and `ISO-8859-1`. + + **kwargs: keyword arguments. + + + Returns: + A BigQuery DataFrames. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/pandas/_typing.py b/third_party/bigframes_vendored/pandas/pandas/_typing.py new file mode 100644 index 00000000000..e665339fc83 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/pandas/_typing.py @@ -0,0 +1,425 @@ +# Copied from https://github.com/pandas-dev/pandas/blob/main/pandas/_typing.py +from __future__ import annotations + +from datetime import datetime, timedelta, tzinfo +from os import PathLike +import sys +from typing import ( + Any, + Callable, + Dict, + Hashable, + Iterator, + List, + Literal, + Mapping, + Optional, + Protocol, + Sequence, + Tuple, +) +from typing import Type as type_t +from typing import TYPE_CHECKING, TypeVar, Union + +import numpy as np + +# To prevent import cycles place any internal imports in the branch below +# and use a string literal forward reference to it in subsequent types +# https://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles +if TYPE_CHECKING: + import numpy.typing as npt + from pandas import Interval + from pandas._libs import NaTType, Period, Timedelta, Timestamp + from pandas._libs.tslibs import BaseOffset + from pandas.arrays import DatetimeArray, TimedeltaArray + from pandas.core.arrays.base import ExtensionArray + from pandas.core.dtypes.dtypes import ExtensionDtype + from pandas.core.frame import DataFrame + from pandas.core.generic import NDFrame + from pandas.core.groupby.generic import DataFrameGroupBy, GroupBy, SeriesGroupBy + from pandas.core.indexes.base import Index + from pandas.core.internals import ( + ArrayManager, + BlockManager, + SingleArrayManager, + SingleBlockManager, + ) + from pandas.core.resample import Resampler + from pandas.core.series import Series + from pandas.core.window.rolling import BaseWindow + from pandas.io.formats.format import EngFormatter + + ScalarLike_co = Union[ + int, + float, + complex, + str, + bytes, + np.generic, + ] + + # numpy compatible types + NumpyValueArrayLike = Union[ScalarLike_co, npt.ArrayLike] + # Name "npt._ArrayLikeInt_co" is not defined [name-defined] + NumpySorter = Optional[npt._ArrayLikeInt_co] # type: ignore[name-defined] + + if sys.version_info >= (3, 10): + from typing import TypeGuard + else: + from typing_extensions import TypeGuard # pyright: reportUnusedImport = false + + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self # pyright: reportUnusedImport = false +else: + npt: Any = None + Self: Any = None + TypeGuard: Any = None + +HashableT = TypeVar("HashableT", bound=Hashable) + +# array-like + +ArrayLike = Union["ExtensionArray", np.ndarray] +AnyArrayLike = Union[ArrayLike, "Index", "Series"] +TimeArrayLike = Union["DatetimeArray", "TimedeltaArray"] + +# list-like + +# Cannot use `Sequence` because a string is a sequence, and we don't want to +# accept that. Could refine if https://github.com/python/typing/issues/256 is +# resolved to differentiate between Sequence[str] and str +ListLike = Union[AnyArrayLike, List, range] + +# scalars + +PythonScalar = Union[str, float, bool] +DatetimeLikeScalar = Union["Period", "Timestamp", "Timedelta"] +PandasScalar = Union["Period", "Timestamp", "Timedelta", "Interval"] +Scalar = Union[PythonScalar, PandasScalar, np.datetime64, np.timedelta64, datetime] +IntStrT = TypeVar("IntStrT", int, str) + + +# timestamp and timedelta convertible types + +TimestampConvertibleTypes = Union[ + "Timestamp", datetime, np.datetime64, np.int64, float, str +] +TimedeltaConvertibleTypes = Union[ + "Timedelta", timedelta, np.timedelta64, np.int64, float, str +] +Timezone = Union[str, tzinfo] + +ToTimestampHow = Literal["s", "e", "start", "end"] + +# NDFrameT is stricter and ensures that the same subclass of NDFrame always is +# used. E.g. `def func(a: NDFrameT) -> NDFrameT: ...` means that if a +# Series is passed into a function, a Series is always returned and if a DataFrame is +# passed in, a DataFrame is always returned. +NDFrameT = TypeVar("NDFrameT", bound="NDFrame") + +NumpyIndexT = TypeVar("NumpyIndexT", np.ndarray, "Index") + +AxisInt = int +Axis = Union[AxisInt, Literal["index", "columns", "rows"]] +IndexLabel = Union[Hashable, Sequence[Hashable]] +Level = Hashable +Shape = Tuple[int, ...] +Suffixes = Tuple[Optional[str], Optional[str]] +Ordered = Optional[bool] +JSONSerializable = Optional[Union[PythonScalar, List, Dict]] +Frequency = Union[str, "BaseOffset"] +Axes = ListLike + +RandomState = Union[ + int, + np.ndarray, + np.random.Generator, + np.random.BitGenerator, + np.random.RandomState, +] + +# dtypes +NpDtype = Union[str, np.dtype, type_t[Union[str, complex, bool, object]]] +Dtype = Union["ExtensionDtype", NpDtype] +AstypeArg = Union["ExtensionDtype", "npt.DTypeLike"] +# DtypeArg specifies all allowable dtypes in a functions its dtype argument +DtypeArg = Union[Dtype, Dict[Hashable, Dtype]] +DtypeObj = Union[np.dtype, "ExtensionDtype"] + +# converters +ConvertersArg = Dict[Hashable, Callable[[Dtype], Dtype]] + +# parse_dates +ParseDatesArg = Union[ + bool, List[Hashable], List[List[Hashable]], Dict[Hashable, List[Hashable]] +] + +# For functions like rename that convert one label to another +Renamer = Union[Mapping[Any, Hashable], Callable[[Any], Hashable]] + +# to maintain type information across generic functions and parametrization +T = TypeVar("T") + +# used in decorators to preserve the signature of the function it decorates +# see https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators +FuncType = Callable[..., Any] +F = TypeVar("F", bound=FuncType) + +# types of vectorized key functions for DataFrame::sort_values and +# DataFrame::sort_index, among others +ValueKeyFunc = Optional[Callable[["Series"], Union["Series", AnyArrayLike]]] +IndexKeyFunc = Optional[Callable[["Index"], Union["Index", AnyArrayLike]]] + +# types of `func` kwarg for DataFrame.aggregate and Series.aggregate +AggFuncTypeBase = Union[Callable, str] +AggFuncTypeDict = Dict[Hashable, Union[AggFuncTypeBase, List[AggFuncTypeBase]]] +AggFuncType = Union[ + AggFuncTypeBase, + List[AggFuncTypeBase], + AggFuncTypeDict, +] +AggObjType = Union[ + "Series", + "DataFrame", + "GroupBy", + "SeriesGroupBy", + "DataFrameGroupBy", + "BaseWindow", + "Resampler", +] + +PythonFuncType = Callable[[Any], Any] + +# filenames and file-like-objects +AnyStr_co = TypeVar("AnyStr_co", str, bytes, covariant=True) +AnyStr_contra = TypeVar("AnyStr_contra", str, bytes, contravariant=True) + + +class BaseBuffer(Protocol): + @property + def mode(self) -> str: + # for _get_filepath_or_buffer + ... + + def seek(self, __offset: int, __whence: int = ...) -> int: + # with one argument: gzip.GzipFile, bz2.BZ2File + # with two arguments: zip.ZipFile, read_sas + ... + + def seekable(self) -> bool: + # for bz2.BZ2File + ... + + def tell(self) -> int: + # for zip.ZipFile, read_stata, to_stata + ... + + +class ReadBuffer(BaseBuffer, Protocol[AnyStr_co]): + def read(self, __n: int = ...) -> AnyStr_co: + # for BytesIOWrapper, gzip.GzipFile, bz2.BZ2File + ... + + +class WriteBuffer(BaseBuffer, Protocol[AnyStr_contra]): + def write(self, __b: AnyStr_contra) -> Any: + # for gzip.GzipFile, bz2.BZ2File + ... + + def flush(self) -> Any: + # for gzip.GzipFile, bz2.BZ2File + ... + + +class ReadPickleBuffer(ReadBuffer[bytes], Protocol): + def readline(self) -> bytes: + ... + + +class WriteExcelBuffer(WriteBuffer[bytes], Protocol): + def truncate(self, size: int | None = ...) -> int: + ... + + +class ReadCsvBuffer(ReadBuffer[AnyStr_co], Protocol): + def __iter__(self) -> Iterator[AnyStr_co]: + # for engine=python + ... + + def fileno(self) -> int: + # for _MMapWrapper + ... + + def readline(self) -> AnyStr_co: + # for engine=python + ... + + @property + def closed(self) -> bool: + # for enine=pyarrow + ... + + +FilePath = Union[str, "PathLike[str]"] + +# for arbitrary kwargs passed during reading/writing files +StorageOptions = Optional[Dict[str, Any]] + + +# compression keywords and compression +CompressionDict = Dict[str, Any] +CompressionOptions = Optional[ + Union[Literal["infer", "gzip", "bz2", "zip", "xz", "zstd", "tar"], CompressionDict] +] + +# types in DataFrameFormatter +FormattersType = Union[ + List[Callable], Tuple[Callable, ...], Mapping[Union[str, int], Callable] +] +ColspaceType = Mapping[Hashable, Union[str, int]] +FloatFormatType = Union[str, Callable, "EngFormatter"] +ColspaceArgType = Union[ + str, int, Sequence[Union[str, int]], Mapping[Hashable, Union[str, int]] +] + +# Arguments for fillna() +FillnaOptions = Literal["backfill", "bfill", "ffill", "pad"] + +# internals +Manager = Union[ + "ArrayManager", "SingleArrayManager", "BlockManager", "SingleBlockManager" +] +SingleManager = Union["SingleArrayManager", "SingleBlockManager"] +Manager2D = Union["ArrayManager", "BlockManager"] + +# indexing +# PositionalIndexer -> valid 1D positional indexer, e.g. can pass +# to ndarray.__getitem__ +# ScalarIndexer is for a single value as the index +# SequenceIndexer is for list like or slices (but not tuples) +# PositionalIndexerTuple is extends the PositionalIndexer for 2D arrays +# These are used in various __getitem__ overloads +# TODO(typing#684): add Ellipsis, see +# https://github.com/python/typing/issues/684#issuecomment-548203158 +# https://bugs.python.org/issue41810 +# Using List[int] here rather than Sequence[int] to disallow tuples. +ScalarIndexer = Union[int, np.integer] +SequenceIndexer = Union[slice, List[int], np.ndarray] +PositionalIndexer = Union[ScalarIndexer, SequenceIndexer] +PositionalIndexerTuple = Tuple[PositionalIndexer, PositionalIndexer] +PositionalIndexer2D = Union[PositionalIndexer, PositionalIndexerTuple] +if TYPE_CHECKING: + TakeIndexer = Union[Sequence[int], Sequence[np.integer], npt.NDArray[np.integer]] +else: + TakeIndexer = Any + +# Shared by functions such as drop and astype +IgnoreRaise = Literal["ignore", "raise"] + +# Windowing rank methods +WindowingRankType = Literal["average", "min", "max"] + +# read_csv engines +CSVEngine = Literal["c", "python", "pyarrow", "python-fwf"] + +# read_json engines +JSONEngine = Literal["ujson", "pyarrow"] + +# read_xml parsers +XMLParsers = Literal["lxml", "etree"] + +# Interval closed type +IntervalLeftRight = Literal["left", "right"] +IntervalClosedType = Union[IntervalLeftRight, Literal["both", "neither"]] + +# datetime and NaTType +DatetimeNaTType = Union[datetime, "NaTType"] +DateTimeErrorChoices = Union[IgnoreRaise, Literal["coerce"]] + +# sort_index +SortKind = Literal["quicksort", "mergesort", "heapsort", "stable"] +NaPosition = Literal["first", "last"] + +# Arguments for nsmalles and n_largest +NsmallestNlargestKeep = Literal["first", "last", "all"] + +# quantile interpolation +QuantileInterpolation = Literal["linear", "lower", "higher", "midpoint", "nearest"] + +# plotting +PlottingOrientation = Literal["horizontal", "vertical"] + +# dropna +AnyAll = Literal["any", "all"] + +# merge +MergeHow = Literal["left", "right", "inner", "outer", "cross"] +MergeValidate = Literal[ + "one_to_one", + "1:1", + "one_to_many", + "1:m", + "many_to_one", + "m:1", + "many_to_many", + "m:m", +] + +# join +JoinHow = Literal["left", "right", "inner", "outer"] +JoinValidate = Literal[ + "one_to_one", + "1:1", + "one_to_many", + "1:m", + "many_to_one", + "m:1", + "many_to_many", + "m:m", +] + +# reindex +ReindexMethod = Union[FillnaOptions, Literal["nearest"]] + +MatplotlibColor = Union[str, Sequence[float]] +TimeGrouperOrigin = Union[ + "Timestamp", Literal["epoch", "start", "start_day", "end", "end_day"] +] +TimeAmbiguous = Union[Literal["infer", "NaT", "raise"], "npt.NDArray[np.bool_]"] +TimeNonexistent = Union[ + Literal["shift_forward", "shift_backward", "NaT", "raise"], timedelta +] +DropKeep = Literal["first", "last", False] +CorrelationMethod = Union[ + Literal["pearson", "kendall", "spearman"], Callable[[np.ndarray, np.ndarray], float] +] +AlignJoin = Literal["outer", "inner", "left", "right"] +DtypeBackend = Literal["pyarrow", "numpy_nullable"] + +TimeUnit = Literal["s", "ms", "us", "ns"] +OpenFileErrors = Literal[ + "strict", + "ignore", + "replace", + "surrogateescape", + "xmlcharrefreplace", + "backslashreplace", + "namereplace", +] + +# update +UpdateJoin = Literal["left"] + +# applymap +NaAction = Literal["ignore"] + +# from_dict +FromDictOrient = Literal["columns", "index", "tight"] + +# to_gbc +ToGbqIfexist = Literal["fail", "replace", "append"] + +# to_stata +ToStataByteorder = Literal[">", "<", "little", "big"] diff --git a/third_party/bigframes_vendored/sklearn/COPYING b/third_party/bigframes_vendored/sklearn/COPYING new file mode 100644 index 00000000000..b161c890897 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/COPYING @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2007-2023 The scikit-learn developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py new file mode 100644 index 00000000000..925bb76e1a1 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -0,0 +1,128 @@ +"""Base classes for all estimators.""" + +# Author: Gael Varoquaux +# License: BSD 3 clause +# Original location: https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/base.py + +from __future__ import annotations + +import inspect +from typing import Any, Dict, List + + +class BaseEstimator: + """Base class for all estimators. + + Notes: + All estimators should specify all the parameters that can be set + at the class level in their ``__init__`` as explicit keyword + arguments (no ``*args`` or ``**kwargs``). + """ + + @classmethod + def _get_param_names(cls: type[BaseEstimator]) -> List[str]: + """Get parameter names for the estimator""" + # fetch the constructor or the original constructor before + # deprecation wrapping if any + init = getattr(cls.__init__, "deprecated_original", cls.__init__) + if init is object.__init__: + # No explicit constructor to introspect + return [] + + # introspect the constructor arguments to find the model parameters + # to represent + init_signature = inspect.signature(init) + # Consider the constructor parameters excluding 'self' + parameters = [ + p + for p in init_signature.parameters.values() + if p.name != "self" and p.kind != p.VAR_KEYWORD + ] + for p in parameters: + if p.kind == p.VAR_POSITIONAL: + raise RuntimeError( + "Estimators should always " + "specify their parameters in the signature" + " of their __init__ (no varargs)." + " %s with constructor %s doesn't " + " follow this convention." % (cls, init_signature) + ) + # Extract and sort argument names excluding 'self' + return sorted([p.name for p in parameters]) + + def get_params(self, deep: bool = True) -> Dict[str, Any]: + """Get parameters for this estimator. + + Args: + deep: + Default ``True``. If True, will return the parameters for this + estimator and contained subobjects that are estimators. + + Returns: + A dictionary of parameter names mapped to their values. + """ + out: Dict = dict() + for key in self._get_param_names(): + value = getattr(self, key) + if deep and hasattr(value, "get_params") and not isinstance(value, type): + deep_items = value.get_params().items() + out.update((key + "__" + k, val) for k, val in deep_items) + out[key] = value + return out + + +class ClassifierMixin: + """Mixin class for all classifiers.""" + + _estimator_type = "classifier" + + def score(self, X, y): + """Return the mean accuracy on the given test data and labels. + + In multi-label classification, this is the subset accuracy + which is a harsh metric since you require for each sample that + each label set be correctly predicted. + + Args: + X: + DataFrame of shape (n_samples, n_features). Test samples. + + y: + DataFrame of shape (n_samples,) or (n_samples, n_outputs). True + labels for `X`. + + Returns: + A DataFrame of the evaluation result. + """ + raise NotImplementedError("abstract method") + + +class RegressorMixin: + """Mixin class for all regression estimators.""" + + _estimator_type = "regressor" + + def score(self, X, y): + """Return the evaluation metrics of the model. + + Args: + X: + DataFrame of shape (n_samples, n_features). Test samples. For + some estimators this may be a precomputed kernel matrix or a + list of generic objects instead with shape + ``(n_samples, n_samples_fitted)``, where ``n_samples_fitted`` + is the number of samples used in the fitting for the estimator. + + y: + DataFrame of shape (n_samples,) or (n_samples, n_outputs). True + values for `X`. + + Returns: + A DataFrame of the evaluation result. + """ + raise NotImplementedError("abstract method") + + +class MetaEstimatorMixin: + _required_parameters = ["estimator"] + """Mixin class for all meta estimators in scikit-learn.""" diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py new file mode 100644 index 00000000000..8f67cab1c31 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -0,0 +1,71 @@ +"""K-means clustering.""" + +# Authors: Gael Varoquaux +# Thomas Rueckstiess +# James Bergstra +# Jan Schlueter +# Nelle Varoquaux +# Peter Prettenhofer +# Olivier Grisel +# Mathieu Blondel +# Robert Layton +# License: BSD 3 clause + +from abc import ABC +from typing import List, Optional + +from third_party.bigframes_vendored.sklearn.base import BaseEstimator + + +class _BaseKMeans(BaseEstimator, ABC): + """Base class for KMeans and MiniBatchKMeans""" + + def predict(self, X): + """Predict the closest cluster each sample in X belongs to. + + Args: + X: + DataFrame of shape (n_samples, n_features). The data matrix for + which we want to get the predictions. + + Returns: + DataFrame of shape (n_samples,), containing the class labels for + each sample. + """ + raise NotImplementedError("abstract method") + + +class KMeans(_BaseKMeans): + """K-Means clustering. + + Args: + n_clusters: int, default=8 + The number of clusters to form as well as the number of centroids to generate. + Default to 8. + """ + + def fit( + self, + X, + y=None, + transforms: Optional[List[str]] = None, + ): + """Compute k-means clustering. + + Args: + X: + DataFrame of shape (n_samples, n_features). Training data. + y: Ignored + Not used, present here for API consistency by convention. + + transforms: + An optional list of SQL expressions to apply over top of the + model inputs as preprocessing. This preprocessing will be + automatically reapplied to new input data (e.g. in .predict), + and may contain steps (like ML.STANDARD_SCALER) that fit to the + training data. + + Returns: + Fitted Estimator. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py new file mode 100644 index 00000000000..dadfa5d0131 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py @@ -0,0 +1,61 @@ +"""Utilities for meta-estimators""" +# Author: Joel Nothman +# Andreas Mueller +# License: BSD + + +from abc import ABCMeta + +from third_party.bigframes_vendored.sklearn.base import BaseEstimator + + +class _BaseComposition(BaseEstimator, metaclass=ABCMeta): + """Handles parameter management for classifiers composed of named estimators.""" + + +class ColumnTransformer(_BaseComposition): + """Applies transformers to columns of BigQuery DataFrames. + + This estimator allows different columns or column subsets of the input + to be transformed separately and the features generated by each transformer + will be concatenated to form a single feature space. + This is useful for heterogeneous or columnar data, to combine several + feature extraction mechanisms or transformations into a single transformer. + + Args: + transformers: + List of (name, transformer, columns) tuples specifying the transformer + objects to be applied to subsets of the data. + """ + + def fit( + self, + X, + ): + """Fit all transformers using X. + + Args: + X: + DataFrame of shape (n_samples, n_features). Training vector, + where `n_samples` is the number of samples and `n_features` is + the number of features. + + Returns: + Fitted estimator. + """ + raise NotImplementedError("abstract method") + + def transform( + self, + X, + ): + """Transform X separately by each transformer, concatenate results. + + Args: + X: + The DataFrame to be transformed by subset. + + Returns: + Transformed result. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py new file mode 100644 index 00000000000..6f0d17b2e6d --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -0,0 +1,53 @@ +""" Principal Component Analysis. +""" + +# Author: Alexandre Gramfort +# Olivier Grisel +# Mathieu Blondel +# Denis A. Engemann +# Michael Eickenberg +# Giorgio Patrini +# +# License: BSD 3 clause + +from abc import ABCMeta + +from third_party.bigframes_vendored.sklearn.base import BaseEstimator + + +class PCA(BaseEstimator, metaclass=ABCMeta): + """Principal component analysis (PCA). + + Linear dimensionality reduction using Singular Value Decomposition of the + data to project it to a lower dimensional space. The input data is centered + but not scaled for each feature before applying the SVD. + + It uses the LAPACK implementation of the full SVD or a randomized truncated + SVD by the method of Halko et al. 2009, depending on the shape of the input + data and the number of components to extract. + + It can also use the scipy.sparse.linalg ARPACK implementation of the + truncated SVD. + + Args: + n_components: Optional[int] + Number of components to keep. if n_components is not set all components are kept. + + """ + + def fit( + self, + X, + ): + """Fit the model according to the given training data. + + Args: + X: + DataFrame of shape (n_samples, n_features). Training vector, + where `n_samples` is the number of samples and `n_features` is + the number of features. + + Returns: + Fitted estimator. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py new file mode 100644 index 00000000000..c91821c7622 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -0,0 +1,191 @@ +""" +Forest of trees-based ensemble methods. +Those methods include random forests and extremely randomized trees. +The module structure is the following: +- The ``BaseForest`` base class implements a common ``fit`` method for all + the estimators in the module. The ``fit`` method of the base ``Forest`` + class calls the ``fit`` method of each sub-estimator on random samples + (with replacement, a.k.a. bootstrap) of the training set. + The init of the sub-estimator is further delegated to the + ``BaseEnsemble`` constructor. +- The ``ForestClassifier`` and ``ForestRegressor`` base classes further + implement the prediction logic by computing an average of the predicted + outcomes of the sub-estimators. +- The ``RandomForestClassifier`` and ``RandomForestRegressor`` derived + classes provide the user with concrete implementations of + the forest ensemble method using classical, deterministic + ``DecisionTreeClassifier`` and ``DecisionTreeRegressor`` as + sub-estimator implementations. +- The ``ExtraTreesClassifier`` and ``ExtraTreesRegressor`` derived + classes provide the user with concrete implementations of the + forest ensemble method using the extremely randomized trees + ``ExtraTreeClassifier`` and ``ExtraTreeRegressor`` as + sub-estimator implementations. +Single and multi-output problems are both handled. +""" + +# Authors: Gilles Louppe +# Brian Holt +# Joly Arnaud +# Fares Hedayati +# +# License: BSD 3 clause + +from abc import ABCMeta + +from ..base import BaseEstimator, ClassifierMixin, MetaEstimatorMixin, RegressorMixin + + +class BaseForest(MetaEstimatorMixin, BaseEstimator, metaclass=ABCMeta): + """ + Base class for forests of trees. + """ + + def fit(self, X, y): + """Build a forest of trees from the training set (X, y). + + Args: + X: + DataFrame of shape (n_samples, n_features). Training data. + + y: + DataFrame of shape (n_samples,) or (n_samples, n_targets). + Target values. Will be cast to X's dtype if necessary. + + Returns: + Fitted Estimator. + """ + raise NotImplementedError("abstract method") + + +class ForestRegressor(RegressorMixin, BaseForest, metaclass=ABCMeta): + """ + Base class for forest of trees-based regressors. + """ + + def predict(self, X): + """Predict regression target for X. + + The predicted regression target of an input sample is computed as the + mean predicted regression targets of the trees in the forest. + + Args: + X: + DataFrame of shape (n_samples, n_features). The data matrix for + which we want to get the predictions. + + Returns: + The predicted values. + """ + raise NotImplementedError("abstract method") + + +class RandomForestRegressor(ForestRegressor): + """A random forest regressor. + + A random forest is a meta estimator that fits a number of classifying + decision trees on various sub-samples of the dataset and uses averaging + to improve the predictive accuracy and control over-fitting. + + Args: + num_parallel_tree: Optional[int] + Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. + tree_method: Optional[str] + Specify which tree method to use. Default to "auto". If this parameter is set to + default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + "hist". + min_child_weight : Optional[float] + Minimum sum of instance weight(hessian) needed in a child. Default to 1. + colsample_bytree : Optional[float] + Subsample ratio of columns when constructing each tree. Default to 1.0. The value should be between 0 and 1. + colsample_bylevel : Optional[float] + Subsample ratio of columns for each level. Default to 1.0. The value should be between 0 and 1. + colsample_bynode : Optional[float] + Subsample ratio of columns for each split. Default to 0.8. The value should be between 0 and 1. + gamma : Optional[float] + (min_split_loss) Minimum loss reduction required to make a further partition on a + leaf node of the tree. Default to 0.0. + max_depth : Optional[int] + Maximum tree depth for base learners. Default to 15. The value should be greater than 0 and less than 1. + subsample : Optional[float] + Subsample ratio of the training instance. Default to 0.8. The value should be greater than 0 and less than 1. + reg_alpha : Optional[float] + L1 regularization term on weights (xgb's alpha). Default to 0.0. + reg_lambda : Optional[float] + L2 regularization term on weights (xgb's lambda). Default to 1.0. + early_stop: Optional[bool] + Whether training should stop after the first iteration. Default to True. + min_rel_progress: Optional[float] + Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + enable_global_explain: Optional[bool] + Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. + xgboost_version: Optional[str] + Specifies the Xgboost version for model training. Default to "0.9". Possible values: "0.9", "1.1". + """ + + +class ForestClassifier(ClassifierMixin, BaseForest, metaclass=ABCMeta): + """ + Base class for forest of trees-based classifiers. + """ + + def predict(self, X): + """Predict regression target for X. + + The predicted regression target of an input sample is computed as the + mean predicted regression targets of the trees in the forest. + + Args: + X: + DataFrame of shape (n_samples, n_features). The data matrix for + which we want to get the predictions. + + Returns: + The predicted values. + """ + raise NotImplementedError("abstract method") + + +class RandomForestClassifier(ForestClassifier): + """ + A random forest classifier. + + A random forest is a meta estimator that fits a number of decision tree + classifiers on various sub-samples of the dataset and uses averaging to + improve the predictive accuracy and control over-fitting. + + Args: + num_parallel_tree: Optional[int] + Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. + tree_method: Optional[str] + Specify which tree method to use. Default to "auto". If this parameter is set to + default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + "hist". + min_child_weight : Optional[float] + Minimum sum of instance weight(hessian) needed in a child. Default to 1. + colsample_bytree : Optional[float] + Subsample ratio of columns when constructing each tree. Default to 1.0. The value should be between 0 and 1. + colsample_bylevel : Optional[float] + Subsample ratio of columns for each level. Default to 1.0. The value should be between 0 and 1. + colsample_bynode : Optional[float] + Subsample ratio of columns for each split. Default to 0.8. The value should be between 0 and 1. + gamma : Optional[float] + (min_split_loss) Minimum loss reduction required to make a further partition on a + leaf node of the tree. Default to 0.0. + max_depth : Optional[int] + Maximum tree depth for base learners. Default to 15. The value should be greater than 0 and less than 1. + subsample : Optional[float] + Subsample ratio of the training instance. Default to 0.8. The value should be greater than 0 and less than 1. + reg_alpha : Optional[float] + L1 regularization term on weights (xgb's alpha). Default to 0.0. + reg_lambda : Optional[float] + L2 regularization term on weights (xgb's lambda). Default to 1.0. + early_stop: Optional[bool] + Whether training should stop after the first iteration. Default to True. + min_rel_progress: Optional[float] + Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + enable_global_explain: Optional[bool] + Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. + xgboost_version: Optional[str] + Specifies the Xgboost version for model training. Default to "0.9". Possible values: "0.9", "1.1".ß + """ diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py new file mode 100644 index 00000000000..e67edac1aa4 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -0,0 +1,98 @@ +""" +Generalized Linear Models. +""" + +# Author: Alexandre Gramfort +# Fabian Pedregosa +# Olivier Grisel +# Vincent Michel +# Peter Prettenhofer +# Mathieu Blondel +# Lars Buitinck +# Maryan Morel +# Giorgio Patrini +# Maria Telenczuk +# License: BSD 3 clause +# Original location: https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/linear_model/_base.py + +from abc import ABCMeta +from typing import List, Optional + +from third_party.bigframes_vendored.sklearn.base import ( + BaseEstimator, + ClassifierMixin, + RegressorMixin, +) + + +class LinearModel(BaseEstimator, metaclass=ABCMeta): + def predict(self, X): + """Predict using the linear model. + + Args: + X: + DataFrame of shape (n_samples, n_features). Samples. + + Returns: + DataFrame of shape (n_samples,). Returns predicted values. + """ + raise NotImplementedError("abstract method") + + +class LinearClassifierMixin(ClassifierMixin): + def predict(self, X): + """Predict class labels for samples in X. + + Args: + X: + DataFrame of shape (n_samples, n_features). The data matrix for + which we want to get the predictions. + + Returns: + DataFrame of shape (n_samples,), containing the class labels for + each sample. + """ + raise NotImplementedError("abstract method") + + +class LinearRegression(RegressorMixin, LinearModel): + """Ordinary least squares Linear Regression. + + LinearRegression fits a linear model with coefficients w = (w1, ..., wp) + to minimize the residual sum of squares between the observed targets in + the dataset, and the targets predicted by the linear approximation. + + Args: + fit_intercept: + Default ``True``. Whether to calculate the intercept for this + model. If set to False, no intercept will be used in calculations + (i.e. data is expected to be centered). + """ + + def fit( + self, + X, + y, + transforms: Optional[List[str]] = None, + ): + """Fit linear model. + + Args: + X: + DataFrame of shape (n_samples, n_features). Training data. + + y: + DataFrame of shape (n_samples,) or (n_samples, n_targets). + Target values. Will be cast to X's dtype if necessary. + + transforms: + An optional list of SQL expressions to apply over top of the + model inputs as preprocessing. This preprocessing will be + automatically reapplied to new input data (e.g. in .predict), + and may contain steps (like ML.STANDARD_SCALER) that fit to the + training data. + + Returns: + Fitted Estimator. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py new file mode 100644 index 00000000000..6de5bf65e25 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -0,0 +1,83 @@ +""" +Logistic Regression +""" + +# Author: Gael Varoquaux +# Fabian Pedregosa +# Alexandre Gramfort +# Manoj Kumar +# Lars Buitinck +# Simon Wu +# Arthur Mensch ) + + Hsiang-Fu Yu, Fang-Lan Huang, Chih-Jen Lin (2011). Dual coordinate descent + methods for logistic regression and maximum entropy models. + Machine Learning 85(1-2):41-75. + https://www.csie.ntu.edu.tw/~cjlin/papers/maxent_dual.pdf + """ + + def fit( + self, + X, + y, + transforms: Optional[List[str]] = None, + ): + """Fit the model according to the given training data. + + Args: + X: + DataFrame of shape (n_samples, n_features). Training vector, + where `n_samples` is the number of samples and `n_features` is + the number of features. + + y: + DataFrame of shape (n_samples,). Target vector relative to X. + + transforms: + An optional list of SQL expressions to apply over top of the + model inputs as preprocessing. This preprocessing will be + automatically reapplied to new input data (e.g. in .predict), + and may contain steps (like ML.STANDARD_SCALER) that fit to the + training data. + + Returns: + Fitted estimator. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/metrics/_classification.py b/third_party/bigframes_vendored/sklearn/metrics/_classification.py new file mode 100644 index 00000000000..05fb5f6fb6e --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/metrics/_classification.py @@ -0,0 +1,173 @@ +"""Metrics to assess performance on classification task given class prediction. +Functions named as ``*_score`` return a scalar value to maximize: the higher +the better. +Function named as ``*_error`` or ``*_loss`` return a scalar value to minimize: +the lower the better. +""" + +# Authors: Alexandre Gramfort +# Mathieu Blondel +# Olivier Grisel +# Arnaud Joly +# Jochen Wersdorfer +# Lars Buitinck +# Joel Nothman +# Noel Dawe +# Jatin Shah +# Saurabh Jha +# Bernardo Stein +# Shangwu Yao +# Michal Karbownik +# License: BSD 3 clause + + +def accuracy_score(y_true, y_pred, normalize=True) -> float: + """Accuracy classification score. + + Args: + y_true: DataFrame of shape (n_samples,) + Ground truth (correct) labels. + y_pred: DataFrame of shape (n_samples,) + Predicted labels, as returned by a classifier. + normalize: bool, default=True + Default to True. If ``False``, return the number of correctly + classified samples. Otherwise, return the fraction of correctly + classified samples. + + Returns: + score: float. + If ``normalize == True``, return the fraction of correctly + classified samples (float), else returns the number of correctly + classified samples (int). + """ + raise NotImplementedError("abstract method") + + +def confusion_matrix( + y_true, + y_pred, +): + """Compute confusion matrix to evaluate the accuracy of a classification. + + By definition a confusion matrix :math:`C` is such that :math:`C_{i, j}` + is equal to the number of observations known to be in group :math:`i` and + predicted to be in group :math:`j`. + + Thus in binary classification, the count of true negatives is + :math:`C_{0,0}`, false negatives is :math:`C_{1,0}`, true positives is + :math:`C_{1,1}` and false positives is :math:`C_{0,1}`. + + Args: + y_true: DataFrame of shape (n_samples,) + Ground truth (correct) target values. + y_pred: DataFrame of shape (n_samples,) + Estimated targets as returned by a classifier. + + Returns: + C: DataFrame of shape (n_samples, n_features). + Confusion matrix whose i-th row and j-th + column entry indicates the number of + samples with true label being i-th class + and predicted label being j-th class. + """ + raise NotImplementedError("abstract method") + + +def recall_score( + y_true, + y_pred, + average: str = "binary", +): + """Compute the recall. + + The recall is the ratio ``tp / (tp + fn)`` where ``tp`` is the number of + true positives and ``fn`` the number of false negatives. The recall is + intuitively the ability of the classifier to find all the positive samples. + + The best value is 1 and the worst value is 0. + + Args: + y_true: DataFrame of shape (n_samples,) + Ground truth (correct) target values. + y_pred: DataFrame of shape (n_samples,) + Estimated targets as returned by a classifier. + average: {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ + default='binary' + This parameter is required for multiclass/multilabel targets. + Possible values are 'None', 'micro', 'macro', 'samples', 'weighted', 'binary'. + + Returns: + recall: float (if average is not None) or Series of float of shape \ + (n_unique_labels,). + Recall of the positive class in binary classification or weighted + average of the recall of each class for the multiclass task. + """ + raise NotImplementedError("abstract method") + + +def precision_score( + y_true, + y_pred, + average: str = "binary", +): + """Compute the precision. + + The precision is the ratio ``tp / (tp + fp)`` where ``tp`` is the number of + true positives and ``fp`` the number of false positives. The precision is + intuitively the ability of the classifier not to label as positive a sample + that is negative. + + The best value is 1 and the worst value is 0. + + Args: + y_true: DataFrame of shape (n_samples,) + Ground truth (correct) target values. + y_pred: DataFrame of shape (n_samples,) + Estimated targets as returned by a classifier. + average: {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ + default='binary' + This parameter is required for multiclass/multilabel targets. + Possible values are 'None', 'micro', 'macro', 'samples', 'weighted', 'binary'. + + Returns: + precision: float (if average is not None) or Series of float of shape \ + (n_unique_labels,). + Precision of the positive class in binary classification or weighted + average of the precision of each class for the multiclass task. + """ + raise NotImplementedError("abstract method") + + +def f1_score( + y_true, + y_pred, + average: str = "binary", +): + """Compute the F1 score, also known as balanced F-score or F-measure. + + The F1 score can be interpreted as a harmonic mean of the precision and + recall, where an F1 score reaches its best value at 1 and worst score at 0. + The relative contribution of precision and recall to the F1 score are + equal. The formula for the F1 score is: F1 = 2 * (precision * recall) / (precision + recall) + + In the multi-class and multi-label case, this is the average of + the F1 score of each class with weighting depending on the ``average`` + parameter. + + Args: + y_true: DataFrame of shape (n_samples,) + Ground truth (correct) target values. + y_pred: DataFrame of shape (n_samples,) + Estimated targets as returned by a classifier. + average: {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ + default='binary' + This parameter is required for multiclass/multilabel targets. + Possible values are 'None', 'micro', 'macro', 'samples', 'weighted', 'binary'. + + Returns: + f1_score: float or Series of float, shape = [n_unique_labels] + F1 score of the positive class in binary classification or weighted + average of the F1 scores of each class for the multiclass task. + + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py new file mode 100644 index 00000000000..b7625171b56 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py @@ -0,0 +1,100 @@ +"""Metrics to assess performance on classification task given scores. +Functions named as ``*_score`` return a scalar value to maximize: the higher +the better. +Function named as ``*_error`` or ``*_loss`` return a scalar value to minimize: +the lower the better. +""" + +# Authors: Alexandre Gramfort +# Mathieu Blondel +# Olivier Grisel +# Arnaud Joly +# Jochen Wersdorfer +# Lars Buitinck +# Joel Nothman +# Noel Dawe +# Michal Karbownik +# License: BSD 3 clause + + +def auc(x, y) -> float: + """Compute Area Under the Curve (AUC) using the trapezoidal rule. + + This is a general function, given points on a curve. For computing the + area under the ROC-curve, see :func:`roc_auc_score`. For an alternative + way to summarize a precision-recall curve, see + :func:`average_precision_score`. + + Args: + x : DataFrame of shape (n_samples,) + X coordinates. These must be either monotonic increasing or monotonic + decreasing. + y : DataFrame of shape (n_samples,) + Y coordinates. + + Returns: + auc : float + Area Under the Curve. + """ + raise NotImplementedError("abstract method") + + +def roc_auc_score(y_true, y_score) -> float: + """Compute Area Under the Receiver Operating Characteristic Curve (ROC AUC) \ + from prediction scores. + + Args: + y_true: DataFrame of shape (n_samples,) + True labels or binary label indicators. The binary and multiclass cases + expect labels with shape (n_samples,) while the multilabel case expects + binary label indicators with shape (n_samples, n_classes). + y_score: DataFrame of shape (n_samples,) + Target scores. + * In the binary case, it corresponds to an array of shape + `(n_samples,)`. Both probability estimates and non-thresholded + decision values can be provided. The probability estimates correspond + to the **probability of the class with the greater label**, + i.e. `estimator.classes_[1]` and thus + `estimator.predict_proba(X, y)[:, 1]`. The decision values + corresponds to the output of `estimator.decision_function(X, y)`. + + Returns: + auc: float. + Area Under the Curve score. + """ + raise NotImplementedError("abstract method") + + +def roc_curve( + y_true, + y_score, + drop_intermediate: bool = True, +): + """Compute Receiver operating characteristic (ROC). + + Args: + y_true: DataFrame of shape (n_samples,) + True binary labels. If labels are not either {-1, 1} or {0, 1}, then + pos_label should be explicitly given. + y_score: DataFrame of shape (n_samples,) + Target scores, can either be probability estimates of the positive + class, confidence values, or non-thresholded measure of decisions + (as returned by "decision_function" on some classifiers). + drop_intermediate: bool, default=True + Default to True. Whether to drop some suboptimal thresholds which would not appear + on a plotted ROC curve. This is useful in order to create lighter + ROC curves. + + Returns: + fpr: + Increasing false positive rates such that element i is the false + positive rate of predictions with score >= `thresholds[i]`. + tpr: + Increasing true positive rates such that element `i` is the true + positive rate of predictions with score >= `thresholds[i]`. + thresholds: + Decreasing thresholds on the decision function used to compute + fpr and tpr. `thresholds[0]` represents no instances being predicted + and is arbitrarily set to `max(y_score) + 1`. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/metrics/_regression.py b/third_party/bigframes_vendored/sklearn/metrics/_regression.py new file mode 100644 index 00000000000..44ceab48a6a --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/metrics/_regression.py @@ -0,0 +1,53 @@ +"""Metrics to assess performance on regression task. +Functions named as ``*_score`` return a scalar value to maximize: the higher +the better. +Function named as ``*_error`` or ``*_loss`` return a scalar value to minimize: +the lower the better. +""" + +# Authors: Alexandre Gramfort +# Mathieu Blondel +# Olivier Grisel +# Arnaud Joly +# Jochen Wersdorfer +# Lars Buitinck +# Joel Nothman +# Karan Desai +# Noel Dawe +# Manoj Kumar +# Michael Eickenberg +# Konstantin Shmelkov +# Christian Lorentzen +# Ashutosh Hathidara +# Uttam kumar +# Sylvain Marie +# Ohad Michel +# License: BSD 3 clause + + +def r2_score(y_true, y_pred, force_finite=True) -> float: + """:math:`R^2` (coefficient of determination) regression score function. + + Best possible score is 1.0 and it can be negative (because the + model can be arbitrarily worse). In the general case when the true y is + non-constant, a constant model that always predicts the average y + disregarding the input features would get a :math:`R^2` score of 0.0. + + In the particular case when ``y_true`` is constant, the :math:`R^2` score + is not finite: it is either ``NaN`` (perfect predictions) or ``-Inf`` + (imperfect predictions). To prevent such non-finite numbers to pollute + higher-level experiments such as a grid search cross-validation, by default + these cases are replaced with 1.0 (perfect predictions) or 0.0 (imperfect + predictions) respectively. + + Args: + y_true: DataFrame of shape (n_samples,) + Ground truth (correct) target values. + y_pred: DataFrame of shape (n_samples,) + Estimated target values. + + Returns: + z: float. + The :math:`R^2` score. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/pipeline.py b/third_party/bigframes_vendored/sklearn/pipeline.py new file mode 100644 index 00000000000..7374edf990a --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/pipeline.py @@ -0,0 +1,83 @@ +""" +The :mod:`sklearn.pipeline` module implements utilities to build a composite +estimator, as a chain of transforms and estimators. +""" +# Author: Edouard Duchesnay +# Gael Varoquaux +# Virgile Fritsch +# Alexandre Gramfort +# Lars Buitinck +# License: BSD + +from abc import ABCMeta + +from third_party.bigframes_vendored.sklearn.base import BaseEstimator + + +class Pipeline(BaseEstimator, metaclass=ABCMeta): + """Pipeline of transforms with a final estimator. + + Sequentially apply a list of transforms and a final estimator. + Intermediate steps of the pipeline must be 'transforms', that is, they + must implement `fit` and `transform` methods. + The final estimator only needs to implement `fit`. + + The purpose of the pipeline is to assemble several steps that can be + cross-validated together while setting different parameters. This simplifies code, and allows deploying an estimator + and peprocessing together, e.g. with Pipeline.to_gbq(...) + """ + + def fit( + self, + X, + y, + ): + """Fit the model. + + Fit all the transformers one after the other and transform the + data. Finally, fit the transformed data using the final estimator. + + Args: + X: + A BigQuery DataFrames representing training data. Must match the + input requirements of the first step of the pipeline. + y: + A BigQuery DataFrames representing training targets, if applicable. + + Returns: + Pipeline with fitted steps. + """ + raise NotImplementedError("abstract method") + + +def score(self, X, y): + """Transform the data, and apply `score` with the final estimator. + + Call `transform` of each transformer in the pipeline. The transformed + data are finally passed to the final estimator that calls + `score` method. Only valid if the final estimator implements `score`. + + Args: + X: + A BigQuery DataFrames as evaluation data. + y: + A BigQuery DataFrames as evaluation labels. + + Returns: + A BigQuery DataFrames representing the result of calling + `score` on the final estimator. + """ + raise NotImplementedError("abstract method") + + +def predict(self, X): + """Predict the pipeline result for each sample in X. + + Args: + X: + A BigQuery DataFrames to predict. + + Returns: + A BigQuery DataFrames Dataframe representing predicted result. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py new file mode 100644 index 00000000000..7a44c4e66d3 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -0,0 +1,53 @@ +# Authors: Alexandre Gramfort +# Mathieu Blondel +# Olivier Grisel +# Andreas Mueller +# Eric Martin +# Giorgio Patrini +# Eric Chang +# License: BSD 3 clause + +from third_party.bigframes_vendored.sklearn.base import BaseEstimator + + +class StandardScaler(BaseEstimator): + """Standardize features by removing the mean and scaling to unit variance. + + The standard score of a sample `x` is calculated as:z = (x - u) / s + where `u` is the mean of the training samples or zero if `with_mean=False`, + and `s` is the standard deviation of the training samples or one if + `with_std=False`. + + Centering and scaling happen independently on each feature by computing + the relevant statistics on the samples in the training set. Mean and + standard deviation are then stored to be used on later data using + :meth:`transform`. + + Standardization of a dataset is a common requirement for many + machine learning estimators: they might behave badly if the + individual features do not more or less look like standard normally + distributed data (e.g. Gaussian with 0 mean and unit variance). + """ + + def fit(self, X): + """Compute the mean and std to be used for later scaling. + + Args: + X: + A dataframe with training data. + + Returns: + Fitted scaler. + """ + raise NotImplementedError("abstract method") + + def transform(self, X): + """Perform standardization by centering and scaling. + + Args: + X: + The DataFrame to be transformed. + + Returns: + Transformed result.""" + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py new file mode 100644 index 00000000000..6f0565ac136 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -0,0 +1,41 @@ +# Authors: Andreas Mueller +# Joris Van den Bossche +# License: BSD 3 clause + +from third_party.bigframes_vendored.sklearn.base import BaseEstimator + + +class OneHotEncoder(BaseEstimator): + """Encode categorical features as a one-hot format. + + The input to this transformer should be an array-like of integers or + strings, denoting the values taken on by categorical (discrete) features. + The features are encoded using a one-hot (aka 'one-of-K' or 'dummy') + encoding scheme. + + Note that this method deviates from Scikit-Learn; instead of producing sparse + binary columns, the encoding is a single column of STRUCT + """ + + def fit(self, X): + """Fit OneHotEncoder to X. + + Args: + X: + A dataframe with training data. + + Returns: + Fitted encoder. + """ + raise NotImplementedError("abstract method") + + def transform(self, X): + """Transform X using one-hot encoding. + + Args: + X: + The DataFrame to be transformed. + + Returns: + Transformed result.""" + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/xgboost/LICENSE b/third_party/bigframes_vendored/xgboost/LICENSE new file mode 100644 index 00000000000..82832bd6e46 --- /dev/null +++ b/third_party/bigframes_vendored/xgboost/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2019 by Contributors + + 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/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py new file mode 100644 index 00000000000..268484b2c23 --- /dev/null +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -0,0 +1,145 @@ +"""Scikit-Learn Wrapper interface for XGBoost.""" + +from typing import Any + +from ..sklearn.base import BaseEstimator as XGBModelBase +from ..sklearn.base import ClassifierMixin as XGBClassifierBase +from ..sklearn.base import RegressorMixin as XGBRegressorBase + + +class XGBModel(XGBModelBase): + def predict(self, X): + """Predict using the XGB model. + + Args: + X: + DataFrame of shape (n_samples, n_features). Samples. + + Returns: + DataFrame of shape (n_samples,). Returns predicted values. + """ + raise NotImplementedError("abstract method") + + def fit(self, X, y): + """Fit gradient boosting model. + + Note that calling ``fit()`` multiple times will cause the model object to be + re-fit from scratch. To resume training from a previous checkpoint, explicitly + pass ``xgb_model`` argument. + + Args: + X: + DataFrame of shape (n_samples, n_features). Training data. + + y: + DataFrame of shape (n_samples,) or (n_samples, n_targets). + Target values. Will be cast to X's dtype if necessary. + + Returns: + Fitted Estimator. + """ + raise NotImplementedError("abstract method") + + +class XGBClassifierMixIn: + """MixIn for classification.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class XGBRegressor(XGBModel, XGBRegressorBase): + """ + XGBoost regression model. + + Args: + num_parallel_tree: Optional[int] + Number of parallel trees constructed during each iteration. Default to 1. + booster: Optional[str] + Specify which booster to use: gbtree or dart. Default to "gbtree". + dart_normalized_type": Optional[str] + Type of normalization algorithm for DART booster. Possible values: "TREE", "FOREST". Default to "TREE". + tree_method: Optional[str] + Specify which tree method to use. Default to "auto". If this parameter is set to + default, XGBoost will choose the most conservative option available. + min_child_weight : Optional[float] + Minimum sum of instance weight(hessian) needed in a child. Default to 1. + colsample_bytree : Optional[float] + Subsample ratio of columns when constructing each tree. Default to 1.0. + colsample_bylevel : Optional[float] + Subsample ratio of columns for each level. Default to 1.0. + colsample_bynode : Optional[float] + Subsample ratio of columns for each split. Default to 1.0. + gamma : Optional[float] + (min_split_loss) Minimum loss reduction required to make a further partition on a + leaf node of the tree. Default to 0.0. + max_depth : Optional[int] + Maximum tree depth for base learners. Default to 6. + subsample : Optional[float] + Subsample ratio of the training instance. Default to 1.0. + reg_alpha : Optional[float] + L1 regularization term on weights (xgb's alpha). Default to 0.0. + reg_lambda : Optional[float] + L2 regularization term on weights (xgb's lambda). Default to 1.0. + early_stop: Optional[bool] + Whether training should stop after the first iteration. Default to True. + learning_rate: Optional[float] + Boosting learning rate (xgb's "eta"). Default to 0.3. + max_iterations: Optional[int] + Maximum number of rounds for boosting. Default to 20. + min_rel_progress: Optional[float] + Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + enable_global_explain: Optional[bool] + Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. + xgboost_version: Optional[str] + Specifies the Xgboost version for model training. Default to "0.9". + """ + + +class XGBClassifier(XGBModel, XGBClassifierMixIn, XGBClassifierBase): + """ + XGBoost classifier model. + + Args: + num_parallel_tree: Optional[int] + Number of parallel trees constructed during each iteration. Default to 1. + booster: Optional[str] + Specify which booster to use: gbtree or dart. Default to "gbtree". + dart_normalized_type": Optional[str] + Type of normalization algorithm for DART booster. Possible values: "TREE", "FOREST". Default to "TREE". + tree_method: Optional[str] + Specify which tree method to use. Default to "auto". If this parameter is set to + default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + "hist". + min_child_weight : Optional[float] + Minimum sum of instance weight(hessian) needed in a child. Default to 1. + colsample_bytree : Optional[float] + Subsample ratio of columns when constructing each tree. Default to 1.0. + colsample_bylevel : Optional[float] + Subsample ratio of columns for each level. Default to 1.0. + colsample_bynode : Optional[float] + Subsample ratio of columns for each split. Default to 1.0. + gamma : Optional[float] + (min_split_loss) Minimum loss reduction required to make a further partition on a + leaf node of the tree. Default to 0.0. + max_depth : Optional[int] + Maximum tree depth for base learners. Default to 6. + subsample : Optional[float] + Subsample ratio of the training instance. Default to 1.0. + reg_alpha : Optional[float] + L1 regularization term on weights (xgb's alpha). Default to 0.0. + reg_lambda : Optional[float] + L2 regularization term on weights (xgb's lambda). Default to 1.0. + early_stop: Optional[bool] + Whether training should stop after the first iteration. Default to True. + learning_rate: Optional[float] + Boosting learning rate (xgb's "eta"). Default to 0.3. + max_iterations: Optional[int] + Maximum number of rounds for boosting. Default to 20. + min_rel_progress: Optional[float] + Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + enable_global_explain: Optional[bool] + Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. + xgboost_version: Optional[str] + Specifies the Xgboost version for model training. Default to "0.9". Possible values: "0.9", "1.1". + """ From 264142e54498d50ec7427c8b3e45265f19fcd874 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 11 Aug 2023 14:50:04 -0500 Subject: [PATCH 003/921] chore: release 0.1.0 (#4) Syncs to internal commit 792794841b8ae0ca502723a2abead4f5fd5d11a0 Change-Id: Ib799a9c5e5a18d9b471756410aa5c87cb4932fe8 --- .kokoro/continuous/e2e.cfg | 2 +- .kokoro/continuous/nightly.cfg | 2 +- .kokoro/presubmit/e2e.cfg | 2 +- .kokoro/release-nightly.sh | 5 + .repo-metadata.json | 16 + CHANGELOG.md | 4 +- README.rst | 232 +- bigframes/_config/__init__.py | 11 + bigframes/_config/bigquery_options.py | 21 +- bigframes/_config/display_options.py | 3 +- bigframes/_config/sampling_options.py | 30 + bigframes/constants.py | 23 + bigframes/core/__init__.py | 372 ++- bigframes/core/blocks.py | 428 ++- bigframes/core/groupby/__init__.py | 169 +- bigframes/core/indexers.py | 62 +- bigframes/core/indexes/index.py | 75 +- bigframes/core/io.py | 90 + bigframes/core/joins/row_identity.py | 79 +- bigframes/core/joins/single_column.py | 235 +- bigframes/core/ordering.py | 201 +- bigframes/core/reshape/__init__.py | 105 +- bigframes/core/scalar.py | 29 +- bigframes/core/utils.py | 22 + bigframes/dataframe.py | 649 ++-- bigframes/dtypes.py | 69 +- bigframes/formatting_helpers.py | 207 +- bigframes/ml/base.py | 30 +- bigframes/ml/cluster.py | 69 +- bigframes/ml/compose.py | 72 +- bigframes/ml/core.py | 171 +- bigframes/ml/decomposition.py | 70 +- bigframes/ml/ensemble.py | 249 +- bigframes/ml/forecasting.py | 88 +- bigframes/ml/imported.py | 144 +- bigframes/ml/linear_model.py | 132 +- bigframes/ml/llm.py | 90 +- bigframes/ml/loader.py | 100 +- bigframes/ml/metrics.py | 220 +- bigframes/ml/model_selection.py | 46 +- bigframes/ml/pipeline.py | 144 +- bigframes/ml/preprocessing.py | 143 +- bigframes/ml/sql.py | 36 +- bigframes/ml/utils.py | 58 + bigframes/operations/__init__.py | 145 +- bigframes/operations/aggregations.py | 117 +- bigframes/operations/base.py | 15 +- bigframes/operations/strings.py | 109 +- bigframes/pandas/__init__.py | 99 +- bigframes/remote_function.py | 455 ++- bigframes/series.py | 354 ++- bigframes/session.py | 451 ++- docs/getting_started/index.rst | 27 - docs/index.rst | 4 +- docs/reference/bigframes.ml/cluster.rst | 2 + docs/reference/bigframes.ml/compose.rst | 2 + docs/reference/bigframes.ml/decomposition.rst | 2 + docs/reference/bigframes.ml/ensemble.rst | 7 + docs/reference/bigframes.ml/forecasting.rst | 2 + docs/reference/bigframes.ml/imported.rst | 2 + docs/reference/bigframes.ml/index.rst | 2 + docs/reference/bigframes.ml/linear_model.rst | 2 + docs/reference/bigframes.ml/llm.rst | 2 + docs/reference/bigframes.ml/metrics.rst | 2 + .../bigframes.ml/model_selection.rst | 2 + docs/reference/bigframes.ml/pipeline.rst | 2 + docs/reference/bigframes.ml/preprocessing.rst | 2 + docs/reference/bigframes/options.rst | 2 + .../bigframes.pandas/remote_functions.rst | 105 +- notebooks/01 - Getting Started.ipynb | 231 +- notebooks/02 - DataFrame.ipynb | 2 +- ...Using ML - SKLearn linear regression.ipynb | 1166 ++++--- ... - Using ML - Easy linear regression.ipynb | 1136 ++++--- ...6 - Using ML - Large Language Models.ipynb | 2 +- notebooks/10 - Regionalized.ipynb | 2800 +++++++++++++++++ noxfile.py | 54 +- samples/snippets/quickstart.py | 62 +- scripts/upload_to_google_drive.py | 71 + setup.py | 7 +- testing/constraints-3.9.txt | 4 +- tests/system/conftest.py | 24 + tests/system/large/ml/test_cluster.py | 35 +- tests/system/large/ml/test_compose.py | 8 +- tests/system/large/ml/test_core.py | 16 +- tests/system/large/ml/test_decomposition.py | 34 +- tests/system/large/ml/test_ensemble.py | 25 +- tests/system/large/ml/test_forecasting.py | 2 +- tests/system/large/ml/test_linear_model.py | 8 +- tests/system/large/ml/test_pipeline.py | 301 +- tests/system/large/test_remote_function.py | 20 +- tests/system/small/ml/conftest.py | 70 +- tests/system/small/ml/test_cluster.py | 117 +- tests/system/small/ml/test_core.py | 12 +- tests/system/small/ml/test_decomposition.py | 71 +- tests/system/small/ml/test_ensemble.py | 154 +- tests/system/small/ml/test_forecasting.py | 46 +- tests/system/small/ml/test_imported.py | 22 +- tests/system/small/ml/test_linear_model.py | 90 +- tests/system/small/ml/test_llm.py | 34 +- tests/system/small/ml/test_metrics.py | 278 +- tests/system/small/ml/test_model_selection.py | 43 +- tests/system/small/ml/test_preprocessing.py | 136 +- tests/system/small/ml/test_register.py | 8 +- .../system/small/operations/test_datetimes.py | 20 +- tests/system/small/operations/test_strings.py | 155 +- tests/system/small/test_dataframe.py | 418 ++- tests/system/small/test_dataframe_io.py | 100 +- tests/system/small/test_groupby.py | 54 +- tests/system/small/test_ibis.py | 39 + tests/system/small/test_index.py | 12 +- tests/system/small/test_multiindex.py | 224 +- tests/system/small/test_pandas.py | 57 +- tests/system/small/test_pandas_options.py | 194 ++ tests/system/small/test_progress_bar.py | 97 +- tests/system/small/test_remote_function.py | 313 +- tests/system/small/test_series.py | 622 +++- tests/system/small/test_session.py | 97 +- tests/system/small/test_window.py | 43 +- tests/unit/ml/test_sql.py | 28 +- tests/unit/test_core.py | 78 +- tests/unit/test_dtypes.py | 9 +- tests/unit/test_formatting_helper.py | 17 + tests/unit/test_formatting_helpers.py | 46 + tests/unit/test_pandas.py | 32 +- .../bigframes_vendored/ibis/LICENSE.txt | 202 ++ third_party/bigframes_vendored/ibis/README.md | 196 ++ .../bigframes_vendored/ibis/__init__.py | 0 .../ibis/backends/__init__.py | 0 .../ibis/backends/bigquery/__init__.py | 0 .../ibis/backends/bigquery/registry.py | 19 + .../bigframes_vendored/ibis/expr/__init__.py | 0 .../ibis/expr/operations/__init__.py | 4 + .../ibis/expr/operations/reductions.py | 23 + .../pandas/core/config_init.py | 46 +- .../bigframes_vendored/pandas/core/frame.py | 629 ++-- .../bigframes_vendored/pandas/core/generic.py | 146 +- .../pandas/core/groupby/__init__.py | 235 +- .../pandas/core/indexes/accessor.py | 13 +- .../pandas/core/indexes/base.py | 18 +- .../pandas/core/indexing.py | 30 +- .../pandas/core/reshape/__init__.py | 0 .../pandas/core/reshape/concat.py | 3 + .../pandas/core/reshape/tile.py | 65 + .../bigframes_vendored/pandas/core/series.py | 1185 +++---- .../pandas/core/strings/accessor.py | 183 +- .../bigframes_vendored/pandas/io/common.py | 5 +- .../bigframes_vendored/pandas/io/gbq.py | 12 +- .../bigframes_vendored/pandas/io/parquet.py | 15 +- .../pandas/io/parsers/readers.py | 53 +- .../bigframes_vendored/sklearn/base.py | 22 +- .../sklearn/cluster/_kmeans.py | 60 +- .../sklearn/compose/_column_transformer.py | 12 +- .../sklearn/decomposition/_pca.py | 48 +- .../sklearn/ensemble/_forest.py | 13 +- .../sklearn/linear_model/_base.py | 35 +- .../sklearn/linear_model/_logistic.py | 43 +- .../sklearn/metrics/_classification.py | 45 +- .../sklearn/metrics/_ranking.py | 18 +- .../sklearn/metrics/_regression.py | 7 +- .../bigframes_vendored/sklearn/pipeline.py | 37 +- .../sklearn/preprocessing/_data.py | 33 +- .../sklearn/preprocessing/_encoder.py | 49 +- .../bigframes_vendored/xgboost/sklearn.py | 99 +- 163 files changed, 15353 insertions(+), 4913 deletions(-) create mode 100644 .repo-metadata.json create mode 100644 bigframes/_config/sampling_options.py create mode 100644 bigframes/constants.py create mode 100644 bigframes/core/io.py create mode 100644 bigframes/core/utils.py create mode 100644 bigframes/ml/utils.py delete mode 100644 docs/getting_started/index.rst create mode 100644 docs/reference/bigframes.ml/ensemble.rst create mode 100644 notebooks/10 - Regionalized.ipynb create mode 100644 scripts/upload_to_google_drive.py create mode 100644 tests/system/small/test_ibis.py create mode 100644 tests/unit/test_formatting_helper.py create mode 100644 tests/unit/test_formatting_helpers.py create mode 100644 third_party/bigframes_vendored/ibis/LICENSE.txt create mode 100644 third_party/bigframes_vendored/ibis/README.md create mode 100644 third_party/bigframes_vendored/ibis/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/backends/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/backends/bigquery/registry.py create mode 100644 third_party/bigframes_vendored/ibis/expr/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/expr/operations/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/expr/operations/reductions.py create mode 100644 third_party/bigframes_vendored/pandas/core/reshape/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/reshape/tile.py diff --git a/.kokoro/continuous/e2e.cfg b/.kokoro/continuous/e2e.cfg index e2ca8bc78db..d875f360603 100644 --- a/.kokoro/continuous/e2e.cfg +++ b/.kokoro/continuous/e2e.cfg @@ -3,5 +3,5 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "system_prerelease system_noextras e2e notebook samples" + value: "system_noextras e2e notebook samples" } diff --git a/.kokoro/continuous/nightly.cfg b/.kokoro/continuous/nightly.cfg index 5d1ec3a8f75..ac34c4b0c60 100644 --- a/.kokoro/continuous/nightly.cfg +++ b/.kokoro/continuous/nightly.cfg @@ -2,7 +2,7 @@ env_vars: { key: "NOX_SESSION" - value: "unit unit_prerelease system system_prerelease cover lint lint_setup_py mypy format docs e2e notebook" + value: "unit system cover lint lint_setup_py mypy format docs e2e notebook" } build_file: "bigframes/.kokoro/release-nightly.sh" diff --git a/.kokoro/presubmit/e2e.cfg b/.kokoro/presubmit/e2e.cfg index e2ca8bc78db..d875f360603 100644 --- a/.kokoro/presubmit/e2e.cfg +++ b/.kokoro/presubmit/e2e.cfg @@ -3,5 +3,5 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "system_prerelease system_noextras e2e notebook samples" + value: "system_noextras e2e notebook samples" } diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh index 8742a0fc790..e3b6b4d4498 100755 --- a/.kokoro/release-nightly.sh +++ b/.kokoro/release-nightly.sh @@ -211,3 +211,8 @@ gcs_docs () { } gcs_docs + +if ! [ ${DRY_RUN} ]; then + # Copy docs and wheels to Google Drive + python3.10 scripts/upload_to_google_drive.py +fi diff --git a/.repo-metadata.json b/.repo-metadata.json new file mode 100644 index 00000000000..0efaa967d2c --- /dev/null +++ b/.repo-metadata.json @@ -0,0 +1,16 @@ +{ + "name": "bigframes", + "name_pretty": "A unified Python API in BigQuery", + "product_documentation": "https://cloud.google.com/bigquery", + "client_documentation": "https://cloud.google.com/python/docs/reference/bigframes/latest", + "issue_tracker": "https://github.com/googleapis/python-bigquery-dataframes/issues", + "release_level": "preview", + "language": "python", + "library_type": "INTEGRATION", + "repo": "googleapis/python-bigquery-dataframes", + "distribution_name": "bigframes", + "api_id": "bigquery.googleapis.com", + "default_version": "", + "codeowner_team": "@googleapis/api-bigquery-dataframe", + "api_shortname": "bigquery" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 39353a3f1ba..9d966220bd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,14 @@ [1]: https://pypi.org/project/bigframes/#history -## 0.1.0 (TBD) +## 0.1.0 (2023-08-11) ### Features * Add `bigframes.pandas` package with an API compatible with [pandas](https://pandas.pydata.org/). Supported data sources include: BigQuery SQL queries, BigQuery tables, CSV (local and GCS), Parquet (local - and GCS), and more. + and Cloud Storage), and more. * Add `bigframes.ml` package with an API inspired by [scikit-learn](https://scikit-learn.org/stable/). Train machine learning models and run batch predicition, powered by [BigQuery diff --git a/README.rst b/README.rst index 5f473e11890..6f51dfde23e 100644 --- a/README.rst +++ b/README.rst @@ -4,5 +4,233 @@ BigQuery DataFrames BigQuery DataFrames provides a Pythonic DataFrame and machine learning (ML) API powered by the BigQuery engine. -* ``bigframes.pandas`` provides a pandas-like API for analytics. -* ``bigframes.ml`` provides a Scikit-Learn-like API for ML. +* ``bigframes.pandas`` provides a pandas-compatible API for analytics. +* ``bigframes.ml`` provides a scikit-learn-like API for ML. + +Documentation +------------- + +* `BigQuery DataFrames sample notebooks `_ +* `BigQuery DataFrames API reference `_ +* `BigQuery documentation `_ + + +Quickstart +---------- + +Prerequisites +^^^^^^^^^^^^^ + +* Install the ``bigframes`` package. +* Create a Google Cloud project and billing account. +* When running locally, authenticate with application default credentials. See + the `gcloud auth application-default login + `_ + reference. + +Code sample +^^^^^^^^^^^ + +Import ``bigframes.pandas`` for a pandas-like interface. The ``read_gbq`` +method accepts either a fully-qualified table ID or a SQL query. + +.. code-block:: python + + import bigframes.pandas as bpd + + df1 = bpd.read_gbq("project.dataset.table") + df2 = bpd.read_gbq("SELECT a, b, c, FROM `project.dataset.table`") + +* `More code samples `_ + + +Locations +--------- +BigQuery DataFrames uses a +`BigQuery session `_ +internally to manage metadata on the service side. This session is tied to a +`location `_ . +BigQuery DataFrames uses the US multi-region as the default location, but you +can use ``session_options.location`` to set a different location. Every query +in a session is executed in the location where the session was created. + +If you want to reset the location of the created DataFrame or Series objects, +can reset the session by executing ``bigframes.pandas.reset_session()``. +After that, you can reuse ``bigframes.pandas.options.bigquery.location`` to +specify another location. + + +``read_gbq()`` requires you to specify a location if the dataset you are +querying is not in the US multi-region. If you try to read a table from another +location, you get a NotFound exception. + + +ML locations +------------ + +``bigframes.ml`` supports the same locations as BigQuery ML. BigQuery ML model +prediction and other ML functions are supported in all BigQuery regions. Support +for model training varies by region. For more information, see +`BigQuery ML locations `_. + + +Data types +---------- + +BigQuery DataFrames supports the following numpy and pandas dtypes: + +* ``numpy.dtype("O")`` +* ``pandas.BooleanDtype()`` +* ``pandas.Float64Dtype()`` +* ``pandas.Int64Dtype()`` +* ``pandas.StringDtype(storage="pyarrow")`` +* ``pandas.ArrowDtype(pa.date32())`` +* ``pandas.ArrowDtype(pa.time64("us"))`` +* ``pandas.ArrowDtype(pa.timestamp("us"))`` +* ``pandas.ArrowDtype(pa.timestamp("us", tz="UTC"))`` + +BigQuery DataFrames doesn’t support the following BigQuery data types: + +* ``ARRAY`` +* ``NUMERIC`` +* ``BIGNUMERIC`` +* ``INTERVAL`` +* ``STRUCT`` +* ``JSON`` + +All other BigQuery data types display as the object type. + + +Remote functions +---------------- + +BigQuery DataFrames gives you the ability to turn your custom scalar functions +into `BigQuery remote functions +`_ . Creating a remote +function in BigQuery DataFrames creates a BigQuery remote function, a `BigQuery +connection +`_ , +and a `Cloud Functions (2nd gen) function +`_ . + +BigQuery connections are created in the same location as the BigQuery +DataFrames session, using the name you provide in the custom function +definition. To view and manage connections, do the following: + +1. Go to `BigQuery Studio `__. +2. Select the project in which you created the remote function. +3. In the Explorer pane, expand that project and then expand External connections. + +BigQuery remote functions are created in the dataset you specify, or +in a dataset with the name ``bigframes_temp_location``, where location is +the location used by the BigQuery DataFrames session. For example, +``bigframes_temp_us_central1``. To view and manage remote functions, do +the following: + +1. Go to `BigQuery Studio `__. +2. Select the project in which you created the remote function. +3. In the Explorer pane, expand that project, expand the dataset in which you + created the remote function, and then expand Routines. + +To view and manage Cloud Functions functions, use the +`Functions `_ +page and use the project picker to select the project in which you +created the function. For easy identification, the names of the functions +created by BigQuery DataFrames are prefixed by ``bigframes-``. + +**Requirements** + +BigQuery DataFrames uses the ``gcloud`` command-line interface internally, +so you must run ``gcloud auth login`` before using remote functions. + +To use BigQuery DataFrames remote functions, you must enable the following APIs: + +* The BigQuery API (bigquery.googleapis.com) +* The BigQuery Connection API (bigqueryconnection.googleapis.com) +* The Cloud Functions API (cloudfunctions.googleapis.com) +* The Cloud Run API (run.googleapis.com) +* The Artifact Registry API (artifactregistry.googleapis.com) +* The Cloud Build API (cloudbuild.googleapis.com ) +* The Cloud Resource Manager API (cloudresourcemanager.googleapis.com) + +To use BigQuery DataFrames remote functions, you must be granted the +following IAM roles: + +* BigQuery Data Editor (roles/bigquery.dataEditor) +* BigQuery Connection Admin (roles/bigquery.connectionAdmin) +* Cloud Functions Developer (roles/cloudfunctions.developer) +* Service Account User (roles/iam.serviceAccountUser) +* Storage Object Viewer (roles/storage.objectViewer) +* Project IAM Admin (roles/resourcemanager.projectIamAdmin) + +**Limitations** + +* Remote functions take about 90 seconds to become available when you first create them. +* Trivial changes in the notebook, such as inserting a new cell or renaming a variable, + might cause the remote function to be re-created, even if these changes are unrelated + to the remote function code. +* BigQuery DataFrames does not differentiate any personal data you include in the remote + function code. The remote function code is serialized as an opaque box to deploy it as a + Cloud Functions function. +* The Cloud Functions (2nd gen) functions, BigQuery connections, and BigQuery remote + functions created by BigQuery DataFrames persist in Google Cloud. If you don’t want to + keep these resources, you must delete them separately using an appropriate Cloud Functions + or BigQuery interface. +* A project can have up to 1000 Cloud Functions (2nd gen) functions at a time. See Cloud + Functions quotas for all the limits. + + +Quotas and limits +----------------- + +`BigQuery quotas `_ +including hardware, software, and network components. + + +Session termination +------------------- + +Each BigQuery DataFrames DataFrame or Series object is tied to a BigQuery +DataFrames session, which is in turn based on a BigQuery session. BigQuery +sessions +`auto-terminate `_ +; when this happens, you can’t use previously +created DataFrame or Series objects and must re-create them using a new +BigQuery DataFrames session. You can do this by running +``bigframes.pandas.reset_session()`` and then re-running the BigQuery +DataFrames expressions. + + +Data processing location +------------------------ + +BigQuery DataFrames is designed for scale, which it achieves by keeping data +and processing on the BigQuery service. However, you can bring data into the +memory of your client machine by calling ``.execute()`` on a DataFrame or Series +object. If you choose to do this, the memory limitation of your client machine +applies. + + +License +------- + +BigQuery DataFrames is distributed with the `Apache-2.0 license +`_. + +It also contains code derived from the following third-party packages: + +* `Ibis `_ +* `pandas `_ +* `Python `_ +* `scikit-learn `_ +* `XGBoost `_ + +For details, see the `third_party +`_ +directory. + + +Contact Us +---------- + +For further help and provide feedback, you can email us at `bigframes-feedback@google.com `_. diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index 54c529fb0ca..e26eaf88009 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -19,6 +19,7 @@ import bigframes._config.bigquery_options as bigquery_options import bigframes._config.display_options as display_options +import bigframes._config.sampling_options as sampling_options class Options: @@ -27,6 +28,7 @@ class Options: def __init__(self): self._bigquery_options = bigquery_options.BigQueryOptions() self._display_options = display_options.DisplayOptions() + self._sampling_options = sampling_options.SamplingOptions() @property def bigquery(self) -> bigquery_options.BigQueryOptions: @@ -38,6 +40,15 @@ def display(self) -> display_options.DisplayOptions: """Options controlling object representation.""" return self._display_options + @property + def sampling(self) -> sampling_options.SamplingOptions: + """Options controlling downsampling when downloading data + to memory. The data will be downloaded into memory explicitly + (e.g., to_pandas, to_numpy, values) or implicitly (e.g., + matplotlib plotting). This option can be overriden by + parameters in specific functions.""" + return self._sampling_options + options = Options() """Global options for default session.""" diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 5eb5ba7bc85..a103abe1903 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -21,11 +21,14 @@ import google.api_core.exceptions import google.auth.credentials -SESSION_STARTED_MESSAGE = "Cannot change '{attribute}' once a session has started." +SESSION_STARTED_MESSAGE = ( + "Cannot change '{attribute}' once a session has started. " + "Call bigframes.pandas.reset_session() first, if you are using the bigframes.pandas API." +) class BigQueryOptions: - """Encapsulates configuration for working with an Session.""" + """Encapsulates configuration for working with a session.""" def __init__( self, @@ -55,7 +58,7 @@ def credentials(self, value: Optional[google.auth.credentials.Credentials]): @property def location(self) -> Optional[str]: - """Default location for jobs / datasets / tables. + """Default location for job, datasets, and tables. See: https://cloud.google.com/bigquery/docs/locations """ @@ -69,7 +72,7 @@ def location(self, value: Optional[str]): @property def project(self) -> Optional[str]: - """Google Cloud project ID to use for billing and default data project.""" + """Google Cloud project ID to use for billing and as the default project.""" return self._project @project.setter @@ -80,10 +83,12 @@ def project(self, value: Optional[str]): @property def remote_udf_connection(self) -> Optional[str]: - """Name of the BigQuery connection for the purpose of remote UDFs. + """Name of the BigQuery connection to use for remote functions. - It should be either pre created in `location`, or the user should have - privilege to create one. + You should either have the connection already created in the + location you have chosen, or you should have the Project IAM + Admin role to enable the service to create the connection for you if you + need it. """ return self._remote_udf_connection @@ -97,7 +102,7 @@ def remote_udf_connection(self, value: Optional[str]): @property def use_regional_endpoints(self) -> bool: - """In preview. Flag to connect to regional API endpoints. + """Flag to connect to regional API endpoints. Requires ``location`` to also be set. For example, set ``location='asia-northeast1'`` and ``use_regional_endpoints=True`` to diff --git a/bigframes/_config/display_options.py b/bigframes/_config/display_options.py index 43faad0e785..8bd2743f175 100644 --- a/bigframes/_config/display_options.py +++ b/bigframes/_config/display_options.py @@ -16,7 +16,7 @@ import contextlib import dataclasses -from typing import Optional +from typing import Literal, Optional import pandas as pd @@ -30,6 +30,7 @@ class DisplayOptions: max_columns: int = 20 max_rows: int = 25 progress_bar: Optional[str] = "auto" + repr_mode: Literal["head", "deferred"] = "head" @contextlib.contextmanager diff --git a/bigframes/_config/sampling_options.py b/bigframes/_config/sampling_options.py new file mode 100644 index 00000000000..1742dabe17a --- /dev/null +++ b/bigframes/_config/sampling_options.py @@ -0,0 +1,30 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Options for downsampling.""" + +import dataclasses +from typing import Literal, Optional + +import third_party.bigframes_vendored.pandas.core.config_init as vendored_pandas_config + + +@dataclasses.dataclass +class SamplingOptions: + __doc__ = vendored_pandas_config.sampling_options_doc + + max_download_size: Optional[int] = 500 + enable_downsampling: bool = False + sampling_method: Literal["head", "uniform"] = "uniform" + random_state: Optional[int] = None diff --git a/bigframes/constants.py b/bigframes/constants.py new file mode 100644 index 00000000000..3f3f1557336 --- /dev/null +++ b/bigframes/constants.py @@ -0,0 +1,23 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Constants used across BigQuery DataFrames. + +This module should not depend on any others in the package. +""" + +FEEDBACK_LINK = ( + "Share your usecase with the BigQuery DataFrames team at the " + "https://bit.ly/bigframes-feedback survey." +) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 70565fddf56..075e27b0c23 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -25,11 +25,15 @@ import ibis.expr.types as ibis_types import pandas +import bigframes.constants as constants import bigframes.core.guid from bigframes.core.ordering import ( + encode_order_string, ExpressionOrdering, + IntegerEncoding, OrderingColumnReference, - stringify_order_id, + reencode_order_string, + StringEncoding, ) import bigframes.dtypes import bigframes.operations as ops @@ -85,27 +89,22 @@ def __init__( self, session: Session, table: ibis_types.Table, - columns: Optional[Sequence[ibis_types.Value]] = None, + columns: Sequence[ibis_types.Value], hidden_ordering_columns: Optional[Sequence[ibis_types.Value]] = None, - ordering: Optional[ExpressionOrdering] = None, + ordering: ExpressionOrdering = ExpressionOrdering(), predicates: Optional[Collection[ibis_types.BooleanValue]] = None, ): self._session = session self._table = table self._predicates = tuple(predicates) if predicates is not None else () # TODO: Validate ordering - self._ordering = ordering or ExpressionOrdering() + if not ordering.total_ordering_columns: + raise ValueError("Must have total ordering defined by one or more columns") + self._ordering = ordering # Allow creating a DataFrame directly from an Ibis table expression. - if columns is None: - self._columns = tuple( - table[key] - for key in table.columns - if ordering is None or key != ordering.ordering_id - ) - else: - # TODO(swast): Validate that each column references the same table (or - # no table for literal values). - self._columns = tuple(columns) + # TODO(swast): Validate that each column references the same table (or + # no table for literal values). + self._columns = tuple(columns) # Meta columns store ordering, or other data that doesn't correspond to dataframe columns self._hidden_ordering_columns = ( @@ -120,6 +119,20 @@ def __init__( self._hidden_ordering_column_names = { column.get_name(): column for column in self._hidden_ordering_columns } + ### Validation + value_col_ids = self._column_names.keys() + hidden_col_ids = self._hidden_ordering_column_names.keys() + + all_columns = value_col_ids | hidden_col_ids + ordering_valid = all( + col.column_id in all_columns for col in ordering.all_ordering_columns + ) + if value_col_ids & hidden_col_ids: + raise ValueError( + f"Keys in both hidden and exposed list: {value_col_ids & hidden_col_ids}" + ) + if not ordering_valid: + raise ValueError(f"Illegal ordering keys: {ordering.all_ordering_columns}") @classmethod def mem_expr_from_pandas( @@ -146,7 +159,8 @@ def mem_expr_from_pandas( session, # type: ignore # Session cannot normally be none, see "caution" above keys_memtable, ordering=ExpressionOrdering( - ordering_id_column=OrderingColumnReference(ORDER_ID_COLUMN) + ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), ), hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), ) @@ -155,10 +169,6 @@ def mem_expr_from_pandas( def table(self) -> ibis_types.Table: return self._table - @property - def predicates(self) -> typing.Tuple[ibis_types.BooleanValue, ...]: - return self._predicates - @property def reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" @@ -181,18 +191,12 @@ def hidden_ordering_columns(self) -> typing.Tuple[ibis_types.Value, ...]: return self._hidden_ordering_columns @property - def ordering(self) -> Sequence[ibis_types.Value]: + def _ibis_order(self) -> Sequence[ibis_types.Value]: """Returns a sequence of ibis values which can be directly used to order a table expression. Has direction modifiers applied.""" - if not self._ordering: - return [] - else: - # TODO(swast): When we assign literals / scalars, we might not - # have a true Column. Do we need to check this before trying to - # sort by such a column? - return _convert_ordering_to_table_values( - {**self._column_names, **self._hidden_ordering_column_names}, - self._ordering.all_ordering_columns, - ) + return _convert_ordering_to_table_values( + {**self._column_names, **self._hidden_ordering_column_names}, + self._ordering.all_ordering_columns, + ) def builder(self) -> ArrayValueBuilder: """Creates a mutable builder for expressions.""" @@ -202,17 +206,12 @@ def builder(self) -> ArrayValueBuilder: return ArrayValueBuilder( self._session, self._table, - self._columns, - self._hidden_ordering_columns, + columns=self._columns, + hidden_ordering_columns=self._hidden_ordering_columns, ordering=self._ordering, predicates=self._predicates, ) - def insert_column(self, index: int, column: ibis_types.Value) -> ArrayValue: - expr = self.builder() - expr.columns.insert(index, column) - return expr.build() - def drop_columns(self, columns: Iterable[str]) -> ArrayValue: # Must generate offsets if we are dropping a column that ordering depends on expr = self @@ -229,7 +228,9 @@ def drop_columns(self, columns: Iterable[str]) -> ArrayValue: return expr_builder.build() def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: - ibis_type = typing.cast(bigframes.dtypes.IbisDtype, self.get_column(key).type()) + ibis_type = typing.cast( + bigframes.dtypes.IbisDtype, self.get_any_column(key).type() + ) return typing.cast( bigframes.dtypes.Dtype, bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), @@ -286,8 +287,7 @@ def apply_limit(self, max_results: int) -> ArrayValue: def filter(self, predicate: ibis_types.BooleanValue) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" expr = self.builder() - if expr.ordering: - expr.ordering = expr.ordering.with_is_sequential(False) + expr.ordering = expr.ordering.with_non_sequential() expr.predicates = [*self._predicates, predicate] return expr.build() @@ -303,13 +303,33 @@ def reversed(self) -> ArrayValue: expr_builder.ordering = self._ordering.with_reverse() return expr_builder.build() + def _uniform_sampling(self, fraction: float) -> ArrayValue: + table = self.to_ibis_expr( + ordering_mode="order_by", expose_hidden_cols=True, fraction=fraction + ) + columns = [table[column_name] for column_name in self._column_names] + hidden_ordering_columns = [ + table[column_name] for column_name in self._hidden_ordering_column_names + ] + return ArrayValue( + self._session, + table, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=self._ordering, + ) + @property def offsets(self): if not self._ordering.is_sequential: raise ValueError( "Expression does not have offsets. Generate them first using project_offsets." ) - return self._get_hidden_ordering_column(self._ordering.ordering_id) + if not self._ordering.total_order_col: + raise ValueError( + "Ordering is invalid. Marked as sequential but no total order columns." + ) + return self.get_any_column(self._ordering.total_order_col.column_id) def project_offsets(self) -> ArrayValue: """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" @@ -321,8 +341,9 @@ def project_offsets(self) -> ArrayValue: ) columns = [table[column_name] for column_name in self._column_names] ordering = ExpressionOrdering( - ordering_id_column=OrderingColumnReference(ORDER_ID_COLUMN), - is_sequential=True, + ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(True, is_sequential=True), ) return ArrayValue( self._session, @@ -342,13 +363,7 @@ def _hide_column(self, column_id) -> ArrayValue: *self._hidden_ordering_columns, self.get_column(column_id).name(new_name), ] - - ordering_columns = [ - col if col.column_id != column_id else col.with_name(new_name) - for col in self._ordering.ordering_value_columns - ] - - expr_builder.ordering = self._ordering.with_ordering_columns(ordering_columns) + expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) return expr_builder.build() def promote_offsets(self) -> typing.Tuple[ArrayValue, str]: @@ -358,12 +373,12 @@ def promote_offsets(self) -> typing.Tuple[ArrayValue, str]: # Special case: offsets already exist ordering = self._ordering - if (not ordering.is_sequential) or (not ordering.ordering_id): + if (not ordering.is_sequential) or (not ordering.total_order_col): return self.project_offsets().promote_offsets() col_id = bigframes.core.guid.generate_guid() expr_builder = self.builder() expr_builder.columns = [ - self._get_hidden_ordering_column(ordering.ordering_id).name(col_id), + self.get_any_column(ordering.total_order_col.column_id).name(col_id), *self.columns, ] return expr_builder.build(), col_id @@ -409,13 +424,13 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: prefix_size = math.ceil(math.log(len(other) + 1, prefix_base)) # Must normalize all ids to the same encoding size max_encoding_size = max( - self._ordering.ordering_encoding_size, - *[expression._ordering.ordering_encoding_size for expression in other], + self._ordering.string_encoding.length, + *[expression._ordering.string_encoding.length for expression in other], ) for i, expr in enumerate([self, *other]): ordering_prefix = str(i).zfill(prefix_size) table = expr.to_ibis_expr( - ordering_mode="ordered_col", order_col_name=ORDER_ID_COLUMN + ordering_mode="string_encoded", order_col_name=ORDER_ID_COLUMN ) # Rename the value columns based on horizontal offset before applying union. table = table.select( @@ -424,7 +439,9 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: if col != ORDER_ID_COLUMN else ( ordering_prefix - + stringify_order_id(table[ORDER_ID_COLUMN], max_encoding_size) + + reencode_order_string( + table[ORDER_ID_COLUMN], max_encoding_size + ) ).name(ORDER_ID_COLUMN) for i, col in enumerate(table.columns) ] @@ -432,8 +449,9 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: tables.append(table) combined_table = ibis.union(*tables) ordering = ExpressionOrdering( - ordering_id_column=OrderingColumnReference(ORDER_ID_COLUMN), - ordering_encoding_size=prefix_size + max_encoding_size, + ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + string_encoding=StringEncoding(True, prefix_size + max_encoding_size), ) return ArrayValue( self._session, @@ -498,7 +516,7 @@ def aggregate( aggregations: input_column_id, operation, output_column_id tuples dropna: whether null keys should be dropped """ - table = self.to_ibis_expr() + table = self.to_ibis_expr(ordering_mode="unordered") stats = { col_out: agg_op._as_ibis(table[col_in]) for col_in, agg_op, col_out in aggregations @@ -510,9 +528,11 @@ def aggregate( [ OrderingColumnReference(column_id=column_id) for column_id in by_column_ids - ] + ], + total_ordering_columns=frozenset(by_column_ids), ) - expr = ArrayValue(self._session, result, ordering=ordering) + columns = tuple(result[key] for key in result.columns) + expr = ArrayValue(self._session, result, columns=columns, ordering=ordering) if dropna: for column_id in by_column_ids: expr = expr.filter( @@ -525,8 +545,9 @@ def aggregate( result = table.aggregate(**aggregates) # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. ordering = ExpressionOrdering( - ordering_id_column=OrderingColumnReference(column_id=ORDER_ID_COLUMN), - is_sequential=True, + ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), ) return ArrayValue( self._session, @@ -589,10 +610,12 @@ def project_window_op( def to_ibis_expr( self, ordering_mode: Literal[ - "order_by", "ordered_col", "offset_col", "unordered" + "order_by", "string_encoded", "offset_col", "unordered" ] = "order_by", order_col_name: Optional[str] = ORDER_ID_COLUMN, expose_hidden_cols: bool = False, + fraction: Optional[float] = None, + col_id_overrides: typing.Mapping[str, str] = {}, ): """ Creates an Ibis table expression representing the DataFrame. @@ -604,8 +627,7 @@ def to_ibis_expr( column, however there will be an order_by clause applied to the ouput. * "offset_col": Zero-based offsets are generated as a column, this will not sort the rows however. - * "ordered_col": An ordered column is provided in output table, without - guarantee that the values are sequential + * "string_encoded": An ordered string column is provided in output table. * "unordered": No ordering information will be provided in output. Only value columns are projected. @@ -624,12 +646,14 @@ def to_ibis_expr( If True, include the hidden ordering columns in the results. Only compatible with `order_by` and `unordered` ``ordering_mode``. + col_id_overrides: + overrides the column ids for the result Returns: An ibis expression representing the data help by the ArrayValue object. """ assert ordering_mode in ( "order_by", - "ordered_col", + "string_encoded", "offset_col", "unordered", ) @@ -638,87 +662,107 @@ def to_ibis_expr( f"Cannot expose hidden ordering columns with ordering_mode {ordering_mode}" ) - table = self._table columns = list(self._columns) - hidden_ordering_columns = [ - col.column_id - for col in self._ordering.all_ordering_columns - if col.column_id not in self._column_names.keys() - ] + columns_to_drop: list[ + str + ] = [] # Ordering/Filtering columns that will be dropped at end if self.reduced_predicate is not None: columns.append(self.reduced_predicate) - if ordering_mode in ("offset_col", "ordered_col"): - # Generate offsets if current ordering id semantics are not sufficiently strict - if (ordering_mode == "offset_col" and not self._ordering.is_sequential) or ( - ordering_mode == "ordered_col" and not self._ordering.order_id_defined - ): - window = ibis.window(order_by=self.ordering) - if self._predicates: - window = window.group_by(self.reduced_predicate) - columns.append(ibis.row_number().name(order_col_name).over(window)) - elif self._ordering.ordering_id: - columns.append( - self._get_hidden_ordering_column(self._ordering.ordering_id).name( - order_col_name - ) - ) - else: - # Should not be possible. - raise ValueError( - "Expression does not have ordering id and none was generated." - ) - elif ordering_mode == "order_by": - columns.extend( - [ - self._get_hidden_ordering_column(name) - for name in hidden_ordering_columns - ] - ) + # Usually drop predicate as it is will be all TRUE after filtering + if not expose_hidden_cols: + columns_to_drop.append(self.reduced_predicate.get_name()) - # We already need to add the hidden ordering columns for "order_by" so - # we can order by them. - if expose_hidden_cols and ordering_mode != "order_by": - columns.extend( - [ - self._get_hidden_ordering_column(name) - for name in hidden_ordering_columns - ] - ) + order_columns = self._create_order_columns( + ordering_mode, order_col_name, expose_hidden_cols + ) + columns.extend(order_columns) + if (ordering_mode == "order_by") and not expose_hidden_cols: + columns_to_drop.extend(col.get_name() for col in order_columns) # Special case for empty tables, since we can't create an empty # projection. if not columns: return ibis.memtable([]) - table = table.select(columns) + # Make sure all dtypes are the "canonical" ones for BigFrames. This is # important for operations like UNION where the schema must match. - table = bigframes.dtypes.ibis_table_to_canonical_types(table) - + table = self._table.select( + bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns + ) + base_table = table if self.reduced_predicate is not None: - table = table.filter(table[PREDICATE_COLUMN]) - # Drop predicate as it is will be all TRUE after filtering - table = table.drop(PREDICATE_COLUMN) + table = table.filter(base_table[PREDICATE_COLUMN]) if ordering_mode == "order_by": - # Some ordering columns are value columns, while other are used purely for ordering. - # We drop the non-value columns after the ordering table = table.order_by( _convert_ordering_to_table_values( - {col: table[col] for col in table.columns}, + {col: base_table[col] for col in table.columns}, self._ordering.all_ordering_columns, ) # type: ignore ) - # TODO(swast): We should be able to avoid this subquery by ordering - # by columns that don't have to be in the SELECT clause. - if not expose_hidden_cols: - table = table.drop(*hidden_ordering_columns) - + table = table.drop(*columns_to_drop) + if col_id_overrides: + table = table.relabel(col_id_overrides) + if fraction is not None: + table = table.filter(ibis.random() < ibis.literal(fraction)) return table + def _create_order_columns( + self, + ordering_mode: str, + order_col_name: Optional[str], + expose_hidden_cols: bool, + ) -> typing.Sequence[ibis_types.Value]: + # Generate offsets if current ordering id semantics are not sufficiently strict + if ordering_mode == "offset_col": + return (self._create_offset_column().name(order_col_name),) + elif ordering_mode == "string_encoded": + return (self._create_string_ordering_column().name(order_col_name),) + elif ordering_mode == "order_by" or expose_hidden_cols: + return self.hidden_ordering_columns + return () + + def _create_offset_column(self) -> ibis_types.IntegerColumn: + if self._ordering.total_order_col and self._ordering.is_sequential: + offsets = self.get_any_column(self._ordering.total_order_col.column_id) + return typing.cast(ibis_types.IntegerColumn, offsets) + else: + window = ibis.window(order_by=self._ibis_order) + if self._predicates: + window = window.group_by(self.reduced_predicate) + offsets = ibis.row_number().over(window) + return typing.cast(ibis_types.IntegerColumn, offsets) + + def _create_string_ordering_column(self) -> ibis_types.StringColumn: + if self._ordering.total_order_col and self._ordering.is_string_encoded: + string_order_ids = self.get_any_column( + self._ordering.total_order_col.column_id + ) + return typing.cast(ibis_types.StringColumn, string_order_ids) + if ( + self._ordering.total_order_col + and self._ordering.integer_encoding.is_encoded + ): + # Special case: non-negative integer ordering id can be converted directly to string without regenerating row numbers + int_values = self.get_any_column(self._ordering.total_order_col.column_id) + return encode_order_string( + typing.cast(ibis_types.IntegerColumn, int_values), + ) + else: + # Have to build string from scratch + window = ibis.window(order_by=self._ibis_order) + if self._predicates: + window = window.group_by(self.reduced_predicate) + row_nums = typing.cast( + ibis_types.IntegerColumn, ibis.row_number().over(window) + ) + return encode_order_string(row_nums) + def start_query( self, job_config: Optional[bigquery.job.QueryJobConfig] = None, max_results: Optional[int] = None, + expose_extra_columns: bool = False, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """Execute a query and return metadata about the results.""" # TODO(swast): Cache the job ID so we can look it up again if they ask @@ -731,7 +775,7 @@ def start_query( # a LocalSession for unit testing. # TODO(swast): Add a timeout here? If the query is taking a long time, # maybe we just print the job metadata that we have so far? - table = self.to_ibis_expr() + table = self.to_ibis_expr(expose_hidden_cols=expose_extra_columns) sql = self._session.ibis_client.compile(table) # type:ignore return self._session._start_query( sql=sql, @@ -739,6 +783,9 @@ def start_query( max_results=max_results, ) + def _get_table_size(self, destination_table): + return self._session._get_table_size(destination_table) + def _reproject_to_table(self) -> ArrayValue: """ Internal operators that projects the internal representation into a @@ -749,12 +796,16 @@ def _reproject_to_table(self) -> ArrayValue: """ table = self.to_ibis_expr( ordering_mode="unordered", - order_col_name=self._ordering.ordering_id, expose_hidden_cols=True, ) columns = [table[column_name] for column_name in self._column_names] + ordering_col_ids = [ + ref.column_id for ref in self._ordering.all_ordering_columns + ] hidden_ordering_columns = [ - table[column_name] for column_name in self._hidden_ordering_column_names + table[column_name] + for column_name in self._hidden_ordering_column_names + if column_name in ordering_col_ids ] return ArrayValue( self._session, @@ -782,10 +833,10 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = Fal ) if not allow_ties: # Most operator need an unambiguous ordering, so the table's total ordering is appended - order_by = tuple([*order_by, *self.ordering]) + order_by = tuple([*order_by, *self._ibis_order]) elif (window_spec.following is not None) or (window_spec.preceding is not None): # If window spec has following or preceding bounds, we need to apply an unambiguous ordering. - order_by = tuple(self.ordering) + order_by = tuple(self._ibis_order) else: # Unbound grouping window. Suitable for aggregations but not for analytic function application. order_by = None @@ -796,47 +847,56 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = Fal group_by=group_by, ) - def transpose_single_row( + def unpivot_single_row( self, - labels, + row_labels: typing.Sequence[typing.Optional[str]], + unpivot_columns: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], *, index_col_id: str = "index", - value_col_id: str = "values", dtype=pandas.Float64Dtype(), ) -> ArrayValue: - """Pivot a single row into a 3 column expression with index, values and offsets. Only works if all values can be cast to a common type.""" + """Unpivot a single row.""" + # TODO: Generalize to multiple row input table = self.to_ibis_expr(ordering_mode="unordered") sub_expressions = [] - for i, col_id in enumerate(self._column_names.keys()): + + # TODO: validate all columns are equal length, as well as row labels + row_n = len(row_labels) + if not all( + len(source_columns) == row_n for _, source_columns in unpivot_columns + ): + raise ValueError("Columns and row labels must all be same length.") + + # Select each column + for i in range(row_n): + values = [] + for result_col, source_cols in unpivot_columns: + values.append( + ops.AsTypeOp(dtype)._as_ibis(table[source_cols[i]]).name(result_col) + ) + sub_expr = table.select( - ibis_types.literal(labels[i]).name(index_col_id), - ops.AsTypeOp(dtype)._as_ibis(table[col_id]).name(value_col_id), + ibis_types.literal(row_labels[i]).name(index_col_id), + *values, ibis_types.literal(i).name(ORDER_ID_COLUMN), ) sub_expressions.append(sub_expr) rotated_table = ibis.union(*sub_expressions) + + value_columns = [ + rotated_table[value_col_id] for value_col_id, _ in unpivot_columns + ] return ArrayValue( session=self._session, table=rotated_table, - columns=[rotated_table[index_col_id], rotated_table[value_col_id]], + columns=[rotated_table[index_col_id], *value_columns], hidden_ordering_columns=[rotated_table[ORDER_ID_COLUMN]], ordering=ExpressionOrdering( - ordering_id_column=OrderingColumnReference(column_id=ORDER_ID_COLUMN), + ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), ), ) - # TODO(b/282041134) Remove deprecate_rename_column once label/id separation in dataframe - def deprecated_rename_column(self, old_id, new_id) -> ArrayValue: - """ - Don't use this, temporary measure until dataframe supports sqlid!=dataframe col id. - In future, caller shouldn't need to control internal column id strings. - """ - if new_id == old_id: - return self - return self._set_or_replace_by_id(new_id, self.get_column(old_id)).drop_columns( - [old_id] - ) - def assign(self, source_id: str, destination_id: str) -> ArrayValue: return self._set_or_replace_by_id(destination_id, self.get_column(source_id)) @@ -850,12 +910,20 @@ def assign_constant( ibis_value = bigframes.dtypes.literal_to_ibis_scalar(value, dtype) if ibis_value is None: raise NotImplementedError( - f"Type not supported as scalar value {type(value)}" + f"Type not supported as scalar value {type(value)}. {constants.FEEDBACK_LINK}" ) expr = self._set_or_replace_by_id(destination_id, ibis_value) return expr._reproject_to_table() - def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value): + def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> ArrayValue: + """Safely assign by id while maintaining ordering integrity.""" + # TODO: Split into explicit set and replace methods + ordering_col_ids = [ + col_ref.column_id for col_ref in self._ordering.ordering_value_columns + ] + if id in ordering_col_ids: + return self._hide_column(id)._set_or_replace_by_id(id, new_value) + builder = self.builder() if id in self.column_names: builder.columns = [ @@ -937,9 +1005,9 @@ def __init__( self, session: Session, table: ibis_types.Table, + ordering: ExpressionOrdering, columns: Collection[ibis_types.Value] = (), hidden_ordering_columns: Collection[ibis_types.Value] = (), - ordering: Optional[ExpressionOrdering] = None, predicates: Optional[Collection[ibis_types.BooleanValue]] = None, ): self.session = session diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index fe7261522db..5ef92630727 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -23,8 +23,10 @@ import functools import itertools +import random import typing from typing import Iterable, List, Optional, Sequence, Tuple +import warnings import geopandas as gpd # type: ignore import google.cloud.bigquery as bigquery @@ -34,10 +36,12 @@ import pandas as pd import pyarrow as pa # type: ignore +import bigframes.constants as constants import bigframes.core as core import bigframes.core.guid as guid import bigframes.core.indexes as indexes import bigframes.core.ordering as ordering +import bigframes.core.utils import bigframes.dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -45,6 +49,15 @@ # Type constraint for wherever column labels are used Label = typing.Optional[str] +# Bytes to Megabyte Conversion +_BYTES_TO_KILOBYTES = 1024 +_BYTES_TO_MEGABYTES = _BYTES_TO_KILOBYTES * 1024 + +# All sampling method +_HEAD = "head" +_UNIFORM = "uniform" +_SAMPLING_METHODS = (_HEAD, _UNIFORM) + class BlockHolder(typing.Protocol): """Interface for mutable objects with state represented by a block value object.""" @@ -89,6 +102,11 @@ def __init__( raise ValueError( f"'value_columns' (size {len(self.value_columns)}) and 'column_labels' (size {len(self._column_labels)}) must have equal length" ) + # col_id -> [stat_name -> scalar] + # TODO: Preserve cache under safe transforms (eg. drop column, reorder) + self._stats_cache: dict[str, dict[str, typing.Any]] = { + col_id: {} for col_id in self.value_columns + } @property def index(self) -> indexes.IndexValue: @@ -241,7 +259,11 @@ def reset_index(self, drop: bool = True) -> Block: return block def set_index( - self, col_ids: typing.Sequence[str], drop: bool = True, append: bool = False + self, + col_ids: typing.Sequence[str], + drop: bool = True, + append: bool = False, + index_labels: typing.Sequence[Label] = (), ) -> Block: """Set the index of the block to @@ -249,6 +271,7 @@ def set_index( ids: columns to be converted to index columns drop: whether to drop the new index columns as value columns append: whether to discard the existing index or add on to it + index_labels: new index labels Returns: Block with new index @@ -269,6 +292,9 @@ def set_index( else: expr = expr.drop_columns(self.index_columns) + if index_labels: + new_index_labels = list(index_labels) + block = Block( expr, index_columns=new_index_columns, @@ -325,42 +351,222 @@ def _to_dataframe(self, result, schema: ibis_schema.Schema) -> pd.DataFrame: ) return df - def compute( - self, value_keys: Optional[Iterable[str]] = None, max_results=None + def to_pandas( + self, + value_keys: Optional[Iterable[str]] = None, + max_results: Optional[int] = None, + max_download_size: Optional[int] = None, + sampling_method: Optional[str] = None, + random_state: Optional[int] = None, ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame.""" + if max_download_size is None: + max_download_size = bigframes.options.sampling.max_download_size + if sampling_method is None: + sampling_method = ( + bigframes.options.sampling.sampling_method + if bigframes.options.sampling.sampling_method is not None + else _UNIFORM + ) + if random_state is None: + random_state = bigframes.options.sampling.random_state + + sampling_method = sampling_method.lower() + if sampling_method not in _SAMPLING_METHODS: + raise NotImplementedError( + f"The downsampling method {sampling_method} is not implemented, " + f"please choose from {','.join(_SAMPLING_METHODS)}." + ) + df, _, query_job = self._compute_and_count( - value_keys=value_keys, max_results=max_results + value_keys=value_keys, + max_results=max_results, + max_download_size=max_download_size, + sampling_method=sampling_method, + random_state=random_state, ) return df, query_job def _compute_and_count( - self, value_keys: Optional[Iterable[str]] = None, max_results=None + self, + value_keys: Optional[Iterable[str]] = None, + max_results: Optional[int] = None, + max_download_size: Optional[int] = None, + sampling_method: Optional[str] = None, + random_state: Optional[int] = None, ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. - expr = self._expr + expr = self._apply_value_keys_to_expr(value_keys=value_keys) - value_column_names = value_keys or self.value_columns - if value_keys is not None: - index_columns = ( - expr.get_column(column_name) for column_name in self._index_columns + results_iterator, query_job = expr.start_query( + max_results=max_results, expose_extra_columns=True + ) + + table_size = expr._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES + fraction = ( + max_download_size / table_size + if (max_download_size is not None) and (table_size != 0) + else 2 + ) + + if fraction < 1: + if not bigframes.options.sampling.enable_downsampling: + raise RuntimeError( + f"The data size ({table_size:.2f} MB) exceeds the maximum download limit of " + f"{max_download_size} MB. You can:\n\t* Enable downsampling in global options:\n" + "\t\t`bigframes.options.sampling.enable_downsampling = True`\n" + "\t* Update the global `max_download_size` option. Please make sure " + "there is enough memory available:\n" + "\t\t`bigframes.options.sampling.max_download_size = desired_size`" + " # Setting it to None will download all the data\n" + f"{constants.FEEDBACK_LINK}" + ) + + warnings.warn( + f"The data size ({table_size:.2f} MB) exceeds the maximum download limit of" + f"({max_download_size} MB). It will be downsampled to {max_download_size} MB for download." + "\nPlease refer to the documentation for configuring the downloading limit.", + UserWarning, ) - value_columns = (expr.get_column(column_name) for column_name in value_keys) - expr = expr.projection(itertools.chain(index_columns, value_columns)) + if sampling_method == _HEAD: + total_rows = int(results_iterator.total_rows * fraction) + results_iterator.max_results = total_rows + df = self._to_dataframe(results_iterator, expr.to_ibis_expr().schema()) + + if self.index_columns: + df.set_index(list(self.index_columns), inplace=True) + df.index.names = self.index.names # type: ignore + + df.drop( + [col for col in df.columns if col not in self.value_columns], + axis=1, + inplace=True, + ) + elif (sampling_method == _UNIFORM) and (random_state is None): + filtered_expr = self.expr._uniform_sampling(fraction) + block = Block( + filtered_expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + df, total_rows, _ = block._compute_and_count(max_download_size=None) + elif sampling_method == _UNIFORM: + block = self._split( + fracs=(max_download_size / table_size,), + random_state=random_state, + preserve_order=True, + )[0] + df, total_rows, _ = block._compute_and_count(max_download_size=None) + else: + # This part should never be called, just in case. + raise NotImplementedError( + f"The downsampling method {sampling_method} is not implemented, " + f"please choose from {','.join(_SAMPLING_METHODS)}." + ) + else: + total_rows = results_iterator.total_rows + df = self._to_dataframe(results_iterator, expr.to_ibis_expr().schema()) + + if self.index_columns: + df.set_index(list(self.index_columns), inplace=True) + df.index.names = self.index.names # type: ignore + + df.drop( + [col for col in df.columns if col not in self.value_columns], + axis=1, + inplace=True, + ) + + return df, total_rows, query_job - results_iterator, query_job = expr.start_query(max_results=max_results) - df = self._to_dataframe( - results_iterator, - expr.to_ibis_expr().schema(), + def _split( + self, + ns: Iterable[int] = (), + fracs: Iterable[float] = (), + *, + random_state: Optional[int] = None, + preserve_order: Optional[bool] = False, + ) -> List[Block]: + """Internal function to support splitting Block to multiple parts along index axis. + + At most one of ns and fracs can be passed in. If neither, default to ns = (1,). + Return a list of sampled Blocks. + """ + block = self + if ns and fracs: + raise ValueError("Only one of 'ns' or 'fracs' parameter must be specified.") + + if not ns and not fracs: + ns = (1,) + + if ns: + sample_sizes = ns + else: + total_rows = block.shape[0] + # Round to nearest integer. "round half to even" rule applies. + # At least to be 1. + sample_sizes = [round(frac * total_rows) or 1 for frac in fracs] + + if random_state is None: + random_state = random.randint(-(2**63), 2**63 - 1) + + # Create a new column with random_state value. + block, random_state_col = block.create_constant(str(random_state)) + + # Create an ordering col and convert to string + block, ordering_col = block.promote_offsets() + block, string_ordering_col = block.apply_unary_op( + ordering_col, ops.AsTypeOp("string[pyarrow]") + ) + + # Apply hash method to sum col and order by it. + block, string_sum_col = block.apply_binary_op( + string_ordering_col, random_state_col, ops.concat_op ) + block, hash_string_sum_col = block.apply_unary_op(string_sum_col, ops.hash_op) + block = block.order_by([ordering.OrderingColumnReference(hash_string_sum_col)]) - df = df.loc[:, [*self.index_columns, *value_column_names]] - if self.index_columns: - df = df.set_index(list(self.index_columns)) - df.index.names = self.index.names # type: ignore + intervals = [] + cur = 0 - return df, results_iterator.total_rows, query_job + for sample_size in sample_sizes: + intervals.append((cur, cur + sample_size)) + cur += sample_size + + sliced_blocks = [ + typing.cast(Block, block.slice(start=lower, stop=upper)) + for lower, upper in intervals + ] + if preserve_order: + sliced_blocks = [ + sliced_block.order_by([ordering.OrderingColumnReference(ordering_col)]) + for sliced_block in sliced_blocks + ] + + drop_cols = [ + random_state_col, + ordering_col, + string_ordering_col, + string_sum_col, + hash_string_sum_col, + ] + 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: + expr = self._apply_value_keys_to_expr(value_keys=value_keys) + job_config = bigquery.QueryJobConfig(dry_run=True) + _, query_job = expr.start_query(job_config=job_config) + return query_job + + def _apply_value_keys_to_expr(self, value_keys: Optional[Iterable[str]] = None): + expr = self._expr + if value_keys is not None: + expr = expr.select_columns(itertools.chain(self._index_columns, value_keys)) + return expr def with_column_labels(self, value: typing.Iterable[Label]) -> Block: label_list = tuple(value) @@ -553,10 +759,17 @@ def assign_label(self, column_id: str, new_label: Label) -> Block: new_labels[col_index] = new_label return self.with_column_labels(new_labels) - def filter(self, column_name: str): + def filter(self, column_name: str, keep_null: bool = False): condition = typing.cast( ibis_types.BooleanValue, self._expr.get_column(column_name) ) + if keep_null: + condition = typing.cast( + ibis_types.BooleanValue, + condition.fillna( + typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) + ), + ) filtered_expr = self.expr.filter(condition) return Block( filtered_expr, @@ -576,10 +789,10 @@ def aggregate_all_and_pivot( aggregations = [(col_id, operation, col_id) for col_id in self.value_columns] result_expr = self.expr.aggregate( aggregations, dropna=dropna - ).transpose_single_row( - labels=self.column_labels, + ).unpivot_single_row( + row_labels=self.column_labels, index_col_id="index", - value_col_id=value_col_id, + unpivot_columns=[(value_col_id, self.value_columns)], dtype=dtype, ) return Block(result_expr, index_columns=["index"], column_labels=[None]) @@ -614,8 +827,8 @@ def rename(self, *, columns: typing.Mapping[Label, Label]): def aggregate( self, - by_column_ids: typing.Sequence[str], - aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp]], + by_column_ids: typing.Sequence[str] = (), + aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp]] = (), *, as_index: bool = True, dropna: bool = True, @@ -623,7 +836,7 @@ def aggregate( """ Apply aggregations to the block. Callers responsible for setting index column(s) after. Arguments: - by_column_id: column id of the aggregation key, this is preserved through the transform and used as index + by_column_id: column id of the aggregation key, this is preserved through the transform and used as index. aggregations: input_column_id, operation tuples as_index: if True, grouping keys will be index columns in result, otherwise they will be non-index columns. dropna: whether null keys should be dropped @@ -639,15 +852,12 @@ def aggregate( [agg[0] for agg in aggregations] ) if as_index: - # TODO: Generalize to multi-index names: typing.List[Label] = [] for by_col_id in by_column_ids: - if by_col_id in self.index_columns: - # Groupby level 0 case, keep index name - index_name = self.col_id_to_index_name[by_col_id] + if by_col_id in self.value_columns: + names.append(self.col_id_to_label[by_col_id]) else: - index_name = self.col_id_to_label[by_col_id] - names.append(index_name) + names.append(self.col_id_to_index_name[by_col_id]) return ( Block( result_expr, @@ -657,10 +867,89 @@ def aggregate( ), output_col_ids, ) - else: - by_column_labels = self._get_labels_for_columns(by_column_ids) + else: # as_index = False + # If as_index=False, drop grouping levels, but keep grouping value columns + by_value_columns = [ + col for col in by_column_ids if col in self.value_columns + ] + by_column_labels = self._get_labels_for_columns(by_value_columns) labels = (*by_column_labels, *aggregate_labels) - return Block(result_expr, column_labels=labels), output_col_ids + result_expr_pruned = result_expr.select_columns( + [*by_value_columns, *output_col_ids] + ) + return Block(result_expr_pruned, column_labels=labels), output_col_ids + + def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): + """Gets aggregates immediately, and caches it""" + if stat.name in self._stats_cache[column_id]: + return self._stats_cache[column_id][stat.name] + + # TODO: Convert nonstandard stats into standard stats where possible (popvar, etc.) + # if getting a standard stat, just go get the rest of them + standard_stats = self._standard_stats(column_id) + stats_to_fetch = standard_stats if stat in standard_stats else [stat] + + aggregations = [(column_id, stat, stat.name) for stat in stats_to_fetch] + expr = self.expr.aggregate(aggregations) + block = Block(expr, column_labels=[s.name for s in stats_to_fetch]) + df, _ = block.to_pandas() + + # Carefully extract stats such that they aren't coerced to a common type + stats_map = {stat_name: df.loc[0, stat_name] for stat_name in df.columns} + self._stats_cache[column_id].update(stats_map) + return stats_map[stat.name] + + def summarize( + self, + column_ids: typing.Sequence[str], + stats: typing.Sequence[agg_ops.AggregateOp], + ): + """Get a list of stats as a deferred block object.""" + label_col_id = guid.generate_guid() + labels = [stat.name for stat in stats] + aggregations = [ + (col_id, stat, f"{col_id}-{stat.name}") + for stat in stats + for col_id in column_ids + ] + columns = [ + (col_id, [f"{col_id}-{stat.name}" for stat in stats]) + for col_id in column_ids + ] + expr = self.expr.aggregate(aggregations).unpivot_single_row( + labels, + unpivot_columns=columns, + index_col_id=label_col_id, + ) + labels = self._get_labels_for_columns(column_ids) + return Block(expr, column_labels=labels, index_columns=[label_col_id]) + + def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.AggregateOp]: + """ + Gets a standard set of stats to preemptively fetch for a column if + any other stat is fetched. + Helps prevent repeat scanning of the same column to fetch statistics. + Standard stats should be: + - commonly used + - efficiently computable. + """ + # TODO: annotate aggregations themself with this information + dtype = self.expr.get_column_type(column_id) + stats: list[agg_ops.AggregateOp] = [agg_ops.count_op] + if dtype not in bigframes.dtypes.UNORDERED_DTYPES: + stats += [agg_ops.min_op, agg_ops.max_op] + if dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES: + # Notable exclusions: + # prod op tends to cause overflows + # Also, var_op is redundant as can be derived from std + stats += [ + agg_ops.std_op, + agg_ops.mean_op, + agg_ops.var_op, + agg_ops.sum_op, + ] + + return stats def _get_labels_for_columns(self, column_ids: typing.Sequence[str]): """Get column label for value columns, or index name for index columns""" @@ -699,6 +988,29 @@ def slice( ) return block + def retrieve_repr_request_results( + self, max_results: int + ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: + """ + Retrieves a pandas dataframe containing only max_results many rows for use + with printing methods. + + Returns a tuple of the dataframe and the overall number of rows of the query. + """ + # TODO(swast): Select a subset of columns if max_columns is less than the + # number of columns in the schema. + count = self.shape[0] + if count > max_results: + head_block = self.slice(0, max_results) + computed_df, query_job = head_block.to_pandas(max_results=max_results) + else: + head_block = self + computed_df, query_job = head_block.to_pandas() + formatted_df = computed_df.set_axis(self.column_labels, axis=1) + # we reset the axis and substitute the bf index name for the default + formatted_df.index.name = self.index.name + return formatted_df, count, query_job + def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: expr, result_id = self._expr.promote_offsets() return ( @@ -712,7 +1024,7 @@ def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: ) def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: - axis_number = _get_axis_number(axis) + axis_number = bigframes.core.utils.get_axis_number(axis) if axis_number == 0: expr = self._expr for index_col in self._index_columns: @@ -735,7 +1047,7 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: ) def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: - axis_number = _get_axis_number(axis) + axis_number = bigframes.core.utils.get_axis_number(axis) if axis_number == 0: expr = self._expr for index_col in self._index_columns: @@ -788,6 +1100,15 @@ def concat( result_block = result_block.reset_index() return result_block + def _force_reproject(self) -> Block: + """Forces a reprojection of the underlying tables expression. Used to force predicate/order application before subsequent operations.""" + return Block( + self._expr._reproject_to_table(), + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + def block_from_local(data, session=None, use_index=True) -> Block: # TODO(tbergeron): Handle duplicate column labels @@ -795,14 +1116,20 @@ def block_from_local(data, session=None, use_index=True) -> Block: column_labels = list(pd_data.columns) if not all((label is None) or isinstance(label, str) for label in column_labels): - raise NotImplementedError("Only string column labels supported") + raise NotImplementedError( + f"Only string column labels supported. {constants.FEEDBACK_LINK}" + ) if use_index: if pd_data.index.nlevels > 1: - raise NotImplementedError("multi-indices not supported.") + raise NotImplementedError( + f"multi-indices not supported. {constants.FEEDBACK_LINK}" + ) index_label = pd_data.index.name if (index_label is not None) and (not isinstance(index_label, str)): - raise NotImplementedError("Only string index names supported") + raise NotImplementedError( + f"Only string index names supported. {constants.FEEDBACK_LINK}" + ) index_id = guid.generate_guid() pd_data = pd_data.reset_index(names=index_id) @@ -853,11 +1180,11 @@ def _align_indices(blocks: typing.Sequence[Block]) -> typing.Sequence[Label]: for block in blocks[1:]: if len(names) != block.index.nlevels: raise NotImplementedError( - "Cannot combine indices with different number of levels. Use 'ignore_index'=True." + f"Cannot combine indices with different number of levels. Use 'ignore_index'=True. {constants.FEEDBACK_LINK}" ) if block.index.dtypes != types: raise NotImplementedError( - "Cannot combine different index dtypes. Use 'ignore_index'=True." + f"Cannot combine different index dtypes. Use 'ignore_index'=True. {constants.FEEDBACK_LINK}" ) names = [ lname if lname == rname else None @@ -875,7 +1202,7 @@ def _combine_schema_inner( if label in right: if type != right[label]: raise ValueError( - f"Cannot concat rows with label {label} due to mismatched types" + f"Cannot concat rows with label {label} due to mismatched types. {constants.FEEDBACK_LINK}" ) result[label] = type return result @@ -889,7 +1216,7 @@ def _combine_schema_outer( for label, type in left.items(): if (label in right) and (type != right[label]): raise ValueError( - f"Cannot concat rows with label {label} due to mismatched types" + f"Cannot concat rows with label {label} due to mismatched types. {constants.FEEDBACK_LINK}" ) result[label] = type for label, type in right.items(): @@ -906,12 +1233,3 @@ def _get_block_schema( for label, dtype in zip(block.column_labels, block.dtypes): result[label] = typing.cast(bigframes.dtypes.Dtype, dtype) return result - - -def _get_axis_number(axis: str | int | None) -> typing.Literal[0, 1]: - if axis in {0, "index", "rows", None}: - return 0 - elif axis in {1, "columns"}: - return 1 - else: - raise ValueError(f"Not a valid axis: {axis}") diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index a8655116e93..288dcdd5b0b 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -16,12 +16,16 @@ import typing +import pandas as pd +import typing_extensions + +import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.ordering as order import bigframes.core.window as windows import bigframes.dataframe as df -import bigframes.dtypes +import bigframes.dtypes as dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.series as series @@ -36,6 +40,7 @@ def __init__( block: blocks.Block, by_col_ids: typing.Sequence[str], *, + selected_cols: typing.Optional[typing.Sequence[str]] = None, dropna: bool = True, as_index: bool = True, ): @@ -48,8 +53,52 @@ def __init__( ) } self._by_col_ids = by_col_ids - self._dropna = dropna # Applies to aggregations but not windowing + + self._dropna = dropna self._as_index = as_index + if selected_cols: + for col in selected_cols: + if col not in self._block.value_columns: + raise ValueError(f"Invalid column selection: {col}") + self._selected_cols = selected_cols + else: + self._selected_cols = [ + col_id + for col_id in self._block.value_columns + if col_id not in self._by_col_ids + ] + + def __getitem__( + self, + key: typing.Union[ + blocks.Label, + typing.Sequence[blocks.Label], + ], + ): + if _is_list_like(key): + keys = list(key) + else: + keys = [key] + columns = [ + col_id for col_id, label in self._col_id_labels.items() if label in keys + ] + + if len(columns) > 1 or (not self._as_index): + return DataFrameGroupBy( + self._block, + self._by_col_ids, + selected_cols=columns, + dropna=self._dropna, + as_index=self._as_index, + ) + else: + return SeriesGroupBy( + self._block, + columns[0], + self._by_col_ids, + value_name=self._col_id_labels[columns[0]], + dropna=self._dropna, + ) def sum(self, numeric_only: bool = False, *args) -> df.DataFrame: if not numeric_only: @@ -61,15 +110,22 @@ def mean(self, numeric_only: bool = False, *args) -> df.DataFrame: self._raise_on_non_numeric("mean") return self._aggregate(agg_ops.mean_op, numeric_only=True) - def min(self, numeric_only: bool = False, *args) -> df.DataFrame: + def median( + self, numeric_only: bool = False, *, exact: bool = False + ) -> df.DataFrame: + if exact: + raise NotImplementedError( + f"Only approximate median is supported. {constants.FEEDBACK_LINK}" + ) if not numeric_only: - self._raise_on_non_numeric("min") - return self._aggregate(agg_ops.min_op, numeric_only=True) + self._raise_on_non_numeric("median") + return self._aggregate(agg_ops.median_op, numeric_only=True) + + def min(self, numeric_only: bool = False, *args) -> df.DataFrame: + return self._aggregate(agg_ops.min_op, numeric_only=numeric_only) def max(self, numeric_only: bool = False, *args) -> df.DataFrame: - if not numeric_only: - self._raise_on_non_numeric("max") - return self._aggregate(agg_ops.max_op, numeric_only=True) + return self._aggregate(agg_ops.max_op, numeric_only=numeric_only) def std( self, @@ -101,45 +157,40 @@ def count(self) -> df.DataFrame: def cumsum(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("cumsum") - window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) - return self._apply_window_op(agg_ops.sum_op, window, numeric_only=True) + return self._apply_window_op(agg_ops.sum_op, numeric_only=True) def cummin(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: - if not numeric_only: - self._raise_on_non_numeric("cummin") - window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) - return self._apply_window_op(agg_ops.min_op, window, numeric_only=True) + return self._apply_window_op(agg_ops.min_op, numeric_only=numeric_only) def cummax(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: - if not numeric_only: - self._raise_on_non_numeric("cummax") - window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) - return self._apply_window_op(agg_ops.max_op, window, numeric_only=True) + return self._apply_window_op(agg_ops.max_op, numeric_only=numeric_only) def cumprod(self, *args, **kwargs) -> df.DataFrame: - window = bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0) - return self._apply_window_op(agg_ops.product_op, window, numeric_only=True) + return self._apply_window_op(agg_ops.product_op, numeric_only=True) def _raise_on_non_numeric(self, op: str): if not all( - dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES - for dtype in self._block.dtypes + dtype in dtypes.NUMERIC_BIGFRAMES_TYPES for dtype in self._block.dtypes ): raise NotImplementedError( - f"'{op}' does not support non-numeric columns. Set 'numeric_only'=True to ignore non-numeric columns" + f"'{op}' does not support non-numeric columns. " + "Set 'numeric_only'=True to ignore non-numeric columns. " + f"{constants.FEEDBACK_LINK}" ) return self - def _aggregated_columns(self, numeric_only: bool = False): - return [ - col_id - for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) - if col_id not in self._by_col_ids - and ( - (not numeric_only) - or (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) - ) - ] + def _aggregated_columns(self, numeric_only: bool = False) -> typing.Sequence[str]: + valid_agg_cols: list[str] = [] + for col_id in self._selected_cols: + is_numeric = self._column_type(col_id) in dtypes.NUMERIC_BIGFRAMES_TYPES + if is_numeric or not numeric_only: + valid_agg_cols.append(col_id) + return valid_agg_cols + + def _column_type(self, col_id: str) -> dtypes.Dtype: + col_offset = self._block.value_columns.index(col_id) + dtype = self._block.dtypes[col_offset] + return dtype def _aggregate( self, aggregate_op: agg_ops.AggregateOp, numeric_only: bool = False @@ -147,8 +198,8 @@ def _aggregate( aggregated_col_ids = self._aggregated_columns(numeric_only=numeric_only) aggregations = [(col_id, aggregate_op) for col_id in aggregated_col_ids] result_block, _ = self._block.aggregate( - self._by_col_ids, - aggregations, + by_column_ids=self._by_col_ids, + aggregations=aggregations, as_index=self._as_index, dropna=self._dropna, ) @@ -157,15 +208,16 @@ def _aggregate( def _apply_window_op( self, op: agg_ops.WindowOp, - window_spec: bigframes.core.WindowSpec, + window: typing.Optional[core.WindowSpec] = None, numeric_only: bool = False, ): + """Apply window op to groupby. Defaults to grouped cumulative window.""" + window_spec = window or core.WindowSpec( + grouping_keys=self._by_col_ids, following=0 + ) columns = self._aggregated_columns(numeric_only=numeric_only) - block = self._block.select_columns([*columns, *window_spec.grouping_keys]) block = self._block.multi_apply_window_op( - columns, - op, - window_spec=window_spec, + columns, op, window_spec=window_spec, skip_null_groups=self._dropna ) block = block.select_columns(columns) return df.DataFrame(block) @@ -190,7 +242,7 @@ def __init__( self._dropna = dropna # Applies to aggregations but not windowing @property - def value(self): + def _value(self): return self._block.expr.get_column(self._value_column) def all(self) -> series.Series: @@ -199,16 +251,24 @@ def all(self) -> series.Series: def any(self) -> series.Series: return self._aggregate(agg_ops.any_op) + def min(self, *args) -> series.Series: + return self._aggregate(agg_ops.min_op) + + def max(self, *args) -> series.Series: + return self._aggregate(agg_ops.max_op) + def count(self) -> series.Series: return self._aggregate(agg_ops.count_op) def sum(self, *args) -> series.Series: - """Sums the numeric values for each group in the series. Ignores null/nan.""" return self._aggregate(agg_ops.sum_op) def mean(self, *args) -> series.Series: return self._aggregate(agg_ops.mean_op) + def median(self, *args, **kwargs) -> series.Series: + return self._aggregate(agg_ops.mean_op) + def std(self, *args, **kwargs) -> series.Series: return self._aggregate(agg_ops.std_op) @@ -221,45 +281,39 @@ def prod(self, *args) -> series.Series: def cumsum(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.sum_op, - bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), ) def cumprod(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.product_op, - bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), ) def cummax(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.max_op, - bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), ) def cummin(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.min_op, - bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), ) def cumcount(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.rank_op, - bigframes.core.WindowSpec(grouping_keys=self._by_col_ids, following=0), discard_name=True, )._apply_unary_op(ops.partial_right(ops.sub_op, 1)) def shift(self, periods=1) -> series.Series: """Shift index by desired number of periods.""" - window = bigframes.core.WindowSpec( + window = core.WindowSpec( grouping_keys=self._by_col_ids, preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) - return self._apply_window_op(agg_ops.ShiftOp(periods), window) + return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) def diff(self) -> series.Series: - """Difference between each element and previous element.""" return self._ungroup() - self.shift(1) def rolling(self, window: int, min_periods=None) -> windows.Window: @@ -278,7 +332,9 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = core.WindowSpec( - grouping_keys=self._by_col_ids, following=0, min_periods=min_periods + grouping_keys=self._by_col_ids, + following=0, + min_periods=min_periods, ) block = self._block.order_by( [order.OrderingColumnReference(col) for col in self._by_col_ids], @@ -301,9 +357,14 @@ def _aggregate(self, aggregate_op: agg_ops.AggregateOp) -> series.Series: def _apply_window_op( self, op: agg_ops.WindowOp, - window_spec: bigframes.core.WindowSpec, discard_name=False, + window: typing.Optional[core.WindowSpec] = None, ): + """Apply window op to groupby. Defaults to grouped cumulative window.""" + window_spec = window or core.WindowSpec( + grouping_keys=self._by_col_ids, following=0 + ) + label = self._value_name if not discard_name else None block, result_id = self._block.apply_window_op( self._value_column, @@ -313,3 +374,7 @@ def _apply_window_op( skip_null_groups=self._dropna, ) return series.Series(block.select_column(result_id)) + + +def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: + return pd.api.types.is_list_like(obj) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index f65cb02941e..0aaf169bea4 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -20,6 +20,7 @@ import ibis import pandas as pd +import bigframes.constants as constants import bigframes.core as core import bigframes.core.guid as guid import bigframes.core.indexes as indexes @@ -47,10 +48,14 @@ def __setitem__(self, key, value) -> None: # TODO(swast): support MultiIndex if isinstance(key, slice): # TODO(swast): Implement loc with slices. - raise NotImplementedError("loc does not yet support slices") + raise NotImplementedError( + f"loc does not yet support slices. {constants.FEEDBACK_LINK}" + ) elif isinstance(key, list): # TODO(tbergeron): Implement loc for index label list. - raise NotImplementedError("loc does not yet support index label lists") + raise NotImplementedError( + f"loc does not yet support index label lists. {constants.FEEDBACK_LINK}" + ) # Assume the key is for the index label. block = self._series._block @@ -149,6 +154,7 @@ def __setitem__( ): raise NotImplementedError( "Only setting a column by DataFrame.loc[:, 'column'] is supported." + f"{constants.FEEDBACK_LINK}" ) # TODO(swast): Support setting multiple columns with key[1] as a list @@ -213,19 +219,45 @@ def _loc_getitem_series_or_dataframe( typing.Union[bigframes.dataframe.DataFrame, bigframes.series.Series], series_or_dataframe.iloc[0:0], ) + + # We can't upload a DataFrame with None as the column name, so set it + # an arbitrary string. index_name = series_or_dataframe.index.name + index_name_is_none = index_name is None + if index_name_is_none: + index_name = "unnamed_col" + keys_df = bigframes.dataframe.DataFrame( {index_name: key}, session=series_or_dataframe._get_block().expr._session ) keys_df = keys_df.set_index(index_name, drop=True) + + if index_name_is_none: + keys_df.index.name = None return _perform_loc_list_join(series_or_dataframe, keys_df) elif isinstance(key, slice): - return series_or_dataframe._slice(key.start, key.stop, key.step) + if (key.start is None) and (key.stop is None) and (key.step is None): + return series_or_dataframe.copy() + raise NotImplementedError( + f"loc does not yet support indexing with a slice. {constants.FEEDBACK_LINK}" + ) elif callable(key): - raise NotImplementedError("loc does not yet support indexing with a callable") + raise NotImplementedError( + f"loc does not yet support indexing with a callable. {constants.FEEDBACK_LINK}" + ) + elif pd.api.types.is_scalar(key): + index_name = "unnamed_col" + keys_df = bigframes.dataframe.DataFrame( + {index_name: [key]}, session=series_or_dataframe._get_block().expr._session + ) + keys_df = keys_df.set_index(index_name, drop=True) + keys_df.index.name = None + return _perform_loc_list_join(series_or_dataframe, keys_df) else: raise TypeError( - "Invalid argument type. loc currently only supports indexing with a boolean bigframes Series or a list of index entries." + "Invalid argument type. loc currently only supports indexing with a " + "boolean bigframes Series, a list of index entries or a single index entry. " + f"{constants.FEEDBACK_LINK}" ) @@ -257,11 +289,13 @@ def _perform_loc_list_join( name = series_or_dataframe.name if series_or_dataframe.name is not None else "0" result = typing.cast( bigframes.series.Series, - series_or_dataframe.to_frame().join(keys_df, how="right")[name], + series_or_dataframe.to_frame()._perform_join_by_index(keys_df, how="right")[ + name + ], ) result = result.rename(original_name) else: - result = series_or_dataframe.join(keys_df, how="right") # type: ignore + result = series_or_dataframe._perform_join_by_index(keys_df, how="right") # type: ignore result = result.rename_axis(original_index_names) return result @@ -284,12 +318,8 @@ def _iloc_getitem_series_or_dataframe( series_or_dataframe: bigframes.dataframe.DataFrame | bigframes.series.Series, key ) -> bigframes.dataframe.DataFrame | bigframes.series.Series | bigframes.core.scalar.Scalar | pd.Series: if isinstance(key, int): - if key < 0: - raise NotImplementedError( - "iloc does not yet support negative single positional index" - ) internal_slice_result = series_or_dataframe._slice(key, key + 1, 1) - result_pd_df = internal_slice_result.compute() + result_pd_df = internal_slice_result.to_pandas() if result_pd_df.empty: raise IndexError("single positional indexer is out-of-bounds") return result_pd_df.iloc[0] @@ -329,9 +359,11 @@ def _iloc_getitem_series_or_dataframe( elif isinstance(key, tuple): raise NotImplementedError( - "iloc does not yet support indexing with a (row, column) tuple" + f"iloc does not yet support indexing with a (row, column) tuple. {constants.FEEDBACK_LINK}" ) elif callable(key): - raise NotImplementedError("iloc does not yet support indexing with a callable") + raise NotImplementedError( + f"iloc does not yet support indexing with a callable. {constants.FEEDBACK_LINK}" + ) else: - raise TypeError("Invalid argument type.") + raise TypeError(f"Invalid argument type. {constants.FEEDBACK_LINK}") diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index f7fa7ac1195..bb1fcc4b175 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -20,8 +20,9 @@ from typing import Callable, Tuple import numpy as np -import pandas as pd +import pandas +import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.joins as joins @@ -45,6 +46,7 @@ def name(self, value: blocks.Label): @property def names(self) -> typing.Sequence[blocks.Label]: + """Returns the names of the Index.""" return self._data._get_block()._index_labels @names.setter @@ -57,21 +59,40 @@ def shape(self) -> typing.Tuple[int]: @property def size(self) -> int: + """Returns the size of the Index.""" return self.shape[0] @property def empty(self) -> bool: + """Returns True if the Index is empty, otherwise returns False.""" return self.shape[0] == 0 - def to_pandas(self) -> pd.Index: - """Get the Index as a pandas Index.""" - return IndexValue(self._data._get_block()).compute() + def __getitem__(self, key: int) -> typing.Any: + if isinstance(key, int): + result_pd_df, _ = self._data._get_block().slice(key, key + 1, 1).to_pandas() + if result_pd_df.empty: + raise IndexError("single positional indexer is out-of-bounds") + return result_pd_df.index[0] + else: + raise NotImplementedError(f"Index key not supported {key}") + + def to_pandas(self) -> pandas.Index: + """Gets the Index as a pandas Index. + + Returns: + pandas.Index: + A pandas Index with all of the labels from this Index. + """ + return IndexValue(self._data._get_block()).to_pandas() + + def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: + return self.to_pandas().to_numpy(dtype, **kwargs) + + __array__ = to_numpy def __len__(self): return self.shape[0] - compute = to_pandas - class IndexValue: """An immutable index.""" @@ -107,10 +128,10 @@ def __repr__(self) -> str: # maybe we just print the job metadata that we have so far? # TODO(swast): Avoid downloading the whole index by using job # metadata, like we do with DataFrame. - preview = self.compute() + preview = self.to_pandas() return repr(preview) - def compute(self) -> pd.Index: + def to_pandas(self) -> pandas.Index: """Executes deferred operations and downloads the results.""" # Project down to only the index column. So the query can be cached to visualize other data. index_column = self._block.index_columns[0] @@ -123,26 +144,35 @@ def compute(self) -> pd.Index: return index def join( - self, other: IndexValue, *, how="left", sort=False + self, + other: IndexValue, + *, + how="left", + sort=False, + block_identity_join: bool = False, ) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: if not isinstance(other, IndexValue): # TODO(swast): We need to improve this error message to be more # actionable for the user. For example, it's possible they # could call set_index and try again to resolve this error. raise ValueError( - "Can't mixed objects with explicit Index and ImpliedJoiner" + f"Tried to join with an unexpected type: {type(other)}. {constants.FEEDBACK_LINK}" ) # TODO(swast): Support cross-joins (requires reindexing). if how not in {"outer", "left", "right", "inner"}: raise NotImplementedError( - "Only how='outer','left','right','inner' currently supported" + f"Only how='outer','left','right','inner' currently supported. {constants.FEEDBACK_LINK}" ) if self.nlevels == other.nlevels == 1: - return join_mono_indexed(self, other, how=how, sort=sort) + return join_mono_indexed( + self, other, how=how, sort=sort, block_identity_join=block_identity_join + ) else: # Always sort mult-index join - return join_multi_indexed(self, other, how=how, sort=sort) + return join_multi_indexed( + self, other, how=how, sort=sort, block_identity_join=block_identity_join + ) def resolve_level_name(self: IndexValue, label: blocks.Label) -> str: matches = self._block.index_name_to_col_id.get(label, []) @@ -157,7 +187,12 @@ def is_uniquely_named(self: IndexValue): def join_mono_indexed( - left: IndexValue, right: IndexValue, *, how="left", sort=False + left: IndexValue, + right: IndexValue, + *, + how="left", + sort=False, + block_identity_join: bool = False, ) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: ( combined_expr, @@ -170,6 +205,7 @@ def join_mono_indexed( right._block.index_columns, how=how, sort=sort, + allow_row_identity_join=(not block_identity_join), ) # Drop original indices from each side. and used the coalesced combination generated by the join. left_indices = [get_column_left(col_id) for col_id in left._block.index_columns] @@ -188,7 +224,12 @@ def join_mono_indexed( def join_multi_indexed( - left: IndexValue, right: IndexValue, *, how="left", sort=False + left: IndexValue, + right: IndexValue, + *, + how="left", + sort=False, + block_identity_join: bool = False, ) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: if not (left.is_uniquely_named() and right.is_uniquely_named()): raise ValueError("Joins not supported on indices with non-unique level names") @@ -203,6 +244,7 @@ def join_multi_indexed( left_join_ids = [left.resolve_level_name(name) for name in common_names] right_join_ids = [right.resolve_level_name(name) for name in common_names] + names_fully_match = len(left_only_names) == 0 and len(right_only_names) == 0 ( combined_expr, joined_index_col_names, @@ -214,6 +256,9 @@ def join_multi_indexed( right_join_ids, how=how, sort=sort, + # If we're only joining on a subset of the index columns, we need to + # perform a true join. + allow_row_identity_join=names_fully_match and not block_identity_join, ) # Drop original indices from each side. and used the coalesced combination generated by the join. combined_expr = combined_expr.drop_columns( diff --git a/bigframes/core/io.py b/bigframes/core/io.py new file mode 100644 index 00000000000..6b7ed52488d --- /dev/null +++ b/bigframes/core/io.py @@ -0,0 +1,90 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Private module: Helpers for I/O operations.""" + +import datetime +import textwrap +from typing import Dict, Union + +import google.cloud.bigquery as bigquery + +IO_ORDERING_ID = "bqdf_row_nums" + + +def create_export_csv_statement( + table_id: str, uri: str, field_delimiter: str, header: bool +) -> str: + return create_export_data_statement( + table_id, + uri, + "CSV", + { + "field_delimiter": field_delimiter, + "header": header, + }, + ) + + +def create_export_data_statement( + table_id: str, uri: str, format: str, export_options: Dict[str, Union[bool, str]] +) -> str: + all_options: Dict[str, Union[bool, str]] = { + "uri": uri, + "format": format, + # TODO(swast): Does pandas have an option not to overwrite files? + "overwrite": True, + } + all_options.update(export_options) + export_options_str = ", ".join( + format_option(key, value) for key, value in all_options.items() + ) + # Manually generate ORDER BY statement since ibis will not always generate + # it in the top level statement. This causes BigQuery to then run + # non-distributed sort and run out of memory. + return textwrap.dedent( + f""" + EXPORT DATA + OPTIONS ( + {export_options_str} + ) AS + SELECT * EXCEPT ({IO_ORDERING_ID}) + FROM `{table_id}` + ORDER BY {IO_ORDERING_ID} + """ + ) + + +def create_snapshot_sql( + table_ref: bigquery.TableReference, current_timestamp: datetime.datetime +) -> str: + """Query a table via 'time travel' for consistent reads.""" + + # If we have a _SESSION table, assume that it's already a copy. Nothing to do here. + if table_ref.dataset_id.upper() == "_SESSION": + return f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" + + return textwrap.dedent( + f""" + SELECT * + FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}` + FOR SYSTEM_TIME AS OF TIMESTAMP({repr(current_timestamp.isoformat())}) + """ + ) + + +def format_option(key: str, value: Union[bool, str]) -> str: + if isinstance(value, bool): + return f"{key}=true" if value else f"{key}=false" + return f"{key}={repr(value)}" diff --git a/bigframes/core/joins/row_identity.py b/bigframes/core/joins/row_identity.py index b77b5d80adc..66eb2239904 100644 --- a/bigframes/core/joins/row_identity.py +++ b/bigframes/core/joins/row_identity.py @@ -23,6 +23,7 @@ import ibis import ibis.expr.types as ibis_types +import bigframes.constants as constants import bigframes.core as core SUPPORTED_ROW_IDENTITY_HOW = {"outer", "left", "inner"} @@ -33,7 +34,9 @@ def join_by_row_identity( ) -> Tuple[core.ArrayValue, Tuple[Callable[[str], str], Callable[[str], str]],]: """Compute join when we are joining by row identity not a specific column.""" if how not in SUPPORTED_ROW_IDENTITY_HOW: - raise NotImplementedError("Only how='outer','left','inner' currently supported") + raise NotImplementedError( + f"Only how='outer','left','inner' currently supported. {constants.FEEDBACK_LINK}" + ) if not left.table.equals(right.table): raise ValueError( @@ -67,47 +70,41 @@ def join_by_row_identity( for key in right.column_names.keys() ] - hidden_ordering_columns = [] - new_ordering = core.ExpressionOrdering() - if left._ordering and right._ordering: - # These ordering columns will be present in the ArrayValue, as we - # haven't hidden any value / index column(s). Code that is aware of - # which columns are index columns / value columns columns will need to - # add the previous columns to hidden columns. - new_ordering = left._ordering.with_ordering_columns( - [ - col_ref.with_name(map_left_id(col_ref.column_id)) - for col_ref in left._ordering.ordering_value_columns - ] - + [ - col_ref.with_name(map_right_id(col_ref.column_id)) - for col_ref in right._ordering.ordering_value_columns - ] - ) - - hidden_ordering_columns = [ - left._get_hidden_ordering_column(key.column_id).name( - map_left_id(key.column_id) - ) - for key in left._ordering.ordering_value_columns - if key.column_id in left._hidden_ordering_column_names.keys() + # If left isn't being masked, can just use left ordering + if not left_mask: + col_mapping = { + order_ref.column_id: map_left_id(order_ref.column_id) + for order_ref in left._ordering.ordering_value_columns + } + new_ordering = left._ordering.with_column_remap(col_mapping) + else: + ordering_columns = [ + col_ref.with_name(map_left_id(col_ref.column_id)) + for col_ref in left._ordering.ordering_value_columns ] + [ - right._get_hidden_ordering_column(key.column_id).name( - map_right_id(key.column_id) - ) - for key in right._ordering.ordering_value_columns - if key.column_id in right._hidden_ordering_column_names.keys() + col_ref.with_name(map_right_id(col_ref.column_id)) + for col_ref in right._ordering.ordering_value_columns ] + left_total_order_cols = frozenset( + map_left_id(col) for col in left._ordering.total_ordering_columns + ) + # Assume that left ordering is sufficient since 1:1 join over same base table + join_total_order_cols = left_total_order_cols + new_ordering = core.ExpressionOrdering( + ordering_columns, total_ordering_columns=join_total_order_cols + ) - left_ordering_id = left._ordering.ordering_id - if left_ordering_id: - new_ordering = new_ordering.with_ordering_id(map_left_id(left_ordering_id)) - if left_ordering_id in left._hidden_ordering_column_names.keys(): - hidden_ordering_columns.append( - left._get_hidden_ordering_column(left_ordering_id).name( - map_left_id(left_ordering_id) - ) - ) + hidden_ordering_columns = [ + left._get_hidden_ordering_column(key.column_id).name(map_left_id(key.column_id)) + for key in left._ordering.ordering_value_columns + if key.column_id in left._hidden_ordering_column_names.keys() + ] + [ + right._get_hidden_ordering_column(key.column_id).name( + map_right_id(key.column_id) + ) + for key in right._ordering.ordering_value_columns + if key.column_id in right._hidden_ordering_column_names.keys() + ] joined_expr = core.ArrayValue( left._session, @@ -169,7 +166,9 @@ def _join_predicates( ) return (*left_predicates, *right_relative_predicates) else: - raise ValueError("Unsupported join_type: " + join_type) + raise ValueError( + f"Unsupported join_type: {join_type}. {constants.FEEDBACK_LINK}" + ) def _get_relative_predicates( diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py index 34873e6f7ee..4c865fffdf7 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/joins/single_column.py @@ -23,6 +23,7 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types +import bigframes.constants as constants import bigframes.core as core import bigframes.core.guid import bigframes.core.joins.row_identity @@ -42,7 +43,8 @@ def join_by_column( "right", ], sort: bool = False, - get_both_join_key_cols: bool = False, + coalesce_join_keys: bool = True, + allow_row_identity_join: bool = True, ) -> Tuple[ core.ArrayValue, typing.Sequence[str], @@ -56,8 +58,11 @@ def join_by_column( right: Expression for right table to join. right_column_ids: Column IDs (not label) to join by. how: The type of join to perform. - get_both_join_key_cols: if set to True, returned column ids will contain + coalesce_join_keys: if set to False, returned column ids will contain both left and right join key columns. + allow_row_identity_join (bool): + If True, allow matching by row identity. Set to False to always + perform a true JOIN in generated SQL. Returns: The joined expression and the objects needed to interpret it. @@ -66,19 +71,22 @@ def join_by_column( * Sequence[str]: Column IDs of the coalesced join columns. Sometimes either the left/right table will have missing rows. This column pulls the non-NULL value from either left/right. - If get_both_join_key_cols is True, will return uncombined left and + If coalesce_join_keys is False, will return uncombined left and right key columns. * Tuple[Callable, Callable]: For a given column ID from left or right, respectively, return the new column id from the combined expression. """ - if ( - how in bigframes.core.joins.row_identity.SUPPORTED_ROW_IDENTITY_HOW + allow_row_identity_join + and how in bigframes.core.joins.row_identity.SUPPORTED_ROW_IDENTITY_HOW and left.table.equals(right.table) - # Compare ibis expressions for left/right columns because its possible that - # they both have the same names but were modified in different ways. + # Make sure we're joining on exactly the same column(s), at least with + # regards to value its possible that they both have the same names but + # were modified in different ways. Ignore differences in the names. and all( - left.get_any_column(lcol).equals(right.get_any_column(rcol)) + left.get_any_column(lcol) + .name("index") + .equals(right.get_any_column(rcol).name("index")) for lcol, rcol in zip(left_column_ids, right_column_ids) ) ): @@ -86,15 +94,42 @@ def join_by_column( get_column_left, get_column_right, ) = bigframes.core.joins.row_identity.join_by_row_identity(left, right, how=how) - original_ordering = combined_expr._ordering + left_join_keys = [ + combined_expr.get_column(get_column_left(col)) for col in left_column_ids + ] + right_join_keys = [ + combined_expr.get_column(get_column_right(col)) for col in right_column_ids + ] + join_key_cols = get_join_cols( + left_join_keys, right_join_keys, how, coalesce_join_keys + ) + join_key_ids = [col.get_name() for col in join_key_cols] + combined_expr = combined_expr.projection( + [*join_key_cols, *combined_expr.columns] + ) + if sort: + combined_expr = combined_expr.order_by( + [ + core.OrderingColumnReference(join_col_id) + for join_col_id in join_key_ids + ] + ) + return ( + combined_expr, + join_key_ids, + ( + get_column_left, + get_column_right, + ), + ) else: # Generate offsets if non-default ordering is applied # Assumption, both sides are totally ordered, otherwise offsets will be nondeterministic left_table = left.to_ibis_expr( - ordering_mode="ordered_col", order_col_name=core.ORDER_ID_COLUMN + ordering_mode="string_encoded", order_col_name=core.ORDER_ID_COLUMN ) right_table = right.to_ibis_expr( - ordering_mode="ordered_col", order_col_name=core.ORDER_ID_COLUMN + ordering_mode="string_encoded", order_col_name=core.ORDER_ID_COLUMN ) join_conditions = [ value_to_join_key(left_table[left_index]) @@ -144,66 +179,105 @@ def get_column_right(key: str) -> str: return key left_ordering_encoding_size = ( - left._ordering.ordering_encoding_size - or bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH + left._ordering.string_encoding.length + if left._ordering.is_string_encoded + else bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH ) right_ordering_encoding_size = ( - right._ordering.ordering_encoding_size - or bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH + right._ordering.string_encoding.length + if right._ordering.is_string_encoded + else bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH ) # Preserve original ordering accross joins. left_order_id = get_column_left(core.ORDER_ID_COLUMN) right_order_id = get_column_right(core.ORDER_ID_COLUMN) new_order_id_col = _merge_order_ids( - combined_table[left_order_id], + typing.cast(ibis_types.StringColumn, combined_table[left_order_id]), left_ordering_encoding_size, - combined_table[right_order_id], + typing.cast(ibis_types.StringColumn, combined_table[right_order_id]), right_ordering_encoding_size, how, ) new_order_id = new_order_id_col.get_name() if new_order_id is None: raise ValueError("new_order_id unexpectedly has no name") + hidden_columns = (new_order_id_col,) - original_ordering = core.ExpressionOrdering( - ordering_id_column=core.OrderingColumnReference(new_order_id) - if (new_order_id_col is not None) - else None, - ordering_encoding_size=left_ordering_encoding_size - + right_ordering_encoding_size, + ordering = core.ExpressionOrdering( + # Order id is non-nullable but na_last=False generates simpler sql with current impl + ordering_value_columns=[ + core.OrderingColumnReference(new_order_id, na_last=False) + ], + total_ordering_columns=frozenset([new_order_id]), + string_encoding=core.StringEncoding( + True, left_ordering_encoding_size + right_ordering_encoding_size + ), + ) + + left_join_keys = [ + combined_table[get_column_left(col)] for col in left_column_ids + ] + right_join_keys = [ + combined_table[get_column_right(col)] for col in right_column_ids + ] + join_key_cols = get_join_cols( + left_join_keys, right_join_keys, how, coalesce_join_keys + ) + # We could filter out the original join columns, but predicates/ordering + # might still reference them in implicit joins. + columns = ( + join_key_cols + + [combined_table[get_column_left(col.get_name())] for col in left.columns] + + [ + combined_table[get_column_right(col.get_name())] + for col in right.columns + ] ) combined_expr = core.ArrayValue( left._session, combined_table, + columns=columns, hidden_ordering_columns=hidden_columns, + ordering=ordering, + ) + if sort: + combined_expr = combined_expr.order_by( + [ + core.OrderingColumnReference(join_key_col.get_name()) + for join_key_col in join_key_cols + ] + ) + return ( + combined_expr, + [key.get_name() for key in join_key_cols], + (get_column_left, get_column_right), ) + +def get_join_cols( + left_join_cols: typing.Iterable[ibis_types.Value], + right_join_cols: typing.Iterable[ibis_types.Value], + how: str, + coalesce_join_keys: bool = True, +) -> typing.List[ibis_types.Value]: join_key_cols: list[ibis_types.Value] = [] - for lcol, rcol in zip(left_column_ids, right_column_ids): - if get_both_join_key_cols: + for left_col, right_col in zip(left_join_cols, right_join_cols): + if not coalesce_join_keys: join_key_cols.append( - combined_expr.get_column(get_column_left(lcol)).name( - bigframes.core.guid.generate_guid(prefix="index_") - ) + left_col.name(bigframes.core.guid.generate_guid(prefix="index_")) ) join_key_cols.append( - combined_expr.get_column(get_column_right(rcol)).name( - bigframes.core.guid.generate_guid(prefix="index_") - ) + right_col.name(bigframes.core.guid.generate_guid(prefix="index_")) ) else: if how == "left" or how == "inner": join_key_cols.append( - combined_expr.get_column(get_column_left(lcol)).name( - bigframes.core.guid.generate_guid(prefix="index_") - ) + left_col.name(bigframes.core.guid.generate_guid(prefix="index_")) ) elif how == "right": join_key_cols.append( - combined_expr.get_column(get_column_right(rcol)).name( - bigframes.core.guid.generate_guid(prefix="index_") - ) + right_col.name(bigframes.core.guid.generate_guid(prefix="index_")) ) elif how == "outer": # The left index and the right index might contain null values, for @@ -211,48 +285,25 @@ def get_column_right(key: str) -> str: # these to take the index value from either column. # Use a random name in case the left index and the right index have the # same name. In such a case, _x and _y suffixes will already be used. - join_key_cols.append( - ibis.coalesce( - combined_expr.get_column(get_column_left(lcol)), - combined_expr.get_column(get_column_right(rcol)), - ).name(bigframes.core.guid.generate_guid(prefix="index_")) - ) + # Don't need to coalesce if they are exactly the same column. + if left_col.name("index").equals(right_col.name("index")): + join_key_cols.append( + left_col.name( + bigframes.core.guid.generate_guid(prefix="index_") + ) + ) + else: + join_key_cols.append( + ibis.coalesce( + left_col, + right_col, + ).name(bigframes.core.guid.generate_guid(prefix="index_")) + ) else: - raise ValueError(f"Unexpected join type: {how}") - - # We could filter out the original join columns, but predicates/ordering - # might still reference them in implicit joins. - columns = ( - join_key_cols - + [ - combined_expr.get_column(get_column_left(key)) - for key in left.column_names.keys() - ] - + [ - combined_expr.get_column(get_column_right(key)) - for key in right.column_names.keys() - ] - ) - - if sort: - ordering = original_ordering.with_ordering_columns( - [ - core.OrderingColumnReference(join_key_col.get_name()) - for join_key_col in join_key_cols - ] - ) - else: - ordering = original_ordering - - combined_expr_builder = combined_expr.builder() - combined_expr_builder.columns = columns - combined_expr_builder.ordering = ordering - combined_expr = combined_expr_builder.build() - return ( - combined_expr, - [key.get_name() for key in join_key_cols], - (get_column_left, get_column_right), - ) + raise ValueError( + f"Unexpected join type: {how}. {constants.FEEDBACK_LINK}" + ) + return join_key_cols def value_to_join_key(value: ibis_types.Value): @@ -263,19 +314,31 @@ def value_to_join_key(value: ibis_types.Value): def _merge_order_ids( - left_id: ibis_types.Value, + left_id: ibis_types.StringColumn, left_encoding_size: int, - right_id: ibis_types.Value, + right_id: ibis_types.StringColumn, right_encoding_size: int, how: str, -) -> ibis_types.StringValue: +) -> ibis_types.StringColumn: if how == "right": return _merge_order_ids( right_id, right_encoding_size, left_id, left_encoding_size, "left" ) - return ( - ( - bigframes.core.ordering.stringify_order_id(left_id, left_encoding_size) - + bigframes.core.ordering.stringify_order_id(right_id, right_encoding_size) + + if how == "left": + right_id = typing.cast( + ibis_types.StringColumn, + right_id.fillna(ibis_types.literal(":" * right_encoding_size)), + ) + elif how != "inner": # outer join + left_id = typing.cast( + ibis_types.StringColumn, + left_id.fillna(ibis_types.literal(":" * left_encoding_size)), ) - ).name(bigframes.core.guid.generate_guid(prefix="bigframes_ordering_id_")) + right_id = typing.cast( + ibis_types.StringColumn, + right_id.fillna(ibis_types.literal(":" * right_encoding_size)), + ) + return (left_id + right_id).name( + bigframes.core.guid.generate_guid(prefix="bigframes_ordering_id_") + ) diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 6f8d35e52ae..d5f07ecf91a 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -14,7 +14,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum import math import typing @@ -28,6 +28,8 @@ # Sufficient to store any value up to 2^63 DEFAULT_ORDERING_ID_LENGTH: int = math.ceil(63 * math.log(2, ORDERING_ID_STRING_BASE)) +STABLE_SORTS = ["mergesort", "stable"] + class OrderingDirection(Enum): ASC = 1 @@ -61,121 +63,162 @@ def with_reverse(self): ) +# Encoding classes specify additional properties for some ordering representations @dataclass(frozen=True) -class ExpressionOrdering: - """Immutable object that holds information about the ordering of rows in a ArrayValue object.""" +class StringEncoding: + """String encoded order ids are fixed length and can be concat together in joins.""" - ordering_value_columns: Sequence[OrderingColumnReference] = () - ordering_id_column: Optional[OrderingColumnReference] = None - is_sequential: bool = False + is_encoded: bool = False # Encoding size must be tracked in order to know what how to combine ordering ids across tables (eg how much to pad when combining different length). # Also will be needed to determine when length is too large and need to compact ordering id with a ROW_NUMBER operation. - ordering_encoding_size: int = DEFAULT_ORDERING_ID_LENGTH + length: int = DEFAULT_ORDERING_ID_LENGTH + + +@dataclass(frozen=True) +class IntegerEncoding: + """Integer encoded order ids are guaranteed non-negative.""" + + is_encoded: bool = False + is_sequential: bool = False + - def with_is_sequential(self, is_sequential: bool): +@dataclass(frozen=True) +class ExpressionOrdering: + """Immutable object that holds information about the ordering of rows in a ArrayValue object.""" + + ordering_value_columns: Sequence[OrderingColumnReference] = () + integer_encoding: IntegerEncoding = IntegerEncoding(False) + string_encoding: StringEncoding = StringEncoding(False) + # A table has a total ordering defined by the identities of a set of 1 or more columns. + # These columns must always be part of the ordering, in order to guarantee that the ordering is total. + # Therefore, any modifications(or drops) done to these columns must result in hidden copies being made. + total_ordering_columns: frozenset[str] = field(default_factory=frozenset) + + def with_non_sequential(self): """Create a copy that is marked as non-sequential. This is useful when filtering, but not sorting, an expression. """ - return ExpressionOrdering( - self.ordering_value_columns, - self.ordering_id_column, - is_sequential, - ordering_encoding_size=self.ordering_encoding_size, - ) + if self.integer_encoding.is_sequential: + return ExpressionOrdering( + self.ordering_value_columns, + integer_encoding=IntegerEncoding( + self.integer_encoding.is_encoded, is_sequential=False + ), + total_ordering_columns=self.total_ordering_columns, + ) + + return self def with_ordering_columns( self, ordering_value_columns: Sequence[OrderingColumnReference] = (), stable: bool = False, - ): - """Creates a new ordering that preserves ordering id, but replaces ordering value column list.""" + ) -> ExpressionOrdering: + """Creates a new ordering that reorders by the given columns. + + Args: + ordering_value_columns: + In decreasing precedence order, the values used to sort the ordering + stable: + If True, will use apply a stable sorting, using the old ordering where + the new ordering produces ties. Otherwise, ties will be resolved in + a performance maximizing way, + + Returns: + Modified ExpressionOrdering + """ + col_ids_new = [ + ordering_ref.column_id for ordering_ref in ordering_value_columns + ] if stable: - col_ids_new = [ - ordering_ref.column_id for ordering_ref in ordering_value_columns - ] # Only reference each column once, so discard old referenc if there is a new reference old_ordering_keep = [ ordering_ref for ordering_ref in self.ordering_value_columns if ordering_ref.column_id not in col_ids_new ] - new_ordering = (*ordering_value_columns, *old_ordering_keep) - else: # Not stable, so discard old ordering completely - new_ordering = tuple(ordering_value_columns) + else: + # New ordering needs to keep all total ordering columns no matter what. + # All other old ordering references can be discarded as does not need + # to be a stable sort. + old_ordering_keep = [ + ordering_ref + for ordering_ref in self.ordering_value_columns + if (ordering_ref.column_id not in col_ids_new) + and (ordering_ref.column_id in self.total_ordering_columns) + ] + new_ordering = (*ordering_value_columns, *old_ordering_keep) return ExpressionOrdering( new_ordering, - self.ordering_id_column, - is_sequential=False, - ordering_encoding_size=self.ordering_encoding_size, - ) - - def with_ordering_id(self, ordering_id: str): - """Creates a new ordering that preserves other properties, but with a different ordering id. - - Useful when reprojecting ordering for implicit joins. - """ - return ExpressionOrdering( - self.ordering_value_columns, - OrderingColumnReference(ordering_id), - is_sequential=self.is_sequential, - ordering_encoding_size=self.ordering_encoding_size, + total_ordering_columns=self.total_ordering_columns, ) def with_reverse(self): """Reverses the ordering.""" return ExpressionOrdering( tuple([col.with_reverse() for col in self.ordering_value_columns]), - self.ordering_id_column.with_reverse() - if self.ordering_id_column is not None - else None, - is_sequential=False, - ordering_encoding_size=self.ordering_encoding_size, + total_ordering_columns=self.total_ordering_columns, + ) + + def with_column_remap(self, mapping: typing.Mapping[str, str]): + new_value_columns = [ + col.with_name(mapping.get(col.column_id, col.column_id)) + for col in self.ordering_value_columns + ] + new_total_order = frozenset( + mapping.get(col_id, col_id) for col_id in self.total_ordering_columns + ) + return ExpressionOrdering( + new_value_columns, + integer_encoding=self.integer_encoding, + string_encoding=self.string_encoding, + total_ordering_columns=new_total_order, ) @property - def ordering_id(self) -> Optional[str]: - return self.ordering_id_column.column_id if self.ordering_id_column else None + def total_order_col(self) -> Optional[OrderingColumnReference]: + """Returns column id of columns that defines total ordering, if such as column exists""" + if len(self.ordering_value_columns) != 1: + return None + order_ref = self.ordering_value_columns[0] + if order_ref.direction != OrderingDirection.ASC: + return None + return order_ref @property - def order_id_defined(self) -> bool: - """True if ordering is fully defined in ascending order by its ordering id.""" - return bool( - self.ordering_id_column - and (not self.ordering_value_columns) - and self.ordering_id_column.direction == OrderingDirection.ASC - ) + def is_string_encoded(self) -> bool: + """True if ordering is fully defined by a fixed length string column.""" + return self.string_encoding.is_encoded + + @property + def is_sequential(self) -> bool: + return self.integer_encoding.is_encoded and self.integer_encoding.is_sequential @property def all_ordering_columns(self) -> Sequence[OrderingColumnReference]: - return ( - list(self.ordering_value_columns) - if self.ordering_id_column is None - else [*self.ordering_value_columns, self.ordering_id_column] - ) + return list(self.ordering_value_columns) -def stringify_order_id( - order_id: ibis_types.Value, length: int = DEFAULT_ORDERING_ID_LENGTH -) -> ibis_types.StringValue: +def encode_order_string( + order_id: ibis_types.IntegerColumn, length: int = DEFAULT_ORDERING_ID_LENGTH +) -> ibis_types.StringColumn: """Converts an order id value to string if it is not already a string. MUST produced fixed-length strings.""" - if order_id.type().is_int64(): - # This is very inefficient encoding base-10 string uses only 10 characters per byte(out of 256 bit combinations) - # Furthermore, if know tighter bounds on order id are known, can produce smaller strings. - # 19 characters chosen as it can represent any positive Int64 in base-10 - # For missing values, ":" * 19 is used as it is larger than any other value this function produces, so null values will be last. - string_order_id = ( - typing.cast( - ibis_types.StringValue, - typing.cast(ibis_types.IntegerValue, order_id).cast(ibis_dtypes.string), - ) - .lpad(length, "0") - .fillna(ibis_types.literal(":" * length)) - ) - else: - string_order_id = ( - typing.cast(ibis_types.StringValue, order_id) - .lpad(length, "0") - .fillna(ibis_types.literal(":" * length)) - ) - return typing.cast(ibis_types.StringValue, string_order_id) + # This is very inefficient encoding base-10 string uses only 10 characters per byte(out of 256 bit combinations) + # Furthermore, if know tighter bounds on order id are known, can produce smaller strings. + # 19 characters chosen as it can represent any positive Int64 in base-10 + # For missing values, ":" * 19 is used as it is larger than any other value this function produces, so null values will be last. + string_order_id = typing.cast( + ibis_types.StringValue, + order_id.cast(ibis_dtypes.string), + ).lpad(length, "0") + return typing.cast(ibis_types.StringColumn, string_order_id) + + +def reencode_order_string( + order_id: ibis_types.StringColumn, length: int +) -> ibis_types.StringColumn: + return typing.cast( + ibis_types.StringColumn, + (typing.cast(ibis_types.StringValue, order_id).lpad(length, "0")), + ) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index 1443a8e08fc..339ce7466a1 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -14,48 +14,107 @@ from __future__ import annotations import typing -from typing import Iterable, Literal, Union +from typing import Iterable, Literal, Optional, Union +import bigframes.constants as constants +import bigframes.core as core +import bigframes.core.utils as utils import bigframes.dataframe +import bigframes.operations.aggregations as agg_ops import bigframes.series @typing.overload def concat( - objs: Iterable[bigframes.dataframe.DataFrame], *, join, ignore_index + objs: Iterable[bigframes.series.Series], + *, + axis: typing.Literal["index", 0] = ..., + join=..., + ignore_index=..., +) -> bigframes.series.Series: + ... + + +@typing.overload +def concat( + objs: Iterable[bigframes.dataframe.DataFrame], + *, + axis: typing.Literal["index", 0] = ..., + join=..., + ignore_index=..., ) -> bigframes.dataframe.DataFrame: ... @typing.overload def concat( - objs: Iterable[bigframes.series.Series], *, join, ignore_index -) -> bigframes.series.Series: + objs: Iterable[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]], + *, + axis: typing.Literal["columns", 1], + join=..., + ignore_index=..., +) -> bigframes.dataframe.DataFrame: ... +@typing.overload def concat( - objs: Union[ - Iterable[bigframes.dataframe.DataFrame], Iterable[bigframes.series.Series] - ], + objs: Iterable[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]], *, + axis=..., + join=..., + ignore_index=..., +) -> Union[bigframes.dataframe.DataFrame, bigframes.series.Series]: + ... + + +def concat( + objs: Iterable[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]], + *, + axis: typing.Union[str, int] = 0, join: Literal["inner", "outer"] = "outer", ignore_index: bool = False, ) -> Union[bigframes.dataframe.DataFrame, bigframes.series.Series]: - contains_dataframes = any( - isinstance(x, bigframes.dataframe.DataFrame) for x in objs - ) - if not contains_dataframes: - # Special case, all series, so align everything into single column even if labels don't match - series = typing.cast(typing.Iterable[bigframes.series.Series], objs) - names = {s.name for s in series} - # For series case, labels are stripped if they don't all match - if len(names) > 1: - blocks = [s._block.with_column_labels([None]) for s in series] - else: - blocks = [s._block for s in series] + axis_n = utils.get_axis_number(axis) + if axis_n == 0: + contains_dataframes = any( + isinstance(x, bigframes.dataframe.DataFrame) for x in objs + ) + if not contains_dataframes: + # Special case, all series, so align everything into single column even if labels don't match + series = typing.cast(typing.Iterable[bigframes.series.Series], objs) + names = {s.name for s in series} + # For series case, labels are stripped if they don't all match + if len(names) > 1: + blocks = [s._block.with_column_labels([None]) for s in series] + else: + blocks = [s._block for s in series] + block = blocks[0].concat(blocks[1:], how=join, ignore_index=ignore_index) + return bigframes.series.Series(block) + blocks = [obj._block for obj in objs] block = blocks[0].concat(blocks[1:], how=join, ignore_index=ignore_index) - return bigframes.series.Series(block) - blocks = [obj._block for obj in objs] - block = blocks[0].concat(blocks[1:], how=join, ignore_index=ignore_index) - return bigframes.dataframe.DataFrame(block) + return bigframes.dataframe.DataFrame(block) + else: + # Note: does not validate inputs + block_list = [obj._block for obj in objs] + block = block_list[0] + for rblock in block_list[1:]: + combined_index, _ = block.index.join(rblock.index, how=join) + block = combined_index._block + return bigframes.dataframe.DataFrame(block) + + +def cut( + x: bigframes.series.Series, + bins: int, + *, + labels: Optional[bool] = None, +) -> bigframes.series.Series: + if bins <= 0: + raise ValueError("`bins` should be a positive integer.") + + if labels is not False: + raise NotImplementedError( + f"Only labels=False is supported in BigQuery DataFrames so far. {constants.FEEDBACK_LINK}" + ) + return x._apply_window_op(agg_ops.CutOp(bins), window_spec=core.WindowSpec()) diff --git a/bigframes/core/scalar.py b/bigframes/core/scalar.py index 67ced525b99..6dfbd31b77b 100644 --- a/bigframes/core/scalar.py +++ b/bigframes/core/scalar.py @@ -15,10 +15,14 @@ from __future__ import annotations import typing -from typing import Any +from typing import Any, Optional +import google.cloud.bigquery as bigquery import ibis.expr.types as ibis_types +import bigframes +import bigframes.formatting_helpers as formatter + if typing.TYPE_CHECKING: import bigframes.session @@ -29,19 +33,36 @@ class DeferredScalar: def __init__(self, value: ibis_types.Scalar, session: bigframes.session.Session): self._value = value self._session = session + self._query_job: Optional[bigquery.QueryJob] = None + + @property + def query_job(self) -> Optional[bigquery.QueryJob]: + """BigQuery job metadata for the most recent query.""" + if self._query_job is None: + self._query_job = self._compute_dry_run() + return self._query_job def __repr__(self) -> str: """Converts a Series to a string.""" # TODO(swast): Add a timeout here? If the query is taking a long time, # maybe we just print the job metadata that we have so far? - return repr(self.compute()) + opts = bigframes.options.display + if opts.repr_mode == "deferred": + return formatter.repr_query_job(self.query_job) + else: + return repr(self.to_pandas()) - def compute(self) -> Any: + def to_pandas(self) -> Any: """Executes deferred operations and downloads the resulting scalar.""" - result, _ = self._session._start_query(self._value.compile()) + result, query_job = self._session._start_query(self._value.compile()) + self._query_job = query_job df = self._session._rows_to_dataframe(result) return df.iloc[0, 0] + def _compute_dry_run(self): + job_config = bigquery.QueryJobConfig(dry_run=True) + return self._session._start_query(self._value.compile(), job_config=job_config) + # All public APIs return Any at present # Later implementation may sometimes return a lazy scalar diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py new file mode 100644 index 00000000000..8ad58fb166e --- /dev/null +++ b/bigframes/core/utils.py @@ -0,0 +1,22 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import typing + + +def get_axis_number(axis: typing.Union[str, int, None]) -> typing.Literal[0, 1]: + if axis in {0, "index", "rows", None}: + return 0 + elif axis in {1, "columns"}: + return 1 + raise ValueError(f"Not a valid axis: {axis}") diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0155aafa0b0..cb062f09249 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -16,7 +16,6 @@ from __future__ import annotations -import random import re import textwrap import typing @@ -33,13 +32,13 @@ ) import google.cloud.bigquery as bigquery -import ibis.expr.datatypes as ibis_dtypes import numpy -import pandas as pd +import pandas import typing_extensions import bigframes import bigframes._config.display_options as display_options +import bigframes.constants as constants import bigframes.core import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks @@ -47,9 +46,12 @@ import bigframes.core.guid import bigframes.core.indexers as indexers import bigframes.core.indexes as indexes +import bigframes.core.io import bigframes.core.joins as joins import bigframes.core.ordering as order +import bigframes.core.utils as utils import bigframes.dtypes +import bigframes.formatting_helpers as formatter import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.series @@ -64,11 +66,21 @@ # BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. # TODO(tbergeron): Convert to bytes-based limit -MAX_INLINE_DF_SIZE = 5000 +# TODO(swast): Address issues with string escaping and empty tables before +# re-enabling inline data (ibis.memtable) feature. +MAX_INLINE_DF_SIZE = -1 -LevelsType = typing.Union[str, int, typing.Sequence[typing.Union[str, int]]] +LevelType = typing.Union[str, int] +LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] SingleItemValue = Union[bigframes.series.Series, int, float, Callable] +ERROR_IO_ONLY_GS_PATHS = f"Only Google Cloud Storage (gs://...) paths are supported. {constants.FEEDBACK_LINK}" +ERROR_IO_REQUIRES_WILDCARD = ( + "Google Cloud Storage path must contain a wildcard '*' character. See: " + "https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#export_data_statement" + f"{constants.FEEDBACK_LINK}" +) + # Inherits from pandas DataFrame so that we can use the same docstrings. class DataFrame(vendored_pandas_frame.DataFrame): @@ -87,7 +99,9 @@ def __init__( session: typing.Optional[bigframes.session.Session] = None, ): if copy is not None and not copy: - raise ValueError("DataFrame constructor only supports copy=True") + raise ValueError( + f"DataFrame constructor only supports copy=True. {constants.FEEDBACK_LINK}" + ) # Check to see if constructing from BigQuery-backed objects before # falling back to pandas constructor @@ -106,7 +120,9 @@ def __init__( ): if not all(isinstance(data[key], bf_series.Series) for key in data.keys()): # TODO(tbergeron): Support local list/series data by converting to memtable. - raise NotImplementedError("Cannot mix Series with other types.") + raise NotImplementedError( + f"Cannot mix Series with other types. {constants.FEEDBACK_LINK}" + ) keys = list(data.keys()) first_label, first_series = keys[0], data[keys[0]] block = ( @@ -128,7 +144,8 @@ def __init__( if block: if index: raise NotImplementedError( - "DataFrame 'index' constructor parameter not supported when passing BigQuery-backed objects" + "DataFrame 'index' constructor parameter not supported " + f"when passing BigQuery-backed objects. {constants.FEEDBACK_LINK}" ) if columns: block = block.select_columns(list(columns)) # type:ignore @@ -141,7 +158,7 @@ def __init__( else: import bigframes.pandas - pd_dataframe = pd.DataFrame( + pd_dataframe = pandas.DataFrame( data=data, index=index, # type:ignore columns=columns, # type:ignore @@ -182,12 +199,14 @@ def _find_indices( def _resolve_label_exact(self, label) -> str: matches = self._block.label_to_col_id.get(label, []) if len(matches) != 1: - raise ValueError("Index data must be 1-dimensional") + raise ValueError( + f"Index data must be 1-dimensional. {constants.FEEDBACK_LINK}" + ) return matches[0] def _sql_names( self, - columns: Union[blocks.Label, Sequence[blocks.Label], pd.Index], + columns: Union[blocks.Label, Sequence[blocks.Label], pandas.Index], tolerance: bool = False, ) -> Sequence[str]: """Retrieve sql name (column name in BQ schema) of column(s).""" @@ -215,11 +234,11 @@ def iloc(self) -> indexers.ILocDataFrameIndexer: return indexers.ILocDataFrameIndexer(self) @property - def dtypes(self) -> pd.Series: - return pd.Series(data=self._block.dtypes, index=self._block.column_labels) + def dtypes(self) -> pandas.Series: + return pandas.Series(data=self._block.dtypes, index=self._block.column_labels) @property - def columns(self) -> pd.Index: + def columns(self) -> pandas.Index: return self.dtypes.index @property @@ -253,19 +272,21 @@ def astype( ) -> DataFrame: return self._apply_to_rows(ops.AsTypeOp(dtype)) - def to_sql_query( + def _to_sql_query( self, always_include_index: bool ) -> Tuple[str, List[Tuple[str, bool]]]: - """Compiles this dataframe's expression tree to SQL, optionally - including unnamed index columns + """Compiles this DataFrame's expression tree to SQL, optionally + including unnamed index columns. Args: - always_include_index: whether to include unnamed index columns. - If False, only named indexes are included. + always_include_index (bool): + whether to include unnamed index columns. If False, only named + indexes are included. - Returns: a tuple of (sql_string, index_column_list). Each entry in the - index column list is a tuple of (column_name, named). If named is - is false, then the column name exists only in SQL""" + Returns: a tuple of (sql_string, index_column_list) + Each entry in the index column list is a tuple of (column_name, named). + If named is false, then the column name exists only in SQL + """ # Has to be unordered as it is impossible to order the sql without # including metadata columns in selection with ibis. ibis_expr = self._block.expr.to_ibis_expr(ordering_mode="unordered") @@ -325,15 +346,25 @@ def to_sql_query( @property def sql(self) -> str: - """Compiles this dataframe's expression tree to SQL""" - sql, _ = self.to_sql_query(always_include_index=False) + """Compiles this DataFrame's expression tree to SQL.""" + sql, _ = self._to_sql_query(always_include_index=False) return sql @property def query_job(self) -> Optional[bigquery.QueryJob]: - """BigQuery job metadata for the most recent query.""" + """BigQuery job metadata for the most recent query. + + Returns: + The most recent `QueryJob + `_. + """ + if self._query_job is None: + self._set_internal_query_job(self._compute_dry_run()) return self._query_job + def _set_internal_query_job(self, query_job: bigquery.QueryJob): + self._query_job = query_job + @typing.overload def __getitem__(self, key: bigframes.series.Series) -> DataFrame: ... @@ -343,7 +374,7 @@ def __getitem__(self, key: Sequence[blocks.Label]) -> DataFrame: # type:ignore ... @typing.overload - def __getitem__(self, key: pd.Index) -> DataFrame: # type:ignore + def __getitem__(self, key: pandas.Index) -> DataFrame: # type:ignore ... @typing.overload @@ -356,7 +387,7 @@ def __getitem__( blocks.Label, Sequence[blocks.Label], # Index of column labels can be treated the same as a sequence of column labels. - pd.Index, + pandas.Index, bigframes.series.Series, ], ) -> Union[bigframes.series.Series, "DataFrame"]: @@ -397,8 +428,10 @@ def __getitem__( # Bool Series selects rows def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: - if not key._to_ibis_expr().type() == ibis_dtypes.bool: - raise ValueError("Only boolean series currently supported for indexing.") + if not key.dtype == pandas.BooleanDtype(): + raise NotImplementedError( + f"Only boolean series currently supported for indexing. {constants.FEEDBACK_LINK}" + ) # TODO: enforce stricter alignment combined_index, ( get_column_left, @@ -413,16 +446,12 @@ def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: def __getattr__(self, key: str): if key in self._block.column_labels: return self.__getitem__(key) - elif hasattr(pd.DataFrame, key): + elif hasattr(pandas.DataFrame, key): raise NotImplementedError( textwrap.dedent( f""" BigQuery DataFrames has not yet implemented an equivalent to - 'pandas.DataFrame.{key}'. Please check - https://github.com/googleapis/python-bigquery-dataframes/issues for - existing feature requests, or file your own. - Please include information about your use case, as well as - relevant code snippets. + 'pandas.DataFrame.{key}'. {constants.FEEDBACK_LINK} """ ) ) @@ -432,14 +461,21 @@ def __getattr__(self, key: str): def __repr__(self) -> str: """Converts a DataFrame to a string. Calls compute. - Only represents the first ``bigframes.options.display.max_rows``. + Only represents the first `bigframes.options.display.max_rows`. """ opts = bigframes.options.display max_results = opts.max_rows + if opts.repr_mode == "deferred": + return formatter.repr_query_job(self.query_job) # TODO(swast): pass max_columns and get the true column count back. Maybe # get 1 more column than we have requested so that pandas can add the # ... for us? - pandas_df, row_count = self._retrieve_repr_request_results(max_results) + pandas_df, row_count, query_job = self._block.retrieve_repr_request_results( + max_results + ) + + self._set_internal_query_job(query_job) + column_count = len(pandas_df.columns) with display_options.pandas_repr(opts): @@ -466,10 +502,17 @@ def _repr_html_(self) -> str: """ opts = bigframes.options.display max_results = bigframes.options.display.max_rows + if opts.repr_mode == "deferred": + return formatter.repr_query_job_html(self.query_job) # TODO(swast): pass max_columns and get the true column count back. Maybe # get 1 more column than we have requested so that pandas can add the # ... for us? - pandas_df, row_count = self._retrieve_repr_request_results(max_results) + pandas_df, row_count, query_job = self._block.retrieve_repr_request_results( + max_results + ) + + self._set_internal_query_job(query_job) + column_count = len(pandas_df.columns) with display_options.pandas_repr(opts): @@ -479,33 +522,6 @@ def _repr_html_(self) -> str: html_string += f"[{row_count} rows x {column_count} columns in total]" return html_string - def _retrieve_repr_request_results( - self, max_results: int - ) -> Tuple[pd.DataFrame, int]: - """ - Retrieves a pandas dataframe containing only max_results many rows for use - with printing methods. - - Returns a tuple of the dataframe and the overall number of rows of the query. - """ - # TODO(swast): Select a subset of columns if max_columns is less than the - # number of columns in the schema. - count = self.shape[0] - if count > max_results: - head_df = self.head(n=max_results) - computed_df, query_job = head_df._block.compute(max_results=max_results) - else: - head_df = self - computed_df, query_job = head_df._block.compute() - - formatted_df = computed_df.set_axis(self._block.column_labels, axis=1) - # don't update details when the cache is hit - if self.query_job is None or not query_job.cache_hit: - self._query_job = query_job - # we reset the axis and substitute the bf index name for the default - formatted_df.index.name = self.index.name - return formatted_df, count - def __setitem__(self, key: str, value: SingleItemValue): """Modify or insert a column into the DataFrame. @@ -527,6 +543,7 @@ def _apply_binop( return self._apply_series_binop(other, op, axis=axis) raise NotImplementedError( f"binary operation is not implemented on the second operand of type {type(other).__name__}." + f"{constants.FEEDBACK_LINK}" ) def _apply_scalar_binop(self, other: float | int, op: ops.BinaryOp) -> DataFrame: @@ -549,7 +566,9 @@ def _apply_series_binop( raise ValueError(f"Invalid input: axis {axis}.") if axis in ("columns", 1): - raise NotImplementedError("Row Series operations haven't been supported.") + raise NotImplementedError( + f"Row Series operations haven't been supported. {constants.FEEDBACK_LINK}" + ) joined_index, (get_column_left, get_column_right) = self._block.index.join( other._block.index, how="outer" @@ -573,6 +592,16 @@ def _apply_series_binop( block = block.with_index_labels(self.index.names) return DataFrame(block) + def eq(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: + return self._apply_binop(other, ops.eq_op, axis=axis) + + def ne(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: + return self._apply_binop(other, ops.ne_op, axis=axis) + + __eq__ = eq # type: ignore + + __ne__ = ne # type: ignore + def le(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.le_op, axis=axis) @@ -661,12 +690,47 @@ def rmod(self, other: int | bigframes.series.Series, axis: str | int = "columns" __rmod__ = rmod - def compute(self) -> pd.DataFrame: - """Executes deferred operations and downloads the results.""" + def to_pandas( + self, + max_download_size: Optional[int] = None, + sampling_method: Optional[str] = None, + random_state: Optional[int] = None, + ) -> pandas.DataFrame: + """Write DataFrame to pandas DataFrame. + + Args: + max_download_size (int, default None): + Download size threshold in MB. If max_download_size is exceeded when downloading data + (e.g., to_pandas()), the data will be downsampled if + bigframes.options.sampling.enable_downsampling is True, otherwise, an error will be + raised. If set to a value other than None, this will supersede the global config. + sampling_method (str, default None): + Downsampling algorithms to be chosen from, the choices are: "head": This algorithm + returns a portion of the data from the beginning. It is fast and requires minimal + computations to perform the downsampling; "uniform": This algorithm returns uniform + random samples of the data. If set to a value other than None, this will supersede + the global config. + random_state (int, default None): + The seed for the uniform downsampling algorithm. If provided, the uniform method may + take longer to execute and require more computation. If set to a value other than + None, this will supersede the global config. + + 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. + """ # TODO(orrbradford): Optimize this in future. Potentially some cases where we can return the stored query job - df, query_job = self._block.compute() - self._query_job = query_job - return df.set_axis(self._block.column_labels, axis=1) + df, query_job = self._block.to_pandas( + max_download_size=max_download_size, + sampling_method=sampling_method, + random_state=random_state, + ) + self._set_internal_query_job(query_job) + return df.set_axis(self._block.column_labels, axis=1, copy=False) + + def _compute_dry_run(self) -> bigquery.QueryJob: + return self._block._compute_dry_run() def copy(self) -> DataFrame: return DataFrame(self._block) @@ -677,12 +741,50 @@ def head(self, n: int = 5) -> DataFrame: def tail(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[-n:]) - def drop(self, *, columns: Union[str, Iterable[str]]) -> DataFrame: - if not _is_list_like(columns): - columns = [columns] # type:ignore - columns = list(columns) + def drop( + self, + labels: typing.Any = None, + *, + axis: typing.Union[int, str] = 0, + index: typing.Any = None, + columns: Union[blocks.Label, Iterable[blocks.Label]] = None, + level: typing.Optional[LevelType] = None, + ) -> DataFrame: + if labels: + if index or columns: + raise ValueError("Cannot specify both 'labels' and 'index'/'columns") + axis_n = utils.get_axis_number(axis) + if axis_n == 0: + index = labels + else: + columns = labels + + block = self._block + if index: + level_id = self._resolve_levels(level or 0)[0] - block = self._block.drop_columns(self._sql_names(columns)) + if _is_list_like(index): + block, inverse_condition_id = block.apply_unary_op( + level_id, ops.IsInOp(index, match_nulls=True) + ) + block, condition_id = block.apply_unary_op( + inverse_condition_id, ops.invert_op + ) + else: + block, condition_id = block.apply_unary_op( + level_id, ops.partial_right(ops.ne_op, index) + ) + block = block.filter(condition_id, keep_null=True).select_columns( + self._block.value_columns + ) + if columns: + if not _is_list_like(columns): + columns = [columns] # type:ignore + columns = list(columns) + + block = block.drop_columns(self._sql_names(columns)) + if not index and not columns: + raise ValueError("Must specify 'labels' or 'index'/'columns") return DataFrame(block) def droplevel(self, level: LevelsType): @@ -722,7 +824,7 @@ def rename_axis( ) -> DataFrame: if len(kwargs) != 0: raise NotImplementedError( - "rename_axis does not currently support any keyword arguments." + f"rename_axis does not currently support any keyword arguments. {constants.FEEDBACK_LINK}" ) # limited implementation: the new index name is simply the 'mapper' parameter if _is_list_like(mapper): @@ -795,7 +897,7 @@ def _assign_series_join_on_index( # Update case, remove after copying into columns block = block.drop_columns([source_column]) - return DataFrame(block.with_index_labels([self.index.name])) + return DataFrame(block.with_index_labels(self.index.names)) def reset_index(self, *, drop: bool = False) -> DataFrame: block = self._block.reset_index(drop) @@ -814,9 +916,20 @@ def set_index( col_ids = [self._resolve_label_exact(key) for key in keys] return DataFrame(self._block.set_index(col_ids, append=append, drop=drop)) - def sort_index(self) -> DataFrame: + def sort_index( + self, ascending: bool = True, na_position: Literal["first", "last"] = "last" + ) -> DataFrame: + if na_position not in ["first", "last"]: + raise ValueError("Param na_position must be one of 'first' or 'last'") + direction = ( + order.OrderingDirection.ASC if ascending else order.OrderingDirection.DESC + ) + na_last = na_position == "last" index_columns = self._block.index_columns - ordering = [order.OrderingColumnReference(column) for column in index_columns] + ordering = [ + order.OrderingColumnReference(column, direction=direction, na_last=na_last) + for column in index_columns + ] return DataFrame(self._block.order_by(ordering)) def sort_values( @@ -824,6 +937,7 @@ def sort_values( by: str | typing.Sequence[str], *, ascending: bool | typing.Sequence[bool] = True, + kind: str = "quicksort", na_position: typing.Literal["first", "last"] = "last", ) -> DataFrame: if na_position not in {"first", "last"}: @@ -854,8 +968,9 @@ def sort_values( column_id, direction=direction, na_last=na_last ) ) - - return DataFrame(self._block.order_by(ordering)) + return DataFrame( + self._block.order_by(ordering, stable=kind in order.STABLE_SORTS) + ) def value_counts( self, @@ -902,7 +1017,7 @@ def any( else: frame = self._drop_non_bool() block = frame._block.aggregate_all_and_pivot( - agg_ops.any_op, dtype=pd.BooleanDtype() + agg_ops.any_op, dtype=pandas.BooleanDtype() ) return bigframes.series.Series(block.select_column("values")) @@ -912,7 +1027,7 @@ def all(self, *, bool_only: bool = False) -> bigframes.series.Series: else: frame = self._drop_non_bool() block = frame._block.aggregate_all_and_pivot( - agg_ops.all_op, dtype=pd.BooleanDtype() + agg_ops.all_op, dtype=pandas.BooleanDtype() ) return bigframes.series.Series(block.select_column("values")) @@ -932,6 +1047,20 @@ def mean(self, *, numeric_only: bool = False) -> bigframes.series.Series: block = frame._block.aggregate_all_and_pivot(agg_ops.mean_op) return bigframes.series.Series(block.select_column("values")) + def median( + self, *, numeric_only: bool = False, exact: bool = False + ) -> bigframes.series.Series: + if exact: + raise NotImplementedError( + f"Only approximate median is supported. {constants.FEEDBACK_LINK}" + ) + if not numeric_only: + frame = self._raise_on_non_numeric("median") + else: + frame = self._drop_non_numeric() + block = frame._block.aggregate_all_and_pivot(agg_ops.median_op) + return bigframes.series.Series(block.select_column("values")) + def std(self, *, numeric_only: bool = False) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("std") @@ -986,11 +1115,52 @@ def nunique(self) -> bigframes.series.Series: block = self._block.aggregate_all_and_pivot(agg_ops.nunique_op) return bigframes.series.Series(block.select_column("values")) - def _drop_non_numeric(self) -> DataFrame: + def agg( + self, func: str | typing.Sequence[str] + ) -> DataFrame | bigframes.series.Series: + if _is_list_like(func): + if any( + dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES + for dtype in self.dtypes + ): + raise NotImplementedError( + f"Multiple aggregations only supported on numeric columns. {constants.FEEDBACK_LINK}" + ) + aggregations = [agg_ops.AGGREGATIONS_LOOKUP[f] for f in func] + return DataFrame( + self._block.summarize( + self._block.value_columns, + aggregations, + ) + ) + else: + return bigframes.series.Series( + self._block.aggregate_all_and_pivot( + agg_ops.AGGREGATIONS_LOOKUP[typing.cast(str, func)] + ) + ) + + aggregate = agg + + def describe(self) -> DataFrame: + df_numeric = self._drop_non_numeric(keep_bool=False) + if len(df_numeric.columns) == 0: + raise NotImplementedError( + f"df.describe() currently only supports numeric values. {constants.FEEDBACK_LINK}" + ) + result = df_numeric.agg( + ["count", "mean", "std", "min", "25%", "50%", "75%", "max"] + ) + return typing.cast(DataFrame, result) + + def _drop_non_numeric(self, keep_bool=True) -> DataFrame: + types_to_keep = set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) + if not keep_bool: + types_to_keep -= set(bigframes.dtypes.BOOL_BIGFRAMES_TYPES) non_numeric_cols = [ col_id for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) - if dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES + if dtype not in types_to_keep ] return DataFrame(self._block.drop_columns(non_numeric_cols)) @@ -1008,7 +1178,8 @@ def _raise_on_non_numeric(self, op: str): for dtype in self._block.dtypes ): raise NotImplementedError( - f"'{op}' does not support non-numeric columns. Set 'numeric_only'=True to ignore non-numeric columns" + f"'{op}' does not support non-numeric columns. " + f"Set 'numeric_only'=True to ignore non-numeric columns. {constants.FEEDBACK_LINK}" ) return self @@ -1018,7 +1189,8 @@ def _raise_on_non_boolean(self, op: str): for dtype in self._block.dtypes ): raise NotImplementedError( - f"'{op}' does not support non-bool columns. Set 'bool_only'=True to ignore non-bool columns" + f"'{op}' does not support non-bool columns. " + f"Set 'bool_only'=True to ignore non-bool columns. {constants.FEEDBACK_LINK}" ) return self @@ -1044,10 +1216,12 @@ def merge( ) -> DataFrame: if on is None: if left_on is None or right_on is None: - raise ValueError("Must specify either on or left_on + right_on.") + raise ValueError("Must specify `on` or `left_on` + `right_on`.") else: if left_on is not None or right_on is not None: - raise ValueError("Can not pass both on and left_on + right_on params.") + raise ValueError( + "Can not pass both `on` and `left_on` + `right_on` params." + ) left_on, right_on = on, on left = self @@ -1075,7 +1249,7 @@ def merge( sort=sort, # In merging on the same column, it only returns 1 key column from coalesced both. # While if 2 different columns, both will be presented in the result. - get_both_join_key_cols=(left_on != right_on), + coalesce_join_keys=(left_on == right_on), ) # TODO(swast): Add suffixes to the column labels instead of reusing the # column IDs as the new labels. @@ -1138,14 +1312,57 @@ def _get_merged_col_labels( return left_col_labels + right_col_labels - def join(self, other: DataFrame, *, how: str = "left") -> DataFrame: - if not self.columns.intersection(other.columns).empty: - raise NotImplementedError("Deduping column names is not implemented") + def join( + self, other: DataFrame, *, on: Optional[str] = None, how: str = "left" + ) -> DataFrame: + left, right = self, other + if not left.columns.intersection(right.columns).empty: + raise NotImplementedError( + f"Deduping column names is not implemented. {constants.FEEDBACK_LINK}" + ) - left = self - right = other - combined_index, (get_column_left, get_column_right) = left._block.index.join( - right._block.index, how=how + # Join left columns with right index + if on is not None: + if other._block.index.nlevels != 1: + raise ValueError( + "Join on columns must match the index level of the other DataFrame. Join on column with multi-index haven't been supported." + ) + # Switch left index with on column + left_columns = left.columns + left_idx_original_names = left.index.names + left_idx_names_in_cols = [ + f"bigframes_left_idx_name_{i}" for i in range(len(left.index.names)) + ] + left.index.names = left_idx_names_in_cols + left = left.reset_index(drop=False) + left = left.set_index(on) + + # Join on index and switch back + combined_df = left._perform_join_by_index(right, how=how) + combined_df.index.name = on + combined_df = combined_df.reset_index(drop=False) + combined_df = combined_df.set_index(left_idx_names_in_cols) + + # To be consistent with Pandas + combined_df.index.names = ( + left_idx_original_names + if how in ("inner", "left") + else ([None] * len(combined_df.index.names)) + ) + + # Reorder columns + combined_df = combined_df[list(left_columns) + list(right.columns)] + return combined_df + + # Join left index with right index + if left._block.index.nlevels != right._block.index.nlevels: + raise ValueError("Index to join on must have the same number of levels.") + + return left._perform_join_by_index(right, how=how) + + def _perform_join_by_index(self, other: DataFrame, *, how: str = "left"): + combined_index, _ = self._block.index.join( + other._block.index, how=how, block_identity_join=True ) return DataFrame(combined_index._block) @@ -1178,7 +1395,7 @@ def _groupby_level( ): return groupby.DataFrameGroupBy( self._block, - self._resolve_levels(level), + by_col_ids=self._resolve_levels(level), as_index=as_index, dropna=dropna, ) @@ -1220,13 +1437,13 @@ def _groupby_series( matches = [*col_matches, *level_matches] if len(matches) != 1: raise ValueError( - f"GroupBy key {key} does not map to unambiguous column or index level" + f"GroupBy key {key} does not match a unique column or index level. BigQuery DataFrames only interprets lists of strings as column or index names, not directly as per-row group assignments." ) col_ids = [*col_ids, matches[0]] return groupby.DataFrameGroupBy( block, - col_ids, + by_col_ids=col_ids, as_index=as_index, dropna=dropna, ) @@ -1311,8 +1528,9 @@ def sample( ns = (n,) if n is not None else () fracs = (frac,) if frac is not None else () - - return self._split(ns=ns, fracs=fracs, random_state=random_state)[0] + return DataFrame( + self._block._split(ns=ns, fracs=fracs, random_state=random_state)[0] + ) def _split( self, @@ -1326,88 +1544,33 @@ def _split( At most one of ns and fracs can be passed in. If neither, default to ns = (1,). Return a list of sampled DataFrames. """ - if ns and fracs: - raise ValueError("Only one of 'ns' or 'fracs' parameter must be specified.") - - block = self._block - if not ns and not fracs: - ns = (1,) - - if ns: - sample_sizes = ns - else: - total_rows = block.shape[0] - # Round to nearest integer. "round half to even" rule applies. - # At least to be 1. - sample_sizes = [round(frac * total_rows) or 1 for frac in fracs] - - # Set random_state if it is not provided - if random_state is None: - random_state = random.randint(-(2**30), 2**30) - - # Create a new column with random_state value. - block, random_state_col = block.create_constant(random_state) - - # Create an ordering col and a new sum col which is ordering+random_state. - block, ordering_col = block.promote_offsets() - block, sum_col = block.apply_binary_op( - ordering_col, random_state_col, ops.add_op - ) - - # Apply hash method to sum col and order by it. - block, string_sum_col = block.apply_unary_op( - sum_col, ops.AsTypeOp("string[pyarrow]") - ) - block, hash_string_sum_col = block.apply_unary_op(string_sum_col, ops.hash_op) - block = block.order_by([order.OrderingColumnReference(hash_string_sum_col)]) - - drop_cols = [ - random_state_col, - ordering_col, - sum_col, - string_sum_col, - hash_string_sum_col, - ] - block = block.drop_columns(drop_cols) - df = DataFrame(block) - - intervals = [] - cur = 0 - for sample_size in sample_sizes: - intervals.append((cur, cur + sample_size)) - cur += sample_size - - # DF.iloc[slice] always returns DF. - return [ - typing.cast(DataFrame, df.iloc[lower:upper]) for lower, upper in intervals - ] + blocks = self._block._split(ns=ns, fracs=fracs, random_state=random_state) + return [DataFrame(block) for block in blocks] - def to_pandas(self) -> pd.DataFrame: - """Writes DataFrame to Pandas DataFrame.""" - # TODO(chelsealin): Support block parameters. - # TODO(chelsealin): Add to_pandas_batches() API. - return self.compute() - - def to_csv(self, path_or_buf: str, *, index: bool = True) -> None: + def to_csv( + self, path_or_buf: str, sep=",", *, header: bool = True, index: bool = True + ) -> None: # TODO(swast): Can we support partition columns argument? # TODO(chelsealin): Support local file paths. # TODO(swast): Some warning that wildcard is recommended for large # query results? See: # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size if not path_or_buf.startswith("gs://"): - raise NotImplementedError( - "Only Google Cloud Storage (gs://...) paths are supported." - ) + raise NotImplementedError(ERROR_IO_ONLY_GS_PATHS) + if "*" not in path_or_buf: + raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) - source_table = self._execute_query(index=index) - job_config = bigquery.ExtractJobConfig( - destination_format=bigquery.DestinationFormat.CSV + result_table = self._run_io_query( + index=index, ordering_id=bigframes.core.io.IO_ORDERING_ID ) - self._block.expr._session._extract_table( - source_table, - destination_uris=[path_or_buf], - job_config=job_config, + export_data_statement = bigframes.core.io.create_export_csv_statement( + f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", + uri=path_or_buf, + field_delimiter=sep, + header=header, ) + _, query_job = self._block.expr._session._start_query(export_data_statement) + self._set_internal_query_job(query_job) def to_json( self, @@ -1421,13 +1584,11 @@ def to_json( ) -> None: # TODO(swast): Can we support partition columns argument? # TODO(chelsealin): Support local file paths. - # TODO(swast): Some warning that wildcard is recommended for large - # query results? See: - # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size if not path_or_buf.startswith("gs://"): - raise NotImplementedError( - "Only Google Cloud Storage (gs://...) paths are supported." - ) + raise NotImplementedError(ERROR_IO_ONLY_GS_PATHS) + + if "*" not in path_or_buf: + raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) if lines is True and orient != "records": raise ValueError( @@ -1438,18 +1599,20 @@ def to_json( # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#to_json_string if lines is False: raise NotImplementedError( - "Only newline delimited JSON format is supported." + f"Only newline delimited JSON format is supported. {constants.FEEDBACK_LINK}" ) - source_table = self._execute_query(index=index) - job_config = bigquery.ExtractJobConfig( - destination_format=bigquery.DestinationFormat.NEWLINE_DELIMITED_JSON + result_table = self._run_io_query( + index=index, ordering_id=bigframes.core.io.IO_ORDERING_ID ) - self._block.expr._session._extract_table( - source_table, - destination_uris=[path_or_buf], - job_config=job_config, + export_data_statement = bigframes.core.io.create_export_data_statement( + f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", + uri=path_or_buf, + format="JSON", + export_options={}, ) + _, query_job = self._block.expr._session._start_query(export_data_statement) + self._set_internal_query_job(query_job) def to_gbq( self, @@ -1457,6 +1620,7 @@ def to_gbq( *, if_exists: Optional[Literal["fail", "replace", "append"]] = "fail", index: bool = True, + ordering_id: Optional[str] = None, ) -> None: if "." not in destination_table: raise ValueError( @@ -1480,12 +1644,12 @@ def to_gbq( ), ) - self._execute_query(index=index, job_config=job_config) + self._run_io_query(index=index, ordering_id=ordering_id, job_config=job_config) def to_numpy( self, dtype=None, copy=False, na_value=None, **kwargs ) -> numpy.ndarray: - return self.compute().to_numpy(dtype, copy, na_value, **kwargs) + return self.to_pandas().to_numpy(dtype, copy, na_value, **kwargs) __array__ = to_numpy @@ -1496,30 +1660,29 @@ def to_parquet(self, path: str, *, index: bool = True) -> None: # query results? See: # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size if not path.startswith("gs://"): - raise NotImplementedError( - "Only Google Cloud Storage (gs://...) paths are supported." - ) + raise NotImplementedError(ERROR_IO_ONLY_GS_PATHS) - source_table = self._execute_query(index=index) - job_config = bigquery.ExtractJobConfig( - destination_format=bigquery.DestinationFormat.PARQUET - ) + if "*" not in path: + raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) - self._block.expr._session._extract_table( - source_table, - destination_uris=[path], - job_config=job_config, + result_table = self._run_io_query( + index=index, ordering_id=bigframes.core.io.IO_ORDERING_ID ) + export_data_statement = bigframes.core.io.create_export_data_statement( + f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", + uri=path, + format="PARQUET", + export_options={}, + ) + _, query_job = self._block.expr._session._start_query(export_data_statement) + self._set_internal_query_job(query_job) def _apply_to_rows(self, operation: ops.UnaryOp): block = self._block.multi_apply_unary_op(self._block.value_columns, operation) return DataFrame(block) - def _execute_query( - self, index: bool, job_config: Optional[bigquery.job.QueryJobConfig] = None - ): - """Executes a query job presenting this dataframe and returns the destination - table.""" + def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: + """Create query text representing this dataframe for I/O.""" expr = self._block.expr session = expr._session columns = list(self._block.value_columns) @@ -1531,26 +1694,53 @@ def _execute_query( if index and self.index.name is not None: columns.extend(self._block.index_columns) column_labels.extend(self.index.names) - # TODO(chelsealin): normalize the file formats if we needs, such as arbitrary - # unicode for column labels. - value_columns = (expr.get_column(column_name) for column_name in columns) - expr = expr.projection(value_columns) + else: + expr = expr.drop_columns(self._block.index_columns) # Make columns in SQL reflect _labels_ not _ids_. Note: This may use # the arbitrary unicode column labels feature in BigQuery, which is # currently (June 2023) in preview. # TODO(swast): Handle duplicate and NULL labels. - ibis_expr = expr.to_ibis_expr() - renamed_columns = [ - ibis_expr[col_id].name(col_label) + id_overrides = { + col_id: col_label for col_id, col_label in zip(columns, column_labels) - ] - ibis_expr = ibis_expr.select(*renamed_columns) - sql = session.ibis_client.compile(ibis_expr) # type: ignore + if col_label + } + + if ordering_id is not None: + ibis_expr = expr.to_ibis_expr( + ordering_mode="offset_col", + col_id_overrides=id_overrides, + order_col_name=ordering_id, + ) + else: + ibis_expr = expr.to_ibis_expr( + ordering_mode="unordered", + col_id_overrides=id_overrides, + ) + + return session.ibis_client.compile(ibis_expr) # type: ignore + + def _run_io_query( + self, + index: bool, + ordering_id: Optional[str] = None, + job_config: Optional[bigquery.job.QueryJobConfig] = None, + ) -> bigquery.TableReference: + """Executes a query job presenting this dataframe and returns the destination + table.""" + expr = self._block.expr + session = expr._session + sql = self._create_io_query(index=index, ordering_id=ordering_id) _, query_job = session._start_query( sql=sql, job_config=job_config # type: ignore ) - return query_job.destination + self._set_internal_query_job(query_job) + + # The query job should have finished, so there should be always be a result table. + result_table = query_job.destination + assert result_table is not None + return result_table def map(self, func, na_action: Optional[str] = None) -> DataFrame: if not callable(func): @@ -1560,18 +1750,29 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: raise ValueError(f"na_action={na_action} not supported") # TODO(shobs): Support **kwargs - - return self._apply_to_rows( + # Reproject as workaround to applying filter too late. This forces the filter + # to be applied before passing data to remote function, protecting from bad + # inputs causing errors. + reprojected_df = DataFrame(self._block._force_reproject()) + return reprojected_df._apply_to_rows( ops.RemoteFunctionOp(func, apply_on_null=(na_action is None)) ) - def drop_duplicates(self, subset=None, *, keep: str = "first") -> DataFrame: + def drop_duplicates( + self, + subset: typing.Union[blocks.Label, typing.Sequence[blocks.Label]] = None, + *, + keep: str = "first", + ) -> DataFrame: if subset is None: column_ids = self._block.value_columns - else: + elif _is_list_like(subset): column_ids = [ id for label in subset for id in self._block.label_to_col_id[label] ] + else: + # interpret as single label + column_ids = self._block.label_to_col_id[typing.cast(blocks.Label, subset)] block = block_ops.drop_duplicates(self._block, column_ids, keep) return DataFrame(block) @@ -1619,8 +1820,8 @@ def _get_block(self) -> blocks.Block: def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: - return pd.api.types.is_list_like(obj) + return pandas.api.types.is_list_like(obj) def _is_dict_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Mapping]: - return pd.api.types.is_dict_like(obj) + return pandas.api.types.is_dict_like(obj) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index c257d6943eb..95cf737b2e7 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -14,6 +14,7 @@ """Mappings for Pandas dtypes supported by BigQuery DataFrames package""" +import textwrap import typing from typing import Any, Dict, Iterable, Literal, Tuple, Union @@ -25,6 +26,8 @@ import pandas as pd import pyarrow as pa +import bigframes.constants as constants + # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ pd.BooleanDtype, @@ -37,6 +40,9 @@ # Corresponds to the pandas concept of numeric type (such as when 'numeric_only' is specified in an operation) NUMERIC_BIGFRAMES_TYPES = [pd.BooleanDtype(), pd.Float64Dtype(), pd.Int64Dtype()] +# On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable +UNORDERED_DTYPES = [gpd.array.GeometryDtype()] + # Type hints for dtype strings supported by BigQuery DataFrame DtypeString = Literal[ "boolean", @@ -150,7 +156,9 @@ def ibis_dtype_to_bigframes_dtype( if ibis_dtype in IBIS_TO_BIGFRAMES: return IBIS_TO_BIGFRAMES[ibis_dtype] else: - raise ValueError(f"Unexpected Ibis data type {type(ibis_dtype)}") + raise ValueError( + f"Unexpected Ibis data type {type(ibis_dtype)}. {constants.FEEDBACK_LINK}" + ) def ibis_value_to_canonical_type(value: ibis_types.Value) -> ibis_types.Value: @@ -182,14 +190,14 @@ def bigframes_dtype_to_ibis_dtype( """Converts a BigQuery DataFrames supported dtype to an Ibis dtype. Args: - bigframes_dtype: A dtype supported by BigQuery DataFrame + bigframes_dtype: + A dtype supported by BigQuery DataFrame Returns: - The corresponding Ibis type + IbisDtype: The corresponding Ibis type Raises: - ValueError: - If passed a dtype not supported by BigQuery DataFrames. + ValueError: If passed a dtype not supported by BigQuery DataFrames. """ type_string = str(bigframes_dtype) if type_string in BIGFRAMES_STRING_TO_BIGFRAMES: @@ -197,7 +205,23 @@ def bigframes_dtype_to_ibis_dtype( typing.cast(DtypeString, type_string) ] else: - raise ValueError(f"Unexpected data type {bigframes_dtype}") + raise ValueError( + textwrap.dedent( + f""" + Unexpected data type {bigframes_dtype}. The following + str dtypes are supppted: 'boolean','Float64','Int64', 'string', + 'tring[pyarrow]','timestamp[us, tz=UTC][pyarrow]', + 'timestamp[us][pyarrow]','date32[day][pyarrow]', + 'time64[us][pyarrow]'. The following pandas.ExtensionDtype are + supported: pandas.BooleanDtype(), pandas.Float64Dtype(), + pandas.Int64Dtype(), pandas.StringDtype(storage="pyarrow"), + pd.ArrowDtype(pa.date32()), pd.ArrowDtype(pa.time64("us")), + pd.ArrowDtype(pa.timestamp("us")), + pd.ArrowDtype(pa.timestamp("us", tz="UTC")). + {constants.FEEDBACK_LINK} + """ + ) + ) return BIGFRAMES_TO_IBIS[bigframes_dtype] @@ -209,8 +233,10 @@ def literal_to_ibis_scalar( expression with a BigQuery DataFrames compatible data type Args: - literal: any value accepted by Ibis - force_dtype: force the value to a specific dtype + literal: + any value accepted by Ibis + force_dtype: + force the value to a specific dtype validate: If true, will raise ValueError if type cannot be stored in a BigQuery DataFrames object. If used as a subexpression, this should @@ -227,7 +253,9 @@ def literal_to_ibis_scalar( if pd.api.types.is_list_like(literal): if validate: - raise ValueError("List types can't be stored in BigQuery DataFrames") + raise ValueError( + f"List types can't be stored in BigQuery DataFrames. {constants.FEEDBACK_LINK}" + ) # "correct" way would be to use ibis.array, but this produces invalid BQ SQL syntax return tuple(literal) if not pd.api.types.is_list_like(literal) and pd.isna(literal): @@ -246,7 +274,9 @@ def literal_to_ibis_scalar( # TODO(bmil): support other literals that can be coerced to compatible types if validate and (scalar_expr.type() not in BIGFRAMES_TO_IBIS.values()): - raise ValueError(f"Literal did not coerce to a supported data type: {literal}") + raise ValueError( + f"Literal did not coerce to a supported data type: {literal}. {constants.FEEDBACK_LINK}" + ) return scalar_expr @@ -255,9 +285,11 @@ def cast_ibis_value(value: ibis_types.Value, to_type: IbisDtype) -> ibis_types.V """Perform compatible type casts of ibis values Args: - value: Ibis value, which could be a literal, scalar, or column + value: + Ibis value, which could be a literal, scalar, or column - to_type: The Ibis type to cast to + to_type: + The Ibis type to cast to Returns: A new Ibis value of type to_type @@ -275,7 +307,7 @@ def cast_ibis_value(value: ibis_types.Value, to_type: IbisDtype) -> ibis_types.V ibis_dtypes.float64, ibis_dtypes.string, ), - ibis_dtypes.float64: (ibis_dtypes.string,), + ibis_dtypes.float64: (ibis_dtypes.string, ibis_dtypes.int64), ibis_dtypes.string: (), ibis_dtypes.date: (), ibis_dtypes.time: (), @@ -289,7 +321,9 @@ def cast_ibis_value(value: ibis_types.Value, to_type: IbisDtype) -> ibis_types.V return value.cast(to_type) else: # this should never happen - raise TypeError(f"Unexpected value type {value.type()}") + raise TypeError( + f"Unexpected value type {value.type()}. {constants.FEEDBACK_LINK}" + ) # casts that need some encouragement @@ -301,4 +335,9 @@ def cast_ibis_value(value: ibis_types.Value, to_type: IbisDtype) -> ibis_types.V if value.type() == ibis_dtypes.bool and to_type == ibis_dtypes.float64: return value.cast(ibis_dtypes.int64).cast(ibis_dtypes.float64) - raise TypeError(f"Unsupported cast {value.type()} to {to_type}") + if value.type() == ibis_dtypes.float64 and to_type == ibis_dtypes.bool: + return value != ibis_types.literal(0) + + raise TypeError( + f"Unsupported cast {value.type()} to {to_type}. {constants.FEEDBACK_LINK}" + ) diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index fa0ed0c64d1..2c3c50ba754 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -13,28 +13,53 @@ # limitations under the License. """Shared helper functions for formatting jobs related info.""" +# TODO(orrbradford): cleanup up typings and documenttion in this file import datetime -from typing import Optional +from typing import Any, Optional, Union +import google.api_core.exceptions as api_core_exceptions import google.cloud.bigquery as bigquery import humanize import IPython import IPython.display as display import ipywidgets as widgets +import bigframes.constants as constants + +GenericJob = Union[bigquery.LoadJob, bigquery.ExtractJob, bigquery.QueryJob] + query_job_prop_pairs = { "Job Id": "job_id", "Destination Table": "destination", "Slot Time": "slot_millis", - "Bytes Processed": "estimated_bytes_processed", + "Bytes Processed": "total_bytes_processed", "Cache hit": "cache_hit", } -def repr_query_job(query_job: Optional[bigquery.QueryJob]): +def add_feedback_link( + exception: Union[ + api_core_exceptions.RetryError, api_core_exceptions.GoogleAPICallError + ] +): + exception.message = exception.message + f" {constants.FEEDBACK_LINK}" + + +def repr_query_job_html(query_job: Optional[bigquery.QueryJob]): + """Return query job in html format. + Args: + query_job: + The job representing the execution of the query on the server. + Returns: + Pywidget html table. + """ if query_job is None: return widgets.HTML("No job information available") + if query_job.dry_run: + return widgets.HTML( + f"Computation deferred. Computation will process {get_formatted_bytes(query_job.total_bytes_processed)}" + ) table_html = "" table_html += "" for key, value in query_job_prop_pairs.items(): @@ -54,7 +79,35 @@ def repr_query_job(query_job: Optional[bigquery.QueryJob]): return widgets.HTML(table_html) -def wait_for_job( +def repr_query_job(query_job: Optional[bigquery.QueryJob]): + """Return query job as a formatted string. + Args: + query_job: + The job representing the execution of the query on the server. + Returns: + Pywidget html table. + """ + if query_job is None: + return "No job information available" + if query_job.dry_run: + return f"Computation deferred. Computation will process {get_formatted_bytes(query_job.total_bytes_processed)}" + res = "Query Job Info" + for key, value in query_job_prop_pairs.items(): + job_val = getattr(query_job, value) + if job_val is not None: + res += "\n" + if key == "Job Id": # add link to job + res += f"""Job url: {get_job_url(query_job)}""" + elif key == "Slot Time": + res += f"""{key}: {get_formatted_time(job_val)}""" + elif key == "Bytes Processed": + res += f"""{key}: {get_formatted_bytes(job_val)}""" + else: + res += f"""{key}: {job_val}""" + return res + + +def wait_for_query_job( query_job: bigquery.QueryJob, max_results: Optional[int] = None, progress_bar: Optional[str] = None, @@ -72,19 +125,75 @@ def wait_for_job( if progress_bar == "auto": progress_bar = "notebook" if in_ipython() else "terminal" - if progress_bar == "notebook": - display.display(loading_bar) - query_result = query_job.result(max_results=max_results) - query_job.reload() - loading_bar.close() - elif progress_bar == "terminal": - print(get_query_job_loading_string(query_job)) - query_result = query_job.result(max_results=max_results) - query_job.reload() - return query_result + try: + if progress_bar == "notebook": + display.display(loading_bar) + query_result = query_job.result(max_results=max_results) + query_job.reload() + loading_bar.value = get_query_job_loading_html(query_job) + elif progress_bar == "terminal": + initial_loading_bar = get_query_job_loading_string(query_job) + print(initial_loading_bar) + query_result = query_job.result(max_results=max_results) + query_job.reload() + if initial_loading_bar != get_query_job_loading_string(query_job): + print(get_query_job_loading_string(query_job)) + else: + # No progress bar. + query_result = query_job.result(max_results=max_results) + query_job.reload() + return query_result + except api_core_exceptions.RetryError as exc: + add_feedback_link(exc) + raise + except api_core_exceptions.GoogleAPICallError as exc: + add_feedback_link(exc) + raise + + +def wait_for_job(job: GenericJob, progress_bar: Optional[str] = None): + """Waits for job results. Displays a progress bar while the job is running + Args: + job: + The bigquery job to be executed + """ + loading_bar = widgets.HTML(get_base_job_loading_html(job)) + if progress_bar == "auto": + progress_bar = "notebook" if in_ipython() else "terminal" + + try: + if progress_bar == "notebook": + display.display(loading_bar) + job.result() + job.reload() + loading_bar.value = get_base_job_loading_html(job) + elif progress_bar == "terminal": + inital_loading_bar = get_base_job_loading_string(job) + print(inital_loading_bar) + job.result() + job.reload() + if get_base_job_loading_string != inital_loading_bar: + print(get_base_job_loading_string(job)) + else: + # No progress bar. + job.result() + job.reload() + except api_core_exceptions.RetryError as exc: + add_feedback_link(exc) + raise + except api_core_exceptions.GoogleAPICallError as exc: + add_feedback_link(exc) + raise -def get_job_url(query_job: bigquery.QueryJob): +def get_job_url(query_job: GenericJob): + """Return url to the query job in cloud console. + Args: + query_job: + The job representing the execution of the query on the server. + Returns: + String url. + """ if ( query_job.project is None or query_job.location is None @@ -95,16 +204,57 @@ def get_job_url(query_job: bigquery.QueryJob): def get_query_job_loading_html(query_job: bigquery.QueryJob): - return f"""Job {query_job.job_id} is {query_job.state}. Open Job""" + """Return progress bar html string + Args: + query_job: + The job representing the execution of the query on the server. + Returns: + Html string. + """ + return f"""Query job {query_job.job_id} is {query_job.state}. {get_bytes_processed_string(query_job.total_bytes_processed)}Open Job""" def get_query_job_loading_string(query_job: bigquery.QueryJob): - return ( - f"""Job {query_job.job_id} is {query_job.state}. \n{get_job_url(query_job)}""" - ) + """Return progress bar string + Args: + query_job: + The job representing the execution of the query on the server. + Returns: + String + """ + return f"""Query job {query_job.job_id} is {query_job.state}.{get_bytes_processed_string(query_job.total_bytes_processed)} \n{get_job_url(query_job)}""" + + +def get_base_job_loading_html(job: GenericJob): + """Return progress bar html string + Args: + job: + The job representing the execution of the query on the server. + Returns: + Html string. + """ + return f"""{job.job_type.capitalize()} job {job.job_id} is {job.state}. Open Job""" + + +def get_base_job_loading_string(job: GenericJob): + """Return progress bar string + Args: + job: + The job representing the execution of the query on the server. + Returns: + String + """ + return f"""{job.job_type.capitalize()} job {job.job_id} is {job.state}. \n{get_job_url(job)}""" def get_formatted_time(val): + """Try to format time + Args: + val: + Time in ms + Returns: + Duration string + """ try: return humanize.naturaldelta(datetime.timedelta(milliseconds=float(val))) except Exception: @@ -112,7 +262,24 @@ def get_formatted_time(val): def get_formatted_bytes(val): - return humanize.naturalsize(val) + """Try to format bytes + Args: + val (Any): + Bytes to format + Returns: + Duration string + """ + if isinstance(val, int): + return humanize.naturalsize(val) + return "N/A" + + +def get_bytes_processed_string(val: Any): + """Try to get bytes processed string. Return empty if passed non int value""" + bytes_processed_string = "" + if isinstance(val, int): + bytes_processed_string = f"""{get_formatted_bytes(val)} processed. """ + return bytes_processed_string def in_ipython(): diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index e0a1027dfea..e4c68eb17c2 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -22,9 +22,9 @@ """ import abc -from typing import Optional, TypeVar +from typing import cast, Optional, TypeVar -from bigframes.ml.core import BqmlModel +from bigframes.ml import core import third_party.bigframes_vendored.sklearn.base @@ -92,7 +92,7 @@ class Predictor(BaseEstimator): """A BigQuery DataFrames ML Model base class that can be used to predict outputs.""" def __init__(self): - self._bqml_model: Optional[BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None @abc.abstractmethod def predict(self, X): @@ -102,21 +102,32 @@ def predict(self, X): def register(self: _T, vertex_ai_model_id: Optional[str] = None) -> _T: """Register the model to Vertex AI. + + After register, go to https://pantheon.corp.google.com/vertex-ai/models to manage the model registries. + Refer to https://cloud.google.com/vertex-ai/docs/model-registry/introduction for more options. + Args: - vertex_ai_model_id: optional string id as model id in Vertex. If not set, will by default to 'bigframes_{bq_model_id}'. + vertex_ai_model_id (Optional[str], default None): + optional string id as model id in Vertex. If not set, will by default to 'bigframes_{bq_model_id}'. + Vertex Ai model id will be truncated to 63 characters due to its limitation. Returns: BigQuery DataFrames Model after register. """ if not self._bqml_model: - raise RuntimeError("A model must be trained before register.") + # TODO(garrettwu): find a more elegant way to do this. + try: + self._bqml_model = self._create_bqml_model() # type: ignore + except AttributeError: + raise RuntimeError("A model must be trained before register.") + self._bqml_model = cast(core.BqmlModel, self._bqml_model) self._bqml_model.register(vertex_ai_model_id) return self class TrainablePredictor(Predictor): - """A BigQuery DataFrame ML Model base class that can be used to fit and predict outputs. + """A BigQuery DataFrames ML Model base class that can be used to fit and predict outputs. Also the predictor can be attached to a pipeline with transformers.""" @@ -124,10 +135,9 @@ class TrainablePredictor(Predictor): def fit(self, X, y, transforms): pass - # TODO(b/289280565): enable signatures after updating KMeans and PCA - # @abc.abstractmethod - # def score(self, X, y): - # pass + @abc.abstractmethod + def score(self, X, y): + pass # TODO(b/291812029): move to Predictor after implement in LLM and imported models @abc.abstractmethod diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 2dd95bfc1c9..6f6efb9f376 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -13,35 +13,33 @@ # limitations under the License. """Clustering models. This module is styled after Scikit-Learn's cluster module: -https://scikit-learn.org/stable/modules/clustering.html""" +https://scikit-learn.org/stable/modules/clustering.html.""" from __future__ import annotations -from typing import cast, Dict, List, Optional, TYPE_CHECKING +from typing import cast, Dict, List, Optional, Union from google.cloud import bigquery -if TYPE_CHECKING: - import bigframes - -import bigframes.ml.base -import bigframes.ml.core +import bigframes +from bigframes.ml import base, core, utils +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.cluster._kmeans class KMeans( third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans.__doc__ def __init__(self, n_clusters=8): self.n_clusters = n_clusters - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod - def _from_bq(session: bigframes.Session, model: bigquery.Model) -> KMeans: + @classmethod + def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> KMeans: assert model.model_type == "KMEANS" kwargs = {} @@ -51,8 +49,8 @@ def _from_bq(session: bigframes.Session, model: bigquery.Model) -> KMeans: if "numClusters" in last_fitting: kwargs["n_clusters"] = int(last_fitting["numClusters"]) - new_kmeans = KMeans(**kwargs) - new_kmeans._bqml_model = bigframes.ml.core.BqmlModel(session, model) + new_kmeans = cls(**kwargs) + new_kmeans._bqml_model = core.BqmlModel(session, model) return new_kmeans @property @@ -62,36 +60,55 @@ def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y=None, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> KMeans: + (X,) = utils.convert_to_dataframe(X) + + self._bqml_model = core.create_bqml_model( train_X=X, transforms=transforms, options=self._bqml_options, ) + return self def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + self, + X: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") - return cast( - bigframes.dataframe.DataFrame, self._bqml_model.predict(X)[["CENTROID_ID"]] - ) + (X,) = utils.convert_to_dataframe(X) + + return cast(bpd.DataFrame, self._bqml_model.predict(X)[["CENTROID_ID"]]) def to_gbq(self, model_name: str, replace: bool = False) -> KMeans: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + KMeans: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) + + def score( + self, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + (X,) = utils.convert_to_dataframe(X) + + return self._bqml_model.evaluate(X) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 09645d4cf8e..49b4899bebd 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -12,33 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Build composite transformers on heterogenous data. This module is styled +"""Build composite transformers on heterogeneous data. This module is styled after Scikit-Learn's compose module: -https://scikit-learn.org/stable/modules/classes.html#module-sklearn.compose""" +https://scikit-learn.org/stable/modules/classes.html#module-sklearn.compose.""" from __future__ import annotations import typing -from typing import List, Optional, Tuple, TYPE_CHECKING, Union +from typing import List, Optional, Tuple, Union -if TYPE_CHECKING: - import bigframes - -import bigframes.ml.base -import bigframes.ml.compose -import bigframes.ml.core -import bigframes.ml.preprocessing +from bigframes import constants +from bigframes.ml import base, core, preprocessing, utils +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.compose._column_transformer CompilablePreprocessorType = Union[ - bigframes.ml.preprocessing.OneHotEncoder, - bigframes.ml.preprocessing.StandardScaler, + preprocessing.OneHotEncoder, + preprocessing.StandardScaler, ] class ColumnTransformer( third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, - bigframes.ml.base.BaseEstimator, + base.BaseEstimator, ): __doc__ = ( third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer.__doc__ @@ -54,14 +50,17 @@ def __init__( ] ], ): + # TODO: if any(transformers) has fitted raise warning self.transformers = transformers - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None + # call self.transformers_ to check chained transformers + self.transformers_ @property def transformers_( self, ) -> List[Tuple[str, CompilablePreprocessorType, str,]]: - """The collection of transformers as tuples of (name, transformer, column)""" + """The collection of transformers as tuples of (name, transformer, column).""" result: List[ Tuple[ str, @@ -69,13 +68,22 @@ def transformers_( str, ] ] = [] + + column_set: set[str] = set() for entry in self.transformers: name, transformer, column_or_columns = entry - if isinstance(column_or_columns, str): - result.append((name, transformer, column_or_columns)) - else: - for column in column_or_columns: - result.append((name, transformer, column)) + columns = ( + column_or_columns + if isinstance(column_or_columns, List) + else [column_or_columns] + ) + + for column in columns: + if column in column_set: + raise NotImplementedError( + f"Chained transformers on the same column isn't supported. {constants.FEEDBACK_LINK}" + ) + result.append((name, transformer, column)) return result @@ -84,9 +92,11 @@ def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: a BQML TRANSFORM clause Args: - columns: a list of column names to transform + columns (List[str]): + a list of column names to transform - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: + a list of tuples of (sql_expression, output_name)""" return [ transformer._compile_to_sql([column])[0] for column in columns @@ -96,12 +106,15 @@ def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - ): + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> ColumnTransformer: + (X,) = utils.convert_to_dataframe(X) + compiled_transforms = self._compile_to_sql(X.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - self._bqml_model = bigframes.ml.core.create_bqml_model( + self._bqml_model = core.create_bqml_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, @@ -109,15 +122,16 @@ def fit( # The schema of TRANSFORM output is not available in the model API, so save it during fitting self._output_names = [name for _, name in compiled_transforms] + return self - def transform( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("Must be fitted before transform") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.transform(X) return typing.cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[self._output_names], ) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 2663a8e17e5..9629ca0f4d5 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -21,9 +21,10 @@ from google.cloud import bigquery -import bigframes.dataframe -import bigframes.ml.sql -import bigframes.session +import bigframes +import bigframes.constants as constants +from bigframes.ml import sql as ml_sql +import bigframes.pandas as bpd class BqmlModel: @@ -33,7 +34,7 @@ class BqmlModel: BigQuery DataFrames ML. """ - def __init__(self, session: bigframes.session.Session, model: bigquery.Model): + def __init__(self, session: bigframes.Session, model: bigquery.Model): self._session = session self._model = model @@ -52,29 +53,35 @@ def model(self) -> bigquery.Model: """Get the BQML model associated with this wrapper""" return self._model - @staticmethod + @classmethod def _apply_sql( + cls, session: bigframes.Session, - input_data: bigframes.dataframe.DataFrame, + input_data: bpd.DataFrame, func: Callable[[str], str], - ) -> bigframes.dataframe.DataFrame: + ) -> bpd.DataFrame: """Helper to wrap a dataframe in a SQL query, keeping the index intact. Args: - session: the active bigframes.Session + session (bigframes.Session): + the active bigframes.Session - input_data: the dataframe to be wrapped + input_data (bigframes.dataframe.DataFrame): + the dataframe to be wrapped - func: a function that will accept a SQL string and produce a new SQL + func (function): + a function that will accept a SQL string and produce a new SQL string from which to construct the output dataframe. It must include the index columns of the input SQL. """ - source_sql, tagged_index_cols = input_data.to_sql_query( + source_sql, tagged_index_cols = input_data._to_sql_query( always_include_index=True ) if len(tagged_index_cols) != 1: - raise NotImplementedError("Only exactly one index column is supported") + raise NotImplementedError( + f"Only exactly one index column is supported. {constants.FEEDBACK_LINK}" + ) index_col_name, is_named_index = tagged_index_cols[0] sql = func(source_sql) @@ -84,79 +91,75 @@ def _apply_sql( return df - def predict( - self, input_data: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def predict(self, input_data: bpd.DataFrame) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( self._session, input_data, - lambda source_sql: bigframes.ml.sql.ml_predict( + lambda source_sql: ml_sql.ml_predict( model_name=self.model_name, source_sql=source_sql ), ) - def transform( - self, input_data: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def transform(self, input_data: bpd.DataFrame) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( self._session, input_data, - lambda source_sql: bigframes.ml.sql.ml_transform( + lambda source_sql: ml_sql.ml_transform( model_name=self.model_name, source_sql=source_sql ), ) def generate_text( self, - input_data: bigframes.dataframe.DataFrame, + input_data: bpd.DataFrame, options: Mapping[str, int | float], - ) -> bigframes.dataframe.DataFrame: + ) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( self._session, input_data, - lambda source_sql: bigframes.ml.sql.ml_generate_text( + lambda source_sql: ml_sql.ml_generate_text( model_name=self.model_name, source_sql=source_sql, - struct_options=bigframes.ml.sql.struct_options(**options), + struct_options=ml_sql.struct_options(**options), ), ) - def embed_text( + def generate_text_embedding( self, - input_data: bigframes.dataframe.DataFrame, + input_data: bpd.DataFrame, options: Mapping[str, int | float], - ) -> bigframes.dataframe.DataFrame: + ) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( self._session, input_data, - lambda source_sql: bigframes.ml.sql.ml_embed_text( + lambda source_sql: ml_sql.ml_generate_text_embedding( model_name=self.model_name, source_sql=source_sql, - struct_options=bigframes.ml.sql.struct_options(**options), + struct_options=ml_sql.struct_options(**options), ), ) - def forecast(self) -> bigframes.dataframe.DataFrame: - sql = bigframes.ml.sql.ml_forecast(self.model_name) + def forecast(self) -> bpd.DataFrame: + sql = ml_sql.ml_forecast(self.model_name) return self._session.read_gbq(sql) - def evaluate(self, input_data: Union[bigframes.dataframe.DataFrame, None] = None): + def evaluate(self, input_data: Optional[bpd.DataFrame] = None): # TODO: validate input data schema # Note: don't need index as evaluate returns a new table source_sql, _ = ( - input_data.to_sql_query(always_include_index=False) + input_data._to_sql_query(always_include_index=False) if (input_data is not None) else (None, None) ) - sql = bigframes.ml.sql.ml_evaluate(self.model_name, source_sql) + sql = ml_sql.ml_evaluate(self.model_name, source_sql) return self._session.read_gbq(sql) - def copy(self, new_model_name, replace=False) -> BqmlModel: + def copy(self, new_model_name: str, replace: bool = False) -> BqmlModel: job_config = bigquery.job.CopyJobConfig() if replace: job_config.write_disposition = "WRITE_TRUNCATE" @@ -173,10 +176,11 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: # vertex id needs to start with letters. https://cloud.google.com/vertex-ai/docs/general/resource-naming vertex_ai_model_id = "bigframes_" + cast(str, self._model.model_id) - options_sql = bigframes.ml.sql.options( - **{"vertex_ai_model_id": vertex_ai_model_id} - ) - sql = bigframes.ml.sql.alter_model(self.model_name, options_sql=options_sql) + # truncate as Vertex ID only accepts 63 characters, easily exceeding the limit for temp models. + # The possibility of conflicts should be low. + vertex_ai_model_id = vertex_ai_model_id[:63] + options_sql = ml_sql.options(**{"vertex_ai_model_id": vertex_ai_model_id}) + sql = ml_sql.alter_model(self.model_name, options_sql=options_sql) # Register the model and wait it to finish self._session.bqclient.query(sql).result() @@ -185,8 +189,8 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: def create_bqml_model( - train_X: bigframes.dataframe.DataFrame, - train_y: Optional[bigframes.dataframe.DataFrame] = None, + train_X: bpd.DataFrame, + train_y: Optional[bpd.DataFrame] = None, transforms: Optional[Iterable[str]] = None, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: @@ -217,29 +221,22 @@ def create_bqml_model( # for now, drop index to avoid including the index in feature columns input_data = input_data.reset_index(drop=True) - model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" source_sql = input_data.sql - options_sql = bigframes.ml.sql.options(**options) - transform_sql = ( - bigframes.ml.sql.transform(*transforms) if transforms is not None else None - ) - sql = bigframes.ml.sql.create_model( - model_name=model_name, + options_sql = ml_sql.options(**options) + transform_sql = ml_sql.transform(*transforms) if transforms is not None else None + sql = ml_sql.create_model( + model_name=_create_temp_model_name(), source_sql=source_sql, transform_sql=transform_sql, options_sql=options_sql, ) - # fit the model, synchronously - session.bqclient.query(sql).result() - - model = session.bqclient.get_model(model_name) - return BqmlModel(session, model) + return _create_bqml_model_with_sql(session=session, sql=sql) def create_bqml_time_series_model( - train_X: bigframes.dataframe.DataFrame, - train_y: bigframes.dataframe.DataFrame, + train_X: bpd.DataFrame, + train_y: bpd.DataFrame, transforms: Optional[Iterable[str]] = None, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: @@ -258,25 +255,18 @@ def create_bqml_time_series_model( # pickpocket session object from the dataframe session = train_X._get_block().expr._session - model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" source_sql = input_data.sql - options_sql = bigframes.ml.sql.options(**options) + options_sql = ml_sql.options(**options) - transform_sql = ( - bigframes.ml.sql.transform(*transforms) if transforms is not None else None - ) - sql = bigframes.ml.sql.create_model( - model_name=model_name, + transform_sql = ml_sql.transform(*transforms) if transforms is not None else None + sql = ml_sql.create_model( + model_name=_create_temp_model_name(), source_sql=source_sql, transform_sql=transform_sql, options_sql=options_sql, ) - # fit the model, synchronously - session.bqclient.query(sql).result() - - model = session.bqclient.get_model(model_name) - return BqmlModel(session, model) + return _create_bqml_model_with_sql(session=session, sql=sql) def create_bqml_remote_model( @@ -287,25 +277,22 @@ def create_bqml_remote_model( """Create a session-temporary BQML remote model with the CREATE MODEL statement Args: - connection_name: a BQ connection to talk with Vertex AI, of the format ... https://cloud.google.com/bigquery/docs/create-cloud-resource-connection - options: a dict of options to configure the model. Generates a BQML OPTIONS - clause + connection_name: + a BQ connection to talk with Vertex AI, of the format ... https://cloud.google.com/bigquery/docs/create-cloud-resource-connection + options: + a dict of options to configure the model. Generates a BQML OPTIONS clause - Returns: a BqmlModel, wrapping a trained model in BigQuery + Returns: + BqmlModel: a BqmlModel wrapping a trained model in BigQuery """ - model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" - options_sql = bigframes.ml.sql.options(**options) - sql = bigframes.ml.sql.create_remote_model( - model_name=model_name, + options_sql = ml_sql.options(**options) + sql = ml_sql.create_remote_model( + model_name=_create_temp_model_name(), connection_name=connection_name, options_sql=options_sql, ) - # create the model, synchronously - session.bqclient.query(sql).result() - - model = session.bqclient.get_model(model_name) - return BqmlModel(session, model) + return _create_bqml_model_with_sql(session=session, sql=sql) def create_bqml_imported_model( @@ -320,15 +307,25 @@ def create_bqml_imported_model( Returns: a BqmlModel, wrapping a trained model in BigQuery """ - model_name = f"{session._session_dataset_id}.{uuid.uuid4().hex}" - options_sql = bigframes.ml.sql.options(**options) - sql = bigframes.ml.sql.create_imported_model( - model_name=model_name, + options_sql = ml_sql.options(**options) + sql = ml_sql.create_imported_model( + model_name=_create_temp_model_name(), options_sql=options_sql, ) - # create the model, synchronously - session.bqclient.query(sql).result() + return _create_bqml_model_with_sql(session=session, sql=sql) + + +def _create_temp_model_name() -> str: + return uuid.uuid4().hex + + +def _create_bqml_model_with_sql(session: bigframes.Session, sql: str) -> BqmlModel: + # fit the model, synchronously + job = session.bqclient.query(sql) + job.result() - model = session.bqclient.get_model(model_name) + # real model path in the session specific hidden dataset and table prefix + model_name_full = f"{job.destination.dataset_id}.{job.destination.table_id}" + model = session.bqclient.get_model(model_name_full) return BqmlModel(session, model) diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 08551d1cb8c..76b4f9ced6a 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -13,34 +13,32 @@ # limitations under the License. """Matrix Decomposition models. This module is styled after Scikit-Learn's decomposition module: -https://scikit-learn.org/stable/modules/decomposition.html""" +https://scikit-learn.org/stable/modules/decomposition.html.""" from __future__ import annotations -from typing import cast, List, Optional, TYPE_CHECKING +from typing import cast, List, Optional, Union from google.cloud import bigquery -if TYPE_CHECKING: - import bigframes - -import bigframes.ml.base -import bigframes.ml.core +import bigframes +from bigframes.ml import base, core, utils +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.decomposition._pca class PCA( third_party.bigframes_vendored.sklearn.decomposition._pca.PCA, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.sklearn.decomposition._pca.PCA.__doc__ def __init__(self, n_components=3): self.n_components = n_components - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod - def _from_bq(session: bigframes.Session, model: bigquery.Model) -> PCA: + @classmethod + def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> PCA: assert model.model_type == "PCA" kwargs = {} @@ -50,17 +48,19 @@ def _from_bq(session: bigframes.Session, model: bigquery.Model) -> PCA: if "numPrincipalComponents" in last_fitting: kwargs["n_components"] = int(last_fitting["numPrincipalComponents"]) - new_pca = PCA(**kwargs) - new_pca._bqml_model = bigframes.ml.core.BqmlModel(session, model) + new_pca = cls(**kwargs) + new_pca._bqml_model = core.BqmlModel(session, model) return new_pca def fit( self, - X: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], y=None, transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> PCA: + (X,) = utils.convert_to_dataframe(X) + + self._bqml_model = core.create_bqml_model( train_X=X, transforms=transforms, options={ @@ -68,37 +68,45 @@ def fit( "num_principal_components": self.n_components, }, ) + return self - def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: - """Predict the closest cluster for each sample in X. - - Args: - X: a BigQuery DataFrame to predict. - y: ignored for API consistency. - - Returns: predicted BigQuery DataFrames.""" + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") + (X,) = utils.convert_to_dataframe(X) + return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, self._bqml_model.predict(X)[ ["principal_component_" + str(i + 1) for i in range(self.n_components)] ], ) def to_gbq(self, model_name: str, replace: bool = False) -> PCA: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + PCA: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) + + def score( + self, + X=None, + y=None, + ) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + # TODO(b/291973741): X param is ignored. Update BQML supports input in ML.EVALUTE. + return self._bqml_model.evaluate() diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 117759ca1c6..56a0cc3d949 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -12,20 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Linear models. This module is styled after Scikit-Learn's linear_model module: -https://scikit-learn.org/stable/modules/linear_model.html""" +"""Ensemble models. This module is styled after Scikit-Learn's ensemble module: +https://scikit-learn.org/stable/modules/ensemble.html""" from __future__ import annotations -from typing import cast, Dict, List, Literal, Optional, TYPE_CHECKING +from typing import cast, Dict, List, Literal, Optional, Union from google.cloud import bigquery -if TYPE_CHECKING: - import bigframes - -import bigframes.ml.base -import bigframes.ml.core +import bigframes +from bigframes.ml import base, core, utils +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.ensemble._forest import third_party.bigframes_vendored.xgboost.sklearn @@ -51,7 +49,7 @@ class XGBRegressor( third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor.__doc__ @@ -96,10 +94,12 @@ def __init__( self.min_rel_progress = min_rel_progress self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod - def _from_bq(session: bigframes.Session, model: bigquery.Model) -> XGBRegressor: + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> XGBRegressor: assert model.model_type == "BOOSTED_TREE_REGRESSOR" kwargs = {} @@ -107,14 +107,14 @@ def _from_bq(session: bigframes.Session, model: bigquery.Model) -> XGBRegressor: # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun last_fitting = model.training_runs[-1]["trainingOptions"] - dummy_regressor = XGBRegressor() + dummy_regressor = cls() for bf_param, bf_value in dummy_regressor.__dict__.items(): bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) if bqml_param is not None: kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) - new_xgb_regressor = XGBRegressor(**kwargs) - new_xgb_regressor._bqml_model = bigframes.ml.core.BqmlModel(session, model) + new_xgb_regressor = cls(**kwargs) + new_xgb_regressor._bqml_model = core.BqmlModel(session, model) return new_xgb_regressor @property @@ -145,26 +145,31 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> XGBRegressor: + X, y = utils.convert_to_dataframe(X, y) + + self._bqml_model = core.create_bqml_model( X, y, transforms=transforms, options=self._bqml_options, ) + return self def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + self, + X: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") + (X,) = utils.convert_to_dataframe(X) df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -175,9 +180,11 @@ def predict( def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], ): + X, y = utils.convert_to_dataframe(X, y) + if not self._bqml_model: raise RuntimeError("A model must be fitted before score") @@ -187,11 +194,13 @@ def score( return self._bqml_model.evaluate(input_data) def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. Returns: saved model.""" if not self._bqml_model: @@ -203,7 +212,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: class XGBClassifier( third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier.__doc__ @@ -249,10 +258,12 @@ def __init__( self.min_rel_progress = min_rel_progress self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod - def _from_bq(session: bigframes.Session, model: bigquery.Model) -> XGBClassifier: + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> XGBClassifier: assert model.model_type == "BOOSTED_TREE_CLASSIFIER" kwargs = {} @@ -266,8 +277,8 @@ def _from_bq(session: bigframes.Session, model: bigquery.Model) -> XGBClassifier if bqml_param is not None: kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) - new_xgb_classifier = XGBClassifier(**kwargs) - new_xgb_classifier._bqml_model = bigframes.ml.core.BqmlModel(session, model) + new_xgb_classifier = cls(**kwargs) + new_xgb_classifier._bqml_model = core.BqmlModel(session, model) return new_xgb_classifier @property @@ -298,26 +309,29 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> XGBClassifier: + X, y = utils.convert_to_dataframe(X, y) + + self._bqml_model = core.create_bqml_model( X, y, transforms=transforms, options=self._bqml_options, ) + return self - def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -328,25 +342,30 @@ def predict( def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], ): if not self._bqml_model: raise RuntimeError("A model must be fitted before score") + X, y = utils.convert_to_dataframe(X, y) + input_data = ( X.join(y, how="outer") if (X is not None) and (y is not None) else None ) return self._bqml_model.evaluate(input_data) def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + XGBClassifier: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") @@ -356,7 +375,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: class RandomForestRegressor( third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = ( @@ -396,11 +415,11 @@ def __init__( self.min_rel_progress = min_rel_progress self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod + @classmethod def _from_bq( - session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, model: bigquery.Model ) -> RandomForestRegressor: assert model.model_type == "RANDOM_FOREST_REGRESSOR" @@ -409,16 +428,14 @@ def _from_bq( # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun last_fitting = model.training_runs[-1]["trainingOptions"] - dummy_model = RandomForestRegressor() + dummy_model = cls() for bf_param, bf_value in dummy_model.__dict__.items(): bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) if bqml_param is not None: kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) - new_random_forest_regressor = RandomForestRegressor(**kwargs) - new_random_forest_regressor._bqml_model = bigframes.ml.core.BqmlModel( - session, model - ) + new_random_forest_regressor = cls(**kwargs) + new_random_forest_regressor._bqml_model = core.BqmlModel(session, model) return new_random_forest_regressor @property @@ -446,26 +463,32 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> RandomForestRegressor: + X, y = utils.convert_to_dataframe(X, y) + + self._bqml_model = core.create_bqml_model( X, y, transforms=transforms, options=self._bqml_options, ) + return self def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + self, + X: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -476,36 +499,41 @@ def predict( def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], ): """Calculate evaluation metrics of the model. Args: - X: a BigFrames DataFrame as evaluation data. - y: a BigFrames DataFrame as evaluation labels. - - Returns: a BigFrames DataFrame as evaluation result.""" + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation data. + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation labels. + + Returns: + bigframes.dataframe.DataFrame: The DataFrame as evaluation result. + """ if not self._bqml_model: raise RuntimeError("A model must be fitted before score") - if (X is None) != (y is None): - raise ValueError( - "Either both or neither of test_X and test_y must be specified" - ) + X, y = utils.convert_to_dataframe(X, y) + input_data = ( X.join(y, how="outer") if (X is not None) and (y is not None) else None ) return self._bqml_model.evaluate(input_data) def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegressor: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + RandomForestRegressor: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") @@ -515,7 +543,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegresso class RandomForestClassifier( third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = ( @@ -555,11 +583,11 @@ def __init__( self.min_rel_progress = min_rel_progress self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod + @classmethod def _from_bq( - session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, model: bigquery.Model ) -> RandomForestClassifier: assert model.model_type == "RANDOM_FOREST_CLASSIFIER" @@ -574,10 +602,8 @@ def _from_bq( if bqml_param is not None: kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) - new_random_forest_classifier = RandomForestClassifier(**kwargs) - new_random_forest_classifier._bqml_model = bigframes.ml.core.BqmlModel( - session, model - ) + new_random_forest_classifier = cls(**kwargs) + new_random_forest_classifier._bqml_model = core.BqmlModel(session, model) return new_random_forest_classifier @property @@ -605,26 +631,32 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> RandomForestClassifier: + X, y = utils.convert_to_dataframe(X, y) + + self._bqml_model = core.create_bqml_model( X, y, transforms=transforms, options=self._bqml_options, ) + return self def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + self, + X: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -635,36 +667,41 @@ def predict( def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], ): """Calculate evaluation metrics of the model. Args: - X: a BigFrames DataFrame as evaluation data. - y: a BigFrames DataFrame as evaluation labels. - - Returns: a BigFrames DataFrame as evaluation result.""" + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation data. + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation labels. + + Returns: + bigframes.dataframe.DataFrame: The DataFrame as evaluation result. + """ if not self._bqml_model: raise RuntimeError("A model must be fitted before score") - if (X is None) != (y is None): - raise ValueError( - "Either both or neither of test_X and test_y must be specified" - ) + X, y = utils.convert_to_dataframe(X, y) + input_data = ( X.join(y, how="outer") if (X is not None) and (y is not None) else None ) return self._bqml_model.evaluate(input_data) def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestClassifier: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + RandomForestClassifier: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 24afaad7f26..b7e0553ecb7 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -12,35 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Forcasting models.""" + from __future__ import annotations -from typing import cast, Dict, List, Optional, TYPE_CHECKING +from typing import cast, Dict, List, Optional, Union from google.cloud import bigquery -if TYPE_CHECKING: - import bigframes - -import bigframes.ml.base -import bigframes.ml.core +import bigframes +from bigframes.ml import base, core, utils +import bigframes.pandas as bpd _PREDICT_OUTPUT_COLUMNS = ["forecast_timestamp", "forecast_value"] -class ARIMAPlus(bigframes.ml.base.TrainablePredictor): +class ARIMAPlus(base.TrainablePredictor): """Time Series ARIMA Plus model.""" def __init__(self): - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod - def _from_bq(session: bigframes.Session, model: bigquery.Model) -> ARIMAPlus: + @classmethod + def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ARIMAPlus: assert model.model_type == "ARIMA_PLUS" kwargs: Dict[str, str | int | bool | float | List[str]] = {} - new_arima_plus = ARIMAPlus(**kwargs) - new_arima_plus._bqml_model = bigframes.ml.core.BqmlModel(session, model) + new_arima_plus = cls(**kwargs) + new_arima_plus._bqml_model = core.BqmlModel(session, model) return new_arima_plus @property @@ -50,74 +50,90 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, ): - """Fit the model to training data + """Fit the model to training data. Args: - X: A dataframe of training timestamp. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A dataframe of training timestamp. + + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + Target values for training. + transforms (Optional[List[str]], default None): + Do not use. Internal param to be deprecated. + Use bigframes.ml.pipeline instead. + + Returns: + ARIMAPlus: Fitted estimator. + """ + X, y = utils.convert_to_dataframe(X, y) - y: Target values for training.""" - self._bqml_model = bigframes.ml.core.create_bqml_time_series_model( + self._bqml_model = core.create_bqml_time_series_model( X, y, transforms=transforms, options=self._bqml_options, ) - def predict(self, X=None) -> bigframes.dataframe.DataFrame: + def predict(self, X=None) -> bpd.DataFrame: """Predict the closest cluster for each sample in X. Args: - X: ignored, to be compatible with other APIs. + X (default None): + ignored, to be compatible with other APIs. + Returns: - The predicted BigQuery DataFrames. Which contains 2 columns - "forecast_timestamp" and "forecast_value". + bigframes.dataframe.DataFrame: The predicted DataFrames. Which + contains 2 columns "forecast_timestamp" and "forecast_value". """ if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, self._bqml_model.forecast()[_PREDICT_OUTPUT_COLUMNS], ) - # Unlike regression models, time series forcasting can only evaluate with unseen data. X and y must be providee. def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, - ) -> bigframes.dataframe.DataFrame: + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: """Calculate evaluation metrics of the model. Args: - X: - A BigQuery DataFrames only contains 1 column as + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame only contains 1 column as evaluation timestamp. The timestamp must be within the horizon of the model, which by default is 1000 data points. - y: - A BigQuery DataFrames only contains 1 column as + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame only contains 1 column as evaluation numeric values. Returns: - A BigQuery DataFrames as evaluation result. + bigframes.dataframe.DataFrame: A DataFrame as evaluation result. """ if not self._bqml_model: raise RuntimeError("A model must be fitted before score") + X, y = utils.convert_to_dataframe(X, y) input_data = X.join(y, how="outer") return self._bqml_model.evaluate(input_data) def to_gbq(self, model_name: str, replace: bool = False) -> ARIMAPlus: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + ARIMAPlus: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index 581ee2b1e24..89078f8267f 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -12,47 +12,68 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Imported models.""" + from __future__ import annotations -from typing import cast, TYPE_CHECKING +from typing import cast, Optional, Union -if TYPE_CHECKING: - import bigframes +from google.cloud import bigquery -import bigframes.ml.base -import bigframes.ml.core +import bigframes +from bigframes.ml import base, core, utils +import bigframes.pandas as bpd -class TensorFlowModel(bigframes.ml.base.Predictor): +class TensorFlowModel(base.Predictor): """Imported TensorFlow model. Args: - session: BQ session to create the model - model_path: GCS path that holds the model files.""" + session (BigQuery Session): + BQ session to create the model + model_path (str): + GCS path that holds the model files.""" - def __init__(self, session: bigframes.Session, model_path: str): + def __init__(self, session: bigframes.Session, model_path: Optional[str] = None): self.session = session self.model_path = model_path - self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + self._bqml_model: Optional[core.BqmlModel] = None def _create_bqml_model(self): options = {"model_type": "TENSORFLOW", "model_path": self.model_path} - return bigframes.ml.core.create_bqml_imported_model( - session=self.session, options=options - ) + return core.create_bqml_imported_model(session=self.session, options=options) + + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> TensorFlowModel: + assert model.model_type == "TENSORFLOW" - def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + tf_model = cls(session=session, model_path=None) + tf_model._bqml_model = core.BqmlModel(session, model) + return tf_model + + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. Args: - X: Input DataFrame, schema is defined by the model. + X (bigframes.dataframe.DataFrame): + Input DataFrame, schema is defined by the model. + + Returns: + bigframes.dataframe.DataFrame: Output DataFrame, schema is defined by the model.""" + + if not self._bqml_model: + if self.model_path is None: + raise ValueError("Model GCS path must be provided.") + self._bqml_model = self._create_bqml_model() + self._bqml_model = cast(core.BqmlModel, self._bqml_model) + + (X,) = utils.convert_to_dataframe(X) - Returns: Output DataFrame, schema is defined by the model.""" df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -61,37 +82,74 @@ def predict( ], ) + def to_gbq(self, model_name: str, replace: bool = False) -> TensorFlowModel: + """Save the model to BigQuery. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + TensorFlowModel: saved model.""" + if not self._bqml_model: + if self.model_path is None: + raise ValueError("Model GCS path must be provided.") + self._bqml_model = self._create_bqml_model() + self._bqml_model = cast(core.BqmlModel, self._bqml_model) + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) -class OnnxModel(bigframes.ml.base.BaseEstimator): + +class ONNXModel(base.Predictor): """Imported Open Neural Network Exchange (ONNX) model. Args: - session: BQ session to create the model - model_path: GCS path that holds the model files.""" + session (BigQuery Session): + BQ session to create the model + model_path (str): + Cloud Storage path that holds the model files.""" - def __init__(self, session: bigframes.Session, model_path: str): + def __init__(self, session: bigframes.Session, model_path: Optional[str] = None): self.session = session self.model_path = model_path - self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + self._bqml_model: Optional[core.BqmlModel] = None def _create_bqml_model(self): options = {"model_type": "ONNX", "model_path": self.model_path} - return bigframes.ml.core.create_bqml_imported_model( - session=self.session, options=options - ) + return core.create_bqml_imported_model(session=self.session, options=options) + + @classmethod + def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ONNXModel: + assert model.model_type == "ONNX" - def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + onnx_model = cls(session=session, model_path=None) + onnx_model._bqml_model = core.BqmlModel(session, model) + return onnx_model + + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. Args: - X: Input DataFrame, schema is defined by the model. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame or Series, schema is defined by the model. + + Returns: + bigframes.dataframe.DataFrame: Output DataFrame, schema is defined by the model.""" + + if not self._bqml_model: + if self.model_path is None: + raise ValueError("Model GCS path must be provided.") + self._bqml_model = self._create_bqml_model() + self._bqml_model = cast(core.BqmlModel, self._bqml_model) + + (X,) = utils.convert_to_dataframe(X) - Returns: Output DataFrame, schema is defined by the model.""" df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -99,3 +157,23 @@ def predict( ] ], ) + + def to_gbq(self, model_name: str, replace: bool = False) -> ONNXModel: + """Save the model to BigQuery. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + ONNXModel: saved model.""" + if not self._bqml_model: + if self.model_path is None: + raise ValueError("Model GCS path must be provided.") + self._bqml_model = self._create_bqml_model() + self._bqml_model = cast(core.BqmlModel, self._bqml_model) + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 2b2ea5c2afc..0b18db93154 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -12,27 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Linear models. This module is styled after Scikit-Learn's linear_model module: -https://scikit-learn.org/stable/modules/linear_model.html""" +"""Linear models. This module is styled after scikit-learn's linear_model module: +https://scikit-learn.org/stable/modules/linear_model.html.""" from __future__ import annotations -from typing import cast, Dict, List, Optional, TYPE_CHECKING +from typing import cast, Dict, List, Optional, Union from google.cloud import bigquery -if TYPE_CHECKING: - import bigframes - -import bigframes.ml.base -import bigframes.ml.core +import bigframes +import bigframes.constants as constants +from bigframes.ml import base, core, utils +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.linear_model._base import third_party.bigframes_vendored.sklearn.linear_model._logistic class LinearRegression( third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = ( third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression.__doc__ @@ -43,10 +42,12 @@ def __init__( fit_intercept=True, ): self.fit_intercept = fit_intercept - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod - def _from_bq(session: bigframes.Session, model: bigquery.Model) -> LinearRegression: + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> LinearRegression: assert model.model_type == "LINEAR_REGRESSION" # TODO(bmil): construct a standard way to extract these properties @@ -57,8 +58,8 @@ def _from_bq(session: bigframes.Session, model: bigquery.Model) -> LinearRegress if "fitIntercept" in last_fitting: kwargs["fit_intercept"] = last_fitting["fitIntercept"] - new_linear_regression = LinearRegression(**kwargs) - new_linear_regression._bqml_model = bigframes.ml.core.BqmlModel(session, model) + new_linear_regression = cls(**kwargs) + new_linear_regression._bqml_model = core.BqmlModel(session, model) return new_linear_regression @property @@ -72,26 +73,29 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> LinearRegression: + X, y = utils.convert_to_dataframe(X, y) + + self._bqml_model = core.create_bqml_model( X, y, transforms=transforms, options=self._bqml_options, ) + return self - def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -102,25 +106,28 @@ def predict( def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, - ) -> bigframes.dataframe.DataFrame: + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before score") - input_data = ( - X.join(y, how="outer") if (X is not None) and (y is not None) else None - ) + X, y = utils.convert_to_dataframe(X, y) + + input_data = X.join(y, how="outer") return self._bqml_model.evaluate(input_data) def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + LinearRegression: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") @@ -130,7 +137,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: class LogisticRegression( third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression, - bigframes.ml.base.TrainablePredictor, + base.TrainablePredictor, ): __doc__ = ( third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression.__doc__ @@ -144,11 +151,11 @@ def __init__( ): self.fit_intercept = fit_intercept self.auto_class_weights = auto_class_weights - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None - @staticmethod + @classmethod def _from_bq( - session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, model: bigquery.Model ) -> LogisticRegression: assert model.model_type == "LOGISTIC_REGRESSION" @@ -166,10 +173,8 @@ def _from_bq( # if "labelClassWeights" in last_fitting: # kwargs["class_weights"] = last_fitting["labelClassWeights"] - new_logistic_regression = LogisticRegression(**kwargs) - new_logistic_regression._bqml_model = bigframes.ml.core.BqmlModel( - session, model - ) + new_logistic_regression = cls(**kwargs) + new_logistic_regression._bqml_model = core.BqmlModel(session, model) return new_logistic_regression @property @@ -186,26 +191,32 @@ def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: def fit( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, - ): - self._bqml_model = bigframes.ml.core.create_bqml_model( + ) -> LogisticRegression: + X, y = utils.convert_to_dataframe(X, y) + + self._bqml_model = core.create_bqml_model( X, y, transforms=transforms, options=self._bqml_options, ) + return self def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + self, + X: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.predict(X) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[ [ cast(str, field.name) @@ -216,32 +227,37 @@ def predict( def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, - ) -> bigframes.dataframe.DataFrame: + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before score") - input_data = ( - X.join(y, how="outer") if (X is not None) and (y is not None) else None - ) + X, y = utils.convert_to_dataframe(X, y) + + input_data = X.join(y, how="outer") return self._bqml_model.evaluate(input_data) def to_gbq(self, model_name: str, replace: bool = False) -> LogisticRegression: - """Save the model to Google Cloud BigQuey. + """Save the model to BigQuery. Args: - model_name: the name of the model. - replace: whether to replace if the model already exists. Default to False. + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: + LogisticRegression: saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") # TODO(ashleyxu): b/285162045 support auto_class_weights once the API is # fixed and enable the tests. if self.auto_class_weights is True: - raise NotImplementedError("auto_class_weight is not supported yet.") + raise NotImplementedError( + f"auto_class_weight is not supported yet. {constants.FEEDBACK_LINK}" + ) new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index ef4f28f1f7d..def97b56ff0 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -12,80 +12,92 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""LLM models.""" + from __future__ import annotations -from typing import cast +from typing import cast, Union import bigframes +import bigframes.constants as constants from bigframes.core import blocks -import bigframes.ml.base -import bigframes.ml.core +from bigframes.ml import base, core, utils +import bigframes.pandas as bpd _REMOTE_TEXT_GENERATOR_MODEL_CODE = "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1" _TEXT_GENERATE_RESULT_COLUMN = "ml_generate_text_llm_result" _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE = "CLOUD_AI_TEXT_EMBEDDING_MODEL_V1" -_EMBED_TEXT_RESULT_COLUMN = "ml_embed_text_embedding" +_EMBED_TEXT_RESULT_COLUMN = "text_embedding" -class PaLM2TextGenerator(bigframes.ml.base.Predictor): +class PaLM2TextGenerator(base.Predictor): """PaLM2 text generator LLM model. Args: - session: BQ session to create the model - connection_name: connection to connect with remote service. str of the format ..""" + session (BigQuery Session): + BQ session to create the model + connection_name (str): + connection to connect with remote service. str of the format ..""" def __init__(self, session: bigframes.Session, connection_name: str): self.session = session self.connection_name = connection_name - self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + self._bqml_model: core.BqmlModel = self._create_bqml_model() def _create_bqml_model(self): options = { "remote_service_type": _REMOTE_TEXT_GENERATOR_MODEL_CODE, } - return bigframes.ml.core.create_bqml_remote_model( + return core.create_bqml_remote_model( session=self.session, connection_name=self.connection_name, options=options ) def predict( self, - X: bigframes.dataframe.DataFrame, + X: Union[bpd.DataFrame, bpd.Series], temperature: float = 0.0, max_output_tokens: int = 128, top_k: int = 40, top_p: float = 0.95, - ) -> bigframes.dataframe.DataFrame: + ) -> bpd.DataFrame: """Predict the result from input DataFrame. Args: - X: Input DataFrame, which needs to contain a column with name "prompt". Only the column will be used as input. Prompts can include preamble, questions, suggestions, instructions, or examples. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame or Series, which needs to contain a column with name "prompt". Only the column will be used as input. + Prompts can include preamble, questions, suggestions, instructions, or examples. - temperature: The temperature is used for sampling during the response generation, which occurs when topP and topK are applied. + temperature (float, default 0.0): + The temperature is used for sampling during the response generation, which occurs when topP and topK are applied. Temperature controls the degree of randomness in token selection. Lower temperatures are good for prompts that expect a true or correct response, while higher temperatures can lead to more diverse or unexpected results. A temperature of 0 is deterministic: the highest probability token is always selected. For most use cases, try starting with a temperature of 0.2. Default 0. - max_output_tokens: Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. + max_output_tokens (int, default 128): + Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. A token may be smaller than a word. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. Default 128. - top_k: Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens + top_k (int, default 40): + Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model’s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature). For each token selection step, the top K tokens with the highest probabilities are sampled. Then tokens are further filtered based on topP with the final token selected using temperature sampling. Specify a lower value for less random responses and a higher value for more random responses. Default 40. - top_p: Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value. + top_p (float, default 0.95):: + Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value. For example, if tokens A, B, and C have a probability of 0.3, 0.2, and 0.1 and the top-p value is 0.5, then the model will select either A or B as the next token (using temperature) and not consider C at all. Specify a lower value for less random responses and a higher value for more random responses. Default 0.95. - Returns: Output DataFrame with only 1 column as the output text results.""" + Returns: + bigframes.dataframe.DataFrame: Output DataFrame with only 1 column as the output text results.""" # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models if temperature < 0.0 or temperature > 1.0: @@ -98,8 +110,13 @@ def predict( raise ValueError(f"top_k must be [1, 40], but is {top_k}.") if top_p < 0.0 or top_p > 1.0: raise ValueError(f"top_p must be [0.0, 1.0], but is {top_p}.") + + (X,) = utils.convert_to_dataframe(X) + if len(X.columns) != 1: - raise ValueError("Only support one column as input.") + raise ValueError( + f"Only support one column as input. {constants.FEEDBACK_LINK}" + ) # BQML identified the column by name col_label = cast(blocks.Label, X.columns[0]) @@ -114,45 +131,52 @@ def predict( } df = self._bqml_model.generate_text(X, options) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[[_TEXT_GENERATE_RESULT_COLUMN]], ) -class PaLM2EmbeddingGenerator(bigframes.ml.base.Predictor): - """PaLM2 embedding generator LLM model. +class PaLM2TextEmbeddingGenerator(base.Predictor): + """PaLM2 text embedding generator LLM model. Args: - session: BQ session to create the model - connection_name: connection to connect with remote service. str of the format ..""" + session (BigQuery Session): + BQ session to create the model + connection_name (str): + connection to connect with remote service. str of the format ..""" def __init__(self, session: bigframes.Session, connection_name: str): self.session = session self.connection_name = connection_name - self._bqml_model: bigframes.ml.core.BqmlModel = self._create_bqml_model() + self._bqml_model: core.BqmlModel = self._create_bqml_model() def _create_bqml_model(self): options = { "remote_service_type": _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE, } - return bigframes.ml.core.create_bqml_remote_model( + return core.create_bqml_remote_model( session=self.session, connection_name=self.connection_name, options=options ) - def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. Args: - X: Input DataFrame, which needs to contain a column with name "content". Only the column will be used as input. Content can include preamble, questions, suggestions, instructions, or examples. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame, which needs to contain a column with name "content". Only the column will be used as input. Content can include preamble, questions, suggestions, instructions, or examples. - Returns: Output DataFrame with only 1 column as the output embedding results.""" + Returns: + bigframes.dataframe.DataFrame: Output DataFrame with only 1 column as the output embedding results + """ # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models + (X,) = utils.convert_to_dataframe(X) + if len(X.columns) != 1: - raise ValueError("Only support one column as input.") + raise ValueError( + f"Only support one column as input. {constants.FEEDBACK_LINK}" + ) # BQML identified the column by name col_label = cast(blocks.Label, X.columns[0]) @@ -161,8 +185,8 @@ def predict( options = { "flatten_json_output": True, } - df = self._bqml_model.embed_text(X, options) + df = self._bqml_model.generate_text_embedding(X, options) return cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[[_EMBED_TEXT_RESULT_COLUMN]], ) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index cc43b47698e..805747c49ba 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -14,61 +14,81 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from types import MappingProxyType +from typing import Union from google.cloud import bigquery -if TYPE_CHECKING: - import bigframes +import bigframes +import bigframes.constants as constants +from bigframes.ml import ( + cluster, + decomposition, + ensemble, + forecasting, + imported, + linear_model, + pipeline, +) -import bigframes.ml.cluster -import bigframes.ml.decomposition -import bigframes.ml.ensemble -import bigframes.ml.forecasting -import bigframes.ml.linear_model +_BQML_MODEL_TYPE_MAPPING = MappingProxyType( + { + "LINEAR_REGRESSION": linear_model.LinearRegression, + "LOGISTIC_REGRESSION": linear_model.LogisticRegression, + "KMEANS": cluster.KMeans, + "PCA": decomposition.PCA, + "BOOSTED_TREE_REGRESSOR": ensemble.XGBRegressor, + "BOOSTED_TREE_CLASSIFIER": ensemble.XGBClassifier, + "ARIMA_PLUS": forecasting.ARIMAPlus, + "RANDOM_FOREST_REGRESSOR": ensemble.RandomForestRegressor, + "RANDOM_FOREST_CLASSIFIER": ensemble.RandomForestClassifier, + "TENSORFLOW": imported.TensorFlowModel, + "ONNX": imported.ONNXModel, + } +) def from_bq( - session: bigframes.Session, model: bigquery.Model + session: bigframes.Session, bq_model: bigquery.Model ) -> Union[ - bigframes.ml.decomposition.PCA, - bigframes.ml.cluster.KMeans, - bigframes.ml.linear_model.LinearRegression, - bigframes.ml.linear_model.LogisticRegression, - bigframes.ml.ensemble.XGBRegressor, - bigframes.ml.ensemble.XGBClassifier, - bigframes.ml.forecasting.ARIMAPlus, - bigframes.ml.ensemble.RandomForestRegressor, - bigframes.ml.ensemble.RandomForestClassifier, + decomposition.PCA, + cluster.KMeans, + linear_model.LinearRegression, + linear_model.LogisticRegression, + ensemble.XGBRegressor, + ensemble.XGBClassifier, + forecasting.ARIMAPlus, + ensemble.RandomForestRegressor, + ensemble.RandomForestClassifier, + imported.TensorFlowModel, + imported.ONNXModel, + pipeline.Pipeline, ]: """Load a BQML model to BigQuery DataFrames ML. Args: session: a BigQuery DataFrames session. - model: a BigQuery model. + bq_model: a BigQuery model. Returns: A BigQuery DataFrames ML model object. """ - if model.model_type == "LINEAR_REGRESSION": - return bigframes.ml.linear_model.LinearRegression._from_bq(session, model) - elif model.model_type == "KMEANS": - return bigframes.ml.cluster.KMeans._from_bq(session, model) - elif model.model_type == "PCA": - return bigframes.ml.decomposition.PCA._from_bq(session, model) - elif model.model_type == "LOGISTIC_REGRESSION": - return bigframes.ml.linear_model.LogisticRegression._from_bq(session, model) - elif model.model_type == "BOOSTED_TREE_REGRESSOR": - return bigframes.ml.ensemble.XGBRegressor._from_bq(session, model) - elif model.model_type == "BOOSTED_TREE_CLASSIFIER": - return bigframes.ml.ensemble.XGBClassifier._from_bq(session, model) - elif model.model_type == "ARIMA_PLUS": - return bigframes.ml.forecasting.ARIMAPlus._from_bq(session, model) - elif model.model_type == "RANDOM_FOREST_REGRESSOR": - return bigframes.ml.ensemble.RandomForestRegressor._from_bq(session, model) - elif model.model_type == "RANDOM_FOREST_CLASSIFIER": - return bigframes.ml.ensemble.RandomForestClassifier._from_bq(session, model) - else: - raise NotImplementedError( - f"Model type {model.model_type} is not yet supported by BigQuery DataFrames." + if _is_bq_model_pipeline(bq_model): + return pipeline.Pipeline._from_bq(session, bq_model) + + return _model_from_bq(session, bq_model) + + +def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): + if bq_model.model_type in _BQML_MODEL_TYPE_MAPPING: + return _BQML_MODEL_TYPE_MAPPING[bq_model.model_type]._from_bq( # type: ignore + session=session, model=bq_model ) + + raise NotImplementedError( + f"Model type {bq_model.model_type} is not yet supported by BigQuery DataFrames. {constants.FEEDBACK_LINK}" + ) + + +def _is_bq_model_pipeline(bq_model: bigquery.Model) -> bool: + return "transformColumns" in bq_model._properties diff --git a/bigframes/ml/metrics.py b/bigframes/ml/metrics.py index 861e1f02d10..3bcb621f745 100644 --- a/bigframes/ml/metrics.py +++ b/bigframes/ml/metrics.py @@ -13,17 +13,18 @@ # limitations under the License. """Metrics functions for evaluating models. This module is styled after -Scikit-Learn's metrics module: https://scikit-learn.org/stable/modules/metrics.html""" +Scikit-Learn's metrics module: https://scikit-learn.org/stable/modules/metrics.html.""" import inspect import typing -from typing import Tuple +from typing import Tuple, Union import numpy as np import pandas as pd import sklearn.metrics as sklearn_metrics # type: ignore -import bigframes.core.blocks as blocks +import bigframes.constants as constants +from bigframes.ml import utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.metrics._classification as vendored_mertics_classification import third_party.bigframes_vendored.sklearn.metrics._ranking as vendored_mertics_ranking @@ -31,33 +32,20 @@ def r2_score( - y_true: bpd.DataFrame, - y_pred: bpd.DataFrame, + y_true: Union[bpd.DataFrame, bpd.Series], + y_pred: Union[bpd.DataFrame, bpd.Series], force_finite=True, ) -> float: - # TODO(bmil): support multioutput - if len(y_true.columns) > 1 or len(y_pred.columns) > 1: - raise NotImplementedError( - "Only one labels column, one predictions column is supported" - ) - - y_true_series = typing.cast( - bpd.Series, y_true[typing.cast(str, y_true.columns.tolist()[0])] - ) - y_pred_series = typing.cast( - bpd.Series, y_pred[typing.cast(str, y_pred.columns.tolist()[0])] - ) + y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) # total sum of squares # (dataframe, scalar) binops - # TODO(bmil): remove multiply by self when bigframes supports pow() # TODO(tbergeron): These stats are eagerly evaluated. Move to lazy representation once scalar subqueries supported. delta_from_mean = y_true_series - y_true_series.mean() ss_total = (delta_from_mean * delta_from_mean).sum() # residual sum of squares # (scalar, scalar) binops - # TODO(bmil): remove multiply by self when bigframes supports pow() delta_from_pred = y_true_series - y_pred_series ss_res = (delta_from_pred * delta_from_pred).sum() @@ -71,22 +59,12 @@ def r2_score( def accuracy_score( - y_true: bpd.DataFrame, - y_pred: bpd.DataFrame, + y_true: Union[bpd.DataFrame, bpd.Series], + y_pred: Union[bpd.DataFrame, bpd.Series], normalize=True, ) -> float: # TODO(ashleyxu): support sample_weight as the parameter - if len(y_true.columns) != 1 or len(y_pred.columns) != 1: - raise NotImplementedError( - "Only one labels column, one predictions column is supported" - ) - - y_true_series = typing.cast( - bpd.Series, y_true[typing.cast(str, y_true.columns.tolist()[0])] - ) - y_pred_series = typing.cast( - bpd.Series, y_pred[typing.cast(str, y_pred.columns.tolist()[0])] - ) + y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) # Compute accuracy for each possible representation # TODO(ashleyxu): add multilabel classification support where y_type @@ -103,73 +81,86 @@ def accuracy_score( def roc_curve( - y_true: bpd.DataFrame, - y_score: bpd.DataFrame, + y_true: Union[bpd.DataFrame, bpd.Series], + y_score: Union[bpd.DataFrame, bpd.Series], drop_intermediate: bool = True, ) -> Tuple[bpd.Series, bpd.Series, bpd.Series]: # TODO(bmil): Add multi-class support # TODO(bmil): Add multi-label support - if len(y_true.columns) > 1 or len(y_score.columns) > 1: - raise NotImplementedError("Only binary classification is supported") # TODO(bmil): Implement drop_intermediate if drop_intermediate: - raise NotImplementedError("drop_intermediate is not yet implemented") + raise NotImplementedError( + f"drop_intermediate is not yet implemented. {constants.FEEDBACK_LINK}" + ) - # TODO(bmil): remove this once bigframes supports the necessary operations - session = y_true._block.expr._session - pd_y_true = y_true.to_pandas() - pd_y_score = y_score.to_pandas() + y_true_series, y_score_series = utils.convert_to_series(y_true, y_score) + + session = y_true_series._block.expr._session # We operate on rows, so, remove the index if there is one # TODO(bmil): check that the indexes are equivalent before removing - pd_y_true = pd_y_true.reset_index(drop=True) - pd_y_score = pd_y_score.reset_index(drop=True) - pd_df = pd.DataFrame( + y_true_series = typing.cast(bpd.Series, y_true_series.reset_index(drop=True)) + y_score_series = typing.cast(bpd.Series, y_score_series.reset_index(drop=True)) + + df = bpd.DataFrame( { - "y_true": pd_y_true[pd_y_true.columns[0]], - "y_score": pd_y_score[pd_y_score.columns[0]], + "y_true": y_true_series, + "y_score": y_score_series, } ) - total_positives = pd_df.y_true.sum() - total_negatives = len(pd_df) - total_positives + total_positives = y_true_series.sum() + total_negatives = y_true_series.count() - total_positives - pd_df = pd_df.sort_values(by="y_score", ascending=False) - pd_df["cum_tp"] = pd_df.y_true.cumsum() - pd_df["cum_fp"] = (~pd_df.y_true.astype(bool)).cumsum() + df = df.sort_values(by="y_score", ascending=False) + df["cum_tp"] = df["y_true"].cumsum() + # have to astype("Int64") as not supported boolean cumsum yet. + df["cum_fp"] = ( + (~typing.cast(bpd.Series, df["y_true"].astype("boolean"))) + .astype("Int64") + .cumsum() + ) # produce just one data point per y_score - pd_df = pd_df.groupby("y_score", as_index=False).last() - pd_df = pd_df.sort_values(by="y_score", ascending=False) + df = df.drop_duplicates(subset="y_score", keep="last") + df = df.sort_values(by="y_score", ascending=False) - pd_df["tpr"] = pd_df.cum_tp / total_positives - pd_df["fpr"] = pd_df.cum_fp / total_negatives - pd_df["thresholds"] = pd_df.y_score + df["tpr"] = typing.cast(bpd.Series, df["cum_tp"]) / total_positives + df["fpr"] = typing.cast(bpd.Series, df["cum_fp"]) / total_negatives + df["thresholds"] = typing.cast(bpd.Series, df["y_score"].astype("Float64")) # sklearn includes an extra datapoint for the origin with threshold np.inf - pd_origin = pd.DataFrame({"tpr": [0.0], "fpr": [0.0], "thresholds": np.inf}) - pd_df = pd.concat([pd_origin, pd_df]) + # having problems with concating inline + df_origin = session.read_pandas( + pd.DataFrame({"tpr": [0.0], "fpr": [0.0], "thresholds": np.inf}) + ) + df = typing.cast(bpd.DataFrame, bpd.concat([df_origin, df], ignore_index=True)) + df = df.reset_index(drop=True) - df = session.read_pandas(pd_df) - return df.fpr, df.tpr, df.thresholds + return ( + typing.cast(bpd.Series, df["fpr"]), + typing.cast(bpd.Series, df["tpr"]), + typing.cast(bpd.Series, df["thresholds"]), + ) roc_curve.__doc__ = inspect.getdoc(vendored_mertics_ranking.roc_curve) -def roc_auc_score(y_true: bpd.DataFrame, y_score: bpd.DataFrame) -> float: +def roc_auc_score( + y_true: Union[bpd.DataFrame, bpd.Series], y_score: Union[bpd.DataFrame, bpd.Series] +) -> float: # TODO(bmil): Add multi-class support # TODO(bmil): Add multi-label support - if len(y_true.columns) > 1 or len(y_score.columns) > 1: - raise NotImplementedError("Only binary classification is supported") + y_true_series, y_score_series = utils.convert_to_series(y_true, y_score) - fpr, tpr, _ = roc_curve(y_true, y_score, drop_intermediate=False) + fpr, tpr, _ = roc_curve(y_true_series, y_score_series, drop_intermediate=False) # TODO(bmil): remove this once bigframes supports the necessary operations - pd_fpr = fpr.compute() - pd_tpr = tpr.compute() + pd_fpr = fpr.to_pandas() + pd_tpr = tpr.to_pandas() # Use the trapezoid rule to compute the area under the ROC curve width_diff = pd_fpr.diff().iloc[1:].reset_index(drop=True) @@ -181,14 +172,13 @@ def roc_auc_score(y_true: bpd.DataFrame, y_score: bpd.DataFrame) -> float: def auc( - x: bpd.DataFrame, - y: bpd.DataFrame, + x: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], ) -> float: - if len(x.columns) != 1 or len(y.columns) != 1: - raise ValueError("Only 1-D data structure is supported") + x_series, y_series = utils.convert_to_series(x, y) # TODO(b/286410053) Support ML exceptions and error handling. - auc = sklearn_metrics.auc(x.to_pandas(), y.to_pandas()) + auc = sklearn_metrics.auc(x_series.to_pandas(), y_series.to_pandas()) return auc @@ -196,30 +186,24 @@ def auc( def confusion_matrix( - y_true: bpd.DataFrame, - y_pred: bpd.DataFrame, + y_true: Union[bpd.DataFrame, bpd.Series], + y_pred: Union[bpd.DataFrame, bpd.Series], ) -> pd.DataFrame: # TODO(ashleyxu): support labels and sample_weight parameters - # TODO(ashleyxu): support bpd.Series as input type - if len(y_true.columns) != 1 or len(y_pred.columns) != 1: - raise NotImplementedError( - "Only one labels column, one predictions column is supported" - ) + y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) - y_true_column = typing.cast(blocks.Label, y_true.columns[0]) - y_pred_series = typing.cast( - bpd.Series, - y_pred[typing.cast(blocks.Label, y_pred.columns.tolist()[0])], - ) - confusion_df = y_true.assign(y_pred=y_pred_series) + y_true_series = y_true_series.rename("y_true") + confusion_df = y_true_series.to_frame().assign(y_pred=y_pred_series) confusion_df = confusion_df.assign(dummy=0) groupby_count = ( - confusion_df.groupby(by=[y_true_column, "y_pred"], as_index=False) + confusion_df.groupby(by=["y_true", "y_pred"], as_index=False) .count() .to_pandas() ) - unique_values = sorted(set(groupby_count.y_true).union(set(groupby_count.y_pred))) + unique_values = sorted( + set(groupby_count["y_true"]).union(set(groupby_count["y_pred"])) + ) confusion_matrix = pd.DataFrame( 0, index=pd.Index(unique_values), columns=pd.Index(unique_values), dtype=int @@ -241,28 +225,17 @@ def confusion_matrix( def recall_score( - y_true: bpd.DataFrame, - y_pred: bpd.DataFrame, + y_true: Union[bpd.DataFrame, bpd.Series], + y_pred: Union[bpd.DataFrame, bpd.Series], average: str = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" - # TODO(ashleyxu): support bpd.Series as input type - if len(y_true.columns) != 1 or len(y_pred.columns) != 1: + if average is not None: raise NotImplementedError( - "Only one labels column, one predictions column is supported" + f"Only average=None is supported. {constants.FEEDBACK_LINK}" ) - if average is not None: - raise NotImplementedError("Only average=None is supported") - - y_true_series = typing.cast( - bpd.Series, - y_true[typing.cast(blocks.Label, y_true.columns.tolist()[0])], - ) - y_pred_series = typing.cast( - bpd.Series, - y_pred[typing.cast(blocks.Label, y_pred.columns.tolist()[0])], - ) + y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) is_accurate = y_true_series == y_pred_series unique_labels = ( @@ -275,7 +248,7 @@ def recall_score( recall = ( is_accurate.groupby(y_true_series).sum() / is_accurate.groupby(y_true_series).count() - ).compute() + ).to_pandas() recall_score = pd.Series(0, index=index) for i in recall_score.index: @@ -288,28 +261,17 @@ def recall_score( def precision_score( - y_true: bpd.DataFrame, - y_pred: bpd.DataFrame, + y_true: Union[bpd.DataFrame, bpd.Series], + y_pred: Union[bpd.DataFrame, bpd.Series], average: str = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" - # TODO(ashleyxu): support bpd.Series as input type - if len(y_true.columns) != 1 or len(y_pred.columns) != 1: + if average is not None: raise NotImplementedError( - "Only one labels column, one predictions column is supported" + f"Only average=None is supported. {constants.FEEDBACK_LINK}" ) - if average is not None: - raise NotImplementedError("Only average=None is supported") - - y_true_series = typing.cast( - bpd.Series, - y_true[typing.cast(blocks.Label, y_true.columns.tolist()[0])], - ) - y_pred_series = typing.cast( - bpd.Series, - y_pred[typing.cast(blocks.Label, y_pred.columns.tolist()[0])], - ) + y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) is_accurate = y_true_series == y_pred_series unique_labels = ( @@ -322,7 +284,7 @@ def precision_score( precision = ( is_accurate.groupby(y_pred_series).sum() / is_accurate.groupby(y_pred_series).count() - ).compute() + ).to_pandas() precision_score = pd.Series(0, index=index) for i in precision.index: @@ -337,22 +299,20 @@ def precision_score( def f1_score( - y_true: bpd.DataFrame, - y_pred: bpd.DataFrame, + y_true: Union[bpd.DataFrame, bpd.Series], + y_pred: Union[bpd.DataFrame, bpd.Series], average: str = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" - # TODO(ashleyxu): support bpd.Series as input type - if len(y_true.columns) != 1 or len(y_pred.columns) != 1: - raise NotImplementedError( - "Only one labels column, one predictions column is supported" - ) + y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) if average is not None: - raise NotImplementedError("Only average=None is supported") + raise NotImplementedError( + f"Only average=None is supported. {constants.FEEDBACK_LINK}" + ) - recall = recall_score(y_true, y_pred, average=None) - precision = precision_score(y_true, y_pred, average=None) + recall = recall_score(y_true_series, y_pred_series, average=None) + precision = precision_score(y_true_series, y_pred_series, average=None) f1_score = pd.Series(0, index=recall.index) for index in recall.index: diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index 73a2fd76a9a..110cbcf4934 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -14,40 +14,40 @@ """Functions for test/train split and model tuning. This module is styled after Scikit-Learn's model_selection module: -https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection""" +https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection.""" from typing import List, Union -import bigframes -import bigframes.dataframe +from bigframes.ml import utils +import bigframes.pandas as bpd def train_test_split( - *dataframes: bigframes.dataframe.DataFrame, + *arrays: Union[bpd.DataFrame, bpd.Series], test_size: Union[float, None] = None, train_size: Union[float, None] = None, random_state: Union[int, None] = None, -) -> List[bigframes.dataframe.DataFrame]: - """Splits dataframes into random train and test subsets +) -> List[Union[bpd.DataFrame, bpd.Series]]: + """Splits dataframes or series into random train and test subsets. Args: - *dataframes: - A sequence of BigQuery DataFrames that can be joined on + *arrays (bigframes.dataframe.DataFrame or bigframes.series.Series): + A sequence of BigQuery DataFrames or Series that can be joined on their indexes - test_size: + test_size (default None): The proportion of the dataset to include in the test split. If None, this will default to the complement of train_size. If both are none, it will be set to 0.25. - train_size: + train_size (default None): The proportion of the dataset to include in the train split. If None, this will default to the complement of test_size. - random_state: + random_state (default None): A seed to use for randomly choosing the rows of the split. If not set, a random split will be generated each time. Returns: - A list of BigQuery DataFrames. + List[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]]: A list of BigQuery DataFrames or Series. """ # TODO(garrettwu): Scikit-Learn throws an error when the dataframes don't have the same @@ -73,14 +73,22 @@ def train_test_split( f"The sum of train_size and test_size exceeds 1.0. train_size: {train_size}. test_size: {test_size}" ) - results = dataframes[0]._split( - fracs=(train_size, test_size), random_state=random_state - ) - train_index = results[0].index - test_index = results[1].index + dfs = list(utils.convert_to_dataframe(*arrays)) - results += [ - df.loc[index] for df in dataframes[1:] for index in (train_index, test_index) + split_dfs = dfs[0]._split(fracs=(train_size, test_size), random_state=random_state) + train_index = split_dfs[0].index + test_index = split_dfs[1].index + + split_dfs += [ + df.loc[index] for df in dfs[1:] for index in (train_index, test_index) ] + # convert back to Series. + results: List[Union[bpd.DataFrame, bpd.Series]] = [] + for i, array in enumerate(arrays): + if isinstance(array, bpd.Series): + results += utils.convert_to_series(split_dfs[2 * i], split_dfs[2 * i + 1]) + else: + results += (split_dfs[2 * i], split_dfs[2 * i + 1]) + return results diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 022ba0148d7..bfd03925265 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -13,15 +13,19 @@ # limitations under the License. """For composing estimators together. This module is styled after Scikit-Learn's -pipeline module: https://scikit-learn.org/stable/modules/pipeline.html""" +pipeline module: https://scikit-learn.org/stable/modules/pipeline.html.""" from __future__ import annotations -from typing import List, Optional, Tuple +from typing import cast, List, Optional, Tuple, Union + +from google.cloud import bigquery import bigframes -from bigframes.ml import base, cluster, compose, decomposition, preprocessing +import bigframes.constants as constants +from bigframes.ml import base, compose, loader, preprocessing, utils +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.pipeline @@ -36,7 +40,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): if len(steps) != 2: raise NotImplementedError( - "Currently only two step (transform, estimator) pipelines are supported" + f"Currently only two step (transform, estimator) pipelines are supported. {constants.FEEDBACK_LINK}" ) transform, estimator = steps[0][1], steps[1][1] @@ -51,7 +55,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): self._transform = transform else: raise NotImplementedError( - f"Transform {transform} is not yet supported by Pipeline" + f"Transform {transform} is not yet supported by Pipeline. {constants.FEEDBACK_LINK}" ) if not isinstance( @@ -59,44 +63,134 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): base.TrainablePredictor, ): raise NotImplementedError( - f"Estimator {estimator} is not supported by Pipeline" + f"Estimator {estimator} is not supported by Pipeline. {constants.FEEDBACK_LINK}" ) self._transform = transform self._estimator = estimator + @classmethod + def _from_bq(cls, session: bigframes.Session, bq_model: bigquery.Model) -> Pipeline: + col_transformer = _extract_as_column_transformer(bq_model) + transform = _merge_column_transformer(bq_model, col_transformer) + + estimator = loader._model_from_bq(session, bq_model) + return cls([("transform", transform), ("estimator", estimator)]) + def fit( self, - X: bigframes.dataframe.DataFrame, - y: Optional[bigframes.dataframe.DataFrame] = None, - ): + X: Union[bpd.DataFrame, bpd.Series], + y: Optional[Union[bpd.DataFrame, bpd.Series]] = None, + ) -> Pipeline: + (X,) = utils.convert_to_dataframe(X) + compiled_transforms = self._transform._compile_to_sql(X.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] if y is not None: # If labels columns are present, they should pass through un-transformed + (y,) = utils.convert_to_dataframe(y) transform_sqls.extend(y.columns.tolist()) self._estimator.fit(X=X, y=y, transforms=transform_sqls) + return self - def predict( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: return self._estimator.predict(X) def score( self, - X: bigframes.dataframe.DataFrame, - y: bigframes.dataframe.DataFrame, - ): - if isinstance(self._estimator, (cluster.KMeans, decomposition.PCA)): - raise NotImplementedError("KMeans/PCA haven't supported score method.") - - # TODO(b/289280565): remove type ignore after updating KMeans and PCA - return self._estimator.score(X=X, y=y) # type: ignore - - def to_gbq(self, model_name: str, replace: bool = False): - self._estimator.to_gbq(model_name, replace) + X: Union[bpd.DataFrame, bpd.Series], + y: Optional[Union[bpd.DataFrame, bpd.Series]] = None, + ) -> bpd.DataFrame: + (X,) = utils.convert_to_dataframe(X) + if y is not None: + (y,) = utils.convert_to_dataframe(y) + + return self._estimator.score(X=X, y=y) + + def to_gbq(self, model_name: str, replace: bool = False) -> Pipeline: + """Save the pipeline to BigQuery. + + Args: + model_name (str): + the name of the model(pipeline). + replace (bool, default False): + whether to replace if the model(pipeline) already exists. Default to False. + + Returns: + Pipeline: saved model(pipeline).""" + if not self._estimator._bqml_model: + raise RuntimeError("A model must be fitted before it can be saved") + + new_model = self._estimator._bqml_model.copy(model_name, replace) + + return new_model.session.read_gbq_model(model_name) + + +def _extract_as_column_transformer( + bq_model: bigquery.Model, +) -> compose.ColumnTransformer: + """Extract transformers as ColumnTransformer obj from a BQ Model.""" + assert "transformColumns" in bq_model._properties + + transformers: List[ + Tuple[ + str, + Union[preprocessing.OneHotEncoder, preprocessing.StandardScaler], + Union[str, List[str]], + ] + ] = [] + for transform_col in bq_model._properties["transformColumns"]: + # pass the columns that are not transformed + if "transformSql" not in transform_col: + continue + + transform_sql: str = cast(dict, transform_col)["transformSql"] + if transform_sql.startswith("ML.STANDARD_SCALER"): + transformers.append( + ( + "standard_scaler", + *preprocessing.StandardScaler._parse_from_sql(transform_sql), + ) + ) + elif transform_sql.startswith("ML.ONE_HOT_ENCODER"): + transformers.append( + ( + "ont_hot_encoder", + *preprocessing.OneHotEncoder._parse_from_sql(transform_sql), + ) + ) + else: + raise NotImplementedError( + f"Unsupported transformer type. {constants.FEEDBACK_LINK}" + ) - # TODO: should instead load from GBQ, but loading pipelines is not implemented yet - return self + return compose.ColumnTransformer(transformers=transformers) + + +def _merge_column_transformer( + bq_model: bigquery.Model, column_transformer: compose.ColumnTransformer +) -> Union[ + compose.ColumnTransformer, + preprocessing.StandardScaler, + preprocessing.OneHotEncoder, +]: + """Try to merge the column transformer to a simple transformer.""" + transformers = column_transformer.transformers_ + + assert len(transformers) > 0 + _, transformer_0, column_0 = transformers[0] + columns = [column_0] + for _, transformer, column in transformers[1:]: + # all transformers are the same + if transformer != transformer_0: + return column_transformer + columns.append(column) + # all feature columns are transformed + if sorted( + [cast(str, feature_column.name) for feature_column in bq_model.feature_columns] + ) == sorted(columns): + return transformer_0 + + return column_transformer diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 59d2cc2ae9e..500a9fcb244 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -13,29 +13,35 @@ # limitations under the License. """Transformers that prepare data for other estimators. This module is styled after -Scikit-Learn's preprocessing module: https://scikit-learn.org/stable/modules/preprocessing.html""" +Scikit-Learn's preprocessing module: https://scikit-learn.org/stable/modules/preprocessing.html.""" +from __future__ import annotations import typing -from typing import List, Optional, Tuple +from typing import Any, cast, List, Literal, Optional, Tuple, Union -import bigframes -import bigframes.ml -import bigframes.ml.sql +from bigframes.ml import base, core +from bigframes.ml import sql as ml_sql +from bigframes.ml import utils +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.preprocessing._data import third_party.bigframes_vendored.sklearn.preprocessing._encoder class StandardScaler( third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler, - bigframes.ml.base.BaseEstimator, + base.BaseEstimator, ): __doc__ = ( third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler.__doc__ ) def __init__(self): - self._bqml_model: Optional[bigframes.ml.core.BqmlModel] = None + self._bqml_model: Optional[core.BqmlModel] = None + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return type(other) is StandardScaler and self._bqml_model == other._bqml_model def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in @@ -47,20 +53,35 @@ def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: Returns: a list of tuples of (sql_expression, output_name)""" return [ ( - bigframes.ml.sql.ml_standard_scaler(column, f"scaled_{column}"), + ml_sql.ml_standard_scaler(column, f"scaled_{column}"), f"scaled_{column}", ) for column in columns ] + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[StandardScaler, str]: + """Parse SQL to tuple(StandardScaler, column_label). + + Args: + sql: SQL string of format "ML.STANDARD_SCALER({col_label}) OVER()" + + Returns: + tuple(StandardScaler, column_label)""" + col_label = sql[sql.find("(") + 1 : sql.find(")")] + return cls(), col_label + def fit( self, - X: bigframes.dataframe.DataFrame, - ): + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> StandardScaler: + (X,) = utils.convert_to_dataframe(X) + compiled_transforms = self._compile_to_sql(X.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - self._bqml_model = bigframes.ml.core.create_bqml_model( + self._bqml_model = core.create_bqml_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, @@ -68,57 +89,124 @@ def fit( # The schema of TRANSFORM output is not available in the model API, so save it during fitting self._output_names = [name for _, name in compiled_transforms] + return self - def transform( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("Must be fitted before transform") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.transform(X) return typing.cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[self._output_names], ) class OneHotEncoder( third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, - bigframes.ml.base.BaseEstimator, + base.BaseEstimator, ): + # BQML max value https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder#syntax + TOP_K_DEFAULT = 1000000 + FREQUENCY_THRESHOLD_DEFAULT = 0 + __doc__ = ( third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder.__doc__ ) # All estimators must implement __init__ to document their parameters, even # if they don't have any - def __init__(self): - pass + def __init__( + self, + drop: Optional[Literal["most_frequent"]] = None, + min_frequency: Optional[int] = None, + max_categories: Optional[int] = None, + ): + if max_categories is not None and max_categories < 2: + raise ValueError( + f"max_categories has to be larger than or equal to 2, input is {max_categories}." + ) + self.drop = drop + self.min_frequency = min_frequency + self.max_categories = max_categories + self._bqml_model: Optional[core.BqmlModel] = None + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return ( + type(other) is OneHotEncoder + and self._bqml_model == other._bqml_model + and self.drop == other.drop + and self.min_frequency == other.min_frequency + and self.max_categories == other.max_categories + ) def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: a list of column names to transform + columns: + a list of column names to transform Returns: a list of tuples of (sql_expression, output_name)""" + + drop = self.drop if self.drop is not None else "none" + # minus one here since BQML's inplimentation always includes index 0, and top_k is on top of that. + top_k = ( + (self.max_categories - 1) + if self.max_categories is not None + else OneHotEncoder.TOP_K_DEFAULT + ) + frequency_threshold = ( + self.min_frequency + if self.min_frequency is not None + else OneHotEncoder.FREQUENCY_THRESHOLD_DEFAULT + ) return [ ( - bigframes.ml.sql.ml_one_hot_encoder(column, f"onehotencoded_{column}"), + ml_sql.ml_one_hot_encoder( + column, drop, top_k, frequency_threshold, f"onehotencoded_{column}" + ), f"onehotencoded_{column}", ) for column in columns ] + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[OneHotEncoder, str]: + """Parse SQL to tuple(OneHotEncoder, column_label). + + Args: + sql: SQL string of format "ML.ONE_HOT_ENCODER({col_label}, '{drop}', {top_k}, {frequency_threshold}) OVER() " + + Returns: + tuple(OneHotEncoder, column_label)""" + s = sql[sql.find("(") + 1 : sql.find(")")] + col_label, drop_str, top_k, frequency_threshold = s.split(", ") + drop = ( + cast(Literal["most_frequent"], "most_frequent") + if drop_str.lower() == "'most_frequent'" + else None + ) + max_categories = int(top_k) + 1 + min_frequency = int(frequency_threshold) + + return cls(drop, min_frequency, max_categories), col_label + def fit( self, - X: bigframes.dataframe.DataFrame, - ): + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> OneHotEncoder: + (X,) = utils.convert_to_dataframe(X) + compiled_transforms = self._compile_to_sql(X.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - self._bqml_model = bigframes.ml.core.create_bqml_model( + self._bqml_model = core.create_bqml_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, @@ -126,15 +214,16 @@ def fit( # The schema of TRANSFORM output is not available in the model API, so save it during fitting self._output_names = [name for _, name in compiled_transforms] + return self - def transform( - self, X: bigframes.dataframe.DataFrame - ) -> bigframes.dataframe.DataFrame: + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("Must be fitted before transform") + (X,) = utils.convert_to_dataframe(X) + df = self._bqml_model.transform(X) return typing.cast( - bigframes.dataframe.DataFrame, + bpd.DataFrame, df[self._output_names], ) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 7017b2a4cd1..b8d9e2c6737 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -18,6 +18,8 @@ from typing import Iterable, Optional, Union +import bigframes.constants as constants + def _encode_value(v: Union[str, int, float, Iterable[str]]) -> str: """Encode a parameter value for SQL""" @@ -29,7 +31,7 @@ def _encode_value(v: Union[str, int, float, Iterable[str]]) -> str: inner = ", ".join([_encode_value(x) for x in v]) return f"[{inner}]" else: - raise ValueError("Unexpected value type") + raise ValueError(f"Unexpected value type. {constants.FEEDBACK_LINK}") def _build_param_Iterable(**kwargs: Union[str, int, float, Iterable[str]]) -> str: @@ -77,9 +79,12 @@ def ml_standard_scaler(numeric_expr_sql: str, name: str) -> str: return f"""ML.STANDARD_SCALER({numeric_expr_sql}) OVER() AS {name}""" -def ml_one_hot_encoder(numeric_expr_sql: str, name: str) -> str: - """Encode ML.ONE_HOT_ENCODER for BQML""" - return f"""ML.ONE_HOT_ENCODER({numeric_expr_sql}) OVER() AS {name}""" +def ml_one_hot_encoder( + numeric_expr_sql: str, drop: str, top_k: int, frequency_threshold: int, name: str +) -> str: + """Encode ML.ONE_HOT_ENCODER for BQML. + https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder for params.""" + return f"""ML.ONE_HOT_ENCODER({numeric_expr_sql}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {name}""" def create_model( @@ -88,9 +93,8 @@ def create_model( transform_sql: Optional[str] = None, options_sql: Optional[str] = None, ) -> str: - """Encode the CREATE MODEL statement for BQML""" - # TODO(garrettwu): This should be CREATE TEMP MODEL after b/145824779 is fixed - parts = [f"CREATE MODEL `{model_name}`"] + """Encode the CREATE TEMP MODEL statement for BQML""" + parts = [f"CREATE TEMP MODEL `{model_name}`"] if transform_sql: parts.append(transform_sql) if options_sql: @@ -104,9 +108,8 @@ def create_remote_model( connection_name: str, options_sql: Optional[str] = None, ) -> str: - """Encode the CREATE MODEL statement for BQML""" - # TODO(garrettwu): This should be CREATE TEMP MODEL after b/145824779 is fixed - parts = [f"CREATE MODEL `{model_name}`"] + """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + parts = [f"CREATE TEMP MODEL `{model_name}`"] parts.append(connection(connection_name)) if options_sql: parts.append(options_sql) @@ -117,9 +120,8 @@ def create_imported_model( model_name: str, options_sql: Optional[str] = None, ) -> str: - """Encode the CREATE MODEL statement for BQML""" - # TODO(garrettwu): This should be CREATE TEMP MODEL after b/145824779 is fixed - parts = [f"CREATE MODEL `{model_name}`"] + """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + parts = [f"CREATE TEMP MODEL `{model_name}`"] if options_sql: parts.append(options_sql) return "\n".join(parts) @@ -162,9 +164,11 @@ def ml_generate_text(model_name: str, source_sql: str, struct_options: str) -> s ({source_sql}), {struct_options})""" -def ml_embed_text(model_name: str, source_sql: str, struct_options: str) -> str: - """Encode ML.EMBED_TEXT for BQML""" - return f"""SELECT * FROM ML.EMBED_TEXT(MODEL `{model_name}`, +def ml_generate_text_embedding( + model_name: str, source_sql: str, struct_options: str +) -> str: + """Encode ML.GENERATE_TEXT_EMBEDDING for BQML""" + return f"""SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `{model_name}`, ({source_sql}), {struct_options})""" diff --git a/bigframes/ml/utils.py b/bigframes/ml/utils.py new file mode 100644 index 00000000000..299282d3337 --- /dev/null +++ b/bigframes/ml/utils.py @@ -0,0 +1,58 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +from typing import Iterable, Union + +import bigframes.constants as constants +from bigframes.core import blocks +import bigframes.pandas as bpd + +# Internal type alias +ArrayType = Union[bpd.DataFrame, bpd.Series] + + +def convert_to_dataframe(*input: ArrayType) -> Iterable[bpd.DataFrame]: + return (_convert_to_dataframe(frame) for frame in input) + + +def _convert_to_dataframe(frame: ArrayType) -> bpd.DataFrame: + if isinstance(frame, bpd.DataFrame): + return frame + if isinstance(frame, bpd.Series): + return frame.to_frame() + raise ValueError( + f"Unsupported type {type(frame)} to convert to DataFrame. {constants.FEEDBACK_LINK}" + ) + + +def convert_to_series(*input: ArrayType) -> Iterable[bpd.Series]: + return (_convert_to_series(frame) for frame in input) + + +def _convert_to_series(frame: ArrayType) -> bpd.Series: + if isinstance(frame, bpd.DataFrame): + if len(frame.columns) != 1: + raise ValueError( + "To convert into Series, DataFrames can only contain one column. " + f"Try input with only one column. {constants.FEEDBACK_LINK}" + ) + + label = typing.cast(blocks.Label, frame.columns.tolist()[0]) + return typing.cast(bpd.Series, frame[label]) + if isinstance(frame, bpd.Series): + return frame + raise ValueError( + f"Unsupported type {type(frame)} to convert to Series. {constants.FEEDBACK_LINK}" + ) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 781cc493397..13063af75f9 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -23,11 +23,14 @@ import ibis.expr.operations.generic import ibis.expr.types as ibis_types import numpy as np +import pandas as pd +import bigframes.constants as constants import bigframes.dtypes import bigframes.dtypes as dtypes _ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) +_NAN = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.nan)) _INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.inf)) BinaryOp = typing.Callable[[ibis_types.Value, ibis_types.Value], ibis_types.Value] @@ -39,7 +42,9 @@ ### Unary Ops class UnaryOp: def _as_ibis(self, x): - raise NotImplementedError("Base class UnaryOp has no implementation.") + raise NotImplementedError( + f"Base class UnaryOp has no implementation. {constants.FEEDBACK_LINK}" + ) @property def is_windowed(self): @@ -113,6 +118,79 @@ def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.StringValue, x).capitalize() +class ContainsStringOp(UnaryOp): + def __init__(self, pat: str, case: bool = True): + self._pat = pat + + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).contains(self._pat) + + +class ContainsRegexOp(UnaryOp): + def __init__(self, pat: str): + self._pat = pat + + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_search(self._pat) + + +class ReplaceStringOp(UnaryOp): + def __init__(self, pat: str, repl: str): + self._pat = pat + self._repl = repl + + def _as_ibis(self, x: ibis_types.Value): + pat_str_value = typing.cast( + ibis_types.StringValue, ibis_types.literal(self._pat) + ) + repl_str_value = typing.cast( + ibis_types.StringValue, ibis_types.literal(self._pat) + ) + + return typing.cast(ibis_types.StringValue, x).replace( + pat_str_value, repl_str_value + ) + + +class ReplaceRegexOp(UnaryOp): + def __init__(self, pat: str, repl: str): + self._pat = pat + self._repl = repl + + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_replace(self._pat, self._repl) + + +class StartsWithOp(UnaryOp): + def __init__(self, pat: typing.Sequence[str]): + self._pat = pat + + def _as_ibis(self, x: ibis_types.Value): + any_match = None + for pat in self._pat: + pat_match = typing.cast(ibis_types.StringValue, x).startswith(pat) + if any_match is not None: + any_match = any_match | pat_match + else: + any_match = pat_match + return any_match if any_match is not None else ibis_types.literal(False) + + +class EndsWithOp(UnaryOp): + def __init__(self, pat: typing.Sequence[str]): + self._pat = pat + + def _as_ibis(self, x: ibis_types.Value): + any_match = None + for pat in self._pat: + pat_match = typing.cast(ibis_types.StringValue, x).endswith(pat) + if any_match is not None: + any_match = any_match | pat_match + else: + any_match = pat_match + return any_match if any_match is not None else ibis_types.literal(False) + + class HashOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.IntegerValue, x).hash() @@ -192,6 +270,15 @@ def _as_ibis(self, x: ibis_types.Value): ) +class ExtractOp(UnaryOp): + def __init__(self, pat: str, n: int = 1): + self._pat = pat + self._n = n + + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_extract(self._pat, self._n) + + class SliceOp(UnaryOp): def __init__(self, start, stop): self._start = start @@ -201,6 +288,20 @@ def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.StringValue, x)[self._start : self._stop] +class IsInOp(UnaryOp): + def __init__(self, values, match_nulls: bool = True): + self._values = values + self._match_nulls = match_nulls + + def _as_ibis(self, x: ibis_types.Value): + if self._match_nulls and any(is_null(value) for value in self._values): + return x.isnull() | x.isin( + [val for val in self._values if not is_null(val)] + ) + else: + return x.isin(self._values) + + class BinopPartialRight(UnaryOp): def __init__(self, binop: BinaryOp, right_scalar: typing.Any): self._binop = binop @@ -231,7 +332,7 @@ class RemoteFunctionOp(UnaryOp): def __init__(self, func: typing.Callable, apply_on_null=True): if not hasattr(func, "bigframes_remote_function"): raise TypeError( - "only a bigframes remote function is supported as a callable" + f"only a bigframes remote function is supported as a callable. {constants.FEEDBACK_LINK}" ) self._func = func @@ -330,13 +431,6 @@ def or_op( ) -def isin_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x.isin(y) - - @short_circuit_nulls() def add_op( x: ibis_types.Value, @@ -431,27 +525,43 @@ def floordiv_op( ) +def _is_float(x: ibis_types.Value): + return isinstance(x, (ibis_types.FloatingColumn, ibis_types.FloatingScalar)) + + @short_circuit_nulls() def mod_op( x: ibis_types.Value, y: ibis_types.Value, ): - # TODO(tbergeron): fully support floats, including when mixed with integer - # Pandas has inconsitency about whether N mod 0. Most conventions have this be NAN. - # For some dtypes, the result is 0 instead. This implementation results in NA always. - x_numeric = typing.cast(ibis_types.NumericValue, x) - y_numeric = typing.cast(ibis_types.NumericValue, y) + is_result_float = _is_float(x) | _is_float(y) + x_numeric = typing.cast( + ibis_types.NumericValue, + x.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) + if is_result_float + else x, + ) + y_numeric = typing.cast( + ibis_types.NumericValue, + y.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) + if is_result_float + else y, + ) # Hacky short-circuit to avoid passing zero-literal to sql backend, evaluate locally instead to null. op = y.op() if isinstance(op, ibis.expr.operations.generic.Literal) and op.value == 0: return ibis_types.null().cast(x.type()) bq_mod = x_numeric % y_numeric # Bigquery will maintain x sign here + if is_result_float: + bq_mod = typing.cast(ibis_types.NumericValue, bq_mod.cast(ibis_dtypes.float64)) + # In BigQuery returned value has the same sign as X. In pandas, the sign of y is used, so we need to flip the result if sign(x) != sign(y) return ( ibis.case() .when( - y_numeric == _ZERO, _ZERO * x_numeric + y_numeric == _ZERO, + _NAN * x_numeric if is_result_float else _ZERO * x_numeric, ) # Dummy op to propogate nulls and type from x arg .when( (y_numeric < _ZERO) & (bq_mod > _ZERO), (y_numeric + bq_mod) @@ -544,3 +654,8 @@ def clip_op( .else_(original) .end() ) + + +def is_null(value) -> bool: + # float NaN/inf should be treated as distinct from 'true' null values + return typing.cast(bool, pd.isna(value)) and not isinstance(value, float) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 028ed4b6065..1687f705a1e 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -20,6 +20,9 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types +import bigframes.constants as constants +import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops + class WindowOp: def _as_ibis(self, value: ibis_types.Column, window=None): @@ -37,6 +40,8 @@ def handles_ties(self): class AggregateOp(WindowOp): + name = "abstract_aggregate" + def _as_ibis(self, value: ibis_types.Column, window=None): raise NotImplementedError("Base class AggregateOp has no implementaiton.") @@ -51,13 +56,15 @@ def constrained_op(op, column: ibis_types.Column, window=None): return operation(op, column, window) else: raise ValueError( - f"Numeric operation cannot be applied to type {column.type()}" + f"Numeric operation cannot be applied to type {column.type()}. {constants.FEEDBACK_LINK}" ) return constrained_op class SumOp(AggregateOp): + name = "sum" + @numeric_op def _as_ibis( self, column: ibis_types.NumericColumn, window=None @@ -69,7 +76,50 @@ def _as_ibis( ) +class MedianOp(AggregateOp): + name = "median" + + @numeric_op + def _as_ibis( + self, column: ibis_types.NumericColumn, window=None + ) -> ibis_types.NumericValue: + # PERCENTILE_CONT has very few allowed windows. For example, "window + # framing clause is not allowed for analytic function percentile_cont". + if window is not None: + raise NotImplementedError( + f"Median with windowing is not supported. {constants.FEEDBACK_LINK}" + ) + + # TODO(swast): Allow switching between exact and approximate median. + # For now, the best we can do is an approximate median when we're doing + # an aggregation, as PERCENTILE_CONT is only an analytic function. + return typing.cast(ibis_types.NumericValue, column.approx_median()) + + +class ApproxQuartilesOp(AggregateOp): + def __init__(self, quartile: int): + self.name = f"{quartile*25}%" + self._quartile = quartile + + @numeric_op + def _as_ibis( + self, column: ibis_types.NumericColumn, window=None + ) -> ibis_types.NumericValue: + # PERCENTILE_CONT has very few allowed windows. For example, "window + # framing clause is not allowed for analytic function percentile_cont". + if window is not None: + raise NotImplementedError( + f"Approx Quartiles with windowing is not supported. {constants.FEEDBACK_LINK}" + ) + value = vendored_ibis_ops.ApproximateMultiQuantile( + column, num_bins=4 # type: ignore + ).to_expr()[self._quartile] + return typing.cast(ibis_types.NumericValue, value) + + class MeanOp(AggregateOp): + name = "mean" + @numeric_op def _as_ibis( self, column: ibis_types.NumericColumn, window=None @@ -78,6 +128,8 @@ def _as_ibis( class ProductOp(AggregateOp): + name = "product" + @numeric_op def _as_ibis( self, column: ibis_types.NumericColumn, window=None @@ -117,16 +169,22 @@ def _as_ibis( class MaxOp(AggregateOp): + name = "max" + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(column.max(), window) class MinOp(AggregateOp): + name = "min" + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(column.min(), window) class StdOp(AggregateOp): + name = "std" + @numeric_op def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present( @@ -135,6 +193,8 @@ def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: class VarOp(AggregateOp): + name = "var" + @numeric_op def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present( @@ -143,6 +203,8 @@ def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: class CountOp(AggregateOp): + name = "count" + def _as_ibis( self, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: @@ -153,7 +215,32 @@ def skips_nulls(self): return False +class CutOp(WindowOp): + def __init__(self, bins: int): + self._bins = bins + + def _as_ibis(self, x: ibis_types.Column, window=None): + col_min = _apply_window_if_present(x.min(), window) + col_max = _apply_window_if_present(x.max(), window) + bin_width = (col_max - col_min) / self._bins + out = ibis.case() + for bin in range(self._bins - 1): + out = out.when(x <= (col_min + (bin + 1) * bin_width), bin) + out = out.when(x.notnull(), self._bins - 1) + return out.end() + + @property + def skips_nulls(self): + return False + + @property + def handles_ties(self): + return True + + class NuniqueOp(AggregateOp): + name = "nunique" + def _as_ibis( self, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: @@ -165,6 +252,8 @@ def skips_nulls(self): class RankOp(WindowOp): + name = "rank" + def _as_ibis( self, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: @@ -230,6 +319,8 @@ def _as_ibis( class AnyOp(AggregateOp): + name = "any" + def _as_ibis( self, column: ibis_types.Column, window=None ) -> ibis_types.BooleanValue: @@ -274,6 +365,7 @@ def _map_to_literal( sum_op = SumOp() mean_op = MeanOp() +median_op = MedianOp() product_op = ProductOp() max_op = MaxOp() min_op = MinOp() @@ -286,3 +378,26 @@ def _map_to_literal( all_op = AllOp() any_op = AnyOp() first_op = FirstOp() + + +# TODO: Alternative names and lookup from numpy function objects +AGGREGATIONS_LOOKUP: dict[str, AggregateOp] = { + op.name: op + for op in [ + sum_op, + mean_op, + median_op, + product_op, + max_op, + min_op, + std_op, + var_op, + count_op, + all_op, + any_op, + nunique_op, + ApproxQuartilesOp(1), + ApproxQuartilesOp(2), + ApproxQuartilesOp(3), + ] +} diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 29017d2bbe3..caef33919b3 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -19,6 +19,7 @@ import ibis.expr.types as ibis_types import pandas as pd +import bigframes.constants as constants import bigframes.core.blocks as blocks import bigframes.core.scalar as scalars import bigframes.dtypes @@ -29,7 +30,9 @@ # BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. # TODO(tbergeron): Convert to bytes-based limit -MAX_INLINE_SERIES_SIZE = 5000 +# TODO(swast): Address issues with string escaping and empty tables before +# re-enabling inline data (ibis.memtable) feature. +MAX_INLINE_SERIES_SIZE = -1 class SeriesMethods: @@ -47,7 +50,9 @@ def __init__( ): block = None if copy is not None and not copy: - raise ValueError("Series constructor only supports copy=True") + raise ValueError( + f"Series constructor only supports copy=True. {constants.FEEDBACK_LINK}" + ) if isinstance(data, blocks.Block): assert len(data.value_columns) == 1 assert len(data.column_labels) == 1 @@ -60,12 +65,12 @@ def __init__( if name: if not isinstance(name, str): raise NotImplementedError( - "BigQuery DataFrames only supports string series names." + f"BigQuery DataFrames only supports string series names. {constants.FEEDBACK_LINK}" ) block = block.with_column_labels([name]) if index: raise NotImplementedError( - "Series 'index' constructor parameter not supported when passing BigQuery-backed objects" + f"Series 'index' constructor parameter not supported when passing BigQuery-backed objects. {constants.FEEDBACK_LINK}" ) if dtype: block = block.multi_apply_unary_op( @@ -138,7 +143,7 @@ def _apply_binary_op( if isinstance(other, pd.Series): # TODO: Convert to BigQuery DataFrames series raise NotImplementedError( - "Pandas series not supported supported as operand." + f"Pandas series not supported supported as operand. {constants.FEEDBACK_LINK}" ) if isinstance(other, series.Series): (left, right, block) = self._align(other, how=alignment) diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 57029ede416..a16ecb0d328 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -14,13 +14,23 @@ from __future__ import annotations -from typing import Literal, Optional, Union +import re +from typing import cast, Literal, Optional, Union +import bigframes.constants as constants +import bigframes.dataframe as df import bigframes.operations as ops import bigframes.operations.base import bigframes.series as series import third_party.bigframes_vendored.pandas.core.strings.accessor as vendorstr +# Maps from python to re2 +REGEXP_FLAGS = { + re.IGNORECASE: "i", + re.MULTILINE: "m", + re.DOTALL: "s", +} + class StringMethods(bigframes.operations.base.SeriesMethods, vendorstr.StringMethods): __doc__ = vendorstr.StringMethods.__doc__ @@ -72,6 +82,84 @@ def repeat(self, repeats: int) -> series.Series: def capitalize(self) -> series.Series: return self._apply_unary_op(ops.capitalize_op) + def contains( + self, pat, case: bool = True, flags: int = 0, *, regex: bool = True + ) -> series.Series: + if not case: + return self.contains(pat, flags=flags | re.IGNORECASE, regex=True) + if regex: + re2flags = _parse_flags(flags) + if re2flags: + pat = re2flags + pat + return self._apply_unary_op(ops.ContainsRegexOp(pat)) + else: + return self._apply_unary_op(ops.ContainsStringOp(pat)) + + def extract(self, pat: str, flags: int = 0): + re2flags = _parse_flags(flags) + if re2flags: + pat = re2flags + pat + compiled = re.compile(pat) + if compiled.groups == 0: + raise ValueError("No capture groups in 'pat'") + + results: list[str] = [] + block = self._block + for i in range(compiled.groups): + labels = [ + label + for label, groupn in compiled.groupindex.items() + if i + 1 == groupn + ] + label = labels[0] if labels else str(i) + block, id = block.apply_unary_op( + self._value_column, ops.ExtractOp(pat, i + 1), result_label=label + ) + results.append(id) + block = block.select_columns(results) + return df.DataFrame(block) + + def replace( + self, + pat: Union[str, re.Pattern], + repl: str, + *, + case: Optional[bool] = None, + flags: int = 0, + regex: bool = False, + ) -> series.Series: + is_compiled = isinstance(pat, re.Pattern) + patstr = cast(str, pat.pattern if is_compiled else pat) # type: ignore + if case is False: + return self.replace(pat, repl, flags=flags | re.IGNORECASE, regex=True) + if regex: + re2flags = _parse_flags(flags) + if re2flags: + patstr = re2flags + patstr + return self._apply_unary_op(ops.ReplaceRegexOp(patstr, repl)) + else: + if is_compiled: + raise ValueError( + "Must set 'regex'=True if using compiled regex pattern." + ) + return self._apply_unary_op(ops.ReplaceStringOp(patstr, repl)) + + def startswith( + self, + pat: Union[str, tuple[str, ...]], + ) -> series.Series: + if not isinstance(pat, tuple): + pat = (pat,) + return self._apply_unary_op(ops.StartsWithOp(pat)) + + def endswith( + self, + pat: Union[str, tuple[str, ...]], + ) -> series.Series: + if not isinstance(pat, tuple): + pat = (pat,) + return self._apply_unary_op(ops.EndsWithOp(pat)) + def cat( self, others: Union[str, series.Series], @@ -79,3 +167,22 @@ def cat( join: Literal["outer", "left"] = "left", ) -> series.Series: return self._apply_binary_op(others, ops.concat_op, alignment=join) + + +def _parse_flags(flags: int) -> Optional[str]: + re2flags = [] + for reflag, re2flag in REGEXP_FLAGS.items(): + if flags & flags: + re2flags.append(re2flag) + flags = flags ^ reflag + + # Remaining flags couldn't be mapped to re2 engine + if flags: + raise NotImplementedError( + f"Could not handle RegexFlag: {flags}. {constants.FEEDBACK_LINK}" + ) + + if re2flags: + return "(?" + "".join(re2flags) + ")" + else: + return None diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index e7c14351513..cc8b4e5cc4d 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -46,40 +46,94 @@ import bigframes.series import bigframes.session import third_party.bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat +import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile + +# Support pandas dtype attribute +NA = pandas.NA +BooleanDtype = pandas.BooleanDtype +Float64Dtype = pandas.Float64Dtype +Int64Dtype = pandas.Int64Dtype +StringDtype = pandas.StringDtype +ArrowDtype = pandas.ArrowDtype # Include method definition so that the method appears in our docs for # bigframes.pandas general functions. @typing.overload def concat( - objs: Iterable[bigframes.dataframe.DataFrame], *, join, ignore_index + objs: Iterable[bigframes.series.Series], + *, + axis: typing.Literal["index", 0] = ..., + join=..., + ignore_index=..., +) -> bigframes.series.Series: + ... + + +@typing.overload +def concat( + objs: Iterable[bigframes.dataframe.DataFrame], + *, + axis: typing.Literal["index", 0] = ..., + join=..., + ignore_index=..., ) -> bigframes.dataframe.DataFrame: ... @typing.overload def concat( - objs: Iterable[bigframes.series.Series], *, join, ignore_index -) -> bigframes.series.Series: + objs: Iterable[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]], + *, + axis: typing.Literal["columns", 1], + join=..., + ignore_index=..., +) -> bigframes.dataframe.DataFrame: ... +@typing.overload def concat( - objs: Union[ - Iterable[bigframes.dataframe.DataFrame], Iterable[bigframes.series.Series] - ], + objs: Iterable[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]], *, + axis=..., + join=..., + ignore_index=..., +) -> Union[bigframes.dataframe.DataFrame, bigframes.series.Series]: + ... + + +def concat( + objs: Iterable[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]], + *, + axis: typing.Union[str, int] = 0, join: Literal["inner", "outer"] = "outer", ignore_index: bool = False, ) -> Union[bigframes.dataframe.DataFrame, bigframes.series.Series]: return bigframes.core.reshape.concat( - objs=objs, join=join, ignore_index=ignore_index + objs=objs, axis=axis, join=join, ignore_index=ignore_index ) concat.__doc__ = vendored_pandas_concat.concat.__doc__ +def cut( + x: bigframes.series.Series, + bins: int, + *, + labels: Optional[bool] = None, +) -> bigframes.series.Series: + return bigframes.core.reshape.cut( + x, + bins, + labels=labels, + ) + + +cut.__doc__ = vendored_pandas_tile.cut.__doc__ + + options = config.options """Global :class:`~bigframes._config.Options` to configure BigQuery DataFrames.""" @@ -88,9 +142,12 @@ def concat( def reset_session() -> None: - """Start a fresh session next time a function requires a session. + """Start a fresh session the next time a function requires a session. Closes the current session if it was already started. + + Returns: + None """ global _global_session @@ -124,15 +181,22 @@ def _with_default_session(func: Callable[..., _T], *args, **kwargs) -> _T: def _set_default_session_location_if_possible(query): - # If the default session has not started yet and this is the first API user - # is calling, then set the default location as per the query. + # Set the location as per the query if this is the first query the user is + # running and: + # (1) Default session has not started yet, and + # (2) Location is not set yet, and + # (3) Use of regional endpoints is not set. # If query is a table name, then it would be the location of the table. # If query is a SQL with a table, then it would be table's location. # If query is a SQL with no table, then it would be the BQ default location. - if options.bigquery._session_started or options.bigquery.use_regional_endpoints: + if ( + options.bigquery._session_started + or options.bigquery.location + or options.bigquery.use_regional_endpoints + ): return - bqclient, _, _ = bigframes.session._create_bq_clients( + bqclient, _, _, _ = bigframes.session._create_cloud_clients( project=options.bigquery.project, location=options.bigquery.location, use_regional_endpoints=options.bigquery.use_regional_endpoints, @@ -320,6 +384,16 @@ def remote_function( remote_function.__doc__ = inspect.getdoc(bigframes.session.Session.remote_function) +def read_gbq_function(function_name: str): + return _with_default_session( + bigframes.session.Session.read_gbq_function, + function_name=function_name, + ) + + +read_gbq_function.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_function) + + # Other aliases DataFrame = bigframes.dataframe.DataFrame Index = bigframes.core.indexes.Index @@ -332,6 +406,7 @@ def remote_function( "options", "read_csv", "read_gbq", + "read_gbq_function", "read_gbq_model", "read_pandas", "remote_function", diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 3fe4e38d785..5924941cc54 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -27,20 +27,28 @@ import tempfile import textwrap import time -import typing +from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from bigframes.session import Session import cloudpickle import google.api_core.exceptions from google.cloud import bigquery, bigquery_connection_v1, functions_v2 +from google.cloud.bigquery.routine import Routine +from google.cloud.bigquery.standard_sql import StandardSqlTypeNames from ibis.backends.bigquery.compiler import compiles from ibis.backends.bigquery.datatypes import BigQueryType +from ibis.expr.datatypes.core import boolean +from ibis.expr.datatypes.core import DataType as IbisDataType from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type +from ibis.expr.datatypes.core import float64, int64 +from ibis.expr.datatypes.core import string as ibis_string import ibis.expr.operations as ops import ibis.expr.rules as rlz +import bigframes.constants as constants + # TODO(shobs): Change the min log level to INFO after the development stabilizes # before June 2023 logging.basicConfig( @@ -52,10 +60,14 @@ # https://docs.python.org/3/library/pickle.html#data-stream-format _pickle_protocol_version = 4 -# Input and output python types supported by BigQuery DataFrames remote functions. +# Input and output types supported by BigQuery DataFrames remote functions. # TODO(shobs): Extend the support to all types supported by BQ remote functions # https://cloud.google.com/bigquery/docs/remote-functions#limitations -_supported_io_types = set((bool, float, int, str)) +_supported_io_ibis_types = {boolean, float64, int64, ibis_string} +TYPE_ERROR_MESSAGE_FORMAT = ( + f"Type {{}} not supported, supported types are {_supported_io_ibis_types}. " + f"{constants.FEEDBACK_LINK}" +) def get_remote_function_locations(bq_location): @@ -94,9 +106,20 @@ def _run_system_command(command): if exit_code: raise RuntimeError( f"Command: {command}\nOutput: {stdout.decode()}\nError: {stderr.decode()}" + f"{constants.FEEDBACK_LINK}" ) +def routine_ref_to_string_for_query(routine_ref: bigquery.RoutineReference) -> str: + return f"`{routine_ref.project}.{routine_ref.dataset_id}`.{routine_ref.routine_id}" + + +class IbisSignature(NamedTuple): + parameter_names: List[str] + input_types: List[IbisDataType] + output_type: IbisDataType + + def get_cloud_function_name(def_, uniq_suffix=None): """Get the name of the cloud function.""" cf_name = _get_hash(def_) @@ -123,6 +146,7 @@ def __init__( self, gcp_project_id, cloud_function_region, + cloud_functions_client, bq_location, bq_dataset, bq_client, @@ -131,6 +155,7 @@ def __init__( ): self._gcp_project_id = gcp_project_id self._cloud_function_region = cloud_function_region + self._cloud_functions_client = cloud_functions_client self._bq_location = bq_location self._bq_dataset = bq_dataset self._bq_client = bq_client @@ -184,21 +209,18 @@ def create_bq_remote_function( f"{name} {BigQueryType.from_ibis(input_types[idx])}" ) create_function_ddl = f""" - CREATE OR REPLACE FUNCTION `{self._gcp_project_id}.{self._bq_dataset}`.{bq_function_name}({','.join(bq_function_args)}) - RETURNS {bq_function_return_type} - REMOTE WITH CONNECTION `{self._gcp_project_id}.{self._bq_location}.{self._bq_connection_id}` - OPTIONS ( - endpoint = "{endpoint}" - )""" + CREATE OR REPLACE FUNCTION `{self._gcp_project_id}.{self._bq_dataset}`.{bq_function_name}({','.join(bq_function_args)}) + RETURNS {bq_function_return_type} + REMOTE WITH CONNECTION `{self._gcp_project_id}.{self._bq_location}.{self._bq_connection_id}` + OPTIONS ( + endpoint = "{endpoint}" + )""" logger.info(f"Creating BQ remote function: {create_function_ddl}") + # TODO: Use session._start_query() so we get progress bar query_job = self._bq_client.query(create_function_ddl) # Make an API request. query_job.result() # Wait for the job to complete. logger.info(f"Created remote function {query_job.ddl_target_routine}") - def get_remote_function_fully_qualified_name(self, name): - "Get the fully qualilfied name for a BQ remote function." - return "{}.{}.{}".format(self._gcp_project_id, self._bq_dataset, name) - def get_cloud_function_fully_qualified_name(self, name): "Get the fully qualilfied name for a cloud function." return "projects/{}/locations/{}/functions/{}".format( @@ -207,10 +229,11 @@ def get_cloud_function_fully_qualified_name(self, name): def get_cloud_function_endpoint(self, name): """Get the http endpoint of a cloud function if it exists.""" - client = functions_v2.FunctionServiceClient() fully_qualified_name = self.get_cloud_function_fully_qualified_name(name) try: - response = client.get_function(name=fully_qualified_name) + response = self._cloud_functions_client.get_function( + name=fully_qualified_name + ) return response.service_config.uri except google.api_core.exceptions.NotFound: pass @@ -233,6 +256,34 @@ def create_bq_connection(self): def check_bq_connection_exists(self): """Check if the BigQuery Connection exists.""" client = self._bq_connection_client + if self._bq_connection_id.count(".") == 1: + bq_location, bq_connection_id = self._bq_connection_id.split(".") + if bq_location != self._bq_location: + logger.info( + f"Reset location {self._bq_location} to match the" + + f"location in connection name: {bq_location}" + ) + self._bq_location = bq_location + self._bq_connection_id = bq_connection_id + elif self._bq_connection_id.count(".") == 2: + ( + gcp_project_id, + bq_location, + bq_connection_id, + ) = self._bq_connection_id.split(".") + if gcp_project_id != self._gcp_project_id: + raise ValueError( + "The project_id does not match BigQuery connection gcp_project_id: " + f"{self._gcp_project_id}." + ) + if bq_location != self._bq_location: + logger.info( + f"Reset location {self._bq_location} to match the" + + f"location in connection name: {bq_location}" + ) + self._gcp_project_id = gcp_project_id + self._bq_location = bq_location + self._bq_connection_id = bq_connection_id request = bigquery_connection_v1.GetConnectionRequest( name=client.connection_path( self._gcp_project_id, self._bq_location, self._bq_connection_id @@ -299,7 +350,6 @@ def generate_cloud_function_main_code(self, def_, dir): def {handler_func_name}(request): request_json = request.get_json(silent=True) - print("[debug] received json request: " + str(request_json)) calls = request_json["calls"] replies = [] for call in calls: @@ -402,7 +452,9 @@ def create_cloud_function(self, def_, cf_name): # Fetch the endpoint of the just created function endpoint = self.get_cloud_function_endpoint(cf_name) if not endpoint: - raise ValueError("Couldn't fetch the http endpoint") + raise ValueError( + f"Couldn't fetch the http endpoint. {constants.FEEDBACK_LINK}" + ) logger.info( f"Successfully created cloud function {cf_name} with uri ({endpoint})" @@ -466,7 +518,8 @@ def check_cloud_function_tools_and_permissions(self): # cloud function and BigQuery remote function respectively if not shutil.which("gcloud"): raise ValueError( - "gcloud tool not installed, install it from https://cloud.google.com/sdk/docs/install" + "gcloud tool not installed, install it from https://cloud.google.com/sdk/docs/install. " + f"{constants.FEEDBACK_LINK}" ) # TODO(shobs): Check for permissions too @@ -481,110 +534,231 @@ def check_cloud_function_tools_and_permissions(self): # `cloudasset.googleapis.com` +def remote_function_node( + routine_ref: bigquery.RoutineReference, ibis_signature: IbisSignature +): + """Creates an Ibis node representing a remote function call.""" + + fields = { + name: rlz.value(type_) + for name, type_ in zip( + ibis_signature.parameter_names, ibis_signature.input_types + ) + } + + try: + fields["output_type"] = rlz.shape_like("args", dtype=ibis_signature.output_type) # type: ignore + except TypeError: + fields["output_dtype"] = property(lambda _: ibis_signature.output_type) + fields["output_shape"] = rlz.shape_like("args") + + node = type(routine_ref_to_string_for_query(routine_ref), (ops.ValueOp,), fields) # type: ignore + + @compiles(node) + def compile_node(t, op): + return "{}({})".format(node.__name__, ", ".join(map(t.translate, op.args))) + + def f(*args, **kwargs): + return node(*args, **kwargs).to_expr() + + f.bigframes_remote_function = str(routine_ref) # type: ignore + + return f + + +def ibis_type_from_python_type(t: type) -> IbisDataType: + ibis_type = python_type_to_bigquery_type(t) + assert ibis_type in _supported_io_ibis_types, TYPE_ERROR_MESSAGE_FORMAT.format( + ibis_type + ) + return ibis_type + + +def ibis_type_from_type_kind(tk: StandardSqlTypeNames) -> IbisDataType: + ibis_type = BigQueryType.to_ibis(tk) + assert ibis_type in _supported_io_ibis_types, TYPE_ERROR_MESSAGE_FORMAT.format( + ibis_type + ) + return ibis_type + + +def ibis_signature_from_python_signature( + signature: inspect.Signature, + input_types: Sequence[type], + output_type: type, +) -> IbisSignature: + return IbisSignature( + parameter_names=list(signature.parameters.keys()), + input_types=[ibis_type_from_python_type(t) for t in input_types], + output_type=ibis_type_from_python_type(output_type), + ) + + +def ibis_signature_from_routine( + routine: Routine, +) -> IbisSignature: + return IbisSignature( + parameter_names=[arg.name for arg in routine.arguments], + input_types=[ + ibis_type_from_type_kind(arg.data_type.type_kind) + for arg in routine.arguments + ], + output_type=ibis_type_from_type_kind(routine.return_type.type_kind), + ) + + +class DatasetMissingError(ValueError): + pass + + +def get_routine_reference( + routine_ref_str: str, + bigquery_client: bigquery.Client, + session: Optional[Session], +) -> bigquery.RoutineReference: + try: + # Handle cases ".." and + # ".". + return bigquery.RoutineReference.from_string( + routine_ref_str, + default_project=bigquery_client.project, + ) + except ValueError: + # Handle case of "". + if not session: + raise DatasetMissingError + + dataset_ref = bigquery.DatasetReference( + bigquery_client.project, session._session_dataset_id + ) + return dataset_ref.routine(routine_ref_str) + + # Inspired by @udf decorator implemented in ibis-bigquery package # https://github.com/ibis-project/ibis-bigquery/blob/main/ibis_bigquery/udf/__init__.py # which has moved as @js to the ibis package # https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py def remote_function( - input_types: typing.Sequence[type], + input_types: Sequence[type], output_type: type, - session: typing.Optional[Session] = None, - bigquery_client: typing.Optional[bigquery.Client] = None, - bigquery_connection_client: typing.Optional[ + session: Optional[Session] = None, + bigquery_client: Optional[bigquery.Client] = None, + bigquery_connection_client: Optional[ bigquery_connection_v1.ConnectionServiceClient ] = None, - dataset: typing.Optional[str] = None, - bigquery_connection: typing.Optional[str] = None, + cloud_functions_client: Optional[functions_v2.FunctionServiceClient] = None, + dataset: Optional[str] = None, + bigquery_connection: Optional[str] = None, reuse: bool = True, ): """Decorator to turn a user defined function into a BigQuery remote function. .. deprecated:: 0.0.1 - Use :func:`bigframes.pandas.remote_function` instead. + This is an internal method. Please use :func:`bigframes.pandas.remote_function` instead. + + .. note:: + Please make sure following is setup before using this API: + + 1. Have the below APIs enabled for your project: + + * BigQuery Connection API + * Cloud Functions API + * Cloud Run API + * Cloud Build API + * Artifact Registry API + * Cloud Resource Manager API + + This can be done from the cloud console (change `PROJECT_ID` to yours): + https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID + + Or from the gcloud CLI: + + `$ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com` + + 2. Have following IAM roles enabled for you: + + * BigQuery Data Editor (roles/bigquery.dataEditor) + * BigQuery Connection Admin (roles/bigquery.connectionAdmin) + * Cloud Functions Developer (roles/cloudfunctions.developer) + * Service Account User (roles/iam.serviceAccountUser) + * Storage Object Viewer (roles/storage.objectViewer) + * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) + + 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: + + 1. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection + 2. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + + Alternatively, the IAM could also be setup via the gcloud CLI: + + `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. Args: - input_types : list(type). + input_types list(type): List of input data types in the user defined function. - output_type : type. + output_type type: Data type of the output in the user defined function. - session : bigframes.Session, Optional + session (bigframes.Session, Optional): BigQuery DataFrames session to use for getting default project, dataset and BigQuery connection. - bigquery_client : google.cloud.bigquery.Client, Optional + bigquery_client (google.cloud.bigquery.Client, Optional): Client to use for BigQuery operations. If this param is not provided then bigquery client from the session would be used. - bigquery_connection_client : google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional + bigquery_connection_client (google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional): + Client to use for cloud functions operations. If this param is not + provided then functions client from the session would be used. + cloud_functions_client (google.cloud.functions_v2.FunctionServiceClient, Optional): Client to use for BigQuery connection operations. If this param is not provided then bigquery connection client from the session would be used. - dataset : str, Optional. - Dataset to use to create a BigQuery function. It should be in + dataset (str, Optional.): + Dataset in which to create a BigQuery remote function. It should be in `.` or `` format. If this - param is not provided then session dataset id would be used. - bigquery_connection : str, Optional. - Name of the BigQuery connection. If this param is not provided then - the bigquery connection from the session would be used. If it is pre - created in the same location as the `bigquery_client.location` then - it would be used, otherwise it would be created dynamically using - the `bigquery_connection_client` assuming the user has necessary - priviliges. - reuse : bool, Optional. - Reuse the remote function if already exists. - `True` by default, which will result in reusing an existing remote + parameter is not provided then session dataset id is used. + bigquery_connection (str, Optional): + Name of the BigQuery connection in the form of `CONNECTION_ID` or + `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID`. + If this param is not provided then the bigquery connection from the session + would be used. If it is pre created in the same location as the + `bigquery_client.location` then it would be used, otherwise it is created + dynamically using the `bigquery_connection_client` assuming the user has necessary + priviliges. The PROJECT_ID should be the same as the BigQuery connection project. + reuse (bool, Optional): + Reuse the remote function if is already exists. + `True` by default, which results in reusing an existing remote function (if any) that was previously created for the same udf. - Setting it to false would force creating a unique remote function. + Setting it to false forces the creation of creating a unique remote function. If the required remote function does not exist then it would be created irrespective of this param. - Notes: - Please make sure following is setup before using this API: - - 1. Have the below APIs enabled for your project: - a. BigQuery Connection API - b. Cloud Functions API - c. Cloud Run API - d. Cloud Build API - e. Artifact Registry API - f. Cloud Resource Manager API - - This can be done from the cloud console (change PROJECT_ID to yours): - https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID - Or from the gcloud CLI: - $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com - - 2. Have following IAM roles enabled for you: - a. BigQuery Data Editor (roles/bigquery.dataEditor) - b. BigQuery Connection Admin (roles/bigquery.connectionAdmin) - c. Cloud Functions Developer (roles/cloudfunctions.developer) - d. Service Account User (roles/iam.serviceAccountUser) - e. Storage Object Viewer (roles/storage.objectViewer) - f. Project IAM Admin (roles/resourcemanager.projectIamAdmin) - (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) - - 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: - a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection - b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function - Alternatively, the IAM could also be setup via the gcloud CLI: - $ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker" - """ # A BigQuery client is required to perform BQ operations - if not bigquery_client: - if session: - bigquery_client = session.bqclient + if not bigquery_client and session: + bigquery_client = session.bqclient if not bigquery_client: raise ValueError( - "A bigquery client must be provided, either directly or via session" + "A bigquery client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" ) # A BigQuery connection client is required to perform BQ connection operations + if not bigquery_connection_client and session: + bigquery_connection_client = session.bqconnectionclient if not bigquery_connection_client: + raise ValueError( + "A bigquery connection client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A cloud functions client is required to perform cloud functions operations + if not cloud_functions_client: if session: - bigquery_connection_client = session.bqconnectionclient - if not bigquery_connection_client: + cloud_functions_client = session.cloudfunctionsclient + if not cloud_functions_client: raise ValueError( - "A bigquery connection client must be provided, either directly or via session" + "A functions connection client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" ) # BQ remote function must be persisted, for which we need a dataset @@ -593,16 +767,15 @@ def remote_function( dataset_ref = bigquery.DatasetReference.from_string( dataset, default_project=bigquery_client.project ) - gcp_project_id = dataset_ref.project - bq_dataset = dataset_ref.dataset_id + elif session: + dataset_ref = bigquery.DatasetReference.from_string( + session._session_dataset_id, default_project=bigquery_client.project + ) else: - gcp_project_id = bigquery_client.project - if session: - bq_dataset = session._session_dataset_id - if not gcp_project_id: - raise ValueError("Project must be provided, either directly or via session") - if not bq_dataset: - raise ValueError("Dataset must be provided, either directly or via session") + raise ValueError( + "Project and dataset must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) bq_location, cloud_function_region = get_remote_function_locations( bigquery_client.location @@ -614,7 +787,8 @@ def remote_function( bigquery_connection = session._remote_udf_connection # type: ignore if not bigquery_connection: raise ValueError( - "BigQuery connection must be provided, either directly or via session" + "BigQuery connection must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" ) uniq_suffix = None @@ -628,70 +802,65 @@ def wrapper(f): raise TypeError("f must be callable, got {}".format(f)) signature = inspect.signature(f) - parameter_names = signature.parameters.keys() - - # Check supported python datatypes and convert to ibis datatypes - type_error_message_format = ( - "type {{}} not supported, supported types are {}.".format( - ", ".join([type_.__name__ for type_ in _supported_io_types]) - ) - ) - for type_ in input_types: - assert type_ in _supported_io_types, type_error_message_format.format(type_) - assert output_type in _supported_io_types, type_error_message_format.format( - output_type + ibis_signature = ibis_signature_from_python_signature( + signature, input_types, output_type ) - input_types_ibis = [ - python_type_to_bigquery_type(type_) for type_ in input_types - ] - output_type_ibis = python_type_to_bigquery_type(output_type) - - rf_node_fields = { - name: rlz.value(type) - for name, type in zip(parameter_names, input_types_ibis) - } - - try: - rf_node_fields["output_type"] = rlz.shape_like( - "args", dtype=output_type_ibis - ) - except TypeError: - rf_node_fields["output_dtype"] = property(lambda _: output_type_ibis) - rf_node_fields["output_shape"] = rlz.shape_like("args") remote_function_client = RemoteFunctionClient( - gcp_project_id, + dataset_ref.project, cloud_function_region, + cloud_functions_client, bq_location, - bq_dataset, + dataset_ref.dataset_id, bigquery_client, bigquery_connection_client, bigquery_connection, ) rf_name, cf_name = remote_function_client.provision_bq_remote_function( - f, input_types_ibis, output_type_ibis, uniq_suffix + f, ibis_signature.input_types, ibis_signature.output_type, uniq_suffix ) - rf_fully_qualified_name = f"`{gcp_project_id}.{bq_dataset}`.{rf_name}" - rf_node = type(rf_fully_qualified_name, (ops.ValueOp,), rf_node_fields) - - @compiles(rf_node) - def compiles_rf_node(t, op): - return "{}({})".format( - rf_node.__name__, ", ".join(map(t.translate, op.args)) - ) - @functools.wraps(f) - def wrapped(*args, **kwargs): - node = rf_node(*args, **kwargs) - return node.to_expr() + node = remote_function_node(dataset_ref.routine(rf_name), ibis_signature) - wrapped.__signature__ = signature - wrapped.bigframes_remote_function = ( - remote_function_client.get_remote_function_fully_qualified_name(rf_name) - ) - wrapped.bigframes_cloud_function = ( + node = functools.wraps(f)(node) + node.__signature__ = signature + node.bigframes_cloud_function = ( remote_function_client.get_cloud_function_fully_qualified_name(cf_name) ) - return wrapped + + return node return wrapper + + +def read_gbq_function( + function_name: str, + session: Optional[Session] = None, + bigquery_client: Optional[bigquery.Client] = None, +): + """ + Read an existing BigQuery function and prepare it for use in future queries. + """ + + # A BigQuery client is required to perform BQ operations + if not bigquery_client and session: + bigquery_client = session.bqclient + if not bigquery_client: + raise ValueError( + "A bigquery client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + try: + routine_ref = get_routine_reference(function_name, bigquery_client, session) + except DatasetMissingError: + raise ValueError( + "Project and dataset must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # Find the routine and get its arguments. + routine = bigquery_client.get_routine(routine_ref) + ibis_signature = ibis_signature_from_routine(routine) + + return remote_function_node(routine_ref, ibis_signature) diff --git a/bigframes/series.py b/bigframes/series.py index f008e0fb060..537991ed004 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -16,9 +16,10 @@ from __future__ import annotations +import numbers import textwrap import typing -from typing import Any, Optional, Union +from typing import Any, Mapping, Optional, Tuple, Union import google.cloud.bigquery as bigquery import ibis.expr.types as ibis_types @@ -27,6 +28,7 @@ import pandas.core.dtypes.common import typing_extensions +import bigframes.constants as constants import bigframes.core from bigframes.core import WindowSpec import bigframes.core.block_transforms as block_ops @@ -34,11 +36,16 @@ import bigframes.core.groupby as groupby import bigframes.core.indexers import bigframes.core.indexes as indexes -from bigframes.core.ordering import OrderingColumnReference, OrderingDirection +from bigframes.core.ordering import ( + OrderingColumnReference, + OrderingDirection, + STABLE_SORTS, +) import bigframes.core.scalar as scalars import bigframes.core.window import bigframes.dataframe import bigframes.dtypes +import bigframes.formatting_helpers as formatter import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.operations.base @@ -46,7 +53,8 @@ import bigframes.operations.strings as strings import third_party.bigframes_vendored.pandas.core.series as vendored_pandas_series -LevelsType = typing.Union[str, int, typing.Sequence[typing.Union[str, int]]] +LevelType = typing.Union[str, int] +LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Series): @@ -100,21 +108,68 @@ def values(self) -> numpy.ndarray: @property def query_job(self) -> Optional[bigquery.QueryJob]: + """BigQuery job metadata for the most recent query. + + Returns: + The most recent `QueryJob + `_. + """ + if self._query_job is None: + self._set_internal_query_job(self._compute_dry_run()) return self._query_job + def _set_internal_query_job(self, query_job: bigquery.QueryJob): + self._query_job = query_job + def __len__(self): return self.shape[0] def copy(self) -> Series: return Series(self._block) - def rename(self, index: Optional[str], **kwargs) -> Series: + def rename( + self, index: Union[blocks.Label, Mapping[Any, Any]] = None, **kwargs + ) -> Series: if len(kwargs) != 0: raise NotImplementedError( - "rename does not currently support any keyword arguments." + f"rename does not currently support any keyword arguments. {constants.FEEDBACK_LINK}" ) - block = self._block.with_column_labels([index]) - return Series(block) + + # rename the Series name + if index is None or isinstance( + index, str + ): # Python 3.9 doesn't allow isinstance of Optional + index = typing.cast(Optional[str], index) + block = self._block.with_column_labels([index]) + return Series(block) + + # rename the index + if isinstance(index, Mapping): + index = typing.cast(Mapping[Any, Any], index) + block = self._block + for k, v in index.items(): + new_idx_ids = [] + for idx_id, idx_dtype in zip(block.index_columns, block.index_dtypes): + # Will throw if key type isn't compatible with index type, which leads to invalid SQL. + block.create_constant(k, dtype=idx_dtype) + + # Will throw if value type isn't compatible with index type. + block, const_id = block.create_constant(v, dtype=idx_dtype) + block, cond_id = block.apply_unary_op( + idx_id, ops.BinopPartialRight(ops.ne_op, k) + ) + block, new_idx_id = block.apply_ternary_op( + idx_id, cond_id, const_id, ops.where_op + ) + + new_idx_ids.append(new_idx_id) + block = block.drop_columns([const_id, cond_id]) + + block = block.set_index(new_idx_ids, index_labels=block.index_labels) + + return Series(block) + + raise ValueError(f"Unsupported type of parameter index: {type(index)}") def rename_axis( self, @@ -123,7 +178,7 @@ def rename_axis( ) -> Series: if len(kwargs) != 0: raise NotImplementedError( - "rename_axis does not currently support any keyword arguments." + f"rename_axis does not currently support any keyword arguments. {constants.FEEDBACK_LINK}" ) # limited implementation: the new index name is simply the 'mapper' parameter if _is_list_like(mapper): @@ -151,8 +206,15 @@ def __repr__(self) -> str: # maybe we just print the job metadata that we have so far? # TODO(swast): Avoid downloading the whole series by using job # metadata, like we do with DataFrame. - preview = self.compute() - return repr(preview) + opts = bigframes.options.display + max_results = opts.max_rows + if opts.repr_mode == "deferred": + return formatter.repr_query_job(self.query_job) + + pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) + self._set_internal_query_job(query_job) + + return repr(pandas_df.iloc[:, 0]) def _to_ibis_expr(self): """Creates an Ibis table expression representing the Series.""" @@ -168,31 +230,77 @@ def astype( ) -> Series: return self._apply_unary_op(bigframes.operations.AsTypeOp(dtype)) - def compute(self) -> pandas.Series: - """Executes deferred operations and downloads the results.""" - df, query_job = self._block.compute((self._value_column,)) - self._query_job = query_job + def to_pandas( + self, + max_download_size: Optional[int] = None, + sampling_method: Optional[str] = None, + random_state: Optional[int] = None, + ) -> pandas.Series: + """Writes Series to pandas Series. + + Args: + max_download_size (int, default None): + Download size threshold in MB. If max_download_size is exceeded when downloading data + (e.g., to_pandas()), the data will be downsampled if + bigframes.options.sampling.enable_downsampling is True, otherwise, an error will be + raised. If set to a value other than None, this will supersede the global config. + sampling_method (str, default None): + Downsampling algorithms to be chosen from, the choices are: "head": This algorithm + returns a portion of the data from the beginning. It is fast and requires minimal + computations to perform the downsampling; "uniform": This algorithm returns uniform + random samples of the data. If set to a value other than None, this will supersede + the global config. + random_state (int, default None): + The seed for the uniform downsampling algorithm. If provided, the uniform method may + take longer to execute and require more computation. If set to a value other than + None, this will supersede the global config. + + 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. + """ + df, query_job = self._block.to_pandas( + (self._value_column,), + max_download_size=max_download_size, + sampling_method=sampling_method, + random_state=random_state, + ) + self._set_internal_query_job(query_job) series = df[self._value_column] series.name = self._name return series - def drop(self, labels: blocks.Label | typing.Sequence[blocks.Label] = None): - block = self._block - index_column = block.index_columns[0] + def _compute_dry_run(self) -> bigquery.QueryJob: + return self._block._compute_dry_run((self._value_column,)) + + def drop( + self, + labels: typing.Any = None, + *, + axis: typing.Union[int, str] = 0, + index: typing.Any = None, + columns: Union[blocks.Label, typing.Iterable[blocks.Label]] = None, + level: typing.Optional[LevelType] = None, + ) -> Series: + if labels and index: + raise ValueError("Must specify exacly one of 'labels' or 'index'") + index = labels or index + # ignore axis, columns params + block = self._block + level_id = self._resolve_levels(level or 0)[0] if _is_list_like(labels): block, inverse_condition_id = block.apply_unary_op( - index_column, ops.partial_right(ops.isin_op, labels) + level_id, ops.IsInOp(index, match_nulls=True) ) block, condition_id = block.apply_unary_op( inverse_condition_id, ops.invert_op ) - else: block, condition_id = block.apply_unary_op( - index_column, ops.partial_right(ops.ne_op, labels) + level_id, ops.partial_right(ops.ne_op, labels) ) - block = block.filter(condition_id) + block = block.filter(condition_id, keep_null=True) block = block.drop_columns([condition_id]) return Series(block.select_column(self._value_column)) @@ -248,6 +356,11 @@ def cummin(self) -> Series: agg_ops.min_op, bigframes.core.WindowSpec(following=0) ) + def cumprod(self) -> Series: + return self._apply_window_op( + agg_ops.product_op, bigframes.core.WindowSpec(following=0) + ) + def shift(self, periods: int = 1) -> Series: window = bigframes.core.WindowSpec( preceding=periods if periods > 0 else None, @@ -255,8 +368,8 @@ def shift(self, periods: int = 1) -> Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) - def diff(self) -> Series: - return self - self.shift(1) + def diff(self, periods: int = 1) -> Series: + return self - self.shift(periods=periods) def rank( self, @@ -337,64 +450,66 @@ def notna(self) -> "Series": notnull = notna - def __and__(self, other: bool | int | Series | pandas.Series) -> Series: + def __and__(self, other: bool | int | Series) -> Series: return self._apply_binary_op(other, ops.and_op) __rand__ = __and__ - def __or__(self, other: bool | int | Series | pandas.Series) -> Series: + def __or__(self, other: bool | int | Series) -> Series: return self._apply_binary_op(other, ops.or_op) __ror__ = __or__ - def __add__(self, other: float | int | Series | pandas.Series) -> Series: + def __add__(self, other: float | int | Series) -> Series: return self.add(other) - def __radd__(self, other: float | int | Series | pandas.Series) -> Series: + def __radd__(self, other: float | int | Series) -> Series: return self.radd(other) - def add(self, other: float | int | Series | pandas.Series) -> Series: + def add(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.add_op) - def radd(self, other: float | int | Series | pandas.Series) -> Series: + def radd(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.reverse(ops.add_op)) - def __sub__(self, other: float | int | Series | pandas.Series) -> Series: + def __sub__(self, other: float | int | Series) -> Series: return self.sub(other) - def __rsub__(self, other: float | int | Series | pandas.Series) -> Series: + def __rsub__(self, other: float | int | Series) -> Series: return self.rsub(other) - def sub(self, other: float | int | Series | pandas.Series) -> Series: + def sub(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.sub_op) - def rsub(self, other: float | int | Series | pandas.Series) -> Series: + def rsub(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.reverse(ops.sub_op)) - def __mul__(self, other: float | int | Series | pandas.Series) -> Series: + subtract = sub + + def __mul__(self, other: float | int | Series) -> Series: return self.mul(other) - def __rmul__(self, other: float | int | Series | pandas.Series) -> Series: + def __rmul__(self, other: float | int | Series) -> Series: return self.rmul(other) - def mul(self, other: float | int | Series | pandas.Series) -> Series: + def mul(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.mul_op) - def rmul(self, other: float | int | Series | pandas.Series) -> Series: + def rmul(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.reverse(ops.mul_op)) multiply = mul - def __truediv__(self, other: float | int | Series | pandas.Series) -> Series: + def __truediv__(self, other: float | int | Series) -> Series: return self.truediv(other) - def __rtruediv__(self, other: float | int | Series | pandas.Series) -> Series: + def __rtruediv__(self, other: float | int | Series) -> Series: return self.rtruediv(other) - def truediv(self, other: float | int | Series | pandas.Series) -> Series: + def truediv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.div_op) - def rtruediv(self, other: float | int | Series | pandas.Series) -> Series: + def rtruediv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.reverse(ops.div_op)) div = truediv @@ -403,22 +518,22 @@ def rtruediv(self, other: float | int | Series | pandas.Series) -> Series: rdiv = rtruediv - def __floordiv__(self, other: float | int | Series | pandas.Series) -> Series: + def __floordiv__(self, other: float | int | Series) -> Series: return self.floordiv(other) - def __rfloordiv__(self, other: float | int | Series | pandas.Series) -> Series: + def __rfloordiv__(self, other: float | int | Series) -> Series: return self.rfloordiv(other) - def floordiv(self, other: float | int | Series | pandas.Series) -> Series: + def floordiv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.floordiv_op) - def rfloordiv(self, other: float | int | Series | pandas.Series) -> Series: + def rfloordiv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.reverse(ops.floordiv_op)) - def __lt__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + def __lt__(self, other: float | int | Series) -> Series: # type: ignore return self.lt(other) - def __le__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + def __le__(self, other: float | int | Series) -> Series: # type: ignore return self.le(other) def lt(self, other) -> Series: @@ -427,10 +542,10 @@ def lt(self, other) -> Series: def le(self, other) -> Series: return self._apply_binary_op(other, ops.le_op) - def __gt__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + def __gt__(self, other: float | int | Series) -> Series: # type: ignore return self.gt(other) - def __ge__(self, other: float | int | Series | pandas.Series) -> Series: # type: ignore + def __ge__(self, other: float | int | Series) -> Series: # type: ignore return self.ge(other) def gt(self, other) -> Series: @@ -451,6 +566,16 @@ def mod(self, other) -> Series: # type: ignore def rmod(self, other) -> Series: # type: ignore return self._apply_binary_op(other, ops.reverse(ops.mod_op)) + def divmod(self, other) -> Tuple[Series, Series]: # type: ignore + # TODO(huanc): when self and other both has dtype int and other contains zeros, + # the output should be dtype float, both floordiv and mod returns dtype int in this case. + return (self.floordiv(other), self.mod(other)) + + def rdivmod(self, other) -> Tuple[Series, Series]: # type: ignore + # TODO(huanc): when self and other both has dtype int and self contains zeros, + # the output should be dtype float, both floordiv and mod returns dtype int in this case. + return (self.rfloordiv(other), self.rmod(other)) + def __matmul__(self, other): return (self * other).sum() @@ -503,11 +628,48 @@ def _central_moment(self, n: int) -> float: delta_power = delta_power * mean_deltas return delta_power.mean() - def kurt(self) -> float: - # TODO(tbergeron): Cache intermediate count/moment/etc. statistics at block level + def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: + if _is_list_like(func): + if self.dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES: + raise NotImplementedError( + f"Multiple aggregations only supported on numeric series. {constants.FEEDBACK_LINK}" + ) + aggregations = [agg_ops.AGGREGATIONS_LOOKUP[f] for f in func] + return Series( + self._block.summarize( + [self._value_column], + aggregations, + ) + ) + else: + + return self._apply_aggregation( + agg_ops.AGGREGATIONS_LOOKUP[typing.cast(str, func)] + ) + + def skew(self): + count = self.count() + if count < 3: + return pandas.NA + + moment3 = self._central_moment(3) + moment2 = self.var() * (count - 1) / count # Convert sample var to pop var + + # See G1 estimator: + # https://en.wikipedia.org/wiki/Skewness#Sample_skewness + numerator = moment3 + denominator = moment2 ** (3 / 2) + adjustment = (count * (count - 1)) ** 0.5 / (count - 2) + + return (numerator / denominator) * adjustment + + def kurt(self): count = self.count() + if count < 4: + return pandas.NA + moment4 = self._central_moment(4) - moment2 = self._central_moment(2) # AKA: Population Variance + moment2 = self.var() * (count - 1) / count # Convert sample var to pop var # Kurtosis is often defined as the second standardize moment: moment(4)/moment(2)**2 # Pandas however uses Fisher’s estimator, implemented below @@ -523,8 +685,8 @@ def mode(self) -> Series: block = self._block # Approach: Count each value, return each value for which count(x) == max(counts)) block, agg_ids = block.aggregate( - [self._value_column], - ((self._value_column, agg_ops.count_op),), + by_column_ids=[self._value_column], + aggregations=((self._value_column, agg_ops.count_op),), as_index=False, ) value_count_col_id = agg_ids[0] @@ -551,6 +713,13 @@ def mode(self) -> Series: def mean(self) -> float: return typing.cast(float, self._apply_aggregation(agg_ops.mean_op)) + def median(self, *, exact: bool = False) -> float: + if exact: + raise NotImplementedError( + f"Only approximate median is supported. {constants.FEEDBACK_LINK}" + ) + return typing.cast(float, self._apply_aggregation(agg_ops.median_op)) + def sum(self) -> float: return typing.cast(float, self._apply_aggregation(agg_ops.sum_op)) @@ -622,12 +791,26 @@ def argmin(self) -> scalars.Scalar: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) - def __getitem__(self, indexer: Series): + def __getitem__(self, indexer): # TODO: enforce stricter alignment, should fail if indexer is missing any keys. - (left, right, block) = self._align(indexer, "left") - block = block.filter(right) - block = block.select_column(left) - return Series(block) + use_iloc = ( + isinstance(indexer, slice) + and all( + isinstance(x, numbers.Integral) or (x is None) + for x in [indexer.start, indexer.stop, indexer.step] + ) + ) or ( + isinstance(indexer, numbers.Integral) + and not isinstance(self._block.index.dtypes[0], pandas.Int64Dtype) + ) + if use_iloc: + return self.iloc[indexer] + if isinstance(indexer, Series): + (left, right, block) = self._align(indexer, "left") + block = block.filter(right) + block = block.select_column(left) + return Series(block) + return self.loc[indexer] def __getattr__(self, key: str): if hasattr(pandas.Series, key): @@ -635,11 +818,7 @@ def __getattr__(self, key: str): textwrap.dedent( f""" BigQuery DataFrames has not yet implemented an equivalent to - 'pandas.Series.{key}'. Please check - https://github.com/googleapis/python-bigquery-dataframes/issues for - existing feature requests, or file your own. - Please include information about your use case, as well as - relevant code snippets. + 'pandas.Series.{key}'. {constants.FEEDBACK_LINK} """ ) ) @@ -652,12 +831,7 @@ def _align3(self, other1: Series | scalars.Scalar, other2: Series | scalars.Scal return (values[0], values[1], values[2], index) def _apply_aggregation(self, op: agg_ops.AggregateOp) -> Any: - aggregation_result = typing.cast( - ibis_types.Scalar, op._as_ibis(self[self.notnull()]._to_ibis_expr()) - ) - return bigframes.core.scalar.DeferredScalar( - aggregation_result, self._block._expr._session - ).compute() + return self._block.get_stat(self._value_column, op) def _apply_window_op( self, @@ -687,7 +861,9 @@ def value_counts( ) return Series(block) - def sort_values(self, *, axis=0, ascending=True, na_position="last") -> Series: + def sort_values( + self, *, axis=0, ascending=True, kind: str = "quicksort", na_position="last" + ) -> Series: if na_position not in ["first", "last"]: raise ValueError("Param na_position must be one of 'first' or 'last'") direction = OrderingDirection.ASC if ascending else OrderingDirection.DESC @@ -698,7 +874,8 @@ def sort_values(self, *, axis=0, ascending=True, na_position="last") -> Series: direction=direction, na_last=(na_position == "last"), ) - ] + ], + stable=kind in STABLE_SORTS, ) return Series(block) @@ -750,6 +927,8 @@ def groupby( raise ValueError("as_index=False only valid with DataFrame") if axis: raise ValueError("No axis named {} for object type Series".format(level)) + if not as_index: + raise ValueError("'as_index'=False only applies to DataFrame") if by is not None: return self._groupby_values(by, dropna) if level is not None: @@ -765,7 +944,7 @@ def _groupby_level( return groupby.SeriesGroupBy( self._block, self._value_column, - self._resolve_levels(level), + by_col_ids=self._resolve_levels(level), value_name=self.name, dropna=dropna, ) @@ -805,14 +984,14 @@ def _groupby_values( matches = block.index_name_to_col_id.get(key, []) if len(matches) != 1: raise ValueError( - f"GroupBy key {key} does not map to unambiguous index level" + f"GroupBy key {key} does not match a unique index level. BigQuery DataFrames only interprets lists of strings as index level names, not directly as per-row group assignments." ) grouping_cols = [*grouping_cols, matches[0]] return groupby.SeriesGroupBy( block, value_col, - grouping_cols, + by_col_ids=grouping_cols, value_name=self.name, dropna=dropna, ) @@ -851,7 +1030,8 @@ def mask(self, cond, other=None) -> Series: if not isinstance(cond, Series): raise TypeError( - f"Only bigframes series condition is supported, received {type(cond).__name__}" + f"Only bigframes series condition is supported, received {type(cond).__name__}. " + f"{constants.FEEDBACK_LINK}" ) return self.where(~cond, other) @@ -862,13 +1042,13 @@ def to_frame(self) -> bigframes.dataframe.DataFrame: def to_csv(self, path_or_buf=None, **kwargs) -> typing.Optional[str]: # TODO(b/280651142): Implement version that leverages bq export native csv support to bypass local pandas step. - return self.compute().to_csv(path_or_buf, **kwargs) + return self.to_pandas().to_csv(path_or_buf, **kwargs) def to_dict(self, into: type[dict] = dict) -> typing.Mapping: - return typing.cast(dict, self.compute().to_dict(into)) + return typing.cast(dict, self.to_pandas().to_dict(into)) def to_excel(self, excel_writer, sheet_name="Sheet1", **kwargs) -> None: - return self.compute().to_excel(excel_writer, sheet_name, **kwargs) + return self.to_pandas().to_excel(excel_writer, sheet_name, **kwargs) def to_json( self, @@ -879,17 +1059,17 @@ def to_json( **kwargs, ) -> typing.Optional[str]: # TODO(b/280651142): Implement version that leverages bq export native csv support to bypass local pandas step. - return self.compute().to_json(path_or_buf, **kwargs) + return self.to_pandas().to_json(path_or_buf, **kwargs) def to_latex( self, buf=None, columns=None, header=True, index=True, **kwargs ) -> typing.Optional[str]: - return self.compute().to_latex( + return self.to_pandas().to_latex( buf, columns=columns, header=header, index=index, **kwargs ) def tolist(self) -> list: - return self.compute().to_list() + return self.to_pandas().to_list() to_list = tolist @@ -900,17 +1080,17 @@ def to_markdown( index: bool = True, **kwargs, ) -> typing.Optional[str]: - return self.compute().to_markdown(buf, mode=mode, index=index, **kwargs) # type: ignore + return self.to_pandas().to_markdown(buf, mode=mode, index=index, **kwargs) # type: ignore def to_numpy( self, dtype=None, copy=False, na_value=None, **kwargs ) -> numpy.ndarray: - return self.compute().to_numpy(dtype, copy, na_value, **kwargs) + return self.to_pandas().to_numpy(dtype, copy, na_value, **kwargs) __array__ = to_numpy def to_pickle(self, path, **kwargs) -> None: - return self.compute().to_pickle(path, **kwargs) + return self.to_pandas().to_pickle(path, **kwargs) def to_string( self, @@ -925,7 +1105,7 @@ def to_string( max_rows=None, min_rows=None, ) -> typing.Optional[str]: - return self.compute().to_string( + return self.to_pandas().to_string( buf, na_rep, float_format, @@ -939,7 +1119,7 @@ def to_string( ) def to_xarray(self): - return self.compute().to_xarray() + return self.to_pandas().to_xarray() # Keep this at the bottom of the Series class to avoid # confusing type checker by overriding str diff --git a/bigframes/session.py b/bigframes/session.py index 9590ecb8c77..28a38f93073 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -46,6 +46,7 @@ import google.cloud.bigquery as bigquery import google.cloud.bigquery_connection_v1 import google.cloud.bigquery_storage_v1 +import google.cloud.functions_v2 import google.cloud.storage as storage # type: ignore import ibis import ibis.backends.bigquery as ibis_bigquery @@ -56,15 +57,21 @@ import pydata_google_auth import bigframes._config.bigquery_options as bigquery_options +import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.guid as guid -from bigframes.core.ordering import OrderingColumnReference +import bigframes.core.io as bigframes_io +from bigframes.core.ordering import IntegerEncoding, OrderingColumnReference import bigframes.dataframe as dataframe import bigframes.formatting_helpers as formatting_helpers -import bigframes.ml.loader +from bigframes.remote_function import read_gbq_function as bigframes_rgf from bigframes.remote_function import remote_function as bigframes_rf import bigframes.version + +# Even though the ibis.backends.bigquery.registry import is unused, it's needed +# to register new and replacement ops with the Ibis BigQuery backend. +import third_party.bigframes_vendored.ibis.backends.bigquery.registry # noqa import third_party.bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import third_party.bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet import third_party.bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers @@ -81,13 +88,10 @@ _BIGQUERYCONNECTION_REGIONAL_ENDPOINT = "{location}-bigqueryconnection.googleapis.com" _BIGQUERYSTORAGE_REGIONAL_ENDPOINT = "{location}-bigquerystorage.googleapis.com" -# TODO(swast): Need to connect to regional endpoints when performing remote -# functions operations (BQ Connection API, Cloud Run / Cloud Functions). +_MAX_CLUSTER_COLUMNS = 4 -# pydata-google-auth credentials in case auth credentials are not available -# otherwise -_pydata_google_auth_credentials: Optional[google.auth.credentials.Credentials] = None -_pydata_google_auth_project: Optional[str] = None +# TODO(swast): Need to connect to regional endpoints when performing remote +# functions operations (BQ Connection IAM, Cloud Run / Cloud Functions). logger = logging.getLogger(__name__) @@ -97,53 +101,11 @@ def _is_query(query_or_table: str) -> bool: return re.search(r"\s", query_or_table.strip(), re.MULTILINE) is not None -# TODO(shobs): Remove it after the same is available via pydata-google-auth -# after https://github.com/pydata/pydata-google-auth/pull/71 is merged, released -# and upgraded in the google colab image. -def _ensure_application_default_credentials_in_colab_environment(): - # This is a special handling for google colab environment where we want to - # use the colab specific authentication flow - # https://github.com/googlecolab/colabtools/blob/3c8772efd332289e1c6d1204826b0915d22b5b95/google/colab/auth.py#L209 - try: - from google.colab import auth - - auth.authenticate_user() - except Exception: - # We are catching a broad exception class here because we want to be - # agnostic to anything that could internally go wrong in the google - # colab auth. Some of the known exception we want to pass on are: - # - # ModuleNotFoundError: No module named 'google.colab' - # ImportError: cannot import name 'auth' from 'google.cloud' - # MessageError: Error: credential propagation was unsuccessful - # - # The MessageError happens on Vertex Colab when it fails to resolve auth - # from the Compute Engine Metadata server. - pass - - -pydata_google_auth.auth._ensure_application_default_credentials_in_colab_environment = ( - _ensure_application_default_credentials_in_colab_environment -) - - def _get_default_credentials_with_project(): - global _pydata_google_auth_credentials, _pydata_google_auth_project - if not _pydata_google_auth_credentials or not _pydata_google_auth_credentials.valid: - # We want to initiate auth via a non-local web server which - # particularly helps in a cloud notebook environment where the - # machine running the notebook UI and the VM running the notebook - # runtime are not the same. - # TODO(shobs, b/278903498): Use BigQuery DataFrames's own client id - # and secret - ( - _pydata_google_auth_credentials, - _pydata_google_auth_project, - ) = pydata_google_auth.default(_SCOPES, use_local_webserver=False) - return _pydata_google_auth_credentials, _pydata_google_auth_project + return pydata_google_auth.default(scopes=_SCOPES, use_local_webserver=False) -def _create_bq_clients( +def _create_cloud_clients( project: Optional[str], location: Optional[str], use_regional_endpoints: Optional[bool], @@ -152,6 +114,7 @@ def _create_bq_clients( bigquery.Client, google.cloud.bigquery_connection_v1.ConnectionServiceClient, google.cloud.bigquery_storage_v1.BigQueryReadClient, + google.cloud.functions_v2.FunctionServiceClient, ]: """Create and initialize BigQuery client objects.""" @@ -170,7 +133,10 @@ def _create_bq_clients( ) if not project: - raise ValueError("Project must be set to initialize BigQuery client.") + raise ValueError( + "Project must be set to initialize BigQuery client. " + "Try setting `bigframes.options.bigquery.project` first." + ) if use_regional_endpoints: bq_options = google.api_core.client_options.ClientOptions( @@ -193,6 +159,7 @@ def _create_bq_clients( client_options=bq_options, credentials=credentials, project=project, + location=location, ) bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( @@ -213,7 +180,15 @@ def _create_bq_clients( credentials=credentials, ) - return bqclient, bqconnectionclient, bqstorageclient + functions_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + cloudfunctionsclient = google.cloud.functions_v2.FunctionServiceClient( + client_info=functions_info, + credentials=credentials, + ) + + return bqclient, bqconnectionclient, bqstorageclient, cloudfunctionsclient class Session( @@ -242,7 +217,8 @@ def __init__(self, context: Optional[bigquery_options.BigQueryOptions] = None): self.bqclient, self.bqconnectionclient, self.bqstorageclient, - ) = _create_bq_clients( + self.cloudfunctionsclient, + ) = _create_cloud_clients( project=context.project, location=self._location, use_regional_endpoints=context.use_regional_endpoints, @@ -310,8 +286,23 @@ def close(self): 24 hours of inactivity or after 7 days.""" if self._session_id is not None and self.bqclient is not None: abort_session_query = "CALL BQ.ABORT_SESSION('{}')".format(self._session_id) - query_job = self.bqclient.query(abort_session_query) - query_job.result() # blocks until finished + try: + query_job = self.bqclient.query(abort_session_query) + query_job.result() # blocks until finished + except google.api_core.exceptions.BadRequest as e: + # Ignore the exception when the BQ session itself has expired + # https://cloud.google.com/bigquery/docs/sessions-terminating#auto-terminate_a_session + if not e.message.startswith( + f"Session {self._session_id} has expired and is no longer available." + ): + raise + except google.auth.exceptions.RefreshError: + # The refresh token may itself have been invalidated or expired + # https://developers.google.com/identity/protocols/oauth2#expiration + # Don't raise the exception in this case while closing the + # BigFrames session, so that the end user has a path for getting + # out of a bad session due to unusable credentials. + pass self._session_id = None def read_gbq( @@ -367,16 +358,10 @@ def read_gbq_query( else: index_cols = list(index_col) - # Make sure we cluster by the index column so that subsequent - # operations are as speedy as they can be. - if index_cols: - destination: bigquery.Table | bigquery.TableReference = ( - self._query_to_session_table(query, index_cols) - ) - else: - _, query_job = self._start_query(query) - query_job.result() # Wait for job to finish. - destination = query_job.destination + # Can't cluster since don't know if index_cols are clusterable data types + # TODO(tbergeron): Maybe use dryrun to determine types of index_cols to see if can cluster + _, query_job = self._start_query(query) + destination = query_job.destination # If there was no destination table, that means the query must have # been DDL or DML. Return some job metadata, instead. @@ -411,6 +396,9 @@ def read_gbq_table( See also: :meth:`Session.read_gbq`. """ + if max_results and max_results <= 0: + raise ValueError("`max_results` should be a positive number.") + # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so # these docstrings are inline. # TODO(swast): Can we re-use the temp table from other reads in the @@ -425,7 +413,6 @@ def read_gbq_table( f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" ) else: - # TODO(swast): Read from a table snapshot so that reads are consistent. table_expression = self.ibis_client.table( table_ref.table_id, database=f"{table_ref.project}.{table_ref.dataset_id}", @@ -442,6 +429,8 @@ def read_gbq_table( else: index_cols = list(index_col) + hidden_cols: typing.Sequence[str] = () + for key in index_cols: if key not in table_expression.columns: raise ValueError( @@ -466,7 +455,7 @@ def read_gbq_table( SELECT (SELECT COUNT(*) FROM full_table) AS total_count, (SELECT COUNT(*) FROM distinct_table) AS distinct_count """ - results, _ = self._start_query(is_unique_sql) + results, query_job = self._start_query(is_unique_sql) row = next(iter(results)) total_count = row["total_count"] @@ -476,9 +465,24 @@ def read_gbq_table( ordering_value_columns=[ core.OrderingColumnReference(column_id) for column_id in index_cols ], + total_ordering_columns=frozenset(index_cols), ) - if not is_total_ordering: + # We have a total ordering, so query via "time travel" so that + # the underlying data doesn't mutate. + if is_total_ordering: + + # Get the timestamp from the job metadata rather than the query + # text so that the query for determining uniqueness of the ID + # columns can be cached. + current_timestamp = query_job.started + + # The job finished, so we should have a start time. + assert current_timestamp is not None + table_expression = self.ibis_client.sql( + bigframes_io.create_snapshot_sql(table_ref, current_timestamp) + ) + else: # Make sure when we generate an ordering, the row_number() # coresponds to the index columns. table_expression = table_expression.order_by(index_cols) @@ -491,26 +495,37 @@ def read_gbq_table( """, ) ) + + # When ordering by index columns, apply limit after ordering to + # make limit more predictable. + if max_results is not None: + table_expression = table_expression.limit(max_results) else: + if max_results is not None: + # Apply limit before generating rownums and creating temp table + # This makes sure the offsets are valid and limits the number of + # rows for which row numbers must be generated + table_expression = table_expression.limit(max_results) table_expression, ordering = self._create_sequential_ordering( table_expression ) - ordering_id_column = ordering.ordering_id - assert ordering_id_column is not None + hidden_cols = ( + (ordering.total_order_col.column_id,) + if ordering.total_order_col + else () + ) + assert len(ordering.ordering_value_columns) > 0 is_total_ordering = True - index_cols = [ordering_id_column] - index_labels = [None] - - if max_results is not None: - if max_results <= 0: - raise ValueError("`max_results` should be a positive number.") - table_expression = table_expression.limit(max_results) + # Block constructor will generate default index if passed empty + index_cols = [] + index_labels = [] return self._read_gbq_with_ordering( table_expression=table_expression, col_order=col_order, index_cols=index_cols, index_labels=index_labels, + hidden_cols=hidden_cols, ordering=ordering, is_total_ordering=is_total_ordering, ) @@ -522,16 +537,23 @@ def _read_gbq_with_ordering( col_order: Iterable[str] = (), index_cols: Sequence[str] = (), index_labels: Sequence[Optional[str]] = (), + hidden_cols: Sequence[str] = (), ordering: core.ExpressionOrdering, is_total_ordering: bool = False, ) -> dataframe.DataFrame: """Internal helper method that loads DataFrame from Google BigQuery given an ordering column. Args: - table_expression: an ibis table expression to be executed in BigQuery. - col_order: List of BigQuery column names in the desired order for results DataFrame. - index_cols: List of column names to use as the index or multi-index. - ordering: Column name to be used for ordering. If not supplied, a default ordering is generated. + table_expression: + an ibis table expression to be executed in BigQuery. + col_order: + List of BigQuery column names in the desired order for results DataFrame. + index_cols: + List of column names to use as the index or multi-index. + hidden_cols: + Columns that should be hidden. Ordering columns may (not always) be hidden + ordering: + Column name to be used for ordering. If not supplied, a default ordering is generated. Returns: A DataFrame representing results of the query or table. @@ -542,30 +564,23 @@ def _read_gbq_with_ordering( f"Got {len(index_labels)}, expected {len(index_cols)}." ) - if not index_cols: - raise ValueError("Need at least 1 index column.") - # Logic: # no total ordering, index -> create sequential order, ordered by index, use for both ordering and index # total ordering, index -> use ordering as ordering, index as index # This code block ensures the existence of a total ordering. + column_keys = list(col_order) + if len(column_keys) == 0: + non_value_columns = set([*index_cols, *hidden_cols]) + column_keys = [ + key for key in table_expression.columns if key not in non_value_columns + ] if not is_total_ordering: # Rows are not ordered, we need to generate a default ordering and materialize it table_expression, ordering = self._create_sequential_ordering( table_expression, index_cols ) - index_col_values = [table_expression[index_id] for index_id in index_cols] - - column_keys = list(col_order) - if len(column_keys) == 0: - non_columns = set(index_cols) - if ordering.ordering_id is not None: - non_columns.add(ordering.ordering_id) - column_keys = [ - key for key in table_expression.columns if key not in non_columns - ] return self._read_ibis( table_expression, index_col_values, @@ -589,7 +604,7 @@ def _read_bigquery_load_job( index_cols = list(index_col) if not job_config.clustering_fields and index_cols: - job_config.clustering_fields = index_cols + job_config.clustering_fields = index_cols[:_MAX_CLUSTER_COLUMNS] if isinstance(filepath_or_buffer, str): if filepath_or_buffer.startswith("gs://"): @@ -606,7 +621,7 @@ def _read_bigquery_load_job( filepath_or_buffer, table, job_config=job_config ) - load_job.result() # Wait for the job to complete + self._start_generic_job(load_job) # The BigQuery REST API for tables.get doesn't take a session ID, so we # can't get the schema for a temp table that way. @@ -622,12 +637,9 @@ def _read_ibis( index_cols: Sequence[ibis_types.Value], index_labels: Sequence[Optional[str]], column_keys: Sequence[str], - ordering: Optional[core.ExpressionOrdering] = None, - ): + ordering: core.ExpressionOrdering, + ) -> dataframe.DataFrame: """Turns a table expression (plus index column) into a DataFrame.""" - hidden_ordering_columns = None - if ordering is not None and ordering.ordering_id is not None: - hidden_ordering_columns = (table_expression[ordering.ordering_id],) columns = list(index_cols) for key in column_keys: @@ -635,6 +647,12 @@ def _read_ibis( raise ValueError(f"Column '{key}' not found in this table.") columns.append(table_expression[key]) + non_hidden_ids = [col.get_name() for col in columns] + hidden_ordering_columns = [] + for ref in ordering.all_ordering_columns: + if ref.column_id not in non_hidden_ids: + hidden_ordering_columns.append(table_expression[ref.column_id]) + block = blocks.Block( core.ArrayValue( self, table_expression, columns, hidden_ordering_columns, ordering @@ -646,16 +664,19 @@ def _read_ibis( return dataframe.DataFrame(block) def read_gbq_model(self, model_name: str): - """Loads a BQML model from Google BigQuery. + """Loads a BigQuery ML model from BigQuery. Args: - model_name : the model's name in BigQuery in the format - `project_id.dataset_id.model_id`, or just `dataset_id.model_id` - to load from the default project. + model_name (str): + the model's name in BigQuery in the format + `project_id.dataset_id.model_id`, or just `dataset_id.model_id` + to load from the default project. Returns: A bigframes.ml Model wrapping the model. """ + import bigframes.ml.loader + model_ref = bigquery.ModelReference.from_string( model_name, default_project=self.bqclient.project ) @@ -663,16 +684,17 @@ def read_gbq_model(self, model_name: str): return bigframes.ml.loader.from_bq(self, model) def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame: - """Loads DataFrame from a Pandas DataFrame. + """Loads DataFrame from a pandas DataFrame. - The Pandas DataFrame will be persisted as a temporary BigQuery table, which can be + The pandas DataFrame will be persisted as a temporary BigQuery table, which can be automatically recycled after the Session is closed. Args: - pandas_dataframe: a Pandas DataFrame object to be loaded. + pandas_dataframe (pandas.DataFrame): + a pandas DataFrame object to be loaded. Returns: - A BigQuery DataFrames. + bigframes.dataframe.DataFrame: The BigQuery DataFrame. """ # Add order column to pandas DataFrame to preserve order in BigQuery ordering_col = "rowid" @@ -686,7 +708,7 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame pandas_dataframe_copy[ordering_col] = np.arange(pandas_dataframe_copy.shape[0]) # Specify the datetime dtypes, which is auto-detected as timestamp types. - schema = [] + schema: list[bigquery.SchemaField] = [] for column, dtype in zip(pandas_dataframe.columns, pandas_dataframe.dtypes): if dtype == "timestamp[us][pyarrow]": schema.append( @@ -699,11 +721,13 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame filter(lambda name: name is not None, pandas_dataframe_copy.index.names) ) index_labels = typing.cast(List[Optional[str]], index_cols) - cluster_cols = index_cols + [ordering_col] + + # Clustering probably not needed anyways as pandas tables are small + cluster_cols = [ordering_col] if len(index_cols) == 0: - index_cols = [ordering_col] - index_labels = [None] + # Block constructor will implicitly build default index + pass job_config = bigquery.LoadJobConfig(schema=schema) job_config.clustering_fields = cluster_cols @@ -718,10 +742,12 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame load_table_destination, job_config=job_config, ) - load_job.result() # Wait for the job to complete + self._start_generic_job(load_job) ordering = core.ExpressionOrdering( - ordering_id_column=OrderingColumnReference(ordering_col), is_sequential=True + ordering_value_columns=[OrderingColumnReference(ordering_col)], + total_ordering_columns=frozenset([ordering_col]), + integer_encoding=IntegerEncoding(True, is_sequential=True), ) table_expression = self.ibis_client.sql( f"SELECT * FROM `{load_table_destination.table_id}`" @@ -731,6 +757,7 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame table_expression=table_expression, index_cols=index_cols, index_labels=index_labels, + hidden_cols=(ordering_col,), ordering=ordering, is_total_ordering=True, ) @@ -771,14 +798,16 @@ def read_csv( if any(param is not None for param in (dtype, names)): not_supported = ("dtype", "names") raise NotImplementedError( - f"BigQuery engine does not support these arguments: {not_supported}" + f"BigQuery engine does not support these arguments: {not_supported}. " + f"{constants.FEEDBACK_LINK}" ) if index_col is not None and ( not index_col or not isinstance(index_col, str) ): raise NotImplementedError( - "BigQuery engine only supports a single column name for `index_col`." + "BigQuery engine only supports a single column name for `index_col`. " + f"{constants.FEEDBACK_LINK}" ) # None value for index_col cannot be passed to read_gbq @@ -794,13 +823,15 @@ def read_csv( col_order = tuple(col for col in usecols) else: raise NotImplementedError( - "BigQuery engine only supports an iterable of strings for `usecols`." + "BigQuery engine only supports an iterable of strings for `usecols`. " + f"{constants.FEEDBACK_LINK}" ) valid_encodings = {"UTF-8", "ISO-8859-1"} if encoding is not None and encoding not in valid_encodings: raise NotImplementedError( - f"BigQuery engine only supports the following encodings: {valid_encodings}" + f"BigQuery engine only supports the following encodings: {valid_encodings}. " + f"{constants.FEEDBACK_LINK}" ) job_config = bigquery.LoadJobConfig() @@ -830,7 +861,8 @@ def read_csv( else: if any(arg in kwargs for arg in ("chunksize", "iterator")): raise NotImplementedError( - "'chunksize' and 'iterator' arguments are not supported." + "'chunksize' and 'iterator' arguments are not supported. " + f"{constants.FEEDBACK_LINK}" ) if isinstance(filepath_or_buffer, str): @@ -904,20 +936,32 @@ def _create_sequential_ordering( ibis.row_number().cast(ibis_dtypes.int64).name(default_ordering_name) ) table = table.mutate(**{default_ordering_name: default_ordering_col}) + clusterable_index_cols = [ + col for col in index_cols if _can_cluster(table[col].type()) + ] + cluster_cols = (clusterable_index_cols + [default_ordering_name])[ + :_MAX_CLUSTER_COLUMNS + ] table_ref = self._query_to_session_table( self.ibis_client.compile(table), - cluster_cols=list(index_cols) + [default_ordering_name], + cluster_cols=cluster_cols, ) table = self.ibis_client.sql(f"SELECT * FROM `{table_ref.table_id}`") ordering_reference = core.OrderingColumnReference(default_ordering_name) ordering = core.ExpressionOrdering( - ordering_id_column=ordering_reference, is_sequential=True + ordering_value_columns=[ordering_reference], + total_ordering_columns=frozenset([default_ordering_name]), + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), ) return table, ordering def _query_to_session_table( self, query_text: str, cluster_cols: Iterable[str] ) -> bigquery.TableReference: + if len(list(cluster_cols)) > _MAX_CLUSTER_COLUMNS: + raise ValueError( + f"Too many cluster columns: {list(cluster_cols)}, max {_MAX_CLUSTER_COLUMNS} allowed." + ) # Can't set a table in _SESSION as destination via query job API, so we # run DDL, instead. table = self._create_session_table() @@ -929,9 +973,8 @@ def _query_to_session_table( CLUSTER BY {cluster_cols_sql} AS {query_text} """ - query_job = self.bqclient.query(ddl_text) try: - query_job.result() # Wait for the job to complete + self._start_query(ddl_text) # Wait for the job to complete except google.api_core.exceptions.Conflict: # Allow query retry to succeed. pass @@ -947,21 +990,58 @@ def remote_function( ): """Decorator to turn a user defined function into a BigQuery remote function. + .. note:: + Please make sure following is setup before using this API: + + 1. Have the below APIs enabled for your project: + + * BigQuery Connection API + * Cloud Functions API + * Cloud Run API + * Cloud Build API + * Artifact Registry API + * Cloud Resource Manager API + + This can be done from the cloud console (change `PROJECT_ID` to yours): + https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID + + Or from the gcloud CLI: + + `$ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com` + + 2. Have following IAM roles enabled for you: + + * BigQuery Data Editor (roles/bigquery.dataEditor) + * BigQuery Connection Admin (roles/bigquery.connectionAdmin) + * Cloud Functions Developer (roles/cloudfunctions.developer) + * Service Account User (roles/iam.serviceAccountUser) + * Storage Object Viewer (roles/storage.objectViewer) + * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) + + 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: + + 1. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection + 2. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + + Alternatively, the IAM could also be setup via the gcloud CLI: + + `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. + Args: input_types (list(type)): List of input data types in the user defined function. output_type (type): Data type of the output in the user defined function. dataset (str, Optional): - Dataset to use to create a BigQuery function. It should be in + Dataset in which to create a BigQuery remote function. It should be in `.` or `` format. If this - param is not provided then session dataset id would be used. + parameter is not provided then session dataset id is used. bigquery_connection (str, Optional): - Name of the BigQuery connection. If it is pre created in the same - location as the `bigquery_client.location` then it would be used, - otherwise it would be created dynamically assuming the user has - necessary priviliges. If this param is not provided then the - bigquery connection from the session would be used. + Name of the BigQuery connection. You should either have the + connection already created in the `location` you have chosen, or + you should have the Project IAM Admin role to enable the service + to create the connection for you if you need it.If this parameter is + not provided then the BigQuery connection from the session is used. reuse (bool, Optional): Reuse the remote function if already exists. `True` by default, which will result in reusing an existing remote @@ -969,38 +1049,14 @@ def remote_function( Setting it to false would force creating a unique remote function. If the required remote function does not exist then it would be created irrespective of this param. + Returns: + callable: A remote function object pointing to the cloud assets created + in the background to support the remote execution. The cloud assets can be + located through the following properties set in the object: - Notes: - Please make sure following is setup before using this API: - - 1. Have the below APIs enabled for your project: - a. BigQuery Connection API - b. Cloud Functions API - c. Cloud Run API - d. Cloud Build API - e. Artifact Registry API - f. Cloud Resource Manager API - - This can be done from the cloud console (change PROJECT_ID to yours): - https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID - Or from the gcloud CLI: - $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com - - 2. Have following IAM roles enabled for you: - a. BigQuery Data Editor (roles/bigquery.dataEditor) - b. BigQuery Connection Admin (roles/bigquery.connectionAdmin) - c. Cloud Functions Developer (roles/cloudfunctions.developer) - d. Service Account User (roles/iam.serviceAccountUser) - e. Storage Object Viewer (roles/storage.objectViewer) - f. Project IAM Admin (roles/resourcemanager.projectIamAdmin) - (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) - - 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: - a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection - b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function - Alternatively, the IAM could also be setup via the gcloud CLI: - $ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker" + `bigframes_cloud_function` - The google cloud function deployed for the user defined code. + `bigframes_remote_function` - The bigquery remote function capable of calling into `bigframes_cloud_function`. """ return bigframes_rf( input_types, @@ -1011,34 +1067,62 @@ def remote_function( reuse=reuse, ) + def read_gbq_function( + self, + function_name: str, + ): + """Loads a BigQuery function from BigQuery. + + Then it can be applied to a DataFrame or Series. + + Args: + function_name (str): + the function's name in BigQuery in the format + `project_id.dataset_id.function_name`, or + `dataset_id.function_name` to load from the default project, or + `function_name` to load from the default project and the dataset + associated with the current session. + + Returns: + callable: A function object pointing to the BigQuery function read + from BigQuery. + + The object is similar to the one created by the `remote_function` + decorator, including the `bigframes_remote_function` property, but + not including the `bigframes_cloud_function` property. + """ + + return bigframes_rgf( + function_name=function_name, + session=self, + ) + def _start_query( self, sql: str, job_config: Optional[bigquery.job.QueryJobConfig] = None, max_results: Optional[int] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """ + Starts query job and waits for results + """ if job_config is not None: query_job = self.bqclient.query(sql, job_config=job_config) else: query_job = self.bqclient.query(sql) opts = bigframes.options.display - if opts.progress_bar is not None: - results_iterator = formatting_helpers.wait_for_job( + if opts.progress_bar is not None and not query_job.configuration.dry_run: + results_iterator = formatting_helpers.wait_for_query_job( query_job, max_results, opts.progress_bar ) else: results_iterator = query_job.result(max_results=max_results) return results_iterator, query_job - def _extract_table(self, source_table, destination_uris, job_config): - extract_job = self.bqclient.extract_table( - source=source_table, - destination_uris=destination_uris, - job_config=job_config, - ) - extract_job.result() - return extract_job + def _get_table_size(self, destination_table): + table = self.bqclient.get_table(destination_table) + return table.num_bytes def _rows_to_dataframe( self, row_iterator: bigquery.table.RowIterator @@ -1050,6 +1134,27 @@ def _rows_to_dataframe( string_dtype=pandas.StringDtype(storage="pyarrow"), ) + def _start_generic_job(self, job: formatting_helpers.GenericJob): + if bigframes.options.display.progress_bar is not None: + formatting_helpers.wait_for_job( + job, bigframes.options.display.progress_bar + ) # Wait for the job to complete + else: + job.result() + def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: return Session(context) + + +def _can_cluster(ibis_type: ibis_dtypes.DataType): + # https://cloud.google.com/bigquery/docs/clustered-tables + # Notably, float is excluded + return ( + ibis_type.is_integer() + or ibis_type.is_string() + or ibis_type.is_decimal() + or ibis_type.is_date() + or ibis_type.is_timestamp() + or ibis_type.is_boolean() + ) diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst deleted file mode 100644 index cde2a70a3fb..00000000000 --- a/docs/getting_started/index.rst +++ /dev/null @@ -1,27 +0,0 @@ -Getting started -=============== - -Prerequisites --------------- - -* Install the ``bigframes`` package. -* Create a GCP project and billing account. -* When running locally, authenticate with application default credentials. See - the `gcloud auth application-default login - `_ - reference. - -Code sample ------------ - -.. literalinclude:: ../samples/snippets/quickstart.py - :language: python - :dedent: 4 - :start-after: [START bigquery_bigframes_quickstart] - :end-before: [END bigquery_bigframes_quickstart] - -Learn more ----------- - -* See :ref:`user_guide` for more samples. -* See :ref:`bigframes_ml` for more ML samples. diff --git a/docs/index.rst b/docs/index.rst index eb721fbf293..ff1cd09eb7d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,9 +1,11 @@ .. include:: README.rst +API reference +------------- + .. toctree:: :maxdepth: 3 - getting_started/index user_guide/index reference/index diff --git a/docs/reference/bigframes.ml/cluster.rst b/docs/reference/bigframes.ml/cluster.rst index 5456b728b67..e91a28c0511 100644 --- a/docs/reference/bigframes.ml/cluster.rst +++ b/docs/reference/bigframes.ml/cluster.rst @@ -3,3 +3,5 @@ bigframes.ml.cluster .. automodule:: bigframes.ml.cluster :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/compose.rst b/docs/reference/bigframes.ml/compose.rst index e41f17a33b1..9992728362f 100644 --- a/docs/reference/bigframes.ml/compose.rst +++ b/docs/reference/bigframes.ml/compose.rst @@ -3,3 +3,5 @@ bigframes.ml.compose .. automodule:: bigframes.ml.compose :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/decomposition.rst b/docs/reference/bigframes.ml/decomposition.rst index 122dbc2b168..ec804ac8cdc 100644 --- a/docs/reference/bigframes.ml/decomposition.rst +++ b/docs/reference/bigframes.ml/decomposition.rst @@ -3,3 +3,5 @@ bigframes.ml.decomposition .. automodule:: bigframes.ml.decomposition :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/ensemble.rst b/docs/reference/bigframes.ml/ensemble.rst new file mode 100644 index 00000000000..2652ab5aa4d --- /dev/null +++ b/docs/reference/bigframes.ml/ensemble.rst @@ -0,0 +1,7 @@ +bigframes.ml.ensemble +===================== + +.. automodule:: bigframes.ml.ensemble + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/forecasting.rst b/docs/reference/bigframes.ml/forecasting.rst index 037bd7e82ef..04015c99117 100644 --- a/docs/reference/bigframes.ml/forecasting.rst +++ b/docs/reference/bigframes.ml/forecasting.rst @@ -3,3 +3,5 @@ bigframes.ml.forecasting .. automodule:: bigframes.ml.forecasting :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/imported.rst b/docs/reference/bigframes.ml/imported.rst index 54120534c02..c151cbda6f1 100644 --- a/docs/reference/bigframes.ml/imported.rst +++ b/docs/reference/bigframes.ml/imported.rst @@ -3,3 +3,5 @@ bigframes.ml.imported .. automodule:: bigframes.ml.imported :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/index.rst b/docs/reference/bigframes.ml/index.rst index e17edc2ee98..f3cbe1174a7 100644 --- a/docs/reference/bigframes.ml/index.rst +++ b/docs/reference/bigframes.ml/index.rst @@ -13,6 +13,8 @@ API Reference decomposition + ensemble + forecasting imported diff --git a/docs/reference/bigframes.ml/linear_model.rst b/docs/reference/bigframes.ml/linear_model.rst index 5936858e64b..8c6c2765b12 100644 --- a/docs/reference/bigframes.ml/linear_model.rst +++ b/docs/reference/bigframes.ml/linear_model.rst @@ -3,3 +3,5 @@ bigframes.ml.linear_model .. automodule:: bigframes.ml.linear_model :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/llm.rst b/docs/reference/bigframes.ml/llm.rst index 4debef74ffb..20ae7793e73 100644 --- a/docs/reference/bigframes.ml/llm.rst +++ b/docs/reference/bigframes.ml/llm.rst @@ -3,3 +3,5 @@ bigframes.ml.llm .. automodule:: bigframes.ml.llm :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/metrics.rst b/docs/reference/bigframes.ml/metrics.rst index dc951ebed42..aca11f7e9fc 100644 --- a/docs/reference/bigframes.ml/metrics.rst +++ b/docs/reference/bigframes.ml/metrics.rst @@ -3,3 +3,5 @@ bigframes.ml.metrics .. automodule:: bigframes.ml.metrics :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/model_selection.rst b/docs/reference/bigframes.ml/model_selection.rst index b0d4e5ed3ee..d662285f990 100644 --- a/docs/reference/bigframes.ml/model_selection.rst +++ b/docs/reference/bigframes.ml/model_selection.rst @@ -3,3 +3,5 @@ bigframes.ml.model_selection .. automodule:: bigframes.ml.model_selection :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/pipeline.rst b/docs/reference/bigframes.ml/pipeline.rst index 239ee59c431..22e877dc5b3 100644 --- a/docs/reference/bigframes.ml/pipeline.rst +++ b/docs/reference/bigframes.ml/pipeline.rst @@ -3,3 +3,5 @@ bigframes.ml.pipeline .. automodule:: bigframes.ml.pipeline :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/preprocessing.rst b/docs/reference/bigframes.ml/preprocessing.rst index 4d2279fc8ea..eac72da1730 100644 --- a/docs/reference/bigframes.ml/preprocessing.rst +++ b/docs/reference/bigframes.ml/preprocessing.rst @@ -3,3 +3,5 @@ bigframes.ml.preprocessing .. automodule:: bigframes.ml.preprocessing :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes/options.rst b/docs/reference/bigframes/options.rst index 914e4976d54..d831a519fe6 100644 --- a/docs/reference/bigframes/options.rst +++ b/docs/reference/bigframes/options.rst @@ -10,3 +10,5 @@ Options and settings .. autoclass:: bigframes._config.bigquery_options.BigQueryOptions .. autoclass:: bigframes._config.display_options.DisplayOptions + +.. autoclass:: bigframes._config.sampling_options.SamplingOptions diff --git a/docs/user_guide/bigframes.pandas/remote_functions.rst b/docs/user_guide/bigframes.pandas/remote_functions.rst index 3a1a9d342f0..abfe6a10caa 100644 --- a/docs/user_guide/bigframes.pandas/remote_functions.rst +++ b/docs/user_guide/bigframes.pandas/remote_functions.rst @@ -9,32 +9,125 @@ find more details on it via `help` command. .. code-block:: python - import bigframes.pandas as pd - help(pd.remote_function) + import bigframes.pandas as bpd + help(bpd.remote_function) Read a table and inspect the column of interest. .. code-block:: python - df = pd.read_gbq("bigquery-public-data.ml_datasets.penguins") + df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") df["body_mass_g"].head(10) Define a custom function, and specify the intent to turn it into a remote function. It requires a BigQuery connection. If the connection is not already created, BigQuery DataFrames will attempt to create one assuming the necessary -APIs and IAM permissions are setup in the project. +APIs and IAM permissions are setup in the project. In our examples we would be +using a pre-created connection named `bigframes-rf-conn`. Let's try a +`pandas`-like use case in which we want to apply a user defined scalar function +to every value in a `Series`, more specifically bucketize the `body_mass_g` value +of the penguins, which is a real number, into a category, which is a string. .. code-block:: python - @pd.remote_function([float], str, bigquery_connection='bigframes-rf-conn') + @bpd.remote_function([float], str, bigquery_connection='bigframes-rf-conn') def get_bucket(num): if not num: return "NA" boundary = 4000 return "at_or_above_4000" if num >= boundary else "below_4000" -Run the custom function on the column of interest to create a new column. +Then we can apply the remote function on the `Series`` of interest via `apply` +API and store the result in a new column in the DataFrame. .. code-block:: python df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket)) + +This will add a new column `body_mass_bucket` in the DataFrame. You can preview +the original value and the bucketized value side by side. + +.. code-block:: python + df[['body_mass_g', 'body_mass_bucket']].head(10) + +This operation was possible by doing all the computation on the cloud. For that, +there is a google cloud function deployed by serializing the user code. + +.. warning:: + The deployed cloud function may be visible to other users with sufficient + privilege in the project. The user should be careful about having any + sensitive data in the code that will be deployed as a remote function. + +The cloud function can be located from a property set in the remote function object. + +.. code-block:: python + + get_bucket.bigframes_cloud_function + +and then there is a BigQuery remote function created configured to call into the +cloud function via the BigQuery connection. That can also be located from +another property set in the remote function object. + +.. code-block:: python + + get_bucket.bigframes_remote_function + +The cloud assets created are persistant and the user can manage them directy +from the Google Cloud Console. + +Let's continue trying other potential use cases of remote functions. Let's say +we consider the `species`, `island` and `sex` of the penguins sensitive +information and want to redact that by replacing with their hash code instead. +Let's define another scalar custom function and decorated it as a remote function: + +.. code-block:: python + + @bpd.remote_function([str], str, bigquery_connection='bigframes-rf-conn') + def get_hash(input): + import hashlib + # handle missing value + if input is None: + input = "" + encoded_input = input.encode() + hash = hashlib.md5(encoded_input) + return hash.hexdigest() + +We can use this remote function in another `pandas`-like API `map` that can be +applied on a DataFrame: + +.. code-block:: python + + df_redacted = df[["species", "island", "sex"]].map(get_hash) + df_redacted.head(10). + +Using Existing Functions +======================== + +If you have already defined a custom function in BigQuery, either in the +BigQuery Studio or with the `remote_function` decorator above or otherwise, you +may use it with BigQuery DataFrames with the `read_gbq_function` method. + +More details are available via the `help` command: + +.. code-block:: python + + import bigframes.pandas as pd + help(pd.read_gbq_function) + +Here is an example of using `read_gbq_function` to load an existing function +named `get_bucket`: + +.. code-block:: python + + import bigframes.pandas as pd + + df = pd.read_gbq("bigquery-public-data.ml_datasets.penguins") + get_bucket = pd.read_gbq_function("get_bucket") + + df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket)) + df.head(10) + +Note: As mentioned above, if a function is created using the `remote_function` +decorator, its generated name (including project and dataset) is accessible +immediately afterward in the function's `bigframes_remote_function` attribute. +The same string can be passed to `read_gbq_function` later in another context. diff --git a/notebooks/01 - Getting Started.ipynb b/notebooks/01 - Getting Started.ipynb index 132cdf8db0d..473bdd8ceae 100644 --- a/notebooks/01 - Getting Started.ipynb +++ b/notebooks/01 - Getting Started.ipynb @@ -28,7 +28,22 @@ "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5c480b60490940d3a45fa6b9ca2cecdb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 411d90c8-8b22-40b0-ad42-04f9e38c074e is DONE. 0 Bytes processed. body_mass_g\n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", "
sex
tag_number
11720Dream32.115.5188.03050.036.618.4184.03475.0FEMALE
1371Biscoe37.716.0183.03075.0FEMALE1Dream39.819.1184.04650.0MALE
1417Torgersen38.617.0188.02900.02Dream40.918.9184.03900.0MALE
4Dream37.316.8192.03000.0FEMALE
12045Dream43.218.5192.04100.0MALE
9Dream40.220.1200.03975.0MALE
10Dream40.717.0190.03725.040.818.9208.04300.0MALE
1251Biscoe37.617.011Dream39.018.7185.03600.03650.0MALE
12Dream37.016.9185.03000.0FEMALE
1422Torgersen35.717.0189.03350.014Dream34.017.1185.03400.0FEMALE
1394Torgersen40.217.0176.03450.015Dream37.016.5185.03400.0FEMALE
116318Dream39.717.9193.04250.0MALE
19Dream37.818.1193.03750.0MALE
22Dream36.417.0195.03325.040.217.1193.03400.0FEMALE
1329Biscoe38.117.0181.03175.023Dream36.818.5193.03500.0FEMALE
1406Torgersen44.118.0210.026Dream41.518.5201.04000.0MALE
119631Dream36.518.0182.03150.033.116.1178.02900.0FEMALE
1228Biscoe41.618.0192.03950.032Dream37.218.1178.03900.0MALE
1412Torgersen40.318.0195.033Dream39.516.7178.03250.0FEMALE
114235Dream35.718.0202.03550.036.018.5186.03100.0FEMALE
1430Torgersen33.519.0190.03600.0FEMALE36Dream39.618.1186.04450.0MALE
1333Biscoe43.219.0197.04775.038Dream41.320.3194.03550.0MALE
1414Torgersen38.719.0195.03450.041Dream35.718.0202.03550.0FEMALE
119751Dream41.119.0182.038.117.6187.03425.0MALE
1443Torgersen40.619.0199.04000.0MALEFEMALE
1295Biscoe41.020.0203.04725.0MALE53Dream36.017.1187.03700.0FEMALE
\n", + "

25 rows × 6 columns

\n", "[146 rows x 6 columns in total]" ], "text/plain": [ - " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", - "tag_number \n", - "1172 Dream 32.1 15.5 188.0 \n", - "1371 Biscoe 37.7 16.0 183.0 \n", - "1417 Torgersen 38.6 17.0 188.0 \n", - "1204 Dream 40.7 17.0 190.0 \n", - "1251 Biscoe 37.6 17.0 185.0 \n", - "1422 Torgersen 35.7 17.0 189.0 \n", - "1394 Torgersen 40.2 17.0 176.0 \n", - "1163 Dream 36.4 17.0 195.0 \n", - "1329 Biscoe 38.1 17.0 181.0 \n", - "1406 Torgersen 44.1 18.0 210.0 \n", - "1196 Dream 36.5 18.0 182.0 \n", - "1228 Biscoe 41.6 18.0 192.0 \n", - "1412 Torgersen 40.3 18.0 195.0 \n", - "1142 Dream 35.7 18.0 202.0 \n", - "1430 Torgersen 33.5 19.0 190.0 \n", - "1333 Biscoe 43.2 19.0 197.0 \n", - "1414 Torgersen 38.7 19.0 195.0 \n", - "1197 Dream 41.1 19.0 182.0 \n", - "1443 Torgersen 40.6 19.0 199.0 \n", - "1295 Biscoe 41.0 20.0 203.0 \n", - "1207 Dream 38.8 20.0 190.0 \n", - "1349 Biscoe 38.2 20.0 190.0 \n", - "1350 Biscoe 37.8 20.0 190.0 \n", - "1351 Biscoe 38.1 16.5 198.0 \n", - "1116 Dream 37.0 16.5 185.0 \n", + " island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", + "0 Dream 36.6 18.4 184.0 3475.0 \n", + "1 Dream 39.8 19.1 184.0 4650.0 \n", + "2 Dream 40.9 18.9 184.0 3900.0 \n", + "4 Dream 37.3 16.8 192.0 3000.0 \n", + "5 Dream 43.2 18.5 192.0 4100.0 \n", + "9 Dream 40.2 20.1 200.0 3975.0 \n", + "10 Dream 40.8 18.9 208.0 4300.0 \n", + "11 Dream 39.0 18.7 185.0 3650.0 \n", + "12 Dream 37.0 16.9 185.0 3000.0 \n", + "14 Dream 34.0 17.1 185.0 3400.0 \n", + "15 Dream 37.0 16.5 185.0 3400.0 \n", + "18 Dream 39.7 17.9 193.0 4250.0 \n", + "19 Dream 37.8 18.1 193.0 3750.0 \n", + "22 Dream 40.2 17.1 193.0 3400.0 \n", + "23 Dream 36.8 18.5 193.0 3500.0 \n", + "26 Dream 41.5 18.5 201.0 4000.0 \n", + "31 Dream 33.1 16.1 178.0 2900.0 \n", + "32 Dream 37.2 18.1 178.0 3900.0 \n", + "33 Dream 39.5 16.7 178.0 3250.0 \n", + "35 Dream 36.0 18.5 186.0 3100.0 \n", + "36 Dream 39.6 18.1 186.0 4450.0 \n", + "38 Dream 41.3 20.3 194.0 3550.0 \n", + "41 Dream 35.7 18.0 202.0 3550.0 \n", + "51 Dream 38.1 17.6 187.0 3425.0 \n", + "53 Dream 36.0 17.1 187.0 3700.0 \n", "\n", - " body_mass_g sex \n", - "tag_number \n", - "1172 3050.0 FEMALE \n", - "1371 3075.0 FEMALE \n", - "1417 2900.0 FEMALE \n", - "1204 3725.0 MALE \n", - "1251 3600.0 FEMALE \n", - "1422 3350.0 FEMALE \n", - "1394 3450.0 FEMALE \n", - "1163 3325.0 FEMALE \n", - "1329 3175.0 FEMALE \n", - "1406 4000.0 MALE \n", - "1196 3150.0 FEMALE \n", - "1228 3950.0 MALE \n", - "1412 3250.0 FEMALE \n", - "1142 3550.0 FEMALE \n", - "1430 3600.0 FEMALE \n", - "1333 4775.0 MALE \n", - "1414 3450.0 FEMALE \n", - "1197 3425.0 MALE \n", - "1443 4000.0 MALE \n", - "1295 4725.0 MALE \n", - "1207 3950.0 MALE \n", - "1349 3900.0 MALE \n", - "1350 4250.0 MALE \n", - "1351 3825.0 FEMALE \n", - "1116 3400.0 FEMALE \n", + " sex \n", + "0 FEMALE \n", + "1 MALE \n", + "2 MALE \n", + "4 FEMALE \n", + "5 MALE \n", + "9 MALE \n", + "10 MALE \n", + "11 MALE \n", + "12 FEMALE \n", + "14 FEMALE \n", + "15 FEMALE \n", + "18 MALE \n", + "19 MALE \n", + "22 FEMALE \n", + "23 FEMALE \n", + "26 MALE \n", + "31 FEMALE \n", + "32 MALE \n", + "33 FEMALE \n", + "35 FEMALE \n", + "36 MALE \n", + "38 MALE \n", + "41 FEMALE \n", + "51 FEMALE \n", + "53 FEMALE \n", "...\n", "\n", "[146 rows x 6 columns]" ] }, - "execution_count": 2, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# set a friendlier index to uniquely identify the rows\n", - "df = df.set_index(\"tag_number\")\n", - "\n", "# filter down to the data we want to analyze\n", "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", "\n", @@ -683,9 +843,24 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 14, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "81f9aa34c7234bd88b6b7a4bc77d4b4e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 288f0daa-a51e-45b4-86bf-d054467c4a99 is DONE. 28.9 kB processed.
\n", " \n", " \n", - " tag_number\n", " species\n", " island\n", " culmen_length_mm\n", @@ -65,288 +120,319 @@ " \n", " \n", " 0\n", - " 1225\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " <NA>\n", - " <NA>\n", - " <NA>\n", - " <NA>\n", - " <NA>\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 36.6\n", + " 18.4\n", + " 184.0\n", + " 3475.0\n", + " FEMALE\n", " \n", " \n", " 1\n", - " 1278\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 42.0\n", - " 13.5\n", - " 210.0\n", - " 4150.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 39.8\n", + " 19.1\n", + " 184.0\n", + " 4650.0\n", + " MALE\n", " \n", " \n", " 2\n", - " 1275\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 46.5\n", - " 13.5\n", - " 210.0\n", - " 4550.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 40.9\n", + " 18.9\n", + " 184.0\n", + " 3900.0\n", + " MALE\n", " \n", " \n", " 3\n", - " 1233\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 43.3\n", - " 14.0\n", - " 208.0\n", - " 4575.0\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 46.5\n", + " 17.9\n", + " 192.0\n", + " 3500.0\n", " FEMALE\n", " \n", " \n", " 4\n", - " 1311\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 47.5\n", - " 14.0\n", - " 212.0\n", - " 4875.0\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 37.3\n", + " 16.8\n", + " 192.0\n", + " 3000.0\n", " FEMALE\n", " \n", " \n", " 5\n", - " 1316\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 49.1\n", - " 14.5\n", - " 212.0\n", - " 4625.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 43.2\n", + " 18.5\n", + " 192.0\n", + " 4100.0\n", + " MALE\n", " \n", " \n", " 6\n", - " 1313\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 45.5\n", - " 14.5\n", - " 212.0\n", - " 4750.0\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 46.9\n", + " 16.6\n", + " 192.0\n", + " 2700.0\n", " FEMALE\n", " \n", " \n", " 7\n", - " 1381\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 47.6\n", - " 14.5\n", - " 215.0\n", - " 5400.0\n", - " MALE\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 50.5\n", + " 18.4\n", + " 200.0\n", + " 3400.0\n", + " FEMALE\n", " \n", " \n", " 8\n", - " 1377\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 45.1\n", - " 14.5\n", - " 207.0\n", - " 5050.0\n", - " FEMALE\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 49.5\n", + " 19.0\n", + " 200.0\n", + " 3800.0\n", + " MALE\n", " \n", " \n", " 9\n", - " 1380\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 45.1\n", - " 14.5\n", - " 215.0\n", - " 5000.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 40.2\n", + " 20.1\n", + " 200.0\n", + " 3975.0\n", + " MALE\n", " \n", " \n", " 10\n", - " 1257\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 46.2\n", - " 14.5\n", - " 209.0\n", - " 4800.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 40.8\n", + " 18.9\n", + " 208.0\n", + " 4300.0\n", + " MALE\n", " \n", " \n", " 11\n", - " 1336\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 46.5\n", - " 14.5\n", - " 213.0\n", - " 4400.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 39.0\n", + " 18.7\n", + " 185.0\n", + " 3650.0\n", + " MALE\n", " \n", " \n", " 12\n", - " 1237\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 43.2\n", - " 14.5\n", - " 208.0\n", - " 4450.0\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 37.0\n", + " 16.9\n", + " 185.0\n", + " 3000.0\n", " FEMALE\n", " \n", " \n", " 13\n", - " 1302\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 48.5\n", - " 15.0\n", - " 219.0\n", - " 4850.0\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 47.0\n", + " 17.3\n", + " 185.0\n", + " 3700.0\n", " FEMALE\n", " \n", " \n", " 14\n", - " 1325\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 49.1\n", - " 15.0\n", - " 228.0\n", - " 5500.0\n", - " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 34.0\n", + " 17.1\n", + " 185.0\n", + " 3400.0\n", + " FEMALE\n", " \n", " \n", " 15\n", - " 1285\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 47.5\n", - " 15.0\n", - " 218.0\n", - " 4950.0\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 37.0\n", + " 16.5\n", + " 185.0\n", + " 3400.0\n", " FEMALE\n", " \n", " \n", " 16\n", - " 1242\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 49.6\n", - " 15.0\n", - " 216.0\n", - " 4750.0\n", - " MALE\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 45.7\n", + " 17.3\n", + " 193.0\n", + " 3600.0\n", + " FEMALE\n", " \n", " \n", " 17\n", - " 1246\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 47.7\n", - " 15.0\n", - " 216.0\n", - " 4750.0\n", - " FEMALE\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 50.6\n", + " 19.4\n", + " 193.0\n", + " 3800.0\n", + " MALE\n", " \n", " \n", " 18\n", - " 1320\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 45.5\n", - " 15.0\n", - " 220.0\n", - " 5000.0\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 39.7\n", + " 17.9\n", + " 193.0\n", + " 4250.0\n", " MALE\n", " \n", " \n", " 19\n", - " 1244\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 46.4\n", - " 15.0\n", - " 216.0\n", - " 4700.0\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 37.8\n", + " 18.1\n", + " 193.0\n", + " 3750.0\n", + " MALE\n", + " \n", + " \n", + " 20\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 46.6\n", + " 17.8\n", + " 193.0\n", + " 3800.0\n", + " FEMALE\n", + " \n", + " \n", + " 21\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 51.3\n", + " 19.2\n", + " 193.0\n", + " 3650.0\n", + " MALE\n", + " \n", + " \n", + " 22\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 40.2\n", + " 17.1\n", + " 193.0\n", + " 3400.0\n", + " FEMALE\n", + " \n", + " \n", + " 23\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 36.8\n", + " 18.5\n", + " 193.0\n", + " 3500.0\n", " FEMALE\n", " \n", + " \n", + " 24\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " Dream\n", + " 49.6\n", + " 18.2\n", + " 193.0\n", + " 3775.0\n", + " MALE\n", + " \n", " \n", "\n", - "[347 rows x 8 columns in total]" + "

25 rows × 7 columns

\n", + "[344 rows x 7 columns in total]" ], "text/plain": [ - " tag_number species island culmen_length_mm \\\n", - "0 1225 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "1 1278 Gentoo penguin (Pygoscelis papua) Biscoe 42.0 \n", - "2 1275 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", - "3 1233 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", - "4 1311 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", - "5 1316 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", - "6 1313 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", - "7 1381 Gentoo penguin (Pygoscelis papua) Biscoe 47.6 \n", - "8 1377 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", - "9 1380 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", - "10 1257 Gentoo penguin (Pygoscelis papua) Biscoe 46.2 \n", - "11 1336 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", - "12 1237 Gentoo penguin (Pygoscelis papua) Biscoe 43.2 \n", - "13 1302 Gentoo penguin (Pygoscelis papua) Biscoe 48.5 \n", - "14 1325 Gentoo penguin (Pygoscelis papua) Biscoe 49.1 \n", - "15 1285 Gentoo penguin (Pygoscelis papua) Biscoe 47.5 \n", - "16 1242 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", - "17 1246 Gentoo penguin (Pygoscelis papua) Biscoe 47.7 \n", - "18 1320 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", - "19 1244 Gentoo penguin (Pygoscelis papua) Biscoe 46.4 \n", - "20 1390 Gentoo penguin (Pygoscelis papua) Biscoe 50.7 \n", - "21 1379 Gentoo penguin (Pygoscelis papua) Biscoe 47.8 \n", - "22 1267 Gentoo penguin (Pygoscelis papua) Biscoe 50.1 \n", - "23 1389 Gentoo penguin (Pygoscelis papua) Biscoe 47.2 \n", - "24 1269 Gentoo penguin (Pygoscelis papua) Biscoe 49.6 \n", + " species island culmen_length_mm \\\n", + "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", + "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", + "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", + "5 Adelie Penguin (Pygoscelis adeliae) Dream 43.2 \n", + "6 Chinstrap penguin (Pygoscelis antarctica) Dream 46.9 \n", + "7 Chinstrap penguin (Pygoscelis antarctica) Dream 50.5 \n", + "8 Chinstrap penguin (Pygoscelis antarctica) Dream 49.5 \n", + "9 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", + "10 Adelie Penguin (Pygoscelis adeliae) Dream 40.8 \n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream 39.0 \n", + "12 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", + "13 Chinstrap penguin (Pygoscelis antarctica) Dream 47.0 \n", + "14 Adelie Penguin (Pygoscelis adeliae) Dream 34.0 \n", + "15 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", + "16 Chinstrap penguin (Pygoscelis antarctica) Dream 45.7 \n", + "17 Chinstrap penguin (Pygoscelis antarctica) Dream 50.6 \n", + "18 Adelie Penguin (Pygoscelis adeliae) Dream 39.7 \n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.8 \n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream 46.6 \n", + "21 Chinstrap penguin (Pygoscelis antarctica) Dream 51.3 \n", + "22 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream 36.8 \n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream 49.6 \n", "\n", " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "0 \n", - "1 13.5 210.0 4150.0 FEMALE \n", - "2 13.5 210.0 4550.0 FEMALE \n", - "3 14.0 208.0 4575.0 FEMALE \n", - "4 14.0 212.0 4875.0 FEMALE \n", - "5 14.5 212.0 4625.0 FEMALE \n", - "6 14.5 212.0 4750.0 FEMALE \n", - "7 14.5 215.0 5400.0 MALE \n", - "8 14.5 207.0 5050.0 FEMALE \n", - "9 14.5 215.0 5000.0 FEMALE \n", - "10 14.5 209.0 4800.0 FEMALE \n", - "11 14.5 213.0 4400.0 FEMALE \n", - "12 14.5 208.0 4450.0 FEMALE \n", - "13 15.0 219.0 4850.0 FEMALE \n", - "14 15.0 228.0 5500.0 MALE \n", - "15 15.0 218.0 4950.0 FEMALE \n", - "16 15.0 216.0 4750.0 MALE \n", - "17 15.0 216.0 4750.0 FEMALE \n", - "18 15.0 220.0 5000.0 MALE \n", - "19 15.0 216.0 4700.0 FEMALE \n", - "20 15.0 223.0 5550.0 MALE \n", - "21 15.0 215.0 5650.0 MALE \n", - "22 15.0 225.0 5000.0 MALE \n", - "23 15.5 215.0 4975.0 FEMALE \n", - "24 16.0 225.0 5700.0 MALE \n", + "0 18.4 184.0 3475.0 FEMALE \n", + "1 19.1 184.0 4650.0 MALE \n", + "2 18.9 184.0 3900.0 MALE \n", + "3 17.9 192.0 3500.0 FEMALE \n", + "4 16.8 192.0 3000.0 FEMALE \n", + "5 18.5 192.0 4100.0 MALE \n", + "6 16.6 192.0 2700.0 FEMALE \n", + "7 18.4 200.0 3400.0 FEMALE \n", + "8 19.0 200.0 3800.0 MALE \n", + "9 20.1 200.0 3975.0 MALE \n", + "10 18.9 208.0 4300.0 MALE \n", + "11 18.7 185.0 3650.0 MALE \n", + "12 16.9 185.0 3000.0 FEMALE \n", + "13 17.3 185.0 3700.0 FEMALE \n", + "14 17.1 185.0 3400.0 FEMALE \n", + "15 16.5 185.0 3400.0 FEMALE \n", + "16 17.3 193.0 3600.0 FEMALE \n", + "17 19.4 193.0 3800.0 MALE \n", + "18 17.9 193.0 4250.0 MALE \n", + "19 18.1 193.0 3750.0 MALE \n", + "20 17.8 193.0 3800.0 FEMALE \n", + "21 19.2 193.0 3650.0 MALE \n", + "22 17.1 193.0 3400.0 FEMALE \n", + "23 18.5 193.0 3500.0 FEMALE \n", + "24 18.2 193.0 3775.0 MALE \n", "...\n", "\n", - "[347 rows x 8 columns]" + "[344 rows x 7 columns]" ] }, - "execution_count": 1, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -371,9 +457,51 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "782462924bc84b9281d6d66f7f4acbe0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 6f0e1877-369d-4e9f-81a8-9c00ab1b57b3 is DONE. 28.9 kB processed.
body_mass_g\n", " sex\n", " \n", - " \n", - " tag_number\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", " \n", " \n", " \n", - " 1172\n", + " 0\n", " Dream\n", - " 32.1\n", - " 15.5\n", - " 188.0\n", - " 3050.0\n", + " 36.6\n", + " 18.4\n", + " 184.0\n", + " 3475.0\n", " FEMALE\n", " \n", " \n", - " 1371\n", - " Biscoe\n", - " 37.7\n", - " 16.0\n", - " 183.0\n", - " 3075.0\n", - " FEMALE\n", + " 1\n", + " Dream\n", + " 39.8\n", + " 19.1\n", + " 184.0\n", + " 4650.0\n", + " MALE\n", " \n", " \n", - " 1417\n", - " Torgersen\n", - " 38.6\n", - " 17.0\n", - " 188.0\n", - " 2900.0\n", + " 2\n", + " Dream\n", + " 40.9\n", + " 18.9\n", + " 184.0\n", + " 3900.0\n", + " MALE\n", + " \n", + " \n", + " 4\n", + " Dream\n", + " 37.3\n", + " 16.8\n", + " 192.0\n", + " 3000.0\n", " FEMALE\n", " \n", " \n", - " 1204\n", + " 5\n", " Dream\n", - " 40.7\n", - " 17.0\n", - " 190.0\n", - " 3725.0\n", + " 43.2\n", + " 18.5\n", + " 192.0\n", + " 4100.0\n", " MALE\n", " \n", " \n", - " 1251\n", - " Biscoe\n", - " 37.6\n", - " 17.0\n", + " 9\n", + " Dream\n", + " 40.2\n", + " 20.1\n", + " 200.0\n", + " 3975.0\n", + " MALE\n", + " \n", + " \n", + " 10\n", + " Dream\n", + " 40.8\n", + " 18.9\n", + " 208.0\n", + " 4300.0\n", + " MALE\n", + " \n", + " \n", + " 11\n", + " Dream\n", + " 39.0\n", + " 18.7\n", " 185.0\n", - " 3600.0\n", + " 3650.0\n", + " MALE\n", + " \n", + " \n", + " 12\n", + " Dream\n", + " 37.0\n", + " 16.9\n", + " 185.0\n", + " 3000.0\n", " FEMALE\n", " \n", " \n", - " 1422\n", - " Torgersen\n", - " 35.7\n", - " 17.0\n", - " 189.0\n", - " 3350.0\n", + " 14\n", + " Dream\n", + " 34.0\n", + " 17.1\n", + " 185.0\n", + " 3400.0\n", " FEMALE\n", " \n", " \n", - " 1394\n", - " Torgersen\n", - " 40.2\n", - " 17.0\n", - " 176.0\n", - " 3450.0\n", + " 15\n", + " Dream\n", + " 37.0\n", + " 16.5\n", + " 185.0\n", + " 3400.0\n", " FEMALE\n", " \n", " \n", - " 1163\n", + " 18\n", + " Dream\n", + " 39.7\n", + " 17.9\n", + " 193.0\n", + " 4250.0\n", + " MALE\n", + " \n", + " \n", + " 19\n", + " Dream\n", + " 37.8\n", + " 18.1\n", + " 193.0\n", + " 3750.0\n", + " MALE\n", + " \n", + " \n", + " 22\n", " Dream\n", - " 36.4\n", - " 17.0\n", - " 195.0\n", - " 3325.0\n", + " 40.2\n", + " 17.1\n", + " 193.0\n", + " 3400.0\n", " FEMALE\n", " \n", " \n", - " 1329\n", - " Biscoe\n", - " 38.1\n", - " 17.0\n", - " 181.0\n", - " 3175.0\n", + " 23\n", + " Dream\n", + " 36.8\n", + " 18.5\n", + " 193.0\n", + " 3500.0\n", " FEMALE\n", " \n", " \n", - " 1406\n", - " Torgersen\n", - " 44.1\n", - " 18.0\n", - " 210.0\n", + " 26\n", + " Dream\n", + " 41.5\n", + " 18.5\n", + " 201.0\n", " 4000.0\n", " MALE\n", " \n", " \n", - " 1196\n", + " 31\n", " Dream\n", - " 36.5\n", - " 18.0\n", - " 182.0\n", - " 3150.0\n", + " 33.1\n", + " 16.1\n", + " 178.0\n", + " 2900.0\n", " FEMALE\n", " \n", " \n", - " 1228\n", - " Biscoe\n", - " 41.6\n", - " 18.0\n", - " 192.0\n", - " 3950.0\n", + " 32\n", + " Dream\n", + " 37.2\n", + " 18.1\n", + " 178.0\n", + " 3900.0\n", " MALE\n", " \n", " \n", - " 1412\n", - " Torgersen\n", - " 40.3\n", - " 18.0\n", - " 195.0\n", + " 33\n", + " Dream\n", + " 39.5\n", + " 16.7\n", + " 178.0\n", " 3250.0\n", " FEMALE\n", " \n", " \n", - " 1142\n", + " 35\n", " Dream\n", - " 35.7\n", - " 18.0\n", - " 202.0\n", - " 3550.0\n", + " 36.0\n", + " 18.5\n", + " 186.0\n", + " 3100.0\n", " FEMALE\n", " \n", " \n", - " 1430\n", - " Torgersen\n", - " 33.5\n", - " 19.0\n", - " 190.0\n", - " 3600.0\n", - " FEMALE\n", + " 36\n", + " Dream\n", + " 39.6\n", + " 18.1\n", + " 186.0\n", + " 4450.0\n", + " MALE\n", " \n", " \n", - " 1333\n", - " Biscoe\n", - " 43.2\n", - " 19.0\n", - " 197.0\n", - " 4775.0\n", + " 38\n", + " Dream\n", + " 41.3\n", + " 20.3\n", + " 194.0\n", + " 3550.0\n", " MALE\n", " \n", " \n", - " 1414\n", - " Torgersen\n", - " 38.7\n", - " 19.0\n", - " 195.0\n", - " 3450.0\n", + " 41\n", + " Dream\n", + " 35.7\n", + " 18.0\n", + " 202.0\n", + " 3550.0\n", " FEMALE\n", " \n", " \n", - " 1197\n", + " 51\n", " Dream\n", - " 41.1\n", - " 19.0\n", - " 182.0\n", + " 38.1\n", + " 17.6\n", + " 187.0\n", " 3425.0\n", - " MALE\n", - " \n", - " \n", - " 1443\n", - " Torgersen\n", - " 40.6\n", - " 19.0\n", - " 199.0\n", - " 4000.0\n", - " MALE\n", + " FEMALE\n", " \n", " \n", - " 1295\n", - " Biscoe\n", - " 41.0\n", - " 20.0\n", - " 203.0\n", - " 4725.0\n", - " MALE\n", + " 53\n", + " Dream\n", + " 36.0\n", + " 17.1\n", + " 187.0\n", + " 3700.0\n", + " FEMALE\n", " \n", " \n", "\n", + "

25 rows × 6 columns

\n", "[146 rows x 6 columns in total]" ], "text/plain": [ - " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", - "tag_number \n", - "1172 Dream 32.1 15.5 188.0 \n", - "1371 Biscoe 37.7 16.0 183.0 \n", - "1417 Torgersen 38.6 17.0 188.0 \n", - "1204 Dream 40.7 17.0 190.0 \n", - "1251 Biscoe 37.6 17.0 185.0 \n", - "1422 Torgersen 35.7 17.0 189.0 \n", - "1394 Torgersen 40.2 17.0 176.0 \n", - "1163 Dream 36.4 17.0 195.0 \n", - "1329 Biscoe 38.1 17.0 181.0 \n", - "1406 Torgersen 44.1 18.0 210.0 \n", - "1196 Dream 36.5 18.0 182.0 \n", - "1228 Biscoe 41.6 18.0 192.0 \n", - "1412 Torgersen 40.3 18.0 195.0 \n", - "1142 Dream 35.7 18.0 202.0 \n", - "1430 Torgersen 33.5 19.0 190.0 \n", - "1333 Biscoe 43.2 19.0 197.0 \n", - "1414 Torgersen 38.7 19.0 195.0 \n", - "1197 Dream 41.1 19.0 182.0 \n", - "1443 Torgersen 40.6 19.0 199.0 \n", - "1295 Biscoe 41.0 20.0 203.0 \n", - "1207 Dream 38.8 20.0 190.0 \n", - "1349 Biscoe 38.2 20.0 190.0 \n", - "1350 Biscoe 37.8 20.0 190.0 \n", - "1351 Biscoe 38.1 16.5 198.0 \n", - "1116 Dream 37.0 16.5 185.0 \n", + " island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", + "0 Dream 36.6 18.4 184.0 3475.0 \n", + "1 Dream 39.8 19.1 184.0 4650.0 \n", + "2 Dream 40.9 18.9 184.0 3900.0 \n", + "4 Dream 37.3 16.8 192.0 3000.0 \n", + "5 Dream 43.2 18.5 192.0 4100.0 \n", + "9 Dream 40.2 20.1 200.0 3975.0 \n", + "10 Dream 40.8 18.9 208.0 4300.0 \n", + "11 Dream 39.0 18.7 185.0 3650.0 \n", + "12 Dream 37.0 16.9 185.0 3000.0 \n", + "14 Dream 34.0 17.1 185.0 3400.0 \n", + "15 Dream 37.0 16.5 185.0 3400.0 \n", + "18 Dream 39.7 17.9 193.0 4250.0 \n", + "19 Dream 37.8 18.1 193.0 3750.0 \n", + "22 Dream 40.2 17.1 193.0 3400.0 \n", + "23 Dream 36.8 18.5 193.0 3500.0 \n", + "26 Dream 41.5 18.5 201.0 4000.0 \n", + "31 Dream 33.1 16.1 178.0 2900.0 \n", + "32 Dream 37.2 18.1 178.0 3900.0 \n", + "33 Dream 39.5 16.7 178.0 3250.0 \n", + "35 Dream 36.0 18.5 186.0 3100.0 \n", + "36 Dream 39.6 18.1 186.0 4450.0 \n", + "38 Dream 41.3 20.3 194.0 3550.0 \n", + "41 Dream 35.7 18.0 202.0 3550.0 \n", + "51 Dream 38.1 17.6 187.0 3425.0 \n", + "53 Dream 36.0 17.1 187.0 3700.0 \n", "\n", - " body_mass_g sex \n", - "tag_number \n", - "1172 3050.0 FEMALE \n", - "1371 3075.0 FEMALE \n", - "1417 2900.0 FEMALE \n", - "1204 3725.0 MALE \n", - "1251 3600.0 FEMALE \n", - "1422 3350.0 FEMALE \n", - "1394 3450.0 FEMALE \n", - "1163 3325.0 FEMALE \n", - "1329 3175.0 FEMALE \n", - "1406 4000.0 MALE \n", - "1196 3150.0 FEMALE \n", - "1228 3950.0 MALE \n", - "1412 3250.0 FEMALE \n", - "1142 3550.0 FEMALE \n", - "1430 3600.0 FEMALE \n", - "1333 4775.0 MALE \n", - "1414 3450.0 FEMALE \n", - "1197 3425.0 MALE \n", - "1443 4000.0 MALE \n", - "1295 4725.0 MALE \n", - "1207 3950.0 MALE \n", - "1349 3900.0 MALE \n", - "1350 4250.0 MALE \n", - "1351 3825.0 FEMALE \n", - "1116 3400.0 FEMALE \n", + " sex \n", + "0 FEMALE \n", + "1 MALE \n", + "2 MALE \n", + "4 FEMALE \n", + "5 MALE \n", + "9 MALE \n", + "10 MALE \n", + "11 MALE \n", + "12 FEMALE \n", + "14 FEMALE \n", + "15 FEMALE \n", + "18 MALE \n", + "19 MALE \n", + "22 FEMALE \n", + "23 FEMALE \n", + "26 MALE \n", + "31 FEMALE \n", + "32 MALE \n", + "33 FEMALE \n", + "35 FEMALE \n", + "36 MALE \n", + "38 MALE \n", + "41 FEMALE \n", + "51 FEMALE \n", + "53 FEMALE \n", "...\n", "\n", "[146 rows x 6 columns]" ] }, - "execution_count": 2, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# set a friendlier index to uniquely identify the rows\n", - "df = df.set_index(\"tag_number\")\n", - "\n", "# filter down to the data we want to analyze\n", "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", "\n", @@ -682,7 +842,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -704,7 +864,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -719,9 +879,79 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "367359e2069c4d198a820d1ced057b81", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 56778fb7-779c-4e44-b2a3-04d2e174c562 is DONE. 31.9 kB processed.
\n", " \n", "\n", + "

1 rows × 6 columns

\n", "[1 rows x 6 columns in total]" ], "text/plain": [ @@ -775,7 +1006,7 @@ "[1 rows x 6 columns]" ] }, - "execution_count": 5, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -787,9 +1018,79 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "025941ee47864dae956ef25bfe815da4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 1eabd729-6c75-4087-9c87-0d95327b615c is RUNNING.
\n", " predicted_body_mass_g\n", " \n", - " \n", - " tag_number\n", - " \n", - " \n", " \n", " \n", " \n", - " 1393\n", + " 292\n", " 3459.735118\n", " \n", - " \n", - " 1525\n", - " 3947.881639\n", - " \n", - " \n", - " 1524\n", - " 4304.175638\n", - " \n", - " \n", - " 1523\n", - " 3471.668379\n", - " \n", " \n", "\n", - "[4 rows x 1 columns in total]" + "

1 rows × 1 columns

\n", + "[1 rows x 1 columns in total]" ], "text/plain": [ - " predicted_body_mass_g\n", - "tag_number \n", - "1393 3459.735118\n", - "1525 3947.881639\n", - "1524 4304.175638\n", - "1523 3471.668379\n", + " predicted_body_mass_g\n", + "292 3459.735118\n", "\n", - "[4 rows x 1 columns]" + "[1 rows x 1 columns]" ] }, - "execution_count": 6, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -870,7 +1152,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -879,7 +1161,7 @@ "LinearRegression()" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -906,7 +1188,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.10.12" }, "orig_nbformat": 4, "vscode": { diff --git a/notebooks/06 - Using ML - Large Language Models.ipynb b/notebooks/06 - Using ML - Large Language Models.ipynb index c2c0b83bef2..45a46c44af9 100644 --- a/notebooks/06 - Using ML - Large Language Models.ipynb +++ b/notebooks/06 - Using ML - Large Language Models.ipynb @@ -137,7 +137,7 @@ } ], "source": [ - "pred = model.predict(bf_df).compute()\n", + "pred = model.predict(bf_df).to_pandas()\n", "pred" ] }, diff --git a/notebooks/10 - Regionalized.ipynb b/notebooks/10 - Regionalized.ipynb new file mode 100644 index 00000000000..a7ff5db84e3 --- /dev/null +++ b/notebooks/10 - Regionalized.ipynb @@ -0,0 +1,2800 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# README\n", + "\n", + "This Notebook runs differently depending on the following environent variable:\n", + "1. BIGQUERY_LOCATION - can take values as per https://cloud.google.com/bigquery/docs/locations, e.g. `us`, `asia-east1`" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Infer location and set up data in that location if needed" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", + " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n", + "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", + " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BigQuery project: bigframes-dev\n", + "BigQuery location: us\n", + "Penguins Table: bigquery-public-data.ml_datasets.penguins\n", + "ML Model Dataset: bigframes-dev.bigframes_testing\n" + ] + } + ], + "source": [ + "# Take multi-region US as the default BQ location, where most of the BQ data lies including the BQ public datasets\n", + "BQ_LOCATION = \"us\"\n", + "PROJECT = \"bigframes-dev\"\n", + "DATASET = \"bigframes_testing\"\n", + "PENGUINS_TABLE = \"bigquery-public-data.ml_datasets.penguins\"\n", + "\n", + "\n", + "# Check for a location set in the environment and do location-specific setup if needed\n", + "\n", + "import os\n", + "import google.api_core.exceptions\n", + "from google.cloud import bigquery\n", + "import bigframes\n", + " \n", + "env_bq_location = os.getenv(\"BIGQUERY_LOCATION\")\n", + "if env_bq_location and env_bq_location != BQ_LOCATION:\n", + " BQ_LOCATION = env_bq_location.lower()\n", + "\n", + "client = bigquery.Client()\n", + "\n", + "if BQ_LOCATION != \"us\":\n", + " bq_location_normalized = BQ_LOCATION.replace('-', '_')\n", + "\n", + " # Nominate a local penguins table\n", + " penguins_table_ref = bigquery.TableReference.from_string(PENGUINS_TABLE)\n", + " penguins_local_dataset_name = f\"{DATASET}_{bq_location_normalized}\"\n", + " penguins_local_dataset_ref = bigquery.DatasetReference(project=PROJECT, dataset_id=penguins_local_dataset_name)\n", + " penguins_local_dataset = bigquery.Dataset(penguins_local_dataset_ref)\n", + " penguins_local_dataset.location = BQ_LOCATION\n", + " penguins_local_table_ref= bigquery.TableReference(penguins_local_dataset, penguins_table_ref.table_id)\n", + " penguins_local_table = str(penguins_local_table_ref)\n", + " try:\n", + " client.get_table(penguins_local_table_ref)\n", + " except google.api_core.exceptions.NotFound:\n", + " client.create_dataset(penguins_local_dataset, exists_ok=True)\n", + "\n", + " # Read the public table as an in-memory dataframe and then write to the local table\n", + " session_us = bigframes.connect()\n", + " df = session_us.read_gbq(PENGUINS_TABLE).to_pandas()\n", + " df.to_gbq(penguins_local_table)\n", + "\n", + " # Finally point the penguins table to the local table\n", + " PENGUINS_TABLE=penguins_local_table\n", + "\n", + " # Also update the dataset name used for test artifacts\n", + " DATASET = f\"{DATASET}_{bq_location_normalized}\"\n", + "\n", + "# Create the dataset to store the model if it doesn't exist \n", + "model_local_dataset = bigquery.Dataset(bigquery.DatasetReference(project=PROJECT, dataset_id=DATASET))\n", + "model_local_dataset.location = BQ_LOCATION\n", + "model_dataset = client.create_dataset(model_local_dataset, exists_ok=True)\n", + "\n", + "# Finally log the variables driving the core notebook execution\n", + "log = ('\\n'.join(f\"{name}: {str(value)}\" for name, value in {\n", + " \"BigQuery project\" : PROJECT,\n", + " \"BigQuery location\" : BQ_LOCATION,\n", + " \"Penguins Table\" : PENGUINS_TABLE,\n", + " \"ML Model Dataset\" : model_dataset.reference\n", + "}.items())) \n", + "print(log)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using the BigQuery DataFrames API" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas\n", + "\n", + "bigframes.pandas.options.bigquery.project = PROJECT\n", + "bigframes.pandas.options.bigquery.location = BQ_LOCATION" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialize a dataframe for a BigQuery table" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", + " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "02af1103261a4e63a4c15efd26b1bc9a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job e1a62d56-8cab-4bc1-9ad3-457f48b71d9c is RUNNING.
= 4000.0]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using the Remote Functions" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### BigQuery DataFrames gives you the ability to turn your custom scalar functions into a BigQuery remote function.\n", + "\n", + "It requires the GCP project to be set up appropriately and the user having sufficient privileges to use them. One can find more details on it via `help` command." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function remote_function in module bigframes.pandas:\n", + "\n", + "remote_function(input_types: 'List[type]', output_type: 'type', dataset: 'Optional[str]' = None, bigquery_connection: 'Optional[str]' = None, reuse: 'bool' = True)\n", + " Decorator to turn a user defined function into a BigQuery remote function.\n", + " \n", + " .. note::\n", + " Please make sure following is setup before using this API:\n", + " \n", + " 1. Have the below APIs enabled for your project:\n", + " a. BigQuery Connection API\n", + " b. Cloud Functions API\n", + " c. Cloud Run API\n", + " d. Cloud Build API\n", + " e. Artifact Registry API\n", + " f. Cloud Resource Manager API\n", + " \n", + " This can be done from the cloud console (change PROJECT_ID to yours):\n", + " https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID\n", + " Or from the gcloud CLI:\n", + " $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com\n", + " \n", + " 2. Have following IAM roles enabled for you:\n", + " a. BigQuery Data Editor (roles/bigquery.dataEditor)\n", + " b. BigQuery Connection Admin (roles/bigquery.connectionAdmin)\n", + " c. Cloud Functions Developer (roles/cloudfunctions.developer)\n", + " d. Service Account User (roles/iam.serviceAccountUser)\n", + " e. Storage Object Viewer (roles/storage.objectViewer)\n", + " f. Project IAM Admin (roles/resourcemanager.projectIamAdmin)\n", + " (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.)\n", + " \n", + " 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set:\n", + " a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection\n", + " b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function\n", + " Alternatively, the IAM could also be setup via the gcloud CLI:\n", + " $ gcloud projects add-iam-policy-binding PROJECT_ID --member=\"serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID\" --role=\"roles/run.invoker\"\n", + " \n", + " Args:\n", + " input_types (list(type)):\n", + " List of input data types in the user defined function.\n", + " output_type (type):\n", + " Data type of the output in the user defined function.\n", + " dataset (str, Optional):\n", + " Dataset to use to create a BigQuery function. It should be in\n", + " `.` or `` format. If this\n", + " param is not provided then session dataset id would be used.\n", + " bigquery_connection (str, Optional):\n", + " Name of the BigQuery connection. If it is pre created in the same\n", + " location as the `bigquery_client.location` then it would be used,\n", + " otherwise it would be created dynamically assuming the user has\n", + " necessary priviliges. If this param is not provided then the\n", + " bigquery connection from the session would be used.\n", + " reuse (bool, Optional):\n", + " Reuse the remote function if already exists.\n", + " `True` by default, which will result in reusing an existing remote\n", + " function (if any) that was previously created for the same udf.\n", + " Setting it to false would force creating a unique remote function.\n", + " If the required remote function does not exist then it would be\n", + " created irrespective of this param.\n", + "\n" + ] + } + ], + "source": [ + "import bigframes.pandas as pd\n", + "help(pd.remote_function)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define a custom function, and specify the intent to turn it into a remote function.\n", + "\n", + "It requires a BigQuery connection. If the connection is not already created,\n", + "the BigQuery DataFrames package attempts to create one assuming the necessary\n", + "APIs and IAM permissions are setup in the project." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO][2023-08-05 23:12:12,870][bigframes.remote_function] Creating new cloud function: gcloud functions deploy bigframes-f9320ad496b5aeca2d7f343cbab03e2f --gen2 --runtime=python310 --project=bigframes-dev --region=us-central1 --source=/tmp/tmps5m0qu4z --entry-point=udf_http --trigger-http --no-allow-unauthenticated\n", + "[INFO][2023-08-05 23:13:20,660][bigframes.remote_function] Successfully created cloud function bigframes-f9320ad496b5aeca2d7f343cbab03e2f with uri (https://bigframes-f9320ad496b5aeca2d7f343cbab03e2f-7krlje3eoq-uc.a.run.app)\n", + "[INFO][2023-08-05 23:13:32,717][bigframes.remote_function] Connector bigframes-rf-conn already exists\n", + "[INFO][2023-08-05 23:13:32,719][bigframes.remote_function] Creating BQ remote function: \n", + " CREATE OR REPLACE FUNCTION `bigframes-dev.bigframes_temp_us`.bigframes_f9320ad496b5aeca2d7f343cbab03e2f(num FLOAT64)\n", + " RETURNS STRING\n", + " REMOTE WITH CONNECTION `bigframes-dev.us.bigframes-rf-conn`\n", + " OPTIONS (\n", + " endpoint = \"https://bigframes-f9320ad496b5aeca2d7f343cbab03e2f-7krlje3eoq-uc.a.run.app\"\n", + " )\n", + "[INFO][2023-08-05 23:13:33,697][bigframes.remote_function] Created remote function bigframes-dev.bigframes_temp_us.bigframes_f9320ad496b5aeca2d7f343cbab03e2f\n" + ] + } + ], + "source": [ + "@pd.remote_function([float], str, bigquery_connection='bigframes-rf-conn')\n", + "def get_bucket(num):\n", + " if not num: return \"NA\"\n", + " boundary = 4000\n", + " return \"at_or_above_4000\" if num >= boundary else \"below_4000\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the custom function on the BigQuery-backed dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0dba87f5bcb74dca9efebe8f522beeff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 5f30816f-f4d0-4063-bb9e-2905b89f717d is DONE. 2.8 kB processed. \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
body_mass_gbody_mass_bucket
03475.0below_4000
14650.0at_or_above_4000
23900.0below_4000
33500.0below_4000
43000.0below_4000
54100.0at_or_above_4000
62700.0below_4000
73400.0below_4000
83800.0below_4000
93975.0below_4000
\n", + "

10 rows × 2 columns

\n", + "[10 rows x 2 columns in total]" + ], + "text/plain": [ + " body_mass_g body_mass_bucket\n", + "0 3475.0 below_4000\n", + "1 4650.0 at_or_above_4000\n", + "2 3900.0 below_4000\n", + "3 3500.0 below_4000\n", + "4 3000.0 below_4000\n", + "5 4100.0 at_or_above_4000\n", + "6 2700.0 below_4000\n", + "7 3400.0 below_4000\n", + "8 3800.0 below_4000\n", + "9 3975.0 below_4000\n", + "\n", + "[10 rows x 2 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket))\n", + "df[['body_mass_g', 'body_mass_bucket']].head(10)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using the ML API" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialize a DataFrame from a BigQuery table" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "89bf7ae3fd8641bcbdc5a4614a82f48a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job acd770bb-5ccb-463f-beec-2386132ded6b is RUNNING.
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Dream36.618.4184.03475.0FEMALE
1Dream39.819.1184.04650.0MALE
2Dream40.918.9184.03900.0MALE
4Dream37.316.8192.03000.0FEMALE
5Dream43.218.5192.04100.0MALE
9Dream40.220.1200.03975.0MALE
10Dream40.818.9208.04300.0MALE
11Dream39.018.7185.03650.0MALE
12Dream37.016.9185.03000.0FEMALE
14Dream34.017.1185.03400.0FEMALE
15Dream37.016.5185.03400.0FEMALE
18Dream39.717.9193.04250.0MALE
19Dream37.818.1193.03750.0MALE
22Dream40.217.1193.03400.0FEMALE
23Dream36.818.5193.03500.0FEMALE
26Dream41.518.5201.04000.0MALE
31Dream33.116.1178.02900.0FEMALE
32Dream37.218.1178.03900.0MALE
33Dream39.516.7178.03250.0FEMALE
35Dream36.018.5186.03100.0FEMALE
36Dream39.618.1186.04450.0MALE
38Dream41.320.3194.03550.0MALE
41Dream35.718.0202.03550.0FEMALE
51Dream38.117.6187.03425.0FEMALE
53Dream36.017.1187.03700.0FEMALE
\n", + "

25 rows × 6 columns

\n", + "[146 rows x 6 columns in total]" + ], + "text/plain": [ + " island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", + "0 Dream 36.6 18.4 184.0 3475.0 \n", + "1 Dream 39.8 19.1 184.0 4650.0 \n", + "2 Dream 40.9 18.9 184.0 3900.0 \n", + "4 Dream 37.3 16.8 192.0 3000.0 \n", + "5 Dream 43.2 18.5 192.0 4100.0 \n", + "9 Dream 40.2 20.1 200.0 3975.0 \n", + "10 Dream 40.8 18.9 208.0 4300.0 \n", + "11 Dream 39.0 18.7 185.0 3650.0 \n", + "12 Dream 37.0 16.9 185.0 3000.0 \n", + "14 Dream 34.0 17.1 185.0 3400.0 \n", + "15 Dream 37.0 16.5 185.0 3400.0 \n", + "18 Dream 39.7 17.9 193.0 4250.0 \n", + "19 Dream 37.8 18.1 193.0 3750.0 \n", + "22 Dream 40.2 17.1 193.0 3400.0 \n", + "23 Dream 36.8 18.5 193.0 3500.0 \n", + "26 Dream 41.5 18.5 201.0 4000.0 \n", + "31 Dream 33.1 16.1 178.0 2900.0 \n", + "32 Dream 37.2 18.1 178.0 3900.0 \n", + "33 Dream 39.5 16.7 178.0 3250.0 \n", + "35 Dream 36.0 18.5 186.0 3100.0 \n", + "36 Dream 39.6 18.1 186.0 4450.0 \n", + "38 Dream 41.3 20.3 194.0 3550.0 \n", + "41 Dream 35.7 18.0 202.0 3550.0 \n", + "51 Dream 38.1 17.6 187.0 3425.0 \n", + "53 Dream 36.0 17.1 187.0 3700.0 \n", + "\n", + " sex \n", + "0 FEMALE \n", + "1 MALE \n", + "2 MALE \n", + "4 FEMALE \n", + "5 MALE \n", + "9 MALE \n", + "10 MALE \n", + "11 MALE \n", + "12 FEMALE \n", + "14 FEMALE \n", + "15 FEMALE \n", + "18 MALE \n", + "19 MALE \n", + "22 FEMALE \n", + "23 FEMALE \n", + "26 MALE \n", + "31 FEMALE \n", + "32 MALE \n", + "33 FEMALE \n", + "35 FEMALE \n", + "36 MALE \n", + "38 MALE \n", + "41 FEMALE \n", + "51 FEMALE \n", + "53 FEMALE \n", + "...\n", + "\n", + "[146 rows x 6 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# filter down to the data we want to analyze\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# drop the columns we don't care about\n", + "adelie_data = adelie_data.drop(columns=[\"species\"])\n", + "\n", + "# drop rows with nulls to get our training data\n", + "training_data = adelie_data.dropna()\n", + "\n", + "# take a peek at the training data\n", + "training_data" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# pick feature columns and label column\n", + "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "label_columns = training_data[['body_mass_g']]\n", + "\n", + "# also get the rows that we want to make predictions for (i.e. where the feature column is null)\n", + "missing_body_mass = adelie_data[adelie_data.body_mass_g.isnull()]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Train and evaluate a linear regression model using the ML API" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "865f6bb75c5b48e4a52a3183fe3c2582", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 4b0c58e4-4752-4b96-b490-a95e3ae326c0 is DONE. 31.9 kB processed.
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", + "

1 rows × 6 columns

\n", + "[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 223.878763 78553.601634 0.005614 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 181.330911 0.623951 0.623951 \n", + "\n", + "[1 rows x 6 columns]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "# as in scikit-learn, a newly created model is just a bundle of parameters\n", + "# default parameters are fine here\n", + "model = LinearRegression()\n", + "\n", + "# this will train a temporary model in BigQuery Machine Learning\n", + "model.fit(feature_columns, label_columns)\n", + "\n", + "# check how the model performed\n", + "model.score(feature_columns, label_columns)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Make predictions using the model" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "caf8cefe06a14da2a02f31aa1e12c23a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job fe2fe252-8433-4d20-861c-681a8dfbf2c4 is RUNNING.
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_g
2923459.735118
\n", + "

1 rows × 1 columns

\n", + "[1 rows x 1 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g\n", + "292 3459.735118\n", + "\n", + "[1 rows x 1 columns]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.predict(missing_body_mass)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save the trained model to BigQuery, so we can load it later" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression()" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.to_gbq(f\"{DATASET}.penguins_model\", replace=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/noxfile.py b/noxfile.py index 5e54c12fb2f..d4d97ca2c4e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,6 +16,7 @@ from __future__ import absolute_import +from multiprocessing import Process import os import pathlib import re @@ -82,7 +83,6 @@ "docfx", "unit", "unit_noextras", - "unit_prerelease", "system", "doctest", "cover", @@ -603,24 +603,62 @@ def notebook(session): ] notebooks = [os.path.join("notebooks", nb) for nb in notebooks] + # Regionalized notebooks + notebooks_reg = { + "10 - Regionalized.ipynb": [ + "asia-southeast1", + "eu", + "europe-west4", + "southamerica-west1", + "us", + "us-central1", + ] + } + notebooks_reg = { + os.path.join("notebooks", nb): regions for nb, regions in notebooks_reg.items() + } + # For some reason nbmake exits silently with "no tests ran" message if # one of the notebook paths supplied does not exist. Let's make sure that # each path exists - for nb in notebooks: + for nb in notebooks + list(notebooks_reg): assert os.path.exists(nb), nb - # Use retries because sometimes parallel runs of the same notebook can try - # to create the same artifacts and may run into resoure conflict at the GCP - # level. - session.run( + # TODO(shobs): For some reason --retries arg masks exceptions occurred in + # notebook failures, and shows unhelpful INTERNALERROR. Investigate that + # and enable retries if we can find a way to surface the real exception + # bacause the notebook is running against real GCP and something may fail + # due to transient issues. + pytest_command = [ "py.test", - "-nauto", "--nbmake", "--nbmake-timeout=600", - "--retries=3", + ] + + # Run self-contained notebooks in single session.run + # achieve parallelization via -n + session.run( + *pytest_command, + "-nauto", *notebooks, ) + # Run regionalized notebooks in parallel session.run's, since each notebook + # takes a different region via env param. + processes = [] + for notebook, regions in notebooks_reg.items(): + for region in regions: + process = Process( + target=session.run, + args=(*pytest_command, notebook), + kwargs={"env": {"BIGQUERY_LOCATION": region}}, + ) + process.start() + processes.append(process) + + for process in processes: + process.join() + @nox.session(python="3.10") def release_dry_run(session): diff --git a/samples/snippets/quickstart.py b/samples/snippets/quickstart.py index dc8ab7ba2d0..a15ea168534 100644 --- a/samples/snippets/quickstart.py +++ b/samples/snippets/quickstart.py @@ -14,64 +14,46 @@ def run_quickstart(project_id: str): - # [START bigquery_bigframes_quickstart] - import bigframes.pandas as pd + import bigframes + + session_options = bigframes.BigQueryOptions() + session = bigframes.connect(session_options) + + your_gcp_project_id = project_id + query_or_table = "bigquery-public-data.ml_datasets.penguins" + df_session = session.read_gbq(query_or_table) + average_body_mass = df_session["body_mass_g"].mean() + print(f"average_body_mass (df_session): {average_body_mass}") - # TODO: (Optional) Setup your session with the configuration. Some of these - # settings cannot be changed once a session has started. - pd.options.bigquery.project = "your-gcp-project-id" - pd.options.bigquery.location = "us" + # [START bigquery_bigframes_quickstart] + import bigframes.pandas as bpd - # [START_EXCLUDE silent] - # Ignore the lines between "EXCLUDE" comments. They replace values so this - # sample runs in our test suite. - pd.options.bigquery.project = project_id - # [END_EXCLUDE] + # Set BigQuery DataFrames options + bpd.options.bigquery.project = your_gcp_project_id + bpd.options.bigquery.location = "us" + # Create a DataFrame from a BigQuery table query_or_table = "bigquery-public-data.ml_datasets.penguins" - df = pd.read_gbq(query_or_table) + df = bpd.read_gbq(query_or_table) # Use the DataFrame just as you would a pandas DataFrame, but calculations # happen in the BigQuery query engine instead of the local system. average_body_mass = df["body_mass_g"].mean() - print(f"average_body_mass: {average_body_mass}") - # IMPORTANT: The `bigframes.pandas` package creates a BigQuery session for - # queries and temporary tables. A BigQuery session has a limited lifetime - # (https://cloud.google.com/bigquery/docs/sessions-intro#limitations) and - # does not support concurrent queries. For long lived applications, create - # session objects as needed, instead. - - import bigframes - - session_options = bigframes.BigQueryOptions() - session_options.project = "your-gcp-project-id" - session_options.location = "us" - - # [START_EXCLUDE silent] - # Ignore the lines between "EXCLUDE" comments. They replace values so this - # sample runs in our test suite. - session_options.project = project_id - # [END_EXCLUDE] - - session = bigframes.connect(session_options) - df_session = session.read_gbq(query_or_table) - average_body_mass = df_session["body_mass_g"].mean() - print(f"average_body_mass (df_session): {average_body_mass}") - + # Create the Linear Regression model from bigframes.ml.linear_model import LinearRegression - # filter down to the data we want to analyze + # Filter down to the data we want to analyze adelie_data = df[df.species == "Adelie Penguin (Pygoscelis adeliae)"] - # drop the columns we don't care about + # Drop the columns we don't care about adelie_data = adelie_data.drop(columns=["species"]) - # drop rows with nulls to get our training data + # Drop rows with nulls to get our training data training_data = adelie_data.dropna() - # pick feature columns and label column + # Pick feature columns and label column X = training_data[ [ "island", diff --git a/scripts/upload_to_google_drive.py b/scripts/upload_to_google_drive.py new file mode 100644 index 00000000000..e5791513595 --- /dev/null +++ b/scripts/upload_to_google_drive.py @@ -0,0 +1,71 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Upload latest wheel to Google Drive. + +Based on +https://github.com/googleapis/google-resumable-media-python/blob/main/google/resumable_media/requests/__init__.py + +Before running, execute the following to make sure you can use the Google Drive API: + +gcloud auth application-default login --scopes=openid,https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/sqlservice.login,https://www.googleapis.com/auth/drive +""" + +import pathlib + +import google.auth +import google.auth.transport.requests +import google.resumable_media._upload +import google.resumable_media.requests as resumable_requests + +repo_root = pathlib.Path(__file__).parent.parent + +# Use PATCH instead of POST to replace existing files. +google.resumable_media._upload._POST = "PATCH" + +credentials, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/drive"]) +transport = google.auth.transport.requests.AuthorizedSession(credentials) + +wheel_id = "15fZ1DkrFDk4ibMNTzms4akpxmf2pzeAR" +wheel_path = next(iter((repo_root / "dist").glob("bigframes-*.whl"))) + +pdf_id = "1agYjxmPLrxelsaHI-lc41QHcgnQYemcX" +pdf_path = repo_root / "docs" / "_build" / "latex" / "bigframes-latest.pdf" + +uploads = ( + (wheel_id, wheel_path, "application/octet-stream"), + (pdf_id, pdf_path, "application/pdf"), +) + +upload_template = ( + "https://www.googleapis.com/upload/drive/v3/files/{file_id}?uploadType=resumable" +) +chunk_size = 1024 * 1024 # 1MB + +for file_id, file_path, content_type in uploads: + print(f"Uploading {file_path}") + transport = google.auth.transport.requests.AuthorizedSession(credentials) + upload = resumable_requests.ResumableUpload( + upload_template.format(file_id=file_id), chunk_size + ) + + with open(file_path, "rb") as stream: + response = upload.initiate( + transport, stream, metadata={}, content_type=content_type + ) + print(response) + while not upload.finished: + response = upload.transmit_next_chunk(transport) + print(response) diff --git a/setup.py b/setup.py index 311471f2627..3e36cd3ecfb 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ # 'Development Status :: 5 - Production/Stable' release_status = "Development Status :: 3 - Alpha" dependencies = [ - "cloudpickle >= 2.2.1", + "cloudpickle >= 2.0.0", "fsspec >=2023.3.0", "gcsfs >=2023.3.0", "geopandas >=0.12.2", @@ -39,9 +39,10 @@ "google-cloud-functions >=1.10.1", "google-cloud-bigquery-connection >=1.12.0", "google-cloud-storage >=2.0.0", - "ibis-framework[bigquery] >=6.0.0", + # TODO: Relax upper bound once we have fixed `system_prerelease` tests. + "ibis-framework[bigquery] >=6.0.0,<=6.1.0", "pandas >=1.5.0", - "pydata-google-auth >=1.5.0", + "pydata-google-auth >=1.8.2", "scikit-learn >=1.2.2", "sqlalchemy >=1.4,<3.0", "ipywidgets >=7.7.1", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index aa72e5426b7..fe3d49ef208 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -11,7 +11,7 @@ cffi==1.15.1 cfgv==3.3.1 charset-normalizer==3.1.0 click==8.1.3 -cloudpickle==2.2.1 +cloudpickle==2.0.0 colorlog==6.7.0 coverage==7.2.2 cryptography==40.0.1 @@ -81,7 +81,7 @@ pyarrow==11.0.0 pyasn1==0.4.8 pyasn1-modules==0.2.8 pycparser==2.21 -pydata-google-auth==1.5.0 +pydata-google-auth==1.8.2 Pygments==2.14.0 PyJWT==2.6.0 pyperclip==1.8.2 diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 03638698966..b6b02e4c27a 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -22,6 +22,7 @@ import google.cloud.bigquery as bigquery import google.cloud.bigquery_connection_v1 as bigquery_connection_v1 import google.cloud.exceptions +import google.cloud.functions_v2 as functions_v2 import google.cloud.storage as storage # type: ignore import ibis.backends.base import pandas as pd @@ -93,6 +94,13 @@ def bigqueryconnection_client( return session.bqconnectionclient +@pytest.fixture(scope="session") +def cloudfunctions_client( + session: bigframes.Session, +) -> functions_v2.FunctionServiceClient: + return session.cloudfunctionsclient + + @pytest.fixture(scope="session") def session() -> bigframes.Session: return bigframes.Session() @@ -665,3 +673,19 @@ def penguins_randomforest_classifier_model_name( session.bqclient.query(sql).result() finally: return model_name + + +@pytest.fixture() +def deferred_repr(): + bigframes.options.display.repr_mode = "deferred" + yield + bigframes.options.display.repr_mode = "head" + + +@pytest.fixture() +def restore_sampling_settings(): + enable_downsampling = bigframes.options.sampling.enable_downsampling + max_download_size = bigframes.options.sampling.max_download_size + yield + bigframes.options.sampling.enable_downsampling = enable_downsampling + bigframes.options.sampling.max_download_size = max_download_size diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py index bb64b19076f..eae6896669b 100644 --- a/tests/system/large/ml/test_cluster.py +++ b/tests/system/large/ml/test_cluster.py @@ -12,14 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas +import pandas as pd +import pytest -import bigframes.ml.cluster +from bigframes.ml import cluster from tests.system.utils import assert_pandas_df_equal_ignore_ordering -def test_cluster_configure_fit_predict(session, penguins_df_default_index, dataset_id): - model = bigframes.ml.cluster.KMeans(n_clusters=3) +@pytest.mark.flaky(retries=2, delay=120) +def test_cluster_configure_fit_score_predict( + session, penguins_df_default_index, dataset_id +): + model = cluster.KMeans(n_clusters=3) df = penguins_df_default_index.dropna()[ [ @@ -37,7 +41,7 @@ def test_cluster_configure_fit_predict(session, penguins_df_default_index, datas model.fit(df) - pd_new_penguins = pandas.DataFrame.from_dict( + pd_new_penguins = pd.DataFrame.from_dict( { "test1": { "species": "Adelie Penguin (Pygoscelis adeliae)", @@ -81,13 +85,24 @@ def test_cluster_configure_fit_predict(session, penguins_df_default_index, datas pd_new_penguins.index.name = "observation" new_penguins = session.read_pandas(pd_new_penguins) - result = model.predict(new_penguins).compute() - expected = pandas.DataFrame( + + # Check score to ensure the model was fitted + score_result = model.score(new_penguins).to_pandas() + score_expected = pd.DataFrame( + {"davies_bouldin_index": [1.502182], "mean_squared_distance": [1.953408]}, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + result = model.predict(new_penguins).to_pandas() + expected = pd.DataFrame( {"CENTROID_ID": [2, 3, 1, 2]}, dtype="Int64", - index=pandas.Index( - ["test1", "test2", "test3", "test4"], dtype="string[pyarrow]" - ), + index=pd.Index(["test1", "test2", "test3", "test4"], dtype="string[pyarrow]"), ) expected.index.name = "observation" assert_pandas_df_equal_ignore_ordering(result, expected) diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 52f347151ed..0c2744819d4 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -56,8 +56,12 @@ def test_columntransformer_standalone_fit_transform( [{"index": 1, "value": 1.0}], [{"index": 2, "value": 1.0}], ], - "scaled_culmen_length_mm": [-0.8099, -0.9931, -1.103], - "scaled_flipper_length_mm": [-0.3495, -1.416, -0.9185], + "scaled_culmen_length_mm": [ + -0.811119671289163, + -0.9945520581113803, + -1.104611490204711, + ], + "scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], }, index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), ) diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py index cd829f0ae2d..ab33e5d7185 100644 --- a/tests/system/large/ml/test_core.py +++ b/tests/system/large/ml/test_core.py @@ -37,7 +37,7 @@ def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_d ) # no data - report evaluation from the automatic data split - evaluate_result = model.evaluate().compute() + evaluate_result = model.evaluate().to_pandas() evaluate_expected = pandas.DataFrame( { "mean_absolute_error": [225.817334], @@ -57,13 +57,13 @@ def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_d ) # evaluate on all training data - evaluate_result = model.evaluate(df).compute() + evaluate_result = model.evaluate(df).to_pandas() pandas.testing.assert_frame_equal( evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 ) # predict new labels - predictions = model.predict(new_penguins_df).compute() + predictions = model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, dtype="Float64", @@ -104,7 +104,7 @@ def test_bqml_manual_preprocessing_e2e( ) # no data - report evaluation from the automatic data split - evaluate_result = model.evaluate().compute() + evaluate_result = model.evaluate().to_pandas() evaluate_expected = pandas.DataFrame( { "mean_absolute_error": [309.477334], @@ -125,13 +125,13 @@ def test_bqml_manual_preprocessing_e2e( ) # evaluate on all training data - evaluate_result = model.evaluate(df).compute() + evaluate_result = model.evaluate(df).to_pandas() pandas.testing.assert_frame_equal( evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 ) # predict new labels - predictions = model.predict(new_penguins_df).compute() + predictions = model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_body_mass_g": [3968.8, 3176.3, 3545.2]}, dtype="Float64", @@ -156,11 +156,11 @@ def test_bqml_standalone_transform(penguins_df_default_index, new_penguins_df): options={"model_type": "transform_only"}, transforms=[ "ML.STANDARD_SCALER(culmen_length_mm) OVER() AS scaled_culmen_length_mm", - "ML.ONE_HOT_ENCODER(species) OVER() AS onehotencoded_species", + "ML.ONE_HOT_ENCODER(species, 'none', 1000000, 0) OVER() AS onehotencoded_species", ], ) - transformed = model.transform(new_penguins_df).compute() + transformed = model.transform(new_penguins_df).to_pandas() expected = pandas.DataFrame( { "scaled_culmen_length_mm": [-0.8099, -0.9931, -1.103], diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index 57507c11342..460f07b8168 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -12,19 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas +import pandas as pd -import bigframes.ml.decomposition +from bigframes.ml import decomposition -def test_decomposition_configure_fit_predict( +def test_decomposition_configure_fit_score_predict( session, penguins_df_default_index, dataset_id ): - model = bigframes.ml.decomposition.PCA(n_components=3) + model = decomposition.PCA(n_components=3) model.fit(penguins_df_default_index) - pd_new_penguins = session.read_pandas( - pandas.DataFrame( + new_penguins = session.read_pandas( + pd.DataFrame( { "tag_number": [1633, 1672, 1690], "species": [ @@ -42,17 +42,31 @@ def test_decomposition_configure_fit_predict( ).set_index("tag_number") ) - result = model.predict(pd_new_penguins).compute() - expected = pandas.DataFrame( + # Check score to ensure the model was fitted + score_result = model.score(new_penguins).to_pandas() + score_expected = pd.DataFrame( + { + "total_explained_variance_ratio": [0.812383], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + result = model.predict(new_penguins).to_pandas() + expected = pd.DataFrame( { "principal_component_1": [-1.459, 2.258, -1.685], "principal_component_2": [-1.120, -1.351, -0.874], "principal_component_3": [-0.646, 0.443, -0.704], }, dtype="Float64", - index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pandas.testing.assert_frame_equal( + pd.testing.assert_frame_equal( result.sort_index(), expected, check_exact=False, diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index 0d5da380d1e..88c5ccd2f0d 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -15,10 +15,12 @@ from unittest import TestCase import pandas +import pytest import bigframes.ml.ensemble +@pytest.mark.flaky(retries=2, delay=120) def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBRegressor() @@ -37,7 +39,7 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [97.368139], @@ -62,6 +64,7 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): ) +@pytest.mark.flaky(retries=2, delay=120) def test_xgbregressor_dart_booster_multiple_params( penguins_df_default_index, dataset_id ): @@ -97,7 +100,7 @@ def test_xgbregressor_dart_booster_multiple_params( model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "mean_absolute_error", @@ -136,6 +139,7 @@ def test_xgbregressor_dart_booster_multiple_params( assert reloaded_model.num_parallel_tree == 2 +@pytest.mark.flaky(retries=2, delay=120) def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBClassifier() @@ -153,7 +157,7 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -175,6 +179,7 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): ) +@pytest.mark.flaky(retries=2, delay=120) def test_xgbclassifier_dart_booster_multiple_params( penguins_df_default_index, dataset_id ): @@ -209,7 +214,7 @@ def test_xgbclassifier_dart_booster_multiple_params( model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -248,6 +253,7 @@ def test_xgbclassifier_dart_booster_multiple_params( assert reloaded_model.num_parallel_tree == 2 +@pytest.mark.flaky(retries=2, delay=120) def test_randomforestregressor_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestRegressor() @@ -266,7 +272,7 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "mean_absolute_error", @@ -288,6 +294,7 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset ) +@pytest.mark.flaky(retries=2, delay=120) def test_randomforestregressor_multiple_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestRegressor( tree_method="AUTO", @@ -318,7 +325,7 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "mean_absolute_error", @@ -354,6 +361,7 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase assert reloaded_model.enable_global_explain is False +@pytest.mark.flaky(retries=2, delay=120) def test_randomforestclassifier_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestClassifier() @@ -371,7 +379,7 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -393,6 +401,7 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase ) +@pytest.mark.flaky(retries=2, delay=120) def test_randomforestclassifier_multiple_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestClassifier( tree_method="AUTO", @@ -422,7 +431,7 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index 98726eb289d..d1e2d12296d 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -27,7 +27,7 @@ def test_arima_plus_model_fit_score( result = model.score( new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] - ).compute() + ).to_pandas() expected = pd.DataFrame( { "mean_absolute_error": [154.742547], diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 33010a005cb..332b460fe51 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -35,7 +35,7 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() expected = pd.DataFrame( { "mean_absolute_error": [225.735767], @@ -80,7 +80,7 @@ def test_linear_regression_manual_split_configure_fit_score( model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() expected = pd.DataFrame( { "mean_absolute_error": [225.735767], @@ -121,7 +121,7 @@ def test_logistic_regression_auto_class_weights_configure_fit_score( model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() expected = pd.DataFrame( { "precision": [0.58085], @@ -169,7 +169,7 @@ def test_logistic_regression_manual_split_configure_fit_score( model.fit(train_X, train_y) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).compute() + result = model.score(train_X, train_y).to_pandas() expected = pd.DataFrame( { "precision": [0.616753], diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 9700ba2bf65..bec1a51a991 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -13,6 +13,7 @@ # limitations under the License. import pandas as pd +import pytest from bigframes.ml import ( cluster, @@ -38,24 +39,24 @@ def test_pipeline_linear_regression_fit_score_predict( ) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", ] ] - train_y = df[["body_mass_g"]] - pl.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) # Check score to ensure the model was fitted - score_result = pl.score(train_X, train_y).compute() + score_result = pl.score(X_train, y_train).to_pandas() score_expected = pd.DataFrame( { - "mean_absolute_error": [309.477334], - "mean_squared_error": [152184.227218], + "mean_absolute_error": [309.477331], + "mean_squared_error": [152184.227219], "mean_squared_log_error": [0.009524], - "median_absolute_error": [257.727777], + "median_absolute_error": [257.728263], "r2_score": [0.764356], "explained_variance": [0.764356], }, @@ -96,6 +97,61 @@ def test_pipeline_linear_regression_fit_score_predict( ) +def test_pipeline_linear_regression_series_fit_score_predict( + session, penguins_df_default_index +): + """Test a supervised model with a minimal preprocessing step""" + pl = pipeline.Pipeline( + [ + ("scale", preprocessing.StandardScaler()), + ("linreg", linear_model.LinearRegression()), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df["culmen_length_mm"] + y_train = df["body_mass_g"] + pl.fit(X_train, y_train) + + # Check score to ensure the model was fitted + score_result = pl.score(X_train, y_train).to_pandas() + score_expected = pd.DataFrame( + { + "mean_absolute_error": [528.495599], + "mean_squared_error": [421722.261808], + "mean_squared_log_error": [0.022963], + "median_absolute_error": [468.895249], + "r2_score": [0.346999], + "explained_variance": [0.346999], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + # predict new labels + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "culmen_length_mm": [39.5, 38.5, 37.9], + } + ).set_index("tag_number") + ) + predictions = pl.predict(new_penguins["culmen_length_mm"]).to_pandas() + expected = pd.DataFrame( + {"predicted_body_mass_g": [3818.845703, 3732.022253, 3679.928123]}, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + ) + + def test_pipeline_logistic_regression_fit_score_predict( session, penguins_df_default_index ): @@ -108,18 +164,18 @@ def test_pipeline_logistic_regression_fit_score_predict( ) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", ] ] - train_y = df[["sex"]] - pl.fit(train_X, train_y) + y_train = df[["sex"]] + pl.fit(X_train, y_train) # Check score to ensure the model was fitted - score_result = pl.score(train_X, train_y).compute() + score_result = pl.score(X_train, y_train).to_pandas() score_expected = pd.DataFrame( { "precision": [0.537091], @@ -166,6 +222,7 @@ def test_pipeline_logistic_regression_fit_score_predict( ) +@pytest.mark.flaky(retries=2, delay=120) def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_index): """Test a supervised model with a minimal preprocessing step""" pl = pipeline.Pipeline( @@ -176,26 +233,26 @@ def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_in ) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", ] ] - train_y = df[["body_mass_g"]] - pl.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) # Check score to ensure the model was fitted - score_result = pl.score(train_X, train_y).compute() + score_result = pl.score(X_train, y_train).to_pandas() score_expected = pd.DataFrame( { - "mean_absolute_error": [203.4001727989334], - "mean_squared_error": [74898.80551717622], - "mean_squared_log_error": [0.004394266810531861], - "median_absolute_error": [152.01806640625], - "r2_score": [0.8840255831308607], - "explained_variance": [0.8858505311591299], + "mean_absolute_error": [202.298434], + "mean_squared_error": [74515.108971], + "mean_squared_log_error": [0.004365], + "median_absolute_error": [142.949219], + "r2_score": [0.88462], + "explained_variance": [0.886454], }, dtype="Float64", ) @@ -240,6 +297,7 @@ def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_in ) +@pytest.mark.flaky(retries=2, delay=120) def test_pipeline_random_forest_classifier_fit_score_predict( session, penguins_df_default_index ): @@ -252,26 +310,26 @@ def test_pipeline_random_forest_classifier_fit_score_predict( ) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", ] ] - train_y = df[["sex"]] - pl.fit(train_X, train_y) + y_train = df[["sex"]] + pl.fit(X_train, y_train) # Check score to ensure the model was fitted - score_result = pl.score(train_X, train_y).compute() + score_result = pl.score(X_train, y_train).to_pandas() score_expected = pd.DataFrame( { - "precision": [0.587673], - "recall": [0.588781], - "accuracy": [0.88024], - "f1_score": [0.587644], - "log_loss": [0.859459], - "roc_auc": [0.971737], + "precision": [0.585505], + "recall": [0.58676], + "accuracy": [0.877246], + "f1_score": [0.585657], + "log_loss": [0.880643], + "roc_auc": [0.970697], }, dtype="Float64", ) @@ -310,7 +368,7 @@ def test_pipeline_random_forest_classifier_fit_score_predict( ) -def test_pipeline_PCA_fit_predict(session, penguins_df_default_index): +def test_pipeline_PCA_fit_score_predict(session, penguins_df_default_index): """Test a supervised model with a minimal preprocessing step""" pl = pipeline.Pipeline( [ @@ -320,14 +378,14 @@ def test_pipeline_PCA_fit_predict(session, penguins_df_default_index): ) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", ] ] - pl.fit(train_X) + pl.fit(X_train) # predict new labels new_penguins = session.read_pandas( @@ -347,12 +405,27 @@ def test_pipeline_PCA_fit_predict(session, penguins_df_default_index): } ).set_index("tag_number") ) + + # Check score to ensure the model was fitted + score_result = pl.score(new_penguins).to_pandas() + score_expected = pd.DataFrame( + { + "total_explained_variance_ratio": [1.0], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + predictions = pl.predict(new_penguins).to_pandas() expected = pd.DataFrame( { - "principal_component_1": [-1.115259, -1.506141, -1.471174], - "principal_component_2": [-0.074824, 0.69664, 0.406104], - "principal_component_3": [0.500012, -0.544479, 0.075849], + "principal_component_1": [-1.115259, -1.506141, -1.471173], + "principal_component_2": [-0.074825, 0.69664, 0.406103], + "principal_component_3": [0.500013, -0.544479, 0.075849], }, dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), @@ -367,7 +440,8 @@ def test_pipeline_PCA_fit_predict(session, penguins_df_default_index): ) -def test_pipeline_standard_scaler_kmeans_fit_predict( +@pytest.mark.flaky(retries=2, delay=120) +def test_pipeline_standard_scaler_kmeans_fit_score_predict( session, penguins_pandas_df_default_index ): """Test an unsupervised model with a non-BQML implementation of StandardScaler""" @@ -380,14 +454,14 @@ def test_pipeline_standard_scaler_kmeans_fit_predict( # kmeans is sensitive to the order with this configuration, so use ordered source data df = session.read_pandas(penguins_pandas_df_default_index).dropna() - train_X = df[ + X_train = df[ [ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", ] ] - pl.fit(train_X) + pl.fit(X_train) # predict new labels pd_new_penguins = pd.DataFrame.from_dict( @@ -452,6 +526,19 @@ def test_pipeline_standard_scaler_kmeans_fit_predict( pd_new_penguins.index.name = "observation" new_penguins = session.read_pandas(pd_new_penguins) + + # Check score to ensure the model was fitted + score_result = pl.score(new_penguins).to_pandas() + score_expected = pd.DataFrame( + {"davies_bouldin_index": [7.542981], "mean_squared_distance": [94.692409]}, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + result = pl.predict(new_penguins).to_pandas().sort_index() expected = pd.DataFrame( {"CENTROID_ID": [1, 2, 1, 2, 1, 2]}, @@ -466,7 +553,7 @@ def test_pipeline_standard_scaler_kmeans_fit_predict( def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_index): - """Test a preprocessing step that manages heterogenous data with ColumnTransformer""" + """Test a preprocessing step that manages heterogeneous data with ColumnTransformer""" pl = pipeline.Pipeline( [ ( @@ -491,9 +578,9 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind ) df = penguins_df_default_index.dropna() - train_X = df[["species", "culmen_length_mm", "flipper_length_mm"]] - train_y = df[["body_mass_g"]] - pl.fit(train_X, train_y) + X_train = df[["species", "culmen_length_mm", "flipper_length_mm"]] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) # predict new labels new_penguins = session.read_pandas( @@ -522,3 +609,129 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind pd.testing.assert_frame_equal( predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 ) + + +def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ( + "transform", + compose.ColumnTransformer( + [ + ( + "ont_hot_encoder", + preprocessing.OneHotEncoder( + drop="most_frequent", + min_frequency=5, + max_categories=100, + ), + "species", + ), + ( + "standard_scaler", + preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ), + ), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[["species", "culmen_length_mm", "flipper_length_mm"]] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_col_transformer", replace=True + ) + + assert isinstance(pl_loaded._transform, compose.ColumnTransformer) + transformers = pl_loaded._transform.transformers_ + assert len(transformers) == 3 + + assert transformers[0][0] == "ont_hot_encoder" + assert isinstance(transformers[0][1], preprocessing.OneHotEncoder) + one_hot_encoder = transformers[0][1] + assert one_hot_encoder.drop == "most_frequent" + assert one_hot_encoder.min_frequency == 5 + assert one_hot_encoder.max_categories == 100 + assert transformers[0][2] == "species" + + assert transformers[1][0] == "standard_scaler" + assert isinstance(transformers[1][1], preprocessing.StandardScaler) + assert transformers[1][2] == "culmen_length_mm" + + assert transformers[2][0] == "standard_scaler" + assert isinstance(transformers[2][1], preprocessing.StandardScaler) + assert transformers[2][2] == "flipper_length_mm" + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False + + +def test_pipeline_standard_scaler_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ("transform", preprocessing.StandardScaler()), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_standard_scaler", replace=True + ) + assert isinstance(pl_loaded._transform, preprocessing.StandardScaler) + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False + + +def test_pipeline_one_hot_encoder_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ( + "transform", + preprocessing.OneHotEncoder( + drop="most_frequent", min_frequency=5, max_categories=100 + ), + ), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "sex", + "species", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_one_hot_encoder", replace=True + ) + assert isinstance(pl_loaded._transform, preprocessing.OneHotEncoder) + + one_hot_encoder = pl_loaded._transform + assert one_hot_encoder.drop == "most_frequent" + assert one_hot_encoder.min_frequency == 5 + assert one_hot_encoder.max_categories == 100 + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 714252c9e00..8033f79c47b 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -295,7 +295,7 @@ def square(x): bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(square) bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() ) pd_int64_col = scalars_pandas_df["int64_col"] @@ -339,7 +339,7 @@ def add_one(x): bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(remote_add_one) bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() ) pd_int64_col = scalars_pandas_df["int64_col"] @@ -392,7 +392,7 @@ def sign(num): bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(remote_sign) bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() ) pd_int64_col = scalars_pandas_df["int64_col"] @@ -437,7 +437,7 @@ def circumference(radius): bf_float64_col_filtered = bf_float64_col[bf_float64_col_filter] bf_result_col = bf_float64_col_filtered.apply(remote_circumference) bf_result = ( - bf_float64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_float64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() ) pd_float64_col = scalars_pandas_df["float64_col"] @@ -486,7 +486,7 @@ def find_team(num): bf_float64_col_filtered = bf_float64_col[bf_float64_col_filter] bf_result_col = bf_float64_col_filtered.apply(remote_find_team) bf_result = ( - bf_float64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_float64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() ) pd_float64_col = scalars_pandas_df["float64_col"] @@ -577,7 +577,9 @@ def inner_test(): bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(remote_add_one) bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_int64_col_filtered.to_frame() + .assign(result=bf_result_col) + .to_pandas() ) pd_int64_col = scalars_pandas_df["int64_col"] @@ -675,7 +677,7 @@ def is_odd(num): bf_int64_col = scalars_df["int64_col"] bf_result_col = bf_int64_col.mask(is_odd_remote) - bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).compute() + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).to_pandas() pd_int64_col = scalars_pandas_df["int64_col"] pd_result_col = pd_int64_col.mask(is_odd) @@ -718,7 +720,7 @@ def is_odd(num): # for now filter out the nulls and test the rest bf_int64_col = scalars_df["int64_col"] bf_result_col = bf_int64_col[bf_int64_col.notnull()].mask(is_odd_remote, -1) - bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).compute() + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).to_pandas() pd_int64_col = scalars_pandas_df["int64_col"] pd_result_col = pd_int64_col[pd_int64_col.notnull()].mask(is_odd, -1) @@ -754,7 +756,7 @@ def test_remote_udf_lambda( bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(add_one_lambda_remote) bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() ) pd_int64_col = scalars_pandas_df["int64_col"] diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 5c3abf25dcc..8325cec50aa 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -21,11 +21,17 @@ import pandas as pd import pytest -from bigframes.ml import core, forecasting, imported, llm -import bigframes.ml.cluster -import bigframes.ml.core -import bigframes.ml.ensemble -import bigframes.ml.linear_model +import bigframes +from bigframes.ml import ( + cluster, + core, + decomposition, + ensemble, + forecasting, + imported, + linear_model, + llm, +) @pytest.fixture(scope="session") @@ -34,17 +40,15 @@ def ml_connection() -> str: @pytest.fixture(scope="session") -def penguins_bqml_linear_model( - session, penguins_linear_model_name -) -> bigframes.ml.core.BqmlModel: +def penguins_bqml_linear_model(session, penguins_linear_model_name) -> core.BqmlModel: model = session.bqclient.get_model(penguins_linear_model_name) - return bigframes.ml.core.BqmlModel(session, model) + return core.BqmlModel(session, model) @pytest.fixture(scope="function") def ephemera_penguins_bqml_linear_model( penguins_bqml_linear_model, -) -> bigframes.ml.linear_model.LinearRegression: +) -> linear_model.LinearRegression: model = penguins_bqml_linear_model return model.copy( f"{model._model.project}.{model._model.dataset_id}.{uuid.uuid4().hex}" @@ -54,18 +58,18 @@ def ephemera_penguins_bqml_linear_model( @pytest.fixture(scope="session") def penguins_linear_model( session, penguins_linear_model_name: str -) -> bigframes.ml.linear_model.LinearRegression: +) -> linear_model.LinearRegression: return cast( - bigframes.ml.linear_model.LinearRegression, + linear_model.LinearRegression, session.read_gbq_model(penguins_linear_model_name), ) @pytest.fixture(scope="function") def ephemera_penguins_linear_model( - ephemera_penguins_bqml_linear_model: bigframes.ml.core.BqmlModel, -) -> bigframes.ml.linear_model.LinearRegression: - bf_model = bigframes.ml.linear_model.LinearRegression() + ephemera_penguins_bqml_linear_model: core.BqmlModel, +) -> linear_model.LinearRegression: + bf_model = linear_model.LinearRegression() bf_model._bqml_model = ephemera_penguins_bqml_linear_model return bf_model @@ -73,9 +77,9 @@ def ephemera_penguins_linear_model( @pytest.fixture(scope="session") def penguins_logistic_model( session, penguins_logistic_model_name -) -> bigframes.ml.linear_model.LogisticRegression: +) -> linear_model.LogisticRegression: return cast( - bigframes.ml.linear_model.LogisticRegression, + linear_model.LogisticRegression, session.read_gbq_model(penguins_logistic_model_name), ) @@ -83,9 +87,9 @@ def penguins_logistic_model( @pytest.fixture(scope="session") def penguins_xgbregressor_model( session, penguins_xgbregressor_model_name -) -> bigframes.ml.ensemble.XGBRegressor: +) -> ensemble.XGBRegressor: return cast( - bigframes.ml.ensemble.XGBRegressor, + ensemble.XGBRegressor, session.read_gbq_model(penguins_xgbregressor_model_name), ) @@ -93,9 +97,9 @@ def penguins_xgbregressor_model( @pytest.fixture(scope="session") def penguins_xgbclassifier_model( session, penguins_xgbclassifier_model_name -) -> bigframes.ml.ensemble.XGBClassifier: +) -> ensemble.XGBClassifier: return cast( - bigframes.ml.ensemble.XGBClassifier, + ensemble.XGBClassifier, session.read_gbq_model(penguins_xgbclassifier_model_name), ) @@ -103,9 +107,9 @@ def penguins_xgbclassifier_model( @pytest.fixture(scope="session") def penguins_randomforest_regressor_model( session, penguins_randomforest_regressor_model_name -) -> bigframes.ml.ensemble.RandomForestRegressor: +) -> ensemble.RandomForestRegressor: return cast( - bigframes.ml.ensemble.RandomForestRegressor, + ensemble.RandomForestRegressor, session.read_gbq_model(penguins_randomforest_regressor_model_name), ) @@ -113,9 +117,9 @@ def penguins_randomforest_regressor_model( @pytest.fixture(scope="session") def penguins_randomforest_classifier_model( session, penguins_randomforest_classifier_model_name -) -> bigframes.ml.ensemble.RandomForestClassifier: +) -> ensemble.RandomForestClassifier: return cast( - bigframes.ml.ensemble.RandomForestClassifier, + ensemble.RandomForestClassifier, session.read_gbq_model(penguins_randomforest_classifier_model_name), ) @@ -123,7 +127,7 @@ def penguins_randomforest_classifier_model( @pytest.fixture(scope="session") def penguins_kmeans_model( session: bigframes.Session, dataset_id_permanent, penguins_table_id -) -> bigframes.ml.cluster.KMeans: +) -> cluster.KMeans: """Provides a pretrained model as a test fixture that is cached across test runs. This lets us run system tests without having to wait for a model.fit(...)""" sql = f""" @@ -154,7 +158,7 @@ def penguins_kmeans_model( @pytest.fixture(scope="session") def penguins_pca_model( session: bigframes.Session, dataset_id_permanent, penguins_table_id -) -> bigframes.ml.decomposition.PCA: +) -> decomposition.PCA: # TODO(yunmengxie): Create a shared method to get different types of pretrained models. sql = f""" @@ -248,8 +252,10 @@ def ephemera_palm2_text_generator_model( @pytest.fixture(scope="session") def palm2_embedding_generator_model( session, ml_connection -) -> llm.PaLM2EmbeddingGenerator: - return llm.PaLM2EmbeddingGenerator(session=session, connection_name=ml_connection) +) -> llm.PaLM2TextEmbeddingGenerator: + return llm.PaLM2TextEmbeddingGenerator( + session=session, connection_name=ml_connection + ) @pytest.fixture(scope="session") @@ -257,7 +263,7 @@ def time_series_bqml_arima_plus_model( session, time_series_arima_plus_model_name ) -> core.BqmlModel: model = session.bqclient.get_model(time_series_arima_plus_model_name) - return bigframes.ml.core.BqmlModel(session, model) + return core.BqmlModel(session, model) @pytest.fixture(scope="session") @@ -287,8 +293,8 @@ def ephemera_imported_tensorflow_model(session) -> imported.TensorFlowModel: @pytest.fixture(scope="session") -def imported_onnx_model(session) -> imported.OnnxModel: - return imported.OnnxModel( +def imported_onnx_model(session) -> imported.ONNXModel: + return imported.ONNXModel( session=session, model_path="gs://cloud-samples-data/bigquery/ml/onnx/pipeline_rf.onnx", ) diff --git a/tests/system/small/ml/test_cluster.py b/tests/system/small/ml/test_cluster.py index 4aefc5fa697..a003cd1ec1b 100644 --- a/tests/system/small/ml/test_cluster.py +++ b/tests/system/small/ml/test_cluster.py @@ -12,68 +12,81 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas +import pandas as pd -import bigframes.ml.cluster +from bigframes.ml import cluster from tests.system.utils import assert_pandas_df_equal_ignore_ordering - -def test_model_predict(session, penguins_kmeans_model: bigframes.ml.cluster.KMeans): - pd_new_penguins = pandas.DataFrame.from_dict( - { - "test1": { - "species": "Adelie Penguin (Pygoscelis adeliae)", - "island": "Dream", - "culmen_length_mm": 37.5, - "culmen_depth_mm": 18.5, - "flipper_length_mm": 199, - "body_mass_g": 4475, - "sex": "MALE", - }, - "test2": { - "species": "Chinstrap penguin (Pygoscelis antarctica)", - "island": "Dream", - "culmen_length_mm": 55.8, - "culmen_depth_mm": 19.8, - "flipper_length_mm": 207, - "body_mass_g": 4000, - "sex": "MALE", - }, - "test3": { - "species": "Adelie Penguin (Pygoscelis adeliae)", - "island": "Biscoe", - "culmen_length_mm": 39.7, - "culmen_depth_mm": 18.9, - "flipper_length_mm": 184, - "body_mass_g": 3550, - "sex": "MALE", - }, - "test4": { - "species": "Gentoo penguin (Pygoscelis papua)", - "island": "Biscoe", - "culmen_length_mm": 43.8, - "culmen_depth_mm": 13.9, - "flipper_length_mm": 208, - "body_mass_g": 4300, - "sex": "FEMALE", - }, +_PD_NEW_PENGUINS = pd.DataFrame.from_dict( + { + "test1": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Dream", + "culmen_length_mm": 37.5, + "culmen_depth_mm": 18.5, + "flipper_length_mm": 199, + "body_mass_g": 4475, + "sex": "MALE", }, - orient="index", - ) - pd_new_penguins.index.name = "observation" + "test2": { + "species": "Chinstrap penguin (Pygoscelis antarctica)", + "island": "Dream", + "culmen_length_mm": 55.8, + "culmen_depth_mm": 19.8, + "flipper_length_mm": 207, + "body_mass_g": 4000, + "sex": "MALE", + }, + "test3": { + "species": "Adelie Penguin (Pygoscelis adeliae)", + "island": "Biscoe", + "culmen_length_mm": 39.7, + "culmen_depth_mm": 18.9, + "flipper_length_mm": 184, + "body_mass_g": 3550, + "sex": "MALE", + }, + "test4": { + "species": "Gentoo penguin (Pygoscelis papua)", + "island": "Biscoe", + "culmen_length_mm": 43.8, + "culmen_depth_mm": 13.9, + "flipper_length_mm": 208, + "body_mass_g": 4300, + "sex": "FEMALE", + }, + }, + orient="index", +) - new_penguins = session.read_pandas(pd_new_penguins) - result = penguins_kmeans_model.predict(new_penguins).compute() - expected = pandas.DataFrame( + +def test_kmeans_predict(session, penguins_kmeans_model: cluster.KMeans): + new_penguins = session.read_pandas(_PD_NEW_PENGUINS) + result = penguins_kmeans_model.predict(new_penguins).to_pandas() + expected = pd.DataFrame( {"CENTROID_ID": [2, 3, 1, 2]}, dtype="Int64", - index=pandas.Index( - ["test1", "test2", "test3", "test4"], dtype="string[pyarrow]" - ), + index=pd.Index(["test1", "test2", "test3", "test4"], dtype="string[pyarrow]"), ) - expected.index.name = "observation" assert_pandas_df_equal_ignore_ordering(result, expected) +def test_kmeans_score(session, penguins_kmeans_model: cluster.KMeans): + new_penguins = session.read_pandas(_PD_NEW_PENGUINS) + result = penguins_kmeans_model.score(new_penguins).to_pandas() + expected = pd.DataFrame( + {"davies_bouldin_index": [1.523606], "mean_squared_distance": [1.965944]}, + dtype="Float64", + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + def test_loaded_config(penguins_kmeans_model): assert penguins_kmeans_model.n_clusters == 3 diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index d63e401dd7a..8b864d9b55c 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -27,7 +27,7 @@ def test_model_eval( penguins_bqml_linear_model, ): - result = penguins_bqml_linear_model.evaluate().compute() + result = penguins_bqml_linear_model.evaluate().to_pandas() expected = pd.DataFrame( { "mean_absolute_error": [227.01223], @@ -52,7 +52,7 @@ def test_model_eval( def test_model_eval_with_data(penguins_bqml_linear_model, penguins_df_default_index): result = penguins_bqml_linear_model.evaluate( penguins_df_default_index.dropna() - ).compute() + ).to_pandas() expected = pd.DataFrame( { "mean_absolute_error": [225.817334], @@ -77,7 +77,7 @@ def test_model_eval_with_data(penguins_bqml_linear_model, penguins_df_default_in def test_model_predict( penguins_bqml_linear_model: bigframes.ml.core.BqmlModel, new_penguins_df ): - predictions = penguins_bqml_linear_model.predict(new_penguins_df).compute() + predictions = penguins_bqml_linear_model.predict(new_penguins_df).to_pandas() expected = pd.DataFrame( {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, dtype="Float64", @@ -105,7 +105,7 @@ def test_model_predict_with_unnamed_index( new_penguins_df[new_penguins_df.tag_number != 1672], ) - predictions = penguins_bqml_linear_model.predict(new_penguins_df).compute() + predictions = penguins_bqml_linear_model.predict(new_penguins_df).to_pandas() expected = pd.DataFrame( {"predicted_body_mass_g": [4030.1, 3177.9]}, @@ -132,7 +132,7 @@ def test_model_generate_text( } df = bqml_palm2_text_generator_model.generate_text( llm_text_df, options=options - ).compute() + ).to_pandas() TestCase().assertSequenceEqual(df.shape, (3, 4)) TestCase().assertSequenceEqual( @@ -150,7 +150,7 @@ def test_model_generate_text( def test_model_forecast(time_series_bqml_arima_plus_model: bigframes.ml.core.BqmlModel): utc = pytz.utc - forecast = time_series_bqml_arima_plus_model.forecast().compute()[ + forecast = time_series_bqml_arima_plus_model.forecast().to_pandas()[ ["forecast_timestamp", "forecast_value"] ] expected = pd.DataFrame( diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index 7e0421129eb..01d52077505 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -12,44 +12,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas - -import bigframes.ml.decomposition - - -def test_model_predict(session, penguins_pca_model: bigframes.ml.decomposition.PCA): - new_penguins = session.read_pandas( - pandas.DataFrame( - { - "tag_number": [1633, 1672, 1690], - "species": [ - "Adelie Penguin (Pygoscelis adeliae)", - "Gentoo penguin (Pygoscelis papua)", - "Adelie Penguin (Pygoscelis adeliae)", - ], - "island": ["Dream", "Biscoe", "Torgersen"], - "culmen_length_mm": [37.8, 46.5, 41.1], - "culmen_depth_mm": [18.1, 14.8, 18.6], - "flipper_length_mm": [193.0, 217.0, 189.0], - "body_mass_g": [3750.0, 5200.0, 3325.0], - "sex": ["MALE", "FEMALE", "MALE"], - } - ).set_index("tag_number") - ) +import pandas as pd + +from bigframes.ml import decomposition + +_PD_NEW_PENGUINS = pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Gentoo penguin (Pygoscelis papua)", + "Adelie Penguin (Pygoscelis adeliae)", + ], + "island": ["Dream", "Biscoe", "Torgersen"], + "culmen_length_mm": [37.8, 46.5, 41.1], + "culmen_depth_mm": [18.1, 14.8, 18.6], + "flipper_length_mm": [193.0, 217.0, 189.0], + "body_mass_g": [3750.0, 5200.0, 3325.0], + "sex": ["MALE", "FEMALE", "MALE"], + } +).set_index("tag_number") + - predictions = penguins_pca_model.predict(new_penguins).compute() - expected = pandas.DataFrame( +def test_pca_predict(session, penguins_pca_model: decomposition.PCA): + new_penguins = session.read_pandas(_PD_NEW_PENGUINS) + + predictions = penguins_pca_model.predict(new_penguins).to_pandas() + expected = pd.DataFrame( { "principal_component_1": [-1.459, 2.258, -1.685], "principal_component_2": [-1.120, -1.351, -0.874], "principal_component_3": [-0.646, 0.443, -0.704], }, dtype="Float64", - index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pandas.testing.assert_frame_equal( + pd.testing.assert_frame_equal( predictions.sort_index(), expected, check_exact=False, rtol=0.1, ) + + +def test_pca_score(session, penguins_pca_model: decomposition.PCA): + result = penguins_pca_model.score().to_pandas() + expected = pd.DataFrame( + {"total_explained_variance_ratio": [0.812383]}, + dtype="Float64", + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + ) diff --git a/tests/system/small/ml/test_ensemble.py b/tests/system/small/ml/test_ensemble.py index 61c60c249f2..fde3cc431e1 100644 --- a/tests/system/small/ml/test_ensemble.py +++ b/tests/system/small/ml/test_ensemble.py @@ -36,7 +36,44 @@ def test_xgbregressor_model_score( ] ] test_y = df[["sex"]] - result = penguins_xgbregressor_model.score(test_X, test_y).compute() + result = penguins_xgbregressor_model.score(test_X, test_y).to_pandas() + expected = pandas.DataFrame( + { + "mean_absolute_error": [108.77582], + "mean_squared_error": [20943.272738], + "mean_squared_log_error": [0.00135], + "median_absolute_error": [86.313477], + "r2_score": [0.967571], + "explained_variance": [0.967609], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_xgbregressor_model_score_series( + penguins_xgbregressor_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df["sex"] + result = penguins_xgbregressor_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [108.77582], @@ -61,7 +98,7 @@ def test_xgbregressor_model_score( def test_xgbregressor_model_predict( penguins_xgbregressor_model: bigframes.ml.ensemble.XGBRegressor, new_penguins_df ): - result = penguins_xgbregressor_model.predict(new_penguins_df).compute() + result = penguins_xgbregressor_model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_body_mass_g": ["4293.1538089", "3410.0271", "3357.944"]}, dtype="Float64", @@ -94,7 +131,7 @@ def test_to_gbq_saved_xgbregressor_model_scores( ] ] test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).compute() + result = saved_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [109.016973], @@ -139,7 +176,35 @@ def test_xgbclassifier_model_score( ] ] test_y = df[["sex"]] - result = penguins_xgbclassifier_model.score(test_X, test_y).compute() + result = penguins_xgbclassifier_model.score(test_X, test_y).to_pandas() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + +def test_xgbclassifier_model_score_series( + penguins_xgbclassifier_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df["sex"] + result = penguins_xgbclassifier_model.score(test_X, test_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -155,7 +220,7 @@ def test_xgbclassifier_model_score( def test_xgbclassifier_model_predict( penguins_xgbclassifier_model: bigframes.ml.ensemble.XGBClassifier, new_penguins_df ): - result = penguins_xgbclassifier_model.predict(new_penguins_df).compute() + result = penguins_xgbclassifier_model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, dtype="string[pyarrow]", @@ -188,7 +253,7 @@ def test_to_gbq_saved_xgbclassifier_model_scores( ] ] test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).compute() + result = saved_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "precision": [1.0], @@ -235,7 +300,44 @@ def test_randomforestregressor_model_score( ] ] test_y = df[["sex"]] - result = penguins_randomforest_regressor_model.score(test_X, test_y).compute() + result = penguins_randomforest_regressor_model.score(test_X, test_y).to_pandas() + expected = pandas.DataFrame( + { + "mean_absolute_error": [317.031042], + "mean_squared_error": [159713.053504], + "mean_squared_log_error": [0.008449], + "median_absolute_error": [258.385742], + "r2_score": [0.752698], + "explained_variance": [0.756173], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigFramese + check_index_type=False, + ) + + +def test_randomforestregressor_model_score_series( + penguins_randomforest_regressor_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df["sex"] + result = penguins_randomforest_regressor_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [317.031042], @@ -261,7 +363,7 @@ def test_randomforestregressor_model_predict( penguins_randomforest_regressor_model: bigframes.ml.ensemble.RandomForestRegressor, new_penguins_df, ): - result = penguins_randomforest_regressor_model.predict(new_penguins_df).compute() + result = penguins_randomforest_regressor_model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_body_mass_g": ["3897.341797", "3458.385742", "3458.385742"]}, dtype="Float64", @@ -294,7 +396,7 @@ def test_to_gbq_saved_randomforestregressor_model_scores( ] ] test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).compute() + result = saved_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [319.239235], @@ -343,7 +445,35 @@ def test_randomforestclassifier_model_score( ] ] test_y = df[["sex"]] - result = penguins_randomforest_classifier_model.score(test_X, test_y).compute() + result = penguins_randomforest_classifier_model.score(test_X, test_y).to_pandas() + TestCase().assertSequenceEqual(result.shape, (1, 6)) + for col_name in [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ]: + assert col_name in result.columns + + +def test_randomforestclassifier_model_score_series( + penguins_randomforest_classifier_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df["sex"] + result = penguins_randomforest_classifier_model.score(test_X, test_y).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -360,7 +490,7 @@ def test_randomforestclassifier_model_predict( penguins_randomforest_classifier_model: bigframes.ml.ensemble.RandomForestClassifier, new_penguins_df, ): - result = penguins_randomforest_classifier_model.predict(new_penguins_df).compute() + result = penguins_randomforest_classifier_model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, dtype="string[pyarrow]", @@ -393,7 +523,7 @@ def test_to_gbq_saved_randomforestclassifier_model_scores( ] ] test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).compute() + result = saved_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "precision": [0.636746], diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index 153299f5989..cb27dd388c3 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -21,7 +21,7 @@ def test_model_predict(time_series_arima_plus_model): utc = pytz.utc - predictions = time_series_arima_plus_model.predict().compute() + predictions = time_series_arima_plus_model.predict().to_pandas() expected = pd.DataFrame( { "forecast_timestamp": [ @@ -42,3 +42,47 @@ def test_model_predict(time_series_arima_plus_model): rtol=0.1, check_index_type=False, ) + + +def test_model_score(time_series_arima_plus_model, new_time_series_df): + result = time_series_arima_plus_model.score( + new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] + ).to_pandas() + expected = pd.DataFrame( + { + "mean_absolute_error": [154.742547], + "mean_squared_error": [26844.868855], + "root_mean_squared_error": [163.844038], + "mean_absolute_percentage_error": [6.189702], + "symmetric_mean_absolute_percentage_error": [6.097155], + }, + dtype="Float64", + ) + pd.testing.assert_frame_equal( + result, + expected, + rtol=0.1, + check_index_type=False, + ) + + +def test_model_score_series(time_series_arima_plus_model, new_time_series_df): + result = time_series_arima_plus_model.score( + new_time_series_df["parsed_date"], new_time_series_df["total_visits"] + ).to_pandas() + expected = pd.DataFrame( + { + "mean_absolute_error": [154.742547], + "mean_squared_error": [26844.868855], + "root_mean_squared_error": [163.844038], + "mean_absolute_percentage_error": [6.189702], + "symmetric_mean_absolute_percentage_error": [6.097155], + }, + dtype="Float64", + ) + pd.testing.assert_frame_equal( + result, + expected, + rtol=0.1, + check_index_type=False, + ) diff --git a/tests/system/small/ml/test_imported.py b/tests/system/small/ml/test_imported.py index b2a9abc46e7..6274ab12450 100644 --- a/tests/system/small/ml/test_imported.py +++ b/tests/system/small/ml/test_imported.py @@ -12,8 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import google.api_core.exceptions import numpy as np import pandas as pd +import pytest + +from bigframes.ml import imported def test_tensorflow_create_model(imported_tensorflow_model): @@ -23,7 +27,7 @@ def test_tensorflow_create_model(imported_tensorflow_model): def test_tensorflow_model_predict(imported_tensorflow_model, llm_text_df): df = llm_text_df.rename(columns={"prompt": "input"}) - result = imported_tensorflow_model.predict(df).compute() + result = imported_tensorflow_model.predict(df).to_pandas() # The values are non-human-readable. As they are a dense layer of Neural Network. # And since it is pretrained and imported, the model is a opaque-box. # We may want to switch to better test model and cases. @@ -44,13 +48,21 @@ def test_tensorflow_model_predict(imported_tensorflow_model, llm_text_df): ) +def test_tensorflow_model_to_gbq( + imported_tensorflow_model: imported.TensorFlowModel, dataset_id: str +): + imported_tensorflow_model.to_gbq(f"{dataset_id}.test_tf_model", replace=True) + with pytest.raises(google.api_core.exceptions.Conflict): + imported_tensorflow_model.to_gbq(f"{dataset_id}.test_tf_model") + + def test_onnx_create_model(imported_onnx_model): # Model creation doesn't return error assert imported_onnx_model is not None def test_onnx_model_predict(imported_onnx_model, onnx_iris_df): - result = imported_onnx_model.predict(onnx_iris_df).compute() + result = imported_onnx_model.predict(onnx_iris_df).to_pandas() value1 = np.array([0.9999993443489075, 0.0, 0.0]) value2 = np.array([0.0, 0.0, 0.9999993443489075]) expected = pd.DataFrame( @@ -66,3 +78,9 @@ def test_onnx_model_predict(imported_onnx_model, onnx_iris_df): check_exact=False, atol=0.1, ) + + +def test_onnx_model_to_gbq(imported_onnx_model: imported.ONNXModel, dataset_id: str): + imported_onnx_model.to_gbq(f"{dataset_id}.test_onnx_model", replace=True) + with pytest.raises(google.api_core.exceptions.Conflict): + imported_onnx_model.to_gbq(f"{dataset_id}.test_onnx_model") diff --git a/tests/system/small/ml/test_linear_model.py b/tests/system/small/ml/test_linear_model.py index d5d6957eec3..bbb7e2820c3 100644 --- a/tests/system/small/ml/test_linear_model.py +++ b/tests/system/small/ml/test_linear_model.py @@ -30,7 +30,44 @@ def test_linear_reg_model_score(penguins_linear_model, penguins_df_default_index ] ] test_y = df[["body_mass_g"]] - result = penguins_linear_model.score(test_X, test_y).compute() + result = penguins_linear_model.score(test_X, test_y).to_pandas() + expected = pandas.DataFrame( + { + "mean_absolute_error": [225.817334], + "mean_squared_error": [80540.705944], + "mean_squared_log_error": [0.004972], + "median_absolute_error": [173.080816], + "r2_score": [0.87529], + "explained_variance": [0.87529], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_linear_reg_model_score_series( + penguins_linear_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + test_y = df["body_mass_g"] + result = penguins_linear_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [225.817334], @@ -53,7 +90,7 @@ def test_linear_reg_model_score(penguins_linear_model, penguins_df_default_index def test_linear_reg_model_predict(penguins_linear_model, new_penguins_df): - predictions = penguins_linear_model.predict(new_penguins_df).compute() + predictions = penguins_linear_model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, dtype="Float64", @@ -85,7 +122,7 @@ def test_to_gbq_saved_linear_reg_model_scores( ] ] test_y = df[["body_mass_g"]] - result = saved_model.score(test_X, test_y).compute() + result = saved_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [227.01223], @@ -126,7 +163,44 @@ def test_logistic_model_score(penguins_logistic_model, penguins_df_default_index ] ] test_y = df[["sex"]] - result = penguins_logistic_model.score(test_X, test_y).compute() + result = penguins_logistic_model.score(test_X, test_y).to_pandas() + expected = pandas.DataFrame( + { + "precision": [0.616753], + "recall": [0.618615], + "accuracy": [0.92515], + "f1_score": [0.617681], + "log_loss": [1.498832], + "roc_auc": [0.975807], + }, + dtype="Float64", + ) + pandas.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + ) + + +def test_logistic_model_score_series( + penguins_logistic_model, penguins_df_default_index +): + df = penguins_df_default_index.dropna() + test_X = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + ] + ] + test_y = df["sex"] + result = penguins_logistic_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "precision": [0.616753], @@ -149,7 +223,7 @@ def test_logistic_model_score(penguins_logistic_model, penguins_df_default_index def test_logsitic_model_predict(penguins_logistic_model, new_penguins_df): - predictions = penguins_logistic_model.predict(new_penguins_df).compute() + predictions = penguins_logistic_model.predict(new_penguins_df).to_pandas() expected = pandas.DataFrame( {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, dtype="string[pyarrow]", @@ -163,7 +237,7 @@ def test_logsitic_model_predict(penguins_logistic_model, new_penguins_df): ) -def test_to_gbq_saved_logsitic_model_score( +def test_logsitic_model_to_gbq_saved_score( penguins_logistic_model, dataset_id, penguins_df_default_index ): saved_model = penguins_logistic_model.to_gbq( @@ -181,7 +255,7 @@ def test_to_gbq_saved_logsitic_model_score( ] ] test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).compute() + result = saved_model.score(test_X, test_y).to_pandas() expected = pandas.DataFrame( { "precision": [0.616753], @@ -203,7 +277,7 @@ def test_to_gbq_saved_logsitic_model_score( ) -def test_to_logistic_model_gbq_replace(penguins_logistic_model, dataset_id): +def test_logistic_model_to_gbq_replace(penguins_logistic_model, dataset_id): penguins_logistic_model.to_gbq(f"{dataset_id}.test_penguins_model", replace=True) with pytest.raises(google.api_core.exceptions.Conflict): penguins_logistic_model.to_gbq(f"{dataset_id}.test_penguins_model") diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index b579e754e5a..74356c81e1b 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -25,7 +25,17 @@ def test_create_text_generator_model(palm2_text_generator_model): def test_text_generator_predict_default_params_success( palm2_text_generator_model, llm_text_df ): - df = palm2_text_generator_model.predict(llm_text_df).compute() + df = palm2_text_generator_model.predict(llm_text_df).to_pandas() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +def test_text_generator_predict_series_default_params_success( + palm2_text_generator_model, llm_text_df +): + df = palm2_text_generator_model.predict(llm_text_df["prompt"]).to_pandas() TestCase().assertSequenceEqual(df.shape, (3, 1)) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] @@ -36,7 +46,7 @@ def test_text_generator_predict_arbitrary_col_label_success( palm2_text_generator_model, llm_text_df ): llm_text_df = llm_text_df.rename(columns={"prompt": "arbitrary"}) - df = palm2_text_generator_model.predict(llm_text_df).compute() + df = palm2_text_generator_model.predict(llm_text_df).to_pandas() TestCase().assertSequenceEqual(df.shape, (3, 1)) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] @@ -48,7 +58,7 @@ def test_text_generator_predict_with_params_success( ): df = palm2_text_generator_model.predict( llm_text_df, temperature=0.5, max_output_tokens=100, top_k=20, top_p=0.5 - ).compute() + ).to_pandas() TestCase().assertSequenceEqual(df.shape, (3, 1)) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] @@ -63,10 +73,22 @@ def test_create_embedding_generator_model(palm2_embedding_generator_model): def test_embedding_generator_predict_success( palm2_embedding_generator_model, llm_text_df ): - df = palm2_embedding_generator_model.predict(llm_text_df).compute() + df = palm2_embedding_generator_model.predict(llm_text_df).to_pandas() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "text_embedding" in df.columns + series = df["text_embedding"] + value = series[0] + assert isinstance(value, np.ndarray) + assert value.size == 768 + + +def test_embedding_generator_predict_series_success( + palm2_embedding_generator_model, llm_text_df +): + df = palm2_embedding_generator_model.predict(llm_text_df["prompt"]).to_pandas() TestCase().assertSequenceEqual(df.shape, (3, 1)) - assert "ml_embed_text_embedding" in df.columns - series = df["ml_embed_text_embedding"] + assert "text_embedding" in df.columns + series = df["text_embedding"] value = series[0] assert isinstance(value, np.ndarray) assert value.size == 768 diff --git a/tests/system/small/ml/test_metrics.py b/tests/system/small/ml/test_metrics.py index 5b4c486b570..b40982e2829 100644 --- a/tests/system/small/ml/test_metrics.py +++ b/tests/system/small/ml/test_metrics.py @@ -23,10 +23,20 @@ def test_r2_score_perfect_fit(session): - pd_df = pd.DataFrame({"y_true": [1, 7, 3, 2, 5], "y_pred": [1, 7, 3, 2, 5]}) + pd_df = pd.DataFrame( + { + "y_true_arbitrary_name": [1, 7, 3, 2, 5], + "y_pred_arbitrary_name": [1, 7, 3, 2, 5], + } + ) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred"]]) == 1.0 + assert ( + bigframes.ml.metrics.r2_score( + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]] + ) + == 1.0 + ) def test_r2_score_bad_fit(session): @@ -67,11 +77,28 @@ def test_r2_score_ok_fit_matches_sklearn(session): assert math.isclose(bf_result, sklearn_result) -def test_accuracy_score_perfect_fit(session): +def test_r2_score_series(session): pd_df = pd.DataFrame({"y_true": [1, 7, 3, 2, 5], "y_pred": [1, 7, 3, 2, 5]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) == 1.0 + assert bigframes.ml.metrics.r2_score(df["y_true"], df["y_pred"]) == 1.0 + + +def test_accuracy_score_perfect_fit(session): + pd_df = pd.DataFrame( + { + "y_true_arbitrary_name": [1, 7, 3, 2, 5], + "y_pred_arbitrary_name": [1, 7, 3, 2, 5], + } + ) + + df = session.read_pandas(pd_df) + assert ( + bigframes.ml.metrics.accuracy_score( + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]] + ) + == 1.0 + ) def test_accuracy_score_bad_fit(session): @@ -93,7 +120,6 @@ def test_accuracy_score_not_normailze(session): ) -@pytest.mark.skipif(sklearn_metrics is None, reason="requires sklearn") def test_accuracy_score_fit_matches_sklearn(session): pd_df = pd.DataFrame({"y_true": [1, 2, 3, 4, 5], "y_pred": [2, 3, 4, 3, 6]}) @@ -105,22 +131,42 @@ def test_accuracy_score_fit_matches_sklearn(session): assert math.isclose(bf_result, sklearn_result) +def test_accuracy_score_series(session): + pd_df = pd.DataFrame({"y_true": [1, 7, 3, 2, 5], "y_pred": [1, 7, 3, 2, 5]}) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.accuracy_score(df["y_true"], df["y_pred"]) == 1.0 + + def test_roc_curve_binary_classification_prediction_returns_expected(session): pd_df = pd.DataFrame( { - "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], - "y_score": [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45], + "y_true_arbitrary_name": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score_arbitrary_name": [ + 0.1, + 0.4, + 0.35, + 0.8, + 0.65, + 0.9, + 0.5, + 0.3, + 0.6, + 0.45, + ], } ) df = session.read_pandas(pd_df) fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( - df[["y_true"]], df[["y_score"]], drop_intermediate=False + df[["y_true_arbitrary_name"]], + df[["y_score_arbitrary_name"]], + drop_intermediate=False, ) - pd_fpr = fpr.compute() - pd_tpr = tpr.compute() - pd_thresholds = thresholds.compute() + pd_fpr = fpr.to_pandas() + pd_tpr = tpr.to_pandas() + pd_thresholds = thresholds.to_pandas() pd.testing.assert_series_equal( # skip testing the first value, as it is redundant and inconsistent across sklearn versions @@ -181,9 +227,9 @@ def test_roc_curve_binary_classification_prediction_matches_sklearn(session): ) # sklearn returns float64 np arrays - np_fpr = fpr.compute().astype("float64").array - np_tpr = tpr.compute().astype("float64").array - np_thresholds = thresholds.compute().astype("float64").array + np_fpr = fpr.to_pandas().astype("float64").array + np_tpr = tpr.to_pandas().astype("float64").array + np_thresholds = thresholds.to_pandas().astype("float64").array np.testing.assert_array_equal( # skip testing the first value, as it is redundant and inconsistent across sklearn versions @@ -217,9 +263,9 @@ def test_roc_curve_binary_classification_decision_returns_expected(session): df[["y_true"]], df[["y_score"]], drop_intermediate=False ) - pd_fpr = fpr.compute() - pd_tpr = tpr.compute() - pd_thresholds = thresholds.compute() + pd_fpr = fpr.to_pandas() + pd_tpr = tpr.to_pandas() + pd_thresholds = thresholds.to_pandas() pd.testing.assert_series_equal( # skip testing the first value, as it is redundant and inconsistent across sklearn versions @@ -276,9 +322,9 @@ def test_roc_curve_binary_classification_decision_matches_sklearn(session): ) # sklearn returns float64 np arrays - np_fpr = fpr.compute().astype("float64").array - np_tpr = tpr.compute().astype("float64").array - np_thresholds = thresholds.compute().astype("float64").array + np_fpr = fpr.to_pandas().astype("float64").array + np_tpr = tpr.to_pandas().astype("float64").array + np_thresholds = thresholds.to_pandas().astype("float64").array np.testing.assert_array_equal( # skip testing the first value, as it is redundant and inconsistent across sklearn versions @@ -295,7 +341,7 @@ def test_roc_curve_binary_classification_decision_matches_sklearn(session): ) -def test_roc_auc_score_returns_expected(session): +def test_roc_curve_binary_classification_prediction_series(session): pd_df = pd.DataFrame( { "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], @@ -304,7 +350,79 @@ def test_roc_auc_score_returns_expected(session): ) df = session.read_pandas(pd_df) - score = bigframes.ml.metrics.roc_auc_score(df[["y_true"]], df[["y_score"]]) + fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + df["y_true"], df["y_score"], drop_intermediate=False + ) + + pd_fpr = fpr.to_pandas() + pd_tpr = tpr.to_pandas() + pd_thresholds = thresholds.to_pandas() + + pd.testing.assert_series_equal( + # skip testing the first value, as it is redundant and inconsistent across sklearn versions + pd_thresholds[1:], + pd.Series( + [0.9, 0.8, 0.65, 0.6, 0.5, 0.45, 0.4, 0.35, 0.3, 0.1], + dtype="Float64", + name="thresholds", + ), + check_index=False, + ) + pd.testing.assert_series_equal( + pd_fpr, + pd.Series( + [0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 0.75, 1.0], + dtype="Float64", + name="fpr", + ), + check_index_type=False, + ) + pd.testing.assert_series_equal( + pd_tpr, + pd.Series( + [ + 0.0, + 0.16666667, + 0.33333333, + 0.33333333, + 0.5, + 0.5, + 0.66666667, + 0.66666667, + 0.83333333, + 1.0, + 1.0, + ], + dtype="Float64", + name="tpr", + ), + check_index_type=False, + ) + + +def test_roc_auc_score_returns_expected(session): + pd_df = pd.DataFrame( + { + "y_true_arbitrary_name": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score_arbitrary_name": [ + 0.1, + 0.4, + 0.35, + 0.8, + 0.65, + 0.9, + 0.5, + 0.3, + 0.6, + 0.45, + ], + } + ) + + df = session.read_pandas(pd_df) + score = bigframes.ml.metrics.roc_auc_score( + df[["y_true_arbitrary_name"]], df[["y_score_arbitrary_name"]] + ) assert score == 0.625 @@ -326,11 +444,25 @@ def test_roc_auc_score_returns_matches_sklearn(session): assert score == expected_score +def test_roc_auc_score_series(session): + pd_df = pd.DataFrame( + { + "y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + "y_score": [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45], + } + ) + + df = session.read_pandas(pd_df) + score = bigframes.ml.metrics.roc_auc_score(df["y_true"], df["y_score"]) + + assert score == 0.625 + + def test_auc_invalid_x_size(session): - pd_df = pd.DataFrame({"x": [0], "y": [0]}) + pd_df = pd.DataFrame({"x_arbitrary_name": [0], "y_arbitrary_name": [0]}) df = session.read_pandas(pd_df) with pytest.raises(ValueError): - bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) + bigframes.ml.metrics.auc(df[["x_arbitrary_name"]], df[["y_arbitrary_name"]]) def test_auc_nondecreasing_x(session): @@ -352,16 +484,23 @@ def test_auc_nonincreasing_x_negative(session): assert bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) == -0.75 +def test_auc_series(session): + pd_df = pd.DataFrame({"x": [0, 0, 0.5, 0.5, 1], "y": [0, 0.5, 0.5, 1, 1]}) + + df = session.read_pandas(pd_df) + assert bigframes.ml.metrics.auc(df["x"], df["y"]) == 0.75 + + def test_confusion_matrix(session): pd_df = pd.DataFrame( { - "y_true": [2, 0, 2, 2, 0, 1], - "y_pred": [0, 0, 2, 2, 0, 2], + "y_true_arbitrary_name": [2, 0, 2, 2, 0, 1], + "y_pred_arbitrary_name": [0, 0, 2, 2, 0, 2], } ).astype("Int64") df = session.read_pandas(pd_df) confusion_matrix = bigframes.ml.metrics.confusion_matrix( - df[["y_true"]], df[["y_pred"]] + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]] ) expected_pd_df = pd.DataFrame( { @@ -441,7 +580,7 @@ def test_confusion_matrix_str_matches_sklearn(session): ) -def test_recall_score(session): +def test_confusion_matrix_series(session): pd_df = pd.DataFrame( { "y_true": [2, 0, 2, 2, 0, 1], @@ -449,8 +588,29 @@ def test_recall_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) + confusion_matrix = bigframes.ml.metrics.confusion_matrix(df["y_true"], df["y_pred"]) + expected_pd_df = pd.DataFrame( + { + 0: [2, 0, 1], + 1: [0, 0, 0], + 2: [0, 1, 2], + } + ).astype("int64") + pd.testing.assert_frame_equal( + confusion_matrix, expected_pd_df, check_index_type=False + ) + + +def test_recall_score(session): + pd_df = pd.DataFrame( + { + "y_true_arbitrary_name": [2, 0, 2, 2, 0, 1], + "y_pred_arbitrary_name": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) recall = bigframes.ml.metrics.recall_score( - df[["y_true"]], df[["y_pred"]], average=None + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [1.000000, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -497,7 +657,7 @@ def test_recall_score_str_matches_sklearn(session): pd.testing.assert_series_equal(recall, expected_recall, check_index_type=False) -def test_precision_score(session): +def test_recall_score_series(session): pd_df = pd.DataFrame( { "y_true": [2, 0, 2, 2, 0, 1], @@ -505,8 +665,24 @@ def test_precision_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) + recall = bigframes.ml.metrics.recall_score(df["y_true"], df["y_pred"], average=None) + expected_values = [1.000000, 0.000000, 0.666667] + expected_index = [0, 1, 2] + expected_recall = pd.Series(expected_values, index=expected_index) + + pd.testing.assert_series_equal(recall, expected_recall, check_index_type=False) + + +def test_precision_score(session): + pd_df = pd.DataFrame( + { + "y_true_arbitrary_name": [2, 0, 2, 2, 0, 1], + "y_pred_arbitrary_name": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) precision_score = bigframes.ml.metrics.precision_score( - df[["y_true"]], df[["y_pred"]], average=None + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [0.666667, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -559,7 +735,7 @@ def test_precision_score_str_matches_sklearn(session): ) -def test_f1_score(session): +def test_precision_score_series(session): pd_df = pd.DataFrame( { "y_true": [2, 0, 2, 2, 0, 1], @@ -567,8 +743,28 @@ def test_f1_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) + precision_score = bigframes.ml.metrics.precision_score( + df["y_true"], df["y_pred"], average=None + ) + expected_values = [0.666667, 0.000000, 0.666667] + expected_index = [0, 1, 2] + expected_precision = pd.Series(expected_values, index=expected_index) + + pd.testing.assert_series_equal( + precision_score, expected_precision, check_index_type=False + ) + + +def test_f1_score(session): + pd_df = pd.DataFrame( + { + "y_true_arbitrary_name": [2, 0, 2, 2, 0, 1], + "y_pred_arbitrary_name": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) f1_score = bigframes.ml.metrics.f1_score( - df[["y_true"]], df[["y_pred"]], average=None + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [0.8, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -613,3 +809,19 @@ def test_f1_score_str_matches_sklearn(session): expected_index = ["ant", "bird", "cat"] expected_f1 = pd.Series(expected_values, index=expected_index) pd.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + + +def test_f1_score_series(session): + pd_df = pd.DataFrame( + { + "y_true": [2, 0, 2, 2, 0, 1], + "y_pred": [0, 0, 2, 2, 0, 2], + } + ).astype("Int64") + df = session.read_pandas(pd_df) + f1_score = bigframes.ml.metrics.f1_score(df["y_true"], df["y_pred"], average=None) + expected_values = [0.8, 0.000000, 0.666667] + expected_index = [0, 1, 2] + expected_f1 = pd.Series(expected_values, index=expected_index) + + pd.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) diff --git a/tests/system/small/ml/test_model_selection.py b/tests/system/small/ml/test_model_selection.py index add455ab9ac..9eb36455913 100644 --- a/tests/system/small/ml/test_model_selection.py +++ b/tests/system/small/ml/test_model_selection.py @@ -15,7 +15,8 @@ import pandas as pd import pytest -import bigframes.ml.model_selection +from bigframes.ml import model_selection +import bigframes.pandas as bpd def test_train_test_split_default_correct_shape(penguins_df_default_index): @@ -27,9 +28,7 @@ def test_train_test_split_default_correct_shape(penguins_df_default_index): ] ] y = penguins_df_default_index[["body_mass_g"]] - X_train, X_test, y_train, y_test = bigframes.ml.model_selection.train_test_split( - X, y - ) + X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y) # even though the default seed is random, it should always result in this shape assert X_train.shape == (258, 3) @@ -38,6 +37,22 @@ def test_train_test_split_default_correct_shape(penguins_df_default_index): assert y_test.shape == (86, 1) +def test_train_test_split_series_default_correct_shape(penguins_df_default_index): + X = penguins_df_default_index[["species"]] + y = penguins_df_default_index["body_mass_g"] + X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y) + assert isinstance(X_train, bpd.DataFrame) + assert isinstance(X_test, bpd.DataFrame) + assert isinstance(y_train, bpd.Series) + assert isinstance(y_test, bpd.Series) + + # even though the default seed is random, it should always result in this shape + assert X_train.shape == (258, 1) + assert X_test.shape == (86, 1) + assert y_train.shape == (258,) + assert y_test.shape == (86,) + + def test_train_test_double_split_correct_shape(penguins_df_default_index): X = penguins_df_default_index[ [ @@ -47,7 +62,7 @@ def test_train_test_double_split_correct_shape(penguins_df_default_index): ] ] y = penguins_df_default_index[["body_mass_g"]] - X_train, X_test, y_train, y_test = bigframes.ml.model_selection.train_test_split( + X_train, X_test, y_train, y_test = model_selection.train_test_split( X, y, test_size=0.2, train_size=0.4 ) @@ -78,7 +93,7 @@ def test_train_test_three_dataframes_correct_shape(penguins_df_default_index): B_test, C_train, C_test, - ) = bigframes.ml.model_selection.train_test_split(A, B, C) + ) = model_selection.train_test_split(A, B, C) assert A_train.shape == (258, 2) assert A_test.shape == (86, 2) @@ -111,7 +126,7 @@ def test_train_test_split_seeded_correct_rows( ] ] y = df[["body_mass_g"]] - X_train, X_test, y_train, y_test = bigframes.ml.model_selection.train_test_split( + X_train, X_test, y_train, y_test = model_selection.train_test_split( X, y, random_state=42 ) @@ -124,31 +139,31 @@ def test_train_test_split_seeded_correct_rows( [ 144, 146, - 148, 168, 183, 186, 217, + 221, 225, - 226, 237, + 240, 244, 245, + 257, 260, 262, 263, + 264, 266, + 267, 268, - 269, - 289, 290, - 291, ], dtype="Int64", name="rowindex", ) test_index = pd.Index( - [161, 221, 240, 257, 264, 267, 278], dtype="Int64", name="rowindex" + [148, 161, 226, 269, 278, 289, 291], dtype="Int64", name="rowindex" ) all_data.index.name = "_" @@ -209,6 +224,6 @@ def test_train_test_split_value_error(penguins_df_default_index, train_size, tes ] y = penguins_df_default_index[["body_mass_g"]] with pytest.raises(ValueError): - bigframes.ml.model_selection.train_test_split( + model_selection.train_test_split( X, y, train_size=train_size, test_size=test_size ) diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index f31b93b4cc2..420a80754fb 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -20,7 +20,7 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): - # TODO(bmil): add a second test that compares output to sklearn.preprocessing.StandardScaler + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. scaler = bigframes.ml.preprocessing.StandardScaler() scaler.fit( penguins_df_default_index[ @@ -34,10 +34,9 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): ] ).to_pandas() - # If standard-scaled correctly, mean should be 0.0 and standard deviation 1.0 + # If standard-scaled correctly, mean should be 0.0 for column in result.columns: assert math.isclose(result[column].mean(), 0.0, abs_tol=1e-3) - assert math.isclose(result[column].std(), 1.0, abs_tol=1e-3) result = scaler.transform(new_penguins_df).to_pandas() @@ -48,9 +47,9 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): expected = pd.DataFrame( { - "scaled_culmen_depth_mm": [0.8349, 0.02473, 0.4805], - "scaled_culmen_length_mm": [-0.8099, -0.9931, -1.103], - "scaled_flipper_length_mm": [-0.3495, -1.4163, -0.9185], + "scaled_culmen_depth_mm": [0.836148, 0.024748, 0.48116], + "scaled_culmen_length_mm": [-0.81112, -0.994552, -1.104611], + "scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], }, dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), @@ -59,9 +58,42 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): pd.testing.assert_frame_equal(result, expected, rtol=1e-3) -def test_one_hot_encoder_encodes(penguins_df_default_index, new_penguins_df): +def test_standard_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. + scaler = bigframes.ml.preprocessing.StandardScaler() + scaler.fit(penguins_df_default_index["culmen_length_mm"]) + + result = scaler.transform(penguins_df_default_index["culmen_length_mm"]).to_pandas() + + # If standard-scaled correctly, mean should be 0.0 + for column in result.columns: + assert math.isclose(result[column].mean(), 0.0, abs_tol=1e-3) + + result = scaler.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "scaled_culmen_length_mm": [ + -0.811119671289163, + -0.9945520581113803, + -1.104611490204711, + ], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_one_hot_encoder_default_params(new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder() - encoder.fit(penguins_df_default_index["species", "sex"]) + encoder.fit(new_penguins_df["species", "sex"]) result = encoder.transform(new_penguins_df).to_pandas() @@ -87,3 +119,91 @@ def test_one_hot_encoder_encodes(penguins_df_default_index, new_penguins_df): ) pd.testing.assert_frame_equal(result, expected) + + +def test_one_hot_encoder_series_default_params(new_penguins_df): + encoder = bigframes.ml.preprocessing.OneHotEncoder() + encoder.fit(new_penguins_df["species"]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +def test_one_hot_encoder_params(new_penguins_df): + encoder = bigframes.ml.preprocessing.OneHotEncoder("most_frequent", 100, 2) + encoder.fit(new_penguins_df["species", "sex"]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "onehotencoded_sex": [ + [{"index": 0, "value": 1.0}], + [{"index": 0, "value": 1.0}], + [{"index": 0, "value": 1.0}], + ], + "onehotencoded_species": [ + [{"index": 0, "value": 1.0}], + [{"index": 0, "value": 1.0}], + [{"index": 0, "value": 1.0}], + ], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_df): + encoder = bigframes.ml.preprocessing.OneHotEncoder() + encoder.fit(penguins_df_default_index["species", "sex"]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "onehotencoded_sex": [ + [{"index": 3, "value": 1.0}], + [{"index": 2, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +# TODO(garrettwu): add OneHotEncoder tests to compare with sklearn. diff --git a/tests/system/small/ml/test_register.py b/tests/system/small/ml/test_register.py index ad378d35084..bcf1f4a5b0e 100644 --- a/tests/system/small/ml/test_register.py +++ b/tests/system/small/ml/test_register.py @@ -28,7 +28,7 @@ def test_linear_reg_register( ) # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. assert ( - model_name + model_name[:63] # truncated in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ "vertexAiModelId" ] @@ -44,7 +44,7 @@ def test_linear_reg_register_with_params( # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. assert ( - model_name + model_name[:63] # truncated in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ "vertexAiModelId" ] @@ -62,7 +62,7 @@ def test_palm2_text_generator_register( ) # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. assert ( - model_name + model_name[:63] # truncated in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ "vertexAiModelId" ] @@ -80,7 +80,7 @@ def test_imported_tensorflow_register( ) # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. assert ( - model_name + model_name[:63] # truncated in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ "vertexAiModelId" ] diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 25d3eb69adb..7dc55b9367c 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -30,7 +30,7 @@ def test_day(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.day.compute() + bf_result = bf_series.dt.day.to_pandas() pd_result = scalars_pandas_df[col_name].dt.day assert_series_equal_ignoring_order( @@ -48,7 +48,7 @@ def test_date(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.date.compute() + bf_result = bf_series.dt.date.to_pandas() pd_result = scalars_pandas_df[col_name].dt.date assert_series_equal_ignoring_order( @@ -66,7 +66,7 @@ def test_dayofweek(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.dayofweek.compute() + bf_result = bf_series.dt.dayofweek.to_pandas() pd_result = scalars_pandas_df[col_name].dt.dayofweek assert_series_equal_ignoring_order(pd_result, bf_result, check_dtype=False) @@ -81,7 +81,7 @@ def test_hour(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.hour.compute() + bf_result = bf_series.dt.hour.to_pandas() pd_result = scalars_pandas_df[col_name].dt.hour assert_series_equal_ignoring_order( @@ -99,7 +99,7 @@ def test_minute(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.minute.compute() + bf_result = bf_series.dt.minute.to_pandas() pd_result = scalars_pandas_df[col_name].dt.minute assert_series_equal_ignoring_order( @@ -117,7 +117,7 @@ def test_month(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.month.compute() + bf_result = bf_series.dt.month.to_pandas() pd_result = scalars_pandas_df[col_name].dt.month assert_series_equal_ignoring_order( @@ -135,7 +135,7 @@ def test_quarter(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.quarter.compute() + bf_result = bf_series.dt.quarter.to_pandas() pd_result = scalars_pandas_df[col_name].dt.quarter assert_series_equal_ignoring_order( @@ -153,7 +153,7 @@ def test_second(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.second.compute() + bf_result = bf_series.dt.second.to_pandas() pd_result = scalars_pandas_df[col_name].dt.second assert_series_equal_ignoring_order( @@ -171,7 +171,7 @@ def test_time(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.time.compute() + bf_result = bf_series.dt.time.to_pandas() pd_result = scalars_pandas_df[col_name].dt.time assert_series_equal_ignoring_order( @@ -189,7 +189,7 @@ def test_year(scalars_dfs, col_name): pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.dt.year.compute() + bf_result = bf_series.dt.year.to_pandas() pd_result = scalars_pandas_df[col_name].dt.year assert_series_equal_ignoring_order( diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 76736e3cddd..d7bf3312f09 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + import pandas as pd import pytest @@ -24,7 +26,7 @@ def test_find(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.find("W").compute() + bf_result = bf_series.str.find("W").to_pandas() pd_result = scalars_pandas_df[col_name].str.find("W") # One of type mismatches to be documented. Here, the `bf_result.dtype` is `Int64` but @@ -35,11 +37,138 @@ def test_find(scalars_dfs): ) +@pytest.mark.parametrize( + ("pat", "case", "flags", "regex"), + [ + ("hEllo", True, 0, False), + ("hEllo", False, 0, False), + ("hEllo", False, re.I, True), + (".*", True, 0, True), + (".*", True, 0, False), + ], +) +def test_str_contains(scalars_dfs, pat, case, flags, regex): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + + bf_result = bf_series.str.contains( + pat, case=case, flags=flags, regex=regex + ).to_pandas() + pd_result = scalars_pandas_df[col_name].str.contains( + pat, case=case, flags=flags, regex=regex + ) + + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ("pat"), + [(r"(ell)(lo)"), (r"(?Ph..)"), (r"(?Pe.*o)([g-l]+)")], +) +def test_str_extract(scalars_dfs, pat): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + + bf_result = bf_series.str.extract(pat).to_pandas() + pd_result = scalars_pandas_df[col_name].str.extract(pat) + + # Pandas produces int col labels, while bq df only supports str labels at present + pd_result = pd_result.set_axis(pd_result.columns.astype(str), axis=1) + pd.testing.assert_frame_equal( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ("pat", "repl", "case", "flags", "regex"), + [ + ("hEllo", "blah", True, 0, False), + ("hEllo", "blah", False, 0, False), + ("hEllo", "blah", False, re.I, True), + (".*", "blah", True, 0, True), + ("h.l", "blah", False, 0, True), + (re.compile("(?i).e.."), "blah", None, 0, True), + ], +) +def test_str_replace(scalars_dfs, pat, repl, case, flags, regex): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + + bf_result = bf_series.str.replace( + pat, repl=repl, case=case, flags=flags, regex=regex + ).to_pandas() + pd_result = scalars_pandas_df[col_name].str.replace( + pat, repl=repl, case=case, flags=flags, regex=regex + ) + + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ("pat",), + [ + ("こん",), + ("Tag!",), + ( + ( + "Tag!", + "Hel", + ), + ), + ], +) +def test_str_startswith(scalars_dfs, pat): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + pd_series = scalars_pandas_df[col_name].astype("object") + + bf_result = bf_series.str.startswith(pat).to_pandas() + pd_result = pd_series.str.startswith(pat) + + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("pat",), + [ + ("こん",), + ("Tag!",), + ( + ( + "Tag!", + "Hel", + ), + ), + ], +) +def test_str_endswith(scalars_dfs, pat): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + pd_series = scalars_pandas_df[col_name].astype("object") + + bf_result = bf_series.str.endswith(pat).to_pandas() + pd_result = pd_series.str.endswith(pat) + + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + def test_len(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.len().compute() + bf_result = bf_series.str.len().to_pandas() pd_result = scalars_pandas_df[col_name].str.len() # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is `Int64` but @@ -54,7 +183,7 @@ def test_lower(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.lower().compute() + bf_result = bf_series.str.lower().to_pandas() pd_result = scalars_pandas_df[col_name].str.lower() assert_series_equal_ignoring_order( @@ -67,7 +196,7 @@ def test_reverse(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.reverse().compute() + bf_result = bf_series.str.reverse().to_pandas() pd_result = scalars_pandas_df[col_name].copy() for i in pd_result.index: cell = pd_result.loc[i] @@ -89,7 +218,7 @@ def test_slice(scalars_dfs, start, stop): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.slice(start, stop).compute() + bf_result = bf_series.str.slice(start, stop).to_pandas() pd_series = scalars_pandas_df[col_name] pd_result = pd_series.str.slice(start, stop) @@ -103,7 +232,7 @@ def test_strip(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.strip().compute() + bf_result = bf_series.str.strip().to_pandas() pd_result = scalars_pandas_df[col_name].str.strip() assert_series_equal_ignoring_order( @@ -116,7 +245,7 @@ def test_upper(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.upper().compute() + bf_result = bf_series.str.upper().to_pandas() pd_result = scalars_pandas_df[col_name].str.upper() assert_series_equal_ignoring_order( @@ -149,7 +278,7 @@ def test_isnumeric(session): df = session.read_pandas(pandas_df) pd_result = pandas_df.numeric_string_col.str.isnumeric() - bf_result = df.numeric_string_col.str.isnumeric().compute() + bf_result = df.numeric_string_col.str.isnumeric().to_pandas() assert_series_equal_ignoring_order( bf_result, @@ -163,7 +292,7 @@ def test_rstrip(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.rstrip().compute() + bf_result = bf_series.str.rstrip().to_pandas() pd_result = scalars_pandas_df[col_name].str.rstrip() assert_series_equal_ignoring_order( @@ -176,7 +305,7 @@ def test_lstrip(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.lstrip().compute() + bf_result = bf_series.str.lstrip().to_pandas() pd_result = scalars_pandas_df[col_name].str.lstrip() assert_series_equal_ignoring_order( @@ -190,7 +319,7 @@ def test_repeat(scalars_dfs, repeats): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.repeat(repeats).compute() + bf_result = bf_series.str.repeat(repeats).to_pandas() pd_result = scalars_pandas_df[col_name].str.repeat(repeats) assert_series_equal_ignoring_order( @@ -203,7 +332,7 @@ def test_capitalize(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" bf_series: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_series.str.capitalize().compute() + bf_result = bf_series.str.capitalize().to_pandas() pd_result = scalars_pandas_df[col_name].str.capitalize() assert_series_equal_ignoring_order( @@ -218,7 +347,7 @@ def test_cat_with_series(scalars_dfs): bf_filter: bigframes.series.Series = scalars_df["bool_col"] bf_left: bigframes.series.Series = scalars_df[col_name][bf_filter] bf_right: bigframes.series.Series = scalars_df[col_name] - bf_result = bf_left.str.cat(others=bf_right).compute() + bf_result = bf_left.str.cat(others=bf_right).to_pandas() pd_filter = scalars_pandas_df["bool_col"] pd_left = scalars_pandas_df[col_name][pd_filter] pd_right = scalars_pandas_df[col_name] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 1833c674fdd..c7e17f5a2dd 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -13,6 +13,8 @@ # limitations under the License. import operator +import typing +from typing import Tuple import geopandas as gpd # type: ignore import numpy as np @@ -24,6 +26,7 @@ import bigframes import bigframes._config.display_options as display_options import bigframes.dataframe as dataframe +import bigframes.series as series from tests.system.utils import ( assert_pandas_df_equal_ignore_ordering, assert_series_equal_ignoring_order, @@ -33,7 +36,7 @@ def test_df_construct_copy(scalars_dfs): columns = ["int64_col", "string_col", "float64_col"] scalars_df, scalars_pandas_df = scalars_dfs - bf_result = dataframe.DataFrame(scalars_df, columns=columns).compute() + bf_result = dataframe.DataFrame(scalars_df, columns=columns).to_pandas() pd_result = pd.DataFrame(scalars_pandas_df, columns=columns) pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -41,7 +44,7 @@ def test_df_construct_copy(scalars_dfs): def test_df_construct_pandas(scalars_dfs): columns = ["int64_too", "int64_col", "float64_col", "bool_col", "string_col"] _, scalars_pandas_df = scalars_dfs - bf_result = dataframe.DataFrame(scalars_pandas_df, columns=columns).compute() + bf_result = dataframe.DataFrame(scalars_pandas_df, columns=columns).to_pandas() pd_result = pd.DataFrame(scalars_pandas_df, columns=columns) pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -56,7 +59,7 @@ def test_df_construct_pandas_set_dtype(scalars_dfs): _, scalars_pandas_df = scalars_dfs bf_result = dataframe.DataFrame( scalars_pandas_df, columns=columns, dtype="Float64" - ).compute() + ).to_pandas() pd_result = pd.DataFrame(scalars_pandas_df, columns=columns, dtype="Float64") pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -66,7 +69,7 @@ def test_df_construct_from_series(scalars_dfs): bf_result = dataframe.DataFrame( {"a": scalars_df["int64_col"], "b": scalars_df["string_col"]}, dtype="string[pyarrow]", - ).compute() + ).to_pandas() pd_result = pd.DataFrame( {"a": scalars_pandas_df["int64_col"], "b": scalars_pandas_df["string_col"]}, dtype="string[pyarrow]", @@ -78,7 +81,7 @@ def test_get_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" series = scalars_df[col_name] - bf_result = series.compute() + bf_result = series.to_pandas() pd_result = scalars_pandas_df[col_name] assert_series_equal_ignoring_order(bf_result, pd_result) @@ -96,7 +99,7 @@ def test_head_with_custom_column_labels(scalars_df_index, scalars_pandas_df_inde "string_col": "言語列", } bf_df = scalars_df_index.rename(columns=rename_mapping).head(3) - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_result = scalars_pandas_df_index.rename(columns=rename_mapping).head(3) pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -107,7 +110,7 @@ def test_tail_with_custom_column_labels(scalars_df_index, scalars_pandas_df_inde "string_col": "言語列", } bf_df = scalars_df_index.rename(columns=rename_mapping).tail(3) - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_result = scalars_pandas_df_index.rename(columns=rename_mapping).tail(3) pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -115,7 +118,7 @@ def test_tail_with_custom_column_labels(scalars_df_index, scalars_pandas_df_inde def test_get_column_by_attr(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs series = scalars_df.int64_col - bf_result = series.compute() + bf_result = series.to_pandas() pd_result = scalars_pandas_df.int64_col assert_series_equal_ignoring_order(bf_result, pd_result) @@ -124,7 +127,7 @@ def test_get_columns(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_names = ["bool_col", "float64_col", "int64_col"] df_subset = scalars_df.get(col_names) - df_pandas = df_subset.compute() + df_pandas = df_subset.to_pandas() pd.testing.assert_index_equal( df_pandas.columns, scalars_pandas_df[col_names].columns ) @@ -140,7 +143,7 @@ def test_get_columns_default(scalars_dfs): def test_drop_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" - df_pandas = scalars_df.drop(columns=col_name).compute() + df_pandas = scalars_df.drop(columns=col_name).to_pandas() pd.testing.assert_index_equal( df_pandas.columns, scalars_pandas_df.drop(columns=col_name).columns ) @@ -149,12 +152,22 @@ def test_drop_column(scalars_dfs): def test_drop_columns(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_names = ["int64_col", "geography_col", "time_col"] - df_pandas = scalars_df.drop(columns=col_names).compute() + df_pandas = scalars_df.drop(columns=col_names).to_pandas() pd.testing.assert_index_equal( df_pandas.columns, scalars_pandas_df.drop(columns=col_names).columns ) +def test_drop_labels_axis_1(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + labels = ["int64_col", "geography_col", "time_col"] + + pd_result = scalars_pandas_df.drop(labels=labels, axis=1) + bf_result = scalars_df.drop(labels=labels, axis=1).to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + def test_drop_with_custom_column_labels(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs rename_mapping = { @@ -166,17 +179,44 @@ def test_drop_with_custom_column_labels(scalars_dfs): "timestamp_col", ] bf_df = scalars_df.rename(columns=rename_mapping).drop(columns=dropped_columns) - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_result = scalars_pandas_df.rename(columns=rename_mapping).drop( columns=dropped_columns ) assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +def test_drop_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = scalars_pandas_df.drop(index=[4, 1, 2]) + bf_result = scalars_df.drop(index=[4, 1, 2]).to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + +def test_drop_labels_axis_0(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = scalars_pandas_df.drop(labels=[4, 1, 2], axis=0) + bf_result = scalars_df.drop(labels=[4, 1, 2], axis=0).to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + +def test_drop_index_and_columns(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = scalars_pandas_df.drop(index=[4, 1, 2], columns="int64_col") + bf_result = scalars_df.drop(index=[4, 1, 2], columns="int64_col").to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + def test_rename(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name_dict = {"bool_col": "boolean_col"} - df_pandas = scalars_df.rename(columns=col_name_dict).compute() + df_pandas = scalars_df.rename(columns=col_name_dict).to_pandas() pd.testing.assert_index_equal( df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns ) @@ -210,7 +250,7 @@ def test_repr_w_all_rows(scalars_dfs): def test_repr_html_w_all_rows(scalars_dfs): scalars_df, _ = scalars_dfs # get a pandas df of the expected format - df, _ = scalars_df._block.compute() + df, _ = scalars_df._block.to_pandas() pandas_df = df.set_axis(scalars_df._block.column_labels, axis=1) pandas_df.index.name = scalars_df.index.name @@ -229,7 +269,7 @@ def test_repr_html_w_all_rows(scalars_dfs): def test_df_column_name_with_space(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name_dict = {"bool_col": "bool col"} - df_pandas = scalars_df.rename(columns=col_name_dict).compute() + df_pandas = scalars_df.rename(columns=col_name_dict).to_pandas() pd.testing.assert_index_equal( df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns ) @@ -238,7 +278,7 @@ def test_df_column_name_with_space(scalars_dfs): def test_df_column_name_duplicate(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name_dict = {"int64_too": "int64_col"} - df_pandas = scalars_df.rename(columns=col_name_dict).compute() + df_pandas = scalars_df.rename(columns=col_name_dict).to_pandas() pd.testing.assert_index_equal( df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns ) @@ -248,7 +288,7 @@ def test_get_df_column_name_duplicate(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name_dict = {"int64_too": "int64_col"} - bf_result = scalars_df.rename(columns=col_name_dict)["int64_col"].compute() + bf_result = scalars_df.rename(columns=col_name_dict)["int64_col"].to_pandas() pd_result = scalars_pandas_df.rename(columns=col_name_dict)["int64_col"] pd.testing.assert_index_equal(bf_result.columns, pd_result.columns) @@ -257,7 +297,7 @@ def test_filter_df(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_bool_series = scalars_df["bool_col"] - bf_result = scalars_df[bf_bool_series].compute() + bf_result = scalars_df[bf_bool_series].to_pandas() pd_bool_series = scalars_pandas_df["bool_col"] pd_result = scalars_pandas_df[pd_bool_series] @@ -269,7 +309,7 @@ def test_assign_new_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs kwargs = {"new_col": 2} df = scalars_df.assign(**kwargs) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.assign(**kwargs) # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -284,7 +324,7 @@ def test_assign_new_column_w_loc(scalars_dfs): pd_df = scalars_pandas_df.copy() bf_df.loc[:, "new_col"] = 2 pd_df.loc[:, "new_col"] = 2 - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_result = pd_df # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -299,7 +339,7 @@ def test_assign_new_column_w_setitem(scalars_dfs): pd_df = scalars_pandas_df.copy() bf_df["new_col"] = 2 pd_df["new_col"] = 2 - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_result = pd_df # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -312,7 +352,7 @@ def test_assign_existing_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs kwargs = {"int64_col": 2} df = scalars_df.assign(**kwargs) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.assign(**kwargs) # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -325,7 +365,7 @@ def test_assign_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs column_name = "int64_col" df = scalars_df.assign(new_col=scalars_df[column_name]) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.assign(new_col=scalars_pandas_df[column_name]) assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -335,7 +375,7 @@ def test_assign_series_overwrite(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs column_name = "int64_col" df = scalars_df.assign(**{column_name: scalars_df[column_name] + 3}) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.assign( **{column_name: scalars_pandas_df[column_name] + 3} ) @@ -347,7 +387,7 @@ def test_assign_sequential(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs kwargs = {"int64_col": 2, "new_col": 3, "new_col2": 4} df = scalars_df.assign(**kwargs) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.assign(**kwargs) # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -371,7 +411,7 @@ def test_assign_same_table_different_index_performs_self_join( ) bf_df_2 = bf_df.set_index("alternative_index") pd_df_2 = pd_df.set_index("alternative_index") - bf_result = bf_df.assign(new_col=bf_df_2[column_name] * 10).compute() + bf_result = bf_df.assign(new_col=bf_df_2[column_name] * 10).to_pandas() pd_result = pd_df.assign(new_col=pd_df_2[column_name] * 10) pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -383,7 +423,7 @@ def test_assign_different_df( ): column_name = "int64_col" df = scalars_df_index.assign(new_col=scalars_df_2_index[column_name]) - bf_result = df.compute() + bf_result = df.to_pandas() # Doesn't matter to pandas if it comes from the same DF or a different DF. pd_result = scalars_pandas_df_index.assign( new_col=scalars_pandas_df_index[column_name] @@ -402,7 +442,7 @@ def test_assign_different_df_w_loc( assert "int64_col" in pd_df.columns bf_df.loc[:, "int64_col"] = bf_df2.loc[:, "int64_col"] + 1 pd_df.loc[:, "int64_col"] = pd_df.loc[:, "int64_col"] + 1 - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_result = pd_df # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -421,7 +461,7 @@ def test_assign_different_df_w_setitem( assert "int64_col" in pd_df.columns bf_df["int64_col"] = bf_df2["int64_col"] + 1 pd_df["int64_col"] = pd_df["int64_col"] + 1 - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_result = pd_df # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -434,7 +474,7 @@ def test_assign_callable_lambda(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs kwargs = {"new_col": lambda x: x["int64_col"] + x["int64_too"]} df = scalars_df.assign(**kwargs) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.assign(**kwargs) # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. @@ -446,7 +486,7 @@ def test_assign_callable_lambda(scalars_dfs): def test_dropna(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs df = scalars_df.dropna() - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.dropna() assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -472,7 +512,7 @@ def test_merge(scalars_dfs, merge_how): right = scalars_df[right_columns].assign(rowindex_2=scalars_df["rowindex_2"] + 2) df = left.merge(right, merge_how, on, sort=True) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df[left_columns].merge( scalars_pandas_df[right_columns].assign( @@ -506,7 +546,7 @@ def test_merge_custom_col_name(scalars_dfs, merge_how): left = left.rename(columns=rename_columns) right = scalars_df[right_columns] df = left.merge(right, merge_how, on, sort=True) - bf_result = df.compute() + bf_result = df.to_pandas() pandas_left_df = scalars_pandas_df[left_columns] pandas_left_df = pandas_left_df.rename(columns=rename_columns) @@ -536,7 +576,7 @@ def test_merge_left_on_right_on(scalars_dfs, merge_how): df = left.merge( right, merge_how, left_on="int64_too", right_on="rowindex_2", sort=True ) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df[left_columns].merge( scalars_pandas_df[right_columns], @@ -633,7 +673,7 @@ def test_empty_false(scalars_dfs): assert bf_result == pd_result -def test_empty_true(scalars_dfs): +def test_empty_true_column_filter(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df[[]].empty @@ -642,6 +682,31 @@ def test_empty_true(scalars_dfs): assert bf_result == pd_result +def test_empty_true_row_filter(scalars_dfs: Tuple[dataframe.DataFrame, pd.DataFrame]): + scalars_df, scalars_pandas_df = scalars_dfs + bf_bool: series.Series = typing.cast(series.Series, scalars_df["bool_col"]) + pd_bool: pd.Series = scalars_pandas_df["bool_col"] + bf_false = bf_bool.notna() & (bf_bool != bf_bool) + pd_false = pd_bool.notna() & (pd_bool != pd_bool) + + bf_result = scalars_df[bf_false].empty + pd_result = scalars_pandas_df[pd_false].empty + + assert pd_result + assert bf_result == pd_result + + +def test_empty_true_memtable(session: bigframes.Session): + bf_df = dataframe.DataFrame(session=session) + pd_df = pd.DataFrame() + + bf_result = bf_df.empty + pd_result = pd_df.empty + + assert pd_result + assert bf_result == pd_result + + @pytest.mark.parametrize( ("drop",), ((True,), (False,)), @@ -650,7 +715,7 @@ def test_reset_index(scalars_df_index, scalars_pandas_df_index, drop): df = scalars_df_index.reset_index(drop=drop) assert df.index.name is None - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df_index.reset_index(drop=drop) # Pandas uses int64 instead of Int64 (nullable) dtype. @@ -666,7 +731,7 @@ def test_reset_index_then_filter( ): bf_filter = scalars_df_index["bool_col"].fillna(True) bf_df = scalars_df_index.reset_index()[bf_filter] - bf_result = bf_df.compute() + bf_result = bf_df.to_pandas() pd_filter = scalars_pandas_df_index["bool_col"].fillna(True) pd_result = scalars_pandas_df_index.reset_index()[pd_filter] @@ -693,7 +758,7 @@ def test_reset_index_with_unnamed_index( # reset_index(drop=False) creates a new column "index". assert df.columns[0] == "index" - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df_index.reset_index(drop=False) # Pandas uses int64 instead of Int64 (nullable) dtype. @@ -720,7 +785,7 @@ def test_reset_index_with_unnamed_index_and_index_column( # reset_index(drop=False) creates a new column "level_0" if the "index" column already exists. assert df.columns[0] == "level_0" - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df_index.assign( index=scalars_pandas_df_index["int64_col"] ).reset_index(drop=False) @@ -753,7 +818,7 @@ def test_reset_index_with_unnamed_index_and_index_column( def test_set_index(scalars_dfs, index_column, drop, append): scalars_df, scalars_pandas_df = scalars_dfs df = scalars_df.set_index(index_column, append=append, drop=drop) - bf_result = df.compute() + bf_result = df.to_pandas() pd_result = scalars_pandas_df.set_index(index_column, append=append, drop=drop) # Sort to disambiguate when there are duplicate index labels. @@ -767,11 +832,30 @@ def test_set_index(scalars_dfs, index_column, drop, append): pandas.testing.assert_frame_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("ascending",), + ((True,), (False,)), +) +@pytest.mark.parametrize( + ("na_position",), + (("first",), ("last",)), +) +def test_sort_index(scalars_dfs, ascending, na_position): + index_column = "int64_col" + scalars_df, scalars_pandas_df = scalars_dfs + df = scalars_df.set_index(index_column) + bf_result = df.sort_index(ascending=ascending, na_position=na_position).to_pandas() + pd_result = scalars_pandas_df.set_index(index_column).sort_index( + ascending=ascending, na_position=na_position + ) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + def test_df_abs(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs columns = ["int64_col", "int64_too", "float64_col"] - bf_result = scalars_df[columns].abs().compute() + bf_result = scalars_df[columns].abs().to_pandas() pd_result = scalars_pandas_df[columns].abs() assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -781,7 +865,7 @@ def test_df_isnull(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs columns = ["int64_col", "int64_too", "string_col", "bool_col"] - bf_result = scalars_df[columns].isnull().compute() + bf_result = scalars_df[columns].isnull().to_pandas() pd_result = scalars_pandas_df[columns].isnull() # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is @@ -798,7 +882,7 @@ def test_df_notnull(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs columns = ["int64_col", "int64_too", "string_col", "bool_col"] - bf_result = scalars_df[columns].notnull().compute() + bf_result = scalars_df[columns].notnull().to_pandas() pd_result = scalars_pandas_df[columns].notnull() # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is @@ -819,6 +903,8 @@ def test_df_notnull(scalars_dfs): operator.mul, operator.truediv, operator.floordiv, + operator.eq, + operator.ne, operator.gt, operator.ge, operator.lt, @@ -830,6 +916,8 @@ def test_df_notnull(scalars_dfs): "multiply", "true_divide", "floor_divide", + "eq", + "ne", "gt", "ge", "lt", @@ -845,7 +933,7 @@ def test_scalar_binop(scalars_dfs, op, other_scalar, reverse_operands): maybe_reversed_op = (lambda x, y: op(y, x)) if reverse_operands else op - bf_result = maybe_reversed_op(scalars_df[columns], other_scalar).compute() + bf_result = maybe_reversed_op(scalars_df[columns], other_scalar).to_pandas() pd_result = maybe_reversed_op(scalars_pandas_df[columns], other_scalar) assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -857,7 +945,7 @@ def test_mod(scalars_dfs, other_scalar): # This is likely a pandas bug as mod 0 is undefined in other dtypes, and most programming languages. scalars_df, scalars_pandas_df = scalars_dfs - bf_result = (scalars_df[["int64_col", "int64_too"]] % other_scalar).compute() + bf_result = (scalars_df[["int64_col", "int64_too"]] % other_scalar).to_pandas() pd_result = scalars_pandas_df[["int64_col", "int64_too"]] % other_scalar assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -867,7 +955,7 @@ def test_scalar_binop_str_exception(scalars_dfs): scalars_df, _ = scalars_dfs columns = ["string_col"] with pytest.raises(TypeError): - (scalars_df[columns] + 1).compute() + (scalars_df[columns] + 1).to_pandas() @pytest.mark.parametrize( @@ -913,7 +1001,7 @@ def test_series_binop_axis_index( df_columns = ["int64_col", "float64_col"] series_column = "int64_too" - bf_result = op(scalars_df[df_columns], scalars_df[series_column]).compute() + bf_result = op(scalars_df[df_columns], scalars_df[series_column]).to_pandas() pd_result = op(scalars_pandas_df[df_columns], scalars_pandas_df[series_column]) assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -963,7 +1051,7 @@ def test_dataframe_binop_axis_index_throws_not_implemented( other_df_columns = ["int64_too"] with pytest.raises(NotImplementedError): - op(scalars_df[df_columns], scalars_df[other_df_columns]).compute() + op(scalars_df[df_columns], scalars_df[other_df_columns]).to_pandas() # Differnt table will only work for explicit index, since default index orders are arbitrary. @@ -976,7 +1064,7 @@ def test_series_binop_add_different_table( bf_result = ( scalars_df_index[df_columns] .add(scalars_df_2_index[series_column], axis="index") - .compute() + .to_pandas() ) pd_result = scalars_pandas_df_index[df_columns].add( scalars_pandas_df_index[series_column], axis="index" @@ -1001,14 +1089,12 @@ def test_series_binop_add_different_table( @all_joins def test_join_same_table(scalars_dfs, how): bf_df, pd_df = scalars_dfs - if how == "right" and pd_df.index.name != "rowindex": - pytest.skip("right join not supported without an index") - bf_df_a = bf_df[["string_col", "int64_col"]] - bf_df_b = bf_df[["float64_col"]] - bf_result = bf_df_a.join(bf_df_b, how=how).compute() - pd_df_a = pd_df[["string_col", "int64_col"]] - pd_df_b = pd_df[["float64_col"]] + bf_df_a = bf_df.set_index("int64_too")[["string_col", "int64_col"]] + bf_df_b = bf_df.set_index("int64_too")[["float64_col"]] + bf_result = bf_df_a.join(bf_df_b, how=how).to_pandas() + pd_df_a = pd_df.set_index("int64_too")[["string_col", "int64_col"]] + pd_df_b = pd_df.set_index("int64_too")[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, how=how) assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -1019,7 +1105,7 @@ def test_join_different_table( ): bf_df_a = scalars_df_index[["string_col", "int64_col"]] bf_df_b = scalars_df_2_index.dropna()[["float64_col"]] - bf_result = bf_df_a.join(bf_df_b, how=how).compute() + bf_result = bf_df_a.join(bf_df_b, how=how).to_pandas() pd_df_a = scalars_pandas_df_index[["string_col", "int64_col"]] pd_df_b = scalars_pandas_df_index.dropna()[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, how=how) @@ -1031,7 +1117,23 @@ def test_join_duplicate_columns_raises_not_implemented(scalars_dfs): df_a = scalars_df[["string_col", "float64_col"]] df_b = scalars_df[["float64_col"]] with pytest.raises(NotImplementedError): - df_a.join(df_b, how="outer").compute() + df_a.join(df_b, how="outer").to_pandas() + + +@all_joins +def test_join_param_on(scalars_dfs, how): + bf_df, pd_df = scalars_dfs + + bf_df_a = bf_df[["string_col", "int64_col", "rowindex_2"]] + bf_df_a = bf_df_a.assign(rowindex_2=bf_df_a["rowindex_2"] + 2) + bf_df_b = bf_df[["float64_col"]] + bf_result = bf_df_a.join(bf_df_b, on="rowindex_2", how=how).to_pandas() + + pd_df_a = pd_df[["string_col", "int64_col", "rowindex_2"]] + pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) + pd_df_b = pd_df[["float64_col"]] + pd_result = pd_df_a.join(pd_df_b, on="rowindex_2", how=how) + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @pytest.mark.parametrize( @@ -1050,7 +1152,7 @@ def test_dataframe_sort_values( # Test needs values to be unique bf_result = scalars_df_index.sort_values( by, ascending=ascending, na_position=na_position - ).compute() + ).to_pandas() pd_result = scalars_pandas_df_index.sort_values( by, ascending=ascending, na_position=na_position ) @@ -1061,6 +1163,22 @@ def test_dataframe_sort_values( ) +def test_dataframe_sort_values_stable(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index.sort_values("int64_col", kind="stable") + .sort_values("bool_col", kind="stable") + .to_pandas() + ) + pd_result = scalars_pandas_df_index.sort_values( + "int64_col", kind="stable" + ).sort_values("bool_col", kind="stable") + + pandas.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + @pytest.mark.parametrize( ("operator", "columns"), [ @@ -1086,7 +1204,7 @@ def test_dataframe_numeric_analytic_op( # TODO: Add nullable ints (pandas 1.x has poor behavior on these) bf_series = operator(scalars_df_index[columns]) pd_series = operator(scalars_pandas_df_index[columns]) - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() pd.testing.assert_frame_equal(pd_series, bf_result, check_dtype=False) @@ -1111,13 +1229,90 @@ def test_dataframe_general_analytic_op( col_names = ["int64_too", "float64_col", "int64_col", "bool_col"] bf_series = operator(scalars_df_index[col_names]) pd_series = operator(scalars_pandas_df_index[col_names]) - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() pd.testing.assert_frame_equal( pd_series, bf_result, ) +def test_dataframe_agg_single_string(scalars_dfs): + numeric_cols = ["int64_col", "int64_too", "float64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[numeric_cols].agg("sum").to_pandas() + pd_result = scalars_pandas_df[numeric_cols].agg("sum") + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + +def test_dataframe_agg_multi_string(scalars_dfs): + numeric_cols = ["int64_col", "int64_too", "float64_col"] + aggregations = [ + "sum", + "mean", + "median", + "std", + "var", + "min", + "max", + "nunique", + "count", + ] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[numeric_cols].agg(aggregations).to_pandas() + pd_result = scalars_pandas_df[numeric_cols].agg(aggregations) + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + # Drop median, as it's an approximation. + bf_median = bf_result.loc["median", :] + bf_result = bf_result.drop(labels=["median"]) + pd_result = pd_result.drop(labels=["median"]) + + pd.testing.assert_frame_equal(pd_result, bf_result, check_index_type=False) + + # Double-check that median is at least plausible. + assert ( + (bf_result.loc["min", :] <= bf_median) & (bf_median <= bf_result.loc["max", :]) + ).all() + + +def test_df_describe(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + # pyarrows time columns fail in pandas + unsupported_columns = ["datetime_col", "timestamp_col", "time_col", "date_col"] + bf_result = scalars_df.describe().to_pandas() + + modified_pd_df = scalars_pandas_df.drop(columns=unsupported_columns) + pd_result = modified_pd_df.describe() + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + # Drop quartiles, as they are approximate + bf_min = bf_result.loc["min", :] + bf_p25 = bf_result.loc["25%", :] + bf_p50 = bf_result.loc["50%", :] + bf_p75 = bf_result.loc["75%", :] + bf_max = bf_result.loc["max", :] + + bf_result = bf_result.drop(labels=["25%", "50%", "75%"]) + pd_result = pd_result.drop(labels=["25%", "50%", "75%"]) + + pd.testing.assert_frame_equal(pd_result, bf_result, check_index_type=False) + + # Double-check that quantiles are at least plausible. + assert ( + (bf_min <= bf_p25) + & (bf_p25 <= bf_p50) + & (bf_p50 <= bf_p50) + & (bf_p75 <= bf_max) + ).all() + + def test_ipython_key_completions_with_drop(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_names = "string_col" @@ -1206,7 +1401,7 @@ def test__dir__with_rename(scalars_dfs): ], ) def test_iloc_slice(scalars_df_index, scalars_pandas_df_index, start, stop, step): - bf_result = scalars_df_index.iloc[start:stop:step].compute() + bf_result = scalars_df_index.iloc[start:stop:step].to_pandas() pd_result = scalars_pandas_df_index.iloc[start:stop:step] # Pandas may assign non-object dtype to empty series and series index @@ -1230,7 +1425,7 @@ def test_iloc_slice_zero_step(scalars_df_index): def test_iloc_slice_nested(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.iloc[1:].iloc[1:].compute() + bf_result = scalars_df_index.iloc[1:].iloc[1:].to_pandas() pd_result = scalars_pandas_df_index.iloc[1:].iloc[1:] pd.testing.assert_frame_equal( @@ -1241,7 +1436,7 @@ def test_iloc_slice_nested(scalars_df_index, scalars_pandas_df_index): @pytest.mark.parametrize( "index", - [0, 5], + [0, 5, -2], ) def test_iloc_single_integer(scalars_df_index, scalars_pandas_df_index, index): bf_result = scalars_df_index.iloc[index] @@ -1261,7 +1456,7 @@ def test_iloc_single_integer_out_of_bound_error( def test_loc_bool_series(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.loc[scalars_df_index.bool_col].compute() + bf_result = scalars_df_index.loc[scalars_df_index.bool_col].to_pandas() pd_result = scalars_pandas_df_index.loc[scalars_pandas_df_index.bool_col] pd.testing.assert_frame_equal( @@ -1271,7 +1466,7 @@ def test_loc_bool_series(scalars_df_index, scalars_pandas_df_index): def test_loc_select_column(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.loc[:, "int64_col"].compute() + bf_result = scalars_df_index.loc[:, "int64_col"].to_pandas() pd_result = scalars_pandas_df_index.loc[:, "int64_col"] pd.testing.assert_series_equal( bf_result, @@ -1279,6 +1474,32 @@ def test_loc_select_column(scalars_df_index, scalars_pandas_df_index): ) +def test_loc_single_index_with_duplicate(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.set_index("string_col", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index( + "string_col", drop=False + ) + index = "Hello, World!" + bf_result = scalars_df_index.loc[index] + pd_result = scalars_pandas_df_index.loc[index] + pd.testing.assert_frame_equal( + bf_result.to_pandas(), + pd_result, + ) + + +def test_loc_single_index_no_duplicate(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.set_index("int64_too", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index("int64_too", drop=False) + index = -2345 + bf_result = scalars_df_index.loc[index] + pd_result = scalars_pandas_df_index.loc[index] + pd.testing.assert_series_equal( + bf_result.to_pandas().iloc[0, :], + pd_result, + ) + + @pytest.mark.parametrize( ("op"), [ @@ -1297,7 +1518,7 @@ def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op): col_names = ["int64_too", "float64_col", "string_col", "int64_col", "bool_col"] bf_series = op(scalars_df_index[col_names]) pd_series = op(scalars_pandas_df_index[col_names]) - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_series = pd_series.astype("Float64") @@ -1305,6 +1526,21 @@ def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op): pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) +def test_dataframe_aggregates_median(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col"] + bf_result = scalars_df_index[col_names].median(numeric_only=True).to_pandas() + pd_result = scalars_pandas_df_index[col_names].agg(["min", "max"]) + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + # Median is an approximation, but double-check that median is plausible. + for col in col_names: + assert (pd_result.loc["min", col] <= bf_result[col]) and ( + bf_result[col] <= pd_result.loc["max", col] + ) + + @pytest.mark.parametrize( ("op"), [ @@ -1320,7 +1556,7 @@ def test_dataframe_bool_aggregates(scalars_df_index, scalars_pandas_df_index, op ) bf_series = op(scalars_df_index) pd_series = op(scalars_pandas_df_index).astype("boolean") - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() # Pandas has object index type pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) @@ -1330,7 +1566,7 @@ def test_dataframe_prod(scalars_df_index, scalars_pandas_df_index): col_names = ["int64_too", "float64_col"] bf_series = scalars_df_index[col_names].prod() pd_series = scalars_pandas_df_index[col_names].prod() - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_series = pd_series.astype("Float64") @@ -1358,7 +1594,7 @@ def test_dataframe_prod(scalars_df_index, scalars_pandas_df_index): def test_sample(scalars_dfs, frac, n, random_state): scalars_df, _ = scalars_dfs df = scalars_df.sample(frac=frac, n=n, random_state=random_state) - bf_result = df.compute() + bf_result = df.to_pandas() n = 1 if n is None else n expected_sample_size = round(frac * scalars_df.shape[0]) if frac is not None else n @@ -1384,7 +1620,7 @@ def test_sample_raises_value_error(scalars_dfs): def test_df_add_prefix(scalars_df_index, scalars_pandas_df_index, axis): if pd.__version__.startswith("1."): pytest.skip("add_prefix axis parameter not supported in pandas 1.x.") - bf_result = scalars_df_index.add_prefix("prefix_", axis).compute() + bf_result = scalars_df_index.add_prefix("prefix_", axis).to_pandas() pd_result = scalars_pandas_df_index.add_prefix("prefix_", axis) @@ -1405,7 +1641,7 @@ def test_df_add_prefix(scalars_df_index, scalars_pandas_df_index, axis): def test_df_add_suffix(scalars_df_index, scalars_pandas_df_index, axis): if pd.__version__.startswith("1."): pytest.skip("add_prefix axis parameter not supported in pandas 1.x.") - bf_result = scalars_df_index.add_suffix("_suffix", axis).compute() + bf_result = scalars_df_index.add_suffix("_suffix", axis).to_pandas() pd_result = scalars_pandas_df_index.add_suffix("_suffix", axis) @@ -1462,11 +1698,11 @@ def test_loc_list_string_index(scalars_df_index, scalars_pandas_df_index): scalars_df_index = scalars_df_index.set_index("string_col") scalars_pandas_df_index = scalars_pandas_df_index.set_index("string_col") - bf_result = scalars_df_index.loc[index_list] + bf_result = scalars_df_index.loc[index_list].to_pandas() pd_result = scalars_pandas_df_index.loc[index_list] pd.testing.assert_frame_equal( - bf_result.compute(), + bf_result, pd_result, ) @@ -1478,7 +1714,7 @@ def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.loc[index_list] pd.testing.assert_frame_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1490,7 +1726,7 @@ def test_iloc_list(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.iloc[index_list] pd.testing.assert_frame_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1501,7 +1737,7 @@ def test_iloc_empty_list(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.iloc[index_list] pd_result = scalars_pandas_df_index.iloc[index_list] - bf_result = bf_result.compute() + bf_result = bf_result.to_pandas() assert bf_result.shape == pd_result.shape # types are known to be different @@ -1510,7 +1746,7 @@ def test_rename_axis(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.rename_axis("newindexname") pd.testing.assert_frame_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1526,7 +1762,7 @@ def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.loc[pd_string_series] pd.testing.assert_frame_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1539,7 +1775,7 @@ def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.loc[pd_index] pd.testing.assert_frame_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1559,7 +1795,7 @@ def test_loc_bf_index_integer_index_renamed_col( pd_result = scalars_pandas_df_index.loc[pd_index] pd.testing.assert_frame_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1568,6 +1804,7 @@ def test_loc_bf_index_integer_index_renamed_col( ("subset"), [ None, + "bool_col", ["bool_col", "int64_too"], ], ) @@ -1581,7 +1818,7 @@ def test_loc_bf_index_integer_index_renamed_col( ) def test_df_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep, subset): columns = ["bool_col", "int64_too", "int64_col"] - bf_series = scalars_df_index[columns].drop_duplicates(subset, keep=keep).compute() + bf_series = scalars_df_index[columns].drop_duplicates(subset, keep=keep).to_pandas() pd_series = scalars_pandas_df_index[columns].drop_duplicates(subset, keep=keep) pd.testing.assert_frame_equal( pd_series, @@ -1606,7 +1843,7 @@ def test_df_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep, sub ) def test_df_duplicated(scalars_df_index, scalars_pandas_df_index, keep, subset): columns = ["bool_col", "int64_too", "int64_col"] - bf_series = scalars_df_index[columns].duplicated(subset, keep=keep).compute() + bf_series = scalars_df_index[columns].duplicated(subset, keep=keep).to_pandas() pd_series = scalars_pandas_df_index[columns].duplicated(subset, keep=keep) pd.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) @@ -1625,7 +1862,7 @@ def test_df_value_counts(scalars_dfs, subset, normalize, ascending, dropna): bf_result = ( scalars_df[["string_col", "bool_col"]] .value_counts(subset, normalize=normalize, ascending=ascending, dropna=dropna) - .compute() + .to_pandas() ) pd_result = scalars_pandas_df[["string_col", "bool_col"]].value_counts( subset, normalize=normalize, ascending=ascending, dropna=dropna @@ -1670,7 +1907,7 @@ def test_df_rank_with_nulls( ascending=ascending, numeric_only=numeric_only, ) - .compute() + .to_pandas() ) pd_result = ( scalars_pandas_df_index.drop(columns=unsupported_columns) @@ -1692,3 +1929,14 @@ def test_df_rank_with_nulls( def test_df_bool_interpretation_error(scalars_df_index): with pytest.raises(ValueError): True if scalars_df_index else False + + +def test_query_job_setters(scalars_df_default_index: dataframe.DataFrame): + job_ids = set() + repr(scalars_df_default_index) + assert scalars_df_default_index.query_job is not None + job_ids.add(scalars_df_default_index.query_job.job_id) + scalars_df_default_index.to_pandas() + job_ids.add(scalars_df_default_index.query_job.job_id) + + assert len(job_ids) == 2 diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index a4318a8658f..7ad753e1bc0 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -72,15 +72,17 @@ def test_to_csv_index( gcs_folder: str, index: bool, ): + if pd.__version__.startswith("1."): + pytest.skip("date_format parameter not supported in pandas 1.x.") """Test the `to_csv` API with the `index` parameter.""" scalars_df, scalars_pandas_df = scalars_dfs index_col = None if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_csv_index_{index}" + path = gcs_folder + f"test_index_df_to_csv_index_{index}*.csv" if index: index_col = scalars_df.index.name else: - path = gcs_folder + f"test_default_index_df_to_csv_index_{index}" + path = gcs_folder + f"test_default_index_df_to_csv_index_{index}*.csv" # TODO(swast): Support "date_format" parameter and make sure our # DATETIME/TIMESTAMP column export is the same format as pandas by default. @@ -90,14 +92,60 @@ def test_to_csv_index( # BigQuery-backed dataframes, so manually convert the dtypes specifically # here. dtype = scalars_df.reset_index().dtypes.to_dict() - dtype.pop("timestamp_col") dtype.pop("geography_col") + dtype.pop("rowindex") gcs_df = pd.read_csv( - path, dtype=dtype, parse_dates=["timestamp_col"], index_col=index_col + path, + dtype=dtype, + date_format={"timestamp_col": "YYYY-MM-DD HH:MM:SS Z"}, + index_col=index_col, ) convert_pandas_dtypes(gcs_df, bytes_col=True) + gcs_df.index.name = scalars_df.index.name - assert_pandas_df_equal_ignore_ordering(gcs_df, scalars_pandas_df) + scalars_pandas_df = scalars_pandas_df.copy() + scalars_pandas_df.index = scalars_pandas_df.index.astype("int64") + + # Ordering should be maintained for tables smaller than 1 GB. + pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) + + +def test_to_csv_tabs( + scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], + gcs_folder: str, +): + if pd.__version__.startswith("1."): + pytest.skip("date_format parameter not supported in pandas 1.x.") + """Test the `to_csv` API with the `sep` parameter.""" + scalars_df, scalars_pandas_df = scalars_dfs + index_col = scalars_df.index.name + path = gcs_folder + "test_to_csv_tabs*.csv" + + # TODO(swast): Support "date_format" parameter and make sure our + # DATETIME/TIMESTAMP column export is the same format as pandas by default. + scalars_df.to_csv(path, sep="\t", index=True) + + # Pandas dataframes dtypes from read_csv are not fully compatible with + # BigQuery-backed dataframes, so manually convert the dtypes specifically + # here. + dtype = scalars_df.reset_index().dtypes.to_dict() + dtype.pop("geography_col") + dtype.pop("rowindex") + gcs_df = pd.read_csv( + path, + sep="\t", + dtype=dtype, + date_format={"timestamp_col": "YYYY-MM-DD HH:MM:SS Z"}, + index_col=index_col, + ) + convert_pandas_dtypes(gcs_df, bytes_col=True) + gcs_df.index.name = scalars_df.index.name + + scalars_pandas_df = scalars_pandas_df.copy() + scalars_pandas_df.index = scalars_pandas_df.index.astype("int64") + + # Ordering should be maintained for tables smaller than 1 GB. + pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) @pytest.mark.parametrize( @@ -190,9 +238,9 @@ def test_to_json_index_invalid_orient( ): scalars_df, scalars_pandas_df = scalars_dfs if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_json_index_{index}" + path = gcs_folder + f"test_index_df_to_json_index_{index}*.jsonl" else: - path = gcs_folder + f"test_default_index_df_to_json_index_{index}" + path = gcs_folder + f"test_default_index_df_to_json_index_{index}*.jsonl" with pytest.raises(ValueError): scalars_df.to_json(path, index=index, lines=True) @@ -208,9 +256,9 @@ def test_to_json_index_invalid_lines( ): scalars_df, scalars_pandas_df = scalars_dfs if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_json_index_{index}" + path = gcs_folder + f"test_index_df_to_json_index_{index}.jsonl" else: - path = gcs_folder + f"test_default_index_df_to_json_index_{index}" + path = gcs_folder + f"test_default_index_df_to_json_index_{index}.jsonl" with pytest.raises(NotImplementedError): scalars_df.to_json(path, index=index) @@ -227,9 +275,9 @@ def test_to_json_index_records_orient( """Test the `to_json` API with the `index` parameter.""" scalars_df, scalars_pandas_df = scalars_dfs if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_json_index_{index}" + path = gcs_folder + f"test_index_df_to_json_index_{index}*.jsonl" else: - path = gcs_folder + f"test_default_index_df_to_json_index_{index}" + path = gcs_folder + f"test_default_index_df_to_json_index_{index}*.jsonl" """ Test the `to_json` API with `orient` is `records` and `lines` is True""" scalars_df.to_json(path, index=index, orient="records", lines=True) @@ -241,7 +289,13 @@ def test_to_json_index_records_orient( assert len(gcs_df.index) == len(scalars_pandas_df.index) pd.testing.assert_index_equal(gcs_df.columns, scalars_pandas_df.columns) - assert_pandas_df_equal_ignore_ordering(gcs_df, scalars_pandas_df) + + gcs_df.index.name = scalars_df.index.name + gcs_df.index = gcs_df.index.astype("Int64") + scalars_pandas_df.index = scalars_pandas_df.index.astype("Int64") + + # Ordering should be maintained for tables smaller than 1 GB. + pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) @pytest.mark.parametrize( @@ -251,10 +305,12 @@ def test_to_json_index_records_orient( def test_to_parquet_index(scalars_dfs, gcs_folder, index): """Test the `to_parquet` API with the `index` parameter.""" scalars_df, scalars_pandas_df = scalars_dfs + scalars_pandas_df = scalars_pandas_df.copy() + if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_parquet_{index}" + path = gcs_folder + f"test_index_df_to_parquet_{index}*.parquet" else: - path = gcs_folder + f"test_default_index_df_to_parquet_{index}" + path = gcs_folder + f"test_default_index_df_to_parquet_{index}*.parquet" # TODO(b/268693993): Type GEOGRAPHY is not currently supported for parquet. scalars_df = scalars_df.drop(columns="geography_col") @@ -265,20 +321,26 @@ def test_to_parquet_index(scalars_dfs, gcs_folder, index): # table. scalars_df.to_parquet(path, index=index) - gcs_df = pd.read_parquet(path) + gcs_df = pd.read_parquet(path.replace("*", "000000000000")) convert_pandas_dtypes(gcs_df, bytes_col=False) if index and scalars_df.index.name is not None: gcs_df = gcs_df.set_index(scalars_df.index.name) assert len(gcs_df.index) == len(scalars_pandas_df.index) pd.testing.assert_index_equal(gcs_df.columns, scalars_pandas_df.columns) - assert_pandas_df_equal_ignore_ordering(gcs_df, scalars_pandas_df) + + gcs_df.index.name = scalars_df.index.name + gcs_df.index = gcs_df.index.astype("Int64") + scalars_pandas_df.index = scalars_pandas_df.index.astype("Int64") + + # Ordering should be maintained for tables smaller than 1 GB. + pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) def test_to_sql_query_named_index_included( session, scalars_df_index, scalars_pandas_df_index ): - sql, index_columns = scalars_df_index.to_sql_query(always_include_index=True) + sql, index_columns = scalars_df_index._to_sql_query(always_include_index=True) assert len(index_columns) == 1 index_column, is_named = index_columns[0] assert index_column == "rowindex" @@ -294,7 +356,7 @@ def test_to_sql_query_unnamed_index_excluded( session, scalars_df_default_index, scalars_pandas_df_default_index ): # The .sql property should return SQL without the unnamed indexes - sql, index_columns = scalars_df_default_index.to_sql_query( + sql, index_columns = scalars_df_default_index._to_sql_query( always_include_index=False ) assert len(index_columns) == 0 @@ -310,7 +372,7 @@ def test_to_sql_query_unnamed_index_always_include( scalars_df_default_index: bigframes.dataframe.DataFrame, scalars_pandas_df_default_index, ): - sql, index_columns = scalars_df_default_index.to_sql_query( + sql, index_columns = scalars_df_default_index._to_sql_query( always_include_index=True ) assert len(index_columns) == 1 diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 066e20bb12c..e72d75729be 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -41,11 +41,27 @@ def test_dataframe_groupby_numeric_aggregate( col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = operator(scalars_df_index[col_names].groupby("string_col")) pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) - bf_result_computed = bf_result.compute() + bf_result_computed = bf_result.to_pandas() # Pandas std function produces float64, not matching Float64 from bigframes pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +def test_dataframe_groupby_median(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = ( + scalars_df_index[col_names].groupby("string_col").median(numeric_only=True) + ) + pd_min = ( + scalars_pandas_df_index[col_names].groupby("string_col").min(numeric_only=True) + ) + pd_max = ( + scalars_pandas_df_index[col_names].groupby("string_col").max(numeric_only=True) + ) + bf_result_computed = bf_result.to_pandas() + # Median is approximate. Just check for plausibility. + assert ((pd_min <= bf_result_computed) & (bf_result_computed <= pd_max)).all().all() + + @pytest.mark.parametrize( ("operator"), [ @@ -65,7 +81,7 @@ def test_dataframe_groupby_aggregate( col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = operator(scalars_df_index[col_names].groupby("string_col")) pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) - bf_result_computed = bf_result.compute() + bf_result_computed = bf_result.to_pandas() pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) @@ -91,7 +107,7 @@ def test_dataframe_groupby_multi_sum( .groupby(["bool_col", "int64_col"], as_index=as_index) .sum(numeric_only=True) ) - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() if not as_index: # BigQuery DataFrames default indices use nullable Int64 always @@ -124,6 +140,36 @@ def test_dataframe_groupby_analytic( col_names = ["float64_col", "int64_col", "bool_col", "string_col"] bf_result = operator(scalars_df_index[col_names].groupby("string_col")) pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) - bf_result_computed = bf_result.compute() + bf_result_computed = bf_result.to_pandas() pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + +def test_dataframe_groupby_getitem( + scalars_df_index, + scalars_pandas_df_index, +): + col_names = ["float64_col", "int64_col", "bool_col", "string_col"] + bf_result = ( + scalars_df_index[col_names].groupby("string_col")["int64_col"].min().to_pandas() + ) + pd_result = ( + scalars_pandas_df_index[col_names].groupby("string_col")["int64_col"].min() + ) + + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + +def test_dataframe_groupby_getitem_list( + scalars_df_index, + scalars_pandas_df_index, +): + col_names = ["float64_col", "int64_col", "bool_col", "string_col"] + bf_result = ( + scalars_df_index[col_names].groupby("string_col")[col_names].min().to_pandas() + ) + pd_result = ( + scalars_pandas_df_index[col_names].groupby("string_col")[col_names].min() + ) + + pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) diff --git a/tests/system/small/test_ibis.py b/tests/system/small/test_ibis.py new file mode 100644 index 00000000000..58b78e00481 --- /dev/null +++ b/tests/system/small/test_ibis.py @@ -0,0 +1,39 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for monkeypatched ibis code.""" + +import ibis.expr.types as ibis_types + +import bigframes +import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops + + +def test_approximate_quantiles(session: bigframes.Session, scalars_table_id: str): + num_bins = 3 + ibis_client = session.ibis_client + _, dataset, table_id = scalars_table_id.split(".") + ibis_table: ibis_types.Table = ibis_client.table(table_id, database=dataset) + ibis_column: ibis_types.NumericColumn = ibis_table["int64_col"] + quantiles: ibis_types.ArrayScalar = vendored_ibis_ops.ApproximateMultiQuantile( # type: ignore + ibis_column, num_bins=num_bins + ).to_expr() + value = quantiles[1] + num_edges = quantiles.length() + + sql = ibis_client.compile(value) + num_edges_result = num_edges.to_pandas() + + assert "APPROX_QUANTILES" in sql + assert num_edges_result == num_bins + 1 diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index 312e86d7f23..ac1f8c7220d 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -19,7 +19,7 @@ def test_get_index(scalars_df_index, scalars_pandas_df_index): index = scalars_df_index.index - bf_result = index.compute() + bf_result = index.to_pandas() pd_result = scalars_pandas_df_index.index assert_pandas_index_equal_ignore_index_type(bf_result, pd_result) @@ -40,7 +40,13 @@ def test_index_len(scalars_df_index, scalars_pandas_df_index): def test_index_array(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.__array__() - pd_result = scalars_pandas_df_index.__array__() + bf_result = scalars_df_index.index.__array__() + pd_result = scalars_pandas_df_index.index.__array__() numpy.array_equal(bf_result, pd_result) + + +def test_index_getitem_int(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.index[-2] + pd_result = scalars_pandas_df_index.index[-2] + assert bf_result == pd_result diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index bf2a2080de3..914be6dae4a 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -16,10 +16,11 @@ import pytest import bigframes.pandas as bpd +from tests.system.utils import assert_pandas_df_equal_ignore_ordering def test_set_multi_index(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.set_index(["bool_col", "int64_too"]).compute() + bf_result = scalars_df_index.set_index(["bool_col", "int64_too"]).to_pandas() pd_result = scalars_pandas_df_index.set_index(["bool_col", "int64_too"]) pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -27,7 +28,7 @@ def test_set_multi_index(scalars_df_index, scalars_pandas_df_index): def test_reset_multi_index(scalars_df_index, scalars_pandas_df_index): bf_result = ( - scalars_df_index.set_index(["bool_col", "int64_too"]).reset_index().compute() + scalars_df_index.set_index(["bool_col", "int64_too"]).reset_index().to_pandas() ) pd_result = scalars_pandas_df_index.set_index( ["bool_col", "int64_too"] @@ -51,7 +52,7 @@ def test_binop_series_series_matching_multi_indices( pd_result = pd_left["int64_col"] + pd_right["int64_too"] pandas.testing.assert_series_equal( - bf_result.sort_index().compute(), pd_result.sort_index() + bf_result.sort_index().to_pandas(), pd_result.sort_index() ) @@ -67,7 +68,7 @@ def test_binop_df_series_matching_multi_indices( pd_result = pd_left[["int64_col", "int64_too"]].add(pd_right["int64_too"], axis=0) pandas.testing.assert_frame_equal( - bf_result.sort_index().compute(), pd_result.sort_index() + bf_result.sort_index().to_pandas(), pd_result.sort_index() ) @@ -80,7 +81,7 @@ def test_binop_multi_index_mono_index(scalars_df_index, scalars_pandas_df_index) bf_result = bf_left["int64_col"] + bf_right["int64_too"] pd_result = pd_left["int64_col"] + pd_right["int64_too"] - pandas.testing.assert_series_equal(bf_result.compute(), pd_result) + pandas.testing.assert_series_equal(bf_result.to_pandas(), pd_result) def test_binop_overlapping_multi_indices(scalars_df_index, scalars_pandas_df_index): @@ -93,7 +94,7 @@ def test_binop_overlapping_multi_indices(scalars_df_index, scalars_pandas_df_ind pd_result = pd_left["int64_col"] + pd_right["int64_too"] pandas.testing.assert_series_equal( - bf_result.sort_index().compute(), pd_result.sort_index() + bf_result.sort_index().to_pandas(), pd_result.sort_index() ) @@ -108,7 +109,7 @@ def test_concat_compatible_multi_indices(scalars_df_index, scalars_pandas_df_ind bf_result = bpd.concat([bf_left, bf_right]) pd_result = pandas.concat([pd_left, pd_right]) - pandas.testing.assert_frame_equal(bf_result.compute(), pd_result) + pandas.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) def test_concat_multi_indices_ignore_index(scalars_df_index, scalars_pandas_df_index): @@ -123,12 +124,12 @@ def test_concat_multi_indices_ignore_index(scalars_df_index, scalars_pandas_df_i # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pandas.Int64Dtype()) - pandas.testing.assert_frame_equal(bf_result.compute(), pd_result) + pandas.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) def test_multi_index_loc(scalars_df_index, scalars_pandas_df_index): bf_result = ( - scalars_df_index.set_index(["int64_too", "bool_col"]).loc[[2, 0]].compute() + scalars_df_index.set_index(["int64_too", "bool_col"]).loc[[2, 0]].to_pandas() ) pd_result = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]).loc[[2, 0]] @@ -139,7 +140,7 @@ def test_multi_index_getitem_bool(scalars_df_index, scalars_pandas_df_index): bf_frame = scalars_df_index.set_index(["int64_too", "bool_col"]) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) - bf_result = bf_frame[bf_frame["int64_col"] > 0].compute() + bf_result = bf_frame[bf_frame["int64_col"] > 0].to_pandas() pd_result = pd_frame[pd_frame["int64_col"] > 0] pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -159,12 +160,30 @@ def test_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, level) bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) - bf_result = bf_frame.droplevel(level).compute() + bf_result = bf_frame.droplevel(level).to_pandas() pd_result = pd_frame.droplevel(level) pandas.testing.assert_frame_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("labels", "level"), + [ + (1, 0), + ([0, 1], 0), + ([True, None], 1), + ], +) +def test_multi_index_drop(scalars_df_index, scalars_pandas_df_index, labels, level): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + + bf_result = bf_frame.drop(labels=labels, axis="index", level=level).to_pandas() + pd_result = pd_frame.drop(labels=labels, axis="index", level=level) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + @pytest.mark.parametrize( ("order"), [ @@ -182,7 +201,7 @@ def test_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_index, o bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) - bf_result = bf_frame.reorder_levels(order).compute() + bf_result = bf_frame.reorder_levels(order).to_pandas() pd_result = pd_frame.reorder_levels(order) pandas.testing.assert_frame_equal(bf_result, pd_result) @@ -194,7 +213,7 @@ def test_multi_index_series_groupby(scalars_df_index, scalars_pandas_df_index): bf_frame["float64_col"] .groupby([bf_frame.int64_col % 2, "bool_col"]) .mean() - .compute() + .to_pandas() ) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) pd_result = ( @@ -220,7 +239,7 @@ def test_multi_index_series_groupby_level( scalars_df_index.set_index(["int64_too", "bool_col"])["float64_col"] .groupby(level=level) .mean() - .compute() + .to_pandas() ) pd_result = ( scalars_pandas_df_index.set_index(["int64_too", "bool_col"])["float64_col"] @@ -236,7 +255,7 @@ def test_multi_index_dataframe_groupby(scalars_df_index, scalars_pandas_df_index bf_result = ( bf_frame.groupby([bf_frame.int64_col % 2, "bool_col"]) .mean(numeric_only=True) - .compute() + .to_pandas() ) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) pd_result = pd_frame.groupby([pd_frame.int64_col % 2, "bool_col"]).mean( @@ -247,27 +266,180 @@ def test_multi_index_dataframe_groupby(scalars_df_index, scalars_pandas_df_index @pytest.mark.parametrize( - ("level"), + ("level", "as_index"), [ - (1), - ([0]), - (["bool_col"]), - (["bool_col", "int64_too"]), + (1, True), + ([0], False), + (["bool_col"], True), + (["bool_col", "int64_too"], False), ], ) -def test_multi_index_dataframe_groupby_level( - scalars_df_index, scalars_pandas_df_index, level +def test_multi_index_dataframe_groupby_level_aggregate( + scalars_df_index, scalars_pandas_df_index, level, as_index ): bf_result = ( scalars_df_index.set_index(["int64_too", "bool_col"]) - .groupby(level=level) + .groupby(level=level, as_index=as_index) .mean(numeric_only=True) - .compute() + .to_pandas() ) pd_result = ( scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) - .groupby(level=level) + .groupby(level=level, as_index=as_index) .mean(numeric_only=True) ) - pandas.testing.assert_frame_equal(bf_result, pd_result) + # Pandas will have int64 index, while bigquery will have Int64 when resetting + pandas.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + +@pytest.mark.parametrize( + ("level", "as_index"), + [ + (1, True), + ([0], False), + ( + ["bool_col"], + True, + ), + (["bool_col", "int64_too"], False), + ], +) +def test_multi_index_dataframe_groupby_level_analytic( + scalars_df_index, scalars_pandas_df_index, level, as_index +): + bf_result = ( + scalars_df_index.set_index(["int64_too", "bool_col"]) + .groupby(level=level, as_index=as_index, dropna=False) + .cumsum(numeric_only=True) + .to_pandas() + ) + pd_result = ( + scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) + .groupby(level=level, as_index=as_index, dropna=False) + .cumsum(numeric_only=True) + ) + + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +all_joins = pytest.mark.parametrize( + ("how",), + ( + ("outer",), + ("left",), + ("right",), + ("inner",), + ), +) + + +@all_joins +# Both DFs are multi-index +def test_multi_index_dataframe_join(scalars_dfs, how): + bf_df, pd_df = scalars_dfs + + bf_df_a = bf_df.set_index((["bool_col", "rowindex_2"]))[["string_col", "int64_col"]] + bf_df_b = bf_df.assign(rowindex_2=bf_df["rowindex_2"] + 2).set_index( + (["bool_col", "rowindex_2"]) + )[["float64_col"]] + bf_result = bf_df_a.join(bf_df_b, how=how).to_pandas() + + pd_df_a = pd_df.set_index((["bool_col", "rowindex_2"]))[["string_col", "int64_col"]] + pd_df_b = pd_df.assign(rowindex_2=pd_df["rowindex_2"] + 2).set_index( + (["bool_col", "rowindex_2"]) + )[["float64_col"]] + pd_result = pd_df_a.join(pd_df_b, how=how) + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@all_joins +# Only left DF is multi-index +def test_multi_index_dataframe_join_on(scalars_dfs, how): + bf_df, pd_df = scalars_dfs + + bf_df_a = bf_df.set_index((["int64_too", "bool_col"]))[ + ["string_col", "int64_col", "rowindex_2"] + ] + bf_df_a = bf_df_a.assign(rowindex_2=bf_df_a["rowindex_2"] + 2) + bf_df_b = bf_df[["float64_col"]] + bf_result = bf_df_a.join(bf_df_b, on="rowindex_2", how=how).to_pandas() + + pd_df_a = pd_df.set_index((["int64_too", "bool_col"]))[ + ["string_col", "int64_col", "rowindex_2"] + ] + pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) + pd_df_b = pd_df[["float64_col"]] + pd_result = pd_df_a.join(pd_df_b, on="rowindex_2", how=how) + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("level",), + [ + (1,), + ([0],), + (["bool_col"],), + (["bool_col", "int64_too"],), + ], +) +def test_multi_index_series_groupby_level_aggregate( + scalars_df_index, scalars_pandas_df_index, level +): + bf_result = ( + scalars_df_index.set_index(["int64_too", "bool_col"])["float64_col"] + .groupby(level=level) + .mean() + .to_pandas() + ) + pd_result = ( + scalars_pandas_df_index.set_index(["int64_too", "bool_col"])["float64_col"] + .groupby(level=level) + .mean() + ) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("level",), + [ + (1,), + ([0],), + (["bool_col"],), + (["bool_col", "int64_too"],), + ], +) +def test_multi_index_series_groupby_level_analytic( + scalars_df_index, scalars_pandas_df_index, level +): + bf_result = ( + scalars_df_index.set_index(["int64_too", "bool_col"])["float64_col"] + .groupby(level=level, dropna=False) + .cumsum() + .to_pandas() + ) + pd_result = ( + scalars_pandas_df_index.set_index(["int64_too", "bool_col"])["float64_col"] + .groupby(level=level, dropna=False) + .cumsum() + ) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + + +def test_multi_index_series_rename_dict_same_type( + scalars_df_index, scalars_pandas_df_index +): + bf_result = ( + scalars_df_index.set_index(["rowindex_2", "int64_too"])["string_col"] + .rename({1: 100, 2: 200}) + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index(["rowindex_2", "int64_too"])[ + "string_col" + ].rename({1: 100, 2: 200}) + + pandas.testing.assert_series_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index a612ad946fa..98bafc6392c 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -21,7 +21,7 @@ def test_concat_dataframe(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = bpd.concat(11 * [scalars_df]) - bf_result = bf_result.compute() + bf_result = bf_result.to_pandas() pd_result = pd.concat(11 * [scalars_pandas_df]) pd.testing.assert_frame_equal(bf_result, pd_result) @@ -32,7 +32,7 @@ def test_concat_series(scalars_dfs): bf_result = bpd.concat( [scalars_df.int64_col, scalars_df.int64_too, scalars_df.int64_col] ) - bf_result = bf_result.compute() + bf_result = bf_result.to_pandas() pd_result = pd.concat( [ scalars_pandas_df.int64_col, @@ -45,10 +45,10 @@ def test_concat_series(scalars_dfs): @pytest.mark.parametrize( - ("how",), + ("how"), [ - ("inner",), - ("outer",), + ("inner"), + ("outer"), ], ) def test_concat_dataframe_mismatched_columns(scalars_dfs, how): @@ -56,9 +56,52 @@ def test_concat_dataframe_mismatched_columns(scalars_dfs, how): cols2 = ["int64_col", "string_col", "int64_too"] scalars_df, scalars_pandas_df = scalars_dfs bf_result = bpd.concat([scalars_df[cols1], scalars_df[cols2]], join=how) - bf_result = bf_result.compute() + bf_result = bf_result.to_pandas() pd_result = pd.concat( - [scalars_pandas_df[cols1], scalars_pandas_df[cols2]], join=how + [scalars_pandas_df[cols1], scalars_pandas_df[cols2]], + join=how, ) pd.testing.assert_frame_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("how",), + [ + ("inner",), + ("outer",), + ], +) +def test_concat_axis_1(scalars_dfs, how): + if pd.__version__.startswith("1."): + pytest.skip("pandas has different behavior in 1.x") + scalars_df, scalars_pandas_df = scalars_dfs + cols1 = ["int64_col", "float64_col", "rowindex_2"] + cols2 = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + part1 = scalars_df[cols1] + part1.index.name = "newindexname" + # Offset the rows somewhat so that outer join can have an effect. + part2 = ( + scalars_df[cols2] + .assign(rowindex_2=scalars_df["rowindex_2"] + 2) + .sort_values(["string_col"], kind="stable") + ) + part3 = scalars_df["int64_too"].cumsum().iloc[2:] + + bf_result = bpd.concat([part1, part2, part3], join=how, axis=1) + + # Copy since modifying index + pd_part1 = scalars_pandas_df.copy()[cols1] + pd_part1.index.name = "newindexname" + # Offset the rows somewhat so that outer join can have an effect. + pd_part2 = ( + scalars_pandas_df[cols2] + .assign(rowindex_2=scalars_pandas_df["rowindex_2"] + 2) + .sort_values(["string_col"], kind="stable") + ) + pd_part3 = scalars_pandas_df["int64_too"].cumsum().iloc[2:] + + pd_result = pd.concat([pd_part1, pd_part2, pd_part3], join=how, axis=1) + + pd.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index 261e0d5b14e..96697dbcabe 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -12,7 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime +from unittest import mock + import google.api_core.exceptions +import google.auth +import google.auth.exceptions import pytest import bigframes.pandas as bpd @@ -79,6 +84,10 @@ def test_read_gbq_start_sets_session_location( # There should still be the previous location set in the bigquery options assert bpd.options.bigquery.location == tokyo_location + # Reset the location to be able to query another location + bpd.options.bigquery.location = None + assert not bpd.options.bigquery.location + # Starting over the user journey with read_gbq* should work for a table # in another location, in this case US df = read_method(query) @@ -143,3 +152,188 @@ def test_read_gbq_after_session_start_must_comply_with_default_location( # read_gbq* from a table in the default location should work df = read_method(query) assert df is not None + + +@pytest.mark.parametrize( + ("read_method", "query_prefix"), + [ + (bpd.read_gbq, None), + (bpd.read_gbq, "SELECT COUNT(1) FROM "), + (bpd.read_gbq_table, None), + (bpd.read_gbq_query, "SELECT COUNT(1) FROM "), + ], + ids=[ + "read_gbq-on-table-name", + "read_gbq-on-sql", + "read_gbq_table-on-table-name", + "read_gbq_query-on-sql", + ], +) +def test_read_gbq_must_comply_with_set_location_US( + test_data_tables, + test_data_tables_tokyo, + dataset_id_permanent_tokyo, + read_method, + query_prefix, +): + # Form query as a table name or a SQL depending on the test scenario + query_tokyo = test_data_tables_tokyo["scalars"] + query = test_data_tables["scalars"] + if query_prefix: + query_tokyo = f"{query_prefix} {query_tokyo}" + query = f"{query_prefix} {query}" + + # Initially there is no location set in the bigquery options + assert not bpd.options.bigquery.location + + # Explicitly set location + bpd.options.bigquery.location = "US" + assert bpd.options.bigquery.location == "US" + + # Starting user journey with read_gbq* from another location should fail + with pytest.raises( + google.api_core.exceptions.NotFound, + match=f"404 Not found: Dataset {dataset_id_permanent_tokyo} was not found in location US", + ): + read_method(query_tokyo) + + # Starting user journey with read_gbq* should work for a table in the same + # location, in this case tokyo + df = read_method(query) + assert df is not None + + +@pytest.mark.parametrize( + ("read_method", "query_prefix"), + [ + (bpd.read_gbq, None), + (bpd.read_gbq, "SELECT COUNT(1) FROM "), + (bpd.read_gbq_table, None), + (bpd.read_gbq_query, "SELECT COUNT(1) FROM "), + ], + ids=[ + "read_gbq-on-table-name", + "read_gbq-on-sql", + "read_gbq_table-on-table-name", + "read_gbq_query-on-sql", + ], +) +def test_read_gbq_must_comply_with_set_location_non_US( + tokyo_location, + test_data_tables, + test_data_tables_tokyo, + dataset_id_permanent, + read_method, + query_prefix, +): + # Form query as a table name or a SQL depending on the test scenario + query_tokyo = test_data_tables_tokyo["scalars"] + query = test_data_tables["scalars"] + if query_prefix: + query_tokyo = f"{query_prefix} {query_tokyo}" + query = f"{query_prefix} {query}" + + # Initially there is no location set in the bigquery options + assert not bpd.options.bigquery.location + + # Explicitly set location + bpd.options.bigquery.location = tokyo_location + assert bpd.options.bigquery.location == tokyo_location + + # Starting user journey with read_gbq* from another location should fail + with pytest.raises( + google.api_core.exceptions.NotFound, + match=f"404 Not found: Dataset {dataset_id_permanent} was not found in location {tokyo_location}", + ): + read_method(query) + + # Starting user journey with read_gbq* should work for a table in the same + # location, in this case tokyo + df = read_method(query_tokyo) + assert df is not None + + +def test_reset_session_after_bq_session_ended(): + # Use a simple test query to verify that default session works to interact + # with BQ + test_query = "SELECT 1" + + # Confirm that there is a session id in the default session + session = bpd.get_global_session() + assert session._session_id + + # Confirm that session works as usual + df = bpd.read_gbq(test_query) + assert df is not None + + # Abort the session to simulate the auto-expiration + # https://cloud.google.com/bigquery/docs/sessions-terminating#auto-terminate_a_session + abort_session_query = "CALL BQ.ABORT_SESSION()" + query_job = session.bqclient.query(abort_session_query) + query_job.result() # blocks until finished + + # Confirm that session is unusable to run any jobs + with pytest.raises( + google.api_core.exceptions.BadRequest, + match=f"Session {session._session_id} has expired and is no longer available.", + ): + query_job = session.bqclient.query(test_query) + query_job.result() # blocks until finished + + # Confirm that as a result bigframes.pandas interface is unusable + with pytest.raises( + google.api_core.exceptions.BadRequest, + match=f"Session {session._session_id} has expired and is no longer available.", + ): + bpd.read_gbq(test_query) + + # Now try to reset session and verify that it works + bpd.reset_session() + assert bpd._global_session is None + + # Now verify that use is able to start over + df = bpd.read_gbq(test_query) + assert df is not None + + +def test_reset_session_after_credentials_need_reauthentication(monkeypatch): + # Use a simple test query to verify that default session works to interact + # with BQ + test_query = "SELECT 1" + + # Confirm that default session has BQ client with valid credentials + session = bpd.get_global_session() + assert session.bqclient._credentials.valid + + # Confirm that default session works as usual + df = bpd.read_gbq(test_query) + assert df is not None + + with monkeypatch.context() as m: + # Simulate expired credentials to trigger the credential refresh flow + m.setattr(session.bqclient._credentials, "expiry", datetime.datetime.utcnow()) + assert not session.bqclient._credentials.valid + + # Simulate an exception during the credential refresh flow + m.setattr( + session.bqclient._credentials, + "refresh", + mock.Mock(side_effect=google.auth.exceptions.RefreshError()), + ) + + # Confirm that session is unusable to run any jobs + with pytest.raises(google.auth.exceptions.RefreshError): + query_job = session.bqclient.query(test_query) + query_job.result() # blocks until finished + + # Confirm that as a result bigframes.pandas interface is unusable + with pytest.raises(google.auth.exceptions.RefreshError): + bpd.read_gbq(test_query) + + # Now verify that resetting the session works + bpd.reset_session() + assert bpd._global_session is None + + # Now verify that use is able to start over + df = bpd.read_gbq(test_query) + assert df is not None diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index 6eeee3a3a43..00380c26390 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import tempfile + +import pandas as pd + import bigframes as bf import bigframes.formatting_helpers as formatting_helpers @@ -20,11 +24,12 @@ def test_progress_bar_dataframe( penguins_df_default_index: bf.dataframe.DataFrame, capsys ): bf.options.display.progress_bar = "notebook" - penguins_df_default_index.compute() + penguins_df_default_index.to_pandas() html_check = "HTML(value=" open_job_check = "Open Job" lines = capsys.readouterr().out.split("\n") - lines = filter(None, lines) + lines = [line for line in lines if len(line) > 0] + assert len(lines) > 0 assert penguins_df_default_index.query_job is not None for line in lines: assert html_check in line and open_job_check in line @@ -33,11 +38,12 @@ def test_progress_bar_dataframe( def test_progress_bar_series(penguins_df_default_index: bf.dataframe.DataFrame, capsys): bf.options.display.progress_bar = "notebook" series = penguins_df_default_index["body_mass_g"].head(10) - series.compute() + series.to_pandas() html_check = "HTML(value=" open_job_check = "Open Job" lines = capsys.readouterr().out.split("\n") - lines = filter(None, lines) + lines = [line for line in lines if len(line) > 0] + assert len(lines) > 0 assert series.query_job is not None for line in lines: assert html_check in line and open_job_check in line @@ -49,18 +55,63 @@ def test_progress_bar_scalar(penguins_df_default_index: bf.dataframe.DataFrame, html_check = "HTML(value=" open_job_check = "Open Job" lines = capsys.readouterr().out.split("\n") - lines = filter(None, lines) + lines = [line for line in lines if len(line) > 0] + assert len(lines) > 0 for line in lines: assert html_check in line and open_job_check in line -def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): +def test_progress_bar_read_gbq(session: bf.Session, penguins_table_id: str, capsys): + bf.options.display.progress_bar = "notebook" + session.read_gbq(penguins_table_id) + html_check = "HTML(value=" + open_job_check = "Open Job" + lines = capsys.readouterr().out.split("\n") + lines = [line for line in lines if len(line) > 0] + assert len(lines) > 0 + for line in lines: + assert html_check in line and open_job_check in line + + +def test_progress_bar_extract_jobs( + penguins_df_default_index: bf.dataframe.DataFrame, gcs_folder, capsys +): + bf.options.display.progress_bar = "notebook" + path = gcs_folder + "test_read_csv_progress_bar*.csv" + penguins_df_default_index.to_csv(path) + html_check = "HTML(value=" + open_job_check = "Open Job" + lines = capsys.readouterr().out.split("\n") + lines = [line for line in lines if len(line) > 0] + assert len(lines) > 0 + for line in lines: + assert html_check in line and open_job_check in line + + +def test_progress_bar_load_jobs( + session: bf.Session, penguins_pandas_df_default_index: pd.DataFrame, capsys +): + bf.options.display.progress_bar = "notebook" + with tempfile.TemporaryDirectory() as dir: + path = dir + "/test_read_csv_progress_bar*.csv" + penguins_pandas_df_default_index.to_csv(path, index=False) + session.read_csv(path) + html_check = "HTML(value=" + open_job_check = "Open Job" + lines = capsys.readouterr().out.split("\n") + lines = [line for line in lines if len(line) > 0] + assert len(lines) > 0 + for line in lines: + assert html_check in line and open_job_check in line + + +def test_query_job_repr_html(penguins_df_default_index: bf.dataframe.DataFrame): bf.options.display.progress_bar = "notebook" penguins_df_default_index._block._expr._session.bqclient.default_query_job_config.use_query_cache = ( False ) - penguins_df_default_index.compute() - query_job_repr = formatting_helpers.repr_query_job( + penguins_df_default_index.to_pandas() + query_job_repr = formatting_helpers.repr_query_job_html( penguins_df_default_index.query_job ).value string_checks = [ @@ -72,3 +123,33 @@ def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): ] for string in string_checks: assert string in query_job_repr + + +def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): + penguins_df_default_index._block._expr._session.bqclient.default_query_job_config.use_query_cache = ( + False + ) + penguins_df_default_index.to_pandas() + query_job_repr = formatting_helpers.repr_query_job( + penguins_df_default_index.query_job + ) + string_checks = [ + "Job", + "Destination Table", + "Slot Time", + "Bytes Processed", + "Cache hit", + ] + for string in string_checks: + assert string in query_job_repr + + +def test_query_job_dry_run( + penguins_df_default_index: bf.dataframe.DataFrame, capsys, deferred_repr +): + repr(penguins_df_default_index) + repr(penguins_df_default_index["body_mass_g"]) + lines = capsys.readouterr().out.split("\n") + lines = filter(None, lines) + for line in lines: + assert "Computation deferred. Computation will process" in line diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 4c75b5d3d27..fe4b1c5a97a 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -16,7 +16,7 @@ import pytest import bigframes -from bigframes.remote_function import remote_function +from bigframes.remote_function import read_gbq_function, remote_function from tests.system.utils import assert_pandas_df_equal_ignore_ordering @@ -28,6 +28,38 @@ def bq_cf_connection() -> str: return "bigframes-rf-conn" +@pytest.fixture(scope="module") +def bq_cf_connection_location() -> str: + """Pre-created BQ connection to invoke cloud function for bigframes-dev + $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn + """ + return "us.bigframes-rf-conn" + + +@pytest.fixture(scope="module") +def bq_cf_connection_location_mistached() -> str: + """Pre-created BQ connection to invoke cloud function for bigframes-dev + $ bq show --connection --location=us-east1 --project_id=bigframes-dev bigframes-rf-conn + """ + return "us-east1.bigframes-rf-conn" + + +@pytest.fixture(scope="module") +def bq_cf_connection_location_project() -> str: + """Pre-created BQ connection to invoke cloud function for bigframes-dev + $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn + """ + return "bigframes-dev.us.bigframes-rf-conn" + + +@pytest.fixture(scope="module") +def bq_cf_connection_location_project_mistached() -> str: + """Pre-created BQ connection to invoke cloud function for bigframes-dev + $ bq show --connection --location=us-east1 --project_id=bigframes-metrics bigframes-rf-conn + """ + return "bigframes-metrics.us-east1.bigframes-rf-conn" + + @pytest.fixture(scope="module") def session_with_bq_connection(bq_cf_connection) -> bigframes.Session: return bigframes.Session( @@ -35,10 +67,42 @@ def session_with_bq_connection(bq_cf_connection) -> bigframes.Session: ) +@pytest.fixture(scope="module") +def session_with_bq_connection_location_specified( + bq_cf_connection_location, +) -> bigframes.Session: + return bigframes.Session( + bigframes.BigQueryOptions(remote_udf_connection=bq_cf_connection_location) + ) + + +@pytest.fixture(scope="module") +def session_with_bq_connection_location_mistached( + bq_cf_connection_location_mistached, +) -> bigframes.Session: + return bigframes.Session( + bigframes.BigQueryOptions( + remote_udf_connection=bq_cf_connection_location_mistached + ) + ) + + +@pytest.fixture(scope="module") +def session_with_bq_connection_location_project_specified( + bq_cf_connection_location_project, +) -> bigframes.Session: + return bigframes.Session( + bigframes.BigQueryOptions( + remote_udf_connection=bq_cf_connection_location_project + ) + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_direct_no_session_param( bigquery_client, bigqueryconnection_client, + cloudfunctions_client, scalars_dfs, dataset_id_permanent, bq_cf_connection, @@ -48,6 +112,7 @@ def test_remote_function_direct_no_session_param( int, bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, + cloud_functions_client=cloudfunctions_client, dataset=dataset_id_permanent, bigquery_connection=bq_cf_connection, # See e2e tests for tests that actually deploy the Cloud Function. @@ -56,13 +121,112 @@ def test_remote_function_direct_no_session_param( def square(x): return x * x + assert square.bigframes_remote_function + assert square.bigframes_cloud_function + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_direct_no_session_param_location_specified( + bigquery_client, + bigqueryconnection_client, + cloudfunctions_client, + scalars_dfs, + dataset_id_permanent, + bq_cf_connection_location, +): + @remote_function( + [int], + int, + bigquery_client=bigquery_client, + bigquery_connection_client=bigqueryconnection_client, + cloud_functions_client=cloudfunctions_client, + dataset=dataset_id_permanent, + bigquery_connection=bq_cf_connection_location, + # See e2e tests for tests that actually deploy the Cloud Function. + reuse=True, + ) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_direct_no_session_param_location_mistached( + bigquery_client, + bigqueryconnection_client, + cloudfunctions_client, + scalars_dfs, + dataset_id_permanent, + bq_cf_connection_location_mistached, +): + @remote_function( + [int], + int, + bigquery_client=bigquery_client, + bigquery_connection_client=bigqueryconnection_client, + cloud_functions_client=cloudfunctions_client, + dataset=dataset_id_permanent, + bigquery_connection=bq_cf_connection_location_mistached, + # See e2e tests for tests that actually deploy the Cloud Function. + reuse=True, + ) + def square(x): + return x * x + scalars_df, scalars_pandas_df = scalars_dfs bf_int64_col = scalars_df["int64_col"] bf_int64_col_filter = bf_int64_col.notnull() bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) pd_int64_col = scalars_pandas_df["int64_col"] pd_int64_col_filter = pd_int64_col.notnull() @@ -78,6 +242,78 @@ def square(x): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_direct_no_session_param_location_project_specified( + bigquery_client, + bigqueryconnection_client, + cloudfunctions_client, + scalars_dfs, + dataset_id_permanent, + bq_cf_connection_location_project, +): + @remote_function( + [int], + int, + bigquery_client=bigquery_client, + bigquery_connection_client=bigqueryconnection_client, + cloud_functions_client=cloudfunctions_client, + dataset=dataset_id_permanent, + bigquery_connection=bq_cf_connection_location_project, + # See e2e tests for tests that actually deploy the Cloud Function. + reuse=True, + ) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_direct_no_session_param_project_mismatched( + bigquery_client, + bigqueryconnection_client, + cloudfunctions_client, + dataset_id_permanent, + bq_cf_connection_location_project_mistached, +): + with pytest.raises(ValueError): + + @remote_function( + [int], + int, + bigquery_client=bigquery_client, + bigquery_connection_client=bigqueryconnection_client, + cloud_functions_client=cloudfunctions_client, + dataset=dataset_id_permanent, + bigquery_connection=bq_cf_connection_location_project_mistached, + # See e2e tests for tests that actually deploy the Cloud Function. + reuse=True, + ) + def square(x): + return x * x + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_direct_session_param(session_with_bq_connection, scalars_dfs): @remote_function( @@ -94,7 +330,9 @@ def square(x): bf_int64_col_filter = bf_int64_col.notnull() bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) pd_int64_col = scalars_pandas_df["int64_col"] pd_int64_col_filter = pd_int64_col.notnull() @@ -129,7 +367,9 @@ def square(x): bf_int64_col_filter = bf_int64_col.notnull() bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) pd_int64_col = scalars_pandas_df["int64_col"] pd_int64_col_filter = pd_int64_col.notnull() @@ -166,7 +406,9 @@ def square(x): bf_int64_col_filter = bf_int64_col.notnull() bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) pd_int64_col = scalars_pandas_df["int64_col"] pd_int64_col_filter = pd_int64_col.notnull() @@ -210,7 +452,9 @@ def square(x): bf_int64_col_filter = bf_int64_col.notnull() bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = bf_int64_col_filtered.to_frame().assign(result=bf_result_col).compute() + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) pd_int64_col = scalars_pandas_df["int64_col"] pd_int64_col_filter = pd_int64_col.notnull() @@ -238,7 +482,7 @@ def add_one(x): bf_int64_df = scalars_df[int64_cols] bf_int64_df_filtered = bf_int64_df.dropna() - bf_result = bf_int64_df_filtered.applymap(remote_add_one).compute() + bf_result = bf_int64_df_filtered.applymap(remote_add_one).to_pandas() pd_int64_df = scalars_pandas_df[int64_cols] pd_int64_df_filtered = pd_int64_df.dropna() @@ -264,7 +508,7 @@ def add_one(x): int64_cols = ["int64_col", "int64_too"] bf_int64_df = scalars_df[int64_cols] - bf_result = bf_int64_df.applymap(remote_add_one, na_action="ignore").compute() + bf_result = bf_int64_df.applymap(remote_add_one, na_action="ignore").to_pandas() pd_int64_df = scalars_pandas_df[int64_cols] pd_result = pd_int64_df.applymap(add_one, na_action="ignore") @@ -276,3 +520,56 @@ def add_one(x): pd_result[col] = pd_result[col].astype(pd_int64_df[col].dtype) assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_like_original( + bigquery_client, + bigqueryconnection_client, + cloudfunctions_client, + scalars_df_index, + dataset_id_permanent, + bq_cf_connection, +): + @remote_function( + [int], + int, + bigquery_client=bigquery_client, + bigquery_connection_client=bigqueryconnection_client, + dataset=dataset_id_permanent, + cloud_functions_client=cloudfunctions_client, + bigquery_connection=bq_cf_connection, + reuse=True, + ) + def square1(x): + return x * x + + square2 = read_gbq_function( + function_name=square1.bigframes_remote_function, + bigquery_client=bigquery_client, + ) + + # The newly-created function (square1) should have a remote function AND a + # cloud function associated with it, while the read-back version (square2) + # should only have a remote function. + assert square1.bigframes_remote_function + assert square1.bigframes_cloud_function + + assert square2.bigframes_remote_function + assert not hasattr(square2, "bigframes_cloud_function") + + # They should point to the same function. + assert square1.bigframes_remote_function == square2.bigframes_remote_function + + # The result of applying them should be the same. + int64_col = scalars_df_index["int64_col"] + int64_col_filter = int64_col.notnull() + int64_col_filtered = int64_col[int64_col_filter] + + s1_result_col = int64_col_filtered.apply(square1) + s1_result = int64_col_filtered.to_frame().assign(result=s1_result_col) + + s2_result_col = int64_col_filtered.apply(square2) + s2_result = int64_col_filtered.to_frame().assign(result=s2_result_col) + + assert_pandas_df_equal_ignore_ordering(s1_result.to_pandas(), s2_result.to_pandas()) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 751521bd754..1c1e7b035bd 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -13,6 +13,7 @@ # limitations under the License. import math +import re import tempfile import geopandas as gpd # type: ignore @@ -21,6 +22,7 @@ import pyarrow as pa # type: ignore import pytest +import bigframes.pandas import bigframes.series as series from tests.system.utils import ( assert_pandas_df_equal_ignore_ordering, @@ -32,7 +34,7 @@ def test_series_construct_copy(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = series.Series( scalars_df["int64_col"], name="test_series", dtype="Float64" - ).compute() + ).to_pandas() pd_result = pd.Series( scalars_pandas_df["int64_col"], name="test_series", dtype="Float64" ) @@ -48,11 +50,11 @@ def test_series_construct_pandas(scalars_dfs): scalars_pandas_df["int64_col"], name="test_series", dtype="Float64" ) assert bf_result.shape == pd_result.shape - pd.testing.assert_series_equal(bf_result.compute(), pd_result) + pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) def test_series_construct_from_list(): - bf_result = series.Series([1, 1, 2, 3, 5, 8, 13], dtype="Int64").compute() + bf_result = series.Series([1, 1, 2, 3, 5, 8, 13], dtype="Int64").to_pandas() pd_result = pd.Series([1, 1, 2, 3, 5, 8, 13], dtype="Int64") # BigQuery DataFrame default indices use nullable Int64 always @@ -61,6 +63,22 @@ def test_series_construct_from_list(): pd.testing.assert_series_equal(bf_result, pd_result) +def test_series_construct_from_list_escaped_strings(): + """Check that special characters are supported.""" + strings = [ + "string\nwith\nnewline", + "string\twith\ttabs", + "string\\with\\backslashes", + ] + bf_result = series.Series(strings, name="test_series", dtype="string[pyarrow]") + pd_result = pd.Series(strings, name="test_series", dtype="string[pyarrow]") + + # BigQuery DataFrame default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + + @pytest.mark.parametrize( ["col_name", "expected_dtype"], [ @@ -83,7 +101,7 @@ def test_series_construct_from_list(): def test_get_column(scalars_dfs, col_name, expected_dtype): scalars_df, scalars_pandas_df = scalars_dfs series = scalars_df[col_name] - series_pandas = series.compute() + series_pandas = series.to_pandas() assert series_pandas.dtype == expected_dtype assert series_pandas.shape[0] == scalars_pandas_df.shape[0] @@ -94,6 +112,54 @@ def test_series_get_column_default(scalars_dfs): assert result == "default_val" +def test_series_get_with_default_index(scalars_dfs): + col_name = "float64_col" + key = 2 + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].get(key) + pd_result = scalars_pandas_df[col_name].get(key) + assert bf_result.to_pandas().iloc[0] == pd_result + + +@pytest.mark.parametrize( + ("index_col", "key"), + ( + ("int64_too", 2), + ("string_col", "Hello, World!"), + ("int64_too", slice(2, 6)), + ), +) +def test_series___getitem__(scalars_dfs, index_col, key): + col_name = "float64_col" + scalars_df, scalars_pandas_df = scalars_dfs + scalars_df = scalars_df.set_index(index_col, drop=False) + scalars_pandas_df = scalars_pandas_df.set_index(index_col, drop=False) + bf_result = scalars_df[col_name][key] + pd_result = scalars_pandas_df[col_name][key] + pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + + +def test_series___getitem___with_int_key(scalars_dfs): + col_name = "int64_too" + index_col = "string_col" + key = 2 + scalars_df, scalars_pandas_df = scalars_dfs + scalars_df = scalars_df.set_index(index_col, drop=False) + scalars_pandas_df = scalars_pandas_df.set_index(index_col, drop=False) + bf_result = scalars_df[col_name][key] + pd_result = scalars_pandas_df[col_name][key] + assert bf_result == pd_result + + +def test_series___getitem___with_default_index(scalars_dfs): + col_name = "float64_col" + key = 2 + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name][key] + pd_result = scalars_pandas_df[col_name][key] + assert bf_result.to_pandas().iloc[0] == pd_result + + @pytest.mark.parametrize( ("col_name",), ( @@ -103,7 +169,7 @@ def test_series_get_column_default(scalars_dfs): ) def test_abs(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df[col_name].abs().compute() + bf_result = scalars_df[col_name].abs().to_pandas() pd_result = scalars_pandas_df[col_name].abs() assert_series_equal_ignoring_order(pd_result, bf_result) @@ -112,7 +178,7 @@ def test_abs(scalars_dfs, col_name): def test_fillna(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_result = scalars_df[col_name].fillna("Missing").compute() + bf_result = scalars_df[col_name].fillna("Missing").to_pandas() pd_result = scalars_pandas_df[col_name].fillna("Missing") assert_series_equal_ignoring_order( pd_result, @@ -120,6 +186,25 @@ def test_fillna(scalars_dfs): ) +def test_series_agg_single_string(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["int64_col"].agg("sum") + pd_result = scalars_pandas_df["int64_col"].agg("sum") + assert math.isclose(pd_result, bf_result) + + +def test_series_agg_multi_string(scalars_dfs): + aggregations = ["sum", "mean", "std", "var", "min", "max", "nunique", "count"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["int64_col"].agg(aggregations).to_pandas() + pd_result = scalars_pandas_df["int64_col"].agg(aggregations) + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + @pytest.mark.parametrize( ("col_name",), ( @@ -176,6 +261,36 @@ def test_kurt(scalars_dfs, col_name): assert math.isclose(pd_result, bf_result) +@pytest.mark.parametrize( + ("col_name",), + ( + ("float64_col",), + ("int64_col",), + ), +) +def test_skew(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].skew() + pd_result = scalars_pandas_df[col_name].skew() + assert math.isclose(pd_result, bf_result) + + +def test_skew_undefined(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["int64_col"].iloc[:2].skew() + pd_result = scalars_pandas_df["int64_col"].iloc[:2].skew() + # both should be pd.NA + assert pd_result is bf_result + + +def test_kurt_undefined(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["int64_col"].iloc[:3].kurt() + pd_result = scalars_pandas_df["int64_col"].iloc[:3].kurt() + # both should be pd.NA + assert pd_result is bf_result + + @pytest.mark.parametrize( ("col_name",), ( @@ -198,7 +313,7 @@ def test_var(scalars_dfs, col_name): ), ) def test_mode_stat(scalars_df_index, scalars_pandas_df_index, col_name): - bf_result = scalars_df_index[col_name].mode().compute() + bf_result = scalars_df_index[col_name].mode().to_pandas() pd_result = scalars_pandas_df_index[col_name].mode() ## Mode implicitly resets index, and bigframes default indices use nullable Int64 @@ -244,7 +359,7 @@ def test_series_int_int_operators_scalar( maybe_reversed_op = (lambda x, y: operator(y, x)) if reverse_operands else operator - bf_result = maybe_reversed_op(scalars_df["int64_col"], other_scalar).compute() + bf_result = maybe_reversed_op(scalars_df["int64_col"], other_scalar).to_pandas() pd_result = maybe_reversed_op(scalars_pandas_df["int64_col"], other_scalar) assert_series_equal_ignoring_order(pd_result, bf_result) @@ -270,7 +385,7 @@ def test_series_bool_bool_operators_scalar( maybe_reversed_op = (lambda x, y: operator(y, x)) if reverse_operands else operator - bf_result = maybe_reversed_op(scalars_df["bool_col"], other_scalar).compute() + bf_result = maybe_reversed_op(scalars_df["bool_col"], other_scalar).to_pandas() pd_result = maybe_reversed_op(scalars_pandas_df["bool_col"], other_scalar) assert_series_equal_ignoring_order(pd_result.astype(pd.BooleanDtype()), bf_result) @@ -309,12 +424,101 @@ def test_series_bool_bool_operators_scalar( ) def test_series_int_int_operators_series(scalars_dfs, operator): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = operator(scalars_df["int64_col"], scalars_df["int64_too"]).compute() + bf_result = operator(scalars_df["int64_col"], scalars_df["int64_too"]).to_pandas() pd_result = operator(scalars_pandas_df["int64_col"], scalars_pandas_df["int64_too"]) - assert_series_equal_ignoring_order(pd_result, bf_result) +@pytest.mark.parametrize( + ("col_x",), + [ + ("int64_col",), + ("int64_too",), + ("float64_col",), + ], +) +@pytest.mark.parametrize( + ("col_y",), + [ + ("int64_col",), + ("int64_too",), + ("float64_col",), + ], +) +@pytest.mark.parametrize( + ("method",), + [ + ("mod",), + ("rmod",), + ], +) +def test_mods(scalars_dfs, col_x, col_y, method): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = getattr(scalars_df[col_x], method)(scalars_df[col_y]).to_pandas() + pd_result = getattr(scalars_pandas_df[col_x], method)(scalars_pandas_df[col_y]) + pd.testing.assert_series_equal(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("col_x",), + [ + ("int64_col",), + ("float64_col",), + ], +) +@pytest.mark.parametrize( + ("col_y",), + [ + ("int64_col",), + ("float64_col",), + ], +) +@pytest.mark.parametrize( + ("method",), + [ + ("divmod",), + ("rdivmod",), + ], +) +def test_divmods_series(scalars_dfs, col_x, col_y, method): + scalars_df, scalars_pandas_df = scalars_dfs + bf_div_result, bf_mod_result = getattr(scalars_df[col_x], method)(scalars_df[col_y]) + pd_div_result, pd_mod_result = getattr(scalars_pandas_df[col_x], method)( + scalars_pandas_df[col_y] + ) + pd.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) + pd.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + + +@pytest.mark.parametrize( + ("col_x",), + [ + ("int64_col",), + ("float64_col",), + ], +) +@pytest.mark.parametrize( + ("other",), + [ + (-1000,), + (678,), + ], +) +@pytest.mark.parametrize( + ("method",), + [ + ("divmod",), + ("rdivmod",), + ], +) +def test_divmods_scalars(scalars_dfs, col_x, other, method): + scalars_df, scalars_pandas_df = scalars_dfs + bf_div_result, bf_mod_result = getattr(scalars_df[col_x], method)(other) + pd_div_result, pd_mod_result = getattr(scalars_pandas_df[col_x], method)(other) + pd.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) + pd.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + + @pytest.mark.parametrize( ("other",), [ @@ -324,7 +528,7 @@ def test_series_int_int_operators_series(scalars_dfs, operator): ) def test_series_add_scalar(scalars_dfs, other): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = (scalars_df["float64_col"] + other).compute() + bf_result = (scalars_df["float64_col"] + other).to_pandas() pd_result = scalars_pandas_df["float64_col"] + other assert_series_equal_ignoring_order(pd_result, bf_result) @@ -340,7 +544,7 @@ def test_series_add_scalar(scalars_dfs, other): ) def test_series_add_bigframes_series(scalars_dfs, left_col, right_col): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = (scalars_df[left_col] + scalars_df[right_col]).compute() + bf_result = (scalars_df[left_col] + scalars_df[right_col]).to_pandas() pd_result = scalars_pandas_df[left_col] + scalars_pandas_df[right_col] assert_series_equal_ignoring_order(pd_result, bf_result) @@ -360,7 +564,7 @@ def test_series_add_bigframes_series_nested( scalars_df, scalars_pandas_df = scalars_dfs bf_result = ( (scalars_df[left_col] + scalars_df[right_col]) + scalars_df[righter_col] - ).compute() + ).to_pandas() pd_result = ( scalars_pandas_df[left_col] + scalars_pandas_df[right_col] ) + scalars_pandas_df[righter_col] @@ -375,12 +579,12 @@ def test_series_add_different_table_default_index( bf_result = ( scalars_df_default_index["float64_col"] + scalars_df_2_default_index["float64_col"] - ).compute() + ).to_pandas() pd_result = ( # Default index may not have a well defined order, but it should at - # least be consistent across compute() calls. - scalars_df_default_index["float64_col"].compute() - + scalars_df_2_default_index["float64_col"].compute() + # least be consistent across to_pandas() calls. + scalars_df_default_index["float64_col"].to_pandas() + + scalars_df_2_default_index["float64_col"].to_pandas() ) # TODO(swast): Can remove sort_index() when there's default ordering. pd.testing.assert_series_equal(bf_result.sort_index(), pd_result.sort_index()) @@ -394,7 +598,7 @@ def test_series_add_different_table_with_index( # When index values are unique, we can emulate with values from the same # DataFrame. pd_result = scalars_pandas_df["float64_col"] + scalars_pandas_df["int64_col"] - pd.testing.assert_series_equal(bf_result.compute(), pd_result) + pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) def test_reset_index_drop(scalars_df_index, scalars_pandas_df_index): @@ -413,7 +617,7 @@ def test_reset_index_drop(scalars_df_index, scalars_pandas_df_index): # BigQuery DataFrames default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - pd.testing.assert_series_equal(bf_result.compute(), pd_result) + pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -440,7 +644,7 @@ def test_reset_index_no_drop(scalars_df_index, scalars_pandas_df_index, name): # BigQuery DataFrames default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - pd.testing.assert_frame_equal(bf_result.compute(), pd_result) + pd.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) def test_series_add_pandas_series_not_implemented(scalars_dfs): @@ -451,7 +655,7 @@ def test_series_add_pandas_series_not_implemented(scalars_dfs): + pd.Series( [1, 1, 1, 1], ) - ).compute() + ).to_pandas() def test_copy(scalars_df_index, scalars_pandas_df_index): @@ -467,14 +671,14 @@ def test_copy(scalars_df_index, scalars_pandas_df_index): pd_copy.loc[0] = 5.6 pd_series.loc[0] = 3.4 - assert bf_copy.compute().loc[0] != bf_series.compute().loc[0] - pd.testing.assert_series_equal(bf_copy.compute(), pd_copy) + assert bf_copy.to_pandas().loc[0] != bf_series.to_pandas().loc[0] + pd.testing.assert_series_equal(bf_copy.to_pandas(), pd_copy) def test_isnull(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "float64_col" - bf_series = scalars_df[col_name].isnull().compute() + bf_series = scalars_df[col_name].isnull().to_pandas() pd_series = scalars_pandas_df[col_name].isnull() # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but @@ -485,7 +689,7 @@ def test_isnull(scalars_dfs): def test_notnull(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series = scalars_df[col_name].notnull().compute() + bf_series = scalars_df[col_name].notnull().to_pandas() pd_series = scalars_pandas_df[col_name].notnull() # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but @@ -496,7 +700,7 @@ def test_notnull(scalars_dfs): def test_round(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "float64_col" - bf_result = scalars_df[col_name].round().compute() + bf_result = scalars_df[col_name].round().to_pandas() pd_result = scalars_pandas_df[col_name].round() assert_series_equal_ignoring_order(pd_result, bf_result) @@ -505,7 +709,7 @@ def test_round(scalars_dfs): def test_eq_scalar(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_result = scalars_df[col_name].eq(0).compute() + bf_result = scalars_df[col_name].eq(0).to_pandas() pd_result = scalars_pandas_df[col_name].eq(0) assert_series_equal_ignoring_order(pd_result, bf_result) @@ -514,7 +718,7 @@ def test_eq_scalar(scalars_dfs): def test_eq_wider_type_scalar(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_result = scalars_df[col_name].eq(1.0).compute() + bf_result = scalars_df[col_name].eq(1.0).to_pandas() pd_result = scalars_pandas_df[col_name].eq(1.0) assert_series_equal_ignoring_order(pd_result, bf_result) @@ -523,7 +727,7 @@ def test_eq_wider_type_scalar(scalars_dfs): def test_ne_scalar(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_result = (scalars_df[col_name] != 0).compute() + bf_result = (scalars_df[col_name] != 0).to_pandas() pd_result = scalars_pandas_df[col_name] != 0 assert_series_equal_ignoring_order(pd_result, bf_result) @@ -532,7 +736,7 @@ def test_ne_scalar(scalars_dfs): def test_eq_int_scalar(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_result = (scalars_df[col_name] == 0).compute() + bf_result = (scalars_df[col_name] == 0).to_pandas() pd_result = scalars_pandas_df[col_name] == 0 assert_series_equal_ignoring_order(pd_result, bf_result) @@ -549,7 +753,7 @@ def test_eq_int_scalar(scalars_dfs): def test_eq_same_type_series(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_result = (scalars_df[col_name] == scalars_df[col_name]).compute() + bf_result = (scalars_df[col_name] == scalars_df[col_name]).to_pandas() pd_result = scalars_pandas_df[col_name] == scalars_pandas_df[col_name] # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but @@ -564,18 +768,18 @@ def test_loc_setitem_cell(scalars_df_index, scalars_pandas_df_index): pd_series = scalars_pandas_df_index["string_col"].copy() bf_series.loc[2] = "This value isn't in the test data." pd_series.loc[2] = "This value isn't in the test data." - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() pd_result = pd_series pd.testing.assert_series_equal(bf_result, pd_result) # Per Copy-on-Write semantics, other references to the original DataFrame # should remain unchanged. - pd.testing.assert_series_equal(bf_original.compute(), pd_original) + pd.testing.assert_series_equal(bf_original.to_pandas(), pd_original) def test_ne_obj_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_result = (scalars_df[col_name] != scalars_df[col_name]).compute() + bf_result = (scalars_df[col_name] != scalars_df[col_name]).to_pandas() pd_result = scalars_pandas_df[col_name] != scalars_pandas_df[col_name] # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but @@ -586,7 +790,7 @@ def test_ne_obj_series(scalars_dfs): def test_indexing_using_unselected_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_result = scalars_df[col_name][scalars_df["int64_too"].eq(0)].compute() + bf_result = scalars_df[col_name][scalars_df["int64_too"].eq(0)].to_pandas() pd_result = scalars_pandas_df[col_name][scalars_pandas_df["int64_too"].eq(0)] assert_series_equal_ignoring_order( @@ -600,7 +804,7 @@ def test_indexing_using_selected_series(scalars_dfs): col_name = "string_col" bf_result = scalars_df[col_name][ scalars_df["string_col"].eq("Hello, World!") - ].compute() + ].to_pandas() pd_result = scalars_pandas_df[col_name][ scalars_pandas_df["string_col"].eq("Hello, World!") ] @@ -618,7 +822,7 @@ def test_nested_filter(scalars_dfs): bool_col = scalars_df["bool_col"] == bool( True ) # Convert from nullable bool to nonnullable bool usable as indexer - bf_result = string_col[int64_too == 0][~bool_col].compute() + bf_result = string_col[int64_too == 0][~bool_col].to_pandas() pd_string_col = scalars_pandas_df["string_col"] pd_int64_too = scalars_pandas_df["int64_too"] @@ -633,12 +837,35 @@ def test_nested_filter(scalars_dfs): ) +def test_binop_repeated_application_does_row_identity_joins(scalars_dfs): + """Make sure row identity joins kick in so that we don't do way more joins than expected.""" + scalars_df, scalars_pandas_df = scalars_dfs + bf_series = scalars_df["int64_col"] + pd_series = scalars_pandas_df["int64_col"] + + num_joins = 10 + for _ in range(num_joins): + bf_series = bf_series + bf_series + pd_series = pd_series + pd_series + + bf_result = bf_series.to_pandas() + pd_result = pd_series + assert_series_equal_ignoring_order( + bf_result, + pd_result, + ) + + bf_sql, _ = bf_series.to_frame()._to_sql_query(always_include_index=True) + selects = re.findall("SELECT", bf_sql.upper()) + assert 0 < len(selects) < (num_joins // 2) + + def test_binop_opposite_filters(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs int64_col1 = scalars_df["int64_col"] int64_col2 = scalars_df["int64_col"] bool_col = scalars_df["bool_col"] - bf_result = (int64_col1[bool_col] + int64_col2[bool_col.__invert__()]).compute() + bf_result = (int64_col1[bool_col] + int64_col2[bool_col.__invert__()]).to_pandas() pd_int64_col1 = scalars_pandas_df["int64_col"] pd_int64_col2 = scalars_pandas_df["int64_col"] @@ -656,7 +883,7 @@ def test_binop_left_filtered(scalars_dfs): int64_col = scalars_df["int64_col"] float64_col = scalars_df["float64_col"] bool_col = scalars_df["bool_col"] - bf_result = (int64_col[bool_col] + float64_col).compute() + bf_result = (int64_col[bool_col] + float64_col).to_pandas() pd_int64_col = scalars_pandas_df["int64_col"] pd_float64_col = scalars_pandas_df["float64_col"] @@ -674,7 +901,7 @@ def test_binop_right_filtered(scalars_dfs): int64_col = scalars_df["int64_col"] float64_col = scalars_df["float64_col"] bool_col = scalars_df["bool_col"] - bf_result = (float64_col + int64_col[bool_col]).compute() + bf_result = (float64_col + int64_col[bool_col]).to_pandas() pd_int64_col = scalars_pandas_df["int64_col"] pd_float64_col = scalars_pandas_df["float64_col"] @@ -695,6 +922,16 @@ def test_mean(scalars_dfs): assert math.isclose(pd_result, bf_result) +def test_median(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = scalars_df[col_name].median() + pd_max = scalars_pandas_df[col_name].max() + pd_min = scalars_pandas_df[col_name].min() + # Median is approximate, so just check for plausibility. + assert pd_min < bf_result < pd_max + + def test_repr(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs if scalars_pandas_df.index.name != "rowindex": @@ -722,6 +959,19 @@ def test_product(scalars_dfs): assert math.isclose(pd_result, bf_result) +def test_cumprod(scalars_dfs): + if pd.__version__.startswith("1."): + pytest.skip("Series.cumprod NA mask are different in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "float64_col" + bf_result = scalars_df[col_name].cumprod() + pd_result = scalars_pandas_df[col_name].cumprod() + pd.testing.assert_series_equal( + pd_result, + bf_result.to_pandas(), + ) + + def test_count(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" @@ -762,7 +1012,7 @@ def test_groupby_sum(scalars_dfs): scalars_pandas_df[col_name].groupby(scalars_pandas_df["string_col"]).sum() ) # TODO(swast): Update groupby to use index based on group by key(s). - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() assert_series_equal_ignoring_order( pd_series, bf_result, @@ -780,7 +1030,7 @@ def test_groupby_std(scalars_dfs): .std() .astype(pd.Float64Dtype()) ) - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() assert_series_equal_ignoring_order( pd_series, bf_result, @@ -795,7 +1045,7 @@ def test_groupby_var(scalars_dfs): pd_series = ( scalars_pandas_df[col_name].groupby(scalars_pandas_df["string_col"]).var() ) - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() assert_series_equal_ignoring_order( pd_series, bf_result, @@ -815,7 +1065,7 @@ def test_groupby_level_sum(scalars_dfs): # TODO(swast): Update groupby to use index based on group by key(s). pd.testing.assert_series_equal( pd_series.sort_index(), - bf_series.compute().sort_index(), + bf_series.to_pandas().sort_index(), ) @@ -831,7 +1081,7 @@ def test_groupby_level_list_sum(scalars_dfs): # TODO(swast): Update groupby to use index based on group by key(s). pd.testing.assert_series_equal( pd_series.sort_index(), - bf_series.compute().sort_index(), + bf_series.to_pandas().sort_index(), ) @@ -847,13 +1097,36 @@ def test_groupby_mean(scalars_dfs): .mean() ) # TODO(swast): Update groupby to use index based on group by key(s). - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() assert_series_equal_ignoring_order( pd_series, bf_result, ) +def test_groupby_median(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_series = ( + scalars_df[col_name].groupby(scalars_df["string_col"], dropna=False).median() + ) + pd_max = ( + scalars_pandas_df[col_name] + .groupby(scalars_pandas_df["string_col"], dropna=False) + .max() + ) + pd_min = ( + scalars_pandas_df[col_name] + .groupby(scalars_pandas_df["string_col"], dropna=False) + .min() + ) + # TODO(swast): Update groupby to use index based on group by key(s). + bf_result = bf_series.to_pandas() + + # Median is approximate, so just check that it's plausible. + assert ((pd_min <= bf_result) & (bf_result <= pd_max)).all() + + def test_groupby_prod(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" @@ -862,7 +1135,7 @@ def test_groupby_prod(scalars_dfs): scalars_pandas_df[col_name].groupby(scalars_pandas_df["int64_col"]).prod() ) # TODO(swast): Update groupby to use index based on group by key(s). - bf_result = bf_series.compute() + bf_result = bf_series.to_pandas() assert_series_equal_ignoring_order( pd_series, bf_result, @@ -897,7 +1170,7 @@ def test_groupby_window_ops(scalars_df_index, scalars_pandas_df_index, operator) group_key = "int64_too" # has some duplicates values, good for grouping bf_series = ( operator(scalars_df_index[col_name].groupby(scalars_df_index[group_key])) - ).compute() + ).to_pandas() pd_series = operator( scalars_pandas_df_index[col_name].groupby(scalars_pandas_df_index[group_key]) ).astype(pd.Int64Dtype()) @@ -909,7 +1182,7 @@ def test_groupby_window_ops(scalars_df_index, scalars_pandas_df_index, operator) def test_drop_label(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" - bf_series = scalars_df_index[col_name].drop(1).compute() + bf_series = scalars_df_index[col_name].drop(1).to_pandas() pd_series = scalars_pandas_df_index[col_name].drop(1) pd.testing.assert_series_equal( pd_series, @@ -919,7 +1192,7 @@ def test_drop_label(scalars_df_index, scalars_pandas_df_index): def test_drop_label_list(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" - bf_series = scalars_df_index[col_name].drop([1, 3]).compute() + bf_series = scalars_df_index[col_name].drop([1, 3]).to_pandas() pd_series = scalars_pandas_df_index[col_name].drop([1, 3]) pd.testing.assert_series_equal( pd_series, @@ -943,7 +1216,7 @@ def test_drop_label_list(scalars_df_index, scalars_pandas_df_index): ], ) def test_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep, col_name): - bf_series = scalars_df_index[col_name].drop_duplicates(keep=keep).compute() + bf_series = scalars_df_index[col_name].drop_duplicates(keep=keep).to_pandas() pd_series = scalars_pandas_df_index[col_name].drop_duplicates(keep=keep) pd.testing.assert_series_equal( pd_series, @@ -980,7 +1253,7 @@ def test_unique(scalars_df_index, scalars_pandas_df_index, col_name): ], ) def test_duplicated(scalars_df_index, scalars_pandas_df_index, keep, col_name): - bf_series = scalars_df_index[col_name].duplicated(keep=keep).compute() + bf_series = scalars_df_index[col_name].duplicated(keep=keep).to_pandas() pd_series = scalars_pandas_df_index[col_name].duplicated(keep=keep) pd.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) @@ -1021,7 +1294,7 @@ def test_empty_false(scalars_dfs): assert pd_result == bf_result -def test_empty_true(scalars_dfs): +def test_empty_true_row_filter(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df["string_col"][ @@ -1031,9 +1304,21 @@ def test_empty_true(scalars_dfs): scalars_pandas_df["string_col"] == "won't find this" ].empty + assert pd_result assert pd_result == bf_result +def test_empty_true_memtable(session: bigframes.Session): + bf_series: series.Series = series.Series(session=session) + pd_series: pd.Series = pd.Series() + + bf_result = bf_series.empty + pd_result = pd_series.empty + + assert pd_result + assert bf_result == pd_result + + def test_dtype(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs @@ -1058,7 +1343,7 @@ def test_head(scalars_dfs): if scalars_df.index.name is None: pytest.skip("Require explicit index for offset ops.") - bf_result = scalars_df["string_col"].head(2).compute() + bf_result = scalars_df["string_col"].head(2).to_pandas() pd_result = scalars_pandas_df["string_col"].head(2) assert_series_equal_ignoring_order( @@ -1073,7 +1358,7 @@ def test_tail(scalars_dfs): if scalars_df.index.name is None: pytest.skip("Require explicit index for offset ops.") - bf_result = scalars_df["string_col"].tail(2).compute() + bf_result = scalars_df["string_col"].tail(2).to_pandas() pd_result = scalars_pandas_df["string_col"].tail(2) assert_series_equal_ignoring_order( @@ -1088,7 +1373,7 @@ def test_head_then_scalar_operation(scalars_dfs): if scalars_df.index.name is None: pytest.skip("Require explicit index for offset ops.") - bf_result = (scalars_df["float64_col"].head(1) + 4).compute() + bf_result = (scalars_df["float64_col"].head(1) + 4).to_pandas() pd_result = scalars_pandas_df["float64_col"].head(1) + 4 pd.testing.assert_series_equal( @@ -1105,7 +1390,7 @@ def test_head_then_series_operation(scalars_dfs): bf_result = ( scalars_df["float64_col"].head(4) + scalars_df["float64_col"].head(2) - ).compute() + ).to_pandas() pd_result = scalars_pandas_df["float64_col"].head(4) + scalars_pandas_df[ "float64_col" ].head(2) @@ -1118,7 +1403,7 @@ def test_head_then_series_operation(scalars_dfs): def test_shift(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" - bf_result = scalars_df_index[col_name].shift().compute() + bf_result = scalars_df_index[col_name].shift().to_pandas() # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].shift().astype(pd.Int64Dtype()) @@ -1133,7 +1418,7 @@ def test_cumsum_int(scalars_df_index, scalars_pandas_df_index): pytest.skip("Series.cumsum NA mask are different in pandas 1.x.") col_name = "int64_col" - bf_result = scalars_df_index[col_name].cumsum().compute() + bf_result = scalars_df_index[col_name].cumsum().to_pandas() # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].cumsum().astype(pd.Int64Dtype()) @@ -1143,6 +1428,27 @@ def test_cumsum_int(scalars_df_index, scalars_pandas_df_index): ) +def test_cumsum_int_ordered(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("1."): + pytest.skip("Series.cumsum NA mask are different in pandas 1.x.") + + col_name = "int64_col" + bf_result = ( + scalars_df_index.sort_values(by="rowindex_2")[col_name].cumsum().to_pandas() + ) + # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA + pd_result = ( + scalars_pandas_df_index.sort_values(by="rowindex_2")[col_name] + .cumsum() + .astype(pd.Int64Dtype()) + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + @pytest.mark.parametrize( ("na_option",), [ @@ -1167,7 +1473,7 @@ def test_cumsum_int(scalars_df_index, scalars_pandas_df_index): def test_rank_with_nulls(scalars_df_index, scalars_pandas_df_index, na_option, method): col_name = "bool_col" bf_result = ( - scalars_df_index[col_name].rank(na_option=na_option, method=method).compute() + scalars_df_index[col_name].rank(na_option=na_option, method=method).to_pandas() ) pd_result = ( scalars_pandas_df_index[col_name] @@ -1191,7 +1497,7 @@ def test_rank_with_nulls(scalars_df_index, scalars_pandas_df_index, na_option, m ) def test_nlargest(scalars_df_index, scalars_pandas_df_index, keep): col_name = "bool_col" - bf_result = scalars_df_index[col_name].nlargest(4, keep=keep).compute() + bf_result = scalars_df_index[col_name].nlargest(4, keep=keep).to_pandas() pd_result = scalars_pandas_df_index[col_name].nlargest(4, keep=keep) pd.testing.assert_series_equal( @@ -1200,6 +1506,29 @@ def test_nlargest(scalars_df_index, scalars_pandas_df_index, keep): ) +@pytest.mark.parametrize( + ("periods",), + [ + (1,), + (2,), + (-1,), + ], +) +def test_diff(scalars_df_index, scalars_pandas_df_index, periods): + bf_result = scalars_df_index["int64_col"].diff(periods=periods).to_pandas() + # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA + pd_result = ( + scalars_pandas_df_index["int64_col"] + .diff(periods=periods) + .astype(pd.Int64Dtype()) + ) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + @pytest.mark.parametrize( ("keep",), [ @@ -1210,7 +1539,7 @@ def test_nlargest(scalars_df_index, scalars_pandas_df_index, keep): ) def test_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): col_name = "bool_col" - bf_result = scalars_df_index[col_name].nsmallest(2, keep=keep).compute() + bf_result = scalars_df_index[col_name].nsmallest(2, keep=keep).to_pandas() pd_result = scalars_pandas_df_index[col_name].nsmallest(2, keep=keep) pd.testing.assert_series_equal( @@ -1221,7 +1550,7 @@ def test_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): def test_rank_ints(scalars_df_index, scalars_pandas_df_index): col_name = "int64_too" - bf_result = scalars_df_index[col_name].rank().compute() + bf_result = scalars_df_index[col_name].rank().to_pandas() pd_result = scalars_pandas_df_index[col_name].rank().astype(pd.Float64Dtype()) pd.testing.assert_series_equal( @@ -1230,9 +1559,33 @@ def test_rank_ints(scalars_df_index, scalars_pandas_df_index): ) +def test_cast_float_to_int(scalars_df_index, scalars_pandas_df_index): + col_name = "float64_col" + bf_result = scalars_df_index[col_name].astype(pd.Int64Dtype()).to_pandas() + # cumsum does not behave well on nullable floats in pandas, produces object type and never ignores NA + pd_result = scalars_pandas_df_index[col_name].astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_cast_float_to_bool(scalars_df_index, scalars_pandas_df_index): + col_name = "float64_col" + bf_result = scalars_df_index[col_name].astype(pd.BooleanDtype()).to_pandas() + # cumsum does not behave well on nullable floats in pandas, produces object type and never ignores NA + pd_result = scalars_pandas_df_index[col_name].astype(pd.BooleanDtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + def test_cumsum_nested(scalars_df_index, scalars_pandas_df_index): col_name = "float64_col" - bf_result = scalars_df_index[col_name].cumsum().cumsum().cumsum().compute() + bf_result = scalars_df_index[col_name].cumsum().cumsum().cumsum().to_pandas() # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA pd_result = ( scalars_pandas_df_index[col_name] @@ -1252,7 +1605,7 @@ def test_cumsum_int_filtered(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" bf_col = scalars_df_index[col_name] - bf_result = bf_col[bf_col > -2].cumsum().compute() + bf_result = bf_col[bf_col > -2].cumsum().to_pandas() pd_col = scalars_pandas_df_index[col_name] # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA @@ -1266,7 +1619,7 @@ def test_cumsum_int_filtered(scalars_df_index, scalars_pandas_df_index): def test_cumsum_float(scalars_df_index, scalars_pandas_df_index): col_name = "float64_col" - bf_result = scalars_df_index[col_name].cumsum().compute() + bf_result = scalars_df_index[col_name].cumsum().to_pandas() # cumsum does not behave well on nullable floats in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].cumsum().astype(pd.Float64Dtype()) @@ -1278,7 +1631,7 @@ def test_cumsum_float(scalars_df_index, scalars_pandas_df_index): def test_cummin_int(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" - bf_result = scalars_df_index[col_name].cummin().compute() + bf_result = scalars_df_index[col_name].cummin().to_pandas() pd_result = scalars_pandas_df_index[col_name].cummin() pd.testing.assert_series_equal( @@ -1289,7 +1642,7 @@ def test_cummin_int(scalars_df_index, scalars_pandas_df_index): def test_cummax_int(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" - bf_result = scalars_df_index[col_name].cummax().compute() + bf_result = scalars_df_index[col_name].cummax().to_pandas() pd_result = scalars_pandas_df_index[col_name].cummax() pd.testing.assert_series_equal( @@ -1302,7 +1655,7 @@ def test_value_counts(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_result = scalars_df[col_name].value_counts().compute() + bf_result = scalars_df[col_name].value_counts().to_pandas() pd_result = scalars_pandas_df[col_name].value_counts() # Older pandas version may not have these values, bigframes tries to emulate 2.0+ @@ -1315,9 +1668,29 @@ def test_value_counts(scalars_dfs): ) +def test_value_counts_w_cut(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + + bf_cut = bigframes.pandas.cut(scalars_df[col_name], 3, labels=False) + pd_cut = pd.cut(scalars_pandas_df[col_name], 3, labels=False) + + bf_result = bf_cut.value_counts().to_pandas() + pd_result = pd_cut.value_counts() + # Older pandas version may not have these values, bigframes tries to emulate 2.0+ + pd_result.name = "count" + pd_result.index.name = col_name + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.Int64Dtype()), + ) + + def test_iloc_nested(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index["string_col"].iloc[1:].iloc[1:].compute() + bf_result = scalars_df_index["string_col"].iloc[1:].iloc[1:].to_pandas() pd_result = scalars_pandas_df_index["string_col"].iloc[1:].iloc[1:] pd.testing.assert_series_equal( @@ -1347,7 +1720,7 @@ def test_iloc_nested(scalars_df_index, scalars_pandas_df_index): ], ) def test_series_iloc(scalars_df_index, scalars_pandas_df_index, start, stop, step): - bf_result = scalars_df_index["string_col"].iloc[start:stop:step].compute() + bf_result = scalars_df_index["string_col"].iloc[start:stop:step].to_pandas() pd_result = scalars_pandas_df_index["string_col"].iloc[start:stop:step] # Pandas may assign non-object dtype to empty series and series index @@ -1362,7 +1735,7 @@ def test_series_iloc(scalars_df_index, scalars_pandas_df_index, start, stop, ste def test_series_add_prefix(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index["int64_too"].add_prefix("prefix_").compute() + bf_result = scalars_df_index["int64_too"].add_prefix("prefix_").to_pandas() pd_result = scalars_pandas_df_index["int64_too"].add_prefix("prefix_") @@ -1375,7 +1748,7 @@ def test_series_add_prefix(scalars_df_index, scalars_pandas_df_index): def test_series_add_suffix(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index["int64_too"].add_suffix("_suffix").compute() + bf_result = scalars_df_index["int64_too"].add_suffix("_suffix").to_pandas() pd_result = scalars_pandas_df_index["int64_too"].add_suffix("_suffix") @@ -1391,7 +1764,7 @@ def test_where_with_series(scalars_df_index, scalars_pandas_df_index): bf_result = ( scalars_df_index["int64_col"] .where(scalars_df_index["bool_col"], scalars_df_index["int64_too"]) - .compute() + .to_pandas() ) pd_result = scalars_pandas_df_index["int64_col"].where( scalars_pandas_df_index["bool_col"], scalars_pandas_df_index["int64_too"] @@ -1411,7 +1784,7 @@ def test_where_with_different_indices(scalars_df_index, scalars_pandas_df_index) scalars_df_index["bool_col"].iloc[2:], scalars_df_index["int64_too"].iloc[:5], ) - .compute() + .to_pandas() ) pd_result = ( scalars_pandas_df_index["int64_col"] @@ -1430,7 +1803,7 @@ def test_where_with_different_indices(scalars_df_index, scalars_pandas_df_index) def test_where_with_default(scalars_df_index, scalars_pandas_df_index): bf_result = ( - scalars_df_index["int64_col"].where(scalars_df_index["bool_col"]).compute() + scalars_df_index["int64_col"].where(scalars_df_index["bool_col"]).to_pandas() ) pd_result = scalars_pandas_df_index["int64_col"].where( scalars_pandas_df_index["bool_col"] @@ -1446,7 +1819,7 @@ def test_clip(scalars_df_index, scalars_pandas_df_index): col_bf = scalars_df_index["int64_col"] lower_bf = scalars_df_index["int64_too"] - 1 upper_bf = scalars_df_index["int64_too"] + 1 - bf_result = col_bf.clip(lower_bf, upper_bf).compute() + bf_result = col_bf.clip(lower_bf, upper_bf).to_pandas() col_pd = scalars_pandas_df_index["int64_col"] lower_pd = scalars_pandas_df_index["int64_too"] - 1 @@ -1463,7 +1836,7 @@ def test_clip_filtered_two_sided(scalars_df_index, scalars_pandas_df_index): col_bf = scalars_df_index["int64_col"].iloc[::2] lower_bf = scalars_df_index["int64_too"].iloc[2:] - 1 upper_bf = scalars_df_index["int64_too"].iloc[:5] + 1 - bf_result = col_bf.clip(lower_bf, upper_bf).compute() + bf_result = col_bf.clip(lower_bf, upper_bf).to_pandas() col_pd = scalars_pandas_df_index["int64_col"].iloc[::2] lower_pd = scalars_pandas_df_index["int64_too"].iloc[2:] - 1 @@ -1479,7 +1852,7 @@ def test_clip_filtered_two_sided(scalars_df_index, scalars_pandas_df_index): def test_clip_filtered_one_sided(scalars_df_index, scalars_pandas_df_index): col_bf = scalars_df_index["int64_col"].iloc[::2] lower_bf = scalars_df_index["int64_too"].iloc[2:] - 1 - bf_result = col_bf.clip(lower_bf, None).compute() + bf_result = col_bf.clip(lower_bf, None).to_pandas() col_pd = scalars_pandas_df_index["int64_col"].iloc[::2] lower_pd = scalars_pandas_df_index["int64_too"].iloc[2:] - 1 @@ -1510,7 +1883,9 @@ def test_dot(scalars_dfs): ], ) def test_between(scalars_df_index, scalars_pandas_df_index, left, right, inclusive): - bf_result = scalars_df_index["int64_col"].between(left, right, inclusive).compute() + bf_result = ( + scalars_df_index["int64_col"].between(left, right, inclusive).to_pandas() + ) pd_result = scalars_pandas_df_index["int64_col"].between(left, right, inclusive) pd.testing.assert_series_equal( @@ -1522,7 +1897,7 @@ def test_between(scalars_df_index, scalars_pandas_df_index, left, right, inclusi def test_to_frame(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df["int64_col"].to_frame().compute() + bf_result = scalars_df["int64_col"].to_frame().to_pandas() pd_result = scalars_pandas_df["int64_col"].to_frame() assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @@ -1651,7 +2026,7 @@ def test_sort_values(scalars_df_index, scalars_pandas_df_index, ascending, na_po bf_result = ( scalars_df_index["int64_col"] .sort_values(ascending=ascending, na_position=na_position) - .compute() + .to_pandas() ) pd_result = scalars_pandas_df_index["int64_col"].sort_values( ascending=ascending, na_position=na_position @@ -1671,7 +2046,9 @@ def test_sort_values(scalars_df_index, scalars_pandas_df_index, ascending, na_po ], ) def test_sort_index(scalars_df_index, scalars_pandas_df_index, ascending): - bf_result = scalars_df_index["int64_too"].sort_index(ascending=ascending).compute() + bf_result = ( + scalars_df_index["int64_too"].sort_index(ascending=ascending).to_pandas() + ) pd_result = scalars_pandas_df_index["int64_too"].sort_index(ascending=ascending) pd.testing.assert_series_equal( @@ -1685,7 +2062,7 @@ def test_mask_default_value(scalars_dfs): bf_col = scalars_df["int64_col"] bf_col_masked = bf_col.mask(bf_col % 2 == 1) - bf_result = bf_col.to_frame().assign(int64_col_masked=bf_col_masked).compute() + bf_result = bf_col.to_frame().assign(int64_col_masked=bf_col_masked).to_pandas() pd_col = scalars_pandas_df["int64_col"] pd_col_masked = pd_col.mask(pd_col % 2 == 1) @@ -1699,7 +2076,7 @@ def test_mask_custom_value(scalars_dfs): bf_col = scalars_df["int64_col"] bf_col_masked = bf_col.mask(bf_col % 2 == 1, -1) - bf_result = bf_col.to_frame().assign(int64_col_masked=bf_col_masked).compute() + bf_result = bf_col.to_frame().assign(int64_col_masked=bf_col_masked).to_pandas() pd_col = scalars_pandas_df["int64_col"] pd_col_masked = pd_col.mask(pd_col % 2 == 1, -1) @@ -1739,14 +2116,14 @@ def test_mask_custom_value(scalars_dfs): ], ) def test_astype(scalars_df_index, scalars_pandas_df_index, column, to_type): - bf_result = scalars_df_index[column].astype(to_type).compute() + bf_result = scalars_df_index[column].astype(to_type).to_pandas() pd_result = scalars_pandas_df_index[column].astype(to_type) pd.testing.assert_series_equal(bf_result, pd_result) @pytest.mark.parametrize( "index", - [0, 5], + [0, 5, -2], ) def test_iloc_single_integer(scalars_df_index, scalars_pandas_df_index, index): bf_result = scalars_df_index.string_col.iloc[index] @@ -1763,7 +2140,7 @@ def test_iloc_single_integer_out_of_bound_error( def test_loc_bool_series_explicit_index(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.string_col.loc[scalars_df_index.bool_col].compute() + bf_result = scalars_df_index.string_col.loc[scalars_df_index.bool_col].to_pandas() pd_result = scalars_pandas_df_index.string_col.loc[scalars_pandas_df_index.bool_col] pd.testing.assert_series_equal( @@ -1777,7 +2154,7 @@ def test_loc_bool_series_default_index( ): bf_result = scalars_df_default_index.string_col.loc[ scalars_df_default_index.bool_col - ].compute() + ].to_pandas() pd_result = scalars_pandas_df_default_index.string_col.loc[ scalars_pandas_df_default_index.bool_col ] @@ -1815,7 +2192,19 @@ def test_rename(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.string_col.rename("newname") pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), + pd_result, + ) + + +def test_rename_dict_same_type(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.string_col.rename({1: 100, 2: 200}) + pd_result = scalars_pandas_df_index.string_col.rename({1: 100, 2: 200}) + + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal( + bf_result.to_pandas(), pd_result, ) @@ -1825,7 +2214,7 @@ def test_rename_axis(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.string_col.rename_axis("newindexname") pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1842,7 +2231,7 @@ def test_loc_list_string_index(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.string_col.loc[index_list] pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1854,7 +2243,7 @@ def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.bool_col.loc[index_list] pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1866,7 +2255,7 @@ def test_iloc_list(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.string_col.iloc[index_list] pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1880,7 +2269,7 @@ def test_iloc_list_nameless(scalars_df_index, scalars_pandas_df_index): pd_result = pd_series.iloc[index_list] pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1895,7 +2284,7 @@ def test_loc_list_nameless(scalars_df_index, scalars_pandas_df_index): pd_result = pd_series.loc[index_list] pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1911,7 +2300,7 @@ def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.date_col.loc[pd_string_series] pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) @@ -1924,11 +2313,46 @@ def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.date_col.loc[pd_index] pd.testing.assert_series_equal( - bf_result.compute(), + bf_result.to_pandas(), pd_result, ) +def test_loc_single_index_with_duplicate(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.set_index("string_col", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index( + "string_col", drop=False + ) + index = "Hello, World!" + bf_result = scalars_df_index.date_col.loc[index] + pd_result = scalars_pandas_df_index.date_col.loc[index] + pd.testing.assert_series_equal( + bf_result.to_pandas(), + pd_result, + ) + + +def test_loc_single_index_no_duplicate(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.set_index("int64_too", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index("int64_too", drop=False) + index = -2345 + bf_result = scalars_df_index.date_col.loc[index] + pd_result = scalars_pandas_df_index.date_col.loc[index] + assert bf_result.to_pandas().iloc[0] == pd_result + + def test_series_bool_interpretation_error(scalars_df_index): with pytest.raises(ValueError): True if scalars_df_index["string_col"] else False + + +def test_query_job_setters(scalars_dfs): + job_ids = set() + df, _ = scalars_dfs + series = df["int64_col"] + assert series.query_job is not None + repr(series) + job_ids.add(series.query_job.job_id) + series.to_pandas() + job_ids.add(series.query_job.job_id) + assert len(job_ids) == 2 diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 31ab4dee121..599b8aabbc9 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -27,6 +27,8 @@ import bigframes.dtypes import bigframes.ml.linear_model +FIRST_FILE = "000000000000" + def test_read_gbq_tokyo( session_tokyo: bigframes.Session, @@ -35,7 +37,7 @@ def test_read_gbq_tokyo( tokyo_location: str, ): df = session_tokyo.read_gbq(scalars_table_tokyo, index_col=["rowindex"]) - result = df.sort_index().compute() + result = df.sort_index().to_pandas() expected = scalars_pandas_df_index _, query_job = df._block.expr.start_query() @@ -86,7 +88,11 @@ def test_read_gbq_w_col_order( @pytest.mark.parametrize( ("query_or_table", "index_col"), [ - pytest.param("{scalars_table_id}", ["bool_col", "int64_col"], id="multiindex"), + pytest.param( + "{scalars_table_id}", + ["bool_col", "int64_col"], + id="unique_multiindex_table", + ), pytest.param( """SELECT t.float64_col * 2 AS my_floats, @@ -97,11 +103,43 @@ def test_read_gbq_w_col_order( ["my_strings"], id="string_index", ), + pytest.param( + "SELECT GENERATE_UUID() AS uuid, 0 AS my_value FROM UNNEST(GENERATE_ARRAY(1, 20))", + ["uuid"], + id="unique_uuid_index_query", + ), pytest.param( "{scalars_table_id}", ["bool_col"], id="non_unique_index", ), + pytest.param( + "{scalars_table_id}", + ["float64_col"], + id="non_unique_float_index", + ), + pytest.param( + "{scalars_table_id}", + [ + "timestamp_col", + "float64_col", + "datetime_col", + "int64_too", + ], + id="multi_part_index_direct", + ), + pytest.param( + "SELECT * FROM {scalars_table_id}", + [ + "timestamp_col", + "float64_col", + "string_col", + "bool_col", + "int64_col", + "int64_too", + ], + id="multi_part_index_w_query", + ), ], ) def test_read_gbq_w_index_col( @@ -116,6 +154,11 @@ def test_read_gbq_w_index_col( ) assert list(df.index.names) == index_col + # Verify that we get the expected number of results. + bf_shape = df.shape + result = df.to_pandas() + assert bf_shape == result.shape + @pytest.mark.parametrize( ("query_or_table", "max_results"), @@ -152,7 +195,7 @@ def test_read_gbq_w_max_results( query_or_table.format(scalars_table_id=scalars_table_id), max_results=max_results, ) - bf_result = df.compute() + bf_result = df.to_pandas() assert bf_result.shape[0] == max_results @@ -181,7 +224,7 @@ def test_read_pandas(session, scalars_dfs): df = session.read_pandas(scalars_pandas_df) assert df._block._expr._ordering is not None - result = df.compute() + result = df.to_pandas() expected = scalars_pandas_df pd.testing.assert_frame_equal(result, expected) @@ -189,7 +232,7 @@ def test_read_pandas(session, scalars_dfs): def test_read_pandas_multi_index(session, scalars_pandas_df_multi_index): df = session.read_pandas(scalars_pandas_df_multi_index) - result = df.compute() + result = df.to_pandas() pd.testing.assert_frame_equal(result, scalars_pandas_df_multi_index) @@ -199,7 +242,8 @@ def test_read_pandas_rowid_exists_adds_suffix(session, scalars_pandas_df_default ) df = session.read_pandas(scalars_pandas_df_default_index) - assert df._block._expr._ordering.ordering_id == "rowid_2" + total_order_col = df._block._expr._ordering.total_order_col + assert total_order_col and total_order_col.column_id == "rowid_2" def test_read_pandas_tokyo( @@ -208,7 +252,7 @@ def test_read_pandas_tokyo( tokyo_location: str, ): df = session_tokyo.read_pandas(scalars_pandas_df_index) - result = df.compute() + result = df.to_pandas() expected = scalars_pandas_df_index _, query_job = df._block.expr.start_query() @@ -220,14 +264,15 @@ def test_read_pandas_tokyo( def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs if scalars_df.index.name is not None: - path = gcs_folder + "test_read_csv_gcs_default_engine_w_index.csv" + path = gcs_folder + "test_read_csv_gcs_default_engine_w_index*.csv" else: - path = gcs_folder + "test_read_csv_gcs_default_engine_wo_index.csv" + path = gcs_folder + "test_read_csv_gcs_default_engine_wo_index*.csv" + read_path = path.replace("*", FIRST_FILE) scalars_df.to_csv(path, index=False) dtype = scalars_df.dtypes.to_dict() dtype.pop("geography_col") df = session.read_csv( - path, + read_path, # Convert default pandas dtypes to match BigQuery DataFrames dtypes. dtype=dtype, ) @@ -247,9 +292,9 @@ def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): def test_read_csv_gcs_bq_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs if scalars_df.index.name is not None: - path = gcs_folder + "test_read_csv_gcs_bq_engine_w_index.csv" + path = gcs_folder + "test_read_csv_gcs_bq_engine_w_index*.csv" else: - path = gcs_folder + "test_read_csv_gcs_bq_engine_wo_index.csv" + path = gcs_folder + "test_read_csv_gcs_bq_engine_wo_index*.csv" scalars_df.to_csv(path, index=False) df = session.read_csv(path, engine="bigquery") @@ -419,22 +464,25 @@ def test_read_csv_default_engine_throws_not_implemented_error( match, ): path = ( - gcs_folder + "test_read_csv_gcs_default_engine_throws_not_implemented_error.csv" + gcs_folder + + "test_read_csv_gcs_default_engine_throws_not_implemented_error*.csv" ) + read_path = path.replace("*", FIRST_FILE) scalars_df_index.to_csv(path) with pytest.raises(NotImplementedError, match=match): - session.read_csv(path, **kwargs) + session.read_csv(read_path, **kwargs) def test_read_csv_gcs_default_engine_w_header(session, scalars_df_index, gcs_folder): - path = gcs_folder + "test_read_csv_gcs_default_engine_w_header.csv" + path = gcs_folder + "test_read_csv_gcs_default_engine_w_header*.csv" + read_path = path.replace("*", FIRST_FILE) scalars_df_index.to_csv(path) # Skips header=N rows, normally considers the N+1th row as the header, but overridden by # passing the `names` argument. In this case, pandas will skip the N+1th row too, take # the column names from `names`, and begin reading data from the N+2th row. df = session.read_csv( - path, + read_path, header=2, names=scalars_df_index.columns.to_list(), ) @@ -443,7 +491,7 @@ def test_read_csv_gcs_default_engine_w_header(session, scalars_df_index, gcs_fol def test_read_csv_gcs_bq_engine_w_header(session, scalars_df_index, gcs_folder): - path = gcs_folder + "test_read_csv_gcs_bq_engine_w_header.csv" + path = gcs_folder + "test_read_csv_gcs_bq_engine_w_header*.csv" scalars_df_index.to_csv(path, index=False) # Skip the header and the first 2 data rows. Without provided schema, the column names @@ -487,10 +535,11 @@ def test_read_csv_local_bq_engine_w_header(session, scalars_pandas_df_index): def test_read_csv_gcs_default_engine_w_index_col_name( session, scalars_df_default_index, gcs_folder ): - path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_name.csv" + path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_name*.csv" + read_path = path.replace("*", FIRST_FILE) scalars_df_default_index.to_csv(path) - df = session.read_csv(path, index_col="rowindex") + df = session.read_csv(read_path, index_col="rowindex") scalars_df_default_index = scalars_df_default_index.set_index( "rowindex" ).sort_index() @@ -501,11 +550,12 @@ def test_read_csv_gcs_default_engine_w_index_col_name( def test_read_csv_gcs_default_engine_w_index_col_index( session, scalars_df_default_index, gcs_folder ): - path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_index.csv" + path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_index*.csv" + read_path = path.replace("*", FIRST_FILE) scalars_df_default_index.to_csv(path) index_col = scalars_df_default_index.columns.to_list().index("rowindex") - df = session.read_csv(path, index_col=index_col) + df = session.read_csv(read_path, index_col=index_col) scalars_df_default_index = scalars_df_default_index.set_index( "rowindex" ).sort_index() @@ -559,11 +609,12 @@ def test_read_csv_local_default_engine_w_index_col_index( ) def test_read_csv_gcs_w_usecols(session, scalars_df_index, gcs_folder, engine): path = gcs_folder + "test_read_csv_gcs_w_usecols" - path = path + "_default_engine.csv" if engine is None else path + "_bq_engine.csv" + path = path + "_default_engine*.csv" if engine is None else path + "_bq_engine*.csv" + read_path = path.replace("*", FIRST_FILE) if engine is None else path scalars_df_index.to_csv(path) # df should only have 1 column which is bool_col. - df = session.read_csv(path, usecols=["bool_col"], engine=engine) + df = session.read_csv(read_path, usecols=["bool_col"], engine=engine) assert len(df.columns) == 1 diff --git a/tests/system/small/test_window.py b/tests/system/small/test_window.py index 34b56fcd730..e2f0fe999b1 100644 --- a/tests/system/small/test_window.py +++ b/tests/system/small/test_window.py @@ -19,42 +19,31 @@ @pytest.mark.parametrize( ("windowing"), [ - (lambda x: x.expanding()), - (lambda x: x.rolling(3, min_periods=3)), - (lambda x: x.groupby(x % 2).rolling(3, min_periods=3)), - (lambda x: x.groupby(x % 3).expanding(min_periods=2)), - ], - ids=[ - "expanding", - "rolling", - "rollinggroupby", - "expandinggroupby", + pytest.param(lambda x: x.expanding(), id="expanding"), + pytest.param(lambda x: x.rolling(3, min_periods=3), id="rolling"), + pytest.param( + lambda x: x.groupby(x % 2).rolling(3, min_periods=3), id="rollinggroupby" + ), + pytest.param( + lambda x: x.groupby(x % 3).expanding(min_periods=2), id="expandinggroupby" + ), ], ) @pytest.mark.parametrize( ("agg_op"), [ - (lambda x: x.sum()), - (lambda x: x.min()), - (lambda x: x.max()), - (lambda x: x.mean()), - (lambda x: x.count()), - (lambda x: x.std()), - (lambda x: x.var()), - ], - ids=[ - "sum", - "min", - "max", - "mean", - "count", - "std", - "var", + pytest.param(lambda x: x.sum(), id="sum"), + pytest.param(lambda x: x.min(), id="min"), + pytest.param(lambda x: x.max(), id="max"), + pytest.param(lambda x: x.mean(), id="mean"), + pytest.param(lambda x: x.count(), id="count"), + pytest.param(lambda x: x.std(), id="std"), + pytest.param(lambda x: x.var(), id="var"), ], ) def test_window_agg_ops(scalars_df_index, scalars_pandas_df_index, windowing, agg_op): col_name = "int64_too" - bf_series = agg_op(windowing(scalars_df_index[col_name])).compute() + bf_series = agg_op(windowing(scalars_df_index[col_name])).to_pandas() pd_series = agg_op(windowing(scalars_pandas_df_index[col_name])) # Pandas always converts to float64, even for min/max/count, which is not desired diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 92cb52a6815..6f5c96da49d 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -45,8 +45,10 @@ def test_standard_scaler_produces_correct_sql(): def test_one_hot_encoder_produces_correct_sql(): - sql = ml_sql.ml_one_hot_encoder("col_a", "encoded_col_a") - assert sql == "ML.ONE_HOT_ENCODER(col_a) OVER() AS encoded_col_a" + sql = ml_sql.ml_one_hot_encoder("col_a", "none", 1000000, 0, "encoded_col_a") + assert ( + sql == "ML.ONE_HOT_ENCODER(col_a, 'none', 1000000, 0) OVER() AS encoded_col_a" + ) def test_create_model_produces_correct_sql(): @@ -57,7 +59,7 @@ def test_create_model_produces_correct_sql(): ) assert ( sql - == """CREATE MODEL `my_dataset.my_model` + == """CREATE TEMP MODEL `my_dataset.my_model` my_options_sql AS my_source_sql""" ) @@ -65,14 +67,14 @@ def test_create_model_produces_correct_sql(): def test_create_model_transform_produces_correct_sql(): sql = ml_sql.create_model( - model_name="my_dataset.my_model", + model_name="my_model", source_sql="my_source_sql", options_sql="my_options_sql", transform_sql="my_transform_sql", ) assert ( sql - == """CREATE MODEL `my_dataset.my_model` + == """CREATE TEMP MODEL `my_model` my_transform_sql my_options_sql AS my_source_sql""" @@ -81,18 +83,30 @@ def test_create_model_transform_produces_correct_sql(): def test_create_remote_model_produces_correct_sql(): sql = ml_sql.create_remote_model( - model_name="my_dataset.my_model", + model_name="my_model", connection_name="my_project.us.my_connection", options_sql="my_options_sql", ) assert ( sql - == """CREATE MODEL `my_dataset.my_model` + == """CREATE TEMP MODEL `my_model` REMOTE WITH CONNECTION `my_project.us.my_connection` my_options_sql""" ) +def test_create_imported_model_produces_correct_sql(): + sql = ml_sql.create_imported_model( + model_name="my_model", + options_sql="my_options_sql", + ) + assert ( + sql + == """CREATE TEMP MODEL `my_model` +my_options_sql""" + ) + + def test_alter_model_correct_sql(): sql = ml_sql.alter_model( model_name="my_dataset.my_model", diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 25286189637..123dae79395 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -12,80 +12,36 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest import mock - import ibis -from ibis.expr.types import Column, Table +from ibis.expr.types import Table from bigframes import core +ORDERING = core.ExpressionOrdering( + [ + core.OrderingColumnReference("int64_col"), + core.OrderingColumnReference("string_col"), + ], + total_ordering_columns=frozenset(["int64_col", "string_col"]), +) + def test_constructor_from_ibis_table_adds_all_columns( session, scalars_ibis_table: Table ): - actual = core.ArrayValue(session=session, table=scalars_ibis_table) + columns = tuple(scalars_ibis_table[key] for key in scalars_ibis_table.columns) + actual = core.ArrayValue( + session=session, table=scalars_ibis_table, columns=columns, ordering=ORDERING + ) assert actual._table is scalars_ibis_table assert len(actual._columns) == len(scalars_ibis_table.columns) -def test_builder_doesnt_change_original(session): - mock_table = mock.create_autospec(Table) - mock_column = mock.create_autospec(Column) - original = core.ArrayValue(session=session, table=mock_table, columns=[mock_column]) - assert original._table is mock_table - assert len(original._columns) == 1 - assert original._columns[0] is mock_column - - # Create a new expression from a builder. - builder = original.builder() - new_table = mock.create_autospec(Table) - assert new_table is not mock_table - builder.table = new_table - new_column = mock.create_autospec(Column) - assert new_column is not mock_column - builder.columns.append(new_column) - actual = builder.build() - - # Expected values are present. - assert actual._table is new_table - assert len(actual._columns) == 2 - assert actual._columns[0] is mock_column - assert actual._columns[1] is new_column - # Don't modify the original. - assert original._table is mock_table - assert len(original._columns) == 1 - assert original._columns[0] is mock_column - - -def test_projection_doesnt_change_original(session): - mock_table = mock.create_autospec(Table) - mock_column = mock.create_autospec(Column) - original = core.ArrayValue(session=session, table=mock_table, columns=[mock_column]) - assert original._table is mock_table - assert len(original._columns) == 1 - assert original._columns[0] is mock_column - - # Create a new expression from a projection. - new_column_1 = mock.create_autospec(Column) - new_column_2 = mock.create_autospec(Column) - assert new_column_1 is not mock_column - assert new_column_2 is not mock_column - actual = original.projection([new_column_1, mock_column, new_column_2]) - - # Expected values are present. - assert actual._table is mock_table - assert len(actual._columns) == 3 - assert actual._columns[0] is new_column_1 - assert actual._columns[1] is mock_column - assert actual._columns[2] is new_column_2 - # Don't modify the original. - assert original._table is mock_table - assert len(original._columns) == 1 - assert original._columns[0] is mock_column - - def test_to_ibis_expr_with_projection(session, scalars_ibis_table: Table): - expr = core.ArrayValue(session=session, table=scalars_ibis_table).projection( + columns = tuple(scalars_ibis_table[key] for key in scalars_ibis_table.columns) + expr = core.ArrayValue( + session=session, table=scalars_ibis_table, columns=columns, ordering=ORDERING + ).projection( [ scalars_ibis_table["int64_col"], ibis.literal(123456789).name("literals"), diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index ec49315f0fe..dafed08980c 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -175,10 +175,9 @@ def test_literal_to_ibis_scalar_throws_on_incompatible_literal(): def test_remote_function_io_types_are_supported_bigframes_types(): - from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type + from bigframes.remote_function import ( + _supported_io_ibis_types as rf_supported_io_ibis_types, + ) - from bigframes.remote_function import _supported_io_types as rf_supported_io_types - - for python_type in rf_supported_io_types: - ibis_type = python_type_to_bigquery_type(python_type) + for ibis_type in rf_supported_io_ibis_types: assert ibis_type in bigframes.dtypes.IBIS_TO_BIGFRAMES diff --git a/tests/unit/test_formatting_helper.py b/tests/unit/test_formatting_helper.py new file mode 100644 index 00000000000..ea29869e824 --- /dev/null +++ b/tests/unit/test_formatting_helper.py @@ -0,0 +1,17 @@ +import pytest + +import bigframes.formatting_helpers as formatter + + +@pytest.mark.parametrize( + "test_input, expected", [(None, "N/A"), ("string", "N/A"), (100000, "100.0 kB")] +) +def test_get_formatted_bytes(test_input, expected): + assert formatter.get_formatted_bytes(test_input) == expected + + +@pytest.mark.parametrize( + "test_input, expected", [(None, None), ("string", "string"), (100000, "a minute")] +) +def test_get_formatted_time(test_input, expected): + assert formatter.get_formatted_time(test_input) == expected diff --git a/tests/unit/test_formatting_helpers.py b/tests/unit/test_formatting_helpers.py new file mode 100644 index 00000000000..9db9b372e21 --- /dev/null +++ b/tests/unit/test_formatting_helpers.py @@ -0,0 +1,46 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest.mock as mock + +import google.api_core.exceptions as api_core_exceptions +import google.cloud.bigquery as bigquery +import pytest + +import bigframes.constants as constants +import bigframes.formatting_helpers as formatting_helpers + + +def test_wait_for_query_job_error_includes_feedback_link(): + mock_query_job = mock.create_autospec(bigquery.QueryJob) + mock_query_job.result.side_effect = api_core_exceptions.BadRequest( + "Test message 123." + ) + + with pytest.raises(api_core_exceptions.BadRequest) as cap_exc: + formatting_helpers.wait_for_query_job(mock_query_job) + + cap_exc.match("Test message 123.") + cap_exc.match(constants.FEEDBACK_LINK) + + +def test_wait_for_job_error_includes_feedback_link(): + mock_job = mock.create_autospec(bigquery.LoadJob) + mock_job.result.side_effect = api_core_exceptions.BadRequest("Test message 123.") + + with pytest.raises(api_core_exceptions.BadRequest) as cap_exc: + formatting_helpers.wait_for_job(mock_job) + + cap_exc.match("Test message 123.") + cap_exc.match(constants.FEEDBACK_LINK) diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 3e8e16ee6ab..a178a454388 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -15,10 +15,12 @@ import inspect import re import sys +import unittest.mock as mock +import pandas as pd import pytest -import bigframes.pandas +import bigframes.pandas as bpd import bigframes.session leading_whitespace = re.compile(r"^\s+", flags=re.MULTILINE) @@ -79,3 +81,31 @@ def test_method_matches_session(method_name: str): 1: ] assert pandas_signature.return_annotation == session_signature.return_annotation + + +def test_cut_raises_with_labels(): + with pytest.raises(NotImplementedError, match="Only labels=False"): + mock_series = mock.create_autospec(bigframes.pandas.Series, instance=True) + bigframes.pandas.cut(mock_series, 4, labels=["a", "b", "c", "d"]) + + +@pytest.mark.parametrize( + ("bins",), + ( + (0,), + (-1,), + ), +) +def test_cut_raises_with_invalid_bins(bins: int): + with pytest.raises(ValueError, match="`bins` should be a positive integer."): + mock_series = mock.create_autospec(bigframes.pandas.Series, instance=True) + bigframes.pandas.cut(mock_series, bins, labels=False) + + +def test_pandas_attribute(): + assert bpd.NA is pd.NA + assert bpd.BooleanDtype is pd.BooleanDtype + assert bpd.Float64Dtype is pd.Float64Dtype + assert bpd.Int64Dtype is pd.Int64Dtype + assert bpd.StringDtype is pd.StringDtype + assert bpd.ArrowDtype is pd.ArrowDtype diff --git a/third_party/bigframes_vendored/ibis/LICENSE.txt b/third_party/bigframes_vendored/ibis/LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/third_party/bigframes_vendored/ibis/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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/third_party/bigframes_vendored/ibis/README.md b/third_party/bigframes_vendored/ibis/README.md new file mode 100644 index 00000000000..8a00750e920 --- /dev/null +++ b/third_party/bigframes_vendored/ibis/README.md @@ -0,0 +1,196 @@ +# Ibis + +[![Documentation Status](https://img.shields.io/badge/docs-docs.ibis--project.org-blue.svg)](http://ibis-project.org) +[![Anaconda-Server Badge](https://anaconda.org/conda-forge/ibis-framework/badges/version.svg)](https://anaconda.org/conda-forge/ibis-framework) +[![PyPI](https://img.shields.io/pypi/v/ibis-framework.svg)](https://pypi.org/project/ibis-framework) +[![Build status](https://github.com/ibis-project/ibis/actions/workflows/ibis-main.yml/badge.svg)](https://github.com/ibis-project/ibis/actions/workflows/ibis-main.yml?query=branch%3Amaster) +[![Build status](https://github.com/ibis-project/ibis/actions/workflows/ibis-backends.yml/badge.svg)](https://github.com/ibis-project/ibis/actions/workflows/ibis-backends.yml?query=branch%3Amaster) +[![Codecov branch](https://img.shields.io/codecov/c/github/ibis-project/ibis/master.svg)](https://codecov.io/gh/ibis-project/ibis) + +## What is Ibis? + +Ibis is a Python library that provides a lightweight, universal interface for data wrangling. It helps Python users explore and transform data of any size, stored anywhere. + +Ibis has three primary components: + +1. **A dataframe API for Python**. + Python users can write Ibis code to manipulate tabular data. +2. **Interfaces to 15+ query engines.** + Wherever data is stored, people can use Ibis as their API of choice to communicate with any of those query engines. +3. **Deferred execution**. + Ibis uses deferred execution, so execution of code is pushed to the query engine. + Users can execute at the speed of their backend, not their local computer. + +## Why Use Ibis? + +Ibis aims to be a future-proof solution to interacting with data using Python and can accomplish this goal through its main features: + +- **Familiar API**: Ibis’s API design borrows from popular APIs like pandas and dplyr that most users already know and like to use. +- **Consistent syntax**: Ibis aims to be a universal Python API for tabular data of any size, big or small. +- **Deferred execution**: Ibis pushes code execution to the query engine and only moves required data into memory when necessary. + Analytics workflows are faster and more efficient +- **Interactive mode**: Ibis provides an interactive mode in which users can quickly diagnose problems, explore data, and mock up workflows and pipelines locally. +- **10+ supported backends**: Ibis supports multiple query engines and DataFrame APIs. + Use one interface to transform with your data wherever it lives: from DataFrames in pandas to Parquet files through DuckDB to tables in BigQuery. +- **Minimize rewrites**: Teams can often keep their Ibis code the same regardless of backend changes, like increasing or decreasing computing power, changing the number or size of their databases, or switching backends entirely. +- **Flexibility when you need it**: When Ibis doesn't support something, it provides a way to jump directly into SQL. + +## Common Use Cases + +- **Speed up prototype to production.** + Scale code written and tested locally to a distributed system or cloud SQL engine with minimal rewrites. +- **Boost performance of existing Python or pandas code.** + For example a general rule of thumb for pandas is "Have 5 to 10 times as much RAM as the size of your dataset". + When a dataset exceeds this rule using in-memory frameworks like pandas can be slow. + Instead, using Ibis will significantly speed up your workflows because of its deferred execution. + Ibis also empowers you to switch to a faster database engine, without changing much of your code. +- **Get rid of long, error-prone, `f`-strings.** + Ibis provides one syntax for multiple query engines and dataframe APIs that lets you avoid learning new flavors of SQL or other framework-specific code. + Learn the syntax once and use that syntax anywhere. + +## Backends + +Ibis acts as a universal frontend to the following systems: + +- [Apache Arrow DataFusion](https://ibis-project.org/backends/datafusion/) (experimental) +- [Apache Druid](https://ibis-project.org/backends/druid/) (experimental) +- [Apache Impala](https://ibis-project.org/backends/impala/) +- [Apache PySpark](https://ibis-project.org/backends/pyspark/) +- [BigQuery](https://ibis-project.org/backends/bigquery/) +- [ClickHouse](https://ibis-project.org/backends/clickhouse/) +- [Dask](https://ibis-project.org/backends/dask/) +- [DuckDB](https://ibis-project.org/backends/duckdb/) +- [HeavyAI](https://github.com/heavyai/ibis-heavyai) +- [MySQL](https://ibis-project.org/backends/mysql/) +- [Oracle](https://ibis-project.org/backends/oracle/) (experimental) +- [Pandas](https://ibis-project.org/backends/pandas/) +- [Polars](https://ibis-project.org/backends/polars/) (experimental) +- [PostgreSQL](https://ibis-project.org/backends/postgresql/) +- [SQL Server](https://ibis-project.org/backends/mssql/) +- [SQLite](https://ibis-project.org/backends/sqlite/) +- [Snowflake](https://ibis-project.org/backends/snowflake) (experimental) +- [Trino](https://ibis-project.org/backends/trino/) (experimental) + +The list of supported backends is continuously growing. Anyone can get involved +in adding new ones! Learn more about contributing to ibis in our contributing +documentation at https://github.com/ibis-project/ibis/blob/master/docs/CONTRIBUTING.md + +## Installation + +Install Ibis from PyPI with: + +```bash +pip install 'ibis-framework[duckdb]' +``` + +Or from conda-forge with: + +```bash +conda install ibis-framework -c conda-forge +``` + +(It’s a common mistake to `pip install ibis`. If you try to use Ibis and get errors early on try uninstalling `ibis` and installing `ibis-framework`) + +To discover ibis, we suggest starting with the DuckDB backend (which is included by default in the conda-forge package). The DuckDB backend is performant and fully featured. + +To use ibis with other backends, include the backend name in brackets for PyPI: + +```bash +pip install 'ibis-framework[postgres]' +``` + +Or use `ibis-$BACKEND` where `$BACKEND` is the specific backend you want to use when installing from conda-forge: + +```bash +conda install ibis-postgres -c conda-forge +``` + +## Getting Started with Ibis + +We provide a number of tutorial and example notebooks in the +[ibis-examples](https://github.com/ibis-project/ibis-examples). The easiest way +to try these out is through the online interactive notebook environment +provided here: +[![Binder](https://static.mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/ibis-project/ibis-examples/main) + +You can also get started analyzing any dataset, anywhere with just a few lines +of Ibis code. Here’s an example of how to use Ibis with a SQLite database. + +Download the SQLite database from the `ibis-tutorial-data` GCS (Google Cloud +Storage) bucket, then connect to it using ibis. + +```bash +curl -LsS -o geography.db 'https://storage.googleapis.com/ibis-tutorial-data/geography.db' +``` + +Connect to the database and show the available tables + +```python +>>> import ibis +>>> from ibis import _ +>>> ibis.options.interactive = True +>>> con = ibis.sqlite.connect("geography.db") +>>> con.tables +Tables +------ +- countries +- gdp +- independence +``` + +Choose the `countries` table and preview its first few rows + +```python +>>> countries = con.tables.countries +>>> countries.head() +┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┓ +┃ iso_alpha2 ┃ iso_alpha3 ┃ iso_numeric ┃ fips ┃ name ┃ capital ┃ area_km2 ┃ population ┃ continent ┃ +┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━┩ +│ string │ string │ int32 │ string │ string │ string │ float64 │ int32 │ string │ +├────────────┼────────────┼─────────────┼────────┼──────────────────────┼──────────────────┼──────────┼────────────┼───────────┤ +│ AD │ AND │ 20 │ AN │ Andorra │ Andorra la Vella │ 468.0 │ 84000 │ EU │ +│ AE │ ARE │ 784 │ AE │ United Arab Emirates │ Abu Dhabi │ 82880.0 │ 4975593 │ AS │ +│ AF │ AFG │ 4 │ AF │ Afghanistan │ Kabul │ 647500.0 │ 29121286 │ AS │ +│ AG │ ATG │ 28 │ AC │ Antigua and Barbuda │ St. Johns │ 443.0 │ 86754 │ NA │ +│ AI │ AIA │ 660 │ AV │ Anguilla │ The Valley │ 102.0 │ 13254 │ NA │ +└────────────┴────────────┴─────────────┴────────┴──────────────────────┴──────────────────┴──────────┴────────────┴───────────┘ +``` + +Show the 5 least populous countries in Asia + +```python + +>>> ( +... countries.filter(_.continent == "AS") +... .select("name", "population") +... .order_by(_.population) +... .limit(5) +... ) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ +┃ name ┃ population ┃ +┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩ +│ string │ int32 │ +├────────────────────────────────┼────────────┤ +│ Cocos [Keeling] Islands │ 628 │ +│ British Indian Ocean Territory │ 4000 │ +│ Brunei │ 395027 │ +│ Maldives │ 395650 │ +│ Macao │ 449198 │ +└────────────────────────────────┴────────────┘ +``` + +## Community and Contributing + +Ibis is an open source project and welcomes contributions from anyone in the community. + +- Read [the contributing guide](https://github.com/ibis-project/ibis/blob/master/docs/CONTRIBUTING.md). +- We care about keeping the community welcoming for all. Check out [the code of conduct](https://github.com/ibis-project/ibis/blob/master/docs/CODE_OF_CONDUCT.md). +- The Ibis project is open sourced under the [Apache License](https://github.com/ibis-project/ibis/blob/master/LICENSE.txt). + +Join our community here: + +- Twitter: https://twitter.com/IbisData +- Gitter: https://gitter.im/ibis-dev/Lobby +- StackOverflow: https://stackoverflow.com/questions/tagged/ibis + +For more information visit https://ibis-project.org/. diff --git a/third_party/bigframes_vendored/ibis/__init__.py b/third_party/bigframes_vendored/ibis/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/ibis/backends/__init__.py b/third_party/bigframes_vendored/ibis/backends/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py b/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py new file mode 100644 index 00000000000..d209284ab7d --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -0,0 +1,19 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/registry.py +"""Module to convert from Ibis expression to SQL string.""" + +from ibis.backends.bigquery.registry import OPERATION_REGISTRY + +import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops + + +def _approx_quantiles(translator, op: vendored_ibis_ops.ApproximateMultiQuantile): + arg = translator.translate(op.arg) + num_bins = translator.translate(op.num_bins) + return f"APPROX_QUANTILES({arg}, {num_bins})" + + +patched_ops = { + vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, +} + +OPERATION_REGISTRY.update(patched_ops) diff --git a/third_party/bigframes_vendored/ibis/expr/__init__.py b/third_party/bigframes_vendored/ibis/expr/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py new file mode 100644 index 00000000000..f3ab753a3b1 --- /dev/null +++ b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py @@ -0,0 +1,4 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/__init__.py +from __future__ import annotations + +from third_party.bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F403 diff --git a/third_party/bigframes_vendored/ibis/expr/operations/reductions.py b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py new file mode 100644 index 00000000000..5e6ad9ecf2b --- /dev/null +++ b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py @@ -0,0 +1,23 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/reductions.py + +from __future__ import annotations + +import ibis.expr.datatypes as dt +from ibis.expr.operations.reductions import Filterable, Reduction +import ibis.expr.rules as rlz + + +class ApproximateMultiQuantile(Filterable, Reduction): + """Calculate (approximately) evenly-spaced quantiles. + + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/approximate_aggregate_functions#approx_quantiles + """ + + arg = rlz.any + num_bins = rlz.value(dt.int64) + output_dtype = dt.Array(dt.float64) + + +__all__ = [ + "ApproximateMultiQuantile", +] diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index 10f5546f108..198654015e0 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -16,10 +16,44 @@ Encapsulates configuration for displaying objects. Attributes: - max_columns (int): - If max_columns is exceeded, switch to truncate view. - max_rows (int): - If max_rows is exceeded, switch to truncate view. - progress_bar Optional(str): - Determines if progress bars are shown during job runs. Use auto | notebook | terminal. Set to 'None' to remove progress bars + max_columns (int, default 20): + If `max_columns` is exceeded, switch to truncate view. + max_rows (int, default 25): + If `max_rows` is exceeded, switch to truncate view. + progress_bar (Optional(str), default "auto"): + Determines if progress bars are shown during job runs. + Valid values are `auto`, `notebook`, and `terminal`. Set + to `None` to remove progress bars. + repr_mode (Literal[`head`, `deferred`]): + `head`: + Execute, download, and display results (limited to head) from + dataframe and series objects during repr. + `deferred`: + Prevent executions from repr statements in dataframe and series objects. + Instead estimated bytes processed will be shown. Dataframe and Series + objects can still be computed with methods that explicitly execute and + download results. +""" + +sampling_options_doc = """ +Encapsulates configuration for data sampling. + +Attributes: + max_download_size (int, default 500): + Download size threshold in MB. If value set to None, the download size + won't be checked. + enable_downsampling (bool, default False): + Whether to enable downsampling, If max_download_size is exceeded when + downloading data (e.g., to_pandas()), the data will be downsampled + if enable_downsampling is True, otherwise, an error will be raised. + sampling_method (str, default "uniform"): + Downsampling algorithms to be chosen from, the choices are: + "head": This algorithm returns a portion of the data from + the beginning. It is fast and requires minimal computations + to perform the downsampling.; "uniform": This algorithm returns + uniform random samples of the data. + random_state (int, default None): + The seed for the uniform downsampling algorithm. If provided, + the uniform method may take longer to execute and require more + computation. """ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 93b7c7683cf..653b65c8346 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -13,6 +13,8 @@ from typing import Iterable, Literal, Mapping, Optional, Sequence, Union +import numpy + from third_party.bigframes_vendored.pandas.core.generic import NDFrame # ----------------------------------------------------------------------- @@ -33,8 +35,61 @@ def shape(self) -> tuple[int, int]: """Return a tuple representing the dimensionality of the DataFrame.""" raise NotImplementedError("abstract method") + @property + def axes(self) -> list: + """ + Return a list representing the axes of the DataFrame. + + It has the row axis labels and column axis labels as the only members. + They are returned in that order. + + Examples + .. code-block:: + + df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + df.axes + [RangeIndex(start=0, stop=2, step=1), Index(['col1', 'col2'], + dtype='object')] + """ + return [self.index, self.columns] + + @property + def values(self) -> numpy.ndarray: + """Return the values of DataFrame in the form of a NumPy array. + + Args: + dytype (default None): + The dtype to pass to `numpy.asarray()`. + copy (bool, default False): + Whether to ensure that the returned value is not a view + on another array. + na_value (default None): + The value to use for missing values. + """ + raise NotImplementedError("abstract method") + # ---------------------------------------------------------------------- # IO methods (to / from other formats) + def to_numpy( + self, dtype=None, copy=False, na_value=None, **kwargs + ) -> numpy.ndarray: + """ + Convert the DataFrame to a NumPy array. + + Args: + dtype (None): + The dtype to pass to `numpy.asarray()`. + copy (bool, default None): + Whether to ensure that the returned value is not a view + on another array. + na_value (Any, default None): + The value to use for missing values. The default value + depends on dtype and the dtypes of the DataFrame columns. + + Returns: + numpy.ndarray: The converted NumPy array. + """ + raise NotImplementedError("abstract method") def to_gbq( self, @@ -42,15 +97,16 @@ def to_gbq( *, if_exists: Optional[Literal["fail", "replace", "append"]] = "fail", index: bool = True, + ordering_id: Optional[str] = None, ) -> None: - """Write a DataFrame to a Google BigQuery table. + """Write a DataFrame to a BigQuery table. Args: - destination_table : str + destination_table (str): Name of table to be written, in the form ``dataset.tablename`` or ``project.dataset.tablename``. - if_exists : str, default 'fail' + if_exists (str, default 'fail'): Behavior when the destination table exists. Value can be one of: ``'fail'`` @@ -60,7 +116,12 @@ def to_gbq( ``'append'`` If table exists, insert data. Create if does not exist. - index: whether write row names (index) or not. + index (bool. default True): + whether write row names (index) or not. + + ordering_id (Optional[str], default None): + If set, write the ordering of the DataFrame as a column in the + result table with this name. """ raise NotImplementedError("abstract method") @@ -70,19 +131,19 @@ def to_parquet( *, index: bool = True, ) -> None: - """Write a DataFrame to the binary parquet format. + """Write a DataFrame to the binary Parquet format. This function writes the dataframe as a `parquet file - `_ to Google Cloud Storage. + `_ to Cloud Storage. Args: - path: - Destination URI(s) of GCS files(s) to store the extracted dataframe + path (str): + Destination URI(s) of Cloud Storage files(s) to store the extracted dataframe in format of ``gs:///``. If the data size is more than 1GB, you must use a wildcard to export the data into multiple files and the size of the files varies. - index : bool, default None + index (bool, default True): If ``True``, include the dataframe's index(es) in the file output. If ``False``, they will not be written to the file. @@ -101,6 +162,12 @@ def assign(self, **kwargs) -> DataFrame: Returns a new object with all original columns in addition to new ones. Existing columns that are re-assigned will be overwritten. + .. note:: + Assigning multiple columns within the same ``assign`` is possible. + Later items in '\*\*kwargs' may refer to newly created or modified + columns in 'df'; items are computed and assigned into 'df' in + order. + Args: kwargs: A dictionary of ``{str: values}``. The column names are @@ -108,14 +175,8 @@ def assign(self, **kwargs) -> DataFrame: are simply assigned to the column. Returns: - A new DataFrame with the new columns in addition to + DataFrame: A new DataFrame with the new columns in addition to all the existing columns. - - Notes: - Assigning multiple columns within the same ``assign`` is possible. - Later items in '\*\*kwargs' may refer to newly created or modified - columns in 'df'; items are computed and assigned into 'df' in - order. """ raise NotImplementedError("abstract method") @@ -123,24 +184,31 @@ def assign(self, **kwargs) -> DataFrame: # Reindexing and alignment def drop( - self, - *, - columns: Union[str, Iterable[str]], + self, labels=None, *, axis=0, index=None, columns=None, level=None ) -> DataFrame | None: """Drop specified labels from columns. Remove columns by directly specifying column names. Args: + labels: + Index or column labels to drop. + axis: + Whether to drop labels from the index (0 or 'index') or + columns (1 or 'columns'). + index: + Alternative to specifying axis (``labels, axis=0`` + is equivalent to ``index=labels``). columns: - Single label or list-like. - + Alternative to specifying axis (``labels, axis=1`` + is equivalent to ``columns=labels``). + level: + For MultiIndex, level from which the labels will be removed. Returns: - DataFrame without the removed column labels. + DataFrame: DataFrame without the removed column labels. Raises: - KeyError: - If any of the labels is not found in the selected axis. + KeyError: If any of the labels is not found in the selected axis. """ raise NotImplementedError("abstract method") @@ -155,15 +223,14 @@ def rename( will be left as-is. Extra labels listed don't throw an error. Args: - columns: + columns (Mapping): Dict-like from old column labels to new column labels. Returns: - DataFrame with the renamed axis labels. + DataFrame: DataFrame with the renamed axis labels. Raises: - KeyError: - If any of the labels is not found. + KeyError: If any of the labels is not found. """ raise NotImplementedError("abstract method") @@ -171,14 +238,16 @@ def rename_axis(self, mapper: Optional[str], **kwargs) -> DataFrame: """ Set the name of the axis for the index. - Note: currently only accepts a single string parameter (the new name of the index) + .. Note:: + + Currently only accepts a single string parameter (the new name of the index). Args: - mapper : str + mapper str: Value to set the axis name attribute. Returns: - DataFrame with the new index name + DataFrame: DataFrame with the new index name """ raise NotImplementedError("abstract method") @@ -197,12 +266,11 @@ def set_index( Args: keys: A label. This parameter can be a single column key. - drop: + drop : Delete columns to be used as the new index. Returns: - DataFrame: - Changed row labels. + DataFrame: Changed row labels. """ raise NotImplementedError("abstract method") @@ -211,12 +279,12 @@ def reorder_levels(self, order: Sequence[int | str]) -> DataFrame: Rearrange index levels using input order. May not drop or duplicate levels. Args: - order: list of int or list of str + order (list of int or list of str): List representing new level order. Reference level by number (position) or by key (label). Returns: - DataFrame + DataFrame: DataFrame of rearranged index. """ raise NotImplementedError("abstract method") @@ -225,12 +293,12 @@ def droplevel(self, level): Return DataFrame with requested index / column level(s) removed. Args: - level: int, str, or list-like + level (int, str, or list-like): If a string is given, must be the name of a level If list-like, elements must be names or positional indexes of levels. Returns: - DataFrame with requested index / column level(s) removed. + DataFrame: DataFrame with requested index / column level(s) removed. """ raise NotImplementedError("abstract method") @@ -244,12 +312,12 @@ def reset_index( Reset the index of the DataFrame, and use the default one instead. Args: - drop: + drop (bool, default False): Do not try to insert index into dataframe columns. This resets the index to the default integer index. Returns: - DataFrame with the new index. + DataFrame: DataFrame with the new index. """ raise NotImplementedError("abstract method") @@ -265,10 +333,10 @@ def drop_duplicates( are ignored. Args: - subset : column label or sequence of labels, optional + subset (column label or sequence of labels, optional): Only consider certain columns for identifying duplicates, by default use all of the columns. - keep : {'first', 'last', ``False``}, default 'first' + keep ({'first', 'last', ``False``}, default 'first'): Determines which duplicates (if any) to keep. - 'first' : Drop duplicates except for the first occurrence. @@ -276,7 +344,7 @@ def drop_duplicates( - ``False`` : Drop all duplicates. Returns: - DataFrame with duplicates removed + DataFrame: DataFrame with duplicates removed """ raise NotImplementedError("abstract method") @@ -287,10 +355,10 @@ def duplicated(self, subset=None, keep="first"): Considering certain columns is optional. Args: - subset : column label or sequence of labels, optional + subset (column label or sequence of labels, optional): Only consider certain columns for identifying duplicates, by default use all of the columns. - keep : {'first', 'last', False}, default 'first' + keep ({'first', 'last', False}, default 'first'): Determines which duplicates (if any) to mark. - ``first`` : Mark duplicates as ``True`` except for the first occurrence. @@ -311,7 +379,7 @@ def dropna( """Remove missing values. Returns: - DataFrame with NA entries dropped from it. + DataFrame: DataFrame with NA entries dropped from it. """ raise NotImplementedError("abstract method") @@ -323,18 +391,23 @@ def sort_values( by: str | Sequence[str], *, ascending: bool | Sequence[bool] = True, + kind: str = "quicksort", na_position="last", ) -> DataFrame: """Sort by the values along row axis. Args: - by: + by (str or Sequence[str]): Name or list of names to sort by. - ascending: + ascending (bool or Sequence[bool], default True): Sort ascending vs. descending. Specify list for multiple sort orders. If this is a list of bools, must match the length of the by. - na_position: + kind (str, default `quicksort`): + Choice of sorting algorithm. Accepts 'quicksort’, ‘mergesort’, + ‘heapsort’, ‘stable’. Ignored except when determining whether to + sort stably. 'mergesort' or 'stable' will result in stable reorder. + na_position ({'first', 'last'}, default `last`): ``{'first', 'last'}``, default 'last' Puts NaNs at the beginning if `first`; `last` puts NaNs at the end. @@ -356,8 +429,9 @@ def sort_index( # ---------------------------------------------------------------------- # Arithmetic Methods - def le(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'less than or equal to' of dataframe and other, element-wise (binary operator `<=`). + def eq(self, other, axis: str | int = "columns") -> DataFrame: + """ + Get equal to of DataFrame and other, element-wise (binary operator `eq`). Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison operators. @@ -366,24 +440,20 @@ def le(self, other, axis: str | int = "columns") -> DataFrame: (rows or columns) and level for comparison. Args: - other: scalar, sequence, Series, or DataFrame + other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. - axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + axis ({0 or 'index', 1 or 'columns'}, default 'columns'): Whether to compare by the index (0 or 'index') or columns (1 or 'columns'). Returns: - DataFrame of bool. The result of the comparison. - - Notes: - Mismatched indices will be unioned together. `NaN` values in - floating point columns are considered different - (i.e. `NaN` != `NaN`). + Result of the comparison. """ raise NotImplementedError("abstract method") - def lt(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'less than' of dataframe and other, element-wise (binary operator `<`). + def ne(self, other, axis: str | int = "columns") -> DataFrame: + """ + Get not equal to of DataFrame and other, element-wise (binary operator `ne`). Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison operators. @@ -392,24 +462,44 @@ def lt(self, other, axis: str | int = "columns") -> DataFrame: (rows or columns) and level for comparison. Args: - other: scalar, sequence, Series, or DataFrame + other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. - axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + axis ({0 or 'index', 1 or 'columns'}, default 'columns'): Whether to compare by the index (0 or 'index') or columns (1 or 'columns'). - Returns: - DataFrame of bool. The result of the comparison. + DataFrame: Result of the comparison. + """ + raise NotImplementedError("abstract method") - Notes: + def le(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'less than or equal to' of dataframe and other, element-wise (binary operator `<=`). + + Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison + operators. + + Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis + (rows or columns) and level for comparison. + + .. note:: Mismatched indices will be unioned together. `NaN` values in floating point columns are considered different (i.e. `NaN` != `NaN`). + + Args: + other (scalar, sequence, Series, or DataFrame): + Any single or multiple element data structure, or list-like object. + axis ({0 or 'index', 1 or 'columns'}, default 'columns'): + Whether to compare by the index (0 or 'index') or columns + (1 or 'columns'). + + Returns: + DataFrame: DataFrame of bool. The result of the comparison. """ raise NotImplementedError("abstract method") - def ge(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'greater than or equal to' of dataframe and other, element-wise (binary operator `>=`). + def lt(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'less than' of DataFrame and other, element-wise (binary operator `<`). Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison operators. @@ -417,25 +507,51 @@ def ge(self, other, axis: str | int = "columns") -> DataFrame: Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis (rows or columns) and level for comparison. + .. note:: + Mismatched indices will be unioned together. `NaN` values in + floating point columns are considered different + (i.e. `NaN` != `NaN`). + Args: - other: scalar, sequence, Series, or DataFrame + other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. - axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + axis ({0 or 'index', 1 or 'columns'}, default 'columns'): Whether to compare by the index (0 or 'index') or columns (1 or 'columns'). Returns: - DataFrame of bool. The result of the comparison. + DataFrame: DataFrame of bool. The result of the comparison. + """ + raise NotImplementedError("abstract method") - Notes: + def ge(self, other, axis: str | int = "columns") -> DataFrame: + """Get 'greater than or equal to' of DataFrame and other, element-wise (binary operator `>=`). + + Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison + operators. + + Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis + (rows or columns) and level for comparison. + + .. note:: Mismatched indices will be unioned together. `NaN` values in floating point columns are considered different (i.e. `NaN` != `NaN`). + + Args: + other (scalar, sequence, Series, or DataFrame): + Any single or multiple element data structure, or list-like object. + axis ({0 or 'index', 1 or 'columns'}, default 'columns'): + Whether to compare by the index (0 or 'index') or columns + (1 or 'columns'). + + Returns: + DataFrame: DataFrame of bool. The result of the comparison. """ raise NotImplementedError("abstract method") def gt(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'greater than' of dataframe and other, element-wise (binary operator `>`). + """Get 'greater than' of DataFrame and other, element-wise (binary operator `>`). Among flexible wrappers (`eq`, `ne`, `le`, `lt`, `ge`, `gt`) to comparison operators. @@ -443,260 +559,250 @@ def gt(self, other, axis: str | int = "columns") -> DataFrame: Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis (rows or columns) and level for comparison. + .. note:: + Mismatched indices will be unioned together. `NaN` values in + floating point columns are considered different + (i.e. `NaN` != `NaN`). + Args: - other: scalar, sequence, Series, or DataFrame + other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. - axis: {{0 or 'index', 1 or 'columns'}}, default 'columns' + axis ({0 or 'index', 1 or 'columns'}, default 'columns'): Whether to compare by the index (0 or 'index') or columns (1 or 'columns'). Returns: - DataFrame of bool. The result of the comparison. - - Notes: - Mismatched indices will be unioned together. `NaN` values in - floating point columns are considered different - (i.e. `NaN` != `NaN`). + DataFrame: DataFrame of bool: The result of the comparison. """ raise NotImplementedError("abstract method") def add(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'addition' of dataframe and other, element-wise (binary operator `+`). + """Get addition of DataFrame and other, element-wise (binary operator `+`). Equivalent to ``dataframe + other``. With reverse version, `radd`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def sub(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'subtraction' of dataframe and other, element-wise (binary operator `-`). + """Get subtraction of DataFrame and other, element-wise (binary operator `-`). Equivalent to ``dataframe - other``. With reverse version, `rsub`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def rsub(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'subtraction' of dataframe and other, element-wise (binary operator `-`). + """Get subtraction of DataFrame and other, element-wise (binary operator `-`). Equivalent to ``other - dataframe``. With reverse version, `sub`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def mul(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'multiplication' of dataframe and other, element-wise (binary operator `*`). + """Get multiplication of DataFrame and other, element-wise (binary operator `*`). Equivalent to ``dataframe * other``. With reverse version, `rmul`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def truediv(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'floating division' of dataframe and other, element-wise (binary operator `/`). + """Get floating division of DataFrame and other, element-wise (binary operator `/`). Equivalent to ``dataframe / other``. With reverse version, `rtruediv`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def rtruediv(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'floating division' of dataframe and other, element-wise (binary operator `/`). + """Get floating division of DataFrame and other, element-wise (binary operator `/`). Equivalent to ``other / dataframe``. With reverse version, `truediv`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. """ raise NotImplementedError("abstract method") def floordiv(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'integer division' of dataframe and other, element-wise (binary operator `//`). + """Get integer division of DataFrame and other, element-wise (binary operator `//`). Equivalent to ``dataframe // other``. With reverse version, `rfloordiv`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def rfloordiv(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'integer division' of dataframe and other, element-wise (binary operator `//`). + """Get integer division of DataFrame and other, element-wise (binary operator `//`). Equivalent to ``other // dataframe``. With reverse version, `rfloordiv`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def mod(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'modulo' of dataframe and other, element-wise (binary operator `%`). + """Get modulo of DataFrame and other, element-wise (binary operator `%`). Equivalent to ``dataframe % other``. With reverse version, `rmod`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: other: Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") def rmod(self, other, axis: str | int = "columns") -> DataFrame: - """Get 'modulo' of dataframe and other, element-wise (binary operator `%`). + """Get modulo of DataFrame and other, element-wise (binary operator `%`). Equivalent to ``other % dataframe``. With reverse version, `mod`. Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + .. note:: + Mismatched indices will be unioned together. + Args: - other: + other (float, int, or Series): Any single or multiple element data structure, or list-like object. - axis: - ``{{0 or 'index', 1 or 'columns'}}``. Whether to compare by the - index (0 or 'index') or columns. (1 or 'columns'). For Series - input, axis to match Series index on. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. - - Notes: - Mismatched indices will be unioned together. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError("abstract method") @@ -719,20 +825,20 @@ def groupby( groups. Args: - by: + by (str, Sequence[str]): A label or list of labels may be passed to group by the columns in ``self``. Notice that a tuple is interpreted as a (single) key. - level : int, level name, or sequence of such, default None + level (int, level name, or sequence of such, default None): If the axis is a MultiIndex (hierarchical), group by a particular level or levels. Do not specify both ``by`` and ``level``. - as_index: + as_index (bool, default True): Default True. Return object with group labels as the index. Only relevant for DataFrame input. ``as_index=False`` is effectively "SQL-style" grouped output. This argument has no effect on filtrations such as ``head()``, ``tail()``, ``nth()`` and in transformations. - dropna: + dropna (bool, default True): Default True. If True, and if group keys contain NA values, NA values together with row/column will be dropped. If False, NA values will also be treated as the key in groups. @@ -748,31 +854,31 @@ def groupby( def map(self, func, na_action: Optional[str] = None) -> DataFrame: """Apply a function to a Dataframe elementwise. + This method applies a function that accepts and returns a scalar + to every element of a DataFrame. + .. note:: In pandas 2.1.0, DataFrame.applymap is deprecated and renamed to DataFrame.map. - This method applies a function that accepts and returns a scalar - to every element of a DataFrame. - Args: func: Python function wrapped by ``remote_function`` decorator, returns a single value from a single value. - na_action: + na_action (Optional[str], default None): ``{None, 'ignore'}``, default None. If ‘ignore’, propagate NaN values, without passing them to func. Returns: - Transformed DataFrame. + DataFrame: Transformed DataFrame. """ raise NotImplementedError("abstract method") # ---------------------------------------------------------------------- # Merging / joining methods - def join(self, other, *, how: str) -> DataFrame: + def join(self, other, *, on: Optional[str] = None, how: str) -> DataFrame: """Join columns of another DataFrame. Join columns with `other` DataFrame on index @@ -780,22 +886,20 @@ def join(self, other, *, how: str) -> DataFrame: Args: other: DataFrame with an Index similar to the Index of this one. - how: - ``{'left', 'right', 'outer', 'inner'}, default 'left'`` - + on: + Column in the caller to join on the index in other, otherwise + joins index-on-index. Like an Excel VLOOKUP operation. + how ({'left', 'right', 'outer', 'inner'}, default 'left'`): How to handle the operation of the two objects. - - * left: use calling frame's index (or column if on is specified) - * right: use `other`'s index. - * outer: form union of calling frame's index (or column if on is - specified) with `other`'s index, and sort it. - lexicographically. - * inner: form intersection of calling frame's index (or column if - on is specified) with `other`'s index, preserving the order - of the calling's one. + ``left``: use calling frame's index (or column if on is specified) + ``right``: use `other`'s index. ``outer``: form union of calling + frame's index (or column if on is specified) with `other`'s index, + and sort it lexicographically. ``inner``: form intersection of + calling frame's index (or column if on is specified) with `other`'s + index, preserving the order of the calling's one. Returns: - A dataframe containing columns from both the caller and `other`. + DataFrame: A dataframe containing columns from both the caller and `other`. """ raise NotImplementedError("abstract method") @@ -824,7 +928,6 @@ def merge( allowed. .. warning:: - If both key columns contain rows where the key is a null value, those rows will be matched against each other. This is different from usual SQL join behaviour and can lead to unexpected results. @@ -835,15 +938,14 @@ def merge( how: ``{'left', 'right', 'outer', 'inner'}, default 'inner'`` Type of merge to be performed. - - * left: use only keys from left frame, similar to a SQL left outer join; - preserve key order. - * right: use only keys from right frame, similar to a SQL right outer join; - preserve key order. - * outer: use union of keys from both frames, similar to a SQL full outer - join; sort keys lexicographically. - * inner: use intersection of keys from both frames, similar to a SQL inner - join; preserve the order of the left keys. + ``left``: use only keys from left frame, similar to a SQL left outer join; + preserve key order. + ``right``: use only keys from right frame, similar to a SQL right outer join; + preserve key order. + ``outer``: use union of keys from both frames, similar to a SQL full outer + join; sort keys lexicographically. + ``inner``: use intersection of keys from both frames, similar to a SQL inner + join; preserve the order of the left keys. on: Column join on. It must be found in both DataFrames. Either on or left_on + right_on @@ -867,7 +969,7 @@ def merge( no suffix. At least one of the values must not be None. Returns: - A DataFrame of the two merged objects. + DataFrame: A DataFrame of the two merged objects. """ raise NotImplementedError("abstract method") @@ -883,7 +985,7 @@ def any(self, *, bool_only: bool = False): non-empty). Args: - bool_only: + bool_only (bool. default False): Include only boolean columns. Returns: @@ -895,12 +997,12 @@ def all(self, *, bool_only: bool = False): """ Return whether all elements are True, potentially over an axis. - Returns True unless there at least one element within a series or - along a Dataframe axis that is False or equivalent (e.g. zero or + Returns True unless there at least one element within a Series or + along a DataFrame axis that is False or equivalent (e.g. zero or empty). Args: - bool_only: + bool_only (bool. default False): Include only boolean columns. Returns: @@ -913,7 +1015,7 @@ def prod(self, *, numeric_only: bool = False): Return the product of the values over the requested axis. Args: - numeric_only: + numeric_only (bool. default False): Include only float, int, boolean columns. Returns: @@ -928,7 +1030,7 @@ def min(self, *, numeric_only: bool = False): equivalent of the ``numpy.ndarray`` method ``argmin``. Args: - numeric_only: + numeric_only (bool, default False): Default False. Include only float, int, boolean columns. Returns: @@ -943,7 +1045,7 @@ def max(self, *, numeric_only: bool = False): the equivalent of the ``numpy.ndarray`` method ``argmax``. Args: - numeric_only: + numeric_only (bool. default False): Default False. Include only float, int, boolean columns. Returns: @@ -957,7 +1059,7 @@ def sum(self, *, numeric_only: bool = False): This is equivalent to the method ``numpy.sum``. Args: - numeric_only: + numeric_only (bool. default False): Default False. Include only float, int, boolean columns. Returns: @@ -969,7 +1071,7 @@ def mean(self, *, numeric_only: bool = False): """Return the mean of the values over the requested axis. Args: - numeric_only: + numeric_only (bool. default False): Default False. Include only float, int, boolean columns. Returns: @@ -977,13 +1079,28 @@ def mean(self, *, numeric_only: bool = False): """ raise NotImplementedError("abstract method") + def median(self, *, numeric_only: bool = False, exact: bool = False): + """Return the median of the values over the requested axis. + + Args: + numeric_only (bool. default False): + Default False. Include only float, int, boolean columns. + exact (bool. default False): + Default False. Get the exact median instead of an approximate + one. Note: ``exact=True`` not yet supported. + + Returns: + Series + """ + raise NotImplementedError("abstract method") + def var(self, *, numeric_only: bool = False): """Return unbiased variance over requested axis. Normalized by N-1 by default. Args: - numeric_only: + numeric_only (bool. default False): Default False. Include only float, int, boolean columns. Returns: @@ -997,7 +1114,7 @@ def std(self, *, numeric_only: bool = False): Normalized by N-1 by default. Args: - numeric_only: + numeric_only (bool. default False): Default False. Include only float, int, boolean columns. Returns: @@ -1013,7 +1130,7 @@ def count(self, *, numeric_only: bool = False): on `pandas.options.mode.use_inf_as_na`) are considered NA. Args: - numeric_only : bool, default False + numeric_only (bool, default False): Include only `float`, `int` or `boolean` data. Returns: @@ -1039,7 +1156,7 @@ def cummin(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative minimum. Returns: - Return cumulative minimum of DataFrame. + DataFrame: Return cumulative minimum of DataFrame. """ raise NotImplementedError("abstract method") @@ -1049,7 +1166,7 @@ def cummax(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative maximum. Returns: - Return cumulative maximum of DataFrame. + DataFrame: Return cumulative maximum of DataFrame. """ raise NotImplementedError("abstract method") @@ -1059,7 +1176,7 @@ def cumsum(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative sum. Returns: - Return cumulative sum of DataFrame. + DataFrame: Return cumulative sum of DataFrame. """ raise NotImplementedError("abstract method") @@ -1069,7 +1186,49 @@ def cumprod(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative product. Returns: - Return cumulative product of DataFrame. + DataFrame: Return cumulative product of DataFrame. + """ + raise NotImplementedError("abstract method") + + def agg(self, func): + """ + Aggregate using one or more operations over the specified axis. + + Args: + func (function): + Function to use for aggregating the data. + Accepted combinations are: string function name, list of + function names, e.g. ``['sum', 'mean']``. + + Returns: + Series or DataFrame: Aggregated results + """ + raise NotImplementedError("abstract method") + + def describe(self): + """ + Generate descriptive statistics. + + Descriptive statistics include those that summarize the central + tendency, dispersion and shape of a + dataset's distribution, excluding ``NaN`` values. + + Only supports numeric columns. + + .. note:: + Percentile values are approximates only. + + Returns: + Summary statistics of the Series or Dataframe provided. + + + Notes + ----- + For numeric data, the result's index will include ``count``, + ``mean``, ``std``, ``min``, ``max`` as well as lower, ``50`` and + upper percentiles. By default the lower percentile is ``25`` and the + upper percentile is ``75``. The ``50`` percentile is the + same as the median. """ raise NotImplementedError("abstract method") @@ -1107,18 +1266,18 @@ def value_counts( Return a Series containing counts of unique rows in the DataFrame. Args: - subset : label or list of labels, optional + subset (label or list of labels, optional): Columns to use when counting unique combinations. - normalize : bool, default False + normalize (bool, default False): Return proportions rather than frequencies. - sort : bool, default True + sort (bool, default True): Sort by frequencies. - ascending : bool, default False + ascending (bool, default False): Sort in ascending order. - dropna : bool, default True + dropna (bool, default True): Don’t include counts of rows that contain NA values. Returns: - Series + Series: Series containing counts of unique rows in the DataFrame """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index b6ee19b46ac..4843c971da2 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -19,7 +19,8 @@ class NDFrame(indexing.IndexingMixin): def ndim(self) -> int: """Return an int representing the number of axes / array dimensions. - Return 1 if Series. Otherwise return 2 if DataFrame. + Returns: + int: Return 1 if Series. Otherwise return 2 if DataFrame. """ raise NotImplementedError("abstract method") @@ -27,8 +28,9 @@ def ndim(self) -> int: def size(self) -> int: """Return an int representing the number of elements in this object. - Return the number of rows if Series. Otherwise return the number of - rows times number of columns if DataFrame. + Returns: + int: Return the number of rows if Series. Otherwise return the number of + rows times number of columns if DataFrame. """ raise NotImplementedError("abstract method") @@ -42,6 +44,7 @@ def abs(self): Returns: Series/DataFrame containing the absolute value of each element. + Returns a Series/DataFrame containing the absolute value of each element. """ raise NotImplementedError("abstract method") @@ -49,18 +52,18 @@ def astype(self, dtype): """ Cast a pandas object to a specified dtype ``dtype``. - Parameters - ---------- - dtype : str, data type, Series or Mapping of column name -> data type - Use a str, numpy.dtype, pandas.ExtensionDtype or Python type to - cast entire pandas object to the same type. Alternatively, use a - mapping, e.g. {col: dtype, ...}, where col is a column label and dtype is - a numpy.dtype or Python type to cast one or more of the DataFrame's - columns to column-specific types. + Args: + dtype (str or pandas.ExtensionDtype): + A dtype supported by BigQuery DataFrame include 'boolean','Float64','Int64', + 'string', 'tring[pyarrow]','timestamp[us, tz=UTC][pyarrow]', + 'timestamp[us][pyarrow]','date32[day][pyarrow]','time64[us][pyarrow]' + A pandas.ExtensionDtype include pandas.BooleanDtype(), pandas.Float64Dtype(), + pandas.Int64Dtype(), pandas.StringDtype(storage="pyarrow"), + pd.ArrowDtype(pa.date32()), pd.ArrowDtype(pa.time64("us")), + pd.ArrowDtype(pa.timestamp("us")), pd.ArrowDtype(pa.timestamp("us", tz="UTC")). - Returns - ------- - same type as caller + Returns: + same type as caller """ raise NotImplementedError("abstract method") @@ -70,17 +73,17 @@ def astype(self, dtype): @property def empty(self) -> bool: - """Indicator whether Series/DataFrame is empty. + """Indicates whether Series/DataFrame is empty. True if Series/DataFrame is entirely empty (no items), meaning any of the axes are of length 0. - Returns: - If Series/DataFrame is empty, return True, if not return False. - - Note: + .. note:: If Series/DataFrame contains only NA values, it is still not considered empty. + + Returns: + bool: If Series/DataFrame is empty, return True, if not return False. """ raise NotImplementedError("abstract method") @@ -97,28 +100,28 @@ def to_json( index: bool = True, lines: bool = False, ) -> str | None: - """Convert the object to a JSON string, written to GCS. + """Convert the object to a JSON string, written to Cloud Storage. Note NaN's and None will be converted to null and datetime objects will be converted to UNIX timestamps. + .. note:: + Only ``orient='records'`` and ``lines=True`` is supported so far. + Args: - path_or_buf: - A destination URI of GCS files(s) to store the extracted dataframe - in format of ``gs:///``. + path_or_buf (str): + A destination URI of Cloud Storage files(s) to store the extracted + dataframe in format of ``gs:///``. + Must contain a wildcard `*` character. If the data size is more than 1GB, you must use a wildcard to export the data into multiple files and the size of the files varies. None, file-like objects or local file paths not yet supported. - orient: + orient ({`split`, `records`, `index`, `columns`, `values`, `table`}, default 'columns): Indication of expected JSON string format. - .. note:: - - In BigQuery DataFrame, only `orient='records'` is supported so far. - * Series: - default is 'index' @@ -141,30 +144,25 @@ def to_json( - 'table' : dict like {{'schema': {{schema}}, 'data': {{data}}}} Describing the data, where data component is like ``orient='records'``. + index (bool, default True): + If True, write row names (index). - lines: + lines (bool, default False): If 'orient' is 'records' write out line-delimited json format. Will throw ValueError if incorrect 'orient' since others are not list-like. - .. note:: - - BigQuery DataFrames only supports ``lines=True`` so far. - - index: - If True, write row names (index). - Returns: - None. String output not yet supported. + None: String output not yet supported. """ raise NotImplementedError("abstract method") def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: - """Write object to a comma-separated values (csv) file on GCS. + """Write object to a comma-separated values (csv) file on Cloud Storage. Args: - path_or_buf: - A destination URI of GCS files(s) to store the extracted dataframe + path_or_buf (str): + A destination URI of Cloud Storage files(s) to store the extracted dataframe in format of ``gs:///``. If the data size is more than 1GB, you must use a wildcard to @@ -173,11 +171,11 @@ def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: None, file-like objects or local file paths not yet supported. - index: + index (bool, default True): If True, write row names (index). Returns: - None. String output not yet supported. + None: String output not yet supported. """ raise NotImplementedError("abstract method") @@ -208,11 +206,11 @@ def add_prefix(self, prefix: str, axis: int | str | None = None): For DataFrame, the column labels are prefixed. Args: - prefix: + prefix (str): The string to add before each label. - axis: + axis (int or str or None, default None): ``{{0 or 'index', 1 or 'columns', None}}``, default None. Axis - to add prefix on + to add prefix on. Returns: New Series or DataFrame with updated labels. @@ -250,7 +248,7 @@ def head(self, n: int = 5): If n is larger than the number of rows, this function returns all rows. Args: - n: + n (int, default 5): Default 5. Number of rows to select. Returns: @@ -271,7 +269,8 @@ def tail(self, n: int = 5): If n is larger than the number of rows, this function returns all rows. Args: - n: int, default 5. Number of rows to select. + n (int, default 5): + Number of rows to select. Returns: The last `n` rows of the caller object. @@ -290,12 +289,12 @@ def sample( You can use `random_state` for reproducibility. Args: - n: + n (Optional[int], default None): Number of items from axis to return. Cannot be used with `frac`. Default = 1 if `frac` = None. - frac: + frac (Optional[float], default None): Fraction of axis items to return. Cannot be used with `n`. - random_state: + random_state (Optional[int], default None): Seed for random number generator. Returns: @@ -360,7 +359,7 @@ def notna(self) -> NDFrame: NA values get mapped to False values. Returns: - Mask of bool values for each element that indicates whether an + NDFrame: Mask of bool values for each element that indicates whether an element is not an NA value. """ raise NotImplementedError("abstract method") @@ -376,11 +375,11 @@ def shift( Shifts the index without realigning the data. Args: - periods: + periods int: Number of periods to shift. Can be positive or negative. Returns: - Copy of input object, shifted. + NDFrame: Copy of input object, shifted. """ raise NotImplementedError("abstract method") @@ -398,34 +397,27 @@ def rank( By default, equal values are assigned a rank that is the average of the ranks of those values. - Parameters - ---------- - method : {'average', 'min', 'max', 'first', 'dense'}, default 'average' - How to rank the group of records that have the same value (i.e. ties): - - * average: average rank of the group - * min: lowest rank in the group - * max: highest rank in the group - * first: ranks assigned in order they appear in the array - * dense: like 'min', but rank always increases by 1 between groups. - - numeric_only : bool, default False - For DataFrame objects, rank only numeric columns if set to True. + Args: + method ({'average', 'min', 'max', 'first', 'dense'}, default 'average'): + How to rank the group of records that have the same value (i.e. ties): + `average`: average rank of the group, `min`: lowest rank in the group + max`: highest rank in the group, `first`: ranks assigned in order they + appear in the array, `dense`: like 'min', but rank always increases by + 1 between groups. - na_option : {'keep', 'top', 'bottom'}, default 'keep' - How to rank NaN values: + numeric_only (bool, default False): + For DataFrame objects, rank only numeric columns if set to True. - * keep: assign NaN rank to NaN values - * top: assign lowest rank to NaN values - * bottom: assign highest rank to NaN values + na_option ({'keep', 'top', 'bottom'}, default 'keep'): + How to rank NaN values: `keep`: assign NaN rank to NaN values, + , `top`: assign lowest rank to NaN values, `bottom`: assign highest + rank to NaN values. - ascending : bool, default True - Whether or not the elements should be ranked in ascending order. + ascending (bool, default True): + Whether or not the elements should be ranked in ascending order. - Returns - ------- - same type as caller - Return a Series or DataFrame with data ranks as values. + Returns: + same type as caller: Return a Series or DataFrame with data ranks as values. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 5b5a9f206b3..6975e6edf2b 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -17,26 +17,23 @@ class GroupBy: def any(self): """ - Return True if any value in the group is truthful, else False. + Return True if any value in the group is true, else False. - - Returns - ------- - Series or DataFrame - DataFrame or Series of boolean values, where a value is True if any element - is True within its respective group, False otherwise. + Returns: + Series or DataFrame: DataFrame or Series of boolean values, + where a value is True if any element is True within its + respective group, False otherwise. """ raise NotImplementedError("abstract property") def all(self): """ - Return True if all values in the group are truthful, else False. + Return True if all values in the group are true, else False. - Returns - ------- - Series or DataFrame - DataFrame or Series of boolean values, where a value is True if all elements - are True within its respective group, False otherwise. + Returns: + Series or DataFrame: DataFrame or Series of boolean values, + where a value is True if all elements are True within its + respective group, False otherwise. """ raise NotImplementedError("abstract property") @@ -44,10 +41,8 @@ def count(self): """ Compute count of group, excluding missing values. - Returns - ------- - Series or DataFrame - Count of values within each group. + Returns: + Series or DataFrame: Count of values within each group. """ raise NotImplementedError("abstract property") @@ -58,14 +53,33 @@ def mean( """ Compute mean of groups, excluding missing values. - Parameters - ---------- - numeric_only : bool, default False - Include only float, int, boolean columns. + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. - Returns - ------- - pandas.Series or pandas.DataFrame + Returns: + pandas.Series or pandas.DataFrame: Mean of groups. + """ + raise NotImplementedError("abstract property") + + def median( + self, + numeric_only: bool = False, + *, + exact: bool = False, + ): + """ + Compute median of groups, excluding missing values. + + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. + exact (bool, default False): + Calculate the exact median instead of an approximation. Note: + ``exact=True`` not yet supported. + + Returns: + pandas.Series or pandas.DataFrame: Median of groups. """ raise NotImplementedError("abstract property") @@ -79,15 +93,12 @@ def std( For multiple groupings, the result index will be a MultiIndex. - Parameters - ---------- - numeric_only : bool, default False - Include only `float`, `int` or `boolean` data. + Args: + numeric_only (bool, default False): + Include only `float`, `int` or `boolean` data. - Returns - ------- - Series or DataFrame - Standard deviation of values within each group. + Returns: + Series or DataFrame: Standard deviation of values within each group. """ raise NotImplementedError("abstract property") @@ -101,15 +112,13 @@ def var( For multiple groupings, the result index will be a MultiIndex. - Parameters - ---------- - numeric_only : bool, default False - Include only `float`, `int` or `boolean` data. + Args: + numeric_only (bool, default False): + Include only `float`, `int` or `boolean` data. - Returns - ------- - Series or DataFrame - Variance of values within each group. + Returns: + Series or DataFrame + Variance of values within each group. """ raise NotImplementedError("abstract property") @@ -121,17 +130,15 @@ def sum( """ Compute sum of group values. - Parameters - ---------- - numeric_only : bool, default False - Include only float, int, boolean columns. - min_count : int, default 0 - The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. - Returns - ------- - Series or DataFrame - Computed sum of values within each group. + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. + min_count (int, default 0): + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + + Returns: + Series or DataFrame: Computed sum of values within each group. """ raise NotImplementedError("abstract property") @@ -139,17 +146,15 @@ def prod(self, numeric_only: bool = False, min_count: int = 0): """ Compute prod of group values. - Parameters - ---------- - numeric_only : bool, default False - Include only float, int, boolean columns. - min_count : int, default 0 - The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. - Returns - ------- - Series or DataFrame - Computed prod of values within each group. + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. + min_count (int, default 0): + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + + Returns: + Series or DataFrame: Computed prod of values within each group. """ raise NotImplementedError("abstract property") @@ -161,17 +166,15 @@ def min( """ Compute min of group values. - Parameters - ---------- - numeric_only : bool, default False - Include only float, int, boolean columns. - min_count : int, default 0 - The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. - Returns - ------- - Series or DataFrame - Computed min of values within each group. + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. + min_count (int, default 0): + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + + Returns: + Series or DataFrame: Computed min of values within each group. """ raise NotImplementedError("abstract property") @@ -183,17 +186,15 @@ def max( """ Compute max of group values. - Parameters - ---------- - numeric_only : bool, default False - Include only float, int, boolean columns. - min_count : int, default 0 - The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. - Returns - ------- - Series or DataFrame - Computed max of values within each group. + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. + min_count (int, default 0): + The required number of valid values to perform the operation. If fewer + than ``min_count`` non-NA values are present the result will be NA. + + Returns: + Series or DataFrame: Computed max of values within each group. """ raise NotImplementedError("abstract property") @@ -201,15 +202,12 @@ def cumcount(self, ascending: bool = True): """ Number each item in each group from 0 to the length of that group - 1. - Parameters - ---------- - ascending : bool, default True - If False, number in reverse, from length of group - 1 to 0. + Args: + ascending (bool, default True): + If False, number in reverse, from length of group - 1 to 0. - Returns - ------- - Series - Sequence number of each element within each group. + Returns: + Series: Sequence number of each element within each group. """ raise NotImplementedError("abstract property") @@ -217,9 +215,8 @@ def cumprod(self, *args, **kwargs): """ Cumulative product for each group. - Returns - ------- - Series or DataFrame + Returns: + Series or DataFrame: Cumulative product for each group. """ raise NotImplementedError("abstract property") @@ -227,9 +224,8 @@ def cumsum(self, *args, **kwargs): """ Cumulative sum for each group. - Returns - ------- - Series or DataFrame + Returns: + Series or DataFrame: Cumulative sum for each group. """ raise NotImplementedError("abstract property") @@ -237,9 +233,8 @@ def cummin(self, *args, numeric_only: bool = False, **kwargs): """ Cumulative min for each group. - Returns - ------- - Series or DataFrame + Returns: + Series or DataFrame: Cumulative min for each group. """ raise NotImplementedError("abstract property") @@ -247,9 +242,8 @@ def cummax(self, *args, numeric_only: bool = False, **kwargs): """ Cumulative max for each group. - Returns - ------- - Series or DataFrame + Returns: + Series or DataFrame: Cumulative max for each group. """ raise NotImplementedError("abstract property") @@ -259,35 +253,30 @@ def diff(self): Calculates the difference of each element compared with another element in the group (default is element in previous row). - Returns - ------- - Series or DataFrame - First differences. + Returns: + Series or DataFrame: First differences. """ raise NotImplementedError("abstract property") def shift(self, periods: int = 1): """ Shift each group by periods observations. - If freq is passed, the index will be increased using the periods and the freq. - Parameters - ---------- - periods : int, default 1 - Number of periods to shift. - Returns - ------- - Series or DataFrame - Object shifted within each group. + Args: + periods (int, default 1): + Number of periods to shift. + + Returns: + Series or DataFrame: Object shifted within each group. """ raise NotImplementedError("abstract property") def rolling(self, *args, **kwargs): """ - Return a rolling grouper, providing rolling functionality per group. + Returns a rolling grouper, providing rolling functionality per group. Args: - min_periods : int, default None + min_periods (int, default None): Minimum number of observations in window required to have a value; otherwise, result is ``np.nan``. @@ -298,14 +287,16 @@ def rolling(self, *args, **kwargs): to the size of the window. Returns: - Return a new grouper with our rolling appended. + Series or DataFrame: Return a new grouper with our rolling appended. """ raise NotImplementedError("abstract property") def expanding(self, *args, **kwargs): """ - Return an expanding grouper, providing expanding - functionality per group. + Provides expanding functionality. + + Returns: + Series or DataFrame: A expanding grouper, providing expanding functionality per group. """ raise NotImplementedError("abstract property") diff --git a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py index 5d431a8a949..d59886e8aad 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py @@ -1,6 +1,6 @@ class DatetimeProperties: """ - Accessor object for datetimelike properties of the Series values. + Accessor object for datetime-like properties of the Series values. """ @property @@ -19,20 +19,19 @@ def dayofweek(self): values (using the `dt` accessor) or DatetimeIndex. Returns: - Series or Index - Containing integers indicating the day number. + Series or Index: Containing integers indicating the day number. """ raise NotImplementedError("abstract method") @property def date(self): - """Returns numpy array of python :class:`datetime.date` objects. + """Returns numpy array of Python :class:`datetime.date` objects. Namely, the date part of Timestamps without time and timezone information. - warning: + .. warning:: This method returns a Series whereas pandas returns a numpy array. """ @@ -69,7 +68,7 @@ def time(self): The time part of the Timestamps. - warning: + .. warning:: This method returns a Series whereas pandas returns a numpy array. """ @@ -80,7 +79,7 @@ def time(self): def quarter(self): """The quarter of the date. - warning: + .. warning:: This method returns a Series whereas pandas returns a numpy array. """ diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index bdd05a01007..ebad5eb9181 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -9,7 +9,7 @@ class Index: @property def name(self): - """Return Index name.""" + """Returns Index name.""" raise NotImplementedError("abstract method") @property @@ -18,3 +18,19 @@ def shape(self): Return a tuple of the shape of the underlying data. """ raise NotImplementedError("abstract method") + + def to_numpy(self, dtype): + """ + A NumPy ndarray representing the values in this Series or Index. + + Args: + dtype: + The dtype to pass to :meth:`numpy.asarray`. + **kwargs: + Additional keywords passed through to the ``to_numpy`` method + of the underlying array (for extension arrays). + + Returns: + numpy.ndarray + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/indexing.py b/third_party/bigframes_vendored/pandas/core/indexing.py index 87fb34b32a3..d5b9f3c079f 100644 --- a/third_party/bigframes_vendored/pandas/core/indexing.py +++ b/third_party/bigframes_vendored/pandas/core/indexing.py @@ -21,7 +21,7 @@ def iloc(self): - A slice object with ints, e.g. ``1:7``. - **Not supported yet** A boolean array. - **Not supported yet** A ``callable`` function with one argument (the - calling Series or DataFrame) and that returns valid output for + calling Series or DataFrame) that returns valid output for indexing (one of the above). This is useful in method chains, when you don't have a reference to the calling object, but would like to base your selection on some value. @@ -43,30 +43,24 @@ def loc(self): Allowed inputs are: - - **Not supported yet** A single label, e.g. ``5`` or ``'a'``, (note + - A single label, e.g. ``5`` or ``'a'``, (note that ``5`` is interpreted as a *label* of the index, and **never** as an integer position along the index). - - **Not supported yet** A list or array of labels, e.g. ``['a', 'b', 'c']``. - - **Not supported yet** A slice object with labels, e.g. ``'a':'f'``. - - .. warning:: Note that contrary to usual python slices, **both** the - start and the stop are included - - - **Not supported yet** A boolean array of the same length as the axis being sliced, + - A list of labels, e.g. ``['a', 'b', 'c']``. + - A boolean series of the same length as the axis being sliced, e.g. ``[True, False, True]``. - - An alignable boolean Series. The index of the key will be aligned before - masking. - - **Not supported yet** An alignable Index. The Index of the returned + - An alignable Index. The index of the returned selection will be the input. + - **Not supported yet** An alignable boolean Series. The index of the key will be aligned before + masking. + - **Not supported yet** A slice object with labels, e.g. ``'a':'f'``. + Note: contrary to usual python slices, **both** the start and the stop are included. - **Not supported yet** A ``callable`` function with one argument (the - calling Series or DataFrame) - and that returns valid output for indexing (one of the above) + calling Series or DataFrame) that returns valid output for indexing + (one of the above). Raises: - KeyError: If any items are not found. - IndexingError: - If an indexed key is passed and its index is unalignable to the - frame index. + NotImplementError: if the inputs are not supported. """ raise NotImplementedError("abstract methdod") diff --git a/third_party/bigframes_vendored/pandas/core/reshape/__init__.py b/third_party/bigframes_vendored/pandas/core/reshape/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/core/reshape/concat.py b/third_party/bigframes_vendored/pandas/core/reshape/concat.py index 98b43c7dfba..6a5a9fdde92 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/concat.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/concat.py @@ -8,6 +8,7 @@ def concat( objs, *, + axis=0, join: str = "outer", ignore_index: bool = False, ): @@ -25,6 +26,8 @@ def concat( objs: Objects to concatenate. Any None objects will be dropped silently unless they are all None in which case a ValueError will be raised. + axis : {0/'index', 1/'columns'}, default 0 + The axis to concatenate along. join: {'inner', 'outer'}, default 'outer' How to handle indexes on other axis (or axes). ignore_index : bool, default False diff --git a/third_party/bigframes_vendored/pandas/core/reshape/tile.py b/third_party/bigframes_vendored/pandas/core/reshape/tile.py new file mode 100644 index 00000000000..9381ad4552a --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/reshape/tile.py @@ -0,0 +1,65 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/reshape/tile.py +""" +Quantilization functions and related stuff +""" +from __future__ import annotations + + +def cut( + x, + bins, + *, + labels=None, +): + """ + Bin values into discrete intervals. + + Use `cut` when you need to segment and sort data values into bins. This + function is also useful for going from a continuous variable to a + categorical variable. For example, `cut` could convert ages to groups of + age ranges. Supports binning into an equal number of bins, or a + pre-specified array of bins. + + ``labels=False`` implies you just want the bins back. + + Examples: + + .. code-block:: + + import bigframes.pandas as pd + + pd.options.display.progress_bar = None + s = pd.Series([0, 1, 1, 2]) + pd.cut(s, bins=4, labels=False) + + 0 0 + 1 1 + 2 1 + 3 3 + dtype: Int64 + + Args: + x (Series): + The input Series to be binned. Must be 1-dimensional. + bins (int): + The criteria to bin by. + + int : Defines the number of equal-width bins in the range of `x`. The + range of `x` is extended by .1% on each side to include the minimum + and maximum values of `x`. + labels (None): + Specifies the labels for the returned bins. Must be the same length as + the resulting bins. If False, returns only integer indicators of the + bins. This affects the type of the output container (see below). + If True, raises an error. When `ordered=False`, labels must be + provided. + + Returns: + Series: A Series representing the respective bin for each value + of `x`. The type depends on the value of `labels`. + sequence of scalars : returns a Series for Series `x` or a + Categorical for all other inputs. The values stored within + are whatever the type in the sequence is. + False : returns an ndarray of integers. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index a254fcf842d..215f7ec4e08 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -21,7 +21,7 @@ class Series(NDFrame): # type: ignore[misc] @property def dt(self): """ - Accessor object for datetimelike properties of the Series values. + Accessor object for datetime-like properties of the Series values. """ raise NotImplementedError("abstract property") @@ -58,10 +58,9 @@ def name(self) -> Hashable: to form a DataFrame. It is also used whenever displaying the Series using the interpreter. - Returns - ------- - label (hashable object) - The name of the Series, also the column name if part of a DataFrame. + Returns: + hashable object: The name of the Series, also the column name + if part of a DataFrame. """ raise NotImplementedError("abstract property") @@ -78,24 +77,21 @@ def reset_index( when the index is meaningless and needs to be reset to the default before another operation. - Parameters - ---------- - drop : bool, default False - Just reset the index, without inserting it as a column in - the new DataFrame. - name : object, optional - The name to use for the column containing the original Series - values. Uses ``self.name`` by default. This argument is ignored - when `drop` is True. + Args: + drop (bool, default False): + Just reset the index, without inserting it as a column in + the new DataFrame. + name (object, optional): + The name to use for the column containing the original Series + values. Uses ``self.name`` by default. This argument is ignored + when `drop` is True. - Returns - ------- - Series or DataFrame or None - When `drop` is False (the default), a DataFrame is returned. - The newly created columns will come first in the DataFrame, - followed by the original Series values. - When `drop` is True, a `Series` is returned. - In either case, if ``inplace=True``, no value is returned. + Returns: + Series or DataFrame or None; When `drop` is False (the default), + a DataFrame is returned. The newly created columns will come first + in the DataFrame, followed by the original Series values. + When `drop` is True, a `Series` is returned. + In either case, if ``inplace=True``, no value is returned. """ raise NotImplementedError("abstract method") @@ -125,36 +121,34 @@ def to_string( """ Render a string representation of the Series. - Parameters - ---------- - buf : StringIO-like, optional - Buffer to write to. - na_rep : str, optional - String representation of NaN to use, default 'NaN'. - float_format : one-parameter function, optional - Formatter function to apply to columns' elements if they are - floats, default None. - header : bool, default True - Add the Series header (index name). - index : bool, optional - Add index (row) labels, default True. - length : bool, default False - Add the Series length. - dtype : bool, default False - Add the Series dtype. - name : bool, default False - Add the Series name if not None. - max_rows : int, optional - Maximum number of rows to show before truncating. If None, show - all. - min_rows : int, optional - The number of rows to display in a truncated repr (when number - of rows is above `max_rows`). + Args: + buf (StringIO-like, optional): + Buffer to write to. + na_rep (str, optional): + String representation of NaN to use, default 'NaN'. + float_format (one-parameter function, optional): + Formatter function to apply to columns' elements if they are + floats, default None. + header (bool, default True): + Add the Series header (index name). + index (bool, optional): + Add index (row) labels, default True. + length (bool, default False): + Add the Series length. + dtype (bool, default False): + Add the Series dtype. + name (bool, default False): + Add the Series name if not None. + max_rows (int, optional): + Maximum number of rows to show before truncating. If None, show + all. + min_rows (int, optional): + The number of rows to display in a truncated repr (when number + of rows is above `max_rows`). - Returns - ------- - str or None - String representation of Series if ``buf=None``, otherwise None. + Returns: + str or None: String representation of Series if ``buf=None``, + otherwise None. """ formatter = fmt.SeriesFormatter( self, @@ -183,18 +177,16 @@ def to_markdown( """ Print {klass} in Markdown-friendly format. - Parameters - ---------- - buf : str, Path or StringIO-like, optional, default None - Buffer to write to. If None, the output is returned as a string. - mode : str, optional - Mode in which file is opened, "wt" by default. - index : bool, optional, default True - Add index (row) labels. - Returns - ------- - str - {klass} in Markdown-friendly format. + Args: + buf (str, Path or StringIO-like, optional, default None): + Buffer to write to. If None, the output is returned as a string. + mode (str, optional): + Mode in which file is opened, "wt" by default. + index (bool, optional, default True): + Add index (row) labels. + + Returns: + str: {klass} in Markdown-friendly format. """ raise NotImplementedError("abstract method") @@ -202,18 +194,15 @@ def to_dict(self, into: type[dict] = dict) -> Mapping: """ Convert Series to {label -> value} dict or dict-like object. - Parameters - ---------- - into : class, default dict - The collections.abc.Mapping subclass to use as the return - object. Can be the actual class or an empty - instance of the mapping type you want. If you want a - collections.defaultdict, you must pass it initialized. + Args: + into (class, default dict): + The collections.abc.Mapping subclass to use as the return + object. Can be the actual class or an empty + instance of the mapping type you want. If you want a + collections.defaultdict, you must pass it initialized. - Returns - ------- - collections.abc.Mapping - Key-value representation of Series. + Returns: + collections.abc.Mapping: Key-value representation of Series. """ raise NotImplementedError("abstract method") @@ -221,10 +210,8 @@ def to_frame(self) -> DataFrame: """ Convert Series to DataFrame. - Returns - ------- - DataFrame - DataFrame representation of Series. + Returns: + DataFrame: DataFrame representation of Series. """ raise NotImplementedError("abstract method") @@ -242,12 +229,11 @@ def to_excel(self, excel_writer, sheet_name): Note that creating an `ExcelWriter` object with a file name that already exists will result in the contents of the existing file being erased. - Parameters - ---------- - excel_writer : path-like, file-like, or ExcelWriter object - File path or existing ExcelWriter. - sheet_name : str, default 'Sheet1' - Name of sheet which will contain DataFrame. + Args: + excel_writer (path-like, file-like, or ExcelWriter object): + File path or existing ExcelWriter. + sheet_name (str, default 'Sheet1'): + Name of sheet to contain Series. """ raise NotImplementedError("abstract method") @@ -255,23 +241,20 @@ def to_latex(self, buf=None, columns=None, header=True, index=True, **kwargs): """ Render object to a LaTeX tabular, longtable, or nested table. - Parameters - ---------- - buf : str, Path or StringIO-like, optional, default None - Buffer to write to. If None, the output is returned as a string. - columns : list of label, optional - The subset of columns to write. Writes all columns by default. - header : bool or list of str, default True - Write out the column names. If a list of strings is given, - it is assumed to be aliases for the column names. - index : bool, default True - Write row names (index). - + Args: + buf (str, Path or StringIO-like, optional, default None): + Buffer to write to. If None, the output is returned as a string. + columns (list of label, optional): + The subset of columns to write. Writes all columns by default. + header (bool or list of str, default True): + Write out the column names. If a list of strings is given, + it is assumed to be aliases for the column names. + index (bool, default True): + Write row names (index). - Returns - ------- - str or None - If buf is None, returns the result as a string. Otherwise returns None. + Returns: + str or None: If buf is None, returns the result as a string. + Otherwise returns None. """ raise NotImplementedError("abstract method") @@ -281,11 +264,10 @@ def tolist(self) -> list: These are each a scalar type, which is a Python scalar (for str, int, float) or a pandas scalar - (for Timestamp/Timedelta/Interval/Period) + (for Timestamp/Timedelta/Interval/Period). - Returns - ------- - list + Returns: + list: list of the values """ raise NotImplementedError("abstract method") @@ -295,25 +277,24 @@ def to_numpy(self, dtype, copy=False, na_value=None): """ A NumPy ndarray representing the values in this Series or Index. - Parameters - ---------- - dtype : str or numpy.dtype, optional - The dtype to pass to :meth:`numpy.asarray`. - copy : bool, default False - Whether to ensure that the returned value is not a view on - another array. Note that ``copy=False`` does not *ensure* that - ``to_numpy()`` is no-copy. Rather, ``copy=True`` ensure that - a copy is made, even if not strictly necessary. - na_value : Any, optional - The value to use for missing values. The default value depends - on `dtype` and the type of the array. - **kwargs - Additional keywords passed through to the ``to_numpy`` method - of the underlying array (for extension arrays). + Args: + dtype (str or numpy.dtype, optional): + The dtype to pass to :meth:`numpy.asarray`. + copy (bool, default False): + Whether to ensure that the returned value is not a view on + another array. Note that ``copy=False`` does not *ensure* that + ``to_numpy()`` is no-copy. Rather, ``copy=True`` ensure that + a copy is made, even if not strictly necessary. + na_value (Any, optional): + The value to use for missing values. The default value depends + on `dtype` and the type of the array. + ``**kwargs``: + Additional keywords passed through to the ``to_numpy`` method + of the underlying array (for extension arrays). - Returns - ------- - numpy.ndarray + Returns: + numpy.ndarray: A NumPy ndarray representing the values in this + Series or Index. """ raise NotImplementedError("abstract method") @@ -321,12 +302,11 @@ def to_pickle(self, path, **kwargs): """ Pickle (serialize) object to file. - Parameters - ---------- - path : str, path object, or file-like object - String, path object (implementing ``os.PathLike[str]``), or file-like - object implementing a binary ``write()`` function. File path where - the pickled object will be stored. + Args: + path (str, path object, or file-like object): + String, path object (implementing ``os.PathLike[str]``), or file-like + object implementing a binary ``write()`` function. File path where + the pickled object will be stored. """ raise NotImplementedError("abstract method") @@ -334,11 +314,10 @@ def to_xarray(self): """ Return an xarray object from the pandas object. - Returns - ------- - xarray.DataArray or xarray.Dataset - Data in the pandas structure converted to Dataset if the object is - a DataFrame, or a DataArray if the object is a Series. + Returns: + xarray.DataArray or xarray.Dataset: Data in the pandas structure + converted to Dataset if the object is a DataFrame, or a DataArray if + the object is a Series. """ raise NotImplementedError("abstract method") @@ -356,43 +335,24 @@ def to_json( Note NaN's and None will be converted to null and datetime objects will be converted to UNIX timestamps. - Parameters - ---------- - path_or_buf : str, path object, file-like object, or None, default None - String, path object (implementing os.PathLike[str]), or file-like - object implementing a write() function. If None, the result is - returned as a string. - orient: - Indication of expected JSON string format. - - * Series: - - - default is 'index' - - allowed values are: {{'split', 'records', 'index', 'table'}}. - - * DataFrame: - - - default is 'columns' - - allowed values are: {{'split', 'records', 'index', 'columns', - 'values', 'table'}}. - - * The format of the JSON string: - - - 'split' : dict like {{'index' -> [index], 'columns' -> [columns], - 'data' -> [values]}} - - 'records' : list like [{{column -> value}}, ... , {{column -> value}}] - - 'index' : dict like {{index -> {{column -> value}}}} - - 'columns' : dict like {{column -> {{index -> value}}}} - - 'values' : just the values array - - 'table' : dict like {{'schema': {{schema}}, 'data': {{data}}}} - + Args: + path_or_buf (str, path object, file-like object, or None, default None): + String, path object (implementing os.PathLike[str]), or file-like + object implementing a write() function. If None, the result is + returned as a string. + orient ({"split", "records", "index", "columns", "values", "table"}, default "columns"): + Indication of expected JSON string format. + 'split' : dict like {{'index' -> [index], 'columns' -> [columns],'data' -> [values]}} + 'records' : list like [{{column -> value}}, ... , {{column -> value}}] + 'index' : dict like {{index -> {{column -> value}}}} + 'columns' : dict like {{column -> {{index -> value}}}} + 'values' : just the values array + 'table' : dict like {{'schema': {{schema}}, 'data': {{data}}}} Describing the data, where data component is like ``orient='records'``. - Returns - ------- - None or str - If path_or_buf is None, returns the resulting json format as a - string. Otherwise returns None. + Returns: + None or str: If path_or_buf is None, returns the resulting json format as a + string. Otherwise returns None. """ raise NotImplementedError("abstract method") @@ -400,20 +360,32 @@ def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: """ Write object to a comma-separated values (csv) file. - Parameters - ---------- - path_or_buf : str, path object, file-like object, or None, default None - String, path object (implementing os.PathLike[str]), or file-like - object implementing a write() function. If None, the result is - returned as a string. If a non-binary file object is passed, it should - be opened with `newline=''`, disabling universal newlines. If a binary - file object is passed, `mode` might need to contain a `'b'`. + Args: + path_or_buf (str, path object, file-like object, or None, default None): + String, path object (implementing os.PathLike[str]), or file-like + object implementing a write() function. If None, the result is + returned as a string. If a non-binary file object is passed, it should + be opened with `newline=''`, disabling universal newlines. If a binary + file object is passed, `mode` might need to contain a `'b'`. - Returns - ------- - None or str - If path_or_buf is None, returns the resulting csv format as a - string. Otherwise returns None. + Returns: + None or str: If path_or_buf is None, returns the resulting csv format + as a string. Otherwise returns None. + """ + raise NotImplementedError("abstract method") + + def agg(self, func): + """ + Aggregate using one or more operations over the specified axis. + + Args: + func (function): + Function to use for aggregating the data. + Accepted combinations are: string function name, list of + function names, e.g. ``['sum', 'mean']``. + + Returns: + scalar or Series: Aggregated results """ raise NotImplementedError("abstract method") @@ -421,10 +393,9 @@ def count(self): """ Return number of non-NA/null observations in the Series. - Returns - ------- - int or Series (if level specified) - Number of non-null values in the Series. + Returns: + int or Series (if level specified): Number of non-null values in the + Series. """ raise NotImplementedError("abstract method") @@ -434,9 +405,8 @@ def nunique(self) -> int: Excludes NA values by default. - Returns - ------- - int + Returns: + int: number of unique elements in the object. """ raise NotImplementedError("abstract method") @@ -448,10 +418,8 @@ def mode(self) -> Series: Always returns Series even if only one value is returned. - Returns - ------- - Series - Modes of the Series in sorted order. + Returns: + Series: Modes of the Series in sorted order. """ raise NotImplementedError("abstract method") @@ -464,15 +432,15 @@ def drop_duplicates( Return Series with duplicate values removed. Args: - keep : {'first', 'last', ``False``}, default 'first' + keep ({'first', 'last', ``False``}, default 'first'): Method to handle dropping duplicates: - - 'first' : Drop duplicates except for the first occurrence. - - 'last' : Drop duplicates except for the last occurrence. - - ``False`` : Drop all duplicates. + 'first' : Drop duplicates except for the first occurrence. + 'last' : Drop duplicates except for the last occurrence. + ``False`` : Drop all duplicates. Returns: - Series with duplicates dropped or None if ``inplace=True``. + Series: Series with duplicates dropped or None if ``inplace=True``. """ raise NotImplementedError("abstract method") @@ -485,17 +453,17 @@ def duplicated(self, keep="first") -> Series: last occurrence of duplicates can be indicated. Args: - keep : {'first', 'last', False}, default 'first' + keep ({'first', 'last', False}, default 'first'): Method to handle dropping duplicates: - - 'first' : Mark duplicates as ``True`` except for the first - occurrence. - - 'last' : Mark duplicates as ``True`` except for the last - occurrence. - - ``False`` : Mark all duplicates as ``True``. + 'first' : Mark duplicates as ``True`` except for the first + occurrence. + 'last' : Mark duplicates as ``True`` except for the last + occurrence. + ``False`` : Mark all duplicates as ``True``. Returns: - Series indicating whether each value has occurred in the + Series: Series indicating whether each value has occurred in the preceding values. """ raise NotImplementedError("abstract method") @@ -504,16 +472,13 @@ def round(self, decimals: int = 0) -> Series: """ Round each value in a Series to the given number of decimals. - Parameters - ---------- - decimals : int, default 0 - Number of decimal places to round to. If decimals is negative, - it specifies the number of positions to the left of the decimal point. + Args: + decimals (int, default 0): + Number of decimal places to round to. If decimals is negative, + it specifies the number of positions to the left of the decimal point. - Returns - ------- - Series - Rounded values of the Series. + Returns: + Series: Rounded values of the Series. """ raise NotImplementedError("abstract method") @@ -524,10 +489,13 @@ def diff(self) -> Series: Calculates the difference of a {klass} element compared with another element in the {klass} (default is element in previous row). - Returns - ------- - {klass} - First differences of the Series. + Args: + periods (int, default 1): + Periods to shift for calculating difference, accepts negative + values. + + Returns: + {klass}: First differences of the Series. """ raise NotImplementedError("abstract method") @@ -541,23 +509,22 @@ def dot(self, other) -> Series | np.ndarray: It can also be called using `self @ other` in Python >= 3.5. - Parameters - ---------- - other : Series, DataFrame or array-like - The other object to compute the dot product with its columns. + .. note:: + The Series and other has to share the same index if other is a Series + or a DataFrame. + BigQuery Dataframes does not validate this property and will produce + incorrect results if indices are not equal. - Returns - ------- - scalar, Series or numpy.ndarray - Return the dot product of the Series and other if other is a - Series, the Series of the dot product of Series and each rows of - other if other is a DataFrame or a numpy.ndarray between the Series - and each columns of the numpy array. + Args: + other (Series): + The other object to compute the dot product with its columns. + + Returns: + scalar, Series or numpy.ndarray: Return the dot product of the Series + and other if other is a Series, the Series of the dot product of + Series and each rows of other if other is a DataFrame or a + numpy.ndarray between the Series and each columns of the numpy array. - Notes - ----- - The Series and other has to share the same index if other is a Series - or a DataFrame. """ raise NotImplementedError("abstract method") @@ -579,6 +546,7 @@ def sort_values( *, axis: Axis = 0, ascending: bool | int | Sequence[bool] | Sequence[int] = True, + kind: str = "quicksort", na_position: str = "last", ) -> Series | None: """ @@ -587,20 +555,21 @@ def sort_values( Sort a Series in ascending or descending order by some criterion. - Parameters - ---------- - axis : {0 or 'index'} - Unused. Parameter needed for compatibility with DataFrame. - ascending : bool or list of bools, default True - If True, sort values in ascending order, otherwise descending. - na_position : {'first' or 'last'}, default 'last' - Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at - the end. + Args: + axis (0 or 'index'): + Unused. Parameter needed for compatibility with DataFrame. + ascending (bool or list of bools, default True): + If True, sort values in ascending order, otherwise descending. + kind (str, default to 'quicksort'): + Choice of sorting algorithm. Accepts 'quicksort’, ‘mergesort’, + ‘heapsort’, ‘stable’. Ignored except when determining whether to + sort stably. 'mergesort' or 'stable' will result in stable reorder + na_position ({'first' or 'last'}, default 'last'): + Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at + the end. - Returns - ------- - Series or None - Series ordered by values or None if ``inplace=True``. + Returns: + Series or None: Series ordered by values or None if ``inplace=True``. """ raise NotImplementedError("abstract method") @@ -617,21 +586,19 @@ def sort_index( Returns a new Series sorted by label if `inplace` argument is ``False``, otherwise updates the original series and returns None. - Parameters - ---------- - axis : {0 or 'index'} - Unused. Parameter needed for compatibility with DataFrame. - ascending : bool or list-like of bools, default True - Sort ascending vs. descending. When the index is a MultiIndex the - sort direction can be controlled for each level individually. - na_position : {'first', 'last'}, default 'last' - If 'first' puts NaNs at the beginning, 'last' puts NaNs at the end. - Not implemented for MultiIndex. + Args: + axis ({0 or 'index'}): + Unused. Parameter needed for compatibility with DataFrame. + ascending (bool or list-like of bools, default True): + Sort ascending vs. descending. When the index is a MultiIndex the + sort direction can be controlled for each level individually. + na_position ({'first', 'last'}, default 'last'): + If 'first' puts NaNs at the beginning, 'last' puts NaNs at the end. + Not implemented for MultiIndex. - Returns - ------- - Series or None - The original Series sorted by the labels or None if ``inplace=True``. + Returns: + Series or None: The original Series sorted by the labels or None if + ``inplace=True``. """ @@ -643,25 +610,21 @@ def nlargest( """ Return the largest `n` elements. - Parameters - ---------- - n : int, default 5 - Return this many descending sorted values. - keep : {'first', 'last', 'all'}, default 'first' - When there are duplicate values that cannot all fit in a - Series of `n` elements: - - - ``first`` : return the first `n` occurrences in order - of appearance. - - ``last`` : return the last `n` occurrences in reverse - order of appearance. - - ``all`` : keep all occurrences. This can result in a Series of - size larger than `n`. + Args: + n (int, default 5): + Return this many descending sorted values. + keep ({'first', 'last', 'all'}, default 'first'): + When there are duplicate values that cannot all fit in a + Series of `n` elements: + ``first`` : return the first `n` occurrences in order + of appearance. + ``last`` : return the last `n` occurrences in reverse + order of appearance. + ``all`` : keep all occurrences. This can result in a Series of + size larger than `n`. - Returns - ------- - Series - The `n` largest values in the Series, sorted in decreasing order. + Returns: + Series: The `n` largest values in the Series, sorted in decreasing order. """ raise NotImplementedError("abstract method") @@ -669,25 +632,22 @@ def nsmallest(self, n: int = 5, keep: str = "first") -> Series: """ Return the smallest `n` elements. - Parameters - ---------- - n : int, default 5 - Return this many ascending sorted values. - keep : {'first', 'last', 'all'}, default 'first' - When there are duplicate values that cannot all fit in a - Series of `n` elements: - - - ``first`` : return the first `n` occurrences in order - of appearance. - - ``last`` : return the last `n` occurrences in reverse - order of appearance. - - ``all`` : keep all occurrences. This can result in a Series of - size larger than `n`. + Args: + n (int, default 5): + Return this many ascending sorted values. + keep ({'first', 'last', 'all'}, default 'first'): + When there are duplicate values that cannot all fit in a + Series of `n` elements: + + ``first`` : return the first `n` occurrences in order + of appearance. + ``last`` : return the last `n` occurrences in reverse + order of appearance. + ``all`` : keep all occurrences. This can result in a Series of + size larger than `n`. - Returns - ------- - Series - The `n` smallest values in the Series, sorted in increasing order. + Returns: + Series: The `n` smallest values in the Series, sorted in increasing order. """ raise NotImplementedError("abstract method") @@ -704,14 +664,13 @@ def apply( Can be ufunc (a NumPy function that applies to the entire Series) or a Python function that only works on single values. - Parameters - ---------- - func : function - Python function or NumPy ufunc to apply. - Returns - ------- - Series or DataFrame - If func returns a Series object the result will be a DataFrame. + Args: + func (function): + Python function or NumPy ufunc to apply. + + Returns: + Series or DataFrame: If func returns a Series object the result + will be a DataFrame. """ raise NotImplementedError("abstract method") @@ -731,49 +690,45 @@ def groupby( used to group large amounts of data and compute operations on these groups. - Parameters - ---------- - by : mapping, function, label, pd.Grouper or list of such - Used to determine the groups for the groupby. - If ``by`` is a function, it's called on each value of the object's - index. If a dict or Series is passed, the Series or dict VALUES - will be used to determine the groups (the Series' values are first - aligned; see ``.align()`` method). If a list or ndarray of length - equal to the selected axis is passed (see the `groupby user guide - `_), - the values are used as-is to determine the groups. A label or list - of labels may be passed to group by the columns in ``self``. - Notice that a tuple is interpreted as a (single) key. - axis : {0 or 'index', 1 or 'columns'}, default 0 - Split along rows (0) or columns (1). For `Series` this parameter - is unused and defaults to 0. - level : int, level name, or sequence of such, default None - If the axis is a MultiIndex (hierarchical), group by a particular - level or levels. Do not specify both ``by`` and ``level``. - as_index : bool, default True - Return object with group labels as the - index. Only relevant for DataFrame input. as_index=False is - effectively "SQL-style" grouped output. This argument has no effect - on filtrations (see the `filtrations in the user guide - `_), - such as ``head()``, ``tail()``, ``nth()`` and in transformations - (see the `transformations in the user guide - `_). - dropna : bool, default True - If True, and if group keys contain NA values, NA values together - with row/column will be dropped. - If False, NA values will also be treated as the key in groups. + Args: + by (mapping, function, label, pd.Grouper or list of such, default None): + Used to determine the groups for the groupby. + If ``by`` is a function, it's called on each value of the object's + index. If a dict or Series is passed, the Series or dict VALUES + will be used to determine the groups (the Series' values are first + aligned; see ``.align()`` method). If a list or ndarray of length + equal to the selected axis is passed (see the `groupby user guide + `_), + the values are used as-is to determine the groups. A label or list + of labels may be passed to group by the columns in ``self``. + Notice that a tuple is interpreted as a (single) key. + axis ({0 or 'index', 1 or 'columns'}, default 0): + Split along rows (0) or columns (1). For `Series` this parameter + is unused and defaults to 0. + level (int, level name, or sequence of such, default None): + If the axis is a MultiIndex (hierarchical), group by a particular + level or levels. Do not specify both ``by`` and ``level``. + as_index (bool, default True): + Return object with group labels as the + index. Only relevant for DataFrame input. as_index=False is + effectively "SQL-style" grouped output. This argument has no effect + on filtrations (see the "filtrations in the user guide" + ``_), + such as ``head()``, ``tail()``, ``nth()`` and in transformations + (see the "transformations in the user guide" + ``_). + dropna : bool, default True + If True, and if group keys contain NA values, NA values together + with row/column will be dropped. + If False, NA values will also be treated as the key in groups. - Returns - ------- - SeriesGroupBy - Returns a groupby object that contains information about the groups. + Returns: + SeriesGroupBy: Returns a groupby object that contains information about the groups. """ raise NotImplementedError("abstract method") def drop( - self, - labels=None, + self, labels=None, *, axis=0, index=None, columns=None, level=None ) -> Series | None: """ Return Series with specified index labels removed. @@ -782,10 +737,18 @@ def drop( When using a multi-index, labels on different levels can be removed by specifying the level. - Parameters - ---------- - labels : single label or list-like - Index labels to drop. + Args: + labels (single label or list-like): + Index labels to drop. + axis: + Unused. Parameter needed for compatibility with DataFrame. + index: + Redundant for application on Series, but 'index' can be used instead + of 'labels'. + columns: + No change is made to the Series; use 'index' or 'labels' instead. + level: + For MultiIndex, level for which the labels will be removed. Returns ------- @@ -806,7 +769,7 @@ def reorder_levels(self, order: Sequence) -> Series: May not drop or duplicate levels. Args: - order: list of int representing new level order + order (list of int representing new level order): Reference level by number or key. Returns: @@ -819,7 +782,7 @@ def droplevel(self, level): Return Series with requested index / column level(s) removed. Args: - level: int, str, or list-like + level (int, str, or list-like): If a string is given, must be the name of a level If list-like, elements must be names or positional indexes of levels. @@ -833,6 +796,16 @@ def fillna( self, value=None, ) -> Series | None: + """ + Fill NA/NaN values using the specified method. + + Args: + value (scalar, dict, Series, or DataFrame, default None): + Value to use to fill holes (e.g. 0). + + Returns: + Series or None: Object with missing values filled or None. + """ raise NotImplementedError("abstract method") def between( @@ -848,24 +821,33 @@ def between( corresponding Series element is between the boundary values `left` and `right`. NA values are treated as `False`. - Parameters - ---------- - left : scalar or list-like - Left boundary. - right : scalar or list-like - Right boundary. - inclusive : {"both", "neither", "left", "right"} - Include boundaries. Whether to set each bound as closed or open. + Args: + left (scalar or list-like): + Left boundary. + right (scalar or list-like): + Right boundary. + inclusive ({"both", "neither", "left", "right"}): + Include boundaries. Whether to set each bound as closed or open. - Returns - ------- - Series - Series representing whether each element is between left and + Returns: + Series: Series representing whether each element is between left and right (inclusive). """ raise NotImplementedError("abstract method") + def cumprod(self): + """ + Return cumulative product over a DataFrame or Series axis. + + Returns a DataFrame or Series of the same size containing the cumulative + product. + + Returns: + Return cumulative sum of scalar or Series. + """ + raise NotImplementedError("abstract method") + def cumsum(self): """ Return cumulative sum over a DataFrame or Series axis. @@ -873,16 +855,13 @@ def cumsum(self): Returns a DataFrame or Series of the same size containing the cumulative sum. - Parameters - ---------- - axis : {{0 or 'index', 1 or 'columns'}}, default 0 - The index or the name of the axis. 0 is equivalent to None or 'index'. - For `Series` this parameter is unused and defaults to 0. + Args: + axis ({0 or 'index', 1 or 'columns'}, default 0): + The index or the name of the axis. 0 is equivalent to None or 'index'. + For `Series` this parameter is unused and defaults to 0. - Returns - ------- - scalar or Series - Return cumulative sum of scalar or Series. + Returns: + scalar or Series: Return cumulative sum of scalar or Series. """ raise NotImplementedError("abstract method") @@ -893,16 +872,13 @@ def cummax(self): Returns a DataFrame or Series of the same size containing the cumulative maximum. - Parameters - ---------- - axis : {{0 or 'index', 1 or 'columns'}}, default 0 - The index or the name of the axis. 0 is equivalent to None or 'index'. - For `Series` this parameter is unused and defaults to 0. + Args: + axis ({{0 or 'index', 1 or 'columns'}}, default 0): + The index or the name of the axis. 0 is equivalent to None or 'index'. + For `Series` this parameter is unused and defaults to 0. - Returns - ------- - scalar or Series - Return cumulative maximum of scalar or Series. + Returns: + scalar or Series: Return cumulative maximum of scalar or Series. """ raise NotImplementedError("abstract method") @@ -913,57 +889,54 @@ def cummin(self): Returns a DataFrame or Series of the same size containing the cumulative minimum. - Parameters - ---------- - axis : {{0 or 'index', 1 or 'columns'}}, default 0 - The index or the name of the axis. 0 is equivalent to None or 'index'. - For `Series` this parameter is unused and defaults to 0. - skipna : bool, default True - Exclude NA/null values. If an entire row/column is NA, the result - will be NA. - *args, **kwargs - Additional keywords have no effect but might be accepted for - compatibility with NumPy. + Args: + axis ({0 or 'index', 1 or 'columns'}, default 0): + The index or the name of the axis. 0 is equivalent to None or 'index'. + For `Series` this parameter is unused and defaults to 0. + skipna (bool, default True): + Exclude NA/null values. If an entire row/column is NA, the result + will be NA. + `*args`, `**kwargs`: + Additional keywords have no effect but might be accepted for + compatibility with NumPy. - Returns - ------- - scalar or Series - Return cumulative minimum of scalar or Series. + Returns: + scalar or Series: Return cumulative minimum of scalar or Series. """ raise NotImplementedError("abstract method") def eq(self, other) -> Series: - """Return Equal of series and other, element-wise (binary operator eq). + """Return equal of Series and other, element-wise (binary operator eq). Equivalent to ``other == series``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def ne(self, other) -> Series: - """Return Not equal of series and other, element-wise (binary operator ne). + """Return not equal of Series and other, element-wise (binary operator ne). Equivalent to ``other != series``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the comparison. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def le(self, other) -> Series: - """Get 'less than or equal to' of series and other, element-wise (binary operator `<=`). + """Get 'less than or equal to' of Series and other, element-wise (binary operator `<=`). Equivalent to ``series <= other``, but with support to substitute a fill_value for missing data in either one of the inputs. @@ -978,61 +951,76 @@ def le(self, other) -> Series: raise NotImplementedError("abstract method") def lt(self, other) -> Series: - """Get 'less than' of series and other, element-wise (binary operator `<`). + """Get 'less than' of Series and other, element-wise (binary operator `<`). - Equivalent to ``series < other``, but with support to substitute a fill_value for - missing data in either one of the inputs. + Equivalent to ``series < other``, but with support to substitute a fill_value for + missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): - Returns: - Series. The result of the comparison. + Returns: + Series: The result of the operation. """ raise NotImplementedError("abstract method") def ge(self, other) -> Series: - """Get 'greater than or equal to' of series and other, element-wise (binary operator `>=`). + """Get 'greater than or equal to' of Series and other, element-wise (binary operator `>=`). Equivalent to ``series >= other``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the comparison. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def gt(self, other) -> Series: - """Get 'less than or equal to' of series and other, element-wise (binary operator `<=`). + """Get 'less than or equal to' of Series and other, element-wise (binary operator `<=`). Equivalent to ``series <= other``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def add(self, other) -> Series: - """Return Addition of series and other, element-wise (binary operator add). + """Return addition of Series and other, element-wise (binary operator add). Equivalent to ``series + other``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): + + Returns: + Series: The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def radd(self, other) -> Series: + """Return addition of Series and other, element-wise (binary operator radd). + + Equivalent to ``other + series``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1041,136 +1029,180 @@ def sub( self, other, ) -> Series: - """Return Subtraction of series and other, element-wise (binary operator sub). + """Return subtraction of Series and other, element-wise (binary operator sub). Equivalent to ``series - other``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def rsub(self, other) -> Series: - """Return Subtraction of series and other, element-wise (binary operator rsub). + """Return subtraction of Series and other, element-wise (binary operator rsub). Equivalent to ``other - series``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def mul(self, other) -> Series: - """Return Multiplication of series and other, element-wise (binary operator mul). + """Return multiplication of Series and other, element-wise (binary operator mul). Equivalent to ``other * series``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") + def rmul(self, other) -> Series: + """Return multiplication of Series and other, element-wise (binary operator mul). + + Equivalent to ``series * others``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other (Series, or scalar value): + + Returns: + Series: The result of the operation. + """ + raise NotImplementedError("abstract method") + def truediv(self, other) -> Series: - """Return Floating division of series and other, element-wise (binary operator truediv). + """Return floating division of Series and other, element-wise (binary operator truediv). Equivalent to ``series / other``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def rtruediv(self, other) -> Series: - """Return Floating division of series and other, element-wise (binary operator rtruediv). + """Return floating division of Series and other, element-wise (binary operator rtruediv). Equivalent to ``other / series``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def floordiv(self, other) -> Series: - """Return Integer division of series and other, element-wise (binary operator floordiv). + """Return integer division of Series and other, element-wise (binary operator floordiv). Equivalent to ``series // other``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def rfloordiv(self, other) -> Series: - """Return Integer division of series and other, element-wise (binary operator rfloordiv). + """Return integer division of Series and other, element-wise (binary operator rfloordiv). Equivalent to ``other // series``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def mod(self, other) -> Series: - """Return Modulo of series and other, element-wise (binary operator mod). + """Return modulo of Series and other, element-wise (binary operator mod). Equivalent to ``series % other``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: - other: Series, or scalar value + other (Series, or scalar value): Returns: - Series. The result of the operation. + Series: The result of the operation. """ raise NotImplementedError("abstract method") def rmod(self, other) -> Series: - """Get Modulo of series and other, element-wise (binary operator `rmod`). + """Get modulo of Series and other, element-wise (binary operator `rmod`). Equivalent to ``other % series``, but with support to substitute a fill_value for missing data in either one of the inputs. + Args: + other (Series, or scalar value): + + Returns: + Series: The result of the operation. + + """ + raise NotImplementedError("abstract method") + + def divmod(self, other) -> Series: + """Return integer division and modulo of Series and other, element-wise (binary operator divmod). + + Equivalent to divmod(series, other). + Args: other: Series, or scalar value Returns: - Series. The result of the operation. + 2-Tuple of Series. The result of the operation. The result is always + consistent with (floordiv, mod) (though pandas may not). + + """ + raise NotImplementedError("abstract method") + + def rdivmod(self, other) -> Series: + """Return integer division and modulo of Series and other, element-wise (binary operator rdivmod). + + Equivalent to other divmod series. + + Args: + other: Series, or scalar value + + Returns: + 2-Tuple of Series. The result of the operation. The result is always + consistent with (rfloordiv, rmod) (though pandas may not). """ raise NotImplementedError("abstract method") @@ -1181,15 +1213,12 @@ def all( """ Return whether all elements are True, potentially over an axis. - Returns True unless there at least one element within a series or along a - Dataframe axis that is False or equivalent (e.g. zero or empty). + Returns True unless there at least one element within a Series or along a + DataFrame axis that is False or equivalent (e.g. zero or empty). - - Returns - ------- - scalar or Series - If level is specified, then, Series is returned; otherwise, scalar - is returned. + Returns: + scalar or Series: If level is specified, then, Series is returned; + otherwise, scalar is returned. """ raise NotImplementedError("abstract method") @@ -1202,12 +1231,9 @@ def any( Returns False unless there is at least one element within a series or along a Dataframe axis that is True or equivalent (e.g. non-zero or non-empty). - - Returns - ------- - scalar or Series - If level is specified, then, Series is returned; otherwise, scalar - is returned. + Returns: + scalar or Series: If level is specified, then, Series is returned; + otherwise, scalar is returned. """ raise NotImplementedError("abstract method") @@ -1221,9 +1247,8 @@ def max( of the ``numpy.ndarray`` method ``argmax``. - Returns - ------- - scalar or scalar + Returns: + scalar or scalar """ raise NotImplementedError("abstract method") @@ -1236,10 +1261,8 @@ def min( If you want the index of the minimum, use ``idxmin``. This is the equivalent of the ``numpy.ndarray`` method ``argmin``. - - Returns - ------- - scalar or scalar + Returns: + scalar or scalar """ raise NotImplementedError("abstract method") @@ -1249,7 +1272,7 @@ def std( """ Return sample standard deviation over requested axis. - Normalized by N-1 by default. This can be changed using the ddof argument. + Normalized by N-1 by default. Returns @@ -1264,12 +1287,10 @@ def var( """ Return unbiased variance over requested axis. - Normalized by N-1 by default. This can be changed using the ddof argument. + Normalized by N-1 by default. - - Returns - ------- - scalar or Series (if level specified) + Returns: + scalar or Series (if level specified) """ raise NotImplementedError("abstract method") @@ -1291,6 +1312,19 @@ def mean(self): """ raise NotImplementedError("abstract method") + def median(self, *, exact: bool = False): + """Return the median of the values over the requested axis. + + Args: + exact (bool. default False): + Default False. Get the exact median instead of an approximate + one. Note: ``exact=True`` not yet supported. + + Returns: + scalar + """ + raise NotImplementedError("abstract method") + def prod(self): """Return the product of the values over the requested axis. @@ -1299,66 +1333,71 @@ def prod(self): """ raise NotImplementedError("abstract method") + def skew(self): + """Return unbiased skew over requested axis. + + Normalized by N-1. + + Returns: + scalar + """ + raise NotImplementedError("abstract method") + def kurt(self): """Return unbiased kurtosis over requested axis. Kurtosis obtained using Fisher’s definition of kurtosis (kurtosis of normal == 0.0). Normalized by N-1. - Returns - ------- - scalar or scalar + Returns: + scalar or scalar: Unbiased kurtosis over requested axis. """ raise NotImplementedError("abstract method") def where(self, cond, other): """Replace values where the condition is False. - Parameters - ---------- - cond: bool Series/DataFrame, array-like, or callable - Where cond is True, keep the original value. Where False, replace - with corresponding value from other. If cond is callable, it is - computed on the Series/DataFrame and should return boolean - Series/DataFrame or array. The callable must not change input - Series/DataFrame (though pandas doesn’t check it). - other: scalar, Series/DataFrame, or callable - Entries where cond is False are replaced with corresponding value - from other. If other is callable, it is computed on the - Series/DataFrame and should return scalar or Series/DataFrame. - The callable must not change input Series/DataFrame (though pandas - doesn’t check it). If not specified, entries will be filled with - the corresponding NULL value (np.nan for numpy dtypes, pd.NA for - extension dtypes). + Args: + cond (bool Series/DataFrame, array-like, or callable): + Where cond is True, keep the original value. Where False, replace + with corresponding value from other. If cond is callable, it is + computed on the Series/DataFrame and returns boolean + Series/DataFrame or array. The callable must not change input + Series/DataFrame (though pandas doesn’t check it). + other (scalar, Series/DataFrame, or callable): + Entries where cond is False are replaced with corresponding value + from other. If other is callable, it is computed on the + Series/DataFrame and returns scalar or Series/DataFrame. + The callable must not change input Series/DataFrame (though pandas + doesn’t check it). If not specified, entries will be filled with + the corresponding NULL value (np.nan for numpy dtypes, pd.NA for + extension dtypes). - Returns - ------- - Series + Returns: + Series """ raise NotImplementedError("abstract method") def mask(self, cond, other): """Replace values where the condition is True. - Parameters - ---------- - cond: bool Series/DataFrame, array-like, or callable - Where cond is False, keep the original value. Where True, replace - with corresponding value from other. If cond is callable, it is - computed on the Series/DataFrame and should return boolean - Series/DataFrame or array. The callable must not change input - Series/DataFrame (though pandas doesn’t check it). - other: scalar, Series/DataFrame, or callable - Entries where cond is True are replaced with corresponding value - from other. If other is callable, it is computed on the - Series/DataFrame and should return scalar or Series/DataFrame. - The callable must not change input Series/DataFrame (though pandas - doesn’t check it). If not specified, entries will be filled with - the corresponding NULL value (np.nan for numpy dtypes, pd.NA for - extension dtypes). + Args: + cond (bool Series/DataFrame, array-like, or callable): + Where cond is False, keep the original value. Where True, replace + with corresponding value from other. If cond is callable, it is + computed on the Series/DataFrame and should return boolean + Series/DataFrame or array. The callable must not change input + Series/DataFrame (though pandas doesn’t check it). + other (scalar, Series/DataFrame, or callable): + Entries where cond is True are replaced with corresponding value + from other. If other is callable, it is computed on the + Series/DataFrame and should return scalar or Series/DataFrame. + The callable must not change input Series/DataFrame (though pandas + doesn’t check it). If not specified, entries will be filled with + the corresponding NULL value (np.nan for numpy dtypes, pd.NA for + extension dtypes). - Returns - ------- - Series + Returns: + Series """ raise NotImplementedError("abstract method") @@ -1369,18 +1408,15 @@ def clip(self): singular values or array like, and in the latter case the clipping is performed element-wise in the specified axis. - Parameters - ---------- - - lower: float or array-like, default None - Minimum threshold value. All values below this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. + Args: + lower (float or array-like, default None): + Minimum threshold value. All values below this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. - upper: float or array-like, default None - Maximum threshold value. All values above this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. + upper (float or array-like, default None): + Maximum threshold value. All values above this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. - Returns - ------- - Series + Returns: + Series. """ raise NotImplementedError("abstract method") @@ -1390,10 +1426,8 @@ def argmax(self): If the minimum is achieved in multiple locations, the first row position is returned. - Returns - ------- - Series - Row position of the maximum value. + Returns: + Series: Row position of the maximum value. """ raise NotImplementedError("abstract method") @@ -1403,10 +1437,8 @@ def argmin(self): If the maximum is achieved in multiple locations, the first row position is returned. - Returns - ------- - Series - Row position of the minimum value. + Returns: + Series: Row position of the minimum value. """ raise NotImplementedError("abstract method") @@ -1420,18 +1452,15 @@ def rename(self, index, **kwargs) -> Series | None: Alternatively, change ``Series.name`` with a scalar value. - Parameters - ---------- - index : scalar, hashable sequence, dict-like or function optional - Functions or dict-like are transformations to apply to - the index. - Scalar or hashable sequence-like will alter the ``Series.name`` - attribute. + Args: + index (scalar, hashable sequence, dict-like or function optional): + Functions or dict-like are transformations to apply to + the index. + Scalar or hashable sequence-like will alter the ``Series.name`` + attribute. - Returns - ------- - Series - Series with index labels + Returns: + Series: Series with index labels """ raise NotImplementedError("abstract method") @@ -1440,14 +1469,12 @@ def rename_axis(self, mapper, **kwargs): """ Set the name of the axis for the index or columns. - Parameters - ---------- - mapper : scalar, list-like, optional - Value to set the axis name attribute. + Args: + mapper (scalar, list-like, optional): + Value to set the axis name attribute. - Returns - ------- - Series + Returns: + Series: Series with the name of the axis set. """ raise NotImplementedError("abstract method") @@ -1459,39 +1486,35 @@ def rolling( """ Provide rolling window calculations. - Parameters - ---------- - window : int, timedelta, str, offset, or BaseIndexer subclass - Size of the moving window. + Args: + window (int, timedelta, str, offset, or BaseIndexer subclass): + Size of the moving window. - If an integer, the fixed number of observations used for - each window. + If an integer, the fixed number of observations used for + each window. - If a timedelta, str, or offset, the time period of each window. Each - window will be a variable sized based on the observations included in - the time-period. This is only valid for datetimelike indexes. - To learn more about the offsets & frequency strings, please see `this link - `__. + If a timedelta, str, or offset, the time period of each window. Each + window will be a variable sized based on the observations included in + the time-period. This is only valid for datetime-like indexes. + To learn more about the offsets & frequency strings, please see `this link + `__. - If a BaseIndexer subclass, the window boundaries - based on the defined ``get_window_bounds`` method. Additional rolling - keyword arguments, namely ``min_periods``, ``center``, ``closed`` and - ``step`` will be passed to ``get_window_bounds``. + If a BaseIndexer subclass, the window boundaries + based on the defined ``get_window_bounds`` method. Additional rolling + keyword arguments, namely ``min_periods``, ``center``, ``closed`` and + ``step`` will be passed to ``get_window_bounds``. - min_periods : int, default None - Minimum number of observations in window required to have a value; - otherwise, result is ``np.nan``. + min_periods (int, default None): + Minimum number of observations in window required to have a value; + otherwise, result is ``np.nan``. - For a window that is specified by an offset, ``min_periods`` will default to 1. + For a window that is specified by an offset, ``min_periods`` will default to 1. - For a window that is specified by an integer, ``min_periods`` will default - to the size of the window. + For a window that is specified by an integer, ``min_periods`` will default + to the size of the window. - Returns - ------- - ``Window`` subclass if a ``win_type`` is passed - - ``Rolling`` subclass if ``win_type`` is not passed + Returns: + ``Window`` subclass if a ``win_type`` is passed.``Rolling`` subclass if ``win_type`` is not passed """ raise NotImplementedError("abstract method") @@ -1499,14 +1522,12 @@ def expanding(self, min_periods=1): """ Provide expanding window calculations. - Parameters - ---------- - min_periods : int, default 1 - Minimum number of observations in window required to have a value; - otherwise, result is ``np.nan``. + Args: + min_periods (int, default 1): + Minimum number of observations in window required to have a value; + otherwise, result is ``np.nan``. - Returns - ------- + Returns: ``Expanding`` subclass """ raise NotImplementedError("abstract method") @@ -1527,18 +1548,18 @@ def value_counts( Excludes NA values by default. Args: - normalize : bool, default False + normalize (bool, default False): If True then the object returned will contain the relative frequencies of the unique values. - sort : bool, default True + sort (bool, default True): Sort by frequencies. - ascending : bool, default False + ascending (bool, default False): Sort in ascending order. - dropna : bool, default True + dropna (bool, default True): Don't include counts of NaN. Returns: - Series + Series: Series containing counts of unique values. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py index 81b9bd3d146..e464843c779 100644 --- a/third_party/bigframes_vendored/pandas/core/strings/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -1,3 +1,7 @@ +import re +import typing + + class StringMethods: """ Vectorized string functions for Series and Index. @@ -7,6 +11,29 @@ class StringMethods: R's stringr package. """ + def extract(self, pat: str, flags: int = 0): + """ + Extract capture groups in the regex `pat` as columns in a DataFrame. + + For each subject string in the Series, extract groups from the + first match of regular expression `pat`. + + Args: + pat: + Regular expression pattern with capturing groups. + flags: + Flags from the ``re`` module, e.g. ``re.IGNORECASE``, that + modify regular expression matching for things like case, + spaces, etc. For more details, see :mod:`re`. + + Returns: + A DataFrame with one row for each subject string, and one + column for each group. Any capture group names in regular + expression pat will be used for column names; otherwise + capture group numbers will be used. + """ + raise NotImplementedError("abstract method") + def find(self, sub, start: int = 0, end=None): """Return lowest indexes in each strings in the Series/Index. @@ -17,15 +44,14 @@ def find(self, sub, start: int = 0, end=None): Args: sub: Substring being searched. - start: + start (int, default 0): Left edge index. - end: + end (None): Right edge index. Returns: - Series or Index of int. + bigframes.series.Series: Series with lowest indexes in each strings. """ - raise NotImplementedError("abstract method") def len(self): @@ -35,9 +61,8 @@ def len(self): (such as a dictionary). Returns: - Series or Index of int - A Series or Index of integer values indicating the length of each - element in the Series or Index. + bigframes.series.Series: A Series or Index of integer values indicating + the length of each element in the Series or Index. """ raise NotImplementedError("abstract method") @@ -48,7 +73,7 @@ def lower(self): Equivalent to :meth:`str.lower`. Returns: - Series or Index of object + bigframes.series.Series: Series with lowercase. """ raise NotImplementedError("abstract method") @@ -57,16 +82,16 @@ def slice(self, start=None, stop=None): """Slice substrings from each element in the Series or Index. Args: - start : int, optional + start (int, optional): Start position for slice operation. - stop : int, optional + stop (int, optional): Stop position for slice operation. - step : int, optional + step (int, optional): Step size for slice operation. Returns: - Series or Index of object - Series or Index from sliced substring from original string object. + bigframes.series.Series:: Series or Index from sliced + substring from original string object. """ raise NotImplementedError("abstract method") @@ -80,7 +105,8 @@ def strip(self): Equivalent to :meth:`str.strip`. Returns: - Series or Index of object + bigframes.series.Series: Series or Index without leading + and trailing characters. """ raise NotImplementedError("abstract method") @@ -91,7 +117,7 @@ def upper(self): Equivalent to :meth:`str.upper`. Returns: - Series or Index of object + bigframes.series.Series: Series with uppercase strings. """ raise NotImplementedError("abstract method") @@ -104,9 +130,8 @@ def isnumeric(self): has zero characters, ``False`` is returned for that check. Returns: - Series or Index of bool - Series or Index of boolean values with the same length as the original - Series/Index. + bigframes.series.Series: Series or Index of boolean values with the + same length as the original Series/Index. """ raise NotImplementedError("abstract method") @@ -120,7 +145,7 @@ def rstrip(self): Equivalent to :meth:`str.rstrip`. Returns: - Series or Index of object + bigframes.series.Series: Series without trailing characters. """ raise NotImplementedError("abstract method") @@ -134,7 +159,7 @@ def lstrip(self): Equivalent to :meth:`str.lstrip`. Returns: - Series or Index of object` + bigframes.series.Series: Series without leading characters. """ raise NotImplementedError("abstract method") @@ -147,9 +172,8 @@ def repeat(self, repeats: int): Same value for all (int) or different value per (sequence). Returns: - Series or pandas.Index - Series or Index of repeated string objects specified by - input parameter repeats. + bigframes.series.Series: Series or Index of repeated string + objects specified by input parameter repeats. """ raise NotImplementedError("abstract method") @@ -160,7 +184,7 @@ def capitalize(self): Equivalent to :meth:`str.capitalize`. Returns: - Series or Index of object + bigframes.series.Series: Series with captitalized strings. """ raise NotImplementedError("abstract method") @@ -172,16 +196,121 @@ def cat(self, others, *, join): and elements of `others` element-wise. Args: - others : Series + others (Series): - join : {'left', 'outer'}, default 'left' + join ({'left', 'outer'}, default 'left'): Determines the join-style between the calling Series and any Series in `others` (objects without an index need to match the length of the calling Series). To disable alignment, use `.values` on any Series/Index/DataFrame in `others`. Returns: - Series + bigframes.series.Series: Series with concatenated strings. """ raise NotImplementedError("abstract method") + + def contains(self, pat, case: bool = True, flags: int = 0, *, regex: bool = True): + """ + Test if pattern or regex is contained within a string of a Series or Index. + + Return boolean Series or Index based on whether a given pattern or regex is + contained within a string of a Series or Index. + + Args: + pat (str, re.Pattern): + Character sequence or regular expression. + case (bool, default True): + If True, case sensitive. + flags (int, default 0): + Flags to pass through to the re module, e.g. re.IGNORECASE. + regex (bool, default True): + If True, assumes the pat is a regular expression. + If False, treats the pat as a literal string. + + Returns: + bigframes.series.Series: A Series or Index of boolean values indicating + whether the given pattern is contained within the string of each + element of the Series or Index. + """ + raise NotImplementedError("abstract method") + + def replace( + self, + pat: typing.Union[str, re.Pattern], + repl: str, + *, + case: typing.Optional[bool] = None, + flags: int = 0, + regex: bool = False, + ): + """ + Replace each occurrence of pattern/regex in the Series/Index. + + Equivalent to :meth:`str.replace` or :func:`re.sub`, depending on + the regex value. + + Args: + pat (str, re.Pattern): + String can be a character sequence or regular expression. + repl (str): + Replacement string. + case (default None): + Determines if replace is case sensitive: + + - If True, case sensitive (the default if `pat` is a string) + - Set to False for case insensitive + - Cannot be set if `pat` is a compiled regex. + flags (int, default 0): + Regex module flags, e.g. re.IGNORECASE. Cannot be set if `pat` is a compiled + regex. + regex (bool: default False): + Determines if the passed-in pattern is a regular expression: + + - If True, assumes the passed-in pattern is a regular expression. + - If False, treats the pattern as a literal string + - Cannot be set to False if `pat` is a compiled regex or `repl` is + a callable. + + Returns: + bigframes.series.Series: A copy of the object with all matching occurrences + of `pat` replaced by `repl`. + + """ + raise NotImplementedError("abstract method") + + def startswith( + self, + pat: typing.Union[str, tuple[str, ...]], + ): + """ + Test if the start of each string element matches a pattern. + + Args: + pat (str, tuple[str, ...]): + Character sequence or tuple of strings. Regular expressions are not + accepted. + + Returns: + bigframes.series.Series: A Series of booleans indicating whether the given + pattern matches the start of each string element. + """ + raise NotImplementedError("abstract method") + + def endswith( + self, + pat: typing.Union[str, tuple[str, ...]], + ): + """ + Test if the end of each string element matches a pattern. + + Args: + pat (str, tuple[str, ...]): + Character sequence or tuple of strings. Regular expressions are not + accepted. + + Returns: + bigframes.series.Series: A Series of booleans indicating whether the given + pattern matches the end of each string element. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/io/common.py b/third_party/bigframes_vendored/pandas/io/common.py index 057ed96721d..506984e64da 100644 --- a/third_party/bigframes_vendored/pandas/io/common.py +++ b/third_party/bigframes_vendored/pandas/io/common.py @@ -17,9 +17,10 @@ def dedup_names( but a custom pattern may be supported in the future. Examples - -------- - >>> dedup_names(["x", "y", "x", "x"], is_potential_multiindex=False) + ``` + dedup_names(["x", "y", "x", "x"], is_potential_multiindex=False) ['x', 'y', 'x.1', 'x.2'] + ``` """ names = list(names) # so we can index counts: DefaultDict[Hashable, int] = defaultdict(int) diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 4f4b1b8199f..9425ead0e34 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -15,23 +15,23 @@ def read_gbq( col_order: Iterable[str] = (), max_results: Optional[int] = None, ): - """Loads DataFrame from Google BigQuery. + """Loads DataFrame from BigQuery. Args: - query: + query (str): A SQL string to be executed or a BigQuery table to be read. The table must be specified in the format of `project.dataset.tablename` or `dataset.tablename`. - index_col: + index_col (Iterable[str] or str): Name of result column(s) to use for index in results DataFrame. - col_order: + col_order (Iterable[str]): List of BigQuery column names in the desired order for results DataFrame. - max_results: + max_results (Optional[int], default None): If set, limit the maximum number of rows to fetch from the query results. Returns: - A DataFrame representing results of the query or table. + bigframes.dataframe.DataFrame: A DataFrame representing results of the query or table. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/io/parquet.py b/third_party/bigframes_vendored/pandas/io/parquet.py index ee2e9a65f22..6f0a2b3cb46 100644 --- a/third_party/bigframes_vendored/pandas/io/parquet.py +++ b/third_party/bigframes_vendored/pandas/io/parquet.py @@ -8,15 +8,18 @@ def read_parquet( self, path: str, ): - r"""Load a parquet object from the file path (local or GCS), returning a DataFrame. + r"""Load a Parquet object from the file path (local or Cloud Storage), returning a DataFrame. - Args: - path: - Local or GCS path to parquet file. - - Note: + .. note:: This method will not guarantee the same ordering as the file. Instead, set a serialized index column as the index and sort by that in the resulting DataFrame. + + Args: + path (str): + Local or Cloud Storage path to Parquet file. + + Returns: + bigframes.dataframe.DataFrame: A BigQuery DataFrames. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py index e5ed39b54aa..e01eb734fb8 100644 --- a/third_party/bigframes_vendored/pandas/io/parsers/readers.py +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -32,27 +32,30 @@ def read_csv( encoding: Optional[str] = None, **kwargs, ): - r"""Loads DataFrame from comma-separated values (csv) file locally or from GCS. + """Loads DataFrame from comma-separated values (csv) file locally or from + Cloud Storage. The CSV file data will be persisted as a temporary BigQuery table, which can be automatically recycled after the Session is closed. - Note: using `engine="bigquery"` will not guarantee the same ordering as the - file. Instead, set a serialized index column as the index and sort by - that in the resulting DataFrame. + .. note:: + using `engine="bigquery"` will not guarantee the same ordering as the + file. Instead, set a serialized index column as the index and sort by + that in the resulting DataFrame. Args: - filepath_or_buffer: a string path including GCS and local file. - - sep: the separator for fields in a CSV file. For the BigQuery engine, the separator + filepath_or_buffer (str): + a string path including Cloud Storage and local file. + sep (Optional[str], default ","): + the separator for fields in a CSV file. For the BigQuery engine, the separator can be any ISO-8859-1 single-byte character. To use a character in the range 128-255, you must encode the character as UTF-8. Both engines support `sep="\t"` to specify tab character as separator. Default engine supports having any number of spaces as separator by specifying `sep="\s+"`. Separators longer than 1 character are interpreted as regular expressions by the default engine. BigQuery engine only supports single character separators. - - header: row number to use as the column names. + header (Optional[int], default 0): + row number to use as the column names. - ``None``: Instructs autodetect that there are no headers and data should be read starting from the first row. - ``0``: If using `engine="bigquery"`, Autodetect tries to detect headers in the @@ -68,19 +71,19 @@ def read_csv( contains column names unless the `names` argument is specified. If `names` is provided, row N+1 will be ignored, row N+2 will be read as data, and column names are inferred from `names`. - - names: a list of column names to use. If the file contains a header row and you + names (default None): + a list of column names to use. If the file contains a header row and you want to pass this parameter, then `header=0` should be passed as well so the first (header) row is ignored. Only to be used with default engine. - - index_col: column(s) to use as the row labels of the DataFrame, either given as + index_col (default None): + column(s) to use as the row labels of the DataFrame, either given as string name or column index. `index_col=False` can be used with the default engine only to enforce that the first column is not used as the index. Using column index instead of column name is only supported with the default engine. The BigQuery engine only supports having a single column name as the `index_col`. Neither engine supports having a multi-column index. - - usecols: list of column names to use. The BigQuery engine only supports having a list + usecols (default None): + List of column names to use): The BigQuery engine only supports having a list of string column names. Column indices and callable functions are only supported with the default engine. Using the default engine, the column names in `usecols` can be defined to correspond to column names provided with the `names` parameter @@ -89,22 +92,22 @@ def read_csv( column names provided with the BigQuery engine will be consistent in the resulting dataframe. If using a callable function with the default engine, only column names that evaluate to True by the callable function will be in the resulting dataframe. - - dtype: data type for data or columns. Only to be used with default engine. - - engine: type of engine to use. If `engine="bigquery"` is specified, then BigQuery's - load API will be used. Otherwise, the engine will be passed to `pandas.read_csv`. - - encoding: the character encoding of the data. The default encoding is `UTF-8` for both + dtype (data type for data or columns): + Data type for data or columns. Only to be used with default engine. + engine (Optional[Dict], default None): + Type of engine to use. If `engine="bigquery"` is specified, then BigQuery's load API will be used. + Otherwise, the engine will be passed to `pandas.read_csv`. + encoding (Optional[str], default to None): + encoding the character encoding of the data. The default encoding is `UTF-8` for both engines. The default engine acceps a wide range of encodings. Refer to Python documentation for a comprehensive list, https://docs.python.org/3/library/codecs.html#standard-encodings The BigQuery engine only supports `UTF-8` and `ISO-8859-1`. - - **kwargs: keyword arguments. + **kwargs: + keyword arguments. Returns: - A BigQuery DataFrames. + bigframes.dataframe.DataFrame: A BigQuery DataFrames. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index 925bb76e1a1..03958f75950 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -13,7 +13,7 @@ class BaseEstimator: """Base class for all estimators. - Notes: + .. note:: All estimators should specify all the parameters that can be set at the class level in their ``__init__`` as explicit keyword arguments (no ``*args`` or ``**kwargs``). @@ -54,12 +54,12 @@ def get_params(self, deep: bool = True) -> Dict[str, Any]: """Get parameters for this estimator. Args: - deep: + deep (bool, default True): Default ``True``. If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - A dictionary of parameter names mapped to their values. + Dictionary: A dictionary of parameter names mapped to their values. """ out: Dict = dict() for key in self._get_param_names(): @@ -84,15 +84,15 @@ def score(self, X, y): each label set be correctly predicted. Args: - X: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples, n_features). Test samples. - y: + y (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples,) or (n_samples, n_outputs). True labels for `X`. Returns: - A DataFrame of the evaluation result. + bigframes.dataframe.DataFrame: A DataFrame of the evaluation result. """ raise NotImplementedError("abstract method") @@ -106,19 +106,19 @@ def score(self, X, y): """Return the evaluation metrics of the model. Args: - X: - DataFrame of shape (n_samples, n_features). Test samples. For + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). Test samples. For some estimators this may be a precomputed kernel matrix or a list of generic objects instead with shape ``(n_samples, n_samples_fitted)``, where ``n_samples_fitted`` is the number of samples used in the fitting for the estimator. - y: - DataFrame of shape (n_samples,) or (n_samples, n_outputs). True + y (bigframes.dataframe.DataFrame or bigframes.series.Series: + Series or DataFrame of shape (n_samples,) or (n_samples, n_outputs). True values for `X`. Returns: - A DataFrame of the evaluation result. + bigframes.dataframe.DataFrame: A DataFrame of the evaluation result. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index 8f67cab1c31..bddb82c7ba0 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -24,13 +24,13 @@ def predict(self, X): """Predict the closest cluster each sample in X belongs to. Args: - X: - DataFrame of shape (n_samples, n_features). The data matrix for + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). The data matrix for which we want to get the predictions. Returns: - DataFrame of shape (n_samples,), containing the class labels for - each sample. + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,), containing the + class labels for each sample. """ raise NotImplementedError("abstract method") @@ -39,7 +39,7 @@ class KMeans(_BaseKMeans): """K-Means clustering. Args: - n_clusters: int, default=8 + n_clusters (int, default 8): The number of clusters to form as well as the number of centroids to generate. Default to 8. """ @@ -53,19 +53,51 @@ def fit( """Compute k-means clustering. Args: - X: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples, n_features). Training data. - y: Ignored + y (default None): Not used, present here for API consistency by convention. + transforms (Optional[List[str]], default None): + Do not use. Internal param to be deprecated. + Use bigframes.ml.pipeline instead. - transforms: - An optional list of SQL expressions to apply over top of the - model inputs as preprocessing. This preprocessing will be - automatically reapplied to new input data (e.g. in .predict), - and may contain steps (like ML.STANDARD_SCALER) that fit to the - training data. Returns: - Fitted Estimator. + KMeans: Fitted Estimator. + """ + raise NotImplementedError("abstract method") + + def predict( + self, + X, + ): + """Predict the closest cluster each sample in X belongs to. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + DataFrame of shape (n_samples, n_features). New data to predict. + y: (default None) + Not used, present here for API consistency by convention. + + Returns: + bigframes.dataframe.DataFrame: DataFrame of the cluster each sample belongs to. + """ + raise NotImplementedError("abstract method") + + def score( + self, + X, + y=None, + ): + """Metrics of the model. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + DataFrame of shape (n_samples, n_features). New Data. + y (default None) + Not used, present here for API consistency by convention. + + Returns: + bigframes.dataframe.DataFrame: DataFrame of the metrics. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py index dadfa5d0131..bc8bc3980ac 100644 --- a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py +++ b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py @@ -35,13 +35,13 @@ def fit( """Fit all transformers using X. Args: - X: - DataFrame of shape (n_samples, n_features). Training vector, + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Series or DataFrame of shape (n_samples, n_features). Training vector, where `n_samples` is the number of samples and `n_features` is the number of features. Returns: - Fitted estimator. + ColumnTransformer: Fitted estimator. """ raise NotImplementedError("abstract method") @@ -52,10 +52,10 @@ def transform( """Transform X separately by each transformer, concatenate results. Args: - X: - The DataFrame to be transformed by subset. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Series or DataFrame to be transformed by subset. Returns: - Transformed result. + bigframes.dataframe.DataFrame: Transformed result. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 6f0d17b2e6d..619c13f35d7 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -30,24 +30,54 @@ class PCA(BaseEstimator, metaclass=ABCMeta): truncated SVD. Args: - n_components: Optional[int] - Number of components to keep. if n_components is not set all components are kept. + n_components (Optional[int], default 3): + Number of components to keep. if n_components is not set all components + are kept. """ - def fit( - self, - X, - ): + def fit(self, X, y=None): """Fit the model according to the given training data. Args: - X: - DataFrame of shape (n_samples, n_features). Training vector, + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). Training vector, where `n_samples` is the number of samples and `n_features` is the number of features. + y (default None): + Ignored. + + transforms (Optional[List[str]], default None): + Do not use. Internal param to be deprecated. + Use bigframes.ml.pipeline instead. + Returns: - Fitted estimator. + PCA: Fitted estimator. """ raise NotImplementedError("abstract method") + + def score(self, X=None, y=None): + """Return the metrics of the model. + + Args: + X (default None): + Ignored. + + y (default None): + Ignored. + Returns: + bigframes.dataframe.DataFrame: DataFrame that represents model metrics. + """ + raise NotImplementedError("abstract method") + + def predict(self, X): + """Predict the closest cluster for each sample in X. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or a DataFrame to predict. + + Returns: + bigframes.dataframe.DataFrame: predicted DataFrames.""" + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py index c91821c7622..73f4684dc30 100644 --- a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -46,12 +46,17 @@ def fit(self, X, y): Args: X: - DataFrame of shape (n_samples, n_features). Training data. + Series or DataFrame of shape (n_samples, n_features). Training data. y: - DataFrame of shape (n_samples,) or (n_samples, n_targets). + Series or DataFrame of shape (n_samples,) or (n_samples, n_targets). Target values. Will be cast to X's dtype if necessary. + transforms (Optional[List[str]], default None): + Do not use. Internal param to be deprecated. + Use bigframes.ml.pipeline instead. + + Returns: Fitted Estimator. """ @@ -71,7 +76,7 @@ def predict(self, X): Args: X: - DataFrame of shape (n_samples, n_features). The data matrix for + Series or DataFrame of shape (n_samples, n_features). The data matrix for which we want to get the predictions. Returns: @@ -137,7 +142,7 @@ def predict(self, X): Args: X: - DataFrame of shape (n_samples, n_features). The data matrix for + Series or DataFrame of shape (n_samples, n_features). The data matrix for which we want to get the predictions. Returns: diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index e67edac1aa4..65e895298df 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -30,11 +30,11 @@ def predict(self, X): """Predict using the linear model. Args: - X: - DataFrame of shape (n_samples, n_features). Samples. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). Samples. Returns: - DataFrame of shape (n_samples,). Returns predicted values. + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,). Returns predicted values. """ raise NotImplementedError("abstract method") @@ -44,13 +44,13 @@ def predict(self, X): """Predict class labels for samples in X. Args: - X: - DataFrame of shape (n_samples, n_features). The data matrix for + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). The data matrix for which we want to get the predictions. Returns: - DataFrame of shape (n_samples,), containing the class labels for - each sample. + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,), containing + the class labels for each sample. """ raise NotImplementedError("abstract method") @@ -63,7 +63,7 @@ class LinearRegression(RegressorMixin, LinearModel): the dataset, and the targets predicted by the linear approximation. Args: - fit_intercept: + fit_intercept (default True): Default ``True``. Whether to calculate the intercept for this model. If set to False, no intercept will be used in calculations (i.e. data is expected to be centered). @@ -78,21 +78,18 @@ def fit( """Fit linear model. Args: - X: - DataFrame of shape (n_samples, n_features). Training data. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). Training data. - y: - DataFrame of shape (n_samples,) or (n_samples, n_targets). + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples,) or (n_samples, n_targets). Target values. Will be cast to X's dtype if necessary. - transforms: - An optional list of SQL expressions to apply over top of the - model inputs as preprocessing. This preprocessing will be - automatically reapplied to new input data (e.g. in .predict), - and may contain steps (like ML.STANDARD_SCALER) that fit to the - training data. + transforms (Optional[List[str]], default None): + Do not use. Internal param to be deprecated. + Use bigframes.ml.pipeline instead. Returns: - Fitted Estimator. + LinearRegression: Fitted Estimator. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 6de5bf65e25..8525e570689 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -24,33 +24,12 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): """Logistic Regression (aka logit, MaxEnt) classifier. Args: - fit_intercept: + fit_intercept (default True): Default True. Specifies if a constant (a.k.a. bias or intercept) should be added to the decision function. - auto_class_weights: + auto_class_weights (default False): Default False. If True, balance class labels using weights for each class in inverse proportion to the frequency of that class. - - References: - L-BFGS-B -- Software for Large-scale Bound-constrained Optimization - Ciyou Zhu, Richard Byrd, Jorge Nocedal and Jose Luis Morales. - http://users.iems.northwestern.edu/~nocedal/lbfgsb.html - - LIBLINEAR -- A Library for Large Linear Classification - https://www.csie.ntu.edu.tw/~cjlin/liblinear/ - - SAG -- Mark Schmidt, Nicolas Le Roux, and Francis Bach - Minimizing Finite Sums with the Stochastic Average Gradient - https://hal.inria.fr/hal-00860051/document - - SAGA -- Defazio, A., Bach F. & Lacoste-Julien S. (2014). - "SAGA: A Fast Incremental Gradient Method With Support - for Non-Strongly Convex Composite Objectives" (Arxiv <1407.0202>) - - Hsiang-Fu Yu, Fang-Lan Huang, Chih-Jen Lin (2011). Dual coordinate descent - methods for logistic regression and maximum entropy models. - Machine Learning 85(1-2):41-75. - https://www.csie.ntu.edu.tw/~cjlin/papers/maxent_dual.pdf """ def fit( @@ -62,22 +41,20 @@ def fit( """Fit the model according to the given training data. Args: - X: - DataFrame of shape (n_samples, n_features). Training vector, + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). Training vector, where `n_samples` is the number of samples and `n_features` is the number of features. - y: + y (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples,). Target vector relative to X. - transforms: - An optional list of SQL expressions to apply over top of the - model inputs as preprocessing. This preprocessing will be - automatically reapplied to new input data (e.g. in .predict), - and may contain steps (like ML.STANDARD_SCALER) that fit to the - training data. + transforms (Optional[List[str]], default None): + Do not use. Internal param to be deprecated. + Use bigframes.ml.pipeline instead. + Returns: - Fitted estimator. + LogisticRegression: Fitted Estimator. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/metrics/_classification.py b/third_party/bigframes_vendored/sklearn/metrics/_classification.py index 05fb5f6fb6e..6d9692ac8d6 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_classification.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_classification.py @@ -25,18 +25,17 @@ def accuracy_score(y_true, y_pred, normalize=True) -> float: """Accuracy classification score. Args: - y_true: DataFrame of shape (n_samples,) + y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) labels. - y_pred: DataFrame of shape (n_samples,) + y_pred (Series or DataFrame of shape (n_samples,)): Predicted labels, as returned by a classifier. - normalize: bool, default=True + normalize (bool, default True): Default to True. If ``False``, return the number of correctly classified samples. Otherwise, return the fraction of correctly classified samples. Returns: - score: float. - If ``normalize == True``, return the fraction of correctly + float: If ``normalize == True``, return the fraction of correctly classified samples (float), else returns the number of correctly classified samples (int). """ @@ -58,17 +57,16 @@ def confusion_matrix( :math:`C_{1,1}` and false positives is :math:`C_{0,1}`. Args: - y_true: DataFrame of shape (n_samples,) + y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) target values. - y_pred: DataFrame of shape (n_samples,) + y_pred (Series or DataFrame of shape (n_samples,)): Estimated targets as returned by a classifier. Returns: - C: DataFrame of shape (n_samples, n_features). - Confusion matrix whose i-th row and j-th - column entry indicates the number of - samples with true label being i-th class - and predicted label being j-th class. + DataFrame of shape (n_samples, n_features): Confusion matrix whose + i-th row and j-th column entry indicates the number of + samples with true label being i-th class and predicted label + being j-th class. """ raise NotImplementedError("abstract method") @@ -87,19 +85,18 @@ def recall_score( The best value is 1 and the worst value is 0. Args: - y_true: DataFrame of shape (n_samples,) + y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) target values. - y_pred: DataFrame of shape (n_samples,) + y_pred (Series or DataFrame of shape (n_samples,)): Estimated targets as returned by a classifier. - average: {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ - default='binary' + average ({'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ + default='binary'): This parameter is required for multiclass/multilabel targets. Possible values are 'None', 'micro', 'macro', 'samples', 'weighted', 'binary'. Returns: - recall: float (if average is not None) or Series of float of shape \ - (n_unique_labels,). - Recall of the positive class in binary classification or weighted + float (if average is not None) or Series of float of shape n_unique_labels,): Recall + of the positive class in binary classification or weighted average of the recall of each class for the multiclass task. """ raise NotImplementedError("abstract method") @@ -120,9 +117,9 @@ def precision_score( The best value is 1 and the worst value is 0. Args: - y_true: DataFrame of shape (n_samples,) + y_true: Series or DataFrame of shape (n_samples,) Ground truth (correct) target values. - y_pred: DataFrame of shape (n_samples,) + y_pred: Series or DataFrame of shape (n_samples,) Estimated targets as returned by a classifier. average: {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ default='binary' @@ -148,16 +145,16 @@ def f1_score( The F1 score can be interpreted as a harmonic mean of the precision and recall, where an F1 score reaches its best value at 1 and worst score at 0. The relative contribution of precision and recall to the F1 score are - equal. The formula for the F1 score is: F1 = 2 * (precision * recall) / (precision + recall) + equal. The formula for the F1 score is: F1 = 2 * (precision * recall) / (precision + recall). In the multi-class and multi-label case, this is the average of the F1 score of each class with weighting depending on the ``average`` parameter. Args: - y_true: DataFrame of shape (n_samples,) + y_true: Series or DataFrame of shape (n_samples,) Ground truth (correct) target values. - y_pred: DataFrame of shape (n_samples,) + y_pred: Series or DataFrame of shape (n_samples,) Estimated targets as returned by a classifier. average: {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ default='binary' diff --git a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py index b7625171b56..693996070f0 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py @@ -26,15 +26,14 @@ def auc(x, y) -> float: :func:`average_precision_score`. Args: - x : DataFrame of shape (n_samples,) + x (Series or DataFrame of shape (n_samples,)): X coordinates. These must be either monotonic increasing or monotonic decreasing. - y : DataFrame of shape (n_samples,) + y (Series or DataFrame of shape (n_samples,)): Y coordinates. Returns: - auc : float - Area Under the Curve. + float: Area Under the Curve. """ raise NotImplementedError("abstract method") @@ -44,11 +43,11 @@ def roc_auc_score(y_true, y_score) -> float: from prediction scores. Args: - y_true: DataFrame of shape (n_samples,) + y_true (Series or DataFrame of shape (n_samples,)): True labels or binary label indicators. The binary and multiclass cases expect labels with shape (n_samples,) while the multilabel case expects binary label indicators with shape (n_samples, n_classes). - y_score: DataFrame of shape (n_samples,) + y_score (Series or DataFrame of shape (n_samples,)): Target scores. * In the binary case, it corresponds to an array of shape `(n_samples,)`. Both probability estimates and non-thresholded @@ -59,8 +58,7 @@ def roc_auc_score(y_true, y_score) -> float: corresponds to the output of `estimator.decision_function(X, y)`. Returns: - auc: float. - Area Under the Curve score. + float: Area Under the Curve score. """ raise NotImplementedError("abstract method") @@ -73,10 +71,10 @@ def roc_curve( """Compute Receiver operating characteristic (ROC). Args: - y_true: DataFrame of shape (n_samples,) + y_true: Series or DataFrame of shape (n_samples,) True binary labels. If labels are not either {-1, 1} or {0, 1}, then pos_label should be explicitly given. - y_score: DataFrame of shape (n_samples,) + y_score: Series or DataFrame of shape (n_samples,) Target scores, can either be probability estimates of the positive class, confidence values, or non-thresholded measure of decisions (as returned by "decision_function" on some classifiers). diff --git a/third_party/bigframes_vendored/sklearn/metrics/_regression.py b/third_party/bigframes_vendored/sklearn/metrics/_regression.py index 44ceab48a6a..b90c4158878 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_regression.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_regression.py @@ -41,13 +41,12 @@ def r2_score(y_true, y_pred, force_finite=True) -> float: predictions) respectively. Args: - y_true: DataFrame of shape (n_samples,) + y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) target values. - y_pred: DataFrame of shape (n_samples,) + y_pred (Series or DataFrame of shape (n_samples,)): Estimated target values. Returns: - z: float. - The :math:`R^2` score. + float: The :math:`R^2` score. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/pipeline.py b/third_party/bigframes_vendored/sklearn/pipeline.py index 7374edf990a..f8bbae86df6 100644 --- a/third_party/bigframes_vendored/sklearn/pipeline.py +++ b/third_party/bigframes_vendored/sklearn/pipeline.py @@ -18,13 +18,13 @@ class Pipeline(BaseEstimator, metaclass=ABCMeta): """Pipeline of transforms with a final estimator. Sequentially apply a list of transforms and a final estimator. - Intermediate steps of the pipeline must be 'transforms', that is, they + Intermediate steps of the pipeline must be `transforms`, that is, they must implement `fit` and `transform` methods. The final estimator only needs to implement `fit`. The purpose of the pipeline is to assemble several steps that can be cross-validated together while setting different parameters. This simplifies code, and allows deploying an estimator - and peprocessing together, e.g. with Pipeline.to_gbq(...) + and peprocessing together, e.g. with `Pipeline.to_gbq(...).` """ def fit( @@ -38,14 +38,14 @@ def fit( data. Finally, fit the transformed data using the final estimator. Args: - X: - A BigQuery DataFrames representing training data. Must match the + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A DataFrame or Series representing training data. Must match the input requirements of the first step of the pipeline. - y: - A BigQuery DataFrames representing training targets, if applicable. + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + A DataFrame or Series representing training targets, if applicable. Returns: - Pipeline with fitted steps. + Pipeline: Pipeline with fitted steps. """ raise NotImplementedError("abstract method") @@ -58,14 +58,14 @@ def score(self, X, y): `score` method. Only valid if the final estimator implements `score`. Args: - X: - A BigQuery DataFrames as evaluation data. - y: - A BigQuery DataFrames as evaluation labels. - - Returns: - A BigQuery DataFrames representing the result of calling - `score` on the final estimator. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A DataFrame or Series as evaluation data. + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + A DataFrame or Series as evaluation labels. + + Returns:; + DataFrame: A DataFrame representing the result + of calling `score` on the final estimator. """ raise NotImplementedError("abstract method") @@ -74,10 +74,11 @@ def predict(self, X): """Predict the pipeline result for each sample in X. Args: - X: - A BigQuery DataFrames to predict. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A DataFrame or Series to predict. Returns: - A BigQuery DataFrames Dataframe representing predicted result. + bigframes.dataframe.DataFrame: A Dataframe representing + predicted result. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py index 7a44c4e66d3..c57d1f22304 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -32,12 +32,32 @@ class StandardScaler(BaseEstimator): def fit(self, X): """Compute the mean and std to be used for later scaling. + Examples: + + .. code-block:: + + from bigframes.ml.preprocessing import StandardScaler + + enc = StandardScaler() + X = [['Male', 1], ['Female', 3], ['Female', 2]] + enc.fit(X) + + Examples: + + .. code-block:: + + from bigframes.ml import StandardScaler + + enc = StandardScaler() + X = [['Male', 1], ['Female', 3], ['Female', 2]] + enc.fit(X) + Args: - X: - A dataframe with training data. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Dataframe or Series with training data. Returns: - Fitted scaler. + StandardScaler: Fitted scaler. """ raise NotImplementedError("abstract method") @@ -45,9 +65,10 @@ def transform(self, X): """Perform standardization by centering and scaling. Args: - X: - The DataFrame to be transformed. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. Returns: - Transformed result.""" + bigframes.dataframe.DataFrame: Transformed result. + """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index 6f0565ac136..a6c32d91c1d 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -14,18 +14,52 @@ class OneHotEncoder(BaseEstimator): encoding scheme. Note that this method deviates from Scikit-Learn; instead of producing sparse - binary columns, the encoding is a single column of STRUCT + binary columns, the encoding is a single column of `STRUCT`. + + Args: + drop (Optional[Literal["most_frequent"]], default None): + Specifies a methodology to use to drop one of the categories per feature. + This is useful in situations where perfectly collinear features cause problems, + such as when feeding the resulting data into an unregularized linear regression model. + However, dropping one category breaks the symmetry of the original representation + and can therefore induce a bias in downstream models, for instance for penalized + linear classification or regression models. + Default None: retain all the categories. + "most_frequent": Drop the most frequent category found in the string expression. + Selecting this value causes the function to use dummy encoding. + min_frequency (Optional[int], default None): + Specifies the minimum frequency below which a category will be considered infrequent. + Default None. + int: categories with a smaller cardinality will be considered infrequent as index 0. + max_categories (Optional[int], default None): + Specifies an upper limit to the number of output features for each input feature + when considering infrequent categories. If there are infrequent categories, + max_categories includes the category representing the infrequent categories along with the frequent categories. + Default None, set limit to 1,000,000. """ def fit(self, X): """Fit OneHotEncoder to X. + Examples: + + Given a dataset with two features, we let the encoder find the unique + values per feature and transform the data to a binary one-hot encoding. + + .. code-block:: + + from bigframes.ml.preprocessing import OneHotEncoder + + enc = OneHotEncoder() + X = [['Male', 1], ['Female', 3], ['Female', 2]] + enc.fit(X) + Args: - X: - A dataframe with training data. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series with training data. Returns: - Fitted encoder. + OneHotEncoder: Fitted encoder. """ raise NotImplementedError("abstract method") @@ -33,9 +67,10 @@ def transform(self, X): """Transform X using one-hot encoding. Args: - X: - The DataFrame to be transformed. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. Returns: - Transformed result.""" + bigframes.dataframe.DataFrame: The result is categorized as index: number, value: number. + Where index is the position of the dict that seeing the category, and value is 0 or 1.""" raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index 268484b2c23..fcb5d2ec593 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -12,11 +12,11 @@ def predict(self, X): """Predict using the XGB model. Args: - X: - DataFrame of shape (n_samples, n_features). Samples. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). Samples. Returns: - DataFrame of shape (n_samples,). Returns predicted values. + DataFrame of shape (n_samples,): Returns predicted values. """ raise NotImplementedError("abstract method") @@ -28,15 +28,19 @@ def fit(self, X, y): pass ``xgb_model`` argument. Args: - X: - DataFrame of shape (n_samples, n_features). Training data. + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). Training data. - y: + y (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples,) or (n_samples, n_targets). Target values. Will be cast to X's dtype if necessary. + transforms (Optional[List[str]], default None): + Do not use. Internal param to be deprecated. + Use bigframes.ml.pipeline instead. + Returns: - Fitted Estimator. + XGBModel: Fitted Estimator. """ raise NotImplementedError("abstract method") @@ -53,46 +57,47 @@ class XGBRegressor(XGBModel, XGBRegressorBase): XGBoost regression model. Args: - num_parallel_tree: Optional[int] + num_parallel_tree (Optional[int]): Number of parallel trees constructed during each iteration. Default to 1. - booster: Optional[str] + booster (Optional[str]): Specify which booster to use: gbtree or dart. Default to "gbtree". - dart_normalized_type": Optional[str] + dart_normalized_type (Optional[str]): Type of normalization algorithm for DART booster. Possible values: "TREE", "FOREST". Default to "TREE". - tree_method: Optional[str] + tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to - default, XGBoost will choose the most conservative option available. - min_child_weight : Optional[float] + default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + "hist". + min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. - colsample_bytree : Optional[float] + colsample_bytree (Optional[float]): Subsample ratio of columns when constructing each tree. Default to 1.0. - colsample_bylevel : Optional[float] + colsample_bylevel (Optional[float]): Subsample ratio of columns for each level. Default to 1.0. - colsample_bynode : Optional[float] + colsample_bynode (Optional[float]): Subsample ratio of columns for each split. Default to 1.0. - gamma : Optional[float] + gamma (Optional[float]): (min_split_loss) Minimum loss reduction required to make a further partition on a leaf node of the tree. Default to 0.0. - max_depth : Optional[int] + max_depth (Optional[int]): Maximum tree depth for base learners. Default to 6. - subsample : Optional[float] + subsample (Optional[float]): Subsample ratio of the training instance. Default to 1.0. - reg_alpha : Optional[float] + reg_alpha (Optional[float]): L1 regularization term on weights (xgb's alpha). Default to 0.0. - reg_lambda : Optional[float] + reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop: Optional[bool] + early_stop (Optional[bool]): Whether training should stop after the first iteration. Default to True. - learning_rate: Optional[float] + learning_rate (Optional[float]): Boosting learning rate (xgb's "eta"). Default to 0.3. - max_iterations: Optional[int] + max_iterations (Optional[int]): Maximum number of rounds for boosting. Default to 20. - min_rel_progress: Optional[float] + min_rel_progress (Optional[float]): Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. - enable_global_explain: Optional[bool] + enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. - xgboost_version: Optional[str] - Specifies the Xgboost version for model training. Default to "0.9". + xgboost_version (Optional[str]): + Specifies the Xgboost version for model training. Default to "0.9". Possible values: "0.9", "1.1". """ @@ -101,45 +106,45 @@ class XGBClassifier(XGBModel, XGBClassifierMixIn, XGBClassifierBase): XGBoost classifier model. Args: - num_parallel_tree: Optional[int] + num_parallel_tree (Optional[int]): Number of parallel trees constructed during each iteration. Default to 1. - booster: Optional[str] + booster (Optional[str]): Specify which booster to use: gbtree or dart. Default to "gbtree". - dart_normalized_type": Optional[str] + dart_normalized_type (Optional[str]): Type of normalization algorithm for DART booster. Possible values: "TREE", "FOREST". Default to "TREE". - tree_method: Optional[str] + tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", "hist". - min_child_weight : Optional[float] + min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. - colsample_bytree : Optional[float] + colsample_bytree (Optional[float]): Subsample ratio of columns when constructing each tree. Default to 1.0. - colsample_bylevel : Optional[float] + colsample_bylevel (Optional[float]): Subsample ratio of columns for each level. Default to 1.0. - colsample_bynode : Optional[float] + colsample_bynode (Optional[float]): Subsample ratio of columns for each split. Default to 1.0. - gamma : Optional[float] + gamma (Optional[float]): (min_split_loss) Minimum loss reduction required to make a further partition on a leaf node of the tree. Default to 0.0. - max_depth : Optional[int] + max_depth (Optional[int]): Maximum tree depth for base learners. Default to 6. - subsample : Optional[float] + subsample (Optional[float]): Subsample ratio of the training instance. Default to 1.0. - reg_alpha : Optional[float] + reg_alpha (Optional[float]): L1 regularization term on weights (xgb's alpha). Default to 0.0. - reg_lambda : Optional[float] + reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop: Optional[bool] + early_stop (Optional[bool]): Whether training should stop after the first iteration. Default to True. - learning_rate: Optional[float] + learning_rate (Optional[float]): Boosting learning rate (xgb's "eta"). Default to 0.3. - max_iterations: Optional[int] + max_iterations (Optional[int]): Maximum number of rounds for boosting. Default to 20. - min_rel_progress: Optional[float] + min_rel_progress (Optional[float]): Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. - enable_global_explain: Optional[bool] + enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. - xgboost_version: Optional[str] + xgboost_version (Optional[str]): Specifies the Xgboost version for model training. Default to "0.9". Possible values: "0.9", "1.1". """ From 3e143d793e300f1cf27e2b1822170d69876eaa22 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 14 Aug 2023 10:36:04 -0500 Subject: [PATCH 004/921] chore: release 0.1.1 (#5) Change-Id: I10199358657195d60fb414360f1bff4a94490bad --- CHANGELOG.md | 7 +++++++ README.rst | 4 ++-- bigframes/version.py | 2 +- docs/user_guide/bigframes.pandas/remote_functions.rst | 5 +++-- setup.py | 8 +++++--- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d966220bd2..19bf3a117b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/bigframes/#history +## 0.1.1 (2023-08-14) + +### Documentation + +* Correct link to code repository in `setup.py` and use correct terminology for + `console.cloud.google.com` links. + ## 0.1.0 (2023-08-11) ### Features diff --git a/README.rst b/README.rst index 6f51dfde23e..c6dbb059576 100644 --- a/README.rst +++ b/README.rst @@ -117,7 +117,7 @@ BigQuery connections are created in the same location as the BigQuery DataFrames session, using the name you provide in the custom function definition. To view and manage connections, do the following: -1. Go to `BigQuery Studio `__. +1. Go to `BigQuery in the Google Cloud Console `__. 2. Select the project in which you created the remote function. 3. In the Explorer pane, expand that project and then expand External connections. @@ -127,7 +127,7 @@ the location used by the BigQuery DataFrames session. For example, ``bigframes_temp_us_central1``. To view and manage remote functions, do the following: -1. Go to `BigQuery Studio `__. +1. Go to `BigQuery in the Google Cloud Console `__. 2. Select the project in which you created the remote function. 3. In the Explorer pane, expand that project, expand the dataset in which you created the remote function, and then expand Routines. diff --git a/bigframes/version.py b/bigframes/version.py index df6dc3c17c8..c387f59d2c6 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/docs/user_guide/bigframes.pandas/remote_functions.rst b/docs/user_guide/bigframes.pandas/remote_functions.rst index abfe6a10caa..7540ba8a28c 100644 --- a/docs/user_guide/bigframes.pandas/remote_functions.rst +++ b/docs/user_guide/bigframes.pandas/remote_functions.rst @@ -104,8 +104,9 @@ Using Existing Functions ======================== If you have already defined a custom function in BigQuery, either in the -BigQuery Studio or with the `remote_function` decorator above or otherwise, you -may use it with BigQuery DataFrames with the `read_gbq_function` method. +BigQuery Google Cloud Console or with the `remote_function` decorator above or +otherwise, you may use it with BigQuery DataFrames with the `read_gbq_function` +method. More details are available via the `help` command: diff --git a/setup.py b/setup.py index 3e36cd3ecfb..20f080b1668 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,9 @@ # Package metadata. name = "bigframes" -description = "Scalable DataFrames with BigQuery" +description = ( + "BigQuery DataFrames -- scalable analytics and machine learning with BigQuery" +) # Should be one of: # 'Development Status :: 3 - Alpha' @@ -85,9 +87,9 @@ description=description, long_description=readme, author="Google LLC", - author_email="googleapis-packages@google.com", + author_email="bigframes-feedback@google.com", license="Apache 2.0", - url="https://github.com/googleapis/python-bigquery", + url="https://github.com/googleapis/python-bigquery-dataframes", classifiers=[ release_status, "Intended Audience :: Developers", From 76f4daa45db9be12abb112683f2bd2f89589ff86 Mon Sep 17 00:00:00 2001 From: Genesis929 <142538604+Genesis929@users.noreply.github.com> Date: Thu, 17 Aug 2023 13:17:39 -0700 Subject: [PATCH 005/921] chore: release 0.2.0 (#6) * chore: release 0.2.0 Change-Id: Id9f7843e3c5238e82a2514b5f9b81d7335460c6e * Update CHANGELOG Change-Id: Ic77a9acfdbffe3bf97103d164f606f85fc9fe4db --- .kokoro/docker/docs/Dockerfile | 88 + .kokoro/docker/docs/fetch_gpg_keys.sh | 45 + .kokoro/docs/common.cfg | 61 + .kokoro/docs/docs-presubmit.cfg | 23 + .kokoro/docs/docs.cfg | 1 + .kokoro/publish-docs.sh | 46 + .kokoro/trampoline_v2.sh | 487 ++++ .trampolinerc | 58 + CHANGELOG.md | 19 + bigframes/core/__init__.py | 7 +- bigframes/core/blocks.py | 10 +- bigframes/core/groupby/__init__.py | 122 +- bigframes/core/indexes/index.py | 6 +- bigframes/core/io.py | 5 + bigframes/core/utils.py | 11 + bigframes/dataframe.py | 73 +- bigframes/formatting_helpers.py | 32 +- bigframes/ml/cluster.py | 9 + bigframes/ml/core.py | 17 +- bigframes/ml/sql.py | 7 + bigframes/operations/__init__.py | 36 + bigframes/operations/base.py | 6 +- bigframes/operations/strings.py | 25 +- bigframes/pandas/__init__.py | 5 + bigframes/remote_function.py | 55 +- bigframes/series.py | 41 +- bigframes/session.py | 37 +- bigframes/version.py | 2 +- docs/templates/toc.yml | 130 + .../03 - Using ML - ML fundamentals.ipynb | 2425 ++++++++++++----- noxfile.py | 25 +- tests/system/conftest.py | 32 + tests/system/small/ml/conftest.py | 41 +- tests/system/small/ml/test_cluster.py | 66 + tests/system/small/ml/test_core.py | 78 +- tests/system/small/operations/test_strings.py | 83 + tests/system/small/test_dataframe.py | 20 +- tests/system/small/test_dataframe_io.py | 6 +- tests/system/small/test_groupby.py | 105 + tests/system/small/test_remote_function.py | 70 +- tests/system/small/test_series.py | 93 + tests/system/small/test_session.py | 21 + tests/unit/core/test_io.py | 49 + tests/unit/ml/test_sql.py | 5 + .../pandas/core/groupby/__init__.py | 45 +- .../bigframes_vendored/pandas/core/series.py | 45 + .../pandas/core/strings/accessor.py | 113 + .../sklearn/cluster/_kmeans.py | 21 + 48 files changed, 3883 insertions(+), 924 deletions(-) create mode 100644 .kokoro/docker/docs/Dockerfile create mode 100644 .kokoro/docker/docs/fetch_gpg_keys.sh create mode 100644 .kokoro/docs/common.cfg create mode 100644 .kokoro/docs/docs-presubmit.cfg create mode 100644 .kokoro/docs/docs.cfg create mode 100644 .kokoro/publish-docs.sh create mode 100644 .kokoro/trampoline_v2.sh create mode 100644 .trampolinerc create mode 100644 docs/templates/toc.yml create mode 100644 tests/unit/core/test_io.py diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile new file mode 100644 index 00000000000..d300bee2607 --- /dev/null +++ b/.kokoro/docker/docs/Dockerfile @@ -0,0 +1,88 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ubuntu:22.04 + +ENV DEBIAN_FRONTEND noninteractive + +# Ensure local Python is preferred over distribution Python. +ENV PATH /usr/local/bin:$PATH + +# Install dependencies. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + apt-transport-https \ + build-essential \ + ca-certificates \ + curl \ + dirmngr \ + git \ + gpg-agent \ + graphviz \ + libbz2-dev \ + libdb5.3-dev \ + libexpat1-dev \ + libffi-dev \ + liblzma-dev \ + libreadline-dev \ + libsnappy-dev \ + libssl-dev \ + libsqlite3-dev \ + portaudio19-dev \ + python3-distutils \ + redis-server \ + software-properties-common \ + ssh \ + sudo \ + tcl \ + tcl-dev \ + tk \ + tk-dev \ + uuid-dev \ + wget \ + zlib1g-dev \ + && add-apt-repository universe \ + && apt-get update \ + && apt-get -y install jq \ + && apt-get clean autoclean \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* \ + && rm -f /var/cache/apt/archives/*.deb + +###################### Install python 3.9.13 and 3.10.5 + +# Download python 3.9.13 and 3.10.5 +RUN wget https://www.python.org/ftp/python/3.9.13/Python-3.9.13.tgz +RUN wget https://www.python.org/ftp/python/3.9.13/Python-3.10.5.tgz + +# Extract files +RUN tar -xvf Python-3.9.13.tgz +RUN tar -xvf Python-3.10.5.tgz + +# Install python 3.9.13 and 3.10.5 +RUN ./Python-3.9.13/configure --enable-optimizations +RUN ./Python-3.10.5/configure --enable-optimizations +RUN make altinstall + +###################### Install pip +RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ + && python3 /tmp/get-pip.py \ + && rm /tmp/get-pip.py + +# Test pip +RUN python3 -m pip +RUN python3.9 -m pip +RUN python3.10 -m pip + +CMD ["python3.10"] diff --git a/.kokoro/docker/docs/fetch_gpg_keys.sh b/.kokoro/docker/docs/fetch_gpg_keys.sh new file mode 100644 index 00000000000..c4a92a33eaa --- /dev/null +++ b/.kokoro/docker/docs/fetch_gpg_keys.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A script to fetch gpg keys with retry. +# Avoid jinja parsing the file. +# + +function retry { + if [[ "${#}" -le 1 ]]; then + echo "Usage: ${0} retry_count commands.." + exit 1 + fi + local retries=${1} + local command="${@:2}" + until [[ "${retries}" -le 0 ]]; do + $command && return 0 + if [[ $? -ne 0 ]]; then + echo "command failed, retrying" + ((retries--)) + fi + done + return 1 +} + +# 3.6.9, 3.7.5 (Ned Deily) +retry 3 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys \ + 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D + +# 3.8.0 (Łukasz Langa) +retry 3 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys \ + E3FF2839C048B25C084DEBE9B26995E310250568 + +# diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg new file mode 100644 index 00000000000..3e36916024a --- /dev/null +++ b/.kokoro/docs/common.cfg @@ -0,0 +1,61 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-lib-docs" +} +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/publish-docs.sh" +} + +env_vars: { + key: "V2_STAGING_BUCKET" + # Push google cloud library docs to the Cloud RAD bucket `docs-staging-v2` + value: "docs-staging-v2" +} + +# It will upload the docker image after successful builds. +env_vars: { + key: "TRAMPOLINE_IMAGE_UPLOAD" + value: "true" +} + +# It will always build the docker image. +env_vars: { + key: "TRAMPOLINE_DOCKERFILE" + value: ".kokoro/docker/docs/Dockerfile" +} + +# Fetch the token needed for reporting release status to GitHub +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "yoshi-automation-github-key" + } + } +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "docuploader_service_account" + } + } +} diff --git a/.kokoro/docs/docs-presubmit.cfg b/.kokoro/docs/docs-presubmit.cfg new file mode 100644 index 00000000000..43ec87185e4 --- /dev/null +++ b/.kokoro/docs/docs-presubmit.cfg @@ -0,0 +1,23 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "V2_STAGING_BUCKET" + value: "gcloud-python-test" +} + +# We only upload the image in the main `docs` build. +env_vars: { + key: "TRAMPOLINE_IMAGE_UPLOAD" + value: "false" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/build.sh" +} + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "docfx" +} diff --git a/.kokoro/docs/docs.cfg b/.kokoro/docs/docs.cfg new file mode 100644 index 00000000000..18a4c35325b --- /dev/null +++ b/.kokoro/docs/docs.cfg @@ -0,0 +1 @@ +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh new file mode 100644 index 00000000000..3fc3905ac1c --- /dev/null +++ b/.kokoro/publish-docs.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +set -eo pipefail + +# Disable buffering, so that the logs stream through. +export PYTHONUNBUFFERED=1 + +export PATH="${HOME}/.local/bin:${PATH}" + +# Install nox +python3 -m pip install --require-hashes -r .kokoro/requirements.txt +python3 -m nox --version + +# docfx yaml files +nox -s docfx + +# create metadata. +python3 -m docuploader create-metadata \ + --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ + --version=$(python3 setup.py --version) \ + --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ + --distribution-name=$(python3 setup.py --name) \ + --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ + --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ + --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) + +cat docs.metadata + +# Replace toc.yml template file +mv docs/templates/toc.yml docs/_build/html/docfx_yaml/toc.yml + +# upload docs +python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}" diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh new file mode 100644 index 00000000000..59a7cf3a937 --- /dev/null +++ b/.kokoro/trampoline_v2.sh @@ -0,0 +1,487 @@ +#!/usr/bin/env bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# trampoline_v2.sh +# +# This script does 3 things. +# +# 1. Prepare the Docker image for the test +# 2. Run the Docker with appropriate flags to run the test +# 3. Upload the newly built Docker image +# +# in a way that is somewhat compatible with trampoline_v1. +# +# To run this script, first download few files from gcs to /dev/shm. +# (/dev/shm is passed into the container as KOKORO_GFILE_DIR). +# +# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/secrets_viewer_service_account.json /dev/shm +# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/automl_secrets.txt /dev/shm +# +# Then run the script. +# .kokoro/trampoline_v2.sh +# +# These environment variables are required: +# TRAMPOLINE_IMAGE: The docker image to use. +# TRAMPOLINE_DOCKERFILE: The location of the Dockerfile. +# +# You can optionally change these environment variables: +# TRAMPOLINE_IMAGE_UPLOAD: +# (true|false): Whether to upload the Docker image after the +# successful builds. +# TRAMPOLINE_BUILD_FILE: The script to run in the docker container. +# TRAMPOLINE_WORKSPACE: The workspace path in the docker container. +# Defaults to /workspace. +# Potentially there are some repo specific envvars in .trampolinerc in +# the project root. + + +set -euo pipefail + +TRAMPOLINE_VERSION="2.0.5" + +if command -v tput >/dev/null && [[ -n "${TERM:-}" ]]; then + readonly IO_COLOR_RED="$(tput setaf 1)" + readonly IO_COLOR_GREEN="$(tput setaf 2)" + readonly IO_COLOR_YELLOW="$(tput setaf 3)" + readonly IO_COLOR_RESET="$(tput sgr0)" +else + readonly IO_COLOR_RED="" + readonly IO_COLOR_GREEN="" + readonly IO_COLOR_YELLOW="" + readonly IO_COLOR_RESET="" +fi + +function function_exists { + [ $(LC_ALL=C type -t $1)"" == "function" ] +} + +# Logs a message using the given color. The first argument must be one +# of the IO_COLOR_* variables defined above, such as +# "${IO_COLOR_YELLOW}". The remaining arguments will be logged in the +# given color. The log message will also have an RFC-3339 timestamp +# prepended (in UTC). You can disable the color output by setting +# TERM=vt100. +function log_impl() { + local color="$1" + shift + local timestamp="$(date -u "+%Y-%m-%dT%H:%M:%SZ")" + echo "================================================================" + echo "${color}${timestamp}:" "$@" "${IO_COLOR_RESET}" + echo "================================================================" +} + +# Logs the given message with normal coloring and a timestamp. +function log() { + log_impl "${IO_COLOR_RESET}" "$@" +} + +# Logs the given message in green with a timestamp. +function log_green() { + log_impl "${IO_COLOR_GREEN}" "$@" +} + +# Logs the given message in yellow with a timestamp. +function log_yellow() { + log_impl "${IO_COLOR_YELLOW}" "$@" +} + +# Logs the given message in red with a timestamp. +function log_red() { + log_impl "${IO_COLOR_RED}" "$@" +} + +readonly tmpdir=$(mktemp -d -t ci-XXXXXXXX) +readonly tmphome="${tmpdir}/h" +mkdir -p "${tmphome}" + +function cleanup() { + rm -rf "${tmpdir}" +} +trap cleanup EXIT + +RUNNING_IN_CI="${RUNNING_IN_CI:-false}" + +# The workspace in the container, defaults to /workspace. +TRAMPOLINE_WORKSPACE="${TRAMPOLINE_WORKSPACE:-/workspace}" + +pass_down_envvars=( + # TRAMPOLINE_V2 variables. + # Tells scripts whether they are running as part of CI or not. + "RUNNING_IN_CI" + # Indicates which CI system we're in. + "TRAMPOLINE_CI" + # Indicates the version of the script. + "TRAMPOLINE_VERSION" +) + +log_yellow "Building with Trampoline ${TRAMPOLINE_VERSION}" + +# Detect which CI systems we're in. If we're in any of the CI systems +# we support, `RUNNING_IN_CI` will be true and `TRAMPOLINE_CI` will be +# the name of the CI system. Both envvars will be passing down to the +# container for telling which CI system we're in. +if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then + # descriptive env var for indicating it's on CI. + RUNNING_IN_CI="true" + TRAMPOLINE_CI="kokoro" + if [[ "${TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT:-}" == "true" ]]; then + if [[ ! -f "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" ]]; then + log_red "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json does not exist. Did you forget to mount cloud-devrel-kokoro-resources/trampoline? Aborting." + exit 1 + fi + # This service account will be activated later. + TRAMPOLINE_SERVICE_ACCOUNT="${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" + else + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + gcloud auth list + fi + log_yellow "Configuring Container Registry access" + gcloud auth configure-docker --quiet + fi + pass_down_envvars+=( + # KOKORO dynamic variables. + "KOKORO_BUILD_NUMBER" + "KOKORO_BUILD_ID" + "KOKORO_JOB_NAME" + "KOKORO_GIT_COMMIT" + "KOKORO_GITHUB_COMMIT" + "KOKORO_GITHUB_PULL_REQUEST_NUMBER" + "KOKORO_GITHUB_PULL_REQUEST_COMMIT" + # For FlakyBot + "KOKORO_GITHUB_COMMIT_URL" + "KOKORO_GITHUB_PULL_REQUEST_URL" + ) +elif [[ "${TRAVIS:-}" == "true" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="travis" + pass_down_envvars+=( + "TRAVIS_BRANCH" + "TRAVIS_BUILD_ID" + "TRAVIS_BUILD_NUMBER" + "TRAVIS_BUILD_WEB_URL" + "TRAVIS_COMMIT" + "TRAVIS_COMMIT_MESSAGE" + "TRAVIS_COMMIT_RANGE" + "TRAVIS_JOB_NAME" + "TRAVIS_JOB_NUMBER" + "TRAVIS_JOB_WEB_URL" + "TRAVIS_PULL_REQUEST" + "TRAVIS_PULL_REQUEST_BRANCH" + "TRAVIS_PULL_REQUEST_SHA" + "TRAVIS_PULL_REQUEST_SLUG" + "TRAVIS_REPO_SLUG" + "TRAVIS_SECURE_ENV_VARS" + "TRAVIS_TAG" + ) +elif [[ -n "${GITHUB_RUN_ID:-}" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="github-workflow" + pass_down_envvars+=( + "GITHUB_WORKFLOW" + "GITHUB_RUN_ID" + "GITHUB_RUN_NUMBER" + "GITHUB_ACTION" + "GITHUB_ACTIONS" + "GITHUB_ACTOR" + "GITHUB_REPOSITORY" + "GITHUB_EVENT_NAME" + "GITHUB_EVENT_PATH" + "GITHUB_SHA" + "GITHUB_REF" + "GITHUB_HEAD_REF" + "GITHUB_BASE_REF" + ) +elif [[ "${CIRCLECI:-}" == "true" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="circleci" + pass_down_envvars+=( + "CIRCLE_BRANCH" + "CIRCLE_BUILD_NUM" + "CIRCLE_BUILD_URL" + "CIRCLE_COMPARE_URL" + "CIRCLE_JOB" + "CIRCLE_NODE_INDEX" + "CIRCLE_NODE_TOTAL" + "CIRCLE_PREVIOUS_BUILD_NUM" + "CIRCLE_PROJECT_REPONAME" + "CIRCLE_PROJECT_USERNAME" + "CIRCLE_REPOSITORY_URL" + "CIRCLE_SHA1" + "CIRCLE_STAGE" + "CIRCLE_USERNAME" + "CIRCLE_WORKFLOW_ID" + "CIRCLE_WORKFLOW_JOB_ID" + "CIRCLE_WORKFLOW_UPSTREAM_JOB_IDS" + "CIRCLE_WORKFLOW_WORKSPACE_ID" + ) +fi + +# Configure the service account for pulling the docker image. +function repo_root() { + local dir="$1" + while [[ ! -d "${dir}/.git" ]]; do + dir="$(dirname "$dir")" + done + echo "${dir}" +} + +# Detect the project root. In CI builds, we assume the script is in +# the git tree and traverse from there, otherwise, traverse from `pwd` +# to find `.git` directory. +if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + PROGRAM_PATH="$(realpath "$0")" + PROGRAM_DIR="$(dirname "${PROGRAM_PATH}")" + PROJECT_ROOT="$(repo_root "${PROGRAM_DIR}")" +else + PROJECT_ROOT="$(repo_root $(pwd))" +fi + +log_yellow "Changing to the project root: ${PROJECT_ROOT}." +cd "${PROJECT_ROOT}" + +# To support relative path for `TRAMPOLINE_SERVICE_ACCOUNT`, we need +# to use this environment variable in `PROJECT_ROOT`. +if [[ -n "${TRAMPOLINE_SERVICE_ACCOUNT:-}" ]]; then + + mkdir -p "${tmpdir}/gcloud" + gcloud_config_dir="${tmpdir}/gcloud" + + log_yellow "Using isolated gcloud config: ${gcloud_config_dir}." + export CLOUDSDK_CONFIG="${gcloud_config_dir}" + + log_yellow "Using ${TRAMPOLINE_SERVICE_ACCOUNT} for authentication." + gcloud auth activate-service-account \ + --key-file "${TRAMPOLINE_SERVICE_ACCOUNT}" + log_yellow "Configuring Container Registry access" + gcloud auth configure-docker --quiet +fi + +required_envvars=( + # The basic trampoline configurations. + "TRAMPOLINE_IMAGE" + "TRAMPOLINE_BUILD_FILE" +) + +if [[ -f "${PROJECT_ROOT}/.trampolinerc" ]]; then + source "${PROJECT_ROOT}/.trampolinerc" +fi + +log_yellow "Checking environment variables." +for e in "${required_envvars[@]}" +do + if [[ -z "${!e:-}" ]]; then + log "Missing ${e} env var. Aborting." + exit 1 + fi +done + +# We want to support legacy style TRAMPOLINE_BUILD_FILE used with V1 +# script: e.g. "github/repo-name/.kokoro/run_tests.sh" +TRAMPOLINE_BUILD_FILE="${TRAMPOLINE_BUILD_FILE#github/*/}" +log_yellow "Using TRAMPOLINE_BUILD_FILE: ${TRAMPOLINE_BUILD_FILE}" + +# ignore error on docker operations and test execution +set +e + +log_yellow "Preparing Docker image." +# We only download the docker image in CI builds. +if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + # Download the docker image specified by `TRAMPOLINE_IMAGE` + + # We may want to add --max-concurrent-downloads flag. + + log_yellow "Start pulling the Docker image: ${TRAMPOLINE_IMAGE}." + if docker pull "${TRAMPOLINE_IMAGE}"; then + log_green "Finished pulling the Docker image: ${TRAMPOLINE_IMAGE}." + has_image="true" + else + log_red "Failed pulling the Docker image: ${TRAMPOLINE_IMAGE}." + has_image="false" + fi +else + # For local run, check if we have the image. + if docker images "${TRAMPOLINE_IMAGE}:latest" | grep "${TRAMPOLINE_IMAGE}"; then + has_image="true" + else + has_image="false" + fi +fi + + +# The default user for a Docker container has uid 0 (root). To avoid +# creating root-owned files in the build directory we tell docker to +# use the current user ID. +user_uid="$(id -u)" +user_gid="$(id -g)" +user_name="$(id -un)" + +# To allow docker in docker, we add the user to the docker group in +# the host os. +docker_gid=$(cut -d: -f3 < <(getent group docker)) + +update_cache="false" +if [[ "${TRAMPOLINE_DOCKERFILE:-none}" != "none" ]]; then + # Build the Docker image from the source. + context_dir=$(dirname "${TRAMPOLINE_DOCKERFILE}") + docker_build_flags=( + "-f" "${TRAMPOLINE_DOCKERFILE}" + "-t" "${TRAMPOLINE_IMAGE}" + "--build-arg" "UID=${user_uid}" + "--build-arg" "USERNAME=${user_name}" + ) + if [[ "${has_image}" == "true" ]]; then + docker_build_flags+=("--cache-from" "${TRAMPOLINE_IMAGE}") + fi + + log_yellow "Start building the docker image." + if [[ "${TRAMPOLINE_VERBOSE:-false}" == "true" ]]; then + echo "docker build" "${docker_build_flags[@]}" "${context_dir}" + fi + + # ON CI systems, we want to suppress docker build logs, only + # output the logs when it fails. + if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + if docker build "${docker_build_flags[@]}" "${context_dir}" \ + > "${tmpdir}/docker_build.log" 2>&1; then + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + cat "${tmpdir}/docker_build.log" + fi + + log_green "Finished building the docker image." + update_cache="true" + else + log_red "Failed to build the Docker image, aborting." + log_yellow "Dumping the build logs:" + cat "${tmpdir}/docker_build.log" + exit 1 + fi + else + if docker build "${docker_build_flags[@]}" "${context_dir}"; then + log_green "Finished building the docker image." + update_cache="true" + else + log_red "Failed to build the Docker image, aborting." + exit 1 + fi + fi +else + if [[ "${has_image}" != "true" ]]; then + log_red "We do not have ${TRAMPOLINE_IMAGE} locally, aborting." + exit 1 + fi +fi + +# We use an array for the flags so they are easier to document. +docker_flags=( + # Remove the container after it exists. + "--rm" + + # Use the host network. + "--network=host" + + # Run in priviledged mode. We are not using docker for sandboxing or + # isolation, just for packaging our dev tools. + "--privileged" + + # Run the docker script with the user id. Because the docker image gets to + # write in ${PWD} you typically want this to be your user id. + # To allow docker in docker, we need to use docker gid on the host. + "--user" "${user_uid}:${docker_gid}" + + # Pass down the USER. + "--env" "USER=${user_name}" + + # Mount the project directory inside the Docker container. + "--volume" "${PROJECT_ROOT}:${TRAMPOLINE_WORKSPACE}" + "--workdir" "${TRAMPOLINE_WORKSPACE}" + "--env" "PROJECT_ROOT=${TRAMPOLINE_WORKSPACE}" + + # Mount the temporary home directory. + "--volume" "${tmphome}:/h" + "--env" "HOME=/h" + + # Allow docker in docker. + "--volume" "/var/run/docker.sock:/var/run/docker.sock" + + # Mount the /tmp so that docker in docker can mount the files + # there correctly. + "--volume" "/tmp:/tmp" + # Pass down the KOKORO_GFILE_DIR and KOKORO_KEYSTORE_DIR + # TODO(tmatsuo): This part is not portable. + "--env" "TRAMPOLINE_SECRET_DIR=/secrets" + "--volume" "${KOKORO_GFILE_DIR:-/dev/shm}:/secrets/gfile" + "--env" "KOKORO_GFILE_DIR=/secrets/gfile" + "--volume" "${KOKORO_KEYSTORE_DIR:-/dev/shm}:/secrets/keystore" + "--env" "KOKORO_KEYSTORE_DIR=/secrets/keystore" +) + +# Add an option for nicer output if the build gets a tty. +if [[ -t 0 ]]; then + docker_flags+=("-it") +fi + +# Passing down env vars +for e in "${pass_down_envvars[@]}" +do + if [[ -n "${!e:-}" ]]; then + docker_flags+=("--env" "${e}=${!e}") + fi +done + +# If arguments are given, all arguments will become the commands run +# in the container, otherwise run TRAMPOLINE_BUILD_FILE. +if [[ $# -ge 1 ]]; then + log_yellow "Running the given commands '" "${@:1}" "' in the container." + readonly commands=("${@:1}") + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}" + fi + docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}" +else + log_yellow "Running the tests in a Docker container." + docker_flags+=("--entrypoint=${TRAMPOLINE_BUILD_FILE}") + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" + fi + docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" +fi + + +test_retval=$? + +if [[ ${test_retval} -eq 0 ]]; then + log_green "Build finished with ${test_retval}" +else + log_red "Build finished with ${test_retval}" +fi + +# Only upload it when the test passes. +if [[ "${update_cache}" == "true" ]] && \ + [[ $test_retval == 0 ]] && \ + [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]]; then + log_yellow "Uploading the Docker image." + if docker push "${TRAMPOLINE_IMAGE}"; then + log_green "Finished uploading the Docker image." + else + log_red "Failed uploading the Docker image." + fi + # Call trampoline_after_upload_hook if it's defined. + if function_exists trampoline_after_upload_hook; then + trampoline_after_upload_hook + fi + +fi + +exit "${test_retval}" diff --git a/.trampolinerc b/.trampolinerc new file mode 100644 index 00000000000..4c16e33fee0 --- /dev/null +++ b/.trampolinerc @@ -0,0 +1,58 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Add required env vars here. +required_envvars+=( +) + +# Add env vars which are passed down into the container here. +pass_down_envvars+=( + "NOX_SESSION" + ############### + "V2_STAGING_BUCKET" + ################## + # Samples builds + ################## + "INSTALL_LIBRARY_FROM_SOURCE" + "RUN_TESTS_SESSION" + "BUILD_SPECIFIC_GCLOUD_PROJECT" + # Target directories. + "RUN_TESTS_DIRS" + # The nox session to run. + "RUN_TESTS_SESSION" +) + +# Prevent unintentional override on the default image. +if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && \ + [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then + echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image." + exit 1 +fi + +# Define the default value if it makes sense. +if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then + TRAMPOLINE_IMAGE_UPLOAD="" +fi + +if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then + TRAMPOLINE_IMAGE="" +fi + +if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then + TRAMPOLINE_DOCKERFILE="" +fi + +if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then + TRAMPOLINE_BUILD_FILE="" +fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 19bf3a117b6..9ab2b05d571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ [1]: https://pypi.org/project/bigframes/#history +## 0.2.0 (2023-08-17) + +### Features +* Add KMeans.cluster_centers_. +* Allow column labels to be any type handled by bq df, column labels can be integers now. +* Add dataframegroupby.agg(). +* Add Series Property is_monotonic_increasing and is_monotonic_decreasing. +* Add match, fullmatch, get, pad str methods. +* Add series isin function. + +### Bug Fixes +* Update ML package to use sessions for queries. +* Optimize `read_gbq` with `index_col` set to cluster by `index_col`. +* Raise ValueError if the location mismatched. +* `read_gbq` no longer uses 'time travel' with query inputs. + +### Documentation +* Add docstring to _uniform_sampling to avoid user using it. + ## 0.1.1 (2023-08-14) ### Documentation diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 075e27b0c23..9f392ce1492 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -304,6 +304,11 @@ def reversed(self) -> ArrayValue: return expr_builder.build() def _uniform_sampling(self, fraction: float) -> ArrayValue: + """Sampling the table on given fraction. + + .. warning:: + The row numbers of result is non-deterministic, avoid to use. + """ table = self.to_ibis_expr( ordering_mode="order_by", expose_hidden_cols=True, fraction=fraction ) @@ -849,7 +854,7 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = Fal def unpivot_single_row( self, - row_labels: typing.Sequence[typing.Optional[str]], + row_labels: typing.Sequence[typing.Hashable], unpivot_columns: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], *, index_col_id: str = "index", diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 5ef92630727..f696b8287bc 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -47,7 +47,7 @@ import bigframes.operations.aggregations as agg_ops # Type constraint for wherever column labels are used -Label = typing.Optional[str] +Label = typing.Hashable # Bytes to Megabyte Conversion _BYTES_TO_KILOBYTES = 1024 @@ -585,8 +585,8 @@ def with_column_labels(self, value: typing.Iterable[Label]) -> Block: def with_index_labels(self, value: typing.Sequence[Label]) -> Block: if len(value) != len(self.index_columns): raise ValueError( - f"The index labels size `{len(value)} ` should equal to the index" - + f"columns size: {len(self.value_columns)}." + f"The index labels size `{len(value)} ` should equal to the index " + + f"columns size: {len(self.index_columns)}." ) return Block( self._expr, @@ -1126,10 +1126,6 @@ def block_from_local(data, session=None, use_index=True) -> Block: f"multi-indices not supported. {constants.FEEDBACK_LINK}" ) index_label = pd_data.index.name - if (index_label is not None) and (not isinstance(index_label, str)): - raise NotImplementedError( - f"Only string index names supported. {constants.FEEDBACK_LINK}" - ) index_id = guid.generate_guid() pd_data = pd_data.reset_index(names=index_id) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 288dcdd5b0b..10bee4f56d9 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -15,14 +15,13 @@ from __future__ import annotations import typing - -import pandas as pd -import typing_extensions +import warnings import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.ordering as order +import bigframes.core.utils as utils import bigframes.core.window as windows import bigframes.dataframe as df import bigframes.dtypes as dtypes @@ -75,7 +74,7 @@ def __getitem__( typing.Sequence[blocks.Label], ], ): - if _is_list_like(key): + if utils.is_list_like(key): keys = list(key) else: keys = [key] @@ -168,6 +167,76 @@ def cummax(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: def cumprod(self, *args, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.product_op, numeric_only=True) + def agg(self, func=None, **kwargs) -> df.DataFrame: + column_labels = [] + if func: + warnings.warn( + "DataFrameGroupby aggregate produces single-level column labels only currently. Subject to change in future versions." + ) + if isinstance(func, str): + aggregations = [ + (col_id, agg_ops.AGGREGATIONS_LOOKUP[func]) + for col_id in self._aggregated_columns() + ] + elif utils.is_dict_like(func): + aggregations = [] + for label, funcs_for_id in func.items(): + col_id = self._resolve_label(label) + func_list = ( + funcs_for_id + if utils.is_list_like(funcs_for_id) + else [funcs_for_id] + ) + for f in func_list: + aggregations.append((col_id, agg_ops.AGGREGATIONS_LOOKUP[f])) + # Pandas creates multi-index here instead + column_labels.append(f"{label}_{f}") + elif utils.is_list_like(func): + aggregations = [ + (col_id, agg_ops.AGGREGATIONS_LOOKUP[f]) + for col_id in self._aggregated_columns() + for f in func + ] + column_labels = [ + f"{self._block.col_id_to_label[col_id]}_{f}" + for col_id in self._aggregated_columns() + for f in func + ] + else: + raise NotImplementedError( + f"Aggregate with {func} not supported. {constants.FEEDBACK_LINK}" + ) + else: + aggregations = [] + for k, v in kwargs.items(): + if not isinstance(k, str): + raise NotImplementedError( + f"Only string aggregate names supported. {constants.FEEDBACK_LINK}" + ) + if not hasattr(v, "column") or not hasattr(v, "aggfunc"): + import bigframes.pandas as bpd + + raise NotImplementedError( + f"kwargs values must be {bpd.NamedAgg.__qualname__}" + ) + col_id = self._resolve_label(v.column) + aggregations.append((col_id, agg_ops.AGGREGATIONS_LOOKUP[v.aggfunc])) + column_labels.append(k) + + agg_block, _ = self._block.aggregate( + by_column_ids=self._by_col_ids, + aggregations=aggregations, + as_index=self._as_index, + dropna=self._dropna, + ) + + if column_labels: + agg_block = agg_block.with_column_labels(column_labels) + + return df.DataFrame(agg_block) + + aggregate = agg + def _raise_on_non_numeric(self, op: str): if not all( dtype in dtypes.NUMERIC_BIGFRAMES_TYPES for dtype in self._block.dtypes @@ -222,6 +291,15 @@ def _apply_window_op( block = block.select_columns(columns) return df.DataFrame(block) + def _resolve_label(self, label: blocks.Label) -> str: + """Resolve label to column id.""" + col_ids = self._block.label_to_col_id.get(label, ()) + if len(col_ids) > 1: + raise ValueError(f"Label {label} is ambiguous") + if len(col_ids) == 0: + raise ValueError(f"Label {label} does not match any columns") + return col_ids[0] + class SeriesGroupBy(vendored_pandas_groupby.SeriesGroupBy): __doc__ = vendored_pandas_groupby.GroupBy.__doc__ @@ -231,7 +309,7 @@ def __init__( block: blocks.Block, value_column: str, by_col_ids: typing.Sequence[str], - value_name: typing.Optional[str] = None, + value_name: blocks.Label = None, dropna=True, ): # TODO(tbergeron): Support more group-by expression types @@ -278,6 +356,36 @@ def var(self, *args, **kwargs) -> series.Series: def prod(self, *args) -> series.Series: return self._aggregate(agg_ops.product_op) + def agg(self, func=None) -> typing.Union[df.DataFrame, series.Series]: + column_names: list[str] = [] + if isinstance(func, str): + aggregations = [(self._value_column, agg_ops.AGGREGATIONS_LOOKUP[func])] + column_names = [func] + elif utils.is_list_like(func): + aggregations = [ + (self._value_column, agg_ops.AGGREGATIONS_LOOKUP[f]) for f in func + ] + column_names = list(func) + else: + raise NotImplementedError( + f"Aggregate with {func} not supported. {constants.FEEDBACK_LINK}" + ) + + agg_block, _ = self._block.aggregate( + by_column_ids=self._by_col_ids, + aggregations=aggregations, + dropna=self._dropna, + ) + + if column_names: + agg_block = agg_block.with_column_labels(column_names) + + if len(aggregations) > 1: + return df.DataFrame(agg_block) + return series.Series(agg_block) + + aggregate = agg + def cumsum(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.sum_op, @@ -374,7 +482,3 @@ def _apply_window_op( skip_null_groups=self._dropna, ) return series.Series(block.select_column(result_id)) - - -def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: - return pd.api.types.is_list_like(obj) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index bb1fcc4b175..4eb37e6d926 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -37,7 +37,7 @@ def __init__(self, data: blocks.BlockHolder): self._data = data @property - def name(self) -> typing.Optional[str]: + def name(self) -> blocks.Label: return self.names[0] @name.setter @@ -105,11 +105,11 @@ def _expr(self) -> core.ArrayValue: return self._block.expr @property - def name(self) -> typing.Optional[str]: + def name(self) -> blocks.Label: return self._block._index_labels[0] @property - def names(self) -> typing.Sequence[typing.Optional[str]]: + def names(self) -> typing.Sequence[blocks.Label]: return self._block._index_labels @property diff --git a/bigframes/core/io.py b/bigframes/core/io.py index 6b7ed52488d..3c2e5a25f5a 100644 --- a/bigframes/core/io.py +++ b/bigframes/core/io.py @@ -75,6 +75,11 @@ def create_snapshot_sql( if table_ref.dataset_id.upper() == "_SESSION": return f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" + # If we have an anonymous query results table, it can't be modified and + # there isn't any BigQuery time travel. + if table_ref.dataset_id.startswith("_"): + return f"SELECT * FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}`" + return textwrap.dedent( f""" SELECT * diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 8ad58fb166e..a3300029054 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -13,6 +13,9 @@ # limitations under the License. import typing +import pandas as pd +import typing_extensions + def get_axis_number(axis: typing.Union[str, int, None]) -> typing.Literal[0, 1]: if axis in {0, "index", "rows", None}: @@ -20,3 +23,11 @@ def get_axis_number(axis: typing.Union[str, int, None]) -> typing.Literal[0, 1]: elif axis in {1, "columns"}: return 1 raise ValueError(f"Not a valid axis: {axis}") + + +def is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: + return pd.api.types.is_list_like(obj) + + +def is_dict_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Mapping]: + return pd.api.types.is_dict_like(obj) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index cb062f09249..d4b6e470255 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -34,7 +34,6 @@ import google.cloud.bigquery as bigquery import numpy import pandas -import typing_extensions import bigframes import bigframes._config.display_options as display_options @@ -114,7 +113,7 @@ def __init__( # Dict of Series elif ( - _is_dict_like(data) + utils.is_dict_like(data) and len(data) >= 1 and any(isinstance(data[key], bf_series.Series) for key in data.keys()) ): @@ -178,7 +177,13 @@ def __dir__(self): return dir(type(self)) + self._block.column_labels def _ipython_key_completions_(self) -> List[str]: - return list([label for label in self._block.column_labels if label]) + return list( + [ + label + for label in self._block.column_labels + if label and isinstance(label, str) + ] + ) def _find_indices( self, @@ -210,7 +215,7 @@ def _sql_names( tolerance: bool = False, ) -> Sequence[str]: """Retrieve sql name (column name in BQ schema) of column(s).""" - labels = columns if _is_list_like(columns) else [columns] # type:ignore + labels = columns if utils.is_list_like(columns) else [columns] # type:ignore results: Sequence[str] = [] for label in labels: col_ids = self._block.label_to_col_id.get(label, []) @@ -303,13 +308,13 @@ def _to_sql_query( index_labels = [] unnamed_index_count = 0 for index_label in self._block.index_labels: - if index_label is None: + if isinstance(index_label, str): + index_labels.append(index_label) + else: index_labels.append( indexes.INDEX_COLUMN_ID.format(unnamed_index_count), ) unnamed_index_count += 1 - else: - index_labels.append(index_label) column_labels = column_labels + typing.cast( List[Optional[str]], index_labels @@ -365,22 +370,6 @@ def query_job(self) -> Optional[bigquery.QueryJob]: def _set_internal_query_job(self, query_job: bigquery.QueryJob): self._query_job = query_job - @typing.overload - def __getitem__(self, key: bigframes.series.Series) -> DataFrame: - ... - - @typing.overload - def __getitem__(self, key: Sequence[blocks.Label]) -> DataFrame: # type:ignore - ... - - @typing.overload - def __getitem__(self, key: pandas.Index) -> DataFrame: # type:ignore - ... - - @typing.overload - def __getitem__(self, key: blocks.Label) -> bigframes.series.Series: # type:ignore - ... - def __getitem__( self, key: Union[ @@ -390,7 +379,7 @@ def __getitem__( pandas.Index, bigframes.series.Series, ], - ) -> Union[bigframes.series.Series, "DataFrame"]: + ): # No return type annotations (like pandas) as type cannot always be determined statically """Gets the specified column(s) from the DataFrame.""" # NOTE: This implements the operations described in # https://pandas.pydata.org/docs/getting_started/intro_tutorials/03_subset_data.html @@ -399,8 +388,8 @@ def __getitem__( return self._getitem_bool_series(key) sql_names = self._sql_names(key) - # Only input is a str and only find one column, returns a Series - if isinstance(key, str) and len(sql_names) == 1: + # Only input is a single key and only find one column, returns a Series + if (not utils.is_list_like(key)) and len(sql_names) == 1: return bigframes.series.Series(self._block.select_column(sql_names[0])) # Select a subset of columns or re-order columns. @@ -417,7 +406,7 @@ def __getitem__( # projection? # Select a number of columns as DF. - key = key if _is_list_like(key) else [key] # type:ignore + key = key if utils.is_list_like(key) else [key] # type:ignore selected_ids: Tuple[str, ...] = () for label in key: @@ -763,7 +752,7 @@ def drop( if index: level_id = self._resolve_levels(level or 0)[0] - if _is_list_like(index): + if utils.is_list_like(index): block, inverse_condition_id = block.apply_unary_op( level_id, ops.IsInOp(index, match_nulls=True) ) @@ -778,7 +767,7 @@ def drop( self._block.value_columns ) if columns: - if not _is_list_like(columns): + if not utils.is_list_like(columns): columns = [columns] # type:ignore columns = list(columns) @@ -796,7 +785,7 @@ def reorder_levels(self, order: LevelsType): return DataFrame(self._block.reorder_levels(resolved_level_ids)) def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: - if _is_list_like(level): + if utils.is_list_like(level): levels = list(level) else: levels = [level] @@ -804,7 +793,7 @@ def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: for level_ref in levels: if isinstance(level_ref, int): resolved_level_ids.append(self._block.index_columns[level_ref]) - elif isinstance(level_ref, str): + elif isinstance(level_ref, typing.Hashable): matching_ids = self._block.index_name_to_col_id.get(level_ref, []) if len(matching_ids) != 1: raise ValueError("level name cannot be found or is ambiguous") @@ -827,7 +816,7 @@ def rename_axis( f"rename_axis does not currently support any keyword arguments. {constants.FEEDBACK_LINK}" ) # limited implementation: the new index name is simply the 'mapper' parameter - if _is_list_like(mapper): + if utils.is_list_like(mapper): labels = mapper else: labels = [mapper] @@ -909,7 +898,7 @@ def set_index( append: bool = False, drop: bool = True, ) -> DataFrame: - if not _is_list_like(keys): + if not utils.is_list_like(keys): keys = typing.cast(typing.Sequence[blocks.Label], (keys,)) else: keys = typing.cast(typing.Sequence[blocks.Label], tuple(keys)) @@ -943,7 +932,7 @@ def sort_values( if na_position not in {"first", "last"}: raise ValueError("Param na_position must be one of 'first' or 'last'") - sort_labels = (by,) if isinstance(by, str) else tuple(by) + sort_labels = tuple(by) if utils.is_list_like(by) else (by,) sort_column_ids = self._sql_names(sort_labels) len_by = len(sort_labels) @@ -1118,7 +1107,7 @@ def nunique(self) -> bigframes.series.Series: def agg( self, func: str | typing.Sequence[str] ) -> DataFrame | bigframes.series.Series: - if _is_list_like(func): + if utils.is_list_like(func): if any( dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES for dtype in self.dtypes @@ -1410,7 +1399,7 @@ def _groupby_series( as_index: bool = True, dropna: bool = True, ): - if not isinstance(by, bigframes.series.Series) and _is_list_like(by): + if not isinstance(by, bigframes.series.Series) and utils.is_list_like(by): by = list(by) else: by = [typing.cast(typing.Union[blocks.Label, bigframes.series.Series], by)] @@ -1704,7 +1693,7 @@ def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: id_overrides = { col_id: col_label for col_id, col_label in zip(columns, column_labels) - if col_label + if col_label and isinstance(col_label, str) } if ordering_id is not None: @@ -1766,7 +1755,7 @@ def drop_duplicates( ) -> DataFrame: if subset is None: column_ids = self._block.value_columns - elif _is_list_like(subset): + elif utils.is_list_like(subset): column_ids = [ id for label in subset for id in self._block.label_to_col_id[label] ] @@ -1817,11 +1806,3 @@ def _set_block(self, block: blocks.Block): def _get_block(self) -> blocks.Block: return self._block - - -def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: - return pandas.api.types.is_list_like(obj) - - -def _is_dict_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Mapping]: - return pandas.api.types.is_dict_like(obj) diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index 2c3c50ba754..82e2510e2af 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -27,7 +27,9 @@ import bigframes.constants as constants -GenericJob = Union[bigquery.LoadJob, bigquery.ExtractJob, bigquery.QueryJob] +GenericJob = Union[ + bigquery.LoadJob, bigquery.ExtractJob, bigquery.QueryJob, bigquery.CopyJob +] query_job_prop_pairs = { "Job Id": "job_id", @@ -49,7 +51,7 @@ def add_feedback_link( def repr_query_job_html(query_job: Optional[bigquery.QueryJob]): """Return query job in html format. Args: - query_job: + query_job (bigquery.QueryJob, Optional): The job representing the execution of the query on the server. Returns: Pywidget html table. @@ -114,10 +116,12 @@ def wait_for_query_job( ) -> bigquery.table.RowIterator: """Return query results. Displays a progress bar while the query is running Args: - query_job: + query_job (bigquery.QueryJob, Optional): The job representing the execution of the query on the server. - max_results: + max_results (int, Optional): The maximum number of rows the row iterator should return. + progress_bar (str, Optional): + Which progress bar to show. Returns: A row iterator over the query results. """ @@ -154,8 +158,10 @@ def wait_for_query_job( def wait_for_job(job: GenericJob, progress_bar: Optional[str] = None): """Waits for job results. Displays a progress bar while the job is running Args: - job: - The bigquery job to be executed + job (GenericJob): + The bigquery job to be executed. + progress_bar (str, Optional): + Which progress bar to show. """ loading_bar = widgets.HTML(get_base_job_loading_html(job)) if progress_bar == "auto": @@ -189,7 +195,7 @@ def wait_for_job(job: GenericJob, progress_bar: Optional[str] = None): def get_job_url(query_job: GenericJob): """Return url to the query job in cloud console. Args: - query_job: + query_job (GenericJob): The job representing the execution of the query on the server. Returns: String url. @@ -206,7 +212,7 @@ def get_job_url(query_job: GenericJob): def get_query_job_loading_html(query_job: bigquery.QueryJob): """Return progress bar html string Args: - query_job: + query_job (bigquery.QueryJob): The job representing the execution of the query on the server. Returns: Html string. @@ -217,7 +223,7 @@ def get_query_job_loading_html(query_job: bigquery.QueryJob): def get_query_job_loading_string(query_job: bigquery.QueryJob): """Return progress bar string Args: - query_job: + query_job (bigquery.QueryJob): The job representing the execution of the query on the server. Returns: String @@ -228,7 +234,7 @@ def get_query_job_loading_string(query_job: bigquery.QueryJob): def get_base_job_loading_html(job: GenericJob): """Return progress bar html string Args: - job: + job (GenericJob): The job representing the execution of the query on the server. Returns: Html string. @@ -239,7 +245,7 @@ def get_base_job_loading_html(job: GenericJob): def get_base_job_loading_string(job: GenericJob): """Return progress bar string Args: - job: + job (GenericJob): The job representing the execution of the query on the server. Returns: String @@ -250,8 +256,8 @@ def get_base_job_loading_string(job: GenericJob): def get_formatted_time(val): """Try to format time Args: - val: - Time in ms + val (Any): + Time in ms. Returns: Duration string """ diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 6f6efb9f376..9a20fd9ad74 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -73,6 +73,15 @@ def fit( ) return self + @property + def cluster_centers_(self) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError( + "A model must be fitted before calling cluster_centers_." + ) + + return self._bqml_model.centroids() + def predict( self, X: Union[bpd.DataFrame, bpd.Series], diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 9629ca0f4d5..812bb08dc38 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -159,14 +159,22 @@ def evaluate(self, input_data: Optional[bpd.DataFrame] = None): return self._session.read_gbq(sql) + def centroids(self): + assert self._model.model_type == "KMEANS" + + sql = ml_sql.ml_centroids(self.model_name) + + return self._session.read_gbq(sql) + def copy(self, new_model_name: str, replace: bool = False) -> BqmlModel: job_config = bigquery.job.CopyJobConfig() if replace: job_config.write_disposition = "WRITE_TRUNCATE" - self._session.bqclient.copy_table( + copy_job = self._session.bqclient.copy_table( self.model_name, new_model_name, job_config=job_config - ).result() + ) + self._session._start_generic_job(copy_job) new_model = self._session.bqclient.get_model(new_model_name) return BqmlModel(self._session, new_model) @@ -182,7 +190,7 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: options_sql = ml_sql.options(**{"vertex_ai_model_id": vertex_ai_model_id}) sql = ml_sql.alter_model(self.model_name, options_sql=options_sql) # Register the model and wait it to finish - self._session.bqclient.query(sql).result() + self._session._start_query(sql) self._model = self._session.bqclient.get_model(self.model_name) return self @@ -322,8 +330,7 @@ def _create_temp_model_name() -> str: def _create_bqml_model_with_sql(session: bigframes.Session, sql: str) -> BqmlModel: # fit the model, synchronously - job = session.bqclient.query(sql) - job.result() + _, job = session._start_query(sql) # real model path in the session specific hidden dataset and table prefix model_name_full = f"{job.destination.dataset_id}.{job.destination.table_id}" diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index b8d9e2c6737..80054d40e19 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -146,6 +146,13 @@ def ml_evaluate(model_name: str, source_sql: Union[str, None] = None) -> str: ({source_sql}))""" +def ml_centroids( + model_name: str, +) -> str: + """Encode ML.CENTROIDS for BQML""" + return f"""SELECT * FROM ML.CENTROIDS(MODEL `{model_name}`)""" + + def ml_predict(model_name: str, source_sql: str) -> str: """Encode ML.PREDICT for BQML""" return f"""SELECT * FROM ML.PREDICT(MODEL `{model_name}`, diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 13063af75f9..7bdd97812e9 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -134,6 +134,42 @@ def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.StringValue, x).re_search(self._pat) +class StrGetOp(UnaryOp): + def __init__(self, i: int): + self._i = i + + def _as_ibis(self, x: ibis_types.Value): + substr = typing.cast( + ibis_types.StringValue, typing.cast(ibis_types.StringValue, x)[self._i] + ) + return substr.nullif(ibis_types.literal("")) + + +class StrPadOp(UnaryOp): + def __init__( + self, length: int, fillchar: str, side: typing.Literal["both", "left", "right"] + ): + self._length = length + self._fillchar = fillchar + self._side = side + + def _as_ibis(self, x: ibis_types.Value): + str_val = typing.cast(ibis_types.StringValue, x) + + # SQL pad operations will truncate, we do not want to truncate though. + pad_length = ibis.greatest(str_val.length(), self._length) + if self._side == "left": + return str_val.lpad(pad_length, self._fillchar) + elif self._side == "right": + return str_val.rpad(pad_length, self._fillchar) + else: # side == both + # Pad more on right side if can't pad both sides equally + lpad_amount = ((pad_length - str_val.length()) // 2) + str_val.length() + return str_val.lpad(lpad_amount, self._fillchar).rpad( + pad_length, self._fillchar + ) + + class ReplaceStringOp(UnaryOp): def __init__(self, pat: str, repl: str): self._pat = pat diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index caef33919b3..361fdca0558 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -63,9 +63,9 @@ def __init__( if block: if name: - if not isinstance(name, str): - raise NotImplementedError( - f"BigQuery DataFrames only supports string series names. {constants.FEEDBACK_LINK}" + if not isinstance(name, typing.Hashable): + raise ValueError( + f"BigQuery DataFrames only supports hashable series names. {constants.FEEDBACK_LINK}" ) block = block.with_column_labels([name]) if index: diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index a16ecb0d328..0f1395c78f2 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -82,6 +82,29 @@ def repeat(self, repeats: int) -> series.Series: def capitalize(self) -> series.Series: return self._apply_unary_op(ops.capitalize_op) + def match(self, pat, case=True, flags=0) -> series.Series: + # \A anchors start of entire string rather than start of any line in multiline mode + adj_pat = rf"\A{pat}" + return self.contains(adj_pat, case=case, flags=flags) + + def fullmatch(self, pat, case=True, flags=0) -> series.Series: + # \A anchors start of entire string rather than start of any line in multiline mode + # \z likewise anchors to the end of the entire multiline string + adj_pat = rf"\A{pat}\z" + return self.contains(adj_pat, case=case, flags=flags) + + def get(self, i: int) -> series.Series: + return self._apply_unary_op(ops.StrGetOp(i)) + + def pad(self, width, side="left", fillchar=" ") -> series.Series: + return self._apply_unary_op(ops.StrPadOp(width, fillchar, side)) + + def ljust(self, width, fillchar=" ") -> series.Series: + return self._apply_unary_op(ops.StrPadOp(width, fillchar, "right")) + + def rjust(self, width, fillchar=" ") -> series.Series: + return self._apply_unary_op(ops.StrPadOp(width, fillchar, "left")) + def contains( self, pat, case: bool = True, flags: int = 0, *, regex: bool = True ) -> series.Series: @@ -95,7 +118,7 @@ def contains( else: return self._apply_unary_op(ops.ContainsStringOp(pat)) - def extract(self, pat: str, flags: int = 0): + def extract(self, pat: str, flags: int = 0) -> df.DataFrame: re2flags = _parse_flags(flags) if re2flags: pat = re2flags + pat diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index cc8b4e5cc4d..ed7a09e7b71 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -16,6 +16,7 @@ from __future__ import annotations +from collections import namedtuple import inspect import threading import typing @@ -399,6 +400,9 @@ def read_gbq_function(function_name: str): Index = bigframes.core.indexes.Index Series = bigframes.series.Series +# Used by DataFrameGroupby.agg +NamedAgg = namedtuple("NamedAgg", ["column", "aggfunc"]) + # Use __all__ to let type checkers know what is part of the public API. __all___ = [ "concat", @@ -411,4 +415,5 @@ def read_gbq_function(function_name: str): "read_pandas", "remote_function", "Series", + "NamedAgg", ] diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 5924941cc54..27e2b8f7c23 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -256,34 +256,6 @@ def create_bq_connection(self): def check_bq_connection_exists(self): """Check if the BigQuery Connection exists.""" client = self._bq_connection_client - if self._bq_connection_id.count(".") == 1: - bq_location, bq_connection_id = self._bq_connection_id.split(".") - if bq_location != self._bq_location: - logger.info( - f"Reset location {self._bq_location} to match the" - + f"location in connection name: {bq_location}" - ) - self._bq_location = bq_location - self._bq_connection_id = bq_connection_id - elif self._bq_connection_id.count(".") == 2: - ( - gcp_project_id, - bq_location, - bq_connection_id, - ) = self._bq_connection_id.split(".") - if gcp_project_id != self._gcp_project_id: - raise ValueError( - "The project_id does not match BigQuery connection gcp_project_id: " - f"{self._gcp_project_id}." - ) - if bq_location != self._bq_location: - logger.info( - f"Reset location {self._bq_location} to match the" - + f"location in connection name: {bq_location}" - ) - self._gcp_project_id = gcp_project_id - self._bq_location = bq_location - self._bq_connection_id = bq_connection_id request = bigquery_connection_v1.GetConnectionRequest( name=client.connection_path( self._gcp_project_id, self._bq_location, self._bq_connection_id @@ -797,6 +769,33 @@ def remote_function( random.choices(string.ascii_lowercase + string.digits, k=8) ) + # Check connection_id with `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID` format. + if bigquery_connection.count(".") == 1: + bq_connection_location, bq_connection_id = bigquery_connection.split(".") + if bq_connection_location.casefold() != bq_location.casefold(): + raise ValueError( + "The location does not match BigQuery connection location: " + f"{bq_location}." + ) + bigquery_connection = bq_connection_id + elif bigquery_connection.count(".") == 2: + ( + gcp_project_id, + bq_connection_location, + bq_connection_id, + ) = bigquery_connection.split(".") + if gcp_project_id.casefold() != dataset_ref.project.casefold(): + raise ValueError( + "The project_id does not match BigQuery connection gcp_project_id: " + f"{dataset_ref.project}." + ) + if bq_connection_location.casefold() != bq_location.casefold(): + raise ValueError( + "The location does not match BigQuery connection location: " + f"{bq_location}." + ) + bigquery_connection = bq_connection_id + def wrapper(f): if not callable(f): raise TypeError("f must be callable, got {}".format(f)) diff --git a/bigframes/series.py b/bigframes/series.py index 537991ed004..f8f44dc2e64 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -87,7 +87,7 @@ def iloc(self) -> bigframes.core.indexers.IlocSeriesIndexer: return bigframes.core.indexers.IlocSeriesIndexer(self) @property - def name(self) -> Optional[str]: + def name(self) -> blocks.Label: return self._name @property @@ -169,6 +169,12 @@ def rename( return Series(block) + # rename the Series name + if isinstance(index, typing.Hashable): + index = typing.cast(Optional[str], index) + block = self._block.with_column_labels([index]) + return Series(block) + raise ValueError(f"Unsupported type of parameter index: {type(index)}") def rename_axis( @@ -321,7 +327,7 @@ def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: for level_ref in levels: if isinstance(level_ref, int): resolved_level_ids.append(self._block.index_columns[level_ref]) - elif isinstance(level_ref, str): + elif isinstance(level_ref, typing.Hashable): matching_ids = self._block.index_name_to_col_id.get(level_ref, []) if len(matching_ids) != 1: raise ValueError("level name cannot be found or is ambiguous") @@ -440,6 +446,17 @@ def nsmallest(self, n: int = 5, keep: str = "first") -> Series: block = block.select_column(self._value_column) return Series(block) + def isin(self, values) -> "Series" | None: + if not _is_list_like(values): + raise TypeError( + "only list-like objects are allowed to be passed to " + f"isin(), you passed a [{type(values).__name__}]" + ) + + return self._apply_unary_op(ops.IsInOp(values, match_nulls=True)).fillna( + value=False + ) + def isna(self) -> "Series": return self._apply_unary_op(ops.isnull_op) @@ -791,6 +808,26 @@ def argmin(self) -> scalars.Scalar: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) + @property + def is_monotonic_increasing(self) -> bool: + period = 1 + window = bigframes.core.WindowSpec( + preceding=period, + following=None, + ) + shifted_series = self._apply_window_op(agg_ops.ShiftOp(period), window) + return self.notna().__and__(self >= shifted_series).all() + + @property + def is_monotonic_decreasing(self) -> bool: + period = 1 + window = bigframes.core.WindowSpec( + preceding=period, + following=None, + ) + shifted_series = self._apply_window_op(agg_ops.ShiftOp(period), window) + return self.notna().__and__(self <= shifted_series).all() + def __getitem__(self, indexer): # TODO: enforce stricter alignment, should fail if indexer is missing any keys. use_iloc = ( diff --git a/bigframes/session.py b/bigframes/session.py index 28a38f93073..73fdd731063 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -358,10 +358,16 @@ def read_gbq_query( else: index_cols = list(index_col) - # Can't cluster since don't know if index_cols are clusterable data types - # TODO(tbergeron): Maybe use dryrun to determine types of index_cols to see if can cluster - _, query_job = self._start_query(query) - destination = query_job.destination + # Make sure we cluster by the index column so that subsequent + # operations are as speedy as they can be. + if index_cols: + # Since index_cols are specified, assume that we have a normal SQL + # query. DDL or DML not supported. + ibis_expr = self.ibis_client.sql(query) + destination = self._ibis_to_session_table(ibis_expr, index_cols) + else: + _, query_job = self._start_query(query) + destination = query_job.destination # If there was no destination table, that means the query must have # been DDL or DML. Return some job metadata, instead. @@ -936,15 +942,9 @@ def _create_sequential_ordering( ibis.row_number().cast(ibis_dtypes.int64).name(default_ordering_name) ) table = table.mutate(**{default_ordering_name: default_ordering_col}) - clusterable_index_cols = [ - col for col in index_cols if _can_cluster(table[col].type()) - ] - cluster_cols = (clusterable_index_cols + [default_ordering_name])[ - :_MAX_CLUSTER_COLUMNS - ] - table_ref = self._query_to_session_table( - self.ibis_client.compile(table), - cluster_cols=cluster_cols, + table_ref = self._ibis_to_session_table( + table, + cluster_cols=list(index_cols) + [default_ordering_name], ) table = self.ibis_client.sql(f"SELECT * FROM `{table_ref.table_id}`") ordering_reference = core.OrderingColumnReference(default_ordering_name) @@ -955,6 +955,17 @@ def _create_sequential_ordering( ) return table, ordering + def _ibis_to_session_table( + self, table: ibis_types.Table, cluster_cols: Iterable[str] + ) -> bigquery.TableReference: + clusterable_cols = [ + col for col in cluster_cols if _can_cluster(table[col].type()) + ][:_MAX_CLUSTER_COLUMNS] + return self._query_to_session_table( + self.ibis_client.compile(table), + cluster_cols=clusterable_cols, + ) + def _query_to_session_table( self, query_text: str, cluster_cols: Iterable[str] ) -> bigquery.TableReference: diff --git a/bigframes/version.py b/bigframes/version.py index c387f59d2c6..d386742d59f 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.1" +__version__ = "0.2.0" diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml new file mode 100644 index 00000000000..91abc59bc0a --- /dev/null +++ b/docs/templates/toc.yml @@ -0,0 +1,130 @@ +- items: + - href: index.md + name: Overview + - href: changelog.md + name: Changelog + - items: + - items: + - name: Options + uid: bigframes._config.Options + - name: BigQueryOptions + uid: bigframes._config.bigquery_options.BigQueryOptions + - name: DisplayOptions + uid: bigframes._config.display_options.DisplayOptions + - name: SamplingOptions + uid: bigframes._config.sampling_options.SamplingOptions + name: Options and settings + - items: + - name: Session + uid: bigframes.session.Session + name: Session + name: Core Objects + - items: + - name: DataFrame + uid: bigframes.dataframe.DataFrame + - items: + - name: DataFrameGroupBy + uid: bigframes.core.groupby.DataFrameGroupBy + - name: SeriesGroupBy + uid: bigframes.core.groupby.SeriesGroupBy + name: Groupby + - name: Indexes + uid: bigframes.core.indexes.index.Index + - name: pandas + uid: bigframes.pandas + - items: + - name: Series + uid: bigframes.series.Series + - name: DatetimeMethods + uid: bigframes.operations.datetimes.DatetimeMethods + - name: StringMethods + uid: bigframes.operations.strings.StringMethods + name: Series + - name: Window + uid: bigframes.core.window.Window + name: bigframes.pandas + - items: + - items: + - name: Overview + uid: bigframes.ml.cluster + - name: KMeans + uid: bigframes.ml.cluster.KMeans + name: cluster + - items: + - name: Overview + uid: bigframes.ml.compose + - name: ColumnTransformer + uid: bigframes.ml.compose.ColumnTransformer + name: compose + - items: + - name: Overview + uid: bigframes.ml.decomposition + - name: PCA + uid: bigframes.ml.decomposition.PCA + name: decomposition + - items: + - name: Overview + uid: bigframes.ml.ensemble + - name: RandomForestClassifier + uid: bigframes.ml.ensemble.RandomForestClassifier + - name: RandomForestRegressor + uid: bigframes.ml.ensemble.RandomForestRegressor + - name: XGBClassifier + uid: bigframes.ml.ensemble.XGBClassifier + - name: XGBRegressor + uid: bigframes.ml.ensemble.XGBRegressor + name: ensemble + - items: + - name: Overview + uid: bigframes.ml.forecasting + - name: ARIMAPlus + uid: bigframes.ml.forecasting.ARIMAPlus + name: forecasting + - items: + - name: Overview + uid: bigframes.ml.imported + - name: ONNXModel + uid: bigframes.ml.imported.ONNXModel + - name: TensorFlowModel + uid: bigframes.ml.imported.TensorFlowModel + name: imported + - items: + - name: Overview + uid: bigframes.ml.linear_model + - name: LinearRegression + uid: bigframes.ml.linear_model.LinearRegression + - name: LogisticRegression + uid: bigframes.ml.linear_model.LogisticRegression + name: linear_model + - items: + - name: Overview + uid: bigframes.ml.llm + - name: PaLM2TextGenerator + uid: bigframes.ml.llm.PaLM2TextGenerator + - name: PaLM2TextEmbeddingGenerator + uid: bigframes.ml.llm.PaLM2TextEmbeddingGenerator + name: llm + - items: + - name: metrics + uid: bigframes.ml.metrics + name: metrics + - items: + - name: model_selection + uid: bigframes.ml.model_selection + name: model_selection + - items: + - name: Overview + uid: bigframes.ml.pipeline + - name: Pipeline + uid: bigframes.ml.pipeline.Pipeline + name: pipeline + - items: + - name: Overview + uid: bigframes.ml.preprocessing + - name: OneHotEncoder + uid: bigframes.ml.preprocessing.OneHotEncoder + - name: StandardScaler + uid: bigframes.ml.preprocessing.StandardScaler + name: preprocessing + name: bigframes.ml + name: BigQuery DataFrames diff --git a/notebooks/03 - Using ML - ML fundamentals.ipynb b/notebooks/03 - Using ML - ML fundamentals.ipynb index c181b67e647..ade50bcbc2e 100644 --- a/notebooks/03 - Using ML - ML fundamentals.ipynb +++ b/notebooks/03 - Using ML - ML fundamentals.ipynb @@ -14,9 +14,65 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "590f15dab2c940b0a23af16c4b1b8fe1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 802292ea-3f16-400e-97e2-34009774a1f0 is RUNNING.
\n", " \n", " \n", - " 125\n", - " 3550.0\n", + " 241\n", + " 5300.0\n", " \n", " \n", - " 260\n", - " 5750.0\n", + " 121\n", + " 3325.0\n", " \n", " \n", - " 93\n", - " 4300.0\n", + " 209\n", + " 4075.0\n", " \n", " \n", - " 255\n", - " 4850.0\n", + " 270\n", + " 3075.0\n", " \n", " \n", - " 71\n", - " 3050.0\n", + " 187\n", + " 4600.0\n", " \n", " \n", "\n", + "

5 rows × 1 columns

\n", "[5 rows x 1 columns in total]" ], "text/plain": [ " body_mass_g\n", "penguin_id \n", - "125 3550.0\n", - "260 5750.0\n", - "93 4300.0\n", - "255 4850.0\n", - "71 3050.0\n", + "241 5300.0\n", + "121 3325.0\n", + "209 4075.0\n", + "270 3075.0\n", + "187 4600.0\n", "\n", "[5 rows x 1 columns]" ] @@ -648,6 +911,76 @@ "execution_count": 5, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "299c0c895e3d4a83a8495924a1966ce0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job f54865db-fdb4-4022-af30-7f282a6b81c0 is DONE. 31.7 kB processed.
\n", " \n", " \n", - " 23\n", - " -1.310231\n", - " 0.687253\n", - " -0.568262\n", + " 0\n", + " -1.364965\n", + " 0.629892\n", + " -1.226537\n", " \n", " \n", - " 83\n", - " 2.575955\n", - " 0.329548\n", - " -1.432429\n", + " 1\n", + " -0.771824\n", + " 0.984275\n", + " -1.226537\n", + " \n", + " \n", + " 2\n", + " -0.567932\n", + " 0.883023\n", + " -1.226537\n", + " \n", + " \n", + " 3\n", + " 0.470064\n", + " 0.376761\n", + " -0.652517\n", " \n", " \n", - " 195\n", - " 0.816172\n", - " -1.407876\n", - " 0.151877\n", + " 4\n", + " -1.235216\n", + " -0.180128\n", + " -0.652517\n", " \n", " \n", - " 264\n", - " 1.109469\n", - " -0.436962\n", - " 2.096254\n", + " 5\n", + " -0.141612\n", + " 0.680518\n", + " -0.652517\n", " \n", " \n", - " 237\n", - " 0.816172\n", - " -1.305674\n", - " 0.872017\n", + " 6\n", + " 0.544207\n", + " -0.281381\n", + " -0.652517\n", " \n", " \n", - " 261\n", - " 0.614531\n", - " -0.947969\n", - " 1.520142\n", + " 7\n", + " 1.21149\n", + " 0.629892\n", + " -0.078497\n", " \n", " \n", - " 253\n", - " 0.321234\n", - " -1.663379\n", - " 0.944031\n", + " 8\n", + " 1.026133\n", + " 0.933649\n", + " -0.078497\n", " \n", " \n", - " 164\n", - " 0.101261\n", - " -0.743566\n", - " 1.160073\n", + " 10\n", + " -0.586468\n", + " 0.883023\n", + " 0.495523\n", " \n", " \n", - " 293\n", - " -0.686975\n", - " -0.079257\n", - " -1.792499\n", + " 11\n", + " -0.92011\n", + " 0.781771\n", + " -1.154784\n", " \n", " \n", - " 176\n", - " 0.871166\n", - " -1.561178\n", - " 0.655975\n", + " 14\n", + " -1.846892\n", + " -0.028249\n", + " -1.154784\n", " \n", " \n", - " 45\n", - " 1.476091\n", - " 1.811469\n", - " 0.655975\n", + " 15\n", + " -1.290822\n", + " -0.332007\n", + " -1.154784\n", " \n", " \n", - " 185\n", - " 1.347773\n", - " -1.510077\n", - " 1.232087\n", + " 16\n", + " 0.321779\n", + " 0.073003\n", + " -0.580765\n", " \n", " \n", - " 141\n", - " 1.036145\n", - " -1.101271\n", - " 1.088059\n", + " 17\n", + " 1.230026\n", + " 1.136154\n", + " -0.580765\n", " \n", " \n", - " 147\n", - " 1.109469\n", - " -0.641365\n", - " 1.66417\n", + " 18\n", + " -0.79036\n", + " 0.376761\n", + " -0.580765\n", " \n", " \n", - " 320\n", - " -0.925279\n", - " 0.329548\n", - " -1.432429\n", + " 20\n", + " 0.4886\n", + " 0.326134\n", + " -0.580765\n", " \n", " \n", - " 285\n", - " -0.192036\n", - " -2.072185\n", - " 1.016045\n", + " 21\n", + " 1.359775\n", + " 1.034902\n", + " -0.580765\n", " \n", " \n", - " 305\n", - " 0.027937\n", - " 0.43175\n", - " 0.655975\n", + " 24\n", + " 1.044669\n", + " 0.528639\n", + " -0.580765\n", " \n", " \n", - " 37\n", - " 1.421098\n", - " 1.607066\n", - " -0.496248\n", + " 26\n", + " -0.456718\n", + " 0.680518\n", + " -0.006745\n", " \n", " \n", - " 98\n", - " -0.521995\n", - " 0.176246\n", - " -0.784304\n", + " 27\n", + " 1.21149\n", + " 1.237407\n", + " -0.006745\n", " \n", " \n", - " 10\n", - " -0.576989\n", - " 0.891656\n", - " 0.511947\n", + " 29\n", + " 1.378311\n", + " 0.933649\n", + " -0.006745\n", + " \n", + " \n", + " 31\n", + " -2.013713\n", + " -0.534512\n", + " -1.657052\n", + " \n", + " \n", + " 32\n", + " -1.253751\n", + " 0.478013\n", + " -1.657052\n", + " \n", + " \n", + " 33\n", + " -0.827431\n", + " -0.230754\n", + " -1.657052\n", " \n", " \n", "\n", + "

25 rows × 3 columns

\n", "[267 rows x 3 columns in total]" ], "text/plain": [ " scaled_culmen_length_mm scaled_culmen_depth_mm \\\n", "penguin_id \n", - "23 -1.310231 0.687253 \n", - "83 2.575955 0.329548 \n", - "195 0.816172 -1.407876 \n", - "264 1.109469 -0.436962 \n", - "237 0.816172 -1.305674 \n", - "261 0.614531 -0.947969 \n", - "253 0.321234 -1.663379 \n", - "164 0.101261 -0.743566 \n", - "293 -0.686975 -0.079257 \n", - "176 0.871166 -1.561178 \n", - "45 1.476091 1.811469 \n", - "185 1.347773 -1.510077 \n", - "141 1.036145 -1.101271 \n", - "147 1.109469 -0.641365 \n", - "320 -0.925279 0.329548 \n", - "285 -0.192036 -2.072185 \n", - "305 0.027937 0.43175 \n", - "37 1.421098 1.607066 \n", - "98 -0.521995 0.176246 \n", - "10 -0.576989 0.891656 \n", - "307 -1.420218 0.022944 \n", - "337 -0.393678 1.14716 \n", - "26 -0.448671 0.687253 \n", - "97 -1.45688 0.380649 \n", - "27 1.201125 1.249361 \n", + "0 -1.364965 0.629892 \n", + "1 -0.771824 0.984275 \n", + "2 -0.567932 0.883023 \n", + "3 0.470064 0.376761 \n", + "4 -1.235216 -0.180128 \n", + "5 -0.141612 0.680518 \n", + "6 0.544207 -0.281381 \n", + "7 1.21149 0.629892 \n", + "8 1.026133 0.933649 \n", + "10 -0.586468 0.883023 \n", + "11 -0.92011 0.781771 \n", + "14 -1.846892 -0.028249 \n", + "15 -1.290822 -0.332007 \n", + "16 0.321779 0.073003 \n", + "17 1.230026 1.136154 \n", + "18 -0.79036 0.376761 \n", + "20 0.4886 0.326134 \n", + "21 1.359775 1.034902 \n", + "24 1.044669 0.528639 \n", + "26 -0.456718 0.680518 \n", + "27 1.21149 1.237407 \n", + "29 1.378311 0.933649 \n", + "31 -2.013713 -0.534512 \n", + "32 -1.253751 0.478013 \n", + "33 -0.827431 -0.230754 \n", "\n", " scaled_flipper_length_mm \n", "penguin_id \n", - "23 -0.568262 \n", - "83 -1.432429 \n", - "195 0.151877 \n", - "264 2.096254 \n", - "237 0.872017 \n", - "261 1.520142 \n", - "253 0.944031 \n", - "164 1.160073 \n", - "293 -1.792499 \n", - "176 0.655975 \n", - "45 0.655975 \n", - "185 1.232087 \n", - "141 1.088059 \n", - "147 1.66417 \n", - "320 -1.432429 \n", - "285 1.016045 \n", - "305 0.655975 \n", - "37 -0.496248 \n", - "98 -0.784304 \n", - "10 0.511947 \n", - "307 -1.000346 \n", - "337 -0.208192 \n", - "26 0.00785 \n", - "97 -0.784304 \n", - "27 0.00785 \n", + "0 -1.226537 \n", + "1 -1.226537 \n", + "2 -1.226537 \n", + "3 -0.652517 \n", + "4 -0.652517 \n", + "5 -0.652517 \n", + "6 -0.652517 \n", + "7 -0.078497 \n", + "8 -0.078497 \n", + "10 0.495523 \n", + "11 -1.154784 \n", + "14 -1.154784 \n", + "15 -1.154784 \n", + "16 -0.580765 \n", + "17 -0.580765 \n", + "18 -0.580765 \n", + "20 -0.580765 \n", + "21 -0.580765 \n", + "24 -0.580765 \n", + "26 -0.006745 \n", + "27 -0.006745 \n", + "29 -0.006745 \n", + "31 -1.657052 \n", + "32 -1.657052 \n", + "33 -1.657052 \n", "...\n", "\n", "[267 rows x 3 columns]" @@ -890,6 +1254,76 @@ "execution_count": 6, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6ec4ab3e60844e989dbebad89e7665ca", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job a8029b51-2ef1-4acd-9759-d808db954298 is DONE. 31.7 kB processed.
\n", " \n", " \n", - " 125\n", - " -0.77863\n", - " 0.891656\n", - " -1.216387\n", + " 9\n", + " -0.697682\n", + " 1.490538\n", + " -0.078497\n", " \n", " \n", - " 260\n", - " 1.182794\n", - " -0.743566\n", - " 1.520142\n", + " 12\n", + " -1.290822\n", + " -0.129502\n", + " -1.154784\n", " \n", " \n", - " 93\n", - " -0.521995\n", - " 0.482851\n", - " 0.295905\n", + " 13\n", + " 0.562742\n", + " 0.073003\n", + " -1.154784\n", " \n", " \n", - " 255\n", - " 0.101261\n", - " -1.254574\n", - " 0.944031\n", + " 19\n", + " -1.142537\n", + " 0.478013\n", + " -0.580765\n", " \n", " \n", - " 71\n", - " -2.171791\n", - " -0.845768\n", - " -0.928332\n", + " 22\n", + " -0.697682\n", + " -0.028249\n", + " -0.580765\n", " \n", " \n", - " 327\n", - " -0.521995\n", - " 0.227347\n", - " -1.360415\n", + " 23\n", + " -1.327894\n", + " 0.680518\n", + " -0.580765\n", " \n", " \n", - " 338\n", - " -0.980272\n", - " 2.066973\n", - " -0.71229\n", + " 25\n", + " 1.489525\n", + " 0.478013\n", + " -0.006745\n", " \n", " \n", - " 68\n", - " 1.29278\n", - " 0.840556\n", - " 0.151877\n", + " 28\n", + " 1.897309\n", + " 1.844922\n", + " -0.006745\n", " \n", " \n", - " 17\n", - " 1.219456\n", - " 1.14716\n", - " -0.568262\n", + " 30\n", + " 1.267097\n", + " 0.680518\n", + " -0.006745\n", " \n", " \n", - " 116\n", - " -0.302023\n", - " 2.066973\n", - " -0.71229\n", + " 38\n", + " -0.49379\n", + " 1.59179\n", + " -0.509012\n", " \n", " \n", - " 106\n", - " -0.94361\n", - " 1.453764\n", - " -0.784304\n", + " 41\n", + " -1.531786\n", + " 0.427387\n", + " 0.065008\n", " \n", " \n", - " 227\n", - " 1.512753\n", - " -0.028156\n", - " 1.952226\n", + " 45\n", + " 1.489525\n", + " 1.794295\n", + " 0.639028\n", " \n", " \n", - " 314\n", - " -0.210367\n", - " 0.687253\n", - " -0.424234\n", + " 49\n", + " -0.567932\n", + " -0.281381\n", + " -1.011279\n", " \n", " \n", - " 246\n", - " -1.126921\n", - " 0.585052\n", - " -1.936527\n", + " 50\n", + " -0.271362\n", + " -0.230754\n", + " -1.011279\n", " \n", " \n", - " 138\n", - " 1.201125\n", - " -0.99907\n", - " 1.088059\n", + " 62\n", + " -1.402036\n", + " -0.078876\n", + " -0.43726\n", " \n", " \n", - " 126\n", - " -1.383556\n", - " -0.028156\n", - " -1.216387\n", + " 65\n", + " -0.586468\n", + " 0.629892\n", + " -0.43726\n", " \n", " \n", - " 121\n", - " 0.761179\n", - " -0.385861\n", - " -0.136178\n", + " 66\n", + " 0.989062\n", + " 1.389285\n", + " 0.13676\n", " \n", " \n", - " 231\n", - " -0.320354\n", - " 1.198261\n", - " -0.280206\n", + " 83\n", + " 2.601663\n", + " 0.326134\n", + " -1.441794\n", " \n", " \n", - " 40\n", - " 1.146132\n", - " 0.840556\n", - " 0.079863\n", + " 87\n", + " 1.526596\n", + " 0.832397\n", + " -0.293755\n", " \n", " \n", - " 60\n", - " 0.504544\n", - " 0.380649\n", - " -0.424234\n", + " 92\n", + " 1.767559\n", + " 1.389285\n", + " 0.280265\n", " \n", - " \n", - "\n", - "[67 rows x 3 columns in total]" - ], + " \n", + " 98\n", + " -0.530861\n", + " 0.174256\n", + " -0.796022\n", + " \n", + " \n", + " 104\n", + " 0.35885\n", + " -0.028249\n", + " -0.796022\n", + " \n", + " \n", + " 114\n", + " 0.229101\n", + " -0.281381\n", + " -0.72427\n", + " \n", + " \n", + " 115\n", + " 0.840777\n", + " 0.174256\n", + " -0.72427\n", + " \n", + " \n", + " 118\n", + " -1.550322\n", + " 0.174256\n", + " -0.72427\n", + " \n", + " \n", + "\n", + "

25 rows × 3 columns

\n", + "[67 rows x 3 columns in total]" + ], "text/plain": [ " scaled_culmen_length_mm scaled_culmen_depth_mm \\\n", "penguin_id \n", - "125 -0.77863 0.891656 \n", - "260 1.182794 -0.743566 \n", - "93 -0.521995 0.482851 \n", - "255 0.101261 -1.254574 \n", - "71 -2.171791 -0.845768 \n", - "327 -0.521995 0.227347 \n", - "338 -0.980272 2.066973 \n", - "68 1.29278 0.840556 \n", - "17 1.219456 1.14716 \n", - "116 -0.302023 2.066973 \n", - "106 -0.94361 1.453764 \n", - "227 1.512753 -0.028156 \n", - "314 -0.210367 0.687253 \n", - "246 -1.126921 0.585052 \n", - "138 1.201125 -0.99907 \n", - "126 -1.383556 -0.028156 \n", - "121 0.761179 -0.385861 \n", - "231 -0.320354 1.198261 \n", - "40 1.146132 0.840556 \n", - "60 0.504544 0.380649 \n", - "331 -1.548535 0.176246 \n", - "218 0.192916 -0.896869 \n", - "154 -1.10859 0.738354 \n", - "130 -0.357016 1.198261 \n", - "89 1.604408 1.351563 \n", + "9 -0.697682 1.490538 \n", + "12 -1.290822 -0.129502 \n", + "13 0.562742 0.073003 \n", + "19 -1.142537 0.478013 \n", + "22 -0.697682 -0.028249 \n", + "23 -1.327894 0.680518 \n", + "25 1.489525 0.478013 \n", + "28 1.897309 1.844922 \n", + "30 1.267097 0.680518 \n", + "38 -0.49379 1.59179 \n", + "41 -1.531786 0.427387 \n", + "45 1.489525 1.794295 \n", + "49 -0.567932 -0.281381 \n", + "50 -0.271362 -0.230754 \n", + "62 -1.402036 -0.078876 \n", + "65 -0.586468 0.629892 \n", + "66 0.989062 1.389285 \n", + "83 2.601663 0.326134 \n", + "87 1.526596 0.832397 \n", + "92 1.767559 1.389285 \n", + "98 -0.530861 0.174256 \n", + "104 0.35885 -0.028249 \n", + "114 0.229101 -0.281381 \n", + "115 0.840777 0.174256 \n", + "118 -1.550322 0.174256 \n", "\n", " scaled_flipper_length_mm \n", "penguin_id \n", - "125 -1.216387 \n", - "260 1.520142 \n", - "93 0.295905 \n", - "255 0.944031 \n", - "71 -0.928332 \n", - "327 -1.360415 \n", - "338 -0.71229 \n", - "68 0.151877 \n", - "17 -0.568262 \n", - "116 -0.71229 \n", - "106 -0.784304 \n", - "227 1.952226 \n", - "314 -0.424234 \n", - "246 -1.936527 \n", - "138 1.088059 \n", - "126 -1.216387 \n", - "121 -0.136178 \n", - "231 -0.280206 \n", - "40 0.079863 \n", - "60 -0.424234 \n", - "331 -0.784304 \n", - "218 1.376114 \n", - "154 -0.568262 \n", - "130 -0.064164 \n", - "89 -0.280206 \n", + "9 -0.078497 \n", + "12 -1.154784 \n", + "13 -1.154784 \n", + "19 -0.580765 \n", + "22 -0.580765 \n", + "23 -0.580765 \n", + "25 -0.006745 \n", + "28 -0.006745 \n", + "30 -0.006745 \n", + "38 -0.509012 \n", + "41 0.065008 \n", + "45 0.639028 \n", + "49 -1.011279 \n", + "50 -1.011279 \n", + "62 -0.43726 \n", + "65 -0.43726 \n", + "66 0.13676 \n", + "83 -1.441794 \n", + "87 -0.293755 \n", + "92 0.280265 \n", + "98 -0.796022 \n", + "104 -0.796022 \n", + "114 -0.72427 \n", + "115 -0.72427 \n", + "118 -0.72427 \n", "...\n", "\n", "[67 rows x 3 columns]" @@ -1133,6 +1598,104 @@ "execution_count": 7, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "94efb3f362234593906e44c16b1842e4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 792a19f4-e3c2-4344-8555-7f5a781c3c7d is DONE. 32.0 kB processed.
\n", " \n", " \n", - " 23\n", + " 0\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.364965\n", + " 0.629892\n", + " -1.226537\n", " [{'index': 2, 'value': 1.0}]\n", - " -1.310231\n", - " 0.687253\n", - " -0.568262\n", " [{'index': 1, 'value': 1.0}]\n", + " \n", + " \n", + " 1\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.771824\n", + " 0.984275\n", + " -1.226537\n", + " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 83\n", + " 2\n", " [{'index': 2, 'value': 1.0}]\n", - " 2.575955\n", - " 0.329548\n", - " -1.432429\n", + " -0.567932\n", + " 0.883023\n", + " -1.226537\n", + " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", + " \n", + " \n", + " 3\n", + " [{'index': 2, 'value': 1.0}]\n", + " 0.470064\n", + " 0.376761\n", + " -0.652517\n", + " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 195\n", - " [{'index': 1, 'value': 1.0}]\n", - " 0.816172\n", - " -1.407876\n", - " 0.151877\n", + " 4\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.235216\n", + " -0.180128\n", + " -0.652517\n", + " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", - " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 264\n", - " [{'index': 1, 'value': 1.0}]\n", - " 1.109469\n", - " -0.436962\n", - " 2.096254\n", + " 5\n", " [{'index': 2, 'value': 1.0}]\n", + " -0.141612\n", + " 0.680518\n", + " -0.652517\n", " [{'index': 3, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 237\n", - " [{'index': 1, 'value': 1.0}]\n", - " 0.816172\n", - " -1.305674\n", - " 0.872017\n", + " 6\n", + " [{'index': 2, 'value': 1.0}]\n", + " 0.544207\n", + " -0.281381\n", + " -0.652517\n", + " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 261\n", - " [{'index': 1, 'value': 1.0}]\n", - " 0.614531\n", - " -0.947969\n", - " 1.520142\n", + " 7\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.21149\n", + " 0.629892\n", + " -0.078497\n", + " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 253\n", - " [{'index': 1, 'value': 1.0}]\n", - " 0.321234\n", - " -1.663379\n", - " 0.944031\n", - " [{'index': 1, 'value': 1.0}]\n", + " 8\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.026133\n", + " 0.933649\n", + " -0.078497\n", " [{'index': 3, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 164\n", - " [{'index': 1, 'value': 1.0}]\n", - " 0.101261\n", - " -0.743566\n", - " 1.160073\n", - " [{'index': 0, 'value': 1.0}]\n", + " 10\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.586468\n", + " 0.883023\n", + " 0.495523\n", " [{'index': 3, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 293\n", + " 11\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.92011\n", + " 0.781771\n", + " -1.154784\n", " [{'index': 3, 'value': 1.0}]\n", - " -0.686975\n", - " -0.079257\n", - " -1.792499\n", - " [{'index': 1, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 176\n", + " 14\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.846892\n", + " -0.028249\n", + " -1.154784\n", + " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", - " 0.871166\n", - " -1.561178\n", - " 0.655975\n", + " \n", + " \n", + " 15\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.290822\n", + " -0.332007\n", + " -1.154784\n", + " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", - " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 45\n", + " 16\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.476091\n", - " 1.811469\n", - " 0.655975\n", + " 0.321779\n", + " 0.073003\n", + " -0.580765\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 185\n", - " [{'index': 1, 'value': 1.0}]\n", - " 1.347773\n", - " -1.510077\n", - " 1.232087\n", + " 17\n", " [{'index': 2, 'value': 1.0}]\n", + " 1.230026\n", + " 1.136154\n", + " -0.580765\n", " [{'index': 3, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 141\n", + " 18\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.79036\n", + " 0.376761\n", + " -0.580765\n", + " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", - " 1.036145\n", - " -1.101271\n", - " 1.088059\n", + " \n", + " \n", + " 20\n", + " [{'index': 2, 'value': 1.0}]\n", + " 0.4886\n", + " 0.326134\n", + " -0.580765\n", " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", + " \n", + " \n", + " 21\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.359775\n", + " 1.034902\n", + " -0.580765\n", " [{'index': 3, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 147\n", - " [{'index': 1, 'value': 1.0}]\n", - " 1.109469\n", - " -0.641365\n", - " 1.66417\n", + " 24\n", " [{'index': 2, 'value': 1.0}]\n", + " 1.044669\n", + " 0.528639\n", + " -0.580765\n", " [{'index': 3, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 320\n", + " 26\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.456718\n", + " 0.680518\n", + " -0.006745\n", " [{'index': 3, 'value': 1.0}]\n", - " -0.925279\n", - " 0.329548\n", - " -1.432429\n", - " [{'index': 1, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 285\n", - " [{'index': 1, 'value': 1.0}]\n", - " -0.192036\n", - " -2.072185\n", - " 1.016045\n", - " [{'index': 1, 'value': 1.0}]\n", + " 27\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.21149\n", + " 1.237407\n", + " -0.006745\n", " [{'index': 3, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 305\n", + " 29\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.378311\n", + " 0.933649\n", + " -0.006745\n", " [{'index': 3, 'value': 1.0}]\n", - " 0.027937\n", - " 0.43175\n", - " 0.655975\n", " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 37\n", - " [{'index': 2, 'value': 1.0}]\n", - " 1.421098\n", - " 1.607066\n", - " -0.496248\n", + " 31\n", " [{'index': 2, 'value': 1.0}]\n", + " -2.013713\n", + " -0.534512\n", + " -1.657052\n", " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 98\n", - " [{'index': 2, 'value': 1.0}]\n", - " -0.521995\n", - " 0.176246\n", - " -0.784304\n", + " 32\n", " [{'index': 2, 'value': 1.0}]\n", + " -1.253751\n", + " 0.478013\n", + " -1.657052\n", + " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 10\n", + " 33\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.576989\n", - " 0.891656\n", - " 0.511947\n", + " -0.827431\n", + " -0.230754\n", + " -1.657052\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", "\n", + "

25 rows × 6 columns

\n", "[267 rows x 6 columns in total]" ], "text/plain": [ " onehotencoded_island scaled_culmen_length_mm \\\n", "penguin_id \n", - "23 [{'index': 2, 'value': 1.0}] -1.310231 \n", - "83 [{'index': 2, 'value': 1.0}] 2.575955 \n", - "195 [{'index': 1, 'value': 1.0}] 0.816172 \n", - "264 [{'index': 1, 'value': 1.0}] 1.109469 \n", - "237 [{'index': 1, 'value': 1.0}] 0.816172 \n", - "261 [{'index': 1, 'value': 1.0}] 0.614531 \n", - "253 [{'index': 1, 'value': 1.0}] 0.321234 \n", - "164 [{'index': 1, 'value': 1.0}] 0.101261 \n", - "293 [{'index': 3, 'value': 1.0}] -0.686975 \n", - "176 [{'index': 1, 'value': 1.0}] 0.871166 \n", - "45 [{'index': 2, 'value': 1.0}] 1.476091 \n", - "185 [{'index': 1, 'value': 1.0}] 1.347773 \n", - "141 [{'index': 1, 'value': 1.0}] 1.036145 \n", - "147 [{'index': 1, 'value': 1.0}] 1.109469 \n", - "320 [{'index': 3, 'value': 1.0}] -0.925279 \n", - "285 [{'index': 1, 'value': 1.0}] -0.192036 \n", - "305 [{'index': 3, 'value': 1.0}] 0.027937 \n", - "37 [{'index': 2, 'value': 1.0}] 1.421098 \n", - "98 [{'index': 2, 'value': 1.0}] -0.521995 \n", - "10 [{'index': 2, 'value': 1.0}] -0.576989 \n", - "307 [{'index': 3, 'value': 1.0}] -1.420218 \n", - "337 [{'index': 3, 'value': 1.0}] -0.393678 \n", - "26 [{'index': 2, 'value': 1.0}] -0.448671 \n", - "97 [{'index': 2, 'value': 1.0}] -1.45688 \n", - "27 [{'index': 2, 'value': 1.0}] 1.201125 \n", + "0 [{'index': 2, 'value': 1.0}] -1.364965 \n", + "1 [{'index': 2, 'value': 1.0}] -0.771824 \n", + "2 [{'index': 2, 'value': 1.0}] -0.567932 \n", + "3 [{'index': 2, 'value': 1.0}] 0.470064 \n", + "4 [{'index': 2, 'value': 1.0}] -1.235216 \n", + "5 [{'index': 2, 'value': 1.0}] -0.141612 \n", + "6 [{'index': 2, 'value': 1.0}] 0.544207 \n", + "7 [{'index': 2, 'value': 1.0}] 1.21149 \n", + "8 [{'index': 2, 'value': 1.0}] 1.026133 \n", + "10 [{'index': 2, 'value': 1.0}] -0.586468 \n", + "11 [{'index': 2, 'value': 1.0}] -0.92011 \n", + "14 [{'index': 2, 'value': 1.0}] -1.846892 \n", + "15 [{'index': 2, 'value': 1.0}] -1.290822 \n", + "16 [{'index': 2, 'value': 1.0}] 0.321779 \n", + "17 [{'index': 2, 'value': 1.0}] 1.230026 \n", + "18 [{'index': 2, 'value': 1.0}] -0.79036 \n", + "20 [{'index': 2, 'value': 1.0}] 0.4886 \n", + "21 [{'index': 2, 'value': 1.0}] 1.359775 \n", + "24 [{'index': 2, 'value': 1.0}] 1.044669 \n", + "26 [{'index': 2, 'value': 1.0}] -0.456718 \n", + "27 [{'index': 2, 'value': 1.0}] 1.21149 \n", + "29 [{'index': 2, 'value': 1.0}] 1.378311 \n", + "31 [{'index': 2, 'value': 1.0}] -2.013713 \n", + "32 [{'index': 2, 'value': 1.0}] -1.253751 \n", + "33 [{'index': 2, 'value': 1.0}] -0.827431 \n", "\n", " scaled_culmen_depth_mm scaled_flipper_length_mm \\\n", "penguin_id \n", - "23 0.687253 -0.568262 \n", - "83 0.329548 -1.432429 \n", - "195 -1.407876 0.151877 \n", - "264 -0.436962 2.096254 \n", - "237 -1.305674 0.872017 \n", - "261 -0.947969 1.520142 \n", - "253 -1.663379 0.944031 \n", - "164 -0.743566 1.160073 \n", - "293 -0.079257 -1.792499 \n", - "176 -1.561178 0.655975 \n", - "45 1.811469 0.655975 \n", - "185 -1.510077 1.232087 \n", - "141 -1.101271 1.088059 \n", - "147 -0.641365 1.66417 \n", - "320 0.329548 -1.432429 \n", - "285 -2.072185 1.016045 \n", - "305 0.43175 0.655975 \n", - "37 1.607066 -0.496248 \n", - "98 0.176246 -0.784304 \n", - "10 0.891656 0.511947 \n", - "307 0.022944 -1.000346 \n", - "337 1.14716 -0.208192 \n", - "26 0.687253 0.00785 \n", - "97 0.380649 -0.784304 \n", - "27 1.249361 0.00785 \n", + "0 0.629892 -1.226537 \n", + "1 0.984275 -1.226537 \n", + "2 0.883023 -1.226537 \n", + "3 0.376761 -0.652517 \n", + "4 -0.180128 -0.652517 \n", + "5 0.680518 -0.652517 \n", + "6 -0.281381 -0.652517 \n", + "7 0.629892 -0.078497 \n", + "8 0.933649 -0.078497 \n", + "10 0.883023 0.495523 \n", + "11 0.781771 -1.154784 \n", + "14 -0.028249 -1.154784 \n", + "15 -0.332007 -1.154784 \n", + "16 0.073003 -0.580765 \n", + "17 1.136154 -0.580765 \n", + "18 0.376761 -0.580765 \n", + "20 0.326134 -0.580765 \n", + "21 1.034902 -0.580765 \n", + "24 0.528639 -0.580765 \n", + "26 0.680518 -0.006745 \n", + "27 1.237407 -0.006745 \n", + "29 0.933649 -0.006745 \n", + "31 -0.534512 -1.657052 \n", + "32 0.478013 -1.657052 \n", + "33 -0.230754 -1.657052 \n", "\n", " onehotencoded_sex onehotencoded_species \n", "penguin_id \n", - "23 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "83 [{'index': 1, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "195 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "264 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "237 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "261 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "253 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "164 [{'index': 0, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "293 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "176 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "45 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "185 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "141 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "147 [{'index': 2, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "320 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "285 [{'index': 1, 'value': 1.0}] [{'index': 3, 'value': 1.0}] \n", - "305 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "37 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "98 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "10 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "307 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "337 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "26 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "97 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "27 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "0 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "1 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "2 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "3 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "4 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "5 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "6 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "7 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "8 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "10 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "11 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "14 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "15 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "16 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "17 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "18 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "20 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "21 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "24 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "26 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "27 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "29 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", + "31 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "32 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + "33 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", "...\n", "\n", "[267 rows x 6 columns]" @@ -1490,6 +2099,76 @@ "execution_count": 8, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b7c22c0858914b46951adde174b43e25", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job b8c690b1-fa1b-4c4c-b802-c8ab37c930c9 is DONE. 359 Bytes processed.
\n", " \n", " \n", - " 255\n", - " 4789.541771\n", + " 9\n", + " 4295.335461\n", " \n", " \n", - " 158\n", - " 4642.382347\n", + " 12\n", + " 3338.44131\n", " \n", " \n", - " 230\n", - " 3562.484258\n", + " 13\n", + " 3201.820204\n", " \n", " \n", - " 218\n", - " 5321.625743\n", + " 19\n", + " 3982.814079\n", " \n", " \n", - " 297\n", - " 3926.179647\n", + " 22\n", + " 3538.610664\n", " \n", " \n", - " 93\n", - " 4158.344927\n", + " 23\n", + " 3613.50305\n", " \n", " \n", - " 338\n", - " 4093.191646\n", + " 25\n", + " 4009.759444\n", " \n", " \n", - " 267\n", - " 5590.390771\n", + " 28\n", + " 4240.515635\n", " \n", " \n", - " 275\n", - " 3623.482531\n", + " 30\n", + " 4028.904195\n", " \n", " \n", - " 200\n", - " 5494.402339\n", + " 38\n", + " 4206.810346\n", " \n", " \n", - " 107\n", - " 3450.730176\n", + " 41\n", + " 3736.225488\n", " \n", " \n", - " 227\n", - " 5707.948707\n", + " 45\n", + " 4383.368544\n", " \n", " \n", - " 199\n", - " 4855.771085\n", + " 49\n", + " 3125.772789\n", " \n", " \n", - " 260\n", - " 5477.219436\n", + " 50\n", + " 3149.28765\n", " \n", " \n", - " 201\n", - " 4965.59121\n", + " 62\n", + " 3531.69488\n", " \n", " \n", - " 7\n", - " 3670.281516\n", + " 65\n", + " 4073.900616\n", " \n", " \n", - " 8\n", - " 4072.678667\n", + " 66\n", + " 4160.810162\n", " \n", " \n", - " 126\n", - " 3339.624869\n", + " 83\n", + " 3272.396279\n", " \n", " \n", - " 39\n", - " 3556.890093\n", + " 87\n", + " 3989.364493\n", " \n", " \n", - " 12\n", - " 3302.203276\n", + " 92\n", + " 4240.495294\n", + " \n", + " \n", + " 98\n", + " 3911.455384\n", + " \n", + " \n", + " 104\n", + " 3271.202866\n", + " \n", + " \n", + " 114\n", + " 3244.728549\n", + " \n", + " \n", + " 115\n", + " 3737.374636\n", + " \n", + " \n", + " 118\n", + " 3485.95604\n", " \n", " \n", "\n", + "

25 rows × 1 columns

\n", "[67 rows x 1 columns in total]" ], "text/plain": [ " predicted_body_mass_g\n", "penguin_id \n", - "255 4789.541771\n", - "158 4642.382347\n", - "230 3562.484258\n", - "218 5321.625743\n", - "297 3926.179647\n", - "93 4158.344927\n", - "338 4093.191646\n", - "267 5590.390771\n", - "275 3623.482531\n", - "200 5494.402339\n", - "107 3450.730176\n", - "227 5707.948707\n", - "199 4855.771085\n", - "260 5477.219436\n", - "201 4965.59121\n", - "7 3670.281516\n", - "8 4072.678667\n", - "126 3339.624869\n", - "39 3556.890093\n", - "12 3302.203276\n", - "34 3227.822355\n", - "206 3864.010614\n", - "53 3329.248666\n", - "282 5297.303727\n", - "225 5770.689468\n", + "9 4295.335461\n", + "12 3338.44131\n", + "13 3201.820204\n", + "19 3982.814079\n", + "22 3538.610664\n", + "23 3613.50305\n", + "25 4009.759444\n", + "28 4240.515635\n", + "30 4028.904195\n", + "38 4206.810346\n", + "41 3736.225488\n", + "45 4383.368544\n", + "49 3125.772789\n", + "50 3149.28765\n", + "62 3531.69488\n", + "65 4073.900616\n", + "66 4160.810162\n", + "83 3272.396279\n", + "87 3989.364493\n", + "92 4240.495294\n", + "98 3911.455384\n", + "104 3271.202866\n", + "114 3244.728549\n", + "115 3737.374636\n", + "118 3485.95604\n", "...\n", "\n", "[67 rows x 1 columns]" @@ -1670,6 +2370,76 @@ "execution_count": 9, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fa6bd24b44cb42ec946e262ac2f25d09", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 8e88f903-5b13-4305-b259-d17194be16da is DONE. 809 Bytes processed.
\n", " \n", " \n", - " 17\n", - " 1\n", + " 9\n", + " 4\n", " \n", " \n", - " 230\n", + " 12\n", + " 4\n", + " \n", + " \n", + " 13\n", " 2\n", " \n", " \n", - " 291\n", + " 19\n", + " 4\n", + " \n", + " \n", + " 22\n", " 4\n", " \n", " \n", - " 177\n", - " 3\n", + " 23\n", + " 4\n", " \n", " \n", - " 206\n", + " 25\n", " 2\n", " \n", " \n", - " 331\n", + " 28\n", " 2\n", " \n", " \n", - " 18\n", + " 30\n", " 2\n", " \n", " \n", - " 218\n", + " 38\n", " 4\n", " \n", " \n", - " 116\n", - " 2\n", + " 41\n", + " 4\n", " \n", " \n", - " 271\n", + " 45\n", " 2\n", " \n", " \n", - " 165\n", - " 3\n", + " 49\n", + " 4\n", " \n", " \n", - " 194\n", + " 50\n", " 2\n", " \n", " \n", - " 39\n", - " 1\n", + " 62\n", + " 4\n", " \n", " \n", - " 255\n", - " 3\n", + " 65\n", + " 4\n", " \n", " \n", - " 225\n", - " 4\n", + " 66\n", + " 2\n", " \n", " \n", - " 40\n", - " 1\n", + " 83\n", + " 2\n", " \n", " \n", - " 8\n", - " 1\n", + " 87\n", + " 2\n", " \n", " \n", - " 155\n", - " 4\n", + " 92\n", + " 2\n", " \n", " \n", - " 239\n", + " 98\n", " 4\n", " \n", " \n", - " 138\n", - " 3\n", + " 104\n", + " 2\n", + " \n", + " \n", + " 114\n", + " 2\n", + " \n", + " \n", + " 115\n", + " 2\n", + " \n", + " \n", + " 118\n", + " 4\n", " \n", " \n", "\n", + "

25 rows × 1 columns

\n", "[67 rows x 1 columns in total]" ], "text/plain": [ " CENTROID_ID\n", "penguin_id \n", - "17 1\n", - "230 2\n", - "291 4\n", - "177 3\n", - "206 2\n", - "331 2\n", - "18 2\n", - "218 4\n", - "116 2\n", - "271 2\n", - "165 3\n", - "194 2\n", - "39 1\n", - "255 3\n", - "225 4\n", - "40 1\n", - "8 1\n", - "155 4\n", - "239 4\n", - "138 3\n", - "89 1\n", - "34 1\n", - "106 2\n", - "12 2\n", - "158 3\n", + "9 4\n", + "12 4\n", + "13 2\n", + "19 4\n", + "22 4\n", + "23 4\n", + "25 2\n", + "28 2\n", + "30 2\n", + "38 4\n", + "41 4\n", + "45 2\n", + "49 4\n", + "50 2\n", + "62 4\n", + "65 4\n", + "66 2\n", + "83 2\n", + "87 2\n", + "92 2\n", + "98 4\n", + "104 2\n", + "114 2\n", + "115 2\n", + "118 4\n", "...\n", "\n", "[67 rows x 1 columns]" @@ -1890,6 +2681,76 @@ "execution_count": 11, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f0465986682742af92759c3f5fce96e0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job c7d094cb-cc51-4f11-8887-b169c23aceb2 is DONE. 32.3 kB processed.
\n", " \n", " \n", - " 125\n", - " 3911.489556\n", + " 9\n", + " 4295.328991\n", " \n", " \n", - " 260\n", - " 5477.221344\n", + " 12\n", + " 3338.434943\n", " \n", " \n", - " 93\n", - " 4158.347237\n", + " 13\n", + " 3201.813783\n", " \n", " \n", - " 255\n", - " 4789.543554\n", + " 19\n", + " 3982.807707\n", " \n", " \n", - " 71\n", - " 3153.676672\n", + " 22\n", + " 3538.604385\n", " \n", " \n", - " 327\n", - " 3358.229939\n", + " 23\n", + " 3613.496641\n", " \n", " \n", - " 338\n", - " 4093.194446\n", + " 25\n", + " 4009.753161\n", " \n", " \n", - " 68\n", - " 4133.390538\n", + " 28\n", + " 4240.509087\n", " \n", " \n", - " 17\n", - " 4013.257221\n", + " 30\n", + " 4028.897875\n", " \n", " \n", - " 116\n", - " 4189.275197\n", + " 38\n", + " 4206.80377\n", " \n", " \n", - " 106\n", - " 4019.304549\n", + " 41\n", + " 3736.219256\n", " \n", " \n", - " 227\n", - " 5707.950809\n", + " 45\n", + " 4383.362136\n", " \n", " \n", - " 314\n", - " 4037.311435\n", + " 49\n", + " 3125.766474\n", " \n", " \n", - " 246\n", - " 3297.59755\n", + " 50\n", + " 3149.281322\n", " \n", " \n", - " 138\n", - " 4971.171877\n", + " 62\n", + " 3531.688645\n", " \n", " \n", - " 126\n", - " 3339.627076\n", + " 65\n", + " 4073.894238\n", " \n", " \n", - " 121\n", - " 3462.463115\n", + " 66\n", + " 4160.803738\n", + " \n", + " \n", + " 83\n", + " 3272.389735\n", + " \n", + " \n", + " 87\n", + " 3989.358086\n", + " \n", + " \n", + " 92\n", + " 4240.488891\n", + " \n", + " \n", + " 98\n", + " 3911.449023\n", " \n", " \n", - " 231\n", - " 4206.392245\n", + " 104\n", + " 3271.196535\n", " \n", " \n", - " 40\n", - " 4102.581912\n", + " 114\n", + " 3244.722283\n", " \n", " \n", - " 60\n", - " 3483.351408\n", + " 115\n", + " 3737.368277\n", + " \n", + " \n", + " 118\n", + " 3485.949702\n", " \n", " \n", "\n", + "

25 rows × 1 columns

\n", "[67 rows x 1 columns in total]" ], "text/plain": [ " predicted_body_mass_g\n", "penguin_id \n", - "125 3911.489556\n", - "260 5477.221344\n", - "93 4158.347237\n", - "255 4789.543554\n", - "71 3153.676672\n", - "327 3358.229939\n", - "338 4093.194446\n", - "68 4133.390538\n", - "17 4013.257221\n", - "116 4189.275197\n", - "106 4019.304549\n", - "227 5707.950809\n", - "314 4037.311435\n", - "246 3297.59755\n", - "138 4971.171877\n", - "126 3339.627076\n", - "121 3462.463115\n", - "231 4206.392245\n", - "40 4102.581912\n", - "60 3483.351408\n", - "331 3370.089387\n", - "218 5321.627618\n", - "154 3618.98119\n", - "130 4249.575399\n", - "89 4145.000048\n", + "9 4295.328991\n", + "12 3338.434943\n", + "13 3201.813783\n", + "19 3982.807707\n", + "22 3538.604385\n", + "23 3613.496641\n", + "25 4009.753161\n", + "28 4240.509087\n", + "30 4028.897875\n", + "38 4206.80377\n", + "41 3736.219256\n", + "45 4383.362136\n", + "49 3125.766474\n", + "50 3149.281322\n", + "62 3531.688645\n", + "65 4073.894238\n", + "66 4160.803738\n", + "83 3272.389735\n", + "87 3989.358086\n", + "92 4240.488891\n", + "98 3911.449023\n", + "104 3271.196535\n", + "114 3244.722283\n", + "115 3737.368277\n", + "118 3485.949702\n", "...\n", "\n", "[67 rows x 1 columns]" @@ -2071,6 +2953,76 @@ "execution_count": 12, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e6409bce719940f4ae326a8b18871d9b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job a427bad2-9875-453f-ad2a-1eefaf085657 is DONE. 32.3 kB processed.
\n", " \n", " 0\n", - " 223.862457\n", - " 73619.165369\n", - " 0.004765\n", - " 209.607289\n", - " 0.888368\n", - " 0.901838\n", + " 241.640738\n", + " 90117.84266\n", + " 0.005652\n", + " 200.718678\n", + " 0.8727\n", + " 0.878359\n", " \n", " \n", "\n", + "

1 rows × 6 columns

\n", "[1 rows x 6 columns in total]" ], "text/plain": [ " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - "0 223.862457 73619.165369 0.004765 \n", + "0 241.640738 90117.84266 0.005652 \n", "\n", " median_absolute_error r2_score explained_variance \n", - "0 209.607289 0.888368 0.901838 \n", + "0 200.718678 0.8727 0.878359 \n", "\n", "[1 rows x 6 columns]" ] @@ -2147,10 +3100,52 @@ "execution_count": 13, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e56e2cc197894ee9acc448a8c12e8a30", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 929c826c-1051-47fc-9256-546f4ef11c32 is DONE. 31.7 kB processed.
str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...)""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='kmeans', + num_clusters=3 +) AS SELECT + culmen_length_mm, + culmen_depth_mm, + flipper_length_mm, + sex +FROM `{penguins_table_id}`""" + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.penguins_logistic_reg_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "penguins_logistic_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + @pytest.fixture(scope="session") def penguins_xgbregressor_model_name( session: bigframes.Session, dataset_id_permanent, penguins_table_id diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 8325cec50aa..87ea46f969c 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -48,13 +48,19 @@ def penguins_bqml_linear_model(session, penguins_linear_model_name) -> core.Bqml @pytest.fixture(scope="function") def ephemera_penguins_bqml_linear_model( penguins_bqml_linear_model, -) -> linear_model.LinearRegression: +) -> core.BqmlModel: model = penguins_bqml_linear_model return model.copy( f"{model._model.project}.{model._model.dataset_id}.{uuid.uuid4().hex}" ) +@pytest.fixture(scope="session") +def penguins_bqml_kmeans_model(session, penguins_kmeans_model_name) -> core.BqmlModel: + model = session.bqclient.get_model(penguins_kmeans_model_name) + return core.BqmlModel(session, model) + + @pytest.fixture(scope="session") def penguins_linear_model( session, penguins_linear_model_name: str @@ -125,34 +131,11 @@ def penguins_randomforest_classifier_model( @pytest.fixture(scope="session") -def penguins_kmeans_model( - session: bigframes.Session, dataset_id_permanent, penguins_table_id -) -> cluster.KMeans: - """Provides a pretrained model as a test fixture that is cached across test runs. - This lets us run system tests without having to wait for a model.fit(...)""" - sql = f""" -CREATE OR REPLACE MODEL `$model_name` -OPTIONS ( - model_type='kmeans', - num_clusters=3 -) AS SELECT - culmen_length_mm, - culmen_depth_mm, - flipper_length_mm, - sex -FROM `{penguins_table_id}`""" - # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited - model_name = f"{dataset_id_permanent}.penguins_cluster_{hashlib.md5(sql.encode()).hexdigest()}" - sql = sql.replace("$model_name", model_name) - - try: - return session.read_gbq_model(model_name) - except google.cloud.exceptions.NotFound: - logging.info( - "penguins_kmeans_model fixture was not found in the permanent dataset, regenerating it..." - ) - session.bqclient.query(sql).result() - return session.read_gbq_model(model_name) +def penguins_kmeans_model(session, penguins_kmeans_model_name: str) -> cluster.KMeans: + return cast( + cluster.KMeans, + session.read_gbq_model(penguins_kmeans_model_name), + ) @pytest.fixture(scope="session") diff --git a/tests/system/small/ml/test_cluster.py b/tests/system/small/ml/test_cluster.py index a003cd1ec1b..d95a1e1bc22 100644 --- a/tests/system/small/ml/test_cluster.py +++ b/tests/system/small/ml/test_cluster.py @@ -88,5 +88,71 @@ def test_kmeans_score(session, penguins_kmeans_model: cluster.KMeans): ) +def test_kmeans_cluster_centers(penguins_kmeans_model: cluster.KMeans): + result = penguins_kmeans_model.cluster_centers_.to_pandas() + expected = pd.DataFrame( + { + "centroid_id": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], + "feature": [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + * 3, + "numerical_value": [ + 47.509677, + 14.993548, + 217.040123, + pd.NA, + 38.207813, + 18.03125, + 187.992188, + pd.NA, + 47.036346, + 18.834808, + 197.1612, + pd.NA, + ], + "categorical_value": [ + [], + [], + [], + [ + {"category": ".", "value": 0.008064516129032258}, + {"category": "MALE", "value": 0.49193548387096775}, + {"category": "FEMALE", "value": 0.47580645161290325}, + {"category": "_null_filler", "value": 0.024193548387096774}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.34375}, + {"category": "FEMALE", "value": 0.625}, + {"category": "_null_filler", "value": 0.03125}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.6847826086956522}, + {"category": "FEMALE", "value": 0.2826086956521739}, + {"category": "_null_filler", "value": 0.03260869565217391}, + ], + ], + }, + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + check_dtype=False, + ) + + def test_loaded_config(penguins_kmeans_model): assert penguins_kmeans_model.n_clusters == 3 diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 8b864d9b55c..4b184b0d4c8 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -21,7 +21,7 @@ import pytz import bigframes -import bigframes.ml.core +from bigframes.ml import core def test_model_eval( @@ -74,9 +74,73 @@ def test_model_eval_with_data(penguins_bqml_linear_model, penguins_df_default_in ) -def test_model_predict( - penguins_bqml_linear_model: bigframes.ml.core.BqmlModel, new_penguins_df -): +def test_model_centroids(penguins_bqml_kmeans_model: core.BqmlModel): + result = penguins_bqml_kmeans_model.centroids().to_pandas() + expected = pd.DataFrame( + { + "centroid_id": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], + "feature": [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + * 3, + "numerical_value": [ + 47.509677, + 14.993548, + 217.040123, + pd.NA, + 38.207813, + 18.03125, + 187.992188, + pd.NA, + 47.036346, + 18.834808, + 197.1612, + pd.NA, + ], + "categorical_value": [ + [], + [], + [], + [ + {"category": ".", "value": 0.008064516129032258}, + {"category": "MALE", "value": 0.49193548387096775}, + {"category": "FEMALE", "value": 0.47580645161290325}, + {"category": "_null_filler", "value": 0.024193548387096774}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.34375}, + {"category": "FEMALE", "value": 0.625}, + {"category": "_null_filler", "value": 0.03125}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.6847826086956522}, + {"category": "FEMALE", "value": 0.2826086956521739}, + {"category": "_null_filler", "value": 0.03260869565217391}, + ], + ], + }, + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + check_dtype=False, + ) + + +def test_model_predict(penguins_bqml_linear_model: core.BqmlModel, new_penguins_df): predictions = penguins_bqml_linear_model.predict(new_penguins_df).to_pandas() expected = pd.DataFrame( {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, @@ -92,7 +156,7 @@ def test_model_predict( def test_model_predict_with_unnamed_index( - penguins_bqml_linear_model: bigframes.ml.core.BqmlModel, new_penguins_df + penguins_bqml_linear_model: core.BqmlModel, new_penguins_df ): # This will result in an index that lacks a name, which the ML library will @@ -121,7 +185,7 @@ def test_model_predict_with_unnamed_index( def test_model_generate_text( - bqml_palm2_text_generator_model: bigframes.ml.core.BqmlModel, llm_text_df + bqml_palm2_text_generator_model: core.BqmlModel, llm_text_df ): options = { "temperature": 0.5, @@ -148,7 +212,7 @@ def test_model_generate_text( assert all(series.str.len() > 20) -def test_model_forecast(time_series_bqml_arima_plus_model: bigframes.ml.core.BqmlModel): +def test_model_forecast(time_series_bqml_arima_plus_model: core.BqmlModel): utc = pytz.utc forecast = time_series_bqml_arima_plus_model.forecast().to_pandas()[ ["forecast_timestamp", "forecast_value"] diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index d7bf3312f09..31b64f4314d 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -357,3 +357,86 @@ def test_cat_with_series(scalars_dfs): pd_result, bf_result, ) + + +def test_str_match(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + pattern = "[A-Z].*" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.match(pattern).to_pandas() + pd_result = scalars_pandas_df[col_name].str.match(pattern) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_str_fullmatch(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + pattern = "[A-Z].*!" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.fullmatch(pattern).to_pandas() + pd_result = scalars_pandas_df[col_name].str.fullmatch(pattern) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_str_get(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.get(8).to_pandas() + pd_result = scalars_pandas_df[col_name].str.get(8) + + print(pd_result) + print(bf_result) + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_str_pad(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.pad(8, side="both", fillchar="%").to_pandas() + pd_result = scalars_pandas_df[col_name].str.pad(8, side="both", fillchar="%") + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_str_ljust(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.ljust(7, fillchar="%").to_pandas() + pd_result = scalars_pandas_df[col_name].str.ljust(7, fillchar="%") + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) + + +def test_str_rjust(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.str.rjust(9, fillchar="%").to_pandas() + pd_result = scalars_pandas_df[col_name].str.rjust(9, fillchar="%") + + assert_series_equal_ignoring_order( + pd_result, + bf_result, + ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index c7e17f5a2dd..2c44dd80670 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -86,6 +86,14 @@ def test_get_column(scalars_dfs): assert_series_equal_ignoring_order(bf_result, pd_result) +def test_get_column_nonstring(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + series = scalars_df.rename(columns={"int64_col": 123.1})[123.1] + bf_result = series.to_pandas() + pd_result = scalars_pandas_df.rename(columns={"int64_col": 123.1})[123.1] + assert_series_equal_ignoring_order(bf_result, pd_result) + + def test_hasattr(scalars_dfs): scalars_df, _ = scalars_dfs assert hasattr(scalars_df, "int64_col") @@ -215,7 +223,7 @@ def test_drop_index_and_columns(scalars_dfs): def test_rename(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - col_name_dict = {"bool_col": "boolean_col"} + col_name_dict = {"bool_col": 1.2345} df_pandas = scalars_df.rename(columns=col_name_dict).to_pandas() pd.testing.assert_index_equal( df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns @@ -1751,6 +1759,16 @@ def test_rename_axis(scalars_df_index, scalars_pandas_df_index): ) +def test_rename_axis_nonstring(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.rename_axis((4,)) + pd_result = scalars_pandas_df_index.rename_axis((4,)) + + pd.testing.assert_frame_equal( + bf_result.to_pandas(), + pd_result, + ) + + def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): pd_string_series = scalars_pandas_df_index.string_col.iloc[[0, 5, 1, 1, 5]] bf_string_series = scalars_df_index.string_col.iloc[[0, 5, 1, 1, 5]] diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 7ad753e1bc0..1f5aa906c8c 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -28,6 +28,8 @@ except ImportError: pandas_gbq = None +import typing + import bigframes import bigframes.dataframe @@ -80,7 +82,7 @@ def test_to_csv_index( if scalars_df.index.name is not None: path = gcs_folder + f"test_index_df_to_csv_index_{index}*.csv" if index: - index_col = scalars_df.index.name + index_col = typing.cast(str, scalars_df.index.name) else: path = gcs_folder + f"test_default_index_df_to_csv_index_{index}*.csv" @@ -118,7 +120,7 @@ def test_to_csv_tabs( pytest.skip("date_format parameter not supported in pandas 1.x.") """Test the `to_csv` API with the `sep` parameter.""" scalars_df, scalars_pandas_df = scalars_dfs - index_col = scalars_df.index.name + index_col = typing.cast(str, scalars_df.index.name) path = gcs_folder + "test_to_csv_tabs*.csv" # TODO(swast): Support "date_format" parameter and make sure our diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index e72d75729be..d5dd4e357bc 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -15,6 +15,8 @@ import pandas as pd import pytest +import bigframes.pandas as bpd + @pytest.mark.parametrize( ("operator"), @@ -86,6 +88,73 @@ def test_dataframe_groupby_aggregate( pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +def test_dataframe_groupby_agg_string(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = scalars_df_index[col_names].groupby("string_col").agg("count") + pd_result = scalars_pandas_df_index[col_names].groupby("string_col").agg("count") + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_frame_equal( + pd_result, + bf_result_computed, + check_dtype=False, + ) + + +def test_dataframe_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = scalars_df_index[col_names].groupby("string_col").agg(["count", "min"]) + pd_result = ( + scalars_pandas_df_index[col_names].groupby("string_col").agg(["count", "min"]) + ) + bf_result_computed = bf_result.to_pandas() + + # Pandas produces multi-index which isn't supported in bq df yet + pd_result = pd_result.set_axis(bf_result.columns, axis=1) + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + +def test_dataframe_groupby_agg_dict(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = ( + scalars_df_index[col_names] + .groupby("string_col") + .agg({"int64_too": ["mean", "max"], "string_col": "count"}) + ) + pd_result = ( + scalars_pandas_df_index[col_names] + .groupby("string_col") + .agg({"int64_too": ["mean", "max"], "string_col": "count"}) + ) + bf_result_computed = bf_result.to_pandas() + + # Pandas produces multi-index which isn't supported in bq df yet + pd_result = pd_result.set_axis(bf_result.columns, axis=1) + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + +def test_dataframe_groupby_agg_named(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = ( + scalars_df_index[col_names] + .groupby("string_col") + .agg( + agg1=bpd.NamedAgg("int64_too", "sum"), + agg2=bpd.NamedAgg("float64_col", "max"), + ) + ) + pd_result = ( + scalars_pandas_df_index[col_names] + .groupby("string_col") + .agg( + agg1=pd.NamedAgg("int64_too", "sum"), agg2=pd.NamedAgg("float64_col", "max") + ) + ) + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + @pytest.mark.parametrize( ("as_index"), [ @@ -173,3 +242,39 @@ def test_dataframe_groupby_getitem_list( ) pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + + +def test_series_groupby_agg_string(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_col"] + .groupby(scalars_df_index["string_col"]) + .agg("count") + ) + pd_result = ( + scalars_pandas_df_index["int64_col"] + .groupby(scalars_pandas_df_index["string_col"]) + .agg("count") + ) + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_series_equal( + pd_result, bf_result_computed, check_dtype=False, check_names=False + ) + + +def test_series_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_col"] + .groupby(scalars_df_index["string_col"]) + .agg(["sum", "mean"]) + ) + pd_result = ( + scalars_pandas_df_index["int64_col"] + .groupby(scalars_pandas_df_index["string_col"]) + .agg(["sum", "mean"]) + ) + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_frame_equal( + pd_result, bf_result_computed, check_dtype=False, check_names=False + ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index fe4b1c5a97a..47d758763ba 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -37,11 +37,11 @@ def bq_cf_connection_location() -> str: @pytest.fixture(scope="module") -def bq_cf_connection_location_mistached() -> str: +def bq_cf_connection_location_mismatched() -> str: """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=us-east1 --project_id=bigframes-dev bigframes-rf-conn + $ bq show --connection --location=eu --project_id=bigframes-dev bigframes-rf-conn """ - return "us-east1.bigframes-rf-conn" + return "eu.bigframes-rf-conn" @pytest.fixture(scope="module") @@ -53,11 +53,11 @@ def bq_cf_connection_location_project() -> str: @pytest.fixture(scope="module") -def bq_cf_connection_location_project_mistached() -> str: +def bq_cf_connection_location_project_mismatched() -> str: """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=us-east1 --project_id=bigframes-metrics bigframes-rf-conn + $ bq show --connection --location=eu --project_id=bigframes-metrics bigframes-rf-conn """ - return "bigframes-metrics.us-east1.bigframes-rf-conn" + return "bigframes-metrics.eu.bigframes-rf-conn" @pytest.fixture(scope="module") @@ -196,50 +196,28 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_direct_no_session_param_location_mistached( +def test_remote_function_direct_no_session_param_location_mismatched( bigquery_client, bigqueryconnection_client, cloudfunctions_client, - scalars_dfs, dataset_id_permanent, - bq_cf_connection_location_mistached, + bq_cf_connection_location_mismatched, ): - @remote_function( - [int], - int, - bigquery_client=bigquery_client, - bigquery_connection_client=bigqueryconnection_client, - cloud_functions_client=cloudfunctions_client, - dataset=dataset_id_permanent, - bigquery_connection=bq_cf_connection_location_mistached, - # See e2e tests for tests that actually deploy the Cloud Function. - reuse=True, - ) - def square(x): - return x * x - - scalars_df, scalars_pandas_df = scalars_dfs - - bf_int64_col = scalars_df["int64_col"] - bf_int64_col_filter = bf_int64_col.notnull() - bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] - bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() - ) - - pd_int64_col = scalars_pandas_df["int64_col"] - pd_int64_col_filter = pd_int64_col.notnull() - pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] - pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) - # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. - # pd_int64_col_filtered.dtype is Int64Dtype() - # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. - # For this test let's force the pandas dtype to be same as bigframes' dtype. - pd_result_col = pd_result_col.astype(pd.Int64Dtype()) - pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + with pytest.raises(ValueError): - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + @remote_function( + [int], + int, + bigquery_client=bigquery_client, + bigquery_connection_client=bigqueryconnection_client, + cloud_functions_client=cloudfunctions_client, + dataset=dataset_id_permanent, + bigquery_connection=bq_cf_connection_location_mismatched, + # See e2e tests for tests that actually deploy the Cloud Function. + reuse=True, + ) + def square(x): + return x * x @pytest.mark.flaky(retries=2, delay=120) @@ -295,7 +273,7 @@ def test_remote_function_direct_no_session_param_project_mismatched( bigqueryconnection_client, cloudfunctions_client, dataset_id_permanent, - bq_cf_connection_location_project_mistached, + bq_cf_connection_location_project_mismatched, ): with pytest.raises(ValueError): @@ -306,7 +284,7 @@ def test_remote_function_direct_no_session_param_project_mismatched( bigquery_connection_client=bigqueryconnection_client, cloud_functions_client=cloudfunctions_client, dataset=dataset_id_permanent, - bigquery_connection=bq_cf_connection_location_project_mistached, + bigquery_connection=bq_cf_connection_location_project_mismatched, # See e2e tests for tests that actually deploy the Cloud Function. reuse=True, ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 1c1e7b035bd..70c56e5e137 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -675,6 +675,51 @@ def test_copy(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_series_equal(bf_copy.to_pandas(), pd_copy) +def test_isin_raise_error(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_too" + with pytest.raises(TypeError): + scalars_df_index[col_name].isin("whatever").to_pandas() + + +@pytest.mark.parametrize( + ( + "col_name", + "test_set", + ), + [ + ( + "int64_col", + [314159, 2.0, 3, pd.NA], + ), + ( + "int64_col", + [2, 55555, 4], + ), + ( + "float64_col", + [-123.456, 1.25, pd.NA], + ), + ( + "int64_too", + [1, 2, pd.NA], + ), + ( + "string_col", + ["Hello, World!", "Hi", "こんにちは"], + ), + ], +) +def test_isin(scalars_dfs, col_name, test_set): + scalars_df, scalars_pandas_df = scalars_dfs + print(type(scalars_pandas_df["datetime_col"].iloc[0])) + bf_result = scalars_df[col_name].isin(test_set).to_pandas() + pd_result = scalars_pandas_df[col_name].isin(test_set).astype("boolean") + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + def test_isnull(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "float64_col" @@ -2197,6 +2242,16 @@ def test_rename(scalars_df_index, scalars_pandas_df_index): ) +def test_rename_nonstring(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.string_col.rename((4, 2)) + pd_result = scalars_pandas_df_index.string_col.rename((4, 2)) + + pd.testing.assert_series_equal( + bf_result.to_pandas(), + pd_result, + ) + + def test_rename_dict_same_type(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.string_col.rename({1: 100, 2: 200}) pd_result = scalars_pandas_df_index.string_col.rename({1: 100, 2: 200}) @@ -2356,3 +2411,41 @@ def test_query_job_setters(scalars_dfs): series.to_pandas() job_ids.add(series.query_job.job_id) assert len(job_ids) == 2 + + +@pytest.mark.parametrize( + ("series_input",), + [ + ([1, 2, 3, 4, 5],), + ([1, 1, 3, 5, 5],), + ([1, pd.NA, 4, 5, 5],), + ([1, 3, 2, 5, 4],), + ([pd.NA, pd.NA],), + ([1, 1, 1, 1, 1],), + ], +) +def test_is_monotonic_increasing(series_input): + scalars_df = series.Series(series_input) + scalars_pandas_df = pd.Series(series_input) + assert ( + scalars_df.is_monotonic_increasing == scalars_pandas_df.is_monotonic_increasing + ) + + +@pytest.mark.parametrize( + ("series_input",), + [ + ([1],), + ([5, 4, 3, 2, 1],), + ([5, 5, 3, 1, 1],), + ([1, pd.NA, 4, 5, 5],), + ([5, pd.NA, 4, 2, 1],), + ([1, 1, 1, 1, 1],), + ], +) +def test_is_monotonic_decreasing(series_input): + scalars_df = series.Series(series_input) + scalars_pandas_df = pd.Series(series_input) + assert ( + scalars_df.is_monotonic_decreasing == scalars_pandas_df.is_monotonic_decreasing + ) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 599b8aabbc9..2fc34f9bae9 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -11,8 +11,10 @@ # 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 random import tempfile +import textwrap import typing from typing import List @@ -160,6 +162,25 @@ def test_read_gbq_w_index_col( assert bf_shape == result.shape +def test_read_gbq_w_anonymous_query_results_table(session: bigframes.Session): + """Ensure BigQuery DataFrames can be used to inspect the results of a query job.""" + query = textwrap.dedent( + """ + SELECT SUM(`number`) AS total_people, name + FROM `bigquery-public-data.usa_names.usa_1910_2013` + GROUP BY name + HAVING name < "B" + """ + ) + job = session.bqclient.query(query) + expected = job.to_dataframe().set_index("name").sort_index() + destination = f"{job.destination.project}.{job.destination.dataset_id}.{job.destination.table_id}" + df = session.read_gbq(destination, index_col="name") + result = df.to_pandas() + expected.index = expected.index.astype(result.index.dtype) + pd.testing.assert_frame_equal(result, expected, check_dtype=False) + + @pytest.mark.parametrize( ("query_or_table", "max_results"), [ diff --git a/tests/unit/core/test_io.py b/tests/unit/core/test_io.py new file mode 100644 index 00000000000..c5074f80c2c --- /dev/null +++ b/tests/unit/core/test_io.py @@ -0,0 +1,49 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime + +import google.cloud.bigquery as bigquery + +import bigframes.core.io + + +def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): + table_ref = bigquery.TableReference.from_string( + "my-test-project._e8166e0cdb.anonbb92cd" + ) + + sql = bigframes.core.io.create_snapshot_sql( + table_ref, datetime.datetime.now(datetime.timezone.utc) + ) + + # Anonymous query results tables don't support time travel. + assert "SYSTEM_TIME" not in sql + + # Need fully-qualified table name. + assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql + + +def test_create_snapshot_sql_doesnt_timetravel_session_datasets(): + table_ref = bigquery.TableReference.from_string("my-test-project._session.abcdefg") + + sql = bigframes.core.io.create_snapshot_sql( + table_ref, datetime.datetime.now(datetime.timezone.utc) + ) + + # We aren't modifying _SESSION tables, so don't use time travel. + assert "SYSTEM_TIME" not in sql + + # Don't need the project ID for _SESSION tables. + assert "my-test-project" not in sql diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 6f5c96da49d..d8c8a2d108e 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -146,6 +146,11 @@ def test_ml_evaluate_no_source_produces_correct_sql(): assert sql == """SELECT * FROM ML.EVALUATE(MODEL `my_dataset.my_model`)""" +def test_ml_centroids_produces_correct_sql(): + sql = ml_sql.ml_centroids(model_name="my_dataset.my_model") + assert sql == """SELECT * FROM ML.CENTROIDS(MODEL `my_dataset.my_model`)""" + + def test_ml_generate_text_produces_correct_sql(): sql = ml_sql.ml_generate_text( model_name="my_dataset.my_model", diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 6975e6edf2b..95822718c3a 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -302,8 +302,49 @@ def expanding(self, *args, **kwargs): class SeriesGroupBy(GroupBy): - pass + def agg(self, func): + """ + Aggregate using one or more operations. + + Args: + func : function, str, list, dict or None + Function to use for aggregating the data. + + Accepted combinations are: + + - string function name + - list of function names, e.g. ``['sum', 'mean']`` + + Returns: + Series or DataFrame + """ + raise NotImplementedError("abstract property") class DataFrameGroupBy(GroupBy): - pass + def agg(self, func, **kwargs): + """ + Aggregate using one or more operations. + + Args: + func (function, str, list, dict or None): + Function to use for aggregating the data. + + Accepted combinations are: + + - string function name + - list of function names, e.g. ``['sum', 'mean']`` + - dict of axis labels -> function names or list of such. + - None, in which case ``**kwargs`` are used with Named Aggregation. Here the + output has one column for each element in ``**kwargs``. The name of the + column is keyword, whereas the value determines the aggregation used to compute + the values in the column. + + kwargs + If ``func`` is None, ``**kwargs`` are used to define the output names and + aggregations via Named Aggregation. See ``func`` entry. + + Returns: + DataFrame + """ + raise NotImplementedError("abstract property") diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 215f7ec4e08..039dc1eae0f 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1572,3 +1572,48 @@ def str(self): after Python’s string methods, with some inspiration from R’s stringr package. """ raise NotImplementedError("abstract property") + + def isin(self, values): + """ + Whether elements in Series are contained in values. + + Return a boolean Series showing whether each element in the Series matches an + element in the passed sequence of values exactly. + + .. note:: + This function treats all NaN-like values(e.g., pd.NA, numpy.nan, None) as + the same. That is, if any form of NaN is present in values, all forms + of NaN in the series will be considered a match. (though pandas may not) + + Args: + values (list-like): + The sequence of values to test. Passing in a single string will raise a + TypeError. Instead, turn a single string into a list of one element. + + Returns: + bigframes.series.Series: Series of booleans indicating if each element is in values. + + Raises: + TypeError: If input is not list-like. + """ + raise NotImplementedError("abstract method") + + @property + def is_monotonic_increasing(self) -> bool: + """ + Return boolean if values in the object are monotonically increasing. + + Returns: + bool + """ + raise NotImplementedError("abstract property") + + @property + def is_monotonic_decreasing(self) -> bool: + """ + Return boolean if values in the object are monotonically decreasing. + + Returns: + bool + """ + raise NotImplementedError("abstract property") diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py index e464843c779..a27093b552c 100644 --- a/third_party/bigframes_vendored/pandas/core/strings/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -314,3 +314,116 @@ def endswith( pattern matches the end of each string element. """ raise NotImplementedError("abstract method") + + def match(self, pat: str, case: bool = True, flags: int = 0): + """ + Determine if each string starts with a match of a regular expression. + + Args: + pat (str): + Character sequence or regular expression. + case (bool): + If True, case sensitive. + flags (int, default 0): + Regex module flags, e.g. re.IGNORECASE. + + Returns: + bigframes.series.Series: Series of boolean values + """ + raise NotImplementedError("abstract method") + + def fullmatch(self, pat: str, case: bool = True, flags: int = 0): + """ + Determine if each string entirely matches a regular expression. + + Args: + pat (str): + Character sequence or regular expression. + case (bool): + If True, case sensitive. + flags (int, default 0): + Regex module flags, e.g. re.IGNORECASE. + + Returns: + bigframes.series.Series: Series of boolean values + """ + raise NotImplementedError("abstract method") + + def get(self, i: int): + """ + Extract element from each component at specified position or with specified key. + + Extract element from lists, tuples, dict, or strings in each element in the + Series/Index. + + Args: + i (int): + Position or key of element to extract. + + Returns: + bigframes.series.Series: Series + """ + raise NotImplementedError("abstract method") + + def pad( + self, + width: int, + side: typing.Literal["left", "right", "both"] = "left", + fillchar: str = " ", + ): + """ + Pad strings in the Series/Index up to width. + + Args: + width (int): + Minimum width of resulting string; additional characters will be filled + with character defined in `fillchar`. + side ({'left', 'right', 'both'}, default 'left'): + Side from which to fill resulting string. + fillchar (str, default ' '): + Additional character for filling, default is whitespace. + + Returns: + bigframes.series.Series: Returns Series or Index with minimum number of char in object. + """ + raise NotImplementedError("abstract method") + + def ljust( + self, + width: int, + fillchar: str = " ", + ): + """ + Pad right side of strings in the Series/Index up to width. + + Args: + width (int): + Minimum width of resulting string; additional characters will be filled + with character defined in `fillchar`. + fillchar (str, default ' '): + Additional character for filling, default is whitespace. + + Returns: + bigframes.series.Series: Returns Series or Index with minimum number of char in object. + """ + raise NotImplementedError("abstract method") + + def rjust( + self, + width: int, + fillchar: str = " ", + ): + """ + Pad left side of strings in the Series/Index up to width. + + Args: + width (int): + Minimum width of resulting string; additional characters will be filled + with character defined in `fillchar`. + fillchar (str, default ' '): + Additional character for filling, default is whitespace. + + Returns: + bigframes.series.Series: Returns Series or Index with minimum number of char in object. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index bddb82c7ba0..068aa4d2908 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -101,3 +101,24 @@ def score( bigframes.dataframe.DataFrame: DataFrame of the metrics. """ raise NotImplementedError("abstract method") + + @property + def cluster_centers_(self): + """Information of cluster centers. + + Returns: + bigframes.dataframe.DataFrame: DataFrame of cluster centers, containing following columns: + centroid_id: An integer that identifies the centroid. + + feature: The column name that contains the feature. + + numerical_value: If feature is numeric, the value of feature for the centroid that centroid_id identifies. If feature is not numeric, the value is NULL. + + categorical_value: An list of mappings containing information about categorical features. Each mapping contains the following fields: + categorical_value.category: The name of each category. + + categorical_value.value: The value of categorical_value.category for the centroid that centroid_id identifies. + + The output contains one row per feature per centroid. + """ + raise NotImplementedError("abstract method") From a32b74751785c8e8aec40ce01df639dd7c4fbb77 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 24 Aug 2023 11:04:40 -0500 Subject: [PATCH 006/921] chore: sync latest changes from internal repo (#7) docs: highlight bigframes is open-source docs: correct the return types of Dataframe and Series docs: create subfolders for notebooks feat: add `bigframes.get_global_session()` and `bigframes.reset_session()` aliases chore: mark ml.llm tests flaky chore: make kokoro/build.sh executable feat: add `Series.str` methods `isalpha`, `isdigit`, `isdecimal`, `isalnum`, `isspace`, `islower`, `isupper`, `zfill`, `center` chore: pin max pytest-retry plugin version in tests docs: sample ML Drug Name Generation notebook docs: add samples and best practices to `read_gbq` docs chore: fix Python download path in docs-presubmit tests perf: add local cache for `__repr_*__` methods feat: support `DataFrame.pivot` fix: don't use query cache for Session construction feat: add `bigframes.pandas.read_pickle` function feat: support MultiIndex for DataFrame columns chore: change the docs kokoro setup to Gerrit path docs: transform remote function user guide into sample code fix: raise exception for invalid function in `read_gbq_function` docs: add release status to table of contents feat: add `fit_transform` to `bigquery.ml` transformers feat: use `pandas.Index` for column labels docs: add ML section under Overview fix: check that types are specified in `read_gbq_function` fix: add error message to `set_index` --- .kokoro/build.sh | 0 .kokoro/docker/docs/Dockerfile | 13 +- .kokoro/docs/common.cfg | 4 +- .kokoro/docs/docs-presubmit.cfg | 2 +- README.rst | 126 +- bigframes/__init__.py | 5 +- bigframes/core/blocks.py | 239 +- bigframes/core/global_session.py | 65 + bigframes/core/groupby/__init__.py | 149 +- bigframes/core/indexers.py | 2 +- bigframes/dataframe.py | 120 +- bigframes/ml/base.py | 24 +- bigframes/ml/compose.py | 2 +- bigframes/ml/preprocessing.py | 4 +- bigframes/operations/__init__.py | 96 +- bigframes/operations/aggregations.py | 32 +- bigframes/operations/strings.py | 41 + bigframes/pandas/__init__.py | 138 +- bigframes/remote_function.py | 83 +- bigframes/series.py | 4 +- bigframes/session.py | 42 +- docs/index.rst | 1 - docs/templates/toc.yml | 1 + .../bigframes.pandas/remote_functions.rst | 134 -- docs/user_guide/index.rst | 9 - notebooks/00 - Summary.ipynb | 2060 ----------------- notebooks/01 - Getting Started.ipynb | 1190 ---------- .../dataframe.ipynb} | 4 +- .../longer_ml_demo.ipynb} | 0 ...q_dataframes_ml_drug_name_generation.ipynb | 980 ++++++++ .../large_language_models.ipynb} | 0 .../bq_dataframes_llm_code_generation.ipynb | 891 +++++++ .../bq_dataframes_ml_linear_regression.ipynb | 743 ++++++ .../getting_started_bq_dataframes.ipynb | 971 ++++++++ .../ml_fundamentals.ipynb} | 0 .../regionalized.ipynb} | 0 .../easy_linear_regression.ipynb} | 0 .../sklearn_linear_regression.ipynb} | 0 .../remote_function.ipynb} | 0 noxfile.py | 50 +- pytest.ini | 1 + samples/snippets/remote_function.py | 147 ++ samples/snippets/remote_function_test.py | 32 + tests/data/hockey_players.json | 37 + tests/data/hockey_players.jsonl | 10 + tests/system/conftest.py | 79 + tests/system/large/ml/test_compose.py | 53 +- tests/system/small/ml/test_llm.py | 8 + tests/system/small/ml/test_preprocessing.py | 68 +- tests/system/small/operations/test_strings.py | 125 +- tests/system/small/test_dataframe.py | 44 + tests/system/small/test_groupby.py | 4 - tests/system/small/test_ipython.py | 28 + tests/system/small/test_multiindex.py | 177 ++ tests/system/small/test_pandas_options.py | 5 +- tests/system/small/test_remote_function.py | 151 +- tests/system/small/test_session.py | 38 + tests/unit/test_dtypes.py | 7 +- .../bigframes_vendored/pandas/core/frame.py | 115 +- .../bigframes_vendored/pandas/core/series.py | 91 +- .../pandas/core/strings/accessor.py | 140 ++ .../bigframes_vendored/pandas/io/gbq.py | 51 +- .../bigframes_vendored/pandas/io/pickle.py | 55 + .../bigframes_vendored/sklearn/base.py | 23 +- .../sklearn/cluster/_kmeans.py | 2 - 65 files changed, 5909 insertions(+), 3807 deletions(-) mode change 100644 => 100755 .kokoro/build.sh create mode 100644 bigframes/core/global_session.py delete mode 100644 docs/user_guide/bigframes.pandas/remote_functions.rst delete mode 100644 docs/user_guide/index.rst delete mode 100644 notebooks/00 - Summary.ipynb delete mode 100644 notebooks/01 - Getting Started.ipynb rename notebooks/{02 - DataFrame.ipynb => dataframes/dataframe.ipynb} (99%) rename notebooks/{99 - Longer ML demo.ipynb => experimental/longer_ml_demo.ipynb} (100%) create mode 100644 notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb rename notebooks/{06 - Using ML - Large Language Models.ipynb => generative_ai/large_language_models.ipynb} (100%) create mode 100644 notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb create mode 100644 notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb create mode 100644 notebooks/getting_started/getting_started_bq_dataframes.ipynb rename notebooks/{03 - Using ML - ML fundamentals.ipynb => getting_started/ml_fundamentals.ipynb} (100%) rename notebooks/{10 - Regionalized.ipynb => location/regionalized.ipynb} (100%) rename notebooks/{05 - Using ML - Easy linear regression.ipynb => regression/easy_linear_regression.ipynb} (100%) rename notebooks/{04 - Using ML - SKLearn linear regression.ipynb => regression/sklearn_linear_regression.ipynb} (100%) rename notebooks/{50 - Remote Function.ipynb => remote_functions/remote_function.ipynb} (100%) create mode 100644 samples/snippets/remote_function.py create mode 100644 samples/snippets/remote_function_test.py create mode 100644 tests/data/hockey_players.json create mode 100644 tests/data/hockey_players.jsonl create mode 100644 tests/system/small/test_ipython.py create mode 100644 third_party/bigframes_vendored/pandas/io/pickle.py diff --git a/.kokoro/build.sh b/.kokoro/build.sh old mode 100644 new mode 100755 diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index d300bee2607..e8ee8191ee1 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -60,19 +60,16 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* \ && rm -f /var/cache/apt/archives/*.deb -###################### Install python 3.9.13 and 3.10.5 +###################### Install python 3.9.13 -# Download python 3.9.13 and 3.10.5 +# Download python 3.9.13 RUN wget https://www.python.org/ftp/python/3.9.13/Python-3.9.13.tgz -RUN wget https://www.python.org/ftp/python/3.9.13/Python-3.10.5.tgz # Extract files RUN tar -xvf Python-3.9.13.tgz -RUN tar -xvf Python-3.10.5.tgz -# Install python 3.9.13 and 3.10.5 +# Install python 3.9.13 RUN ./Python-3.9.13/configure --enable-optimizations -RUN ./Python-3.10.5/configure --enable-optimizations RUN make altinstall ###################### Install pip @@ -82,7 +79,5 @@ RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ # Test pip RUN python3 -m pip -RUN python3.9 -m pip -RUN python3.10 -m pip -CMD ["python3.10"] +CMD ["python3.9"] diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg index 3e36916024a..ce84d7ec49a 100644 --- a/.kokoro/docs/common.cfg +++ b/.kokoro/docs/common.cfg @@ -11,7 +11,7 @@ action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" +build_file: "bigframes/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { @@ -20,7 +20,7 @@ env_vars: { } env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-bigquery-dataframes/.kokoro/publish-docs.sh" + value: "git/bigframes/.kokoro/publish-docs.sh" } env_vars: { diff --git a/.kokoro/docs/docs-presubmit.cfg b/.kokoro/docs/docs-presubmit.cfg index 43ec87185e4..1d0dc4b4991 100644 --- a/.kokoro/docs/docs-presubmit.cfg +++ b/.kokoro/docs/docs-presubmit.cfg @@ -13,7 +13,7 @@ env_vars: { env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-bigquery-dataframes/.kokoro/build.sh" + value: ".kokoro/build.sh" } # Only run this nox session. diff --git a/README.rst b/README.rst index c6dbb059576..6ae3753eed4 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,9 @@ powered by the BigQuery engine. * ``bigframes.pandas`` provides a pandas-compatible API for analytics. * ``bigframes.ml`` provides a scikit-learn-like API for ML. +BigQuery DataFrames is an open-source package. You can run +``pip install --upgrade bigframes`` to install the latest version. + Documentation ------------- @@ -65,6 +68,127 @@ querying is not in the US multi-region. If you try to read a table from another location, you get a NotFound exception. +ML Capabilities +--------------- + +The ML capabilities in BigQuery DataFrames let you preprocess data, and +then train models on that data. You can also chain these actions together to +create data pipelines. + +Preprocess data +^^^^^^^^^^^^^^^^^^^^^^^^ + +Create transformers to prepare data for use in estimators (models) by +using the +`bigframes.ml.preprocessing module `_ +and the `bigframes.ml.compose module `_. +BigQuery DataFrames offers the following transformations: + +* Use the `OneHotEncoder class `_ + in the ``bigframes.ml.preprocessing`` module to transform categorical values into numeric format. +* Use the `StandardScaler class `_ + in the ``bigframes.ml.preprocessing`` module to standardize features by removing the mean and scaling to unit variance. +* Use the `ColumnTransformer class `_ + in the ``bigframes.ml.compose`` module to apply transformers to DataFrames columns. + + +Train models +^^^^^^^^^^^^ + +Create estimators to train models in BigQuery DataFrames. + +**Clustering models** + +Create estimators for clustering models by using the +`bigframes.ml.cluster module `_. + +* Use the `KMeans class `_ + to create K-means clustering models. Use these models for + data segmentation. For example, identifying customer segments. K-means is an + unsupervised learning technique, so model training doesn't require labels or split + data for training or evaluation. + +**Decomposition models** + +Create estimators for decomposition models by using the `bigframes.ml.decomposition module `_. + +* Use the `PCA class `_ + to create principal component analysis (PCA) models. Use these + models for computing principal components and using them to perform a change of + basis on the data. This provides dimensionality reduction by projecting each data + point onto only the first few principal components to obtain lower-dimensional + data while preserving as much of the data's variation as possible. + + +**Ensemble models** + +Create estimators for ensemble models by using the `bigframes.ml.ensemble module `_. + +* Use the `RandomForestClassifier class `_ + to create random forest classifier models. Use these models for constructing multiple + learning method decision trees for classification. +* Use the `RandomForestRegressor class `_ + to create random forest regression models. Use + these models for constructing multiple learning method decision trees for regression. +* Use the `XGBClassifier class `_ + to create gradient boosted tree classifier models. Use these models for additively + constructing multiple learning method decision trees for classification. +* Use the `XGBRegressor class `_ + to create gradient boosted tree regression models. Use these models for additively + constructing multiple learning method decision trees for regression. + + +**Forecasting models** + +Create estimators for forecasting models by using the `bigframes.ml.forecasting module `_. + +* Use the `ARIMAPlus class `_ + to create time series forecasting models. + +**Imported models** + +Create estimators for imported models by using the `bigframes.ml.imported module `_. + +* Use the `ONNXModel class `_ + to import Open Neural Network Exchange (ONNX) models. +* Use the `TensorFlowModel class `_ + to import TensorFlow models. + +**Linear models** + +Create estimators for linear models by using the `bigframes.ml.linear_model module `_. + +* Use the `LinearRegression class `_ + to create linear regression models. Use these models for forecasting. For example, + forecasting the sales of an item on a given day. +* Use the `LogisticRegression class `_ + to create logistic regression models. Use these models for the classification of two + or more possible values such as whether an input is ``low-value``, ``medium-value``, + or ``high-value``. + +**Large language models** + +Create estimators for LLMs by using the `bigframes.ml.llm module `_. + +* Use the `PaLM2TextGenerator class `_ to create PaLM2 text generator models. Use these models + for text generation tasks. +* Use the `PaLM2TextEmbeddingGenerator class `_ to create PaLM2 text embedding generator models. + Use these models for text embedding generation tasks. + + +Create pipelines +^^^^^^^^^^^^^^^^ + +Create ML pipelines by using +`bigframes.ml.pipeline module `_. +Pipelines let you assemble several ML steps to be cross-validated together while setting +different parameters. This simplifies your code, and allows you to deploy data preprocessing +steps and an estimator together. + +* Use the `Pipeline class `_ + to create a pipeline of transforms with a final estimator. + + ML locations ------------ @@ -181,7 +305,7 @@ following IAM roles: Quotas and limits ------------------ +------------------ `BigQuery quotas `_ including hardware, software, and network components. diff --git a/bigframes/__init__.py b/bigframes/__init__.py index 2ee745bc52e..3e54a6d0903 100644 --- a/bigframes/__init__.py +++ b/bigframes/__init__.py @@ -16,13 +16,16 @@ from bigframes._config import options from bigframes._config.bigquery_options import BigQueryOptions +from bigframes.core.global_session import get_global_session, reset_session from bigframes.session import connect, Session from bigframes.version import __version__ __all__ = [ + "options", "BigQueryOptions", + "get_global_session", + "reset_session", "connect", - "options", "Session", "__version__", ] diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index f696b8287bc..2731990febe 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -53,6 +53,10 @@ _BYTES_TO_KILOBYTES = 1024 _BYTES_TO_MEGABYTES = _BYTES_TO_KILOBYTES * 1024 +# This is the max limit of physical columns in BQ +# May choose to set smaller limit for number of block columns to allow overhead for ordering, etc. +_BQ_MAX_COLUMNS = 10000 + # All sampling method _HEAD = "head" _UNIFORM = "uniform" @@ -75,9 +79,9 @@ class Block: def __init__( self, expr: core.ArrayValue, - index_columns: Iterable[str] = (), - column_labels: Optional[Sequence[Label]] = None, - index_labels: Optional[Sequence[Label]] = None, + index_columns: Iterable[str], + column_labels: typing.Union[pd.Index, typing.Sequence[Label]], + index_labels: typing.Union[pd.Index, typing.Sequence[Label], None] = None, ): """Construct a block object, will create default index if no index columns specified.""" if index_labels and (len(index_labels) != len(list(index_columns))): @@ -88,15 +92,18 @@ def __init__( expr, new_index_col_id = expr.promote_offsets() index_columns = [new_index_col_id] self._index_columns = tuple(index_columns) + # Index labels don't need complicated hierarchical access so can store as tuple self._index_labels = ( tuple(index_labels) if index_labels else tuple([None for _ in index_columns]) ) self._expr = self._normalize_expression(expr, self._index_columns) - # TODO(tbergeron): Force callers to provide column labels + # Use pandas index to more easily replicate column indexing, especially for hierarchical column index self._column_labels = ( - tuple(column_labels) if column_labels else tuple(self.value_columns) + column_labels.copy() + if isinstance(column_labels, pd.Index) + else pd.Index(column_labels) ) if len(self.value_columns) != len(self._column_labels): raise ValueError( @@ -139,8 +146,8 @@ def value_columns(self) -> Sequence[str]: ] @property - def column_labels(self) -> List[Label]: - return list(self._column_labels) + def column_labels(self) -> pd.Index: + return self._column_labels @property def expr(self) -> core.ArrayValue: @@ -193,6 +200,24 @@ def index_name_to_col_id(self) -> typing.Mapping[Label, typing.Sequence[str]]: mapping[label] = (*mapping.get(label, ()), id) return mapping + def cols_matching_label(self, partial_label: Label) -> typing.Sequence[str]: + """ + Unlike label_to_col_id, this works with partial labels for multi-index. + + Only some methods, like __getitem__ can use a partial key to get columns + from a dataframe. These methods should use cols_matching_label, while + methods that require exact label matches should use label_to_col_id. + """ + # TODO(tbergeron): Refactor so that all label lookups use this method + if partial_label not in self.column_labels: + return [] + loc = self.column_labels.get_loc(partial_label) + if isinstance(loc, int): + return [self.value_columns[loc]] + if isinstance(loc, slice): + return self.value_columns[loc] + return [col for col, is_present in zip(self.value_columns, loc) if is_present] + def order_by( self, by: typing.Sequence[ordering.OrderingColumnReference], @@ -237,8 +262,9 @@ def reset_index(self, drop: bool = True) -> Block: index_labels=[None], ) else: + # Add index names to column index index_labels = self.index.names - index_labels_rewritten = [] + column_labels_modified = self.column_labels for level, label in enumerate(index_labels): if label is None: if "index" not in self.column_labels: @@ -248,12 +274,17 @@ def reset_index(self, drop: bool = True) -> Block: if label in self.column_labels: raise ValueError(f"cannot insert {label}, already exists") - index_labels_rewritten.append(label) + if isinstance(self.column_labels, pd.MultiIndex): + nlevels = self.column_labels.nlevels + label = tuple(label if i == 0 else "" for i in range(nlevels)) + # Create index copy with label inserted + # See: https://pandas.pydata.org/docs/reference/api/pandas.Index.insert.html + column_labels_modified = column_labels_modified.insert(level, label) block = Block( expr, index_columns=[new_index_col_id], - column_labels=[*index_labels_rewritten, *self.column_labels], + column_labels=column_labels_modified, index_labels=[None], ) return block @@ -568,8 +599,11 @@ def _apply_value_keys_to_expr(self, value_keys: Optional[Iterable[str]] = None): expr = expr.select_columns(itertools.chain(self._index_columns, value_keys)) return expr - def with_column_labels(self, value: typing.Iterable[Label]) -> Block: - label_list = tuple(value) + def with_column_labels( + self, + value: typing.Union[pd.Index, typing.Iterable[Label]], + ) -> Block: + label_list = value.copy() if isinstance(value, pd.Index) else pd.Index(value) if len(label_list) != len(self.value_columns): raise ValueError( f"The column labels size `{len(label_list)} ` should equal to the value" @@ -742,7 +776,9 @@ def create_constant( ) -> typing.Tuple[Block, str]: result_id = guid.generate_guid() expr = self.expr.assign_constant(result_id, scalar_constant, dtype=dtype) - labels = [*self.column_labels, label] + # Create index copy with label inserted + # See: https://pandas.pydata.org/docs/reference/api/pandas.Index.insert.html + labels = self.column_labels.insert(len(self.column_labels), label) return ( Block( expr, @@ -755,8 +791,11 @@ def create_constant( def assign_label(self, column_id: str, new_label: Label) -> Block: col_index = self.value_columns.index(column_id) - new_labels = list(self.column_labels) - new_labels[col_index] = new_label + # Create index copy with label inserted + # See: https://pandas.pydata.org/docs/reference/api/pandas.Index.insert.html + new_labels = self.column_labels.insert(col_index, new_label).delete( + col_index + 1 + ) return self.with_column_labels(new_labels) def filter(self, column_name: str, keep_null: bool = False): @@ -790,7 +829,7 @@ def aggregate_all_and_pivot( result_expr = self.expr.aggregate( aggregations, dropna=dropna ).unpivot_single_row( - row_labels=self.column_labels, + row_labels=self.column_labels.to_list(), index_col_id="index", unpivot_columns=[(value_col_id, self.value_columns)], dtype=dtype, @@ -818,11 +857,28 @@ def drop_columns(self, ids_to_drop: typing.Sequence[str]) -> Block: labels = self._get_labels_for_columns(remaining_value_col_ids) return Block(expr, self.index_columns, labels, self.index.names) - def rename(self, *, columns: typing.Mapping[Label, Label]): - # TODO(tbergeron) Support function(Callable) as columns parameter. - col_labels = [ - (columns.get(col_label, col_label)) for col_label in self.column_labels - ] + def rename( + self, + *, + columns: typing.Mapping[Label, Label] | typing.Callable[[typing.Any], Label], + ): + if isinstance(columns, typing.Mapping): + + def remap_f(x): + return columns.get(x, x) + + else: + remap_f = columns + if isinstance(self.column_labels, pd.MultiIndex): + col_labels: list[Label] = [] + for col_label in self.column_labels: + # Mapper applies to each level separately + modified_label = tuple(remap_f(part) for part in col_label) + col_labels.append(modified_label) + else: + col_labels = [] + for col_label in self.column_labels: + col_labels.append(remap_f(col_label)) return self.with_column_labels(col_labels) def aggregate( @@ -874,10 +930,16 @@ def aggregate( ] by_column_labels = self._get_labels_for_columns(by_value_columns) labels = (*by_column_labels, *aggregate_labels) - result_expr_pruned = result_expr.select_columns( + result_expr_pruned, offsets_id = result_expr.select_columns( [*by_value_columns, *output_col_ids] + ).promote_offsets() + + return ( + Block( + result_expr_pruned, index_columns=[offsets_id], column_labels=labels + ), + output_col_ids, ) - return Block(result_expr_pruned, column_labels=labels), output_col_ids def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): """Gets aggregates immediately, and caches it""" @@ -891,7 +953,12 @@ def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): aggregations = [(column_id, stat, stat.name) for stat in stats_to_fetch] expr = self.expr.aggregate(aggregations) - block = Block(expr, column_labels=[s.name for s in stats_to_fetch]) + expr, offset_index_id = expr.promote_offsets() + block = Block( + expr, + index_columns=[offset_index_id], + column_labels=[s.name for s in stats_to_fetch], + ) df, _ = block.to_pandas() # Carefully extract stats such that they aren't coerced to a common type @@ -988,6 +1055,10 @@ def slice( ) return block + # Using cache to optimize for Jupyter Notebook's behavior where both '__repr__' + # and '__repr_html__' are called in a single display action, reducing redundant + # queries. + @functools.cache def retrieve_repr_request_results( self, max_results: int ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: @@ -1038,13 +1109,7 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: index_labels=self.index.names, ) if axis_number == 1: - expr = self._expr - return Block( - self._expr, - index_columns=self.index_columns, - column_labels=[f"{prefix}{label}" for label in self.column_labels], - index_labels=self.index.names, - ) + return self.rename(columns=lambda label: f"{prefix}{label}") def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: axis_number = bigframes.core.utils.get_axis_number(axis) @@ -1061,13 +1126,110 @@ def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: index_labels=self.index.names, ) if axis_number == 1: - expr = self._expr - return Block( - self._expr, - index_columns=self.index_columns, - column_labels=[f"{label}{suffix}" for label in self.column_labels], - index_labels=self.index.names, + return self.rename(columns=lambda label: f"{label}{suffix}") + + def pivot( + self, + *, + columns: Sequence[str], + values: Sequence[str], + values_in_index: typing.Optional[bool] = None, + ): + # Columns+index should uniquely identify rows + # Warning: This is not validated, breaking this constraint will result in silently non-deterministic behavior. + # -1 to allow for ordering column in addition to pivot columns + max_unique_value = (_BQ_MAX_COLUMNS - 1) // len(values) + columns_values = self._get_unique_values(columns, max_unique_value) + column_index = columns_values + + column_ids: list[str] = [] + block = self + for value in values: + for uvalue in columns_values: + block, masked_id = self._create_pivot_col(block, columns, value, uvalue) + column_ids.append(masked_id) + + block = block.select_columns(column_ids) + aggregations = [(col_id, agg_ops.AnyValueOp()) for col_id in column_ids] + result_block, _ = block.aggregate( + by_column_ids=self.index_columns, + aggregations=aggregations, + as_index=True, + dropna=True, + ) + + if values_in_index or len(values) > 1: + value_labels = self._get_labels_for_columns(values) + column_index = self._create_pivot_column_index(value_labels, columns_values) + else: + column_index = columns_values + + return result_block.with_column_labels(column_index) + + @staticmethod + def _create_pivot_column_index( + value_labels: Sequence[typing.Hashable], columns_values: pd.Index + ): + index_parts = [] + for value in value_labels: + as_frame = columns_values.to_frame() + as_frame.insert(0, None, value) # type: ignore + ipart = pd.MultiIndex.from_frame( + as_frame, names=(None, *columns_values.names) ) + index_parts.append(ipart) + return functools.reduce(lambda x, y: x.append(y), index_parts) + + @staticmethod + def _create_pivot_col( + block: Block, columns: typing.Sequence[str], value_col: str, value + ) -> typing.Tuple[Block, str]: + cond_id = "" + nlevels = len(columns) + for i in range(len(columns)): + uvalue_level = value[i] if nlevels > 1 else value + if pd.isna(uvalue_level): + block, eq_id = block.apply_unary_op( + columns[i], + ops.isnull_op, + ) + else: + block, eq_id = block.apply_unary_op( + columns[i], ops.partial_right(ops.eq_op, uvalue_level) + ) + if cond_id: + block, cond_id = block.apply_binary_op(eq_id, cond_id, ops.and_op) + else: + cond_id = eq_id + block, masked_id = block.apply_binary_op( + value_col, cond_id, ops.partial_arg3(ops.where_op, None) + ) + + return block, masked_id + + def _get_unique_values( + self, columns: Sequence[str], max_unique_values: int + ) -> pd.Index: + """Gets N unique values for a column immediately.""" + # Importing here to avoid circular import + import bigframes.core.block_transforms as block_tf + import bigframes.dataframe as df + + unique_value_block = block_tf.drop_duplicates( + self.select_columns(columns), columns + ) + pd_values = ( + df.DataFrame(unique_value_block).head(max_unique_values + 1).to_pandas() + ) + if len(pd_values) > max_unique_values: + raise ValueError(f"Too many unique values: {pd_values}") + + if len(columns) > 1: + return pd.MultiIndex.from_frame( + pd_values.sort_values(by=list(pd_values.columns), na_position="first") + ) + else: + return pd.Index(pd_values.squeeze(axis=1).sort_values(na_position="first")) def concat( self, @@ -1138,8 +1300,9 @@ def block_from_local(data, session=None, use_index=True) -> Block: ) else: keys_expr = core.ArrayValue.mem_expr_from_pandas(pd_data, session) + keys_expr, offsets_id = keys_expr.promote_offsets() # Constructor will create default range index - return Block(keys_expr, column_labels=column_labels) + return Block(keys_expr, index_columns=[offsets_id], column_labels=column_labels) def _align_block_to_schema( diff --git a/bigframes/core/global_session.py b/bigframes/core/global_session.py new file mode 100644 index 00000000000..68529981cda --- /dev/null +++ b/bigframes/core/global_session.py @@ -0,0 +1,65 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for managing a default, globally available Session object.""" + +import threading +from typing import Callable, Optional, TypeVar + +import bigframes._config +import bigframes.session + +_global_session: Optional[bigframes.session.Session] = None +_global_session_lock = threading.Lock() + + +def reset_session() -> None: + """Start a fresh session the next time a function requires a session. + + Closes the current session if it was already started. + + Returns: + None + """ + global _global_session + + with _global_session_lock: + if _global_session is not None: + _global_session.close() + _global_session = None + + bigframes._config.options.bigquery._session_started = False + + +def get_global_session(): + """Gets the global session. + + Creates the global session if it does not exist. + """ + global _global_session, _global_session_lock + + with _global_session_lock: + if _global_session is None: + _global_session = bigframes.session.connect( + bigframes._config.options.bigquery + ) + + return _global_session + + +_T = TypeVar("_T") + + +def with_default_session(func: Callable[..., _T], *args, **kwargs) -> _T: + return func(get_global_session(), *args, **kwargs) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 10bee4f56d9..5b217effdd1 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -15,7 +15,8 @@ from __future__ import annotations import typing -import warnings + +import pandas as pd import bigframes.constants as constants import bigframes.core as core @@ -102,12 +103,12 @@ def __getitem__( def sum(self, numeric_only: bool = False, *args) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("sum") - return self._aggregate(agg_ops.sum_op, numeric_only=True) + return self._aggregate_all(agg_ops.sum_op, numeric_only=True) def mean(self, numeric_only: bool = False, *args) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("mean") - return self._aggregate(agg_ops.mean_op, numeric_only=True) + return self._aggregate_all(agg_ops.mean_op, numeric_only=True) def median( self, numeric_only: bool = False, *, exact: bool = False @@ -118,13 +119,13 @@ def median( ) if not numeric_only: self._raise_on_non_numeric("median") - return self._aggregate(agg_ops.median_op, numeric_only=True) + return self._aggregate_all(agg_ops.median_op, numeric_only=True) def min(self, numeric_only: bool = False, *args) -> df.DataFrame: - return self._aggregate(agg_ops.min_op, numeric_only=numeric_only) + return self._aggregate_all(agg_ops.min_op, numeric_only=numeric_only) def max(self, numeric_only: bool = False, *args) -> df.DataFrame: - return self._aggregate(agg_ops.max_op, numeric_only=numeric_only) + return self._aggregate_all(agg_ops.max_op, numeric_only=numeric_only) def std( self, @@ -133,7 +134,7 @@ def std( ) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("std") - return self._aggregate(agg_ops.std_op, numeric_only=True) + return self._aggregate_all(agg_ops.std_op, numeric_only=True) def var( self, @@ -142,16 +143,16 @@ def var( ) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("var") - return self._aggregate(agg_ops.var_op, numeric_only=True) + return self._aggregate_all(agg_ops.var_op, numeric_only=True) def all(self) -> df.DataFrame: - return self._aggregate(agg_ops.all_op) + return self._aggregate_all(agg_ops.all_op) def any(self) -> df.DataFrame: - return self._aggregate(agg_ops.any_op) + return self._aggregate_all(agg_ops.any_op) def count(self) -> df.DataFrame: - return self._aggregate(agg_ops.count_op) + return self._aggregate_all(agg_ops.count_op) def cumsum(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: if not numeric_only: @@ -168,71 +169,97 @@ def cumprod(self, *args, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.product_op, numeric_only=True) def agg(self, func=None, **kwargs) -> df.DataFrame: - column_labels = [] if func: - warnings.warn( - "DataFrameGroupby aggregate produces single-level column labels only currently. Subject to change in future versions." - ) if isinstance(func, str): - aggregations = [ - (col_id, agg_ops.AGGREGATIONS_LOOKUP[func]) - for col_id in self._aggregated_columns() - ] + return self._agg_string(func) elif utils.is_dict_like(func): - aggregations = [] - for label, funcs_for_id in func.items(): - col_id = self._resolve_label(label) - func_list = ( - funcs_for_id - if utils.is_list_like(funcs_for_id) - else [funcs_for_id] - ) - for f in func_list: - aggregations.append((col_id, agg_ops.AGGREGATIONS_LOOKUP[f])) - # Pandas creates multi-index here instead - column_labels.append(f"{label}_{f}") + return self._agg_dict(func) elif utils.is_list_like(func): - aggregations = [ - (col_id, agg_ops.AGGREGATIONS_LOOKUP[f]) - for col_id in self._aggregated_columns() - for f in func - ] - column_labels = [ - f"{self._block.col_id_to_label[col_id]}_{f}" - for col_id in self._aggregated_columns() - for f in func - ] + return self._agg_list(func) else: raise NotImplementedError( f"Aggregate with {func} not supported. {constants.FEEDBACK_LINK}" ) else: - aggregations = [] - for k, v in kwargs.items(): - if not isinstance(k, str): - raise NotImplementedError( - f"Only string aggregate names supported. {constants.FEEDBACK_LINK}" - ) - if not hasattr(v, "column") or not hasattr(v, "aggfunc"): - import bigframes.pandas as bpd - - raise NotImplementedError( - f"kwargs values must be {bpd.NamedAgg.__qualname__}" - ) - col_id = self._resolve_label(v.column) - aggregations.append((col_id, agg_ops.AGGREGATIONS_LOOKUP[v.aggfunc])) - column_labels.append(k) + return self._agg_named(**kwargs) + def _agg_string(self, func: str) -> df.DataFrame: + aggregations = [ + (col_id, agg_ops.lookup_agg_func(func)) + for col_id in self._aggregated_columns() + ] agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, as_index=self._as_index, dropna=self._dropna, ) + return df.DataFrame(agg_block) - if column_labels: - agg_block = agg_block.with_column_labels(column_labels) + def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: + aggregations = [] + column_labels = [] + for label, funcs_for_id in func.items(): + col_id = self._resolve_label(label) + func_list = ( + funcs_for_id if utils.is_list_like(funcs_for_id) else [funcs_for_id] + ) + for f in func_list: + aggregations.append((col_id, agg_ops.lookup_agg_func(f))) + column_labels.append((col_id, f)) + agg_block, _ = self._block.aggregate( + by_column_ids=self._by_col_ids, + aggregations=aggregations, + as_index=self._as_index, + dropna=self._dropna, + ) + agg_block = agg_block.with_column_labels( + pd.MultiIndex.from_tuples(column_labels) + ) + return df.DataFrame(agg_block) + def _agg_list(self, func: typing.Sequence) -> df.DataFrame: + aggregations = [ + (col_id, agg_ops.lookup_agg_func(f)) + for col_id in self._aggregated_columns() + for f in func + ] + column_labels = [ + (col_id, f) for col_id in self._aggregated_columns() for f in func + ] + agg_block, _ = self._block.aggregate( + by_column_ids=self._by_col_ids, + aggregations=aggregations, + as_index=self._as_index, + dropna=self._dropna, + ) + agg_block = agg_block.with_column_labels( + pd.MultiIndex.from_tuples(column_labels) + ) + return df.DataFrame(agg_block) + + def _agg_named(self, **kwargs) -> df.DataFrame: + aggregations = [] + column_labels = [] + for k, v in kwargs.items(): + if not isinstance(k, str): + raise NotImplementedError( + f"Only string aggregate names supported. {constants.FEEDBACK_LINK}" + ) + if not hasattr(v, "column") or not hasattr(v, "aggfunc"): + import bigframes.pandas as bpd + + raise TypeError(f"kwargs values must be {bpd.NamedAgg.__qualname__}") + col_id = self._resolve_label(v.column) + aggregations.append((col_id, agg_ops.lookup_agg_func(v.aggfunc))) + column_labels.append(k) + agg_block, _ = self._block.aggregate( + by_column_ids=self._by_col_ids, + aggregations=aggregations, + as_index=self._as_index, + dropna=self._dropna, + ) + agg_block = agg_block.with_column_labels(column_labels) return df.DataFrame(agg_block) aggregate = agg @@ -261,7 +288,7 @@ def _column_type(self, col_id: str) -> dtypes.Dtype: dtype = self._block.dtypes[col_offset] return dtype - def _aggregate( + def _aggregate_all( self, aggregate_op: agg_ops.AggregateOp, numeric_only: bool = False ) -> df.DataFrame: aggregated_col_ids = self._aggregated_columns(numeric_only=numeric_only) @@ -359,11 +386,11 @@ def prod(self, *args) -> series.Series: def agg(self, func=None) -> typing.Union[df.DataFrame, series.Series]: column_names: list[str] = [] if isinstance(func, str): - aggregations = [(self._value_column, agg_ops.AGGREGATIONS_LOOKUP[func])] + aggregations = [(self._value_column, agg_ops.lookup_agg_func(func))] column_names = [func] elif utils.is_list_like(func): aggregations = [ - (self._value_column, agg_ops.AGGREGATIONS_LOOKUP[f]) for f in func + (self._value_column, agg_ops.lookup_agg_func(f)) for f in func ] column_names = list(func) else: diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 0aaf169bea4..46091f211ac 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -29,7 +29,7 @@ import bigframes.series if typing.TYPE_CHECKING: - LocSingleKey = bigframes.series.Series | indexes.Index | slice + LocSingleKey = typing.Union[bigframes.series.Series, indexes.Index, slice] class LocSeriesIndexer: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d4b6e470255..5fbe5d1f9eb 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -174,7 +174,11 @@ def __init__( self._query_job: Optional[bigquery.QueryJob] = None def __dir__(self): - return dir(type(self)) + self._block.column_labels + return dir(type(self)) + [ + label + for label in self._block.column_labels + if label and isinstance(label, str) + ] def _ipython_key_completions_(self) -> List[str]: return list( @@ -201,13 +205,16 @@ def _find_indices( col_ids = self._sql_names(columns, tolerance) return [self._block.value_columns.index(col_id) for col_id in col_ids] - def _resolve_label_exact(self, label) -> str: + def _resolve_label_exact(self, label) -> Optional[str]: + """Returns the column id matching the label if there is exactly + one such column. If there are multiple columns with the same name, + raises an error. If there is no such column, returns None.""" matches = self._block.label_to_col_id.get(label, []) - if len(matches) != 1: + if len(matches) > 1: raise ValueError( - f"Index data must be 1-dimensional. {constants.FEEDBACK_LINK}" + f"Multiple columns matching id {label} were found. {constants.FEEDBACK_LINK}" ) - return matches[0] + return matches[0] if len(matches) != 0 else None def _sql_names( self, @@ -215,7 +222,11 @@ def _sql_names( tolerance: bool = False, ) -> Sequence[str]: """Retrieve sql name (column name in BQ schema) of column(s).""" - labels = columns if utils.is_list_like(columns) else [columns] # type:ignore + labels = ( + columns + if utils.is_list_like(columns) and not isinstance(columns, tuple) + else [columns] + ) # type:ignore results: Sequence[str] = [] for label in labels: col_ids = self._block.label_to_col_id.get(label, []) @@ -246,6 +257,11 @@ def dtypes(self) -> pandas.Series: def columns(self) -> pandas.Index: return self.dtypes.index + @columns.setter + def columns(self, labels: pandas.Index): + new_block = self._block.with_column_labels(labels) + self._set_block(new_block) + @property def shape(self) -> Tuple[int, int]: return self._block.shape @@ -295,7 +311,7 @@ def _to_sql_query( # Has to be unordered as it is impossible to order the sql without # including metadata columns in selection with ibis. ibis_expr = self._block.expr.to_ibis_expr(ordering_mode="unordered") - column_labels = self._block.column_labels + column_labels = list(self._block.column_labels) # TODO(swast): Need to have a better way of controlling when to include # the index or not. @@ -387,11 +403,8 @@ def __getitem__( if isinstance(key, bigframes.series.Series): return self._getitem_bool_series(key) - sql_names = self._sql_names(key) - # Only input is a single key and only find one column, returns a Series - if (not utils.is_list_like(key)) and len(sql_names) == 1: - return bigframes.series.Series(self._block.select_column(sql_names[0])) - + if isinstance(key, typing.Hashable): + return self._getitem_label(key) # Select a subset of columns or re-order columns. # In Ibis after you apply a projection, any column objects from the # table before the projection can't be combined with column objects @@ -410,11 +423,31 @@ def __getitem__( selected_ids: Tuple[str, ...] = () for label in key: - col_ids = self._block.label_to_col_id.get(label, []) + col_ids = self._block.label_to_col_id[label] selected_ids = (*selected_ids, *col_ids) return DataFrame(self._block.select_columns(selected_ids)) + def _getitem_label(self, key: blocks.Label): + col_ids = self._block.cols_matching_label(key) + if len(col_ids) == 0: + raise KeyError(key) + block = self._block.select_columns(col_ids) + if isinstance(self.columns, pandas.MultiIndex): + # Multiindex should drop-level if not selecting entire + key_levels = len(key) if isinstance(key, tuple) else 1 + index_levels = self.columns.nlevels + if key_levels < index_levels: + block = block.with_column_labels( + block.column_labels.droplevel(list(range(key_levels))) + ) + # Force return DataFrame in this case, even if only single column + return DataFrame(block) + + if len(col_ids) == 1: + return bigframes.series.Series(block) + return DataFrame(block) + # Bool Series selects rows def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: if not key.dtype == pandas.BooleanDtype(): @@ -736,7 +769,7 @@ def drop( *, axis: typing.Union[int, str] = 0, index: typing.Any = None, - columns: Union[blocks.Label, Iterable[blocks.Label]] = None, + columns: Union[blocks.Label, Sequence[blocks.Label]] = None, level: typing.Optional[LevelType] = None, ) -> DataFrame: if labels: @@ -767,10 +800,6 @@ def drop( self._block.value_columns ) if columns: - if not utils.is_list_like(columns): - columns = [columns] # type:ignore - columns = list(columns) - block = block.drop_columns(self._sql_names(columns)) if not index and not columns: raise ValueError("Must specify 'labels' or 'index'/'columns") @@ -849,7 +878,7 @@ def _assign_single_item( def _assign_scalar(self, label: str, value: Union[int, float]) -> DataFrame: # TODO(swast): Make sure that k is the ID / SQL name, not a label, # which could be invalid SQL. - col_ids = self._sql_names(label, tolerance=True) + col_ids = self._block.cols_matching_label(label) block, constant_col_id = self._block.create_constant(value, label) for col_id in col_ids: @@ -868,7 +897,7 @@ def _assign_series_join_on_index( ) column_ids = [ - get_column_left(col_id) for col_id in self._sql_names(label, tolerance=True) + get_column_left(col_id) for col_id in self._block.cols_matching_label(label) ] block = joined_index._block source_column = get_column_right(series._value_column) @@ -903,7 +932,12 @@ def set_index( else: keys = typing.cast(typing.Sequence[blocks.Label], tuple(keys)) col_ids = [self._resolve_label_exact(key) for key in keys] - return DataFrame(self._block.set_index(col_ids, append=append, drop=drop)) + missing = [keys[i] for i in range(len(col_ids)) if col_ids[i] is None] + if len(missing) > 0: + raise KeyError(f"None of {missing} are in the columns") + # convert col_ids to non-optional strs since we just determined they are not None + col_ids_strs: List[str] = [col_id for col_id in col_ids if col_id is not None] + return DataFrame(self._block.set_index(col_ids_strs, append=append, drop=drop)) def sort_index( self, ascending: bool = True, na_position: Literal["first", "last"] = "last" @@ -932,7 +966,7 @@ def sort_values( if na_position not in {"first", "last"}: raise ValueError("Param na_position must be one of 'first' or 'last'") - sort_labels = tuple(by) if utils.is_list_like(by) else (by,) + sort_labels = list(by) if utils.is_list_like(by) else [by] sort_column_ids = self._sql_names(sort_labels) len_by = len(sort_labels) @@ -982,9 +1016,11 @@ def value_counts( return bigframes.series.Series(block) def add_prefix(self, prefix: str, axis: int | str | None = None) -> DataFrame: + axis = 1 if axis is None else axis return DataFrame(self._get_block().add_prefix(prefix, axis)) 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 dropna(self) -> DataFrame: @@ -1115,7 +1151,7 @@ def agg( raise NotImplementedError( f"Multiple aggregations only supported on numeric columns. {constants.FEEDBACK_LINK}" ) - aggregations = [agg_ops.AGGREGATIONS_LOOKUP[f] for f in func] + aggregations = [agg_ops.lookup_agg_func(f) for f in func] return DataFrame( self._block.summarize( self._block.value_columns, @@ -1125,7 +1161,7 @@ def agg( else: return bigframes.series.Series( self._block.aggregate_all_and_pivot( - agg_ops.AGGREGATIONS_LOOKUP[typing.cast(str, func)] + agg_ops.lookup_agg_func(typing.cast(str, func)) ) ) @@ -1142,6 +1178,37 @@ def describe(self) -> DataFrame: ) return typing.cast(DataFrame, result) + def pivot( + self, + *, + columns: typing.Union[blocks.Label, Sequence[blocks.Label]], + index: typing.Optional[ + typing.Union[blocks.Label, Sequence[blocks.Label]] + ] = None, + values: typing.Optional[ + typing.Union[blocks.Label, Sequence[blocks.Label]] + ] = None, + ) -> DataFrame: + if index: + block = self.set_index(index)._block + else: + block = self._block + + column_ids = self._sql_names(columns) + if values: + value_col_ids = self._sql_names(values) + else: + value_col_ids = [ + col for col in block.value_columns if col not in column_ids + ] + + pivot_block = block.pivot( + columns=column_ids, + values=value_col_ids, + values_in_index=utils.is_list_like(values), + ) + return DataFrame(pivot_block) + def _drop_non_numeric(self, keep_bool=True) -> DataFrame: types_to_keep = set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) if not keep_bool: @@ -1263,7 +1330,10 @@ def merge( ) # Constructs default index - block = blocks.Block(expr, column_labels=labels) + expr, offset_index_id = expr.promote_offsets() + block = blocks.Block( + expr, index_columns=[offset_index_id], column_labels=labels + ) return DataFrame(block) def _get_merged_col_labels( diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index e4c68eb17c2..9f9d9f85d0e 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -22,9 +22,10 @@ """ import abc -from typing import cast, Optional, TypeVar +from typing import cast, Optional, TypeVar, Union from bigframes.ml import core +import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.base @@ -143,3 +144,24 @@ def score(self, X, y): @abc.abstractmethod def to_gbq(self, model_name, replace): pass + + +class Transformer(BaseEstimator): + """A BigQuery DataFrames Transformer base class that transforms data. + + Also the transformers can be attached to a pipeline with a predictor.""" + + @abc.abstractmethod + def fit(self, X, y): + pass + + @abc.abstractmethod + def transform(self, X): + pass + + def fit_transform( + self, + X: Union[bpd.DataFrame, bpd.Series], + y: Optional[Union[bpd.DataFrame, bpd.Series]] = None, + ) -> bpd.DataFrame: + return self.fit(X, y).transform(X) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 49b4899bebd..df01303ffac 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -34,7 +34,7 @@ class ColumnTransformer( third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, - base.BaseEstimator, + base.Transformer, ): __doc__ = ( third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer.__doc__ diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 500a9fcb244..ee46a370520 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -30,7 +30,7 @@ class StandardScaler( third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler, - base.BaseEstimator, + base.Transformer, ): __doc__ = ( third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler.__doc__ @@ -106,7 +106,7 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: class OneHotEncoder( third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, - base.BaseEstimator, + base.Transformer, ): # BQML max value https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder#syntax TOP_K_DEFAULT = 1000000 diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 7bdd97812e9..58f19ea8e7f 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -76,6 +76,12 @@ def _as_ibis(self, x: ibis_types.Value): return x.notnull() +class HashOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.IntegerValue, x).hash() + + +## String Operation class ReverseOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.StringValue, x).reverse() @@ -100,7 +106,58 @@ class IsNumericOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): # catches all members of the Unicode number class, which matches pandas isnumeric # see https://cloud.google.com/bigquery/docs/reference/standard-sql/string_functions#regexp_contains - return typing.cast(ibis_types.StringValue, x).re_search(r"^(\pN*)$") + # TODO: Validate correctness, my miss eg ⅕ character + return typing.cast(ibis_types.StringValue, x).re_search(r"^(\pN+)$") + + +class IsAlphaOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_search( + r"^(\p{Lm}|\p{Lt}|\p{Lu}|\p{Ll}|\p{Lo})+$" + ) + + +class IsDigitOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + # Based on docs, should include superscript/subscript-ed numbers + # Tests however pass only when set to Nd unicode class + return typing.cast(ibis_types.StringValue, x).re_search(r"^(\p{Nd})+$") + + +class IsDecimalOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_search(r"^(\p{Nd})+$") + + +class IsAlnumOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_search( + r"^(\p{N}|\p{Lm}|\p{Lt}|\p{Lu}|\p{Ll}|\p{Lo})+$" + ) + + +class IsSpaceOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + # All characters are whitespace characters, False for empty string + return typing.cast(ibis_types.StringValue, x).re_search(r"^\s+$") + + +class IsLowerOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + # No upper case characters, min one cased character + # See: https://docs.python.org/3/library/stdtypes.html#str + return typing.cast(ibis_types.StringValue, x).re_search( + r"\p{Ll}" + ) & ~typing.cast(ibis_types.StringValue, x).re_search(r"\p{Lu}|\p{Lt}") + + +class IsUpperOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + # No lower case characters, min one cased character + # See: https://docs.python.org/3/library/stdtypes.html#str + return typing.cast(ibis_types.StringValue, x).re_search( + r"\p{Lu}" + ) & ~typing.cast(ibis_types.StringValue, x).re_search(r"\p{Ll}|\p{Lt}") class RstripOp(UnaryOp): @@ -227,11 +284,25 @@ def _as_ibis(self, x: ibis_types.Value): return any_match if any_match is not None else ibis_types.literal(False) -class HashOp(UnaryOp): +class ZfillOp(UnaryOp): + def __init__(self, width: int): + self._width = width + def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.IntegerValue, x).hash() + str_value = typing.cast(ibis_types.StringValue, x) + return ( + ibis.case() + .when( + str_value[0] == "-", + "-" + + StrPadOp(self._width - 1, "0", "left")._as_ibis(str_value.substr(1)), + ) + .else_(StrPadOp(self._width, "0", "left")._as_ibis(str_value)) + .end() + ) +## Datetime Ops class DayOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.TimestampValue, x).day() @@ -390,7 +461,14 @@ def _as_ibis(self, x: ibis_types.Value): lower_op = LowerOp() upper_op = UpperOp() strip_op = StripOp() +isalnum_op = IsAlnumOp() +isalpha_op = IsAlphaOp() +isdecimal_op = IsDecimalOp() +isdigit_op = IsDigitOp() isnumeric_op = IsNumericOp() +isspace_op = IsSpaceOp() +islower_op = IsLowerOp() +isupper_op = IsUpperOp() rstrip_op = RstripOp() lstrip_op = LstripOp() hash_op = HashOp() @@ -692,6 +770,18 @@ def clip_op( ) +def partial_arg1(op: TernaryOp, scalar: typing.Any) -> BinaryOp: + return lambda x, y: op(dtypes.literal_to_ibis_scalar(scalar, validate=False), x, y) + + +def partial_arg2(op: TernaryOp, scalar: typing.Any) -> BinaryOp: + return lambda x, y: op(x, dtypes.literal_to_ibis_scalar(scalar, validate=False), y) + + +def partial_arg3(op: TernaryOp, scalar: typing.Any) -> BinaryOp: + return lambda x, y: op(x, y, dtypes.literal_to_ibis_scalar(scalar, validate=False)) + + def is_null(value) -> bool: # float NaN/inf should be treated as distinct from 'true' null values return typing.cast(bool, pd.isna(value)) and not isinstance(value, float) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 1687f705a1e..874c264194c 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -251,6 +251,21 @@ def skips_nulls(self): return False +class AnyValueOp(AggregateOp): + # Warning: only use if all values are equal. Non-deterministic otherwise. + # Do not expose to users. For special cases only (e.g. pivot). + name = "any_value" + + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.IntegerValue: + return _apply_window_if_present(column.arbitrary(), window) + + @property + def skips_nulls(self): + return True + + class RankOp(WindowOp): name = "rank" @@ -381,7 +396,7 @@ def _map_to_literal( # TODO: Alternative names and lookup from numpy function objects -AGGREGATIONS_LOOKUP: dict[str, AggregateOp] = { +_AGGREGATIONS_LOOKUP: dict[str, AggregateOp] = { op.name: op for op in [ sum_op, @@ -401,3 +416,18 @@ def _map_to_literal( ApproxQuartilesOp(3), ] } + + +def lookup_agg_func(key: str) -> AggregateOp: + if callable(key): + raise NotImplementedError( + "Aggregating with callable object not supported, pass method name as string instead (eg. 'sum' instead of np.sum)." + ) + if not isinstance(key, str): + raise ValueError( + f"Cannot aggregate using object of type: {type(key)}. Use string method name (eg. 'sum')" + ) + if key in _AGGREGATIONS_LOOKUP: + return _AGGREGATIONS_LOOKUP[key] + else: + raise ValueError(f"Unrecognize aggregate function: {key}") diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 0f1395c78f2..0545ea34d6a 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -70,6 +70,41 @@ def upper(self) -> series.Series: def isnumeric(self) -> series.Series: return self._apply_unary_op(ops.isnumeric_op) + def isalpha( + self, + ) -> series.Series: + return self._apply_unary_op(ops.isalpha_op) + + def isdigit( + self, + ) -> series.Series: + return self._apply_unary_op(ops.isdigit_op) + + def isdecimal( + self, + ) -> series.Series: + return self._apply_unary_op(ops.isdecimal_op) + + def isalnum( + self, + ) -> series.Series: + return self._apply_unary_op(ops.isalnum_op) + + def isspace( + self, + ) -> series.Series: + return self._apply_unary_op(ops.isspace_op) + + def islower( + self, + ) -> series.Series: + return self._apply_unary_op(ops.islower_op) + + def isupper( + self, + ) -> series.Series: + return self._apply_unary_op(ops.isupper_op) + def rstrip(self) -> series.Series: return self._apply_unary_op(ops.rstrip_op) @@ -183,6 +218,12 @@ def endswith( pat = (pat,) return self._apply_unary_op(ops.EndsWithOp(pat)) + def zfill(self, width: int) -> series.Series: + return self._apply_unary_op(ops.ZfillOp(width)) + + def center(self, width: int, fillchar: str = " ") -> series.Series: + return self._apply_unary_op(ops.StrPadOp(width, fillchar, "both")) + def cat( self, others: Union[str, series.Series], diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index ed7a09e7b71..b688c18723b 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -18,7 +18,6 @@ from collections import namedtuple import inspect -import threading import typing from typing import ( Any, @@ -32,15 +31,21 @@ Optional, Sequence, Tuple, - TypeVar, Union, ) from google.cloud import bigquery import numpy import pandas +from pandas._typing import ( + CompressionOptions, + FilePath, + ReadPickleBuffer, + StorageOptions, +) import bigframes._config as config +import bigframes.core.global_session as global_session import bigframes.core.indexes import bigframes.core.reshape import bigframes.dataframe @@ -49,14 +54,6 @@ import third_party.bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile -# Support pandas dtype attribute -NA = pandas.NA -BooleanDtype = pandas.BooleanDtype -Float64Dtype = pandas.Float64Dtype -Int64Dtype = pandas.Int64Dtype -StringDtype = pandas.StringDtype -ArrowDtype = pandas.ArrowDtype - # Include method definition so that the method appears in our docs for # bigframes.pandas general functions. @@ -135,52 +132,6 @@ def cut( cut.__doc__ = vendored_pandas_tile.cut.__doc__ -options = config.options -"""Global :class:`~bigframes._config.Options` to configure BigQuery DataFrames.""" - -_global_session: Optional[bigframes.session.Session] = None -_global_session_lock = threading.Lock() - - -def reset_session() -> None: - """Start a fresh session the next time a function requires a session. - - Closes the current session if it was already started. - - Returns: - None - """ - global _global_session - - with _global_session_lock: - if _global_session is not None: - _global_session.close() - _global_session = None - - options.bigquery._session_started = False - - -def get_global_session(): - """Gets the global session. - - Creates the global session if it does not exist. - """ - global _global_session, _global_session_lock - - with _global_session_lock: - if _global_session is None: - _global_session = bigframes.session.connect(options.bigquery) - - return _global_session - - -_T = TypeVar("_T") - - -def _with_default_session(func: Callable[..., _T], *args, **kwargs) -> _T: - return func(get_global_session(), *args, **kwargs) - - def _set_default_session_location_if_possible(query): # Set the location as per the query if this is the first query the user is # running and: @@ -257,7 +208,7 @@ def read_csv( encoding: Optional[str] = None, **kwargs, ) -> bigframes.dataframe.DataFrame: - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_csv, filepath_or_buffer=filepath_or_buffer, sep=sep, @@ -283,7 +234,7 @@ def read_gbq( max_results: Optional[int] = None, ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_gbq, query, index_col=index_col, @@ -296,7 +247,7 @@ def read_gbq( def read_gbq_model(model_name: str): - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_gbq_model, model_name, ) @@ -313,7 +264,7 @@ def read_gbq_query( max_results: Optional[int] = None, ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_gbq_query, query, index_col=index_col, @@ -333,7 +284,7 @@ def read_gbq_table( max_results: Optional[int] = None, ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_gbq_table, query, index_col=index_col, @@ -346,7 +297,7 @@ def read_gbq_table( def read_pandas(pandas_dataframe: pandas.DataFrame) -> bigframes.dataframe.DataFrame: - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_pandas, pandas_dataframe, ) @@ -355,8 +306,24 @@ def read_pandas(pandas_dataframe: pandas.DataFrame) -> bigframes.dataframe.DataF read_pandas.__doc__ = inspect.getdoc(bigframes.session.Session.read_pandas) +def read_pickle( + filepath_or_buffer: FilePath | ReadPickleBuffer, + compression: CompressionOptions = "infer", + storage_options: StorageOptions = None, +): + return global_session.with_default_session( + bigframes.session.Session.read_pickle, + filepath_or_buffer=filepath_or_buffer, + compression=compression, + storage_options=storage_options, + ) + + +read_pickle.__doc__ = inspect.getdoc(bigframes.session.Session.read_pickle) + + def read_parquet(path: str | IO["bytes"]) -> bigframes.dataframe.DataFrame: - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_parquet, path, ) @@ -372,7 +339,7 @@ def remote_function( bigquery_connection: Optional[str] = None, reuse: bool = True, ): - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.remote_function, input_types=input_types, output_type=output_type, @@ -386,7 +353,7 @@ def remote_function( def read_gbq_function(function_name: str): - return _with_default_session( + return global_session.with_default_session( bigframes.session.Session.read_gbq_function, function_name=function_name, ) @@ -395,25 +362,58 @@ def read_gbq_function(function_name: str): read_gbq_function.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_function) -# Other aliases +# pandas dtype attributes +NA = pandas.NA +BooleanDtype = pandas.BooleanDtype +Float64Dtype = pandas.Float64Dtype +Int64Dtype = pandas.Int64Dtype +StringDtype = pandas.StringDtype +ArrowDtype = pandas.ArrowDtype + +# Class aliases +# TODO(swast): Make these real classes so we can refer to these in type +# checking and docstrings. DataFrame = bigframes.dataframe.DataFrame Index = bigframes.core.indexes.Index Series = bigframes.series.Series -# Used by DataFrameGroupby.agg +# Other public pandas attributes NamedAgg = namedtuple("NamedAgg", ["column", "aggfunc"]) +options = config.options +"""Global :class:`~bigframes._config.Options` to configure BigQuery DataFrames.""" + +# Session management APIs +get_global_session = global_session.get_global_session +reset_session = global_session.reset_session + + # Use __all__ to let type checkers know what is part of the public API. __all___ = [ + # Functions "concat", - "DataFrame", - "options", "read_csv", "read_gbq", "read_gbq_function", "read_gbq_model", "read_pandas", + "read_pickle", "remote_function", + # pandas dtype attributes + "NA", + "BooleanDtype", + "Float64Dtype", + "Int64Dtype", + "StringDtype", + "ArrowDtype" + # Class aliases + "DataFrame", + "Index", "Series", + # Other public pandas attributes "NamedAgg", + "options", + # Session management APIs + "get_global_session", + "reset_session", ] diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 27e2b8f7c23..7cf74d63111 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -35,15 +35,10 @@ import cloudpickle import google.api_core.exceptions from google.cloud import bigquery, bigquery_connection_v1, functions_v2 -from google.cloud.bigquery.routine import Routine -from google.cloud.bigquery.standard_sql import StandardSqlTypeNames from ibis.backends.bigquery.compiler import compiles from ibis.backends.bigquery.datatypes import BigQueryType -from ibis.expr.datatypes.core import boolean from ibis.expr.datatypes.core import DataType as IbisDataType from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type -from ibis.expr.datatypes.core import float64, int64 -from ibis.expr.datatypes.core import string as ibis_string import ibis.expr.operations as ops import ibis.expr.rules as rlz @@ -63,11 +58,16 @@ # Input and output types supported by BigQuery DataFrames remote functions. # TODO(shobs): Extend the support to all types supported by BQ remote functions # https://cloud.google.com/bigquery/docs/remote-functions#limitations -_supported_io_ibis_types = {boolean, float64, int64, ibis_string} -TYPE_ERROR_MESSAGE_FORMAT = ( - f"Type {{}} not supported, supported types are {_supported_io_ibis_types}. " - f"{constants.FEEDBACK_LINK}" -) +SUPPORTED_IO_PYTHON_TYPES = {bool, float, int, str} +SUPPORTED_IO_BIGQUERY_TYPEKINDS = { + "BOOLEAN", + "BOOL", + "FLOAT", + "FLOAT64", + "INT64", + "INTEGER", + "STRING", +} def get_remote_function_locations(bq_location): @@ -116,7 +116,7 @@ def routine_ref_to_string_for_query(routine_ref: bigquery.RoutineReference) -> s class IbisSignature(NamedTuple): parameter_names: List[str] - input_types: List[IbisDataType] + input_types: List[Optional[IbisDataType]] output_type: IbisDataType @@ -512,7 +512,7 @@ def remote_function_node( """Creates an Ibis node representing a remote function call.""" fields = { - name: rlz.value(type_) + name: rlz.value(type_) if type_ else rlz.any for name, type_ in zip( ibis_signature.parameter_names, ibis_signature.input_types ) @@ -538,20 +538,22 @@ def f(*args, **kwargs): return f +class UnsupportedTypeError(ValueError): + def __init__(self, type_, supported_types): + self.type = type_ + self.supported_types = supported_types + + def ibis_type_from_python_type(t: type) -> IbisDataType: - ibis_type = python_type_to_bigquery_type(t) - assert ibis_type in _supported_io_ibis_types, TYPE_ERROR_MESSAGE_FORMAT.format( - ibis_type - ) - return ibis_type + if t not in SUPPORTED_IO_PYTHON_TYPES: + raise UnsupportedTypeError(t, SUPPORTED_IO_PYTHON_TYPES) + return python_type_to_bigquery_type(t) -def ibis_type_from_type_kind(tk: StandardSqlTypeNames) -> IbisDataType: - ibis_type = BigQueryType.to_ibis(tk) - assert ibis_type in _supported_io_ibis_types, TYPE_ERROR_MESSAGE_FORMAT.format( - ibis_type - ) - return ibis_type +def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> IbisDataType: + if tk not in SUPPORTED_IO_BIGQUERY_TYPEKINDS: + raise UnsupportedTypeError(tk, SUPPORTED_IO_BIGQUERY_TYPEKINDS) + return BigQueryType.to_ibis(tk) def ibis_signature_from_python_signature( @@ -566,13 +568,18 @@ def ibis_signature_from_python_signature( ) -def ibis_signature_from_routine( - routine: Routine, -) -> IbisSignature: +class ReturnTypeMissingError(ValueError): + pass + + +def ibis_signature_from_routine(routine: bigquery.Routine) -> IbisSignature: + if not routine.return_type: + raise ReturnTypeMissingError + return IbisSignature( parameter_names=[arg.name for arg in routine.arguments], input_types=[ - ibis_type_from_type_kind(arg.data_type.type_kind) + ibis_type_from_type_kind(arg.data_type.type_kind) if arg.data_type else None for arg in routine.arguments ], output_type=ibis_type_from_type_kind(routine.return_type.type_kind), @@ -584,9 +591,7 @@ class DatasetMissingError(ValueError): def get_routine_reference( - routine_ref_str: str, - bigquery_client: bigquery.Client, - session: Optional[Session], + routine_ref_str: str, bigquery_client: bigquery.Client, session: Optional[Session] ) -> bigquery.RoutineReference: try: # Handle cases ".." and @@ -859,7 +864,21 @@ def read_gbq_function( ) # Find the routine and get its arguments. - routine = bigquery_client.get_routine(routine_ref) - ibis_signature = ibis_signature_from_routine(routine) + try: + routine = bigquery_client.get_routine(routine_ref) + except google.api_core.exceptions.NotFound: + raise ValueError(f"Unknown function '{routine_ref}'. {constants.FEEDBACK_LINK}") + + try: + ibis_signature = ibis_signature_from_routine(routine) + except ReturnTypeMissingError: + raise ValueError( + "Function return type must be specified. {constants.FEEDBACK_LINK}" + ) + except UnsupportedTypeError as e: + raise ValueError( + f"Type {e.type} not supported, supported types are {e.supported_types}. " + f"{constants.FEEDBACK_LINK}" + ) return remote_function_node(routine_ref, ibis_signature) diff --git a/bigframes/series.py b/bigframes/series.py index f8f44dc2e64..a1da93dee38 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -651,7 +651,7 @@ def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: raise NotImplementedError( f"Multiple aggregations only supported on numeric series. {constants.FEEDBACK_LINK}" ) - aggregations = [agg_ops.AGGREGATIONS_LOOKUP[f] for f in func] + aggregations = [agg_ops.lookup_agg_func(f) for f in func] return Series( self._block.summarize( [self._value_column], @@ -661,7 +661,7 @@ def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: else: return self._apply_aggregation( - agg_ops.AGGREGATIONS_LOOKUP[typing.cast(str, func)] + agg_ops.lookup_agg_func(typing.cast(str, func)) ) def skew(self): diff --git a/bigframes/session.py b/bigframes/session.py index 73fdd731063..3ef52507460 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -54,6 +54,12 @@ import ibis.expr.types as ibis_types import numpy as np import pandas +from pandas._typing import ( + CompressionOptions, + FilePath, + ReadPickleBuffer, + StorageOptions, +) import pydata_google_auth import bigframes._config.bigquery_options as bigquery_options @@ -75,6 +81,7 @@ import third_party.bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import third_party.bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet import third_party.bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers +import third_party.bigframes_vendored.pandas.io.pickle as third_party_pandas_pickle _ENV_DEFAULT_PROJECT = "GOOGLE_CLOUD_PROJECT" _APPLICATION_NAME = f"bigframes/{bigframes.version.__version__}" @@ -194,6 +201,7 @@ def _create_cloud_clients( class Session( third_party_pandas_gbq.GBQIOMixin, third_party_pandas_parquet.ParquetIOMixin, + third_party_pandas_pickle.PickleIOMixin, third_party_pandas_readers.ReaderIOMixin, ): """Establishes a BigQuery connection to capture a group of job activities related to @@ -252,6 +260,8 @@ def _create_and_bind_bq_session(self): """Create a BQ session and bind the session id with clients to capture BQ activities: go/bigframes-transient-data""" job_config = bigquery.QueryJobConfig(create_session=True) + # Make sure the session is a new one, not one associated with another query. + job_config.use_query_cache = False query_job = self.bqclient.query( "SELECT 1", job_config=job_config, location=self._location ) @@ -458,8 +468,8 @@ def read_gbq_table( {self.ibis_client.compile(distinct_table)} ) - SELECT (SELECT COUNT(*) FROM full_table) AS total_count, - (SELECT COUNT(*) FROM distinct_table) AS distinct_count + SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, + (SELECT COUNT(*) FROM distinct_table) AS `distinct_count` """ results, query_job = self._start_query(is_unique_sql) row = next(iter(results)) @@ -467,6 +477,7 @@ def read_gbq_table( total_count = row["total_count"] distinct_count = row["distinct_count"] is_total_ordering = total_count == distinct_count + ordering = core.ExpressionOrdering( ordering_value_columns=[ core.OrderingColumnReference(column_id) for column_id in index_cols @@ -477,7 +488,6 @@ def read_gbq_table( # We have a total ordering, so query via "time travel" so that # the underlying data doesn't mutate. if is_total_ordering: - # Get the timestamp from the job metadata rather than the query # text so that the query for determining uniqueness of the ID # columns can be cached. @@ -663,7 +673,8 @@ def _read_ibis( core.ArrayValue( self, table_expression, columns, hidden_ordering_columns, ordering ), - [index_col.get_name() for index_col in index_cols], + index_columns=[index_col.get_name() for index_col in index_cols], + column_labels=column_keys, index_labels=index_labels, ) @@ -887,6 +898,25 @@ def read_csv( ) return self.read_pandas(pandas_df) + def read_pickle( + self, + filepath_or_buffer: FilePath | ReadPickleBuffer, + compression: CompressionOptions = "infer", + storage_options: StorageOptions = None, + ): + pandas_obj = pandas.read_pickle( + filepath_or_buffer, + compression=compression, + storage_options=storage_options, + ) + + if isinstance(pandas_obj, pandas.Series): + if pandas_obj.name is None: + pandas_obj.name = "0" + bigframes_df = self.read_pandas(pandas_obj.to_frame()) + return bigframes_df[bigframes_df.columns[0]] + return self.read_pandas(pandas_obj) + def read_parquet( self, path: str | IO["bytes"], @@ -1086,6 +1116,10 @@ def read_gbq_function( Then it can be applied to a DataFrame or Series. + .. note:: + The return type of the function must be explicitly specified in the + function's original definition even if not otherwise required. + Args: function_name (str): the function's name in BigQuery in the format diff --git a/docs/index.rst b/docs/index.rst index ff1cd09eb7d..d239ea3a785 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,6 @@ API reference .. toctree:: :maxdepth: 3 - user_guide/index reference/index Changelog diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 91abc59bc0a..891f15a51b4 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -128,3 +128,4 @@ name: preprocessing name: bigframes.ml name: BigQuery DataFrames + status: beta diff --git a/docs/user_guide/bigframes.pandas/remote_functions.rst b/docs/user_guide/bigframes.pandas/remote_functions.rst deleted file mode 100644 index 7540ba8a28c..00000000000 --- a/docs/user_guide/bigframes.pandas/remote_functions.rst +++ /dev/null @@ -1,134 +0,0 @@ - -Using the Remote Functions -========================== - -BigQuery DataFrames gives you the ability to turn your custom scalar functions -into a BigQuery remote function. It requires the GCP project to be set up -appropriately and the user having sufficient privileges to use them. One can -find more details on it via `help` command. - -.. code-block:: python - - import bigframes.pandas as bpd - help(bpd.remote_function) - -Read a table and inspect the column of interest. - -.. code-block:: python - - df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") - df["body_mass_g"].head(10) - -Define a custom function, and specify the intent to turn it into a remote -function. It requires a BigQuery connection. If the connection is not already -created, BigQuery DataFrames will attempt to create one assuming the necessary -APIs and IAM permissions are setup in the project. In our examples we would be -using a pre-created connection named `bigframes-rf-conn`. Let's try a -`pandas`-like use case in which we want to apply a user defined scalar function -to every value in a `Series`, more specifically bucketize the `body_mass_g` value -of the penguins, which is a real number, into a category, which is a string. - -.. code-block:: python - - @bpd.remote_function([float], str, bigquery_connection='bigframes-rf-conn') - def get_bucket(num): - if not num: return "NA" - boundary = 4000 - return "at_or_above_4000" if num >= boundary else "below_4000" - -Then we can apply the remote function on the `Series`` of interest via `apply` -API and store the result in a new column in the DataFrame. - -.. code-block:: python - - df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket)) - -This will add a new column `body_mass_bucket` in the DataFrame. You can preview -the original value and the bucketized value side by side. - -.. code-block:: python - - df[['body_mass_g', 'body_mass_bucket']].head(10) - -This operation was possible by doing all the computation on the cloud. For that, -there is a google cloud function deployed by serializing the user code. - -.. warning:: - The deployed cloud function may be visible to other users with sufficient - privilege in the project. The user should be careful about having any - sensitive data in the code that will be deployed as a remote function. - -The cloud function can be located from a property set in the remote function object. - -.. code-block:: python - - get_bucket.bigframes_cloud_function - -and then there is a BigQuery remote function created configured to call into the -cloud function via the BigQuery connection. That can also be located from -another property set in the remote function object. - -.. code-block:: python - - get_bucket.bigframes_remote_function - -The cloud assets created are persistant and the user can manage them directy -from the Google Cloud Console. - -Let's continue trying other potential use cases of remote functions. Let's say -we consider the `species`, `island` and `sex` of the penguins sensitive -information and want to redact that by replacing with their hash code instead. -Let's define another scalar custom function and decorated it as a remote function: - -.. code-block:: python - - @bpd.remote_function([str], str, bigquery_connection='bigframes-rf-conn') - def get_hash(input): - import hashlib - # handle missing value - if input is None: - input = "" - encoded_input = input.encode() - hash = hashlib.md5(encoded_input) - return hash.hexdigest() - -We can use this remote function in another `pandas`-like API `map` that can be -applied on a DataFrame: - -.. code-block:: python - - df_redacted = df[["species", "island", "sex"]].map(get_hash) - df_redacted.head(10). - -Using Existing Functions -======================== - -If you have already defined a custom function in BigQuery, either in the -BigQuery Google Cloud Console or with the `remote_function` decorator above or -otherwise, you may use it with BigQuery DataFrames with the `read_gbq_function` -method. - -More details are available via the `help` command: - -.. code-block:: python - - import bigframes.pandas as pd - help(pd.read_gbq_function) - -Here is an example of using `read_gbq_function` to load an existing function -named `get_bucket`: - -.. code-block:: python - - import bigframes.pandas as pd - - df = pd.read_gbq("bigquery-public-data.ml_datasets.penguins") - get_bucket = pd.read_gbq_function("get_bucket") - - df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket)) - df.head(10) - -Note: As mentioned above, if a function is created using the `remote_function` -decorator, its generated name (including project and dataset) is accessible -immediately afterward in the function's `bigframes_remote_function` attribute. -The same string can be passed to `read_gbq_function` later in another context. diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst deleted file mode 100644 index 915e172159c..00000000000 --- a/docs/user_guide/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. _user_guide: - -User Guide -========== - -.. toctree:: - :maxdepth: 2 - - bigframes.pandas/remote_functions diff --git a/notebooks/00 - Summary.ipynb b/notebooks/00 - Summary.ipynb deleted file mode 100644 index 66ac9a8de84..00000000000 --- a/notebooks/00 - Summary.ipynb +++ /dev/null @@ -1,2060 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using the BigQuery DataFrames API" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Set BigQuery DataFrames options" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import bigframes.pandas\n", - "\n", - "bigframes.pandas.options.bigquery.project = \"bigframes-dev\"\n", - "bigframes.pandas.options.bigquery.location = \"us\"" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Initialize a dataframe for a BigQuery table" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "df = bigframes.pandas.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## View the DataFrame" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Adelie Penguin (Pygoscelis adeliae)Dream36.618.4184.03475.0FEMALE
1Adelie Penguin (Pygoscelis adeliae)Dream39.819.1184.04650.0MALE
2Adelie Penguin (Pygoscelis adeliae)Dream40.918.9184.03900.0MALE
3Chinstrap penguin (Pygoscelis antarctica)Dream46.517.9192.03500.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Dream37.316.8192.03000.0FEMALE
5Adelie Penguin (Pygoscelis adeliae)Dream43.218.5192.04100.0MALE
6Chinstrap penguin (Pygoscelis antarctica)Dream46.916.6192.02700.0FEMALE
7Chinstrap penguin (Pygoscelis antarctica)Dream50.518.4200.03400.0FEMALE
8Chinstrap penguin (Pygoscelis antarctica)Dream49.519.0200.03800.0MALE
9Adelie Penguin (Pygoscelis adeliae)Dream40.220.1200.03975.0MALE
10Adelie Penguin (Pygoscelis adeliae)Dream40.818.9208.04300.0MALE
11Adelie Penguin (Pygoscelis adeliae)Dream39.018.7185.03650.0MALE
12Adelie Penguin (Pygoscelis adeliae)Dream37.016.9185.03000.0FEMALE
13Chinstrap penguin (Pygoscelis antarctica)Dream47.017.3185.03700.0FEMALE
14Adelie Penguin (Pygoscelis adeliae)Dream34.017.1185.03400.0FEMALE
15Adelie Penguin (Pygoscelis adeliae)Dream37.016.5185.03400.0FEMALE
16Chinstrap penguin (Pygoscelis antarctica)Dream45.717.3193.03600.0FEMALE
17Chinstrap penguin (Pygoscelis antarctica)Dream50.619.4193.03800.0MALE
18Adelie Penguin (Pygoscelis adeliae)Dream39.717.9193.04250.0MALE
19Adelie Penguin (Pygoscelis adeliae)Dream37.818.1193.03750.0MALE
\n", - "
[344 rows x 7 columns in total]" - ], - "text/plain": [ - " species island culmen_length_mm \\\n", - "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", - "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", - "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", - "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", - "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", - "5 Adelie Penguin (Pygoscelis adeliae) Dream 43.2 \n", - "6 Chinstrap penguin (Pygoscelis antarctica) Dream 46.9 \n", - "7 Chinstrap penguin (Pygoscelis antarctica) Dream 50.5 \n", - "8 Chinstrap penguin (Pygoscelis antarctica) Dream 49.5 \n", - "9 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "10 Adelie Penguin (Pygoscelis adeliae) Dream 40.8 \n", - "11 Adelie Penguin (Pygoscelis adeliae) Dream 39.0 \n", - "12 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "13 Chinstrap penguin (Pygoscelis antarctica) Dream 47.0 \n", - "14 Adelie Penguin (Pygoscelis adeliae) Dream 34.0 \n", - "15 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "16 Chinstrap penguin (Pygoscelis antarctica) Dream 45.7 \n", - "17 Chinstrap penguin (Pygoscelis antarctica) Dream 50.6 \n", - "18 Adelie Penguin (Pygoscelis adeliae) Dream 39.7 \n", - "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.8 \n", - "20 Chinstrap penguin (Pygoscelis antarctica) Dream 46.6 \n", - "21 Chinstrap penguin (Pygoscelis antarctica) Dream 51.3 \n", - "22 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "23 Adelie Penguin (Pygoscelis adeliae) Dream 36.8 \n", - "24 Chinstrap penguin (Pygoscelis antarctica) Dream 49.6 \n", - "\n", - " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "0 18.4 184.0 3475.0 FEMALE \n", - "1 19.1 184.0 4650.0 MALE \n", - "2 18.9 184.0 3900.0 MALE \n", - "3 17.9 192.0 3500.0 FEMALE \n", - "4 16.8 192.0 3000.0 FEMALE \n", - "5 18.5 192.0 4100.0 MALE \n", - "6 16.6 192.0 2700.0 FEMALE \n", - "7 18.4 200.0 3400.0 FEMALE \n", - "8 19.0 200.0 3800.0 MALE \n", - "9 20.1 200.0 3975.0 MALE \n", - "10 18.9 208.0 4300.0 MALE \n", - "11 18.7 185.0 3650.0 MALE \n", - "12 16.9 185.0 3000.0 FEMALE \n", - "13 17.3 185.0 3700.0 FEMALE \n", - "14 17.1 185.0 3400.0 FEMALE \n", - "15 16.5 185.0 3400.0 FEMALE \n", - "16 17.3 193.0 3600.0 FEMALE \n", - "17 19.4 193.0 3800.0 MALE \n", - "18 17.9 193.0 4250.0 MALE \n", - "19 18.1 193.0 3750.0 MALE \n", - "20 17.8 193.0 3800.0 FEMALE \n", - "21 19.2 193.0 3650.0 MALE \n", - "22 17.1 193.0 3400.0 FEMALE \n", - "23 18.5 193.0 3500.0 FEMALE \n", - "24 18.2 193.0 3775.0 MALE \n", - "...\n", - "\n", - "[344 rows x 7 columns]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### View the column names in the dataframe (aka columns names in the table)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Index(['species', 'island', 'culmen_length_mm', 'culmen_depth_mm',\n", - " 'flipper_length_mm', 'body_mass_g', 'sex'],\n", - " dtype='object')" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df.columns" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### View the table schema" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "species string[pyarrow]\n", - "island string[pyarrow]\n", - "culmen_length_mm Float64\n", - "culmen_depth_mm Float64\n", - "flipper_length_mm Float64\n", - "body_mass_g Float64\n", - "sex string[pyarrow]\n", - "dtype: object" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df.dtypes" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Select a subset of columns" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speciesislandbody_mass_g
0Adelie Penguin (Pygoscelis adeliae)Dream3475.0
1Adelie Penguin (Pygoscelis adeliae)Dream4650.0
2Adelie Penguin (Pygoscelis adeliae)Dream3900.0
3Chinstrap penguin (Pygoscelis antarctica)Dream3500.0
4Adelie Penguin (Pygoscelis adeliae)Dream3000.0
5Adelie Penguin (Pygoscelis adeliae)Dream4100.0
6Chinstrap penguin (Pygoscelis antarctica)Dream2700.0
7Chinstrap penguin (Pygoscelis antarctica)Dream3400.0
8Chinstrap penguin (Pygoscelis antarctica)Dream3800.0
9Adelie Penguin (Pygoscelis adeliae)Dream3975.0
10Adelie Penguin (Pygoscelis adeliae)Dream4300.0
11Adelie Penguin (Pygoscelis adeliae)Dream3650.0
12Adelie Penguin (Pygoscelis adeliae)Dream3000.0
13Chinstrap penguin (Pygoscelis antarctica)Dream3700.0
14Adelie Penguin (Pygoscelis adeliae)Dream3400.0
15Adelie Penguin (Pygoscelis adeliae)Dream3400.0
16Chinstrap penguin (Pygoscelis antarctica)Dream3600.0
17Chinstrap penguin (Pygoscelis antarctica)Dream3800.0
18Adelie Penguin (Pygoscelis adeliae)Dream4250.0
19Adelie Penguin (Pygoscelis adeliae)Dream3750.0
\n", - "
[344 rows x 3 columns in total]" - ], - "text/plain": [ - " species island body_mass_g\n", - "0 Adelie Penguin (Pygoscelis adeliae) Dream 3475.0\n", - "1 Adelie Penguin (Pygoscelis adeliae) Dream 4650.0\n", - "2 Adelie Penguin (Pygoscelis adeliae) Dream 3900.0\n", - "3 Chinstrap penguin (Pygoscelis antarctica) Dream 3500.0\n", - "4 Adelie Penguin (Pygoscelis adeliae) Dream 3000.0\n", - "5 Adelie Penguin (Pygoscelis adeliae) Dream 4100.0\n", - "6 Chinstrap penguin (Pygoscelis antarctica) Dream 2700.0\n", - "7 Chinstrap penguin (Pygoscelis antarctica) Dream 3400.0\n", - "8 Chinstrap penguin (Pygoscelis antarctica) Dream 3800.0\n", - "9 Adelie Penguin (Pygoscelis adeliae) Dream 3975.0\n", - "10 Adelie Penguin (Pygoscelis adeliae) Dream 4300.0\n", - "11 Adelie Penguin (Pygoscelis adeliae) Dream 3650.0\n", - "12 Adelie Penguin (Pygoscelis adeliae) Dream 3000.0\n", - "13 Chinstrap penguin (Pygoscelis antarctica) Dream 3700.0\n", - "14 Adelie Penguin (Pygoscelis adeliae) Dream 3400.0\n", - "15 Adelie Penguin (Pygoscelis adeliae) Dream 3400.0\n", - "16 Chinstrap penguin (Pygoscelis antarctica) Dream 3600.0\n", - "17 Chinstrap penguin (Pygoscelis antarctica) Dream 3800.0\n", - "18 Adelie Penguin (Pygoscelis adeliae) Dream 4250.0\n", - "19 Adelie Penguin (Pygoscelis adeliae) Dream 3750.0\n", - "20 Chinstrap penguin (Pygoscelis antarctica) Dream 3800.0\n", - "21 Chinstrap penguin (Pygoscelis antarctica) Dream 3650.0\n", - "22 Adelie Penguin (Pygoscelis adeliae) Dream 3400.0\n", - "23 Adelie Penguin (Pygoscelis adeliae) Dream 3500.0\n", - "24 Chinstrap penguin (Pygoscelis antarctica) Dream 3775.0\n", - "...\n", - "\n", - "[344 rows x 3 columns]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df = df[[\n", - " \"species\",\n", - " \"island\",\n", - " \"body_mass_g\",\n", - "]]\n", - "df" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### View the first ten values of a series" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 3475.0\n", - "1 4650.0\n", - "2 3900.0\n", - "3 3500.0\n", - "4 3000.0\n", - "5 4100.0\n", - "6 2700.0\n", - "7 3400.0\n", - "8 3800.0\n", - "9 3975.0\n", - "Name: body_mass_g, dtype: Float64" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df['body_mass_g'].head(10)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Compute the mean of a series" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "4201.7543859649095" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df['body_mass_g'].mean()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Filter the DataFrame" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speciesislandbody_mass_g
1Adelie Penguin (Pygoscelis adeliae)Dream4650.0
5Adelie Penguin (Pygoscelis adeliae)Dream4100.0
10Adelie Penguin (Pygoscelis adeliae)Dream4300.0
18Adelie Penguin (Pygoscelis adeliae)Dream4250.0
25Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
26Adelie Penguin (Pygoscelis adeliae)Dream4000.0
27Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
28Chinstrap penguin (Pygoscelis antarctica)Dream4300.0
30Chinstrap penguin (Pygoscelis antarctica)Dream4450.0
36Adelie Penguin (Pygoscelis adeliae)Dream4450.0
44Chinstrap penguin (Pygoscelis antarctica)Dream4100.0
45Chinstrap penguin (Pygoscelis antarctica)Dream4800.0
57Chinstrap penguin (Pygoscelis antarctica)Dream4400.0
61Chinstrap penguin (Pygoscelis antarctica)Dream4150.0
66Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
67Chinstrap penguin (Pygoscelis antarctica)Dream4050.0
68Chinstrap penguin (Pygoscelis antarctica)Dream4100.0
74Adelie Penguin (Pygoscelis adeliae)Dream4350.0
77Adelie Penguin (Pygoscelis adeliae)Dream4150.0
78Adelie Penguin (Pygoscelis adeliae)Dream4400.0
\n", - "
[177 rows x 3 columns in total]" - ], - "text/plain": [ - " species island body_mass_g\n", - "1 Adelie Penguin (Pygoscelis adeliae) Dream 4650.0\n", - "5 Adelie Penguin (Pygoscelis adeliae) Dream 4100.0\n", - "10 Adelie Penguin (Pygoscelis adeliae) Dream 4300.0\n", - "18 Adelie Penguin (Pygoscelis adeliae) Dream 4250.0\n", - "25 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", - "26 Adelie Penguin (Pygoscelis adeliae) Dream 4000.0\n", - "27 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", - "28 Chinstrap penguin (Pygoscelis antarctica) Dream 4300.0\n", - "30 Chinstrap penguin (Pygoscelis antarctica) Dream 4450.0\n", - "36 Adelie Penguin (Pygoscelis adeliae) Dream 4450.0\n", - "44 Chinstrap penguin (Pygoscelis antarctica) Dream 4100.0\n", - "45 Chinstrap penguin (Pygoscelis antarctica) Dream 4800.0\n", - "57 Chinstrap penguin (Pygoscelis antarctica) Dream 4400.0\n", - "61 Chinstrap penguin (Pygoscelis antarctica) Dream 4150.0\n", - "66 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", - "67 Chinstrap penguin (Pygoscelis antarctica) Dream 4050.0\n", - "68 Chinstrap penguin (Pygoscelis antarctica) Dream 4100.0\n", - "74 Adelie Penguin (Pygoscelis adeliae) Dream 4350.0\n", - "77 Adelie Penguin (Pygoscelis adeliae) Dream 4150.0\n", - "78 Adelie Penguin (Pygoscelis adeliae) Dream 4400.0\n", - "80 Chinstrap penguin (Pygoscelis antarctica) Dream 4300.0\n", - "90 Chinstrap penguin (Pygoscelis antarctica) Dream 4150.0\n", - "92 Chinstrap penguin (Pygoscelis antarctica) Dream 4500.0\n", - "93 Adelie Penguin (Pygoscelis adeliae) Dream 4300.0\n", - "94 Chinstrap penguin (Pygoscelis antarctica) Dream 4550.0\n", - "...\n", - "\n", - "[177 rows x 3 columns]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df[df['body_mass_g'] >= 4000.0]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using the Remote Functions" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### BigQuery DataFrames gives you the ability to turn your custom scalar functions into a BigQuery remote function.\n", - "\n", - "It requires the GCP project to be set up appropriately and the user having sufficient privileges to use them. One can find more details on it via `help` command." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on function remote_function in module bigframes.pandas:\n", - "\n", - "remote_function(input_types: 'List[type]', output_type: 'type', dataset: 'Optional[str]' = None, bigquery_connection: 'Optional[str]' = None, reuse: 'bool' = True)\n", - " Decorator to turn a user defined function into a BigQuery remote function.\n", - " \n", - " Args:\n", - " input_types (list(type)):\n", - " List of input data types in the user defined function.\n", - " output_type (type):\n", - " Data type of the output in the user defined function.\n", - " dataset (str, Optional):\n", - " Dataset to use to create a BigQuery function. It should be in\n", - " `.` or `` format. If this\n", - " param is not provided then session dataset id would be used.\n", - " bigquery_connection (str, Optional):\n", - " Name of the BigQuery connection. If it is pre created in the same\n", - " location as the `bigquery_client.location` then it would be used,\n", - " otherwise it would be created dynamically assuming the user has\n", - " necessary priviliges. If this param is not provided then the\n", - " bigquery connection from the session would be used.\n", - " reuse (bool, Optional):\n", - " Reuse the remote function if already exists.\n", - " `True` by default, which will result in reusing an existing remote\n", - " function (if any) that was previously created for the same udf.\n", - " Setting it to false would force creating a unique remote function.\n", - " If the required remote function does not exist then it would be\n", - " created irrespective of this param.\n", - " \n", - " Notes:\n", - " Please make sure following is setup before using this API:\n", - " \n", - " 1. Have the below APIs enabled for your project:\n", - " a. BigQuery Connection API\n", - " b. Cloud Functions API\n", - " c. Cloud Run API\n", - " d. Cloud Build API\n", - " e. Artifact Registry API\n", - " f. Cloud Resource Manager API\n", - " \n", - " This can be done from the cloud console (change PROJECT_ID to yours):\n", - " https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID\n", - " Or from the gcloud CLI:\n", - " $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com\n", - " \n", - " 2. Have following IAM roles enabled for you:\n", - " a. BigQuery Data Editor (roles/bigquery.dataEditor)\n", - " b. BigQuery Connection Admin (roles/bigquery.connectionAdmin)\n", - " c. Cloud Functions Developer (roles/cloudfunctions.developer)\n", - " d. Service Account User (roles/iam.serviceAccountUser)\n", - " e. Storage Object Viewer (roles/storage.objectViewer)\n", - " f. Project IAM Admin (roles/resourcemanager.projectIamAdmin)\n", - " (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.)\n", - " \n", - " 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set:\n", - " a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection\n", - " b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function\n", - " Alternatively, the IAM could also be setup via the gcloud CLI:\n", - " $ gcloud projects add-iam-policy-binding PROJECT_ID --member=\"serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID\" --role=\"roles/run.invoker\"\n", - "\n" - ] - } - ], - "source": [ - "import bigframes.pandas as pd\n", - "help(pd.remote_function)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Define a custom function, and specify the intent to turn it into a remote function.\n", - "\n", - "It requires a BigQuery connection. If the connection is not already created,\n", - "the BigQuery DataFrames package attempts to create one assuming the necessary\n", - "APIs and IAM permissions are setup in the project." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO][2023-06-28 23:31:49,355][bigframes.remote_function] Creating new cloud function: gcloud functions deploy bigframes-b3fab64f5997ad6a516379defe8d4202 --gen2 --runtime=python310 --project=bigframes-dev --region=us-central1 --source=/tmp/tmp9w5e89lh --entry-point=udf_http --trigger-http --no-allow-unauthenticated\n", - "Preparing function...\n", - ".done.\n", - "Deploying function...\n", - "[Build]..........................................................................................................................................................................................................................................................................................................................................................................................................................done\n", - "[Service].........................................................................................................................................................................................................done\n", - "Done.\n", - "You can view your function in the Cloud Console here: https://console.cloud.google.com/functions/details/us-central1/bigframes-b3fab64f5997ad6a516379defe8d4202?project=bigframes-dev\n", - "\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "buildConfig:\n", - " build: projects/1084210331973/locations/us-central1/builds/780b1780-9b38-4515-ae60-89d05454ef83\n", - " entryPoint: udf_http\n", - " runtime: python310\n", - " source:\n", - " storageSource:\n", - " bucket: gcf-v2-sources-1084210331973-us-central1\n", - " object: bigframes-b3fab64f5997ad6a516379defe8d4202/function-source.zip\n", - " sourceProvenance:\n", - " resolvedStorageSource:\n", - " bucket: gcf-v2-sources-1084210331973-us-central1\n", - " generation: '1687995112300727'\n", - " object: bigframes-b3fab64f5997ad6a516379defe8d4202/function-source.zip\n", - "environment: GEN_2\n", - "labels:\n", - " deployment-tool: cli-gcloud\n", - "name: projects/bigframes-dev/locations/us-central1/functions/bigframes-b3fab64f5997ad6a516379defe8d4202\n", - "serviceConfig:\n", - " allTrafficOnLatestRevision: true\n", - " availableCpu: '0.1666'\n", - " availableMemory: 256M\n", - " ingressSettings: ALLOW_ALL\n", - " maxInstanceCount: 100\n", - " maxInstanceRequestConcurrency: 1\n", - " revision: bigframes-b3fab64f5997ad6a516379defe8d4202-00001-tut\n", - " service: projects/bigframes-dev/locations/us-central1/services/bigframes-b3fab64f5997ad6a516379defe8d4202\n", - " serviceAccountEmail: 1084210331973-compute@developer.gserviceaccount.com\n", - " timeoutSeconds: 60\n", - " uri: https://bigframes-b3fab64f5997ad6a516379defe8d4202-7krlje3eoq-uc.a.run.app\n", - "state: ACTIVE\n", - "updateTime: '2023-06-28T23:32:51.911131997Z'\n", - "url: https://us-central1-bigframes-dev.cloudfunctions.net/bigframes-b3fab64f5997ad6a516379defe8d4202\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO][2023-06-28 23:32:55,330][bigframes.remote_function] Successfully created cloud function bigframes-b3fab64f5997ad6a516379defe8d4202 with uri (https://bigframes-b3fab64f5997ad6a516379defe8d4202-7krlje3eoq-uc.a.run.app)\n", - "[INFO][2023-06-28 23:32:59,378][bigframes.remote_function] Connector bigframes-rf-conn already exists\n", - "[INFO][2023-06-28 23:32:59,379][bigframes.remote_function] Creating BQ remote function: \n", - " CREATE OR REPLACE FUNCTION `bigframes-dev.bigframes_temp_us`.bigframes_b3fab64f5997ad6a516379defe8d4202(num FLOAT64)\n", - " RETURNS STRING\n", - " REMOTE WITH CONNECTION `bigframes-dev.us.bigframes-rf-conn`\n", - " OPTIONS (\n", - " endpoint = \"https://bigframes-b3fab64f5997ad6a516379defe8d4202-7krlje3eoq-uc.a.run.app\"\n", - " )\n", - "[INFO][2023-06-28 23:33:00,338][bigframes.remote_function] Created remote function bigframes-dev.bigframes_temp_us.bigframes_b3fab64f5997ad6a516379defe8d4202\n" - ] - } - ], - "source": [ - "@pd.remote_function([float], str, bigquery_connection='bigframes-rf-conn')\n", - "def get_bucket(num):\n", - " if not num: return \"NA\"\n", - " boundary = 4000\n", - " return \"at_or_above_4000\" if num >= boundary else \"below_4000\"" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Run the custom function on the BigQuery-backed dataframe" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
body_mass_gbody_mass_bucket
03475.0below_4000
14650.0at_or_above_4000
23900.0below_4000
33500.0below_4000
43000.0below_4000
54100.0at_or_above_4000
62700.0below_4000
73400.0below_4000
83800.0below_4000
93975.0below_4000
\n", - "
[10 rows x 2 columns in total]" - ], - "text/plain": [ - " body_mass_g body_mass_bucket\n", - "0 3475.0 below_4000\n", - "1 4650.0 at_or_above_4000\n", - "2 3900.0 below_4000\n", - "3 3500.0 below_4000\n", - "4 3000.0 below_4000\n", - "5 4100.0 at_or_above_4000\n", - "6 2700.0 below_4000\n", - "7 3400.0 below_4000\n", - "8 3800.0 below_4000\n", - "9 3975.0 below_4000\n", - "\n", - "[10 rows x 2 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df = df.assign(body_mass_bucket=df['body_mass_g'].apply(get_bucket))\n", - "df[['body_mass_g', 'body_mass_bucket']].head(10)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using the ML API" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Initialize a DataFrame from a BigQuery table" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Adelie Penguin (Pygoscelis adeliae)Dream36.618.4184.03475.0FEMALE
1Adelie Penguin (Pygoscelis adeliae)Dream39.819.1184.04650.0MALE
2Adelie Penguin (Pygoscelis adeliae)Dream40.918.9184.03900.0MALE
3Chinstrap penguin (Pygoscelis antarctica)Dream46.517.9192.03500.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Dream37.316.8192.03000.0FEMALE
5Adelie Penguin (Pygoscelis adeliae)Dream43.218.5192.04100.0MALE
6Chinstrap penguin (Pygoscelis antarctica)Dream46.916.6192.02700.0FEMALE
7Chinstrap penguin (Pygoscelis antarctica)Dream50.518.4200.03400.0FEMALE
8Chinstrap penguin (Pygoscelis antarctica)Dream49.519.0200.03800.0MALE
9Adelie Penguin (Pygoscelis adeliae)Dream40.220.1200.03975.0MALE
10Adelie Penguin (Pygoscelis adeliae)Dream40.818.9208.04300.0MALE
11Adelie Penguin (Pygoscelis adeliae)Dream39.018.7185.03650.0MALE
12Adelie Penguin (Pygoscelis adeliae)Dream37.016.9185.03000.0FEMALE
13Chinstrap penguin (Pygoscelis antarctica)Dream47.017.3185.03700.0FEMALE
14Adelie Penguin (Pygoscelis adeliae)Dream34.017.1185.03400.0FEMALE
15Adelie Penguin (Pygoscelis adeliae)Dream37.016.5185.03400.0FEMALE
16Chinstrap penguin (Pygoscelis antarctica)Dream45.717.3193.03600.0FEMALE
17Chinstrap penguin (Pygoscelis antarctica)Dream50.619.4193.03800.0MALE
18Adelie Penguin (Pygoscelis adeliae)Dream39.717.9193.04250.0MALE
19Adelie Penguin (Pygoscelis adeliae)Dream37.818.1193.03750.0MALE
\n", - "
[344 rows x 7 columns in total]" - ], - "text/plain": [ - " species island culmen_length_mm \\\n", - "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", - "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", - "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", - "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", - "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", - "5 Adelie Penguin (Pygoscelis adeliae) Dream 43.2 \n", - "6 Chinstrap penguin (Pygoscelis antarctica) Dream 46.9 \n", - "7 Chinstrap penguin (Pygoscelis antarctica) Dream 50.5 \n", - "8 Chinstrap penguin (Pygoscelis antarctica) Dream 49.5 \n", - "9 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "10 Adelie Penguin (Pygoscelis adeliae) Dream 40.8 \n", - "11 Adelie Penguin (Pygoscelis adeliae) Dream 39.0 \n", - "12 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "13 Chinstrap penguin (Pygoscelis antarctica) Dream 47.0 \n", - "14 Adelie Penguin (Pygoscelis adeliae) Dream 34.0 \n", - "15 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "16 Chinstrap penguin (Pygoscelis antarctica) Dream 45.7 \n", - "17 Chinstrap penguin (Pygoscelis antarctica) Dream 50.6 \n", - "18 Adelie Penguin (Pygoscelis adeliae) Dream 39.7 \n", - "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.8 \n", - "20 Chinstrap penguin (Pygoscelis antarctica) Dream 46.6 \n", - "21 Chinstrap penguin (Pygoscelis antarctica) Dream 51.3 \n", - "22 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "23 Adelie Penguin (Pygoscelis adeliae) Dream 36.8 \n", - "24 Chinstrap penguin (Pygoscelis antarctica) Dream 49.6 \n", - "\n", - " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "0 18.4 184.0 3475.0 FEMALE \n", - "1 19.1 184.0 4650.0 MALE \n", - "2 18.9 184.0 3900.0 MALE \n", - "3 17.9 192.0 3500.0 FEMALE \n", - "4 16.8 192.0 3000.0 FEMALE \n", - "5 18.5 192.0 4100.0 MALE \n", - "6 16.6 192.0 2700.0 FEMALE \n", - "7 18.4 200.0 3400.0 FEMALE \n", - "8 19.0 200.0 3800.0 MALE \n", - "9 20.1 200.0 3975.0 MALE \n", - "10 18.9 208.0 4300.0 MALE \n", - "11 18.7 185.0 3650.0 MALE \n", - "12 16.9 185.0 3000.0 FEMALE \n", - "13 17.3 185.0 3700.0 FEMALE \n", - "14 17.1 185.0 3400.0 FEMALE \n", - "15 16.5 185.0 3400.0 FEMALE \n", - "16 17.3 193.0 3600.0 FEMALE \n", - "17 19.4 193.0 3800.0 MALE \n", - "18 17.9 193.0 4250.0 MALE \n", - "19 18.1 193.0 3750.0 MALE \n", - "20 17.8 193.0 3800.0 FEMALE \n", - "21 19.2 193.0 3650.0 MALE \n", - "22 17.1 193.0 3400.0 FEMALE \n", - "23 18.5 193.0 3500.0 FEMALE \n", - "24 18.2 193.0 3775.0 MALE \n", - "...\n", - "\n", - "[344 rows x 7 columns]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df = bigframes.pandas.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", - "df" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Clean and prepare the data" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Dream36.618.4184.03475.0FEMALE
1Dream39.819.1184.04650.0MALE
2Dream40.918.9184.03900.0MALE
4Dream37.316.8192.03000.0FEMALE
5Dream43.218.5192.04100.0MALE
9Dream40.220.1200.03975.0MALE
10Dream40.818.9208.04300.0MALE
11Dream39.018.7185.03650.0MALE
12Dream37.016.9185.03000.0FEMALE
14Dream34.017.1185.03400.0FEMALE
15Dream37.016.5185.03400.0FEMALE
18Dream39.717.9193.04250.0MALE
19Dream37.818.1193.03750.0MALE
22Dream40.217.1193.03400.0FEMALE
23Dream36.818.5193.03500.0FEMALE
26Dream41.518.5201.04000.0MALE
31Dream33.116.1178.02900.0FEMALE
32Dream37.218.1178.03900.0MALE
33Dream39.516.7178.03250.0FEMALE
35Dream36.018.5186.03100.0FEMALE
\n", - "
[146 rows x 6 columns in total]" - ], - "text/plain": [ - " island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", - "0 Dream 36.6 18.4 184.0 3475.0 \n", - "1 Dream 39.8 19.1 184.0 4650.0 \n", - "2 Dream 40.9 18.9 184.0 3900.0 \n", - "4 Dream 37.3 16.8 192.0 3000.0 \n", - "5 Dream 43.2 18.5 192.0 4100.0 \n", - "9 Dream 40.2 20.1 200.0 3975.0 \n", - "10 Dream 40.8 18.9 208.0 4300.0 \n", - "11 Dream 39.0 18.7 185.0 3650.0 \n", - "12 Dream 37.0 16.9 185.0 3000.0 \n", - "14 Dream 34.0 17.1 185.0 3400.0 \n", - "15 Dream 37.0 16.5 185.0 3400.0 \n", - "18 Dream 39.7 17.9 193.0 4250.0 \n", - "19 Dream 37.8 18.1 193.0 3750.0 \n", - "22 Dream 40.2 17.1 193.0 3400.0 \n", - "23 Dream 36.8 18.5 193.0 3500.0 \n", - "26 Dream 41.5 18.5 201.0 4000.0 \n", - "31 Dream 33.1 16.1 178.0 2900.0 \n", - "32 Dream 37.2 18.1 178.0 3900.0 \n", - "33 Dream 39.5 16.7 178.0 3250.0 \n", - "35 Dream 36.0 18.5 186.0 3100.0 \n", - "36 Dream 39.6 18.1 186.0 4450.0 \n", - "38 Dream 41.3 20.3 194.0 3550.0 \n", - "41 Dream 35.7 18.0 202.0 3550.0 \n", - "51 Dream 38.1 17.6 187.0 3425.0 \n", - "53 Dream 36.0 17.1 187.0 3700.0 \n", - "\n", - " sex \n", - "0 FEMALE \n", - "1 MALE \n", - "2 MALE \n", - "4 FEMALE \n", - "5 MALE \n", - "9 MALE \n", - "10 MALE \n", - "11 MALE \n", - "12 FEMALE \n", - "14 FEMALE \n", - "15 FEMALE \n", - "18 MALE \n", - "19 MALE \n", - "22 FEMALE \n", - "23 FEMALE \n", - "26 MALE \n", - "31 FEMALE \n", - "32 MALE \n", - "33 FEMALE \n", - "35 FEMALE \n", - "36 MALE \n", - "38 MALE \n", - "41 FEMALE \n", - "51 FEMALE \n", - "53 FEMALE \n", - "...\n", - "\n", - "[146 rows x 6 columns]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# filter down to the data we want to analyze\n", - "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", - "\n", - "# drop the columns we don't care about\n", - "adelie_data = adelie_data.drop(columns=[\"species\"])\n", - "\n", - "# drop rows with nulls to get our training data\n", - "training_data = adelie_data.dropna()\n", - "\n", - "# take a peek at the training data\n", - "training_data" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "# pick feature columns and label column\n", - "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", - "label_columns = training_data[['body_mass_g']]\n", - "\n", - "# also get the rows that we want to make predictions for (i.e. where the feature column is null)\n", - "missing_body_mass = adelie_data[adelie_data.body_mass_g.isnull()]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Train and evaluate a linear regression model using the ML API" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", - "
[1 rows x 6 columns in total]" - ], - "text/plain": [ - " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - "0 223.878763 78553.601634 0.005614 \n", - "\n", - " median_absolute_error r2_score explained_variance \n", - "0 181.330911 0.623951 0.623951 \n", - "\n", - "[1 rows x 6 columns]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from bigframes.ml.linear_model import LinearRegression\n", - "\n", - "# as in scikit-learn, a newly created model is just a bundle of parameters\n", - "# default parameters are fine here\n", - "model = LinearRegression()\n", - "\n", - "# this will train a temporary model in BigQuery Machine Learning\n", - "model.fit(feature_columns, label_columns)\n", - "\n", - "# check how the model performed\n", - "model.score(feature_columns, label_columns)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Make predictions using the model" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
predicted_body_mass_g
2923603.735118
\n", - "
[1 rows x 1 columns in total]" - ], - "text/plain": [ - " predicted_body_mass_g\n", - "292 3603.735118\n", - "\n", - "[1 rows x 1 columns]" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "model.predict(missing_body_mass)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Save the trained model to BigQuery, so we can load it later" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "LinearRegression()" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "model.to_gbq(\"bqml_tutorial.penguins_model\", replace=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/01 - Getting Started.ipynb b/notebooks/01 - Getting Started.ipynb deleted file mode 100644 index 473bdd8ceae..00000000000 --- a/notebooks/01 - Getting Started.ipynb +++ /dev/null @@ -1,1190 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "d7a03de2-c0ef-4f80-9cd5-f96e87cf2d54", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# On the instance where you are running jupyter,\n", - "# authenticate with gcloud first:\n", - "#\n", - "# gcloud auth application-default login\n", - "\n", - "import bigframes.pandas as bpd\n", - "\n", - "# Change this location to the location of your datasets.\n", - "# We use \"us\" as that is the location of the sample data.\n", - "bpd.options.bigquery.location = \"us\"" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "941cb6c3-8c54-42ce-a945-4fa604176b2e", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5c480b60490940d3a45fa6b9ca2cecdb", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 411d90c8-8b22-40b0-ad42-04f9e38c074e is DONE. 0 Bytes processed.
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
taxi_idtrip_end_timestamptrip_secondstrip_milespickup_census_tractdropoff_census_tractpickup_community_areadropoff_community_areafaretips...extrastrip_totalpayment_typecompanypickup_latitudepickup_longitudepickup_locationdropoff_latitudedropoff_longitudedropoff_location
trip_start_timestampunique_key
2023-07-01 00:00:00+00:00fb004dfe98302ccf34e5e11bf1081568a94843a275cf3a53aae5e5858361a7ca64f75d3407dc0a44d7bc42...2023-07-01 00:00:00+00:005621.82<NA><NA>22229.52.53...0.012.03MobileCity Service41.922761-87.699155POINT (-87.6991553432 41.9227606205)41.922761-87.699155POINT (-87.6991553432 41.9227606205)
\n", - "

1 rows × 21 columns

\n", - "[1 rows x 21 columns in total]" - ], - "text/plain": [ - " taxi_id \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 75cf3a53aae5e5858361a7ca64f75d3407dc0a44d7bc42... \n", - "\n", - " trip_end_timestamp \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 2023-07-01 00:00:00+00:00 \n", - "\n", - " trip_seconds \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 562 \n", - "\n", - " trip_miles \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 1.82 \n", - "\n", - " pickup_census_tract \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 \n", - "\n", - " dropoff_census_tract \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 \n", - "\n", - " pickup_community_area \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 22 \n", - "\n", - " dropoff_community_area \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 22 \n", - "\n", - " fare \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 9.5 \n", - "\n", - " tips ... \\\n", - "trip_start_timestamp unique_key ... \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 2.53 ... \n", - "\n", - " extras \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 0.0 \n", - "\n", - " trip_total \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 12.03 \n", - "\n", - " payment_type \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 Mobile \n", - "\n", - " company \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 City Service \n", - "\n", - " pickup_latitude \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 41.922761 \n", - "\n", - " pickup_longitude \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 -87.699155 \n", - "\n", - " pickup_location \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 POINT (-87.6991553432 41.9227606205) \n", - "\n", - " dropoff_latitude \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 41.922761 \n", - "\n", - " dropoff_longitude \\\n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 -87.699155 \n", - "\n", - " dropoff_location \n", - "trip_start_timestamp unique_key \n", - "2023-07-01 00:00:00+00:00 fb004dfe98302ccf34e5e11bf1081568a94843a2 POINT (-87.6991553432 41.9227606205) \n", - "\n", - "[1 rows x 21 columns]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df.tail(n=1)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "f1eee1c6-7214-440e-bc17-6839d53a6718", - "metadata": {}, - "outputs": [], - "source": [ - "df = df[[\n", - " \"company\",\n", - " \"trip_miles\",\n", - " \"fare\",\n", - " \"tips\",\n", - "]]" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "66071984-e371-4161-8d7e-00d810b5cfab", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0ce3a9e3bd90477dac718b8e60fd3458", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job b105f8f6-fe3d-4f90-85c7-2cbede41413b is DONE. 0 Bytes processed.
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
companytrip_milesfaretips
trip_start_timestampunique_key
2013-01-01 00:00:00+00:00006d53094ff5b3c81140c00aa91fdb5467db0802<NA>2.37.850.0
00a723cf08cf29c56faa8ef77be519f6590b3405<NA>1.05.650.0
01396f0debefe45ccce23e48dd471edd67bd32e8Dispatch Taxi Affiliation0.08.850.0
01e9a03fd793670ed35ef7195eeb99775895611fDispatch Taxi Affiliation1.87.450.0
024f0f183c43ad2be33f232fd8dca0a86a3e4925<NA>4.914.050.0
04373ef74229c2122ff90d92642fcd94c88302e4Dispatch Taxi Affiliation0.06.650.0
0501a22fba176594d6f974360e20737af1b6cb66<NA>2.27.650.0
05faff951f2a2703fe3eb24efac026c2a1669a92<NA>3.39.650.0
0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbeBlue Ribbon Taxi Association Inc.0.06.250.0
06b4dc6dcd6295e69284e1c9d5a98015b0588ea6Choice Taxi Association0.013.850.0
07483a0baa6df0af68696b6f2483b7f429ff1827Northwest Management LLC0.64.650.0
07585d085f65cd512b94de09684253fe0ffb31a2Taxi Affiliation Services7.04.650.0
090ea9dc5dc26374ee06cdab1b809895beb1befcDispatch Taxi Affiliation0.010.050.0
091eda4ec41520d6ba96003f05eb10744f73aa82Northwest Management LLC0.015.250.0
097f2efed248cb5900792a89bba96a64918345ceBlue Ribbon Taxi Association Inc.0.012.250.0
09ffa3f40d36dbf8d51ca903738d9bba92557ed7Taxi Affiliation Services0.06.450.0
0a3446fb4f72d6485548883cd7f6cfe068d44829Taxi Affiliation Services0.014.250.0
0a7ea707a8700555e898489200a66b546a834170<NA>5.0413.852.0
0be545d9934dd343a2d929fca2e53d3d2851137bDispatch Taxi Affiliation1.15.650.0
0c2b3705c9748a5949bf5b7d727b2d1f2e18805f<NA>12.326.250.0
0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4Taxi Affiliation Services0.015.250.0
0dafcce34426b5377fd3925418a4c24b3e45c7d9<NA>1.26.250.0
0f5f7162b289dd9743b9d2e7fc75bf69696bb7bcTaxi Affiliation Services0.09.850.0
0f63007e437d589c9139acb026f03a832da3240fNorthwest Management LLC1.56.650.0
10508745a1069e9cdaad8760a6a243dc924cc0f2<NA>5.513.250.0
\n", - "

25 rows × 4 columns

\n", - "[208395018 rows x 4 columns in total]" - ], - "text/plain": [ - " company \\\n", - "trip_start_timestamp unique_key \n", - "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 \n", - " 00a723cf08cf29c56faa8ef77be519f6590b3405 \n", - " 01396f0debefe45ccce23e48dd471edd67bd32e8 Dispatch Taxi Affiliation \n", - " 01e9a03fd793670ed35ef7195eeb99775895611f Dispatch Taxi Affiliation \n", - " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 \n", - " 04373ef74229c2122ff90d92642fcd94c88302e4 Dispatch Taxi Affiliation \n", - " 0501a22fba176594d6f974360e20737af1b6cb66 \n", - " 05faff951f2a2703fe3eb24efac026c2a1669a92 \n", - " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe Blue Ribbon Taxi Association Inc. \n", - " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 Choice Taxi Association \n", - " 07483a0baa6df0af68696b6f2483b7f429ff1827 Northwest Management LLC \n", - " 07585d085f65cd512b94de09684253fe0ffb31a2 Taxi Affiliation Services \n", - " 090ea9dc5dc26374ee06cdab1b809895beb1befc Dispatch Taxi Affiliation \n", - " 091eda4ec41520d6ba96003f05eb10744f73aa82 Northwest Management LLC \n", - " 097f2efed248cb5900792a89bba96a64918345ce Blue Ribbon Taxi Association Inc. \n", - " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 Taxi Affiliation Services \n", - " 0a3446fb4f72d6485548883cd7f6cfe068d44829 Taxi Affiliation Services \n", - " 0a7ea707a8700555e898489200a66b546a834170 \n", - " 0be545d9934dd343a2d929fca2e53d3d2851137b Dispatch Taxi Affiliation \n", - " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f \n", - " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 Taxi Affiliation Services \n", - " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 \n", - " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc Taxi Affiliation Services \n", - " 0f63007e437d589c9139acb026f03a832da3240f Northwest Management LLC \n", - " 10508745a1069e9cdaad8760a6a243dc924cc0f2 \n", - "\n", - " trip_miles \\\n", - "trip_start_timestamp unique_key \n", - "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 2.3 \n", - " 00a723cf08cf29c56faa8ef77be519f6590b3405 1.0 \n", - " 01396f0debefe45ccce23e48dd471edd67bd32e8 0.0 \n", - " 01e9a03fd793670ed35ef7195eeb99775895611f 1.8 \n", - " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 4.9 \n", - " 04373ef74229c2122ff90d92642fcd94c88302e4 0.0 \n", - " 0501a22fba176594d6f974360e20737af1b6cb66 2.2 \n", - " 05faff951f2a2703fe3eb24efac026c2a1669a92 3.3 \n", - " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe 0.0 \n", - " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 0.0 \n", - " 07483a0baa6df0af68696b6f2483b7f429ff1827 0.6 \n", - " 07585d085f65cd512b94de09684253fe0ffb31a2 7.0 \n", - " 090ea9dc5dc26374ee06cdab1b809895beb1befc 0.0 \n", - " 091eda4ec41520d6ba96003f05eb10744f73aa82 0.0 \n", - " 097f2efed248cb5900792a89bba96a64918345ce 0.0 \n", - " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 0.0 \n", - " 0a3446fb4f72d6485548883cd7f6cfe068d44829 0.0 \n", - " 0a7ea707a8700555e898489200a66b546a834170 5.04 \n", - " 0be545d9934dd343a2d929fca2e53d3d2851137b 1.1 \n", - " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f 12.3 \n", - " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 0.0 \n", - " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 1.2 \n", - " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc 0.0 \n", - " 0f63007e437d589c9139acb026f03a832da3240f 1.5 \n", - " 10508745a1069e9cdaad8760a6a243dc924cc0f2 5.5 \n", - "\n", - " fare \\\n", - "trip_start_timestamp unique_key \n", - "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 7.85 \n", - " 00a723cf08cf29c56faa8ef77be519f6590b3405 5.65 \n", - " 01396f0debefe45ccce23e48dd471edd67bd32e8 8.85 \n", - " 01e9a03fd793670ed35ef7195eeb99775895611f 7.45 \n", - " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 14.05 \n", - " 04373ef74229c2122ff90d92642fcd94c88302e4 6.65 \n", - " 0501a22fba176594d6f974360e20737af1b6cb66 7.65 \n", - " 05faff951f2a2703fe3eb24efac026c2a1669a92 9.65 \n", - " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe 6.25 \n", - " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 13.85 \n", - " 07483a0baa6df0af68696b6f2483b7f429ff1827 4.65 \n", - " 07585d085f65cd512b94de09684253fe0ffb31a2 4.65 \n", - " 090ea9dc5dc26374ee06cdab1b809895beb1befc 10.05 \n", - " 091eda4ec41520d6ba96003f05eb10744f73aa82 15.25 \n", - " 097f2efed248cb5900792a89bba96a64918345ce 12.25 \n", - " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 6.45 \n", - " 0a3446fb4f72d6485548883cd7f6cfe068d44829 14.25 \n", - " 0a7ea707a8700555e898489200a66b546a834170 13.85 \n", - " 0be545d9934dd343a2d929fca2e53d3d2851137b 5.65 \n", - " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f 26.25 \n", - " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 15.25 \n", - " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 6.25 \n", - " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc 9.85 \n", - " 0f63007e437d589c9139acb026f03a832da3240f 6.65 \n", - " 10508745a1069e9cdaad8760a6a243dc924cc0f2 13.25 \n", - "\n", - " tips \n", - "trip_start_timestamp unique_key \n", - "2013-01-01 00:00:00+00:00 006d53094ff5b3c81140c00aa91fdb5467db0802 0.0 \n", - " 00a723cf08cf29c56faa8ef77be519f6590b3405 0.0 \n", - " 01396f0debefe45ccce23e48dd471edd67bd32e8 0.0 \n", - " 01e9a03fd793670ed35ef7195eeb99775895611f 0.0 \n", - " 024f0f183c43ad2be33f232fd8dca0a86a3e4925 0.0 \n", - " 04373ef74229c2122ff90d92642fcd94c88302e4 0.0 \n", - " 0501a22fba176594d6f974360e20737af1b6cb66 0.0 \n", - " 05faff951f2a2703fe3eb24efac026c2a1669a92 0.0 \n", - " 0651a5e1eb4792a53395c3b3b4fd9f7f8383cfbe 0.0 \n", - " 06b4dc6dcd6295e69284e1c9d5a98015b0588ea6 0.0 \n", - " 07483a0baa6df0af68696b6f2483b7f429ff1827 0.0 \n", - " 07585d085f65cd512b94de09684253fe0ffb31a2 0.0 \n", - " 090ea9dc5dc26374ee06cdab1b809895beb1befc 0.0 \n", - " 091eda4ec41520d6ba96003f05eb10744f73aa82 0.0 \n", - " 097f2efed248cb5900792a89bba96a64918345ce 0.0 \n", - " 09ffa3f40d36dbf8d51ca903738d9bba92557ed7 0.0 \n", - " 0a3446fb4f72d6485548883cd7f6cfe068d44829 0.0 \n", - " 0a7ea707a8700555e898489200a66b546a834170 2.0 \n", - " 0be545d9934dd343a2d929fca2e53d3d2851137b 0.0 \n", - " 0c2b3705c9748a5949bf5b7d727b2d1f2e18805f 0.0 \n", - " 0d37bbfeb207fbb2353e1911fdf878f3c24ed4c4 0.0 \n", - " 0dafcce34426b5377fd3925418a4c24b3e45c7d9 0.0 \n", - " 0f5f7162b289dd9743b9d2e7fc75bf69696bb7bc 0.0 \n", - " 0f63007e437d589c9139acb026f03a832da3240f 0.0 \n", - " 10508745a1069e9cdaad8760a6a243dc924cc0f2 0.0 \n", - "...\n", - "\n", - "[208395018 rows x 4 columns]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "e51b687e-0282-459c-8fd0-0ca22cfb153d", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "bc4d2a7cee5e4fc1a9513ec2fb54d293", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 494b3016-0582-45e0-9fcf-bb38eb00722e is RUNNING.
\n", + " \n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.9" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "The goal of this notebook is to demonstrate an enterprise generative AI use case. A marketing user can provide information about a new pharmaceutical drug and its generic name, and receive ideas on marketing-oriented brand names for that drug.\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/bigquery/docs/dataframes-quickstart)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn about Generative AI concepts such as prompting and few-shot learning, as well as how to use BigFrames ML for performing these tasks simply using an intuitive dataframe API.\n", + "\n", + "The steps performed include:\n", + "\n", + "1. Ask the user for the generic name and usage for the drug.\n", + "1. Use `bigframes` to query the FDA dataset of over 100,000 drugs, filtered on the brand name, generic name, and indications & usage columns.\n", + "1. Filter this dataset to find prototypical brand names that can be used as examples in prompt tuning.\n", + "1. Create a prompt with the user input, general instructions, examples and counter-examples for the desired brand name.\n", + "1. Use the `bigframes.ml.llm.PaLM2TextGenerator` to generate choices of brand names." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This notebook uses the [FDA dataset](https://cloud.google.com/blog/topics/healthcare-life-sciences/fda-mystudies-comes-to-google-cloud) available at [`bigquery-public-data.fda_drug`](https://console.cloud.google.com/bigquery?ws=!1m4!1m3!3m2!1sbigquery-public-data!2sfda_drug)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages required to execute this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "!pip install -U --quiet bigframes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "58707a750154" + }, + "source": [ + "### Colab only: Uncomment the following cell to restart the kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# # Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "from google.cloud import bigquery_connection_v1 as bq_connection\n", + "from bigframes.ml.llm import PaLM2TextGenerator\n", + "from IPython.display import Markdown" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**1. Vertex AI Workbench**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**3. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "evsJaAj5te0X" + }, + "source": [ + "#### BigFrames configuration\n", + "\n", + "Next, we will specify a [BigQuery connection](https://cloud.google.com/bigquery/docs/working-with-connections). If you already have a connection, you can simplify provide the name and skip the following creation steps.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G1vVsPiMsL2X" + }, + "outputs": [], + "source": [ + "# Please fill in these values.\n", + "LOCATION = \"us\" # @param {type:\"string\"}\n", + "CONNECTION = \"\" # @param {type:\"string\"}\n", + "\n", + "connection_name = f\"{PROJECT_ID}.{LOCATION}.{CONNECTION}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WGS_TzhWlPBN" + }, + "source": [ + "We will now try to use the provided connection, and if it doesn't exist, create a new one. We will also print the service account used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "56Hw42m6kFrj" + }, + "outputs": [], + "source": [ + "# Initialize client and set request parameters\n", + "client = bq_connection.ConnectionServiceClient()\n", + "new_conn_parent = f\"projects/{PROJECT_ID}/locations/{LOCATION}\"\n", + "exists_conn_parent = f\"projects/{PROJECT_ID}/locations/{LOCATION}/connections/{CONNECTION}\"\n", + "cloud_resource_properties = bq_connection.CloudResourceProperties({})\n", + "\n", + "# Try to connect using provided connection\n", + "try:\n", + " request = client.get_connection(\n", + " request=bq_connection.GetConnectionRequest(name=exists_conn_parent)\n", + " )\n", + " CONN_SERVICE_ACCOUNT = f\"serviceAccount:{request.cloud_resource.service_account_id}\"\n", + "# Create a new connection on error\n", + "except Exception:\n", + " connection = bq_connection.types.Connection(\n", + " {\"friendly_name\": CONNECTION, \"cloud_resource\": cloud_resource_properties}\n", + " )\n", + " request = bq_connection.CreateConnectionRequest(\n", + " {\n", + " \"parent\": new_conn_parent,\n", + " \"connection_id\": CONNECTION,\n", + " \"connection\": connection,\n", + " }\n", + " )\n", + " response = client.create_connection(request)\n", + " CONN_SERVICE_ACCOUNT = (\n", + " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", + " )\n", + "# Set service account permissions\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'\n", + "\n", + "print(CONN_SERVICE_ACCOUNT)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Initialize BigFrames client\n", + "\n", + "Here, we set the project configuration based on the provided parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OCccLirpkSRz" + }, + "outputs": [], + "source": [ + "bpd.options.bigquery.project = PROJECT_ID\n", + "bpd.options.bigquery.location = LOCATION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m8UCEtX9uLn6" + }, + "source": [ + "## Generate a name\n", + "\n", + "Let's start with entering a generic name and description of the drug." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oxphj2gnuKou" + }, + "outputs": [], + "source": [ + "GENERIC_NAME = \"Entropofloxacin\" # @param {type:\"string\"}\n", + "USAGE = \"Entropofloxacin is a fluoroquinolone antibiotic that is used to treat a variety of bacterial infections, including: pneumonia, streptococcus infections, salmonella infections, escherichia coli infections, and pseudomonas aeruginosa infections It is taken by mouth or by injection. The dosage and frequency of administration will vary depending on the type of infection being treated. It should be taken for the full course of treatment, even if symptoms improve after a few days. Stopping the medication early may increase the risk of the infection coming back.\" # @param {type:\"string\"}\n", + "NUM_NAMES = 10 # @param {type:\"integer\"}\n", + "TEMPERATURE = 0.5 # @param {type: \"number\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1q-vlbalzu1Q" + }, + "source": [ + "We can now create a prompt string, and populate it with the name and description." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0knz5ZWMzed-" + }, + "outputs": [], + "source": [ + "zero_shot_prompt = f\"\"\"Provide {NUM_NAMES} unique and modern brand names in Markdown bullet point format. Do not provide any additional explanation.\n", + "\n", + "Be creative with the brand names. Don't use English words directly; use variants or invented words.\n", + "\n", + "The generic name is: {GENERIC_NAME}\n", + "\n", + "The indications and usage are: {USAGE}.\"\"\"\n", + "\n", + "print(zero_shot_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LCRE2L720f5y" + }, + "source": [ + "Next, let's create a helper function to predict with our model. It will take a string input, and add it to a temporary BigFrames `DataFrame`. It will also return the string extracted from the response `DataFrame`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LB3xgDroIxlx" + }, + "outputs": [], + "source": [ + "def predict(prompt: str, temperature: float = TEMPERATURE) -> str:\n", + " # Create dataframe\n", + " input = bpd.DataFrame(\n", + " {\n", + " \"prompt\": [prompt],\n", + " }\n", + " )\n", + "\n", + " # Return response\n", + " return model.predict(input, temperature).ml_generate_text_llm_result.iloc[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b1ZapNZsJW2p" + }, + "source": [ + "We can now initialize the model, and get a response to our prompt!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UW2fQ2k5Hsic" + }, + "outputs": [], + "source": [ + "# Get BigFrames session\n", + "session = bpd.get_global_session()\n", + "\n", + "# Define the model\n", + "model = PaLM2TextGenerator(session=session, connection_name=connection_name)\n", + "\n", + "# Invoke LLM with prompt\n", + "response = predict(zero_shot_prompt)\n", + "\n", + "# Print results as Markdown\n", + "Markdown(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "o3yIhHV2jsUT" + }, + "source": [ + "We're off to a great start! Let's see if we can refine our response." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mBroUzWS8xOL" + }, + "source": [ + "## Few-shot learning\n", + "\n", + "Let's try using [few-shot learning](https://paperswithcode.com/task/few-shot-learning). We will provide a few examples of what we're looking for along with our prompt.\n", + "\n", + "Our prompt will consist of 3 parts:\n", + "* General instructions (e.g. generate $n$ brand names)\n", + "* Multiple examples\n", + "* Information about the drug we'd like to generate a name for\n", + "\n", + "Let's walk through how to construct this prompt.\n", + "\n", + "Our first step will be to define how many examples we want to provide in the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MXdI78SOElyt" + }, + "outputs": [], + "source": [ + "# Specify number of examples to include\n", + "\n", + "NUM_EXAMPLES = 3 # @param {type:\"integer\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U8w4puVM_892" + }, + "source": [ + "Next, let's define a prefix that will set the overall context." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aQ2iscnhF2cx" + }, + "outputs": [], + "source": [ + "prefix_prompt = f\"\"\"Provide {NUM_NAMES} unique and modern brand names in Markdown bullet point format, related to the drug at the bottom of this prompt.\n", + "\n", + "Be creative with the brand names. Don't use English words directly; use variants or invented words.\n", + "\n", + "First, we will provide {NUM_EXAMPLES} examples to help with your thought process.\n", + "\n", + "Then, we will provide the generic name and usage for the drug we'd like you to generate brand names for.\n", + "\"\"\"\n", + "\n", + "print(prefix_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VI0Spv-axN7d" + }, + "source": [ + "Our next step will be to include examples into the prompt.\n", + "\n", + "We will start out by retrieving the raw data for the examples, by querying the BigQuery public dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IoO_Bp8wA07N" + }, + "outputs": [], + "source": [ + "# Query 3 columns of interest from drug label dataset\n", + "df = bpd.read_gbq(\"bigquery-public-data.fda_drug.drug_label\",\n", + " col_order=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"])\n", + "\n", + "# Exclude any rows with missing data\n", + "df = df.dropna()\n", + "\n", + "# Drop duplicate rows\n", + "df = df.drop_duplicates()\n", + "\n", + "# Print values\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W5kOtbNGBTI2" + }, + "source": [ + "Let's now filter the results to remove atypical names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "95WDe2eCCeLx" + }, + "outputs": [], + "source": [ + "# Remove names with spaces\n", + "df = df[df[\"openfda_brand_name\"].str.find(\" \") == -1]\n", + "\n", + "# Remove names with 5 or fewer characters\n", + "df = df[df[\"openfda_brand_name\"].str.len() > 5]\n", + "\n", + "# Remove names where the generic and brand name match (case-insensitive)\n", + "df = df[df[\"openfda_generic_name\"].str.lower() != df[\"openfda_brand_name\"].str.lower()]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FZD89ep4EyYc" + }, + "source": [ + "Let's take `NUM_EXAMPLES` samples to include in the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2ohZYg7QEyJV" + }, + "outputs": [], + "source": [ + "# Take a sample and convert to a Pandas dataframe for local usage.\n", + "df_examples = df.sample(NUM_EXAMPLES, random_state=3).to_pandas()\n", + "\n", + "df_examples" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J-Qa1_SCImXy" + }, + "source": [ + "Let's now convert the data to a JSON structure, to enable embedding into a prompt. For consistency, we'll capitalize each example brand name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PcJdSaw0EGcW" + }, + "outputs": [], + "source": [ + "examples = [\n", + " {\n", + " \"brand_name\": brand_name.capitalize(),\n", + " \"generic_name\": generic_name,\n", + " \"usage\": usage,\n", + " }\n", + " for brand_name, generic_name, usage in zip(\n", + " df_examples[\"openfda_brand_name\"],\n", + " df_examples[\"openfda_generic_name\"],\n", + " df_examples[\"indications_and_usage\"],\n", + " )\n", + "]\n", + "\n", + "print(examples)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oU4mb1Dwgq64" + }, + "source": [ + "We'll create a prompt template for each example, and view the first one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kzAVsF6wJ93S" + }, + "outputs": [], + "source": [ + "example_prompt = \"\"\n", + "for example in examples:\n", + " example_prompt += f\"Generic name: {example['generic_name']}\\nUsage: {example['usage']}\\nBrand name: {example['brand_name']}\\n\\n\"\n", + "\n", + "example_prompt" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kbV2X1CXAyLV" + }, + "source": [ + "Finally, we can create a suffix to our prompt. This will contain the generic name of the drug, its usage, ending with a request for brand names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OYp6W_XfHTlo" + }, + "outputs": [], + "source": [ + "suffix_prompt = f\"\"\"Generic name: {GENERIC_NAME}\n", + "Usage: {USAGE}\n", + "Brand names:\"\"\"\n", + "\n", + "print(suffix_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RiaisW1nihJP" + }, + "source": [ + "Let's pull it altogether into a few shot prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "99xdU7l8C1h8" + }, + "outputs": [], + "source": [ + "# Define the prompt\n", + "few_shot_prompt = prefix_prompt + example_prompt + suffix_prompt\n", + "\n", + "# Print the prompt\n", + "print(few_shot_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nbUWdHtfitWn" + }, + "source": [ + "Now, let's pass our prompt to the LLM, and get a response!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d4ODRJdvLhlQ" + }, + "outputs": [], + "source": [ + "response = predict(few_shot_prompt)\n", + "\n", + "Markdown(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pFakjrTElOBs" + }, + "source": [ + "# Bulk generation\n", + "\n", + "Let's take these experiments to the next level by generating many names in bulk. We'll see how to leverage BigFrames at scale!\n", + "\n", + "We can start by finding drugs that are missing brand names. There are approximately 4,000 drugs that meet this criteria. We'll put a limit of 100 in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8eAutS41mx6U" + }, + "outputs": [], + "source": [ + "# Query 3 columns of interest from drug label dataset\n", + "df_missing = bpd.read_gbq(\"bigquery-public-data.fda_drug.drug_label\",\n", + " col_order=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"])\n", + "\n", + "# Exclude any rows with missing data\n", + "df_missing = df_missing.dropna()\n", + "\n", + "# Include rows in which openfda_brand_name equals openfda_generic_name\n", + "df_missing = df_missing[df_missing[\"openfda_generic_name\"] == df_missing[\"openfda_brand_name\"]]\n", + "\n", + "# Limit the number of rows for demonstration purposes\n", + "df_missing = df_missing.head(100)\n", + "\n", + "# Print values\n", + "df_missing.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fm6L8S7eVnCI" + }, + "source": [ + "We will create a column `prompt` with a customized prompt for each row." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "19TvGN1PVmVX" + }, + "outputs": [], + "source": [ + "df_missing[\"prompt\"] = (\n", + " \"Provide a unique and modern brand name related to this pharmaceutical drug.\"\n", + " + \"Don't use English words directly; use variants or invented words. The generic name is: \"\n", + " + df_missing[\"openfda_generic_name\"]\n", + " + \". The indications and usage are: \"\n", + " + df_missing[\"indications_and_usage\"]\n", + " + \".\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "njxwBvCKgMPE" + }, + "source": [ + "We'll create a new helper method, `batch_predict()` and query the LLM. The job may take a couple minutes to execute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tiSHa5B4aFhw" + }, + "outputs": [], + "source": [ + "def batch_predict(\n", + " input: bpd.DataFrame, temperature: float = TEMPERATURE\n", + ") -> bpd.DataFrame:\n", + " return model.predict(input, temperature).ml_generate_text_llm_result\n", + "\n", + "\n", + "response = batch_predict(df_missing[\"prompt\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K5a2nHdLgZEj" + }, + "source": [ + "Let's check the results for one of our responses!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TnizdeqBdbZj" + }, + "outputs": [], + "source": [ + "# Pick a sample\n", + "k = 0\n", + "\n", + "# Gather the prompt and response details\n", + "prompt_generic = df_missing[\"openfda_generic_name\"][k].iloc[0]\n", + "prompt_usage = df_missing[\"indications_and_usage\"][k].iloc[0]\n", + "response_str = response[k].iloc[0]\n", + "\n", + "# Print details\n", + "print(f\"Generic name: {prompt_generic}\")\n", + "print(f\"Brand name: {prompt_usage}\")\n", + "print(f\"Response: {response_str}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W4MviwyMI-Qh" + }, + "source": [ + "Congratulations! You have learned how to use generative AI to jumpstart the creative process.\n", + "\n", + "You've also seen how BigFrames can manage each step of the process, including gathering data, data manipulation, and querying the LLM." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Bys6--dVmq7R" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cIODjOLump_-" + }, + "outputs": [], + "source": [ + "# Delete the BigQuery Connection\n", + "from google.cloud import bigquery_connection_v1 as bq_connection\n", + "client = bq_connection.ConnectionServiceClient()\n", + "CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{LOCATION}/connections/{CONNECTION}\"\n", + "client.delete_connection(name=CONNECTION_ID)\n", + "print(f\"Deleted connection {CONNECTION_ID}.\")" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/06 - Using ML - Large Language Models.ipynb b/notebooks/generative_ai/large_language_models.ipynb similarity index 100% rename from notebooks/06 - Using ML - Large Language Models.ipynb rename to notebooks/generative_ai/large_language_models.ipynb diff --git a/notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb b/notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb new file mode 100644 index 00000000000..39e2ef535cc --- /dev/null +++ b/notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb @@ -0,0 +1,891 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2022 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "## Use BigQuery DataFrames with Generative AI for code generation\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Use this notebook to walk through an example use case of generating sample code by using BigQuery DataFrames and its integration with Generative AI support on Vertex AI.\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you create a CSV file containing sample code for calling a given set of APIs.\n", + "\n", + "The steps include:\n", + "\n", + "- Defining an LLM model in BigQuery DataFrames, specifically the [`text-bison` model of the PaLM API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text), using `bigframes.ml.llm`.\n", + "- Creating a DataFrame by reading in data from Cloud Storage.\n", + "- Manipulating data in the DataFrame to build LLM prompts.\n", + "- Sending DataFrame prompts to the LLM model using the `predict` method.\n", + "- Creating and using a custom function to transform the output provided by the LLM model response.\n", + "- Exporting the resulting transformed DataFrame as a CSV file." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses a dataset listing the names of various pandas DataFrame and Series APIs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery\n", + "* Generative AI support on Vertex AI\n", + "* Cloud Functions\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "[Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models), and [Cloud Functions pricing](https://cloud.google.com/functions/pricing), and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages, which are required to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "!pip install bigframes --upgrade --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,artifactregistry.googleapis.com,cloudbuild.googleapis.com,cloudresourcemanager.googleapis.com) to enable the following APIs:\n", + "\n", + " * BigQuery API\n", + " * BigQuery Connection API\n", + " * Cloud Functions API\n", + " * Cloud Run API\n", + " * Artifact Registry API\n", + " * Cloud Build API\n", + " * Cloud Resource Manager API\n", + " * Vertex AI API\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ], + "metadata": { + "id": "Wbr2aVtFQBcg" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "from google.cloud import bigquery\n", + "from google.cloud import bigquery_connection_v1 as bq_connection" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ], + "metadata": { + "id": "DTVtFlqeFbrU" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Define the LLM model\n", + "\n", + "BigQuery DataFrames provides integration with [`text-bison` model of the PaLM API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) via Vertex AI.\n", + "\n", + "This section walks through a few steps required in order to use the model in your notebook." + ], + "metadata": { + "id": "6eytf4xQHzcF" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Create a BigQuery Cloud resource connection\n", + "\n", + "You need to create a [Cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection) to enable BigQuery DataFrames to interact with Vertex AI services." + ], + "metadata": { + "id": "rS4VO1TGiO4G" + } + }, + { + "cell_type": "code", + "source": [ + "CONN_NAME = \"bqdf-llm\"\n", + "\n", + "client = bq_connection.ConnectionServiceClient()\n", + "new_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}\"\n", + "exists_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", + "cloud_resource_properties = bq_connection.CloudResourceProperties({})\n", + "\n", + "try:\n", + " request = client.get_connection(\n", + " request=bq_connection.GetConnectionRequest(name=exists_conn_parent)\n", + " )\n", + " CONN_SERVICE_ACCOUNT = f\"serviceAccount:{request.cloud_resource.service_account_id}\"\n", + "except Exception:\n", + " connection = bq_connection.types.Connection(\n", + " {\"friendly_name\": CONN_NAME, \"cloud_resource\": cloud_resource_properties}\n", + " )\n", + " request = bq_connection.CreateConnectionRequest(\n", + " {\n", + " \"parent\": new_conn_parent,\n", + " \"connection_id\": CONN_NAME,\n", + " \"connection\": connection,\n", + " }\n", + " )\n", + " response = client.create_connection(request)\n", + " CONN_SERVICE_ACCOUNT = (\n", + " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", + " )\n", + "print(CONN_SERVICE_ACCOUNT)" + ], + "metadata": { + "id": "KFPjDM4LVh96" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Set permissions for the service account\n", + "\n", + "The resource connection service account requires certain project-level permissions:\n", + " - `roles/aiplatform.user` and `roles/bigquery.connectionUser`: These roles are required for the connection to create a model definition using the LLM model in Vertex AI ([documentation](https://cloud.google.com/bigquery/docs/generate-text#give_the_service_account_access)).\n", + " - `roles/run.invoker`: This role is required for the connection to have read-only access to Cloud Run services that back custom/remote functions ([documentation](https://cloud.google.com/bigquery/docs/remote-functions#grant_permission_on_function)).\n", + "\n", + "Set these permissions by running the following `gcloud` commands:" + ], + "metadata": { + "id": "W6l6Ol2biU9h" + } + }, + { + "cell_type": "code", + "source": [ + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'" + ], + "metadata": { + "id": "d8wja24SVq6s" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Define the model\n", + "\n", + "Use `bigframes.ml.llm` to define the model:" + ], + "metadata": { + "id": "qUjT8nw-jIXp" + } + }, + { + "cell_type": "code", + "source": [ + "from bigframes.ml.llm import PaLM2TextGenerator\n", + "\n", + "session = bf.get_global_session()\n", + "connection = f\"{PROJECT_ID}.{REGION}.{CONN_NAME}\"\n", + "model = PaLM2TextGenerator(session=session, connection_name=connection)" + ], + "metadata": { + "id": "sdjeXFwcHfl7" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Read data from Cloud Storage into BigQuery DataFrames\n", + "\n", + "You can create a BigQuery DataFrames DataFrame by reading data from any of the following locations:\n", + "\n", + "* A local data file\n", + "* Data stored in a BigQuery table\n", + "* A data file stored in Cloud Storage\n", + "* An in-memory pandas DataFrame\n", + "\n", + "In this tutorial, you create BigQuery DataFrames DataFrames by reading two CSV files stored in Cloud Storage, one containing a list of DataFrame API names and one containing a list of Series API names." + ], + "metadata": { + "id": "GbW0oCnU1s1N" + } + }, + { + "cell_type": "code", + "source": [ + "df_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/df.csv\")\n", + "series_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/series.csv\")" + ], + "metadata": { + "id": "SchiTkQGIJog" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Take a peek at a few rows of data for each file:" + ], + "metadata": { + "id": "7OBjw2nmQY3-" + } + }, + { + "cell_type": "code", + "source": [ + "df_api.head(2)" + ], + "metadata": { + "id": "QCqgVCIsGGuv" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "series_api.head(2)" + ], + "metadata": { + "id": "BGJnZbgEGS5-" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Generate code using the LLM model\n", + "\n", + "Prepare the prompts and send them to the LLM model for prediction." + ], + "metadata": { + "id": "m3ZJEsi7SUKV" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Prompt design in BigQuery DataFrames\n", + "\n", + "Designing prompts for LLMs is a fast growing area and you can read more in [this documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/introduction-prompt-design).\n", + "\n", + "For this tutorial, you use a simple prompt to ask the LLM model for sample code for each of the API methods (or rows) from the last step's DataFrames. The output is the new DataFrames `df_prompt` and `series_prompt`, which contain the full prompt text." + ], + "metadata": { + "id": "9EMAqR37AfLS" + } + }, + { + "cell_type": "code", + "source": [ + "df_prompt_prefix = \"Generate Pandas sample code for DataFrame.\"\n", + "series_prompt_prefix = \"Generate Pandas sample code for Series.\"\n", + "\n", + "df_prompt = (df_prompt_prefix + df_api['API'])\n", + "series_prompt = (series_prompt_prefix + series_api['API'])\n", + "\n", + "df_prompt.head(2)" + ], + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Make predictions using the LLM model\n", + "\n", + "Use the BigQuery DataFrames DataFrame containing the full prompt text as the input to the `predict` method. The `predict` method calls the LLM model and returns its generated text output back to two new BigQuery DataFrames DataFrames, `df_pred` and `series_pred`.\n", + "\n", + "Note: The predictions might take a few minutes to run." + ], + "metadata": { + "id": "rwPLjqW2Ajzh" + } + }, + { + "cell_type": "code", + "source": [ + "df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024)\n", + "series_pred = model.predict(series_prompt.to_frame(), max_output_tokens=1024)" + ], + "metadata": { + "id": "6i6HkFJZa8na" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Once the predictions are processed, take a look at the sample output from the LLM, which provides code samples for the API names listed in the DataFrames dataset." + ], + "metadata": { + "id": "89cB8MW4UIdV" + } + }, + { + "cell_type": "code", + "source": [ + "print(df_pred['ml_generate_text_llm_result'].iloc[0])" + ], + "metadata": { + "id": "9A2gw6hP_2nX" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Manipulate LLM output using a remote function\n", + "\n", + "The output that the LLM provides often contains additional text beyond the code sample itself. Using BigQuery DataFrames, you can deploy custom Python functions that process and transform this output.\n", + "\n" + ], + "metadata": { + "id": "Fx4lsNqMorJ-" + } + }, + { + "cell_type": "markdown", + "source": [ + "Running the cell below creates a custom function that you can use to process the LLM output data in two ways:\n", + "1. Strip the LLM text output to include only the code block.\n", + "2. Substitute `import pandas as pd` with `import bigframes.pandas as bf` so that the resulting code block works with BigQuery DataFrames." + ], + "metadata": { + "id": "d8L7SN03VByG" + } + }, + { + "cell_type": "code", + "source": [ + "@bf.remote_function([str], str, bigquery_connection=CONN_NAME)\n", + "def extract_code(text: str):\n", + " try:\n", + " res = text[text.find('\\n')+1:text.find('```', 3)]\n", + " res = res.replace(\"import pandas as pd\", \"import bigframes.pandas as bf\")\n", + " if \"import bigframes.pandas as bf\" not in res:\n", + " res = \"import bigframes.pandas as bf\\n\" + res\n", + " return res\n", + " except:\n", + " return \"\"" + ], + "metadata": { + "id": "GskyyUQPowBT" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "The custom function is deployed as a Cloud Function, and then integrated with BigQuery as a [remote function](https://cloud.google.com/bigquery/docs/remote-functions). Save both of the function names so that you can clean them up at the end of this notebook." + ], + "metadata": { + "id": "hVQAoqBUOJQf" + } + }, + { + "cell_type": "code", + "source": [ + "CLOUD_FUNCTION_NAME = format(extract_code.bigframes_cloud_function)\n", + "print(\"Cloud Function Name \" + CLOUD_FUNCTION_NAME)\n", + "REMOTE_FUNCTION_NAME = format(extract_code.bigframes_remote_function)\n", + "print(\"Remote Function Name \" + REMOTE_FUNCTION_NAME)" + ], + "metadata": { + "id": "PBlp-C-DOHRO" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Apply the custom function to each LLM output DataFrame to get the processed results:" + ], + "metadata": { + "id": "4FEucaiqVs3H" + } + }, + { + "cell_type": "code", + "source": [ + "df_code = df_pred.assign(code=df_pred['ml_generate_text_llm_result'].apply(extract_code))\n", + "series_code = series_pred.assign(code=series_pred['ml_generate_text_llm_result'].apply(extract_code))" + ], + "metadata": { + "id": "bsQ9cmoWo0Ps" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "You can see the differences by inspecting the first row of data:" + ], + "metadata": { + "id": "ujQVVuhfWA3y" + } + }, + { + "cell_type": "code", + "source": [ + "print(df_code['code'].iloc[0])" + ], + "metadata": { + "id": "7yWzjhGy_zcy" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Save the results to Cloud Storage\n", + "\n", + "BigQuery DataFrames lets you save a BigQuery DataFrames DataFrame as a CSV file in Cloud Storage for further use. Try that now with your processed LLM output data." + ], + "metadata": { + "id": "GTRdUw-Ro5R1" + } + }, + { + "cell_type": "markdown", + "source": [ + "Create a new Cloud Storage bucket with a unique name:" + ], + "metadata": { + "id": "9DQ7eiQxPTi3" + } + }, + { + "cell_type": "code", + "source": [ + "import uuid\n", + "BUCKET_ID = \"code-samples-\" + str(uuid.uuid1())\n", + "\n", + "!gsutil mb gs://{BUCKET_ID}" + ], + "metadata": { + "id": "-J5LHgS6LLZ0" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Use `to_csv` to write each BigQuery DataFrames DataFrame as a CSV file in the Cloud Storage bucket:" + ], + "metadata": { + "id": "tyxZXj0UPYUv" + } + }, + { + "cell_type": "code", + "source": [ + "df_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/df_code*.csv\")\n", + "series_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/series_code*.csv\")" + ], + "metadata": { + "id": "Zs_b5L-4IvER" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "You can navigate to the Cloud Storage bucket browser to download the two files and view them.\n", + "\n", + "Run the following cell, and then follow the link to your Cloud Storage bucket browser:" + ], + "metadata": { + "id": "UDBtDlrTuuh8" + } + }, + { + "cell_type": "code", + "source": [ + "print(f'https://console.developers.google.com/storage/browser/{BUCKET_ID}/')" + ], + "metadata": { + "id": "PspCXu-qu_ND" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Summary and next steps\n", + "\n", + "You've used BigQuery DataFrames' integration with LLM models (`bigframes.ml.llm`) to generate code samples, and have tranformed LLM output by creating and using a custom function in BigQuery DataFrames.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ], + "metadata": { + "id": "RGSvUk48RK20" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "source": [ + "# # Delete the BigQuery Connection\n", + "# from google.cloud import bigquery_connection_v1 as bq_connection\n", + "# client = bq_connection.ConnectionServiceClient()\n", + "# CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", + "# client.delete_connection(name=CONNECTION_ID)\n", + "# print(f\"Deleted connection '{CONNECTION_ID}'.\")" + ], + "metadata": { + "id": "yw7A461XLjvW" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "# # Delete the Cloud Function\n", + "# ! gcloud functions delete {CLOUD_FUNCTION_NAME} --quiet\n", + "# # Delete the Remote Function\n", + "# REMOTE_FUNCTION_NAME = REMOTE_FUNCTION_NAME.replace(PROJECT_ID + \".\", \"\")\n", + "# ! bq rm --routine --force=true {REMOTE_FUNCTION_NAME}" + ] + }, + { + "cell_type": "code", + "source": [ + "# # Delete the Google Cloud Storage bucket and files\n", + "# ! gsutil rm -r gs://{BUCKET_ID}\n", + "# print(f\"Deleted bucket '{BUCKET_ID}'.\")" + ], + "metadata": { + "id": "iQFo6OUBLmi3" + }, + "execution_count": null, + "outputs": [] + } + ], + "metadata": { + "colab": { + "toc_visible": true, + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb b/notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb new file mode 100644 index 00000000000..9a74beaad86 --- /dev/null +++ b/notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb @@ -0,0 +1,743 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "## Train a linear regression model with BigQuery DataFrames ML\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Use this notebook to learn how to train a linear regression model by using BigQuery DataFrames ML. BigQuery DataFrames ML provides a provides a scikit-learn-like API for ML powered by the BigQuery engine.\n", + "\n", + "This example is adapted from the [BQML linear regression tutorial](https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial).\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you use BigQuery DataFrames to create a linear regression model that predicts the weight of an Adelie penguin based on the penguin's island of residence, culmen length and depth, flipper length, and sex.\n", + "\n", + "The steps include:\n", + "\n", + "- Creating a DataFrame from a BigQuery table.\n", + "- Cleaning and preparing data using pandas.\n", + "- Creating a linear regression model using `bigframes.ml`.\n", + "- Saving the ML model to BigQuery for future use." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) (a BigQuery Public Dataset) which includes data on a set of penguins including species, island of residence, weight, culmen length and depth, flipper length, and sex." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models)\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages, which are required to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9O0Ka4W2MNF3" + }, + "outputs": [], + "source": [ + "!pip install bigframes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "58707a750154" + }, + "source": [ + "### Colab only\n", + "\n", + "Uncomment and run the following cell to restart the kernel:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oDfTjfACBvJk" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D21CoOlfFTYI" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9EMAqR37AfLS" + }, + "source": [ + "## Read a BigQuery table into a BigQuery DataFrames DataFrame\n", + "\n", + "Read the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) into a BigQuery DataFrames DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "outputs": [], + "source": [ + "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DJu837YEXD7B" + }, + "source": [ + "Take a look at the DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_gPD0Zn1Stdb" + }, + "outputs": [], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rwPLjqW2Ajzh" + }, + "source": [ + "## Clean and prepare data\n", + "\n", + "You can use pandas as you normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of your local environment.\n", + "\n", + "Because this model will focus on the Adelie Penguin species, you need to filter the data for only those rows representing Adelie penguins. Then you drop the `species` column because it is no longer needed.\n", + "\n", + "As these functions are applied, only the new DataFrame object `adelie_data` is modified. The source table and the original DataFrame object `df` don't change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6i6HkFJZa8na" + }, + "outputs": [], + "source": [ + "# Filter down to the data to the Adelie Penguin species\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# Drop the species column\n", + "adelie_data = adelie_data.drop(columns=[\"species\"])\n", + "\n", + "# Take a look at the filtered DataFrame\n", + "adelie_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jhK2OlyMbY4L" + }, + "source": [ + "Drop rows with `NULL` values in order to create a BigQuery DataFrames DataFrame for the training data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0am3hdlXZfxZ" + }, + "outputs": [], + "source": [ + "# Drop rows with nulls to get training data\n", + "training_data = adelie_data.dropna()\n", + "\n", + "# Take a peek at the training data\n", + "training_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M_-0X7NxYK5f" + }, + "source": [ + "Specify your feature (or input) columns and the label (or output) column:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YKwCW7Nsavap" + }, + "outputs": [], + "source": [ + "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "label_columns = training_data[['body_mass_g']]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CjyM7vZJZ0sQ" + }, + "source": [ + "There is a row within the `adelie_data` BigQuery DataFrames DataFrame that has a `NULL` value for the `body mass` column. `body mass` is the label column, which is the value that the model you are creating is trying to predict.\n", + "\n", + "Create a new BigQuery DataFrames DataFrame, `test_data`, for this row so that you can use it as test data on which to make a prediction later:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wej78IDUaRW9" + }, + "outputs": [], + "source": [ + "test_data = adelie_data[adelie_data.body_mass_g.isnull()]\n", + "\n", + "test_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fx4lsNqMorJ-" + }, + "source": [ + "## Create the linear regression model\n", + "\n", + "BigQuery DataFrames ML lets you move from exploring data to creating machine learning models through its scikit-learn-like API, `bigframes.ml`. BigQuery DataFrames ML supports several types of [ML models](https://cloud.google.com/python/docs/reference/bigframes/latest#ml-capabilities).\n", + "\n", + "In this notebook, you create a linear regression model, a type of regression model that generates a continuous value from a linear combination of input features.\n", + "\n", + "When you create a model with BigQuery DataFrames ML, it is saved locally and limited to the BigQuery session. However, as you'll see in the next section, you can use `to_gbq` to save the model permanently to your BigQuery project." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EloGtMnverFF" + }, + "source": [ + "### Create the model using `bigframes.ml`\n", + "\n", + "When you pass the feature columns without transforms, BigQuery ML uses\n", + "[automatic preprocessing](https://cloud.google.com/bigquery/docs/auto-preprocessing) to encode string values and scale numeric values.\n", + "\n", + "BigQuery ML also [automatically splits the data for training and evaluation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-glm#data_split_method), although for datasets with less than 500 rows (such as this one), all rows are used for training." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GskyyUQPowBT" + }, + "outputs": [], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "model = LinearRegression()\n", + "\n", + "model.fit(feature_columns, label_columns)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UGjeMPC2caKK" + }, + "source": [ + "### Score the model\n", + "\n", + "Check how the model performed by using the `score` method. More information on model scoring can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#mlevaluate_output)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kGBJKafpo0dl" + }, + "outputs": [], + "source": [ + "model.score(feature_columns, label_columns)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P2lUiZZ_cjri" + }, + "source": [ + "### Predict using the model\n", + "\n", + "Use the model to predict the body mass of the data row you saved earlier to the `test_data` DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bsQ9cmoWo0Ps" + }, + "outputs": [], + "source": [ + "model.predict(test_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GTRdUw-Ro5R1" + }, + "source": [ + "## Save the model in BigQuery\n", + "\n", + "The model is saved locally within this session. You can save the model permanently to BigQuery for use in future sessions, and to make the model sharable with others." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K0mPaoGpcwwy" + }, + "source": [ + "Create a BigQuery dataset to house the model, adding a name for your dataset as the `DATASET_ID` variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZSP7gt13QrQt" + }, + "outputs": [], + "source": [ + "DATASET_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "from google.cloud import bigquery\n", + "client = bigquery.Client(project=PROJECT_ID)\n", + "dataset = bigquery.Dataset(PROJECT_ID + \".\" + DATASET_ID)\n", + "dataset.location = REGION\n", + "dataset = client.create_dataset(dataset, exists_ok=True)\n", + "print(f\"Dataset {dataset.dataset_id} created.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zqAIWWgJczp-" + }, + "source": [ + "Save the model using the `to_gbq` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QE_GD4Byo_jb" + }, + "outputs": [], + "source": [ + "model.to_gbq(DATASET_ID + \".penguin_weight\" , replace=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f7uHacAy49rT" + }, + "source": [ + "You can view the saved model in the BigQuery console under the dataset you created in the first step. Run the following cell and follow the link to view your BigQuery console:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qDBoiA_0488Z" + }, + "outputs": [], + "source": [ + "print(f'https://console.developers.google.com/bigquery?p={PROJECT_ID}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G_wjSfXpWTuy" + }, + "source": [ + "# Summary and next steps\n", + "\n", + "You've created a linear regression model using `bigframes.ml`.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "# # Delete the BigQuery dataset and associated ML model\n", + "# from google.cloud import bigquery\n", + "# client = bigquery.Client(project=PROJECT_ID)\n", + "# client.delete_dataset(\n", + "# DATASET_ID, delete_contents=True, not_found_ok=True\n", + "# )\n", + "# print(\"Deleted dataset '{}'.\".format(DATASET_ID))" + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/getting_started/getting_started_bq_dataframes.ipynb b/notebooks/getting_started/getting_started_bq_dataframes.ipynb new file mode 100644 index 00000000000..7815182e54d --- /dev/null +++ b/notebooks/getting_started/getting_started_bq_dataframes.ipynb @@ -0,0 +1,971 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Get started with BigQuery DataFrames\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Use this notebook to get started with BigQuery DataFrames, including setup, installation, and basic tutorials.\n", + "\n", + "BigQuery DataFrames provides a Pythonic DataFrame and machine learning (ML) API powered by the BigQuery engine.\n", + "\n", + "* `bigframes.pandas` provides a pandas-like API for analytics.\n", + "* `bigframes.ml` provides a scikit-learn-like API for ML.\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn how to install BigQuery DataFrames, load data into a BigQuery DataFrames DataFrame, and inspect and manipulate the data using pandas and a custom Python function, running at BigQuery scale.\n", + "\n", + "The steps include:\n", + "\n", + "- Creating a BigQuery DataFrames DataFrame: Access data from a local CSV to create a BigQuery DataFrames DataFrame.\n", + "- Inspecting and manipulating data: Use pandas to perform data cleaning and preparation on the DataFrame.\n", + "- Deploying a custom function: Deploy a [remote function ](https://cloud.google.com/bigquery/docs/remote-functions)that runs a scalar Python function at BigQuery scale." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) (a BigQuery public dataset), which contains data on a set of penguins including species, island of residence, weight, culmen length and depth, flipper length, and sex.\n", + "\n", + "The same dataset is also stored in a public Cloud Storage bucket as a CSV file so that you can use it to try ingesting data from a local environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (storage and compute)\n", + "* Cloud Functions\n", + "\n", + "Learn about [BigQuery storage pricing](https://cloud.google.com/bigquery/pricing#storage),\n", + "[BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "and [Cloud Functions pricing](https://cloud.google.com/functions/pricing),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages, which are required to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mfPoOwPLGpSr" + }, + "outputs": [], + "source": [ + "!pip install bigframes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "58707a750154" + }, + "source": [ + "### Colab only\n", + "\n", + "Uncomment and run the following cell to restart the kernel:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yq7zKYWelRQP" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,artifactregistry.googleapis.com,cloudbuild.googleapis.com,cloudresourcemanager.googleapis.com) to enable the following APIs:\n", + "\n", + " * BigQuery API\n", + " * BigQuery Connection API\n", + " * Cloud Functions API\n", + " * Cloud Run API\n", + " * Artifact Registry API\n", + " * Cloud Build API\n", + " * Cloud Resource Manager API\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "\n", + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pDfrKwMKE_dK" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-19Uiwoo9pP4" + }, + "source": [ + "## See the power of BigQuery DataFrames first-hand\n", + "\n", + "BigQuery DataFrames enables you to interact with datasets of any size, so that you can explore, transform, and understand even your biggest datasets using familiar tools like pandas and scikit-learn." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KMX4D2uoBwM0" + }, + "source": [ + "For example, take the BigQuery sample table `bigquery-samples.wikipedia_pageviews.200809h`, which is ~60 GB is size. This is not a dataset you'd likely be able process in pandas without extra infrastructure.\n", + "\n", + "With BigQuery DataFrames, however, computation is handled by BigQuery's highly scalable compute engine, meaning you can focus on doing data science without hitting size limitations." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i98c46p1CXoV" + }, + "source": [ + "If you'd like to try creating a BigQuery DataFrames DataFrame from this table, uncomment and run the next cell to load the table using the `read_gbq` method.\n", + "\n", + "> Note: Keep in mind that running these operations will count against your monthly [free tier allowance in BigQuery](https://cloud.google.com/bigquery/pricing#free-tier)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vyex9BQI-BNa" + }, + "outputs": [], + "source": [ + "# bq_df_sample = bf.read_gbq(\"bigquery-samples.wikipedia_pageviews.200809h\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gE6CEALjDZZV" + }, + "source": [ + "No problem! BigQuery DataFrames makes a DataFrame, `bq_df_sample`, containing the entirety of the source table of data." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T6lAIeelDwLz" + }, + "source": [ + "Uncomment and run the following cell to see pandas in action over your new BigQuery DataFrames DataFrame.\n", + "\n", + "This code uses regex to filter the DataFrame to include only rows with Wikipedia page titles containing the word \"Google\", sums the total views by page title, and then returns the top 100 results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XfGq5apK-D_e" + }, + "outputs": [], + "source": [ + "# bq_df_sample[bq_df_sample.title.str.contains(r\"[Gg]oogle\")]\\\n", + "# .groupby(['title'], as_index=False)['views'].sum(numeric_only=True)\\\n", + "# .sort_values('views', ascending=False)\\\n", + "# .head(100)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i6XV-HTN-IFF" + }, + "source": [ + "In addition to giving you access to pandas, BigQuery DataFrames also enables you to build ML models, run inference, and deploy and run your own Python functions at scale. You'll see examples throughout this and other notebooks in this GitHub repo.\n", + "\n", + "Now you'll move to the smaller `penguins` dataset for the remainder of this getting started guide." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9EMAqR37AfLS" + }, + "source": [ + "## Create a BigQuery DataFrames DataFrame\n", + "\n", + "You can create a BigQuery DataFrames DataFrame by reading data from any of the following locations:\n", + "\n", + "* A local data file\n", + "* Data stored in a BigQuery table\n", + "* A data file stored in Cloud Storage\n", + "* An in-memory pandas DataFrame\n", + "\n", + "The following sections show how to use the first two options." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iZDjzglh9eWZ" + }, + "source": [ + "### Create a DataFrame from a local file\n", + "\n", + "Use the instructions in the following sections to create a BigQuery DataFrames DataFrame from a local file.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8Jry3NoFv3Wm" + }, + "source": [ + "#### Get the CSV file\n", + "\n", + "First, copy and paste the following link into a new browser window to download the CSV file of the penguin data to your local machine:\n", + "\n", + "> http://storage.googleapis.com/cloud-samples-data/vertex-ai/bigframe/penguins.csv\n", + "\n", + "Next, upload the local CSV file to your notebook environment, using the relevant instructions for your environment:\n", + "\n", + "**Vertex AI Workbench or a local JupyterLab instance**\n", + "\n", + "1. Follow these [directions](https://jupyterlab.readthedocs.io/en/latest/user/files.html#uploading-and-downloading) to upload the file from your machine to your notebook environment by using the UI.\n", + "2. Uncomment the next cell, set the variable `fn` to match the path to your file, and then run the cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SvyXzkRl783u" + }, + "outputs": [], + "source": [ + "# fn = 'penguins.csv'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yqcuF1JNvFse" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3QHQYlnoBLpt" + }, + "outputs": [], + "source": [ + "# from google.colab import files\n", + "# uploaded = files.upload()\n", + "# for fn in uploaded.keys():\n", + "# print('User uploaded file \"{name}\" with length {length} bytes'.format(\n", + "# name=fn, length=len(uploaded[fn])))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sJsrwAQY_H6g" + }, + "source": [ + "#### Create a DataFrame\n", + "\n", + "Create a BigQuery DataFrames DataFrame from the uploaded CSV file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "outputs": [], + "source": [ + "df_from_local = bf.read_csv(fn)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U-RVfNCu_h_h" + }, + "source": [ + "Take a look at the first few rows of the DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_gPD0Zn1Stdb" + }, + "outputs": [], + "source": [ + "df_from_local.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rK0lNJmz_xkA" + }, + "source": [ + "### Ingest data from a DataFrame to a BigQuery table\n", + "\n", + "BigQuery DataFrames lets you create a BigQuery table from a BigQuery DataFrames DataFrame on-the-fly." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V1DWpmSCAEql" + }, + "source": [ + "First, create a BigQuery dataset to house the table. Choose a name for your dataset, or keep the suggestion of `birds`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZSP7gt13QrQt" + }, + "outputs": [], + "source": [ + "DATASET_ID = \"birds\" # @param {type:\"string\"}\n", + "\n", + "from google.cloud import bigquery\n", + "client = bigquery.Client(project=PROJECT_ID)\n", + "dataset = bigquery.Dataset(PROJECT_ID + \".\" + DATASET_ID)\n", + "dataset.location = REGION\n", + "dataset = client.create_dataset(dataset, exists_ok=True)\n", + "print(f\"Dataset {dataset.dataset_id} created.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Jd0dFISwAPPa" + }, + "source": [ + "Next, use the `to_gbq` method to create a BigQuery table from the DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oP1NIAmUBjop" + }, + "outputs": [], + "source": [ + "df_from_local.to_gbq(PROJECT_ID + \".\" + DATASET_ID + \".penguins\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kfF6fnmmAZEK" + }, + "source": [ + "### Create a DataFrame from BigQuery data\n", + "You can create a BigQuery DataFrames DataFrame from a BigQuery table by using the `read_gbq` method and referencing either an entire table or a SQL query." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TEy5jHJDD6hx" + }, + "source": [ + "Create a BigQuery DataFrames DataFrame from the BigQuery table you created in the previous section, and view a few rows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IBuo-d6dWfsA" + }, + "outputs": [], + "source": [ + "query_or_table = f\"\"\"{PROJECT_ID}.{DATASET_ID}.penguins\"\"\"\n", + "bq_df = bf.read_gbq(query_or_table)\n", + "bq_df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rwPLjqW2Ajzh" + }, + "source": [ + "## Inspect and manipulate data in BigQuery DataFrames" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bExmYlL_ELtV" + }, + "source": [ + "### Using pandas\n", + "\n", + "You can use pandas as you normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of your local environment. There are 150+ pandas functions supported in BigQuery DataFrames. You can view the list in [the documentation](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZHFUc3Q_FHc1" + }, + "source": [ + "To see this in action, inspect one of the columns (or series) of the BigQuery DataFrames DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6i6HkFJZa8na" + }, + "outputs": [], + "source": [ + "bq_df[\"body_mass_g\"].head(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EJIZJaNXFQzh" + }, + "source": [ + "Compute the mean of this series:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YKwCW7Nsavap" + }, + "outputs": [], + "source": [ + "average_body_mass = bq_df[\"body_mass_g\"].mean()\n", + "print(f\"average_body_mass: {average_body_mass}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DSs1cnca-MOU" + }, + "source": [ + "Calculate the mean `body_mass_g` by `species` using the `groupby` operation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4PyKMR61-Mjy" + }, + "outputs": [], + "source": [ + "bq_df[\"species\", \"body_mass_g\"].groupby(by=bq_df[\"species\"]).mean(numeric_only=True).head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6sf9kZ2C9Ixe" + }, + "source": [ + "You can confirm that the calculations were run in BigQuery by clicking \"Open job\" from the previous cells' output. This takes you to the BigQuery console to view the SQL statement and job details." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cWVNZ8D_FUtT" + }, + "source": [ + "### Using custom functions\n", + "\n", + "Running your own Python functions (or being able to bring your packages) and using them at scale is a challenge many data scientists face. BigQuery DataFrames makes it easy to deploy [remote functions](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.pandas#bigframes_pandas_remote_function) that run scalar Python functions at BigQuery scale. These functions are persisted as [BigQuery remote functions](https://cloud.google.com/bigquery/docs/remote-functions) that you can then re-use." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zjw8toUbHuRD" + }, + "source": [ + "Running the cell below creates a custom function using the `remote_function` method. This function categorizes a value into one of two buckets: >= 4000 or <4000.\n", + "\n", + "> Note: Creating a function requires a [BigQuery connection](https://cloud.google.com/bigquery/docs/remote-functions#create_a_remote_function). This code assumes a pre-created connection named `bigframes-rf-conn`. If\n", + "the connection is not already created, BigQuery DataFrames attempts to create one assuming the [necessary APIs\n", + "and IAM permissions](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.pandas#bigframes_pandas_remote_function) are set up in the project.\n", + "\n", + "This cell takes a few minutes to run because it creates the BigQuery connection (if applicable) and deploys the Cloud Function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rSWTOG-vb2Fc" + }, + "outputs": [], + "source": [ + "@bf.remote_function([float], str, bigquery_connection='bigframes-rf-conn')\n", + "def get_bucket(num):\n", + " if not num: return \"NA\"\n", + " boundary = 4000\n", + " return \"at_or_above_4000\" if num >= boundary else \"below_4000\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N7JH0BI5IOpK" + }, + "source": [ + "The custom function is deployed as a Cloud Function, and is then integrated with BigQuery as a remote function.\n", + "\n", + "Save both of the function names so that you can clean them up at the end of this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6ejPXoyEQpWE" + }, + "outputs": [], + "source": [ + "CLOUD_FUNCTION_NAME = format(get_bucket.bigframes_cloud_function)\n", + "print(\"Cloud Function Name \" + CLOUD_FUNCTION_NAME)\n", + "REMOTE_FUNCTION_NAME = format(get_bucket.bigframes_remote_function)\n", + "print(\"Remote Function Name \" + REMOTE_FUNCTION_NAME)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vHV3JqKjJHsH" + }, + "source": [ + "Apply the custom function to the BigQuery DataFrames DataFrame to bucketize the `body_mass_g` value of the penguins:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NxSd9WZFcIji" + }, + "outputs": [], + "source": [ + "bq_df = bq_df.assign(body_mass_bucket=bq_df['body_mass_g'].apply(get_bucket))\n", + "bq_df[['body_mass_g', 'body_mass_bucket']].head(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wCsmt0IwFkDy" + }, + "source": [ + "## Summary and next steps\n", + "\n", + "You've created BigQuery DataFrames DataFrames, and inspected and manipulated data with pandas and custom remote functions at BigQuery scale and speed.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks), including an introductory notebook for `bigframes.ml`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "### Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "# # Delete the BigQuery dataset\n", + "# from google.cloud import bigquery\n", + "# client = bigquery.Client(project=PROJECT_ID)\n", + "# client.delete_dataset(\n", + "# DATASET_ID, delete_contents=True, not_found_ok=True\n", + "# )\n", + "# print(\"Deleted dataset '{}'.\".format(DATASET_ID))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_dTCXvCxtPw9" + }, + "outputs": [], + "source": [ + "# # Delete the BigQuery Connection\n", + "# from google.cloud import bigquery_connection_v1 as bq_connection\n", + "# client = bq_connection.ConnectionServiceClient()\n", + "# CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/bigframes-rf-conn\"\n", + "# client.delete_connection(name=CONNECTION_ID)\n", + "# print(\"Deleted connection '{}'.\".format(CONNECTION_ID))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EDAIIfcpwNOF" + }, + "outputs": [], + "source": [ + "# # Delete the Cloud Function\n", + "# ! gcloud functions delete {CLOUD_FUNCTION_NAME} --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QwumLUKmVpuH" + }, + "outputs": [], + "source": [ + "# # Delete the Remote Function\n", + "# REMOTE_FUNCTION_NAME = REMOTE_FUNCTION_NAME.replace(PROJECT_ID + \".\", \"\")\n", + "# ! bq rm --routine --force=true {REMOTE_FUNCTION_NAME}" + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/03 - Using ML - ML fundamentals.ipynb b/notebooks/getting_started/ml_fundamentals.ipynb similarity index 100% rename from notebooks/03 - Using ML - ML fundamentals.ipynb rename to notebooks/getting_started/ml_fundamentals.ipynb diff --git a/notebooks/10 - Regionalized.ipynb b/notebooks/location/regionalized.ipynb similarity index 100% rename from notebooks/10 - Regionalized.ipynb rename to notebooks/location/regionalized.ipynb diff --git a/notebooks/05 - Using ML - Easy linear regression.ipynb b/notebooks/regression/easy_linear_regression.ipynb similarity index 100% rename from notebooks/05 - Using ML - Easy linear regression.ipynb rename to notebooks/regression/easy_linear_regression.ipynb diff --git a/notebooks/04 - Using ML - SKLearn linear regression.ipynb b/notebooks/regression/sklearn_linear_regression.ipynb similarity index 100% rename from notebooks/04 - Using ML - SKLearn linear regression.ipynb rename to notebooks/regression/sklearn_linear_regression.ipynb diff --git a/notebooks/50 - Remote Function.ipynb b/notebooks/remote_functions/remote_function.ipynb similarity index 100% rename from notebooks/50 - Remote Function.ipynb rename to notebooks/remote_functions/remote_function.ipynb diff --git a/noxfile.py b/noxfile.py index 7d4cb1c61b1..ec5a1b16510 100644 --- a/noxfile.py +++ b/noxfile.py @@ -19,6 +19,7 @@ from multiprocessing import Process import os import pathlib +from pathlib import Path import re import shutil from typing import Dict, List @@ -30,6 +31,10 @@ BLACK_VERSION = "black==22.3.0" ISORT_VERSION = "isort==5.12.0" SPHINX_VERSION = "sphinx==4.5.0" + +# pytest-retry 1.4.0 fails on Python 3.11. +# https://github.com/str0zzapreti/pytest-retry/issues/17 +PYTEST_RETRY_VERSION = "pytest-retry<=1.3.0" LINT_PATHS = ["docs", "bigframes", "tests", "noxfile.py", "setup.py"] DEFAULT_PYTHON_VERSION = "3.10" @@ -55,7 +60,7 @@ "openpyxl", "pytest", "pytest-cov", - "pytest-retry", + PYTEST_RETRY_VERSION, "pytest-timeout", "pytest-xdist", "google-cloud-testutils", @@ -590,23 +595,37 @@ def system_prerelease(session: nox.sessions.Session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def notebook(session): session.install("-e", ".[all]") - session.install("pytest", "pytest-xdist", "pytest-retry", "nbmake") - - notebooks = [ - "00 - Summary.ipynb", - "01 - Getting Started.ipynb", - "02 - DataFrame.ipynb", - "03 - Using ML - ML fundamentals.ipynb", - "04 - Using ML - SKLearn linear regression.ipynb", - "05 - Using ML - Easy linear regression.ipynb", - "06 - Using ML - Large Language Models.ipynb", - "50 - Remote Function.ipynb", + session.install("pytest", "pytest-xdist", PYTEST_RETRY_VERSION, "nbmake") + + notebooks_list = list(Path("notebooks/").glob("*/*.ipynb")) + + denylist = [ + # Regionalized testing is manually added later. + "notebooks/location/regionalized.ipynb", + # These notebooks contain special colab `param {type:"string"}` + # comments, which make it easy for customers to fill in their + # own information. + # TODO(ashleyxu): Test these notebooks by replacing parameters with + # appropriate values and omitting cleanup logic that may break + # our test infrastructure. + "notebooks/getting_started/getting_started_bq_dataframes.ipynb", + "notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb", + "notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb", + "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", + # The experimental notebooks imagine features that don't yet + # exist or only exist as temporary prototypes. + "notebooks/experimental/longer_ml_demo.ipynb", ] - notebooks = [os.path.join("notebooks", nb) for nb in notebooks] + + # Convert each Path notebook object to a string using a list comprehension. + notebooks = [str(nb) for nb in notebooks_list] + + # Remove tests that we choose not to test. + notebooks = list(filter(lambda nb: nb not in denylist, notebooks)) # Regionalized notebooks notebooks_reg = { - "10 - Regionalized.ipynb": [ + "regionalized.ipynb": [ "asia-southeast1", "eu", "europe-west4", @@ -616,7 +635,8 @@ def notebook(session): ] } notebooks_reg = { - os.path.join("notebooks", nb): regions for nb, regions in notebooks_reg.items() + os.path.join("notebooks/location", nb): regions + for nb, regions in notebooks_reg.items() } # For some reason nbmake exits silently with "no tests ran" message if diff --git a/pytest.ini b/pytest.ini index 693439f47ca..204c743bbfa 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] +doctest_optionflags = NORMALIZE_WHITESPACE filterwarnings = ignore::pandas.errors.SettingWithCopyWarning diff --git a/samples/snippets/remote_function.py b/samples/snippets/remote_function.py new file mode 100644 index 00000000000..37972672c32 --- /dev/null +++ b/samples/snippets/remote_function.py @@ -0,0 +1,147 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def run_remote_function_and_read_gbq_function(project_id: str): + your_gcp_project_id = project_id + + # [START bigquery_dataframes_remote_function] + import bigframes.pandas as bpd + + # Set BigQuery DataFrames options + bpd.options.bigquery.project = your_gcp_project_id + bpd.options.bigquery.location = "us" + + # BigQuery DataFrames gives you the ability to turn your custom scalar + # functions into a BigQuery remote function. It requires the GCP project to + # be set up appropriately and the user having sufficient privileges to use + # them. One can find more details about the usage and the requirements via + # `help` command. + help(bpd.remote_function) + + # Read a table and inspect the column of interest. + df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + df["body_mass_g"].head(10) + + # Define a custom function, and specify the intent to turn it into a remote + # function. It requires a BigQuery connection. If the connection is not + # already created, BigQuery DataFrames will attempt to create one assuming + # the necessary APIs and IAM permissions are setup in the project. In our + # examples we would be using a pre-created connection named + # `bigframes-rf-conn`. Let's try a `pandas`-like use case in which we want + # to apply a user defined scalar function to every value in a `Series`, more + # specifically bucketize the `body_mass_g` value of the penguins, which is a + # real number, into a category, which is a string. + @bpd.remote_function([float], str, bigquery_connection="bigframes-rf-conn") + def get_bucket(num): + if not num: + return "NA" + boundary = 4000 + return "at_or_above_4000" if num >= boundary else "below_4000" + + # Then we can apply the remote function on the `Series`` of interest via + # `apply` API and store the result in a new column in the DataFrame. + df = df.assign(body_mass_bucket=df["body_mass_g"].apply(get_bucket)) + + # This will add a new column `body_mass_bucket` in the DataFrame. You can + # preview the original value and the bucketized value side by side. + df[["body_mass_g", "body_mass_bucket"]].head(10) + + # The above operation was possible by doing all the computation on the + # cloud. For that, there is a google cloud function deployed by serializing + # the user code, and a BigQuery remote function created to call the cloud + # function via the latter's http endpoint on the data in the DataFrame. + + # The BigQuery remote function created to support the BigQuery DataFrames + # remote function can be located via a property `bigframes_remote_function` + # set in the remote function object. + print(f"Created BQ remote function: {get_bucket.bigframes_remote_function}") + + # The cloud function can be located via another property + # `bigframes_cloud_function` set in the remote function object. + print(f"Created cloud function: {get_bucket.bigframes_cloud_function}") + + # Warning: The deployed cloud function may be visible to other users with + # sufficient privilege in the project, so the user should be careful about + # having any sensitive data in the code that will be deployed as a remote + # function. + + # Let's continue trying other potential use cases of remote functions. Let's + # say we consider the `species`, `island` and `sex` of the penguins + # sensitive information and want to redact that by replacing with their hash + # code instead. Let's define another scalar custom function and decorated it + # as a remote function + @bpd.remote_function([str], str, bigquery_connection="bigframes-rf-conn") + def get_hash(input): + import hashlib + + # handle missing value + if input is None: + input = "" + encoded_input = input.encode() + hash = hashlib.md5(encoded_input) + return hash.hexdigest() + + # We can use this remote function in another `pandas`-like API `map` that + # can be applied on a DataFrame + df_redacted = df[["species", "island", "sex"]].map(get_hash) + df_redacted.head(10) + + # [END bigquery_dataframes_remote_function] + + existing_get_bucket_bq_udf = get_bucket.bigframes_remote_function + + # [START bigquery_dataframes_read_gbq_function] + + # If you have already defined a custom function in BigQuery, either via the + # BigQuery Google Cloud Console or with the `remote_function` decorator, + # or otherwise, you may use it with BigQuery DataFrames with the + # `read_gbq_function` method. More details are available via the `help` + # command. + import bigframes.pandas as pd + + help(pd.read_gbq_function) + + # Here is an example of using `read_gbq_function` to load an existing + # BigQuery function. + df = pd.read_gbq("bigquery-public-data.ml_datasets.penguins") + get_bucket_function = pd.read_gbq_function(existing_get_bucket_bq_udf) + + df = df.assign(body_mass_bucket=df["body_mass_g"].apply(get_bucket_function)) + df.head(10) + + # It should be noted that if a function is created using the + # `remote_function` decorator, its created BQ remote function is accessible + # immediately afterward via the function's `bigframes_remote_function` + # attribute. The same string can be passed to `read_gbq_function` later in + # another context. + + # [END bigquery_dataframes_read_gbq_function] + + # Clean up cloud artifacts + session = bpd.get_global_session() + for function in (get_bucket, get_hash): + try: + session.bqclient.delete_routine(function.bigframes_remote_function) + except Exception: + # Ignore exception during clean-up + pass + + try: + session.cloudfunctionsclient.delete_function( + name=function.bigframes_cloud_function + ) + except Exception: + # Ignore exception during clean-up + pass diff --git a/samples/snippets/remote_function_test.py b/samples/snippets/remote_function_test.py new file mode 100644 index 00000000000..8b51e46b45a --- /dev/null +++ b/samples/snippets/remote_function_test.py @@ -0,0 +1,32 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.pandas + +from . import remote_function + + +def test_remote_function_and_read_gbq_function( + capsys: pytest.CaptureFixture[str], +) -> None: + # We need a fresh session since we're modifying connection options. + bigframes.pandas.reset_session() + + # TODO(swast): Get project from environment so contributors can run tests. + remote_function.run_remote_function_and_read_gbq_function("bigframes-dev") + out, _ = capsys.readouterr() + assert "Created BQ remote function:" in out + assert "Created cloud function:" in out diff --git a/tests/data/hockey_players.json b/tests/data/hockey_players.json new file mode 100644 index 00000000000..8a9b252992d --- /dev/null +++ b/tests/data/hockey_players.json @@ -0,0 +1,37 @@ +[ + { + "mode": "NULLABLE", + "name": "team_name", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "position", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "player_name", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "goals", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "assists", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "number", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "season", + "type": "INTEGER" + } +] diff --git a/tests/data/hockey_players.jsonl b/tests/data/hockey_players.jsonl new file mode 100644 index 00000000000..d2b26cffdd0 --- /dev/null +++ b/tests/data/hockey_players.jsonl @@ -0,0 +1,10 @@ +{"team_name":"Canucks", "position":"C", "player_name":"Elias Petterson", "goals":39, "assists":63, "number":40, "season":2023} +{"team_name":"Canucks", "position":"LW", "player_name":"Ilya Mikheyev", "goals":13, "assists":15, "number":65, "season":2023} +{"team_name":"Canucks", "position":"RW", "player_name":"Andrei Kuzmenko", "goals":39, "assists":35, "number":40, "season":2023} +{"team_name":"Kraken", "position":"C", "player_name":"Jared McCann", "goals":40, "assists":30, "number":19, "season":2023} +{"team_name":"Kraken", "position":"LW", "player_name":"Yanni Gourde", "goals":14, "assists":34, "number":37, "season":2023} +{"team_name":"Kraken", "position":"RW", "player_name":"Jordan Eberle", "goals":20, "assists":43, "number":7, "season":2023} +{"team_name":"Canucks", "position":"C", "player_name":"Elias Petterson", "goals":32, "assists":36, "number":40, "season":2022} +{"team_name":"Kraken", "position":"C", "player_name":"Jared McCann", "goals":27, "assists":23, "number":19, "season":2022} +{"team_name":"Kraken", "position":"LW", "player_name":"Yanni Gourde", "goals":21, "assists":27, "number":37, "season":2022} +{"team_name":"Kraken", "position":"RW", "player_name":"Jordan Eberle", "goals":21, "assists":23, "number":7, "season":2022} diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 41c8eaffd78..bf5cf12c74b 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -211,6 +211,7 @@ def load_test_data_tables( ("scalars_too", "scalars_schema.json", "scalars.jsonl"), ("penguins", "penguins_schema.json", "penguins.jsonl"), ("time_series", "time_series_schema.json", "time_series.jsonl"), + ("hockey_players", "hockey_players.json", "hockey_players.jsonl"), ]: test_data_hash = hashlib.md5() _hash_digest_file(test_data_hash, DATA_DIR / schema_filename) @@ -255,6 +256,11 @@ def scalars_table_id(test_data_tables) -> str: return test_data_tables["scalars"] +@pytest.fixture(scope="session") +def hockey_table_id(test_data_tables) -> str: + return test_data_tables["hockey_players"] + + @pytest.fixture(scope="session") def scalars_table_id_2(test_data_tables) -> str: return test_data_tables["scalars_too"] @@ -354,6 +360,34 @@ def scalars_dfs( return scalars_df_index, scalars_pandas_df_index +@pytest.fixture(scope="session") +def hockey_df( + hockey_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq(hockey_table_id) + + +@pytest.fixture(scope="session") +def hockey_pandas_df() -> pd.DataFrame: + """pd.DataFrame pointing at test data.""" + df = pd.read_json( + DATA_DIR / "hockey_players.jsonl", + lines=True, + dtype={ + "team_name": pd.StringDtype(storage="pyarrow"), + "position": pd.StringDtype(storage="pyarrow"), + "player_name": pd.StringDtype(storage="pyarrow"), + "goals": pd.Int64Dtype(), + "assists": pd.Int64Dtype(), + "number": pd.Int64Dtype(), + "season": pd.Int64Dtype(), + }, + ) + df.index = df.index.astype("Int64") + return df + + @pytest.fixture(scope="session") def penguins_df_default_index( penguins_table_id: str, session: bigframes.Session @@ -721,3 +755,48 @@ def restore_sampling_settings(): yield bigframes.options.sampling.enable_downsampling = enable_downsampling bigframes.options.sampling.max_download_size = max_download_size + + +@pytest.fixture() +def weird_strings_pd(): + df = pd.DataFrame( + { + "string_col": [ + "٠١٢٣٤٥٦٧٨٩", + "", + "0", + "字", + "五", + "0123456789", + pd.NA, + "abc 123 mixed letters and numbers", + "no numbers here", + "123a", + "23!", + " 45", + "a45", + "Dž", + "tT", + "-123", + "-123.4", + "-0", + "-.0", + ".0", + ".1", + "⅙", + "²", + "\t", + "a\ta", + "p1\np2", + " ", + ] + }, + dtype=pd.StringDtype(storage="pyarrow"), + ) + df.index = df.index.astype("Int64") + return df.string_col + + +@pytest.fixture() +def weird_strings(session, weird_strings_pd): + return session.read_pandas(weird_strings_pd.to_frame()).string_col diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 0c2744819d4..b65baa63ebd 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -21,7 +21,7 @@ import bigframes.ml.preprocessing -def test_columntransformer_standalone_fit_transform( +def test_columntransformer_standalone_fit_and_transform( penguins_df_default_index, new_penguins_df ): transformer = bigframes.ml.compose.ColumnTransformer( @@ -73,3 +73,54 @@ def test_columntransformer_standalone_fit_transform( ) pandas.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_columntransformer_standalone_fit_transform(new_penguins_df): + transformer = bigframes.ml.compose.ColumnTransformer( + [ + ( + "onehot", + bigframes.ml.preprocessing.OneHotEncoder(), + "species", + ), + ( + "scale", + bigframes.ml.preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ) + + result = transformer.fit_transform( + new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] + ).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pandas.DataFrame( + { + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + "scaled_culmen_length_mm": [ + 1.313249, + -0.20198, + -1.111118, + ], + "scaled_flipper_length_mm": [1.251098, -1.196588, -0.054338], + }, + index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), + ) + expected.scaled_culmen_length_mm = expected.scaled_culmen_length_mm.astype( + "Float64" + ) + expected.scaled_flipper_length_mm = expected.scaled_flipper_length_mm.astype( + "Float64" + ) + + pandas.testing.assert_frame_equal(result, expected, rtol=1e-3) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 74356c81e1b..181678ebcb2 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -15,6 +15,7 @@ from unittest import TestCase import numpy as np +import pytest def test_create_text_generator_model(palm2_text_generator_model): @@ -22,6 +23,8 @@ def test_create_text_generator_model(palm2_text_generator_model): assert palm2_text_generator_model is not None +# Marked as flaky only because BQML LLM is in preview, the service only has limited capacity, not stable enough. +@pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_default_params_success( palm2_text_generator_model, llm_text_df ): @@ -32,6 +35,7 @@ def test_text_generator_predict_default_params_success( assert all(series.str.len() > 20) +@pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_series_default_params_success( palm2_text_generator_model, llm_text_df ): @@ -42,6 +46,7 @@ def test_text_generator_predict_series_default_params_success( assert all(series.str.len() > 20) +@pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_arbitrary_col_label_success( palm2_text_generator_model, llm_text_df ): @@ -53,6 +58,7 @@ def test_text_generator_predict_arbitrary_col_label_success( assert all(series.str.len() > 20) +@pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_with_params_success( palm2_text_generator_model, llm_text_df ): @@ -70,6 +76,7 @@ def test_create_embedding_generator_model(palm2_embedding_generator_model): assert palm2_embedding_generator_model is not None +@pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_success( palm2_embedding_generator_model, llm_text_df ): @@ -82,6 +89,7 @@ def test_embedding_generator_predict_success( assert value.size == 768 +@pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_series_success( palm2_embedding_generator_model, llm_text_df ): diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 420a80754fb..57b9900c489 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -24,13 +24,13 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): scaler = bigframes.ml.preprocessing.StandardScaler() scaler.fit( penguins_df_default_index[ - "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm" + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] ] ) result = scaler.transform( penguins_df_default_index[ - "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm" + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] ] ).to_pandas() @@ -58,6 +58,35 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_standard_scaler_normalizeds_fit_transform(new_penguins_df): + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. + scaler = bigframes.ml.preprocessing.StandardScaler() + result = scaler.fit_transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + # If standard-scaled correctly, mean should be 0.0 + for column in result.columns: + assert math.isclose(result[column].mean(), 0.0, abs_tol=1e-3) + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "scaled_culmen_depth_mm": [1.17072, -1.272416, 0.101848], + "scaled_culmen_length_mm": [1.313249, -0.20198, -1.111118], + "scaled_flipper_length_mm": [1.251089, -1.196588, -0.054338], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + def test_standard_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. scaler = bigframes.ml.preprocessing.StandardScaler() @@ -93,7 +122,7 @@ def test_standard_scaler_series_normalizes(penguins_df_default_index, new_pengui def test_one_hot_encoder_default_params(new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder() - encoder.fit(new_penguins_df["species", "sex"]) + encoder.fit(new_penguins_df[["species", "sex"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -121,6 +150,35 @@ def test_one_hot_encoder_default_params(new_penguins_df): pd.testing.assert_frame_equal(result, expected) +def test_one_hot_encoder_default_params_fit_transform(new_penguins_df): + encoder = bigframes.ml.preprocessing.OneHotEncoder() + + result = encoder.fit_transform(new_penguins_df[["species", "sex"]]).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "onehotencoded_sex": [ + [{"index": 2, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + ], + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + def test_one_hot_encoder_series_default_params(new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder() encoder.fit(new_penguins_df["species"]) @@ -148,7 +206,7 @@ def test_one_hot_encoder_series_default_params(new_penguins_df): def test_one_hot_encoder_params(new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder("most_frequent", 100, 2) - encoder.fit(new_penguins_df["species", "sex"]) + encoder.fit(new_penguins_df[["species", "sex"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -178,7 +236,7 @@ def test_one_hot_encoder_params(new_penguins_df): def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder() - encoder.fit(penguins_df_default_index["species", "sex"]) + encoder.fit(penguins_df_default_index[["species", "sex"]]) result = encoder.transform(new_penguins_df).to_pandas() diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 31b64f4314d..241cbd576b1 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -254,31 +254,93 @@ def test_upper(scalars_dfs): ) -def test_isnumeric(session): - pandas_df = pd.DataFrame( - { - "numeric_string_col": [ - "٠١٢٣٤٥٦٧٨٩", - "", - "0", - "字", - "五", - "0123456789", - pd.NA, - "abc 123 mixed letters and numbers", - "no numbers here", - "123a", - "23!", - " 45", - "a45", - ] - } - ) - - df = session.read_pandas(pandas_df) - - pd_result = pandas_df.numeric_string_col.str.isnumeric() - bf_result = df.numeric_string_col.str.isnumeric().to_pandas() +def test_isnumeric(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.isnumeric() + bf_result = weird_strings.str.isnumeric().to_pandas() + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_isalpha(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.isalpha() + bf_result = weird_strings.str.isalpha().to_pandas() + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_isdigit(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.isdigit() + bf_result = weird_strings.str.isdigit().to_pandas() + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_isdecimal(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.isdecimal() + bf_result = weird_strings.str.isdecimal().to_pandas() + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_isalnum(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.isalnum() + bf_result = weird_strings.str.isalnum().to_pandas() + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_isspace(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.isspace() + bf_result = weird_strings.str.isspace().to_pandas() + + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_islower(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.islower() + bf_result = weird_strings.str.islower().to_pandas() + + assert_series_equal_ignoring_order( + bf_result, + pd_result.astype(pd.BooleanDtype()) + # the dtype here is a case of intentional diversion from pandas + # see go/bigframes-dtypes + ) + + +def test_isupper(weird_strings, weird_strings_pd): + pd_result = weird_strings_pd.str.isupper() + bf_result = weird_strings.str.isupper().to_pandas() assert_series_equal_ignoring_order( bf_result, @@ -394,9 +456,6 @@ def test_str_get(scalars_dfs): bf_result = bf_series.str.get(8).to_pandas() pd_result = scalars_pandas_df[col_name].str.get(8) - print(pd_result) - print(bf_result) - assert_series_equal_ignoring_order( pd_result, bf_result, @@ -416,6 +475,16 @@ def test_str_pad(scalars_dfs): ) +def test_str_zfill(weird_strings, weird_strings_pd): + bf_result = weird_strings.str.zfill(5).to_pandas() + pd_result = weird_strings_pd.str.zfill(5) + + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + def test_str_ljust(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 2c44dd80670..5b4f9ebcccf 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -840,6 +840,14 @@ def test_set_index(scalars_dfs, index_column, drop, append): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_set_index_key_error(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + with pytest.raises(KeyError): + scalars_pandas_df.set_index(["not_a_col"]) + with pytest.raises(KeyError): + scalars_df.set_index(["not_a_col"]) + + @pytest.mark.parametrize( ("ascending",), ((True,), (False,)), @@ -1321,6 +1329,41 @@ def test_df_describe(scalars_dfs): ).all() +@pytest.mark.parametrize( + ("values", "index", "columns"), + [ + ("int64_col", "int64_too", ["string_col"]), + (["int64_col"], "int64_too", ["string_col"]), + (["int64_col", "float64_col"], "int64_too", ["string_col"]), + ], +) +def test_df_pivot(scalars_dfs, values, index, columns): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.pivot( + values=values, index=index, columns=columns + ).to_pandas() + pd_result = scalars_pandas_df.pivot(values=values, index=index, columns=columns) + + # Pandas produces NaN, where bq dataframes produces pd.NA + pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("values", "index", "columns"), + [ + (["goals", "assists"], ["team_name", "season"], ["position"]), + (["goals", "assists"], ["season"], ["team_name", "position"]), + ], +) +def test_df_pivot_hockey(hockey_df, hockey_pandas_df, values, index, columns): + bf_result = hockey_df.pivot(values=values, index=index, columns=columns).to_pandas() + pd_result = hockey_pandas_df.pivot(values=values, index=index, columns=columns) + + # Pandas produces NaN, where bq dataframes produces pd.NA + pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + def test_ipython_key_completions_with_drop(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_names = "string_col" @@ -1621,6 +1664,7 @@ def test_sample_raises_value_error(scalars_dfs): @pytest.mark.parametrize( ("axis",), [ + (None,), (0,), (1,), ], diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index d5dd4e357bc..5a2562bfb20 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -109,8 +109,6 @@ def test_dataframe_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): ) bf_result_computed = bf_result.to_pandas() - # Pandas produces multi-index which isn't supported in bq df yet - pd_result = pd_result.set_axis(bf_result.columns, axis=1) pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) @@ -128,8 +126,6 @@ def test_dataframe_groupby_agg_dict(scalars_df_index, scalars_pandas_df_index): ) bf_result_computed = bf_result.to_pandas() - # Pandas produces multi-index which isn't supported in bq df yet - pd_result = pd_result.set_axis(bf_result.columns, axis=1) pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) diff --git a/tests/system/small/test_ipython.py b/tests/system/small/test_ipython.py new file mode 100644 index 00000000000..6725805d9a7 --- /dev/null +++ b/tests/system/small/test_ipython.py @@ -0,0 +1,28 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +IPython = pytest.importorskip("IPython") + + +def test_repr_cache(scalars_df_index): + display_formatter = IPython.core.formatters.DisplayFormatter() + # Make sure the df has a new block that the method return value + # is not already cached. + test_df = scalars_df_index.head() + results = display_formatter.format(test_df) + assert results[0].keys() == {"text/plain", "text/html"} + assert test_df._block.retrieve_repr_request_results.cache_info().misses == 1 + assert test_df._block.retrieve_repr_request_results.cache_info().hits == 1 diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 914be6dae4a..b2937d7da9c 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -19,6 +19,7 @@ from tests.system.utils import assert_pandas_df_equal_ignore_ordering +# Row Multi-index tests def test_set_multi_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.set_index(["bool_col", "int64_too"]).to_pandas() pd_result = scalars_pandas_df_index.set_index(["bool_col", "int64_too"]) @@ -443,3 +444,179 @@ def test_multi_index_series_rename_dict_same_type( pandas.testing.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) + + +# Column Multi-index tests + + +def test_column_multi_index_getitem(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "string_col", "bool_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], columns)) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_a = bf_df["a"].to_pandas() + pd_a = pd_df["a"] + pandas.testing.assert_frame_equal(bf_a, pd_a) + + bf_b = bf_df["b"].to_pandas() + pd_b = pd_df["b"] + pandas.testing.assert_frame_equal(bf_b, pd_b) + + bf_fullkey = bf_df[("a", "int64_too")].to_pandas() + pd_fullkey = pd_df[("a", "int64_too")] + pandas.testing.assert_series_equal(bf_fullkey, pd_fullkey) + + +def test_column_multi_index_concat(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "string_col", "bool_col", "int64_col"] + multi_columns1 = pandas.MultiIndex.from_tuples( + zip(["a", "b", "a", "b"], [1, 1, 2, 2]) + ) + multi_columns2 = pandas.MultiIndex.from_tuples( + zip(["a", "b", "a", "c"], [3, 1, 2, 1]) + ) + + bf_df1 = scalars_df_index[columns].copy() + bf_df1.columns = multi_columns1 + bf_df2 = scalars_df_index[columns].copy() + bf_df2.columns = multi_columns2 + + pd_df1 = scalars_pandas_df_index[columns].copy() + pd_df1.columns = multi_columns1 + pd_df2 = scalars_pandas_df_index[columns].copy() + pd_df2.columns = multi_columns2 + + bf_result = bpd.concat([bf_df1, bf_df2, bf_df1]).to_pandas() + pd_result = pandas.concat([pd_df1, pd_df2, pd_df1]) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_column_multi_index_drop(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "string_col", "bool_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], columns)) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_a = bf_df.drop(("a", "int64_too"), axis=1).to_pandas() + pd_a = pd_df.drop(("a", "int64_too"), axis=1) + pandas.testing.assert_frame_equal(bf_a, pd_a) + + +@pytest.mark.parametrize( + ("key",), + [ + ("a",), + ("b",), + ("c",), + ], +) +def test_column_multi_index_assign(scalars_df_index, scalars_pandas_df_index, key): + columns = ["int64_too", "int64_col", "float64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], columns)) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + kwargs = {key: 42} + bf_result = bf_df.assign(**kwargs).to_pandas() + pd_result = pd_df.assign(**kwargs) + + # Pandas assign results in non-nullable dtype + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +def test_column_multi_index_rename(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "float64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.rename(columns={"b": "c"}).to_pandas() + pd_result = pd_df.rename(columns={"b": "c"}) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_column_multi_index_reset_index(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "float64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.reset_index().to_pandas() + pd_result = pd_df.reset_index() + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pandas.Int64Dtype()) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_column_multi_index_binary_op(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "float64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = (bf_df[("a", "a")] + 3).to_pandas() + pd_result = pd_df[("a", "a")] + 3 + + pandas.testing.assert_series_equal(bf_result, pd_result) + + +def test_column_multi_index_agg(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "float64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.agg(["sum", "mean"]).to_pandas() + pd_result = pd_df.agg(["sum", "mean"]) + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + pandas.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + +def test_column_multi_index_prefix_suffix(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "float64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.add_prefix("prefixed_").add_suffix("_suffixed").to_pandas() + pd_result = pd_df.add_prefix("prefixed_").add_suffix("_suffixed") + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_column_multi_index_cumsum(scalars_df_index, scalars_pandas_df_index): + if pandas.__version__.startswith("1."): + pytest.skip("pandas 1.x. does not handle nullable ints properly in cumsum") + columns = ["int64_too", "int64_col", "float64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.cumsum().to_pandas() + pd_result = pd_df.cumsum() + + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index 96697dbcabe..9a3d55aed23 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -20,6 +20,7 @@ import google.auth.exceptions import pytest +import bigframes.core.global_session import bigframes.pandas as bpd @@ -289,7 +290,7 @@ def test_reset_session_after_bq_session_ended(): # Now try to reset session and verify that it works bpd.reset_session() - assert bpd._global_session is None + assert bigframes.core.global_session._global_session is None # Now verify that use is able to start over df = bpd.read_gbq(test_query) @@ -332,7 +333,7 @@ def test_reset_session_after_credentials_need_reauthentication(monkeypatch): # Now verify that resetting the session works bpd.reset_session() - assert bpd._global_session is None + assert bigframes.core.global_session._global_session is None # Now verify that use is able to start over df = bpd.read_gbq(test_query) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 47d758763ba..e40addc4ebd 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -12,11 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +from google.cloud import bigquery +from ibis.backends.bigquery import datatypes as bq_types +from ibis.expr import datatypes as ibis_types import pandas as pd import pytest import bigframes -from bigframes.remote_function import read_gbq_function, remote_function +from bigframes import remote_function as rf from tests.system.utils import assert_pandas_df_equal_ignore_ordering @@ -98,6 +101,16 @@ def session_with_bq_connection_location_project_specified( ) +def test_supported_types_correspond(): + # The same types should be representable by the supported Python and BigQuery types. + ibis_types_from_python = {ibis_types.dtype(t) for t in rf.SUPPORTED_IO_PYTHON_TYPES} + ibis_types_from_bigquery = { + bq_types.BigQueryType.to_ibis(tk) for tk in rf.SUPPORTED_IO_BIGQUERY_TYPEKINDS + } + + assert ibis_types_from_python == ibis_types_from_bigquery + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_direct_no_session_param( bigquery_client, @@ -107,7 +120,7 @@ def test_remote_function_direct_no_session_param( dataset_id_permanent, bq_cf_connection, ): - @remote_function( + @rf.remote_function( [int], int, bigquery_client=bigquery_client, @@ -157,7 +170,7 @@ def test_remote_function_direct_no_session_param_location_specified( dataset_id_permanent, bq_cf_connection_location, ): - @remote_function( + @rf.remote_function( [int], int, bigquery_client=bigquery_client, @@ -205,7 +218,7 @@ def test_remote_function_direct_no_session_param_location_mismatched( ): with pytest.raises(ValueError): - @remote_function( + @rf.remote_function( [int], int, bigquery_client=bigquery_client, @@ -229,7 +242,7 @@ def test_remote_function_direct_no_session_param_location_project_specified( dataset_id_permanent, bq_cf_connection_location_project, ): - @remote_function( + @rf.remote_function( [int], int, bigquery_client=bigquery_client, @@ -277,7 +290,7 @@ def test_remote_function_direct_no_session_param_project_mismatched( ): with pytest.raises(ValueError): - @remote_function( + @rf.remote_function( [int], int, bigquery_client=bigquery_client, @@ -294,7 +307,7 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_direct_session_param(session_with_bq_connection, scalars_dfs): - @remote_function( + @rf.remote_function( [int], int, session=session_with_bq_connection, @@ -500,6 +513,18 @@ def add_one(x): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_detects_invalid_function(bigquery_client, dataset_id): + dataset_ref = bigquery.DatasetReference.from_string(dataset_id) + with pytest.raises(ValueError) as e: + rf.read_gbq_function( + str(dataset_ref.routine("not_a_function")), + bigquery_client=bigquery_client, + ) + + assert "Unknown function" in str(e.value) + + @pytest.mark.flaky(retries=2, delay=120) def test_read_gbq_function_like_original( bigquery_client, @@ -509,7 +534,7 @@ def test_read_gbq_function_like_original( dataset_id_permanent, bq_cf_connection, ): - @remote_function( + @rf.remote_function( [int], int, bigquery_client=bigquery_client, @@ -522,7 +547,7 @@ def test_read_gbq_function_like_original( def square1(x): return x * x - square2 = read_gbq_function( + square2 = rf.read_gbq_function( function_name=square1.bigframes_remote_function, bigquery_client=bigquery_client, ) @@ -551,3 +576,111 @@ def square1(x): s2_result = int64_col_filtered.to_frame().assign(result=s2_result_col) assert_pandas_df_equal_ignore_ordering(s1_result.to_pandas(), s2_result.to_pandas()) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_reads_udfs(bigquery_client, scalars_dfs, dataset_id): + dataset_ref = bigquery.DatasetReference.from_string(dataset_id) + arg = bigquery.RoutineArgument( + name="x", + data_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + ) + sql_routine = bigquery.Routine( + dataset_ref.routine("square_sql"), + body="x * x", + arguments=[arg], + return_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + js_routine = bigquery.Routine( + dataset_ref.routine("square_js"), + body="return x * x", + language="JAVASCRIPT", + arguments=[arg], + return_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + + for routine in (sql_routine, js_routine): + # Create the routine in BigQuery and read it back using read_gbq_function. + bigquery_client.create_routine(routine, exists_ok=True) + square = rf.read_gbq_function( + str(routine.reference), bigquery_client=bigquery_client + ) + + # It should point to the named routine and yield the expected results. + assert square.bigframes_remote_function == str(routine.reference) + + src = {"x": [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]} + + routine_ref_str = rf.routine_ref_to_string_for_query(routine.reference) + direct_sql = " UNION ALL ".join( + [f"SELECT {x} AS x, {routine_ref_str}({x}) AS y" for x in src["x"]] + ) + direct_df = bigquery_client.query(direct_sql).to_dataframe() + + indirect_df = bigframes.dataframe.DataFrame(src) + indirect_df = indirect_df.assign(y=indirect_df.x.apply(square)) + indirect_df = indirect_df.to_pandas() + + assert_pandas_df_equal_ignore_ordering(direct_df, indirect_df) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_enforces_explicit_types(bigquery_client, dataset_id): + dataset_ref = bigquery.DatasetReference.from_string(dataset_id) + typed_arg = bigquery.RoutineArgument( + name="x", + data_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + ) + untyped_arg = bigquery.RoutineArgument( + name="x", + kind="ANY_TYPE", # With this kind, data_type not required for SQL functions. + ) + + both_types_specified = bigquery.Routine( + dataset_ref.routine("both_types_specified"), + body="x * x", + arguments=[typed_arg], + return_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + only_return_type_specified = bigquery.Routine( + dataset_ref.routine("only_return_type_specified"), + body="x * x", + arguments=[untyped_arg], + return_type=bigquery.StandardSqlDataType(bigquery.StandardSqlTypeNames.INT64), + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + only_arg_type_specified = bigquery.Routine( + dataset_ref.routine("only_arg_type_specified"), + body="x * x", + arguments=[typed_arg], + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + neither_type_specified = bigquery.Routine( + dataset_ref.routine("neither_type_specified"), + body="x * x", + arguments=[untyped_arg], + type_=bigquery.RoutineType.SCALAR_FUNCTION, + ) + + bigquery_client.create_routine(both_types_specified, exists_ok=True) + bigquery_client.create_routine(only_return_type_specified, exists_ok=True) + bigquery_client.create_routine(only_arg_type_specified, exists_ok=True) + bigquery_client.create_routine(neither_type_specified, exists_ok=True) + + rf.read_gbq_function( + str(both_types_specified.reference), bigquery_client=bigquery_client + ) + rf.read_gbq_function( + str(only_return_type_specified.reference), bigquery_client=bigquery_client + ) + with pytest.raises(ValueError): + rf.read_gbq_function( + str(only_arg_type_specified.reference), bigquery_client=bigquery_client + ) + with pytest.raises(ValueError): + rf.read_gbq_function( + str(neither_type_specified.reference), bigquery_client=bigquery_client + ) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 2fc34f9bae9..d825c625617 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import io import random import tempfile import textwrap @@ -683,6 +684,43 @@ def test_read_csv_local_w_encoding(session, penguins_pandas_df_default_index, en assert df.shape[0] == penguins_pandas_df_default_index.shape[0] +def test_read_pickle_local(session, penguins_pandas_df_default_index, tmp_path): + path = tmp_path / "test_read_csv_local_w_encoding.pkl" + + penguins_pandas_df_default_index.to_pickle(path) + df = session.read_pickle(path) + + pd.testing.assert_frame_equal(penguins_pandas_df_default_index, df.to_pandas()) + + +def test_read_pickle_buffer(session, penguins_pandas_df_default_index): + buffer = io.BytesIO() + penguins_pandas_df_default_index.to_pickle(buffer) + buffer.seek(0) + df = session.read_pickle(buffer) + + pd.testing.assert_frame_equal(penguins_pandas_df_default_index, df.to_pandas()) + + +def test_read_pickle_series_buffer(session): + pd_series = pd.Series([1, 2, 3, 4, 5], dtype="Int64") + buffer = io.BytesIO() + pd_series.to_pickle(buffer) + buffer.seek(0) + bf_series = session.read_pickle(buffer).to_pandas() + pd_series.index = pd_series.index.astype("Int64") + + assert (pd_series == bf_series).all() + + +def test_read_pickle_gcs(session, penguins_pandas_df_default_index, gcs_folder): + path = gcs_folder + "test_read_pickle_gcs.pkl" + penguins_pandas_df_default_index.to_pickle(path) + df = session.read_pickle(path) + + pd.testing.assert_frame_equal(penguins_pandas_df_default_index, df.to_pandas()) + + def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs # Include wildcard so that multiple files can be written/read if > 1 GB. diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index dafed08980c..bb8ae570dca 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -175,9 +175,12 @@ def test_literal_to_ibis_scalar_throws_on_incompatible_literal(): def test_remote_function_io_types_are_supported_bigframes_types(): + from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type + from bigframes.remote_function import ( - _supported_io_ibis_types as rf_supported_io_ibis_types, + SUPPORTED_IO_PYTHON_TYPES as rf_supported_io_types, ) - for ibis_type in rf_supported_io_ibis_types: + for python_type in rf_supported_io_types: + ibis_type = python_type_to_bigquery_type(python_type) assert ibis_type in bigframes.dtypes.IBIS_TO_BIGFRAMES diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 653b65c8346..6762afc61fd 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -44,6 +44,7 @@ def axes(self) -> list: They are returned in that order. Examples + .. code-block:: df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) @@ -175,8 +176,8 @@ def assign(self, **kwargs) -> DataFrame: are simply assigned to the column. Returns: - DataFrame: A new DataFrame with the new columns in addition to - all the existing columns. + bigframes.dataframe.DataFrame: A new DataFrame with the new columns + in addition to all the existing columns. """ raise NotImplementedError("abstract method") @@ -205,7 +206,7 @@ def drop( level: For MultiIndex, level from which the labels will be removed. Returns: - DataFrame: DataFrame without the removed column labels. + bigframes.dataframe.DataFrame: DataFrame without the removed column labels. Raises: KeyError: If any of the labels is not found in the selected axis. @@ -227,7 +228,7 @@ def rename( Dict-like from old column labels to new column labels. Returns: - DataFrame: DataFrame with the renamed axis labels. + bigframes.dataframe.DataFrame: DataFrame with the renamed axis labels. Raises: KeyError: If any of the labels is not found. @@ -238,7 +239,7 @@ def rename_axis(self, mapper: Optional[str], **kwargs) -> DataFrame: """ Set the name of the axis for the index. - .. Note:: + .. note:: Currently only accepts a single string parameter (the new name of the index). @@ -247,7 +248,7 @@ def rename_axis(self, mapper: Optional[str], **kwargs) -> DataFrame: Value to set the axis name attribute. Returns: - DataFrame: DataFrame with the new index name + bigframes.dataframe.DataFrame: DataFrame with the new index name """ raise NotImplementedError("abstract method") @@ -317,7 +318,7 @@ def reset_index( the index to the default integer index. Returns: - DataFrame: DataFrame with the new index. + bigframes.dataframe.DataFrame: DataFrame with the new index. """ raise NotImplementedError("abstract method") @@ -344,7 +345,7 @@ def drop_duplicates( - ``False`` : Drop all duplicates. Returns: - DataFrame: DataFrame with duplicates removed + bigframes.dataframe.DataFrame: DataFrame with duplicates removed """ raise NotImplementedError("abstract method") @@ -366,7 +367,7 @@ def duplicated(self, subset=None, keep="first"): - False : Mark all duplicates as ``True``. Returns: - Boolean series for each duplicated rows. + bigframes.series.Series: Boolean series for each duplicated rows. """ raise NotImplementedError("abstract method") @@ -379,7 +380,7 @@ def dropna( """Remove missing values. Returns: - DataFrame: DataFrame with NA entries dropped from it. + bigframes.dataframe.DataFrame: DataFrame with NA entries dropped from it. """ raise NotImplementedError("abstract method") @@ -844,7 +845,7 @@ def groupby( values will also be treated as the key in groups. Returns: - A groupby object that contains information about the groups. + bigframes.core.groupby.SeriesGroupBy: A groupby object that contains information about the groups. """ raise NotImplementedError("abstract method") @@ -871,7 +872,7 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: values, without passing them to func. Returns: - DataFrame: Transformed DataFrame. + bigframes.dataframe.DataFrame: Transformed DataFrame. """ raise NotImplementedError("abstract method") @@ -899,7 +900,7 @@ def join(self, other, *, on: Optional[str] = None, how: str) -> DataFrame: index, preserving the order of the calling's one. Returns: - DataFrame: A dataframe containing columns from both the caller and `other`. + bigframes.dataframe.DataFrame: A dataframe containing columns from both the caller and `other`. """ raise NotImplementedError("abstract method") @@ -969,7 +970,7 @@ def merge( no suffix. At least one of the values must not be None. Returns: - DataFrame: A DataFrame of the two merged objects. + bigframes.dataframe.DataFrame: A DataFrame of the two merged objects. """ raise NotImplementedError("abstract method") @@ -1006,7 +1007,7 @@ def all(self, *, bool_only: bool = False): Include only boolean columns. Returns: - Series + bigframes.series.Series: Series if all elements are True. """ raise NotImplementedError("abstract method") @@ -1019,7 +1020,7 @@ def prod(self, *, numeric_only: bool = False): Include only float, int, boolean columns. Returns: - Series + bigframes.series.Series: Series with the product of the values. """ raise NotImplementedError("abstract method") @@ -1034,7 +1035,7 @@ def min(self, *, numeric_only: bool = False): Default False. Include only float, int, boolean columns. Returns: - Series + bigframes.series.Series: Series with the minimum of the values. """ raise NotImplementedError("abstract method") @@ -1049,7 +1050,7 @@ def max(self, *, numeric_only: bool = False): Default False. Include only float, int, boolean columns. Returns: - Series + bigframes.series.Series: Series after the maximum of values. """ raise NotImplementedError("abstract method") @@ -1063,7 +1064,7 @@ def sum(self, *, numeric_only: bool = False): Default False. Include only float, int, boolean columns. Returns: - Series + bigframes.series.Series: Series with the sum of values. """ raise NotImplementedError("abstract method") @@ -1075,7 +1076,7 @@ def mean(self, *, numeric_only: bool = False): Default False. Include only float, int, boolean columns. Returns: - Series + bigframes.series.Series: Series with the mean of values. """ raise NotImplementedError("abstract method") @@ -1090,7 +1091,7 @@ def median(self, *, numeric_only: bool = False, exact: bool = False): one. Note: ``exact=True`` not yet supported. Returns: - Series + bigframes.series.Series: Series with the median of values. """ raise NotImplementedError("abstract method") @@ -1104,7 +1105,7 @@ def var(self, *, numeric_only: bool = False): Default False. Include only float, int, boolean columns. Returns: - Series + bigframes.series.Series: Series with unbiased variance over requested axis. """ raise NotImplementedError("abstract method") @@ -1118,7 +1119,7 @@ def std(self, *, numeric_only: bool = False): Default False. Include only float, int, boolean columns. Returns: - Series + bigframes.series.Series: Series with sample standard deviation. """ raise NotImplementedError("abstract method") @@ -1134,8 +1135,8 @@ def count(self, *, numeric_only: bool = False): Include only `float`, `int` or `boolean` data. Returns: - For each column/row the number of non-NA/null entries. - If `level` is specified returns a `DataFrame`. + bigframes.series.Series: For each column/row the number of + non-NA/null entries. If `level` is specified returns a `DataFrame`. """ raise NotImplementedError("abstract method") @@ -1143,10 +1144,8 @@ def nunique(self): """ Count number of distinct elements in specified axis. - Return Series with number of distinct elements. - Returns: - Series + bigframes.series.Series: Series with number of distinct elements. """ raise NotImplementedError("abstract method") @@ -1156,7 +1155,7 @@ def cummin(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative minimum. Returns: - DataFrame: Return cumulative minimum of DataFrame. + bigframes.dataframe.DataFrame: Return cumulative minimum of DataFrame. """ raise NotImplementedError("abstract method") @@ -1166,7 +1165,7 @@ def cummax(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative maximum. Returns: - DataFrame: Return cumulative maximum of DataFrame. + bigframes.dataframe.DataFrame: Return cumulative maximum of DataFrame. """ raise NotImplementedError("abstract method") @@ -1176,7 +1175,7 @@ def cumsum(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative sum. Returns: - DataFrame: Return cumulative sum of DataFrame. + bigframes.dataframe.DataFrame: Return cumulative sum of DataFrame. """ raise NotImplementedError("abstract method") @@ -1186,7 +1185,7 @@ def cumprod(self) -> DataFrame: Returns a DataFrame of the same size containing the cumulative product. Returns: - DataFrame: Return cumulative product of DataFrame. + bigframes.dataframe.DataFrame: Return cumulative product of DataFrame. """ raise NotImplementedError("abstract method") @@ -1201,7 +1200,7 @@ def agg(self, func): function names, e.g. ``['sum', 'mean']``. Returns: - Series or DataFrame: Aggregated results + DataFrame or bigframes.series.Series: Aggregated results. """ raise NotImplementedError("abstract method") @@ -1218,17 +1217,51 @@ def describe(self): .. note:: Percentile values are approximates only. + .. note:: + For numeric data, the result's index will include ``count``, + ``mean``, ``std``, ``min``, ``max`` as well as lower, ``50`` and + upper percentiles. By default the lower percentile is ``25`` and the + upper percentile is ``75``. The ``50`` percentile is the + same as the median. + Returns: - Summary statistics of the Series or Dataframe provided. + bigframes.dataframe.DataFrame: Summary statistics of the Series or Dataframe provided. + """ + raise NotImplementedError("abstract method") + + def pivot(self, *, columns, index=None, values=None): + """ + Return reshaped DataFrame organized by given index / column values. + + Reshape data (produce a "pivot" table) based on column values. Uses + unique values from specified `index` / `columns` to form axes of the + resulting DataFrame. This function does not support data + aggregation, multiple values will result in a MultiIndex in the + columns. + + .. note:: + BigQuery supports up to 10000 columns. Pivot operations on columns + with too many unique values will fail if they would exceed this limit. + + .. note:: + The validity of the pivot operation is not checked. If columns and index + do not together uniquely identify input rows, the output will be + silently non-deterministic. + Args: + columns (str or object or a list of str): + Column to use to make new frame's columns. - Notes - ----- - For numeric data, the result's index will include ``count``, - ``mean``, ``std``, ``min``, ``max`` as well as lower, ``50`` and - upper percentiles. By default the lower percentile is ``25`` and the - upper percentile is ``75``. The ``50`` percentile is the - same as the median. + index (str or object or a list of str, optional): + Column to use to make new frame's index. If not given, uses existing index. + + values (str, object or a list of the previous, optional): + Column(s) to use for populating new frame's values. If not + specified, all remaining columns will be used and the result will + have hierarchically indexed columns. + + Returns: + Returns reshaped DataFrame. """ raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 039dc1eae0f..79eb4026968 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -211,7 +211,7 @@ def to_frame(self) -> DataFrame: Convert Series to DataFrame. Returns: - DataFrame: DataFrame representation of Series. + bigframes.dataframe.DataFrame: DataFrame representation of Series. """ raise NotImplementedError("abstract method") @@ -419,7 +419,7 @@ def mode(self) -> Series: Always returns Series even if only one value is returned. Returns: - Series: Modes of the Series in sorted order. + bigframes.series.Series: Modes of the Series in sorted order. """ raise NotImplementedError("abstract method") @@ -440,7 +440,7 @@ def drop_duplicates( ``False`` : Drop all duplicates. Returns: - Series: Series with duplicates dropped or None if ``inplace=True``. + bigframes.series.Series: Series with duplicates dropped or None if ``inplace=True``. """ raise NotImplementedError("abstract method") @@ -463,8 +463,8 @@ def duplicated(self, keep="first") -> Series: ``False`` : Mark all duplicates as ``True``. Returns: - Series: Series indicating whether each value has occurred in the - preceding values. + bigframes.series.Series: Series indicating whether each value has occurred in the + preceding values. """ raise NotImplementedError("abstract method") @@ -478,7 +478,7 @@ def round(self, decimals: int = 0) -> Series: it specifies the number of positions to the left of the decimal point. Returns: - Series: Rounded values of the Series. + bigframes.series.Series: Rounded values of the Series. """ raise NotImplementedError("abstract method") @@ -569,7 +569,7 @@ def sort_values( the end. Returns: - Series or None: Series ordered by values or None if ``inplace=True``. + bigframes.series.Series: Series ordered by values or None if ``inplace=True``. """ raise NotImplementedError("abstract method") @@ -597,7 +597,7 @@ def sort_index( Not implemented for MultiIndex. Returns: - Series or None: The original Series sorted by the labels or None if + bigframes.series.Series: The original Series sorted by the labels or None if ``inplace=True``. """ @@ -624,7 +624,7 @@ def nlargest( size larger than `n`. Returns: - Series: The `n` largest values in the Series, sorted in decreasing order. + bigframes.series.Series: The `n` largest values in the Series, sorted in decreasing order. """ raise NotImplementedError("abstract method") @@ -647,7 +647,7 @@ def nsmallest(self, n: int = 5, keep: str = "first") -> Series: size larger than `n`. Returns: - Series: The `n` smallest values in the Series, sorted in increasing order. + bigframes.series.Series: The `n` smallest values in the Series, sorted in increasing order. """ raise NotImplementedError("abstract method") @@ -669,7 +669,7 @@ def apply( Python function or NumPy ufunc to apply. Returns: - Series or DataFrame: If func returns a Series object the result + bigframes.series.Series: If func returns a Series object the result will be a DataFrame. """ raise NotImplementedError("abstract method") @@ -723,7 +723,8 @@ def groupby( If False, NA values will also be treated as the key in groups. Returns: - SeriesGroupBy: Returns a groupby object that contains information about the groups. + bigframes.core.groupby.SeriesGroupBy: Returns a groupby object that contains + information about the groups. """ raise NotImplementedError("abstract method") @@ -750,15 +751,12 @@ def drop( level: For MultiIndex, level for which the labels will be removed. - Returns - ------- - Series or None - Series with specified index labels removed or None if ``inplace=True``. + Returns: + bigframes.series.Series: Series with specified index labels removed + or None if ``inplace=True``. - Raises - ------ - KeyError - If none of the labels are found in the index. + Raises: + KeyError: If none of the labels are found in the index. """ raise NotImplementedError("abstract method") @@ -844,7 +842,7 @@ def cumprod(self): product. Returns: - Return cumulative sum of scalar or Series. + bigframes.series.Series: Return cumulative sum of scalar or Series. """ raise NotImplementedError("abstract method") @@ -878,7 +876,7 @@ def cummax(self): For `Series` this parameter is unused and defaults to 0. Returns: - scalar or Series: Return cumulative maximum of scalar or Series. + bigframes.series.Series: Return cumulative maximum of scalar or Series. """ raise NotImplementedError("abstract method") @@ -901,7 +899,7 @@ def cummin(self): compatibility with NumPy. Returns: - scalar or Series: Return cumulative minimum of scalar or Series. + bigframes.series.Series: Return cumulative minimum of scalar or Series. """ raise NotImplementedError("abstract method") @@ -930,7 +928,7 @@ def ne(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -945,7 +943,7 @@ def le(self, other) -> Series: other: Series, or scalar value Returns: - Series. The result of the comparison. + bigframes.series.Series. The result of the comparison. """ raise NotImplementedError("abstract method") @@ -960,7 +958,7 @@ def lt(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -975,7 +973,7 @@ def ge(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -990,7 +988,7 @@ def gt(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1005,7 +1003,7 @@ def add(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1020,7 +1018,7 @@ def radd(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1038,7 +1036,7 @@ def sub( other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1053,7 +1051,7 @@ def rsub(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1068,7 +1066,7 @@ def mul(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1097,7 +1095,7 @@ def truediv(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1112,7 +1110,7 @@ def rtruediv(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1127,7 +1125,7 @@ def floordiv(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1142,7 +1140,7 @@ def rfloordiv(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1157,7 +1155,7 @@ def mod(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1172,7 +1170,7 @@ def rmod(self, other) -> Series: other (Series, or scalar value): Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError("abstract method") @@ -1373,7 +1371,7 @@ def where(self, cond, other): extension dtypes). Returns: - Series + bigframes.series.Series: Series after the replacement. """ raise NotImplementedError("abstract method") @@ -1397,7 +1395,7 @@ def mask(self, cond, other): extension dtypes). Returns: - Series + bigframes.series.Series: Series after the replacement. """ raise NotImplementedError("abstract method") @@ -1460,7 +1458,7 @@ def rename(self, index, **kwargs) -> Series | None: attribute. Returns: - Series: Series with index labels + bigframes.series.Series: Series with index labels. """ raise NotImplementedError("abstract method") @@ -1474,7 +1472,7 @@ def rename_axis(self, mapper, **kwargs): Value to set the axis name attribute. Returns: - Series: Series with the name of the axis set. + bigframes.series.Series: Series with the name of the axis set. """ raise NotImplementedError("abstract method") @@ -1514,7 +1512,8 @@ def rolling( to the size of the window. Returns: - ``Window`` subclass if a ``win_type`` is passed.``Rolling`` subclass if ``win_type`` is not passed + bigframes.core.window.Window: ``Window`` subclass if a ``win_type`` is passed. + ``Rolling`` subclass if ``win_type`` is not passed. """ raise NotImplementedError("abstract method") @@ -1528,7 +1527,7 @@ def expanding(self, min_periods=1): otherwise, result is ``np.nan``. Returns: - ``Expanding`` subclass + bigframes.core.window.Window: ``Expanding`` subclass. """ raise NotImplementedError("abstract method") @@ -1591,7 +1590,7 @@ def isin(self, values): TypeError. Instead, turn a single string into a list of one element. Returns: - bigframes.series.Series: Series of booleans indicating if each element is in values. + bigframes.series.Series: Series of booleans indicating if each element is in values. Raises: TypeError: If input is not list-like. diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py index a27093b552c..5e3d0b047ff 100644 --- a/third_party/bigframes_vendored/pandas/core/strings/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -136,6 +136,102 @@ def isnumeric(self): raise NotImplementedError("abstract method") + def isalpha(self): + """Check whether all characters in each string are alphabetic. + + This is equivalent to running the Python string method + :meth:`str.isalpha` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + bigframes.series.Series: Series with the same length as the originalSeries/Index. + """ + + raise NotImplementedError("abstract method") + + def isdigit(self): + """Check whether all characters in each string are digits. + + This is equivalent to running the Python string method + :meth:`str.isdigit` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + bigframes.series.Series: Series with the same length as the originalSeries/Index. + """ + + raise NotImplementedError("abstract method") + + def isalnum(self): + """Check whether all characters in each string are alphanumeric. + + This is equivalent to running the Python string method + :meth:`str.isalnum` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + bigframes.series.Series: Series or Index of boolean values with the + same length as the original Series/Index. + """ + + raise NotImplementedError("abstract method") + + def isspace(self): + """Check whether all characters in each string are whitespace. + + This is equivalent to running the Python string method + :meth:`str.isspace` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + bigframes.series.Series: Series or Index of boolean values with the + same length as the original Series/Index. + """ + + raise NotImplementedError("abstract method") + + def islower(self): + """Check whether all characters in each string are lowercase. + + This is equivalent to running the Python string method + :meth:`str.islower` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + bigframes.series.Series: Series or Index of boolean values with the + same length as the original Series/Index. + """ + + raise NotImplementedError("abstract method") + + def isupper(self): + """Check whether all characters in each string are uppercase. + + This is equivalent to running the Python string method + :meth:`str.isupper` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + bigframes.series.Series: Series or Index of boolean values with the + same length as the original Series/Index. + """ + + raise NotImplementedError("abstract method") + + def isdecimal(self): + """Check whether all characters in each string are decimal. + + This is equivalent to running the Python string method + :meth:`str.isdecimal` for each element of the Series/Index. If a string + has zero characters, ``False`` is returned for that check. + + Returns: + bigframes.series.Series: Series or Index of boolean values with the + same length as the original Series/Index. + """ + + raise NotImplementedError("abstract method") + def rstrip(self): """Remove trailing characters. @@ -427,3 +523,47 @@ def rjust( bigframes.series.Series: Returns Series or Index with minimum number of char in object. """ raise NotImplementedError("abstract method") + + def zfill( + self, + width: int, + ): + """ + Pad strings in the Series/Index by prepending '0' characters. + + Strings in the Series/Index are padded with '0' characters on the + left of the string to reach a total string length `width`. Strings + in the Series/Index with length greater or equal to `width` are + unchanged. + + Args: + width (int): + Minimum length of resulting string; strings with length less + than `width` be prepended with '0' characters. + + Returns: + bigframes.series.Series: Series of objects. + """ + raise NotImplementedError("abstract method") + + def center( + self, + width: int, + fillchar: str = " ", + ): + """ + Pad left and right side of strings in the Series/Index. + + Equivalent to :meth:`str.center`. + + Args: + width (int): + Minimum width of resulting string; additional characters will be filled + with character defined in `fillchar`. + fillchar (str, default ' '): + Additional character for filling, default is whitespace. + + Returns: + bigframes.series.Series: Returns Series or Index with minimum number of char in object. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 9425ead0e34..730872034dc 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -15,7 +15,56 @@ def read_gbq( col_order: Iterable[str] = (), max_results: Optional[int] = None, ): - """Loads DataFrame from BigQuery. + """Loads a DataFrame from BigQuery. + + BigQuery tables are an unordered, unindexed data source. By default, + the DataFrame will have an arbitrary index and ordering. + + Set the `index_col` argument to one or more columns to choose an + index. The resulting DataFrame is sorted by the index columns. For the + best performance, ensure the index columns don't contain duplicate + values. + + .. note:: + By default, even SQL query inputs with an ORDER BY clause create a + DataFrame with an arbitrary ordering. Use ``row_number() OVER + (ORDER BY ...) AS rowindex`` in your SQL query and set + ``index_col='rowindex'`` to preserve the desired ordering. + + If your query doesn't have an ordering, select ``GENERATE_UUID() AS + rowindex`` in your SQL and set ``index_col='rowindex'`` for the + best performance. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Preserve ordering in a query input. + + >>> bpd.read_gbq(''' + ... SELECT + ... -- Instead of an ORDER BY clause on the query, use + ... -- ROW_NUMBER() to create an ordered DataFrame. + ... ROW_NUMBER() OVER (ORDER BY AVG(pitchSpeed) DESC) + ... AS rowindex, + ... + ... pitcherFirstName, + ... pitcherLastName, + ... AVG(pitchSpeed) AS averagePitchSpeed + ... FROM `bigquery-public-data.baseball.games_wide` + ... WHERE year = 2016 + ... GROUP BY pitcherFirstName, pitcherLastName + ... ''', index_col="rowindex").head(n=5) + pitcherFirstName pitcherLastName averagePitchSpeed + rowindex + 1 Albertin Chapman 96.514113 + 2 Zachary Britton 94.591039 + 3 Trevor Rosenthal 94.213953 + 4 Jose Torres 94.103448 + 5 Tayron Guerrero 93.863636 + + [5 rows x 3 columns] Args: query (str): diff --git a/third_party/bigframes_vendored/pandas/io/pickle.py b/third_party/bigframes_vendored/pandas/io/pickle.py new file mode 100644 index 00000000000..a160ef0c4ea --- /dev/null +++ b/third_party/bigframes_vendored/pandas/io/pickle.py @@ -0,0 +1,55 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/io/pickle.py +""" pickle compat """ +from __future__ import annotations + +from pandas._typing import ( + CompressionOptions, + FilePath, + ReadPickleBuffer, + StorageOptions, +) + + +class PickleIOMixin: + def read_pickle( + self, + filepath_or_buffer: FilePath | ReadPickleBuffer, + compression: CompressionOptions = "infer", + storage_options: StorageOptions = None, + ): + """Load pickled BigFrames object (or any object) from file. + + .. note:: + If the content of the pickle file is a Series and its name attribute is None, + the name will be set to '0' by default. + + Args: + filepath_or_buffer (str, path object, or file-like object): + String, path object (implementing os.PathLike[str]), or file-like object + implementing a binary readlines() function. Also accepts URL. URL is not + limited to S3 and GCS. + compression (str or dict, default 'infer'): + For on-the-fly decompression of on-disk data. If 'infer' and + 'filepath_or_buffer' is path-like, then detect compression from the following + extensions: '.gz', '.bz2', '.zip', '.xz', '.zst', '.tar', '.tar.gz', '.tar.xz' + or '.tar.bz2' (otherwise no compression). If using 'zip' or 'tar', the ZIP + file must contain only one data file to be read in. Set to None for no + decompression. Can also be a dict with key 'method' set to one of {'zip', + 'gzip', 'bz2', 'zstd', 'tar'} and other key-value pairs are forwarded to + zipfile.ZipFile, gzip.GzipFile, bz2.BZ2File, zstandard.ZstdDecompressor or + tarfile.TarFile, respectively. As an example, the following could be passed + for Zstandard decompression using a custom compression dictionary + compression={'method': 'zstd', 'dict_data': my_compression_dict}. + storage_options (dict, default None): + Extra options that make sense for a particular storage connection, e.g. host, + port, username, password, etc. For HTTP(S) URLs the key-value pairs are + forwarded to urllib.request.Request as header options. For other URLs (e.g. + starting with “s3://”, and “gcs://”) the key-value pairs are forwarded to + fsspec.open. Please see fsspec and urllib for more details, and for more + examples on storage options refer here. + + Returns: + bigframes.dataframe.DataFrame or bigframes.series.Series: same type as object + stored in file. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index 03958f75950..fc48cde85ba 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -113,7 +113,7 @@ def score(self, X, y): ``(n_samples, n_samples_fitted)``, where ``n_samples_fitted`` is the number of samples used in the fitting for the estimator. - y (bigframes.dataframe.DataFrame or bigframes.series.Series: + y (bigframes.dataframe.DataFrame or bigframes.series.Series): Series or DataFrame of shape (n_samples,) or (n_samples, n_outputs). True values for `X`. @@ -123,6 +123,27 @@ def score(self, X, y): raise NotImplementedError("abstract method") +class TransformerMixin: + """Mixin class for all transformers.""" + + def fit_transform(self, X, y=None): + """Fit to data, then transform it. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples, n_features). + Input samples. + + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or DataFrame of shape (n_samples,) or (n_samples, n_outputs). Default None. + Target values (None for unsupervised transformations). + + Returns: + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_features_new) + Transformed DataFrame. + """ + + class MetaEstimatorMixin: _required_parameters = ["estimator"] """Mixin class for all meta estimators in scikit-learn.""" diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index 068aa4d2908..ea4df0dc023 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -76,8 +76,6 @@ def predict( Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples, n_features). New data to predict. - y: (default None) - Not used, present here for API consistency by convention. Returns: bigframes.dataframe.DataFrame: DataFrame of the cluster each sample belongs to. From 89b95033d6b449bfc21249057d7c024d096c80d0 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 29 Aug 2023 11:37:20 -0500 Subject: [PATCH 007/921] chore: sync changes from internal repo (#10) feat: support `Series.corr` fix: raise AttributeError for unimplemented pandas methods feat: support `DataFrame.stack` feat: support `np.arcsin`, `np.arccos`, `np.arctan`, `np.sinh`, `np.cosh`, `np.tanh`, `np.arcsinh`, `np.arccosh`, `np.arctanh`, `np.exp` with Series argument fix: align column names with pandas in `DataFrame.agg` results docs: set `options.bigquery.project` in sample code chore: unit test internal `get_standardized_ids` method fix: include survey link in abstract `NotImplementedError` exception messages perf: lazily instantiate client library objects fix: allow (but still not recommended) `ORDER BY` in `read_gbq` input when an `index_col` is defined feat: support `read_json` with `engine=bigquery` for newline-delimited JSON files chore: remove unneeded `types-retry` reference feat: support `np.sin`, `np.cos`, `np.tan`, `np.log`, `np.log10`, `np.sqrt`, `np.abs` with Series argument fix: label temp table creation jobs with `source=bigquery-dataframes-temp` label fix: support spaces in column names in `DataFrame` initializater chore: fix permissions on publish docs script feat: support `df[my_column] = [a python list]` feat: add `components_`, `explained_variance_`, and `explained_variance_ratio_` properties to `bigframes.ml.decomposition.PCA` chore: add execute permissions on publish docs script docs: fix link to GitHub chore: fix docs build fix: check for IAM role on the BigQuery connection when initializing a `remote_function` chore: revert pin to maximum pytest-retry plugin version in tests --- .kokoro/docs/common.cfg | 2 +- .kokoro/publish-docs.sh | 0 README.rst | 9 +- bigframes/constants.py | 2 + bigframes/core/__init__.py | 102 +- bigframes/core/block_transforms.py | 32 + bigframes/core/blocks.py | 159 ++- bigframes/core/groupby/__init__.py | 23 +- bigframes/core/indexes/__init__.py | 3 - bigframes/core/utils.py | 53 + bigframes/dataframe.py | 167 ++- bigframes/ml/core.py | 32 +- bigframes/ml/decomposition.py | 29 + bigframes/ml/sql.py | 10 + bigframes/operations/__init__.py | 163 +++ bigframes/operations/base.py | 5 + bigframes/pandas/__init__.py | 31 +- bigframes/remote_function.py | 109 +- bigframes/series.py | 55 +- bigframes/session.py | 467 ++++-- mypy.ini | 3 + notebooks/dataframes/dataframe.ipynb | 1249 +++++++++++++++-- .../bq_dataframes_ml_linear_regression.ipynb | 6 +- .../getting_started_bq_dataframes.ipynb | 6 +- noxfile.py | 8 +- setup.py | 2 + testing/constraints-3.9.txt | 2 + tests/system/conftest.py | 77 + tests/system/small/ml/conftest.py | 43 +- tests/system/small/ml/test_core.py | 94 ++ tests/system/small/ml/test_decomposition.py | 109 +- tests/system/small/test_dataframe.py | 80 +- tests/system/small/test_dataframe_io.py | 92 +- tests/system/small/test_groupby.py | 21 +- tests/system/small/test_multiindex.py | 36 + tests/system/small/test_numpy.py | 69 + tests/system/small/test_remote_function.py | 12 + tests/system/small/test_series.py | 20 +- tests/system/small/test_session.py | 125 +- tests/unit/core/test_utils.py | 56 + tests/unit/ml/test_sql.py | 15 + .../bigframes_vendored/pandas/core/frame.py | 144 +- .../bigframes_vendored/pandas/core/generic.py | 37 +- .../pandas/core/groupby/__init__.py | 46 +- .../pandas/core/indexes/accessor.py | 23 +- .../pandas/core/indexes/base.py | 8 +- .../pandas/core/indexing.py | 6 +- .../pandas/core/reshape/concat.py | 4 +- .../pandas/core/reshape/tile.py | 4 +- .../bigframes_vendored/pandas/core/series.py | 206 +-- .../pandas/core/strings/accessor.py | 66 +- .../pandas/core/window/rolling.py | 16 +- .../bigframes_vendored/pandas/io/gbq.py | 4 +- .../bigframes_vendored/pandas/io/parquet.py | 4 +- .../pandas/io/parsers/readers.py | 82 +- .../bigframes_vendored/pandas/io/pickle.py | 4 +- .../bigframes_vendored/sklearn/base.py | 6 +- .../sklearn/cluster/_kmeans.py | 11 +- .../sklearn/compose/_column_transformer.py | 5 +- .../sklearn/decomposition/_pca.py | 54 +- .../sklearn/ensemble/_forest.py | 8 +- .../sklearn/linear_model/_base.py | 7 +- .../sklearn/linear_model/_logistic.py | 3 +- .../sklearn/metrics/_classification.py | 12 +- .../sklearn/metrics/_ranking.py | 8 +- .../sklearn/metrics/_regression.py | 4 +- .../bigframes_vendored/sklearn/pipeline.py | 7 +- .../sklearn/preprocessing/_data.py | 5 +- .../sklearn/preprocessing/_encoder.py | 5 +- .../bigframes_vendored/xgboost/sklearn.py | 6 +- 70 files changed, 3599 insertions(+), 774 deletions(-) mode change 100644 => 100755 .kokoro/publish-docs.sh create mode 100644 tests/system/small/test_numpy.py create mode 100644 tests/unit/core/test_utils.py diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg index ce84d7ec49a..bd73988540c 100644 --- a/.kokoro/docs/common.cfg +++ b/.kokoro/docs/common.cfg @@ -20,7 +20,7 @@ env_vars: { } env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "git/bigframes/.kokoro/publish-docs.sh" + value: ".kokoro/publish-docs.sh" } env_vars: { diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh old mode 100644 new mode 100755 diff --git a/README.rst b/README.rst index 6ae3753eed4..935c54cc8b5 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,7 @@ method accepts either a fully-qualified table ID or a SQL query. import bigframes.pandas as bpd + bpd.options.bigquery.project = your_gcp_project_id df1 = bpd.read_gbq("project.dataset.table") df2 = bpd.read_gbq("SELECT a, b, c, FROM `project.dataset.table`") @@ -260,7 +261,7 @@ To view and manage Cloud Functions functions, use the `Functions `_ page and use the project picker to select the project in which you created the function. For easy identification, the names of the functions -created by BigQuery DataFrames are prefixed by ``bigframes-``. +created by BigQuery DataFrames are prefixed by ``bigframes``. **Requirements** @@ -283,7 +284,9 @@ following IAM roles: * BigQuery Data Editor (roles/bigquery.dataEditor) * BigQuery Connection Admin (roles/bigquery.connectionAdmin) * Cloud Functions Developer (roles/cloudfunctions.developer) -* Service Account User (roles/iam.serviceAccountUser) +* Service Account User (roles/iam.serviceAccountUser) on the + `service account ` + ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` * Storage Object Viewer (roles/storage.objectViewer) * Project IAM Admin (roles/resourcemanager.projectIamAdmin) @@ -330,7 +333,7 @@ Data processing location BigQuery DataFrames is designed for scale, which it achieves by keeping data and processing on the BigQuery service. However, you can bring data into the -memory of your client machine by calling ``.execute()`` on a DataFrame or Series +memory of your client machine by calling ``.to_pandas()`` on a DataFrame or Series object. If you choose to do this, the memory limitation of your client machine applies. diff --git a/bigframes/constants.py b/bigframes/constants.py index 3f3f1557336..90837c79eba 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -21,3 +21,5 @@ "Share your usecase with the BigQuery DataFrames team at the " "https://bit.ly/bigframes-feedback survey." ) + +ABSTRACT_METHOD_ERROR_MESSAGE = f"Abstract method. You have likely encountered a bug. Please share this stacktrace and how you reached it with the BigQuery DataFrames team. {FEEDBACK_LINK}" diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 9f392ce1492..d6509e4c0a9 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -35,6 +35,7 @@ reencode_order_string, StringEncoding, ) +import bigframes.core.utils as utils import bigframes.dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -562,6 +563,36 @@ def aggregate( ordering=ordering, ) + def corr_aggregate( + self, corr_aggregations: typing.Sequence[typing.Tuple[str, str, str]] + ) -> ArrayValue: + """ + Get correlations between each lef_column_id and right_column_id, stored in the respective output_column_id. + This uses BigQuery's CORR under the hood, and thus only Pearson's method is used. + Arguments: + corr_aggregations: left_column_id, right_column_id, output_column_id tuples + """ + table = self.to_ibis_expr(ordering_mode="unordered") + stats = { + col_out: table[col_left].corr(table[col_right], how="pop") + for col_left, col_right, col_out in corr_aggregations + } + aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} + result = table.aggregate(**aggregates) + # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. + ordering = ExpressionOrdering( + ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), + ) + return ArrayValue( + self._session, + result, + columns=[result[col_id] for col_id in [*stats.keys()]], + hidden_ordering_columns=[result[ORDER_ID_COLUMN]], + ordering=ordering, + ) + def project_window_op( self, column_name: str, @@ -852,38 +883,75 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = Fal group_by=group_by, ) - def unpivot_single_row( + def unpivot( self, row_labels: typing.Sequence[typing.Hashable], - unpivot_columns: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], + unpivot_columns: typing.Sequence[ + typing.Tuple[str, typing.Sequence[typing.Optional[str]]] + ], *, + passthrough_columns: typing.Sequence[str] = (), index_col_id: str = "index", - dtype=pandas.Float64Dtype(), + dtype: typing.Union[ + bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] + ] = pandas.Float64Dtype(), ) -> ArrayValue: - """Unpivot a single row.""" - # TODO: Generalize to multiple row input - table = self.to_ibis_expr(ordering_mode="unordered") + """ + Unpivot ArrayValue columns. + + Args: + row_labels: Identifies the source of the row. Must be equal to length to source column list in unpivot_columns argument. + unpivot_columns: Mapping of column id to list of input column ids. Lists of input columns may use None. + passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. + index_col_id (str): The column id to be used for the row labels. + dtype (dtype or list of dtype): Dtype to use for the unpivot columns. If list, must be equal in number to unpivot_columns. + + Returns: + ArrayValue: The unpivoted ArrayValue + """ + table = self.to_ibis_expr(ordering_mode="offset_col") sub_expressions = [] - # TODO: validate all columns are equal length, as well as row labels + # Use ibis memtable to infer type of rowlabels (if possible) + # TODO: Allow caller to specify dtype + labels_ibis_type = ibis.memtable({"col": row_labels})["col"].type() + labels_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(labels_ibis_type) + row_n = len(row_labels) if not all( len(source_columns) == row_n for _, source_columns in unpivot_columns ): raise ValueError("Columns and row labels must all be same length.") - # Select each column for i in range(row_n): values = [] - for result_col, source_cols in unpivot_columns: - values.append( - ops.AsTypeOp(dtype)._as_ibis(table[source_cols[i]]).name(result_col) - ) - + for j in range(len(unpivot_columns)): + result_col, source_cols = unpivot_columns[j] + col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype + if source_cols[i] is not None: + values.append( + ops.AsTypeOp(col_dtype) + ._as_ibis(table[source_cols[i]]) + .name(result_col) + ) + else: + values.append( + bigframes.dtypes.literal_to_ibis_scalar( + None, force_dtype=col_dtype + ).name(result_col) + ) + offsets_value = ( + ((table[ORDER_ID_COLUMN] * row_n) + i) + .cast(ibis_dtypes.int64) + .name(ORDER_ID_COLUMN), + ) sub_expr = table.select( - ibis_types.literal(row_labels[i]).name(index_col_id), + passthrough_columns, + bigframes.dtypes.literal_to_ibis_scalar( + row_labels[i], force_dtype=labels_dtype # type:ignore + ).name(index_col_id), *values, - ibis_types.literal(i).name(ORDER_ID_COLUMN), + offsets_value, ) sub_expressions.append(sub_expr) rotated_table = ibis.union(*sub_expressions) @@ -891,13 +959,15 @@ def unpivot_single_row( value_columns = [ rotated_table[value_col_id] for value_col_id, _ in unpivot_columns ] + passthrough_values = [rotated_table[col] for col in passthrough_columns] return ArrayValue( session=self._session, table=rotated_table, - columns=[rotated_table[index_col_id], *value_columns], + columns=[rotated_table[index_col_id], *value_columns, *passthrough_values], hidden_ordering_columns=[rotated_table[ORDER_ID_COLUMN]], ordering=ExpressionOrdering( ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), total_ordering_columns=frozenset([ORDER_ID_COLUMN]), ), ) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index b13d7bf2d3a..abf8b887d82 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -197,3 +197,35 @@ def rank( ) return block.select_columns(rownum_col_ids).with_column_labels(labels) + + +def dropna(block: blocks.Block, how: typing.Literal["all", "any"] = "any"): + """ + Drop na entries from block + """ + if how == "any": + filtered_block = block + for column in block.value_columns: + filtered_block, result_id = filtered_block.apply_unary_op( + column, ops.notnull_op + ) + filtered_block = filtered_block.filter(result_id) + filtered_block = filtered_block.drop_columns([result_id]) + return filtered_block + else: # "all" + filtered_block = block + predicate = None + for column in block.value_columns: + filtered_block, partial_predicate = filtered_block.apply_unary_op( + column, ops.notnull_op + ) + if predicate: + filtered_block, predicate = filtered_block.apply_binary_op( + partial_predicate, predicate, ops.or_op + ) + else: + predicate = partial_predicate + if predicate: + filtered_block = filtered_block.filter(predicate) + filtered_block = filtered_block.select_columns(block.value_columns) + return filtered_block diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 2731990febe..f23a4d0b5c9 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -80,15 +80,18 @@ def __init__( self, expr: core.ArrayValue, index_columns: Iterable[str], - column_labels: typing.Union[pd.Index, typing.Sequence[Label]], - index_labels: typing.Union[pd.Index, typing.Sequence[Label], None] = None, + column_labels: typing.Union[pd.Index, typing.Iterable[Label]], + index_labels: typing.Union[pd.Index, typing.Iterable[Label], None] = None, ): """Construct a block object, will create default index if no index columns specified.""" - if index_labels and (len(index_labels) != len(list(index_columns))): - raise ValueError( - "'index_columns' and 'index_labels' must have equal length" - ) - if len(list(index_columns)) == 0: + index_columns = list(index_columns) + if index_labels: + index_labels = list(index_labels) + if len(index_labels) != len(index_columns): + raise ValueError( + "'index_columns' and 'index_labels' must have equal length" + ) + if len(index_columns) == 0: expr, new_index_col_id = expr.promote_offsets() index_columns = [new_index_col_id] self._index_columns = tuple(index_columns) @@ -114,6 +117,7 @@ def __init__( self._stats_cache: dict[str, dict[str, typing.Any]] = { col_id: {} for col_id in self.value_columns } + # TODO(kemppeterson) Add a cache for corr to parallel the single-column stats. @property def index(self) -> indexes.IndexValue: @@ -826,9 +830,7 @@ def aggregate_all_and_pivot( dtype=pd.Float64Dtype(), ) -> Block: aggregations = [(col_id, operation, col_id) for col_id in self.value_columns] - result_expr = self.expr.aggregate( - aggregations, dropna=dropna - ).unpivot_single_row( + result_expr = self.expr.aggregate(aggregations, dropna=dropna).unpivot( row_labels=self.column_labels.to_list(), index_col_id="index", unpivot_columns=[(value_col_id, self.value_columns)], @@ -966,6 +968,26 @@ def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): self._stats_cache[column_id].update(stats_map) return stats_map[stat.name] + def get_corr_stat(self, column_id_left: str, column_id_right: str): + # TODO(kemppeterson): Clean up the column names for DataFrames.corr support + # TODO(kemppeterson): Add a cache here. + corr_aggregations = [ + ( + column_id_left, + column_id_right, + "corr_" + column_id_left + column_id_right, + ) + ] + expr = self.expr.corr_aggregate(corr_aggregations) + expr, offset_index_id = expr.promote_offsets() + block = Block( + expr, + index_columns=[offset_index_id], + column_labels=[a[2] for a in corr_aggregations], + ) + df, _ = block.to_pandas() + return df.loc[0, "corr_" + column_id_left + column_id_right] + def summarize( self, column_ids: typing.Sequence[str], @@ -983,7 +1005,7 @@ def summarize( (col_id, [f"{col_id}-{stat.name}" for stat in stats]) for col_id in column_ids ] - expr = self.expr.aggregate(aggregations).unpivot_single_row( + expr = self.expr.aggregate(aggregations).unpivot( labels, unpivot_columns=columns, index_col_id=label_col_id, @@ -1166,6 +1188,121 @@ def pivot( return result_block.with_column_labels(column_index) + def stack(self): + """Unpivot last column axis level into row axis""" + if isinstance(self.column_labels, pd.MultiIndex): + return self._stack_multi() + else: + return self._stack_mono() + + def _stack_mono(self): + if isinstance(self.column_labels, pd.MultiIndex): + raise ValueError("Expected single level index") + + # These are the values that will be turned into rows + stack_values = self.column_labels.drop_duplicates().sort_values() + + # Get matching columns + unpivot_columns: List[Tuple[str, List[str]]] = [] + dtypes: List[bigframes.dtypes.Dtype] = [] + col_id = guid.generate_guid("unpivot_") + dtype = None + input_columns: Sequence[Optional[str]] = [] + for uvalue in stack_values: + matching_ids = self.label_to_col_id.get(uvalue, []) + input_id = matching_ids[0] if len(matching_ids) > 0 else None + if input_id: + if dtype and dtype != self._column_type(input_id): + raise NotImplementedError( + "Cannot stack columns with non-matching dtypes." + ) + else: + dtype = self._column_type(input_id) + input_columns.append(input_id) + unpivot_columns.append((col_id, input_columns)) + if dtype: + dtypes.append(dtype or pd.Float64Dtype()) + + added_index_column = col_id = guid.generate_guid() + unpivot_expr = self._expr.unpivot( + row_labels=stack_values, + passthrough_columns=self.index_columns, + unpivot_columns=unpivot_columns, + index_col_id=added_index_column, + dtype=dtypes, + ) + block = Block( + unpivot_expr, + index_columns=[*self.index_columns, added_index_column], + column_labels=[None], + index_labels=[*self._index_labels, self.column_labels.names[-1]], + ) + return block + + def _stack_multi(self): + if not isinstance(self.column_labels, pd.MultiIndex): + raise ValueError("Expected multi-index") + + # These are the values that will be turned into rows + stack_values = ( + self.column_labels.get_level_values(-1).drop_duplicates().sort_values() + ) + + result_col_labels = ( + self.column_labels.droplevel(-1) + .drop_duplicates() + .sort_values() + .dropna(how="all") + ) + + # Get matching columns + unpivot_columns: List[Tuple[str, List[str]]] = [] + dtypes = [] + for val in result_col_labels: + col_id = guid.generate_guid("unpivot_") + dtype = None + input_columns: Sequence[Optional[str]] = [] + for uvalue in stack_values: + # Need to unpack if still a multi-index after dropping 1 level + label_to_match = ( + (val, uvalue) if result_col_labels.nlevels == 1 else (*val, uvalue) + ) + matching_ids = self.label_to_col_id.get(label_to_match, []) + input_id = matching_ids[0] if len(matching_ids) > 0 else None + if input_id: + if dtype and dtype != self._column_type(input_id): + raise NotImplementedError( + "Cannot stack columns with non-matching dtypes." + ) + else: + dtype = self._column_type(input_id) + input_columns.append(input_id) + # Input column i is the first one that + unpivot_columns.append((col_id, input_columns)) + if dtype: + dtypes.append(dtype or pd.Float64Dtype()) + + added_index_column = col_id = guid.generate_guid() + unpivot_expr = self._expr.unpivot( + row_labels=stack_values, + passthrough_columns=self.index_columns, + unpivot_columns=unpivot_columns, + index_col_id=added_index_column, + dtype=dtypes, + ) + block = Block( + unpivot_expr, + index_columns=[*self.index_columns, added_index_column], + column_labels=result_col_labels, + index_labels=[*self._index_labels, self.column_labels.names[-1]], + ) + return block + + def _column_type(self, col_id: str) -> bigframes.dtypes.Dtype: + col_offset = self.value_columns.index(col_id) + dtype = self.dtypes[col_offset] + return dtype + @staticmethod def _create_pivot_column_index( value_labels: Sequence[typing.Hashable], columns_values: pd.Index diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 5b217effdd1..589c5c251ca 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -197,8 +197,11 @@ def _agg_string(self, func: str) -> df.DataFrame: return df.DataFrame(agg_block) def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: - aggregations = [] + aggregations: typing.List[typing.Tuple[str, agg_ops.AggregateOp]] = [] column_labels = [] + + want_aggfunc_level = any(utils.is_list_like(aggs) for aggs in func.values()) + for label, funcs_for_id in func.items(): col_id = self._resolve_label(label) func_list = ( @@ -206,16 +209,22 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: ) for f in func_list: aggregations.append((col_id, agg_ops.lookup_agg_func(f))) - column_labels.append((col_id, f)) + column_labels.append(label) agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, as_index=self._as_index, dropna=self._dropna, ) - agg_block = agg_block.with_column_labels( - pd.MultiIndex.from_tuples(column_labels) - ) + if want_aggfunc_level: + agg_block = agg_block.with_column_labels( + utils.combine_indices( + pd.Index(column_labels), + pd.Index(agg[1].name for agg in aggregations), + ) + ) + else: + agg_block = agg_block.with_column_labels(pd.Index(column_labels)) return df.DataFrame(agg_block) def _agg_list(self, func: typing.Sequence) -> df.DataFrame: @@ -234,7 +243,9 @@ def _agg_list(self, func: typing.Sequence) -> df.DataFrame: dropna=self._dropna, ) agg_block = agg_block.with_column_labels( - pd.MultiIndex.from_tuples(column_labels) + pd.MultiIndex.from_tuples( + column_labels, names=[*self._block.column_labels.names, None] + ) ) return df.DataFrame(agg_block) diff --git a/bigframes/core/indexes/__init__.py b/bigframes/core/indexes/__init__.py index d797c57955a..184a9ce262f 100644 --- a/bigframes/core/indexes/__init__.py +++ b/bigframes/core/indexes/__init__.py @@ -14,10 +14,7 @@ from bigframes.core.indexes.index import Index, IndexValue -INDEX_COLUMN_ID = "bigframes_index_{}" - __all__ = [ "Index", "IndexValue", - "INDEX_COLUMN_ID", ] diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index a3300029054..1c0a2a1a81e 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -12,10 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import typing +from typing import Hashable, Iterable, List import pandas as pd import typing_extensions +import third_party.bigframes_vendored.pandas.io.common as vendored_pandas_io_common + +UNNAMED_COLUMN_ID = "bigframes_unnamed_column" +UNNAMED_INDEX_ID = "bigframes_unnamed_index" + def get_axis_number(axis: typing.Union[str, int, None]) -> typing.Literal[0, 1]: if axis in {0, "index", "rows", None}: @@ -31,3 +37,50 @@ def is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence def is_dict_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Mapping]: return pd.api.types.is_dict_like(obj) + + +def combine_indices(index1: pd.Index, index2: pd.Index) -> pd.MultiIndex: + """Combines indices into multi-index while preserving dtypes, names.""" + multi_index = pd.MultiIndex.from_frame( + pd.concat([index1.to_frame(index=False), index2.to_frame(index=False)], axis=1) + ) + # to_frame will produce numbered default names, we don't want these + multi_index.names = [*index1.names, *index2.names] + return multi_index + + +def get_standardized_ids( + col_labels: Iterable[Hashable], idx_labels: Iterable[Hashable] = () +) -> tuple[list[str], list[str]]: + """Get stardardized column ids as column_ids_list, index_ids_list. + The standardized_column_id must be valid BQ SQL schema column names, can only be string type and unique. + + Args: + col_labels: column labels + + idx_labels: index labels, optional. If empty, will only return column ids. + + Return: + Tuple of (standardized_column_ids, standardized_index_ids) + """ + col_ids = [ + UNNAMED_COLUMN_ID if col_label is None else str(col_label) + for col_label in col_labels + ] + idx_ids = [ + UNNAMED_INDEX_ID if idx_label is None else str(idx_label) + for idx_label in idx_labels + ] + + ids = idx_ids + col_ids + # Column values will be loaded as null if the column name has spaces. + # https://github.com/googleapis/python-bigquery/issues/1566 + ids = [id.replace(" ", "_") for id in ids] + + ids = typing.cast( + List[str], + vendored_pandas_io_common.dedup_names(ids, is_potential_multiindex=False), + ) + idx_ids, col_ids = ids[: len(idx_ids)], ids[len(idx_ids) :] + + return col_ids, idx_ids diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 5fbe5d1f9eb..ef443db0797 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -56,7 +56,6 @@ import bigframes.series import bigframes.series as bf_series import third_party.bigframes_vendored.pandas.core.frame as vendored_pandas_frame -import third_party.bigframes_vendored.pandas.io.common as vendored_pandas_io_common import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing if typing.TYPE_CHECKING: @@ -291,84 +290,59 @@ def astype( self, dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], ) -> DataFrame: - return self._apply_to_rows(ops.AsTypeOp(dtype)) + return self._apply_unary_op(ops.AsTypeOp(dtype)) def _to_sql_query( - self, always_include_index: bool - ) -> Tuple[str, List[Tuple[str, bool]]]: + self, include_index: bool + ) -> Tuple[str, list[str], list[blocks.Label]]: """Compiles this DataFrame's expression tree to SQL, optionally - including unnamed index columns. + including index columns. Args: - always_include_index (bool): - whether to include unnamed index columns. If False, only named - indexes are included. + include_index (bool): + whether to include index columns. - Returns: a tuple of (sql_string, index_column_list) - Each entry in the index column list is a tuple of (column_name, named). - If named is false, then the column name exists only in SQL + Returns: + a tuple of (sql_string, index_column_id_list, index_column_label_list). + If include_index is set to False, index_column_id_list and index_column_label_list + return empty lists. """ # Has to be unordered as it is impossible to order the sql without # including metadata columns in selection with ibis. ibis_expr = self._block.expr.to_ibis_expr(ordering_mode="unordered") - column_labels = list(self._block.column_labels) + col_labels, idx_labels = list(self._block.column_labels), list( + self._block.index_labels + ) + old_col_ids, old_idx_ids = list(self._block.value_columns), list( + self._block.index_columns + ) - # TODO(swast): Need to have a better way of controlling when to include - # the index or not. - index_has_names = all([name is not None for name in self.index.names]) - if index_has_names: - column_labels = column_labels + list(self.index.names) - elif always_include_index: - # In this mode include the index even if it is a nameless generated - # column like 'bigframes_index_0' - index_labels = [] - unnamed_index_count = 0 - for index_label in self._block.index_labels: - if isinstance(index_label, str): - index_labels.append(index_label) - else: - index_labels.append( - indexes.INDEX_COLUMN_ID.format(unnamed_index_count), - ) - unnamed_index_count += 1 + if not include_index: + idx_labels, old_idx_ids = [], [] + ibis_expr = ibis_expr.drop(*self._block.index_columns) - column_labels = column_labels + typing.cast( - List[Optional[str]], index_labels - ) + old_ids = old_idx_ids + old_col_ids + + new_col_ids, new_idx_ids = utils.get_standardized_ids(col_labels, idx_labels) + new_ids = new_idx_ids + new_col_ids - column_labels_deduped = typing.cast( - List[str], - vendored_pandas_io_common.dedup_names( - column_labels, is_potential_multiindex=False - ), - ) - column_ids = self._block.value_columns substitutions = {} - for column_id, column_label in zip(column_ids, column_labels_deduped): + for old_id, new_id in zip(old_ids, new_ids): # TODO(swast): Do we need to further escape this, or can we rely on # the BigQuery unicode column name feature? - substitutions[column_id] = column_label - - index_cols: List[Tuple[str, bool]] = [] - first_index_offset = len(self._block.column_labels) - if index_has_names or always_include_index: - for i, index_col in enumerate(self._block.index_columns): - offset = first_index_offset + i - substitutions[index_col] = column_labels_deduped[offset] - index_cols = [ - (label, index_has_names) - for label in column_labels_deduped[first_index_offset:] - ] - else: - ibis_expr = ibis_expr.drop(*self._block.index_columns) + substitutions[old_id] = new_id ibis_expr = ibis_expr.relabel(substitutions) - return typing.cast(str, ibis_expr.compile()), index_cols + return ( + typing.cast(str, ibis_expr.compile()), + new_ids[: len(idx_labels)], + idx_labels, + ) @property def sql(self) -> str: """Compiles this DataFrame's expression tree to SQL.""" - sql, _ = self._to_sql_query(always_include_index=False) + sql, _, _ = self._to_sql_query(include_index=False) return sql @property @@ -469,12 +443,12 @@ def __getattr__(self, key: str): if key in self._block.column_labels: return self.__getitem__(key) elif hasattr(pandas.DataFrame, key): - raise NotImplementedError( + raise AttributeError( textwrap.dedent( f""" - BigQuery DataFrames has not yet implemented an equivalent to - 'pandas.DataFrame.{key}'. {constants.FEEDBACK_LINK} - """ + BigQuery DataFrames has not yet implemented an equivalent to + 'pandas.DataFrame.{key}'. {constants.FEEDBACK_LINK} + """ ) ) else: @@ -872,6 +846,32 @@ def _assign_single_item( copy = self.copy() copy[k] = v(copy) return copy + elif utils.is_list_like(v): + given_rows = len(v) + actual_rows = len(self) + if given_rows != actual_rows: + raise ValueError( + f"Length of values ({given_rows}) does not match length of index ({actual_rows})" + ) + + local_df = bigframes.dataframe.DataFrame( + {k: v}, session=self._get_block().expr._session + ) + # local_df is likely (but not guarunteed) to be cached locally + # since the original list came from memory and so is probably < MAX_INLINE_DF_SIZE + + this_expr, this_offsets_col_id = self._get_block()._expr.promote_offsets() + block = blocks.Block( + expr=this_expr, + index_labels=self.index.names, + index_columns=self._block.index_columns, + column_labels=[this_offsets_col_id] + list(self._block.value_columns), + ) # offsets are temporarily the first value column, label set to id + this_df_with_offsets = DataFrame(data=block) + join_result = this_df_with_offsets.join( + other=local_df, on=this_offsets_col_id, how="left" + ) + return join_result.drop(columns=[this_offsets_col_id]) else: return self._assign_scalar(k, v) @@ -1024,13 +1024,7 @@ def add_suffix(self, suffix: str, axis: int | str | None = None) -> DataFrame: return DataFrame(self._get_block().add_suffix(suffix, axis)) def dropna(self) -> DataFrame: - block = self._block - for column in self._block.value_columns: - block, result_id = block.apply_unary_op(column, ops.notnull_op) - block = block.filter(result_id) - block = block.drop_columns([result_id]) - - return DataFrame(block) + return DataFrame(block_ops.dropna(self._block, how="any")) def any( self, @@ -1209,6 +1203,14 @@ def pivot( ) return DataFrame(pivot_block) + def stack(self): + # TODO: support 'level' param by simply reordering levels such that selected level is last before passing to Block.stack. + # TODO: support 'dropna' param by executing dropna only conditionally + result_block = block_ops.dropna(self._block.stack(), how="all") + if not isinstance(self.columns, pandas.MultiIndex): + return bigframes.series.Series(result_block) + return DataFrame(result_block) + def _drop_non_numeric(self, keep_bool=True) -> DataFrame: types_to_keep = set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) if not keep_bool: @@ -1508,15 +1510,15 @@ def _groupby_series( ) def abs(self) -> DataFrame: - return self._apply_to_rows(ops.abs_op) + return self._apply_unary_op(ops.abs_op) def isna(self) -> DataFrame: - return self._apply_to_rows(ops.isnull_op) + return self._apply_unary_op(ops.isnull_op) isnull = isna def notna(self) -> DataFrame: - return self._apply_to_rows(ops.notnull_op) + return self._apply_unary_op(ops.notnull_op) notnull = notna @@ -1736,7 +1738,7 @@ def to_parquet(self, path: str, *, index: bool = True) -> None: _, query_job = self._block.expr._session._start_query(export_data_statement) self._set_internal_query_job(query_job) - def _apply_to_rows(self, operation: ops.UnaryOp): + def _apply_unary_op(self, operation: ops.UnaryOp) -> DataFrame: block = self._block.multi_apply_unary_op(self._block.value_columns, operation) return DataFrame(block) @@ -1813,7 +1815,7 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: # to be applied before passing data to remote function, protecting from bad # inputs causing errors. reprojected_df = DataFrame(self._block._force_reproject()) - return reprojected_df._apply_to_rows( + return reprojected_df._apply_unary_op( ops.RemoteFunctionOp(func, apply_on_null=(na_action is None)) ) @@ -1871,6 +1873,25 @@ def _slice( block = self._block.slice(start=start, stop=stop, step=step) return DataFrame(block) + def __array_ufunc__( + self, ufunc: numpy.ufunc, method: str, *inputs, **kwargs + ) -> DataFrame: + """Used to support numpy ufuncs. + See: https://numpy.org/doc/stable/reference/ufuncs.html + """ + if ( + inputs[0] is not self + or method != "__call__" + or len(inputs) > 1 + or len(kwargs) > 0 + ): + return NotImplemented + + if ufunc in ops.NUMPY_TO_OP: + return self._apply_unary_op(ops.NUMPY_TO_OP[ufunc]) + + return NotImplemented + def _set_block(self, block: blocks.Block): self._block = block diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 812bb08dc38..27727c9f81f 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -74,20 +74,18 @@ def _apply_sql( string from which to construct the output dataframe. It must include the index columns of the input SQL. """ - source_sql, tagged_index_cols = input_data._to_sql_query( - always_include_index=True + source_sql, index_col_ids, index_labels = input_data._to_sql_query( + include_index=True ) - if len(tagged_index_cols) != 1: + if len(index_col_ids) != 1: raise NotImplementedError( f"Only exactly one index column is supported. {constants.FEEDBACK_LINK}" ) - index_col_name, is_named_index = tagged_index_cols[0] sql = func(source_sql) - df = session.read_gbq(sql, index_col=[index_col_name]) - if not is_named_index: - df.index.name = None + df = session.read_gbq(sql, index_col=index_col_ids) + df.index.names = index_labels return df @@ -150,10 +148,10 @@ def forecast(self) -> bpd.DataFrame: def evaluate(self, input_data: Optional[bpd.DataFrame] = None): # TODO: validate input data schema # Note: don't need index as evaluate returns a new table - source_sql, _ = ( - input_data._to_sql_query(always_include_index=False) + source_sql, _, _ = ( + input_data._to_sql_query(include_index=False) if (input_data is not None) - else (None, None) + else (None, None, None) ) sql = ml_sql.ml_evaluate(self.model_name, source_sql) @@ -166,6 +164,20 @@ def centroids(self): return self._session.read_gbq(sql) + def principal_components(self): + assert self._model.model_type == "PCA" + + sql = ml_sql.ml_principal_components(self.model_name) + + return self._session.read_gbq(sql) + + def principal_component_info(self): + assert self._model.model_type == "PCA" + + sql = ml_sql.ml_principal_component_info(self.model_name) + + return self._session.read_gbq(sql) + def copy(self, new_model_name: str, replace: bool = False) -> BqmlModel: job_config = bigquery.job.CopyJobConfig() if replace: diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 76b4f9ced6a..16106d3a7b3 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -70,6 +70,35 @@ def fit( ) return self + @property + def components_(self) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("A model must be fitted before calling components_.") + + return self._bqml_model.principal_components() + + @property + def explained_variance_(self) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError( + "A model must be fitted before calling explained_variance_." + ) + + return self._bqml_model.principal_component_info()[ + ["principal_component_id", "eigenvalue"] + ].rename(columns={"eigenvalue": "explained_variance"}) + + @property + def explained_variance_ratio_(self) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError( + "A model must be fitted before calling explained_variance_ratio_." + ) + + return self._bqml_model.principal_component_info()[ + ["principal_component_id", "explained_variance_ratio"] + ] + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 80054d40e19..bcd82435823 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -182,3 +182,13 @@ def ml_generate_text_embedding( def ml_forecast(model_name: str) -> str: """Encode ML.FORECAST for BQML""" return f"""SELECT * FROM ML.FORECAST(MODEL `{model_name}`)""" + + +def ml_principal_components(model_name: str) -> str: + """Encode ML.PRINCIPAL_COMPONENTS for BQML""" + return f"""SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `{model_name}`)""" + + +def ml_principal_component_info(model_name: str) -> str: + """Encode ML.PRINCIPAL_COMPONENT_INFO for BQML""" + return f"""SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `{model_name}`)""" diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 58f19ea8e7f..9305cf1ddab 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -32,6 +32,12 @@ _ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) _NAN = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.nan)) _INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.inf)) +_NEG_INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(-np.inf)) + +# Approx Highest number you can pass in to EXP function and get a valid FLOAT64 result +# FLOAT64 has 11 exponent bits, so max values is about 2**(2**10) +# ln(2**(2**10)) == (2**10)*ln(2) ~= 709.78, so EXP(x) for x>709.78 will overflow. +_FLOAT64_EXP_BOUND = typing.cast(ibis_types.NumericValue, ibis_types.literal(709.78)) BinaryOp = typing.Callable[[ibis_types.Value, ibis_types.Value], ibis_types.Value] TernaryOp = typing.Callable[ @@ -51,11 +57,142 @@ def is_windowed(self): return False +# Trig Functions class AbsOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.NumericValue, x).abs() +class SinOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).sin() + + +class CosOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).cos() + + +class TanOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).tan() + + +# Inverse trig functions +class ArcsinOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value.abs() <= _ibis_num(1) + return (~domain).ifelse(_NAN, numeric_value.asin()) + + +class ArccosOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value.abs() <= _ibis_num(1) + return (~domain).ifelse(_NAN, numeric_value.acos()) + + +class ArctanOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).atan() + + +# Hyperbolic trig functions +# BQ has these functions, but Ibis doesn't +class SinhOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + sinh_result = ( + numeric_value.exp() - (numeric_value.negate()).exp() + ) / _ibis_num(2) + domain = numeric_value.abs() < _FLOAT64_EXP_BOUND + return (~domain).ifelse(_INF * numeric_value.sign(), sinh_result) + + +class CoshOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + cosh_result = ( + numeric_value.exp() + (numeric_value.negate()).exp() + ) / _ibis_num(2) + domain = numeric_value.abs() < _FLOAT64_EXP_BOUND + return (~domain).ifelse(_INF, cosh_result) + + +class TanhOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + tanh_result = (numeric_value.exp() - (numeric_value.negate()).exp()) / ( + numeric_value.exp() + (numeric_value.negate()).exp() + ) + # Beyond +-20, is effectively just the sign function + domain = numeric_value.abs() < _ibis_num(20) + return (~domain).ifelse(numeric_value.sign(), tanh_result) + + +class ArcsinhOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + sqrt_part = ((numeric_value * numeric_value) + _ibis_num(1)).sqrt() + return (numeric_value.abs() + sqrt_part).ln() * numeric_value.sign() + + +class ArccoshOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + sqrt_part = ((numeric_value * numeric_value) - _ibis_num(1)).sqrt() + acosh_result = (numeric_value + sqrt_part).ln() + domain = numeric_value >= _ibis_num(1) + return (~domain).ifelse(_NAN, acosh_result) + + +class ArctanhOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value.abs() < _ibis_num(1) + numerator = numeric_value + _ibis_num(1) + denominator = _ibis_num(1) - numeric_value + ln_input = typing.cast(ibis_types.NumericValue, numerator.div(denominator)) + atanh_result = ln_input.ln().div(2) + + out_of_domain = (numeric_value.abs() == _ibis_num(1)).ifelse( + _INF * numeric_value, _NAN + ) + + return (~domain).ifelse(out_of_domain, atanh_result) + + +class SqrtOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value >= _ZERO + return (~domain).ifelse(_NAN, numeric_value.sqrt()) + + +class Log10Op(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value > _ZERO + out_of_domain = (numeric_value == _ZERO).ifelse(_NEG_INF, _NAN) + return (~domain).ifelse(out_of_domain, numeric_value.log10()) + + +class LnOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value > _ZERO + out_of_domain = (numeric_value == _ZERO).ifelse(_NEG_INF, _NAN) + return (~domain).ifelse(out_of_domain, numeric_value.ln()) + + +class ExpOp(UnaryOp): + def _as_ibis(self, x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value < _FLOAT64_EXP_BOUND + return (~domain).ifelse(_INF, numeric_value.exp()) + + class InvertOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): return typing.cast(ibis_types.NumericValue, x).negate() @@ -484,6 +621,28 @@ def _as_ibis(self, x: ibis_types.Value): year_op = YearOp() capitalize_op = CapitalizeOp() +# Just parameterless unary ops for now +# TODO: Parameter mappings +NUMPY_TO_OP: typing.Final = { + np.sin: SinOp(), + np.cos: CosOp(), + np.tan: TanOp(), + np.arcsin: ArcsinOp(), + np.arccos: ArccosOp(), + np.arctan: ArctanOp(), + np.sinh: SinhOp(), + np.cosh: CoshOp(), + np.tanh: TanhOp(), + np.arcsinh: ArcsinhOp(), + np.arccosh: ArccoshOp(), + np.arctanh: ArctanhOp(), + np.exp: ExpOp(), + np.log: LnOp(), + np.log10: Log10Op(), + np.sqrt: SqrtOp(), + np.abs: AbsOp(), +} + ### Binary Ops def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): @@ -785,3 +944,7 @@ def partial_arg3(op: TernaryOp, scalar: typing.Any) -> BinaryOp: def is_null(value) -> bool: # float NaN/inf should be treated as distinct from 'true' null values return typing.cast(bool, pd.isna(value)) and not isinstance(value, float) + + +def _ibis_num(number: float): + return typing.cast(ibis_types.NumericValue, ibis_types.literal(number)) diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 361fdca0558..81a5bc4c412 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -167,6 +167,11 @@ def _apply_binary_op( partial_op = ops.BinopPartialRight(op, other) return self._apply_unary_op(partial_op) + def _apply_corr_aggregation(self, other: series.Series) -> float: + (left, right, block) = self._align(other, how="outer") + + return block.get_corr_stat(left, right) + def _align(self, other: series.Series, how="outer") -> tuple[str, str, blocks.Block]: # type: ignore """Aligns the series value with another scalar or series object. Returns new left column id, right column id and joined tabled expression.""" values, block = self._align_n( diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index b688c18723b..280fce11126 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -148,13 +148,15 @@ def _set_default_session_location_if_possible(query): ): return - bqclient, _, _, _ = bigframes.session._create_cloud_clients( + clients_provider = bigframes.session.ClientsProvider( project=options.bigquery.project, location=options.bigquery.location, use_regional_endpoints=options.bigquery.use_regional_endpoints, credentials=options.bigquery.credentials, ) + bqclient = clients_provider.bqclient + if bigframes.session._is_query(query): job = bqclient.query(query, bigquery.QueryJobConfig(dry_run=True)) options.bigquery.location = job.location @@ -226,6 +228,33 @@ def read_csv( read_csv.__doc__ = inspect.getdoc(bigframes.session.Session.read_csv) +def read_json( + path_or_buf: str | IO["bytes"], + *, + orient: Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + dtype: Optional[Dict] = None, + encoding: Optional[str] = None, + lines: bool = False, + engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", + **kwargs, +) -> bigframes.dataframe.DataFrame: + return global_session.with_default_session( + bigframes.session.Session.read_json, + path_or_buf=path_or_buf, + orient=orient, + dtype=dtype, + encoding=encoding, + lines=lines, + engine=engine, + **kwargs, + ) + + +read_json.__doc__ = inspect.getdoc(bigframes.session.Session.read_json) + + def read_gbq( query: str, *, diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 7cf74d63111..2a4b919dab0 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -34,7 +34,14 @@ import cloudpickle import google.api_core.exceptions -from google.cloud import bigquery, bigquery_connection_v1, functions_v2 +import google.api_core.retry +from google.cloud import ( + bigquery, + bigquery_connection_v1, + functions_v2, + resourcemanager_v3, +) +import google.iam.v1 from ibis.backends.bigquery.compiler import compiles from ibis.backends.bigquery.datatypes import BigQueryType from ibis.expr.datatypes.core import DataType as IbisDataType @@ -152,6 +159,7 @@ def __init__( bq_client, bq_connection_client, bq_connection_id, + cloud_resource_manager_client, ): self._gcp_project_id = gcp_project_id self._cloud_function_region = cloud_function_region @@ -161,6 +169,7 @@ def __init__( self._bq_client = bq_client self._bq_connection_client = bq_connection_client self._bq_connection_id = bq_connection_id + self._cloud_resource_manager_client = cloud_resource_manager_client def create_bq_remote_function( self, input_args, input_types, output_type, endpoint, bq_function_name @@ -175,7 +184,8 @@ def create_bq_remote_function( # raise ValueError("Failed to enable BigQuery Connection API") # If the intended connection does not exist then create it - if self.check_bq_connection_exists(): + service_account_id = self.get_service_account_if_connection_exists() + if service_account_id: logger.info(f"Connector {self._bq_connection_id} already exists") else: connection_name, service_account_id = self.create_bq_connection() @@ -183,21 +193,9 @@ def create_bq_remote_function( f"Created BQ connection {connection_name} with service account id: {service_account_id}" ) - # Set up access on the newly created BQ connection - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function - # We would explicitly wait for 60+ seconds for the IAM binding to take effect - command_iam = ( - f"gcloud projects add-iam-policy-binding {self._gcp_project_id}" - + f' --member="serviceAccount:{service_account_id}"' - + ' --role="roles/run.invoker"' - ) - logger.info(f"Setting up IAM binding on the BQ connection: {command_iam}") - _run_system_command(command_iam) - - logger.info( - f"Waiting {self._iam_wait_seconds} seconds for IAM to take effect.." - ) - time.sleep(self._iam_wait_seconds) + # Ensure IAM role on the BQ connection + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + self._ensure_iam_binding(service_account_id, "run.invoker") # Create BQ function # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 @@ -239,6 +237,53 @@ def get_cloud_function_endpoint(self, name): pass return None + # 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'. + @google.api_core.retry.Retry( + predicate=google.api_core.retry.if_exception_type( + google.api_core.exceptions.Aborted + ), + initial=10, + maximum=20, + multiplier=2, + timeout=60, + ) + def _ensure_iam_binding(self, service_account: str, role: str): + """Ensure necessary IAM role is configured on a service account.""" + project = f"projects/{self._gcp_project_id}" + service_account = f"serviceAccount:{service_account}" + role = f"roles/{role}" + request = google.iam.v1.iam_policy_pb2.GetIamPolicyRequest(resource=project) + policy = self._cloud_resource_manager_client.get_iam_policy(request=request) + + # Check if the binding already exists, and if does, do nothing more + for binding in policy.bindings: + if binding.role == role: + if service_account in binding.members: + return + + # Create a new binding + new_binding = google.iam.v1.policy_pb2.Binding( + role=role, members=[service_account] + ) + policy.bindings.append(new_binding) + request = google.iam.v1.iam_policy_pb2.SetIamPolicyRequest( + resource=project, policy=policy + ) + self._cloud_resource_manager_client.set_iam_policy(request=request) + + # We would wait for the IAM policy change to take effect + # https://cloud.google.com/iam/docs/access-change-propagation + logger.info( + f"Waiting {self._iam_wait_seconds} seconds for IAM to take effect.." + ) + time.sleep(self._iam_wait_seconds) + def create_bq_connection(self): """Create the BigQuery Connection and returns corresponding service account id.""" client = self._bq_connection_client @@ -253,7 +298,7 @@ def create_bq_connection(self): connection = client.create_connection(request) return connection.name, connection.cloud_resource.service_account_id - def check_bq_connection_exists(self): + def get_service_account_if_connection_exists(self) -> Optional[str]: """Check if the BigQuery Connection exists.""" client = self._bq_connection_client request = bigquery_connection_v1.GetConnectionRequest( @@ -262,12 +307,15 @@ def check_bq_connection_exists(self): ) ) + service_account = None try: - client.get_connection(request=request) - return True + service_account = client.get_connection( + request=request + ).cloud_resource.service_account_id except google.api_core.exceptions.NotFound: pass - return False + + return service_account def generate_udf_code(self, def_, dir): """Generate serialized bytecode using cloudpickle given a udf.""" @@ -624,6 +672,7 @@ def remote_function( bigquery_connection_v1.ConnectionServiceClient ] = None, cloud_functions_client: Optional[functions_v2.FunctionServiceClient] = None, + resource_manager_client: Optional[resourcemanager_v3.ProjectsClient] = None, dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, reuse: bool = True, @@ -688,6 +737,11 @@ def remote_function( Client to use for BigQuery connection operations. If this param is not provided then bigquery connection client from the session would be used. + resource_manager_client (google.cloud.resourcemanager_v3.ProjectsClient, Optional): + Client to use for cloud resource management operations, e.g. for + getting and setting IAM roles on cloud resources. If this param is + not provided then resource manager client from the session would be + used. dataset (str, Optional.): Dataset in which to create a BigQuery remote function. It should be in `.` or `` format. If this @@ -734,7 +788,17 @@ def remote_function( cloud_functions_client = session.cloudfunctionsclient if not cloud_functions_client: raise ValueError( - "A functions connection client must be provided, either directly or via session. " + "A cloud functions client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A resource manager client is required to get/set IAM operations + if not resource_manager_client: + if session: + resource_manager_client = session.resourcemanagerclient + if not resource_manager_client: + raise ValueError( + "A resource manager client must be provided, either directly or via session. " f"{constants.FEEDBACK_LINK}" ) @@ -819,6 +883,7 @@ def wrapper(f): bigquery_client, bigquery_connection_client, bigquery_connection, + resource_manager_client, ) rf_name, cf_name = remote_function_client.provision_bq_remote_function( f, ibis_signature.input_types, ibis_signature.output_type, uniq_suffix diff --git a/bigframes/series.py b/bigframes/series.py index a1da93dee38..49b0a5b1f08 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -609,6 +609,39 @@ def round_op(x: ibis_types.Value, y: ibis_types.Value): return self._apply_binary_op(decimals, round_op) + def corr(self, other: Series, method="pearson", min_periods=None) -> float: + """ + Compute the correlation with the other Series. Non-number values are ignored in the + computation. + + Uses the "Pearson" method of correlation. Numbers are converted to float before + calculation, so the result may be unstable. + + Args: + other (Series): + The series with which this is to be correlated. + method (string, default "pearson"): + Correlation method to use - currently only "pearson" is supported. + min_periods (int, default None): + The minimum number of observations needed to return a result. Non-default values + are not yet supported, so a result will be returned for at least two observations. + + Returns: + float; Will return NaN if there are fewer than two numeric pairs, either series has a + variance or covariance of zero, or any input value is infinite. + """ + # TODO(kemppeterson): Validate early that both are numeric + # TODO(kemppeterson): Handle partially-numeric columns + if method != "pearson": + raise NotImplementedError( + f"Only Pearson correlation is currently supported. {constants.FEEDBACK_LINK}" + ) + if min_periods: + raise NotImplementedError( + f"min_periods not yet supported. {constants.FEEDBACK_LINK}" + ) + return self._apply_corr_aggregation(other) + def all(self) -> bool: return typing.cast(bool, self._apply_aggregation(agg_ops.all_op)) @@ -851,7 +884,7 @@ def __getitem__(self, indexer): def __getattr__(self, key: str): if hasattr(pandas.Series, key): - raise NotImplementedError( + raise AttributeError( textwrap.dedent( f""" BigQuery DataFrames has not yet implemented an equivalent to @@ -1158,6 +1191,26 @@ def to_string( def to_xarray(self): return self.to_pandas().to_xarray() + def __array_ufunc__( + self, ufunc: numpy.ufunc, method: str, *inputs, **kwargs + ) -> Series: + """Used to support numpy ufuncs. + See: https://numpy.org/doc/stable/reference/ufuncs.html + """ + # Only __call__ supported with zero arguments + if ( + inputs[0] is not self + or method != "__call__" + or len(inputs) > 1 + or len(kwargs) > 0 + ): + return NotImplemented + + if ufunc in ops.NUMPY_TO_OP: + return self._apply_unary_op(ops.NUMPY_TO_OP[ufunc]) + + return NotImplemented + # Keep this at the bottom of the Series class to avoid # confusing type checker by overriding str @property diff --git a/bigframes/session.py b/bigframes/session.py index 3ef52507460..ac2f8fa53ab 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -47,6 +47,7 @@ import google.cloud.bigquery_connection_v1 import google.cloud.bigquery_storage_v1 import google.cloud.functions_v2 +import google.cloud.resourcemanager_v3 import google.cloud.storage as storage # type: ignore import ibis import ibis.backends.bigquery as ibis_bigquery @@ -69,6 +70,7 @@ import bigframes.core.guid as guid import bigframes.core.io as bigframes_io from bigframes.core.ordering import IntegerEncoding, OrderingColumnReference +import bigframes.core.utils as utils import bigframes.dataframe as dataframe import bigframes.formatting_helpers as formatting_helpers from bigframes.remote_function import read_gbq_function as bigframes_rgf @@ -99,6 +101,16 @@ # TODO(swast): Need to connect to regional endpoints when performing remote # functions operations (BQ Connection IAM, Cloud Run / Cloud Functions). +# Also see if resource manager client library supports regional endpoints. + +_VALID_ENCODINGS = { + "UTF-8", + "ISO-8859-1", + "UTF-16BE", + "UTF-16LE", + "UTF-32BE", + "UTF-32LE", +} logger = logging.getLogger(__name__) @@ -112,90 +124,143 @@ def _get_default_credentials_with_project(): return pydata_google_auth.default(scopes=_SCOPES, use_local_webserver=False) -def _create_cloud_clients( - project: Optional[str], - location: Optional[str], - use_regional_endpoints: Optional[bool], - credentials: Optional[google.auth.credentials.Credentials], -) -> typing.Tuple[ - bigquery.Client, - google.cloud.bigquery_connection_v1.ConnectionServiceClient, - google.cloud.bigquery_storage_v1.BigQueryReadClient, - google.cloud.functions_v2.FunctionServiceClient, -]: - """Create and initialize BigQuery client objects.""" - - credentials_project = None - if credentials is None: - credentials, credentials_project = _get_default_credentials_with_project() - - # Prefer the project in this order: - # 1. Project explicitly specified by the user - # 2. Project set in the environment - # 3. Project associated with the default credentials - project = ( - project - or os.getenv(_ENV_DEFAULT_PROJECT) - or typing.cast(Optional[str], credentials_project) - ) +class ClientsProvider: + """Provides client instances necessary to perform cloud operations.""" - if not project: - raise ValueError( - "Project must be set to initialize BigQuery client. " - "Try setting `bigframes.options.bigquery.project` first." + def __init__( + self, + project: Optional[str], + location: Optional[str], + use_regional_endpoints: Optional[bool], + credentials: Optional[google.auth.credentials.Credentials], + ): + credentials_project = None + if credentials is None: + credentials, credentials_project = _get_default_credentials_with_project() + + # Prefer the project in this order: + # 1. Project explicitly specified by the user + # 2. Project set in the environment + # 3. Project associated with the default credentials + project = ( + project + or os.getenv(_ENV_DEFAULT_PROJECT) + or typing.cast(Optional[str], credentials_project) ) - if use_regional_endpoints: - bq_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERY_REGIONAL_ENDPOINT.format(location=location), - ) - bqstorage_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERYSTORAGE_REGIONAL_ENDPOINT.format(location=location) - ) - bqconnection_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERYCONNECTION_REGIONAL_ENDPOINT.format(location=location) - ) - else: - bq_options = None - bqstorage_options = None - bqconnection_options = None - - bq_info = google.api_core.client_info.ClientInfo(user_agent=_APPLICATION_NAME) - bqclient = bigquery.Client( - client_info=bq_info, - client_options=bq_options, - credentials=credentials, - project=project, - location=location, - ) + if not project: + raise ValueError( + "Project must be set to initialize BigQuery client. " + "Try setting `bigframes.options.bigquery.project` first." + ) - bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - bqconnectionclient = google.cloud.bigquery_connection_v1.ConnectionServiceClient( - client_info=bqconnection_info, - client_options=bqconnection_options, - credentials=credentials, - ) + self._project = project + self._location = location + self._use_regional_endpoints = use_regional_endpoints + self._credentials = credentials - bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - bqstorageclient = google.cloud.bigquery_storage_v1.BigQueryReadClient( - client_info=bqstorage_info, - client_options=bqstorage_options, - credentials=credentials, - ) + # cloud clients initialized for lazy load + self._bqclient = None + self._bqconnectionclient = None + self._bqstorageclient = None + self._cloudfunctionsclient = None + self._resourcemanagerclient = None - functions_info = google.api_core.gapic_v1.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - cloudfunctionsclient = google.cloud.functions_v2.FunctionServiceClient( - client_info=functions_info, - credentials=credentials, - ) + @property + def bqclient(self): + if not self._bqclient: + bq_options = None + if self._use_regional_endpoints: + bq_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERY_REGIONAL_ENDPOINT.format( + location=self._location + ), + ) + bq_info = google.api_core.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + self._bqclient = bigquery.Client( + client_info=bq_info, + client_options=bq_options, + credentials=self._credentials, + project=self._project, + location=self._location, + ) - return bqclient, bqconnectionclient, bqstorageclient, cloudfunctionsclient + return self._bqclient + + @property + def bqconnectionclient(self): + if not self._bqconnectionclient: + bqconnection_options = None + if self._use_regional_endpoints: + bqconnection_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERYCONNECTION_REGIONAL_ENDPOINT.format( + location=self._location + ) + ) + bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + self._bqconnectionclient = ( + google.cloud.bigquery_connection_v1.ConnectionServiceClient( + client_info=bqconnection_info, + client_options=bqconnection_options, + credentials=self._credentials, + ) + ) + + return self._bqconnectionclient + + @property + def bqstorageclient(self): + if not self._bqstorageclient: + bqstorage_options = None + if self._use_regional_endpoints: + bqstorage_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERYSTORAGE_REGIONAL_ENDPOINT.format( + location=self._location + ) + ) + bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + self._bqstorageclient = google.cloud.bigquery_storage_v1.BigQueryReadClient( + client_info=bqstorage_info, + client_options=bqstorage_options, + credentials=self._credentials, + ) + + return self._bqstorageclient + + @property + def cloudfunctionsclient(self): + if not self._cloudfunctionsclient: + functions_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + self._cloudfunctionsclient = ( + google.cloud.functions_v2.FunctionServiceClient( + client_info=functions_info, + credentials=self._credentials, + ) + ) + + return self._cloudfunctionsclient + + @property + def resourcemanagerclient(self): + if not self._resourcemanagerclient: + resourcemanager_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=_APPLICATION_NAME + ) + self._resourcemanagerclient = ( + google.cloud.resourcemanager_v3.ProjectsClient( + credentials=self._credentials, client_info=resourcemanager_info + ) + ) + + return self._resourcemanagerclient class Session( @@ -221,12 +286,9 @@ def __init__(self, context: Optional[bigquery_options.BigQueryOptions] = None): else: self._location = context.location - ( - self.bqclient, - self.bqconnectionclient, - self.bqstorageclient, - self.cloudfunctionsclient, - ) = _create_cloud_clients( + # Instantiate a clients provider to help with cloud clients that will be + # used in the future operations in the session + self._clients_provider = ClientsProvider( project=context.project, location=self._location, use_regional_endpoints=context.use_regional_endpoints, @@ -249,6 +311,26 @@ def __init__(self, context: Optional[bigquery_options.BigQueryOptions] = None): # changed. context._session_started = True + @property + def bqclient(self): + return self._clients_provider.bqclient + + @property + def bqconnectionclient(self): + return self._clients_provider.bqconnectionclient + + @property + def bqstorageclient(self): + return self._clients_provider.bqstorageclient + + @property + def cloudfunctionsclient(self): + return self._clients_provider.cloudfunctionsclient + + @property + def resourcemanagerclient(self): + return self._clients_provider.resourcemanagerclient + @property def _session_dataset_id(self): """A dataset for storing temporary objects local to the session @@ -343,6 +425,38 @@ def read_gbq( max_results=max_results, ) + def _query_to_destination( + self, query: str, index_cols: List[str] + ) -> Tuple[Optional[bigquery.TableReference], Optional[bigquery.QueryJob]]: + # If there are no index columns, then there's no reason to cache to a + # (clustered) session table, as we'll just have to query it again to + # create a default index & ordering. + if not index_cols: + _, query_job = self._start_query(query) + return query_job.destination, query_job + + # If a dry_run indicates this is not a query type job, then don't + # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. + dry_run_config = bigquery.QueryJobConfig() + dry_run_config.dry_run = True + _, dry_run_job = self._start_query(query, job_config=dry_run_config) + if dry_run_job.statement_type != "SELECT": + _, query_job = self._start_query(query) + return query_job.destination, query_job + + # Make sure we cluster by the index column(s) so that subsequent + # operations are as speedy as they can be. + try: + ibis_expr = self.ibis_client.sql(query) + return self._ibis_to_session_table(ibis_expr, index_cols), None + except google.api_core.exceptions.BadRequest: + # Some SELECT statements still aren't compatible with CREATE TEMP + # TABLE ... AS SELECT ... statements. For example, if the query has + # a top-level ORDER BY, this conflicts with our ability to cluster + # the table by the index column(s). + _, query_job = self._start_query(query) + return query_job.destination, query_job + def read_gbq_query( self, query: str, @@ -368,16 +482,7 @@ def read_gbq_query( else: index_cols = list(index_col) - # Make sure we cluster by the index column so that subsequent - # operations are as speedy as they can be. - if index_cols: - # Since index_cols are specified, assume that we have a normal SQL - # query. DDL or DML not supported. - ibis_expr = self.ibis_client.sql(query) - destination = self._ibis_to_session_table(ibis_expr, index_cols) - else: - _, query_job = self._start_query(query) - destination = query_job.destination + destination, query_job = self._query_to_destination(query, index_cols) # If there was no destination table, that means the query must have # been DDL or DML. Return some job metadata, instead. @@ -385,9 +490,11 @@ def read_gbq_query( return dataframe.DataFrame( data=pandas.DataFrame( { - "statement_type": [query_job.statement_type], - "job_id": [query_job.job_id], - "location": [query_job.location], + "statement_type": [ + query_job.statement_type if query_job else "unknown" + ], + "job_id": [query_job.job_id if query_job else "unknown"], + "location": [query_job.location if query_job else "unknown"], } ), session=self, @@ -551,9 +658,10 @@ def _read_gbq_with_ordering( table_expression: ibis_types.Table, *, col_order: Iterable[str] = (), - index_cols: Sequence[str] = (), - index_labels: Sequence[Optional[str]] = (), - hidden_cols: Sequence[str] = (), + col_labels: Iterable[Optional[str]] = (), + index_cols: Iterable[str] = (), + index_labels: Iterable[Optional[str]] = (), + hidden_cols: Iterable[str] = (), ordering: core.ExpressionOrdering, is_total_ordering: bool = False, ) -> dataframe.DataFrame: @@ -563,9 +671,13 @@ def _read_gbq_with_ordering( table_expression: an ibis table expression to be executed in BigQuery. col_order: - List of BigQuery column names in the desired order for results DataFrame. + List of BigQuery column ids in the desired order for results DataFrame. + col_labels: + List of column labels as the column names. index_cols: - List of column names to use as the index or multi-index. + List of index ids to use as the index or multi-index. + index_labels: + List of index labels as names of index. hidden_cols: Columns that should be hidden. Ordering columns may (not always) be hidden ordering: @@ -574,6 +686,7 @@ def _read_gbq_with_ordering( Returns: A DataFrame representing results of the query or table. """ + index_cols, index_labels = list(index_cols), list(index_labels) if len(index_cols) != len(index_labels): raise ValueError( "Needs same number of index labels are there are index columns. " @@ -597,11 +710,14 @@ def _read_gbq_with_ordering( table_expression, index_cols ) index_col_values = [table_expression[index_id] for index_id in index_cols] + if not col_labels: + col_labels = column_keys return self._read_ibis( table_expression, index_col_values, index_labels, column_keys, + col_labels, ordering=ordering, ) @@ -650,9 +766,10 @@ def _read_bigquery_load_job( def _read_ibis( self, table_expression: ibis_types.Table, - index_cols: Sequence[ibis_types.Value], - index_labels: Sequence[Optional[str]], - column_keys: Sequence[str], + index_cols: Iterable[ibis_types.Value], + index_labels: Iterable[blocks.Label], + column_keys: Iterable[str], + column_labels: Iterable[blocks.Label], ordering: core.ExpressionOrdering, ) -> dataframe.DataFrame: """Turns a table expression (plus index column) into a DataFrame.""" @@ -674,7 +791,7 @@ def _read_ibis( self, table_expression, columns, hidden_ordering_columns, ordering ), index_columns=[index_col.get_name() for index_col in index_cols], - column_labels=column_keys, + column_labels=column_labels, index_labels=index_labels, ) @@ -713,15 +830,23 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame Returns: bigframes.dataframe.DataFrame: The BigQuery DataFrame. """ + col_labels, idx_labels = ( + pandas_dataframe.columns.to_list(), + pandas_dataframe.index.names, + ) + new_col_ids, new_idx_ids = utils.get_standardized_ids(col_labels, idx_labels) + # Add order column to pandas DataFrame to preserve order in BigQuery ordering_col = "rowid" - columns = frozenset(pandas_dataframe.columns) + columns = frozenset(col_labels + idx_labels) suffix = 2 while ordering_col in columns: ordering_col = f"rowid_{suffix}" suffix += 1 pandas_dataframe_copy = pandas_dataframe.copy() + pandas_dataframe_copy.index.names = new_idx_ids + pandas_dataframe_copy.columns = pandas.Index(new_col_ids) pandas_dataframe_copy[ordering_col] = np.arange(pandas_dataframe_copy.shape[0]) # Specify the datetime dtypes, which is auto-detected as timestamp types. @@ -732,27 +857,12 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame bigquery.SchemaField(column, bigquery.enums.SqlTypeNames.DATETIME) ) - # Unnamed are not copied to BigQuery when load_table_from_dataframe - # executes. - index_cols = list( - filter(lambda name: name is not None, pandas_dataframe_copy.index.names) - ) - index_labels = typing.cast(List[Optional[str]], index_cols) - # Clustering probably not needed anyways as pandas tables are small cluster_cols = [ordering_col] - if len(index_cols) == 0: - # Block constructor will implicitly build default index - pass - job_config = bigquery.LoadJobConfig(schema=schema) job_config.clustering_fields = cluster_cols - # TODO(swast): Rename the unnamed index columns and restore them after - # the load job completes. - # Column values will be loaded as null if the column name has spaces. - # https://github.com/googleapis/python-bigquery/issues/1566 load_table_destination = self._create_session_table() load_job = self.bqclient.load_table_from_dataframe( pandas_dataframe_copy, @@ -770,14 +880,22 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame f"SELECT * FROM `{load_table_destination.table_id}`" ) - return self._read_gbq_with_ordering( + # b/297590178 Potentially a bug in bqclient.load_table_from_dataframe(), that only when the DF is empty, the index columns disappear in table_expression. + if any( + [new_idx_id not in table_expression.columns for new_idx_id in new_idx_ids] + ): + new_idx_ids, idx_labels = [], [] + + df = self._read_gbq_with_ordering( table_expression=table_expression, - index_cols=index_cols, - index_labels=index_labels, + col_labels=col_labels, + index_cols=new_idx_ids, + index_labels=idx_labels, hidden_cols=(ordering_col,), ordering=ordering, is_total_ordering=True, ) + return df def read_csv( self, @@ -844,10 +962,9 @@ def read_csv( f"{constants.FEEDBACK_LINK}" ) - valid_encodings = {"UTF-8", "ISO-8859-1"} - if encoding is not None and encoding not in valid_encodings: + if encoding is not None and encoding not in _VALID_ENCODINGS: raise NotImplementedError( - f"BigQuery engine only supports the following encodings: {valid_encodings}. " + f"BigQuery engine only supports the following encodings: {_VALID_ENCODINGS}. " f"{constants.FEEDBACK_LINK}" ) @@ -933,6 +1050,86 @@ def read_parquet( return self._read_bigquery_load_job(path, table, job_config=job_config) + def read_json( + self, + path_or_buf: str | IO["bytes"], + *, + orient: Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + dtype: Optional[Dict] = None, + encoding: Optional[str] = None, + lines: bool = False, + engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", + **kwargs, + ) -> dataframe.DataFrame: + table = bigquery.Table(self._create_session_table()) + + if engine == "bigquery": + + if dtype is not None: + raise NotImplementedError( + "BigQuery engine does not support the dtype arguments." + ) + + if not lines: + raise NotImplementedError( + "Only newline delimited JSON format is supported." + ) + + if encoding is not None and encoding not in _VALID_ENCODINGS: + raise NotImplementedError( + f"BigQuery engine only supports the following encodings: {_VALID_ENCODINGS}" + ) + + if lines and orient != "records": + raise ValueError( + "'lines' keyword is only valid when 'orient' is 'records'." + ) + + job_config = bigquery.LoadJobConfig() + job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED + job_config.source_format = bigquery.SourceFormat.NEWLINE_DELIMITED_JSON + job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY + job_config.autodetect = True + job_config.encoding = encoding + + return self._read_bigquery_load_job( + path_or_buf, + table, + job_config=job_config, + ) + else: + if any(arg in kwargs for arg in ("chunksize", "iterator")): + raise NotImplementedError( + "'chunksize' and 'iterator' arguments are not supported." + ) + + if isinstance(path_or_buf, str): + self._check_file_size(path_or_buf) + + if engine == "ujson": + pandas_df = pandas.read_json( # type: ignore + path_or_buf, + orient=orient, + dtype=dtype, + encoding=encoding, + lines=lines, + **kwargs, + ) + + else: + pandas_df = pandas.read_json( # type: ignore + path_or_buf, + orient=orient, + dtype=dtype, + encoding=encoding, + lines=lines, + engine=engine, + **kwargs, + ) + return self.read_pandas(pandas_df) + def _check_file_size(self, filepath: str): max_size = 1024 * 1024 * 1024 # 1 GB in bytes if filepath.startswith("gs://"): # GCS file path @@ -1008,14 +1205,26 @@ def _query_to_session_table( table = self._create_session_table() cluster_cols_sql = ", ".join(f"`{cluster_col}`" for cluster_col in cluster_cols) - # TODO(swast): This might not support multi-statement SQL queries. + # TODO(swast): This might not support multi-statement SQL queries (scripts). ddl_text = f""" CREATE TEMP TABLE `_SESSION`.`{table.table_id}` CLUSTER BY {cluster_cols_sql} AS {query_text} """ + + job_config = bigquery.QueryJobConfig() + + # Include a label so that Dataplex Lineage can identify temporary + # tables that BigQuery DataFrames creates. Googlers: See internal issue + # 296779699. We're labeling the job instead of the table because + # otherwise we get `BadRequest: 400 OPTIONS on temporary tables are not + # supported`. + job_config.labels = {"source": "bigquery-dataframes-temp"} + try: - self._start_query(ddl_text) # Wait for the job to complete + self._start_query( + ddl_text, job_config=job_config + ) # Wait for the job to complete except google.api_core.exceptions.Conflict: # Allow query retry to succeed. pass diff --git a/mypy.ini b/mypy.ini index ce78c4686ed..901394813aa 100644 --- a/mypy.ini +++ b/mypy.ini @@ -15,6 +15,9 @@ ignore_missing_imports = True [mypy-google.colab] ignore_missing_imports = True +[mypy-google.iam.*] +ignore_missing_imports = True + [mypy-pytz] ignore_missing_imports = True diff --git a/notebooks/dataframes/dataframe.ipynb b/notebooks/dataframes/dataframe.ipynb index 241c767f572..85ea61d2810 100644 --- a/notebooks/dataframes/dataframe.ipynb +++ b/notebooks/dataframes/dataframe.ipynb @@ -31,7 +31,22 @@ "execution_count": 2, "id": "96757c59-fc22-420e-a42f-c6cb956110ec", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "944f0e4417154e81b6496302fe756465", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job ac4d1f2b-e9f3-4d95-b78d-57e40eee93fa is RUNNING. Cubs\n", " 175\n", " \n", + " \n", + " 20\n", + " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " 2016\n", + " Nationals\n", + " Cubs\n", + " 257\n", + " \n", + " \n", + " 21\n", + " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " 2016\n", + " Brewers\n", + " Cubs\n", + " 178\n", + " \n", + " \n", + " 22\n", + " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " 2016\n", + " Brewers\n", + " Cubs\n", + " 171\n", + " \n", + " \n", + " 23\n", + " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 2016\n", + " Brewers\n", + " Cubs\n", + " 248\n", + " \n", + " \n", + " 24\n", + " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 2016\n", + " Astros\n", + " Cubs\n", + " 174\n", + " \n", " \n", "\n", + "

25 rows × 5 columns

\n", "[2431 rows x 5 columns in total]" ], "text/plain": [ @@ -419,6 +503,34 @@ "tags": [] }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "276760df4c904ced81cbaff3a65d026e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 1943ae42-bcbd-4c2f-914f-209377b5c4d9 is DONE. 0 Bytes processed.
Cubs\n", " 175\n", " \n", + " \n", + " 20\n", + " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " 2016\n", + " Nationals\n", + " Cubs\n", + " 257\n", + " \n", + " \n", + " 21\n", + " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " 2016\n", + " Brewers\n", + " Cubs\n", + " 178\n", + " \n", + " \n", + " 22\n", + " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " 2016\n", + " Brewers\n", + " Cubs\n", + " 171\n", + " \n", + " \n", + " 23\n", + " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 2016\n", + " Brewers\n", + " Cubs\n", + " 248\n", + " \n", + " \n", + " 24\n", + " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 2016\n", + " Astros\n", + " Cubs\n", + " 174\n", + " \n", " \n", "\n", + "

25 rows × 5 columns

\n", "[2431 rows x 5 columns in total]" ], "text/plain": [ @@ -3063,6 +3768,34 @@ "id": "ac3ceabe-4317-453c-9418-826de5094454", "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c73064d64afe41cea6738085b273e29d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 08477df8-3e86-4f94-a905-8ac2f36e2b69 is DONE. 0 Bytes processed.
core.BqmlModel: +def penguins_bqml_kmeans_model( + session: bigframes.Session, penguins_kmeans_model_name: str +) -> core.BqmlModel: model = session.bqclient.get_model(penguins_kmeans_model_name) return core.BqmlModel(session, model) +@pytest.fixture(scope="session") +def penguins_bqml_pca_model( + session: bigframes.Session, penguins_pca_model_name: str +) -> core.BqmlModel: + model = session.bqclient.get_model(penguins_pca_model_name) + return core.BqmlModel(session, model) + + @pytest.fixture(scope="session") def penguins_linear_model( session, penguins_linear_model_name: str @@ -140,32 +147,12 @@ def penguins_kmeans_model(session, penguins_kmeans_model_name: str) -> cluster.K @pytest.fixture(scope="session") def penguins_pca_model( - session: bigframes.Session, dataset_id_permanent, penguins_table_id + session: bigframes.Session, penguins_pca_model_name: str ) -> decomposition.PCA: - - # TODO(yunmengxie): Create a shared method to get different types of pretrained models. - sql = f""" -CREATE OR REPLACE MODEL `$model_name` -OPTIONS ( - model_type='pca', - num_principal_components=3 -) AS SELECT - * -FROM `{penguins_table_id}`""" - # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited - model_name = ( - f"{dataset_id_permanent}.penguins_pca_{hashlib.md5(sql.encode()).hexdigest()}" + return cast( + decomposition.PCA, + session.read_gbq_model(penguins_pca_model_name), ) - sql = sql.replace("$model_name", model_name) - - try: - return session.read_gbq_model(model_name) - except google.cloud.exceptions.NotFound: - logging.info( - "penguins_pca_model fixture was not found in the permanent dataset, regenerating it..." - ) - session.bqclient.query(sql).result() - return session.read_gbq_model(model_name) @pytest.fixture(scope="session") diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 4b184b0d4c8..6c3e8e06f5b 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -140,6 +140,100 @@ def test_model_centroids(penguins_bqml_kmeans_model: core.BqmlModel): ) +def test_pca_model_principal_components(penguins_bqml_pca_model: core.BqmlModel): + result = penguins_bqml_pca_model.principal_components().to_pandas() + assert result.shape == (21, 4) + + # result is too long, only check the first principal component here. + result = result.head(7) + expected = pd.DataFrame( + { + "principal_component_id": [0] * 7, + "feature": [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + "sex", + ], + "numerical_value": [ + pd.NA, + pd.NA, + 0.401489, + -0.377482, + 0.524052, + 0.501174, + pd.NA, + ], + "categorical_value": [ + [ + { + "category": "Gentoo penguin (Pygoscelis papua)", + "value": 0.25068877125667804, + }, + { + "category": "Adelie Penguin (Pygoscelis adeliae)", + "value": -0.20622291900416198, + }, + { + "category": "Chinstrap penguin (Pygoscelis antarctica)", + "value": -0.030161149275185855, + }, + ], + [ + {"category": "Biscoe", "value": 0.19761120114410635}, + {"category": "Dream", "value": -0.11264736305259061}, + {"category": "Torgersen", "value": -0.07065913511418596}, + ], + [], + [], + [], + [], + [ + {"category": ".", "value": 0.0015916894448071784}, + {"category": "MALE", "value": 0.06869704739750442}, + {"category": "FEMALE", "value": -0.052521171596813174}, + {"category": "_null_filler", "value": -0.0034628622681684906}, + ], + ], + }, + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + check_dtype=False, + ) + + +def test_pca_model_principal_component_info(penguins_bqml_pca_model: core.BqmlModel): + result = penguins_bqml_pca_model.principal_component_info().to_pandas() + assert result.shape == (3, 4) + + expected = pd.DataFrame( + { + "principal_component_id": [0, 1, 2], + "eigenvalue": [3.278657, 1.270829, 1.125354], + "explained_variance_ratio": [0.469357, 0.181926, 0.1611], + "cumulative_explained_variance_ratio": [0.469357, 0.651283, 0.812383], + }, + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame + check_index_type=False, + check_dtype=False, + ) + + def test_model_predict(penguins_bqml_linear_model: core.BqmlModel, new_penguins_df): predictions = penguins_bqml_linear_model.predict(new_penguins_df).to_pandas() expected = pd.DataFrame( diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index 01d52077505..8df4145fcf0 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -55,7 +55,7 @@ def test_pca_predict(session, penguins_pca_model: decomposition.PCA): ) -def test_pca_score(session, penguins_pca_model: decomposition.PCA): +def test_pca_score(penguins_pca_model: decomposition.PCA): result = penguins_pca_model.score().to_pandas() expected = pd.DataFrame( {"total_explained_variance_ratio": [0.812383]}, @@ -68,3 +68,110 @@ def test_pca_score(session, penguins_pca_model: decomposition.PCA): rtol=0.1, check_index_type=False, ) + + +def test_pca_components_(penguins_pca_model: decomposition.PCA): + result = penguins_pca_model.components_.to_pandas() + + # result is too long, only check the first principal component here. + result = result.head(7) + expected = pd.DataFrame( + { + "principal_component_id": [0] * 7, + "feature": [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + "sex", + ], + "numerical_value": [ + pd.NA, + pd.NA, + 0.401489, + -0.377482, + 0.524052, + 0.501174, + pd.NA, + ], + "categorical_value": [ + [ + { + "category": "Gentoo penguin (Pygoscelis papua)", + "value": 0.25068877125667804, + }, + { + "category": "Adelie Penguin (Pygoscelis adeliae)", + "value": -0.20622291900416198, + }, + { + "category": "Chinstrap penguin (Pygoscelis antarctica)", + "value": -0.030161149275185855, + }, + ], + [ + {"category": "Biscoe", "value": 0.19761120114410635}, + {"category": "Dream", "value": -0.11264736305259061}, + {"category": "Torgersen", "value": -0.07065913511418596}, + ], + [], + [], + [], + [], + [ + {"category": ".", "value": 0.0015916894448071784}, + {"category": "MALE", "value": 0.06869704739750442}, + {"category": "FEMALE", "value": -0.052521171596813174}, + {"category": "_null_filler", "value": -0.0034628622681684906}, + ], + ], + }, + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + check_dtype=False, + ) + + +def test_pca_explained_variance_(penguins_pca_model: decomposition.PCA): + result = penguins_pca_model.explained_variance_.to_pandas() + + expected = pd.DataFrame( + { + "principal_component_id": [0, 1, 2], + "explained_variance": [3.278657, 1.270829, 1.125354], + }, + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + check_dtype=False, + ) + + +def test_pca_explained_variance_ratio_(penguins_pca_model: decomposition.PCA): + result = penguins_pca_model.explained_variance_ratio_.to_pandas() + + expected = pd.DataFrame( + { + "principal_component_id": [0, 1, 2], + "explained_variance_ratio": [0.469357, 0.181926, 0.1611], + }, + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + rtol=0.1, + check_index_type=False, + check_dtype=False, + ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 5b4f9ebcccf..01305adb204 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -77,6 +77,20 @@ def test_df_construct_from_series(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_df_construct_from_dict(): + input_dict = { + "Animal": ["Falcon", "Falcon", "Parrot", "Parrot"], + # With a space in column name. We use standardized SQL schema ids to solve the problem that BQ schema doesn't support column names with spaces. b/296751058 + "Max Speed": [380.0, 370.0, 24.0, 26.0], + } + bf_result = dataframe.DataFrame(input_dict).to_pandas() + pd_result = pd.DataFrame(input_dict) + + pandas.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + def test_get_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" @@ -356,6 +370,52 @@ def test_assign_new_column_w_setitem(scalars_dfs): pd.testing.assert_frame_equal(bf_result, pd_result) +def test_assign_new_column_w_setitem_list(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] + pd_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] + bf_result = bf_df.to_pandas() + pd_result = pd_df + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_assign_new_column_w_setitem_list_custom_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + + # set the custom index + pd_df = pd_df.set_index("string_col") + bf_df = bf_df.set_index("string_col") + + bf_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] + pd_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] + bf_result = bf_df.to_pandas() + pd_result = pd_df + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_assign_new_column_w_setitem_list_error(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + + with pytest.raises(ValueError): + pd_df["new_col"] = [1, 2, 3] # should be len 9, is 3 + with pytest.raises(ValueError): + bf_df["new_col"] = [1, 2, 3] + + def test_assign_existing_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs kwargs = {"int64_col": 2} @@ -1329,6 +1389,21 @@ def test_df_describe(scalars_dfs): ).all() +def test_df_stack(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + # To match bigquery dataframes + scalars_pandas_df = scalars_pandas_df.copy() + scalars_pandas_df.columns = scalars_pandas_df.columns.astype("string[pyarrow]") + # Can only stack identically-typed columns + columns = ["int64_col", "int64_too", "rowindex_2"] + + bf_result = scalars_df[columns].stack().to_pandas() + pd_result = scalars_pandas_df[columns].stack() + + # Pandas produces NaN, where bq dataframes produces pd.NA + pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + + @pytest.mark.parametrize( ("values", "index", "columns"), [ @@ -1734,8 +1809,9 @@ def test_df___array__(scalars_df_index, scalars_pandas_df_index): ) -def test_getattr_not_implemented(scalars_df_index): - with pytest.raises(NotImplementedError): +def test_getattr_attribute_error_when_pandas_has(scalars_df_index): + # asof is implemented in pandas but not in bigframes + with pytest.raises(AttributeError): scalars_df_index.asof() diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 1f5aa906c8c..3886b85f40c 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -32,6 +32,7 @@ import bigframes import bigframes.dataframe +import bigframes.pandas as bpd def test_to_pandas_w_correct_dtypes(scalars_df_default_index): @@ -339,51 +340,68 @@ def test_to_parquet_index(scalars_dfs, gcs_folder, index): pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) +def test_to_sql_query_unnamed_index_included( + session: bigframes.Session, + scalars_df_default_index: bpd.DataFrame, + scalars_pandas_df_default_index: pd.DataFrame, +): + bf_df = scalars_df_default_index.reset_index(drop=True) + sql, idx_ids, idx_labels = bf_df._to_sql_query(include_index=True) + assert len(idx_labels) == 1 + assert len(idx_ids) == 1 + assert idx_labels[0] is None + assert idx_ids[0].startswith("bigframes") + + pd_df = scalars_pandas_df_default_index.reset_index(drop=True) + roundtrip = session.read_gbq(sql, index_col=idx_ids) + roundtrip.index.names = [None] + assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) + + def test_to_sql_query_named_index_included( - session, scalars_df_index, scalars_pandas_df_index + session: bigframes.Session, + scalars_df_default_index: bpd.DataFrame, + scalars_pandas_df_default_index: pd.DataFrame, ): - sql, index_columns = scalars_df_index._to_sql_query(always_include_index=True) - assert len(index_columns) == 1 - index_column, is_named = index_columns[0] - assert index_column == "rowindex" - assert is_named - - roundtrip = session.read_gbq(sql, index_col=[index_column]) - assert_pandas_df_equal_ignore_ordering( - roundtrip.to_pandas(), scalars_pandas_df_index - ) + bf_df = scalars_df_default_index.set_index("rowindex_2", drop=True) + sql, idx_ids, idx_labels = bf_df._to_sql_query(include_index=True) + assert len(idx_labels) == 1 + assert len(idx_ids) == 1 + assert idx_labels[0] == "rowindex_2" + assert idx_ids[0] == "rowindex_2" + + pd_df = scalars_pandas_df_default_index.set_index("rowindex_2", drop=True) + roundtrip = session.read_gbq(sql, index_col=idx_ids) + assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) def test_to_sql_query_unnamed_index_excluded( - session, scalars_df_default_index, scalars_pandas_df_default_index + session: bigframes.Session, + scalars_df_default_index: bpd.DataFrame, + scalars_pandas_df_default_index: pd.DataFrame, ): - # The .sql property should return SQL without the unnamed indexes - sql, index_columns = scalars_df_default_index._to_sql_query( - always_include_index=False - ) - assert len(index_columns) == 0 + bf_df = scalars_df_default_index.reset_index(drop=True) + sql, idx_ids, idx_labels = bf_df._to_sql_query(include_index=False) + assert len(idx_labels) == 0 + assert len(idx_ids) == 0 + pd_df = scalars_pandas_df_default_index.reset_index(drop=True) roundtrip = session.read_gbq(sql) - assert_pandas_df_equal_ignore_ordering( - roundtrip.to_pandas(), scalars_pandas_df_default_index - ) + assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) -def test_to_sql_query_unnamed_index_always_include( - session, - scalars_df_default_index: bigframes.dataframe.DataFrame, - scalars_pandas_df_default_index, +def test_to_sql_query_named_index_excluded( + session: bigframes.Session, + scalars_df_default_index: bpd.DataFrame, + scalars_pandas_df_default_index: pd.DataFrame, ): - sql, index_columns = scalars_df_default_index._to_sql_query( - always_include_index=True - ) - assert len(index_columns) == 1 - index_column, is_named = index_columns[0] - assert index_column == "bigframes_index_0" - assert not is_named - - roundtrip = session.read_gbq(sql, index_col=[index_column]) - roundtrip.index.name = None - assert_pandas_df_equal_ignore_ordering( - roundtrip.to_pandas(), scalars_pandas_df_default_index - ) + bf_df = scalars_df_default_index.set_index("rowindex_2", drop=True) + sql, idx_ids, idx_labels = bf_df._to_sql_query(include_index=False) + assert len(idx_labels) == 0 + assert len(idx_ids) == 0 + + pd_df = scalars_pandas_df_default_index.set_index( + "rowindex_2", drop=True + ).reset_index(drop=True) + roundtrip = session.read_gbq(sql) + assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 5a2562bfb20..987368ce771 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -112,7 +112,9 @@ def test_dataframe_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) -def test_dataframe_groupby_agg_dict(scalars_df_index, scalars_pandas_df_index): +def test_dataframe_groupby_agg_dict_with_list( + scalars_df_index, scalars_pandas_df_index +): col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = ( scalars_df_index[col_names] @@ -129,6 +131,23 @@ def test_dataframe_groupby_agg_dict(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +def test_dataframe_groupby_agg_dict_no_lists(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = ( + scalars_df_index[col_names] + .groupby("string_col") + .agg({"int64_too": "mean", "string_col": "count"}) + ) + pd_result = ( + scalars_pandas_df_index[col_names] + .groupby("string_col") + .agg({"int64_too": "mean", "string_col": "count"}) + ) + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + def test_dataframe_groupby_agg_named(scalars_df_index, scalars_pandas_df_index): col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = ( diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index b2937d7da9c..1baf3e66508 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -620,3 +620,39 @@ def test_column_multi_index_cumsum(scalars_df_index, scalars_pandas_df_index): pd_result = pd_df.cumsum() pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +def test_column_multi_index_stack(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "rowindex_2"] + level1 = pandas.Index(["b", "a", "b"]) + # Need resulting column to be pyarrow string rather than object dtype + level2 = pandas.Index(["a", "b", "b"], dtype="string[pyarrow]") + multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.stack().to_pandas() + pd_result = pd_df.stack() + + # Pandas produces NaN, where bq dataframes produces pd.NA + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "rowindex_2"] + level1 = pandas.Index(["b", pandas.NA, pandas.NA]) + # Need resulting column to be pyarrow string rather than object dtype + level2 = pandas.Index([pandas.NA, "b", "b"], dtype="string[pyarrow]") + multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.stack().to_pandas() + pd_result = pd_df.stack() + + # Pandas produces NaN, where bq dataframes produces pd.NA + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) diff --git a/tests/system/small/test_numpy.py b/tests/system/small/test_numpy.py new file mode 100644 index 00000000000..fff689cabac --- /dev/null +++ b/tests/system/small/test_numpy.py @@ -0,0 +1,69 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd +import pytest + + +@pytest.mark.parametrize( + ("opname",), + [ + ("sin",), + ("cos",), + ("tan",), + ("arcsin",), + ("arccos",), + ("arctan",), + ("sinh",), + ("cosh",), + ("tanh",), + ("arcsinh",), + ("arccosh",), + ("arctanh",), + ("exp",), + ("log",), + ("log10",), + ("sqrt",), + ("abs",), + ], +) +def test_series_ufuncs(floats_pd, floats_bf, opname): + bf_result = getattr(np, opname)(floats_bf).to_pandas() + pd_result = getattr(np, opname)(floats_pd) + + pd.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("opname",), + [ + ("sin",), + ("cos",), + ("tan",), + ("log",), + ("log10",), + ("sqrt",), + ("abs",), + ], +) +def test_df_ufuncs(scalars_dfs, opname): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = getattr(np, opname)( + scalars_df[["float64_col", "int64_col"]] + ).to_pandas() + pd_result = getattr(np, opname)(scalars_pandas_df[["float64_col", "int64_col"]]) + + pd.testing.assert_frame_equal(bf_result, pd_result) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index e40addc4ebd..c60d270fcaa 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -116,6 +116,7 @@ def test_remote_function_direct_no_session_param( bigquery_client, bigqueryconnection_client, cloudfunctions_client, + resourcemanager_client, scalars_dfs, dataset_id_permanent, bq_cf_connection, @@ -126,6 +127,7 @@ def test_remote_function_direct_no_session_param( bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, cloud_functions_client=cloudfunctions_client, + resource_manager_client=resourcemanager_client, dataset=dataset_id_permanent, bigquery_connection=bq_cf_connection, # See e2e tests for tests that actually deploy the Cloud Function. @@ -166,6 +168,7 @@ def test_remote_function_direct_no_session_param_location_specified( bigquery_client, bigqueryconnection_client, cloudfunctions_client, + resourcemanager_client, scalars_dfs, dataset_id_permanent, bq_cf_connection_location, @@ -176,6 +179,7 @@ def test_remote_function_direct_no_session_param_location_specified( bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, cloud_functions_client=cloudfunctions_client, + resource_manager_client=resourcemanager_client, dataset=dataset_id_permanent, bigquery_connection=bq_cf_connection_location, # See e2e tests for tests that actually deploy the Cloud Function. @@ -213,6 +217,7 @@ def test_remote_function_direct_no_session_param_location_mismatched( bigquery_client, bigqueryconnection_client, cloudfunctions_client, + resourcemanager_client, dataset_id_permanent, bq_cf_connection_location_mismatched, ): @@ -224,6 +229,7 @@ def test_remote_function_direct_no_session_param_location_mismatched( bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, cloud_functions_client=cloudfunctions_client, + resource_manager_client=resourcemanager_client, dataset=dataset_id_permanent, bigquery_connection=bq_cf_connection_location_mismatched, # See e2e tests for tests that actually deploy the Cloud Function. @@ -238,6 +244,7 @@ def test_remote_function_direct_no_session_param_location_project_specified( bigquery_client, bigqueryconnection_client, cloudfunctions_client, + resourcemanager_client, scalars_dfs, dataset_id_permanent, bq_cf_connection_location_project, @@ -248,6 +255,7 @@ def test_remote_function_direct_no_session_param_location_project_specified( bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, cloud_functions_client=cloudfunctions_client, + resource_manager_client=resourcemanager_client, dataset=dataset_id_permanent, bigquery_connection=bq_cf_connection_location_project, # See e2e tests for tests that actually deploy the Cloud Function. @@ -285,6 +293,7 @@ def test_remote_function_direct_no_session_param_project_mismatched( bigquery_client, bigqueryconnection_client, cloudfunctions_client, + resourcemanager_client, dataset_id_permanent, bq_cf_connection_location_project_mismatched, ): @@ -296,6 +305,7 @@ def test_remote_function_direct_no_session_param_project_mismatched( bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, cloud_functions_client=cloudfunctions_client, + resource_manager_client=resourcemanager_client, dataset=dataset_id_permanent, bigquery_connection=bq_cf_connection_location_project_mismatched, # See e2e tests for tests that actually deploy the Cloud Function. @@ -530,6 +540,7 @@ def test_read_gbq_function_like_original( bigquery_client, bigqueryconnection_client, cloudfunctions_client, + resourcemanager_client, scalars_df_index, dataset_id_permanent, bq_cf_connection, @@ -541,6 +552,7 @@ def test_read_gbq_function_like_original( bigquery_connection_client=bigqueryconnection_client, dataset=dataset_id_permanent, cloud_functions_client=cloudfunctions_client, + resource_manager_client=resourcemanager_client, bigquery_connection=bq_cf_connection, reuse=True, ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 70c56e5e137..88ad2245c9b 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -459,6 +459,19 @@ def test_mods(scalars_dfs, col_x, col_y, method): pd.testing.assert_series_equal(pd_result, bf_result) +# We work around a pandas bug that doesn't handle correlating nullable dtypes by doing this +# manually with dumb self-correlation instead of parameterized as test_mods is above. +def test_corr(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["int64_too"].corr(scalars_df["int64_too"]) + pd_result = ( + scalars_pandas_df["int64_too"] + .astype("int64") + .corr(scalars_pandas_df["int64_too"].astype("int64")) + ) + assert math.isclose(pd_result, bf_result) + + @pytest.mark.parametrize( ("col_x",), [ @@ -900,7 +913,7 @@ def test_binop_repeated_application_does_row_identity_joins(scalars_dfs): pd_result, ) - bf_sql, _ = bf_series.to_frame()._to_sql_query(always_include_index=True) + bf_sql, _, _ = bf_series.to_frame()._to_sql_query(include_index=True) selects = re.findall("SELECT", bf_sql.upper()) assert 0 < len(selects) < (num_joins // 2) @@ -2222,8 +2235,9 @@ def test_argmax(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result -def test_getattr_not_implemented(scalars_df_index): - with pytest.raises(NotImplementedError): +def test_getattr_attribute_error_when_pandas_has(scalars_df_index): + # asof is implemented in pandas but not in bigframes + with pytest.raises(AttributeError): scalars_df_index.string_col.asof() diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index d825c625617..b7bee16ffd4 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -111,6 +111,54 @@ def test_read_gbq_w_col_order( ["uuid"], id="unique_uuid_index_query", ), + pytest.param( + """ + SELECT my_index, my_value + FROM UNNEST( + [ + STRUCT(0, 12), + STRUCT(1, 12), + STRUCT(2, 24) + ] + ) + -- Can't normally cluster tables with ORDER BY clause. + ORDER BY my_index DESC + """, + ["my_index"], + id="unique_index_query_has_order_by", + ), + pytest.param( + """ + WITH my_table AS ( + SELECT * + FROM UNNEST( + [ + STRUCT(0, 12), + STRUCT(1, 12), + STRUCT(2, 24) + ] + ) + ) + SELECT my_index, my_value FROM my_table + """, + ["my_index"], + id="unique_index_query_with_named_table_expression", + ), + pytest.param( + """ + CREATE TEMP TABLE test_read_gbq_w_index_col_unique_index_query_with_script + AS SELECT * FROM UNNEST( + [ + STRUCT(0, 12), + STRUCT(1, 12), + STRUCT(2, 24) + ] + ); + SELECT my_index, my_value FROM test_read_gbq_w_index_col_unique_index_query_with_script + """, + ["my_index"], + id="unique_index_query_with_script", + ), pytest.param( "{scalars_table_id}", ["bool_col"], @@ -221,7 +269,7 @@ def test_read_gbq_w_max_results( assert bf_result.shape[0] == max_results -def test_read_gbq_w_script(session, dataset_id: str): +def test_read_gbq_w_script_no_select(session, dataset_id: str): ddl = f""" CREATE TABLE `{dataset_id}.test_read_gbq_w_ddl` ( `col_a` INT64, @@ -252,6 +300,20 @@ def test_read_pandas(session, scalars_dfs): pd.testing.assert_frame_equal(result, expected) +def test_read_pandas_col_label_w_space(session: bigframes.Session): + expected = pd.DataFrame( + { + "Animal": ["Falcon", "Falcon", "Parrot", "Parrot"], + "Max Speed": [380.0, 370.0, 24.0, 26.0], + } + ) + result = session.read_pandas(expected).to_pandas() + + pd.testing.assert_frame_equal( + result, expected, check_index_type=False, check_dtype=False + ) + + def test_read_pandas_multi_index(session, scalars_pandas_df_multi_index): df = session.read_pandas(scalars_pandas_df_multi_index) result = df.to_pandas() @@ -755,6 +817,67 @@ def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): pd.testing.assert_frame_equal(pd_df_in, pd_df_out) +def test_read_json_gcs_bq_engine(session, scalars_dfs, gcs_folder): + scalars_df, _ = scalars_dfs + path = gcs_folder + "test_read_json_gcs_bq_engine_w_index*.json" + read_path = path.replace("*", FIRST_FILE) + scalars_df.to_json(path, index=False, lines=True, orient="records") + df = session.read_json(read_path, lines=True, orient="records", engine="bigquery") + + # The auto detects of BigQuery load job does not preserve any ordering of columns for json. + pd.testing.assert_index_equal( + df.columns.sort_values(), scalars_df.columns.sort_values() + ) + + # The auto detects of BigQuery load job have restrictions to detect the bytes, + # datetime, numeric and geometry types, so they're skipped here. + df = df.drop(columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"]) + scalars_df = scalars_df.drop( + columns=["bytes_col", "datetime_col", "numeric_col", "geography_col"] + ) + assert df.shape[0] == scalars_df.shape[0] + pd.testing.assert_series_equal( + df.dtypes.sort_index(), scalars_df.dtypes.sort_index() + ) + + +def test_read_json_gcs_default_engine(session, scalars_dfs, gcs_folder): + scalars_df, _ = scalars_dfs + path = gcs_folder + "test_read_json_gcs_default_engine_w_index*.json" + read_path = path.replace("*", FIRST_FILE) + scalars_df.to_json( + path, + index=False, + lines=True, + orient="records", + ) + dtype = scalars_df.dtypes.to_dict() + dtype.pop("geography_col") + + df = session.read_json( + read_path, + # Convert default pandas dtypes to match BigQuery DataFrames dtypes. + dtype=dtype, + lines=True, + orient="records", + ) + + assert df._block._expr._ordering is not None + pd.testing.assert_index_equal(df.columns, scalars_df.columns) + + # The auto detects of BigQuery load job have restrictions to detect the bytes, + # numeric and geometry types, so they're skipped here. + df = df.drop(columns=["bytes_col", "numeric_col", "geography_col"]) + scalars_df = scalars_df.drop(columns=["bytes_col", "numeric_col", "geography_col"]) + + # pandas read_json does not respect the dtype overrides for these columns + df = df.drop(columns=["date_col", "datetime_col", "time_col"]) + scalars_df = scalars_df.drop(columns=["date_col", "datetime_col", "time_col"]) + + assert df.shape[0] == scalars_df.shape[0] + pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) + + def test_session_id(session): assert session._session_id is not None diff --git a/tests/unit/core/test_utils.py b/tests/unit/core/test_utils.py new file mode 100644 index 00000000000..fc34f35d9c2 --- /dev/null +++ b/tests/unit/core/test_utils.py @@ -0,0 +1,56 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes.core import utils + + +def test_get_standardized_ids_columns(): + col_labels = ["string", 0, None, "duplicate", "duplicate", "with space"] + + col_ids, idx_ids = utils.get_standardized_ids(col_labels) + + assert col_ids == [ + "string", + "0", + utils.UNNAMED_COLUMN_ID, + "duplicate", + "duplicate.1", + "with_space", + ] + assert idx_ids == [] + + +def test_get_standardized_ids_indexes(): + col_labels = ["duplicate"] + idx_labels = ["string", 0, None, "duplicate", "duplicate", "with space"] + + col_ids, idx_ids = utils.get_standardized_ids(col_labels, idx_labels) + + assert col_ids == ["duplicate.2"] + assert idx_ids == [ + "string", + "0", + utils.UNNAMED_INDEX_ID, + "duplicate", + "duplicate.1", + "with_space", + ] + + +def test_get_standardized_ids_tuple(): + col_labels = [("foo", 1), ("foo", 2), ("bar", 1)] + + col_ids, _ = utils.get_standardized_ids(col_labels) + + assert col_ids == ["('foo',_1)", "('foo',_2)", "('bar',_1)"] diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index d8c8a2d108e..c20a17f7d6e 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -162,3 +162,18 @@ def test_ml_generate_text_produces_correct_sql(): == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_dataset.my_model`, (SELECT * FROM my_table), STRUCT(value AS item))""" ) + + +def test_ml_principal_components_produces_correct_sql(): + sql = ml_sql.ml_principal_components(model_name="my_dataset.my_model") + assert ( + sql == """SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `my_dataset.my_model`)""" + ) + + +def test_ml_principal_component_info_produces_correct_sql(): + sql = ml_sql.ml_principal_component_info(model_name="my_dataset.my_model") + assert ( + sql + == """SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `my_dataset.my_model`)""" + ) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 6762afc61fd..5a812dae7e7 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -15,6 +15,7 @@ import numpy +from bigframes import constants from third_party.bigframes_vendored.pandas.core.generic import NDFrame # ----------------------------------------------------------------------- @@ -33,7 +34,7 @@ class DataFrame(NDFrame): @property def shape(self) -> tuple[int, int]: """Return a tuple representing the dimensionality of the DataFrame.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def axes(self) -> list: @@ -67,7 +68,7 @@ def values(self) -> numpy.ndarray: na_value (default None): The value to use for missing values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # IO methods (to / from other formats) @@ -90,7 +91,7 @@ def to_numpy( Returns: numpy.ndarray: The converted NumPy array. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_gbq( self, @@ -124,7 +125,7 @@ def to_gbq( If set, write the ordering of the DataFrame as a column in the result table with this name. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_parquet( self, @@ -151,7 +152,7 @@ def to_parquet( Returns: None. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Unsorted @@ -179,7 +180,7 @@ def assign(self, **kwargs) -> DataFrame: bigframes.dataframe.DataFrame: A new DataFrame with the new columns in addition to all the existing columns. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Reindexing and alignment @@ -211,7 +212,7 @@ def drop( Raises: KeyError: If any of the labels is not found in the selected axis. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rename( self, @@ -233,7 +234,7 @@ def rename( Raises: KeyError: If any of the labels is not found. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rename_axis(self, mapper: Optional[str], **kwargs) -> DataFrame: """ @@ -250,7 +251,7 @@ def rename_axis(self, mapper: Optional[str], **kwargs) -> DataFrame: Returns: bigframes.dataframe.DataFrame: DataFrame with the new index name """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def set_index( self, @@ -273,7 +274,7 @@ def set_index( Returns: DataFrame: Changed row labels. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def reorder_levels(self, order: Sequence[int | str]) -> DataFrame: """ @@ -287,7 +288,7 @@ def reorder_levels(self, order: Sequence[int | str]) -> DataFrame: Returns: DataFrame: DataFrame of rearranged index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def droplevel(self, level): """ @@ -301,7 +302,7 @@ def droplevel(self, level): Returns: DataFrame: DataFrame with requested index / column level(s) removed. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def reset_index( self, @@ -320,7 +321,7 @@ def reset_index( Returns: bigframes.dataframe.DataFrame: DataFrame with the new index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def drop_duplicates( self, @@ -347,7 +348,7 @@ def drop_duplicates( Returns: bigframes.dataframe.DataFrame: DataFrame with duplicates removed """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def duplicated(self, subset=None, keep="first"): """ @@ -369,7 +370,7 @@ def duplicated(self, subset=None, keep="first"): Returns: bigframes.series.Series: Boolean series for each duplicated rows. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Reindex-based selection methods @@ -382,7 +383,7 @@ def dropna( Returns: bigframes.dataframe.DataFrame: DataFrame with NA entries dropped from it. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Sorting @@ -415,7 +416,7 @@ def sort_values( Returns: DataFrame with sorted values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sort_index( self, @@ -425,7 +426,7 @@ def sort_index( Returns: The original DataFrame sorted by the labels. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Arithmetic Methods @@ -450,7 +451,7 @@ def eq(self, other, axis: str | int = "columns") -> DataFrame: Returns: Result of the comparison. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def ne(self, other, axis: str | int = "columns") -> DataFrame: """ @@ -471,7 +472,7 @@ def ne(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: Result of the comparison. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def le(self, other, axis: str | int = "columns") -> DataFrame: """Get 'less than or equal to' of dataframe and other, element-wise (binary operator `<=`). @@ -497,7 +498,7 @@ def le(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame of bool. The result of the comparison. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def lt(self, other, axis: str | int = "columns") -> DataFrame: """Get 'less than' of DataFrame and other, element-wise (binary operator `<`). @@ -523,7 +524,7 @@ def lt(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame of bool. The result of the comparison. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def ge(self, other, axis: str | int = "columns") -> DataFrame: """Get 'greater than or equal to' of DataFrame and other, element-wise (binary operator `>=`). @@ -549,7 +550,7 @@ def ge(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame of bool. The result of the comparison. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def gt(self, other, axis: str | int = "columns") -> DataFrame: """Get 'greater than' of DataFrame and other, element-wise (binary operator `>`). @@ -575,7 +576,7 @@ def gt(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame of bool: The result of the comparison. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def add(self, other, axis: str | int = "columns") -> DataFrame: """Get addition of DataFrame and other, element-wise (binary operator `+`). @@ -598,7 +599,7 @@ def add(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sub(self, other, axis: str | int = "columns") -> DataFrame: """Get subtraction of DataFrame and other, element-wise (binary operator `-`). @@ -621,7 +622,7 @@ def sub(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rsub(self, other, axis: str | int = "columns") -> DataFrame: """Get subtraction of DataFrame and other, element-wise (binary operator `-`). @@ -644,7 +645,7 @@ def rsub(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mul(self, other, axis: str | int = "columns") -> DataFrame: """Get multiplication of DataFrame and other, element-wise (binary operator `*`). @@ -667,7 +668,7 @@ def mul(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def truediv(self, other, axis: str | int = "columns") -> DataFrame: """Get floating division of DataFrame and other, element-wise (binary operator `/`). @@ -690,7 +691,7 @@ def truediv(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rtruediv(self, other, axis: str | int = "columns") -> DataFrame: """Get floating division of DataFrame and other, element-wise (binary operator `/`). @@ -713,7 +714,7 @@ def rtruediv(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def floordiv(self, other, axis: str | int = "columns") -> DataFrame: """Get integer division of DataFrame and other, element-wise (binary operator `//`). @@ -736,7 +737,7 @@ def floordiv(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rfloordiv(self, other, axis: str | int = "columns") -> DataFrame: """Get integer division of DataFrame and other, element-wise (binary operator `//`). @@ -759,7 +760,7 @@ def rfloordiv(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mod(self, other, axis: str | int = "columns") -> DataFrame: """Get modulo of DataFrame and other, element-wise (binary operator `%`). @@ -782,7 +783,7 @@ def mod(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rmod(self, other, axis: str | int = "columns") -> DataFrame: """Get modulo of DataFrame and other, element-wise (binary operator `%`). @@ -805,7 +806,7 @@ def rmod(self, other, axis: str | int = "columns") -> DataFrame: Returns: DataFrame: DataFrame result of the arithmetic operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Data reshaping @@ -847,7 +848,7 @@ def groupby( Returns: bigframes.core.groupby.SeriesGroupBy: A groupby object that contains information about the groups. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Function application @@ -874,7 +875,7 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: Returns: bigframes.dataframe.DataFrame: Transformed DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Merging / joining methods @@ -902,7 +903,7 @@ def join(self, other, *, on: Optional[str] = None, how: str) -> DataFrame: Returns: bigframes.dataframe.DataFrame: A dataframe containing columns from both the caller and `other`. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def merge( self, @@ -972,7 +973,7 @@ def merge( Returns: bigframes.dataframe.DataFrame: A DataFrame of the two merged objects. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # ndarray-like stats methods @@ -992,7 +993,7 @@ def any(self, *, bool_only: bool = False): Returns: Series """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def all(self, *, bool_only: bool = False): """ @@ -1009,7 +1010,7 @@ def all(self, *, bool_only: bool = False): Returns: bigframes.series.Series: Series if all elements are True. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def prod(self, *, numeric_only: bool = False): """ @@ -1022,7 +1023,7 @@ def prod(self, *, numeric_only: bool = False): Returns: bigframes.series.Series: Series with the product of the values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def min(self, *, numeric_only: bool = False): """Return the minimum of the values over the requested axis. @@ -1037,7 +1038,7 @@ def min(self, *, numeric_only: bool = False): Returns: bigframes.series.Series: Series with the minimum of the values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def max(self, *, numeric_only: bool = False): """Return the maximum of the values over the requested axis. @@ -1052,7 +1053,7 @@ def max(self, *, numeric_only: bool = False): Returns: bigframes.series.Series: Series after the maximum of values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sum(self, *, numeric_only: bool = False): """Return the sum of the values over the requested axis. @@ -1066,7 +1067,7 @@ def sum(self, *, numeric_only: bool = False): Returns: bigframes.series.Series: Series with the sum of values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mean(self, *, numeric_only: bool = False): """Return the mean of the values over the requested axis. @@ -1078,7 +1079,7 @@ def mean(self, *, numeric_only: bool = False): Returns: bigframes.series.Series: Series with the mean of values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def median(self, *, numeric_only: bool = False, exact: bool = False): """Return the median of the values over the requested axis. @@ -1093,7 +1094,7 @@ def median(self, *, numeric_only: bool = False, exact: bool = False): Returns: bigframes.series.Series: Series with the median of values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def var(self, *, numeric_only: bool = False): """Return unbiased variance over requested axis. @@ -1107,7 +1108,7 @@ def var(self, *, numeric_only: bool = False): Returns: bigframes.series.Series: Series with unbiased variance over requested axis. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def std(self, *, numeric_only: bool = False): """Return sample standard deviation over requested axis. @@ -1121,7 +1122,7 @@ def std(self, *, numeric_only: bool = False): Returns: bigframes.series.Series: Series with sample standard deviation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def count(self, *, numeric_only: bool = False): """ @@ -1138,7 +1139,7 @@ def count(self, *, numeric_only: bool = False): bigframes.series.Series: For each column/row the number of non-NA/null entries. If `level` is specified returns a `DataFrame`. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def nunique(self): """ @@ -1147,7 +1148,7 @@ def nunique(self): Returns: bigframes.series.Series: Series with number of distinct elements. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummin(self) -> DataFrame: """Return cumulative minimum over a DataFrame axis. @@ -1157,7 +1158,7 @@ def cummin(self) -> DataFrame: Returns: bigframes.dataframe.DataFrame: Return cumulative minimum of DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummax(self) -> DataFrame: """Return cumulative maximum over a DataFrame axis. @@ -1167,7 +1168,7 @@ def cummax(self) -> DataFrame: Returns: bigframes.dataframe.DataFrame: Return cumulative maximum of DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumsum(self) -> DataFrame: """Return cumulative sum over a DataFrame axis. @@ -1177,7 +1178,7 @@ def cumsum(self) -> DataFrame: Returns: bigframes.dataframe.DataFrame: Return cumulative sum of DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumprod(self) -> DataFrame: """Return cumulative product over a DataFrame axis. @@ -1187,7 +1188,7 @@ def cumprod(self) -> DataFrame: Returns: bigframes.dataframe.DataFrame: Return cumulative product of DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def agg(self, func): """ @@ -1202,7 +1203,7 @@ def agg(self, func): Returns: DataFrame or bigframes.series.Series: Aggregated results. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def describe(self): """ @@ -1227,7 +1228,7 @@ def describe(self): Returns: bigframes.dataframe.DataFrame: Summary statistics of the Series or Dataframe provided. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def pivot(self, *, columns, index=None, values=None): """ @@ -1263,7 +1264,30 @@ def pivot(self, *, columns, index=None, values=None): Returns: Returns reshaped DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def stack(self): + """ + Stack the prescribed level(s) from columns to index. + + Return a reshaped DataFrame or Series having a multi-level + index with one or more new inner-most levels compared to the current + DataFrame. The new inner-most levels are created by pivoting the + columns of the current dataframe: + + - if the columns have a single level, the output is a Series; + - if the columns have multiple levels, the new index + level(s) is (are) taken from the prescribed level(s) and + the output is a DataFrame. + + .. note:: + BigQuery DataFrames does not support stack operations that would + combine columns of different dtypes. + + Returns: + DataFrame or Series: Stacked dataframe or series. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Add index and columns @@ -1280,12 +1304,12 @@ def index(self): Returns: The index labels of the DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def columns(self): "The column labels of the DataFrame." - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def value_counts( self, @@ -1313,4 +1337,4 @@ def value_counts( Returns: Series: Series containing counts of unique rows in the DataFrame """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 4843c971da2..56d3b2434f0 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -3,6 +3,7 @@ from typing import Literal, Optional +from bigframes import constants from third_party.bigframes_vendored.pandas.core import indexing @@ -22,7 +23,7 @@ def ndim(self) -> int: Returns: int: Return 1 if Series. Otherwise return 2 if DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def size(self) -> int: @@ -32,7 +33,7 @@ def size(self) -> int: int: Return the number of rows if Series. Otherwise return the number of rows times number of columns if DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ------------------------------------------------------------------------- # Unary Methods @@ -46,7 +47,7 @@ def abs(self): Series/DataFrame containing the absolute value of each element. Returns a Series/DataFrame containing the absolute value of each element. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def astype(self, dtype): """ @@ -66,7 +67,7 @@ def astype(self, dtype): same type as caller """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Iteration @@ -85,7 +86,7 @@ def empty(self) -> bool: Returns: bool: If Series/DataFrame is empty, return True, if not return False. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # I/O Methods @@ -155,7 +156,7 @@ def to_json( Returns: None: String output not yet supported. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: """Write object to a comma-separated values (csv) file on Cloud Storage. @@ -177,7 +178,7 @@ def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: Returns: None: String output not yet supported. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Unsorted @@ -215,7 +216,7 @@ def add_prefix(self, prefix: str, axis: int | str | None = None): Returns: New Series or DataFrame with updated labels. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def add_suffix(self, suffix: str, axis: int | str | None = None): """Suffix labels with string `suffix`. @@ -233,7 +234,7 @@ def add_suffix(self, suffix: str, axis: int | str | None = None): Returns: New Series or DataFrame with updated labels. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def head(self, n: int = 5): """Return the first `n` rows. @@ -254,7 +255,7 @@ def head(self, n: int = 5): Returns: The first `n` rows of the caller object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def tail(self, n: int = 5): """Return the last `n` rows. @@ -275,7 +276,7 @@ def tail(self, n: int = 5): Returns: The last `n` rows of the caller object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sample( self, @@ -301,7 +302,7 @@ def sample( A new object of same type as caller containing `n` items randomly sampled from the caller object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Internal Interface Methods @@ -317,7 +318,7 @@ def dtypes(self): Returns: A *pandas* Series with the data type of each column. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def copy(self): """Make a copy of this object's indices and data. @@ -329,7 +330,7 @@ def copy(self): Returns: Object type matches caller. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # Action Methods @@ -346,7 +347,7 @@ def isna(self) -> NDFrame: Mask of bool values for each element that indicates whether an element is an NA value. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) isnull = isna @@ -362,7 +363,7 @@ def notna(self) -> NDFrame: NDFrame: Mask of bool values for each element that indicates whether an element is not an NA value. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) notnull = notna @@ -381,7 +382,7 @@ def shift( Returns: NDFrame: Copy of input object, shifted. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rank( self, @@ -419,7 +420,7 @@ def rank( Returns: same type as caller: Return a Series or DataFrame with data ranks as values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def __nonzero__(self): raise ValueError( diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 95822718c3a..9271da8a5ea 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -9,6 +9,8 @@ class providing the base-class of operations. """ from __future__ import annotations +from bigframes import constants + class GroupBy: """ @@ -24,7 +26,7 @@ def any(self): where a value is True if any element is True within its respective group, False otherwise. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def all(self): """ @@ -35,7 +37,7 @@ def all(self): where a value is True if all elements are True within its respective group, False otherwise. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def count(self): """ @@ -44,7 +46,7 @@ def count(self): Returns: Series or DataFrame: Count of values within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mean( self, @@ -60,7 +62,7 @@ def mean( Returns: pandas.Series or pandas.DataFrame: Mean of groups. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def median( self, @@ -81,7 +83,7 @@ def median( Returns: pandas.Series or pandas.DataFrame: Median of groups. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def std( self, @@ -100,7 +102,7 @@ def std( Returns: Series or DataFrame: Standard deviation of values within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def var( self, @@ -120,7 +122,7 @@ def var( Series or DataFrame Variance of values within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sum( self, @@ -140,7 +142,7 @@ def sum( Returns: Series or DataFrame: Computed sum of values within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def prod(self, numeric_only: bool = False, min_count: int = 0): """ @@ -156,7 +158,7 @@ def prod(self, numeric_only: bool = False, min_count: int = 0): Returns: Series or DataFrame: Computed prod of values within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def min( self, @@ -176,7 +178,7 @@ def min( Returns: Series or DataFrame: Computed min of values within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def max( self, @@ -196,7 +198,7 @@ def max( Returns: Series or DataFrame: Computed max of values within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumcount(self, ascending: bool = True): """ @@ -209,7 +211,7 @@ def cumcount(self, ascending: bool = True): Returns: Series: Sequence number of each element within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumprod(self, *args, **kwargs): """ @@ -218,7 +220,7 @@ def cumprod(self, *args, **kwargs): Returns: Series or DataFrame: Cumulative product for each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumsum(self, *args, **kwargs): """ @@ -227,7 +229,7 @@ def cumsum(self, *args, **kwargs): Returns: Series or DataFrame: Cumulative sum for each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummin(self, *args, numeric_only: bool = False, **kwargs): """ @@ -236,7 +238,7 @@ def cummin(self, *args, numeric_only: bool = False, **kwargs): Returns: Series or DataFrame: Cumulative min for each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummax(self, *args, numeric_only: bool = False, **kwargs): """ @@ -245,7 +247,7 @@ def cummax(self, *args, numeric_only: bool = False, **kwargs): Returns: Series or DataFrame: Cumulative max for each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def diff(self): """ @@ -256,7 +258,7 @@ def diff(self): Returns: Series or DataFrame: First differences. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def shift(self, periods: int = 1): """ @@ -269,7 +271,7 @@ def shift(self, periods: int = 1): Returns: Series or DataFrame: Object shifted within each group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rolling(self, *args, **kwargs): """ @@ -289,7 +291,7 @@ def rolling(self, *args, **kwargs): Returns: Series or DataFrame: Return a new grouper with our rolling appended. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def expanding(self, *args, **kwargs): """ @@ -298,7 +300,7 @@ def expanding(self, *args, **kwargs): Returns: Series or DataFrame: A expanding grouper, providing expanding functionality per group. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class SeriesGroupBy(GroupBy): @@ -318,7 +320,7 @@ def agg(self, func): Returns: Series or DataFrame """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class DataFrameGroupBy(GroupBy): @@ -347,4 +349,4 @@ def agg(self, func, **kwargs): Returns: DataFrame """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py index d59886e8aad..2b4a3263175 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py @@ -1,3 +1,6 @@ +from bigframes import constants + + class DatetimeProperties: """ Accessor object for datetime-like properties of the Series values. @@ -7,7 +10,7 @@ class DatetimeProperties: def day(self): """The day of the datetime.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def dayofweek(self): @@ -22,7 +25,7 @@ def dayofweek(self): Series or Index: Containing integers indicating the day number. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def date(self): @@ -36,31 +39,31 @@ def date(self): a numpy array. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def hour(self): """The hours of the datetime.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def minute(self): """The minutes of the datetime.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def month(self): """The month as January=1, December=12.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def second(self): """The seconds of the datetime.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def time(self): @@ -73,7 +76,7 @@ def time(self): a numpy array. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def quarter(self): @@ -84,10 +87,10 @@ def quarter(self): a numpy array. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def year(self): """The year of the datetime.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index ebad5eb9181..404a99809c0 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -1,5 +1,7 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexes/base.py +from bigframes import constants + class Index: """Immutable sequence used for indexing and alignment. @@ -10,14 +12,14 @@ class Index: @property def name(self): """Returns Index name.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def shape(self): """ Return a tuple of the shape of the underlying data. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_numpy(self, dtype): """ @@ -33,4 +35,4 @@ def to_numpy(self, dtype): Returns: numpy.ndarray """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/indexing.py b/third_party/bigframes_vendored/pandas/core/indexing.py index d5b9f3c079f..fae5d6261f2 100644 --- a/third_party/bigframes_vendored/pandas/core/indexing.py +++ b/third_party/bigframes_vendored/pandas/core/indexing.py @@ -1,5 +1,7 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexing.py +from bigframes import constants + class IndexingMixin: """ @@ -32,7 +34,7 @@ def iloc(self): out-of-bounds, except *slice* indexers which allow out-of-bounds indexing (this conforms with python/numpy *slice* semantics). """ - raise NotImplementedError("abstract methdod") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def loc(self): @@ -63,4 +65,4 @@ def loc(self): NotImplementError: if the inputs are not supported. """ - raise NotImplementedError("abstract methdod") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/concat.py b/third_party/bigframes_vendored/pandas/core/reshape/concat.py index 6a5a9fdde92..6e6d2d8b5c9 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/concat.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/concat.py @@ -4,6 +4,8 @@ """ from __future__ import annotations +from bigframes import constants + def concat( objs, @@ -135,4 +137,4 @@ def concat( [4 rows x 2 columns] """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/tile.py b/third_party/bigframes_vendored/pandas/core/reshape/tile.py index 9381ad4552a..4f5f2efef0a 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/tile.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/tile.py @@ -4,6 +4,8 @@ """ from __future__ import annotations +from bigframes import constants + def cut( x, @@ -62,4 +64,4 @@ def cut( are whatever the type in the sequence is. False : returns an ndarray of integers. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 79eb4026968..8d505c1eadb 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -10,6 +10,7 @@ from pandas._typing import Axis, FilePath, NaPosition, WriteBuffer import pandas.io.formats.format as fmt +from bigframes import constants from third_party.bigframes_vendored.pandas.core.generic import NDFrame if TYPE_CHECKING: @@ -23,31 +24,31 @@ def dt(self): """ Accessor object for datetime-like properties of the Series values. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def index(self): """The index (axis labels) of the Series.""" - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def shape(self): """Return a tuple of the shape of the underlying data.""" - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def dtype(self): """ Return the dtype object of the underlying data. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def dtypes(self): """ Return the dtype object of the underlying data. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def name(self) -> Hashable: @@ -62,7 +63,7 @@ def name(self) -> Hashable: hashable object: The name of the Series, also the column name if part of a DataFrame. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def reset_index( self, @@ -94,13 +95,13 @@ def reset_index( In either case, if ``inplace=True``, no value is returned. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def __repr__(self) -> str: """ Return a string representation for a particular Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # IO methods (to / from other formats) @@ -165,7 +166,7 @@ def to_string( result = formatter.to_string() # catch contract violations - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_markdown( self, @@ -188,7 +189,7 @@ def to_markdown( Returns: str: {klass} in Markdown-friendly format. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_dict(self, into: type[dict] = dict) -> Mapping: """ @@ -204,7 +205,7 @@ def to_dict(self, into: type[dict] = dict) -> Mapping: Returns: collections.abc.Mapping: Key-value representation of Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_frame(self) -> DataFrame: """ @@ -213,7 +214,7 @@ def to_frame(self) -> DataFrame: Returns: bigframes.dataframe.DataFrame: DataFrame representation of Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_excel(self, excel_writer, sheet_name): """ @@ -235,7 +236,7 @@ def to_excel(self, excel_writer, sheet_name): sheet_name (str, default 'Sheet1'): Name of sheet to contain Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_latex(self, buf=None, columns=None, header=True, index=True, **kwargs): """ @@ -256,7 +257,7 @@ def to_latex(self, buf=None, columns=None, header=True, index=True, **kwargs): str or None: If buf is None, returns the result as a string. Otherwise returns None. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def tolist(self) -> list: """ @@ -269,7 +270,7 @@ def tolist(self) -> list: Returns: list: list of the values """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) to_list = tolist @@ -296,7 +297,7 @@ def to_numpy(self, dtype, copy=False, na_value=None): numpy.ndarray: A NumPy ndarray representing the values in this Series or Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_pickle(self, path, **kwargs): """ @@ -308,7 +309,7 @@ def to_pickle(self, path, **kwargs): object implementing a binary ``write()`` function. File path where the pickled object will be stored. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_xarray(self): """ @@ -319,7 +320,7 @@ def to_xarray(self): converted to Dataset if the object is a DataFrame, or a DataArray if the object is a Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_json( self, @@ -354,7 +355,7 @@ def to_json( None or str: If path_or_buf is None, returns the resulting json format as a string. Otherwise returns None. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: """ @@ -372,7 +373,7 @@ def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: None or str: If path_or_buf is None, returns the resulting csv format as a string. Otherwise returns None. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def agg(self, func): """ @@ -387,7 +388,7 @@ def agg(self, func): Returns: scalar or Series: Aggregated results """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def count(self): """ @@ -397,7 +398,7 @@ def count(self): int or Series (if level specified): Number of non-null values in the Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def nunique(self) -> int: """ @@ -408,7 +409,7 @@ def nunique(self) -> int: Returns: int: number of unique elements in the object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mode(self) -> Series: """ @@ -421,7 +422,7 @@ def mode(self) -> Series: Returns: bigframes.series.Series: Modes of the Series in sorted order. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def drop_duplicates( self, @@ -442,7 +443,7 @@ def drop_duplicates( Returns: bigframes.series.Series: Series with duplicates dropped or None if ``inplace=True``. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def duplicated(self, keep="first") -> Series: """ @@ -466,7 +467,7 @@ def duplicated(self, keep="first") -> Series: bigframes.series.Series: Series indicating whether each value has occurred in the preceding values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def round(self, decimals: int = 0) -> Series: """ @@ -480,6 +481,29 @@ def round(self, decimals: int = 0) -> Series: Returns: bigframes.series.Series: Rounded values of the Series. """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def corr(self, other, method="pearson", min_periods=None) -> float: + """ + Compute the correlation with the other Series. Non-number values are ignored in the + computation. + + Uses the "Pearson" method of correlation. Numbers are converted to float before + calculation, so the result may be unstable. + + Args: + other (Series): + The series with which this is to be correlated. + method (string, default "pearson"): + Correlation method to use - currently only "pearson" is supported. + min_periods (int, default None): + The minimum number of observations needed to return a result. Non-default values + are not yet supported, so a result will be returned for at least two observations. + + Returns: + float; Will return NaN if there are fewer than two numeric pairs, either series has a + variance or covariance of zero, or any input value is infinite. + """ raise NotImplementedError("abstract method") def diff(self) -> Series: @@ -497,7 +521,7 @@ def diff(self) -> Series: Returns: {klass}: First differences of the Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def dot(self, other) -> Series | np.ndarray: """ @@ -527,19 +551,19 @@ def dot(self, other) -> Series | np.ndarray: """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def __matmul__(self, other): """ Matrix multiplication using binary `@` operator in Python>=3.5. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def __rmatmul__(self, other): """ Matrix multiplication using binary `@` operator in Python>=3.5. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sort_values( self, @@ -571,7 +595,7 @@ def sort_values( Returns: bigframes.series.Series: Series ordered by values or None if ``inplace=True``. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sort_index( self, @@ -602,7 +626,7 @@ def sort_index( """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def nlargest( self, n: int = 5, keep: Literal["first", "last", "all"] = "first" @@ -626,7 +650,7 @@ def nlargest( Returns: bigframes.series.Series: The `n` largest values in the Series, sorted in decreasing order. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def nsmallest(self, n: int = 5, keep: str = "first") -> Series: """ @@ -649,7 +673,7 @@ def nsmallest(self, n: int = 5, keep: str = "first") -> Series: Returns: bigframes.series.Series: The `n` smallest values in the Series, sorted in increasing order. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- # function application @@ -672,7 +696,7 @@ def apply( bigframes.series.Series: If func returns a Series object the result will be a DataFrame. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def groupby( self, @@ -726,7 +750,7 @@ def groupby( bigframes.core.groupby.SeriesGroupBy: Returns a groupby object that contains information about the groups. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def drop( self, labels=None, *, axis=0, index=None, columns=None, level=None @@ -758,7 +782,7 @@ def drop( Raises: KeyError: If none of the labels are found in the index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def reorder_levels(self, order: Sequence) -> Series: """ @@ -773,7 +797,7 @@ def reorder_levels(self, order: Sequence) -> Series: Returns: type of caller (new object) """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def droplevel(self, level): """ @@ -788,7 +812,7 @@ def droplevel(self, level): Returns: Series with requested index / column level(s) removed. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def fillna( self, @@ -804,7 +828,7 @@ def fillna( Returns: Series or None: Object with missing values filled or None. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def between( self, @@ -832,7 +856,7 @@ def between( right (inclusive). """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumprod(self): """ @@ -844,7 +868,7 @@ def cumprod(self): Returns: bigframes.series.Series: Return cumulative sum of scalar or Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumsum(self): """ @@ -861,7 +885,7 @@ def cumsum(self): Returns: scalar or Series: Return cumulative sum of scalar or Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummax(self): """ @@ -878,7 +902,7 @@ def cummax(self): Returns: bigframes.series.Series: Return cumulative maximum of scalar or Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummin(self): """ @@ -901,7 +925,7 @@ def cummin(self): Returns: bigframes.series.Series: Return cumulative minimum of scalar or Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def eq(self, other) -> Series: """Return equal of Series and other, element-wise (binary operator eq). @@ -916,7 +940,7 @@ def eq(self, other) -> Series: Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def ne(self, other) -> Series: """Return not equal of Series and other, element-wise (binary operator ne). @@ -931,7 +955,7 @@ def ne(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def le(self, other) -> Series: """Get 'less than or equal to' of Series and other, element-wise (binary operator `<=`). @@ -946,7 +970,7 @@ def le(self, other) -> Series: bigframes.series.Series. The result of the comparison. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def lt(self, other) -> Series: """Get 'less than' of Series and other, element-wise (binary operator `<`). @@ -961,7 +985,7 @@ def lt(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def ge(self, other) -> Series: """Get 'greater than or equal to' of Series and other, element-wise (binary operator `>=`). @@ -976,7 +1000,7 @@ def ge(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def gt(self, other) -> Series: """Get 'less than or equal to' of Series and other, element-wise (binary operator `<=`). @@ -991,7 +1015,7 @@ def gt(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def add(self, other) -> Series: """Return addition of Series and other, element-wise (binary operator add). @@ -1006,7 +1030,7 @@ def add(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def radd(self, other) -> Series: """Return addition of Series and other, element-wise (binary operator radd). @@ -1021,7 +1045,7 @@ def radd(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sub( self, @@ -1039,7 +1063,7 @@ def sub( bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rsub(self, other) -> Series: """Return subtraction of Series and other, element-wise (binary operator rsub). @@ -1054,7 +1078,7 @@ def rsub(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mul(self, other) -> Series: """Return multiplication of Series and other, element-wise (binary operator mul). @@ -1069,7 +1093,7 @@ def mul(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rmul(self, other) -> Series: """Return multiplication of Series and other, element-wise (binary operator mul). @@ -1083,7 +1107,7 @@ def rmul(self, other) -> Series: Returns: Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def truediv(self, other) -> Series: """Return floating division of Series and other, element-wise (binary operator truediv). @@ -1098,7 +1122,7 @@ def truediv(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rtruediv(self, other) -> Series: """Return floating division of Series and other, element-wise (binary operator rtruediv). @@ -1113,7 +1137,7 @@ def rtruediv(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def floordiv(self, other) -> Series: """Return integer division of Series and other, element-wise (binary operator floordiv). @@ -1128,7 +1152,7 @@ def floordiv(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rfloordiv(self, other) -> Series: """Return integer division of Series and other, element-wise (binary operator rfloordiv). @@ -1143,7 +1167,7 @@ def rfloordiv(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mod(self, other) -> Series: """Return modulo of Series and other, element-wise (binary operator mod). @@ -1158,7 +1182,7 @@ def mod(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rmod(self, other) -> Series: """Get modulo of Series and other, element-wise (binary operator `rmod`). @@ -1173,7 +1197,7 @@ def rmod(self, other) -> Series: bigframes.series.Series: The result of the operation. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def divmod(self, other) -> Series: """Return integer division and modulo of Series and other, element-wise (binary operator divmod). @@ -1188,7 +1212,7 @@ def divmod(self, other) -> Series: consistent with (floordiv, mod) (though pandas may not). """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rdivmod(self, other) -> Series: """Return integer division and modulo of Series and other, element-wise (binary operator rdivmod). @@ -1203,7 +1227,7 @@ def rdivmod(self, other) -> Series: consistent with (rfloordiv, rmod) (though pandas may not). """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def all( self, @@ -1218,7 +1242,7 @@ def all( scalar or Series: If level is specified, then, Series is returned; otherwise, scalar is returned. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def any( self, @@ -1233,7 +1257,7 @@ def any( scalar or Series: If level is specified, then, Series is returned; otherwise, scalar is returned. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def max( self, @@ -1248,7 +1272,7 @@ def max( Returns: scalar or scalar """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def min( self, @@ -1262,7 +1286,7 @@ def min( Returns: scalar or scalar """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def std( self, @@ -1277,7 +1301,7 @@ def std( ------- scalar or Series (if level specified) """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def var( self, @@ -1290,7 +1314,7 @@ def var( Returns: scalar or Series (if level specified) """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sum(self): """Return the sum of the values over the requested axis. @@ -1300,7 +1324,7 @@ def sum(self): Returns: scalar """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mean(self): """Return the mean of the values over the requested axis. @@ -1308,7 +1332,7 @@ def mean(self): Returns: scalar """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def median(self, *, exact: bool = False): """Return the median of the values over the requested axis. @@ -1321,7 +1345,7 @@ def median(self, *, exact: bool = False): Returns: scalar """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def prod(self): """Return the product of the values over the requested axis. @@ -1329,7 +1353,7 @@ def prod(self): Returns: scalar """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def skew(self): """Return unbiased skew over requested axis. @@ -1339,7 +1363,7 @@ def skew(self): Returns: scalar """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def kurt(self): """Return unbiased kurtosis over requested axis. @@ -1349,7 +1373,7 @@ def kurt(self): Returns: scalar or scalar: Unbiased kurtosis over requested axis. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def where(self, cond, other): """Replace values where the condition is False. @@ -1373,7 +1397,7 @@ def where(self, cond, other): Returns: bigframes.series.Series: Series after the replacement. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mask(self, cond, other): """Replace values where the condition is True. @@ -1397,7 +1421,7 @@ def mask(self, cond, other): Returns: bigframes.series.Series: Series after the replacement. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def clip(self): """Trim values at input threshold(s). @@ -1416,7 +1440,7 @@ def clip(self): Returns: Series. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def argmax(self): """ @@ -1427,7 +1451,7 @@ def argmax(self): Returns: Series: Row position of the maximum value. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def argmin(self): """ @@ -1438,7 +1462,7 @@ def argmin(self): Returns: Series: Row position of the minimum value. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rename(self, index, **kwargs) -> Series | None: """ @@ -1461,7 +1485,7 @@ def rename(self, index, **kwargs) -> Series | None: bigframes.series.Series: Series with index labels. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rename_axis(self, mapper, **kwargs): """ @@ -1474,7 +1498,7 @@ def rename_axis(self, mapper, **kwargs): Returns: bigframes.series.Series: Series with the name of the axis set. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rolling( self, @@ -1515,7 +1539,7 @@ def rolling( bigframes.core.window.Window: ``Window`` subclass if a ``win_type`` is passed. ``Rolling`` subclass if ``win_type`` is not passed. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def expanding(self, min_periods=1): """ @@ -1529,7 +1553,7 @@ def expanding(self, min_periods=1): Returns: bigframes.core.window.Window: ``Expanding`` subclass. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def value_counts( self, @@ -1560,7 +1584,7 @@ def value_counts( Returns: Series: Series containing counts of unique values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def str(self): @@ -1570,7 +1594,7 @@ def str(self): NAs stay NA unless handled otherwise by a particular method. Patterned after Python’s string methods, with some inspiration from R’s stringr package. """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isin(self, values): """ @@ -1595,7 +1619,7 @@ def isin(self, values): Raises: TypeError: If input is not list-like. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def is_monotonic_increasing(self) -> bool: @@ -1605,7 +1629,7 @@ def is_monotonic_increasing(self) -> bool: Returns: bool """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def is_monotonic_decreasing(self) -> bool: @@ -1615,4 +1639,4 @@ def is_monotonic_decreasing(self) -> bool: Returns: bool """ - raise NotImplementedError("abstract property") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py index 5e3d0b047ff..ecdd9547d54 100644 --- a/third_party/bigframes_vendored/pandas/core/strings/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -1,6 +1,8 @@ import re import typing +from bigframes import constants + class StringMethods: """ @@ -32,7 +34,7 @@ def extract(self, pat: str, flags: int = 0): expression pat will be used for column names; otherwise capture group numbers will be used. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def find(self, sub, start: int = 0, end=None): """Return lowest indexes in each strings in the Series/Index. @@ -52,7 +54,7 @@ def find(self, sub, start: int = 0, end=None): Returns: bigframes.series.Series: Series with lowest indexes in each strings. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def len(self): """Compute the length of each element in the Series/Index. @@ -65,7 +67,7 @@ def len(self): the length of each element in the Series or Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def lower(self): """Convert strings in the Series/Index to lowercase. @@ -76,7 +78,7 @@ def lower(self): bigframes.series.Series: Series with lowercase. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def slice(self, start=None, stop=None): """Slice substrings from each element in the Series or Index. @@ -94,7 +96,7 @@ def slice(self, start=None, stop=None): substring from original string object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def strip(self): """Remove leading and trailing characters. @@ -109,7 +111,7 @@ def strip(self): and trailing characters. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def upper(self): """Convert strings in the Series/Index to uppercase. @@ -120,7 +122,7 @@ def upper(self): bigframes.series.Series: Series with uppercase strings. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isnumeric(self): """Check whether all characters in each string are numeric. @@ -134,7 +136,7 @@ def isnumeric(self): same length as the original Series/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isalpha(self): """Check whether all characters in each string are alphabetic. @@ -147,7 +149,7 @@ def isalpha(self): bigframes.series.Series: Series with the same length as the originalSeries/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isdigit(self): """Check whether all characters in each string are digits. @@ -160,7 +162,7 @@ def isdigit(self): bigframes.series.Series: Series with the same length as the originalSeries/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isalnum(self): """Check whether all characters in each string are alphanumeric. @@ -174,7 +176,7 @@ def isalnum(self): same length as the original Series/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isspace(self): """Check whether all characters in each string are whitespace. @@ -188,7 +190,7 @@ def isspace(self): same length as the original Series/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def islower(self): """Check whether all characters in each string are lowercase. @@ -202,7 +204,7 @@ def islower(self): same length as the original Series/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isupper(self): """Check whether all characters in each string are uppercase. @@ -216,7 +218,7 @@ def isupper(self): same length as the original Series/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def isdecimal(self): """Check whether all characters in each string are decimal. @@ -230,7 +232,7 @@ def isdecimal(self): same length as the original Series/Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rstrip(self): """Remove trailing characters. @@ -244,7 +246,7 @@ def rstrip(self): bigframes.series.Series: Series without trailing characters. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def lstrip(self): """Remove leading characters. @@ -258,7 +260,7 @@ def lstrip(self): bigframes.series.Series: Series without leading characters. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def repeat(self, repeats: int): """Duplicate each string in the Series or Index. @@ -272,7 +274,7 @@ def repeat(self, repeats: int): objects specified by input parameter repeats. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def capitalize(self): """Convert strings in the Series/Index to be capitalized. @@ -283,7 +285,7 @@ def capitalize(self): bigframes.series.Series: Series with captitalized strings. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cat(self, others, *, join): """Concatenate strings in the Series/Index with given separator. @@ -304,7 +306,7 @@ def cat(self, others, *, join): bigframes.series.Series: Series with concatenated strings. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def contains(self, pat, case: bool = True, flags: int = 0, *, regex: bool = True): """ @@ -329,7 +331,7 @@ def contains(self, pat, case: bool = True, flags: int = 0, *, regex: bool = True whether the given pattern is contained within the string of each element of the Series or Index. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def replace( self, @@ -373,7 +375,7 @@ def replace( of `pat` replaced by `repl`. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def startswith( self, @@ -391,7 +393,7 @@ def startswith( bigframes.series.Series: A Series of booleans indicating whether the given pattern matches the start of each string element. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def endswith( self, @@ -409,7 +411,7 @@ def endswith( bigframes.series.Series: A Series of booleans indicating whether the given pattern matches the end of each string element. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def match(self, pat: str, case: bool = True, flags: int = 0): """ @@ -426,7 +428,7 @@ def match(self, pat: str, case: bool = True, flags: int = 0): Returns: bigframes.series.Series: Series of boolean values """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def fullmatch(self, pat: str, case: bool = True, flags: int = 0): """ @@ -443,7 +445,7 @@ def fullmatch(self, pat: str, case: bool = True, flags: int = 0): Returns: bigframes.series.Series: Series of boolean values """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def get(self, i: int): """ @@ -459,7 +461,7 @@ def get(self, i: int): Returns: bigframes.series.Series: Series """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def pad( self, @@ -482,7 +484,7 @@ def pad( Returns: bigframes.series.Series: Returns Series or Index with minimum number of char in object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def ljust( self, @@ -502,7 +504,7 @@ def ljust( Returns: bigframes.series.Series: Returns Series or Index with minimum number of char in object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rjust( self, @@ -522,7 +524,7 @@ def rjust( Returns: bigframes.series.Series: Returns Series or Index with minimum number of char in object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def zfill( self, @@ -544,7 +546,7 @@ def zfill( Returns: bigframes.series.Series: Series of objects. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def center( self, @@ -566,4 +568,4 @@ def center( Returns: bigframes.series.Series: Returns Series or Index with minimum number of char in object. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/window/rolling.py b/third_party/bigframes_vendored/pandas/core/window/rolling.py index 7a9239b70cf..a869c86e72a 100644 --- a/third_party/bigframes_vendored/pandas/core/window/rolling.py +++ b/third_party/bigframes_vendored/pandas/core/window/rolling.py @@ -4,34 +4,36 @@ similar to how we have a Groupby object. """ +from bigframes import constants + class Window: """Provide window calculations.""" def count(self): """Calculate the window count of non-NULL observations.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def sum(self): """Calculate the weighted window sum.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def mean(self): """Calculate the weighted window mean.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def var(self): """Calculate the weighted window variance.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def std(self): """Calculate the weighted window standard deviation.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def max(self): """Calculate the weighted window maximum.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def min(self): """Calculate the weighted window minimum.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 730872034dc..95531ff5e82 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -5,6 +5,8 @@ from typing import Iterable, Optional +from bigframes import constants + class GBQIOMixin: def read_gbq( @@ -83,4 +85,4 @@ def read_gbq( Returns: bigframes.dataframe.DataFrame: A DataFrame representing results of the query or table. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/io/parquet.py b/third_party/bigframes_vendored/pandas/io/parquet.py index 6f0a2b3cb46..9aed9af5a82 100644 --- a/third_party/bigframes_vendored/pandas/io/parquet.py +++ b/third_party/bigframes_vendored/pandas/io/parquet.py @@ -2,6 +2,8 @@ """ parquet compat """ from __future__ import annotations +from bigframes import constants + class ParquetIOMixin: def read_parquet( @@ -22,4 +24,4 @@ def read_parquet( Returns: bigframes.dataframe.DataFrame: A BigQuery DataFrames. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py index e01eb734fb8..d19a92ecdf8 100644 --- a/third_party/bigframes_vendored/pandas/io/parsers/readers.py +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -6,10 +6,22 @@ """ from __future__ import annotations -from typing import Any, Dict, Literal, MutableSequence, Optional, Sequence, Tuple, Union +from typing import ( + Any, + Dict, + IO, + Literal, + MutableSequence, + Optional, + Sequence, + Tuple, + Union, +) import numpy as np +from bigframes import constants + class ReaderIOMixin: def read_csv( @@ -45,7 +57,8 @@ def read_csv( Args: filepath_or_buffer (str): - a string path including Cloud Storage and local file. + A local or Google Cloud Storage (`gs://`) path with `engine="bigquery"` + otherwise passed to pandas.read_csv. sep (Optional[str], default ","): the separator for fields in a CSV file. For the BigQuery engine, the separator can be any ISO-8859-1 single-byte character. To use a character in the range @@ -104,10 +117,71 @@ def read_csv( https://docs.python.org/3/library/codecs.html#standard-encodings The BigQuery engine only supports `UTF-8` and `ISO-8859-1`. **kwargs: - keyword arguments. + keyword arguments for `pandas.read_csv` when not using the BigQuery engine. Returns: bigframes.dataframe.DataFrame: A BigQuery DataFrames. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def read_json( + self, + path_or_buf: str | IO["bytes"], + *, + orient: Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + dtype: Optional[Dict] = None, + encoding: Optional[str] = None, + lines: bool = False, + engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", + **kwargs, + ): + """ + Convert a JSON string to DataFrame object. + + .. note:: + using `engine="bigquery"` will not guarantee the same ordering as the + file. Instead, set a serialized index column as the index and sort by + that in the resulting DataFrame. + + Args: + path_or_buf (a valid JSON str, path object or file-like object): + A local or Google Cloud Storage (`gs://`) path with `engine="bigquery"` + otherwise passed to pandas.read_json. + orient (str, optional): + If `engine="bigquery"` orient only supports "records". + Indication of expected JSON string format. + Compatible JSON strings can be produced by ``to_json()`` with a + corresponding orient value. + The set of possible orients is: + + - ``'split'`` : dict like + ``{{index -> [index], columns -> [columns], data -> [values]}}`` + - ``'records'`` : list like + ``[{{column -> value}}, ... , {{column -> value}}]`` + - ``'index'`` : dict like ``{{index -> {{column -> value}}}}`` + - ``'columns'`` : dict like ``{{column -> {{index -> value}}}}`` + - ``'values'`` : just the values array + + dtype (bool or dict, default None): + If True, infer dtypes; if a dict of column to dtype, then use those; + if False, then don't infer dtypes at all, applies only to the data. + + For all ``orient`` values except ``'table'``, default is True. + encoding (str, default is 'utf-8'): + The encoding to use to decode py3 bytes. + lines (bool, default False): + Read the file as a json object per line. If using `engine="bigquery"` lines only supports True. + engine ({{"ujson", "pyarrow", "bigquery"}}, default "ujson"): + Type of engine to use. If `engine="bigquery"` is specified, then BigQuery's load API will be used. + Otherwise, the engine will be passed to `pandas.read_json`. + **kwargs: + keyword arguments for `pandas.read_json` when not using the BigQuery engine. + + Returns: + bigframes.dataframe.DataFrame: + The DataFrame representing JSON contents. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/io/pickle.py b/third_party/bigframes_vendored/pandas/io/pickle.py index a160ef0c4ea..71b31956a07 100644 --- a/third_party/bigframes_vendored/pandas/io/pickle.py +++ b/third_party/bigframes_vendored/pandas/io/pickle.py @@ -9,6 +9,8 @@ StorageOptions, ) +from bigframes import constants + class PickleIOMixin: def read_pickle( @@ -52,4 +54,4 @@ def read_pickle( bigframes.dataframe.DataFrame or bigframes.series.Series: same type as object stored in file. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index fc48cde85ba..847ad06f750 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -9,6 +9,8 @@ import inspect from typing import Any, Dict, List +from bigframes import constants + class BaseEstimator: """Base class for all estimators. @@ -94,7 +96,7 @@ def score(self, X, y): Returns: bigframes.dataframe.DataFrame: A DataFrame of the evaluation result. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class RegressorMixin: @@ -120,7 +122,7 @@ def score(self, X, y): Returns: bigframes.dataframe.DataFrame: A DataFrame of the evaluation result. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class TransformerMixin: diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index ea4df0dc023..ff1c04edbe3 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -14,6 +14,7 @@ from abc import ABC from typing import List, Optional +from bigframes import constants from third_party.bigframes_vendored.sklearn.base import BaseEstimator @@ -32,7 +33,7 @@ def predict(self, X): bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,), containing the class labels for each sample. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class KMeans(_BaseKMeans): @@ -65,7 +66,7 @@ def fit( Returns: KMeans: Fitted Estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def predict( self, @@ -80,7 +81,7 @@ def predict( Returns: bigframes.dataframe.DataFrame: DataFrame of the cluster each sample belongs to. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def score( self, @@ -98,7 +99,7 @@ def score( Returns: bigframes.dataframe.DataFrame: DataFrame of the metrics. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def cluster_centers_(self): @@ -119,4 +120,4 @@ def cluster_centers_(self): The output contains one row per feature per centroid. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py index bc8bc3980ac..dead173b2df 100644 --- a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py +++ b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py @@ -6,6 +6,7 @@ from abc import ABCMeta +from bigframes import constants from third_party.bigframes_vendored.sklearn.base import BaseEstimator @@ -43,7 +44,7 @@ def fit( Returns: ColumnTransformer: Fitted estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def transform( self, @@ -58,4 +59,4 @@ def transform( Returns: bigframes.dataframe.DataFrame: Transformed result. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 619c13f35d7..85feab00245 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -12,6 +12,7 @@ from abc import ABCMeta +from bigframes import constants from third_party.bigframes_vendored.sklearn.base import BaseEstimator @@ -55,7 +56,7 @@ def fit(self, X, y=None): Returns: PCA: Fitted estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def score(self, X=None, y=None): """Return the metrics of the model. @@ -69,7 +70,7 @@ def score(self, X=None, y=None): Returns: bigframes.dataframe.DataFrame: DataFrame that represents model metrics. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def predict(self, X): """Predict the closest cluster for each sample in X. @@ -80,4 +81,51 @@ def predict(self, X): Returns: bigframes.dataframe.DataFrame: predicted DataFrames.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def components_(self): + """Principal axes in feature space, representing the directions of maximum variance in the data. + + Returns: + bigframes.dataframe.DataFrame: DataFrame of principal components, containing following columns: + principal_component_id: An integer that identifies the principal component. + + feature: The column name that contains the feature. + + numerical_value: If feature is numeric, the value of feature for the principal component that principal_component_id identifies. If feature isn't numeric, the value is NULL. + + categorical_value: An list of mappings containing information about categorical features. Each mapping contains the following fields: + categorical_value.category: The name of each category. + + categorical_value.value: The value of categorical_value.category for the centroid that centroid_id identifies. + + The output contains one row per feature per component. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def explained_variance_(self): + """The amount of variance explained by each of the selected components. + + Returns: + bigframes.dataframe.DataFrame: DataFrame containing following columns: + principal_component_id: An integer that identifies the principal component. + + explained_variance: The factor by which the eigenvector is scaled. Eigenvalue and explained variance are the same concepts in PCA. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def explained_variance_ratio_(self): + """Percentage of variance explained by each of the selected components. + + Returns: + bigframes.dataframe.DataFrame: DataFrame containing following columns: + principal_component_id: An integer that identifies the principal component. + + explained_variance_ratio: the total variance is the sum of variances, also known as eigenvalues, of all + of the individual principal components. The explained variance ratio by a principal component is + the ratio between the variance, also known as eigenvalue, of that principal component and the total variance. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py index 73f4684dc30..79224a772d6 100644 --- a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -33,6 +33,8 @@ class calls the ``fit`` method of each sub-estimator on random samples from abc import ABCMeta +from bigframes import constants + from ..base import BaseEstimator, ClassifierMixin, MetaEstimatorMixin, RegressorMixin @@ -60,7 +62,7 @@ def fit(self, X, y): Returns: Fitted Estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class ForestRegressor(RegressorMixin, BaseForest, metaclass=ABCMeta): @@ -82,7 +84,7 @@ def predict(self, X): Returns: The predicted values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class RandomForestRegressor(ForestRegressor): @@ -148,7 +150,7 @@ def predict(self, X): Returns: The predicted values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class RandomForestClassifier(ForestClassifier): diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index 65e895298df..8141da4e3bc 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -18,6 +18,7 @@ from abc import ABCMeta from typing import List, Optional +from bigframes import constants from third_party.bigframes_vendored.sklearn.base import ( BaseEstimator, ClassifierMixin, @@ -36,7 +37,7 @@ def predict(self, X): Returns: bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,). Returns predicted values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class LinearClassifierMixin(ClassifierMixin): @@ -52,7 +53,7 @@ def predict(self, X): bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,), containing the class labels for each sample. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class LinearRegression(RegressorMixin, LinearModel): @@ -92,4 +93,4 @@ def fit( Returns: LinearRegression: Fitted Estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 8525e570689..a06035eef66 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -14,6 +14,7 @@ from typing import List, Optional +from bigframes import constants from third_party.bigframes_vendored.sklearn.linear_model._base import ( BaseEstimator, LinearClassifierMixin, @@ -57,4 +58,4 @@ def fit( Returns: LogisticRegression: Fitted Estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_classification.py b/third_party/bigframes_vendored/sklearn/metrics/_classification.py index 6d9692ac8d6..a9d8038e59b 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_classification.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_classification.py @@ -20,6 +20,8 @@ # Michal Karbownik # License: BSD 3 clause +from bigframes import constants + def accuracy_score(y_true, y_pred, normalize=True) -> float: """Accuracy classification score. @@ -39,7 +41,7 @@ def accuracy_score(y_true, y_pred, normalize=True) -> float: classified samples (float), else returns the number of correctly classified samples (int). """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def confusion_matrix( @@ -68,7 +70,7 @@ def confusion_matrix( samples with true label being i-th class and predicted label being j-th class. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def recall_score( @@ -99,7 +101,7 @@ def recall_score( of the positive class in binary classification or weighted average of the recall of each class for the multiclass task. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def precision_score( @@ -132,7 +134,7 @@ def precision_score( Precision of the positive class in binary classification or weighted average of the precision of each class for the multiclass task. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def f1_score( @@ -167,4 +169,4 @@ def f1_score( average of the F1 scores of each class for the multiclass task. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py index 693996070f0..ac919edbe33 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py @@ -16,6 +16,8 @@ # Michal Karbownik # License: BSD 3 clause +from bigframes import constants + def auc(x, y) -> float: """Compute Area Under the Curve (AUC) using the trapezoidal rule. @@ -35,7 +37,7 @@ def auc(x, y) -> float: Returns: float: Area Under the Curve. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def roc_auc_score(y_true, y_score) -> float: @@ -60,7 +62,7 @@ def roc_auc_score(y_true, y_score) -> float: Returns: float: Area Under the Curve score. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def roc_curve( @@ -95,4 +97,4 @@ def roc_curve( fpr and tpr. `thresholds[0]` represents no instances being predicted and is arbitrarily set to `max(y_score) + 1`. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_regression.py b/third_party/bigframes_vendored/sklearn/metrics/_regression.py index b90c4158878..9740c540e90 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_regression.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_regression.py @@ -24,6 +24,8 @@ # Ohad Michel # License: BSD 3 clause +from bigframes import constants + def r2_score(y_true, y_pred, force_finite=True) -> float: """:math:`R^2` (coefficient of determination) regression score function. @@ -49,4 +51,4 @@ def r2_score(y_true, y_pred, force_finite=True) -> float: Returns: float: The :math:`R^2` score. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/pipeline.py b/third_party/bigframes_vendored/sklearn/pipeline.py index f8bbae86df6..4b8eb25a973 100644 --- a/third_party/bigframes_vendored/sklearn/pipeline.py +++ b/third_party/bigframes_vendored/sklearn/pipeline.py @@ -11,6 +11,7 @@ from abc import ABCMeta +from bigframes import constants from third_party.bigframes_vendored.sklearn.base import BaseEstimator @@ -47,7 +48,7 @@ def fit( Returns: Pipeline: Pipeline with fitted steps. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def score(self, X, y): @@ -67,7 +68,7 @@ def score(self, X, y): DataFrame: A DataFrame representing the result of calling `score` on the final estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def predict(self, X): @@ -81,4 +82,4 @@ def predict(self, X): bigframes.dataframe.DataFrame: A Dataframe representing predicted result. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py index c57d1f22304..d0130434678 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -7,6 +7,7 @@ # Eric Chang # License: BSD 3 clause +from bigframes import constants from third_party.bigframes_vendored.sklearn.base import BaseEstimator @@ -59,7 +60,7 @@ def fit(self, X): Returns: StandardScaler: Fitted scaler. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def transform(self, X): """Perform standardization by centering and scaling. @@ -71,4 +72,4 @@ def transform(self, X): Returns: bigframes.dataframe.DataFrame: Transformed result. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index a6c32d91c1d..b1cf17e539d 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -2,6 +2,7 @@ # Joris Van den Bossche # License: BSD 3 clause +from bigframes import constants from third_party.bigframes_vendored.sklearn.base import BaseEstimator @@ -61,7 +62,7 @@ def fit(self, X): Returns: OneHotEncoder: Fitted encoder. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def transform(self, X): """Transform X using one-hot encoding. @@ -73,4 +74,4 @@ def transform(self, X): Returns: bigframes.dataframe.DataFrame: The result is categorized as index: number, value: number. Where index is the position of the dict that seeing the category, and value is 0 or 1.""" - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index fcb5d2ec593..620c87fa3db 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -2,6 +2,8 @@ from typing import Any +from bigframes import constants + from ..sklearn.base import BaseEstimator as XGBModelBase from ..sklearn.base import ClassifierMixin as XGBClassifierBase from ..sklearn.base import RegressorMixin as XGBRegressorBase @@ -18,7 +20,7 @@ def predict(self, X): Returns: DataFrame of shape (n_samples,): Returns predicted values. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def fit(self, X, y): """Fit gradient boosting model. @@ -42,7 +44,7 @@ def fit(self, X, y): Returns: XGBModel: Fitted Estimator. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class XGBClassifierMixIn: From 7011a127722514fc562ff5a7ee259a5904f8c9ab Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 29 Aug 2023 11:40:36 -0500 Subject: [PATCH 008/921] chore: add templated GitHub config files (#12) Pulled manually from https://github.com/googleapis/synthtool/tree/master/synthtool/gcp/templates/python_library/.github Change-Id: I7f36912dffb427af2cd388abb8109670ac162701 --- .github/CODEOWNERS | 11 ++++ .github/CONTRIBUTING.md | 28 ++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 43 ++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 18 ++++++ .github/ISSUE_TEMPLATE/support_request.md | 7 +++ .github/PULL_REQUEST_TEMPLATE.md | 7 +++ .github/auto-approve.yml | 3 + .github/auto-label.yaml | 15 +++++ .github/header-checker-lint.yml | 15 +++++ .github/release-please.yml | 2 + .github/release-trigger.yml | 1 + .github/snippet-bot.yml | 0 .github/sync-repo-settings.yaml | 31 +++++++++++ .github/workflows/docs.yml | 38 +++++++++++++ .github/workflows/lint.yml | 25 +++++++++ .github/workflows/unittest.yml | 57 +++++++++++++++++++ SECURITY.md | 7 +++ owlbot.py | 68 +++++++++++++++++++++++ 18 files changed, 376 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/support_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/auto-approve.yml create mode 100644 .github/auto-label.yaml create mode 100644 .github/header-checker-lint.yml create mode 100644 .github/release-please.yml create mode 100644 .github/release-trigger.yml create mode 100644 .github/snippet-bot.yml create mode 100644 .github/sync-repo-settings.yaml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/unittest.yml create mode 100644 SECURITY.md create mode 100644 owlbot.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..cd904459a91 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. +# +# For syntax help see: +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax +# Note: This file is autogenerated. To make changes to the codeowner team, please update .repo-metadata.json. +# @googleapis/yoshi-python @googleapis/api-bigquery-dataframe are the default owners for changes in this repo +* @googleapis/yoshi-python @googleapis/api-bigquery-dataframe + +# @googleapis/python-samples-reviewers @googleapis/api-bigquery-dataframe are the default owners for samples changes +/samples/ @googleapis/python-samples-reviewers @googleapis/api-bigquery-dataframe diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..939e5341e74 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google.com/conduct/). diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..7b0900728e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +Thanks for stopping by to let us know something could be better! + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + +Please run down the following list and make sure you've tried the usual "quick fixes": + + - Search the issues already opened: https://github.com/googleapis/python-bigquery-dataframes/issues + - Search StackOverflow: https://stackoverflow.com/questions/tagged/google-cloud-platform+python + +If you are still having issues, please be sure to include as much information as possible: + +#### Environment details + + - OS type and version: + - Python version: `python --version` + - pip version: `pip --version` + - `bigframes` version: `pip show bigframes` + +#### Steps to reproduce + + 1. ? + 2. ? + +#### Code example + +```python +# example +``` + +#### Stack trace +``` +# example +``` + +Making sure to follow these steps will guarantee the quickest resolution possible. + +Thanks! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..6365857f33c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Suggest an idea for this library + +--- + +Thanks for stopping by to let us know something could be better! + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + + **Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + **Describe the solution you'd like** +A clear and concise description of what you want to happen. + **Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + **Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 00000000000..99586903212 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,7 @@ +--- +name: Support request +about: If you have a support contract with Google, please create an issue in the Google Cloud Support console. + +--- + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..3e59d9a70d1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: +- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Ensure the tests and linter pass +- [ ] Code coverage does not decrease (if any source code was changed) +- [ ] Appropriate docs were updated (if necessary) + +Fixes # 🦕 diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml new file mode 100644 index 00000000000..311ebbb853a --- /dev/null +++ b/.github/auto-approve.yml @@ -0,0 +1,3 @@ +# https://github.com/googleapis/repo-automation-bots/tree/main/packages/auto-approve +processes: + - "OwlBotTemplateChanges" diff --git a/.github/auto-label.yaml b/.github/auto-label.yaml new file mode 100644 index 00000000000..b2016d119b4 --- /dev/null +++ b/.github/auto-label.yaml @@ -0,0 +1,15 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +requestsize: + enabled: true diff --git a/.github/header-checker-lint.yml b/.github/header-checker-lint.yml new file mode 100644 index 00000000000..3058bec338c --- /dev/null +++ b/.github/header-checker-lint.yml @@ -0,0 +1,15 @@ +{"allowedCopyrightHolders": ["Google LLC"], + "allowedLicenses": ["Apache-2.0", "MIT", "BSD-3"], + "ignoreFiles": ["**/requirements.txt", "**/requirements-test.txt", "**/__init__.py", "samples/**/constraints.txt", "samples/**/constraints-test.txt"], + "sourceFileExtensions": [ + "ts", + "js", + "java", + "sh", + "Dockerfile", + "yaml", + "py", + "html", + "txt" + ] +} diff --git a/.github/release-please.yml b/.github/release-please.yml new file mode 100644 index 00000000000..466597e5b19 --- /dev/null +++ b/.github/release-please.yml @@ -0,0 +1,2 @@ +releaseType: python +handleGHRelease: true diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml new file mode 100644 index 00000000000..d4ca94189e1 --- /dev/null +++ b/.github/release-trigger.yml @@ -0,0 +1 @@ +enabled: true diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 00000000000..5004e872b4c --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,31 @@ +# https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings +# Rules for main branch protection +branchProtectionRules: +# Identifies the protection rule pattern. Name of the branch to be protected. +# Defaults to `main` +- pattern: main + requiresCodeOwnerReviews: true + requiresStrictStatusChecks: true + requiredStatusCheckContexts: + - 'cla/google' + - 'OwlBot Post Processor' + - 'docs' + - 'lint' + - 'unit (3.9)' + - 'unit (3.10)' + - 'unit (3.11)' + - 'cover' + # TODO(tswast): add Kokoro once we've enabled it +permissionRules: + - team: actools-python + permission: admin + - team: actools + permission: admin + - team: api-bigquery-dataframe + permission: push + - team: yoshi-python + permission: push + - team: python-samples-owners + permission: push + - team: python-samples-reviewers + permission: push diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000000..e97d89e484c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,38 @@ +on: + pull_request: + branches: + - main +name: docs +jobs: + docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.9" + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run docs + run: | + nox -s docs + docfx: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.9" + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run docfx + run: | + nox -s docfx diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000000..d2aee5b7d8e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +on: + pull_request: + branches: + - main +name: lint +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run lint + run: | + nox -s lint + - name: Run lint_setup_py + run: | + nox -s lint_setup_py diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 00000000000..bb268fe6c66 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,57 @@ +on: + pull_request: + branches: + - main +name: unittest +jobs: + unit: + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run unit tests + env: + COVERAGE_FILE: .coverage-${{ matrix.python }} + run: | + nox -s unit-${{ matrix.python }} + - name: Upload coverage results + uses: actions/upload-artifact@v3 + with: + name: coverage-artifacts + path: .coverage-${{ matrix.python }} + + cover: + runs-on: ubuntu-latest + needs: + - unit + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install coverage + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install coverage + - name: Download coverage results + uses: actions/download-artifact@v3 + with: + name: coverage-artifacts + path: .coverage-results/ + - name: Report coverage results + run: | + coverage combine .coverage-results/.coverage* + coverage report --show-missing --fail-under=40 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..8b58ae9c01a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). + +The Google Security Team will respond within 5 working days of your report on g.co/vulnz. + +We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. diff --git a/owlbot.py b/owlbot.py new file mode 100644 index 00000000000..4ba7d14eb5e --- /dev/null +++ b/owlbot.py @@ -0,0 +1,68 @@ +# Copyright 2021 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. + +"""This script is used to synthesize generated parts of this library.""" + +import pathlib + +from synthtool import gcp +import synthtool as s +from synthtool.languages import python + +REPO_ROOT = pathlib.Path(__file__).parent.absolute() + +common = gcp.CommonTemplates() + +# ---------------------------------------------------------------------------- +# Add templated files +# ---------------------------------------------------------------------------- + +templated_files = common.py_library( + unit_test_python_versions=["3.9", "3.10", "3.11"], + system_test_python_versions=["3.9", "3.11"], + cov_level=40, + intersphinx_dependencies={ + "pandas": "https://pandas.pydata.org/pandas-docs/stable/", + "pydata-google-auth": "https://pydata-google-auth.readthedocs.io/en/latest/", + }, +) +s.move( + templated_files, + excludes=[ + # Multi-processing note isn't relevant, as pandas_gbq is responsible for + # creating clients, not the end user. + "docs/multiprocessing.rst", + "noxfile.py", + "README.rst", + ], +) + +# ---------------------------------------------------------------------------- +# Fixup files +# ---------------------------------------------------------------------------- + + +# ---------------------------------------------------------------------------- +# Samples templates +# ---------------------------------------------------------------------------- + +python.py_samples(skip_readmes=True) + +# ---------------------------------------------------------------------------- +# Final cleanup +# ---------------------------------------------------------------------------- + +s.shell.run(["nox", "-s", "blacken"], hide_output=False) +for noxfile in REPO_ROOT.glob("samples/**/noxfile.py"): + s.shell.run(["nox", "-s", "blacken"], cwd=noxfile.parent, hide_output=False) From bf6ecb81afeb199b3dad07d1fd2057668352f939 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:16:44 -0700 Subject: [PATCH 009/921] chore(deps): bump cryptography from 41.0.1 to 41.0.3 in /.kokoro (#3) Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.1 to 41.0.3. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/41.0.1...41.0.3) --- updated-dependencies: - dependency-name: cryptography dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .kokoro/requirements.txt | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 37a007667f1..fd0e0b9d489 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -123,26 +123,30 @@ commonmark==0.9.1 \ --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 # via recommonmark -cryptography==41.0.1 \ - --hash=sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db \ - --hash=sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a \ - --hash=sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039 \ - --hash=sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c \ - --hash=sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3 \ - --hash=sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485 \ - --hash=sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c \ - --hash=sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca \ - --hash=sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5 \ - --hash=sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5 \ - --hash=sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3 \ - --hash=sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb \ - --hash=sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43 \ - --hash=sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31 \ - --hash=sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc \ - --hash=sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b \ - --hash=sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006 \ - --hash=sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a \ - --hash=sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699 +cryptography==41.0.3 \ + --hash=sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306 \ + --hash=sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84 \ + --hash=sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47 \ + --hash=sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d \ + --hash=sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116 \ + --hash=sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207 \ + --hash=sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81 \ + --hash=sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087 \ + --hash=sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd \ + --hash=sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507 \ + --hash=sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858 \ + --hash=sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae \ + --hash=sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34 \ + --hash=sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906 \ + --hash=sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd \ + --hash=sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922 \ + --hash=sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7 \ + --hash=sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4 \ + --hash=sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574 \ + --hash=sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1 \ + --hash=sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c \ + --hash=sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e \ + --hash=sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de # via # gcp-releasetool # secretstorage @@ -416,6 +420,7 @@ protobuf==3.20.3 \ # gcp-docuploader # gcp-releasetool # google-api-core + # googleapis-common-protos pyasn1==0.5.0 \ --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \ --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde From cccac8c16c2daeb4f2defe4562cf099b1c738e07 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:42:04 -0700 Subject: [PATCH 010/921] chore: set up the kokoro release pipeline (#14) --- .kokoro/release.sh | 29 ++++++++++++++++++++++ .kokoro/release/common.cfg | 49 +++++++++++++++++++++++++++++++++++++ .kokoro/release/release.cfg | 1 + 3 files changed, 79 insertions(+) create mode 100644 .kokoro/release.sh create mode 100644 .kokoro/release/common.cfg create mode 100644 .kokoro/release/release.cfg diff --git a/.kokoro/release.sh b/.kokoro/release.sh new file mode 100644 index 00000000000..697b04009db --- /dev/null +++ b/.kokoro/release.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +set -eo pipefail + +# Start the releasetool reporter +python3 -m pip install --require-hashes -r github/python-bigquery-dataframes/.kokoro/requirements.txt +python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script + +# Disable buffering, so that the logs stream through. +export PYTHONUNBUFFERED=1 + +# Move into the package, build the distribution and upload. +TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-1") +cd github/python-bigquery-dataframes +python3 setup.py sdist bdist_wheel +twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* \ No newline at end of file diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg new file mode 100644 index 00000000000..7ffa79c7a1d --- /dev/null +++ b/.kokoro/release/common.cfg @@ -0,0 +1,49 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-multi" +} +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/release.sh" +} + +# Fetch PyPI password +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "google-cloud-pypi-token-keystore-1" + } + } +} + +# Tokens needed to report release status back to GitHub +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" +} + +# Store the packages we uploaded to PyPI. That way, we have a record of exactly +# what we published, which we can use to generate SBOMs and attestations. +action { + define_artifacts { + regex: "github/python-bigquery-dataframes/**/*.tar.gz" + strip_prefix: "github/python-bigquery-dataframes" + } +} \ No newline at end of file diff --git a/.kokoro/release/release.cfg b/.kokoro/release/release.cfg new file mode 100644 index 00000000000..8f43917d92f --- /dev/null +++ b/.kokoro/release/release.cfg @@ -0,0 +1 @@ +# Format: //devtools/kokoro/config/proto/build.proto \ No newline at end of file From 8fab75576757230bca5c7df10994837ac406300f Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Sat, 2 Sep 2023 09:14:42 -0500 Subject: [PATCH 011/921] chore: sync changes from internal repo (#15) feat: support `DataFrame.isin` with list and dict inputs test: move flaky `reset_session` test to unit tests chore: don't run redundant tests in nightly build feat: support `DataFrame`-`DataFrame` binary operations feat: support `Series.map` feat: support `Index.is_monotonic` docs: update remote function notebook with read_gbq_function usage feat: use default session and connection in `ml.llm` and `ml.imported` chore: disable broken stack tests feat: support `pow()` and power operator in `DataFrame` and `Series` feat: support for `np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.power` perf: use `row_number()` filter for `head` / `tail` feat: support `bigframes.pandas.merge()` fix: make `X_train` argument names consistent across methods chore: refactor ml core feat: add `Series.dropna` and `DataFrame.fillna` chore: fix gcs notebooks upload in 'nightly' build chore: fix Kokoro build files to support GitHub directories chore: fix unit test to not require authentication --- .kokoro/build.sh | 8 +- .kokoro/continuous/nightly.cfg | 5 - .kokoro/release-nightly.sh | 29 +- OWNERS | 1 + bigframes/_config/bigquery_options.py | 22 +- bigframes/clients.py | 163 ++++ bigframes/core/__init__.py | 14 - bigframes/core/blocks.py | 71 ++ bigframes/core/indexes/index.py | 30 + bigframes/core/joins/__init__.py | 2 + bigframes/core/joins/merge.py | 67 ++ bigframes/dataframe.py | 180 ++++- bigframes/ml/cluster.py | 2 +- bigframes/ml/core.py | 58 +- bigframes/ml/decomposition.py | 2 +- bigframes/ml/imported.py | 16 +- bigframes/ml/llm.py | 88 +- bigframes/ml/sql.py | 16 +- bigframes/operations/__init__.py | 123 ++- bigframes/pandas/__init__.py | 34 +- bigframes/remote_function.py | 115 +-- bigframes/series.py | 118 ++- bigframes/session.py | 38 +- notebooks/experimental/longer_ml_demo.ipynb | 34 +- .../getting_started/ml_fundamentals.ipynb | 58 +- .../sklearn_linear_regression.ipynb | 8 +- .../remote_functions/remote_function.ipynb | 749 +++++++++++++----- tests/system/conftest.py | 18 +- tests/system/large/ml/test_core.py | 16 +- tests/system/large/ml/test_ensemble.py | 64 +- tests/system/large/ml/test_forecasting.py | 6 +- tests/system/large/ml/test_linear_model.py | 32 +- tests/system/small/ml/conftest.py | 40 +- tests/system/small/ml/test_ensemble.py | 72 +- tests/system/small/ml/test_imported.py | 10 + tests/system/small/ml/test_linear_model.py | 36 +- tests/system/small/ml/test_llm.py | 28 + tests/system/small/test_dataframe.py | 133 +++- tests/system/small/test_index.py | 14 + tests/system/small/test_multiindex.py | 68 +- tests/system/small/test_numpy.py | 66 ++ tests/system/small/test_pandas.py | 104 +++ tests/system/small/test_pandas_options.py | 43 - tests/system/small/test_remote_function.py | 35 +- tests/system/small/test_series.py | 89 +++ tests/unit/_config/test_bigquery_options.py | 4 +- tests/unit/conftest.py | 223 ------ .../core/{test_utils.py => test_bf_utils.py} | 0 tests/unit/resources.py | 73 ++ tests/unit/test_core.py | 62 +- tests/unit/test_pandas.py | 39 + tests/unit/test_session.py | 15 +- .../bigframes_vendored/pandas/core/frame.py | 98 +++ .../pandas/core/reshape/merge.py | 78 ++ .../bigframes_vendored/pandas/core/series.py | 102 ++- 55 files changed, 2617 insertions(+), 1002 deletions(-) create mode 100644 bigframes/clients.py create mode 100644 bigframes/core/joins/merge.py delete mode 100644 tests/unit/conftest.py rename tests/unit/core/{test_utils.py => test_bf_utils.py} (100%) create mode 100644 tests/unit/resources.py create mode 100644 third_party/bigframes_vendored/pandas/core/reshape/merge.py diff --git a/.kokoro/build.sh b/.kokoro/build.sh index f80cf6eab97..402ac0eb8c9 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -15,8 +15,14 @@ set -eo pipefail +if [[ -z "${KOKORO_GOB_COMMIT}" ]]; then + PROJECT_SCM="github" +else + PROJECT_SCM="git" +fi + if [[ -z "${PROJECT_ROOT:-}" ]]; then - PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/git/bigframes" + PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/${PROJECT_SCM}/bigframes" fi cd "${PROJECT_ROOT}" diff --git a/.kokoro/continuous/nightly.cfg b/.kokoro/continuous/nightly.cfg index ac34c4b0c60..63c3f51d053 100644 --- a/.kokoro/continuous/nightly.cfg +++ b/.kokoro/continuous/nightly.cfg @@ -1,8 +1,3 @@ # Format: //devtools/kokoro/config/proto/build.proto -env_vars: { - key: "NOX_SESSION" - value: "unit system cover lint lint_setup_py mypy format docs e2e notebook" -} - build_file: "bigframes/.kokoro/release-nightly.sh" diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh index e3b6b4d4498..582808a15ce 100755 --- a/.kokoro/release-nightly.sh +++ b/.kokoro/release-nightly.sh @@ -34,8 +34,14 @@ while [ $# -gt 0 ] ; do shift 1; done +if [[ -z "${KOKORO_GOB_COMMIT}" ]]; then + PROJECT_SCM="github" +else + PROJECT_SCM="git" +fi + if [ -z "${PROJECT_ROOT:-}" ]; then - PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/git/bigframes" + PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/${PROJECT_SCM}/bigframes" fi # Move into the package, build the distribution and upload to shared bucket. @@ -57,16 +63,6 @@ export PYTHONUNBUFFERED=1 # Install dependencies, as the following steps depend on it python3.10 -m pip install -e .[all] -# If NOX_SESSION is set, it only runs the specified session, -# otherwise run all the sessions. -if ! [ ${DRY_RUN} ]; then - if [ -n "${NOX_SESSION:-}" ]; then - python3.10 -m nox -s ${NOX_SESSION:-} - else - python3.10 -m nox - fi -fi - # Generate third party notices and include it in the licenses in setup.cfg # TODO(shobs): Don't include it in the package once vertex colab can pick it # from elsewhere @@ -138,15 +134,8 @@ if ! [ ${DRY_RUN} ]; then gsutil cp -v dist/* ${gcs_path} gsutil cp -v LICENSE ${gcs_path} gsutil cp -v ${THIRD_PARTY_NOTICES_FILE} ${gcs_path} - gsutil -m cp -v "notebooks/00 - Summary.ipynb" \ - "notebooks/01 - Getting Started.ipynb" \ - "notebooks/02 - DataFrame.ipynb" \ - "notebooks/03 - Using ML - ML fundamentals.ipynb" \ - "notebooks/04 - Using ML - SKLearn linear regression.ipynb" \ - "notebooks/05 - Using ML - Easy linear regression.ipynb" \ - "notebooks/06 - Using ML - Large Language Models.ipynb" \ - "notebooks/50 - Remote Function.ipynb" \ - ${gcs_path}notebooks/ + gsutil -m cp -r -v "notebooks/" ${gcs_path}notebooks/ + done # publish API coverage information to BigQuery diff --git a/OWNERS b/OWNERS index f2a0b4383d1..672da38afa5 100644 --- a/OWNERS +++ b/OWNERS @@ -5,6 +5,7 @@ henryjsolberg@google.com hormati@google.com huanc@google.com jiaxun@google.com +kemppeterson@google.com shobs@google.com swast@google.com tbergeron@google.com diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index a103abe1903..ea1864ed5f7 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -35,13 +35,13 @@ def __init__( credentials: Optional[google.auth.credentials.Credentials] = None, project: Optional[str] = None, location: Optional[str] = None, - remote_udf_connection: Optional[str] = None, + bq_connection: Optional[str] = None, use_regional_endpoints: bool = False, ): self._credentials = credentials self._project = project self._location = location - self._remote_udf_connection = remote_udf_connection + self._bq_connection = bq_connection self._use_regional_endpoints = use_regional_endpoints self._session_started = False @@ -82,23 +82,21 @@ def project(self, value: Optional[str]): self._project = value @property - def remote_udf_connection(self) -> Optional[str]: - """Name of the BigQuery connection to use for remote functions. + def bq_connection(self) -> Optional[str]: + """Name of the BigQuery connection to use. You should either have the connection already created in the location you have chosen, or you should have the Project IAM Admin role to enable the service to create the connection for you if you need it. """ - return self._remote_udf_connection + return self._bq_connection - @remote_udf_connection.setter - def remote_udf_connection(self, value: Optional[str]): - if self._session_started and self._remote_udf_connection != value: - raise ValueError( - SESSION_STARTED_MESSAGE.format(attribute="remote_udf_connection") - ) - self._remote_udf_connection = value + @bq_connection.setter + def bq_connection(self, value: Optional[str]): + if self._session_started and self._bq_connection != value: + raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="bq_connection")) + self._bq_connection = value @property def use_regional_endpoints(self) -> bool: diff --git a/bigframes/clients.py b/bigframes/clients.py new file mode 100644 index 00000000000..5c019e0fc89 --- /dev/null +++ b/bigframes/clients.py @@ -0,0 +1,163 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BigQuery DataFrame clients to interact with other cloud resources""" + +from __future__ import annotations + +import logging +import time +from typing import Optional + +import google.api_core.exceptions +from google.cloud import bigquery_connection_v1, resourcemanager_v3 +from google.iam.v1 import iam_policy_pb2, policy_pb2 + +logging.basicConfig( + level=logging.INFO, format="[%(levelname)s][%(asctime)s][%(name)s] %(message)s" +) +logger = logging.getLogger(__name__) + + +class BqConnectionManager: + """Manager to handle operations with BQ connections.""" + + # Wait time (in seconds) for an IAM binding to take effect after creation + _IAM_WAIT_SECONDS = 120 + + def __init__( + self, + bq_connection_client: bigquery_connection_v1.ConnectionServiceClient, + cloud_resource_manager_client: resourcemanager_v3.ProjectsClient, + ): + self._bq_connection_client = bq_connection_client + self._cloud_resource_manager_client = cloud_resource_manager_client + + def create_bq_connection( + self, project_id: str, location: str, connection_id: str, iam_role: str + ): + """Create the BQ connection if not exist. In addition, try to add the IAM role to the connection to ensure required permissions. + + Args: + project_id: + ID of the project. + location: + Location of the connection. + connection_id: + ID of the connection. + iam_role: + str of the IAM role that the service account of the created connection needs to aquire. E.g. 'run.invoker', 'aiplatform.user' + """ + # TODO(shobs): The below command to enable BigQuery Connection API needs + # to be automated. Disabling for now since most target users would not + # have the privilege to enable API in a project. + # log("Making sure BigQuery Connection API is enabled") + # if os.system("gcloud services enable bigqueryconnection.googleapis.com"): + # raise ValueError("Failed to enable BigQuery Connection API") + # If the intended connection does not exist then create it + service_account_id = self._get_service_account_if_connection_exists( + project_id, location, connection_id + ) + if service_account_id: + logger.info( + f"Connector {project_id}.{location}.{connection_id} already exists" + ) + else: + connection_name, service_account_id = self._create_bq_connection( + project_id, location, connection_id + ) + logger.info( + f"Created BQ connection {connection_name} with service account id: {service_account_id}" + ) + # Ensure IAM role on the 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'. + @google.api_core.retry.Retry( + predicate=google.api_core.retry.if_exception_type( + google.api_core.exceptions.Aborted + ), + initial=10, + maximum=20, + multiplier=2, + timeout=60, + ) + def _ensure_iam_binding( + self, project_id: str, service_account_id: str, iam_role: str + ): + """Ensure necessary IAM role is configured on a service account.""" + project = f"projects/{project_id}" + service_account = f"serviceAccount:{service_account_id}" + role = f"roles/{iam_role}" + request = iam_policy_pb2.GetIamPolicyRequest(resource=project) + policy = self._cloud_resource_manager_client.get_iam_policy(request=request) + + # Check if the binding already exists, and if does, do nothing more + for binding in policy.bindings: + if binding.role == role: + if service_account in binding.members: + return + + # Create a new binding + new_binding = policy_pb2.Binding(role=role, members=[service_account]) + policy.bindings.append(new_binding) + request = iam_policy_pb2.SetIamPolicyRequest(resource=project, policy=policy) + self._cloud_resource_manager_client.set_iam_policy(request=request) + + # We would wait for the IAM policy change to take effect + # https://cloud.google.com/iam/docs/access-change-propagation + logger.info( + f"Waiting {self._IAM_WAIT_SECONDS} seconds for IAM to take effect.." + ) + time.sleep(self._IAM_WAIT_SECONDS) + + def _create_bq_connection(self, project_id: str, location: str, connection_id: str): + """Create the BigQuery Connection and returns corresponding service account id.""" + client = self._bq_connection_client + connection = bigquery_connection_v1.Connection( + cloud_resource=bigquery_connection_v1.CloudResourceProperties() + ) + request = bigquery_connection_v1.CreateConnectionRequest( + parent=client.common_location_path(project_id, location), + connection_id=connection_id, + connection=connection, + ) + connection = client.create_connection(request) + return connection.name, connection.cloud_resource.service_account_id + + def _get_service_account_if_connection_exists( + self, project_id: str, location: str, connection_id: str + ) -> Optional[str]: + """Check if the BigQuery Connection exists.""" + client = self._bq_connection_client + request = bigquery_connection_v1.GetConnectionRequest( + name=client.connection_path(project_id, location, connection_id) + ) + + service_account = None + try: + service_account = client.get_connection( + request=request + ).cloud_resource.service_account_id + except google.api_core.exceptions.NotFound: + pass + + return service_account diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index d6509e4c0a9..7086269af9b 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -1021,20 +1021,6 @@ def slice( if not step: step = 1 - # Special cases for head() and tail(), where we don't need to project - # offsets. LIMIT clause is much more efficient in BigQuery than a - # filter on row_number(). - if ( - (start is None or start == 0) - and step == 1 - and stop is not None - and stop > 0 - ): - return self.apply_limit(stop) - - if start is not None and start < 0 and step == 1 and stop is None: - return self.reversed().apply_limit(abs(start)).reversed() - expr_with_offsets = self.project_offsets() # start with True and reduce with start, stop, and step conditions diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index f23a4d0b5c9..482cfd0141e 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -62,6 +62,10 @@ _UNIFORM = "uniform" _SAMPLING_METHODS = (_HEAD, _UNIFORM) +# Monotonic Cache Names +_MONOTONIC_INCREASING = "monotonic_increasing" +_MONOTONIC_DECREASING = "monotonic_decreasing" + class BlockHolder(typing.Protocol): """Interface for mutable objects with state represented by a block value object.""" @@ -119,6 +123,8 @@ def __init__( } # TODO(kemppeterson) Add a cache for corr to parallel the single-column stats. + self._stats_cache[" ".join(self.index_columns)] = {} + @property def index(self) -> indexes.IndexValue: """Row identities for values in the Block.""" @@ -1408,6 +1414,71 @@ def _force_reproject(self) -> Block: index_labels=self.index.names, ) + def is_monotonic_increasing( + self, column_id: typing.Union[str, Sequence[str]] + ) -> bool: + return self._is_monotonic(column_id, increasing=True) + + def is_monotonic_decreasing( + self, column_id: typing.Union[str, Sequence[str]] + ) -> bool: + return self._is_monotonic(column_id, increasing=False) + + def _is_monotonic( + self, column_ids: typing.Union[str, Sequence[str]], increasing: bool + ) -> bool: + if isinstance(column_ids, str): + column_ids = (column_ids,) + + op_name = _MONOTONIC_INCREASING if increasing else _MONOTONIC_DECREASING + + column_name = " ".join(column_ids) + if op_name in self._stats_cache[column_name]: + return self._stats_cache[column_name][op_name] + + period = 1 + window = bigframes.core.WindowSpec( + preceding=period, + following=None, + ) + + # any NaN value means not monotonic + block, last_notna_id = self.apply_unary_op(column_ids[0], ops.notnull_op) + for column_id in column_ids[1:]: + block, notna_id = block.apply_unary_op(column_id, ops.notnull_op) + block, last_notna_id = block.apply_binary_op( + last_notna_id, notna_id, ops.and_op + ) + + # loop over all columns to check monotonicity + last_result_id = None + for column_id in column_ids[::-1]: + block, lag_result_id = block.apply_window_op( + column_id, agg_ops.ShiftOp(period), window + ) + block, strict_monotonic_id = block.apply_binary_op( + column_id, lag_result_id, ops.gt_op if increasing else ops.lt_op + ) + block, equal_id = block.apply_binary_op(column_id, lag_result_id, ops.eq_op) + if last_result_id is None: + block, last_result_id = block.apply_binary_op( + equal_id, strict_monotonic_id, ops.or_op + ) + continue + block, equal_monotonic_id = block.apply_binary_op( + equal_id, last_result_id, ops.and_op + ) + block, last_result_id = block.apply_binary_op( + equal_monotonic_id, strict_monotonic_id, ops.or_op + ) + + block, monotonic_result_id = block.apply_binary_op( + last_result_id, last_notna_id, ops.and_op # type: ignore + ) + result = block.get_stat(monotonic_result_id, agg_ops.all_op) + self._stats_cache[column_name].update({op_name: result}) + return result + def block_from_local(data, session=None, use_index=True) -> Block: # TODO(tbergeron): Handle duplicate column labels diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 4eb37e6d926..04b9a36b64a 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -67,6 +67,36 @@ def empty(self) -> bool: """Returns True if the Index is empty, otherwise returns False.""" return self.shape[0] == 0 + @property + def is_monotonic_increasing(self) -> bool: + """ + Return a boolean if the values are equal or increasing. + + Returns: + bool + """ + return typing.cast( + bool, + self._data._get_block().is_monotonic_increasing( + self._data._get_block().index_columns + ), + ) + + @property + def is_monotonic_decreasing(self) -> bool: + """ + Return a boolean if the values are equal or decreasing. + + Returns: + bool + """ + return typing.cast( + bool, + self._data._get_block().is_monotonic_decreasing( + self._data._get_block().index_columns + ), + ) + def __getitem__(self, key: int) -> typing.Any: if isinstance(key, int): result_pd_df, _ = self._data._get_block().slice(key, key + 1, 1).to_pandas() diff --git a/bigframes/core/joins/__init__.py b/bigframes/core/joins/__init__.py index 58a1c2cfd79..3f9447aef0d 100644 --- a/bigframes/core/joins/__init__.py +++ b/bigframes/core/joins/__init__.py @@ -14,10 +14,12 @@ """Helpers to join ArrayValue objects.""" +from bigframes.core.joins.merge import merge from bigframes.core.joins.row_identity import join_by_row_identity from bigframes.core.joins.single_column import join_by_column __all__ = ( "join_by_row_identity", "join_by_column", + "merge", ) diff --git a/bigframes/core/joins/merge.py b/bigframes/core/joins/merge.py new file mode 100644 index 00000000000..fac16b36078 --- /dev/null +++ b/bigframes/core/joins/merge.py @@ -0,0 +1,67 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Functions for Merging Data Structures in BigFrames. +""" + +from __future__ import annotations + +from typing import Literal, Optional + +from bigframes.dataframe import DataFrame +from bigframes.series import Series + + +def merge( + left: DataFrame, + right: DataFrame, + how: Literal[ + "inner", + "left", + "outer", + "right", + ] = "inner", + on: Optional[str] = None, + *, + left_on: Optional[str] = None, + right_on: Optional[str] = None, + sort: bool = False, + suffixes: tuple[str, str] = ("_x", "_y"), +) -> DataFrame: + left = _validate_operand(left) + right = _validate_operand(right) + + return left.merge( + right, + how=how, + on=on, + left_on=left_on, + right_on=right_on, + sort=sort, + suffixes=suffixes, + ) + + +def _validate_operand(obj: DataFrame | Series) -> DataFrame: + if isinstance(obj, DataFrame): + return obj + elif isinstance(obj, Series): + if obj.name is None: + raise ValueError("Cannot merge a Series without a name") + return obj.to_frame() + else: + raise TypeError( + f"Can only merge Series or DataFrame objects, a {type(obj)} was passed" + ) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ef443db0797..31777f3face 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -529,7 +529,7 @@ def __setitem__(self, key: str, value: SingleItemValue): def _apply_binop( self, - other: float | int | bigframes.series.Series, + other: float | int | bigframes.series.Series | DataFrame, op, axis: str | int = "columns", ): @@ -537,6 +537,8 @@ def _apply_binop( return self._apply_scalar_binop(other, op) elif isinstance(other, bigframes.series.Series): return self._apply_series_binop(other, op, axis=axis) + elif isinstance(other, DataFrame): + return self._apply_dataframe_binop(other, op) raise NotImplementedError( f"binary operation is not implemented on the second operand of type {type(other).__name__}." f"{constants.FEEDBACK_LINK}" @@ -588,6 +590,47 @@ def _apply_series_binop( block = block.with_index_labels(self.index.names) return DataFrame(block) + def _apply_dataframe_binop( + self, + other: DataFrame, + op: ops.BinaryOp, + ) -> DataFrame: + # Join rows + joined_index, (get_column_left, get_column_right) = self._block.index.join( + other._block.index, how="outer" + ) + # join columns schema + columns, lcol_indexer, rcol_indexer = self.columns.join( + other.columns, how="outer", return_indexers=True + ) + + binop_result_ids = [] + block = joined_index._block + for left_index, right_index in zip(lcol_indexer, rcol_indexer): + if left_index >= 0 and right_index >= 0: # -1 indices indicate missing + left_col_id = self._block.value_columns[left_index] + right_col_id = other._block.value_columns[right_index] + block, result_col_id = block.apply_binary_op( + get_column_left(left_col_id), + get_column_right(right_col_id), + op, + ) + binop_result_ids.append(result_col_id) + elif left_index >= 0: + dtype = self.dtypes[left_index] + block, null_col_id = block.create_constant(None, dtype=dtype) + binop_result_ids.append(null_col_id) + elif right_index >= 0: + dtype = other.dtypes[right_index] + block, null_col_id = block.create_constant(None, dtype=dtype) + binop_result_ids.append(null_col_id) + else: + # Should not be possible + raise ValueError("No right or left index.") + + block = block.select_columns(binop_result_ids).with_column_labels(columns) + return DataFrame(block) + def eq(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.eq_op, axis=axis) @@ -619,7 +662,9 @@ def gt(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: __ge__ = ge def add( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: # TODO(swast): Support fill_value parameter. # TODO(swast): Support level parameter with MultiIndex. @@ -628,64 +673,92 @@ def add( __radd__ = __add__ = radd = add def sub( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: return self._apply_binop(other, ops.sub_op, axis=axis) __sub__ = subtract = sub def rsub( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: return self._apply_binop(other, ops.reverse(ops.sub_op), axis=axis) __rsub__ = rsub def mul( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: return self._apply_binop(other, ops.mul_op, axis=axis) __rmul__ = __mul__ = rmul = multiply = mul def truediv( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: return self._apply_binop(other, ops.div_op, axis=axis) div = divide = __truediv__ = truediv def rtruediv( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: return self._apply_binop(other, ops.reverse(ops.div_op), axis=axis) __rtruediv__ = rdiv = rtruediv def floordiv( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: return self._apply_binop(other, ops.floordiv_op, axis=axis) __floordiv__ = floordiv def rfloordiv( - self, other: float | int | bigframes.series.Series, axis: str | int = "columns" + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", ) -> DataFrame: return self._apply_binop(other, ops.reverse(ops.floordiv_op), axis=axis) __rfloordiv__ = rfloordiv - def mod(self, other: int | bigframes.series.Series, axis: str | int = "columns") -> DataFrame: # type: ignore + def mod(self, other: int | bigframes.series.Series | DataFrame, axis: str | int = "columns") -> DataFrame: # type: ignore return self._apply_binop(other, ops.mod_op, axis=axis) - def rmod(self, other: int | bigframes.series.Series, axis: str | int = "columns") -> DataFrame: # type: ignore + def rmod(self, other: int | bigframes.series.Series | DataFrame, axis: str | int = "columns") -> DataFrame: # type: ignore return self._apply_binop(other, ops.reverse(ops.mod_op), axis=axis) __mod__ = mod __rmod__ = rmod + def pow( + self, other: int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.pow_op, axis=axis) + + def rpow( + self, other: int | bigframes.series.Series, axis: str | int = "columns" + ) -> DataFrame: + return self._apply_binop(other, ops.reverse(ops.pow_op), axis=axis) + + __pow__ = pow + + __rpow__ = rpow + def to_pandas( self, max_download_size: Optional[int] = None, @@ -1023,8 +1096,72 @@ 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 dropna(self) -> DataFrame: - return DataFrame(block_ops.dropna(self._block, how="any")) + def fillna(self, value=None) -> DataFrame: + return self._apply_binop(value, ops.fillna_op) + + def isin(self, values) -> DataFrame: + if utils.is_dict_like(values): + block = self._block + result_ids = [] + for col, label in zip(self._block.value_columns, self._block.column_labels): + if label in values.keys(): + value_for_key = values[label] + block, result_id = block.apply_unary_op( + col, ops.IsInOp(value_for_key, match_nulls=True), label + ) + result_ids.append(result_id) + else: + block, result_id = block.create_constant( + False, label=label, dtype=pandas.BooleanDtype() + ) + result_ids.append(result_id) + return DataFrame(block.select_columns(result_ids)).fillna(value=False) + elif utils.is_list_like(values): + return self._apply_unary_op(ops.IsInOp(values, match_nulls=True)).fillna( + value=False + ) + else: + raise TypeError( + "only list-like objects are allowed to be passed to " + f"isin(), you passed a [{type(values).__name__}]" + ) + + def dropna( + self, + *, + axis: int | str = 0, + inplace: bool = False, + how: str = "any", + ignore_index=False, + ) -> DataFrame: + if inplace: + raise NotImplementedError( + "'inplace'=True not supported. {constants.FEEDBACK_LINK}" + ) + if how not in ("any", "all"): + raise ValueError("'how' must be one of 'any', 'all'") + + axis_n = utils.get_axis_number(axis) + + if axis_n == 0: + result = block_ops.dropna(self._block, how=how) # type: ignore + if ignore_index: + result = result.reset_index() + return DataFrame(result) + else: + isnull_block = self._block.multi_apply_unary_op( + self._block.value_columns, ops.isnull_op + ) + if how == "any": + null_locations = DataFrame(isnull_block).any().to_pandas() + else: # 'all' + null_locations = DataFrame(isnull_block).all().to_pandas() + keep_columns = [ + col + for col, to_drop in zip(self._block.value_columns, null_locations) + if not to_drop + ] + return DataFrame(self._block.select_columns(keep_columns)) def any( self, @@ -1205,7 +1342,7 @@ def pivot( def stack(self): # TODO: support 'level' param by simply reordering levels such that selected level is last before passing to Block.stack. - # TODO: support 'dropna' param by executing dropna only conditionally + # TODO: match impl to pandas future_stack as described in pandas 2.1 release notes result_block = block_ops.dropna(self._block.stack(), how="all") if not isinstance(self.columns, pandas.MultiIndex): return bigframes.series.Series(result_block) @@ -1879,16 +2016,17 @@ def __array_ufunc__( """Used to support numpy ufuncs. See: https://numpy.org/doc/stable/reference/ufuncs.html """ - if ( - inputs[0] is not self - or method != "__call__" - or len(inputs) > 1 - or len(kwargs) > 0 - ): + if method != "__call__" or len(inputs) > 2 or len(kwargs) > 0: return NotImplemented - if ufunc in ops.NUMPY_TO_OP: + if len(inputs) == 1 and ufunc in ops.NUMPY_TO_OP: return self._apply_unary_op(ops.NUMPY_TO_OP[ufunc]) + if len(inputs) == 2 and ufunc in ops.NUMPY_TO_BINOP: + binop = ops.NUMPY_TO_BINOP[ufunc] + if inputs[0] is self: + return self._apply_binop(inputs[1], binop) + else: + return self._apply_binop(inputs[0], ops.reverse(binop)) return NotImplemented diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 9a20fd9ad74..2501d2b21f1 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -67,7 +67,7 @@ def fit( (X,) = utils.convert_to_dataframe(X) self._bqml_model = core.create_bqml_model( - train_X=X, + X_train=X, transforms=transforms, options=self._bqml_options, ) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 27727c9f81f..57f610c4c4b 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -22,7 +22,6 @@ from google.cloud import bigquery import bigframes -import bigframes.constants as constants from bigframes.ml import sql as ml_sql import bigframes.pandas as bpd @@ -53,10 +52,8 @@ def model(self) -> bigquery.Model: """Get the BQML model associated with this wrapper""" return self._model - @classmethod def _apply_sql( - cls, - session: bigframes.Session, + self, input_data: bpd.DataFrame, func: Callable[[str], str], ) -> bpd.DataFrame: @@ -78,13 +75,8 @@ def _apply_sql( include_index=True ) - if len(index_col_ids) != 1: - raise NotImplementedError( - f"Only exactly one index column is supported. {constants.FEEDBACK_LINK}" - ) - sql = func(source_sql) - df = session.read_gbq(sql, index_col=index_col_ids) + df = self._session.read_gbq(sql, index_col=index_col_ids) df.index.names = index_labels return df @@ -92,7 +84,6 @@ def _apply_sql( def predict(self, input_data: bpd.DataFrame) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( - self._session, input_data, lambda source_sql: ml_sql.ml_predict( model_name=self.model_name, source_sql=source_sql @@ -102,7 +93,6 @@ def predict(self, input_data: bpd.DataFrame) -> bpd.DataFrame: def transform(self, input_data: bpd.DataFrame) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( - self._session, input_data, lambda source_sql: ml_sql.ml_transform( model_name=self.model_name, source_sql=source_sql @@ -116,7 +106,6 @@ def generate_text( ) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( - self._session, input_data, lambda source_sql: ml_sql.ml_generate_text( model_name=self.model_name, @@ -132,7 +121,6 @@ def generate_text_embedding( ) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( - self._session, input_data, lambda source_sql: ml_sql.ml_generate_text_embedding( model_name=self.model_name, @@ -209,16 +197,16 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: def create_bqml_model( - train_X: bpd.DataFrame, - train_y: Optional[bpd.DataFrame] = None, + X_train: bpd.DataFrame, + y_train: Optional[bpd.DataFrame] = None, transforms: Optional[Iterable[str]] = None, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: """Create a session-temporary BQML model with the CREATE MODEL statement Args: - train_X: features columns for training - train_y: labels columns for training, if applicable + X_train: features columns for training + y_train: labels columns for training, if applicable transforms: an optional list of SQL expressions that implement preprocessing on top of the input data. Generates a BQML TRANSFORM clause options: a dict of options to configure the model. Generates a BQML OPTIONS @@ -227,19 +215,13 @@ def create_bqml_model( Returns: a BqmlModel, wrapping a trained model in BigQuery """ options = dict(options) - if train_y is None: - input_data = train_X + if y_train is None: + input_data = X_train else: - # TODO: handle case where train_y columns are renamed in the join - input_data = train_X.join(train_y, how="outer") - options.update({"INPUT_LABEL_COLS": train_y.columns.tolist()}) + input_data = X_train.join(y_train, how="outer") + options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) - # pickpocket session object from the dataframe - session = train_X._get_block().expr._session - - # TODO(garrettwu): add wrapper to select the feature columns - # for now, drop index to avoid including the index in feature columns - input_data = input_data.reset_index(drop=True) + session = X_train._get_block().expr._session source_sql = input_data.sql options_sql = ml_sql.options(**options) @@ -255,25 +237,25 @@ def create_bqml_model( def create_bqml_time_series_model( - train_X: bpd.DataFrame, - train_y: bpd.DataFrame, + X_train: bpd.DataFrame, + y_train: bpd.DataFrame, transforms: Optional[Iterable[str]] = None, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: assert ( - train_X.columns.size == 1 + X_train.columns.size == 1 ), "Time series timestamp input must only contain 1 column." assert ( - train_y.columns.size == 1 + y_train.columns.size == 1 ), "Time stamp data input must only contain 1 column." options = dict(options) - input_data = train_X.join(train_y, how="outer") - options.update({"TIME_SERIES_TIMESTAMP_COL": train_X.columns.tolist()[0]}) - options.update({"TIME_SERIES_DATA_COL": train_y.columns.tolist()[0]}) - # pickpocket session object from the dataframe - session = train_X._get_block().expr._session + input_data = X_train.join(y_train, how="outer") + options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) + options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) + + session = X_train._get_block().expr._session source_sql = input_data.sql options_sql = ml_sql.options(**options) diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 16106d3a7b3..75b57f2e54f 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -61,7 +61,7 @@ def fit( (X,) = utils.convert_to_dataframe(X) self._bqml_model = core.create_bqml_model( - train_X=X, + X_train=X, transforms=transforms, options={ "model_type": "PCA", diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index 89078f8267f..d4571eb3e5b 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -34,8 +34,12 @@ class TensorFlowModel(base.Predictor): model_path (str): GCS path that holds the model files.""" - def __init__(self, session: bigframes.Session, model_path: Optional[str] = None): - self.session = session + def __init__( + self, + session: Optional[bigframes.Session] = None, + model_path: Optional[str] = None, + ): + self.session = session or bpd.get_global_session() self.model_path = model_path self._bqml_model: Optional[core.BqmlModel] = None @@ -112,8 +116,12 @@ class ONNXModel(base.Predictor): model_path (str): Cloud Storage path that holds the model files.""" - def __init__(self, session: bigframes.Session, model_path: Optional[str] = None): - self.session = session + def __init__( + self, + session: Optional[bigframes.Session] = None, + model_path: Optional[str] = None, + ): + self.session = session or bpd.get_global_session() self.model_path = model_path self._bqml_model: Optional[core.BqmlModel] = None diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index def97b56ff0..345e3deb728 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -16,10 +16,10 @@ from __future__ import annotations -from typing import cast, Union +from typing import cast, Optional, Union import bigframes -import bigframes.constants as constants +from bigframes import clients, constants from bigframes.core import blocks from bigframes.ml import base, core, utils import bigframes.pandas as bpd @@ -35,17 +35,43 @@ class PaLM2TextGenerator(base.Predictor): """PaLM2 text generator LLM model. Args: - session (BigQuery Session): - BQ session to create the model - connection_name (str): - connection to connect with remote service. str of the format ..""" - - def __init__(self, session: bigframes.Session, connection_name: str): - self.session = session - self.connection_name = connection_name + session (bigframes.Session or None): + BQ session to create the model. If None, use the global default session. + connection_name (str or None): + connection to connect with remote service. str of the format ... + if None, use default connection in session context. + """ + + def __init__( + self, + session: Optional[bigframes.Session] = None, + connection_name: Optional[str] = None, + ): + self.session = session or bpd.get_global_session() + self.connection_name = connection_name or self.session._bq_connection + self._bq_connection_manager = clients.BqConnectionManager( + self.session.bqconnectionclient, self.session.resourcemanagerclient + ) self._bqml_model: core.BqmlModel = self._create_bqml_model() def _create_bqml_model(self): + # Parse and create connection if needed. + if not self.connection_name: + raise ValueError( + "Must provide connection_name, either in constructor or through session options." + ) + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", + ) + options = { "remote_service_type": _REMOTE_TEXT_GENERATOR_MODEL_CODE, } @@ -140,17 +166,43 @@ class PaLM2TextEmbeddingGenerator(base.Predictor): """PaLM2 text embedding generator LLM model. Args: - session (BigQuery Session): - BQ session to create the model - connection_name (str): - connection to connect with remote service. str of the format ..""" - - def __init__(self, session: bigframes.Session, connection_name: str): - self.session = session - self.connection_name = connection_name + session (bigframes.Session or None): + BQ session to create the model. If None, use the global default session. + connection_name (str or None): + connection to connect with remote service. str of the format ... + if None, use default connection in session context. + """ + + def __init__( + self, + session: Optional[bigframes.Session] = None, + connection_name: Optional[str] = None, + ): + self.session = session or bpd.get_global_session() + self.connection_name = connection_name or self.session._bq_connection + self._bq_connection_manager = clients.BqConnectionManager( + self.session.bqconnectionclient, self.session.resourcemanagerclient + ) self._bqml_model: core.BqmlModel = self._create_bqml_model() def _create_bqml_model(self): + # Parse and create connection if needed. + if not self.connection_name: + raise ValueError( + "Must provide connection_name, either in constructor or through session options." + ) + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", + ) + options = { "remote_service_type": _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE, } diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index bcd82435823..feb7ff7835b 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -34,8 +34,8 @@ def _encode_value(v: Union[str, int, float, Iterable[str]]) -> str: raise ValueError(f"Unexpected value type. {constants.FEEDBACK_LINK}") -def _build_param_Iterable(**kwargs: Union[str, int, float, Iterable[str]]) -> str: - """Encode a dict of values into a formatted Iterable of KVPs for SQL""" +def _build_parameters(**kwargs: Union[str, int, float, Iterable[str]]) -> str: + """Encode a dict of values into a formatted Iterable of key-value pairs for SQL""" indent_str = " " param_strs = [f"{k}={_encode_value(v)}" for k, v in kwargs.items()] return "\n" + indent_str + f",\n{indent_str}".join(param_strs) @@ -43,10 +43,10 @@ def _build_param_Iterable(**kwargs: Union[str, int, float, Iterable[str]]) -> st def options(**kwargs: Union[str, int, float, Iterable[str]]) -> str: """Encode the OPTIONS clause for BQML""" - return f"OPTIONS({_build_param_Iterable(**kwargs)})" + return f"OPTIONS({_build_parameters(**kwargs)})" -def _build_struct_param_Iterable(**kwargs: Union[int, float]) -> str: +def _build_structs(**kwargs: Union[int, float]) -> str: """Encode a dict of values into a formatted STRUCT items for SQL""" indent_str = " " param_strs = [f"{v} AS {k}" for k, v in kwargs.items()] @@ -55,10 +55,10 @@ def _build_struct_param_Iterable(**kwargs: Union[int, float]) -> str: def struct_options(**kwargs: Union[int, float]) -> str: """Encode a BQ STRUCT as options.""" - return f"STRUCT({_build_struct_param_Iterable(**kwargs)})" + return f"STRUCT({_build_structs(**kwargs)})" -def _build_expr_Iterable(*expr_sqls: str) -> str: +def _build_expressions(*expr_sqls: str) -> str: """Encode a Iterable of SQL expressions into a formatted Iterable for SQL""" indent_str = " " return "\n" + indent_str + f",\n{indent_str}".join(expr_sqls) @@ -66,7 +66,7 @@ def _build_expr_Iterable(*expr_sqls: str) -> str: def transform(*expr_sqls: str) -> str: """Encode the TRANSFORM clause for BQML""" - return f"TRANSFORM({_build_expr_Iterable(*expr_sqls)})" + return f"TRANSFORM({_build_expressions(*expr_sqls)})" def connection(conn_name: str) -> str: @@ -137,7 +137,7 @@ def alter_model( return "\n".join(parts) -def ml_evaluate(model_name: str, source_sql: Union[str, None] = None) -> str: +def ml_evaluate(model_name: str, source_sql: Optional[str] = None) -> str: """Encode ML.EVALUATE for BQML""" if source_sql is None: return f"""SELECT * FROM ML.EVALUATE(MODEL `{model_name}`)""" diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 9305cf1ddab..fa43f725f64 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -38,6 +38,7 @@ # FLOAT64 has 11 exponent bits, so max values is about 2**(2**10) # ln(2**(2**10)) == (2**10)*ln(2) ~= 709.78, so EXP(x) for x>709.78 will overflow. _FLOAT64_EXP_BOUND = typing.cast(ibis_types.NumericValue, ibis_types.literal(709.78)) +_INT64_EXP_BOUND = typing.cast(ibis_types.NumericValue, ibis_types.literal(43.6)) BinaryOp = typing.Callable[[ibis_types.Value, ibis_types.Value], ibis_types.Value] TernaryOp = typing.Callable[ @@ -538,12 +539,27 @@ def __init__(self, values, match_nulls: bool = True): self._match_nulls = match_nulls def _as_ibis(self, x: ibis_types.Value): - if self._match_nulls and any(is_null(value) for value in self._values): - return x.isnull() | x.isin( - [val for val in self._values if not is_null(val)] - ) + contains_nulls = any(is_null(value) for value in self._values) + matchable_ibis_values = [] + for item in self._values: + if not is_null(item): + try: + # we want values that *could* be cast to the dtype, but we don't want + # to actually cast it, as that could be lossy (eg float -> int) + item_inferred_type = ibis.literal(item).type() + if ( + x.type() == item_inferred_type + or x.type().is_numeric() + and item_inferred_type.is_numeric() + ): + matchable_ibis_values.append(item) + except TypeError: + pass + + if self._match_nulls and contains_nulls: + return x.isnull() | x.isin(matchable_ibis_values) else: - return x.isin(self._values) + return x.isin(matchable_ibis_values) class BinopPartialRight(UnaryOp): @@ -746,6 +762,94 @@ def div_op( ) +@short_circuit_nulls(ibis_dtypes.float) +def pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + if x.type().is_integer() and y.type().is_integer(): + return _int_pow_op(x, y) + else: + return _float_pow_op(x, y) + + +def _int_pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + # Need to avoid any error cases - should produce NaN instead + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions#pow + x_as_decimal = typing.cast( + ibis_types.NumericValue, + x.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)), + ) + y_val = typing.cast(ibis_types.NumericValue, y) + + # BQ POW() function outputs FLOAT64, which can lose precision. + # Therefore, we do math in NUMERIC and cast back down after. + # Also, explicit bounds checks, pandas will silently overflow. + pow_result = x_as_decimal**y_val + overflow_cond = (pow_result > _ibis_num((2**63) - 1)) | ( + pow_result < _ibis_num(-(2**63)) + ) + + return ( + ibis.case() + .when((overflow_cond), ibis.null()) + .else_(pow_result.cast(ibis_dtypes.int64)) + .end() + ) + + +def _float_pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + # Most conditions here seek to prevent calling BQ POW with inputs that would generate errors. + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions#pow + x_val = typing.cast(ibis_types.NumericValue, x) + y_val = typing.cast(ibis_types.NumericValue, y) + + overflow_cond = (x_val != _ZERO) & ((y_val * x_val.abs().ln()) > _FLOAT64_EXP_BOUND) + + # Float64 lose integer precision beyond 2**53, beyond this insufficient precision to get parity + exp_too_big = y_val.abs() > _ibis_num(2**53) + # Treat very large exponents as +=INF + norm_exp = exp_too_big.ifelse(_INF * y_val.sign(), y_val) + + pow_result = x_val**norm_exp + + # This cast is dangerous, need to only excuted where y_val has been bounds-checked + # Ibis needs try_cast binding to bq safe_cast + exponent_is_whole = y_val.cast(ibis_dtypes.int64) == y_val + odd_exponent = (x_val < _ZERO) & ( + y_val.cast(ibis_dtypes.int64) % _ibis_num(2) == _ibis_num(1) + ) + infinite_base = x_val.abs() == _INF + + return ( + ibis.case() + # Might be able to do something more clever with x_val==0 case + .when(y_val == _ZERO, _ibis_num(1)) + .when( + x_val == _ibis_num(1), _ibis_num(1) + ) # Need to ignore exponent, even if it is NA + .when( + (x_val == _ZERO) & (y_val < _ZERO), _INF + ) # This case would error POW function in BQ + .when(infinite_base, pow_result) + .when( + exp_too_big, pow_result + ) # Bigquery can actually handle the +-inf cases gracefully + .when((x_val < _ZERO) & (~exponent_is_whole), _NAN) + .when( + overflow_cond, _INF * odd_exponent.ifelse(_ibis_num(-1), _ibis_num(1)) + ) # finite overflows would cause bq to error + .else_(pow_result) + .end() + ) + + @short_circuit_nulls(ibis_dtypes.bool) def lt_op( x: ibis_types.Value, @@ -880,6 +984,15 @@ def partial_right(op: BinaryOp, scalar: typing.Any) -> UnaryOp: return BinopPartialRight(op, scalar) +NUMPY_TO_BINOP: typing.Final = { + np.add: add_op, + np.subtract: sub_op, + np.multiply: mul_op, + np.divide: div_op, + np.power: pow_op, +} + + # Ternary ops def where_op( original: ibis_types.Value, diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 280fce11126..e1a23e67a1c 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -52,6 +52,7 @@ import bigframes.series import bigframes.session import third_party.bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat +import third_party.bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile @@ -132,6 +133,37 @@ def cut( cut.__doc__ = vendored_pandas_tile.cut.__doc__ +def merge( + left: DataFrame, + right: DataFrame, + how: Literal[ + "inner", + "left", + "outer", + "right", + ] = "inner", + on: Optional[str] = None, + *, + left_on: Optional[str] = None, + right_on: Optional[str] = None, + sort: bool = False, + suffixes: tuple[str, str] = ("_x", "_y"), +) -> DataFrame: + return bigframes.core.joins.merge( + left, + right, + how=how, + on=on, + left_on=left_on, + right_on=right_on, + sort=sort, + suffixes=suffixes, + ) + + +merge.__doc__ = vendored_pandas_merge.merge.__doc__ + + def _set_default_session_location_if_possible(query): # Set the location as per the query if this is the first query the user is # running and: @@ -390,7 +422,6 @@ def read_gbq_function(function_name: str): read_gbq_function.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_function) - # pandas dtype attributes NA = pandas.NA BooleanDtype = pandas.BooleanDtype @@ -421,6 +452,7 @@ def read_gbq_function(function_name: str): __all___ = [ # Functions "concat", + "merge", "read_csv", "read_gbq", "read_gbq_function", diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 2a4b919dab0..6932e5b5805 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -26,7 +26,6 @@ import sys import tempfile import textwrap -import time from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING if TYPE_CHECKING: @@ -49,6 +48,7 @@ import ibis.expr.operations as ops import ibis.expr.rules as rlz +from bigframes import clients import bigframes.constants as constants # TODO(shobs): Change the min log level to INFO after the development stabilizes @@ -167,35 +167,22 @@ def __init__( self._bq_location = bq_location self._bq_dataset = bq_dataset self._bq_client = bq_client - self._bq_connection_client = bq_connection_client self._bq_connection_id = bq_connection_id - self._cloud_resource_manager_client = cloud_resource_manager_client + self._bq_connection_manager = clients.BqConnectionManager( + bq_connection_client, cloud_resource_manager_client + ) def create_bq_remote_function( self, input_args, input_types, output_type, endpoint, bq_function_name ): """Create a BigQuery remote function given the artifacts of a user defined function and the http endpoint of a corresponding cloud function.""" - # TODO(shobs): The below command to enable BigQuery Connection API needs - # to be automated. Disabling for now since most target users would not - # have the privilege to enable API in a project. - # log("Making sure BigQuery Connection API is enabled") - # if os.system("gcloud services enable bigqueryconnection.googleapis.com"): - # raise ValueError("Failed to enable BigQuery Connection API") - - # If the intended connection does not exist then create it - service_account_id = self.get_service_account_if_connection_exists() - if service_account_id: - logger.info(f"Connector {self._bq_connection_id} already exists") - else: - connection_name, service_account_id = self.create_bq_connection() - logger.info( - f"Created BQ connection {connection_name} with service account id: {service_account_id}" - ) - - # Ensure IAM role on the BQ connection - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function - self._ensure_iam_binding(service_account_id, "run.invoker") + self._bq_connection_manager.create_bq_connection( + self._gcp_project_id, + self._bq_location, + self._bq_connection_id, + "run.invoker", + ) # Create BQ function # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 @@ -237,86 +224,6 @@ def get_cloud_function_endpoint(self, name): pass return None - # 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'. - @google.api_core.retry.Retry( - predicate=google.api_core.retry.if_exception_type( - google.api_core.exceptions.Aborted - ), - initial=10, - maximum=20, - multiplier=2, - timeout=60, - ) - def _ensure_iam_binding(self, service_account: str, role: str): - """Ensure necessary IAM role is configured on a service account.""" - project = f"projects/{self._gcp_project_id}" - service_account = f"serviceAccount:{service_account}" - role = f"roles/{role}" - request = google.iam.v1.iam_policy_pb2.GetIamPolicyRequest(resource=project) - policy = self._cloud_resource_manager_client.get_iam_policy(request=request) - - # Check if the binding already exists, and if does, do nothing more - for binding in policy.bindings: - if binding.role == role: - if service_account in binding.members: - return - - # Create a new binding - new_binding = google.iam.v1.policy_pb2.Binding( - role=role, members=[service_account] - ) - policy.bindings.append(new_binding) - request = google.iam.v1.iam_policy_pb2.SetIamPolicyRequest( - resource=project, policy=policy - ) - self._cloud_resource_manager_client.set_iam_policy(request=request) - - # We would wait for the IAM policy change to take effect - # https://cloud.google.com/iam/docs/access-change-propagation - logger.info( - f"Waiting {self._iam_wait_seconds} seconds for IAM to take effect.." - ) - time.sleep(self._iam_wait_seconds) - - def create_bq_connection(self): - """Create the BigQuery Connection and returns corresponding service account id.""" - client = self._bq_connection_client - connection = bigquery_connection_v1.Connection( - cloud_resource=bigquery_connection_v1.CloudResourceProperties() - ) - request = bigquery_connection_v1.CreateConnectionRequest( - parent=client.common_location_path(self._gcp_project_id, self._bq_location), - connection_id=self._bq_connection_id, - connection=connection, - ) - connection = client.create_connection(request) - return connection.name, connection.cloud_resource.service_account_id - - def get_service_account_if_connection_exists(self) -> Optional[str]: - """Check if the BigQuery Connection exists.""" - client = self._bq_connection_client - request = bigquery_connection_v1.GetConnectionRequest( - name=client.connection_path( - self._gcp_project_id, self._bq_location, self._bq_connection_id - ) - ) - - service_account = None - try: - service_account = client.get_connection( - request=request - ).cloud_resource.service_account_id - except google.api_core.exceptions.NotFound: - pass - - return service_account - def generate_udf_code(self, def_, dir): """Generate serialized bytecode using cloudpickle given a udf.""" udf_code_file_name = "udf.py" @@ -825,7 +732,7 @@ def remote_function( # A connection is required for BQ remote function # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function if not bigquery_connection and session: - bigquery_connection = session._remote_udf_connection # type: ignore + bigquery_connection = session._bq_connection # type: ignore if not bigquery_connection: raise ValueError( "BigQuery connection must be provided, either directly or via session. " diff --git a/bigframes/series.py b/bigframes/series.py index 49b0a5b1f08..8eadee37ed0 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -387,9 +387,24 @@ def rank( ) -> Series: return Series(block_ops.rank(self._block, method, na_option, ascending)) - def fillna(self, value=None) -> "Series" | None: + def fillna(self, value=None) -> Series: return self._apply_binary_op(value, ops.fillna_op) + def dropna( + self, + *, + axis: int = 0, + inplace: bool = False, + how: typing.Optional[str] = None, + ignore_index: bool = False, + ) -> Series: + if inplace: + raise NotImplementedError("'inplace'=True not supported") + result = block_ops.dropna(self._block, how="any") + if ignore_index: + result = result.reset_index() + return Series(result) + def head(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[0:n]) @@ -547,6 +562,18 @@ def floordiv(self, other: float | int | Series) -> Series: def rfloordiv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.reverse(ops.floordiv_op)) + def __pow__(self, other: float | int | Series) -> Series: + return self.pow(other) + + def __rpow__(self, other: float | int | Series) -> Series: + return self.rpow(other) + + def pow(self, other: float | int | Series) -> Series: + return self._apply_binary_op(other, ops.pow_op) + + def rpow(self, other: float | int | Series) -> Series: + return self._apply_binary_op(other, ops.reverse(ops.pow_op)) + def __lt__(self, other: float | int | Series) -> Series: # type: ignore return self.lt(other) @@ -843,23 +870,15 @@ def argmin(self) -> scalars.Scalar: @property def is_monotonic_increasing(self) -> bool: - period = 1 - window = bigframes.core.WindowSpec( - preceding=period, - following=None, + return typing.cast( + bool, self._block.is_monotonic_increasing(self._value_column) ) - shifted_series = self._apply_window_op(agg_ops.ShiftOp(period), window) - return self.notna().__and__(self >= shifted_series).all() @property def is_monotonic_decreasing(self) -> bool: - period = 1 - window = bigframes.core.WindowSpec( - preceding=period, - following=None, + return typing.cast( + bool, self._block.is_monotonic_decreasing(self._value_column) ) - shifted_series = self._apply_window_op(agg_ops.ShiftOp(period), window) - return self.notna().__and__(self <= shifted_series).all() def __getitem__(self, indexer): # TODO: enforce stricter alignment, should fail if indexer is missing any keys. @@ -1105,9 +1124,12 @@ def mask(self, cond, other=None) -> Series: ) return self.where(~cond, other) - def to_frame(self) -> bigframes.dataframe.DataFrame: + def to_frame(self, name: blocks.Label = None) -> bigframes.dataframe.DataFrame: + provided_name = name if name else self.name # To be consistent with Pandas, it assigns 0 as the column name if missing. 0 is the first element of RangeIndex. - block = self._block.with_column_labels([self.name] if self.name else ["0"]) + block = self._block.with_column_labels( + [provided_name] if provided_name else ["0"] + ) return bigframes.dataframe.DataFrame(block) def to_csv(self, path_or_buf=None, **kwargs) -> typing.Optional[str]: @@ -1191,6 +1213,57 @@ def to_string( def to_xarray(self): return self.to_pandas().to_xarray() + def _throw_if_index_contains_duplicates( + self, error_message: typing.Optional[str] = None + ) -> None: + duplicates_block, _ = block_ops.indicate_duplicates( + self._get_block(), self._get_block().index_columns + ) + duplicates_block = duplicates_block.with_column_labels( + ["values", "is_duplicate"] + ) + duplicates_df = bigframes.dataframe.DataFrame(duplicates_block) + if duplicates_df["is_duplicate"].any(): + error_message = ( + error_message + if error_message + else "Index contains duplicate entries, but uniqueness is required." + ) + raise pandas.errors.InvalidIndexError(error_message) + + def map( + self, + arg: typing.Union[Mapping, Series], + na_action: Optional[str] = None, + *, + verify_integrity: bool = False, + ) -> Series: + if na_action: + raise NotImplementedError( + f"Non-None na_action argument is not yet supported for Series.map. {constants.FEEDBACK_LINK}" + ) + if isinstance(arg, Series): + if verify_integrity: + error_message = "When verify_integrity is True in Series.map, index of arg parameter must not have duplicate entries." + arg._throw_if_index_contains_duplicates(error_message=error_message) + map_df = bigframes.dataframe.DataFrame(arg._block) + map_df = map_df.rename(columns={arg.name: self.name}) + elif isinstance(arg, Mapping): + map_df = bigframes.dataframe.DataFrame( + {"keys": list(arg.keys()), self.name: list(arg.values())}, + session=self._get_block().expr._session, + ) + map_df = map_df.set_index("keys") + elif callable(arg): + return self.apply(arg) + else: + # Mirroring pandas, call the uncallable object + arg() # throws TypeError: object is not callable + + self_df = self.to_frame(name="series") + result_df = self_df.join(map_df, on="series") + return result_df[self.name] + def __array_ufunc__( self, ufunc: numpy.ufunc, method: str, *inputs, **kwargs ) -> Series: @@ -1198,16 +1271,17 @@ def __array_ufunc__( See: https://numpy.org/doc/stable/reference/ufuncs.html """ # Only __call__ supported with zero arguments - if ( - inputs[0] is not self - or method != "__call__" - or len(inputs) > 1 - or len(kwargs) > 0 - ): + if method != "__call__" or len(inputs) > 2 or len(kwargs) > 0: return NotImplemented - if ufunc in ops.NUMPY_TO_OP: + if len(inputs) == 1 and ufunc in ops.NUMPY_TO_OP: return self._apply_unary_op(ops.NUMPY_TO_OP[ufunc]) + if len(inputs) == 2 and ufunc in ops.NUMPY_TO_BINOP: + binop = ops.NUMPY_TO_BINOP[ufunc] + if inputs[0] is self: + return self._apply_binary_op(inputs[1], binop) + else: + return self._apply_binary_op(inputs[0], ops.reverse(binop)) return NotImplemented diff --git a/bigframes/session.py b/bigframes/session.py index ac2f8fa53ab..17444077726 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -270,9 +270,22 @@ class Session( third_party_pandas_readers.ReaderIOMixin, ): """Establishes a BigQuery connection to capture a group of job activities related to - DataFrames.""" + DataFrames. - def __init__(self, context: Optional[bigquery_options.BigQueryOptions] = None): + Args: + context (bigframes._config.bigquery_options.BigQueryOptions): + Configuration adjusting how to connect to BigQuery and related + APIs. Note that some options are ignored if ``clients_provider`` is + set. + clients_provider (bigframes.session.ClientsProvider): + An object providing client library objects. + """ + + def __init__( + self, + context: Optional[bigquery_options.BigQueryOptions] = None, + clients_provider: Optional[ClientsProvider] = None, + ): if context is None: context = bigquery_options.BigQueryOptions() @@ -288,12 +301,15 @@ def __init__(self, context: Optional[bigquery_options.BigQueryOptions] = None): # Instantiate a clients provider to help with cloud clients that will be # used in the future operations in the session - self._clients_provider = ClientsProvider( - project=context.project, - location=self._location, - use_regional_endpoints=context.use_regional_endpoints, - credentials=context.credentials, - ) + if clients_provider: + self._clients_provider = clients_provider + else: + self._clients_provider = ClientsProvider( + project=context.project, + location=self._location, + use_regional_endpoints=context.use_regional_endpoints, + credentials=context.credentials, + ) self._create_and_bind_bq_session() self.ibis_client = typing.cast( @@ -305,7 +321,7 @@ def __init__(self, context: Optional[bigquery_options.BigQueryOptions] = None): ), ) - self._remote_udf_connection = context.remote_udf_connection + self._bq_connection = context.bq_connection # Now that we're starting the session, don't allow the options to be # changed. @@ -381,10 +397,10 @@ def close(self): try: query_job = self.bqclient.query(abort_session_query) query_job.result() # blocks until finished - except google.api_core.exceptions.BadRequest as e: + except google.api_core.exceptions.BadRequest as exc: # Ignore the exception when the BQ session itself has expired # https://cloud.google.com/bigquery/docs/sessions-terminating#auto-terminate_a_session - if not e.message.startswith( + if not exc.message.startswith( f"Session {self._session_id} has expired and is no longer available." ): raise diff --git a/notebooks/experimental/longer_ml_demo.ipynb b/notebooks/experimental/longer_ml_demo.ipynb index c4d133421f6..793ff58ecdf 100644 --- a/notebooks/experimental/longer_ml_demo.ipynb +++ b/notebooks/experimental/longer_ml_demo.ipynb @@ -1373,9 +1373,9 @@ } ], "source": [ - "train_x = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", - "train_y = training_data[['body_mass_g']]\n", - "model.fit(train_x, train_y)\n", + "X_train = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "y_train = training_data[['body_mass_g']]\n", + "model.fit(X_train, y_train)\n", "model" ] }, @@ -1453,7 +1453,7 @@ } ], "source": [ - "model.score(train_x, train_y)" + "model.score(X_train, y_train)" ] }, { @@ -1490,12 +1490,12 @@ "# lets define a preprocessing step that adjust the linear measurements to use the cube\n", "'''\n", "def cubify(penguin_df):\n", - " penguin_df.culmen_length_mm = train_x.culmen_length_mm.pow(3)\n", - " penguin_df.culmen_depth_mm = train_x.culmen_depth_mm.pow(3)\n", - " penguin_df.flipper_length_mm = train_x.flipper_length_mm.pow(3)\n", + " penguin_df.culmen_length_mm = X_train.culmen_length_mm.pow(3)\n", + " penguin_df.culmen_depth_mm = X_train.culmen_depth_mm.pow(3)\n", + " penguin_df.flipper_length_mm = X_train.flipper_length_mm.pow(3)\n", "\n", - "cubify(train_x)\n", - "train_x\n", + "cubify(X_train)\n", + "X_train\n", "'''" ] }, @@ -1519,7 +1519,7 @@ "source": [ "# AS ABOVE, SKIP FOR NOW\n", "'''\n", - "model.fit(train_x, train_y)\n", + "model.fit(X_train, y_train)\n", "model.evaluate()\n", "'''" ] @@ -1756,15 +1756,15 @@ "training_data = training_data.dropna()\n", "\n", "# And we'll include species in our features\n", - "train_x = training_data[['species', 'island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", - "train_y = training_data[['body_mass_g']]\n", - "model.fit(train_x, train_y)\n", + "X_train = training_data[['species', 'island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "y_train = training_data[['body_mass_g']]\n", + "model.fit(X_train, y_train)\n", "\n", "# And we'll evaluate it on the Adelie penguins only\n", "adelie_data = training_data[training_data.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", - "test_x = adelie_data[['species', 'island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", - "test_y = adelie_data[['body_mass_g']]\n", - "model.score(test_x, test_y)" + "X_test = adelie_data[['species', 'island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "y_test = adelie_data[['body_mass_g']]\n", + "model.score(X_test, y_test)" ] }, { @@ -1852,7 +1852,7 @@ " ('linreg', LinearRegression())\n", "])\n", "\n", - "pipe.fit(train_x, train_y)\n", + "pipe.fit(X_train, y_train)\n", "pipe.evaluate()" ] }, diff --git a/notebooks/getting_started/ml_fundamentals.ipynb b/notebooks/getting_started/ml_fundamentals.ipynb index ade50bcbc2e..e48aff1d57c 100644 --- a/notebooks/getting_started/ml_fundamentals.ipynb +++ b/notebooks/getting_started/ml_fundamentals.ipynb @@ -567,10 +567,10 @@ "name": "stdout", "output_type": "stream", "text": [ - "train_X shape: (267, 6)\n", - "test_X shape: (67, 6)\n", - "train_y shape: (267, 1)\n", - "test_y shape: (67, 1)\n" + "X_train shape: (267, 6)\n", + "X_test shape: (67, 6)\n", + "y_train shape: (267, 1)\n", + "y_test shape: (67, 1)\n" ] } ], @@ -584,14 +584,14 @@ "\n", "# This will split X and y into test and training sets, with 20% of the rows in the test set,\n", "# and the rest in the training set\n", - "train_X, test_X, train_y, test_y = train_test_split(\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", " X, y, test_size=0.2)\n", "\n", "# Show the shape of the data after the split\n", - "print(f\"\"\"train_X shape: {train_X.shape}\n", - "test_X shape: {test_X.shape}\n", - "train_y shape: {train_y.shape}\n", - "test_y shape: {test_y.shape}\"\"\")" + "print(f\"\"\"X_train shape: {X_train.shape}\n", + "X_test shape: {X_test.shape}\n", + "y_train shape: {y_train.shape}\n", + "y_test shape: {y_test.shape}\"\"\")" ] }, { @@ -758,7 +758,7 @@ "source": [ "# If we look at the data, we can see that random rows were selected for\n", "# each side of the split\n", - "test_X.head(5)" + "X_test.head(5)" ] }, { @@ -880,8 +880,8 @@ } ], "source": [ - "# Note that this matches the rows in test_X\n", - "test_y.head(5)" + "# Note that this matches the rows in X_test\n", + "y_test.head(5)" ] }, { @@ -1242,11 +1242,11 @@ "numeric_columns = [\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]\n", "\n", "scaler = StandardScaler()\n", - "scaler.fit(train_X[numeric_columns])\n", + "scaler.fit(X_train[numeric_columns])\n", "\n", "# Now, standardscaler should transform the numbers to have mean of zero\n", "# and standard deviation of one:\n", - "scaler.transform(train_X[numeric_columns])" + "scaler.transform(X_train[numeric_columns])" ] }, { @@ -1580,7 +1580,7 @@ ], "source": [ "# We can then repeat this transformation on new data\n", - "scaler.transform(test_X[numeric_columns])" + "scaler.transform(X_test[numeric_columns])" ] }, { @@ -2070,12 +2070,12 @@ " (\"encode\", OneHotEncoder(), [\"species\", \"sex\", \"island\"])])\n", "\n", "# Now we can fit all columns of the training data\n", - "preproc.fit(train_X)\n", + "preproc.fit(X_train)\n", "\n", - "processed_train_X = preproc.transform(train_X)\n", - "processed_test_X = preproc.transform(test_X)\n", + "processed_X_train = preproc.transform(X_train)\n", + "processed_X_test = preproc.transform(X_test)\n", "\n", - "processed_train_X" + "processed_X_train" ] }, { @@ -2347,12 +2347,12 @@ "linreg = LinearRegression()\n", "\n", "# Learn from the training data how to predict output y\n", - "linreg.fit(processed_train_X, train_y)\n", + "linreg.fit(processed_X_train, y_train)\n", "\n", "# Predict y for the test data\n", - "predicted_test_y = linreg.predict(processed_test_X)\n", + "predicted_y_test = linreg.predict(processed_X_test)\n", "\n", - "predicted_test_y" + "predicted_y_test" ] }, { @@ -2617,9 +2617,9 @@ "\n", "kmeans = KMeans(n_clusters=4)\n", "\n", - "kmeans.fit(processed_train_X)\n", + "kmeans.fit(processed_X_train)\n", "\n", - "kmeans.predict(processed_test_X)" + "kmeans.predict(processed_X_test)" ] }, { @@ -2924,10 +2924,10 @@ } ], "source": [ - "pipeline.fit(train_X, train_y)\n", + "pipeline.fit(X_train, y_train)\n", "\n", - "predicted_test_y = pipeline.predict(test_X)\n", - "predicted_test_y" + "predicted_y_test = pipeline.predict(X_test)\n", + "predicted_y_test" ] }, { @@ -3084,7 +3084,7 @@ ], "source": [ "# In the case of a pipeline, this will be equivalent to calling .score on the contained LinearRegression\n", - "pipeline.score(test_X, test_y)" + "pipeline.score(X_test, y_test)" ] }, { @@ -3156,7 +3156,7 @@ "source": [ "from bigframes.ml.metrics import r2_score\n", "\n", - "r2_score(test_y, predicted_test_y)" + "r2_score(y_test, predicted_y_test)" ] }, { diff --git a/notebooks/regression/sklearn_linear_regression.ipynb b/notebooks/regression/sklearn_linear_regression.ipynb index 8c0a21c0ff2..beb77ef092d 100644 --- a/notebooks/regression/sklearn_linear_regression.ipynb +++ b/notebooks/regression/sklearn_linear_regression.ipynb @@ -867,7 +867,7 @@ "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", "label_columns = training_data[['body_mass_g']] \n", "\n", - "train_X, test_X, train_y, test_y = train_test_split(\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", " feature_columns, label_columns, test_size=0.2)" ] }, @@ -940,7 +940,7 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline.fit(train_X, train_y)" + "pipeline.fit(X_train, y_train)" ] }, { @@ -1040,9 +1040,9 @@ "source": [ "from bigframes.ml.metrics import r2_score\n", "\n", - "pred_y = pipeline.predict(test_X)\n", + "pred_y = pipeline.predict(X_test)\n", "\n", - "r2_score(test_y, pred_y)" + "r2_score(y_test, pred_y)" ] }, { diff --git a/notebooks/remote_functions/remote_function.ipynb b/notebooks/remote_functions/remote_function.ipynb index 76c87f8629c..06be0e7293e 100644 --- a/notebooks/remote_functions/remote_function.ipynb +++ b/notebooks/remote_functions/remote_function.ipynb @@ -2,27 +2,38 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "3613b1cd", "metadata": {}, "outputs": [], "source": [ + "# BigQuery table data on which notebook should be run\n", "TABLE='bigquery-public-data.stackoverflow.comments'\n", - "MAX_ROWS=1000000 # 1 Million" + "\n", + "# Change this up to test the scale, down to run the notebook faster\n", + "MAX_ROWS=100000" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "f1175247", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", + " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 30.5 s, sys: 4.25 s, total: 34.8 s\n", - "Wall time: 1min 49s\n" + "CPU times: user 25.4 s, sys: 2.5 s, total: 27.9 s\n", + "Wall time: 2min 31s\n" ] }, { @@ -130,7 +141,7 @@ "9 154 Sure, but what about a solution using O(1) mem... 8" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -149,7 +160,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "fd8a04a3", "metadata": {}, "outputs": [], @@ -180,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "2b5e4568", "metadata": {}, "outputs": [ @@ -188,8 +199,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 5.48 s, sys: 27.6 ms, total: 5.51 s\n", - "Wall time: 5.49 s\n" + "CPU times: user 4.22 s, sys: 18.2 ms, total: 4.24 s\n", + "Wall time: 4.26 s\n" ] }, { @@ -308,7 +319,7 @@ "9 154 Sure, but what about a solution using O(1) mem... 8 19" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -322,18 +333,84 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "b81feaef", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", + " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n", + "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", + " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2b1c9d671db14d2ca3be6a0b0c698430", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 6b0a39de-40a0-4dd4-be88-248bd8ebcd77 is RUNNING. \n", " 0\n", " \n", " \n", " 1\n", - " 35156124\n", - " Sorry I didn't include my timeout method befor...\n", + " 11013760\n", + " You *should* be concerned with the disk being ...\n", " 0\n", " \n", " \n", " 2\n", - " 35157401\n", - " As soon as I defined some sort of primary Key ...\n", + " 11013784\n", + " have you looked at `Integrate` or `NIntegrate`?\n", " 0\n", " \n", " \n", " 3\n", - " 35158649\n", - " @user3355243 I've edited it to give $values an...\n", + " 11015512\n", + " sorry, is a typo. The variable name is dist. (...\n", " 0\n", " \n", " \n", " 4\n", - " 35162039\n", - " I pasted my exes @Matt.\n", + " 11016238\n", + " Pfff, I'm having trouble with that formula too...\n", " 0\n", " \n", " \n", " 5\n", - " 35162396\n", - " @Gene - I do have separate fields, but I also ...\n", + " 11016276\n", + " Thanks thinksteep! Does this mean that by usin...\n", " 0\n", " \n", " \n", " 6\n", - " 35162907\n", - " could you please provide any kind of sketch ho...\n", + " 11016551\n", + " Jason, thanks for the reply. I've been workin...\n", " 0\n", " \n", " \n", " 7\n", - " 35166498\n", - " We use PhoneGap. Using Angular.\n", + " 11017973\n", + " I assume an `off` of 0.5 would put be exactly ...\n", " 0\n", " \n", " \n", " 8\n", - " 35170138\n", - " `decltype((int(Foo::*)(int))(&Foo::foo))` That...\n", + " 11018225\n", + " Thank you very much. I do worry too much abou...\n", " 0\n", " \n", " \n", " 9\n", - " 35172348\n", - " Yes, I found that one already. And I understan...\n", + " 11018370\n", + " @IanClelland, I edited my question a bit. The ...\n", " 0\n", " \n", " \n", "\n", + "

10 rows × 3 columns

\n", "[10 rows x 3 columns in total]" ], "text/plain": [ " id text score\n", - "0 35153602 Are you trying to access a nested array? 0\n", - "1 35156124 Sorry I didn't include my timeout method befor... 0\n", - "2 35157401 As soon as I defined some sort of primary Key ... 0\n", - "3 35158649 @user3355243 I've edited it to give $values an... 0\n", - "4 35162039 I pasted my exes @Matt. 0\n", - "5 35162396 @Gene - I do have separate fields, but I also ... 0\n", - "6 35162907 could you please provide any kind of sketch ho... 0\n", - "7 35166498 We use PhoneGap. Using Angular. 0\n", - "8 35170138 `decltype((int(Foo::*)(int))(&Foo::foo))` That... 0\n", - "9 35172348 Yes, I found that one already. And I understan... 0\n", + "0 11012908 you're welcome! according to the docs it shoul... 0\n", + "1 11013760 You *should* be concerned with the disk being ... 0\n", + "2 11013784 have you looked at `Integrate` or `NIntegrate`? 0\n", + "3 11015512 sorry, is a typo. The variable name is dist. (... 0\n", + "4 11016238 Pfff, I'm having trouble with that formula too... 0\n", + "5 11016276 Thanks thinksteep! Does this mean that by usin... 0\n", + "6 11016551 Jason, thanks for the reply. I've been workin... 0\n", + "7 11017973 I assume an `off` of 0.5 would put be exactly ... 0\n", + "8 11018225 Thank you very much. I do worry too much abou... 0\n", + "9 11018370 @IanClelland, I edited my question a bit. The ... 0\n", "\n", "[10 rows x 3 columns]" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -461,7 +539,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "55ed241e", "metadata": {}, "outputs": [ @@ -474,59 +552,73 @@ "remote_function(input_types: 'List[type]', output_type: 'type', dataset: 'Optional[str]' = None, bigquery_connection: 'Optional[str]' = None, reuse: 'bool' = True)\n", " Decorator to turn a user defined function into a BigQuery remote function.\n", " \n", + " .. note::\n", + " Please make sure following is setup before using this API:\n", + " \n", + " 1. Have the below APIs enabled for your project:\n", + " \n", + " * BigQuery Connection API\n", + " * Cloud Functions API\n", + " * Cloud Run API\n", + " * Cloud Build API\n", + " * Artifact Registry API\n", + " * Cloud Resource Manager API\n", + " \n", + " This can be done from the cloud console (change `PROJECT_ID` to yours):\n", + " https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID\n", + " \n", + " Or from the gcloud CLI:\n", + " \n", + " `$ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com`\n", + " \n", + " 2. Have following IAM roles enabled for you:\n", + " \n", + " * BigQuery Data Editor (roles/bigquery.dataEditor)\n", + " * BigQuery Connection Admin (roles/bigquery.connectionAdmin)\n", + " * Cloud Functions Developer (roles/cloudfunctions.developer)\n", + " * Service Account User (roles/iam.serviceAccountUser)\n", + " * Storage Object Viewer (roles/storage.objectViewer)\n", + " * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.)\n", + " \n", + " 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set:\n", + " \n", + " 1. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection\n", + " 2. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function\n", + " \n", + " Alternatively, the IAM could also be setup via the gcloud CLI:\n", + " \n", + " `$ gcloud projects add-iam-policy-binding PROJECT_ID --member=\"serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID\" --role=\"roles/run.invoker\"`.\n", + " \n", " Args:\n", - " input_types : list(type)\n", + " input_types (list(type)):\n", " List of input data types in the user defined function.\n", - " output_type : type\n", + " output_type (type):\n", " Data type of the output in the user defined function.\n", - " dataset : str, Optional\n", - " Dataset to use to create a BigQuery function. It should be in\n", + " dataset (str, Optional):\n", + " Dataset in which to create a BigQuery remote function. It should be in\n", " `.` or `` format. If this\n", - " param is not provided then session dataset id would be used.\n", - " bigquery_connection : str, Optional\n", - " Name of the BigQuery connection. If it is pre created in the same\n", - " location as the `bigquery_client.location` then it would be used,\n", - " otherwise it would be created dynamically assuming the user has\n", - " necessary priviliges. If this param is not provided then the\n", - " bigquery connection from the session would be used.\n", - " reuse : bool, Optional\n", + " parameter is not provided then session dataset id is used.\n", + " bigquery_connection (str, Optional):\n", + " Name of the BigQuery connection. You should either have the\n", + " connection already created in the `location` you have chosen, or\n", + " you should have the Project IAM Admin role to enable the service\n", + " to create the connection for you if you need it.If this parameter is\n", + " not provided then the BigQuery connection from the session is used.\n", + " reuse (bool, Optional):\n", " Reuse the remote function if already exists.\n", " `True` by default, which will result in reusing an existing remote\n", " function (if any) that was previously created for the same udf.\n", " Setting it to false would force creating a unique remote function.\n", " If the required remote function does not exist then it would be\n", " created irrespective of this param.\n", + " Returns:\n", + " callable: A remote function object pointing to the cloud assets created\n", + " in the background to support the remote execution. The cloud assets can be\n", + " located through the following properties set in the object:\n", " \n", - " Notes:\n", - " Please make sure following is setup before using this API:\n", - " \n", - " 1. Have the below APIs enabled for your project:\n", - " a. BigQuery Connection API\n", - " b. Cloud Functions API\n", - " c. Cloud Run API\n", - " d. Cloud Build API\n", - " e. Artifact Registry API\n", - " f. Cloud Resource Manager API\n", + " `bigframes_cloud_function` - The google cloud function deployed for the user defined code.\n", " \n", - " This can be done from the cloud console (change PROJECT_ID to yours):\n", - " https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID\n", - " Or from the gcloud CLI:\n", - " $ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com\n", - " \n", - " 2. Have following IAM roles enabled for you:\n", - " a. BigQuery Data Editor (roles/bigquery.dataEditor)\n", - " b. BigQuery Connection Admin (roles/bigquery.connectionAdmin)\n", - " c. Cloud Functions Developer (roles/cloudfunctions.developer)\n", - " d. Service Account User (roles/iam.serviceAccountUser)\n", - " e. Storage Object Viewer (roles/storage.objectViewer)\n", - " f. Project IAM Admin (roles/resourcemanager.projectIamAdmin)\n", - " (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.)\n", - " \n", - " 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set:\n", - " a. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection\n", - " b. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function\n", - " Alternatively, the IAM could also be setup via the gcloud CLI:\n", - " $ gcloud projects add-iam-policy-binding PROJECT_ID --member=\"serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID\" --role=\"roles/run.invoker\"\n", + " `bigframes_remote_function` - The bigquery remote function capable of calling into `bigframes_cloud_function`.\n", "\n" ] } @@ -539,7 +631,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "c9a8d03d", "metadata": {}, "outputs": [], @@ -555,7 +647,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "fbc27f81", "metadata": {}, "outputs": [ @@ -563,69 +655,17 @@ "name": "stderr", "output_type": "stream", "text": [ - "[INFO][2023-06-29 01:03:30,557][bigframes.remote_function] Creating new cloud function: gcloud functions deploy bigframes-ac72c931423f68dddb4f84f6754e2b28 --gen2 --runtime=python310 --project=bigframes-dev --region=us-central1 --source=/tmp/tmposspvw3v --entry-point=udf_http --trigger-http --no-allow-unauthenticated\n", - "Preparing function...\n", - ".done.\n", - "Deploying function...\n", - "[Build]......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................done\n", - "[Service]....................................................................................................done\n", - "Done.\n", - "You can view your function in the Cloud Console here: https://console.cloud.google.com/functions/details/us-central1/bigframes-ac72c931423f68dddb4f84f6754e2b28?project=bigframes-dev\n", - "\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "buildConfig:\n", - " build: projects/1084210331973/locations/us-central1/builds/57859622-4287-47bc-ab86-3cd3f4c47548\n", - " entryPoint: udf_http\n", - " runtime: python310\n", - " source:\n", - " storageSource:\n", - " bucket: gcf-v2-sources-1084210331973-us-central1\n", - " object: bigframes-ac72c931423f68dddb4f84f6754e2b28/function-source.zip\n", - " sourceProvenance:\n", - " resolvedStorageSource:\n", - " bucket: gcf-v2-sources-1084210331973-us-central1\n", - " generation: '1688000614202977'\n", - " object: bigframes-ac72c931423f68dddb4f84f6754e2b28/function-source.zip\n", - "environment: GEN_2\n", - "labels:\n", - " deployment-tool: cli-gcloud\n", - "name: projects/bigframes-dev/locations/us-central1/functions/bigframes-ac72c931423f68dddb4f84f6754e2b28\n", - "serviceConfig:\n", - " allTrafficOnLatestRevision: true\n", - " availableCpu: '0.1666'\n", - " availableMemory: 256M\n", - " ingressSettings: ALLOW_ALL\n", - " maxInstanceCount: 100\n", - " maxInstanceRequestConcurrency: 1\n", - " revision: bigframes-ac72c931423f68dddb4f84f6754e2b28-00001-faf\n", - " service: projects/bigframes-dev/locations/us-central1/services/bigframes-ac72c931423f68dddb4f84f6754e2b28\n", - " serviceAccountEmail: 1084210331973-compute@developer.gserviceaccount.com\n", - " timeoutSeconds: 60\n", - " uri: https://bigframes-ac72c931423f68dddb4f84f6754e2b28-7krlje3eoq-uc.a.run.app\n", - "state: ACTIVE\n", - "updateTime: '2023-06-29T01:04:34.306070995Z'\n", - "url: https://us-central1-bigframes-dev.cloudfunctions.net/bigframes-ac72c931423f68dddb4f84f6754e2b28\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO][2023-06-29 01:04:37,239][bigframes.remote_function] Successfully created cloud function bigframes-ac72c931423f68dddb4f84f6754e2b28 with uri (https://bigframes-ac72c931423f68dddb4f84f6754e2b28-7krlje3eoq-uc.a.run.app)\n", - "[INFO][2023-06-29 01:04:42,402][bigframes.remote_function] Connector bigframes-rf-conn already exists\n", - "[INFO][2023-06-29 01:04:42,404][bigframes.remote_function] Creating BQ remote function: \n", - " CREATE OR REPLACE FUNCTION `bigframes-dev.bigframes_temp_us`.bigframes_ac72c931423f68dddb4f84f6754e2b28(n INT64)\n", - " RETURNS INT64\n", - " REMOTE WITH CONNECTION `bigframes-dev.us.bigframes-rf-conn`\n", - " OPTIONS (\n", - " endpoint = \"https://bigframes-ac72c931423f68dddb4f84f6754e2b28-7krlje3eoq-uc.a.run.app\"\n", - " )\n", - "[INFO][2023-06-29 01:04:43,456][bigframes.remote_function] Created remote function bigframes-dev.bigframes_temp_us.bigframes_ac72c931423f68dddb4f84f6754e2b28\n" + "[INFO][2023-08-18 21:23:29,687][bigframes.remote_function] Creating new cloud function: gcloud functions deploy bigframes-b0feb1fbaf8188b64d7e70118d93c5d4 --gen2 --runtime=python310 --project=bigframes-dev --region=us-central1 --source=/tmp/tmpl2ewfnue --entry-point=udf_http --trigger-http --no-allow-unauthenticated\n", + "[INFO][2023-08-18 21:24:43,689][bigframes.remote_function] Successfully created cloud function bigframes-b0feb1fbaf8188b64d7e70118d93c5d4 with uri (https://bigframes-b0feb1fbaf8188b64d7e70118d93c5d4-7krlje3eoq-uc.a.run.app)\n", + "[INFO][2023-08-18 21:24:57,348][bigframes.remote_function] Connector bigframes-rf-conn already exists\n", + "[INFO][2023-08-18 21:24:57,351][bigframes.remote_function] Creating BQ remote function: \n", + " CREATE OR REPLACE FUNCTION `bigframes-dev.bigframes_temp_us`.bigframes_b0feb1fbaf8188b64d7e70118d93c5d4(n INT64)\n", + " RETURNS INT64\n", + " REMOTE WITH CONNECTION `bigframes-dev.us.bigframes-rf-conn`\n", + " OPTIONS (\n", + " endpoint = \"https://bigframes-b0feb1fbaf8188b64d7e70118d93c5d4-7krlje3eoq-uc.a.run.app\"\n", + " )\n", + "[INFO][2023-08-18 21:24:58,300][bigframes.remote_function] Created remote function bigframes-dev.bigframes_temp_us.bigframes_b0feb1fbaf8188b64d7e70118d93c5d4\n" ] }, { @@ -633,7 +673,7 @@ "output_type": "stream", "text": [ "\n", - "Wall time: 76.2659 s\n" + "Wall time: 89.0601 s\n" ] } ], @@ -672,7 +712,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "c1c9355f", "metadata": {}, "outputs": [ @@ -680,10 +720,52 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 22.2 ms, sys: 0 ns, total: 22.2 ms\n", - "Wall time: 22.4 ms\n" + "CPU times: user 16.8 ms, sys: 61 µs, total: 16.8 ms\n", + "Wall time: 17 ms\n" ] }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2f840ad27c514ed19c759a004b32de33", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 0f421233-9d02-4746-bb39-86a3b0880aba is RUNNING.
\n", " \n", " 0\n", - " 35153602\n", - " Are you trying to access a nested array?\n", + " 11012908\n", + " you're welcome! according to the docs it shoul...\n", " 0\n", " -1\n", " \n", " \n", " 1\n", - " 35156124\n", - " Sorry I didn't include my timeout method befor...\n", + " 11013760\n", + " You *should* be concerned with the disk being ...\n", " 0\n", " -1\n", " \n", " \n", " 2\n", - " 35157401\n", - " As soon as I defined some sort of primary Key ...\n", + " 11013784\n", + " have you looked at `Integrate` or `NIntegrate`?\n", " 0\n", " -1\n", " \n", " \n", " 3\n", - " 35158649\n", - " @user3355243 I've edited it to give $values an...\n", + " 11015512\n", + " sorry, is a typo. The variable name is dist. (...\n", " 0\n", " -1\n", " \n", " \n", " 4\n", - " 35162039\n", - " I pasted my exes @Matt.\n", + " 11016238\n", + " Pfff, I'm having trouble with that formula too...\n", " 0\n", " -1\n", " \n", " \n", " 5\n", - " 35162396\n", - " @Gene - I do have separate fields, but I also ...\n", + " 11016276\n", + " Thanks thinksteep! Does this mean that by usin...\n", " 0\n", " -1\n", " \n", " \n", " 6\n", - " 35162907\n", - " could you please provide any kind of sketch ho...\n", + " 11016551\n", + " Jason, thanks for the reply. I've been workin...\n", " 0\n", " -1\n", " \n", " \n", " 7\n", - " 35166498\n", - " We use PhoneGap. Using Angular.\n", + " 11017973\n", + " I assume an `off` of 0.5 would put be exactly ...\n", " 0\n", " -1\n", " \n", " \n", " 8\n", - " 35170138\n", - " `decltype((int(Foo::*)(int))(&Foo::foo))` That...\n", + " 11018225\n", + " Thank you very much. I do worry too much abou...\n", " 0\n", " -1\n", " \n", " \n", " 9\n", - " 35172348\n", - " Yes, I found that one already. And I understan...\n", + " 11018370\n", + " @IanClelland, I edited my question a bit. The ...\n", " 0\n", " -1\n", " \n", " \n", "\n", + "

10 rows × 4 columns

\n", "[10 rows x 4 columns in total]" ], "text/plain": [ " id text score n_prime\n", - "0 35153602 Are you trying to access a nested array? 0 -1\n", - "1 35156124 Sorry I didn't include my timeout method befor... 0 -1\n", - "2 35157401 As soon as I defined some sort of primary Key ... 0 -1\n", - "3 35158649 @user3355243 I've edited it to give $values an... 0 -1\n", - "4 35162039 I pasted my exes @Matt. 0 -1\n", - "5 35162396 @Gene - I do have separate fields, but I also ... 0 -1\n", - "6 35162907 could you please provide any kind of sketch ho... 0 -1\n", - "7 35166498 We use PhoneGap. Using Angular. 0 -1\n", - "8 35170138 `decltype((int(Foo::*)(int))(&Foo::foo))` That... 0 -1\n", - "9 35172348 Yes, I found that one already. And I understan... 0 -1\n", + "0 11012908 you're welcome! according to the docs it shoul... 0 -1\n", + "1 11013760 You *should* be concerned with the disk being ... 0 -1\n", + "2 11013784 have you looked at `Integrate` or `NIntegrate`? 0 -1\n", + "3 11015512 sorry, is a typo. The variable name is dist. (... 0 -1\n", + "4 11016238 Pfff, I'm having trouble with that formula too... 0 -1\n", + "5 11016276 Thanks thinksteep! Does this mean that by usin... 0 -1\n", + "6 11016551 Jason, thanks for the reply. I've been workin... 0 -1\n", + "7 11017973 I assume an `off` of 0.5 would put be exactly ... 0 -1\n", + "8 11018225 Thank you very much. I do worry too much abou... 0 -1\n", + "9 11018370 @IanClelland, I edited my question a bit. The ... 0 -1\n", "\n", "[10 rows x 4 columns]" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -810,9 +893,299 @@ "source": [ "%%time\n", "\n", + "# Let's apply the function to the dataframe\n", "df = df.assign(n_prime=df['score'].apply(nth_prime))\n", "df.head(10)" ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2701cb81", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bigframes-dev.bigframes_temp_us.bigframes_b0feb1fbaf8188b64d7e70118d93c5d4\n", + "projects/bigframes-dev/locations/us-central1/functions/bigframes-b0feb1fbaf8188b64d7e70118d93c5d4\n" + ] + } + ], + "source": [ + "# We can see the path to the BQ remote function and the google cloud function\n", + "# that was created under the hood\n", + "print(nth_prime.bigframes_remote_function)\n", + "print(nth_prime.bigframes_cloud_function)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "920fa18e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function read_gbq_function in module bigframes.pandas:\n", + "\n", + "read_gbq_function(function_name: 'str')\n", + " Loads a BigQuery function from BigQuery.\n", + " \n", + " Then it can be applied to a DataFrame or Series.\n", + " \n", + " Args:\n", + " function_name (str):\n", + " the function's name in BigQuery in the format\n", + " `project_id.dataset_id.function_name`, or\n", + " `dataset_id.function_name` to load from the default project, or\n", + " `function_name` to load from the default project and the dataset\n", + " associated with the current session.\n", + " \n", + " Returns:\n", + " callable: A function object pointing to the BigQuery function read\n", + " from BigQuery.\n", + " \n", + " The object is similar to the one created by the `remote_function`\n", + " decorator, including the `bigframes_remote_function` property, but\n", + " not including the `bigframes_cloud_function` property.\n", + "\n" + ] + } + ], + "source": [ + "# Let's try to simulate a scenario in which user shares this remote funciton to\n", + "# their colleague who simply wants to reuse it. BigFrames provides an API to do\n", + "# so via `read_gbq_function`. Usage details are available via `help` command.\n", + "help(pd.read_gbq_function)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a6c9da0a", + "metadata": {}, + "outputs": [], + "source": [ + "EXISTING_REMOTE_FUNCTION=nth_prime.bigframes_remote_function\n", + "\n", + "# Let's read the existing remote function in bigframes\n", + "nth_prime_existing = pd.read_gbq_function(EXISTING_REMOTE_FUNCTION)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "d7e7de7f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 10.9 ms, sys: 0 ns, total: 10.9 ms\n", + "Wall time: 11.4 ms\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "73d1a73593cb4115821ab128c221a48d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job bec5f7d1-3df1-4292-8c68-c396bce7dc5d is RUNNING.
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idtextscoren_primen_prime_again
011012908you're welcome! according to the docs it shoul...0-1-1
111013760You *should* be concerned with the disk being ...0-1-1
211013784have you looked at `Integrate` or `NIntegrate`?0-1-1
311015512sorry, is a typo. The variable name is dist. (...0-1-1
411016238Pfff, I'm having trouble with that formula too...0-1-1
511016276Thanks thinksteep! Does this mean that by usin...0-1-1
611016551Jason, thanks for the reply. I've been workin...0-1-1
711017973I assume an `off` of 0.5 would put be exactly ...0-1-1
811018225Thank you very much. I do worry too much abou...0-1-1
911018370@IanClelland, I edited my question a bit. The ...0-1-1
\n", + "

10 rows × 5 columns

\n", + "[10 rows x 5 columns in total]" + ], + "text/plain": [ + " id text score \\\n", + "0 11012908 you're welcome! according to the docs it shoul... 0 \n", + "1 11013760 You *should* be concerned with the disk being ... 0 \n", + "2 11013784 have you looked at `Integrate` or `NIntegrate`? 0 \n", + "3 11015512 sorry, is a typo. The variable name is dist. (... 0 \n", + "4 11016238 Pfff, I'm having trouble with that formula too... 0 \n", + "5 11016276 Thanks thinksteep! Does this mean that by usin... 0 \n", + "6 11016551 Jason, thanks for the reply. I've been workin... 0 \n", + "7 11017973 I assume an `off` of 0.5 would put be exactly ... 0 \n", + "8 11018225 Thank you very much. I do worry too much abou... 0 \n", + "9 11018370 @IanClelland, I edited my question a bit. The ... 0 \n", + "\n", + " n_prime n_prime_again \n", + "0 -1 -1 \n", + "1 -1 -1 \n", + "2 -1 -1 \n", + "3 -1 -1 \n", + "4 -1 -1 \n", + "5 -1 -1 \n", + "6 -1 -1 \n", + "7 -1 -1 \n", + "8 -1 -1 \n", + "9 -1 -1 \n", + "\n", + "[10 rows x 5 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "\n", + "# Let's apply the existing function to the dataframe\n", + "df = df.assign(n_prime_again=df['score'].apply(nth_prime_existing))\n", + "df.head(10)" + ] } ], "metadata": { @@ -831,7 +1204,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/tests/system/conftest.py b/tests/system/conftest.py index bc94e32e12d..3153bd1559e 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -849,7 +849,7 @@ def floats_pd(): { "float64_col": [ float("-inf"), - float("-inf"), + float("inf"), float("nan"), float(-234239487.4), float(-1.0), @@ -863,17 +863,33 @@ def floats_pd(): float(math.e), float(math.pi), float(234239487.4), + float(1.23124 * (2**70)), pd.NA, ] }, dtype=pd.Float64Dtype(), ) + # Index helps debug failed cases df.index = df.float64_col # Upload fails if index name same as column name df.index.name = None return df.float64_col +@pytest.fixture() +def floats_product_pd(floats_pd): + df = pd.merge(floats_pd, floats_pd, how="cross") + # Index helps debug failed cases + df = df.set_index([df.float64_col_x, df.float64_col_y]) + df.index.names = ["left", "right"] + return df + + @pytest.fixture() def floats_bf(session, floats_pd): return session.read_pandas(floats_pd.to_frame()).float64_col + + +@pytest.fixture() +def floats_product_bf(session, floats_product_pd): + return session.read_pandas(floats_product_pd) diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py index ab33e5d7185..133af2dae4a 100644 --- a/tests/system/large/ml/test_core.py +++ b/tests/system/large/ml/test_core.py @@ -20,7 +20,7 @@ def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_df): df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "species", "island", @@ -30,10 +30,10 @@ def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_d "sex", ] ] - train_y = df[["body_mass_g"]] + y_train = df[["body_mass_g"]] model = bigframes.ml.core.create_bqml_model( - train_X, train_y, options={"model_type": "linear_reg"} + X_train, y_train, options={"model_type": "linear_reg"} ) # no data - report evaluation from the automatic data split @@ -85,22 +85,22 @@ def test_bqml_manual_preprocessing_e2e( session, dataset_id, penguins_df_default_index, new_penguins_df ): df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", ] ] - train_y = df[["body_mass_g"]] + y_train = df[["body_mass_g"]] transforms = [ bigframes.ml.sql.ml_standard_scaler(column, column) - for column in train_X.columns.tolist() + for column in X_train.columns.tolist() ] - transforms.extend(train_y.columns.tolist()) + transforms.extend(y_train.columns.tolist()) options = {"model_type": "linear_reg"} model = bigframes.ml.core.create_bqml_model( - train_X, train_y, transforms=transforms, options=options + X_train, y_train, transforms=transforms, options=options ) # no data - report evaluation from the automatic data split diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index 88c5ccd2f0d..9b2872d6737 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -25,7 +25,7 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBRegressor() df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "species", "island", @@ -35,11 +35,11 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): "sex", ] ] - train_y = df[["body_mass_g"]] - model.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [97.368139], @@ -86,7 +86,7 @@ def test_xgbregressor_dart_booster_multiple_params( ) df = penguins_df_default_index.dropna().sample(n=70) - train_X = df[ + X_train = df[ [ "species", "island", @@ -96,11 +96,11 @@ def test_xgbregressor_dart_booster_multiple_params( "sex", ] ] - train_y = df[["body_mass_g"]] - model.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "mean_absolute_error", @@ -144,7 +144,7 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBClassifier() df = penguins_df_default_index.dropna().sample(n=70) - train_X = df[ + X_train = df[ [ "species", "island", @@ -153,11 +153,11 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): "flipper_length_mm", ] ] - train_y = df[["sex"]] - model.fit(train_X, train_y) + y_train = df[["sex"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -201,7 +201,7 @@ def test_xgbclassifier_dart_booster_multiple_params( ) df = penguins_df_default_index.dropna().sample(n=70) - train_X = df[ + X_train = df[ [ "species", "island", @@ -210,11 +210,11 @@ def test_xgbclassifier_dart_booster_multiple_params( "flipper_length_mm", ] ] - train_y = df[["sex"]] - model.fit(train_X, train_y) + y_train = df[["sex"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -258,7 +258,7 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset model = bigframes.ml.ensemble.RandomForestRegressor() df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "species", "island", @@ -268,11 +268,11 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset "sex", ] ] - train_y = df[["body_mass_g"]] - model.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "mean_absolute_error", @@ -311,7 +311,7 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase ) df = penguins_df_default_index.dropna().sample(n=70) - train_X = df[ + X_train = df[ [ "species", "island", @@ -321,11 +321,11 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase "sex", ] ] - train_y = df[["body_mass_g"]] - model.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "mean_absolute_error", @@ -366,7 +366,7 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase model = bigframes.ml.ensemble.RandomForestClassifier() df = penguins_df_default_index.dropna().sample(n=70) - train_X = df[ + X_train = df[ [ "species", "island", @@ -375,11 +375,11 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase "flipper_length_mm", ] ] - train_y = df[["sex"]] - model.fit(train_X, train_y) + y_train = df[["sex"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -418,7 +418,7 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas ) df = penguins_df_default_index.dropna().sample(n=70) - train_X = df[ + X_train = df[ [ "species", "island", @@ -427,11 +427,11 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas "flipper_length_mm", ] ] - train_y = df[["sex"]] - model.fit(train_X, train_y) + y_train = df[["sex"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index d1e2d12296d..33b835e8522 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -21,9 +21,9 @@ def test_arima_plus_model_fit_score( time_series_df_default_index, dataset_id, new_time_series_df ): model = forecasting.ARIMAPlus() - train_X = time_series_df_default_index[["parsed_date"]] - train_y = time_series_df_default_index[["total_visits"]] - model.fit(train_X, train_y) + X_train = time_series_df_default_index[["parsed_date"]] + y_train = time_series_df_default_index[["total_visits"]] + model.fit(X_train, y_train) result = model.score( new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 332b460fe51..3b90568450b 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -21,7 +21,7 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase model = bigframes.ml.linear_model.LinearRegression(fit_intercept=False) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "species", "island", @@ -31,11 +31,11 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase "sex", ] ] - train_y = df[["body_mass_g"]] - model.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { "mean_absolute_error": [225.735767], @@ -66,7 +66,7 @@ def test_linear_regression_manual_split_configure_fit_score( model = bigframes.ml.linear_model.LinearRegression(fit_intercept=True) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "species", "island", @@ -76,11 +76,11 @@ def test_linear_regression_manual_split_configure_fit_score( "sex", ] ] - train_y = df[["body_mass_g"]] - model.fit(train_X, train_y) + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { "mean_absolute_error": [225.735767], @@ -108,7 +108,7 @@ def test_logistic_regression_auto_class_weights_configure_fit_score( ): model = bigframes.ml.linear_model.LogisticRegression() df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "species", "island", @@ -117,11 +117,11 @@ def test_logistic_regression_auto_class_weights_configure_fit_score( "flipper_length_mm", ] ] - train_y = df[["sex"]] - model.fit(train_X, train_y) + y_train = df[["sex"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { "precision": [0.58085], @@ -155,7 +155,7 @@ def test_logistic_regression_manual_split_configure_fit_score( model = bigframes.ml.linear_model.LogisticRegression(fit_intercept=True) df = penguins_df_default_index.dropna() - train_X = df[ + X_train = df[ [ "species", "island", @@ -165,11 +165,11 @@ def test_logistic_regression_manual_split_configure_fit_score( "body_mass_g", ] ] - train_y = df[["sex"]] - model.fit(train_X, train_y) + y_train = df[["sex"]] + model.fit(X_train, y_train) # Check score to ensure the model was fitted - result = model.score(train_X, train_y).to_pandas() + result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { "precision": [0.616753], diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index ebe768b685e..9ca5a2fd0ef 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -32,8 +32,8 @@ @pytest.fixture(scope="session") -def ml_connection() -> str: - return "bigframes-dev.us.bigframes-ml" +def bq_connection() -> str: + return "bigframes-dev.us.bigframes-rf-conn" @pytest.fixture(scope="session") @@ -198,33 +198,33 @@ def llm_text_df(session, llm_text_pandas_df): @pytest.fixture(scope="session") -def bqml_palm2_text_generator_model(session, ml_connection) -> core.BqmlModel: +def bqml_palm2_text_generator_model(session, bq_connection) -> core.BqmlModel: options = { "remote_service_type": "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1", } return core.create_bqml_remote_model( - session=session, connection_name=ml_connection, options=options + session=session, connection_name=bq_connection, options=options ) @pytest.fixture(scope="session") -def palm2_text_generator_model(session, ml_connection) -> llm.PaLM2TextGenerator: - return llm.PaLM2TextGenerator(session=session, connection_name=ml_connection) +def palm2_text_generator_model(session, bq_connection) -> llm.PaLM2TextGenerator: + return llm.PaLM2TextGenerator(session=session, connection_name=bq_connection) @pytest.fixture(scope="function") def ephemera_palm2_text_generator_model( - session, ml_connection + session, bq_connection ) -> llm.PaLM2TextGenerator: - return llm.PaLM2TextGenerator(session=session, connection_name=ml_connection) + return llm.PaLM2TextGenerator(session=session, connection_name=bq_connection) @pytest.fixture(scope="session") def palm2_embedding_generator_model( - session, ml_connection + session, bq_connection ) -> llm.PaLM2TextEmbeddingGenerator: return llm.PaLM2TextEmbeddingGenerator( - session=session, connection_name=ml_connection + session=session, connection_name=bq_connection ) @@ -247,10 +247,22 @@ def time_series_arima_plus_model( @pytest.fixture(scope="session") -def imported_tensorflow_model(session) -> imported.TensorFlowModel: +def imported_tensorflow_model_path() -> str: + return "gs://cloud-training-demos/txtclass/export/exporter/1549825580/*" + + +@pytest.fixture(scope="session") +def imported_onnx_model_path() -> str: + return "gs://cloud-samples-data/bigquery/ml/onnx/pipeline_rf.onnx" + + +@pytest.fixture(scope="session") +def imported_tensorflow_model( + session, imported_tensorflow_model_path +) -> imported.TensorFlowModel: return imported.TensorFlowModel( session=session, - model_path="gs://cloud-training-demos/txtclass/export/exporter/1549825580/*", + model_path=imported_tensorflow_model_path, ) @@ -263,8 +275,8 @@ def ephemera_imported_tensorflow_model(session) -> imported.TensorFlowModel: @pytest.fixture(scope="session") -def imported_onnx_model(session) -> imported.ONNXModel: +def imported_onnx_model(session, imported_onnx_model_path) -> imported.ONNXModel: return imported.ONNXModel( session=session, - model_path="gs://cloud-samples-data/bigquery/ml/onnx/pipeline_rf.onnx", + model_path=imported_onnx_model_path, ) diff --git a/tests/system/small/ml/test_ensemble.py b/tests/system/small/ml/test_ensemble.py index fde3cc431e1..bba083d98d9 100644 --- a/tests/system/small/ml/test_ensemble.py +++ b/tests/system/small/ml/test_ensemble.py @@ -25,7 +25,7 @@ def test_xgbregressor_model_score( penguins_xgbregressor_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -35,8 +35,8 @@ def test_xgbregressor_model_score( "body_mass_g", ] ] - test_y = df[["sex"]] - result = penguins_xgbregressor_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = penguins_xgbregressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [108.77582], @@ -62,7 +62,7 @@ def test_xgbregressor_model_score_series( penguins_xgbregressor_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -72,8 +72,8 @@ def test_xgbregressor_model_score_series( "body_mass_g", ] ] - test_y = df["sex"] - result = penguins_xgbregressor_model.score(test_X, test_y).to_pandas() + y_test = df["sex"] + result = penguins_xgbregressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [108.77582], @@ -120,7 +120,7 @@ def test_to_gbq_saved_xgbregressor_model_scores( f"{dataset_id}.test_penguins_model", replace=True ) df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -130,8 +130,8 @@ def test_to_gbq_saved_xgbregressor_model_scores( "body_mass_g", ] ] - test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [109.016973], @@ -165,7 +165,7 @@ def test_xgbclassifier_model_score( penguins_xgbclassifier_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -175,8 +175,8 @@ def test_xgbclassifier_model_score( "body_mass_g", ] ] - test_y = df[["sex"]] - result = penguins_xgbclassifier_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = penguins_xgbclassifier_model.score(X_test, y_test).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -193,7 +193,7 @@ def test_xgbclassifier_model_score_series( penguins_xgbclassifier_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -203,8 +203,8 @@ def test_xgbclassifier_model_score_series( "body_mass_g", ] ] - test_y = df["sex"] - result = penguins_xgbclassifier_model.score(test_X, test_y).to_pandas() + y_test = df["sex"] + result = penguins_xgbclassifier_model.score(X_test, y_test).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -242,7 +242,7 @@ def test_to_gbq_saved_xgbclassifier_model_scores( f"{dataset_id}.test_penguins_model", replace=True ) df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -252,8 +252,8 @@ def test_to_gbq_saved_xgbclassifier_model_scores( "body_mass_g", ] ] - test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "precision": [1.0], @@ -289,7 +289,7 @@ def test_randomforestregressor_model_score( penguins_randomforest_regressor_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -299,8 +299,8 @@ def test_randomforestregressor_model_score( "body_mass_g", ] ] - test_y = df[["sex"]] - result = penguins_randomforest_regressor_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = penguins_randomforest_regressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [317.031042], @@ -326,7 +326,7 @@ def test_randomforestregressor_model_score_series( penguins_randomforest_regressor_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -336,8 +336,8 @@ def test_randomforestregressor_model_score_series( "body_mass_g", ] ] - test_y = df["sex"] - result = penguins_randomforest_regressor_model.score(test_X, test_y).to_pandas() + y_test = df["sex"] + result = penguins_randomforest_regressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [317.031042], @@ -385,7 +385,7 @@ def test_to_gbq_saved_randomforestregressor_model_scores( f"{dataset_id}.test_penguins_model", replace=True ) df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -395,8 +395,8 @@ def test_to_gbq_saved_randomforestregressor_model_scores( "body_mass_g", ] ] - test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [319.239235], @@ -434,7 +434,7 @@ def test_randomforestclassifier_model_score( penguins_randomforest_classifier_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -444,8 +444,8 @@ def test_randomforestclassifier_model_score( "body_mass_g", ] ] - test_y = df[["sex"]] - result = penguins_randomforest_classifier_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = penguins_randomforest_classifier_model.score(X_test, y_test).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -462,7 +462,7 @@ def test_randomforestclassifier_model_score_series( penguins_randomforest_classifier_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -472,8 +472,8 @@ def test_randomforestclassifier_model_score_series( "body_mass_g", ] ] - test_y = df["sex"] - result = penguins_randomforest_classifier_model.score(test_X, test_y).to_pandas() + y_test = df["sex"] + result = penguins_randomforest_classifier_model.score(X_test, y_test).to_pandas() TestCase().assertSequenceEqual(result.shape, (1, 6)) for col_name in [ "precision", @@ -512,7 +512,7 @@ def test_to_gbq_saved_randomforestclassifier_model_scores( f"{dataset_id}.test_penguins_model", replace=True ) df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -522,8 +522,8 @@ def test_to_gbq_saved_randomforestclassifier_model_scores( "body_mass_g", ] ] - test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "precision": [0.636746], diff --git a/tests/system/small/ml/test_imported.py b/tests/system/small/ml/test_imported.py index 6274ab12450..d3055670668 100644 --- a/tests/system/small/ml/test_imported.py +++ b/tests/system/small/ml/test_imported.py @@ -25,6 +25,11 @@ def test_tensorflow_create_model(imported_tensorflow_model): assert imported_tensorflow_model is not None +def test_tensorflow_create_model_default_session(imported_tensorflow_model_path): + model = imported.TensorFlowModel(model_path=imported_tensorflow_model_path) + assert model is not None + + def test_tensorflow_model_predict(imported_tensorflow_model, llm_text_df): df = llm_text_df.rename(columns={"prompt": "input"}) result = imported_tensorflow_model.predict(df).to_pandas() @@ -61,6 +66,11 @@ def test_onnx_create_model(imported_onnx_model): assert imported_onnx_model is not None +def test_onnx_create_model_default_session(imported_onnx_model_path): + model = imported.TensorFlowModel(model_path=imported_onnx_model_path) + assert model is not None + + def test_onnx_model_predict(imported_onnx_model, onnx_iris_df): result = imported_onnx_model.predict(onnx_iris_df).to_pandas() value1 = np.array([0.9999993443489075, 0.0, 0.0]) diff --git a/tests/system/small/ml/test_linear_model.py b/tests/system/small/ml/test_linear_model.py index bbb7e2820c3..3a8232ed9ec 100644 --- a/tests/system/small/ml/test_linear_model.py +++ b/tests/system/small/ml/test_linear_model.py @@ -19,7 +19,7 @@ def test_linear_reg_model_score(penguins_linear_model, penguins_df_default_index): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -29,8 +29,8 @@ def test_linear_reg_model_score(penguins_linear_model, penguins_df_default_index "sex", ] ] - test_y = df[["body_mass_g"]] - result = penguins_linear_model.score(test_X, test_y).to_pandas() + y_test = df[["body_mass_g"]] + result = penguins_linear_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [225.817334], @@ -56,7 +56,7 @@ def test_linear_reg_model_score_series( penguins_linear_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -66,8 +66,8 @@ def test_linear_reg_model_score_series( "sex", ] ] - test_y = df["body_mass_g"] - result = penguins_linear_model.score(test_X, test_y).to_pandas() + y_test = df["body_mass_g"] + result = penguins_linear_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [225.817334], @@ -111,7 +111,7 @@ def test_to_gbq_saved_linear_reg_model_scores( f"{dataset_id}.test_penguins_model", replace=True ) df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -121,8 +121,8 @@ def test_to_gbq_saved_linear_reg_model_scores( "sex", ] ] - test_y = df[["body_mass_g"]] - result = saved_model.score(test_X, test_y).to_pandas() + y_test = df[["body_mass_g"]] + result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "mean_absolute_error": [227.01223], @@ -152,7 +152,7 @@ def test_to_gbq_replace(penguins_linear_model, dataset_id): def test_logistic_model_score(penguins_logistic_model, penguins_df_default_index): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -162,8 +162,8 @@ def test_logistic_model_score(penguins_logistic_model, penguins_df_default_index "body_mass_g", ] ] - test_y = df[["sex"]] - result = penguins_logistic_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = penguins_logistic_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "precision": [0.616753], @@ -189,7 +189,7 @@ def test_logistic_model_score_series( penguins_logistic_model, penguins_df_default_index ): df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -199,8 +199,8 @@ def test_logistic_model_score_series( "body_mass_g", ] ] - test_y = df["sex"] - result = penguins_logistic_model.score(test_X, test_y).to_pandas() + y_test = df["sex"] + result = penguins_logistic_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "precision": [0.616753], @@ -244,7 +244,7 @@ def test_logsitic_model_to_gbq_saved_score( f"{dataset_id}.test_penguins_model", replace=True ) df = penguins_df_default_index.dropna() - test_X = df[ + X_test = df[ [ "species", "island", @@ -254,8 +254,8 @@ def test_logsitic_model_to_gbq_saved_score( "body_mass_g", ] ] - test_y = df[["sex"]] - result = saved_model.score(test_X, test_y).to_pandas() + y_test = df[["sex"]] + result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { "precision": [0.616753], diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 181678ebcb2..7486277487d 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -17,10 +17,25 @@ import numpy as np import pytest +from bigframes.ml import llm + def test_create_text_generator_model(palm2_text_generator_model): # Model creation doesn't return error assert palm2_text_generator_model is not None + assert palm2_text_generator_model._bqml_model is not None + + +def test_create_text_generator_model_defaults(bq_connection): + import bigframes.pandas as bpd + + bpd.reset_session() + bpd.options.bigquery.bq_connection = bq_connection + bpd.options.bigquery.location = "us" + + model = llm.PaLM2TextGenerator() + assert model is not None + assert model._bqml_model is not None # Marked as flaky only because BQML LLM is in preview, the service only has limited capacity, not stable enough. @@ -74,6 +89,19 @@ def test_text_generator_predict_with_params_success( def test_create_embedding_generator_model(palm2_embedding_generator_model): # Model creation doesn't return error assert palm2_embedding_generator_model is not None + assert palm2_embedding_generator_model._bqml_model is not None + + +def test_create_text_embedding_generator_model_defaults(bq_connection): + import bigframes.pandas as bpd + + bpd.reset_session() + bpd.options.bigquery.bq_connection = bq_connection + bpd.options.bigquery.location = "us" + + model = llm.PaLM2TextEmbeddingGenerator() + assert model is not None + assert model._bqml_model is not None @pytest.mark.flaky(retries=2, delay=120) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 01305adb204..85c3cce1d77 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -551,13 +551,69 @@ def test_assign_callable_lambda(scalars_dfs): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) -def test_dropna(scalars_dfs): +@pytest.mark.parametrize( + ("axis", "how", "ignore_index"), + [ + (0, "any", False), + (0, "any", True), + (1, "any", False), + (1, "all", False), + ], +) +def test_df_dropna(scalars_dfs, axis, how, ignore_index): + if pd.__version__.startswith("1."): + pytest.skip("ignore_index parameter not supported in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs - df = scalars_df.dropna() + df = scalars_df.dropna(axis=axis, how=how, ignore_index=ignore_index) bf_result = df.to_pandas() - pd_result = scalars_pandas_df.dropna() + pd_result = scalars_pandas_df.dropna(axis=axis, how=how, ignore_index=ignore_index) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_fillna(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + df = scalars_df[["int64_col", "float64_col"]].fillna(3) + bf_result = df.to_pandas() + pd_result = scalars_pandas_df[["int64_col", "float64_col"]].fillna(3) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_isin_list(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + values = ["Hello, World!", 55555, 2.51, pd.NA, True] + bf_result = ( + scalars_df[["int64_col", "float64_col", "string_col", "bool_col"]] + .isin(values) + .to_pandas() + ) + pd_result = scalars_pandas_df[ + ["int64_col", "float64_col", "string_col", "bool_col"] + ].isin(values) + + pandas.testing.assert_frame_equal(bf_result, pd_result.astype("boolean")) + + +def test_df_isin_dict(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + values = { + "string_col": ["Hello, World!", 55555, 2.51, pd.NA, True], + "int64_col": [5555, 2.51], + "bool_col": [pd.NA], + } + bf_result = ( + scalars_df[["int64_col", "float64_col", "string_col", "bool_col"]] + .isin(values) + .to_pandas() + ) + pd_result = scalars_pandas_df[ + ["int64_col", "float64_col", "string_col", "bool_col"] + ].isin(values) + + pandas.testing.assert_frame_equal(bf_result, pd_result.astype("boolean")) @pytest.mark.parametrize( @@ -1084,50 +1140,43 @@ def test_series_binop_axis_index( @pytest.mark.parametrize( - ("op"), + ("left_labels", "right_labels"), [ - (lambda x, y: x.add(y, axis="index")), - (lambda x, y: x.radd(y, axis="index")), - (lambda x, y: x.sub(y, axis="index")), - (lambda x, y: x.rsub(y, axis="index")), - (lambda x, y: x.mul(y, axis="index")), - (lambda x, y: x.rmul(y, axis="index")), - (lambda x, y: x.truediv(y, axis="index")), - (lambda x, y: x.rtruediv(y, axis="index")), - (lambda x, y: x.floordiv(y, axis="index")), - (lambda x, y: x.floordiv(y, axis="index")), - (lambda x, y: x.gt(y, axis="index")), - (lambda x, y: x.ge(y, axis="index")), - (lambda x, y: x.lt(y, axis="index")), - (lambda x, y: x.le(y, axis="index")), + (["a", "a", "b"], ["c", "c", "d"]), + (["a", "b", "c"], ["c", "a", "b"]), + (["a", "c", "c"], ["c", "a", "c"]), ], ids=[ - "add", - "radd", - "sub", - "rsub", - "mul", - "rmul", - "truediv", - "rtruediv", - "floordiv", - "rfloordiv", - "gt", - "ge", - "lt", - "le", + "no_overlap", + "one_one_match", + "multi_match", ], ) -def test_dataframe_binop_axis_index_throws_not_implemented( - scalars_dfs, - op, +def test_binop_df_df_binary_op( + scalars_df_index, + scalars_df_2_index, + scalars_pandas_df_index, + left_labels, + right_labels, ): - scalars_df, scalars_pandas_df = scalars_dfs - df_columns = ["int64_col", "float64_col"] - other_df_columns = ["int64_too"] - - with pytest.raises(NotImplementedError): - op(scalars_df[df_columns], scalars_df[other_df_columns]).to_pandas() + if pd.__version__.startswith("1."): + pytest.skip("pd.NA vs NaN not handled well in pandas 1.x.") + columns = ["int64_too", "int64_col", "float64_col"] + + bf_df_a = scalars_df_index[columns] + bf_df_a.columns = left_labels + bf_df_b = scalars_df_2_index[columns] + bf_df_b.columns = right_labels + bf_result = (bf_df_a - bf_df_b).to_pandas() + + pd_df_a = scalars_pandas_df_index[columns] + pd_df_a.columns = left_labels + pd_df_b = scalars_pandas_df_index[columns] + pd_df_b.columns = right_labels + pd_result = pd_df_a - pd_df_b + + # Some dtype inconsistency for all-NULL columns + pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) # Differnt table will only work for explicit index, since default index orders are arbitrary. diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index ac1f8c7220d..558dd12e697 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -50,3 +50,17 @@ def test_index_getitem_int(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.index[-2] pd_result = scalars_pandas_df_index.index[-2] assert bf_result == pd_result + + +def test_is_monotonic_increasing(scalars_df_index, scalars_pandas_df_index): + assert ( + scalars_df_index.index.is_monotonic_increasing + == scalars_pandas_df_index.index.is_monotonic_increasing + ) + + +def test_is_monotonic_decreasing(scalars_df_index, scalars_pandas_df_index): + assert ( + scalars_df_index.index.is_monotonic_increasing + == scalars_pandas_df_index.index.is_monotonic_increasing + ) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 1baf3e66508..25d1e2ad498 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -634,12 +634,17 @@ def test_column_multi_index_stack(scalars_df_index, scalars_pandas_df_index): pd_df.columns = multi_columns bf_result = bf_df.stack().to_pandas() + # Shifting sort behavior in stack pd_result = pd_df.stack() # Pandas produces NaN, where bq dataframes produces pd.NA - pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + # Column ordering seems to depend on pandas version + pandas.testing.assert_frame_equal( + bf_result.sort_index(axis=1), pd_result.sort_index(axis=1), check_dtype=False + ) +@pytest.mark.skip(reason="Pandas fails in newer versions.") def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "int64_col", "rowindex_2"] level1 = pandas.Index(["b", pandas.NA, pandas.NA]) @@ -656,3 +661,64 @@ def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index # Pandas produces NaN, where bq dataframes produces pd.NA pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("index_names",), + [ + (["rowindex_2", "int64_too"],), + (["int64_too", "rowindex_2"],), + ], +) +def test_is_monotonic_increasing( + scalars_df_index, scalars_pandas_df_index, index_names +): + bf_result = scalars_df_index.set_index(index_names).index + pd_result = scalars_pandas_df_index.set_index(index_names).index + + assert bf_result.is_monotonic_increasing == pd_result.is_monotonic_increasing + + +@pytest.mark.parametrize( + ("indexes",), + [ + ({"A": [1, 2, 3], "B": [1, 2, 3], "C": [1, 2, 3]},), + ({"A": [1, 2, 3], "B": [1, 2, 3], "C": [1, None, 3]},), + ({"A": [1, 2, 2], "B": [1, 2, 1], "C": [1, 2, 3]},), + ({"A": [1, 2, 2], "B": [1, 2, 3], "C": [1, 2, 1]},), + ({"A": [1, 2, 1], "B": [1, 2, 3], "C": [1, 2, 1]},), + ({"A": [3, 2, 1], "B": [3, 2, 1], "C": [2, 2, 1]},), + ], +) +def test_is_monotonic_increasing_extra(indexes): + bf_result = bpd.DataFrame(indexes) + bf_result = bf_result.set_index(["A", "B", "C"]) + pd_result = pandas.DataFrame(indexes) + pd_result = pd_result.set_index(["A", "B", "C"]) + + assert ( + bf_result.index.is_monotonic_increasing + == pd_result.index.is_monotonic_increasing + ) + + +@pytest.mark.parametrize( + ("indexes",), + [ + ({"A": [3, 2, 1], "B": [3, 2, 1], "C": [3, 2, 1]},), + ({"A": [3, 2, 1], "B": [3, 2, 1], "C": [3, None, 1]},), + ({"A": [2, 2, 1], "B": [1, 2, 1], "C": [3, 2, 1]},), + ({"A": [2, 2, 1], "B": [3, 2, 1], "C": [1, 2, 1]},), + ({"A": [1, 2, 1], "B": [3, 2, 1], "C": [1, 2, 1]},), + ], +) +def test_is_monotonic_decreasing_extra(indexes): + bf_result = bpd.DataFrame(indexes) + bf_result = bf_result.set_index(["A", "B", "C"]) + pd_result = pandas.DataFrame(indexes) + pd_result = pd_result.set_index(["A", "B", "C"]) + + assert ( + bf_result.index.is_monotonic_decreasing + == pd_result.index.is_monotonic_decreasing + ) diff --git a/tests/system/small/test_numpy.py b/tests/system/small/test_numpy.py index fff689cabac..5c2a93ec394 100644 --- a/tests/system/small/test_numpy.py +++ b/tests/system/small/test_numpy.py @@ -67,3 +67,69 @@ def test_df_ufuncs(scalars_dfs, opname): pd_result = getattr(np, opname)(scalars_pandas_df[["float64_col", "int64_col"]]) pd.testing.assert_frame_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("opname",), + [ + ("add",), + ("subtract",), + ("multiply",), + ("divide",), + ("power",), + ], +) +def test_series_binary_ufuncs(floats_product_pd, floats_product_bf, opname): + bf_result = getattr(np, opname)( + floats_product_bf.float64_col_x, floats_product_bf.float64_col_y + ).to_pandas() + pd_result = getattr(np, opname)( + floats_product_pd.float64_col_x, floats_product_pd.float64_col_y + ) + pd.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("opname",), + [ + ("add",), + ("subtract",), + ("multiply",), + ("divide",), + ("power",), + ], +) +def test_df_binary_ufuncs(scalars_dfs, opname): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = getattr(np, opname)( + scalars_df[["float64_col", "int64_col"]], 5.1 + ).to_pandas() + pd_result = getattr(np, opname)( + scalars_pandas_df[["float64_col", "int64_col"]], 5.1 + ) + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_series_binary_ufuncs_reverse(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + # Could be any non-symmetric binary op + bf_result = np.subtract(5.1, scalars_df["int64_col"]).to_pandas() + pd_result = np.subtract(5.1, scalars_pandas_df["int64_col"]) + + pd.testing.assert_series_equal(bf_result, pd_result) + + +def test_df_binary_ufuncs_reverse(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + # Could be any non-symmetric binary op + bf_result = np.subtract(5.1, scalars_df[["float64_col", "int64_col"]]).to_pandas() + pd_result = np.subtract( + 5.1, + scalars_pandas_df[["float64_col", "int64_col"]], + ) + + pd.testing.assert_frame_equal(bf_result, pd_result) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index 98bafc6392c..e451d5c3a2c 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -16,6 +16,7 @@ import pytest import bigframes.pandas as bpd +from tests.system.utils import assert_pandas_df_equal_ignore_ordering def test_concat_dataframe(scalars_dfs): @@ -105,3 +106,106 @@ def test_concat_axis_1(scalars_dfs, how): pd_result = pd.concat([pd_part1, pd_part2, pd_part3], join=how, axis=1) pd.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) + + +@pytest.mark.parametrize( + ("merge_how",), + [ + ("inner",), + ("outer",), + ("left",), + ("right",), + ], +) +def test_merge(scalars_dfs, merge_how): + scalars_df, scalars_pandas_df = scalars_dfs + on = "rowindex_2" + left_columns = ["int64_col", "float64_col", "rowindex_2"] + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_columns] + # Offset the rows somewhat so that outer join can have an effect. + right = scalars_df[right_columns].assign(rowindex_2=scalars_df["rowindex_2"] + 2) + + df = bpd.merge(left, right, merge_how, on, sort=True) + bf_result = df.to_pandas() + + pd_result = pd.merge( + scalars_pandas_df[left_columns], + scalars_pandas_df[right_columns].assign( + rowindex_2=scalars_pandas_df["rowindex_2"] + 2 + ), + merge_how, + on, + sort=True, + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("merge_how",), + [ + ("inner",), + ("outer",), + ("left",), + ("right",), + ], +) +def test_merge_left_on_right_on(scalars_dfs, merge_how): + scalars_df, scalars_pandas_df = scalars_dfs + left_columns = ["int64_col", "float64_col", "int64_too"] + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_columns] + right = scalars_df[right_columns] + + df = bpd.merge( + left, right, merge_how, left_on="int64_too", right_on="rowindex_2", sort=True + ) + bf_result = df.to_pandas() + + pd_result = pd.merge( + scalars_pandas_df[left_columns], + scalars_pandas_df[right_columns], + merge_how, + left_on="int64_too", + right_on="rowindex_2", + sort=True, + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("merge_how",), + [ + ("inner",), + ("outer",), + ("left",), + ("right",), + ], +) +def test_merge_series(scalars_dfs, merge_how): + scalars_df, scalars_pandas_df = scalars_dfs + left_column = "int64_too" + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_column] + right = scalars_df[right_columns] + + df = bpd.merge( + left, right, merge_how, left_on="int64_too", right_on="rowindex_2", sort=True + ) + bf_result = df.to_pandas() + + pd_result = pd.merge( + scalars_pandas_df[left_column], + scalars_pandas_df[right_columns], + merge_how, + left_on="int64_too", + right_on="rowindex_2", + sort=True, + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index 9a3d55aed23..6510c4fa271 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -254,49 +254,6 @@ def test_read_gbq_must_comply_with_set_location_non_US( assert df is not None -def test_reset_session_after_bq_session_ended(): - # Use a simple test query to verify that default session works to interact - # with BQ - test_query = "SELECT 1" - - # Confirm that there is a session id in the default session - session = bpd.get_global_session() - assert session._session_id - - # Confirm that session works as usual - df = bpd.read_gbq(test_query) - assert df is not None - - # Abort the session to simulate the auto-expiration - # https://cloud.google.com/bigquery/docs/sessions-terminating#auto-terminate_a_session - abort_session_query = "CALL BQ.ABORT_SESSION()" - query_job = session.bqclient.query(abort_session_query) - query_job.result() # blocks until finished - - # Confirm that session is unusable to run any jobs - with pytest.raises( - google.api_core.exceptions.BadRequest, - match=f"Session {session._session_id} has expired and is no longer available.", - ): - query_job = session.bqclient.query(test_query) - query_job.result() # blocks until finished - - # Confirm that as a result bigframes.pandas interface is unusable - with pytest.raises( - google.api_core.exceptions.BadRequest, - match=f"Session {session._session_id} has expired and is no longer available.", - ): - bpd.read_gbq(test_query) - - # Now try to reset session and verify that it works - bpd.reset_session() - assert bigframes.core.global_session._global_session is None - - # Now verify that use is able to start over - df = bpd.read_gbq(test_query) - assert df is not None - - def test_reset_session_after_credentials_need_reauthentication(monkeypatch): # Use a simple test query to verify that default session works to interact # with BQ diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index c60d270fcaa..77fb81d2c93 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -65,9 +65,7 @@ def bq_cf_connection_location_project_mismatched() -> str: @pytest.fixture(scope="module") def session_with_bq_connection(bq_cf_connection) -> bigframes.Session: - return bigframes.Session( - bigframes.BigQueryOptions(remote_udf_connection=bq_cf_connection) - ) + return bigframes.Session(bigframes.BigQueryOptions(bq_connection=bq_cf_connection)) @pytest.fixture(scope="module") @@ -75,7 +73,7 @@ def session_with_bq_connection_location_specified( bq_cf_connection_location, ) -> bigframes.Session: return bigframes.Session( - bigframes.BigQueryOptions(remote_udf_connection=bq_cf_connection_location) + bigframes.BigQueryOptions(bq_connection=bq_cf_connection_location) ) @@ -84,9 +82,7 @@ def session_with_bq_connection_location_mistached( bq_cf_connection_location_mistached, ) -> bigframes.Session: return bigframes.Session( - bigframes.BigQueryOptions( - remote_udf_connection=bq_cf_connection_location_mistached - ) + bigframes.BigQueryOptions(bq_connection=bq_cf_connection_location_mistached) ) @@ -95,9 +91,7 @@ def session_with_bq_connection_location_project_specified( bq_cf_connection_location_project, ) -> bigframes.Session: return bigframes.Session( - bigframes.BigQueryOptions( - remote_udf_connection=bq_cf_connection_location_project - ) + bigframes.BigQueryOptions(bq_connection=bq_cf_connection_location_project) ) @@ -432,7 +426,7 @@ def test_remote_function_via_session_context_connection_setter( # Creating a session scoped only to this test as we would be setting a # property in it context = bigframes.BigQueryOptions() - context.remote_udf_connection = bq_cf_connection + context.bq_connection = bq_cf_connection session = bigframes.connect(context) # Without an explicit bigquery connection, the one present in Session, @@ -523,6 +517,25 @@ def add_one(x): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +@pytest.mark.flaky(retries=2, delay=120) +def test_series_map(session_with_bq_connection, scalars_dfs): + def add_one(x): + return x + 1 + + remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.int64_too.map(remote_add_one).to_pandas() + pd_result = scalars_pandas_df.int64_too.map(add_one) + pd_result = pd_result.astype("Int64") # pandas type differences + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_read_gbq_function_detects_invalid_function(bigquery_client, dataset_id): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 88ad2245c9b..07dc892ddc3 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -186,6 +186,23 @@ def test_fillna(scalars_dfs): ) +@pytest.mark.parametrize( + ("ignore_index",), + ( + (True,), + (False,), + ), +) +def test_series_dropna(scalars_dfs, ignore_index): + if pd.__version__.startswith("1."): + pytest.skip("ignore_index parameter not supported in pandas 1.x.") + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = scalars_df[col_name].dropna(ignore_index=ignore_index).to_pandas() + pd_result = scalars_pandas_df[col_name].dropna(ignore_index=ignore_index) + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + def test_series_agg_single_string(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df["int64_col"].agg("sum") @@ -365,6 +382,24 @@ def test_series_int_int_operators_scalar( assert_series_equal_ignoring_order(pd_result, bf_result) +def test_series_pow_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = (scalars_df["int64_col"] ** 2).to_pandas() + pd_result = scalars_pandas_df["int64_col"] ** 2 + + assert_series_equal_ignoring_order(pd_result, bf_result) + + +def test_series_pow_scalar_reverse(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = (0.8 ** scalars_df["int64_col"]).to_pandas() + pd_result = 0.8 ** scalars_pandas_df["int64_col"] + + assert_series_equal_ignoring_order(pd_result, bf_result) + + @pytest.mark.parametrize( ("operator"), [ @@ -2463,3 +2498,57 @@ def test_is_monotonic_decreasing(series_input): assert ( scalars_df.is_monotonic_decreasing == scalars_pandas_df.is_monotonic_decreasing ) + + +def test_map_dict_input(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + local_map = dict() + # construct a local map, incomplete to cover behavior + for s in scalars_pandas_df.string_col[:-3]: + if isinstance(s, str): + local_map[s] = ord(s[0]) + + pd_result = scalars_pandas_df.string_col.map(local_map) + pd_result = pd_result.astype("Int64") # pandas type differences + bf_result = scalars_df.string_col.map(local_map) + + pd.testing.assert_series_equal( + bf_result.to_pandas(), + pd_result, + ) + + +def test_map_series_input(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + new_index = scalars_pandas_df.int64_too.drop_duplicates() + pd_map_series = scalars_pandas_df.string_col.iloc[0 : len(new_index)] + pd_map_series.index = new_index + bf_map_series = series.Series( + pd_map_series, session=scalars_df._get_block().expr._session + ) + + pd_result = scalars_pandas_df.int64_too.map(pd_map_series) + bf_result = scalars_df.int64_too.map(bf_map_series) + + pd.testing.assert_series_equal( + bf_result.to_pandas(), + pd_result, + ) + + +def test_map_series_input_duplicates_error(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + new_index = scalars_pandas_df.int64_too + pd_map_series = scalars_pandas_df.string_col.iloc[0 : len(new_index)] + pd_map_series.index = new_index + bf_map_series = series.Series( + pd_map_series, session=scalars_df._get_block().expr._session + ) + + with pytest.raises(pd.errors.InvalidIndexError): + scalars_pandas_df.int64_too.map(pd_map_series) + with pytest.raises(pd.errors.InvalidIndexError): + scalars_df.int64_too.map(bf_map_series, verify_integrity=True) diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index 43b5663bf75..aeee0583195 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -26,7 +26,7 @@ ("credentials", object(), object()), ("location", "us-east1", "us-central1"), ("project", "my-project", "my-other-project"), - ("remote_udf_connection", "path/to/connection/1", "path/to/connection/2"), + ("bq_connection", "path/to/connection/1", "path/to/connection/2"), ], ) def test_setter_raises_if_session_started(attribute, original_value, new_value): @@ -56,7 +56,7 @@ def test_setter_raises_if_session_started(attribute, original_value, new_value): "credentials", "location", "project", - "remote_udf_connection", + "bq_connection", ] ], ) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py deleted file mode 100644 index dcf2d918a5e..00000000000 --- a/tests/unit/conftest.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -from typing import Callable, Optional, Tuple, Union -from unittest import mock - -import google.api_core.exceptions -import google.auth -import google.cloud.bigquery as bigquery -import google.cloud.bigquery.table -import google.oauth2.credentials # type: ignore -import ibis.expr.types as ibis_types -import pandas -import pytest - -import bigframes -import bigframes.core -import bigframes.dataframe - -SCALARS_TABLE_ID = "project.dataset.scalars_table" - - -@pytest.fixture -def scalars_pandas_df_default_index() -> pandas.DataFrame: - # Note: as of 2023-02-07, using nullable dtypes with the ibis pandas - # backend requires running ibis at HEAD. See: - # https://github.com/ibis-project/ibis/pull/5345 - return pandas.DataFrame( - { - "rowindex": pandas.Series( - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - ], - dtype="Int64", - ), - "bool_col": pandas.Series( - [ - True, - None, - False, - True, - None, - False, - True, - None, - False, - True, - ], - dtype="boolean", - ), - "int64_col": pandas.Series( - [ - 1, - 2, - 3, - None, - 0, - -1, - -2, - 2**63 - 1, - -(2**63), - None, - ], - dtype="Int64", - ), - "float64_col": pandas.Series( - [ - None, - 1, - math.pi, - math.e * 1e10, - 0, - float("nan"), - float("inf"), - float("-inf"), - -2.23e-308, - 1.8e308, - ], - dtype="Float64", - ), - "string_col": pandas.Series( - [ - "abc", - "XYZ", - "aBcDeFgHiJkLmNoPqRsTuVwXyZ", - "1_2-3+4=5~6*7/8&9%10#11@12$" "", - None, - "こんにちは", - "你好", - "வணக்கம்", - "שלום", - ], - dtype="string[pyarrow]", - ), - } - ) - - -# We parameterize the fixtures at this point with the real pandas -# dataframes and deferred bigframes dataframes as we have the following -# chain of dependencies: -# -> index/default_index parameterization -# -> pandas dataframe -# -> bqclient mock -# -> session -# -> bigframes dataframe -@pytest.fixture -def scalars_testdata_setup( - scalars_pandas_df_default_index, -) -> Tuple[ - pandas.DataFrame, Callable[[bigframes.Session], bigframes.dataframe.DataFrame] -]: - return ( - scalars_pandas_df_default_index.set_index("rowindex"), - lambda session: session.read_gbq(SCALARS_TABLE_ID, index_col=["rowindex"]), - ) - - -@pytest.fixture(autouse=True) -def mock_bigquery_client(monkeypatch, scalars_testdata_setup) -> bigquery.Client: - scalars_pandas_df, _ = scalars_testdata_setup - mock_client = mock.create_autospec(bigquery.Client) - # Constructor returns the mock itself, so this mock can be treated as the - # constructor or the instance. - mock_client.return_value = mock_client - mock_client.project = "default-project" - most_recent_table = None - - def mock_bigquery_client_get_table( - table_ref: Union[google.cloud.bigquery.table.TableReference, str] - ): - global most_recent_table - - if isinstance(table_ref, google.cloud.bigquery.table.TableReference): - table_name = table_ref.__str__() - else: - table_name = table_ref - - schema = [ - {"mode": "NULLABLE", "name": "rowindex", "type": "INTEGER"}, - { - "mode": "NULLABLE", - "name": "bigframes_ordering_id", - "type": "INTEGER", - }, - ] - - if table_name == SCALARS_TABLE_ID: - schema += [ - {"mode": "NULLABLE", "name": "bool_col", "type": "BOOL"}, - {"mode": "NULLABLE", "name": "int64_col", "type": "INTEGER"}, - {"mode": "NULLABLE", "name": "float64_col", "type": "FLOAT"}, - {"mode": "NULLABLE", "name": "string_col", "type": "STRING"}, - ] - else: - raise google.api_core.exceptions.NotFound("Not Found Table") - - most_recent_table = bigquery.Table(table_name, schema) # type: ignore - return most_recent_table # type: ignore - - def mock_query( - sql: str, - job_config: Optional[bigquery.QueryJobConfig] = None, - location: str = "US", - ) -> bigquery.QueryJob: - global most_recent_table - - def mock_result(max_results=None): - mock_rows = mock.create_autospec(google.cloud.bigquery.table.RowIterator) - mock_rows.total_rows = len(scalars_pandas_df.index) - mock_rows.schema = [ - bigquery.SchemaField(name=name, field_type="INT64") - for name in scalars_pandas_df.columns - ] - # Use scalars_pandas_df instead of ibis_expr.execute() to preserve dtypes. - mock_rows.to_dataframe.return_value = scalars_pandas_df.head(n=max_results) - return mock_rows - - mock_job = mock.create_autospec(bigquery.QueryJob) - mock_job.result = mock_result - return mock_job - - mock_client.get_table = mock_bigquery_client_get_table - mock_client.query.side_effect = mock_query - monkeypatch.setattr(bigquery, "Client", mock_client) - mock_client.reset_mock() - return mock_client - - -@pytest.fixture -def session() -> bigframes.Session: - return bigframes.Session( - context=bigframes.BigQueryOptions( - credentials=mock.create_autospec(google.oauth2.credentials.Credentials), - project="unit-test-project", - ) - ) - - -@pytest.fixture -def scalars_ibis_table(session) -> ibis_types.Table: - return session.ibis_client.table(SCALARS_TABLE_ID) diff --git a/tests/unit/core/test_utils.py b/tests/unit/core/test_bf_utils.py similarity index 100% rename from tests/unit/core/test_utils.py rename to tests/unit/core/test_bf_utils.py diff --git a/tests/unit/resources.py b/tests/unit/resources.py new file mode 100644 index 00000000000..c8ed6e86ed6 --- /dev/null +++ b/tests/unit/resources.py @@ -0,0 +1,73 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, List, Optional +import unittest.mock as mock + +import google.auth.credentials +import google.cloud.bigquery +import ibis +import pandas + +import bigframes +import bigframes.core as core + +"""Utilities for creating test resources.""" + + +def create_bigquery_session( + bqclient: Optional[google.cloud.bigquery.Client] = None, session_id: str = "abcxyz" +) -> bigframes.Session: + credentials = mock.create_autospec( + google.auth.credentials.Credentials, instance=True + ) + + if bqclient is None: + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + + clients_provider = mock.create_autospec(bigframes.session.ClientsProvider) + type(clients_provider).bqclient = mock.PropertyMock(return_value=bqclient) + clients_provider._credentials = credentials + + bqoptions = bigframes.BigQueryOptions( + credentials=credentials, location="test-region" + ) + session = bigframes.Session(context=bqoptions, clients_provider=clients_provider) + session._session_id = session_id + return session + + +def create_pandas_session(tables: Dict[str, pandas.DataFrame]) -> bigframes.Session: + # TODO(tswast): Refactor to make helper available for all tests. Consider + # providing a proper "local Session" for use by downstream developers. + session = mock.create_autospec(bigframes.Session, instance=True) + ibis_client = ibis.pandas.connect(tables) + type(session).ibis_client = mock.PropertyMock(return_value=ibis_client) + return session + + +def create_arrayvalue( + df: pandas.DataFrame, total_ordering_columns: List[str] +) -> bigframes.core.ArrayValue: + session = create_pandas_session({"test_table": df}) + ibis_table = session.ibis_client.table("test_table") + columns = tuple(ibis_table[key] for key in ibis_table.columns) + ordering = core.ExpressionOrdering( + [core.OrderingColumnReference(column) for column in total_ordering_columns], + total_ordering_columns=frozenset(total_ordering_columns), + ) + return core.ArrayValue( + session=session, table=ibis_table, columns=columns, ordering=ordering + ) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 123dae79395..e01638e22e1 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -13,39 +13,55 @@ # limitations under the License. import ibis -from ibis.expr.types import Table +import pandas -from bigframes import core +import bigframes.core as core -ORDERING = core.ExpressionOrdering( - [ - core.OrderingColumnReference("int64_col"), - core.OrderingColumnReference("string_col"), - ], - total_ordering_columns=frozenset(["int64_col", "string_col"]), -) +from . import resources -def test_constructor_from_ibis_table_adds_all_columns( - session, scalars_ibis_table: Table -): - columns = tuple(scalars_ibis_table[key] for key in scalars_ibis_table.columns) +def test_arrayvalue_constructor_from_ibis_table_adds_all_columns(): + session = resources.create_pandas_session( + { + "test_table": pandas.DataFrame( + { + "col1": [1, 2, 3], + "not_included": [True, False, True], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ) + } + ) + ibis_table = session.ibis_client.table("test_table") + columns = (ibis_table["col1"], ibis_table["col2"], ibis_table["col3"]) + ordering = core.ExpressionOrdering( + [core.OrderingColumnReference("col1")], + total_ordering_columns=frozenset(["col1"]), + ) actual = core.ArrayValue( - session=session, table=scalars_ibis_table, columns=columns, ordering=ORDERING + session=session, table=ibis_table, columns=columns, ordering=ordering ) - assert actual._table is scalars_ibis_table - assert len(actual._columns) == len(scalars_ibis_table.columns) + assert actual.table is ibis_table + assert len(actual.columns) == 3 -def test_to_ibis_expr_with_projection(session, scalars_ibis_table: Table): - columns = tuple(scalars_ibis_table[key] for key in scalars_ibis_table.columns) - expr = core.ArrayValue( - session=session, table=scalars_ibis_table, columns=columns, ordering=ORDERING - ).projection( +def test_arrayvalue_to_ibis_expr_with_projection(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.projection( [ - scalars_ibis_table["int64_col"], + (value.table["col1"] + ibis.literal(-1)).name("int64_col"), ibis.literal(123456789).name("literals"), - scalars_ibis_table["string_col"], + value.table["col2"].name("string_col"), ] ) actual = expr.to_ibis_expr() diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index a178a454388..2325fc96a0a 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -17,12 +17,17 @@ import sys import unittest.mock as mock +import google.api_core.exceptions +import google.cloud.bigquery import pandas as pd import pytest +import bigframes.core.global_session import bigframes.pandas as bpd import bigframes.session +from . import resources + leading_whitespace = re.compile(r"^\s+", flags=re.MULTILINE) @@ -109,3 +114,37 @@ def test_pandas_attribute(): assert bpd.Int64Dtype is pd.Int64Dtype assert bpd.StringDtype is pd.StringDtype assert bpd.ArrowDtype is pd.ArrowDtype + + +def test_reset_session_after_bq_session_ended(monkeypatch): + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + session = resources.create_bigquery_session( + bqclient=bqclient, session_id="JUST_A_TEST" + ) + + # Simulate that the session has already expired. + # Note: this needs to be done after the Session is constructed, as the + # initializer sends a query to start the BigQuery Session. + query_job = mock.create_autospec(google.cloud.bigquery.QueryJob, instance=True) + query_job.result.side_effect = google.api_core.exceptions.BadRequest( + "Session JUST_A_TEST has expired and is no longer available." + ) + bqclient.query.return_value = query_job + + # Simulate that the session has already started. + monkeypatch.setattr(bigframes.core.global_session, "_global_session", session) + bpd.options.bigquery._session_started = True + + # Confirm that as a result bigframes.pandas interface is unusable + with pytest.raises( + google.api_core.exceptions.BadRequest, + match="Session JUST_A_TEST has expired and is no longer available.", + ): + bpd.read_gbq("SELECT 1") + + # Even though the query to stop the session raises an exception, we should + # still be able to reset it without raising an error to the user. + bpd.reset_session() + assert "CALL BQ.ABORT_SESSION('JUST_A_TEST')" in bqclient.query.call_args.args[0] + assert bigframes.core.global_session._global_session is None diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index ab573c4c11f..e39a316e5b6 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -20,9 +20,13 @@ import bigframes +from . import resources + @pytest.mark.parametrize("missing_parts_table_id", [(""), ("table")]) -def test_read_gbq_missing_parts(session, missing_parts_table_id): +def test_read_gbq_missing_parts(missing_parts_table_id): + session = resources.create_bigquery_session() + with pytest.raises(ValueError): session.read_gbq(missing_parts_table_id) @@ -31,7 +35,14 @@ def test_read_gbq_missing_parts(session, missing_parts_table_id): "not_found_table_id", [("unknown.dataset.table"), ("project.unknown.table"), ("project.dataset.unknown")], ) -def test_read_gdb_not_found_tables(session, not_found_table_id): +def test_read_gdb_not_found_tables(not_found_table_id): + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + bqclient.get_table.side_effect = google.api_core.exceptions.NotFound( + "table not found" + ) + session = resources.create_bigquery_session(bqclient=bqclient) + with pytest.raises(google.api_core.exceptions.NotFound): session.read_gbq(not_found_table_id) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 5a812dae7e7..8c81b23b6ca 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -380,11 +380,44 @@ def dropna( ) -> DataFrame: """Remove missing values. + Args: + axis ({0 or 'index', 1 or 'columns'}, default 'columns'): + Determine if rows or columns which contain missing values are + removed. + + * 0, or 'index' : Drop rows which contain missing values. + * 1, or 'columns' : Drop columns which contain missing value. + how ({'any', 'all'}, default 'any'): + Determine if row or column is removed from DataFrame, when we have + at least one NA or all NA. + + * 'any' : If any NA values are present, drop that row or column. + * 'all' : If all values are NA, drop that row or column. + ignore_index (bool, default ``False``): + If ``True``, the resulting axis will be labeled 0, 1, …, n - 1. + + Returns: bigframes.dataframe.DataFrame: DataFrame with NA entries dropped from it. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def isin(self, values): + """ + Whether each element in the DataFrame is contained in values. + + Args: + values (iterable, or dict): + The result will only be true at a location if all the + labels match. If `values` is a dict, the keys must be + the column names, which must match. + + Returns: + DataFrame: DataFrame of booleans showing whether each element + in the DataFrame is contained in values. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # Sorting @@ -808,6 +841,54 @@ def rmod(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def pow(self, other, axis: str | int = "columns") -> DataFrame: + """Get Exponential power of dataframe and other, element-wise (binary operator `pow`). + + Equivalent to ``dataframe ** other``, but with support to substitute a fill_value + for missing data in one of the inputs. With reverse version, `rpow`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + .. note:: + Mismatched indices will be unioned together. + + Args: + other (float, int, or Series): + Any single or multiple element data structure, or list-like object. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. + + Returns: + DataFrame: DataFrame result of the arithmetic operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def rpow(self, other, axis: str | int = "columns") -> DataFrame: + """Get Exponential power of dataframe and other, element-wise (binary operator `rpow`). + + Equivalent to ``other ** dataframe``, but with support to substitute a fill_value + for missing data in one of the inputs. With reverse version, `pow`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + .. note:: + Mismatched indices will be unioned together. + + Args: + other (float, int, or Series): + Any single or multiple element data structure, or list-like object. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. + + Returns: + DataFrame: DataFrame result of the arithmetic operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # Data reshaping @@ -1338,3 +1419,20 @@ def value_counts( Series: Series containing counts of unique rows in the DataFrame """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def fillna(self, value): + """ + Fill NA/NaN values using the specified method. + + Args: + value (scalar, Series): + Value to use to fill holes (e.g. 0), alternately a + Series of values specifying which value to use for + each index (for a Series) or column (for a DataFrame). Values not + in the Series will not be filled. This value cannot + be a list. + + Returns: + DataFrame: Object with missing values filled + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/merge.py b/third_party/bigframes_vendored/pandas/core/reshape/merge.py new file mode 100644 index 00000000000..ee02d698da3 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/reshape/merge.py @@ -0,0 +1,78 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/reshape/merge.py +""" +SQL-style merge routines +""" +from __future__ import annotations + + +def merge( + left, + right, + how="inner", + on=None, + *, + left_on=None, + right_on=None, + sort=False, + suffixes=("_x", "_y"), +): + + """ + Merge DataFrame objects with a database-style join. + + The join is done on columns or indexes. If joining columns on + columns, the DataFrame indexes *will be ignored*. Otherwise if joining indexes + on indexes or indexes on a column or columns, the index will be passed on. + When performing a cross merge, no column specifications to merge on are + allowed. + + .. note:: + A named Series object is treated as a DataFrame with a single named column. + + .. warning:: + If both key columns contain rows where the key is a null value, those + rows will be matched against each other. This is different from usual SQL + join behaviour and can lead to unexpected results. + + Args: + left: + The primary object to be merged. + right: + Object to merge with. + how: + ``{'left', 'right', 'outer', 'inner'}, default 'inner'`` + Type of merge to be performed. + ``left``: use only keys from left frame, similar to a SQL left outer join; + preserve key order. + ``right``: use only keys from right frame, similar to a SQL right outer join; + preserve key order. + ``outer``: use union of keys from both frames, similar to a SQL full outer + join; sort keys lexicographically. + ``inner``: use intersection of keys from both frames, similar to a SQL inner + join; preserve the order of the left keys. + + on: + Column join on. It must be found in both DataFrames. Either on or left_on + right_on + must be passed in. + left_on: + Column join on in the left DataFrame. Either on or left_on + right_on + must be passed in. + right_on: + Column join on in the right DataFrame. Either on or left_on + right_on + must be passed in. + sort: + Default False. Sort the join keys lexicographically in the + result DataFrame. If False, the order of the join keys depends + on the join type (how keyword). + suffixes: + Default ``("_x", "_y")``. A length-2 sequence where each + element is optionally a string indicating the suffix to add to + overlapping column names in `left` and `right` respectively. + Pass a value of `None` instead of a string to indicate that the + column name from `left` or `right` should be left as-is, with + no suffix. At least one of the values must not be None. + + Returns: + bigframes.dataframe.DataFrame: A DataFrame of the two merged objects. + """ + raise NotImplementedError("abstract method") diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 8d505c1eadb..76fb46a7003 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -207,10 +207,16 @@ def to_dict(self, into: type[dict] = dict) -> Mapping: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def to_frame(self) -> DataFrame: + def to_frame(self, name=None) -> DataFrame: """ Convert Series to DataFrame. + The column in the new dataframe will be named name (the keyword parameter) + if the name parameter is provided and not None. + + Args: + name (Hashable, default None) + Returns: bigframes.dataframe.DataFrame: DataFrame representation of Series. """ @@ -830,6 +836,23 @@ def fillna( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def dropna(self, *, axis=0, inplace: bool = False, how=None) -> Series: + """ + Return a new Series with missing values removed. + + Args: + axis (0 or 'index'): + Unused. Parameter needed for compatibility with DataFrame. + inplace (bool, default False): + Unsupported, do not set. + how (str, optional): + Not in use. Kept for compatibility. + + Returns: + Series: Series with NA entries dropped from it. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def between( self, left, @@ -1185,9 +1208,39 @@ def mod(self, other) -> Series: raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def rmod(self, other) -> Series: - """Get modulo of Series and other, element-wise (binary operator `rmod`). + """Return modulo of Series and other, element-wise (binary operator mod). + + Equivalent to ``series % other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other (Series, or scalar value): + + Returns: + bigframes.series.Series: The result of the operation. + + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - Equivalent to ``other % series``, but with support to substitute a fill_value for + def pow(self, other) -> Series: + """Return Exponential power of series and other, element-wise (binary operator `pow`). + + Equivalent to ``series ** other``, but with support to substitute a fill_value for + missing data in either one of the inputs. + + Args: + other (Series, or scalar value): + + Returns: + bigframes.series.Series: The result of the operation. + + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def rpow(self, other) -> Series: + """Return Exponential power of series and other, element-wise (binary operator `rpow`). + + Equivalent to ``other ** series``, but with support to substitute a fill_value for missing data in either one of the inputs. Args: @@ -1640,3 +1693,46 @@ def is_monotonic_decreasing(self) -> bool: bool """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def map( + self, + arg, + na_action=None, + *, + verify_integrity=False, + ) -> Series: + """ + Map values of Series according to an input mapping or function. + + Used for substituting each value in a Series with another value, + that may be derived from a remote function, ``dict``, or a :class:`Series`. + + If arg is a remote function, the overhead for remote functions + applies. If mapping with a dict, fully deferred computation is possible. + If mapping with a Series, fully deferred computation is only possible if + verify_integrity=False. + + .. note:: + Bigframes does not yet support ``dict`` subclasses that define + ``__missing__`` (i.e. provide a method for default values). These + are treated the same as ``dict``. + + Args: + arg (function, Mapping, Series): + remote function, collections.abc.Mapping subclass or Series + Mapping correspondence. + na_action: (str, default None) + Only None is currently supported, indicating that arg may + map values to scalars. values won't be ignored. + Passing 'ignore' will raise NotImplementedException. + verify_integrity: (bool, default False) + Only applies when arg is a Series. If True, throw if the Series + index contains duplicate entries (this matches pandas behavior). + If False, skip the expensive computation, and any duplicate + index entries will produce duplicate rows in the result for each + index entry. + + Returns: + Series: Same index as caller. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 106c75c79e6ba33f613075053c84ac7fe7d22c2c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 06:20:31 -0500 Subject: [PATCH 012/921] chore(main): release 0.3.0 (#13) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab2b05d571..7770534cad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,73 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.3.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.2.0...v0.3.0) (2023-09-02) + + +### Features + +* Add `bigframes.get_global_session()` and `bigframes.reset_session()` aliases ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Add `bigframes.pandas.read_pickle` function ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Add `components_`, `explained_variance_`, and `explained_variance_ratio_` properties to `bigframes.ml.decomposition.PCA` ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Add `fit_transform` to `bigquery.ml` transformers ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Add `Series.dropna` and `DataFrame.fillna` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Add `Series.str` methods `isalpha`, `isdigit`, `isdecimal`, `isalnum`, `isspace`, `islower`, `isupper`, `zfill`, `center` ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Support `bigframes.pandas.merge()` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Support `DataFrame.isin` with list and dict inputs ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Support `DataFrame.pivot` ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Support `DataFrame.stack` ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Support `DataFrame`-`DataFrame` binary operations ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Support `df[my_column] = [a python list]` ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Support `Index.is_monotonic` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Support `np.arcsin`, `np.arccos`, `np.arctan`, `np.sinh`, `np.cosh`, `np.tanh`, `np.arcsinh`, `np.arccosh`, `np.arctanh`, `np.exp` with Series argument ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Support `np.sin`, `np.cos`, `np.tan`, `np.log`, `np.log10`, `np.sqrt`, `np.abs` with Series argument ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Support `pow()` and power operator in `DataFrame` and `Series` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Support `read_json` with `engine=bigquery` for newline-delimited JSON files ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Support `Series.corr` ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Support `Series.map` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Support for `np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.power` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Support MultiIndex for DataFrame columns ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Use `pandas.Index` for column labels ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Use default session and connection in `ml.llm` and `ml.imported` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) + + +### Bug Fixes + +* Add error message to `set_index` ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Align column names with pandas in `DataFrame.agg` results ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Allow (but still not recommended) `ORDER BY` in `read_gbq` input when an `index_col` is defined ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Check for IAM role on the BigQuery connection when initializing a `remote_function` ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Check that types are specified in `read_gbq_function` ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Don't use query cache for Session construction ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Include survey link in abstract `NotImplementedError` exception messages ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Label temp table creation jobs with `source=bigquery-dataframes-temp` label ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Make `X_train` argument names consistent across methods ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) +* Raise AttributeError for unimplemented pandas methods ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Raise exception for invalid function in `read_gbq_function` ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Support spaces in column names in `DataFrame` initializater ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) + + +### Performance Improvements + +* Add local cache for `__repr_*__` methods ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Lazily instantiate client library objects ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Use `row_number()` filter for `head` / `tail` ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) + + +### Documentation + +* Add ML section under Overview ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Add release status to table of contents ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Add samples and best practices to `read_gbq` docs ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Correct the return types of Dataframe and Series ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Create subfolders for notebooks ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Fix link to GitHub ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Highlight bigframes is open-source ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Sample ML Drug Name Generation notebook ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Set `options.bigquery.project` in sample code ([89b9503](https://github.com/googleapis/python-bigquery-dataframes/commit/89b95033d6b449bfc21249057d7c024d096c80d0)) +* Transform remote function user guide into sample code ([a32b747](https://github.com/googleapis/python-bigquery-dataframes/commit/a32b74751785c8e8aec40ce01df639dd7c4fbb77)) +* Update remote function notebook with read_gbq_function usage ([8fab755](https://github.com/googleapis/python-bigquery-dataframes/commit/8fab75576757230bca5c7df10994837ac406300f)) + ## 0.2.0 (2023-08-17) ### Features diff --git a/bigframes/version.py b/bigframes/version.py index d386742d59f..4cc4639705d 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.2.0" +__version__ = "0.3.0" From 99722e7642fee629e1fc0558d7c2d6c71915f1cf Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Tue, 5 Sep 2023 12:29:40 -0700 Subject: [PATCH 013/921] build: enable multi-scm for release job (#16) --- .github/release-trigger.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml index d4ca94189e1..b0a6cadefca 100644 --- a/.github/release-trigger.yml +++ b/.github/release-trigger.yml @@ -1 +1,2 @@ enabled: true +multiScmName: bigframes From 3dd25b379ed832ea062e188f483d2789830de67b Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 5 Sep 2023 15:17:39 -0500 Subject: [PATCH 014/921] fix(release): use correct directory name for release build config (#17) multi_scm name is "bigframes" --- .kokoro/release/common.cfg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index 7ffa79c7a1d..021198fb269 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -11,7 +11,7 @@ action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" +build_file: "bigframes/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { @@ -20,7 +20,7 @@ env_vars: { } env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-bigquery-dataframes/.kokoro/release.sh" + value: "github/bigframes/.kokoro/release.sh" } # Fetch PyPI password @@ -43,7 +43,7 @@ env_vars: { # what we published, which we can use to generate SBOMs and attestations. action { define_artifacts { - regex: "github/python-bigquery-dataframes/**/*.tar.gz" - strip_prefix: "github/python-bigquery-dataframes" + regex: "github/bigframes/**/*.tar.gz" + strip_prefix: "github/bigframes" } -} \ No newline at end of file +} From 9e581edbc7352b8a9567c1a30864b54e102e1f7c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:21:57 -0500 Subject: [PATCH 015/921] chore(main): release 0.3.1 (#18) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ bigframes/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7770534cad4..1bcfeacfd0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.3.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.3.0...v0.3.1) (2023-09-05) + + +### Bug Fixes + +* **release:** Use correct directory name for release build config ([#17](https://github.com/googleapis/python-bigquery-dataframes/issues/17)) ([3dd25b3](https://github.com/googleapis/python-bigquery-dataframes/commit/3dd25b379ed832ea062e188f483d2789830de67b)) + ## [0.3.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.2.0...v0.3.0) (2023-09-02) diff --git a/bigframes/version.py b/bigframes/version.py index 4cc4639705d..90bf336b9d6 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.0" +__version__ = "0.3.1" From 995161068b118a639903878acfde3202087c25f8 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 6 Sep 2023 08:56:43 -0500 Subject: [PATCH 016/921] fix: make release.sh script for PyPI upload executable (#20) Change-Id: I45939017295e5c6c4942087afecb5659b423d4e9 --- .kokoro/release.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) mode change 100644 => 100755 .kokoro/release.sh diff --git a/.kokoro/release.sh b/.kokoro/release.sh old mode 100644 new mode 100755 index 697b04009db..2bce143e1c9 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -16,7 +16,7 @@ set -eo pipefail # Start the releasetool reporter -python3 -m pip install --require-hashes -r github/python-bigquery-dataframes/.kokoro/requirements.txt +python3 -m pip install --require-hashes -r github/bigframes/.kokoro/requirements.txt python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script # Disable buffering, so that the logs stream through. @@ -24,6 +24,6 @@ export PYTHONUNBUFFERED=1 # Move into the package, build the distribution and upload. TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-1") -cd github/python-bigquery-dataframes +cd github/bigframes python3 setup.py sdist bdist_wheel -twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* \ No newline at end of file +twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* From e76b5e7a0738c8d408e336ea21ad0c86e5b4511a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:04:03 -0500 Subject: [PATCH 017/921] chore(main): release 0.3.2 (#21) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ bigframes/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcfeacfd0e..aa736877f4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.3.2](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.3.1...v0.3.2) (2023-09-06) + + +### Bug Fixes + +* Make release.sh script for PyPI upload executable ([#20](https://github.com/googleapis/python-bigquery-dataframes/issues/20)) ([9951610](https://github.com/googleapis/python-bigquery-dataframes/commit/995161068b118a639903878acfde3202087c25f8)) + ## [0.3.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.3.0...v0.3.1) (2023-09-05) diff --git a/bigframes/version.py b/bigframes/version.py index 90bf336b9d6..ef4f01f6294 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.1" +__version__ = "0.3.2" From ffad464a99219a2139b1fd456703041a0e185750 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 7 Sep 2023 13:07:44 -0500 Subject: [PATCH 018/921] chore: enable owlbot (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: enable owlbot Change-Id: Idb2a03ab01d414f136a846d18f90329a9a50e4aa * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * revert some owlbot changes Change-Id: Iabe654bf693d82f5f1afdef7e9663577ded96025 * only exclude common.cfg Change-Id: I1199737edca971b648ec2cfddb82b4f378b3f97d * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * use correct path in build.sh Change-Id: I407dc0500f52c52ccd90e108ba821fd518a7fe1d * decrease coverage and use repo name in 'nightly' build Change-Id: I7d450364dd234c640d905c9ac2c2a96e7df08d02 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * restore pre-commit checks Change-Id: Iaf059d5b197d9cc9e87e3d17adf6c7c48ca41cb8 --------- Co-authored-by: Owl Bot --- .coveragerc | 39 ++ .flake8 | 4 +- .github/.OwlBot.lock.yaml | 17 + .github/.OwlBot.yaml | 18 + .github/CODEOWNERS | 1 + .github/header-checker-lint.yml | 14 +- .github/sync-repo-settings.yaml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/unittest.yml | 4 +- .gitignore | 7 - .kokoro/build.sh | 6 +- .kokoro/continuous/continuous.cfg | 2 +- .kokoro/continuous/prerelease-deps.cfg | 7 + .kokoro/docker/docs/Dockerfile | 2 +- .kokoro/docs/common.cfg | 14 +- .kokoro/docs/docs-presubmit.cfg | 9 +- .kokoro/docs/docs.cfg | 2 +- .kokoro/populate-secrets.sh | 43 ++ .kokoro/presubmit/common.cfg | 2 +- .kokoro/presubmit/prerelease-deps.cfg | 7 + .kokoro/presubmit/presubmit.cfg | 2 +- .kokoro/publish-docs.sh | 19 + .kokoro/release-nightly.sh | 49 +-- .kokoro/release.sh | 4 +- .kokoro/release/common.cfg | 8 +- .kokoro/requirements.in | 3 - .kokoro/requirements.txt | 384 +++++++----------- .kokoro/samples/lint/common.cfg | 34 ++ .kokoro/samples/lint/continuous.cfg | 6 + .kokoro/samples/lint/periodic.cfg | 6 + .kokoro/samples/lint/presubmit.cfg | 6 + .kokoro/samples/python3.10/common.cfg | 40 ++ .kokoro/samples/python3.10/continuous.cfg | 6 + .kokoro/samples/python3.10/periodic-head.cfg | 11 + .kokoro/samples/python3.10/periodic.cfg | 6 + .kokoro/samples/python3.10/presubmit.cfg | 6 + .kokoro/samples/python3.11/common.cfg | 40 ++ .kokoro/samples/python3.11/continuous.cfg | 6 + .kokoro/samples/python3.11/periodic-head.cfg | 11 + .kokoro/samples/python3.11/periodic.cfg | 6 + .kokoro/samples/python3.11/presubmit.cfg | 6 + .kokoro/samples/python3.7/common.cfg | 40 ++ .kokoro/samples/python3.7/continuous.cfg | 6 + .kokoro/samples/python3.7/periodic-head.cfg | 11 + .kokoro/samples/python3.7/periodic.cfg | 6 + .kokoro/samples/python3.7/presubmit.cfg | 6 + .kokoro/samples/python3.8/common.cfg | 40 ++ .kokoro/samples/python3.8/continuous.cfg | 6 + .kokoro/samples/python3.8/periodic-head.cfg | 11 + .kokoro/samples/python3.8/periodic.cfg | 6 + .kokoro/samples/python3.8/presubmit.cfg | 6 + .kokoro/samples/python3.9/common.cfg | 40 ++ .kokoro/samples/python3.9/continuous.cfg | 6 + .kokoro/samples/python3.9/periodic-head.cfg | 11 + .kokoro/samples/python3.9/periodic.cfg | 6 + .kokoro/samples/python3.9/presubmit.cfg | 6 + .kokoro/test-samples-against-head.sh | 26 ++ .kokoro/test-samples-impl.sh | 102 +++++ .kokoro/test-samples.sh | 44 ++ .kokoro/trampoline.sh | 28 ++ .kokoro/trampoline_v2.sh | 0 .pre-commit-config.yaml | 8 +- .trampolinerc | 3 + CODE_OF_CONDUCT.md | 3 +- CONTRIBUTING.rst | 277 +++++++++++++ MANIFEST.in | 26 ++ docs/_static/custom.css | 5 + docs/_templates/layout.html | 5 + docs/conf.py | 41 +- noxfile.py | 3 +- owlbot.py | 54 ++- renovate.json | 12 + scripts/decrypt-secrets.sh | 46 +++ scripts/readme-gen/readme_gen.py | 69 ++++ scripts/readme-gen/templates/README.tmpl.rst | 87 ++++ scripts/readme-gen/templates/auth.tmpl.rst | 9 + .../templates/auth_api_key.tmpl.rst | 14 + .../templates/install_deps.tmpl.rst | 29 ++ .../templates/install_portaudio.tmpl.rst | 35 ++ scripts/upload_to_google_drive.py | 5 +- setup.cfg | 16 +- testing/.gitignore | 3 + 82 files changed, 1652 insertions(+), 366 deletions(-) create mode 100644 .coveragerc create mode 100644 .github/.OwlBot.lock.yaml create mode 100644 .github/.OwlBot.yaml create mode 100644 .kokoro/continuous/prerelease-deps.cfg create mode 100755 .kokoro/populate-secrets.sh create mode 100644 .kokoro/presubmit/prerelease-deps.cfg create mode 100644 .kokoro/samples/lint/common.cfg create mode 100644 .kokoro/samples/lint/continuous.cfg create mode 100644 .kokoro/samples/lint/periodic.cfg create mode 100644 .kokoro/samples/lint/presubmit.cfg create mode 100644 .kokoro/samples/python3.10/common.cfg create mode 100644 .kokoro/samples/python3.10/continuous.cfg create mode 100644 .kokoro/samples/python3.10/periodic-head.cfg create mode 100644 .kokoro/samples/python3.10/periodic.cfg create mode 100644 .kokoro/samples/python3.10/presubmit.cfg create mode 100644 .kokoro/samples/python3.11/common.cfg create mode 100644 .kokoro/samples/python3.11/continuous.cfg create mode 100644 .kokoro/samples/python3.11/periodic-head.cfg create mode 100644 .kokoro/samples/python3.11/periodic.cfg create mode 100644 .kokoro/samples/python3.11/presubmit.cfg create mode 100644 .kokoro/samples/python3.7/common.cfg create mode 100644 .kokoro/samples/python3.7/continuous.cfg create mode 100644 .kokoro/samples/python3.7/periodic-head.cfg create mode 100644 .kokoro/samples/python3.7/periodic.cfg create mode 100644 .kokoro/samples/python3.7/presubmit.cfg create mode 100644 .kokoro/samples/python3.8/common.cfg create mode 100644 .kokoro/samples/python3.8/continuous.cfg create mode 100644 .kokoro/samples/python3.8/periodic-head.cfg create mode 100644 .kokoro/samples/python3.8/periodic.cfg create mode 100644 .kokoro/samples/python3.8/presubmit.cfg create mode 100644 .kokoro/samples/python3.9/common.cfg create mode 100644 .kokoro/samples/python3.9/continuous.cfg create mode 100644 .kokoro/samples/python3.9/periodic-head.cfg create mode 100644 .kokoro/samples/python3.9/periodic.cfg create mode 100644 .kokoro/samples/python3.9/presubmit.cfg create mode 100755 .kokoro/test-samples-against-head.sh create mode 100755 .kokoro/test-samples-impl.sh create mode 100755 .kokoro/test-samples.sh create mode 100755 .kokoro/trampoline.sh mode change 100644 => 100755 .kokoro/trampoline_v2.sh create mode 100644 CONTRIBUTING.rst create mode 100644 MANIFEST.in create mode 100644 renovate.json create mode 100755 scripts/decrypt-secrets.sh create mode 100644 scripts/readme-gen/readme_gen.py create mode 100644 scripts/readme-gen/templates/README.tmpl.rst create mode 100644 scripts/readme-gen/templates/auth.tmpl.rst create mode 100644 scripts/readme-gen/templates/auth_api_key.tmpl.rst create mode 100644 scripts/readme-gen/templates/install_deps.tmpl.rst create mode 100644 scripts/readme-gen/templates/install_portaudio.tmpl.rst create mode 100644 testing/.gitignore diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000000..dffe22a6f46 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# Generated by synthtool. DO NOT EDIT! +[run] +branch = True +omit = + google/__init__.py + google/cloud/__init__.py + +[report] +fail_under = 35 +show_missing = True +exclude_lines = + # Re-enable the standard pragma + pragma: NO COVER + # Ignore debug-only repr + def __repr__ + # Ignore abstract methods + raise NotImplementedError +omit = + */gapic/*.py + */proto/*.py + */core/*.py + */site-packages/*.py + google/cloud/__init__.py diff --git a/.flake8 b/.flake8 index 7826eceab6a..87f6e408c47 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2020 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ [flake8] ignore = E203, E231, E266, E501, W503 exclude = - third_party/** - # Exclude generated code. **/proto/** **/gapic/** diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml new file mode 100644 index 00000000000..a3da1b0d4cd --- /dev/null +++ b/.github/.OwlBot.lock.yaml @@ -0,0 +1,17 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +docker: + image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest + digest: sha256:3e3800bb100af5d7f9e810d48212b37812c1856d20ffeafb99ebe66461b61fc7 +# created: 2023-08-02T10:53:29.114535628Z diff --git a/.github/.OwlBot.yaml b/.github/.OwlBot.yaml new file mode 100644 index 00000000000..c379bd3092d --- /dev/null +++ b/.github/.OwlBot.yaml @@ -0,0 +1,18 @@ +# Copyright 2021 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. + +docker: + image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest + +begin-after-commit-hash: 92006bb3cdc84677aa93c7f5235424ec2b157146 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd904459a91..7686a50da62 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,7 @@ # For syntax help see: # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax # Note: This file is autogenerated. To make changes to the codeowner team, please update .repo-metadata.json. + # @googleapis/yoshi-python @googleapis/api-bigquery-dataframe are the default owners for changes in this repo * @googleapis/yoshi-python @googleapis/api-bigquery-dataframe diff --git a/.github/header-checker-lint.yml b/.github/header-checker-lint.yml index 3058bec338c..6fe78aa7987 100644 --- a/.github/header-checker-lint.yml +++ b/.github/header-checker-lint.yml @@ -2,14 +2,14 @@ "allowedLicenses": ["Apache-2.0", "MIT", "BSD-3"], "ignoreFiles": ["**/requirements.txt", "**/requirements-test.txt", "**/__init__.py", "samples/**/constraints.txt", "samples/**/constraints-test.txt"], "sourceFileExtensions": [ - "ts", - "js", - "java", - "sh", - "Dockerfile", - "yaml", + "ts", + "js", + "java", + "sh", + "Dockerfile", + "yaml", "py", "html", "txt" ] -} +} \ No newline at end of file diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 5004e872b4c..b7dae76ba3b 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -15,7 +15,7 @@ branchProtectionRules: - 'unit (3.10)' - 'unit (3.11)' - 'cover' - # TODO(tswast): add Kokoro once we've enabled it + - 'Kokoro presubmit' permissionRules: - team: actools-python permission: admin diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d2aee5b7d8e..16d5a9e90f6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.8" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index bb268fe6c66..465199fc9a9 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -41,7 +41,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.8" - name: Install coverage run: | python -m pip install --upgrade setuptools pip wheel @@ -54,4 +54,4 @@ jobs: - name: Report coverage results run: | coverage combine .coverage-results/.coverage* - coverage report --show-missing --fail-under=40 + coverage report --show-missing --fail-under=35 diff --git a/.gitignore b/.gitignore index 2cb117c23ac..b4243ced74e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,12 +26,9 @@ pip-log.txt # Unit test / coverage reports .coverage -.coverage.* .nox .cache -.mypy_cache .pytest_cache -.pytype # Mac @@ -53,7 +50,6 @@ docs.metadata # Virtual environment env/ -venv/ # Test logs coverage.xml @@ -65,6 +61,3 @@ system_tests/local_test_setup # Make sure a generated file isn't accidentally committed. pylintrc pylintrc.test - -# Notebook scratch -.ipynb_checkpoints diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 402ac0eb8c9..f91c541c6c9 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -16,13 +16,13 @@ set -eo pipefail if [[ -z "${KOKORO_GOB_COMMIT}" ]]; then - PROJECT_SCM="github" + PROJECT_SCM="github/python-bigquery-dataframes" else - PROJECT_SCM="git" + PROJECT_SCM="git/bigframes" fi if [[ -z "${PROJECT_ROOT:-}" ]]; then - PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/${PROJECT_SCM}/bigframes" + PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/${PROJECT_SCM}" fi cd "${PROJECT_ROOT}" diff --git a/.kokoro/continuous/continuous.cfg b/.kokoro/continuous/continuous.cfg index 18a4c35325b..8f43917d92f 100644 --- a/.kokoro/continuous/continuous.cfg +++ b/.kokoro/continuous/continuous.cfg @@ -1 +1 @@ -# Format: //devtools/kokoro/config/proto/build.proto +# Format: //devtools/kokoro/config/proto/build.proto \ No newline at end of file diff --git a/.kokoro/continuous/prerelease-deps.cfg b/.kokoro/continuous/prerelease-deps.cfg new file mode 100644 index 00000000000..3595fb43f5c --- /dev/null +++ b/.kokoro/continuous/prerelease-deps.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "prerelease_deps" +} diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index e8ee8191ee1..8e39a2cc438 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -80,4 +80,4 @@ RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ # Test pip RUN python3 -m pip -CMD ["python3.9"] +CMD ["python3.8"] diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg index bd73988540c..e6e409f29c2 100644 --- a/.kokoro/docs/common.cfg +++ b/.kokoro/docs/common.cfg @@ -11,7 +11,7 @@ action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "bigframes/.kokoro/trampoline_v2.sh" +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { @@ -20,12 +20,18 @@ env_vars: { } env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: ".kokoro/publish-docs.sh" + value: "github/python-bigquery-dataframes/.kokoro/publish-docs.sh" +} + +env_vars: { + key: "STAGING_BUCKET" + value: "docs-staging" } env_vars: { key: "V2_STAGING_BUCKET" - # Push google cloud library docs to the Cloud RAD bucket `docs-staging-v2` + # Push non-cloud library docs to `docs-staging-v2-staging` instead of the + # Cloud RAD bucket `docs-staging-v2` value: "docs-staging-v2" } @@ -58,4 +64,4 @@ before_action { keyname: "docuploader_service_account" } } -} +} \ No newline at end of file diff --git a/.kokoro/docs/docs-presubmit.cfg b/.kokoro/docs/docs-presubmit.cfg index 1d0dc4b4991..805cfd162b2 100644 --- a/.kokoro/docs/docs-presubmit.cfg +++ b/.kokoro/docs/docs-presubmit.cfg @@ -1,5 +1,10 @@ # Format: //devtools/kokoro/config/proto/build.proto +env_vars: { + key: "STAGING_BUCKET" + value: "gcloud-python-test" +} + env_vars: { key: "V2_STAGING_BUCKET" value: "gcloud-python-test" @@ -13,11 +18,11 @@ env_vars: { env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: ".kokoro/build.sh" + value: "github/python-bigquery-dataframes/.kokoro/build.sh" } # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "docfx" + value: "docs docfx" } diff --git a/.kokoro/docs/docs.cfg b/.kokoro/docs/docs.cfg index 18a4c35325b..8f43917d92f 100644 --- a/.kokoro/docs/docs.cfg +++ b/.kokoro/docs/docs.cfg @@ -1 +1 @@ -# Format: //devtools/kokoro/config/proto/build.proto +# Format: //devtools/kokoro/config/proto/build.proto \ No newline at end of file diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh new file mode 100755 index 00000000000..6f3972140e8 --- /dev/null +++ b/.kokoro/populate-secrets.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Copyright 2023 Google LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eo pipefail + +function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} +function msg { println "$*" >&2 ;} +function println { printf '%s\n' "$(now) $*" ;} + + +# Populates requested secrets set in SECRET_MANAGER_KEYS from service account: +# kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com +SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" +msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" +mkdir -p ${SECRET_LOCATION} +for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") +do + msg "Retrieving secret ${key}" + docker run --entrypoint=gcloud \ + --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ + gcr.io/google.com/cloudsdktool/cloud-sdk \ + secrets versions access latest \ + --project cloud-devrel-kokoro-resources \ + --secret ${key} > \ + "${SECRET_LOCATION}/${key}" + if [[ $? == 0 ]]; then + msg "Secret written to ${SECRET_LOCATION}/${key}" + else + msg "Error retrieving secret ${key}" + fi +done diff --git a/.kokoro/presubmit/common.cfg b/.kokoro/presubmit/common.cfg index 5d40578ac79..97e0651aa92 100644 --- a/.kokoro/presubmit/common.cfg +++ b/.kokoro/presubmit/common.cfg @@ -7,4 +7,4 @@ action { } } -build_file: "bigframes/.kokoro/build.sh" +build_file: "python-bigquery-dataframes/.kokoro/build.sh" diff --git a/.kokoro/presubmit/prerelease-deps.cfg b/.kokoro/presubmit/prerelease-deps.cfg new file mode 100644 index 00000000000..3595fb43f5c --- /dev/null +++ b/.kokoro/presubmit/prerelease-deps.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "prerelease_deps" +} diff --git a/.kokoro/presubmit/presubmit.cfg b/.kokoro/presubmit/presubmit.cfg index 18a4c35325b..8f43917d92f 100644 --- a/.kokoro/presubmit/presubmit.cfg +++ b/.kokoro/presubmit/presubmit.cfg @@ -1 +1 @@ -# Format: //devtools/kokoro/config/proto/build.proto +# Format: //devtools/kokoro/config/proto/build.proto \ No newline at end of file diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index 3fc3905ac1c..7700c90ee92 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -24,6 +24,25 @@ export PATH="${HOME}/.local/bin:${PATH}" python3 -m pip install --require-hashes -r .kokoro/requirements.txt python3 -m nox --version +# build docs +nox -s docs + +# create metadata +python3 -m docuploader create-metadata \ + --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ + --version=$(python3 setup.py --version) \ + --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ + --distribution-name=$(python3 setup.py --name) \ + --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ + --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ + --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) + +cat docs.metadata + +# upload docs +python3 -m docuploader upload docs/_build/html --metadata-file docs.metadata --staging-bucket "${STAGING_BUCKET}" + + # docfx yaml files nox -s docfx diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh index 582808a15ce..488dbb9e130 100755 --- a/.kokoro/release-nightly.sh +++ b/.kokoro/release-nightly.sh @@ -35,13 +35,13 @@ while [ $# -gt 0 ] ; do done if [[ -z "${KOKORO_GOB_COMMIT}" ]]; then - PROJECT_SCM="github" + PROJECT_SCM="github/python-bigquery-dataframes" else - PROJECT_SCM="git" + PROJECT_SCM="git/bigframes" fi if [ -z "${PROJECT_ROOT:-}" ]; then - PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/${PROJECT_SCM}/bigframes" + PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/${PROJECT_SCM}" fi # Move into the package, build the distribution and upload to shared bucket. @@ -158,49 +158,6 @@ sed -i -e "s/$RELEASE_VERSION/$BIGFRAMES_VERSION/g" bigframes/version.py mv -f ${SETUP_CFG_BKP} setup.cfg rm -f ${THIRD_PARTY_NOTICES_FILE} -# Keep this last so as not to block the release on PDF docs build. -pdf_docs () { - sudo apt update - sudo apt install -y texlive texlive-latex-extra latexmk - - pushd "${PROJECT_ROOT}/docs" - make latexpdf - - cp "_build/latex/bigframes.pdf" "_build/latex/bigframes-${RELEASE_VERSION}.pdf" - cp "_build/latex/bigframes.pdf" "_build/latex/bigframes-latest.pdf" - - if ! [ ${DRY_RUN} ]; then - for gcs_path in gs://vertex_sdk_private_releases/bigframe/ \ - gs://dl-platform-colab/bigframes/ \ - gs://bigframes-wheels/; - do - gsutil cp -v "_build/latex/bigframes-*.pdf" ${gcs_path} - done - fi - - popd -} - -pdf_docs - -# Copy html docs to GCS from where it can be deployed to anywhere else -gcs_docs () { - docs_gcs_bucket=gs://bigframes-docs - docs_local_html_folder=docs/_build/html - if [ ! -d ${docs_local_html_folder} ]; then - python3.10 -m nox -s docs - fi - - if ! [ ${DRY_RUN} ]; then - gsutil -m cp -v -r ${docs_local_html_folder} ${docs_gcs_bucket}/${GIT_HASH} - - # Copy the script to refresh firebase docs website from GCS to GCS itself - gsutil -m cp -v scripts/update_firebase_docs_site.sh ${docs_gcs_bucket} - fi -} - -gcs_docs - if ! [ ${DRY_RUN} ]; then # Copy docs and wheels to Google Drive python3.10 scripts/upload_to_google_drive.py diff --git a/.kokoro/release.sh b/.kokoro/release.sh index 2bce143e1c9..320ac51271e 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -16,7 +16,7 @@ set -eo pipefail # Start the releasetool reporter -python3 -m pip install --require-hashes -r github/bigframes/.kokoro/requirements.txt +python3 -m pip install --require-hashes -r github/python-bigquery-dataframes/.kokoro/requirements.txt python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script # Disable buffering, so that the logs stream through. @@ -24,6 +24,6 @@ export PYTHONUNBUFFERED=1 # Move into the package, build the distribution and upload. TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-1") -cd github/bigframes +cd github/python-bigquery-dataframes python3 setup.py sdist bdist_wheel twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index 021198fb269..a0c39946cf1 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -11,7 +11,7 @@ action { gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" # Use the trampoline script to run in docker. -build_file: "bigframes/.kokoro/trampoline_v2.sh" +build_file: "python-bigquery-dataframes/.kokoro/trampoline.sh" # Configure the docker image for kokoro-trampoline. env_vars: { @@ -20,7 +20,7 @@ env_vars: { } env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/bigframes/.kokoro/release.sh" + value: "github/python-bigquery-dataframes/.kokoro/release.sh" } # Fetch PyPI password @@ -43,7 +43,7 @@ env_vars: { # what we published, which we can use to generate SBOMs and attestations. action { define_artifacts { - regex: "github/bigframes/**/*.tar.gz" - strip_prefix: "github/bigframes" + regex: "github/python-bigquery-dataframes/**/*.tar.gz" + strip_prefix: "github/python-bigquery-dataframes" } } diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in index 9fd9e127baf..ec867d9fd65 100644 --- a/.kokoro/requirements.in +++ b/.kokoro/requirements.in @@ -8,6 +8,3 @@ setuptools nox>=2022.11.21 # required to remove dependency on py charset-normalizer<3 click<8.1.0 -sphinx==4.5.0 # docs PDF see noxfile for HTML docs build -alabaster # docs PDF -recommonmark # docs PDF diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index fd0e0b9d489..029bd342de9 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -1,38 +1,28 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile --allow-unsafe --generate-hashes requirements.in # -alabaster==0.7.13 \ - --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \ - --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2 - # via - # -r requirements.in - # sphinx -argcomplete==3.1.1 \ - --hash=sha256:35fa893a88deea85ea7b20d241100e64516d6af6d7b0ae2bed1d263d26f70948 \ - --hash=sha256:6c4c563f14f01440aaffa3eae13441c5db2357b5eec639abe7c0b15334627dff +argcomplete==2.0.0 \ + --hash=sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20 \ + --hash=sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e # via nox -attrs==23.1.0 \ - --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ - --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 +attrs==22.1.0 \ + --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \ + --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c # via gcp-releasetool -babel==2.12.1 \ - --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \ - --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455 - # via sphinx -bleach==6.0.0 \ - --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \ - --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4 +bleach==5.0.1 \ + --hash=sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a \ + --hash=sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c # via readme-renderer -cachetools==5.3.1 \ - --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \ - --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b +cachetools==5.2.0 \ + --hash=sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757 \ + --hash=sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db # via google-auth -certifi==2023.5.7 \ - --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ - --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 +certifi==2023.7.22 \ + --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ + --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 # via requests cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ @@ -122,7 +112,7 @@ colorlog==6.7.0 \ commonmark==0.9.1 \ --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 - # via recommonmark + # via rich cryptography==41.0.3 \ --hash=sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306 \ --hash=sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84 \ @@ -154,34 +144,31 @@ distlib==0.3.6 \ --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e # via virtualenv -docutils==0.17.1 \ - --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ - --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 - # via - # readme-renderer - # recommonmark - # sphinx -filelock==3.12.2 \ - --hash=sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81 \ - --hash=sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec +docutils==0.19 \ + --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ + --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc + # via readme-renderer +filelock==3.8.0 \ + --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \ + --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4 # via virtualenv -gcp-docuploader==0.6.5 \ - --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ - --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea +gcp-docuploader==0.6.4 \ + --hash=sha256:01486419e24633af78fd0167db74a2763974765ee8078ca6eb6964d0ebd388af \ + --hash=sha256:70861190c123d907b3b067da896265ead2eeb9263969d6955c9e0bb091b5ccbf # via -r requirements.in -gcp-releasetool==1.14.0 \ - --hash=sha256:a39708206838477bf42804a9dd7b549131ff3fc08c6abb0dd6939e274bd6dfd4 \ - --hash=sha256:aa163ec8322045c9d46e03f8f7500974568bd5d8d322b6a2d492068a5bdf57bf +gcp-releasetool==1.10.5 \ + --hash=sha256:174b7b102d704b254f2a26a3eda2c684fd3543320ec239baf771542a2e58e109 \ + --hash=sha256:e29d29927fe2ca493105a82958c6873bb2b90d503acac56be2c229e74de0eec9 # via -r requirements.in -google-api-core==2.11.1 \ - --hash=sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a \ - --hash=sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a +google-api-core==2.10.2 \ + --hash=sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320 \ + --hash=sha256:34f24bd1d5f72a8c4519773d99ca6bf080a6c4e041b4e9f024fe230191dda62e # via # google-cloud-core # google-cloud-storage -google-auth==2.20.0 \ - --hash=sha256:030af34138909ccde0fbce611afc178f1d65d32fbff281f25738b1fe1c6f3eaa \ - --hash=sha256:23b7b0950fcda519bfb6692bf0d5289d2ea49fc143717cc7188458ec620e63fa +google-auth==2.14.1 \ + --hash=sha256:ccaa901f31ad5cbb562615eb8b664b3dd0bf5404a67618e642307f00613eda4d \ + --hash=sha256:f5d8701633bebc12e0deea4df8abd8aff31c28b355360597f7f2ee60f2e4d016 # via # gcp-releasetool # google-api-core @@ -191,9 +178,9 @@ google-cloud-core==2.3.2 \ --hash=sha256:8417acf6466be2fa85123441696c4badda48db314c607cf1e5d543fa8bdc22fe \ --hash=sha256:b9529ee7047fd8d4bf4a2182de619154240df17fbe60ead399078c1ae152af9a # via google-cloud-storage -google-cloud-storage==2.9.0 \ - --hash=sha256:83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94 \ - --hash=sha256:9b6ae7b509fc294bdacb84d0f3ea8e20e2c54a8b4bbe39c5707635fec214eff3 +google-cloud-storage==2.6.0 \ + --hash=sha256:104ca28ae61243b637f2f01455cc8a05e8f15a2a18ced96cb587241cdd3820f5 \ + --hash=sha256:4ad0415ff61abdd8bb2ae81c1f8f7ec7d91a1011613f2db87c614c550f97bfe9 # via gcp-docuploader google-crc32c==1.5.0 \ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ @@ -265,25 +252,21 @@ google-crc32c==1.5.0 \ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4 # via google-resumable-media -google-resumable-media==2.5.0 \ - --hash=sha256:218931e8e2b2a73a58eb354a288e03a0fd5fb1c4583261ac6e4c078666468c93 \ - --hash=sha256:da1bd943e2e114a56d85d6848497ebf9be6a14d3db23e9fc57581e7c3e8170ec +google-resumable-media==2.4.0 \ + --hash=sha256:2aa004c16d295c8f6c33b2b4788ba59d366677c0a25ae7382436cb30f776deaa \ + --hash=sha256:8d5518502f92b9ecc84ac46779bd4f09694ecb3ba38a3e7ca737a86d15cbca1f # via google-cloud-storage -googleapis-common-protos==1.59.1 \ - --hash=sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e \ - --hash=sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a +googleapis-common-protos==1.57.0 \ + --hash=sha256:27a849d6205838fb6cc3c1c21cb9800707a661bb21c6ce7fb13e99eb1f8a0c46 \ + --hash=sha256:a9f4a1d7f6d9809657b7f1316a1aa527f6664891531bcfcc13b6696e685f443c # via google-api-core idna==3.4 \ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a - # via sphinx -importlib-metadata==6.7.0 \ - --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ - --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 +importlib-metadata==5.0.0 \ + --hash=sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab \ + --hash=sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43 # via # -r requirements.in # keyring @@ -301,97 +284,76 @@ jeepney==0.8.0 \ jinja2==3.1.2 \ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 - # via - # gcp-releasetool - # sphinx -keyring==24.0.0 \ - --hash=sha256:4e87665a19c514c7edada8b15015cf89bd99b8d7edabc5c43cca77166fa8dfad \ - --hash=sha256:770f609eed2a16c65a6349f3ba1545d00c73f9fed4254c13766c674fe6d0d22b + # via gcp-releasetool +keyring==23.11.0 \ + --hash=sha256:3dd30011d555f1345dec2c262f0153f2f0ca6bca041fb1dc4588349bb4c0ac1e \ + --hash=sha256:ad192263e2cdd5f12875dedc2da13534359a7e760e77f8d04b50968a821c2361 # via # gcp-releasetool # twine -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb - # via rich -markupsafe==2.1.3 \ - --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ - --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ - --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ - --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ - --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ - --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ - --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ - --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ - --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ - --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ - --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ - --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ - --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ - --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ - --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ - --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ - --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ - --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ - --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ - --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ - --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ - --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ - --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ - --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ - --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ - --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ - --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ - --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ - --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ - --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ - --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ - --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ - --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ - --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ - --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ - --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ - --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ - --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ - --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ - --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ - --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ - --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ - --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ - --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ - --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ - --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ - --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ - --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ - --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ - --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 +markupsafe==2.1.1 \ + --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \ + --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \ + --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \ + --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \ + --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \ + --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \ + --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \ + --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \ + --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \ + --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \ + --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \ + --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \ + --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \ + --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \ + --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \ + --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \ + --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \ + --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \ + --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \ + --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \ + --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \ + --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \ + --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \ + --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \ + --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \ + --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \ + --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \ + --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \ + --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \ + --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \ + --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \ + --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \ + --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \ + --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \ + --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \ + --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \ + --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \ + --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \ + --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \ + --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7 # via jinja2 -mdurl==0.1.2 \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba - # via markdown-it-py -more-itertools==9.1.0 \ - --hash=sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d \ - --hash=sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3 +more-itertools==9.0.0 \ + --hash=sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41 \ + --hash=sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab # via jaraco-classes -nox==2023.4.22 \ - --hash=sha256:0b1adc619c58ab4fa57d6ab2e7823fe47a32e70202f287d78474adcc7bda1891 \ - --hash=sha256:46c0560b0dc609d7d967dc99e22cb463d3c4caf54a5fda735d6c11b5177e3a9f +nox==2022.11.21 \ + --hash=sha256:0e41a990e290e274cb205a976c4c97ee3c5234441a8132c8c3fd9ea3c22149eb \ + --hash=sha256:e21c31de0711d1274ca585a2c5fde36b1aa962005ba8e9322bf5eeed16dcd684 # via -r requirements.in -packaging==23.1 \ - --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ - --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f +packaging==21.3 \ + --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ + --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 # via # gcp-releasetool # nox - # sphinx -pkginfo==1.9.6 \ - --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \ - --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046 +pkginfo==1.8.3 \ + --hash=sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594 \ + --hash=sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c # via twine -platformdirs==3.7.0 \ - --hash=sha256:87fbf6473e87c078d536980ba970a472422e94f17b752cfad17024c18876d481 \ - --hash=sha256:cfd065ba43133ff103ab3bd10aecb095c2a0035fcd1f07217c9376900d94ba07 +platformdirs==2.5.4 \ + --hash=sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7 \ + --hash=sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10 # via virtualenv protobuf==3.20.3 \ --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ @@ -420,32 +382,34 @@ protobuf==3.20.3 \ # gcp-docuploader # gcp-releasetool # google-api-core - # googleapis-common-protos -pyasn1==0.5.0 \ - --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \ - --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde +pyasn1==0.4.8 \ + --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ + --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba # via # pyasn1-modules # rsa -pyasn1-modules==0.3.0 \ - --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ - --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d +pyasn1-modules==0.2.8 \ + --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \ + --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 # via google-auth pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pygments==2.15.1 \ - --hash=sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c \ - --hash=sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1 +pygments==2.15.0 \ + --hash=sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094 \ + --hash=sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500 # via # readme-renderer # rich - # sphinx -pyjwt==2.7.0 \ - --hash=sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1 \ - --hash=sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074 +pyjwt==2.6.0 \ + --hash=sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd \ + --hash=sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14 # via gcp-releasetool +pyparsing==3.0.9 \ + --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ + --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc + # via packaging pyperclip==1.8.2 \ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 # via gcp-releasetool @@ -453,14 +417,10 @@ python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 # via gcp-releasetool -readme-renderer==40.0 \ - --hash=sha256:9f77b519d96d03d7d7dce44977ba543090a14397c4f60de5b6eb5b8048110aa4 \ - --hash=sha256:e18feb2a1e7706f2865b81ebb460056d93fb29d69daa10b223c00faa7bd9a00a +readme-renderer==37.3 \ + --hash=sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273 \ + --hash=sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343 # via twine -recommonmark==0.7.1 \ - --hash=sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f \ - --hash=sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67 - # via -r requirements.in requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 @@ -469,19 +429,18 @@ requests==2.31.0 \ # google-api-core # google-cloud-storage # requests-toolbelt - # sphinx # twine -requests-toolbelt==1.0.0 \ - --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ - --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 +requests-toolbelt==0.10.1 \ + --hash=sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7 \ + --hash=sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d # via twine rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==13.4.2 \ - --hash=sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec \ - --hash=sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898 +rich==12.6.0 \ + --hash=sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e \ + --hash=sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0 # via twine rsa==4.9 \ --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ @@ -499,74 +458,39 @@ six==1.16.0 \ # gcp-docuploader # google-auth # python-dateutil -snowballstemmer==2.2.0 \ - --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ - --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a - # via sphinx -sphinx==4.5.0 \ - --hash=sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6 \ - --hash=sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226 - # via - # -r requirements.in - # recommonmark -sphinxcontrib-applehelp==1.0.4 \ - --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \ - --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e - # via sphinx -sphinxcontrib-devhelp==1.0.2 \ - --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ - --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 - # via sphinx -sphinxcontrib-htmlhelp==2.0.1 \ - --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \ - --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903 - # via sphinx -sphinxcontrib-jsmath==1.0.1 \ - --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ - --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 - # via sphinx -sphinxcontrib-qthelp==1.0.3 \ - --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ - --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 - # via sphinx -sphinxcontrib-serializinghtml==1.1.5 \ - --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ - --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 - # via sphinx -twine==4.0.2 \ - --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \ - --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8 +twine==4.0.1 \ + --hash=sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e \ + --hash=sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0 # via -r requirements.in -typing-extensions==4.6.3 \ - --hash=sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26 \ - --hash=sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5 +typing-extensions==4.4.0 \ + --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ + --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via -r requirements.in -urllib3==1.26.16 \ - --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \ - --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14 +urllib3==1.26.12 \ + --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ + --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 # via - # google-auth # requests # twine -virtualenv==20.23.1 \ - --hash=sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419 \ - --hash=sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1 +virtualenv==20.16.7 \ + --hash=sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e \ + --hash=sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29 # via nox webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 # via bleach -wheel==0.40.0 \ - --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \ - --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247 +wheel==0.38.4 \ + --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ + --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 # via -r requirements.in -zipp==3.15.0 \ - --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ - --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 +zipp==3.10.0 \ + --hash=sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1 \ + --hash=sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==68.0.0 \ - --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \ - --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235 +setuptools==65.5.1 \ + --hash=sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31 \ + --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f # via -r requirements.in diff --git a/.kokoro/samples/lint/common.cfg b/.kokoro/samples/lint/common.cfg new file mode 100644 index 00000000000..b4d26c1f982 --- /dev/null +++ b/.kokoro/samples/lint/common.cfg @@ -0,0 +1,34 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "lint" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/lint/continuous.cfg b/.kokoro/samples/lint/continuous.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/lint/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/lint/periodic.cfg b/.kokoro/samples/lint/periodic.cfg new file mode 100644 index 00000000000..50fec964973 --- /dev/null +++ b/.kokoro/samples/lint/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} \ No newline at end of file diff --git a/.kokoro/samples/lint/presubmit.cfg b/.kokoro/samples/lint/presubmit.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/lint/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.10/common.cfg b/.kokoro/samples/python3.10/common.cfg new file mode 100644 index 00000000000..8f9c66c571b --- /dev/null +++ b/.kokoro/samples/python3.10/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.10" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-310" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.10/continuous.cfg b/.kokoro/samples/python3.10/continuous.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.10/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.10/periodic-head.cfg b/.kokoro/samples/python3.10/periodic-head.cfg new file mode 100644 index 00000000000..123a35fbd3d --- /dev/null +++ b/.kokoro/samples/python3.10/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.10/periodic.cfg b/.kokoro/samples/python3.10/periodic.cfg new file mode 100644 index 00000000000..71cd1e597e3 --- /dev/null +++ b/.kokoro/samples/python3.10/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.10/presubmit.cfg b/.kokoro/samples/python3.10/presubmit.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.10/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.11/common.cfg b/.kokoro/samples/python3.11/common.cfg new file mode 100644 index 00000000000..1bba39114aa --- /dev/null +++ b/.kokoro/samples/python3.11/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.11" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-311" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.11/continuous.cfg b/.kokoro/samples/python3.11/continuous.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.11/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.11/periodic-head.cfg b/.kokoro/samples/python3.11/periodic-head.cfg new file mode 100644 index 00000000000..123a35fbd3d --- /dev/null +++ b/.kokoro/samples/python3.11/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.11/periodic.cfg b/.kokoro/samples/python3.11/periodic.cfg new file mode 100644 index 00000000000..71cd1e597e3 --- /dev/null +++ b/.kokoro/samples/python3.11/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.11/presubmit.cfg b/.kokoro/samples/python3.11/presubmit.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.11/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.7/common.cfg b/.kokoro/samples/python3.7/common.cfg new file mode 100644 index 00000000000..09d7af02ba9 --- /dev/null +++ b/.kokoro/samples/python3.7/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.7" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-py37" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.7/continuous.cfg b/.kokoro/samples/python3.7/continuous.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.7/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.7/periodic-head.cfg b/.kokoro/samples/python3.7/periodic-head.cfg new file mode 100644 index 00000000000..123a35fbd3d --- /dev/null +++ b/.kokoro/samples/python3.7/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.7/periodic.cfg b/.kokoro/samples/python3.7/periodic.cfg new file mode 100644 index 00000000000..71cd1e597e3 --- /dev/null +++ b/.kokoro/samples/python3.7/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.7/presubmit.cfg b/.kokoro/samples/python3.7/presubmit.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.7/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.8/common.cfg b/.kokoro/samples/python3.8/common.cfg new file mode 100644 index 00000000000..976d9ce8c5c --- /dev/null +++ b/.kokoro/samples/python3.8/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.8" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-py38" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.8/continuous.cfg b/.kokoro/samples/python3.8/continuous.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.8/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.8/periodic-head.cfg b/.kokoro/samples/python3.8/periodic-head.cfg new file mode 100644 index 00000000000..123a35fbd3d --- /dev/null +++ b/.kokoro/samples/python3.8/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.8/periodic.cfg b/.kokoro/samples/python3.8/periodic.cfg new file mode 100644 index 00000000000..71cd1e597e3 --- /dev/null +++ b/.kokoro/samples/python3.8/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.8/presubmit.cfg b/.kokoro/samples/python3.8/presubmit.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.8/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.9/common.cfg b/.kokoro/samples/python3.9/common.cfg new file mode 100644 index 00000000000..603cfffa280 --- /dev/null +++ b/.kokoro/samples/python3.9/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.9" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-py39" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.9/continuous.cfg b/.kokoro/samples/python3.9/continuous.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.9/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.9/periodic-head.cfg b/.kokoro/samples/python3.9/periodic-head.cfg new file mode 100644 index 00000000000..123a35fbd3d --- /dev/null +++ b/.kokoro/samples/python3.9/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.9/periodic.cfg b/.kokoro/samples/python3.9/periodic.cfg new file mode 100644 index 00000000000..71cd1e597e3 --- /dev/null +++ b/.kokoro/samples/python3.9/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.9/presubmit.cfg b/.kokoro/samples/python3.9/presubmit.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.9/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/test-samples-against-head.sh b/.kokoro/test-samples-against-head.sh new file mode 100755 index 00000000000..63ac41dfae1 --- /dev/null +++ b/.kokoro/test-samples-against-head.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# A customized test runner for samples. +# +# For periodic builds, you can specify this file for testing against head. + +# `-e` enables the script to automatically fail when a command fails +# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero +set -eo pipefail +# Enables `**` to include files nested inside sub-folders +shopt -s globstar + +exec .kokoro/test-samples-impl.sh diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh new file mode 100755 index 00000000000..5a0f5fab6a8 --- /dev/null +++ b/.kokoro/test-samples-impl.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + + +# `-e` enables the script to automatically fail when a command fails +# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero +set -eo pipefail +# Enables `**` to include files nested inside sub-folders +shopt -s globstar + +# Exit early if samples don't exist +if ! find samples -name 'requirements.txt' | grep -q .; then + echo "No tests run. './samples/**/requirements.txt' not found" + exit 0 +fi + +# Disable buffering, so that the logs stream through. +export PYTHONUNBUFFERED=1 + +# Debug: show build environment +env | grep KOKORO + +# Install nox +python3.9 -m pip install --upgrade --quiet nox + +# Use secrets acessor service account to get secrets +if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then + gcloud auth activate-service-account \ + --key-file="${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" \ + --project="cloud-devrel-kokoro-resources" +fi + +# This script will create 3 files: +# - testing/test-env.sh +# - testing/service-account.json +# - testing/client-secrets.json +./scripts/decrypt-secrets.sh + +source ./testing/test-env.sh +export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/testing/service-account.json + +# For cloud-run session, we activate the service account for gcloud sdk. +gcloud auth activate-service-account \ + --key-file "${GOOGLE_APPLICATION_CREDENTIALS}" + +export GOOGLE_CLIENT_SECRETS=$(pwd)/testing/client-secrets.json + +echo -e "\n******************** TESTING PROJECTS ********************" + +# Switch to 'fail at end' to allow all tests to complete before exiting. +set +e +# Use RTN to return a non-zero value if the test fails. +RTN=0 +ROOT=$(pwd) +# Find all requirements.txt in the samples directory (may break on whitespace). +for file in samples/**/requirements.txt; do + cd "$ROOT" + # Navigate to the project folder. + file=$(dirname "$file") + cd "$file" + + echo "------------------------------------------------------------" + echo "- testing $file" + echo "------------------------------------------------------------" + + # Use nox to execute the tests for the project. + python3.9 -m nox -s "$RUN_TESTS_SESSION" + EXIT=$? + + # If this is a periodic build, send the test log to the FlakyBot. + # See https://github.com/googleapis/repo-automation-bots/tree/main/packages/flakybot. + if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot + fi + + if [[ $EXIT -ne 0 ]]; then + RTN=1 + echo -e "\n Testing failed: Nox returned a non-zero exit code. \n" + else + echo -e "\n Testing completed.\n" + fi + +done +cd "$ROOT" + +# Workaround for Kokoro permissions issue: delete secrets +rm testing/{test-env.sh,client-secrets.json,service-account.json} + +exit "$RTN" diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh new file mode 100755 index 00000000000..50b35a48c19 --- /dev/null +++ b/.kokoro/test-samples.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# The default test runner for samples. +# +# For periodic builds, we rewinds the repo to the latest release, and +# run test-samples-impl.sh. + +# `-e` enables the script to automatically fail when a command fails +# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero +set -eo pipefail +# Enables `**` to include files nested inside sub-folders +shopt -s globstar + +# Run periodic samples tests at latest release +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then + # preserving the test runner implementation. + cp .kokoro/test-samples-impl.sh "${TMPDIR}/test-samples-impl.sh" + echo "--- IMPORTANT IMPORTANT IMPORTANT ---" + echo "Now we rewind the repo back to the latest release..." + LATEST_RELEASE=$(git describe --abbrev=0 --tags) + git checkout $LATEST_RELEASE + echo "The current head is: " + echo $(git rev-parse --verify HEAD) + echo "--- IMPORTANT IMPORTANT IMPORTANT ---" + # move back the test runner implementation if there's no file. + if [ ! -f .kokoro/test-samples-impl.sh ]; then + cp "${TMPDIR}/test-samples-impl.sh" .kokoro/test-samples-impl.sh + fi +fi + +exec .kokoro/test-samples-impl.sh diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh new file mode 100755 index 00000000000..d85b1f26769 --- /dev/null +++ b/.kokoro/trampoline.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eo pipefail + +# Always run the cleanup script, regardless of the success of bouncing into +# the container. +function cleanup() { + chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + echo "cleanup"; +} +trap cleanup EXIT + +$(dirname $0)/populate-secrets.sh # Secret Manager secrets. +python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" \ No newline at end of file diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh old mode 100644 new mode 100755 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07d7e6b7445..6e0fd8b98fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,11 +29,11 @@ repos: - repo: https://github.com/psf/black rev: 22.3.0 hooks: - - id: black + - id: black - repo: https://github.com/pycqa/flake8 - rev: 3.9.2 + rev: 6.1.0 hooks: - - id: flake8 + - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.1.1 hooks: diff --git a/.trampolinerc b/.trampolinerc index 4c16e33fee0..a7dfeb42c6d 100644 --- a/.trampolinerc +++ b/.trampolinerc @@ -20,6 +20,9 @@ required_envvars+=( pass_down_envvars+=( "NOX_SESSION" ############### + # Docs builds + ############### + "STAGING_BUCKET" "V2_STAGING_BUCKET" ################## # Samples builds diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 51d6abc4d3d..039f4368120 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,3 +1,4 @@ + # Code of Conduct ## Our Pledge @@ -91,4 +92,4 @@ harassment or threats to anyone's safety, we may take action without notice. This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at -https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000000..3933152cf78 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,277 @@ +.. Generated by synthtool. DO NOT EDIT! +############ +Contributing +############ + +#. **Please sign one of the contributor license agreements below.** +#. Fork the repo, develop and test your code changes, add docs. +#. Make sure that your commit messages clearly describe the changes. +#. Send a pull request. (Please Read: `Faster Pull Request Reviews`_) + +.. _Faster Pull Request Reviews: https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews + +.. contents:: Here are some guidelines for hacking on the Google Cloud Client libraries. + +*************** +Adding Features +*************** + +In order to add a feature: + +- The feature must be documented in both the API and narrative + documentation. + +- The feature must work fully on the following CPython versions: + 3.9, 3.10 and 3.11 on both UNIX and Windows. + +- The feature must not add unnecessary dependencies (where + "unnecessary" is of course subjective, but new dependencies should + be discussed). + +**************************** +Using a Development Checkout +**************************** + +You'll have to create a development environment using a Git checkout: + +- While logged into your GitHub account, navigate to the + ``python-bigquery-dataframes`` `repo`_ on GitHub. + +- Fork and clone the ``python-bigquery-dataframes`` repository to your GitHub account by + clicking the "Fork" button. + +- Clone your fork of ``python-bigquery-dataframes`` from your GitHub account to your local + computer, substituting your account username and specifying the destination + as ``hack-on-python-bigquery-dataframes``. E.g.:: + + $ cd ${HOME} + $ git clone git@github.com:USERNAME/python-bigquery-dataframes.git hack-on-python-bigquery-dataframes + $ cd hack-on-python-bigquery-dataframes + # Configure remotes such that you can pull changes from the googleapis/python-bigquery-dataframes + # repository into your local repository. + $ git remote add upstream git@github.com:googleapis/python-bigquery-dataframes.git + # fetch and merge changes from upstream into main + $ git fetch upstream + $ git merge upstream/main + +Now your local repo is set up such that you will push changes to your GitHub +repo, from which you can submit a pull request. + +To work on the codebase and run the tests, we recommend using ``nox``, +but you can also use a ``virtualenv`` of your own creation. + +.. _repo: https://github.com/googleapis/python-bigquery-dataframes + +Using ``nox`` +============= + +We use `nox `__ to instrument our tests. + +- To test your changes, run unit tests with ``nox``:: + $ nox -s unit + +- To run a single unit test:: + + $ nox -s unit-3.11 -- -k + + + .. note:: + + The unit tests and system tests are described in the + ``noxfile.py`` files in each directory. + +.. nox: https://pypi.org/project/nox/ + +***************************************** +I'm getting weird errors... Can you help? +***************************************** + +If the error mentions ``Python.h`` not being found, +install ``python-dev`` and try again. +On Debian/Ubuntu:: + + $ sudo apt-get install python-dev + +************ +Coding Style +************ +- We use the automatic code formatter ``black``. You can run it using + the nox session ``blacken``. This will eliminate many lint errors. Run via:: + + $ nox -s blacken + +- PEP8 compliance is required, with exceptions defined in the linter configuration. + If you have ``nox`` installed, you can test that you have not introduced + any non-compliant code via:: + + $ nox -s lint + +- In order to make ``nox -s lint`` run faster, you can set some environment + variables:: + + export GOOGLE_CLOUD_TESTING_REMOTE="upstream" + export GOOGLE_CLOUD_TESTING_BRANCH="main" + + By doing this, you are specifying the location of the most up-to-date + version of ``python-bigquery-dataframes``. The + remote name ``upstream`` should point to the official ``googleapis`` + checkout and the branch should be the default branch on that remote (``main``). + +- This repository contains configuration for the + `pre-commit `__ tool, which automates checking + our linters during a commit. If you have it installed on your ``$PATH``, + you can enable enforcing those checks via: + +.. code-block:: bash + + $ pre-commit install + pre-commit installed at .git/hooks/pre-commit + +Exceptions to PEP8: + +- Many unit tests use a helper method, ``_call_fut`` ("FUT" is short for + "Function-Under-Test"), which is PEP8-incompliant, but more readable. + Some also use a local variable, ``MUT`` (short for "Module-Under-Test"). + +******************** +Running System Tests +******************** + +- To run system tests, you can execute:: + + # Run all system tests + $ nox -s system + + # Run a single system test + $ nox -s system-3.11 -- -k + + + .. note:: + + System tests are only configured to run under Python 3.9 and 3.11. + For expediency, we do not run them in older versions of Python 3. + + This alone will not run the tests. You'll need to change some local + auth settings and change some configuration in your project to + run all the tests. + +- System tests will be run against an actual project. You should use local credentials from gcloud when possible. See `Best practices for application authentication `__. Some tests require a service account. For those tests see `Authenticating as a service account `__. + +************* +Test Coverage +************* + +- The codebase *must* have 100% test statement coverage after each commit. + You can test coverage via ``nox -s cover``. + +****************************************************** +Documentation Coverage and Building HTML Documentation +****************************************************** + +If you fix a bug, and the bug requires an API or behavior modification, all +documentation in this package which references that API or behavior must be +changed to reflect the bug fix, ideally in the same commit that fixes the bug +or adds the feature. + +Build the docs via: + + $ nox -s docs + +************************* +Samples and code snippets +************************* + +Code samples and snippets live in the `samples/` catalogue. Feel free to +provide more examples, but make sure to write tests for those examples. +Each folder containing example code requires its own `noxfile.py` script +which automates testing. If you decide to create a new folder, you can +base it on the `samples/snippets` folder (providing `noxfile.py` and +the requirements files). + +The tests will run against a real Google Cloud Project, so you should +configure them just like the System Tests. + +- To run sample tests, you can execute:: + + # Run all tests in a folder + $ cd samples/snippets + $ nox -s py-3.8 + + # Run a single sample test + $ cd samples/snippets + $ nox -s py-3.8 -- -k + +******************************************** +Note About ``README`` as it pertains to PyPI +******************************************** + +The `description on PyPI`_ for the project comes directly from the +``README``. Due to the reStructuredText (``rst``) parser used by +PyPI, relative links which will work on GitHub (e.g. ``CONTRIBUTING.rst`` +instead of +``https://github.com/googleapis/python-bigquery-dataframes/blob/main/CONTRIBUTING.rst``) +may cause problems creating links or rendering the description. + +.. _description on PyPI: https://pypi.org/project/bigframes + + +************************* +Supported Python Versions +************************* + +We support: + +- `Python 3.9`_ +- `Python 3.10`_ +- `Python 3.11`_ + +.. _Python 3.9: https://docs.python.org/3.9/ +.. _Python 3.10: https://docs.python.org/3.10/ +.. _Python 3.11: https://docs.python.org/3.11/ + + +Supported versions can be found in our ``noxfile.py`` `config`_. + +.. _config: https://github.com/googleapis/python-bigquery-dataframes/blob/main/noxfile.py + + +We also explicitly decided to support Python 3 beginning with version 3.9. +Reasons for this include: + +- Encouraging use of newest versions of Python 3 +- Taking the lead of `prominent`_ open-source `projects`_ +- `Unicode literal support`_ which allows for a cleaner codebase that + works in both Python 2 and Python 3 + +.. _prominent: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django +.. _projects: http://flask.pocoo.org/docs/0.10/python3/ +.. _Unicode literal support: https://www.python.org/dev/peps/pep-0414/ + +********** +Versioning +********** + +This library follows `Semantic Versioning`_. + +.. _Semantic Versioning: http://semver.org/ + +Some packages are currently in major version zero (``0.y.z``), which means that +anything may change at any time and the public API should not be considered +stable. + +****************************** +Contributor License Agreements +****************************** + +Before we can accept your pull requests you'll need to sign a Contributor +License Agreement (CLA): + +- **If you are an individual writing original source code** and **you own the + intellectual property**, then you'll need to sign an + `individual CLA `__. +- **If you work for a company that wants to allow you to contribute your work**, + then you'll need to sign a + `corporate CLA `__. + +You can sign these electronically (just scroll to the bottom). After that, +we'll be able to accept your pull requests. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000000..b422266a96a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# Generated by synthtool. DO NOT EDIT! +include README.rst LICENSE +recursive-include third_party * +recursive-include bigframes *.json *.proto py.typed +recursive-include tests * +global-exclude *.py[co] +global-exclude __pycache__ + +# Exclude scripts for samples readmegen +prune scripts/readme-gen diff --git a/docs/_static/custom.css b/docs/_static/custom.css index c663a6ef337..b0a295464b2 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,3 +1,8 @@ +div#python2-eol { + border-color: red; + border-width: medium; +} + /* Ensure minimum width for 'Parameters' / 'Returns' column */ dl.field-list > dt { min-width: 100px diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index bfdcc4759c5..6316a537f72 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -20,6 +20,11 @@ {% endblock %}
diff --git a/docs/conf.py b/docs/conf.py index 3ab5be51035..af8c5efda89 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2021 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ # serve to show the default. import os +import shlex import sys -from typing import Any, Dict # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -80,8 +80,8 @@ root_doc = "index" # General information about the project. -project = "BigQuery DataFrames" -copyright = "2022-2023 Google LLC" +project = "bigframes" +copyright = "2019, Google" author = "Google APIs" # The version info for the project you're documenting, acts as replacement for @@ -98,7 +98,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = "en" +language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -154,10 +154,10 @@ # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "description": "BigQuery DataFrames provides DataFrame APIs on the BigQuery engine.", - # "github_user": "googleapis", - # "github_repo": "python-bigquery-storage", - # "github_banner": True, + "description": "BigQuery DataFrames provides DataFrame APIs on the BigQuery engine", + "github_user": "googleapis", + "github_repo": "python-bigquery-dataframes", + "github_banner": True, "font_family": "'Roboto', Georgia, sans", "head_font_family": "'Roboto', Georgia, serif", "code_font_family": "'Roboto Mono', 'Consolas', monospace", @@ -185,7 +185,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ["_static"] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -264,13 +264,15 @@ # -- Options for LaTeX output --------------------------------------------- -latex_elements: Dict[str, Any] = { - # Avoid "too deeply nested" error by using enumitem package. - # See: https://stackoverflow.com/a/28454426/101923 - "preamble": r""" -\usepackage{enumitem} -\setlistdepth{99} -""" +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples @@ -366,6 +368,11 @@ "grpc": ("https://grpc.github.io/grpc/python/", None), "proto-plus": ("https://proto-plus-python.readthedocs.io/en/latest/", None), "protobuf": ("https://googleapis.dev/python/protobuf/latest/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "pydata-google-auth": ( + "https://pydata-google-auth.readthedocs.io/en/latest/", + None, + ), } diff --git a/noxfile.py b/noxfile.py index 1ceca6831b2..2355a9b27ba 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,7 +31,7 @@ BLACK_VERSION = "black==22.3.0" ISORT_VERSION = "isort==5.12.0" SPHINX_VERSION = "sphinx==4.5.0" -LINT_PATHS = ["docs", "bigframes", "tests", "noxfile.py", "setup.py"] +LINT_PATHS = ["docs", "bigframes", "tests", "third_party", "noxfile.py", "setup.py"] DEFAULT_PYTHON_VERSION = "3.10" @@ -108,6 +108,7 @@ def lint(session): "--check", *LINT_PATHS, ) + # TODO(tswast): lint all LINT_PATHS session.run("flake8", "bigframes", "tests") diff --git a/owlbot.py b/owlbot.py index 4ba7d14eb5e..be30eea5c2c 100644 --- a/owlbot.py +++ b/owlbot.py @@ -15,6 +15,7 @@ """This script is used to synthesize generated parts of this library.""" import pathlib +import re from synthtool import gcp import synthtool as s @@ -27,11 +28,10 @@ # ---------------------------------------------------------------------------- # Add templated files # ---------------------------------------------------------------------------- - templated_files = common.py_library( unit_test_python_versions=["3.9", "3.10", "3.11"], system_test_python_versions=["3.9", "3.11"], - cov_level=40, + cov_level=35, intersphinx_dependencies={ "pandas": "https://pandas.pydata.org/pandas-docs/stable/", "pydata-google-auth": "https://pydata-google-auth.readthedocs.io/en/latest/", @@ -40,11 +40,17 @@ s.move( templated_files, excludes=[ - # Multi-processing note isn't relevant, as pandas_gbq is responsible for + # Multi-processing note isn't relevant, as bigframes is responsible for # creating clients, not the end user. "docs/multiprocessing.rst", "noxfile.py", + ".pre-commit-config.yaml", "README.rst", + ".github/release-trigger.yml", + # BigQuery DataFrames manages its own Kokoro cluster for presubmit & continuous tests. + ".kokoro/build.sh", + ".kokoro/continuous/common.cfg", + ".kokoro/presubmit/common.cfg", ], ) @@ -52,6 +58,46 @@ # Fixup files # ---------------------------------------------------------------------------- +# Make sure build includes all necessary files. +s.replace( + ["MANIFEST.in"], + re.escape("recursive-include google"), + "recursive-include third_party *\nrecursive-include bigframes", +) + +# Even though BigQuery DataFrames isn't technically a client library, we are +# opting into Cloud RAD for docs hosting. +s.replace( + [".kokoro/docs/common.cfg"], + re.escape('value: "docs-staging-v2-staging"'), + 'value: "docs-staging-v2"', +) + +# Use a custom table of contents since the default one isn't organized well +# enough for the number of classes we have. +s.replace( + [".kokoro/publish-docs.sh"], + ( + re.escape("# upload docs") + + "\n" + + re.escape( + 'python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}"' + ) + ), + ( + "# Replace toc.yml template file\n" + + "mv docs/templates/toc.yml docs/_build/html/docfx_yaml/toc.yml\n\n" + + "# upload docs\n" + + 'python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}"' + ), +) + +# Fixup the documentation. +s.replace( + ["docs/conf.py"], + re.escape("Google Cloud Client Libraries for bigframes"), + "BigQuery DataFrames provides DataFrame APIs on the BigQuery engine", +) # ---------------------------------------------------------------------------- # Samples templates @@ -63,6 +109,6 @@ # Final cleanup # ---------------------------------------------------------------------------- -s.shell.run(["nox", "-s", "blacken"], hide_output=False) +s.shell.run(["nox", "-s", "format"], hide_output=False) for noxfile in REPO_ROOT.glob("samples/**/noxfile.py"): s.shell.run(["nox", "-s", "blacken"], cwd=noxfile.parent, hide_output=False) diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000000..39b2a0ec929 --- /dev/null +++ b/renovate.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "config:base", + "group:all", + ":preserveSemverRanges", + ":disableDependencyDashboard" + ], + "ignorePaths": [".pre-commit-config.yaml", ".kokoro/requirements.txt", "setup.py"], + "pip_requirements": { + "fileMatch": ["requirements-test.txt", "samples/[\\S/]*constraints.txt", "samples/[\\S/]*constraints-test.txt"] + } +} diff --git a/scripts/decrypt-secrets.sh b/scripts/decrypt-secrets.sh new file mode 100755 index 00000000000..0018b421ddf --- /dev/null +++ b/scripts/decrypt-secrets.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Copyright 2023 Google LLC All rights reserved. +# +# 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. + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ROOT=$( dirname "$DIR" ) + +# Work from the project root. +cd $ROOT + +# Prevent it from overriding files. +# We recommend that sample authors use their own service account files and cloud project. +# In that case, they are supposed to prepare these files by themselves. +if [[ -f "testing/test-env.sh" ]] || \ + [[ -f "testing/service-account.json" ]] || \ + [[ -f "testing/client-secrets.json" ]]; then + echo "One or more target files exist, aborting." + exit 1 +fi + +# Use SECRET_MANAGER_PROJECT if set, fallback to cloud-devrel-kokoro-resources. +PROJECT_ID="${SECRET_MANAGER_PROJECT:-cloud-devrel-kokoro-resources}" + +gcloud secrets versions access latest --secret="python-docs-samples-test-env" \ + --project="${PROJECT_ID}" \ + > testing/test-env.sh +gcloud secrets versions access latest \ + --secret="python-docs-samples-service-account" \ + --project="${PROJECT_ID}" \ + > testing/service-account.json +gcloud secrets versions access latest \ + --secret="python-docs-samples-client-secrets" \ + --project="${PROJECT_ID}" \ + > testing/client-secrets.json diff --git a/scripts/readme-gen/readme_gen.py b/scripts/readme-gen/readme_gen.py new file mode 100644 index 00000000000..1acc119835b --- /dev/null +++ b/scripts/readme-gen/readme_gen.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generates READMEs using configuration defined in yaml.""" + +import argparse +import io +import os +import subprocess + +import jinja2 +import yaml + + +jinja_env = jinja2.Environment( + trim_blocks=True, + loader=jinja2.FileSystemLoader( + os.path.abspath(os.path.join(os.path.dirname(__file__), "templates")) + ), + autoescape=True, +) + +README_TMPL = jinja_env.get_template("README.tmpl.rst") + + +def get_help(file): + return subprocess.check_output(["python", file, "--help"]).decode() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("source") + parser.add_argument("--destination", default="README.rst") + + args = parser.parse_args() + + source = os.path.abspath(args.source) + root = os.path.dirname(source) + destination = os.path.join(root, args.destination) + + jinja_env.globals["get_help"] = get_help + + with io.open(source, "r") as f: + config = yaml.load(f) + + # This allows get_help to execute in the right directory. + os.chdir(root) + + output = README_TMPL.render(config) + + with io.open(destination, "w") as f: + f.write(output) + + +if __name__ == "__main__": + main() diff --git a/scripts/readme-gen/templates/README.tmpl.rst b/scripts/readme-gen/templates/README.tmpl.rst new file mode 100644 index 00000000000..4fd239765b0 --- /dev/null +++ b/scripts/readme-gen/templates/README.tmpl.rst @@ -0,0 +1,87 @@ +{# The following line is a lie. BUT! Once jinja2 is done with it, it will + become truth! #} +.. This file is automatically generated. Do not edit this file directly. + +{{product.name}} Python Samples +=============================================================================== + +.. image:: https://gstatic.com/cloudssh/images/open-btn.png + :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor={{folder}}/README.rst + + +This directory contains samples for {{product.name}}. {{product.description}} + +{{description}} + +.. _{{product.name}}: {{product.url}} + +{% if required_api_url %} +To run the sample, you need to enable the API at: {{required_api_url}} +{% endif %} + +{% if required_role %} +To run the sample, you need to have `{{required_role}}` role. +{% endif %} + +{{other_required_steps}} + +{% if setup %} +Setup +------------------------------------------------------------------------------- + +{% for section in setup %} + +{% include section + '.tmpl.rst' %} + +{% endfor %} +{% endif %} + +{% if samples %} +Samples +------------------------------------------------------------------------------- + +{% for sample in samples %} +{{sample.name}} ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +{% if not sample.hide_cloudshell_button %} +.. image:: https://gstatic.com/cloudssh/images/open-btn.png + :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor={{folder}}/{{sample.file}},{{folder}}/README.rst +{% endif %} + + +{{sample.description}} + +To run this sample: + +.. code-block:: bash + + $ python {{sample.file}} +{% if sample.show_help %} + + {{get_help(sample.file)|indent}} +{% endif %} + + +{% endfor %} +{% endif %} + +{% if cloud_client_library %} + +The client library +------------------------------------------------------------------------------- + +This sample uses the `Google Cloud Client Library for Python`_. +You can read the documentation for more details on API usage and use GitHub +to `browse the source`_ and `report issues`_. + +.. _Google Cloud Client Library for Python: + https://googlecloudplatform.github.io/google-cloud-python/ +.. _browse the source: + https://github.com/GoogleCloudPlatform/google-cloud-python +.. _report issues: + https://github.com/GoogleCloudPlatform/google-cloud-python/issues + +{% endif %} + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ \ No newline at end of file diff --git a/scripts/readme-gen/templates/auth.tmpl.rst b/scripts/readme-gen/templates/auth.tmpl.rst new file mode 100644 index 00000000000..1446b94a5e3 --- /dev/null +++ b/scripts/readme-gen/templates/auth.tmpl.rst @@ -0,0 +1,9 @@ +Authentication +++++++++++++++ + +This sample requires you to have authentication setup. Refer to the +`Authentication Getting Started Guide`_ for instructions on setting up +credentials for applications. + +.. _Authentication Getting Started Guide: + https://cloud.google.com/docs/authentication/getting-started diff --git a/scripts/readme-gen/templates/auth_api_key.tmpl.rst b/scripts/readme-gen/templates/auth_api_key.tmpl.rst new file mode 100644 index 00000000000..11957ce2714 --- /dev/null +++ b/scripts/readme-gen/templates/auth_api_key.tmpl.rst @@ -0,0 +1,14 @@ +Authentication +++++++++++++++ + +Authentication for this service is done via an `API Key`_. To obtain an API +Key: + +1. Open the `Cloud Platform Console`_ +2. Make sure that billing is enabled for your project. +3. From the **Credentials** page, create a new **API Key** or use an existing + one for your project. + +.. _API Key: + https://developers.google.com/api-client-library/python/guide/aaa_apikeys +.. _Cloud Console: https://console.cloud.google.com/project?_ diff --git a/scripts/readme-gen/templates/install_deps.tmpl.rst b/scripts/readme-gen/templates/install_deps.tmpl.rst new file mode 100644 index 00000000000..6f069c6c87a --- /dev/null +++ b/scripts/readme-gen/templates/install_deps.tmpl.rst @@ -0,0 +1,29 @@ +Install Dependencies +++++++++++++++++++++ + +#. Clone python-docs-samples and change directory to the sample directory you want to use. + + .. code-block:: bash + + $ git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. You may want to refer to the `Python Development Environment Setup Guide`_ for Google Cloud Platform for instructions. + + .. _Python Development Environment Setup Guide: + https://cloud.google.com/python/setup + +#. Create a virtualenv. Samples are compatible with Python 3.7+. + + .. code-block:: bash + + $ virtualenv env + $ source env/bin/activate + +#. Install the dependencies needed to run the samples. + + .. code-block:: bash + + $ pip install -r requirements.txt + +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ diff --git a/scripts/readme-gen/templates/install_portaudio.tmpl.rst b/scripts/readme-gen/templates/install_portaudio.tmpl.rst new file mode 100644 index 00000000000..5ea33d18c00 --- /dev/null +++ b/scripts/readme-gen/templates/install_portaudio.tmpl.rst @@ -0,0 +1,35 @@ +Install PortAudio ++++++++++++++++++ + +Install `PortAudio`_. This is required by the `PyAudio`_ library to stream +audio from your computer's microphone. PyAudio depends on PortAudio for cross-platform compatibility, and is installed differently depending on the +platform. + +* For Mac OS X, you can use `Homebrew`_:: + + brew install portaudio + + **Note**: if you encounter an error when running `pip install` that indicates + it can't find `portaudio.h`, try running `pip install` with the following + flags:: + + pip install --global-option='build_ext' \ + --global-option='-I/usr/local/include' \ + --global-option='-L/usr/local/lib' \ + pyaudio + +* For Debian / Ubuntu Linux:: + + apt-get install portaudio19-dev python-all-dev + +* Windows may work without having to install PortAudio explicitly (it will get + installed with PyAudio). + +For more details, see the `PyAudio installation`_ page. + + +.. _PyAudio: https://people.csail.mit.edu/hubert/pyaudio/ +.. _PortAudio: http://www.portaudio.com/ +.. _PyAudio installation: + https://people.csail.mit.edu/hubert/pyaudio/#downloads +.. _Homebrew: http://brew.sh diff --git a/scripts/upload_to_google_drive.py b/scripts/upload_to_google_drive.py index e5791513595..dcdc9168ba2 100644 --- a/scripts/upload_to_google_drive.py +++ b/scripts/upload_to_google_drive.py @@ -41,12 +41,9 @@ wheel_id = "15fZ1DkrFDk4ibMNTzms4akpxmf2pzeAR" wheel_path = next(iter((repo_root / "dist").glob("bigframes-*.whl"))) -pdf_id = "1agYjxmPLrxelsaHI-lc41QHcgnQYemcX" -pdf_path = repo_root / "docs" / "_build" / "latex" / "bigframes-latest.pdf" - uploads = ( (wheel_id, wheel_path, "application/octet-stream"), - (pdf_id, pdf_path, "application/pdf"), + # (pdf_id, pdf_path, "application/pdf"), ) upload_template = ( diff --git a/setup.cfg b/setup.cfg index 8bd749387ed..05235008950 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2020 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,17 +17,3 @@ # Generated by synthtool. DO NOT EDIT! [bdist_wheel] universal = 1 - -[pytype] -python_version = 3.9 -inputs = - google/cloud/ -exclude = - tests/ - google/cloud/bigquery_v2/ # Legacy proto-based types. -output = .pytype/ -disable = - # There's some issue with finding some pyi files, thus disabling. - # The issue https://github.com/google/pytype/issues/150 is closed, but the - # error still occurs for some reason. - pyi-error diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 00000000000..b05fbd63088 --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,3 @@ +test-env.sh +service-account.json +client-secrets.json \ No newline at end of file From 7c6b0dd2f99139c8830e762201a45b28486532ff Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Sat, 16 Sep 2023 12:27:51 +0000 Subject: [PATCH 019/921] feat: add `DataFrame.combine` and `DataFrame.combine_first` (#27) feat: add `DataFrame.skew` and `GroupBy.skew` test: remove unneeded mock perf: `bigframes-api` label to I/O query jobs fix: `remote_function` uses same credentials as other APIs test: BQML golden SQL unit tests feat: add `DataFrame.pct_change` and `Series.pct_change` test: disable `remote_function` reuse in tests test: fix flaky repr_cache tests test: add unit tests for private `ArrayValue` class feat: add `DataFrame.to_dict`, `to_excel`, `to_latex`, `to_records`, `to_string`, `to_markdown`, `to_pickle`, `to_orc` fix: use for literals `Int64Dtype` in `cut` feat: add `DataFrame.nlargest`, `nsmallest` chore: refactor PCA tests feat: add `bfill` and `ffill` to `DataFrame` and `Series` feat: add `reindex_like` to `DataFrame` and `Series` fix: use lowercase strings for parameter literals in `bigframes.ml` (**breaking change**) feat: support `DataFrame.loc[bool_series, column] = scalar` fix: support column joins with "None indexer" docs: document region logic in README feat: add partial support for `Sereies.replace` fix: add type hints to models test: add more unit tests for internal `ArrayValue` feat: add `filter` and `reindex` to `Series` and `DataFrame` docs: document possible parameter values for PaLM2TextGenerator test: mark generate_text test as flaky feat: support a persistent `name` in `remote_function` fix: raise error when ARIMAPlus is used with Pipeline feat: add `swaplevel` to `DataFrame` and `Series` feat: add `axis` parameter to `droplevel` and `reorder_levels` docs: fix OneHotEncoder sample fix: remove `transforms` parameter in `model.fit` (**breaking change**) feat: add `diff` method to `DataFrame` and `GroupBy` --- .kokoro/docs/docs-presubmit-gerrit.cfg | 23 + .kokoro/presubmit/e2e-gerrit.cfg | 7 + .kokoro/presubmit/presubmit-gerrit.cfg | 1 + OWNERS | 1 + README.rst | 11 +- bigframes/core/__init__.py | 2 +- bigframes/core/block_transforms.py | 176 +++++++ bigframes/core/blocks.py | 16 +- bigframes/core/groupby/__init__.py | 44 +- bigframes/core/indexers.py | 42 +- bigframes/core/indexes/index.py | 20 + bigframes/core/utils.py | 4 +- bigframes/dataframe.py | 472 ++++++++++++++++- bigframes/dtypes.py | 2 +- bigframes/ml/base.py | 32 +- bigframes/ml/cluster.py | 6 +- bigframes/ml/compose.py | 2 +- bigframes/ml/decomposition.py | 6 +- bigframes/ml/ensemble.py | 64 +-- bigframes/ml/forecasting.py | 4 +- bigframes/ml/linear_model.py | 11 +- bigframes/ml/llm.py | 8 +- bigframes/ml/pipeline.py | 16 +- bigframes/ml/preprocessing.py | 4 +- bigframes/operations/aggregations.py | 70 ++- bigframes/pandas/__init__.py | 2 + bigframes/remote_function.py | 192 ++++--- bigframes/series.py | 218 +++++--- bigframes/session.py | 107 +++- noxfile.py | 1 + samples/snippets/remote_function.py | 24 +- setup.py | 3 +- testing/constraints-3.9.txt | 5 +- tests/system/large/ml/test_ensemble.py | 10 +- tests/system/large/test_remote_function.py | 296 ++++++++++- tests/system/small/ml/test_core.py | 2 + tests/system/small/ml/test_decomposition.py | 29 +- tests/system/small/test_dataframe.py | 489 ++++++++++++++++++ tests/system/small/test_groupby.py | 26 + tests/system/small/test_ipython.py | 5 +- tests/system/small/test_multiindex.py | 162 +++++- tests/system/small/test_pandas.py | 14 + tests/system/small/test_series.py | 179 ++++++- tests/unit/ml/test_golden_sql.py | 47 ++ tests/unit/ml/test_pipeline.py | 44 +- tests/unit/test_core.py | 169 ++++++ .../ibis/backends/bigquery/registry.py | 12 + .../ibis/expr/operations/__init__.py | 1 + .../ibis/expr/operations/analytic.py | 26 + .../bigframes_vendored/pandas/core/frame.py | 481 ++++++++++++++++- .../bigframes_vendored/pandas/core/generic.py | 89 ++++ .../pandas/core/groupby/__init__.py | 20 + .../pandas/core/indexes/base.py | 10 + .../bigframes_vendored/pandas/core/series.py | 124 ++++- .../bigframes_vendored/sklearn/base.py | 1 + .../sklearn/cluster/_kmeans.py | 5 - .../sklearn/decomposition/_pca.py | 4 - .../sklearn/ensemble/_forest.py | 4 - .../sklearn/linear_model/_base.py | 5 - .../sklearn/linear_model/_logistic.py | 5 - .../sklearn/preprocessing/_data.py | 31 +- .../sklearn/preprocessing/_encoder.py | 14 +- .../bigframes_vendored/xgboost/sklearn.py | 4 - 63 files changed, 3487 insertions(+), 417 deletions(-) create mode 100644 .kokoro/docs/docs-presubmit-gerrit.cfg create mode 100644 .kokoro/presubmit/e2e-gerrit.cfg create mode 100644 .kokoro/presubmit/presubmit-gerrit.cfg create mode 100644 tests/unit/ml/test_golden_sql.py create mode 100644 third_party/bigframes_vendored/ibis/expr/operations/analytic.py diff --git a/.kokoro/docs/docs-presubmit-gerrit.cfg b/.kokoro/docs/docs-presubmit-gerrit.cfg new file mode 100644 index 00000000000..1d0dc4b4991 --- /dev/null +++ b/.kokoro/docs/docs-presubmit-gerrit.cfg @@ -0,0 +1,23 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "V2_STAGING_BUCKET" + value: "gcloud-python-test" +} + +# We only upload the image in the main `docs` build. +env_vars: { + key: "TRAMPOLINE_IMAGE_UPLOAD" + value: "false" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: ".kokoro/build.sh" +} + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "docfx" +} diff --git a/.kokoro/presubmit/e2e-gerrit.cfg b/.kokoro/presubmit/e2e-gerrit.cfg new file mode 100644 index 00000000000..d875f360603 --- /dev/null +++ b/.kokoro/presubmit/e2e-gerrit.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "system_noextras e2e notebook samples" +} diff --git a/.kokoro/presubmit/presubmit-gerrit.cfg b/.kokoro/presubmit/presubmit-gerrit.cfg new file mode 100644 index 00000000000..18a4c35325b --- /dev/null +++ b/.kokoro/presubmit/presubmit-gerrit.cfg @@ -0,0 +1 @@ +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/OWNERS b/OWNERS index 672da38afa5..f86ad551efa 100644 --- a/OWNERS +++ b/OWNERS @@ -1,3 +1,4 @@ +ashleyxu@google.com bmil@google.com chelsealin@google.com garrettwu@google.com diff --git a/README.rst b/README.rst index 935c54cc8b5..23aea446ff7 100644 --- a/README.rst +++ b/README.rst @@ -57,9 +57,13 @@ internally to manage metadata on the service side. This session is tied to a BigQuery DataFrames uses the US multi-region as the default location, but you can use ``session_options.location`` to set a different location. Every query in a session is executed in the location where the session was created. +BigQuery DataFrames +auto-populates ``bf.options.bigquery.location`` if the user starts with +``read_gbq/read_gbq_table/read_gbq_query()`` and specifies a table, either +directly or in a SQL statement. If you want to reset the location of the created DataFrame or Series objects, -can reset the session by executing ``bigframes.pandas.reset_session()``. +you can reset the session by executing ``bigframes.pandas.reset_session()``. After that, you can reuse ``bigframes.pandas.options.bigquery.location`` to specify another location. @@ -68,6 +72,11 @@ specify another location. querying is not in the US multi-region. If you try to read a table from another location, you get a NotFound exception. +Project +------- +If ``bf.options.bigquery.project`` is not set, the ``$GOOGLE_CLOUD_PROJECT`` +environment variable is used, which is set in the notebook runtime serving the +BigQuery Studio/Vertex Notebooks. ML Capabilities --------------- diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 7086269af9b..27fe4a4fe6c 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -518,8 +518,8 @@ def aggregate( """ Apply aggregations to the expression. Arguments: - by_column_id: column id of the aggregation key, this is preserved through the transform aggregations: input_column_id, operation, output_column_id tuples + by_column_id: column id of the aggregation key, this is preserved through the transform dropna: whether null keys should be dropped """ table = self.to_ibis_expr(ordering_mode="unordered") diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index abf8b887d82..5dcd9fe7538 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -117,6 +117,25 @@ def value_counts( return block.select_column(count_id).with_column_labels(["count"]) +def pct_change(block: blocks.Block, periods: int = 1) -> blocks.Block: + column_labels = block.column_labels + window_spec = core.WindowSpec( + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + + original_columns = block.value_columns + block, shift_columns = block.multi_apply_window_op( + original_columns, agg_ops.ShiftOp(periods), window_spec=window_spec + ) + result_ids = [] + for original_col, shifted_col in zip(original_columns, shift_columns): + block, change_id = block.apply_binary_op(original_col, shifted_col, ops.sub_op) + block, pct_change_id = block.apply_binary_op(change_id, shifted_col, ops.div_op) + result_ids.append(pct_change_id) + return block.select_columns(result_ids).with_column_labels(column_labels) + + def rank( block: blocks.Block, method: str = "average", @@ -229,3 +248,160 @@ def dropna(block: blocks.Block, how: typing.Literal["all", "any"] = "any"): filtered_block = filtered_block.filter(predicate) filtered_block = filtered_block.select_columns(block.value_columns) return filtered_block + + +def nsmallest( + block: blocks.Block, + n: int, + column_ids: typing.Sequence[str], + keep: str, +) -> blocks.Block: + if keep not in ("first", "last", "all"): + raise ValueError("'keep must be one of 'first', 'last', or 'all'") + if keep == "last": + block = block.reversed() + order_refs = [ + ordering.OrderingColumnReference( + col_id, direction=ordering.OrderingDirection.ASC + ) + for col_id in column_ids + ] + block = block.order_by(order_refs, stable=True) + if keep in ("first", "last"): + return block.slice(0, n) + else: # keep == "all": + block, counter = block.apply_window_op( + column_ids[0], + agg_ops.rank_op, + window_spec=core.WindowSpec(ordering=order_refs), + ) + block, condition = block.apply_unary_op( + counter, ops.partial_right(ops.le_op, n) + ) + block = block.filter(condition) + return block.drop_columns([counter, condition]) + + +def nlargest( + block: blocks.Block, + n: int, + column_ids: typing.Sequence[str], + keep: str, +) -> blocks.Block: + if keep not in ("first", "last", "all"): + raise ValueError("'keep must be one of 'first', 'last', or 'all'") + if keep == "last": + block = block.reversed() + order_refs = [ + ordering.OrderingColumnReference( + col_id, direction=ordering.OrderingDirection.DESC + ) + for col_id in column_ids + ] + block = block.order_by(order_refs, stable=True) + if keep in ("first", "last"): + return block.slice(0, n) + else: # keep == "all": + block, counter = block.apply_window_op( + column_ids[0], + agg_ops.rank_op, + window_spec=core.WindowSpec(ordering=order_refs), + ) + block, condition = block.apply_unary_op( + counter, ops.partial_right(ops.le_op, n) + ) + block = block.filter(condition) + return block.drop_columns([counter, condition]) + + +def skew( + block: blocks.Block, + skew_column_ids: typing.Sequence[str], + grouping_column_ids: typing.Sequence[str] = (), +) -> blocks.Block: + + original_columns = skew_column_ids + column_labels = block.select_columns(original_columns).column_labels + + block, delta3_ids = _mean_delta_to_power( + block, 3, original_columns, grouping_column_ids + ) + # counts, moment3 for each column + aggregations = [] + for i, col in enumerate(original_columns): + count_agg = (col, agg_ops.count_op) + moment3_agg = (delta3_ids[i], agg_ops.mean_op) + variance_agg = (col, agg_ops.PopVarOp()) + aggregations.extend([count_agg, moment3_agg, variance_agg]) + + block, agg_ids = block.aggregate( + by_column_ids=grouping_column_ids, aggregations=aggregations + ) + + skew_ids = [] + for i, col in enumerate(original_columns): + # Corresponds to order of aggregations in preceding loop + count_id, moment3_id, var_id = agg_ids[i * 3 : (i * 3) + 3] + block, skew_id = _skew_from_moments_and_count( + block, count_id, moment3_id, var_id + ) + skew_ids.append(skew_id) + + block = block.select_columns(skew_ids).with_column_labels(column_labels) + if not grouping_column_ids: + # When ungrouped, stack everything into single column so can be returned as series + block = block.stack() + block = block.drop_levels([block.index_columns[0]]) + return block + + +def _mean_delta_to_power( + block: blocks.Block, + n_power, + column_ids: typing.Sequence[str], + grouping_column_ids: typing.Sequence[str], +) -> typing.Tuple[blocks.Block, typing.Sequence[str]]: + """Calculate (x-mean(x))^n. Useful for calculating moment statistics such as skew and kurtosis.""" + window = core.WindowSpec(grouping_keys=grouping_column_ids) + block, mean_ids = block.multi_apply_window_op(column_ids, agg_ops.mean_op, window) + delta_ids = [] + cube_op = ops.partial_right(ops.pow_op, n_power) + for val_id, mean_val_id in zip(column_ids, mean_ids): + block, delta_id = block.apply_binary_op(val_id, mean_val_id, ops.sub_op) + block, delta_power_id = block.apply_unary_op(delta_id, cube_op) + block = block.drop_columns(delta_id) + delta_ids.append(delta_power_id) + return block, delta_ids + + +def _skew_from_moments_and_count( + block: blocks.Block, count_id: str, moment3_id: str, var_id: str +) -> typing.Tuple[blocks.Block, str]: + # Calculate skew using count, third moment and population variance + # See G1 estimator: + # https://en.wikipedia.org/wiki/Skewness#Sample_skewness + block, denominator_id = block.apply_unary_op( + var_id, ops.partial_right(ops.pow_op, 3 / 2) + ) + block, base_id = block.apply_binary_op(moment3_id, denominator_id, ops.div_op) + block, countminus1_id = block.apply_unary_op( + count_id, ops.partial_right(ops.sub_op, 1) + ) + block, countminus2_id = block.apply_unary_op( + count_id, ops.partial_right(ops.sub_op, 2) + ) + block, adjustment_id = block.apply_binary_op(count_id, countminus1_id, ops.mul_op) + block, adjustment_id = block.apply_unary_op( + adjustment_id, ops.partial_right(ops.pow_op, 1 / 2) + ) + block, adjustment_id = block.apply_binary_op( + adjustment_id, countminus2_id, ops.div_op + ) + block, skew_id = block.apply_binary_op(base_id, adjustment_id, ops.mul_op) + + # Need to produce NA if have less than 3 data points + block, na_cond_id = block.apply_unary_op(count_id, ops.partial_right(ops.ge_op, 3)) + block, skew_id = block.apply_binary_op( + skew_id, na_cond_id, ops.partial_arg3(ops.where_op, None) + ) + return block, skew_id diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 482cfd0141e..5b414252ee9 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -709,8 +709,9 @@ def multi_apply_window_op( window_spec: core.WindowSpec, *, skip_null_groups: bool = False, - ) -> Block: + ) -> typing.Tuple[Block, typing.Sequence[str]]: block = self + result_ids = [] for i, col_id in enumerate(columns): label = self.col_id_to_label[col_id] block, result_id = block.apply_window_op( @@ -721,9 +722,8 @@ def multi_apply_window_op( result_label=label, skip_null_groups=skip_null_groups, ) - block = block.copy_values(result_id, col_id) - block = block.drop_columns([result_id]) - return block + result_ids.append(result_id) + return block, result_ids def multi_apply_unary_op( self, @@ -1123,7 +1123,9 @@ def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: ) def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: - axis_number = bigframes.core.utils.get_axis_number(axis) + axis_number = bigframes.core.utils.get_axis_number( + "rows" if (axis is None) else axis + ) if axis_number == 0: expr = self._expr for index_col in self._index_columns: @@ -1140,7 +1142,9 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: return self.rename(columns=lambda label: f"{prefix}{label}") def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: - axis_number = bigframes.core.utils.get_axis_number(axis) + axis_number = bigframes.core.utils.get_axis_number( + "rows" if (axis is None) else axis + ) if axis_number == 0: expr = self._expr for index_col in self._index_columns: diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 589c5c251ca..810e145d33f 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -20,6 +20,7 @@ import bigframes.constants as constants import bigframes.core as core +import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.ordering as order import bigframes.core.utils as utils @@ -145,6 +146,16 @@ def var( self._raise_on_non_numeric("var") return self._aggregate_all(agg_ops.var_op, numeric_only=True) + def skew( + self, + *, + numeric_only: bool = False, + ) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("skew") + block = block_ops.skew(self._block, self._selected_cols, self._by_col_ids) + return df.DataFrame(block) + def all(self) -> df.DataFrame: return self._aggregate_all(agg_ops.all_op) @@ -168,6 +179,22 @@ def cummax(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: def cumprod(self, *args, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.product_op, numeric_only=True) + def shift(self, periods=1) -> series.Series: + window = core.WindowSpec( + grouping_keys=self._by_col_ids, + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) + + def diff(self, periods=1) -> series.Series: + window = core.WindowSpec( + grouping_keys=self._by_col_ids, + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.DiffOp(periods), window=window) + def agg(self, func=None, **kwargs) -> df.DataFrame: if func: if isinstance(func, str): @@ -323,10 +350,10 @@ def _apply_window_op( grouping_keys=self._by_col_ids, following=0 ) columns = self._aggregated_columns(numeric_only=numeric_only) - block = self._block.multi_apply_window_op( + block, result_ids = self._block.multi_apply_window_op( columns, op, window_spec=window_spec, skip_null_groups=self._dropna ) - block = block.select_columns(columns) + block = block.select_columns(result_ids) return df.DataFrame(block) def _resolve_label(self, label: blocks.Label) -> str: @@ -391,6 +418,10 @@ def std(self, *args, **kwargs) -> series.Series: def var(self, *args, **kwargs) -> series.Series: return self._aggregate(agg_ops.var_op) + def skew(self, *args, **kwargs) -> series.Series: + block = block_ops.skew(self._block, [self._value_column], self._by_col_ids) + return series.Series(block) + def prod(self, *args) -> series.Series: return self._aggregate(agg_ops.product_op) @@ -459,8 +490,13 @@ def shift(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) - def diff(self) -> series.Series: - return self._ungroup() - self.shift(1) + def diff(self, periods=1) -> series.Series: + window = core.WindowSpec( + grouping_keys=self._by_col_ids, + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.DiffOp(periods), window=window) def rolling(self, window: int, min_periods=None) -> windows.Window: # To get n size window, need current row and n-1 preceding rows. diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 46091f211ac..28bce05338c 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -145,23 +145,41 @@ def __setitem__( value: bigframes.dataframe.SingleItemValue, ): if ( - not isinstance(key, tuple) - or len(key) != 2 - or not isinstance(key[0], slice) - or (key[0].start is not None and key[0].start != 0) - or (key[0].step is not None and key[0].step != 1) - or key[0].stop is not None + isinstance(key, tuple) + and len(key) == 2 + and isinstance(key[0], slice) + and (key[0].start is None or key[0].start == 0) + and (key[0].step is None or key[0].step == 1) + and key[0].stop is None ): + # TODO(swast): Support setting multiple columns with key[1] as a list + # of labels and value as a DataFrame. + df = self._dataframe.assign(**{key[1]: value}) + self._dataframe._set_block(df._get_block()) + elif ( + isinstance(key, tuple) + and len(key) == 2 + and isinstance(key[0], bigframes.series.Series) + and key[0].dtype == "boolean" + ) and pd.api.types.is_scalar(value): + new_column = key[0].map({True: value, False: None}) + try: + original_column = self._dataframe[key[1]] + except KeyError: + self._dataframe[key[1]] = new_column + return + try: + self._dataframe[key[1]] = new_column.fillna(original_column) + except ibis.common.exceptions.IbisTypeError: + raise TypeError( + f"Cannot assign scalar of type {type(value)} to column of type {original_column.dtype}, or index type of series argument does not match dataframe." + ) + else: raise NotImplementedError( - "Only setting a column by DataFrame.loc[:, 'column'] is supported." + "Only DataFrame.loc[:, 'column'] and DataFrame.loc[bool series, 'column'] = Scalar are supported." f"{constants.FEEDBACK_LINK}" ) - # TODO(swast): Support setting multiple columns with key[1] as a list - # of labels and value as a DataFrame. - df = self._dataframe.assign(**{key[1]: value}) - self._dataframe._set_block(df._get_block()) - class ILocDataFrameIndexer: def __init__(self, dataframe: bigframes.dataframe.DataFrame): diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 04b9a36b64a..748a68c9447 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -53,6 +53,10 @@ def names(self) -> typing.Sequence[blocks.Label]: def names(self, values: typing.Sequence[blocks.Label]): return self._data._set_block(self._data._get_block().with_index_labels(values)) + @property + def nlevels(self) -> int: + return len(self._data._get_block().index_columns) + @property def shape(self) -> typing.Tuple[int]: return (self._data._get_block().shape[0],) @@ -97,6 +101,22 @@ def is_monotonic_decreasing(self) -> bool: ), ) + @property + def is_unique(self) -> bool: + # TODO: Cache this at block level + # Avoid circular imports + import bigframes.core.block_transforms as block_ops + import bigframes.dataframe as df + + duplicates_block, _ = block_ops.indicate_duplicates( + self._data._get_block(), self._data._get_block().index_columns + ) + duplicates_block = duplicates_block.with_column_labels( + ["values", "is_duplicate"] + ) + duplicates_df = df.DataFrame(duplicates_block) + return not duplicates_df["is_duplicate"].any() + def __getitem__(self, key: int) -> typing.Any: if isinstance(key, int): result_pd_df, _ = self._data._get_block().slice(key, key + 1, 1).to_pandas() diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 1c0a2a1a81e..75175690ce0 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -23,8 +23,8 @@ UNNAMED_INDEX_ID = "bigframes_unnamed_index" -def get_axis_number(axis: typing.Union[str, int, None]) -> typing.Literal[0, 1]: - if axis in {0, "index", "rows", None}: +def get_axis_number(axis: typing.Union[str, int]) -> typing.Literal[0, 1]: + if axis in {0, "index", "rows"}: return 0 elif axis in {1, "columns"}: return 1 diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 31777f3face..d65d4ce344e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -455,7 +455,7 @@ def __getattr__(self, key: str): raise AttributeError(key) def __repr__(self) -> str: - """Converts a DataFrame to a string. Calls compute. + """Converts a DataFrame to a string. Calls to_pandas. Only represents the first `bigframes.options.display.max_rows`. """ @@ -532,13 +532,14 @@ def _apply_binop( other: float | int | bigframes.series.Series | DataFrame, op, axis: str | int = "columns", + how: str = "outer", ): if isinstance(other, (float, int)): return self._apply_scalar_binop(other, op) elif isinstance(other, bigframes.series.Series): - return self._apply_series_binop(other, op, axis=axis) + return self._apply_series_binop(other, op, axis=axis, how=how) elif isinstance(other, DataFrame): - return self._apply_dataframe_binop(other, op) + return self._apply_dataframe_binop(other, op, how=how) raise NotImplementedError( f"binary operation is not implemented on the second operand of type {type(other).__name__}." f"{constants.FEEDBACK_LINK}" @@ -559,6 +560,7 @@ def _apply_series_binop( other: bigframes.series.Series, op: ops.BinaryOp, axis: str | int = "columns", + how: str = "outer", ) -> DataFrame: if axis not in ("columns", "index", 0, 1): raise ValueError(f"Invalid input: axis {axis}.") @@ -569,7 +571,7 @@ def _apply_series_binop( ) joined_index, (get_column_left, get_column_right) = self._block.index.join( - other._block.index, how="outer" + other._block.index, how=how ) series_column_id = other._value.get_name() @@ -591,22 +593,27 @@ def _apply_series_binop( return DataFrame(block) def _apply_dataframe_binop( - self, - other: DataFrame, - op: ops.BinaryOp, + self, other: DataFrame, op: ops.BinaryOp, how: str = "outer" ) -> DataFrame: # Join rows joined_index, (get_column_left, get_column_right) = self._block.index.join( - other._block.index, how="outer" + other._block.index, how=how ) # join columns schema + # indexers will be none for exact match columns, lcol_indexer, rcol_indexer = self.columns.join( - other.columns, how="outer", return_indexers=True + other.columns, how=how, return_indexers=True ) binop_result_ids = [] block = joined_index._block - for left_index, right_index in zip(lcol_indexer, rcol_indexer): + + column_indices = zip( + lcol_indexer if (lcol_indexer is not None) else range(len(columns)), + rcol_indexer if (lcol_indexer is not None) else range(len(columns)), + ) + + for left_index, right_index in column_indices: if left_index >= 0 and right_index >= 0: # -1 indices indicate missing left_col_id = self._block.value_columns[left_index] right_col_id = other._block.value_columns[right_index] @@ -617,13 +624,19 @@ def _apply_dataframe_binop( ) binop_result_ids.append(result_col_id) elif left_index >= 0: - dtype = self.dtypes[left_index] - block, null_col_id = block.create_constant(None, dtype=dtype) - binop_result_ids.append(null_col_id) + left_col_id = self._block.value_columns[left_index] + block, result_col_id = block.apply_unary_op( + get_column_left(left_col_id), + ops.partial_right(op, None), + ) + binop_result_ids.append(result_col_id) elif right_index >= 0: - dtype = other.dtypes[right_index] - block, null_col_id = block.create_constant(None, dtype=dtype) - binop_result_ids.append(null_col_id) + right_col_id = other._block.value_columns[right_index] + block, result_col_id = block.apply_unary_op( + get_column_right(right_col_id), + ops.partial_left(op, None), + ) + binop_result_ids.append(result_col_id) else: # Should not be possible raise ValueError("No right or left index.") @@ -759,6 +772,75 @@ def rpow( __rpow__ = rpow + def combine( + self, + other: DataFrame, + func: typing.Callable[ + [bigframes.series.Series, bigframes.series.Series], bigframes.series.Series + ], + fill_value=None, + overwrite: bool = True, + ) -> DataFrame: + # Join rows + joined_index, (get_column_left, get_column_right) = self._block.index.join( + other._block.index, how="outer" + ) + columns, lcol_indexer, rcol_indexer = self.columns.join( + other.columns, how="outer", return_indexers=True + ) + + column_indices = zip( + lcol_indexer if (lcol_indexer is not None) else range(len(columns)), + rcol_indexer if (lcol_indexer is not None) else range(len(columns)), + ) + + block = joined_index._block + results = [] + for left_index, right_index in column_indices: + if left_index >= 0 and right_index >= 0: # -1 indices indicate missing + left_col_id = get_column_left(self._block.value_columns[left_index]) + right_col_id = get_column_right(other._block.value_columns[right_index]) + left_series = bigframes.series.Series(block.select_column(left_col_id)) + right_series = bigframes.series.Series( + block.select_column(right_col_id) + ) + if fill_value is not None: + left_series = left_series.fillna(fill_value) + right_series = right_series.fillna(fill_value) + results.append(func(left_series, right_series)) + elif left_index >= 0: + # Does not exist in other + if overwrite: + dtype = self.dtypes[left_index] + block, null_col_id = block.create_constant(None, dtype=dtype) + result = bigframes.series.Series(block.select_column(null_col_id)) + results.append(result) + else: + left_col_id = get_column_left(self._block.value_columns[left_index]) + result = bigframes.series.Series(block.select_column(left_col_id)) + if fill_value is not None: + result = result.fillna(fill_value) + results.append(result) + elif right_index >= 0: + right_col_id = get_column_right(other._block.value_columns[right_index]) + result = bigframes.series.Series(block.select_column(right_col_id)) + if fill_value is not None: + result = result.fillna(fill_value) + results.append(result) + else: + # Should not be possible + raise ValueError("No right or left index.") + + if all([isinstance(val, bigframes.series.Series) for val in results]): + import bigframes.core.reshape as rs + + return rs.concat(results, axis=1) + else: + raise ValueError("'func' must return Series") + + def combine_first(self, other: DataFrame): + return self._apply_dataframe_binop(other, ops.fillna_op) + def to_pandas( self, max_download_size: Optional[int] = None, @@ -810,6 +892,28 @@ def head(self, n: int = 5) -> DataFrame: def tail(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[-n:]) + def nlargest( + self, + n: int, + columns: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], + keep: str = "first", + ) -> DataFrame: + if keep not in ("first", "last", "all"): + raise ValueError("'keep must be one of 'first', 'last', or 'all'") + column_ids = self._sql_names(columns) + return DataFrame(block_ops.nlargest(self._block, n, column_ids, keep=keep)) + + def nsmallest( + self, + n: int, + columns: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], + keep: str = "first", + ) -> DataFrame: + if keep not in ("first", "last", "all"): + raise ValueError("'keep must be one of 'first', 'last', or 'all'") + column_ids = self._sql_names(columns) + return DataFrame(block_ops.nsmallest(self._block, n, column_ids, keep=keep)) + def drop( self, labels: typing.Any = None, @@ -852,13 +956,50 @@ def drop( raise ValueError("Must specify 'labels' or 'index'/'columns") return DataFrame(block) - def droplevel(self, level: LevelsType): - resolved_level_ids = self._resolve_levels(level) - return DataFrame(self._block.drop_levels(resolved_level_ids)) + def droplevel(self, level: LevelsType, axis: int | str = 0): + axis_n = utils.get_axis_number(axis) + if axis_n == 0: + resolved_level_ids = self._resolve_levels(level) + return DataFrame(self._block.drop_levels(resolved_level_ids)) + else: + if isinstance(self.columns, pandas.MultiIndex): + new_df = self.copy() + new_df.columns = self.columns.droplevel(level) + return new_df + else: + raise ValueError("Columns must be a multiindex to drop levels.") + + def swaplevel(self, i: int = -2, j: int = -1, axis: int | str = 0): + axis_n = utils.get_axis_number(axis) + if axis_n == 0: + level_i = self._block.index_columns[i] + level_j = self._block.index_columns[j] + mapping = {level_i: level_j, level_j: level_i} + reordering = [ + mapping.get(index_id, index_id) + for index_id in self._block.index_columns + ] + return DataFrame(self._block.reorder_levels(reordering)) + else: + if isinstance(self.columns, pandas.MultiIndex): + new_df = self.copy() + new_df.columns = self.columns.swaplevel(i, j) + return new_df + else: + raise ValueError("Columns must be a multiindex to reorder levels.") - def reorder_levels(self, order: LevelsType): - resolved_level_ids = self._resolve_levels(order) - return DataFrame(self._block.reorder_levels(resolved_level_ids)) + def reorder_levels(self, order: LevelsType, axis: int | str = 0): + axis_n = utils.get_axis_number(axis) + if axis_n == 0: + resolved_level_ids = self._resolve_levels(order) + return DataFrame(self._block.reorder_levels(resolved_level_ids)) + else: + if isinstance(self.columns, pandas.MultiIndex): + new_df = self.copy() + new_df.columns = self.columns.reorder_levels(order) + return new_df + else: + raise ValueError("Columns must be a multiindex to reorder levels.") def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: if utils.is_list_like(level): @@ -1096,8 +1237,177 @@ 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 filter( + self, + items: typing.Optional[typing.Iterable] = None, + like: typing.Optional[str] = None, + regex: typing.Optional[str] = None, + axis: int | str | None = None, + ) -> DataFrame: + if sum([(items is not None), (like is not None), (regex is not None)]) != 1: + raise ValueError( + "Need to provide exactly one of 'items', 'like', or 'regex'" + ) + axis_n = utils.get_axis_number(axis) if (axis is not None) else 1 + if axis_n == 0: # row labels + return self._filter_rows(items, like, regex) + else: # column labels + return self._filter_columns(items, like, regex) + + def _filter_rows( + self, + items: typing.Optional[typing.Iterable] = None, + like: typing.Optional[str] = None, + regex: typing.Optional[str] = None, + ) -> DataFrame: + if len(self._block.index_columns) > 1: + raise NotImplementedError( + "Method filter does not support rows multiindex. {constants.FEEDBACK_LINK}" + ) + if (like is not None) or (regex is not None): + block = self._block + block, label_string_id = block.apply_unary_op( + self._block.index_columns[0], + ops.AsTypeOp(pandas.StringDtype(storage="pyarrow")), + ) + if like is not None: + block, mask_id = block.apply_unary_op( + label_string_id, ops.ContainsStringOp(pat=like) + ) + else: # regex + assert regex is not None + block, mask_id = block.apply_unary_op( + label_string_id, ops.ContainsRegexOp(pat=regex) + ) + + block = block.filter(mask_id) + block = block.select_columns(self._block.value_columns) + return DataFrame(block) + elif items is not None: + # Behavior matches pandas 2.1+, older pandas versions would reindex + block = self._block + block, mask_id = block.apply_unary_op( + self._block.index_columns[0], ops.IsInOp(values=list(items)) + ) + block = block.filter(mask_id) + block = block.select_columns(self._block.value_columns) + return DataFrame(block) + else: + raise ValueError("Need to provide 'items', 'like', or 'regex'") + + def _filter_columns( + self, + items: typing.Optional[typing.Iterable] = None, + like: typing.Optional[str] = None, + regex: typing.Optional[str] = None, + ) -> DataFrame: + if (like is not None) or (regex is not None): + + def label_filter(label): + label_str = label if isinstance(label, str) else str(label) + if like: + return like in label_str + else: # regex + return re.match(regex, label_str) is not None + + cols = [ + col_id + for col_id, label in zip(self._block.value_columns, self.columns) + if label_filter(label) + ] + return DataFrame(self._block.select_columns(cols)) + if items is not None: + # Behavior matches pandas 2.1+, older pandas versions would reorder using order of items + new_columns = self.columns.intersection(pandas.Index(items)) + return self.reindex(columns=new_columns) + else: + raise ValueError("Need to provide 'items', 'like', or 'regex'") + + def reindex( + self, + labels=None, + *, + index=None, + columns=None, + axis: typing.Optional[typing.Union[str, int]] = None, + validate: typing.Optional[bool] = None, + ): + if labels: + if index or columns: + raise ValueError("Cannot specify both 'labels' and 'index'/'columns") + axis_n = utils.get_axis_number(axis) if (axis is not None) else 0 + if axis_n == 0: + index = labels + else: + columns = labels + if (index is not None) and (columns is not None): + return self._reindex_columns(columns)._reindex_rows( + index, validate=validate or False + ) + if index is not None: + return self._reindex_rows(index, validate=validate or False) + if columns is not None: + return self._reindex_columns(columns) + + def _reindex_rows( + self, + index, + *, + validate: typing.Optional[bool] = None, + ): + if validate and not self.index.is_unique: + raise ValueError("Original index must be unique to reindex") + keep_original_names = False + if isinstance(index, indexes.Index): + new_indexer = DataFrame(data=index._data._get_block())[[]] + else: + if not isinstance(index, pandas.Index): + keep_original_names = True + index = pandas.Index(index) + if index.nlevels != self.index.nlevels: + raise NotImplementedError( + "Cannot reindex with index with different nlevels" + ) + new_indexer = DataFrame(index=index)[[]] + # multiindex join is senstive to index names, so we will set all these + result = new_indexer.rename_axis(range(new_indexer.index.nlevels)).join( + self.rename_axis(range(self.index.nlevels)), + how="left", + ) + # and then reset the names after the join + return result.rename_axis( + self.index.names if keep_original_names else index.names + ) + + def _reindex_columns(self, columns): + block = self._block + new_column_index, indexer = self.columns.reindex(columns) + result_cols = [] + for label, index in zip(columns, indexer): + if index >= 0: + result_cols.append(self._block.value_columns[index]) + else: + block, null_col = block.create_constant( + pandas.NA, label, dtype=pandas.Float64Dtype() + ) + result_cols.append(null_col) + result_df = DataFrame(block.select_columns(result_cols)) + result_df.columns = new_column_index + return result_df + + def reindex_like(self, other: DataFrame, *, validate: typing.Optional[bool] = None): + return self.reindex(index=other.index, columns=other.columns, validate=validate) + def fillna(self, value=None) -> DataFrame: - return self._apply_binop(value, ops.fillna_op) + return self._apply_binop(value, ops.fillna_op, how="left") + + def ffill(self, *, limit: typing.Optional[int] = None) -> DataFrame: + window = bigframes.core.WindowSpec(preceding=limit, following=0) + return self._apply_window_op(agg_ops.LastNonNullOp(), window) + + def bfill(self, *, limit: typing.Optional[int] = None) -> DataFrame: + window = bigframes.core.WindowSpec(preceding=0, following=limit) + return self._apply_window_op(agg_ops.FirstNonNullOp(), window) def isin(self, values) -> DataFrame: if utils.is_dict_like(values): @@ -1309,6 +1619,14 @@ def describe(self) -> DataFrame: ) return typing.cast(DataFrame, result) + def skew(self, *, numeric_only: bool = False): + if not numeric_only: + frame = self._raise_on_non_numeric("skew") + else: + frame = self._drop_non_numeric() + result_block = block_ops.skew(frame._block, frame._block.value_columns) + return bigframes.series.Series(result_block) + def pivot( self, *, @@ -1702,17 +2020,29 @@ def shift(self, periods: int = 1) -> DataFrame: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) + def diff(self, periods: int = 1) -> DataFrame: + window = bigframes.core.WindowSpec( + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.DiffOp(periods), window) + + def pct_change(self, periods: int = 1) -> DataFrame: + # Future versions of pandas will not perfrom ffill automatically + df = self.ffill() + return DataFrame(block_ops.pct_change(df._block, periods=periods)) + def _apply_window_op( self, op: agg_ops.WindowOp, window_spec: bigframes.core.WindowSpec, ): - block = self._block.multi_apply_window_op( + block, result_ids = self._block.multi_apply_window_op( self._block.value_columns, op, window_spec=window_spec, ) - return DataFrame(block) + return DataFrame(block.select_columns(result_ids)) def sample( self, @@ -1875,6 +2205,98 @@ def to_parquet(self, path: str, *, index: bool = True) -> None: _, query_job = self._block.expr._session._start_query(export_data_statement) self._set_internal_query_job(query_job) + def to_dict( + self, + orient: Literal[ + "dict", "list", "series", "split", "tight", "records", "index" + ] = "dict", + into: type[dict] = dict, + **kwargs, + ) -> dict | list[dict]: + return self.to_pandas().to_dict(orient, into, **kwargs) # type: ignore + + def to_excel(self, excel_writer, sheet_name: str = "Sheet1", **kwargs) -> None: + return self.to_pandas().to_excel(excel_writer, sheet_name, **kwargs) + + def to_latex( + self, + buf=None, + columns: Sequence | None = None, + header: bool | Sequence[str] = True, + index: bool = True, + **kwargs, + ) -> str | None: + return self.to_pandas().to_latex( + buf, columns=columns, header=header, index=index, **kwargs # type: ignore + ) + + def to_records( + self, index: bool = True, column_dtypes=None, index_dtypes=None + ) -> numpy.recarray: + return self.to_pandas().to_records(index, column_dtypes, index_dtypes) + + def to_string( + self, + buf=None, + columns: Sequence[str] | None = None, + col_space=None, + header: bool | Sequence[str] = True, + index: bool = True, + na_rep: str = "NaN", + formatters=None, + float_format=None, + sparsify: bool | None = None, + index_names: bool = True, + justify: str | None = None, + max_rows: int | None = None, + max_cols: int | None = None, + show_dimensions: bool = False, + decimal: str = ".", + line_width: int | None = None, + min_rows: int | None = None, + max_colwidth: int | None = None, + encoding: str | None = None, + ) -> str | None: + return self.to_pandas().to_string( + buf, + columns, # type: ignore + col_space, + header, # type: ignore + index, + na_rep, + formatters, + float_format, + sparsify, + index_names, + justify, + max_rows, + max_cols, + show_dimensions, + decimal, + line_width, + min_rows, + max_colwidth, + encoding, + ) + + def to_markdown( + self, + buf=None, + mode: str = "wt", + index: bool = True, + **kwargs, + ) -> str | None: + return self.to_pandas().to_markdown(buf, mode, index, **kwargs) # type: ignore + + def to_pickle(self, path, **kwargs) -> None: + return self.to_pandas().to_pickle(path, **kwargs) + + def to_orc(self, path=None, **kwargs) -> bytes | None: + as_pandas = self.to_pandas() + # to_orc only works with default index + as_pandas_default_index = as_pandas.reset_index() + return as_pandas_default_index.to_orc(path, **kwargs) + def _apply_unary_op(self, operation: ops.UnaryOp) -> DataFrame: block = self._block.multi_apply_unary_op(self._block.value_columns, operation) return DataFrame(block) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 95cf737b2e7..af3209b0e1c 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -157,7 +157,7 @@ def ibis_dtype_to_bigframes_dtype( return IBIS_TO_BIGFRAMES[ibis_dtype] else: raise ValueError( - f"Unexpected Ibis data type {type(ibis_dtype)}. {constants.FEEDBACK_LINK}" + f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" ) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 9f9d9f85d0e..f07274f8fcb 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -133,7 +133,7 @@ class TrainablePredictor(Predictor): Also the predictor can be attached to a pipeline with transformers.""" @abc.abstractmethod - def fit(self, X, y, transforms): + def _fit(self, X, y, transforms=None): pass @abc.abstractmethod @@ -146,6 +146,36 @@ def to_gbq(self, model_name, replace): pass +class SupervisedTrainablePredictor(TrainablePredictor): + """A BigQuery DataFrames ML Supervised Model base class that can be used to fit and predict outputs. + + Need to provide both X and y in supervised tasks.""" + + _T = TypeVar("_T", bound="SupervisedTrainablePredictor") + + def fit( + self: _T, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + ) -> _T: + return self._fit(X, y) + + +class UnsupervisedTrainablePredictor(TrainablePredictor): + """A BigQuery DataFrames ML Unsupervised Model base class that can be used to fit and predict outputs. + + Only need to provide both X (y is optional and ignored) in unsupervised tasks.""" + + _T = TypeVar("_T", bound="UnsupervisedTrainablePredictor") + + def fit( + self: _T, + X: Union[bpd.DataFrame, bpd.Series], + y: Optional[Union[bpd.DataFrame, bpd.Series]] = None, + ) -> _T: + return self._fit(X, y) + + class Transformer(BaseEstimator): """A BigQuery DataFrames Transformer base class that transforms data. diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 2501d2b21f1..14cce2879e9 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -28,13 +28,13 @@ class KMeans( + base.UnsupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans, - base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans.__doc__ - def __init__(self, n_clusters=8): + def __init__(self, n_clusters: int = 8): self.n_clusters = n_clusters self._bqml_model: Optional[core.BqmlModel] = None @@ -58,7 +58,7 @@ def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: """The model options as they will be set for BQML""" return {"model_type": "KMEANS", "num_clusters": self.n_clusters} - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y=None, # ignored diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index df01303ffac..a3d3503ad0a 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -33,8 +33,8 @@ class ColumnTransformer( - third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, base.Transformer, + third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, ): __doc__ = ( third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer.__doc__ diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 75b57f2e54f..0cfe3b3ddf9 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -28,12 +28,12 @@ class PCA( + base.UnsupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.decomposition._pca.PCA, - base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.sklearn.decomposition._pca.PCA.__doc__ - def __init__(self, n_components=3): + def __init__(self, n_components: int = 3): self.n_components = n_components self._bqml_model: Optional[core.BqmlModel] = None @@ -52,7 +52,7 @@ def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> PCA: new_pca._bqml_model = core.BqmlModel(session, model) return new_pca - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y=None, diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 56a0cc3d949..142edaa00f6 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -48,8 +48,8 @@ class XGBRegressor( + base.SupervisedTrainablePredictor, third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor, - base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor.__doc__ @@ -57,22 +57,22 @@ def __init__( self, num_parallel_tree: int = 1, booster: Literal["gbtree", "dart"] = "gbtree", - dart_normalized_type: Literal["TREE", "FOREST"] = "TREE", + dart_normalized_type: Literal["tree", "forest"] = "tree", tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", min_tree_child_weight: int = 1, - colsample_bytree=1.0, - colsample_bylevel=1.0, - colsample_bynode=1.0, - gamma=0.0, + colsample_bytree: float = 1.0, + colsample_bylevel: float = 1.0, + colsample_bynode: float = 1.0, + gamma: float = 0.0, max_depth: int = 6, - subsample=1.0, - reg_alpha=0.0, - reg_lambda=1.0, - early_stop=True, - learning_rate=0.3, + subsample: float = 1.0, + reg_alpha: float = 0.0, + reg_lambda: float = 1.0, + early_stop: float = True, + learning_rate: float = 0.3, max_iterations: int = 20, - min_rel_progress=0.01, - enable_global_explain=False, + min_rel_progress: float = 0.01, + enable_global_explain: bool = False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): self.num_parallel_tree = num_parallel_tree @@ -143,7 +143,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "xgboost_version": self.xgboost_version, } - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y: Union[bpd.DataFrame, bpd.Series], @@ -211,8 +211,8 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: class XGBClassifier( + base.SupervisedTrainablePredictor, third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier, - base.TrainablePredictor, ): __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier.__doc__ @@ -221,22 +221,22 @@ def __init__( self, num_parallel_tree: int = 1, booster: Literal["gbtree", "dart"] = "gbtree", - dart_normalized_type: Literal["TREE", "FOREST"] = "TREE", + dart_normalized_type: Literal["tree", "forest"] = "tree", tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", min_tree_child_weight: int = 1, - colsample_bytree=1.0, - colsample_bylevel=1.0, - colsample_bynode=1.0, - gamma=0.0, + colsample_bytree: float = 1.0, + colsample_bylevel: float = 1.0, + colsample_bynode: float = 1.0, + gamma: float = 0.0, max_depth: int = 6, - subsample=1.0, - reg_alpha=0.0, - reg_lambda=1.0, - early_stop=True, - learning_rate=0.3, + subsample: float = 1.0, + reg_alpha: float = 0.0, + reg_lambda: float = 1.0, + early_stop: bool = True, + learning_rate: float = 0.3, max_iterations: int = 20, - min_rel_progress=0.01, - enable_global_explain=False, + min_rel_progress: float = 0.01, + enable_global_explain: bool = False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): self.num_parallel_tree = num_parallel_tree @@ -307,7 +307,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "xgboost_version": self.xgboost_version, } - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y: Union[bpd.DataFrame, bpd.Series], @@ -374,8 +374,8 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: class RandomForestRegressor( + base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor, - base.TrainablePredictor, ): __doc__ = ( @@ -461,7 +461,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "xgboost_version": self.xgboost_version, } - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y: Union[bpd.DataFrame, bpd.Series], @@ -542,8 +542,8 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegresso class RandomForestClassifier( + base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier, - base.TrainablePredictor, ): __doc__ = ( @@ -629,7 +629,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "xgboost_version": self.xgboost_version, } - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y: Union[bpd.DataFrame, bpd.Series], diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index b7e0553ecb7..22d81294fc3 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -27,7 +27,7 @@ _PREDICT_OUTPUT_COLUMNS = ["forecast_timestamp", "forecast_value"] -class ARIMAPlus(base.TrainablePredictor): +class ARIMAPlus(base.SupervisedTrainablePredictor): """Time Series ARIMA Plus model.""" def __init__(self): @@ -48,7 +48,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: """The model options as they will be set for BQML.""" return {"model_type": "ARIMA_PLUS"} - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y: Union[bpd.DataFrame, bpd.Series], diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 0b18db93154..1606a15d730 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -30,8 +30,8 @@ class LinearRegression( + base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression, - base.TrainablePredictor, ): __doc__ = ( third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression.__doc__ @@ -39,7 +39,7 @@ class LinearRegression( def __init__( self, - fit_intercept=True, + fit_intercept: bool = True, ): self.fit_intercept = fit_intercept self._bqml_model: Optional[core.BqmlModel] = None @@ -71,7 +71,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "fit_intercept": self.fit_intercept, } - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y: Union[bpd.DataFrame, bpd.Series], @@ -136,8 +136,8 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: class LogisticRegression( + base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression, - base.TrainablePredictor, ): __doc__ = ( third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression.__doc__ @@ -189,12 +189,13 @@ def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: # "class_weights": self.class_weights, } - def fit( + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], y: Union[bpd.DataFrame, bpd.Series], transforms: Optional[List[str]] = None, ) -> LogisticRegression: + """Fit model with transforms.""" X, y = utils.convert_to_dataframe(X, y) self._bqml_model = core.create_bqml_model( diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 345e3deb728..973fbf2ad96 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -100,26 +100,26 @@ def predict( Temperature controls the degree of randomness in token selection. Lower temperatures are good for prompts that expect a true or correct response, while higher temperatures can lead to more diverse or unexpected results. A temperature of 0 is deterministic: the highest probability token is always selected. For most use cases, try starting with a temperature of 0.2. - Default 0. + Default 0. Possible values [0.0, 1.0]. max_output_tokens (int, default 128): Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. A token may be smaller than a word. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. - Default 128. + Default 128. Possible values [1, 1024]. top_k (int, default 40): Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model’s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature). For each token selection step, the top K tokens with the highest probabilities are sampled. Then tokens are further filtered based on topP with the final token selected using temperature sampling. Specify a lower value for less random responses and a higher value for more random responses. - Default 40. + Default 40. Possible values [1, 40]. top_p (float, default 0.95):: Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value. For example, if tokens A, B, and C have a probability of 0.3, 0.2, and 0.1 and the top-p value is 0.5, then the model will select either A or B as the next token (using temperature) and not consider C at all. Specify a lower value for less random responses and a higher value for more random responses. - Default 0.95. + Default 0.95. Possible values [0.0, 1.0]. Returns: diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index bfd03925265..bff0bf36ad4 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -24,14 +24,14 @@ import bigframes import bigframes.constants as constants -from bigframes.ml import base, compose, loader, preprocessing, utils +from bigframes.ml import base, compose, forecasting, loader, preprocessing, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.pipeline class Pipeline( - third_party.bigframes_vendored.sklearn.pipeline.Pipeline, base.BaseEstimator, + third_party.bigframes_vendored.sklearn.pipeline.Pipeline, ): __doc__ = third_party.bigframes_vendored.sklearn.pipeline.Pipeline.__doc__ @@ -55,7 +55,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): self._transform = transform else: raise NotImplementedError( - f"Transform {transform} is not yet supported by Pipeline. {constants.FEEDBACK_LINK}" + f"Transformer type {type(transform)} is not yet supported by Pipeline. {constants.FEEDBACK_LINK}" ) if not isinstance( @@ -63,7 +63,13 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): base.TrainablePredictor, ): raise NotImplementedError( - f"Estimator {estimator} is not supported by Pipeline. {constants.FEEDBACK_LINK}" + f"Estimator type {type(estimator)} is not supported by Pipeline. {constants.FEEDBACK_LINK}" + ) + + # BQML doesn't support ARIMA_PLUS with transformers. b/298676367 + if isinstance(estimator, forecasting.ARIMAPlus): + raise NotImplementedError( + f"Estimator type {type(estimator)} is not supported by Pipeline. {constants.FEEDBACK_LINK}" ) self._transform = transform @@ -92,7 +98,7 @@ def fit( (y,) = utils.convert_to_dataframe(y) transform_sqls.extend(y.columns.tolist()) - self._estimator.fit(X=X, y=y, transforms=transform_sqls) + self._estimator._fit(X=X, y=y, transforms=transform_sqls) return self def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index ee46a370520..8add7bdd767 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -29,8 +29,8 @@ class StandardScaler( - third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler, base.Transformer, + third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler, ): __doc__ = ( third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler.__doc__ @@ -105,8 +105,8 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: class OneHotEncoder( - third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, base.Transformer, + third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, ): # BQML max value https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder#syntax TOP_K_DEFAULT = 1000000 diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 874c264194c..23271e82205 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -19,8 +19,10 @@ import ibis import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types +from pandas import Int64Dtype import bigframes.constants as constants +import bigframes.dtypes as dtypes import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops @@ -202,6 +204,16 @@ def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: ) +class PopVarOp(AggregateOp): + name = "popvar" + + @numeric_op + def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present( + typing.cast(ibis_types.NumericColumn, x).var(how="pop"), window + ) + + class CountOp(AggregateOp): name = "count" @@ -217,16 +229,20 @@ def skips_nulls(self): class CutOp(WindowOp): def __init__(self, bins: int): - self._bins = bins + self._bins_ibis = dtypes.literal_to_ibis_scalar(bins, force_dtype=Int64Dtype()) + self._bins_int = bins def _as_ibis(self, x: ibis_types.Column, window=None): col_min = _apply_window_if_present(x.min(), window) col_max = _apply_window_if_present(x.max(), window) - bin_width = (col_max - col_min) / self._bins + bin_width = (col_max - col_min) / self._bins_ibis out = ibis.case() - for bin in range(self._bins - 1): - out = out.when(x <= (col_min + (bin + 1) * bin_width), bin) - out = out.when(x.notnull(), self._bins - 1) + for this_bin in range(self._bins_int - 1): + out = out.when( + x <= (col_min + (this_bin + 1) * bin_width), + dtypes.literal_to_ibis_scalar(this_bin, force_dtype=Int64Dtype()), + ) + out = out.when(x.notnull(), self._bins_ibis - 1) return out.end() @property @@ -305,6 +321,28 @@ def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(column.first(), window) +class FirstNonNullOp(WindowOp): + @property + def skips_nulls(self): + return False + + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present( + vendored_ibis_ops.FirstNonNullValue(column).to_expr(), window # type: ignore + ) + + +class LastNonNullOp(WindowOp): + @property + def skips_nulls(self): + return False + + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present( + vendored_ibis_ops.LastNonNullValue(column).to_expr(), window # type: ignore + ) + + class ShiftOp(WindowOp): def __init__(self, periods: int): self._periods = periods @@ -321,6 +359,28 @@ def skips_nulls(self): return False +class DiffOp(WindowOp): + def __init__(self, periods: int): + self._periods = periods + + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + shifted = ShiftOp(self._periods)._as_ibis(column, window) + if column.type().is_boolean(): + return typing.cast(ibis_types.BooleanColumn, column) != typing.cast( + ibis_types.BooleanColumn, shifted + ) + elif column.type().is_numeric(): + return typing.cast(ibis_types.NumericColumn, column) - typing.cast( + ibis_types.NumericColumn, shifted + ) + else: + raise TypeError(f"Cannot perform diff on type{column.type()}") + + @property + def skips_nulls(self): + return False + + class AllOp(AggregateOp): def _as_ibis( self, column: ibis_types.Column, window=None diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index e1a23e67a1c..cb278345907 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -399,6 +399,7 @@ def remote_function( dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, reuse: bool = True, + name: Optional[str] = None, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -407,6 +408,7 @@ def remote_function( dataset=dataset, bigquery_connection=bigquery_connection, reuse=reuse, + name=name, ) diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 6932e5b5805..6fc2f8e59fb 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -28,6 +28,8 @@ import textwrap from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING +import requests + if TYPE_CHECKING: from bigframes.session import Session @@ -99,7 +101,7 @@ def get_remote_function_locations(bq_location): def _get_hash(def_): - "Get hash of a function." + "Get hash (32 digits alphanumeric) of a function." def_repr = cloudpickle.dumps(def_, protocol=_pickle_protocol_version) return hashlib.md5(def_repr).hexdigest() @@ -128,7 +130,7 @@ class IbisSignature(NamedTuple): def get_cloud_function_name(def_, uniq_suffix=None): - """Get the name of the cloud function.""" + "Get a name for the cloud function for the given user defined function." cf_name = _get_hash(def_) cf_name = f"bigframes-{cf_name}" # for identification if uniq_suffix: @@ -137,7 +139,7 @@ def get_cloud_function_name(def_, uniq_suffix=None): def get_remote_function_name(def_, uniq_suffix=None): - """Get the name for the BQ remote function.""" + "Get a name for the BQ remote function for the given user defined function." bq_rf_name = _get_hash(def_) bq_rf_name = f"bigframes_{bq_rf_name}" # for identification if uniq_suffix: @@ -206,9 +208,15 @@ def create_bq_remote_function( query_job.result() # Wait for the job to complete. logger.info(f"Created remote function {query_job.ddl_target_routine}") + def get_cloud_function_fully_qualified_parent(self): + "Get the fully qualilfied parent for a cloud function." + return self._cloud_functions_client.common_location_path( + self._gcp_project_id, self._cloud_function_region + ) + def get_cloud_function_fully_qualified_name(self, name): "Get the fully qualilfied name for a cloud function." - return "projects/{}/locations/{}/functions/{}".format( + return self._cloud_functions_client.function_path( self._gcp_project_id, self._cloud_function_region, name ) @@ -319,6 +327,7 @@ def create_cloud_function(self, def_, cf_name): # Build and deploy folder structure containing cloud function with tempfile.TemporaryDirectory() as dir: entry_point = self.generate_cloud_function_code(def_, dir) + archive_path = shutil.make_archive(dir, "zip", dir) # We are creating cloud function source code from the currently running # python version. Use the same version to deploy. This is necessary @@ -331,50 +340,56 @@ def create_cloud_function(self, def_, cf_name): sys.version_info.major, sys.version_info.minor ) - # deploy/redeploy the cloud function - # TODO(shobs): Figure out a way to skip this step if a cloud function - # already exists with the same name and source code - command = ( - "gcloud functions deploy" - + f" {cf_name} --gen2" - + f" --runtime={python_version}" - + f" --project={self._gcp_project_id}" - + f" --region={self._cloud_function_region}" - + f" --source={dir}" - + f" --entry-point={entry_point}" - + " --trigger-http" + # Determine an upload URL for user code + upload_url_request = functions_v2.GenerateUploadUrlRequest() + upload_url_request.parent = self.get_cloud_function_fully_qualified_parent() + upload_url_response = self._cloud_functions_client.generate_upload_url( + request=upload_url_request ) - # If the cloud function is being created for the first time, then let's - # make it not allow unauthenticated calls. If it was previously created - # then this invocation will update it, in which case do not touch that - # aspect and let the previous policy hold. The reason we do this is to - # avoid an IAM permission needed to update the invocation policy. - # For example, when a cloud function is being created for the first - # time, i.e. - # $ gcloud functions deploy python-foo-http --gen2 --runtime=python310 - # --region=us-central1 - # --source=/source/code/dir - # --entry-point=foo_http - # --trigger-http - # --no-allow-unauthenticated - # It works. When an invocation of the same command is done for the - # second time, it may run into an error like: - # ERROR: (gcloud.functions.deploy) PERMISSION_DENIED: Permission - # 'run.services.setIamPolicy' denied on resource - # 'projects/my_project/locations/us-central1/services/python-foo-http' (or resource may not exist) - # But when --no-allow-unauthenticated is omitted then it goes through. - # It suggests that in the second invocation the command is trying to set - # the IAM policy of the service, and the user running BigQuery - # DataFrame may not have privilege to do so, so better avoid this - # if we can. - if self.get_cloud_function_endpoint(cf_name): - logger.info(f"Updating existing cloud function: {command}") - else: - command = f"{command} --no-allow-unauthenticated" - logger.info(f"Creating new cloud function: {command}") - - _run_system_command(command) + # Upload the code to GCS + with open(archive_path, "rb") as f: + response = requests.put( + upload_url_response.upload_url, + data=f, + headers={"content-type": "application/zip"}, + ) + if response.status_code != 200: + raise RuntimeError( + "Failed to upload user code. code={}, reason={}, text={}".format( + response.status_code, response.reason, response.text + ) + ) + + # Deploy Cloud Function + create_function_request = functions_v2.CreateFunctionRequest() + create_function_request.parent = ( + self.get_cloud_function_fully_qualified_parent() + ) + create_function_request.function_id = cf_name + function = functions_v2.Function() + function.name = self.get_cloud_function_fully_qualified_name(cf_name) + function.build_config = functions_v2.BuildConfig() + function.build_config.runtime = python_version + function.build_config.entry_point = entry_point + function.build_config.source = functions_v2.Source() + function.build_config.source.storage_source = functions_v2.StorageSource() + function.build_config.source.storage_source.bucket = ( + upload_url_response.storage_source.bucket + ) + function.build_config.source.storage_source.object_ = ( + upload_url_response.storage_source.object_ + ) + create_function_request.function = function + + # Create the cloud function and wait for it to be ready to use + operation = self._cloud_functions_client.create_function( + request=create_function_request + ) + operation.result() + + # Cleanup + os.remove(archive_path) # Fetch the endpoint of the just created function endpoint = self.get_cloud_function_endpoint(cf_name) @@ -389,23 +404,47 @@ def create_cloud_function(self, def_, cf_name): return endpoint def provision_bq_remote_function( - self, def_, input_types, output_type, uniq_suffix=None + self, + def_, + input_types, + output_type, + reuse, + name, ): """Provision a BigQuery remote function.""" - # Derive the name of the underlying cloud function and first create - # it if it does not exist + # If reuse of any existing function with the same name (indicated by the + # same hash of its source code) is not intended, then attach a unique + # suffix to the intended function name to make it unique. + uniq_suffix = None + if not reuse: + uniq_suffix = "".join( + random.choices(string.ascii_lowercase + string.digits, k=8) + ) + + # Derive the name of the cloud function underlying the intended BQ + # remote function cloud_function_name = get_cloud_function_name(def_, uniq_suffix) cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) + + # Create the cloud function if it does not exist if not cf_endpoint: - self.check_cloud_function_tools_and_permissions() cf_endpoint = self.create_cloud_function(def_, cloud_function_name) else: logger.info(f"Cloud function {cloud_function_name} already exists.") - # Derive the name of the remote function and create/replace it if needed - remote_function_name = get_remote_function_name(def_, uniq_suffix) + # Derive the name of the remote function + remote_function_name = name + if not remote_function_name: + remote_function_name = get_remote_function_name(def_, uniq_suffix) rf_endpoint, rf_conn = self.get_remote_function_specs(remote_function_name) - if rf_endpoint != cf_endpoint or rf_conn != self._bq_connection_id: + + # Create the BQ remote function in following circumstances: + # 1. It does not exist + # 2. It exists but the existing remote function has different + # configuration than intended + if not rf_endpoint or ( + rf_endpoint != cf_endpoint or rf_conn != self._bq_connection_id + ): input_args = inspect.getargs(def_.__code__).args if len(input_args) != len(input_types): raise ValueError( @@ -439,27 +478,6 @@ def get_remote_function_specs(self, remote_function_name): break return (http_endpoint, bq_connection) - def check_cloud_function_tools_and_permissions(self): - """Check if the necessary tools and permissions are in place for creating remote function""" - # gcloud CLI comes with bq CLI and they are required for creating google - # cloud function and BigQuery remote function respectively - if not shutil.which("gcloud"): - raise ValueError( - "gcloud tool not installed, install it from https://cloud.google.com/sdk/docs/install. " - f"{constants.FEEDBACK_LINK}" - ) - - # TODO(shobs): Check for permissions too - # I (shobs) tried the following method - # $ gcloud asset search-all-iam-policies \ - # --format=json \ - # --scope=projects/{gcp_project_id} \ - # --query='policy.role.permissions:cloudfunctions.functions.create' - # as a proxy to all the privilges necessary to create cloud function - # https://cloud.google.com/functions/docs/reference/iam/roles#cloudfunctions.developer - # but that itself required the runner to have the permission to enable - # `cloudasset.googleapis.com` - def remote_function_node( routine_ref: bigquery.RoutineReference, ibis_signature: IbisSignature @@ -583,6 +601,7 @@ def remote_function( dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, reuse: bool = True, + name: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -613,7 +632,7 @@ def remote_function( * BigQuery Data Editor (roles/bigquery.dataEditor) * BigQuery Connection Admin (roles/bigquery.connectionAdmin) * Cloud Functions Developer (roles/cloudfunctions.developer) - * Service Account User (roles/iam.serviceAccountUser) + * Service Account User (roles/iam.serviceAccountUser) on the service account `PROJECT_NUMBER-compute@developer.gserviceaccount.com` * Storage Object Viewer (roles/storage.objectViewer) * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) @@ -664,10 +683,16 @@ def remote_function( reuse (bool, Optional): Reuse the remote function if is already exists. `True` by default, which results in reusing an existing remote - function (if any) that was previously created for the same udf. - Setting it to false forces the creation of creating a unique remote function. + function and corresponding cloud function (if any) that was + previously created for the same udf. + Setting it to `False` forces the creation of a unique remote function. If the required remote function does not exist then it would be created irrespective of this param. + name (str, Optional): + Explicit name of the persisted BigQuery remote function. Use it with + caution, because two users working in the same project and dataset + could overwrite each other's remote functions if they use the same + persistent name. """ @@ -739,12 +764,6 @@ def remote_function( f"{constants.FEEDBACK_LINK}" ) - uniq_suffix = None - if not reuse: - uniq_suffix = "".join( - random.choices(string.ascii_lowercase + string.digits, k=8) - ) - # Check connection_id with `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID` format. if bigquery_connection.count(".") == 1: bq_connection_location, bq_connection_id = bigquery_connection.split(".") @@ -792,8 +811,13 @@ def wrapper(f): bigquery_connection, resource_manager_client, ) + rf_name, cf_name = remote_function_client.provision_bq_remote_function( - f, ibis_signature.input_types, ibis_signature.output_type, uniq_suffix + f, + ibis_signature.input_types, + ibis_signature.output_type, + reuse, + name, ) node = remote_function_node(dataset_ref.routine(rf_name), ibis_signature) diff --git a/bigframes/series.py b/bigframes/series.py index 8eadee37ed0..12e72c58b61 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -42,6 +42,7 @@ STABLE_SORTS, ) import bigframes.core.scalar as scalars +import bigframes.core.utils as utils import bigframes.core.window import bigframes.dataframe import bigframes.dtypes @@ -310,11 +311,20 @@ def drop( block = block.drop_columns([condition_id]) return Series(block.select_column(self._value_column)) - def droplevel(self, level: LevelsType): + def droplevel(self, level: LevelsType, axis: int | str = 0): resolved_level_ids = self._resolve_levels(level) return Series(self._block.drop_levels(resolved_level_ids)) - def reorder_levels(self, order: LevelsType): + def swaplevel(self, i: int = -2, j: int = -1): + level_i = self._block.index_columns[i] + level_j = self._block.index_columns[j] + mapping = {level_i: level_j, level_j: level_i} + reordering = [ + mapping.get(index_id, index_id) for index_id in self._block.index_columns + ] + return Series(self._block.reorder_levels(reordering)) + + def reorder_levels(self, order: LevelsType, axis: int | str = 0): resolved_level_ids = self._resolve_levels(order) return Series(self._block.reorder_levels(resolved_level_ids)) @@ -352,6 +362,14 @@ def cumsum(self) -> Series: agg_ops.sum_op, bigframes.core.WindowSpec(following=0) ) + def ffill(self, *, limit: typing.Optional[int] = None) -> Series: + window = bigframes.core.WindowSpec(preceding=limit, following=0) + return self._apply_window_op(agg_ops.LastNonNullOp(), window) + + def bfill(self, *, limit: typing.Optional[int] = None) -> Series: + window = bigframes.core.WindowSpec(preceding=0, following=limit) + return self._apply_window_op(agg_ops.FirstNonNullOp(), window) + def cummax(self) -> Series: return self._apply_window_op( agg_ops.max_op, bigframes.core.WindowSpec(following=0) @@ -375,7 +393,16 @@ def shift(self, periods: int = 1) -> Series: return self._apply_window_op(agg_ops.ShiftOp(periods), window) def diff(self, periods: int = 1) -> Series: - return self - self.shift(periods=periods) + window = bigframes.core.WindowSpec( + preceding=periods if periods > 0 else None, + following=-periods if periods < 0 else None, + ) + return self._apply_window_op(agg_ops.DiffOp(periods), window) + + def pct_change(self, periods: int = 1) -> Series: + # Future versions of pandas will not perfrom ffill automatically + series = self.ffill() + return Series(block_ops.pct_change(series._block, periods=periods)) def rank( self, @@ -390,6 +417,47 @@ def rank( def fillna(self, value=None) -> Series: return self._apply_binary_op(value, ops.fillna_op) + def replace( + self, to_replace: typing.Any, value: typing.Any = None, *, regex: bool = False + ): + if regex: + if not (isinstance(to_replace, str) and isinstance(value, str)): + raise NotImplementedError( + f"replace regex mode only supports strings for 'to_replace' and 'value'. {constants.FEEDBACK_LINK}" + ) + block, result_col = self._block.apply_unary_op( + self._value_column, + ops.ReplaceRegexOp(to_replace, value), + result_label=self.name, + ) + return Series(block.select_column(result_col)) + elif utils.is_dict_like(to_replace): + raise NotImplementedError( + f"Dict 'to_replace' not supported. {constants.FEEDBACK_LINK}" + ) + elif utils.is_list_like(to_replace): + block, cond = self._block.apply_unary_op( + self._value_column, ops.IsInOp(to_replace) + ) + block, result_col = block.apply_binary_op( + cond, + self._value_column, + ops.partial_arg1(ops.where_op, value), + result_label=self.name, + ) + return Series(block.select_column(result_col)) + else: # Scalar + block, cond = self._block.apply_unary_op( + self._value_column, ops.BinopPartialLeft(ops.eq_op, to_replace) + ) + block, result_col = block.apply_binary_op( + cond, + self._value_column, + ops.partial_arg1(ops.where_op, value), + result_label=self.name, + ) + return Series(block.select_column(result_col)) + def dropna( self, *, @@ -414,52 +482,16 @@ def tail(self, n: int = 5) -> Series: def nlargest(self, n: int = 5, keep: str = "first") -> Series: if keep not in ("first", "last", "all"): raise ValueError("'keep must be one of 'first', 'last', or 'all'") - block = self._block - if keep == "last": - block = block.reversed() - ordering = ( - OrderingColumnReference( - self._value_column, direction=OrderingDirection.DESC - ), + return Series( + block_ops.nlargest(self._block, n, [self._value_column], keep=keep) ) - block = block.order_by(ordering, stable=True) - if keep in ("first", "last"): - return Series(block.slice(0, n)) - else: # keep == "all": - block, counter = block.apply_window_op( - self._value_column, - agg_ops.rank_op, - window_spec=WindowSpec(ordering=ordering), - ) - block, condition = block.apply_unary_op( - counter, ops.partial_right(ops.le_op, n) - ) - block = block.filter(condition) - block = block.select_column(self._value_column) - return Series(block) def nsmallest(self, n: int = 5, keep: str = "first") -> Series: if keep not in ("first", "last", "all"): raise ValueError("'keep must be one of 'first', 'last', or 'all'") - block = self._block - if keep == "last": - block = block.reversed() - ordering = (OrderingColumnReference(self._value_column),) - block = block.order_by(ordering, stable=True) - if keep in ("first", "last"): - return Series(block.slice(0, n)) - else: # keep == "all": - block, counter = block.apply_window_op( - self._value_column, - agg_ops.rank_op, - window_spec=WindowSpec(ordering=ordering), - ) - block, condition = block.apply_unary_op( - counter, ops.partial_right(ops.le_op, n) - ) - block = block.filter(condition) - block = block.select_column(self._value_column) - return Series(block) + return Series( + block_ops.nsmallest(self._block, n, [self._value_column], keep=keep) + ) def isin(self, values) -> "Series" | None: if not _is_list_like(values): @@ -697,13 +729,9 @@ def _central_moment(self, n: int) -> float: """Useful helper for calculating central moment statistics""" # Nth central moment is mean((x-mean(x))^n) # See: https://en.wikipedia.org/wiki/Moment_(mathematics) - mean = self.mean() - mean_deltas = self - mean - delta_power = mean_deltas - # TODO(tbergeron): Replace with pow once implemented - for i in range(1, n): - delta_power = delta_power * mean_deltas - return delta_power.mean() + mean_deltas = self - self.mean() + delta_powers = mean_deltas**n + return delta_powers.mean() def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: if _is_list_like(func): @@ -1096,6 +1124,85 @@ 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 filter( + self, + items: typing.Optional[typing.Iterable] = None, + like: typing.Optional[str] = None, + regex: typing.Optional[str] = None, + axis: typing.Optional[typing.Union[str, int]] = None, + ) -> Series: + if (axis is not None) and utils.get_axis_number(axis) != 0: + raise ValueError(f"Invalid axis for series: {axis}") + if sum([(items is not None), (like is not None), (regex is not None)]) != 1: + raise ValueError( + "Need to provide exactly one of 'items', 'like', or 'regex'" + ) + if len(self._block.index_columns) > 1: + raise NotImplementedError( + "Method filter does not support rows multiindex. {constants.FEEDBACK_LINK}" + ) + if (like is not None) or (regex is not None): + block = self._block + block, label_string_id = block.apply_unary_op( + self._block.index_columns[0], + ops.AsTypeOp(pandas.StringDtype(storage="pyarrow")), + ) + if like is not None: + block, mask_id = block.apply_unary_op( + label_string_id, ops.ContainsStringOp(pat=like) + ) + else: # regex + assert regex is not None + block, mask_id = block.apply_unary_op( + label_string_id, ops.ContainsRegexOp(pat=regex) + ) + + block = block.filter(mask_id) + block = block.select_columns([self._value_column]) + return Series(block) + elif items is not None: + # Behavior matches pandas 2.1+, older pandas versions would reindex + block = self._block + block, mask_id = block.apply_unary_op( + self._block.index_columns[0], ops.IsInOp(values=list(items)) + ) + block = block.filter(mask_id) + block = block.select_columns([self._value_column]) + return Series(block) + else: + raise ValueError("Need to provide 'items', 'like', or 'regex'") + + def reindex(self, index=None, *, validate: typing.Optional[bool] = None): + if validate and not self.index.is_unique: + raise ValueError("Original index must be unique to reindex") + keep_original_names = False + if isinstance(index, indexes.Index): + new_indexer = bigframes.dataframe.DataFrame(data=index._data._get_block())[ + [] + ] + else: + if not isinstance(index, pandas.Index): + keep_original_names = True + index = pandas.Index(index) + if index.nlevels != self.index.nlevels: + raise NotImplementedError( + "Cannot reindex with index with different nlevels" + ) + new_indexer = bigframes.dataframe.DataFrame(index=index)[[]] + # multiindex join is senstive to index names, so we will set all these + result = new_indexer.rename_axis(range(new_indexer.index.nlevels)).join( + self.to_frame().rename_axis(range(self.index.nlevels)), + how="left", + ) + # and then reset the names after the join + result_block = result.rename_axis( + self.index.names if keep_original_names else index.names + )._block + return Series(result_block) + + def reindex_like(self, other: Series, *, validate: typing.Optional[bool] = None): + return self.reindex(other.index, validate=validate) + def drop_duplicates(self, *, keep: str = "first") -> Series: block = block_ops.drop_duplicates(self._block, (self._value_column,), keep) return Series(block) @@ -1216,14 +1323,7 @@ def to_xarray(self): def _throw_if_index_contains_duplicates( self, error_message: typing.Optional[str] = None ) -> None: - duplicates_block, _ = block_ops.indicate_duplicates( - self._get_block(), self._get_block().index_columns - ) - duplicates_block = duplicates_block.with_column_labels( - ["values", "is_duplicate"] - ) - duplicates_df = bigframes.dataframe.DataFrame(duplicates_block) - if duplicates_df["is_duplicate"].any(): + if not self.index.is_unique: error_message = ( error_message if error_message diff --git a/bigframes/session.py b/bigframes/session.py index 17444077726..3ca79a7b531 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -424,25 +424,30 @@ def read_gbq( ) -> dataframe.DataFrame: # TODO(b/281571214): Generate prompt to show the progress of read_gbq. if _is_query(query): - return self.read_gbq_query( + return self._read_gbq_query( query, index_col=index_col, col_order=col_order, max_results=max_results, + api_name="read_gbq", ) else: # TODO(swast): Query the snapshot table but mark it as a # deterministic query so we can avoid serializing if we have a # unique index. - return self.read_gbq_table( + return self._read_gbq_table( query, index_col=index_col, col_order=col_order, max_results=max_results, + api_name="read_gbq", ) def _query_to_destination( - self, query: str, index_cols: List[str] + self, + query: str, + index_cols: List[str], + api_name: str, ) -> Tuple[Optional[bigquery.TableReference], Optional[bigquery.QueryJob]]: # If there are no index columns, then there's no reason to cache to a # (clustered) session table, as we'll just have to query it again to @@ -464,7 +469,7 @@ def _query_to_destination( # operations are as speedy as they can be. try: ibis_expr = self.ibis_client.sql(query) - return self._ibis_to_session_table(ibis_expr, index_cols), None + return self._ibis_to_session_table(ibis_expr, index_cols, api_name), None except google.api_core.exceptions.BadRequest: # Some SELECT statements still aren't compatible with CREATE TEMP # TABLE ... AS SELECT ... statements. For example, if the query has @@ -490,15 +495,33 @@ def read_gbq_query( See also: :meth:`Session.read_gbq`. """ + return self._read_gbq_query( + query=query, + index_col=index_col, + col_order=col_order, + max_results=max_results, + api_name="read_gbq_query", + ) + + def _read_gbq_query( + self, + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, + api_name: str, + ) -> dataframe.DataFrame: # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so # these docstrings are inline. - if isinstance(index_col, str): index_cols = [index_col] else: index_cols = list(index_col) - destination, query_job = self._query_to_destination(query, index_cols) + destination, query_job = self._query_to_destination( + query, index_cols, api_name="read_gbq_query" + ) # If there was no destination table, that means the query must have # been DDL or DML. Return some job metadata, instead. @@ -535,6 +558,23 @@ def read_gbq_table( See also: :meth:`Session.read_gbq`. """ + return self._read_gbq_table( + query=query, + index_col=index_col, + col_order=col_order, + max_results=max_results, + api_name="read_gbq_table", + ) + + def _read_gbq_table( + self, + query: str, + *, + index_col: Iterable[str] | str = (), + col_order: Iterable[str] = (), + max_results: Optional[int] = None, + api_name: str, + ) -> dataframe.DataFrame: if max_results and max_results <= 0: raise ValueError("`max_results` should be a positive number.") @@ -646,7 +686,8 @@ def read_gbq_table( # rows for which row numbers must be generated table_expression = table_expression.limit(max_results) table_expression, ordering = self._create_sequential_ordering( - table_expression + table=table_expression, + api_name=api_name, ) hidden_cols = ( (ordering.total_order_col.column_id,) @@ -667,6 +708,7 @@ def read_gbq_table( hidden_cols=hidden_cols, ordering=ordering, is_total_ordering=is_total_ordering, + api_name=api_name, ) def _read_gbq_with_ordering( @@ -680,6 +722,7 @@ def _read_gbq_with_ordering( hidden_cols: Iterable[str] = (), ordering: core.ExpressionOrdering, is_total_ordering: bool = False, + api_name: str, ) -> dataframe.DataFrame: """Internal helper method that loads DataFrame from Google BigQuery given an ordering column. @@ -698,6 +741,8 @@ def _read_gbq_with_ordering( Columns that should be hidden. Ordering columns may (not always) be hidden ordering: Column name to be used for ordering. If not supplied, a default ordering is generated. + api_name: + The name of the API method. Returns: A DataFrame representing results of the query or table. @@ -723,7 +768,9 @@ def _read_gbq_with_ordering( if not is_total_ordering: # Rows are not ordered, we need to generate a default ordering and materialize it table_expression, ordering = self._create_sequential_ordering( - table_expression, index_cols + table=table_expression, + index_cols=index_cols, + api_name=api_name, ) index_col_values = [table_expression[index_id] for index_id in index_cols] if not col_labels: @@ -846,6 +893,11 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame Returns: bigframes.dataframe.DataFrame: The BigQuery DataFrame. """ + return self._read_pandas(pandas_dataframe, "read_pandas") + + def _read_pandas( + self, pandas_dataframe: pandas.DataFrame, api_name: str + ) -> dataframe.DataFrame: col_labels, idx_labels = ( pandas_dataframe.columns.to_list(), pandas_dataframe.index.names, @@ -878,6 +930,7 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame job_config = bigquery.LoadJobConfig(schema=schema) job_config.clustering_fields = cluster_cols + job_config.labels = {"bigframes-api": api_name} load_table_destination = self._create_session_table() load_job = self.bqclient.load_table_from_dataframe( @@ -910,6 +963,7 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame hidden_cols=(ordering_col,), ordering=ordering, is_total_ordering=True, + api_name=api_name, ) return df @@ -991,6 +1045,7 @@ def read_csv( job_config.autodetect = True job_config.field_delimiter = sep job_config.encoding = encoding + job_config.labels = {"bigframes-api": "read_csv"} # We want to match pandas behavior. If header is 0, no rows should be skipped, so we # do not need to set `skip_leading_rows`. If header is None, then there is no header. @@ -1048,7 +1103,7 @@ def read_pickle( pandas_obj.name = "0" bigframes_df = self.read_pandas(pandas_obj.to_frame()) return bigframes_df[bigframes_df.columns[0]] - return self.read_pandas(pandas_obj) + return self._read_pandas(pandas_obj, "read_pickle") def read_parquet( self, @@ -1063,6 +1118,7 @@ def read_parquet( job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED job_config.source_format = bigquery.SourceFormat.PARQUET job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY + job_config.labels = {"bigframes-api": "read_parquet"} return self._read_bigquery_load_job(path, table, job_config=job_config) @@ -1109,6 +1165,7 @@ def read_json( job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY job_config.autodetect = True job_config.encoding = encoding + job_config.labels = {"bigframes-api": "read_json"} return self._read_bigquery_load_job( path_or_buf, @@ -1176,7 +1233,10 @@ def _create_session_table(self) -> bigquery.TableReference: return dataset.table(table_name) def _create_sequential_ordering( - self, table: ibis_types.Table, index_cols: Iterable[str] = () + self, + table: ibis_types.Table, + index_cols: Iterable[str] = (), + api_name: str = "", ) -> Tuple[ibis_types.Table, core.ExpressionOrdering]: # Since this might also be used as the index, don't use the default # "ordering ID" name. @@ -1188,6 +1248,7 @@ def _create_sequential_ordering( table_ref = self._ibis_to_session_table( table, cluster_cols=list(index_cols) + [default_ordering_name], + api_name=api_name, ) table = self.ibis_client.sql(f"SELECT * FROM `{table_ref.table_id}`") ordering_reference = core.OrderingColumnReference(default_ordering_name) @@ -1199,7 +1260,10 @@ def _create_sequential_ordering( return table, ordering def _ibis_to_session_table( - self, table: ibis_types.Table, cluster_cols: Iterable[str] + self, + table: ibis_types.Table, + cluster_cols: Iterable[str], + api_name: str, ) -> bigquery.TableReference: clusterable_cols = [ col for col in cluster_cols if _can_cluster(table[col].type()) @@ -1207,10 +1271,14 @@ def _ibis_to_session_table( return self._query_to_session_table( self.ibis_client.compile(table), cluster_cols=clusterable_cols, + api_name=api_name, ) def _query_to_session_table( - self, query_text: str, cluster_cols: Iterable[str] + self, + query_text: str, + cluster_cols: Iterable[str], + api_name: str, ) -> bigquery.TableReference: if len(list(cluster_cols)) > _MAX_CLUSTER_COLUMNS: raise ValueError( @@ -1236,6 +1304,7 @@ def _query_to_session_table( # otherwise we get `BadRequest: 400 OPTIONS on temporary tables are not # supported`. job_config.labels = {"source": "bigquery-dataframes-temp"} + job_config.labels["bigframes-api"] = api_name try: self._start_query( @@ -1253,6 +1322,7 @@ def remote_function( dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, reuse: bool = True, + name: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -1280,7 +1350,7 @@ def remote_function( * BigQuery Data Editor (roles/bigquery.dataEditor) * BigQuery Connection Admin (roles/bigquery.connectionAdmin) * Cloud Functions Developer (roles/cloudfunctions.developer) - * Service Account User (roles/iam.serviceAccountUser) + * Service Account User (roles/iam.serviceAccountUser) on the service account `PROJECT_NUMBER-compute@developer.gserviceaccount.com` * Storage Object Viewer (roles/storage.objectViewer) * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) @@ -1311,10 +1381,16 @@ def remote_function( reuse (bool, Optional): Reuse the remote function if already exists. `True` by default, which will result in reusing an existing remote - function (if any) that was previously created for the same udf. - Setting it to false would force creating a unique remote function. + function and corresponding cloud function (if any) that was + previously created for the same udf. + Setting it to `False` would force creating a unique remote function. If the required remote function does not exist then it would be created irrespective of this param. + name (str, Optional): + Explicit name of the persisted BigQuery remote function. Use it with + caution, because two users working in the same project and dataset + could overwrite each other's remote functions if they use the same + persistent name. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1331,6 +1407,7 @@ def remote_function( dataset=dataset, bigquery_connection=bigquery_connection, reuse=reuse, + name=name, ) def read_gbq_function( diff --git a/noxfile.py b/noxfile.py index 2355a9b27ba..033bbfefe40 100644 --- a/noxfile.py +++ b/noxfile.py @@ -42,6 +42,7 @@ "pytest", "pytest-cov", "pytest-asyncio", + "pytest-mock", ] UNIT_TEST_EXTERNAL_DEPENDENCIES: List[str] = [] UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [] diff --git a/samples/snippets/remote_function.py b/samples/snippets/remote_function.py index 37972672c32..9998a23eb2c 100644 --- a/samples/snippets/remote_function.py +++ b/samples/snippets/remote_function.py @@ -39,11 +39,19 @@ def run_remote_function_and_read_gbq_function(project_id: str): # already created, BigQuery DataFrames will attempt to create one assuming # the necessary APIs and IAM permissions are setup in the project. In our # examples we would be using a pre-created connection named - # `bigframes-rf-conn`. Let's try a `pandas`-like use case in which we want - # to apply a user defined scalar function to every value in a `Series`, more - # specifically bucketize the `body_mass_g` value of the penguins, which is a - # real number, into a category, which is a string. - @bpd.remote_function([float], str, bigquery_connection="bigframes-rf-conn") + # `bigframes-rf-conn`. We will also set `reuse=False` to make sure we don't + # step over someone else creating remote function in the same project from + # the exact same source code at the same time. Let's try a `pandas`-like use + # case in which we want to apply a user defined scalar function to every + # value in a `Series`, more specifically bucketize the `body_mass_g` value + # of the penguins, which is a real number, into a category, which is a + # string. + @bpd.remote_function( + [float], + str, + bigquery_connection="bigframes-rf-conn", + reuse=False, + ) def get_bucket(num): if not num: return "NA" @@ -80,9 +88,11 @@ def get_bucket(num): # Let's continue trying other potential use cases of remote functions. Let's # say we consider the `species`, `island` and `sex` of the penguins # sensitive information and want to redact that by replacing with their hash - # code instead. Let's define another scalar custom function and decorated it + # code instead. Let's define another scalar custom function and decorate it # as a remote function - @bpd.remote_function([str], str, bigquery_connection="bigframes-rf-conn") + @bpd.remote_function( + [str], str, bigquery_connection="bigframes-rf-conn", reuse=False + ) def get_hash(input): import hashlib diff --git a/setup.py b/setup.py index 139873e6fc9..69b71c88f13 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ "ibis-framework[bigquery] >=6.0.0,<=6.1.0", "pandas >=1.5.0", "pydata-google-auth >=1.8.2", + "requests >=2.27.1", "scikit-learn >=1.2.2", "sqlalchemy >=1.4,<3.0", "ipywidgets >=7.7.1", @@ -58,7 +59,7 @@ "pandas-gbq >=0.19.0", ], # Packages required for basic development flow. - "dev": ["pytest", "pre-commit", "nox", "google-cloud-testutils"], + "dev": ["pytest", "pytest-mock", "pre-commit", "nox", "google-cloud-testutils"], } extras["all"] = list(sorted(frozenset(itertools.chain.from_iterable(extras.values())))) diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 523256ee835..cd69d45dc92 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -9,7 +9,7 @@ cachetools==5.3.0 certifi==2022.12.7 cffi==1.15.1 cfgv==3.3.1 -charset-normalizer==3.1.0 +charset-normalizer==2.0.0 click==8.1.3 cloudpickle==2.0.0 colorlog==6.7.0 @@ -90,13 +90,14 @@ pyperclip==1.8.2 pytest==7.2.2 pytest-asyncio==0.21.0 pytest-cov==4.0.0 +pytest-mock==3.11.1 pytest-retry==1.1.0 pytest-xdist==3.2.1 python-dateutil==2.8.2 pytz==2023.3 PyYAML==6.0 readme-renderer==37.3 -requests==2.28.2 +requests==2.27.1 requests-oauthlib==1.3.1 requests-toolbelt==0.10.1 rfc3986==2.0.0 diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index 9b2872d6737..a8613dfeb9b 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -70,7 +70,7 @@ def test_xgbregressor_dart_booster_multiple_params( ): model = bigframes.ml.ensemble.XGBRegressor( booster="dart", - tree_method="AUTO", + tree_method="auto", min_tree_child_weight=2, colsample_bytree=0.95, colsample_bylevel=0.95, @@ -121,7 +121,7 @@ def test_xgbregressor_dart_booster_multiple_params( in reloaded_model._bqml_model.model_name ) assert reloaded_model.booster == "DART" - assert reloaded_model.dart_normalized_type == "TREE" + assert reloaded_model.dart_normalized_type == "tree" assert reloaded_model.tree_method == "AUTO" assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 @@ -185,7 +185,7 @@ def test_xgbclassifier_dart_booster_multiple_params( ): model = bigframes.ml.ensemble.XGBClassifier( booster="dart", - tree_method="AUTO", + tree_method="auto", min_tree_child_weight=2, colsample_bytree=0.95, colsample_bylevel=0.95, @@ -235,7 +235,7 @@ def test_xgbclassifier_dart_booster_multiple_params( in reloaded_model._bqml_model.model_name ) assert reloaded_model.booster == "DART" - assert reloaded_model.dart_normalized_type == "TREE" + assert reloaded_model.dart_normalized_type == "tree" assert reloaded_model.tree_method == "AUTO" assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 @@ -297,7 +297,7 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset @pytest.mark.flaky(retries=2, delay=120) def test_randomforestregressor_multiple_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestRegressor( - tree_method="AUTO", + tree_method="auto", min_tree_child_weight=2, colsample_bytree=0.95, colsample_bylevel=0.95, diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 8033f79c47b..2f231f40c95 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -61,16 +61,32 @@ def get_remote_function_endpoints(bigquery_client, dataset_id): return endpoints -def get_cloud_functions(functions_client, project, location, name_prefix="bigframes-"): +def get_cloud_functions( + functions_client, project, location, name=None, name_prefix=None +): """Get the cloud functions in the given project and location.""" + + assert ( + not name or not name_prefix + ), f"At most one of the {name.__name__} or {name_prefix.__name__} can be passed." + _, location = get_remote_function_locations(location) parent = f"projects/{project}/locations/{location}" request = functions_v2.ListFunctionsRequest(parent=parent) page_result = functions_client.list_functions(request=request) - full_name_prefix = parent + f"/functions/{name_prefix}" for response in page_result: - if not name_prefix or response.name.startswith(full_name_prefix): - yield response + # If name is provided and it does not match then skip + if bool(name): + full_name = parent + f"/functions/{name}" + if response.name != full_name: + continue + # If name prefix is provided and it does not match then skip + elif bool(name_prefix): + full_name_prefix = parent + f"/functions/{name_prefix}" + if not response.name.startswith(full_name_prefix): + continue + + yield response def delete_cloud_function(functions_client, full_name): @@ -84,8 +100,17 @@ def cleanup_remote_function_assets( bigquery_client, functions_client, remote_udf, ignore_failures=True ): """Clean up the GCP assets behind a bigframes remote function.""" + + # Clean up BQ remote function try: bigquery_client.delete_routine(remote_udf.bigframes_remote_function) + except Exception: + # By default don't raise exception in cleanup + if not ignore_failures: + raise + + # Clean up cloud function + try: delete_cloud_function(functions_client, remote_udf.bigframes_cloud_function) except Exception: # By default don't raise exception in cleanup @@ -94,7 +119,15 @@ def cleanup_remote_function_assets( def make_uniq_udf(udf): - """Transform a udf to another with same behavior but a unique name.""" + """Transform a udf to another with same behavior but a unique name. + Use this to test remote functions with reuse=True, in which case parallel + instances of the same tests may evaluate same named cloud functions and BQ + remote functions, therefore interacting with each other and causing unwanted + failures. With this method one can transform a udf into another with the + same behavior but a different name which will remain unique for the + lifetime of one test instance. + """ + prefixer = test_utils.prefixer.Prefixer(udf.__name__, "") udf_uniq_name = prefixer.create_prefix() udf_file_name = f"{udf_uniq_name}.py" @@ -111,7 +144,18 @@ def make_uniq_udf(udf): target_code = source_code.replace(source_key, target_key, 1) f.write(target_code) spec = importlib.util.spec_from_file_location(udf_file_name, udf_file_path) - return getattr(spec.loader.load_module(), udf_uniq_name), tmpdir + udf_uniq = getattr(spec.loader.load_module(), udf_uniq_name) + + # This is a bit of a hack but we need to remove the reference to a foreign + # module, otherwise the serialization would keep the foreign module + # reference and deserialization would fail with error like following: + # ModuleNotFoundError: No module named 'add_one_2nxcmd9j' + # TODO(shobs): Figure out if there is a better way of generating the unique + # function object, but for now let's just set it to same module as the + # original udf. + udf_uniq.__module__ = udf.__module__ + + return udf_uniq, tmpdir @pytest.fixture(scope="module") @@ -136,7 +180,10 @@ def cleanup_cloud_functions(session, functions_client, dataset_id_permanent): ) delete_count = 0 for cloud_function in get_cloud_functions( - functions_client, session.bqclient.project, session.bqclient.location + functions_client, + session.bqclient.project, + session.bqclient.location, + name_prefix="bigframes-", ): # Ignore bigframes cloud functions referred by the remote functions in # the permanent dataset @@ -524,15 +571,6 @@ def add_one(x): # Make a unique udf add_one_uniq, add_one_uniq_dir = make_uniq_udf(add_one) - # This is a bit of a hack but we need to remove the reference to a foreign - # module, otherwise the serialization would keep the foreign module - # reference and deserialization would fail with error like following: - # ModuleNotFoundError: No module named 'add_one_2nxcmd9j' - # TODO(shobs): Figure out if there is a better way of generating the unique - # function object, but for now let's just set it to same module as the - # original udf. - add_one_uniq.__module__ = add_one.__module__ - # Expected cloud function name for the unique udf add_one_uniq_cf_name = get_cloud_function_name(add_one_uniq) @@ -542,7 +580,7 @@ def add_one(x): functions_client, session.bqclient.project, session.bqclient.location, - name_prefix=add_one_uniq_cf_name, + name=add_one_uniq_cf_name, ) ) assert len(cloud_functions) == 0 @@ -563,7 +601,7 @@ def add_one(x): functions_client, session.bqclient.project, session.bqclient.location, - name_prefix=add_one_uniq_cf_name, + name=add_one_uniq_cf_name, ) ) assert len(cloud_functions) == 1 @@ -611,7 +649,7 @@ def inner_test(): functions_client, session.bqclient.project, session.bqclient.location, - name_prefix=add_one_uniq_cf_name, + name=add_one_uniq_cf_name, ) ) assert len(cloud_functions) == 0 @@ -633,7 +671,7 @@ def inner_test(): functions_client, session.bqclient.project, session.bqclient.location, - name_prefix=add_one_uniq_cf_name, + name=add_one_uniq_cf_name, ) ) assert len(cloud_functions) == 1 @@ -776,3 +814,221 @@ def test_remote_udf_lambda( cleanup_remote_function_assets( session.bqclient, functions_client, add_one_lambda_remote ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_with_explicit_name( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + def square(x): + return x * x + + prefixer = test_utils.prefixer.Prefixer(square.__name__, "") + rf_name = prefixer.create_prefix() + expected_remote_function = f"{dataset_id}.{rf_name}" + + # Initially the expected BQ remote function should not exist + with pytest.raises(NotFound): + session.bqclient.get_routine(expected_remote_function) + + # Create the remote function with the name provided explicitly + square_remote = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + reuse=False, + name=rf_name, + )(square) + + # The remote function should reflect the explicitly provided name + assert square_remote.bigframes_remote_function == expected_remote_function + + # Now the expected BQ remote function should exist + session.bqclient.get_routine(expected_remote_function) + + # The behavior of the created remote function should be as expected + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_too"] + bf_result_col = bf_int64_col.apply(square_remote) + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_too"] + pd_result_col = pd_int64_col.apply(square) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col.dtype is Int64Dtype() + # pd_int64_col.apply(square).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, square_remote + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_with_explicit_name_reuse( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + dirs_to_cleanup = [] + + # Define a user code + def square(x): + return x * x + + # Make it a unique udf + square_uniq, square_uniq_dir = make_uniq_udf(square) + dirs_to_cleanup.append(square_uniq_dir) + + # Define a common routine which accepts a remote function and the + # corresponding user defined function and tests that bigframes bahavior + # on the former is in parity with the pandas behaviour on the latter + def test_internal(rf, udf): + # The behavior of the created remote function should be as expected + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_too"] + bf_result_col = bf_int64_col.apply(rf) + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_too"] + pd_result_col = pd_int64_col.apply(udf) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col.dtype is Int64Dtype() + # pd_int64_col.apply(square).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + # Create an explicit name for the remote function + prefixer = test_utils.prefixer.Prefixer("foo", "") + rf_name = prefixer.create_prefix() + expected_remote_function = f"{dataset_id}.{rf_name}" + + # Initially the expected BQ remote function should not exist + with pytest.raises(NotFound): + session.bqclient.get_routine(expected_remote_function) + + # Create a new remote function with the name provided explicitly + square_remote1 = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + name=rf_name, + )(square_uniq) + + # The remote function should reflect the explicitly provided name + assert square_remote1.bigframes_remote_function == expected_remote_function + + # Now the expected BQ remote function should exist + routine = session.bqclient.get_routine(expected_remote_function) + square_remote1_created = routine.created + square_remote1_cf_updated = session.cloudfunctionsclient.get_function( + name=square_remote1.bigframes_cloud_function + ).update_time + + # Test pandas parity with square udf + test_internal(square_remote1, square) + + # Now Create another remote function with the same name provided + # explicitly. Since reuse is True by default, the previously created + # remote function with the same name will be reused. + square_remote2 = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + name=rf_name, + )(square_uniq) + + # The new remote function should still reflect the explicitly provided name + assert square_remote2.bigframes_remote_function == expected_remote_function + + # The expected BQ remote function should still exist + routine = session.bqclient.get_routine(expected_remote_function) + square_remote2_created = routine.created + square_remote2_cf_updated = session.cloudfunctionsclient.get_function( + name=square_remote2.bigframes_cloud_function + ).update_time + + # The new remote function should reflect that the previous BQ remote + # function and the cloud function were reused instead of creating anew + assert square_remote2_created == square_remote1_created + assert ( + square_remote2.bigframes_cloud_function + == square_remote1.bigframes_cloud_function + ) + assert square_remote2_cf_updated == square_remote1_cf_updated + + # Test again that the new remote function is actually same as the + # previous remote function + test_internal(square_remote2, square) + + # Now define a different user code + def plusone(x): + return x + 1 + + # Make it a unique udf + plusone_uniq, plusone_uniq_dir = make_uniq_udf(plusone) + dirs_to_cleanup.append(plusone_uniq_dir) + + # Now Create a third remote function with the same name provided + # explicitly. Even though reuse is True by default, the previously + # created remote function with the same name should not be reused since + # this time it is a different user code. + plusone_remote = session.remote_function( + [int], + int, + dataset_id, + bq_cf_connection, + name=rf_name, + )(plusone_uniq) + + # The new remote function should still reflect the explicitly provided name + assert plusone_remote.bigframes_remote_function == expected_remote_function + + # The expected BQ remote function should still exist + routine = session.bqclient.get_routine(expected_remote_function) + plusone_remote_created = routine.created + plusone_remote_cf_updated = session.cloudfunctionsclient.get_function( + name=plusone_remote.bigframes_cloud_function + ).update_time + + # The new remote function should reflect that the previous BQ remote + # function and the cloud function were NOT reused, instead were created + # anew + assert plusone_remote_created > square_remote2_created + assert ( + plusone_remote.bigframes_cloud_function + != square_remote2.bigframes_cloud_function + ) + assert plusone_remote_cf_updated > square_remote2_cf_updated + + # Test again that the new remote function is equivalent to the new user + # defined function + test_internal(plusone_remote, plusone) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, square_remote1 + ) + cleanup_remote_function_assets( + session.bqclient, functions_client, square_remote2 + ) + cleanup_remote_function_assets( + session.bqclient, functions_client, plusone_remote + ) + for dir_ in dirs_to_cleanup: + shutil.rmtree(dir_) diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 6c3e8e06f5b..ace943956fe 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -18,6 +18,7 @@ import pandas as pd import pyarrow as pa +import pytest import pytz import bigframes @@ -278,6 +279,7 @@ def test_model_predict_with_unnamed_index( ) +@pytest.mark.flaky(retries=2, delay=120) def test_model_generate_text( bqml_palm2_text_generator_model: core.BqmlModel, llm_text_df ): diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index 8df4145fcf0..c71bbbe3b0c 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -16,33 +16,14 @@ from bigframes.ml import decomposition -_PD_NEW_PENGUINS = pd.DataFrame( - { - "tag_number": [1633, 1672, 1690], - "species": [ - "Adelie Penguin (Pygoscelis adeliae)", - "Gentoo penguin (Pygoscelis papua)", - "Adelie Penguin (Pygoscelis adeliae)", - ], - "island": ["Dream", "Biscoe", "Torgersen"], - "culmen_length_mm": [37.8, 46.5, 41.1], - "culmen_depth_mm": [18.1, 14.8, 18.6], - "flipper_length_mm": [193.0, 217.0, 189.0], - "body_mass_g": [3750.0, 5200.0, 3325.0], - "sex": ["MALE", "FEMALE", "MALE"], - } -).set_index("tag_number") - -def test_pca_predict(session, penguins_pca_model: decomposition.PCA): - new_penguins = session.read_pandas(_PD_NEW_PENGUINS) - - predictions = penguins_pca_model.predict(new_penguins).to_pandas() +def test_pca_predict(penguins_pca_model, new_penguins_df): + predictions = penguins_pca_model.predict(new_penguins_df).to_pandas() expected = pd.DataFrame( { - "principal_component_1": [-1.459, 2.258, -1.685], - "principal_component_2": [-1.120, -1.351, -0.874], - "principal_component_3": [-0.646, 0.443, -0.704], + "principal_component_1": [-1.314041, -0.855813, -1.848786], + "principal_component_2": [-0.889106, -1.259753, -0.983304], + "principal_component_3": [-0.704345, 0.322555, -0.095759], }, dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 85c3cce1d77..a85777c59d6 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -13,6 +13,7 @@ # limitations under the License. import operator +import tempfile import typing from typing import Tuple @@ -137,6 +138,46 @@ def test_tail_with_custom_column_labels(scalars_df_index, scalars_pandas_df_inde pandas.testing.assert_frame_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + ("all",), + ], +) +def test_df_nlargest(scalars_df_index, scalars_pandas_df_index, keep): + bf_result = scalars_df_index.nlargest( + 3, ["bool_col", "int64_too"], keep=keep + ).to_pandas() + pd_result = scalars_pandas_df_index.nlargest( + 3, ["bool_col", "int64_too"], keep=keep + ) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + ("all",), + ], +) +def test_df_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): + bf_result = scalars_df_index.nsmallest(6, ["bool_col"], keep=keep).to_pandas() + pd_result = scalars_pandas_df_index.nsmallest(6, ["bool_col"], keep=keep) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + def test_get_column_by_attr(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs series = scalars_df.int64_col @@ -582,6 +623,22 @@ def test_df_fillna(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_df_ffill(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[["int64_col", "float64_col"]].ffill(limit=1).to_pandas() + pd_result = scalars_pandas_df[["int64_col", "float64_col"]].ffill(limit=1) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_bfill(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[["int64_col", "float64_col"]].bfill().to_pandas() + pd_result = scalars_pandas_df[["int64_col", "float64_col"]].bfill() + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + def test_df_isin_list(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs values = ["Hello, World!", 55555, 2.51, pd.NA, True] @@ -1027,6 +1084,88 @@ def test_df_notnull(scalars_dfs): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +@pytest.mark.parametrize( + ("left_labels", "right_labels", "overwrite", "fill_value"), + [ + (["a", "b", "c"], ["c", "a", "b"], True, None), + (["a", "b", "c"], ["c", "a", "b"], False, None), + (["a", "b", "c"], ["a", "b", "c"], False, 2), + ], + ids=[ + "one_one_match_overwrite", + "one_one_match_no_overwrite", + "exact_match", + ], +) +def test_combine( + scalars_df_index, + scalars_df_2_index, + scalars_pandas_df_index, + left_labels, + right_labels, + overwrite, + fill_value, +): + if pd.__version__.startswith("1."): + pytest.skip("pd.NA vs NaN not handled well in pandas 1.x.") + columns = ["int64_too", "int64_col", "float64_col"] + + bf_df_a = scalars_df_index[columns] + bf_df_a.columns = left_labels + bf_df_b = scalars_df_2_index[columns] + bf_df_b.columns = right_labels + bf_result = bf_df_a.combine( + bf_df_b, + lambda x, y: x**2 + 2 * x * y + y**2, + overwrite=overwrite, + fill_value=fill_value, + ).to_pandas() + + pd_df_a = scalars_pandas_df_index[columns] + pd_df_a.columns = left_labels + pd_df_b = scalars_pandas_df_index[columns] + pd_df_b.columns = right_labels + pd_result = pd_df_a.combine( + pd_df_b, + lambda x, y: x**2 + 2 * x * y + y**2, + overwrite=overwrite, + fill_value=fill_value, + ) + + # Some dtype inconsistency for all-NULL columns + pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +def test_combine_first( + scalars_df_index, + scalars_df_2_index, + scalars_pandas_df_index, +): + if pd.__version__.startswith("1."): + pytest.skip("pd.NA vs NaN not handled well in pandas 1.x.") + columns = ["int64_too", "int64_col", "float64_col"] + + bf_df_a = scalars_df_index[columns].iloc[0:6] + bf_df_a.columns = ["a", "b", "c"] + bf_df_b = scalars_df_2_index[columns].iloc[2:8] + bf_df_b.columns = ["b", "a", "d"] + bf_result = bf_df_a.combine_first(bf_df_b).to_pandas() + + pd_df_a = scalars_pandas_df_index[columns].iloc[0:6] + pd_df_a.columns = ["a", "b", "c"] + pd_df_b = scalars_pandas_df_index[columns].iloc[2:8] + pd_df_b.columns = ["b", "a", "d"] + pd_result = pd_df_a.combine_first(pd_df_b) + + print("pandas") + print(pd_result.to_string()) + print("bigframes") + print(bf_result.to_string()) + + # Some dtype inconsistency for all-NULL columns + pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + @pytest.mark.parametrize( ("op"), [ @@ -1145,11 +1284,13 @@ def test_series_binop_axis_index( (["a", "a", "b"], ["c", "c", "d"]), (["a", "b", "c"], ["c", "a", "b"]), (["a", "c", "c"], ["c", "a", "c"]), + (["a", "b", "c"], ["a", "b", "c"]), ], ids=[ "no_overlap", "one_one_match", "multi_match", + "exact_match", ], ) def test_binop_df_df_binary_op( @@ -1361,6 +1502,42 @@ def test_dataframe_general_analytic_op( ) +@pytest.mark.parametrize( + ("periods",), + [ + (1,), + (2,), + (-1,), + ], +) +def test_dataframe_diff(scalars_df_index, scalars_pandas_df_index, periods): + col_names = ["int64_too", "float64_col", "int64_col"] + bf_result = scalars_df_index[col_names].diff(periods=periods).to_pandas() + pd_result = scalars_pandas_df_index[col_names].diff(periods=periods) + pd.testing.assert_frame_equal( + pd_result, + bf_result, + ) + + +@pytest.mark.parametrize( + ("periods",), + [ + (1,), + (2,), + (-1,), + ], +) +def test_dataframe_pct_change(scalars_df_index, scalars_pandas_df_index, periods): + col_names = ["int64_too", "float64_col", "int64_col"] + bf_result = scalars_df_index[col_names].pct_change(periods=periods).to_pandas() + pd_result = scalars_pandas_df_index[col_names].pct_change(periods=periods) + pd.testing.assert_frame_equal( + pd_result, + bf_result, + ) + + def test_dataframe_agg_single_string(scalars_dfs): numeric_cols = ["int64_col", "int64_too", "float64_col"] scalars_df, scalars_pandas_df = scalars_dfs @@ -1675,6 +1852,52 @@ def test_loc_single_index_no_duplicate(scalars_df_index, scalars_pandas_df_index ) +def test_loc_setitem_bool_series_scalar_new_col(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df.loc[bf_df["int64_too"] == 0, "new_col"] = 99 + pd_df.loc[pd_df["int64_too"] == 0, "new_col"] = 99 + + # pandas type difference + pd_df["new_col"] = pd_df["new_col"].astype("Float64") + + pd.testing.assert_frame_equal( + bf_df.to_pandas(), + pd_df, + ) + + +def test_loc_setitem_bool_series_scalar_existing_col(scalars_dfs): + if pd.__version__.startswith("1."): + pytest.skip("this loc overload not supported in pandas 1.x.") + + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df.loc[bf_df["int64_too"] == 1, "string_col"] = "hello" + pd_df.loc[pd_df["int64_too"] == 1, "string_col"] = "hello" + + pd.testing.assert_frame_equal( + bf_df.to_pandas(), + pd_df, + ) + + +def test_loc_setitem_bool_series_scalar_type_error(scalars_dfs): + if pd.__version__.startswith("1."): + pytest.skip("this loc overload not supported in pandas 1.x.") + + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + + with pytest.raises(TypeError): + bf_df.loc[bf_df["int64_too"] == 1, "string_col"] = 99 + with pytest.raises(TypeError): + pd_df.loc[pd_df["int64_too"] == 1, "string_col"] = 99 + + @pytest.mark.parametrize( ("op"), [ @@ -1749,6 +1972,30 @@ def test_dataframe_prod(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) +def test_df_skew_too_few_values(scalars_dfs): + columns = ["float64_col", "int64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[columns].head(2).skew().to_pandas() + pd_result = scalars_pandas_df[columns].head(2).skew() + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + +def test_df_skew(scalars_dfs): + columns = ["float64_col", "int64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[columns].skew().to_pandas() + pd_result = scalars_pandas_df[columns].skew() + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + @pytest.mark.parametrize( ("frac", "n", "random_state"), [ @@ -1828,6 +2075,161 @@ def test_df_add_suffix(scalars_df_index, scalars_pandas_df_index, axis): ) +def test_df_columns_filter_items(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("2.0") or pd.__version__.startswith("1."): + pytest.skip("pandas filter items behavior different pre-2.1") + bf_result = scalars_df_index.filter(items=["string_col", "int64_col"]).to_pandas() + + pd_result = scalars_pandas_df_index.filter(items=["string_col", "int64_col"]) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_columns_filter_like(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.filter(like="64_col").to_pandas() + + pd_result = scalars_pandas_df_index.filter(like="64_col") + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_columns_filter_regex(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.filter(regex="^[^_]+$").to_pandas() + + pd_result = scalars_pandas_df_index.filter(regex="^[^_]+$") + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_rows_filter_items(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("2.0") or pd.__version__.startswith("1."): + pytest.skip("pandas filter items behavior different pre-2.1") + bf_result = scalars_df_index.filter(items=[5, 1, 3], axis=0).to_pandas() + + pd_result = scalars_pandas_df_index.filter(items=[5, 1, 3], axis=0) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_rows_filter_like(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.copy().set_index("string_col") + scalars_pandas_df_index = scalars_pandas_df_index.copy().set_index("string_col") + + bf_result = scalars_df_index.filter(like="ello", axis=0).to_pandas() + + pd_result = scalars_pandas_df_index.filter(like="ello", axis=0) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_rows_filter_regex(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.copy().set_index("string_col") + scalars_pandas_df_index = scalars_pandas_df_index.copy().set_index("string_col") + + bf_result = scalars_df_index.filter(regex="^[GH].*", axis=0).to_pandas() + + pd_result = scalars_pandas_df_index.filter(regex="^[GH].*", axis=0) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_reindex_rows_list(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.reindex(index=[5, 1, 3, 99, 1]).to_pandas() + + pd_result = scalars_pandas_df_index.reindex(index=[5, 1, 3, 99, 1]) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_reindex_rows_index(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.reindex( + index=pd.Index([5, 1, 3, 99, 1], name="newname") + ).to_pandas() + + pd_result = scalars_pandas_df_index.reindex( + index=pd.Index([5, 1, 3, 99, 1], name="newname") + ) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_reindex_nonunique(scalars_df_index): + with pytest.raises(ValueError): + # int64_too is non-unique + scalars_df_index.set_index("int64_too").reindex( + index=[5, 1, 3, 99, 1], validate=True + ) + + +def test_df_reindex_columns(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.reindex( + columns=["not_a_col", "int64_col", "int64_too"] + ).to_pandas() + + pd_result = scalars_pandas_df_index.reindex( + columns=["not_a_col", "int64_col", "int64_too"] + ) + + # Pandas uses float64 as default for newly created empty column, bf uses Float64 + pd_result.not_a_col = pd_result.not_a_col.astype(pandas.Float64Dtype()) + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_reindex_like(scalars_df_index, scalars_pandas_df_index): + reindex_target_bf = scalars_df_index.reindex( + columns=["not_a_col", "int64_col", "int64_too"], index=[5, 1, 3, 99, 1] + ) + bf_result = scalars_df_index.reindex_like(reindex_target_bf).to_pandas() + + reindex_target_pd = scalars_pandas_df_index.reindex( + columns=["not_a_col", "int64_col", "int64_too"], index=[5, 1, 3, 99, 1] + ) + pd_result = scalars_pandas_df_index.reindex_like(reindex_target_pd) + + # Pandas uses float64 as default for newly created empty column, bf uses Float64 + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + # Pandas uses float64 as default for newly created empty column, bf uses Float64 + pd_result.not_a_col = pd_result.not_a_col.astype(pandas.Float64Dtype()) + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + def test_df_values(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.values @@ -2035,6 +2437,93 @@ def test_df_duplicated(scalars_df_index, scalars_pandas_df_index, keep, subset): pd.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) +def test_df_to_dict(scalars_df_index, scalars_pandas_df_index): + unsupported = ["numeric_col"] # formatted differently + bf_result = scalars_df_index.drop(columns=unsupported).to_dict() + pd_result = scalars_pandas_df_index.drop(columns=unsupported).to_dict() + + assert bf_result == pd_result + + +def test_df_to_excel(scalars_df_index, scalars_pandas_df_index): + unsupported = ["timestamp_col"] + with tempfile.TemporaryFile() as bf_result_file, tempfile.TemporaryFile() as pd_result_file: + scalars_df_index.drop(columns=unsupported).to_excel(bf_result_file) + scalars_pandas_df_index.drop(columns=unsupported).to_excel(pd_result_file) + bf_result = bf_result_file.read() + pd_result = bf_result_file.read() + + assert bf_result == pd_result + + +def test_df_to_latex(scalars_df_index, scalars_pandas_df_index): + unsupported = ["numeric_col"] # formatted differently + bf_result = scalars_df_index.drop(columns=unsupported).to_latex() + pd_result = scalars_pandas_df_index.drop(columns=unsupported).to_latex() + + assert bf_result == pd_result + + +def test_df_to_records(scalars_df_index, scalars_pandas_df_index): + unsupported = ["numeric_col"] + bf_result = scalars_df_index.drop(columns=unsupported).to_records() + pd_result = scalars_pandas_df_index.drop(columns=unsupported).to_records() + + for bfi, pdi in zip(bf_result, pd_result): + for bfj, pdj in zip(bfi, pdi): + assert pd.isna(bfj) and pd.isna(pdj) or bfj == pdj + + +def test_df_to_string(scalars_df_index, scalars_pandas_df_index): + unsupported = ["numeric_col"] # formatted differently + + bf_result = scalars_df_index.drop(columns=unsupported).to_string() + pd_result = scalars_pandas_df_index.drop(columns=unsupported).to_string() + + assert bf_result == pd_result + + +def test_df_to_markdown(scalars_df_index, scalars_pandas_df_index): + # Nulls have bug from tabulate https://github.com/astanin/python-tabulate/issues/231 + bf_result = scalars_df_index.dropna().to_markdown() + pd_result = scalars_pandas_df_index.dropna().to_markdown() + + assert bf_result == pd_result + + +def test_df_to_pickle(scalars_df_index, scalars_pandas_df_index): + with tempfile.TemporaryFile() as bf_result_file, tempfile.TemporaryFile() as pd_result_file: + scalars_df_index.to_pickle(bf_result_file) + scalars_pandas_df_index.to_pickle(pd_result_file) + bf_result = bf_result_file.read() + pd_result = bf_result_file.read() + + assert bf_result == pd_result + + +def test_df_to_orc(scalars_df_index, scalars_pandas_df_index): + unsupported = [ + "numeric_col", + "bytes_col", + "date_col", + "datetime_col", + "time_col", + "timestamp_col", + "geography_col", + ] + + bf_result_file = tempfile.TemporaryFile() + pd_result_file = tempfile.TemporaryFile() + scalars_df_index.drop(columns=unsupported).to_orc(bf_result_file) + scalars_pandas_df_index.drop(columns=unsupported).reset_index().to_orc( + pd_result_file + ) + bf_result = bf_result_file.read() + pd_result = bf_result_file.read() + + assert bf_result == pd_result + + @pytest.mark.parametrize( ("subset", "normalize", "ascending", "dropna"), [ diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 987368ce771..18741468c5d 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -210,12 +210,14 @@ def test_dataframe_groupby_multi_sum( (lambda x: x.cummax(numeric_only=True)), (lambda x: x.cummin(numeric_only=True)), (lambda x: x.cumprod()), + (lambda x: x.shift(periods=2)), ], ids=[ "cumsum", "cummax", "cummin", "cumprod", + "shift", ], ) def test_dataframe_groupby_analytic( @@ -229,6 +231,30 @@ def test_dataframe_groupby_analytic( pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +def test_series_groupby_skew(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.groupby("bool_col")["int64_too"].skew().to_pandas() + pd_result = scalars_pandas_df_index.groupby("bool_col")["int64_too"].skew() + + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + +def test_dataframe_groupby_skew(scalars_df_index, scalars_pandas_df_index): + col_names = ["float64_col", "int64_col", "bool_col"] + bf_result = scalars_df_index[col_names].groupby("bool_col").skew().to_pandas() + pd_result = scalars_pandas_df_index[col_names].groupby("bool_col").skew() + + pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + + +def test_dataframe_groupby_diff(scalars_df_index, scalars_pandas_df_index): + col_names = ["float64_col", "int64_col", "string_col"] + bf_result = scalars_df_index[col_names].groupby("string_col").diff(-1) + pd_result = scalars_pandas_df_index[col_names].groupby("string_col").diff(-1) + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + def test_dataframe_groupby_getitem( scalars_df_index, scalars_pandas_df_index, diff --git a/tests/system/small/test_ipython.py b/tests/system/small/test_ipython.py index 6725805d9a7..be98ce00674 100644 --- a/tests/system/small/test_ipython.py +++ b/tests/system/small/test_ipython.py @@ -22,7 +22,8 @@ def test_repr_cache(scalars_df_index): # Make sure the df has a new block that the method return value # is not already cached. test_df = scalars_df_index.head() + test_df._block.retrieve_repr_request_results.cache_clear() results = display_formatter.format(test_df) assert results[0].keys() == {"text/plain", "text/html"} - assert test_df._block.retrieve_repr_request_results.cache_info().misses == 1 - assert test_df._block.retrieve_repr_request_results.cache_info().hits == 1 + assert test_df._block.retrieve_repr_request_results.cache_info().misses >= 1 + assert test_df._block.retrieve_repr_request_results.cache_info().hits >= 1 diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 25d1e2ad498..1e38b47b4cc 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -157,7 +157,7 @@ def test_multi_index_getitem_bool(scalars_df_index, scalars_pandas_df_index): ], ids=["level_num", "level_name", "list", "mixed_list"], ) -def test_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, level): +def test_df_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, level): bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) @@ -167,6 +167,26 @@ def test_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, level) pandas.testing.assert_frame_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("level"), + [ + (1), + ("int64_too"), + ([0, 2]), + ([2, "bool_col"]), + ], + ids=["level_num", "level_name", "list", "mixed_list"], +) +def test_series_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, level): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + + bf_result = bf_frame["string_col"].droplevel(level).to_pandas() + pd_result = pd_frame["string_col"].droplevel(level) + + pandas.testing.assert_series_equal(bf_result, pd_result) + + @pytest.mark.parametrize( ("labels", "level"), [ @@ -198,7 +218,9 @@ def test_multi_index_drop(scalars_df_index, scalars_pandas_df_index, labels, lev "num_names_mixed", ], ) -def test_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_index, order): +def test_df_multi_index_reorder_levels( + scalars_df_index, scalars_pandas_df_index, order +): bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) @@ -208,6 +230,51 @@ def test_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_index, o pandas.testing.assert_frame_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("order"), + [ + (1, 0, 2), + (["int64_col", "bool_col", "int64_too"]), + (["int64_col", "bool_col", 0]), + ], + ids=[ + "level_nums", + "level_names", + "num_names_mixed", + ], +) +def test_series_multi_index_reorder_levels( + scalars_df_index, scalars_pandas_df_index, order +): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + + bf_result = bf_frame["string_col"].reorder_levels(order).to_pandas() + pd_result = pd_frame["string_col"].reorder_levels(order) + + pandas.testing.assert_series_equal(bf_result, pd_result) + + +def test_df_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + + bf_result = bf_frame.swaplevel().to_pandas() + pd_result = pd_frame.swaplevel() + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_series_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): + bf_frame = scalars_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col", "int64_col"]) + + bf_result = bf_frame["string_col"].swaplevel(0, 2).to_pandas() + pd_result = pd_frame["string_col"].swaplevel(0, 2) + + pandas.testing.assert_series_equal(bf_result, pd_result) + + def test_multi_index_series_groupby(scalars_df_index, scalars_pandas_df_index): bf_frame = scalars_df_index.set_index(["int64_too", "bool_col"]) bf_result = ( @@ -446,6 +513,24 @@ def test_multi_index_series_rename_dict_same_type( ) +def test_multi_index_df_reindex(scalars_df_index, scalars_pandas_df_index): + new_index = pandas.MultiIndex.from_tuples( + [(4, "Hello, World!"), (99, "some_new_string")], + names=["new_index1", "new_index2"], + ) + bf_result = ( + scalars_df_index.set_index(["rowindex_2", "string_col"]) + .reindex(index=new_index) + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index(["rowindex_2", "string_col"]).reindex( + index=new_index + ) + pandas.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + # Column Multi-index tests @@ -722,3 +807,76 @@ def test_is_monotonic_decreasing_extra(indexes): bf_result.index.is_monotonic_decreasing == pd_result.index.is_monotonic_decreasing ) + + +def test_column_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "string_col", "bool_col"] + multi_columns = pandas.MultiIndex.from_tuples( + zip(["a", "b", "a"], ["c", "d", "e"], ["f", "g", "f"]) + ) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.droplevel(1, axis=1).to_pandas() + pd_result = pd_df.droplevel(1, axis=1) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_column_multi_index_reindex(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "rowindex_2"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + new_index = pandas.MultiIndex.from_tuples( + [("z", "a"), ("a", "a")], names=["newname1", "newname2"] + ) + + bf_result = bf_df.reindex(columns=new_index).to_pandas() + + pd_result = pd_df.reindex(columns=new_index) + + # Pandas uses float64 as default for newly created empty column, bf uses Float64 + pd_result[("z", "a")] = pd_result[("z", "a")].astype(pandas.Float64Dtype()) + + pandas.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_column_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "string_col", "bool_col"] + multi_columns = pandas.MultiIndex.from_tuples( + zip(["a", "b", "a"], ["c", "d", "e"], ["f", "g", "f"]) + ) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.reorder_levels([-2, -1, 0], axis=1).to_pandas() + pd_result = pd_df.reorder_levels([-2, -1, 0], axis=1) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_column_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "string_col", "bool_col"] + multi_columns = pandas.MultiIndex.from_tuples( + zip(["a", "b", "a"], ["c", "d", "e"], ["f", "g", "f"]) + ) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.swaplevel(-3, -1, axis=1).to_pandas() + pd_result = pd_df.swaplevel(-3, -1, axis=1) + + pandas.testing.assert_frame_equal(bf_result, pd_result) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index e451d5c3a2c..a429c6551d6 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -209,3 +209,17 @@ def test_merge_series(scalars_dfs, merge_how): ) assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + +def test_cut(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = pd.cut(scalars_pandas_df["float64_col"], 5, labels=False) + bf_result = bpd.cut(scalars_df["float64_col"], 5, labels=False) + + # make sure the result is a supported dtype + assert bf_result.dtype == bpd.Int64Dtype() + + bf_result = bf_result.to_pandas() + pd_result = pd_result.astype("Int64") + pd.testing.assert_series_equal(bf_result, pd_result) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 07dc892ddc3..d3560540cc9 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -186,6 +186,54 @@ def test_fillna(scalars_dfs): ) +def test_series_replace_scalar_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = ( + scalars_df[col_name].replace("Hello, World!", "Howdy, Planet!").to_pandas() + ) + pd_result = scalars_pandas_df[col_name].replace("Hello, World!", "Howdy, Planet!") + + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + +def test_series_replace_regex_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = ( + scalars_df[col_name].replace("^H.l", "Howdy, Planet!", regex=True).to_pandas() + ) + pd_result = scalars_pandas_df[col_name].replace( + "^H.l", "Howdy, Planet!", regex=True + ) + + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + +def test_series_replace_list_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = ( + scalars_df[col_name] + .replace(["Hello, World!", "T"], "Howdy, Planet!") + .to_pandas() + ) + pd_result = scalars_pandas_df[col_name].replace( + ["Hello, World!", "T"], "Howdy, Planet!" + ) + + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + @pytest.mark.parametrize( ("ignore_index",), ( @@ -759,7 +807,6 @@ def test_isin_raise_error(scalars_df_index, scalars_pandas_df_index): ) def test_isin(scalars_dfs, col_name, test_set): scalars_df, scalars_pandas_df = scalars_dfs - print(type(scalars_pandas_df["datetime_col"].iloc[0])) bf_result = scalars_df[col_name].isin(test_set).to_pandas() pd_result = scalars_pandas_df[col_name].isin(test_set).astype("boolean") pd.testing.assert_series_equal( @@ -1506,6 +1553,28 @@ def test_shift(scalars_df_index, scalars_pandas_df_index): ) +def test_series_ffill(scalars_df_index, scalars_pandas_df_index): + col_name = "numeric_col" + bf_result = scalars_df_index[col_name].ffill(limit=1).to_pandas() + pd_result = scalars_pandas_df_index[col_name].ffill(limit=1) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_series_bfill(scalars_df_index, scalars_pandas_df_index): + col_name = "numeric_col" + bf_result = scalars_df_index[col_name].bfill(limit=2).to_pandas() + pd_result = scalars_pandas_df_index[col_name].bfill(limit=2) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + def test_cumsum_int(scalars_df_index, scalars_pandas_df_index): if pd.__version__.startswith("1."): pytest.skip("Series.cumsum NA mask are different in pandas 1.x.") @@ -1588,7 +1657,7 @@ def test_rank_with_nulls(scalars_df_index, scalars_pandas_df_index, na_option, m ("all",), ], ) -def test_nlargest(scalars_df_index, scalars_pandas_df_index, keep): +def test_series_nlargest(scalars_df_index, scalars_pandas_df_index, keep): col_name = "bool_col" bf_result = scalars_df_index[col_name].nlargest(4, keep=keep).to_pandas() pd_result = scalars_pandas_df_index[col_name].nlargest(4, keep=keep) @@ -1622,6 +1691,25 @@ def test_diff(scalars_df_index, scalars_pandas_df_index, periods): ) +@pytest.mark.parametrize( + ("periods",), + [ + (1,), + (2,), + (-1,), + ], +) +def test_series_pct_change(scalars_df_index, scalars_pandas_df_index, periods): + bf_result = scalars_df_index["int64_col"].pct_change(periods=periods).to_pandas() + # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA + pd_result = scalars_pandas_df_index["int64_col"].pct_change(periods=periods) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + @pytest.mark.parametrize( ("keep",), [ @@ -1630,7 +1718,7 @@ def test_diff(scalars_df_index, scalars_pandas_df_index, periods): ("all",), ], ) -def test_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): +def test_series_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): col_name = "bool_col" bf_result = scalars_df_index[col_name].nsmallest(2, keep=keep).to_pandas() pd_result = scalars_pandas_df_index[col_name].nsmallest(2, keep=keep) @@ -1853,6 +1941,91 @@ def test_series_add_suffix(scalars_df_index, scalars_pandas_df_index): ) +def test_series_filter_items(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("2.0") or pd.__version__.startswith("1."): + pytest.skip("pandas filter items behavior different pre-2.1") + bf_result = scalars_df_index["float64_col"].filter(items=[5, 1, 3]).to_pandas() + + pd_result = scalars_pandas_df_index["float64_col"].filter(items=[5, 1, 3]) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_series_filter_like(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.copy().set_index("string_col") + scalars_pandas_df_index = scalars_pandas_df_index.copy().set_index("string_col") + + bf_result = scalars_df_index["float64_col"].filter(like="ello").to_pandas() + + pd_result = scalars_pandas_df_index["float64_col"].filter(like="ello") + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_series_filter_regex(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.copy().set_index("string_col") + scalars_pandas_df_index = scalars_pandas_df_index.copy().set_index("string_col") + + bf_result = scalars_df_index["float64_col"].filter(regex="^[GH].*").to_pandas() + + pd_result = scalars_pandas_df_index["float64_col"].filter(regex="^[GH].*") + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_series_reindex(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["float64_col"].reindex(index=[5, 1, 3, 99, 1]).to_pandas() + ) + + pd_result = scalars_pandas_df_index["float64_col"].reindex(index=[5, 1, 3, 99, 1]) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + +def test_series_reindex_nonunique(scalars_df_index): + with pytest.raises(ValueError): + # int64_too is non-unique + scalars_df_index.set_index("int64_too")["float64_col"].reindex( + index=[5, 1, 3, 99, 1], validate=True + ) + + +def test_series_reindex_like(scalars_df_index, scalars_pandas_df_index): + bf_reindex_target = scalars_df_index["float64_col"].reindex(index=[5, 1, 3, 99, 1]) + bf_result = ( + scalars_df_index["int64_too"].reindex_like(bf_reindex_target).to_pandas() + ) + + pd_reindex_target = scalars_pandas_df_index["float64_col"].reindex( + index=[5, 1, 3, 99, 1] + ) + pd_result = scalars_pandas_df_index["int64_too"].reindex_like(pd_reindex_target) + + # Pandas uses int64 instead of Int64 (nullable) dtype. + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + def test_where_with_series(scalars_df_index, scalars_pandas_df_index): bf_result = ( scalars_df_index["int64_col"] diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py new file mode 100644 index 00000000000..8d4932a3c3c --- /dev/null +++ b/tests/unit/ml/test_golden_sql.py @@ -0,0 +1,47 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +import pytest_mock + +import bigframes +from bigframes.ml import linear_model +import bigframes.pandas as bpd + + +def test_linear_regression_default_fit(mocker: pytest_mock.MockerFixture): + mock_session = mock.create_autospec(spec=bigframes.Session) + + mock_X = mock.create_autospec(spec=bpd.DataFrame) + mock_X._get_block().expr._session = mock_session + + mock_y = mock.create_autospec(spec=bpd.DataFrame) + mock_y.columns.tolist.return_value = ["input_label_column"] + + mock_X.join(mock_y).sql = "input_dataframe_sql" + + # return values we don't care about, but need to provide to continue the program + mock_session._start_query.return_value = (None, mock.MagicMock()) + + mocker.patch( + "bigframes.ml.core._create_temp_model_name", return_value="temp_model_name" + ) + + model = linear_model.LinearRegression() + model.fit(mock_X, mock_y) + + mock_session._start_query.assert_called_once_with( + 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n INPUT_LABEL_COLS=["input_label_column"])\nAS input_dataframe_sql' + ) diff --git a/tests/unit/ml/test_pipeline.py b/tests/unit/ml/test_pipeline.py index 27706a1a07f..ed5c621b1df 100644 --- a/tests/unit/ml/test_pipeline.py +++ b/tests/unit/ml/test_pipeline.py @@ -18,38 +18,35 @@ import sklearn.pipeline as sklearn_pipeline # type: ignore import sklearn.preprocessing as sklearn_preprocessing # type: ignore -import bigframes.ml.compose -import bigframes.ml.linear_model -import bigframes.ml.pipeline -import bigframes.ml.preprocessing +from bigframes.ml import compose, forecasting, linear_model, pipeline, preprocessing def test_pipeline_repr(): - pipeline = bigframes.ml.pipeline.Pipeline( + pl = pipeline.Pipeline( [ ( "preproc", - bigframes.ml.compose.ColumnTransformer( + compose.ColumnTransformer( [ ( "onehot", - bigframes.ml.preprocessing.OneHotEncoder(), + preprocessing.OneHotEncoder(), "species", ), ( "scale", - bigframes.ml.preprocessing.StandardScaler(), + preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), ] ), ), - ("linreg", bigframes.ml.linear_model.LinearRegression()), + ("linreg", linear_model.LinearRegression()), ] ) assert ( - pipeline.__repr__() + pl.__repr__() == """Pipeline(steps=[('preproc', ColumnTransformer(transformers=[('onehot', OneHotEncoder(), 'species'), @@ -62,29 +59,29 @@ def test_pipeline_repr(): @pytest.mark.skipif(sklearn_pipeline is None, reason="requires sklearn") def test_pipeline_repr_matches_sklearn(): - bf_pipeline = bigframes.ml.pipeline.Pipeline( + bf_pl = pipeline.Pipeline( [ ( "preproc", - bigframes.ml.compose.ColumnTransformer( + compose.ColumnTransformer( [ ( "onehot", - bigframes.ml.preprocessing.OneHotEncoder(), + preprocessing.OneHotEncoder(), "species", ), ( "scale", - bigframes.ml.preprocessing.StandardScaler(), + preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), ] ), ), - ("linreg", bigframes.ml.linear_model.LinearRegression()), + ("linreg", linear_model.LinearRegression()), ] ) - sk_pipeline = sklearn_pipeline.Pipeline( + sk_pl = sklearn_pipeline.Pipeline( [ ( "preproc", @@ -107,4 +104,17 @@ def test_pipeline_repr_matches_sklearn(): ] ) - assert bf_pipeline.__repr__() == sk_pipeline.__repr__() + assert bf_pl.__repr__() == sk_pl.__repr__() + + +def test_pipeline_arima_plus_not_implemented(): + with pytest.raises(NotImplementedError): + pipeline.Pipeline( + [ + ( + "transform", + preprocessing.StandardScaler(), + ), + ("estimator", forecasting.ARIMAPlus()), + ] + ) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index e01638e22e1..8f3e0beb0e4 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -13,9 +13,12 @@ # limitations under the License. import ibis +import ibis.expr.types as ibis_types import pandas import bigframes.core as core +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops from . import resources @@ -46,6 +49,42 @@ def test_arrayvalue_constructor_from_ibis_table_adds_all_columns(): assert len(actual.columns) == 3 +def test_arrayvalue_with_get_column_type(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + col1_type = value.get_column_type("col1") + col2_type = value.get_column_type("col2") + col3_type = value.get_column_type("col3") + assert isinstance(col1_type, pandas.Int64Dtype) + assert isinstance(col2_type, pandas.StringDtype) + assert isinstance(col3_type, pandas.Float64Dtype) + + +def test_arrayvalue_with_get_column(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + col1 = value.get_column("col1") + assert isinstance(col1, ibis_types.Value) + assert col1.get_name() == "col1" + assert col1.type().is_int64() + + def test_arrayvalue_to_ibis_expr_with_projection(): value = resources.create_arrayvalue( pandas.DataFrame( @@ -69,3 +108,133 @@ def test_arrayvalue_to_ibis_expr_with_projection(): assert actual.columns[0] == "int64_col" assert actual.columns[1] == "literals" assert actual.columns[2] == "string_col" + + +def test_arrayvalues_to_ibis_expr_with_get_column(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.get_column("col1") + assert expr.get_name() == "col1" + assert expr.type().is_int64() + + +def test_arrayvalues_to_ibis_expr_with_concat(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.concat([value]) + actual = expr.to_ibis_expr() + assert len(actual.columns) == 3 + # TODO(ashleyxu, b/299631930): test out the union expression + assert actual.columns[0] == "column_0" + assert actual.columns[1] == "column_1" + assert actual.columns[2] == "column_2" + + +def test_arrayvalues_to_ibis_expr_with_project_unary_op(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.project_unary_op("col1", ops.AsTypeOp("string")) + assert value.columns[0].type().is_int64() + assert expr.columns[0].type().is_string() + + +def test_arrayvalues_to_ibis_expr_with_project_binary_op(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": [0.2, 0.3, 0.4], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.project_binary_op("col2", "col3", ops.add_op, "col4") + assert expr.columns[3].type().is_float64() + actual = expr.to_ibis_expr() + assert len(expr.columns) == 4 + assert actual.columns[3] == "col4" + + +def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": [0.2, 0.3, 0.4], + "col3": [True, False, False], + "col4": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.project_ternary_op("col2", "col3", "col4", ops.where_op, "col5") + assert expr.columns[4].type().is_float64() + actual = expr.to_ibis_expr() + assert len(expr.columns) == 5 + assert actual.columns[4] == "col5" + + +def test_arrayvalue_to_ibis_expr_with_aggregate(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.aggregate( + aggregations=(("col1", agg_ops.sum_op, "col4"),), + by_column_ids=["col1"], + dropna=False, + ) + actual = expr.to_ibis_expr() + assert len(expr.columns) == 2 + assert actual.columns[0] == "col1" + assert actual.columns[1] == "col4" + assert expr.columns[1].type().is_int64() + + +def test_arrayvalue_to_ibis_expr_with_corr_aggregate(): + value = resources.create_arrayvalue( + pandas.DataFrame( + { + "col1": [1, 2, 3], + "col2": ["a", "b", "c"], + "col3": [0.1, 0.2, 0.3], + } + ), + total_ordering_columns=["col1"], + ) + expr = value.corr_aggregate(corr_aggregations=[("col1", "col3", "col4")]) + actual = expr.to_ibis_expr() + assert len(expr.columns) == 1 + assert actual.columns[0] == "col4" + assert expr.columns[0].type().is_float64() diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py index d209284ab7d..a4e61ca0f91 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -12,8 +12,20 @@ def _approx_quantiles(translator, op: vendored_ibis_ops.ApproximateMultiQuantile return f"APPROX_QUANTILES({arg}, {num_bins})" +def _first_non_null_value(translator, op: vendored_ibis_ops.FirstNonNullValue): + arg = translator.translate(op.arg) + return f"FIRST_VALUE({arg} IGNORE NULLS)" + + +def _last_non_null_value(translator, op: vendored_ibis_ops.LastNonNullValue): + arg = translator.translate(op.arg) + return f"LAST_VALUE({arg} IGNORE NULLS)" + + patched_ops = { vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, + vendored_ibis_ops.FirstNonNullValue: _first_non_null_value, + vendored_ibis_ops.LastNonNullValue: _last_non_null_value, } OPERATION_REGISTRY.update(patched_ops) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py index f3ab753a3b1..1612d9c12e5 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py @@ -1,4 +1,5 @@ # Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/__init__.py from __future__ import annotations +from third_party.bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F403 from third_party.bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F403 diff --git a/third_party/bigframes_vendored/ibis/expr/operations/analytic.py b/third_party/bigframes_vendored/ibis/expr/operations/analytic.py new file mode 100644 index 00000000000..038987cac93 --- /dev/null +++ b/third_party/bigframes_vendored/ibis/expr/operations/analytic.py @@ -0,0 +1,26 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/analytic.py + +from __future__ import annotations + +from ibis.expr.operations.analytic import Analytic +import ibis.expr.rules as rlz + + +class FirstNonNullValue(Analytic): + """Retrieve the first element.""" + + arg = rlz.column(rlz.any) + output_dtype = rlz.dtype_like("arg") + + +class LastNonNullValue(Analytic): + """Retrieve the last element.""" + + arg = rlz.column(rlz.any) + output_dtype = rlz.dtype_like("arg") + + +__all__ = [ + "FirstNonNullValue", + "LastNonNullValue", +] diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 8c81b23b6ca..113c6547a0e 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -13,7 +13,7 @@ from typing import Iterable, Literal, Mapping, Optional, Sequence, Union -import numpy +import numpy as np from bigframes import constants from third_party.bigframes_vendored.pandas.core.generic import NDFrame @@ -56,7 +56,7 @@ def axes(self) -> list: return [self.index, self.columns] @property - def values(self) -> numpy.ndarray: + def values(self) -> np.ndarray: """Return the values of DataFrame in the form of a NumPy array. Args: @@ -72,9 +72,7 @@ def values(self) -> numpy.ndarray: # ---------------------------------------------------------------------- # IO methods (to / from other formats) - def to_numpy( - self, dtype=None, copy=False, na_value=None, **kwargs - ) -> numpy.ndarray: + def to_numpy(self, dtype=None, copy=False, na_value=None, **kwargs) -> np.ndarray: """ Convert the DataFrame to a NumPy array. @@ -154,6 +152,250 @@ def to_parquet( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def to_dict( + self, + orient: Literal[ + "dict", "list", "series", "split", "tight", "records", "index" + ] = "dict", + into: type[dict] = dict, + **kwargs, + ) -> dict | list[dict]: + """ + Convert the DataFrame to a dictionary. + + The type of the key-value pairs can be customized with the parameters + (see below). + + Args: + orient (str {'dict', 'list', 'series', 'split', 'tight', 'records', 'index'}): + Determines the type of the values of the dictionary. + 'dict' (default) : dict like {column -> {index -> value}}. + 'list' : dict like {column -> [values]}. + 'series' : dict like {column -> Series(values)}. + split' : dict like {'index' -> [index], 'columns' -> [columns], 'data' -> [values]}. + 'tight' : dict like {'index' -> [index], 'columns' -> [columns], 'data' -> [values], + 'index_names' -> [index.names], 'column_names' -> [column.names]}. + 'records' : list like [{column -> value}, ... , {column -> value}]. + 'index' : dict like {index -> {column -> value}}. + into (class, default dict): + The collections.abc.Mapping subclass used for all Mappings + in the return value. Can be the actual class or an empty + instance of the mapping type you want. If you want a + collections.defaultdict, you must pass it initialized. + + index (bool, default True): + Whether to include the index item (and index_names item if `orient` + is 'tight') in the returned dictionary. Can only be ``False`` + when `orient` is 'split' or 'tight'. + + Returns: + dict or list of dict: Return a collections.abc.Mapping object representing the DataFrame. + The resulting transformation depends on the `orient` parameter. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_excel(self, excel_writer, sheet_name: str = "Sheet1", **kwargs) -> None: + """ + Write DataFrame to an Excel sheet. + + To write a single DataFrame to an Excel .xlsx file it is only necessary to + specify a target file name. To write to multiple sheets it is necessary to + create an `ExcelWriter` object with a target file name, and specify a sheet + in the file to write to. + + Multiple sheets may be written to by specifying unique `sheet_name`. + With all data written to the file it is necessary to save the changes. + Note that creating an `ExcelWriter` object with a file name that already + exists will result in the contents of the existing file being erased. + + Args: + excel_writer (path-like, file-like, or ExcelWriter object): + File path or existing ExcelWriter. + sheet_name (str, default 'Sheet1'): + Name of sheet which will contain DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_latex( + self, buf=None, columns=None, header=True, index=True, **kwargs + ) -> str | None: + r""" + Render object to a LaTeX tabular, longtable, or nested table. + + Requires ``\usepackage{{booktabs}}``. The output can be copy/pasted + into a main LaTeX document or read from an external file + with ``\input{{table.tex}}``. + + Args: + buf (str, Path or StringIO-like, optional, default None): + Buffer to write to. If None, the output is returned as a string. + columns (list of label, optional): + The subset of columns to write. Writes all columns by default. + header (bool or list of str, default True): + Write out the column names. If a list of strings is given, + it is assumed to be aliases for the column names. + index (bool, default True): + Write row names (index). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_records( + self, index: bool = True, column_dtypes=None, index_dtypes=None + ) -> np.recarray: + """ + Convert DataFrame to a NumPy record array. + + Index will be included as the first field of the record array if + requested. + + Args: + index (bool, default True): + Include index in resulting record array, stored in 'index' + field or using the index label, if set. + column_dtypes (str, type, dict, default None): + If a string or type, the data type to store all columns. If + a dictionary, a mapping of column names and indices (zero-indexed) + to specific data types. + index_dtypes (str, type, dict, default None): + If a string or type, the data type to store all index levels. If + a dictionary, a mapping of index level names and indices + (zero-indexed) to specific data types. + + This mapping is applied only if `index=True`. + + Returns: + np.recarray: NumPy ndarray with the DataFrame labels as fields and each row + of the DataFrame as entries. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_string( + self, + buf=None, + columns: Sequence[str] | None = None, + col_space=None, + header: bool | Sequence[str] = True, + index: bool = True, + na_rep: str = "NaN", + formatters=None, + float_format=None, + sparsify: bool | None = None, + index_names: bool = True, + justify: str | None = None, + max_rows: int | None = None, + max_cols: int | None = None, + show_dimensions: bool = False, + decimal: str = ".", + line_width: int | None = None, + min_rows: int | None = None, + max_colwidth: int | None = None, + encoding: str | None = None, + ): + """Render a DataFrame to a console-friendly tabular output. + + Args: + buf (str, Path or StringIO-like, optional, default None): + Buffer to write to. If None, the output is returned as a string. + columns (sequence, optional, default None): + The subset of columns to write. Writes all columns by default. + col_space (int, list or dict of int, optional): + The minimum width of each column. + header (bool or sequence, optional): + Write out the column names. If a list of strings is given, it is assumed to be aliases for the column names. + index (bool, optional, default True): + Whether to print index (row) labels. + na_rep (str, optional, default 'NaN'): + String representation of NAN to use. + formatters (list, tuple or dict of one-param. functions, optional): + Formatter functions to apply to columns' elements by position or + name. + The result of each function must be a unicode string. + List/tuple must be of length equal to the number of columns. + float_format (one-parameter function, optional, default None): + Formatter function to apply to columns' elements if they are + floats. The result of this function must be a unicode string. + sparsify (bool, optional, default True): + Set to False for a DataFrame with a hierarchical index to print + every multiindex key at each row. + index_names (bool, optional, default True): + Prints the names of the indexes. + justify (str, default None): + How to justify the column labels. If None uses the option from + the print configuration (controlled by set_option), 'right' out + of the box. Valid values are, 'left', 'right', 'center', 'justify', + 'justify-all', 'start', 'end', 'inherit', 'match-parent', 'initial', + 'unset'. + max_rows (int, optional): + Maximum number of rows to display in the console. + min_rows (int, optional): + The number of rows to display in the console in a truncated repr + (when number of rows is above `max_rows`). + max_cols (int, optional): + Maximum number of columns to display in the console. + show_dimensions (bool, default False): + Display DataFrame dimensions (number of rows by number of columns). + decimal (str, default '.'): + Character recognized as decimal separator, e.g. ',' in Europe. + line_width (int, optional): + Width to wrap a line in characters. + max_colwidth (int, optional): + Max width to truncate each column in characters. By default, no limit. + encoding (str, default "utf-8"): + Set character encoding. + + Returns: + str or None: If buf is None, returns the result as a string. Otherwise returns + None. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_markdown( + self, + buf=None, + mode: str = "wt", + index: bool = True, + **kwargs, + ): + """Print DataFrame in Markdown-friendly format. + + Args: + buf (str, Path or StringIO-like, optional, default None): + Buffer to write to. If None, the output is returned as a string. + mode (str, optional): + Mode in which file is opened. + index (bool, optional, default True): + Add index (row) labels. + **kwargs + These parameters will be passed to `tabulate `_. + + Returns: + DataFrame in Markdown-friendly format. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_pickle(self, path, **kwargs) -> None: + """Pickle (serialize) object to file. + + Args: + path (str): + File path where the pickled object will be stored. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_orc(self, path=None, **kwargs) -> bytes | None: + """ + Write a DataFrame to the ORC format. + + Args: + path (str, file-like object or None, default None): + If a string, it will be used as Root Directory path + when writing a partitioned dataset. By file-like object, + we refer to objects with a write() method, such as a file handle + (e.g. via builtin open function). If path is None, + a bytes object is returned. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # Unsorted @@ -185,6 +427,53 @@ def assign(self, **kwargs) -> DataFrame: # ---------------------------------------------------------------------- # Reindexing and alignment + def reindex( + self, + labels=None, + *, + index=None, + columns=None, + axis=None, + ): + """Conform DataFrame to new index with optional filling logic. + + Places NA in locations having no value in the previous index. A new object + is produced. + + Args: + labels (array-like, optional): + New labels / index to conform the axis specified by 'axis' to. + index (array-like, optional): + New labels for the index. Preferably an Index object to avoid + duplicating data. + columns (array-like, optional): + New labels for the columns. Preferably an Index object to avoid + duplicating data. + axis (int or str, optional): + Axis to target. Can be either the axis name ('index', 'columns') + or number (0, 1). + Returns: + DataFrame: DataFrame with changed index. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def reindex_like(self, other): + """Return an object with matching indices as other object. + + Conform the object to the same index on all axes. Optional + filling logic, placing Null in locations having no value + in the previous index. + + Args: + other (Object of the same data type): + Its row and column indices are used to define the new indices + of this object. + + Returns: + Series or DataFrame: Same type as caller, but with changed indices on each axis. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def drop( self, labels=None, *, axis=0, index=None, columns=None, level=None ) -> DataFrame | None: @@ -276,7 +565,9 @@ def set_index( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def reorder_levels(self, order: Sequence[int | str]) -> DataFrame: + def reorder_levels( + self, order: Sequence[int | str], axis: str | int = 0 + ) -> DataFrame: """ Rearrange index levels using input order. May not drop or duplicate levels. @@ -284,13 +575,33 @@ def reorder_levels(self, order: Sequence[int | str]) -> DataFrame: order (list of int or list of str): List representing new level order. Reference level by number (position) or by key (label). + axis ({0 or 'index', 1 or 'columns'}, default 0): + Where to reorder levels. Returns: DataFrame: DataFrame of rearranged index. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def droplevel(self, level): + def swaplevel(self, i, j, axis: str | int = 0) -> DataFrame: + """ + Swap levels i and j in a :class:`MultiIndex`. + + Default is to swap the two innermost levels of the index. + + Args: + i, j (int or str): + Levels of the indices to be swapped. Can pass level name as string. + axis ({0 or 'index', 1 or 'columns'}, default 0): + The axis to swap levels on. 0 or 'index' for row-wise, 1 or + 'columns' for column-wise. + + Returns: + DataFrame: DataFrame with levels swapped in MultiIndex. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def droplevel(self, level, axis: str | int = 0): """ Return DataFrame with requested index / column level(s) removed. @@ -299,6 +610,11 @@ def droplevel(self, level): If a string is given, must be the name of a level If list-like, elements must be names or positional indexes of levels. + axis ({0 or 'index', 1 or 'columns'}, default 0): + Axis along which the level(s) is removed: + + * 0 or 'index': remove level(s) in column. + * 1 or 'columns': remove level(s) in row. Returns: DataFrame: DataFrame with requested index / column level(s) removed. """ @@ -889,6 +1205,54 @@ def rpow(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def combine( + self, other, func, fill_value=None, overwrite: bool = True + ) -> DataFrame: + """Perform column-wise combine with another DataFrame. + + Combines a DataFrame with `other` DataFrame using `func` + to element-wise combine columns. The row and column indexes of the + resulting DataFrame will be the union of the two. + + Args: + other (DataFrame): + The DataFrame to merge column-wise. + func (function): + Function that takes two series as inputs and return a Series or a + scalar. Used to merge the two dataframes column by columns. + fill_value (scalar value, default None): + The value to fill NaNs with prior to passing any column to the + merge func. + overwrite (bool, default True): + If True, columns in `self` that do not exist in `other` will be + overwritten with NaNs. + + Returns: + DataFrame: Combination of the provided DataFrames. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def combine_first(self, other) -> DataFrame: + """ + Update null elements with value in the same location in `other`. + + Combine two DataFrame objects by filling null values in one DataFrame + with non-null values from other DataFrame. The row and column indexes + of the resulting DataFrame will be the union of the two. The resulting + dataframe contains the 'first' dataframe values and overrides the + second one values where both first.loc[index, col] and + second.loc[index, col] are not missing values, upon calling + first.combine_first(second). + + Args: + other (DataFrame): + Provided DataFrame to use to fill null values. + + Returns: + DataFrame: The result of combining the provided DataFrame with the other object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # Data reshaping @@ -1191,6 +1555,20 @@ def var(self, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def skew(self, *, numeric_only: bool = False): + """Return unbiased skew over requested axis. + + Normalized by N-1. + + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def std(self, *, numeric_only: bool = False): """Return sample standard deviation over requested axis. @@ -1222,6 +1600,76 @@ def count(self, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def nlargest(self, n: int, columns, keep: str = "first"): + """ + Return the first `n` rows ordered by `columns` in descending order. + + Return the first `n` rows with the largest values in `columns`, in + descending order. The columns that are not specified are returned as + well, but not used for ordering. + + This method is equivalent to + ``df.sort_values(columns, ascending=False).head(n)``, but more + performant. + + Args: + n (int): + Number of rows to return. + columns (label or list of labels): + Column label(s) to order by. + keep ({'first', 'last', 'all'}, default 'first'): + Where there are duplicate values: + + - ``first`` : prioritize the first occurrence(s) + - ``last`` : prioritize the last occurrence(s) + - ``all`` : do not drop any duplicates, even it means + selecting more than `n` items. + + Returns: + DataFrame: The first `n` rows ordered by the given columns in descending order. + + .. note:: + This function cannot be used with all column types. For example, when + specifying columns with `object` or `category` dtypes, ``TypeError`` is + raised. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def nsmallest(self, n: int, columns, keep: str = "first"): + """ + Return the first `n` rows ordered by `columns` in ascending order. + + Return the first `n` rows with the smallest values in `columns`, in + ascending order. The columns that are not specified are returned as + well, but not used for ordering. + + This method is equivalent to + ``df.sort_values(columns, ascending=True).head(n)``, but more + performant. + + Args: + n (int): + Number of rows to return. + columns (label or list of labels): + Column label(s) to order by. + keep ({'first', 'last', 'all'}, default 'first'): + Where there are duplicate values: + + - ``first`` : prioritize the first occurrence(s) + - ``last`` : prioritize the last occurrence(s) + - ``all`` : do not drop any duplicates, even it means + selecting more than `n` items. + + Returns: + DataFrame: The first `n` rows ordered by the given columns in ascending order. + + .. note:: + This function cannot be used with all column types. For example, when + specifying columns with `object` or `category` dtypes, ``TypeError`` is + raised. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def nunique(self): """ Count number of distinct elements in specified axis. @@ -1271,6 +1719,25 @@ def cumprod(self) -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def diff( + self, + periods: int = 1, + ) -> NDFrame: + """First discrete difference of element. + + Calculates the difference of a DataFrame element compared with another + element in the DataFrame (default is element in previous row). + + Args: + periods (int, default 1): + Periods to shift for calculating difference, accepts negative + values. + + Returns: + bigframes.dataframe.DataFrame: First differences of the Series. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def agg(self, func): """ Aggregate using one or more operations over the specified axis. diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 56d3b2434f0..7d496891b0b 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -335,6 +335,41 @@ def copy(self): # ---------------------------------------------------------------------- # Action Methods + def ffill(self, *, limit: Optional[int] = None): + """Fill NA/NaN values by propagating the last valid observation to next valid. + + Args: + limit : int, default None + If method is specified, this is the maximum number of consecutive + NaN values to forward/backward fill. In other words, if there is + a gap with more than this number of consecutive NaNs, it will only + be partially filled. If method is not specified, this is the + maximum number of entries along the entire axis where NaNs will be + filled. Must be greater than 0 if not None. + + + Returns: + Series/DataFrame or None: Object with missing values filled. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def bfill(self, *, limit: Optional[int] = None): + """Fill NA/NaN values by using the next valid observation to fill the gap. + + Args: + limit : int, default None + If method is specified, this is the maximum number of consecutive + NaN values to forward/backward fill. In other words, if there is + a gap with more than this number of consecutive NaNs, it will only + be partially filled. If method is not specified, this is the + maximum number of entries along the entire axis where NaNs will be + filled. Must be greater than 0 if not None. + + Returns: + Series/DataFrame or None: Object with missing values filled. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def isna(self) -> NDFrame: """Detect missing values. @@ -367,6 +402,36 @@ def notna(self) -> NDFrame: notnull = notna + def filter( + self, + items=None, + like: str | None = None, + regex: str | None = None, + axis=None, + ) -> NDFrame: + """ + Subset the dataframe rows or columns according to the specified index labels. + + Note that this routine does not filter a dataframe on its + contents. The filter is applied to the labels of the index. + + Args: + items (list-like): + Keep labels from axis which are in items. + like (str): + Keep labels from axis for which "like in label == True". + regex (str (regular expression)): + Keep labels from axis for which re.search(regex, label) == True. + axis ({0 or 'index', 1 or 'columns', None}, default None): + The axis to filter on, expressed either as an index (int) + or axis name (str). By default this is the info axis, 'columns' for + DataFrame. For `Series` this parameter is unused and defaults to `None`. + + Returns: + same type as input object + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def shift( self, periods: int = 1, @@ -384,6 +449,30 @@ def shift( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def pct_change(self, periods: int = 1): + """ + Fractional change between the current and a prior element. + + Computes the fractional change from the immediately previous row by + default. This is useful in comparing the fraction of change in a time + series of elements. + + .. note:: + + Despite the name of this method, it calculates fractional change + (also known as per unit change or relative change) and not + percentage change. If you need the percentage change, multiply + these values by 100. + + Args: + periods (int, default 1): + Periods to shift for forming percent change. + + Returns: + Series or DataFrame: The same type as the calling object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rank( self, axis=0, diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 9271da8a5ea..7849a3afd52 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -124,6 +124,26 @@ def var( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def skew( + self, + *, + numeric_only: bool = False, + ): + """ + Return unbiased skew within groups. + + Normalized by N-1. + + Args: + numeric_only (bool, default False): + Include only `float`, `int` or `boolean` data. + + Returns: + Series or DataFrame + Variance of values within each group. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def sum( self, numeric_only: bool = False, diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index 404a99809c0..864007b7749 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -21,6 +21,16 @@ def shape(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def nlevels(self) -> int: + """Number of levels.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def is_unique(self) -> bool: + """Return if the index has unique values.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def to_numpy(self, dtype): """ A NumPy ndarray representing the values in this Series or Index. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 76fb46a7003..c6dd973372c 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -758,6 +758,41 @@ def groupby( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def reindex(self, index=None): + """ + Conform Series to new index with optional filling logic. + + Places NA/NaN in locations having no value in the previous index. A new object + is produced unless the new index is equivalent to the current one and + ``copy=False``. + + Args: + index (array-like, optional): + New labels for the index. Preferably an Index object to avoid + duplicating data. + + Returns: + Series: Series with changed index. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def reindex_like(self, other): + """Return an object with matching indices as other object. + + Conform the object to the same index on all axes. Optional + filling logic, placing Null in locations having no value + in the previous index. + + Args: + other (Object of the same data type): + Its row and column indices are used to define the new indices + of this object. + + Returns: + Series or DataFrame: Same type as caller, but with changed indices on each axis. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def drop( self, labels=None, *, axis=0, index=None, columns=None, level=None ) -> Series | None: @@ -790,7 +825,7 @@ def drop( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def reorder_levels(self, order: Sequence) -> Series: + def reorder_levels(self, order: Sequence, axis) -> Series: """ Rearrange index levels using input order. @@ -800,12 +835,31 @@ def reorder_levels(self, order: Sequence) -> Series: order (list of int representing new level order): Reference level by number or key. + axis ({0 or 'index', 1 or 'columns'}, default 0): + For `Series` this parameter is unused and defaults to 0. + + Returns: type of caller (new object) """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def droplevel(self, level): + def swaplevel(self, i, j): + """ + Swap levels i and j in a `MultiIndex`. + + Default is to swap the two innermost levels of the index. + + Args: + i, j (int or str): + Levels of the indices to be swapped. Can pass level name as string. + + Returns: + Series: Series with levels swapped in MultiIndex + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def droplevel(self, level, axis): """ Return Series with requested index / column level(s) removed. @@ -815,6 +869,9 @@ def droplevel(self, level): If list-like, elements must be names or positional indexes of levels. + axis ({0 or 'index', 1 or 'columns'}, default 0): + For `Series` this parameter is unused and defaults to 0. + Returns: Series with requested index / column level(s) removed. """ @@ -836,6 +893,69 @@ def fillna( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def replace( + self, + to_replace, + value=None, + ) -> Series | None: + """ + Replace values given in `to_replace` with `value`. + + Values of the Series/DataFrame are replaced with other values dynamically. + This differs from updating with ``.loc`` or ``.iloc``, which require + you to specify a location to update with some value. + + Args: + to_replace (str, regex, list, int, float or None): + How to find the values that will be replaced. + + * numeric, str or regex: + + - numeric: numeric values equal to `to_replace` will be + replaced with `value` + - str: string exactly matching `to_replace` will be replaced + with `value` + - regex: regexs matching `to_replace` will be replaced with + `value` + + * list of str, regex, or numeric: + + - First, if `to_replace` and `value` are both lists, they + **must** be the same length. + - Second, if ``regex=True`` then all of the strings in **both** + lists will be interpreted as regexs otherwise they will match + directly. This doesn't matter much for `value` since there + are only a few possible substitution regexes you can use. + - str, regex and numeric rules apply as above. + + value (scalar, default None): + Value to replace any values matching `to_replace` with. + For a DataFrame a dict of values can be used to specify which + value to use for each column (columns not in the dict will not be + filled). Regular expressions, strings and lists or dicts of such + objects are also allowed. + regex (bool, default False): + Whether to interpret `to_replace` and/or `value` as regular + expressions. If this is ``True`` then `to_replace` *must* be a + string. + + Returns: + Series/DataFrame: Object after replacement. + + Raises: + TypeError: + * If `to_replace` is not a scalar, array-like, ``dict``, or ``None`` + * If `to_replace` is a ``dict`` and `value` is not a ``list``, + ``dict``, ``ndarray``, or ``Series`` + * If `to_replace` is ``None`` and `regex` is not compilable + into a regular expression or is a list, dict, ndarray, or + Series. + * When replacing multiple ``bool`` or ``datetime64`` objects and + the arguments to `to_replace` does not match the type of the + value being replaced + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def dropna(self, *, axis=0, inplace: bool = False, how=None) -> Series: """ Return a new Series with missing values removed. diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index 847ad06f750..42868ce51f2 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -144,6 +144,7 @@ def fit_transform(self, X, y=None): bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_features_new) Transformed DataFrame. """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) class MetaEstimatorMixin: diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index ff1c04edbe3..ece62dc1470 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -49,7 +49,6 @@ def fit( self, X, y=None, - transforms: Optional[List[str]] = None, ): """Compute k-means clustering. @@ -58,10 +57,6 @@ def fit( DataFrame of shape (n_samples, n_features). Training data. y (default None): Not used, present here for API consistency by convention. - transforms (Optional[List[str]], default None): - Do not use. Internal param to be deprecated. - Use bigframes.ml.pipeline instead. - Returns: KMeans: Fitted Estimator. diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 85feab00245..97fee5a501e 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -49,10 +49,6 @@ def fit(self, X, y=None): y (default None): Ignored. - transforms (Optional[List[str]], default None): - Do not use. Internal param to be deprecated. - Use bigframes.ml.pipeline instead. - Returns: PCA: Fitted estimator. """ diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py index 79224a772d6..6be41bf9aa5 100644 --- a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -54,10 +54,6 @@ def fit(self, X, y): Series or DataFrame of shape (n_samples,) or (n_samples, n_targets). Target values. Will be cast to X's dtype if necessary. - transforms (Optional[List[str]], default None): - Do not use. Internal param to be deprecated. - Use bigframes.ml.pipeline instead. - Returns: Fitted Estimator. diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index 8141da4e3bc..81b4fca1572 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -74,7 +74,6 @@ def fit( self, X, y, - transforms: Optional[List[str]] = None, ): """Fit linear model. @@ -86,10 +85,6 @@ def fit( Series or DataFrame of shape (n_samples,) or (n_samples, n_targets). Target values. Will be cast to X's dtype if necessary. - transforms (Optional[List[str]], default None): - Do not use. Internal param to be deprecated. - Use bigframes.ml.pipeline instead. - Returns: LinearRegression: Fitted Estimator. """ diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index a06035eef66..133dc4498e4 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -37,7 +37,6 @@ def fit( self, X, y, - transforms: Optional[List[str]] = None, ): """Fit the model according to the given training data. @@ -50,10 +49,6 @@ def fit( y (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples,). Target vector relative to X. - transforms (Optional[List[str]], default None): - Do not use. Internal param to be deprecated. - Use bigframes.ml.pipeline instead. - Returns: LogisticRegression: Fitted Estimator. diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py index d0130434678..89981e34c03 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -8,10 +8,10 @@ # License: BSD 3 clause from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator +from third_party.bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin -class StandardScaler(BaseEstimator): +class StandardScaler(BaseEstimator, TransformerMixin): """Standardize features by removing the mean and scaling to unit variance. The standard score of a sample `x` is calculated as:z = (x - u) / s @@ -28,30 +28,23 @@ class StandardScaler(BaseEstimator): machine learning estimators: they might behave badly if the individual features do not more or less look like standard normally distributed data (e.g. Gaussian with 0 mean and unit variance). - """ - - def fit(self, X): - """Compute the mean and std to be used for later scaling. - Examples: + Examples: .. code-block:: from bigframes.ml.preprocessing import StandardScaler + import bigframes.pandas as bpd - enc = StandardScaler() - X = [['Male', 1], ['Female', 3], ['Female', 2]] - enc.fit(X) - - Examples: - - .. code-block:: - - from bigframes.ml import StandardScaler + scaler = StandardScaler() + data = bpd.DataFrame({"a": [0, 0, 1, 1], "b":[0, 0, 1, 1]}) + scaler.fit(data) + print(scaler.transform(data)) + print(scaler.transform(bpd.DataFrame({"a": [2], "b":[2]}))) + """ - enc = StandardScaler() - X = [['Male', 1], ['Female', 3], ['Female', 2]] - enc.fit(X) + def fit(self, X): + """Compute the mean and std to be used for later scaling. Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index b1cf17e539d..b0f0df8e157 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -37,12 +37,8 @@ class OneHotEncoder(BaseEstimator): when considering infrequent categories. If there are infrequent categories, max_categories includes the category representing the infrequent categories along with the frequent categories. Default None, set limit to 1,000,000. - """ - - def fit(self, X): - """Fit OneHotEncoder to X. - Examples: + Examples: Given a dataset with two features, we let the encoder find the unique values per feature and transform the data to a binary one-hot encoding. @@ -50,10 +46,16 @@ def fit(self, X): .. code-block:: from bigframes.ml.preprocessing import OneHotEncoder + import bigframes.pandas as bpd enc = OneHotEncoder() - X = [['Male', 1], ['Female', 3], ['Female', 2]] + X = bpd.DataFrame({"a": ["Male", "Female", "Female"], "b": ["1", "3", "2"]}) enc.fit(X) + print(enc.transform(bpd.DataFrame({"a": ["Female", "Male"], "b": ["1", "4"]}))) + """ + + def fit(self, X): + """Fit OneHotEncoder to X. Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index 620c87fa3db..b7b43b85a3e 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -37,10 +37,6 @@ def fit(self, X, y): DataFrame of shape (n_samples,) or (n_samples, n_targets). Target values. Will be cast to X's dtype if necessary. - transforms (Optional[List[str]], default None): - Do not use. Internal param to be deprecated. - Use bigframes.ml.pipeline instead. - Returns: XGBModel: Fitted Estimator. """ From 94779edca7240a8893498ca537266bc628c9bba3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 09:13:43 -0500 Subject: [PATCH 020/921] chore(main): release 0.4.0 (#28) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa736877f4c..de2edcf31e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,49 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.4.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.3.2...v0.4.0) (2023-09-16) + + +### Features + +* Add `axis` parameter to `droplevel` and `reorder_levels` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `bfill` and `ffill` to `DataFrame` and `Series` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `DataFrame.combine` and `DataFrame.combine_first` ([#27](https://github.com/googleapis/python-bigquery-dataframes/issues/27)) ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `DataFrame.nlargest`, `nsmallest` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `DataFrame.pct_change` and `Series.pct_change` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `DataFrame.skew` and `GroupBy.skew` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `DataFrame.to_dict`, `to_excel`, `to_latex`, `to_records`, `to_string`, `to_markdown`, `to_pickle`, `to_orc` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `diff` method to `DataFrame` and `GroupBy` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `filter` and `reindex` to `Series` and `DataFrame` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `reindex_like` to `DataFrame` and `Series` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add `swaplevel` to `DataFrame` and `Series` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add partial support for `Sereies.replace` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Support `DataFrame.loc[bool_series, column] = scalar` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Support a persistent `name` in `remote_function` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) + + +### Bug Fixes + +* `remote_function` uses same credentials as other APIs ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Add type hints to models ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Raise error when ARIMAPlus is used with Pipeline ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Remove `transforms` parameter in `model.fit` (**breaking change**) ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Support column joins with "None indexer" ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Use for literals `Int64Dtype` in `cut` ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Use lowercase strings for parameter literals in `bigframes.ml` (**breaking change**) ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) + + +### Performance Improvements + +* `bigframes-api` label to I/O query jobs ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) + + +### Documentation + +* Document possible parameter values for PaLM2TextGenerator ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Document region logic in README ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) +* Fix OneHotEncoder sample ([7c6b0dd](https://github.com/googleapis/python-bigquery-dataframes/commit/7c6b0dd2f99139c8830e762201a45b28486532ff)) + ## [0.3.2](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.3.1...v0.3.2) (2023-09-06) diff --git a/bigframes/version.py b/bigframes/version.py index ef4f01f6294..65b984a0d73 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.2" +__version__ = "0.4.0" From bbbd21ea0d8c5fa13ba66877ce28d20247884afe Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 18 Sep 2023 16:38:16 +0000 Subject: [PATCH 021/921] chore: use correct SCM name for release trigger (#29) Closes #19 --- .github/release-trigger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml index b0a6cadefca..4fbd4aa427b 100644 --- a/.github/release-trigger.yml +++ b/.github/release-trigger.yml @@ -1,2 +1,2 @@ enabled: true -multiScmName: bigframes +multiScmName: python-bigquery-dataframes From 69e51a67fb4aba9001fd5ea2b25cfcd6a6fdb80f Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 18 Sep 2023 23:12:13 +0000 Subject: [PATCH 022/921] chore: enforce use of conventional commits (#31) This will prevent accidental merging of commits that release-please can't handle. --- .github/sync-repo-settings.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index b7dae76ba3b..cfa62f787c9 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -7,6 +7,7 @@ branchProtectionRules: requiresCodeOwnerReviews: true requiresStrictStatusChecks: true requiredStatusCheckContexts: + - 'conventionalcommits.org' - 'cla/google' - 'OwlBot Post Processor' - 'docs' From c1900c29a44199d5d8d036d6d842b4f00448fa79 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 20 Sep 2023 16:26:56 +0000 Subject: [PATCH 023/921] chore: sync internal changes to GitHub (#34) feat: support `optimize_strategy` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `l2_reg` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `max_iterations` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `learn_rate_strategy` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `early_stop` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `min_rel_progress` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `ls_init_learn_rate` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `calculate_p_values` parameter in `bigframes.ml.linear_model.LinearRegression` feat: support `enable_global_explain` parameter in `bigframes.ml.linear_model.LinearRegression` test: add golden SQL test for logistic model test: extend ml golden sql test linear_reg docs: link to Remote Functions code samples from README and API reference feat: support `df[column_name] = df_only_one_column` feat: add `DataFrame.rolling` and `DataFrame.expanding` methods feat: add `DataFrame.kurtosis` / `DF.kurt` method feat: support `class_weights="balanced"` in `LogisticRegression` model --- README.rst | 4 +- bigframes/core/__init__.py | 21 +-- bigframes/core/block_transforms.py | 101 +++++++++++++- bigframes/core/blocks.py | 12 +- bigframes/core/groupby/__init__.py | 70 +++++++++- bigframes/core/window/__init__.py | 50 ++++--- bigframes/dataframe.py | 37 +++++ bigframes/ml/core.py | 4 +- bigframes/ml/ensemble.py | 4 +- bigframes/ml/linear_model.py | 79 +++++++++-- bigframes/operations/__init__.py | 11 ++ bigframes/series.py | 4 +- bigframes/session.py | 3 +- tests/system/large/ml/test_linear_model.py | 93 ++++++++----- tests/system/small/test_dataframe.py | 48 +++++++ tests/system/small/test_groupby.py | 24 ++++ tests/system/small/test_window.py | 42 +++++- tests/unit/ml/test_golden_sql.py | 130 ++++++++++++++++-- .../bigframes_vendored/pandas/core/frame.py | 15 ++ .../bigframes_vendored/pandas/core/generic.py | 55 ++++++++ .../pandas/core/groupby/__init__.py | 21 +++ .../bigframes_vendored/pandas/core/series.py | 55 -------- .../sklearn/linear_model/_base.py | 22 ++- .../sklearn/linear_model/_logistic.py | 11 +- 24 files changed, 745 insertions(+), 171 deletions(-) diff --git a/README.rst b/README.rst index 23aea446ff7..77c42e43251 100644 --- a/README.rst +++ b/README.rst @@ -241,7 +241,9 @@ Remote functions BigQuery DataFrames gives you the ability to turn your custom scalar functions into `BigQuery remote functions `_ . Creating a remote -function in BigQuery DataFrames creates a BigQuery remote function, a `BigQuery +function in BigQuery DataFrames (See `code samples +`_) +creates a BigQuery remote function, a `BigQuery connection `_ , and a `Cloud Functions (2nd gen) function diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 27fe4a4fe6c..8e7beb73dbb 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -600,7 +600,7 @@ def project_window_op( window_spec: WindowSpec, output_name=None, *, - skip_null_groups=False, + never_skip_nulls=False, skip_reproject_unsafe: bool = False, ) -> ArrayValue: """ @@ -609,7 +609,7 @@ def project_window_op( op: the windowable operator to apply to the input column window_spec: a specification of the window over which to apply the operator output_name: the id to assign to the output of the operator, by default will replace input col if distinct output id not provided - skip_null_groups: will filter out any rows where any of the grouping keys is null + never_skip_nulls: will disable null skipping for operators that would otherwise do so skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection """ column = typing.cast(ibis_types.Column, self.get_column(column_name)) @@ -618,20 +618,23 @@ def project_window_op( window_op = op._as_ibis(column, window) clauses = [] - if op.skips_nulls: + if op.skips_nulls and not never_skip_nulls: clauses.append((column.isnull(), ibis.NA)) - if skip_null_groups: - for key in window_spec.grouping_keys: - clauses.append((self.get_column(key).isnull(), ibis.NA)) if window_spec.min_periods: + if op.skips_nulls: + # Most operations do not count NULL values towards min_periods + observation_count = agg_ops.count_op._as_ibis(column, window) + else: + # Operations like count treat even NULLs as valid observations for the sake of min_periods + # notnull is just used to convert null values to non-null (FALSE) values to be counted + denulled_value = typing.cast(ibis_types.BooleanColumn, column.notnull()) + observation_count = agg_ops.count_op._as_ibis(denulled_value, window) clauses.append( ( - agg_ops.count_op._as_ibis(column, window) - < ibis_types.literal(window_spec.min_periods), + observation_count < ibis_types.literal(window_spec.min_periods), ibis.NA, ) ) - if clauses: case_statement = ibis.case() for clause in clauses: diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 5dcd9fe7538..da6ba65b8a9 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -355,6 +355,46 @@ def skew( return block +def kurt( + block: blocks.Block, + skew_column_ids: typing.Sequence[str], + grouping_column_ids: typing.Sequence[str] = (), +) -> blocks.Block: + original_columns = skew_column_ids + column_labels = block.select_columns(original_columns).column_labels + + block, delta4_ids = _mean_delta_to_power( + block, 4, original_columns, grouping_column_ids + ) + # counts, moment4 for each column + aggregations = [] + for i, col in enumerate(original_columns): + count_agg = (col, agg_ops.count_op) + moment4_agg = (delta4_ids[i], agg_ops.mean_op) + variance_agg = (col, agg_ops.PopVarOp()) + aggregations.extend([count_agg, moment4_agg, variance_agg]) + + block, agg_ids = block.aggregate( + by_column_ids=grouping_column_ids, aggregations=aggregations + ) + + kurt_ids = [] + for i, col in enumerate(original_columns): + # Corresponds to order of aggregations in preceding loop + count_id, moment4_id, var_id = agg_ids[i * 3 : (i * 3) + 3] + block, kurt_id = _kurt_from_moments_and_count( + block, count_id, moment4_id, var_id + ) + kurt_ids.append(kurt_id) + + block = block.select_columns(kurt_ids).with_column_labels(column_labels) + if not grouping_column_ids: + # When ungrouped, stack everything into single column so can be returned as series + block = block.stack() + block = block.drop_levels([block.index_columns[0]]) + return block + + def _mean_delta_to_power( block: blocks.Block, n_power, @@ -375,13 +415,13 @@ def _mean_delta_to_power( def _skew_from_moments_and_count( - block: blocks.Block, count_id: str, moment3_id: str, var_id: str + block: blocks.Block, count_id: str, moment3_id: str, moment2_id: str ) -> typing.Tuple[blocks.Block, str]: # Calculate skew using count, third moment and population variance # See G1 estimator: # https://en.wikipedia.org/wiki/Skewness#Sample_skewness block, denominator_id = block.apply_unary_op( - var_id, ops.partial_right(ops.pow_op, 3 / 2) + moment2_id, ops.partial_right(ops.unsafe_pow_op, 3 / 2) ) block, base_id = block.apply_binary_op(moment3_id, denominator_id, ops.div_op) block, countminus1_id = block.apply_unary_op( @@ -392,7 +432,7 @@ def _skew_from_moments_and_count( ) block, adjustment_id = block.apply_binary_op(count_id, countminus1_id, ops.mul_op) block, adjustment_id = block.apply_unary_op( - adjustment_id, ops.partial_right(ops.pow_op, 1 / 2) + adjustment_id, ops.partial_right(ops.unsafe_pow_op, 1 / 2) ) block, adjustment_id = block.apply_binary_op( adjustment_id, countminus2_id, ops.div_op @@ -405,3 +445,58 @@ def _skew_from_moments_and_count( skew_id, na_cond_id, ops.partial_arg3(ops.where_op, None) ) return block, skew_id + + +def _kurt_from_moments_and_count( + block: blocks.Block, count_id: str, moment4_id: str, moment2_id: str +) -> typing.Tuple[blocks.Block, str]: + # Kurtosis is often defined as the second standardize moment: moment(4)/moment(2)**2 + # Pandas however uses Fisher’s estimator, implemented below + # numerator = (count + 1) * (count - 1) * moment4 + # denominator = (count - 2) * (count - 3) * moment2**2 + # adjustment = 3 * (count - 1) ** 2 / ((count - 2) * (count - 3)) + # kurtosis = (numerator / denominator) - adjustment + + # Numerator + block, countminus1_id = block.apply_unary_op( + count_id, ops.partial_right(ops.sub_op, 1) + ) + block, countplus1_id = block.apply_unary_op( + count_id, ops.partial_right(ops.add_op, 1) + ) + block, num_adj = block.apply_binary_op(countplus1_id, countminus1_id, ops.mul_op) + block, numerator_id = block.apply_binary_op(moment4_id, num_adj, ops.mul_op) + + # Denominator + block, countminus2_id = block.apply_unary_op( + count_id, ops.partial_right(ops.sub_op, 2) + ) + block, countminus3_id = block.apply_unary_op( + count_id, ops.partial_right(ops.sub_op, 3) + ) + block, denom_adj = block.apply_binary_op(countminus2_id, countminus3_id, ops.mul_op) + block, popvar_squared = block.apply_unary_op( + moment2_id, ops.partial_right(ops.unsafe_pow_op, 2) + ) + block, denominator_id = block.apply_binary_op(popvar_squared, denom_adj, ops.mul_op) + + # Adjustment + block, countminus1_square = block.apply_unary_op( + countminus1_id, ops.partial_right(ops.unsafe_pow_op, 2) + ) + block, adj_num = block.apply_unary_op( + countminus1_square, ops.partial_right(ops.mul_op, 3) + ) + block, adj_denom = block.apply_binary_op(countminus2_id, countminus3_id, ops.mul_op) + block, adjustment_id = block.apply_binary_op(adj_num, adj_denom, ops.div_op) + + # Combine + block, base_id = block.apply_binary_op(numerator_id, denominator_id, ops.div_op) + block, kurt_id = block.apply_binary_op(base_id, adjustment_id, ops.sub_op) + + # Need to produce NA if have less than 4 data points + block, na_cond_id = block.apply_unary_op(count_id, ops.partial_right(ops.ge_op, 4)) + block, kurt_id = block.apply_binary_op( + kurt_id, na_cond_id, ops.partial_arg3(ops.where_op, None) + ) + return block, kurt_id diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 5b414252ee9..fb9ede9f4c9 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -709,6 +709,7 @@ def multi_apply_window_op( window_spec: core.WindowSpec, *, skip_null_groups: bool = False, + never_skip_nulls: bool = False, ) -> typing.Tuple[Block, typing.Sequence[str]]: block = self result_ids = [] @@ -721,6 +722,7 @@ def multi_apply_window_op( skip_reproject_unsafe=(i + 1) < len(columns), result_label=label, skip_null_groups=skip_null_groups, + never_skip_nulls=never_skip_nulls, ) result_ids.append(result_id) return block, result_ids @@ -751,15 +753,21 @@ def apply_window_op( result_label: Label = None, skip_null_groups: bool = False, skip_reproject_unsafe: bool = False, + never_skip_nulls: bool = False, ) -> typing.Tuple[Block, str]: + block = self + if skip_null_groups: + for key in window_spec.grouping_keys: + block, not_null_id = block.apply_unary_op(key, ops.notnull_op) + block = block.filter(not_null_id).drop_columns([not_null_id]) result_id = guid.generate_guid() - expr = self._expr.project_window_op( + expr = block._expr.project_window_op( column, op, window_spec, result_id, - skip_null_groups=skip_null_groups, skip_reproject_unsafe=skip_reproject_unsafe, + never_skip_nulls=never_skip_nulls, ) block = Block( expr, diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 810e145d33f..9be7f22a714 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -156,6 +156,18 @@ def skew( block = block_ops.skew(self._block, self._selected_cols, self._by_col_ids) return df.DataFrame(block) + def kurt( + self, + *, + numeric_only: bool = False, + ) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("kurt") + block = block_ops.kurt(self._block, self._selected_cols, self._by_col_ids) + return df.DataFrame(block) + + kurtosis = kurt + def all(self) -> df.DataFrame: return self._aggregate_all(agg_ops.all_op) @@ -195,6 +207,36 @@ def diff(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.DiffOp(periods), window=window) + 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 = core.WindowSpec( + grouping_keys=self._by_col_ids, + preceding=window - 1, + following=0, + min_periods=min_periods or window, + ) + block = self._block.order_by( + [order.OrderingColumnReference(col) for col in self._by_col_ids], + stable=True, + ) + return windows.Window( + block, window_spec, self._selected_cols, drop_null_groups=self._dropna + ) + + def expanding(self, min_periods: int = 1) -> windows.Window: + window_spec = core.WindowSpec( + grouping_keys=self._by_col_ids, + following=0, + min_periods=min_periods, + ) + block = self._block.order_by( + [order.OrderingColumnReference(col) for col in self._by_col_ids], + stable=True, + ) + return windows.Window( + block, window_spec, self._selected_cols, drop_null_groups=self._dropna + ) + def agg(self, func=None, **kwargs) -> df.DataFrame: if func: if isinstance(func, str): @@ -351,7 +393,7 @@ def _apply_window_op( ) columns = self._aggregated_columns(numeric_only=numeric_only) block, result_ids = self._block.multi_apply_window_op( - columns, op, window_spec=window_spec, skip_null_groups=self._dropna + columns, op, window_spec=window_spec ) block = block.select_columns(result_ids) return df.DataFrame(block) @@ -422,6 +464,12 @@ def skew(self, *args, **kwargs) -> series.Series: block = block_ops.skew(self._block, [self._value_column], self._by_col_ids) return series.Series(block) + def kurt(self, *args, **kwargs) -> series.Series: + block = block_ops.kurt(self._block, [self._value_column], self._by_col_ids) + return series.Series(block) + + kurtosis = kurt + def prod(self, *args) -> series.Series: return self._aggregate(agg_ops.product_op) @@ -510,7 +558,13 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: [order.OrderingColumnReference(col) for col in self._by_col_ids], stable=True, ) - return windows.Window(block, window_spec, self._value_column) + return windows.Window( + block, + window_spec, + [self._value_column], + drop_null_groups=self._dropna, + is_series=True, + ) def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = core.WindowSpec( @@ -522,10 +576,13 @@ def expanding(self, min_periods: int = 1) -> windows.Window: [order.OrderingColumnReference(col) for col in self._by_col_ids], stable=True, ) - return windows.Window(block, window_spec, self._value_column) - - def _ungroup(self) -> series.Series: - return series.Series(self._block.select_column(self._value_column)) + return windows.Window( + block, + window_spec, + [self._value_column], + drop_null_groups=self._dropna, + is_series=True, + ) def _aggregate(self, aggregate_op: agg_ops.AggregateOp) -> series.Series: result_block, _ = self._block.aggregate( @@ -553,6 +610,5 @@ def _apply_window_op( op, result_label=label, window_spec=window_spec, - skip_null_groups=self._dropna, ) return series.Series(block.select_column(result_id)) diff --git a/bigframes/core/window/__init__.py b/bigframes/core/window/__init__.py index 8994004e0b8..d3d081124e9 100644 --- a/bigframes/core/window/__init__.py +++ b/bigframes/core/window/__init__.py @@ -21,54 +21,57 @@ import bigframes.operations.aggregations as agg_ops import third_party.bigframes_vendored.pandas.core.window.rolling as vendored_pandas_rolling -if typing.TYPE_CHECKING: - from bigframes.series import Series - class Window(vendored_pandas_rolling.Window): __doc__ = vendored_pandas_rolling.Window.__doc__ - # TODO(tbergeron): Windows with groupings should create multi-indexed results - def __init__( self, block: blocks.Block, window_spec: core.WindowSpec, - value_column_id: str, + value_column_ids: typing.Sequence[str], + drop_null_groups: bool = True, + is_series: bool = False, ): self._block = block self._window_spec = window_spec - self._value_column_id = value_column_id + self._value_column_ids = value_column_ids + self._drop_null_groups = drop_null_groups + self._is_series = is_series - def count(self) -> Series: + def count(self): return self._apply_aggregate(agg_ops.count_op) - def sum(self) -> Series: + def sum(self): return self._apply_aggregate(agg_ops.sum_op) - def mean(self) -> Series: + def mean(self): return self._apply_aggregate(agg_ops.mean_op) - def var(self) -> Series: + def var(self): return self._apply_aggregate(agg_ops.var_op) - def std(self) -> Series: + def std(self): return self._apply_aggregate(agg_ops.std_op) - def max(self) -> Series: + def max(self): return self._apply_aggregate(agg_ops.max_op) - def min(self) -> Series: + def min(self): return self._apply_aggregate(agg_ops.min_op) def _apply_aggregate( self, op: agg_ops.AggregateOp, - ) -> Series: + ): block = self._block - label = block.col_id_to_label[self._value_column_id] - block, result_id = block.apply_window_op( - self._value_column_id, op, self._window_spec, result_label=label + labels = [block.col_id_to_label[col] for col in self._value_column_ids] + block, result_ids = block.multi_apply_window_op( + self._value_column_ids, + op, + self._window_spec, + skip_null_groups=self._drop_null_groups, + never_skip_nulls=True, ) if self._window_spec.grouping_keys: @@ -80,6 +83,13 @@ def _apply_aggregate( ) block = block.set_index(col_ids=index_ids) - from bigframes.series import Series + if self._is_series: + from bigframes.series import Series - return Series(block.select_column(result_id)) + return Series(block.select_columns(result_ids).with_column_labels(labels)) + else: + from bigframes.dataframe import DataFrame + + return DataFrame( + block.select_columns(result_ids).with_column_labels(labels) + ) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d65d4ce344e..de4adb912e6 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -49,6 +49,7 @@ import bigframes.core.joins as joins import bigframes.core.ordering as order import bigframes.core.utils as utils +import bigframes.core.window import bigframes.dtypes import bigframes.formatting_helpers as formatter import bigframes.operations as ops @@ -282,6 +283,10 @@ def empty(self) -> bool: def values(self) -> numpy.ndarray: return self.to_numpy() + @property + def _session(self) -> bigframes.Session: + return self._get_block().expr._session + def __len__(self): rows, _ = self.shape return rows @@ -1056,6 +1061,13 @@ def _assign_single_item( ) -> DataFrame: if isinstance(v, bigframes.series.Series): return self._assign_series_join_on_index(k, v) + elif isinstance(v, bigframes.dataframe.DataFrame): + v_df_col_count = len(v._block.value_columns) + if v_df_col_count != 1: + raise ValueError( + f"Cannot set a DataFrame with {v_df_col_count} columns to the single column {k}" + ) + return self._assign_series_join_on_index(k, v[v.columns[0]]) elif callable(v): copy = self.copy() copy[k] = v(copy) @@ -1627,6 +1639,16 @@ def skew(self, *, numeric_only: bool = False): result_block = block_ops.skew(frame._block, frame._block.value_columns) return bigframes.series.Series(result_block) + def kurt(self, *, numeric_only: bool = False): + if not numeric_only: + frame = self._raise_on_non_numeric("kurt") + else: + frame = self._drop_non_numeric() + result_block = block_ops.kurt(frame._block, frame._block.value_columns) + return bigframes.series.Series(result_block) + + kurtosis = kurt + def pivot( self, *, @@ -1882,6 +1904,21 @@ def _perform_join_by_index(self, other: DataFrame, *, how: str = "left"): ) return DataFrame(combined_index._block) + 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 = bigframes.core.WindowSpec( + preceding=window - 1, following=0, min_periods=min_periods or window + ) + return bigframes.core.window.Window( + self._block, window_spec, self._block.value_columns + ) + + def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: + window_spec = bigframes.core.WindowSpec(following=0, min_periods=min_periods) + return bigframes.core.window.Window( + self._block, window_spec, self._block.value_columns + ) + def groupby( self, by: typing.Union[ diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 57f610c4c4b..667d42f7ee6 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -221,7 +221,7 @@ def create_bqml_model( input_data = X_train.join(y_train, how="outer") options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) - session = X_train._get_block().expr._session + session = X_train._session source_sql = input_data.sql options_sql = ml_sql.options(**options) @@ -255,7 +255,7 @@ def create_bqml_time_series_model( options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) - session = X_train._get_block().expr._session + session = X_train._session source_sql = input_data.sql options_sql = ml_sql.options(**options) diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 142edaa00f6..b0f3e5f0813 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -110,7 +110,7 @@ def _from_bq( dummy_regressor = cls() for bf_param, bf_value in dummy_regressor.__dict__.items(): bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param is not None: + if bqml_param in last_fitting: kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) new_xgb_regressor = cls(**kwargs) @@ -431,7 +431,7 @@ def _from_bq( dummy_model = cls() for bf_param, bf_value in dummy_model.__dict__.items(): bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param is not None: + if bqml_param in last_fitting: kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) new_random_forest_regressor = cls(**kwargs) diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 1606a15d730..f27b798eea1 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import cast, Dict, List, Optional, Union +from typing import cast, Dict, List, Literal, Optional, Union from google.cloud import bigquery @@ -28,6 +28,23 @@ import third_party.bigframes_vendored.sklearn.linear_model._base import third_party.bigframes_vendored.sklearn.linear_model._logistic +_BQML_PARAMS_MAPPING = { + "optimize_strategy": "optimizationStrategy", + "fit_intercept": "fitIntercept", + "l1_reg": "l1Regularization", + "l2_reg": "l2Regularization", + "max_iterations": "maxIterations", + "learn_rate_strategy": "learnRateStrategy", + "learn_rate": "learnRate", + "early_stop": "earlyStop", + "min_rel_progress": "minRelativeProgress", + "ls_init_learn_rate": "initialLearnRate", + "warm_start": "warmStart", + "calculate_p_values": "calculatePValues", + "enable_global_explain": "enableGlobalExplain", + "category_encoding_method": "categoryEncodingMethod", +} + class LinearRegression( base.SupervisedTrainablePredictor, @@ -39,9 +56,29 @@ class LinearRegression( def __init__( self, + optimize_strategy: Literal[ + "auto_strategy", "batch_gradient_descent", "normal_equation" + ] = "normal_equation", fit_intercept: bool = True, + l2_reg: float = 0.0, + max_iterations: int = 20, + learn_rate_strategy: Literal["line_search", "constant"] = "line_search", + early_stop: bool = True, + min_rel_progress: float = 0.01, + ls_init_learn_rate: float = 0.1, + calculate_p_values: bool = False, + enable_global_explain: bool = False, ): + self.optimize_strategy = optimize_strategy self.fit_intercept = fit_intercept + self.l2_reg = l2_reg + self.max_iterations = max_iterations + self.learn_rate_strategy = learn_rate_strategy + self.early_stop = early_stop + self.min_rel_progress = min_rel_progress + self.ls_init_learn_rate = ls_init_learn_rate + self.calculate_p_values = calculate_p_values + self.enable_global_explain = enable_global_explain self._bqml_model: Optional[core.BqmlModel] = None @classmethod @@ -55,8 +92,12 @@ def _from_bq( # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun last_fitting = model.training_runs[-1]["trainingOptions"] - if "fitIntercept" in last_fitting: - kwargs["fit_intercept"] = last_fitting["fitIntercept"] + + dummy_linear = cls() + for bf_param, bf_value in dummy_linear.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param in last_fitting: + kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) new_linear_regression = cls(**kwargs) new_linear_regression._bqml_model = core.BqmlModel(session, model) @@ -65,10 +106,20 @@ def _from_bq( @property def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: """The model options as they will be set for BQML""" + # TODO: Support l1_reg, warm_start, and learn_rate with error catching. return { "model_type": "LINEAR_REG", "data_split_method": "NO_SPLIT", + "optimize_strategy": self.optimize_strategy, "fit_intercept": self.fit_intercept, + "l2_reg": self.l2_reg, + "max_iterations": self.max_iterations, + "learn_rate_strategy": self.learn_rate_strategy, + "early_stop": self.early_stop, + "min_rel_progress": self.min_rel_progress, + "ls_init_learn_rate": self.ls_init_learn_rate, + "calculate_p_values": self.calculate_p_values, + "enable_global_explain": self.enable_global_explain, } def _fit( @@ -147,10 +198,11 @@ class LogisticRegression( def __init__( self, fit_intercept: bool = True, - auto_class_weights: bool = False, + class_weights: Optional[Union[Literal["balanced"], Dict[str, float]]] = None, ): self.fit_intercept = fit_intercept - self.auto_class_weights = auto_class_weights + self.class_weights = class_weights + self._auto_class_weight = class_weights == "balanced" self._bqml_model: Optional[core.BqmlModel] = None @classmethod @@ -165,10 +217,8 @@ def _from_bq( last_fitting = model.training_runs[-1]["trainingOptions"] if "fitIntercept" in last_fitting: kwargs["fit_intercept"] = last_fitting["fitIntercept"] - # TODO(ashleyxu): b/285162045 support auto_class_weights once the API is - # fixed and enable the tests. - if "autoClassWeights" in last_fitting: - kwargs["auto_class_weights"] = last_fitting["autoClassWeights"] + if last_fitting["autoClassWeights"]: + kwargs["class_weights"] = "balanced" # TODO(ashleyxu) support class_weights in the constructor. # if "labelClassWeights" in last_fitting: # kwargs["class_weights"] = last_fitting["labelClassWeights"] @@ -184,8 +234,8 @@ def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: "model_type": "LOGISTIC_REG", "data_split_method": "NO_SPLIT", "fit_intercept": self.fit_intercept, - "auto_class_weights": self.auto_class_weights, - # TODO(ashleyxu): support class_weights (struct array) + "auto_class_weights": self._auto_class_weight, + # TODO(ashleyxu): support class_weights (struct array as dict in our API) # "class_weights": self.class_weights, } @@ -253,11 +303,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LogisticRegression: if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") - # TODO(ashleyxu): b/285162045 support auto_class_weights once the API is - # fixed and enable the tests. - if self.auto_class_weights is True: + # TODO(ashleyxu): support class_weights (struct array as dict in our API) + if self.class_weights not in (None, "balanced"): raise NotImplementedError( - f"auto_class_weight is not supported yet. {constants.FEEDBACK_LINK}" + f"class_weights is not supported yet. {constants.FEEDBACK_LINK}" ) new_model = self._bqml_model.copy(model_name, replace) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index fa43f725f64..c5c55607ae5 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -773,6 +773,17 @@ def pow_op( return _float_pow_op(x, y) +@short_circuit_nulls(ibis_dtypes.float) +def unsafe_pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + """For internal use only - where domain and overflow checks are not needed.""" + return typing.cast(ibis_types.NumericValue, x) ** typing.cast( + ibis_types.NumericValue, y + ) + + def _int_pow_op( x: ibis_types.Value, y: ibis_types.Value, diff --git a/bigframes/series.py b/bigframes/series.py index 12e72c58b61..9db64fae9c9 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1016,13 +1016,13 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window preceding=window - 1, following=0, min_periods=min_periods or window ) return bigframes.core.window.Window( - self._block, window_spec, self._value_column + self._block, window_spec, self._block.value_columns, is_series=True ) def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: window_spec = WindowSpec(following=0, min_periods=min_periods) return bigframes.core.window.Window( - self._block, window_spec, self._value_column + self._block, window_spec, self._block.value_columns, is_series=True ) def groupby( diff --git a/bigframes/session.py b/bigframes/session.py index 3ca79a7b531..04ae6ba4541 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -1324,7 +1324,8 @@ def remote_function( reuse: bool = True, name: Optional[str] = None, ): - """Decorator to turn a user defined function into a BigQuery remote function. + """Decorator to turn a user defined function into a BigQuery remote function. Check out + the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. .. note:: Please make sure following is setup before using this API: diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 3b90568450b..a0f4182e6fb 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -18,7 +18,7 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, dataset_id): - model = bigframes.ml.linear_model.LinearRegression(fit_intercept=False) + model = bigframes.ml.linear_model.LinearRegression() df = penguins_df_default_index.dropna() X_train = df[ @@ -55,15 +55,24 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase assert ( f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name ) - - # TODO(yunmengxie): enable this once b/277242951 (fit_intercept missing from API) is fixed - # assert reloaded_model.fit_intercept == False + assert reloaded_model.optimize_strategy == "NORMAL_EQUATION" + assert reloaded_model.fit_intercept is True + assert reloaded_model.calculate_p_values is False + assert reloaded_model.early_stop is True + assert reloaded_model.enable_global_explain is False + assert reloaded_model.l2_reg == 0.0 + assert reloaded_model.learn_rate_strategy == "line_search" + assert reloaded_model.ls_init_learn_rate == 0.1 + assert reloaded_model.max_iterations == 20 + assert reloaded_model.min_rel_progress == 0.01 -def test_linear_regression_manual_split_configure_fit_score( +def test_linear_regression_customized_params_fit_score( penguins_df_default_index, dataset_id ): - model = bigframes.ml.linear_model.LinearRegression(fit_intercept=True) + model = bigframes.ml.linear_model.LinearRegression( + fit_intercept=False, l2_reg=0.1, min_rel_progress=0.01 + ) df = penguins_df_default_index.dropna() X_train = df[ @@ -83,12 +92,12 @@ def test_linear_regression_manual_split_configure_fit_score( result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { - "mean_absolute_error": [225.735767], - "mean_squared_error": [80417.461828], - "mean_squared_log_error": [0.004967], - "median_absolute_error": [172.543702], - "r2_score": [0.87548], - "explained_variance": [0.87548], + "mean_absolute_error": [226.108411], + "mean_squared_error": [80459.668456], + "mean_squared_log_error": [0.00497], + "median_absolute_error": [171.618872], + "r2_score": [0.875415], + "explained_variance": [0.875417], }, dtype="Float64", ) @@ -100,13 +109,21 @@ def test_linear_regression_manual_split_configure_fit_score( assert ( f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name ) - assert reloaded_model.fit_intercept is True - - -def test_logistic_regression_auto_class_weights_configure_fit_score( - penguins_df_default_index, dataset_id -): + assert reloaded_model.optimize_strategy == "NORMAL_EQUATION" + assert reloaded_model.fit_intercept is False + assert reloaded_model.calculate_p_values is False + assert reloaded_model.early_stop is True + assert reloaded_model.enable_global_explain is False + assert reloaded_model.l2_reg == 0.1 + assert reloaded_model.learn_rate_strategy == "line_search" + assert reloaded_model.ls_init_learn_rate == 0.1 + assert reloaded_model.max_iterations == 20 + assert reloaded_model.min_rel_progress == 0.01 + + +def test_logistic_regression_configure_fit_score(penguins_df_default_index, dataset_id): model = bigframes.ml.linear_model.LogisticRegression() + df = penguins_df_default_index.dropna() X_train = df[ [ @@ -115,6 +132,7 @@ def test_logistic_regression_auto_class_weights_configure_fit_score( "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", + "body_mass_g", ] ] y_train = df[["sex"]] @@ -124,12 +142,12 @@ def test_logistic_regression_auto_class_weights_configure_fit_score( result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { - "precision": [0.58085], - "recall": [0.582576], - "accuracy": [0.871257], - "f1_score": [0.58171], - "log_loss": [1.59285], - "roc_auc": [0.9602], + "precision": [0.616753], + "recall": [0.618615], + "accuracy": [0.92515], + "f1_score": [0.617681], + "log_loss": [1.498832], + "roc_auc": [0.975807], }, dtype="Float64", ) @@ -145,15 +163,15 @@ def test_logistic_regression_auto_class_weights_configure_fit_score( in reloaded_model._bqml_model.model_name ) assert reloaded_model.fit_intercept is True - # TODO(gaotianxiang): enable this once (auto_class_weights missing from API) is fixed - # assert reloaded_model.auto_class_weights is True + assert reloaded_model.class_weights is None -def test_logistic_regression_manual_split_configure_fit_score( +def test_logistic_regression_customized_params_fit_score( penguins_df_default_index, dataset_id ): - model = bigframes.ml.linear_model.LogisticRegression(fit_intercept=True) - + model = bigframes.ml.linear_model.LogisticRegression( + fit_intercept=False, class_weights="balanced" + ) df = penguins_df_default_index.dropna() X_train = df[ [ @@ -162,7 +180,6 @@ def test_logistic_regression_manual_split_configure_fit_score( "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", - "body_mass_g", ] ] y_train = df[["sex"]] @@ -172,12 +189,12 @@ def test_logistic_regression_manual_split_configure_fit_score( result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { - "precision": [0.616753], - "recall": [0.618615], - "accuracy": [0.92515], - "f1_score": [0.617681], - "log_loss": [1.498832], - "roc_auc": [0.975807], + "precision": [0.58483], + "recall": [0.586616], + "accuracy": [0.877246], + "f1_score": [0.58571], + "log_loss": [1.032699], + "roc_auc": [0.924132], }, dtype="Float64", ) @@ -192,5 +209,5 @@ def test_logistic_regression_manual_split_configure_fit_score( f"{dataset_id}.temp_configured_logistic_reg_model" in reloaded_model._bqml_model.model_name ) - assert reloaded_model.fit_intercept is True - assert reloaded_model.auto_class_weights is False + assert reloaded_model.fit_intercept is False + assert reloaded_model.class_weights == "balanced" diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index a85777c59d6..ed682c855b7 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -411,6 +411,30 @@ def test_assign_new_column_w_setitem(scalars_dfs): pd.testing.assert_frame_equal(bf_result, pd_result) +def test_assign_new_column_w_setitem_dataframe(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df["int64_col"] = bf_df["int64_too"].to_frame() + pd_df["int64_col"] = pd_df["int64_too"].to_frame() + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_df["int64_col"] = pd_df["int64_col"].astype("Int64") + + pd.testing.assert_frame_equal(bf_df.to_pandas(), pd_df) + + +def test_assign_new_column_w_setitem_dataframe_error(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + + with pytest.raises(ValueError): + bf_df["impossible_col"] = bf_df[["int64_too", "string_col"]] + with pytest.raises(ValueError): + pd_df["impossible_col"] = pd_df[["int64_too", "string_col"]] + + def test_assign_new_column_w_setitem_list(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_df = scalars_df.copy() @@ -1996,6 +2020,30 @@ def test_df_skew(scalars_dfs): pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) +def test_df_kurt_too_few_values(scalars_dfs): + columns = ["float64_col", "int64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[columns].head(2).kurt().to_pandas() + pd_result = scalars_pandas_df[columns].head(2).kurt() + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + +def test_df_kurt(scalars_dfs): + columns = ["float64_col", "int64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[columns].kurt().to_pandas() + pd_result = scalars_pandas_df[columns].kurt() + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + @pytest.mark.parametrize( ("frac", "n", "random_state"), [ diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 18741468c5d..05154f7ab7d 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -238,6 +238,16 @@ def test_series_groupby_skew(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) +def test_series_groupby_kurt(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.groupby("bool_col")["int64_too"].kurt().to_pandas() + # Pandas doesn't have groupby.kurt yet: https://github.com/pandas-dev/pandas/issues/40139 + pd_result = scalars_pandas_df_index.groupby("bool_col")["int64_too"].apply( + pd.Series.kurt + ) + + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + def test_dataframe_groupby_skew(scalars_df_index, scalars_pandas_df_index): col_names = ["float64_col", "int64_col", "bool_col"] bf_result = scalars_df_index[col_names].groupby("bool_col").skew().to_pandas() @@ -246,6 +256,20 @@ def test_dataframe_groupby_skew(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) +def test_dataframe_groupby_kurt(scalars_df_index, scalars_pandas_df_index): + col_names = ["float64_col", "int64_col", "bool_col"] + bf_result = scalars_df_index[col_names].groupby("bool_col").kurt().to_pandas() + # Pandas doesn't have groupby.kurt yet: https://github.com/pandas-dev/pandas/issues/40139 + pd_result = ( + scalars_pandas_df_index[col_names] + .groupby("bool_col") + .apply(pd.Series.kurt) + .drop("bool_col", axis=1) + ) + + pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + + def test_dataframe_groupby_diff(scalars_df_index, scalars_pandas_df_index): col_names = ["float64_col", "int64_col", "string_col"] bf_result = scalars_df_index[col_names].groupby("string_col").diff(-1) diff --git a/tests/system/small/test_window.py b/tests/system/small/test_window.py index e2f0fe999b1..2b9ec1a3c0f 100644 --- a/tests/system/small/test_window.py +++ b/tests/system/small/test_window.py @@ -41,7 +41,9 @@ pytest.param(lambda x: x.var(), id="var"), ], ) -def test_window_agg_ops(scalars_df_index, scalars_pandas_df_index, windowing, agg_op): +def test_series_window_agg_ops( + scalars_df_index, scalars_pandas_df_index, windowing, agg_op +): col_name = "int64_too" bf_series = agg_op(windowing(scalars_df_index[col_name])).to_pandas() pd_series = agg_op(windowing(scalars_pandas_df_index[col_name])) @@ -53,3 +55,41 @@ def test_window_agg_ops(scalars_df_index, scalars_pandas_df_index, windowing, ag pd_series, bf_series, ) + + +@pytest.mark.parametrize( + ("windowing"), + [ + pytest.param(lambda x: x.expanding(), id="expanding"), + pytest.param(lambda x: x.rolling(3, min_periods=3), id="rolling"), + pytest.param( + lambda x: x.groupby(level=0).rolling(3, min_periods=3), id="rollinggroupby" + ), + pytest.param( + lambda x: x.groupby("int64_too").expanding(min_periods=2), + id="expandinggroupby", + ), + ], +) +@pytest.mark.parametrize( + ("agg_op"), + [ + pytest.param(lambda x: x.sum(), id="sum"), + pytest.param(lambda x: x.min(), id="min"), + pytest.param(lambda x: x.max(), id="max"), + pytest.param(lambda x: x.mean(), id="mean"), + pytest.param(lambda x: x.count(), id="count"), + pytest.param(lambda x: x.std(), id="std"), + pytest.param(lambda x: x.var(), id="var"), + ], +) +def test_dataframe_window_agg_ops( + scalars_df_index, scalars_pandas_df_index, windowing, agg_op +): + scalars_df_index = scalars_df_index.set_index("bool_col") + scalars_pandas_df_index = scalars_pandas_df_index.set_index("bool_col") + col_names = ["int64_too", "float64_col"] + bf_result = agg_op(windowing(scalars_df_index[col_names])).to_pandas() + pd_result = agg_op(windowing(scalars_pandas_df_index[col_names])) + + pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 8d4932a3c3c..584d080d428 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -14,34 +14,146 @@ from unittest import mock +from google.cloud import bigquery +import pandas as pd +import pytest import pytest_mock import bigframes -from bigframes.ml import linear_model +from bigframes.ml import core, linear_model import bigframes.pandas as bpd -def test_linear_regression_default_fit(mocker: pytest_mock.MockerFixture): +@pytest.fixture +def mock_session(): mock_session = mock.create_autospec(spec=bigframes.Session) - mock_X = mock.create_autospec(spec=bpd.DataFrame) - mock_X._get_block().expr._session = mock_session + # return values we don't care about, but need to provide to continue the program when calling session._start_query() + mock_session._start_query.return_value = (None, mock.MagicMock()) + + return mock_session + +@pytest.fixture +def mock_y(): mock_y = mock.create_autospec(spec=bpd.DataFrame) - mock_y.columns.tolist.return_value = ["input_label_column"] + mock_y.columns = pd.Index(["input_column_label"]) - mock_X.join(mock_y).sql = "input_dataframe_sql" + return mock_y - # return values we don't care about, but need to provide to continue the program - mock_session._start_query.return_value = (None, mock.MagicMock()) +@pytest.fixture +def mock_X(mock_y, mock_session): + mock_X = mock.create_autospec(spec=bpd.DataFrame) + mock_X._session = mock_session + mock_X._to_sql_query.return_value = ( + "input_X_sql", + ["index_column_id"], + ["index_column_label"], + ) + mock_X.join(mock_y).sql = "input_X_y_sql" + mock_X.join(mock_y)._to_sql_query.return_value = ( + "input_X_y_sql", + ["index_column_id"], + ["index_column_label"], + ) + + return mock_X + + +@pytest.fixture +def bqml_model(mock_session): + bqml_model = core.BqmlModel( + mock_session, bigquery.Model("model_project.model_dataset.model_name") + ) + + return bqml_model + + +@pytest.fixture +def ml_mocker(mocker: pytest_mock.MockerFixture): mocker.patch( "bigframes.ml.core._create_temp_model_name", return_value="temp_model_name" ) + return mocker + + +def test_linear_regression_default_fit(ml_mocker, mock_session, mock_X, mock_y): + model = linear_model.LinearRegression() + model.fit(mock_X, mock_y) + + mock_session._start_query.assert_called_once_with( + 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + ) + + +def test_linear_regression_params_fit(ml_mocker, mock_session, mock_X, mock_y): + model = linear_model.LinearRegression(fit_intercept=False) + model.fit(mock_X, mock_y) + + mock_session._start_query.assert_called_once_with( + 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + ) + + +def test_linear_regression_predict(mock_session, bqml_model, mock_X): + model = linear_model.LinearRegression() + model._bqml_model = bqml_model + model.predict(mock_X) + + mock_session.read_gbq.assert_called_once_with( + "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_name`,\n (input_X_sql))", + index_col=["index_column_id"], + ) + + +def test_linear_regression_score(mock_session, bqml_model, mock_X, mock_y): model = linear_model.LinearRegression() + model._bqml_model = bqml_model + model.score(mock_X, mock_y) + + mock_session.read_gbq.assert_called_once_with( + "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_name`,\n (input_X_y_sql))" + ) + + +def test_logistic_regression_default_fit(ml_mocker, mock_session, mock_X, mock_y): + model = linear_model.LogisticRegression() model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n INPUT_LABEL_COLS=["input_label_column"])\nAS input_dataframe_sql' + 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + ) + + +def test_logistic_regression_params_fit(ml_mocker, mock_session, mock_X, mock_y): + model = linear_model.LogisticRegression( + fit_intercept=False, class_weights="balanced" + ) + model.fit(mock_X, mock_y) + + mock_session._start_query.assert_called_once_with( + 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + ) + + +def test_logistic_regression_predict(mock_session, bqml_model, mock_X): + model = linear_model.LogisticRegression() + model._bqml_model = bqml_model + model.predict(mock_X) + + mock_session.read_gbq.assert_called_once_with( + "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_name`,\n (input_X_sql))", + index_col=["index_column_id"], + ) + + +def test_logistic_regression_score(mock_session, bqml_model, mock_X, mock_y): + model = linear_model.LogisticRegression() + model._bqml_model = bqml_model + model.score(mock_X, mock_y) + + mock_session.read_gbq.assert_called_once_with( + "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_name`,\n (input_X_y_sql))" ) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 113c6547a0e..27cc2144e0f 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1569,6 +1569,21 @@ def skew(self, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def kurt(self, *, numeric_only: bool = False): + """Return unbiased kurtosis over requested axis. + + Kurtosis obtained using Fisher's definition of + kurtosis (kurtosis of normal == 0.0). Normalized by N-1. + + Args: + numeric_only (bool, default False): + Include only float, int, boolean columns. + + Returns: + Series + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def std(self, *, numeric_only: bool = False): """Return sample standard deviation over requested axis. diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 7d496891b0b..27d2e845376 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -511,6 +511,61 @@ def rank( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rolling( + self, + window, + min_periods: int | None = None, + ): + """ + Provide rolling window calculations. + + Args: + window (int, timedelta, str, offset, or BaseIndexer subclass): + Size of the moving window. + + If an integer, the fixed number of observations used for + each window. + + If a timedelta, str, or offset, the time period of each window. Each + window will be a variable sized based on the observations included in + the time-period. This is only valid for datetime-like indexes. + To learn more about the offsets & frequency strings, please see `this link + `__. + + If a BaseIndexer subclass, the window boundaries + based on the defined ``get_window_bounds`` method. Additional rolling + keyword arguments, namely ``min_periods``, ``center``, ``closed`` and + ``step`` will be passed to ``get_window_bounds``. + + min_periods (int, default None): + Minimum number of observations in window required to have a value; + otherwise, result is ``np.nan``. + + For a window that is specified by an offset, ``min_periods`` will default to 1. + + For a window that is specified by an integer, ``min_periods`` will default + to the size of the window. + + Returns: + bigframes.core.window.Window: ``Window`` subclass if a ``win_type`` is passed. + ``Rolling`` subclass if ``win_type`` is not passed. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def expanding(self, min_periods=1): + """ + Provide expanding window calculations. + + Args: + min_periods (int, default 1): + Minimum number of observations in window required to have a value; + otherwise, result is ``np.nan``. + + Returns: + bigframes.core.window.Window: ``Expanding`` subclass. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __nonzero__(self): raise ValueError( f"The truth value of a {type(self).__name__} is ambiguous. " diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 7849a3afd52..b05319b4f7d 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -144,6 +144,27 @@ def skew( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def kurt( + self, + *, + numeric_only: bool = False, + ): + """ + Return unbiased kurtosis over requested axis. + + Kurtosis obtained using Fisher's definition of + kurtosis (kurtosis of normal == 0.0). Normalized by N-1. + + Args: + numeric_only (bool, default False): + Include only `float`, `int` or `boolean` data. + + Returns: + Series or DataFrame + Variance of values within each group. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def sum( self, numeric_only: bool = False, diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index c6dd973372c..d58c1ccc3b4 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1673,61 +1673,6 @@ def rename_axis(self, mapper, **kwargs): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def rolling( - self, - window, - min_periods: int | None = None, - ): - """ - Provide rolling window calculations. - - Args: - window (int, timedelta, str, offset, or BaseIndexer subclass): - Size of the moving window. - - If an integer, the fixed number of observations used for - each window. - - If a timedelta, str, or offset, the time period of each window. Each - window will be a variable sized based on the observations included in - the time-period. This is only valid for datetime-like indexes. - To learn more about the offsets & frequency strings, please see `this link - `__. - - If a BaseIndexer subclass, the window boundaries - based on the defined ``get_window_bounds`` method. Additional rolling - keyword arguments, namely ``min_periods``, ``center``, ``closed`` and - ``step`` will be passed to ``get_window_bounds``. - - min_periods (int, default None): - Minimum number of observations in window required to have a value; - otherwise, result is ``np.nan``. - - For a window that is specified by an offset, ``min_periods`` will default to 1. - - For a window that is specified by an integer, ``min_periods`` will default - to the size of the window. - - Returns: - bigframes.core.window.Window: ``Window`` subclass if a ``win_type`` is passed. - ``Rolling`` subclass if ``win_type`` is not passed. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - - def expanding(self, min_periods=1): - """ - Provide expanding window calculations. - - Args: - min_periods (int, default 1): - Minimum number of observations in window required to have a value; - otherwise, result is ``np.nan``. - - Returns: - bigframes.core.window.Window: ``Expanding`` subclass. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def value_counts( self, normalize: bool = False, diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index 81b4fca1572..8dc3b6280a4 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -64,10 +64,30 @@ class LinearRegression(RegressorMixin, LinearModel): the dataset, and the targets predicted by the linear approximation. Args: - fit_intercept (default True): + optimize_strategy (str, default "normal_equation"): + The strategy to train linear regression models. Possible values are + "auto_strategy", "batch_gradient_descent", "normal_equation". Default + to "normal_equation". + fit_intercept (bool, default True): Default ``True``. Whether to calculate the intercept for this model. If set to False, no intercept will be used in calculations (i.e. data is expected to be centered). + l2_reg (float, default 0.0): + The amount of L2 regularization applied. Default to 0. + max_iterations (int, default 20): + The maximum number of training iterations or steps. Default to 20. + learn_rate_strategy (str, default "line_search"): + The strategy for specifying the learning rate during training. Default to "line_search". + early_stop (bool, default True): + Whether training should stop after the first iteration in which the relative loss improvement is less than the value specified for min_rel_progress. Default to True. + min_rel_progress (float, default 0.01): + The minimum relative loss improvement that is necessary to continue training when EARLY_STOP is set to true. For example, a value of 0.01 specifies that each iteration must reduce the loss by 1% for training to continue. Default to 0.01. + ls_init_learn_rate (float, default 0.1): + Sets the initial learning rate that learn_rate_strategy='line_search' uses. This option can only be used if line_search is specified. Default to 0.1. + calculate_p_values (bool, default False): + Specifies whether to compute p-values and standard errors during training. Default to False. + enable_global_explain (bool, default False): + Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. """ def fit( diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 133dc4498e4..989ca03c827 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -28,9 +28,14 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): fit_intercept (default True): Default True. Specifies if a constant (a.k.a. bias or intercept) should be added to the decision function. - auto_class_weights (default False): - Default False. If True, balance class labels using weights for each - class in inverse proportion to the frequency of that class. + class_weights (dict or 'balanced', default None): + Default None. Weights associated with classes in the form + ``{class_label: weight}``.If not given, all classes are supposed + to have weight one. The "balanced" mode uses the values of y to + automatically adjust weights inversely proportional to class + frequencies in the input data as + ``n_samples / (n_classes * np.bincount(y))``. Dict isn't + supported now. """ def fit( From 5056da6b385dbcfc179d2bcbb6549fa539428cda Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 20 Sep 2023 15:16:38 -0700 Subject: [PATCH 024/921] perf: simplify join order to use multiple order keys instead of string. (#36) Change-Id: I8c37e9296b2e4e0ea87f6a7e836d48988d161d37 --- bigframes/core/joins/single_column.py | 118 +++++++++++--------------- 1 file changed, 50 insertions(+), 68 deletions(-) diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py index 4c865fffdf7..7aba71fd95e 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/joins/single_column.py @@ -63,7 +63,6 @@ def join_by_column( allow_row_identity_join (bool): If True, allow matching by row identity. Set to False to always perform a true JOIN in generated SQL. - Returns: The joined expression and the objects needed to interpret it. @@ -123,13 +122,13 @@ def join_by_column( ), ) else: - # Generate offsets if non-default ordering is applied - # Assumption, both sides are totally ordered, otherwise offsets will be nondeterministic left_table = left.to_ibis_expr( - ordering_mode="string_encoded", order_col_name=core.ORDER_ID_COLUMN + ordering_mode="unordered", + expose_hidden_cols=True, ) right_table = right.to_ibis_expr( - ordering_mode="string_encoded", order_col_name=core.ORDER_ID_COLUMN + ordering_mode="unordered", + expose_hidden_cols=True, ) join_conditions = [ value_to_join_key(left_table[left_index]) @@ -178,41 +177,13 @@ def get_column_right(key: str) -> str: return key - left_ordering_encoding_size = ( - left._ordering.string_encoding.length - if left._ordering.is_string_encoded - else bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH - ) - right_ordering_encoding_size = ( - right._ordering.string_encoding.length - if right._ordering.is_string_encoded - else bigframes.core.ordering.DEFAULT_ORDERING_ID_LENGTH - ) - - # Preserve original ordering accross joins. - left_order_id = get_column_left(core.ORDER_ID_COLUMN) - right_order_id = get_column_right(core.ORDER_ID_COLUMN) - new_order_id_col = _merge_order_ids( - typing.cast(ibis_types.StringColumn, combined_table[left_order_id]), - left_ordering_encoding_size, - typing.cast(ibis_types.StringColumn, combined_table[right_order_id]), - right_ordering_encoding_size, - how, - ) - new_order_id = new_order_id_col.get_name() - if new_order_id is None: - raise ValueError("new_order_id unexpectedly has no name") - - hidden_columns = (new_order_id_col,) - ordering = core.ExpressionOrdering( - # Order id is non-nullable but na_last=False generates simpler sql with current impl - ordering_value_columns=[ - core.OrderingColumnReference(new_order_id, na_last=False) - ], - total_ordering_columns=frozenset([new_order_id]), - string_encoding=core.StringEncoding( - True, left_ordering_encoding_size + right_ordering_encoding_size - ), + # Preserve ordering accross joins. + ordering = join_orderings( + left._ordering, + right._ordering, + get_column_left, + get_column_right, + left_order_dominates=(how != "right"), ) left_join_keys = [ @@ -234,11 +205,21 @@ def get_column_right(key: str) -> str: for col in right.columns ] ) + hidden_ordering_columns = [ + *[ + combined_table[get_column_left(col.get_name())] + for col in left.hidden_ordering_columns + ], + *[ + combined_table[get_column_right(col.get_name())] + for col in right.hidden_ordering_columns + ], + ] combined_expr = core.ArrayValue( left._session, combined_table, columns=columns, - hidden_ordering_columns=hidden_columns, + hidden_ordering_columns=hidden_ordering_columns, ordering=ordering, ) if sort: @@ -313,32 +294,33 @@ def value_to_join_key(value: ibis_types.Value): return value.fillna(ibis_types.literal("$NULL_SENTINEL$")) -def _merge_order_ids( - left_id: ibis_types.StringColumn, - left_encoding_size: int, - right_id: ibis_types.StringColumn, - right_encoding_size: int, - how: str, -) -> ibis_types.StringColumn: - if how == "right": - return _merge_order_ids( - right_id, right_encoding_size, left_id, left_encoding_size, "left" - ) +def join_orderings( + left: core.ExpressionOrdering, + right: core.ExpressionOrdering, + left_id_mapping: Callable[[str], str], + right_id_mapping: Callable[[str], str], + left_order_dominates: bool = True, +) -> core.ExpressionOrdering: + left_ordering_refs = [ + ref.with_name(left_id_mapping(ref.column_id)) + for ref in left.all_ordering_columns + ] + right_ordering_refs = [ + ref.with_name(right_id_mapping(ref.column_id)) + for ref in right.all_ordering_columns + ] + if left_order_dominates: + joined_refs = [*left_ordering_refs, *right_ordering_refs] + else: + joined_refs = [*right_ordering_refs, *left_ordering_refs] - if how == "left": - right_id = typing.cast( - ibis_types.StringColumn, - right_id.fillna(ibis_types.literal(":" * right_encoding_size)), - ) - elif how != "inner": # outer join - left_id = typing.cast( - ibis_types.StringColumn, - left_id.fillna(ibis_types.literal(":" * left_encoding_size)), - ) - right_id = typing.cast( - ibis_types.StringColumn, - right_id.fillna(ibis_types.literal(":" * right_encoding_size)), - ) - return (left_id + right_id).name( - bigframes.core.guid.generate_guid(prefix="bigframes_ordering_id_") + left_total_order_cols = frozenset( + [left_id_mapping(id) for id in left.total_ordering_columns] + ) + right_total_order_cols = frozenset( + [right_id_mapping(id) for id in right.total_ordering_columns] + ) + return core.ExpressionOrdering( + ordering_value_columns=joined_refs, + total_ordering_columns=left_total_order_cols | right_total_order_cols, ) From edabdbb131150707ea9211292cacbb60b8d076dd Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 20 Sep 2023 17:22:07 -0700 Subject: [PATCH 025/921] fix: loosen filter items tests to accomodate shifting pandas impl (#41) --- tests/system/small/test_dataframe.py | 10 ++++++---- tests/system/small/test_series.py | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ed682c855b7..6c96387e97e 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2129,10 +2129,10 @@ def test_df_columns_filter_items(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.filter(items=["string_col", "int64_col"]).to_pandas() pd_result = scalars_pandas_df_index.filter(items=["string_col", "int64_col"]) - + # Ignore column ordering as pandas order differently depending on version pd.testing.assert_frame_equal( - bf_result, - pd_result, + bf_result.sort_index(axis=1), + pd_result.sort_index(axis=1), ) @@ -2167,9 +2167,11 @@ def test_df_rows_filter_items(scalars_df_index, scalars_pandas_df_index): # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - pd.testing.assert_frame_equal( + # Ignore ordering as pandas order differently depending on version + assert_pandas_df_equal_ignore_ordering( bf_result, pd_result, + check_names=False, ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index d3560540cc9..d702049e684 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1950,9 +1950,11 @@ def test_series_filter_items(scalars_df_index, scalars_pandas_df_index): # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - pd.testing.assert_series_equal( + # Ignore ordering as pandas order differently depending on version + assert_series_equal_ignoring_order( bf_result, pd_result, + check_names=False, ) From 109ee24108875389a654674569583e2c7a32d853 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 20 Sep 2023 19:52:36 -0700 Subject: [PATCH 026/921] refactor: remove ibis references outside of arrayvalue code. (#37) Change-Id: I1386355446e90f89a43cee8a9f447f0775639902 --- bigframes/core/__init__.py | 59 ++++++++++++++----- bigframes/core/blocks.py | 84 ++++++++++++++++++--------- bigframes/core/indexers.py | 44 +++++--------- bigframes/core/joins/single_column.py | 4 +- bigframes/core/scalar.py | 50 +--------------- bigframes/dataframe.py | 46 +++------------ bigframes/operations/__init__.py | 34 ++++++++--- bigframes/series.py | 16 +---- tests/unit/test_core.py | 12 ++-- 9 files changed, 160 insertions(+), 189 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 8e7beb73dbb..dd91f80e63b 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -269,7 +269,7 @@ def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) def apply_limit(self, max_results: int) -> ArrayValue: - table = self.to_ibis_expr( + table = self._to_ibis_expr( ordering_mode="order_by", expose_hidden_cols=True, ).limit(max_results) @@ -285,11 +285,23 @@ def apply_limit(self, max_results: int) -> ArrayValue: ordering=self._ordering, ) - def filter(self, predicate: ibis_types.BooleanValue) -> ArrayValue: + def filter(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: + """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + condition = typing.cast(ibis_types.BooleanValue, self.get_column(predicate_id)) + if keep_null: + condition = typing.cast( + ibis_types.BooleanValue, + condition.fillna( + typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) + ), + ) + return self._filter(condition) + + def _filter(self, predicate_value: ibis_types.BooleanValue) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" expr = self.builder() expr.ordering = expr.ordering.with_non_sequential() - expr.predicates = [*self._predicates, predicate] + expr.predicates = [*self._predicates, predicate_value] return expr.build() def order_by( @@ -310,7 +322,7 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: .. warning:: The row numbers of result is non-deterministic, avoid to use. """ - table = self.to_ibis_expr( + table = self._to_ibis_expr( ordering_mode="order_by", expose_hidden_cols=True, fraction=fraction ) columns = [table[column_name] for column_name in self._column_names] @@ -342,7 +354,7 @@ def project_offsets(self) -> ArrayValue: if self._ordering.is_sequential: return self # TODO(tbergeron): Enforce total ordering - table = self.to_ibis_expr( + table = self._to_ibis_expr( ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN ) columns = [table[column_name] for column_name in self._column_names] @@ -412,7 +424,7 @@ def projection(self, columns: Iterable[ibis_types.Value]) -> ArrayValue: def shape(self) -> typing.Tuple[int, int]: """Returns dimensions as (length, width) tuple.""" width = len(self.columns) - count_expr = self.to_ibis_expr(ordering_mode="unordered").count() + count_expr = self._to_ibis_expr(ordering_mode="unordered").count() sql = self._session.ibis_client.compile(count_expr) row_iterator, _ = self._session._start_query( sql=sql, @@ -435,7 +447,7 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: ) for i, expr in enumerate([self, *other]): ordering_prefix = str(i).zfill(prefix_size) - table = expr.to_ibis_expr( + table = expr._to_ibis_expr( ordering_mode="string_encoded", order_col_name=ORDER_ID_COLUMN ) # Rename the value columns based on horizontal offset before applying union. @@ -522,7 +534,7 @@ def aggregate( by_column_id: column id of the aggregation key, this is preserved through the transform dropna: whether null keys should be dropped """ - table = self.to_ibis_expr(ordering_mode="unordered") + table = self._to_ibis_expr(ordering_mode="unordered") stats = { col_out: agg_op._as_ibis(table[col_in]) for col_in, agg_op, col_out in aggregations @@ -541,7 +553,7 @@ def aggregate( expr = ArrayValue(self._session, result, columns=columns, ordering=ordering) if dropna: for column_id in by_column_ids: - expr = expr.filter( + expr = expr._filter( ops.notnull_op._as_ibis(expr.get_column(column_id)) ) # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation @@ -572,7 +584,7 @@ def corr_aggregate( Arguments: corr_aggregations: left_column_id, right_column_id, output_column_id tuples """ - table = self.to_ibis_expr(ordering_mode="unordered") + table = self._to_ibis_expr(ordering_mode="unordered") stats = { col_out: table[col_left].corr(table[col_right], how="pop") for col_left, col_right, col_out in corr_aggregations @@ -646,7 +658,24 @@ def project_window_op( # TODO(tbergeron): Automatically track analytic expression usage and defer reprojection until required for valid query generation. return result._reproject_to_table() if not skip_reproject_unsafe else result - def to_ibis_expr( + def to_sql( + self, + ordering_mode: Literal[ + "order_by", "string_encoded", "offset_col", "unordered" + ] = "order_by", + order_col_name: Optional[str] = ORDER_ID_COLUMN, + col_id_overrides: typing.Mapping[str, str] = {}, + ) -> str: + sql = self._session.ibis_client.compile( + self._to_ibis_expr( + ordering_mode=ordering_mode, + order_col_name=order_col_name, + col_id_overrides=col_id_overrides, + ) + ) + return typing.cast(str, sql) + + def _to_ibis_expr( self, ordering_mode: Literal[ "order_by", "string_encoded", "offset_col", "unordered" @@ -814,7 +843,7 @@ def start_query( # a LocalSession for unit testing. # TODO(swast): Add a timeout here? If the query is taking a long time, # maybe we just print the job metadata that we have so far? - table = self.to_ibis_expr(expose_hidden_cols=expose_extra_columns) + table = self._to_ibis_expr(expose_hidden_cols=expose_extra_columns) sql = self._session.ibis_client.compile(table) # type:ignore return self._session._start_query( sql=sql, @@ -833,7 +862,7 @@ def _reproject_to_table(self) -> ArrayValue: some operations such as window operations that cannot be used recursively in projections. """ - table = self.to_ibis_expr( + table = self._to_ibis_expr( ordering_mode="unordered", expose_hidden_cols=True, ) @@ -912,7 +941,7 @@ def unpivot( Returns: ArrayValue: The unpivoted ArrayValue """ - table = self.to_ibis_expr(ordering_mode="offset_col") + table = self._to_ibis_expr(ordering_mode="offset_col") sub_expressions = [] # Use ibis memtable to infer type of rowlabels (if possible) @@ -1054,7 +1083,7 @@ def slice( start = start if (start is not None) else last_offset cond_list.append((start - expr_with_offsets.offsets) % (-step) == 0) - sliced_expr = expr_with_offsets.filter( + sliced_expr = expr_with_offsets._filter( functools.reduce(lambda x, y: x & y, cond_list) ) return sliced_expr if step > 0 else sliced_expr.reversed() diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index fb9ede9f4c9..e691a30f9cd 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -30,8 +30,6 @@ import geopandas as gpd # type: ignore import google.cloud.bigquery as bigquery -import ibis.expr.schema as ibis_schema -import ibis.expr.types as ibis_types import numpy import pandas as pd import pyarrow as pa # type: ignore @@ -42,6 +40,7 @@ import bigframes.core.indexes as indexes import bigframes.core.ordering as ordering import bigframes.core.utils +import bigframes.core.utils as utils import bigframes.dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -368,7 +367,10 @@ def reorder_levels(self, ids: typing.Sequence[str]): level_names = [self.col_id_to_index_name[index_id] for index_id in ids] return Block(self.expr, ids, self.column_labels, level_names) - def _to_dataframe(self, result, schema: ibis_schema.Schema) -> pd.DataFrame: + @classmethod + def _to_dataframe( + cls, result, schema: typing.Mapping[str, bigframes.dtypes.Dtype] + ) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" df = result.to_dataframe( bool_dtype=pd.BooleanDtype(), @@ -382,8 +384,8 @@ def _to_dataframe(self, result, schema: ibis_schema.Schema) -> pd.DataFrame: ) # Convert Geography column from StringDType to GeometryDtype. - for column_name, ibis_dtype in schema.items(): - if ibis_dtype.is_geospatial(): + for column_name, dtype in schema.items(): + if dtype == gpd.array.GeometryDtype(): df[column_name] = gpd.GeoSeries.from_wkt( # https://github.com/geopandas/geopandas/issues/1879 df[column_name].replace({numpy.nan: None}), @@ -473,7 +475,8 @@ def _compute_and_count( if sampling_method == _HEAD: total_rows = int(results_iterator.total_rows * fraction) results_iterator.max_results = total_rows - df = self._to_dataframe(results_iterator, expr.to_ibis_expr().schema()) + schema = dict(zip(self.value_columns, self.dtypes)) + df = self._to_dataframe(results_iterator, schema) if self.index_columns: df.set_index(list(self.index_columns), inplace=True) @@ -508,7 +511,8 @@ def _compute_and_count( ) else: total_rows = results_iterator.total_rows - df = self._to_dataframe(results_iterator, expr.to_ibis_expr().schema()) + schema = dict(zip(self.value_columns, self.dtypes)) + df = self._to_dataframe(results_iterator, schema) if self.index_columns: df.set_index(list(self.index_columns), inplace=True) @@ -639,13 +643,6 @@ def with_index_labels(self, value: typing.Sequence[Label]) -> Block: index_labels=tuple(value), ) - def get_value_col_exprs( - self, column_names: Optional[Sequence[str]] = None - ) -> List[ibis_types.Value]: - """Retrive value column expressions.""" - column_names = self.value_columns if column_names is None else column_names - return [self._expr.get_column(column_name) for column_name in column_names] - def apply_unary_op( self, column: str, op: ops.UnaryOp, result_label: Label = None ) -> typing.Tuple[Block, str]: @@ -816,20 +813,9 @@ def assign_label(self, column_id: str, new_label: Label) -> Block: ) return self.with_column_labels(new_labels) - def filter(self, column_name: str, keep_null: bool = False): - condition = typing.cast( - ibis_types.BooleanValue, self._expr.get_column(column_name) - ) - if keep_null: - condition = typing.cast( - ibis_types.BooleanValue, - condition.fillna( - typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) - ), - ) - filtered_expr = self.expr.filter(condition) + def filter(self, column_id: str, keep_null: bool = False): return Block( - filtered_expr, + self._expr.filter(column_id, keep_null), index_columns=self.index_columns, column_labels=self.column_labels, index_labels=self.index.names, @@ -1436,6 +1422,50 @@ def is_monotonic_decreasing( ) -> bool: return self._is_monotonic(column_id, increasing=False) + def to_sql_query( + self, include_index: bool + ) -> typing.Tuple[str, list[str], list[Label]]: + """ + Compiles this DataFrame's expression tree to SQL, optionally + including index columns. + + Args: + include_index (bool): + whether to include index columns. + + Returns: + a tuple of (sql_string, index_column_id_list, index_column_label_list). + If include_index is set to False, index_column_id_list and index_column_label_list + return empty lists. + """ + array_value = self._expr + col_labels, idx_labels = list(self.column_labels), list(self.index_labels) + old_col_ids, old_idx_ids = list(self.value_columns), list(self.index_columns) + + if not include_index: + idx_labels, old_idx_ids = [], [] + array_value = array_value.drop_columns(self.index_columns) + + old_ids = old_idx_ids + old_col_ids + + new_col_ids, new_idx_ids = utils.get_standardized_ids(col_labels, idx_labels) + new_ids = new_idx_ids + new_col_ids + + substitutions = {} + for old_id, new_id in zip(old_ids, new_ids): + # TODO(swast): Do we need to further escape this, or can we rely on + # the BigQuery unicode column name feature? + substitutions[old_id] = new_id + + sql = array_value.to_sql( + ordering_mode="unordered", col_id_overrides=substitutions + ) + return ( + sql, + new_ids[: len(idx_labels)], + idx_labels, + ) + def _is_monotonic( self, column_ids: typing.Union[str, Sequence[str]], increasing: bool ) -> bool: diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 28bce05338c..a538c80711b 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -21,11 +21,11 @@ import pandas as pd import bigframes.constants as constants -import bigframes.core as core import bigframes.core.guid as guid import bigframes.core.indexes as indexes import bigframes.core.scalar import bigframes.dataframe +import bigframes.operations as ops import bigframes.series if typing.TYPE_CHECKING: @@ -59,35 +59,23 @@ def __setitem__(self, key, value) -> None: # Assume the key is for the index label. block = self._series._block - value_column = self._series._value - index_column = block.expr.get_column(block.index_columns[0]) - new_value = ( - ibis.case() - .when( - index_column == ibis.literal(key, index_column.type()), - ibis.literal(value, value_column.type()), - ) - .else_(value_column) - .end() - .name(value_column.get_name()) + value_column = self._series._value_column + index_column = block.index_columns[0] + + # if index == key return value else value_colum + block, insert_cond = block.apply_unary_op( + index_column, ops.partial_right(ops.eq_op, key) ) - all_columns = [] - for column in block.expr.columns: - if column.get_name() != value_column.get_name(): - all_columns.append(column) - else: - all_columns.append(new_value) - new_expr = block.expr.projection(all_columns) - - # TODO(tbergeron): Use block operators rather than directly building desired ibis expressions. - self._series._set_block( - core.blocks.Block( - new_expr, - self._series._block.index_columns, - self._series._block.column_labels, - self._series._block.index.names, - ) + block, result_id = block.apply_binary_op( + insert_cond, + self._series._value_column, + ops.partial_arg1(ops.where_op, value), ) + block = block.copy_values(result_id, value_column).drop_columns( + [insert_cond, result_id] + ) + + self._series._set_block(block) class IlocSeriesIndexer: diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py index 7aba71fd95e..434cc2cd794 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/joins/single_column.py @@ -122,11 +122,11 @@ def join_by_column( ), ) else: - left_table = left.to_ibis_expr( + left_table = left._to_ibis_expr( ordering_mode="unordered", expose_hidden_cols=True, ) - right_table = right.to_ibis_expr( + right_table = right._to_ibis_expr( ordering_mode="unordered", expose_hidden_cols=True, ) diff --git a/bigframes/core/scalar.py b/bigframes/core/scalar.py index 6dfbd31b77b..5db83b4a627 100644 --- a/bigframes/core/scalar.py +++ b/bigframes/core/scalar.py @@ -14,55 +14,7 @@ from __future__ import annotations -import typing -from typing import Any, Optional - -import google.cloud.bigquery as bigquery -import ibis.expr.types as ibis_types - -import bigframes -import bigframes.formatting_helpers as formatter - -if typing.TYPE_CHECKING: - import bigframes.session - - -class DeferredScalar: - """A deferred scalar object.""" - - def __init__(self, value: ibis_types.Scalar, session: bigframes.session.Session): - self._value = value - self._session = session - self._query_job: Optional[bigquery.QueryJob] = None - - @property - def query_job(self) -> Optional[bigquery.QueryJob]: - """BigQuery job metadata for the most recent query.""" - if self._query_job is None: - self._query_job = self._compute_dry_run() - return self._query_job - - def __repr__(self) -> str: - """Converts a Series to a string.""" - # TODO(swast): Add a timeout here? If the query is taking a long time, - # maybe we just print the job metadata that we have so far? - opts = bigframes.options.display - if opts.repr_mode == "deferred": - return formatter.repr_query_job(self.query_job) - else: - return repr(self.to_pandas()) - - def to_pandas(self) -> Any: - """Executes deferred operations and downloads the resulting scalar.""" - result, query_job = self._session._start_query(self._value.compile()) - self._query_job = query_job - df = self._session._rows_to_dataframe(result) - return df.iloc[0, 0] - - def _compute_dry_run(self): - job_config = bigquery.QueryJobConfig(dry_run=True) - return self._session._start_query(self._value.compile(), job_config=job_config) - +from typing import Any # All public APIs return Any at present # Later implementation may sometimes return a lazy scalar diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index de4adb912e6..46c27eaccbf 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -312,37 +312,7 @@ def _to_sql_query( If include_index is set to False, index_column_id_list and index_column_label_list return empty lists. """ - # Has to be unordered as it is impossible to order the sql without - # including metadata columns in selection with ibis. - ibis_expr = self._block.expr.to_ibis_expr(ordering_mode="unordered") - col_labels, idx_labels = list(self._block.column_labels), list( - self._block.index_labels - ) - old_col_ids, old_idx_ids = list(self._block.value_columns), list( - self._block.index_columns - ) - - if not include_index: - idx_labels, old_idx_ids = [], [] - ibis_expr = ibis_expr.drop(*self._block.index_columns) - - old_ids = old_idx_ids + old_col_ids - - new_col_ids, new_idx_ids = utils.get_standardized_ids(col_labels, idx_labels) - new_ids = new_idx_ids + new_col_ids - - substitutions = {} - for old_id, new_id in zip(old_ids, new_ids): - # TODO(swast): Do we need to further escape this, or can we rely on - # the BigQuery unicode column name feature? - substitutions[old_id] = new_id - - ibis_expr = ibis_expr.relabel(substitutions) - return ( - typing.cast(str, ibis_expr.compile()), - new_ids[: len(idx_labels)], - idx_labels, - ) + return self._block.to_sql_query(include_index) @property def sql(self) -> str: @@ -2340,8 +2310,7 @@ def _apply_unary_op(self, operation: ops.UnaryOp) -> DataFrame: def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: """Create query text representing this dataframe for I/O.""" - expr = self._block.expr - session = expr._session + array_value = self._block.expr columns = list(self._block.value_columns) column_labels = list(self._block.column_labels) # This code drops unnamed indexes to keep consistent with the behavior of @@ -2352,7 +2321,7 @@ def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: columns.extend(self._block.index_columns) column_labels.extend(self.index.names) else: - expr = expr.drop_columns(self._block.index_columns) + array_value = array_value.drop_columns(self._block.index_columns) # Make columns in SQL reflect _labels_ not _ids_. Note: This may use # the arbitrary unicode column labels feature in BigQuery, which is @@ -2365,19 +2334,17 @@ def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: } if ordering_id is not None: - ibis_expr = expr.to_ibis_expr( + return array_value.to_sql( ordering_mode="offset_col", col_id_overrides=id_overrides, order_col_name=ordering_id, ) else: - ibis_expr = expr.to_ibis_expr( + return array_value.to_sql( ordering_mode="unordered", col_id_overrides=id_overrides, ) - return session.ibis_client.compile(ibis_expr) # type: ignore - def _run_io_query( self, index: bool, @@ -2458,6 +2425,9 @@ def rank( df = self._drop_non_numeric() if numeric_only else self return DataFrame(block_ops.rank(df._block, method, na_option, ascending)) + def first_valid_index(self): + return + applymap = map def _slice( diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index c5c55607ae5..bc08298eb7b 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -206,7 +206,7 @@ def _as_ibis(self, x: ibis_types.Value): class LenOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).length() + return typing.cast(ibis_types.StringValue, x).length().cast(ibis_dtypes.int64) class NotNullOp(UnaryOp): @@ -443,7 +443,7 @@ def _as_ibis(self, x: ibis_types.Value): ## Datetime Ops class DayOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).day() + return typing.cast(ibis_types.TimestampValue, x).day().cast(ibis_dtypes.int64) class DateOp(UnaryOp): @@ -453,32 +453,42 @@ def _as_ibis(self, x: ibis_types.Value): class DayofweekOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).day_of_week.index() + return ( + typing.cast(ibis_types.TimestampValue, x) + .day_of_week.index() + .cast(ibis_dtypes.int64) + ) class HourOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).hour() + return typing.cast(ibis_types.TimestampValue, x).hour().cast(ibis_dtypes.int64) class MinuteOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).minute() + return ( + typing.cast(ibis_types.TimestampValue, x).minute().cast(ibis_dtypes.int64) + ) class MonthOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).month() + return typing.cast(ibis_types.TimestampValue, x).month().cast(ibis_dtypes.int64) class QuarterOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).quarter() + return ( + typing.cast(ibis_types.TimestampValue, x).quarter().cast(ibis_dtypes.int64) + ) class SecondOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).second() + return ( + typing.cast(ibis_types.TimestampValue, x).second().cast(ibis_dtypes.int64) + ) class TimeOp(UnaryOp): @@ -488,7 +498,7 @@ def _as_ibis(self, x: ibis_types.Value): class YearOp(UnaryOp): def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).year() + return typing.cast(ibis_types.TimestampValue, x).year().cast(ibis_dtypes.int64) # Parameterized ops @@ -969,6 +979,12 @@ def fillna_op( return x.fillna(typing.cast(ibis_types.Scalar, y)) +def round_op(x: ibis_types.Value, y: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).round( + digits=typing.cast(ibis_types.IntegerValue, y) + ) + + def clip_lower( value: ibis_types.Value, lower: ibis_types.Value, diff --git a/bigframes/series.py b/bigframes/series.py index 9db64fae9c9..8f3a24698b3 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -22,7 +22,6 @@ from typing import Any, Mapping, Optional, Tuple, Union import google.cloud.bigquery as bigquery -import ibis.expr.types as ibis_types import numpy import pandas import pandas.core.dtypes.common @@ -223,14 +222,6 @@ def __repr__(self) -> str: return repr(pandas_df.iloc[:, 0]) - def _to_ibis_expr(self): - """Creates an Ibis table expression representing the Series.""" - expr = self._block.expr.projection([self._value]) - ibis_expr = expr.to_ibis_expr()[self._value_column] - if self._name: - return ibis_expr.name(self._name) - return ibis_expr - def astype( self, dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], @@ -661,12 +652,7 @@ def abs(self) -> Series: return self._apply_unary_op(ops.abs_op) def round(self, decimals=0) -> "Series": - def round_op(x: ibis_types.Value, y: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).round( - digits=typing.cast(ibis_types.IntegerValue, y) - ) - - return self._apply_binary_op(decimals, round_op) + return self._apply_binary_op(decimals, ops.round_op) def corr(self, other: Series, method="pearson", min_periods=None) -> float: """ diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 8f3e0beb0e4..ee0cefb3d28 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -103,7 +103,7 @@ def test_arrayvalue_to_ibis_expr_with_projection(): value.table["col2"].name("string_col"), ] ) - actual = expr.to_ibis_expr() + actual = expr._to_ibis_expr() assert len(actual.columns) == 3 assert actual.columns[0] == "int64_col" assert actual.columns[1] == "literals" @@ -138,7 +138,7 @@ def test_arrayvalues_to_ibis_expr_with_concat(): total_ordering_columns=["col1"], ) expr = value.concat([value]) - actual = expr.to_ibis_expr() + actual = expr._to_ibis_expr() assert len(actual.columns) == 3 # TODO(ashleyxu, b/299631930): test out the union expression assert actual.columns[0] == "column_0" @@ -175,7 +175,7 @@ def test_arrayvalues_to_ibis_expr_with_project_binary_op(): ) expr = value.project_binary_op("col2", "col3", ops.add_op, "col4") assert expr.columns[3].type().is_float64() - actual = expr.to_ibis_expr() + actual = expr._to_ibis_expr() assert len(expr.columns) == 4 assert actual.columns[3] == "col4" @@ -194,7 +194,7 @@ def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): ) expr = value.project_ternary_op("col2", "col3", "col4", ops.where_op, "col5") assert expr.columns[4].type().is_float64() - actual = expr.to_ibis_expr() + actual = expr._to_ibis_expr() assert len(expr.columns) == 5 assert actual.columns[4] == "col5" @@ -215,7 +215,7 @@ def test_arrayvalue_to_ibis_expr_with_aggregate(): by_column_ids=["col1"], dropna=False, ) - actual = expr.to_ibis_expr() + actual = expr._to_ibis_expr() assert len(expr.columns) == 2 assert actual.columns[0] == "col1" assert actual.columns[1] == "col4" @@ -234,7 +234,7 @@ def test_arrayvalue_to_ibis_expr_with_corr_aggregate(): total_ordering_columns=["col1"], ) expr = value.corr_aggregate(corr_aggregations=[("col1", "col3", "col4")]) - actual = expr.to_ibis_expr() + actual = expr._to_ibis_expr() assert len(expr.columns) == 1 assert actual.columns[0] == "col4" assert expr.columns[0].type().is_float64() From 3adc1b3aa3e2b218d4fa5debdaa4298276bdf801 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 21 Sep 2023 09:56:41 -0700 Subject: [PATCH 027/921] feat: add `items`, `apply` methods to `DataFrame`. (#43) Change-Id: Id3a0e78da3bb9ccce64e190f7797f737b239c33f Co-authored-by: Tim Swast --- bigframes/dataframe.py | 18 +++++++ tests/system/small/test_dataframe.py | 51 +++++++++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 34 +++++++++++++ 3 files changed, 103 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 46c27eaccbf..0b741feff67 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1418,6 +1418,12 @@ def isin(self, values) -> DataFrame: f"isin(), you passed a [{type(values).__name__}]" ) + def items(self): + column_ids = self._block.value_columns + column_labels = self._block.column_labels + for col_id, col_label in zip(column_ids, column_labels): + yield col_label, bigframes.series.Series(self._block.select_column(col_id)) + def dropna( self, *, @@ -2382,6 +2388,18 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: ops.RemoteFunctionOp(func, apply_on_null=(na_action is None)) ) + def apply(self, func, *, args: typing.Tuple = (), **kwargs): + results = {name: func(col, *args, **kwargs) for name, col in self.items()} + if all( + [ + isinstance(val, bigframes.series.Series) or utils.is_list_like(val) + for val in results.values() + ] + ): + return DataFrame(data=results) + else: + return pandas.Series(data=results) + def drop_duplicates( self, subset: typing.Union[blocks.Label, typing.Sequence[blocks.Label]] = None, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 6c96387e97e..3eeb368ad23 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -663,6 +663,57 @@ def test_df_bfill(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_apply_series_series_callable( + scalars_df_index, + scalars_pandas_df_index, +): + columns = ["int64_too", "int64_col"] + + def foo(series, arg1, arg2, *, kwarg1=0, kwarg2=0): + return series**2 + (arg1 * arg2 % 4) + (kwarg1 * kwarg2 % 7) + + bf_result = ( + scalars_df_index[columns] + .apply(foo, args=(33, 61), kwarg1=52, kwarg2=21) + .to_pandas() + ) + + pd_result = scalars_pandas_df_index[columns].apply( + foo, args=(33, 61), kwarg1=52, kwarg2=21 + ) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_apply_series_listlike_callable( + scalars_df_index, + scalars_pandas_df_index, +): + columns = ["int64_too", "int64_col"] + bf_result = ( + scalars_df_index[columns].apply(lambda x: [len(x), x.min(), 24]).to_pandas() + ) + + pd_result = scalars_pandas_df_index[columns].apply(lambda x: [len(x), x.min(), 24]) + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result.index = pd_result.index.astype("Int64") + pd_result = pd_result.astype("Int64") + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_apply_series_scalar_callable( + scalars_df_index, + scalars_pandas_df_index, +): + columns = ["int64_too", "int64_col"] + bf_result = scalars_df_index[columns].apply(lambda x: x.sum()) + + pd_result = scalars_pandas_df_index[columns].apply(lambda x: x.sum()) + + pandas.testing.assert_series_equal(bf_result, pd_result) + + def test_df_isin_list(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs values = ["Hello, World!", 55555, 2.51, pd.NA, True] diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 27cc2144e0f..9d26938e080 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -734,6 +734,18 @@ def isin(self, values): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def items(self): + """ + Iterate over (column name, Series) pairs. + + Iterates over the DataFrame columns, returning a tuple with + the column name and the content as a Series. + + Returns: + Iterator: Iterator of label, Series for each column. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # Sorting @@ -1420,6 +1432,28 @@ def merge( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def apply(self, func, *, args=(), **kwargs): + """Apply a function along an axis of the DataFrame. + + Objects passed to the function are Series objects whose index is + the DataFrame's index (``axis=0``) the final return type + is inferred from the return type of the applied function. + + Args: + func (function): + Function to apply to each column or row. + args (tuple): + Positional arguments to pass to `func` in addition to the + array/series. + **kwargs: + Additional keyword arguments to pass as keywords arguments to + `func`. + + Returns: + pandas.Series or bigframes.DataFrame: Result of applying ``func`` along the given axis of the DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # ndarray-like stats methods From 1a254a496633957b9506dd8392dcc6fd10762201 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 21 Sep 2023 10:42:59 -0700 Subject: [PATCH 028/921] feat: add index `dtype`, `astype`, `drop`, `fillna`, aggregate attributes. (#38) Change-Id: I4af249d10b2fcd779ad05d1f1d95049893e40135 --- bigframes/core/indexes/index.py | 155 ++++++++++++++++-- bigframes/series.py | 4 +- tests/system/small/test_index.py | 110 +++++++++++++ .../pandas/core/indexes/base.py | 120 ++++++++++++++ 4 files changed, 373 insertions(+), 16 deletions(-) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 748a68c9447..c08c851c91d 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -17,7 +17,7 @@ from __future__ import annotations import typing -from typing import Callable, Tuple +from typing import Callable, Sequence, Tuple, Union import numpy as np import pandas @@ -26,7 +26,11 @@ import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.joins as joins +import bigframes.core.utils as utils +import bigframes.dtypes import bigframes.dtypes as bf_dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops import third_party.bigframes_vendored.pandas.core.indexes.base as vendored_pandas_index @@ -51,16 +55,34 @@ def names(self) -> typing.Sequence[blocks.Label]: @names.setter def names(self, values: typing.Sequence[blocks.Label]): - return self._data._set_block(self._data._get_block().with_index_labels(values)) + return self._data._set_block(self._block.with_index_labels(values)) @property def nlevels(self) -> int: return len(self._data._get_block().index_columns) + @property + def values(self) -> np.ndarray: + return self.to_numpy() + + @property + def ndim(self) -> int: + return 1 + @property def shape(self) -> typing.Tuple[int]: return (self._data._get_block().shape[0],) + @property + def dtype(self): + return self._block.index_dtypes[0] if self.nlevels == 1 else np.dtype("O") + + @property + def dtypes(self) -> pandas.Series: + return pandas.Series( + data=self._block.index_dtypes, index=self._block.index_labels # type:ignore + ) + @property def size(self) -> int: """Returns the size of the Index.""" @@ -103,23 +125,120 @@ def is_monotonic_decreasing(self) -> bool: @property def is_unique(self) -> bool: + # TODO: Cache this at block level + # Avoid circular imports + return not self.has_duplicates + + @property + def has_duplicates(self) -> bool: # TODO: Cache this at block level # Avoid circular imports import bigframes.core.block_transforms as block_ops import bigframes.dataframe as df - duplicates_block, _ = block_ops.indicate_duplicates( - self._data._get_block(), self._data._get_block().index_columns - ) - duplicates_block = duplicates_block.with_column_labels( - ["values", "is_duplicate"] + duplicates_block, indicator = block_ops.indicate_duplicates( + self._block, self._block.index_columns ) + duplicates_block = duplicates_block.select_columns( + [indicator] + ).with_column_labels(["is_duplicate"]) duplicates_df = df.DataFrame(duplicates_block) - return not duplicates_df["is_duplicate"].any() + return duplicates_df["is_duplicate"].any() + + @property + def _block(self) -> blocks.Block: + return self._data._get_block() + + def astype( + self, + dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], + ) -> Index: + if self.nlevels > 1: + raise TypeError("Multiindex does not support 'astype'") + return self._apply_unary_op(ops.AsTypeOp(dtype)) + + def all(self) -> bool: + if self.nlevels > 1: + raise TypeError("Multiindex does not support 'all'") + return typing.cast(bool, self._apply_aggregation(agg_ops.all_op)) + + def any(self) -> bool: + if self.nlevels > 1: + raise TypeError("Multiindex does not support 'any'") + return typing.cast(bool, self._apply_aggregation(agg_ops.any_op)) + + def nunique(self) -> int: + return typing.cast(int, self._apply_aggregation(agg_ops.nunique_op)) + + def max(self) -> typing.Any: + return self._apply_aggregation(agg_ops.max_op) + + def min(self) -> typing.Any: + return self._apply_aggregation(agg_ops.min_op) + + def fillna(self, value=None) -> Index: + if self.nlevels > 1: + raise TypeError("Multiindex does not support 'fillna'") + return self._apply_unary_op(ops.partial_right(ops.fillna_op, value)) + + def rename(self, name: Union[str, Sequence[str]]) -> Index: + names = [name] if isinstance(name, str) else list(name) + if len(names) != self.nlevels: + raise ValueError("'name' must be same length as levels") + + import bigframes.dataframe as df + + return Index(df.DataFrame(self._block.with_index_labels(names))) + + def drop( + self, + labels: typing.Any, + ) -> Index: + # ignore axis, columns params + block = self._block + level_id = self._block.index_columns[0] + if utils.is_list_like(labels): + block, inverse_condition_id = block.apply_unary_op( + level_id, ops.IsInOp(labels, match_nulls=True) + ) + block, condition_id = block.apply_unary_op( + inverse_condition_id, ops.invert_op + ) + else: + block, condition_id = block.apply_unary_op( + level_id, ops.partial_right(ops.ne_op, labels) + ) + block = block.filter(condition_id, keep_null=True) + block = block.drop_columns([condition_id]) + import bigframes.dataframe as df + + return Index(df.DataFrame(block.select_columns([]))) + + def _apply_unary_op( + self, + op: ops.UnaryOp, + ) -> Index: + """Applies a unary operator to the index.""" + block = self._block + result_ids = [] + for col in self._block.index_columns: + block, result_id = block.apply_unary_op(col, op) + result_ids.append(result_id) + + block = block.set_index(result_ids, index_labels=self._block.index_labels) + import bigframes.dataframe as df + + return Index(df.DataFrame(block)) + + def _apply_aggregation(self, op: agg_ops.AggregateOp) -> typing.Any: + if self.nlevels > 1: + raise NotImplementedError(f"Multiindex does not yet support {op.name}") + column_id = self._block.index_columns[0] + return self._block.get_stat(column_id, op) def __getitem__(self, key: int) -> typing.Any: if isinstance(key, int): - result_pd_df, _ = self._data._get_block().slice(key, key + 1, 1).to_pandas() + result_pd_df, _ = self._block.slice(key, key + 1, 1).to_pandas() if result_pd_df.empty: raise IndexError("single positional indexer is out-of-bounds") return result_pd_df.index[0] @@ -133,7 +252,7 @@ def to_pandas(self) -> pandas.Index: pandas.Index: A pandas Index with all of the labels from this Index. """ - return IndexValue(self._data._get_block()).to_pandas() + return IndexValue(self._block).to_pandas() def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: return self.to_pandas().to_numpy(dtype, **kwargs) @@ -184,13 +303,15 @@ def __repr__(self) -> str: def to_pandas(self) -> pandas.Index: """Executes deferred operations and downloads the results.""" # Project down to only the index column. So the query can be cached to visualize other data. - index_column = self._block.index_columns[0] - expr = self._expr.projection([self._expr.get_any_column(index_column)]) + index_columns = list(self._block.index_columns) + expr = self._expr.projection( + [self._expr.get_any_column(col) for col in index_columns] + ) results, _ = expr.start_query() df = expr._session._rows_to_dataframe(results) - df.set_index(index_column) + df = df.set_index(index_columns) index = df.index - index.name = self._block._index_labels[0] + index.names = list(self._block._index_labels) return index def join( @@ -235,6 +356,12 @@ def resolve_level_name(self: IndexValue, label: blocks.Label) -> str: def is_uniquely_named(self: IndexValue): return len(set(self.names)) == len(self.names) + def _set_block(self, block: blocks.Block): + self._block = block + + def _get_block(self) -> blocks.Block: + return self._block + def join_mono_indexed( left: IndexValue, diff --git a/bigframes/series.py b/bigframes/series.py index 8f3a24698b3..8e47088c146 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -287,7 +287,7 @@ def drop( # ignore axis, columns params block = self._block level_id = self._resolve_levels(level or 0)[0] - if _is_list_like(labels): + if _is_list_like(index): block, inverse_condition_id = block.apply_unary_op( level_id, ops.IsInOp(index, match_nulls=True) ) @@ -296,7 +296,7 @@ def drop( ) else: block, condition_id = block.apply_unary_op( - level_id, ops.partial_right(ops.ne_op, labels) + level_id, ops.partial_right(ops.ne_op, index) ) block = block.filter(condition_id, keep_null=True) block = block.drop_columns([condition_id]) diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index 558dd12e697..7f09e3a9d51 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -13,6 +13,7 @@ # limitations under the License. import numpy +import pandas as pd from tests.system.utils import assert_pandas_index_equal_ignore_index_type @@ -25,6 +26,44 @@ def test_get_index(scalars_df_index, scalars_pandas_df_index): assert_pandas_index_equal_ignore_index_type(bf_result, pd_result) +def test_index_has_duplicates(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.has_duplicates + pd_result = scalars_pandas_df_index.set_index("int64_col").index.has_duplicates + assert bf_result == pd_result + + +def test_index_values(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.index.values + pd_result = scalars_pandas_df_index.index.values + + # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe + pd.testing.assert_series_equal( + pd.Series(bf_result), pd.Series(pd_result), check_dtype=False + ) + + +def test_index_ndim(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.index.ndim + pd_result = scalars_pandas_df_index.index.ndim + + assert pd_result == bf_result + + +def test_index_dtype(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.index.dtype + pd_result = scalars_pandas_df_index.index.dtype + + assert pd_result == bf_result + + +def test_index_dtypes(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index(["string_col", "int64_too"]).index.dtypes + pd_result = scalars_pandas_df_index.set_index( + ["string_col", "int64_too"] + ).index.dtypes + pd.testing.assert_series_equal(bf_result, pd_result) + + def test_index_shape(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.index.shape pd_result = scalars_pandas_df_index.index.shape @@ -32,6 +71,77 @@ def test_index_shape(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result +def test_index_astype(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index.set_index("int64_col").index.astype("Float64").to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index("int64_col").index.astype("Float64") + pd.testing.assert_index_equal(bf_result, pd_result) + + +def test_index_any(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.any() + pd_result = scalars_pandas_df_index.set_index("int64_col").index.any() + assert bf_result == pd_result + + +def test_index_all(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.all() + pd_result = scalars_pandas_df_index.set_index("int64_col").index.all() + assert bf_result == pd_result + + +def test_index_max(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.max() + pd_result = scalars_pandas_df_index.set_index("int64_col").index.max() + assert bf_result == pd_result + + +def test_index_min(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.min() + pd_result = scalars_pandas_df_index.set_index("int64_col").index.min() + assert bf_result == pd_result + + +def test_index_nunique(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.nunique() + pd_result = scalars_pandas_df_index.set_index("int64_col").index.nunique() + assert bf_result == pd_result + + +def test_index_fillna(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.fillna(42).to_pandas() + pd_result = scalars_pandas_df_index.set_index("int64_col").index.fillna(42) + + pd.testing.assert_index_equal(bf_result, pd_result) + + +def test_index_drop(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index.set_index("int64_col").index.drop([2, 314159]).to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index("int64_col").index.drop([2, 314159]) + pd.testing.assert_index_equal(bf_result, pd_result) + + +def test_index_rename(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index("int64_col").index.rename("name").to_pandas() + pd_result = scalars_pandas_df_index.set_index("int64_col").index.rename("name") + pd.testing.assert_index_equal(bf_result, pd_result) + + +def test_index_multi_rename(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index.set_index(["int64_col", "int64_too"]) + .index.rename(["new", "names"]) + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index( + ["int64_col", "int64_too"] + ).index.rename(["new", "names"]) + pd.testing.assert_index_equal(bf_result, pd_result) + + def test_index_len(scalars_df_index, scalars_pandas_df_index): bf_result = len(scalars_df_index.index) pd_result = len(scalars_pandas_df_index.index) diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index 864007b7749..f89964e2208 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -1,4 +1,5 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexes/base.py +from __future__ import annotations from bigframes import constants @@ -14,6 +15,11 @@ def name(self): """Returns Index name.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def values(self): + """Return an array representing the data in the Index.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property def shape(self): """ @@ -31,6 +37,120 @@ def is_unique(self) -> bool: """Return if the index has unique values.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def has_duplicates(self) -> bool: + """Check if the Index has duplicate values.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def dtype(self): + """Return the dtype object of the underlying data.""" + + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def dtypes(self): + """Return the dtypes as a Series for the underlying MultiIndex.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def astype(self, dtype): + """Create an Index with values cast to dtypes. + + The class of a new Index is determined by dtype. When conversion is + impossible, a TypeError exception is raised. + + Args: + dtype (numpy dtype or pandas type): + + Returns: + Index: Index with values cast to specified dtype. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def all(self) -> bool: + """Return whether all elements are Truthy. + + Returns: + bool: A single element array-like may be converted to bool. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def any(self) -> bool: + """Return whether any element is Truthy. + + Returns: + bool: A single element array-like may be converted to bool. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def min(self): + """Return the minimum value of the Index. + + Returns: + scalar: Minimum value. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def max(self): + """Return the maximum value of the Index. + + Returns: + scalar: Maximum value. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def nunique(self) -> int: + """Return number of unique elements in the object. + + Excludes NA values by default. + + Returns: + int + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def fillna(self, value) -> Index: + """ + Fill NA/NaN values with the specified value. + + Args: + value (scalar): + Scalar value to use to fill holes (e.g. 0). + This value cannot be a list-likes. + + Returns: + Index + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def rename(self, name) -> Index: + """ + Alter Index or MultiIndex name. + + Able to set new names without level. Defaults to returning new index. + Length of names must match number of levels in MultiIndex. + + Args: + name (label or list of labels): + Name(s) to set. + + Returns: + Index: The same type as the caller. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def drop(self, labels) -> Index: + """ + Make new Index with passed list of labels deleted. + + Args: + labels (array-like or scalar): + + Returns: + Index: Will be same type as self + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def to_numpy(self, dtype): """ A NumPy ndarray representing the values in this Series or Index. From 5e199ecf1ecf13a68a2ed0dd4464afd9db977ab1 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 21 Sep 2023 14:28:13 -0500 Subject: [PATCH 029/921] perf: inline small `Series` and `DataFrames` in query text (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prevents unnecessary load and query jobs. Towards internal issue 296474170 🦕 --- bigframes/core/__init__.py | 62 ++++++++++++++++++++----- bigframes/core/blocks.py | 55 +++++++++++----------- bigframes/dataframe.py | 4 +- bigframes/dtypes.py | 21 +++++---- bigframes/operations/base.py | 4 +- setup.py | 4 +- testing/constraints-3.9.txt | 2 +- tests/unit/core/__init__.py | 13 ++++++ tests/unit/core/test_blocks.py | 85 ++++++++++++++++++++++++++++++++++ 9 files changed, 194 insertions(+), 56 deletions(-) create mode 100644 tests/unit/core/__init__.py create mode 100644 tests/unit/core/test_blocks.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index dd91f80e63b..3b3754642e7 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -144,21 +144,56 @@ def mem_expr_from_pandas( """ Builds an in-memory only (SQL only) expr from a pandas dataframe. - Caution: If session is None, only a subset of expr functionality will be available (null Session is usually not supported). + Caution: If session is None, only a subset of expr functionality will + be available (null Session is usually not supported). """ - # must set non-null column labels. these are not the user-facing labels - pd_df = pd_df.set_axis( - [column or bigframes.core.guid.generate_guid() for column in pd_df.columns], - axis="columns", - ) + # We can't include any hidden columns in the ArrayValue constructor, so + # grab the column names before we add the hidden ordering column. + column_names = [str(column) for column in pd_df.columns] + # Make sure column names are all strings. + pd_df = pd_df.set_axis(column_names, axis="columns") pd_df = pd_df.assign(**{ORDER_ID_COLUMN: range(len(pd_df))}) + # ibis memtable cannot handle NA, must convert to None pd_df = pd_df.astype("object") # type: ignore pd_df = pd_df.where(pandas.notnull(pd_df), None) + + # NULL type isn't valid in BigQuery, so retry with an explicit schema in these cases. keys_memtable = ibis.memtable(pd_df) + schema = keys_memtable.schema() + new_schema = [] + for column_index, column in enumerate(schema): + if column == ORDER_ID_COLUMN: + new_type: ibis_dtypes.DataType = ibis_dtypes.int64 + else: + column_type = schema[column] + # The autodetected type might not be one we can support, such + # as NULL type for empty rows, so convert to a type we do + # support. + new_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype( + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(column_type) + ) + # TODO(swast): Ibis memtable doesn't use backticks in struct + # field names, so spaces and other characters aren't allowed in + # the memtable context. Blocked by + # https://github.com/ibis-project/ibis/issues/7187 + column = f"col_{column_index}" + new_schema.append((column, new_type)) + + # must set non-null column labels. these are not the user-facing labels + pd_df = pd_df.set_axis( + [column for column, _ in new_schema], + axis="columns", + ) + keys_memtable = ibis.memtable(pd_df, schema=ibis.schema(new_schema)) + return cls( session, # type: ignore # Session cannot normally be none, see "caution" above keys_memtable, + columns=[ + keys_memtable[f"col_{column_index}"].name(column) + for column_index, column in enumerate(column_names) + ], ordering=ExpressionOrdering( ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], total_ordering_columns=frozenset([ORDER_ID_COLUMN]), @@ -426,11 +461,16 @@ def shape(self) -> typing.Tuple[int, int]: width = len(self.columns) count_expr = self._to_ibis_expr(ordering_mode="unordered").count() sql = self._session.ibis_client.compile(count_expr) - row_iterator, _ = self._session._start_query( - sql=sql, - max_results=1, - ) - length = next(row_iterator)[0] + + # Support in-memory engines for hermetic unit tests. + if not isinstance(sql, str): + length = self._session.ibis_client.execute(count_expr) + else: + row_iterator, _ = self._session._start_query( + sql=sql, + max_results=1, + ) + length = next(row_iterator)[0] return (length, width) def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index e691a30f9cd..ad4f72070f5 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -44,6 +44,7 @@ import bigframes.dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops +import third_party.bigframes_vendored.pandas.io.common as vendored_pandas_io_common # Type constraint for wherever column labels are used Label = typing.Hashable @@ -1522,37 +1523,35 @@ def _is_monotonic( return result -def block_from_local(data, session=None, use_index=True) -> Block: - # TODO(tbergeron): Handle duplicate column labels +def block_from_local(data, session=None) -> Block: pd_data = pd.DataFrame(data) + columns = pd_data.columns - column_labels = list(pd_data.columns) - if not all((label is None) or isinstance(label, str) for label in column_labels): - raise NotImplementedError( - f"Only string column labels supported. {constants.FEEDBACK_LINK}" - ) + # Make a flattened version to treat as a table. + if len(pd_data.columns.names) > 1: + pd_data.columns = columns.to_flat_index() - if use_index: - if pd_data.index.nlevels > 1: - raise NotImplementedError( - f"multi-indices not supported. {constants.FEEDBACK_LINK}" - ) - index_label = pd_data.index.name - - index_id = guid.generate_guid() - pd_data = pd_data.reset_index(names=index_id) - keys_expr = core.ArrayValue.mem_expr_from_pandas(pd_data, session) - return Block( - keys_expr, - column_labels=column_labels, - index_columns=[index_id], - index_labels=[index_label], - ) - else: - keys_expr = core.ArrayValue.mem_expr_from_pandas(pd_data, session) - keys_expr, offsets_id = keys_expr.promote_offsets() - # Constructor will create default range index - return Block(keys_expr, index_columns=[offsets_id], column_labels=column_labels) + index_labels = list(pd_data.index.names) + # The ArrayValue layer doesn't know about indexes, so make sure indexes + # are real columns with unique IDs. + pd_data = pd_data.reset_index( + names=[f"level_{level}" for level in range(len(index_labels))] + ) + pd_data = pd_data.set_axis( + vendored_pandas_io_common.dedup_names( + list(pd_data.columns), is_potential_multiindex=False + ), + axis="columns", + ) + index_ids = pd_data.columns[: len(index_labels)] + + keys_expr = core.ArrayValue.mem_expr_from_pandas(pd_data, session) + return Block( + keys_expr, + column_labels=columns, + index_columns=index_ids, + index_labels=index_labels, + ) def _align_block_to_schema( diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0b741feff67..76377cd477f 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -65,9 +65,7 @@ # BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. # TODO(tbergeron): Convert to bytes-based limit -# TODO(swast): Address issues with string escaping and empty tables before -# re-enabling inline data (ibis.memtable) feature. -MAX_INLINE_DF_SIZE = -1 +MAX_INLINE_DF_SIZE = 5000 LevelType = typing.Union[str, int] LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index af3209b0e1c..271b8aa2f24 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -96,13 +96,13 @@ ), ) -BIGFRAMES_TO_IBIS: Dict[Dtype, IbisDtype] = { +BIGFRAMES_TO_IBIS: Dict[Dtype, ibis_dtypes.DataType] = { pandas: ibis for ibis, pandas in BIDIRECTIONAL_MAPPINGS } -IBIS_TO_BIGFRAMES: Dict[ - Union[IbisDtype, ReadOnlyIbisDtype], Union[Dtype, np.dtype[Any]] -] = {ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS} +IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, Union[Dtype, np.dtype[Any]]] = { + ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS +} # Allow REQUIRED fields to map correctly. IBIS_TO_BIGFRAMES.update( {ibis.copy(nullable=False): pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS} @@ -130,7 +130,7 @@ def ibis_dtype_to_bigframes_dtype( - ibis_dtype: Union[IbisDtype, ReadOnlyIbisDtype] + ibis_dtype: ibis_dtypes.DataType, ) -> Union[Dtype, np.dtype[Any]]: """Converts an Ibis dtype to a BigQuery DataFrames dtype @@ -155,6 +155,9 @@ def ibis_dtype_to_bigframes_dtype( if ibis_dtype in IBIS_TO_BIGFRAMES: return IBIS_TO_BIGFRAMES[ibis_dtype] + elif isinstance(ibis_dtype, ibis_dtypes.Null): + # Fallback to STRING for NULL values for most flexibility in SQL. + return IBIS_TO_BIGFRAMES[ibis_dtypes.string] else: raise ValueError( f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" @@ -185,8 +188,8 @@ def ibis_table_to_canonical_types(table: ibis_types.Table) -> ibis_types.Table: def bigframes_dtype_to_ibis_dtype( - bigframes_dtype: Union[DtypeString, Dtype] -) -> IbisDtype: + bigframes_dtype: Union[DtypeString, Dtype, np.dtype[Any]] +) -> ibis_dtypes.DataType: """Converts a BigQuery DataFrames supported dtype to an Ibis dtype. Args: @@ -281,7 +284,9 @@ def literal_to_ibis_scalar( return scalar_expr -def cast_ibis_value(value: ibis_types.Value, to_type: IbisDtype) -> ibis_types.Value: +def cast_ibis_value( + value: ibis_types.Value, to_type: ibis_dtypes.DataType +) -> ibis_types.Value: """Perform compatible type casts of ibis values Args: diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 81a5bc4c412..add6af57f41 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -30,9 +30,7 @@ # BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. # TODO(tbergeron): Convert to bytes-based limit -# TODO(swast): Address issues with string escaping and empty tables before -# re-enabling inline data (ibis.memtable) feature. -MAX_INLINE_SERIES_SIZE = -1 +MAX_INLINE_SERIES_SIZE = 5000 class SeriesMethods: diff --git a/setup.py b/setup.py index 69b71c88f13..29eacb74a9a 100644 --- a/setup.py +++ b/setup.py @@ -44,12 +44,12 @@ "google-cloud-resource-manager >=1.10.3", "google-cloud-storage >=2.0.0", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. - "ibis-framework[bigquery] >=6.0.0,<=6.1.0", + "ibis-framework[bigquery] >=6.2.0,<7.0.0dev", "pandas >=1.5.0", "pydata-google-auth >=1.8.2", "requests >=2.27.1", "scikit-learn >=1.2.2", - "sqlalchemy >=1.4,<3.0", + "sqlalchemy >=1.4,<3.0dev", "ipywidgets >=7.7.1", "humanize >= 4.6.0", ] diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index cd69d45dc92..f43d3b4ca01 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -45,7 +45,7 @@ greenlet==2.0.2 grpc-google-iam-v1==0.12.6 grpcio==1.53.0 grpcio-status==1.48.2 -ibis-framework==6.0.0 +ibis-framework==6.2.0 humanize==4.6.0 identify==2.5.22 idna==3.4 diff --git a/tests/unit/core/__init__.py b/tests/unit/core/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/unit/core/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py new file mode 100644 index 00000000000..a7e9b5a84b7 --- /dev/null +++ b/tests/unit/core/test_blocks.py @@ -0,0 +1,85 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas +import pandas.testing +import pytest + +import bigframes.core.blocks as blocks + +from .. import resources + + +@pytest.mark.parametrize( + ("data",), + ( + pytest.param( + {"test 1": [1, 2, 3], "test 2": [0.25, 0.5, 0.75]}, + id="dict_spaces_in_column_names", + ), + pytest.param( + [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], + id="nested_list", + ), + pytest.param( + pandas.concat( + [ + pandas.Series([1, 2, 3], name="some col"), + pandas.Series([2, 3, 4], name="some col"), + ], + axis="columns", + ), + id="duplicate_column_names", + ), + pytest.param( + pandas.DataFrame( + {"test": [1, 2, 3]}, + index=pandas.Index(["a", "b", "c"], name="string index"), + ), + id="string_index", + ), + pytest.param( + pandas.DataFrame( + [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], + columns=pandas.MultiIndex.from_tuples( + [(1, 1), (1, 2), (0, 0), (0, 1)], + names=["some level", "another level"], + ), + ), + marks=[ + pytest.mark.skipif( + tuple(pandas.__version__.split()) < ("2", "0", "0"), + reason="pandas 1.5.3 treats column MultiIndex as Index of tuples", + ), + ], + id="multiindex_columns", + ), + pytest.param( + pandas.DataFrame( + {"test": [1, 2, 3]}, + index=pandas.MultiIndex.from_tuples([(1, 1), (1, 2), (0, 0)]), + ), + id="multiindex_rows", + ), + ), +) +def test_block_from_local(data): + expected = pandas.DataFrame(data) + session = resources.create_pandas_session({}) + + block = blocks.block_from_local(data, session=session) + + pandas.testing.assert_index_equal(block.column_labels, expected.columns) + assert tuple(block.index_labels) == tuple(expected.index.names) + assert block.shape == expected.shape From 33274c2fc5035541fe9eb035fb5a1d92b35af4e5 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:14:44 -0700 Subject: [PATCH 030/921] refactor: ml.sql to Object (#44) * refactor: ml.sql to Object Change-Id: Ibf795b81619778eaf28572fccd95a09b65f8ad58 --- bigframes/ml/cluster.py | 5 +- bigframes/ml/compose.py | 5 +- bigframes/ml/core.py | 301 +++++++++++++------------- bigframes/ml/decomposition.py | 5 +- bigframes/ml/ensemble.py | 14 +- bigframes/ml/forecasting.py | 12 +- bigframes/ml/globals.py | 30 +++ bigframes/ml/imported.py | 12 +- bigframes/ml/linear_model.py | 8 +- bigframes/ml/llm.py | 8 +- bigframes/ml/preprocessing.py | 16 +- bigframes/ml/sql.py | 326 +++++++++++++++-------------- tests/system/large/ml/test_core.py | 16 +- tests/system/small/ml/conftest.py | 3 +- tests/unit/ml/test_golden_sql.py | 52 +++-- tests/unit/ml/test_sql.py | 156 +++++++++----- 16 files changed, 559 insertions(+), 410 deletions(-) create mode 100644 bigframes/ml/globals.py diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 14cce2879e9..772b90f666e 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -22,7 +22,7 @@ from google.cloud import bigquery import bigframes -from bigframes.ml import base, core, utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.cluster._kmeans @@ -37,6 +37,7 @@ class KMeans( def __init__(self, n_clusters: int = 8): self.n_clusters = n_clusters self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> KMeans: @@ -66,7 +67,7 @@ def _fit( ) -> KMeans: (X,) = utils.convert_to_dataframe(X) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X_train=X, transforms=transforms, options=self._bqml_options, diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index a3d3503ad0a..02365f261c8 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -22,7 +22,7 @@ from typing import List, Optional, Tuple, Union from bigframes import constants -from bigframes.ml import base, core, preprocessing, utils +from bigframes.ml import base, core, globals, preprocessing, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.compose._column_transformer @@ -53,6 +53,7 @@ def __init__( # TODO: if any(transformers) has fitted raise warning self.transformers = transformers self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() # call self.transformers_ to check chained transformers self.transformers_ @@ -114,7 +115,7 @@ def fit( compiled_transforms = self._compile_to_sql(X.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 667d42f7ee6..70be0d35ee1 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -36,6 +36,9 @@ class BqmlModel: def __init__(self, session: bigframes.Session, model: bigquery.Model): self._session = session self._model = model + self._model_manipulation_sql_generator = ml_sql.ModelManipulationSqlGenerator( + self.model_name + ) @property def session(self) -> bigframes.Session: @@ -85,18 +88,14 @@ def predict(self, input_data: bpd.DataFrame) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( input_data, - lambda source_sql: ml_sql.ml_predict( - model_name=self.model_name, source_sql=source_sql - ), + self._model_manipulation_sql_generator.ml_predict, ) def transform(self, input_data: bpd.DataFrame) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( input_data, - lambda source_sql: ml_sql.ml_transform( - model_name=self.model_name, source_sql=source_sql - ), + self._model_manipulation_sql_generator.ml_transform, ) def generate_text( @@ -107,10 +106,11 @@ def generate_text( # TODO: validate input data schema return self._apply_sql( input_data, - lambda source_sql: ml_sql.ml_generate_text( - model_name=self.model_name, + lambda source_sql: self._model_manipulation_sql_generator.ml_generate_text( source_sql=source_sql, - struct_options=ml_sql.struct_options(**options), + struct_options=self._model_manipulation_sql_generator.struct_options( + **options + ), ), ) @@ -122,15 +122,16 @@ def generate_text_embedding( # TODO: validate input data schema return self._apply_sql( input_data, - lambda source_sql: ml_sql.ml_generate_text_embedding( - model_name=self.model_name, + lambda source_sql: self._model_manipulation_sql_generator.ml_generate_text_embedding( source_sql=source_sql, - struct_options=ml_sql.struct_options(**options), + struct_options=self._model_manipulation_sql_generator.struct_options( + **options + ), ), ) def forecast(self) -> bpd.DataFrame: - sql = ml_sql.ml_forecast(self.model_name) + sql = self._model_manipulation_sql_generator.ml_forecast() return self._session.read_gbq(sql) def evaluate(self, input_data: Optional[bpd.DataFrame] = None): @@ -141,28 +142,28 @@ def evaluate(self, input_data: Optional[bpd.DataFrame] = None): if (input_data is not None) else (None, None, None) ) - sql = ml_sql.ml_evaluate(self.model_name, source_sql) + sql = self._model_manipulation_sql_generator.ml_evaluate(source_sql) return self._session.read_gbq(sql) - def centroids(self): + def centroids(self) -> bpd.DataFrame: assert self._model.model_type == "KMEANS" - sql = ml_sql.ml_centroids(self.model_name) + sql = self._model_manipulation_sql_generator.ml_centroids() return self._session.read_gbq(sql) - def principal_components(self): + def principal_components(self) -> bpd.DataFrame: assert self._model.model_type == "PCA" - sql = ml_sql.ml_principal_components(self.model_name) + sql = self._model_manipulation_sql_generator.ml_principal_components() return self._session.read_gbq(sql) - def principal_component_info(self): + def principal_component_info(self) -> bpd.DataFrame: assert self._model.model_type == "PCA" - sql = ml_sql.ml_principal_component_info(self.model_name) + sql = self._model_manipulation_sql_generator.ml_principal_component_info() return self._session.read_gbq(sql) @@ -187,8 +188,12 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: # truncate as Vertex ID only accepts 63 characters, easily exceeding the limit for temp models. # The possibility of conflicts should be low. vertex_ai_model_id = vertex_ai_model_id[:63] - options_sql = ml_sql.options(**{"vertex_ai_model_id": vertex_ai_model_id}) - sql = ml_sql.alter_model(self.model_name, options_sql=options_sql) + options_sql = self._model_manipulation_sql_generator.options( + **{"vertex_ai_model_id": vertex_ai_model_id} + ) + sql = self._model_manipulation_sql_generator.alter_model( + options_sql=options_sql + ) # Register the model and wait it to finish self._session._start_query(sql) @@ -196,137 +201,149 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: return self -def create_bqml_model( - X_train: bpd.DataFrame, - y_train: Optional[bpd.DataFrame] = None, - transforms: Optional[Iterable[str]] = None, - options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, -) -> BqmlModel: - """Create a session-temporary BQML model with the CREATE MODEL statement +class BqmlModelFactory: + def __init__(self): + model_id = self._create_temp_model_id() + self._model_creation_sql_generator = ml_sql.ModelCreationSqlGenerator(model_id) - Args: - X_train: features columns for training - y_train: labels columns for training, if applicable - transforms: an optional list of SQL expressions that implement preprocessing - on top of the input data. Generates a BQML TRANSFORM clause - options: a dict of options to configure the model. Generates a BQML OPTIONS - clause + def _create_temp_model_id(self) -> str: + return uuid.uuid4().hex - Returns: a BqmlModel, wrapping a trained model in BigQuery - """ - options = dict(options) - if y_train is None: - input_data = X_train - else: + def _reset_model_id(self): + self._model_creation_sql_generator._model_id = self._create_temp_model_id() + + def _create_model_with_sql(self, session: bigframes.Session, sql: str) -> BqmlModel: + # fit the model, synchronously + _, job = session._start_query(sql) + + # real model path in the session specific hidden dataset and table prefix + model_name_full = f"{job.destination.dataset_id}.{job.destination.table_id}" + model = session.bqclient.get_model(model_name_full) + + self._reset_model_id() + return BqmlModel(session, model) + + def create_model( + self, + X_train: bpd.DataFrame, + y_train: Optional[bpd.DataFrame] = None, + transforms: Optional[Iterable[str]] = None, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> BqmlModel: + """Create a session-temporary BQML model with the CREATE MODEL statement + + Args: + X_train: features columns for training + y_train: labels columns for training, if applicable + transforms: an optional list of SQL expressions that implement preprocessing + on top of the input data. Generates a BQML TRANSFORM clause + options: a dict of options to configure the model. Generates a BQML OPTIONS + clause + + Returns: a BqmlModel, wrapping a trained model in BigQuery + """ + options = dict(options) + if y_train is None: + input_data = X_train + else: + input_data = X_train.join(y_train, how="outer") + options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) + + session = X_train._session + + source_sql = input_data.sql + options_sql = self._model_creation_sql_generator.options(**options) + transform_sql = ( + self._model_creation_sql_generator.transform(*transforms) + if transforms is not None + else None + ) + sql = self._model_creation_sql_generator.create_model( + source_sql=source_sql, + transform_sql=transform_sql, + options_sql=options_sql, + ) + + return self._create_model_with_sql(session=session, sql=sql) + + def create_time_series_model( + self, + X_train: bpd.DataFrame, + y_train: bpd.DataFrame, + transforms: Optional[Iterable[str]] = None, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> BqmlModel: + assert ( + X_train.columns.size == 1 + ), "Time series timestamp input must only contain 1 column." + assert ( + y_train.columns.size == 1 + ), "Time stamp data input must only contain 1 column." + + options = dict(options) input_data = X_train.join(y_train, how="outer") - options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) - - session = X_train._session - - source_sql = input_data.sql - options_sql = ml_sql.options(**options) - transform_sql = ml_sql.transform(*transforms) if transforms is not None else None - sql = ml_sql.create_model( - model_name=_create_temp_model_name(), - source_sql=source_sql, - transform_sql=transform_sql, - options_sql=options_sql, - ) - - return _create_bqml_model_with_sql(session=session, sql=sql) - - -def create_bqml_time_series_model( - X_train: bpd.DataFrame, - y_train: bpd.DataFrame, - transforms: Optional[Iterable[str]] = None, - options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, -) -> BqmlModel: - - assert ( - X_train.columns.size == 1 - ), "Time series timestamp input must only contain 1 column." - assert ( - y_train.columns.size == 1 - ), "Time stamp data input must only contain 1 column." - - options = dict(options) - input_data = X_train.join(y_train, how="outer") - options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) - options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) - - session = X_train._session - - source_sql = input_data.sql - options_sql = ml_sql.options(**options) - - transform_sql = ml_sql.transform(*transforms) if transforms is not None else None - sql = ml_sql.create_model( - model_name=_create_temp_model_name(), - source_sql=source_sql, - transform_sql=transform_sql, - options_sql=options_sql, - ) - - return _create_bqml_model_with_sql(session=session, sql=sql) - - -def create_bqml_remote_model( - session: bigframes.Session, - connection_name: str, - options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, -) -> BqmlModel: - """Create a session-temporary BQML remote model with the CREATE MODEL statement - - Args: - connection_name: - a BQ connection to talk with Vertex AI, of the format ... https://cloud.google.com/bigquery/docs/create-cloud-resource-connection - options: - a dict of options to configure the model. Generates a BQML OPTIONS clause - - Returns: - BqmlModel: a BqmlModel wrapping a trained model in BigQuery - """ - options_sql = ml_sql.options(**options) - sql = ml_sql.create_remote_model( - model_name=_create_temp_model_name(), - connection_name=connection_name, - options_sql=options_sql, - ) + options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) + options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) - return _create_bqml_model_with_sql(session=session, sql=sql) + session = X_train._session + source_sql = input_data.sql + options_sql = self._model_creation_sql_generator.options(**options) -def create_bqml_imported_model( - session: bigframes.Session, - options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, -) -> BqmlModel: - """Create a session-temporary BQML imported model with the CREATE MODEL statement + transform_sql = ( + self._model_creation_sql_generator.transform(*transforms) + if transforms is not None + else None + ) + sql = self._model_creation_sql_generator.create_model( + source_sql=source_sql, + transform_sql=transform_sql, + options_sql=options_sql, + ) - Args: - options: a dict of options to configure the model. Generates a BQML OPTIONS - clause + return self._create_model_with_sql(session=session, sql=sql) - Returns: a BqmlModel, wrapping a trained model in BigQuery - """ - options_sql = ml_sql.options(**options) - sql = ml_sql.create_imported_model( - model_name=_create_temp_model_name(), - options_sql=options_sql, - ) + def create_remote_model( + self, + session: bigframes.Session, + connection_name: str, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> BqmlModel: + """Create a session-temporary BQML remote model with the CREATE MODEL statement - return _create_bqml_model_with_sql(session=session, sql=sql) + Args: + connection_name: + a BQ connection to talk with Vertex AI, of the format ... https://cloud.google.com/bigquery/docs/create-cloud-resource-connection + options: + a dict of options to configure the model. Generates a BQML OPTIONS clause + + Returns: + BqmlModel: a BqmlModel wrapping a trained model in BigQuery + """ + options_sql = self._model_creation_sql_generator.options(**options) + sql = self._model_creation_sql_generator.create_remote_model( + connection_name=connection_name, + options_sql=options_sql, + ) + return self._create_model_with_sql(session=session, sql=sql) -def _create_temp_model_name() -> str: - return uuid.uuid4().hex + def create_imported_model( + self, + session: bigframes.Session, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> BqmlModel: + """Create a session-temporary BQML imported model with the CREATE MODEL statement + Args: + options: a dict of options to configure the model. Generates a BQML OPTIONS + clause -def _create_bqml_model_with_sql(session: bigframes.Session, sql: str) -> BqmlModel: - # fit the model, synchronously - _, job = session._start_query(sql) + Returns: a BqmlModel, wrapping a trained model in BigQuery + """ + options_sql = self._model_creation_sql_generator.options(**options) + sql = self._model_creation_sql_generator.create_imported_model( + options_sql=options_sql, + ) - # real model path in the session specific hidden dataset and table prefix - model_name_full = f"{job.destination.dataset_id}.{job.destination.table_id}" - model = session.bqclient.get_model(model_name_full) - return BqmlModel(session, model) + return self._create_model_with_sql(session=session, sql=sql) diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 0cfe3b3ddf9..8e6be6d28c3 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -22,7 +22,7 @@ from google.cloud import bigquery import bigframes -from bigframes.ml import base, core, utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.decomposition._pca @@ -36,6 +36,7 @@ class PCA( def __init__(self, n_components: int = 3): self.n_components = n_components self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> PCA: @@ -60,7 +61,7 @@ def _fit( ) -> PCA: (X,) = utils.convert_to_dataframe(X) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X_train=X, transforms=transforms, options={ diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index b0f3e5f0813..113ad872b5c 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -22,7 +22,7 @@ from google.cloud import bigquery import bigframes -from bigframes.ml import base, core, utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.ensemble._forest import third_party.bigframes_vendored.xgboost.sklearn @@ -95,6 +95,7 @@ def __init__( self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq( @@ -151,7 +152,7 @@ def _fit( ) -> XGBRegressor: X, y = utils.convert_to_dataframe(X, y) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, y, transforms=transforms, @@ -259,6 +260,7 @@ def __init__( self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq( @@ -315,7 +317,7 @@ def _fit( ) -> XGBClassifier: X, y = utils.convert_to_dataframe(X, y) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, y, transforms=transforms, @@ -416,6 +418,7 @@ def __init__( self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq( @@ -469,7 +472,7 @@ def _fit( ) -> RandomForestRegressor: X, y = utils.convert_to_dataframe(X, y) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, y, transforms=transforms, @@ -584,6 +587,7 @@ def __init__( self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq( @@ -637,7 +641,7 @@ def _fit( ) -> RandomForestClassifier: X, y = utils.convert_to_dataframe(X, y) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, y, transforms=transforms, diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 22d81294fc3..8a6de1dd81f 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -21,7 +21,7 @@ from google.cloud import bigquery import bigframes -from bigframes.ml import base, core, utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd _PREDICT_OUTPUT_COLUMNS = ["forecast_timestamp", "forecast_value"] @@ -32,6 +32,7 @@ class ARIMAPlus(base.SupervisedTrainablePredictor): def __init__(self): self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ARIMAPlus: @@ -69,9 +70,16 @@ def _fit( Returns: ARIMAPlus: Fitted estimator. """ + if X.columns.size != 1: + raise ValueError( + "Time series timestamp input X must only contain 1 column." + ) + if y.columns.size != 1: + raise ValueError("Time series data input y must only contain 1 column.") + X, y = utils.convert_to_dataframe(X, y) - self._bqml_model = core.create_bqml_time_series_model( + self._bqml_model = self._bqml_model_factory.create_time_series_model( X, y, transforms=transforms, diff --git a/bigframes/ml/globals.py b/bigframes/ml/globals.py new file mode 100644 index 00000000000..c139476daaa --- /dev/null +++ b/bigframes/ml/globals.py @@ -0,0 +1,30 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Global Singletons for BigQuery DataFrame ML.""" + +from bigframes.ml import core, sql + +_BASE_SQL_GENERATOR = sql.BaseSqlGenerator() +_BQML_MODEL_FACTORY = core.BqmlModelFactory() + + +def base_sql_generator() -> sql.BaseSqlGenerator: + """Base SQL Generator.""" + return _BASE_SQL_GENERATOR + + +def bqml_model_factory() -> core.BqmlModelFactory: + """BQML Model Factory""" + return _BQML_MODEL_FACTORY diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index d4571eb3e5b..fb8aa98befd 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -21,7 +21,7 @@ from google.cloud import bigquery import bigframes -from bigframes.ml import base, core, utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd @@ -42,10 +42,13 @@ def __init__( self.session = session or bpd.get_global_session() self.model_path = model_path self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() def _create_bqml_model(self): options = {"model_type": "TENSORFLOW", "model_path": self.model_path} - return core.create_bqml_imported_model(session=self.session, options=options) + return self._bqml_model_factory.create_imported_model( + session=self.session, options=options + ) @classmethod def _from_bq( @@ -124,10 +127,13 @@ def __init__( self.session = session or bpd.get_global_session() self.model_path = model_path self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() def _create_bqml_model(self): options = {"model_type": "ONNX", "model_path": self.model_path} - return core.create_bqml_imported_model(session=self.session, options=options) + return self._bqml_model_factory.create_imported_model( + session=self.session, options=options + ) @classmethod def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ONNXModel: diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index f27b798eea1..f11879500b9 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -23,7 +23,7 @@ import bigframes import bigframes.constants as constants -from bigframes.ml import base, core, utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.linear_model._base import third_party.bigframes_vendored.sklearn.linear_model._logistic @@ -80,6 +80,7 @@ def __init__( self.calculate_p_values = calculate_p_values self.enable_global_explain = enable_global_explain self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq( @@ -130,7 +131,7 @@ def _fit( ) -> LinearRegression: X, y = utils.convert_to_dataframe(X, y) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, y, transforms=transforms, @@ -204,6 +205,7 @@ def __init__( self.class_weights = class_weights self._auto_class_weight = class_weights == "balanced" self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() @classmethod def _from_bq( @@ -248,7 +250,7 @@ def _fit( """Fit model with transforms.""" X, y = utils.convert_to_dataframe(X, y) - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, y, transforms=transforms, diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 973fbf2ad96..c86e5fb3b67 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -21,7 +21,7 @@ import bigframes from bigframes import clients, constants from bigframes.core import blocks -from bigframes.ml import base, core, utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd _REMOTE_TEXT_GENERATOR_MODEL_CODE = "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1" @@ -52,6 +52,7 @@ def __init__( self._bq_connection_manager = clients.BqConnectionManager( self.session.bqconnectionclient, self.session.resourcemanagerclient ) + self._bqml_model_factory = globals.bqml_model_factory() self._bqml_model: core.BqmlModel = self._create_bqml_model() def _create_bqml_model(self): @@ -76,7 +77,7 @@ def _create_bqml_model(self): "remote_service_type": _REMOTE_TEXT_GENERATOR_MODEL_CODE, } - return core.create_bqml_remote_model( + return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options ) @@ -183,6 +184,7 @@ def __init__( self._bq_connection_manager = clients.BqConnectionManager( self.session.bqconnectionclient, self.session.resourcemanagerclient ) + self._bqml_model_factory = globals.bqml_model_factory() self._bqml_model: core.BqmlModel = self._create_bqml_model() def _create_bqml_model(self): @@ -207,7 +209,7 @@ def _create_bqml_model(self): "remote_service_type": _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE, } - return core.create_bqml_remote_model( + return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options ) diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 8add7bdd767..cd4ae27b8c8 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -20,9 +20,7 @@ import typing from typing import Any, cast, List, Literal, Optional, Tuple, Union -from bigframes.ml import base, core -from bigframes.ml import sql as ml_sql -from bigframes.ml import utils +from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.preprocessing._data import third_party.bigframes_vendored.sklearn.preprocessing._encoder @@ -38,6 +36,8 @@ class StandardScaler( def __init__(self): self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() # TODO(garrettwu): implement __hash__ def __eq__(self, other: Any) -> bool: @@ -53,7 +53,7 @@ def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: Returns: a list of tuples of (sql_expression, output_name)""" return [ ( - ml_sql.ml_standard_scaler(column, f"scaled_{column}"), + self._base_sql_generator.ml_standard_scaler(column, f"scaled_{column}"), f"scaled_{column}", ) for column in columns @@ -81,7 +81,7 @@ def fit( compiled_transforms = self._compile_to_sql(X.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, @@ -132,6 +132,8 @@ def __init__( self.min_frequency = min_frequency self.max_categories = max_categories self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() # TODO(garrettwu): implement __hash__ def __eq__(self, other: Any) -> bool: @@ -167,7 +169,7 @@ def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: ) return [ ( - ml_sql.ml_one_hot_encoder( + self._base_sql_generator.ml_one_hot_encoder( column, drop, top_k, frequency_threshold, f"onehotencoded_{column}" ), f"onehotencoded_{column}", @@ -206,7 +208,7 @@ def fit( compiled_transforms = self._compile_to_sql(X.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - self._bqml_model = core.create_bqml_model( + self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index feb7ff7835b..7cf030485bd 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -21,174 +21,180 @@ import bigframes.constants as constants -def _encode_value(v: Union[str, int, float, Iterable[str]]) -> str: - """Encode a parameter value for SQL""" - if isinstance(v, str): - return f'"{v}"' - elif isinstance(v, int) or isinstance(v, float): - return f"{v}" - elif isinstance(v, Iterable): - inner = ", ".join([_encode_value(x) for x in v]) - return f"[{inner}]" - else: - raise ValueError(f"Unexpected value type. {constants.FEEDBACK_LINK}") - - -def _build_parameters(**kwargs: Union[str, int, float, Iterable[str]]) -> str: - """Encode a dict of values into a formatted Iterable of key-value pairs for SQL""" - indent_str = " " - param_strs = [f"{k}={_encode_value(v)}" for k, v in kwargs.items()] - return "\n" + indent_str + f",\n{indent_str}".join(param_strs) - - -def options(**kwargs: Union[str, int, float, Iterable[str]]) -> str: - """Encode the OPTIONS clause for BQML""" - return f"OPTIONS({_build_parameters(**kwargs)})" - - -def _build_structs(**kwargs: Union[int, float]) -> str: - """Encode a dict of values into a formatted STRUCT items for SQL""" - indent_str = " " - param_strs = [f"{v} AS {k}" for k, v in kwargs.items()] - return "\n" + indent_str + f",\n{indent_str}".join(param_strs) - - -def struct_options(**kwargs: Union[int, float]) -> str: - """Encode a BQ STRUCT as options.""" - return f"STRUCT({_build_structs(**kwargs)})" - - -def _build_expressions(*expr_sqls: str) -> str: - """Encode a Iterable of SQL expressions into a formatted Iterable for SQL""" - indent_str = " " - return "\n" + indent_str + f",\n{indent_str}".join(expr_sqls) - - -def transform(*expr_sqls: str) -> str: - """Encode the TRANSFORM clause for BQML""" - return f"TRANSFORM({_build_expressions(*expr_sqls)})" - - -def connection(conn_name: str) -> str: - """Encode the REMOTE WITH CONNECTION clause for BQML. conn_name is of the format ...""" - return f"REMOTE WITH CONNECTION `{conn_name}`" - - -def ml_standard_scaler(numeric_expr_sql: str, name: str) -> str: - """Encode ML.STANDARD_SCALER for BQML""" - return f"""ML.STANDARD_SCALER({numeric_expr_sql}) OVER() AS {name}""" - - -def ml_one_hot_encoder( - numeric_expr_sql: str, drop: str, top_k: int, frequency_threshold: int, name: str -) -> str: - """Encode ML.ONE_HOT_ENCODER for BQML. - https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder for params.""" - return f"""ML.ONE_HOT_ENCODER({numeric_expr_sql}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {name}""" - - -def create_model( - model_name: str, - source_sql: str, - transform_sql: Optional[str] = None, - options_sql: Optional[str] = None, -) -> str: - """Encode the CREATE TEMP MODEL statement for BQML""" - parts = [f"CREATE TEMP MODEL `{model_name}`"] - if transform_sql: - parts.append(transform_sql) - if options_sql: - parts.append(options_sql) - parts.append(f"AS {source_sql}") - return "\n".join(parts) - - -def create_remote_model( - model_name: str, - connection_name: str, - options_sql: Optional[str] = None, -) -> str: - """Encode the CREATE TEMP MODEL statement for BQML remote model.""" - parts = [f"CREATE TEMP MODEL `{model_name}`"] - parts.append(connection(connection_name)) - if options_sql: - parts.append(options_sql) - return "\n".join(parts) - - -def create_imported_model( - model_name: str, - options_sql: Optional[str] = None, -) -> str: - """Encode the CREATE TEMP MODEL statement for BQML remote model.""" - parts = [f"CREATE TEMP MODEL `{model_name}`"] - if options_sql: - parts.append(options_sql) - return "\n".join(parts) - - -def alter_model( - model_name: str, - options_sql: str, -) -> str: - """Encode the ALTER MODEL statement for BQML""" - parts = [f"ALTER MODEL `{model_name}`"] - parts.append(f"SET {options_sql}") - return "\n".join(parts) - - -def ml_evaluate(model_name: str, source_sql: Optional[str] = None) -> str: - """Encode ML.EVALUATE for BQML""" - if source_sql is None: - return f"""SELECT * FROM ML.EVALUATE(MODEL `{model_name}`)""" - else: - return f"""SELECT * FROM ML.EVALUATE(MODEL `{model_name}`, +class BaseSqlGenerator: + """Generate base SQL strings for ML. Model name isn't needed in this class.""" + + # General methods + def encode_value(self, v: Union[str, int, float, Iterable[str]]) -> str: + """Encode a parameter value for SQL""" + if isinstance(v, str): + return f'"{v}"' + elif isinstance(v, int) or isinstance(v, float): + return f"{v}" + elif isinstance(v, Iterable): + inner = ", ".join([self.encode_value(x) for x in v]) + return f"[{inner}]" + else: + raise ValueError(f"Unexpected value type. {constants.FEEDBACK_LINK}") + + def build_parameters(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: + """Encode a dict of values into a formatted Iterable of key-value pairs for SQL""" + indent_str = " " + param_strs = [f"{k}={self.encode_value(v)}" for k, v in kwargs.items()] + return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + + def build_structs(self, **kwargs: Union[int, float]) -> str: + """Encode a dict of values into a formatted STRUCT items for SQL""" + indent_str = " " + param_strs = [f"{v} AS {k}" for k, v in kwargs.items()] + return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + + def build_expressions(self, *expr_sqls: str) -> str: + """Encode a Iterable of SQL expressions into a formatted Iterable for SQL""" + indent_str = " " + return "\n" + indent_str + f",\n{indent_str}".join(expr_sqls) + + def options(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: + """Encode the OPTIONS clause for BQML""" + return f"OPTIONS({self.build_parameters(**kwargs)})" + + def struct_options(self, **kwargs: Union[int, float]) -> str: + """Encode a BQ STRUCT as options.""" + return f"STRUCT({self.build_structs(**kwargs)})" + + # Connection + def connection(self, conn_name: str) -> str: + """Encode the REMOTE WITH CONNECTION clause for BQML. conn_name is of the format ...""" + return f"REMOTE WITH CONNECTION `{conn_name}`" + + # Transformers + def transform(self, *expr_sqls: str) -> str: + """Encode the TRANSFORM clause for BQML""" + return f"TRANSFORM({self.build_expressions(*expr_sqls)})" + + def ml_standard_scaler(self, numeric_expr_sql: str, name: str) -> str: + """Encode ML.STANDARD_SCALER for BQML""" + return f"""ML.STANDARD_SCALER({numeric_expr_sql}) OVER() AS {name}""" + + def ml_one_hot_encoder( + self, + numeric_expr_sql: str, + drop: str, + top_k: int, + frequency_threshold: int, + name: str, + ) -> str: + """Encode ML.ONE_HOT_ENCODER for BQML. + https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder for params.""" + return f"""ML.ONE_HOT_ENCODER({numeric_expr_sql}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {name}""" + + +class ModelCreationSqlGenerator(BaseSqlGenerator): + """Sql generator for creating a model entity. Model id is the standalone id without project id and dataset id.""" + + def __init__(self, model_id: str): + self._model_id = model_id + + # Model create and alter + def create_model( + self, + source_sql: str, + transform_sql: Optional[str] = None, + options_sql: Optional[str] = None, + ) -> str: + """Encode the CREATE TEMP MODEL statement for BQML""" + parts = [f"CREATE TEMP MODEL `{self._model_id}`"] + if transform_sql: + parts.append(transform_sql) + if options_sql: + parts.append(options_sql) + parts.append(f"AS {source_sql}") + return "\n".join(parts) + + def create_remote_model( + self, + connection_name: str, + options_sql: Optional[str] = None, + ) -> str: + """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + parts = [f"CREATE TEMP MODEL `{self._model_id}`"] + parts.append(self.connection(connection_name)) + if options_sql: + parts.append(options_sql) + return "\n".join(parts) + + def create_imported_model( + self, + options_sql: Optional[str] = None, + ) -> str: + """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + parts = [f"CREATE TEMP MODEL `{self._model_id}`"] + if options_sql: + parts.append(options_sql) + return "\n".join(parts) + + +class ModelManipulationSqlGenerator(BaseSqlGenerator): + """Sql generator for manipulating a model entity. Model name is the fully model path of project_id.dataset_id.model_id.""" + + def __init__(self, model_name: str): + self._model_name = model_name + + # Alter model + def alter_model( + self, + options_sql: str, + ) -> str: + """Encode the ALTER MODEL statement for BQML""" + parts = [f"ALTER MODEL `{self._model_name}`"] + parts.append(f"SET {options_sql}") + return "\n".join(parts) + + # ML prediction TVFs + def ml_predict(self, source_sql: str) -> str: + """Encode ML.PREDICT for BQML""" + return f"""SELECT * FROM ML.PREDICT(MODEL `{self._model_name}`, ({source_sql}))""" + def ml_forecast(self) -> str: + """Encode ML.FORECAST for BQML""" + return f"""SELECT * FROM ML.FORECAST(MODEL `{self._model_name}`)""" -def ml_centroids( - model_name: str, -) -> str: - """Encode ML.CENTROIDS for BQML""" - return f"""SELECT * FROM ML.CENTROIDS(MODEL `{model_name}`)""" - - -def ml_predict(model_name: str, source_sql: str) -> str: - """Encode ML.PREDICT for BQML""" - return f"""SELECT * FROM ML.PREDICT(MODEL `{model_name}`, - ({source_sql}))""" - - -def ml_transform(model_name: str, source_sql: str) -> str: - """Encode ML.TRANSFORM for BQML""" - return f"""SELECT * FROM ML.TRANSFORM(MODEL `{model_name}`, - ({source_sql}))""" - - -def ml_generate_text(model_name: str, source_sql: str, struct_options: str) -> str: - """Encode ML.GENERATE_TEXT for BQML""" - return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL `{model_name}`, + def ml_generate_text(self, source_sql: str, struct_options: str) -> str: + """Encode ML.GENERATE_TEXT for BQML""" + return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL `{self._model_name}`, ({source_sql}), {struct_options})""" - -def ml_generate_text_embedding( - model_name: str, source_sql: str, struct_options: str -) -> str: - """Encode ML.GENERATE_TEXT_EMBEDDING for BQML""" - return f"""SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `{model_name}`, + def ml_generate_text_embedding(self, source_sql: str, struct_options: str) -> str: + """Encode ML.GENERATE_TEXT_EMBEDDING for BQML""" + return f"""SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `{self._model_name}`, ({source_sql}), {struct_options})""" + # ML evaluation TVFs + def ml_evaluate(self, source_sql: Optional[str] = None) -> str: + """Encode ML.EVALUATE for BQML""" + if source_sql is None: + return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`)""" + else: + return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, + ({source_sql}))""" -def ml_forecast(model_name: str) -> str: - """Encode ML.FORECAST for BQML""" - return f"""SELECT * FROM ML.FORECAST(MODEL `{model_name}`)""" - + def ml_centroids(self) -> str: + """Encode ML.CENTROIDS for BQML""" + return f"""SELECT * FROM ML.CENTROIDS(MODEL `{self._model_name}`)""" -def ml_principal_components(model_name: str) -> str: - """Encode ML.PRINCIPAL_COMPONENTS for BQML""" - return f"""SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `{model_name}`)""" + def ml_principal_components(self) -> str: + """Encode ML.PRINCIPAL_COMPONENTS for BQML""" + return f"""SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `{self._model_name}`)""" + def ml_principal_component_info(self) -> str: + """Encode ML.PRINCIPAL_COMPONENT_INFO for BQML""" + return ( + f"""SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `{self._model_name}`)""" + ) -def ml_principal_component_info(model_name: str) -> str: - """Encode ML.PRINCIPAL_COMPONENT_INFO for BQML""" - return f"""SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `{model_name}`)""" + # ML transform TVF, that require a transform_only type model + def ml_transform(self, source_sql: str) -> str: + """Encode ML.TRANSFORM for BQML""" + return f"""SELECT * FROM ML.TRANSFORM(MODEL `{self._model_name}`, + ({source_sql}))""" diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py index 133af2dae4a..3b30d7eb1d9 100644 --- a/tests/system/large/ml/test_core.py +++ b/tests/system/large/ml/test_core.py @@ -14,8 +14,7 @@ import pandas -import bigframes.ml.core -import bigframes.ml.sql +from bigframes.ml import globals def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_df): @@ -32,7 +31,7 @@ def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_d ] y_train = df[["body_mass_g"]] - model = bigframes.ml.core.create_bqml_model( + model = globals.bqml_model_factory().create_model( X_train, y_train, options={"model_type": "linear_reg"} ) @@ -84,6 +83,9 @@ def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_d def test_bqml_manual_preprocessing_e2e( session, dataset_id, penguins_df_default_index, new_penguins_df ): + base_sql_generator = globals.base_sql_generator() + bqml_model_factory = globals.bqml_model_factory() + df = penguins_df_default_index.dropna() X_train = df[ [ @@ -94,12 +96,12 @@ def test_bqml_manual_preprocessing_e2e( ] y_train = df[["body_mass_g"]] transforms = [ - bigframes.ml.sql.ml_standard_scaler(column, column) + base_sql_generator.ml_standard_scaler(column, column) for column in X_train.columns.tolist() ] transforms.extend(y_train.columns.tolist()) options = {"model_type": "linear_reg"} - model = bigframes.ml.core.create_bqml_model( + model = bqml_model_factory.create_model( X_train, y_train, transforms=transforms, options=options ) @@ -150,8 +152,10 @@ def test_bqml_manual_preprocessing_e2e( def test_bqml_standalone_transform(penguins_df_default_index, new_penguins_df): + bqml_model_factory = globals.bqml_model_factory() + X = penguins_df_default_index[["culmen_length_mm", "species"]] - model = bigframes.ml.core.create_bqml_model( + model = bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=[ diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 9ca5a2fd0ef..1dd1c813b81 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -25,6 +25,7 @@ decomposition, ensemble, forecasting, + globals, imported, linear_model, llm, @@ -202,7 +203,7 @@ def bqml_palm2_text_generator_model(session, bq_connection) -> core.BqmlModel: options = { "remote_service_type": "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1", } - return core.create_bqml_remote_model( + return globals.bqml_model_factory().create_remote_model( session=session, connection_name=bq_connection, options=options ) diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 584d080d428..3ca7e144a53 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -62,38 +62,44 @@ def mock_X(mock_y, mock_session): @pytest.fixture -def bqml_model(mock_session): - bqml_model = core.BqmlModel( - mock_session, bigquery.Model("model_project.model_dataset.model_name") +def bqml_model_factory(mocker: pytest_mock.MockerFixture): + mocker.patch( + "bigframes.ml.core.BqmlModelFactory._create_temp_model_id", + return_value="temp_model_id", ) + bqml_model_factory = core.BqmlModelFactory() - return bqml_model + return bqml_model_factory @pytest.fixture -def ml_mocker(mocker: pytest_mock.MockerFixture): - mocker.patch( - "bigframes.ml.core._create_temp_model_name", return_value="temp_model_name" +def bqml_model(mock_session): + bqml_model = core.BqmlModel( + mock_session, bigquery.Model("model_project.model_dataset.model_id") ) - return mocker + return bqml_model -def test_linear_regression_default_fit(ml_mocker, mock_session, mock_X, mock_y): +def test_linear_regression_default_fit( + bqml_model_factory, mock_session, mock_X, mock_y +): model = linear_model.LinearRegression() + model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) -def test_linear_regression_params_fit(ml_mocker, mock_session, mock_X, mock_y): +def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, mock_y): model = linear_model.LinearRegression(fit_intercept=False) + model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -103,7 +109,7 @@ def test_linear_regression_predict(mock_session, bqml_model, mock_X): model.predict(mock_X) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_name`,\n (input_X_sql))", + "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_id`,\n (input_X_sql))", index_col=["index_column_id"], ) @@ -114,27 +120,33 @@ def test_linear_regression_score(mock_session, bqml_model, mock_X, mock_y): model.score(mock_X, mock_y) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_name`,\n (input_X_y_sql))" + "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_id`,\n (input_X_y_sql))" ) -def test_logistic_regression_default_fit(ml_mocker, mock_session, mock_X, mock_y): +def test_logistic_regression_default_fit( + bqml_model_factory, mock_session, mock_X, mock_y +): model = linear_model.LogisticRegression() + model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) -def test_logistic_regression_params_fit(ml_mocker, mock_session, mock_X, mock_y): +def test_logistic_regression_params_fit( + bqml_model_factory, mock_session, mock_X, mock_y +): model = linear_model.LogisticRegression( fit_intercept=False, class_weights="balanced" ) + model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_name`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -144,7 +156,7 @@ def test_logistic_regression_predict(mock_session, bqml_model, mock_X): model.predict(mock_X) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_name`,\n (input_X_sql))", + "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_id`,\n (input_X_sql))", index_col=["index_column_id"], ) @@ -155,5 +167,5 @@ def test_logistic_regression_score(mock_session, bqml_model, mock_X, mock_y): model.score(mock_X, mock_y) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_name`,\n (input_X_y_sql))" + "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_id`,\n (input_X_y_sql))" ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index c20a17f7d6e..495e8759e8a 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -12,11 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + import bigframes.ml.sql as ml_sql -def test_options_produces_correct_sql(): - sql = ml_sql.options(model_type="lin_reg", input_label_cols=["col_a"], l1_reg=0.6) +@pytest.fixture(scope="session") +def base_sql_generator() -> ml_sql.BaseSqlGenerator: + return ml_sql.BaseSqlGenerator() + + +@pytest.fixture(scope="session") +def model_creation_sql_generator() -> ml_sql.ModelCreationSqlGenerator: + return ml_sql.ModelCreationSqlGenerator(model_id="my_model_id") + + +@pytest.fixture(scope="session") +def model_manipulation_sql_generator() -> ml_sql.ModelManipulationSqlGenerator: + return ml_sql.ModelManipulationSqlGenerator( + model_name="my_project_id.my_dataset_id.my_model_id" + ) + + +def test_options_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenerator): + sql = base_sql_generator.options( + model_type="lin_reg", input_label_cols=["col_a"], l1_reg=0.6 + ) assert ( sql == """OPTIONS( @@ -26,8 +47,8 @@ def test_options_produces_correct_sql(): ) -def test_transform_produces_correct_sql(): - sql = ml_sql.transform( +def test_transform_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenerator): + sql = base_sql_generator.transform( "ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a", "ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b", ) @@ -39,141 +60,172 @@ def test_transform_produces_correct_sql(): ) -def test_standard_scaler_produces_correct_sql(): - sql = ml_sql.ml_standard_scaler("col_a", "scaled_col_a") +def test_standard_scaler_produces_correct_sql( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_standard_scaler("col_a", "scaled_col_a") assert sql == "ML.STANDARD_SCALER(col_a) OVER() AS scaled_col_a" -def test_one_hot_encoder_produces_correct_sql(): - sql = ml_sql.ml_one_hot_encoder("col_a", "none", 1000000, 0, "encoded_col_a") +def test_one_hot_encoder_produces_correct_sql( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_one_hot_encoder( + "col_a", "none", 1000000, 0, "encoded_col_a" + ) assert ( sql == "ML.ONE_HOT_ENCODER(col_a, 'none', 1000000, 0) OVER() AS encoded_col_a" ) -def test_create_model_produces_correct_sql(): - sql = ml_sql.create_model( - model_name="my_dataset.my_model", +def test_create_model_produces_correct_sql( + model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, +): + sql = model_creation_sql_generator.create_model( source_sql="my_source_sql", options_sql="my_options_sql", ) assert ( sql - == """CREATE TEMP MODEL `my_dataset.my_model` + == """CREATE TEMP MODEL `my_model_id` my_options_sql AS my_source_sql""" ) -def test_create_model_transform_produces_correct_sql(): - sql = ml_sql.create_model( - model_name="my_model", +def test_create_model_transform_produces_correct_sql( + model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, +): + sql = model_creation_sql_generator.create_model( source_sql="my_source_sql", options_sql="my_options_sql", transform_sql="my_transform_sql", ) assert ( sql - == """CREATE TEMP MODEL `my_model` + == """CREATE TEMP MODEL `my_model_id` my_transform_sql my_options_sql AS my_source_sql""" ) -def test_create_remote_model_produces_correct_sql(): - sql = ml_sql.create_remote_model( - model_name="my_model", +def test_create_remote_model_produces_correct_sql( + model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, +): + sql = model_creation_sql_generator.create_remote_model( connection_name="my_project.us.my_connection", options_sql="my_options_sql", ) assert ( sql - == """CREATE TEMP MODEL `my_model` + == """CREATE TEMP MODEL `my_model_id` REMOTE WITH CONNECTION `my_project.us.my_connection` my_options_sql""" ) -def test_create_imported_model_produces_correct_sql(): - sql = ml_sql.create_imported_model( - model_name="my_model", +def test_create_imported_model_produces_correct_sql( + model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, +): + sql = model_creation_sql_generator.create_imported_model( options_sql="my_options_sql", ) assert ( sql - == """CREATE TEMP MODEL `my_model` + == """CREATE TEMP MODEL `my_model_id` my_options_sql""" ) -def test_alter_model_correct_sql(): - sql = ml_sql.alter_model( - model_name="my_dataset.my_model", +def test_alter_model_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.alter_model( options_sql="my_options_sql", ) assert ( sql - == """ALTER MODEL `my_dataset.my_model` + == """ALTER MODEL `my_project_id.my_dataset_id.my_model_id` SET my_options_sql""" ) -def test_ml_predict_produces_correct_sql(): - sql = ml_sql.ml_predict( - model_name="my_dataset.my_model", source_sql="SELECT * FROM my_table" +def test_ml_predict_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_predict( + source_sql="SELECT * FROM my_table" ) assert ( sql - == """SELECT * FROM ML.PREDICT(MODEL `my_dataset.my_model`, + == """SELECT * FROM ML.PREDICT(MODEL `my_project_id.my_dataset_id.my_model_id`, (SELECT * FROM my_table))""" ) -def test_ml_evaluate_produces_correct_sql(): - sql = ml_sql.ml_evaluate( - model_name="my_dataset.my_model", source_sql="SELECT * FROM my_table" +def test_ml_evaluate_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_evaluate( + source_sql="SELECT * FROM my_table" ) assert ( sql - == """SELECT * FROM ML.EVALUATE(MODEL `my_dataset.my_model`, + == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, (SELECT * FROM my_table))""" ) -def test_ml_evaluate_no_source_produces_correct_sql(): - sql = ml_sql.ml_evaluate(model_name="my_dataset.my_model") - assert sql == """SELECT * FROM ML.EVALUATE(MODEL `my_dataset.my_model`)""" +def test_ml_evaluate_no_source_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_evaluate() + assert ( + sql + == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + ) -def test_ml_centroids_produces_correct_sql(): - sql = ml_sql.ml_centroids(model_name="my_dataset.my_model") - assert sql == """SELECT * FROM ML.CENTROIDS(MODEL `my_dataset.my_model`)""" +def test_ml_centroids_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_centroids() + assert ( + sql + == """SELECT * FROM ML.CENTROIDS(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + ) -def test_ml_generate_text_produces_correct_sql(): - sql = ml_sql.ml_generate_text( - model_name="my_dataset.my_model", +def test_ml_generate_text_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_generate_text( source_sql="SELECT * FROM my_table", struct_options="STRUCT(value AS item)", ) assert ( sql - == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_dataset.my_model`, + == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_project_id.my_dataset_id.my_model_id`, (SELECT * FROM my_table), STRUCT(value AS item))""" ) -def test_ml_principal_components_produces_correct_sql(): - sql = ml_sql.ml_principal_components(model_name="my_dataset.my_model") +def test_ml_principal_components_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_principal_components() assert ( - sql == """SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `my_dataset.my_model`)""" + sql + == """SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `my_project_id.my_dataset_id.my_model_id`)""" ) -def test_ml_principal_component_info_produces_correct_sql(): - sql = ml_sql.ml_principal_component_info(model_name="my_dataset.my_model") +def test_ml_principal_component_info_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_principal_component_info() assert ( sql - == """SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `my_dataset.my_model`)""" + == """SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `my_project_id.my_dataset_id.my_model_id`)""" ) From 25104610e5ffe526315923946533a66713c1d155 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:50:47 -0700 Subject: [PATCH 031/921] feat: add ml.preprocessing.LabelEncoder (#50) --- bigframes/ml/compose.py | 1 + bigframes/ml/pipeline.py | 15 +- bigframes/ml/preprocessing.py | 119 ++++++++++++++ bigframes/ml/sql.py | 11 ++ tests/system/large/ml/test_pipeline.py | 61 ++++++- tests/system/small/ml/test_preprocessing.py | 149 ++++++++++++++++++ tests/unit/ml/test_compose.py | 3 + tests/unit/ml/test_sql.py | 11 +- .../sklearn/preprocessing/_label.py | 52 ++++++ 9 files changed, 415 insertions(+), 7 deletions(-) create mode 100644 third_party/bigframes_vendored/sklearn/preprocessing/_label.py diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 02365f261c8..db5d8cf260d 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -29,6 +29,7 @@ CompilablePreprocessorType = Union[ preprocessing.OneHotEncoder, preprocessing.StandardScaler, + preprocessing.LabelEncoder, ] diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index bff0bf36ad4..71c21d565a1 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -50,6 +50,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): compose.ColumnTransformer, preprocessing.StandardScaler, preprocessing.OneHotEncoder, + preprocessing.LabelEncoder, ), ): self._transform = transform @@ -143,7 +144,11 @@ def _extract_as_column_transformer( transformers: List[ Tuple[ str, - Union[preprocessing.OneHotEncoder, preprocessing.StandardScaler], + Union[ + preprocessing.OneHotEncoder, + preprocessing.StandardScaler, + preprocessing.LabelEncoder, + ], Union[str, List[str]], ] ] = [] @@ -167,6 +172,13 @@ def _extract_as_column_transformer( *preprocessing.OneHotEncoder._parse_from_sql(transform_sql), ) ) + elif transform_sql.startswith("ML.LABEL_ENCODER"): + transformers.append( + ( + "label_encoder", + *preprocessing.LabelEncoder._parse_from_sql(transform_sql), + ) + ) else: raise NotImplementedError( f"Unsupported transformer type. {constants.FEEDBACK_LINK}" @@ -181,6 +193,7 @@ def _merge_column_transformer( compose.ColumnTransformer, preprocessing.StandardScaler, preprocessing.OneHotEncoder, + preprocessing.LabelEncoder, ]: """Try to merge the column transformer to a simple transformer.""" transformers = column_transformer.transformers_ diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index cd4ae27b8c8..6ee17751df0 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -24,6 +24,7 @@ import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.preprocessing._data import third_party.bigframes_vendored.sklearn.preprocessing._encoder +import third_party.bigframes_vendored.sklearn.preprocessing._label class StandardScaler( @@ -229,3 +230,121 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: bpd.DataFrame, df[self._output_names], ) + + +class LabelEncoder( + base.Transformer, + third_party.bigframes_vendored.sklearn.preprocessing._label.LabelEncoder, +): + # BQML max value https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder#syntax + TOP_K_DEFAULT = 1000000 + FREQUENCY_THRESHOLD_DEFAULT = 0 + + __doc__ = ( + third_party.bigframes_vendored.sklearn.preprocessing._label.LabelEncoder.__doc__ + ) + + # All estimators must implement __init__ to document their parameters, even + # if they don't have any + def __init__( + self, + min_frequency: Optional[int] = None, + max_categories: Optional[int] = None, + ): + if max_categories is not None and max_categories < 2: + raise ValueError( + f"max_categories has to be larger than or equal to 2, input is {max_categories}." + ) + self.min_frequency = min_frequency + self.max_categories = max_categories + self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return ( + type(other) is LabelEncoder + and self._bqml_model == other._bqml_model + and self.min_frequency == other.min_frequency + and self.max_categories == other.max_categories + ) + + def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: + a list of column names to transform + + Returns: a list of tuples of (sql_expression, output_name)""" + + # minus one here since BQML's inplimentation always includes index 0, and top_k is on top of that. + top_k = ( + (self.max_categories - 1) + if self.max_categories is not None + else LabelEncoder.TOP_K_DEFAULT + ) + frequency_threshold = ( + self.min_frequency + if self.min_frequency is not None + else LabelEncoder.FREQUENCY_THRESHOLD_DEFAULT + ) + return [ + ( + self._base_sql_generator.ml_label_encoder( + column, top_k, frequency_threshold, f"labelencoded_{column}" + ), + f"labelencoded_{column}", + ) + for column in columns + ] + + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[LabelEncoder, str]: + """Parse SQL to tuple(LabelEncoder, column_label). + + Args: + sql: SQL string of format "ML.LabelEncoder({col_label}, {top_k}, {frequency_threshold}) OVER() " + + Returns: + tuple(LabelEncoder, column_label)""" + s = sql[sql.find("(") + 1 : sql.find(")")] + col_label, top_k, frequency_threshold = s.split(", ") + max_categories = int(top_k) + 1 + min_frequency = int(frequency_threshold) + + return cls(min_frequency, max_categories), col_label + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> LabelEncoder: + (X,) = utils.convert_to_dataframe(X) + + compiled_transforms = self._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = self._bqml_model_factory.create_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + return self + + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + (X,) = utils.convert_to_dataframe(X) + + df = self._bqml_model.transform(X) + return typing.cast( + bpd.DataFrame, + df[self._output_names], + ) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 7cf030485bd..5d35a10b966 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -88,6 +88,17 @@ def ml_one_hot_encoder( https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder for params.""" return f"""ML.ONE_HOT_ENCODER({numeric_expr_sql}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {name}""" + def ml_label_encoder( + self, + numeric_expr_sql: str, + top_k: int, + frequency_threshold: int, + name: str, + ) -> str: + """Encode ML.LABEL_ENCODER for BQML. + https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-label-encoder for params.""" + return f"""ML.LABEL_ENCODER({numeric_expr_sql}, {top_k}, {frequency_threshold}) OVER() AS {name}""" + class ModelCreationSqlGenerator(BaseSqlGenerator): """Sql generator for creating a model entity. Model id is the standalone id without project id and dataset id.""" diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index bec1a51a991..87664b4c3d4 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -570,6 +570,11 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "label", + preprocessing.LabelEncoder(), + "species", + ), ] ), ), @@ -632,6 +637,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "label", + preprocessing.LabelEncoder(), + "species", + ), ] ), ), @@ -650,7 +660,7 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id assert isinstance(pl_loaded._transform, compose.ColumnTransformer) transformers = pl_loaded._transform.transformers_ - assert len(transformers) == 3 + assert len(transformers) == 4 assert transformers[0][0] == "ont_hot_encoder" assert isinstance(transformers[0][1], preprocessing.OneHotEncoder) @@ -660,13 +670,20 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id assert one_hot_encoder.max_categories == 100 assert transformers[0][2] == "species" - assert transformers[1][0] == "standard_scaler" - assert isinstance(transformers[1][1], preprocessing.StandardScaler) - assert transformers[1][2] == "culmen_length_mm" + assert transformers[1][0] == "label_encoder" + assert isinstance(transformers[1][1], preprocessing.LabelEncoder) + one_hot_encoder = transformers[1][1] + assert one_hot_encoder.min_frequency == 0 + assert one_hot_encoder.max_categories == 1000001 + assert transformers[1][2] == "species" assert transformers[2][0] == "standard_scaler" assert isinstance(transformers[2][1], preprocessing.StandardScaler) - assert transformers[2][2] == "flipper_length_mm" + assert transformers[2][2] == "culmen_length_mm" + + assert transformers[3][0] == "standard_scaler" + assert isinstance(transformers[2][1], preprocessing.StandardScaler) + assert transformers[3][2] == "flipper_length_mm" assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) assert pl_loaded._estimator.fit_intercept is False @@ -735,3 +752,37 @@ def test_pipeline_one_hot_encoder_to_gbq(penguins_df_default_index, dataset_id): assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) assert pl_loaded._estimator.fit_intercept is False + + +def test_pipeline_label_encoder_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ( + "transform", + preprocessing.LabelEncoder(min_frequency=5, max_categories=100), + ), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "sex", + "species", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_label_encoder", replace=True + ) + assert isinstance(pl_loaded._transform, preprocessing.LabelEncoder) + + label_encoder = pl_loaded._transform + assert label_encoder.min_frequency == 5 + assert label_encoder.max_categories == 100 + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 57b9900c489..1f08ef2c2c9 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -264,4 +264,153 @@ def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_ pd.testing.assert_frame_equal(result, expected) +def test_label_encoder_default_params(new_penguins_df): + encoder = bigframes.ml.preprocessing.LabelEncoder() + encoder.fit(new_penguins_df[["species", "sex"]]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "labelencoded_sex": [ + 2, + 1, + 1, + ], + "labelencoded_species": [ + 1, + 1, + 2, + ], + }, + dtype="Int64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +def test_label_encoder_default_params_fit_transform(new_penguins_df): + encoder = bigframes.ml.preprocessing.LabelEncoder() + + result = encoder.fit_transform(new_penguins_df[["species", "sex"]]).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "labelencoded_sex": [ + 2, + 1, + 1, + ], + "labelencoded_species": [ + 1, + 1, + 2, + ], + }, + dtype="Int64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +def test_label_encoder_series_default_params(new_penguins_df): + encoder = bigframes.ml.preprocessing.LabelEncoder() + encoder.fit(new_penguins_df["species"]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "labelencoded_species": [ + 1, + 1, + 2, + ], + }, + dtype="Int64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +def test_label_encoder_params(new_penguins_df): + encoder = bigframes.ml.preprocessing.LabelEncoder(100, 2) + encoder.fit(new_penguins_df[["species", "sex"]]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "labelencoded_sex": [ + 0, + 0, + 0, + ], + "labelencoded_species": [ + 0, + 0, + 0, + ], + }, + dtype="Int64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +def test_label_encoder_different_data(penguins_df_default_index, new_penguins_df): + encoder = bigframes.ml.preprocessing.LabelEncoder() + encoder.fit(penguins_df_default_index[["species", "sex"]]) + + result = encoder.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "labelencoded_sex": [ + 3, + 2, + 2, + ], + "labelencoded_species": [ + 1, + 1, + 2, + ], + }, + dtype="Int64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + # TODO(garrettwu): add OneHotEncoder tests to compare with sklearn. diff --git a/tests/unit/ml/test_compose.py b/tests/unit/ml/test_compose.py index 772a148c95f..c5b3b508762 100644 --- a/tests/unit/ml/test_compose.py +++ b/tests/unit/ml/test_compose.py @@ -23,10 +23,12 @@ def test_columntransformer_init_expectedtransforms(): onehot_transformer = bigframes.ml.preprocessing.OneHotEncoder() scaler_transformer = bigframes.ml.preprocessing.StandardScaler() + label_transformer = bigframes.ml.preprocessing.LabelEncoder() column_transformer = bigframes.ml.compose.ColumnTransformer( [ ("onehot", onehot_transformer, "species"), ("scale", scaler_transformer, ["culmen_length_mm", "flipper_length_mm"]), + ("onehot", label_transformer, "species"), ] ) @@ -34,6 +36,7 @@ def test_columntransformer_init_expectedtransforms(): ("onehot", onehot_transformer, "species"), ("scale", scaler_transformer, "culmen_length_mm"), ("scale", scaler_transformer, "flipper_length_mm"), + ("onehot", label_transformer, "species"), ] diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 495e8759e8a..23b68aa150a 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -51,12 +51,14 @@ def test_transform_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenera sql = base_sql_generator.transform( "ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a", "ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b", + "ML.LABEL_ENCODER(col_c) OVER(col_c) AS encoded_col_c", ) assert ( sql == """TRANSFORM( ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a, - ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b)""" + ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b, + ML.LABEL_ENCODER(col_c) OVER(col_c) AS encoded_col_c)""" ) @@ -78,6 +80,13 @@ def test_one_hot_encoder_produces_correct_sql( ) +def test_label_encoder_produces_correct_sql( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_label_encoder("col_a", 1000000, 0, "encoded_col_a") + assert sql == "ML.LABEL_ENCODER(col_a, 1000000, 0) OVER() AS encoded_col_a" + + def test_create_model_produces_correct_sql( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_label.py b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py new file mode 100644 index 00000000000..7e60c846d45 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py @@ -0,0 +1,52 @@ +# Authors: Alexandre Gramfort +# Mathieu Blondel +# Olivier Grisel +# Andreas Mueller +# Joel Nothman +# Hamzeh Alsalhi +# License: BSD 3 clause + +from bigframes import constants +from third_party.bigframes_vendored.sklearn.base import BaseEstimator + + +class LabelEncoder(BaseEstimator): + """Encode target labels with value between 0 and n_classes-1. + + This transformer should be used to encode target values, *i.e.* `y`, and + not the input `X`. + + Args: + min_frequency (Optional[int], default None): + Specifies the minimum frequency below which a category will be considered infrequent. + Default None. + int: categories with a smaller cardinality will be considered infrequent as ßindex 0. + max_categories (Optional[int], default None): + Specifies an upper limit to the number of output features for each input feature + when considering infrequent categories. If there are infrequent categories, + max_categories includes the category representing the infrequent categories along with the frequent categories. + Default None, set limit to 1,000,000. + """ + + def fit(self, X): + """Fit LabelEncoder to X. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series with training data. + + Returns: + LabelEncoder: Fitted encoder. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transform(self, X): + """Transform X using label encoding. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. + + Returns: + bigframes.dataframe.DataFrame: The result is an array-like of values.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From f9a93ce71d053aa17b1e3a2946c90e0227076184 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 22 Sep 2023 14:00:16 -0700 Subject: [PATCH 032/921] perf: reimplement unpivot to use cross join rather than union (#47) --- bigframes/core/__init__.py | 97 ++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 3b3754642e7..ce3b1aa630b 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -982,61 +982,78 @@ def unpivot( ArrayValue: The unpivoted ArrayValue """ table = self._to_ibis_expr(ordering_mode="offset_col") - sub_expressions = [] - - # Use ibis memtable to infer type of rowlabels (if possible) - # TODO: Allow caller to specify dtype - labels_ibis_type = ibis.memtable({"col": row_labels})["col"].type() - labels_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(labels_ibis_type) - row_n = len(row_labels) if not all( len(source_columns) == row_n for _, source_columns in unpivot_columns ): raise ValueError("Columns and row labels must all be same length.") - for i in range(row_n): - values = [] - for j in range(len(unpivot_columns)): - result_col, source_cols = unpivot_columns[j] - col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype - if source_cols[i] is not None: - values.append( - ops.AsTypeOp(col_dtype) - ._as_ibis(table[source_cols[i]]) - .name(result_col) - ) - else: - values.append( - bigframes.dtypes.literal_to_ibis_scalar( - None, force_dtype=col_dtype - ).name(result_col) - ) - offsets_value = ( - ((table[ORDER_ID_COLUMN] * row_n) + i) - .cast(ibis_dtypes.int64) - .name(ORDER_ID_COLUMN), + unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") + unpivot_table = table.cross_join( + ibis.memtable({unpivot_offset_id: range(row_n)}) + ) + unpivot_offsets_value = ( + ( + (unpivot_table[ORDER_ID_COLUMN] * row_n) + + unpivot_table[unpivot_offset_id] ) - sub_expr = table.select( - passthrough_columns, + .cast(ibis_dtypes.int64) + .name(ORDER_ID_COLUMN), + ) + + # Use ibis memtable to infer type of rowlabels (if possible) + # TODO: Allow caller to specify dtype + labels_ibis_type = ibis.memtable({"col": row_labels})["col"].type() + labels_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(labels_ibis_type) + cases = [ + ( + i, bigframes.dtypes.literal_to_ibis_scalar( row_labels[i], force_dtype=labels_dtype # type:ignore - ).name(index_col_id), - *values, - offsets_value, + ), + ) + for i in range(len(row_labels)) + ] + labels_value = ( + typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) + .cases(cases, default=None) # type:ignore + .name(index_col_id) + ) + + unpivot_values = [] + for j in range(len(unpivot_columns)): + col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype + result_col, source_cols = unpivot_columns[j] + null_value = bigframes.dtypes.literal_to_ibis_scalar( + None, force_dtype=col_dtype + ) + ibis_values = [ + ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) + if col is not None + else null_value + for col in source_cols + ] + cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] + unpivot_value = typing.cast( + ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] + ).cases( + cases, default=null_value # type:ignore ) - sub_expressions.append(sub_expr) - rotated_table = ibis.union(*sub_expressions) + unpivot_values.append(unpivot_value.name(result_col)) + + unpivot_table = unpivot_table.select( + passthrough_columns, labels_value, *unpivot_values, unpivot_offsets_value + ) value_columns = [ - rotated_table[value_col_id] for value_col_id, _ in unpivot_columns + unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns ] - passthrough_values = [rotated_table[col] for col in passthrough_columns] + passthrough_values = [unpivot_table[col] for col in passthrough_columns] return ArrayValue( session=self._session, - table=rotated_table, - columns=[rotated_table[index_col_id], *value_columns, *passthrough_values], - hidden_ordering_columns=[rotated_table[ORDER_ID_COLUMN]], + table=unpivot_table, + columns=[unpivot_table[index_col_id], *value_columns, *passthrough_values], + hidden_ordering_columns=[unpivot_table[ORDER_ID_COLUMN]], ordering=ExpressionOrdering( ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), From 416d7cb9b560d7e33dcc0227f03a00d43f55ba0d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 22 Sep 2023 17:48:02 -0700 Subject: [PATCH 033/921] perf: add ability to cache dataframe and series to session table (#51) --- bigframes/core/__init__.py | 23 +++++++++++++++++++++++ bigframes/core/blocks.py | 9 +++++++++ bigframes/dataframe.py | 3 +++ bigframes/series.py | 3 +++ tests/system/small/test_dataframe.py | 10 ++++++++++ 5 files changed, 48 insertions(+) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index ce3b1aa630b..c529f833510 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -1145,6 +1145,29 @@ def slice( ) return sliced_expr if step > 0 else sliced_expr.reversed() + def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: + """Write the ArrayValue to a session table and create a new block object that references it.""" + ibis_expr = self._to_ibis_expr( + ordering_mode="unordered", expose_hidden_cols=True + ) + destination = self._session._ibis_to_session_table( + ibis_expr, cluster_cols=cluster_cols, api_name="cache" + ) + table_expression = self._session.ibis_client.sql( + f"SELECT * FROM `_SESSION`.`{destination.table_id}`" + ) + new_columns = [table_expression[column] for column in self.column_names] + new_hidden_columns = [ + table_expression[column] for column in self._hidden_ordering_column_names + ] + return ArrayValue( + self._session, + table_expression, + columns=new_columns, + hidden_ordering_columns=new_hidden_columns, + ordering=self._ordering, + ) + class ArrayValueBuilder: """Mutable expression class. diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index ad4f72070f5..c4127c5fd52 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1467,6 +1467,15 @@ def to_sql_query( idx_labels, ) + def cached(self) -> Block: + """Write the block to a session table and create a new block object that references it.""" + return Block( + self.expr.cached(cluster_cols=self.index_columns), + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index_labels, + ) + def _is_monotonic( self, column_ids: typing.Union[str, Sequence[str]], increasing: bool ) -> bool: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 76377cd477f..0d357e7c3d3 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2480,3 +2480,6 @@ def _set_block(self, block: blocks.Block): def _get_block(self) -> blocks.Block: return self._block + + def _cached(self) -> DataFrame: + return DataFrame(self._block.cached()) diff --git a/bigframes/series.py b/bigframes/series.py index 8e47088c146..c1c0cb05372 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1389,6 +1389,9 @@ def _slice( ), ) + def _cached(self) -> Series: + return Series(self._block.cached()) + def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: return pandas.api.types.is_list_like(obj) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 3eeb368ad23..b6ca958c038 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2717,3 +2717,13 @@ def test_query_job_setters(scalars_df_default_index: dataframe.DataFrame): job_ids.add(scalars_df_default_index.query_job.job_id) assert len(job_ids) == 2 + + +def test_df_cached(scalars_df_index): + df = scalars_df_index.set_index(["int64_too", "int64_col"]).sort_values( + "string_col" + ) + df = df[df["rowindex_2"] % 2 == 0] + + df_cached_copy = df._cached() + pandas.testing.assert_frame_equal(df.to_pandas(), df_cached_copy.to_pandas()) From 14b262bde2bb86093bf4df63862e369c5a84b0ad Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:33:40 -0700 Subject: [PATCH 034/921] feat: add ml.preprocessing.MaxAbsScaler (#56) --- bigframes/clients.py | 3 +- bigframes/ml/compose.py | 1 + bigframes/ml/pipeline.py | 10 ++ bigframes/ml/preprocessing.py | 86 +++++++++++++- bigframes/ml/sql.py | 4 + tests/system/large/ml/test_compose.py | 24 ++-- tests/system/large/ml/test_pipeline.py | 82 ++++++++++---- tests/system/small/ml/test_preprocessing.py | 105 ++++++++++++++++-- tests/unit/ml/test_compose.py | 72 ++++++++---- tests/unit/ml/test_sql.py | 7 ++ .../sklearn/preprocessing/_data.py | 42 ++++++- .../sklearn/preprocessing/_encoder.py | 5 +- 12 files changed, 370 insertions(+), 71 deletions(-) diff --git a/bigframes/clients.py b/bigframes/clients.py index 5c019e0fc89..b60fcba04af 100644 --- a/bigframes/clients.py +++ b/bigframes/clients.py @@ -18,7 +18,7 @@ import logging import time -from typing import Optional +from typing import cast, Optional import google.api_core.exceptions from google.cloud import bigquery_connection_v1, resourcemanager_v3 @@ -80,6 +80,7 @@ def create_bq_connection( logger.info( f"Created BQ connection {connection_name} with service account id: {service_account_id}" ) + service_account_id = cast(str, service_account_id) # Ensure IAM role on the 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) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index db5d8cf260d..a1075c23981 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -29,6 +29,7 @@ CompilablePreprocessorType = Union[ preprocessing.OneHotEncoder, preprocessing.StandardScaler, + preprocessing.MaxAbsScaler, preprocessing.LabelEncoder, ] diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 71c21d565a1..86b20996194 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -50,6 +50,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): compose.ColumnTransformer, preprocessing.StandardScaler, preprocessing.OneHotEncoder, + preprocessing.MaxAbsScaler, preprocessing.LabelEncoder, ), ): @@ -147,6 +148,7 @@ def _extract_as_column_transformer( Union[ preprocessing.OneHotEncoder, preprocessing.StandardScaler, + preprocessing.MaxAbsScaler, preprocessing.LabelEncoder, ], Union[str, List[str]], @@ -172,6 +174,13 @@ def _extract_as_column_transformer( *preprocessing.OneHotEncoder._parse_from_sql(transform_sql), ) ) + elif transform_sql.startswith("ML.MAX_ABS_SCALER"): + transformers.append( + ( + "max_abs_encoder", + *preprocessing.MaxAbsScaler._parse_from_sql(transform_sql), + ) + ) elif transform_sql.startswith("ML.LABEL_ENCODER"): transformers.append( ( @@ -193,6 +202,7 @@ def _merge_column_transformer( compose.ColumnTransformer, preprocessing.StandardScaler, preprocessing.OneHotEncoder, + preprocessing.MaxAbsScaler, preprocessing.LabelEncoder, ]: """Try to merge the column transformer to a simple transformer.""" diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 6ee17751df0..f4f5446651c 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -54,8 +54,10 @@ def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: Returns: a list of tuples of (sql_expression, output_name)""" return [ ( - self._base_sql_generator.ml_standard_scaler(column, f"scaled_{column}"), - f"scaled_{column}", + self._base_sql_generator.ml_standard_scaler( + column, f"standard_scaled_{column}" + ), + f"standard_scaled_{column}", ) for column in columns ] @@ -105,6 +107,86 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +class MaxAbsScaler( + base.Transformer, + third_party.bigframes_vendored.sklearn.preprocessing._data.MaxAbsScaler, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.preprocessing._data.MaxAbsScaler.__doc__ + ) + + def __init__(self): + self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return type(other) is MaxAbsScaler and self._bqml_model == other._bqml_model + + def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: a list of column names to transform + + Returns: a list of tuples of (sql_expression, output_name)""" + return [ + ( + self._base_sql_generator.ml_max_abs_scaler( + column, f"max_abs_scaled_{column}" + ), + f"max_abs_scaled_{column}", + ) + for column in columns + ] + + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[MaxAbsScaler, str]: + """Parse SQL to tuple(StandardScaler, column_label). + + Args: + sql: SQL string of format "ML.MAX_ABS_SCALER({col_label}) OVER()" + + Returns: + tuple(StandardScaler, column_label)""" + col_label = sql[sql.find("(") + 1 : sql.find(")")] + return cls(), col_label + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> MaxAbsScaler: + (X,) = utils.convert_to_dataframe(X) + + compiled_transforms = self._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = self._bqml_model_factory.create_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + return self + + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + (X,) = utils.convert_to_dataframe(X) + + df = self._bqml_model.transform(X) + return typing.cast( + bpd.DataFrame, + df[self._output_names], + ) + + class OneHotEncoder( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 5d35a10b966..a54d39e6b2f 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -76,6 +76,10 @@ def ml_standard_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.STANDARD_SCALER for BQML""" return f"""ML.STANDARD_SCALER({numeric_expr_sql}) OVER() AS {name}""" + def ml_max_abs_scaler(self, numeric_expr_sql: str, name: str) -> str: + """Encode ML.MAX_ABS_SCALER for BQML""" + return f"""ML.MAX_ABS_SCALER({numeric_expr_sql}) OVER() AS {name}""" + def ml_one_hot_encoder( self, numeric_expr_sql: str, diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index b65baa63ebd..0c280e5d020 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -56,20 +56,20 @@ def test_columntransformer_standalone_fit_and_transform( [{"index": 1, "value": 1.0}], [{"index": 2, "value": 1.0}], ], - "scaled_culmen_length_mm": [ + "standard_scaled_culmen_length_mm": [ -0.811119671289163, -0.9945520581113803, -1.104611490204711, ], - "scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], + "standard_scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], }, index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), ) - expected.scaled_culmen_length_mm = expected.scaled_culmen_length_mm.astype( - "Float64" + expected.standard_scaled_culmen_length_mm = ( + expected.standard_scaled_culmen_length_mm.astype("Float64") ) - expected.scaled_flipper_length_mm = expected.scaled_flipper_length_mm.astype( - "Float64" + expected.standard_scaled_flipper_length_mm = ( + expected.standard_scaled_flipper_length_mm.astype("Float64") ) pandas.testing.assert_frame_equal(result, expected, rtol=1e-3) @@ -107,20 +107,20 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): [{"index": 1, "value": 1.0}], [{"index": 2, "value": 1.0}], ], - "scaled_culmen_length_mm": [ + "standard_scaled_culmen_length_mm": [ 1.313249, -0.20198, -1.111118, ], - "scaled_flipper_length_mm": [1.251098, -1.196588, -0.054338], + "standard_scaled_flipper_length_mm": [1.251098, -1.196588, -0.054338], }, index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), ) - expected.scaled_culmen_length_mm = expected.scaled_culmen_length_mm.astype( - "Float64" + expected.standard_scaled_culmen_length_mm = ( + expected.standard_scaled_culmen_length_mm.astype("Float64") ) - expected.scaled_flipper_length_mm = expected.scaled_flipper_length_mm.astype( - "Float64" + expected.standard_scaled_flipper_length_mm = ( + expected.standard_scaled_flipper_length_mm.astype("Float64") ) pandas.testing.assert_frame_equal(result, expected, rtol=1e-3) diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 87664b4c3d4..c69a00b81ca 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -566,10 +566,15 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind "species", ), ( - "scale", + "standard_scale", preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "max_abs_scale", + preprocessing.MaxAbsScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -637,6 +642,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "max_abs_scale", + preprocessing.MaxAbsScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -660,30 +670,26 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id assert isinstance(pl_loaded._transform, compose.ColumnTransformer) transformers = pl_loaded._transform.transformers_ - assert len(transformers) == 4 - - assert transformers[0][0] == "ont_hot_encoder" - assert isinstance(transformers[0][1], preprocessing.OneHotEncoder) - one_hot_encoder = transformers[0][1] - assert one_hot_encoder.drop == "most_frequent" - assert one_hot_encoder.min_frequency == 5 - assert one_hot_encoder.max_categories == 100 - assert transformers[0][2] == "species" - - assert transformers[1][0] == "label_encoder" - assert isinstance(transformers[1][1], preprocessing.LabelEncoder) - one_hot_encoder = transformers[1][1] - assert one_hot_encoder.min_frequency == 0 - assert one_hot_encoder.max_categories == 1000001 - assert transformers[1][2] == "species" - - assert transformers[2][0] == "standard_scaler" - assert isinstance(transformers[2][1], preprocessing.StandardScaler) - assert transformers[2][2] == "culmen_length_mm" + expected = [ + ( + "ont_hot_encoder", + preprocessing.OneHotEncoder( + drop="most_frequent", max_categories=100, min_frequency=5 + ), + "species", + ), + ( + "label_encoder", + preprocessing.LabelEncoder(max_categories=1000001, min_frequency=0), + "species", + ), + ("standard_scaler", preprocessing.StandardScaler(), "culmen_length_mm"), + ("max_abs_encoder", preprocessing.MaxAbsScaler(), "culmen_length_mm"), + ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), + ("max_abs_encoder", preprocessing.MaxAbsScaler(), "flipper_length_mm"), + ] - assert transformers[3][0] == "standard_scaler" - assert isinstance(transformers[2][1], preprocessing.StandardScaler) - assert transformers[3][2] == "flipper_length_mm" + assert transformers == expected assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) assert pl_loaded._estimator.fit_intercept is False @@ -717,6 +723,34 @@ def test_pipeline_standard_scaler_to_gbq(penguins_df_default_index, dataset_id): assert pl_loaded._estimator.fit_intercept is False +def test_pipeline_max_abs_scaler_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ("transform", preprocessing.MaxAbsScaler()), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_standard_scaler", replace=True + ) + assert isinstance(pl_loaded._transform, preprocessing.MaxAbsScaler) + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False + + def test_pipeline_one_hot_encoder_to_gbq(penguins_df_default_index, dataset_id): pl = pipeline.Pipeline( [ diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 1f08ef2c2c9..7779eb8f6ea 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -47,9 +47,9 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): expected = pd.DataFrame( { - "scaled_culmen_depth_mm": [0.836148, 0.024748, 0.48116], - "scaled_culmen_length_mm": [-0.81112, -0.994552, -1.104611], - "scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], + "standard_scaled_culmen_depth_mm": [0.836148, 0.024748, 0.48116], + "standard_scaled_culmen_length_mm": [-0.81112, -0.994552, -1.104611], + "standard_scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], }, dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), @@ -76,9 +76,9 @@ def test_standard_scaler_normalizeds_fit_transform(new_penguins_df): expected = pd.DataFrame( { - "scaled_culmen_depth_mm": [1.17072, -1.272416, 0.101848], - "scaled_culmen_length_mm": [1.313249, -0.20198, -1.111118], - "scaled_flipper_length_mm": [1.251089, -1.196588, -0.054338], + "standard_scaled_culmen_depth_mm": [1.17072, -1.272416, 0.101848], + "standard_scaled_culmen_length_mm": [1.313249, -0.20198, -1.111118], + "standard_scaled_flipper_length_mm": [1.251089, -1.196588, -0.054338], }, dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), @@ -107,7 +107,7 @@ def test_standard_scaler_series_normalizes(penguins_df_default_index, new_pengui expected = pd.DataFrame( { - "scaled_culmen_length_mm": [ + "standard_scaled_culmen_length_mm": [ -0.811119671289163, -0.9945520581113803, -1.104611490204711, @@ -120,6 +120,97 @@ def test_standard_scaler_series_normalizes(penguins_df_default_index, new_pengui pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_max_abs_scaler_normalizes(penguins_df_default_index, new_penguins_df): + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. + scaler = bigframes.ml.preprocessing.MaxAbsScaler() + scaler.fit( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ) + + result = scaler.transform( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ).to_pandas() + + # If maxabs-scaled correctly, max should be 1.0 + for column in result.columns: + assert math.isclose(result[column].max(), 1.0, abs_tol=1e-3) + + result = scaler.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "max_abs_scaled_culmen_depth_mm": [0.874419, 0.8, 0.84186], + "max_abs_scaled_culmen_length_mm": [0.662752, 0.645973, 0.635906], + "max_abs_scaled_flipper_length_mm": [0.848485, 0.78355, 0.813853], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_max_abs_scaler_normalizeds_fit_transform(new_penguins_df): + scaler = bigframes.ml.preprocessing.MaxAbsScaler() + result = scaler.fit_transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "max_abs_scaled_culmen_depth_mm": [1.0, 0.914894, 0.962766], + "max_abs_scaled_culmen_length_mm": [1.0, 0.974684, 0.959494], + "max_abs_scaled_flipper_length_mm": [1.0, 0.923469, 0.959184], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): + scaler = bigframes.ml.preprocessing.MaxAbsScaler() + scaler.fit(penguins_df_default_index["culmen_length_mm"]) + + result = scaler.transform(penguins_df_default_index["culmen_length_mm"]).to_pandas() + + # If maxabs-scaled correctly, max should be 1.0 + for column in result.columns: + assert math.isclose(result[column].max(), 1.0, abs_tol=1e-3) + + result = scaler.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "max_abs_scaled_culmen_length_mm": [0.662752, 0.645973, 0.635906], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + def test_one_hot_encoder_default_params(new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder() encoder.fit(new_penguins_df[["species", "sex"]]) diff --git a/tests/unit/ml/test_compose.py b/tests/unit/ml/test_compose.py index c5b3b508762..24cf0a333e7 100644 --- a/tests/unit/ml/test_compose.py +++ b/tests/unit/ml/test_compose.py @@ -12,45 +12,60 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest import sklearn.compose as sklearn_compose # type: ignore import sklearn.preprocessing as sklearn_preprocessing # type: ignore -import bigframes.ml.compose -import bigframes.ml.preprocessing +from bigframes.ml import compose, preprocessing def test_columntransformer_init_expectedtransforms(): - onehot_transformer = bigframes.ml.preprocessing.OneHotEncoder() - scaler_transformer = bigframes.ml.preprocessing.StandardScaler() - label_transformer = bigframes.ml.preprocessing.LabelEncoder() - column_transformer = bigframes.ml.compose.ColumnTransformer( + onehot_transformer = preprocessing.OneHotEncoder() + standard_scaler_transformer = preprocessing.StandardScaler() + max_abs_scaler_transformer = preprocessing.MaxAbsScaler() + label_transformer = preprocessing.LabelEncoder() + column_transformer = compose.ColumnTransformer( [ ("onehot", onehot_transformer, "species"), - ("scale", scaler_transformer, ["culmen_length_mm", "flipper_length_mm"]), - ("onehot", label_transformer, "species"), + ( + "standard_scale", + standard_scaler_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), + ( + "max_abs_scale", + max_abs_scaler_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), + ("label", label_transformer, "species"), ] ) assert column_transformer.transformers_ == [ ("onehot", onehot_transformer, "species"), - ("scale", scaler_transformer, "culmen_length_mm"), - ("scale", scaler_transformer, "flipper_length_mm"), - ("onehot", label_transformer, "species"), + ("standard_scale", standard_scaler_transformer, "culmen_length_mm"), + ("standard_scale", standard_scaler_transformer, "flipper_length_mm"), + ("max_abs_scale", max_abs_scaler_transformer, "culmen_length_mm"), + ("max_abs_scale", max_abs_scaler_transformer, "flipper_length_mm"), + ("label", label_transformer, "species"), ] def test_columntransformer_repr(): - column_transformer = bigframes.ml.compose.ColumnTransformer( + column_transformer = compose.ColumnTransformer( [ ( "onehot", - bigframes.ml.preprocessing.OneHotEncoder(), + preprocessing.OneHotEncoder(), "species", ), ( - "scale", - bigframes.ml.preprocessing.StandardScaler(), + "standard_scale", + preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ( + "max_abs_scale", + preprocessing.MaxAbsScaler(), ["culmen_length_mm", "flipper_length_mm"], ), ] @@ -59,23 +74,29 @@ def test_columntransformer_repr(): assert ( column_transformer.__repr__() == """ColumnTransformer(transformers=[('onehot', OneHotEncoder(), 'species'), - ('scale', StandardScaler(), + ('standard_scale', StandardScaler(), + ['culmen_length_mm', 'flipper_length_mm']), + ('max_abs_scale', MaxAbsScaler(), ['culmen_length_mm', 'flipper_length_mm'])])""" ) -@pytest.mark.skipif(sklearn_compose is None, reason="requires sklearn") def test_columntransformer_repr_matches_sklearn(): - bf_column_transformer = bigframes.ml.compose.ColumnTransformer( + bf_column_transformer = compose.ColumnTransformer( [ ( "onehot", - bigframes.ml.preprocessing.OneHotEncoder(), + preprocessing.OneHotEncoder(), "species", ), ( - "scale", - bigframes.ml.preprocessing.StandardScaler(), + "standard_scale", + preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ( + "max_abs_scale", + preprocessing.MaxAbsScaler(), ["culmen_length_mm", "flipper_length_mm"], ), ] @@ -88,10 +109,15 @@ def test_columntransformer_repr_matches_sklearn(): "species", ), ( - "scale", + "standard_scale", sklearn_preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "max_abs_scale", + sklearn_preprocessing.MaxAbsScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 23b68aa150a..c1b29c5e52d 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -69,6 +69,13 @@ def test_standard_scaler_produces_correct_sql( assert sql == "ML.STANDARD_SCALER(col_a) OVER() AS scaled_col_a" +def test_max_abs_scaler_produces_correct_sql( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_max_abs_scaler("col_a", "scaled_col_a") + assert sql == "ML.MAX_ABS_SCALER(col_a) OVER() AS scaled_col_a" + + def test_one_hot_encoder_produces_correct_sql( base_sql_generator: ml_sql.BaseSqlGenerator, ): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py index 89981e34c03..40b4f76ab7f 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -43,13 +43,16 @@ class StandardScaler(BaseEstimator, TransformerMixin): print(scaler.transform(bpd.DataFrame({"a": [2], "b":[2]}))) """ - def fit(self, X): + def fit(self, X, y=None): """Compute the mean and std to be used for later scaling. Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): The Dataframe or Series with training data. + y (default None): + Ignored. + Returns: StandardScaler: Fitted scaler. """ @@ -66,3 +69,40 @@ def transform(self, X): bigframes.dataframe.DataFrame: Transformed result. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +class MaxAbsScaler(BaseEstimator, TransformerMixin): + """Scale each feature by its maximum absolute value. + + This estimator scales and translates each feature individually such + that the maximal absolute value of each feature in the + training set will be 1.0. It does not shift/center the data, and + thus does not destroy any sparsity. + """ + + def fit(self, X, y=None): + """Compute the maximum absolute value to be used for later scaling. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Dataframe or Series with training data. + + y (default None): + Ignored. + + Returns: + MaxAbsScaler: Fitted scaler. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transform(self, X): + """Scale the data. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. + + Returns: + bigframes.dataframe.DataFrame: Transformed result. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index b0f0df8e157..cf660ece5d4 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -54,13 +54,16 @@ class OneHotEncoder(BaseEstimator): print(enc.transform(bpd.DataFrame({"a": ["Female", "Male"], "b": ["1", "4"]}))) """ - def fit(self, X): + def fit(self, X, y=None): """Fit OneHotEncoder to X. Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): The DataFrame or Series with training data. + y (default None): + Ignored. + Returns: OneHotEncoder: Fitted encoder. """ From 9cf99721ed83704e6ee28b15c699326c431eb252 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 25 Sep 2023 15:46:10 -0700 Subject: [PATCH 035/921] feat: add axis param to simple df aggregations (#52) --- bigframes/core/blocks.py | 50 ++++++++++++--- bigframes/dataframe.py | 63 ++++++++++++------- tests/system/small/test_dataframe.py | 30 ++++++++- .../bigframes_vendored/pandas/core/frame.py | 42 ++++++++++--- 4 files changed, 143 insertions(+), 42 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index c4127c5fd52..b53c2212c10 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -822,22 +822,54 @@ def filter(self, column_id: str, keep_null: bool = False): index_labels=self.index.names, ) - def aggregate_all_and_pivot( + def aggregate_all_and_stack( self, operation: agg_ops.AggregateOp, *, + axis: int | str = 0, value_col_id: str = "values", dropna: bool = True, dtype=pd.Float64Dtype(), ) -> Block: - aggregations = [(col_id, operation, col_id) for col_id in self.value_columns] - result_expr = self.expr.aggregate(aggregations, dropna=dropna).unpivot( - row_labels=self.column_labels.to_list(), - index_col_id="index", - unpivot_columns=[(value_col_id, self.value_columns)], - dtype=dtype, - ) - return Block(result_expr, index_columns=["index"], column_labels=[None]) + axis_n = utils.get_axis_number(axis) + if axis_n == 0: + aggregations = [ + (col_id, operation, col_id) for col_id in self.value_columns + ] + result_expr = self.expr.aggregate(aggregations, dropna=dropna).unpivot( + row_labels=self.column_labels.to_list(), + index_col_id="index", + unpivot_columns=[(value_col_id, self.value_columns)], + dtype=dtype, + ) + return Block(result_expr, index_columns=["index"], column_labels=[None]) + else: # axis_n == 1 + # using offsets as identity to group on. + # TODO: Allow to promote identity/total_order columns instead for better perf + expr_with_offsets, offset_col = self.expr.promote_offsets() + stacked_expr = expr_with_offsets.unpivot( + row_labels=self.column_labels.to_list(), + index_col_id=guid.generate_guid(), + unpivot_columns=[(value_col_id, self.value_columns)], + passthrough_columns=[*self.index_columns, offset_col], + dtype=dtype, + ) + index_aggregations = [ + (col_id, agg_ops.AnyValueOp(), col_id) + for col_id in [*self.index_columns] + ] + main_aggregation = (value_col_id, operation, value_col_id) + result_expr = stacked_expr.aggregate( + [*index_aggregations, main_aggregation], + by_column_ids=[offset_col], + dropna=dropna, + ) + return Block( + result_expr.drop_columns([offset_col]), + self.index_columns, + column_labels=[None], + index_labels=self.index_labels, + ) def select_column(self, id: str) -> Block: return self.select_columns([id]) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0d357e7c3d3..e4e22e03066 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1462,41 +1462,48 @@ def dropna( def any( self, *, + axis: typing.Union[str, int] = 0, bool_only: bool = False, ) -> bigframes.series.Series: if not bool_only: frame = self._raise_on_non_boolean("any") else: frame = self._drop_non_bool() - block = frame._block.aggregate_all_and_pivot( - agg_ops.any_op, dtype=pandas.BooleanDtype() + block = frame._block.aggregate_all_and_stack( + agg_ops.any_op, dtype=pandas.BooleanDtype(), axis=axis ) return bigframes.series.Series(block.select_column("values")) - def all(self, *, bool_only: bool = False) -> bigframes.series.Series: + def all( + self, axis: typing.Union[str, int] = 0, *, bool_only: bool = False + ) -> bigframes.series.Series: if not bool_only: frame = self._raise_on_non_boolean("all") else: frame = self._drop_non_bool() - block = frame._block.aggregate_all_and_pivot( - agg_ops.all_op, dtype=pandas.BooleanDtype() + block = frame._block.aggregate_all_and_stack( + agg_ops.all_op, dtype=pandas.BooleanDtype(), axis=axis ) return bigframes.series.Series(block.select_column("values")) - def sum(self, *, numeric_only: bool = False) -> bigframes.series.Series: + def sum( + self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False + ) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("sum") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.sum_op) + block = frame._block.aggregate_all_and_stack(agg_ops.sum_op, axis=axis) return bigframes.series.Series(block.select_column("values")) - def mean(self, *, numeric_only: bool = False) -> bigframes.series.Series: + def mean( + self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False + ) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("mean") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.mean_op) + block = frame._block.aggregate_all_and_stack(agg_ops.mean_op, axis=axis) return bigframes.series.Series(block.select_column("values")) def median( @@ -1510,47 +1517,57 @@ def median( frame = self._raise_on_non_numeric("median") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.median_op) + block = frame._block.aggregate_all_and_stack(agg_ops.median_op) return bigframes.series.Series(block.select_column("values")) - def std(self, *, numeric_only: bool = False) -> bigframes.series.Series: + def std( + self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False + ) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("std") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.std_op) + block = frame._block.aggregate_all_and_stack(agg_ops.std_op, axis=axis) return bigframes.series.Series(block.select_column("values")) - def var(self, *, numeric_only: bool = False) -> bigframes.series.Series: + def var( + self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False + ) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("var") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.var_op) + block = frame._block.aggregate_all_and_stack(agg_ops.var_op, axis=axis) return bigframes.series.Series(block.select_column("values")) - def min(self, *, numeric_only: bool = False) -> bigframes.series.Series: + def min( + self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False + ) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("min") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.min_op) + block = frame._block.aggregate_all_and_stack(agg_ops.min_op, axis=axis) return bigframes.series.Series(block.select_column("values")) - def max(self, *, numeric_only: bool = False) -> bigframes.series.Series: + def max( + self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False + ) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("max") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.max_op) + block = frame._block.aggregate_all_and_stack(agg_ops.max_op, axis=axis) return bigframes.series.Series(block.select_column("values")) - def prod(self, *, numeric_only: bool = False) -> bigframes.series.Series: + def prod( + self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False + ) -> bigframes.series.Series: if not numeric_only: frame = self._raise_on_non_numeric("prod") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.product_op) + block = frame._block.aggregate_all_and_stack(agg_ops.product_op, axis=axis) return bigframes.series.Series(block.select_column("values")) product = prod @@ -1560,11 +1577,11 @@ def count(self, *, numeric_only: bool = False) -> bigframes.series.Series: frame = self else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_pivot(agg_ops.count_op) + block = frame._block.aggregate_all_and_stack(agg_ops.count_op) return bigframes.series.Series(block.select_column("values")) def nunique(self) -> bigframes.series.Series: - block = self._block.aggregate_all_and_pivot(agg_ops.nunique_op) + block = self._block.aggregate_all_and_stack(agg_ops.nunique_op) return bigframes.series.Series(block.select_column("values")) def agg( @@ -1587,7 +1604,7 @@ def agg( ) else: return bigframes.series.Series( - self._block.aggregate_all_and_pivot( + self._block.aggregate_all_and_stack( agg_ops.lookup_agg_func(typing.cast(str, func)) ) ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index b6ca958c038..adf17848ee6 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1999,6 +1999,29 @@ def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op): pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) +@pytest.mark.parametrize( + ("op"), + [ + (lambda x: x.sum(axis=1, numeric_only=True)), + (lambda x: x.mean(axis=1, numeric_only=True)), + (lambda x: x.min(axis=1, numeric_only=True)), + (lambda x: x.max(axis=1, numeric_only=True)), + (lambda x: x.std(axis=1, numeric_only=True)), + (lambda x: x.var(axis=1, numeric_only=True)), + ], + ids=["sum", "mean", "min", "max", "std", "var"], +) +def test_dataframe_aggregates_axis_1(scalars_df_index, scalars_pandas_df_index, op): + col_names = ["int64_too", "int64_col", "float64_col", "bool_col", "string_col"] + bf_result = op(scalars_df_index[col_names]).to_pandas() + pd_result = op(scalars_pandas_df_index[col_names]) + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + # Pandas has object index type + pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + + def test_dataframe_aggregates_median(scalars_df_index, scalars_pandas_df_index): col_names = ["int64_too", "float64_col", "int64_col", "bool_col"] bf_result = scalars_df_index[col_names].median(numeric_only=True).to_pandas() @@ -2019,11 +2042,16 @@ def test_dataframe_aggregates_median(scalars_df_index, scalars_pandas_df_index): [ (lambda x: x.all(bool_only=True)), (lambda x: x.any(bool_only=True)), + (lambda x: x.all(axis=1, bool_only=True)), + (lambda x: x.any(axis=1, bool_only=True)), ], - ids=["all", "any"], + ids=["all_axis0", "any_axis0", "all_axis1", "any_axis1"], ) def test_dataframe_bool_aggregates(scalars_df_index, scalars_pandas_df_index, op): # Pandas will drop nullable 'boolean' dtype so we convert first to bool, then cast back later + scalars_df_index = scalars_df_index.assign( + bool_col=scalars_df_index.bool_col.fillna(False) + ) scalars_pandas_df_index = scalars_pandas_df_index.assign( bool_col=scalars_pandas_df_index.bool_col.fillna(False).astype("bool") ) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 9d26938e080..6ce11cd7e98 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -11,7 +11,7 @@ """ from __future__ import annotations -from typing import Iterable, Literal, Mapping, Optional, Sequence, Union +from typing import Literal, Mapping, Optional, Sequence, Union import numpy as np @@ -1457,7 +1457,7 @@ def apply(self, func, *, args=(), **kwargs): # ---------------------------------------------------------------------- # ndarray-like stats methods - def any(self, *, bool_only: bool = False): + def any(self, *, axis=0, bool_only: bool = False): """ Return whether any element is True, potentially over an axis. @@ -1466,6 +1466,9 @@ def any(self, *, bool_only: bool = False): non-empty). Args: + axis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. bool_only (bool. default False): Include only boolean columns. @@ -1474,7 +1477,7 @@ def any(self, *, bool_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def all(self, *, bool_only: bool = False): + def all(self, axis=0, *, bool_only: bool = False): """ Return whether all elements are True, potentially over an axis. @@ -1483,6 +1486,9 @@ def all(self, *, bool_only: bool = False): empty). Args: + axis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. bool_only (bool. default False): Include only boolean columns. @@ -1491,11 +1497,14 @@ def all(self, *, bool_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def prod(self, *, numeric_only: bool = False): + def prod(self, axis=0, *, numeric_only: bool = False): """ Return the product of the values over the requested axis. Args: + aßxis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. numeric_only (bool. default False): Include only float, int, boolean columns. @@ -1504,13 +1513,16 @@ def prod(self, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def min(self, *, numeric_only: bool = False): + def min(self, axis=0, *, numeric_only: bool = False): """Return the minimum of the values over the requested axis. If you want the *index* of the minimum, use ``idxmin``. This is the equivalent of the ``numpy.ndarray`` method ``argmin``. Args: + axis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. numeric_only (bool, default False): Default False. Include only float, int, boolean columns. @@ -1519,13 +1531,16 @@ def min(self, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def max(self, *, numeric_only: bool = False): + def max(self, axis=0, *, numeric_only: bool = False): """Return the maximum of the values over the requested axis. If you want the *index* of the maximum, use ``idxmax``. This is the equivalent of the ``numpy.ndarray`` method ``argmax``. Args: + axis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. numeric_only (bool. default False): Default False. Include only float, int, boolean columns. @@ -1534,12 +1549,15 @@ def max(self, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def sum(self, *, numeric_only: bool = False): + def sum(self, axis=0, *, numeric_only: bool = False): """Return the sum of the values over the requested axis. This is equivalent to the method ``numpy.sum``. Args: + axis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. numeric_only (bool. default False): Default False. Include only float, int, boolean columns. @@ -1548,10 +1566,13 @@ def sum(self, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def mean(self, *, numeric_only: bool = False): + def mean(self, axis=0, *, numeric_only: bool = False): """Return the mean of the values over the requested axis. Args: + axis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. numeric_only (bool. default False): Default False. Include only float, int, boolean columns. @@ -1575,12 +1596,15 @@ def median(self, *, numeric_only: bool = False, exact: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def var(self, *, numeric_only: bool = False): + def var(self, axis=0, *, numeric_only: bool = False): """Return unbiased variance over requested axis. Normalized by N-1 by default. Args: + axis ({index (0), columns (1)}): + Axis for the function to be applied on. + For Series this parameter is unused and defaults to 0. numeric_only (bool. default False): Default False. Include only float, int, boolean columns. From d56258cbfcda168cb9e437a021e282818d622d6a Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 26 Sep 2023 01:03:24 +0000 Subject: [PATCH 036/921] fix: Fix header skipping logic in `read_csv` (#49) Change-Id: Ib575e2c2b07f819d1dc499a271fea91107fbb8b4 --- bigframes/session.py | 7 +++---- tests/system/small/test_session.py | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/bigframes/session.py b/bigframes/session.py index 04ae6ba4541..7b827c7dcfc 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -1050,11 +1050,10 @@ def read_csv( # We want to match pandas behavior. If header is 0, no rows should be skipped, so we # do not need to set `skip_leading_rows`. If header is None, then there is no header. # Setting skip_leading_rows to 0 does that. If header=N and N>0, we want to skip N rows. - # `skip_leading_rows` skips N-1 rows, so we set it to header+1. - if header is not None and header > 0: - job_config.skip_leading_rows = header + 1 - elif header is None: + if header is None: job_config.skip_leading_rows = 0 + elif header > 0: + job_config.skip_leading_rows = header return self._read_bigquery_load_job( filepath_or_buffer, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index b7bee16ffd4..614c9537649 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -578,9 +578,12 @@ def test_read_csv_gcs_bq_engine_w_header(session, scalars_df_index, gcs_folder): path = gcs_folder + "test_read_csv_gcs_bq_engine_w_header*.csv" scalars_df_index.to_csv(path, index=False) - # Skip the header and the first 2 data rows. Without provided schema, the column names - # would be like `bool_field_0`, `string_field_1` and etc. - df = session.read_csv(path, header=2, engine="bigquery") + # Skip the header and the first 2 data rows. Note that one line of header + # also got added while writing the csv through `to_csv`, so we would have to + # pass headers=3 in the `read_csv` to skip reading the header and two rows. + # Without provided schema, the column names would be like `bool_field_0`, + # `string_field_1` and etc. + df = session.read_csv(path, header=3, engine="bigquery") assert df.shape[0] == scalars_df_index.shape[0] - 2 assert len(df.columns) == len(scalars_df_index.columns) @@ -609,9 +612,12 @@ def test_read_csv_local_bq_engine_w_header(session, scalars_pandas_df_index): # Using the pandas to_csv method because the BQ one does not support local write. scalars_pandas_df_index.to_csv(path, index=False) - # Skip the header and the first 2 data rows. Without provided schema, the column names - # would be like `bool_field_0`, `string_field_1` and etc. - df = session.read_csv(path, header=2, engine="bigquery") + # Skip the header and the first 2 data rows. Note that one line of + # header also got added while writing the csv through `to_csv`, so we + # would have to pass headers=3 in the `read_csv` to skip reading the + # header and two rows. Without provided schema, the column names would + # be like `bool_field_0`, `string_field_1` and etc. + df = session.read_csv(path, header=3, engine="bigquery") assert df.shape[0] == scalars_pandas_df_index.shape[0] - 2 assert len(df.columns) == len(scalars_pandas_df_index.columns) From 632caec420a7e23188f01b96a00c354d205da74e Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 25 Sep 2023 21:17:11 -0700 Subject: [PATCH 037/921] fix: LabelEncoder params consistent with Sklearn (#60) * fix: LabelEncoder params consistent with Sklearn * fix:add LabelTransformer * fix: address comments for base LabelTransformer * fix: type for params --- bigframes/ml/base.py | 20 +++++++++++++ bigframes/ml/preprocessing.py | 17 +++++------ tests/system/small/ml/test_preprocessing.py | 30 ++++--------------- .../sklearn/preprocessing/_label.py | 12 ++++---- 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index f07274f8fcb..f899ac71193 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -195,3 +195,23 @@ def fit_transform( y: Optional[Union[bpd.DataFrame, bpd.Series]] = None, ) -> bpd.DataFrame: return self.fit(X, y).transform(X) + + +class LabelTransformer(BaseEstimator): + """A BigQuery DataFrames Label Transformer base class that transforms data. + + Also the transformers can be attached to a pipeline with a predictor.""" + + @abc.abstractmethod + def fit(self, y): + pass + + @abc.abstractmethod + def transform(self, y): + pass + + def fit_transform( + self, + y: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: + return self.fit(y).transform(y) diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index f4f5446651c..ed0b36deef9 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -315,7 +315,7 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: class LabelEncoder( - base.Transformer, + base.LabelTransformer, third_party.bigframes_vendored.sklearn.preprocessing._label.LabelEncoder, ): # BQML max value https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder#syntax @@ -401,16 +401,15 @@ def _parse_from_sql(cls, sql: str) -> tuple[LabelEncoder, str]: def fit( self, - X: Union[bpd.DataFrame, bpd.Series], - y=None, # ignored + y: Union[bpd.DataFrame, bpd.Series], ) -> LabelEncoder: - (X,) = utils.convert_to_dataframe(X) + (y,) = utils.convert_to_dataframe(y) - compiled_transforms = self._compile_to_sql(X.columns.tolist()) + compiled_transforms = self._compile_to_sql(y.columns.tolist()) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] self._bqml_model = self._bqml_model_factory.create_model( - X, + y, options={"model_type": "transform_only"}, transforms=transform_sqls, ) @@ -419,13 +418,13 @@ def fit( self._output_names = [name for _, name in compiled_transforms] return self - def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + def transform(self, y: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("Must be fitted before transform") - (X,) = utils.convert_to_dataframe(X) + (y,) = utils.convert_to_dataframe(y) - df = self._bqml_model.transform(X) + df = self._bqml_model.transform(y) return typing.cast( bpd.DataFrame, df[self._output_names], diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 7779eb8f6ea..61bddb144d6 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -357,9 +357,9 @@ def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_ def test_label_encoder_default_params(new_penguins_df): encoder = bigframes.ml.preprocessing.LabelEncoder() - encoder.fit(new_penguins_df[["species", "sex"]]) + encoder.fit(new_penguins_df["species"]) - result = encoder.transform(new_penguins_df).to_pandas() + result = encoder.transform(new_penguins_df["species"]).to_pandas() # TODO: bug? feature columns seem to be in nondeterministic random order # workaround: sort columns by name. Can't repro it in pantheon, so could @@ -368,11 +368,6 @@ def test_label_encoder_default_params(new_penguins_df): expected = pd.DataFrame( { - "labelencoded_sex": [ - 2, - 1, - 1, - ], "labelencoded_species": [ 1, 1, @@ -389,7 +384,7 @@ def test_label_encoder_default_params(new_penguins_df): def test_label_encoder_default_params_fit_transform(new_penguins_df): encoder = bigframes.ml.preprocessing.LabelEncoder() - result = encoder.fit_transform(new_penguins_df[["species", "sex"]]).to_pandas() + result = encoder.fit_transform(new_penguins_df[["species"]]).to_pandas() # TODO: bug? feature columns seem to be in nondeterministic random order # workaround: sort columns by name. Can't repro it in pantheon, so could @@ -398,11 +393,6 @@ def test_label_encoder_default_params_fit_transform(new_penguins_df): expected = pd.DataFrame( { - "labelencoded_sex": [ - 2, - 1, - 1, - ], "labelencoded_species": [ 1, 1, @@ -444,7 +434,7 @@ def test_label_encoder_series_default_params(new_penguins_df): def test_label_encoder_params(new_penguins_df): encoder = bigframes.ml.preprocessing.LabelEncoder(100, 2) - encoder.fit(new_penguins_df[["species", "sex"]]) + encoder.fit(new_penguins_df[["species"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -455,11 +445,6 @@ def test_label_encoder_params(new_penguins_df): expected = pd.DataFrame( { - "labelencoded_sex": [ - 0, - 0, - 0, - ], "labelencoded_species": [ 0, 0, @@ -475,7 +460,7 @@ def test_label_encoder_params(new_penguins_df): def test_label_encoder_different_data(penguins_df_default_index, new_penguins_df): encoder = bigframes.ml.preprocessing.LabelEncoder() - encoder.fit(penguins_df_default_index[["species", "sex"]]) + encoder.fit(penguins_df_default_index[["species"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -486,11 +471,6 @@ def test_label_encoder_different_data(penguins_df_default_index, new_penguins_df expected = pd.DataFrame( { - "labelencoded_sex": [ - 3, - 2, - 2, - ], "labelencoded_species": [ 1, 1, diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_label.py b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py index 7e60c846d45..83f8eb0f9c2 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_label.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py @@ -28,11 +28,11 @@ class LabelEncoder(BaseEstimator): Default None, set limit to 1,000,000. """ - def fit(self, X): - """Fit LabelEncoder to X. + def fit(self, y): + """Fit label encoder. Args: - X (bigframes.dataframe.DataFrame or bigframes.series.Series): + y (bigframes.dataframe.DataFrame or bigframes.series.Series): The DataFrame or Series with training data. Returns: @@ -40,11 +40,11 @@ def fit(self, X): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def transform(self, X): - """Transform X using label encoding. + def transform(self, y): + """Transform y using label encoding. Args: - X (bigframes.dataframe.DataFrame or bigframes.series.Series): + y (bigframes.dataframe.DataFrame or bigframes.series.Series): The DataFrame or Series to be transformed. Returns: From 3502f835b35c437933430698e7a1c9badaddcb99 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 26 Sep 2023 12:09:25 -0700 Subject: [PATCH 038/921] feat: support casting string to integer or float (#59) --- bigframes/dtypes.py | 2 +- tests/system/small/test_series.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 271b8aa2f24..59d3007fab0 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -313,7 +313,7 @@ def cast_ibis_value( ibis_dtypes.string, ), ibis_dtypes.float64: (ibis_dtypes.string, ibis_dtypes.int64), - ibis_dtypes.string: (), + ibis_dtypes.string: (ibis_dtypes.int64, ibis_dtypes.float64), ibis_dtypes.date: (), ibis_dtypes.time: (), ibis_dtypes.timestamp: (ibis_dtypes.Timestamp(timezone="UTC"),), diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index d702049e684..588dcc2c83a 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2389,6 +2389,29 @@ def test_astype(scalars_df_index, scalars_pandas_df_index, column, to_type): pd.testing.assert_series_equal(bf_result, pd_result) +def test_string_astype_int(): + pd_series = pd.Series(["4", "-7", "0", " -03"]) + bf_series = series.Series(pd_series) + + pd_result = pd_series.astype("Int64") + bf_result = bf_series.astype("Int64").to_pandas() + + pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + + +def test_string_astype_float(): + pd_series = pd.Series( + ["1", "-1", "-0", "000", " -03.235", "naN", "-inf", "INf", ".33", "7.235e-8"] + ) + + bf_series = series.Series(pd_series) + + pd_result = pd_series.astype("Float64") + bf_result = bf_series.astype("Float64").to_pandas() + + pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + + @pytest.mark.parametrize( "index", [0, 5, -2], From a6e32aa875370063c48ce7922c2aa369a770bd30 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 26 Sep 2023 13:40:07 -0700 Subject: [PATCH 039/921] feat: Add more index methods (#54) --- bigframes/core/block_transforms.py | 10 +- bigframes/core/indexes/index.py | 118 +++++++++++++-- bigframes/dataframe.py | 7 +- bigframes/series.py | 6 +- tests/system/small/test_index.py | 120 ++++++++++++++++ .../pandas/core/indexes/base.py | 135 ++++++++++++++++++ 6 files changed, 373 insertions(+), 23 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index da6ba65b8a9..d22112417cc 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -218,13 +218,17 @@ def rank( return block.select_columns(rownum_col_ids).with_column_labels(labels) -def dropna(block: blocks.Block, how: typing.Literal["all", "any"] = "any"): +def dropna( + block: blocks.Block, + column_ids: typing.Sequence[str], + how: typing.Literal["all", "any"] = "any", +): """ Drop na entries from block """ if how == "any": filtered_block = block - for column in block.value_columns: + for column in column_ids: filtered_block, result_id = filtered_block.apply_unary_op( column, ops.notnull_op ) @@ -234,7 +238,7 @@ def dropna(block: blocks.Block, how: typing.Literal["all", "any"] = "any"): else: # "all" filtered_block = block predicate = None - for column in block.value_columns: + for column in column_ids: filtered_block, partial_predicate = filtered_block.apply_unary_op( column, ops.notnull_op ) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index c08c851c91d..f211afe4d56 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -24,8 +24,10 @@ import bigframes.constants as constants import bigframes.core as core +import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.joins as joins +import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.dtypes import bigframes.dtypes as bf_dtypes @@ -149,6 +151,27 @@ def has_duplicates(self) -> bool: def _block(self) -> blocks.Block: return self._data._get_block() + @property + def T(self) -> Index: + return self.transpose() + + def transpose(self) -> Index: + return self + + def sort_values(self, *, ascending: bool = True, na_position: str = "last"): + if na_position not in ["first", "last"]: + raise ValueError("Param na_position must be one of 'first' or 'last'") + direction = ( + order.OrderingDirection.ASC if ascending else order.OrderingDirection.DESC + ) + na_last = na_position == "last" + index_columns = self._block.index_columns + ordering = [ + order.OrderingColumnReference(column, direction=direction, na_last=na_last) + for column in index_columns + ] + return Index._from_block(self._block.order_by(ordering)) + def astype( self, dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], @@ -176,6 +199,57 @@ def max(self) -> typing.Any: def min(self) -> typing.Any: return self._apply_aggregation(agg_ops.min_op) + def argmax(self) -> int: + block, row_nums = self._block.promote_offsets() + block = block.order_by( + [ + *[ + order.OrderingColumnReference( + col, direction=order.OrderingDirection.DESC + ) + for col in self._block.index_columns + ], + order.OrderingColumnReference(row_nums), + ] + ) + import bigframes.series as series + + return typing.cast(int, series.Series(block.select_column(row_nums)).iloc[0]) + + def argmin(self) -> int: + block, row_nums = self._block.promote_offsets() + block = block.order_by( + [ + *[ + order.OrderingColumnReference(col) + for col in self._block.index_columns + ], + order.OrderingColumnReference(row_nums), + ] + ) + import bigframes.series as series + + return typing.cast(int, series.Series(block.select_column(row_nums)).iloc[0]) + + def value_counts( + self, + normalize: bool = False, + sort: bool = True, + ascending: bool = False, + *, + dropna: bool = True, + ): + block = block_ops.value_counts( + self._block, + self._block.index_columns, + normalize=normalize, + ascending=ascending, + dropna=dropna, + ) + import bigframes.series as series + + return series.Series(block) + def fillna(self, value=None) -> Index: if self.nlevels > 1: raise TypeError("Multiindex does not support 'fillna'") @@ -185,10 +259,7 @@ def rename(self, name: Union[str, Sequence[str]]) -> Index: names = [name] if isinstance(name, str) else list(name) if len(names) != self.nlevels: raise ValueError("'name' must be same length as levels") - - import bigframes.dataframe as df - - return Index(df.DataFrame(self._block.with_index_labels(names))) + return Index._from_block(self._block.with_index_labels(names)) def drop( self, @@ -210,9 +281,28 @@ def drop( ) block = block.filter(condition_id, keep_null=True) block = block.drop_columns([condition_id]) - import bigframes.dataframe as df + return Index._from_block(block) + + def dropna(self, how: str = "any") -> Index: + if how not in ("any", "all"): + raise ValueError("'how' must be one of 'any', 'all'") + result = block_ops.dropna(self._block, self._block.index_columns, how=how) # type: ignore + return Index._from_block(result) + + def drop_duplicates(self, *, keep: str = "first") -> Index: + block = block_ops.drop_duplicates(self._block, self._block.index_columns, keep) + return Index._from_block(block) + + def isin(self, values) -> Index: + if not utils.is_list_like(values): + raise TypeError( + "only list-like objects are allowed to be passed to " + f"isin(), you passed a [{type(values).__name__}]" + ) - return Index(df.DataFrame(block.select_columns([]))) + return self._apply_unary_op(ops.IsInOp(values, match_nulls=True)).fillna( + value=False + ) def _apply_unary_op( self, @@ -226,9 +316,7 @@ def _apply_unary_op( result_ids.append(result_id) block = block.set_index(result_ids, index_labels=self._block.index_labels) - import bigframes.dataframe as df - - return Index(df.DataFrame(block)) + return Index._from_block(block) def _apply_aggregation(self, op: agg_ops.AggregateOp) -> typing.Any: if self.nlevels > 1: @@ -262,6 +350,12 @@ def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: def __len__(self): return self.shape[0] + @classmethod + def _from_block(cls, block: blocks.Block) -> Index: + import bigframes.dataframe as df + + return Index(df.DataFrame(block)) + class IndexValue: """An immutable index.""" @@ -356,12 +450,6 @@ def resolve_level_name(self: IndexValue, label: blocks.Label) -> str: def is_uniquely_named(self: IndexValue): return len(set(self.names)) == len(self.names) - def _set_block(self, block: blocks.Block): - self._block = block - - def _get_block(self) -> blocks.Block: - return self._block - def join_mono_indexed( left: IndexValue, diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index e4e22e03066..113355589b1 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1440,7 +1440,7 @@ def dropna( axis_n = utils.get_axis_number(axis) if axis_n == 0: - result = block_ops.dropna(self._block, how=how) # type: ignore + result = block_ops.dropna(self._block, self._block.value_columns, how=how) # type: ignore if ignore_index: result = result.reset_index() return DataFrame(result) @@ -1674,7 +1674,10 @@ def pivot( def stack(self): # TODO: support 'level' param by simply reordering levels such that selected level is last before passing to Block.stack. # TODO: match impl to pandas future_stack as described in pandas 2.1 release notes - result_block = block_ops.dropna(self._block.stack(), how="all") + stack_block = self._block.stack() + result_block = block_ops.dropna( + stack_block, stack_block.value_columns, how="all" + ) if not isinstance(self.columns, pandas.MultiIndex): return bigframes.series.Series(result_block) return DataFrame(result_block) diff --git a/bigframes/series.py b/bigframes/series.py index c1c0cb05372..47298d59f50 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -459,7 +459,7 @@ def dropna( ) -> Series: if inplace: raise NotImplementedError("'inplace'=True not supported") - result = block_ops.dropna(self._block, how="any") + result = block_ops.dropna(self._block, [self._value_column], how="any") if ignore_index: result = result.reset_index() return Series(result) @@ -856,7 +856,7 @@ def clip(self, lower, upper): ) return Series(block.select_column(result_id).with_column_labels([self.name])) - def argmax(self) -> scalars.Scalar: + def argmax(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( [ @@ -870,7 +870,7 @@ def argmax(self) -> scalars.Scalar: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) - def argmin(self) -> scalars.Scalar: + def argmin(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( [ diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index 7f09e3a9d51..f7fa0f0855e 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -14,6 +14,7 @@ import numpy import pandas as pd +import pytest from tests.system.utils import assert_pandas_index_equal_ignore_index_type @@ -174,3 +175,122 @@ def test_is_monotonic_decreasing(scalars_df_index, scalars_pandas_df_index): scalars_df_index.index.is_monotonic_increasing == scalars_pandas_df_index.index.is_monotonic_increasing ) + + +def test_index_argmin(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("1."): + pytest.skip("doesn't work in pandas 1.x.") + bf_result = scalars_df_index.set_index(["int64_too", "rowindex_2"]).index.argmin() + pd_result = scalars_pandas_df_index.set_index( + ["int64_too", "rowindex_2"] + ).index.argmin() + assert bf_result == pd_result + + +def test_index_argmax(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("1."): + pytest.skip("doesn't work in pandas 1.x.") + bf_result = scalars_df_index.set_index(["int64_too", "rowindex_2"]).index.argmax() + pd_result = scalars_pandas_df_index.set_index( + ["int64_too", "rowindex_2"] + ).index.argmax() + assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("ascending", "na_position"), + [ + (True, "first"), + (True, "last"), + (False, "first"), + (False, "last"), + ], +) +def test_index_sort_values( + scalars_df_index, scalars_pandas_df_index, ascending, na_position +): + # Test needs values to be unique + bf_result = ( + scalars_df_index.set_index(["int64_too", "rowindex_2"]) + .index.sort_values(ascending=ascending, na_position=na_position) + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index( + ["int64_too", "rowindex_2"] + ).index.sort_values(ascending=ascending, na_position=na_position) + + pd.testing.assert_index_equal( + bf_result, + pd_result, + ) + + +def test_index_value_counts(scalars_df_index, scalars_pandas_df_index): + if pd.__version__.startswith("1."): + pytest.skip("value_counts results different in pandas 1.x.") + bf_result = ( + scalars_df_index.set_index(["int64_too", "rowindex_2"]) + .index.value_counts() + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index( + ["int64_too", "rowindex_2"] + ).index.value_counts() + + pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("how",), + [ + ("any",), + ("all",), + ], +) +def test_index_dropna(scalars_df_index, scalars_pandas_df_index, how): + bf_result = ( + scalars_df_index.set_index(["int64_col", "float64_col"]) + .index.dropna(how=how) + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index( + ["int64_col", "float64_col"] + ).index.dropna(how=how) + pd.testing.assert_index_equal(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("keep",), + [ + ("first",), + ("last",), + (False,), + ], +) +def test_index_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep): + bf_series = ( + scalars_df_index.set_index("int64_col") + .index.drop_duplicates(keep=keep) + .to_pandas() + ) + pd_series = scalars_pandas_df_index.set_index("int64_col").index.drop_duplicates( + keep=keep + ) + pd.testing.assert_index_equal( + pd_series, + bf_series, + ) + + +def test_index_isin(scalars_df_index, scalars_pandas_df_index): + bf_series = ( + scalars_df_index.set_index("int64_col").index.isin([2, 55555, 4]).to_pandas() + ) + pd_result_array = scalars_pandas_df_index.set_index("int64_col").index.isin( + [2, 55555, 4] + ) + pd.testing.assert_index_equal( + pd.Index(pd_result_array), + bf_series, + check_names=False, + ) diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index f89964e2208..e8737341a34 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -53,6 +53,20 @@ def dtypes(self): """Return the dtypes as a Series for the underlying MultiIndex.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def T(self) -> Index: + """Return the transpose, which is by definition self.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transpose(self) -> Index: + """ + Return the transpose, which is by definition self. + + Returns: + Index + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def astype(self, dtype): """Create an Index with values cast to dtypes. @@ -67,6 +81,23 @@ def astype(self, dtype): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def isin(self, values): + """ + Return a boolean array where the index values are in `values`. + + Compute boolean array of whether each index value is found in the + passed set of values. The length of the returned boolean array matches + the length of the index. + + Args: + values (set or list-like): + Sought values. + + Returns: + Series: Series of boolean values. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def all(self) -> bool: """Return whether all elements are Truthy. @@ -99,6 +130,30 @@ def max(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def argmin(self) -> int: + """ + Return int position of the smallest value in the Series. + + If the minimum is achieved in multiple locations, + the first row position is returned. + + Returns: + int: Row position of the minimum value. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def argmax(self) -> int: + """ + Return int position of the largest value in the Series. + + If the maximum is achieved in multiple locations, + the first row position is returned. + + Returns: + int: Row position of the maximum value. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def nunique(self) -> int: """Return number of unique elements in the object. @@ -109,6 +164,57 @@ def nunique(self) -> int: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def sort_values( + self, *, ascending: bool = True, na_position: str = "last" + ) -> Index: + """ + Return a sorted copy of the index. + + Return a sorted copy of the index, and optionally return the indices + that sorted the index itself. + + Args: + ascending (bool, default True): + Should the index values be sorted in an ascending order. + na_position ({'first' or 'last'}, default 'last'): + Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at + the end. + + Returns: + pandas.Index: Sorted copy of the index. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def value_counts( + self, + normalize: bool = True, + sort: bool = True, + ascending: bool = False, + *, + dropna: bool = True, + ): + """Return a Series containing counts of unique values. + + The resulting object will be in descending order so that the + first element is the most frequently-occurring element. + Excludes NA values by default. + + Args: + normalize (bool, default False): + If True then the object returned will contain the relative + frequencies of the unique values. + sort (bool, default True): + Sort by frequencies. + ascending (bool, default False): + Sort in ascending order. + dropna (bool, default True): + Don't include counts of NaN. + + Returns: + Series + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def fillna(self, value) -> Index: """ Fill NA/NaN values with the specified value. @@ -151,6 +257,35 @@ def drop(self, labels) -> Index: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def dropna(self, how: str = "any"): + """Return Index without NA/NaN values. + + Args: + how ({'any', 'all'}, default 'any'): + If the Index is a MultiIndex, drop the value when any or all levels + are NaN. + + Returns: + Index + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def drop_duplicates(self, *, keep: str = "first"): + """ + Return Index with duplicate values removed. + + Args: + keep ({'first', 'last', ``False``}, default 'first'): + One of: + 'first' : Drop duplicates except for the first occurrence. + 'last' : Drop duplicates except for the last occurrence. + ``False`` : Drop all duplicates. + + Returns: + Index + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def to_numpy(self, dtype): """ A NumPy ndarray representing the values in this Series or Index. From e804e130c218d8ac81a8fc0a853eeb7a93884a50 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 26 Sep 2023 14:42:40 -0700 Subject: [PATCH 040/921] refactor: push down SQL generate logic in core.BqmlModelFactory (#62) --- bigframes/ml/core.py | 33 ++++++------------------ bigframes/ml/sql.py | 23 ++++++++++++----- tests/unit/ml/test_sql.py | 54 +++++++++++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 46 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 70be0d35ee1..37478d8bafd 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -251,17 +251,10 @@ def create_model( session = X_train._session - source_sql = input_data.sql - options_sql = self._model_creation_sql_generator.options(**options) - transform_sql = ( - self._model_creation_sql_generator.transform(*transforms) - if transforms is not None - else None - ) sql = self._model_creation_sql_generator.create_model( - source_sql=source_sql, - transform_sql=transform_sql, - options_sql=options_sql, + source=input_data, + transforms=transforms, + options=options, ) return self._create_model_with_sql(session=session, sql=sql) @@ -287,18 +280,10 @@ def create_time_series_model( session = X_train._session - source_sql = input_data.sql - options_sql = self._model_creation_sql_generator.options(**options) - - transform_sql = ( - self._model_creation_sql_generator.transform(*transforms) - if transforms is not None - else None - ) sql = self._model_creation_sql_generator.create_model( - source_sql=source_sql, - transform_sql=transform_sql, - options_sql=options_sql, + source=input_data, + transforms=transforms, + options=options, ) return self._create_model_with_sql(session=session, sql=sql) @@ -320,10 +305,9 @@ def create_remote_model( Returns: BqmlModel: a BqmlModel wrapping a trained model in BigQuery """ - options_sql = self._model_creation_sql_generator.options(**options) sql = self._model_creation_sql_generator.create_remote_model( connection_name=connection_name, - options_sql=options_sql, + options=options, ) return self._create_model_with_sql(session=session, sql=sql) @@ -341,9 +325,8 @@ def create_imported_model( Returns: a BqmlModel, wrapping a trained model in BigQuery """ - options_sql = self._model_creation_sql_generator.options(**options) sql = self._model_creation_sql_generator.create_imported_model( - options_sql=options_sql, + options=options, ) return self._create_model_with_sql(session=session, sql=sql) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index a54d39e6b2f..c1b4f46e185 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -16,9 +16,10 @@ Generates SQL queries needed for BigQuery DataFrames ML """ -from typing import Iterable, Optional, Union +from typing import Iterable, Mapping, Optional, Union import bigframes.constants as constants +import bigframes.pandas as bpd class BaseSqlGenerator: @@ -113,11 +114,15 @@ def __init__(self, model_id: str): # Model create and alter def create_model( self, - source_sql: str, - transform_sql: Optional[str] = None, - options_sql: Optional[str] = None, + source: bpd.DataFrame, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + transforms: Optional[Iterable[str]] = None, ) -> str: """Encode the CREATE TEMP MODEL statement for BQML""" + source_sql = source.sql + transform_sql = self.transform(*transforms) if transforms is not None else None + options_sql = self.options(**options) + parts = [f"CREATE TEMP MODEL `{self._model_id}`"] if transform_sql: parts.append(transform_sql) @@ -129,9 +134,11 @@ def create_model( def create_remote_model( self, connection_name: str, - options_sql: Optional[str] = None, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + options_sql = self.options(**options) + parts = [f"CREATE TEMP MODEL `{self._model_id}`"] parts.append(self.connection(connection_name)) if options_sql: @@ -140,9 +147,11 @@ def create_remote_model( def create_imported_model( self, - options_sql: Optional[str] = None, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + options_sql = self.options(**options) + parts = [f"CREATE TEMP MODEL `{self._model_id}`"] if options_sql: parts.append(options_sql) @@ -150,7 +159,7 @@ def create_imported_model( class ModelManipulationSqlGenerator(BaseSqlGenerator): - """Sql generator for manipulating a model entity. Model name is the fully model path of project_id.dataset_id.model_id.""" + """Sql generator for manipulating a model entity. Model name is the full model path of project_id.dataset_id.model_id.""" def __init__(self, model_name: str): self._model_name = model_name diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index c1b29c5e52d..b88523c7efa 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock + import pytest import bigframes.ml.sql as ml_sql +import bigframes.pandas as bpd @pytest.fixture(scope="session") @@ -34,6 +37,14 @@ def model_manipulation_sql_generator() -> ml_sql.ModelManipulationSqlGenerator: ) +@pytest.fixture(scope="session") +def mock_df(): + mock_df = mock.create_autospec(spec=bpd.DataFrame) + mock_df.sql = "input_X_y_sql" + + return mock_df + + def test_options_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenerator): sql = base_sql_generator.options( model_type="lin_reg", input_label_cols=["col_a"], l1_reg=0.6 @@ -96,33 +107,44 @@ def test_label_encoder_produces_correct_sql( def test_create_model_produces_correct_sql( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, + mock_df: bpd.DataFrame, ): sql = model_creation_sql_generator.create_model( - source_sql="my_source_sql", - options_sql="my_options_sql", + source=mock_df, + options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( sql == """CREATE TEMP MODEL `my_model_id` -my_options_sql -AS my_source_sql""" +OPTIONS( + option_key1="option_value1", + option_key2=2) +AS input_X_y_sql""" ) def test_create_model_transform_produces_correct_sql( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, + mock_df: bpd.DataFrame, ): sql = model_creation_sql_generator.create_model( - source_sql="my_source_sql", - options_sql="my_options_sql", - transform_sql="my_transform_sql", + source=mock_df, + options={"option_key1": "option_value1", "option_key2": 2}, + transforms=[ + "ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a", + "ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b", + ], ) assert ( sql == """CREATE TEMP MODEL `my_model_id` -my_transform_sql -my_options_sql -AS my_source_sql""" +TRANSFORM( + ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a, + ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b) +OPTIONS( + option_key1="option_value1", + option_key2=2) +AS input_X_y_sql""" ) @@ -131,13 +153,15 @@ def test_create_remote_model_produces_correct_sql( ): sql = model_creation_sql_generator.create_remote_model( connection_name="my_project.us.my_connection", - options_sql="my_options_sql", + options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( sql == """CREATE TEMP MODEL `my_model_id` REMOTE WITH CONNECTION `my_project.us.my_connection` -my_options_sql""" +OPTIONS( + option_key1="option_value1", + option_key2=2)""" ) @@ -145,12 +169,14 @@ def test_create_imported_model_produces_correct_sql( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): sql = model_creation_sql_generator.create_imported_model( - options_sql="my_options_sql", + options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( sql == """CREATE TEMP MODEL `my_model_id` -my_options_sql""" +OPTIONS( + option_key1="option_value1", + option_key2=2)""" ) From 392113b70d6a8c407accbb6684d75b31261e3741 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 26 Sep 2023 16:25:59 -0700 Subject: [PATCH 041/921] feat: add ml.preprocessing.MinMaxScaler (#64) * feat: add ml.preprocessing.MinMaxScaler * fix comments and typo * add test check for min value * nit fix --- bigframes/ml/compose.py | 1 + bigframes/ml/pipeline.py | 12 ++- bigframes/ml/preprocessing.py | 84 ++++++++++++++++- bigframes/ml/sql.py | 4 + tests/system/large/ml/test_pipeline.py | 46 ++++++++- tests/system/small/ml/test_preprocessing.py | 93 +++++++++++++++++++ tests/unit/ml/test_compose.py | 25 +++++ tests/unit/ml/test_sql.py | 7 ++ .../sklearn/preprocessing/_data.py | 36 +++++++ 9 files changed, 302 insertions(+), 6 deletions(-) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index a1075c23981..9effbf1968c 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -30,6 +30,7 @@ preprocessing.OneHotEncoder, preprocessing.StandardScaler, preprocessing.MaxAbsScaler, + preprocessing.MinMaxScaler, preprocessing.LabelEncoder, ] diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 86b20996194..ac02c391129 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -51,6 +51,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): preprocessing.StandardScaler, preprocessing.OneHotEncoder, preprocessing.MaxAbsScaler, + preprocessing.MinMaxScaler, preprocessing.LabelEncoder, ), ): @@ -149,6 +150,7 @@ def _extract_as_column_transformer( preprocessing.OneHotEncoder, preprocessing.StandardScaler, preprocessing.MaxAbsScaler, + preprocessing.MinMaxScaler, preprocessing.LabelEncoder, ], Union[str, List[str]], @@ -177,10 +179,17 @@ def _extract_as_column_transformer( elif transform_sql.startswith("ML.MAX_ABS_SCALER"): transformers.append( ( - "max_abs_encoder", + "max_abs_scaler", *preprocessing.MaxAbsScaler._parse_from_sql(transform_sql), ) ) + elif transform_sql.startswith("ML.MIN_MAX_SCALER"): + transformers.append( + ( + "min_max_scaler", + *preprocessing.MinMaxScaler._parse_from_sql(transform_sql), + ) + ) elif transform_sql.startswith("ML.LABEL_ENCODER"): transformers.append( ( @@ -203,6 +212,7 @@ def _merge_column_transformer( preprocessing.StandardScaler, preprocessing.OneHotEncoder, preprocessing.MaxAbsScaler, + preprocessing.MinMaxScaler, preprocessing.LabelEncoder, ]: """Try to merge the column transformer to a simple transformer.""" diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index ed0b36deef9..caf4657a633 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -144,13 +144,13 @@ def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: @classmethod def _parse_from_sql(cls, sql: str) -> tuple[MaxAbsScaler, str]: - """Parse SQL to tuple(StandardScaler, column_label). + """Parse SQL to tuple(MaxAbsScaler, column_label). Args: sql: SQL string of format "ML.MAX_ABS_SCALER({col_label}) OVER()" Returns: - tuple(StandardScaler, column_label)""" + tuple(MaxAbsScaler, column_label)""" col_label = sql[sql.find("(") + 1 : sql.find(")")] return cls(), col_label @@ -187,6 +187,86 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +class MinMaxScaler( + base.Transformer, + third_party.bigframes_vendored.sklearn.preprocessing._data.MinMaxScaler, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.preprocessing._data.MinMaxScaler.__doc__ + ) + + def __init__(self): + self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return type(other) is MinMaxScaler and self._bqml_model == other._bqml_model + + def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: a list of column names to transform + + Returns: a list of tuples of (sql_expression, output_name)""" + return [ + ( + self._base_sql_generator.ml_min_max_scaler( + column, f"min_max_scaled_{column}" + ), + f"min_max_scaled_{column}", + ) + for column in columns + ] + + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[MinMaxScaler, str]: + """Parse SQL to tuple(MinMaxScaler, column_label). + + Args: + sql: SQL string of format "ML.MIN_MAX_SCALER({col_label}) OVER()" + + Returns: + tuple(MinMaxScaler, column_label)""" + col_label = sql[sql.find("(") + 1 : sql.find(")")] + return cls(), col_label + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> MinMaxScaler: + (X,) = utils.convert_to_dataframe(X) + + compiled_transforms = self._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = self._bqml_model_factory.create_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + return self + + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + (X,) = utils.convert_to_dataframe(X) + + df = self._bqml_model.transform(X) + return typing.cast( + bpd.DataFrame, + df[self._output_names], + ) + + class OneHotEncoder( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index c1b4f46e185..3897d1be390 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -81,6 +81,10 @@ def ml_max_abs_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.MAX_ABS_SCALER for BQML""" return f"""ML.MAX_ABS_SCALER({numeric_expr_sql}) OVER() AS {name}""" + def ml_min_max_scaler(self, numeric_expr_sql: str, name: str) -> str: + """Encode ML.MIN_MAX_SCALER for BQML""" + return f"""ML.MIN_MAX_SCALER({numeric_expr_sql}) OVER() AS {name}""" + def ml_one_hot_encoder( self, numeric_expr_sql: str, diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index c69a00b81ca..34a2ca01011 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -575,6 +575,11 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind preprocessing.MaxAbsScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "min_max_scale", + preprocessing.MinMaxScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -647,6 +652,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id preprocessing.MaxAbsScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "min_max_scale", + preprocessing.MinMaxScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -684,9 +694,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id "species", ), ("standard_scaler", preprocessing.StandardScaler(), "culmen_length_mm"), - ("max_abs_encoder", preprocessing.MaxAbsScaler(), "culmen_length_mm"), + ("max_abs_scaler", preprocessing.MaxAbsScaler(), "culmen_length_mm"), + ("min_max_scaler", preprocessing.MinMaxScaler(), "culmen_length_mm"), ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), - ("max_abs_encoder", preprocessing.MaxAbsScaler(), "flipper_length_mm"), + ("max_abs_scaler", preprocessing.MaxAbsScaler(), "flipper_length_mm"), + ("min_max_scaler", preprocessing.MinMaxScaler(), "flipper_length_mm"), ] assert transformers == expected @@ -743,7 +755,7 @@ def test_pipeline_max_abs_scaler_to_gbq(penguins_df_default_index, dataset_id): pl.fit(X_train, y_train) pl_loaded = pl.to_gbq( - f"{dataset_id}.test_penguins_pipeline_standard_scaler", replace=True + f"{dataset_id}.test_penguins_pipeline_min_max_scaler", replace=True ) assert isinstance(pl_loaded._transform, preprocessing.MaxAbsScaler) @@ -751,6 +763,34 @@ def test_pipeline_max_abs_scaler_to_gbq(penguins_df_default_index, dataset_id): assert pl_loaded._estimator.fit_intercept is False +def test_pipeline_min_max_scaler_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ("transform", preprocessing.MinMaxScaler()), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_min_max_scaler", replace=True + ) + assert isinstance(pl_loaded._transform, preprocessing.MinMaxScaler) + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False + + def test_pipeline_one_hot_encoder_to_gbq(penguins_df_default_index, dataset_id): pl = pipeline.Pipeline( [ diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 61bddb144d6..fc8f3251bda 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -211,6 +211,99 @@ def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguin pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_min_max_scaler_normalizeds_fit_transform(new_penguins_df): + scaler = bigframes.ml.preprocessing.MinMaxScaler() + result = scaler.fit_transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "min_max_scaled_culmen_depth_mm": [1.0, 0.0, 0.5625], + "min_max_scaled_culmen_length_mm": [1.0, 0.375, 0.0], + "min_max_scaled_flipper_length_mm": [1.0, 0.0, 0.466667], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_min_max_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): + scaler = bigframes.ml.preprocessing.MinMaxScaler() + scaler.fit(penguins_df_default_index["culmen_length_mm"]) + + result = scaler.transform(penguins_df_default_index["culmen_length_mm"]).to_pandas() + + # If minmax-scaled correctly, min should be 0 and max should be 1. + for column in result.columns: + assert math.isclose(result[column].max(), 1.0, abs_tol=1e-3) + assert math.isclose(result[column].min(), 0.0, abs_tol=1e-3) + + result = scaler.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "min_max_scaled_culmen_length_mm": [0.269091, 0.232727, 0.210909], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. + scaler = bigframes.ml.preprocessing.MinMaxScaler() + scaler.fit( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ) + + result = scaler.transform( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ).to_pandas() + + # If minmax-scaled correctly, min should be 0 and max should be 1. + for column in result.columns: + assert math.isclose(result[column].max(), 1.0, abs_tol=1e-3) + assert math.isclose(result[column].min(), 0.0, abs_tol=1e-3) + + result = scaler.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "min_max_scaled_culmen_depth_mm": [0.678571, 0.4880952, 0.595238], + "min_max_scaled_culmen_length_mm": [0.269091, 0.232727, 0.210909], + "min_max_scaled_flipper_length_mm": [0.40678, 0.152542, 0.271186], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + def test_one_hot_encoder_default_params(new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder() encoder.fit(new_penguins_df[["species", "sex"]]) diff --git a/tests/unit/ml/test_compose.py b/tests/unit/ml/test_compose.py index 24cf0a333e7..8c8fbd6ab54 100644 --- a/tests/unit/ml/test_compose.py +++ b/tests/unit/ml/test_compose.py @@ -22,6 +22,7 @@ def test_columntransformer_init_expectedtransforms(): onehot_transformer = preprocessing.OneHotEncoder() standard_scaler_transformer = preprocessing.StandardScaler() max_abs_scaler_transformer = preprocessing.MaxAbsScaler() + min_max_scaler_transformer = preprocessing.MinMaxScaler() label_transformer = preprocessing.LabelEncoder() column_transformer = compose.ColumnTransformer( [ @@ -36,6 +37,11 @@ def test_columntransformer_init_expectedtransforms(): max_abs_scaler_transformer, ["culmen_length_mm", "flipper_length_mm"], ), + ( + "min_max_scale", + min_max_scaler_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), ("label", label_transformer, "species"), ] ) @@ -46,6 +52,8 @@ def test_columntransformer_init_expectedtransforms(): ("standard_scale", standard_scaler_transformer, "flipper_length_mm"), ("max_abs_scale", max_abs_scaler_transformer, "culmen_length_mm"), ("max_abs_scale", max_abs_scaler_transformer, "flipper_length_mm"), + ("min_max_scale", min_max_scaler_transformer, "culmen_length_mm"), + ("min_max_scale", min_max_scaler_transformer, "flipper_length_mm"), ("label", label_transformer, "species"), ] @@ -68,6 +76,11 @@ def test_columntransformer_repr(): preprocessing.MaxAbsScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "min_max_scale", + preprocessing.MinMaxScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) @@ -77,6 +90,8 @@ def test_columntransformer_repr(): ('standard_scale', StandardScaler(), ['culmen_length_mm', 'flipper_length_mm']), ('max_abs_scale', MaxAbsScaler(), + ['culmen_length_mm', 'flipper_length_mm']), + ('min_max_scale', MinMaxScaler(), ['culmen_length_mm', 'flipper_length_mm'])])""" ) @@ -99,6 +114,11 @@ def test_columntransformer_repr_matches_sklearn(): preprocessing.MaxAbsScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "min_max_scale", + preprocessing.MinMaxScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) sk_column_transformer = sklearn_compose.ColumnTransformer( @@ -118,6 +138,11 @@ def test_columntransformer_repr_matches_sklearn(): sklearn_preprocessing.MaxAbsScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "min_max_scale", + sklearn_preprocessing.MinMaxScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index b88523c7efa..f461dc76df8 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -87,6 +87,13 @@ def test_max_abs_scaler_produces_correct_sql( assert sql == "ML.MAX_ABS_SCALER(col_a) OVER() AS scaled_col_a" +def test_min_max_scaler_produces_correct_sql( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_min_max_scaler("col_a", "scaled_col_a") + assert sql == "ML.MIN_MAX_SCALER(col_a) OVER() AS scaled_col_a" + + def test_one_hot_encoder_produces_correct_sql( base_sql_generator: ml_sql.BaseSqlGenerator, ): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py index 40b4f76ab7f..58e16e135b7 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -106,3 +106,39 @@ def transform(self, X): bigframes.dataframe.DataFrame: Transformed result. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +class MinMaxScaler(BaseEstimator, TransformerMixin): + """Transform features by scaling each feature to a given range. + + This estimator scales and translates each feature individually such + that it is in the given range on the training set, e.g. between + zero and one. + """ + + def fit(self, X, y=None): + """Compute the minimum and maximum to be used for later scaling. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Dataframe or Series with training data. + + y (default None): + Ignored. + + Returns: + MaxAbsScaler: Fitted scaler. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transform(self, X): + """Scale the data. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. + + Returns: + bigframes.dataframe.DataFrame: Transformed result. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 61200bd3ae08ddafcc5e59131ac0295188e81f53 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 27 Sep 2023 11:14:19 -0700 Subject: [PATCH 042/921] refactor: push down SQL generate logic in core.BqmlModel (#66) --- bigframes/ml/core.py | 41 ++++++++++--------------------- bigframes/ml/sql.py | 43 ++++++++++++++++++++++++--------- tests/unit/ml/test_sql.py | 51 +++++++++++++++++++++++++++------------ 3 files changed, 80 insertions(+), 55 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 37478d8bafd..4c5a48cf625 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -58,7 +58,7 @@ def model(self) -> bigquery.Model: def _apply_sql( self, input_data: bpd.DataFrame, - func: Callable[[str], str], + func: Callable[[bpd.DataFrame], str], ) -> bpd.DataFrame: """Helper to wrap a dataframe in a SQL query, keeping the index intact. @@ -74,11 +74,9 @@ def _apply_sql( string from which to construct the output dataframe. It must include the index columns of the input SQL. """ - source_sql, index_col_ids, index_labels = input_data._to_sql_query( - include_index=True - ) + _, index_col_ids, index_labels = input_data._to_sql_query(include_index=True) - sql = func(source_sql) + sql = func(input_data) df = self._session.read_gbq(sql, index_col=index_col_ids) df.index.names = index_labels @@ -106,11 +104,9 @@ def generate_text( # TODO: validate input data schema return self._apply_sql( input_data, - lambda source_sql: self._model_manipulation_sql_generator.ml_generate_text( - source_sql=source_sql, - struct_options=self._model_manipulation_sql_generator.struct_options( - **options - ), + lambda source_df: self._model_manipulation_sql_generator.ml_generate_text( + source_df=source_df, + struct_options=options, ), ) @@ -122,11 +118,9 @@ def generate_text_embedding( # TODO: validate input data schema return self._apply_sql( input_data, - lambda source_sql: self._model_manipulation_sql_generator.ml_generate_text_embedding( - source_sql=source_sql, - struct_options=self._model_manipulation_sql_generator.struct_options( - **options - ), + lambda source_df: self._model_manipulation_sql_generator.ml_generate_text_embedding( + source_df=source_df, + struct_options=options, ), ) @@ -136,13 +130,7 @@ def forecast(self) -> bpd.DataFrame: def evaluate(self, input_data: Optional[bpd.DataFrame] = None): # TODO: validate input data schema - # Note: don't need index as evaluate returns a new table - source_sql, _, _ = ( - input_data._to_sql_query(include_index=False) - if (input_data is not None) - else (None, None, None) - ) - sql = self._model_manipulation_sql_generator.ml_evaluate(source_sql) + sql = self._model_manipulation_sql_generator.ml_evaluate(input_data) return self._session.read_gbq(sql) @@ -188,11 +176,8 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: # truncate as Vertex ID only accepts 63 characters, easily exceeding the limit for temp models. # The possibility of conflicts should be low. vertex_ai_model_id = vertex_ai_model_id[:63] - options_sql = self._model_manipulation_sql_generator.options( - **{"vertex_ai_model_id": vertex_ai_model_id} - ) sql = self._model_manipulation_sql_generator.alter_model( - options_sql=options_sql + options={"vertex_ai_model_id": vertex_ai_model_id} ) # Register the model and wait it to finish self._session._start_query(sql) @@ -252,7 +237,7 @@ def create_model( session = X_train._session sql = self._model_creation_sql_generator.create_model( - source=input_data, + source_df=input_data, transforms=transforms, options=options, ) @@ -281,7 +266,7 @@ def create_time_series_model( session = X_train._session sql = self._model_creation_sql_generator.create_model( - source=input_data, + source_df=input_data, transforms=transforms, options=options, ) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 3897d1be390..57c8ba672af 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -118,12 +118,12 @@ def __init__(self, model_id: str): # Model create and alter def create_model( self, - source: bpd.DataFrame, + source_df: bpd.DataFrame, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, transforms: Optional[Iterable[str]] = None, ) -> str: """Encode the CREATE TEMP MODEL statement for BQML""" - source_sql = source.sql + source_sql = source_df.sql transform_sql = self.transform(*transforms) if transforms is not None else None options_sql = self.options(**options) @@ -168,39 +168,58 @@ class ModelManipulationSqlGenerator(BaseSqlGenerator): def __init__(self, model_name: str): self._model_name = model_name + def _source_sql(self, source_df: bpd.DataFrame) -> str: + """Return DataFrame sql with index columns.""" + _source_sql, _, _ = source_df._to_sql_query(include_index=True) + return _source_sql + # Alter model def alter_model( self, - options_sql: str, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: """Encode the ALTER MODEL statement for BQML""" + options_sql = self.options(**options) + parts = [f"ALTER MODEL `{self._model_name}`"] parts.append(f"SET {options_sql}") return "\n".join(parts) # ML prediction TVFs - def ml_predict(self, source_sql: str) -> str: + def ml_predict(self, source_df: bpd.DataFrame) -> str: """Encode ML.PREDICT for BQML""" return f"""SELECT * FROM ML.PREDICT(MODEL `{self._model_name}`, - ({source_sql}))""" + ({self._source_sql(source_df)}))""" def ml_forecast(self) -> str: """Encode ML.FORECAST for BQML""" return f"""SELECT * FROM ML.FORECAST(MODEL `{self._model_name}`)""" - def ml_generate_text(self, source_sql: str, struct_options: str) -> str: + def ml_generate_text( + self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] + ) -> str: """Encode ML.GENERATE_TEXT for BQML""" + struct_options_sql = self.struct_options(**struct_options) return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL `{self._model_name}`, - ({source_sql}), {struct_options})""" + ({self._source_sql(source_df)}), {struct_options_sql})""" - def ml_generate_text_embedding(self, source_sql: str, struct_options: str) -> str: + def ml_generate_text_embedding( + self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] + ) -> str: """Encode ML.GENERATE_TEXT_EMBEDDING for BQML""" + struct_options_sql = self.struct_options(**struct_options) return f"""SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `{self._model_name}`, - ({source_sql}), {struct_options})""" + ({self._source_sql(source_df)}), {struct_options_sql})""" # ML evaluation TVFs - def ml_evaluate(self, source_sql: Optional[str] = None) -> str: + def ml_evaluate(self, source_df: Optional[bpd.DataFrame] = None) -> str: """Encode ML.EVALUATE for BQML""" + if source_df is None: + source_sql = None + else: + # Note: don't need index as evaluate returns a new table + source_sql, _, _ = source_df._to_sql_query(include_index=False) + if source_sql is None: return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`)""" else: @@ -222,7 +241,7 @@ def ml_principal_component_info(self) -> str: ) # ML transform TVF, that require a transform_only type model - def ml_transform(self, source_sql: str) -> str: + def ml_transform(self, source_df: bpd.DataFrame) -> str: """Encode ML.TRANSFORM for BQML""" return f"""SELECT * FROM ML.TRANSFORM(MODEL `{self._model_name}`, - ({source_sql}))""" + ({self._source_sql(source_df)}))""" diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index f461dc76df8..a3338e762d4 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -41,6 +41,7 @@ def model_manipulation_sql_generator() -> ml_sql.ModelManipulationSqlGenerator: def mock_df(): mock_df = mock.create_autospec(spec=bpd.DataFrame) mock_df.sql = "input_X_y_sql" + mock_df._to_sql_query.return_value = "input_X_sql", None, None return mock_df @@ -117,7 +118,7 @@ def test_create_model_produces_correct_sql( mock_df: bpd.DataFrame, ): sql = model_creation_sql_generator.create_model( - source=mock_df, + source_df=mock_df, options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( @@ -135,7 +136,7 @@ def test_create_model_transform_produces_correct_sql( mock_df: bpd.DataFrame, ): sql = model_creation_sql_generator.create_model( - source=mock_df, + source_df=mock_df, options={"option_key1": "option_value1", "option_key2": 2}, transforms=[ "ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a", @@ -191,38 +192,38 @@ def test_alter_model_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): sql = model_manipulation_sql_generator.alter_model( - options_sql="my_options_sql", + options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( sql == """ALTER MODEL `my_project_id.my_dataset_id.my_model_id` -SET my_options_sql""" +SET OPTIONS( + option_key1="option_value1", + option_key2=2)""" ) def test_ml_predict_produces_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, + mock_df: bpd.DataFrame, ): - sql = model_manipulation_sql_generator.ml_predict( - source_sql="SELECT * FROM my_table" - ) + sql = model_manipulation_sql_generator.ml_predict(source_df=mock_df) assert ( sql == """SELECT * FROM ML.PREDICT(MODEL `my_project_id.my_dataset_id.my_model_id`, - (SELECT * FROM my_table))""" + (input_X_sql))""" ) def test_ml_evaluate_produces_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, + mock_df: bpd.DataFrame, ): - sql = model_manipulation_sql_generator.ml_evaluate( - source_sql="SELECT * FROM my_table" - ) + sql = model_manipulation_sql_generator.ml_evaluate(source_df=mock_df) assert ( sql == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, - (SELECT * FROM my_table))""" + (input_X_sql))""" ) @@ -248,15 +249,35 @@ def test_ml_centroids_produces_correct_sql( def test_ml_generate_text_produces_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, + mock_df: bpd.DataFrame, ): sql = model_manipulation_sql_generator.ml_generate_text( - source_sql="SELECT * FROM my_table", - struct_options="STRUCT(value AS item)", + source_df=mock_df, + struct_options={"option_key1": 1, "option_key2": 2.2}, ) assert ( sql == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_project_id.my_dataset_id.my_model_id`, - (SELECT * FROM my_table), STRUCT(value AS item))""" + (input_X_sql), STRUCT( + 1 AS option_key1, + 2.2 AS option_key2))""" + ) + + +def test_ml_generate_text_embedding_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, + mock_df: bpd.DataFrame, +): + sql = model_manipulation_sql_generator.ml_generate_text_embedding( + source_df=mock_df, + struct_options={"option_key1": 1, "option_key2": 2.2}, + ) + assert ( + sql + == """SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `my_project_id.my_dataset_id.my_model_id`, + (input_X_sql), STRUCT( + 1 AS option_key1, + 2.2 AS option_key2))""" ) From 7ab65e88deb0080e9c36c2709f8a5385ccaf8cf2 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 27 Sep 2023 21:00:26 -0700 Subject: [PATCH 043/921] fix: generate unique ids on join to avoid id collisions (#65) * fix: generate unique ids on join to avoid id collisions --- bigframes/core/joins/single_column.py | 80 +++++++++++---------------- 1 file changed, 31 insertions(+), 49 deletions(-) diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py index 434cc2cd794..8a9825cf0b5 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/joins/single_column.py @@ -16,6 +16,7 @@ from __future__ import annotations +import itertools import typing from typing import Callable, Literal, Tuple @@ -25,7 +26,7 @@ import bigframes.constants as constants import bigframes.core as core -import bigframes.core.guid +import bigframes.core.guid as guid import bigframes.core.joins.row_identity import bigframes.core.ordering @@ -122,17 +123,38 @@ def join_by_column( ), ) else: + lmapping = { + col_id: guid.generate_guid() + for col_id in itertools.chain( + left.column_names, left._hidden_ordering_column_names + ) + } + rmapping = { + col_id: guid.generate_guid() + for col_id in itertools.chain( + right.column_names, right._hidden_ordering_column_names + ) + } + + def get_column_left(col_id): + return lmapping[col_id] + + def get_column_right(col_id): + return rmapping[col_id] + left_table = left._to_ibis_expr( ordering_mode="unordered", expose_hidden_cols=True, + col_id_overrides=lmapping, ) right_table = right._to_ibis_expr( ordering_mode="unordered", expose_hidden_cols=True, + col_id_overrides=rmapping, ) join_conditions = [ - value_to_join_key(left_table[left_index]) - == value_to_join_key(right_table[right_index]) + value_to_join_key(left_table[lmapping[left_index]]) + == value_to_join_key(right_table[rmapping[right_index]]) for left_index, right_index in zip(left_column_ids, right_column_ids) ] @@ -145,38 +167,6 @@ def join_by_column( rname="{name}_y", ) - def get_column_left(key: str) -> str: - if ( - how == "inner" - and key in left_column_ids - and key in combined_table.columns - ): - # Ibis doesn't rename the column if the values are guaranteed - # to be equal on left and right (because they're part of an - # inner join condition). See: - # https://github.com/ibis-project/ibis/pull/4651 - pass - elif key in right_table.columns: - key = f"{key}_x" - - return key - - def get_column_right(key: str) -> str: - if ( - how == "inner" - and key in right_column_ids - and key in combined_table.columns - ): - # Ibis doesn't rename the column if the values are guaranteed - # to be equal on left and right (because they're part of an - # inner join condition). See: - # https://github.com/ibis-project/ibis/pull/4651 - pass - elif key in left_table.columns: - key = f"{key}_y" - - return key - # Preserve ordering accross joins. ordering = join_orderings( left._ordering, @@ -245,20 +235,14 @@ def get_join_cols( join_key_cols: list[ibis_types.Value] = [] for left_col, right_col in zip(left_join_cols, right_join_cols): if not coalesce_join_keys: - join_key_cols.append( - left_col.name(bigframes.core.guid.generate_guid(prefix="index_")) - ) - join_key_cols.append( - right_col.name(bigframes.core.guid.generate_guid(prefix="index_")) - ) + join_key_cols.append(left_col.name(guid.generate_guid(prefix="index_"))) + join_key_cols.append(right_col.name(guid.generate_guid(prefix="index_"))) else: if how == "left" or how == "inner": - join_key_cols.append( - left_col.name(bigframes.core.guid.generate_guid(prefix="index_")) - ) + join_key_cols.append(left_col.name(guid.generate_guid(prefix="index_"))) elif how == "right": join_key_cols.append( - right_col.name(bigframes.core.guid.generate_guid(prefix="index_")) + right_col.name(guid.generate_guid(prefix="index_")) ) elif how == "outer": # The left index and the right index might contain null values, for @@ -269,16 +253,14 @@ def get_join_cols( # Don't need to coalesce if they are exactly the same column. if left_col.name("index").equals(right_col.name("index")): join_key_cols.append( - left_col.name( - bigframes.core.guid.generate_guid(prefix="index_") - ) + left_col.name(guid.generate_guid(prefix="index_")) ) else: join_key_cols.append( ibis.coalesce( left_col, right_col, - ).name(bigframes.core.guid.generate_guid(prefix="index_")) + ).name(guid.generate_guid(prefix="index_")) ) else: raise ValueError( From 0e0493f6099e4084d1978afb3f60fb3e0a872379 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:42:06 -0500 Subject: [PATCH 044/921] chore(main): release 0.5.0 (#35) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de2edcf31e9..e4b2bff3c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,54 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.5.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.4.0...v0.5.0) (2023-09-28) + + +### Features + +* Add `DataFrame.kurtosis` / `DF.kurt` method ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Add `DataFrame.rolling` and `DataFrame.expanding` methods ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Add `items`, `apply` methods to `DataFrame`. ([#43](https://github.com/googleapis/python-bigquery-dataframes/issues/43)) ([3adc1b3](https://github.com/googleapis/python-bigquery-dataframes/commit/3adc1b3aa3e2b218d4fa5debdaa4298276bdf801)) +* Add axis param to simple df aggregations ([#52](https://github.com/googleapis/python-bigquery-dataframes/issues/52)) ([9cf9972](https://github.com/googleapis/python-bigquery-dataframes/commit/9cf99721ed83704e6ee28b15c699326c431eb252)) +* Add index `dtype`, `astype`, `drop`, `fillna`, aggregate attributes. ([#38](https://github.com/googleapis/python-bigquery-dataframes/issues/38)) ([1a254a4](https://github.com/googleapis/python-bigquery-dataframes/commit/1a254a496633957b9506dd8392dcc6fd10762201)) +* Add ml.preprocessing.LabelEncoder ([#50](https://github.com/googleapis/python-bigquery-dataframes/issues/50)) ([2510461](https://github.com/googleapis/python-bigquery-dataframes/commit/25104610e5ffe526315923946533a66713c1d155)) +* Add ml.preprocessing.MaxAbsScaler ([#56](https://github.com/googleapis/python-bigquery-dataframes/issues/56)) ([14b262b](https://github.com/googleapis/python-bigquery-dataframes/commit/14b262bde2bb86093bf4df63862e369c5a84b0ad)) +* Add ml.preprocessing.MinMaxScaler ([#64](https://github.com/googleapis/python-bigquery-dataframes/issues/64)) ([392113b](https://github.com/googleapis/python-bigquery-dataframes/commit/392113b70d6a8c407accbb6684d75b31261e3741)) +* Add more index methods ([#54](https://github.com/googleapis/python-bigquery-dataframes/issues/54)) ([a6e32aa](https://github.com/googleapis/python-bigquery-dataframes/commit/a6e32aa875370063c48ce7922c2aa369a770bd30)) +* Support `calculate_p_values` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `class_weights="balanced"` in `LogisticRegression` model ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `df[column_name] = df_only_one_column` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `early_stop` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `enable_global_explain` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `l2_reg` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `learn_rate_strategy` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `ls_init_learn_rate` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `max_iterations` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `min_rel_progress` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support `optimize_strategy` parameter in `bigframes.ml.linear_model.LinearRegression` ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) +* Support casting string to integer or float ([#59](https://github.com/googleapis/python-bigquery-dataframes/issues/59)) ([3502f83](https://github.com/googleapis/python-bigquery-dataframes/commit/3502f835b35c437933430698e7a1c9badaddcb99)) + + +### Bug Fixes + +* Fix header skipping logic in `read_csv` ([#49](https://github.com/googleapis/python-bigquery-dataframes/issues/49)) ([d56258c](https://github.com/googleapis/python-bigquery-dataframes/commit/d56258cbfcda168cb9e437a021e282818d622d6a)) +* Generate unique ids on join to avoid id collisions ([#65](https://github.com/googleapis/python-bigquery-dataframes/issues/65)) ([7ab65e8](https://github.com/googleapis/python-bigquery-dataframes/commit/7ab65e88deb0080e9c36c2709f8a5385ccaf8cf2)) +* LabelEncoder params consistent with Sklearn ([#60](https://github.com/googleapis/python-bigquery-dataframes/issues/60)) ([632caec](https://github.com/googleapis/python-bigquery-dataframes/commit/632caec420a7e23188f01b96a00c354d205da74e)) +* Loosen filter items tests to accomodate shifting pandas impl ([#41](https://github.com/googleapis/python-bigquery-dataframes/issues/41)) ([edabdbb](https://github.com/googleapis/python-bigquery-dataframes/commit/edabdbb131150707ea9211292cacbb60b8d076dd)) + + +### Performance Improvements + +* Add ability to cache dataframe and series to session table ([#51](https://github.com/googleapis/python-bigquery-dataframes/issues/51)) ([416d7cb](https://github.com/googleapis/python-bigquery-dataframes/commit/416d7cb9b560d7e33dcc0227f03a00d43f55ba0d)) +* Inline small `Series` and `DataFrames` in query text ([#45](https://github.com/googleapis/python-bigquery-dataframes/issues/45)) ([5e199ec](https://github.com/googleapis/python-bigquery-dataframes/commit/5e199ecf1ecf13a68a2ed0dd4464afd9db977ab1)) +* Reimplement unpivot to use cross join rather than union ([#47](https://github.com/googleapis/python-bigquery-dataframes/issues/47)) ([f9a93ce](https://github.com/googleapis/python-bigquery-dataframes/commit/f9a93ce71d053aa17b1e3a2946c90e0227076184)) +* Simplify join order to use multiple order keys instead of string. ([#36](https://github.com/googleapis/python-bigquery-dataframes/issues/36)) ([5056da6](https://github.com/googleapis/python-bigquery-dataframes/commit/5056da6b385dbcfc179d2bcbb6549fa539428cda)) + + +### Documentation + +* Link to Remote Functions code samples from README and API reference ([c1900c2](https://github.com/googleapis/python-bigquery-dataframes/commit/c1900c29a44199d5d8d036d6d842b4f00448fa79)) + ## [0.4.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.3.2...v0.4.0) (2023-09-16) diff --git a/bigframes/version.py b/bigframes/version.py index 65b984a0d73..ad3c3082c52 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.0" +__version__ = "0.5.0" From bc7be7f3529d39e647913824ca8bb86446da941c Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 28 Sep 2023 15:34:21 -0500 Subject: [PATCH 045/921] chore: remove Git-on-Borg check from config (#69) * chore: remove Git-on-Borg check from config Continuous jobs are incorrectly identifying themselves as Git-on-Borg jobs and using the wrong job path. * update job configs too --- .kokoro/build.sh | 6 +----- .kokoro/continuous/common.cfg | 2 +- .kokoro/continuous/nightly.cfg | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index f91c541c6c9..a0fa4bc787b 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -15,11 +15,7 @@ set -eo pipefail -if [[ -z "${KOKORO_GOB_COMMIT}" ]]; then - PROJECT_SCM="github/python-bigquery-dataframes" -else - PROJECT_SCM="git/bigframes" -fi +PROJECT_SCM="github/python-bigquery-dataframes" if [[ -z "${PROJECT_ROOT:-}" ]]; then PROJECT_ROOT="${KOKORO_ARTIFACTS_DIR}/${PROJECT_SCM}" diff --git a/.kokoro/continuous/common.cfg b/.kokoro/continuous/common.cfg index 5d40578ac79..97e0651aa92 100644 --- a/.kokoro/continuous/common.cfg +++ b/.kokoro/continuous/common.cfg @@ -7,4 +7,4 @@ action { } } -build_file: "bigframes/.kokoro/build.sh" +build_file: "python-bigquery-dataframes/.kokoro/build.sh" diff --git a/.kokoro/continuous/nightly.cfg b/.kokoro/continuous/nightly.cfg index 63c3f51d053..2b7111664f7 100644 --- a/.kokoro/continuous/nightly.cfg +++ b/.kokoro/continuous/nightly.cfg @@ -1,3 +1,3 @@ # Format: //devtools/kokoro/config/proto/build.proto -build_file: "bigframes/.kokoro/release-nightly.sh" +build_file: "python-bigquery-dataframes/.kokoro/release-nightly.sh" From bf050cf475ad8a9e3e0ca3f896ddaf96dbe13ae3 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 28 Sep 2023 15:51:25 -0700 Subject: [PATCH 046/921] feat: add update and align methods to dataframe (#57) * feat: add update and align methods to dataframe --- bigframes/core/block_transforms.py | 72 ++++++++++++ bigframes/dataframe.py | 108 +++++++++++------- tests/system/small/test_dataframe.py | 76 +++++++++++- .../bigframes_vendored/pandas/core/frame.py | 62 ++++++++++ 4 files changed, 271 insertions(+), 47 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index d22112417cc..6e99a7c7740 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -504,3 +504,75 @@ def _kurt_from_moments_and_count( kurt_id, na_cond_id, ops.partial_arg3(ops.where_op, None) ) return block, kurt_id + + +def align( + left_block: blocks.Block, + right_block: blocks.Block, + join: str = "outer", + axis: typing.Union[str, int, None] = None, +) -> typing.Tuple[blocks.Block, blocks.Block]: + axis_n = core.utils.get_axis_number(axis) if axis is not None else None + # Must align columns first as other way will likely create extra joins + if (axis_n is None) or axis_n == 1: + left_block, right_block = align_columns(left_block, right_block, join=join) + if (axis_n is None) or axis_n == 0: + left_block, right_block = align_rows(left_block, right_block, join=join) + return left_block, right_block + + +def align_rows( + left_block: blocks.Block, + right_block: blocks.Block, + join: str = "outer", +): + joined_index, (get_column_left, get_column_right) = left_block.index.join( + right_block.index, how=join + ) + left_columns = [get_column_left(col) for col in left_block.value_columns] + right_columns = [get_column_right(col) for col in right_block.value_columns] + + left_block = joined_index._block.select_columns(left_columns) + right_block = joined_index._block.select_columns(right_columns) + return left_block, right_block + + +def align_columns( + left_block: blocks.Block, + right_block: blocks.Block, + join: str = "outer", +): + columns, lcol_indexer, rcol_indexer = left_block.column_labels.join( + right_block.column_labels, how=join, return_indexers=True + ) + column_indices = zip( + lcol_indexer if (lcol_indexer is not None) else range(len(columns)), + rcol_indexer if (rcol_indexer is not None) else range(len(columns)), + ) + left_column_ids = [] + right_column_ids = [] + + original_left_block = left_block + original_right_block = right_block + + for left_index, right_index in column_indices: + if left_index >= 0: + left_col_id = original_left_block.value_columns[left_index] + else: + dtype = right_block.dtypes[right_index] + left_block, left_col_id = left_block.create_constant( + None, dtype=dtype, label=original_right_block.column_labels[right_index] + ) + left_column_ids.append(left_col_id) + + if right_index >= 0: + right_col_id = original_right_block.value_columns[right_index] + else: + dtype = original_left_block.dtypes[left_index] + right_block, right_col_id = right_block.create_constant( + None, dtype=dtype, label=left_block.column_labels[left_index] + ) + right_column_ids.append(right_col_id) + left_final = left_block.select_columns(left_column_ids) + right_final = right_block.select_columns(right_column_ids) + return left_final, right_final diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 113355589b1..828d2df64ef 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -745,6 +745,55 @@ def rpow( __rpow__ = rpow + def align( + self, + other: typing.Union[DataFrame, bigframes.series.Series], + join: str = "outer", + axis: typing.Union[str, int, None] = None, + ) -> typing.Tuple[ + typing.Union[DataFrame, bigframes.series.Series], + typing.Union[DataFrame, bigframes.series.Series], + ]: + axis_n = utils.get_axis_number(axis) if axis else None + if axis_n == 1 and isinstance(other, bigframes.series.Series): + raise NotImplementedError( + f"align with series and axis=1 not supported. {constants.FEEDBACK_LINK}" + ) + left_block, right_block = block_ops.align( + self._block, other._block, join=join, axis=axis + ) + return DataFrame(left_block), other.__class__(right_block) + + def update(self, other, join: str = "left", overwrite=True, filter_func=None): + other = other if isinstance(other, DataFrame) else DataFrame(other) + if join != "left": + raise ValueError("Only 'left' join supported for update") + + if filter_func is not None: # Will always take other if possible + + def update_func( + left: bigframes.series.Series, right: bigframes.series.Series + ) -> bigframes.series.Series: + return left.mask(right.notna() & filter_func(left), right) + + elif overwrite: + + def update_func( + left: bigframes.series.Series, right: bigframes.series.Series + ) -> bigframes.series.Series: + return left.mask(right.notna(), right) + + else: + + def update_func( + left: bigframes.series.Series, right: bigframes.series.Series + ) -> bigframes.series.Series: + return left.mask(left.isna(), right) + + result = self.combine(other, update_func, how=join) + + self._set_block(result._block) + def combine( self, other: DataFrame, @@ -753,56 +802,31 @@ def combine( ], fill_value=None, overwrite: bool = True, + *, + how: str = "outer", ) -> DataFrame: - # Join rows - joined_index, (get_column_left, get_column_right) = self._block.index.join( - other._block.index, how="outer" - ) - columns, lcol_indexer, rcol_indexer = self.columns.join( - other.columns, how="outer", return_indexers=True - ) + l_aligned, r_aligned = block_ops.align(self._block, other._block, join=how) - column_indices = zip( - lcol_indexer if (lcol_indexer is not None) else range(len(columns)), - rcol_indexer if (lcol_indexer is not None) else range(len(columns)), + other_missing_labels = self._block.column_labels.difference( + other._block.column_labels ) - block = joined_index._block + l_frame = DataFrame(l_aligned) + r_frame = DataFrame(r_aligned) results = [] - for left_index, right_index in column_indices: - if left_index >= 0 and right_index >= 0: # -1 indices indicate missing - left_col_id = get_column_left(self._block.value_columns[left_index]) - right_col_id = get_column_right(other._block.value_columns[right_index]) - left_series = bigframes.series.Series(block.select_column(left_col_id)) - right_series = bigframes.series.Series( - block.select_column(right_col_id) - ) + for (label, lseries), (_, rseries) in zip(l_frame.items(), r_frame.items()): + if not ((label in other_missing_labels) and not overwrite): if fill_value is not None: - left_series = left_series.fillna(fill_value) - right_series = right_series.fillna(fill_value) - results.append(func(left_series, right_series)) - elif left_index >= 0: - # Does not exist in other - if overwrite: - dtype = self.dtypes[left_index] - block, null_col_id = block.create_constant(None, dtype=dtype) - result = bigframes.series.Series(block.select_column(null_col_id)) - results.append(result) + result = func( + lseries.fillna(fill_value), rseries.fillna(fill_value) + ) else: - left_col_id = get_column_left(self._block.value_columns[left_index]) - result = bigframes.series.Series(block.select_column(left_col_id)) - if fill_value is not None: - result = result.fillna(fill_value) - results.append(result) - elif right_index >= 0: - right_col_id = get_column_right(other._block.value_columns[right_index]) - result = bigframes.series.Series(block.select_column(right_col_id)) - if fill_value is not None: - result = result.fillna(fill_value) - results.append(result) + result = func(lseries, rseries) else: - # Should not be possible - raise ValueError("No right or left index.") + result = ( + lseries.fillna(fill_value) if fill_value is not None else lseries + ) + results.append(result) if all([isinstance(val, bigframes.series.Series) for val in results]): import bigframes.core.reshape as rs diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index adf17848ee6..ba76c4b0d3e 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1211,6 +1211,77 @@ def test_combine( pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) +@pytest.mark.parametrize( + ("overwrite", "filter_func"), + [ + (True, None), + (False, None), + (True, lambda x: x.isna() | (x % 2 == 0)), + ], + ids=[ + "default", + "overwritefalse", + "customfilter", + ], +) +def test_df_update(overwrite, filter_func): + if pd.__version__.startswith("1."): + pytest.skip("dtype handled differently in pandas 1.x.") + index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") + index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") + pd_df1 = pandas.DataFrame( + {"a": [1, None, 3, 4], "b": [5, 6, None, 8]}, dtype="Int64", index=index1 + ) + pd_df2 = pandas.DataFrame( + {"a": [None, 20, 30, 40], "c": [90, None, 110, 120]}, + dtype="Int64", + index=index2, + ) + + bf_df1 = dataframe.DataFrame(pd_df1) + bf_df2 = dataframe.DataFrame(pd_df2) + + bf_df1.update(bf_df2, overwrite=overwrite, filter_func=filter_func) + pd_df1.update(pd_df2, overwrite=overwrite, filter_func=filter_func) + + pd.testing.assert_frame_equal(bf_df1.to_pandas(), pd_df1) + + +@pytest.mark.parametrize( + ("join", "axis"), + [ + ("outer", None), + ("outer", 0), + ("outer", 1), + ("left", 0), + ("right", 1), + ("inner", None), + ("inner", 1), + ], +) +def test_df_align(join, axis): + index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") + index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") + pd_df1 = pandas.DataFrame( + {"a": [1, None, 3, 4], "b": [5, 6, None, 8]}, dtype="Int64", index=index1 + ) + pd_df2 = pandas.DataFrame( + {"a": [None, 20, 30, 40], "c": [90, None, 110, 120]}, + dtype="Int64", + index=index2, + ) + + bf_df1 = dataframe.DataFrame(pd_df1) + bf_df2 = dataframe.DataFrame(pd_df2) + + bf_result1, bf_result2 = bf_df1.align(bf_df2, join=join, axis=axis) + pd_result1, pd_result2 = pd_df1.align(pd_df2, join=join, axis=axis) + + # Don't check dtype as pandas does unnecessary float conversion + pd.testing.assert_frame_equal(bf_result1.to_pandas(), pd_result1, check_dtype=False) + pd.testing.assert_frame_equal(bf_result2.to_pandas(), pd_result2, check_dtype=False) + + def test_combine_first( scalars_df_index, scalars_df_2_index, @@ -1232,11 +1303,6 @@ def test_combine_first( pd_df_b.columns = ["b", "a", "d"] pd_result = pd_df_a.combine_first(pd_df_b) - print("pandas") - print(pd_result.to_string()) - print("bigframes") - print(bf_result.to_string()) - # Some dtype inconsistency for all-NULL columns pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 6ce11cd7e98..5cd9fe51637 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -503,6 +503,35 @@ def drop( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def align( + self, + other, + join="outer", + axis=None, + ) -> tuple: + """ + Align two objects on their axes with the specified join method. + + Join method is specified for each axis Index. + + Args: + other (DataFrame or Series): + join ({{'outer', 'inner', 'left', 'right'}}, default 'outer'): + Type of alignment to be performed. + left: use only keys from left frame, preserve key order. + right: use only keys from right frame, preserve key order. + outer: use union of keys from both frames, sort keys lexicographically. + inner: use intersection of keys from both frames, + preserve the order of the left keys. + + axis (allowed axis of the other object, default None): + Align on index (0), columns (1), or both (None). + + Returns: + tuple of (DataFrame, type of other): Aligned objects. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rename( self, *, @@ -1265,6 +1294,39 @@ def combine_first(self, other) -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def update( + self, other, join: str = "left", overwrite: bool = True, filter_func=None + ) -> DataFrame: + """ + Modify in place using non-NA values from another DataFrame. + + Aligns on indices. There is no return value. + + Args: + other (DataFrame, or object coercible into a DataFrame): + Should have at least one matching index/column label + with the original DataFrame. If a Series is passed, + its name attribute must be set, and that will be + used as the column name to align with the original DataFrame. + join ({'left'}, default 'left'): + Only left join is implemented, keeping the index and columns of the + original object. + overwrite (bool, default True): + How to handle non-NA values for overlapping keys: + True: overwrite original DataFrame's values + with values from `other`. + False: only update values that are NA in + the original DataFrame. + + filter_func (callable(1d-array) -> bool 1d-array, optional): + Can choose to replace values other than NA. Return True for values + that should be updated. + + Returns: + None: This method directly changes calling object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # Data reshaping From d8910d4d9335626d25891fb150d646bdb3527564 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:03:21 -0700 Subject: [PATCH 047/921] chore: add vertex sdkhand bigframes integration notebooks (#70) --- .../vertex_sdk/sdk2_bigframes_pytorch.ipynb | 723 +++++++++++++++++ .../vertex_sdk/sdk2_bigframes_sklearn.ipynb | 727 ++++++++++++++++++ .../sdk2_bigframes_tensorflow.ipynb | 646 ++++++++++++++++ noxfile.py | 3 + 4 files changed, 2099 insertions(+) create mode 100644 notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb create mode 100644 notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb create mode 100644 notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb diff --git a/notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb b/notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb new file mode 100644 index 00000000000..598d958f0c3 --- /dev/null +++ b/notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb @@ -0,0 +1,723 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Train a pytorch model with Vertex AI SDK 2.0 and Bigframes\n", + "\n", + "\n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"VertexOpen in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial demonstrates how to train a pytorch model using Vertex AI local-to-remote training with Vertex AI SDK 2.0 and BigQuery Bigframes as the data source.\n", + "\n", + "Learn more about [bigframes](https://cloud.google.com/bigquery/docs/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn to use `Vertex AI SDK 2.0` with Bigframes as input data source.\n", + "\n", + "\n", + "This tutorial uses the following Google Cloud ML services:\n", + "\n", + "- `Vertex AI Training`\n", + "- `Vertex AI Remote Training`\n", + "\n", + "\n", + "The steps performed include:\n", + "\n", + "- Initialize a dataframe from a BigQuery table and split the dataset\n", + "- Perform transformations as a Vertex AI remote training.\n", + "- Train the model remotely and evaluate the model locally\n", + "\n", + "**Local-to-remote training**\n", + "\n", + "```\n", + "import vertexai\n", + "from my_module import MyModelClass\n", + "\n", + "vertexai.preview.init(remote=True, project=\"my-project\", location=\"my-location\", staging_bucket=\"gs://my-bucket\")\n", + "\n", + "# Wrap the model class with `vertex_ai.preview.remote`\n", + "MyModelClass = vertexai.preview.remote(MyModelClass)\n", + "\n", + "# Instantiate the class\n", + "model = MyModelClass(...)\n", + "\n", + "# Optional set remote config\n", + "model.fit.vertex.remote_config.display_name = \"MyModelClass-remote-training\"\n", + "model.fit.vertex.remote_config.staging_bucket = \"gs://my-bucket\"\n", + "\n", + "# This `fit` call will be executed remotely\n", + "model.fit(...)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the IRIS dataset, which predicts the iris species." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* BigQuery\n", + "* Cloud Storage\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing),\n", + "[BigQuery pricing](https://cloud.google.com/bigquery/pricing),\n", + "and [Cloud Storage pricing](https://cloud.google.com/storage/pricing), \n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages required to execute this notebook. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "# Install the packages\n", + "! pip3 install --upgrade --quiet google-cloud-aiplatform[preview]\n", + "! pip3 install --upgrade --quiet bigframes\n", + "! pip3 install --upgrade --quiet torch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "58707a750154" + }, + "source": [ + "### Colab only: Uncomment the following cell to restart the kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Region\n", + "\n", + "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "region" + }, + "outputs": [], + "source": [ + "REGION = \"us-central1\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**1. Vertex AI Workbench**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**3. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f6b2ccc891ed" + }, + "source": [ + "**4. Service account or other**\n", + "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zgPO1eR3CYjk" + }, + "source": [ + "### Create a Cloud Storage bucket\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MzGDU7TWdts_" + }, + "outputs": [], + "source": [ + "BUCKET_URI = f\"gs://your-bucket-name-{PROJECT_ID}-unique\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-EcIXiGsCePi" + }, + "source": [ + "**Only if your bucket doesn't already exist**: Run the following cell to create your Cloud Storage bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NIq7R4HZCfIc" + }, + "outputs": [], + "source": [ + "! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries and define constants" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "import torch\n", + "import vertexai\n", + "from vertexai.preview import VertexModel\n", + "\n", + "bf.options.bigquery.location = \"us\" # Dataset is in 'us' not 'us-central1'\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "\n", + "from bigframes.ml.model_selection import \\\n", + " train_test_split as bf_train_test_split" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "## Initialize Vertex AI SDK for Python\n", + "\n", + "Initialize the Vertex AI SDK for Python for your project and corresponding bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "outputs": [], + "source": [ + "vertexai.init(\n", + " project=PROJECT_ID,\n", + " location=REGION,\n", + " staging_bucket=BUCKET_URI,\n", + ")\n", + "\n", + "REMOTE_JOB_NAME = \"sdk2-bigframes-pytorch\"\n", + "REMOTE_JOB_BUCKET = f\"{BUCKET_URI}/{REMOTE_JOB_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "105334524e96" + }, + "source": [ + "## Prepare the dataset\n", + "\n", + "Now load the Iris dataset and split the data into train and test sets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b44cdc4e03f1" + }, + "outputs": [], + "source": [ + "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.iris\")\n", + "\n", + "species_categories = {\n", + " \"versicolor\": 0,\n", + " \"virginica\": 1,\n", + " \"setosa\": 2,\n", + "}\n", + "df[\"species\"] = df[\"species\"].map(species_categories)\n", + "\n", + "# Assign an index column name\n", + "index_col = \"index\"\n", + "df.index.name = index_col" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9cb8616b1997" + }, + "outputs": [], + "source": [ + "feature_columns = df[[\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]]\n", + "label_columns = df[[\"species\"]]\n", + "train_X, test_X, train_y, test_y = bf_train_test_split(\n", + " feature_columns, label_columns, test_size=0.2\n", + ")\n", + "\n", + "print(\"X_train size: \", train_X.size)\n", + "print(\"X_test size: \", test_X.size)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "23fe7b734b08" + }, + "outputs": [], + "source": [ + "# Switch to remote mode for training\n", + "vertexai.preview.init(remote=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5904a0f1bb03" + }, + "source": [ + "## PyTorch remote training with CPU (Custom PyTorch model)\n", + "\n", + "First, train a PyTorch model as a remote training job:\n", + "\n", + "- Reinitialize Vertex AI for remote training.\n", + "- Set TorchLogisticRegression for the remote training job.\n", + "- Invoke TorchLogisticRegression locally which will launch the remote training job." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2a1b85195a17" + }, + "outputs": [], + "source": [ + "# define the custom model\n", + "class TorchLogisticRegression(VertexModel, torch.nn.Module):\n", + " def __init__(self, input_size: int, output_size: int):\n", + " torch.nn.Module.__init__(self)\n", + " VertexModel.__init__(self)\n", + " self.linear = torch.nn.Linear(input_size, output_size)\n", + " self.softmax = torch.nn.Softmax(dim=1)\n", + "\n", + " def forward(self, x):\n", + " return self.softmax(self.linear(x))\n", + "\n", + " @vertexai.preview.developer.mark.train()\n", + " def train(self, X, y, num_epochs, lr):\n", + " X = X.to(torch.float32)\n", + " y = torch.flatten(y) # necessary to get 1D tensor\n", + " dataloader = torch.utils.data.DataLoader(\n", + " torch.utils.data.TensorDataset(X, y),\n", + " batch_size=10,\n", + " shuffle=True,\n", + " generator=torch.Generator(device=X.device),\n", + " )\n", + "\n", + " criterion = torch.nn.CrossEntropyLoss()\n", + " optimizer = torch.optim.SGD(self.parameters(), lr=lr)\n", + "\n", + " for t in range(num_epochs):\n", + " for batch, (X, y) in enumerate(dataloader):\n", + " optimizer.zero_grad()\n", + " pred = self(X)\n", + " loss = criterion(pred, y)\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " @vertexai.preview.developer.mark.predict()\n", + " def predict(self, X):\n", + " X = torch.tensor(X).to(torch.float32)\n", + " with torch.no_grad():\n", + " pred = torch.argmax(self(X), dim=1)\n", + " return pred" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4e35593f520a" + }, + "outputs": [], + "source": [ + "# Switch to remote mode for training\n", + "vertexai.preview.init(remote=True)\n", + "\n", + "# Instantiate model\n", + "model = TorchLogisticRegression(4, 3)\n", + "\n", + "# Set training config\n", + "model.train.vertex.remote_config.custom_commands = [\n", + " \"pip install torchdata\",\n", + " \"pip install torcharrow\",\n", + "]\n", + "model.train.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-torch-model\"\n", + "model.train.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", + "\n", + "# Train model on Vertex\n", + "model.train(train_X, train_y, num_epochs=200, lr=0.05)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "edf4d0708f02" + }, + "source": [ + "## Remote prediction\n", + "\n", + "Obtain predictions from the trained model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "42dfbff0ca15" + }, + "outputs": [], + "source": [ + "vertexai.preview.init(remote=True)\n", + "\n", + "# Set remote config\n", + "model.predict.vertex.remote_config.custom_commands = [\n", + " \"pip install torchdata\",\n", + " \"pip install torcharrow\",\n", + "]\n", + "model.predict.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-torch-predict\"\n", + "model.predict.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", + "\n", + "predictions = model.predict(test_X)\n", + "\n", + "print(f\"Remote predictions: {predictions}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4340ed8316cd" + }, + "source": [ + "## Local evaluation\n", + "\n", + "Evaluate model results locally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eb27a31cec6f" + }, + "outputs": [], + "source": [ + "# User must convert bigframes to torch tensor for local evaluation\n", + "train_X_tensor = torch.from_numpy(\n", + " train_X.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", + ")\n", + "train_y_tensor = torch.from_numpy(\n", + " train_y.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", + ")\n", + "\n", + "test_X_tensor = torch.from_numpy(\n", + " test_X.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", + ")\n", + "test_y_tensor = torch.from_numpy(\n", + " test_y.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7db44ad81389" + }, + "outputs": [], + "source": [ + "from sklearn.metrics import accuracy_score\n", + "\n", + "# Switch to local mode for evaluation\n", + "vertexai.preview.init(remote=False)\n", + "\n", + "# Evaluate model's accuracy score\n", + "print(\n", + " f\"Train accuracy: {accuracy_score(train_y_tensor, model.predict(train_X_tensor))}\"\n", + ")\n", + "\n", + "print(f\"Test accuracy: {accuracy_score(test_y_tensor, model.predict(test_X_tensor))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Delete Cloud Storage objects that were created\n", + "delete_bucket = False\n", + "if delete_bucket or os.getenv(\"IS_TESTING\"):\n", + " ! gsutil -m rm -r $BUCKET_URI" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "sdk2_bigframes_pytorch.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb b/notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb new file mode 100644 index 00000000000..021c0707535 --- /dev/null +++ b/notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb @@ -0,0 +1,727 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Train a scikit-learn model with Vertex AI SDK 2.0 and Bigframes\n", + "\n", + "\n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"VertexOpen in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial demonstrates how to train a scikit-learn model using Vertex AI local-to-remote training with Vertex AI SDK 2.0 and BigQuery Bigframes as the data source.\n", + "\n", + "Learn more about [bigframes](https://cloud.google.com/bigquery/docs/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn to use `Vertex AI SDK 2.0` with Bigframes as input data source.\n", + "\n", + "\n", + "This tutorial uses the following Google Cloud ML services:\n", + "\n", + "- `Vertex AI Training`\n", + "- `Vertex AI Remote Training`\n", + "\n", + "\n", + "The steps performed include:\n", + "\n", + "- Initialize a dataframe from a BigQuery table and split the dataset\n", + "- Perform transformations as a Vertex AI remote training.\n", + "- Train the model remotely and evaluate the model locally\n", + "\n", + "**Local-to-remote training**\n", + "\n", + "```\n", + "import vertexai\n", + "from my_module import MyModelClass\n", + "\n", + "vertexai.preview.init(remote=True, project=\"my-project\", location=\"my-location\", staging_bucket=\"gs://my-bucket\")\n", + "\n", + "# Wrap the model class with `vertex_ai.preview.remote`\n", + "MyModelClass = vertexai.preview.remote(MyModelClass)\n", + "\n", + "# Instantiate the class\n", + "model = MyModelClass(...)\n", + "\n", + "# Optional set remote config\n", + "model.fit.vertex.remote_config.display_name = \"MyModelClass-remote-training\"\n", + "model.fit.vertex.remote_config.staging_bucket = \"gs://my-bucket\"\n", + "\n", + "# This `fit` call will be executed remotely\n", + "model.fit(...)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the IRIS dataset, which predicts the iris species." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* BigQuery\n", + "* Cloud Storage\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing),\n", + "[BigQuery pricing](https://cloud.google.com/bigquery/pricing),\n", + "and [Cloud Storage pricing](https://cloud.google.com/storage/pricing), \n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages required to execute this notebook. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "# Install the packages\n", + "! pip3 install --upgrade --quiet google-cloud-aiplatform[preview]\n", + "! pip3 install --upgrade --quiet bigframes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "58707a750154" + }, + "source": [ + "### Colab only: Uncomment the following cell to restart the kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Region\n", + "\n", + "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "region" + }, + "outputs": [], + "source": [ + "REGION = \"us-central1\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**1. Vertex AI Workbench**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**3. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f6b2ccc891ed" + }, + "source": [ + "**4. Service account or other**\n", + "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zgPO1eR3CYjk" + }, + "source": [ + "### Create a Cloud Storage bucket\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MzGDU7TWdts_" + }, + "outputs": [], + "source": [ + "BUCKET_URI = f\"gs://your-bucket-name-{PROJECT_ID}-unique\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-EcIXiGsCePi" + }, + "source": [ + "**Only if your bucket doesn't already exist**: Run the following cell to create your Cloud Storage bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NIq7R4HZCfIc" + }, + "outputs": [], + "source": [ + "! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries and define constants" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "import vertexai\n", + "\n", + "bf.options.bigquery.location = \"us\" # Dataset is in 'us' not 'us-central1'\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "\n", + "from bigframes.ml.model_selection import \\\n", + " train_test_split as bf_train_test_split\n", + "\n", + "REMOTE_JOB_NAME = \"sdk2-bigframes-sklearn\"\n", + "REMOTE_JOB_BUCKET = f\"{BUCKET_URI}/{REMOTE_JOB_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "## Initialize Vertex AI SDK for Python\n", + "\n", + "Initialize the Vertex AI SDK for Python for your project and corresponding bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "outputs": [], + "source": [ + "vertexai.init(\n", + " project=PROJECT_ID,\n", + " location=REGION,\n", + " staging_bucket=BUCKET_URI,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "105334524e96" + }, + "source": [ + "## Prepare the dataset\n", + "\n", + "Now load the Iris dataset and split the data into train and test sets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b44cdc4e03f1" + }, + "outputs": [], + "source": [ + "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.iris\")\n", + "\n", + "species_categories = {\n", + " \"versicolor\": 0,\n", + " \"virginica\": 1,\n", + " \"setosa\": 2,\n", + "}\n", + "df[\"species\"] = df[\"species\"].map(species_categories)\n", + "\n", + "# Assign an index column name\n", + "index_col = \"index\"\n", + "df.index.name = index_col" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9cb8616b1997" + }, + "outputs": [], + "source": [ + "feature_columns = df[[\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]]\n", + "label_columns = df[[\"species\"]]\n", + "train_X, test_X, train_y, test_y = bf_train_test_split(\n", + " feature_columns, label_columns, test_size=0.2\n", + ")\n", + "\n", + "print(\"X_train size: \", train_X.size)\n", + "print(\"X_test size: \", test_X.size)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8306545fcc57" + }, + "source": [ + "## Feature transformation\n", + "\n", + "Next, you do feature transformations on the data using the Vertex AI remote training service.\n", + "\n", + "First, you re-initialize Vertex AI to enable remote training." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "55e701c31036" + }, + "outputs": [], + "source": [ + "# Switch to remote mode for training\n", + "vertexai.preview.init(remote=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4a0e9d59b273" + }, + "source": [ + "### Execute remote job for fit_transform() on training data\n", + "\n", + "Next, indicate that the `StandardScalar` class is to be executed remotely. Then set up the data transform and call the `fit_transform()` method is executed remotely." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "90333089d362" + }, + "outputs": [], + "source": [ + "from sklearn.preprocessing import StandardScaler\n", + "\n", + "# Wrap classes to enable Vertex remote execution\n", + "StandardScaler = vertexai.preview.remote(StandardScaler)\n", + "\n", + "# Instantiate transformer\n", + "transformer = StandardScaler()\n", + "\n", + "# Set training config\n", + "transformer.fit_transform.vertex.remote_config.display_name = (\n", + " f\"{REMOTE_JOB_NAME}-fit-transformer-bigframes\"\n", + ")\n", + "transformer.fit_transform.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", + "\n", + "# Execute transformer on Vertex (train_X is bigframes.dataframe.DataFrame, X_train is np.array)\n", + "X_train = transformer.fit_transform(train_X)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6bf95574c907" + }, + "source": [ + "### Remote transform on test data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "da6eea22a89a" + }, + "outputs": [], + "source": [ + "# Transform test dataset before calculate test score\n", + "transformer.transform.vertex.remote_config.display_name = (\n", + " REMOTE_JOB_NAME + \"-transformer\"\n", + ")\n", + "transformer.transform.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", + "\n", + "# Execute transformer on Vertex (test_X is bigframes.dataframe.DataFrame, X_test is np.array)\n", + "X_test = transformer.transform(test_X)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ddf906c886e4" + }, + "source": [ + "## Remote training\n", + "\n", + "First, train the scikit-learn model as a remote training job:\n", + "\n", + "- Set LogisticRegression for the remote training job.\n", + "- Invoke LogisticRegression locally which will launch the remote training job." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c7b0116fa60c" + }, + "outputs": [], + "source": [ + "from sklearn.linear_model import LogisticRegression\n", + "\n", + "# Wrap classes to enable Vertex remote execution\n", + "LogisticRegression = vertexai.preview.remote(LogisticRegression)\n", + "\n", + "# Instantiate model, warm_start=True for uptraining\n", + "model = LogisticRegression(warm_start=True)\n", + "\n", + "# Set training config\n", + "model.fit.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-sklearn-model\"\n", + "model.fit.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", + "\n", + "# Train model on Vertex\n", + "model.fit(train_X, train_y)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ffe1d5903bcb" + }, + "source": [ + "## Remote prediction\n", + "\n", + "Obtain predictions from the trained model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d00ce35920fa" + }, + "outputs": [], + "source": [ + "# Remote evaluation\n", + "vertexai.preview.init(remote=True)\n", + "\n", + "# Evaluate model's accuracy score\n", + "predictions = model.predict(test_X)\n", + "\n", + "print(f\"Remote predictions: {predictions}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a8cd6cbd4403" + }, + "source": [ + "## Local evaluation\n", + "\n", + "Score model results locally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dc105dafdfb9" + }, + "outputs": [], + "source": [ + "# User must convert bigframes to pandas dataframe for local evaluation\n", + "train_X_pd = train_X.to_pandas().reset_index(drop=True)\n", + "train_y_pd = train_y.to_pandas().reset_index(drop=True)\n", + "\n", + "test_X_pd = test_X.to_pandas().reset_index(drop=True)\n", + "test_y_pd = test_y.to_pandas().reset_index(drop=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "25fec549de69" + }, + "outputs": [], + "source": [ + "# Switch to local mode for testing\n", + "vertexai.preview.init(remote=False)\n", + "\n", + "# Evaluate model's accuracy score\n", + "print(f\"Train accuracy: {model.score(train_X_pd, train_y_pd)}\")\n", + "\n", + "print(f\"Test accuracy: {model.score(test_X_pd, test_y_pd)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Delete Cloud Storage objects that were created\n", + "delete_bucket = False\n", + "if delete_bucket or os.getenv(\"IS_TESTING\"):\n", + " ! gsutil -m rm -r $BUCKET_URI" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "sdk2_bigframes_sklearn.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb b/notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb new file mode 100644 index 00000000000..e6843b66b57 --- /dev/null +++ b/notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb @@ -0,0 +1,646 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Train a Tensorflow Keras model with Vertex AI SDK 2.0 and Bigframes \n", + "\n", + "\n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"VertexOpen in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial demonstrates how to train a tensorflow keras model using Vertex AI local-to-remote training with Vertex AI SDK 2.0 and BigQuery Bigframes as the data source.\n", + "\n", + "Learn more about [bigframes](https://cloud.google.com/bigquery/docs/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn to use `Vertex AI SDK 2.0` with Bigframes as input data source.\n", + "\n", + "\n", + "This tutorial uses the following Google Cloud ML services:\n", + "\n", + "- `Vertex AI Training`\n", + "- `Vertex AI Remote Training`\n", + "\n", + "\n", + "The steps performed include:\n", + "\n", + "- Initialize a dataframe from a BigQuery table and split the dataset\n", + "- Perform transformations as a Vertex AI remote training.\n", + "- Train the model remotely and evaluate the model locally\n", + "\n", + "**Local-to-remote training**\n", + "\n", + "```\n", + "import vertexai\n", + "from my_module import MyModelClass\n", + "\n", + "vertexai.preview.init(remote=True, project=\"my-project\", location=\"my-location\", staging_bucket=\"gs://my-bucket\")\n", + "\n", + "# Wrap the model class with `vertex_ai.preview.remote`\n", + "MyModelClass = vertexai.preview.remote(MyModelClass)\n", + "\n", + "# Instantiate the class\n", + "model = MyModelClass(...)\n", + "\n", + "# Optional set remote config\n", + "model.fit.vertex.remote_config.display_name = \"MyModelClass-remote-training\"\n", + "model.fit.vertex.remote_config.staging_bucket = \"gs://my-bucket\"\n", + "\n", + "# This `fit` call will be executed remotely\n", + "model.fit(...)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the IRIS dataset, which predicts the iris species." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* BigQuery\n", + "* Cloud Storage\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing),\n", + "[BigQuery pricing](https://cloud.google.com/bigquery/pricing),\n", + "and [Cloud Storage pricing](https://cloud.google.com/storage/pricing), \n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages required to execute this notebook. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "# Install the packages\n", + "! pip3 install --upgrade --quiet google-cloud-aiplatform[preview]\n", + "! pip3 install --upgrade --quiet bigframes\n", + "! pip3 install --upgrade --quiet tensorflow==2.12.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "58707a750154" + }, + "source": [ + "### Colab only: Uncomment the following cell to restart the kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Region\n", + "\n", + "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "region" + }, + "outputs": [], + "source": [ + "REGION = \"us-central1\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**1. Vertex AI Workbench**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**3. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f6b2ccc891ed" + }, + "source": [ + "**4. Service account or other**\n", + "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zgPO1eR3CYjk" + }, + "source": [ + "### Create a Cloud Storage bucket\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MzGDU7TWdts_" + }, + "outputs": [], + "source": [ + "BUCKET_URI = f\"gs://your-bucket-name-{PROJECT_ID}-unique\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-EcIXiGsCePi" + }, + "source": [ + "**Only if your bucket doesn't already exist**: Run the following cell to create your Cloud Storage bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NIq7R4HZCfIc" + }, + "outputs": [], + "source": [ + "! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries and define constants" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "import tensorflow as tf\n", + "import vertexai\n", + "from tensorflow import keras\n", + "\n", + "bf.options.bigquery.location = \"us\" # Dataset is in 'us' not 'us-central1'\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "\n", + "from bigframes.ml.model_selection import \\\n", + " train_test_split as bf_train_test_split" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "## Initialize Vertex AI SDK for Python\n", + "\n", + "Initialize the Vertex AI SDK for Python for your project and corresponding bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "outputs": [], + "source": [ + "vertexai.init(\n", + " project=PROJECT_ID,\n", + " location=REGION,\n", + " staging_bucket=BUCKET_URI,\n", + ")\n", + "\n", + "REMOTE_JOB_NAME = \"sdk2-bigframes-tensorflow\"\n", + "REMOTE_JOB_BUCKET = f\"{BUCKET_URI}/{REMOTE_JOB_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "105334524e96" + }, + "source": [ + "## Prepare the dataset\n", + "\n", + "Now load the Iris dataset and split the data into train and test sets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "94576deccd8c" + }, + "outputs": [], + "source": [ + "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.iris\")\n", + "\n", + "species_categories = {\n", + " \"versicolor\": 0,\n", + " \"virginica\": 1,\n", + " \"setosa\": 2,\n", + "}\n", + "df[\"target\"] = df[\"species\"].map(species_categories)\n", + "df = df.drop(columns=[\"species\"])\n", + "\n", + "train, test = bf_train_test_split(df, test_size=0.2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cfcbce726efa" + }, + "source": [ + "## Remote training with GPU\n", + "\n", + "First, train a TensorFlow model as a remote training job:\n", + "\n", + "- Reinitialize Vertex AI for remote training.\n", + "- Instantiate the tensorflow keras model for the remote training job.\n", + "- Invoke the tensorflow keras model.fit() locally which will launch the remote training job." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fd865b0c4e8b" + }, + "outputs": [], + "source": [ + "# Switch to remote mode for training\n", + "vertexai.preview.init(remote=True)\n", + "\n", + "keras.Sequential = vertexai.preview.remote(keras.Sequential)\n", + "\n", + "# Instantiate model\n", + "model = keras.Sequential(\n", + " [keras.layers.Dense(5, input_shape=(4,)), keras.layers.Softmax()]\n", + ")\n", + "\n", + "# Specify optimizer and loss function\n", + "model.compile(optimizer=\"adam\", loss=\"mean_squared_error\")\n", + "\n", + "# Set training config\n", + "model.fit.vertex.remote_config.enable_cuda = True\n", + "model.fit.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-keras-model-gpu\"\n", + "model.fit.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", + "model.fit.vertex.remote_config.custom_commands = [\"pip install tensorflow-io==0.32.0\"]\n", + "\n", + "# Manually set compute resources this time\n", + "model.fit.vertex.remote_config.machine_type = \"n1-highmem-4\"\n", + "model.fit.vertex.remote_config.accelerator_type = \"NVIDIA_TESLA_K80\"\n", + "model.fit.vertex.remote_config.accelerator_count = 4\n", + "\n", + "# Train model on Vertex\n", + "model.fit(train, epochs=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f1af94ac1477" + }, + "source": [ + "## Remote prediction\n", + "\n", + "Obtain predictions from the trained model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1d75879948b5" + }, + "outputs": [], + "source": [ + "vertexai.preview.init(remote=True)\n", + "\n", + "# Set remote config\n", + "model.predict.vertex.remote_config.enable_cuda = False\n", + "model.predict.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-keras-predict-cpu\"\n", + "model.predict.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", + "model.predict.vertex.remote_config.custom_commands = [\n", + " \"pip install tensorflow-io==0.32.0\"\n", + "]\n", + "\n", + "predictions = model.predict(train)\n", + "\n", + "print(f\"Remote predictions: {predictions}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "798b77c95067" + }, + "source": [ + "## Local evaluation\n", + "\n", + "Evaluate model results locally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "88e734e30791" + }, + "outputs": [], + "source": [ + "# User must convert bigframes to pandas dataframe for local evaluation\n", + "feature_columns = [\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]\n", + "label_columns = [\"target\"]\n", + "\n", + "train_X_np = train[feature_columns].to_pandas().values.astype(float)\n", + "train_y_np = train[label_columns].to_pandas().values.astype(float)\n", + "train_ds = tf.data.Dataset.from_tensor_slices((train_X_np, train_y_np))\n", + "\n", + "test_X_np = test[feature_columns].to_pandas().values.astype(float)\n", + "test_y_np = test[label_columns].to_pandas().values.astype(float)\n", + "test_ds = tf.data.Dataset.from_tensor_slices((test_X_np, test_y_np))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cb8637f783ad" + }, + "outputs": [], + "source": [ + "# Switch to local mode for evaluation\n", + "vertexai.preview.init(remote=False)\n", + "\n", + "# Evaluate model's mean square errors\n", + "print(f\"Train loss: {model.evaluate(train_ds.batch(32))}\")\n", + "\n", + "print(f\"Test loss: {model.evaluate(test_ds.batch(32))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Delete Cloud Storage objects that were created\n", + "delete_bucket = False\n", + "if delete_bucket or os.getenv(\"IS_TESTING\"):\n", + " ! gsutil -m rm -r $BUCKET_URI" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "sdk2_bigframes_tensorflow.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/noxfile.py b/noxfile.py index 033bbfefe40..81ed9c2b2c4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -610,6 +610,9 @@ def notebook(session): "notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb", "notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb", "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", + "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", + "notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb", + "notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb", # The experimental notebooks imagine features that don't yet # exist or only exist as temporary prototypes. "notebooks/experimental/longer_ml_demo.ipynb", From 4a84714e2fb07f70c70c79f8b8da9fcb41096e33 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 29 Sep 2023 14:16:37 -0700 Subject: [PATCH 048/921] feat: add df.unstack (#63) --- bigframes/core/__init__.py | 113 +++++++++----- bigframes/core/blocks.py | 143 +++++++----------- bigframes/core/utils.py | 20 +++ bigframes/dataframe.py | 21 +++ tests/system/small/test_dataframe.py | 20 +++ tests/system/small/test_multiindex.py | 31 ++++ .../bigframes_vendored/pandas/core/frame.py | 15 ++ 7 files changed, 241 insertions(+), 122 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index c529f833510..5e0675fd134 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -963,10 +963,11 @@ def unpivot( ], *, passthrough_columns: typing.Sequence[str] = (), - index_col_id: str = "index", + index_col_ids: typing.Sequence[str] = ["index"], dtype: typing.Union[ bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] ] = pandas.Float64Dtype(), + how="left", ) -> ArrayValue: """ Unpivot ArrayValue columns. @@ -981,8 +982,11 @@ def unpivot( Returns: ArrayValue: The unpivoted ArrayValue """ - table = self._to_ibis_expr(ordering_mode="offset_col") + if how not in ("left", "right"): + raise ValueError("'how' must be 'left' or 'right'") + table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) row_n = len(row_labels) + hidden_col_ids = self._hidden_ordering_column_names.keys() if not all( len(source_columns) == row_n for _, source_columns in unpivot_columns ): @@ -992,33 +996,44 @@ def unpivot( unpivot_table = table.cross_join( ibis.memtable({unpivot_offset_id: range(row_n)}) ) - unpivot_offsets_value = ( - ( - (unpivot_table[ORDER_ID_COLUMN] * row_n) - + unpivot_table[unpivot_offset_id] - ) - .cast(ibis_dtypes.int64) - .name(ORDER_ID_COLUMN), - ) - # Use ibis memtable to infer type of rowlabels (if possible) # TODO: Allow caller to specify dtype - labels_ibis_type = ibis.memtable({"col": row_labels})["col"].type() - labels_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(labels_ibis_type) - cases = [ - ( - i, - bigframes.dtypes.literal_to_ibis_scalar( - row_labels[i], force_dtype=labels_dtype # type:ignore - ), - ) - for i in range(len(row_labels)) + if isinstance(row_labels[0], tuple): + labels_table = ibis.memtable(row_labels) + labels_ibis_types = [ + labels_table[col].type() for col in labels_table.columns + ] + else: + labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] + labels_dtypes = [ + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) + for ibis_type in labels_ibis_types ] - labels_value = ( - typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) - .cases(cases, default=None) # type:ignore - .name(index_col_id) - ) + + label_columns = [] + for label_part, (col_id, label_dtype) in enumerate( + zip(index_col_ids, labels_dtypes) + ): + # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels + labels_as_tuples = [ + label if isinstance(label, tuple) else (label,) for label in row_labels + ] + cases = [ + ( + i, + bigframes.dtypes.literal_to_ibis_scalar( + label_tuple[label_part], # type:ignore + force_dtype=label_dtype, # type:ignore + ), + ) + for i, label_tuple in enumerate(labels_as_tuples) + ] + labels_value = ( + typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) + .cases(cases, default=None) # type:ignore + .name(col_id) + ) + label_columns.append(labels_value) unpivot_values = [] for j in range(len(unpivot_columns)): @@ -1042,23 +1057,53 @@ def unpivot( unpivot_values.append(unpivot_value.name(result_col)) unpivot_table = unpivot_table.select( - passthrough_columns, labels_value, *unpivot_values, unpivot_offsets_value + passthrough_columns, + *label_columns, + *unpivot_values, + *hidden_col_ids, + unpivot_offset_id, ) + # Extend the original ordering using unpivot_offset_id + old_ordering = self._ordering + if how == "left": + new_ordering = ExpressionOrdering( + ordering_value_columns=[ + *old_ordering.ordering_value_columns, + OrderingColumnReference(unpivot_offset_id), + ], + total_ordering_columns=frozenset( + [*old_ordering.total_ordering_columns, unpivot_offset_id] + ), + ) + else: # how=="right" + new_ordering = ExpressionOrdering( + ordering_value_columns=[ + OrderingColumnReference(unpivot_offset_id), + *old_ordering.ordering_value_columns, + ], + total_ordering_columns=frozenset( + [*old_ordering.total_ordering_columns, unpivot_offset_id] + ), + ) value_columns = [ unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns ] passthrough_values = [unpivot_table[col] for col in passthrough_columns] + hidden_ordering_columns = [ + unpivot_table[unpivot_offset_id], + *[unpivot_table[hidden_col] for hidden_col in hidden_col_ids], + ] return ArrayValue( session=self._session, table=unpivot_table, - columns=[unpivot_table[index_col_id], *value_columns, *passthrough_values], - hidden_ordering_columns=[unpivot_table[ORDER_ID_COLUMN]], - ordering=ExpressionOrdering( - ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - ), + columns=[ + *[unpivot_table[col_id] for col_id in index_col_ids], + *value_columns, + *passthrough_values, + ], + hidden_ordering_columns=hidden_ordering_columns, + ordering=new_ordering, ) def assign(self, source_id: str, destination_id: str) -> ArrayValue: diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index b53c2212c10..863852c684c 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -838,7 +838,7 @@ def aggregate_all_and_stack( ] result_expr = self.expr.aggregate(aggregations, dropna=dropna).unpivot( row_labels=self.column_labels.to_list(), - index_col_id="index", + index_col_ids=["index"], unpivot_columns=[(value_col_id, self.value_columns)], dtype=dtype, ) @@ -849,7 +849,7 @@ def aggregate_all_and_stack( expr_with_offsets, offset_col = self.expr.promote_offsets() stacked_expr = expr_with_offsets.unpivot( row_labels=self.column_labels.to_list(), - index_col_id=guid.generate_guid(), + index_col_ids=[guid.generate_guid()], unpivot_columns=[(value_col_id, self.value_columns)], passthrough_columns=[*self.index_columns, offset_col], dtype=dtype, @@ -1041,7 +1041,7 @@ def summarize( expr = self.expr.aggregate(aggregations).unpivot( labels, unpivot_columns=columns, - index_col_id=label_col_id, + index_col_ids=[label_col_id], ) labels = self._get_labels_for_columns(column_ids) return Block(expr, column_labels=labels, index_columns=[label_col_id]) @@ -1225,116 +1225,83 @@ def pivot( return result_block.with_column_labels(column_index) - def stack(self): + def stack(self, how="left", dropna=True, sort=True, levels: int = 1): """Unpivot last column axis level into row axis""" - if isinstance(self.column_labels, pd.MultiIndex): - return self._stack_multi() - else: - return self._stack_mono() - - def _stack_mono(self): - if isinstance(self.column_labels, pd.MultiIndex): - raise ValueError("Expected single level index") - # These are the values that will be turned into rows - stack_values = self.column_labels.drop_duplicates().sort_values() - # Get matching columns - unpivot_columns: List[Tuple[str, List[str]]] = [] - dtypes: List[bigframes.dtypes.Dtype] = [] - col_id = guid.generate_guid("unpivot_") - dtype = None - input_columns: Sequence[Optional[str]] = [] - for uvalue in stack_values: - matching_ids = self.label_to_col_id.get(uvalue, []) - input_id = matching_ids[0] if len(matching_ids) > 0 else None - if input_id: - if dtype and dtype != self._column_type(input_id): - raise NotImplementedError( - "Cannot stack columns with non-matching dtypes." - ) - else: - dtype = self._column_type(input_id) - input_columns.append(input_id) - unpivot_columns.append((col_id, input_columns)) - if dtype: - dtypes.append(dtype or pd.Float64Dtype()) + col_labels, row_labels = utils.split_index(self.column_labels, levels=levels) + if dropna: + row_labels = row_labels.drop_duplicates() + if sort: + row_labels = row_labels.sort_values() - added_index_column = col_id = guid.generate_guid() - unpivot_expr = self._expr.unpivot( - row_labels=stack_values, - passthrough_columns=self.index_columns, - unpivot_columns=unpivot_columns, - index_col_id=added_index_column, - dtype=dtypes, - ) - block = Block( - unpivot_expr, - index_columns=[*self.index_columns, added_index_column], - column_labels=[None], - index_labels=[*self._index_labels, self.column_labels.names[-1]], - ) - return block - - def _stack_multi(self): - if not isinstance(self.column_labels, pd.MultiIndex): - raise ValueError("Expected multi-index") - - # These are the values that will be turned into rows - stack_values = ( - self.column_labels.get_level_values(-1).drop_duplicates().sort_values() - ) + row_label_tuples = utils.index_as_tuples(row_labels) - result_col_labels = ( - self.column_labels.droplevel(-1) - .drop_duplicates() - .sort_values() - .dropna(how="all") - ) + if col_labels is not None: + result_index = col_labels.drop_duplicates().sort_values().dropna(how="all") + result_col_labels = utils.index_as_tuples(result_index) + else: + result_index = pd.Index([None]) + result_col_labels = list([()]) # Get matching columns unpivot_columns: List[Tuple[str, List[str]]] = [] dtypes = [] for val in result_col_labels: col_id = guid.generate_guid("unpivot_") - dtype = None - input_columns: Sequence[Optional[str]] = [] - for uvalue in stack_values: - # Need to unpack if still a multi-index after dropping 1 level - label_to_match = ( - (val, uvalue) if result_col_labels.nlevels == 1 else (*val, uvalue) - ) - matching_ids = self.label_to_col_id.get(label_to_match, []) - input_id = matching_ids[0] if len(matching_ids) > 0 else None - if input_id: - if dtype and dtype != self._column_type(input_id): - raise NotImplementedError( - "Cannot stack columns with non-matching dtypes." - ) - else: - dtype = self._column_type(input_id) - input_columns.append(input_id) - # Input column i is the first one that + input_columns, dtype = self._create_stack_column(val, row_label_tuples) unpivot_columns.append((col_id, input_columns)) if dtype: dtypes.append(dtype or pd.Float64Dtype()) - added_index_column = col_id = guid.generate_guid() + added_index_columns = [guid.generate_guid() for _ in range(row_labels.nlevels)] unpivot_expr = self._expr.unpivot( - row_labels=stack_values, + row_labels=row_label_tuples, passthrough_columns=self.index_columns, unpivot_columns=unpivot_columns, - index_col_id=added_index_column, + index_col_ids=added_index_columns, dtype=dtypes, + how=how, ) + new_index_level_names = self.column_labels.names[-levels:] + if how == "left": + index_columns = [*self.index_columns, *added_index_columns] + index_labels = [*self._index_labels, *new_index_level_names] + else: + index_columns = [*added_index_columns, *self.index_columns] + index_labels = [*new_index_level_names, *self._index_labels] + block = Block( unpivot_expr, - index_columns=[*self.index_columns, added_index_column], - column_labels=result_col_labels, - index_labels=[*self._index_labels, self.column_labels.names[-1]], + index_columns=index_columns, + column_labels=result_index, + index_labels=index_labels, ) return block + def _create_stack_column( + self, col_label: typing.Tuple, stack_labels: typing.Sequence[typing.Tuple] + ): + dtype = None + input_columns: list[Optional[str]] = [] + for uvalue in stack_labels: + label_to_match = (*col_label, *uvalue) + label_to_match = ( + label_to_match[0] if len(label_to_match) == 1 else label_to_match + ) + matching_ids = self.label_to_col_id.get(label_to_match, []) + input_id = matching_ids[0] if len(matching_ids) > 0 else None + if input_id: + if dtype and dtype != self._column_type(input_id): + raise NotImplementedError( + "Cannot stack columns with non-matching dtypes." + ) + else: + dtype = self._column_type(input_id) + input_columns.append(input_id) + # Input column i is the first one that + return input_columns, dtype or pd.Float64Dtype() + def _column_type(self, col_id: str) -> bigframes.dtypes.Dtype: col_offset = self.value_columns.index(col_id) dtype = self.dtypes[col_offset] diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 75175690ce0..1a5d3ab37ca 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -49,6 +49,26 @@ def combine_indices(index1: pd.Index, index2: pd.Index) -> pd.MultiIndex: return multi_index +def index_as_tuples(index: pd.Index) -> typing.Sequence[typing.Tuple]: + if isinstance(index, pd.MultiIndex): + return [label for label in index] + else: + return [(label,) for label in index] + + +def split_index( + index: pd.Index, levels: int = 1 +) -> typing.Tuple[typing.Optional[pd.Index], pd.Index]: + nlevels = index.nlevels + remaining = nlevels - levels + if remaining > 0: + return index.droplevel(list(range(remaining, nlevels))), index.droplevel( + list(range(0, remaining)) + ) + else: + return (None, index) + + def get_standardized_ids( col_labels: Iterable[Hashable], idx_labels: Iterable[Hashable] = () ) -> tuple[list[str], list[str]]: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 828d2df64ef..ea06e28cdfc 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1706,6 +1706,27 @@ def stack(self): return bigframes.series.Series(result_block) return DataFrame(result_block) + def unstack(self): + block = self._block + # Special case, unstack with mono-index transpose into a series + if self.index.nlevels == 1: + block = block.stack( + how="right", dropna=False, sort=False, levels=self.columns.nlevels + ) + return bigframes.series.Series(block) + + # Pivot by last level of index + index_ids = block.index_columns + block = block.reset_index(drop=False) + block = block.set_index(index_ids[:-1]) + + pivot_block = block.pivot( + columns=[index_ids[-1]], + values=self._block.value_columns, + values_in_index=True, + ) + return DataFrame(pivot_block) + def _drop_non_numeric(self, keep_bool=True) -> DataFrame: types_to_keep = set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) if not keep_bool: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ba76c4b0d3e..e71b1430e61 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1771,6 +1771,26 @@ def test_df_stack(scalars_dfs): pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) +def test_df_unstack(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + # To match bigquery dataframes + scalars_pandas_df = scalars_pandas_df.copy() + scalars_pandas_df.columns = scalars_pandas_df.columns.astype("string[pyarrow]") + # Can only stack identically-typed columns + columns = [ + "rowindex_2", + "int64_col", + "int64_too", + ] + + # unstack on mono-index produces series + bf_result = scalars_df[columns].unstack().to_pandas() + pd_result = scalars_pandas_df[columns].unstack() + + # Pandas produces NaN, where bq dataframes produces pd.NA + pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + + @pytest.mark.parametrize( ("values", "index", "columns"), [ diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 1e38b47b4cc..10f2a74b21b 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -729,6 +729,26 @@ def test_column_multi_index_stack(scalars_df_index, scalars_pandas_df_index): ) +def test_column_multi_index_unstack(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "rowindex_2"] + level1 = pandas.Index(["b", "a", "b"], dtype="string[pyarrow]") + # Need resulting column to be pyarrow string rather than object dtype + level2 = pandas.Index(["a", "b", "b"], dtype="string[pyarrow]") + multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.unstack().to_pandas() + # Shifting sort behavior in stack + pd_result = pd_df.unstack() + + # Pandas produces NaN, where bq dataframes produces pd.NA + # Column ordering seems to depend on pandas version + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + + @pytest.mark.skip(reason="Pandas fails in newer versions.") def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "int64_col", "rowindex_2"] @@ -866,6 +886,17 @@ def test_column_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_i pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_multi_index_unstack(hockey_df, hockey_pandas_df): + bf_result = ( + hockey_df.set_index(["team_name", "season", "position"]).unstack().to_pandas() + ) + pd_result = hockey_pandas_df.set_index( + ["team_name", "season", "position"] + ).unstack() + + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + def test_column_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "string_col", "bool_col"] multi_columns = pandas.MultiIndex.from_tuples( diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 5cd9fe51637..348145a4d65 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1972,6 +1972,21 @@ def stack(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def unstack(self): + """ + Pivot a level of the (necessarily hierarchical) index labels. + + Returns a DataFrame having a new level of column labels whose inner-most level + consists of the pivoted index labels. + + If the index is not a MultiIndex, the output will be a Series + (the analogue of stack when the columns are not a MultiIndex). + + Returns: + DataFrame or Series + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # Add index and columns From 971d091cac9ad662145a3d43d8f9a785eb0ccc23 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Mon, 2 Oct 2023 20:24:53 -0700 Subject: [PATCH 049/921] fix: fix df/series.iloc by list with multiindex (#79) * fix: fix df/series.iloc by list with multiindex * fix git error --------- Co-authored-by: Henry J Solberg --- bigframes/core/indexers.py | 15 ++++++++------- tests/system/small/test_dataframe.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index a538c80711b..e22a5aed857 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -332,8 +332,6 @@ def _iloc_getitem_series_or_dataframe( elif isinstance(key, slice): return series_or_dataframe._slice(key.start, key.stop, key.step) elif pd.api.types.is_list_like(key): - # TODO(henryjsolberg): support MultiIndex - if len(key) == 0: return typing.cast( typing.Union[bigframes.dataframe.DataFrame, bigframes.series.Series], @@ -346,15 +344,18 @@ def _iloc_getitem_series_or_dataframe( original_series_name if original_series_name is not None else "0" ) df = series_or_dataframe.to_frame() - original_index_name = df.index.name - temporary_index_name = guid.generate_guid(prefix="temp_iloc_index_") - df = df.rename_axis(temporary_index_name) + original_index_names = df.index.names + temporary_index_names = [ + guid.generate_guid(prefix="temp_iloc_index_") + for _ in range(len(df.index.names)) + ] + df = df.rename_axis(temporary_index_names) # set to offset index and use regular loc, then restore index df = df.reset_index(drop=False) result = df.loc[key] - result = result.set_index(temporary_index_name) - result = result.rename_axis(original_index_name) + result = result.set_index(temporary_index_names) + result = result.rename_axis(original_index_names) if isinstance(series_or_dataframe, bigframes.series.Series): result = result[series_name] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e71b1430e61..aad6faa4b6b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2525,6 +2525,24 @@ def test_iloc_list(scalars_df_index, scalars_pandas_df_index): ) +def test_iloc_list_multiindex(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + scalars_df = scalars_df.copy() + scalars_pandas_df = scalars_pandas_df.copy() + scalars_df = scalars_df.set_index(["bytes_col", "numeric_col"]) + scalars_pandas_df = scalars_pandas_df.set_index(["bytes_col", "numeric_col"]) + + index_list = [0, 0, 0, 5, 4, 7] + + bf_result = scalars_df.iloc[index_list] + pd_result = scalars_pandas_df.iloc[index_list] + + pd.testing.assert_frame_equal( + bf_result.to_pandas(), + pd_result, + ) + + def test_iloc_empty_list(scalars_df_index, scalars_pandas_df_index): index_list = [] From c9fa85cc338be5e9a8dde59b255690aedbbc1127 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 3 Oct 2023 00:59:44 -0700 Subject: [PATCH 050/921] feat: add multi-column dataframe merge (#73) --- bigframes/core/blocks.py | 73 ++++++++++ bigframes/core/joins/single_column.py | 60 +++------ bigframes/core/utils.py | 33 +++++ bigframes/dataframe.py | 127 +++++------------- tests/system/small/test_dataframe.py | 35 ++++- .../bigframes_vendored/pandas/core/frame.py | 12 +- .../pandas/core/reshape/merge.py | 12 +- 7 files changed, 206 insertions(+), 146 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 863852c684c..0161d173618 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -38,6 +38,7 @@ import bigframes.core as core import bigframes.core.guid as guid import bigframes.core.indexes as indexes +import bigframes.core.joins as joins import bigframes.core.ordering as ordering import bigframes.core.utils import bigframes.core.utils as utils @@ -1403,6 +1404,78 @@ def concat( result_block = result_block.reset_index() return result_block + def merge( + self, + other: Block, + how: typing.Literal[ + "inner", + "left", + "outer", + "right", + ], + left_col_ids: typing.Sequence[str], + right_col_ids: typing.Sequence[str], + sort: bool, + suffixes: tuple[str, str] = ("_x", "_y"), + ) -> Block: + ( + joined_expr, + coalesced_join_cols, + (get_column_left, get_column_right), + ) = joins.join_by_column( + self.expr, + left_col_ids, + other.expr, + right_col_ids, + how=how, + sort=sort, + ) + + # which join key parts should be coalesced + merge_join_key_mask = [ + str(self.col_id_to_label[left_id]) == str(other.col_id_to_label[right_id]) + for left_id, right_id in zip(left_col_ids, right_col_ids) + ] + labels_to_coalesce = [ + self.col_id_to_label[col_id] + for i, col_id in enumerate(left_col_ids) + if merge_join_key_mask[i] + ] + + def left_col_mapping(col_id: str) -> str: + if col_id in left_col_ids: + join_key_part = left_col_ids.index(col_id) + if merge_join_key_mask[join_key_part]: + return coalesced_join_cols[join_key_part] + return get_column_left(col_id) + + def right_col_mapping(col_id: str) -> typing.Optional[str]: + if col_id in right_col_ids: + join_key_part = right_col_ids.index(col_id) + if merge_join_key_mask[join_key_part]: + return None + return get_column_right(col_id) + + left_columns = [left_col_mapping(col_id) for col_id in self.value_columns] + + right_columns = [ + typing.cast(str, right_col_mapping(col_id)) + for col_id in other.value_columns + if right_col_mapping(col_id) + ] + + expr = joined_expr.select_columns([*left_columns, *right_columns]) + labels = utils.merge_column_labels( + self.column_labels, + other.column_labels, + coalesce_labels=labels_to_coalesce, + suffixes=suffixes, + ) + + # Constructs default index + expr, offset_index_id = expr.promote_offsets() + return Block(expr, index_columns=[offset_index_id], column_labels=labels) + def _force_reproject(self) -> Block: """Forces a reprojection of the underlying tables expression. Used to force predicate/order application before subsequent operations.""" return Block( diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py index 8a9825cf0b5..2d616fc3f0c 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/joins/single_column.py @@ -44,7 +44,6 @@ def join_by_column( "right", ], sort: bool = False, - coalesce_join_keys: bool = True, allow_row_identity_join: bool = True, ) -> Tuple[ core.ArrayValue, @@ -59,8 +58,6 @@ def join_by_column( right: Expression for right table to join. right_column_ids: Column IDs (not label) to join by. how: The type of join to perform. - coalesce_join_keys: if set to False, returned column ids will contain - both left and right join key columns. allow_row_identity_join (bool): If True, allow matching by row identity. Set to False to always perform a true JOIN in generated SQL. @@ -71,8 +68,6 @@ def join_by_column( * Sequence[str]: Column IDs of the coalesced join columns. Sometimes either the left/right table will have missing rows. This column pulls the non-NULL value from either left/right. - If coalesce_join_keys is False, will return uncombined left and - right key columns. * Tuple[Callable, Callable]: For a given column ID from left or right, respectively, return the new column id from the combined expression. """ @@ -100,9 +95,7 @@ def join_by_column( right_join_keys = [ combined_expr.get_column(get_column_right(col)) for col in right_column_ids ] - join_key_cols = get_join_cols( - left_join_keys, right_join_keys, how, coalesce_join_keys - ) + join_key_cols = get_coalesced_join_cols(left_join_keys, right_join_keys, how) join_key_ids = [col.get_name() for col in join_key_cols] combined_expr = combined_expr.projection( [*join_key_cols, *combined_expr.columns] @@ -182,9 +175,7 @@ def get_column_right(col_id): right_join_keys = [ combined_table[get_column_right(col)] for col in right_column_ids ] - join_key_cols = get_join_cols( - left_join_keys, right_join_keys, how, coalesce_join_keys - ) + join_key_cols = get_coalesced_join_cols(left_join_keys, right_join_keys, how) # We could filter out the original join columns, but predicates/ordering # might still reference them in implicit joins. columns = ( @@ -226,46 +217,35 @@ def get_column_right(col_id): ) -def get_join_cols( +def get_coalesced_join_cols( left_join_cols: typing.Iterable[ibis_types.Value], right_join_cols: typing.Iterable[ibis_types.Value], how: str, - coalesce_join_keys: bool = True, ) -> typing.List[ibis_types.Value]: join_key_cols: list[ibis_types.Value] = [] for left_col, right_col in zip(left_join_cols, right_join_cols): - if not coalesce_join_keys: + if how == "left" or how == "inner": join_key_cols.append(left_col.name(guid.generate_guid(prefix="index_"))) + elif how == "right": join_key_cols.append(right_col.name(guid.generate_guid(prefix="index_"))) - else: - if how == "left" or how == "inner": + elif how == "outer": + # The left index and the right index might contain null values, for + # example due to an outer join with different numbers of rows. Coalesce + # these to take the index value from either column. + # Use a random name in case the left index and the right index have the + # same name. In such a case, _x and _y suffixes will already be used. + # Don't need to coalesce if they are exactly the same column. + if left_col.name("index").equals(right_col.name("index")): join_key_cols.append(left_col.name(guid.generate_guid(prefix="index_"))) - elif how == "right": - join_key_cols.append( - right_col.name(guid.generate_guid(prefix="index_")) - ) - elif how == "outer": - # The left index and the right index might contain null values, for - # example due to an outer join with different numbers of rows. Coalesce - # these to take the index value from either column. - # Use a random name in case the left index and the right index have the - # same name. In such a case, _x and _y suffixes will already be used. - # Don't need to coalesce if they are exactly the same column. - if left_col.name("index").equals(right_col.name("index")): - join_key_cols.append( - left_col.name(guid.generate_guid(prefix="index_")) - ) - else: - join_key_cols.append( - ibis.coalesce( - left_col, - right_col, - ).name(guid.generate_guid(prefix="index_")) - ) else: - raise ValueError( - f"Unexpected join type: {how}. {constants.FEEDBACK_LINK}" + join_key_cols.append( + ibis.coalesce( + left_col, + right_col, + ).name(guid.generate_guid(prefix="index_")) ) + else: + raise ValueError(f"Unexpected join type: {how}. {constants.FEEDBACK_LINK}") return join_key_cols diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 1a5d3ab37ca..dc7c7090117 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -104,3 +104,36 @@ def get_standardized_ids( idx_ids, col_ids = ids[: len(idx_ids)], ids[len(idx_ids) :] return col_ids, idx_ids + + +def merge_column_labels( + left_labels: pd.Index, + right_labels: pd.Index, + coalesce_labels: typing.Sequence, + suffixes: tuple[str, str] = ("_x", "_y"), +) -> pd.Index: + result_labels = [] + + for col_label in left_labels: + if col_label in right_labels: + if col_label in coalesce_labels: + # Merging on the same column only returns 1 key column from coalesce both. + # Take the left key column. + result_labels.append(col_label) + else: + result_labels.append(str(col_label) + suffixes[0]) + else: + result_labels.append(col_label) + + for col_label in right_labels: + if col_label in left_labels: + if col_label in coalesce_labels: + # Merging on the same column only returns 1 key column from coalesce both. + # Pass the right key column. + pass + else: + result_labels.append(str(col_label) + suffixes[1]) + else: + result_labels.append(col_label) + + return pd.Index(result_labels) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ea06e28cdfc..b8ae37ee6c0 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -46,7 +46,6 @@ import bigframes.core.indexers as indexers import bigframes.core.indexes as indexes import bigframes.core.io -import bigframes.core.joins as joins import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.core.window @@ -1779,12 +1778,10 @@ def merge( ] = "inner", # TODO(garrettwu): Currently can take inner, outer, left and right. To support # cross joins - # TODO(garrettwu): Support "on" list of columns and None. Currently a single - # column must be provided - on: Optional[str] = None, + on: Union[blocks.Label, Sequence[blocks.Label], None] = None, *, - left_on: Optional[str] = None, - right_on: Optional[str] = None, + left_on: Union[blocks.Label, Sequence[blocks.Label], None] = None, + right_on: Union[blocks.Label, Sequence[blocks.Label], None] = None, sort: bool = False, suffixes: tuple[str, str] = ("_x", "_y"), ) -> DataFrame: @@ -1798,97 +1795,41 @@ def merge( ) left_on, right_on = on, on - left = self - left_on_sql = self._sql_names(left_on) - # 0 elements already throws an exception - if len(left_on_sql) > 1: - raise ValueError(f"The column label {left_on} is not unique.") - left_on_sql = left_on_sql[0] - - right_on_sql = right._sql_names(right_on) - if len(right_on_sql) > 1: - raise ValueError(f"The column label {right_on} is not unique.") - right_on_sql = right_on_sql[0] - - ( - joined_expr, - join_key_ids, - (get_column_left, get_column_right), - ) = joins.join_by_column( - left._block.expr, - [left_on_sql], - right._block.expr, - [right_on_sql], - how=how, - sort=sort, - # In merging on the same column, it only returns 1 key column from coalesced both. - # While if 2 different columns, both will be presented in the result. - coalesce_join_keys=(left_on == right_on), - ) - # TODO(swast): Add suffixes to the column labels instead of reusing the - # column IDs as the new labels. - # Drop the index column(s) to be consistent with pandas. - left_columns = [ - join_key_ids[0] if (col_id == left_on_sql) else get_column_left(col_id) - for col_id in left._block.value_columns - ] - - right_columns = [] - for col_id in right._block.value_columns: - if col_id == right_on_sql: - # When left_on == right_on - if len(join_key_ids) > 1: - right_columns.append(join_key_ids[1]) - else: - right_columns.append(get_column_right(col_id)) - - expr = joined_expr.select_columns([*left_columns, *right_columns]) - labels = self._get_merged_col_labels( - right, left_on=left_on, right_on=right_on, suffixes=suffixes - ) + if utils.is_list_like(left_on): + left_on = list(left_on) # type: ignore + else: + left_on = [left_on] - # Constructs default index - expr, offset_index_id = expr.promote_offsets() - block = blocks.Block( - expr, index_columns=[offset_index_id], column_labels=labels + if utils.is_list_like(right_on): + right_on = list(right_on) # type: ignore + else: + right_on = [right_on] + + left_join_ids = [] + for label in left_on: # type: ignore + left_col_id = self._resolve_label_exact(label) + # 0 elements already throws an exception + if not left_col_id: + raise ValueError(f"No column {label} found in self.") + left_join_ids.append(left_col_id) + + right_join_ids = [] + for label in right_on: # type: ignore + right_col_id = right._resolve_label_exact(label) + if not right_col_id: + raise ValueError(f"No column {label} found in other.") + right_join_ids.append(right_col_id) + + block = self._block.merge( + right._block, + how, + left_join_ids, + right_join_ids, + sort=sort, + suffixes=suffixes, ) return DataFrame(block) - def _get_merged_col_labels( - self, - right: DataFrame, - left_on: str, - right_on: str, - suffixes: tuple[str, str] = ("_x", "_y"), - ) -> List[blocks.Label]: - on_col_equal = left_on == right_on - - left_col_labels: list[blocks.Label] = [] - for col_label in self._block.column_labels: - if col_label in right._block.column_labels: - if on_col_equal and col_label == left_on: - # Merging on the same column only returns 1 key column from coalesce both. - # Take the left key column. - left_col_labels.append(col_label) - else: - left_col_labels.append(str(col_label) + suffixes[0]) - else: - left_col_labels.append(col_label) - - right_col_labels: list[blocks.Label] = [] - for col_label in right._block.column_labels: - if col_label in self._block.column_labels: - if on_col_equal and col_label == left_on: - # Merging on the same column only returns 1 key column from coalesce both. - # Pass the right key column. - pass - else: - right_col_labels.append(str(col_label) + suffixes[1]) - else: - right_col_labels.append(col_label) - - return left_col_labels + right_col_labels - def join( self, other: DataFrame, *, on: Optional[str] = None, how: str = "left" ) -> DataFrame: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index aad6faa4b6b..a27d8fd487c 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -757,7 +757,7 @@ def test_df_isin_dict(scalars_dfs): ("right",), ], ) -def test_merge(scalars_dfs, merge_how): +def test_df_merge(scalars_dfs, merge_how): scalars_df, scalars_pandas_df = scalars_dfs on = "rowindex_2" left_columns = ["int64_col", "float64_col", "rowindex_2"] @@ -782,6 +782,39 @@ def test_merge(scalars_dfs, merge_how): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +@pytest.mark.parametrize( + ("left_on", "right_on"), + [ + (["int64_col", "rowindex_2"], ["int64_col", "rowindex_2"]), + (["rowindex_2", "int64_col"], ["int64_col", "rowindex_2"]), + (["rowindex_2", "float64_col"], ["int64_col", "rowindex_2"]), + ], +) +def test_df_merge_multi_key(scalars_dfs, left_on, right_on): + scalars_df, scalars_pandas_df = scalars_dfs + left_columns = ["int64_col", "float64_col", "rowindex_2"] + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_columns] + # Offset the rows somewhat so that outer join can have an effect. + right = scalars_df[right_columns].assign(rowindex_2=scalars_df["rowindex_2"] + 2) + + df = left.merge(right, "outer", left_on=left_on, right_on=right_on, sort=True) + bf_result = df.to_pandas() + + pd_result = scalars_pandas_df[left_columns].merge( + scalars_pandas_df[right_columns].assign( + rowindex_2=scalars_pandas_df["rowindex_2"] + 2 + ), + "outer", + left_on=left_on, + right_on=right_on, + sort=True, + ) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + @pytest.mark.parametrize( ("merge_how",), [ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 348145a4d65..11aaf84b14e 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1468,14 +1468,14 @@ def merge( ``inner``: use intersection of keys from both frames, similar to a SQL inner join; preserve the order of the left keys. - on: - Column join on. It must be found in both DataFrames. Either on or left_on + right_on + on (label or list of labels): + Columns to join on. It must be found in both DataFrames. Either on or left_on + right_on must be passed in. - left_on: - Column join on in the left DataFrame. Either on or left_on + right_on + left_on (label or list of labels): + Columns to join on in the left DataFrame. Either on or left_on + right_on must be passed in. - right_on: - Column join on in the right DataFrame. Either on or left_on + right_on + right_on (label or list of labels): + Columns to join on in the right DataFrame. Either on or left_on + right_on must be passed in. sort: Default False. Sort the join keys lexicographically in the diff --git a/third_party/bigframes_vendored/pandas/core/reshape/merge.py b/third_party/bigframes_vendored/pandas/core/reshape/merge.py index ee02d698da3..cc81de405b9 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/merge.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/merge.py @@ -51,14 +51,14 @@ def merge( ``inner``: use intersection of keys from both frames, similar to a SQL inner join; preserve the order of the left keys. - on: - Column join on. It must be found in both DataFrames. Either on or left_on + right_on + on (label or list of labels): + Columns to join on. It must be found in both DataFrames. Either on or left_on + right_on must be passed in. - left_on: - Column join on in the left DataFrame. Either on or left_on + right_on + left_on (label or list of labels): + Columns to join on in the left DataFrame. Either on or left_on + right_on must be passed in. - right_on: - Column join on in the right DataFrame. Either on or left_on + right_on + right_on (label or list of labels): + Columns to join on in the right DataFrame. Either on or left_on + right_on must be passed in. sort: Default False. Sort the join keys lexicographically in the From c506200dfc770b981d81013c63d822466ea6350b Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:39:27 -0500 Subject: [PATCH 051/921] chore: [autoapprove] bump cryptography from 41.0.3 to 41.0.4 (#75) Source-Link: https://github.com/googleapis/synthtool/commit/dede53ff326079b457cfb1aae5bbdc82cbb51dc3 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:fac304457974bb530cc5396abd4ab25d26a469cd3bc97cbfb18c8d4324c584eb Co-authored-by: Owl Bot Co-authored-by: Tim Swast --- .github/.OwlBot.lock.yaml | 4 ++-- .gitignore | 1 + .kokoro/requirements.txt | 49 ++++++++++++++++++++------------------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index a3da1b0d4cd..a9bdb1b7ac0 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:3e3800bb100af5d7f9e810d48212b37812c1856d20ffeafb99ebe66461b61fc7 -# created: 2023-08-02T10:53:29.114535628Z + digest: sha256:fac304457974bb530cc5396abd4ab25d26a469cd3bc97cbfb18c8d4324c584eb +# created: 2023-10-02T21:31:03.517640371Z diff --git a/.gitignore b/.gitignore index b4243ced74e..d083ea1ddc3 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ docs.metadata # Virtual environment env/ +venv/ # Test logs coverage.xml diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 029bd342de9..96d593c8c82 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -113,30 +113,30 @@ commonmark==0.9.1 \ --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 # via rich -cryptography==41.0.3 \ - --hash=sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306 \ - --hash=sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84 \ - --hash=sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47 \ - --hash=sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d \ - --hash=sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116 \ - --hash=sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207 \ - --hash=sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81 \ - --hash=sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087 \ - --hash=sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd \ - --hash=sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507 \ - --hash=sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858 \ - --hash=sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae \ - --hash=sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34 \ - --hash=sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906 \ - --hash=sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd \ - --hash=sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922 \ - --hash=sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7 \ - --hash=sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4 \ - --hash=sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574 \ - --hash=sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1 \ - --hash=sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c \ - --hash=sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e \ - --hash=sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de +cryptography==41.0.4 \ + --hash=sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67 \ + --hash=sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311 \ + --hash=sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8 \ + --hash=sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13 \ + --hash=sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143 \ + --hash=sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f \ + --hash=sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829 \ + --hash=sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd \ + --hash=sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397 \ + --hash=sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac \ + --hash=sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d \ + --hash=sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a \ + --hash=sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839 \ + --hash=sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e \ + --hash=sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6 \ + --hash=sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9 \ + --hash=sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860 \ + --hash=sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca \ + --hash=sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91 \ + --hash=sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d \ + --hash=sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714 \ + --hash=sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb \ + --hash=sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f # via # gcp-releasetool # secretstorage @@ -382,6 +382,7 @@ protobuf==3.20.3 \ # gcp-docuploader # gcp-releasetool # google-api-core + # googleapis-common-protos pyasn1==0.4.8 \ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba From 158c00ca1e48f0d8175ed7d2a9b47ab1dc8f288f Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 3 Oct 2023 12:50:14 -0500 Subject: [PATCH 052/921] chore: fix flaky system tests (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove a mutation that could affect subsequent tests. * Make error message from API more flexible. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/small/test_pandas_options.py | 10 +++++----- tests/system/small/test_session.py | 7 +++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index 6510c4fa271..956b29ae12f 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -75,7 +75,7 @@ def test_read_gbq_start_sets_session_location( # Now read_gbq* from another location should fail with pytest.raises( google.api_core.exceptions.NotFound, - match=f"404 Not found: Dataset {dataset_id_permanent} was not found in location {tokyo_location}", + match=dataset_id_permanent, ): read_method(query) @@ -100,7 +100,7 @@ def test_read_gbq_start_sets_session_location( # Now read_gbq* from another location should fail with pytest.raises( google.api_core.exceptions.NotFound, - match=f"404 Not found: Dataset {dataset_id_permanent_tokyo} was not found in location US", + match=dataset_id_permanent_tokyo, ): read_method(query_tokyo) @@ -146,7 +146,7 @@ def test_read_gbq_after_session_start_must_comply_with_default_location( # Doing read_gbq* from a table in another location should fail with pytest.raises( google.api_core.exceptions.NotFound, - match=f"404 Not found: Dataset {dataset_id_permanent_tokyo} was not found in location US", + match=dataset_id_permanent_tokyo, ): read_method(query_tokyo) @@ -194,7 +194,7 @@ def test_read_gbq_must_comply_with_set_location_US( # Starting user journey with read_gbq* from another location should fail with pytest.raises( google.api_core.exceptions.NotFound, - match=f"404 Not found: Dataset {dataset_id_permanent_tokyo} was not found in location US", + match=dataset_id_permanent_tokyo, ): read_method(query_tokyo) @@ -244,7 +244,7 @@ def test_read_gbq_must_comply_with_set_location_non_US( # Starting user journey with read_gbq* from another location should fail with pytest.raises( google.api_core.exceptions.NotFound, - match=f"404 Not found: Dataset {dataset_id_permanent} was not found in location {tokyo_location}", + match=dataset_id_permanent, ): read_method(query) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 614c9537649..d2560174e43 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -321,11 +321,10 @@ def test_read_pandas_multi_index(session, scalars_pandas_df_multi_index): def test_read_pandas_rowid_exists_adds_suffix(session, scalars_pandas_df_default_index): - scalars_pandas_df_default_index["rowid"] = np.arange( - scalars_pandas_df_default_index.shape[0] - ) + pandas_df = scalars_pandas_df_default_index.copy() + pandas_df["rowid"] = np.arange(pandas_df.shape[0]) - df = session.read_pandas(scalars_pandas_df_default_index) + df = session.read_pandas(pandas_df) total_order_col = df._block._expr._ordering.total_order_col assert total_order_col and total_order_col.column_id == "rowid_2" From 8f3b5b240f0f28fef92465abc53504e875d7335a Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 3 Oct 2023 15:16:15 -0500 Subject: [PATCH 053/921] fix: avoid `403 response too large to return` error with `read_gbq` and large query results (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Towards internal issue 303057336 🦕 --- bigframes/core/__init__.py | 4 +- bigframes/core/io.py | 45 +++++- bigframes/session.py | 155 +++++++++++--------- tests/system/small/ml/test_core.py | 3 +- tests/system/small/ml/test_decomposition.py | 5 +- tests/system/small/test_session.py | 1 + tests/unit/core/test_io.py | 55 +++++++ 7 files changed, 193 insertions(+), 75 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 5e0675fd134..8008c1189a1 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -1198,8 +1198,8 @@ def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: destination = self._session._ibis_to_session_table( ibis_expr, cluster_cols=cluster_cols, api_name="cache" ) - table_expression = self._session.ibis_client.sql( - f"SELECT * FROM `_SESSION`.`{destination.table_id}`" + table_expression = self._session.ibis_client.table( + f"{destination.project}.{destination.dataset_id}.{destination.table_id}" ) new_columns = [table_expression[column] for column in self.column_names] new_hidden_columns = [ diff --git a/bigframes/core/io.py b/bigframes/core/io.py index 3c2e5a25f5a..d47efbdddcf 100644 --- a/bigframes/core/io.py +++ b/bigframes/core/io.py @@ -16,7 +16,8 @@ import datetime import textwrap -from typing import Dict, Union +import types +from typing import Dict, Iterable, Union import google.cloud.bigquery as bigquery @@ -89,6 +90,48 @@ def create_snapshot_sql( ) +# BigQuery REST API returns types in Legacy SQL format +# https://cloud.google.com/bigquery/docs/data-types but we use Standard SQL +# names +# https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types +BQ_STANDARD_TYPES = types.MappingProxyType( + { + "BOOLEAN": "BOOL", + "INTEGER": "INT64", + "FLOAT": "FLOAT64", + } +) + + +def bq_field_to_type_sql(field: bigquery.SchemaField): + if field.mode == "REPEATED": + nested_type = bq_field_to_type_sql( + bigquery.SchemaField( + field.name, field.field_type, mode="NULLABLE", fields=field.fields + ) + ) + return f"ARRAY<{nested_type}>" + + if field.field_type == "RECORD": + nested_fields_sql = ", ".join( + bq_field_to_sql(child_field) for child_field in field.fields + ) + return f"STRUCT<{nested_fields_sql}>" + + type_ = field.field_type + return BQ_STANDARD_TYPES.get(type_, type_) + + +def bq_field_to_sql(field: bigquery.SchemaField): + name = field.name + type_ = bq_field_to_type_sql(field) + return f"`{name}` {type_}" + + +def bq_schema_to_sql(schema: Iterable[bigquery.SchemaField]): + return ", ".join(bq_field_to_sql(field) for field in schema) + + def format_option(key: str, value: Union[bool, str]) -> str: if isinstance(value, bool): return f"{key}=true" if value else f"{key}=false" diff --git a/bigframes/session.py b/bigframes/session.py index 7b827c7dcfc..ac48c977cb0 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -449,13 +449,6 @@ def _query_to_destination( index_cols: List[str], api_name: str, ) -> Tuple[Optional[bigquery.TableReference], Optional[bigquery.QueryJob]]: - # If there are no index columns, then there's no reason to cache to a - # (clustered) session table, as we'll just have to query it again to - # create a default index & ordering. - if not index_cols: - _, query_job = self._start_query(query) - return query_job.destination, query_job - # If a dry_run indicates this is not a query type job, then don't # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. dry_run_config = bigquery.QueryJobConfig() @@ -465,15 +458,24 @@ def _query_to_destination( _, query_job = self._start_query(query) return query_job.destination, query_job - # Make sure we cluster by the index column(s) so that subsequent - # operations are as speedy as they can be. + # Create a table to workaround BigQuery 10 GB query results limit. See: + # internal issue 303057336. + # Since we have a `statement_type == 'SELECT'`, schema should be populated. + schema = typing.cast(Iterable[bigquery.SchemaField], dry_run_job.schema) + temp_table = self._create_session_table_empty(api_name, schema, index_cols) + + job_config = bigquery.QueryJobConfig() + job_config.destination = temp_table + try: - ibis_expr = self.ibis_client.sql(query) - return self._ibis_to_session_table(ibis_expr, index_cols, api_name), None + # Write to temp table to workaround BigQuery 10 GB query results + # limit. See: internal issue 303057336. + _, query_job = self._start_query(query, job_config=job_config) + return query_job.destination, query_job except google.api_core.exceptions.BadRequest: - # Some SELECT statements still aren't compatible with CREATE TEMP - # TABLE ... AS SELECT ... statements. For example, if the query has - # a top-level ORDER BY, this conflicts with our ability to cluster + # Some SELECT statements still aren't compatible with cluster + # tables as the destination. For example, if the query has a + # top-level ORDER BY, this conflicts with our ability to cluster # the table by the index column(s). _, query_job = self._start_query(query) return query_job.destination, query_job @@ -1231,6 +1233,54 @@ def _create_session_table(self) -> bigquery.TableReference: ) return dataset.table(table_name) + def _create_session_table_empty( + self, + api_name: str, + schema: Iterable[bigquery.SchemaField], + cluster_cols: List[str], + ) -> bigquery.TableReference: + # Can't set a table in _SESSION as destination via query job API, so we + # run DDL, instead. + table = self._create_session_table() + schema_sql = bigframes_io.bq_schema_to_sql(schema) + + clusterable_cols = [ + col.name + for col in schema + if col.name in cluster_cols and _can_cluster_bq(col) + ][:_MAX_CLUSTER_COLUMNS] + + if clusterable_cols: + cluster_cols_sql = ", ".join( + f"`{cluster_col}`" for cluster_col in clusterable_cols + ) + cluster_sql = f"CLUSTER BY {cluster_cols_sql}" + else: + cluster_sql = "" + + ddl_text = f""" + CREATE TEMP TABLE + `_SESSION`.`{table.table_id}` + ({schema_sql}) + {cluster_sql} + """ + + job_config = bigquery.QueryJobConfig() + + # Include a label so that Dataplex Lineage can identify temporary + # tables that BigQuery DataFrames creates. Googlers: See internal issue + # 296779699. We're labeling the job instead of the table because + # otherwise we get `BadRequest: 400 OPTIONS on temporary tables are not + # supported`. + job_config.labels = {"source": "bigquery-dataframes-temp"} + job_config.labels["bigframes-api"] = api_name + + _, query_job = self._start_query(ddl_text, job_config=job_config) + + # Use fully-qualified name instead of `_SESSION` name so that the + # created table can be used as the destination table. + return query_job.destination + def _create_sequential_ordering( self, table: ibis_types.Table, @@ -1249,7 +1299,9 @@ def _create_sequential_ordering( cluster_cols=list(index_cols) + [default_ordering_name], api_name=api_name, ) - table = self.ibis_client.sql(f"SELECT * FROM `{table_ref.table_id}`") + table = self.ibis_client.table( + f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" + ) ordering_reference = core.OrderingColumnReference(default_ordering_name) ordering = core.ExpressionOrdering( ordering_value_columns=[ordering_reference], @@ -1264,55 +1316,13 @@ def _ibis_to_session_table( cluster_cols: Iterable[str], api_name: str, ) -> bigquery.TableReference: - clusterable_cols = [ - col for col in cluster_cols if _can_cluster(table[col].type()) - ][:_MAX_CLUSTER_COLUMNS] - return self._query_to_session_table( + desination, _ = self._query_to_destination( self.ibis_client.compile(table), - cluster_cols=clusterable_cols, + index_cols=list(cluster_cols), api_name=api_name, ) - - def _query_to_session_table( - self, - query_text: str, - cluster_cols: Iterable[str], - api_name: str, - ) -> bigquery.TableReference: - if len(list(cluster_cols)) > _MAX_CLUSTER_COLUMNS: - raise ValueError( - f"Too many cluster columns: {list(cluster_cols)}, max {_MAX_CLUSTER_COLUMNS} allowed." - ) - # Can't set a table in _SESSION as destination via query job API, so we - # run DDL, instead. - table = self._create_session_table() - cluster_cols_sql = ", ".join(f"`{cluster_col}`" for cluster_col in cluster_cols) - - # TODO(swast): This might not support multi-statement SQL queries (scripts). - ddl_text = f""" - CREATE TEMP TABLE `_SESSION`.`{table.table_id}` - CLUSTER BY {cluster_cols_sql} - AS {query_text} - """ - - job_config = bigquery.QueryJobConfig() - - # Include a label so that Dataplex Lineage can identify temporary - # tables that BigQuery DataFrames creates. Googlers: See internal issue - # 296779699. We're labeling the job instead of the table because - # otherwise we get `BadRequest: 400 OPTIONS on temporary tables are not - # supported`. - job_config.labels = {"source": "bigquery-dataframes-temp"} - job_config.labels["bigframes-api"] = api_name - - try: - self._start_query( - ddl_text, job_config=job_config - ) # Wait for the job to complete - except google.api_core.exceptions.Conflict: - # Allow query retry to succeed. - pass - return table + # There should always be a destination table for this query type. + return typing.cast(bigquery.TableReference, desination) def remote_function( self, @@ -1494,14 +1504,21 @@ def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Sessi return Session(context) -def _can_cluster(ibis_type: ibis_dtypes.DataType): +def _can_cluster_bq(field: bigquery.SchemaField): # https://cloud.google.com/bigquery/docs/clustered-tables # Notably, float is excluded - return ( - ibis_type.is_integer() - or ibis_type.is_string() - or ibis_type.is_decimal() - or ibis_type.is_date() - or ibis_type.is_timestamp() - or ibis_type.is_boolean() + type_ = field.field_type + return type_ in ( + "INTEGER", + "INT64", + "STRING", + "NUMERIC", + "DECIMAL", + "BIGNUMERIC", + "BIGDECIMAL", + "DATE", + "DATETIME", + "TIMESTAMP", + "BOOL", + "BOOLEAN", ) diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index ace943956fe..f911dd7eebc 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -23,6 +23,7 @@ import bigframes from bigframes.ml import core +import tests.system.utils def test_model_eval( @@ -224,7 +225,7 @@ def test_pca_model_principal_component_info(penguins_bqml_pca_model: core.BqmlMo "cumulative_explained_variance_ratio": [0.469357, 0.651283, 0.812383], }, ) - pd.testing.assert_frame_equal( + tests.system.utils.assert_pandas_df_equal_ignore_ordering( result, expected, check_exact=False, diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index c71bbbe3b0c..e31681f4a09 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -15,6 +15,7 @@ import pandas as pd from bigframes.ml import decomposition +import tests.system.utils def test_pca_predict(penguins_pca_model, new_penguins_df): @@ -129,7 +130,7 @@ def test_pca_explained_variance_(penguins_pca_model: decomposition.PCA): "explained_variance": [3.278657, 1.270829, 1.125354], }, ) - pd.testing.assert_frame_equal( + tests.system.utils.assert_pandas_df_equal_ignore_ordering( result, expected, check_exact=False, @@ -148,7 +149,7 @@ def test_pca_explained_variance_ratio_(penguins_pca_model: decomposition.PCA): "explained_variance_ratio": [0.469357, 0.181926, 0.1611], }, ) - pd.testing.assert_frame_equal( + tests.system.utils.assert_pandas_df_equal_ignore_ordering( result, expected, check_exact=False, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index d2560174e43..53ddfa3c495 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -57,6 +57,7 @@ def test_read_gbq_tokyo( ), pytest.param( """SELECT + t.int64_col + 1 as my_ints, t.float64_col * 2 AS my_floats, CONCAT(t.string_col, "_2") AS my_strings, t.int64_col > 0 AS my_bools, diff --git a/tests/unit/core/test_io.py b/tests/unit/core/test_io.py index c5074f80c2c..afb38a5f753 100644 --- a/tests/unit/core/test_io.py +++ b/tests/unit/core/test_io.py @@ -13,8 +13,10 @@ # limitations under the License. import datetime +from typing import Iterable import google.cloud.bigquery as bigquery +import pytest import bigframes.core.io @@ -47,3 +49,56 @@ def test_create_snapshot_sql_doesnt_timetravel_session_datasets(): # Don't need the project ID for _SESSION tables. assert "my-test-project" not in sql + + +@pytest.mark.parametrize( + ("schema", "expected"), + ( + ( + [bigquery.SchemaField("My Column", "INTEGER")], + "`My Column` INT64", + ), + ( + [ + bigquery.SchemaField("My Column", "INTEGER"), + bigquery.SchemaField("Float Column", "FLOAT"), + bigquery.SchemaField("Bool Column", "BOOLEAN"), + ], + "`My Column` INT64, `Float Column` FLOAT64, `Bool Column` BOOL", + ), + ( + [ + bigquery.SchemaField("My Column", "INTEGER", mode="REPEATED"), + bigquery.SchemaField("Float Column", "FLOAT", mode="REPEATED"), + bigquery.SchemaField("Bool Column", "BOOLEAN", mode="REPEATED"), + ], + "`My Column` ARRAY, `Float Column` ARRAY, `Bool Column` ARRAY", + ), + ( + [ + bigquery.SchemaField( + "My Column", + "RECORD", + mode="REPEATED", + fields=( + bigquery.SchemaField("Float Column", "FLOAT", mode="REPEATED"), + bigquery.SchemaField("Bool Column", "BOOLEAN", mode="REPEATED"), + bigquery.SchemaField( + "Nested Column", + "RECORD", + fields=(bigquery.SchemaField("Int Column", "INTEGER"),), + ), + ), + ), + ], + ( + "`My Column` ARRAY," + + " `Bool Column` ARRAY," + + " `Nested Column` STRUCT<`Int Column` INT64>>>" + ), + ), + ), +) +def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str): + pass From 17afac9ff70a2b93ed70dc7bcce7beb9a53c2ece Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 3 Oct 2023 16:04:14 -0500 Subject: [PATCH 054/921] feat: support STRUCT data type with `Series.struct.field` to extract child fields (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .kokoro/build.sh | 3 + bigframes/dataframe.py | 10 +- bigframes/dtypes.py | 63 ++++++++++++- bigframes/operations/base.py | 10 +- bigframes/operations/structs.py | 61 ++++++++++++ bigframes/series.py | 5 + noxfile.py | 2 +- tests/system/small/test_dataframe.py | 14 ++- tests/unit/test_dtypes.py | 64 +++++++++++++ .../pandas/core/arrays/__init__.py | 0 .../pandas/core/arrays/arrow/__init__.py | 0 .../pandas/core/arrays/arrow/accessors.py | 94 +++++++++++++++++++ .../bigframes_vendored/sklearn/__init__.py | 0 .../sklearn/ensemble/__init__.py | 0 .../bigframes_vendored/xgboost/__init__.py | 0 15 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 bigframes/operations/structs.py create mode 100644 third_party/bigframes_vendored/pandas/core/arrays/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/arrays/arrow/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py create mode 100644 third_party/bigframes_vendored/sklearn/__init__.py create mode 100644 third_party/bigframes_vendored/sklearn/ensemble/__init__.py create mode 100644 third_party/bigframes_vendored/xgboost/__init__.py diff --git a/.kokoro/build.sh b/.kokoro/build.sh index a0fa4bc787b..58eaa7fedf9 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -26,6 +26,9 @@ cd "${PROJECT_ROOT}" # Disable buffering, so that the logs stream through. export PYTHONUNBUFFERED=1 +# Workaround https://github.com/pytest-dev/pytest/issues/9567 +export PY_IGNORE_IMPORTMISMATCH=1 + # Debug: show build environment env | grep KOKORO diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index b8ae37ee6c0..345afbe6e6b 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -160,7 +160,15 @@ def __init__( columns=columns, # type:ignore dtype=dtype, # type:ignore ) - if pd_dataframe.size < MAX_INLINE_DF_SIZE: + if ( + pd_dataframe.size < MAX_INLINE_DF_SIZE + # TODO(swast): Workaround data types limitation in inline data. + and not any( + dt.pyarrow_dtype + for dt in pd_dataframe.dtypes + if isinstance(dt, pandas.ArrowDtype) + ) + ): self._block = blocks.block_from_local( pd_dataframe, session or bigframes.pandas.get_global_session() ) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 59d3007fab0..46a7a1cb503 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -84,10 +84,10 @@ BIDIRECTIONAL_MAPPINGS: Iterable[Tuple[IbisDtype, Dtype]] = ( (ibis_dtypes.boolean, pd.BooleanDtype()), + (ibis_dtypes.date, pd.ArrowDtype(pa.date32())), (ibis_dtypes.float64, pd.Float64Dtype()), (ibis_dtypes.int64, pd.Int64Dtype()), (ibis_dtypes.string, pd.StringDtype(storage="pyarrow")), - (ibis_dtypes.date, pd.ArrowDtype(pa.date32())), (ibis_dtypes.time, pd.ArrowDtype(pa.time64("us"))), (ibis_dtypes.Timestamp(timezone=None), pd.ArrowDtype(pa.timestamp("us"))), ( @@ -100,6 +100,19 @@ pandas: ibis for ibis, pandas in BIDIRECTIONAL_MAPPINGS } +IBIS_TO_ARROW: Dict[ibis_dtypes.DataType, pa.DataType] = { + ibis_dtypes.boolean: pa.bool_(), + ibis_dtypes.date: pa.date32(), + ibis_dtypes.float64: pa.float64(), + ibis_dtypes.int64: pa.int64(), + ibis_dtypes.string: pa.string(), + ibis_dtypes.time: pa.time64("us"), + ibis_dtypes.Timestamp(timezone=None): pa.timestamp("us"), + ibis_dtypes.Timestamp(timezone="UTC"): pa.timestamp("us", tz="UTC"), +} + +ARROW_TO_IBIS = {arrow: ibis for ibis, arrow in IBIS_TO_ARROW.items()} + IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, Union[Dtype, np.dtype[Any]]] = { ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS } @@ -148,11 +161,12 @@ def ibis_dtype_to_bigframes_dtype( # Special cases: Ibis supports variations on these types, but currently # our IO returns them as objects. Eventually, we should support them as # ArrowDType (and update the IO accordingly) - if isinstance(ibis_dtype, ibis_dtypes.Array) or isinstance( - ibis_dtype, ibis_dtypes.Struct - ): + if isinstance(ibis_dtype, ibis_dtypes.Array): return np.dtype("O") + if isinstance(ibis_dtype, ibis_dtypes.Struct): + return pd.ArrowDtype(ibis_dtype_to_arrow_dtype(ibis_dtype)) + if ibis_dtype in IBIS_TO_BIGFRAMES: return IBIS_TO_BIGFRAMES[ibis_dtype] elif isinstance(ibis_dtype, ibis_dtypes.Null): @@ -164,6 +178,26 @@ def ibis_dtype_to_bigframes_dtype( ) +def ibis_dtype_to_arrow_dtype(ibis_dtype: ibis_dtypes.DataType) -> pa.DataType: + if isinstance(ibis_dtype, ibis_dtypes.Array): + return pa.list_(ibis_dtype_to_arrow_dtype(ibis_dtype.value_type)) + + if isinstance(ibis_dtype, ibis_dtypes.Struct): + return pa.struct( + [ + (name, ibis_dtype_to_arrow_dtype(dtype)) + for name, dtype in ibis_dtype.fields.items() + ] + ) + + if ibis_dtype in IBIS_TO_ARROW: + return IBIS_TO_ARROW[ibis_dtype] + else: + raise ValueError( + f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" + ) + + def ibis_value_to_canonical_type(value: ibis_types.Value) -> ibis_types.Value: """Converts an Ibis expression to canonical type. @@ -187,6 +221,24 @@ def ibis_table_to_canonical_types(table: ibis_types.Table) -> ibis_types.Table: return table.select(*casted_columns) +def arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType: + if pa.types.is_struct(arrow_dtype): + struct_dtype = typing.cast(pa.StructType, arrow_dtype) + return ibis_dtypes.Struct.from_tuples( + [ + (field.name, arrow_dtype_to_ibis_dtype(field.type)) + for field in struct_dtype + ] + ) + + if arrow_dtype in ARROW_TO_IBIS: + return ARROW_TO_IBIS[arrow_dtype] + else: + raise ValueError( + f"Unexpected Arrow data type {arrow_dtype}. {constants.FEEDBACK_LINK}" + ) + + def bigframes_dtype_to_ibis_dtype( bigframes_dtype: Union[DtypeString, Dtype, np.dtype[Any]] ) -> ibis_dtypes.DataType: @@ -202,6 +254,9 @@ def bigframes_dtype_to_ibis_dtype( Raises: ValueError: If passed a dtype not supported by BigQuery DataFrames. """ + if isinstance(bigframes_dtype, pd.ArrowDtype): + return arrow_dtype_to_ibis_dtype(bigframes_dtype.pyarrow_dtype) + type_string = str(bigframes_dtype) if type_string in BIGFRAMES_STRING_TO_BIGFRAMES: bigframes_dtype = BIGFRAMES_STRING_TO_BIGFRAMES[ diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index add6af57f41..51eaad18b9b 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -86,7 +86,15 @@ def __init__( if pd_series.name is None: # to_frame will set default numeric column label if unnamed, but we do not support int column label, so must rename pd_dataframe = pd_dataframe.set_axis(["unnamed_col"], axis=1) - if pd_dataframe.size < MAX_INLINE_SERIES_SIZE: + if ( + pd_dataframe.size < MAX_INLINE_SERIES_SIZE + # TODO(swast): Workaround data types limitation in inline data. + and not any( + dt.pyarrow_dtype + for dt in pd_dataframe.dtypes + if isinstance(dt, pd.ArrowDtype) + ) + ): self._block = blocks.block_from_local( pd_dataframe, session or bigframes.pandas.get_global_session() ) diff --git a/bigframes/operations/structs.py b/bigframes/operations/structs.py new file mode 100644 index 00000000000..80d51115d01 --- /dev/null +++ b/bigframes/operations/structs.py @@ -0,0 +1,61 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing + +import ibis.expr.types as ibis_types + +import bigframes.dataframe +import bigframes.operations +import bigframes.operations.base +import bigframes.series +import third_party.bigframes_vendored.pandas.core.arrays.arrow.accessors as vendoracessors + + +class StructField(bigframes.operations.UnaryOp): + def __init__(self, name_or_index: str | int): + self._name_or_index = name_or_index + + def _as_ibis(self, x: ibis_types.Value): + struct_value = typing.cast(ibis_types.StructValue, x) + if isinstance(self._name_or_index, str): + name = self._name_or_index + else: + name = struct_value.names[self._name_or_index] + return struct_value[name].name(name) + + +class StructAccessor( + bigframes.operations.base.SeriesMethods, vendoracessors.StructAccessor +): + __doc__ = vendoracessors.StructAccessor.__doc__ + + def field(self, name_or_index: str | int) -> bigframes.series.Series: + series = self._apply_unary_op(StructField(name_or_index)) + if isinstance(name_or_index, str): + name = name_or_index + else: + struct_field = self._dtype.pyarrow_dtype[name_or_index] + name = struct_field.name + return series.rename(name) + + def explode(self) -> bigframes.dataframe.DataFrame: + import bigframes.pandas + + pa_type = self._dtype.pyarrow_dtype + return bigframes.pandas.concat( + [self.field(i) for i in range(pa_type.num_fields)], axis="columns" + ) diff --git a/bigframes/series.py b/bigframes/series.py index 47298d59f50..84d737210ef 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -51,6 +51,7 @@ import bigframes.operations.base import bigframes.operations.datetimes as dt import bigframes.operations.strings as strings +import bigframes.operations.structs as structs import third_party.bigframes_vendored.pandas.core.series as vendored_pandas_series LevelType = typing.Union[str, int] @@ -118,6 +119,10 @@ def query_job(self) -> Optional[bigquery.QueryJob]: self._set_internal_query_job(self._compute_dry_run()) return self._query_job + @property + def struct(self) -> structs.StructAccessor: + return structs.StructAccessor(self._block) + def _set_internal_query_job(self, query_job: bigquery.QueryJob): self._query_job = query_job diff --git a/noxfile.py b/noxfile.py index 81ed9c2b2c4..a113e1fcde4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -362,7 +362,7 @@ def doctest(session: nox.sessions.Session): run_system( session=session, prefix_name="doctest", - extra_pytest_options=("--doctest-modules",), + extra_pytest_options=("--doctest-modules", "third_party"), test_folder="bigframes", check_cov=True, ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index a27d8fd487c..2880932d103 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -917,7 +917,19 @@ def test_get_dtypes_array_struct(session): dtypes = df.dtypes pd.testing.assert_series_equal( dtypes, - pd.Series({"array_column": np.dtype("O"), "struct_column": np.dtype("O")}), + pd.Series( + { + "array_column": np.dtype("O"), + "struct_column": pd.ArrowDtype( + pa.struct( + [ + ("string_field", pa.string()), + ("float_field", pa.float64()), + ] + ) + ), + } + ), ) diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index bb8ae570dca..3baff2e1f58 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -85,6 +85,70 @@ def test_ibis_float32_raises_unexpected_datatype(): bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_dtypes.float32) +IBIS_ARROW_DTYPES = ( + (ibis_dtypes.boolean, pa.bool_()), + (ibis_dtypes.date, pa.date32()), + (ibis_dtypes.Timestamp(), pa.timestamp("us")), + (ibis_dtypes.float64, pa.float64()), + ( + ibis_dtypes.Timestamp(timezone="UTC"), + pa.timestamp("us", tz="UTC"), + ), + ( + ibis_dtypes.Struct.from_tuples( + [ + ("name", ibis_dtypes.string()), + ("version", ibis_dtypes.int64()), + ] + ), + pa.struct( + [ + ("name", pa.string()), + ("version", pa.int64()), + ] + ), + ), + ( + ibis_dtypes.Struct.from_tuples( + [ + ( + "nested", + ibis_dtypes.Struct.from_tuples( + [ + ("field", ibis_dtypes.string()), + ] + ), + ), + ] + ), + pa.struct( + [ + ( + "nested", + pa.struct( + [ + ("field", pa.string()), + ] + ), + ), + ] + ), + ), +) + + +@pytest.mark.parametrize(("ibis_dtype", "arrow_dtype"), IBIS_ARROW_DTYPES) +def test_arrow_dtype_to_ibis_dtype(ibis_dtype, arrow_dtype): + result = bigframes.dtypes.arrow_dtype_to_ibis_dtype(arrow_dtype) + assert result == ibis_dtype + + +@pytest.mark.parametrize(("ibis_dtype", "arrow_dtype"), IBIS_ARROW_DTYPES) +def test_ibis_dtype_to_arrow_dtype(ibis_dtype, arrow_dtype): + result = bigframes.dtypes.ibis_dtype_to_arrow_dtype(ibis_dtype) + assert result == arrow_dtype + + @pytest.mark.parametrize( ["bigframes_dtype", "ibis_dtype"], [ diff --git a/third_party/bigframes_vendored/pandas/core/arrays/__init__.py b/third_party/bigframes_vendored/pandas/core/arrays/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/core/arrays/arrow/__init__.py b/third_party/bigframes_vendored/pandas/core/arrays/arrow/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py new file mode 100644 index 00000000000..8e3ea06a3d4 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py @@ -0,0 +1,94 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/arrays/arrow/accessors.py +"""Accessors for arrow-backed data.""" + +from __future__ import annotations + +from bigframes import constants + + +class StructAccessor: + """ + Accessor object for structured data properties of the Series values. + """ + + def field(self, name_or_index: str | int): + """ + Extract a child field of a struct as a Series. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pyarrow as pa + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... [ + ... {"version": 1, "project": "pandas"}, + ... {"version": 2, "project": "pandas"}, + ... {"version": 1, "project": "numpy"}, + ... ], + ... dtype=bpd.ArrowDtype(pa.struct( + ... [("version", pa.int64()), ("project", pa.string())] + ... )) + ... ) + + Extract by field name. + + >>> s.struct.field("project") + 0 pandas + 1 pandas + 2 numpy + Name: project, dtype: string + + Extract by field index. + + >>> s.struct.field(0) + 0 1 + 1 2 + 2 1 + Name: version, dtype: Int64 + + Args: + name_or_index: + Name (str) or index (int) of the child field to extract. + + Returns: + Series: + The data corresponding to the selected child field. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def explode(self): + """ + Extract all child fields of a struct as a DataFrame. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pyarrow as pa + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... [ + ... {"version": 1, "project": "pandas"}, + ... {"version": 2, "project": "pandas"}, + ... {"version": 1, "project": "numpy"}, + ... ], + ... dtype=bpd.ArrowDtype(pa.struct( + ... [("version", pa.int64()), ("project", pa.string())] + ... )) + ... ) + + Extract all child fields. + + >>> s.struct.explode() + version project + 0 1 pandas + 1 2 pandas + 2 1 numpy + + [3 rows x 2 columns] + + Returns: + DataFrame: + The data corresponding to all child fields. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/__init__.py b/third_party/bigframes_vendored/sklearn/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/sklearn/ensemble/__init__.py b/third_party/bigframes_vendored/sklearn/ensemble/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/xgboost/__init__.py b/third_party/bigframes_vendored/xgboost/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 781307ec22d31a7657f8ee5c6eedc0e419450ccd Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 3 Oct 2023 16:12:52 -0700 Subject: [PATCH 055/921] feat: add idxmin, idxmax to series, dataframe (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add idxmin, idxmax to series, dataframe * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 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 --- bigframes/core/block_transforms.py | 51 +++++++++++++++++++ bigframes/dataframe.py | 6 +++ bigframes/series.py | 28 ++++++++++ tests/system/small/test_dataframe.py | 28 ++++++++++ tests/system/small/test_multiindex.py | 11 ++++ tests/system/small/test_series.py | 12 +++++ .../bigframes_vendored/pandas/core/frame.py | 22 ++++++++ .../bigframes_vendored/pandas/core/series.py | 40 +++++++++------ 8 files changed, 182 insertions(+), 16 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 6e99a7c7740..30c79029817 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -17,6 +17,7 @@ import pandas as pd +import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.ordering as ordering @@ -576,3 +577,53 @@ def align_columns( left_final = left_block.select_columns(left_column_ids) right_final = right_block.select_columns(right_column_ids) return left_final, right_final + + +def idxmin(block: blocks.Block) -> blocks.Block: + return _idx_extrema(block, "min") + + +def idxmax(block: blocks.Block) -> blocks.Block: + return _idx_extrema(block, "max") + + +def _idx_extrema( + block: blocks.Block, min_or_max: typing.Literal["min", "max"] +) -> blocks.Block: + if len(block.index_columns) != 1: + # TODO: Need support for tuple dtype + raise NotImplementedError( + f"idxmin not support for multi-index. {constants.FEEDBACK_LINK}" + ) + + original_block = block + result_cols = [] + for value_col in original_block.value_columns: + direction = ( + ordering.OrderingDirection.ASC + if min_or_max == "min" + else ordering.OrderingDirection.DESC + ) + # Have to find the min for each + order_refs = [ + ordering.OrderingColumnReference(value_col, direction), + *[ + ordering.OrderingColumnReference(idx_col) + for idx_col in original_block.index_columns + ], + ] + window_spec = core.WindowSpec(ordering=order_refs) + idx_col = original_block.index_columns[0] + block, result_col = block.apply_window_op( + idx_col, agg_ops.first_op, window_spec + ) + result_cols.append(result_col) + + block = block.select_columns(result_cols).with_column_labels( + original_block.column_labels + ) + # Stack the entire column axis to produce single-column result + # Assumption: uniform dtype for stackability + return block.aggregate_all_and_stack( + agg_ops.AnyValueOp(), dtype=block.dtypes[0] + ).with_column_labels([original_block.index.name]) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 345afbe6e6b..eea8beb130a 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1642,6 +1642,12 @@ def agg( aggregate = agg + def idxmin(self) -> bigframes.series.Series: + return bigframes.series.Series(block_ops.idxmin(self._block)) + + def idxmax(self) -> bigframes.series.Series: + return bigframes.series.Series(block_ops.idxmax(self._block)) + def describe(self) -> DataFrame: df_numeric = self._drop_non_numeric(keep_bool=False) if len(df_numeric.columns) == 0: diff --git a/bigframes/series.py b/bigframes/series.py index 84d737210ef..8815a6abdee 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -887,6 +887,34 @@ def argmin(self) -> int: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) + def idxmax(self) -> blocks.Label: + block = self._block.order_by( + [ + OrderingColumnReference( + self._value_column, direction=OrderingDirection.DESC + ), + *[ + OrderingColumnReference(idx_col) + for idx_col in self._block.index_columns + ], + ] + ) + block = block.slice(0, 1) + return indexes.Index._from_block(block).to_pandas()[0] + + def idxmin(self) -> blocks.Label: + block = self._block.order_by( + [ + OrderingColumnReference(self._value_column), + *[ + OrderingColumnReference(idx_col) + for idx_col in self._block.index_columns + ], + ] + ) + block = block.slice(0, 1) + return indexes.Index._from_block(block).to_pandas()[0] + @property def is_monotonic_increasing(self) -> bool: return typing.cast( diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 2880932d103..f3e7a0c0e9d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1292,6 +1292,34 @@ def test_df_update(overwrite, filter_func): pd.testing.assert_frame_equal(bf_df1.to_pandas(), pd_df1) +def test_df_idxmin(): + pd_df = pd.DataFrame( + {"a": [1, 2, 3], "b": [7, None, 3], "c": [4, 4, 4]}, index=["x", "y", "z"] + ) + bf_df = dataframe.DataFrame(pd_df) + + bf_result = bf_df.idxmin().to_pandas() + pd_result = pd_df.idxmin() + + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_dtype=False + ) + + +def test_df_idxmax(): + pd_df = pd.DataFrame( + {"a": [1, 2, 3], "b": [7, None, 3], "c": [4, 4, 4]}, index=["x", "y", "z"] + ) + bf_df = dataframe.DataFrame(pd_df) + + bf_result = bf_df.idxmax().to_pandas() + pd_result = pd_df.idxmax() + + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_dtype=False + ) + + @pytest.mark.parametrize( ("join", "axis"), [ diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 10f2a74b21b..19f1c557ef2 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -41,6 +41,17 @@ def test_reset_multi_index(scalars_df_index, scalars_pandas_df_index): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_series_multi_index_idxmin(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index(["bool_col", "int64_too"])[ + "float64_col" + ].idxmin() + pd_result = scalars_pandas_df_index.set_index(["bool_col", "int64_too"])[ + "float64_col" + ].idxmin() + + assert bf_result == pd_result + + def test_binop_series_series_matching_multi_indices( scalars_df_index, scalars_pandas_df_index ): diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 588dcc2c83a..645638fb3aa 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2468,6 +2468,18 @@ def test_argmax(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result +def test_series_idxmin(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.string_col.idxmin() + pd_result = scalars_pandas_df_index.string_col.idxmin() + assert bf_result == pd_result + + +def test_series_idxmax(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.int64_too.idxmax() + pd_result = scalars_pandas_df_index.int64_too.idxmax() + assert bf_result == pd_result + + def test_getattr_attribute_error_when_pandas_has(scalars_df_index): # asof is implemented in pandas but not in bigframes with pytest.raises(AttributeError): diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 11aaf84b14e..17d941fbdd0 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1805,6 +1805,28 @@ def nsmallest(self, n: int, columns, keep: str = "first"): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def idxmin(self): + """ + Return index of first occurrence of minimum over requested axis. + + NA/null values are excluded. + + Returns: + Series: Indexes of minima along the specified axis. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def idxmax(self): + """ + Return index of first occurrence of maximum over requested axis. + + NA/null values are excluded. + + Returns: + Series: Indexes of maxima along the specified axis. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def nunique(self): """ Count number of distinct elements in specified axis. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index d58c1ccc3b4..a41a3454ca4 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -8,7 +8,6 @@ import numpy as np from pandas._libs import lib from pandas._typing import Axis, FilePath, NaPosition, WriteBuffer -import pandas.io.formats.format as fmt from bigframes import constants from third_party.bigframes_vendored.pandas.core.generic import NDFrame @@ -151,21 +150,6 @@ def to_string( str or None: String representation of Series if ``buf=None``, otherwise None. """ - formatter = fmt.SeriesFormatter( - self, - name=name, - length=length, - header=header, - index=index, - dtype=dtype, - na_rep=na_rep, - float_format=float_format, - min_rows=min_rows, - max_rows=max_rows, - ) - result = formatter.to_string() - - # catch contract violations raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def to_markdown( @@ -475,6 +459,30 @@ def duplicated(self, keep="first") -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def idxmin(self) -> Hashable: + """ + Return the row label of the minimum value. + + If multiple values equal the minimum, the first row label with that + value is returned. + + Returns: + Index: Label of the minimum value. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def idxmax(self) -> Hashable: + """ + Return the row label of the maximum value. + + If multiple values equal the maximum, the first row label with that + value is returned. + + Returns: + Index: Label of the maximum value. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def round(self, decimals: int = 0) -> Series: """ Round each value in a Series to the given number of decimals. From fff3d45f03ffbc7bb23143a1572e3dd157463ca9 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 3 Oct 2023 18:57:06 -0700 Subject: [PATCH 056/921] fix: change return type of `Series.loc[scalar]` (#40) * bug: change return type of `Series.loc[scalar]` Change-Id: Id60a7da3021972da5c8a28fb8f3620e10643c0ed * add scalar case and update return types * remove unneeded iloc in series getitem test * fix test_series_get_with_default_index * Run query manual for clarity/redundance --------- Co-authored-by: Henry J Solberg --- bigframes/core/indexers.py | 81 ++++++++++++++++++---------- bigframes/ml/model_selection.py | 8 +-- tests/system/small/test_dataframe.py | 2 +- tests/system/small/test_series.py | 6 +-- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index e22a5aed857..1a88b2abd60 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -15,7 +15,7 @@ from __future__ import annotations import typing -from typing import Tuple +from typing import Tuple, Union import ibis import pandas as pd @@ -29,20 +29,19 @@ import bigframes.series if typing.TYPE_CHECKING: - LocSingleKey = typing.Union[bigframes.series.Series, indexes.Index, slice] + LocSingleKey = Union[ + bigframes.series.Series, indexes.Index, slice, bigframes.core.scalar.Scalar + ] class LocSeriesIndexer: def __init__(self, series: bigframes.series.Series): self._series = series - def __getitem__(self, key) -> bigframes.series.Series: - """ - Only indexing by a boolean bigframes.series.Series or list of index entries is currently supported - """ - return typing.cast( - bigframes.series.Series, _loc_getitem_series_or_dataframe(self._series, key) - ) + def __getitem__( + self, key + ) -> Union[bigframes.core.scalar.Scalar, bigframes.series.Series]: + return _loc_getitem_series_or_dataframe(self._series, key) def __setitem__(self, key, value) -> None: # TODO(swast): support MultiIndex @@ -84,7 +83,7 @@ def __init__(self, series: bigframes.series.Series): def __getitem__( self, key - ) -> bigframes.core.scalar.Scalar | bigframes.series.Series: + ) -> Union[bigframes.core.scalar.Scalar, bigframes.series.Series]: """ Index series using integer offsets. Currently supports index by key type: @@ -103,13 +102,17 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame): self._dataframe = dataframe @typing.overload - def __getitem__(self, key: LocSingleKey) -> bigframes.dataframe.DataFrame: + def __getitem__( + self, key: LocSingleKey + ) -> Union[bigframes.dataframe.DataFrame, pd.Series]: ... # Technically this is wrong since we can have duplicate column labels, but # this is expected to be rare. @typing.overload - def __getitem__(self, key: Tuple[LocSingleKey, str]) -> bigframes.series.Series: + def __getitem__( + self, key: Tuple[LocSingleKey, str] + ) -> Union[bigframes.series.Series, bigframes.core.scalar.Scalar]: ... def __getitem__(self, key): @@ -173,7 +176,7 @@ class ILocDataFrameIndexer: def __init__(self, dataframe: bigframes.dataframe.DataFrame): self._dataframe = dataframe - def __getitem__(self, key) -> bigframes.dataframe.DataFrame | pd.Series: + def __getitem__(self, key) -> Union[bigframes.dataframe.DataFrame, pd.Series]: """ Index dataframe using integer offsets. Currently supports index by key type: @@ -188,21 +191,26 @@ def __getitem__(self, key) -> bigframes.dataframe.DataFrame | pd.Series: @typing.overload def _loc_getitem_series_or_dataframe( series_or_dataframe: bigframes.series.Series, key -) -> bigframes.series.Series: +) -> Union[bigframes.core.scalar.Scalar, bigframes.series.Series]: ... @typing.overload def _loc_getitem_series_or_dataframe( series_or_dataframe: bigframes.dataframe.DataFrame, key -) -> bigframes.dataframe.DataFrame: +) -> Union[bigframes.dataframe.DataFrame, pd.Series]: ... def _loc_getitem_series_or_dataframe( - series_or_dataframe: bigframes.dataframe.DataFrame | bigframes.series.Series, + series_or_dataframe: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], key: LocSingleKey, -) -> bigframes.dataframe.DataFrame | bigframes.series.Series: +) -> Union[ + bigframes.dataframe.DataFrame, + bigframes.series.Series, + pd.Series, + bigframes.core.scalar.Scalar, +]: if isinstance(key, bigframes.series.Series) and key.dtype == "boolean": return series_or_dataframe[key] elif isinstance(key, bigframes.series.Series): @@ -222,7 +230,7 @@ def _loc_getitem_series_or_dataframe( # TODO(henryjsolberg): support MultiIndex if len(key) == 0: # type: ignore return typing.cast( - typing.Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + Union[bigframes.dataframe.DataFrame, bigframes.series.Series], series_or_dataframe.iloc[0:0], ) @@ -258,11 +266,22 @@ def _loc_getitem_series_or_dataframe( ) keys_df = keys_df.set_index(index_name, drop=True) keys_df.index.name = None - return _perform_loc_list_join(series_or_dataframe, keys_df) + result = _perform_loc_list_join(series_or_dataframe, keys_df) + pandas_result = result.to_pandas() + # although loc[scalar_key] returns multiple results when scalar_key + # is not unique, we download the results here and return the computed + # individual result (as a scalar or pandas series) when the key is unique, + # since we expect unique index keys to be more common. loc[[scalar_key]] + # can be used to retrieve one-item DataFrames or Series. + if len(pandas_result) == 1: + return pandas_result.iloc[0] + # when the key is not unique, we return a bigframes data type + # as usual for methods that return dataframes/series + return result else: raise TypeError( - "Invalid argument type. loc currently only supports indexing with a " - "boolean bigframes Series, a list of index entries or a single index entry. " + "Invalid argument type. Expected bigframes.Series, bigframes.Index, " + "list, : (empty slice), or scalar. " f"{constants.FEEDBACK_LINK}" ) @@ -284,9 +303,9 @@ def _perform_loc_list_join( def _perform_loc_list_join( - series_or_dataframe: bigframes.dataframe.DataFrame | bigframes.series.Series, + series_or_dataframe: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], keys_df: bigframes.dataframe.DataFrame, -) -> bigframes.series.Series | bigframes.dataframe.DataFrame: +) -> Union[bigframes.series.Series, bigframes.dataframe.DataFrame]: # right join based on the old index so that the matching rows from the user's # original dataframe will be duplicated and reordered appropriately original_index_names = series_or_dataframe.index.names @@ -309,20 +328,26 @@ def _perform_loc_list_join( @typing.overload def _iloc_getitem_series_or_dataframe( series_or_dataframe: bigframes.series.Series, key -) -> bigframes.series.Series | bigframes.core.scalar.Scalar: +) -> Union[bigframes.series.Series, bigframes.core.scalar.Scalar]: ... @typing.overload def _iloc_getitem_series_or_dataframe( series_or_dataframe: bigframes.dataframe.DataFrame, key -) -> bigframes.dataframe.DataFrame | pd.Series: +) -> Union[bigframes.dataframe.DataFrame, pd.Series]: ... def _iloc_getitem_series_or_dataframe( - series_or_dataframe: bigframes.dataframe.DataFrame | bigframes.series.Series, key -) -> bigframes.dataframe.DataFrame | bigframes.series.Series | bigframes.core.scalar.Scalar | pd.Series: + series_or_dataframe: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + key, +) -> Union[ + bigframes.dataframe.DataFrame, + bigframes.series.Series, + bigframes.core.scalar.Scalar, + pd.Series, +]: if isinstance(key, int): internal_slice_result = series_or_dataframe._slice(key, key + 1, 1) result_pd_df = internal_slice_result.to_pandas() @@ -334,7 +359,7 @@ def _iloc_getitem_series_or_dataframe( elif pd.api.types.is_list_like(key): if len(key) == 0: return typing.cast( - typing.Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + Union[bigframes.dataframe.DataFrame, bigframes.series.Series], series_or_dataframe.iloc[0:0], ) df = series_or_dataframe diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index 110cbcf4934..443b9e7be6e 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -17,6 +17,7 @@ https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection.""" +import typing from typing import List, Union from bigframes.ml import utils @@ -79,9 +80,10 @@ def train_test_split( train_index = split_dfs[0].index test_index = split_dfs[1].index - split_dfs += [ - df.loc[index] for df in dfs[1:] for index in (train_index, test_index) - ] + split_dfs += typing.cast( + List[bpd.DataFrame], + [df.loc[index] for df in dfs[1:] for index in (train_index, test_index)], + ) # convert back to Series. results: List[Union[bpd.DataFrame, bpd.Series]] = [] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index f3e7a0c0e9d..b8616a54d65 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2081,7 +2081,7 @@ def test_loc_single_index_no_duplicate(scalars_df_index, scalars_pandas_df_index bf_result = scalars_df_index.loc[index] pd_result = scalars_pandas_df_index.loc[index] pd.testing.assert_series_equal( - bf_result.to_pandas().iloc[0, :], + bf_result, pd_result, ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 645638fb3aa..8c1c36720bf 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -118,7 +118,7 @@ def test_series_get_with_default_index(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df[col_name].get(key) pd_result = scalars_pandas_df[col_name].get(key) - assert bf_result.to_pandas().iloc[0] == pd_result + assert bf_result == pd_result @pytest.mark.parametrize( @@ -157,7 +157,7 @@ def test_series___getitem___with_default_index(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df[col_name][key] pd_result = scalars_pandas_df[col_name][key] - assert bf_result.to_pandas().iloc[0] == pd_result + assert bf_result == pd_result @pytest.mark.parametrize( @@ -2652,7 +2652,7 @@ def test_loc_single_index_no_duplicate(scalars_df_index, scalars_pandas_df_index index = -2345 bf_result = scalars_df_index.date_col.loc[index] pd_result = scalars_pandas_df_index.date_col.loc[index] - assert bf_result.to_pandas().iloc[0] == pd_result + assert bf_result == pd_result def test_series_bool_interpretation_error(scalars_df_index): From 24c625638984f6a84191c7a4c8ac9fb6c3cf1dca Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 4 Oct 2023 07:20:45 -0700 Subject: [PATCH 057/921] feat: add ml.preprocessing.KBinsDiscretizer (#81) * feat: add ml.preprocessing.KBinsDiscretizer * fix: address all the comments * fix: address additional comments * fix: fix the failed test * Empty commit * Trigger Kokoro --- bigframes/ml/compose.py | 13 +- bigframes/ml/pipeline.py | 12 +- bigframes/ml/preprocessing.py | 152 ++++++++++++++++-- bigframes/ml/sql.py | 9 ++ tests/system/large/ml/test_pipeline.py | 46 ++++++ tests/system/small/ml/test_preprocessing.py | 131 ++++++++++++++- tests/unit/ml/test_compose.py | 26 +++ tests/unit/ml/test_sql.py | 7 + .../sklearn/preprocessing/_discretization.py | 47 ++++++ 9 files changed, 426 insertions(+), 17 deletions(-) create mode 100644 third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 9effbf1968c..bf046ff6914 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -31,6 +31,7 @@ preprocessing.StandardScaler, preprocessing.MaxAbsScaler, preprocessing.MinMaxScaler, + preprocessing.KBinsDiscretizer, preprocessing.LabelEncoder, ] @@ -91,18 +92,24 @@ def transformers_( return result - def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + def _compile_to_sql( + self, + columns: List[str], + X: bpd.DataFrame, + ) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: columns (List[str]): a list of column names to transform + X (bpd.DataFrame): + The Dataframe with training data. Returns: a list of tuples of (sql_expression, output_name)""" return [ - transformer._compile_to_sql([column])[0] + transformer._compile_to_sql([column], X=X)[0] for column in columns for _, transformer, target_column in self.transformers_ if column == target_column @@ -115,7 +122,7 @@ def fit( ) -> ColumnTransformer: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist()) + compiled_transforms = self._compile_to_sql(X.columns.tolist(), X) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] self._bqml_model = self._bqml_model_factory.create_model( diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index ac02c391129..ad0b3fae111 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -52,6 +52,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): preprocessing.OneHotEncoder, preprocessing.MaxAbsScaler, preprocessing.MinMaxScaler, + preprocessing.KBinsDiscretizer, preprocessing.LabelEncoder, ), ): @@ -93,7 +94,7 @@ def fit( ) -> Pipeline: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._transform._compile_to_sql(X.columns.tolist()) + compiled_transforms = self._transform._compile_to_sql(X.columns.tolist(), X=X) transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] if y is not None: @@ -151,6 +152,7 @@ def _extract_as_column_transformer( preprocessing.StandardScaler, preprocessing.MaxAbsScaler, preprocessing.MinMaxScaler, + preprocessing.KBinsDiscretizer, preprocessing.LabelEncoder, ], Union[str, List[str]], @@ -190,6 +192,13 @@ def _extract_as_column_transformer( *preprocessing.MinMaxScaler._parse_from_sql(transform_sql), ) ) + elif transform_sql.startswith("ML.BUCKETIZE"): + transformers.append( + ( + "k_bins_discretizer", + *preprocessing.KBinsDiscretizer._parse_from_sql(transform_sql), + ) + ) elif transform_sql.startswith("ML.LABEL_ENCODER"): transformers.append( ( @@ -213,6 +222,7 @@ def _merge_column_transformer( preprocessing.OneHotEncoder, preprocessing.MaxAbsScaler, preprocessing.MinMaxScaler, + preprocessing.KBinsDiscretizer, preprocessing.LabelEncoder, ]: """Try to merge the column transformer to a simple transformer.""" diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index caf4657a633..5f44d402184 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -23,6 +23,7 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.preprocessing._data +import third_party.bigframes_vendored.sklearn.preprocessing._discretization import third_party.bigframes_vendored.sklearn.preprocessing._encoder import third_party.bigframes_vendored.sklearn.preprocessing._label @@ -44,12 +45,15 @@ def __init__(self): def __eq__(self, other: Any) -> bool: return type(other) is StandardScaler and self._bqml_model == other._bqml_model - def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: a list of column names to transform + columns: + a list of column names to transform. + X (default None): + Ignored. Returns: a list of tuples of (sql_expression, output_name)""" return [ @@ -124,12 +128,15 @@ def __init__(self): def __eq__(self, other: Any) -> bool: return type(other) is MaxAbsScaler and self._bqml_model == other._bqml_model - def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: a list of column names to transform + columns: + a list of column names to transform. + X (default None): + Ignored. Returns: a list of tuples of (sql_expression, output_name)""" return [ @@ -204,12 +211,15 @@ def __init__(self): def __eq__(self, other: Any) -> bool: return type(other) is MinMaxScaler and self._bqml_model == other._bqml_model - def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: a list of column names to transform + columns: + a list of column names to transform. + X (default None): + Ignored. Returns: a list of tuples of (sql_expression, output_name)""" return [ @@ -267,6 +277,124 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +class KBinsDiscretizer( + base.Transformer, + third_party.bigframes_vendored.sklearn.preprocessing._discretization.KBinsDiscretizer, +): + __doc__ = ( + third_party.bigframes_vendored.sklearn.preprocessing._discretization.KBinsDiscretizer.__doc__ + ) + + def __init__( + self, + n_bins: int = 5, + strategy: Literal["uniform", "quantile"] = "quantile", + ): + if strategy != "uniform": + raise NotImplementedError( + f"Only strategy = 'uniform' is supported now, input is {strategy}." + ) + if n_bins < 2: + raise ValueError( + f"n_bins has to be larger than or equal to 2, input is {n_bins}." + ) + self.n_bins = n_bins + self.strategy = strategy + self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return ( + type(other) is KBinsDiscretizer + and self.n_bins == other.n_bins + and self._bqml_model == other._bqml_model + ) + + def _compile_to_sql( + self, + columns: List[str], + X: bpd.DataFrame, + ) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: + a list of column names to transform + X: + The Dataframe with training data. + + Returns: a list of tuples of (sql_expression, output_name)""" + array_split_points = {} + if self.strategy == "uniform": + for column in columns: + min_value = X[column].min() + max_value = X[column].max() + bin_size = (max_value - min_value) / self.n_bins + array_split_points[column] = [ + min_value + i * bin_size for i in range(self.n_bins - 1) + ] + + return [ + ( + self._base_sql_generator.ml_bucketize( + column, array_split_points[column], f"kbinsdiscretizer_{column}" + ), + f"kbinsdiscretizer_{column}", + ) + for column in columns + ] + + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[KBinsDiscretizer, str]: + """Parse SQL to tuple(KBinsDiscretizer, column_label). + + Args: + sql: SQL string of format "ML.BUCKETIZE({col_label}, array_split_points, FALSE) OVER()" + + Returns: + tuple(KBinsDiscretizer, column_label)""" + s = sql[sql.find("(") + 1 : sql.find(")")] + array_split_points = s[s.find("[") + 1 : s.find("]")] + col_label = s[: s.find(",")] + n_bins = array_split_points.count(",") + 2 + return cls(n_bins, "uniform"), col_label + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> KBinsDiscretizer: + (X,) = utils.convert_to_dataframe(X) + + compiled_transforms = self._compile_to_sql(X.columns.tolist(), X) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = self._bqml_model_factory.create_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + return self + + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + (X,) = utils.convert_to_dataframe(X) + + df = self._bqml_model.transform(X) + return typing.cast( + bpd.DataFrame, + df[self._output_names], + ) + + class OneHotEncoder( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, @@ -308,13 +436,15 @@ def __eq__(self, other: Any) -> bool: and self.max_categories == other.max_categories ) - def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: columns: - a list of column names to transform + a list of column names to transform. + X (default None): + Ignored. Returns: a list of tuples of (sql_expression, output_name)""" @@ -432,13 +562,15 @@ def __eq__(self, other: Any) -> bool: and self.max_categories == other.max_categories ) - def _compile_to_sql(self, columns: List[str]) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: columns: - a list of column names to transform + a list of column names to transform. + X (default None): + Ignored. Returns: a list of tuples of (sql_expression, output_name)""" diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 57c8ba672af..601b2710999 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -85,6 +85,15 @@ def ml_min_max_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.MIN_MAX_SCALER for BQML""" return f"""ML.MIN_MAX_SCALER({numeric_expr_sql}) OVER() AS {name}""" + def ml_bucketize( + self, + numeric_expr_sql: str, + array_split_points: Iterable[Union[int, float]], + name: str, + ) -> str: + """Encode ML.MIN_MAX_SCALER for BQML""" + return f"""ML.BUCKETIZE({numeric_expr_sql}, {array_split_points}, FALSE) AS {name}""" + def ml_one_hot_encoder( self, numeric_expr_sql: str, diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 34a2ca01011..9294740dd67 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -580,6 +580,11 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind preprocessing.MinMaxScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "k_bins_discretizer", + preprocessing.KBinsDiscretizer(strategy="uniform"), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -657,6 +662,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id preprocessing.MinMaxScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "k_bins_discretizer", + preprocessing.KBinsDiscretizer(strategy="uniform"), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -696,9 +706,19 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id ("standard_scaler", preprocessing.StandardScaler(), "culmen_length_mm"), ("max_abs_scaler", preprocessing.MaxAbsScaler(), "culmen_length_mm"), ("min_max_scaler", preprocessing.MinMaxScaler(), "culmen_length_mm"), + ( + "k_bins_discretizer", + preprocessing.KBinsDiscretizer(strategy="uniform"), + "culmen_length_mm", + ), ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), ("max_abs_scaler", preprocessing.MaxAbsScaler(), "flipper_length_mm"), ("min_max_scaler", preprocessing.MinMaxScaler(), "flipper_length_mm"), + ( + "k_bins_discretizer", + preprocessing.KBinsDiscretizer(strategy="uniform"), + "flipper_length_mm", + ), ] assert transformers == expected @@ -791,6 +811,32 @@ def test_pipeline_min_max_scaler_to_gbq(penguins_df_default_index, dataset_id): assert pl_loaded._estimator.fit_intercept is False +def test_pipeline_k_bins_discretizer_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ("transform", preprocessing.KBinsDiscretizer(strategy="uniform")), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "culmen_length_mm", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_k_bins_discretizer", replace=True + ) + assert isinstance(pl_loaded._transform, preprocessing.KBinsDiscretizer) + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False + + def test_pipeline_one_hot_encoder_to_gbq(penguins_df_default_index, dataset_id): pl = pipeline.Pipeline( [ diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index fc8f3251bda..45548acca32 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -121,7 +121,7 @@ def test_standard_scaler_series_normalizes(penguins_df_default_index, new_pengui def test_max_abs_scaler_normalizes(penguins_df_default_index, new_penguins_df): - # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.MaxAbsScaler, when BQML's change is in prod. scaler = bigframes.ml.preprocessing.MaxAbsScaler() scaler.fit( penguins_df_default_index[ @@ -211,7 +211,7 @@ def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguin pd.testing.assert_frame_equal(result, expected, rtol=1e-3) -def test_min_max_scaler_normalizeds_fit_transform(new_penguins_df): +def test_min_max_scaler_normalized_fit_transform(new_penguins_df): scaler = bigframes.ml.preprocessing.MinMaxScaler() result = scaler.fit_transform( new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] @@ -265,7 +265,7 @@ def test_min_max_scaler_series_normalizes(penguins_df_default_index, new_penguin def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): - # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.MinMaxScaler, when BQML's change is in prod. scaler = bigframes.ml.preprocessing.MinMaxScaler() scaler.fit( penguins_df_default_index[ @@ -304,6 +304,131 @@ def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_k_bins_discretizer_normalized_fit_transform_default_params(new_penguins_df): + discretizer = bigframes.ml.preprocessing.KBinsDiscretizer(strategy="uniform") + result = discretizer.fit_transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "kbinsdiscretizer_culmen_depth_mm": ["bin_5", "bin_2", "bin_4"], + "kbinsdiscretizer_culmen_length_mm": ["bin_5", "bin_3", "bin_2"], + "kbinsdiscretizer_flipper_length_mm": ["bin_5", "bin_2", "bin_4"], + }, + dtype="string[pyarrow]", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_k_bins_discretizer_series_normalizes( + penguins_df_default_index, new_penguins_df +): + discretizer = bigframes.ml.preprocessing.KBinsDiscretizer(strategy="uniform") + discretizer.fit(penguins_df_default_index["culmen_length_mm"]) + + result = discretizer.transform( + penguins_df_default_index["culmen_length_mm"] + ).to_pandas() + result = discretizer.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "kbinsdiscretizer_culmen_length_mm": ["bin_3", "bin_3", "bin_3"], + }, + dtype="string[pyarrow]", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_k_bins_discretizer_normalizes(penguins_df_default_index, new_penguins_df): + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.KBinsDiscretizer, when BQML's change is in prod. + discretizer = bigframes.ml.preprocessing.KBinsDiscretizer(strategy="uniform") + discretizer.fit( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ) + + result = discretizer.transform( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ).to_pandas() + + result = discretizer.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "kbinsdiscretizer_culmen_depth_mm": ["bin_5", "bin_4", "bin_4"], + "kbinsdiscretizer_culmen_length_mm": ["bin_3", "bin_3", "bin_3"], + "kbinsdiscretizer_flipper_length_mm": ["bin_4", "bin_2", "bin_3"], + }, + dtype="string[pyarrow]", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + +def test_k_bins_discretizer_normalizes_different_params( + penguins_df_default_index, new_penguins_df +): + # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.KBinsDiscretizer, when BQML's change is in prod. + discretizer = bigframes.ml.preprocessing.KBinsDiscretizer( + n_bins=6, strategy="uniform" + ) + discretizer.fit( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ) + + result = discretizer.transform( + penguins_df_default_index[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ).to_pandas() + + result = discretizer.transform(new_penguins_df).to_pandas() + + # TODO: bug? feature columns seem to be in nondeterministic random order + # workaround: sort columns by name. Can't repro it in pantheon, so could + # be a bigframes issue... + result = result.reindex(sorted(result.columns), axis=1) + + expected = pd.DataFrame( + { + "kbinsdiscretizer_culmen_depth_mm": ["bin_6", "bin_4", "bin_5"], + "kbinsdiscretizer_culmen_length_mm": ["bin_3", "bin_3", "bin_3"], + "kbinsdiscretizer_flipper_length_mm": ["bin_4", "bin_2", "bin_3"], + }, + dtype="string[pyarrow]", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + + def test_one_hot_encoder_default_params(new_penguins_df): encoder = bigframes.ml.preprocessing.OneHotEncoder() encoder.fit(new_penguins_df[["species", "sex"]]) diff --git a/tests/unit/ml/test_compose.py b/tests/unit/ml/test_compose.py index 8c8fbd6ab54..60dcc75b63c 100644 --- a/tests/unit/ml/test_compose.py +++ b/tests/unit/ml/test_compose.py @@ -23,6 +23,7 @@ def test_columntransformer_init_expectedtransforms(): standard_scaler_transformer = preprocessing.StandardScaler() max_abs_scaler_transformer = preprocessing.MaxAbsScaler() min_max_scaler_transformer = preprocessing.MinMaxScaler() + k_bins_discretizer_transformer = preprocessing.KBinsDiscretizer(strategy="uniform") label_transformer = preprocessing.LabelEncoder() column_transformer = compose.ColumnTransformer( [ @@ -42,6 +43,11 @@ def test_columntransformer_init_expectedtransforms(): min_max_scaler_transformer, ["culmen_length_mm", "flipper_length_mm"], ), + ( + "k_bins_discretizer", + k_bins_discretizer_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), ("label", label_transformer, "species"), ] ) @@ -54,6 +60,8 @@ def test_columntransformer_init_expectedtransforms(): ("max_abs_scale", max_abs_scaler_transformer, "flipper_length_mm"), ("min_max_scale", min_max_scaler_transformer, "culmen_length_mm"), ("min_max_scale", min_max_scaler_transformer, "flipper_length_mm"), + ("k_bins_discretizer", k_bins_discretizer_transformer, "culmen_length_mm"), + ("k_bins_discretizer", k_bins_discretizer_transformer, "flipper_length_mm"), ("label", label_transformer, "species"), ] @@ -81,6 +89,11 @@ def test_columntransformer_repr(): preprocessing.MinMaxScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "k_bins_discretizer", + preprocessing.KBinsDiscretizer(strategy="uniform"), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) @@ -92,6 +105,9 @@ def test_columntransformer_repr(): ('max_abs_scale', MaxAbsScaler(), ['culmen_length_mm', 'flipper_length_mm']), ('min_max_scale', MinMaxScaler(), + ['culmen_length_mm', 'flipper_length_mm']), + ('k_bins_discretizer', + KBinsDiscretizer(strategy='uniform'), ['culmen_length_mm', 'flipper_length_mm'])])""" ) @@ -119,6 +135,11 @@ def test_columntransformer_repr_matches_sklearn(): preprocessing.MinMaxScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "k_bins_discretizer", + preprocessing.KBinsDiscretizer(strategy="uniform"), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) sk_column_transformer = sklearn_compose.ColumnTransformer( @@ -143,6 +164,11 @@ def test_columntransformer_repr_matches_sklearn(): sklearn_preprocessing.MinMaxScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "k_bins_discretizer", + sklearn_preprocessing.KBinsDiscretizer(strategy="uniform"), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index a3338e762d4..34a02edd424 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -95,6 +95,13 @@ def test_min_max_scaler_produces_correct_sql( assert sql == "ML.MIN_MAX_SCALER(col_a) OVER() AS scaled_col_a" +def test_k_bins_discretizer_produces_correct_sql( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_bucketize("col_a", [1, 2, 3, 4], "scaled_col_a") + assert sql == "ML.BUCKETIZE(col_a, [1, 2, 3, 4], FALSE) AS scaled_col_a" + + def test_one_hot_encoder_produces_correct_sql( base_sql_generator: ml_sql.BaseSqlGenerator, ): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py new file mode 100644 index 00000000000..0236558dd4b --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py @@ -0,0 +1,47 @@ +# Author: Henry Lin +# Tom Dupré la Tour + +# License: BSD + +from bigframes import constants +from third_party.bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin + + +class KBinsDiscretizer(TransformerMixin, BaseEstimator): + """ + Bin continuous data into intervals. + + Args: + n_bins (int, default 5): + The number of bins to produce. Raises ValueError if ``n_bins < 2``. + strategy ({'uniform', 'quantile'}, default='quantile'): + Strategy used to define the widths of the bins. 'uniform': All bins + in each feature have identical widths. 'quantile': All bins in each + feature have the same number of points. Only `uniform` is supported now. + """ + + def fit(self, X, y=None): + """Fit the estimator. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Dataframe or Series with training data. + + y (default None): + Ignored. + + Returns: + KBinsDiscretizer: Fitted scaler. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transform(self, X): + """Discretize the data. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. + + Returns: + bigframes.dataframe.DataFrame: Transformed result.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From bd692d847567d9c31cfc7d652ed8f7d5e7aeaa64 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 08:48:03 -0700 Subject: [PATCH 058/921] chore(main): release 0.6.0 (#72) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 19 +++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b2bff3c71..880f7916250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.6.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.5.0...v0.6.0) (2023-10-04) + + +### Features + +* Add df.unstack ([#63](https://github.com/googleapis/python-bigquery-dataframes/issues/63)) ([4a84714](https://github.com/googleapis/python-bigquery-dataframes/commit/4a84714e2fb07f70c70c79f8b8da9fcb41096e33)) +* Add idxmin, idxmax to series, dataframe ([#74](https://github.com/googleapis/python-bigquery-dataframes/issues/74)) ([781307e](https://github.com/googleapis/python-bigquery-dataframes/commit/781307ec22d31a7657f8ee5c6eedc0e419450ccd)) +* Add ml.preprocessing.KBinsDiscretizer ([#81](https://github.com/googleapis/python-bigquery-dataframes/issues/81)) ([24c6256](https://github.com/googleapis/python-bigquery-dataframes/commit/24c625638984f6a84191c7a4c8ac9fb6c3cf1dca)) +* Add multi-column dataframe merge ([#73](https://github.com/googleapis/python-bigquery-dataframes/issues/73)) ([c9fa85c](https://github.com/googleapis/python-bigquery-dataframes/commit/c9fa85cc338be5e9a8dde59b255690aedbbc1127)) +* Add update and align methods to dataframe ([#57](https://github.com/googleapis/python-bigquery-dataframes/issues/57)) ([bf050cf](https://github.com/googleapis/python-bigquery-dataframes/commit/bf050cf475ad8a9e3e0ca3f896ddaf96dbe13ae3)) +* Support STRUCT data type with `Series.struct.field` to extract child fields ([#71](https://github.com/googleapis/python-bigquery-dataframes/issues/71)) ([17afac9](https://github.com/googleapis/python-bigquery-dataframes/commit/17afac9ff70a2b93ed70dc7bcce7beb9a53c2ece)) + + +### Bug Fixes + +* Avoid `403 response too large to return` error with `read_gbq` and large query results ([#77](https://github.com/googleapis/python-bigquery-dataframes/issues/77)) ([8f3b5b2](https://github.com/googleapis/python-bigquery-dataframes/commit/8f3b5b240f0f28fef92465abc53504e875d7335a)) +* Change return type of `Series.loc[scalar]` ([#40](https://github.com/googleapis/python-bigquery-dataframes/issues/40)) ([fff3d45](https://github.com/googleapis/python-bigquery-dataframes/commit/fff3d45f03ffbc7bb23143a1572e3dd157463ca9)) +* Fix df/series.iloc by list with multiindex ([#79](https://github.com/googleapis/python-bigquery-dataframes/issues/79)) ([971d091](https://github.com/googleapis/python-bigquery-dataframes/commit/971d091cac9ad662145a3d43d8f9a785eb0ccc23)) + ## [0.5.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.4.0...v0.5.0) (2023-09-28) diff --git a/bigframes/version.py b/bigframes/version.py index ad3c3082c52..238b64473a0 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.5.0" +__version__ = "0.6.0" From c0efec8956198247b27904345a795f09c80d3502 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 4 Oct 2023 10:14:08 -0700 Subject: [PATCH 059/921] feat: add aliases for several series properties (#80) * feat: add aliases for several series properties --------- Co-authored-by: Tim Swast --- bigframes/series.py | 15 +++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/bigframes/series.py b/bigframes/series.py index 8815a6abdee..2a0b1ff1fa4 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -99,6 +99,10 @@ def shape(self) -> typing.Tuple[int]: def size(self) -> int: return self.shape[0] + @property + def ndim(self) -> int: + return 1 + @property def empty(self) -> bool: return self.shape[0] == 0 @@ -123,6 +127,13 @@ def query_job(self) -> Optional[bigquery.QueryJob]: def struct(self) -> structs.StructAccessor: return structs.StructAccessor(self._block) + @property + def T(self) -> Series: + return self.transpose() + + def transpose(self) -> Series: + return self + def _set_internal_query_job(self, query_job: bigquery.QueryJob): self._query_job = query_job @@ -362,6 +373,8 @@ def ffill(self, *, limit: typing.Optional[int] = None) -> Series: window = bigframes.core.WindowSpec(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) + pad = ffill + def bfill(self, *, limit: typing.Optional[int] = None) -> Series: window = bigframes.core.WindowSpec(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) @@ -743,6 +756,8 @@ def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: agg_ops.lookup_agg_func(typing.cast(str, func)) ) + aggregate = agg + def skew(self): count = self.count() if count < 3: diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index a41a3454ca4..970ab1d8b43 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -64,6 +64,20 @@ def name(self) -> Hashable: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def T(self) -> Series: + """Return the transpose, which is by definition self.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transpose(self) -> Series: + """ + Return the transpose, which is by definition self. + + Returns: + Series + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def reset_index( self, *, From 740c45176f79d4d2f7f28cb5f6c9eeb1327c8397 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 4 Oct 2023 16:17:20 -0700 Subject: [PATCH 060/921] feat: Allow df.drop to take an index object (#68) * feat: Allow passing index objects to df.drop * remove notebook files * add first implementation for df.drop(index) * use index_columns property * don't use _expr.keys() * fix order bug and add test * fix index names * support multiindex * remove accidentally added files * add type hint * remove debug print statements * fix: fix df/series.iloc by list with multiindex * complete merge * remove unneeded isinstance * refactor _drop_by_index --------- Co-authored-by: Henry J Solberg Co-authored-by: Tim Swast --- bigframes/dataframe.py | 27 +++++++++++++- tests/system/small/test_dataframe.py | 55 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index eea8beb130a..8874a4edb89 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -937,7 +937,7 @@ def drop( columns = labels block = self._block - if index: + if index is not None: level_id = self._resolve_levels(level or 0)[0] if utils.is_list_like(index): @@ -947,6 +947,8 @@ def drop( block, condition_id = block.apply_unary_op( inverse_condition_id, ops.invert_op ) + elif isinstance(index, indexes.Index): + return self._drop_by_index(index) else: block, condition_id = block.apply_unary_op( level_id, ops.partial_right(ops.ne_op, index) @@ -956,10 +958,31 @@ def drop( ) if columns: block = block.drop_columns(self._sql_names(columns)) - if not index and not columns: + if index is None and not columns: raise ValueError("Must specify 'labels' or 'index'/'columns") return DataFrame(block) + def _drop_by_index(self, index: indexes.Index) -> DataFrame: + block = index._data._get_block() + block, ordering_col = block.promote_offsets() + joined_index, (get_column_left, get_column_right) = self._block.index.join( + block.index + ) + + new_ordering_col = get_column_right(ordering_col) + drop_block = joined_index._block + drop_block, drop_col = drop_block.apply_unary_op( + new_ordering_col, + ops.isnull_op, + ) + + drop_block = drop_block.filter(drop_col) + original_columns = [ + get_column_left(column) for column in self._block.value_columns + ] + drop_block = drop_block.select_columns(original_columns) + return DataFrame(drop_block) + def droplevel(self, level: LevelsType, axis: int | str = 0): axis_n = utils.get_axis_number(axis) if axis_n == 0: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index b8616a54d65..19ea9b8ae55 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -258,6 +258,61 @@ def test_drop_index(scalars_dfs): pd.testing.assert_frame_equal(pd_result, bf_result) +def test_drop_pandas_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + drop_index = scalars_pandas_df.iloc[[4, 1, 2]].index + + pd_result = scalars_pandas_df.drop(index=drop_index) + bf_result = scalars_df.drop(index=drop_index).to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + +def test_drop_bigframes_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + drop_index = scalars_df.loc[[4, 1, 2]].index + drop_pandas_index = scalars_pandas_df.loc[[4, 1, 2]].index + + pd_result = scalars_pandas_df.drop(index=drop_pandas_index) + bf_result = scalars_df.drop(index=drop_index).to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + +def test_drop_bigframes_index_with_na(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + scalars_df = scalars_df.copy() + scalars_pandas_df = scalars_pandas_df.copy() + scalars_df = scalars_df.set_index("bytes_col") + scalars_pandas_df = scalars_pandas_df.set_index("bytes_col") + drop_index = scalars_df.iloc[[3, 5]].index + drop_pandas_index = scalars_pandas_df.iloc[[3, 5]].index + + pd_result = scalars_pandas_df.drop(index=drop_pandas_index) # drop_pandas_index) + bf_result = scalars_df.drop(index=drop_index).to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + +def test_drop_bigframes_multiindex(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + scalars_df = scalars_df.copy() + scalars_pandas_df = scalars_pandas_df.copy() + sub_df = scalars_df.iloc[[4, 1, 2]] + sub_pandas_df = scalars_pandas_df.iloc[[4, 1, 2]] + sub_df = sub_df.set_index(["bytes_col", "numeric_col"]) + sub_pandas_df = sub_pandas_df.set_index(["bytes_col", "numeric_col"]) + drop_index = sub_df.index + drop_pandas_index = sub_pandas_df.index + + scalars_df = scalars_df.set_index(["bytes_col", "numeric_col"]) + scalars_pandas_df = scalars_pandas_df.set_index(["bytes_col", "numeric_col"]) + bf_result = scalars_df.drop(index=drop_index).to_pandas() + pd_result = scalars_pandas_df.drop(index=drop_pandas_index) + + pd.testing.assert_frame_equal(pd_result, bf_result) + + def test_drop_labels_axis_0(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs From ff3bb89cca2e29625166b4646a34ce6b889d58f9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 5 Oct 2023 13:19:39 -0700 Subject: [PATCH 061/921] refactor: simplify ArrayValue public interface (#82) * refactor: simplify ArrayValue public interface --------- Co-authored-by: Tim Swast --- bigframes/core/__init__.py | 245 ++++++++++---------------- bigframes/core/blocks.py | 116 ++++++++---- bigframes/core/groupby/__init__.py | 4 - bigframes/core/indexes/index.py | 4 +- bigframes/core/joins/row_identity.py | 16 +- bigframes/core/joins/single_column.py | 22 +-- bigframes/dataframe.py | 6 +- bigframes/operations/base.py | 6 - bigframes/series.py | 6 +- tests/unit/test_core.py | 42 +---- 10 files changed, 212 insertions(+), 255 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 8008c1189a1..ccfd682215c 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -16,8 +16,9 @@ from dataclasses import dataclass import functools import math +import textwrap import typing -from typing import Collection, Dict, Iterable, Literal, Optional, Sequence, Tuple +from typing import Collection, Iterable, Literal, Optional, Sequence, Tuple from google.cloud import bigquery import ibis @@ -201,31 +202,27 @@ def mem_expr_from_pandas( hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), ) - @property - def table(self) -> ibis_types.Table: - return self._table - - @property - def reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: - """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" - return ( - _reduce_predicate_list(self._predicates).name(PREDICATE_COLUMN) - if self._predicates - else None - ) - @property def columns(self) -> typing.Tuple[ibis_types.Value, ...]: return self._columns @property - def column_names(self) -> Dict[str, ibis_types.Value]: - return self._column_names + def column_ids(self) -> typing.Sequence[str]: + return tuple(self._column_names.keys()) @property def hidden_ordering_columns(self) -> typing.Tuple[ibis_types.Value, ...]: return self._hidden_ordering_columns + @property + def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: + """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" + return ( + _reduce_predicate_list(self._predicates).name(PREDICATE_COLUMN) + if self._predicates + else None + ) + @property def _ibis_order(self) -> Sequence[ibis_types.Value]: """Returns a sequence of ibis values which can be directly used to order a table expression. Has direction modifiers applied.""" @@ -265,24 +262,22 @@ def drop_columns(self, columns: Iterable[str]) -> ArrayValue: def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: ibis_type = typing.cast( - bigframes.dtypes.IbisDtype, self.get_any_column(key).type() + bigframes.dtypes.IbisDtype, self._get_any_column(key).type() ) return typing.cast( bigframes.dtypes.Dtype, bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), ) - def get_column(self, key: str) -> ibis_types.Value: + def _get_ibis_column(self, key: str) -> ibis_types.Value: """Gets the Ibis expression for a given column.""" - if key not in self._column_names.keys(): + if key not in self.column_ids: raise ValueError( - "Column name {} not in set of values: {}".format( - key, self._column_names.keys() - ) + "Column name {} not in set of values: {}".format(key, self.column_ids) ) return typing.cast(ibis_types.Value, self._column_names[key]) - def get_any_column(self, key: str) -> ibis_types.Value: + def _get_any_column(self, key: str) -> ibis_types.Value: """Gets the Ibis expression for a given column. Will also get hidden columns.""" all_columns = {**self._column_names, **self._hidden_ordering_column_names} if key not in all_columns.keys(): @@ -303,26 +298,11 @@ def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: ) return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) - def apply_limit(self, max_results: int) -> ArrayValue: - table = self._to_ibis_expr( - ordering_mode="order_by", - expose_hidden_cols=True, - ).limit(max_results) - columns = [table[column_name] for column_name in self._column_names] - hidden_ordering_columns = [ - table[column_name] for column_name in self._hidden_ordering_column_names - ] - return ArrayValue( - self._session, - table, - columns=columns, - hidden_ordering_columns=hidden_ordering_columns, - ordering=self._ordering, - ) - def filter(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - condition = typing.cast(ibis_types.BooleanValue, self.get_column(predicate_id)) + condition = typing.cast( + ibis_types.BooleanValue, self._get_ibis_column(predicate_id) + ) if keep_null: condition = typing.cast( ibis_types.BooleanValue, @@ -358,7 +338,7 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: The row numbers of result is non-deterministic, avoid to use. """ table = self._to_ibis_expr( - ordering_mode="order_by", expose_hidden_cols=True, fraction=fraction + "unordered", expose_hidden_cols=True, fraction=fraction ) columns = [table[column_name] for column_name in self._column_names] hidden_ordering_columns = [ @@ -373,7 +353,7 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: ) @property - def offsets(self): + def _offsets(self) -> ibis_types.IntegerColumn: if not self._ordering.is_sequential: raise ValueError( "Expression does not have offsets. Generate them first using project_offsets." @@ -382,9 +362,10 @@ def offsets(self): raise ValueError( "Ordering is invalid. Marked as sequential but no total order columns." ) - return self.get_any_column(self._ordering.total_order_col.column_id) + column = self._get_any_column(self._ordering.total_order_col.column_id) + return typing.cast(ibis_types.IntegerColumn, column) - def project_offsets(self) -> ArrayValue: + def _project_offsets(self) -> ArrayValue: """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" if self._ordering.is_sequential: return self @@ -414,7 +395,7 @@ def _hide_column(self, column_id) -> ArrayValue: new_name = bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") expr_builder.hidden_ordering_columns = [ *self._hidden_ordering_columns, - self.get_column(column_id).name(new_name), + self._get_ibis_column(column_id).name(new_name), ] expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) return expr_builder.build() @@ -427,26 +408,28 @@ def promote_offsets(self) -> typing.Tuple[ArrayValue, str]: ordering = self._ordering if (not ordering.is_sequential) or (not ordering.total_order_col): - return self.project_offsets().promote_offsets() + return self._project_offsets().promote_offsets() col_id = bigframes.core.guid.generate_guid() expr_builder = self.builder() expr_builder.columns = [ - self.get_any_column(ordering.total_order_col.column_id).name(col_id), + self._get_any_column(ordering.total_order_col.column_id).name(col_id), *self.columns, ] return expr_builder.build(), col_id def select_columns(self, column_ids: typing.Sequence[str]): - return self.projection([self.get_column(col_id) for col_id in column_ids]) + return self._projection( + [self._get_ibis_column(col_id) for col_id in column_ids] + ) - def projection(self, columns: Iterable[ibis_types.Value]) -> ArrayValue: + def _projection(self, columns: Iterable[ibis_types.Value]) -> ArrayValue: """Creates a new expression based on this expression with new columns.""" # TODO(swast): We might want to do validation here that columns derive # from the same table expression instead of (in addition to?) at # construction time. expr = self - for ordering_column in set(self.column_names.keys()).intersection( + for ordering_column in set(self.column_ids).intersection( [col_ref.column_id for col_ref in self._ordering.ordering_value_columns] ): # Need to hide ordering columns that are being dropped. Alternatively, could project offsets @@ -459,7 +442,7 @@ def projection(self, columns: Iterable[ibis_types.Value]) -> ArrayValue: def shape(self) -> typing.Tuple[int, int]: """Returns dimensions as (length, width) tuple.""" width = len(self.columns) - count_expr = self._to_ibis_expr(ordering_mode="unordered").count() + count_expr = self._to_ibis_expr("unordered").count() sql = self._session.ibis_client.compile(count_expr) # Support in-memory engines for hermetic unit tests. @@ -527,7 +510,7 @@ def project_unary_op( self, column_name: str, op: ops.UnaryOp, output_name=None ) -> ArrayValue: """Creates a new expression based on this expression with unary operation applied to one column.""" - value = op._as_ibis(self.get_column(column_name)).name( + value = op._as_ibis(self._get_ibis_column(column_name)).name( output_name or column_name ) return self._set_or_replace_by_id(output_name or column_name, value) @@ -541,7 +524,8 @@ def project_binary_op( ) -> ArrayValue: """Creates a new expression based on this expression with binary operation applied to two columns.""" value = op( - self.get_column(left_column_id), self.get_column(right_column_id) + self._get_ibis_column(left_column_id), + self._get_ibis_column(right_column_id), ).name(output_column_id) return self._set_or_replace_by_id(output_column_id, value) @@ -555,9 +539,9 @@ def project_ternary_op( ) -> ArrayValue: """Creates a new expression based on this expression with ternary operation applied to three columns.""" value = op( - self.get_column(col_id_1), - self.get_column(col_id_2), - self.get_column(col_id_3), + self._get_ibis_column(col_id_1), + self._get_ibis_column(col_id_2), + self._get_ibis_column(col_id_3), ).name(output_column_id) return self._set_or_replace_by_id(output_column_id, value) @@ -574,7 +558,7 @@ def aggregate( by_column_id: column id of the aggregation key, this is preserved through the transform dropna: whether null keys should be dropped """ - table = self._to_ibis_expr(ordering_mode="unordered") + table = self._to_ibis_expr("unordered") stats = { col_out: agg_op._as_ibis(table[col_in]) for col_in, agg_op, col_out in aggregations @@ -594,10 +578,10 @@ def aggregate( if dropna: for column_id in by_column_ids: expr = expr._filter( - ops.notnull_op._as_ibis(expr.get_column(column_id)) + ops.notnull_op._as_ibis(expr._get_ibis_column(column_id)) ) # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation - return expr.project_offsets() + return expr._project_offsets() else: aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} result = table.aggregate(**aggregates) @@ -624,7 +608,7 @@ def corr_aggregate( Arguments: corr_aggregations: left_column_id, right_column_id, output_column_id tuples """ - table = self._to_ibis_expr(ordering_mode="unordered") + table = self._to_ibis_expr("unordered") stats = { col_out: table[col_left].corr(table[col_right], how="pop") for col_left, col_right, col_out in corr_aggregations @@ -664,7 +648,7 @@ def project_window_op( never_skip_nulls: will disable null skipping for operators that would otherwise do so skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection """ - column = typing.cast(ibis_types.Column, self.get_column(column_name)) + column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) window = self._ibis_window_from_spec(window_spec, allow_ties=op.handles_ties) window_op = op._as_ibis(column, window) @@ -700,26 +684,34 @@ def project_window_op( def to_sql( self, - ordering_mode: Literal[ - "order_by", "string_encoded", "offset_col", "unordered" - ] = "order_by", - order_col_name: Optional[str] = ORDER_ID_COLUMN, + offset_column: typing.Optional[str] = None, col_id_overrides: typing.Mapping[str, str] = {}, + sorted: bool = False, ) -> str: + offsets_id = offset_column or ORDER_ID_COLUMN + sql = self._session.ibis_client.compile( self._to_ibis_expr( - ordering_mode=ordering_mode, - order_col_name=order_col_name, + ordering_mode="offset_col" + if (offset_column or sorted) + else "unordered", + order_col_name=offsets_id, col_id_overrides=col_id_overrides, ) ) + if sorted: + sql = textwrap.dedent( + f""" + SELECT * EXCEPT (`{offsets_id}`) + FROM ({sql}) + ORDER BY `{offsets_id}` + """ + ) return typing.cast(str, sql) def _to_ibis_expr( self, - ordering_mode: Literal[ - "order_by", "string_encoded", "offset_col", "unordered" - ] = "order_by", + ordering_mode: Literal["string_encoded", "offset_col", "unordered"], order_col_name: Optional[str] = ORDER_ID_COLUMN, expose_hidden_cols: bool = False, fraction: Optional[float] = None, @@ -731,8 +723,6 @@ def _to_ibis_expr( ArrayValue objects are sorted, so the following options are available to reflect this in the ibis expression. - * "order_by" (Default): The output table will not have an ordering - column, however there will be an order_by clause applied to the ouput. * "offset_col": Zero-based offsets are generated as a column, this will not sort the rows however. * "string_encoded": An ordered string column is provided in output table. @@ -760,7 +750,6 @@ def _to_ibis_expr( An ibis expression representing the data help by the ArrayValue object. """ assert ordering_mode in ( - "order_by", "string_encoded", "offset_col", "unordered", @@ -775,18 +764,16 @@ def _to_ibis_expr( str ] = [] # Ordering/Filtering columns that will be dropped at end - if self.reduced_predicate is not None: - columns.append(self.reduced_predicate) + if self._reduced_predicate is not None: + columns.append(self._reduced_predicate) # Usually drop predicate as it is will be all TRUE after filtering if not expose_hidden_cols: - columns_to_drop.append(self.reduced_predicate.get_name()) + columns_to_drop.append(self._reduced_predicate.get_name()) order_columns = self._create_order_columns( ordering_mode, order_col_name, expose_hidden_cols ) columns.extend(order_columns) - if (ordering_mode == "order_by") and not expose_hidden_cols: - columns_to_drop.extend(col.get_name() for col in order_columns) # Special case for empty tables, since we can't create an empty # projection. @@ -799,15 +786,8 @@ def _to_ibis_expr( bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns ) base_table = table - if self.reduced_predicate is not None: + if self._reduced_predicate is not None: table = table.filter(base_table[PREDICATE_COLUMN]) - if ordering_mode == "order_by": - table = table.order_by( - _convert_ordering_to_table_values( - {col: base_table[col] for col in table.columns}, - self._ordering.all_ordering_columns, - ) # type: ignore - ) table = table.drop(*columns_to_drop) if col_id_overrides: table = table.relabel(col_id_overrides) @@ -826,24 +806,24 @@ def _create_order_columns( return (self._create_offset_column().name(order_col_name),) elif ordering_mode == "string_encoded": return (self._create_string_ordering_column().name(order_col_name),) - elif ordering_mode == "order_by" or expose_hidden_cols: + elif expose_hidden_cols: return self.hidden_ordering_columns return () def _create_offset_column(self) -> ibis_types.IntegerColumn: if self._ordering.total_order_col and self._ordering.is_sequential: - offsets = self.get_any_column(self._ordering.total_order_col.column_id) + offsets = self._get_any_column(self._ordering.total_order_col.column_id) return typing.cast(ibis_types.IntegerColumn, offsets) else: window = ibis.window(order_by=self._ibis_order) if self._predicates: - window = window.group_by(self.reduced_predicate) + window = window.group_by(self._reduced_predicate) offsets = ibis.row_number().over(window) return typing.cast(ibis_types.IntegerColumn, offsets) def _create_string_ordering_column(self) -> ibis_types.StringColumn: if self._ordering.total_order_col and self._ordering.is_string_encoded: - string_order_ids = self.get_any_column( + string_order_ids = self._get_any_column( self._ordering.total_order_col.column_id ) return typing.cast(ibis_types.StringColumn, string_order_ids) @@ -852,7 +832,7 @@ def _create_string_ordering_column(self) -> ibis_types.StringColumn: and self._ordering.integer_encoding.is_encoded ): # Special case: non-negative integer ordering id can be converted directly to string without regenerating row numbers - int_values = self.get_any_column(self._ordering.total_order_col.column_id) + int_values = self._get_any_column(self._ordering.total_order_col.column_id) return encode_order_string( typing.cast(ibis_types.IntegerColumn, int_values), ) @@ -860,7 +840,7 @@ def _create_string_ordering_column(self) -> ibis_types.StringColumn: # Have to build string from scratch window = ibis.window(order_by=self._ibis_order) if self._predicates: - window = window.group_by(self.reduced_predicate) + window = window.group_by(self._reduced_predicate) row_nums = typing.cast( ibis_types.IntegerColumn, ibis.row_number().over(window) ) @@ -870,7 +850,8 @@ def start_query( self, job_config: Optional[bigquery.job.QueryJobConfig] = None, max_results: Optional[int] = None, - expose_extra_columns: bool = False, + *, + sorted: bool = True, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """Execute a query and return metadata about the results.""" # TODO(swast): Cache the job ID so we can look it up again if they ask @@ -883,8 +864,7 @@ def start_query( # a LocalSession for unit testing. # TODO(swast): Add a timeout here? If the query is taking a long time, # maybe we just print the job metadata that we have so far? - table = self._to_ibis_expr(expose_hidden_cols=expose_extra_columns) - sql = self._session.ibis_client.compile(table) # type:ignore + sql = self.to_sql(sorted=True) # type:ignore return self._session._start_query( sql=sql, job_config=job_config, @@ -903,7 +883,7 @@ def _reproject_to_table(self) -> ArrayValue: recursively in projections. """ table = self._to_ibis_expr( - ordering_mode="unordered", + "unordered", expose_hidden_cols=True, ) columns = [table[column_name] for column_name in self._column_names] @@ -926,14 +906,16 @@ def _reproject_to_table(self) -> ArrayValue: def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = False): group_by: typing.List[ibis_types.Value] = ( [ - typing.cast(ibis_types.Column, _as_identity(self.get_column(column))) + typing.cast( + ibis_types.Column, _as_identity(self._get_ibis_column(column)) + ) for column in window_spec.grouping_keys ] if window_spec.grouping_keys else [] ) - if self.reduced_predicate is not None: - group_by.append(self.reduced_predicate) + if self._reduced_predicate is not None: + group_by.append(self._reduced_predicate) if window_spec.ordering: order_by = _convert_ordering_to_table_values( {**self._column_names, **self._hidden_ordering_column_names}, @@ -984,7 +966,7 @@ def unpivot( """ if how not in ("left", "right"): raise ValueError("'how' must be 'left' or 'right'") - table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) + table = self._to_ibis_expr("unordered", expose_hidden_cols=True) row_n = len(row_labels) hidden_col_ids = self._hidden_ordering_column_names.keys() if not all( @@ -1107,7 +1089,9 @@ def unpivot( ) def assign(self, source_id: str, destination_id: str) -> ArrayValue: - return self._set_or_replace_by_id(destination_id, self.get_column(source_id)) + return self._set_or_replace_by_id( + destination_id, self._get_ibis_column(source_id) + ) def assign_constant( self, @@ -1134,74 +1118,25 @@ def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> ArrayVa return self._hide_column(id)._set_or_replace_by_id(id, new_value) builder = self.builder() - if id in self.column_names: + if id in self.column_ids: builder.columns = [ val if (col_id != id) else new_value.name(id) - for col_id, val in self.column_names.items() + for col_id, val in zip(self.column_ids, self._columns) ] else: builder.columns = [*self.columns, new_value.name(id)] return builder.build() - def slice( - self, - start: typing.Optional[int] = None, - stop: typing.Optional[int] = None, - step: typing.Optional[int] = None, - ) -> ArrayValue: - if step == 0: - raise ValueError("slice step cannot be zero") - - if not step: - step = 1 - - expr_with_offsets = self.project_offsets() - - # start with True and reduce with start, stop, and step conditions - cond_list = [expr_with_offsets.offsets == expr_with_offsets.offsets] - - last_offset = expr_with_offsets.offsets.max() - - # Convert negative indexes to positive indexes - if start and start < 0: - start = last_offset + start + 1 - if stop and stop < 0: - stop = last_offset + stop + 1 - - if start is not None: - if step >= 1: - cond_list.append(expr_with_offsets.offsets >= start) - else: - cond_list.append(expr_with_offsets.offsets <= start) - if stop is not None: - if step >= 1: - cond_list.append(expr_with_offsets.offsets < stop) - else: - cond_list.append(expr_with_offsets.offsets > stop) - if step > 1: - start = start if (start is not None) else 0 - cond_list.append((expr_with_offsets.offsets - start) % step == 0) - if step < 0: - start = start if (start is not None) else last_offset - cond_list.append((start - expr_with_offsets.offsets) % (-step) == 0) - - sliced_expr = expr_with_offsets._filter( - functools.reduce(lambda x, y: x & y, cond_list) - ) - return sliced_expr if step > 0 else sliced_expr.reversed() - def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: """Write the ArrayValue to a session table and create a new block object that references it.""" - ibis_expr = self._to_ibis_expr( - ordering_mode="unordered", expose_hidden_cols=True - ) + ibis_expr = self._to_ibis_expr("unordered", expose_hidden_cols=True) destination = self._session._ibis_to_session_table( ibis_expr, cluster_cols=cluster_cols, api_name="cache" ) table_expression = self._session.ibis_client.table( f"{destination.project}.{destination.dataset_id}.{destination.table_id}" ) - new_columns = [table_expression[column] for column in self.column_names] + new_columns = [table_expression[column] for column in self.column_ids] new_hidden_columns = [ table_expression[column] for column in self._hidden_ordering_column_names ] diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 0161d173618..852b1fe02ab 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -152,7 +152,7 @@ def value_columns(self) -> Sequence[str]: """All value columns, mutually exclusive with index columns.""" return [ column - for column in self._expr.column_names + for column in self._expr.column_ids if column not in self.index_columns ] @@ -444,9 +444,7 @@ def _compute_and_count( # TODO(swast): Allow for dry run and timeout. expr = self._apply_value_keys_to_expr(value_keys=value_keys) - results_iterator, query_job = expr.start_query( - max_results=max_results, expose_extra_columns=True - ) + results_iterator, query_job = expr.start_query(max_results=max_results) table_size = expr._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES fraction = ( @@ -483,12 +481,6 @@ def _compute_and_count( if self.index_columns: df.set_index(list(self.index_columns), inplace=True) df.index.names = self.index.names # type: ignore - - df.drop( - [col for col in df.columns if col not in self.value_columns], - axis=1, - inplace=True, - ) elif (sampling_method == _UNIFORM) and (random_state is None): filtered_expr = self.expr._uniform_sampling(fraction) block = Block( @@ -520,12 +512,6 @@ def _compute_and_count( df.set_index(list(self.index_columns), inplace=True) df.index.names = self.index.names # type: ignore - df.drop( - [col for col in df.columns if col not in self.value_columns], - axis=1, - inplace=True, - ) - return df, total_rows, query_job def _split( @@ -1087,7 +1073,7 @@ def _normalize_expression( ): """Normalizes expression by moving index columns to left.""" value_columns = [ - col_id for col_id in expr.column_names.keys() if col_id not in index_columns + col_id for col_id in expr.column_ids if col_id not in index_columns ] if (assert_value_size is not None) and ( len(value_columns) != assert_value_size @@ -1096,20 +1082,92 @@ def _normalize_expression( return expr.select_columns([*index_columns, *value_columns]) def slice( - self: bigframes.core.blocks.Block, + self, start: typing.Optional[int] = None, stop: typing.Optional[int] = None, step: typing.Optional[int] = None, ) -> bigframes.core.blocks.Block: - sliced_expr = self.expr.slice(start=start, stop=stop, step=step) - # since this is slice, return a copy even if unchanged - block = Block( - sliced_expr, - index_columns=self.index_columns, - column_labels=self.column_labels, - index_labels=self._index_labels, + if step is None: + step = 1 + if step == 0: + raise ValueError("slice step cannot be zero") + if step < 0: + reverse_start = (-start - 1) if start else 0 + reverse_stop = (-stop - 1) if stop else None + reverse_step = -step + return self.reversed()._forward_slice( + reverse_start, reverse_stop, reverse_step + ) + return self._forward_slice(start or 0, stop, step) + + def _forward_slice(self, start: int = 0, stop=None, step: int = 1): + """Performs slice but only for positive step size.""" + if step <= 0: + raise ValueError("forward_slice only supports positive step size") + + use_postive_offsets = ( + (start > 0) + or ((stop is not None) and (stop >= 0)) + or ((step > 1) and (start >= 0)) ) - return block + use_negative_offsets = ( + (start < 0) or (stop and (stop < 0)) or ((step > 1) and (start < 0)) + ) + + block = self + + # only generate offsets that are used + positive_offsets = None + negative_offsets = None + + if use_postive_offsets: + block, positive_offsets = self.promote_offsets() + if use_negative_offsets: + block, negative_offsets = block.reversed().promote_offsets() + block = block.reversed() + + conditions = [] + if start != 0: + if start > 0: + op = ops.partial_right(ops.ge_op, start) + assert positive_offsets + block, start_cond = block.apply_unary_op(positive_offsets, op) + else: + op = ops.partial_right(ops.le_op, -start - 1) + assert negative_offsets + block, start_cond = block.apply_unary_op(negative_offsets, op) + conditions.append(start_cond) + if stop is not None: + if stop >= 0: + op = ops.partial_right(ops.lt_op, stop) + assert positive_offsets + block, stop_cond = block.apply_unary_op(positive_offsets, op) + else: + op = ops.partial_right(ops.gt_op, -stop - 1) + assert negative_offsets + block, stop_cond = block.apply_unary_op(negative_offsets, op) + conditions.append(stop_cond) + + if step > 1: + op = ops.partial_right(ops.mod_op, step) + if start >= 0: + op = ops.partial_right(ops.sub_op, start) + assert positive_offsets + block, start_diff = block.apply_unary_op(positive_offsets, op) + else: + op = ops.partial_right(ops.sub_op, -start + 1) + assert negative_offsets + block, start_diff = block.apply_unary_op(negative_offsets, op) + modulo_op = ops.partial_right(ops.mod_op, step) + block, mod = block.apply_unary_op(start_diff, modulo_op) + is_zero_op = ops.partial_right(ops.eq_op, 0) + block, step_cond = block.apply_unary_op(mod, is_zero_op) + conditions.append(step_cond) + + for cond in conditions: + block = block.filter(cond) + + return block.select_columns(self.value_columns) # Using cache to optimize for Jupyter Notebook's behavior where both '__repr__' # and '__repr_html__' are called in a single display action, reducing redundant @@ -1396,7 +1454,7 @@ def concat( ) result_block = Block( result_expr, - index_columns=list(result_expr.column_names.keys())[:index_nlevels], + index_columns=list(result_expr.column_ids)[:index_nlevels], column_labels=aligned_blocks[0].column_labels, index_labels=result_labels, ) @@ -1530,9 +1588,7 @@ def to_sql_query( # the BigQuery unicode column name feature? substitutions[old_id] = new_id - sql = array_value.to_sql( - ordering_mode="unordered", col_id_overrides=substitutions - ) + sql = array_value.to_sql(col_id_overrides=substitutions) return ( sql, new_ids[: len(idx_labels)], diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 9be7f22a714..db0843fcbc0 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -426,10 +426,6 @@ def __init__( self._value_name = value_name self._dropna = dropna # Applies to aggregations but not windowing - @property - def _value(self): - return self._block.expr.get_column(self._value_column) - def all(self) -> series.Series: return self._aggregate(agg_ops.all_op) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index f211afe4d56..7d15e676493 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -398,9 +398,7 @@ def to_pandas(self) -> pandas.Index: """Executes deferred operations and downloads the results.""" # Project down to only the index column. So the query can be cached to visualize other data. index_columns = list(self._block.index_columns) - expr = self._expr.projection( - [self._expr.get_any_column(col) for col in index_columns] - ) + expr = self._expr.select_columns(index_columns) results, _ = expr.start_query() df = expr._session._rows_to_dataframe(results) df = df.set_index(index_columns) diff --git a/bigframes/core/joins/row_identity.py b/bigframes/core/joins/row_identity.py index 66eb2239904..156e7aef407 100644 --- a/bigframes/core/joins/row_identity.py +++ b/bigframes/core/joins/row_identity.py @@ -38,11 +38,11 @@ def join_by_row_identity( f"Only how='outer','left','inner' currently supported. {constants.FEEDBACK_LINK}" ) - if not left.table.equals(right.table): + if not left._table.equals(right._table): raise ValueError( "Cannot combine objects without an explicit join/merge key. " - f"Left based on: {left.table.compile()}, but " - f"right based on: {right.table.compile()}" + f"Left based on: {left._table.compile()}, but " + f"right based on: {right._table.compile()}" ) left_predicates = left._predicates @@ -63,11 +63,11 @@ def join_by_row_identity( left_mask = left_relative_predicates if how in ["right", "outer"] else None right_mask = right_relative_predicates if how in ["left", "outer"] else None joined_columns = [ - _mask_value(left.get_column(key), left_mask).name(map_left_id(key)) - for key in left.column_names.keys() + _mask_value(left._get_ibis_column(key), left_mask).name(map_left_id(key)) + for key in left.column_ids ] + [ - _mask_value(right.get_column(key), right_mask).name(map_right_id(key)) - for key in right.column_names.keys() + _mask_value(right._get_ibis_column(key), right_mask).name(map_right_id(key)) + for key in right.column_ids ] # If left isn't being masked, can just use left ordering @@ -108,7 +108,7 @@ def join_by_row_identity( joined_expr = core.ArrayValue( left._session, - left.table, + left._table, columns=joined_columns, hidden_ordering_columns=hidden_ordering_columns, ordering=new_ordering, diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py index 2d616fc3f0c..f194b8f8c41 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/joins/single_column.py @@ -74,14 +74,14 @@ def join_by_column( if ( allow_row_identity_join and how in bigframes.core.joins.row_identity.SUPPORTED_ROW_IDENTITY_HOW - and left.table.equals(right.table) + and left._table.equals(right._table) # Make sure we're joining on exactly the same column(s), at least with # regards to value its possible that they both have the same names but # were modified in different ways. Ignore differences in the names. and all( - left.get_any_column(lcol) + left._get_any_column(lcol) .name("index") - .equals(right.get_any_column(rcol).name("index")) + .equals(right._get_any_column(rcol).name("index")) for lcol, rcol in zip(left_column_ids, right_column_ids) ) ): @@ -90,14 +90,16 @@ def join_by_column( get_column_right, ) = bigframes.core.joins.row_identity.join_by_row_identity(left, right, how=how) left_join_keys = [ - combined_expr.get_column(get_column_left(col)) for col in left_column_ids + combined_expr._get_ibis_column(get_column_left(col)) + for col in left_column_ids ] right_join_keys = [ - combined_expr.get_column(get_column_right(col)) for col in right_column_ids + combined_expr._get_ibis_column(get_column_right(col)) + for col in right_column_ids ] join_key_cols = get_coalesced_join_cols(left_join_keys, right_join_keys, how) join_key_ids = [col.get_name() for col in join_key_cols] - combined_expr = combined_expr.projection( + combined_expr = combined_expr._projection( [*join_key_cols, *combined_expr.columns] ) if sort: @@ -119,13 +121,13 @@ def join_by_column( lmapping = { col_id: guid.generate_guid() for col_id in itertools.chain( - left.column_names, left._hidden_ordering_column_names + left.column_ids, left._hidden_ordering_column_names ) } rmapping = { col_id: guid.generate_guid() for col_id in itertools.chain( - right.column_names, right._hidden_ordering_column_names + right.column_ids, right._hidden_ordering_column_names ) } @@ -136,12 +138,12 @@ def get_column_right(col_id): return rmapping[col_id] left_table = left._to_ibis_expr( - ordering_mode="unordered", + "unordered", expose_hidden_cols=True, col_id_overrides=lmapping, ) right_table = right._to_ibis_expr( - ordering_mode="unordered", + "unordered", expose_hidden_cols=True, col_id_overrides=rmapping, ) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 8874a4edb89..0492e62c153 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -554,7 +554,7 @@ def _apply_series_binop( other._block.index, how=how ) - series_column_id = other._value.get_name() + series_column_id = other._value_column series_col = get_column_right(series_column_id) block = joined_index._block for column_id, label in zip( @@ -2382,13 +2382,11 @@ def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: if ordering_id is not None: return array_value.to_sql( - ordering_mode="offset_col", + offset_column=ordering_id, col_id_overrides=id_overrides, - order_col_name=ordering_id, ) else: return array_value.to_sql( - ordering_mode="unordered", col_id_overrides=id_overrides, ) diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 51eaad18b9b..fc76d07edbb 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -16,7 +16,6 @@ import typing -import ibis.expr.types as ibis_types import pandas as pd import bigframes.constants as constants @@ -106,11 +105,6 @@ def __init__( if pd_series.name is None: self._block = self._block.with_column_labels([None]) - @property - def _value(self) -> ibis_types.Value: - """Private property to get Ibis expression for the value column.""" - return self._block.expr.get_column(self._value_column) - @property def _value_column(self) -> str: return self._block.value_columns[0] diff --git a/bigframes/series.py b/bigframes/series.py index 2a0b1ff1fa4..717a85a93e8 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1150,7 +1150,11 @@ def _groupby_values( def apply(self, func) -> Series: # TODO(shobs, b/274645634): Support convert_dtype, args, **kwargs # is actually a ternary op - return self._apply_unary_op(ops.RemoteFunctionOp(func)) + # Reproject as workaround to applying filter too late. This forces the filter + # to be applied before passing data to remote function, protecting from bad + # inputs causing errors. + reprojected_series = Series(self._block._force_reproject()) + return reprojected_series._apply_unary_op(ops.RemoteFunctionOp(func)) def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_prefix(prefix)) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index ee0cefb3d28..69b9e798075 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import ibis import ibis.expr.types as ibis_types import pandas @@ -45,7 +44,7 @@ def test_arrayvalue_constructor_from_ibis_table_adds_all_columns(): actual = core.ArrayValue( session=session, table=ibis_table, columns=columns, ordering=ordering ) - assert actual.table is ibis_table + assert actual._table is ibis_table assert len(actual.columns) == 3 @@ -79,37 +78,12 @@ def test_arrayvalue_with_get_column(): ), total_ordering_columns=["col1"], ) - col1 = value.get_column("col1") + col1 = value._get_ibis_column("col1") assert isinstance(col1, ibis_types.Value) assert col1.get_name() == "col1" assert col1.type().is_int64() -def test_arrayvalue_to_ibis_expr_with_projection(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value.projection( - [ - (value.table["col1"] + ibis.literal(-1)).name("int64_col"), - ibis.literal(123456789).name("literals"), - value.table["col2"].name("string_col"), - ] - ) - actual = expr._to_ibis_expr() - assert len(actual.columns) == 3 - assert actual.columns[0] == "int64_col" - assert actual.columns[1] == "literals" - assert actual.columns[2] == "string_col" - - def test_arrayvalues_to_ibis_expr_with_get_column(): value = resources.create_arrayvalue( pandas.DataFrame( @@ -121,7 +95,7 @@ def test_arrayvalues_to_ibis_expr_with_get_column(): ), total_ordering_columns=["col1"], ) - expr = value.get_column("col1") + expr = value._get_ibis_column("col1") assert expr.get_name() == "col1" assert expr.type().is_int64() @@ -138,7 +112,7 @@ def test_arrayvalues_to_ibis_expr_with_concat(): total_ordering_columns=["col1"], ) expr = value.concat([value]) - actual = expr._to_ibis_expr() + actual = expr._to_ibis_expr("unordered") assert len(actual.columns) == 3 # TODO(ashleyxu, b/299631930): test out the union expression assert actual.columns[0] == "column_0" @@ -175,7 +149,7 @@ def test_arrayvalues_to_ibis_expr_with_project_binary_op(): ) expr = value.project_binary_op("col2", "col3", ops.add_op, "col4") assert expr.columns[3].type().is_float64() - actual = expr._to_ibis_expr() + actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 4 assert actual.columns[3] == "col4" @@ -194,7 +168,7 @@ def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): ) expr = value.project_ternary_op("col2", "col3", "col4", ops.where_op, "col5") assert expr.columns[4].type().is_float64() - actual = expr._to_ibis_expr() + actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 5 assert actual.columns[4] == "col5" @@ -215,7 +189,7 @@ def test_arrayvalue_to_ibis_expr_with_aggregate(): by_column_ids=["col1"], dropna=False, ) - actual = expr._to_ibis_expr() + actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 2 assert actual.columns[0] == "col1" assert actual.columns[1] == "col4" @@ -234,7 +208,7 @@ def test_arrayvalue_to_ibis_expr_with_corr_aggregate(): total_ordering_columns=["col1"], ) expr = value.corr_aggregate(corr_aggregations=[("col1", "col3", "col4")]) - actual = expr._to_ibis_expr() + actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 1 assert actual.columns[0] == "col4" assert expr.columns[0].type().is_float64() From 636a209e0853501abd50784a11a87cf7f2282ee5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 5 Oct 2023 14:09:14 -0700 Subject: [PATCH 062/921] feat: add equals methods to series/dataframe (#76) * feat: add equals methods to series/dataframe --- bigframes/core/block_transforms.py | 33 +++++++++ bigframes/dataframe.py | 6 ++ bigframes/operations/__init__.py | 10 +++ bigframes/series.py | 8 +++ tests/system/small/test_dataframe.py | 68 +++++++++++++++++++ tests/system/small/test_series.py | 38 +++++++++++ .../bigframes_vendored/pandas/core/frame.py | 22 ++++++ 7 files changed, 185 insertions(+) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 30c79029817..4c30d7631dc 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -25,6 +25,39 @@ import bigframes.operations.aggregations as agg_ops +def equals(block1: blocks.Block, block2: blocks.Block) -> bool: + if not block1.column_labels.equals(block2.column_labels): + return False + if block1.dtypes != block2.dtypes: + return False + # TODO: More advanced expression tree traversals to short circuit actually querying data + + block1 = block1.reset_index(drop=False) + block2 = block2.reset_index(drop=False) + + joined, (lmap, rmap) = block1.index.join(block2.index, how="outer") + joined_block = joined._block + + equality_ids = [] + for lcol, rcol in zip(block1.value_columns, block2.value_columns): + lcolmapped = lmap(lcol) + rcolmapped = rmap(rcol) + joined_block, result_id = joined_block.apply_binary_op( + lcolmapped, rcolmapped, ops.eq_nulls_match_op + ) + joined_block, result_id = joined_block.apply_unary_op( + result_id, ops.partial_right(ops.fillna_op, False) + ) + equality_ids.append(result_id) + + joined_block = joined_block.select_columns(equality_ids).with_column_labels( + list(range(len(equality_ids))) + ) + stacked_block = joined_block.stack(dropna=False, sort=False) + result = stacked_block.get_stat(stacked_block.value_columns[0], agg_ops.all_op) + return typing.cast(bool, result) + + def indicate_duplicates( block: blocks.Block, columns: typing.Sequence[str], keep: str = "first" ) -> typing.Tuple[blocks.Block, str]: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0492e62c153..87d0f21b629 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1066,6 +1066,12 @@ def rename_axis( labels = [mapper] return DataFrame(self._block.with_index_labels(labels)) + def equals(self, other: typing.Union[bigframes.series.Series, DataFrame]) -> bool: + # Must be same object type, same column dtypes, and same label values + if not isinstance(other, DataFrame): + return False + return block_ops.equals(self._block, other._block) + def assign(self, **kwargs) -> DataFrame: # TODO(garrettwu) Support list-like values. Requires ordering. # TODO(garrettwu) Support callable values. diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index bc08298eb7b..f330a703b20 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -705,6 +705,16 @@ def eq_op( return x == y +def eq_nulls_match_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + """Variant of eq_op where nulls match each other. Only use where dtypes are known to be same.""" + left = x.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) + right = y.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) + return left == right + + def ne_op( x: ibis_types.Value, y: ibis_types.Value, diff --git a/bigframes/series.py b/bigframes/series.py index 717a85a93e8..e5afe91e445 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -209,6 +209,14 @@ def rename_axis( labels = [mapper] return Series(self._block.with_index_labels(labels)) + def equals( + self, other: typing.Union[Series, bigframes.dataframe.DataFrame] + ) -> bool: + # Must be same object type, same column dtypes, and same label values + if not isinstance(other, Series): + return False + return block_ops.equals(self._block, other._block) + def reset_index( self, *, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 19ea9b8ae55..711da10c55c 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2551,6 +2551,74 @@ def test_df_reindex_columns(scalars_df_index, scalars_pandas_df_index): ) +def test_df_equals_identical(scalars_df_index, scalars_pandas_df_index): + unsupported = [ + "geography_col", + ] + scalars_df_index = scalars_df_index.drop(columns=unsupported) + scalars_pandas_df_index = scalars_pandas_df_index.drop(columns=unsupported) + + bf_result = scalars_df_index.equals(scalars_df_index) + pd_result = scalars_pandas_df_index.equals(scalars_pandas_df_index) + + assert pd_result == bf_result + + +def test_df_equals_series(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index[["int64_col"]].equals(scalars_df_index["int64_col"]) + pd_result = scalars_pandas_df_index[["int64_col"]].equals( + scalars_pandas_df_index["int64_col"] + ) + + assert pd_result == bf_result + + +def test_df_equals_different_dtype(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_col", "int64_too"] + scalars_df_index = scalars_df_index[columns] + scalars_pandas_df_index = scalars_pandas_df_index[columns] + + bf_modified = scalars_df_index.copy() + bf_modified = bf_modified.astype("Float64") + + pd_modified = scalars_pandas_df_index.copy() + pd_modified = pd_modified.astype("Float64") + + bf_result = scalars_df_index.equals(bf_modified) + pd_result = scalars_pandas_df_index.equals(pd_modified) + + assert pd_result == bf_result + + +def test_df_equals_different_values(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_col", "int64_too"] + scalars_df_index = scalars_df_index[columns] + scalars_pandas_df_index = scalars_pandas_df_index[columns] + + bf_modified = scalars_df_index.copy() + bf_modified["int64_col"] = bf_modified.int64_col + 1 + + pd_modified = scalars_pandas_df_index.copy() + pd_modified["int64_col"] = pd_modified.int64_col + 1 + + bf_result = scalars_df_index.equals(bf_modified) + pd_result = scalars_pandas_df_index.equals(pd_modified) + + assert pd_result == bf_result + + +def test_df_equals_extra_column(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_col", "int64_too"] + more_columns = ["int64_col", "int64_too", "float64_col"] + + bf_result = scalars_df_index[columns].equals(scalars_df_index[more_columns]) + pd_result = scalars_pandas_df_index[columns].equals( + scalars_pandas_df_index[more_columns] + ) + + assert pd_result == bf_result + + def test_df_reindex_like(scalars_df_index, scalars_pandas_df_index): reindex_target_bf = scalars_df_index.reindex( columns=["not_a_col", "int64_col", "int64_too"], index=[5, 1, 3, 99, 1] diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 8c1c36720bf..993df18c959 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -112,6 +112,44 @@ def test_series_get_column_default(scalars_dfs): assert result == "default_val" +def test_series_equals_identical(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.int64_col.equals(scalars_df_index.int64_col) + pd_result = scalars_pandas_df_index.int64_col.equals( + scalars_pandas_df_index.int64_col + ) + + assert pd_result == bf_result + + +def test_series_equals_df(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_col"].equals(scalars_df_index[["int64_col"]]) + pd_result = scalars_pandas_df_index["int64_col"].equals( + scalars_pandas_df_index[["int64_col"]] + ) + + assert pd_result == bf_result + + +def test_series_equals_different_dtype(scalars_df_index, scalars_pandas_df_index): + bf_series = scalars_df_index["int64_col"] + pd_series = scalars_pandas_df_index["int64_col"] + + bf_result = bf_series.equals(bf_series.astype("Float64")) + pd_result = pd_series.equals(pd_series.astype("Float64")) + + assert pd_result == bf_result + + +def test_series_equals_different_values(scalars_df_index, scalars_pandas_df_index): + bf_series = scalars_df_index["int64_col"] + pd_series = scalars_pandas_df_index["int64_col"] + + bf_result = bf_series.equals(bf_series + 1) + pd_result = pd_series.equals(pd_series + 1) + + assert pd_result == bf_result + + def test_series_get_with_default_index(scalars_dfs): col_name = "float64_col" key = 2 diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 17d941fbdd0..644e043e83d 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -399,6 +399,28 @@ def to_orc(self, path=None, **kwargs) -> bytes | None: # ---------------------------------------------------------------------- # Unsorted + def equals(self, other) -> bool: + """ + Test whether two objects contain the same elements. + + This function allows two Series or DataFrames to be compared against + each other to see if they have the same shape and elements. NaNs in + the same location are considered equal. + + The row/column index do not need to have the same type, as long + as the values are considered equal. Corresponding columns must be of + the same dtype. + + Args: + other (Series or DataFrame): + The other Series or DataFrame to be compared with the first. + + Returns: + bool: True if all elements are the same in both objects, False + otherwise. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def assign(self, **kwargs) -> DataFrame: r""" Assign new columns to a DataFrame. From 969800d669204de4d0f2e5e61da521217e55668b Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 6 Oct 2023 19:41:23 -0700 Subject: [PATCH 063/921] fix: change the invalid url in docs (#93) * fix: change the invalid url in docs * fix: address the comment --- bigframes/ml/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index f899ac71193..f2478b1ce23 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -104,7 +104,8 @@ def predict(self, X): def register(self: _T, vertex_ai_model_id: Optional[str] = None) -> _T: """Register the model to Vertex AI. - After register, go to https://pantheon.corp.google.com/vertex-ai/models to manage the model registries. + After register, go to Google Cloud Console (https://console.cloud.google.com/vertex-ai/models) + to manage the model registries. Refer to https://cloud.google.com/vertex-ai/docs/model-registry/introduction for more options. Args: From 5b0ee39bcffb8d39e28ea10d024f7b277e62cc18 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:12:56 -0400 Subject: [PATCH 064/921] chore(deps): bump urllib3 from 1.26.12 to 1.26.17 in /.kokoro (#96) Source-Link: https://github.com/googleapis/synthtool/commit/0c7b0333f44b2b7075447f43a121a12d15a7b76a Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:08e34975760f002746b1d8c86fdc90660be45945ee6d9db914d1508acdf9a547 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/requirements.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index a9bdb1b7ac0..dd98abbdeeb 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:fac304457974bb530cc5396abd4ab25d26a469cd3bc97cbfb18c8d4324c584eb -# created: 2023-10-02T21:31:03.517640371Z + digest: sha256:08e34975760f002746b1d8c86fdc90660be45945ee6d9db914d1508acdf9a547 +# created: 2023-10-09T14:06:13.397766266Z diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 96d593c8c82..0332d3267e1 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -467,9 +467,9 @@ typing-extensions==4.4.0 \ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via -r requirements.in -urllib3==1.26.12 \ - --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ - --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 +urllib3==1.26.17 \ + --hash=sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21 \ + --hash=sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b # via # requests # twine From 159231505f339173560cd802dae3fed3e63a663b Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:19:26 -0700 Subject: [PATCH 065/921] docs: add more preprocessing models into the docs menu. (#97) * docs: add more preprocessing models into the docs menu. * Trigger kokoro rebuild --- docs/templates/toc.yml | 12 ++++++-- .../sklearn/preprocessing/_data.py | 2 +- .../sklearn/preprocessing/_encoder.py | 30 +++++++++---------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 891f15a51b4..0758bb41d8a 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -121,10 +121,18 @@ - items: - name: Overview uid: bigframes.ml.preprocessing - - name: OneHotEncoder - uid: bigframes.ml.preprocessing.OneHotEncoder + - name: KBinsDiscretizer + uid: bigframes.ml.preprocessing.KBinsDiscretizer + - name: LabelEncoder + uid: bigframes.ml.preprocessing.LabelEncoder + - name: MaxAbsScaler + uid: bigframes.ml.preprocessing.MaxAbsScaler + - name: MinMaxScaler + uid: bigframes.ml.preprocessing.MinMaxScaler - name: StandardScaler uid: bigframes.ml.preprocessing.StandardScaler + - name: OneHotEncoder + uid: bigframes.ml.preprocessing.OneHotEncoder name: preprocessing name: bigframes.ml name: BigQuery DataFrames diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py index 58e16e135b7..5ce102d573f 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -29,7 +29,7 @@ class StandardScaler(BaseEstimator, TransformerMixin): individual features do not more or less look like standard normally distributed data (e.g. Gaussian with 0 mean and unit variance). - Examples: + **Examples:** .. code-block:: diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index cf660ece5d4..8da9a98c53f 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -17,6 +17,21 @@ class OneHotEncoder(BaseEstimator): Note that this method deviates from Scikit-Learn; instead of producing sparse binary columns, the encoding is a single column of `STRUCT`. + **Examples:** + + Given a dataset with two features, we let the encoder find the unique + values per feature and transform the data to a binary one-hot encoding. + + .. code-block:: + + from bigframes.ml.preprocessing import OneHotEncoder + import bigframes.pandas as bpd + + enc = OneHotEncoder() + X = bpd.DataFrame({"a": ["Male", "Female", "Female"], "b": ["1", "3", "2"]}) + enc.fit(X) + print(enc.transform(bpd.DataFrame({"a": ["Female", "Male"], "b": ["1", "4"]}))) + Args: drop (Optional[Literal["most_frequent"]], default None): Specifies a methodology to use to drop one of the categories per feature. @@ -37,21 +52,6 @@ class OneHotEncoder(BaseEstimator): when considering infrequent categories. If there are infrequent categories, max_categories includes the category representing the infrequent categories along with the frequent categories. Default None, set limit to 1,000,000. - - Examples: - - Given a dataset with two features, we let the encoder find the unique - values per feature and transform the data to a binary one-hot encoding. - - .. code-block:: - - from bigframes.ml.preprocessing import OneHotEncoder - import bigframes.pandas as bpd - - enc = OneHotEncoder() - X = bpd.DataFrame({"a": ["Male", "Female", "Female"], "b": ["1", "3", "2"]}) - enc.fit(X) - print(enc.transform(bpd.DataFrame({"a": ["Female", "Male"], "b": ["1", "4"]}))) """ def fit(self, X, y=None): From e1ee09b51acd75656f243a54027fd4154ef4bfd1 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:56:14 -0700 Subject: [PATCH 066/921] refactor: refactor the directory of the notebooks (#89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../bq_dataframes_llm_code_generation.ipynb | 392 +++++++++--------- .../bq_dataframes_ml_linear_regression.ipynb | 6 +- noxfile.py | 4 +- 3 files changed, 201 insertions(+), 201 deletions(-) rename notebooks/{getting_started => generative_ai}/bq_dataframes_llm_code_generation.ipynb (95%) rename notebooks/{getting_started => regression}/bq_dataframes_ml_linear_regression.ipynb (98%) diff --git a/notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb similarity index 95% rename from notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb rename to notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index 39e2ef535cc..2e4ce3e5109 100644 --- a/notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -34,18 +34,18 @@ "\n", "\n", " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", "
\n", - " \n", + " \n", " \"Colab Run in Colab\n", " \n", " \n", - " \n", + " \n", " \"GitHub\n", " View on GitHub\n", " \n", " \n", - " \n", + " \n", " \"Vertex\n", " Open in Vertex AI Workbench\n", " \n", @@ -162,6 +162,9 @@ }, { "cell_type": "markdown", + "metadata": { + "id": "Wbr2aVtFQBcg" + }, "source": [ "### Set up your Google Cloud project\n", "\n", @@ -183,10 +186,7 @@ " * Vertex AI API\n", "\n", "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." - ], - "metadata": { - "id": "Wbr2aVtFQBcg" - } + ] }, { "cell_type": "markdown", @@ -350,39 +350,44 @@ }, { "cell_type": "markdown", - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." - ], "metadata": { "id": "DTVtFlqeFbrU" - } + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] }, { "cell_type": "markdown", + "metadata": { + "id": "6eytf4xQHzcF" + }, "source": [ "# Define the LLM model\n", "\n", "BigQuery DataFrames provides integration with [`text-bison` model of the PaLM API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) via Vertex AI.\n", "\n", "This section walks through a few steps required in order to use the model in your notebook." - ], - "metadata": { - "id": "6eytf4xQHzcF" - } + ] }, { "cell_type": "markdown", + "metadata": { + "id": "rS4VO1TGiO4G" + }, "source": [ "## Create a BigQuery Cloud resource connection\n", "\n", "You need to create a [Cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection) to enable BigQuery DataFrames to interact with Vertex AI services." - ], - "metadata": { - "id": "rS4VO1TGiO4G" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KFPjDM4LVh96" + }, + "outputs": [], "source": [ "CONN_NAME = \"bqdf-llm\"\n", "\n", @@ -412,15 +417,13 @@ " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", " )\n", "print(CONN_SERVICE_ACCOUNT)" - ], - "metadata": { - "id": "KFPjDM4LVh96" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", + "metadata": { + "id": "W6l6Ol2biU9h" + }, "source": [ "## Set permissions for the service account\n", "\n", @@ -429,52 +432,52 @@ " - `roles/run.invoker`: This role is required for the connection to have read-only access to Cloud Run services that back custom/remote functions ([documentation](https://cloud.google.com/bigquery/docs/remote-functions#grant_permission_on_function)).\n", "\n", "Set these permissions by running the following `gcloud` commands:" - ], - "metadata": { - "id": "W6l6Ol2biU9h" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d8wja24SVq6s" + }, + "outputs": [], "source": [ "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'" - ], - "metadata": { - "id": "d8wja24SVq6s" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", + "metadata": { + "id": "qUjT8nw-jIXp" + }, "source": [ "## Define the model\n", "\n", "Use `bigframes.ml.llm` to define the model:" - ], - "metadata": { - "id": "qUjT8nw-jIXp" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sdjeXFwcHfl7" + }, + "outputs": [], "source": [ "from bigframes.ml.llm import PaLM2TextGenerator\n", "\n", "session = bf.get_global_session()\n", "connection = f\"{PROJECT_ID}.{REGION}.{CONN_NAME}\"\n", "model = PaLM2TextGenerator(session=session, connection_name=connection)" - ], - "metadata": { - "id": "sdjeXFwcHfl7" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", + "metadata": { + "id": "GbW0oCnU1s1N" + }, "source": [ "# Read data from Cloud Storage into BigQuery DataFrames\n", "\n", @@ -486,80 +489,82 @@ "* An in-memory pandas DataFrame\n", "\n", "In this tutorial, you create BigQuery DataFrames DataFrames by reading two CSV files stored in Cloud Storage, one containing a list of DataFrame API names and one containing a list of Series API names." - ], - "metadata": { - "id": "GbW0oCnU1s1N" - } + ] }, { "cell_type": "code", - "source": [ - "df_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/df.csv\")\n", - "series_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/series.csv\")" - ], + "execution_count": null, "metadata": { "id": "SchiTkQGIJog" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "df_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/df.csv\")\n", + "series_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/series.csv\")" + ] }, { "cell_type": "markdown", - "source": [ - "Take a peek at a few rows of data for each file:" - ], "metadata": { "id": "7OBjw2nmQY3-" - } + }, + "source": [ + "Take a peek at a few rows of data for each file:" + ] }, { "cell_type": "code", - "source": [ - "df_api.head(2)" - ], + "execution_count": null, "metadata": { "id": "QCqgVCIsGGuv" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "df_api.head(2)" + ] }, { "cell_type": "code", - "source": [ - "series_api.head(2)" - ], + "execution_count": null, "metadata": { "id": "BGJnZbgEGS5-" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "series_api.head(2)" + ] }, { "cell_type": "markdown", + "metadata": { + "id": "m3ZJEsi7SUKV" + }, "source": [ "# Generate code using the LLM model\n", "\n", "Prepare the prompts and send them to the LLM model for prediction." - ], - "metadata": { - "id": "m3ZJEsi7SUKV" - } + ] }, { "cell_type": "markdown", + "metadata": { + "id": "9EMAqR37AfLS" + }, "source": [ "## Prompt design in BigQuery DataFrames\n", "\n", "Designing prompts for LLMs is a fast growing area and you can read more in [this documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/introduction-prompt-design).\n", "\n", "For this tutorial, you use a simple prompt to ask the LLM model for sample code for each of the API methods (or rows) from the last step's DataFrames. The output is the new DataFrames `df_prompt` and `series_prompt`, which contain the full prompt text." - ], - "metadata": { - "id": "9EMAqR37AfLS" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "outputs": [], "source": [ "df_prompt_prefix = \"Generate Pandas sample code for DataFrame.\"\n", "series_prompt_prefix = \"Generate Pandas sample code for Series.\"\n", @@ -568,83 +573,83 @@ "series_prompt = (series_prompt_prefix + series_api['API'])\n", "\n", "df_prompt.head(2)" - ], - "metadata": { - "id": "EDAaIwHpQCDZ" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", + "metadata": { + "id": "rwPLjqW2Ajzh" + }, "source": [ "## Make predictions using the LLM model\n", "\n", "Use the BigQuery DataFrames DataFrame containing the full prompt text as the input to the `predict` method. The `predict` method calls the LLM model and returns its generated text output back to two new BigQuery DataFrames DataFrames, `df_pred` and `series_pred`.\n", "\n", "Note: The predictions might take a few minutes to run." - ], - "metadata": { - "id": "rwPLjqW2Ajzh" - } + ] }, { "cell_type": "code", - "source": [ - "df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024)\n", - "series_pred = model.predict(series_prompt.to_frame(), max_output_tokens=1024)" - ], + "execution_count": null, "metadata": { "id": "6i6HkFJZa8na" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024)\n", + "series_pred = model.predict(series_prompt.to_frame(), max_output_tokens=1024)" + ] }, { "cell_type": "markdown", - "source": [ - "Once the predictions are processed, take a look at the sample output from the LLM, which provides code samples for the API names listed in the DataFrames dataset." - ], "metadata": { "id": "89cB8MW4UIdV" - } + }, + "source": [ + "Once the predictions are processed, take a look at the sample output from the LLM, which provides code samples for the API names listed in the DataFrames dataset." + ] }, { "cell_type": "code", - "source": [ - "print(df_pred['ml_generate_text_llm_result'].iloc[0])" - ], + "execution_count": null, "metadata": { "id": "9A2gw6hP_2nX" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "print(df_pred['ml_generate_text_llm_result'].iloc[0])" + ] }, { "cell_type": "markdown", + "metadata": { + "id": "Fx4lsNqMorJ-" + }, "source": [ "# Manipulate LLM output using a remote function\n", "\n", "The output that the LLM provides often contains additional text beyond the code sample itself. Using BigQuery DataFrames, you can deploy custom Python functions that process and transform this output.\n", "\n" - ], - "metadata": { - "id": "Fx4lsNqMorJ-" - } + ] }, { "cell_type": "markdown", + "metadata": { + "id": "d8L7SN03VByG" + }, "source": [ "Running the cell below creates a custom function that you can use to process the LLM output data in two ways:\n", "1. Strip the LLM text output to include only the code block.\n", "2. Substitute `import pandas as pd` with `import bigframes.pandas as bf` so that the resulting code block works with BigQuery DataFrames." - ], - "metadata": { - "id": "d8L7SN03VByG" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GskyyUQPowBT" + }, + "outputs": [], "source": [ "@bf.remote_function([str], str, bigquery_connection=CONN_NAME)\n", "def extract_code(text: str):\n", @@ -656,166 +661,161 @@ " return res\n", " except:\n", " return \"\"" - ], - "metadata": { - "id": "GskyyUQPowBT" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "The custom function is deployed as a Cloud Function, and then integrated with BigQuery as a [remote function](https://cloud.google.com/bigquery/docs/remote-functions). Save both of the function names so that you can clean them up at the end of this notebook." - ], "metadata": { "id": "hVQAoqBUOJQf" - } + }, + "source": [ + "The custom function is deployed as a Cloud Function, and then integrated with BigQuery as a [remote function](https://cloud.google.com/bigquery/docs/remote-functions). Save both of the function names so that you can clean them up at the end of this notebook." + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PBlp-C-DOHRO" + }, + "outputs": [], "source": [ "CLOUD_FUNCTION_NAME = format(extract_code.bigframes_cloud_function)\n", "print(\"Cloud Function Name \" + CLOUD_FUNCTION_NAME)\n", "REMOTE_FUNCTION_NAME = format(extract_code.bigframes_remote_function)\n", "print(\"Remote Function Name \" + REMOTE_FUNCTION_NAME)" - ], - "metadata": { - "id": "PBlp-C-DOHRO" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "Apply the custom function to each LLM output DataFrame to get the processed results:" - ], "metadata": { "id": "4FEucaiqVs3H" - } + }, + "source": [ + "Apply the custom function to each LLM output DataFrame to get the processed results:" + ] }, { "cell_type": "code", - "source": [ - "df_code = df_pred.assign(code=df_pred['ml_generate_text_llm_result'].apply(extract_code))\n", - "series_code = series_pred.assign(code=series_pred['ml_generate_text_llm_result'].apply(extract_code))" - ], + "execution_count": null, "metadata": { "id": "bsQ9cmoWo0Ps" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "df_code = df_pred.assign(code=df_pred['ml_generate_text_llm_result'].apply(extract_code))\n", + "series_code = series_pred.assign(code=series_pred['ml_generate_text_llm_result'].apply(extract_code))" + ] }, { "cell_type": "markdown", - "source": [ - "You can see the differences by inspecting the first row of data:" - ], "metadata": { "id": "ujQVVuhfWA3y" - } + }, + "source": [ + "You can see the differences by inspecting the first row of data:" + ] }, { "cell_type": "code", - "source": [ - "print(df_code['code'].iloc[0])" - ], + "execution_count": null, "metadata": { "id": "7yWzjhGy_zcy" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "print(df_code['code'].iloc[0])" + ] }, { "cell_type": "markdown", + "metadata": { + "id": "GTRdUw-Ro5R1" + }, "source": [ "# Save the results to Cloud Storage\n", "\n", "BigQuery DataFrames lets you save a BigQuery DataFrames DataFrame as a CSV file in Cloud Storage for further use. Try that now with your processed LLM output data." - ], - "metadata": { - "id": "GTRdUw-Ro5R1" - } + ] }, { "cell_type": "markdown", - "source": [ - "Create a new Cloud Storage bucket with a unique name:" - ], "metadata": { "id": "9DQ7eiQxPTi3" - } + }, + "source": [ + "Create a new Cloud Storage bucket with a unique name:" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-J5LHgS6LLZ0" + }, + "outputs": [], "source": [ "import uuid\n", "BUCKET_ID = \"code-samples-\" + str(uuid.uuid1())\n", "\n", "!gsutil mb gs://{BUCKET_ID}" - ], - "metadata": { - "id": "-J5LHgS6LLZ0" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "Use `to_csv` to write each BigQuery DataFrames DataFrame as a CSV file in the Cloud Storage bucket:" - ], "metadata": { "id": "tyxZXj0UPYUv" - } + }, + "source": [ + "Use `to_csv` to write each BigQuery DataFrames DataFrame as a CSV file in the Cloud Storage bucket:" + ] }, { "cell_type": "code", - "source": [ - "df_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/df_code*.csv\")\n", - "series_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/series_code*.csv\")" - ], + "execution_count": null, "metadata": { "id": "Zs_b5L-4IvER" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "df_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/df_code*.csv\")\n", + "series_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/series_code*.csv\")" + ] }, { "cell_type": "markdown", + "metadata": { + "id": "UDBtDlrTuuh8" + }, "source": [ "You can navigate to the Cloud Storage bucket browser to download the two files and view them.\n", "\n", "Run the following cell, and then follow the link to your Cloud Storage bucket browser:" - ], - "metadata": { - "id": "UDBtDlrTuuh8" - } + ] }, { "cell_type": "code", - "source": [ - "print(f'https://console.developers.google.com/storage/browser/{BUCKET_ID}/')" - ], + "execution_count": null, "metadata": { "id": "PspCXu-qu_ND" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "print(f'https://console.developers.google.com/storage/browser/{BUCKET_ID}/')" + ] }, { "cell_type": "markdown", + "metadata": { + "id": "RGSvUk48RK20" + }, "source": [ "# Summary and next steps\n", "\n", "You've used BigQuery DataFrames' integration with LLM models (`bigframes.ml.llm`) to generate code samples, and have tranformed LLM output by creating and using a custom function in BigQuery DataFrames.\n", "\n", "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." - ], - "metadata": { - "id": "RGSvUk48RK20" - } + ] }, { "cell_type": "markdown", @@ -833,6 +833,11 @@ }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yw7A461XLjvW" + }, + "outputs": [], "source": [ "# # Delete the BigQuery Connection\n", "# from google.cloud import bigquery_connection_v1 as bq_connection\n", @@ -840,12 +845,7 @@ "# CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", "# client.delete_connection(name=CONNECTION_ID)\n", "# print(f\"Deleted connection '{CONNECTION_ID}'.\")" - ], - "metadata": { - "id": "yw7A461XLjvW" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "code", @@ -864,22 +864,22 @@ }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iQFo6OUBLmi3" + }, + "outputs": [], "source": [ "# # Delete the Google Cloud Storage bucket and files\n", "# ! gsutil rm -r gs://{BUCKET_ID}\n", "# print(f\"Deleted bucket '{BUCKET_ID}'.\")" - ], - "metadata": { - "id": "iQFo6OUBLmi3" - }, - "execution_count": null, - "outputs": [] + ] } ], "metadata": { "colab": { - "toc_visible": true, - "provenance": [] + "provenance": [], + "toc_visible": true }, "kernelspec": { "display_name": "Python 3", diff --git a/notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb b/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb similarity index 98% rename from notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb rename to notebooks/regression/bq_dataframes_ml_linear_regression.ipynb index d317217810f..338d6edf4fb 100644 --- a/notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb +++ b/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb @@ -35,18 +35,18 @@ "\n", "\n", " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
\n", - " \n", + " \n", " \"Colab Run in Colab\n", " \n", " \n", - " \n", + " \n", " \"GitHub\n", " View on GitHub\n", " \n", " \n", - " \n", + " \n", " \"Vertex\n", " Open in Vertex AI Workbench\n", " \n", diff --git a/noxfile.py b/noxfile.py index a113e1fcde4..84e5ab11bb4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -607,8 +607,8 @@ def notebook(session): # appropriate values and omitting cleanup logic that may break # our test infrastructure. "notebooks/getting_started/getting_started_bq_dataframes.ipynb", - "notebooks/getting_started/bq_dataframes_llm_code_generation.ipynb", - "notebooks/getting_started/bq_dataframes_ml_linear_regression.ipynb", + "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", + "notebooks/regression/bq_dataframes_ml_linear_regression.ipynb", "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", "notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb", From 228aeba09782ae2421040c7601c15d4af92790b6 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 10 Oct 2023 11:00:03 -0700 Subject: [PATCH 067/921] feat: add iat and iloc accessing by tuples of integers (#90) * feat: add iat indexers * select_columns correction * add iloc for tuples * add dataframe tests * add series tests * add third party comments --------- Co-authored-by: Henry J Solberg --- bigframes/core/indexers.py | 44 +++++++++++++++ bigframes/dataframe.py | 4 ++ bigframes/series.py | 4 ++ tests/system/small/test_dataframe.py | 55 ++++++++++++++++++- tests/system/small/test_series.py | 14 +++++ .../bigframes_vendored/pandas/core/frame.py | 10 ++++ .../bigframes_vendored/pandas/core/series.py | 10 ++++ 7 files changed, 140 insertions(+), 1 deletion(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 1a88b2abd60..a74880041ca 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -97,6 +97,16 @@ def __getitem__( return _iloc_getitem_series_or_dataframe(self._series, key) +class IatSeriesIndexer: + def __init__(self, series: bigframes.series.Series): + self._series = series + + def __getitem__(self, key: int) -> bigframes.core.scalar.Scalar: + if not isinstance(key, int): + raise ValueError("Series iAt based indexing can only have integer indexers") + return self._series.iloc[key] + + class LocDataFrameIndexer: def __init__(self, dataframe: bigframes.dataframe.DataFrame): self._dataframe = dataframe @@ -188,6 +198,28 @@ def __getitem__(self, key) -> Union[bigframes.dataframe.DataFrame, pd.Series]: return _iloc_getitem_series_or_dataframe(self._dataframe, key) +class IatDataFrameIndexer: + def __init__(self, dataframe: bigframes.dataframe.DataFrame): + self._dataframe = dataframe + + def __getitem__(self, key: tuple) -> bigframes.core.scalar.Scalar: + error_message = "DataFrame.iat should be indexed by a tuple of exactly 2 ints" + # we raise TypeError or ValueError under the same conditions that pandas does + if isinstance(key, int): + raise TypeError(error_message) + if not isinstance(key, tuple): + raise ValueError(error_message) + key_values_are_ints = [isinstance(key_value, int) for key_value in key] + if not all(key_values_are_ints): + raise ValueError(error_message) + if len(key) != 2: + raise TypeError(error_message) + block = self._dataframe._block + column_block = block.select_columns([block.value_columns[key[1]]]) + column = bigframes.series.Series(column_block) + return column.iloc[key[0]] + + @typing.overload def _loc_getitem_series_or_dataframe( series_or_dataframe: bigframes.series.Series, key @@ -356,6 +388,18 @@ def _iloc_getitem_series_or_dataframe( return result_pd_df.iloc[0] elif isinstance(key, slice): return series_or_dataframe._slice(key.start, key.stop, key.step) + elif isinstance(key, tuple) and len(key) == 0: + return series_or_dataframe + elif isinstance(key, tuple) and len(key) == 1: + return _iloc_getitem_series_or_dataframe(series_or_dataframe, key[0]) + elif ( + isinstance(key, tuple) + and isinstance(series_or_dataframe, bigframes.dataframe.DataFrame) + and len(key) == 2 + ): + return series_or_dataframe.iat[key] + elif isinstance(key, tuple): + raise pd.errors.IndexingError("Too many indexers") elif pd.api.types.is_list_like(key): if len(key) == 0: return typing.cast( diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 87d0f21b629..d5b2fa86e9d 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -254,6 +254,10 @@ def loc(self) -> indexers.LocDataFrameIndexer: def iloc(self) -> indexers.ILocDataFrameIndexer: return indexers.ILocDataFrameIndexer(self) + @property + def iat(self) -> indexers.IatDataFrameIndexer: + return indexers.IatDataFrameIndexer(self) + @property def dtypes(self) -> pandas.Series: return pandas.Series(data=self._block.dtypes, index=self._block.column_labels) diff --git a/bigframes/series.py b/bigframes/series.py index e5afe91e445..56e1b43a032 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -87,6 +87,10 @@ def loc(self) -> bigframes.core.indexers.LocSeriesIndexer: def iloc(self) -> bigframes.core.indexers.IlocSeriesIndexer: return bigframes.core.indexers.IlocSeriesIndexer(self) + @property + def iat(self) -> bigframes.core.indexers.IatSeriesIndexer: + return bigframes.core.indexers.IatSeriesIndexer(self) + @property def name(self) -> blocks.Label: return self._name diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 711da10c55c..0262a31a92e 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2077,7 +2077,7 @@ def test_iloc_slice_nested(scalars_df_index, scalars_pandas_df_index): @pytest.mark.parametrize( "index", - [0, 5, -2], + [0, 5, -2, (2,)], ) def test_iloc_single_integer(scalars_df_index, scalars_pandas_df_index, index): bf_result = scalars_df_index.iloc[index] @@ -2089,6 +2089,59 @@ def test_iloc_single_integer(scalars_df_index, scalars_pandas_df_index, index): ) +@pytest.mark.parametrize( + "index", + [(2, 5), (5, 0), (0, 0)], +) +def test_iloc_tuple(scalars_df_index, scalars_pandas_df_index, index): + bf_result = scalars_df_index.iloc[index] + pd_result = scalars_pandas_df_index.iloc[index] + + assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("index", "error"), + [ + ((1, 1, 1), pd.errors.IndexingError), + (("asd", "asd", "asd"), pd.errors.IndexingError), + (("asd"), TypeError), + ], +) +def test_iloc_tuple_errors(scalars_df_index, scalars_pandas_df_index, index, error): + with pytest.raises(error): + scalars_df_index.iloc[index] + with pytest.raises(error): + scalars_pandas_df_index.iloc[index] + + +@pytest.mark.parametrize( + "index", + [(2, 5), (5, 0), (0, 0)], +) +def test_iat(scalars_df_index, scalars_pandas_df_index, index): + bf_result = scalars_df_index.iat[index] + pd_result = scalars_pandas_df_index.iat[index] + + assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("index", "error"), + [ + (0, TypeError), + ("asd", ValueError), + ((1, 2, 3), TypeError), + (("asd", "asd"), ValueError), + ], +) +def test_iat_errors(scalars_df_index, scalars_pandas_df_index, index, error): + with pytest.raises(error): + scalars_pandas_df_index.iat[index] + with pytest.raises(error): + scalars_df_index.iat[index] + + def test_iloc_single_integer_out_of_bound_error( scalars_df_index, scalars_pandas_df_index ): diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 993df18c959..802425510aa 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1953,6 +1953,20 @@ def test_series_iloc(scalars_df_index, scalars_pandas_df_index, start, stop, ste ) +def test_iat(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index["int64_too"].iat[3] + pd_result = scalars_pandas_df_index["int64_too"].iat[3] + + assert bf_result == pd_result + + +def test_iat_error(scalars_df_index, scalars_pandas_df_index): + with pytest.raises(ValueError): + scalars_pandas_df_index["int64_too"].iat["asd"] + with pytest.raises(ValueError): + scalars_df_index["int64_too"].iat["asd"] + + def test_series_add_prefix(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index["int64_too"].add_prefix("prefix_").to_pandas() diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 644e043e83d..80a5428b36d 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2097,3 +2097,13 @@ def fillna(self, value): DataFrame: Object with missing values filled """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def iloc(self): + """Purely integer-location based indexing for selection by position.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def iat(self): + """Access a single value for a row/column pair by integer position.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 970ab1d8b43..03729922d5a 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1823,3 +1823,13 @@ def map( Series: Same index as caller. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def iloc(self): + """Purely integer-location based indexing for selection by position.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def iat(self): + """Access a single value for a row/column pair by integer position.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 97b8bec1175499c74448a4fd46b4888c4b4c35c1 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 10 Oct 2023 15:10:26 -0700 Subject: [PATCH 068/921] feat: add level param to DataFrame.stack (#88) * feat: add level param to DataFrame.stack --- bigframes/core/block_transforms.py | 2 +- bigframes/core/blocks.py | 12 +++---- bigframes/dataframe.py | 49 ++++++++++++++++++++------- tests/system/small/test_dataframe.py | 4 ++- tests/system/small/test_multiindex.py | 38 ++++++++++++++------- 5 files changed, 72 insertions(+), 33 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 4c30d7631dc..904da7f3122 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -53,7 +53,7 @@ def equals(block1: blocks.Block, block2: blocks.Block) -> bool: joined_block = joined_block.select_columns(equality_ids).with_column_labels( list(range(len(equality_ids))) ) - stacked_block = joined_block.stack(dropna=False, sort=False) + stacked_block = joined_block.stack() result = stacked_block.get_stat(stacked_block.value_columns[0], agg_ops.all_op) return typing.cast(bool, result) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 852b1fe02ab..9b49645c71d 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1284,20 +1284,20 @@ def pivot( return result_block.with_column_labels(column_index) - def stack(self, how="left", dropna=True, sort=True, levels: int = 1): + def stack(self, how="left", levels: int = 1): """Unpivot last column axis level into row axis""" + if levels == 0: + return self + # These are the values that will be turned into rows col_labels, row_labels = utils.split_index(self.column_labels, levels=levels) - if dropna: - row_labels = row_labels.drop_duplicates() - if sort: - row_labels = row_labels.sort_values() + row_labels = row_labels.drop_duplicates() row_label_tuples = utils.index_as_tuples(row_labels) if col_labels is not None: - result_index = col_labels.drop_duplicates().sort_values().dropna(how="all") + result_index = col_labels.drop_duplicates().dropna(how="all") result_col_labels = utils.index_as_tuples(result_index) else: result_index = pd.Index([None]) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d5b2fa86e9d..3ec1b4b6175 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1741,24 +1741,49 @@ def pivot( ) return DataFrame(pivot_block) - def stack(self): - # TODO: support 'level' param by simply reordering levels such that selected level is last before passing to Block.stack. - # TODO: match impl to pandas future_stack as described in pandas 2.1 release notes - stack_block = self._block.stack() - result_block = block_ops.dropna( - stack_block, stack_block.value_columns, how="all" - ) + def stack(self, level: LevelsType = -1): if not isinstance(self.columns, pandas.MultiIndex): - return bigframes.series.Series(result_block) - return DataFrame(result_block) + if level not in [0, -1, self.columns.name]: + raise IndexError(f"Invalid level {level} for single-level index") + return self._stack_mono() + return self._stack_multi(level) + + def _stack_mono(self): + result_block = self._block.stack() + return bigframes.series.Series(result_block) + + def _stack_multi(self, level: LevelsType = -1): + n_levels = self.columns.nlevels + if isinstance(level, int) or isinstance(level, str): + level = [level] + level_indices = [] + for level_ref in level: + if isinstance(level_ref, int): + if level_ref < 0: + level_indices.append(n_levels + level_ref) + else: + level_indices.append(level_ref) + else: # str + level_indices.append(self.columns.names.index(level_ref)) + + new_order = [ + *[i for i in range(n_levels) if i not in level_indices], + *level_indices, + ] + + original_columns = typing.cast(pandas.MultiIndex, self.columns) + new_columns = original_columns.reorder_levels(new_order) + + block = self._block.with_column_labels(new_columns) + + block = block.stack(levels=len(level)) + return DataFrame(block) def unstack(self): block = self._block # Special case, unstack with mono-index transpose into a series if self.index.nlevels == 1: - block = block.stack( - how="right", dropna=False, sort=False, levels=self.columns.nlevels - ) + block = block.stack(how="right", levels=self.columns.nlevels) return bigframes.series.Series(block) # Pivot by last level of index diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 0262a31a92e..9f1092d09d1 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1885,6 +1885,8 @@ def test_df_describe(scalars_dfs): def test_df_stack(scalars_dfs): + if pandas.__version__.startswith("1.") or pandas.__version__.startswith("2.0"): + pytest.skip("pandas <2.1 uses different stack implementation") scalars_df, scalars_pandas_df = scalars_dfs # To match bigquery dataframes scalars_pandas_df = scalars_pandas_df.copy() @@ -1893,7 +1895,7 @@ def test_df_stack(scalars_dfs): columns = ["int64_col", "int64_too", "rowindex_2"] bf_result = scalars_df[columns].stack().to_pandas() - pd_result = scalars_pandas_df[columns].stack() + pd_result = scalars_pandas_df[columns].stack(future_stack=True) # Pandas produces NaN, where bq dataframes produces pd.NA pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 19f1c557ef2..a1326767700 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -718,25 +718,37 @@ def test_column_multi_index_cumsum(scalars_df_index, scalars_pandas_df_index): pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) -def test_column_multi_index_stack(scalars_df_index, scalars_pandas_df_index): - columns = ["int64_too", "int64_col", "rowindex_2"] +@pytest.mark.parametrize( + ("level",), + [(["l3", "l1"],), ([-2, -1],), (["l3"],), ("l2",), (-3,)], +) +def test_column_multi_index_stack(level): + if pandas.__version__.startswith("1.") or pandas.__version__.startswith("2.0"): + pytest.skip("pandas <2.1 uses different stack implementation") + level1 = pandas.Index(["b", "a", "b"]) - # Need resulting column to be pyarrow string rather than object dtype - level2 = pandas.Index(["a", "b", "b"], dtype="string[pyarrow]") - multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) - bf_df = scalars_df_index[columns].copy() - bf_df.columns = multi_columns - pd_df = scalars_pandas_df_index[columns].copy() - pd_df.columns = multi_columns + level2 = pandas.Index(["a", "b", "b"]) + level3 = pandas.Index(["b", "b", "a"]) - bf_result = bf_df.stack().to_pandas() - # Shifting sort behavior in stack - pd_result = pd_df.stack() + multi_columns = pandas.MultiIndex.from_arrays( + [level1, level2, level3], names=["l1", "l2", "l3"] + ) + pd_df = pandas.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=[5, 2, None], + columns=multi_columns, + dtype="Int64", + ) + bf_df = bpd.DataFrame(pd_df) + + bf_result = bf_df.stack(level=level).to_pandas() + # BigFrames emulates future_stack impl + pd_result = pd_df.stack(level=level, future_stack=True) # Pandas produces NaN, where bq dataframes produces pd.NA # Column ordering seems to depend on pandas version pandas.testing.assert_frame_equal( - bf_result.sort_index(axis=1), pd_result.sort_index(axis=1), check_dtype=False + bf_result, pd_result, check_dtype=False, check_index_type=False ) From 4ae4ef995348b95521c4988a8cfb3b5ac792fd69 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 11 Oct 2023 09:49:13 -0700 Subject: [PATCH 069/921] feat: use default session connection (#87) --- bigframes/_config/bigquery_options.py | 4 +- bigframes/clients.py | 24 +++++++ bigframes/ml/llm.py | 23 +++++-- bigframes/remote_function.py | 74 +++++++++------------- bigframes/session.py | 6 +- tests/system/small/ml/test_llm.py | 37 ++++++++++- tests/system/small/test_remote_function.py | 31 +++++++++ tests/unit/test_clients.py | 57 +++++++++++++++++ 8 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 tests/unit/test_clients.py diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index ea1864ed5f7..eb56de826a8 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -83,12 +83,14 @@ def project(self, value: Optional[str]): @property def bq_connection(self) -> Optional[str]: - """Name of the BigQuery connection to use. + """Name of the BigQuery connection to use. Should be of the form ... You should either have the connection already created in the location you have chosen, or you should have the Project IAM Admin role to enable the service to create the connection for you if you need it. + + If this option isn't provided, or project or location aren't provided, session will use its default project/location/connection_id as default connection. """ return self._bq_connection diff --git a/bigframes/clients.py b/bigframes/clients.py index b60fcba04af..dcac611e8cf 100644 --- a/bigframes/clients.py +++ b/bigframes/clients.py @@ -29,6 +29,8 @@ ) logger = logging.getLogger(__name__) +_BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" + class BqConnectionManager: """Manager to handle operations with BQ connections.""" @@ -162,3 +164,25 @@ def _get_service_account_if_connection_exists( pass return service_account + + +def get_connection_name_full( + connection_name: Optional[str], default_project: str, default_location: str +) -> str: + """Retrieve the full connection name of the form ... + Use default project, location or connection_id when any of them are missing.""" + if connection_name is None: + return ( + f"{default_project}.{default_location}.{_BIGFRAMES_DEFAULT_CONNECTION_ID}" + ) + + if connection_name.count(".") == 2: + return connection_name + + if connection_name.count(".") == 1: + return f"{default_project}.{connection_name}" + + if connection_name.count(".") == 0: + return f"{default_project}.{default_location}.{connection_name}" + + raise ValueError(f"Invalid connection name format: {connection_name}.") diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index c86e5fb3b67..a61dd34e6de 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -38,8 +38,9 @@ class PaLM2TextGenerator(base.Predictor): session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. connection_name (str or None): - connection to connect with remote service. str of the format ... - if None, use default connection in session context. + connection to connect with remote service. str of the format ... + if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach + permission if the connection isn't fully setup. """ def __init__( @@ -48,7 +49,14 @@ def __init__( connection_name: Optional[str] = None, ): self.session = session or bpd.get_global_session() - self.connection_name = connection_name or self.session._bq_connection + + connection_name = connection_name or self.session._bq_connection + self.connection_name = clients.get_connection_name_full( + connection_name, + default_project=self.session._project, + default_location=self.session._location, + ) + self._bq_connection_manager = clients.BqConnectionManager( self.session.bqconnectionclient, self.session.resourcemanagerclient ) @@ -180,7 +188,14 @@ def __init__( connection_name: Optional[str] = None, ): self.session = session or bpd.get_global_session() - self.connection_name = connection_name or self.session._bq_connection + + connection_name = connection_name or self.session._bq_connection + self.connection_name = clients.get_connection_name_full( + connection_name, + default_project=self.session._project, + default_location=self.session._location, + ) + self._bq_connection_manager = clients.BqConnectionManager( self.session.bqconnectionclient, self.session.resourcemanagerclient ) diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 6fc2f8e59fb..37c7a2fc64b 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -695,9 +695,12 @@ def remote_function( persistent name. """ + import bigframes.pandas as bpd + + session = session or bpd.get_global_session() # A BigQuery client is required to perform BQ operations - if not bigquery_client and session: + if not bigquery_client: bigquery_client = session.bqclient if not bigquery_client: raise ValueError( @@ -706,7 +709,7 @@ def remote_function( ) # A BigQuery connection client is required to perform BQ connection operations - if not bigquery_connection_client and session: + if not bigquery_connection_client: bigquery_connection_client = session.bqconnectionclient if not bigquery_connection_client: raise ValueError( @@ -716,8 +719,7 @@ def remote_function( # A cloud functions client is required to perform cloud functions operations if not cloud_functions_client: - if session: - cloud_functions_client = session.cloudfunctionsclient + cloud_functions_client = session.cloudfunctionsclient if not cloud_functions_client: raise ValueError( "A cloud functions client must be provided, either directly or via session. " @@ -726,8 +728,7 @@ def remote_function( # A resource manager client is required to get/set IAM operations if not resource_manager_client: - if session: - resource_manager_client = session.resourcemanagerclient + resource_manager_client = session.resourcemanagerclient if not resource_manager_client: raise ValueError( "A resource manager client must be provided, either directly or via session. " @@ -740,15 +741,10 @@ def remote_function( dataset_ref = bigquery.DatasetReference.from_string( dataset, default_project=bigquery_client.project ) - elif session: + else: dataset_ref = bigquery.DatasetReference.from_string( session._session_dataset_id, default_project=bigquery_client.project ) - else: - raise ValueError( - "Project and dataset must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) bq_location, cloud_function_region = get_remote_function_locations( bigquery_client.location @@ -756,40 +752,30 @@ def remote_function( # A connection is required for BQ remote function # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function - if not bigquery_connection and session: - bigquery_connection = session._bq_connection # type: ignore if not bigquery_connection: + bigquery_connection = session._bq_connection # type: ignore + + bigquery_connection = clients.get_connection_name_full( + bigquery_connection, + default_project=dataset_ref.project, + default_location=bq_location, + ) + # Guaranteed to be the form of .. + ( + gcp_project_id, + bq_connection_location, + bq_connection_id, + ) = bigquery_connection.split(".") + if gcp_project_id.casefold() != dataset_ref.project.casefold(): raise ValueError( - "BigQuery connection must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" + "The project_id does not match BigQuery connection gcp_project_id: " + f"{dataset_ref.project}." + ) + if bq_connection_location.casefold() != bq_location.casefold(): + raise ValueError( + "The location does not match BigQuery connection location: " + f"{bq_location}." ) - - # Check connection_id with `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID` format. - if bigquery_connection.count(".") == 1: - bq_connection_location, bq_connection_id = bigquery_connection.split(".") - if bq_connection_location.casefold() != bq_location.casefold(): - raise ValueError( - "The location does not match BigQuery connection location: " - f"{bq_location}." - ) - bigquery_connection = bq_connection_id - elif bigquery_connection.count(".") == 2: - ( - gcp_project_id, - bq_connection_location, - bq_connection_id, - ) = bigquery_connection.split(".") - if gcp_project_id.casefold() != dataset_ref.project.casefold(): - raise ValueError( - "The project_id does not match BigQuery connection gcp_project_id: " - f"{dataset_ref.project}." - ) - if bq_connection_location.casefold() != bq_location.casefold(): - raise ValueError( - "The location does not match BigQuery connection location: " - f"{bq_location}." - ) - bigquery_connection = bq_connection_id def wrapper(f): if not callable(f): @@ -808,7 +794,7 @@ def wrapper(f): dataset_ref.dataset_id, bigquery_client, bigquery_connection_client, - bigquery_connection, + bq_connection_id, resource_manager_client, ) diff --git a/bigframes/session.py b/bigframes/session.py index ac48c977cb0..a7cb78e3ffb 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -350,10 +350,14 @@ def resourcemanagerclient(self): @property def _session_dataset_id(self): """A dataset for storing temporary objects local to the session - This is a workaround for BQML models and remote functions that do not + This is a workaround for remote functions that do not yet support session-temporary instances.""" return self._session_dataset.dataset_id + @property + def _project(self): + return self.bqclient.project + def _create_and_bind_bq_session(self): """Create a BQ session and bind the session id with clients to capture BQ activities: go/bigframes-transient-data""" diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 7486277487d..e546c09f97a 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -26,7 +26,8 @@ def test_create_text_generator_model(palm2_text_generator_model): assert palm2_text_generator_model._bqml_model is not None -def test_create_text_generator_model_defaults(bq_connection): +@pytest.mark.flaky(retries=2, delay=120) +def test_create_text_generator_model_default_session(bq_connection, llm_text_pandas_df): import bigframes.pandas as bpd bpd.reset_session() @@ -36,6 +37,40 @@ def test_create_text_generator_model_defaults(bq_connection): model = llm.PaLM2TextGenerator() assert model is not None assert model._bqml_model is not None + assert model.connection_name.casefold() == "bigframes-dev.us.bigframes-rf-conn" + + llm_text_df = bpd.read_pandas(llm_text_pandas_df) + + df = model.predict(llm_text_df).to_pandas() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_create_text_generator_model_default_connection(llm_text_pandas_df): + from bigframes import _config + import bigframes.pandas as bpd + + bpd.reset_session() + _config.options = _config.Options() # reset configs + + llm_text_df = bpd.read_pandas(llm_text_pandas_df) + + model = llm.PaLM2TextGenerator() + assert model is not None + assert model._bqml_model is not None + assert ( + model.connection_name.casefold() + == "bigframes-dev.us.bigframes-default-connection" + ) + + df = model.predict(llm_text_df).to_pandas() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) # Marked as flaky only because BQML LLM is in preview, the service only has limited capacity, not stable enough. diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 77fb81d2c93..d024a57dedc 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -20,6 +20,7 @@ import bigframes from bigframes import remote_function as rf +import bigframes.pandas as bpd from tests.system.utils import assert_pandas_df_equal_ignore_ordering @@ -465,6 +466,36 @@ def square(x): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_default_connection(scalars_dfs, dataset_id): + @bpd.remote_function([int], int, dataset=dataset_id) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pd.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + + @pytest.mark.flaky(retries=2, delay=120) def test_dataframe_applymap(session_with_bq_connection, scalars_dfs): def add_one(x): diff --git a/tests/unit/test_clients.py b/tests/unit/test_clients.py new file mode 100644 index 00000000000..a90e5b0320b --- /dev/null +++ b/tests/unit/test_clients.py @@ -0,0 +1,57 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from bigframes import clients + + +def test_get_connection_name_full_none(): + connection_name = clients.get_connection_name_full( + None, default_project="default-project", default_location="us" + ) + assert connection_name == "default-project.us.bigframes-default-connection" + + +def test_get_connection_name_full_connection_id(): + connection_name = clients.get_connection_name_full( + "connection-id", default_project="default-project", default_location="us" + ) + assert connection_name == "default-project.us.connection-id" + + +def test_get_connection_name_full_location_connection_id(): + connection_name = clients.get_connection_name_full( + "eu.connection-id", default_project="default-project", default_location="us" + ) + assert connection_name == "default-project.eu.connection-id" + + +def test_get_connection_name_full_all(): + connection_name = clients.get_connection_name_full( + "my-project.eu.connection-id", + default_project="default-project", + default_location="us", + ) + assert connection_name == "my-project.eu.connection-id" + + +def test_get_connection_name_full_raise_value_error(): + + with pytest.raises(ValueError): + clients.get_connection_name_full( + "my-project.eu.connection-id.extra_field", + default_project="default-project", + default_location="us", + ) From d39134db81249e19b266a465ac759bef1dfe229d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 10:58:57 -0700 Subject: [PATCH 070/921] chore(main): release 0.7.0 (#86) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 22 ++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 880f7916250..8d405d06ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.7.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.6.0...v0.7.0) (2023-10-11) + + +### Features + +* Add aliases for several series properties ([#80](https://github.com/googleapis/python-bigquery-dataframes/issues/80)) ([c0efec8](https://github.com/googleapis/python-bigquery-dataframes/commit/c0efec8956198247b27904345a795f09c80d3502)) +* Add equals methods to series/dataframe ([#76](https://github.com/googleapis/python-bigquery-dataframes/issues/76)) ([636a209](https://github.com/googleapis/python-bigquery-dataframes/commit/636a209e0853501abd50784a11a87cf7f2282ee5)) +* Add iat and iloc accessing by tuples of integers ([#90](https://github.com/googleapis/python-bigquery-dataframes/issues/90)) ([228aeba](https://github.com/googleapis/python-bigquery-dataframes/commit/228aeba09782ae2421040c7601c15d4af92790b6)) +* Add level param to DataFrame.stack ([#88](https://github.com/googleapis/python-bigquery-dataframes/issues/88)) ([97b8bec](https://github.com/googleapis/python-bigquery-dataframes/commit/97b8bec1175499c74448a4fd46b4888c4b4c35c1)) +* Allow df.drop to take an index object ([#68](https://github.com/googleapis/python-bigquery-dataframes/issues/68)) ([740c451](https://github.com/googleapis/python-bigquery-dataframes/commit/740c45176f79d4d2f7f28cb5f6c9eeb1327c8397)) +* Use default session connection ([#87](https://github.com/googleapis/python-bigquery-dataframes/issues/87)) ([4ae4ef9](https://github.com/googleapis/python-bigquery-dataframes/commit/4ae4ef995348b95521c4988a8cfb3b5ac792fd69)) + + +### Bug Fixes + +* Change the invalid url in docs ([#93](https://github.com/googleapis/python-bigquery-dataframes/issues/93)) ([969800d](https://github.com/googleapis/python-bigquery-dataframes/commit/969800d669204de4d0f2e5e61da521217e55668b)) + + +### Documentation + +* Add more preprocessing models into the docs menu. ([#97](https://github.com/googleapis/python-bigquery-dataframes/issues/97)) ([1592315](https://github.com/googleapis/python-bigquery-dataframes/commit/159231505f339173560cd802dae3fed3e63a663b)) + ## [0.6.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.5.0...v0.6.0) (2023-10-04) diff --git a/bigframes/version.py b/bigframes/version.py index 238b64473a0..e91e27ff76d 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.0" +__version__ = "0.7.0" From 1d385be1c68342a66ecb9f28c5efc83c18d0e64c Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 11 Oct 2023 20:04:14 +0000 Subject: [PATCH 071/921] fix: create session dataset for remote functions only when needed (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this change BigFrames will not create a dataset upfront at the time of session creation, but instead leave it to the components which need the dataset to create it. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/remote_function.py | 39 ++++++++++++++------ bigframes/session.py | 7 +--- tests/system/conftest.py | 23 +++++++++--- tests/system/large/test_remote_function.py | 43 ++++++++++++++++++++++ tests/system/small/test_session.py | 5 --- 5 files changed, 90 insertions(+), 27 deletions(-) diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 37c7a2fc64b..81ba26600ba 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -202,10 +202,22 @@ def create_bq_remote_function( OPTIONS ( endpoint = "{endpoint}" )""" + logger.info(f"Creating BQ remote function: {create_function_ddl}") + + # Make sure the dataset exists + dataset = bigquery.Dataset( + bigquery.DatasetReference.from_string( + self._bq_dataset, default_project=self._gcp_project_id + ) + ) + dataset.location = self._bq_location + self._bq_client.create_dataset(dataset, exists_ok=True) + # TODO: Use session._start_query() so we get progress bar query_job = self._bq_client.query(create_function_ddl) # Make an API request. query_job.result() # Wait for the job to complete. + logger.info(f"Created remote function {query_job.ddl_target_routine}") def get_cloud_function_fully_qualified_parent(self): @@ -465,17 +477,22 @@ def get_remote_function_specs(self, remote_function_name): routines = self._bq_client.list_routines( f"{self._gcp_project_id}.{self._bq_dataset}" ) - for routine in routines: - if routine.reference.routine_id == remote_function_name: - # TODO(shobs): Use first class properties when they are available - # https://github.com/googleapis/python-bigquery/issues/1552 - rf_options = routine._properties.get("remoteFunctionOptions") - if rf_options: - http_endpoint = rf_options.get("endpoint") - bq_connection = rf_options.get("connection") - if bq_connection: - bq_connection = os.path.basename(bq_connection) - break + try: + for routine in routines: + if routine.reference.routine_id == remote_function_name: + # TODO(shobs): Use first class properties when they are available + # https://github.com/googleapis/python-bigquery/issues/1552 + rf_options = routine._properties.get("remoteFunctionOptions") + if rf_options: + http_endpoint = rf_options.get("endpoint") + bq_connection = rf_options.get("connection") + if bq_connection: + bq_connection = os.path.basename(bq_connection) + break + except google.api_core.exceptions.NotFound: + # The dataset might not exist, in which case the http_endpoint doesn't, either. + # Note: list_routines doesn't make an API request until we iterate on the response object. + pass return (http_endpoint, bq_connection) diff --git a/bigframes/session.py b/bigframes/session.py index a7cb78e3ffb..6ad65000ce3 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -381,17 +381,12 @@ def _create_and_bind_bq_session(self): ] ) - # Dataset for storing BQML models and remote functions, which don't yet + # Dataset for storing remote functions, which don't yet # support proper session temporary storage yet self._session_dataset = bigquery.Dataset( f"{self.bqclient.project}.bigframes_temp_{self._location.lower().replace('-', '_')}" ) self._session_dataset.location = self._location - self._session_dataset.default_table_expiration_ms = 24 * 60 * 60 * 1000 - - # TODO: handle case when the dataset does not exist and the user does - # not have permission to create one (bigquery.datasets.create IAM) - self.bqclient.create_dataset(self._session_dataset, exists_ok=True) def close(self): """Terminated the BQ session, otherwises the session will be terminated automatically after diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 3153bd1559e..ed22a3e8dac 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -134,15 +134,28 @@ def cleanup_datasets(bigquery_client: bigquery.Client) -> None: ) +def get_dataset_id(project_id: str): + "Get a fully qualified dataset id belonging to the given project." + dataset_id = f"{project_id}.{prefixer.create_prefix()}_dataset_id" + return dataset_id + + @pytest.fixture(scope="session") def dataset_id(bigquery_client: bigquery.Client): """Create (and cleanup) a temporary dataset.""" - project_id = bigquery_client.project - dataset_id = f"{project_id}.{prefixer.create_prefix()}_dataset_id" - dataset = bigquery.Dataset(dataset_id) - bigquery_client.create_dataset(dataset) + dataset_id = get_dataset_id(bigquery_client.project) + bigquery_client.create_dataset(dataset_id) + yield dataset_id + bigquery_client.delete_dataset(dataset_id, delete_contents=True) + + +@pytest.fixture +def dataset_id_not_created(bigquery_client: bigquery.Client): + """Return a temporary dataset object without creating it, and clean it up + after it has been used.""" + dataset_id = get_dataset_id(bigquery_client.project) yield dataset_id - bigquery_client.delete_dataset(dataset, delete_contents=True) + bigquery_client.delete_dataset(dataset_id, delete_contents=True) @pytest.fixture(scope="session") diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 2f231f40c95..f2700991828 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -408,6 +408,49 @@ def add_one(x): ) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_explicit_dataset_not_created( + session, scalars_dfs, dataset_id_not_created, bq_cf_connection, functions_client +): + try: + + @session.remote_function( + [int], + int, + dataset_id_not_created, + bq_cf_connection, + reuse=False, + ) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets(session.bqclient, functions_client, square) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_udf_referring_outside_var( session, scalars_dfs, dataset_id, bq_cf_connection, functions_client diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 53ddfa3c495..7655325bfcc 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -894,11 +894,6 @@ def test_session_id(session): # TODO(chelsealin): Verify the session id can be binded with a load job. -def test_session_dataset_exists_and_configured(session: bigframes.Session): - dataset = session.bqclient.get_dataset(session._session_dataset_id) - assert dataset.default_table_expiration_ms == 24 * 60 * 60 * 1000 - - @pytest.mark.flaky(retries=2) def test_to_close_session(): session = bigframes.Session() From a8c286f0995cc8cf2a4c44fb51855773ecf71f72 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 12 Oct 2023 07:13:04 +0000 Subject: [PATCH 072/921] feat: Support compression in `to_parquet` (#91) * feat: Support compression in to_parquet This changes the default behavior from no compression to snappy compression. * feat: Support compression in to_parquet BREAKING CHANGE: The default behavior of to_parquet is changing from no compression to 'snappy' compression. * fix exception message, add tests for not supported compression techniques --- bigframes/dataframe.py | 18 +++- tests/system/small/test_session.py | 85 ++++++++++++++++++- .../bigframes_vendored/pandas/core/frame.py | 5 ++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 3ec1b4b6175..015a7642f8b 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -21,6 +21,7 @@ import typing from typing import ( Callable, + Dict, Iterable, List, Literal, @@ -2270,7 +2271,13 @@ def to_numpy( __array__ = to_numpy - def to_parquet(self, path: str, *, index: bool = True) -> None: + def to_parquet( + self, + path: str, + *, + compression: Optional[Literal["snappy", "gzip"]] = "snappy", + index: bool = True, + ) -> None: # TODO(swast): Can we support partition columns argument? # TODO(chelsealin): Support local file paths. # TODO(swast): Some warning that wildcard is recommended for large @@ -2282,6 +2289,13 @@ def to_parquet(self, path: str, *, index: bool = True) -> None: if "*" not in path: raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) + if compression not in {None, "snappy", "gzip"}: + raise ValueError("'{0}' is not valid for compression".format(compression)) + + export_options: Dict[str, Union[bool, str]] = {} + if compression: + export_options["compression"] = compression.upper() + result_table = self._run_io_query( index=index, ordering_id=bigframes.core.io.IO_ORDERING_ID ) @@ -2289,7 +2303,7 @@ def to_parquet(self, path: str, *, index: bool = True) -> None: f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", uri=path, format="PARQUET", - export_options={}, + export_options=export_options, ) _, query_job = self._block.expr._session._start_query(export_data_statement) self._set_internal_query_job(query_job) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 7655325bfcc..bfe9bc8d0f9 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -793,7 +793,7 @@ def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs # Include wildcard so that multiple files can be written/read if > 1 GB. # https://cloud.google.com/bigquery/docs/exporting-data#exporting_data_into_one_or_more_files - path = gcs_folder + "test_read_parquet_gcs*.parquet" + path = gcs_folder + test_read_parquet_gcs.__name__ + "*.parquet" df_in: bigframes.dataframe.DataFrame = scalars_df.copy() # GEOGRAPHY not supported in parquet export. df_in = df_in.drop(columns="geography_col") @@ -823,6 +823,89 @@ def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): pd.testing.assert_frame_equal(pd_df_in, pd_df_out) +@pytest.mark.parametrize( + "compression", + [ + None, + "gzip", + "snappy", + ], +) +def test_read_parquet_gcs_compressed( + session: bigframes.Session, scalars_dfs, gcs_folder, compression +): + scalars_df, _ = scalars_dfs + # Include wildcard so that multiple files can be written/read if > 1 GB. + # https://cloud.google.com/bigquery/docs/exporting-data#exporting_data_into_one_or_more_files + path = ( + gcs_folder + + test_read_parquet_gcs_compressed.__name__ + + (f"_{compression}" if compression else "") + + "*.parquet" + ) + df_in: bigframes.dataframe.DataFrame = scalars_df.copy() + # GEOGRAPHY not supported in parquet export. + df_in = df_in.drop(columns="geography_col") + # Make sure we can also serialize the order. + df_write = df_in.reset_index(drop=False) + df_write.index.name = f"ordering_id_{random.randrange(1_000_000)}" + df_write.to_parquet(path, compression=compression, index=True) + + df_out = ( + session.read_parquet(path) + # Restore order. + .set_index(df_write.index.name).sort_index() + # Restore index. + .set_index(typing.cast(str, df_in.index.name)) + ) + + # DATETIME gets loaded as TIMESTAMP in parquet. See: + # https://cloud.google.com/bigquery/docs/exporting-data#parquet_export_details + df_out = df_out.assign( + datetime_col=df_out["datetime_col"].astype("timestamp[us][pyarrow]") + ) + + # Make sure we actually have at least some values before comparing. + assert df_out.size != 0 + pd_df_in = df_in.to_pandas() + pd_df_out = df_out.to_pandas() + pd.testing.assert_frame_equal(pd_df_in, pd_df_out) + + +@pytest.mark.parametrize( + "compression", + [ + "brotli", + "lz4", + "zstd", + "unknown", + ], +) +def test_read_parquet_gcs_compression_not_supported( + session: bigframes.Session, scalars_dfs, gcs_folder, compression +): + scalars_df, _ = scalars_dfs + # Include wildcard so that multiple files can be written/read if > 1 GB. + # https://cloud.google.com/bigquery/docs/exporting-data#exporting_data_into_one_or_more_files + path = ( + gcs_folder + + test_read_parquet_gcs_compression_not_supported.__name__ + + (f"_{compression}" if compression else "") + + "*.parquet" + ) + df_in: bigframes.dataframe.DataFrame = scalars_df.copy() + # GEOGRAPHY not supported in parquet export. + df_in = df_in.drop(columns="geography_col") + # Make sure we can also serialize the order. + df_write = df_in.reset_index(drop=False) + df_write.index.name = f"ordering_id_{random.randrange(1_000_000)}" + + with pytest.raises( + ValueError, match=f"'{compression}' is not valid for compression" + ): + df_write.to_parquet(path, compression=compression, index=True) + + def test_read_json_gcs_bq_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs path = gcs_folder + "test_read_json_gcs_bq_engine_w_index*.json" diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 80a5428b36d..e54f984d59b 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -129,6 +129,7 @@ def to_parquet( self, path: str, *, + compression: Optional[Literal["snappy", "gzip"]] = "snappy", index: bool = True, ) -> None: """Write a DataFrame to the binary Parquet format. @@ -143,6 +144,10 @@ def to_parquet( If the data size is more than 1GB, you must use a wildcard to export the data into multiple files and the size of the files varies. + compression (str, default 'snappy'): + Name of the compression to use. Use ``None`` for no compression. + Supported options: ``'gzip'``, ``'snappy'``. + index (bool, default True): If ``True``, include the dataframe's index(es) in the file output. If ``False``, they will not be written to the file. From 1ff2755bfda121977ebde153450dc0c5a0042281 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:40:13 +0000 Subject: [PATCH 073/921] chore(main): release 0.8.0 (#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [0.8.0](https://togithub.com/googleapis/python-bigquery-dataframes/compare/v0.7.0...v0.8.0) (2023-10-12) ### ⚠ BREAKING CHANGES * The default behavior of `to_parquet` is changing from no compression to `'snappy'` compression. ### Features * Support compression in `to_parquet` ([a8c286f](https://togithub.com/googleapis/python-bigquery-dataframes/commit/a8c286f0995cc8cf2a4c44fb51855773ecf71f72)) ### Bug Fixes * Create session dataset for remote functions only when needed ([#94](https://togithub.com/googleapis/python-bigquery-dataframes/issues/94)) ([1d385be](https://togithub.com/googleapis/python-bigquery-dataframes/commit/1d385be1c68342a66ecb9f28c5efc83c18d0e64c)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 16 ++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d405d06ee7..e34b073999a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.8.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.7.0...v0.8.0) (2023-10-12) + + +### ⚠ BREAKING CHANGES + +* The default behavior of `to_parquet` is changing from no compression to `'snappy'` compression. + +### Features + +* Support compression in `to_parquet` ([a8c286f](https://github.com/googleapis/python-bigquery-dataframes/commit/a8c286f0995cc8cf2a4c44fb51855773ecf71f72)) + + +### Bug Fixes + +* Create session dataset for remote functions only when needed ([#94](https://github.com/googleapis/python-bigquery-dataframes/issues/94)) ([1d385be](https://github.com/googleapis/python-bigquery-dataframes/commit/1d385be1c68342a66ecb9f28c5efc83c18d0e64c)) + ## [0.7.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.6.0...v0.7.0) (2023-10-11) diff --git a/bigframes/version.py b/bigframes/version.py index e91e27ff76d..974fbf1ac9b 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.0" +__version__ = "0.8.0" From 3b51a363615e292cb5442eee18ea76a0a3999d2e Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:56:15 -0700 Subject: [PATCH 074/921] chore: address last comments of PR#87 (#102) PR#87 https://togithub.com/googleapis/python-bigquery-dataframes/pull/87 --- bigframes/clients.py | 41 +++++++++++++++--------------------- bigframes/ml/llm.py | 16 +++++++------- bigframes/remote_function.py | 2 +- bigframes/session.py | 4 +++- tests/unit/test_clients.py | 16 ++++---------- 5 files changed, 33 insertions(+), 46 deletions(-) diff --git a/bigframes/clients.py b/bigframes/clients.py index dcac611e8cf..4ba9d93d694 100644 --- a/bigframes/clients.py +++ b/bigframes/clients.py @@ -29,8 +29,6 @@ ) logger = logging.getLogger(__name__) -_BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" - class BqConnectionManager: """Manager to handle operations with BQ connections.""" @@ -46,6 +44,23 @@ def __init__( self._bq_connection_client = bq_connection_client self._cloud_resource_manager_client = cloud_resource_manager_client + @classmethod + def resolve_full_connection_name( + cls, connection_name: str, default_project: str, default_location: str + ) -> str: + """Retrieve the full connection name of the form ... + Use default project, location or connection_id when any of them are missing.""" + if connection_name.count(".") == 2: + return connection_name + + if connection_name.count(".") == 1: + return f"{default_project}.{connection_name}" + + if connection_name.count(".") == 0: + return f"{default_project}.{default_location}.{connection_name}" + + raise ValueError(f"Invalid connection name format: {connection_name}.") + def create_bq_connection( self, project_id: str, location: str, connection_id: str, iam_role: str ): @@ -164,25 +179,3 @@ def _get_service_account_if_connection_exists( pass return service_account - - -def get_connection_name_full( - connection_name: Optional[str], default_project: str, default_location: str -) -> str: - """Retrieve the full connection name of the form ... - Use default project, location or connection_id when any of them are missing.""" - if connection_name is None: - return ( - f"{default_project}.{default_location}.{_BIGFRAMES_DEFAULT_CONNECTION_ID}" - ) - - if connection_name.count(".") == 2: - return connection_name - - if connection_name.count(".") == 1: - return f"{default_project}.{connection_name}" - - if connection_name.count(".") == 0: - return f"{default_project}.{default_location}.{connection_name}" - - raise ValueError(f"Invalid connection name format: {connection_name}.") diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index a61dd34e6de..d78f467537c 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -49,17 +49,17 @@ def __init__( connection_name: Optional[str] = None, ): self.session = session or bpd.get_global_session() + self._bq_connection_manager = clients.BqConnectionManager( + self.session.bqconnectionclient, self.session.resourcemanagerclient + ) connection_name = connection_name or self.session._bq_connection - self.connection_name = clients.get_connection_name_full( + self.connection_name = self._bq_connection_manager.resolve_full_connection_name( connection_name, default_project=self.session._project, default_location=self.session._location, ) - self._bq_connection_manager = clients.BqConnectionManager( - self.session.bqconnectionclient, self.session.resourcemanagerclient - ) self._bqml_model_factory = globals.bqml_model_factory() self._bqml_model: core.BqmlModel = self._create_bqml_model() @@ -188,17 +188,17 @@ def __init__( connection_name: Optional[str] = None, ): self.session = session or bpd.get_global_session() + self._bq_connection_manager = clients.BqConnectionManager( + self.session.bqconnectionclient, self.session.resourcemanagerclient + ) connection_name = connection_name or self.session._bq_connection - self.connection_name = clients.get_connection_name_full( + self.connection_name = self._bq_connection_manager.resolve_full_connection_name( connection_name, default_project=self.session._project, default_location=self.session._location, ) - self._bq_connection_manager = clients.BqConnectionManager( - self.session.bqconnectionclient, self.session.resourcemanagerclient - ) self._bqml_model_factory = globals.bqml_model_factory() self._bqml_model: core.BqmlModel = self._create_bqml_model() diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 81ba26600ba..fd9aec825fa 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -772,7 +772,7 @@ def remote_function( if not bigquery_connection: bigquery_connection = session._bq_connection # type: ignore - bigquery_connection = clients.get_connection_name_full( + bigquery_connection = clients.BqConnectionManager.resolve_full_connection_name( bigquery_connection, default_project=dataset_ref.project, default_location=bq_location, diff --git a/bigframes/session.py b/bigframes/session.py index 6ad65000ce3..4f509f0704a 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -97,6 +97,8 @@ _BIGQUERYCONNECTION_REGIONAL_ENDPOINT = "{location}-bigqueryconnection.googleapis.com" _BIGQUERYSTORAGE_REGIONAL_ENDPOINT = "{location}-bigquerystorage.googleapis.com" +_BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" + _MAX_CLUSTER_COLUMNS = 4 # TODO(swast): Need to connect to regional endpoints when performing remote @@ -321,7 +323,7 @@ def __init__( ), ) - self._bq_connection = context.bq_connection + self._bq_connection = context.bq_connection or _BIGFRAMES_DEFAULT_CONNECTION_ID # Now that we're starting the session, don't allow the options to be # changed. diff --git a/tests/unit/test_clients.py b/tests/unit/test_clients.py index a90e5b0320b..f89cc21397c 100644 --- a/tests/unit/test_clients.py +++ b/tests/unit/test_clients.py @@ -17,29 +17,22 @@ from bigframes import clients -def test_get_connection_name_full_none(): - connection_name = clients.get_connection_name_full( - None, default_project="default-project", default_location="us" - ) - assert connection_name == "default-project.us.bigframes-default-connection" - - def test_get_connection_name_full_connection_id(): - connection_name = clients.get_connection_name_full( + connection_name = clients.BqConnectionManager.resolve_full_connection_name( "connection-id", default_project="default-project", default_location="us" ) assert connection_name == "default-project.us.connection-id" def test_get_connection_name_full_location_connection_id(): - connection_name = clients.get_connection_name_full( + connection_name = clients.BqConnectionManager.resolve_full_connection_name( "eu.connection-id", default_project="default-project", default_location="us" ) assert connection_name == "default-project.eu.connection-id" def test_get_connection_name_full_all(): - connection_name = clients.get_connection_name_full( + connection_name = clients.BqConnectionManager.resolve_full_connection_name( "my-project.eu.connection-id", default_project="default-project", default_location="us", @@ -48,9 +41,8 @@ def test_get_connection_name_full_all(): def test_get_connection_name_full_raise_value_error(): - with pytest.raises(ValueError): - clients.get_connection_name_full( + clients.BqConnectionManager.resolve_full_connection_name( "my-project.eu.connection-id.extra_field", default_project="default-project", default_location="us", From e325fbb1c91e040d87df10f7d4d5ce53f7c052cb Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 12 Oct 2023 14:15:29 -0700 Subject: [PATCH 075/921] feat: send BigQuery cancel request when canceling bigframes process (#103) Co-authored-by: Henry J Solberg --- bigframes/formatting_helpers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index 82e2510e2af..f3e3a76ce42 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -153,6 +153,11 @@ def wait_for_query_job( except api_core_exceptions.GoogleAPICallError as exc: add_feedback_link(exc) raise + except KeyboardInterrupt: + query_job.cancel() + print(f"Requested cancelation for {query_job.job_type} {query_job.job_id}...") + # begin the cancel request before immediately rethrowing + raise def wait_for_job(job: GenericJob, progress_bar: Optional[str] = None): @@ -190,6 +195,11 @@ def wait_for_job(job: GenericJob, progress_bar: Optional[str] = None): except api_core_exceptions.GoogleAPICallError as exc: add_feedback_link(exc) raise + except KeyboardInterrupt: + job.cancel() + print(f"Requested cancelation for {job.job_type} {job.job_id}...") + # begin the cancel request before immediately rethrowing + raise def get_job_url(query_job: GenericJob): From 36693bff398c23e179d9bde95d52cbaddaf85c45 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:02:15 -0700 Subject: [PATCH 076/921] feat!: rename `bigframes.pandas.reset_session` to `close_session` (#101) --- README.rst | 4 ++-- bigframes/__init__.py | 4 ++-- bigframes/_config/bigquery_options.py | 2 +- bigframes/core/global_session.py | 2 +- bigframes/pandas/__init__.py | 4 ++-- .../bq_dataframes_llm_code_generation.ipynb | 2 +- .../getting_started_bq_dataframes.ipynb | 2 +- .../bq_dataframes_ml_linear_regression.ipynb | 2 +- samples/snippets/quickstart_test.py | 2 +- samples/snippets/remote_function_test.py | 2 +- tests/system/small/ml/test_llm.py | 6 +++--- tests/system/small/test_pandas_options.py | 12 ++++++------ tests/unit/test_pandas.py | 6 +++--- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index 77c42e43251..b2ad1ee0179 100644 --- a/README.rst +++ b/README.rst @@ -63,7 +63,7 @@ auto-populates ``bf.options.bigquery.location`` if the user starts with directly or in a SQL statement. If you want to reset the location of the created DataFrame or Series objects, -you can reset the session by executing ``bigframes.pandas.reset_session()``. +you can close the session by executing ``bigframes.pandas.close_session()``. After that, you can reuse ``bigframes.pandas.options.bigquery.location`` to specify another location. @@ -335,7 +335,7 @@ sessions ; when this happens, you can’t use previously created DataFrame or Series objects and must re-create them using a new BigQuery DataFrames session. You can do this by running -``bigframes.pandas.reset_session()`` and then re-running the BigQuery +``bigframes.pandas.close_session()`` and then re-running the BigQuery DataFrames expressions. diff --git a/bigframes/__init__.py b/bigframes/__init__.py index 3e54a6d0903..8f41790072b 100644 --- a/bigframes/__init__.py +++ b/bigframes/__init__.py @@ -16,7 +16,7 @@ from bigframes._config import options from bigframes._config.bigquery_options import BigQueryOptions -from bigframes.core.global_session import get_global_session, reset_session +from bigframes.core.global_session import close_session, get_global_session from bigframes.session import connect, Session from bigframes.version import __version__ @@ -24,7 +24,7 @@ "options", "BigQueryOptions", "get_global_session", - "reset_session", + "close_session", "connect", "Session", "__version__", diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index eb56de826a8..453e7f5dff4 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -23,7 +23,7 @@ SESSION_STARTED_MESSAGE = ( "Cannot change '{attribute}' once a session has started. " - "Call bigframes.pandas.reset_session() first, if you are using the bigframes.pandas API." + "Call bigframes.pandas.close_session() first, if you are using the bigframes.pandas API." ) diff --git a/bigframes/core/global_session.py b/bigframes/core/global_session.py index 68529981cda..1f960839a0a 100644 --- a/bigframes/core/global_session.py +++ b/bigframes/core/global_session.py @@ -24,7 +24,7 @@ _global_session_lock = threading.Lock() -def reset_session() -> None: +def close_session() -> None: """Start a fresh session the next time a function requires a session. Closes the current session if it was already started. diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index cb278345907..ded4760fa3b 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -447,7 +447,7 @@ def read_gbq_function(function_name: str): # Session management APIs get_global_session = global_session.get_global_session -reset_session = global_session.reset_session +close_session = global_session.close_session # Use __all__ to let type checkers know what is part of the public API. @@ -478,5 +478,5 @@ def read_gbq_function(function_name: str): "options", # Session management APIs "get_global_session", - "reset_session", + "close_session", ] diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index 2e4ce3e5109..0f113b84c6d 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -354,7 +354,7 @@ "id": "DTVtFlqeFbrU" }, "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." ] }, { diff --git a/notebooks/getting_started/getting_started_bq_dataframes.ipynb b/notebooks/getting_started/getting_started_bq_dataframes.ipynb index 6936e1cf59b..6cc6acc9935 100644 --- a/notebooks/getting_started/getting_started_bq_dataframes.ipynb +++ b/notebooks/getting_started/getting_started_bq_dataframes.ipynb @@ -383,7 +383,7 @@ "id": "pDfrKwMKE_dK" }, "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." ] }, { diff --git a/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb b/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb index 338d6edf4fb..675416f6ea8 100644 --- a/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb +++ b/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb @@ -370,7 +370,7 @@ "id": "D21CoOlfFTYI" }, "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." ] }, { diff --git a/samples/snippets/quickstart_test.py b/samples/snippets/quickstart_test.py index 6b0c69de999..bbe4a8b3c49 100644 --- a/samples/snippets/quickstart_test.py +++ b/samples/snippets/quickstart_test.py @@ -23,7 +23,7 @@ def test_quickstart( capsys: pytest.CaptureFixture[str], ) -> None: # We need a fresh session since we're modifying connection options. - bigframes.pandas.reset_session() + bigframes.pandas.close_session() # TODO(swast): Get project from environment so contributors can run tests. quickstart.run_quickstart("bigframes-dev") diff --git a/samples/snippets/remote_function_test.py b/samples/snippets/remote_function_test.py index 8b51e46b45a..e1317c6ac08 100644 --- a/samples/snippets/remote_function_test.py +++ b/samples/snippets/remote_function_test.py @@ -23,7 +23,7 @@ def test_remote_function_and_read_gbq_function( capsys: pytest.CaptureFixture[str], ) -> None: # We need a fresh session since we're modifying connection options. - bigframes.pandas.reset_session() + bigframes.pandas.close_session() # TODO(swast): Get project from environment so contributors can run tests. remote_function.run_remote_function_and_read_gbq_function("bigframes-dev") diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index e546c09f97a..b7257dde1be 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -30,7 +30,7 @@ def test_create_text_generator_model(palm2_text_generator_model): def test_create_text_generator_model_default_session(bq_connection, llm_text_pandas_df): import bigframes.pandas as bpd - bpd.reset_session() + bpd.close_session() bpd.options.bigquery.bq_connection = bq_connection bpd.options.bigquery.location = "us" @@ -53,7 +53,7 @@ def test_create_text_generator_model_default_connection(llm_text_pandas_df): from bigframes import _config import bigframes.pandas as bpd - bpd.reset_session() + bpd.close_session() _config.options = _config.Options() # reset configs llm_text_df = bpd.read_pandas(llm_text_pandas_df) @@ -130,7 +130,7 @@ def test_create_embedding_generator_model(palm2_embedding_generator_model): def test_create_text_embedding_generator_model_defaults(bq_connection): import bigframes.pandas as bpd - bpd.reset_session() + bpd.close_session() bpd.options.bigquery.bq_connection = bq_connection bpd.options.bigquery.location = "us" diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index 956b29ae12f..ca67710d4ea 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -26,7 +26,7 @@ @pytest.fixture(autouse=True) def reset_default_session_and_location(): - bpd.reset_session() + bpd.close_session() bpd.options.bigquery.location = None @@ -79,8 +79,8 @@ def test_read_gbq_start_sets_session_location( ): read_method(query) - # Reset global session to start over - bpd.reset_session() + # Close global session to start over + bpd.close_session() # There should still be the previous location set in the bigquery options assert bpd.options.bigquery.location == tokyo_location @@ -254,7 +254,7 @@ def test_read_gbq_must_comply_with_set_location_non_US( assert df is not None -def test_reset_session_after_credentials_need_reauthentication(monkeypatch): +def test_close_session_after_credentials_need_reauthentication(monkeypatch): # Use a simple test query to verify that default session works to interact # with BQ test_query = "SELECT 1" @@ -288,8 +288,8 @@ def test_reset_session_after_credentials_need_reauthentication(monkeypatch): with pytest.raises(google.auth.exceptions.RefreshError): bpd.read_gbq(test_query) - # Now verify that resetting the session works - bpd.reset_session() + # Now verify that closing the session works + bpd.close_session() assert bigframes.core.global_session._global_session is None # Now verify that use is able to start over diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 2325fc96a0a..5d4f69c7c01 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -116,7 +116,7 @@ def test_pandas_attribute(): assert bpd.ArrowDtype is pd.ArrowDtype -def test_reset_session_after_bq_session_ended(monkeypatch): +def test_close_session_after_bq_session_ended(monkeypatch): bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" session = resources.create_bigquery_session( @@ -144,7 +144,7 @@ def test_reset_session_after_bq_session_ended(monkeypatch): bpd.read_gbq("SELECT 1") # Even though the query to stop the session raises an exception, we should - # still be able to reset it without raising an error to the user. - bpd.reset_session() + # still be able to close it without raising an error to the user. + bpd.close_session() assert "CALL BQ.ABORT_SESSION('JUST_A_TEST')" in bqclient.query.call_args.args[0] assert bigframes.core.global_session._global_session is None From 855616a1472f1f3daa69b2dc3f84d4398537ca88 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 12 Oct 2023 16:04:40 -0700 Subject: [PATCH 077/921] refactor: all ArrayValue ops return only ArrayValue (#92) * refactor: all ArrayValue ops return only ArrayValue * copyright notice --------- Co-authored-by: Tim Swast --- bigframes/core/__init__.py | 15 +- bigframes/core/block_transforms.py | 8 +- bigframes/core/blocks.py | 125 +++++++++------- bigframes/core/indexes/index.py | 101 +++++++++---- bigframes/core/joins/name_resolution.py | 46 ++++++ bigframes/core/joins/row_identity.py | 44 +++--- bigframes/core/joins/single_column.py | 182 +++++------------------- bigframes/dataframe.py | 31 ++-- bigframes/operations/__init__.py | 10 ++ bigframes/operations/base.py | 4 +- bigframes/series.py | 6 +- 11 files changed, 290 insertions(+), 282 deletions(-) create mode 100644 bigframes/core/joins/name_resolution.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index ccfd682215c..6c78a07f3ba 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -211,8 +211,8 @@ def column_ids(self) -> typing.Sequence[str]: return tuple(self._column_names.keys()) @property - def hidden_ordering_columns(self) -> typing.Tuple[ibis_types.Value, ...]: - return self._hidden_ordering_columns + def _hidden_column_ids(self) -> typing.Sequence[str]: + return tuple(self._hidden_ordering_column_names.keys()) @property def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: @@ -400,7 +400,7 @@ def _hide_column(self, column_id) -> ArrayValue: expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) return expr_builder.build() - def promote_offsets(self) -> typing.Tuple[ArrayValue, str]: + def promote_offsets(self, col_id: str) -> ArrayValue: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. """ @@ -408,16 +408,15 @@ def promote_offsets(self) -> typing.Tuple[ArrayValue, str]: ordering = self._ordering if (not ordering.is_sequential) or (not ordering.total_order_col): - return self._project_offsets().promote_offsets() - col_id = bigframes.core.guid.generate_guid() + return self._project_offsets().promote_offsets(col_id) expr_builder = self.builder() expr_builder.columns = [ self._get_any_column(ordering.total_order_col.column_id).name(col_id), *self.columns, ] - return expr_builder.build(), col_id + return expr_builder.build() - def select_columns(self, column_ids: typing.Sequence[str]): + def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: return self._projection( [self._get_ibis_column(col_id) for col_id in column_ids] ) @@ -807,7 +806,7 @@ def _create_order_columns( elif ordering_mode == "string_encoded": return (self._create_string_ordering_column().name(order_col_name),) elif expose_hidden_cols: - return self.hidden_ordering_columns + return self._hidden_ordering_columns return () def _create_offset_column(self) -> ibis_types.IntegerColumn: diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 904da7f3122..b0f05f47984 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -40,8 +40,8 @@ def equals(block1: blocks.Block, block2: blocks.Block) -> bool: equality_ids = [] for lcol, rcol in zip(block1.value_columns, block2.value_columns): - lcolmapped = lmap(lcol) - rcolmapped = rmap(rcol) + lcolmapped = lmap[lcol] + rcolmapped = rmap[rcol] joined_block, result_id = joined_block.apply_binary_op( lcolmapped, rcolmapped, ops.eq_nulls_match_op ) @@ -563,8 +563,8 @@ def align_rows( joined_index, (get_column_left, get_column_right) = left_block.index.join( right_block.index, how=join ) - left_columns = [get_column_left(col) for col in left_block.value_columns] - right_columns = [get_column_right(col) for col in right_block.value_columns] + left_columns = [get_column_left[col] for col in left_block.value_columns] + right_columns = [get_column_right[col] for col in right_block.value_columns] left_block = joined_index._block.select_columns(left_columns) right_block = joined_index._block.select_columns(right_columns) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 9b49645c71d..4548fca5934 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -39,6 +39,7 @@ import bigframes.core.guid as guid import bigframes.core.indexes as indexes import bigframes.core.joins as joins +import bigframes.core.joins.name_resolution as join_names import bigframes.core.ordering as ordering import bigframes.core.utils import bigframes.core.utils as utils @@ -97,7 +98,8 @@ def __init__( "'index_columns' and 'index_labels' must have equal length" ) if len(index_columns) == 0: - expr, new_index_col_id = expr.promote_offsets() + new_index_col_id = guid.generate_guid() + expr = expr.promote_offsets(new_index_col_id) index_columns = [new_index_col_id] self._index_columns = tuple(index_columns) # Index labels don't need complicated hierarchical access so can store as tuple @@ -260,7 +262,8 @@ def reset_index(self, drop: bool = True) -> Block: from Index classes that point to this block. """ block = self - expr, new_index_col_id = self._expr.promote_offsets() + new_index_col_id = guid.generate_guid() + expr = self._expr.promote_offsets(new_index_col_id) if drop: # Even though the index might be part of the ordering, keep that # ordering expression as reset_index shouldn't change the row @@ -833,7 +836,8 @@ def aggregate_all_and_stack( else: # axis_n == 1 # using offsets as identity to group on. # TODO: Allow to promote identity/total_order columns instead for better perf - expr_with_offsets, offset_col = self.expr.promote_offsets() + offset_col = guid.generate_guid() + expr_with_offsets = self.expr.promote_offsets(offset_col) stacked_expr = expr_with_offsets.unpivot( row_labels=self.column_labels.to_list(), index_col_ids=[guid.generate_guid()], @@ -952,9 +956,10 @@ def aggregate( ] by_column_labels = self._get_labels_for_columns(by_value_columns) labels = (*by_column_labels, *aggregate_labels) - result_expr_pruned, offsets_id = result_expr.select_columns( + offsets_id = guid.generate_guid() + result_expr_pruned = result_expr.select_columns( [*by_value_columns, *output_col_ids] - ).promote_offsets() + ).promote_offsets(offsets_id) return ( Block( @@ -975,7 +980,8 @@ def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): aggregations = [(column_id, stat, stat.name) for stat in stats_to_fetch] expr = self.expr.aggregate(aggregations) - expr, offset_index_id = expr.promote_offsets() + offset_index_id = guid.generate_guid() + expr = expr.promote_offsets(offset_index_id) block = Block( expr, index_columns=[offset_index_id], @@ -999,7 +1005,8 @@ def get_corr_stat(self, column_id_left: str, column_id_right: str): ) ] expr = self.expr.corr_aggregate(corr_aggregations) - expr, offset_index_id = expr.promote_offsets() + offset_index_id = guid.generate_guid() + expr = expr.promote_offsets(offset_index_id) block = Block( expr, index_columns=[offset_index_id], @@ -1197,7 +1204,8 @@ def retrieve_repr_request_results( return formatted_df, count, query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: - expr, result_id = self._expr.promote_offsets() + result_id = guid.generate_guid() + expr = self._expr.promote_offsets(result_id) return ( Block( expr, @@ -1471,67 +1479,76 @@ def merge( "outer", "right", ], - left_col_ids: typing.Sequence[str], - right_col_ids: typing.Sequence[str], + left_join_ids: typing.Sequence[str], + right_join_ids: typing.Sequence[str], sort: bool, suffixes: tuple[str, str] = ("_x", "_y"), ) -> Block: - ( - joined_expr, - coalesced_join_cols, - (get_column_left, get_column_right), - ) = joins.join_by_column( + joined_expr = joins.join_by_column( self.expr, - left_col_ids, + left_join_ids, other.expr, - right_col_ids, + right_join_ids, how=how, - sort=sort, ) + get_column_left, get_column_right = join_names.JOIN_NAME_REMAPPER( + self.expr.column_ids, other.expr.column_ids + ) + result_columns = [] + matching_join_labels = [] + + coalesced_ids = [] + for left_id, right_id in zip(left_join_ids, right_join_ids): + coalesced_id = guid.generate_guid() + joined_expr = joined_expr.project_binary_op( + get_column_left[left_id], + get_column_right[right_id], + ops.coalesce_op, + coalesced_id, + ) + coalesced_ids.append(coalesced_id) + + for col_id in self.value_columns: + if col_id in left_join_ids: + key_part = left_join_ids.index(col_id) + matching_right_id = right_join_ids[key_part] + if ( + self.col_id_to_label[col_id] + == other.col_id_to_label[matching_right_id] + ): + matching_join_labels.append(self.col_id_to_label[col_id]) + result_columns.append(coalesced_ids[key_part]) + else: + result_columns.append(get_column_left[col_id]) + else: + result_columns.append(get_column_left[col_id]) + for col_id in other.value_columns: + if col_id in right_join_ids: + key_part = right_join_ids.index(col_id) + if other.col_id_to_label[matching_right_id] in matching_join_labels: + pass + else: + result_columns.append(get_column_right[col_id]) + else: + result_columns.append(get_column_right[col_id]) - # which join key parts should be coalesced - merge_join_key_mask = [ - str(self.col_id_to_label[left_id]) == str(other.col_id_to_label[right_id]) - for left_id, right_id in zip(left_col_ids, right_col_ids) - ] - labels_to_coalesce = [ - self.col_id_to_label[col_id] - for i, col_id in enumerate(left_col_ids) - if merge_join_key_mask[i] - ] - - def left_col_mapping(col_id: str) -> str: - if col_id in left_col_ids: - join_key_part = left_col_ids.index(col_id) - if merge_join_key_mask[join_key_part]: - return coalesced_join_cols[join_key_part] - return get_column_left(col_id) - - def right_col_mapping(col_id: str) -> typing.Optional[str]: - if col_id in right_col_ids: - join_key_part = right_col_ids.index(col_id) - if merge_join_key_mask[join_key_part]: - return None - return get_column_right(col_id) - - left_columns = [left_col_mapping(col_id) for col_id in self.value_columns] - - right_columns = [ - typing.cast(str, right_col_mapping(col_id)) - for col_id in other.value_columns - if right_col_mapping(col_id) - ] + if sort: + # sort uses coalesced join keys always + joined_expr = joined_expr.order_by( + [ordering.OrderingColumnReference(col_id) for col_id in coalesced_ids], + stable=True, + ) - expr = joined_expr.select_columns([*left_columns, *right_columns]) + joined_expr = joined_expr.select_columns(result_columns) labels = utils.merge_column_labels( self.column_labels, other.column_labels, - coalesce_labels=labels_to_coalesce, + coalesce_labels=matching_join_labels, suffixes=suffixes, ) - # Constructs default index - expr, offset_index_id = expr.promote_offsets() + offset_index_id = guid.generate_guid() + expr = joined_expr.promote_offsets(offset_index_id) return Block(expr, index_columns=[offset_index_id], column_labels=labels) def _force_reproject(self) -> Block: diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 7d15e676493..677bb8529c2 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -17,7 +17,7 @@ from __future__ import annotations import typing -from typing import Callable, Sequence, Tuple, Union +from typing import Mapping, Sequence, Tuple, Union import numpy as np import pandas @@ -27,6 +27,7 @@ import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.joins as joins +import bigframes.core.joins.name_resolution as join_names import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.dtypes @@ -413,7 +414,7 @@ def join( how="left", sort=False, block_identity_join: bool = False, - ) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: + ) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: if not isinstance(other, IndexValue): # TODO(swast): We need to improve this error message to be more # actionable for the user. For example, it's possible they @@ -456,27 +457,34 @@ def join_mono_indexed( how="left", sort=False, block_identity_join: bool = False, -) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: - ( - combined_expr, - joined_index_col_names, - (get_column_left, get_column_right), - ) = joins.join_by_column( +) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: + left_expr = left._block.expr + right_expr = right._block.expr + get_column_left, get_column_right = join_names.JOIN_NAME_REMAPPER( + left_expr.column_ids, right_expr.column_ids + ) + combined_expr = joins.join_by_column( left._block.expr, left._block.index_columns, right._block.expr, right._block.index_columns, how=how, - sort=sort, allow_row_identity_join=(not block_identity_join), ) # Drop original indices from each side. and used the coalesced combination generated by the join. - left_indices = [get_column_left(col_id) for col_id in left._block.index_columns] - right_indices = [get_column_right(col_id) for col_id in right._block.index_columns] - combined_expr = combined_expr.drop_columns(left_indices).drop_columns(right_indices) + left_index = get_column_left[left._block.index_columns[0]] + right_index = get_column_right[right._block.index_columns[0]] + # Drop original indices from each side. and used the coalesced combination generated by the join. + combined_expr, coalesced_join_cols = coalesce_columns( + combined_expr, [left_index], [right_index], how=how + ) + if sort: + combined_expr = combined_expr.order_by( + [order.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] + ) block = blocks.Block( combined_expr, - index_columns=[*joined_index_col_names], + index_columns=coalesced_join_cols, column_labels=[*left._block.column_labels, *right._block.column_labels], index_labels=[left.name] if left.name == right.name else [None], ) @@ -493,7 +501,7 @@ def join_multi_indexed( how="left", sort=False, block_identity_join: bool = False, -) -> Tuple[IndexValue, Tuple[Callable[[str], str], Callable[[str], str]],]: +) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: if not (left.is_uniquely_named() and right.is_uniquely_named()): raise ValueError("Joins not supported on indices with non-unique level names") @@ -508,25 +516,33 @@ def join_multi_indexed( right_join_ids = [right.resolve_level_name(name) for name in common_names] names_fully_match = len(left_only_names) == 0 and len(right_only_names) == 0 - ( - combined_expr, - joined_index_col_names, - (get_column_left, get_column_right), - ) = joins.join_by_column( - left._block.expr, + + left_expr = left._block.expr + right_expr = right._block.expr + get_column_left, get_column_right = join_names.JOIN_NAME_REMAPPER( + left_expr.column_ids, right_expr.column_ids + ) + + combined_expr = joins.join_by_column( + left_expr, left_join_ids, - right._block.expr, + right_expr, right_join_ids, how=how, - sort=sort, # If we're only joining on a subset of the index columns, we need to # perform a true join. - allow_row_identity_join=names_fully_match and not block_identity_join, + allow_row_identity_join=(names_fully_match and not block_identity_join), ) + left_ids_post_join = [get_column_left[id] for id in left_join_ids] + right_ids_post_join = [get_column_right[id] for id in right_join_ids] # Drop original indices from each side. and used the coalesced combination generated by the join. - combined_expr = combined_expr.drop_columns( - [get_column_left(col) for col in left_join_ids] - ).drop_columns([get_column_right(col) for col in right_join_ids]) + combined_expr, coalesced_join_cols = coalesce_columns( + combined_expr, left_ids_post_join, right_ids_post_join, how=how + ) + if sort: + combined_expr = combined_expr.order_by( + [order.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] + ) if left.nlevels == 1: index_labels = right.names @@ -536,12 +552,13 @@ def join_multi_indexed( index_labels = [*common_names, *left_only_names, *right_only_names] def resolve_label_id(label: blocks.Label) -> str: + # if name is shared between both blocks, coalesce the values if label in common_names: - return joined_index_col_names[common_names.index(label)] + return coalesced_join_cols[common_names.index(label)] if label in left_only_names: - return get_column_left(left.resolve_level_name(label)) + return get_column_left[left.resolve_level_name(label)] if label in right_only_names: - return get_column_right(right.resolve_level_name(label)) + return get_column_right[right.resolve_level_name(label)] raise ValueError(f"Unexpected label: {label}") index_columns = [resolve_label_id(label) for label in index_labels] @@ -556,3 +573,29 @@ def resolve_label_id(label: blocks.Label) -> str: typing.cast(IndexValue, block.index), (get_column_left, get_column_right), ) + + +def coalesce_columns( + expr: core.ArrayValue, + left_ids: typing.Sequence[str], + right_ids: typing.Sequence[str], + how: str, +) -> Tuple[core.ArrayValue, Sequence[str]]: + result_ids = [] + for left_id, right_id in zip(left_ids, right_ids): + if how == "left" or how == "inner": + result_ids.append(left_id) + expr = expr.drop_columns([right_id]) + elif how == "right": + result_ids.append(right_id) + expr = expr.drop_columns([left_id]) + elif how == "outer": + coalesced_id = bigframes.core.guid.generate_guid() + expr = expr.project_binary_op( + left_id, right_id, ops.coalesce_op, coalesced_id + ) + expr = expr.drop_columns([left_id, right_id]) + result_ids.append(coalesced_id) + else: + raise ValueError(f"Unexpected join type: {how}. {constants.FEEDBACK_LINK}") + return expr, result_ids diff --git a/bigframes/core/joins/name_resolution.py b/bigframes/core/joins/name_resolution.py new file mode 100644 index 00000000000..df946b3a590 --- /dev/null +++ b/bigframes/core/joins/name_resolution.py @@ -0,0 +1,46 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from typing import Mapping, Sequence, Tuple + + +class JoinNameRemapper: + def __init__(self, namespace: str) -> None: + self._namespace = namespace + + def __call__( + self, left_column_ids: Sequence[str], right_column_ids: Sequence[str] + ) -> Tuple[Mapping[str, str], Mapping[str, str]]: + """ + When joining column ids from different namespaces, this function defines how names are remapped. + + Take care to map value column ids and hidden column ids in separate namespaces. This is important because value + column ids must be deterministic as they are referenced by dependent operators. The generation of hidden ids is + dependent on compilation context, and should be completely separated from value column id mappings. + """ + # This naming strategy depends on the number of value columns in source tables. + # This means column id mappings must be adjusted if pushing operations above or below join in transformation + new_left_ids = { + col: f"{self._namespace}_l_{i}" for i, col in enumerate(left_column_ids) + } + new_right_ids = { + col: f"{self._namespace}_r_{i}" for i, col in enumerate(right_column_ids) + } + return new_left_ids, new_right_ids + + +# Defines how column ids are remapped, regardless of join strategy or ordering mode +# Use this remapper for all value column remappings. +JOIN_NAME_REMAPPER = JoinNameRemapper("bfjoin") diff --git a/bigframes/core/joins/row_identity.py b/bigframes/core/joins/row_identity.py index 156e7aef407..76e456ec942 100644 --- a/bigframes/core/joins/row_identity.py +++ b/bigframes/core/joins/row_identity.py @@ -18,20 +18,20 @@ import functools import typing -from typing import Callable, Tuple import ibis import ibis.expr.types as ibis_types import bigframes.constants as constants import bigframes.core as core +import bigframes.core.joins.name_resolution as naming SUPPORTED_ROW_IDENTITY_HOW = {"outer", "left", "inner"} def join_by_row_identity( left: core.ArrayValue, right: core.ArrayValue, *, how: str -) -> Tuple[core.ArrayValue, Tuple[Callable[[str], str], Callable[[str], str]],]: +) -> core.ArrayValue: """Compute join when we are joining by row identity not a specific column.""" if how not in SUPPORTED_ROW_IDENTITY_HOW: raise NotImplementedError( @@ -62,31 +62,42 @@ def join_by_row_identity( left_mask = left_relative_predicates if how in ["right", "outer"] else None right_mask = right_relative_predicates if how in ["left", "outer"] else None + + # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result + lpublicmapping, rpublicmapping = naming.JOIN_NAME_REMAPPER( + left.column_ids, right.column_ids + ) + lhiddenmapping, rhiddenmapping = naming.JoinNameRemapper(namespace="hidden")( + left._hidden_column_ids, right._hidden_column_ids + ) + map_left_id = {**lpublicmapping, **lhiddenmapping} + map_right_id = {**rpublicmapping, **rhiddenmapping} + joined_columns = [ - _mask_value(left._get_ibis_column(key), left_mask).name(map_left_id(key)) + _mask_value(left._get_ibis_column(key), left_mask).name(map_left_id[key]) for key in left.column_ids ] + [ - _mask_value(right._get_ibis_column(key), right_mask).name(map_right_id(key)) + _mask_value(right._get_ibis_column(key), right_mask).name(map_right_id[key]) for key in right.column_ids ] # If left isn't being masked, can just use left ordering if not left_mask: col_mapping = { - order_ref.column_id: map_left_id(order_ref.column_id) + order_ref.column_id: map_left_id[order_ref.column_id] for order_ref in left._ordering.ordering_value_columns } new_ordering = left._ordering.with_column_remap(col_mapping) else: ordering_columns = [ - col_ref.with_name(map_left_id(col_ref.column_id)) + col_ref.with_name(map_left_id[col_ref.column_id]) for col_ref in left._ordering.ordering_value_columns ] + [ - col_ref.with_name(map_right_id(col_ref.column_id)) + col_ref.with_name(map_right_id[col_ref.column_id]) for col_ref in right._ordering.ordering_value_columns ] left_total_order_cols = frozenset( - map_left_id(col) for col in left._ordering.total_ordering_columns + map_left_id[col] for col in left._ordering.total_ordering_columns ) # Assume that left ordering is sufficient since 1:1 join over same base table join_total_order_cols = left_total_order_cols @@ -95,12 +106,12 @@ def join_by_row_identity( ) hidden_ordering_columns = [ - left._get_hidden_ordering_column(key.column_id).name(map_left_id(key.column_id)) + left._get_hidden_ordering_column(key.column_id).name(map_left_id[key.column_id]) for key in left._ordering.ordering_value_columns if key.column_id in left._hidden_ordering_column_names.keys() ] + [ right._get_hidden_ordering_column(key.column_id).name( - map_right_id(key.column_id) + map_right_id[key.column_id] ) for key in right._ordering.ordering_value_columns if key.column_id in right._hidden_ordering_column_names.keys() @@ -114,18 +125,7 @@ def join_by_row_identity( ordering=new_ordering, predicates=combined_predicates, ) - return joined_expr, ( - lambda key: map_left_id(key), - lambda key: map_right_id(key), - ) - - -def map_left_id(left_side_id): - return f"{left_side_id}_x" - - -def map_right_id(right_side_id): - return f"{right_side_id}_y" + return joined_expr def _mask_value( diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/joins/single_column.py index f194b8f8c41..0c0e2008b58 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/joins/single_column.py @@ -16,17 +16,15 @@ from __future__ import annotations -import itertools import typing -from typing import Callable, Literal, Tuple +from typing import Literal, Mapping import ibis import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types -import bigframes.constants as constants import bigframes.core as core -import bigframes.core.guid as guid +import bigframes.core.joins.name_resolution as naming import bigframes.core.joins.row_identity import bigframes.core.ordering @@ -43,13 +41,8 @@ def join_by_column( "outer", "right", ], - sort: bool = False, allow_row_identity_join: bool = True, -) -> Tuple[ - core.ArrayValue, - typing.Sequence[str], - Tuple[Callable[[str], str], Callable[[str], str]], -]: +) -> core.ArrayValue: """Join two expressions by column equality. Arguments: @@ -62,14 +55,9 @@ def join_by_column( If True, allow matching by row identity. Set to False to always perform a true JOIN in generated SQL. Returns: - The joined expression and the objects needed to interpret it. - - * ArrayValue: Joined table with all columns from left and right. - * Sequence[str]: Column IDs of the coalesced join columns. Sometimes either the - left/right table will have missing rows. This column pulls the - non-NULL value from either left/right. - * Tuple[Callable, Callable]: For a given column ID from left or right, - respectively, return the new column id from the combined expression. + The joined expression. The resulting columns will be, in order, + first the coalesced join keys, then, all the left columns, and + finally, all the right columns. """ if ( allow_row_identity_join @@ -85,71 +73,33 @@ def join_by_column( for lcol, rcol in zip(left_column_ids, right_column_ids) ) ): - combined_expr, ( - get_column_left, - get_column_right, - ) = bigframes.core.joins.row_identity.join_by_row_identity(left, right, how=how) - left_join_keys = [ - combined_expr._get_ibis_column(get_column_left(col)) - for col in left_column_ids - ] - right_join_keys = [ - combined_expr._get_ibis_column(get_column_right(col)) - for col in right_column_ids - ] - join_key_cols = get_coalesced_join_cols(left_join_keys, right_join_keys, how) - join_key_ids = [col.get_name() for col in join_key_cols] - combined_expr = combined_expr._projection( - [*join_key_cols, *combined_expr.columns] - ) - if sort: - combined_expr = combined_expr.order_by( - [ - core.OrderingColumnReference(join_col_id) - for join_col_id in join_key_ids - ] - ) - return ( - combined_expr, - join_key_ids, - ( - get_column_left, - get_column_right, - ), + return bigframes.core.joins.row_identity.join_by_row_identity( + left, right, how=how ) else: - lmapping = { - col_id: guid.generate_guid() - for col_id in itertools.chain( - left.column_ids, left._hidden_ordering_column_names - ) - } - rmapping = { - col_id: guid.generate_guid() - for col_id in itertools.chain( - right.column_ids, right._hidden_ordering_column_names - ) - } - - def get_column_left(col_id): - return lmapping[col_id] - - def get_column_right(col_id): - return rmapping[col_id] + # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result + l_public_mapping, r_public_mapping = naming.JOIN_NAME_REMAPPER( + left.column_ids, right.column_ids + ) + l_hidden_mapping, r_hidden_mapping = naming.JoinNameRemapper( + namespace="hidden" + )(left._hidden_column_ids, right._hidden_column_ids) + l_mapping = {**l_public_mapping, **l_hidden_mapping} + r_mapping = {**r_public_mapping, **r_hidden_mapping} left_table = left._to_ibis_expr( "unordered", expose_hidden_cols=True, - col_id_overrides=lmapping, + col_id_overrides=l_mapping, ) right_table = right._to_ibis_expr( "unordered", expose_hidden_cols=True, - col_id_overrides=rmapping, + col_id_overrides=r_mapping, ) join_conditions = [ - value_to_join_key(left_table[lmapping[left_index]]) - == value_to_join_key(right_table[rmapping[right_index]]) + value_to_join_key(left_table[l_mapping[left_index]]) + == value_to_join_key(right_table[r_mapping[right_index]]) for left_index, right_index in zip(left_column_ids, right_column_ids) ] @@ -158,97 +108,39 @@ def get_column_right(col_id): right_table, predicates=join_conditions, how=how, - lname="{name}_x", - rname="{name}_y", ) # Preserve ordering accross joins. ordering = join_orderings( left._ordering, right._ordering, - get_column_left, - get_column_right, + l_mapping, + r_mapping, left_order_dominates=(how != "right"), ) - left_join_keys = [ - combined_table[get_column_left(col)] for col in left_column_ids - ] - right_join_keys = [ - combined_table[get_column_right(col)] for col in right_column_ids - ] - join_key_cols = get_coalesced_join_cols(left_join_keys, right_join_keys, how) # We could filter out the original join columns, but predicates/ordering # might still reference them in implicit joins. - columns = ( - join_key_cols - + [combined_table[get_column_left(col.get_name())] for col in left.columns] - + [ - combined_table[get_column_right(col.get_name())] - for col in right.columns - ] - ) + columns = [ + combined_table[l_mapping[col.get_name()]] for col in left.columns + ] + [combined_table[r_mapping[col.get_name()]] for col in right.columns] hidden_ordering_columns = [ *[ - combined_table[get_column_left(col.get_name())] - for col in left.hidden_ordering_columns + combined_table[l_hidden_mapping[col.get_name()]] + for col in left._hidden_ordering_columns ], *[ - combined_table[get_column_right(col.get_name())] - for col in right.hidden_ordering_columns + combined_table[r_hidden_mapping[col.get_name()]] + for col in right._hidden_ordering_columns ], ] - combined_expr = core.ArrayValue( + return core.ArrayValue( left._session, combined_table, columns=columns, hidden_ordering_columns=hidden_ordering_columns, ordering=ordering, ) - if sort: - combined_expr = combined_expr.order_by( - [ - core.OrderingColumnReference(join_key_col.get_name()) - for join_key_col in join_key_cols - ] - ) - return ( - combined_expr, - [key.get_name() for key in join_key_cols], - (get_column_left, get_column_right), - ) - - -def get_coalesced_join_cols( - left_join_cols: typing.Iterable[ibis_types.Value], - right_join_cols: typing.Iterable[ibis_types.Value], - how: str, -) -> typing.List[ibis_types.Value]: - join_key_cols: list[ibis_types.Value] = [] - for left_col, right_col in zip(left_join_cols, right_join_cols): - if how == "left" or how == "inner": - join_key_cols.append(left_col.name(guid.generate_guid(prefix="index_"))) - elif how == "right": - join_key_cols.append(right_col.name(guid.generate_guid(prefix="index_"))) - elif how == "outer": - # The left index and the right index might contain null values, for - # example due to an outer join with different numbers of rows. Coalesce - # these to take the index value from either column. - # Use a random name in case the left index and the right index have the - # same name. In such a case, _x and _y suffixes will already be used. - # Don't need to coalesce if they are exactly the same column. - if left_col.name("index").equals(right_col.name("index")): - join_key_cols.append(left_col.name(guid.generate_guid(prefix="index_"))) - else: - join_key_cols.append( - ibis.coalesce( - left_col, - right_col, - ).name(guid.generate_guid(prefix="index_")) - ) - else: - raise ValueError(f"Unexpected join type: {how}. {constants.FEEDBACK_LINK}") - return join_key_cols def value_to_join_key(value: ibis_types.Value): @@ -261,16 +153,16 @@ def value_to_join_key(value: ibis_types.Value): def join_orderings( left: core.ExpressionOrdering, right: core.ExpressionOrdering, - left_id_mapping: Callable[[str], str], - right_id_mapping: Callable[[str], str], + left_id_mapping: Mapping[str, str], + right_id_mapping: Mapping[str, str], left_order_dominates: bool = True, ) -> core.ExpressionOrdering: left_ordering_refs = [ - ref.with_name(left_id_mapping(ref.column_id)) + ref.with_name(left_id_mapping[ref.column_id]) for ref in left.all_ordering_columns ] right_ordering_refs = [ - ref.with_name(right_id_mapping(ref.column_id)) + ref.with_name(right_id_mapping[ref.column_id]) for ref in right.all_ordering_columns ] if left_order_dominates: @@ -279,10 +171,10 @@ def join_orderings( joined_refs = [*right_ordering_refs, *left_ordering_refs] left_total_order_cols = frozenset( - [left_id_mapping(id) for id in left.total_ordering_columns] + [left_id_mapping[id] for id in left.total_ordering_columns] ) right_total_order_cols = frozenset( - [right_id_mapping(id) for id in right.total_ordering_columns] + [right_id_mapping[id] for id in right.total_ordering_columns] ) return core.ExpressionOrdering( ordering_value_columns=joined_refs, diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 015a7642f8b..c91ddffada8 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -419,7 +419,7 @@ def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: get_column_right, ) = self._block.index.join(key._block.index, how="left") block = combined_index._block - filter_col_id = get_column_right(key._value_column) + filter_col_id = get_column_right[key._value_column] block = block.filter(filter_col_id) block = block.drop_columns([filter_col_id]) return DataFrame(block) @@ -560,18 +560,18 @@ def _apply_series_binop( ) series_column_id = other._value_column - series_col = get_column_right(series_column_id) + series_col = get_column_right[series_column_id] block = joined_index._block for column_id, label in zip( self._block.value_columns, self._block.column_labels ): block, _ = block.apply_binary_op( - get_column_left(column_id), + get_column_left[column_id], series_col, op, result_label=label, ) - block = block.drop_columns([get_column_left(column_id)]) + block = block.drop_columns([get_column_left[column_id]]) block = block.drop_columns([series_col]) block = block.with_index_labels(self.index.names) @@ -603,22 +603,22 @@ def _apply_dataframe_binop( left_col_id = self._block.value_columns[left_index] right_col_id = other._block.value_columns[right_index] block, result_col_id = block.apply_binary_op( - get_column_left(left_col_id), - get_column_right(right_col_id), + get_column_left[left_col_id], + get_column_right[right_col_id], op, ) binop_result_ids.append(result_col_id) elif left_index >= 0: left_col_id = self._block.value_columns[left_index] block, result_col_id = block.apply_unary_op( - get_column_left(left_col_id), + get_column_left[left_col_id], ops.partial_right(op, None), ) binop_result_ids.append(result_col_id) elif right_index >= 0: right_col_id = other._block.value_columns[right_index] block, result_col_id = block.apply_unary_op( - get_column_right(right_col_id), + get_column_right[right_col_id], ops.partial_left(op, None), ) binop_result_ids.append(result_col_id) @@ -974,7 +974,7 @@ def _drop_by_index(self, index: indexes.Index) -> DataFrame: block.index ) - new_ordering_col = get_column_right(ordering_col) + new_ordering_col = get_column_right[ordering_col] drop_block = joined_index._block drop_block, drop_col = drop_block.apply_unary_op( new_ordering_col, @@ -983,7 +983,7 @@ def _drop_by_index(self, index: indexes.Index) -> DataFrame: drop_block = drop_block.filter(drop_col) original_columns = [ - get_column_left(column) for column in self._block.value_columns + get_column_left[column] for column in self._block.value_columns ] drop_block = drop_block.select_columns(original_columns) return DataFrame(drop_block) @@ -1119,7 +1119,8 @@ def _assign_single_item( # local_df is likely (but not guarunteed) to be cached locally # since the original list came from memory and so is probably < MAX_INLINE_DF_SIZE - this_expr, this_offsets_col_id = self._get_block()._expr.promote_offsets() + this_offsets_col_id = bigframes.core.guid.generate_guid() + this_expr = self._get_block()._expr.promote_offsets(this_offsets_col_id) block = blocks.Block( expr=this_expr, index_labels=self.index.names, @@ -1156,10 +1157,10 @@ def _assign_series_join_on_index( ) column_ids = [ - get_column_left(col_id) for col_id in self._block.cols_matching_label(label) + get_column_left[col_id] for col_id in self._block.cols_matching_label(label) ] block = joined_index._block - source_column = get_column_right(series._value_column) + source_column = get_column_right[series._value_column] # Replace each column matching the label for column_id in column_ids: @@ -2032,8 +2033,8 @@ def _groupby_series( key._block.index, how="inner" if dropna else "left" ) col_ids = [ - *[get_column_left(value) for value in col_ids], - get_column_right(key._value_column), + *[get_column_left[value] for value in col_ids], + get_column_right[key._value_column], ] block = combined_index._block else: diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index f330a703b20..a29dd36c72a 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -913,6 +913,16 @@ def ge_op( return x >= y +def coalesce_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + if x.name("name").equals(y.name("name")): + return x + else: + return ibis.coalesce(x, y) + + @short_circuit_nulls(ibis_dtypes.int) def floordiv_op( x: ibis_types.Value, diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index fc76d07edbb..b9abb2cc038 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -196,8 +196,8 @@ def _align_n( get_column_right, ) = block.index.join(other._block.index, how=how) value_ids = [ - *[get_column_left(value) for value in value_ids], - get_column_right(other._value_column), + *[get_column_left[value] for value in value_ids], + get_column_right[other._value_column], ] block = combined_index._block else: diff --git a/bigframes/series.py b/bigframes/series.py index 56e1b43a032..4f2f73bd605 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1136,10 +1136,10 @@ def _groupby_values( key._block.index, how="inner" if dropna else "left" ) - value_col = get_column_left(self._value_column) + value_col = get_column_left[self._value_column] grouping_cols = [ - *[get_column_left(value) for value in grouping_cols], - get_column_right(key._value_column), + *[get_column_left[value] for value in grouping_cols], + get_column_right[key._value_column], ] block = combined_index._block else: From db51fe340f644a0d7c911c11d92c8299a4be3446 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:20:38 -0700 Subject: [PATCH 078/921] docs: add open-source link in API doc (#106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index b2ad1ee0179..7b4f1986bcd 100644 --- a/README.rst +++ b/README.rst @@ -13,6 +13,7 @@ BigQuery DataFrames is an open-source package. You can run Documentation ------------- +* `BigQuery DataFrames source code (GitHub) `_ * `BigQuery DataFrames sample notebooks `_ * `BigQuery DataFrames API reference `_ * `BigQuery documentation `_ From 1b3f3a5374915b2833c6c1ac05670e9708f07bff Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:12:20 -0700 Subject: [PATCH 079/921] docs: update ML overview API doc (#105) --- README.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 7b4f1986bcd..5ddb4a7639a 100644 --- a/README.rst +++ b/README.rst @@ -95,10 +95,18 @@ using the and the `bigframes.ml.compose module `_. BigQuery DataFrames offers the following transformations: -* Use the `OneHotEncoder class `_ - in the ``bigframes.ml.preprocessing`` module to transform categorical values into numeric format. +* Use the `KBinsDiscretizer class `_ + in the ``bigframes.ml.preprocessing`` module to bin continuous data into intervals. +* Use the `LabelEncoder class `_ + in the ``bigframes.ml.preprocessing`` module to normalize the target labels as integer values. +* Use the `MaxAbsScaler class `_ + in the ``bigframes.ml.preprocessing`` module to scale each feature to the range ``[-1, 1]`` by its maximum absolute value. +* Use the `MinMaxScaler class `_ + in the ``bigframes.ml.preprocessing`` module to standardize features by scaling each feature to the range ``[0, 1]``. * Use the `StandardScaler class `_ in the ``bigframes.ml.preprocessing`` module to standardize features by removing the mean and scaling to unit variance. +* Use the `OneHotEncoder class `_ + in the ``bigframes.ml.preprocessing`` module to transform categorical values into numeric format. * Use the `ColumnTransformer class `_ in the ``bigframes.ml.compose`` module to apply transformers to DataFrames columns. From 752a1d606f3e79554bffa7c4f456ee938ec446bc Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Fri, 13 Oct 2023 15:44:13 -0700 Subject: [PATCH 080/921] style: improve cancellation string (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/formatting_helpers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index f3e3a76ce42..6851bdd2bd4 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -155,7 +155,10 @@ def wait_for_query_job( raise except KeyboardInterrupt: query_job.cancel() - print(f"Requested cancelation for {query_job.job_type} {query_job.job_id}...") + print( + f"Requested cancellation for {query_job.job_type.capitalize()}" + f" job {query_job.job_id} in location {query_job.location}..." + ) # begin the cancel request before immediately rethrowing raise @@ -197,7 +200,10 @@ def wait_for_job(job: GenericJob, progress_bar: Optional[str] = None): raise except KeyboardInterrupt: job.cancel() - print(f"Requested cancelation for {job.job_type} {job.job_id}...") + print( + f"Requested cancellation for {job.job_type.capitalize()}" + f" job {job.job_id} in location {job.location}..." + ) # begin the cancel request before immediately rethrowing raise From e6c0cd1777736e0fa7285da59625fbac487573bd Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 16 Oct 2023 16:30:16 -0500 Subject: [PATCH 081/921] perf: if primary keys are defined, `read_gbq` avoids copying table data (#112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We make the same uniqueness assumption as the query engine and use these columns as the total ordering. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue b/305260214 🦕 --- bigframes/core/indexers.py | 3 +- bigframes/session.py | 110 +++++++++++++++++++++++++---- noxfile.py | 1 - tests/system/conftest.py | 31 ++++++++ tests/system/small/test_session.py | 25 +++++++ 5 files changed, 153 insertions(+), 17 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index a74880041ca..09f0d5956c1 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -21,6 +21,7 @@ import pandas as pd import bigframes.constants as constants +import bigframes.core.blocks import bigframes.core.guid as guid import bigframes.core.indexes as indexes import bigframes.core.scalar @@ -214,7 +215,7 @@ def __getitem__(self, key: tuple) -> bigframes.core.scalar.Scalar: raise ValueError(error_message) if len(key) != 2: raise TypeError(error_message) - block = self._dataframe._block + block: bigframes.core.blocks.Block = self._dataframe._block column_block = block.select_columns([block.value_columns[key[1]]]) column = bigframes.series.Series(column_block) return column.iloc[key[0]] diff --git a/bigframes/session.py b/bigframes/session.py index 4f509f0704a..6c1160c88e6 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -498,6 +498,8 @@ def read_gbq_query( See also: :meth:`Session.read_gbq`. """ + # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so + # these docstrings are inline. return self._read_gbq_query( query=query, index_col=index_col, @@ -515,8 +517,6 @@ def _read_gbq_query( max_results: Optional[int] = None, api_name: str, ) -> dataframe.DataFrame: - # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so - # these docstrings are inline. if isinstance(index_col, str): index_cols = [index_col] else: @@ -561,6 +561,8 @@ def read_gbq_table( See also: :meth:`Session.read_gbq`. """ + # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so + # these docstrings are inline. return self._read_gbq_table( query=query, index_col=index_col, @@ -569,6 +571,62 @@ def read_gbq_table( api_name="read_gbq_table", ) + def _read_gbq_table_to_ibis_with_total_ordering( + self, + table_ref: bigquery.table.TableReference, + *, + api_name: str, + ) -> Tuple[ibis_types.Table, Optional[Sequence[str]]]: + """Create a read-only Ibis table expression representing a table. + + If we can get a total ordering from the table, such as via primary key + column(s), then return those too so that ordering generation can be + avoided. + """ + if table_ref.dataset_id.upper() == "_SESSION": + # _SESSION tables aren't supported by the tables.get REST API. + return ( + self.ibis_client.sql( + f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" + ), + None, + ) + + table_expression = self.ibis_client.table( + table_ref.table_id, + database=f"{table_ref.project}.{table_ref.dataset_id}", + ) + + # If there are primary keys defined, the query engine assumes these + # columns are unique, even if the constraint is not enforced. We make + # the same assumption and use these columns as the total ordering keys. + table = self.bqclient.get_table(table_ref) + + # TODO(b/305264153): Use public properties to fetch primary keys once + # added to google-cloud-bigquery. + primary_keys = ( + table._properties.get("tableConstraints", {}) + .get("primaryKey", {}) + .get("columns") + ) + + if not primary_keys: + return table_expression, None + else: + # Read from a snapshot since we won't have to copy the table data to create a total ordering. + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + current_timestamp = list( + self.bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + table_expression = self.ibis_client.sql( + bigframes_io.create_snapshot_sql(table_ref, current_timestamp) + ) + return table_expression, primary_keys + def _read_gbq_table( self, query: str, @@ -581,24 +639,19 @@ def _read_gbq_table( if max_results and max_results <= 0: raise ValueError("`max_results` should be a positive number.") - # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so - # these docstrings are inline. # TODO(swast): Can we re-use the temp table from other reads in the # session, if the original table wasn't modified? table_ref = bigquery.table.TableReference.from_string( query, default_project=self.bqclient.project ) - if table_ref.dataset_id.upper() == "_SESSION": - # _SESSION tables aren't supported by the tables.get REST API. - table_expression = self.ibis_client.sql( - f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" - ) - else: - table_expression = self.ibis_client.table( - table_ref.table_id, - database=f"{table_ref.project}.{table_ref.dataset_id}", - ) + ( + table_expression, + total_ordering_cols, + ) = self._read_gbq_table_to_ibis_with_total_ordering( + table_ref, + api_name=api_name, + ) for key in col_order: if key not in table_expression.columns: @@ -624,7 +677,34 @@ def _read_gbq_table( ordering = None is_total_ordering = False - if len(index_cols) != 0: + if total_ordering_cols is not None: + # Note: currently, this a table has a total ordering only when the + # primary key(s) are set on a table. The query engine assumes such + # columns are unique, even if not enforced. + is_total_ordering = True + ordering = core.ExpressionOrdering( + ordering_value_columns=[ + core.OrderingColumnReference(column_id) + for column_id in total_ordering_cols + ], + total_ordering_columns=frozenset(total_ordering_cols), + ) + + if len(index_cols) != 0: + index_labels = typing.cast(List[Optional[str]], index_cols) + else: + # Use the total_ordering_cols to project offsets to use as the default index. + table_expression = table_expression.order_by(index_cols) + default_index_id = guid.generate_guid("bigframes_index_") + default_index_col = ( + ibis.row_number().cast(ibis_dtypes.int64).name(default_index_id) + ) + table_expression = table_expression.mutate( + **{default_index_id: default_index_col} + ) + index_cols = [default_index_id] + index_labels = [None] + elif len(index_cols) != 0: index_labels = typing.cast(List[Optional[str]], index_cols) distinct_table = table_expression.select(*index_cols).distinct() is_unique_sql = f"""WITH full_table AS ( diff --git a/noxfile.py b/noxfile.py index 84e5ab11bb4..54ccdb9a877 100644 --- a/noxfile.py +++ b/noxfile.py @@ -89,7 +89,6 @@ "system", "doctest", "cover", - "release_dry_run", ] # Error if a python version is missing diff --git a/tests/system/conftest.py b/tests/system/conftest.py index ed22a3e8dac..f36a29b0ab1 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -17,6 +17,7 @@ import logging import math import pathlib +import textwrap import typing from typing import Dict, Optional @@ -795,6 +796,36 @@ def penguins_randomforest_classifier_model_name( return model_name +@pytest.fixture(scope="session") +def usa_names_grouped_table( + session: bigframes.Session, dataset_id_permanent +) -> bigquery.Table: + """Provides a table with primary key(s) set.""" + table_id = f"{dataset_id_permanent}.usa_names_grouped" + try: + return session.bqclient.get_table(table_id) + except google.cloud.exceptions.NotFound: + query = textwrap.dedent( + f""" + CREATE TABLE `{dataset_id_permanent}.usa_names_grouped` + ( + total_people INT64, + name STRING, + gender STRING, + year INT64, + PRIMARY KEY(name, gender, year) NOT ENFORCED + ) + AS + SELECT SUM(`number`) AS total_people, name, gender, year + FROM `bigquery-public-data.usa_names.usa_1910_2013` + GROUP BY name, gender, year + """ + ) + job = session.bqclient.query(query) + job.result() + return session.bqclient.get_table(table_id) + + @pytest.fixture() def deferred_repr(): bigframes.options.display.repr_mode = "deferred" diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index bfe9bc8d0f9..127a88a760b 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -20,6 +20,7 @@ from typing import List import google.api_core.exceptions +import google.cloud.bigquery as bigquery import numpy as np import pandas as pd import pytest @@ -231,6 +232,30 @@ def test_read_gbq_w_anonymous_query_results_table(session: bigframes.Session): pd.testing.assert_frame_equal(result, expected, check_dtype=False) +def test_read_gbq_w_primary_keys_table( + session: bigframes.Session, usa_names_grouped_table: bigquery.Table +): + table = usa_names_grouped_table + # TODO(b/305264153): Use public properties to fetch primary keys once + # added to google-cloud-bigquery. + primary_keys = ( + table._properties.get("tableConstraints", {}) + .get("primaryKey", {}) + .get("columns") + ) + assert len(primary_keys) != 0 + + df = session.read_gbq(f"{table.project}.{table.dataset_id}.{table.table_id}") + result = df.head(100).to_pandas() + + # Verify that the DataFrame is already sorted by primary keys. + sorted_result = result.sort_values(primary_keys) + pd.testing.assert_frame_equal(result, sorted_result) + + # Verify that we're working from a snapshot rather than a copy of the table. + assert "FOR SYSTEM_TIME AS OF TIMESTAMP" in df.sql + + @pytest.mark.parametrize( ("query_or_table", "max_results"), [ From 752b01ff9df114c54ed58eb96956e9ce34a8ed47 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Mon, 16 Oct 2023 17:10:00 -0700 Subject: [PATCH 082/921] feat: add AtIndexer getitems (#107) * feat: add AtIndexer getitems * fix third party docstrings * use loc from at --------- Co-authored-by: Henry J Solberg --- bigframes/core/indexers.py | 24 +++++++++++++++++++ bigframes/dataframe.py | 4 ++++ bigframes/series.py | 4 ++++ tests/system/small/test_dataframe.py | 23 ++++++++++++++++++ tests/system/small/test_series.py | 10 ++++++++ .../bigframes_vendored/pandas/core/frame.py | 5 ++++ .../bigframes_vendored/pandas/core/series.py | 5 ++++ 7 files changed, 75 insertions(+) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 09f0d5956c1..01c1162ae6d 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -108,6 +108,16 @@ def __getitem__(self, key: int) -> bigframes.core.scalar.Scalar: return self._series.iloc[key] +class AtSeriesIndexer: + def __init__(self, series: bigframes.series.Series): + self._series = series + + def __getitem__( + self, key: LocSingleKey + ) -> Union[bigframes.core.scalar.Scalar, bigframes.series.Series]: + return self._series.loc[key] + + class LocDataFrameIndexer: def __init__(self, dataframe: bigframes.dataframe.DataFrame): self._dataframe = dataframe @@ -221,6 +231,20 @@ def __getitem__(self, key: tuple) -> bigframes.core.scalar.Scalar: return column.iloc[key[0]] +class AtDataFrameIndexer: + def __init__(self, dataframe: bigframes.dataframe.DataFrame): + self._dataframe = dataframe + + def __getitem__( + self, key: tuple + ) -> Union[bigframes.core.scalar.Scalar, bigframes.series.Series]: + if not isinstance(key, tuple): + raise TypeError( + "DataFrame.at should be indexed by a (row label, column name) tuple." + ) + return self._dataframe.loc[key] + + @typing.overload def _loc_getitem_series_or_dataframe( series_or_dataframe: bigframes.series.Series, key diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index c91ddffada8..5740d2c4dc8 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -259,6 +259,10 @@ def iloc(self) -> indexers.ILocDataFrameIndexer: def iat(self) -> indexers.IatDataFrameIndexer: return indexers.IatDataFrameIndexer(self) + @property + def at(self) -> indexers.AtDataFrameIndexer: + return indexers.AtDataFrameIndexer(self) + @property def dtypes(self) -> pandas.Series: return pandas.Series(data=self._block.dtypes, index=self._block.column_labels) diff --git a/bigframes/series.py b/bigframes/series.py index 4f2f73bd605..84ca2a578f8 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -91,6 +91,10 @@ def iloc(self) -> bigframes.core.indexers.IlocSeriesIndexer: def iat(self) -> bigframes.core.indexers.IatSeriesIndexer: return bigframes.core.indexers.IatSeriesIndexer(self) + @property + def at(self) -> bigframes.core.indexers.AtSeriesIndexer: + return bigframes.core.indexers.AtSeriesIndexer(self) + @property def name(self) -> blocks.Label: return self._name diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 9f1092d09d1..ecafb7c1bf5 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2196,6 +2196,29 @@ def test_loc_single_index_no_duplicate(scalars_df_index, scalars_pandas_df_index ) +def test_at_with_duplicate(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.set_index("string_col", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index( + "string_col", drop=False + ) + index = "Hello, World!" + bf_result = scalars_df_index.at[index, "int64_too"] + pd_result = scalars_pandas_df_index.at[index, "int64_too"] + pd.testing.assert_series_equal( + bf_result.to_pandas(), + pd_result, + ) + + +def test_at_no_duplicate(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.set_index("int64_too", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index("int64_too", drop=False) + index = -2345 + bf_result = scalars_df_index.at[index, "string_col"] + pd_result = scalars_pandas_df_index.at[index, "string_col"] + assert bf_result == pd_result + + def test_loc_setitem_bool_series_scalar_new_col(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_df = scalars_df.copy() diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 802425510aa..f2ced841daf 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1953,6 +1953,16 @@ def test_series_iloc(scalars_df_index, scalars_pandas_df_index, start, stop, ste ) +def test_at(scalars_df_index, scalars_pandas_df_index): + scalars_df_index = scalars_df_index.set_index("int64_too", drop=False) + scalars_pandas_df_index = scalars_pandas_df_index.set_index("int64_too", drop=False) + index = -2345 + bf_result = scalars_df_index["string_col"].at[index] + pd_result = scalars_pandas_df_index["string_col"].at[index] + + assert bf_result == pd_result + + def test_iat(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index["int64_too"].iat[3] pd_result = scalars_pandas_df_index["int64_too"].iat[3] diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index e54f984d59b..621d052cb8e 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2112,3 +2112,8 @@ def iloc(self): def iat(self): """Access a single value for a row/column pair by integer position.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def at(self): + """Access a single value for a row/column label pair.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 03729922d5a..ec3c07bfa85 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1833,3 +1833,8 @@ def iloc(self): def iat(self): """Access a single value for a row/column pair by integer position.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def at(self): + """Access a single value for a row/column label pair.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From ec10c4a5a7833c42e28fe9e7b734bc0c4fb84b6e Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 17 Oct 2023 02:55:40 +0000 Subject: [PATCH 083/921] feat: Support external packages in `remote_function` (#98) * feat: Support external packages in `remote_function` * Update code sample demonstrating external packages for `remote_function` * GCF customization for hackathon --- bigframes/pandas/__init__.py | 2 + bigframes/remote_function.py | 49 ++++++++++++++++------ bigframes/session.py | 8 +++- samples/snippets/remote_function.py | 18 +++++--- tests/system/large/test_remote_function.py | 45 ++++++++++++++++++++ 5 files changed, 103 insertions(+), 19 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index ded4760fa3b..833f0d1d1dd 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -400,6 +400,7 @@ def remote_function( bigquery_connection: Optional[str] = None, reuse: bool = True, name: Optional[str] = None, + packages: Optional[Sequence[str]] = None, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -409,6 +410,7 @@ def remote_function( bigquery_connection=bigquery_connection, reuse=reuse, name=name, + packages=packages, ) diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index fd9aec825fa..c82ba840564 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -100,9 +100,12 @@ def get_remote_function_locations(bq_location): return bq_location, cloud_function_region -def _get_hash(def_): +def _get_hash(def_, package_requirements=None): "Get hash (32 digits alphanumeric) of a function." def_repr = cloudpickle.dumps(def_, protocol=_pickle_protocol_version) + if package_requirements: + for p in sorted(package_requirements): + def_repr += p.encode() return hashlib.md5(def_repr).hexdigest() @@ -129,18 +132,18 @@ class IbisSignature(NamedTuple): output_type: IbisDataType -def get_cloud_function_name(def_, uniq_suffix=None): +def get_cloud_function_name(def_, uniq_suffix=None, package_requirements=None): "Get a name for the cloud function for the given user defined function." - cf_name = _get_hash(def_) + cf_name = _get_hash(def_, package_requirements) cf_name = f"bigframes-{cf_name}" # for identification if uniq_suffix: cf_name = f"{cf_name}-{uniq_suffix}" return cf_name -def get_remote_function_name(def_, uniq_suffix=None): +def get_remote_function_name(def_, uniq_suffix=None, package_requirements=None): "Get a name for the BQ remote function for the given user defined function." - bq_rf_name = _get_hash(def_) + bq_rf_name = _get_hash(def_, package_requirements) bq_rf_name = f"bigframes_{bq_rf_name}" # for identification if uniq_suffix: bq_rf_name = f"{bq_rf_name}_{uniq_suffix}" @@ -200,7 +203,8 @@ def create_bq_remote_function( RETURNS {bq_function_return_type} REMOTE WITH CONNECTION `{self._gcp_project_id}.{self._bq_location}.{self._bq_connection_id}` OPTIONS ( - endpoint = "{endpoint}" + endpoint = "{endpoint}", + max_batching_rows = 1000 )""" logger.info(f"Creating BQ remote function: {create_function_ddl}") @@ -320,11 +324,14 @@ def {handler_func_name}(request): return handler_func_name - def generate_cloud_function_code(self, def_, dir): + def generate_cloud_function_code(self, def_, dir, package_requirements=None): """Generate the cloud function code for a given user defined function.""" # requirements.txt requirements = ["cloudpickle >= 2.1.0"] + if package_requirements: + requirements.extend(package_requirements) + requirements = sorted(requirements) requirements_txt = os.path.join(dir, "requirements.txt") with open(requirements_txt, "w") as f: f.write("\n".join(requirements)) @@ -333,12 +340,14 @@ def generate_cloud_function_code(self, def_, dir): entry_point = self.generate_cloud_function_main_code(def_, dir) return entry_point - def create_cloud_function(self, def_, cf_name): + def create_cloud_function(self, def_, cf_name, package_requirements=None): """Create a cloud function from the given user defined function.""" # Build and deploy folder structure containing cloud function with tempfile.TemporaryDirectory() as dir: - entry_point = self.generate_cloud_function_code(def_, dir) + entry_point = self.generate_cloud_function_code( + def_, dir, package_requirements + ) archive_path = shutil.make_archive(dir, "zip", dir) # We are creating cloud function source code from the currently running @@ -392,6 +401,9 @@ def create_cloud_function(self, def_, cf_name): function.build_config.source.storage_source.object_ = ( upload_url_response.storage_source.object_ ) + function.service_config = functions_v2.ServiceConfig() + function.service_config.available_memory = "1024M" + function.service_config.timeout_seconds = 600 create_function_request.function = function # Create the cloud function and wait for it to be ready to use @@ -422,6 +434,7 @@ def provision_bq_remote_function( output_type, reuse, name, + package_requirements, ): """Provision a BigQuery remote function.""" # If reuse of any existing function with the same name (indicated by the @@ -435,19 +448,25 @@ def provision_bq_remote_function( # Derive the name of the cloud function underlying the intended BQ # remote function - cloud_function_name = get_cloud_function_name(def_, uniq_suffix) + cloud_function_name = get_cloud_function_name( + def_, uniq_suffix, package_requirements + ) cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) # Create the cloud function if it does not exist if not cf_endpoint: - cf_endpoint = self.create_cloud_function(def_, cloud_function_name) + cf_endpoint = self.create_cloud_function( + def_, cloud_function_name, package_requirements + ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") # Derive the name of the remote function remote_function_name = name if not remote_function_name: - remote_function_name = get_remote_function_name(def_, uniq_suffix) + remote_function_name = get_remote_function_name( + def_, uniq_suffix, package_requirements + ) rf_endpoint, rf_conn = self.get_remote_function_specs(remote_function_name) # Create the BQ remote function in following circumstances: @@ -619,6 +638,7 @@ def remote_function( bigquery_connection: Optional[str] = None, reuse: bool = True, name: Optional[str] = None, + packages: Optional[Sequence[str]] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -710,6 +730,10 @@ def remote_function( caution, because two users working in the same project and dataset could overwrite each other's remote functions if they use the same persistent name. + packages (str[], Optional): + Explicit name of the external package dependencies. Each dependency + is added to the `requirements.txt` as is, and can be of the form + supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. """ import bigframes.pandas as bpd @@ -821,6 +845,7 @@ def wrapper(f): ibis_signature.output_type, reuse, name, + packages, ) node = remote_function_node(dataset_ref.routine(rf_name), ibis_signature) diff --git a/bigframes/session.py b/bigframes/session.py index 6c1160c88e6..fa5b4153507 100644 --- a/bigframes/session.py +++ b/bigframes/session.py @@ -1413,6 +1413,7 @@ def remote_function( bigquery_connection: Optional[str] = None, reuse: bool = True, name: Optional[str] = None, + packages: Optional[Sequence[str]] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1467,7 +1468,7 @@ def remote_function( Name of the BigQuery connection. You should either have the connection already created in the `location` you have chosen, or you should have the Project IAM Admin role to enable the service - to create the connection for you if you need it.If this parameter is + to create the connection for you if you need it. If this parameter is not provided then the BigQuery connection from the session is used. reuse (bool, Optional): Reuse the remote function if already exists. @@ -1482,6 +1483,10 @@ def remote_function( caution, because two users working in the same project and dataset could overwrite each other's remote functions if they use the same persistent name. + packages (str[], Optional): + Explicit name of the external package dependencies. Each dependency + is added to the `requirements.txt` as is, and can be of the form + supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1499,6 +1504,7 @@ def remote_function( bigquery_connection=bigquery_connection, reuse=reuse, name=name, + packages=packages, ) def read_gbq_function( diff --git a/samples/snippets/remote_function.py b/samples/snippets/remote_function.py index 9998a23eb2c..646d7b0c307 100644 --- a/samples/snippets/remote_function.py +++ b/samples/snippets/remote_function.py @@ -89,19 +89,25 @@ def get_bucket(num): # say we consider the `species`, `island` and `sex` of the penguins # sensitive information and want to redact that by replacing with their hash # code instead. Let's define another scalar custom function and decorate it - # as a remote function + # as a remote function. The custom function in this example has external + # package dependency, which can be specified via `packages` parameter. @bpd.remote_function( - [str], str, bigquery_connection="bigframes-rf-conn", reuse=False + [str], + str, + bigquery_connection="bigframes-rf-conn", + reuse=False, + packages=["cryptography"], ) def get_hash(input): - import hashlib + from cryptography.fernet import Fernet # handle missing value if input is None: input = "" - encoded_input = input.encode() - hash = hashlib.md5(encoded_input) - return hash.hexdigest() + + key = Fernet.generate_key() + f = Fernet(key) + return f.encrypt(input.encode()).decode() # We can use this remote function in another `pandas`-like API `map` that # can be applied on a DataFrame diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index f2700991828..730a1dbde4e 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -916,6 +916,51 @@ def square(x): ) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_with_external_package_dependencies( + session, scalars_dfs, dataset_id, bq_cf_connection, functions_client +): + try: + + def pd_np_foo(x): + import numpy as mynp + import pandas as mypd + + return mypd.Series([x, mynp.sqrt(mynp.abs(x))]).sum() + + # Create the remote function with the name provided explicitly + pd_np_foo_remote = session.remote_function( + [int], + float, + dataset_id, + bq_cf_connection, + reuse=False, + packages=["numpy", "pandas >= 2.0.0"], + )(pd_np_foo) + + # The behavior of the created remote function should be as expected + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_too"] + bf_result_col = bf_int64_col.apply(pd_np_foo_remote) + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_too"] + pd_result_col = pd_int64_col.apply(pd_np_foo) + pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) + + # pandas result is non-nullable type float64, make it Float64 before + # comparing for the purpose of this test + pd_result.result = pd_result.result.astype(pandas.Float64Dtype()) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, functions_client, pd_np_foo_remote + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_with_explicit_name_reuse( session, scalars_dfs, dataset_id, bq_cf_connection, functions_client From a6dab9cdb7dd0e56c93ca96b665ab1be1baac5e5 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 17 Oct 2023 17:28:19 -0500 Subject: [PATCH 084/921] docs: add documentation for `Series.struct.field` and `Series.struct.explode` (#114) --- bigframes/core/indexers.py | 2 +- bigframes/operations/structs.py | 4 ++-- docs/reference/bigframes.pandas/series.rst | 8 +++++++ docs/templates/toc.yml | 2 ++ .../bigframes_vendored/pandas/core/series.py | 21 +++++++++++++++++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 01c1162ae6d..f4c4f9011e0 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -225,7 +225,7 @@ def __getitem__(self, key: tuple) -> bigframes.core.scalar.Scalar: raise ValueError(error_message) if len(key) != 2: raise TypeError(error_message) - block: bigframes.core.blocks.Block = self._dataframe._block + block: bigframes.core.blocks.Block = self._dataframe._block # type: ignore column_block = block.select_columns([block.value_columns[key[1]]]) column = bigframes.series.Series(column_block) return column.iloc[key[0]] diff --git a/bigframes/operations/structs.py b/bigframes/operations/structs.py index 80d51115d01..506a5577094 100644 --- a/bigframes/operations/structs.py +++ b/bigframes/operations/structs.py @@ -25,7 +25,7 @@ import third_party.bigframes_vendored.pandas.core.arrays.arrow.accessors as vendoracessors -class StructField(bigframes.operations.UnaryOp): +class _StructField(bigframes.operations.UnaryOp): def __init__(self, name_or_index: str | int): self._name_or_index = name_or_index @@ -44,7 +44,7 @@ class StructAccessor( __doc__ = vendoracessors.StructAccessor.__doc__ def field(self, name_or_index: str | int) -> bigframes.series.Series: - series = self._apply_unary_op(StructField(name_or_index)) + series = self._apply_unary_op(_StructField(name_or_index)) if isinstance(name_or_index, str): name = name_or_index else: diff --git a/docs/reference/bigframes.pandas/series.rst b/docs/reference/bigframes.pandas/series.rst index b179da9ca13..e212904f3ff 100644 --- a/docs/reference/bigframes.pandas/series.rst +++ b/docs/reference/bigframes.pandas/series.rst @@ -34,3 +34,11 @@ String handling :members: :inherited-members: :undoc-members: + +Struct handling +^^^^^^^^^^^^^^^ + +.. automodule:: bigframes.operations.structs + :members: + :inherited-members: + :undoc-members: diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 0758bb41d8a..4fe2ec1a6ad 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -39,6 +39,8 @@ uid: bigframes.operations.datetimes.DatetimeMethods - name: StringMethods uid: bigframes.operations.strings.StringMethods + - name: StructAccessor + uid: bigframes.operations.structs.StructAccessor name: Series - name: Window uid: bigframes.core.window.Window diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index ec3c07bfa85..bd1f9a9a185 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -22,6 +22,23 @@ class Series(NDFrame): # type: ignore[misc] def dt(self): """ Accessor object for datetime-like properties of the Series values. + + Returns: + bigframes.operations.datetimes.DatetimeMethods: + An accessor containing datetime methods. + + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def struct(self): + """ + Accessor object for struct properties of the Series values. + + Returns: + bigframes.operations.structs.StructAccessor: + An accessor containing struct methods. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1733,6 +1750,10 @@ def str(self): NAs stay NA unless handled otherwise by a particular method. Patterned after Python’s string methods, with some inspiration from R’s stringr package. + + Returns: + bigframes.operations.strings.StringMethods: + An accessor containing string methods. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 52d64ffdbbab16b1d94974b543ce9080be1ec0d1 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 17 Oct 2023 18:20:17 -0500 Subject: [PATCH 085/921] feat: add `bigframes.options.bigquery.application_name` for partner attribution (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because `session.py` was getting long, this also refactors `session.py` to separate client construction in a separate module. Fixes internal issue 305950924 🦕 --- bigframes/_config/bigquery_options.py | 19 ++ bigframes/pandas/__init__.py | 4 +- bigframes/{session.py => session/__init__.py} | 170 +-------------- bigframes/session/clients.py | 196 ++++++++++++++++++ tests/unit/_config/test_bigquery_options.py | 4 + tests/unit/resources.py | 3 +- tests/unit/session/__init__.py | 13 ++ tests/unit/session/test_clients.py | 114 ++++++++++ tests/unit/{ => session}/test_session.py | 2 +- 9 files changed, 360 insertions(+), 165 deletions(-) rename bigframes/{session.py => session/__init__.py} (90%) create mode 100644 bigframes/session/clients.py create mode 100644 tests/unit/session/__init__.py create mode 100644 tests/unit/session/test_clients.py rename tests/unit/{ => session}/test_session.py (98%) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 453e7f5dff4..d0cce9492bb 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -37,14 +37,33 @@ def __init__( location: Optional[str] = None, bq_connection: Optional[str] = None, use_regional_endpoints: bool = False, + application_name: Optional[str] = None, ): self._credentials = credentials self._project = project self._location = location self._bq_connection = bq_connection self._use_regional_endpoints = use_regional_endpoints + self._application_name = application_name self._session_started = False + @property + def application_name(self) -> Optional[str]: + """The application name to amend to the user-agent sent to Google APIs. + + Recommended format is ``"appplication-name/major.minor.patch_version"`` + or ``"(gpn:PartnerName;)"`` for official Google partners. + """ + return self._application_name + + @application_name.setter + def application_name(self, value: Optional[str]): + if self._session_started and self._application_name != value: + raise ValueError( + SESSION_STARTED_MESSAGE.format(attribute="application_name") + ) + self._application_name = value + @property def credentials(self) -> Optional[google.auth.credentials.Credentials]: """The OAuth2 Credentials to use for this client.""" diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 833f0d1d1dd..1b9144fb625 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -51,6 +51,7 @@ import bigframes.dataframe import bigframes.series import bigframes.session +import bigframes.session.clients import third_party.bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat import third_party.bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile @@ -180,11 +181,12 @@ def _set_default_session_location_if_possible(query): ): return - clients_provider = bigframes.session.ClientsProvider( + clients_provider = bigframes.session.clients.ClientsProvider( project=options.bigquery.project, location=options.bigquery.location, use_regional_endpoints=options.bigquery.use_regional_endpoints, credentials=options.bigquery.credentials, + application_name=options.bigquery.application_name, ) bqclient = clients_provider.bqclient diff --git a/bigframes/session.py b/bigframes/session/__init__.py similarity index 90% rename from bigframes/session.py rename to bigframes/session/__init__.py index fa5b4153507..1031fde9b5b 100644 --- a/bigframes/session.py +++ b/bigframes/session/__init__.py @@ -61,7 +61,6 @@ ReadPickleBuffer, StorageOptions, ) -import pydata_google_auth import bigframes._config.bigquery_options as bigquery_options import bigframes.constants as constants @@ -75,6 +74,7 @@ import bigframes.formatting_helpers as formatting_helpers from bigframes.remote_function import read_gbq_function as bigframes_rgf from bigframes.remote_function import remote_function as bigframes_rf +import bigframes.session.clients import bigframes.version # Even though the ibis.backends.bigquery.registry import is unused, it's needed @@ -85,18 +85,6 @@ import third_party.bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers import third_party.bigframes_vendored.pandas.io.pickle as third_party_pandas_pickle -_ENV_DEFAULT_PROJECT = "GOOGLE_CLOUD_PROJECT" -_APPLICATION_NAME = f"bigframes/{bigframes.version.__version__}" -_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] - -# BigQuery is a REST API, which requires the protocol as part of the URL. -_BIGQUERY_REGIONAL_ENDPOINT = "https://{location}-bigquery.googleapis.com" - -# BigQuery Connection and Storage are gRPC APIs, which don't support the -# https:// protocol in the API endpoint URL. -_BIGQUERYCONNECTION_REGIONAL_ENDPOINT = "{location}-bigqueryconnection.googleapis.com" -_BIGQUERYSTORAGE_REGIONAL_ENDPOINT = "{location}-bigquerystorage.googleapis.com" - _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" _MAX_CLUSTER_COLUMNS = 4 @@ -122,149 +110,6 @@ def _is_query(query_or_table: str) -> bool: return re.search(r"\s", query_or_table.strip(), re.MULTILINE) is not None -def _get_default_credentials_with_project(): - return pydata_google_auth.default(scopes=_SCOPES, use_local_webserver=False) - - -class ClientsProvider: - """Provides client instances necessary to perform cloud operations.""" - - def __init__( - self, - project: Optional[str], - location: Optional[str], - use_regional_endpoints: Optional[bool], - credentials: Optional[google.auth.credentials.Credentials], - ): - credentials_project = None - if credentials is None: - credentials, credentials_project = _get_default_credentials_with_project() - - # Prefer the project in this order: - # 1. Project explicitly specified by the user - # 2. Project set in the environment - # 3. Project associated with the default credentials - project = ( - project - or os.getenv(_ENV_DEFAULT_PROJECT) - or typing.cast(Optional[str], credentials_project) - ) - - if not project: - raise ValueError( - "Project must be set to initialize BigQuery client. " - "Try setting `bigframes.options.bigquery.project` first." - ) - - self._project = project - self._location = location - self._use_regional_endpoints = use_regional_endpoints - self._credentials = credentials - - # cloud clients initialized for lazy load - self._bqclient = None - self._bqconnectionclient = None - self._bqstorageclient = None - self._cloudfunctionsclient = None - self._resourcemanagerclient = None - - @property - def bqclient(self): - if not self._bqclient: - bq_options = None - if self._use_regional_endpoints: - bq_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERY_REGIONAL_ENDPOINT.format( - location=self._location - ), - ) - bq_info = google.api_core.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - self._bqclient = bigquery.Client( - client_info=bq_info, - client_options=bq_options, - credentials=self._credentials, - project=self._project, - location=self._location, - ) - - return self._bqclient - - @property - def bqconnectionclient(self): - if not self._bqconnectionclient: - bqconnection_options = None - if self._use_regional_endpoints: - bqconnection_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERYCONNECTION_REGIONAL_ENDPOINT.format( - location=self._location - ) - ) - bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - self._bqconnectionclient = ( - google.cloud.bigquery_connection_v1.ConnectionServiceClient( - client_info=bqconnection_info, - client_options=bqconnection_options, - credentials=self._credentials, - ) - ) - - return self._bqconnectionclient - - @property - def bqstorageclient(self): - if not self._bqstorageclient: - bqstorage_options = None - if self._use_regional_endpoints: - bqstorage_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERYSTORAGE_REGIONAL_ENDPOINT.format( - location=self._location - ) - ) - bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - self._bqstorageclient = google.cloud.bigquery_storage_v1.BigQueryReadClient( - client_info=bqstorage_info, - client_options=bqstorage_options, - credentials=self._credentials, - ) - - return self._bqstorageclient - - @property - def cloudfunctionsclient(self): - if not self._cloudfunctionsclient: - functions_info = google.api_core.gapic_v1.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - self._cloudfunctionsclient = ( - google.cloud.functions_v2.FunctionServiceClient( - client_info=functions_info, - credentials=self._credentials, - ) - ) - - return self._cloudfunctionsclient - - @property - def resourcemanagerclient(self): - if not self._resourcemanagerclient: - resourcemanager_info = google.api_core.gapic_v1.client_info.ClientInfo( - user_agent=_APPLICATION_NAME - ) - self._resourcemanagerclient = ( - google.cloud.resourcemanager_v3.ProjectsClient( - credentials=self._credentials, client_info=resourcemanager_info - ) - ) - - return self._resourcemanagerclient - - class Session( third_party_pandas_gbq.GBQIOMixin, third_party_pandas_parquet.ParquetIOMixin, @@ -279,14 +124,14 @@ class Session( Configuration adjusting how to connect to BigQuery and related APIs. Note that some options are ignored if ``clients_provider`` is set. - clients_provider (bigframes.session.ClientsProvider): + clients_provider (bigframes.session.bigframes.session.clients.ClientsProvider): An object providing client library objects. """ def __init__( self, context: Optional[bigquery_options.BigQueryOptions] = None, - clients_provider: Optional[ClientsProvider] = None, + clients_provider: Optional[bigframes.session.clients.ClientsProvider] = None, ): if context is None: context = bigquery_options.BigQueryOptions() @@ -306,11 +151,12 @@ def __init__( if clients_provider: self._clients_provider = clients_provider else: - self._clients_provider = ClientsProvider( + self._clients_provider = bigframes.session.clients.ClientsProvider( project=context.project, location=self._location, use_regional_endpoints=context.use_regional_endpoints, credentials=context.credentials, + application_name=context.application_name, ) self._create_and_bind_bq_session() @@ -319,7 +165,7 @@ def __init__( ibis.bigquery.connect( project_id=context.project, client=self.bqclient, - storage_client=self.bqstorageclient, + storage_client=self.bqstoragereadclient, ), ) @@ -338,8 +184,8 @@ def bqconnectionclient(self): return self._clients_provider.bqconnectionclient @property - def bqstorageclient(self): - return self._clients_provider.bqstorageclient + def bqstoragereadclient(self): + return self._clients_provider.bqstoragereadclient @property def cloudfunctionsclient(self): diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py new file mode 100644 index 00000000000..544f74265fe --- /dev/null +++ b/bigframes/session/clients.py @@ -0,0 +1,196 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Clients manages the connection to Google APIs.""" + +import os +import typing +from typing import Optional + +import google.api_core.client_info +import google.api_core.client_options +import google.api_core.exceptions +import google.api_core.gapic_v1.client_info +import google.auth.credentials +import google.cloud.bigquery as bigquery +import google.cloud.bigquery_connection_v1 +import google.cloud.bigquery_storage_v1 +import google.cloud.functions_v2 +import google.cloud.resourcemanager_v3 +import pydata_google_auth + +import bigframes.version + +_ENV_DEFAULT_PROJECT = "GOOGLE_CLOUD_PROJECT" +_APPLICATION_NAME = f"bigframes/{bigframes.version.__version__}" +_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] + +# BigQuery is a REST API, which requires the protocol as part of the URL. +_BIGQUERY_REGIONAL_ENDPOINT = "https://{location}-bigquery.googleapis.com" + +# BigQuery Connection and Storage are gRPC APIs, which don't support the +# https:// protocol in the API endpoint URL. +_BIGQUERYCONNECTION_REGIONAL_ENDPOINT = "{location}-bigqueryconnection.googleapis.com" +_BIGQUERYSTORAGE_REGIONAL_ENDPOINT = "{location}-bigquerystorage.googleapis.com" + + +def _get_default_credentials_with_project(): + return pydata_google_auth.default(scopes=_SCOPES, use_local_webserver=False) + + +class ClientsProvider: + """Provides client instances necessary to perform cloud operations.""" + + def __init__( + self, + project: Optional[str], + location: Optional[str], + use_regional_endpoints: Optional[bool], + credentials: Optional[google.auth.credentials.Credentials], + application_name: Optional[str], + ): + credentials_project = None + if credentials is None: + credentials, credentials_project = _get_default_credentials_with_project() + + # Prefer the project in this order: + # 1. Project explicitly specified by the user + # 2. Project set in the environment + # 3. Project associated with the default credentials + project = ( + project + or os.getenv(_ENV_DEFAULT_PROJECT) + or typing.cast(Optional[str], credentials_project) + ) + + if not project: + raise ValueError( + "Project must be set to initialize BigQuery client. " + "Try setting `bigframes.options.bigquery.project` first." + ) + + self._application_name = ( + f"{_APPLICATION_NAME} {application_name}" + if application_name + else _APPLICATION_NAME + ) + self._project = project + self._location = location + self._use_regional_endpoints = use_regional_endpoints + self._credentials = credentials + + # cloud clients initialized for lazy load + self._bqclient = None + self._bqconnectionclient = None + self._bqstoragereadclient = None + self._cloudfunctionsclient = None + self._resourcemanagerclient = None + + @property + def bqclient(self): + if not self._bqclient: + bq_options = None + if self._use_regional_endpoints: + bq_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERY_REGIONAL_ENDPOINT.format( + location=self._location + ), + ) + bq_info = google.api_core.client_info.ClientInfo( + user_agent=self._application_name + ) + self._bqclient = bigquery.Client( + client_info=bq_info, + client_options=bq_options, + credentials=self._credentials, + project=self._project, + location=self._location, + ) + + return self._bqclient + + @property + def bqconnectionclient(self): + if not self._bqconnectionclient: + bqconnection_options = None + if self._use_regional_endpoints: + bqconnection_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERYCONNECTION_REGIONAL_ENDPOINT.format( + location=self._location + ) + ) + bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=self._application_name + ) + self._bqconnectionclient = ( + google.cloud.bigquery_connection_v1.ConnectionServiceClient( + client_info=bqconnection_info, + client_options=bqconnection_options, + credentials=self._credentials, + ) + ) + + return self._bqconnectionclient + + @property + def bqstoragereadclient(self): + if not self._bqstoragereadclient: + bqstorage_options = None + if self._use_regional_endpoints: + bqstorage_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERYSTORAGE_REGIONAL_ENDPOINT.format( + location=self._location + ) + ) + bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=self._application_name + ) + self._bqstoragereadclient = ( + google.cloud.bigquery_storage_v1.BigQueryReadClient( + client_info=bqstorage_info, + client_options=bqstorage_options, + credentials=self._credentials, + ) + ) + + return self._bqstoragereadclient + + @property + def cloudfunctionsclient(self): + if not self._cloudfunctionsclient: + functions_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=self._application_name + ) + self._cloudfunctionsclient = ( + google.cloud.functions_v2.FunctionServiceClient( + client_info=functions_info, + credentials=self._credentials, + ) + ) + + return self._cloudfunctionsclient + + @property + def resourcemanagerclient(self): + if not self._resourcemanagerclient: + resourcemanager_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=self._application_name + ) + self._resourcemanagerclient = ( + google.cloud.resourcemanager_v3.ProjectsClient( + credentials=self._credentials, client_info=resourcemanager_info + ) + ) + + return self._resourcemanagerclient diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index aeee0583195..e5b6cfe2f1b 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -22,11 +22,13 @@ @pytest.mark.parametrize( ["attribute", "original_value", "new_value"], [ + ("application_name", None, "test-partner"), # For credentials, the match is by reference. ("credentials", object(), object()), ("location", "us-east1", "us-central1"), ("project", "my-project", "my-other-project"), ("bq_connection", "path/to/connection/1", "path/to/connection/2"), + ("use_regional_endpoints", False, True), ], ) def test_setter_raises_if_session_started(attribute, original_value, new_value): @@ -53,10 +55,12 @@ def test_setter_raises_if_session_started(attribute, original_value, new_value): [ (attribute,) for attribute in [ + "application_name", "credentials", "location", "project", "bq_connection", + "use_regional_endpoints", ] ], ) diff --git a/tests/unit/resources.py b/tests/unit/resources.py index c8ed6e86ed6..0a68600a359 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -22,6 +22,7 @@ import bigframes import bigframes.core as core +import bigframes.session.clients """Utilities for creating test resources.""" @@ -37,7 +38,7 @@ def create_bigquery_session( bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" - clients_provider = mock.create_autospec(bigframes.session.ClientsProvider) + clients_provider = mock.create_autospec(bigframes.session.clients.ClientsProvider) type(clients_provider).bqclient = mock.PropertyMock(return_value=bqclient) clients_provider._credentials = credentials diff --git a/tests/unit/session/__init__.py b/tests/unit/session/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/tests/unit/session/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/session/test_clients.py b/tests/unit/session/test_clients.py new file mode 100644 index 00000000000..f1b2a5045a3 --- /dev/null +++ b/tests/unit/session/test_clients.py @@ -0,0 +1,114 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional +import unittest.mock as mock + +import google.api_core.client_info +import google.api_core.client_options +import google.api_core.exceptions +import google.api_core.gapic_v1.client_info +import google.auth.credentials +import google.cloud.bigquery +import google.cloud.bigquery_connection_v1 +import google.cloud.bigquery_storage_v1 +import google.cloud.functions_v2 +import google.cloud.resourcemanager_v3 + +import bigframes.session.clients as clients +import bigframes.version + + +def create_clients_provider(application_name: Optional[str] = None): + credentials = mock.create_autospec(google.auth.credentials.Credentials) + return clients.ClientsProvider( + project="test-project", + location="test-region", + use_regional_endpoints=False, + credentials=credentials, + application_name=application_name, + ) + + +def monkeypatch_client_constructors(monkeypatch): + bqclient = mock.create_autospec(google.cloud.bigquery.Client) + bqclient.return_value = bqclient + monkeypatch.setattr(google.cloud.bigquery, "Client", bqclient) + + bqconnectionclient = mock.create_autospec( + google.cloud.bigquery_connection_v1.ConnectionServiceClient + ) + bqconnectionclient.return_value = bqconnectionclient + monkeypatch.setattr( + google.cloud.bigquery_connection_v1, + "ConnectionServiceClient", + bqconnectionclient, + ) + + bqstoragereadclient = mock.create_autospec( + google.cloud.bigquery_storage_v1.BigQueryReadClient + ) + bqstoragereadclient.return_value = bqstoragereadclient + monkeypatch.setattr( + google.cloud.bigquery_storage_v1, "BigQueryReadClient", bqstoragereadclient + ) + + cloudfunctionsclient = mock.create_autospec( + google.cloud.functions_v2.FunctionServiceClient + ) + cloudfunctionsclient.return_value = cloudfunctionsclient + monkeypatch.setattr( + google.cloud.functions_v2, "FunctionServiceClient", cloudfunctionsclient + ) + + resourcemanagerclient = mock.create_autospec( + google.cloud.resourcemanager_v3.ProjectsClient + ) + resourcemanagerclient.return_value = resourcemanagerclient + monkeypatch.setattr( + google.cloud.resourcemanager_v3, "ProjectsClient", resourcemanagerclient + ) + + +def assert_constructed_w_user_agent(mock_client: mock.Mock, expected_user_agent: str): + assert ( + expected_user_agent + in mock_client.call_args.kwargs["client_info"].to_user_agent() + ) + + +def assert_clients_w_user_agent( + provider: clients.ClientsProvider, expected_user_agent: str +): + assert_constructed_w_user_agent(provider.bqclient, expected_user_agent) + assert_constructed_w_user_agent(provider.bqconnectionclient, expected_user_agent) + assert_constructed_w_user_agent(provider.bqstoragereadclient, expected_user_agent) + assert_constructed_w_user_agent(provider.cloudfunctionsclient, expected_user_agent) + assert_constructed_w_user_agent(provider.resourcemanagerclient, expected_user_agent) + + +def test_user_agent_default(monkeypatch): + monkeypatch_client_constructors(monkeypatch) + provider = create_clients_provider(application_name=None) + assert_clients_w_user_agent(provider, f"bigframes/{bigframes.version.__version__}") + + +def test_user_agent_custom(monkeypatch): + monkeypatch_client_constructors(monkeypatch) + provider = create_clients_provider(application_name="(gpn:testpartner;)") + assert_clients_w_user_agent(provider, "(gpn:testpartner;)") + + # We still need to include attribution to bigframes, even if there's also a + # partner using the package. + assert_clients_w_user_agent(provider, f"bigframes/{bigframes.version.__version__}") diff --git a/tests/unit/test_session.py b/tests/unit/session/test_session.py similarity index 98% rename from tests/unit/test_session.py rename to tests/unit/session/test_session.py index e39a316e5b6..18fd42e0f31 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/session/test_session.py @@ -20,7 +20,7 @@ import bigframes -from . import resources +from .. import resources @pytest.mark.parametrize("missing_parts_table_id", [(""), ("table")]) From 68e3cd37258084d045ea1075e5e61df12c28faac Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 17 Oct 2023 18:36:28 -0700 Subject: [PATCH 086/921] fix: fix TODOs for loc multiindex (#113) Co-authored-by: Henry J Solberg --- bigframes/core/indexers.py | 59 ++++++++++++++++++---------- tests/system/small/test_dataframe.py | 34 ++++++++++++++++ tests/system/small/test_series.py | 34 ++++++++++++++++ 3 files changed, 107 insertions(+), 20 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index f4c4f9011e0..4f5a9471b99 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -15,7 +15,7 @@ from __future__ import annotations import typing -from typing import Tuple, Union +from typing import List, Tuple, Union import ibis import pandas as pd @@ -271,40 +271,59 @@ def _loc_getitem_series_or_dataframe( if isinstance(key, bigframes.series.Series) and key.dtype == "boolean": return series_or_dataframe[key] elif isinstance(key, bigframes.series.Series): - # TODO(henryjsolberg): support MultiIndex temp_name = guid.generate_guid(prefix="temp_series_name_") + if len(series_or_dataframe.index.names) > 1: + temp_name = series_or_dataframe.index.names[0] key = key.rename(temp_name) keys_df = key.to_frame() keys_df = keys_df.set_index(temp_name, drop=True) return _perform_loc_list_join(series_or_dataframe, keys_df) elif isinstance(key, bigframes.core.indexes.Index): - # TODO(henryjsolberg): support MultiIndex block = key._data._get_block() block = block.select_columns(()) keys_df = bigframes.dataframe.DataFrame(block) return _perform_loc_list_join(series_or_dataframe, keys_df) elif pd.api.types.is_list_like(key): - # TODO(henryjsolberg): support MultiIndex - if len(key) == 0: # type: ignore + key = typing.cast(List, key) + if len(key) == 0: return typing.cast( Union[bigframes.dataframe.DataFrame, bigframes.series.Series], series_or_dataframe.iloc[0:0], ) - - # We can't upload a DataFrame with None as the column name, so set it - # an arbitrary string. - index_name = series_or_dataframe.index.name - index_name_is_none = index_name is None - if index_name_is_none: - index_name = "unnamed_col" - - keys_df = bigframes.dataframe.DataFrame( - {index_name: key}, session=series_or_dataframe._get_block().expr._session - ) - keys_df = keys_df.set_index(index_name, drop=True) - - if index_name_is_none: - keys_df.index.name = None + if pd.api.types.is_list_like(key[0]): + original_index_names = series_or_dataframe.index.names + num_index_cols = len(original_index_names) + + entry_col_count_correct = [len(entry) == num_index_cols for entry in key] + if not all(entry_col_count_correct): + # pandas usually throws TypeError in these cases- tuple causes IndexError, but that + # seems like unintended behavior + raise TypeError( + "All entries must be of equal length when indexing by list of listlikes" + ) + temporary_index_names = [ + guid.generate_guid(prefix="temp_loc_index_") + for _ in range(len(original_index_names)) + ] + index_cols_dict = {} + for i in range(num_index_cols): + index_name = temporary_index_names[i] + values = [entry[i] for entry in key] + index_cols_dict[index_name] = values + keys_df = bigframes.dataframe.DataFrame(index_cols_dict) + keys_df = keys_df.set_index(temporary_index_names, drop=True) + keys_df = keys_df.rename_axis(original_index_names) + else: + # We can't upload a DataFrame with None as the column name, so set it + # an arbitrary string. + index_name = series_or_dataframe.index.name + index_name_is_none = index_name is None + if index_name_is_none: + index_name = "unnamed_col" + keys_df = bigframes.dataframe.DataFrame({index_name: key}) + keys_df = keys_df.set_index(index_name, drop=True) + if index_name_is_none: + keys_df.index.name = None return _perform_loc_list_join(series_or_dataframe, keys_df) elif isinstance(key, slice): if (key.start is None) and (key.stop is None) and (key.step is None): diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ecafb7c1bf5..309e8df4f0d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2787,6 +2787,22 @@ def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): ) +def test_loc_list_multiindex(scalars_df_index, scalars_pandas_df_index): + scalars_df_multiindex = scalars_df_index.set_index(["string_col", "int64_col"]) + scalars_pandas_df_multiindex = scalars_pandas_df_index.set_index( + ["string_col", "int64_col"] + ) + index_list = [("Hello, World!", -234892), ("Hello, World!", 123456789)] + + bf_result = scalars_df_multiindex.loc[index_list] + pd_result = scalars_pandas_df_multiindex.loc[index_list] + + pd.testing.assert_frame_equal( + bf_result.to_pandas(), + pd_result, + ) + + def test_iloc_list(scalars_df_index, scalars_pandas_df_index): index_list = [0, 0, 0, 5, 4, 7] @@ -2863,6 +2879,24 @@ def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): ) +def test_loc_bf_series_multiindex(scalars_df_index, scalars_pandas_df_index): + pd_string_series = scalars_pandas_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + bf_string_series = scalars_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + + scalars_df_multiindex = scalars_df_index.set_index(["string_col", "int64_col"]) + scalars_pandas_df_multiindex = scalars_pandas_df_index.set_index( + ["string_col", "int64_col"] + ) + + bf_result = scalars_df_multiindex.loc[bf_string_series] + pd_result = scalars_pandas_df_multiindex.loc[pd_string_series] + + pd.testing.assert_frame_equal( + bf_result.to_pandas(), + pd_result, + ) + + def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): pd_index = scalars_pandas_df_index.iloc[[0, 5, 1, 1, 5]].index bf_index = scalars_df_index.iloc[[0, 5, 1, 1, 5]].index diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index f2ced841daf..bd9edbb1ca0 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2624,6 +2624,22 @@ def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): ) +def test_loc_list_multiindex(scalars_df_index, scalars_pandas_df_index): + scalars_df_multiindex = scalars_df_index.set_index(["string_col", "int64_col"]) + scalars_pandas_df_multiindex = scalars_pandas_df_index.set_index( + ["string_col", "int64_col"] + ) + index_list = [("Hello, World!", -234892), ("Hello, World!", 123456789)] + + bf_result = scalars_df_multiindex.int64_too.loc[index_list] + pd_result = scalars_pandas_df_multiindex.int64_too.loc[index_list] + + pd.testing.assert_series_equal( + bf_result.to_pandas(), + pd_result, + ) + + def test_iloc_list(scalars_df_index, scalars_pandas_df_index): index_list = [0, 0, 0, 5, 4, 7] @@ -2681,6 +2697,24 @@ def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): ) +def test_loc_bf_series_multiindex(scalars_df_index, scalars_pandas_df_index): + pd_string_series = scalars_pandas_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + bf_string_series = scalars_df_index.string_col.iloc[[0, 5, 1, 1, 5]] + + scalars_df_multiindex = scalars_df_index.set_index(["string_col", "int64_col"]) + scalars_pandas_df_multiindex = scalars_pandas_df_index.set_index( + ["string_col", "int64_col"] + ) + + bf_result = scalars_df_multiindex.int64_too.loc[bf_string_series] + pd_result = scalars_pandas_df_multiindex.int64_too.loc[pd_string_series] + + pd.testing.assert_series_equal( + bf_result.to_pandas(), + pd_result, + ) + + def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): pd_index = scalars_pandas_df_index.iloc[[0, 5, 1, 1, 5]].index bf_index = scalars_df_index.iloc[[0, 5, 1, 1, 5]].index From 9238fadcfa7e843be6564813ff3131893b79f8b0 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 18 Oct 2023 11:48:18 -0500 Subject: [PATCH 087/921] feat: use ArrowDtype for STRUCT columns in `to_pandas` (#85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/blocks.py | 2 + bigframes/dtypes.py | 17 + noxfile.py | 2 + tests/system/small/test_dataframe_io.py | 23 +- .../google_cloud_bigquery/LICENSE | 202 +++++++++ .../google_cloud_bigquery/__init__.py | 13 + .../google_cloud_bigquery/_pandas_helpers.py | 158 +++++++ .../google_cloud_bigquery/tests/__init__.py | 13 + .../tests/unit/__init__.py | 13 + .../tests/unit/test_pandas_helpers.py | 413 ++++++++++++++++++ 10 files changed, 853 insertions(+), 3 deletions(-) create mode 100644 third_party/bigframes_vendored/google_cloud_bigquery/LICENSE create mode 100644 third_party/bigframes_vendored/google_cloud_bigquery/__init__.py create mode 100644 third_party/bigframes_vendored/google_cloud_bigquery/_pandas_helpers.py create mode 100644 third_party/bigframes_vendored/google_cloud_bigquery/tests/__init__.py create mode 100644 third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/__init__.py create mode 100644 third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 4548fca5934..8966b6189bd 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -377,7 +377,9 @@ def _to_dataframe( cls, result, schema: typing.Mapping[str, bigframes.dtypes.Dtype] ) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" + dtypes = bigframes.dtypes.to_pandas_dtypes_overrides(result.schema) df = result.to_dataframe( + dtypes=dtypes, bool_dtype=pd.BooleanDtype(), int_dtype=pd.Int64Dtype(), float_dtype=pd.Float64Dtype(), diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 46a7a1cb503..da221a95aca 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -19,6 +19,7 @@ from typing import Any, Dict, Iterable, Literal, Tuple, Union import geopandas as gpd # type: ignore +import google.cloud.bigquery as bigquery import ibis import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types @@ -27,6 +28,7 @@ import pyarrow as pa import bigframes.constants as constants +import third_party.bigframes_vendored.google_cloud_bigquery._pandas_helpers as gcb3p_pandas_helpers # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ @@ -401,3 +403,18 @@ def cast_ibis_value( raise TypeError( f"Unsupported cast {value.type()} to {to_type}. {constants.FEEDBACK_LINK}" ) + + +def to_pandas_dtypes_overrides(schema: Iterable[bigquery.SchemaField]) -> Dict: + """For each STRUCT field, make sure we specify the full type to use.""" + # TODO(swast): Also override ARRAY fields. + dtypes = {} + for field in schema: + if field.field_type == "RECORD" and field.mode != "REPEATED": + # TODO(swast): We're using a private API here. Would likely be + # better if we called `to_arrow()` and converted to a pandas + # DataFrame ourselves from that. + dtypes[field.name] = pd.ArrowDtype( + gcb3p_pandas_helpers.bq_to_arrow_data_type(field) + ) + return dtypes diff --git a/noxfile.py b/noxfile.py index 54ccdb9a877..1864da9fe7d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -185,6 +185,7 @@ def run_unit(session, install_test_extra): # Run py.test against the unit tests. tests_path = os.path.join("tests", "unit") + third_party_tests_path = os.path.join("third_party", "bigframes_vendored") session.run( "py.test", "--quiet", @@ -196,6 +197,7 @@ def run_unit(session, install_test_extra): "--cov-report=term-missing", "--cov-fail-under=0", tests_path, + third_party_tests_path, *session.posargs, ) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 3886b85f40c..d60083a837e 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -16,6 +16,7 @@ import google.api_core.exceptions import pandas as pd +import pyarrow as pa import pytest from tests.system.utils import ( @@ -44,7 +45,7 @@ def test_to_pandas_w_correct_dtypes(scalars_df_default_index): def test_to_pandas_array_struct_correct_result(session): - """In future, we should support arrays and structs with arrow types. + """In future, we should support arrays with arrow types. For now we fall back to the current connector behavior of converting to Python objects""" df = session.read_gbq( @@ -59,11 +60,27 @@ def test_to_pandas_array_struct_correct_result(session): expected = pd.DataFrame( { "array_column": [[1, 3, 2]], - "struct_column": [{"string_field": "a", "float_field": 1.2}], + "struct_column": pd.Series( + [{"string_field": "a", "float_field": 1.2}], + dtype=pd.ArrowDtype( + pa.struct( + [ + ("string_field", pa.string()), + ("float_field", pa.float64()), + ] + ) + ), + ), } ) expected.index = expected.index.astype("Int64") - pd.testing.assert_frame_equal(result, expected) + pd.testing.assert_series_equal(result.dtypes, expected.dtypes) + pd.testing.assert_series_equal(result["array_column"], expected["array_column"]) + # assert_series_equal not implemented for struct columns yet. Compare + # values as Python objects, instead. + pd.testing.assert_series_equal( + result["struct_column"].astype("O"), expected["struct_column"].astype("O") + ) @pytest.mark.parametrize( diff --git a/third_party/bigframes_vendored/google_cloud_bigquery/LICENSE b/third_party/bigframes_vendored/google_cloud_bigquery/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/third_party/bigframes_vendored/google_cloud_bigquery/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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/third_party/bigframes_vendored/google_cloud_bigquery/__init__.py b/third_party/bigframes_vendored/google_cloud_bigquery/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/third_party/bigframes_vendored/google_cloud_bigquery/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/third_party/bigframes_vendored/google_cloud_bigquery/_pandas_helpers.py b/third_party/bigframes_vendored/google_cloud_bigquery/_pandas_helpers.py new file mode 100644 index 00000000000..5e2a7a7ef0f --- /dev/null +++ b/third_party/bigframes_vendored/google_cloud_bigquery/_pandas_helpers.py @@ -0,0 +1,158 @@ +# Original: https://github.com/googleapis/python-bigquery/blob/main/google/cloud/bigquery/_pandas_helpers.py +# Copyright 2019 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. + +"""Shared helper functions for connecting BigQuery and pandas.""" + +import warnings + +import google.cloud.bigquery.schema as schema +import pyarrow + + +def pyarrow_datetime(): + return pyarrow.timestamp("us", tz=None) + + +def pyarrow_numeric(): + return pyarrow.decimal128(38, 9) + + +def pyarrow_bignumeric(): + # 77th digit is partial. + # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#decimal_types + return pyarrow.decimal256(76, 38) + + +def pyarrow_time(): + return pyarrow.time64("us") + + +def pyarrow_timestamp(): + return pyarrow.timestamp("us", tz="UTC") + + +# This dictionary is duplicated in bigquery_storage/test/unite/test_reader.py +# When modifying it be sure to update it there as well. +BQ_TO_ARROW_SCALARS = { + "BOOL": pyarrow.bool_, + "BOOLEAN": pyarrow.bool_, + "BYTES": pyarrow.binary, + "DATE": pyarrow.date32, + "DATETIME": pyarrow_datetime, + "FLOAT": pyarrow.float64, + "FLOAT64": pyarrow.float64, + "GEOGRAPHY": pyarrow.string, + "INT64": pyarrow.int64, + "INTEGER": pyarrow.int64, + "NUMERIC": pyarrow_numeric, + "STRING": pyarrow.string, + "TIME": pyarrow_time, + "TIMESTAMP": pyarrow_timestamp, + "BIGNUMERIC": pyarrow_bignumeric, +} +ARROW_SCALAR_IDS_TO_BQ = { + # https://arrow.apache.org/docs/python/api/datatypes.html#type-classes + pyarrow.bool_().id: "BOOL", + pyarrow.int8().id: "INT64", + pyarrow.int16().id: "INT64", + pyarrow.int32().id: "INT64", + pyarrow.int64().id: "INT64", + pyarrow.uint8().id: "INT64", + pyarrow.uint16().id: "INT64", + pyarrow.uint32().id: "INT64", + pyarrow.uint64().id: "INT64", + pyarrow.float16().id: "FLOAT64", + pyarrow.float32().id: "FLOAT64", + pyarrow.float64().id: "FLOAT64", + pyarrow.time32("ms").id: "TIME", + pyarrow.time64("ns").id: "TIME", + pyarrow.timestamp("ns").id: "TIMESTAMP", + pyarrow.date32().id: "DATE", + pyarrow.date64().id: "DATETIME", # because millisecond resolution + pyarrow.binary().id: "BYTES", + pyarrow.string().id: "STRING", # also alias for pyarrow.utf8() + # The exact scale and precision don't matter. Only the type ID matters, + # and it's the same for all decimal128/decimal256 instances. + pyarrow.decimal128(38, scale=9).id: "NUMERIC", + pyarrow.decimal256(76, scale=38).id: "BIGNUMERIC", +} + + +BQ_FIELD_TYPE_TO_ARROW_FIELD_METADATA = { + "GEOGRAPHY": { + b"ARROW:extension:name": b"google:sqlType:geography", + b"ARROW:extension:metadata": b'{"encoding": "WKT"}', + }, + "DATETIME": {b"ARROW:extension:name": b"google:sqlType:datetime"}, +} + + +def bq_to_arrow_struct_data_type(field): + arrow_fields = [] + for subfield in field.fields: + arrow_subfield = bq_to_arrow_field(subfield) + if arrow_subfield: + arrow_fields.append(arrow_subfield) + else: + # Could not determine a subfield type. Fallback to type + # inference. + return None + return pyarrow.struct(arrow_fields) + + +def bq_to_arrow_data_type(field): + """Return the Arrow data type, corresponding to a given BigQuery column. + + Returns: + None: if default Arrow type inspection should be used. + """ + if field.mode is not None and field.mode.upper() == "REPEATED": + inner_type = bq_to_arrow_data_type( + schema.SchemaField(field.name, field.field_type, fields=field.fields) + ) + if inner_type: + return pyarrow.list_(inner_type) + return None + + field_type_upper = field.field_type.upper() if field.field_type else "" + if field_type_upper in schema._STRUCT_TYPES: + return bq_to_arrow_struct_data_type(field) + + data_type_constructor = BQ_TO_ARROW_SCALARS.get(field_type_upper) + if data_type_constructor is None: + return None + return data_type_constructor() + + +def bq_to_arrow_field(bq_field, array_type=None): + """Return the Arrow field, corresponding to a given BigQuery column. + + Returns: + None: if the Arrow type cannot be determined. + """ + arrow_type = bq_to_arrow_data_type(bq_field) + if arrow_type is not None: + if array_type is not None: + arrow_type = array_type # For GEOGRAPHY, at least initially + is_nullable = bq_field.mode.upper() == "NULLABLE" + metadata = BQ_FIELD_TYPE_TO_ARROW_FIELD_METADATA.get( + bq_field.field_type.upper() if bq_field.field_type else "" + ) + return pyarrow.field( + bq_field.name, arrow_type, nullable=is_nullable, metadata=metadata + ) + + warnings.warn("Unable to determine type for field '{}'.".format(bq_field.name)) + return None diff --git a/third_party/bigframes_vendored/google_cloud_bigquery/tests/__init__.py b/third_party/bigframes_vendored/google_cloud_bigquery/tests/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/third_party/bigframes_vendored/google_cloud_bigquery/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/__init__.py b/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py b/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py new file mode 100644 index 00000000000..dc4a09cc541 --- /dev/null +++ b/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py @@ -0,0 +1,413 @@ +# Original: https://github.com/googleapis/python-bigquery/blob/main/tests/unit/test__pandas_helpers.py +# Copyright 2019 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 functools +import warnings + +from google.cloud.bigquery import schema +import pyarrow +import pyarrow.parquet +import pyarrow.types +import pytest + + +@pytest.fixture +def module_under_test(): + from third_party.bigframes_vendored.google_cloud_bigquery import _pandas_helpers + + return _pandas_helpers + + +def is_none(value): + return value is None + + +def is_datetime(type_): + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#datetime-type + return all_( + pyarrow.types.is_timestamp, + lambda type_: type_.unit == "us", + lambda type_: type_.tz is None, + )(type_) + + +def is_numeric(type_): + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#numeric-type + return all_( + pyarrow.types.is_decimal, + lambda type_: type_.precision == 38, + lambda type_: type_.scale == 9, + )(type_) + + +def is_bignumeric(type_): + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#numeric-type + return all_( + pyarrow.types.is_decimal, + lambda type_: type_.precision == 76, + lambda type_: type_.scale == 38, + )(type_) + + +def is_timestamp(type_): + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp-type + return all_( + pyarrow.types.is_timestamp, + lambda type_: type_.unit == "us", + lambda type_: type_.tz == "UTC", + )(type_) + + +def do_all(functions, value): + return all((func(value) for func in functions)) + + +def all_(*functions): + return functools.partial(do_all, functions) + + +def test_is_datetime(): + assert is_datetime(pyarrow.timestamp("us", tz=None)) + assert not is_datetime(pyarrow.timestamp("ms", tz=None)) + assert not is_datetime(pyarrow.timestamp("us", tz="UTC")) + assert not is_datetime(pyarrow.timestamp("ns", tz="UTC")) + assert not is_datetime(pyarrow.string()) + + +def test_do_all(): + assert do_all((lambda _: True, lambda _: True), None) + assert not do_all((lambda _: True, lambda _: False), None) + assert not do_all((lambda _: False,), None) + + +def test_all_(): + assert all_(lambda _: True, lambda _: True)(None) + assert not all_(lambda _: True, lambda _: False)(None) + + +@pytest.mark.parametrize( + "bq_type,bq_mode,is_correct_type", + [ + ("STRING", "NULLABLE", pyarrow.types.is_string), + ("STRING", None, pyarrow.types.is_string), + ("string", "NULLABLE", pyarrow.types.is_string), + ("StRiNg", "NULLABLE", pyarrow.types.is_string), + ("BYTES", "NULLABLE", pyarrow.types.is_binary), + ("INTEGER", "NULLABLE", pyarrow.types.is_int64), + ("INT64", "NULLABLE", pyarrow.types.is_int64), + ("FLOAT", "NULLABLE", pyarrow.types.is_float64), + ("FLOAT64", "NULLABLE", pyarrow.types.is_float64), + ("NUMERIC", "NULLABLE", is_numeric), + pytest.param( + "BIGNUMERIC", + "NULLABLE", + is_bignumeric, + ), + ("BOOLEAN", "NULLABLE", pyarrow.types.is_boolean), + ("BOOL", "NULLABLE", pyarrow.types.is_boolean), + ("TIMESTAMP", "NULLABLE", is_timestamp), + ("DATE", "NULLABLE", pyarrow.types.is_date32), + ("TIME", "NULLABLE", pyarrow.types.is_time64), + ("DATETIME", "NULLABLE", is_datetime), + ("GEOGRAPHY", "NULLABLE", pyarrow.types.is_string), + ("UNKNOWN_TYPE", "NULLABLE", is_none), + # Use pyarrow.list_(item_type) for repeated (array) fields. + ( + "STRING", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_string(type_.value_type), + ), + ), + ( + "STRING", + "repeated", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_string(type_.value_type), + ), + ), + ( + "STRING", + "RePeAtEd", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_string(type_.value_type), + ), + ), + ( + "BYTES", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_binary(type_.value_type), + ), + ), + ( + "INTEGER", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_int64(type_.value_type), + ), + ), + ( + "INT64", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_int64(type_.value_type), + ), + ), + ( + "FLOAT", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_float64(type_.value_type), + ), + ), + ( + "FLOAT64", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_float64(type_.value_type), + ), + ), + ( + "NUMERIC", + "REPEATED", + all_(pyarrow.types.is_list, lambda type_: is_numeric(type_.value_type)), + ), + pytest.param( + "BIGNUMERIC", + "REPEATED", + all_(pyarrow.types.is_list, lambda type_: is_bignumeric(type_.value_type)), + ), + ( + "BOOLEAN", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_boolean(type_.value_type), + ), + ), + ( + "BOOL", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_boolean(type_.value_type), + ), + ), + ( + "TIMESTAMP", + "REPEATED", + all_(pyarrow.types.is_list, lambda type_: is_timestamp(type_.value_type)), + ), + ( + "DATE", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_date32(type_.value_type), + ), + ), + ( + "TIME", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_time64(type_.value_type), + ), + ), + ( + "DATETIME", + "REPEATED", + all_(pyarrow.types.is_list, lambda type_: is_datetime(type_.value_type)), + ), + ( + "GEOGRAPHY", + "REPEATED", + all_( + pyarrow.types.is_list, + lambda type_: pyarrow.types.is_string(type_.value_type), + ), + ), + ("RECORD", "REPEATED", is_none), + ("UNKNOWN_TYPE", "REPEATED", is_none), + ], +) +def test_bq_to_arrow_data_type(module_under_test, bq_type, bq_mode, is_correct_type): + field = schema.SchemaField("ignored_name", bq_type, mode=bq_mode) + actual = module_under_test.bq_to_arrow_data_type(field) + assert is_correct_type(actual) + + +@pytest.mark.parametrize("bq_type", ["RECORD", "record", "STRUCT", "struct"]) +def test_bq_to_arrow_data_type_w_struct(module_under_test, bq_type): + fields = ( + schema.SchemaField("field01", "STRING"), + schema.SchemaField("field02", "BYTES"), + schema.SchemaField("field03", "INTEGER"), + schema.SchemaField("field04", "INT64"), + schema.SchemaField("field05", "FLOAT"), + schema.SchemaField("field06", "FLOAT64"), + schema.SchemaField("field07", "NUMERIC"), + schema.SchemaField("field08", "BIGNUMERIC"), + schema.SchemaField("field09", "BOOLEAN"), + schema.SchemaField("field10", "BOOL"), + schema.SchemaField("field11", "TIMESTAMP"), + schema.SchemaField("field12", "DATE"), + schema.SchemaField("field13", "TIME"), + schema.SchemaField("field14", "DATETIME"), + schema.SchemaField("field15", "GEOGRAPHY"), + ) + + field = schema.SchemaField("ignored_name", bq_type, mode="NULLABLE", fields=fields) + actual = module_under_test.bq_to_arrow_data_type(field) + + expected = ( + pyarrow.field("field01", pyarrow.string()), + pyarrow.field("field02", pyarrow.binary()), + pyarrow.field("field03", pyarrow.int64()), + pyarrow.field("field04", pyarrow.int64()), + pyarrow.field("field05", pyarrow.float64()), + pyarrow.field("field06", pyarrow.float64()), + pyarrow.field("field07", module_under_test.pyarrow_numeric()), + pyarrow.field("field08", module_under_test.pyarrow_bignumeric()), + pyarrow.field("field09", pyarrow.bool_()), + pyarrow.field("field10", pyarrow.bool_()), + pyarrow.field("field11", module_under_test.pyarrow_timestamp()), + pyarrow.field("field12", pyarrow.date32()), + pyarrow.field("field13", module_under_test.pyarrow_time()), + pyarrow.field("field14", module_under_test.pyarrow_datetime()), + pyarrow.field("field15", pyarrow.string()), + ) + expected = pyarrow.struct(expected) + + assert pyarrow.types.is_struct(actual) + assert actual.num_fields == len(fields) + assert actual.equals(expected) + + +@pytest.mark.parametrize("bq_type", ["RECORD", "record", "STRUCT", "struct"]) +def test_bq_to_arrow_data_type_w_array_struct(module_under_test, bq_type): + fields = ( + schema.SchemaField("field01", "STRING"), + schema.SchemaField("field02", "BYTES"), + schema.SchemaField("field03", "INTEGER"), + schema.SchemaField("field04", "INT64"), + schema.SchemaField("field05", "FLOAT"), + schema.SchemaField("field06", "FLOAT64"), + schema.SchemaField("field07", "NUMERIC"), + schema.SchemaField("field08", "BIGNUMERIC"), + schema.SchemaField("field09", "BOOLEAN"), + schema.SchemaField("field10", "BOOL"), + schema.SchemaField("field11", "TIMESTAMP"), + schema.SchemaField("field12", "DATE"), + schema.SchemaField("field13", "TIME"), + schema.SchemaField("field14", "DATETIME"), + schema.SchemaField("field15", "GEOGRAPHY"), + ) + + field = schema.SchemaField("ignored_name", bq_type, mode="REPEATED", fields=fields) + actual = module_under_test.bq_to_arrow_data_type(field) + + expected = ( + pyarrow.field("field01", pyarrow.string()), + pyarrow.field("field02", pyarrow.binary()), + pyarrow.field("field03", pyarrow.int64()), + pyarrow.field("field04", pyarrow.int64()), + pyarrow.field("field05", pyarrow.float64()), + pyarrow.field("field06", pyarrow.float64()), + pyarrow.field("field07", module_under_test.pyarrow_numeric()), + pyarrow.field("field08", module_under_test.pyarrow_bignumeric()), + pyarrow.field("field09", pyarrow.bool_()), + pyarrow.field("field10", pyarrow.bool_()), + pyarrow.field("field11", module_under_test.pyarrow_timestamp()), + pyarrow.field("field12", pyarrow.date32()), + pyarrow.field("field13", module_under_test.pyarrow_time()), + pyarrow.field("field14", module_under_test.pyarrow_datetime()), + pyarrow.field("field15", pyarrow.string()), + ) + expected_value_type = pyarrow.struct(expected) + + assert pyarrow.types.is_list(actual) + assert pyarrow.types.is_struct(actual.value_type) + assert actual.value_type.num_fields == len(fields) + assert actual.value_type.equals(expected_value_type) + + +def test_bq_to_arrow_data_type_w_struct_unknown_subfield(module_under_test): + fields = ( + schema.SchemaField("field1", "STRING"), + schema.SchemaField("field2", "INTEGER"), + # Don't know what to convert UNKNOWN_TYPE to, let type inference work, + # instead. + schema.SchemaField("field3", "UNKNOWN_TYPE"), + ) + field = schema.SchemaField("ignored_name", "RECORD", mode="NULLABLE", fields=fields) + + with warnings.catch_warnings(record=True) as warned: + actual = module_under_test.bq_to_arrow_data_type(field) + + assert actual is None + assert len(warned) == 1 + warning = warned[0] + assert "field3" in str(warning) + + +def test_bq_to_arrow_field_type_override(module_under_test): + # When loading pandas data, we may need to override the type + # decision based on data contents, because GEOGRAPHY data can be + # stored as either text or binary. + + assert ( + module_under_test.bq_to_arrow_field(schema.SchemaField("g", "GEOGRAPHY")).type + == pyarrow.string() + ) + + assert ( + module_under_test.bq_to_arrow_field( + schema.SchemaField("g", "GEOGRAPHY"), + pyarrow.binary(), + ).type + == pyarrow.binary() + ) + + +@pytest.mark.parametrize( + "field_type, metadata", + [ + ("datetime", {b"ARROW:extension:name": b"google:sqlType:datetime"}), + ( + "geography", + { + b"ARROW:extension:name": b"google:sqlType:geography", + b"ARROW:extension:metadata": b'{"encoding": "WKT"}', + }, + ), + ], +) +def test_bq_to_arrow_field_metadata(module_under_test, field_type, metadata): + assert ( + module_under_test.bq_to_arrow_field( + schema.SchemaField("g", field_type) + ).metadata + == metadata + ) From e2788a8c9e53516d467b4d5b15da8838540f371e Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:38:14 +0000 Subject: [PATCH 088/921] chore(main): release 0.9.0 (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [0.9.0](https://togithub.com/googleapis/python-bigquery-dataframes/compare/v0.8.0...v0.9.0) (2023-10-18) ### ⚠ BREAKING CHANGES * rename `bigframes.pandas.reset_session` to `close_session` ([#101](https://togithub.com/googleapis/python-bigquery-dataframes/issues/101)) ### Features * Add `bigframes.options.bigquery.application_name` for partner attribution ([#117](https://togithub.com/googleapis/python-bigquery-dataframes/issues/117)) ([52d64ff](https://togithub.com/googleapis/python-bigquery-dataframes/commit/52d64ffdbbab16b1d94974b543ce9080be1ec0d1)) * Add AtIndexer getitems ([#107](https://togithub.com/googleapis/python-bigquery-dataframes/issues/107)) ([752b01f](https://togithub.com/googleapis/python-bigquery-dataframes/commit/752b01ff9df114c54ed58eb96956e9ce34a8ed47)) * Rename `bigframes.pandas.reset_session` to `close_session` ([#101](https://togithub.com/googleapis/python-bigquery-dataframes/issues/101)) ([36693bf](https://togithub.com/googleapis/python-bigquery-dataframes/commit/36693bff398c23e179d9bde95d52cbaddaf85c45)) * Send BigQuery cancel request when canceling bigframes process ([#103](https://togithub.com/googleapis/python-bigquery-dataframes/issues/103)) ([e325fbb](https://togithub.com/googleapis/python-bigquery-dataframes/commit/e325fbb1c91e040d87df10f7d4d5ce53f7c052cb)) * Support external packages in `remote_function` ([#98](https://togithub.com/googleapis/python-bigquery-dataframes/issues/98)) ([ec10c4a](https://togithub.com/googleapis/python-bigquery-dataframes/commit/ec10c4a5a7833c42e28fe9e7b734bc0c4fb84b6e)) * Use ArrowDtype for STRUCT columns in `to_pandas` ([#85](https://togithub.com/googleapis/python-bigquery-dataframes/issues/85)) ([9238fad](https://togithub.com/googleapis/python-bigquery-dataframes/commit/9238fadcfa7e843be6564813ff3131893b79f8b0)) ### Bug Fixes * Support multiindex for three loc getitem overloads ([#113](https://togithub.com/googleapis/python-bigquery-dataframes/issues/113)) ([68e3cd3](https://togithub.com/googleapis/python-bigquery-dataframes/commit/68e3cd37258084d045ea1075e5e61df12c28faac)) ### Performance Improvements * If primary keys are defined, `read_gbq` avoids copying table data ([#112](https://togithub.com/googleapis/python-bigquery-dataframes/issues/112)) ([e6c0cd1](https://togithub.com/googleapis/python-bigquery-dataframes/commit/e6c0cd1777736e0fa7285da59625fbac487573bd)) ### Documentation * Add documentation for `Series.struct.field` and `Series.struct.explode` ([#114](https://togithub.com/googleapis/python-bigquery-dataframes/issues/114)) ([a6dab9c](https://togithub.com/googleapis/python-bigquery-dataframes/commit/a6dab9cdb7dd0e56c93ca96b665ab1be1baac5e5)) * Add open-source link in API doc ([#106](https://togithub.com/googleapis/python-bigquery-dataframes/issues/106)) ([db51fe3](https://togithub.com/googleapis/python-bigquery-dataframes/commit/db51fe340f644a0d7c911c11d92c8299a4be3446)) * Update ML overview API doc ([#105](https://togithub.com/googleapis/python-bigquery-dataframes/issues/105)) ([1b3f3a5](https://togithub.com/googleapis/python-bigquery-dataframes/commit/1b3f3a5374915b2833c6c1ac05670e9708f07bff)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e34b073999a..974e5d3d43d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.9.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.8.0...v0.9.0) (2023-10-18) + + +### ⚠ BREAKING CHANGES + +* rename `bigframes.pandas.reset_session` to `close_session` ([#101](https://github.com/googleapis/python-bigquery-dataframes/issues/101)) + +### Features + +* Add `bigframes.options.bigquery.application_name` for partner attribution ([#117](https://github.com/googleapis/python-bigquery-dataframes/issues/117)) ([52d64ff](https://github.com/googleapis/python-bigquery-dataframes/commit/52d64ffdbbab16b1d94974b543ce9080be1ec0d1)) +* Add AtIndexer getitems ([#107](https://github.com/googleapis/python-bigquery-dataframes/issues/107)) ([752b01f](https://github.com/googleapis/python-bigquery-dataframes/commit/752b01ff9df114c54ed58eb96956e9ce34a8ed47)) +* Rename `bigframes.pandas.reset_session` to `close_session` ([#101](https://github.com/googleapis/python-bigquery-dataframes/issues/101)) ([36693bf](https://github.com/googleapis/python-bigquery-dataframes/commit/36693bff398c23e179d9bde95d52cbaddaf85c45)) +* Send BigQuery cancel request when canceling bigframes process ([#103](https://github.com/googleapis/python-bigquery-dataframes/issues/103)) ([e325fbb](https://github.com/googleapis/python-bigquery-dataframes/commit/e325fbb1c91e040d87df10f7d4d5ce53f7c052cb)) +* Support external packages in `remote_function` ([#98](https://github.com/googleapis/python-bigquery-dataframes/issues/98)) ([ec10c4a](https://github.com/googleapis/python-bigquery-dataframes/commit/ec10c4a5a7833c42e28fe9e7b734bc0c4fb84b6e)) +* Use ArrowDtype for STRUCT columns in `to_pandas` ([#85](https://github.com/googleapis/python-bigquery-dataframes/issues/85)) ([9238fad](https://github.com/googleapis/python-bigquery-dataframes/commit/9238fadcfa7e843be6564813ff3131893b79f8b0)) + + +### Bug Fixes + +* Support multiindex for three loc getitem overloads ([#113](https://github.com/googleapis/python-bigquery-dataframes/issues/113)) ([68e3cd3](https://github.com/googleapis/python-bigquery-dataframes/commit/68e3cd37258084d045ea1075e5e61df12c28faac)) + + +### Performance Improvements + +* If primary keys are defined, `read_gbq` avoids copying table data ([#112](https://github.com/googleapis/python-bigquery-dataframes/issues/112)) ([e6c0cd1](https://github.com/googleapis/python-bigquery-dataframes/commit/e6c0cd1777736e0fa7285da59625fbac487573bd)) + + +### Documentation + +* Add documentation for `Series.struct.field` and `Series.struct.explode` ([#114](https://github.com/googleapis/python-bigquery-dataframes/issues/114)) ([a6dab9c](https://github.com/googleapis/python-bigquery-dataframes/commit/a6dab9cdb7dd0e56c93ca96b665ab1be1baac5e5)) +* Add open-source link in API doc ([#106](https://github.com/googleapis/python-bigquery-dataframes/issues/106)) ([db51fe3](https://github.com/googleapis/python-bigquery-dataframes/commit/db51fe340f644a0d7c911c11d92c8299a4be3446)) +* Update ML overview API doc ([#105](https://github.com/googleapis/python-bigquery-dataframes/issues/105)) ([1b3f3a5](https://github.com/googleapis/python-bigquery-dataframes/commit/1b3f3a5374915b2833c6c1ac05670e9708f07bff)) + ## [0.8.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.7.0...v0.8.0) (2023-10-12) diff --git a/bigframes/version.py b/bigframes/version.py index 974fbf1ac9b..3f7c8e43998 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.8.0" +__version__ = "0.9.0" From 29dd4144c7e0569de3555a16f916be9c4489bf61 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 19 Oct 2023 06:16:29 +0000 Subject: [PATCH 089/921] feat: Implement DataFrame.dot for matrix multiplication (#67) * feat: Implement DataFrame.dot for matrix multiplication Change-Id: I8baa8c8ee0aeef587be25519a9df549e6db70067 * adjust tests for left column alignment with right rows * refactor pivot to use known unique values * Support DataFrame.dot(Series), remove local-only tests * add explanation for `column_unique_values` param * NotImplementedError for multi-index matrix multiplication * avoid multi-column condition check on series * Add feedback link in the exception messages --- bigframes/core/blocks.py | 25 ++++- bigframes/dataframe.py | 102 +++++++++++++++++- tests/data/matrix_2by3.json | 22 ++++ tests/data/matrix_2by3.jsonl | 2 + tests/data/matrix_3by4.json | 27 +++++ tests/data/matrix_3by4.jsonl | 3 + tests/system/conftest.py | 68 ++++++++++++ tests/system/small/test_dataframe.py | 54 ++++++++++ tests/system/small/test_multiindex.py | 44 ++++++++ .../bigframes_vendored/pandas/core/frame.py | 30 ++++++ 10 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 tests/data/matrix_2by3.json create mode 100644 tests/data/matrix_2by3.jsonl create mode 100644 tests/data/matrix_3by4.json create mode 100644 tests/data/matrix_3by4.jsonl diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 8966b6189bd..046d2b3a444 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1261,13 +1261,28 @@ def pivot( *, columns: Sequence[str], values: Sequence[str], + columns_unique_values: typing.Optional[ + typing.Union[pd.Index, Sequence[object]] + ] = None, values_in_index: typing.Optional[bool] = None, ): - # Columns+index should uniquely identify rows - # Warning: This is not validated, breaking this constraint will result in silently non-deterministic behavior. - # -1 to allow for ordering column in addition to pivot columns - max_unique_value = (_BQ_MAX_COLUMNS - 1) // len(values) - columns_values = self._get_unique_values(columns, max_unique_value) + # We need the unique values from the pivot columns to turn them into + # column ids. It can be deteremined by running a SQL query on the + # underlying data. However, the caller can save that if they know the + # unique values upfront by providing them explicitly. + if columns_unique_values is None: + # Columns+index should uniquely identify rows + # Warning: This is not validated, breaking this constraint will + # result in silently non-deterministic behavior. + # -1 to allow for ordering column in addition to pivot columns + max_unique_value = (_BQ_MAX_COLUMNS - 1) // len(values) + columns_values = self._get_unique_values(columns, max_unique_value) + else: + columns_values = ( + columns_unique_values + if isinstance(columns_unique_values, pd.Index) + else pd.Index(columns_unique_values) + ) column_index = columns_values column_ids: list[str] = [] diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 5740d2c4dc8..32a2908a421 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1716,10 +1716,13 @@ def kurt(self, *, numeric_only: bool = False): kurtosis = kurt - def pivot( + def _pivot( self, *, columns: typing.Union[blocks.Label, Sequence[blocks.Label]], + columns_unique_values: typing.Optional[ + typing.Union[pandas.Index, Sequence[object]] + ] = None, index: typing.Optional[ typing.Union[blocks.Label, Sequence[blocks.Label]] ] = None, @@ -1743,10 +1746,24 @@ def pivot( pivot_block = block.pivot( columns=column_ids, values=value_col_ids, + columns_unique_values=columns_unique_values, values_in_index=utils.is_list_like(values), ) return DataFrame(pivot_block) + def pivot( + self, + *, + columns: typing.Union[blocks.Label, Sequence[blocks.Label]], + index: typing.Optional[ + typing.Union[blocks.Label, Sequence[blocks.Label]] + ] = None, + values: typing.Optional[ + typing.Union[blocks.Label, Sequence[blocks.Label]] + ] = None, + ) -> DataFrame: + return self._pivot(columns=columns, index=index, values=values) + def stack(self, level: LevelsType = -1): if not isinstance(self.columns, pandas.MultiIndex): if level not in [0, -1, self.columns.name]: @@ -2578,3 +2595,86 @@ def _get_block(self) -> blocks.Block: def _cached(self) -> DataFrame: return DataFrame(self._block.cached()) + + _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") + + def dot(self, other: _DataFrameOrSeries) -> _DataFrameOrSeries: + if not isinstance(other, (DataFrame, bf_series.Series)): + raise NotImplementedError( + f"Only DataFrame or Series operand is supported. {constants.FEEDBACK_LINK}" + ) + + if len(self.index.names) > 1 or len(other.index.names) > 1: + raise NotImplementedError( + f"Multi-index input is not supported. {constants.FEEDBACK_LINK}" + ) + + if len(self.columns.names) > 1 or ( + isinstance(other, DataFrame) and len(other.columns.names) > 1 + ): + raise NotImplementedError( + f"Multi-level column input is not supported. {constants.FEEDBACK_LINK}" + ) + + # Convert the dataframes into cell-value-decomposed representation, i.e. + # each cell value is present in a separate row + row_id = "row" + col_id = "col" + val_id = "val" + left_suffix = "_left" + right_suffix = "_right" + cvd_columns = [row_id, col_id, val_id] + + def get_left_id(id): + return f"{id}{left_suffix}" + + def get_right_id(id): + return f"{id}{right_suffix}" + + other_frame = other if isinstance(other, DataFrame) else other.to_frame() + + left = self.stack().reset_index() + left.columns = cvd_columns + + right = other_frame.stack().reset_index() + right.columns = cvd_columns + + merged = left.merge( + right, + left_on=col_id, + right_on=row_id, + suffixes=(left_suffix, right_suffix), + ) + + left_row_id = get_left_id(row_id) + right_col_id = get_right_id(col_id) + + aggregated = ( + merged.assign( + val=merged[get_left_id(val_id)] * merged[get_right_id(val_id)] + )[[left_row_id, right_col_id, val_id]] + .groupby([left_row_id, right_col_id]) + .sum(numeric_only=True) + ) + aggregated_noindex = aggregated.reset_index() + aggregated_noindex.columns = cvd_columns + result = aggregated_noindex._pivot( + columns=col_id, columns_unique_values=other_frame.columns, index=row_id + ) + + # Set the index names to match the left side matrix + result.index.names = self.index.names + + # Pivot has the result columns ordered alphabetically. It should still + # match the columns in the right sided matrix. Let's reorder them as per + # the right side matrix + if not result.columns.difference(other_frame.columns).empty: + raise RuntimeError( + f"Could not construct all columns. {constants.FEEDBACK_LINK}" + ) + result = result[other_frame.columns] + + if isinstance(other, bf_series.Series): + result = result[other.name].rename() + + return result diff --git a/tests/data/matrix_2by3.json b/tests/data/matrix_2by3.json new file mode 100644 index 00000000000..a0cf0c14da0 --- /dev/null +++ b/tests/data/matrix_2by3.json @@ -0,0 +1,22 @@ +[ + { + "mode": "REQUIRED", + "name": "rowindex", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "a", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "b", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "c", + "type": "INTEGER" + } + ] diff --git a/tests/data/matrix_2by3.jsonl b/tests/data/matrix_2by3.jsonl new file mode 100644 index 00000000000..c2c96a5423a --- /dev/null +++ b/tests/data/matrix_2by3.jsonl @@ -0,0 +1,2 @@ +{"rowindex": 0, "a": 1, "b": 2, "c": 3} +{"rowindex": 1, "a": 2, "b": 5, "c": 7} diff --git a/tests/data/matrix_3by4.json b/tests/data/matrix_3by4.json new file mode 100644 index 00000000000..6b272ee6001 --- /dev/null +++ b/tests/data/matrix_3by4.json @@ -0,0 +1,27 @@ +[ + { + "mode": "REQUIRED", + "name": "rowindex", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "w", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "x", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "y", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "z", + "type": "INTEGER" + } + ] diff --git a/tests/data/matrix_3by4.jsonl b/tests/data/matrix_3by4.jsonl new file mode 100644 index 00000000000..5a081dafc33 --- /dev/null +++ b/tests/data/matrix_3by4.jsonl @@ -0,0 +1,3 @@ +{"rowindex": "a", "w": 2, "x": 4, "y": 8, "z": 21} +{"rowindex": "b", "w": 1, "x": 5, "y": 10, "z": -11} +{"rowindex": "c", "w": 3, "x": 6, "y": 9, "z": 0} diff --git a/tests/system/conftest.py b/tests/system/conftest.py index f36a29b0ab1..cb664302a8f 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -235,6 +235,8 @@ def load_test_data_tables( ("penguins", "penguins_schema.json", "penguins.jsonl"), ("time_series", "time_series_schema.json", "time_series.jsonl"), ("hockey_players", "hockey_players.json", "hockey_players.jsonl"), + ("matrix_2by3", "matrix_2by3.json", "matrix_2by3.jsonl"), + ("matrix_3by4", "matrix_3by4.json", "matrix_3by4.jsonl"), ]: test_data_hash = hashlib.md5() _hash_digest_file(test_data_hash, DATA_DIR / schema_filename) @@ -304,6 +306,16 @@ def time_series_table_id(test_data_tables) -> str: return test_data_tables["time_series"] +@pytest.fixture(scope="session") +def matrix_2by3_table_id(test_data_tables) -> str: + return test_data_tables["matrix_2by3"] + + +@pytest.fixture(scope="session") +def matrix_3by4_table_id(test_data_tables) -> str: + return test_data_tables["matrix_3by4"] + + @pytest.fixture(scope="session") def scalars_df_default_index( scalars_df_index: bigframes.dataframe.DataFrame, @@ -411,6 +423,62 @@ def hockey_pandas_df() -> pd.DataFrame: return df +@pytest.fixture(scope="session") +def matrix_2by3_df( + matrix_2by3_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at a test 2-by-3 matrix data.""" + df = session.read_gbq(matrix_2by3_table_id) + df = df.set_index("rowindex").sort_index() + return df + + +@pytest.fixture(scope="session") +def matrix_2by3_pandas_df() -> pd.DataFrame: + """pd.DataFrame pointing at a test 2-by-3 matrix data.""" + df = pd.read_json( + DATA_DIR / "matrix_2by3.jsonl", + lines=True, + dtype={ + "rowindex": pd.Int64Dtype(), + "a": pd.Int64Dtype(), + "b": pd.Int64Dtype(), + "c": pd.Int64Dtype(), + }, + ) + df = df.set_index("rowindex").sort_index() + df.index = df.index.astype("Int64") + return df + + +@pytest.fixture(scope="session") +def matrix_3by4_df( + matrix_3by4_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at a test 3-by-4 matrix data.""" + df = session.read_gbq(matrix_3by4_table_id) + df = df.set_index("rowindex").sort_index() + return df + + +@pytest.fixture(scope="session") +def matrix_3by4_pandas_df() -> pd.DataFrame: + """pd.DataFrame pointing at a test 3-by-4 matrix data.""" + df = pd.read_json( + DATA_DIR / "matrix_3by4.jsonl", + lines=True, + dtype={ + "rowindex": pd.StringDtype(storage="pyarrow"), + "w": pd.Int64Dtype(), + "x": pd.Int64Dtype(), + "y": pd.Int64Dtype(), + "z": pd.Int64Dtype(), + }, + ) + df = df.set_index("rowindex").sort_index() + return df + + @pytest.fixture(scope="session") def penguins_df_default_index( penguins_table_id: str, session: bigframes.Session diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 309e8df4f0d..19e50eb06de 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3167,3 +3167,57 @@ def test_df_cached(scalars_df_index): df_cached_copy = df._cached() pandas.testing.assert_frame_equal(df.to_pandas(), df_cached_copy.to_pandas()) + + +def test_df_dot_inline(session): + df1 = pd.DataFrame([[1, 2, 3], [2, 5, 7]]) + df2 = pd.DataFrame([[2, 4, 8], [1, 5, 10], [3, 6, 9]]) + + bf1 = session.read_pandas(df1) + bf2 = session.read_pandas(df2) + bf_result = bf1.dot(bf2).to_pandas() + pd_result = df1.dot(df2) + + # Patch pandas dtypes for testing parity + # Pandas uses int64 instead of Int64 (nullable) dtype. + for name in pd_result.columns: + pd_result[name] = pd_result[name].astype(pd.Int64Dtype()) + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_dot( + matrix_2by3_df, matrix_2by3_pandas_df, matrix_3by4_df, matrix_3by4_pandas_df +): + bf_result = matrix_2by3_df.dot(matrix_3by4_df).to_pandas() + pd_result = matrix_2by3_pandas_df.dot(matrix_3by4_pandas_df) + + # Patch pandas dtypes for testing parity + # Pandas result is object instead of Int64 (nullable) dtype. + for name in pd_result.columns: + pd_result[name] = pd_result[name].astype(pd.Int64Dtype()) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + +def test_df_dot_series( + matrix_2by3_df, matrix_2by3_pandas_df, matrix_3by4_df, matrix_3by4_pandas_df +): + bf_result = matrix_2by3_df.dot(matrix_3by4_df["x"]).to_pandas() + pd_result = matrix_2by3_pandas_df.dot(matrix_3by4_pandas_df["x"]) + + # Patch pandas dtypes for testing parity + # Pandas result is object instead of Int64 (nullable) dtype. + pd_result = pd_result.astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index a1326767700..b5c78de69c2 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -934,3 +934,47 @@ def test_column_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index) pd_result = pd_df.swaplevel(-3, -1, axis=1) pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_multi_index_dot_not_supported(): + left_matrix = [[1, 2, 3], [2, 5, 7]] + right_matrix = [[2, 4, 8], [1, 5, 10], [3, 6, 9]] + + # Left multi-index + left_index = pandas.MultiIndex.from_tuples([("a", "aa"), ("a", "ab")]) + bf1 = bpd.DataFrame(left_matrix, index=left_index) + bf2 = bpd.DataFrame(right_matrix) + with pytest.raises(NotImplementedError, match="Multi-index input is not supported"): + bf1.dot(bf2) + + # right multi-index + right_index = pandas.MultiIndex.from_tuples([("a", "aa"), ("a", "ab"), ("b", "bb")]) + bf1 = bpd.DataFrame(left_matrix) + bf2 = bpd.DataFrame(right_matrix, index=right_index) + with pytest.raises(NotImplementedError, match="Multi-index input is not supported"): + bf1.dot(bf2) + + +def test_column_multi_index_dot_not_supported(): + left_matrix = [[1, 2, 3], [2, 5, 7]] + right_matrix = [[2, 4, 8], [1, 5, 10], [3, 6, 9]] + + multi_level_columns = pandas.MultiIndex.from_arrays( + [["col0", "col0", "col1"], ["col00", "col01", "col11"]] + ) + + # Left multi-columns + bf1 = bpd.DataFrame(left_matrix, columns=multi_level_columns) + bf2 = bpd.DataFrame(right_matrix) + with pytest.raises( + NotImplementedError, match="Multi-level column input is not supported" + ): + bf1.dot(bf2) + + # right multi-columns + bf1 = bpd.DataFrame(left_matrix) + bf2 = bpd.DataFrame(right_matrix, columns=multi_level_columns) + with pytest.raises( + NotImplementedError, match="Multi-level column input is not supported" + ): + bf1.dot(bf2) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 621d052cb8e..a5c12d7b32a 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2117,3 +2117,33 @@ def iat(self): def at(self): """Access a single value for a row/column label pair.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def dot(self, other): + """ + Compute the matrix multiplication between the DataFrame and other. + + This method computes the matrix product between the DataFrame and the + values of an other Series or DataFrame. + + It can also be called using `self @ other`. + + .. note:: + The dimensions of DataFrame and other must be compatible in order to + compute the matrix multiplication. In addition, the column names of + DataFrame and the index of other must contain the same values, as they + will be aligned prior to the multiplication. + + The dot method for Series computes the inner product, instead of the + matrix product here. + + Args: + other (Series or DataFrame): + The other object to compute the matrix product with. + + Returns: + Series or DataFrame + If `other` is a Series, return the matrix product between self and + other as a Series. If other is a DataFrame, return + the matrix product of self and other in a DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 782a215421c743999782f94eba76c82db93cc85c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:43:10 -0500 Subject: [PATCH 090/921] chore(main): release 0.10.0 (#122) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ bigframes/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 974e5d3d43d..4d9f63d4c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.10.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.9.0...v0.10.0) (2023-10-19) + + +### Features + +* Implement DataFrame.dot for matrix multiplication ([#67](https://github.com/googleapis/python-bigquery-dataframes/issues/67)) ([29dd414](https://github.com/googleapis/python-bigquery-dataframes/commit/29dd4144c7e0569de3555a16f916be9c4489bf61)) + ## [0.9.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.8.0...v0.9.0) (2023-10-18) diff --git a/bigframes/version.py b/bigframes/version.py index 3f7c8e43998..7a37ebd2205 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.9.0" +__version__ = "0.10.0" From 59bc7be9891ac12926ac44531d9dbd4a88bcfc44 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:02:14 +0000 Subject: [PATCH 091/921] chore: rename rst files to avoid conflict with service names (#120) Source-Link: https://togithub.com/googleapis/synthtool/commit/d52e638b37b091054c869bfa6f5a9fedaba9e0dd Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:4f9b3b106ad0beafc2c8a415e3f62c1a0cc23cabea115dbe841b848f581cfe99 --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/requirements.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index dd98abbdeeb..7f291dbd5f9 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:08e34975760f002746b1d8c86fdc90660be45945ee6d9db914d1508acdf9a547 -# created: 2023-10-09T14:06:13.397766266Z + digest: sha256:4f9b3b106ad0beafc2c8a415e3f62c1a0cc23cabea115dbe841b848f581cfe99 +# created: 2023-10-18T20:26:37.410353675Z diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 0332d3267e1..16170d0ca7b 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -467,9 +467,9 @@ typing-extensions==4.4.0 \ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via -r requirements.in -urllib3==1.26.17 \ - --hash=sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21 \ - --hash=sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b +urllib3==1.26.18 \ + --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ + --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 # via # requests # twine From c9c46d45eca536d1fc2dfed0cce25bfb41a41554 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 19 Oct 2023 13:08:14 -0500 Subject: [PATCH 092/921] chore: remove unneeded scripts, no longer need THIRD_PARTY_NOTICES (#121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 306233684 🦕 --- .kokoro/release-nightly.sh | 44 +--- scripts/generate_third_party_notices.py | 332 ------------------------ scripts/update_firebase_docs_site.sh | 105 -------- scripts/update_x20_docs_site.sh | 106 -------- 4 files changed, 1 insertion(+), 586 deletions(-) delete mode 100644 scripts/generate_third_party_notices.py delete mode 100644 scripts/update_firebase_docs_site.sh delete mode 100644 scripts/update_x20_docs_site.sh diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh index 488dbb9e130..5cc1275308a 100755 --- a/.kokoro/release-nightly.sh +++ b/.kokoro/release-nightly.sh @@ -63,26 +63,6 @@ export PYTHONUNBUFFERED=1 # Install dependencies, as the following steps depend on it python3.10 -m pip install -e .[all] -# Generate third party notices and include it in the licenses in setup.cfg -# TODO(shobs): Don't include it in the package once vertex colab can pick it -# from elsewhere -THIRD_PARTY_NOTICES_FILE=THIRD_PARTY_NOTICES -python3.10 -m pip install pip-licenses -python3.10 scripts/generate_third_party_notices.py --output-file ${THIRD_PARTY_NOTICES_FILE} -if ! [ -s ${THIRD_PARTY_NOTICES_FILE} ]; then - echo "${THIRD_PARTY_NOTICES_FILE} was generated with zero size" - exit -1 -fi -SETUP_CFG_BKP=`mktemp` -cp -f setup.cfg ${SETUP_CFG_BKP} -cat >> setup.cfg << EOF - -[metadata] -license_files = - LICENSE - ${THIRD_PARTY_NOTICES_FILE} -EOF - # Update version string to include git hash and date CURRENT_DATE=$(date '+%Y%m%d') GIT_HASH=$(git rev-parse --short HEAD) @@ -101,33 +81,13 @@ if [ $num_wheel_files -ne 1 ] ; then exit -1 fi -# Make sure the wheel file has the third party notices included -# TODO(shobs): An utimate validation would be to create a virtual environment -# and install the wheel file, then verify that -# site-packages/bigframes-*.dist-info/ includes third party notices -python3.10 -c " -from zipfile import ZipFile -with ZipFile('$VERSION_WHEEL') as myzip: - third_party_licenses_info = [ - info - for info in myzip.infolist() - if info.filename.endswith('.dist-info/${THIRD_PARTY_NOTICES_FILE}') - ] - assert ( - len(third_party_licenses_info) == 1 - ), f'Found {len(third_party_licenses_info)} third party licenses' - assert ( - third_party_licenses_info[0].file_size > 0 - ), 'Package contains third party license of size 0' -" - # Create a copy of the wheel with a well known, version agnostic name LATEST_WHEEL=dist/bigframes-latest-py2.py3-none-any.whl cp $VERSION_WHEEL $LATEST_WHEEL cp dist/bigframes-*.tar.gz dist/bigframes-latest.tar.gz if ! [ ${DRY_RUN} ]; then - for gcs_path in gs://vertex_sdk_private_releases/bigframe/ \ +for gcs_path in gs://vertex_sdk_private_releases/bigframe/ \ gs://dl-platform-colab/bigframes/ \ gs://bigframes-wheels/; do @@ -155,8 +115,6 @@ fi # the changes were made but before this cleanup, because the script would # terminate with the failure itself. See if we can ensure the cleanup. sed -i -e "s/$RELEASE_VERSION/$BIGFRAMES_VERSION/g" bigframes/version.py -mv -f ${SETUP_CFG_BKP} setup.cfg -rm -f ${THIRD_PARTY_NOTICES_FILE} if ! [ ${DRY_RUN} ]; then # Copy docs and wheels to Google Drive diff --git a/scripts/generate_third_party_notices.py b/scripts/generate_third_party_notices.py deleted file mode 100644 index 7040bb2e5f8..00000000000 --- a/scripts/generate_third_party_notices.py +++ /dev/null @@ -1,332 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import glob -import importlib.metadata -import json -import os.path -import re -import sys - -import piplicenses -import requests - -DEPENDENCY_INFO_SEPARATOR = "*" * 80 + "\n" -PACKAGE_NAME_EXTRACTOR = re.compile("^[a-zA-Z0-9._-]+") - -# These packages don't have LICENSE files distributed in their packages, -# but we have manually confirmed they have a compatible license and -# included it manually in our `third_party` directory. -# -# TODO(swast): We can remove this workaround once these packages bundle the -# license file. -# -# ipython-genutils and recommonmark are both in an archived state with no likely updates in the future -# -# Tracking issues: -# * https://github.com/grpc/grpc/issues/33557 -# * https://github.com/gsnedders/python-webencodings/issues/33 -# * https://github.com/pickleshare/pickleshare/issues/34 -DIRECT_LICENSE_MAPPINGS = { - "grpcio-status": "https://raw.githubusercontent.com/grpc/grpc/master/LICENSE", - "webencodings": "https://raw.githubusercontent.com/gsnedders/python-webencodings/master/LICENSE", - "ipython-genutils": "https://raw.githubusercontent.com/ipython/ipython_genutils/master/COPYING.md", - "pickleshare": "https://raw.githubusercontent.com/pickleshare/pickleshare/master/LICENSE", - "recommonmark": "https://raw.githubusercontent.com/readthedocs/recommonmark/master/license.md", -} - - -def get_package_dependencies(pkg_name): - """Get all package dependencies for a given package, both required and optional.""" - packages = set() - requirements = importlib.metadata.requires(pkg_name) - if requirements: - for req in requirements: - match = PACKAGE_NAME_EXTRACTOR.match(req) - assert match, f"Could not parse {req} for package name" - packages.add(match.group(0)) - return packages - - -# Inspired by third_party/colab/cleanup_filesets.py -def find_dependencies( - roots: set[str], ignore_missing_metadata=False -) -> dict[str, dict[str, set[str]]]: - """Return the transitive dependencies of a set of packages. - Args: - roots: List of package names, e.g. ["pkg1", "pkg2"] - Returns: - A dictionary of dependencies, e.g. - { - "pkg3" : { - "Requires" : set(["pkg4", "pkg5", "pkg6"]), - "RequiredBy": set(["pkg1"]) - }, - "pkg4" : { - "Requires" : set([]), - "RequiredBy": set(["pkg3"]) - }, - ... - } - """ - hops = set() - visited = set() - deps: dict[str, dict[str, set[str]]] = dict() - - # Initialize the start of the graph walk - for root in roots: - # Get the normalized package name - try: - pkg = importlib.metadata.metadata(root) - except importlib.metadata.PackageNotFoundError: - if not ignore_missing_metadata: - raise - continue - hops.add(pkg["Name"]) - - # Start the graph walk - while True: - if not hops: - break - hop = hops.pop() - if hop in visited: - continue - visited.add(hop) - - for dep in get_package_dependencies(hop): - # Get the normalized package name - try: - req_pkg = importlib.metadata.metadata(dep) - except importlib.metadata.PackageNotFoundError: - if not ignore_missing_metadata: - raise - continue - dep = req_pkg["Name"] - - # Create outgoing edge only for non root packages, for which an - # entry must have been created in the deps dictionary when we - # saw the package for the first time during the graph walk - if hop in deps: - deps[hop]["Requires"].add(dep) - - if dep in deps: - # We have already seen this requirement in the graph walk. - # Just update the incoming dependency and carry on. - deps[dep]["RequiredBy"].add(hop) - else: - # This is the first time we came across this requirement. - # Create a new entry with the incoming dependency. - deps[dep] = {"RequiredBy": {hop}, "Requires": set()} - - # Put it in the next hops for further graph traversal - hops.add(dep) - - return deps - - -def get_metadata_and_filename( - package_name: str, - metadata_name: str, - metadata_file: str, - metadata_text: str, - ignore_missing=True, -) -> tuple[str, str] | None: - """Get package metadata and corresponsing file name.""" - - # Check metadata file - metadata_filepath_known = metadata_file != piplicenses.LICENSE_UNKNOWN - if not metadata_filepath_known and not ignore_missing: - raise ValueError(f"No {metadata_name} file found for {package_name}") - - # Check metadata text - if metadata_text != piplicenses.LICENSE_UNKNOWN: - output_filename = metadata_name - if metadata_filepath_known: - output_filename = os.path.basename(metadata_file) - if not output_filename: - raise ValueError( - f"Need a file name to write {metadata_name} text for {package_name}." - ) - return metadata_text, output_filename - elif not ignore_missing: - raise ValueError(f"No {metadata_name} text found for {package_name}") - - return None - - -def fetch_license_and_notice_metadata(packages: list[str]): - """Fetch metadata including license and notice for given packages. - Returns a json object. - """ - parser = piplicenses.create_parser() - args = parser.parse_args( - [ - "--format", - "json", - "--with-license-file", - "--with-notice-file", - "--with-urls", - "--with-description", - "--packages", - *packages, - ] - ) - output_str = piplicenses.create_output_string(args) - metadatas = json.loads(output_str) - return metadatas - - -def write_lines_without_trailing_spaces(file, text: str, key: str): - """Write text lines to a file without the trailing spaces. - This will stop complaints by the trailing-whitespace pre-commit hook.""" - text = "\n".join([line.rstrip() for line in text.split("\n")]) - file.write(f"{key}:\n{text}\n") - - -def write_metadata_to_file( - file, metadata, with_version=False, requires_packages=[], packages_required_by=[] -): - """Write package metadata to a file object.""" - file.write(DEPENDENCY_INFO_SEPARATOR) - - info_keys = ["Name"] - if with_version: - info_keys.append("Version") - info_keys.extend(["License", "URL"]) - file.writelines([f"{key}: {metadata[key]}\n" for key in info_keys]) - - if requires_packages: - file.write(f"Requires: {', '.join(sorted(requires_packages))}\n") - - if packages_required_by: - file.write(f"Required By: {', '.join(sorted(packages_required_by))}\n") - - # Try to generate third party license - - license_info = get_metadata_and_filename( - metadata["Name"], - "LICENSE", - metadata["LicenseFile"], - metadata["LicenseText"], - ignore_missing=metadata["Name"] in DIRECT_LICENSE_MAPPINGS, - ) - - license_text = "" - if license_info: - license_text = license_info[0] - else: - license_text_response = requests.get(DIRECT_LICENSE_MAPPINGS[metadata["Name"]]) - license_text = license_text_response.text - - write_lines_without_trailing_spaces(file, license_text, "License") - - # Try to generate third party notice - notice_info = get_metadata_and_filename( - metadata["Name"], - "NOTICE", - metadata["NoticeFile"], - metadata["NoticeText"], - ignore_missing=True, - ) - - if notice_info: - write_lines_without_trailing_spaces(file, notice_info[0], "Notice") - - file.write(DEPENDENCY_INFO_SEPARATOR) - - -def write_third_party_vendored_license(file, path): - """Write license of a vendored third party library to notices file.""" - file.write(DEPENDENCY_INFO_SEPARATOR) - file.write(f"Vendored Code: {os.path.dirname(path)}\n") - notice_key = f"Notice ({os.path.basename(path)})" - write_lines_without_trailing_spaces(file, open(path).read(), notice_key) - file.write(DEPENDENCY_INFO_SEPARATOR) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Generate third party notices for bigframes dependencies." - ) - parser.add_argument( - "--with-version", - action="store_true", - default=False, - help="Include the version information for each package.", - ) - parser.add_argument( - "--with-requires", - action="store_true", - default=False, - help="Include for each package the packages it requires.", - ) - parser.add_argument( - "--with-required-by", - action="store_true", - default=False, - help="Include for each package the packages that require it.", - ) - parser.add_argument( - "--output-file", - action="store", - default="THIRD_PARTY_NOTICES", - help="The output file to write third party notices in.", - ) - args = parser.parse_args(sys.argv[1:]) - - # Initialize the root package - roots = {"bigframes"} - - # Find dependencies - # Let's ignore the packages that are not installed assuming they are - # just the optional dependencies that bigframes does not require. - # One example is the dependency path bigframes -> SQLAlchemy -> pg8000, - # where pg8000 is only an optional dependency for SQLAlchemy which bigframes - # is not depending on - # https://github.com/sqlalchemy/sqlalchemy/blob/7bc81947e22dc32368b0c49a41c398cd251d94af/setup.cfg#LL62C21-L62C27 - deps = find_dependencies(roots, ignore_missing_metadata=True) - - # Use third party solution to fetch dependency metadata - deps_metadata = fetch_license_and_notice_metadata(list(deps)) - deps_metadata = sorted(deps_metadata, key=lambda m: m["Name"]) - - # Write the file - with open(args.output_file, "w") as f: - # Generate third party metadata for each dependency - for metadata in deps_metadata: - dep = deps[metadata["Name"]] - write_metadata_to_file( - f, - metadata, - args.with_version, - dep["Requires"] if args.with_requires else [], - dep["RequiredBy"] if args.with_required_by else [], - ) - - # Generate third party vendored notices - notices = set() - for filename in [ - "LICENCE", - "LICENCE.txt", - "LICENSE", - "LICENSE.txt", - "NOTICE", - "NOTICE.txt", - "COPYING", - "COPYING.txt", - ]: - notices.update(glob.glob(f"third_party/bigframes_vendored/*/{filename}")) - for path in sorted(notices): - write_third_party_vendored_license(f, path) diff --git a/scripts/update_firebase_docs_site.sh b/scripts/update_firebase_docs_site.sh deleted file mode 100644 index f0ef866c90b..00000000000 --- a/scripts/update_firebase_docs_site.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -e -o pipefail -set -x - -# Replace the docs version by the desired commit -BIGFRAMES_DOCS_VERSION=8ce2a3e - -BIGFRAMES_FIREBASE_PROJECT=bigframes-dev-d4d9a -BIGFRAMES_DOCS_GCS_BUCKET=bigframes-docs - -BIGFRAMES_DOCS_DIR=`mktemp -d` - - -# Install firebase if not already installed -if ! which firebase; then - npm install -g firebase-tools -fi - -# Prepare a working directory for firebase -mkdir -p ${BIGFRAMES_DOCS_DIR} -pushd ${BIGFRAMES_DOCS_DIR} - -# Copy the bigframes version -if [ ! -d ${BIGFRAMES_DOCS_VERSION} ]; then - gsutil -m cp -r gs://${BIGFRAMES_DOCS_GCS_BUCKET}/${BIGFRAMES_DOCS_VERSION} . -fi - -rm -f latest -ln -s ${BIGFRAMES_DOCS_VERSION} latest - -# Set up firebase -firebase login --no-localhost -firebase init hosting - -versions="${BIGFRAMES_DOCS_VERSION} latest" -for version in ${versions}; do - site_name=bigframes-docs-${version} - if ! firebase hosting:sites:list | grep ${site_name}; then - firebase hosting:sites:create ${site_name} - fi - - firebase target:apply hosting ${version} ${site_name} -done - -# Make sure the firebase json config is consistent with ${versions} -# TODO(shobs): Come up with a better way of updating the config than -# a hard overwrite -cat > firebase.json << EOF -{ - "hosting": [ - { - "target": "latest", - "public": "latest", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ] - } - , - { - "target": "${BIGFRAMES_DOCS_VERSION}", - "public": "${BIGFRAMES_DOCS_VERSION}", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ] - } - ] -} -EOF - -# Verify that the intended sites look good -for version in ${versions}; do - echo "Preview the local hosting of the docs site \"${version}\" before actually deploying (Press Ctrl+C to stop)" - firebase serve --only hosting:${version} -done - -echo -n "Go ahead and deploy? [y/N]: " -read deploy_consent - -# Deploy the sites -if [ "$deploy_consent" = y ]; then - echo "Deploying ..." - firebase deploy --only hosting -else - echo "Not Deploying anything." -fi - -popd diff --git a/scripts/update_x20_docs_site.sh b/scripts/update_x20_docs_site.sh deleted file mode 100644 index 31da116bdd4..00000000000 --- a/scripts/update_x20_docs_site.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# Copyright 2020 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 -# -# https://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. - -################################################################################ -# This script uses corp credentials to sync the files to x20. -# Make sure you: -# 1. Have write permission to /x20/teams/bigframes-swe -# 2. Have run `gcert` before running the script -################################################################################ - -set -e -o pipefail -set -x - -GIT_DOCS_DIR=docs/_build/html/ -X20_BIGFRAMES_DIR=/x20/teams/bigframes-swe/bigframes/docs -MAX_BACKFILL=10 -REQ_PYTHON_VERSION=3.9 - -# Create a temporary directory -tempdir=`mktemp --directory --tmpdir bigframes.XXXXXXXXXX` - -# Clone the repository -git clone sso://team/bigquery-query-swe/bigframes ${tempdir} - -# Enter the temporary bigframes directory -pushd ${tempdir} - -# Python version 3.9 is required to build bigframes docs, install if not present -if ! python3 --version | grep ${REQ_PYTHON_VERSION}; then - # Install pyenv to install the required python version - ## https://github.com/pyenv/pyenv#basic-github-checkout - git clone https://github.com/pyenv/pyenv.git .pyenv - pushd .pyenv && src/configure && make -C src && popd - - ## https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv - export PYENV_ROOT=${PWD}/.pyenv - PATH=${PYENV_ROOT}/bin:${PATH} - eval "$(pyenv init -)" - - ## Install the required python version - pyenv install ${REQ_PYTHON_VERSION} - - ## Make the required python version available - pyenv global ${REQ_PYTHON_VERSION} -fi - -# Create a virtual environment with nox installed -python3 -m venv venv -source venv/bin/activate -pip install nox - -# i = 0 means docs for the latest version, and i = 1 onwards means backfill -for i in `seq 0 ${MAX_BACKFILL}`; do - # If it is backfill turn, back off the version by 1 - if [ ${i} -ne 0 ]; then - git reset --hard HEAD~1 - - # Clean up any old docs - rm -rf ${GIT_DOCS_DIR} - fi - - # Construct a docs path in x20 - commit_hash=`git rev-parse --short HEAD` - x20_docs_dir_commit=${X20_BIGFRAMES_DIR}/${commit_hash} - - # If the x20 docs path already exists, let's assume that it was created - # properly in the previous attempt - if fileutil test -d ${x20_docs_dir_commit}; then - echo ${x20_docs_dir_commit} exists, skipping rebuilding it.. - continue - fi - - # Build the docs - echo Building docs for commit ${commit_hash}.. - nox -s docs - - # TODO(shobs): Check if a symlink can be created instead of another copy of - # the latest commit's docs, using fileutil CLI or otherwise - x20_docs_dirs=${x20_docs_dir_commit} - if [ ${i} -eq 0 ]; then - x20_docs_dirs="${x20_docs_dirs} ${X20_BIGFRAMES_DIR}/latest" - fi - - for x20_docs_dir in ${x20_docs_dirs}; do - fileutil mirror -parallelism=4 -force ${GIT_DOCS_DIR} ${x20_docs_dir} - x20_own request_change --recursive --path=${x20_docs_dir} --new_mode=a+r - done -done - -# Exit the temporary bigframes directory -popd - -# Clean up the temporary bigframes directory -rm -rf ${tempdir} From 694a85a0ef90d838700014a204d72b23362db1d8 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 20 Oct 2023 10:52:49 -0500 Subject: [PATCH 093/921] feat: add back `reset_session` as an alias for `close_session` (#124) --- bigframes/pandas/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 1b9144fb625..24b19fa70a2 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -452,6 +452,7 @@ def read_gbq_function(function_name: str): # Session management APIs get_global_session = global_session.get_global_session close_session = global_session.close_session +reset_session = global_session.close_session # Use __all__ to let type checkers know what is part of the public API. From f9bb3c4bc88c5ba2be6f17e12a0ec4f482ce161f Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:48:13 -0700 Subject: [PATCH 094/921] feat: change `query` parameter to `query_or_table` in `read_gbq` (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/pandas/__init__.py | 6 +++--- bigframes/session/__init__.py | 8 ++++---- third_party/bigframes_vendored/pandas/io/gbq.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 24b19fa70a2..971d40f801e 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -290,16 +290,16 @@ def read_json( def read_gbq( - query: str, + query_or_table: str, *, index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, ) -> bigframes.dataframe.DataFrame: - _set_default_session_location_if_possible(query) + _set_default_session_location_if_possible(query_or_table) return global_session.with_default_session( bigframes.session.Session.read_gbq, - query, + query_or_table, index_col=index_col, col_order=col_order, max_results=max_results, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 1031fde9b5b..2f001d7d49a 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -262,7 +262,7 @@ def close(self): def read_gbq( self, - query: str, + query_or_table: str, *, index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), @@ -270,9 +270,9 @@ def read_gbq( # Add a verify index argument that fails if the index is not unique. ) -> dataframe.DataFrame: # TODO(b/281571214): Generate prompt to show the progress of read_gbq. - if _is_query(query): + if _is_query(query_or_table): return self._read_gbq_query( - query, + query_or_table, index_col=index_col, col_order=col_order, max_results=max_results, @@ -283,7 +283,7 @@ def read_gbq( # deterministic query so we can avoid serializing if we have a # unique index. return self._read_gbq_table( - query, + query_or_table, index_col=index_col, col_order=col_order, max_results=max_results, diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 95531ff5e82..8919f4ed16d 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -11,7 +11,7 @@ class GBQIOMixin: def read_gbq( self, - query: str, + query_or_table: str, *, index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), @@ -69,7 +69,7 @@ def read_gbq( [5 rows x 3 columns] Args: - query (str): + query_or_table (str): A SQL string to be executed or a BigQuery table to be read. The table must be specified in the format of `project.dataset.tablename` or `dataset.tablename`. From ba2e824f3abbea30010d845e53d645db52504361 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Sat, 21 Oct 2023 01:18:20 +0000 Subject: [PATCH 095/921] refactor: Tweak notebooks to test minimal IAM permissions (#123) * refactor: Tweak notebooks to test minimal IAM permissions * Make IAM permission comment more helpful --- notebooks/dataframes/dataframe.ipynb | 313 ++- .../getting_started/ml_fundamentals.ipynb | 2005 +++++++++-------- .../regression/easy_linear_regression.ipynb | 190 +- 3 files changed, 1425 insertions(+), 1083 deletions(-) diff --git a/notebooks/dataframes/dataframe.ipynb b/notebooks/dataframes/dataframe.ipynb index 85ea61d2810..c6b276af877 100644 --- a/notebooks/dataframes/dataframe.ipynb +++ b/notebooks/dataframes/dataframe.ipynb @@ -35,12 +35,26 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "944f0e4417154e81b6496302fe756465", + "model_id": "11c27813da5c4d2e8108bf4bd9e7e55d", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HTML(value='Query job ac4d1f2b-e9f3-4d95-b78d-57e40eee93fa is RUNNING. \n", "
241156Biscoe46.214.9221.0MALEGentoo penguin (Pygoscelis papua)
121Dream48.116.4199.014.5209.0FEMALEChinstrap penguin (Pygoscelis antarctica)Gentoo penguin (Pygoscelis papua)
209189Biscoe42.718.3196.0MALE35.318.9187.0FEMALEAdelie Penguin (Pygoscelis adeliae)
270279Biscoe37.716.0183.045.114.5215.0FEMALEAdelie Penguin (Pygoscelis adeliae)Gentoo penguin (Pygoscelis papua)
187245Biscoe43.414.4218.0FEMALE49.516.2229.0MALEGentoo penguin (Pygoscelis papua)
343Torgersen37.320.5199.0MALEAdelie Penguin (Pygoscelis adeliae)
\n", "

5 rows × 6 columns

\n", "[5 rows x 6 columns in total]" ], "text/plain": [ - " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", - "penguin_id \n", - "241 Biscoe 46.2 14.9 221.0 \n", - "121 Dream 48.1 16.4 199.0 \n", - "209 Biscoe 42.7 18.3 196.0 \n", - "270 Biscoe 37.7 16.0 183.0 \n", - "187 Biscoe 43.4 14.4 218.0 \n", + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "penguin_id \n", + "156 Biscoe 46.2 14.5 209.0 \n", + "189 Biscoe 35.3 18.9 187.0 \n", + "279 Biscoe 45.1 14.5 215.0 \n", + "245 Biscoe 49.5 16.2 229.0 \n", + "343 Torgersen 37.3 20.5 199.0 \n", "\n", - " sex species \n", - "penguin_id \n", - "241 MALE Gentoo penguin (Pygoscelis papua) \n", - "121 FEMALE Chinstrap penguin (Pygoscelis antarctica) \n", - "209 MALE Adelie Penguin (Pygoscelis adeliae) \n", - "270 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", - "187 FEMALE Gentoo penguin (Pygoscelis papua) \n", + " sex species \n", + "penguin_id \n", + "156 FEMALE Gentoo penguin (Pygoscelis papua) \n", + "189 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", + "279 FEMALE Gentoo penguin (Pygoscelis papua) \n", + "245 MALE Gentoo penguin (Pygoscelis papua) \n", + "343 MALE Adelie Penguin (Pygoscelis adeliae) \n", "\n", "[5 rows x 6 columns]" ] }, - "execution_count": 3, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -763,18 +749,18 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f09b707c5c9540cdae59af3765339b6e", + "model_id": "d6dd794f89724099950dcc927d63d0f5", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HTML(value='Query job 9f0cf977-a895-41fa-9f33-8b5346c62786 is DONE. 31.7 kB processed. \n", "
2415300.01564800.0
1213325.01893800.0
2094075.02795000.0
2703075.02455800.0
1874600.03433775.0
\n", @@ -865,16 +837,16 @@ "text/plain": [ " body_mass_g\n", "penguin_id \n", - "241 5300.0\n", - "121 3325.0\n", - "209 4075.0\n", - "270 3075.0\n", - "187 4600.0\n", + "156 4800.0\n", + "189 3800.0\n", + "279 5000.0\n", + "245 5800.0\n", + "343 3775.0\n", "\n", "[5 rows x 1 columns]" ] }, - "execution_count": 4, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -908,18 +880,18 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "299c0c895e3d4a83a8495924a1966ce0", + "model_id": "380c57dc3fe54fbd8ad2fb23f1e66e37", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HTML(value='Query job f54865db-fdb4-4022-af30-7f282a6b81c0 is DONE. 31.7 kB processed. \n", " \n", " \n", - " scaled_culmen_length_mm\n", - " scaled_culmen_depth_mm\n", - " scaled_flipper_length_mm\n", + " standard_scaled_culmen_length_mm\n", + " standard_scaled_culmen_depth_mm\n", + " standard_scaled_flipper_length_mm\n", " \n", " \n", " penguin_id\n", @@ -1016,153 +1002,153 @@ " \n", " \n", " 0\n", - " -1.364965\n", - " 0.629892\n", - " -1.226537\n", + " -1.344188\n", + " 0.642519\n", + " -1.193942\n", " \n", " \n", " 1\n", - " -0.771824\n", - " 0.984275\n", - " -1.226537\n", + " -0.750047\n", + " 1.005876\n", + " -1.193942\n", " \n", " \n", " 2\n", - " -0.567932\n", - " 0.883023\n", - " -1.226537\n", - " \n", - " \n", - " 3\n", - " 0.470064\n", - " 0.376761\n", - " -0.652517\n", + " -0.545811\n", + " 0.90206\n", + " -1.193942\n", " \n", " \n", " 4\n", - " -1.235216\n", - " -0.180128\n", - " -0.652517\n", + " -1.214219\n", + " -0.188011\n", + " -0.619171\n", " \n", " \n", " 5\n", - " -0.141612\n", - " 0.680518\n", - " -0.652517\n", + " -0.118772\n", + " 0.694427\n", + " -0.619171\n", " \n", " \n", " 6\n", - " 0.544207\n", - " -0.281381\n", - " -0.652517\n", + " 0.568203\n", + " -0.291828\n", + " -0.619171\n", " \n", " \n", " 7\n", - " 1.21149\n", - " 0.629892\n", - " -0.078497\n", + " 1.236611\n", + " 0.642519\n", + " -0.044401\n", " \n", " \n", - " 8\n", - " 1.026133\n", - " 0.933649\n", - " -0.078497\n", + " 9\n", + " -0.675779\n", + " 1.524957\n", + " -0.044401\n", " \n", " \n", " 10\n", - " -0.586468\n", - " 0.883023\n", - " 0.495523\n", + " -0.564378\n", + " 0.90206\n", + " 0.530369\n", " \n", " \n", " 11\n", - " -0.92011\n", - " 0.781771\n", - " -1.154784\n", + " -0.898582\n", + " 0.798243\n", + " -1.122096\n", + " \n", + " \n", + " 12\n", + " -1.26992\n", + " -0.136103\n", + " -1.122096\n", + " \n", + " \n", + " 13\n", + " 0.58677\n", + " 0.071529\n", + " -1.122096\n", " \n", " \n", " 14\n", - " -1.846892\n", - " -0.028249\n", - " -1.154784\n", + " -1.826927\n", + " -0.032287\n", + " -1.122096\n", " \n", " \n", " 15\n", - " -1.290822\n", - " -0.332007\n", - " -1.154784\n", + " -1.26992\n", + " -0.343736\n", + " -1.122096\n", " \n", " \n", " 16\n", - " 0.321779\n", - " 0.073003\n", - " -0.580765\n", + " 0.3454\n", + " 0.071529\n", + " -0.547325\n", " \n", " \n", - " 17\n", - " 1.230026\n", - " 1.136154\n", - " -0.580765\n", + " 18\n", + " -0.768614\n", + " 0.382978\n", + " -0.547325\n", " \n", " \n", - " 18\n", - " -0.79036\n", - " 0.376761\n", - " -0.580765\n", + " 19\n", + " -1.121385\n", + " 0.486795\n", + " -0.547325\n", " \n", " \n", " 20\n", - " 0.4886\n", - " 0.326134\n", - " -0.580765\n", + " 0.512502\n", + " 0.33107\n", + " -0.547325\n", " \n", " \n", " 21\n", - " 1.359775\n", - " 1.034902\n", - " -0.580765\n", + " 1.385146\n", + " 1.057784\n", + " -0.547325\n", + " \n", + " \n", + " 22\n", + " -0.675779\n", + " -0.032287\n", + " -0.547325\n", " \n", " \n", " 24\n", - " 1.044669\n", - " 0.528639\n", - " -0.580765\n", + " 1.069509\n", + " 0.538703\n", + " -0.547325\n", " \n", " \n", " 26\n", - " -0.456718\n", - " 0.680518\n", - " -0.006745\n", + " -0.43441\n", + " 0.694427\n", + " 0.027445\n", " \n", " \n", - " 27\n", - " 1.21149\n", - " 1.237407\n", - " -0.006745\n", + " 28\n", + " 1.923586\n", + " 1.888314\n", + " 0.027445\n", " \n", " \n", - " 29\n", - " 1.378311\n", - " 0.933649\n", - " -0.006745\n", + " 30\n", + " 1.292312\n", + " 0.694427\n", + " 0.027445\n", " \n", " \n", " 31\n", - " -2.013713\n", - " -0.534512\n", - " -1.657052\n", - " \n", - " \n", - " 32\n", - " -1.253751\n", - " 0.478013\n", - " -1.657052\n", - " \n", - " \n", - " 33\n", - " -0.827431\n", - " -0.230754\n", - " -1.657052\n", + " -1.994029\n", + " -0.551368\n", + " -1.62502\n", " \n", " \n", "\n", @@ -1170,67 +1156,67 @@ "[267 rows x 3 columns in total]" ], "text/plain": [ - " scaled_culmen_length_mm scaled_culmen_depth_mm \\\n", - "penguin_id \n", - "0 -1.364965 0.629892 \n", - "1 -0.771824 0.984275 \n", - "2 -0.567932 0.883023 \n", - "3 0.470064 0.376761 \n", - "4 -1.235216 -0.180128 \n", - "5 -0.141612 0.680518 \n", - "6 0.544207 -0.281381 \n", - "7 1.21149 0.629892 \n", - "8 1.026133 0.933649 \n", - "10 -0.586468 0.883023 \n", - "11 -0.92011 0.781771 \n", - "14 -1.846892 -0.028249 \n", - "15 -1.290822 -0.332007 \n", - "16 0.321779 0.073003 \n", - "17 1.230026 1.136154 \n", - "18 -0.79036 0.376761 \n", - "20 0.4886 0.326134 \n", - "21 1.359775 1.034902 \n", - "24 1.044669 0.528639 \n", - "26 -0.456718 0.680518 \n", - "27 1.21149 1.237407 \n", - "29 1.378311 0.933649 \n", - "31 -2.013713 -0.534512 \n", - "32 -1.253751 0.478013 \n", - "33 -0.827431 -0.230754 \n", + " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "0 -1.344188 0.642519 \n", + "1 -0.750047 1.005876 \n", + "2 -0.545811 0.90206 \n", + "4 -1.214219 -0.188011 \n", + "5 -0.118772 0.694427 \n", + "6 0.568203 -0.291828 \n", + "7 1.236611 0.642519 \n", + "9 -0.675779 1.524957 \n", + "10 -0.564378 0.90206 \n", + "11 -0.898582 0.798243 \n", + "12 -1.26992 -0.136103 \n", + "13 0.58677 0.071529 \n", + "14 -1.826927 -0.032287 \n", + "15 -1.26992 -0.343736 \n", + "16 0.3454 0.071529 \n", + "18 -0.768614 0.382978 \n", + "19 -1.121385 0.486795 \n", + "20 0.512502 0.33107 \n", + "21 1.385146 1.057784 \n", + "22 -0.675779 -0.032287 \n", + "24 1.069509 0.538703 \n", + "26 -0.43441 0.694427 \n", + "28 1.923586 1.888314 \n", + "30 1.292312 0.694427 \n", + "31 -1.994029 -0.551368 \n", "\n", - " scaled_flipper_length_mm \n", - "penguin_id \n", - "0 -1.226537 \n", - "1 -1.226537 \n", - "2 -1.226537 \n", - "3 -0.652517 \n", - "4 -0.652517 \n", - "5 -0.652517 \n", - "6 -0.652517 \n", - "7 -0.078497 \n", - "8 -0.078497 \n", - "10 0.495523 \n", - "11 -1.154784 \n", - "14 -1.154784 \n", - "15 -1.154784 \n", - "16 -0.580765 \n", - "17 -0.580765 \n", - "18 -0.580765 \n", - "20 -0.580765 \n", - "21 -0.580765 \n", - "24 -0.580765 \n", - "26 -0.006745 \n", - "27 -0.006745 \n", - "29 -0.006745 \n", - "31 -1.657052 \n", - "32 -1.657052 \n", - "33 -1.657052 \n", + " standard_scaled_flipper_length_mm \n", + "penguin_id \n", + "0 -1.193942 \n", + "1 -1.193942 \n", + "2 -1.193942 \n", + "4 -0.619171 \n", + "5 -0.619171 \n", + "6 -0.619171 \n", + "7 -0.044401 \n", + "9 -0.044401 \n", + "10 0.530369 \n", + "11 -1.122096 \n", + "12 -1.122096 \n", + "13 -1.122096 \n", + "14 -1.122096 \n", + "15 -1.122096 \n", + "16 -0.547325 \n", + "18 -0.547325 \n", + "19 -0.547325 \n", + "20 -0.547325 \n", + "21 -0.547325 \n", + "22 -0.547325 \n", + "24 -0.547325 \n", + "26 0.027445 \n", + "28 0.027445 \n", + "30 0.027445 \n", + "31 -1.62502 \n", "...\n", "\n", "[267 rows x 3 columns]" ] }, - "execution_count": 5, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -1251,18 +1237,18 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6ec4ab3e60844e989dbebad89e7665ca", + "model_id": "74f3c24c0a434e12bf6a56dc4809b501", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HTML(value='Query job a8029b51-2ef1-4acd-9759-d808db954298 is DONE. 31.7 kB processed. \n", - " -1.364965\n", - " 0.629892\n", - " -1.226537\n", + " -1.344188\n", + " 0.642519\n", + " -1.193942\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 1\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.771824\n", - " 0.984275\n", - " -1.226537\n", + " -0.750047\n", + " 1.005876\n", + " -1.193942\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 2\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.567932\n", - " 0.883023\n", - " -1.226537\n", + " -0.545811\n", + " 0.90206\n", + " -1.193942\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 3\n", - " [{'index': 2, 'value': 1.0}]\n", - " 0.470064\n", - " 0.376761\n", - " -0.652517\n", - " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 2, 'value': 1.0}]\n", - " \n", - " \n", " 4\n", " [{'index': 2, 'value': 1.0}]\n", - " -1.235216\n", - " -0.180128\n", - " -0.652517\n", + " -1.214219\n", + " -0.188011\n", + " -0.619171\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 5\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.141612\n", - " 0.680518\n", - " -0.652517\n", + " -0.118772\n", + " 0.694427\n", + " -0.619171\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 6\n", " [{'index': 2, 'value': 1.0}]\n", - " 0.544207\n", - " -0.281381\n", - " -0.652517\n", + " 0.568203\n", + " -0.291828\n", + " -0.619171\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", " 7\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.21149\n", - " 0.629892\n", - " -0.078497\n", + " 1.236611\n", + " 0.642519\n", + " -0.044401\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 8\n", + " 9\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.026133\n", - " 0.933649\n", - " -0.078497\n", + " -0.675779\n", + " 1.524957\n", + " -0.044401\n", " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 10\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.586468\n", - " 0.883023\n", - " 0.495523\n", + " -0.564378\n", + " 0.90206\n", + " 0.530369\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 11\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.92011\n", - " 0.781771\n", - " -1.154784\n", + " -0.898582\n", + " 0.798243\n", + " -1.122096\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", + " 12\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.26992\n", + " -0.136103\n", + " -1.122096\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", + " \n", + " \n", + " 13\n", + " [{'index': 2, 'value': 1.0}]\n", + " 0.58677\n", + " 0.071529\n", + " -1.122096\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", + " \n", + " \n", " 14\n", " [{'index': 2, 'value': 1.0}]\n", - " -1.846892\n", - " -0.028249\n", - " -1.154784\n", + " -1.826927\n", + " -0.032287\n", + " -1.122096\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 15\n", " [{'index': 2, 'value': 1.0}]\n", - " -1.290822\n", - " -0.332007\n", - " -1.154784\n", + " -1.26992\n", + " -0.343736\n", + " -1.122096\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 16\n", " [{'index': 2, 'value': 1.0}]\n", - " 0.321779\n", - " 0.073003\n", - " -0.580765\n", + " 0.3454\n", + " 0.071529\n", + " -0.547325\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 17\n", + " 18\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.230026\n", - " 1.136154\n", - " -0.580765\n", + " -0.768614\n", + " 0.382978\n", + " -0.547325\n", " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 18\n", + " 19\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.79036\n", - " 0.376761\n", - " -0.580765\n", + " -1.121385\n", + " 0.486795\n", + " -0.547325\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 20\n", " [{'index': 2, 'value': 1.0}]\n", - " 0.4886\n", - " 0.326134\n", - " -0.580765\n", + " 0.512502\n", + " 0.33107\n", + " -0.547325\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", " 21\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.359775\n", - " 1.034902\n", - " -0.580765\n", + " 1.385146\n", + " 1.057784\n", + " -0.547325\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", + " 22\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.675779\n", + " -0.032287\n", + " -0.547325\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", + " \n", + " \n", " 24\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.044669\n", - " 0.528639\n", - " -0.580765\n", + " 1.069509\n", + " 0.538703\n", + " -0.547325\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", " 26\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.456718\n", - " 0.680518\n", - " -0.006745\n", + " -0.43441\n", + " 0.694427\n", + " 0.027445\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 27\n", + " 28\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.21149\n", - " 1.237407\n", - " -0.006745\n", + " 1.923586\n", + " 1.888314\n", + " 0.027445\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 29\n", + " 30\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.378311\n", - " 0.933649\n", - " -0.006745\n", + " 1.292312\n", + " 0.694427\n", + " 0.027445\n", " [{'index': 3, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", " 31\n", " [{'index': 2, 'value': 1.0}]\n", - " -2.013713\n", - " -0.534512\n", - " -1.657052\n", - " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", - " \n", - " \n", - " 32\n", - " [{'index': 2, 'value': 1.0}]\n", - " -1.253751\n", - " 0.478013\n", - " -1.657052\n", - " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", - " \n", - " \n", - " 33\n", - " [{'index': 2, 'value': 1.0}]\n", - " -0.827431\n", - " -0.230754\n", - " -1.657052\n", + " -1.994029\n", + " -0.551368\n", + " -1.62502\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", @@ -1966,95 +1980,123 @@ "[267 rows x 6 columns in total]" ], "text/plain": [ - " onehotencoded_island scaled_culmen_length_mm \\\n", - "penguin_id \n", - "0 [{'index': 2, 'value': 1.0}] -1.364965 \n", - "1 [{'index': 2, 'value': 1.0}] -0.771824 \n", - "2 [{'index': 2, 'value': 1.0}] -0.567932 \n", - "3 [{'index': 2, 'value': 1.0}] 0.470064 \n", - "4 [{'index': 2, 'value': 1.0}] -1.235216 \n", - "5 [{'index': 2, 'value': 1.0}] -0.141612 \n", - "6 [{'index': 2, 'value': 1.0}] 0.544207 \n", - "7 [{'index': 2, 'value': 1.0}] 1.21149 \n", - "8 [{'index': 2, 'value': 1.0}] 1.026133 \n", - "10 [{'index': 2, 'value': 1.0}] -0.586468 \n", - "11 [{'index': 2, 'value': 1.0}] -0.92011 \n", - "14 [{'index': 2, 'value': 1.0}] -1.846892 \n", - "15 [{'index': 2, 'value': 1.0}] -1.290822 \n", - "16 [{'index': 2, 'value': 1.0}] 0.321779 \n", - "17 [{'index': 2, 'value': 1.0}] 1.230026 \n", - "18 [{'index': 2, 'value': 1.0}] -0.79036 \n", - "20 [{'index': 2, 'value': 1.0}] 0.4886 \n", - "21 [{'index': 2, 'value': 1.0}] 1.359775 \n", - "24 [{'index': 2, 'value': 1.0}] 1.044669 \n", - "26 [{'index': 2, 'value': 1.0}] -0.456718 \n", - "27 [{'index': 2, 'value': 1.0}] 1.21149 \n", - "29 [{'index': 2, 'value': 1.0}] 1.378311 \n", - "31 [{'index': 2, 'value': 1.0}] -2.013713 \n", - "32 [{'index': 2, 'value': 1.0}] -1.253751 \n", - "33 [{'index': 2, 'value': 1.0}] -0.827431 \n", + " onehotencoded_island standard_scaled_culmen_length_mm \\\n", + "penguin_id \n", + "0 [{'index': 2, 'value': 1.0}] -1.344188 \n", + "1 [{'index': 2, 'value': 1.0}] -0.750047 \n", + "2 [{'index': 2, 'value': 1.0}] -0.545811 \n", + "4 [{'index': 2, 'value': 1.0}] -1.214219 \n", + "5 [{'index': 2, 'value': 1.0}] -0.118772 \n", + "6 [{'index': 2, 'value': 1.0}] 0.568203 \n", + "7 [{'index': 2, 'value': 1.0}] 1.236611 \n", + "9 [{'index': 2, 'value': 1.0}] -0.675779 \n", + "10 [{'index': 2, 'value': 1.0}] -0.564378 \n", + "11 [{'index': 2, 'value': 1.0}] -0.898582 \n", + "12 [{'index': 2, 'value': 1.0}] -1.26992 \n", + "13 [{'index': 2, 'value': 1.0}] 0.58677 \n", + "14 [{'index': 2, 'value': 1.0}] -1.826927 \n", + "15 [{'index': 2, 'value': 1.0}] -1.26992 \n", + "16 [{'index': 2, 'value': 1.0}] 0.3454 \n", + "18 [{'index': 2, 'value': 1.0}] -0.768614 \n", + "19 [{'index': 2, 'value': 1.0}] -1.121385 \n", + "20 [{'index': 2, 'value': 1.0}] 0.512502 \n", + "21 [{'index': 2, 'value': 1.0}] 1.385146 \n", + "22 [{'index': 2, 'value': 1.0}] -0.675779 \n", + "24 [{'index': 2, 'value': 1.0}] 1.069509 \n", + "26 [{'index': 2, 'value': 1.0}] -0.43441 \n", + "28 [{'index': 2, 'value': 1.0}] 1.923586 \n", + "30 [{'index': 2, 'value': 1.0}] 1.292312 \n", + "31 [{'index': 2, 'value': 1.0}] -1.994029 \n", "\n", - " scaled_culmen_depth_mm scaled_flipper_length_mm \\\n", - "penguin_id \n", - "0 0.629892 -1.226537 \n", - "1 0.984275 -1.226537 \n", - "2 0.883023 -1.226537 \n", - "3 0.376761 -0.652517 \n", - "4 -0.180128 -0.652517 \n", - "5 0.680518 -0.652517 \n", - "6 -0.281381 -0.652517 \n", - "7 0.629892 -0.078497 \n", - "8 0.933649 -0.078497 \n", - "10 0.883023 0.495523 \n", - "11 0.781771 -1.154784 \n", - "14 -0.028249 -1.154784 \n", - "15 -0.332007 -1.154784 \n", - "16 0.073003 -0.580765 \n", - "17 1.136154 -0.580765 \n", - "18 0.376761 -0.580765 \n", - "20 0.326134 -0.580765 \n", - "21 1.034902 -0.580765 \n", - "24 0.528639 -0.580765 \n", - "26 0.680518 -0.006745 \n", - "27 1.237407 -0.006745 \n", - "29 0.933649 -0.006745 \n", - "31 -0.534512 -1.657052 \n", - "32 0.478013 -1.657052 \n", - "33 -0.230754 -1.657052 \n", + " standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "0 0.642519 \n", + "1 1.005876 \n", + "2 0.90206 \n", + "4 -0.188011 \n", + "5 0.694427 \n", + "6 -0.291828 \n", + "7 0.642519 \n", + "9 1.524957 \n", + "10 0.90206 \n", + "11 0.798243 \n", + "12 -0.136103 \n", + "13 0.071529 \n", + "14 -0.032287 \n", + "15 -0.343736 \n", + "16 0.071529 \n", + "18 0.382978 \n", + "19 0.486795 \n", + "20 0.33107 \n", + "21 1.057784 \n", + "22 -0.032287 \n", + "24 0.538703 \n", + "26 0.694427 \n", + "28 1.888314 \n", + "30 0.694427 \n", + "31 -0.551368 \n", "\n", - " onehotencoded_sex onehotencoded_species \n", - "penguin_id \n", - "0 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "1 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "2 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "3 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "4 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "5 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "6 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "7 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "8 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "10 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "11 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "14 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "15 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "16 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "17 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "18 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "20 [{'index': 2, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "21 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "24 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "26 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "27 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "29 [{'index': 3, 'value': 1.0}] [{'index': 2, 'value': 1.0}] \n", - "31 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "32 [{'index': 3, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", - "33 [{'index': 2, 'value': 1.0}] [{'index': 1, 'value': 1.0}] \n", + " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", + "penguin_id \n", + "0 -1.193942 [{'index': 2, 'value': 1.0}] \n", + "1 -1.193942 [{'index': 3, 'value': 1.0}] \n", + "2 -1.193942 [{'index': 3, 'value': 1.0}] \n", + "4 -0.619171 [{'index': 2, 'value': 1.0}] \n", + "5 -0.619171 [{'index': 3, 'value': 1.0}] \n", + "6 -0.619171 [{'index': 2, 'value': 1.0}] \n", + "7 -0.044401 [{'index': 2, 'value': 1.0}] \n", + "9 -0.044401 [{'index': 3, 'value': 1.0}] \n", + "10 0.530369 [{'index': 3, 'value': 1.0}] \n", + "11 -1.122096 [{'index': 3, 'value': 1.0}] \n", + "12 -1.122096 [{'index': 2, 'value': 1.0}] \n", + "13 -1.122096 [{'index': 2, 'value': 1.0}] \n", + "14 -1.122096 [{'index': 2, 'value': 1.0}] \n", + "15 -1.122096 [{'index': 2, 'value': 1.0}] \n", + "16 -0.547325 [{'index': 2, 'value': 1.0}] \n", + "18 -0.547325 [{'index': 3, 'value': 1.0}] \n", + "19 -0.547325 [{'index': 3, 'value': 1.0}] \n", + "20 -0.547325 [{'index': 2, 'value': 1.0}] \n", + "21 -0.547325 [{'index': 3, 'value': 1.0}] \n", + "22 -0.547325 [{'index': 2, 'value': 1.0}] \n", + "24 -0.547325 [{'index': 3, 'value': 1.0}] \n", + "26 0.027445 [{'index': 3, 'value': 1.0}] \n", + "28 0.027445 [{'index': 3, 'value': 1.0}] \n", + "30 0.027445 [{'index': 3, 'value': 1.0}] \n", + "31 -1.62502 [{'index': 2, 'value': 1.0}] \n", + "\n", + " onehotencoded_species \n", + "penguin_id \n", + "0 [{'index': 1, 'value': 1.0}] \n", + "1 [{'index': 1, 'value': 1.0}] \n", + "2 [{'index': 1, 'value': 1.0}] \n", + "4 [{'index': 1, 'value': 1.0}] \n", + "5 [{'index': 1, 'value': 1.0}] \n", + "6 [{'index': 2, 'value': 1.0}] \n", + "7 [{'index': 2, 'value': 1.0}] \n", + "9 [{'index': 1, 'value': 1.0}] \n", + "10 [{'index': 1, 'value': 1.0}] \n", + "11 [{'index': 1, 'value': 1.0}] \n", + "12 [{'index': 1, 'value': 1.0}] \n", + "13 [{'index': 2, 'value': 1.0}] \n", + "14 [{'index': 1, 'value': 1.0}] \n", + "15 [{'index': 1, 'value': 1.0}] \n", + "16 [{'index': 2, 'value': 1.0}] \n", + "18 [{'index': 1, 'value': 1.0}] \n", + "19 [{'index': 1, 'value': 1.0}] \n", + "20 [{'index': 2, 'value': 1.0}] \n", + "21 [{'index': 2, 'value': 1.0}] \n", + "22 [{'index': 1, 'value': 1.0}] \n", + "24 [{'index': 2, 'value': 1.0}] \n", + "26 [{'index': 1, 'value': 1.0}] \n", + "28 [{'index': 2, 'value': 1.0}] \n", + "30 [{'index': 2, 'value': 1.0}] \n", + "31 [{'index': 1, 'value': 1.0}] \n", "...\n", "\n", "[267 rows x 6 columns]" ] }, - "execution_count": 7, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -2096,18 +2138,32 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b7c22c0858914b46951adde174b43e25", + "model_id": "5db4c5c80ba4417db151aa561dab5ee7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job ceced0cc-13a7-4b14-b42c-4d5f69e7e49a is RUNNING. \n", " \n", " \n", - " 9\n", - " 4295.335461\n", + " 3\n", + " 3394.118128\n", " \n", " \n", - " 12\n", - " 3338.44131\n", + " 8\n", + " 4048.685642\n", " \n", " \n", - " 13\n", - " 3201.820204\n", + " 17\n", + " 3976.454093\n", " \n", " \n", - " 19\n", - " 3982.814079\n", + " 23\n", + " 3541.582194\n", " \n", " \n", - " 22\n", - " 3538.610664\n", + " 25\n", + " 4032.844186\n", " \n", " \n", - " 23\n", - " 3613.50305\n", + " 27\n", + " 4118.351772\n", " \n", " \n", - " 25\n", - " 4009.759444\n", + " 29\n", + " 4087.767826\n", " \n", " \n", - " 28\n", - " 4240.515635\n", + " 34\n", + " 3183.755249\n", " \n", " \n", - " 30\n", - " 4028.904195\n", + " 35\n", + " 3418.802274\n", " \n", " \n", - " 38\n", - " 4206.810346\n", + " 39\n", + " 3519.186468\n", " \n", " \n", - " 41\n", - " 3736.225488\n", + " 51\n", + " 3398.135365\n", " \n", " \n", - " 45\n", - " 4383.368544\n", + " 52\n", + " 3223.615957\n", " \n", " \n", - " 49\n", - " 3125.772789\n", + " 60\n", + " 3445.014718\n", " \n", " \n", - " 50\n", - " 3149.28765\n", + " 61\n", + " 3505.638864\n", " \n", " \n", - " 62\n", - " 3531.69488\n", + " 64\n", + " 3515.905786\n", " \n", " \n", " 65\n", - " 4073.900616\n", + " 4028.363185\n", " \n", " \n", - " 66\n", - " 4160.810162\n", + " 67\n", + " 4159.993943\n", " \n", " \n", " 83\n", - " 3272.396279\n", + " 3348.16883\n", " \n", " \n", - " 87\n", - " 3989.364493\n", + " 85\n", + " 3485.050273\n", " \n", " \n", - " 92\n", - " 4240.495294\n", + " 93\n", + " 4172.874548\n", " \n", " \n", - " 98\n", - " 3911.455384\n", + " 104\n", + " 3299.302424\n", " \n", " \n", - " 104\n", - " 3271.202866\n", + " 105\n", + " 3515.687917\n", " \n", " \n", - " 114\n", - " 3244.728549\n", + " 108\n", + " 3405.224618\n", " \n", " \n", - " 115\n", - " 3737.374636\n", + " 113\n", + " 4209.140425\n", " \n", " \n", - " 118\n", - " 3485.95604\n", + " 130\n", + " 4197.905737\n", " \n", " \n", "\n", @@ -2306,37 +2362,37 @@ "text/plain": [ " predicted_body_mass_g\n", "penguin_id \n", - "9 4295.335461\n", - "12 3338.44131\n", - "13 3201.820204\n", - "19 3982.814079\n", - "22 3538.610664\n", - "23 3613.50305\n", - "25 4009.759444\n", - "28 4240.515635\n", - "30 4028.904195\n", - "38 4206.810346\n", - "41 3736.225488\n", - "45 4383.368544\n", - "49 3125.772789\n", - "50 3149.28765\n", - "62 3531.69488\n", - "65 4073.900616\n", - "66 4160.810162\n", - "83 3272.396279\n", - "87 3989.364493\n", - "92 4240.495294\n", - "98 3911.455384\n", - "104 3271.202866\n", - "114 3244.728549\n", - "115 3737.374636\n", - "118 3485.95604\n", + "3 3394.118128\n", + "8 4048.685642\n", + "17 3976.454093\n", + "23 3541.582194\n", + "25 4032.844186\n", + "27 4118.351772\n", + "29 4087.767826\n", + "34 3183.755249\n", + "35 3418.802274\n", + "39 3519.186468\n", + "51 3398.135365\n", + "52 3223.615957\n", + "60 3445.014718\n", + "61 3505.638864\n", + "64 3515.905786\n", + "65 4028.363185\n", + "67 4159.993943\n", + "83 3348.16883\n", + "85 3485.050273\n", + "93 4172.874548\n", + "104 3299.302424\n", + "105 3515.687917\n", + "108 3405.224618\n", + "113 4209.140425\n", + "130 4197.905737\n", "...\n", "\n", "[67 rows x 1 columns]" ] }, - "execution_count": 8, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -2367,18 +2423,32 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "fa6bd24b44cb42ec946e262ac2f25d09", + "model_id": "d7a16e04253a42b7a5ce247d8f63b656", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='Query job 6f19614c-82c0-4f8b-b74b-9d91a894efdd is RUNNING. \n", " \n", " \n", - " 9\n", - " 4\n", + " 3\n", + " 3\n", " \n", " \n", - " 12\n", - " 4\n", + " 8\n", + " 3\n", " \n", " \n", - " 13\n", - " 2\n", + " 17\n", + " 3\n", " \n", " \n", - " 19\n", - " 4\n", + " 23\n", + " 1\n", " \n", " \n", - " 22\n", - " 4\n", + " 25\n", + " 3\n", " \n", " \n", - " 23\n", - " 4\n", + " 27\n", + " 3\n", " \n", " \n", - " 25\n", - " 2\n", + " 29\n", + " 3\n", " \n", " \n", - " 28\n", - " 2\n", + " 34\n", + " 3\n", " \n", " \n", - " 30\n", - " 2\n", + " 35\n", + " 1\n", " \n", " \n", - " 38\n", - " 4\n", + " 39\n", + " 3\n", " \n", " \n", - " 41\n", - " 4\n", + " 51\n", + " 1\n", " \n", " \n", - " 45\n", - " 2\n", + " 52\n", + " 3\n", " \n", " \n", - " 49\n", - " 4\n", + " 60\n", + " 3\n", " \n", " \n", - " 50\n", - " 2\n", + " 61\n", + " 3\n", " \n", " \n", - " 62\n", - " 4\n", + " 64\n", + " 1\n", " \n", " \n", " 65\n", - " 4\n", + " 1\n", " \n", " \n", - " 66\n", - " 2\n", + " 67\n", + " 3\n", " \n", " \n", " 83\n", - " 2\n", + " 3\n", " \n", " \n", - " 87\n", - " 2\n", + " 85\n", + " 1\n", " \n", " \n", - " 92\n", - " 2\n", + " 93\n", + " 1\n", " \n", " \n", - " 98\n", - " 4\n", + " 104\n", + " 3\n", " \n", " \n", - " 104\n", - " 2\n", + " 105\n", + " 1\n", " \n", " \n", - " 114\n", - " 2\n", + " 108\n", + " 3\n", " \n", " \n", - " 115\n", - " 2\n", + " 113\n", + " 3\n", " \n", " \n", - " 118\n", - " 4\n", + " 130\n", + " 1\n", " \n", " \n", "\n", @@ -2577,37 +2647,37 @@ "text/plain": [ " CENTROID_ID\n", "penguin_id \n", - "9 4\n", - "12 4\n", - "13 2\n", - "19 4\n", - "22 4\n", - "23 4\n", - "25 2\n", - "28 2\n", - "30 2\n", - "38 4\n", - "41 4\n", - "45 2\n", - "49 4\n", - "50 2\n", - "62 4\n", - "65 4\n", - "66 2\n", - "83 2\n", - "87 2\n", - "92 2\n", - "98 4\n", - "104 2\n", - "114 2\n", - "115 2\n", - "118 4\n", + "3 3\n", + "8 3\n", + "17 3\n", + "23 1\n", + "25 3\n", + "27 3\n", + "29 3\n", + "34 3\n", + "35 1\n", + "39 3\n", + "51 1\n", + "52 3\n", + "60 3\n", + "61 3\n", + "64 1\n", + "65 1\n", + "67 3\n", + "83 3\n", + "85 1\n", + "93 1\n", + "104 3\n", + "105 1\n", + "108 3\n", + "113 3\n", + "130 1\n", "...\n", "\n", "[67 rows x 1 columns]" ] }, - "execution_count": 9, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -2634,7 +2704,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -2651,7 +2721,7 @@ " ('linreg', LinearRegression())])" ] }, - "execution_count": 10, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -2678,18 +2748,18 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f0465986682742af92759c3f5fce96e0", + "model_id": "887bf58cebf14bdba95db828390fd33d", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HTML(value='Query job c7d094cb-cc51-4f11-8887-b169c23aceb2 is DONE. 32.3 kB processed. \n", " \n", " \n", - " 9\n", - " 4295.328991\n", + " 3\n", + " 3394.116212\n", " \n", " \n", - " 12\n", - " 3338.434943\n", + " 8\n", + " 4048.683645\n", " \n", " \n", - " 13\n", - " 3201.813783\n", + " 17\n", + " 3976.452358\n", " \n", " \n", - " 19\n", - " 3982.807707\n", + " 23\n", + " 3541.580346\n", " \n", " \n", - " 22\n", - " 3538.604385\n", + " 25\n", + " 4032.842027\n", " \n", " \n", - " 23\n", - " 3613.496641\n", + " 27\n", + " 4118.34983\n", " \n", " \n", - " 25\n", - " 4009.753161\n", + " 29\n", + " 4087.765797\n", " \n", " \n", - " 28\n", - " 4240.509087\n", + " 34\n", + " 3183.75379\n", " \n", " \n", - " 30\n", - " 4028.897875\n", + " 35\n", + " 3418.800633\n", " \n", " \n", - " 38\n", - " 4206.80377\n", + " 39\n", + " 3519.18471\n", " \n", " \n", - " 41\n", - " 3736.219256\n", + " 51\n", + " 3398.133564\n", " \n", " \n", - " 45\n", - " 4383.362136\n", + " 52\n", + " 3223.614107\n", " \n", " \n", - " 49\n", - " 3125.766474\n", + " 60\n", + " 3445.012713\n", " \n", " \n", - " 50\n", - " 3149.281322\n", + " 61\n", + " 3505.637004\n", " \n", " \n", - " 62\n", - " 3531.688645\n", + " 64\n", + " 3515.903779\n", " \n", " \n", " 65\n", - " 4073.894238\n", + " 4028.361259\n", " \n", " \n", - " 66\n", - " 4160.803738\n", + " 67\n", + " 4159.991956\n", " \n", " \n", " 83\n", - " 3272.389735\n", + " 3348.167212\n", " \n", " \n", - " 87\n", - " 3989.358086\n", + " 85\n", + " 3485.048557\n", " \n", " \n", - " 92\n", - " 4240.488891\n", + " 93\n", + " 4172.872284\n", " \n", " \n", - " 98\n", - " 3911.449023\n", + " 104\n", + " 3299.300454\n", " \n", " \n", - " 104\n", - " 3271.196535\n", + " 105\n", + " 3515.68617\n", " \n", " \n", - " 114\n", - " 3244.722283\n", + " 108\n", + " 3405.222757\n", " \n", " \n", - " 115\n", - " 3737.368277\n", + " 113\n", + " 4209.13832\n", " \n", " \n", - " 118\n", - " 3485.949702\n", + " 130\n", + " 4197.90382\n", " \n", " \n", "\n", @@ -2888,37 +2972,37 @@ "text/plain": [ " predicted_body_mass_g\n", "penguin_id \n", - "9 4295.328991\n", - "12 3338.434943\n", - "13 3201.813783\n", - "19 3982.807707\n", - "22 3538.604385\n", - "23 3613.496641\n", - "25 4009.753161\n", - "28 4240.509087\n", - "30 4028.897875\n", - "38 4206.80377\n", - "41 3736.219256\n", - "45 4383.362136\n", - "49 3125.766474\n", - "50 3149.281322\n", - "62 3531.688645\n", - "65 4073.894238\n", - "66 4160.803738\n", - "83 3272.389735\n", - "87 3989.358086\n", - "92 4240.488891\n", - "98 3911.449023\n", - "104 3271.196535\n", - "114 3244.722283\n", - "115 3737.368277\n", - "118 3485.949702\n", + "3 3394.116212\n", + "8 4048.683645\n", + "17 3976.452358\n", + "23 3541.580346\n", + "25 4032.842027\n", + "27 4118.34983\n", + "29 4087.765797\n", + "34 3183.75379\n", + "35 3418.800633\n", + "39 3519.18471\n", + "51 3398.133564\n", + "52 3223.614107\n", + "60 3445.012713\n", + "61 3505.637004\n", + "64 3515.903779\n", + "65 4028.361259\n", + "67 4159.991956\n", + "83 3348.167212\n", + "85 3485.048557\n", + "93 4172.872284\n", + "104 3299.300454\n", + "105 3515.68617\n", + "108 3405.222757\n", + "113 4209.13832\n", + "130 4197.90382\n", "...\n", "\n", "[67 rows x 1 columns]" ] }, - "execution_count": 11, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -2950,18 +3034,18 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e6409bce719940f4ae326a8b18871d9b", + "model_id": "2d32081be31f44abb8de67e2209d76cd", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HTML(value='Query job a427bad2-9875-453f-ad2a-1eefaf085657 is DONE. 32.3 kB processed. \n", " \n", " 0\n", - " 241.640738\n", - " 90117.84266\n", - " 0.005652\n", - " 200.718678\n", - " 0.8727\n", - " 0.878359\n", + " 229.48269\n", + " 82962.794947\n", + " 0.004248\n", + " 206.728384\n", + " 0.88633\n", + " 0.892953\n", " \n", " \n", "\n", @@ -3069,15 +3167,15 @@ ], "text/plain": [ " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - "0 241.640738 90117.84266 0.005652 \n", + "0 229.48269 82962.794947 0.004248 \n", "\n", " median_absolute_error r2_score explained_variance \n", - "0 200.718678 0.8727 0.878359 \n", + "0 206.728384 0.88633 0.892953 \n", "\n", "[1 rows x 6 columns]" ] }, - "execution_count": 12, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -3097,18 +3195,18 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e56e2cc197894ee9acc448a8c12e8a30", + "model_id": "f32692d89f00406499f4ea5aa55268fb", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HTML(value='Query job 929c826c-1051-47fc-9256-546f4ef11c32 is DONE. 31.7 kB processed. Date: Fri, 20 Oct 2023 22:27:22 -0700 Subject: [PATCH 096/921] docs: add runnable code samples for reading methods (#125) * docs: add runnable and testable I/O code samples * docs: add runnable and testable reading methods code snippets * fix: assign a df and show the first 2 rows * address comments --- bigframes/session/__init__.py | 100 ++++++++++++++++++ .../bigframes_vendored/pandas/io/gbq.py | 24 +++-- .../bigframes_vendored/pandas/io/parquet.py | 14 +++ .../pandas/io/parsers/readers.py | 30 +++++- .../bigframes_vendored/pandas/io/pickle.py | 18 ++++ 5 files changed, 179 insertions(+), 7 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 2f001d7d49a..5ec3da1a5a4 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -342,6 +342,51 @@ def read_gbq_query( ``row_number() over ()`` if there is no natural unique index or you want to preserve ordering. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Simple query input: + + >>> df = bpd.read_gbq_query(''' + ... SELECT + ... pitcherFirstName, + ... pitcherLastName, + ... pitchSpeed, + ... FROM `bigquery-public-data.baseball.games_wide` + ... ''') + >>> df.head(2) + pitcherFirstName pitcherLastName pitchSpeed + 0 0 + 1 0 + + [2 rows x 3 columns] + + Preserve ordering in a query input. + + >>> df = bpd.read_gbq_query(''' + ... SELECT + ... -- Instead of an ORDER BY clause on the query, use + ... -- ROW_NUMBER() to create an ordered DataFrame. + ... ROW_NUMBER() OVER (ORDER BY AVG(pitchSpeed) DESC) + ... AS rowindex, + ... + ... pitcherFirstName, + ... pitcherLastName, + ... AVG(pitchSpeed) AS averagePitchSpeed + ... FROM `bigquery-public-data.baseball.games_wide` + ... WHERE year = 2016 + ... GROUP BY pitcherFirstName, pitcherLastName + ... ''', index_col="rowindex") + >>> df.head(2) + pitcherFirstName pitcherLastName averagePitchSpeed + rowindex + 1 Albertin Chapman 96.514113 + 2 Zachary Britton 94.591039 + + [2 rows x 3 columns] + See also: :meth:`Session.read_gbq`. """ # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so @@ -405,6 +450,25 @@ def read_gbq_table( ) -> dataframe.DataFrame: """Turn a BigQuery table into a DataFrame. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Read a whole table, with arbitrary ordering or ordering corresponding to the primary key(s). + + >>> df = bpd.read_gbq_table("bigquery-public-data.ml_datasets.penguins") + >>> df.head(2) + species island culmen_length_mm \\ + 0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 + 1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 + + culmen_depth_mm flipper_length_mm body_mass_g sex + 0 18.4 184.0 3475.0 FEMALE + 1 19.1 184.0 4650.0 MALE + + [2 rows x 7 columns] + See also: :meth:`Session.read_gbq`. """ # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so @@ -792,6 +856,16 @@ def _read_ibis( def read_gbq_model(self, model_name: str): """Loads a BigQuery ML model from BigQuery. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Read an existing BigQuery ML model. + + >>> model_name = "bigframes-dev.bqml_tutorial.penguins_model" + >>> model = bpd.read_gbq_model(model_name) + Args: model_name (str): the model's name in BigQuery in the format @@ -815,6 +889,22 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame The pandas DataFrame will be persisted as a temporary BigQuery table, which can be automatically recycled after the Session is closed. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pandas as pd + >>> bpd.options.display.progress_bar = None + + >>> d = {'col1': [1, 2], 'col2': [3, 4]} + >>> pandas_df = pd.DataFrame(data=d) + >>> df = bpd.read_pandas(pandas_df) + >>> df + col1 col2 + 0 1 3 + 1 2 4 + + [2 rows x 2 columns] + Args: pandas_dataframe (pandas.DataFrame): a pandas DataFrame object to be loaded. @@ -1365,6 +1455,16 @@ def read_gbq_function( The return type of the function must be explicitly specified in the function's original definition even if not otherwise required. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> function_name = "bqutil.fn.cw_lower_case_ascii_only" + >>> func = bpd.read_gbq_function(function_name=function_name) + >>> func.bigframes_remote_function + 'bqutil.fn.cw_lower_case_ascii_only' + Args: function_name (str): the function's name in BigQuery in the format diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 8919f4ed16d..575c5016187 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -42,9 +42,23 @@ def read_gbq( >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None + If the input is a table ID: + + >>> df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + >>> df.head(2) + species island culmen_length_mm \\ + 0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 + 1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 + + culmen_depth_mm flipper_length_mm body_mass_g sex + 0 18.4 184.0 3475.0 FEMALE + 1 19.1 184.0 4650.0 MALE + + [2 rows x 7 columns] + Preserve ordering in a query input. - >>> bpd.read_gbq(''' + >>> df = bpd.read_gbq(''' ... SELECT ... -- Instead of an ORDER BY clause on the query, use ... -- ROW_NUMBER() to create an ordered DataFrame. @@ -57,16 +71,14 @@ def read_gbq( ... FROM `bigquery-public-data.baseball.games_wide` ... WHERE year = 2016 ... GROUP BY pitcherFirstName, pitcherLastName - ... ''', index_col="rowindex").head(n=5) + ... ''', index_col="rowindex") + >>> df.head(2) pitcherFirstName pitcherLastName averagePitchSpeed rowindex 1 Albertin Chapman 96.514113 2 Zachary Britton 94.591039 - 3 Trevor Rosenthal 94.213953 - 4 Jose Torres 94.103448 - 5 Tayron Guerrero 93.863636 - [5 rows x 3 columns] + [2 rows x 3 columns] Args: query_or_table (str): diff --git a/third_party/bigframes_vendored/pandas/io/parquet.py b/third_party/bigframes_vendored/pandas/io/parquet.py index 9aed9af5a82..f97bd386a40 100644 --- a/third_party/bigframes_vendored/pandas/io/parquet.py +++ b/third_party/bigframes_vendored/pandas/io/parquet.py @@ -17,6 +17,20 @@ def read_parquet( Instead, set a serialized index column as the index and sort by that in the resulting DataFrame. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> gcs_path = "gs://cloud-samples-data/bigquery/us-states/us-states.parquet" + >>> df = bpd.read_parquet(path=gcs_path) + >>> df.head(2) + name post_abbr + 0 Alabama AL + 1 Alaska AK + + [2 rows x 2 columns] + Args: path (str): Local or Cloud Storage path to Parquet file. diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py index d19a92ecdf8..e8ed6182a6f 100644 --- a/third_party/bigframes_vendored/pandas/io/parsers/readers.py +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -55,6 +55,20 @@ def read_csv( file. Instead, set a serialized index column as the index and sort by that in the resulting DataFrame. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> gcs_path = "gs://cloud-samples-data/bigquery/us-states/us-states.csv" + >>> df = bpd.read_csv(filepath_or_buffer=gcs_path) + >>> df.head(2) + name post_abbr + 0 Alabama AL + 1 Alaska AK + + [2 rows x 2 columns] + Args: filepath_or_buffer (str): A local or Google Cloud Storage (`gs://`) path with `engine="bigquery"` @@ -64,7 +78,7 @@ def read_csv( can be any ISO-8859-1 single-byte character. To use a character in the range 128-255, you must encode the character as UTF-8. Both engines support `sep="\t"` to specify tab character as separator. Default engine supports - having any number of spaces as separator by specifying `sep="\s+"`. Separators + having any number of spaces as separator by specifying `sep="\\s+"`. Separators longer than 1 character are interpreted as regular expressions by the default engine. BigQuery engine only supports single character separators. header (Optional[int], default 0): @@ -146,6 +160,20 @@ def read_json( file. Instead, set a serialized index column as the index and sort by that in the resulting DataFrame. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> gcs_path = "gs://bigframes-dev-testing/sample1.json" + >>> df = bpd.read_json(path_or_buf=gcs_path, lines=True, orient="records") + >>> df.head(2) + id name + 0 1 Alice + 1 2 Bob + + [2 rows x 2 columns] + Args: path_or_buf (a valid JSON str, path object or file-like object): A local or Google Cloud Storage (`gs://`) path with `engine="bigquery"` diff --git a/third_party/bigframes_vendored/pandas/io/pickle.py b/third_party/bigframes_vendored/pandas/io/pickle.py index 71b31956a07..053ba4871c1 100644 --- a/third_party/bigframes_vendored/pandas/io/pickle.py +++ b/third_party/bigframes_vendored/pandas/io/pickle.py @@ -25,6 +25,24 @@ def read_pickle( If the content of the pickle file is a Series and its name attribute is None, the name will be set to '0' by default. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> gcs_path = "gs://bigframes-dev-testing/test_pickle.pkl" + >>> df = bpd.read_pickle(filepath_or_buffer=gcs_path) + >>> df.head(2) + species island culmen_length_mm \\ + 0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 + 1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 + + culmen_depth_mm flipper_length_mm body_mass_g sex + 0 18.4 184.0 3475.0 FEMALE + 1 19.1 184.0 4650.0 MALE + + [2 rows x 7 columns] + Args: filepath_or_buffer (str, path object, or file-like object): String, path object (implementing os.PathLike[str]), or file-like object From 02984a4530d63e00dd628705149a1fe788cc263e Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Sat, 21 Oct 2023 10:33:03 -0700 Subject: [PATCH 097/921] test: add sample code for use BigFrames developer guide (#118) * test: add sample code for use BigFrames developer guide --- .../snippets/load_data_from_bigquery_test.py | 24 +++++++++++++ samples/snippets/load_data_from_csv_test.py | 25 ++++++++++++++ samples/snippets/pandas_methods_test.py | 34 +++++++++++++++++++ samples/snippets/set_options_test.py | 34 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 samples/snippets/load_data_from_bigquery_test.py create mode 100644 samples/snippets/load_data_from_csv_test.py create mode 100644 samples/snippets/pandas_methods_test.py create mode 100644 samples/snippets/set_options_test.py diff --git a/samples/snippets/load_data_from_bigquery_test.py b/samples/snippets/load_data_from_bigquery_test.py new file mode 100644 index 00000000000..e4c65688bdd --- /dev/null +++ b/samples/snippets/load_data_from_bigquery_test.py @@ -0,0 +1,24 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_bigquery_dataframes_load_data_from_bigquery(): + # [START bigquery_dataframes_load_data_from_bigquery] + # Create a DataFrame from a BigQuery table: + import bigframes.pandas as bpd + + query_or_table = "bigquery-public-data.ml_datasets.penguins" + bq_df = bpd.read_gbq(query_or_table) + # [END bigquery_dataframes_load_data_from_bigquery] + assert bq_df is not None diff --git a/samples/snippets/load_data_from_csv_test.py b/samples/snippets/load_data_from_csv_test.py new file mode 100644 index 00000000000..31ab9255bf4 --- /dev/null +++ b/samples/snippets/load_data_from_csv_test.py @@ -0,0 +1,25 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_bigquery_dataframes_load_data_from_csv(): + # [START bigquery_dataframes_load_data_from_csv] + import bigframes.pandas as bpd + + filepath_or_buffer = "gs://cloud-samples-data/bigquery/us-states/us-states.csv" + df_from_gcs = bpd.read_csv(filepath_or_buffer) + # Display the first few rows of the DataFrame: + df_from_gcs.head() + # [END bigquery_dataframes_load_data_from_csv] + assert df_from_gcs is not None diff --git a/samples/snippets/pandas_methods_test.py b/samples/snippets/pandas_methods_test.py new file mode 100644 index 00000000000..1f472d63466 --- /dev/null +++ b/samples/snippets/pandas_methods_test.py @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_bigquery_dataframes_pandas_methods(): + # [START bigquery_dataframes_pandas_methods] + import bigframes.pandas as bpd + + # Load data from BigQuery + query_or_table = "bigquery-public-data.ml_datasets.penguins" + bq_df = bpd.read_gbq(query_or_table) + + # Inspect one of the columns (or series) of the DataFrame: + bq_df["body_mass_g"].head(10) + + # Compute the mean of this series: + average_body_mass = bq_df["body_mass_g"].mean() + print(f"average_body_mass: {average_body_mass}") + + # Calculate the mean body_mass_g by species using the groupby operation: + bq_df["body_mass_g"].groupby(by=bq_df["species"]).mean().head() + # [END bigquery_dataframes_pandas_methods] + assert average_body_mass is not None diff --git a/samples/snippets/set_options_test.py b/samples/snippets/set_options_test.py new file mode 100644 index 00000000000..ef6f41ce541 --- /dev/null +++ b/samples/snippets/set_options_test.py @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_bigquery_dataframes_set_options(): + # Close the session before resetting the options + import bigframes.pandas as bpd + + bpd.close_session() + + # [START bigquery_dataframes_set_options] + import bigframes.pandas as bpd + + PROJECT_ID = "bigframes-dec" # @param {type:"string"} + REGION = "US" # @param {type:"string"} + + # Set BigQuery DataFrames options + bpd.options.bigquery.project = PROJECT_ID + bpd.options.bigquery.location = REGION + + # [END bigquery_dataframes_set_options] + assert bpd.options.bigquery.project == PROJECT_ID + assert bpd.options.bigquery.location == REGION From b17e1f43cd0f7567bc5b59b0e916cd20528312b3 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 23 Oct 2023 12:05:58 -0500 Subject: [PATCH 098/921] fix: expose `bigframes.pandas.reset_session` as a public API (#128) --- bigframes/pandas/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 971d40f801e..5c1928e6f04 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -484,4 +484,5 @@ def read_gbq_function(function_name: str): # Session management APIs "get_global_session", "close_session", + "reset_session", ] From f9ba28c6a6ab1ceaeecd70f7b5a87ec7c404ed13 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 23 Oct 2023 15:30:43 -0500 Subject: [PATCH 099/921] chore: remove unused reference to `THIRD_PARTY_NOTICES` from "nightly" (#130) --- .kokoro/release-nightly.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh index 5cc1275308a..0751cf2502c 100755 --- a/.kokoro/release-nightly.sh +++ b/.kokoro/release-nightly.sh @@ -93,7 +93,6 @@ for gcs_path in gs://vertex_sdk_private_releases/bigframe/ \ do gsutil cp -v dist/* ${gcs_path} gsutil cp -v LICENSE ${gcs_path} - gsutil cp -v ${THIRD_PARTY_NOTICES_FILE} ${gcs_path} gsutil -m cp -r -v "notebooks/" ${gcs_path}notebooks/ done From 386f35d2840ab677bfb83f9a0b6f3c8de06e78e8 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 24 Oct 2023 10:44:13 -0500 Subject: [PATCH 100/921] refactor: move DDL gen to `bigframes.session._io`, add missing test (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Towards internal issue 280662868 🦕 --- bigframes/dataframe.py | 14 +++++++------- bigframes/session/__init__.py | 2 +- bigframes/session/_io/__init__.py | 13 +++++++++++++ bigframes/{core/io.py => session/_io/bigquery.py} | 0 .../test_io.py => session/test_io_bigquery.py} | 9 +++++---- 5 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 bigframes/session/_io/__init__.py rename bigframes/{core/io.py => session/_io/bigquery.py} (100%) rename tests/unit/{core/test_io.py => session/test_io_bigquery.py} (93%) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 32a2908a421..5c0d9b78e11 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -46,7 +46,6 @@ import bigframes.core.guid import bigframes.core.indexers as indexers import bigframes.core.indexes as indexes -import bigframes.core.io import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.core.window @@ -56,6 +55,7 @@ import bigframes.operations.aggregations as agg_ops import bigframes.series import bigframes.series as bf_series +import bigframes.session._io.bigquery import third_party.bigframes_vendored.pandas.core.frame as vendored_pandas_frame import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing @@ -2201,9 +2201,9 @@ def to_csv( raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) result_table = self._run_io_query( - index=index, ordering_id=bigframes.core.io.IO_ORDERING_ID + index=index, ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID ) - export_data_statement = bigframes.core.io.create_export_csv_statement( + export_data_statement = bigframes.session._io.bigquery.create_export_csv_statement( f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", uri=path_or_buf, field_delimiter=sep, @@ -2243,9 +2243,9 @@ def to_json( ) result_table = self._run_io_query( - index=index, ordering_id=bigframes.core.io.IO_ORDERING_ID + index=index, ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID ) - export_data_statement = bigframes.core.io.create_export_data_statement( + export_data_statement = bigframes.session._io.bigquery.create_export_data_statement( f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", uri=path_or_buf, format="JSON", @@ -2319,9 +2319,9 @@ def to_parquet( export_options["compression"] = compression.upper() result_table = self._run_io_query( - index=index, ordering_id=bigframes.core.io.IO_ORDERING_ID + index=index, ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID ) - export_data_statement = bigframes.core.io.create_export_data_statement( + export_data_statement = bigframes.session._io.bigquery.create_export_data_statement( f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", uri=path, format="PARQUET", diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 5ec3da1a5a4..db9c5a353ca 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -67,13 +67,13 @@ import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.guid as guid -import bigframes.core.io as bigframes_io from bigframes.core.ordering import IntegerEncoding, OrderingColumnReference import bigframes.core.utils as utils import bigframes.dataframe as dataframe import bigframes.formatting_helpers as formatting_helpers from bigframes.remote_function import read_gbq_function as bigframes_rgf from bigframes.remote_function import remote_function as bigframes_rf +import bigframes.session._io.bigquery as bigframes_io import bigframes.session.clients import bigframes.version diff --git a/bigframes/session/_io/__init__.py b/bigframes/session/_io/__init__.py new file mode 100644 index 00000000000..1dc90d18483 --- /dev/null +++ b/bigframes/session/_io/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/bigframes/core/io.py b/bigframes/session/_io/bigquery.py similarity index 100% rename from bigframes/core/io.py rename to bigframes/session/_io/bigquery.py diff --git a/tests/unit/core/test_io.py b/tests/unit/session/test_io_bigquery.py similarity index 93% rename from tests/unit/core/test_io.py rename to tests/unit/session/test_io_bigquery.py index afb38a5f753..d2255d5edf6 100644 --- a/tests/unit/core/test_io.py +++ b/tests/unit/session/test_io_bigquery.py @@ -18,7 +18,7 @@ import google.cloud.bigquery as bigquery import pytest -import bigframes.core.io +import bigframes.session._io.bigquery def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): @@ -26,7 +26,7 @@ def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): "my-test-project._e8166e0cdb.anonbb92cd" ) - sql = bigframes.core.io.create_snapshot_sql( + sql = bigframes.session._io.bigquery.create_snapshot_sql( table_ref, datetime.datetime.now(datetime.timezone.utc) ) @@ -40,7 +40,7 @@ def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): def test_create_snapshot_sql_doesnt_timetravel_session_datasets(): table_ref = bigquery.TableReference.from_string("my-test-project._session.abcdefg") - sql = bigframes.core.io.create_snapshot_sql( + sql = bigframes.session._io.bigquery.create_snapshot_sql( table_ref, datetime.datetime.now(datetime.timezone.utc) ) @@ -101,4 +101,5 @@ def test_create_snapshot_sql_doesnt_timetravel_session_datasets(): ), ) def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str): - pass + sql = bigframes.session._io.bigquery.bq_schema_to_sql(schema) + assert sql == expected From 95bff3f1902bc09dc3310798a42df8ffd31ed8ee Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 24 Oct 2023 15:08:14 -0700 Subject: [PATCH 101/921] fix: use series's own session in series.reindex listlike case (#135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/series.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigframes/series.py b/bigframes/series.py index 84ca2a578f8..49df8ab61ed 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1242,7 +1242,9 @@ def reindex(self, index=None, *, validate: typing.Optional[bool] = None): raise NotImplementedError( "Cannot reindex with index with different nlevels" ) - new_indexer = bigframes.dataframe.DataFrame(index=index)[[]] + new_indexer = bigframes.dataframe.DataFrame( + index=index, session=self._get_block().expr._session + )[[]] # multiindex join is senstive to index names, so we will set all these result = new_indexer.rename_axis(range(new_indexer.index.nlevels)).join( self.to_frame().rename_axis(range(self.index.nlevels)), From 6fea8efac35871985677ebeb948a576e64a1ffa4 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 24 Oct 2023 21:07:15 -0700 Subject: [PATCH 102/921] docs: add runnable code samples for DataFrames I/O methods and property (#129) * docs: add runnable code samples for DataFrames I/O methods and property * fix: expose `bigframes.pandas.reset_session` as a public API (#128) * fix: address the comment * Empty commit * fix: address comments for better visualization of the output * Empty commit --------- Co-authored-by: Tim Swast --- .../bigframes_vendored/pandas/core/frame.py | 172 +++++++++++++++++- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index a5c12d7b32a..13a81b46454 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -33,7 +33,19 @@ class DataFrame(NDFrame): @property def shape(self) -> tuple[int, int]: - """Return a tuple representing the dimensionality of the DataFrame.""" + """ + Return a tuple representing the dimensionality of the DataFrame. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2, 3], + ... 'col2': [4, 5, 6]}) + >>> df.shape + (3, 2) + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property @@ -44,14 +56,14 @@ def axes(self) -> list: It has the row axis labels and column axis labels as the only members. They are returned in that order. - Examples + **Examples:** - .. code-block:: + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None - df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) - df.axes - [RangeIndex(start=0, stop=2, step=1), Index(['col1', 'col2'], - dtype='object')] + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df.axes[1:] + [Index(['col1', 'col2'], dtype='object')] """ return [self.index, self.columns] @@ -59,6 +71,16 @@ def axes(self) -> list: def values(self) -> np.ndarray: """Return the values of DataFrame in the form of a NumPy array. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df.values + array([[1, 3], + [2, 4]], dtype=object) + Args: dytype (default None): The dtype to pass to `numpy.asarray()`. @@ -76,6 +98,16 @@ def to_numpy(self, dtype=None, copy=False, na_value=None, **kwargs) -> np.ndarra """ Convert the DataFrame to a NumPy array. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df.to_numpy() + array([[1, 3], + [2, 4]], dtype=object) + Args: dtype (None): The dtype to pass to `numpy.asarray()`. @@ -101,6 +133,15 @@ def to_gbq( ) -> None: """Write a DataFrame to a BigQuery table. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> # destination_table = PROJECT_ID + "." + DATASET_ID + "." + TABLE_NAME + >>> df.to_gbq("bigframes-dev.birds.test-numbers", if_exists="replace") + Args: destination_table (str): Name of table to be written, in the form ``dataset.tablename`` @@ -137,6 +178,15 @@ def to_parquet( This function writes the dataframe as a `parquet file `_ to Cloud Storage. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> gcs_bucket = "gs://bigframes-dev-testing/sample_parquet*.parquet" + >>> df.to_parquet(path=gcs_bucket) + Args: path (str): Destination URI(s) of Cloud Storage files(s) to store the extracted dataframe @@ -171,6 +221,35 @@ def to_dict( The type of the key-value pairs can be customized with the parameters (see below). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df.to_dict() + {'col1': {0: 1, 1: 2}, 'col2': {0: 3, 1: 4}} + + You can specify the return orientation. + + >>> df.to_dict('series') + {'col1': 0 1 + 1 2 + Name: col1, dtype: Int64, + 'col2': 0 3 + 1 4 + Name: col2, dtype: Int64} + + >>> df.to_dict('split') + {'index': [0, 1], 'columns': ['col1', 'col2'], 'data': [[1, 3], [2, 4]]} + + >>> df.to_dict("tight") + {'index': [0, 1], + 'columns': ['col1', 'col2'], + 'data': [[1, 3], [2, 4]], + 'index_names': [None], + 'column_names': [None]} + Args: orient (str {'dict', 'list', 'series', 'split', 'tight', 'records', 'index'}): Determines the type of the values of the dictionary. @@ -213,6 +292,15 @@ def to_excel(self, excel_writer, sheet_name: str = "Sheet1", **kwargs) -> None: Note that creating an `ExcelWriter` object with a file name that already exists will result in the contents of the existing file being erased. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import tempfile + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df.to_excel(tempfile.TemporaryFile()) + Args: excel_writer (path-like, file-like, or ExcelWriter object): File path or existing ExcelWriter. @@ -231,6 +319,23 @@ def to_latex( into a main LaTeX document or read from an external file with ``\input{{table.tex}}``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> print(df.to_latex()) + \begin{tabular}{lrr} + \toprule + & col1 & col2 \\ + \midrule + 0 & 1 & 3 \\ + 1 & 2 & 4 \\ + \bottomrule + \end{tabular} + + Args: buf (str, Path or StringIO-like, optional, default None): Buffer to write to. If None, the output is returned as a string. @@ -253,6 +358,16 @@ def to_records( Index will be included as the first field of the record array if requested. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df.to_records() + rec.array([(0, 1, 3), (1, 2, 4)], + dtype=[('index', 'O'), ('col1', 'O'), ('col2', 'O')]) + Args: index (bool, default True): Include index in resulting record array, stored in 'index' @@ -298,6 +413,17 @@ def to_string( ): """Render a DataFrame to a console-friendly tabular output. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> print(df.to_string()) + col1 col2 + 0 1 3 + 1 2 4 + Args: buf (str, Path or StringIO-like, optional, default None): Buffer to write to. If None, the output is returned as a string. @@ -363,6 +489,18 @@ def to_markdown( ): """Print DataFrame in Markdown-friendly format. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> print(df.to_markdown()) + | | col1 | col2 | + |---:|-------:|-------:| + | 0 | 1 | 3 | + | 1 | 2 | 4 | + Args: buf (str, Path or StringIO-like, optional, default None): Buffer to write to. If None, the output is returned as a string. @@ -371,7 +509,7 @@ def to_markdown( index (bool, optional, default True): Add index (row) labels. **kwargs - These parameters will be passed to `tabulate `_. + These parameters will be passed to `tabulate `_. Returns: DataFrame in Markdown-friendly format. @@ -381,6 +519,15 @@ def to_markdown( def to_pickle(self, path, **kwargs) -> None: """Pickle (serialize) object to file. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> gcs_bucket = "gs://bigframes-dev-testing/sample_pickle_gcs.pkl" + >>> df.to_pickle(path=gcs_bucket) + Args: path (str): File path where the pickled object will be stored. @@ -391,6 +538,15 @@ def to_orc(self, path=None, **kwargs) -> bytes | None: """ Write a DataFrame to the ORC format. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> import tempfile + >>> df.to_orc(tempfile.TemporaryFile()) + Args: path (str, file-like object or None, default None): If a string, it will be used as Root Directory path From 05d7618c50acf7b7f9d73e02a8870c8eac910aab Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 25 Oct 2023 15:52:16 -0700 Subject: [PATCH 103/921] test: allow for alternative PCA solutions in tests (#143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/large/ml/test_decomposition.py | 4 ++-- tests/system/large/ml/test_pipeline.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index 460f07b8168..a7049d4c18e 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -67,8 +67,8 @@ def test_decomposition_configure_fit_score_predict( index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) pd.testing.assert_frame_equal( - result.sort_index(), - expected, + abs(result.sort_index()), # results may differ by a minus sign + abs(expected), check_exact=False, rtol=0.1, ) diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 9294740dd67..6874a9f301a 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -431,10 +431,16 @@ def test_pipeline_PCA_fit_score_predict(session, penguins_df_default_index): index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) pd.testing.assert_frame_equal( - predictions[ - ["principal_component_1", "principal_component_2", "principal_component_3"] - ], - expected, + abs( # results may differ by a minus sign + predictions[ + [ + "principal_component_1", + "principal_component_2", + "principal_component_3", + ] + ] + ), + abs(expected), check_exact=False, rtol=0.1, ) From 1641aff37d601b47e0bc4f25ff148be4f718bd1a Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 26 Oct 2023 00:16:15 +0000 Subject: [PATCH 104/921] ci: Disable presubmit LLM tests temporarily (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/small/ml/test_llm.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index b7257dde1be..a801c36c830 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -26,6 +26,9 @@ def test_create_text_generator_model(palm2_text_generator_model): assert palm2_text_generator_model._bqml_model is not None +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_model_default_session(bq_connection, llm_text_pandas_df): import bigframes.pandas as bpd @@ -48,6 +51,9 @@ def test_create_text_generator_model_default_session(bq_connection, llm_text_pan assert all(series.str.len() > 20) +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_model_default_connection(llm_text_pandas_df): from bigframes import _config @@ -74,6 +80,9 @@ def test_create_text_generator_model_default_connection(llm_text_pandas_df): # Marked as flaky only because BQML LLM is in preview, the service only has limited capacity, not stable enough. +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_default_params_success( palm2_text_generator_model, llm_text_df @@ -85,6 +94,9 @@ def test_text_generator_predict_default_params_success( assert all(series.str.len() > 20) +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_series_default_params_success( palm2_text_generator_model, llm_text_df @@ -96,6 +108,9 @@ def test_text_generator_predict_series_default_params_success( assert all(series.str.len() > 20) +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_arbitrary_col_label_success( palm2_text_generator_model, llm_text_df @@ -108,6 +123,9 @@ def test_text_generator_predict_arbitrary_col_label_success( assert all(series.str.len() > 20) +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_with_params_success( palm2_text_generator_model, llm_text_df @@ -139,6 +157,9 @@ def test_create_text_embedding_generator_model_defaults(bq_connection): assert model._bqml_model is not None +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_success( palm2_embedding_generator_model, llm_text_df @@ -152,6 +173,9 @@ def test_embedding_generator_predict_success( assert value.size == 768 +@pytest.mark.skip( + reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." +) @pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_series_success( palm2_embedding_generator_model, llm_text_df From c3b24b59cd02eeef4fab46761faf1699daa90252 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:39:22 -0700 Subject: [PATCH 105/921] chore(main): release 0.11.0 (#126) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d9f63d4c66..93ebadb56fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.11.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.10.0...v0.11.0) (2023-10-26) + + +### Features + +* Add back `reset_session` as an alias for `close_session` ([#124](https://github.com/googleapis/python-bigquery-dataframes/issues/124)) ([694a85a](https://github.com/googleapis/python-bigquery-dataframes/commit/694a85a0ef90d838700014a204d72b23362db1d8)) +* Change `query` parameter to `query_or_table` in `read_gbq` ([#127](https://github.com/googleapis/python-bigquery-dataframes/issues/127)) ([f9bb3c4](https://github.com/googleapis/python-bigquery-dataframes/commit/f9bb3c4bc88c5ba2be6f17e12a0ec4f482ce161f)) + + +### Bug Fixes + +* Expose `bigframes.pandas.reset_session` as a public API ([#128](https://github.com/googleapis/python-bigquery-dataframes/issues/128)) ([b17e1f4](https://github.com/googleapis/python-bigquery-dataframes/commit/b17e1f43cd0f7567bc5b59b0e916cd20528312b3)) +* Use series's own session in series.reindex listlike case ([#135](https://github.com/googleapis/python-bigquery-dataframes/issues/135)) ([95bff3f](https://github.com/googleapis/python-bigquery-dataframes/commit/95bff3f1902bc09dc3310798a42df8ffd31ed8ee)) + + +### Documentation + +* Add runnable code samples for DataFrames I/O methods and property ([#129](https://github.com/googleapis/python-bigquery-dataframes/issues/129)) ([6fea8ef](https://github.com/googleapis/python-bigquery-dataframes/commit/6fea8efac35871985677ebeb948a576e64a1ffa4)) +* Add runnable code samples for reading methods ([#125](https://github.com/googleapis/python-bigquery-dataframes/issues/125)) ([a669919](https://github.com/googleapis/python-bigquery-dataframes/commit/a669919ff25b56156bd70ccd816a0bf19adb48aa)) + ## [0.10.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.9.0...v0.10.0) (2023-10-19) diff --git a/bigframes/version.py b/bigframes/version.py index 7a37ebd2205..18edfa56150 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.10.0" +__version__ = "0.11.0" From ade505cb9fac96985e62f0c546f0df9b65f301c9 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 26 Oct 2023 05:12:13 +0000 Subject: [PATCH 106/921] Revert "ci: Disable presubmit LLM tests temporarily (#144)" (#148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 1641aff37d601b47e0bc4f25ff148be4f718bd1a, which was merged due to automerge label while still being discussed. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/small/ml/test_llm.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index a801c36c830..b7257dde1be 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -26,9 +26,6 @@ def test_create_text_generator_model(palm2_text_generator_model): assert palm2_text_generator_model._bqml_model is not None -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_model_default_session(bq_connection, llm_text_pandas_df): import bigframes.pandas as bpd @@ -51,9 +48,6 @@ def test_create_text_generator_model_default_session(bq_connection, llm_text_pan assert all(series.str.len() > 20) -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_model_default_connection(llm_text_pandas_df): from bigframes import _config @@ -80,9 +74,6 @@ def test_create_text_generator_model_default_connection(llm_text_pandas_df): # Marked as flaky only because BQML LLM is in preview, the service only has limited capacity, not stable enough. -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_default_params_success( palm2_text_generator_model, llm_text_df @@ -94,9 +85,6 @@ def test_text_generator_predict_default_params_success( assert all(series.str.len() > 20) -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_series_default_params_success( palm2_text_generator_model, llm_text_df @@ -108,9 +96,6 @@ def test_text_generator_predict_series_default_params_success( assert all(series.str.len() > 20) -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_arbitrary_col_label_success( palm2_text_generator_model, llm_text_df @@ -123,9 +108,6 @@ def test_text_generator_predict_arbitrary_col_label_success( assert all(series.str.len() > 20) -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_text_generator_predict_with_params_success( palm2_text_generator_model, llm_text_df @@ -157,9 +139,6 @@ def test_create_text_embedding_generator_model_defaults(bq_connection): assert model._bqml_model is not None -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_success( palm2_embedding_generator_model, llm_text_df @@ -173,9 +152,6 @@ def test_embedding_generator_predict_success( assert value.size == 768 -@pytest.mark.skip( - reason="Temporarily disable to validate the hypothesis that LLM capacity is causing the presubmit tests to take long to run." -) @pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_series_success( palm2_embedding_generator_model, llm_text_df From bfd49a54e52e2d502345aa8f9b54457c902abf5c Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 26 Oct 2023 11:48:14 -0500 Subject: [PATCH 107/921] refactor: make `to_pandas()` call `to_arrow()` and use local dtypes in DataFrame construction (#132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Towards internal issue 280662868 🦕 --- bigframes/core/blocks.py | 41 +--- bigframes/core/indexes/index.py | 3 +- bigframes/dtypes.py | 6 + bigframes/session/__init__.py | 10 +- bigframes/session/_io/pandas.py | 77 +++++++ tests/system/small/test_dataframe.py | 10 - tests/system/small/test_series.py | 48 ++++- tests/unit/session/test_io_pandas.py | 296 +++++++++++++++++++++++++++ tests/unit/test_dtypes.py | 57 +++--- 9 files changed, 457 insertions(+), 91 deletions(-) create mode 100644 bigframes/session/_io/pandas.py create mode 100644 tests/unit/session/test_io_pandas.py diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 046d2b3a444..eab46454779 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -28,11 +28,8 @@ from typing import Iterable, List, Optional, Sequence, Tuple import warnings -import geopandas as gpd # type: ignore import google.cloud.bigquery as bigquery -import numpy import pandas as pd -import pyarrow as pa # type: ignore import bigframes.constants as constants import bigframes.core as core @@ -46,6 +43,7 @@ import bigframes.dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops +import bigframes.session._io.pandas import third_party.bigframes_vendored.pandas.io.common as vendored_pandas_io_common # Type constraint for wherever column labels are used @@ -372,34 +370,11 @@ def reorder_levels(self, ids: typing.Sequence[str]): level_names = [self.col_id_to_index_name[index_id] for index_id in ids] return Block(self.expr, ids, self.column_labels, level_names) - @classmethod - def _to_dataframe( - cls, result, schema: typing.Mapping[str, bigframes.dtypes.Dtype] - ) -> pd.DataFrame: + def _to_dataframe(self, result) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" - dtypes = bigframes.dtypes.to_pandas_dtypes_overrides(result.schema) - df = result.to_dataframe( - dtypes=dtypes, - bool_dtype=pd.BooleanDtype(), - int_dtype=pd.Int64Dtype(), - float_dtype=pd.Float64Dtype(), - string_dtype=pd.StringDtype(storage="pyarrow"), - date_dtype=pd.ArrowDtype(pa.date32()), - datetime_dtype=pd.ArrowDtype(pa.timestamp("us")), - time_dtype=pd.ArrowDtype(pa.time64("us")), - timestamp_dtype=pd.ArrowDtype(pa.timestamp("us", tz="UTC")), - ) - - # Convert Geography column from StringDType to GeometryDtype. - for column_name, dtype in schema.items(): - if dtype == gpd.array.GeometryDtype(): - df[column_name] = gpd.GeoSeries.from_wkt( - # https://github.com/geopandas/geopandas/issues/1879 - df[column_name].replace({numpy.nan: None}), - # BigQuery geography type is based on the WGS84 reference ellipsoid. - crs="EPSG:4326", - ) - return df + dtypes = dict(zip(self.index_columns, self.index_dtypes)) + dtypes.update(zip(self.value_columns, self.dtypes)) + return self._expr._session._rows_to_dataframe(result, dtypes) def to_pandas( self, @@ -480,8 +455,7 @@ def _compute_and_count( if sampling_method == _HEAD: total_rows = int(results_iterator.total_rows * fraction) results_iterator.max_results = total_rows - schema = dict(zip(self.value_columns, self.dtypes)) - df = self._to_dataframe(results_iterator, schema) + df = self._to_dataframe(results_iterator) if self.index_columns: df.set_index(list(self.index_columns), inplace=True) @@ -510,8 +484,7 @@ def _compute_and_count( ) else: total_rows = results_iterator.total_rows - schema = dict(zip(self.value_columns, self.dtypes)) - df = self._to_dataframe(results_iterator, schema) + df = self._to_dataframe(results_iterator) if self.index_columns: df.set_index(list(self.index_columns), inplace=True) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 677bb8529c2..b9ffdff21ec 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -399,9 +399,10 @@ def to_pandas(self) -> pandas.Index: """Executes deferred operations and downloads the results.""" # Project down to only the index column. So the query can be cached to visualize other data. index_columns = list(self._block.index_columns) + dtypes = dict(zip(index_columns, self.dtypes)) expr = self._expr.select_columns(index_columns) results, _ = expr.start_query() - df = expr._session._rows_to_dataframe(results) + df = expr._session._rows_to_dataframe(results, dtypes) df = df.set_index(index_columns) index = df.index index.names = list(self._block._index_labels) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index da221a95aca..079f0cc27a2 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -169,6 +169,10 @@ def ibis_dtype_to_bigframes_dtype( if isinstance(ibis_dtype, ibis_dtypes.Struct): return pd.ArrowDtype(ibis_dtype_to_arrow_dtype(ibis_dtype)) + # BigQuery only supports integers of size 64 bits. + if isinstance(ibis_dtype, ibis_dtypes.Integer): + return pd.Int64Dtype() + if ibis_dtype in IBIS_TO_BIGFRAMES: return IBIS_TO_BIGFRAMES[ibis_dtype] elif isinstance(ibis_dtype, ibis_dtypes.Null): @@ -372,6 +376,8 @@ def cast_ibis_value( ibis_dtypes.float64: (ibis_dtypes.string, ibis_dtypes.int64), ibis_dtypes.string: (ibis_dtypes.int64, ibis_dtypes.float64), ibis_dtypes.date: (), + ibis_dtypes.Decimal(precision=38, scale=9): (ibis_dtypes.float64,), + ibis_dtypes.Decimal(precision=76, scale=38): (ibis_dtypes.float64,), ibis_dtypes.time: (), ibis_dtypes.timestamp: (ibis_dtypes.Timestamp(timezone="UTC"),), ibis_dtypes.Timestamp(timezone="UTC"): (ibis_dtypes.timestamp,), diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index db9c5a353ca..af1f70d54d5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1515,14 +1515,10 @@ def _get_table_size(self, destination_table): return table.num_bytes def _rows_to_dataframe( - self, row_iterator: bigquery.table.RowIterator + self, row_iterator: bigquery.table.RowIterator, dtypes: Dict ) -> pandas.DataFrame: - return row_iterator.to_dataframe( - bool_dtype=pandas.BooleanDtype(), - int_dtype=pandas.Int64Dtype(), - float_dtype=pandas.Float64Dtype(), - string_dtype=pandas.StringDtype(storage="pyarrow"), - ) + arrow_table = row_iterator.to_arrow() + return bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) def _start_generic_job(self, job: formatting_helpers.GenericJob): if bigframes.options.display.progress_bar is not None: diff --git a/bigframes/session/_io/pandas.py b/bigframes/session/_io/pandas.py new file mode 100644 index 00000000000..163127b546d --- /dev/null +++ b/bigframes/session/_io/pandas.py @@ -0,0 +1,77 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Union + +import geopandas # type: ignore +import pandas +import pandas.arrays +import pyarrow # type: ignore +import pyarrow.compute # type: ignore + +import bigframes.constants + + +def arrow_to_pandas( + arrow_table: Union[pyarrow.Table, pyarrow.RecordBatch], dtypes: Dict +): + if len(dtypes) != arrow_table.num_columns: + raise ValueError( + f"Number of types {len(dtypes)} doesn't match number of columns " + f"{arrow_table.num_columns}. {bigframes.constants.FEEDBACK_LINK}" + ) + + serieses = {} + for field, column in zip(arrow_table.schema, arrow_table): + dtype = dtypes[field.name] + + if dtype == geopandas.array.GeometryDtype(): + series = geopandas.GeoSeries.from_wkt( + column, + # BigQuery geography type is based on the WGS84 reference ellipsoid. + crs="EPSG:4326", + ) + elif dtype == pandas.Float64Dtype(): + # Preserve NA/NaN distinction. Note: This is currently needed, even if we use + # nullable Float64Dtype in the types_mapper. See: + # https://github.com/pandas-dev/pandas/issues/55668 + # Regarding type: ignore, this class has been public at this + # location since pandas 1.2.0. See: + # https://pandas.pydata.org/docs/dev/reference/api/pandas.arrays.FloatingArray.html + pd_array = pandas.arrays.FloatingArray( # type: ignore + column.to_numpy(), + pyarrow.compute.is_null(column).to_numpy(), + ) + series = pandas.Series(pd_array, dtype=dtype) + elif dtype == pandas.Int64Dtype(): + # Avoid out-of-bounds errors in Pandas 1.5.x, which incorrectly + # casts to float64 in an intermediate step. + pd_array = pandas.arrays.IntegerArray( + pyarrow.compute.fill_null(column, 0).to_numpy(), + pyarrow.compute.is_null(column).to_numpy(), + ) + series = pandas.Series(pd_array, dtype=dtype) + elif isinstance(dtype, pandas.ArrowDtype): + # Avoid conversion logic if we are backing the pandas Series by the + # arrow array. + series = pandas.Series( + pandas.arrays.ArrowExtensionArray(column), # type: ignore + dtype=dtype, + ) + else: + series = column.to_pandas(types_mapper=lambda _: dtype) + + serieses[field.name] = series + + return pandas.DataFrame(serieses) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 19e50eb06de..84e8def83b3 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2046,16 +2046,6 @@ def test__dir__with_rename(scalars_dfs): def test_iloc_slice(scalars_df_index, scalars_pandas_df_index, start, stop, step): bf_result = scalars_df_index.iloc[start:stop:step].to_pandas() pd_result = scalars_pandas_df_index.iloc[start:stop:step] - - # Pandas may assign non-object dtype to empty series and series index - # dtypes of empty columns are a known area of divergence from pandas - for column in pd_result.columns: - if ( - pd_result[column].empty and column != "geography_col" - ): # for empty geography_col, bigframes assigns non-object dtype - pd_result[column] = pd_result[column].astype("object") - pd_result.index = pd_result.index.astype("object") - pd.testing.assert_frame_equal( bf_result, pd_result, diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index bd9edbb1ca0..c9510290b66 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -575,7 +575,15 @@ def test_series_int_int_operators_series(scalars_dfs, operator): ) def test_mods(scalars_dfs, col_x, col_y, method): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = getattr(scalars_df[col_x], method)(scalars_df[col_y]).to_pandas() + x_bf = scalars_df[col_x] + y_bf = scalars_df[col_y] + bf_series = getattr(x_bf, method)(y_bf) + # BigQuery's mod functions return [BIG]NUMERIC values unless both arguments are integers. + # https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions#mod + if x_bf.dtype == pd.Int64Dtype() and y_bf.dtype == pd.Int64Dtype(): + bf_result = bf_series.to_pandas() + else: + bf_result = bf_series.astype("Float64").to_pandas() pd_result = getattr(scalars_pandas_df[col_x], method)(scalars_pandas_df[col_y]) pd.testing.assert_series_equal(pd_result, bf_result) @@ -620,8 +628,20 @@ def test_divmods_series(scalars_dfs, col_x, col_y, method): pd_div_result, pd_mod_result = getattr(scalars_pandas_df[col_x], method)( scalars_pandas_df[col_y] ) - pd.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) - pd.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + # BigQuery's mod functions return NUMERIC values for non-INT64 inputs. + if bf_div_result.dtype == pd.Int64Dtype(): + pd.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) + else: + pd.testing.assert_series_equal( + pd_div_result, bf_div_result.astype("Float64").to_pandas() + ) + + if bf_mod_result.dtype == pd.Int64Dtype(): + pd.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + else: + pd.testing.assert_series_equal( + pd_mod_result, bf_mod_result.astype("Float64").to_pandas() + ) @pytest.mark.parametrize( @@ -649,8 +669,20 @@ def test_divmods_scalars(scalars_dfs, col_x, other, method): scalars_df, scalars_pandas_df = scalars_dfs bf_div_result, bf_mod_result = getattr(scalars_df[col_x], method)(other) pd_div_result, pd_mod_result = getattr(scalars_pandas_df[col_x], method)(other) - pd.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) - pd.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + # BigQuery's mod functions return NUMERIC values for non-INT64 inputs. + if bf_div_result.dtype == pd.Int64Dtype(): + pd.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) + else: + pd.testing.assert_series_equal( + pd_div_result, bf_div_result.astype("Float64").to_pandas() + ) + + if bf_mod_result.dtype == pd.Int64Dtype(): + pd.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + else: + pd.testing.assert_series_equal( + pd_mod_result, bf_mod_result.astype("Float64").to_pandas() + ) @pytest.mark.parametrize( @@ -1941,12 +1973,6 @@ def test_iloc_nested(scalars_df_index, scalars_pandas_df_index): def test_series_iloc(scalars_df_index, scalars_pandas_df_index, start, stop, step): bf_result = scalars_df_index["string_col"].iloc[start:stop:step].to_pandas() pd_result = scalars_pandas_df_index["string_col"].iloc[start:stop:step] - - # Pandas may assign non-object dtype to empty series and series index - if pd_result.empty: - pd_result = pd_result.astype("object") - pd_result.index = pd_result.index.astype("object") - pd.testing.assert_series_equal( bf_result, pd_result, diff --git a/tests/unit/session/test_io_pandas.py b/tests/unit/session/test_io_pandas.py new file mode 100644 index 00000000000..8b95977ec3d --- /dev/null +++ b/tests/unit/session/test_io_pandas.py @@ -0,0 +1,296 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +from typing import Dict, Union + +import geopandas # type: ignore +import numpy +import pandas +import pandas.arrays +import pandas.testing +import pyarrow # type: ignore +import pytest + +import bigframes.session._io.pandas + + +@pytest.mark.parametrize( + ("arrow_table", "dtypes", "expected"), + ( + pytest.param( + pyarrow.Table.from_pydict({}), + {}, + pandas.DataFrame(), + id="empty-df", + ), + pytest.param( + pyarrow.Table.from_pydict( + { + "bool": pyarrow.array([None, None, None], type=pyarrow.bool_()), + "float": pyarrow.array([None, None, None], type=pyarrow.float64()), + "int": pyarrow.array([None, None, None], type=pyarrow.int64()), + "string": pyarrow.array([None, None, None], type=pyarrow.string()), + "time": pyarrow.array( + [None, None, None], type=pyarrow.time64("us") + ), + } + ), + { + "bool": "boolean", + "float": pandas.Float64Dtype(), + "int": pandas.Int64Dtype(), + "string": "string[pyarrow]", + "time": pandas.ArrowDtype(pyarrow.time64("us")), + }, + pandas.DataFrame( + { + "bool": pandas.Series([None, None, None], dtype="boolean"), + "float": pandas.Series( + pandas.arrays.FloatingArray( # type: ignore + numpy.array( + [float("nan"), float("nan"), float("nan")], + dtype="float64", + ), + numpy.array([True, True, True], dtype="bool"), + ), + dtype=pandas.Float64Dtype(), + ), + "int": pandas.Series( + [None, None, None], + dtype=pandas.Int64Dtype(), + ), + "string": pandas.Series( + [None, None, None], dtype="string[pyarrow]" + ), + "time": pandas.Series( + [ + None, + None, + None, + ], + dtype=pandas.ArrowDtype(pyarrow.time64("us")), + ), + } + ), + id="nulls-df", + ), + pytest.param( + pyarrow.Table.from_pydict( + { + "date": pyarrow.array( + [ + datetime.date(2023, 8, 29), + None, + datetime.date(2024, 4, 9), + datetime.date(1, 1, 1), + ], + type=pyarrow.date32(), + ), + "datetime": pyarrow.array( + [ + datetime.datetime(2023, 8, 29), + None, + datetime.datetime(2024, 4, 9, 23, 59, 59), + datetime.datetime(1, 1, 1, 0, 0, 0, 1), + ], + type=pyarrow.timestamp("us"), + ), + "string": ["123", None, "abc", "xyz"], + "time": pyarrow.array( + [ + datetime.time(0, 0, 0, 1), + datetime.time(12, 0, 0), + None, + datetime.time(23, 59, 59, 999999), + ], + type=pyarrow.time64("us"), + ), + "timestamp": pyarrow.array( + [ + datetime.datetime(2023, 8, 29), + datetime.datetime(1, 1, 1, 0, 0, 0, 1), + None, + datetime.datetime(2024, 4, 9, 23, 59, 59), + ], + type=pyarrow.timestamp("us", datetime.timezone.utc), + ), + } + ), + { + "date": pandas.ArrowDtype(pyarrow.date32()), + "datetime": pandas.ArrowDtype(pyarrow.timestamp("us")), + "string": "string[pyarrow]", + "time": pandas.ArrowDtype(pyarrow.time64("us")), + "timestamp": pandas.ArrowDtype( + pyarrow.timestamp("us", datetime.timezone.utc) + ), + }, + pandas.DataFrame( + { + "date": pandas.Series( + [ + datetime.date(2023, 8, 29), + None, + datetime.date(2024, 4, 9), + datetime.date(1, 1, 1), + ], + dtype=pandas.ArrowDtype(pyarrow.date32()), + ), + "datetime": pandas.Series( + [ + datetime.datetime(2023, 8, 29), + None, + datetime.datetime(2024, 4, 9, 23, 59, 59), + datetime.datetime(1, 1, 1, 0, 0, 0, 1), + ], + dtype=pandas.ArrowDtype(pyarrow.timestamp("us")), + ), + "string": pandas.Series( + ["123", None, "abc", "xyz"], dtype="string[pyarrow]" + ), + "time": pandas.Series( + [ + datetime.time(0, 0, 0, 1), + datetime.time(12, 0, 0), + None, + datetime.time(23, 59, 59, 999999), + ], + dtype=pandas.ArrowDtype(pyarrow.time64("us")), + ), + "timestamp": pandas.Series( + [ + datetime.datetime(2023, 8, 29), + datetime.datetime(1, 1, 1, 0, 0, 0, 1), + None, + datetime.datetime(2024, 4, 9, 23, 59, 59), + ], + dtype=pandas.ArrowDtype( + pyarrow.timestamp("us", datetime.timezone.utc) + ), + ), + } + ), + id="arrow-dtypes", + ), + pytest.param( + pyarrow.Table.from_pydict( + { + "bool": [True, None, True, False], + "bytes": [b"123", None, b"abc", b"xyz"], + "float": pyarrow.array( + [1.0, None, float("nan"), -1.0], + type=pyarrow.float64(), + ), + "int": pyarrow.array( + [1, None, -1, 2**63 - 1], + type=pyarrow.int64(), + ), + "string": ["123", None, "abc", "xyz"], + } + ), + { + "bool": "boolean", + "bytes": "object", + "float": pandas.Float64Dtype(), + "int": pandas.Int64Dtype(), + "string": "string[pyarrow]", + }, + pandas.DataFrame( + { + "bool": pandas.Series([True, None, True, False], dtype="boolean"), + "bytes": [b"123", None, b"abc", b"xyz"], + "float": pandas.Series( + pandas.arrays.FloatingArray( # type: ignore + numpy.array( + [1.0, float("nan"), float("nan"), -1.0], dtype="float64" + ), + numpy.array([False, True, False, False], dtype="bool"), + ), + dtype=pandas.Float64Dtype(), + ), + "int": pandas.Series( + [1, None, -1, 2**63 - 1], + dtype=pandas.Int64Dtype(), + ), + "string": pandas.Series( + ["123", None, "abc", "xyz"], dtype="string[pyarrow]" + ), + } + ), + id="scalar-dtypes", + ), + pytest.param( + pyarrow.Table.from_pydict( + { + "geocol": [ + "POINT(32 210)", + None, + "LINESTRING(1 1, 2 1, 3.1 2.88, 3 -3)", + ] + } + ), + {"geocol": geopandas.array.GeometryDtype()}, + pandas.DataFrame( + { + "geocol": geopandas.GeoSeries.from_wkt( + ["POINT(32 210)", None, "LINESTRING(1 1, 2 1, 3.1 2.88, 3 -3)"], + crs="EPSG:4326", + ), + } + ), + id="geography-dtype", + ), + ), +) +def test_arrow_to_pandas( + arrow_table: Union[pyarrow.Table, pyarrow.RecordBatch], + dtypes: Dict, + expected: pandas.DataFrame, +): + actual = bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) + pandas.testing.assert_series_equal(actual.dtypes, expected.dtypes) + + # assert_frame_equal is converting to numpy internally, which causes some + # loss of precision with the extreme values in this test. + for column in actual.columns: + assert tuple( + (index, value) if (value is pandas.NA or value == value) else (index, "nan") + for index, value in actual[column].items() + ) == tuple( + (index, value) if (value is pandas.NA or value == value) else (index, "nan") + for index, value in expected[column].items() + ) + + +@pytest.mark.parametrize( + ("arrow_table", "dtypes"), + ( + pytest.param( + pyarrow.Table.from_pydict({"col1": [1], "col2": [2]}), + {"col1": "Int64"}, + id="too-few-dtypes", + ), + pytest.param( + pyarrow.RecordBatch.from_pydict({"col1": [1]}), + {"col1": "Int64", "col2": "string[pyarrow]"}, + id="too-many-dtypes", + ), + ), +) +def test_arrow_to_pandas_wrong_size_dtypes( + arrow_table: Union[pyarrow.Table, pyarrow.RecordBatch], dtypes: Dict +): + with pytest.raises(ValueError, match=f"Number of types {len(dtypes)}"): + bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index 3baff2e1f58..6ceaaf911b9 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -29,41 +29,42 @@ # TODO(bmil): Add ARRAY, INTERVAL, STRUCT to cover all the standard # BigQuery data types as they appear in Ibis: # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types - (ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), np.dtype("O")), - (ibis_dtypes.boolean, pd.BooleanDtype()), - (ibis_dtypes.binary, np.dtype("O")), - (ibis_dtypes.date, pd.ArrowDtype(pa.date32())), - (ibis_dtypes.Timestamp(), pd.ArrowDtype(pa.timestamp("us"))), - (ibis_dtypes.float64, pd.Float64Dtype()), - ( + pytest.param( + ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), + np.dtype("O"), + id="bignumeric", + ), + pytest.param(ibis_dtypes.boolean, pd.BooleanDtype(), id="bool"), + pytest.param(ibis_dtypes.binary, np.dtype("O"), id="bytes"), + pytest.param(ibis_dtypes.date, pd.ArrowDtype(pa.date32()), id="date"), + pytest.param( + ibis_dtypes.Timestamp(), pd.ArrowDtype(pa.timestamp("us")), id="datetime" + ), + pytest.param(ibis_dtypes.float64, pd.Float64Dtype(), id="float"), + pytest.param( ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True), gpd.array.GeometryDtype(), + id="geography", ), - (ibis_dtypes.int64, pd.Int64Dtype()), - (ibis_dtypes.json, np.dtype("O")), - (ibis_dtypes.Decimal(precision=38, scale=9, nullable=True), np.dtype("O")), - (ibis_dtypes.string, pd.StringDtype(storage="pyarrow")), - (ibis_dtypes.time, pd.ArrowDtype(pa.time64("us"))), - ( + pytest.param(ibis_dtypes.int8, pd.Int64Dtype(), id="int8-as-int64"), + pytest.param(ibis_dtypes.int64, pd.Int64Dtype(), id="int64"), + # TODO(tswast): custom dtype (or at least string dtype) for JSON objects + pytest.param(ibis_dtypes.json, np.dtype("O"), id="json"), + pytest.param( + ibis_dtypes.Decimal(precision=38, scale=9, nullable=True), + np.dtype("O"), + id="numeric", + ), + pytest.param( + ibis_dtypes.string, pd.StringDtype(storage="pyarrow"), id="string" + ), + pytest.param(ibis_dtypes.time, pd.ArrowDtype(pa.time64("us")), id="time"), + pytest.param( ibis_dtypes.Timestamp(timezone="UTC"), pd.ArrowDtype(pa.timestamp("us", tz="UTC")), # type: ignore + id="timestamp", ), ], - ids=[ - "bignumeric", - "bool", - "bytes", - "date", - "datetime", - "float", - "geography", - "int64", - "json", - "numeric", - "string", - "time", - "timestamp", - ], ) def test_ibis_dtype_converts(ibis_dtype, bigframes_dtype): """Test all the Ibis data types needed to read BigQuery tables""" From d423e102453c070af64aa37741fb9ff6fb9a6d25 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 26 Oct 2023 17:44:14 +0000 Subject: [PATCH 108/921] test: Log slowest tests durations (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- noxfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 1864da9fe7d..d0bbda80fd4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -305,8 +305,10 @@ def run_system( "py.test", "--quiet", "-n=20", - # Any individual test taking longer than 10 mins will be terminated. + # Any individual test taking longer than 15 mins will be terminated. "--timeout=900", + # Log 20 slowest tests + "--durations=20", f"--junitxml={prefix_name}_{session.python}_sponge_log.xml", ] if print_duration: From 45c617fee7becc42f1c129246ffdc32f3a963f12 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:36:14 -0700 Subject: [PATCH 109/921] docs: link to ML.EVALUATE BQML page for score() methods (#137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/ensemble.py | 12 ++++++++++++ bigframes/ml/forecasting.py | 6 ++++++ third_party/bigframes_vendored/sklearn/base.py | 14 +++++++++++++- .../bigframes_vendored/sklearn/cluster/_kmeans.py | 9 +++++++-- .../sklearn/decomposition/_pca.py | 8 +++++++- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 113ad872b5c..19ca8608ffb 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -507,6 +507,12 @@ def score( ): """Calculate evaluation metrics of the model. + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#regression_models + for the outputs relevant to this model type. + Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): A BigQuery DataFrame as evaluation data. @@ -676,6 +682,12 @@ def score( ): """Calculate evaluation metrics of the model. + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#classification_models + for the outputs relevant to this model type. + Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): A BigQuery DataFrame as evaluation data. diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 8a6de1dd81f..8e309d5e736 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -112,6 +112,12 @@ def score( ) -> bpd.DataFrame: """Calculate evaluation metrics of the model. + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#time_series_models + for the outputs relevant to this model type. + Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): A BigQuery DataFrame only contains 1 column as diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index 42868ce51f2..768328e5529 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -85,6 +85,12 @@ def score(self, X, y): which is a harsh metric since you require for each sample that each label set be correctly predicted. + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#classification_models + for the outputs relevant to this model type. + Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): DataFrame of shape (n_samples, n_features). Test samples. @@ -105,7 +111,13 @@ class RegressorMixin: _estimator_type = "regressor" def score(self, X, y): - """Return the evaluation metrics of the model. + """Calculate evaluation metrics of the model. + + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#regression_models + for the outputs relevant to this model type. Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index ece62dc1470..5369d3662d7 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -12,7 +12,6 @@ # License: BSD 3 clause from abc import ABC -from typing import List, Optional from bigframes import constants from third_party.bigframes_vendored.sklearn.base import BaseEstimator @@ -83,7 +82,13 @@ def score( X, y=None, ): - """Metrics of the model. + """Calculate evaluation metrics of the model. + + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#k-means_models + for the outputs relevant to this model type. Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 97fee5a501e..011ecc06dd3 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -55,7 +55,13 @@ def fit(self, X, y=None): raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def score(self, X=None, y=None): - """Return the metrics of the model. + """Calculate evaluation metrics of the model. + + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#pca_models + for the outputs relevant to this model type. Args: X (default None): From c639a3657465e2b68a3b93c363bd3ae1e969d2cc Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:30:15 -0700 Subject: [PATCH 110/921] feat: populate ibis version in user agent (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/session/clients.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index 544f74265fe..e33413002fe 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -28,12 +28,13 @@ import google.cloud.bigquery_storage_v1 import google.cloud.functions_v2 import google.cloud.resourcemanager_v3 +import ibis import pydata_google_auth import bigframes.version _ENV_DEFAULT_PROJECT = "GOOGLE_CLOUD_PROJECT" -_APPLICATION_NAME = f"bigframes/{bigframes.version.__version__}" +_APPLICATION_NAME = f"bigframes/{bigframes.version.__version__} ibis/{ibis.__version__}" _SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] # BigQuery is a REST API, which requires the protocol as part of the URL. From 2ddbf743efc2fd8ffb61ae8d3333fc4b98ce4b55 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 26 Oct 2023 15:12:14 -0500 Subject: [PATCH 111/921] fix: don't override the global logging config (#138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/clients.py | 3 --- bigframes/remote_function.py | 5 ----- 2 files changed, 8 deletions(-) diff --git a/bigframes/clients.py b/bigframes/clients.py index 4ba9d93d694..de2421e499f 100644 --- a/bigframes/clients.py +++ b/bigframes/clients.py @@ -24,9 +24,6 @@ from google.cloud import bigquery_connection_v1, resourcemanager_v3 from google.iam.v1 import iam_policy_pb2, policy_pb2 -logging.basicConfig( - level=logging.INFO, format="[%(levelname)s][%(asctime)s][%(name)s] %(message)s" -) logger = logging.getLogger(__name__) diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index c82ba840564..a39cd033f69 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -53,11 +53,6 @@ from bigframes import clients import bigframes.constants as constants -# TODO(shobs): Change the min log level to INFO after the development stabilizes -# before June 2023 -logging.basicConfig( - level=logging.INFO, format="[%(levelname)s][%(asctime)s][%(name)s] %(message)s" -) logger = logging.getLogger(__name__) # Protocol version 4 is available in python version 3.4 and above From 27c57255c7fe11e1ef9b9826d988d80fc17442a6 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 26 Oct 2023 14:01:04 -0700 Subject: [PATCH 112/921] fix: use indexee's session for loc listlike cases (#152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/indexers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 4f5a9471b99..d18a0a38efe 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -310,7 +310,9 @@ def _loc_getitem_series_or_dataframe( index_name = temporary_index_names[i] values = [entry[i] for entry in key] index_cols_dict[index_name] = values - keys_df = bigframes.dataframe.DataFrame(index_cols_dict) + keys_df = bigframes.dataframe.DataFrame( + index_cols_dict, session=series_or_dataframe._get_block().expr._session + ) keys_df = keys_df.set_index(temporary_index_names, drop=True) keys_df = keys_df.rename_axis(original_index_names) else: @@ -320,7 +322,10 @@ def _loc_getitem_series_or_dataframe( index_name_is_none = index_name is None if index_name_is_none: index_name = "unnamed_col" - keys_df = bigframes.dataframe.DataFrame({index_name: key}) + keys_df = bigframes.dataframe.DataFrame( + {index_name: key}, + session=series_or_dataframe._get_block().expr._session, + ) keys_df = keys_df.set_index(index_name, drop=True) if index_name_is_none: keys_df.index.name = None From 8e4451841ba09099b0ed5433f9102511741dfbed Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 26 Oct 2023 14:40:13 -0700 Subject: [PATCH 113/921] feat: add pandas.qcut (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/reshape/__init__.py | 33 ++++++++++++ bigframes/operations/aggregations.py | 51 +++++++++++++++++++ bigframes/pandas/__init__.py | 13 +++++ tests/system/small/test_pandas.py | 25 +++++++++ .../pandas/core/reshape/tile.py | 30 +++++++++++ 5 files changed, 152 insertions(+) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index 339ce7466a1..dc61c3baada 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -20,6 +20,7 @@ import bigframes.core as core import bigframes.core.utils as utils import bigframes.dataframe +import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.series @@ -118,3 +119,35 @@ def cut( f"Only labels=False is supported in BigQuery DataFrames so far. {constants.FEEDBACK_LINK}" ) return x._apply_window_op(agg_ops.CutOp(bins), window_spec=core.WindowSpec()) + + +def qcut( + x: bigframes.series.Series, + q: typing.Union[int, typing.Sequence[float]], + *, + labels: Optional[bool] = None, + duplicates: typing.Literal["drop", "error"] = "error", +) -> bigframes.series.Series: + if isinstance(q, int) and q <= 0: + raise ValueError("`q` should be a positive integer.") + + if labels is not False: + raise NotImplementedError( + f"Only labels=False is supported in BigQuery DataFrames so far. {constants.FEEDBACK_LINK}" + ) + if duplicates != "drop": + raise NotImplementedError( + f"Only duplicates='drop' is supported in BigQuery DataFrames so far. {constants.FEEDBACK_LINK}" + ) + block = x._block + label = block.col_id_to_label[x._value_column] + block, nullity_id = block.apply_unary_op(x._value_column, ops.notnull_op) + block, result = block.apply_window_op( + x._value_column, + agg_ops.QcutOp(q), + window_spec=core.WindowSpec(grouping_keys=(nullity_id,)), + ) + block, result = block.apply_binary_op( + result, nullity_id, ops.partial_arg3(ops.where_op, None), result_label=label + ) + return bigframes.series.Series(block.select_column(result)) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 23271e82205..465d1887247 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -254,6 +254,53 @@ def handles_ties(self): return True +class QcutOp(WindowOp): + def __init__(self, quantiles: typing.Union[int, typing.Sequence[float]]): + self.name = f"qcut-{quantiles}" + self._quantiles = quantiles + + @numeric_op + def _as_ibis( + self, column: ibis_types.Column, window=None + ) -> ibis_types.IntegerValue: + if isinstance(self._quantiles, int): + quantiles_ibis = dtypes.literal_to_ibis_scalar(self._quantiles) + percent_ranks = typing.cast( + ibis_types.FloatingColumn, + _apply_window_if_present(column.percent_rank(), window), + ) + float_bucket = typing.cast( + ibis_types.FloatingColumn, (percent_ranks * quantiles_ibis) + ) + return float_bucket.ceil().clip(lower=_ibis_num(1)) - _ibis_num(1) + else: + percent_ranks = typing.cast( + ibis_types.FloatingColumn, + _apply_window_if_present(column.percent_rank(), window), + ) + out = ibis.case() + first_ibis_quantile = dtypes.literal_to_ibis_scalar(self._quantiles[0]) + out = out.when(percent_ranks < first_ibis_quantile, None) + for bucket_n in range(len(self._quantiles) - 1): + ibis_quantile = dtypes.literal_to_ibis_scalar( + self._quantiles[bucket_n + 1] + ) + out = out.when( + percent_ranks <= ibis_quantile, + dtypes.literal_to_ibis_scalar(bucket_n, force_dtype=Int64Dtype()), + ) + out = out.else_(None) + return out.end() + + @property + def skips_nulls(self): + return False + + @property + def handles_ties(self): + return True + + class NuniqueOp(AggregateOp): name = "nunique" @@ -491,3 +538,7 @@ def lookup_agg_func(key: str) -> AggregateOp: return _AGGREGATIONS_LOOKUP[key] else: raise ValueError(f"Unrecognize aggregate function: {key}") + + +def _ibis_num(number: float): + return typing.cast(ibis_types.NumericValue, ibis_types.literal(number)) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 5c1928e6f04..8d9726312fd 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -134,6 +134,19 @@ def cut( cut.__doc__ = vendored_pandas_tile.cut.__doc__ +def qcut( + x: bigframes.series.Series, + q: int, + *, + labels: Optional[bool] = None, + duplicates: typing.Literal["drop", "error"] = "error", +) -> bigframes.series.Series: + return bigframes.core.reshape.qcut(x, q, labels=labels, duplicates=duplicates) + + +qcut.__doc__ = vendored_pandas_tile.qcut.__doc__ + + def merge( left: DataFrame, right: DataFrame, diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index a429c6551d6..f8fa78587f9 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -223,3 +223,28 @@ def test_cut(scalars_dfs): bf_result = bf_result.to_pandas() pd_result = pd_result.astype("Int64") pd.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("q",), + [ + (1,), + (2,), + (7,), + (32,), + ([0, 0.1, 0.3, 0.4, 0.9, 1.0],), + ([0.5, 0.9],), + ], +) +def test_qcut(scalars_dfs, q): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = pd.qcut( + scalars_pandas_df["float64_col"], q, labels=False, duplicates="drop" + ) + bf_result = bpd.qcut(scalars_df["float64_col"], q, labels=False, duplicates="drop") + + bf_result = bf_result.to_pandas() + pd_result = pd_result.astype("Int64") + + pd.testing.assert_series_equal(bf_result, pd_result) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/tile.py b/third_party/bigframes_vendored/pandas/core/reshape/tile.py index 4f5f2efef0a..24ea655a5fe 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/tile.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/tile.py @@ -65,3 +65,33 @@ def cut( False : returns an ndarray of integers. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +def qcut(x, q, *, labels=None, duplicates="error"): + """ + Quantile-based discretization function. + + Discretize variable into equal-sized buckets based on rank or based + on sample quantiles. For example 1000 values for 10 quantiles would + produce a Categorical object indicating quantile membership for each data point. + + Args: + x (Series): + The input Series to be binned. Must be 1-dimensional. + q (int or list-like of float): + Number of quantiles. 10 for deciles, 4 for quartiles, etc. Alternately + array of quantiles, e.g. [0, .25, .5, .75, 1.] for quartiles. + labels (None): + Used as labels for the resulting bins. Must be of the same length as + the resulting bins. If False, return only integer indicators of the + bins. If True, raises an error. + duplicates ({default 'raise', 'drop'}, optional): + If bin edges are not unique, raise ValueError or drop non-uniques. + + Returns: + Series: Categorical or Series of integers if labels is False + The return type (Categorical or Series) depends on the input: a Series + of type category if input is a Series else Categorical. Bins are + represented as categories when categorical data is returned. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 5edcd19e6200db9b9ebe3d4945816b3ebf1f7bcd Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 26 Oct 2023 15:20:15 -0700 Subject: [PATCH 114/921] feat: add unstack to series, add level param (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/blocks.py | 26 ++++++++++-- bigframes/dataframe.py | 32 +++++---------- bigframes/series.py | 40 +++++++++++-------- tests/system/conftest.py | 8 +++- tests/system/small/test_dataframe.py | 10 ++++- tests/system/small/test_multiindex.py | 31 ++++++++++++-- .../bigframes_vendored/pandas/core/series.py | 13 ++++++ 7 files changed, 112 insertions(+), 48 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index eab46454779..e8a3968b3db 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -67,6 +67,10 @@ _MONOTONIC_DECREASING = "monotonic_decreasing" +LevelType = typing.Union[str, int] +LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] + + class BlockHolder(typing.Protocol): """Interface for mutable objects with state represented by a block value object.""" @@ -1423,9 +1427,7 @@ def _get_unique_values( raise ValueError(f"Too many unique values: {pd_values}") if len(columns) > 1: - return pd.MultiIndex.from_frame( - pd_values.sort_values(by=list(pd_values.columns), na_position="first") - ) + return pd.MultiIndex.from_frame(pd_values) else: return pd.Index(pd_values.squeeze(axis=1).sort_values(na_position="first")) @@ -1611,6 +1613,24 @@ def cached(self) -> Block: index_labels=self.index_labels, ) + def resolve_index_level(self, level: LevelsType) -> typing.Sequence[str]: + if utils.is_list_like(level): + levels = list(level) + else: + levels = [level] + resolved_level_ids = [] + for level_ref in levels: + if isinstance(level_ref, int): + resolved_level_ids.append(self.index_columns[level_ref]) + elif isinstance(level_ref, typing.Hashable): + matching_ids = self.index_name_to_col_id.get(level_ref, []) + if len(matching_ids) != 1: + raise ValueError("level name cannot be found or is ambiguous") + resolved_level_ids.append(matching_ids[0]) + else: + raise ValueError(f"Unexpected level: {level_ref}") + return resolved_level_ids + def _is_monotonic( self, column_ids: typing.Union[str, Sequence[str]], increasing: bool ) -> bool: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 5c0d9b78e11..869075a9706 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1038,22 +1038,7 @@ def reorder_levels(self, order: LevelsType, axis: int | str = 0): raise ValueError("Columns must be a multiindex to reorder levels.") def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: - if utils.is_list_like(level): - levels = list(level) - else: - levels = [level] - resolved_level_ids = [] - for level_ref in levels: - if isinstance(level_ref, int): - resolved_level_ids.append(self._block.index_columns[level_ref]) - elif isinstance(level_ref, typing.Hashable): - matching_ids = self._block.index_name_to_col_id.get(level_ref, []) - if len(matching_ids) != 1: - raise ValueError("level name cannot be found or is ambiguous") - resolved_level_ids.append(matching_ids[0]) - else: - raise ValueError(f"Unexpected level: {level_ref}") - return resolved_level_ids + return self._block.resolve_index_level(level) def rename(self, *, columns: Mapping[blocks.Label, blocks.Label]) -> DataFrame: block = self._block.rename(columns=columns) @@ -1802,20 +1787,25 @@ def _stack_multi(self, level: LevelsType = -1): block = block.stack(levels=len(level)) return DataFrame(block) - def unstack(self): + def unstack(self, level: LevelsType = -1): + if isinstance(level, int) or isinstance(level, str): + level = [level] + block = self._block # Special case, unstack with mono-index transpose into a series if self.index.nlevels == 1: block = block.stack(how="right", levels=self.columns.nlevels) return bigframes.series.Series(block) - # Pivot by last level of index - index_ids = block.index_columns + # Pivot by index levels + unstack_ids = self._resolve_levels(level) block = block.reset_index(drop=False) - block = block.set_index(index_ids[:-1]) + block = block.set_index( + [col for col in self._block.index_columns if col not in unstack_ids] + ) pivot_block = block.pivot( - columns=[index_ids[-1]], + columns=unstack_ids, values=self._block.value_columns, values_in_index=True, ) diff --git a/bigframes/series.py b/bigframes/series.py index 49df8ab61ed..c1914527834 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -352,22 +352,7 @@ def reorder_levels(self, order: LevelsType, axis: int | str = 0): return Series(self._block.reorder_levels(resolved_level_ids)) def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: - if _is_list_like(level): - levels = list(level) - else: - levels = [level] - resolved_level_ids = [] - for level_ref in levels: - if isinstance(level_ref, int): - resolved_level_ids.append(self._block.index_columns[level_ref]) - elif isinstance(level_ref, typing.Hashable): - matching_ids = self._block.index_name_to_col_id.get(level_ref, []) - if len(matching_ids) != 1: - raise ValueError("level name cannot be found or is ambiguous") - resolved_level_ids.append(matching_ids[0]) - else: - raise ValueError(f"Unexpected level: {level_ref}") - return resolved_level_ids + return self._block.resolve_index_level(level) def between(self, left, right, inclusive="both"): if inclusive not in ["both", "neither", "left", "right"]: @@ -918,6 +903,29 @@ def argmin(self) -> int: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) + def unstack(self, level: LevelsType = -1): + if isinstance(level, int) or isinstance(level, str): + level = [level] + + block = self._block + + if self.index.nlevels == 1: + raise ValueError("Series must have multi-index to unstack") + + # Pivot by index levels + unstack_ids = self._resolve_levels(level) + block = block.reset_index(drop=False) + block = block.set_index( + [col for col in self._block.index_columns if col not in unstack_ids] + ) + + pivot_block = block.pivot( + columns=unstack_ids, + values=self._block.value_columns, + values_in_index=False, + ) + return bigframes.dataframe.DataFrame(pivot_block) + def idxmax(self) -> blocks.Label: block = self._block.order_by( [ diff --git a/tests/system/conftest.py b/tests/system/conftest.py index cb664302a8f..8885b03d34c 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -400,7 +400,11 @@ def hockey_df( hockey_table_id: str, session: bigframes.Session ) -> bigframes.dataframe.DataFrame: """DataFrame pointing at test data.""" - return session.read_gbq(hockey_table_id) + return ( + session.read_gbq(hockey_table_id) + .set_index(["player_name", "season"]) + .sort_index() + ) @pytest.fixture(scope="session") @@ -419,7 +423,7 @@ def hockey_pandas_df() -> pd.DataFrame: "season": pd.Int64Dtype(), }, ) - df.index = df.index.astype("Int64") + df = df.set_index(["player_name", "season"]).sort_index() return df diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 84e8def83b3..a746a1867cf 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1949,8 +1949,14 @@ def test_df_pivot(scalars_dfs, values, index, columns): ], ) def test_df_pivot_hockey(hockey_df, hockey_pandas_df, values, index, columns): - bf_result = hockey_df.pivot(values=values, index=index, columns=columns).to_pandas() - pd_result = hockey_pandas_df.pivot(values=values, index=index, columns=columns) + bf_result = ( + hockey_df.reset_index() + .pivot(values=values, index=index, columns=columns) + .to_pandas() + ) + pd_result = hockey_pandas_df.reset_index().pivot( + values=values, index=index, columns=columns + ) # Pandas produces NaN, where bq dataframes produces pd.NA pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index b5c78de69c2..a87dacae044 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -909,13 +909,36 @@ def test_column_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_i pandas.testing.assert_frame_equal(bf_result, pd_result) -def test_multi_index_unstack(hockey_df, hockey_pandas_df): +@pytest.mark.parametrize( + ("level",), + [(["position", "team_name"],), ([-2, -1],), (["position"],), ("season",), (-3,)], +) +def test_df_multi_index_unstack(hockey_df, hockey_pandas_df, level): bf_result = ( - hockey_df.set_index(["team_name", "season", "position"]).unstack().to_pandas() + hockey_df.set_index(["team_name", "position"], append=True) + .unstack(level=level) + .to_pandas() ) pd_result = hockey_pandas_df.set_index( - ["team_name", "season", "position"] - ).unstack() + ["team_name", "position"], append=True + ).unstack(level=level) + + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("level",), + [(["position", "team_name"],), ([-2, -1],), (["position"],), ("season",), (-3,)], +) +def test_series_multi_index_unstack(hockey_df, hockey_pandas_df, level): + bf_result = ( + hockey_df.set_index(["team_name", "position"], append=True)["number"] + .unstack(level=level) + .to_pandas() + ) + pd_result = hockey_pandas_df.set_index(["team_name", "position"], append=True)[ + "number" + ].unstack(level=level) pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index bd1f9a9a185..f0e13e16f5a 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1654,6 +1654,19 @@ def clip(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def unstack(self, level): + """ + Unstack, also known as pivot, Series with MultiIndex to produce DataFrame. + + Args: + level (int, str, or list of these, default last level): + Level(s) to unstack, can pass level name. + + Returns: + DataFrame: Unstacked Series. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def argmax(self): """ Return int position of the smallest value in the Series. From 3afd4a35f4c38dad86dab17ff62444cd418cab88 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 26 Oct 2023 18:02:14 -0500 Subject: [PATCH 115/921] feat: add `DataFrame.to_pandas_batches()` to download large `DataFrame` objects (#136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on https://togithub.com/googleapis/python-bigquery-dataframes/pull/132 Towards internal issue 280662868 🦕 --- bigframes/core/blocks.py | 29 +++++++++++-- bigframes/dataframe.py | 4 ++ bigframes/session/_io/pandas.py | 20 +++++++-- tests/system/small/test_dataframe_io.py | 8 ++++ tests/unit/session/test_io_pandas.py | 56 +++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 8 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index e8a3968b3db..9db193a04e5 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -416,6 +416,30 @@ def to_pandas( ) return df, query_job + def to_pandas_batches(self): + """Download results one message at a time.""" + dtypes = dict(zip(self.index_columns, self.index_dtypes)) + dtypes.update(zip(self.value_columns, self.dtypes)) + results_iterator, _ = self._expr.start_query() + for arrow_table in results_iterator.to_arrow_iterable( + bqstorage_client=self._expr._session.bqstoragereadclient + ): + df = bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) + self._copy_index_to_pandas(df) + yield df + + def _copy_index_to_pandas(self, df: pd.DataFrame): + """Set the index on pandas DataFrame to match this block. + + Warning: This method modifies ``df`` inplace. + """ + if self.index_columns: + df.set_index(list(self.index_columns), inplace=True) + # Pandas names is annotated as list[str] rather than the more + # general Sequence[Label] that BigQuery DataFrames has. + # See: https://github.com/pandas-dev/pandas-stubs/issues/804 + df.index.names = self.index.names # type: ignore + def _compute_and_count( self, value_keys: Optional[Iterable[str]] = None, @@ -489,10 +513,7 @@ def _compute_and_count( else: total_rows = results_iterator.total_rows df = self._to_dataframe(results_iterator) - - if self.index_columns: - df.set_index(list(self.index_columns), inplace=True) - df.index.names = self.index.names # type: ignore + self._copy_index_to_pandas(df) return df, total_rows, query_job diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 869075a9706..3fd83198768 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -893,6 +893,10 @@ def to_pandas( self._set_internal_query_job(query_job) return df.set_axis(self._block.column_labels, axis=1, copy=False) + def to_pandas_batches(self) -> Iterable[pandas.DataFrame]: + """Stream DataFrame results to an iterable of pandas DataFrame""" + return self._block.to_pandas_batches() + def _compute_dry_run(self) -> bigquery.QueryJob: return self._block._compute_dry_run() diff --git a/bigframes/session/_io/pandas.py b/bigframes/session/_io/pandas.py index 163127b546d..1af00a2d015 100644 --- a/bigframes/session/_io/pandas.py +++ b/bigframes/session/_io/pandas.py @@ -46,20 +46,32 @@ def arrow_to_pandas( # Preserve NA/NaN distinction. Note: This is currently needed, even if we use # nullable Float64Dtype in the types_mapper. See: # https://github.com/pandas-dev/pandas/issues/55668 + mask = pyarrow.compute.is_null(column) + nonnull = pyarrow.compute.fill_null(column, float("nan")) # Regarding type: ignore, this class has been public at this # location since pandas 1.2.0. See: # https://pandas.pydata.org/docs/dev/reference/api/pandas.arrays.FloatingArray.html pd_array = pandas.arrays.FloatingArray( # type: ignore - column.to_numpy(), - pyarrow.compute.is_null(column).to_numpy(), + nonnull.to_numpy() + if isinstance(nonnull, pyarrow.ChunkedArray) + else nonnull.to_numpy(zero_copy_only=False), + mask.to_numpy() + if isinstance(mask, pyarrow.ChunkedArray) + else mask.to_numpy(zero_copy_only=False), ) series = pandas.Series(pd_array, dtype=dtype) elif dtype == pandas.Int64Dtype(): # Avoid out-of-bounds errors in Pandas 1.5.x, which incorrectly # casts to float64 in an intermediate step. + mask = pyarrow.compute.is_null(column) + nonnull = pyarrow.compute.fill_null(column, 0) pd_array = pandas.arrays.IntegerArray( - pyarrow.compute.fill_null(column, 0).to_numpy(), - pyarrow.compute.is_null(column).to_numpy(), + nonnull.to_numpy() + if isinstance(nonnull, pyarrow.ChunkedArray) + else nonnull.to_numpy(zero_copy_only=False), + mask.to_numpy() + if isinstance(mask, pyarrow.ChunkedArray) + else mask.to_numpy(zero_copy_only=False), ) series = pandas.Series(pd_array, dtype=dtype) elif isinstance(dtype, pandas.ArrowDtype): diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index d60083a837e..8f5d706f621 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -83,6 +83,14 @@ def test_to_pandas_array_struct_correct_result(session): ) +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 + for df in scalars_df_default_index.to_pandas_batches(): + actual = df.dtypes + pd.testing.assert_series_equal(actual, expected) + + @pytest.mark.parametrize( ("index"), [True, False], diff --git a/tests/unit/session/test_io_pandas.py b/tests/unit/session/test_io_pandas.py index 8b95977ec3d..0f6f5dae03b 100644 --- a/tests/unit/session/test_io_pandas.py +++ b/tests/unit/session/test_io_pandas.py @@ -231,6 +231,62 @@ ), id="scalar-dtypes", ), + pytest.param( + pyarrow.Table.from_pydict( + { + "bool": pyarrow.chunked_array( + [[True, None], [True, False]], + type=pyarrow.bool_(), + ), + "bytes": pyarrow.chunked_array( + [[b"123", None], [b"abc", b"xyz"]], + type=pyarrow.binary(), + ), + "float": pyarrow.chunked_array( + [[1.0, None], [float("nan"), -1.0]], + type=pyarrow.float64(), + ), + "int": pyarrow.chunked_array( + [[1, None], [-1, 2**63 - 1]], + type=pyarrow.int64(), + ), + "string": pyarrow.chunked_array( + [["123", None], ["abc", "xyz"]], + type=pyarrow.string(), + ), + } + ), + { + "bool": "boolean", + "bytes": "object", + "float": pandas.Float64Dtype(), + "int": pandas.Int64Dtype(), + "string": "string[pyarrow]", + }, + pandas.DataFrame( + { + "bool": pandas.Series([True, None, True, False], dtype="boolean"), + "bytes": [b"123", None, b"abc", b"xyz"], + "float": pandas.Series( + pandas.arrays.FloatingArray( # type: ignore + numpy.array( + [1.0, float("nan"), float("nan"), -1.0], dtype="float64" + ), + numpy.array([False, True, False, False], dtype="bool"), + ), + dtype=pandas.Float64Dtype(), + ), + "int": pandas.Series( + [1, None, -1, 2**63 - 1], + dtype=pandas.Int64Dtype(), + ), + "string": pandas.Series( + ["123", None, "abc", "xyz"], dtype="string[pyarrow]" + ), + } + ), + id="scalar-dtypes-chunked_array", + ), pytest.param( pyarrow.Table.from_pydict( { From 39df43e243ac0374d1a1eb2a75779324825afbe9 Mon Sep 17 00:00:00 2001 From: Bradford Orr <15842009+orrbradford@users.noreply.github.com> Date: Thu, 26 Oct 2023 16:46:14 -0700 Subject: [PATCH 116/921] =?UTF-8?q?fix:=20resolve=20plotly=20rendering=20i?= =?UTF-8?q?ssue=20by=20using=20ipython=20html=20for=20job=20pro=E2=80=A6?= =?UTF-8?q?=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …gress messages Fixes bug that was preventing plotly rendering to show after the progress bar. Original ipywidgets implementation isn't necessary for basic opening of urls Screen recording: https://togithub.com/googleapis/python-bigquery-dataframes/assets/15842009/5225ce05-117a-4808-9ff0-cb2c3aaf3a40 Internal bug: b/297062404 --- bigframes/formatting_helpers.py | 24 ++++--- tests/system/small/test_progress_bar.py | 83 +++++++++++-------------- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index 6851bdd2bd4..752aeb7a10b 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -16,6 +16,7 @@ # TODO(orrbradford): cleanup up typings and documenttion in this file import datetime +import random from typing import Any, Optional, Union import google.api_core.exceptions as api_core_exceptions @@ -57,9 +58,9 @@ def repr_query_job_html(query_job: Optional[bigquery.QueryJob]): Pywidget html table. """ if query_job is None: - return widgets.HTML("No job information available") + return display.HTML("No job information available") if query_job.dry_run: - return widgets.HTML( + return display.HTML( f"Computation deferred. Computation will process {get_formatted_bytes(query_job.total_bytes_processed)}" ) table_html = "" @@ -125,16 +126,20 @@ def wait_for_query_job( Returns: A row iterator over the query results. """ - loading_bar = widgets.HTML(get_query_job_loading_html(query_job)) if progress_bar == "auto": progress_bar = "notebook" if in_ipython() else "terminal" try: if progress_bar == "notebook": - display.display(loading_bar) + display_id = str(random.random()) + loading_bar = display.HTML(get_query_job_loading_html(query_job)) + display.display(loading_bar, display_id=display_id) query_result = query_job.result(max_results=max_results) query_job.reload() - loading_bar.value = get_query_job_loading_html(query_job) + display.update_display( + display.HTML(get_query_job_loading_html(query_job)), + display_id=display_id, + ) elif progress_bar == "terminal": initial_loading_bar = get_query_job_loading_string(query_job) print(initial_loading_bar) @@ -171,16 +176,19 @@ def wait_for_job(job: GenericJob, progress_bar: Optional[str] = None): progress_bar (str, Optional): Which progress bar to show. """ - loading_bar = widgets.HTML(get_base_job_loading_html(job)) if progress_bar == "auto": progress_bar = "notebook" if in_ipython() else "terminal" try: if progress_bar == "notebook": - display.display(loading_bar) + display_id = str(random.random()) + loading_bar = display.HTML(get_base_job_loading_html(job)) + display.display(loading_bar, display_id=display_id) job.result() job.reload() - loading_bar.value = get_base_job_loading_html(job) + display.update_display( + display.HTML(get_base_job_loading_html(job)), display_id=display_id + ) elif progress_bar == "terminal": inital_loading_bar = get_base_job_loading_string(job) print(inital_loading_bar) diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index 00380c26390..f7fc4eaa8f6 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re import tempfile import pandas as pd @@ -19,94 +20,84 @@ import bigframes as bf import bigframes.formatting_helpers as formatting_helpers +job_load_message_regex = r"\w+ job [\w-]+ is \w+\." + def test_progress_bar_dataframe( penguins_df_default_index: bf.dataframe.DataFrame, capsys ): - bf.options.display.progress_bar = "notebook" + bf.options.display.progress_bar = "terminal" + capsys.readouterr() # clear output penguins_df_default_index.to_pandas() - html_check = "HTML(value=" - open_job_check = "Open Job" - lines = capsys.readouterr().out.split("\n") - lines = [line for line in lines if len(line) > 0] - assert len(lines) > 0 + + assert_loading_msg_exist(capsys.readouterr().out) assert penguins_df_default_index.query_job is not None - for line in lines: - assert html_check in line and open_job_check in line def test_progress_bar_series(penguins_df_default_index: bf.dataframe.DataFrame, capsys): - bf.options.display.progress_bar = "notebook" + bf.options.display.progress_bar = "terminal" series = penguins_df_default_index["body_mass_g"].head(10) + capsys.readouterr() # clear output series.to_pandas() - html_check = "HTML(value=" - open_job_check = "Open Job" - lines = capsys.readouterr().out.split("\n") - lines = [line for line in lines if len(line) > 0] - assert len(lines) > 0 + + assert_loading_msg_exist(capsys.readouterr().out) assert series.query_job is not None - for line in lines: - assert html_check in line and open_job_check in line def test_progress_bar_scalar(penguins_df_default_index: bf.dataframe.DataFrame, capsys): - bf.options.display.progress_bar = "notebook" + bf.options.display.progress_bar = "terminal" + capsys.readouterr() # clear output penguins_df_default_index["body_mass_g"].head(10).mean() - html_check = "HTML(value=" - open_job_check = "Open Job" - lines = capsys.readouterr().out.split("\n") - lines = [line for line in lines if len(line) > 0] - assert len(lines) > 0 - for line in lines: - assert html_check in line and open_job_check in line + + assert_loading_msg_exist(capsys.readouterr().out) def test_progress_bar_read_gbq(session: bf.Session, penguins_table_id: str, capsys): - bf.options.display.progress_bar = "notebook" + bf.options.display.progress_bar = "terminal" + capsys.readouterr() # clear output session.read_gbq(penguins_table_id) - html_check = "HTML(value=" - open_job_check = "Open Job" - lines = capsys.readouterr().out.split("\n") - lines = [line for line in lines if len(line) > 0] - assert len(lines) > 0 - for line in lines: - assert html_check in line and open_job_check in line + + assert_loading_msg_exist(capsys.readouterr().out) def test_progress_bar_extract_jobs( penguins_df_default_index: bf.dataframe.DataFrame, gcs_folder, capsys ): - bf.options.display.progress_bar = "notebook" + bf.options.display.progress_bar = "terminal" path = gcs_folder + "test_read_csv_progress_bar*.csv" + capsys.readouterr() # clear output penguins_df_default_index.to_csv(path) - html_check = "HTML(value=" - open_job_check = "Open Job" - lines = capsys.readouterr().out.split("\n") - lines = [line for line in lines if len(line) > 0] - assert len(lines) > 0 - for line in lines: - assert html_check in line and open_job_check in line + + assert_loading_msg_exist(capsys.readouterr().out) def test_progress_bar_load_jobs( session: bf.Session, penguins_pandas_df_default_index: pd.DataFrame, capsys ): - bf.options.display.progress_bar = "notebook" + bf.options.display.progress_bar = "terminal" with tempfile.TemporaryDirectory() as dir: path = dir + "/test_read_csv_progress_bar*.csv" penguins_pandas_df_default_index.to_csv(path, index=False) + capsys.readouterr() # clear output session.read_csv(path) - html_check = "HTML(value=" - open_job_check = "Open Job" - lines = capsys.readouterr().out.split("\n") + + assert_loading_msg_exist(capsys.readouterr().out) + + +def assert_loading_msg_exist(capystOut: str, pattern=job_load_message_regex): + numLoadingMsg = 0 + lines = capystOut.split("\n") lines = [line for line in lines if len(line) > 0] + assert len(lines) > 0 for line in lines: - assert html_check in line and open_job_check in line + if re.match(pattern, line) is not None: + numLoadingMsg += 1 + assert numLoadingMsg > 0 def test_query_job_repr_html(penguins_df_default_index: bf.dataframe.DataFrame): - bf.options.display.progress_bar = "notebook" + bf.options.display.progress_bar = "terminal" penguins_df_default_index._block._expr._session.bqclient.default_query_job_config.use_query_cache = ( False ) From eceeb221f553644411b954ae2db0f0ae5a505687 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 26 Oct 2023 17:28:13 -0700 Subject: [PATCH 117/921] refactor: ArrayValue is now a tree that defers conversion to ibis (#110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 1221 +++-------------- bigframes/core/block_transforms.py | 25 +- bigframes/core/blocks.py | 34 +- bigframes/core/compile/__init__.py | 21 + bigframes/core/compile/compiled.py | 1121 +++++++++++++++ bigframes/core/compile/compiler.py | 185 +++ .../core/{joins => compile}/row_identity.py | 14 +- .../core/{joins => compile}/single_column.py | 35 +- bigframes/core/groupby/__init__.py | 20 +- bigframes/core/indexers.py | 6 +- bigframes/core/indexes/index.py | 15 +- bigframes/core/joins/__init__.py | 9 +- bigframes/core/nodes.py | 245 ++++ bigframes/core/ordering.py | 4 +- bigframes/core/window_spec.py | 35 + bigframes/dataframe.py | 18 +- bigframes/ml/metrics.py | 2 +- bigframes/operations/base.py | 4 +- bigframes/series.py | 34 +- bigframes/session/__init__.py | 44 +- tests/system/small/test_progress_bar.py | 4 +- tests/system/small/test_series.py | 4 +- tests/system/small/test_session.py | 9 +- tests/unit/core/test_blocks.py | 5 +- tests/unit/resources.py | 17 +- tests/unit/test_core.py | 37 +- 26 files changed, 1996 insertions(+), 1172 deletions(-) create mode 100644 bigframes/core/compile/__init__.py create mode 100644 bigframes/core/compile/compiled.py create mode 100644 bigframes/core/compile/compiler.py rename bigframes/core/{joins => compile}/row_identity.py (94%) rename bigframes/core/{joins => compile}/single_column.py (87%) create mode 100644 bigframes/core/nodes.py create mode 100644 bigframes/core/window_spec.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 6c78a07f3ba..4653f0ab6aa 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -14,29 +14,21 @@ from __future__ import annotations from dataclasses import dataclass -import functools -import math -import textwrap +import io import typing -from typing import Collection, Iterable, Literal, Optional, Sequence, Tuple +from typing import Iterable, Literal, Optional, Sequence, Tuple from google.cloud import bigquery import ibis -import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types import pandas -import bigframes.constants as constants +import bigframes.core.compile as compiled import bigframes.core.guid -from bigframes.core.ordering import ( - encode_order_string, - ExpressionOrdering, - IntegerEncoding, - OrderingColumnReference, - reencode_order_string, - StringEncoding, -) -import bigframes.core.utils as utils +import bigframes.core.nodes as nodes +from bigframes.core.ordering import OrderingColumnReference +import bigframes.core.ordering as orderings +from bigframes.core.window_spec import WindowSpec import bigframes.dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -49,470 +41,190 @@ @dataclass(frozen=True) -class WindowSpec: +class ArrayValue: """ - Specifies a window over which aggregate and analytic function may be applied. - grouping_keys: set of column ids to group on - preceding: Number of preceding rows in the window - following: Number of preceding rows in the window - ordering: List of columns ids and ordering direction to override base ordering + ArrayValue is an immutable type representing a 2D array with per-column types. """ - grouping_keys: typing.Sequence[str] = tuple() - ordering: typing.Sequence[OrderingColumnReference] = tuple() - preceding: typing.Optional[int] = None - following: typing.Optional[int] = None - min_periods: int = 0 - - -# TODO(swast): We might want to move this to it's own sub-module. -class ArrayValue: - """Immutable BigQuery DataFrames expression tree. - - Note: Usage of this class is considered to be private and subject to change - at any time. + node: nodes.BigFrameNode - This class is a wrapper around Ibis expressions. Its purpose is to defer - Ibis projection operations to keep generated SQL small and correct when - mixing and matching columns from different versions of a DataFrame. - - Args: - session: - A BigQuery DataFrames session to allow more flexibility in running - queries. - table: An Ibis table expression. - columns: Ibis value expressions that can be projected as columns. - hidden_ordering_columns: Ibis value expressions to store ordering. - ordering: An ordering property of the data frame. - predicates: A list of filters on the data frame. - """ - - def __init__( - self, + @classmethod + def from_ibis( + cls, session: Session, table: ibis_types.Table, columns: Sequence[ibis_types.Value], - hidden_ordering_columns: Optional[Sequence[ibis_types.Value]] = None, - ordering: ExpressionOrdering = ExpressionOrdering(), - predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + hidden_ordering_columns: Sequence[ibis_types.Value], + ordering: orderings.ExpressionOrdering, ): - self._session = session - self._table = table - self._predicates = tuple(predicates) if predicates is not None else () - # TODO: Validate ordering - if not ordering.total_ordering_columns: - raise ValueError("Must have total ordering defined by one or more columns") - self._ordering = ordering - # Allow creating a DataFrame directly from an Ibis table expression. - # TODO(swast): Validate that each column references the same table (or - # no table for literal values). - self._columns = tuple(columns) - - # Meta columns store ordering, or other data that doesn't correspond to dataframe columns - self._hidden_ordering_columns = ( - tuple(hidden_ordering_columns) - if hidden_ordering_columns is not None - else () - ) - - # To allow for more efficient lookup by column name, create a - # dictionary mapping names to column values. - self._column_names = {column.get_name(): column for column in self._columns} - self._hidden_ordering_column_names = { - column.get_name(): column for column in self._hidden_ordering_columns - } - ### Validation - value_col_ids = self._column_names.keys() - hidden_col_ids = self._hidden_ordering_column_names.keys() - - all_columns = value_col_ids | hidden_col_ids - ordering_valid = all( - col.column_id in all_columns for col in ordering.all_ordering_columns + node = nodes.ReadGbqNode( + table=table, + table_session=session, + columns=tuple(columns), + hidden_ordering_columns=tuple(hidden_ordering_columns), + ordering=ordering, ) - if value_col_ids & hidden_col_ids: - raise ValueError( - f"Keys in both hidden and exposed list: {value_col_ids & hidden_col_ids}" - ) - if not ordering_valid: - raise ValueError(f"Illegal ordering keys: {ordering.all_ordering_columns}") + return cls(node) @classmethod - def mem_expr_from_pandas( - cls, - pd_df: pandas.DataFrame, - session: Optional[Session], - ) -> ArrayValue: - """ - Builds an in-memory only (SQL only) expr from a pandas dataframe. + def from_pandas(cls, pd_df: pandas.DataFrame): + iobytes = io.BytesIO() + # Discard row labels and use simple string ids for columns + column_ids = tuple(str(label) for label in pd_df.columns) + pd_df.reset_index(drop=True).set_axis(column_ids, axis=1).to_feather(iobytes) + node = nodes.ReadLocalNode(iobytes.getvalue(), column_ids=column_ids) + return cls(node) - Caution: If session is None, only a subset of expr functionality will - be available (null Session is usually not supported). - """ - # We can't include any hidden columns in the ArrayValue constructor, so - # grab the column names before we add the hidden ordering column. - column_names = [str(column) for column in pd_df.columns] - # Make sure column names are all strings. - pd_df = pd_df.set_axis(column_names, axis="columns") - pd_df = pd_df.assign(**{ORDER_ID_COLUMN: range(len(pd_df))}) - - # ibis memtable cannot handle NA, must convert to None - pd_df = pd_df.astype("object") # type: ignore - pd_df = pd_df.where(pandas.notnull(pd_df), None) + @property + def column_ids(self) -> typing.Sequence[str]: + return self.compile().column_ids - # NULL type isn't valid in BigQuery, so retry with an explicit schema in these cases. - keys_memtable = ibis.memtable(pd_df) - schema = keys_memtable.schema() - new_schema = [] - for column_index, column in enumerate(schema): - if column == ORDER_ID_COLUMN: - new_type: ibis_dtypes.DataType = ibis_dtypes.int64 - else: - column_type = schema[column] - # The autodetected type might not be one we can support, such - # as NULL type for empty rows, so convert to a type we do - # support. - new_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype( - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(column_type) - ) - # TODO(swast): Ibis memtable doesn't use backticks in struct - # field names, so spaces and other characters aren't allowed in - # the memtable context. Blocked by - # https://github.com/ibis-project/ibis/issues/7187 - column = f"col_{column_index}" - new_schema.append((column, new_type)) + @property + def session(self) -> Session: + required_session = self.node.session + from bigframes import get_global_session - # must set non-null column labels. these are not the user-facing labels - pd_df = pd_df.set_axis( - [column for column, _ in new_schema], - axis="columns", - ) - keys_memtable = ibis.memtable(pd_df, schema=ibis.schema(new_schema)) + return self.node.session[0] if required_session else get_global_session() - return cls( - session, # type: ignore # Session cannot normally be none, see "caution" above - keys_memtable, - columns=[ - keys_memtable[f"col_{column_index}"].name(column) - for column_index, column in enumerate(column_names) - ], - ordering=ExpressionOrdering( - ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - ), - hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), - ) - - @property - def columns(self) -> typing.Tuple[ibis_types.Value, ...]: - return self._columns + def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: + return self.compile().get_column_type(key) - @property - def column_ids(self) -> typing.Sequence[str]: - return tuple(self._column_names.keys()) + def compile(self) -> compiled.CompiledArrayValue: + return compiled.compile_node(self.node) - @property - def _hidden_column_ids(self) -> typing.Sequence[str]: - return tuple(self._hidden_ordering_column_names.keys()) + def shape(self) -> typing.Tuple[int, int]: + """Returns dimensions as (length, width) tuple.""" + width = len(self.compile().columns) + count_expr = self.compile()._to_ibis_expr("unordered").count() - @property - def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: - """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" - return ( - _reduce_predicate_list(self._predicates).name(PREDICATE_COLUMN) - if self._predicates - else None + # Support in-memory engines for hermetic unit tests. + if not self.node.session: + try: + length = ibis.pandas.connect({}).execute(count_expr) + return (length, width) + except Exception: + # Not all cases can be handled by pandas engine + pass + + sql = self.session.ibis_client.compile(count_expr) + row_iterator, _ = self.session._start_query( + sql=sql, + max_results=1, ) + length = next(row_iterator)[0] + return (length, width) - @property - def _ibis_order(self) -> Sequence[ibis_types.Value]: - """Returns a sequence of ibis values which can be directly used to order a table expression. Has direction modifiers applied.""" - return _convert_ordering_to_table_values( - {**self._column_names, **self._hidden_ordering_column_names}, - self._ordering.all_ordering_columns, + def to_sql( + self, + offset_column: typing.Optional[str] = None, + col_id_overrides: typing.Mapping[str, str] = {}, + sorted: bool = False, + ) -> str: + return self.compile().to_sql( + offset_column=offset_column, + col_id_overrides=col_id_overrides, + sorted=sorted, ) - def builder(self) -> ArrayValueBuilder: - """Creates a mutable builder for expressions.""" - # Since ArrayValue is intended to be immutable (immutability offers - # potential opportunities for caching, though we might need to introduce - # more node types for that to be useful), we create a builder class. - return ArrayValueBuilder( - self._session, - self._table, - columns=self._columns, - hidden_ordering_columns=self._hidden_ordering_columns, - ordering=self._ordering, - predicates=self._predicates, + def start_query( + self, + job_config: Optional[bigquery.job.QueryJobConfig] = None, + max_results: Optional[int] = None, + *, + sorted: bool = True, + ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """Execute a query and return metadata about the results.""" + # TODO(swast): Cache the job ID so we can look it up again if they ask + # for the results? We'd need a way to invalidate the cache if DataFrame + # becomes mutable, though. Or move this method to the immutable + # expression class. + # TODO(swast): We might want to move this method to Session and/or + # provide our own minimal metadata class. Tight coupling to the + # BigQuery client library isn't ideal, especially if we want to support + # a LocalSession for unit testing. + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + sql = self.to_sql(sorted=sorted) # type:ignore + return self.session._start_query( + sql=sql, + job_config=job_config, + max_results=max_results, ) - def drop_columns(self, columns: Iterable[str]) -> ArrayValue: - # Must generate offsets if we are dropping a column that ordering depends on - expr = self - for ordering_column in set(columns).intersection( - [col.column_id for col in self._ordering.ordering_value_columns] - ): - expr = self._hide_column(ordering_column) - - expr_builder = expr.builder() - remain_cols = [ - column for column in expr.columns if column.get_name() not in columns - ] - expr_builder.columns = remain_cols - return expr_builder.build() - - def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: - ibis_type = typing.cast( - bigframes.dtypes.IbisDtype, self._get_any_column(key).type() + def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: + """Write the ArrayValue to a session table and create a new block object that references it.""" + compiled = self.compile() + ibis_expr = compiled._to_ibis_expr("unordered", expose_hidden_cols=True) + destination = self.session._ibis_to_session_table( + ibis_expr, cluster_cols=cluster_cols, api_name="cache" ) - return typing.cast( - bigframes.dtypes.Dtype, - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), + table_expression = self.session.ibis_client.table( + f"{destination.project}.{destination.dataset_id}.{destination.table_id}" + ) + new_columns = [table_expression[column] for column in compiled.column_ids] + new_hidden_columns = [ + table_expression[column] + for column in compiled._hidden_ordering_column_names + ] + return ArrayValue.from_ibis( + self.session, + table_expression, + columns=new_columns, + hidden_ordering_columns=new_hidden_columns, + ordering=compiled._ordering, ) - def _get_ibis_column(self, key: str) -> ibis_types.Value: - """Gets the Ibis expression for a given column.""" - if key not in self.column_ids: - raise ValueError( - "Column name {} not in set of values: {}".format(key, self.column_ids) - ) - return typing.cast(ibis_types.Value, self._column_names[key]) - - def _get_any_column(self, key: str) -> ibis_types.Value: - """Gets the Ibis expression for a given column. Will also get hidden columns.""" - all_columns = {**self._column_names, **self._hidden_ordering_column_names} - if key not in all_columns.keys(): - raise ValueError( - "Column name {} not in set of values: {}".format( - key, all_columns.keys() - ) - ) - return typing.cast(ibis_types.Value, all_columns[key]) + # Operations - def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: - """Gets the Ibis expression for a given hidden column.""" - if key not in self._hidden_ordering_column_names.keys(): - raise ValueError( - "Column name {} not in set of values: {}".format( - key, self._hidden_ordering_column_names.keys() - ) - ) - return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) + def drop_columns(self, columns: Iterable[str]) -> ArrayValue: + return ArrayValue( + nodes.DropColumnsNode(child=self.node, columns=tuple(columns)) + ) def filter(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - condition = typing.cast( - ibis_types.BooleanValue, self._get_ibis_column(predicate_id) - ) - if keep_null: - condition = typing.cast( - ibis_types.BooleanValue, - condition.fillna( - typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) - ), + return ArrayValue( + nodes.FilterNode( + child=self.node, predicate_id=predicate_id, keep_null=keep_null ) - return self._filter(condition) - - def _filter(self, predicate_value: ibis_types.BooleanValue) -> ArrayValue: - """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - expr = self.builder() - expr.ordering = expr.ordering.with_non_sequential() - expr.predicates = [*self._predicates, predicate_value] - return expr.build() + ) def order_by( self, by: Sequence[OrderingColumnReference], stable: bool = False ) -> ArrayValue: - expr_builder = self.builder() - expr_builder.ordering = self._ordering.with_ordering_columns(by, stable=stable) - return expr_builder.build() - - def reversed(self) -> ArrayValue: - expr_builder = self.builder() - expr_builder.ordering = self._ordering.with_reverse() - return expr_builder.build() - - def _uniform_sampling(self, fraction: float) -> ArrayValue: - """Sampling the table on given fraction. - - .. warning:: - The row numbers of result is non-deterministic, avoid to use. - """ - table = self._to_ibis_expr( - "unordered", expose_hidden_cols=True, fraction=fraction - ) - columns = [table[column_name] for column_name in self._column_names] - hidden_ordering_columns = [ - table[column_name] for column_name in self._hidden_ordering_column_names - ] return ArrayValue( - self._session, - table, - columns=columns, - hidden_ordering_columns=hidden_ordering_columns, - ordering=self._ordering, + nodes.OrderByNode(child=self.node, by=tuple(by), stable=stable) ) - @property - def _offsets(self) -> ibis_types.IntegerColumn: - if not self._ordering.is_sequential: - raise ValueError( - "Expression does not have offsets. Generate them first using project_offsets." - ) - if not self._ordering.total_order_col: - raise ValueError( - "Ordering is invalid. Marked as sequential but no total order columns." - ) - column = self._get_any_column(self._ordering.total_order_col.column_id) - return typing.cast(ibis_types.IntegerColumn, column) - - def _project_offsets(self) -> ArrayValue: - """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" - if self._ordering.is_sequential: - return self - # TODO(tbergeron): Enforce total ordering - table = self._to_ibis_expr( - ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN - ) - columns = [table[column_name] for column_name in self._column_names] - ordering = ExpressionOrdering( - ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(True, is_sequential=True), - ) - return ArrayValue( - self._session, - table, - columns=columns, - hidden_ordering_columns=[table[ORDER_ID_COLUMN]], - ordering=ordering, - ) - - def _hide_column(self, column_id) -> ArrayValue: - """Pushes columns to hidden columns list. Used to hide ordering columns that have been dropped or destructively mutated.""" - expr_builder = self.builder() - # Need to rename column as caller might be creating a new row with the same name but different values. - # Can avoid this if don't allow callers to determine ids and instead generate unique ones in this class. - new_name = bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") - expr_builder.hidden_ordering_columns = [ - *self._hidden_ordering_columns, - self._get_ibis_column(column_id).name(new_name), - ] - expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) - return expr_builder.build() + def reversed(self) -> ArrayValue: + return ArrayValue(nodes.ReversedNode(child=self.node)) def promote_offsets(self, col_id: str) -> ArrayValue: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. """ - # Special case: offsets already exist - ordering = self._ordering - - if (not ordering.is_sequential) or (not ordering.total_order_col): - return self._project_offsets().promote_offsets(col_id) - expr_builder = self.builder() - expr_builder.columns = [ - self._get_any_column(ordering.total_order_col.column_id).name(col_id), - *self.columns, - ] - return expr_builder.build() + return ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)) def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: - return self._projection( - [self._get_ibis_column(col_id) for col_id in column_ids] + return ArrayValue( + nodes.SelectNode(child=self.node, column_ids=tuple(column_ids)) ) - def _projection(self, columns: Iterable[ibis_types.Value]) -> ArrayValue: - """Creates a new expression based on this expression with new columns.""" - # TODO(swast): We might want to do validation here that columns derive - # from the same table expression instead of (in addition to?) at - # construction time. - - expr = self - for ordering_column in set(self.column_ids).intersection( - [col_ref.column_id for col_ref in self._ordering.ordering_value_columns] - ): - # Need to hide ordering columns that are being dropped. Alternatively, could project offsets - expr = expr._hide_column(ordering_column) - builder = expr.builder() - builder.columns = list(columns) - new_expr = builder.build() - return new_expr - - def shape(self) -> typing.Tuple[int, int]: - """Returns dimensions as (length, width) tuple.""" - width = len(self.columns) - count_expr = self._to_ibis_expr("unordered").count() - sql = self._session.ibis_client.compile(count_expr) - - # Support in-memory engines for hermetic unit tests. - if not isinstance(sql, str): - length = self._session.ibis_client.execute(count_expr) - else: - row_iterator, _ = self._session._start_query( - sql=sql, - max_results=1, - ) - length = next(row_iterator)[0] - return (length, width) - def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: """Append together multiple ArrayValue objects.""" - if len(other) == 0: - return self - tables = [] - prefix_base = 10 - prefix_size = math.ceil(math.log(len(other) + 1, prefix_base)) - # Must normalize all ids to the same encoding size - max_encoding_size = max( - self._ordering.string_encoding.length, - *[expression._ordering.string_encoding.length for expression in other], - ) - for i, expr in enumerate([self, *other]): - ordering_prefix = str(i).zfill(prefix_size) - table = expr._to_ibis_expr( - ordering_mode="string_encoded", order_col_name=ORDER_ID_COLUMN - ) - # Rename the value columns based on horizontal offset before applying union. - table = table.select( - [ - table[col].name(f"column_{i}") - if col != ORDER_ID_COLUMN - else ( - ordering_prefix - + reencode_order_string( - table[ORDER_ID_COLUMN], max_encoding_size - ) - ).name(ORDER_ID_COLUMN) - for i, col in enumerate(table.columns) - ] - ) - tables.append(table) - combined_table = ibis.union(*tables) - ordering = ExpressionOrdering( - ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - string_encoding=StringEncoding(True, prefix_size + max_encoding_size), - ) return ArrayValue( - self._session, - combined_table, - columns=[ - combined_table[col] - for col in combined_table.columns - if col != ORDER_ID_COLUMN - ], - hidden_ordering_columns=[combined_table[ORDER_ID_COLUMN]], - ordering=ordering, + nodes.ConcatNode(children=tuple([self.node, *[val.node for val in other]])) ) def project_unary_op( self, column_name: str, op: ops.UnaryOp, output_name=None ) -> ArrayValue: """Creates a new expression based on this expression with unary operation applied to one column.""" - value = op._as_ibis(self._get_ibis_column(column_name)).name( - output_name or column_name + return ArrayValue( + nodes.ProjectUnaryOpNode( + child=self.node, input_id=column_name, op=op, output_id=output_name + ) ) - return self._set_or_replace_by_id(output_name or column_name, value) def project_binary_op( self, @@ -522,11 +234,15 @@ def project_binary_op( output_column_id: str, ) -> ArrayValue: """Creates a new expression based on this expression with binary operation applied to two columns.""" - value = op( - self._get_ibis_column(left_column_id), - self._get_ibis_column(right_column_id), - ).name(output_column_id) - return self._set_or_replace_by_id(output_column_id, value) + return ArrayValue( + nodes.ProjectBinaryOpNode( + child=self.node, + left_input_id=left_column_id, + right_input_id=right_column_id, + op=op, + output_id=output_column_id, + ) + ) def project_ternary_op( self, @@ -537,12 +253,16 @@ def project_ternary_op( output_column_id: str, ) -> ArrayValue: """Creates a new expression based on this expression with ternary operation applied to three columns.""" - value = op( - self._get_ibis_column(col_id_1), - self._get_ibis_column(col_id_2), - self._get_ibis_column(col_id_3), - ).name(output_column_id) - return self._set_or_replace_by_id(output_column_id, value) + return ArrayValue( + nodes.ProjectTernaryOpNode( + child=self.node, + input_id1=col_id_1, + input_id2=col_id_2, + input_id3=col_id_3, + op=op, + output_id=output_column_id, + ) + ) def aggregate( self, @@ -557,46 +277,14 @@ def aggregate( by_column_id: column id of the aggregation key, this is preserved through the transform dropna: whether null keys should be dropped """ - table = self._to_ibis_expr("unordered") - stats = { - col_out: agg_op._as_ibis(table[col_in]) - for col_in, agg_op, col_out in aggregations - } - if by_column_ids: - result = table.group_by(by_column_ids).aggregate(**stats) - # Must have deterministic ordering, so order by the unique "by" column - ordering = ExpressionOrdering( - [ - OrderingColumnReference(column_id=column_id) - for column_id in by_column_ids - ], - total_ordering_columns=frozenset(by_column_ids), - ) - columns = tuple(result[key] for key in result.columns) - expr = ArrayValue(self._session, result, columns=columns, ordering=ordering) - if dropna: - for column_id in by_column_ids: - expr = expr._filter( - ops.notnull_op._as_ibis(expr._get_ibis_column(column_id)) - ) - # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation - return expr._project_offsets() - else: - aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} - result = table.aggregate(**aggregates) - # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. - ordering = ExpressionOrdering( - ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), - ) - return ArrayValue( - self._session, - result, - columns=[result[col_id] for col_id in [*stats.keys()]], - hidden_ordering_columns=[result[ORDER_ID_COLUMN]], - ordering=ordering, + return ArrayValue( + nodes.AggregateNode( + child=self.node, + aggregations=tuple(aggregations), + by_column_ids=tuple(by_column_ids), + dropna=dropna, ) + ) def corr_aggregate( self, corr_aggregations: typing.Sequence[typing.Tuple[str, str, str]] @@ -607,25 +295,8 @@ def corr_aggregate( Arguments: corr_aggregations: left_column_id, right_column_id, output_column_id tuples """ - table = self._to_ibis_expr("unordered") - stats = { - col_out: table[col_left].corr(table[col_right], how="pop") - for col_left, col_right, col_out in corr_aggregations - } - aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} - result = table.aggregate(**aggregates) - # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. - ordering = ExpressionOrdering( - ordering_value_columns=[OrderingColumnReference(ORDER_ID_COLUMN)], - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), - ) return ArrayValue( - self._session, - result, - columns=[result[col_id] for col_id in [*stats.keys()]], - hidden_ordering_columns=[result[ORDER_ID_COLUMN]], - ordering=ordering, + nodes.CorrNode(child=self.node, corr_aggregations=tuple(corr_aggregations)) ) def project_window_op( @@ -647,231 +318,17 @@ def project_window_op( never_skip_nulls: will disable null skipping for operators that would otherwise do so skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection """ - column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) - window = self._ibis_window_from_spec(window_spec, allow_ties=op.handles_ties) - - window_op = op._as_ibis(column, window) - - clauses = [] - if op.skips_nulls and not never_skip_nulls: - clauses.append((column.isnull(), ibis.NA)) - if window_spec.min_periods: - if op.skips_nulls: - # Most operations do not count NULL values towards min_periods - observation_count = agg_ops.count_op._as_ibis(column, window) - else: - # Operations like count treat even NULLs as valid observations for the sake of min_periods - # notnull is just used to convert null values to non-null (FALSE) values to be counted - denulled_value = typing.cast(ibis_types.BooleanColumn, column.notnull()) - observation_count = agg_ops.count_op._as_ibis(denulled_value, window) - clauses.append( - ( - observation_count < ibis_types.literal(window_spec.min_periods), - ibis.NA, - ) - ) - if clauses: - case_statement = ibis.case() - for clause in clauses: - case_statement = case_statement.when(clause[0], clause[1]) - case_statement = case_statement.else_(window_op).end() - window_op = case_statement - - result = self._set_or_replace_by_id(output_name or column_name, window_op) - # TODO(tbergeron): Automatically track analytic expression usage and defer reprojection until required for valid query generation. - return result._reproject_to_table() if not skip_reproject_unsafe else result - - def to_sql( - self, - offset_column: typing.Optional[str] = None, - col_id_overrides: typing.Mapping[str, str] = {}, - sorted: bool = False, - ) -> str: - offsets_id = offset_column or ORDER_ID_COLUMN - - sql = self._session.ibis_client.compile( - self._to_ibis_expr( - ordering_mode="offset_col" - if (offset_column or sorted) - else "unordered", - order_col_name=offsets_id, - col_id_overrides=col_id_overrides, - ) - ) - if sorted: - sql = textwrap.dedent( - f""" - SELECT * EXCEPT (`{offsets_id}`) - FROM ({sql}) - ORDER BY `{offsets_id}` - """ - ) - return typing.cast(str, sql) - - def _to_ibis_expr( - self, - ordering_mode: Literal["string_encoded", "offset_col", "unordered"], - order_col_name: Optional[str] = ORDER_ID_COLUMN, - expose_hidden_cols: bool = False, - fraction: Optional[float] = None, - col_id_overrides: typing.Mapping[str, str] = {}, - ): - """ - Creates an Ibis table expression representing the DataFrame. - - ArrayValue objects are sorted, so the following options are available - to reflect this in the ibis expression. - - * "offset_col": Zero-based offsets are generated as a column, this will - not sort the rows however. - * "string_encoded": An ordered string column is provided in output table. - * "unordered": No ordering information will be provided in output. Only - value columns are projected. - - For offset or ordered column, order_col_name can be used to assign the - output label for the ordering column. If none is specified, the default - column name will be 'bigframes_ordering_id' - - Args: - ordering_mode: - How to construct the Ibis expression from the ArrayValue. See - above for details. - order_col_name: - If the ordering mode outputs a single ordering or offsets - column, use this as the column name. - expose_hidden_cols: - If True, include the hidden ordering columns in the results. - Only compatible with `order_by` and `unordered` - ``ordering_mode``. - col_id_overrides: - overrides the column ids for the result - Returns: - An ibis expression representing the data help by the ArrayValue object. - """ - assert ordering_mode in ( - "string_encoded", - "offset_col", - "unordered", - ) - if expose_hidden_cols and ordering_mode in ("ordered_col", "offset_col"): - raise ValueError( - f"Cannot expose hidden ordering columns with ordering_mode {ordering_mode}" + return ArrayValue( + nodes.WindowOpNode( + child=self.node, + column_name=column_name, + op=op, + window_spec=window_spec, + output_name=output_name, + never_skip_nulls=never_skip_nulls, + skip_reproject_unsafe=skip_reproject_unsafe, ) - - columns = list(self._columns) - columns_to_drop: list[ - str - ] = [] # Ordering/Filtering columns that will be dropped at end - - if self._reduced_predicate is not None: - columns.append(self._reduced_predicate) - # Usually drop predicate as it is will be all TRUE after filtering - if not expose_hidden_cols: - columns_to_drop.append(self._reduced_predicate.get_name()) - - order_columns = self._create_order_columns( - ordering_mode, order_col_name, expose_hidden_cols ) - columns.extend(order_columns) - - # Special case for empty tables, since we can't create an empty - # projection. - if not columns: - return ibis.memtable([]) - - # Make sure all dtypes are the "canonical" ones for BigFrames. This is - # important for operations like UNION where the schema must match. - table = self._table.select( - bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns - ) - base_table = table - if self._reduced_predicate is not None: - table = table.filter(base_table[PREDICATE_COLUMN]) - table = table.drop(*columns_to_drop) - if col_id_overrides: - table = table.relabel(col_id_overrides) - if fraction is not None: - table = table.filter(ibis.random() < ibis.literal(fraction)) - return table - - def _create_order_columns( - self, - ordering_mode: str, - order_col_name: Optional[str], - expose_hidden_cols: bool, - ) -> typing.Sequence[ibis_types.Value]: - # Generate offsets if current ordering id semantics are not sufficiently strict - if ordering_mode == "offset_col": - return (self._create_offset_column().name(order_col_name),) - elif ordering_mode == "string_encoded": - return (self._create_string_ordering_column().name(order_col_name),) - elif expose_hidden_cols: - return self._hidden_ordering_columns - return () - - def _create_offset_column(self) -> ibis_types.IntegerColumn: - if self._ordering.total_order_col and self._ordering.is_sequential: - offsets = self._get_any_column(self._ordering.total_order_col.column_id) - return typing.cast(ibis_types.IntegerColumn, offsets) - else: - window = ibis.window(order_by=self._ibis_order) - if self._predicates: - window = window.group_by(self._reduced_predicate) - offsets = ibis.row_number().over(window) - return typing.cast(ibis_types.IntegerColumn, offsets) - - def _create_string_ordering_column(self) -> ibis_types.StringColumn: - if self._ordering.total_order_col and self._ordering.is_string_encoded: - string_order_ids = self._get_any_column( - self._ordering.total_order_col.column_id - ) - return typing.cast(ibis_types.StringColumn, string_order_ids) - if ( - self._ordering.total_order_col - and self._ordering.integer_encoding.is_encoded - ): - # Special case: non-negative integer ordering id can be converted directly to string without regenerating row numbers - int_values = self._get_any_column(self._ordering.total_order_col.column_id) - return encode_order_string( - typing.cast(ibis_types.IntegerColumn, int_values), - ) - else: - # Have to build string from scratch - window = ibis.window(order_by=self._ibis_order) - if self._predicates: - window = window.group_by(self._reduced_predicate) - row_nums = typing.cast( - ibis_types.IntegerColumn, ibis.row_number().over(window) - ) - return encode_order_string(row_nums) - - def start_query( - self, - job_config: Optional[bigquery.job.QueryJobConfig] = None, - max_results: Optional[int] = None, - *, - sorted: bool = True, - ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - """Execute a query and return metadata about the results.""" - # TODO(swast): Cache the job ID so we can look it up again if they ask - # for the results? We'd need a way to invalidate the cache if DataFrame - # becomes mutable, though. Or move this method to the immutable - # expression class. - # TODO(swast): We might want to move this method to Session and/or - # provide our own minimal metadata class. Tight coupling to the - # BigQuery client library isn't ideal, especially if we want to support - # a LocalSession for unit testing. - # TODO(swast): Add a timeout here? If the query is taking a long time, - # maybe we just print the job metadata that we have so far? - sql = self.to_sql(sorted=True) # type:ignore - return self._session._start_query( - sql=sql, - job_config=job_config, - max_results=max_results, - ) - - def _get_table_size(self, destination_table): - return self._session._get_table_size(destination_table) def _reproject_to_table(self) -> ArrayValue: """ @@ -881,74 +338,25 @@ def _reproject_to_table(self) -> ArrayValue: some operations such as window operations that cannot be used recursively in projections. """ - table = self._to_ibis_expr( - "unordered", - expose_hidden_cols=True, - ) - columns = [table[column_name] for column_name in self._column_names] - ordering_col_ids = [ - ref.column_id for ref in self._ordering.all_ordering_columns - ] - hidden_ordering_columns = [ - table[column_name] - for column_name in self._hidden_ordering_column_names - if column_name in ordering_col_ids - ] return ArrayValue( - self._session, - table, - columns=columns, - hidden_ordering_columns=hidden_ordering_columns, - ordering=self._ordering, - ) - - def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = False): - group_by: typing.List[ibis_types.Value] = ( - [ - typing.cast( - ibis_types.Column, _as_identity(self._get_ibis_column(column)) - ) - for column in window_spec.grouping_keys - ] - if window_spec.grouping_keys - else [] - ) - if self._reduced_predicate is not None: - group_by.append(self._reduced_predicate) - if window_spec.ordering: - order_by = _convert_ordering_to_table_values( - {**self._column_names, **self._hidden_ordering_column_names}, - window_spec.ordering, + nodes.ReprojectOpNode( + child=self.node, ) - if not allow_ties: - # Most operator need an unambiguous ordering, so the table's total ordering is appended - order_by = tuple([*order_by, *self._ibis_order]) - elif (window_spec.following is not None) or (window_spec.preceding is not None): - # If window spec has following or preceding bounds, we need to apply an unambiguous ordering. - order_by = tuple(self._ibis_order) - else: - # Unbound grouping window. Suitable for aggregations but not for analytic function application. - order_by = None - return ibis.window( - preceding=window_spec.preceding, - following=window_spec.following, - order_by=order_by, - group_by=group_by, ) def unpivot( self, row_labels: typing.Sequence[typing.Hashable], unpivot_columns: typing.Sequence[ - typing.Tuple[str, typing.Sequence[typing.Optional[str]]] + typing.Tuple[str, typing.Tuple[typing.Optional[str], ...]] ], *, passthrough_columns: typing.Sequence[str] = (), index_col_ids: typing.Sequence[str] = ["index"], dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] + bigframes.dtypes.Dtype, typing.Tuple[bigframes.dtypes.Dtype, ...] ] = pandas.Float64Dtype(), - how="left", + how: typing.Literal["left", "right"] = "left", ) -> ArrayValue: """ Unpivot ArrayValue columns. @@ -963,133 +371,23 @@ def unpivot( Returns: ArrayValue: The unpivoted ArrayValue """ - if how not in ("left", "right"): - raise ValueError("'how' must be 'left' or 'right'") - table = self._to_ibis_expr("unordered", expose_hidden_cols=True) - row_n = len(row_labels) - hidden_col_ids = self._hidden_ordering_column_names.keys() - if not all( - len(source_columns) == row_n for _, source_columns in unpivot_columns - ): - raise ValueError("Columns and row labels must all be same length.") - - unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") - unpivot_table = table.cross_join( - ibis.memtable({unpivot_offset_id: range(row_n)}) - ) - # Use ibis memtable to infer type of rowlabels (if possible) - # TODO: Allow caller to specify dtype - if isinstance(row_labels[0], tuple): - labels_table = ibis.memtable(row_labels) - labels_ibis_types = [ - labels_table[col].type() for col in labels_table.columns - ] - else: - labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] - labels_dtypes = [ - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) - for ibis_type in labels_ibis_types - ] - - label_columns = [] - for label_part, (col_id, label_dtype) in enumerate( - zip(index_col_ids, labels_dtypes) - ): - # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels - labels_as_tuples = [ - label if isinstance(label, tuple) else (label,) for label in row_labels - ] - cases = [ - ( - i, - bigframes.dtypes.literal_to_ibis_scalar( - label_tuple[label_part], # type:ignore - force_dtype=label_dtype, # type:ignore - ), - ) - for i, label_tuple in enumerate(labels_as_tuples) - ] - labels_value = ( - typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) - .cases(cases, default=None) # type:ignore - .name(col_id) - ) - label_columns.append(labels_value) - - unpivot_values = [] - for j in range(len(unpivot_columns)): - col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype - result_col, source_cols = unpivot_columns[j] - null_value = bigframes.dtypes.literal_to_ibis_scalar( - None, force_dtype=col_dtype - ) - ibis_values = [ - ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) - if col is not None - else null_value - for col in source_cols - ] - cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] - unpivot_value = typing.cast( - ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] - ).cases( - cases, default=null_value # type:ignore - ) - unpivot_values.append(unpivot_value.name(result_col)) - - unpivot_table = unpivot_table.select( - passthrough_columns, - *label_columns, - *unpivot_values, - *hidden_col_ids, - unpivot_offset_id, - ) - - # Extend the original ordering using unpivot_offset_id - old_ordering = self._ordering - if how == "left": - new_ordering = ExpressionOrdering( - ordering_value_columns=[ - *old_ordering.ordering_value_columns, - OrderingColumnReference(unpivot_offset_id), - ], - total_ordering_columns=frozenset( - [*old_ordering.total_ordering_columns, unpivot_offset_id] - ), - ) - else: # how=="right" - new_ordering = ExpressionOrdering( - ordering_value_columns=[ - OrderingColumnReference(unpivot_offset_id), - *old_ordering.ordering_value_columns, - ], - total_ordering_columns=frozenset( - [*old_ordering.total_ordering_columns, unpivot_offset_id] - ), - ) - value_columns = [ - unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns - ] - passthrough_values = [unpivot_table[col] for col in passthrough_columns] - hidden_ordering_columns = [ - unpivot_table[unpivot_offset_id], - *[unpivot_table[hidden_col] for hidden_col in hidden_col_ids], - ] return ArrayValue( - session=self._session, - table=unpivot_table, - columns=[ - *[unpivot_table[col_id] for col_id in index_col_ids], - *value_columns, - *passthrough_values, - ], - hidden_ordering_columns=hidden_ordering_columns, - ordering=new_ordering, + nodes.UnpivotNode( + child=self.node, + row_labels=tuple(row_labels), + unpivot_columns=tuple(unpivot_columns), + passthrough_columns=tuple(passthrough_columns), + index_col_ids=tuple(index_col_ids), + dtype=dtype, + how=how, + ) ) def assign(self, source_id: str, destination_id: str) -> ArrayValue: - return self._set_or_replace_by_id( - destination_id, self._get_ibis_column(source_id) + return ArrayValue( + nodes.AssignNode( + child=self.node, source_id=source_id, destination_id=destination_id + ) ) def assign_constant( @@ -1098,128 +396,41 @@ def assign_constant( value: typing.Any, dtype: typing.Optional[bigframes.dtypes.Dtype], ) -> ArrayValue: - # TODO(b/281587571): Solve scalar constant aggregation problem w/Ibis. - ibis_value = bigframes.dtypes.literal_to_ibis_scalar(value, dtype) - if ibis_value is None: - raise NotImplementedError( - f"Type not supported as scalar value {type(value)}. {constants.FEEDBACK_LINK}" - ) - expr = self._set_or_replace_by_id(destination_id, ibis_value) - return expr._reproject_to_table() - - def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> ArrayValue: - """Safely assign by id while maintaining ordering integrity.""" - # TODO: Split into explicit set and replace methods - ordering_col_ids = [ - col_ref.column_id for col_ref in self._ordering.ordering_value_columns - ] - if id in ordering_col_ids: - return self._hide_column(id)._set_or_replace_by_id(id, new_value) - - builder = self.builder() - if id in self.column_ids: - builder.columns = [ - val if (col_id != id) else new_value.name(id) - for col_id, val in zip(self.column_ids, self._columns) - ] - else: - builder.columns = [*self.columns, new_value.name(id)] - return builder.build() - - def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: - """Write the ArrayValue to a session table and create a new block object that references it.""" - ibis_expr = self._to_ibis_expr("unordered", expose_hidden_cols=True) - destination = self._session._ibis_to_session_table( - ibis_expr, cluster_cols=cluster_cols, api_name="cache" - ) - table_expression = self._session.ibis_client.table( - f"{destination.project}.{destination.dataset_id}.{destination.table_id}" - ) - new_columns = [table_expression[column] for column in self.column_ids] - new_hidden_columns = [ - table_expression[column] for column in self._hidden_ordering_column_names - ] return ArrayValue( - self._session, - table_expression, - columns=new_columns, - hidden_ordering_columns=new_hidden_columns, - ordering=self._ordering, + nodes.AssignConstantNode( + child=self.node, destination_id=destination_id, value=value, dtype=dtype + ) ) - -class ArrayValueBuilder: - """Mutable expression class. - Use ArrayValue.builder() to create from a ArrayValue object. - """ - - def __init__( + def join( self, - session: Session, - table: ibis_types.Table, - ordering: ExpressionOrdering, - columns: Collection[ibis_types.Value] = (), - hidden_ordering_columns: Collection[ibis_types.Value] = (), - predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + self_column_ids: typing.Sequence[str], + other: ArrayValue, + other_column_ids: typing.Sequence[str], + *, + how: Literal[ + "inner", + "left", + "outer", + "right", + ], + allow_row_identity_join: bool = True, ): - self.session = session - self.table = table - self.columns = list(columns) - self.hidden_ordering_columns = list(hidden_ordering_columns) - self.ordering = ordering - self.predicates = list(predicates) if predicates is not None else None - - def build(self) -> ArrayValue: return ArrayValue( - session=self.session, - table=self.table, - columns=self.columns, - hidden_ordering_columns=self.hidden_ordering_columns, - ordering=self.ordering, - predicates=self.predicates, - ) - - -def _reduce_predicate_list( - predicate_list: typing.Collection[ibis_types.BooleanValue], -) -> ibis_types.BooleanValue: - """Converts a list of predicates BooleanValues into a single BooleanValue.""" - if len(predicate_list) == 0: - raise ValueError("Cannot reduce empty list of predicates") - if len(predicate_list) == 1: - (item,) = predicate_list - return item - return functools.reduce(lambda acc, pred: acc.__and__(pred), predicate_list) - - -def _convert_ordering_to_table_values( - value_lookup: typing.Mapping[str, ibis_types.Value], - ordering_columns: typing.Sequence[OrderingColumnReference], -) -> typing.Sequence[ibis_types.Value]: - column_refs = ordering_columns - ordering_values = [] - for ordering_col in column_refs: - column = typing.cast(ibis_types.Column, value_lookup[ordering_col.column_id]) - ordering_value = ( - ibis.asc(column) - if ordering_col.direction.is_ascending - else ibis.desc(column) + nodes.JoinNode( + left_child=self.node, + right_child=other.node, + left_column_ids=tuple(self_column_ids), + right_column_ids=tuple(other_column_ids), + how=how, + allow_row_identity_join=allow_row_identity_join, + ) ) - # Bigquery SQL considers NULLS to be "smallest" values, but we need to override in these cases. - if (not ordering_col.na_last) and (not ordering_col.direction.is_ascending): - # Force nulls to be first - is_null_val = typing.cast(ibis_types.Column, column.isnull()) - ordering_values.append(ibis.desc(is_null_val)) - elif (ordering_col.na_last) and (ordering_col.direction.is_ascending): - # Force nulls to be last - is_null_val = typing.cast(ibis_types.Column, column.isnull()) - ordering_values.append(ibis.asc(is_null_val)) - ordering_values.append(ordering_value) - return ordering_values + def _uniform_sampling(self, fraction: float) -> ArrayValue: + """Sampling the table on given fraction. -def _as_identity(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(): - return value.cast(ibis_dtypes.str) - return value + .. warning:: + The row numbers of result is non-deterministic, avoid to use. + """ + return ArrayValue(nodes.RandomSampleNode(self.node, fraction)) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index b0f05f47984..3706bf1681b 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -21,6 +21,7 @@ import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.ordering as ordering +import bigframes.core.window_spec as windows import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -68,21 +69,21 @@ def indicate_duplicates( if keep == "first": # Count how many copies occur up to current copy of value # Discard this value if there are copies BEFORE - window_spec = core.WindowSpec( + window_spec = windows.WindowSpec( grouping_keys=tuple(columns), following=0, ) elif keep == "last": # Count how many copies occur up to current copy of values # Discard this value if there are copies AFTER - window_spec = core.WindowSpec( + window_spec = windows.WindowSpec( grouping_keys=tuple(columns), preceding=0, ) else: # keep == False # Count how many copies of the value occur in entire series. # Discard this value if there are copies ANYWHERE - window_spec = core.WindowSpec(grouping_keys=tuple(columns)) + window_spec = windows.WindowSpec(grouping_keys=tuple(columns)) block, dummy = block.create_constant(1) block, val_count_col_id = block.apply_window_op( dummy, @@ -131,7 +132,7 @@ def value_counts( ) count_id = agg_ids[0] if normalize: - unbound_window = core.WindowSpec() + unbound_window = windows.WindowSpec() block, total_count_id = block.apply_window_op( count_id, agg_ops.sum_op, unbound_window ) @@ -153,7 +154,7 @@ def value_counts( def pct_change(block: blocks.Block, periods: int = 1) -> blocks.Block: column_labels = block.column_labels - window_spec = core.WindowSpec( + window_spec = windows.WindowSpec( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -195,7 +196,7 @@ def rank( ops.isnull_op, ) nullity_col_ids.append(nullity_col_id) - window = core.WindowSpec( + window = windows.WindowSpec( # BigQuery has syntax to reorder nulls with "NULLS FIRST/LAST", but that is unavailable through ibis presently, so must order on a separate nullity expression first. ordering=( ordering.OrderingColumnReference( @@ -229,7 +230,7 @@ def rank( block, result_id = block.apply_window_op( rownum_col_ids[i], agg_op, - window_spec=core.WindowSpec(grouping_keys=[columns[i]]), + window_spec=windows.WindowSpec(grouping_keys=(columns[i],)), skip_reproject_unsafe=(i < (len(columns) - 1)), ) post_agg_rownum_col_ids.append(result_id) @@ -311,7 +312,7 @@ def nsmallest( block, counter = block.apply_window_op( column_ids[0], agg_ops.rank_op, - window_spec=core.WindowSpec(ordering=order_refs), + window_spec=windows.WindowSpec(ordering=tuple(order_refs)), ) block, condition = block.apply_unary_op( counter, ops.partial_right(ops.le_op, n) @@ -343,7 +344,7 @@ def nlargest( block, counter = block.apply_window_op( column_ids[0], agg_ops.rank_op, - window_spec=core.WindowSpec(ordering=order_refs), + window_spec=windows.WindowSpec(ordering=tuple(order_refs)), ) block, condition = block.apply_unary_op( counter, ops.partial_right(ops.le_op, n) @@ -440,14 +441,14 @@ def _mean_delta_to_power( grouping_column_ids: typing.Sequence[str], ) -> typing.Tuple[blocks.Block, typing.Sequence[str]]: """Calculate (x-mean(x))^n. Useful for calculating moment statistics such as skew and kurtosis.""" - window = core.WindowSpec(grouping_keys=grouping_column_ids) + window = windows.WindowSpec(grouping_keys=tuple(grouping_column_ids)) block, mean_ids = block.multi_apply_window_op(column_ids, agg_ops.mean_op, window) delta_ids = [] cube_op = ops.partial_right(ops.pow_op, n_power) for val_id, mean_val_id in zip(column_ids, mean_ids): block, delta_id = block.apply_binary_op(val_id, mean_val_id, ops.sub_op) block, delta_power_id = block.apply_unary_op(delta_id, cube_op) - block = block.drop_columns(delta_id) + block = block.drop_columns([delta_id]) delta_ids.append(delta_power_id) return block, delta_ids @@ -645,7 +646,7 @@ def _idx_extrema( for idx_col in original_block.index_columns ], ] - window_spec = core.WindowSpec(ordering=order_refs) + window_spec = windows.WindowSpec(ordering=tuple(order_refs)) idx_col = original_block.index_columns[0] block, result_col = block.apply_window_op( idx_col, agg_ops.first_op, window_spec diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 9db193a04e5..cc13edeaf96 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -35,7 +35,6 @@ import bigframes.core as core import bigframes.core.guid as guid import bigframes.core.indexes as indexes -import bigframes.core.joins as joins import bigframes.core.joins.name_resolution as join_names import bigframes.core.ordering as ordering import bigframes.core.utils @@ -378,7 +377,7 @@ def _to_dataframe(self, result) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" dtypes = dict(zip(self.index_columns, self.index_dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) - return self._expr._session._rows_to_dataframe(result, dtypes) + return self._expr.session._rows_to_dataframe(result, dtypes) def to_pandas( self, @@ -422,7 +421,7 @@ def to_pandas_batches(self): dtypes.update(zip(self.value_columns, self.dtypes)) results_iterator, _ = self._expr.start_query() for arrow_table in results_iterator.to_arrow_iterable( - bqstorage_client=self._expr._session.bqstoragereadclient + bqstorage_client=self._expr.session.bqstoragereadclient ): df = bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) self._copy_index_to_pandas(df) @@ -454,7 +453,9 @@ def _compute_and_count( results_iterator, query_job = expr.start_query(max_results=max_results) - table_size = expr._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES + table_size = ( + expr.session._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES + ) fraction = ( max_download_size / table_size if (max_download_size is not None) and (table_size != 0) @@ -819,7 +820,9 @@ def aggregate_all_and_stack( axis: int | str = 0, value_col_id: str = "values", dropna: bool = True, - dtype=pd.Float64Dtype(), + dtype: typing.Union[ + bigframes.dtypes.Dtype, typing.Tuple[bigframes.dtypes.Dtype, ...] + ] = pd.Float64Dtype(), ) -> Block: axis_n = utils.get_axis_number(axis) if axis_n == 0: @@ -829,7 +832,7 @@ def aggregate_all_and_stack( result_expr = self.expr.aggregate(aggregations, dropna=dropna).unpivot( row_labels=self.column_labels.to_list(), index_col_ids=["index"], - unpivot_columns=[(value_col_id, self.value_columns)], + unpivot_columns=tuple([(value_col_id, tuple(self.value_columns))]), dtype=dtype, ) return Block(result_expr, index_columns=["index"], column_labels=[None]) @@ -841,7 +844,7 @@ def aggregate_all_and_stack( stacked_expr = expr_with_offsets.unpivot( row_labels=self.column_labels.to_list(), index_col_ids=[guid.generate_guid()], - unpivot_columns=[(value_col_id, self.value_columns)], + unpivot_columns=[(value_col_id, tuple(self.value_columns))], passthrough_columns=[*self.index_columns, offset_col], dtype=dtype, ) @@ -1029,13 +1032,13 @@ def summarize( for col_id in column_ids ] columns = [ - (col_id, [f"{col_id}-{stat.name}" for stat in stats]) + (col_id, tuple(f"{col_id}-{stat.name}" for stat in stats)) for col_id in column_ids ] expr = self.expr.aggregate(aggregations).unpivot( labels, - unpivot_columns=columns, - index_col_ids=[label_col_id], + unpivot_columns=tuple(columns), + index_col_ids=tuple([label_col_id]), ) labels = self._get_labels_for_columns(column_ids) return Block(expr, column_labels=labels, index_columns=[label_col_id]) @@ -1342,7 +1345,7 @@ def stack(self, how="left", levels: int = 1): passthrough_columns=self.index_columns, unpivot_columns=unpivot_columns, index_col_ids=added_index_columns, - dtype=dtypes, + dtype=tuple(dtypes), how=how, ) new_index_level_names = self.column_labels.names[-levels:] @@ -1382,7 +1385,7 @@ def _create_stack_column( dtype = self._column_type(input_id) input_columns.append(input_id) # Input column i is the first one that - return input_columns, dtype or pd.Float64Dtype() + return tuple(input_columns), dtype or pd.Float64Dtype() def _column_type(self, col_id: str) -> bigframes.dtypes.Dtype: col_offset = self.value_columns.index(col_id) @@ -1497,8 +1500,7 @@ def merge( sort: bool, suffixes: tuple[str, str] = ("_x", "_y"), ) -> Block: - joined_expr = joins.join_by_column( - self.expr, + joined_expr = self.expr.join( left_join_ids, other.expr, right_join_ids, @@ -1708,7 +1710,7 @@ def _is_monotonic( return result -def block_from_local(data, session=None) -> Block: +def block_from_local(data) -> Block: pd_data = pd.DataFrame(data) columns = pd_data.columns @@ -1730,7 +1732,7 @@ def block_from_local(data, session=None) -> Block: ) index_ids = pd_data.columns[: len(index_labels)] - keys_expr = core.ArrayValue.mem_expr_from_pandas(pd_data, session) + keys_expr = core.ArrayValue.from_pandas(pd_data) return Block( keys_expr, column_labels=columns, diff --git a/bigframes/core/compile/__init__.py b/bigframes/core/compile/__init__.py new file mode 100644 index 00000000000..c86f4463dc0 --- /dev/null +++ b/bigframes/core/compile/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes.core.compile.compiled import CompiledArrayValue +from bigframes.core.compile.compiler import compile_node + +__all__ = [ + "compile_node", + "CompiledArrayValue", +] diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py new file mode 100644 index 00000000000..1134f1aab01 --- /dev/null +++ b/bigframes/core/compile/compiled.py @@ -0,0 +1,1121 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import functools +import math +import textwrap +import typing +from typing import Collection, Iterable, Literal, Optional, Sequence + +import ibis +import ibis.backends.bigquery as ibis_bigquery +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types +import pandas + +import bigframes.constants as constants +import bigframes.core.guid +from bigframes.core.ordering import ( + encode_order_string, + ExpressionOrdering, + IntegerEncoding, + OrderingColumnReference, + reencode_order_string, + StringEncoding, +) +import bigframes.core.utils as utils +from bigframes.core.window_spec import WindowSpec +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops + +ORDER_ID_COLUMN = "bigframes_ordering_id" +PREDICATE_COLUMN = "bigframes_predicate" + + +class CompiledArrayValue: + """Immutable BigQuery DataFrames expression tree. + + Note: Usage of this class is considered to be private and subject to change + at any time. + + This class is a wrapper around Ibis expressions. Its purpose is to defer + Ibis projection operations to keep generated SQL small and correct when + mixing and matching columns from different versions of a DataFrame. + + Args: + table: An Ibis table expression. + columns: Ibis value expressions that can be projected as columns. + hidden_ordering_columns: Ibis value expressions to store ordering. + ordering: An ordering property of the data frame. + predicates: A list of filters on the data frame. + """ + + def __init__( + self, + table: ibis_types.Table, + columns: Sequence[ibis_types.Value], + hidden_ordering_columns: Optional[Sequence[ibis_types.Value]] = None, + ordering: ExpressionOrdering = ExpressionOrdering(), + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + ): + self._table = table + self._predicates = tuple(predicates) if predicates is not None else () + # TODO: Validate ordering + if not ordering.total_ordering_columns: + raise ValueError("Must have total ordering defined by one or more columns") + self._ordering = ordering + # Allow creating a DataFrame directly from an Ibis table expression. + # TODO(swast): Validate that each column references the same table (or + # no table for literal values). + self._columns = tuple(columns) + + # Meta columns store ordering, or other data that doesn't correspond to dataframe columns + self._hidden_ordering_columns = ( + tuple(hidden_ordering_columns) + if hidden_ordering_columns is not None + else () + ) + + # To allow for more efficient lookup by column name, create a + # dictionary mapping names to column values. + self._column_names = {column.get_name(): column for column in self._columns} + self._hidden_ordering_column_names = { + column.get_name(): column for column in self._hidden_ordering_columns + } + ### Validation + value_col_ids = self._column_names.keys() + hidden_col_ids = self._hidden_ordering_column_names.keys() + + all_columns = value_col_ids | hidden_col_ids + ordering_valid = all( + col.column_id in all_columns for col in ordering.all_ordering_columns + ) + if value_col_ids & hidden_col_ids: + raise ValueError( + f"Keys in both hidden and exposed list: {value_col_ids & hidden_col_ids}" + ) + if not ordering_valid: + raise ValueError(f"Illegal ordering keys: {ordering.all_ordering_columns}") + + @classmethod + def mem_expr_from_pandas( + cls, + pd_df: pandas.DataFrame, + ) -> CompiledArrayValue: + """ + Builds an in-memory only (SQL only) expr from a pandas dataframe. + """ + # We can't include any hidden columns in the ArrayValue constructor, so + # grab the column names before we add the hidden ordering column. + column_names = [str(column) for column in pd_df.columns] + # Make sure column names are all strings. + pd_df = pd_df.set_axis(column_names, axis="columns") + pd_df = pd_df.assign(**{ORDER_ID_COLUMN: range(len(pd_df))}) + + # ibis memtable cannot handle NA, must convert to None + pd_df = pd_df.astype("object") # type: ignore + pd_df = pd_df.where(pandas.notnull(pd_df), None) + + # NULL type isn't valid in BigQuery, so retry with an explicit schema in these cases. + keys_memtable = ibis.memtable(pd_df) + schema = keys_memtable.schema() + new_schema = [] + for column_index, column in enumerate(schema): + if column == ORDER_ID_COLUMN: + new_type: ibis_dtypes.DataType = ibis_dtypes.int64 + else: + column_type = schema[column] + # The autodetected type might not be one we can support, such + # as NULL type for empty rows, so convert to a type we do + # support. + new_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype( + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(column_type) + ) + # TODO(swast): Ibis memtable doesn't use backticks in struct + # field names, so spaces and other characters aren't allowed in + # the memtable context. Blocked by + # https://github.com/ibis-project/ibis/issues/7187 + column = f"col_{column_index}" + new_schema.append((column, new_type)) + + # must set non-null column labels. these are not the user-facing labels + pd_df = pd_df.set_axis( + [column for column, _ in new_schema], + axis="columns", + ) + keys_memtable = ibis.memtable(pd_df, schema=ibis.schema(new_schema)) + + return cls( + keys_memtable, + columns=[ + keys_memtable[f"col_{column_index}"].name(column) + for column_index, column in enumerate(column_names) + ], + ordering=ExpressionOrdering( + ordering_value_columns=tuple( + [OrderingColumnReference(ORDER_ID_COLUMN)] + ), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + ), + hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), + ) + + @property + def columns(self) -> typing.Tuple[ibis_types.Value, ...]: + return self._columns + + @property + def column_ids(self) -> typing.Sequence[str]: + return tuple(self._column_names.keys()) + + @property + def _hidden_column_ids(self) -> typing.Sequence[str]: + return tuple(self._hidden_ordering_column_names.keys()) + + @property + def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: + """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" + return ( + _reduce_predicate_list(self._predicates).name(PREDICATE_COLUMN) + if self._predicates + else None + ) + + @property + def _ibis_order(self) -> Sequence[ibis_types.Value]: + """Returns a sequence of ibis values which can be directly used to order a table expression. Has direction modifiers applied.""" + return _convert_ordering_to_table_values( + {**self._column_names, **self._hidden_ordering_column_names}, + self._ordering.all_ordering_columns, + ) + + def builder(self) -> ArrayValueBuilder: + """Creates a mutable builder for expressions.""" + # Since ArrayValue is intended to be immutable (immutability offers + # potential opportunities for caching, though we might need to introduce + # more node types for that to be useful), we create a builder class. + return ArrayValueBuilder( + self._table, + columns=self._columns, + hidden_ordering_columns=self._hidden_ordering_columns, + ordering=self._ordering, + predicates=self._predicates, + ) + + def drop_columns(self, columns: Iterable[str]) -> CompiledArrayValue: + # Must generate offsets if we are dropping a column that ordering depends on + expr = self + for ordering_column in set(columns).intersection( + [col.column_id for col in self._ordering.ordering_value_columns] + ): + expr = self._hide_column(ordering_column) + + expr_builder = expr.builder() + remain_cols = [ + column for column in expr.columns if column.get_name() not in columns + ] + expr_builder.columns = remain_cols + return expr_builder.build() + + def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: + ibis_type = typing.cast( + bigframes.dtypes.IbisDtype, self._get_any_column(key).type() + ) + return typing.cast( + bigframes.dtypes.Dtype, + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), + ) + + def _get_ibis_column(self, key: str) -> ibis_types.Value: + """Gets the Ibis expression for a given column.""" + if key not in self.column_ids: + raise ValueError( + "Column name {} not in set of values: {}".format(key, self.column_ids) + ) + return typing.cast(ibis_types.Value, self._column_names[key]) + + def _get_any_column(self, key: str) -> ibis_types.Value: + """Gets the Ibis expression for a given column. Will also get hidden columns.""" + all_columns = {**self._column_names, **self._hidden_ordering_column_names} + if key not in all_columns.keys(): + raise ValueError( + "Column name {} not in set of values: {}".format( + key, all_columns.keys() + ) + ) + return typing.cast(ibis_types.Value, all_columns[key]) + + def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: + """Gets the Ibis expression for a given hidden column.""" + if key not in self._hidden_ordering_column_names.keys(): + raise ValueError( + "Column name {} not in set of values: {}".format( + key, self._hidden_ordering_column_names.keys() + ) + ) + return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) + + def filter(self, predicate_id: str, keep_null: bool = False) -> CompiledArrayValue: + """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + condition = typing.cast( + ibis_types.BooleanValue, self._get_ibis_column(predicate_id) + ) + if keep_null: + condition = typing.cast( + ibis_types.BooleanValue, + condition.fillna( + typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) + ), + ) + return self._filter(condition) + + def _filter(self, predicate_value: ibis_types.BooleanValue) -> CompiledArrayValue: + """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + expr = self.builder() + expr.ordering = expr.ordering.with_non_sequential() + expr.predicates = [*self._predicates, predicate_value] + return expr.build() + + def order_by( + self, by: Sequence[OrderingColumnReference], stable: bool = False + ) -> CompiledArrayValue: + expr_builder = self.builder() + expr_builder.ordering = self._ordering.with_ordering_columns(by, stable=stable) + return expr_builder.build() + + def reversed(self) -> CompiledArrayValue: + expr_builder = self.builder() + expr_builder.ordering = self._ordering.with_reverse() + return expr_builder.build() + + def _uniform_sampling(self, fraction: float) -> CompiledArrayValue: + """Sampling the table on given fraction. + + .. warning:: + The row numbers of result is non-deterministic, avoid to use. + """ + table = self._to_ibis_expr( + "unordered", expose_hidden_cols=True, fraction=fraction + ) + columns = [table[column_name] for column_name in self._column_names] + hidden_ordering_columns = [ + table[column_name] for column_name in self._hidden_ordering_column_names + ] + return CompiledArrayValue( + table, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=self._ordering, + ) + + @property + def _offsets(self) -> ibis_types.IntegerColumn: + if not self._ordering.is_sequential: + raise ValueError( + "Expression does not have offsets. Generate them first using project_offsets." + ) + if not self._ordering.total_order_col: + raise ValueError( + "Ordering is invalid. Marked as sequential but no total order columns." + ) + column = self._get_any_column(self._ordering.total_order_col.column_id) + return typing.cast(ibis_types.IntegerColumn, column) + + def _project_offsets(self) -> CompiledArrayValue: + """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" + if self._ordering.is_sequential: + return self + # TODO(tbergeron): Enforce total ordering + table = self._to_ibis_expr( + ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN + ) + columns = [table[column_name] for column_name in self._column_names] + ordering = ExpressionOrdering( + ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(True, is_sequential=True), + ) + return CompiledArrayValue( + table, + columns=columns, + hidden_ordering_columns=[table[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def _hide_column(self, column_id) -> CompiledArrayValue: + """Pushes columns to hidden columns list. Used to hide ordering columns that have been dropped or destructively mutated.""" + expr_builder = self.builder() + # Need to rename column as caller might be creating a new row with the same name but different values. + # Can avoid this if don't allow callers to determine ids and instead generate unique ones in this class. + new_name = bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") + expr_builder.hidden_ordering_columns = [ + *self._hidden_ordering_columns, + self._get_ibis_column(column_id).name(new_name), + ] + expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) + return expr_builder.build() + + def promote_offsets(self, col_id: str) -> CompiledArrayValue: + """ + Convenience function to promote copy of column offsets to a value column. Can be used to reset index. + """ + # Special case: offsets already exist + ordering = self._ordering + + if (not ordering.is_sequential) or (not ordering.total_order_col): + return self._project_offsets().promote_offsets(col_id) + expr_builder = self.builder() + expr_builder.columns = [ + self._get_any_column(ordering.total_order_col.column_id).name(col_id), + *self.columns, + ] + return expr_builder.build() + + def select_columns(self, column_ids: typing.Sequence[str]) -> CompiledArrayValue: + """Creates a new expression based on this expression with new columns.""" + columns = [self._get_ibis_column(col_id) for col_id in column_ids] + expr = self + for ordering_column in set(self.column_ids).intersection( + [col_ref.column_id for col_ref in self._ordering.ordering_value_columns] + ): + # Need to hide ordering columns that are being dropped. Alternatively, could project offsets + expr = expr._hide_column(ordering_column) + builder = expr.builder() + builder.columns = list(columns) + new_expr = builder.build() + return new_expr + + def concat(self, other: typing.Sequence[CompiledArrayValue]) -> CompiledArrayValue: + """Append together multiple ArrayValue objects.""" + if len(other) == 0: + return self + tables = [] + prefix_base = 10 + prefix_size = math.ceil(math.log(len(other) + 1, prefix_base)) + # Must normalize all ids to the same encoding size + max_encoding_size = max( + self._ordering.string_encoding.length, + *[expression._ordering.string_encoding.length for expression in other], + ) + for i, expr in enumerate([self, *other]): + ordering_prefix = str(i).zfill(prefix_size) + table = expr._to_ibis_expr( + ordering_mode="string_encoded", order_col_name=ORDER_ID_COLUMN + ) + # Rename the value columns based on horizontal offset before applying union. + table = table.select( + [ + table[col].name(f"column_{i}") + if col != ORDER_ID_COLUMN + else ( + ordering_prefix + + reencode_order_string( + table[ORDER_ID_COLUMN], max_encoding_size + ) + ).name(ORDER_ID_COLUMN) + for i, col in enumerate(table.columns) + ] + ) + tables.append(table) + combined_table = ibis.union(*tables) + ordering = ExpressionOrdering( + ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + string_encoding=StringEncoding(True, prefix_size + max_encoding_size), + ) + return CompiledArrayValue( + combined_table, + columns=[ + combined_table[col] + for col in combined_table.columns + if col != ORDER_ID_COLUMN + ], + hidden_ordering_columns=[combined_table[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def project_unary_op( + self, column_name: str, op: ops.UnaryOp, output_name=None + ) -> CompiledArrayValue: + """Creates a new expression based on this expression with unary operation applied to one column.""" + value = op._as_ibis(self._get_ibis_column(column_name)).name( + output_name or column_name + ) + return self._set_or_replace_by_id(output_name or column_name, value) + + def project_binary_op( + self, + left_column_id: str, + right_column_id: str, + op: ops.BinaryOp, + output_column_id: str, + ) -> CompiledArrayValue: + """Creates a new expression based on this expression with binary operation applied to two columns.""" + value = op( + self._get_ibis_column(left_column_id), + self._get_ibis_column(right_column_id), + ).name(output_column_id) + return self._set_or_replace_by_id(output_column_id, value) + + def project_ternary_op( + self, + col_id_1: str, + col_id_2: str, + col_id_3: str, + op: ops.TernaryOp, + output_column_id: str, + ) -> CompiledArrayValue: + """Creates a new expression based on this expression with ternary operation applied to three columns.""" + value = op( + self._get_ibis_column(col_id_1), + self._get_ibis_column(col_id_2), + self._get_ibis_column(col_id_3), + ).name(output_column_id) + return self._set_or_replace_by_id(output_column_id, value) + + def aggregate( + self, + aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp, str]], + by_column_ids: typing.Sequence[str] = (), + dropna: bool = True, + ) -> CompiledArrayValue: + """ + Apply aggregations to the expression. + Arguments: + aggregations: input_column_id, operation, output_column_id tuples + by_column_id: column id of the aggregation key, this is preserved through the transform + dropna: whether null keys should be dropped + """ + table = self._to_ibis_expr("unordered") + stats = { + col_out: agg_op._as_ibis(table[col_in]) + for col_in, agg_op, col_out in aggregations + } + if by_column_ids: + result = table.group_by(by_column_ids).aggregate(**stats) + # Must have deterministic ordering, so order by the unique "by" column + ordering = ExpressionOrdering( + tuple( + [ + OrderingColumnReference(column_id=column_id) + for column_id in by_column_ids + ] + ), + total_ordering_columns=frozenset(by_column_ids), + ) + columns = tuple(result[key] for key in result.columns) + expr = CompiledArrayValue(result, columns=columns, ordering=ordering) + if dropna: + for column_id in by_column_ids: + expr = expr._filter( + ops.notnull_op._as_ibis(expr._get_ibis_column(column_id)) + ) + # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation + return expr._project_offsets() + else: + aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} + result = table.aggregate(**aggregates) + # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. + ordering = ExpressionOrdering( + ordering_value_columns=tuple( + [OrderingColumnReference(ORDER_ID_COLUMN)] + ), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), + ) + return CompiledArrayValue( + result, + columns=[result[col_id] for col_id in [*stats.keys()]], + hidden_ordering_columns=[result[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def corr_aggregate( + self, corr_aggregations: typing.Sequence[typing.Tuple[str, str, str]] + ) -> CompiledArrayValue: + """ + Get correlations between each lef_column_id and right_column_id, stored in the respective output_column_id. + This uses BigQuery's CORR under the hood, and thus only Pearson's method is used. + Arguments: + corr_aggregations: left_column_id, right_column_id, output_column_id tuples + """ + table = self._to_ibis_expr("unordered") + stats = { + col_out: table[col_left].corr(table[col_right], how="pop") + for col_left, col_right, col_out in corr_aggregations + } + aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} + result = table.aggregate(**aggregates) + # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. + ordering = ExpressionOrdering( + ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), + ) + return CompiledArrayValue( + result, + columns=[result[col_id] for col_id in [*stats.keys()]], + hidden_ordering_columns=[result[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def project_window_op( + self, + column_name: str, + op: agg_ops.WindowOp, + window_spec: WindowSpec, + output_name=None, + *, + never_skip_nulls=False, + skip_reproject_unsafe: bool = False, + ) -> CompiledArrayValue: + """ + Creates a new expression based on this expression with unary operation applied to one column. + column_name: the id of the input column present in the expression + op: the windowable operator to apply to the input column + window_spec: a specification of the window over which to apply the operator + output_name: the id to assign to the output of the operator, by default will replace input col if distinct output id not provided + never_skip_nulls: will disable null skipping for operators that would otherwise do so + skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection + """ + column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) + window = self._ibis_window_from_spec(window_spec, allow_ties=op.handles_ties) + + window_op = op._as_ibis(column, window) + + clauses = [] + if op.skips_nulls and not never_skip_nulls: + clauses.append((column.isnull(), ibis.NA)) + if window_spec.min_periods: + if op.skips_nulls: + # Most operations do not count NULL values towards min_periods + observation_count = agg_ops.count_op._as_ibis(column, window) + else: + # Operations like count treat even NULLs as valid observations for the sake of min_periods + # notnull is just used to convert null values to non-null (FALSE) values to be counted + denulled_value = typing.cast(ibis_types.BooleanColumn, column.notnull()) + observation_count = agg_ops.count_op._as_ibis(denulled_value, window) + clauses.append( + ( + observation_count < ibis_types.literal(window_spec.min_periods), + ibis.NA, + ) + ) + if clauses: + case_statement = ibis.case() + for clause in clauses: + case_statement = case_statement.when(clause[0], clause[1]) + case_statement = case_statement.else_(window_op).end() + window_op = case_statement + + result = self._set_or_replace_by_id(output_name or column_name, window_op) + # TODO(tbergeron): Automatically track analytic expression usage and defer reprojection until required for valid query generation. + return result._reproject_to_table() if not skip_reproject_unsafe else result + + def to_sql( + self, + offset_column: typing.Optional[str] = None, + col_id_overrides: typing.Mapping[str, str] = {}, + sorted: bool = False, + ) -> str: + offsets_id = offset_column or ORDER_ID_COLUMN + + sql = ibis_bigquery.Backend().compile( + self._to_ibis_expr( + ordering_mode="offset_col" + if (offset_column or sorted) + else "unordered", + order_col_name=offsets_id, + col_id_overrides=col_id_overrides, + ) + ) + if sorted: + sql = textwrap.dedent( + f""" + SELECT * EXCEPT (`{offsets_id}`) + FROM ({sql}) + ORDER BY `{offsets_id}` + """ + ) + return typing.cast(str, sql) + + def _to_ibis_expr( + self, + ordering_mode: Literal["string_encoded", "offset_col", "unordered"], + order_col_name: Optional[str] = ORDER_ID_COLUMN, + expose_hidden_cols: bool = False, + fraction: Optional[float] = None, + col_id_overrides: typing.Mapping[str, str] = {}, + ): + """ + Creates an Ibis table expression representing the DataFrame. + + ArrayValue objects are sorted, so the following options are available + to reflect this in the ibis expression. + + * "offset_col": Zero-based offsets are generated as a column, this will + not sort the rows however. + * "string_encoded": An ordered string column is provided in output table. + * "unordered": No ordering information will be provided in output. Only + value columns are projected. + + For offset or ordered column, order_col_name can be used to assign the + output label for the ordering column. If none is specified, the default + column name will be 'bigframes_ordering_id' + + Args: + ordering_mode: + How to construct the Ibis expression from the ArrayValue. See + above for details. + order_col_name: + If the ordering mode outputs a single ordering or offsets + column, use this as the column name. + expose_hidden_cols: + If True, include the hidden ordering columns in the results. + Only compatible with `order_by` and `unordered` + ``ordering_mode``. + col_id_overrides: + overrides the column ids for the result + Returns: + An ibis expression representing the data help by the ArrayValue object. + """ + assert ordering_mode in ( + "string_encoded", + "offset_col", + "unordered", + ) + if expose_hidden_cols and ordering_mode in ("ordered_col", "offset_col"): + raise ValueError( + f"Cannot expose hidden ordering columns with ordering_mode {ordering_mode}" + ) + + columns = list(self._columns) + columns_to_drop: list[ + str + ] = [] # Ordering/Filtering columns that will be dropped at end + + if self._reduced_predicate is not None: + columns.append(self._reduced_predicate) + # Usually drop predicate as it is will be all TRUE after filtering + if not expose_hidden_cols: + columns_to_drop.append(self._reduced_predicate.get_name()) + + order_columns = self._create_order_columns( + ordering_mode, order_col_name, expose_hidden_cols + ) + columns.extend(order_columns) + + # Special case for empty tables, since we can't create an empty + # projection. + if not columns: + return ibis.memtable([]) + + # Make sure all dtypes are the "canonical" ones for BigFrames. This is + # important for operations like UNION where the schema must match. + table = self._table.select( + bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns + ) + base_table = table + if self._reduced_predicate is not None: + table = table.filter(base_table[PREDICATE_COLUMN]) + table = table.drop(*columns_to_drop) + if col_id_overrides: + table = table.relabel(col_id_overrides) + if fraction is not None: + table = table.filter(ibis.random() < ibis.literal(fraction)) + return table + + def _create_order_columns( + self, + ordering_mode: str, + order_col_name: Optional[str], + expose_hidden_cols: bool, + ) -> typing.Sequence[ibis_types.Value]: + # Generate offsets if current ordering id semantics are not sufficiently strict + if ordering_mode == "offset_col": + return (self._create_offset_column().name(order_col_name),) + elif ordering_mode == "string_encoded": + return (self._create_string_ordering_column().name(order_col_name),) + elif expose_hidden_cols: + return self._hidden_ordering_columns + return () + + def _create_offset_column(self) -> ibis_types.IntegerColumn: + if self._ordering.total_order_col and self._ordering.is_sequential: + offsets = self._get_any_column(self._ordering.total_order_col.column_id) + return typing.cast(ibis_types.IntegerColumn, offsets) + else: + window = ibis.window(order_by=self._ibis_order) + if self._predicates: + window = window.group_by(self._reduced_predicate) + offsets = ibis.row_number().over(window) + return typing.cast(ibis_types.IntegerColumn, offsets) + + def _create_string_ordering_column(self) -> ibis_types.StringColumn: + if self._ordering.total_order_col and self._ordering.is_string_encoded: + string_order_ids = self._get_any_column( + self._ordering.total_order_col.column_id + ) + return typing.cast(ibis_types.StringColumn, string_order_ids) + if ( + self._ordering.total_order_col + and self._ordering.integer_encoding.is_encoded + ): + # Special case: non-negative integer ordering id can be converted directly to string without regenerating row numbers + int_values = self._get_any_column(self._ordering.total_order_col.column_id) + return encode_order_string( + typing.cast(ibis_types.IntegerColumn, int_values), + ) + else: + # Have to build string from scratch + window = ibis.window(order_by=self._ibis_order) + if self._predicates: + window = window.group_by(self._reduced_predicate) + row_nums = typing.cast( + ibis_types.IntegerColumn, ibis.row_number().over(window) + ) + return encode_order_string(row_nums) + + def _reproject_to_table(self) -> CompiledArrayValue: + """ + Internal operators that projects the internal representation into a + new ibis table expression where each value column is a direct + reference to a column in that table expression. Needed after + some operations such as window operations that cannot be used + recursively in projections. + """ + table = self._to_ibis_expr( + "unordered", + expose_hidden_cols=True, + ) + columns = [table[column_name] for column_name in self._column_names] + ordering_col_ids = [ + ref.column_id for ref in self._ordering.all_ordering_columns + ] + hidden_ordering_columns = [ + table[column_name] + for column_name in self._hidden_ordering_column_names + if column_name in ordering_col_ids + ] + return CompiledArrayValue( + table, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=self._ordering, + ) + + def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = False): + group_by: typing.List[ibis_types.Value] = ( + [ + typing.cast( + ibis_types.Column, _as_identity(self._get_ibis_column(column)) + ) + for column in window_spec.grouping_keys + ] + if window_spec.grouping_keys + else [] + ) + if self._reduced_predicate is not None: + group_by.append(self._reduced_predicate) + if window_spec.ordering: + order_by = _convert_ordering_to_table_values( + {**self._column_names, **self._hidden_ordering_column_names}, + window_spec.ordering, + ) + if not allow_ties: + # Most operator need an unambiguous ordering, so the table's total ordering is appended + order_by = tuple([*order_by, *self._ibis_order]) + elif (window_spec.following is not None) or (window_spec.preceding is not None): + # If window spec has following or preceding bounds, we need to apply an unambiguous ordering. + order_by = tuple(self._ibis_order) + else: + # Unbound grouping window. Suitable for aggregations but not for analytic function application. + order_by = None + return ibis.window( + preceding=window_spec.preceding, + following=window_spec.following, + order_by=order_by, + group_by=group_by, + ) + + def unpivot( + self, + row_labels: typing.Sequence[typing.Hashable], + unpivot_columns: typing.Sequence[ + typing.Tuple[str, typing.Sequence[typing.Optional[str]]] + ], + *, + passthrough_columns: typing.Sequence[str] = (), + index_col_ids: typing.Sequence[str] = ["index"], + dtype: typing.Union[ + bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] + ] = pandas.Float64Dtype(), + how="left", + ) -> CompiledArrayValue: + """ + Unpivot ArrayValue columns. + + Args: + row_labels: Identifies the source of the row. Must be equal to length to source column list in unpivot_columns argument. + unpivot_columns: Mapping of column id to list of input column ids. Lists of input columns may use None. + passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. + index_col_id (str): The column id to be used for the row labels. + dtype (dtype or list of dtype): Dtype to use for the unpivot columns. If list, must be equal in number to unpivot_columns. + + Returns: + ArrayValue: The unpivoted ArrayValue + """ + if how not in ("left", "right"): + raise ValueError("'how' must be 'left' or 'right'") + table = self._to_ibis_expr("unordered", expose_hidden_cols=True) + row_n = len(row_labels) + hidden_col_ids = self._hidden_ordering_column_names.keys() + if not all( + len(source_columns) == row_n for _, source_columns in unpivot_columns + ): + raise ValueError("Columns and row labels must all be same length.") + + unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") + unpivot_table = table.cross_join( + ibis.memtable({unpivot_offset_id: range(row_n)}) + ) + # Use ibis memtable to infer type of rowlabels (if possible) + # TODO: Allow caller to specify dtype + if isinstance(row_labels[0], tuple): + labels_table = ibis.memtable(row_labels) + labels_ibis_types = [ + labels_table[col].type() for col in labels_table.columns + ] + else: + labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] + labels_dtypes = [ + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) + for ibis_type in labels_ibis_types + ] + + label_columns = [] + for label_part, (col_id, label_dtype) in enumerate( + zip(index_col_ids, labels_dtypes) + ): + # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels + labels_as_tuples = [ + label if isinstance(label, tuple) else (label,) for label in row_labels + ] + cases = [ + ( + i, + bigframes.dtypes.literal_to_ibis_scalar( + label_tuple[label_part], # type:ignore + force_dtype=label_dtype, # type:ignore + ), + ) + for i, label_tuple in enumerate(labels_as_tuples) + ] + labels_value = ( + typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) + .cases(cases, default=None) # type:ignore + .name(col_id) + ) + label_columns.append(labels_value) + + unpivot_values = [] + for j in range(len(unpivot_columns)): + col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype + result_col, source_cols = unpivot_columns[j] + null_value = bigframes.dtypes.literal_to_ibis_scalar( + None, force_dtype=col_dtype + ) + ibis_values = [ + ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) + if col is not None + else null_value + for col in source_cols + ] + cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] + unpivot_value = typing.cast( + ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] + ).cases( + cases, default=null_value # type:ignore + ) + unpivot_values.append(unpivot_value.name(result_col)) + + unpivot_table = unpivot_table.select( + passthrough_columns, + *label_columns, + *unpivot_values, + *hidden_col_ids, + unpivot_offset_id, + ) + + # Extend the original ordering using unpivot_offset_id + old_ordering = self._ordering + if how == "left": + new_ordering = ExpressionOrdering( + ordering_value_columns=tuple( + [ + *old_ordering.ordering_value_columns, + OrderingColumnReference(unpivot_offset_id), + ] + ), + total_ordering_columns=frozenset( + [*old_ordering.total_ordering_columns, unpivot_offset_id] + ), + ) + else: # how=="right" + new_ordering = ExpressionOrdering( + ordering_value_columns=tuple( + [ + OrderingColumnReference(unpivot_offset_id), + *old_ordering.ordering_value_columns, + ] + ), + total_ordering_columns=frozenset( + [*old_ordering.total_ordering_columns, unpivot_offset_id] + ), + ) + value_columns = [ + unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns + ] + passthrough_values = [unpivot_table[col] for col in passthrough_columns] + hidden_ordering_columns = [ + unpivot_table[unpivot_offset_id], + *[unpivot_table[hidden_col] for hidden_col in hidden_col_ids], + ] + return CompiledArrayValue( + table=unpivot_table, + columns=[ + *[unpivot_table[col_id] for col_id in index_col_ids], + *value_columns, + *passthrough_values, + ], + hidden_ordering_columns=hidden_ordering_columns, + ordering=new_ordering, + ) + + def assign(self, source_id: str, destination_id: str) -> CompiledArrayValue: + return self._set_or_replace_by_id( + destination_id, self._get_ibis_column(source_id) + ) + + def assign_constant( + self, + destination_id: str, + value: typing.Any, + dtype: typing.Optional[bigframes.dtypes.Dtype], + ) -> CompiledArrayValue: + # TODO(b/281587571): Solve scalar constant aggregation problem w/Ibis. + ibis_value = bigframes.dtypes.literal_to_ibis_scalar(value, dtype) + if ibis_value is None: + raise NotImplementedError( + f"Type not supported as scalar value {type(value)}. {constants.FEEDBACK_LINK}" + ) + expr = self._set_or_replace_by_id(destination_id, ibis_value) + return expr._reproject_to_table() + + def _set_or_replace_by_id( + self, id: str, new_value: ibis_types.Value + ) -> CompiledArrayValue: + """Safely assign by id while maintaining ordering integrity.""" + # TODO: Split into explicit set and replace methods + ordering_col_ids = [ + col_ref.column_id for col_ref in self._ordering.ordering_value_columns + ] + if id in ordering_col_ids: + return self._hide_column(id)._set_or_replace_by_id(id, new_value) + + builder = self.builder() + if id in self.column_ids: + builder.columns = [ + val if (col_id != id) else new_value.name(id) + for col_id, val in zip(self.column_ids, self._columns) + ] + else: + builder.columns = [*self.columns, new_value.name(id)] + return builder.build() + + +class ArrayValueBuilder: + """Mutable expression class. + Use ArrayValue.builder() to create from a ArrayValue object. + """ + + def __init__( + self, + table: ibis_types.Table, + ordering: ExpressionOrdering, + columns: Collection[ibis_types.Value] = (), + hidden_ordering_columns: Collection[ibis_types.Value] = (), + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + ): + self.table = table + self.columns = list(columns) + self.hidden_ordering_columns = list(hidden_ordering_columns) + self.ordering = ordering + self.predicates = list(predicates) if predicates is not None else None + + def build(self) -> CompiledArrayValue: + return CompiledArrayValue( + table=self.table, + columns=self.columns, + hidden_ordering_columns=self.hidden_ordering_columns, + ordering=self.ordering, + predicates=self.predicates, + ) + + +def _reduce_predicate_list( + predicate_list: typing.Collection[ibis_types.BooleanValue], +) -> ibis_types.BooleanValue: + """Converts a list of predicates BooleanValues into a single BooleanValue.""" + if len(predicate_list) == 0: + raise ValueError("Cannot reduce empty list of predicates") + if len(predicate_list) == 1: + (item,) = predicate_list + return item + return functools.reduce(lambda acc, pred: acc.__and__(pred), predicate_list) + + +def _convert_ordering_to_table_values( + value_lookup: typing.Mapping[str, ibis_types.Value], + ordering_columns: typing.Sequence[OrderingColumnReference], +) -> typing.Sequence[ibis_types.Value]: + column_refs = ordering_columns + ordering_values = [] + for ordering_col in column_refs: + column = typing.cast(ibis_types.Column, value_lookup[ordering_col.column_id]) + ordering_value = ( + ibis.asc(column) + if ordering_col.direction.is_ascending + else ibis.desc(column) + ) + # Bigquery SQL considers NULLS to be "smallest" values, but we need to override in these cases. + if (not ordering_col.na_last) and (not ordering_col.direction.is_ascending): + # Force nulls to be first + is_null_val = typing.cast(ibis_types.Column, column.isnull()) + ordering_values.append(ibis.desc(is_null_val)) + elif (ordering_col.na_last) and (ordering_col.direction.is_ascending): + # Force nulls to be last + is_null_val = typing.cast(ibis_types.Column, column.isnull()) + ordering_values.append(ibis.asc(is_null_val)) + ordering_values.append(ordering_value) + return ordering_values + + +def _as_identity(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(): + return value.cast(ibis_dtypes.str) + return value diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py new file mode 100644 index 00000000000..195d830122a --- /dev/null +++ b/bigframes/core/compile/compiler.py @@ -0,0 +1,185 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import functools +import io +import typing + +import pandas as pd + +import bigframes.core.compile as compiled +import bigframes.core.compile.single_column +import bigframes.core.nodes as nodes + +if typing.TYPE_CHECKING: + import bigframes.core + import bigframes.session + + +@functools.cache +def compile_node(node: nodes.BigFrameNode) -> compiled.CompiledArrayValue: + """Compile node into CompileArrayValue. Caches result.""" + return _compile_node(node) + + +@functools.singledispatch +def _compile_node(node: nodes.BigFrameNode) -> compiled.CompiledArrayValue: + """Defines transformation but isn't cached, always use compile_node instead""" + raise ValueError(f"Can't compile unnrecognized node: {node}") + + +@_compile_node.register +def compile_join(node: nodes.JoinNode): + compiled_left = compile_node(node.left_child) + compiled_right = compile_node(node.right_child) + return bigframes.core.compile.single_column.join_by_column( + compiled_left, + node.left_column_ids, + compiled_right, + node.right_column_ids, + how=node.how, + allow_row_identity_join=node.allow_row_identity_join, + ) + + +@_compile_node.register +def compile_select(node: nodes.SelectNode): + return compile_node(node.child).select_columns(node.column_ids) + + +@_compile_node.register +def compile_drop(node: nodes.DropColumnsNode): + return compile_node(node.child).drop_columns(node.columns) + + +@_compile_node.register +def compile_readlocal(node: nodes.ReadLocalNode): + array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) + return compiled.CompiledArrayValue.mem_expr_from_pandas(array_as_pd) + + +@_compile_node.register +def compile_readgbq(node: nodes.ReadGbqNode): + return compiled.CompiledArrayValue( + node.table, + node.columns, + node.hidden_ordering_columns, + node.ordering, + ) + + +@_compile_node.register +def compile_promote_offsets(node: nodes.PromoteOffsetsNode): + return compile_node(node.child).promote_offsets(node.col_id) + + +@_compile_node.register +def compile_filter(node: nodes.FilterNode): + return compile_node(node.child).filter(node.predicate_id, node.keep_null) + + +@_compile_node.register +def compile_orderby(node: nodes.OrderByNode): + return compile_node(node.child).order_by(node.by, node.stable) + + +@_compile_node.register +def compile_reversed(node: nodes.ReversedNode): + return compile_node(node.child).reversed() + + +@_compile_node.register +def compile_project_unary(node: nodes.ProjectUnaryOpNode): + return compile_node(node.child).project_unary_op( + node.input_id, node.op, node.output_id + ) + + +@_compile_node.register +def compile_project_binary(node: nodes.ProjectBinaryOpNode): + return compile_node(node.child).project_binary_op( + node.left_input_id, node.right_input_id, node.op, node.output_id + ) + + +@_compile_node.register +def compile_project_ternary(node: nodes.ProjectTernaryOpNode): + return compile_node(node.child).project_ternary_op( + node.input_id1, node.input_id2, node.input_id3, node.op, node.output_id + ) + + +@_compile_node.register +def compile_concat(node: nodes.ConcatNode): + compiled_nodes = [compile_node(node) for node in node.children] + return compiled_nodes[0].concat(compiled_nodes[1:]) + + +@_compile_node.register +def compile_aggregate(node: nodes.AggregateNode): + return compile_node(node.child).aggregate( + node.aggregations, node.by_column_ids, node.dropna + ) + + +@_compile_node.register +def compile_corr(node: nodes.CorrNode): + return compile_node(node.child).corr_aggregate(node.corr_aggregations) + + +@_compile_node.register +def compile_window(node: nodes.WindowOpNode): + return compile_node(node.child).project_window_op( + node.column_name, + node.op, + node.window_spec, + node.output_name, + never_skip_nulls=node.never_skip_nulls, + skip_reproject_unsafe=node.skip_reproject_unsafe, + ) + + +@_compile_node.register +def compile_reproject(node: nodes.ReprojectOpNode): + return compile_node(node.child)._reproject_to_table() + + +@_compile_node.register +def compile_unpivot(node: nodes.UnpivotNode): + return compile_node(node.child).unpivot( + node.row_labels, + node.unpivot_columns, + passthrough_columns=node.passthrough_columns, + index_col_ids=node.index_col_ids, + dtype=node.dtype, + how=node.how, + ) + + +@_compile_node.register +def compile_assign(node: nodes.AssignNode): + return compile_node(node.child).assign(node.source_id, node.destination_id) + + +@_compile_node.register +def compile_assign_constant(node: nodes.AssignConstantNode): + return compile_node(node.child).assign_constant( + node.destination_id, node.value, node.dtype + ) + + +@_compile_node.register +def compiler_random_sample(node: nodes.RandomSampleNode): + return compile_node(node.child)._uniform_sampling(node.fraction) diff --git a/bigframes/core/joins/row_identity.py b/bigframes/core/compile/row_identity.py similarity index 94% rename from bigframes/core/joins/row_identity.py rename to bigframes/core/compile/row_identity.py index 76e456ec942..2e9bc0527ca 100644 --- a/bigframes/core/joins/row_identity.py +++ b/bigframes/core/compile/row_identity.py @@ -23,15 +23,16 @@ import ibis.expr.types as ibis_types import bigframes.constants as constants -import bigframes.core as core +import bigframes.core.compile as compiled import bigframes.core.joins.name_resolution as naming +import bigframes.core.ordering as orderings SUPPORTED_ROW_IDENTITY_HOW = {"outer", "left", "inner"} def join_by_row_identity( - left: core.ArrayValue, right: core.ArrayValue, *, how: str -) -> core.ArrayValue: + left: compiled.CompiledArrayValue, right: compiled.CompiledArrayValue, *, how: str +) -> compiled.CompiledArrayValue: """Compute join when we are joining by row identity not a specific column.""" if how not in SUPPORTED_ROW_IDENTITY_HOW: raise NotImplementedError( @@ -101,8 +102,8 @@ def join_by_row_identity( ) # Assume that left ordering is sufficient since 1:1 join over same base table join_total_order_cols = left_total_order_cols - new_ordering = core.ExpressionOrdering( - ordering_columns, total_ordering_columns=join_total_order_cols + new_ordering = orderings.ExpressionOrdering( + tuple(ordering_columns), total_ordering_columns=join_total_order_cols ) hidden_ordering_columns = [ @@ -117,8 +118,7 @@ def join_by_row_identity( if key.column_id in right._hidden_ordering_column_names.keys() ] - joined_expr = core.ArrayValue( - left._session, + joined_expr = compiled.CompiledArrayValue( left._table, columns=joined_columns, hidden_ordering_columns=hidden_ordering_columns, diff --git a/bigframes/core/joins/single_column.py b/bigframes/core/compile/single_column.py similarity index 87% rename from bigframes/core/joins/single_column.py rename to bigframes/core/compile/single_column.py index 0c0e2008b58..b992aa1d1d9 100644 --- a/bigframes/core/joins/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -23,16 +23,16 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types -import bigframes.core as core -import bigframes.core.joins.name_resolution as naming -import bigframes.core.joins.row_identity -import bigframes.core.ordering +import bigframes.core.compile as compiled +import bigframes.core.compile.row_identity +import bigframes.core.joins as joining +import bigframes.core.ordering as orderings def join_by_column( - left: core.ArrayValue, + left: compiled.CompiledArrayValue, left_column_ids: typing.Sequence[str], - right: core.ArrayValue, + right: compiled.CompiledArrayValue, right_column_ids: typing.Sequence[str], *, how: Literal[ @@ -42,7 +42,7 @@ def join_by_column( "right", ], allow_row_identity_join: bool = True, -) -> core.ArrayValue: +) -> compiled.CompiledArrayValue: """Join two expressions by column equality. Arguments: @@ -61,7 +61,7 @@ def join_by_column( """ if ( allow_row_identity_join - and how in bigframes.core.joins.row_identity.SUPPORTED_ROW_IDENTITY_HOW + and how in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW and left._table.equals(right._table) # Make sure we're joining on exactly the same column(s), at least with # regards to value its possible that they both have the same names but @@ -73,15 +73,15 @@ def join_by_column( for lcol, rcol in zip(left_column_ids, right_column_ids) ) ): - return bigframes.core.joins.row_identity.join_by_row_identity( + return bigframes.core.compile.row_identity.join_by_row_identity( left, right, how=how ) else: # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - l_public_mapping, r_public_mapping = naming.JOIN_NAME_REMAPPER( + l_public_mapping, r_public_mapping = joining.JOIN_NAME_REMAPPER( left.column_ids, right.column_ids ) - l_hidden_mapping, r_hidden_mapping = naming.JoinNameRemapper( + l_hidden_mapping, r_hidden_mapping = joining.JoinNameRemapper( namespace="hidden" )(left._hidden_column_ids, right._hidden_column_ids) l_mapping = {**l_public_mapping, **l_hidden_mapping} @@ -134,8 +134,7 @@ def join_by_column( for col in right._hidden_ordering_columns ], ] - return core.ArrayValue( - left._session, + return compiled.CompiledArrayValue( combined_table, columns=columns, hidden_ordering_columns=hidden_ordering_columns, @@ -151,12 +150,12 @@ def value_to_join_key(value: ibis_types.Value): def join_orderings( - left: core.ExpressionOrdering, - right: core.ExpressionOrdering, + left: orderings.ExpressionOrdering, + right: orderings.ExpressionOrdering, left_id_mapping: Mapping[str, str], right_id_mapping: Mapping[str, str], left_order_dominates: bool = True, -) -> core.ExpressionOrdering: +) -> orderings.ExpressionOrdering: left_ordering_refs = [ ref.with_name(left_id_mapping[ref.column_id]) for ref in left.all_ordering_columns @@ -176,7 +175,7 @@ def join_orderings( right_total_order_cols = frozenset( [right_id_mapping[id] for id in right.total_ordering_columns] ) - return core.ExpressionOrdering( - ordering_value_columns=joined_refs, + return orderings.ExpressionOrdering( + ordering_value_columns=tuple(joined_refs), total_ordering_columns=left_total_order_cols | right_total_order_cols, ) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index db0843fcbc0..2a19a83dd5f 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -193,7 +193,7 @@ def cumprod(self, *args, **kwargs) -> df.DataFrame: def shift(self, periods=1) -> series.Series: window = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -201,7 +201,7 @@ def shift(self, periods=1) -> series.Series: def diff(self, periods=1) -> series.Series: window = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -210,7 +210,7 @@ def diff(self, periods=1) -> series.Series: 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 = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), preceding=window - 1, following=0, min_periods=min_periods or window, @@ -225,7 +225,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), following=0, min_periods=min_periods, ) @@ -389,7 +389,7 @@ def _apply_window_op( ): """Apply window op to groupby. Defaults to grouped cumulative window.""" window_spec = window or core.WindowSpec( - grouping_keys=self._by_col_ids, following=0 + grouping_keys=tuple(self._by_col_ids), following=0 ) columns = self._aggregated_columns(numeric_only=numeric_only) block, result_ids = self._block.multi_apply_window_op( @@ -528,7 +528,7 @@ def cumcount(self, *args, **kwargs) -> series.Series: def shift(self, periods=1) -> series.Series: """Shift index by desired number of periods.""" window = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -536,7 +536,7 @@ def shift(self, periods=1) -> series.Series: def diff(self, periods=1) -> series.Series: window = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -545,7 +545,7 @@ def diff(self, periods=1) -> series.Series: 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 = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), preceding=window - 1, following=0, min_periods=min_periods or window, @@ -564,7 +564,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = core.WindowSpec( - grouping_keys=self._by_col_ids, + grouping_keys=tuple(self._by_col_ids), following=0, min_periods=min_periods, ) @@ -597,7 +597,7 @@ def _apply_window_op( ): """Apply window op to groupby. Defaults to grouped cumulative window.""" window_spec = window or core.WindowSpec( - grouping_keys=self._by_col_ids, following=0 + grouping_keys=tuple(self._by_col_ids), following=0 ) label = self._value_name if not discard_name else None diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index d18a0a38efe..f6ce084714a 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -311,7 +311,7 @@ def _loc_getitem_series_or_dataframe( values = [entry[i] for entry in key] index_cols_dict[index_name] = values keys_df = bigframes.dataframe.DataFrame( - index_cols_dict, session=series_or_dataframe._get_block().expr._session + index_cols_dict, session=series_or_dataframe._get_block().expr.session ) keys_df = keys_df.set_index(temporary_index_names, drop=True) keys_df = keys_df.rename_axis(original_index_names) @@ -324,7 +324,7 @@ def _loc_getitem_series_or_dataframe( index_name = "unnamed_col" keys_df = bigframes.dataframe.DataFrame( {index_name: key}, - session=series_or_dataframe._get_block().expr._session, + session=series_or_dataframe._get_block().expr.session, ) keys_df = keys_df.set_index(index_name, drop=True) if index_name_is_none: @@ -343,7 +343,7 @@ def _loc_getitem_series_or_dataframe( elif pd.api.types.is_scalar(key): index_name = "unnamed_col" keys_df = bigframes.dataframe.DataFrame( - {index_name: [key]}, session=series_or_dataframe._get_block().expr._session + {index_name: [key]}, session=series_or_dataframe._get_block().expr.session ) keys_df = keys_df.set_index(index_name, drop=True) keys_df.index.name = None diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index b9ffdff21ec..6c66c36062a 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -26,8 +26,7 @@ import bigframes.core as core import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks -import bigframes.core.joins as joins -import bigframes.core.joins.name_resolution as join_names +import bigframes.core.joins as joining import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.dtypes @@ -402,7 +401,7 @@ def to_pandas(self) -> pandas.Index: dtypes = dict(zip(index_columns, self.dtypes)) expr = self._expr.select_columns(index_columns) results, _ = expr.start_query() - df = expr._session._rows_to_dataframe(results, dtypes) + df = expr.session._rows_to_dataframe(results, dtypes) df = df.set_index(index_columns) index = df.index index.names = list(self._block._index_labels) @@ -461,11 +460,10 @@ def join_mono_indexed( ) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: left_expr = left._block.expr right_expr = right._block.expr - get_column_left, get_column_right = join_names.JOIN_NAME_REMAPPER( + get_column_left, get_column_right = joining.JOIN_NAME_REMAPPER( left_expr.column_ids, right_expr.column_ids ) - combined_expr = joins.join_by_column( - left._block.expr, + combined_expr = left._block.expr.join( left._block.index_columns, right._block.expr, right._block.index_columns, @@ -520,12 +518,11 @@ def join_multi_indexed( left_expr = left._block.expr right_expr = right._block.expr - get_column_left, get_column_right = join_names.JOIN_NAME_REMAPPER( + get_column_left, get_column_right = joining.JOIN_NAME_REMAPPER( left_expr.column_ids, right_expr.column_ids ) - combined_expr = joins.join_by_column( - left_expr, + combined_expr = left_expr.join( left_join_ids, right_expr, right_join_ids, diff --git a/bigframes/core/joins/__init__.py b/bigframes/core/joins/__init__.py index 3f9447aef0d..5d407ec22b2 100644 --- a/bigframes/core/joins/__init__.py +++ b/bigframes/core/joins/__init__.py @@ -15,11 +15,6 @@ """Helpers to join ArrayValue objects.""" from bigframes.core.joins.merge import merge -from bigframes.core.joins.row_identity import join_by_row_identity -from bigframes.core.joins.single_column import join_by_column +from bigframes.core.joins.name_resolution import JOIN_NAME_REMAPPER, JoinNameRemapper -__all__ = ( - "join_by_row_identity", - "join_by_column", - "merge", -) +__all__ = ("merge", "JoinNameRemapper", "JOIN_NAME_REMAPPER") diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py new file mode 100644 index 00000000000..7b252b164f6 --- /dev/null +++ b/bigframes/core/nodes.py @@ -0,0 +1,245 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from dataclasses import dataclass, field +import functools +import typing +from typing import Optional, Tuple + +import pandas + +import bigframes.core.guid +from bigframes.core.ordering import OrderingColumnReference +import bigframes.core.window_spec as window +import bigframes.dtypes +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops + +if typing.TYPE_CHECKING: + import ibis.expr.types as ibis_types + + import bigframes.core.ordering as orderings + import bigframes.session + + +@dataclass(frozen=True) +class BigFrameNode: + """ + Immutable node for representing 2D typed array as a tree of operators. + + All subclasses must be hashable so as to be usable as caching key. + """ + + @property + def deterministic(self) -> bool: + """Whether this node will evaluates deterministically.""" + return True + + @property + def child_nodes(self) -> typing.Sequence[BigFrameNode]: + """Direct children of this node""" + return tuple([]) + + @functools.cached_property + def session(self): + sessions = [] + for child in self.child_nodes: + if child.session is not None: + sessions.append(child.session) + unique_sessions = len(set(sessions)) + if unique_sessions > 1: + raise ValueError("Cannot use combine sources from multiple sessions.") + elif unique_sessions == 1: + return sessions[0] + return None + + +@dataclass(frozen=True) +class UnaryNode(BigFrameNode): + child: BigFrameNode + + @property + def child_nodes(self) -> typing.Sequence[BigFrameNode]: + return (self.child,) + + +@dataclass(frozen=True) +class JoinNode(BigFrameNode): + left_child: BigFrameNode + right_child: BigFrameNode + left_column_ids: typing.Tuple[str, ...] + right_column_ids: typing.Tuple[str, ...] + how: typing.Literal[ + "inner", + "left", + "outer", + "right", + ] + allow_row_identity_join: bool = True + + @property + def child_nodes(self) -> typing.Sequence[BigFrameNode]: + return (self.left_child, self.right_child) + + +@dataclass(frozen=True) +class ConcatNode(BigFrameNode): + children: Tuple[BigFrameNode, ...] + + @property + def child_nodes(self) -> typing.Sequence[BigFrameNode]: + return self.children + + +# Input Nodex +@dataclass(frozen=True) +class ReadLocalNode(BigFrameNode): + feather_bytes: bytes + column_ids: typing.Tuple[str, ...] + + +# TODO: Refactor to take raw gbq object reference +@dataclass(frozen=True) +class ReadGbqNode(BigFrameNode): + table: ibis_types.Table = field() + table_session: bigframes.session.Session = field() + columns: Tuple[ibis_types.Value, ...] = field() + hidden_ordering_columns: Tuple[ibis_types.Value, ...] = field() + ordering: orderings.ExpressionOrdering = field() + + @property + def session(self): + return (self.table_session,) + + +# Unary nodes +@dataclass(frozen=True) +class DropColumnsNode(UnaryNode): + columns: Tuple[str, ...] + + +@dataclass(frozen=True) +class PromoteOffsetsNode(UnaryNode): + col_id: str + + +@dataclass(frozen=True) +class FilterNode(UnaryNode): + predicate_id: str + keep_null: bool = False + + +@dataclass(frozen=True) +class OrderByNode(UnaryNode): + by: Tuple[OrderingColumnReference, ...] + stable: bool = False + + +@dataclass(frozen=True) +class ReversedNode(UnaryNode): + pass + + +@dataclass(frozen=True) +class SelectNode(UnaryNode): + column_ids: typing.Tuple[str, ...] + + +@dataclass(frozen=True) +class ProjectUnaryOpNode(UnaryNode): + input_id: str + op: ops.UnaryOp + output_id: Optional[str] = None + + +@dataclass(frozen=True) +class ProjectBinaryOpNode(UnaryNode): + left_input_id: str + right_input_id: str + op: ops.BinaryOp + output_id: str + + +@dataclass(frozen=True) +class ProjectTernaryOpNode(UnaryNode): + input_id1: str + input_id2: str + input_id3: str + op: ops.TernaryOp + output_id: str + + +@dataclass(frozen=True) +class AggregateNode(UnaryNode): + aggregations: typing.Tuple[typing.Tuple[str, agg_ops.AggregateOp, str], ...] + by_column_ids: typing.Tuple[str, ...] = tuple([]) + dropna: bool = True + + +# TODO: Unify into aggregate +@dataclass(frozen=True) +class CorrNode(UnaryNode): + corr_aggregations: typing.Tuple[typing.Tuple[str, str, str], ...] + + +@dataclass(frozen=True) +class WindowOpNode(UnaryNode): + column_name: str + op: agg_ops.WindowOp + window_spec: window.WindowSpec + output_name: typing.Optional[str] = None + never_skip_nulls: bool = False + skip_reproject_unsafe: bool = False + + +@dataclass(frozen=True) +class ReprojectOpNode(UnaryNode): + pass + + +@dataclass(frozen=True) +class UnpivotNode(UnaryNode): + row_labels: typing.Tuple[typing.Hashable, ...] + unpivot_columns: typing.Tuple[ + typing.Tuple[str, typing.Tuple[typing.Optional[str], ...]], ... + ] + passthrough_columns: typing.Tuple[str, ...] = () + index_col_ids: typing.Tuple[str, ...] = ("index",) + dtype: typing.Union[ + bigframes.dtypes.Dtype, typing.Tuple[bigframes.dtypes.Dtype, ...] + ] = (pandas.Float64Dtype(),) + how: typing.Literal["left", "right"] = "left" + + +@dataclass(frozen=True) +class AssignNode(UnaryNode): + source_id: str + destination_id: str + + +@dataclass(frozen=True) +class AssignConstantNode(UnaryNode): + destination_id: str + value: typing.Hashable + dtype: typing.Optional[bigframes.dtypes.Dtype] + + +@dataclass(frozen=True) +class RandomSampleNode(UnaryNode): + fraction: float + + @property + def deterministic(self) -> bool: + return False diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index d5f07ecf91a..2cecd2fe7b2 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -86,7 +86,7 @@ class IntegerEncoding: class ExpressionOrdering: """Immutable object that holds information about the ordering of rows in a ArrayValue object.""" - ordering_value_columns: Sequence[OrderingColumnReference] = () + ordering_value_columns: typing.Tuple[OrderingColumnReference, ...] = () integer_encoding: IntegerEncoding = IntegerEncoding(False) string_encoding: StringEncoding = StringEncoding(False) # A table has a total ordering defined by the identities of a set of 1 or more columns. @@ -170,7 +170,7 @@ def with_column_remap(self, mapping: typing.Mapping[str, str]): mapping.get(col_id, col_id) for col_id in self.total_ordering_columns ) return ExpressionOrdering( - new_value_columns, + tuple(new_value_columns), integer_encoding=self.integer_encoding, string_encoding=self.string_encoding, total_ordering_columns=new_total_order, diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py new file mode 100644 index 00000000000..3458bfb1b8c --- /dev/null +++ b/bigframes/core/window_spec.py @@ -0,0 +1,35 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +import typing + +import bigframes.core.ordering as orderings + + +@dataclass(frozen=True) +class WindowSpec: + """ + Specifies a window over which aggregate and analytic function may be applied. + grouping_keys: set of column ids to group on + preceding: Number of preceding rows in the window + following: Number of preceding rows in the window + ordering: List of columns ids and ordering direction to override base ordering + """ + + grouping_keys: typing.Tuple[str, ...] = tuple() + ordering: typing.Tuple[orderings.OrderingColumnReference, ...] = tuple() + preceding: typing.Optional[int] = None + following: typing.Optional[int] = None + min_periods: int = 0 diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 3fd83198768..9d22c02d871 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -170,9 +170,7 @@ def __init__( if isinstance(dt, pandas.ArrowDtype) ) ): - self._block = blocks.block_from_local( - pd_dataframe, session or bigframes.pandas.get_global_session() - ) + self._block = blocks.block_from_local(pd_dataframe) elif session: self._block = session.read_pandas(pd_dataframe)._get_block() else: @@ -299,7 +297,7 @@ def values(self) -> numpy.ndarray: @property def _session(self) -> bigframes.Session: - return self._get_block().expr._session + return self._get_block().expr.session def __len__(self): rows, _ = self.shape @@ -1107,7 +1105,7 @@ def _assign_single_item( ) local_df = bigframes.dataframe.DataFrame( - {k: v}, session=self._get_block().expr._session + {k: v}, session=self._get_block().expr.session ) # local_df is likely (but not guarunteed) to be cached locally # since the original list came from memory and so is probably < MAX_INLINE_DF_SIZE @@ -2203,7 +2201,7 @@ def to_csv( field_delimiter=sep, header=header, ) - _, query_job = self._block.expr._session._start_query(export_data_statement) + _, query_job = self._block.expr.session._start_query(export_data_statement) self._set_internal_query_job(query_job) def to_json( @@ -2245,7 +2243,7 @@ def to_json( format="JSON", export_options={}, ) - _, query_job = self._block.expr._session._start_query(export_data_statement) + _, query_job = self._block.expr.session._start_query(export_data_statement) self._set_internal_query_job(query_job) def to_gbq( @@ -2274,7 +2272,7 @@ def to_gbq( write_disposition=dispositions[if_exists], destination=bigquery.table.TableReference.from_string( destination_table, - default_project=self._block.expr._session.bqclient.project, + default_project=self._block.expr.session.bqclient.project, ), ) @@ -2321,7 +2319,7 @@ def to_parquet( format="PARQUET", export_options=export_options, ) - _, query_job = self._block.expr._session._start_query(export_data_statement) + _, query_job = self._block.expr.session._start_query(export_data_statement) self._set_internal_query_job(query_job) def to_dict( @@ -2464,7 +2462,7 @@ def _run_io_query( """Executes a query job presenting this dataframe and returns the destination table.""" expr = self._block.expr - session = expr._session + session = expr.session sql = self._create_io_query(index=index, ordering_id=ordering_id) _, query_job = session._start_query( sql=sql, job_config=job_config # type: ignore diff --git a/bigframes/ml/metrics.py b/bigframes/ml/metrics.py index 3bcb621f745..5731b946ca9 100644 --- a/bigframes/ml/metrics.py +++ b/bigframes/ml/metrics.py @@ -96,7 +96,7 @@ def roc_curve( y_true_series, y_score_series = utils.convert_to_series(y_true, y_score) - session = y_true_series._block.expr._session + session = y_true_series._block.expr.session # We operate on rows, so, remove the index if there is one # TODO(bmil): check that the indexes are equivalent before removing diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index b9abb2cc038..d33befe4da6 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -94,9 +94,7 @@ def __init__( if isinstance(dt, pd.ArrowDtype) ) ): - self._block = blocks.block_from_local( - pd_dataframe, session or bigframes.pandas.get_global_session() - ) + self._block = blocks.block_from_local(pd_dataframe) elif session: self._block = session.read_pandas(pd_dataframe)._get_block() else: diff --git a/bigframes/series.py b/bigframes/series.py index c1914527834..37d00d16f32 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -29,7 +29,6 @@ import bigframes.constants as constants import bigframes.core -from bigframes.core import WindowSpec import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.groupby as groupby @@ -43,6 +42,7 @@ import bigframes.core.scalar as scalars import bigframes.core.utils as utils import bigframes.core.window +import bigframes.core.window_spec import bigframes.dataframe import bigframes.dtypes import bigframes.formatting_helpers as formatter @@ -367,43 +367,43 @@ def between(self, left, right, inclusive="both"): def cumsum(self) -> Series: return self._apply_window_op( - agg_ops.sum_op, bigframes.core.WindowSpec(following=0) + agg_ops.sum_op, bigframes.core.window_spec.WindowSpec(following=0) ) def ffill(self, *, limit: typing.Optional[int] = None) -> Series: - window = bigframes.core.WindowSpec(preceding=limit, following=0) + window = bigframes.core.window_spec.WindowSpec(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) pad = ffill def bfill(self, *, limit: typing.Optional[int] = None) -> Series: - window = bigframes.core.WindowSpec(preceding=0, following=limit) + window = bigframes.core.window_spec.WindowSpec(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) def cummax(self) -> Series: return self._apply_window_op( - agg_ops.max_op, bigframes.core.WindowSpec(following=0) + agg_ops.max_op, bigframes.core.window_spec.WindowSpec(following=0) ) def cummin(self) -> Series: return self._apply_window_op( - agg_ops.min_op, bigframes.core.WindowSpec(following=0) + agg_ops.min_op, bigframes.core.window_spec.WindowSpec(following=0) ) def cumprod(self) -> Series: return self._apply_window_op( - agg_ops.product_op, bigframes.core.WindowSpec(following=0) + agg_ops.product_op, bigframes.core.window_spec.WindowSpec(following=0) ) def shift(self, periods: int = 1) -> Series: - window = bigframes.core.WindowSpec( + window = bigframes.core.window_spec.WindowSpec( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) def diff(self, periods: int = 1) -> Series: - window = bigframes.core.WindowSpec( + window = bigframes.core.window_spec.WindowSpec( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -805,7 +805,7 @@ def mode(self) -> Series: block, max_value_count_col_id = block.apply_window_op( value_count_col_id, agg_ops.max_op, - window_spec=WindowSpec(), + window_spec=bigframes.core.window_spec.WindowSpec(), ) block, is_mode_col_id = block.apply_binary_op( value_count_col_id, @@ -1009,9 +1009,7 @@ def _apply_aggregation(self, op: agg_ops.AggregateOp) -> Any: return self._block.get_stat(self._value_column, op) def _apply_window_op( - self, - op: agg_ops.WindowOp, - window_spec: bigframes.core.WindowSpec, + self, op: agg_ops.WindowOp, window_spec: bigframes.core.window_spec.WindowSpec ): block = self._block block, result_id = block.apply_window_op( @@ -1070,7 +1068,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 = WindowSpec( + window_spec = bigframes.core.window_spec.WindowSpec( preceding=window - 1, following=0, min_periods=min_periods or window ) return bigframes.core.window.Window( @@ -1078,7 +1076,9 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window ) def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: - window_spec = WindowSpec(following=0, min_periods=min_periods) + window_spec = bigframes.core.window_spec.WindowSpec( + following=0, min_periods=min_periods + ) return bigframes.core.window.Window( self._block, window_spec, self._block.value_columns, is_series=True ) @@ -1251,7 +1251,7 @@ def reindex(self, index=None, *, validate: typing.Optional[bool] = None): "Cannot reindex with index with different nlevels" ) new_indexer = bigframes.dataframe.DataFrame( - index=index, session=self._get_block().expr._session + index=index, session=self._get_block().expr.session )[[]] # multiindex join is senstive to index names, so we will set all these result = new_indexer.rename_axis(range(new_indexer.index.nlevels)).join( @@ -1415,7 +1415,7 @@ def map( elif isinstance(arg, Mapping): map_df = bigframes.dataframe.DataFrame( {"keys": list(arg.keys()), self.name: list(arg.values())}, - session=self._get_block().expr._session, + session=self._get_block().expr.session, ) map_df = map_df.set_index("keys") elif callable(arg): diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index af1f70d54d5..473de62f53a 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -68,6 +68,7 @@ import bigframes.core.blocks as blocks import bigframes.core.guid as guid from bigframes.core.ordering import IntegerEncoding, OrderingColumnReference +import bigframes.core.ordering as orderings import bigframes.core.utils as utils import bigframes.dataframe as dataframe import bigframes.formatting_helpers as formatting_helpers @@ -206,6 +207,10 @@ def _session_dataset_id(self): def _project(self): return self.bqclient.project + def __hash__(self): + # Stable hash needed to use in expression tree + return hash(self._session_id) + def _create_and_bind_bq_session(self): """Create a BQ session and bind the session id with clients to capture BQ activities: go/bigframes-transient-data""" @@ -592,11 +597,13 @@ def _read_gbq_table( # primary key(s) are set on a table. The query engine assumes such # columns are unique, even if not enforced. is_total_ordering = True - ordering = core.ExpressionOrdering( - ordering_value_columns=[ - core.OrderingColumnReference(column_id) - for column_id in total_ordering_cols - ], + ordering = orderings.ExpressionOrdering( + ordering_value_columns=tuple( + [ + core.OrderingColumnReference(column_id) + for column_id in total_ordering_cols + ] + ), total_ordering_columns=frozenset(total_ordering_cols), ) @@ -634,10 +641,13 @@ def _read_gbq_table( distinct_count = row["distinct_count"] is_total_ordering = total_count == distinct_count - ordering = core.ExpressionOrdering( - ordering_value_columns=[ - core.OrderingColumnReference(column_id) for column_id in index_cols - ], + ordering = orderings.ExpressionOrdering( + ordering_value_columns=tuple( + [ + core.OrderingColumnReference(column_id) + for column_id in index_cols + ] + ), total_ordering_columns=frozenset(index_cols), ) @@ -713,7 +723,7 @@ def _read_gbq_with_ordering( index_cols: Iterable[str] = (), index_labels: Iterable[Optional[str]] = (), hidden_cols: Iterable[str] = (), - ordering: core.ExpressionOrdering, + ordering: orderings.ExpressionOrdering, is_total_ordering: bool = False, api_name: str, ) -> dataframe.DataFrame: @@ -826,7 +836,7 @@ def _read_ibis( index_labels: Iterable[blocks.Label], column_keys: Iterable[str], column_labels: Iterable[blocks.Label], - ordering: core.ExpressionOrdering, + ordering: orderings.ExpressionOrdering, ) -> dataframe.DataFrame: """Turns a table expression (plus index column) into a DataFrame.""" @@ -843,7 +853,7 @@ def _read_ibis( hidden_ordering_columns.append(table_expression[ref.column_id]) block = blocks.Block( - core.ArrayValue( + core.ArrayValue.from_ibis( self, table_expression, columns, hidden_ordering_columns, ordering ), index_columns=[index_col.get_name() for index_col in index_cols], @@ -959,8 +969,8 @@ def _read_pandas( ) self._start_generic_job(load_job) - ordering = core.ExpressionOrdering( - ordering_value_columns=[OrderingColumnReference(ordering_col)], + ordering = orderings.ExpressionOrdering( + ordering_value_columns=tuple([OrderingColumnReference(ordering_col)]), total_ordering_columns=frozenset([ordering_col]), integer_encoding=IntegerEncoding(True, is_sequential=True), ) @@ -1303,7 +1313,7 @@ def _create_sequential_ordering( table: ibis_types.Table, index_cols: Iterable[str] = (), api_name: str = "", - ) -> Tuple[ibis_types.Table, core.ExpressionOrdering]: + ) -> Tuple[ibis_types.Table, orderings.ExpressionOrdering]: # Since this might also be used as the index, don't use the default # "ordering ID" name. default_ordering_name = guid.generate_guid("bigframes_ordering_") @@ -1320,8 +1330,8 @@ def _create_sequential_ordering( f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" ) ordering_reference = core.OrderingColumnReference(default_ordering_name) - ordering = core.ExpressionOrdering( - ordering_value_columns=[ordering_reference], + ordering = orderings.ExpressionOrdering( + ordering_value_columns=tuple([ordering_reference]), total_ordering_columns=frozenset([default_ordering_name]), integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), ) diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index f7fc4eaa8f6..084b723fba6 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -98,7 +98,7 @@ def assert_loading_msg_exist(capystOut: str, pattern=job_load_message_regex): def test_query_job_repr_html(penguins_df_default_index: bf.dataframe.DataFrame): bf.options.display.progress_bar = "terminal" - penguins_df_default_index._block._expr._session.bqclient.default_query_job_config.use_query_cache = ( + penguins_df_default_index._block._expr.session.bqclient.default_query_job_config.use_query_cache = ( False ) penguins_df_default_index.to_pandas() @@ -117,7 +117,7 @@ def test_query_job_repr_html(penguins_df_default_index: bf.dataframe.DataFrame): def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): - penguins_df_default_index._block._expr._session.bqclient.default_query_job_config.use_query_cache = ( + penguins_df_default_index._block._expr.session.bqclient.default_query_job_config.use_query_cache = ( False ) penguins_df_default_index.to_pandas() diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index c9510290b66..05d8b841855 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2858,7 +2858,7 @@ def test_map_series_input(scalars_dfs): pd_map_series = scalars_pandas_df.string_col.iloc[0 : len(new_index)] pd_map_series.index = new_index bf_map_series = series.Series( - pd_map_series, session=scalars_df._get_block().expr._session + pd_map_series, session=scalars_df._get_block().expr.session ) pd_result = scalars_pandas_df.int64_too.map(pd_map_series) @@ -2877,7 +2877,7 @@ def test_map_series_input_duplicates_error(scalars_dfs): pd_map_series = scalars_pandas_df.string_col.iloc[0 : len(new_index)] pd_map_series.index = new_index bf_map_series = series.Series( - pd_map_series, session=scalars_df._get_block().expr._session + pd_map_series, session=scalars_df._get_block().expr.session ) with pytest.raises(pd.errors.InvalidIndexError): diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 127a88a760b..bf72e444eba 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -318,7 +318,6 @@ def test_read_pandas(session, scalars_dfs): _, scalars_pandas_df = scalars_dfs df = session.read_pandas(scalars_pandas_df) - assert df._block._expr._ordering is not None result = df.to_pandas() expected = scalars_pandas_df @@ -350,9 +349,8 @@ def test_read_pandas_rowid_exists_adds_suffix(session, scalars_pandas_df_default pandas_df = scalars_pandas_df_default_index.copy() pandas_df["rowid"] = np.arange(pandas_df.shape[0]) - df = session.read_pandas(pandas_df) - total_order_col = df._block._expr._ordering.total_order_col - assert total_order_col and total_order_col.column_id == "rowid_2" + df_roundtrip = session.read_pandas(pandas_df).to_pandas() + pd.testing.assert_frame_equal(df_roundtrip, pandas_df, check_dtype=False) def test_read_pandas_tokyo( @@ -385,7 +383,6 @@ def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): # Convert default pandas dtypes to match BigQuery DataFrames dtypes. dtype=dtype, ) - assert df._block._expr._ordering is not None # TODO(chelsealin): If we serialize the index, can more easily compare values. pd.testing.assert_index_equal(df.columns, scalars_df.columns) @@ -441,7 +438,6 @@ def test_read_csv_local_default_engine(session, scalars_dfs, sep): # Convert default pandas dtypes to match BigQuery DataFrames dtypes. dtype=dtype, ) - assert df._block._expr._ordering is not None # TODO(chelsealin): If we serialize the index, can more easily compare values. pd.testing.assert_index_equal(df.columns, scalars_df.columns) @@ -976,7 +972,6 @@ def test_read_json_gcs_default_engine(session, scalars_dfs, gcs_folder): orient="records", ) - assert df._block._expr._ordering is not None pd.testing.assert_index_equal(df.columns, scalars_df.columns) # The auto detects of BigQuery load job have restrictions to detect the bytes, diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py index a7e9b5a84b7..86715d090cb 100644 --- a/tests/unit/core/test_blocks.py +++ b/tests/unit/core/test_blocks.py @@ -18,8 +18,6 @@ import bigframes.core.blocks as blocks -from .. import resources - @pytest.mark.parametrize( ("data",), @@ -76,9 +74,8 @@ ) def test_block_from_local(data): expected = pandas.DataFrame(data) - session = resources.create_pandas_session({}) - block = blocks.block_from_local(data, session=session) + block = blocks.block_from_local(data) pandas.testing.assert_index_equal(block.column_labels, expected.columns) assert tuple(block.index_labels) == tuple(expected.index.names) diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 0a68600a359..f660d774f06 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -22,6 +22,7 @@ import bigframes import bigframes.core as core +import bigframes.core.ordering import bigframes.session.clients """Utilities for creating test resources.""" @@ -61,14 +62,20 @@ def create_pandas_session(tables: Dict[str, pandas.DataFrame]) -> bigframes.Sess def create_arrayvalue( df: pandas.DataFrame, total_ordering_columns: List[str] -) -> bigframes.core.ArrayValue: +) -> core.ArrayValue: session = create_pandas_session({"test_table": df}) ibis_table = session.ibis_client.table("test_table") columns = tuple(ibis_table[key] for key in ibis_table.columns) - ordering = core.ExpressionOrdering( - [core.OrderingColumnReference(column) for column in total_ordering_columns], + ordering = bigframes.core.ordering.ExpressionOrdering( + tuple( + [core.OrderingColumnReference(column) for column in total_ordering_columns] + ), total_ordering_columns=frozenset(total_ordering_columns), ) - return core.ArrayValue( - session=session, table=ibis_table, columns=columns, ordering=ordering + return core.ArrayValue.from_ibis( + session=session, + table=ibis_table, + columns=columns, + hidden_ordering_columns=(), + ordering=ordering, ) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 69b9e798075..d9672b2635b 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -16,6 +16,7 @@ import pandas import bigframes.core as core +import bigframes.core.ordering import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -37,15 +38,19 @@ def test_arrayvalue_constructor_from_ibis_table_adds_all_columns(): ) ibis_table = session.ibis_client.table("test_table") columns = (ibis_table["col1"], ibis_table["col2"], ibis_table["col3"]) - ordering = core.ExpressionOrdering( - [core.OrderingColumnReference("col1")], + ordering = bigframes.core.ordering.ExpressionOrdering( + tuple([core.OrderingColumnReference("col1")]), total_ordering_columns=frozenset(["col1"]), ) - actual = core.ArrayValue( - session=session, table=ibis_table, columns=columns, ordering=ordering + actual = core.ArrayValue.from_ibis( + session=session, + table=ibis_table, + columns=columns, + ordering=ordering, + hidden_ordering_columns=(), ) - assert actual._table is ibis_table - assert len(actual.columns) == 3 + assert actual.compile()._table is ibis_table + assert len(actual.column_ids) == 3 def test_arrayvalue_with_get_column_type(): @@ -78,7 +83,7 @@ def test_arrayvalue_with_get_column(): ), total_ordering_columns=["col1"], ) - col1 = value._get_ibis_column("col1") + col1 = value.compile()._get_ibis_column("col1") assert isinstance(col1, ibis_types.Value) assert col1.get_name() == "col1" assert col1.type().is_int64() @@ -95,7 +100,7 @@ def test_arrayvalues_to_ibis_expr_with_get_column(): ), total_ordering_columns=["col1"], ) - expr = value._get_ibis_column("col1") + expr = value.compile()._get_ibis_column("col1") assert expr.get_name() == "col1" assert expr.type().is_int64() @@ -112,7 +117,7 @@ def test_arrayvalues_to_ibis_expr_with_concat(): total_ordering_columns=["col1"], ) expr = value.concat([value]) - actual = expr._to_ibis_expr("unordered") + actual = expr.compile()._to_ibis_expr("unordered") assert len(actual.columns) == 3 # TODO(ashleyxu, b/299631930): test out the union expression assert actual.columns[0] == "column_0" @@ -131,8 +136,8 @@ def test_arrayvalues_to_ibis_expr_with_project_unary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_unary_op("col1", ops.AsTypeOp("string")) - assert value.columns[0].type().is_int64() + expr = value.project_unary_op("col1", ops.AsTypeOp("string")).compile() + assert value.compile().columns[0].type().is_int64() assert expr.columns[0].type().is_string() @@ -147,7 +152,7 @@ def test_arrayvalues_to_ibis_expr_with_project_binary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_binary_op("col2", "col3", ops.add_op, "col4") + expr = value.project_binary_op("col2", "col3", ops.add_op, "col4").compile() assert expr.columns[3].type().is_float64() actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 4 @@ -166,7 +171,9 @@ def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_ternary_op("col2", "col3", "col4", ops.where_op, "col5") + expr = value.project_ternary_op( + "col2", "col3", "col4", ops.where_op, "col5" + ).compile() assert expr.columns[4].type().is_float64() actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 5 @@ -188,7 +195,7 @@ def test_arrayvalue_to_ibis_expr_with_aggregate(): aggregations=(("col1", agg_ops.sum_op, "col4"),), by_column_ids=["col1"], dropna=False, - ) + ).compile() actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 2 assert actual.columns[0] == "col1" @@ -207,7 +214,7 @@ def test_arrayvalue_to_ibis_expr_with_corr_aggregate(): ), total_ordering_columns=["col1"], ) - expr = value.corr_aggregate(corr_aggregations=[("col1", "col3", "col4")]) + expr = value.corr_aggregate(corr_aggregations=[("col1", "col3", "col4")]).compile() actual = expr._to_ibis_expr("unordered") assert len(expr.columns) == 1 assert actual.columns[0] == "col4" From 29032d06811569121f7be2a7de915740df7daf6e Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 26 Oct 2023 18:20:58 -0700 Subject: [PATCH 118/921] fix: fix bug with column names under repeated column assignment (#150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 23 +++++++++++------------ tests/system/small/test_dataframe.py | 22 ++++++++++++++++++++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9d22c02d871..01117d3e0ab 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1110,19 +1110,18 @@ def _assign_single_item( # local_df is likely (but not guarunteed) to be cached locally # since the original list came from memory and so is probably < MAX_INLINE_DF_SIZE - this_offsets_col_id = bigframes.core.guid.generate_guid() - this_expr = self._get_block()._expr.promote_offsets(this_offsets_col_id) - block = blocks.Block( - expr=this_expr, - index_labels=self.index.names, - index_columns=self._block.index_columns, - column_labels=[this_offsets_col_id] + list(self._block.value_columns), - ) # offsets are temporarily the first value column, label set to id - this_df_with_offsets = DataFrame(data=block) - join_result = this_df_with_offsets.join( - other=local_df, on=this_offsets_col_id, how="left" + new_column_block = local_df._block + original_index_column_ids = self._block.index_columns + self_block = self._block.reset_index(drop=False) + result_index, (get_column_left, get_column_right) = self_block.index.join( + new_column_block.index, how="left", block_identity_join=True ) - return join_result.drop(columns=[this_offsets_col_id]) + result_block = result_index._block + result_block = result_block.set_index( + [get_column_left[col_id] for col_id in original_index_column_ids], + index_labels=self._block.index_labels, + ) + return DataFrame(result_block) else: return self._assign_scalar(k, v) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index a746a1867cf..e459e3bee3b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -505,14 +505,32 @@ def test_assign_new_column_w_setitem_list(scalars_dfs): pd.testing.assert_frame_equal(bf_result, pd_result) +def test_assign_new_column_w_setitem_list_repeated(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] + pd_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] + bf_df["new_col_2"] = [1, 3, 2, 5, 4, 7, 6, 9, 8] + pd_df["new_col_2"] = [1, 3, 2, 5, 4, 7, 6, 9, 8] + bf_result = bf_df.to_pandas() + pd_result = pd_df + + # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + pd_result["new_col_2"] = pd_result["new_col_2"].astype("Int64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + def test_assign_new_column_w_setitem_list_custom_index(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_df = scalars_df.copy() pd_df = scalars_pandas_df.copy() # set the custom index - pd_df = pd_df.set_index("string_col") - bf_df = bf_df.set_index("string_col") + pd_df = pd_df.set_index(["string_col", "int64_col"]) + bf_df = bf_df.set_index(["string_col", "int64_col"]) bf_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] pd_df["new_col"] = [9, 8, 7, 6, 5, 4, 3, 2, 1] From 2d7128d9b2107c3667a5ad7f153d446bfdc04df5 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 27 Oct 2023 22:46:13 +0000 Subject: [PATCH 119/921] test: refactor remote function tests (#147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes moves the tests that deploy cloud function to large remote function tests, and the tests that do not make call to bigquery service to unit tests. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/large/test_remote_function.py | 90 ++++++++++++ tests/system/small/test_remote_function.py | 156 ++++----------------- tests/unit/test_remote_function.py | 28 ++++ 3 files changed, 148 insertions(+), 126 deletions(-) create mode 100644 tests/unit/test_remote_function.py diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 730a1dbde4e..c8f8f66ebaa 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -27,6 +27,7 @@ import pytest import test_utils.prefixer +import bigframes from bigframes.remote_function import ( get_cloud_function_name, get_remote_function_locations, @@ -1120,3 +1121,92 @@ def plusone(x): ) for dir_ in dirs_to_cleanup: shutil.rmtree(dir_) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_via_session_context_connection_setter( + scalars_dfs, dataset_id, bq_cf_connection +): + # Creating a session scoped only to this test as we would be setting a + # property in it + context = bigframes.BigQueryOptions() + context.bq_connection = bq_cf_connection + session = bigframes.connect(context) + + try: + # Without an explicit bigquery connection, the one present in Session, + # set via context setter would be used. Without an explicit `reuse` the + # default behavior of reuse=True will take effect. Please note that the + # udf is same as the one used in other tests in this file so the underlying + # cloud function would be common with reuse=True. Since we are using a + # unique dataset_id, even though the cloud function would be reused, the bq + # remote function would still be created, making use of the bq connection + # set in the BigQueryOptions above. + @session.remote_function([int], int, dataset=dataset_id) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_default_connection(session, scalars_dfs, dataset_id): + try: + + @session.remote_function([int], int, dataset=dataset_id) + def square(x): + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square + ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index d024a57dedc..89907a53dfc 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -13,14 +13,11 @@ # limitations under the License. from google.cloud import bigquery -from ibis.backends.bigquery import datatypes as bq_types -from ibis.expr import datatypes as ibis_types import pandas as pd import pytest import bigframes from bigframes import remote_function as rf -import bigframes.pandas as bpd from tests.system.utils import assert_pandas_df_equal_ignore_ordering @@ -65,45 +62,14 @@ def bq_cf_connection_location_project_mismatched() -> str: @pytest.fixture(scope="module") -def session_with_bq_connection(bq_cf_connection) -> bigframes.Session: - return bigframes.Session(bigframes.BigQueryOptions(bq_connection=bq_cf_connection)) - - -@pytest.fixture(scope="module") -def session_with_bq_connection_location_specified( - bq_cf_connection_location, -) -> bigframes.Session: - return bigframes.Session( - bigframes.BigQueryOptions(bq_connection=bq_cf_connection_location) - ) - - -@pytest.fixture(scope="module") -def session_with_bq_connection_location_mistached( - bq_cf_connection_location_mistached, -) -> bigframes.Session: - return bigframes.Session( - bigframes.BigQueryOptions(bq_connection=bq_cf_connection_location_mistached) - ) - - -@pytest.fixture(scope="module") -def session_with_bq_connection_location_project_specified( - bq_cf_connection_location_project, +def session_with_bq_connection_and_permanent_dataset( + bq_cf_connection, dataset_id_permanent ) -> bigframes.Session: - return bigframes.Session( - bigframes.BigQueryOptions(bq_connection=bq_cf_connection_location_project) + session = bigframes.Session( + bigframes.BigQueryOptions(bq_connection=bq_cf_connection) ) - - -def test_supported_types_correspond(): - # The same types should be representable by the supported Python and BigQuery types. - ibis_types_from_python = {ibis_types.dtype(t) for t in rf.SUPPORTED_IO_PYTHON_TYPES} - ibis_types_from_bigquery = { - bq_types.BigQueryType.to_ibis(tk) for tk in rf.SUPPORTED_IO_BIGQUERY_TYPEKINDS - } - - assert ibis_types_from_python == ibis_types_from_bigquery + session._session_dataset = bigquery.Dataset(dataset_id_permanent) + return session @pytest.mark.flaky(retries=2, delay=120) @@ -311,11 +277,13 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_direct_session_param(session_with_bq_connection, scalars_dfs): +def test_remote_function_direct_session_param( + session_with_bq_connection_and_permanent_dataset, scalars_dfs +): @rf.remote_function( [int], int, - session=session_with_bq_connection, + session=session_with_bq_connection_and_permanent_dataset, ) def square(x): return x * x @@ -345,7 +313,9 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_via_session_default(session_with_bq_connection, scalars_dfs): +def test_remote_function_via_session_default( + session_with_bq_connection_and_permanent_dataset, scalars_dfs +): # Session has bigquery connection initialized via context. Without an # explicit dataset the default dataset from the session would be used. # Without an explicit bigquery connection, the one present in Session set @@ -353,7 +323,7 @@ def test_remote_function_via_session_default(session_with_bq_connection, scalars # the default behavior of reuse=True will take effect. Please note that the # udf is same as the one used in other tests in this file so the underlying # cloud function would be common and quickly reused. - @session_with_bq_connection.remote_function([int], int) + @session_with_bq_connection_and_permanent_dataset.remote_function([int], int) def square(x): return x * x @@ -421,87 +391,15 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_via_session_context_connection_setter( - scalars_dfs, dataset_id, bq_cf_connection +def test_dataframe_applymap( + session_with_bq_connection_and_permanent_dataset, scalars_dfs ): - # Creating a session scoped only to this test as we would be setting a - # property in it - context = bigframes.BigQueryOptions() - context.bq_connection = bq_cf_connection - session = bigframes.connect(context) - - # Without an explicit bigquery connection, the one present in Session, - # set via context setter would be used. Without an explicit `reuse` the - # default behavior of reuse=True will take effect. Please note that the - # udf is same as the one used in other tests in this file so the underlying - # cloud function would be common with reuse=True. Since we are using a - # unique dataset_id, even though the cloud function would be reused, the bq - # remote function would still be created, making use of the bq connection - # set in the BigQueryOptions above. - @session.remote_function([int], int, dataset=dataset_id) - def square(x): - return x * x - - scalars_df, scalars_pandas_df = scalars_dfs - - bf_int64_col = scalars_df["int64_col"] - bf_int64_col_filter = bf_int64_col.notnull() - bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] - bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() - ) - - pd_int64_col = scalars_pandas_df["int64_col"] - pd_int64_col_filter = pd_int64_col.notnull() - pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] - pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) - # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. - # pd_int64_col_filtered.dtype is Int64Dtype() - # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. - # For this test let's force the pandas dtype to be same as bigframes' dtype. - pd_result_col = pd_result_col.astype(pd.Int64Dtype()) - pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) - - -@pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_default_connection(scalars_dfs, dataset_id): - @bpd.remote_function([int], int, dataset=dataset_id) - def square(x): - return x * x - - scalars_df, scalars_pandas_df = scalars_dfs - - bf_int64_col = scalars_df["int64_col"] - bf_int64_col_filter = bf_int64_col.notnull() - bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] - bf_result_col = bf_int64_col_filtered.apply(square) - bf_result = ( - bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() - ) - - pd_int64_col = scalars_pandas_df["int64_col"] - pd_int64_col_filter = pd_int64_col.notnull() - pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] - pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) - # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. - # pd_int64_col_filtered.dtype is Int64Dtype() - # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. - # For this test let's force the pandas dtype to be same as bigframes' dtype. - pd_result_col = pd_result_col.astype(pd.Int64Dtype()) - pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) - - -@pytest.mark.flaky(retries=2, delay=120) -def test_dataframe_applymap(session_with_bq_connection, scalars_dfs): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + remote_add_one = session_with_bq_connection_and_permanent_dataset.remote_function( + [int], int + )(add_one) scalars_df, scalars_pandas_df = scalars_dfs int64_cols = ["int64_col", "int64_too"] @@ -524,11 +422,15 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_dataframe_applymap_na_ignore(session_with_bq_connection, scalars_dfs): +def test_dataframe_applymap_na_ignore( + session_with_bq_connection_and_permanent_dataset, scalars_dfs +): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + remote_add_one = session_with_bq_connection_and_permanent_dataset.remote_function( + [int], int + )(add_one) scalars_df, scalars_pandas_df = scalars_dfs int64_cols = ["int64_col", "int64_too"] @@ -549,11 +451,13 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_series_map(session_with_bq_connection, scalars_dfs): +def test_series_map(session_with_bq_connection_and_permanent_dataset, scalars_dfs): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + remote_add_one = session_with_bq_connection_and_permanent_dataset.remote_function( + [int], int + )(add_one) scalars_df, scalars_pandas_df = scalars_dfs @@ -635,7 +539,7 @@ def square1(x): @pytest.mark.flaky(retries=2, delay=120) -def test_read_gbq_function_reads_udfs(bigquery_client, scalars_dfs, dataset_id): +def test_read_gbq_function_reads_udfs(bigquery_client, dataset_id): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) arg = bigquery.RoutineArgument( name="x", diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py new file mode 100644 index 00000000000..540f4020d36 --- /dev/null +++ b/tests/unit/test_remote_function.py @@ -0,0 +1,28 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ibis.backends.bigquery import datatypes as bq_types +from ibis.expr import datatypes as ibis_types + +from bigframes import remote_function as rf + + +def test_supported_types_correspond(): + # The same types should be representable by the supported Python and BigQuery types. + ibis_types_from_python = {ibis_types.dtype(t) for t in rf.SUPPORTED_IO_PYTHON_TYPES} + ibis_types_from_bigquery = { + bq_types.BigQueryType.to_ibis(tk) for tk in rf.SUPPORTED_IO_BIGQUERY_TYPEKINDS + } + + assert ibis_types_from_python == ibis_types_from_bigquery From 4e4409c5b235171f3770aec852193026519948fd Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 27 Oct 2023 19:22:28 -0700 Subject: [PATCH 120/921] feat: add dataframe melt (#116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/blocks.py | 41 +++++++++++++++++- bigframes/dataframe.py | 38 ++++++++++++++++ tests/system/small/test_dataframe.py | 43 +++++++++++++++++++ tests/system/small/test_multiindex.py | 28 ++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 28 ++++++++++++ 5 files changed, 176 insertions(+), 2 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index cc13edeaf96..635e7db8656 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1356,13 +1356,50 @@ def stack(self, how="left", levels: int = 1): index_columns = [*added_index_columns, *self.index_columns] index_labels = [*new_index_level_names, *self._index_labels] - block = Block( + return Block( unpivot_expr, index_columns=index_columns, column_labels=result_index, index_labels=index_labels, ) - return block + + def melt( + self, + id_vars=typing.Sequence[str], + value_vars=typing.Sequence[str], + var_names=typing.Sequence[typing.Hashable], + value_name: typing.Hashable = "value", + ): + # TODO: Implement col_level and ignore_index + unpivot_col_id = guid.generate_guid() + var_col_ids = tuple([guid.generate_guid() for _ in var_names]) + # single unpivot col + unpivot_col = (unpivot_col_id, tuple(value_vars)) + value_labels = [self.col_id_to_label[col_id] for col_id in value_vars] + id_labels = [self.col_id_to_label[col_id] for col_id in id_vars] + + dtype = self._expr.get_column_type(value_vars[0]) + + unpivot_expr = self._expr.unpivot( + row_labels=value_labels, + passthrough_columns=id_vars, + unpivot_columns=(unpivot_col,), + index_col_ids=var_col_ids, + dtype=dtype, + how="right", + ) + index_id = guid.generate_guid() + unpivot_expr = unpivot_expr.promote_offsets(index_id) + # Need to reorder to get id_vars before var_col and unpivot_col + unpivot_expr = unpivot_expr.select_columns( + [index_id, *id_vars, *var_col_ids, unpivot_col_id] + ) + + return Block( + unpivot_expr, + column_labels=[*id_labels, *var_names, value_name], + index_columns=[index_id], + ) def _create_stack_column( self, col_label: typing.Tuple, stack_labels: typing.Sequence[typing.Tuple] diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 01117d3e0ab..49d7ad991a2 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1673,6 +1673,44 @@ def idxmin(self) -> bigframes.series.Series: def idxmax(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmax(self._block)) + def melt( + self, + id_vars: typing.Optional[typing.Iterable[typing.Hashable]] = None, + value_vars: typing.Optional[typing.Iterable[typing.Hashable]] = None, + var_name: typing.Union[ + typing.Hashable, typing.Sequence[typing.Hashable] + ] = None, + value_name: typing.Hashable = "value", + ): + if var_name is None: + # Determine default var_name. Attempt to use column labels if they are unique + if self.columns.nlevels > 1: + if len(set(self.columns.names)) == len(self.columns.names): + var_name = self.columns.names + else: + var_name = [f"variable_{i}" for i in range(len(self.columns.names))] + else: + var_name = self.columns.name or "variable" + + var_name = tuple(var_name) if utils.is_list_like(var_name) else (var_name,) + + if id_vars is not None: + id_col_ids = [self._resolve_label_exact(col) for col in id_vars] + else: + id_col_ids = [] + if value_vars is not None: + val_col_ids = [self._resolve_label_exact(col) for col in value_vars] + else: + val_col_ids = [ + col_id + for col_id in self._block.value_columns + if col_id not in id_col_ids + ] + + return DataFrame( + self._block.melt(id_col_ids, val_col_ids, var_name, value_name) + ) + def describe(self) -> DataFrame: df_numeric = self._drop_non_numeric(keep_bool=False) if len(df_numeric.columns) == 0: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e459e3bee3b..b503f9a31d3 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1919,6 +1919,49 @@ def test_df_stack(scalars_dfs): pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) +def test_df_melt_default(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + # To match bigquery dataframes + scalars_pandas_df = scalars_pandas_df.copy() + scalars_pandas_df.columns = scalars_pandas_df.columns.astype("string[pyarrow]") + # Can only stack identically-typed columns + columns = ["int64_col", "int64_too", "rowindex_2"] + + bf_result = scalars_df[columns].melt().to_pandas() + pd_result = scalars_pandas_df[columns].melt() + + # Pandas produces int64 index, Bigframes produces Int64 (nullable) + pd.testing.assert_frame_equal( + bf_result, pd_result, check_index_type=False, check_dtype=False + ) + + +def test_df_melt_parameterized(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + # To match bigquery dataframes + scalars_pandas_df = scalars_pandas_df.copy() + scalars_pandas_df.columns = scalars_pandas_df.columns.astype("string[pyarrow]") + # Can only stack identically-typed columns + + bf_result = scalars_df.melt( + var_name="alice", + value_name="bob", + id_vars=["string_col"], + value_vars=["int64_col", "int64_too"], + ).to_pandas() + pd_result = scalars_pandas_df.melt( + var_name="alice", + value_name="bob", + id_vars=["string_col"], + value_vars=["int64_col", "int64_too"], + ) + + # Pandas produces int64 index, Bigframes produces Int64 (nullable) + pd.testing.assert_frame_equal( + bf_result, pd_result, check_index_type=False, check_dtype=False + ) + + def test_df_unstack(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs # To match bigquery dataframes diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index a87dacae044..d6bf46f77c8 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -752,6 +752,34 @@ def test_column_multi_index_stack(level): ) +def test_column_multi_index_melt(): + if pandas.__version__.startswith("1.") or pandas.__version__.startswith("2.0"): + pytest.skip("pandas <2.1 uses different stack implementation") + + level1 = pandas.Index(["b", "a", "b"]) + level2 = pandas.Index(["a", "b", "b"]) + level3 = pandas.Index(["b", "b", "a"]) + + multi_columns = pandas.MultiIndex.from_arrays( + [level1, level2, level3], names=["l1", "l2", "l3"] + ) + pd_df = pandas.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=[5, 2, None], + columns=multi_columns, + dtype="Int64", + ) + bf_df = bpd.DataFrame(pd_df) + + bf_result = bf_df.melt().to_pandas() + pd_result = pd_df.melt() + + # BigFrames uses different string and int types, but values are identical + pandas.testing.assert_frame_equal( + bf_result, pd_result, check_index_type=False, check_dtype=False + ) + + def test_column_multi_index_unstack(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "int64_col", "rowindex_2"] level1 = pandas.Index(["b", "a", "b"], dtype="string[pyarrow]") diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 13a81b46454..67836a8fd29 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2010,6 +2010,34 @@ def idxmax(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def melt(self, id_vars, value_vars, var_name, value_name): + """ + Unpivot a DataFrame from wide to long format, optionally leaving identifiers set. + + This function is useful to massage a DataFrame into a format where one + or more columns are identifier variables (`id_vars`), while all other + columns, considered measured variables (`value_vars`), are "unpivoted" to + the row axis, leaving just two non-identifier columns, 'variable' and + 'value'. + + Parameters + ---------- + id_vars (tuple, list, or ndarray, optional): + Column(s) to use as identifier variables. + value_vars (tuple, list, or ndarray, optional): + Column(s) to unpivot. If not specified, uses all columns that + are not set as `id_vars`. + var_name (scalar): + Name to use for the 'variable' column. If None it uses + ``frame.columns.name`` or 'variable'. + value_name (scalar, default 'value'): + Name to use for the 'value' column. + + Returns: + DataFrame: Unpivoted DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def nunique(self): """ Count number of distinct elements in specified axis. From ac44ccd3936cdb28755d2bbe16377d489f08d5e5 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:07:58 -0700 Subject: [PATCH 121/921] docs: add artithmetic df sample code (#153) * docs: add artithmetic df sample code * fix: address comments --- bigframes/session/__init__.py | 4 +- .../bigframes_vendored/pandas/core/frame.py | 494 +++++++++++++++++- 2 files changed, 492 insertions(+), 6 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 473de62f53a..932a41f2830 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -352,7 +352,7 @@ def read_gbq_query( >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None - Simple query input: + Simple query input: >>> df = bpd.read_gbq_query(''' ... SELECT @@ -368,7 +368,7 @@ def read_gbq_query( [2 rows x 3 columns] - Preserve ordering in a query input. + Preserve ordering in a query input. >>> df = bpd.read_gbq_query(''' ... SELECT diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 67836a8fd29..013d1701146 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -697,6 +697,7 @@ def align( Join method is specified for each axis Index. + Args: other (DataFrame or Series): join ({{'outer', 'inner', 'left', 'right'}}, default 'outer'): @@ -978,9 +979,9 @@ def sort_values( Sort ascending vs. descending. Specify list for multiple sort orders. If this is a list of bools, must match the length of the by. - kind (str, default `quicksort`): - Choice of sorting algorithm. Accepts 'quicksort’, ‘mergesort’, - ‘heapsort’, ‘stable’. Ignored except when determining whether to + kind (str, default 'quicksort'): + Choice of sorting algorithm. Accepts 'quicksort', 'mergesort', + 'heapsort', 'stable'. Ignored except when determining whether to sort stably. 'mergesort' or 'stable' will result in stable reorder. na_position ({'first', 'last'}, default `last`): ``{'first', 'last'}``, default 'last' Puts NaNs at the beginning @@ -1014,6 +1015,29 @@ def eq(self, other, axis: str | int = "columns") -> DataFrame: Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis (rows or columns) and level for comparison. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can use method name: + + >>> df = bpd.DataFrame({'angles': [0, 3, 4], + ... 'degrees': [360, 180, 360]}, + ... index=['circle', 'triangle', 'rectangle']) + >>> df["degrees"].eq(360) + circle True + triangle False + rectangle True + Name: degrees, dtype: boolean + + You can also use arithmetic operator ``==``: + >>> df["degrees"] == 360 + circle True + triangle False + rectangle True + Name: degrees, dtype: boolean + Args: other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. @@ -1036,6 +1060,30 @@ def ne(self, other, axis: str | int = "columns") -> DataFrame: Equivalent to `==`, `!=`, `<=`, `<`, `>=`, `>` with support to choose axis (rows or columns) and level for comparison. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can use method name: + + >>> df = bpd.DataFrame({'angles': [0, 3, 4], + ... 'degrees': [360, 180, 360]}, + ... index=['circle', 'triangle', 'rectangle']) + >>> df["degrees"].ne(360) + circle False + triangle True + rectangle False + Name: degrees, dtype: boolean + + You can also use arithmetic operator ``!=``: + + >>> df["degrees"] != 360 + circle False + triangle True + rectangle False + Name: degrees, dtype: boolean + Args: other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. @@ -1061,6 +1109,30 @@ def le(self, other, axis: str | int = "columns") -> DataFrame: floating point columns are considered different (i.e. `NaN` != `NaN`). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can use method name: + + >>> df = bpd.DataFrame({'angles': [0, 3, 4], + ... 'degrees': [360, 180, 360]}, + ... index=['circle', 'triangle', 'rectangle']) + >>> df["degrees"].le(180) + circle False + triangle True + rectangle False + Name: degrees, dtype: boolean + + You can also use arithmetic operator ``<=``: + + >>> df["degrees"] <= 180 + circle False + triangle True + rectangle False + Name: degrees, dtype: boolean + Args: other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. @@ -1087,6 +1159,30 @@ def lt(self, other, axis: str | int = "columns") -> DataFrame: floating point columns are considered different (i.e. `NaN` != `NaN`). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can use method name: + + >>> df = bpd.DataFrame({'angles': [0, 3, 4], + ... 'degrees': [360, 180, 360]}, + ... index=['circle', 'triangle', 'rectangle']) + >>> df["degrees"].lt(180) + circle False + triangle False + rectangle False + Name: degrees, dtype: boolean + + You can also use arithmetic operator ``<``: + + >>> df["degrees"] < 180 + circle False + triangle False + rectangle False + Name: degrees, dtype: boolean + Args: other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. @@ -1113,6 +1209,30 @@ def ge(self, other, axis: str | int = "columns") -> DataFrame: floating point columns are considered different (i.e. `NaN` != `NaN`). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can use method name: + + >>> df = bpd.DataFrame({'angles': [0, 3, 4], + ... 'degrees': [360, 180, 360]}, + ... index=['circle', 'triangle', 'rectangle']) + >>> df["degrees"].ge(360) + circle True + triangle False + rectangle True + Name: degrees, dtype: boolean + + You can also use arithmetic operator ``>=``: + + >>> df["degrees"] >= 360 + circle True + triangle False + rectangle True + Name: degrees, dtype: boolean + Args: other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. @@ -1139,6 +1259,28 @@ def gt(self, other, axis: str | int = "columns") -> DataFrame: floating point columns are considered different (i.e. `NaN` != `NaN`). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'angles': [0, 3, 4], + ... 'degrees': [360, 180, 360]}, + ... index=['circle', 'triangle', 'rectangle']) + >>> df["degrees"].gt(360) + circle False + triangle False + rectangle False + Name: degrees, dtype: boolean + + You can also use arithmetic operator ``>``: + + >>> df["degrees"] > 360 + circle False + triangle False + rectangle False + Name: degrees, dtype: boolean + Args: other (scalar, sequence, Series, or DataFrame): Any single or multiple element data structure, or list-like object. @@ -1162,6 +1304,32 @@ def add(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].add(df['B']) + 0 5 + 1 7 + 2 9 + dtype: Int64 + + You can also use arithmetic operator ``+``: + + >>> df['A'] + (df['B']) + 0 5 + 1 7 + 2 9 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1185,6 +1353,32 @@ def sub(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].sub(df['B']) + 0 -3 + 1 -3 + 2 -3 + dtype: Int64 + + You can also use arithmetic operator ``-``: + + >>> df['A'] - (df['B']) + 0 -3 + 1 -3 + 2 -3 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1208,6 +1402,29 @@ def rsub(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> df['A'].rsub(df['B']) + 0 3 + 1 3 + 2 3 + dtype: Int64 + + It's equivalent to using arithmetic operator: ``-``: + + >>> df['B'] - (df['A']) + 0 3 + 1 3 + 2 3 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1231,6 +1448,32 @@ def mul(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].mul(df['B']) + 0 4 + 1 10 + 2 18 + dtype: Int64 + + You can also use arithmetic operator ``*``: + + >>> df['A'] * (df['B']) + 0 4 + 1 10 + 2 18 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1254,6 +1497,32 @@ def truediv(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].truediv(df['B']) + 0 0.25 + 1 0.4 + 2 0.5 + dtype: Float64 + + You can also use arithmetic operator ``/``: + + >>> df['A'] / (df['B']) + 0 0.25 + 1 0.4 + 2 0.5 + dtype: Float64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1277,6 +1546,29 @@ def rtruediv(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> df['A'].rtruediv(df['B']) + 0 4.0 + 1 2.5 + 2 2.0 + dtype: Float64 + + It's equivalent to using arithmetic operator: ``/``: + + >>> df['B'] / (df['A']) + 0 4.0 + 1 2.5 + 2 2.0 + dtype: Float64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1300,6 +1592,32 @@ def floordiv(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].floordiv(df['B']) + 0 0 + 1 0 + 2 0 + dtype: Int64 + + You can also use arithmetic operator ``//``: + + >>> df['A'] // (df['B']) + 0 0 + 1 0 + 2 0 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1323,6 +1641,29 @@ def rfloordiv(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> df['A'].rfloordiv(df['B']) + 0 4 + 1 2 + 2 2 + dtype: Int64 + + It's equivalent to using arithmetic operator: ``//``: + + >>> df['B'] // (df['A']) + 0 4 + 1 2 + 2 2 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1346,6 +1687,32 @@ def mod(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].mod(df['B']) + 0 1 + 1 2 + 2 3 + dtype: Int64 + + You can also use arithmetic operator ``%``: + + >>> df['A'] % (df['B']) + 0 1 + 1 2 + 2 3 + dtype: Int64 + Args: other: Any single or multiple element data structure, or list-like object. @@ -1369,6 +1736,29 @@ def rmod(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> df['A'].rmod(df['B']) + 0 0 + 1 1 + 2 0 + dtype: Int64 + + It's equivalent to using arithmetic operator: ``%``: + + >>> df['B'] % (df['A']) + 0 0 + 1 1 + 2 0 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1382,7 +1772,7 @@ def rmod(self, other, axis: str | int = "columns") -> DataFrame: raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def pow(self, other, axis: str | int = "columns") -> DataFrame: - """Get Exponential power of dataframe and other, element-wise (binary operator `pow`). + """Get Exponential power of dataframe and other, element-wise (binary operator `**`). Equivalent to ``dataframe ** other``, but with support to substitute a fill_value for missing data in one of the inputs. With reverse version, `rpow`. @@ -1393,6 +1783,32 @@ def pow(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].pow(df['B']) + 0 1 + 1 32 + 2 729 + dtype: Int64 + + You can also use arithmetic operator ``**``: + + >>> df['A'] ** (df['B']) + 0 1 + 1 32 + 2 729 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1417,6 +1833,29 @@ def rpow(self, other, axis: str | int = "columns") -> DataFrame: .. note:: Mismatched indices will be unioned together. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> df['A'].rpow(df['B']) + 0 4 + 1 25 + 2 216 + dtype: Int64 + + It's equivalent to using arithmetic operator: ``**``: + + >>> df['B'] ** (df['A']) + 0 4 + 1 25 + 2 216 + dtype: Int64 + Args: other (float, int, or Series): Any single or multiple element data structure, or list-like object. @@ -1438,6 +1877,21 @@ def combine( to element-wise combine columns. The row and column indexes of the resulting DataFrame will be the union of the two. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df1 = bpd.DataFrame({'A': [0, 0], 'B': [4, 4]}) + >>> df2 = bpd.DataFrame({'A': [1, 1], 'B': [3, 3]}) + >>> take_smaller = lambda s1, s2: s1 if s1.sum() < s2.sum() else s2 + >>> df1.combine(df2, take_smaller) + A B + 0 0 3 + 1 0 3 + + [2 rows x 2 columns] + Args: other (DataFrame): The DataFrame to merge column-wise. @@ -1468,6 +1922,20 @@ def combine_first(self, other) -> DataFrame: second.loc[index, col] are not missing values, upon calling first.combine_first(second). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df1 = bpd.DataFrame({'A': [None, 0], 'B': [None, 4]}) + >>> df2 = bpd.DataFrame({'A': [1, 1], 'B': [3, 3]}) + >>> df1.combine_first(df2) + A B + 0 1.0 3.0 + 1 0.0 4.0 + + [2 rows x 2 columns] + Args: other (DataFrame): Provided DataFrame to use to fill null values. @@ -1485,6 +1953,24 @@ def update( Aligns on indices. There is no return value. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': [1, 2, 3], + ... 'B': [400, 500, 600]}) + >>> new_df = bpd.DataFrame({'B': [4, 5, 6], + ... 'C': [7, 8, 9]}) + >>> df.update(new_df) + >>> df + A B + 0 1 4 + 1 2 5 + 2 3 6 + + [3 rows x 2 columns] + Args: other (DataFrame, or object coercible into a DataFrame): Should have at least one matching index/column label From 79a638eda80c482b640b523426ffd95c42747edc Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 30 Oct 2023 18:56:14 +0000 Subject: [PATCH 122/921] feat: Implement operator `@` for `DataFrame.dot` (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes b/297502513 🦕 --- bigframes/dataframe.py | 2 ++ tests/system/small/test_dataframe.py | 33 +++++++++++++++++++++++++++ tests/system/small/test_multiindex.py | 16 +++++++++++++ 3 files changed, 51 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 49d7ad991a2..3369fb4868e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2707,3 +2707,5 @@ def get_right_id(id): result = result[other.name].rename() return result + + __matmul__ = dot diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index b503f9a31d3..c96faa3526b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3264,6 +3264,23 @@ def test_df_dot( ) +def test_df_dot_operator( + matrix_2by3_df, matrix_2by3_pandas_df, matrix_3by4_df, matrix_3by4_pandas_df +): + bf_result = (matrix_2by3_df @ matrix_3by4_df).to_pandas() + pd_result = matrix_2by3_pandas_df @ matrix_3by4_pandas_df + + # Patch pandas dtypes for testing parity + # Pandas result is object instead of Int64 (nullable) dtype. + for name in pd_result.columns: + pd_result[name] = pd_result[name].astype(pd.Int64Dtype()) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + def test_df_dot_series( matrix_2by3_df, matrix_2by3_pandas_df, matrix_3by4_df, matrix_3by4_pandas_df ): @@ -3278,3 +3295,19 @@ def test_df_dot_series( bf_result, pd_result, ) + + +def test_df_dot_operator_series( + matrix_2by3_df, matrix_2by3_pandas_df, matrix_3by4_df, matrix_3by4_pandas_df +): + bf_result = (matrix_2by3_df @ matrix_3by4_df["x"]).to_pandas() + pd_result = matrix_2by3_pandas_df @ matrix_3by4_pandas_df["x"] + + # Patch pandas dtypes for testing parity + # Pandas result is object instead of Int64 (nullable) dtype. + pd_result = pd_result.astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index d6bf46f77c8..bc35f633fdb 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -998,6 +998,9 @@ def test_df_multi_index_dot_not_supported(): with pytest.raises(NotImplementedError, match="Multi-index input is not supported"): bf1.dot(bf2) + with pytest.raises(NotImplementedError, match="Multi-index input is not supported"): + bf1 @ bf2 + # right multi-index right_index = pandas.MultiIndex.from_tuples([("a", "aa"), ("a", "ab"), ("b", "bb")]) bf1 = bpd.DataFrame(left_matrix) @@ -1005,6 +1008,9 @@ def test_df_multi_index_dot_not_supported(): with pytest.raises(NotImplementedError, match="Multi-index input is not supported"): bf1.dot(bf2) + with pytest.raises(NotImplementedError, match="Multi-index input is not supported"): + bf1 @ bf2 + def test_column_multi_index_dot_not_supported(): left_matrix = [[1, 2, 3], [2, 5, 7]] @@ -1022,6 +1028,11 @@ def test_column_multi_index_dot_not_supported(): ): bf1.dot(bf2) + with pytest.raises( + NotImplementedError, match="Multi-level column input is not supported" + ): + bf1 @ bf2 + # right multi-columns bf1 = bpd.DataFrame(left_matrix) bf2 = bpd.DataFrame(right_matrix, columns=multi_level_columns) @@ -1029,3 +1040,8 @@ def test_column_multi_index_dot_not_supported(): NotImplementedError, match="Multi-level column input is not supported" ): bf1.dot(bf2) + + with pytest.raises( + NotImplementedError, match="Multi-level column input is not supported" + ): + bf1 @ bf2 From cfebfaa91f945f0024ef743d38acb0b2ec8c4079 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:54:17 -0700 Subject: [PATCH 123/921] test: add code snippets for loading data from BigQuery Job (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add code snippets for loading data from BigQuery Job * fix: address the comments * fix: fix the broken test * use BigQuery Client library to get the job_id * feat: Implement operator `@` for `DataFrame.dot` (#139) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes b/297502513 🦕 * fix: fix the comments --------- Co-authored-by: Shobhit Singh --- .../load_data_from_biquery_job_test.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 samples/snippets/load_data_from_biquery_job_test.py diff --git a/samples/snippets/load_data_from_biquery_job_test.py b/samples/snippets/load_data_from_biquery_job_test.py new file mode 100644 index 00000000000..5271574a492 --- /dev/null +++ b/samples/snippets/load_data_from_biquery_job_test.py @@ -0,0 +1,51 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_bigquery_dataframes_load_data_from_bigquery_job(): + from google.cloud import bigquery + + # Construct a BigQuery client object. + client = bigquery.Client(project="bigframes-dev", location="us") + + query = """ + SELECT * + FROM `bigquery-public-data.ml_datasets.penguins` + LIMIT 20 + """ + query_job = client.query(query) + JOB_ID = query_job.job_id + your_project_id = "bigframes-dev" + + # [START bigquery_dataframes_load_data_from_bigquery_job] + from google.cloud import bigquery + + import bigframes.pandas as bpd + + # Project ID inserted based on the query results selected to explore + project = your_project_id + # Location inserted based on the query results selected to explore + location = "us" + client = bigquery.Client(project=project, location=location) + + # Job ID inserted based on the query results selcted to explore + job_id = JOB_ID + job = client.get_job(job_id) + destination = str(job.destination) + + # Load data from a BigQuery table using BigFrames DataFrames: + bq_df = bpd.read_gbq_table(destination) + + # [END bigquery_dataframes_load_data_from_bigquery_job] + assert bq_df is not None From 63c7919e28d2e0b864142320b47374d807f07c03 Mon Sep 17 00:00:00 2001 From: Bradford Orr <15842009+orrbradford@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:46:47 -0700 Subject: [PATCH 124/921] feat: add bigframes.options.compute.maximum_bytes_billed option that sets maximum bytes billed on query jobs (#133) -implement context manager for global options -maximum_bytes_billed only applies to query jobs. This limitation will be set per query. Operations that trigger multiple jobs may result in total usage beyond this setting --- bigframes/__init__.py | 3 +- bigframes/_config/__init__.py | 11 +++++ bigframes/_config/compute_options.py | 35 +++++++++++++++ bigframes/_config/display_options.py | 23 ++++------ bigframes/pandas/__init__.py | 4 ++ bigframes/session/__init__.py | 19 +++++--- docs/reference/bigframes/options.rst | 2 + docs/templates/toc.yml | 2 + tests/system/conftest.py | 7 --- tests/system/small/test_progress_bar.py | 17 ++++--- tests/unit/test_compute_options.py | 30 +++++++++++++ .../pandas/_config/config.py | 45 +++++++++++++++++++ 12 files changed, 162 insertions(+), 36 deletions(-) create mode 100644 bigframes/_config/compute_options.py create mode 100644 tests/unit/test_compute_options.py create mode 100644 third_party/bigframes_vendored/pandas/_config/config.py diff --git a/bigframes/__init__.py b/bigframes/__init__.py index 8f41790072b..bd1476957bf 100644 --- a/bigframes/__init__.py +++ b/bigframes/__init__.py @@ -14,7 +14,7 @@ """BigQuery DataFrames provides a DataFrame API scaled by the BigQuery engine.""" -from bigframes._config import options +from bigframes._config import option_context, options from bigframes._config.bigquery_options import BigQueryOptions from bigframes.core.global_session import close_session, get_global_session from bigframes.session import connect, Session @@ -28,4 +28,5 @@ "connect", "Session", "__version__", + "option_context", ] diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index e26eaf88009..8dcebfce6a2 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -18,8 +18,10 @@ """ import bigframes._config.bigquery_options as bigquery_options +import bigframes._config.compute_options as compute_options import bigframes._config.display_options as display_options import bigframes._config.sampling_options as sampling_options +import third_party.bigframes_vendored.pandas._config.config as pandas_config class Options: @@ -29,6 +31,7 @@ def __init__(self): self._bigquery_options = bigquery_options.BigQueryOptions() self._display_options = display_options.DisplayOptions() self._sampling_options = sampling_options.SamplingOptions() + self._compute_options = compute_options.ComputeOptions() @property def bigquery(self) -> bigquery_options.BigQueryOptions: @@ -49,6 +52,11 @@ def sampling(self) -> sampling_options.SamplingOptions: parameters in specific functions.""" return self._sampling_options + @property + def compute(self) -> compute_options.ComputeOptions: + """Options controlling object computation.""" + return self._compute_options + options = Options() """Global options for default session.""" @@ -58,3 +66,6 @@ def sampling(self) -> sampling_options.SamplingOptions: "Options", "options", ) + + +option_context = pandas_config.option_context diff --git a/bigframes/_config/compute_options.py b/bigframes/_config/compute_options.py new file mode 100644 index 00000000000..20c31d39066 --- /dev/null +++ b/bigframes/_config/compute_options.py @@ -0,0 +1,35 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Options for displaying objects.""" + +import dataclasses +from typing import Optional + + +@dataclasses.dataclass +class ComputeOptions: + """ + Encapsulates configuration for compute options. + + Attributes: + maximum_bytes_billed (int, Options): + Limits the bytes billed for query jobs. Queries that will have + 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 `_. + + """ + + maximum_bytes_billed: Optional[int] = None diff --git a/bigframes/_config/display_options.py b/bigframes/_config/display_options.py index 8bd2743f175..ad3ea3f68cc 100644 --- a/bigframes/_config/display_options.py +++ b/bigframes/_config/display_options.py @@ -40,17 +40,12 @@ def pandas_repr(display_options: DisplayOptions): This context manager makes sure we reset the pandas options when we're done so that we don't override pandas behavior. """ - original_max_cols = pd.options.display.max_columns - original_max_rows = pd.options.display.max_rows - original_show_dimensions = pd.options.display.show_dimensions - - pd.options.display.max_columns = display_options.max_columns - pd.options.display.max_rows = display_options.max_rows - pd.options.display.show_dimensions = True # type: ignore - - try: - yield - finally: - pd.options.display.max_columns = original_max_cols - pd.options.display.max_rows = original_max_rows - pd.options.display.show_dimensions = original_show_dimensions + with pd.option_context( + "display.max_columns", + display_options.max_columns, + "display.max_rows", + display_options.max_rows, + "display.show_dimensions", + True, + ) as pandas_context: + yield (pandas_context) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 8d9726312fd..0fab1109dc7 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -462,6 +462,9 @@ def read_gbq_function(function_name: str): options = config.options """Global :class:`~bigframes._config.Options` to configure BigQuery DataFrames.""" +option_context = config.option_context +"""Global :class:`~bigframes._config.option_context` to configure BigQuery DataFrames.""" + # Session management APIs get_global_session = global_session.get_global_session close_session = global_session.close_session @@ -494,6 +497,7 @@ def read_gbq_function(function_name: str): # Other public pandas attributes "NamedAgg", "options", + "option_context", # Session management APIs "get_global_session", "close_session", diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 932a41f2830..4858c7726a7 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1504,12 +1504,10 @@ def _start_query( max_results: Optional[int] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ - Starts query job and waits for results + Starts query job and waits for results. """ - if job_config is not None: - query_job = self.bqclient.query(sql, job_config=job_config) - else: - query_job = self.bqclient.query(sql) + job_config = self._prepare_job_config(job_config) + query_job = self.bqclient.query(sql, job_config=job_config) opts = bigframes.options.display if opts.progress_bar is not None and not query_job.configuration.dry_run: @@ -1538,6 +1536,17 @@ def _start_generic_job(self, job: formatting_helpers.GenericJob): else: job.result() + def _prepare_job_config( + self, job_config: Optional[bigquery.QueryJobConfig] = None + ) -> bigquery.QueryJobConfig: + if job_config is None: + job_config = self.bqclient.default_query_job_config + if bigframes.options.compute.maximum_bytes_billed is not None: + job_config.maximum_bytes_billed = ( + bigframes.options.compute.maximum_bytes_billed + ) + return job_config + def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: return Session(context) diff --git a/docs/reference/bigframes/options.rst b/docs/reference/bigframes/options.rst index d831a519fe6..991399eb886 100644 --- a/docs/reference/bigframes/options.rst +++ b/docs/reference/bigframes/options.rst @@ -12,3 +12,5 @@ Options and settings .. autoclass:: bigframes._config.display_options.DisplayOptions .. autoclass:: bigframes._config.sampling_options.SamplingOptions + +.. autoclass:: bigframes._config.compute_options.ComputeOptions diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 4fe2ec1a6ad..9879721d286 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -13,6 +13,8 @@ uid: bigframes._config.display_options.DisplayOptions - name: SamplingOptions uid: bigframes._config.sampling_options.SamplingOptions + - name: ComputeOptions + uid: bigframes._config.compute_options.ComputeOptions name: Options and settings - items: - name: Session diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 8885b03d34c..f9f69c6c8ee 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -898,13 +898,6 @@ def usa_names_grouped_table( return session.bqclient.get_table(table_id) -@pytest.fixture() -def deferred_repr(): - bigframes.options.display.repr_mode = "deferred" - yield - bigframes.options.display.repr_mode = "head" - - @pytest.fixture() def restore_sampling_settings(): enable_downsampling = bigframes.options.sampling.enable_downsampling diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index 084b723fba6..30ea63b483a 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -135,12 +135,11 @@ def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): assert string in query_job_repr -def test_query_job_dry_run( - penguins_df_default_index: bf.dataframe.DataFrame, capsys, deferred_repr -): - repr(penguins_df_default_index) - repr(penguins_df_default_index["body_mass_g"]) - lines = capsys.readouterr().out.split("\n") - lines = filter(None, lines) - for line in lines: - assert "Computation deferred. Computation will process" in line +def test_query_job_dry_run(penguins_df_default_index: bf.dataframe.DataFrame, capsys): + with bf.option_context("display.repr_mode", "deferred"): + repr(penguins_df_default_index) + repr(penguins_df_default_index["body_mass_g"]) + lines = capsys.readouterr().out.split("\n") + lines = filter(None, lines) + for line in lines: + assert "Computation deferred. Computation will process" in line diff --git a/tests/unit/test_compute_options.py b/tests/unit/test_compute_options.py new file mode 100644 index 00000000000..499a0a5fefa --- /dev/null +++ b/tests/unit/test_compute_options.py @@ -0,0 +1,30 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import bigframes as bf + +from . import resources + + +def test_maximum_bytes_option(): + session = resources.create_bigquery_session() + num_query_calls = 0 + with bf.option_context("compute.maximum_bytes_billed", 10000): + # clear initial method calls + session.bqclient.method_calls = [] + session._start_query("query") + for call in session.bqclient.method_calls: + _, _, kwargs = call + num_query_calls += 1 + assert kwargs["job_config"].maximum_bytes_billed == 10000 + assert num_query_calls > 0 diff --git a/third_party/bigframes_vendored/pandas/_config/config.py b/third_party/bigframes_vendored/pandas/_config/config.py new file mode 100644 index 00000000000..8abaca76c78 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/_config/config.py @@ -0,0 +1,45 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/_config/config.py +import contextlib +import operator + +import bigframes + + +class option_context(contextlib.ContextDecorator): + """ + Context manager to temporarily set options in the `with` statement context. + + You need to invoke as ``option_context(pat, val, [(pat, val), ...])``. + + Examples + -------- + >>> import bigframes + >>> with bigframes.option_context('display.max_rows', 10, 'display.max_columns', 5): + ... pass + """ + + def __init__(self, *args) -> None: + if len(args) % 2 != 0 or len(args) < 2: + raise ValueError( + "Need to invoke as option_context(pat, val, [(pat, val), ...])." + ) + + self.ops = list(zip(args[::2], args[1::2])) + + def __enter__(self) -> None: + self.undo = [ + (pat, operator.attrgetter(pat)(bigframes.options)) for pat, val in self.ops + ] + + for pat, val in self.ops: + self._set_option(pat, val) + + def __exit__(self, *args) -> None: + if self.undo: + for pat, val in self.undo: + self._set_option(pat, val) + + def _set_option(self, pat, val): + root, attr = pat.rsplit(".", 1) + parent = operator.attrgetter(root)(bigframes.options) + setattr(parent, attr, val) From 0801d96830dab467232277dea9fd2dacee41055c Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 31 Oct 2023 18:00:20 -0500 Subject: [PATCH 125/921] docs: fix indentation on `read_gbq_function` code sample (#163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/session/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 4858c7726a7..5a61ed534f3 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1467,13 +1467,13 @@ def read_gbq_function( **Examples:** - >>> import bigframes.pandas as bpd - >>> bpd.options.display.progress_bar = None + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None - >>> function_name = "bqutil.fn.cw_lower_case_ascii_only" - >>> func = bpd.read_gbq_function(function_name=function_name) - >>> func.bigframes_remote_function - 'bqutil.fn.cw_lower_case_ascii_only' + >>> function_name = "bqutil.fn.cw_lower_case_ascii_only" + >>> func = bpd.read_gbq_function(function_name=function_name) + >>> func.bigframes_remote_function + 'bqutil.fn.cw_lower_case_ascii_only' Args: function_name (str): From d8baad5b71ec67a35a0fb6132ee16e4c7418c456 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 1 Nov 2023 13:41:27 -0700 Subject: [PATCH 126/921] feat: add pd.get_dummies (#149) * feat: add pd.get_dummies * remove unneeded prefix case * param/documentation fixes * be stricter about types in test * be stricter about types in series test * remove unneeded comment * adjust for type difference in pandas 1 * add example code (tested) * fix None columns and add test cases * variable names and _get_unique_values per-column * account for pandas 1 behavior difference * remove already_seen set * avoid unnecessary join/projection * fix column ordering edge case * adjust for picky examples checker * example tweak * make part of the example comments * use ellipsis in doctest comment * add to doctest string * extract parameter standardization * extract submethods --------- Co-authored-by: Henry J Solberg --- bigframes/pandas/__init__.py | 177 ++++++++++++++++++ tests/system/small/test_pandas.py | 112 +++++++++++ .../pandas/core/reshape/concat.py | 2 +- .../pandas/core/reshape/encoding.py | 119 ++++++++++++ .../pandas/core/reshape/merge.py | 1 - .../pandas/core/reshape/tile.py | 2 +- 6 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 third_party/bigframes_vendored/pandas/core/reshape/encoding.py diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 0fab1109dc7..1c52b103fbe 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -45,14 +45,18 @@ ) import bigframes._config as config +import bigframes.constants as constants +import bigframes.core.blocks import bigframes.core.global_session as global_session import bigframes.core.indexes import bigframes.core.reshape import bigframes.dataframe +import bigframes.operations as ops import bigframes.series import bigframes.session import bigframes.session.clients import third_party.bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat +import third_party.bigframes_vendored.pandas.core.reshape.encoding as vendored_pandas_encoding import third_party.bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile @@ -134,6 +138,179 @@ def cut( cut.__doc__ = vendored_pandas_tile.cut.__doc__ +def get_dummies( + data: Union[DataFrame, Series], + prefix: Union[List, dict, str, None] = None, + prefix_sep: Union[List, dict, str, None] = "_", + dummy_na: bool = False, + columns: Optional[List] = None, + drop_first: bool = False, + dtype: Any = None, +) -> DataFrame: + # simplify input parameters into per-input-label lists + # also raise errors for invalid parameters + column_labels, prefixes, prefix_seps = _standardize_get_dummies_params( + data, prefix, prefix_sep, columns, dtype + ) + + # combine prefixes into per-column-id list + full_columns_prefixes, columns_ids = _determine_get_dummies_columns_from_labels( + data, column_labels, prefix is not None, prefixes, prefix_seps + ) + + # run queries to compute unique values + block = data._block + max_unique_value = ( + bigframes.core.blocks._BQ_MAX_COLUMNS + - len(block.value_columns) + - len(block.index_columns) + - 1 + ) // len(column_labels) + columns_values = [ + block._get_unique_values([col_id], max_unique_value) for col_id in columns_ids + ] + + # for each dummified column, add the content of the output columns via block operations + intermediate_col_ids = [] + for i in range(len(columns_values)): + level = columns_values[i].get_level_values(0).sort_values().dropna() + if drop_first: + level = level[1:] + column_label = full_columns_prefixes[i] + column_id = columns_ids[i] + block, new_intermediate_col_ids = _perform_get_dummies_block_operations( + block, level, column_label, column_id, dummy_na + ) + intermediate_col_ids.extend(new_intermediate_col_ids) + + # drop dummified columns (and the intermediate columns we added) + block = block.drop_columns(columns_ids + intermediate_col_ids) + return DataFrame(block) + + +get_dummies.__doc__ = vendored_pandas_encoding.get_dummies.__doc__ + + +def _standardize_get_dummies_params( + data: Union[DataFrame, Series], + prefix: Union[List, dict, str, None], + prefix_sep: Union[List, dict, str, None], + columns: Optional[List], + dtype: Any, +) -> Tuple[List, List[str], List[str]]: + block = data._block + + if isinstance(data, Series): + columns = [block.column_labels[0]] + if columns is not None and not pandas.api.types.is_list_like(columns): + raise TypeError("Input must be a list-like for parameter `columns`") + if dtype is not None and dtype not in [ + pandas.BooleanDtype, + bool, + "Boolean", + "boolean", + "bool", + ]: + raise NotImplementedError( + f"Only Boolean dtype is currently supported. {constants.FEEDBACK_LINK}" + ) + + if columns is None: + default_dummy_types = [pandas.StringDtype, "string[pyarrow]"] + columns = [] + columns_set = set() + for col_id in block.value_columns: + label = block.col_id_to_label[col_id] + if ( + label not in columns_set + and block.expr.get_column_type(col_id) in default_dummy_types + ): + columns.append(label) + columns_set.add(label) + + column_labels: List = typing.cast(List, columns) + + def parse_prefix_kwarg(kwarg, kwarg_name) -> Optional[List[str]]: + if kwarg is None: + return None + if isinstance(kwarg, str): + return [kwarg] * len(column_labels) + if isinstance(kwarg, dict): + return [kwarg[column] for column in column_labels] + kwarg = typing.cast(List, kwarg) + if pandas.api.types.is_list_like(kwarg) and len(kwarg) != len(column_labels): + raise ValueError( + f"Length of '{kwarg_name}' ({len(kwarg)}) did not match " + f"the length of the columns being encoded ({len(column_labels)})." + ) + if pandas.api.types.is_list_like(kwarg): + return list(map(str, kwarg)) + raise TypeError(f"{kwarg_name} kwarg must be a string, list, or dictionary") + + prefix_seps = parse_prefix_kwarg(prefix_sep or "_", "prefix_sep") + prefix_seps = typing.cast(List, prefix_seps) + prefixes = parse_prefix_kwarg(prefix, "prefix") + if prefixes is None: + prefixes = column_labels + prefixes = typing.cast(List, prefixes) + + return column_labels, prefixes, prefix_seps + + +def _determine_get_dummies_columns_from_labels( + data: Union[DataFrame, Series], + column_labels: List, + prefix_given: bool, + prefixes: List[str], + prefix_seps: List[str], +) -> Tuple[List[str], List[str]]: + block = data._block + + columns_ids = [] + columns_prefixes = [] + for i in range(len(column_labels)): + label = column_labels[i] + empty_prefix = label is None or (isinstance(data, Series) and not prefix_given) + full_prefix = "" if empty_prefix else prefixes[i] + prefix_seps[i] + + for col_id in block.label_to_col_id[label]: + columns_ids.append(col_id) + columns_prefixes.append(full_prefix) + + return columns_prefixes, columns_ids + + +def _perform_get_dummies_block_operations( + block: bigframes.core.blocks.Block, + level: pandas.Index, + column_label: str, + column_id: str, + dummy_na: bool, +) -> Tuple[bigframes.core.blocks.Block, List[str]]: + intermediate_col_ids = [] + for value in level: + new_column_label = f"{column_label}{value}" + if column_label == "": + new_column_label = value + new_block, new_id = block.apply_unary_op( + column_id, ops.BinopPartialLeft(ops.eq_op, value) + ) + intermediate_col_ids.append(new_id) + block, _ = new_block.apply_unary_op( + new_id, + ops.BinopPartialRight(ops.fillna_op, False), + result_label=new_column_label, + ) + if dummy_na: + # dummy column name for na depends on the dtype + na_string = str(pandas.Index([None], dtype=level.dtype)[0]) + new_column_label = f"{column_label}{na_string}" + block, _ = block.apply_unary_op( + column_id, ops.isnull_op, result_label=new_column_label + ) + return block, intermediate_col_ids + + def qcut( x: bigframes.series.Series, q: int, diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index f8fa78587f9..0292ebd2069 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -45,6 +45,118 @@ def test_concat_series(scalars_dfs): pd.testing.assert_series_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("kwargs"), + [ + { + "prefix": ["prefix1", "prefix2"], + "prefix_sep": "_", + "dummy_na": None, + "columns": ["bool_col", "int64_col"], + "drop_first": False, + }, + { + "prefix": "prefix", + "prefix_sep": ["_", ","], + "dummy_na": False, + "columns": ["int64_too", "string_col"], + "drop_first": False, + }, + { + "prefix": None, + "prefix_sep": ".", + "dummy_na": True, + "columns": ["time_col", "float64_col"], + "drop_first": True, + }, + ], +) +def test_get_dummies_dataframe(scalars_dfs, kwargs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = bpd.get_dummies(scalars_df, **kwargs, dtype=bool) + pd_result = pd.get_dummies(scalars_pandas_df, **kwargs, dtype=bool) + # dtype argument above is needed for pandas v1 only + + # adjust for expected dtype differences + for (column_name, type_name) in zip(pd_result.columns, pd_result.dtypes): + if type_name == "bool": + pd_result[column_name] = pd_result[column_name].astype("boolean") + + pd.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) + + +def test_get_dummies_dataframe_duplicate_labels(scalars_dfs): + if pd.__version__.startswith("1."): + pytest.skip("pandas has different behavior in 1.x") + + scalars_df, scalars_pandas_df = scalars_dfs + + scalars_renamed_df = scalars_df.rename( + columns={"int64_too": "int64_col", "float64_col": None, "string_col": None} + ) + scalars_renamed_pandas_df = scalars_pandas_df.rename( + columns={"int64_too": "int64_col", "float64_col": None, "string_col": None} + ) + + bf_result = bpd.get_dummies( + scalars_renamed_df, columns=["int64_col", None], dtype=bool + ) + pd_result = pd.get_dummies( + scalars_renamed_pandas_df, columns=["int64_col", None], dtype=bool + ) + # dtype argument above is needed for pandas v1 only + + # adjust for expected dtype differences + for (column_name, type_name) in zip(pd_result.columns, pd_result.dtypes): + if type_name == "bool": + pd_result[column_name] = pd_result[column_name].astype("boolean") + + pd.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) + + +def test_get_dummies_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series = scalars_df.date_col + pd_series = scalars_pandas_df.date_col + + bf_result = bpd.get_dummies(bf_series, dtype=bool) + pd_result = pd.get_dummies(pd_series, dtype=bool) + # dtype argument above is needed for pandas v1 only + + # adjust for expected dtype differences + for (column_name, type_name) in zip(pd_result.columns, pd_result.dtypes): + if type_name == "bool": + pd_result[column_name] = pd_result[column_name].astype("boolean") + pd_result.columns = pd_result.columns.astype(object) + + pd.testing.assert_frame_equal( + bf_result.to_pandas(), + pd_result, + ) + + +def test_get_dummies_series_nameless(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series = scalars_df.date_col.rename(None) + pd_series = scalars_pandas_df.date_col.rename(None) + + bf_result = bpd.get_dummies(bf_series, dtype=bool) + pd_result = pd.get_dummies(pd_series, dtype=bool) + # dtype argument above is needed for pandas v1 only + + # adjust for expected dtype differences + for (column_name, type_name) in zip(pd_result.columns, pd_result.dtypes): + if type_name == "bool": + pd_result[column_name] = pd_result[column_name].astype("boolean") + pd_result.columns = pd_result.columns.astype(object) + + pd.testing.assert_frame_equal( + bf_result.to_pandas(), + pd_result, + ) + + @pytest.mark.parametrize( ("how"), [ diff --git a/third_party/bigframes_vendored/pandas/core/reshape/concat.py b/third_party/bigframes_vendored/pandas/core/reshape/concat.py index 6e6d2d8b5c9..b0472c524ab 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/concat.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/concat.py @@ -1,6 +1,6 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/reshape/concat.py """ -Concat routines. +Concat routines """ from __future__ import annotations diff --git a/third_party/bigframes_vendored/pandas/core/reshape/encoding.py b/third_party/bigframes_vendored/pandas/core/reshape/encoding.py new file mode 100644 index 00000000000..da92b58f505 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/reshape/encoding.py @@ -0,0 +1,119 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/reshape/encoding.py +""" +Encoding routines +""" +from __future__ import annotations + +from bigframes import constants + + +def get_dummies( + data, + prefix=None, + prefix_sep="_", + dummy_na=False, + columns=None, + drop_first=False, + dtype=None, +): + """ + Convert categorical variable into dummy/indicator variables. + + Each variable is converted in as many 0/1 variables as there are + different values. Columns in the output are each named after a value; + if the input is a DataFrame, the name of the original variable is + prepended to the value. + + **Examples:** + >>> import bigframes.pandas as pd + >>> pd.options.display.progress_bar = None + >>> s = pd.Series(list('abca')) + >>> pd.get_dummies(s) + a b c + 0 True False False + 1 False True False + 2 False False True + 3 True False False + + [4 rows x 3 columns] + + >>> s1 = pd.Series(['a', 'b', None]) + >>> pd.get_dummies(s1) + a b + 0 True False + 1 False True + 2 False False + + [3 rows x 2 columns] + + >>> pd.get_dummies(s1, dummy_na=True) + a b + 0 True False False + 1 False True False + 2 False False True + + [3 rows x 3 columns] + + >>> df = pd.DataFrame({'A': ['a', 'b', 'a'], 'B': ['b', 'a', 'c'], 'C': [1, 2, 3]}) + >>> pd.get_dummies(df, prefix=['col1', 'col2']) + C col1_a col1_b col2_a col2_b col2_c + 0 1 True False False True False + 1 2 False True True False False + 2 3 True False False False True + + [3 rows x 6 columns] + + >>> pd.get_dummies(pd.Series(list('abcaa'))) + a b c + 0 True False False + 1 False True False + 2 False False True + 3 True False False + 4 True False False + + [5 rows x 3 columns] + + >>> pd.get_dummies(pd.Series(list('abcaa')), drop_first=True) + b c + 0 False False + 1 True False + 2 False True + 3 False False + 4 False False + + [5 rows x 2 columns] + + Args: + data (Series or DataFrame): + Data of which to get dummy indicators. + + prefix (str, list of str, or dict of str, default None): + String to append DataFrame column names. Pass a list with length + equal to the number of columns when calling get_dummies on a + DataFrame. Alternatively, prefix can be a dictionary mapping column + names to prefixes. + + prefix_sep (str, list of str, or dict of str, default '_'): + Separator/delimiter to use, appended to prefix. Or pass a list or + dictionary as with prefix. + + dummy_na (bool, default False): + Add a column to indicate NaNs, if False NaNs are ignored. + + columns (list-like, default None): + Column names in the DataFrame to be encoded. If columns is None + then only the columns with string dtype will be converted. + + drop_first (bool, default False): + Whether to get k-1 dummies out of k categorical levels by removing the + first level. + + dtype (dtype, default bool): + Data type for new columns. Only a single dtype is allowed. + + Returns: + DataFrame: Dummy-coded data. If data contains other columns than the + dummy-coded one(s), these will be prepended, unaltered, to the + result. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/merge.py b/third_party/bigframes_vendored/pandas/core/reshape/merge.py index cc81de405b9..b03f366fca7 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/merge.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/merge.py @@ -16,7 +16,6 @@ def merge( sort=False, suffixes=("_x", "_y"), ): - """ Merge DataFrame objects with a database-style join. diff --git a/third_party/bigframes_vendored/pandas/core/reshape/tile.py b/third_party/bigframes_vendored/pandas/core/reshape/tile.py index 24ea655a5fe..d4471ed68e4 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/tile.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/tile.py @@ -1,6 +1,6 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/reshape/tile.py """ -Quantilization functions and related stuff +Quantilization functions and related routines """ from __future__ import annotations From bf1ec89f8da2c7b2d042b7516a16a9e7cda6db06 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:49:24 -0700 Subject: [PATCH 127/921] chore(main): release 0.12.0 (#151) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ebadb56fb..845d3634bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.12.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.11.0...v0.12.0) (2023-11-01) + + +### Features + +* Add `DataFrame.melt` ([#113](https://github.com/googleapis/python-bigquery-dataframes/issues/113)) ([4e4409c](https://github.com/googleapis/python-bigquery-dataframes/commit/4e4409c5b235171f3770aec852193026519948fd)) +* Add `DataFrame.to_pandas_batches()` to download large `DataFrame` objects ([#136](https://github.com/googleapis/python-bigquery-dataframes/issues/136)) ([3afd4a3](https://github.com/googleapis/python-bigquery-dataframes/commit/3afd4a35f4c38dad86dab17ff62444cd418cab88)) +* Add bigframes.options.compute.maximum_bytes_billed option that sets maximum bytes billed on query jobs ([#133](https://github.com/googleapis/python-bigquery-dataframes/issues/133)) ([63c7919](https://github.com/googleapis/python-bigquery-dataframes/commit/63c7919e28d2e0b864142320b47374d807f07c03)) +* Add pandas.qcut ([#104](https://github.com/googleapis/python-bigquery-dataframes/issues/104)) ([8e44518](https://github.com/googleapis/python-bigquery-dataframes/commit/8e4451841ba09099b0ed5433f9102511741dfbed)) +* Add pd.get_dummies ([#149](https://github.com/googleapis/python-bigquery-dataframes/issues/149)) ([d8baad5](https://github.com/googleapis/python-bigquery-dataframes/commit/d8baad5b71ec67a35a0fb6132ee16e4c7418c456)) +* Add unstack to series, add level param ([#115](https://github.com/googleapis/python-bigquery-dataframes/issues/115)) ([5edcd19](https://github.com/googleapis/python-bigquery-dataframes/commit/5edcd19e6200db9b9ebe3d4945816b3ebf1f7bcd)) +* Implement operator `@` for `DataFrame.dot` ([#139](https://github.com/googleapis/python-bigquery-dataframes/issues/139)) ([79a638e](https://github.com/googleapis/python-bigquery-dataframes/commit/79a638eda80c482b640b523426ffd95c42747edc)) +* Populate ibis version in user agent ([#140](https://github.com/googleapis/python-bigquery-dataframes/issues/140)) ([c639a36](https://github.com/googleapis/python-bigquery-dataframes/commit/c639a3657465e2b68a3b93c363bd3ae1e969d2cc)) + + +### Bug Fixes + +* Don't override the global logging config ([#138](https://github.com/googleapis/python-bigquery-dataframes/issues/138)) ([2ddbf74](https://github.com/googleapis/python-bigquery-dataframes/commit/2ddbf743efc2fd8ffb61ae8d3333fc4b98ce4b55)) +* Fix bug with column names under repeated column assignment ([#150](https://github.com/googleapis/python-bigquery-dataframes/issues/150)) ([29032d0](https://github.com/googleapis/python-bigquery-dataframes/commit/29032d06811569121f7be2a7de915740df7daf6e)) +* Resolve plotly rendering issue by using ipython html for job pro… ([#134](https://github.com/googleapis/python-bigquery-dataframes/issues/134)) ([39df43e](https://github.com/googleapis/python-bigquery-dataframes/commit/39df43e243ac0374d1a1eb2a75779324825afbe9)) +* Use indexee's session for loc listlike cases ([#152](https://github.com/googleapis/python-bigquery-dataframes/issues/152)) ([27c5725](https://github.com/googleapis/python-bigquery-dataframes/commit/27c57255c7fe11e1ef9b9826d988d80fc17442a6)) + + +### Documentation + +* Add artithmetic df sample code ([#153](https://github.com/googleapis/python-bigquery-dataframes/issues/153)) ([ac44ccd](https://github.com/googleapis/python-bigquery-dataframes/commit/ac44ccd3936cdb28755d2bbe16377d489f08d5e5)) +* Fix indentation on `read_gbq_function` code sample ([#163](https://github.com/googleapis/python-bigquery-dataframes/issues/163)) ([0801d96](https://github.com/googleapis/python-bigquery-dataframes/commit/0801d96830dab467232277dea9fd2dacee41055c)) +* Link to ML.EVALUATE BQML page for score() methods ([#137](https://github.com/googleapis/python-bigquery-dataframes/issues/137)) ([45c617f](https://github.com/googleapis/python-bigquery-dataframes/commit/45c617fee7becc42f1c129246ffdc32f3a963f12)) + ## [0.11.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.10.0...v0.11.0) (2023-10-26) diff --git a/bigframes/version.py b/bigframes/version.py index 18edfa56150..b324ed72340 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.0" +__version__ = "0.12.0" From 3d7a0d6f7172b8b38ac1825990e612ce03645743 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:45:08 -0700 Subject: [PATCH 128/921] test: add code snippets for using bigframes.ml (#159) * test: add code snippets for using bigframes.ml --- samples/snippets/clustering_model_test.py | 35 ++++++++++++++ samples/snippets/gen_ai_model_test.py | 39 ++++++++++++++++ samples/snippets/regression_model_test.py | 57 +++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 samples/snippets/clustering_model_test.py create mode 100644 samples/snippets/gen_ai_model_test.py create mode 100644 samples/snippets/regression_model_test.py diff --git a/samples/snippets/clustering_model_test.py b/samples/snippets/clustering_model_test.py new file mode 100644 index 00000000000..a407fc78058 --- /dev/null +++ b/samples/snippets/clustering_model_test.py @@ -0,0 +1,35 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_clustering_model(): + # [START bigquery_dataframes_clustering_model] + from bigframes.ml.cluster import KMeans + import bigframes.pandas as bpd + + # Load data from BigQuery + query_or_table = "bigquery-public-data.ml_datasets.penguins" + bq_df = bpd.read_gbq(query_or_table) + + # Create the KMeans model + cluster_model = KMeans(n_clusters=10) + cluster_model.fit(bq_df["culmen_length_mm"], bq_df["sex"]) + + # Predict using the model + result = cluster_model.predict(bq_df) + # Score the model + score = cluster_model.score(bq_df) + # [END bigquery_dataframes_clustering_model] + assert result is not None + assert score is not None diff --git a/samples/snippets/gen_ai_model_test.py b/samples/snippets/gen_ai_model_test.py new file mode 100644 index 00000000000..7cbc90d4c03 --- /dev/null +++ b/samples/snippets/gen_ai_model_test.py @@ -0,0 +1,39 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_llm_model(): + PROJECT_ID = "bigframes-dev" + REGION = "us" + CONN_NAME = "bigframes-ml" + # [START bigquery_dataframes_gen_ai_model] + from bigframes.ml.llm import PaLM2TextGenerator + import bigframes.pandas as bpd + + # Create the LLM model + session = bpd.get_global_session() + connection = f"{PROJECT_ID}.{REGION}.{CONN_NAME}" + model = PaLM2TextGenerator(session=session, connection_name=connection) + + df_api = bpd.read_csv("gs://cloud-samples-data/vertex-ai/bigframe/df.csv") + + # Prepare the prompts and send them to the LLM model for prediction + df_prompt_prefix = "Generate Pandas sample code for DataFrame." + df_prompt = df_prompt_prefix + df_api["API"] + + # Predict using the model + df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024) + # [END bigquery_dataframes_gen_ai_model] + assert df_pred["ml_generate_text_llm_result"] is not None + assert df_pred["ml_generate_text_llm_result"].iloc[0] is not None diff --git a/samples/snippets/regression_model_test.py b/samples/snippets/regression_model_test.py new file mode 100644 index 00000000000..7d1bde689cb --- /dev/null +++ b/samples/snippets/regression_model_test.py @@ -0,0 +1,57 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_regression_model(): + # [START bigquery_dataframes_regression_model] + from bigframes.ml.linear_model import LinearRegression + import bigframes.pandas as bpd + + # Load data from BigQuery + query_or_table = "bigquery-public-data.ml_datasets.penguins" + bq_df = bpd.read_gbq(query_or_table) + + # Filter down to the data to the Adelie Penguin species + adelie_data = bq_df[bq_df.species == "Adelie Penguin (Pygoscelis adeliae)"] + + # Drop the species column + adelie_data = adelie_data.drop(columns=["species"]) + + # Drop rows with nulls to get training data + training_data = adelie_data.dropna() + + # Specify your feature (or input) columns and the label (or output) column: + feature_columns = training_data[ + ["island", "culmen_length_mm", "culmen_depth_mm", "flipper_length_mm", "sex"] + ] + label_columns = training_data[["body_mass_g"]] + + test_data = adelie_data[adelie_data.body_mass_g.isnull()] + + # Create the linear model + model = LinearRegression() + model.fit(feature_columns, label_columns) + + # Score the model + score = model.score(feature_columns, label_columns) + + # Predict using the model + result = model.predict(test_data) + # [END bigquery_dataframes_regression_model] + assert test_data is not None + assert feature_columns is not None + assert label_columns is not None + assert model is not None + assert score is not None + assert result is not None From b9cb55c5b9354f9ff60de0aad66fe60049876055 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 1 Nov 2023 17:44:12 -0700 Subject: [PATCH 129/921] feat: add interpolate() to series and dataframe (#157) --- bigframes/core/block_transforms.py | 91 +++++++++++++++++++ bigframes/dataframe.py | 4 + bigframes/series.py | 4 + tests/system/small/test_dataframe.py | 16 ++++ tests/system/small/test_series.py | 26 ++++++ .../bigframes_vendored/pandas/core/frame.py | 37 ++++++++ .../bigframes_vendored/pandas/core/series.py | 32 +++++++ 7 files changed, 210 insertions(+) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 3706bf1681b..917edac0ded 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -105,6 +105,97 @@ def indicate_duplicates( ) +def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: + if method != "linear": + raise NotImplementedError( + f"Only 'linear' interpolate method supported. {constants.FEEDBACK_LINK}" + ) + backwards_window = windows.WindowSpec(following=0) + forwards_window = windows.WindowSpec(preceding=0) + + output_column_ids = [] + + original_columns = block.value_columns + original_labels = block.column_labels + block, offsets = block.promote_offsets() + for column in original_columns: + # null in same places column is null + should_interpolate = block._column_type(column) in [ + pd.Float64Dtype(), + pd.Int64Dtype(), + ] + if should_interpolate: + block, notnull = block.apply_unary_op(column, ops.notnull_op) + block, masked_offsets = block.apply_binary_op( + offsets, notnull, ops.partial_arg3(ops.where_op, None) + ) + + block, previous_value = block.apply_window_op( + column, agg_ops.LastNonNullOp(), backwards_window + ) + block, next_value = block.apply_window_op( + column, agg_ops.FirstNonNullOp(), forwards_window + ) + block, previous_value_offset = block.apply_window_op( + masked_offsets, + agg_ops.LastNonNullOp(), + backwards_window, + skip_reproject_unsafe=True, + ) + block, next_value_offset = block.apply_window_op( + masked_offsets, + agg_ops.FirstNonNullOp(), + forwards_window, + skip_reproject_unsafe=True, + ) + + block, prediction_id = _interpolate( + block, + previous_value_offset, + previous_value, + next_value_offset, + next_value, + offsets, + ) + + block, interpolated_column = block.apply_binary_op( + column, prediction_id, ops.fillna_op + ) + # Pandas performs ffill-like behavior to extrapolate forwards + block, interpolated_and_ffilled = block.apply_binary_op( + interpolated_column, previous_value, ops.fillna_op + ) + + output_column_ids.append(interpolated_and_ffilled) + else: + output_column_ids.append(column) + + # Force reproject since used `skip_project_unsafe` perviously + block = block.select_columns(output_column_ids)._force_reproject() + return block.with_column_labels(original_labels) + + +def _interpolate( + block: blocks.Block, + x0_id: str, + y0_id: str, + x1_id: str, + y1_id: str, + xpredict_id: str, +) -> typing.Tuple[blocks.Block, str]: + """Applies linear interpolation equation to predict y values for xpredict.""" + block, x1x0diff = block.apply_binary_op(x1_id, x0_id, ops.sub_op) + block, y1y0diff = block.apply_binary_op(y1_id, y0_id, ops.sub_op) + block, xpredictx0diff = block.apply_binary_op(xpredict_id, x0_id, ops.sub_op) + + block, y1_weight = block.apply_binary_op(y1y0diff, x1x0diff, ops.div_op) + block, y1_part = block.apply_binary_op(xpredictx0diff, y1_weight, ops.mul_op) + + block, prediction_id = block.apply_binary_op(y0_id, y1_part, ops.add_op) + block = block.drop_columns([x1x0diff, y1y0diff, xpredictx0diff, y1_weight, y1_part]) + return block, prediction_id + + def drop_duplicates( block: blocks.Block, columns: typing.Sequence[str], keep: str = "first" ) -> blocks.Block: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 3369fb4868e..ffcaf0d613d 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1434,6 +1434,10 @@ def _reindex_columns(self, columns): def reindex_like(self, other: DataFrame, *, validate: typing.Optional[bool] = None): return self.reindex(index=other.index, columns=other.columns, validate=validate) + def interpolate(self, method: str = "linear") -> DataFrame: + result = block_ops.interpolate(self._block, method) + return DataFrame(result) + def fillna(self, value=None) -> DataFrame: return self._apply_binop(value, ops.fillna_op, how="left") diff --git a/bigframes/series.py b/bigframes/series.py index 37d00d16f32..824757cf52b 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -468,6 +468,10 @@ def replace( ) return Series(block.select_column(result_col)) + def interpolate(self, method: str = "linear") -> Series: + result = block_ops.interpolate(self._block, method) + return Series(result) + def dropna( self, *, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index c96faa3526b..2b710d692a4 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -711,6 +711,22 @@ def test_df_dropna(scalars_dfs, axis, how, ignore_index): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_df_interpolate(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + columns = ["int64_col", "int64_too", "float64_col"] + bf_result = scalars_df[columns].interpolate().to_pandas() + # Pandas can only interpolate on "float64" columns + # https://github.com/pandas-dev/pandas/issues/40252 + pd_result = scalars_pandas_df[columns].astype("float64").interpolate() + + pandas.testing.assert_frame_equal( + bf_result, + pd_result, + check_index_type=False, + check_dtype=False, + ) + + def test_df_fillna(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs df = scalars_df[["int64_col", "float64_col"]].fillna(3) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 05d8b841855..183ba01c0e0 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -272,6 +272,32 @@ def test_series_replace_list_scalar(scalars_dfs): ) +@pytest.mark.parametrize( + ("values",), + ( + ([None, 1, 2, None, None, 16, None],), + ([None, None, 3.6, None],), + ([403.2, None, 352.1, None, None, 111.9],), + ), +) +def test_series_interpolate(values): + pd_series = pd.Series(values) + bf_series = series.Series(pd_series) + + # Pandas can only interpolate on "float64" columns + # https://github.com/pandas-dev/pandas/issues/40252 + pd_result = pd_series.astype("float64").interpolate() + bf_result = bf_series.interpolate().to_pandas() + + # pd uses non-null types, while bf uses nullable types + pd.testing.assert_series_equal( + pd_result, + bf_result, + check_index_type=False, + check_dtype=False, + ) + + @pytest.mark.parametrize( ("ignore_index",), ( diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 013d1701146..12bd0531795 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2756,6 +2756,43 @@ def value_counts( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def interpolate(self, method: str = "linear"): + """ + Fill NaN values using an interpolation method. + + Args: + method (str, default 'linear'): + Interpolation technique to use. Only 'linear' supported. + 'linear': Ignore the index and treat the values as equally spaced. + This is the only method supported on MultiIndexes. + + Returns: + DataFrame: + Returns the same object type as the caller, interpolated at + some or all ``NaN`` values + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3, None, None, 6], + ... 'B': [None, 6, None, 2, None, 3], + ... }) + >>> df.interpolate() + A B + 0 1.0 + 1 2.0 6.0 + 2 3.0 4.0 + 3 4.0 2.0 + 4 5.0 2.5 + 5 6.0 3.0 + + [6 rows x 2 columns] + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def fillna(self, value): """ Fill NA/NaN values using the specified method. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index f0e13e16f5a..b569e5699c7 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -916,6 +916,38 @@ def droplevel(self, level, axis): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def interpolate(self, method: str = "linear"): + """ + Fill NaN values using an interpolation method. + + Args: + method (str, default 'linear'): + Interpolation technique to use. Only 'linear' supported. + 'linear': Ignore the index and treat the values as equally spaced. + This is the only method supported on MultiIndexes. + + Returns: + Series: + Returns the same object type as the caller, interpolated at + some or all ``NaN`` values + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> series = bpd.Series([1, 2, 3, None, None, 6]) + >>> series.interpolate() + 0 1.0 + 1 2.0 + 2 3.0 + 3 4.0 + 4 5.0 + 5 6.0 + dtype: Float64 + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def fillna( self, value=None, From e1817c9201ba4ea7fd2f8b6f4a667b010a6fec1b Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 2 Nov 2023 10:55:01 -0500 Subject: [PATCH 130/921] feat: `to_gbq` without a destination table writes to a temporary table (#158) * feat: `to_gbq` without a destination table writes to a temporary table * add unit test covering happy path for to_gbq * update to_gbq docs --- bigframes/constants.py | 4 ++ bigframes/dataframe.py | 47 +++++++++++---- bigframes/session/__init__.py | 11 ++++ bigframes/session/_io/bigquery.py | 20 +++++++ tests/unit/resources.py | 34 ++++++++++- tests/unit/session/test_io_bigquery.py | 26 +++++++- tests/unit/test_dataframe.py | 59 +++++++++++++++++++ tests/unit/test_pandas.py | 4 +- .../bigframes_vendored/pandas/core/frame.py | 40 +++++++++++-- 9 files changed, 225 insertions(+), 20 deletions(-) create mode 100644 tests/unit/test_dataframe.py diff --git a/bigframes/constants.py b/bigframes/constants.py index 90837c79eba..82b48dc9675 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime + """Constants used across BigQuery DataFrames. This module should not depend on any others in the package. @@ -23,3 +25,5 @@ ) ABSTRACT_METHOD_ERROR_MESSAGE = f"Abstract method. You have likely encountered a bug. Please share this stacktrace and how you reached it with the BigQuery DataFrames team. {FEEDBACK_LINK}" + +DEFAULT_EXPIRATION = datetime.timedelta(days=1) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ffcaf0d613d..4932008f093 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2289,25 +2289,51 @@ def to_json( def to_gbq( self, - destination_table: str, + destination_table: Optional[str] = None, *, - if_exists: Optional[Literal["fail", "replace", "append"]] = "fail", + if_exists: Optional[Literal["fail", "replace", "append"]] = None, index: bool = True, ordering_id: Optional[str] = None, - ) -> None: - if "." not in destination_table: - raise ValueError( - "Invalid Table Name. Should be of the form 'datasetId.tableId' or " - "'projectId.datasetId.tableId'" - ) - + ) -> str: dispositions = { "fail": bigquery.WriteDisposition.WRITE_EMPTY, "replace": bigquery.WriteDisposition.WRITE_TRUNCATE, "append": bigquery.WriteDisposition.WRITE_APPEND, } + + if destination_table is None: + # TODO(swast): If there have been no modifications to the DataFrame + # since the last time it was written (cached), then return that. + # For `read_gbq` nodes, return the underlying table clone. + destination_table = bigframes.session._io.bigquery.create_temp_table( + self._session.bqclient, + self._session._anonymous_dataset, + # TODO(swast): allow custom expiration times, probably via session configuration. + constants.DEFAULT_EXPIRATION, + ) + + if if_exists is not None and if_exists != "replace": + raise ValueError( + f"Got invalid value {repr(if_exists)} for if_exists. " + "When no destination table is specified, a new table is always created. " + "None or 'replace' are the only valid options in this case." + ) + if_exists = "replace" + + if "." not in destination_table: + raise ValueError( + f"Got invalid value for destination_table {repr(destination_table)}. " + "Should be of the form 'datasetId.tableId' or 'projectId.datasetId.tableId'." + ) + + if if_exists is None: + if_exists = "fail" + if if_exists not in dispositions: - raise ValueError("'{0}' is not valid for if_exists".format(if_exists)) + raise ValueError( + f"Got invalid value {repr(if_exists)} for if_exists. " + f"Valid options include None or one of {dispositions.keys()}." + ) job_config = bigquery.QueryJobConfig( write_disposition=dispositions[if_exists], @@ -2318,6 +2344,7 @@ def to_gbq( ) self._run_io_query(index=index, ordering_id=ordering_id, job_config=job_config) + return destination_table def to_numpy( self, dtype=None, copy=False, na_value=None, **kwargs diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 5a61ed534f3..a1eae697158 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -223,6 +223,17 @@ def _create_and_bind_bq_session(self): query_job.result() # blocks until finished self._session_id = query_job.session_info.session_id + # The anonymous dataset is used by BigQuery to write query results and + # session tables. BigQuery DataFrames also writes temp tables directly + # to the dataset, no BigQuery Session required. Note: there is a + # different anonymous dataset per location. See: + # https://cloud.google.com/bigquery/docs/cached-results#how_cached_results_are_stored + query_destination = query_job.destination + self._anonymous_dataset = bigquery.DatasetReference( + query_destination.project, + query_destination.dataset_id, + ) + self.bqclient.default_query_job_config = bigquery.QueryJobConfig( connection_properties=[ bigquery.ConnectionProperty("session_id", self._session_id) diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index d47efbdddcf..d200a9a861d 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -18,10 +18,12 @@ import textwrap import types from typing import Dict, Iterable, Union +import uuid import google.cloud.bigquery as bigquery IO_ORDERING_ID = "bqdf_row_nums" +TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}" def create_export_csv_statement( @@ -90,6 +92,24 @@ def create_snapshot_sql( ) +def create_temp_table( + bqclient: bigquery.Client, + dataset: bigquery.DatasetReference, + expiration: datetime.timedelta, +) -> str: + """Create an empty table with an expiration in the desired dataset.""" + now = datetime.datetime.now(datetime.timezone.utc) + random_id = uuid.uuid4().hex + table_id = TEMP_TABLE_PREFIX.format( + date=now.strftime("%Y%m%d"), random_id=random_id + ) + table_ref = dataset.table(table_id) + destination = bigquery.Table(table_ref) + destination.expires = now + expiration + bqclient.create_table(destination) + return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" + + # BigQuery REST API returns types in Legacy SQL format # https://cloud.google.com/bigquery/docs/data-types but we use Standard SQL # names diff --git a/tests/unit/resources.py b/tests/unit/resources.py index f660d774f06..8fc8acd1759 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -19,17 +19,21 @@ import google.cloud.bigquery import ibis import pandas +import pytest import bigframes import bigframes.core as core import bigframes.core.ordering +import bigframes.dataframe import bigframes.session.clients """Utilities for creating test resources.""" def create_bigquery_session( - bqclient: Optional[google.cloud.bigquery.Client] = None, session_id: str = "abcxyz" + bqclient: Optional[mock.Mock] = None, + session_id: str = "abcxyz", + anonymous_dataset: Optional[google.cloud.bigquery.DatasetReference] = None, ) -> bigframes.Session: credentials = mock.create_autospec( google.auth.credentials.Credentials, instance=True @@ -39,6 +43,21 @@ def create_bigquery_session( bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" + if anonymous_dataset is None: + anonymous_dataset = google.cloud.bigquery.DatasetReference( + "test-project", + "test_dataset", + ) + + query_job = mock.create_autospec(google.cloud.bigquery.QueryJob) + type(query_job).destination = mock.PropertyMock( + return_value=anonymous_dataset.table("test_table"), + ) + type(query_job).session_info = google.cloud.bigquery.SessionInfo( + {"sessionInfo": {"sessionId": session_id}}, + ) + bqclient.query.return_value = query_job + clients_provider = mock.create_autospec(bigframes.session.clients.ClientsProvider) type(clients_provider).bqclient = mock.PropertyMock(return_value=bqclient) clients_provider._credentials = credentials @@ -51,6 +70,19 @@ def create_bigquery_session( return session +def create_dataframe( + monkeypatch: pytest.MonkeyPatch, session: Optional[bigframes.Session] = None +) -> bigframes.dataframe.DataFrame: + if session is None: + session = create_bigquery_session() + + # Since this may create a ReadLocalNode, the session we explicitly pass in + # might not actually be used. Mock out the global session, too. + monkeypatch.setattr(bigframes.core.global_session, "_global_session", session) + bigframes.options.bigquery._session_started = True + return bigframes.dataframe.DataFrame({}, session=session) + + def create_pandas_session(tables: Dict[str, pandas.DataFrame]) -> bigframes.Session: # TODO(tswast): Refactor to make helper available for all tests. Consider # providing a proper "local Session" for use by downstream developers. diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index d2255d5edf6..cb3003b1cc1 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -14,6 +14,7 @@ import datetime from typing import Iterable +import unittest.mock as mock import google.cloud.bigquery as bigquery import pytest @@ -37,7 +38,7 @@ def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql -def test_create_snapshot_sql_doesnt_timetravel_session_datasets(): +def test_create_snapshot_sql_doesnt_timetravel_session_tables(): table_ref = bigquery.TableReference.from_string("my-test-project._session.abcdefg") sql = bigframes.session._io.bigquery.create_snapshot_sql( @@ -51,6 +52,29 @@ def test_create_snapshot_sql_doesnt_timetravel_session_datasets(): assert "my-test-project" not in sql +def test_create_temp_table_default_expiration(): + """Make sure the created table has an expiration.""" + bqclient = mock.create_autospec(bigquery.Client) + dataset = bigquery.DatasetReference("test-project", "test_dataset") + now = datetime.datetime.now(datetime.timezone.utc) + expiration = datetime.timedelta(days=3) + expected_expires = now + expiration + + bigframes.session._io.bigquery.create_temp_table(bqclient, dataset, expiration) + + bqclient.create_table.assert_called_once() + call_args = bqclient.create_table.call_args + table = call_args.args[0] + assert table.project == "test-project" + assert table.dataset_id == "test_dataset" + assert table.table_id.startswith("bqdf") + assert ( + (expected_expires - datetime.timedelta(minutes=1)) + < table.expires + < (expected_expires + datetime.timedelta(minutes=1)) + ) + + @pytest.mark.parametrize( ("schema", "expected"), ( diff --git a/tests/unit/test_dataframe.py b/tests/unit/test_dataframe.py new file mode 100644 index 00000000000..17a82908893 --- /dev/null +++ b/tests/unit/test_dataframe.py @@ -0,0 +1,59 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import google.cloud.bigquery +import pytest + +from . import resources + + +def test_dataframe_to_gbq_invalid_destination(monkeypatch: pytest.MonkeyPatch): + dataframe = resources.create_dataframe(monkeypatch) + + with pytest.raises(ValueError, match="no_dataset_or_project"): + dataframe.to_gbq("no_dataset_or_project") + + +def test_dataframe_to_gbq_invalid_if_exists(monkeypatch: pytest.MonkeyPatch): + dataframe = resources.create_dataframe(monkeypatch) + + with pytest.raises(ValueError, match="notreallyanoption"): + # Even though the type is annotated with the literals we accept, users + # might not be using a type checker, especially not in an interactive + # notebook. + dataframe.to_gbq(if_exists="notreallyanoption") # type: ignore + + +def test_dataframe_to_gbq_invalid_if_exists_no_destination( + monkeypatch: pytest.MonkeyPatch, +): + dataframe = resources.create_dataframe(monkeypatch) + + with pytest.raises(ValueError, match="append"): + dataframe.to_gbq(if_exists="append") + + +def test_dataframe_to_gbq_writes_to_anonymous_dataset( + monkeypatch: pytest.MonkeyPatch, +): + anonymous_dataset_id = "my-anonymous-project.my_anonymous_dataset" + anonymous_dataset = google.cloud.bigquery.DatasetReference.from_string( + anonymous_dataset_id + ) + session = resources.create_bigquery_session(anonymous_dataset=anonymous_dataset) + dataframe = resources.create_dataframe(monkeypatch, session=session) + + destination = dataframe.to_gbq() + + assert destination.startswith(anonymous_dataset_id) diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 5d4f69c7c01..70c5441c685 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -116,7 +116,7 @@ def test_pandas_attribute(): assert bpd.ArrowDtype is pd.ArrowDtype -def test_close_session_after_bq_session_ended(monkeypatch): +def test_close_session_after_bq_session_ended(monkeypatch: pytest.MonkeyPatch): bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" session = resources.create_bigquery_session( @@ -141,7 +141,7 @@ def test_close_session_after_bq_session_ended(monkeypatch): google.api_core.exceptions.BadRequest, match="Session JUST_A_TEST has expired and is no longer available.", ): - bpd.read_gbq("SELECT 1") + bpd.read_gbq("SELECT 'ABC'") # Even though the query to stop the session raises an exception, we should # still be able to close it without raising an error to the user. diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 12bd0531795..e267fac0f73 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -125,12 +125,12 @@ def to_numpy(self, dtype=None, copy=False, na_value=None, **kwargs) -> np.ndarra def to_gbq( self, - destination_table: str, + destination_table: Optional[str], *, - if_exists: Optional[Literal["fail", "replace", "append"]] = "fail", + if_exists: Optional[Literal["fail", "replace", "append"]] = None, index: bool = True, ordering_id: Optional[str] = None, - ) -> None: + ) -> str: """Write a DataFrame to a BigQuery table. **Examples:** @@ -138,17 +138,40 @@ def to_gbq( >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None + Write a DataFrame to a BigQuery table. + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) >>> # destination_table = PROJECT_ID + "." + DATASET_ID + "." + TABLE_NAME >>> df.to_gbq("bigframes-dev.birds.test-numbers", if_exists="replace") + 'bigframes-dev.birds.test-numbers' + + Write a DataFrame to a temporary BigQuery table in the anonymous dataset. + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> destination = df.to_gbq(ordering_id="ordering_id") + >>> # The table created can be read outside of the current session. + >>> bpd.close_session() # For demonstration, only. + >>> bpd.read_gbq(destination, index_col="ordering_id") + col1 col2 + ordering_id + 0 1 3 + 1 2 4 + + [2 rows x 2 columns] Args: - destination_table (str): + destination_table (Optional[str]): Name of table to be written, in the form ``dataset.tablename`` or ``project.dataset.tablename``. - if_exists (str, default 'fail'): - Behavior when the destination table exists. Value can be one of: + If no ``destination_table`` is set, a new temporary table is + created in the BigQuery anonymous dataset. + + if_exists (Optional[str]): + Behavior when the destination table exists. When + ``destination_table`` is set, this defaults to ``'fail'``. When + ``destination_table`` is not set, this field is not applicable. + A new table is always created. Value can be one of: ``'fail'`` If table exists raise pandas_gbq.gbq.TableCreationError. @@ -163,6 +186,11 @@ def to_gbq( ordering_id (Optional[str], default None): If set, write the ordering of the DataFrame as a column in the result table with this name. + + Returns: + str: + The fully-qualified ID for the written table, in the form + ``project.dataset.tablename``. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 5f0ea37fffff792fc3fbed65e6ace846d8ef6a06 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:49:11 -0700 Subject: [PATCH 131/921] feat: support 32k text-generation and multilingual embedding models (#161) * feat: support 32k text-generation and embedding multilingual models --- bigframes/ml/llm.py | 47 ++++++++++++++++------ tests/system/small/ml/conftest.py | 18 +++++++++ tests/system/small/ml/test_llm.py | 65 +++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 12 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index d78f467537c..2e5a9a1e5e5 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -16,7 +16,7 @@ from __future__ import annotations -from typing import cast, Optional, Union +from typing import cast, Literal, Optional, Union import bigframes from bigframes import clients, constants @@ -25,9 +25,11 @@ import bigframes.pandas as bpd _REMOTE_TEXT_GENERATOR_MODEL_CODE = "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1" +_REMOTE_TEXT_GENERATOR_32K_MODEL_CODE = "text-bison-32k" _TEXT_GENERATE_RESULT_COLUMN = "ml_generate_text_llm_result" _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE = "CLOUD_AI_TEXT_EMBEDDING_MODEL_V1" +_REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_CODE = "textembedding-gecko-multilingual" _EMBED_TEXT_RESULT_COLUMN = "text_embedding" @@ -35,19 +37,25 @@ class PaLM2TextGenerator(base.Predictor): """PaLM2 text generator LLM model. Args: + model_name (str, Default to "text-bison"): + The model for natural language tasks. “text-bison” returns model fine-tuned to follow natural language instructions + and is suitable for a variety of language tasks. "text-bison-32k" supports up to 32k tokens per request. + Default to "text-bison". session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. connection_name (str or None): - connection to connect with remote service. str of the format ... + Connection to connect with remote service. str of the format ... if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach permission if the connection isn't fully setup. """ def __init__( self, + model_name: Literal["text-bison", "text-bison-32k"] = "text-bison", session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, ): + self.model_name = model_name self.session = session or bpd.get_global_session() self._bq_connection_manager = clients.BqConnectionManager( self.session.bqconnectionclient, self.session.resourcemanagerclient @@ -80,11 +88,14 @@ def _create_bqml_model(self): connection_id=connection_name_parts[2], iam_role="aiplatform.user", ) - - options = { - "remote_service_type": _REMOTE_TEXT_GENERATOR_MODEL_CODE, - } - + if self.model_name == "text-bison": + options = { + "remote_service_type": _REMOTE_TEXT_GENERATOR_MODEL_CODE, + } + else: + options = { + "endpoint": _REMOTE_TEXT_GENERATOR_32K_MODEL_CODE, + } return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options ) @@ -118,7 +129,7 @@ def predict( top_k (int, default 40): Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens - in the model’s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature). + in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature). For each token selection step, the top K tokens with the highest probabilities are sampled. Then tokens are further filtered based on topP with the final token selected using temperature sampling. Specify a lower value for less random responses and a higher value for more random responses. Default 40. Possible values [1, 40]. @@ -175,6 +186,10 @@ class PaLM2TextEmbeddingGenerator(base.Predictor): """PaLM2 text embedding generator LLM model. Args: + model_name (str, Default to "textembedding-gecko"): + The model for text embedding. “textembedding-gecko” returns model embeddings for text inputs. + "textembedding-gecko-multilingual" returns model embeddings for text inputs which support over 100 languages + Default to "textembedding-gecko". session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. connection_name (str or None): @@ -184,9 +199,13 @@ class PaLM2TextEmbeddingGenerator(base.Predictor): def __init__( self, + model_name: Literal[ + "textembedding-gecko", "textembedding-gecko-multilingual" + ] = "textembedding-gecko", session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, ): + self.model_name = model_name self.session = session or bpd.get_global_session() self._bq_connection_manager = clients.BqConnectionManager( self.session.bqconnectionclient, self.session.resourcemanagerclient @@ -219,10 +238,14 @@ def _create_bqml_model(self): connection_id=connection_name_parts[2], iam_role="aiplatform.user", ) - - options = { - "remote_service_type": _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE, - } + if self.model_name == "textembedding-gecko": + options = { + "remote_service_type": _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE, + } + else: + options = { + "endpoint": _REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_CODE, + } return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 1dd1c813b81..c11445b79a5 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -213,6 +213,13 @@ def palm2_text_generator_model(session, bq_connection) -> llm.PaLM2TextGenerator return llm.PaLM2TextGenerator(session=session, connection_name=bq_connection) +@pytest.fixture(scope="session") +def palm2_text_generator_32k_model(session, bq_connection) -> llm.PaLM2TextGenerator: + return llm.PaLM2TextGenerator( + model_name="text-bison-32k", session=session, connection_name=bq_connection + ) + + @pytest.fixture(scope="function") def ephemera_palm2_text_generator_model( session, bq_connection @@ -229,6 +236,17 @@ def palm2_embedding_generator_model( ) +@pytest.fixture(scope="session") +def palm2_embedding_generator_multilingual_model( + session, bq_connection +) -> llm.PaLM2TextEmbeddingGenerator: + return llm.PaLM2TextEmbeddingGenerator( + model_name="textembedding-gecko-multilingual", + session=session, + connection_name=bq_connection, + ) + + @pytest.fixture(scope="session") def time_series_bqml_arima_plus_model( session, time_series_arima_plus_model_name diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index b7257dde1be..79d3c40317d 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -26,6 +26,12 @@ def test_create_text_generator_model(palm2_text_generator_model): assert palm2_text_generator_model._bqml_model is not None +def test_create_text_generator_32k_model(palm2_text_generator_32k_model): + # Model creation doesn't return error + assert palm2_text_generator_32k_model is not None + assert palm2_text_generator_32k_model._bqml_model is not None + + @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_model_default_session(bq_connection, llm_text_pandas_df): import bigframes.pandas as bpd @@ -48,6 +54,30 @@ def test_create_text_generator_model_default_session(bq_connection, llm_text_pan assert all(series.str.len() > 20) +@pytest.mark.flaky(retries=2, delay=120) +def test_create_text_generator_32k_model_default_session( + bq_connection, llm_text_pandas_df +): + import bigframes.pandas as bpd + + bpd.close_session() + bpd.options.bigquery.bq_connection = bq_connection + bpd.options.bigquery.location = "us" + + model = llm.PaLM2TextGenerator(model_name="text-bison-32k") + assert model is not None + assert model._bqml_model is not None + assert model.connection_name.casefold() == "bigframes-dev.us.bigframes-rf-conn" + + llm_text_df = bpd.read_pandas(llm_text_pandas_df) + + df = model.predict(llm_text_df).to_pandas() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_model_default_connection(llm_text_pandas_df): from bigframes import _config @@ -127,6 +157,14 @@ def test_create_embedding_generator_model(palm2_embedding_generator_model): assert palm2_embedding_generator_model._bqml_model is not None +def test_create_embedding_generator_multilingual_model( + palm2_embedding_generator_multilingual_model, +): + # Model creation doesn't return error + assert palm2_embedding_generator_multilingual_model is not None + assert palm2_embedding_generator_multilingual_model._bqml_model is not None + + def test_create_text_embedding_generator_model_defaults(bq_connection): import bigframes.pandas as bpd @@ -139,6 +177,20 @@ def test_create_text_embedding_generator_model_defaults(bq_connection): assert model._bqml_model is not None +def test_create_text_embedding_generator_multilingual_model_defaults(bq_connection): + import bigframes.pandas as bpd + + bpd.close_session() + bpd.options.bigquery.bq_connection = bq_connection + bpd.options.bigquery.location = "us" + + model = llm.PaLM2TextEmbeddingGenerator( + model_name="textembedding-gecko-multilingual" + ) + assert model is not None + assert model._bqml_model is not None + + @pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_success( palm2_embedding_generator_model, llm_text_df @@ -152,6 +204,19 @@ def test_embedding_generator_predict_success( assert value.size == 768 +@pytest.mark.flaky(retries=2, delay=120) +def test_embedding_generator_multilingual_predict_success( + palm2_embedding_generator_multilingual_model, llm_text_df +): + df = palm2_embedding_generator_multilingual_model.predict(llm_text_df).to_pandas() + TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert "text_embedding" in df.columns + series = df["text_embedding"] + value = series[0] + assert isinstance(value, np.ndarray) + assert value.size == 768 + + @pytest.mark.flaky(retries=2, delay=120) def test_embedding_generator_predict_series_success( palm2_embedding_generator_model, llm_text_df From 6d1953b7b46b402a2ea52233141ed6fd338b0098 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 20:32:49 -0700 Subject: [PATCH 132/921] chore: update docfx minimum Python version (#167) * chore: update docfx minimum Python version Source-Link: https://github.com/googleapis/synthtool/commit/bc07fd415c39853b382bcf8315f8eeacdf334055 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:30470597773378105e239b59fce8eb27cc97375580d592699206d17d117143d0 * chore: remove restriction on noxfile.py --------- Co-authored-by: Owl Bot Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .github/workflows/docs.yml | 2 +- noxfile.py | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 7f291dbd5f9..ec696b558c3 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:4f9b3b106ad0beafc2c8a415e3f62c1a0cc23cabea115dbe841b848f581cfe99 -# created: 2023-10-18T20:26:37.410353675Z + digest: sha256:30470597773378105e239b59fce8eb27cc97375580d592699206d17d117143d0 +# created: 2023-11-03T00:57:07.335914631Z diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e97d89e484c..221806cedf5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.9" + python-version: "3.10" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/noxfile.py b/noxfile.py index d0bbda80fd4..34b055de445 100644 --- a/noxfile.py +++ b/noxfile.py @@ -451,9 +451,7 @@ def docs(session): ) -# docfx doesn't yet support Python 3.10. -# https://github.com/googleapis/sphinx-docfx-yaml/issues/305 -@nox.session(python="3.9") +@nox.session(python=DEFAULT_PYTHON_VERSION) def docfx(session): """Build the docfx yaml files for this library.""" From 031f253890f8a212309097554c3462980654201d Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 3 Nov 2023 13:20:36 -0500 Subject: [PATCH 133/921] fix: use table clone instead of system time for `read_gbq_table` (#109) * fix: use table clone instead of system time for `read_gbq_table` * accept expiration datetime instead of timedelta for easier testing * don't use table clone on _session tables * remove unnecessary assert * add docstrings --- bigframes/constants.py | 2 +- bigframes/dataframe.py | 4 +- bigframes/session/__init__.py | 59 +++++++---------- bigframes/session/_io/bigquery.py | 89 ++++++++++++++++++++------ tests/system/small/test_session.py | 3 - tests/unit/session/test_io_bigquery.py | 64 +++++++++++------- 6 files changed, 137 insertions(+), 84 deletions(-) diff --git a/bigframes/constants.py b/bigframes/constants.py index 82b48dc9675..a1ffd2b755c 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -26,4 +26,4 @@ ABSTRACT_METHOD_ERROR_MESSAGE = f"Abstract method. You have likely encountered a bug. Please share this stacktrace and how you reached it with the BigQuery DataFrames team. {FEEDBACK_LINK}" -DEFAULT_EXPIRATION = datetime.timedelta(days=1) +DEFAULT_EXPIRATION = datetime.timedelta(days=7) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 4932008f093..45dbcdc78da 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -16,6 +16,7 @@ from __future__ import annotations +import datetime import re import textwrap import typing @@ -2309,7 +2310,8 @@ def to_gbq( self._session.bqclient, self._session._anonymous_dataset, # TODO(swast): allow custom expiration times, probably via session configuration. - constants.DEFAULT_EXPIRATION, + datetime.datetime.now(datetime.timezone.utc) + + constants.DEFAULT_EXPIRATION, ) if if_exists is not None and if_exists != "replace": diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index a1eae697158..12ee91a13a8 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,6 +16,7 @@ from __future__ import annotations +import datetime import logging import os import re @@ -430,7 +431,9 @@ def _read_gbq_query( index_cols = list(index_col) destination, query_job = self._query_to_destination( - query, index_cols, api_name="read_gbq_query" + query, + index_cols, + api_name=api_name, ) # If there was no destination table, that means the query must have @@ -508,6 +511,12 @@ def _read_gbq_table_to_ibis_with_total_ordering( If we can get a total ordering from the table, such as via primary key column(s), then return those too so that ordering generation can be avoided. + + For tables that aren't already read-only, this creates Create a table + clone so that any changes to the underlying table don't affect the + DataFrame and break our assumptions, especially with regards to unique + index and ordering. See: + https://cloud.google.com/bigquery/docs/table-clones-create """ if table_ref.dataset_id.upper() == "_SESSION": # _SESSION tables aren't supported by the tables.get REST API. @@ -518,15 +527,24 @@ def _read_gbq_table_to_ibis_with_total_ordering( None, ) + now = datetime.datetime.now(datetime.timezone.utc) + destination = bigframes_io.create_table_clone( + table_ref, + self._anonymous_dataset, + # TODO(swast): Allow the default expiration to be configured. + now + constants.DEFAULT_EXPIRATION, + self, + api_name, + ) table_expression = self.ibis_client.table( - table_ref.table_id, - database=f"{table_ref.project}.{table_ref.dataset_id}", + destination.table_id, + database=f"{destination.project}.{destination.dataset_id}", ) # If there are primary keys defined, the query engine assumes these # columns are unique, even if the constraint is not enforced. We make # the same assumption and use these columns as the total ordering keys. - table = self.bqclient.get_table(table_ref) + table = self.bqclient.get_table(destination) # TODO(b/305264153): Use public properties to fetch primary keys once # added to google-cloud-bigquery. @@ -535,23 +553,7 @@ def _read_gbq_table_to_ibis_with_total_ordering( .get("primaryKey", {}) .get("columns") ) - - if not primary_keys: - return table_expression, None - else: - # Read from a snapshot since we won't have to copy the table data to create a total ordering. - job_config = bigquery.QueryJobConfig() - job_config.labels["bigframes-api"] = api_name - current_timestamp = list( - self.bqclient.query( - "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", - job_config=job_config, - ).result() - )[0][0] - table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table_ref, current_timestamp) - ) - return table_expression, primary_keys + return table_expression, primary_keys def _read_gbq_table( self, @@ -662,20 +664,7 @@ def _read_gbq_table( total_ordering_columns=frozenset(index_cols), ) - # We have a total ordering, so query via "time travel" so that - # the underlying data doesn't mutate. - if is_total_ordering: - # Get the timestamp from the job metadata rather than the query - # text so that the query for determining uniqueness of the ID - # columns can be cached. - current_timestamp = query_job.started - - # The job finished, so we should have a start time. - assert current_timestamp is not None - table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table_ref, current_timestamp) - ) - else: + if not is_total_ordering: # Make sure when we generate an ordering, the row_number() # coresponds to the index columns. table_expression = table_expression.order_by(index_cols) diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index d200a9a861d..fd3b1c59a7b 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -14,14 +14,21 @@ """Private module: Helpers for I/O operations.""" +from __future__ import annotations + import datetime import textwrap import types +import typing from typing import Dict, Iterable, Union import uuid import google.cloud.bigquery as bigquery +if typing.TYPE_CHECKING: + import bigframes.session + + IO_ORDERING_ID = "bqdf_row_nums" TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}" @@ -69,43 +76,83 @@ def create_export_data_statement( ) -def create_snapshot_sql( - table_ref: bigquery.TableReference, current_timestamp: datetime.datetime -) -> str: - """Query a table via 'time travel' for consistent reads.""" +def random_table(dataset: bigquery.DatasetReference) -> bigquery.TableReference: + """Generate a random table ID with BigQuery DataFrames prefix. + + Args: + dataset (google.cloud.bigquery.DatasetReference): + The dataset to make the table reference in. Usually the anonymous + dataset for the session. + + Returns: + google.cloud.bigquery.TableReference: + Fully qualified table ID of a table that doesn't exist. + """ + now = datetime.datetime.now(datetime.timezone.utc) + random_id = uuid.uuid4().hex + table_id = TEMP_TABLE_PREFIX.format( + date=now.strftime("%Y%m%d"), random_id=random_id + ) + return dataset.table(table_id) + - # If we have a _SESSION table, assume that it's already a copy. Nothing to do here. - if table_ref.dataset_id.upper() == "_SESSION": - return f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" +def table_ref_to_sql(table: bigquery.TableReference) -> str: + """Format a table reference as escaped SQL.""" + return f"`{table.project}`.`{table.dataset_id}`.`{table.table_id}`" + +def create_table_clone( + source: bigquery.TableReference, + dataset: bigquery.DatasetReference, + expiration: datetime.datetime, + session: bigframes.session.Session, + api_name: str, +) -> bigquery.TableReference: + """Create a table clone for consistent reads.""" # If we have an anonymous query results table, it can't be modified and # there isn't any BigQuery time travel. - if table_ref.dataset_id.startswith("_"): - return f"SELECT * FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}`" + if source.dataset_id.startswith("_"): + return source - return textwrap.dedent( + fully_qualified_source_id = table_ref_to_sql(source) + destination = random_table(dataset) + fully_qualified_destination_id = table_ref_to_sql(destination) + + # Include a label so that Dataplex Lineage can identify temporary + # tables that BigQuery DataFrames creates. Googlers: See internal issue + # 296779699. + ddl = textwrap.dedent( f""" - SELECT * - FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}` - FOR SYSTEM_TIME AS OF TIMESTAMP({repr(current_timestamp.isoformat())}) + CREATE OR REPLACE TABLE + {fully_qualified_destination_id} + CLONE {fully_qualified_source_id} + OPTIONS( + expiration_timestamp=TIMESTAMP "{expiration.isoformat()}", + labels=[ + ("source", "bigquery-dataframes-temp"), + ("bigframes-api", {repr(api_name)}) + ] + ) """ ) + job_config = bigquery.QueryJobConfig() + job_config.labels = { + "source": "bigquery-dataframes-temp", + "bigframes-api": api_name, + } + session._start_query(ddl, job_config=job_config) + return destination def create_temp_table( bqclient: bigquery.Client, dataset: bigquery.DatasetReference, - expiration: datetime.timedelta, + expiration: datetime.datetime, ) -> str: """Create an empty table with an expiration in the desired dataset.""" - now = datetime.datetime.now(datetime.timezone.utc) - random_id = uuid.uuid4().hex - table_id = TEMP_TABLE_PREFIX.format( - date=now.strftime("%Y%m%d"), random_id=random_id - ) - table_ref = dataset.table(table_id) + table_ref = random_table(dataset) destination = bigquery.Table(table_ref) - destination.expires = now + expiration + destination.expires = expiration bqclient.create_table(destination) return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index bf72e444eba..28486a1269b 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -252,9 +252,6 @@ def test_read_gbq_w_primary_keys_table( sorted_result = result.sort_values(primary_keys) pd.testing.assert_frame_equal(result, sorted_result) - # Verify that we're working from a snapshot rather than a copy of the table. - assert "FOR SYSTEM_TIME AS OF TIMESTAMP" in df.sql - @pytest.mark.parametrize( ("query_or_table", "max_results"), diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index cb3003b1cc1..7a8691232ba 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -19,46 +19,63 @@ import google.cloud.bigquery as bigquery import pytest +import bigframes.session import bigframes.session._io.bigquery -def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): - table_ref = bigquery.TableReference.from_string( +def test_create_table_clone_doesnt_clone_anonymous_datasets(): + session = mock.create_autospec(bigframes.session.Session) + source = bigquery.TableReference.from_string( "my-test-project._e8166e0cdb.anonbb92cd" ) - sql = bigframes.session._io.bigquery.create_snapshot_sql( - table_ref, datetime.datetime.now(datetime.timezone.utc) + destination = bigframes.session._io.bigquery.create_table_clone( + source, + bigquery.DatasetReference("other-project", "other_dataset"), + datetime.datetime(2023, 11, 2, 15, 43, 21, tzinfo=datetime.timezone.utc), + session, + "test_api", ) - # Anonymous query results tables don't support time travel. - assert "SYSTEM_TIME" not in sql + # Anonymous query results tables don't support CLONE + assert destination is source + session._start_query.assert_not_called() - # Need fully-qualified table name. - assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql - -def test_create_snapshot_sql_doesnt_timetravel_session_tables(): - table_ref = bigquery.TableReference.from_string("my-test-project._session.abcdefg") - - sql = bigframes.session._io.bigquery.create_snapshot_sql( - table_ref, datetime.datetime.now(datetime.timezone.utc) +def test_create_table_clone_sets_expiration(): + session = mock.create_autospec(bigframes.session.Session) + source = bigquery.TableReference.from_string( + "my-test-project.test_dataset.some_table" ) - # We aren't modifying _SESSION tables, so don't use time travel. - assert "SYSTEM_TIME" not in sql + expiration = datetime.datetime( + 2023, 11, 2, 15, 43, 21, tzinfo=datetime.timezone.utc + ) + bigframes.session._io.bigquery.create_table_clone( + source, + bigquery.DatasetReference("other-project", "other_dataset"), + expiration, + session, + "test_api", + ) - # Don't need the project ID for _SESSION tables. - assert "my-test-project" not in sql + session._start_query.assert_called_once() + call_args = session._start_query.call_args + query = call_args.args[0] + assert "CREATE OR REPLACE TABLE" in query + assert "CLONE" in query + assert f'expiration_timestamp=TIMESTAMP "{expiration.isoformat()}"' in query + assert '("source", "bigquery-dataframes-temp")' in query + assert call_args.kwargs["job_config"].labels["bigframes-api"] == "test_api" def test_create_temp_table_default_expiration(): """Make sure the created table has an expiration.""" bqclient = mock.create_autospec(bigquery.Client) dataset = bigquery.DatasetReference("test-project", "test_dataset") - now = datetime.datetime.now(datetime.timezone.utc) - expiration = datetime.timedelta(days=3) - expected_expires = now + expiration + expiration = datetime.datetime( + 2023, 11, 2, 13, 44, 55, 678901, datetime.timezone.utc + ) bigframes.session._io.bigquery.create_temp_table(bqclient, dataset, expiration) @@ -68,10 +85,11 @@ def test_create_temp_table_default_expiration(): assert table.project == "test-project" assert table.dataset_id == "test_dataset" assert table.table_id.startswith("bqdf") + # TODO(swast): Why isn't the expiration exactly what we set it to? assert ( - (expected_expires - datetime.timedelta(minutes=1)) + (expiration - datetime.timedelta(minutes=1)) < table.expires - < (expected_expires + datetime.timedelta(minutes=1)) + < (expiration + datetime.timedelta(minutes=1)) ) From c065071028c2f4ac80ee7f84dbeb1df385c2a512 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 3 Nov 2023 13:42:14 -0700 Subject: [PATCH 134/921] feat: add __iter__, iterrows, itertuples, keys methods (#164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 18 +++++ bigframes/series.py | 6 ++ tests/system/small/test_dataframe.py | 49 ++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 79 +++++++++++++++++++ .../bigframes_vendored/pandas/core/generic.py | 31 +++++++- 5 files changed, 182 insertions(+), 1 deletion(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 45dbcdc78da..40f12671aef 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -304,6 +304,9 @@ def __len__(self): rows, _ = self.shape return rows + def __iter__(self): + return iter(self.columns) + def astype( self, dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], @@ -1477,12 +1480,27 @@ def isin(self, values) -> DataFrame: f"isin(), you passed a [{type(values).__name__}]" ) + def keys(self) -> pandas.Index: + return self.columns + def items(self): column_ids = self._block.value_columns column_labels = self._block.column_labels for col_id, col_label in zip(column_ids, column_labels): yield col_label, bigframes.series.Series(self._block.select_column(col_id)) + def iterrows(self) -> Iterable[tuple[typing.Any, pandas.Series]]: + for df in self.to_pandas_batches(): + for item in df.iterrows(): + yield item + + def itertuples( + self, index: bool = True, name: typing.Optional[str] = "Pandas" + ) -> Iterable[tuple[typing.Any, ...]]: + for df in self.to_pandas_batches(): + for item in df.itertuples(index=index, name=name): + yield item + def dropna( self, *, diff --git a/bigframes/series.py b/bigframes/series.py index 824757cf52b..032bdf6c429 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -16,6 +16,7 @@ from __future__ import annotations +import itertools import numbers import textwrap import typing @@ -148,6 +149,11 @@ def _set_internal_query_job(self, query_job: bigquery.QueryJob): def __len__(self): return self.shape[0] + def __iter__(self) -> typing.Iterator: + return itertools.chain.from_iterable( + map(lambda x: x.index, self._block.to_pandas_batches()) + ) + def copy(self) -> Series: return Series(self._block) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 2b710d692a4..bd5930e5089 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -803,6 +803,55 @@ def test_apply_series_scalar_callable( pandas.testing.assert_series_equal(bf_result, pd_result) +def test_df_keys( + scalars_df_index, + scalars_pandas_df_index, +): + pandas.testing.assert_index_equal( + scalars_df_index.keys(), scalars_pandas_df_index.keys() + ) + + +def test_df_iter( + scalars_df_index, + scalars_pandas_df_index, +): + for bf_i, df_i in zip(scalars_df_index, scalars_pandas_df_index): + assert bf_i == df_i + + +def test_iterrows( + scalars_df_index, + scalars_pandas_df_index, +): + for (bf_index, bf_series), (pd_index, pd_series) in zip( + scalars_df_index.iterrows(), scalars_pandas_df_index.iterrows() + ): + assert bf_index == pd_index + pandas.testing.assert_series_equal(bf_series, pd_series) + + +@pytest.mark.parametrize( + ( + "index", + "name", + ), + [ + ( + True, + "my_df", + ), + (False, None), + ], +) +def test_itertuples(scalars_df_index, index, name): + # Numeric has slightly different representation as a result of conversions. + bf_tuples = scalars_df_index.itertuples(index, name) + pd_tuples = scalars_df_index.to_pandas().itertuples(index, name) + for bf_tuple, pd_tuple in zip(bf_tuples, pd_tuples): + assert bf_tuple == pd_tuple + + def test_df_isin_list(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs values = ["Hello, World!", 55555, 2.51, pd.NA, True] diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index e267fac0f73..6f4f6be35d6 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -975,6 +975,85 @@ def isin(self, values): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def keys(self): + """ + Get the 'info axis'. + + This is index for Series, columns for DataFrame. + + Returns: + Index: Info axis. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> df.keys() + Index(['A', 'B'], dtype='object') + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def iterrows(self): + """ + Iterate over DataFrame rows as (index, Series) pairs. + + Yields: + a tuple (index, data) where data contains row values as a Series + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> index, row = next(df.iterrows()) + >>> index + 0 + >>> row + A 1 + B 4 + Name: 0, dtype: object + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def itertuples(self, index: bool = True, name: str | None = "Pandas"): + """ + Iterate over DataFrame rows as namedtuples. + + Args: + index (bool, default True): + If True, return the index as the first element of the tuple. + name (str or None, default "Pandas"): + The name of the returned namedtuples or None to return regular + tuples. + + Returns: + iterator: + An object to iterate over namedtuples for each row in the + DataFrame with the first field possibly being the index and + following fields being the column values. + + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> next(df.itertuples(name="Pair")) + Pair(Index=0, A=1, B=4) + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def items(self): """ Iterate over (column name, Series) pairs. diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 27d2e845376..127efe6a3d6 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -1,7 +1,7 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/generic.py from __future__ import annotations -from typing import Literal, Optional +from typing import Iterator, Literal, Optional from bigframes import constants from third_party.bigframes_vendored.pandas.core import indexing @@ -35,6 +35,35 @@ def size(self) -> int: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __iter__(self) -> Iterator: + """ + Iterate over info axis. + + Returns + iterator: Info axis as iterator. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> for x in df: + ... print(x) + A + B + + >>> series = bpd.Series(["a", "b", "c"], index=[10, 20, 30]) + >>> for x in series: + ... print(x) + 10 + 20 + 30 + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ------------------------------------------------------------------------- # Unary Methods From dfcc2d3f6918785a3048681c61e0f5f6c99d9d95 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 3 Nov 2023 21:44:23 -0500 Subject: [PATCH 135/921] Revert "fix: use table clone instead of system time for `read_gbq_table` (#109)" (#171) This reverts commit 031f253890f8a212309097554c3462980654201d. --- bigframes/constants.py | 2 +- bigframes/dataframe.py | 4 +- bigframes/session/__init__.py | 59 ++++++++++------- bigframes/session/_io/bigquery.py | 89 ++++++-------------------- tests/system/small/test_session.py | 3 + tests/unit/session/test_io_bigquery.py | 64 +++++++----------- 6 files changed, 84 insertions(+), 137 deletions(-) diff --git a/bigframes/constants.py b/bigframes/constants.py index a1ffd2b755c..82b48dc9675 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -26,4 +26,4 @@ ABSTRACT_METHOD_ERROR_MESSAGE = f"Abstract method. You have likely encountered a bug. Please share this stacktrace and how you reached it with the BigQuery DataFrames team. {FEEDBACK_LINK}" -DEFAULT_EXPIRATION = datetime.timedelta(days=7) +DEFAULT_EXPIRATION = datetime.timedelta(days=1) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 40f12671aef..04a5456e265 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -16,7 +16,6 @@ from __future__ import annotations -import datetime import re import textwrap import typing @@ -2328,8 +2327,7 @@ def to_gbq( self._session.bqclient, self._session._anonymous_dataset, # TODO(swast): allow custom expiration times, probably via session configuration. - datetime.datetime.now(datetime.timezone.utc) - + constants.DEFAULT_EXPIRATION, + constants.DEFAULT_EXPIRATION, ) if if_exists is not None and if_exists != "replace": diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 12ee91a13a8..a1eae697158 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,7 +16,6 @@ from __future__ import annotations -import datetime import logging import os import re @@ -431,9 +430,7 @@ def _read_gbq_query( index_cols = list(index_col) destination, query_job = self._query_to_destination( - query, - index_cols, - api_name=api_name, + query, index_cols, api_name="read_gbq_query" ) # If there was no destination table, that means the query must have @@ -511,12 +508,6 @@ def _read_gbq_table_to_ibis_with_total_ordering( If we can get a total ordering from the table, such as via primary key column(s), then return those too so that ordering generation can be avoided. - - For tables that aren't already read-only, this creates Create a table - clone so that any changes to the underlying table don't affect the - DataFrame and break our assumptions, especially with regards to unique - index and ordering. See: - https://cloud.google.com/bigquery/docs/table-clones-create """ if table_ref.dataset_id.upper() == "_SESSION": # _SESSION tables aren't supported by the tables.get REST API. @@ -527,24 +518,15 @@ def _read_gbq_table_to_ibis_with_total_ordering( None, ) - now = datetime.datetime.now(datetime.timezone.utc) - destination = bigframes_io.create_table_clone( - table_ref, - self._anonymous_dataset, - # TODO(swast): Allow the default expiration to be configured. - now + constants.DEFAULT_EXPIRATION, - self, - api_name, - ) table_expression = self.ibis_client.table( - destination.table_id, - database=f"{destination.project}.{destination.dataset_id}", + table_ref.table_id, + database=f"{table_ref.project}.{table_ref.dataset_id}", ) # If there are primary keys defined, the query engine assumes these # columns are unique, even if the constraint is not enforced. We make # the same assumption and use these columns as the total ordering keys. - table = self.bqclient.get_table(destination) + table = self.bqclient.get_table(table_ref) # TODO(b/305264153): Use public properties to fetch primary keys once # added to google-cloud-bigquery. @@ -553,7 +535,23 @@ def _read_gbq_table_to_ibis_with_total_ordering( .get("primaryKey", {}) .get("columns") ) - return table_expression, primary_keys + + if not primary_keys: + return table_expression, None + else: + # Read from a snapshot since we won't have to copy the table data to create a total ordering. + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + current_timestamp = list( + self.bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + table_expression = self.ibis_client.sql( + bigframes_io.create_snapshot_sql(table_ref, current_timestamp) + ) + return table_expression, primary_keys def _read_gbq_table( self, @@ -664,7 +662,20 @@ def _read_gbq_table( total_ordering_columns=frozenset(index_cols), ) - if not is_total_ordering: + # We have a total ordering, so query via "time travel" so that + # the underlying data doesn't mutate. + if is_total_ordering: + # Get the timestamp from the job metadata rather than the query + # text so that the query for determining uniqueness of the ID + # columns can be cached. + current_timestamp = query_job.started + + # The job finished, so we should have a start time. + assert current_timestamp is not None + table_expression = self.ibis_client.sql( + bigframes_io.create_snapshot_sql(table_ref, current_timestamp) + ) + else: # Make sure when we generate an ordering, the row_number() # coresponds to the index columns. table_expression = table_expression.order_by(index_cols) diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index fd3b1c59a7b..d200a9a861d 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -14,21 +14,14 @@ """Private module: Helpers for I/O operations.""" -from __future__ import annotations - import datetime import textwrap import types -import typing from typing import Dict, Iterable, Union import uuid import google.cloud.bigquery as bigquery -if typing.TYPE_CHECKING: - import bigframes.session - - IO_ORDERING_ID = "bqdf_row_nums" TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}" @@ -76,83 +69,43 @@ def create_export_data_statement( ) -def random_table(dataset: bigquery.DatasetReference) -> bigquery.TableReference: - """Generate a random table ID with BigQuery DataFrames prefix. - - Args: - dataset (google.cloud.bigquery.DatasetReference): - The dataset to make the table reference in. Usually the anonymous - dataset for the session. - - Returns: - google.cloud.bigquery.TableReference: - Fully qualified table ID of a table that doesn't exist. - """ - now = datetime.datetime.now(datetime.timezone.utc) - random_id = uuid.uuid4().hex - table_id = TEMP_TABLE_PREFIX.format( - date=now.strftime("%Y%m%d"), random_id=random_id - ) - return dataset.table(table_id) - - -def table_ref_to_sql(table: bigquery.TableReference) -> str: - """Format a table reference as escaped SQL.""" - return f"`{table.project}`.`{table.dataset_id}`.`{table.table_id}`" +def create_snapshot_sql( + table_ref: bigquery.TableReference, current_timestamp: datetime.datetime +) -> str: + """Query a table via 'time travel' for consistent reads.""" + # If we have a _SESSION table, assume that it's already a copy. Nothing to do here. + if table_ref.dataset_id.upper() == "_SESSION": + return f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" -def create_table_clone( - source: bigquery.TableReference, - dataset: bigquery.DatasetReference, - expiration: datetime.datetime, - session: bigframes.session.Session, - api_name: str, -) -> bigquery.TableReference: - """Create a table clone for consistent reads.""" # If we have an anonymous query results table, it can't be modified and # there isn't any BigQuery time travel. - if source.dataset_id.startswith("_"): - return source - - fully_qualified_source_id = table_ref_to_sql(source) - destination = random_table(dataset) - fully_qualified_destination_id = table_ref_to_sql(destination) + if table_ref.dataset_id.startswith("_"): + return f"SELECT * FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}`" - # Include a label so that Dataplex Lineage can identify temporary - # tables that BigQuery DataFrames creates. Googlers: See internal issue - # 296779699. - ddl = textwrap.dedent( + return textwrap.dedent( f""" - CREATE OR REPLACE TABLE - {fully_qualified_destination_id} - CLONE {fully_qualified_source_id} - OPTIONS( - expiration_timestamp=TIMESTAMP "{expiration.isoformat()}", - labels=[ - ("source", "bigquery-dataframes-temp"), - ("bigframes-api", {repr(api_name)}) - ] - ) + SELECT * + FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}` + FOR SYSTEM_TIME AS OF TIMESTAMP({repr(current_timestamp.isoformat())}) """ ) - job_config = bigquery.QueryJobConfig() - job_config.labels = { - "source": "bigquery-dataframes-temp", - "bigframes-api": api_name, - } - session._start_query(ddl, job_config=job_config) - return destination def create_temp_table( bqclient: bigquery.Client, dataset: bigquery.DatasetReference, - expiration: datetime.datetime, + expiration: datetime.timedelta, ) -> str: """Create an empty table with an expiration in the desired dataset.""" - table_ref = random_table(dataset) + now = datetime.datetime.now(datetime.timezone.utc) + random_id = uuid.uuid4().hex + table_id = TEMP_TABLE_PREFIX.format( + date=now.strftime("%Y%m%d"), random_id=random_id + ) + table_ref = dataset.table(table_id) destination = bigquery.Table(table_ref) - destination.expires = expiration + destination.expires = now + expiration bqclient.create_table(destination) return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 28486a1269b..bf72e444eba 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -252,6 +252,9 @@ def test_read_gbq_w_primary_keys_table( sorted_result = result.sort_values(primary_keys) pd.testing.assert_frame_equal(result, sorted_result) + # Verify that we're working from a snapshot rather than a copy of the table. + assert "FOR SYSTEM_TIME AS OF TIMESTAMP" in df.sql + @pytest.mark.parametrize( ("query_or_table", "max_results"), diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 7a8691232ba..cb3003b1cc1 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -19,63 +19,46 @@ import google.cloud.bigquery as bigquery import pytest -import bigframes.session import bigframes.session._io.bigquery -def test_create_table_clone_doesnt_clone_anonymous_datasets(): - session = mock.create_autospec(bigframes.session.Session) - source = bigquery.TableReference.from_string( +def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): + table_ref = bigquery.TableReference.from_string( "my-test-project._e8166e0cdb.anonbb92cd" ) - destination = bigframes.session._io.bigquery.create_table_clone( - source, - bigquery.DatasetReference("other-project", "other_dataset"), - datetime.datetime(2023, 11, 2, 15, 43, 21, tzinfo=datetime.timezone.utc), - session, - "test_api", + sql = bigframes.session._io.bigquery.create_snapshot_sql( + table_ref, datetime.datetime.now(datetime.timezone.utc) ) - # Anonymous query results tables don't support CLONE - assert destination is source - session._start_query.assert_not_called() + # Anonymous query results tables don't support time travel. + assert "SYSTEM_TIME" not in sql + # Need fully-qualified table name. + assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql -def test_create_table_clone_sets_expiration(): - session = mock.create_autospec(bigframes.session.Session) - source = bigquery.TableReference.from_string( - "my-test-project.test_dataset.some_table" - ) - expiration = datetime.datetime( - 2023, 11, 2, 15, 43, 21, tzinfo=datetime.timezone.utc - ) - bigframes.session._io.bigquery.create_table_clone( - source, - bigquery.DatasetReference("other-project", "other_dataset"), - expiration, - session, - "test_api", +def test_create_snapshot_sql_doesnt_timetravel_session_tables(): + table_ref = bigquery.TableReference.from_string("my-test-project._session.abcdefg") + + sql = bigframes.session._io.bigquery.create_snapshot_sql( + table_ref, datetime.datetime.now(datetime.timezone.utc) ) - session._start_query.assert_called_once() - call_args = session._start_query.call_args - query = call_args.args[0] - assert "CREATE OR REPLACE TABLE" in query - assert "CLONE" in query - assert f'expiration_timestamp=TIMESTAMP "{expiration.isoformat()}"' in query - assert '("source", "bigquery-dataframes-temp")' in query - assert call_args.kwargs["job_config"].labels["bigframes-api"] == "test_api" + # We aren't modifying _SESSION tables, so don't use time travel. + assert "SYSTEM_TIME" not in sql + + # Don't need the project ID for _SESSION tables. + assert "my-test-project" not in sql def test_create_temp_table_default_expiration(): """Make sure the created table has an expiration.""" bqclient = mock.create_autospec(bigquery.Client) dataset = bigquery.DatasetReference("test-project", "test_dataset") - expiration = datetime.datetime( - 2023, 11, 2, 13, 44, 55, 678901, datetime.timezone.utc - ) + now = datetime.datetime.now(datetime.timezone.utc) + expiration = datetime.timedelta(days=3) + expected_expires = now + expiration bigframes.session._io.bigquery.create_temp_table(bqclient, dataset, expiration) @@ -85,11 +68,10 @@ def test_create_temp_table_default_expiration(): assert table.project == "test-project" assert table.dataset_id == "test_dataset" assert table.table_id.startswith("bqdf") - # TODO(swast): Why isn't the expiration exactly what we set it to? assert ( - (expiration - datetime.timedelta(minutes=1)) + (expected_expires - datetime.timedelta(minutes=1)) < table.expires - < (expiration + datetime.timedelta(minutes=1)) + < (expected_expires + datetime.timedelta(minutes=1)) ) From 4ff26cdf862e9f9b91a3a1d2abfa7fbdf0af9c5b Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 6 Nov 2023 12:35:06 -0600 Subject: [PATCH 136/921] fix: update default temp table expiration to 7 days (#174) --- bigframes/constants.py | 2 +- bigframes/dataframe.py | 4 ++- bigframes/session/__init__.py | 4 ++- bigframes/session/_io/bigquery.py | 36 ++++++++++++++++++++------ tests/unit/session/test_io_bigquery.py | 10 +++---- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/bigframes/constants.py b/bigframes/constants.py index 82b48dc9675..a1ffd2b755c 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -26,4 +26,4 @@ ABSTRACT_METHOD_ERROR_MESSAGE = f"Abstract method. You have likely encountered a bug. Please share this stacktrace and how you reached it with the BigQuery DataFrames team. {FEEDBACK_LINK}" -DEFAULT_EXPIRATION = datetime.timedelta(days=1) +DEFAULT_EXPIRATION = datetime.timedelta(days=7) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 04a5456e265..40f12671aef 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -16,6 +16,7 @@ from __future__ import annotations +import datetime import re import textwrap import typing @@ -2327,7 +2328,8 @@ def to_gbq( self._session.bqclient, self._session._anonymous_dataset, # TODO(swast): allow custom expiration times, probably via session configuration. - constants.DEFAULT_EXPIRATION, + datetime.datetime.now(datetime.timezone.utc) + + constants.DEFAULT_EXPIRATION, ) if if_exists is not None and if_exists != "replace": diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index a1eae697158..2537e81e19c 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -430,7 +430,9 @@ def _read_gbq_query( index_cols = list(index_col) destination, query_job = self._query_to_destination( - query, index_cols, api_name="read_gbq_query" + query, + index_cols, + api_name=api_name, ) # If there was no destination table, that means the query must have diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index d200a9a861d..06d240fec65 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -14,6 +14,8 @@ """Private module: Helpers for I/O operations.""" +from __future__ import annotations + import datetime import textwrap import types @@ -69,6 +71,29 @@ def create_export_data_statement( ) +def random_table(dataset: bigquery.DatasetReference) -> bigquery.TableReference: + """Generate a random table ID with BigQuery DataFrames prefix. + Args: + dataset (google.cloud.bigquery.DatasetReference): + The dataset to make the table reference in. Usually the anonymous + dataset for the session. + Returns: + google.cloud.bigquery.TableReference: + Fully qualified table ID of a table that doesn't exist. + """ + now = datetime.datetime.now(datetime.timezone.utc) + random_id = uuid.uuid4().hex + table_id = TEMP_TABLE_PREFIX.format( + date=now.strftime("%Y%m%d"), random_id=random_id + ) + return dataset.table(table_id) + + +def table_ref_to_sql(table: bigquery.TableReference) -> str: + """Format a table reference as escaped SQL.""" + return f"`{table.project}`.`{table.dataset_id}`.`{table.table_id}`" + + def create_snapshot_sql( table_ref: bigquery.TableReference, current_timestamp: datetime.datetime ) -> str: @@ -95,17 +120,12 @@ def create_snapshot_sql( def create_temp_table( bqclient: bigquery.Client, dataset: bigquery.DatasetReference, - expiration: datetime.timedelta, + expiration: datetime.datetime, ) -> str: """Create an empty table with an expiration in the desired dataset.""" - now = datetime.datetime.now(datetime.timezone.utc) - random_id = uuid.uuid4().hex - table_id = TEMP_TABLE_PREFIX.format( - date=now.strftime("%Y%m%d"), random_id=random_id - ) - table_ref = dataset.table(table_id) + table_ref = random_table(dataset) destination = bigquery.Table(table_ref) - destination.expires = now + expiration + destination.expires = expiration bqclient.create_table(destination) return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index cb3003b1cc1..03470208e42 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -56,9 +56,9 @@ def test_create_temp_table_default_expiration(): """Make sure the created table has an expiration.""" bqclient = mock.create_autospec(bigquery.Client) dataset = bigquery.DatasetReference("test-project", "test_dataset") - now = datetime.datetime.now(datetime.timezone.utc) - expiration = datetime.timedelta(days=3) - expected_expires = now + expiration + expiration = datetime.datetime( + 2023, 11, 2, 13, 44, 55, 678901, datetime.timezone.utc + ) bigframes.session._io.bigquery.create_temp_table(bqclient, dataset, expiration) @@ -69,9 +69,9 @@ def test_create_temp_table_default_expiration(): assert table.dataset_id == "test_dataset" assert table.table_id.startswith("bqdf") assert ( - (expected_expires - datetime.timedelta(minutes=1)) + (expiration - datetime.timedelta(minutes=1)) < table.expires - < (expected_expires + datetime.timedelta(minutes=1)) + < (expiration + datetime.timedelta(minutes=1)) ) From 8b6b1c610750e2d75d465a36f829af0b8c835b19 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:56:16 +0000 Subject: [PATCH 137/921] chore(main): release 0.13.0 (#165) :robot: I have created a release *beep* *boop* --- ## [0.13.0](https://togithub.com/googleapis/python-bigquery-dataframes/compare/v0.12.0...v0.13.0) (2023-11-07) ### Features * `to_gbq` without a destination table writes to a temporary table ([#158](https://togithub.com/googleapis/python-bigquery-dataframes/issues/158)) ([e1817c9](https://togithub.com/googleapis/python-bigquery-dataframes/commit/e1817c9201ba4ea7fd2f8b6f4a667b010a6fec1b)) * Add `DataFrame.__iter__`, `DataFrame.iterrows`, `DataFrame.itertuples`, and `DataFrame.keys` methods ([#164](https://togithub.com/googleapis/python-bigquery-dataframes/issues/164)) ([c065071](https://togithub.com/googleapis/python-bigquery-dataframes/commit/c065071028c2f4ac80ee7f84dbeb1df385c2a512)) * Add `Series.__iter__` method ([#164](https://togithub.com/googleapis/python-bigquery-dataframes/issues/164)) ([c065071](https://togithub.com/googleapis/python-bigquery-dataframes/commit/c065071028c2f4ac80ee7f84dbeb1df385c2a512)) * Add interpolate() to series and dataframe ([#157](https://togithub.com/googleapis/python-bigquery-dataframes/issues/157)) ([b9cb55c](https://togithub.com/googleapis/python-bigquery-dataframes/commit/b9cb55c5b9354f9ff60de0aad66fe60049876055)) * Support 32k text-generation and multilingual embedding models ([#161](https://togithub.com/googleapis/python-bigquery-dataframes/issues/161)) ([5f0ea37](https://togithub.com/googleapis/python-bigquery-dataframes/commit/5f0ea37fffff792fc3fbed65e6ace846d8ef6a06)) ### Bug Fixes * Update default temp table expiration to 7 days ([#174](https://togithub.com/googleapis/python-bigquery-dataframes/issues/174)) ([4ff26cd](https://togithub.com/googleapis/python-bigquery-dataframes/commit/4ff26cdf862e9f9b91a3a1d2abfa7fbdf0af9c5b)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 16 ++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 845d3634bc9..fc327b2e966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.13.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.12.0...v0.13.0) (2023-11-07) + + +### Features + +* `to_gbq` without a destination table writes to a temporary table ([#158](https://github.com/googleapis/python-bigquery-dataframes/issues/158)) ([e1817c9](https://github.com/googleapis/python-bigquery-dataframes/commit/e1817c9201ba4ea7fd2f8b6f4a667b010a6fec1b)) +* Add `DataFrame.__iter__`, `DataFrame.iterrows`, `DataFrame.itertuples`, and `DataFrame.keys` methods ([#164](https://github.com/googleapis/python-bigquery-dataframes/issues/164)) ([c065071](https://github.com/googleapis/python-bigquery-dataframes/commit/c065071028c2f4ac80ee7f84dbeb1df385c2a512)) +* Add `Series.__iter__` method ([#164](https://github.com/googleapis/python-bigquery-dataframes/issues/164)) ([c065071](https://github.com/googleapis/python-bigquery-dataframes/commit/c065071028c2f4ac80ee7f84dbeb1df385c2a512)) +* Add interpolate() to series and dataframe ([#157](https://github.com/googleapis/python-bigquery-dataframes/issues/157)) ([b9cb55c](https://github.com/googleapis/python-bigquery-dataframes/commit/b9cb55c5b9354f9ff60de0aad66fe60049876055)) +* Support 32k text-generation and multilingual embedding models ([#161](https://github.com/googleapis/python-bigquery-dataframes/issues/161)) ([5f0ea37](https://github.com/googleapis/python-bigquery-dataframes/commit/5f0ea37fffff792fc3fbed65e6ace846d8ef6a06)) + + +### Bug Fixes + +* Update default temp table expiration to 7 days ([#174](https://github.com/googleapis/python-bigquery-dataframes/issues/174)) ([4ff26cd](https://github.com/googleapis/python-bigquery-dataframes/commit/4ff26cdf862e9f9b91a3a1d2abfa7fbdf0af9c5b)) + ## [0.12.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.11.0...v0.12.0) (2023-11-01) diff --git a/bigframes/version.py b/bigframes/version.py index b324ed72340..0a5df274799 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.0" +__version__ = "0.13.0" From a8a409ab0bd1f99dfb442df0703bf8786e0fe58e Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:50:46 -0800 Subject: [PATCH 138/921] fix: deprecate the `remote_service_type` in llm model (#180) * fix: deprecate the remote_service_type in llm model * fix: address comments --- bigframes/ml/llm.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 2e5a9a1e5e5..3cfc28e61f8 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -24,12 +24,14 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -_REMOTE_TEXT_GENERATOR_MODEL_CODE = "CLOUD_AI_LARGE_LANGUAGE_MODEL_V1" -_REMOTE_TEXT_GENERATOR_32K_MODEL_CODE = "text-bison-32k" +_REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT = "text-bison" +_REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT = "text-bison-32k" _TEXT_GENERATE_RESULT_COLUMN = "ml_generate_text_llm_result" -_REMOTE_EMBEDDING_GENERATOR_MODEL_CODE = "CLOUD_AI_TEXT_EMBEDDING_MODEL_V1" -_REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_CODE = "textembedding-gecko-multilingual" +_REMOTE_EMBEDDING_GENERATOR_MODEL_ENDPOINT = "textembedding-gecko" +_REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT = ( + "textembedding-gecko-multilingual" +) _EMBED_TEXT_RESULT_COLUMN = "text_embedding" @@ -88,14 +90,18 @@ def _create_bqml_model(self): connection_id=connection_name_parts[2], iam_role="aiplatform.user", ) - if self.model_name == "text-bison": + if self.model_name == _REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT: options = { - "remote_service_type": _REMOTE_TEXT_GENERATOR_MODEL_CODE, + "endpoint": _REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT, } - else: + elif self.model_name == _REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT: options = { - "endpoint": _REMOTE_TEXT_GENERATOR_32K_MODEL_CODE, + "endpoint": _REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT, } + else: + raise ValueError( + f"Model name {self.model_name} is not supported. We only support {_REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT} and {_REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT}." + ) return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options ) @@ -240,12 +246,16 @@ def _create_bqml_model(self): ) if self.model_name == "textembedding-gecko": options = { - "remote_service_type": _REMOTE_EMBEDDING_GENERATOR_MODEL_CODE, + "endpoint": _REMOTE_EMBEDDING_GENERATOR_MODEL_ENDPOINT, } - else: + elif self.model_name == _REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT: options = { - "endpoint": _REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_CODE, + "endpoint": _REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT, } + else: + raise ValueError( + f"Model name {self.model_name} is not supported. We only support {_REMOTE_EMBEDDING_GENERATOR_MODEL_ENDPOINT} and {_REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT}." + ) return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options From 0c8bd33806bb99206b8b12dbdf7d7485c6ffb759 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 7 Nov 2023 15:16:14 -0800 Subject: [PATCH 139/921] feat: support `series.at[row_label] = scalar` (#173) Just quickly pipe the existing loc solution to at --- bigframes/core/indexers.py | 12 ++++++++++++ tests/system/small/test_series.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index f6ce084714a..69048b6845a 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -117,6 +117,18 @@ def __getitem__( ) -> Union[bigframes.core.scalar.Scalar, bigframes.series.Series]: return self._series.loc[key] + def __setitem__( + self, + key: LocSingleKey, + value: bigframes.core.scalar.Scalar, + ): + if not pd.api.types.is_scalar(value): + raise NotImplementedError( + "series.at.__setitem__ only supports scalar right-hand values. " + f"{constants.FEEDBACK_LINK}" + ) + self._series.loc[key] = value + class LocDataFrameIndexer: def __init__(self, dataframe: bigframes.dataframe.DataFrame): diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 183ba01c0e0..c8bd0f7afdd 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1012,6 +1012,17 @@ def test_loc_setitem_cell(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_series_equal(bf_original.to_pandas(), pd_original) +def test_at_setitem_row_label_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series = scalars_df["int64_col"] + pd_series = scalars_pandas_df["int64_col"].copy() + bf_series.at[1] = 1000 + pd_series.at[1] = 1000 + bf_result = bf_series.to_pandas() + pd_result = pd_series.astype("Int64") + pd.testing.assert_series_equal(bf_result, pd_result) + + def test_ne_obj_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" From c1d1f42a21cc089877f79ebb46a39ddef6958e04 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 7 Nov 2023 16:00:28 -0800 Subject: [PATCH 140/921] fix: match pandas behavior when assigning listlike to empty dfs (#172) Previously, we weren't able to assign to empty dfs at all, which is certainly not right. This change also matches the behavior for when there is a multi-index but with empty index columns. The pandas behavior is to throw an error with a somewhat odd error message. (I made our error message explicit.) Note: this bug also occurs for assigning series to empty dfs. However, in that case, fixing it would come at the cost of adding a query to check if there are no rows. That's a big cost, so I haven't fixed the series case. --- bigframes/dataframe.py | 49 +++++++++++++++++----------- tests/system/small/test_dataframe.py | 28 ++++++++++++++++ 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 40f12671aef..c5b48822fb1 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1101,23 +1101,38 @@ def _assign_single_item( copy[k] = v(copy) return copy elif utils.is_list_like(v): - given_rows = len(v) - actual_rows = len(self) - if given_rows != actual_rows: - raise ValueError( - f"Length of values ({given_rows}) does not match length of index ({actual_rows})" - ) + return self._assign_single_item_listlike(k, v) + else: + return self._assign_scalar(k, v) - local_df = bigframes.dataframe.DataFrame( - {k: v}, session=self._get_block().expr.session + def _assign_single_item_listlike(self, k: str, v: Sequence) -> DataFrame: + given_rows = len(v) + actual_rows = len(self) + assigning_to_empty_df = len(self.columns) == 0 and actual_rows == 0 + if not assigning_to_empty_df and given_rows != actual_rows: + raise ValueError( + f"Length of values ({given_rows}) does not match length of index ({actual_rows})" ) - # local_df is likely (but not guarunteed) to be cached locally - # since the original list came from memory and so is probably < MAX_INLINE_DF_SIZE - new_column_block = local_df._block - original_index_column_ids = self._block.index_columns - self_block = self._block.reset_index(drop=False) - result_index, (get_column_left, get_column_right) = self_block.index.join( + local_df = bigframes.dataframe.DataFrame( + {k: v}, session=self._get_block().expr.session + ) + # local_df is likely (but not guaranteed) to be cached locally + # since the original list came from memory and so is probably < MAX_INLINE_DF_SIZE + + new_column_block = local_df._block + original_index_column_ids = self._block.index_columns + self_block = self._block.reset_index(drop=False) + if assigning_to_empty_df: + if len(self._block.index_columns) > 1: + # match error raised by pandas here + raise ValueError( + "Assigning listlike to a first column under multiindex is not supported." + ) + result_block = new_column_block.with_index_labels(self._block.index_labels) + result_block = result_block.with_column_labels([k]) + else: + result_index, (get_column_left, get_column_right,) = self_block.index.join( new_column_block.index, how="left", block_identity_join=True ) result_block = result_index._block @@ -1125,13 +1140,9 @@ def _assign_single_item( [get_column_left[col_id] for col_id in original_index_column_ids], index_labels=self._block.index_labels, ) - return DataFrame(result_block) - else: - return self._assign_scalar(k, v) + return DataFrame(result_block) def _assign_scalar(self, label: str, value: Union[int, float]) -> DataFrame: - # TODO(swast): Make sure that k is the ID / SQL name, not a label, - # which could be invalid SQL. col_ids = self._block.cols_matching_label(label) block, constant_col_id = self._block.create_constant(value, label) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index bd5930e5089..a2583e08155 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -567,6 +567,34 @@ def test_assign_existing_column(scalars_dfs): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +def test_assign_listlike_to_empty_df(session): + empty_df = dataframe.DataFrame(session=session) + empty_pandas_df = pd.DataFrame() + + bf_result = empty_df.assign(new_col=[1, 2, 3]) + pd_result = empty_pandas_df.assign(new_col=[1, 2, 3]) + + pd_result["new_col"] = pd_result["new_col"].astype("Int64") + pd_result.index = pd_result.index.astype("Int64") + assert_pandas_df_equal_ignore_ordering(bf_result.to_pandas(), pd_result) + + +def test_assign_to_empty_df_multiindex_error(session): + empty_df = dataframe.DataFrame(session=session) + empty_pandas_df = pd.DataFrame() + empty_df["empty_col_1"] = [] + empty_df["empty_col_2"] = [] + empty_pandas_df["empty_col_1"] = [] + empty_pandas_df["empty_col_2"] = [] + empty_df = empty_df.set_index(["empty_col_1", "empty_col_2"]) + empty_pandas_df = empty_pandas_df.set_index(["empty_col_1", "empty_col_2"]) + + with pytest.raises(ValueError): + empty_df.assign(new_col=[1, 2, 3, 4, 5, 6, 7, 8, 9]) + with pytest.raises(ValueError): + empty_pandas_df.assign(new_col=[1, 2, 3, 4, 5, 6, 7, 8, 9]) + + def test_assign_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs column_name = "int64_col" From 800d44eb5eb77da5d87b2e005f5a2ed53842e7b5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 7 Nov 2023 16:44:18 -0800 Subject: [PATCH 141/921] fix: Use anonymous dataset instead of session dataset for temp tables (#181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 20 +++++---- bigframes/session/__init__.py | 72 +++++++++++-------------------- bigframes/session/_io/bigquery.py | 8 +++- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 4653f0ab6aa..866be9c4003 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -32,6 +32,7 @@ import bigframes.dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops +import bigframes.session._io.bigquery if typing.TYPE_CHECKING: from bigframes.session import Session @@ -153,25 +154,28 @@ def start_query( def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: """Write the ArrayValue to a session table and create a new block object that references it.""" - compiled = self.compile() - ibis_expr = compiled._to_ibis_expr("unordered", expose_hidden_cols=True) - destination = self.session._ibis_to_session_table( - ibis_expr, cluster_cols=cluster_cols, api_name="cache" + compiled_value = self.compile() + ibis_expr = compiled_value._to_ibis_expr( + ordering_mode="unordered", expose_hidden_cols=True ) + tmp_table = self.session._ibis_to_session_table( + ibis_expr, cluster_cols=cluster_cols, api_name="cached" + ) + table_expression = self.session.ibis_client.table( - f"{destination.project}.{destination.dataset_id}.{destination.table_id}" + f"{tmp_table.project}.{tmp_table.dataset_id}.{tmp_table.table_id}" ) - new_columns = [table_expression[column] for column in compiled.column_ids] + new_columns = [table_expression[column] for column in compiled_value.column_ids] new_hidden_columns = [ table_expression[column] - for column in compiled._hidden_ordering_column_names + for column in compiled_value._hidden_ordering_column_names ] return ArrayValue.from_ibis( self.session, table_expression, columns=new_columns, hidden_ordering_columns=new_hidden_columns, - ordering=compiled._ordering, + ordering=compiled_value._ordering, ) # Operations diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 2537e81e19c..b49e2469a91 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,6 +16,7 @@ from __future__ import annotations +import datetime import logging import os import re @@ -325,9 +326,15 @@ def _query_to_destination( # internal issue 303057336. # Since we have a `statement_type == 'SELECT'`, schema should be populated. schema = typing.cast(Iterable[bigquery.SchemaField], dry_run_job.schema) - temp_table = self._create_session_table_empty(api_name, schema, index_cols) + cluster_cols = [ + item.name + for item in schema + if (item.name in index_cols) and _can_cluster_bq(item) + ][:_MAX_CLUSTER_COLUMNS] + temp_table = self._create_empty_temp_table(schema, cluster_cols) job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name job_config.destination = temp_table try: @@ -422,7 +429,7 @@ def _read_gbq_query( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, - api_name: str, + api_name: str = "read_gbq_query", ) -> dataframe.DataFrame: if isinstance(index_col, str): index_cols = [index_col] @@ -430,9 +437,7 @@ def _read_gbq_query( index_cols = list(index_col) destination, query_job = self._query_to_destination( - query, - index_cols, - api_name=api_name, + query, index_cols, api_name=api_name ) # If there was no destination table, that means the query must have @@ -1273,53 +1278,26 @@ def _create_session_table(self) -> bigquery.TableReference: ) return dataset.table(table_name) - def _create_session_table_empty( + def _create_empty_temp_table( self, - api_name: str, schema: Iterable[bigquery.SchemaField], cluster_cols: List[str], ) -> bigquery.TableReference: # Can't set a table in _SESSION as destination via query job API, so we # run DDL, instead. - table = self._create_session_table() - schema_sql = bigframes_io.bq_schema_to_sql(schema) - - clusterable_cols = [ - col.name - for col in schema - if col.name in cluster_cols and _can_cluster_bq(col) - ][:_MAX_CLUSTER_COLUMNS] - - if clusterable_cols: - cluster_cols_sql = ", ".join( - f"`{cluster_col}`" for cluster_col in clusterable_cols - ) - cluster_sql = f"CLUSTER BY {cluster_cols_sql}" - else: - cluster_sql = "" - - ddl_text = f""" - CREATE TEMP TABLE - `_SESSION`.`{table.table_id}` - ({schema_sql}) - {cluster_sql} - """ - - job_config = bigquery.QueryJobConfig() - - # Include a label so that Dataplex Lineage can identify temporary - # tables that BigQuery DataFrames creates. Googlers: See internal issue - # 296779699. We're labeling the job instead of the table because - # otherwise we get `BadRequest: 400 OPTIONS on temporary tables are not - # supported`. - job_config.labels = {"source": "bigquery-dataframes-temp"} - job_config.labels["bigframes-api"] = api_name - - _, query_job = self._start_query(ddl_text, job_config=job_config) + dataset = self._anonymous_dataset + expiration = ( + datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION + ) - # Use fully-qualified name instead of `_SESSION` name so that the - # created table can be used as the destination table. - return query_job.destination + table = bigframes_io.create_temp_table( + self.bqclient, + dataset, + expiration, + schema=schema, + cluster_columns=cluster_cols, + ) + return bigquery.TableReference.from_string(table) def _create_sequential_ordering( self, @@ -1356,13 +1334,13 @@ def _ibis_to_session_table( cluster_cols: Iterable[str], api_name: str, ) -> bigquery.TableReference: - desination, _ = self._query_to_destination( + destination, _ = self._query_to_destination( self.ibis_client.compile(table), index_cols=list(cluster_cols), api_name=api_name, ) # There should always be a destination table for this query type. - return typing.cast(bigquery.TableReference, desination) + return typing.cast(bigquery.TableReference, destination) def remote_function( self, diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index 06d240fec65..badc91e3565 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -19,7 +19,7 @@ import datetime import textwrap import types -from typing import Dict, Iterable, Union +from typing import Dict, Iterable, Optional, Union import uuid import google.cloud.bigquery as bigquery @@ -121,11 +121,17 @@ def create_temp_table( bqclient: bigquery.Client, dataset: bigquery.DatasetReference, expiration: datetime.datetime, + *, + schema: Optional[Iterable[bigquery.SchemaField]] = None, + cluster_columns: Optional[list[str]] = None, ) -> str: """Create an empty table with an expiration in the desired dataset.""" table_ref = random_table(dataset) destination = bigquery.Table(table_ref) destination.expires = expiration + destination.schema = schema + if cluster_columns: + destination.clustering_fields = cluster_columns bqclient.create_table(destination) return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" From f95000d3f88662be4d88c8b0152f1b838e99ec55 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 7 Nov 2023 17:36:15 -0800 Subject: [PATCH 142/921] fix: for reset_index on unnamed multiindex, always use level_[n] label (#182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/blocks.py | 2 +- tests/system/small/test_dataframe.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 635e7db8656..e831b42752f 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -282,7 +282,7 @@ def reset_index(self, drop: bool = True) -> Block: column_labels_modified = self.column_labels for level, label in enumerate(index_labels): if label is None: - if "index" not in self.column_labels: + if "index" not in self.column_labels and len(index_labels) <= 1: label = "index" else: label = f"level_{level}" diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index a2583e08155..3d7ba867ea4 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1235,6 +1235,28 @@ def test_reset_index_with_unnamed_index( pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_reset_index_with_unnamed_multiindex( + scalars_df_index, + scalars_pandas_df_index, +): + bf_df = dataframe.DataFrame( + ([1, 2, 3], [2, 5, 7]), + index=pd.MultiIndex.from_tuples([("a", "aa"), ("a", "aa")]), + ) + pd_df = pd.DataFrame( + ([1, 2, 3], [2, 5, 7]), + index=pd.MultiIndex.from_tuples([("a", "aa"), ("a", "aa")]), + ) + + bf_df = bf_df.reset_index() + pd_df = pd_df.reset_index() + + assert pd_df.columns[0] == "level_0" + assert bf_df.columns[0] == "level_0" + assert pd_df.columns[1] == "level_1" + assert bf_df.columns[1] == "level_1" + + def test_reset_index_with_unnamed_index_and_index_column( scalars_df_index, scalars_pandas_df_index, From aee0e8e2518c59bd1e0b07940c3309871fde8899 Mon Sep 17 00:00:00 2001 From: Alexander Potts Date: Thu, 9 Nov 2023 03:47:29 +1100 Subject: [PATCH 143/921] feat: support `date_series.astype("string[pyarrow]")` to cast DATE to STRING (#186) --- bigframes/dtypes.py | 2 +- tests/system/small/test_series.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 079f0cc27a2..cd35e380c02 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -375,7 +375,7 @@ def cast_ibis_value( ), ibis_dtypes.float64: (ibis_dtypes.string, ibis_dtypes.int64), ibis_dtypes.string: (ibis_dtypes.int64, ibis_dtypes.float64), - ibis_dtypes.date: (), + ibis_dtypes.date: (ibis_dtypes.string,), ibis_dtypes.Decimal(precision=38, scale=9): (ibis_dtypes.float64,), ibis_dtypes.Decimal(precision=76, scale=38): (ibis_dtypes.float64,), ibis_dtypes.time: (), diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index c8bd0f7afdd..6bb5c4755ca 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2498,6 +2498,7 @@ def test_mask_custom_value(scalars_dfs): # with timezone conversions, so we'll allow it. ("timestamp_col", pd.ArrowDtype(pa.timestamp("us"))), ("datetime_col", pd.ArrowDtype(pa.timestamp("us", tz="UTC"))), + ("date_col", "string[pyarrow]"), # TODO(bmil): fix Ibis bug: BigQuery backend rounds to nearest int # ("float64_col", "Int64"), # TODO(bmil): decide whether to fix Ibis bug: BigQuery backend From 765446a929abe1ac076c3037afa7892f64105356 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 8 Nov 2023 14:28:13 -0800 Subject: [PATCH 144/921] feat: add 'cross' join support (#176) * feat: add 'cross' join support --- bigframes/core/__init__.py | 1 + bigframes/core/blocks.py | 1 + bigframes/core/compile/single_column.py | 3 +- bigframes/core/joins/merge.py | 1 + bigframes/core/nodes.py | 1 + bigframes/dataframe.py | 26 +++++++++++ bigframes/pandas/__init__.py | 1 + tests/system/small/test_dataframe.py | 44 ++++++++++++++----- tests/system/small/test_pandas.py | 21 +++++++++ .../bigframes_vendored/pandas/core/frame.py | 5 +++ .../pandas/core/reshape/merge.py | 2 + 11 files changed, 93 insertions(+), 13 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 866be9c4003..9426df54f07 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -417,6 +417,7 @@ def join( "left", "outer", "right", + "cross", ], allow_row_identity_join: bool = True, ): diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index e831b42752f..bf3b9321c3b 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1531,6 +1531,7 @@ def merge( "left", "outer", "right", + "cross", ], left_join_ids: typing.Sequence[str], right_join_ids: typing.Sequence[str], diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index b992aa1d1d9..93ba3f16f1c 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -40,6 +40,7 @@ def join_by_column( "left", "outer", "right", + "cross", ], allow_row_identity_join: bool = True, ) -> compiled.CompiledArrayValue: @@ -107,7 +108,7 @@ def join_by_column( left_table, right_table, predicates=join_conditions, - how=how, + how=how, # type: ignore ) # Preserve ordering accross joins. diff --git a/bigframes/core/joins/merge.py b/bigframes/core/joins/merge.py index fac16b36078..c65e1bdd54d 100644 --- a/bigframes/core/joins/merge.py +++ b/bigframes/core/joins/merge.py @@ -32,6 +32,7 @@ def merge( "left", "outer", "right", + "cross", ] = "inner", on: Optional[str] = None, *, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 7b252b164f6..8f1e2e5e736 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -86,6 +86,7 @@ class JoinNode(BigFrameNode): "left", "outer", "right", + "cross", ] allow_row_identity_join: bool = True diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index c5b48822fb1..701c26f42f6 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1933,6 +1933,7 @@ def merge( "left", "outer", "right", + "cross", ] = "inner", # TODO(garrettwu): Currently can take inner, outer, left and right. To support # cross joins @@ -1943,6 +1944,19 @@ def merge( sort: bool = False, suffixes: tuple[str, str] = ("_x", "_y"), ) -> DataFrame: + if how == "cross": + if on is not None: + raise ValueError("'on' is not supported for cross join.") + result_block = self._block.merge( + right._block, + left_join_ids=[], + right_join_ids=[], + suffixes=suffixes, + how=how, + sort=True, + ) + return DataFrame(result_block) + if on is None: if left_on is None or right_on is None: raise ValueError("Must specify `on` or `left_on` + `right_on`.") @@ -1996,6 +2010,18 @@ def join( raise NotImplementedError( f"Deduping column names is not implemented. {constants.FEEDBACK_LINK}" ) + if how == "cross": + if on is not None: + raise ValueError("'on' is not supported for cross join.") + result_block = left._block.merge( + right._block, + left_join_ids=[], + right_join_ids=[], + suffixes=("", ""), + how="cross", + sort=True, + ) + return DataFrame(result_block) # Join left columns with right index if on is not None: diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 1c52b103fbe..d35f8383665 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -332,6 +332,7 @@ def merge( "left", "outer", "right", + "cross", ] = "inner", on: Optional[str] = None, *, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 3d7ba867ea4..605d4abc1d0 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -914,6 +914,26 @@ def test_df_isin_dict(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result.astype("boolean")) +def test_df_cross_merge(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + left_columns = ["int64_col", "float64_col", "rowindex_2"] + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_columns] + # Offset the rows somewhat so that outer join can have an effect. + right = scalars_df[right_columns].assign(rowindex_2=scalars_df["rowindex_2"] + 2) + + bf_result = left.merge(right, "cross").to_pandas() + + pd_result = scalars_pandas_df[left_columns].merge( + scalars_pandas_df[right_columns].assign( + rowindex_2=scalars_pandas_df["rowindex_2"] + 2 + ), + "cross", + ) + pd.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + @pytest.mark.parametrize( ("merge_how",), [ @@ -1745,12 +1765,7 @@ def test_series_binop_add_different_table( all_joins = pytest.mark.parametrize( ("how",), - ( - ("outer",), - ("left",), - ("right",), - ("inner",), - ), + (("outer",), ("left",), ("right",), ("inner",), ("cross",)), ) @@ -1795,13 +1810,18 @@ def test_join_param_on(scalars_dfs, how): bf_df_a = bf_df[["string_col", "int64_col", "rowindex_2"]] bf_df_a = bf_df_a.assign(rowindex_2=bf_df_a["rowindex_2"] + 2) bf_df_b = bf_df[["float64_col"]] - bf_result = bf_df_a.join(bf_df_b, on="rowindex_2", how=how).to_pandas() - pd_df_a = pd_df[["string_col", "int64_col", "rowindex_2"]] - pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) - pd_df_b = pd_df[["float64_col"]] - pd_result = pd_df_a.join(pd_df_b, on="rowindex_2", how=how) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + if how == "cross": + with pytest.raises(ValueError): + bf_df_a.join(bf_df_b, on="rowindex_2", how=how) + else: + bf_result = bf_df_a.join(bf_df_b, on="rowindex_2", how=how).to_pandas() + + pd_df_a = pd_df[["string_col", "int64_col", "rowindex_2"]] + pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) + pd_df_b = pd_df[["float64_col"]] + pd_result = pd_df_a.join(pd_df_b, on="rowindex_2", how=how) + assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) @pytest.mark.parametrize( diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index 0292ebd2069..b88901f3bcf 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -289,6 +289,27 @@ def test_merge_left_on_right_on(scalars_dfs, merge_how): assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) +def test_pd_merge_cross(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + left_columns = ["int64_col", "float64_col", "int64_too"] + right_columns = ["int64_col", "bool_col", "string_col", "rowindex_2"] + + left = scalars_df[left_columns] + right = scalars_df[right_columns] + + df = bpd.merge(left, right, "cross", sort=True) + bf_result = df.to_pandas() + + pd_result = pd.merge( + scalars_pandas_df[left_columns], + scalars_pandas_df[right_columns], + "cross", + sort=True, + ) + + pd.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + @pytest.mark.parametrize( ("merge_how",), [ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 6f4f6be35d6..3e4cec284c9 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2194,6 +2194,8 @@ def join(self, other, *, on: Optional[str] = None, how: str) -> DataFrame: and sort it lexicographically. ``inner``: form intersection of calling frame's index (or column if on is specified) with `other`'s index, preserving the order of the calling's one. + ``cross``: creates the cartesian product from both frames, preserves + the order of the left keys. Returns: bigframes.dataframe.DataFrame: A dataframe containing columns from both the caller and `other`. @@ -2208,6 +2210,7 @@ def merge( "left", "outer", "right", + "cross", ] = "inner", on: Optional[str] = None, *, @@ -2243,6 +2246,8 @@ def merge( join; sort keys lexicographically. ``inner``: use intersection of keys from both frames, similar to a SQL inner join; preserve the order of the left keys. + ``cross``: creates the cartesian product from both frames, preserves the order + of the left keys. on (label or list of labels): Columns to join on. It must be found in both DataFrames. Either on or left_on + right_on diff --git a/third_party/bigframes_vendored/pandas/core/reshape/merge.py b/third_party/bigframes_vendored/pandas/core/reshape/merge.py index b03f366fca7..704e50f5162 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/merge.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/merge.py @@ -49,6 +49,8 @@ def merge( join; sort keys lexicographically. ``inner``: use intersection of keys from both frames, similar to a SQL inner join; preserve the order of the left keys. + ``cross``: creates the cartesian product from both frames, preserves the order + of the left keys. on (label or list of labels): Columns to join on. It must be found in both DataFrames. Either on or left_on + right_on From 6a2840349a23035bdfdabacd1e231b41bbb5ed7a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 8 Nov 2023 15:24:55 -0800 Subject: [PATCH 145/921] feat: add 'index', 'pad', 'nearest' interpolate methods (#162) * feat: add 'index', 'pad', 'nearest' interpolate methods --- bigframes/core/block_transforms.py | 207 ++++++++++++++---- bigframes/dataframe.py | 2 + bigframes/series.py | 2 + tests/system/small/test_series.py | 21 +- .../bigframes_vendored/pandas/core/frame.py | 51 +++-- .../bigframes_vendored/pandas/core/series.py | 50 +++-- 6 files changed, 243 insertions(+), 90 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 917edac0ded..e095f21f6b7 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -22,6 +22,7 @@ import bigframes.core.blocks as blocks import bigframes.core.ordering as ordering import bigframes.core.window_spec as windows +import bigframes.dtypes as dtypes import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -106,18 +107,33 @@ def indicate_duplicates( def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: - if method != "linear": + supported_methods = [ + "linear", + "values", + "index", + "nearest", + "zero", + "slinear", + ] + if method not in supported_methods: raise NotImplementedError( - f"Only 'linear' interpolate method supported. {constants.FEEDBACK_LINK}" + f"Method {method} not supported, following interpolate methods supported: {', '.join(supported_methods)}. {constants.FEEDBACK_LINK}" ) - backwards_window = windows.WindowSpec(following=0) - forwards_window = windows.WindowSpec(preceding=0) - output_column_ids = [] original_columns = block.value_columns original_labels = block.column_labels - block, offsets = block.promote_offsets() + + if method == "linear": # Assumes evenly spaced, ignore index + block, xvalues = block.promote_offsets() + else: + index_columns = block.index_columns + if len(index_columns) != 1: + raise ValueError("only method 'linear' supports multi-index") + xvalues = block.index_columns[0] + if block.index_dtypes[0] not in dtypes.NUMERIC_BIGFRAMES_TYPES: + raise ValueError("Can only interpolate on numeric index.") + for column in original_columns: # null in same places column is null should_interpolate = block._column_type(column) in [ @@ -125,48 +141,25 @@ def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: pd.Int64Dtype(), ] if should_interpolate: - block, notnull = block.apply_unary_op(column, ops.notnull_op) - block, masked_offsets = block.apply_binary_op( - offsets, notnull, ops.partial_arg3(ops.where_op, None) - ) - - block, previous_value = block.apply_window_op( - column, agg_ops.LastNonNullOp(), backwards_window - ) - block, next_value = block.apply_window_op( - column, agg_ops.FirstNonNullOp(), forwards_window - ) - block, previous_value_offset = block.apply_window_op( - masked_offsets, - agg_ops.LastNonNullOp(), - backwards_window, - skip_reproject_unsafe=True, - ) - block, next_value_offset = block.apply_window_op( - masked_offsets, - agg_ops.FirstNonNullOp(), - forwards_window, - skip_reproject_unsafe=True, - ) - - block, prediction_id = _interpolate( + interpolate_method_map = { + "linear": "linear", + "values": "linear", + "index": "linear", + "slinear": "linear", + "zero": "ffill", + "nearest": "nearest", + } + extrapolating_methods = ["linear", "values", "index"] + interpolate_method = interpolate_method_map[method] + do_extrapolate = method in extrapolating_methods + block, interpolated = _interpolate_column( block, - previous_value_offset, - previous_value, - next_value_offset, - next_value, - offsets, + column, + xvalues, + interpolate_method=interpolate_method, + do_extrapolate=do_extrapolate, ) - - block, interpolated_column = block.apply_binary_op( - column, prediction_id, ops.fillna_op - ) - # Pandas performs ffill-like behavior to extrapolate forwards - block, interpolated_and_ffilled = block.apply_binary_op( - interpolated_column, previous_value, ops.fillna_op - ) - - output_column_ids.append(interpolated_and_ffilled) + output_column_ids.append(interpolated) else: output_column_ids.append(column) @@ -175,7 +168,80 @@ def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: return block.with_column_labels(original_labels) -def _interpolate( +def _interpolate_column( + block: blocks.Block, + column: str, + x_values: str, + interpolate_method: str, + do_extrapolate: bool = True, +) -> typing.Tuple[blocks.Block, str]: + if interpolate_method not in ["linear", "nearest", "ffill"]: + raise ValueError("interpolate method not supported") + window_ordering = (ordering.OrderingColumnReference(x_values),) + backwards_window = windows.WindowSpec(following=0, ordering=window_ordering) + forwards_window = windows.WindowSpec(preceding=0, ordering=window_ordering) + + # Note, this method may + block, notnull = block.apply_unary_op(column, ops.notnull_op) + block, masked_offsets = block.apply_binary_op( + x_values, notnull, ops.partial_arg3(ops.where_op, None) + ) + + block, previous_value = block.apply_window_op( + column, agg_ops.LastNonNullOp(), backwards_window + ) + block, next_value = block.apply_window_op( + column, agg_ops.FirstNonNullOp(), forwards_window + ) + block, previous_value_offset = block.apply_window_op( + masked_offsets, + agg_ops.LastNonNullOp(), + backwards_window, + skip_reproject_unsafe=True, + ) + block, next_value_offset = block.apply_window_op( + masked_offsets, + agg_ops.FirstNonNullOp(), + forwards_window, + skip_reproject_unsafe=True, + ) + + if interpolate_method == "linear": + block, prediction_id = _interpolate_points_linear( + block, + previous_value_offset, + previous_value, + next_value_offset, + next_value, + x_values, + ) + elif interpolate_method == "nearest": + block, prediction_id = _interpolate_points_nearest( + block, + previous_value_offset, + previous_value, + next_value_offset, + next_value, + x_values, + ) + else: # interpolate_method == 'ffill': + block, prediction_id = _interpolate_points_ffill( + block, + previous_value_offset, + previous_value, + next_value_offset, + next_value, + x_values, + ) + if do_extrapolate: + block, prediction_id = block.apply_binary_op( + prediction_id, previous_value, ops.fillna_op + ) + + return block.apply_binary_op(column, prediction_id, ops.fillna_op) + + +def _interpolate_points_linear( block: blocks.Block, x0_id: str, y0_id: str, @@ -196,6 +262,53 @@ def _interpolate( return block, prediction_id +def _interpolate_points_nearest( + block: blocks.Block, + x0_id: str, + y0_id: str, + x1_id: str, + y1_id: str, + xpredict_id: str, +) -> typing.Tuple[blocks.Block, str]: + """Interpolate by taking the y value of the nearest x value""" + block, left_diff = block.apply_binary_op(xpredict_id, x0_id, ops.sub_op) + block, right_diff = block.apply_binary_op(x1_id, xpredict_id, ops.sub_op) + # If diffs equal, choose left + block, choose_left = block.apply_binary_op(left_diff, right_diff, ops.le_op) + block, choose_left = block.apply_unary_op( + choose_left, ops.partial_right(ops.fillna_op, False) + ) + + block, nearest = block.apply_ternary_op(y0_id, choose_left, y1_id, ops.where_op) + + block, y0_exists = block.apply_unary_op(y0_id, ops.notnull_op) + block, y1_exists = block.apply_unary_op(y1_id, ops.notnull_op) + block, is_interpolation = block.apply_binary_op(y0_exists, y1_exists, ops.and_op) + + block, prediction_id = block.apply_binary_op( + nearest, is_interpolation, ops.partial_arg3(ops.where_op, None) + ) + + return block, prediction_id + + +def _interpolate_points_ffill( + block: blocks.Block, + x0_id: str, + y0_id: str, + x1_id: str, + y1_id: str, + xpredict_id: str, +) -> typing.Tuple[blocks.Block, str]: + """Interpolates by using the preceding values""" + # check for existance of y1, otherwise we are extrapolating instead of interpolating + block, y1_exists = block.apply_unary_op(y1_id, ops.notnull_op) + block, prediction_id = block.apply_binary_op( + y0_id, y1_exists, ops.partial_arg3(ops.where_op, None) + ) + return block, prediction_id + + def drop_duplicates( block: blocks.Block, columns: typing.Sequence[str], keep: str = "first" ) -> blocks.Block: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 701c26f42f6..1e143144fe3 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1450,6 +1450,8 @@ def reindex_like(self, other: DataFrame, *, validate: typing.Optional[bool] = No return self.reindex(index=other.index, columns=other.columns, validate=validate) def interpolate(self, method: str = "linear") -> DataFrame: + if method == "pad": + return self.ffill() result = block_ops.interpolate(self._block, method) return DataFrame(result) diff --git a/bigframes/series.py b/bigframes/series.py index 032bdf6c429..9e111618de2 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -475,6 +475,8 @@ def replace( return Series(block.select_column(result_col)) def interpolate(self, method: str = "linear") -> Series: + if method == "pad": + return self.ffill() result = block_ops.interpolate(self._block, method) return Series(result) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 6bb5c4755ca..006d370818d 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -273,21 +273,26 @@ def test_series_replace_list_scalar(scalars_dfs): @pytest.mark.parametrize( - ("values",), + ("method",), ( - ([None, 1, 2, None, None, 16, None],), - ([None, None, 3.6, None],), - ([403.2, None, 352.1, None, None, 111.9],), + ("linear",), + ("values",), + ("slinear",), + ("nearest",), + ("zero",), + ("pad",), ), ) -def test_series_interpolate(values): - pd_series = pd.Series(values) +def test_series_interpolate(method): + values = [None, 1, 2, None, None, 16, None] + index = [-3.2, 11.4, 3.56, 4, 4.32, 5.55, 76.8] + pd_series = pd.Series(values, index) bf_series = series.Series(pd_series) # Pandas can only interpolate on "float64" columns # https://github.com/pandas-dev/pandas/issues/40252 - pd_result = pd_series.astype("float64").interpolate() - bf_result = bf_series.interpolate().to_pandas() + pd_result = pd_series.astype("float64").interpolate(method=method) + bf_result = bf_series.interpolate(method=method).to_pandas() # pd uses non-null types, while bf uses nullable types pd.testing.assert_series_equal( diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 3e4cec284c9..10f73d25b77 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2872,17 +2872,6 @@ def interpolate(self, method: str = "linear"): """ Fill NaN values using an interpolation method. - Args: - method (str, default 'linear'): - Interpolation technique to use. Only 'linear' supported. - 'linear': Ignore the index and treat the values as equally spaced. - This is the only method supported on MultiIndexes. - - Returns: - DataFrame: - Returns the same object type as the caller, interpolated at - some or all ``NaN`` values - **Examples:** >>> import bigframes.pandas as bpd @@ -2891,17 +2880,41 @@ def interpolate(self, method: str = "linear"): >>> df = bpd.DataFrame({ ... 'A': [1, 2, 3, None, None, 6], ... 'B': [None, 6, None, 2, None, 3], - ... }) + ... }, index=[0, 0.1, 0.3, 0.7, 0.9, 1.0]) >>> df.interpolate() - A B - 0 1.0 - 1 2.0 6.0 - 2 3.0 4.0 - 3 4.0 2.0 - 4 5.0 2.5 - 5 6.0 3.0 + A B + 0.0 1.0 + 0.1 2.0 6.0 + 0.3 3.0 4.0 + 0.7 4.0 2.0 + 0.9 5.0 2.5 + 1.0 6.0 3.0 [6 rows x 2 columns] + >>> df.interpolate(method="values") + A B + 0.0 1.0 + 0.1 2.0 6.0 + 0.3 3.0 4.666667 + 0.7 4.714286 2.0 + 0.9 5.571429 2.666667 + 1.0 6.0 3.0 + + [6 rows x 2 columns] + + Args: + method (str, default 'linear'): + Interpolation technique to use. Only 'linear' supported. + 'linear': Ignore the index and treat the values as equally spaced. + This is the only method supported on MultiIndexes. + 'index', 'values': use the actual numerical values of the index. + 'pad': Fill in NaNs using existing values. + 'nearest', 'zero', 'slinear': Emulates `scipy.interpolate.interp1d` + + Returns: + DataFrame: + Returns the same object type as the caller, interpolated at + some or all ``NaN`` values """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index b569e5699c7..b25b0c75cff 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -920,31 +920,49 @@ def interpolate(self, method: str = "linear"): """ Fill NaN values using an interpolation method. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3, None, None, 6], + ... 'B': [None, 6, None, 2, None, 3], + ... }, index=[0, 0.1, 0.3, 0.7, 0.9, 1.0]) + >>> df.interpolate() + A B + 0.0 1.0 + 0.1 2.0 6.0 + 0.3 3.0 4.0 + 0.7 4.0 2.0 + 0.9 5.0 2.5 + 1.0 6.0 3.0 + + [6 rows x 2 columns] + >>> df.interpolate(method="values") + A B + 0.0 1.0 + 0.1 2.0 6.0 + 0.3 3.0 4.666667 + 0.7 4.714286 2.0 + 0.9 5.571429 2.666667 + 1.0 6.0 3.0 + + [6 rows x 2 columns] + + Args: method (str, default 'linear'): Interpolation technique to use. Only 'linear' supported. 'linear': Ignore the index and treat the values as equally spaced. This is the only method supported on MultiIndexes. - + 'index', 'values': use the actual numerical values of the index. + 'pad': Fill in NaNs using existing values. + 'nearest', 'zero', 'slinear': Emulates `scipy.interpolate.interp1d` Returns: Series: Returns the same object type as the caller, interpolated at some or all ``NaN`` values - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> bpd.options.display.progress_bar = None - - >>> series = bpd.Series([1, 2, 3, None, None, 6]) - >>> series.interpolate() - 0 1.0 - 1 2.0 - 2 3.0 - 3 4.0 - 4 5.0 - 5 6.0 - dtype: Float64 """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 58f420c91d94ca085e9810f36513ffe772bfddcf Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 8 Nov 2023 16:04:14 -0800 Subject: [PATCH 146/921] feat: add unordered sql compilation (#156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 33 +- bigframes/core/blocks.py | 9 +- bigframes/core/compile/__init__.py | 10 +- bigframes/core/compile/compiled.py | 1504 ++++++++++------- bigframes/core/compile/compiler.py | 173 +- bigframes/core/compile/concat.py | 100 ++ bigframes/core/compile/row_identity.py | 70 +- bigframes/core/compile/single_column.py | 104 +- bigframes/dataframe.py | 6 + bigframes/series.py | 7 + bigframes/session/__init__.py | 6 +- tests/system/large/ml/test_cluster.py | 4 +- tests/system/large/ml/test_pipeline.py | 4 +- tests/system/large/test_remote_function.py | 32 +- tests/system/small/ml/test_cluster.py | 4 +- tests/system/small/ml/test_core.py | 3 +- tests/system/small/ml/test_decomposition.py | 6 +- .../system/small/operations/test_datetimes.py | 22 +- tests/system/small/operations/test_strings.py | 42 +- tests/system/small/test_dataframe.py | 183 +- tests/system/small/test_dataframe_io.py | 17 +- tests/system/small/test_groupby.py | 35 +- tests/system/small/test_multiindex.py | 6 +- tests/system/small/test_pandas.py | 21 +- tests/system/small/test_remote_function.py | 24 +- tests/system/small/test_series.py | 111 +- tests/system/utils.py | 42 +- tests/unit/test_core.py | 32 +- 28 files changed, 1652 insertions(+), 958 deletions(-) create mode 100644 bigframes/core/compile/concat.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 9426df54f07..63f36d4ddd2 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -23,7 +23,8 @@ import ibis.expr.types as ibis_types import pandas -import bigframes.core.compile as compiled +import bigframes.core.compile.compiled as compiled +import bigframes.core.compile.compiler as compiler import bigframes.core.guid import bigframes.core.nodes as nodes from bigframes.core.ordering import OrderingColumnReference @@ -78,7 +79,7 @@ def from_pandas(cls, pd_df: pandas.DataFrame): @property def column_ids(self) -> typing.Sequence[str]: - return self.compile().column_ids + return self._compile_ordered().column_ids @property def session(self) -> Session: @@ -88,15 +89,18 @@ def session(self) -> Session: return self.node.session[0] if required_session else get_global_session() def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: - return self.compile().get_column_type(key) + return self._compile_ordered().get_column_type(key) - def compile(self) -> compiled.CompiledArrayValue: - return compiled.compile_node(self.node) + def _compile_ordered(self) -> compiled.OrderedIR: + return compiler.compile_ordered(self.node) + + def _compile_unordered(self) -> compiled.UnorderedIR: + return compiler.compile_unordered(self.node) def shape(self) -> typing.Tuple[int, int]: """Returns dimensions as (length, width) tuple.""" - width = len(self.compile().columns) - count_expr = self.compile()._to_ibis_expr("unordered").count() + width = len(self._compile_unordered().columns) + count_expr = self._compile_unordered()._to_ibis_expr().count() # Support in-memory engines for hermetic unit tests. if not self.node.session: @@ -121,11 +125,14 @@ def to_sql( col_id_overrides: typing.Mapping[str, str] = {}, sorted: bool = False, ) -> str: - return self.compile().to_sql( - offset_column=offset_column, - col_id_overrides=col_id_overrides, - sorted=sorted, - ) + if sorted or offset_column: + return self._compile_ordered().to_sql( + offset_column=offset_column, + col_id_overrides=col_id_overrides, + sorted=sorted, + ) + else: + return self._compile_unordered().to_sql(col_id_overrides=col_id_overrides) def start_query( self, @@ -154,7 +161,7 @@ def start_query( def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: """Write the ArrayValue to a session table and create a new block object that references it.""" - compiled_value = self.compile() + compiled_value = self._compile_ordered() ibis_expr = compiled_value._to_ibis_expr( ordering_mode="unordered", expose_hidden_cols=True ) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index bf3b9321c3b..6358d28e2ea 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -386,6 +386,8 @@ def to_pandas( max_download_size: Optional[int] = None, sampling_method: Optional[str] = None, random_state: Optional[int] = None, + *, + ordered: bool = True, ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame.""" if max_download_size is None: @@ -412,6 +414,7 @@ def to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, + ordered=ordered, ) return df, query_job @@ -446,12 +449,16 @@ def _compute_and_count( max_download_size: Optional[int] = None, sampling_method: Optional[str] = None, random_state: Optional[int] = None, + *, + ordered: bool = True, ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. expr = self._apply_value_keys_to_expr(value_keys=value_keys) - results_iterator, query_job = expr.start_query(max_results=max_results) + results_iterator, query_job = expr.start_query( + max_results=max_results, sorted=ordered + ) table_size = ( expr.session._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES diff --git a/bigframes/core/compile/__init__.py b/bigframes/core/compile/__init__.py index c86f4463dc0..761fd9a4650 100644 --- a/bigframes/core/compile/__init__.py +++ b/bigframes/core/compile/__init__.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from bigframes.core.compile.compiled import CompiledArrayValue -from bigframes.core.compile.compiler import compile_node +from bigframes.core.compile.compiled import OrderedIR, UnorderedIR +from bigframes.core.compile.compiler import compile_ordered, compile_unordered __all__ = [ - "compile_node", - "CompiledArrayValue", + "compile_ordered", + "compile_unordered", + "OrderedIR", + "UnorderedIR", ] diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 1134f1aab01..4ba5e6bd080 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -13,8 +13,8 @@ # limitations under the License. from __future__ import annotations +import abc import functools -import math import textwrap import typing from typing import Collection, Iterable, Literal, Optional, Sequence @@ -32,8 +32,6 @@ ExpressionOrdering, IntegerEncoding, OrderingColumnReference, - reencode_order_string, - StringEncoding, ) import bigframes.core.utils as utils from bigframes.core.window_spec import WindowSpec @@ -44,8 +42,568 @@ ORDER_ID_COLUMN = "bigframes_ordering_id" PREDICATE_COLUMN = "bigframes_predicate" +T = typing.TypeVar("T", bound="BaseIbisIR") -class CompiledArrayValue: + +class BaseIbisIR(abc.ABC): + """Implementation detail, contains common logic between ordered and unordered IR""" + + def __init__( + self, + table: ibis_types.Table, + columns: Sequence[ibis_types.Value], + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + ): + self._table = table + self._predicates = tuple(predicates) if predicates is not None else () + # Allow creating a DataFrame directly from an Ibis table expression. + # TODO(swast): Validate that each column references the same table (or + # no table for literal values). + self._columns = tuple(columns) + # To allow for more efficient lookup by column name, create a + # dictionary mapping names to column values. + self._column_names = {column.get_name(): column for column in self._columns} + + @property + def columns(self) -> typing.Tuple[ibis_types.Value, ...]: + return self._columns + + @property + def column_ids(self) -> typing.Sequence[str]: + return tuple(self._column_names.keys()) + + @property + def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: + """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" + return ( + _reduce_predicate_list(self._predicates).name(PREDICATE_COLUMN) + if self._predicates + else None + ) + + @abc.abstractmethod + def select_columns(self: T, column_ids: typing.Sequence[str]) -> T: + """Creates a new expression based on this expression with new columns.""" + ... + + def drop_columns(self: T, columns: Iterable[str]) -> T: + return self.select_columns( + [col for col in self.column_ids if col not in columns] + ) + + @abc.abstractmethod + def filter(self: T, predicate_id: str, keep_null: bool = False) -> T: + """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + ... + + @abc.abstractmethod + def unpivot( + self: T, + row_labels: typing.Sequence[typing.Hashable], + unpivot_columns: typing.Sequence[ + typing.Tuple[str, typing.Sequence[typing.Optional[str]]] + ], + *, + passthrough_columns: typing.Sequence[str] = (), + index_col_ids: typing.Sequence[str] = ["index"], + dtype: typing.Union[ + bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] + ] = pandas.Float64Dtype(), + how="left", + ) -> T: + """ + Unpivot ArrayValue columns. + + Args: + row_labels: Identifies the source of the row. Must be equal to length to source column list in unpivot_columns argument. + unpivot_columns: Mapping of column id to list of input column ids. Lists of input columns may use None. + passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. + index_col_id (str): The column id to be used for the row labels. + dtype (dtype or list of dtype): Dtype to use for the unpivot columns. If list, must be equal in number to unpivot_columns. + + Returns: + ArrayValue: The unpivoted ArrayValue + """ + ... + + @abc.abstractmethod + def _reproject_to_table(self: T) -> T: + """ + Internal operators that projects the internal representation into a + new ibis table expression where each value column is a direct + reference to a column in that table expression. Needed after + some operations such as window operations that cannot be used + recursively in projections. + """ + ... + + def project_unary_op( + self: T, + input_column_id: str, + op: ops.UnaryOp, + output_column_id: typing.Optional[str] = None, + ) -> T: + """Creates a new expression based on this expression with unary operation applied to one column.""" + result_id = ( + output_column_id or input_column_id + ) # overwrite input if not output id provided + value = op._as_ibis(self._get_ibis_column(input_column_id)).name(result_id) + return self._set_or_replace_by_id(result_id, value) + + def project_binary_op( + self: T, + left_column_id: str, + right_column_id: str, + op: ops.BinaryOp, + output_column_id: str, + ) -> T: + """Creates a new expression based on this expression with binary operation applied to two columns.""" + value = op( + self._get_ibis_column(left_column_id), + self._get_ibis_column(right_column_id), + ).name(output_column_id) + return self._set_or_replace_by_id(output_column_id, value) + + def project_ternary_op( + self: T, + col_id_1: str, + col_id_2: str, + col_id_3: str, + op: ops.TernaryOp, + output_column_id: str, + ) -> T: + """Creates a new expression based on this expression with ternary operation applied to three columns.""" + value = op( + self._get_ibis_column(col_id_1), + self._get_ibis_column(col_id_2), + self._get_ibis_column(col_id_3), + ).name(output_column_id) + return self._set_or_replace_by_id(output_column_id, value) + + def assign(self: T, source_id: str, destination_id: str) -> T: + return self._set_or_replace_by_id( + destination_id, self._get_ibis_column(source_id) + ) + + def assign_constant( + self: T, + destination_id: str, + value: typing.Any, + dtype: typing.Optional[bigframes.dtypes.Dtype], + ) -> T: + # TODO(b/281587571): Solve scalar constant aggregation problem w/Ibis. + ibis_value = bigframes.dtypes.literal_to_ibis_scalar(value, dtype) + if ibis_value is None: + raise NotImplementedError( + f"Type not supported as scalar value {type(value)}. {constants.FEEDBACK_LINK}" + ) + expr = self._set_or_replace_by_id(destination_id, ibis_value) + return expr._reproject_to_table() + + @abc.abstractmethod + def _set_or_replace_by_id(self: T, id: str, new_value: ibis_types.Value) -> T: + ... + + def _get_ibis_column(self, key: str) -> ibis_types.Value: + """Gets the Ibis expression for a given column.""" + if key not in self.column_ids: + raise ValueError( + "Column name {} not in set of values: {}".format(key, self.column_ids) + ) + return typing.cast(ibis_types.Value, self._column_names[key]) + + def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: + ibis_type = typing.cast( + bigframes.dtypes.IbisDtype, self._get_ibis_column(key).type() + ) + return typing.cast( + bigframes.dtypes.Dtype, + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), + ) + + +# Ibis Implementations +class UnorderedIR(BaseIbisIR): + def __init__( + self, + table: ibis_types.Table, + columns: Sequence[ibis_types.Value], + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + ): + super().__init__(table, columns, predicates) + + def builder(self): + """Creates a mutable builder for expressions.""" + # Since ArrayValue is intended to be immutable (immutability offers + # potential opportunities for caching, though we might need to introduce + # more node types for that to be useful), we create a builder class. + return UnorderedIR.Builder( + self._table, + columns=self._columns, + predicates=self._predicates, + ) + + def to_sql( + self, + offset_column: typing.Optional[str] = None, + col_id_overrides: typing.Mapping[str, str] = {}, + sorted: bool = False, + ) -> str: + if offset_column or sorted: + raise ValueError("Cannot produce sorted sql in unordered mode") + sql = ibis_bigquery.Backend().compile( + self._to_ibis_expr( + col_id_overrides=col_id_overrides, + ) + ) + return typing.cast(str, sql) + + def _to_ibis_expr( + self, + *, + expose_hidden_cols: bool = False, + fraction: Optional[float] = None, + col_id_overrides: typing.Mapping[str, str] = {}, + ): + """ + Creates an Ibis table expression representing the DataFrame. + + ArrayValue objects are sorted, so the following options are available + to reflect this in the ibis expression. + + * "offset_col": Zero-based offsets are generated as a column, this will + not sort the rows however. + * "string_encoded": An ordered string column is provided in output table. + * "unordered": No ordering information will be provided in output. Only + value columns are projected. + + For offset or ordered column, order_col_name can be used to assign the + output label for the ordering column. If none is specified, the default + column name will be 'bigframes_ordering_id' + + Args: + expose_hidden_cols: + If True, include the hidden ordering columns in the results. + Only compatible with `order_by` and `unordered` + ``ordering_mode``. + col_id_overrides: + overrides the column ids for the result + Returns: + An ibis expression representing the data help by the ArrayValue object. + """ + columns = list(self._columns) + columns_to_drop: list[ + str + ] = [] # Ordering/Filtering columns that will be dropped at end + + if self._reduced_predicate is not None: + columns.append(self._reduced_predicate) + # Usually drop predicate as it is will be all TRUE after filtering + if not expose_hidden_cols: + columns_to_drop.append(self._reduced_predicate.get_name()) + + # Special case for empty tables, since we can't create an empty + # projection. + if not columns: + return ibis.memtable([]) + + # Make sure all dtypes are the "canonical" ones for BigFrames. This is + # important for operations like UNION where the schema must match. + table = self._table.select( + bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns + ) + base_table = table + if self._reduced_predicate is not None: + table = table.filter(base_table[PREDICATE_COLUMN]) + table = table.drop(*columns_to_drop) + if col_id_overrides: + table = table.relabel(col_id_overrides) + if fraction is not None: + table = table.filter(ibis.random() < ibis.literal(fraction)) + return table + + def select_columns(self, column_ids: typing.Sequence[str]) -> UnorderedIR: + """Creates a new expression based on this expression with new columns.""" + columns = [self._get_ibis_column(col_id) for col_id in column_ids] + builder = self.builder() + builder.columns = list(columns) + new_expr = builder.build() + return new_expr + + def filter(self, predicate_id: str, keep_null: bool = False) -> UnorderedIR: + condition = typing.cast( + ibis_types.BooleanValue, self._get_ibis_column(predicate_id) + ) + if keep_null: + condition = typing.cast( + ibis_types.BooleanValue, + condition.fillna( + typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) + ), + ) + return self._filter(condition) + + def _filter(self, predicate_value: ibis_types.BooleanValue) -> UnorderedIR: + """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + expr = self.builder() + expr.predicates = [*self._predicates, predicate_value] + return expr.build() + + def unpivot( + self, + row_labels: typing.Sequence[typing.Hashable], + unpivot_columns: typing.Sequence[ + typing.Tuple[str, typing.Sequence[typing.Optional[str]]] + ], + *, + passthrough_columns: typing.Sequence[str] = (), + index_col_ids: typing.Sequence[str] = ["index"], + dtype: typing.Union[ + bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] + ] = pandas.Float64Dtype(), + how="left", + ) -> UnorderedIR: + if how not in ("left", "right"): + raise ValueError("'how' must be 'left' or 'right'") + table = self._to_ibis_expr() + row_n = len(row_labels) + if not all( + len(source_columns) == row_n for _, source_columns in unpivot_columns + ): + raise ValueError("Columns and row labels must all be same length.") + + unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") + unpivot_table = table.cross_join( + ibis.memtable({unpivot_offset_id: range(row_n)}) + ) + # Use ibis memtable to infer type of rowlabels (if possible) + # TODO: Allow caller to specify dtype + if isinstance(row_labels[0], tuple): + labels_table = ibis.memtable(row_labels) + labels_ibis_types = [ + labels_table[col].type() for col in labels_table.columns + ] + else: + labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] + labels_dtypes = [ + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) + for ibis_type in labels_ibis_types + ] + + label_columns = [] + for label_part, (col_id, label_dtype) in enumerate( + zip(index_col_ids, labels_dtypes) + ): + # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels + labels_as_tuples = [ + label if isinstance(label, tuple) else (label,) for label in row_labels + ] + cases = [ + ( + i, + bigframes.dtypes.literal_to_ibis_scalar( + label_tuple[label_part], # type:ignore + force_dtype=label_dtype, # type:ignore + ), + ) + for i, label_tuple in enumerate(labels_as_tuples) + ] + labels_value = ( + typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) + .cases(cases, default=None) # type:ignore + .name(col_id) + ) + label_columns.append(labels_value) + + unpivot_values = [] + for j in range(len(unpivot_columns)): + col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype + result_col, source_cols = unpivot_columns[j] + null_value = bigframes.dtypes.literal_to_ibis_scalar( + None, force_dtype=col_dtype + ) + ibis_values = [ + ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) + if col is not None + else null_value + for col in source_cols + ] + cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] + unpivot_value = typing.cast( + ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] + ).cases( + cases, default=null_value # type:ignore + ) + unpivot_values.append(unpivot_value.name(result_col)) + + unpivot_table = unpivot_table.select( + passthrough_columns, + *label_columns, + *unpivot_values, + unpivot_offset_id, + ) + + value_columns = [ + unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns + ] + passthrough_values = [unpivot_table[col] for col in passthrough_columns] + return UnorderedIR( + table=unpivot_table, + columns=[ + *[unpivot_table[col_id] for col_id in index_col_ids], + *value_columns, + *passthrough_values, + ], + ) + + def aggregate( + self, + aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp, str]], + by_column_ids: typing.Sequence[str] = (), + dropna: bool = True, + ) -> OrderedIR: + """ + Apply aggregations to the expression. + Arguments: + aggregations: input_column_id, operation, output_column_id tuples + by_column_id: column id of the aggregation key, this is preserved through the transform + dropna: whether null keys should be dropped + """ + table = self._to_ibis_expr() + stats = { + col_out: agg_op._as_ibis(table[col_in]) + for col_in, agg_op, col_out in aggregations + } + if by_column_ids: + result = table.group_by(by_column_ids).aggregate(**stats) + # Must have deterministic ordering, so order by the unique "by" column + ordering = ExpressionOrdering( + tuple( + [ + OrderingColumnReference(column_id=column_id) + for column_id in by_column_ids + ] + ), + total_ordering_columns=frozenset(by_column_ids), + ) + columns = tuple(result[key] for key in result.columns) + expr = OrderedIR(result, columns=columns, ordering=ordering) + if dropna: + for column_id in by_column_ids: + expr = expr._filter( + ops.notnull_op._as_ibis(expr._get_ibis_column(column_id)) + ) + # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation + return expr._project_offsets() + else: + aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} + result = table.aggregate(**aggregates) + # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. + ordering = ExpressionOrdering( + ordering_value_columns=tuple( + [OrderingColumnReference(ORDER_ID_COLUMN)] + ), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), + ) + return OrderedIR( + result, + columns=[result[col_id] for col_id in [*stats.keys()]], + hidden_ordering_columns=[result[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def corr_aggregate( + self, corr_aggregations: typing.Sequence[typing.Tuple[str, str, str]] + ) -> OrderedIR: + """ + Get correlations between each lef_column_id and right_column_id, stored in the respective output_column_id. + This uses BigQuery's CORR under the hood, and thus only Pearson's method is used. + Arguments: + corr_aggregations: left_column_id, right_column_id, output_column_id tuples + """ + table = self._to_ibis_expr() + stats = { + col_out: table[col_left].corr(table[col_right], how="pop") + for col_left, col_right, col_out in corr_aggregations + } + aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} + result = table.aggregate(**aggregates) + # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. + ordering = ExpressionOrdering( + ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), + ) + return OrderedIR( + result, + columns=[result[col_id] for col_id in [*stats.keys()]], + hidden_ordering_columns=[result[ORDER_ID_COLUMN]], + ordering=ordering, + ) + + def _uniform_sampling(self, fraction: float) -> UnorderedIR: + """Sampling the table on given fraction. + + .. warning:: + The row numbers of result is non-deterministic, avoid to use. + """ + table = self._to_ibis_expr(fraction=fraction) + columns = [table[column_name] for column_name in self._column_names] + return UnorderedIR( + table, + columns=columns, + ) + + ## Helpers + def _set_or_replace_by_id( + self, id: str, new_value: ibis_types.Value + ) -> UnorderedIR: + builder = self.builder() + if id in self.column_ids: + builder.columns = [ + val if (col_id != id) else new_value.name(id) + for col_id, val in zip(self.column_ids, self._columns) + ] + else: + builder.columns = [*self.columns, new_value.name(id)] + return builder.build() + + def _reproject_to_table(self) -> UnorderedIR: + """ + Internal operators that projects the internal representation into a + new ibis table expression where each value column is a direct + reference to a column in that table expression. Needed after + some operations such as window operations that cannot be used + recursively in projections. + """ + table = self._to_ibis_expr() + columns = [table[column_name] for column_name in self._column_names] + return UnorderedIR( + table, + columns=columns, + ) + + class Builder: + def __init__( + self, + table: ibis_types.Table, + columns: Collection[ibis_types.Value] = (), + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, + ): + self.table = table + self.columns = list(columns) + self.predicates = list(predicates) if predicates is not None else None + + def build(self) -> UnorderedIR: + return UnorderedIR( + table=self.table, + columns=self.columns, + predicates=self.predicates, + ) + + +class OrderedIR(BaseIbisIR): """Immutable BigQuery DataFrames expression tree. Note: Usage of this class is considered to be private and subject to change @@ -71,17 +629,11 @@ def __init__( ordering: ExpressionOrdering = ExpressionOrdering(), predicates: Optional[Collection[ibis_types.BooleanValue]] = None, ): - self._table = table - self._predicates = tuple(predicates) if predicates is not None else () + super().__init__(table, columns, predicates) # TODO: Validate ordering if not ordering.total_ordering_columns: raise ValueError("Must have total ordering defined by one or more columns") self._ordering = ordering - # Allow creating a DataFrame directly from an Ibis table expression. - # TODO(swast): Validate that each column references the same table (or - # no table for literal values). - self._columns = tuple(columns) - # Meta columns store ordering, or other data that doesn't correspond to dataframe columns self._hidden_ordering_columns = ( tuple(hidden_ordering_columns) @@ -111,10 +663,10 @@ def __init__( raise ValueError(f"Illegal ordering keys: {ordering.all_ordering_columns}") @classmethod - def mem_expr_from_pandas( + def from_pandas( cls, pd_df: pandas.DataFrame, - ) -> CompiledArrayValue: + ) -> OrderedIR: """ Builds an in-memory only (SQL only) expr from a pandas dataframe. """ @@ -173,27 +725,10 @@ def mem_expr_from_pandas( hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), ) - @property - def columns(self) -> typing.Tuple[ibis_types.Value, ...]: - return self._columns - - @property - def column_ids(self) -> typing.Sequence[str]: - return tuple(self._column_names.keys()) - @property def _hidden_column_ids(self) -> typing.Sequence[str]: return tuple(self._hidden_ordering_column_names.keys()) - @property - def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: - """Returns the frame's predicates as an equivalent boolean value, useful where a single predicate value is preferred.""" - return ( - _reduce_predicate_list(self._predicates).name(PREDICATE_COLUMN) - if self._predicates - else None - ) - @property def _ibis_order(self) -> Sequence[ibis_types.Value]: """Returns a sequence of ibis values which can be directly used to order a table expression. Has direction modifiers applied.""" @@ -202,12 +737,15 @@ def _ibis_order(self) -> Sequence[ibis_types.Value]: self._ordering.all_ordering_columns, ) - def builder(self) -> ArrayValueBuilder: + def to_unordered(self) -> UnorderedIR: + return UnorderedIR(self._table, self._columns, self._predicates) + + def builder(self) -> OrderedIR.Builder: """Creates a mutable builder for expressions.""" # Since ArrayValue is intended to be immutable (immutability offers # potential opportunities for caching, though we might need to introduce # more node types for that to be useful), we create a builder class. - return ArrayValueBuilder( + return OrderedIR.Builder( self._table, columns=self._columns, hidden_ordering_columns=self._hidden_ordering_columns, @@ -215,160 +753,39 @@ def builder(self) -> ArrayValueBuilder: predicates=self._predicates, ) - def drop_columns(self, columns: Iterable[str]) -> CompiledArrayValue: - # Must generate offsets if we are dropping a column that ordering depends on - expr = self - for ordering_column in set(columns).intersection( - [col.column_id for col in self._ordering.ordering_value_columns] - ): - expr = self._hide_column(ordering_column) - - expr_builder = expr.builder() - remain_cols = [ - column for column in expr.columns if column.get_name() not in columns - ] - expr_builder.columns = remain_cols - return expr_builder.build() - - def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: - ibis_type = typing.cast( - bigframes.dtypes.IbisDtype, self._get_any_column(key).type() - ) - return typing.cast( - bigframes.dtypes.Dtype, - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), - ) - - def _get_ibis_column(self, key: str) -> ibis_types.Value: - """Gets the Ibis expression for a given column.""" - if key not in self.column_ids: - raise ValueError( - "Column name {} not in set of values: {}".format(key, self.column_ids) - ) - return typing.cast(ibis_types.Value, self._column_names[key]) - - def _get_any_column(self, key: str) -> ibis_types.Value: - """Gets the Ibis expression for a given column. Will also get hidden columns.""" - all_columns = {**self._column_names, **self._hidden_ordering_column_names} - if key not in all_columns.keys(): - raise ValueError( - "Column name {} not in set of values: {}".format( - key, all_columns.keys() - ) - ) - return typing.cast(ibis_types.Value, all_columns[key]) - - def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: - """Gets the Ibis expression for a given hidden column.""" - if key not in self._hidden_ordering_column_names.keys(): - raise ValueError( - "Column name {} not in set of values: {}".format( - key, self._hidden_ordering_column_names.keys() - ) - ) - return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) - - def filter(self, predicate_id: str, keep_null: bool = False) -> CompiledArrayValue: - """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - condition = typing.cast( - ibis_types.BooleanValue, self._get_ibis_column(predicate_id) - ) - if keep_null: - condition = typing.cast( - ibis_types.BooleanValue, - condition.fillna( - typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) - ), - ) - return self._filter(condition) - - def _filter(self, predicate_value: ibis_types.BooleanValue) -> CompiledArrayValue: - """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - expr = self.builder() - expr.ordering = expr.ordering.with_non_sequential() - expr.predicates = [*self._predicates, predicate_value] - return expr.build() - def order_by( self, by: Sequence[OrderingColumnReference], stable: bool = False - ) -> CompiledArrayValue: + ) -> OrderedIR: expr_builder = self.builder() expr_builder.ordering = self._ordering.with_ordering_columns(by, stable=stable) return expr_builder.build() - def reversed(self) -> CompiledArrayValue: + def reversed(self) -> OrderedIR: expr_builder = self.builder() expr_builder.ordering = self._ordering.with_reverse() return expr_builder.build() - def _uniform_sampling(self, fraction: float) -> CompiledArrayValue: + def _uniform_sampling(self, fraction: float) -> OrderedIR: """Sampling the table on given fraction. .. warning:: The row numbers of result is non-deterministic, avoid to use. """ table = self._to_ibis_expr( - "unordered", expose_hidden_cols=True, fraction=fraction + ordering_mode="unordered", expose_hidden_cols=True, fraction=fraction ) columns = [table[column_name] for column_name in self._column_names] hidden_ordering_columns = [ table[column_name] for column_name in self._hidden_ordering_column_names ] - return CompiledArrayValue( + return OrderedIR( table, columns=columns, hidden_ordering_columns=hidden_ordering_columns, ordering=self._ordering, ) - @property - def _offsets(self) -> ibis_types.IntegerColumn: - if not self._ordering.is_sequential: - raise ValueError( - "Expression does not have offsets. Generate them first using project_offsets." - ) - if not self._ordering.total_order_col: - raise ValueError( - "Ordering is invalid. Marked as sequential but no total order columns." - ) - column = self._get_any_column(self._ordering.total_order_col.column_id) - return typing.cast(ibis_types.IntegerColumn, column) - - def _project_offsets(self) -> CompiledArrayValue: - """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" - if self._ordering.is_sequential: - return self - # TODO(tbergeron): Enforce total ordering - table = self._to_ibis_expr( - ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN - ) - columns = [table[column_name] for column_name in self._column_names] - ordering = ExpressionOrdering( - ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(True, is_sequential=True), - ) - return CompiledArrayValue( - table, - columns=columns, - hidden_ordering_columns=[table[ORDER_ID_COLUMN]], - ordering=ordering, - ) - - def _hide_column(self, column_id) -> CompiledArrayValue: - """Pushes columns to hidden columns list. Used to hide ordering columns that have been dropped or destructively mutated.""" - expr_builder = self.builder() - # Need to rename column as caller might be creating a new row with the same name but different values. - # Can avoid this if don't allow callers to determine ids and instead generate unique ones in this class. - new_name = bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") - expr_builder.hidden_ordering_columns = [ - *self._hidden_ordering_columns, - self._get_ibis_column(column_id).name(new_name), - ] - expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) - return expr_builder.build() - - def promote_offsets(self, col_id: str) -> CompiledArrayValue: + def promote_offsets(self, col_id: str) -> OrderedIR: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. """ @@ -384,194 +801,21 @@ def promote_offsets(self, col_id: str) -> CompiledArrayValue: ] return expr_builder.build() - def select_columns(self, column_ids: typing.Sequence[str]) -> CompiledArrayValue: + def select_columns(self, column_ids: typing.Sequence[str]) -> OrderedIR: """Creates a new expression based on this expression with new columns.""" columns = [self._get_ibis_column(col_id) for col_id in column_ids] expr = self for ordering_column in set(self.column_ids).intersection( [col_ref.column_id for col_ref in self._ordering.ordering_value_columns] - ): - # Need to hide ordering columns that are being dropped. Alternatively, could project offsets - expr = expr._hide_column(ordering_column) - builder = expr.builder() - builder.columns = list(columns) - new_expr = builder.build() - return new_expr - - def concat(self, other: typing.Sequence[CompiledArrayValue]) -> CompiledArrayValue: - """Append together multiple ArrayValue objects.""" - if len(other) == 0: - return self - tables = [] - prefix_base = 10 - prefix_size = math.ceil(math.log(len(other) + 1, prefix_base)) - # Must normalize all ids to the same encoding size - max_encoding_size = max( - self._ordering.string_encoding.length, - *[expression._ordering.string_encoding.length for expression in other], - ) - for i, expr in enumerate([self, *other]): - ordering_prefix = str(i).zfill(prefix_size) - table = expr._to_ibis_expr( - ordering_mode="string_encoded", order_col_name=ORDER_ID_COLUMN - ) - # Rename the value columns based on horizontal offset before applying union. - table = table.select( - [ - table[col].name(f"column_{i}") - if col != ORDER_ID_COLUMN - else ( - ordering_prefix - + reencode_order_string( - table[ORDER_ID_COLUMN], max_encoding_size - ) - ).name(ORDER_ID_COLUMN) - for i, col in enumerate(table.columns) - ] - ) - tables.append(table) - combined_table = ibis.union(*tables) - ordering = ExpressionOrdering( - ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - string_encoding=StringEncoding(True, prefix_size + max_encoding_size), - ) - return CompiledArrayValue( - combined_table, - columns=[ - combined_table[col] - for col in combined_table.columns - if col != ORDER_ID_COLUMN - ], - hidden_ordering_columns=[combined_table[ORDER_ID_COLUMN]], - ordering=ordering, - ) - - def project_unary_op( - self, column_name: str, op: ops.UnaryOp, output_name=None - ) -> CompiledArrayValue: - """Creates a new expression based on this expression with unary operation applied to one column.""" - value = op._as_ibis(self._get_ibis_column(column_name)).name( - output_name or column_name - ) - return self._set_or_replace_by_id(output_name or column_name, value) - - def project_binary_op( - self, - left_column_id: str, - right_column_id: str, - op: ops.BinaryOp, - output_column_id: str, - ) -> CompiledArrayValue: - """Creates a new expression based on this expression with binary operation applied to two columns.""" - value = op( - self._get_ibis_column(left_column_id), - self._get_ibis_column(right_column_id), - ).name(output_column_id) - return self._set_or_replace_by_id(output_column_id, value) - - def project_ternary_op( - self, - col_id_1: str, - col_id_2: str, - col_id_3: str, - op: ops.TernaryOp, - output_column_id: str, - ) -> CompiledArrayValue: - """Creates a new expression based on this expression with ternary operation applied to three columns.""" - value = op( - self._get_ibis_column(col_id_1), - self._get_ibis_column(col_id_2), - self._get_ibis_column(col_id_3), - ).name(output_column_id) - return self._set_or_replace_by_id(output_column_id, value) - - def aggregate( - self, - aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp, str]], - by_column_ids: typing.Sequence[str] = (), - dropna: bool = True, - ) -> CompiledArrayValue: - """ - Apply aggregations to the expression. - Arguments: - aggregations: input_column_id, operation, output_column_id tuples - by_column_id: column id of the aggregation key, this is preserved through the transform - dropna: whether null keys should be dropped - """ - table = self._to_ibis_expr("unordered") - stats = { - col_out: agg_op._as_ibis(table[col_in]) - for col_in, agg_op, col_out in aggregations - } - if by_column_ids: - result = table.group_by(by_column_ids).aggregate(**stats) - # Must have deterministic ordering, so order by the unique "by" column - ordering = ExpressionOrdering( - tuple( - [ - OrderingColumnReference(column_id=column_id) - for column_id in by_column_ids - ] - ), - total_ordering_columns=frozenset(by_column_ids), - ) - columns = tuple(result[key] for key in result.columns) - expr = CompiledArrayValue(result, columns=columns, ordering=ordering) - if dropna: - for column_id in by_column_ids: - expr = expr._filter( - ops.notnull_op._as_ibis(expr._get_ibis_column(column_id)) - ) - # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation - return expr._project_offsets() - else: - aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} - result = table.aggregate(**aggregates) - # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. - ordering = ExpressionOrdering( - ordering_value_columns=tuple( - [OrderingColumnReference(ORDER_ID_COLUMN)] - ), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), - ) - return CompiledArrayValue( - result, - columns=[result[col_id] for col_id in [*stats.keys()]], - hidden_ordering_columns=[result[ORDER_ID_COLUMN]], - ordering=ordering, - ) - - def corr_aggregate( - self, corr_aggregations: typing.Sequence[typing.Tuple[str, str, str]] - ) -> CompiledArrayValue: - """ - Get correlations between each lef_column_id and right_column_id, stored in the respective output_column_id. - This uses BigQuery's CORR under the hood, and thus only Pearson's method is used. - Arguments: - corr_aggregations: left_column_id, right_column_id, output_column_id tuples - """ - table = self._to_ibis_expr("unordered") - stats = { - col_out: table[col_left].corr(table[col_right], how="pop") - for col_left, col_right, col_out in corr_aggregations - } - aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} - result = table.aggregate(**aggregates) - # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. - ordering = ExpressionOrdering( - ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), - ) - return CompiledArrayValue( - result, - columns=[result[col_id] for col_id in [*stats.keys()]], - hidden_ordering_columns=[result[ORDER_ID_COLUMN]], - ordering=ordering, - ) + ): + # Need to hide ordering columns that are being dropped. Alternatively, could project offsets + expr = expr._hide_column(ordering_column) + builder = expr.builder() + builder.columns = list(columns) + new_expr = builder.build() + return new_expr + ## Methods that only work with ordering def project_window_op( self, column_name: str, @@ -581,7 +825,7 @@ def project_window_op( *, never_skip_nulls=False, skip_reproject_unsafe: bool = False, - ) -> CompiledArrayValue: + ) -> OrderedIR: """ Creates a new expression based on this expression with unary operation applied to one column. column_name: the id of the input column present in the expression @@ -625,6 +869,168 @@ def project_window_op( # TODO(tbergeron): Automatically track analytic expression usage and defer reprojection until required for valid query generation. return result._reproject_to_table() if not skip_reproject_unsafe else result + def unpivot( + self, + row_labels: typing.Sequence[typing.Hashable], + unpivot_columns: typing.Sequence[ + typing.Tuple[str, typing.Sequence[typing.Optional[str]]] + ], + *, + passthrough_columns: typing.Sequence[str] = (), + index_col_ids: typing.Sequence[str] = ["index"], + dtype: typing.Union[ + bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] + ] = pandas.Float64Dtype(), + how="left", + ) -> OrderedIR: + if how not in ("left", "right"): + raise ValueError("'how' must be 'left' or 'right'") + table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) + row_n = len(row_labels) + hidden_col_ids = self._hidden_ordering_column_names.keys() + if not all( + len(source_columns) == row_n for _, source_columns in unpivot_columns + ): + raise ValueError("Columns and row labels must all be same length.") + + unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") + unpivot_table = table.cross_join( + ibis.memtable({unpivot_offset_id: range(row_n)}) + ) + # Use ibis memtable to infer type of rowlabels (if possible) + # TODO: Allow caller to specify dtype + if isinstance(row_labels[0], tuple): + labels_table = ibis.memtable(row_labels) + labels_ibis_types = [ + labels_table[col].type() for col in labels_table.columns + ] + else: + labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] + labels_dtypes = [ + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) + for ibis_type in labels_ibis_types + ] + + label_columns = [] + for label_part, (col_id, label_dtype) in enumerate( + zip(index_col_ids, labels_dtypes) + ): + # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels + labels_as_tuples = [ + label if isinstance(label, tuple) else (label,) for label in row_labels + ] + cases = [ + ( + i, + bigframes.dtypes.literal_to_ibis_scalar( + label_tuple[label_part], # type:ignore + force_dtype=label_dtype, # type:ignore + ), + ) + for i, label_tuple in enumerate(labels_as_tuples) + ] + labels_value = ( + typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) + .cases(cases, default=None) # type:ignore + .name(col_id) + ) + label_columns.append(labels_value) + + unpivot_values = [] + for j in range(len(unpivot_columns)): + col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype + result_col, source_cols = unpivot_columns[j] + null_value = bigframes.dtypes.literal_to_ibis_scalar( + None, force_dtype=col_dtype + ) + ibis_values = [ + ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) + if col is not None + else null_value + for col in source_cols + ] + cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] + unpivot_value = typing.cast( + ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] + ).cases( + cases, default=null_value # type:ignore + ) + unpivot_values.append(unpivot_value.name(result_col)) + + unpivot_table = unpivot_table.select( + passthrough_columns, + *label_columns, + *unpivot_values, + *hidden_col_ids, + unpivot_offset_id, + ) + + # Extend the original ordering using unpivot_offset_id + old_ordering = self._ordering + if how == "left": + new_ordering = ExpressionOrdering( + ordering_value_columns=tuple( + [ + *old_ordering.ordering_value_columns, + OrderingColumnReference(unpivot_offset_id), + ] + ), + total_ordering_columns=frozenset( + [*old_ordering.total_ordering_columns, unpivot_offset_id] + ), + ) + else: # how=="right" + new_ordering = ExpressionOrdering( + ordering_value_columns=tuple( + [ + OrderingColumnReference(unpivot_offset_id), + *old_ordering.ordering_value_columns, + ] + ), + total_ordering_columns=frozenset( + [*old_ordering.total_ordering_columns, unpivot_offset_id] + ), + ) + value_columns = [ + unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns + ] + passthrough_values = [unpivot_table[col] for col in passthrough_columns] + hidden_ordering_columns = [ + unpivot_table[unpivot_offset_id], + *[unpivot_table[hidden_col] for hidden_col in hidden_col_ids], + ] + return OrderedIR( + table=unpivot_table, + columns=[ + *[unpivot_table[col_id] for col_id in index_col_ids], + *value_columns, + *passthrough_values, + ], + hidden_ordering_columns=hidden_ordering_columns, + ordering=new_ordering, + ) + + def _reproject_to_table(self) -> OrderedIR: + table = self._to_ibis_expr( + ordering_mode="unordered", + expose_hidden_cols=True, + ) + columns = [table[column_name] for column_name in self._column_names] + ordering_col_ids = [ + ref.column_id for ref in self._ordering.all_ordering_columns + ] + hidden_ordering_columns = [ + table[column_name] + for column_name in self._hidden_ordering_column_names + if column_name in ordering_col_ids + ] + return OrderedIR( + table, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=self._ordering, + ) + def to_sql( self, offset_column: typing.Optional[str] = None, @@ -644,21 +1050,22 @@ def to_sql( ) if sorted: sql = textwrap.dedent( - f""" - SELECT * EXCEPT (`{offsets_id}`) - FROM ({sql}) - ORDER BY `{offsets_id}` - """ + f"SELECT * EXCEPT (`{offsets_id}`)\n" + "FROM (\n" + f"{sql}\n" + ")\n" + f"ORDER BY `{offsets_id}`\n" ) return typing.cast(str, sql) def _to_ibis_expr( self, - ordering_mode: Literal["string_encoded", "offset_col", "unordered"], - order_col_name: Optional[str] = ORDER_ID_COLUMN, + *, expose_hidden_cols: bool = False, fraction: Optional[float] = None, col_id_overrides: typing.Mapping[str, str] = {}, + ordering_mode: Literal["string_encoded", "offset_col", "unordered"], + order_col_name: Optional[str] = ORDER_ID_COLUMN, ): """ Creates an Ibis table expression representing the DataFrame. @@ -677,16 +1084,16 @@ def _to_ibis_expr( column name will be 'bigframes_ordering_id' Args: + expose_hidden_cols: + If True, include the hidden ordering columns in the results. + Only compatible with `order_by` and `unordered` + ``ordering_mode``. ordering_mode: How to construct the Ibis expression from the ArrayValue. See above for details. order_col_name: If the ordering mode outputs a single ordering or offsets column, use this as the column name. - expose_hidden_cols: - If True, include the hidden ordering columns in the results. - Only compatible with `order_by` and `unordered` - ``ordering_mode``. col_id_overrides: overrides the column ids for the result Returns: @@ -723,20 +1130,115 @@ def _to_ibis_expr( if not columns: return ibis.memtable([]) - # Make sure all dtypes are the "canonical" ones for BigFrames. This is - # important for operations like UNION where the schema must match. - table = self._table.select( - bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns + # Make sure all dtypes are the "canonical" ones for BigFrames. This is + # important for operations like UNION where the schema must match. + table = self._table.select( + bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns + ) + base_table = table + if self._reduced_predicate is not None: + table = table.filter(base_table[PREDICATE_COLUMN]) + table = table.drop(*columns_to_drop) + if col_id_overrides: + table = table.relabel(col_id_overrides) + if fraction is not None: + table = table.filter(ibis.random() < ibis.literal(fraction)) + return table + + def filter(self, predicate_id: str, keep_null: bool = False) -> OrderedIR: + condition = typing.cast( + ibis_types.BooleanValue, self._get_ibis_column(predicate_id) + ) + if keep_null: + condition = typing.cast( + ibis_types.BooleanValue, + condition.fillna( + typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) + ), + ) + return self._filter(condition) + + def _filter(self, predicate_value: ibis_types.BooleanValue) -> OrderedIR: + """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + expr = self.builder() + expr.ordering = expr.ordering.with_non_sequential() + expr.predicates = [*self._predicates, predicate_value] + return expr.build() + + def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> OrderedIR: + """Safely assign by id while maintaining ordering integrity.""" + # TODO: Split into explicit set and replace methods + ordering_col_ids = [ + col_ref.column_id for col_ref in self._ordering.ordering_value_columns + ] + if id in ordering_col_ids: + return self._hide_column(id)._set_or_replace_by_id(id, new_value) + + builder = self.builder() + if id in self.column_ids: + builder.columns = [ + val if (col_id != id) else new_value.name(id) + for col_id, val in zip(self.column_ids, self._columns) + ] + else: + builder.columns = [*self.columns, new_value.name(id)] + return builder.build() + + ## Ordering specific helpers + def _get_any_column(self, key: str) -> ibis_types.Value: + """Gets the Ibis expression for a given column. Will also get hidden columns.""" + all_columns = {**self._column_names, **self._hidden_ordering_column_names} + if key not in all_columns.keys(): + raise ValueError( + "Column name {} not in set of values: {}".format( + key, all_columns.keys() + ) + ) + return typing.cast(ibis_types.Value, all_columns[key]) + + def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: + """Gets the Ibis expression for a given hidden column.""" + if key not in self._hidden_ordering_column_names.keys(): + raise ValueError( + "Column name {} not in set of values: {}".format( + key, self._hidden_ordering_column_names.keys() + ) + ) + return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) + + def _hide_column(self, column_id) -> OrderedIR: + """Pushes columns to hidden columns list. Used to hide ordering columns that have been dropped or destructively mutated.""" + expr_builder = self.builder() + # Need to rename column as caller might be creating a new row with the same name but different values. + # Can avoid this if don't allow callers to determine ids and instead generate unique ones in this class. + new_name = bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") + expr_builder.hidden_ordering_columns = [ + *self._hidden_ordering_columns, + self._get_ibis_column(column_id).name(new_name), + ] + expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) + return expr_builder.build() + + def _project_offsets(self) -> OrderedIR: + """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" + if self._ordering.is_sequential: + return self + # TODO(tbergeron): Enforce total ordering + table = self._to_ibis_expr( + ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN + ) + columns = [table[column_name] for column_name in self._column_names] + ordering = ExpressionOrdering( + ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + integer_encoding=IntegerEncoding(True, is_sequential=True), + ) + return OrderedIR( + table, + columns=columns, + hidden_ordering_columns=[table[ORDER_ID_COLUMN]], + ordering=ordering, ) - base_table = table - if self._reduced_predicate is not None: - table = table.filter(base_table[PREDICATE_COLUMN]) - table = table.drop(*columns_to_drop) - if col_id_overrides: - table = table.relabel(col_id_overrides) - if fraction is not None: - table = table.filter(ibis.random() < ibis.literal(fraction)) - return table def _create_order_columns( self, @@ -789,34 +1291,6 @@ def _create_string_ordering_column(self) -> ibis_types.StringColumn: ) return encode_order_string(row_nums) - def _reproject_to_table(self) -> CompiledArrayValue: - """ - Internal operators that projects the internal representation into a - new ibis table expression where each value column is a direct - reference to a column in that table expression. Needed after - some operations such as window operations that cannot be used - recursively in projections. - """ - table = self._to_ibis_expr( - "unordered", - expose_hidden_cols=True, - ) - columns = [table[column_name] for column_name in self._column_names] - ordering_col_ids = [ - ref.column_id for ref in self._ordering.all_ordering_columns - ] - hidden_ordering_columns = [ - table[column_name] - for column_name in self._hidden_ordering_column_names - if column_name in ordering_col_ids - ] - return CompiledArrayValue( - table, - columns=columns, - hidden_ordering_columns=hidden_ordering_columns, - ordering=self._ordering, - ) - def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = False): group_by: typing.List[ibis_types.Value] = ( [ @@ -851,229 +1325,29 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = Fal group_by=group_by, ) - def unpivot( - self, - row_labels: typing.Sequence[typing.Hashable], - unpivot_columns: typing.Sequence[ - typing.Tuple[str, typing.Sequence[typing.Optional[str]]] - ], - *, - passthrough_columns: typing.Sequence[str] = (), - index_col_ids: typing.Sequence[str] = ["index"], - dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] - ] = pandas.Float64Dtype(), - how="left", - ) -> CompiledArrayValue: - """ - Unpivot ArrayValue columns. - - Args: - row_labels: Identifies the source of the row. Must be equal to length to source column list in unpivot_columns argument. - unpivot_columns: Mapping of column id to list of input column ids. Lists of input columns may use None. - passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. - index_col_id (str): The column id to be used for the row labels. - dtype (dtype or list of dtype): Dtype to use for the unpivot columns. If list, must be equal in number to unpivot_columns. - - Returns: - ArrayValue: The unpivoted ArrayValue - """ - if how not in ("left", "right"): - raise ValueError("'how' must be 'left' or 'right'") - table = self._to_ibis_expr("unordered", expose_hidden_cols=True) - row_n = len(row_labels) - hidden_col_ids = self._hidden_ordering_column_names.keys() - if not all( - len(source_columns) == row_n for _, source_columns in unpivot_columns - ): - raise ValueError("Columns and row labels must all be same length.") - - unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") - unpivot_table = table.cross_join( - ibis.memtable({unpivot_offset_id: range(row_n)}) - ) - # Use ibis memtable to infer type of rowlabels (if possible) - # TODO: Allow caller to specify dtype - if isinstance(row_labels[0], tuple): - labels_table = ibis.memtable(row_labels) - labels_ibis_types = [ - labels_table[col].type() for col in labels_table.columns - ] - else: - labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] - labels_dtypes = [ - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) - for ibis_type in labels_ibis_types - ] - - label_columns = [] - for label_part, (col_id, label_dtype) in enumerate( - zip(index_col_ids, labels_dtypes) + class Builder: + def __init__( + self, + table: ibis_types.Table, + ordering: ExpressionOrdering, + columns: Collection[ibis_types.Value] = (), + hidden_ordering_columns: Collection[ibis_types.Value] = (), + predicates: Optional[Collection[ibis_types.BooleanValue]] = None, ): - # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels - labels_as_tuples = [ - label if isinstance(label, tuple) else (label,) for label in row_labels - ] - cases = [ - ( - i, - bigframes.dtypes.literal_to_ibis_scalar( - label_tuple[label_part], # type:ignore - force_dtype=label_dtype, # type:ignore - ), - ) - for i, label_tuple in enumerate(labels_as_tuples) - ] - labels_value = ( - typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) - .cases(cases, default=None) # type:ignore - .name(col_id) - ) - label_columns.append(labels_value) - - unpivot_values = [] - for j in range(len(unpivot_columns)): - col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype - result_col, source_cols = unpivot_columns[j] - null_value = bigframes.dtypes.literal_to_ibis_scalar( - None, force_dtype=col_dtype - ) - ibis_values = [ - ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) - if col is not None - else null_value - for col in source_cols - ] - cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] - unpivot_value = typing.cast( - ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] - ).cases( - cases, default=null_value # type:ignore - ) - unpivot_values.append(unpivot_value.name(result_col)) - - unpivot_table = unpivot_table.select( - passthrough_columns, - *label_columns, - *unpivot_values, - *hidden_col_ids, - unpivot_offset_id, - ) - - # Extend the original ordering using unpivot_offset_id - old_ordering = self._ordering - if how == "left": - new_ordering = ExpressionOrdering( - ordering_value_columns=tuple( - [ - *old_ordering.ordering_value_columns, - OrderingColumnReference(unpivot_offset_id), - ] - ), - total_ordering_columns=frozenset( - [*old_ordering.total_ordering_columns, unpivot_offset_id] - ), - ) - else: # how=="right" - new_ordering = ExpressionOrdering( - ordering_value_columns=tuple( - [ - OrderingColumnReference(unpivot_offset_id), - *old_ordering.ordering_value_columns, - ] - ), - total_ordering_columns=frozenset( - [*old_ordering.total_ordering_columns, unpivot_offset_id] - ), - ) - value_columns = [ - unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns - ] - passthrough_values = [unpivot_table[col] for col in passthrough_columns] - hidden_ordering_columns = [ - unpivot_table[unpivot_offset_id], - *[unpivot_table[hidden_col] for hidden_col in hidden_col_ids], - ] - return CompiledArrayValue( - table=unpivot_table, - columns=[ - *[unpivot_table[col_id] for col_id in index_col_ids], - *value_columns, - *passthrough_values, - ], - hidden_ordering_columns=hidden_ordering_columns, - ordering=new_ordering, - ) - - def assign(self, source_id: str, destination_id: str) -> CompiledArrayValue: - return self._set_or_replace_by_id( - destination_id, self._get_ibis_column(source_id) - ) - - def assign_constant( - self, - destination_id: str, - value: typing.Any, - dtype: typing.Optional[bigframes.dtypes.Dtype], - ) -> CompiledArrayValue: - # TODO(b/281587571): Solve scalar constant aggregation problem w/Ibis. - ibis_value = bigframes.dtypes.literal_to_ibis_scalar(value, dtype) - if ibis_value is None: - raise NotImplementedError( - f"Type not supported as scalar value {type(value)}. {constants.FEEDBACK_LINK}" + self.table = table + self.columns = list(columns) + self.hidden_ordering_columns = list(hidden_ordering_columns) + self.ordering = ordering + self.predicates = list(predicates) if predicates is not None else None + + def build(self) -> OrderedIR: + return OrderedIR( + table=self.table, + columns=self.columns, + hidden_ordering_columns=self.hidden_ordering_columns, + ordering=self.ordering, + predicates=self.predicates, ) - expr = self._set_or_replace_by_id(destination_id, ibis_value) - return expr._reproject_to_table() - - def _set_or_replace_by_id( - self, id: str, new_value: ibis_types.Value - ) -> CompiledArrayValue: - """Safely assign by id while maintaining ordering integrity.""" - # TODO: Split into explicit set and replace methods - ordering_col_ids = [ - col_ref.column_id for col_ref in self._ordering.ordering_value_columns - ] - if id in ordering_col_ids: - return self._hide_column(id)._set_or_replace_by_id(id, new_value) - - builder = self.builder() - if id in self.column_ids: - builder.columns = [ - val if (col_id != id) else new_value.name(id) - for col_id, val in zip(self.column_ids, self._columns) - ] - else: - builder.columns = [*self.columns, new_value.name(id)] - return builder.build() - - -class ArrayValueBuilder: - """Mutable expression class. - Use ArrayValue.builder() to create from a ArrayValue object. - """ - - def __init__( - self, - table: ibis_types.Table, - ordering: ExpressionOrdering, - columns: Collection[ibis_types.Value] = (), - hidden_ordering_columns: Collection[ibis_types.Value] = (), - predicates: Optional[Collection[ibis_types.BooleanValue]] = None, - ): - self.table = table - self.columns = list(columns) - self.hidden_ordering_columns = list(hidden_ordering_columns) - self.ordering = ordering - self.predicates = list(predicates) if predicates is not None else None - - def build(self) -> CompiledArrayValue: - return CompiledArrayValue( - table=self.table, - columns=self.columns, - hidden_ordering_columns=self.hidden_ordering_columns, - ordering=self.ordering, - predicates=self.predicates, - ) def _reduce_predicate_list( diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 195d830122a..662e73a4331 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -19,7 +19,8 @@ import pandas as pd -import bigframes.core.compile as compiled +import bigframes.core.compile.compiled as compiled +import bigframes.core.compile.concat as concat_impl import bigframes.core.compile.single_column import bigframes.core.nodes as nodes @@ -28,120 +29,167 @@ import bigframes.session +def compile_ordered(node: nodes.BigFrameNode) -> compiled.OrderedIR: + return typing.cast(compiled.OrderedIR, compile_node(node, True)) + + +def compile_unordered(node: nodes.BigFrameNode) -> compiled.UnorderedIR: + return typing.cast(compiled.UnorderedIR, compile_node(node, False)) + + @functools.cache -def compile_node(node: nodes.BigFrameNode) -> compiled.CompiledArrayValue: +def compile_node( + node: nodes.BigFrameNode, ordered: bool = True +) -> compiled.UnorderedIR | compiled.OrderedIR: """Compile node into CompileArrayValue. Caches result.""" - return _compile_node(node) + return _compile_node(node, ordered) @functools.singledispatch -def _compile_node(node: nodes.BigFrameNode) -> compiled.CompiledArrayValue: +def _compile_node( + node: nodes.BigFrameNode, ordered: bool = True +) -> compiled.UnorderedIR: """Defines transformation but isn't cached, always use compile_node instead""" - raise ValueError(f"Can't compile unnrecognized node: {node}") + raise ValueError(f"Can't compile unrecognized node: {node}") @_compile_node.register -def compile_join(node: nodes.JoinNode): - compiled_left = compile_node(node.left_child) - compiled_right = compile_node(node.right_child) - return bigframes.core.compile.single_column.join_by_column( - compiled_left, - node.left_column_ids, - compiled_right, - node.right_column_ids, - how=node.how, - allow_row_identity_join=node.allow_row_identity_join, - ) +def compile_join(node: nodes.JoinNode, ordered: bool = True): + if ordered: + left_ordered = compile_ordered(node.left_child) + right_ordered = compile_ordered(node.right_child) + return bigframes.core.compile.single_column.join_by_column_ordered( + left_ordered, + node.left_column_ids, + right_ordered, + node.right_column_ids, + how=node.how, + allow_row_identity_join=node.allow_row_identity_join, + ) + else: + left_unordered = compile_unordered(node.left_child) + right_unordered = compile_unordered(node.right_child) + return bigframes.core.compile.single_column.join_by_column_unordered( + left_unordered, + node.left_column_ids, + right_unordered, + node.right_column_ids, + how=node.how, + allow_row_identity_join=node.allow_row_identity_join, + ) @_compile_node.register -def compile_select(node: nodes.SelectNode): - return compile_node(node.child).select_columns(node.column_ids) +def compile_select(node: nodes.SelectNode, ordered: bool = True): + return compile_node(node.child, ordered).select_columns(node.column_ids) @_compile_node.register -def compile_drop(node: nodes.DropColumnsNode): - return compile_node(node.child).drop_columns(node.columns) +def compile_drop(node: nodes.DropColumnsNode, ordered: bool = True): + return compile_node(node.child, ordered).drop_columns(node.columns) @_compile_node.register -def compile_readlocal(node: nodes.ReadLocalNode): +def compile_readlocal(node: nodes.ReadLocalNode, ordered: bool = True): array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) - return compiled.CompiledArrayValue.mem_expr_from_pandas(array_as_pd) + ordered_ir = compiled.OrderedIR.from_pandas(array_as_pd) + if ordered: + return ordered_ir + else: + return ordered_ir.to_unordered() @_compile_node.register -def compile_readgbq(node: nodes.ReadGbqNode): - return compiled.CompiledArrayValue( - node.table, - node.columns, - node.hidden_ordering_columns, - node.ordering, - ) +def compile_readgbq(node: nodes.ReadGbqNode, ordered: bool = True): + if ordered: + return compiled.OrderedIR( + node.table, + node.columns, + node.hidden_ordering_columns, + node.ordering, + ) + else: + return compiled.UnorderedIR( + node.table, + node.columns, + ) @_compile_node.register -def compile_promote_offsets(node: nodes.PromoteOffsetsNode): - return compile_node(node.child).promote_offsets(node.col_id) +def compile_promote_offsets(node: nodes.PromoteOffsetsNode, ordered: bool = True): + result = compile_ordered(node.child).promote_offsets(node.col_id) + return result if ordered else result.to_unordered() @_compile_node.register -def compile_filter(node: nodes.FilterNode): - return compile_node(node.child).filter(node.predicate_id, node.keep_null) +def compile_filter(node: nodes.FilterNode, ordered: bool = True): + return compile_node(node.child, ordered).filter(node.predicate_id, node.keep_null) @_compile_node.register -def compile_orderby(node: nodes.OrderByNode): - return compile_node(node.child).order_by(node.by, node.stable) +def compile_orderby(node: nodes.OrderByNode, ordered: bool = True): + if ordered: + return compile_ordered(node.child).order_by(node.by, node.stable) + else: + return compile_unordered(node.child) @_compile_node.register -def compile_reversed(node: nodes.ReversedNode): - return compile_node(node.child).reversed() +def compile_reversed(node: nodes.ReversedNode, ordered: bool = True): + if ordered: + return compile_ordered(node.child).reversed() + else: + return compile_unordered(node.child) @_compile_node.register -def compile_project_unary(node: nodes.ProjectUnaryOpNode): - return compile_node(node.child).project_unary_op( +def compile_project_unary(node: nodes.ProjectUnaryOpNode, ordered: bool = True): + return compile_node(node.child, ordered).project_unary_op( node.input_id, node.op, node.output_id ) @_compile_node.register -def compile_project_binary(node: nodes.ProjectBinaryOpNode): - return compile_node(node.child).project_binary_op( +def compile_project_binary(node: nodes.ProjectBinaryOpNode, ordered: bool = True): + return compile_node(node.child, ordered).project_binary_op( node.left_input_id, node.right_input_id, node.op, node.output_id ) @_compile_node.register -def compile_project_ternary(node: nodes.ProjectTernaryOpNode): - return compile_node(node.child).project_ternary_op( +def compile_project_ternary(node: nodes.ProjectTernaryOpNode, ordered: bool = True): + return compile_node(node.child, ordered).project_ternary_op( node.input_id1, node.input_id2, node.input_id3, node.op, node.output_id ) @_compile_node.register -def compile_concat(node: nodes.ConcatNode): - compiled_nodes = [compile_node(node) for node in node.children] - return compiled_nodes[0].concat(compiled_nodes[1:]) +def compile_concat(node: nodes.ConcatNode, ordered: bool = True): + if ordered: + compiled_ordered = [compile_ordered(node) for node in node.children] + return concat_impl.concat_ordered(compiled_ordered) + else: + compiled_unordered = [compile_unordered(node) for node in node.children] + return concat_impl.concat_unordered(compiled_unordered) @_compile_node.register -def compile_aggregate(node: nodes.AggregateNode): - return compile_node(node.child).aggregate( +def compile_aggregate(node: nodes.AggregateNode, ordered: bool = True): + result = compile_unordered(node.child).aggregate( node.aggregations, node.by_column_ids, node.dropna ) + return result if ordered else result.to_unordered() @_compile_node.register -def compile_corr(node: nodes.CorrNode): - return compile_node(node.child).corr_aggregate(node.corr_aggregations) +def compile_corr(node: nodes.CorrNode, ordered: bool = True): + result = compile_unordered(node.child).corr_aggregate(node.corr_aggregations) + return result if ordered else result.to_unordered() @_compile_node.register -def compile_window(node: nodes.WindowOpNode): - return compile_node(node.child).project_window_op( +def compile_window(node: nodes.WindowOpNode, ordered: bool = True): + result = compile_ordered(node.child).project_window_op( node.column_name, node.op, node.window_spec, @@ -149,16 +197,17 @@ def compile_window(node: nodes.WindowOpNode): never_skip_nulls=node.never_skip_nulls, skip_reproject_unsafe=node.skip_reproject_unsafe, ) + return result if ordered else result.to_unordered() @_compile_node.register -def compile_reproject(node: nodes.ReprojectOpNode): - return compile_node(node.child)._reproject_to_table() +def compile_reproject(node: nodes.ReprojectOpNode, ordered: bool = True): + return compile_node(node.child, ordered)._reproject_to_table() @_compile_node.register -def compile_unpivot(node: nodes.UnpivotNode): - return compile_node(node.child).unpivot( +def compile_unpivot(node: nodes.UnpivotNode, ordered: bool = True): + return compile_node(node.child, ordered).unpivot( node.row_labels, node.unpivot_columns, passthrough_columns=node.passthrough_columns, @@ -169,17 +218,17 @@ def compile_unpivot(node: nodes.UnpivotNode): @_compile_node.register -def compile_assign(node: nodes.AssignNode): - return compile_node(node.child).assign(node.source_id, node.destination_id) +def compile_assign(node: nodes.AssignNode, ordered: bool = True): + return compile_node(node.child, ordered).assign(node.source_id, node.destination_id) @_compile_node.register -def compile_assign_constant(node: nodes.AssignConstantNode): - return compile_node(node.child).assign_constant( +def compile_assign_constant(node: nodes.AssignConstantNode, ordered: bool = True): + return compile_node(node.child, ordered).assign_constant( node.destination_id, node.value, node.dtype ) @_compile_node.register -def compiler_random_sample(node: nodes.RandomSampleNode): - return compile_node(node.child)._uniform_sampling(node.fraction) +def compiler_random_sample(node: nodes.RandomSampleNode, ordered: bool = True): + return compile_node(node.child, ordered)._uniform_sampling(node.fraction) diff --git a/bigframes/core/compile/concat.py b/bigframes/core/compile/concat.py new file mode 100644 index 00000000000..d39569370e8 --- /dev/null +++ b/bigframes/core/compile/concat.py @@ -0,0 +1,100 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import math +import typing + +import ibis + +import bigframes.core.compile.compiled as compiled +from bigframes.core.ordering import ( + ExpressionOrdering, + OrderingColumnReference, + reencode_order_string, + StringEncoding, +) + +ORDER_ID_COLUMN = "bigframes_ordering_id" + + +def concat_unordered( + items: typing.Sequence[compiled.UnorderedIR], +) -> compiled.UnorderedIR: + """Append together multiple ArrayValue objects.""" + if len(items) == 1: + return items[0] + tables = [] + for expr in items: + table = expr._to_ibis_expr() + # Rename the value columns based on horizontal offset before applying union. + table = table.select( + [table[col].name(f"column_{i}") for i, col in enumerate(table.columns)] + ) + tables.append(table) + combined_table = ibis.union(*tables) + return compiled.UnorderedIR( + combined_table, + columns=[combined_table[col] for col in combined_table.columns], + ) + + +def concat_ordered( + items: typing.Sequence[compiled.OrderedIR], +) -> compiled.OrderedIR: + """Append together multiple ArrayValue objects.""" + if len(items) == 1: + return items[0] + + tables = [] + prefix_base = 10 + prefix_size = math.ceil(math.log(len(items), prefix_base)) + # Must normalize all ids to the same encoding size + max_encoding_size = max( + *[expression._ordering.string_encoding.length for expression in items], + ) + for i, expr in enumerate(items): + ordering_prefix = str(i).zfill(prefix_size) + table = expr._to_ibis_expr( + ordering_mode="string_encoded", order_col_name=ORDER_ID_COLUMN + ) + # Rename the value columns based on horizontal offset before applying union. + table = table.select( + [ + table[col].name(f"column_{i}") + if col != ORDER_ID_COLUMN + else ( + ordering_prefix + + reencode_order_string(table[ORDER_ID_COLUMN], max_encoding_size) + ).name(ORDER_ID_COLUMN) + for i, col in enumerate(table.columns) + ] + ) + tables.append(table) + combined_table = ibis.union(*tables) + ordering = ExpressionOrdering( + ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + string_encoding=StringEncoding(True, prefix_size + max_encoding_size), + ) + return compiled.OrderedIR( + combined_table, + columns=[ + combined_table[col] + for col in combined_table.columns + if col != ORDER_ID_COLUMN + ], + hidden_ordering_columns=[combined_table[ORDER_ID_COLUMN]], + ordering=ordering, + ) diff --git a/bigframes/core/compile/row_identity.py b/bigframes/core/compile/row_identity.py index 2e9bc0527ca..71d53f90dc4 100644 --- a/bigframes/core/compile/row_identity.py +++ b/bigframes/core/compile/row_identity.py @@ -23,16 +23,76 @@ import ibis.expr.types as ibis_types import bigframes.constants as constants -import bigframes.core.compile as compiled +import bigframes.core.compile.compiled as compiled import bigframes.core.joins.name_resolution as naming import bigframes.core.ordering as orderings SUPPORTED_ROW_IDENTITY_HOW = {"outer", "left", "inner"} -def join_by_row_identity( - left: compiled.CompiledArrayValue, right: compiled.CompiledArrayValue, *, how: str -) -> compiled.CompiledArrayValue: +def join_by_row_identity_unordered( + left: compiled.UnorderedIR, + right: compiled.UnorderedIR, + *, + how: str, +) -> compiled.UnorderedIR: + """Compute join when we are joining by row identity not a specific column.""" + if how not in SUPPORTED_ROW_IDENTITY_HOW: + raise NotImplementedError( + f"Only how='outer','left','inner' currently supported. {constants.FEEDBACK_LINK}" + ) + + if not left._table.equals(right._table): + raise ValueError( + "Cannot combine objects without an explicit join/merge key. " + f"Left based on: {left._table.compile()}, but " + f"right based on: {right._table.compile()}" + ) + + left_predicates = left._predicates + right_predicates = right._predicates + # TODO(tbergeron): Skip generating these for inner part of join + ( + left_relative_predicates, + right_relative_predicates, + ) = _get_relative_predicates(left_predicates, right_predicates) + + combined_predicates = [] + if left_predicates or right_predicates: + joined_predicates = _join_predicates( + left_predicates, right_predicates, join_type=how + ) + combined_predicates = list(joined_predicates) # builder expects mutable list + + left_mask = left_relative_predicates if how in ["right", "outer"] else None + right_mask = right_relative_predicates if how in ["left", "outer"] else None + + # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result + map_left_id, map_right_id = naming.JOIN_NAME_REMAPPER( + left.column_ids, right.column_ids + ) + joined_columns = [ + _mask_value(left._get_ibis_column(key), left_mask).name(map_left_id[key]) + for key in left.column_ids + ] + [ + _mask_value(right._get_ibis_column(key), right_mask).name(map_right_id[key]) + for key in right.column_ids + ] + + joined_expr = compiled.UnorderedIR( + left._table, + columns=joined_columns, + predicates=combined_predicates, + ) + return joined_expr + + +def join_by_row_identity_ordered( + left: compiled.OrderedIR, + right: compiled.OrderedIR, + *, + how: str, +) -> compiled.OrderedIR: """Compute join when we are joining by row identity not a specific column.""" if how not in SUPPORTED_ROW_IDENTITY_HOW: raise NotImplementedError( @@ -118,7 +178,7 @@ def join_by_row_identity( if key.column_id in right._hidden_ordering_column_names.keys() ] - joined_expr = compiled.CompiledArrayValue( + joined_expr = compiled.OrderedIR( left._table, columns=joined_columns, hidden_ordering_columns=hidden_ordering_columns, diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index 93ba3f16f1c..a9088feb49b 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -23,16 +23,16 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types -import bigframes.core.compile as compiled +import bigframes.core.compile.compiled as compiled import bigframes.core.compile.row_identity import bigframes.core.joins as joining import bigframes.core.ordering as orderings -def join_by_column( - left: compiled.CompiledArrayValue, +def join_by_column_ordered( + left: compiled.OrderedIR, left_column_ids: typing.Sequence[str], - right: compiled.CompiledArrayValue, + right: compiled.OrderedIR, right_column_ids: typing.Sequence[str], *, how: Literal[ @@ -43,7 +43,7 @@ def join_by_column( "cross", ], allow_row_identity_join: bool = True, -) -> compiled.CompiledArrayValue: +) -> compiled.OrderedIR: """Join two expressions by column equality. Arguments: @@ -68,13 +68,13 @@ def join_by_column( # regards to value its possible that they both have the same names but # were modified in different ways. Ignore differences in the names. and all( - left._get_any_column(lcol) + left._get_ibis_column(lcol) .name("index") - .equals(right._get_any_column(rcol).name("index")) + .equals(right._get_ibis_column(rcol).name("index")) for lcol, rcol in zip(left_column_ids, right_column_ids) ) ): - return bigframes.core.compile.row_identity.join_by_row_identity( + return bigframes.core.compile.row_identity.join_by_row_identity_ordered( left, right, how=how ) else: @@ -89,12 +89,12 @@ def join_by_column( r_mapping = {**r_public_mapping, **r_hidden_mapping} left_table = left._to_ibis_expr( - "unordered", + ordering_mode="unordered", expose_hidden_cols=True, col_id_overrides=l_mapping, ) right_table = right._to_ibis_expr( - "unordered", + ordering_mode="unordered", expose_hidden_cols=True, col_id_overrides=r_mapping, ) @@ -135,7 +135,7 @@ def join_by_column( for col in right._hidden_ordering_columns ], ] - return compiled.CompiledArrayValue( + return compiled.OrderedIR( combined_table, columns=columns, hidden_ordering_columns=hidden_ordering_columns, @@ -143,6 +143,88 @@ def join_by_column( ) +def join_by_column_unordered( + left: compiled.UnorderedIR, + left_column_ids: typing.Sequence[str], + right: compiled.UnorderedIR, + right_column_ids: typing.Sequence[str], + *, + how: Literal[ + "inner", + "left", + "outer", + "right", + "cross", + ], + allow_row_identity_join: bool = True, +) -> compiled.UnorderedIR: + """Join two expressions by column equality. + + Arguments: + left: Expression for left table to join. + left_column_ids: Column IDs (not label) to join by. + right: Expression for right table to join. + right_column_ids: Column IDs (not label) to join by. + how: The type of join to perform. + allow_row_identity_join (bool): + If True, allow matching by row identity. Set to False to always + perform a true JOIN in generated SQL. + Returns: + The joined expression. The resulting columns will be, in order, + first the coalesced join keys, then, all the left columns, and + finally, all the right columns. + """ + if ( + allow_row_identity_join + and how in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW + and left._table.equals(right._table) + # Make sure we're joining on exactly the same column(s), at least with + # regards to value its possible that they both have the same names but + # were modified in different ways. Ignore differences in the names. + and all( + left._get_ibis_column(lcol) + .name("index") + .equals(right._get_ibis_column(rcol).name("index")) + for lcol, rcol in zip(left_column_ids, right_column_ids) + ) + ): + return bigframes.core.compile.row_identity.join_by_row_identity_unordered( + left, right, how=how + ) + else: + # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result + l_mapping, r_mapping = joining.JOIN_NAME_REMAPPER( + left.column_ids, right.column_ids + ) + left_table = left._to_ibis_expr( + col_id_overrides=l_mapping, + ) + right_table = right._to_ibis_expr( + col_id_overrides=r_mapping, + ) + join_conditions = [ + value_to_join_key(left_table[l_mapping[left_index]]) + == value_to_join_key(right_table[r_mapping[right_index]]) + for left_index, right_index in zip(left_column_ids, right_column_ids) + ] + + combined_table = ibis.join( + left_table, + right_table, + predicates=join_conditions, + how=how, # type: ignore + ) + # We could filter out the original join columns, but predicates/ordering + # might still reference them in implicit joins. + columns = [ + combined_table[l_mapping[col.get_name()]] for col in left.columns + ] + [combined_table[r_mapping[col.get_name()]] for col in right.columns] + return compiled.UnorderedIR( + combined_table, + columns=columns, + ) + + def value_to_join_key(value: ibis_types.Value): """Converts nullable values to non-null string SQL will not match null keys together - but pandas does.""" if not value.type().is_string(): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1e143144fe3..0a035754915 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -861,6 +861,8 @@ def to_pandas( max_download_size: Optional[int] = None, sampling_method: Optional[str] = None, random_state: Optional[int] = None, + *, + ordered: bool = True, ) -> pandas.DataFrame: """Write DataFrame to pandas DataFrame. @@ -880,6 +882,9 @@ def to_pandas( The seed for the uniform downsampling algorithm. If provided, the uniform method may take longer to execute and require more computation. If set to a value other than None, this will supersede the global config. + ordered (bool, default True): + Determines whether the resulting pandas dataframe will be deterministically ordered. + In some cases, unordered may result in a faster-executing query. Returns: pandas.DataFrame: A pandas DataFrame with all rows and columns of this DataFrame if the @@ -891,6 +896,7 @@ def to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, + ordered=ordered, ) self._set_internal_query_job(query_job) return df.set_axis(self._block.column_labels, axis=1, copy=False) diff --git a/bigframes/series.py b/bigframes/series.py index 9e111618de2..1952acbf6d5 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -271,6 +271,8 @@ def to_pandas( max_download_size: Optional[int] = None, sampling_method: Optional[str] = None, random_state: Optional[int] = None, + *, + ordered: bool = True, ) -> pandas.Series: """Writes Series to pandas Series. @@ -290,6 +292,10 @@ def to_pandas( The seed for the uniform downsampling algorithm. If provided, the uniform method may take longer to execute and require more computation. If set to a value other than None, this will supersede the global config. + ordered (bool, default True): + Determines whether the resulting pandas series will be deterministically ordered. + In some cases, unordered may result in a faster-executing query. + Returns: pandas.Series: A pandas Series with all rows of this Series if the data_sampling_threshold_mb @@ -300,6 +306,7 @@ def to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, + ordered=ordered, ) self._set_internal_query_job(query_job) series = df[self._value_column] diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index b49e2469a91..8f9fa377873 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -617,10 +617,8 @@ def _read_gbq_table( is_total_ordering = True ordering = orderings.ExpressionOrdering( ordering_value_columns=tuple( - [ - core.OrderingColumnReference(column_id) - for column_id in total_ordering_cols - ] + core.OrderingColumnReference(column_id) + for column_id in total_ordering_cols ), total_ordering_columns=frozenset(total_ordering_cols), ) diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py index eae6896669b..f01116665f1 100644 --- a/tests/system/large/ml/test_cluster.py +++ b/tests/system/large/ml/test_cluster.py @@ -16,7 +16,7 @@ import pytest from bigframes.ml import cluster -from tests.system.utils import assert_pandas_df_equal_ignore_ordering +from tests.system.utils import assert_pandas_df_equal @pytest.mark.flaky(retries=2, delay=120) @@ -105,7 +105,7 @@ def test_cluster_configure_fit_score_predict( index=pd.Index(["test1", "test2", "test3", "test4"], dtype="string[pyarrow]"), ) expected.index.name = "observation" - assert_pandas_df_equal_ignore_ordering(result, expected) + assert_pandas_df_equal(result, expected, ignore_order=True) # save, load, check n_clusters to ensure configuration was kept reloaded_model = model.to_gbq( diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 6874a9f301a..3e569540587 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -24,7 +24,7 @@ pipeline, preprocessing, ) -from tests.system.utils import assert_pandas_df_equal_ignore_ordering +from tests.system.utils import assert_pandas_df_equal def test_pipeline_linear_regression_fit_score_predict( @@ -555,7 +555,7 @@ def test_pipeline_standard_scaler_kmeans_fit_score_predict( ), ) expected.index.name = "observation" - assert_pandas_df_equal_ignore_ordering(result, expected) + assert_pandas_df_equal(result, expected, ignore_order=True) def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_index): diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index c8f8f66ebaa..6ed3e6511a2 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -32,7 +32,7 @@ get_cloud_function_name, get_remote_function_locations, ) -from tests.system.utils import assert_pandas_df_equal_ignore_ordering +from tests.system.utils import assert_pandas_df_equal # Use this to control the number of cloud functions being deleted in a single # test session. This should help soften the spike of the number of mutations per @@ -357,7 +357,7 @@ def square(x): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets(session.bqclient, functions_client, square) @@ -401,7 +401,7 @@ def add_one(x): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -446,7 +446,7 @@ def square(x): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets(session.bqclient, functions_client, square) @@ -497,7 +497,7 @@ def sign(num): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets(session.bqclient, functions_client, remote_sign) @@ -542,7 +542,7 @@ def circumference(radius): pd_result_col = pd_result_col.astype(pandas.Float64Dtype()) pd_result = pd_float64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -591,7 +591,7 @@ def find_team(num): pd_result_col = pd_result_col.astype(pandas.StringDtype(storage="pyarrow")) pd_result = pd_float64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -675,7 +675,7 @@ def inner_test(): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) # Test that the remote function works as expected inner_test() @@ -765,7 +765,7 @@ def is_odd(num): pd_result_col = pd_int64_col.mask(is_odd) pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -808,7 +808,7 @@ def is_odd(num): pd_result_col = pd_int64_col[pd_int64_col.notnull()].mask(is_odd, -1) pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -852,7 +852,7 @@ def test_remote_udf_lambda( pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -909,7 +909,7 @@ def square(x): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -954,7 +954,7 @@ def pd_np_foo(x): # comparing for the purpose of this test pd_result.result = pd_result.result.astype(pandas.Float64Dtype()) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -998,7 +998,7 @@ def test_internal(rf, udf): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) # Create an explicit name for the remote function prefixer = test_utils.prefixer.Prefixer("foo", "") @@ -1167,7 +1167,7 @@ def square(x): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -1204,7 +1204,7 @@ def square(x): pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( diff --git a/tests/system/small/ml/test_cluster.py b/tests/system/small/ml/test_cluster.py index d95a1e1bc22..caeffa77689 100644 --- a/tests/system/small/ml/test_cluster.py +++ b/tests/system/small/ml/test_cluster.py @@ -15,7 +15,7 @@ import pandas as pd from bigframes.ml import cluster -from tests.system.utils import assert_pandas_df_equal_ignore_ordering +from tests.system.utils import assert_pandas_df_equal _PD_NEW_PENGUINS = pd.DataFrame.from_dict( { @@ -68,7 +68,7 @@ def test_kmeans_predict(session, penguins_kmeans_model: cluster.KMeans): dtype="Int64", index=pd.Index(["test1", "test2", "test3", "test4"], dtype="string[pyarrow]"), ) - assert_pandas_df_equal_ignore_ordering(result, expected) + assert_pandas_df_equal(result, expected, ignore_order=True) def test_kmeans_score(session, penguins_kmeans_model: cluster.KMeans): diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index f911dd7eebc..ec1f351d876 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -225,7 +225,7 @@ def test_pca_model_principal_component_info(penguins_bqml_pca_model: core.BqmlMo "cumulative_explained_variance_ratio": [0.469357, 0.651283, 0.812383], }, ) - tests.system.utils.assert_pandas_df_equal_ignore_ordering( + tests.system.utils.assert_pandas_df_equal( result, expected, check_exact=False, @@ -233,6 +233,7 @@ def test_pca_model_principal_component_info(penguins_bqml_pca_model: core.BqmlMo # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame check_index_type=False, check_dtype=False, + ignore_order=True, ) diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index e31681f4a09..cc4d2e5801c 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -130,13 +130,14 @@ def test_pca_explained_variance_(penguins_pca_model: decomposition.PCA): "explained_variance": [3.278657, 1.270829, 1.125354], }, ) - tests.system.utils.assert_pandas_df_equal_ignore_ordering( + tests.system.utils.assert_pandas_df_equal( result, expected, check_exact=False, rtol=0.1, check_index_type=False, check_dtype=False, + ignore_order=True, ) @@ -149,11 +150,12 @@ def test_pca_explained_variance_ratio_(penguins_pca_model: decomposition.PCA): "explained_variance_ratio": [0.469357, 0.181926, 0.1611], }, ) - tests.system.utils.assert_pandas_df_equal_ignore_ordering( + tests.system.utils.assert_pandas_df_equal( result, expected, check_exact=False, rtol=0.1, check_index_type=False, check_dtype=False, + ignore_order=True, ) diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 7dc55b9367c..177194c7a8a 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -16,7 +16,7 @@ import pytest import bigframes.series -from tests.system.utils import assert_series_equal_ignoring_order +from tests.system.utils import assert_series_equal DATETIME_COL_NAMES = [("datetime_col",), ("timestamp_col",)] @@ -33,7 +33,7 @@ def test_day(scalars_dfs, col_name): bf_result = bf_series.dt.day.to_pandas() pd_result = scalars_pandas_df[col_name].dt.day - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -51,7 +51,7 @@ def test_date(scalars_dfs, col_name): bf_result = bf_series.dt.date.to_pandas() pd_result = scalars_pandas_df[col_name].dt.date - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -69,7 +69,7 @@ def test_dayofweek(scalars_dfs, col_name): bf_result = bf_series.dt.dayofweek.to_pandas() pd_result = scalars_pandas_df[col_name].dt.dayofweek - assert_series_equal_ignoring_order(pd_result, bf_result, check_dtype=False) + assert_series_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.parametrize( @@ -84,7 +84,7 @@ def test_hour(scalars_dfs, col_name): bf_result = bf_series.dt.hour.to_pandas() pd_result = scalars_pandas_df[col_name].dt.hour - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -102,7 +102,7 @@ def test_minute(scalars_dfs, col_name): bf_result = bf_series.dt.minute.to_pandas() pd_result = scalars_pandas_df[col_name].dt.minute - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -120,7 +120,7 @@ def test_month(scalars_dfs, col_name): bf_result = bf_series.dt.month.to_pandas() pd_result = scalars_pandas_df[col_name].dt.month - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -138,7 +138,7 @@ def test_quarter(scalars_dfs, col_name): bf_result = bf_series.dt.quarter.to_pandas() pd_result = scalars_pandas_df[col_name].dt.quarter - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -156,7 +156,7 @@ def test_second(scalars_dfs, col_name): bf_result = bf_series.dt.second.to_pandas() pd_result = scalars_pandas_df[col_name].dt.second - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -174,7 +174,7 @@ def test_time(scalars_dfs, col_name): bf_result = bf_series.dt.time.to_pandas() pd_result = scalars_pandas_df[col_name].dt.time - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -192,7 +192,7 @@ def test_year(scalars_dfs, col_name): bf_result = bf_series.dt.year.to_pandas() pd_result = scalars_pandas_df[col_name].dt.year - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 241cbd576b1..27a35134d4a 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -19,7 +19,7 @@ import bigframes.series -from ...utils import assert_series_equal_ignoring_order +from ...utils import assert_series_equal def test_find(scalars_dfs): @@ -31,7 +31,7 @@ def test_find(scalars_dfs): # One of type mismatches to be documented. Here, the `bf_result.dtype` is `Int64` but # the `pd_result.dtype` is `float64`: https://github.com/pandas-dev/pandas/issues/51948 - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -173,7 +173,7 @@ def test_len(scalars_dfs): # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is `Int64` but # the `pd_result.dtype` is `float64`: https://github.com/pandas-dev/pandas/issues/51948 - assert_series_equal_ignoring_order( + assert_series_equal( pd_result.astype(pd.Int64Dtype()), bf_result, ) @@ -186,7 +186,7 @@ def test_lower(scalars_dfs): bf_result = bf_series.str.lower().to_pandas() pd_result = scalars_pandas_df[col_name].str.lower() - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -205,7 +205,7 @@ def test_reverse(scalars_dfs): else: pd_result.loc[i] = cell[::-1] - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -222,7 +222,7 @@ def test_slice(scalars_dfs, start, stop): pd_series = scalars_pandas_df[col_name] pd_result = pd_series.str.slice(start, stop) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -235,7 +235,7 @@ def test_strip(scalars_dfs): bf_result = bf_series.str.strip().to_pandas() pd_result = scalars_pandas_df[col_name].str.strip() - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -248,7 +248,7 @@ def test_upper(scalars_dfs): bf_result = bf_series.str.upper().to_pandas() pd_result = scalars_pandas_df[col_name].str.upper() - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -330,7 +330,7 @@ def test_islower(weird_strings, weird_strings_pd): pd_result = weird_strings_pd.str.islower() bf_result = weird_strings.str.islower().to_pandas() - assert_series_equal_ignoring_order( + assert_series_equal( bf_result, pd_result.astype(pd.BooleanDtype()) # the dtype here is a case of intentional diversion from pandas @@ -342,7 +342,7 @@ def test_isupper(weird_strings, weird_strings_pd): pd_result = weird_strings_pd.str.isupper() bf_result = weird_strings.str.isupper().to_pandas() - assert_series_equal_ignoring_order( + assert_series_equal( bf_result, pd_result.astype(pd.BooleanDtype()) # the dtype here is a case of intentional diversion from pandas @@ -357,7 +357,7 @@ def test_rstrip(scalars_dfs): bf_result = bf_series.str.rstrip().to_pandas() pd_result = scalars_pandas_df[col_name].str.rstrip() - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -370,7 +370,7 @@ def test_lstrip(scalars_dfs): bf_result = bf_series.str.lstrip().to_pandas() pd_result = scalars_pandas_df[col_name].str.lstrip() - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -384,7 +384,7 @@ def test_repeat(scalars_dfs, repeats): bf_result = bf_series.str.repeat(repeats).to_pandas() pd_result = scalars_pandas_df[col_name].str.repeat(repeats) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -397,7 +397,7 @@ def test_capitalize(scalars_dfs): bf_result = bf_series.str.capitalize().to_pandas() pd_result = scalars_pandas_df[col_name].str.capitalize() - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -415,7 +415,7 @@ def test_cat_with_series(scalars_dfs): pd_right = scalars_pandas_df[col_name] pd_result = pd_left.str.cat(others=pd_right) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -429,7 +429,7 @@ def test_str_match(scalars_dfs): bf_result = bf_series.str.match(pattern).to_pandas() pd_result = scalars_pandas_df[col_name].str.match(pattern) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -443,7 +443,7 @@ def test_str_fullmatch(scalars_dfs): bf_result = bf_series.str.fullmatch(pattern).to_pandas() pd_result = scalars_pandas_df[col_name].str.fullmatch(pattern) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -456,7 +456,7 @@ def test_str_get(scalars_dfs): bf_result = bf_series.str.get(8).to_pandas() pd_result = scalars_pandas_df[col_name].str.get(8) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -469,7 +469,7 @@ def test_str_pad(scalars_dfs): bf_result = bf_series.str.pad(8, side="both", fillchar="%").to_pandas() pd_result = scalars_pandas_df[col_name].str.pad(8, side="both", fillchar="%") - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -492,7 +492,7 @@ def test_str_ljust(scalars_dfs): bf_result = bf_series.str.ljust(7, fillchar="%").to_pandas() pd_result = scalars_pandas_df[col_name].str.ljust(7, fillchar="%") - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -505,7 +505,7 @@ def test_str_rjust(scalars_dfs): bf_result = bf_series.str.rjust(9, fillchar="%").to_pandas() pd_result = scalars_pandas_df[col_name].str.rjust(9, fillchar="%") - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 605d4abc1d0..e522878229a 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -28,10 +28,7 @@ import bigframes._config.display_options as display_options import bigframes.dataframe as dataframe import bigframes.series as series -from tests.system.utils import ( - assert_pandas_df_equal_ignore_ordering, - assert_series_equal_ignoring_order, -) +from tests.system.utils import assert_pandas_df_equal, assert_series_equal def test_df_construct_copy(scalars_dfs): @@ -98,7 +95,7 @@ def test_get_column(scalars_dfs): series = scalars_df[col_name] bf_result = series.to_pandas() pd_result = scalars_pandas_df[col_name] - assert_series_equal_ignoring_order(bf_result, pd_result) + assert_series_equal(bf_result, pd_result) def test_get_column_nonstring(scalars_dfs): @@ -106,7 +103,7 @@ def test_get_column_nonstring(scalars_dfs): series = scalars_df.rename(columns={"int64_col": 123.1})[123.1] bf_result = series.to_pandas() pd_result = scalars_pandas_df.rename(columns={"int64_col": 123.1})[123.1] - assert_series_equal_ignoring_order(bf_result, pd_result) + assert_series_equal(bf_result, pd_result) def test_hasattr(scalars_dfs): @@ -116,15 +113,24 @@ def test_hasattr(scalars_dfs): assert not hasattr(scalars_df, "not_exist") -def test_head_with_custom_column_labels(scalars_df_index, scalars_pandas_df_index): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_head_with_custom_column_labels( + scalars_df_index, scalars_pandas_df_index, ordered +): rename_mapping = { "int64_col": "Integer Column", "string_col": "言語列", } bf_df = scalars_df_index.rename(columns=rename_mapping).head(3) - bf_result = bf_df.to_pandas() + bf_result = bf_df.to_pandas(ordered=ordered) pd_result = scalars_pandas_df_index.rename(columns=rename_mapping).head(3) - pandas.testing.assert_frame_equal(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=not ordered) def test_tail_with_custom_column_labels(scalars_df_index, scalars_pandas_df_index): @@ -183,7 +189,7 @@ def test_get_column_by_attr(scalars_dfs): series = scalars_df.int64_col bf_result = series.to_pandas() pd_result = scalars_pandas_df.int64_col - assert_series_equal_ignoring_order(bf_result, pd_result) + assert_series_equal(bf_result, pd_result) def test_get_columns(scalars_dfs): @@ -246,7 +252,7 @@ def test_drop_with_custom_column_labels(scalars_dfs): pd_result = scalars_pandas_df.rename(columns=rename_mapping).drop( columns=dropped_columns ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_drop_index(scalars_dfs): @@ -420,7 +426,7 @@ def test_filter_df(scalars_dfs): pd_bool_series = scalars_pandas_df["bool_col"] pd_result = scalars_pandas_df[pd_bool_series] - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_assign_new_column(scalars_dfs): @@ -433,7 +439,7 @@ def test_assign_new_column(scalars_dfs): # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["new_col"] = pd_result["new_col"].astype("Int64") - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_assign_new_column_w_loc(scalars_dfs): @@ -564,7 +570,7 @@ def test_assign_existing_column(scalars_dfs): # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["int64_col"] = pd_result["int64_col"].astype("Int64") - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_assign_listlike_to_empty_df(session): @@ -576,7 +582,7 @@ def test_assign_listlike_to_empty_df(session): pd_result["new_col"] = pd_result["new_col"].astype("Int64") pd_result.index = pd_result.index.astype("Int64") - assert_pandas_df_equal_ignore_ordering(bf_result.to_pandas(), pd_result) + assert_pandas_df_equal(bf_result.to_pandas(), pd_result) def test_assign_to_empty_df_multiindex_error(session): @@ -595,14 +601,21 @@ def test_assign_to_empty_df_multiindex_error(session): empty_pandas_df.assign(new_col=[1, 2, 3, 4, 5, 6, 7, 8, 9]) -def test_assign_series(scalars_dfs): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_assign_series(scalars_dfs, ordered): scalars_df, scalars_pandas_df = scalars_dfs column_name = "int64_col" df = scalars_df.assign(new_col=scalars_df[column_name]) - bf_result = df.to_pandas() + bf_result = df.to_pandas(ordered=ordered) pd_result = scalars_pandas_df.assign(new_col=scalars_pandas_df[column_name]) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=not ordered) def test_assign_series_overwrite(scalars_dfs): @@ -614,7 +627,7 @@ def test_assign_series_overwrite(scalars_dfs): **{column_name: scalars_pandas_df[column_name] + 3} ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_assign_sequential(scalars_dfs): @@ -629,7 +642,7 @@ def test_assign_sequential(scalars_dfs): pd_result["new_col"] = pd_result["new_col"].astype("Int64") pd_result["new_col2"] = pd_result["new_col2"].astype("Int64") - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) # Require an index so that the self-join is consistent each time. @@ -663,7 +676,7 @@ def test_assign_different_df( new_col=scalars_pandas_df_index[column_name] ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_assign_different_df_w_loc( @@ -714,7 +727,7 @@ def test_assign_callable_lambda(scalars_dfs): # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["new_col"] = pd_result["new_col"].astype("Int64") - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -965,7 +978,9 @@ def test_df_merge(scalars_dfs, merge_how): sort=True, ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal( + bf_result, pd_result, ignore_order=True, check_index_type=False + ) @pytest.mark.parametrize( @@ -998,7 +1013,9 @@ def test_df_merge_multi_key(scalars_dfs, left_on, right_on): sort=True, ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal( + bf_result, pd_result, ignore_order=True, check_index_type=False + ) @pytest.mark.parametrize( @@ -1028,7 +1045,9 @@ def test_merge_custom_col_name(scalars_dfs, merge_how): pandas_right_df = scalars_pandas_df[right_columns] pd_result = pandas_left_df.merge(pandas_right_df, merge_how, on, sort=True) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal( + bf_result, pd_result, ignore_order=True, check_index_type=False + ) @pytest.mark.parametrize( @@ -1061,7 +1080,9 @@ def test_merge_left_on_right_on(scalars_dfs, merge_how): sort=True, ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal( + bf_result, pd_result, ignore_order=True, check_index_type=False + ) def test_get_dtypes(scalars_df_default_index): @@ -1375,7 +1396,7 @@ def test_df_abs(scalars_dfs): bf_result = scalars_df[columns].abs().to_pandas() pd_result = scalars_pandas_df[columns].abs() - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_df_isnull(scalars_dfs): @@ -1392,7 +1413,7 @@ def test_df_isnull(scalars_dfs): pd_result["string_col"] = pd_result["string_col"].astype(pd.BooleanDtype()) pd_result["bool_col"] = pd_result["bool_col"].astype(pd.BooleanDtype()) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_df_notnull(scalars_dfs): @@ -1409,7 +1430,7 @@ def test_df_notnull(scalars_dfs): pd_result["string_col"] = pd_result["string_col"].astype(pd.BooleanDtype()) pd_result["bool_col"] = pd_result["bool_col"].astype(pd.BooleanDtype()) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -1629,7 +1650,7 @@ def test_scalar_binop(scalars_dfs, op, other_scalar, reverse_operands): bf_result = maybe_reversed_op(scalars_df[columns], other_scalar).to_pandas() pd_result = maybe_reversed_op(scalars_pandas_df[columns], other_scalar) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.parametrize(("other_scalar"), [1, -2]) @@ -1641,7 +1662,7 @@ def test_mod(scalars_dfs, other_scalar): bf_result = (scalars_df[["int64_col", "int64_too"]] % other_scalar).to_pandas() pd_result = scalars_pandas_df[["int64_col", "int64_too"]] % other_scalar - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_scalar_binop_str_exception(scalars_dfs): @@ -1697,7 +1718,7 @@ def test_series_binop_axis_index( bf_result = op(scalars_df[df_columns], scalars_df[series_column]).to_pandas() pd_result = op(scalars_pandas_df[df_columns], scalars_pandas_df[series_column]) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -1743,8 +1764,15 @@ def test_binop_df_df_binary_op( # Differnt table will only work for explicit index, since default index orders are arbitrary. +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) def test_series_binop_add_different_table( - scalars_df_index, scalars_pandas_df_index, scalars_df_2_index + scalars_df_index, scalars_pandas_df_index, scalars_df_2_index, ordered ): df_columns = ["int64_col", "float64_col"] series_column = "int64_too" @@ -1752,13 +1780,13 @@ def test_series_binop_add_different_table( bf_result = ( scalars_df_index[df_columns] .add(scalars_df_2_index[series_column], axis="index") - .to_pandas() + .to_pandas(ordered=ordered) ) pd_result = scalars_pandas_df_index[df_columns].add( scalars_pandas_df_index[series_column], axis="index" ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=not ordered) # TODO(garrettwu): Test series binop with different index @@ -1779,7 +1807,7 @@ def test_join_same_table(scalars_dfs, how): pd_df_a = pd_df.set_index("int64_too")[["string_col", "int64_col"]] pd_df_b = pd_df.set_index("int64_too")[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, how=how) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) @all_joins @@ -1792,7 +1820,7 @@ def test_join_different_table( pd_df_a = scalars_pandas_df_index[["string_col", "int64_col"]] pd_df_b = scalars_pandas_df_index.dropna()[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, how=how) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) def test_join_duplicate_columns_raises_not_implemented(scalars_dfs): @@ -1821,7 +1849,7 @@ def test_join_param_on(scalars_dfs, how): pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) pd_df_b = pd_df[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, on="rowindex_2", how=how) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) @pytest.mark.parametrize( @@ -2037,7 +2065,14 @@ def test_df_describe(scalars_dfs): ).all() -def test_df_stack(scalars_dfs): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_df_stack(scalars_dfs, ordered): if pandas.__version__.startswith("1.") or pandas.__version__.startswith("2.0"): pytest.skip("pandas <2.1 uses different stack implementation") scalars_df, scalars_pandas_df = scalars_dfs @@ -2047,11 +2082,13 @@ def test_df_stack(scalars_dfs): # Can only stack identically-typed columns columns = ["int64_col", "int64_too", "rowindex_2"] - bf_result = scalars_df[columns].stack().to_pandas() + bf_result = scalars_df[columns].stack().to_pandas(ordered=ordered) pd_result = scalars_pandas_df[columns].stack(future_stack=True) # Pandas produces NaN, where bq dataframes produces pd.NA - pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + assert_series_equal( + bf_result, pd_result, check_dtype=False, ignore_order=not ordered + ) def test_df_melt_default(scalars_dfs): @@ -2097,7 +2134,14 @@ def test_df_melt_parameterized(scalars_dfs): ) -def test_df_unstack(scalars_dfs): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_df_unstack(scalars_dfs, ordered): scalars_df, scalars_pandas_df = scalars_dfs # To match bigquery dataframes scalars_pandas_df = scalars_pandas_df.copy() @@ -2110,11 +2154,13 @@ def test_df_unstack(scalars_dfs): ] # unstack on mono-index produces series - bf_result = scalars_df[columns].unstack().to_pandas() + bf_result = scalars_df[columns].unstack().to_pandas(ordered=ordered) pd_result = scalars_pandas_df[columns].unstack() # Pandas produces NaN, where bq dataframes produces pd.NA - pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + assert_series_equal( + bf_result, pd_result, check_dtype=False, ignore_order=not ordered + ) @pytest.mark.parametrize( @@ -2259,14 +2305,18 @@ def test_iloc_slice_zero_step(scalars_df_index): scalars_df_index.iloc[0:0:0] -def test_iloc_slice_nested(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.iloc[1:].iloc[1:].to_pandas() +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_iloc_slice_nested(scalars_df_index, scalars_pandas_df_index, ordered): + bf_result = scalars_df_index.iloc[1:].iloc[1:].to_pandas(ordered=ordered) pd_result = scalars_pandas_df_index.iloc[1:].iloc[1:] - pd.testing.assert_frame_equal( - bf_result, - pd_result, - ) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=not ordered) @pytest.mark.parametrize( @@ -2457,6 +2507,13 @@ def test_loc_setitem_bool_series_scalar_type_error(scalars_dfs): pd_df.loc[pd_df["int64_too"] == 1, "string_col"] = 99 +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) @pytest.mark.parametrize( ("op"), [ @@ -2471,16 +2528,18 @@ def test_loc_setitem_bool_series_scalar_type_error(scalars_dfs): ], ids=["sum", "mean", "min", "max", "std", "var", "count", "nunique"], ) -def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op): +def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op, ordered): col_names = ["int64_too", "float64_col", "string_col", "int64_col", "bool_col"] bf_series = op(scalars_df_index[col_names]) pd_series = op(scalars_pandas_df_index[col_names]) - bf_result = bf_series.to_pandas() + bf_result = bf_series.to_pandas(ordered=ordered) # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_series = pd_series.astype("Float64") # Pandas has object index type - pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) + assert_series_equal( + pd_series, bf_result, check_index_type=False, ignore_order=not ordered + ) @pytest.mark.parametrize( @@ -2571,16 +2630,25 @@ def test_df_skew_too_few_values(scalars_dfs): pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) -def test_df_skew(scalars_dfs): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_df_skew(scalars_dfs, ordered): columns = ["float64_col", "int64_col"] scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df[columns].skew().to_pandas() + bf_result = scalars_df[columns].skew().to_pandas(ordered=ordered) pd_result = scalars_pandas_df[columns].skew() # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_result = pd_result.astype("Float64") - pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + assert_series_equal( + pd_result, bf_result, check_index_type=False, ignore_order=not ordered + ) def test_df_kurt_too_few_values(scalars_dfs): @@ -2731,9 +2799,10 @@ def test_df_rows_filter_items(scalars_df_index, scalars_pandas_df_index): # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) # Ignore ordering as pandas order differently depending on version - assert_pandas_df_equal_ignore_ordering( + assert_pandas_df_equal( bf_result, pd_result, + ignore_order=True, check_names=False, ) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 8f5d706f621..d700d93be99 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -19,10 +19,7 @@ import pyarrow as pa import pytest -from tests.system.utils import ( - assert_pandas_df_equal_ignore_ordering, - convert_pandas_dtypes, -) +from tests.system.utils import assert_pandas_df_equal, convert_pandas_dtypes try: import pandas_gbq # type: ignore @@ -380,7 +377,7 @@ def test_to_sql_query_unnamed_index_included( pd_df = scalars_pandas_df_default_index.reset_index(drop=True) roundtrip = session.read_gbq(sql, index_col=idx_ids) roundtrip.index.names = [None] - assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) + assert_pandas_df_equal(roundtrip.to_pandas(), pd_df, check_index_type=False) def test_to_sql_query_named_index_included( @@ -397,7 +394,7 @@ def test_to_sql_query_named_index_included( pd_df = scalars_pandas_df_default_index.set_index("rowindex_2", drop=True) roundtrip = session.read_gbq(sql, index_col=idx_ids) - assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) + assert_pandas_df_equal(roundtrip.to_pandas(), pd_df) def test_to_sql_query_unnamed_index_excluded( @@ -412,7 +409,9 @@ def test_to_sql_query_unnamed_index_excluded( pd_df = scalars_pandas_df_default_index.reset_index(drop=True) roundtrip = session.read_gbq(sql) - assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) + assert_pandas_df_equal( + roundtrip.to_pandas(), pd_df, check_index_type=False, ignore_order=True + ) def test_to_sql_query_named_index_excluded( @@ -429,4 +428,6 @@ def test_to_sql_query_named_index_excluded( "rowindex_2", drop=True ).reset_index(drop=True) roundtrip = session.read_gbq(sql) - assert_pandas_df_equal_ignore_ordering(roundtrip.to_pandas(), pd_df) + assert_pandas_df_equal( + roundtrip.to_pandas(), pd_df, check_index_type=False, ignore_order=True + ) diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 05154f7ab7d..a24713c2b3f 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -16,6 +16,7 @@ import pytest import bigframes.pandas as bpd +from tests.system.utils import assert_pandas_df_equal @pytest.mark.parametrize( @@ -88,16 +89,23 @@ def test_dataframe_groupby_aggregate( pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) -def test_dataframe_groupby_agg_string(scalars_df_index, scalars_pandas_df_index): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_dataframe_groupby_agg_string( + scalars_df_index, scalars_pandas_df_index, ordered +): col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = scalars_df_index[col_names].groupby("string_col").agg("count") pd_result = scalars_pandas_df_index[col_names].groupby("string_col").agg("count") - bf_result_computed = bf_result.to_pandas() + bf_result_computed = bf_result.to_pandas(ordered=ordered) - pd.testing.assert_frame_equal( - pd_result, - bf_result_computed, - check_dtype=False, + assert_pandas_df_equal( + pd_result, bf_result_computed, check_dtype=False, ignore_order=not ordered ) @@ -270,13 +278,22 @@ def test_dataframe_groupby_kurt(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) -def test_dataframe_groupby_diff(scalars_df_index, scalars_pandas_df_index): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_dataframe_groupby_diff(scalars_df_index, scalars_pandas_df_index, ordered): col_names = ["float64_col", "int64_col", "string_col"] bf_result = scalars_df_index[col_names].groupby("string_col").diff(-1) pd_result = scalars_pandas_df_index[col_names].groupby("string_col").diff(-1) - bf_result_computed = bf_result.to_pandas() + bf_result_computed = bf_result.to_pandas(ordered=ordered) - pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + assert_pandas_df_equal( + pd_result, bf_result_computed, check_dtype=False, ignore_order=not ordered + ) def test_dataframe_groupby_getitem( diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index bc35f633fdb..e7e93849c60 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -16,7 +16,7 @@ import pytest import bigframes.pandas as bpd -from tests.system.utils import assert_pandas_df_equal_ignore_ordering +from tests.system.utils import assert_pandas_df_equal # Row Multi-index tests @@ -429,7 +429,7 @@ def test_multi_index_dataframe_join(scalars_dfs, how): (["bool_col", "rowindex_2"]) )[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, how=how) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) @all_joins @@ -450,7 +450,7 @@ def test_multi_index_dataframe_join_on(scalars_dfs, how): pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) pd_df_b = pd_df[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, on="rowindex_2", how=how) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) @pytest.mark.parametrize( diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index b88901f3bcf..a1079288cfb 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -16,16 +16,23 @@ import pytest import bigframes.pandas as bpd -from tests.system.utils import assert_pandas_df_equal_ignore_ordering +from tests.system.utils import assert_pandas_df_equal -def test_concat_dataframe(scalars_dfs): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_concat_dataframe(scalars_dfs, ordered): scalars_df, scalars_pandas_df = scalars_dfs bf_result = bpd.concat(11 * [scalars_df]) - bf_result = bf_result.to_pandas() + bf_result = bf_result.to_pandas(ordered=ordered) pd_result = pd.concat(11 * [scalars_pandas_df]) - pd.testing.assert_frame_equal(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=not ordered) def test_concat_series(scalars_dfs): @@ -252,7 +259,7 @@ def test_merge(scalars_dfs, merge_how): sort=True, ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) @pytest.mark.parametrize( @@ -286,7 +293,7 @@ def test_merge_left_on_right_on(scalars_dfs, merge_how): sort=True, ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) def test_pd_merge_cross(scalars_dfs): @@ -341,7 +348,7 @@ def test_merge_series(scalars_dfs, merge_how): sort=True, ) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) def test_cut(scalars_dfs): diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 89907a53dfc..3d8532a13b6 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -18,7 +18,7 @@ import bigframes from bigframes import remote_function as rf -from tests.system.utils import assert_pandas_df_equal_ignore_ordering +from tests.system.utils import assert_pandas_df_equal @pytest.fixture(scope="module") @@ -121,7 +121,7 @@ def square(x): pd_result_col = pd_result_col.astype(pd.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -170,7 +170,7 @@ def square(x): pd_result_col = pd_result_col.astype(pd.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -246,7 +246,7 @@ def square(x): pd_result_col = pd_result_col.astype(pd.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -309,7 +309,7 @@ def square(x): pd_result_col = pd_result_col.astype(pd.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -348,7 +348,7 @@ def square(x): pd_result_col = pd_result_col.astype(pd.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -387,7 +387,7 @@ def square(x): pd_result_col = pd_result_col.astype(pd.Int64Dtype()) pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -418,7 +418,7 @@ def add_one(x): for col in pd_result: pd_result[col] = pd_result[col].astype(pd_int64_df_filtered[col].dtype) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -447,7 +447,7 @@ def add_one(x): for col in pd_result: pd_result[col] = pd_result[col].astype(pd_int64_df[col].dtype) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.flaky(retries=2, delay=120) @@ -535,7 +535,7 @@ def square1(x): s2_result_col = int64_col_filtered.apply(square2) s2_result = int64_col_filtered.to_frame().assign(result=s2_result_col) - assert_pandas_df_equal_ignore_ordering(s1_result.to_pandas(), s2_result.to_pandas()) + assert_pandas_df_equal(s1_result.to_pandas(), s2_result.to_pandas()) @pytest.mark.flaky(retries=2, delay=120) @@ -583,7 +583,9 @@ def test_read_gbq_function_reads_udfs(bigquery_client, dataset_id): indirect_df = indirect_df.assign(y=indirect_df.x.apply(square)) indirect_df = indirect_df.to_pandas() - assert_pandas_df_equal_ignore_ordering(direct_df, indirect_df) + assert_pandas_df_equal( + direct_df, indirect_df, ignore_order=True, check_index_type=False + ) @pytest.mark.flaky(retries=2, delay=120) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 006d370818d..f59d64fe06f 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -24,10 +24,7 @@ import bigframes.pandas import bigframes.series as series -from tests.system.utils import ( - assert_pandas_df_equal_ignore_ordering, - assert_series_equal_ignoring_order, -) +from tests.system.utils import assert_pandas_df_equal, assert_series_equal def test_series_construct_copy(scalars_dfs): @@ -210,7 +207,7 @@ def test_abs(scalars_dfs, col_name): bf_result = scalars_df[col_name].abs().to_pandas() pd_result = scalars_pandas_df[col_name].abs() - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_fillna(scalars_dfs): @@ -218,7 +215,7 @@ def test_fillna(scalars_dfs): col_name = "string_col" bf_result = scalars_df[col_name].fillna("Missing").to_pandas() pd_result = scalars_pandas_df[col_name].fillna("Missing") - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -496,7 +493,7 @@ def test_series_int_int_operators_scalar( bf_result = maybe_reversed_op(scalars_df["int64_col"], other_scalar).to_pandas() pd_result = maybe_reversed_op(scalars_pandas_df["int64_col"], other_scalar) - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_series_pow_scalar(scalars_dfs): @@ -505,7 +502,7 @@ def test_series_pow_scalar(scalars_dfs): bf_result = (scalars_df["int64_col"] ** 2).to_pandas() pd_result = scalars_pandas_df["int64_col"] ** 2 - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_series_pow_scalar_reverse(scalars_dfs): @@ -514,7 +511,7 @@ def test_series_pow_scalar_reverse(scalars_dfs): bf_result = (0.8 ** scalars_df["int64_col"]).to_pandas() pd_result = 0.8 ** scalars_pandas_df["int64_col"] - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) @pytest.mark.parametrize( @@ -540,7 +537,7 @@ def test_series_bool_bool_operators_scalar( bf_result = maybe_reversed_op(scalars_df["bool_col"], other_scalar).to_pandas() pd_result = maybe_reversed_op(scalars_pandas_df["bool_col"], other_scalar) - assert_series_equal_ignoring_order(pd_result.astype(pd.BooleanDtype()), bf_result) + assert_series_equal(pd_result.astype(pd.BooleanDtype()), bf_result) @pytest.mark.parametrize( @@ -578,7 +575,7 @@ def test_series_int_int_operators_series(scalars_dfs, operator): scalars_df, scalars_pandas_df = scalars_dfs bf_result = operator(scalars_df["int64_col"], scalars_df["int64_too"]).to_pandas() pd_result = operator(scalars_pandas_df["int64_col"], scalars_pandas_df["int64_too"]) - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) @pytest.mark.parametrize( @@ -728,7 +725,7 @@ def test_series_add_scalar(scalars_dfs, other): bf_result = (scalars_df["float64_col"] + other).to_pandas() pd_result = scalars_pandas_df["float64_col"] + other - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) @pytest.mark.parametrize( @@ -744,7 +741,7 @@ def test_series_add_bigframes_series(scalars_dfs, left_col, right_col): bf_result = (scalars_df[left_col] + scalars_df[right_col]).to_pandas() pd_result = scalars_pandas_df[left_col] + scalars_pandas_df[right_col] - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) @pytest.mark.parametrize( @@ -766,7 +763,7 @@ def test_series_add_bigframes_series_nested( scalars_pandas_df[left_col] + scalars_pandas_df[right_col] ) + scalars_pandas_df[righter_col] - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_series_add_different_table_default_index( @@ -924,7 +921,7 @@ def test_isnull(scalars_dfs): # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but # the `pd_series.dtype` is `bool`. - assert_series_equal_ignoring_order(pd_series.astype(pd.BooleanDtype()), bf_series) + assert_series_equal(pd_series.astype(pd.BooleanDtype()), bf_series) def test_notnull(scalars_dfs): @@ -935,7 +932,7 @@ def test_notnull(scalars_dfs): # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but # the `pd_series.dtype` is `bool`. - assert_series_equal_ignoring_order(pd_series.astype(pd.BooleanDtype()), bf_series) + assert_series_equal(pd_series.astype(pd.BooleanDtype()), bf_series) def test_round(scalars_dfs): @@ -944,7 +941,7 @@ def test_round(scalars_dfs): bf_result = scalars_df[col_name].round().to_pandas() pd_result = scalars_pandas_df[col_name].round() - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_eq_scalar(scalars_dfs): @@ -953,7 +950,7 @@ def test_eq_scalar(scalars_dfs): bf_result = scalars_df[col_name].eq(0).to_pandas() pd_result = scalars_pandas_df[col_name].eq(0) - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_eq_wider_type_scalar(scalars_dfs): @@ -962,7 +959,7 @@ def test_eq_wider_type_scalar(scalars_dfs): bf_result = scalars_df[col_name].eq(1.0).to_pandas() pd_result = scalars_pandas_df[col_name].eq(1.0) - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_ne_scalar(scalars_dfs): @@ -971,7 +968,7 @@ def test_ne_scalar(scalars_dfs): bf_result = (scalars_df[col_name] != 0).to_pandas() pd_result = scalars_pandas_df[col_name] != 0 - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) def test_eq_int_scalar(scalars_dfs): @@ -980,7 +977,7 @@ def test_eq_int_scalar(scalars_dfs): bf_result = (scalars_df[col_name] == 0).to_pandas() pd_result = scalars_pandas_df[col_name] == 0 - assert_series_equal_ignoring_order(pd_result, bf_result) + assert_series_equal(pd_result, bf_result) @pytest.mark.parametrize( @@ -999,7 +996,7 @@ def test_eq_same_type_series(scalars_dfs, col_name): # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but # the `pd_series.dtype` is `bool`. - assert_series_equal_ignoring_order(pd_result.astype(pd.BooleanDtype()), bf_result) + assert_series_equal(pd_result.astype(pd.BooleanDtype()), bf_result) def test_loc_setitem_cell(scalars_df_index, scalars_pandas_df_index): @@ -1036,7 +1033,7 @@ def test_ne_obj_series(scalars_dfs): # One of dtype mismatches to be documented. Here, the `bf_series.dtype` is `BooleanDtype` but # the `pd_series.dtype` is `bool`. - assert_series_equal_ignoring_order(pd_result.astype(pd.BooleanDtype()), bf_result) + assert_series_equal(pd_result.astype(pd.BooleanDtype()), bf_result) def test_indexing_using_unselected_series(scalars_dfs): @@ -1045,7 +1042,7 @@ def test_indexing_using_unselected_series(scalars_dfs): bf_result = scalars_df[col_name][scalars_df["int64_too"].eq(0)].to_pandas() pd_result = scalars_pandas_df[col_name][scalars_pandas_df["int64_too"].eq(0)] - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -1061,7 +1058,7 @@ def test_indexing_using_selected_series(scalars_dfs): scalars_pandas_df["string_col"].eq("Hello, World!") ] - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -1083,7 +1080,7 @@ def test_nested_filter(scalars_dfs): ) # Convert from nullable bool to nonnullable bool usable as indexer pd_result = pd_string_col[pd_int64_too == 0][~pd_bool_col] - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -1102,7 +1099,7 @@ def test_binop_repeated_application_does_row_identity_joins(scalars_dfs): bf_result = bf_series.to_pandas() pd_result = pd_series - assert_series_equal_ignoring_order( + assert_series_equal( bf_result, pd_result, ) @@ -1124,10 +1121,9 @@ def test_binop_opposite_filters(scalars_dfs): pd_bool_col = scalars_pandas_df["bool_col"] pd_result = pd_int64_col1[pd_bool_col] + pd_int64_col2[pd_bool_col.__invert__()] - assert_series_equal_ignoring_order( - bf_result, - pd_result, - ) + # Passes with ignore_order=False only with some dependency sets + # TODO: Determine desired behavior and make test more strict + assert_series_equal(bf_result, pd_result, ignore_order=True) def test_binop_left_filtered(scalars_dfs): @@ -1142,10 +1138,9 @@ def test_binop_left_filtered(scalars_dfs): pd_bool_col = scalars_pandas_df["bool_col"] pd_result = pd_int64_col[pd_bool_col] + pd_float64_col - assert_series_equal_ignoring_order( - bf_result, - pd_result, - ) + # Passes with ignore_order=False only with some dependency sets + # TODO: Determine desired behavior and make test more strict + assert_series_equal(bf_result, pd_result, ignore_order=True) def test_binop_right_filtered(scalars_dfs): @@ -1160,7 +1155,7 @@ def test_binop_right_filtered(scalars_dfs): pd_bool_col = scalars_pandas_df["bool_col"] pd_result = pd_float64_col + pd_int64_col[pd_bool_col] - assert_series_equal_ignoring_order( + assert_series_equal( bf_result, pd_result, ) @@ -1265,7 +1260,7 @@ def test_groupby_sum(scalars_dfs): ) # TODO(swast): Update groupby to use index based on group by key(s). bf_result = bf_series.to_pandas() - assert_series_equal_ignoring_order( + assert_series_equal( pd_series, bf_result, check_exact=False, @@ -1283,7 +1278,7 @@ def test_groupby_std(scalars_dfs): .astype(pd.Float64Dtype()) ) bf_result = bf_series.to_pandas() - assert_series_equal_ignoring_order( + assert_series_equal( pd_series, bf_result, check_exact=False, @@ -1298,7 +1293,7 @@ def test_groupby_var(scalars_dfs): scalars_pandas_df[col_name].groupby(scalars_pandas_df["string_col"]).var() ) bf_result = bf_series.to_pandas() - assert_series_equal_ignoring_order( + assert_series_equal( pd_series, bf_result, check_exact=False, @@ -1350,7 +1345,7 @@ def test_groupby_mean(scalars_dfs): ) # TODO(swast): Update groupby to use index based on group by key(s). bf_result = bf_series.to_pandas() - assert_series_equal_ignoring_order( + assert_series_equal( pd_series, bf_result, ) @@ -1388,7 +1383,7 @@ def test_groupby_prod(scalars_dfs): ) # TODO(swast): Update groupby to use index based on group by key(s). bf_result = bf_series.to_pandas() - assert_series_equal_ignoring_order( + assert_series_equal( pd_series, bf_result, ) @@ -1598,7 +1593,7 @@ def test_head(scalars_dfs): bf_result = scalars_df["string_col"].head(2).to_pandas() pd_result = scalars_pandas_df["string_col"].head(2) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -1613,7 +1608,7 @@ def test_tail(scalars_dfs): bf_result = scalars_df["string_col"].tail(2).to_pandas() pd_result = scalars_pandas_df["string_col"].tail(2) - assert_series_equal_ignoring_order( + assert_series_equal( pd_result, bf_result, ) @@ -2081,11 +2076,7 @@ def test_series_filter_items(scalars_df_index, scalars_pandas_df_index): # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) # Ignore ordering as pandas order differently depending on version - assert_series_equal_ignoring_order( - bf_result, - pd_result, - check_names=False, - ) + assert_series_equal(bf_result, pd_result, check_names=False, ignore_order=True) def test_series_filter_like(scalars_df_index, scalars_pandas_df_index): @@ -2213,21 +2204,25 @@ def test_where_with_default(scalars_df_index, scalars_pandas_df_index): ) -def test_clip(scalars_df_index, scalars_pandas_df_index): +@pytest.mark.parametrize( + ("ordered"), + [ + (True), + (False), + ], +) +def test_clip(scalars_df_index, scalars_pandas_df_index, ordered): col_bf = scalars_df_index["int64_col"] lower_bf = scalars_df_index["int64_too"] - 1 upper_bf = scalars_df_index["int64_too"] + 1 - bf_result = col_bf.clip(lower_bf, upper_bf).to_pandas() + bf_result = col_bf.clip(lower_bf, upper_bf).to_pandas(ordered=ordered) col_pd = scalars_pandas_df_index["int64_col"] lower_pd = scalars_pandas_df_index["int64_too"] - 1 upper_pd = scalars_pandas_df_index["int64_too"] + 1 pd_result = col_pd.clip(lower_pd, upper_pd) - pd.testing.assert_series_equal( - bf_result, - pd_result, - ) + assert_series_equal(bf_result, pd_result, ignore_order=not ordered) def test_clip_filtered_two_sided(scalars_df_index, scalars_pandas_df_index): @@ -2298,7 +2293,7 @@ def test_to_frame(scalars_dfs): bf_result = scalars_df["int64_col"].to_frame().to_pandas() pd_result = scalars_pandas_df["int64_col"].to_frame() - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_to_json(scalars_df_index, scalars_pandas_df_index): @@ -2466,7 +2461,7 @@ def test_mask_default_value(scalars_dfs): pd_col_masked = pd_col.mask(pd_col % 2 == 1) pd_result = pd_col.to_frame().assign(int64_col_masked=pd_col_masked) - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) def test_mask_custom_value(scalars_dfs): @@ -2484,7 +2479,7 @@ def test_mask_custom_value(scalars_dfs): # odd so should be left as is, but it is being masked in pandas. # Accidentally the bigframes bahavior matches, but it should be updated # after the resolution of https://github.com/pandas-dev/pandas/issues/52955 - assert_pandas_df_equal_ignore_ordering(bf_result, pd_result) + assert_pandas_df_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -2581,7 +2576,7 @@ def test_loc_bool_series_default_index( scalars_pandas_df_default_index.bool_col ] - assert_pandas_df_equal_ignore_ordering( + assert_pandas_df_equal( bf_result.to_frame(), pd_result.to_frame(), ) diff --git a/tests/system/utils.py b/tests/system/utils.py index e2daf3b8bf0..f7831972b83 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -21,29 +21,33 @@ import pyarrow as pa # type: ignore -def assert_pandas_df_equal_ignore_ordering(df0, df1, **kwargs): - # Sort by a column to get consistent results. - if df0.index.name != "rowindex": - df0 = df0.sort_values( - list(df0.columns.drop("geography_col", errors="ignore")) - ).reset_index(drop=True) - df1 = df1.sort_values( - list(df1.columns.drop("geography_col", errors="ignore")) - ).reset_index(drop=True) - else: - df0 = df0.sort_index() - df1 = df1.sort_index() +def assert_pandas_df_equal(df0, df1, ignore_order: bool = False, **kwargs): + if ignore_order: + # Sort by a column to get consistent results. + if df0.index.name != "rowindex": + df0 = df0.sort_values( + list(df0.columns.drop("geography_col", errors="ignore")) + ).reset_index(drop=True) + df1 = df1.sort_values( + list(df1.columns.drop("geography_col", errors="ignore")) + ).reset_index(drop=True) + else: + df0 = df0.sort_index() + df1 = df1.sort_index() pd.testing.assert_frame_equal(df0, df1, **kwargs) -def assert_series_equal_ignoring_order(left: pd.Series, right: pd.Series, **kwargs): - if left.index.name is None: - left = left.sort_values().reset_index(drop=True) - right = right.sort_values().reset_index(drop=True) - else: - left = left.sort_index() - right = right.sort_index() +def assert_series_equal( + left: pd.Series, right: pd.Series, ignore_order: bool = False, **kwargs +): + if ignore_order: + if left.index.name is None: + left = left.sort_values().reset_index(drop=True) + right = right.sort_values().reset_index(drop=True) + else: + left = left.sort_index() + right = right.sort_index() pd.testing.assert_series_equal(left, right, **kwargs) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index d9672b2635b..623448b3aa0 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -49,7 +49,7 @@ def test_arrayvalue_constructor_from_ibis_table_adds_all_columns(): ordering=ordering, hidden_ordering_columns=(), ) - assert actual.compile()._table is ibis_table + assert actual._compile_ordered()._table is ibis_table assert len(actual.column_ids) == 3 @@ -83,7 +83,7 @@ def test_arrayvalue_with_get_column(): ), total_ordering_columns=["col1"], ) - col1 = value.compile()._get_ibis_column("col1") + col1 = value._compile_ordered()._get_ibis_column("col1") assert isinstance(col1, ibis_types.Value) assert col1.get_name() == "col1" assert col1.type().is_int64() @@ -100,7 +100,7 @@ def test_arrayvalues_to_ibis_expr_with_get_column(): ), total_ordering_columns=["col1"], ) - expr = value.compile()._get_ibis_column("col1") + expr = value._compile_ordered()._get_ibis_column("col1") assert expr.get_name() == "col1" assert expr.type().is_int64() @@ -117,7 +117,7 @@ def test_arrayvalues_to_ibis_expr_with_concat(): total_ordering_columns=["col1"], ) expr = value.concat([value]) - actual = expr.compile()._to_ibis_expr("unordered") + actual = expr._compile_ordered()._to_ibis_expr(ordering_mode="unordered") assert len(actual.columns) == 3 # TODO(ashleyxu, b/299631930): test out the union expression assert actual.columns[0] == "column_0" @@ -136,8 +136,8 @@ def test_arrayvalues_to_ibis_expr_with_project_unary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_unary_op("col1", ops.AsTypeOp("string")).compile() - assert value.compile().columns[0].type().is_int64() + expr = value.project_unary_op("col1", ops.AsTypeOp("string"))._compile_ordered() + assert value._compile_ordered().columns[0].type().is_int64() assert expr.columns[0].type().is_string() @@ -152,9 +152,11 @@ def test_arrayvalues_to_ibis_expr_with_project_binary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_binary_op("col2", "col3", ops.add_op, "col4").compile() + expr = value.project_binary_op( + "col2", "col3", ops.add_op, "col4" + )._compile_ordered() assert expr.columns[3].type().is_float64() - actual = expr._to_ibis_expr("unordered") + actual = expr._to_ibis_expr(ordering_mode="unordered") assert len(expr.columns) == 4 assert actual.columns[3] == "col4" @@ -173,9 +175,9 @@ def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): ) expr = value.project_ternary_op( "col2", "col3", "col4", ops.where_op, "col5" - ).compile() + )._compile_ordered() assert expr.columns[4].type().is_float64() - actual = expr._to_ibis_expr("unordered") + actual = expr._to_ibis_expr(ordering_mode="unordered") assert len(expr.columns) == 5 assert actual.columns[4] == "col5" @@ -195,8 +197,8 @@ def test_arrayvalue_to_ibis_expr_with_aggregate(): aggregations=(("col1", agg_ops.sum_op, "col4"),), by_column_ids=["col1"], dropna=False, - ).compile() - actual = expr._to_ibis_expr("unordered") + )._compile_ordered() + actual = expr._to_ibis_expr(ordering_mode="unordered") assert len(expr.columns) == 2 assert actual.columns[0] == "col1" assert actual.columns[1] == "col4" @@ -214,8 +216,10 @@ def test_arrayvalue_to_ibis_expr_with_corr_aggregate(): ), total_ordering_columns=["col1"], ) - expr = value.corr_aggregate(corr_aggregations=[("col1", "col3", "col4")]).compile() - actual = expr._to_ibis_expr("unordered") + expr = value.corr_aggregate( + corr_aggregations=[("col1", "col3", "col4")] + )._compile_ordered() + actual = expr._to_ibis_expr(ordering_mode="unordered") assert len(expr.columns) == 1 assert actual.columns[0] == "col4" assert expr.columns[0].type().is_float64() From 30b8ba2409f212631189bb2494baa8a34086bd6e Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 00:50:16 +0000 Subject: [PATCH 147/921] build: bump dependencies in .kokoro/requirements.txt (#189) Source-Link: https://togithub.com/googleapis/synthtool/commit/febacccc98d6d224aff9d0bd0373bb5a4cd5969c Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:caffe0a9277daeccc4d1de5c9b55ebba0901b57c2f713ec9c876b0d4ec064f61 --- .github/.OwlBot.lock.yaml | 4 +- .kokoro/requirements.txt | 532 ++++++++++++++++++++------------------ 2 files changed, 277 insertions(+), 259 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index ec696b558c3..453b540c1e5 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:30470597773378105e239b59fce8eb27cc97375580d592699206d17d117143d0 -# created: 2023-11-03T00:57:07.335914631Z + digest: sha256:caffe0a9277daeccc4d1de5c9b55ebba0901b57c2f713ec9c876b0d4ec064f61 +# created: 2023-11-08T19:46:45.022803742Z diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 16170d0ca7b..8957e21104e 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -4,91 +4,75 @@ # # pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==2.0.0 \ - --hash=sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20 \ - --hash=sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e +argcomplete==3.1.4 \ + --hash=sha256:72558ba729e4c468572609817226fb0a6e7e9a0a7d477b882be168c0b4a62b94 \ + --hash=sha256:fbe56f8cda08aa9a04b307d8482ea703e96a6a801611acb4be9bf3942017989f # via nox -attrs==22.1.0 \ - --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \ - --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c +attrs==23.1.0 \ + --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ + --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 # via gcp-releasetool -bleach==5.0.1 \ - --hash=sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a \ - --hash=sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c - # via readme-renderer -cachetools==5.2.0 \ - --hash=sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757 \ - --hash=sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db +cachetools==5.3.2 \ + --hash=sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2 \ + --hash=sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1 # via google-auth certifi==2023.7.22 \ --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 # via requests -cffi==1.15.1 \ - --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ - --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ - --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ - --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ - --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ - --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ - --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ - --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ - --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ - --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ - --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ - --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ - --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ - --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ - --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ - --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ - --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ - --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ - --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ - --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ - --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ - --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ - --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ - --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ - --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ - --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ - --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ - --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ - --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ - --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ - --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ - --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ - --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ - --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ - --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ - --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ - --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ - --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ - --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ - --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ - --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ - --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ - --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ - --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ - --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ - --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ - --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ - --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ - --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ - --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ - --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ - --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ - --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ - --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ - --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ - --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ - --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ - --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ - --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ - --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ - --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ - --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ - --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ - --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 +cffi==1.16.0 \ + --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ + --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ + --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ + --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ + --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ + --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ + --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ + --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ + --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ + --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ + --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ + --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ + --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ + --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ + --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ + --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ + --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ + --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ + --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ + --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ + --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ + --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ + --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ + --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ + --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ + --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ + --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ + --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ + --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ + --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ + --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ + --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ + --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ + --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ + --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ + --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ + --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ + --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ + --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ + --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ + --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ + --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ + --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ + --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ + --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ + --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ + --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ + --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ + --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ + --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ + --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ + --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 # via cryptography charset-normalizer==2.1.1 \ --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ @@ -109,78 +93,74 @@ colorlog==6.7.0 \ # via # gcp-docuploader # nox -commonmark==0.9.1 \ - --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ - --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 - # via rich -cryptography==41.0.4 \ - --hash=sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67 \ - --hash=sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311 \ - --hash=sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8 \ - --hash=sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13 \ - --hash=sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143 \ - --hash=sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f \ - --hash=sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829 \ - --hash=sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd \ - --hash=sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397 \ - --hash=sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac \ - --hash=sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d \ - --hash=sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a \ - --hash=sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839 \ - --hash=sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e \ - --hash=sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6 \ - --hash=sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9 \ - --hash=sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860 \ - --hash=sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca \ - --hash=sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91 \ - --hash=sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d \ - --hash=sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714 \ - --hash=sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb \ - --hash=sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f +cryptography==41.0.5 \ + --hash=sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf \ + --hash=sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84 \ + --hash=sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e \ + --hash=sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8 \ + --hash=sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7 \ + --hash=sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1 \ + --hash=sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88 \ + --hash=sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86 \ + --hash=sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179 \ + --hash=sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81 \ + --hash=sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20 \ + --hash=sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548 \ + --hash=sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d \ + --hash=sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d \ + --hash=sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5 \ + --hash=sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1 \ + --hash=sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147 \ + --hash=sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936 \ + --hash=sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797 \ + --hash=sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696 \ + --hash=sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72 \ + --hash=sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da \ + --hash=sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723 # via # gcp-releasetool # secretstorage -distlib==0.3.6 \ - --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ - --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e +distlib==0.3.7 \ + --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \ + --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8 # via virtualenv -docutils==0.19 \ - --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ - --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc +docutils==0.20.1 \ + --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \ + --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b # via readme-renderer -filelock==3.8.0 \ - --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \ - --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4 +filelock==3.13.1 \ + --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ + --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c # via virtualenv -gcp-docuploader==0.6.4 \ - --hash=sha256:01486419e24633af78fd0167db74a2763974765ee8078ca6eb6964d0ebd388af \ - --hash=sha256:70861190c123d907b3b067da896265ead2eeb9263969d6955c9e0bb091b5ccbf +gcp-docuploader==0.6.5 \ + --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ + --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==1.10.5 \ - --hash=sha256:174b7b102d704b254f2a26a3eda2c684fd3543320ec239baf771542a2e58e109 \ - --hash=sha256:e29d29927fe2ca493105a82958c6873bb2b90d503acac56be2c229e74de0eec9 +gcp-releasetool==1.16.0 \ + --hash=sha256:27bf19d2e87aaa884096ff941aa3c592c482be3d6a2bfe6f06afafa6af2353e3 \ + --hash=sha256:a316b197a543fd036209d0caba7a8eb4d236d8e65381c80cbc6d7efaa7606d63 # via -r requirements.in -google-api-core==2.10.2 \ - --hash=sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320 \ - --hash=sha256:34f24bd1d5f72a8c4519773d99ca6bf080a6c4e041b4e9f024fe230191dda62e +google-api-core==2.12.0 \ + --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \ + --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160 # via # google-cloud-core # google-cloud-storage -google-auth==2.14.1 \ - --hash=sha256:ccaa901f31ad5cbb562615eb8b664b3dd0bf5404a67618e642307f00613eda4d \ - --hash=sha256:f5d8701633bebc12e0deea4df8abd8aff31c28b355360597f7f2ee60f2e4d016 +google-auth==2.23.4 \ + --hash=sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3 \ + --hash=sha256:d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2 # via # gcp-releasetool # google-api-core # google-cloud-core # google-cloud-storage -google-cloud-core==2.3.2 \ - --hash=sha256:8417acf6466be2fa85123441696c4badda48db314c607cf1e5d543fa8bdc22fe \ - --hash=sha256:b9529ee7047fd8d4bf4a2182de619154240df17fbe60ead399078c1ae152af9a +google-cloud-core==2.3.3 \ + --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \ + --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863 # via google-cloud-storage -google-cloud-storage==2.6.0 \ - --hash=sha256:104ca28ae61243b637f2f01455cc8a05e8f15a2a18ced96cb587241cdd3820f5 \ - --hash=sha256:4ad0415ff61abdd8bb2ae81c1f8f7ec7d91a1011613f2db87c614c550f97bfe9 +google-cloud-storage==2.13.0 \ + --hash=sha256:ab0bf2e1780a1b74cf17fccb13788070b729f50c252f0c94ada2aae0ca95437d \ + --hash=sha256:f62dc4c7b6cd4360d072e3deb28035fbdad491ac3d9b0b1815a12daea10f37c7 # via gcp-docuploader google-crc32c==1.5.0 \ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ @@ -251,29 +231,31 @@ google-crc32c==1.5.0 \ --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4 - # via google-resumable-media -google-resumable-media==2.4.0 \ - --hash=sha256:2aa004c16d295c8f6c33b2b4788ba59d366677c0a25ae7382436cb30f776deaa \ - --hash=sha256:8d5518502f92b9ecc84ac46779bd4f09694ecb3ba38a3e7ca737a86d15cbca1f + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.6.0 \ + --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \ + --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b # via google-cloud-storage -googleapis-common-protos==1.57.0 \ - --hash=sha256:27a849d6205838fb6cc3c1c21cb9800707a661bb21c6ce7fb13e99eb1f8a0c46 \ - --hash=sha256:a9f4a1d7f6d9809657b7f1316a1aa527f6664891531bcfcc13b6696e685f443c +googleapis-common-protos==1.61.0 \ + --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \ + --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b # via google-api-core idna==3.4 \ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests -importlib-metadata==5.0.0 \ - --hash=sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab \ - --hash=sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43 +importlib-metadata==6.8.0 \ + --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ + --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 # via # -r requirements.in # keyring # twine -jaraco-classes==3.2.3 \ - --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \ - --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a +jaraco-classes==3.3.0 \ + --hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \ + --hash=sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621 # via keyring jeepney==0.8.0 \ --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ @@ -285,75 +267,121 @@ jinja2==3.1.2 \ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 # via gcp-releasetool -keyring==23.11.0 \ - --hash=sha256:3dd30011d555f1345dec2c262f0153f2f0ca6bca041fb1dc4588349bb4c0ac1e \ - --hash=sha256:ad192263e2cdd5f12875dedc2da13534359a7e760e77f8d04b50968a821c2361 +keyring==24.2.0 \ + --hash=sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6 \ + --hash=sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509 # via # gcp-releasetool # twine -markupsafe==2.1.1 \ - --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \ - --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \ - --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \ - --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \ - --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \ - --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \ - --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \ - --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \ - --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \ - --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \ - --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \ - --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \ - --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \ - --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \ - --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \ - --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \ - --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \ - --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \ - --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \ - --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \ - --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \ - --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \ - --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \ - --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \ - --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \ - --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \ - --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \ - --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \ - --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \ - --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \ - --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \ - --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \ - --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \ - --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \ - --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \ - --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \ - --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \ - --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \ - --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \ - --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7 +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via rich +markupsafe==2.1.3 \ + --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ + --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ + --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ + --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ + --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ + --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ + --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ + --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ + --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ + --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ + --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ + --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ + --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ + --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ + --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ + --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ + --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ + --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ + --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ + --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ + --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ + --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ + --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ + --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ + --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ + --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ + --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ + --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ + --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ + --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ + --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ + --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ + --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ + --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ + --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ + --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ + --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ + --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ + --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ + --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ + --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ + --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ + --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ + --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ + --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ + --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ + --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ + --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ + --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ + --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ + --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ + --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ + --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ + --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ + --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ + --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ + --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ + --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ + --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ + --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 # via jinja2 -more-itertools==9.0.0 \ - --hash=sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41 \ - --hash=sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +more-itertools==10.1.0 \ + --hash=sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a \ + --hash=sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6 # via jaraco-classes -nox==2022.11.21 \ - --hash=sha256:0e41a990e290e274cb205a976c4c97ee3c5234441a8132c8c3fd9ea3c22149eb \ - --hash=sha256:e21c31de0711d1274ca585a2c5fde36b1aa962005ba8e9322bf5eeed16dcd684 +nh3==0.2.14 \ + --hash=sha256:116c9515937f94f0057ef50ebcbcc10600860065953ba56f14473ff706371873 \ + --hash=sha256:18415df36db9b001f71a42a3a5395db79cf23d556996090d293764436e98e8ad \ + --hash=sha256:203cac86e313cf6486704d0ec620a992c8bc164c86d3a4fd3d761dd552d839b5 \ + --hash=sha256:2b0be5c792bd43d0abef8ca39dd8acb3c0611052ce466d0401d51ea0d9aa7525 \ + --hash=sha256:377aaf6a9e7c63962f367158d808c6a1344e2b4f83d071c43fbd631b75c4f0b2 \ + --hash=sha256:525846c56c2bcd376f5eaee76063ebf33cf1e620c1498b2a40107f60cfc6054e \ + --hash=sha256:5529a3bf99402c34056576d80ae5547123f1078da76aa99e8ed79e44fa67282d \ + --hash=sha256:7771d43222b639a4cd9e341f870cee336b9d886de1ad9bec8dddab22fe1de450 \ + --hash=sha256:88c753efbcdfc2644a5012938c6b9753f1c64a5723a67f0301ca43e7b85dcf0e \ + --hash=sha256:93a943cfd3e33bd03f77b97baa11990148687877b74193bf777956b67054dcc6 \ + --hash=sha256:9be2f68fb9a40d8440cbf34cbf40758aa7f6093160bfc7fb018cce8e424f0c3a \ + --hash=sha256:a0c509894fd4dccdff557068e5074999ae3b75f4c5a2d6fb5415e782e25679c4 \ + --hash=sha256:ac8056e937f264995a82bf0053ca898a1cb1c9efc7cd68fa07fe0060734df7e4 \ + --hash=sha256:aed56a86daa43966dd790ba86d4b810b219f75b4bb737461b6886ce2bde38fd6 \ + --hash=sha256:e8986f1dd3221d1e741fda0a12eaa4a273f1d80a35e31a1ffe579e7c621d069e \ + --hash=sha256:f99212a81c62b5f22f9e7c3e347aa00491114a5647e1f13bbebd79c3e5f08d75 + # via readme-renderer +nox==2023.4.22 \ + --hash=sha256:0b1adc619c58ab4fa57d6ab2e7823fe47a32e70202f287d78474adcc7bda1891 \ + --hash=sha256:46c0560b0dc609d7d967dc99e22cb463d3c4caf54a5fda735d6c11b5177e3a9f # via -r requirements.in -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via # gcp-releasetool # nox -pkginfo==1.8.3 \ - --hash=sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594 \ - --hash=sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c +pkginfo==1.9.6 \ + --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \ + --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046 # via twine -platformdirs==2.5.4 \ - --hash=sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7 \ - --hash=sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10 +platformdirs==3.11.0 \ + --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ + --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e # via virtualenv protobuf==3.20.3 \ --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ @@ -383,34 +411,30 @@ protobuf==3.20.3 \ # gcp-releasetool # google-api-core # googleapis-common-protos -pyasn1==0.4.8 \ - --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ - --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba +pyasn1==0.5.0 \ + --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \ + --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde # via # pyasn1-modules # rsa -pyasn1-modules==0.2.8 \ - --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \ - --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 +pyasn1-modules==0.3.0 \ + --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ + --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d # via google-auth pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pygments==2.15.0 \ - --hash=sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094 \ - --hash=sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500 +pygments==2.16.1 \ + --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \ + --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29 # via # readme-renderer # rich -pyjwt==2.6.0 \ - --hash=sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd \ - --hash=sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14 +pyjwt==2.8.0 \ + --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ + --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via gcp-releasetool -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via packaging pyperclip==1.8.2 \ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 # via gcp-releasetool @@ -418,9 +442,9 @@ python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 # via gcp-releasetool -readme-renderer==37.3 \ - --hash=sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273 \ - --hash=sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343 +readme-renderer==42.0 \ + --hash=sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d \ + --hash=sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1 # via twine requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ @@ -431,17 +455,17 @@ requests==2.31.0 \ # google-cloud-storage # requests-toolbelt # twine -requests-toolbelt==0.10.1 \ - --hash=sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7 \ - --hash=sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d +requests-toolbelt==1.0.0 \ + --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ + --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 # via twine rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==12.6.0 \ - --hash=sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e \ - --hash=sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0 +rich==13.6.0 \ + --hash=sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245 \ + --hash=sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef # via twine rsa==4.9 \ --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ @@ -455,43 +479,37 @@ six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via - # bleach # gcp-docuploader - # google-auth # python-dateutil -twine==4.0.1 \ - --hash=sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e \ - --hash=sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0 +twine==4.0.2 \ + --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \ + --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8 # via -r requirements.in -typing-extensions==4.4.0 \ - --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ - --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e +typing-extensions==4.8.0 \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef # via -r requirements.in -urllib3==1.26.18 \ - --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ - --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 +urllib3==2.0.7 \ + --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \ + --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e # via # requests # twine -virtualenv==20.16.7 \ - --hash=sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e \ - --hash=sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29 +virtualenv==20.24.6 \ + --hash=sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af \ + --hash=sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381 # via nox -webencodings==0.5.1 \ - --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ - --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 - # via bleach -wheel==0.38.4 \ - --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ - --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 +wheel==0.41.3 \ + --hash=sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942 \ + --hash=sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841 # via -r requirements.in -zipp==3.10.0 \ - --hash=sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1 \ - --hash=sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8 +zipp==3.17.0 \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==65.5.1 \ - --hash=sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31 \ - --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f +setuptools==68.2.2 \ + --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \ + --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a # via -r requirements.in From d49ae42a379fafd601cc94227e7f8f14b3d5f8c3 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 8 Nov 2023 17:31:50 -0800 Subject: [PATCH 148/921] docs: add llm kmeans notebook as an included example (#177) * docs: add llm kmeans notebook as an included example --------- Co-authored-by: Henry J Solberg Co-authored-by: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> --- .../bq_dataframes_llm_kmeans.ipynb | 690 ++++++++++++++++++ noxfile.py | 1 + 2 files changed, 691 insertions(+) create mode 100644 notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb new file mode 100644 index 00000000000..46c49552889 --- /dev/null +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -0,0 +1,690 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use BigQuery DataFrames to cluster and characterize complaints\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "The goal of this notebook is to demonstrate a comment characterization algorithm for an online business. We will accomplish this using [Google's PaLM 2](https://ai.google/discover/palm2/) and [KMeans clustering](https://en.wikipedia.org/wiki/K-means_clustering) in three steps:\n", + "\n", + "1. Use PaLM2TextEmbeddingGenerator to [generate text embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) for each of 10000 complaints sent to an online bank. If you're not familiar with what a text embedding is, it's a list of numbers that are like coordinates in an imaginary \"meaning space\" for sentences. (It's like [word embeddings](https://en.wikipedia.org/wiki/Word_embedding), but for more general text.) The important point for our purposes is that similar sentences are close to each other in this imaginary space.\n", + "2. Use KMeans clustering to group together complaints whose text embeddings are near to eachother. This will give us sets of similar complaints, but we don't yet know _why_ these complaints are similar.\n", + "3. Prompt PaLM2TextGenerator in English asking what the difference is between the groups of complaints that we got. Thanks to the power of modern LLMs, the response might give us a very good idea of what these complaints are all about, but remember to [\"understand the limits of your dataset and model.\"](https://ai.google/responsibility/responsible-ai-practices/#:~:text=Understand%20the%20limitations%20of%20your%20dataset%20and%20model)\n", + "\n", + "We will tie these pieces together in Python using BigQuery DataFrames. [Click here](https://cloud.google.com/bigquery/docs/dataframes-quickstart) to learn more about BigQuery DataFrames!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dataset\n", + "\n", + "This notebook uses the [CFPB Consumer Complaint Database](https://console.cloud.google.com/marketplace/product/cfpb/complaint-database)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "* Generative AI support on Vertex AI\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models), [Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models),\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,run.googleapis.com,artifactregistry.googleapis.com,cloudbuild.googleapis.com,cloudresourcemanager.googleapis.com) to enable the following APIs:\n", + "\n", + " * BigQuery API\n", + " * BigQuery Connection API\n", + " * Cloud Run API\n", + " * Artifact Registry API\n", + " * Cloud Build API\n", + " * Cloud Resource Manager API\n", + " * Vertex AI API\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, see the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# set your project ID below\n", + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id in gcloud\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Connect to Vertex AI\n", + "\n", + "In order to use PaLM2TextGenerator, we will need to set up a [cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from google.cloud import bigquery_connection_v1 as bq_connection\n", + "\n", + "CONN_NAME = \"bqdf-llm\"\n", + "\n", + "client = bq_connection.ConnectionServiceClient()\n", + "new_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}\"\n", + "exists_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", + "cloud_resource_properties = bq_connection.CloudResourceProperties({})\n", + "\n", + "try:\n", + " request = client.get_connection(\n", + " request=bq_connection.GetConnectionRequest(name=exists_conn_parent)\n", + " )\n", + " CONN_SERVICE_ACCOUNT = f\"serviceAccount:{request.cloud_resource.service_account_id}\"\n", + "except Exception:\n", + " connection = bq_connection.types.Connection(\n", + " {\"friendly_name\": CONN_NAME, \"cloud_resource\": cloud_resource_properties}\n", + " )\n", + " request = bq_connection.CreateConnectionRequest(\n", + " {\n", + " \"parent\": new_conn_parent,\n", + " \"connection_id\": CONN_NAME,\n", + " \"connection\": connection,\n", + " }\n", + " )\n", + " response = client.create_connection(request)\n", + " CONN_SERVICE_ACCOUNT = (\n", + " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", + " )\n", + "print(CONN_SERVICE_ACCOUNT)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set permissions for the service account\n", + "\n", + "The resource connection service account requires certain project-level permissions:\n", + " - `roles/aiplatform.user` and `roles/bigquery.connectionUser`: These roles are required for the connection to create a model definition using the LLM model in Vertex AI ([documentation](https://cloud.google.com/bigquery/docs/generate-text#give_the_service_account_access)).\n", + " - `roles/run.invoker`: This role is required for the connection to have read-only access to Cloud Run services that back custom/remote functions ([documentation](https://cloud.google.com/bigquery/docs/remote-functions#grant_permission_on_function)).\n", + "\n", + "Set these permissions by running the following `gcloud` commands:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we are ready to use BigQuery DataFrames!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "xckgWno6ouHY" + }, + "source": [ + "## Step 1: Text embedding " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Project Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R7STCS8xB5d2" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "v6FGschEowht" + }, + "source": [ + "Data Input - read the data from a publicly available BigQuery dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zDSwoBo1CU3G" + }, + "outputs": [], + "source": [ + "input_df = bf.read_gbq(\"bigquery-public-data.cfpb_complaints.complaint_database\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tYDoaKgJChiq" + }, + "outputs": [], + "source": [ + "issues_df = input_df[[\"consumer_complaint_narrative\"]].dropna()\n", + "issues_df.head(n=5) # View the first five complaints" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Download 10000 complaints to use with PaLM2TextEmbeddingGenerator" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OltYSUEcsSOW" + }, + "outputs": [], + "source": [ + "# Choose 10,000 complaints randomly and store them in a column in a DataFrame\n", + "downsampled_issues_df = issues_df.sample(n=10000)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "Wl2o-NYMoygb" + }, + "source": [ + "Generate the text embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "li38q8FzDDMu" + }, + "outputs": [], + "source": [ + "from bigframes.ml.llm import PaLM2TextEmbeddingGenerator\n", + "\n", + "model = PaLM2TextEmbeddingGenerator() # No connection id needed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cOuSOQ5FDewD" + }, + "outputs": [], + "source": [ + "# Will take ~3 minutes to compute the embeddings\n", + "predicted_embeddings = model.predict(downsampled_issues_df)\n", + "# Notice the lists of numbers that are our text embeddings for each complaint\n", + "predicted_embeddings.head() " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4H_etYfsEOFP" + }, + "outputs": [], + "source": [ + "# Join the complaints with their embeddings in the same DataFrame\n", + "combined_df = downsampled_issues_df.join(predicted_embeddings)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now have the complaints and their text embeddings as two columns in our combined_df. Recall that complaints with numerically similar text embeddings should have similar meanings semantically. We will now group similar complaints together." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "OUZ3NNbzo1Tb" + }, + "source": [ + "## Step 2: KMeans clustering" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AhNTnEC5FRz2" + }, + "outputs": [], + "source": [ + "from bigframes.ml.cluster import KMeans\n", + "\n", + "cluster_model = KMeans(n_clusters=10) # We will divide our complaints into 10 groups" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perform KMeans clustering" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6poSxh-fGJF7" + }, + "outputs": [], + "source": [ + "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", + "cluster_model.fit(combined_df[[\"text_embedding\"]])\n", + "clustered_result = cluster_model.predict(combined_df[[\"text_embedding\"]])\n", + "# Notice the CENTROID_ID column, which is the ID number of the group that\n", + "# each complaint belongs to.\n", + "clustered_result.head(n=5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Join the group number to the complaints and their text embeddings\n", + "combined_clustered_result = combined_df.join(clustered_result)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our dataframe combined_clustered_result now has three columns: the complaints, their text embeddings, and an ID from 1-10 (inclusive) indicating which semantically similar group they belong to." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "21rNsFMHo8hO" + }, + "source": [ + "## Step 3: Summarize the complaints" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Build prompts - we will choose just two of our categories and prompt PaLM2TextGenerator to identify their salient characteristics. The prompt is natural language in a python string." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2E7wXM_jGqo6" + }, + "outputs": [], + "source": [ + "# Using bigframes, with syntax identical to pandas,\n", + "# filter out the first and second groups\n", + "cluster_1_result = combined_clustered_result[\n", + " combined_clustered_result[\"CENTROID_ID\"] == 1\n", + "][[\"consumer_complaint_narrative\"]]\n", + "cluster_1_result_pandas = cluster_1_result.head(5).to_pandas()\n", + "\n", + "cluster_2_result = combined_clustered_result[\n", + " combined_clustered_result[\"CENTROID_ID\"] == 2\n", + "][[\"consumer_complaint_narrative\"]]\n", + "cluster_2_result_pandas = cluster_2_result.head(5).to_pandas()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZNDiueI9IP5e" + }, + "outputs": [], + "source": [ + "# Build plain-text prompts to send to PaLM 2. Use only 5 complaints from each group.\n", + "prompt1 = 'comment list 1:\\n'\n", + "for i in range(5):\n", + " prompt1 += str(i + 1) + '. ' + \\\n", + " cluster_1_result_pandas[\"consumer_complaint_narrative\"].iloc[i] + '\\n'\n", + "\n", + "prompt2 = 'comment list 2:\\n'\n", + "for i in range(5):\n", + " prompt2 += str(i + 1) + '. ' + \\\n", + " cluster_2_result_pandas[\"consumer_complaint_narrative\"].iloc[i] + '\\n'\n", + "\n", + "print(prompt1)\n", + "print(prompt2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BfHGJLirzSvH" + }, + "outputs": [], + "source": [ + "# The plain English request we will make of PaLM 2\n", + "prompt = (\n", + " \"Please highlight the most obvious difference between\"\n", + " \"the two lists of comments:\\n\" + prompt1 + prompt2\n", + ")\n", + "print(prompt)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get a response from PaLM 2 LLM by making a call to Vertex AI using our connection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mL5P0_3X04dE" + }, + "outputs": [], + "source": [ + "from bigframes.ml.llm import PaLM2TextGenerator\n", + "\n", + "session = bf.get_global_session()\n", + "connection = f\"{PROJECT_ID}.{REGION}.{CONN_NAME}\"\n", + "q_a_model = PaLM2TextGenerator(session=session, connection_name=connection)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ICWHsqAW1FNk" + }, + "outputs": [], + "source": [ + "# Make a DataFrame containing only a single row with our prompt for PaLM 2\n", + "df = bf.DataFrame({\"prompt\": [prompt]})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gB7e1LXU1pst" + }, + "outputs": [], + "source": [ + "# Send the request for PaLM 2 to generate a response to our prompt\n", + "major_difference = q_a_model.predict(df)\n", + "# PaLM 2's response is the only row in the dataframe result \n", + "major_difference[\"ml_generate_text_llm_result\"].iloc[0]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now see PaLM2TextGenerator's characterization of the different comment groups. Thanks for using BigQuery DataFrames!" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "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.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/noxfile.py b/noxfile.py index 34b055de445..3dd23ba04f6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -609,6 +609,7 @@ def notebook(session): # our test infrastructure. "notebooks/getting_started/getting_started_bq_dataframes.ipynb", "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", + "notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb", "notebooks/regression/bq_dataframes_ml_linear_regression.ipynb", "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", From 37914a4077c681881491f5c36d1a9c9f4255e18f Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 8 Nov 2023 18:08:13 -0800 Subject: [PATCH 149/921] feat: add series.sample (identical to existing dataframe.sample) (#187) We're duplicating some arg-parsing logic here. Discussed briefly with Trevor. This is the case for other methods as well- we might want to add a sharing mechanism for dataframe/series (superclass like pandas?) in the future. The documentation already exists in third_party/core/generic.py, which is actually what prompted this feat/fix. --- bigframes/series.py | 16 ++++++++++++++++ tests/system/small/test_series.py | 27 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/bigframes/series.py b/bigframes/series.py index 1952acbf6d5..4fab1fe9439 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1447,6 +1447,22 @@ def map( result_df = self_df.join(map_df, on="series") return result_df[self.name] + def sample( + self, + n: Optional[int] = None, + frac: Optional[float] = None, + *, + random_state: Optional[int] = None, + ) -> Series: + if n is not None and frac is not None: + raise ValueError("Only one of 'n' or 'frac' parameter can be specified.") + + ns = (n,) if n is not None else () + fracs = (frac,) if frac is not None else () + return Series( + self._block._split(ns=ns, fracs=fracs, random_state=random_state)[0] + ) + def __array_ufunc__( self, ufunc: numpy.ufunc, method: str, *inputs, **kwargs ) -> Series: diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index f59d64fe06f..d9fc23fad01 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2922,3 +2922,30 @@ def test_map_series_input_duplicates_error(scalars_dfs): scalars_pandas_df.int64_too.map(pd_map_series) with pytest.raises(pd.errors.InvalidIndexError): scalars_df.int64_too.map(bf_map_series, verify_integrity=True) + + +@pytest.mark.parametrize( + ("frac", "n", "random_state"), + [ + (None, 4, None), + (0.5, None, None), + (None, 4, 10), + (0.5, None, 10), + (None, None, None), + ], + ids=[ + "n_wo_random_state", + "frac_wo_random_state", + "n_w_random_state", + "frac_w_random_state", + "n_default", + ], +) +def test_sample(scalars_dfs, frac, n, random_state): + scalars_df, _ = scalars_dfs + df = scalars_df.int64_col.sample(frac=frac, n=n, random_state=random_state) + bf_result = df.to_pandas() + + n = 1 if n is None else n + expected_sample_size = round(frac * scalars_df.shape[0]) if frac is not None else n + assert bf_result.shape[0] == expected_sample_size From c816d843e6f3c5a944cd4395ed0e1e91cec49812 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 9 Nov 2023 22:19:24 +0000 Subject: [PATCH 150/921] docs: Add docstring code samples for `Series.apply` and `DataFrame.map` (#185) * docs: Add docstring code samples for `Series.apply` and `DataFrame.map` * improved docstring with concurrency-safe code samples * Correct indentation of text in code samples --- .../bigframes_vendored/pandas/core/frame.py | 62 ++++++++++++++++- .../bigframes_vendored/pandas/core/series.py | 68 +++++++++++++++++-- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 10f73d25b77..b35d0f3b2e8 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2159,8 +2159,68 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: In pandas 2.1.0, DataFrame.applymap is deprecated and renamed to DataFrame.map. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Let's use ``reuse=False`` flag to make sure a new ``remote_function`` + is created every time we run the following code, but you can skip it + to potentially reuse a previously deployed ``remote_function`` from + the same user defined function. + + >>> @bpd.remote_function([int], float, reuse=False) + ... def minutes_to_hours(x): + ... return x/60 + + >>> df_minutes = bpd.DataFrame( + ... {"system_minutes" : [0, 30, 60, 90, 120], + ... "user_minutes" : [0, 15, 75, 90, 6]}) + >>> df_minutes + system_minutes user_minutes + 0 0 0 + 1 30 15 + 2 60 75 + 3 90 90 + 4 120 6 + + [5 rows x 2 columns] + + >>> df_hours = df_minutes.map(minutes_to_hours) + >>> df_hours + system_minutes user_minutes + 0 0.0 0.0 + 1 0.5 0.25 + 2 1.0 1.25 + 3 1.5 1.5 + 4 2.0 0.1 + + [5 rows x 2 columns] + + If there are ``NA``/``None`` values in the data, you can ignore + applying the remote function on such values by specifying + ``na_action='ignore'``. + + >>> df_minutes = bpd.DataFrame( + ... { + ... "system_minutes" : [0, 30, 60, None, 90, 120, bpd.NA], + ... "user_minutes" : [0, 15, 75, 90, 6, None, bpd.NA] + ... }, dtype="Int64") + >>> df_hours = df_minutes.map(minutes_to_hours, na_action='ignore') + >>> df_hours + system_minutes user_minutes + 0 0.0 0.0 + 1 0.5 0.25 + 2 1.0 1.25 + 3 1.5 + 4 1.5 0.1 + 5 2.0 + 6 + + [7 rows x 2 columns] + Args: - func: + func (function): Python function wrapped by ``remote_function`` decorator, returns a single value from a single value. na_action (Optional[str], default None): diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index b25b0c75cff..c6d98075f57 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -728,18 +728,74 @@ def apply( func, ) -> DataFrame | Series: """ - Invoke function on values of Series. + Invoke function on values of a Series. - Can be ufunc (a NumPy function that applies to the entire Series) - or a Python function that only works on single values. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Let's use ``reuse=False`` flag to make sure a new ``remote_function`` + is created every time we run the following code, but you can skip it + to potentially reuse a previously deployed ``remote_function`` from + the same user defined function. + + >>> @bpd.remote_function([int], float, reuse=False) + ... def minutes_to_hours(x): + ... return x/60 + + >>> minutes = bpd.Series([0, 30, 60, 90, 120]) + >>> minutes + 0 0 + 1 30 + 2 60 + 3 90 + 4 120 + dtype: Int64 + + >>> hours = minutes.apply(minutes_to_hours) + >>> hours + 0 0.0 + 1 0.5 + 2 1.0 + 3 1.5 + 4 2.0 + dtype: Float64 + + You could turn a user defined function with external package + dependencies into a BigQuery DataFrames remote function. You would + provide the names of the packages via ``packages`` param. + + >>> @bpd.remote_function( + ... [str], + ... str, + ... reuse=False, + ... packages=["cryptography"], + ... ) + ... def get_hash(input): + ... from cryptography.fernet import Fernet + ... + ... # handle missing value + ... if input is None: + ... input = "" + ... + ... key = Fernet.generate_key() + ... f = Fernet(key) + ... return f.encrypt(input.encode()).decode() + + >>> names = bpd.Series(["Alice", "Bob"]) + >>> hashes = names.apply(get_hash) Args: func (function): - Python function or NumPy ufunc to apply. + BigFrames DataFrames ``remote_function`` to apply. The function + should take a scalar and return a scalar. It will be applied to + every element in the ``Series``. Returns: - bigframes.series.Series: If func returns a Series object the result - will be a DataFrame. + bigframes.series.Series: A new Series with values representing the + return value of the ``func`` applied to each element of the original + Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 9d2e6dc1ae4e11e80da4aabe0daa3a6044137cc6 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:00:23 -0800 Subject: [PATCH 151/921] fix: use random table when loading data for `read_csv`, `read_json`, `read_parquet` (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/session/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 8f9fa377873..bd5845631b2 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -806,7 +806,7 @@ def _read_gbq_with_ordering( def _read_bigquery_load_job( self, filepath_or_buffer: str | IO["bytes"], - table: bigquery.Table, + table: Union[bigquery.Table, bigquery.TableReference], *, job_config: bigquery.LoadJobConfig, index_col: Iterable[str] | str = (), @@ -1042,7 +1042,7 @@ def read_csv( encoding: Optional[str] = None, **kwargs, ) -> dataframe.DataFrame: - table = bigquery.Table(self._create_session_table()) + table = bigframes_io.random_table(self._anonymous_dataset) if engine is not None and engine == "bigquery": if any(param is not None for param in (dtype, names)): @@ -1156,7 +1156,7 @@ def read_parquet( # Note: "engine" is omitted because it is redundant. Loading a table # from a pandas DataFrame will just create another parquet file + load # job anyway. - table = bigquery.Table(self._create_session_table()) + table = bigframes_io.random_table(self._anonymous_dataset) job_config = bigquery.LoadJobConfig() job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED @@ -1179,7 +1179,7 @@ def read_json( engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", **kwargs, ) -> dataframe.DataFrame: - table = bigquery.Table(self._create_session_table()) + table = bigframes_io.random_table(self._anonymous_dataset) if engine == "bigquery": From 03606cda30eb7645bfd4534460112dcca56b0ab0 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 10 Nov 2023 12:50:14 -0600 Subject: [PATCH 152/921] fix: default to 7 days expiration for `read_csv`, `read_json`, `read_parquet` (#193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Follow-up to https://togithub.com/googleapis/python-bigquery-dataframes/pull/175/files#r1389686556 🦕 --- bigframes/session/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index bd5845631b2..27616de08de 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -836,11 +836,20 @@ def _read_bigquery_load_job( ) self._start_generic_job(load_job) + table_id = f"{table.project}.{table.dataset_id}.{table.table_id}" + + # Update the table expiration so we aren't limited to the default 24 + # hours of the anonymous dataset. + table_expiration = bigquery.Table(table_id) + table_expiration.expires = ( + datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION + ) + self.bqclient.update_table(table_expiration, ["expires"]) # The BigQuery REST API for tables.get doesn't take a session ID, so we # can't get the schema for a temp table that way. return self.read_gbq_table( - f"{table.project}.{table.dataset_id}.{table.table_id}", + table_id, index_col=index_col, col_order=col_order, ) From 741c75e5797e26a1487ff3da76a07953d9537f3f Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:20:12 -0800 Subject: [PATCH 153/921] fix: use random table for `read_pandas` (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: use random table for read_pandas * fix: default to 7 days expiration for `read_csv`, `read_json`, `read_parquet` (#193) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Follow-up to https://togithub.com/googleapis/python-bigquery-dataframes/pull/175/files#r1389686556 🦕 * fix: use random table for read_pandas * fix: use the real table expression --------- Co-authored-by: Tim Swast --- bigframes/core/__init__.py | 2 +- bigframes/session/__init__.py | 19 ++++++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 63f36d4ddd2..b640692bc85 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -165,7 +165,7 @@ def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: ibis_expr = compiled_value._to_ibis_expr( ordering_mode="unordered", expose_hidden_cols=True ) - tmp_table = self.session._ibis_to_session_table( + tmp_table = self.session._ibis_to_temp_table( ibis_expr, cluster_cols=cluster_cols, api_name="cached" ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 27616de08de..9b881de9a02 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -36,7 +36,6 @@ Tuple, Union, ) -import uuid import warnings import google.api_core.client_info @@ -986,7 +985,7 @@ def _read_pandas( job_config.clustering_fields = cluster_cols job_config.labels = {"bigframes-api": api_name} - load_table_destination = self._create_session_table() + load_table_destination = bigframes_io.random_table(self._anonymous_dataset) load_job = self.bqclient.load_table_from_dataframe( pandas_dataframe_copy, load_table_destination, @@ -999,8 +998,9 @@ def _read_pandas( total_ordering_columns=frozenset([ordering_col]), integer_encoding=IntegerEncoding(True, is_sequential=True), ) - table_expression = self.ibis_client.sql( - f"SELECT * FROM `{load_table_destination.table_id}`" + table_expression = self.ibis_client.table( + load_table_destination.table_id, + database=f"{load_table_destination.project}.{load_table_destination.dataset_id}", ) # b/297590178 Potentially a bug in bqclient.load_table_from_dataframe(), that only when the DF is empty, the index columns disappear in table_expression. @@ -1278,13 +1278,6 @@ def _check_file_size(self, filepath: str): "for large files to avoid loading the file into local memory." ) - def _create_session_table(self) -> bigquery.TableReference: - table_name = f"{uuid.uuid4().hex}" - dataset = bigquery.Dataset( - bigquery.DatasetReference(self.bqclient.project, "_SESSION") - ) - return dataset.table(table_name) - def _create_empty_temp_table( self, schema: Iterable[bigquery.SchemaField], @@ -1319,7 +1312,7 @@ def _create_sequential_ordering( ibis.row_number().cast(ibis_dtypes.int64).name(default_ordering_name) ) table = table.mutate(**{default_ordering_name: default_ordering_col}) - table_ref = self._ibis_to_session_table( + table_ref = self._ibis_to_temp_table( table, cluster_cols=list(index_cols) + [default_ordering_name], api_name=api_name, @@ -1335,7 +1328,7 @@ def _create_sequential_ordering( ) return table, ordering - def _ibis_to_session_table( + def _ibis_to_temp_table( self, table: ibis_types.Table, cluster_cols: Iterable[str], From 8ab81dee4d0eee499094f2dd576550f0c59d7551 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 10 Nov 2023 16:12:14 -0800 Subject: [PATCH 154/921] feat: read_gbq creates order deterministically without table copy (#191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 3 +- bigframes/ml/core.py | 20 +- bigframes/series.py | 3 +- bigframes/session/__init__.py | 418 +++++++----------- tests/system/small/ml/test_cluster.py | 110 ++--- tests/system/small/ml/test_core.py | 208 ++++----- tests/system/small/ml/test_decomposition.py | 104 ++--- tests/system/small/ml/test_forecasting.py | 1 + tests/system/small/test_dataframe_io.py | 18 + tests/system/small/test_pandas_options.py | 10 +- tests/system/small/test_progress_bar.py | 8 - tests/unit/ml/test_golden_sql.py | 2 + .../ibis/backends/bigquery/registry.py | 12 +- .../ibis/expr/operations/__init__.py | 5 +- .../ibis/expr/operations/json.py | 9 + .../bigframes_vendored/pandas/io/gbq.py | 10 - .../bigframes_vendored/pandas/io/parquet.py | 6 - .../bigframes_vendored/pandas/io/pickle.py | 10 - 18 files changed, 438 insertions(+), 519 deletions(-) create mode 100644 third_party/bigframes_vendored/ibis/expr/operations/json.py diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0a035754915..bdbc00e6207 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2719,7 +2719,8 @@ def _get_block(self) -> blocks.Block: return self._block def _cached(self) -> DataFrame: - return DataFrame(self._block.cached()) + self._set_block(self._block.cached()) + return self _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 4c5a48cf625..39d01fca8d7 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -126,7 +126,7 @@ def generate_text_embedding( def forecast(self) -> bpd.DataFrame: sql = self._model_manipulation_sql_generator.ml_forecast() - return self._session.read_gbq(sql) + return self._session.read_gbq(sql, index_col="forecast_timestamp").reset_index() def evaluate(self, input_data: Optional[bpd.DataFrame] = None): # TODO: validate input data schema @@ -139,14 +139,18 @@ def centroids(self) -> bpd.DataFrame: sql = self._model_manipulation_sql_generator.ml_centroids() - return self._session.read_gbq(sql) + return self._session.read_gbq( + sql, index_col=["centroid_id", "feature"] + ).reset_index() def principal_components(self) -> bpd.DataFrame: assert self._model.model_type == "PCA" sql = self._model_manipulation_sql_generator.ml_principal_components() - return self._session.read_gbq(sql) + return self._session.read_gbq( + sql, index_col=["principal_component_id", "feature"] + ).reset_index() def principal_component_info(self) -> bpd.DataFrame: assert self._model.model_type == "PCA" @@ -228,10 +232,12 @@ def create_model( Returns: a BqmlModel, wrapping a trained model in BigQuery """ options = dict(options) + # Cache dataframes to make sure base table is not a snapshot + # cached dataframe creates a full copy, never uses snapshot if y_train is None: - input_data = X_train + input_data = X_train._cached() else: - input_data = X_train.join(y_train, how="outer") + input_data = X_train._cached().join(y_train._cached(), how="outer") options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session @@ -259,7 +265,9 @@ def create_time_series_model( ), "Time stamp data input must only contain 1 column." options = dict(options) - input_data = X_train.join(y_train, how="outer") + # Cache dataframes to make sure base table is not a snapshot + # cached dataframe creates a full copy, never uses snapshot + input_data = X_train._cached().join(y_train._cached(), how="outer") options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) diff --git a/bigframes/series.py b/bigframes/series.py index 4fab1fe9439..28290d591e0 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1503,7 +1503,8 @@ def _slice( ) def _cached(self) -> Series: - return Series(self._block.cached()) + self._set_block(self._block.cached()) + return self def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 9b881de9a02..82c5a1c8d0d 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -17,10 +17,10 @@ from __future__ import annotations import datetime +import itertools import logging import os import re -import textwrap import typing from typing import ( Any, @@ -81,6 +81,7 @@ # Even though the ibis.backends.bigquery.registry import is unused, it's needed # to register new and replacement ops with the Ibis BigQuery backend. import third_party.bigframes_vendored.ibis.backends.bigquery.registry # noqa +import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import third_party.bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import third_party.bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet import third_party.bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers @@ -378,12 +379,6 @@ def read_gbq_query( ... pitchSpeed, ... FROM `bigquery-public-data.baseball.games_wide` ... ''') - >>> df.head(2) - pitcherFirstName pitcherLastName pitchSpeed - 0 0 - 1 0 - - [2 rows x 3 columns] Preserve ordering in a query input. @@ -480,16 +475,6 @@ def read_gbq_table( Read a whole table, with arbitrary ordering or ordering corresponding to the primary key(s). >>> df = bpd.read_gbq_table("bigquery-public-data.ml_datasets.penguins") - >>> df.head(2) - species island culmen_length_mm \\ - 0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 - 1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 - - culmen_depth_mm flipper_length_mm body_mass_g sex - 0 18.4 184.0 3475.0 FEMALE - 1 19.1 184.0 4650.0 MALE - - [2 rows x 7 columns] See also: :meth:`Session.read_gbq`. """ @@ -503,7 +488,7 @@ def read_gbq_table( api_name="read_gbq_table", ) - def _read_gbq_table_to_ibis_with_total_ordering( + def _get_snapshot_sql_and_primary_key( self, table_ref: bigquery.table.TableReference, *, @@ -523,7 +508,6 @@ def _read_gbq_table_to_ibis_with_total_ordering( ), None, ) - table_expression = self.ibis_client.table( table_ref.table_id, database=f"{table_ref.project}.{table_ref.dataset_id}", @@ -534,6 +518,11 @@ def _read_gbq_table_to_ibis_with_total_ordering( # the same assumption and use these columns as the total ordering keys. table = self.bqclient.get_table(table_ref) + if table.location.casefold() != self._location.casefold(): + raise ValueError( + f"Current session is in {self._location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" + ) + # TODO(b/305264153): Use public properties to fetch primary keys once # added to google-cloud-bigquery. primary_keys = ( @@ -542,22 +531,18 @@ def _read_gbq_table_to_ibis_with_total_ordering( .get("columns") ) - if not primary_keys: - return table_expression, None - else: - # Read from a snapshot since we won't have to copy the table data to create a total ordering. - job_config = bigquery.QueryJobConfig() - job_config.labels["bigframes-api"] = api_name - current_timestamp = list( - self.bqclient.query( - "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", - job_config=job_config, - ).result() - )[0][0] - table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table_ref, current_timestamp) - ) - return table_expression, primary_keys + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + current_timestamp = list( + self.bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + table_expression = self.ibis_client.sql( + bigframes_io.create_snapshot_sql(table_ref, current_timestamp) + ) + return table_expression, primary_keys def _read_gbq_table( self, @@ -580,10 +565,7 @@ def _read_gbq_table( ( table_expression, total_ordering_cols, - ) = self._read_gbq_table_to_ibis_with_total_ordering( - table_ref, - api_name=api_name, - ) + ) = self._get_snapshot_sql_and_primary_key(table_ref, api_name=api_name) for key in col_order: if key not in table_expression.columns: @@ -596,24 +578,22 @@ def _read_gbq_table( else: index_cols = list(index_col) - hidden_cols: typing.Sequence[str] = () - for key in index_cols: if key not in table_expression.columns: raise ValueError( f"Column `{key}` of `index_col` not found in this table." ) + if col_order: + table_expression = table_expression.select([*index_cols, *col_order]) + # If the index is unique and sortable, then we don't need to generate # an ordering column. ordering = None - is_total_ordering = False - if total_ordering_cols is not None: - # Note: currently, this a table has a total ordering only when the + # Note: currently, a table has a total ordering only when the # primary key(s) are set on a table. The query engine assumes such # columns are unique, even if not enforced. - is_total_ordering = True ordering = orderings.ExpressionOrdering( ordering_value_columns=tuple( core.OrderingColumnReference(column_id) @@ -621,41 +601,17 @@ def _read_gbq_table( ), total_ordering_columns=frozenset(total_ordering_cols), ) - - if len(index_cols) != 0: - index_labels = typing.cast(List[Optional[str]], index_cols) - else: - # Use the total_ordering_cols to project offsets to use as the default index. - table_expression = table_expression.order_by(index_cols) - default_index_id = guid.generate_guid("bigframes_index_") - default_index_col = ( - ibis.row_number().cast(ibis_dtypes.int64).name(default_index_id) - ) - table_expression = table_expression.mutate( - **{default_index_id: default_index_col} - ) - index_cols = [default_index_id] - index_labels = [None] - elif len(index_cols) != 0: - index_labels = typing.cast(List[Optional[str]], index_cols) - distinct_table = table_expression.select(*index_cols).distinct() - is_unique_sql = f"""WITH full_table AS ( - {self.ibis_client.compile(table_expression)} - ), - distinct_table AS ( - {self.ibis_client.compile(distinct_table)} + column_values = [table_expression[col] for col in table_expression.columns] + array_value = core.ArrayValue.from_ibis( + self, + table_expression, + columns=column_values, + hidden_ordering_columns=[], + ordering=ordering, ) - SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, - (SELECT COUNT(*) FROM distinct_table) AS `distinct_count` - """ - results, query_job = self._start_query(is_unique_sql) - row = next(iter(results)) - - total_count = row["total_count"] - distinct_count = row["distinct_count"] - is_total_ordering = total_count == distinct_count - + elif len(index_cols) != 0: + # We have index columns, lets see if those are actually total_order_columns ordering = orderings.ExpressionOrdering( ordering_value_columns=tuple( [ @@ -665,142 +621,61 @@ def _read_gbq_table( ), total_ordering_columns=frozenset(index_cols), ) - - # We have a total ordering, so query via "time travel" so that - # the underlying data doesn't mutate. + is_total_ordering = self._check_index_uniqueness( + table_expression, index_cols + ) if is_total_ordering: - # Get the timestamp from the job metadata rather than the query - # text so that the query for determining uniqueness of the ID - # columns can be cached. - current_timestamp = query_job.started - - # The job finished, so we should have a start time. - assert current_timestamp is not None - table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table_ref, current_timestamp) + column_values = [ + table_expression[col] for col in table_expression.columns + ] + array_value = core.ArrayValue.from_ibis( + self, + table_expression, + columns=column_values, + hidden_ordering_columns=[], + ordering=ordering, ) else: - # Make sure when we generate an ordering, the row_number() - # coresponds to the index columns. - table_expression = table_expression.order_by(index_cols) - warnings.warn( - textwrap.dedent( - f""" - Got a non-unique index. A consistent ordering is not - guaranteed. DataFrame has {total_count} rows, - but only {distinct_count} distinct index values. - """, - ) - ) - - # When ordering by index columns, apply limit after ordering to - # make limit more predictable. - if max_results is not None: - table_expression = table_expression.limit(max_results) + array_value = self._create_total_ordering(table_expression) else: - if max_results is not None: - # Apply limit before generating rownums and creating temp table - # This makes sure the offsets are valid and limits the number of - # rows for which row numbers must be generated - table_expression = table_expression.limit(max_results) - table_expression, ordering = self._create_sequential_ordering( - table=table_expression, - api_name=api_name, - ) - hidden_cols = ( - (ordering.total_order_col.column_id,) - if ordering.total_order_col - else () - ) - assert len(ordering.ordering_value_columns) > 0 - is_total_ordering = True - # Block constructor will generate default index if passed empty - index_cols = [] - index_labels = [] - - return self._read_gbq_with_ordering( - table_expression=table_expression, - col_order=col_order, - index_cols=index_cols, - index_labels=index_labels, - hidden_cols=hidden_cols, - ordering=ordering, - is_total_ordering=is_total_ordering, - api_name=api_name, + array_value = self._create_total_ordering(table_expression) + + value_columns = [col for col in array_value.column_ids if col not in index_cols] + block = blocks.Block( + array_value, + index_columns=index_cols, + column_labels=value_columns, + index_labels=index_cols, ) + if max_results: + block = block.slice(stop=max_results) + df = dataframe.DataFrame(block) - def _read_gbq_with_ordering( - self, - table_expression: ibis_types.Table, - *, - col_order: Iterable[str] = (), - col_labels: Iterable[Optional[str]] = (), - index_cols: Iterable[str] = (), - index_labels: Iterable[Optional[str]] = (), - hidden_cols: Iterable[str] = (), - ordering: orderings.ExpressionOrdering, - is_total_ordering: bool = False, - api_name: str, - ) -> dataframe.DataFrame: - """Internal helper method that loads DataFrame from Google BigQuery given an ordering column. + # If user provided index columns, should sort over it + if len(index_cols) > 0: + df.sort_index() + return df - Args: - table_expression: - an ibis table expression to be executed in BigQuery. - col_order: - List of BigQuery column ids in the desired order for results DataFrame. - col_labels: - List of column labels as the column names. - index_cols: - List of index ids to use as the index or multi-index. - index_labels: - List of index labels as names of index. - hidden_cols: - Columns that should be hidden. Ordering columns may (not always) be hidden - ordering: - Column name to be used for ordering. If not supplied, a default ordering is generated. - api_name: - The name of the API method. + def _check_index_uniqueness( + self, table: ibis_types.Table, index_cols: List[str] + ) -> bool: + distinct_table = table.select(*index_cols).distinct() + is_unique_sql = f"""WITH full_table AS ( + {self.ibis_client.compile(table)} + ), + distinct_table AS ( + {self.ibis_client.compile(distinct_table)} + ) - Returns: - A DataFrame representing results of the query or table. + SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, + (SELECT COUNT(*) FROM distinct_table) AS `distinct_count` """ - index_cols, index_labels = list(index_cols), list(index_labels) - if len(index_cols) != len(index_labels): - raise ValueError( - "Needs same number of index labels are there are index columns. " - f"Got {len(index_labels)}, expected {len(index_cols)}." - ) - - # Logic: - # no total ordering, index -> create sequential order, ordered by index, use for both ordering and index - # total ordering, index -> use ordering as ordering, index as index + results, _ = self._start_query(is_unique_sql) + row = next(iter(results)) - # This code block ensures the existence of a total ordering. - column_keys = list(col_order) - if len(column_keys) == 0: - non_value_columns = set([*index_cols, *hidden_cols]) - column_keys = [ - key for key in table_expression.columns if key not in non_value_columns - ] - if not is_total_ordering: - # Rows are not ordered, we need to generate a default ordering and materialize it - table_expression, ordering = self._create_sequential_ordering( - table=table_expression, - index_cols=index_cols, - api_name=api_name, - ) - index_col_values = [table_expression[index_id] for index_id in index_cols] - if not col_labels: - col_labels = column_keys - return self._read_ibis( - table_expression, - index_col_values, - index_labels, - column_keys, - col_labels, - ordering=ordering, - ) + total_count = row["total_count"] + distinct_count = row["distinct_count"] + return total_count == distinct_count def _read_bigquery_load_job( self, @@ -853,40 +728,6 @@ def _read_bigquery_load_job( col_order=col_order, ) - def _read_ibis( - self, - table_expression: ibis_types.Table, - index_cols: Iterable[ibis_types.Value], - index_labels: Iterable[blocks.Label], - column_keys: Iterable[str], - column_labels: Iterable[blocks.Label], - ordering: orderings.ExpressionOrdering, - ) -> dataframe.DataFrame: - """Turns a table expression (plus index column) into a DataFrame.""" - - columns = list(index_cols) - for key in column_keys: - if key not in table_expression.columns: - raise ValueError(f"Column '{key}' not found in this table.") - columns.append(table_expression[key]) - - non_hidden_ids = [col.get_name() for col in columns] - hidden_ordering_columns = [] - for ref in ordering.all_ordering_columns: - if ref.column_id not in non_hidden_ids: - hidden_ordering_columns.append(table_expression[ref.column_id]) - - block = blocks.Block( - core.ArrayValue.from_ibis( - self, table_expression, columns, hidden_ordering_columns, ordering - ), - index_columns=[index_col.get_name() for index_col in index_cols], - column_labels=column_labels, - index_labels=index_labels, - ) - - return dataframe.DataFrame(block) - def read_gbq_model(self, model_name: str): """Loads a BigQuery ML model from BigQuery. @@ -1009,17 +850,26 @@ def _read_pandas( ): new_idx_ids, idx_labels = [], [] - df = self._read_gbq_with_ordering( - table_expression=table_expression, - col_labels=col_labels, - index_cols=new_idx_ids, - index_labels=idx_labels, - hidden_cols=(ordering_col,), + column_values = [ + table_expression[col] + for col in table_expression.columns + if col != ordering_col + ] + array_value = core.ArrayValue.from_ibis( + self, + table_expression, + columns=column_values, + hidden_ordering_columns=[table_expression[ordering_col]], ordering=ordering, - is_total_ordering=True, - api_name=api_name, ) - return df + + block = blocks.Block( + array_value, + index_columns=new_idx_ids, + column_labels=col_labels, + index_labels=idx_labels, + ) + return dataframe.DataFrame(block) def read_csv( self, @@ -1299,34 +1149,50 @@ def _create_empty_temp_table( ) return bigquery.TableReference.from_string(table) - def _create_sequential_ordering( + def _create_total_ordering( self, table: ibis_types.Table, - index_cols: Iterable[str] = (), - api_name: str = "", - ) -> Tuple[ibis_types.Table, orderings.ExpressionOrdering]: + ) -> core.ArrayValue: # Since this might also be used as the index, don't use the default # "ordering ID" name. - default_ordering_name = guid.generate_guid("bigframes_ordering_") - default_ordering_col = ( - ibis.row_number().cast(ibis_dtypes.int64).name(default_ordering_name) + ordering_hash_part = guid.generate_guid("bigframes_ordering_") + ordering_rand_part = guid.generate_guid("bigframes_ordering_") + + str_values = list( + map(lambda col: _convert_to_string(table[col]), table.columns) ) - table = table.mutate(**{default_ordering_name: default_ordering_col}) - table_ref = self._ibis_to_temp_table( - table, - cluster_cols=list(index_cols) + [default_ordering_name], - api_name=api_name, + full_row_str = ( + str_values[0].concat(*str_values[1:]) + if len(str_values) > 1 + else str_values[0] ) - table = self.ibis_client.table( - f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" + full_row_hash = full_row_str.hash().name(ordering_hash_part) + # Used to disambiguate between identical rows (which will have identical hash) + random_value = ibis.random().name(ordering_rand_part) + + original_column_ids = table.columns + table_with_ordering = table.select( + itertools.chain(original_column_ids, [full_row_hash, random_value]) ) - ordering_reference = core.OrderingColumnReference(default_ordering_name) + + ordering_ref1 = core.OrderingColumnReference(ordering_hash_part) + ordering_ref2 = core.OrderingColumnReference(ordering_rand_part) ordering = orderings.ExpressionOrdering( - ordering_value_columns=tuple([ordering_reference]), - total_ordering_columns=frozenset([default_ordering_name]), - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), + ordering_value_columns=(ordering_ref1, ordering_ref2), + total_ordering_columns=frozenset([ordering_hash_part, ordering_rand_part]), + ) + columns = [table_with_ordering[col] for col in original_column_ids] + hidden_columns = [ + table_with_ordering[ordering_hash_part], + table_with_ordering[ordering_rand_part], + ] + return core.ArrayValue.from_ibis( + self, + table_with_ordering, + columns, + hidden_ordering_columns=hidden_columns, + ordering=ordering, ) - return table, ordering def _ibis_to_temp_table( self, @@ -1561,3 +1427,23 @@ def _can_cluster_bq(field: bigquery.SchemaField): "BOOL", "BOOLEAN", ) + + +def _convert_to_string(column: ibis_types.Column) -> ibis_types.StringColumn: + col_type = column.type() + if ( + col_type.is_numeric() + or col_type.is_boolean() + or col_type.is_binary() + or col_type.is_temporal() + ): + result = column.cast(ibis_dtypes.String(nullable=True)) + elif col_type.is_geospatial(): + result = typing.cast(ibis_types.GeoSpatialColumn, column).as_text() + elif col_type.is_string(): + result = column + else: + # TO_JSON_STRING works with all data types, but isn't the most efficient + # Needed for JSON, STRUCT and ARRAY datatypes + result = vendored_ibis_ops.ToJsonString(column).to_expr() # type: ignore + return typing.cast(ibis_types.StringColumn, result) diff --git a/tests/system/small/ml/test_cluster.py b/tests/system/small/ml/test_cluster.py index caeffa77689..266a38e3ee1 100644 --- a/tests/system/small/ml/test_cluster.py +++ b/tests/system/small/ml/test_cluster.py @@ -89,59 +89,67 @@ def test_kmeans_score(session, penguins_kmeans_model: cluster.KMeans): def test_kmeans_cluster_centers(penguins_kmeans_model: cluster.KMeans): - result = penguins_kmeans_model.cluster_centers_.to_pandas() - expected = pd.DataFrame( - { - "centroid_id": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], - "feature": [ - "culmen_length_mm", - "culmen_depth_mm", - "flipper_length_mm", - "sex", - ] - * 3, - "numerical_value": [ - 47.509677, - 14.993548, - 217.040123, - pd.NA, - 38.207813, - 18.03125, - 187.992188, - pd.NA, - 47.036346, - 18.834808, - 197.1612, - pd.NA, - ], - "categorical_value": [ - [], - [], - [], - [ - {"category": ".", "value": 0.008064516129032258}, - {"category": "MALE", "value": 0.49193548387096775}, - {"category": "FEMALE", "value": 0.47580645161290325}, - {"category": "_null_filler", "value": 0.024193548387096774}, - ], - [], - [], - [], - [ - {"category": "MALE", "value": 0.34375}, - {"category": "FEMALE", "value": 0.625}, - {"category": "_null_filler", "value": 0.03125}, + result = ( + penguins_kmeans_model.cluster_centers_.to_pandas() + .sort_values(["centroid_id", "feature"]) + .reset_index(drop=True) + ) + expected = ( + pd.DataFrame( + { + "centroid_id": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], + "feature": [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + * 3, + "numerical_value": [ + 47.509677, + 14.993548, + 217.040123, + pd.NA, + 38.207813, + 18.03125, + 187.992188, + pd.NA, + 47.036346, + 18.834808, + 197.1612, + pd.NA, ], - [], - [], - [], - [ - {"category": "MALE", "value": 0.6847826086956522}, - {"category": "FEMALE", "value": 0.2826086956521739}, - {"category": "_null_filler", "value": 0.03260869565217391}, + "categorical_value": [ + [], + [], + [], + [ + {"category": ".", "value": 0.008064516129032258}, + {"category": "MALE", "value": 0.49193548387096775}, + {"category": "FEMALE", "value": 0.47580645161290325}, + {"category": "_null_filler", "value": 0.024193548387096774}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.34375}, + {"category": "FEMALE", "value": 0.625}, + {"category": "_null_filler", "value": 0.03125}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.6847826086956522}, + {"category": "FEMALE", "value": 0.2826086956521739}, + {"category": "_null_filler", "value": 0.03260869565217391}, + ], ], - ], - }, + }, + ) + .sort_values(["centroid_id", "feature"]) + .reset_index(drop=True) ) pd.testing.assert_frame_equal( result, diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index ec1f351d876..be34a4871c6 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -78,58 +78,62 @@ def test_model_eval_with_data(penguins_bqml_linear_model, penguins_df_default_in def test_model_centroids(penguins_bqml_kmeans_model: core.BqmlModel): result = penguins_bqml_kmeans_model.centroids().to_pandas() - expected = pd.DataFrame( - { - "centroid_id": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], - "feature": [ - "culmen_length_mm", - "culmen_depth_mm", - "flipper_length_mm", - "sex", - ] - * 3, - "numerical_value": [ - 47.509677, - 14.993548, - 217.040123, - pd.NA, - 38.207813, - 18.03125, - 187.992188, - pd.NA, - 47.036346, - 18.834808, - 197.1612, - pd.NA, - ], - "categorical_value": [ - [], - [], - [], - [ - {"category": ".", "value": 0.008064516129032258}, - {"category": "MALE", "value": 0.49193548387096775}, - {"category": "FEMALE", "value": 0.47580645161290325}, - {"category": "_null_filler", "value": 0.024193548387096774}, + expected = ( + pd.DataFrame( + { + "centroid_id": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], + "feature": [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + * 3, + "numerical_value": [ + 47.509677, + 14.993548, + 217.040123, + pd.NA, + 38.207813, + 18.03125, + 187.992188, + pd.NA, + 47.036346, + 18.834808, + 197.1612, + pd.NA, ], - [], - [], - [], - [ - {"category": "MALE", "value": 0.34375}, - {"category": "FEMALE", "value": 0.625}, - {"category": "_null_filler", "value": 0.03125}, + "categorical_value": [ + [], + [], + [], + [ + {"category": ".", "value": 0.008064516129032258}, + {"category": "MALE", "value": 0.49193548387096775}, + {"category": "FEMALE", "value": 0.47580645161290325}, + {"category": "_null_filler", "value": 0.024193548387096774}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.34375}, + {"category": "FEMALE", "value": 0.625}, + {"category": "_null_filler", "value": 0.03125}, + ], + [], + [], + [], + [ + {"category": "MALE", "value": 0.6847826086956522}, + {"category": "FEMALE", "value": 0.2826086956521739}, + {"category": "_null_filler", "value": 0.03260869565217391}, + ], ], - [], - [], - [], - [ - {"category": "MALE", "value": 0.6847826086956522}, - {"category": "FEMALE", "value": 0.2826086956521739}, - {"category": "_null_filler", "value": 0.03260869565217391}, - ], - ], - }, + }, + ) + .sort_values(["centroid_id", "feature"]) + .reset_index(drop=True) ) pd.testing.assert_frame_equal( result, @@ -148,59 +152,63 @@ def test_pca_model_principal_components(penguins_bqml_pca_model: core.BqmlModel) # result is too long, only check the first principal component here. result = result.head(7) - expected = pd.DataFrame( - { - "principal_component_id": [0] * 7, - "feature": [ - "species", - "island", - "culmen_length_mm", - "culmen_depth_mm", - "flipper_length_mm", - "body_mass_g", - "sex", - ], - "numerical_value": [ - pd.NA, - pd.NA, - 0.401489, - -0.377482, - 0.524052, - 0.501174, - pd.NA, - ], - "categorical_value": [ - [ - { - "category": "Gentoo penguin (Pygoscelis papua)", - "value": 0.25068877125667804, - }, - { - "category": "Adelie Penguin (Pygoscelis adeliae)", - "value": -0.20622291900416198, - }, - { - "category": "Chinstrap penguin (Pygoscelis antarctica)", - "value": -0.030161149275185855, - }, + expected = ( + pd.DataFrame( + { + "principal_component_id": [0] * 7, + "feature": [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + "sex", ], - [ - {"category": "Biscoe", "value": 0.19761120114410635}, - {"category": "Dream", "value": -0.11264736305259061}, - {"category": "Torgersen", "value": -0.07065913511418596}, + "numerical_value": [ + pd.NA, + pd.NA, + 0.401489, + -0.377482, + 0.524052, + 0.501174, + pd.NA, ], - [], - [], - [], - [], - [ - {"category": ".", "value": 0.0015916894448071784}, - {"category": "MALE", "value": 0.06869704739750442}, - {"category": "FEMALE", "value": -0.052521171596813174}, - {"category": "_null_filler", "value": -0.0034628622681684906}, + "categorical_value": [ + [ + { + "category": "Gentoo penguin (Pygoscelis papua)", + "value": 0.25068877125667804, + }, + { + "category": "Adelie Penguin (Pygoscelis adeliae)", + "value": -0.20622291900416198, + }, + { + "category": "Chinstrap penguin (Pygoscelis antarctica)", + "value": -0.030161149275185855, + }, + ], + [ + {"category": "Biscoe", "value": 0.19761120114410635}, + {"category": "Dream", "value": -0.11264736305259061}, + {"category": "Torgersen", "value": -0.07065913511418596}, + ], + [], + [], + [], + [], + [ + {"category": ".", "value": 0.0015916894448071784}, + {"category": "MALE", "value": 0.06869704739750442}, + {"category": "FEMALE", "value": -0.052521171596813174}, + {"category": "_null_filler", "value": -0.0034628622681684906}, + ], ], - ], - }, + }, + ) + .sort_values(["principal_component_id", "feature"]) + .reset_index(drop=True) ) pd.testing.assert_frame_equal( result, diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index cc4d2e5801c..42fea66cf86 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -57,59 +57,63 @@ def test_pca_components_(penguins_pca_model: decomposition.PCA): # result is too long, only check the first principal component here. result = result.head(7) - expected = pd.DataFrame( - { - "principal_component_id": [0] * 7, - "feature": [ - "species", - "island", - "culmen_length_mm", - "culmen_depth_mm", - "flipper_length_mm", - "body_mass_g", - "sex", - ], - "numerical_value": [ - pd.NA, - pd.NA, - 0.401489, - -0.377482, - 0.524052, - 0.501174, - pd.NA, - ], - "categorical_value": [ - [ - { - "category": "Gentoo penguin (Pygoscelis papua)", - "value": 0.25068877125667804, - }, - { - "category": "Adelie Penguin (Pygoscelis adeliae)", - "value": -0.20622291900416198, - }, - { - "category": "Chinstrap penguin (Pygoscelis antarctica)", - "value": -0.030161149275185855, - }, + expected = ( + pd.DataFrame( + { + "principal_component_id": [0] * 7, + "feature": [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "body_mass_g", + "sex", ], - [ - {"category": "Biscoe", "value": 0.19761120114410635}, - {"category": "Dream", "value": -0.11264736305259061}, - {"category": "Torgersen", "value": -0.07065913511418596}, + "numerical_value": [ + pd.NA, + pd.NA, + 0.401489, + -0.377482, + 0.524052, + 0.501174, + pd.NA, ], - [], - [], - [], - [], - [ - {"category": ".", "value": 0.0015916894448071784}, - {"category": "MALE", "value": 0.06869704739750442}, - {"category": "FEMALE", "value": -0.052521171596813174}, - {"category": "_null_filler", "value": -0.0034628622681684906}, + "categorical_value": [ + [ + { + "category": "Gentoo penguin (Pygoscelis papua)", + "value": 0.25068877125667804, + }, + { + "category": "Adelie Penguin (Pygoscelis adeliae)", + "value": -0.20622291900416198, + }, + { + "category": "Chinstrap penguin (Pygoscelis antarctica)", + "value": -0.030161149275185855, + }, + ], + [ + {"category": "Biscoe", "value": 0.19761120114410635}, + {"category": "Dream", "value": -0.11264736305259061}, + {"category": "Torgersen", "value": -0.07065913511418596}, + ], + [], + [], + [], + [], + [ + {"category": ".", "value": 0.0015916894448071784}, + {"category": "MALE", "value": 0.06869704739750442}, + {"category": "FEMALE", "value": -0.052521171596813174}, + {"category": "_null_filler", "value": -0.0034628622681684906}, + ], ], - ], - }, + }, + ) + .sort_values(["principal_component_id", "feature"]) + .reset_index(drop=True) ) pd.testing.assert_frame_equal( result, diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index cb27dd388c3..55079c94cfd 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -36,6 +36,7 @@ def test_model_predict(time_series_arima_plus_model): expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) + pd.testing.assert_frame_equal( predictions, expected, diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index d700d93be99..fb9fb7bb898 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -80,6 +80,24 @@ def test_to_pandas_array_struct_correct_result(session): ) +def test_load_json(session): + df = session.read_gbq( + """SELECT + JSON_OBJECT('foo', 10, 'bar', TRUE) AS json_column + """ + ) + + result = df.to_pandas() + expected = pd.DataFrame( + { + "json_column": ['{"bar":true,"foo":10}'], + } + ) + expected.index = expected.index.astype("Int64") + pd.testing.assert_series_equal(result.dtypes, expected.dtypes) + pd.testing.assert_series_equal(result["json_column"], expected["json_column"]) + + 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_pandas_options.py b/tests/system/small/test_pandas_options.py index ca67710d4ea..c410d70fe73 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -74,7 +74,7 @@ def test_read_gbq_start_sets_session_location( # Now read_gbq* from another location should fail with pytest.raises( - google.api_core.exceptions.NotFound, + (google.api_core.exceptions.NotFound, ValueError), match=dataset_id_permanent, ): read_method(query) @@ -99,7 +99,7 @@ def test_read_gbq_start_sets_session_location( # Now read_gbq* from another location should fail with pytest.raises( - google.api_core.exceptions.NotFound, + (google.api_core.exceptions.NotFound, ValueError), match=dataset_id_permanent_tokyo, ): read_method(query_tokyo) @@ -145,7 +145,7 @@ def test_read_gbq_after_session_start_must_comply_with_default_location( # Doing read_gbq* from a table in another location should fail with pytest.raises( - google.api_core.exceptions.NotFound, + (google.api_core.exceptions.NotFound, ValueError), match=dataset_id_permanent_tokyo, ): read_method(query_tokyo) @@ -193,7 +193,7 @@ def test_read_gbq_must_comply_with_set_location_US( # Starting user journey with read_gbq* from another location should fail with pytest.raises( - google.api_core.exceptions.NotFound, + (google.api_core.exceptions.NotFound, ValueError), match=dataset_id_permanent_tokyo, ): read_method(query_tokyo) @@ -243,7 +243,7 @@ def test_read_gbq_must_comply_with_set_location_non_US( # Starting user journey with read_gbq* from another location should fail with pytest.raises( - google.api_core.exceptions.NotFound, + (google.api_core.exceptions.NotFound, ValueError), match=dataset_id_permanent, ): read_method(query) diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index 30ea63b483a..c6eee820530 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -52,14 +52,6 @@ def test_progress_bar_scalar(penguins_df_default_index: bf.dataframe.DataFrame, assert_loading_msg_exist(capsys.readouterr().out) -def test_progress_bar_read_gbq(session: bf.Session, penguins_table_id: str, capsys): - bf.options.display.progress_bar = "terminal" - capsys.readouterr() # clear output - session.read_gbq(penguins_table_id) - - assert_loading_msg_exist(capsys.readouterr().out) - - def test_progress_bar_extract_jobs( penguins_df_default_index: bf.dataframe.DataFrame, gcs_folder, capsys ): diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 3ca7e144a53..700eb500ffd 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -38,6 +38,7 @@ def mock_session(): def mock_y(): mock_y = mock.create_autospec(spec=bpd.DataFrame) mock_y.columns = pd.Index(["input_column_label"]) + mock_y._cached.return_value = mock_y return mock_y @@ -57,6 +58,7 @@ def mock_X(mock_y, mock_session): ["index_column_id"], ["index_column_label"], ) + mock_X._cached.return_value = mock_X return mock_X diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py index a4e61ca0f91..e1b28690d7c 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -22,10 +22,16 @@ def _last_non_null_value(translator, op: vendored_ibis_ops.LastNonNullValue): return f"LAST_VALUE({arg} IGNORE NULLS)" +def _to_json_string(translator, op: vendored_ibis_ops.ToJsonString): + arg = translator.translate(op.arg) + return f"TO_JSON_STRING({arg})" + + patched_ops = { - vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, - vendored_ibis_ops.FirstNonNullValue: _first_non_null_value, - vendored_ibis_ops.LastNonNullValue: _last_non_null_value, + vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, # type:ignore + vendored_ibis_ops.FirstNonNullValue: _first_non_null_value, # type:ignore + vendored_ibis_ops.LastNonNullValue: _last_non_null_value, # type:ignore + vendored_ibis_ops.ToJsonString: _to_json_string, # type:ignore } OPERATION_REGISTRY.update(patched_ops) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py index 1612d9c12e5..82197013923 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py @@ -1,5 +1,6 @@ # Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/__init__.py from __future__ import annotations -from third_party.bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F403 -from third_party.bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F403 +from third_party.bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F401 F403 +from third_party.bigframes_vendored.ibis.expr.operations.json import * # noqa: F401 F403 +from third_party.bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F401 F403 diff --git a/third_party/bigframes_vendored/ibis/expr/operations/json.py b/third_party/bigframes_vendored/ibis/expr/operations/json.py new file mode 100644 index 00000000000..dbb3fa3066c --- /dev/null +++ b/third_party/bigframes_vendored/ibis/expr/operations/json.py @@ -0,0 +1,9 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/json.py +from __future__ import annotations + +import ibis.expr.datatypes as dt +from ibis.expr.operations.core import Unary + + +class ToJsonString(Unary): + output_dtype = dt.string diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 575c5016187..2161310b07c 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -45,16 +45,6 @@ def read_gbq( If the input is a table ID: >>> df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") - >>> df.head(2) - species island culmen_length_mm \\ - 0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 - 1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 - - culmen_depth_mm flipper_length_mm body_mass_g sex - 0 18.4 184.0 3475.0 FEMALE - 1 19.1 184.0 4650.0 MALE - - [2 rows x 7 columns] Preserve ordering in a query input. diff --git a/third_party/bigframes_vendored/pandas/io/parquet.py b/third_party/bigframes_vendored/pandas/io/parquet.py index f97bd386a40..0f664e70fcb 100644 --- a/third_party/bigframes_vendored/pandas/io/parquet.py +++ b/third_party/bigframes_vendored/pandas/io/parquet.py @@ -24,12 +24,6 @@ def read_parquet( >>> gcs_path = "gs://cloud-samples-data/bigquery/us-states/us-states.parquet" >>> df = bpd.read_parquet(path=gcs_path) - >>> df.head(2) - name post_abbr - 0 Alabama AL - 1 Alaska AK - - [2 rows x 2 columns] Args: path (str): diff --git a/third_party/bigframes_vendored/pandas/io/pickle.py b/third_party/bigframes_vendored/pandas/io/pickle.py index 053ba4871c1..096d9b13d68 100644 --- a/third_party/bigframes_vendored/pandas/io/pickle.py +++ b/third_party/bigframes_vendored/pandas/io/pickle.py @@ -32,16 +32,6 @@ def read_pickle( >>> gcs_path = "gs://bigframes-dev-testing/test_pickle.pkl" >>> df = bpd.read_pickle(filepath_or_buffer=gcs_path) - >>> df.head(2) - species island culmen_length_mm \\ - 0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 - 1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 - - culmen_depth_mm flipper_length_mm body_mass_g sex - 0 18.4 184.0 3475.0 FEMALE - 1 19.1 184.0 4650.0 MALE - - [2 rows x 7 columns] Args: filepath_or_buffer (str, path object, or file-like object): From 7d2be003a16075ba2cd5327e049aaa7ca1a34f14 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 13 Nov 2023 09:12:08 -0600 Subject: [PATCH 155/921] chore: update formatter in CONTRIBUTING.rst (#178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update formatter in CONTRIBUTING.rst * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * use owlbot to update the files * remove for loop to format code samples, these are managed by the root noxfile. * 🦉 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 --- CONTRIBUTING.md | 33 --------------------------------- CONTRIBUTING.rst | 4 ++-- owlbot.py | 9 +++++++-- 3 files changed, 9 insertions(+), 37 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index b16bd944285..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,33 +0,0 @@ -# How to contribute - -We'd love to accept your patches and contributions to this project. - -## Before you begin - -### Sign our Contributor License Agreement - -Contributions to this project must be accompanied by a -[Contributor License Agreement](https://cla.developers.google.com/about) (CLA). -You (or your employer) retain the copyright to your contribution; this simply -gives us permission to use and redistribute your contributions as part of the -project. - -If you or your current employer have already signed the Google CLA (even if it -was for a different project), you probably don't need to do it again. - -Visit to see your current agreements or to -sign a new one. - -### Review our community guidelines - -This project follows -[Google's Open Source Community Guidelines](https://opensource.google/conduct/). - -## Contribution process - -### Code reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 3933152cf78..f9103bfa72c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -96,9 +96,9 @@ On Debian/Ubuntu:: Coding Style ************ - We use the automatic code formatter ``black``. You can run it using - the nox session ``blacken``. This will eliminate many lint errors. Run via:: + the nox session ``format``. This will eliminate many lint errors. Run via:: - $ nox -s blacken + $ nox -s format - PEP8 compliance is required, with exceptions defined in the linter configuration. If you have ``nox`` installed, you can test that you have not introduced diff --git a/owlbot.py b/owlbot.py index be30eea5c2c..082970018dc 100644 --- a/owlbot.py +++ b/owlbot.py @@ -99,6 +99,13 @@ "BigQuery DataFrames provides DataFrame APIs on the BigQuery engine", ) +# Update the contributing guide to reflect some differences in this repo. +s.replace( + ["CONTRIBUTING.rst"], + re.escape("blacken"), + "format", +) + # ---------------------------------------------------------------------------- # Samples templates # ---------------------------------------------------------------------------- @@ -110,5 +117,3 @@ # ---------------------------------------------------------------------------- s.shell.run(["nox", "-s", "format"], hide_output=False) -for noxfile in REPO_ROOT.glob("samples/**/noxfile.py"): - s.shell.run(["nox", "-s", "blacken"], cwd=noxfile.parent, hide_output=False) From 87f84c9e58e7d0ea521ac386c9f02791cdddd19f Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 13 Nov 2023 11:26:14 -0600 Subject: [PATCH 156/921] docs: use `head()` to get top `n` results, not to preview results (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit head() requires ordering. Just peeking at the whole DataFrame or Series is actually more efficient since it doesn't require ordering and still only downloads a fraction of the results. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- samples/snippets/pandas_methods_test.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/samples/snippets/pandas_methods_test.py b/samples/snippets/pandas_methods_test.py index 1f472d63466..bd8e29c0030 100644 --- a/samples/snippets/pandas_methods_test.py +++ b/samples/snippets/pandas_methods_test.py @@ -22,13 +22,20 @@ def test_bigquery_dataframes_pandas_methods(): bq_df = bpd.read_gbq(query_or_table) # Inspect one of the columns (or series) of the DataFrame: - bq_df["body_mass_g"].head(10) + bq_df["body_mass_g"] # Compute the mean of this series: average_body_mass = bq_df["body_mass_g"].mean() print(f"average_body_mass: {average_body_mass}") - # Calculate the mean body_mass_g by species using the groupby operation: - bq_df["body_mass_g"].groupby(by=bq_df["species"]).mean().head() + # Find the heaviest species using the groupby operation to calculate the + # mean body_mass_g: + ( + bq_df["body_mass_g"] + .groupby(by=bq_df["species"]) + .mean() + .sort_values(ascending=False) + .head(10) + ) # [END bigquery_dataframes_pandas_methods] assert average_body_mass is not None From 7506eabf2e58159507809e36abfe90c417dfe92f Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:58:35 -0800 Subject: [PATCH 157/921] docs: add code samples for `read_gbq_function` using community UDFs (#188) * docs: add code samples for read_gbq_function using community UDFs --- bigframes/session/__init__.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 82c5a1c8d0d..67c429eda59 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1322,15 +1322,37 @@ def read_gbq_function( The return type of the function must be explicitly specified in the function's original definition even if not otherwise required. + BigQuery Utils provides many public functions under the ``bqutil`` project on Google Cloud Platform project + (See: https://github.com/GoogleCloudPlatform/bigquery-utils/tree/master/udfs#using-the-udfs). + You can checkout Community UDFs to use community-contributed functions. + (See: https://github.com/GoogleCloudPlatform/bigquery-utils/tree/master/udfs/community#community-udfs). + **Examples:** + Use the ``cw_lower_case_ascii_only`` function from Community UDFs. + (https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/cw_lower_case_ascii_only.sqlx) + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None - >>> function_name = "bqutil.fn.cw_lower_case_ascii_only" - >>> func = bpd.read_gbq_function(function_name=function_name) - >>> func.bigframes_remote_function - 'bqutil.fn.cw_lower_case_ascii_only' + >>> df = bpd.DataFrame({'id': [1, 2, 3], 'name': ['AURÉLIE', 'CÉLESTINE', 'DAPHNÉ']}) + >>> df + id name + 0 1 AURÉLIE + 1 2 CÉLESTINE + 2 3 DAPHNÉ + + [3 rows x 2 columns] + + >>> func = bpd.read_gbq_function("bqutil.fn.cw_lower_case_ascii_only") + >>> df1 = df.assign(new_name=df['name'].apply(func)) + >>> df1 + id name new_name + 0 1 AURÉLIE aurÉlie + 1 2 CÉLESTINE cÉlestine + 2 3 DAPHNÉ daphnÉ + + [3 rows x 3 columns] Args: function_name (str): From 4a02cac88c7d7b46bed1fa813a862fc2ef9ef084 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 14 Nov 2023 11:47:55 -0600 Subject: [PATCH 158/921] feat: temporary resources no longer use BigQuery Sessions (#194) * feat: temporary resources no longer use BigQuery Sessions This allows multiple queries to run in parallel. * use anonymous dataset for models * remove reference to bq session * avoid 'model already exists' error * remove global _model_id --- bigframes/ml/core.py | 39 ++++++++++++------ bigframes/ml/sql.py | 24 +++++++---- bigframes/session/__init__.py | 53 ++++--------------------- tests/system/small/test_progress_bar.py | 6 --- tests/system/small/test_session.py | 24 ----------- tests/unit/ml/test_golden_sql.py | 47 ++++++++++++++-------- tests/unit/ml/test_sql.py | 23 ++++++++--- tests/unit/resources.py | 1 - tests/unit/test_pandas.py | 38 ------------------ 9 files changed, 97 insertions(+), 158 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 39d01fca8d7..d8135f7085f 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -16,12 +16,14 @@ from __future__ import annotations +import datetime from typing import Callable, cast, Iterable, Mapping, Optional, Union import uuid from google.cloud import bigquery import bigframes +import bigframes.constants as constants from bigframes.ml import sql as ml_sql import bigframes.pandas as bpd @@ -192,24 +194,27 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: class BqmlModelFactory: def __init__(self): - model_id = self._create_temp_model_id() - self._model_creation_sql_generator = ml_sql.ModelCreationSqlGenerator(model_id) + self._model_creation_sql_generator = ml_sql.ModelCreationSqlGenerator() - def _create_temp_model_id(self) -> str: - return uuid.uuid4().hex - - def _reset_model_id(self): - self._model_creation_sql_generator._model_id = self._create_temp_model_id() + def _create_model_ref( + self, dataset: bigquery.DatasetReference + ) -> bigquery.ModelReference: + return bigquery.ModelReference.from_string( + f"{dataset.project}.{dataset.dataset_id}.{uuid.uuid4().hex}" + ) def _create_model_with_sql(self, session: bigframes.Session, sql: str) -> BqmlModel: # fit the model, synchronously _, job = session._start_query(sql) # real model path in the session specific hidden dataset and table prefix - model_name_full = f"{job.destination.dataset_id}.{job.destination.table_id}" - model = session.bqclient.get_model(model_name_full) + model_name_full = f"{job.destination.project}.{job.destination.dataset_id}.{job.destination.table_id}" + model = bigquery.Model(model_name_full) + model.expires = ( + datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION + ) + model = session.bqclient.update_model(model, ["expires"]) - self._reset_model_id() return BqmlModel(session, model) def create_model( @@ -219,7 +224,7 @@ def create_model( transforms: Optional[Iterable[str]] = None, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: - """Create a session-temporary BQML model with the CREATE MODEL statement + """Create a session-temporary BQML model with the CREATE OR REPLACE MODEL statement Args: X_train: features columns for training @@ -241,9 +246,11 @@ def create_model( options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session + model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_model( source_df=input_data, + model_ref=model_ref, transforms=transforms, options=options, ) @@ -272,9 +279,11 @@ def create_time_series_model( options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) session = X_train._session + model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_model( source_df=input_data, + model_ref=model_ref, transforms=transforms, options=options, ) @@ -287,7 +296,7 @@ def create_remote_model( connection_name: str, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: - """Create a session-temporary BQML remote model with the CREATE MODEL statement + """Create a session-temporary BQML remote model with the CREATE OR REPLACE MODEL statement Args: connection_name: @@ -298,8 +307,10 @@ def create_remote_model( Returns: BqmlModel: a BqmlModel wrapping a trained model in BigQuery """ + model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_remote_model( connection_name=connection_name, + model_ref=model_ref, options=options, ) @@ -310,7 +321,7 @@ def create_imported_model( session: bigframes.Session, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: - """Create a session-temporary BQML imported model with the CREATE MODEL statement + """Create a session-temporary BQML imported model with the CREATE OR REPLACE MODEL statement Args: options: a dict of options to configure the model. Generates a BQML OPTIONS @@ -318,7 +329,9 @@ def create_imported_model( Returns: a BqmlModel, wrapping a trained model in BigQuery """ + model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_imported_model( + model_ref=model_ref, options=options, ) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 601b2710999..ab051231fb0 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -18,6 +18,8 @@ from typing import Iterable, Mapping, Optional, Union +import google.cloud.bigquery + import bigframes.constants as constants import bigframes.pandas as bpd @@ -121,22 +123,26 @@ def ml_label_encoder( class ModelCreationSqlGenerator(BaseSqlGenerator): """Sql generator for creating a model entity. Model id is the standalone id without project id and dataset id.""" - def __init__(self, model_id: str): - self._model_id = model_id + def _model_id_sql( + self, + model_ref: google.cloud.bigquery.ModelReference, + ): + return f"`{model_ref.project}`.`{model_ref.dataset_id}`.`{model_ref.model_id}`" # Model create and alter def create_model( self, source_df: bpd.DataFrame, + model_ref: google.cloud.bigquery.ModelReference, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, transforms: Optional[Iterable[str]] = None, ) -> str: - """Encode the CREATE TEMP MODEL statement for BQML""" + """Encode the CREATE OR REPLACE MODEL statement for BQML""" source_sql = source_df.sql transform_sql = self.transform(*transforms) if transforms is not None else None options_sql = self.options(**options) - parts = [f"CREATE TEMP MODEL `{self._model_id}`"] + parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] if transform_sql: parts.append(transform_sql) if options_sql: @@ -147,12 +153,13 @@ def create_model( def create_remote_model( self, connection_name: str, + model_ref: google.cloud.bigquery.ModelReference, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: - """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + """Encode the CREATE OR REPLACE MODEL statement for BQML remote model.""" options_sql = self.options(**options) - parts = [f"CREATE TEMP MODEL `{self._model_id}`"] + parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] parts.append(self.connection(connection_name)) if options_sql: parts.append(options_sql) @@ -160,12 +167,13 @@ def create_remote_model( def create_imported_model( self, + model_ref: google.cloud.bigquery.ModelReference, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: - """Encode the CREATE TEMP MODEL statement for BQML remote model.""" + """Encode the CREATE OR REPLACE MODEL statement for BQML remote model.""" options_sql = self.options(**options) - parts = [f"CREATE TEMP MODEL `{self._model_id}`"] + parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] if options_sql: parts.append(options_sql) return "\n".join(parts) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 67c429eda59..1b12e5deeb8 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -161,7 +161,7 @@ def __init__( application_name=context.application_name, ) - self._create_and_bind_bq_session() + self._create_bq_datasets() self.ibis_client = typing.cast( ibis_bigquery.Backend, ibis.bigquery.connect( @@ -210,19 +210,12 @@ def _project(self): def __hash__(self): # Stable hash needed to use in expression tree - return hash(self._session_id) - - def _create_and_bind_bq_session(self): - """Create a BQ session and bind the session id with clients to capture BQ activities: - go/bigframes-transient-data""" - job_config = bigquery.QueryJobConfig(create_session=True) - # Make sure the session is a new one, not one associated with another query. - job_config.use_query_cache = False - query_job = self.bqclient.query( - "SELECT 1", job_config=job_config, location=self._location - ) + return hash(str(self._anonymous_dataset)) + + def _create_bq_datasets(self): + """Create and identify dataset(s) for temporary BQ resources.""" + query_job = self.bqclient.query("SELECT 1", location=self._location) query_job.result() # blocks until finished - self._session_id = query_job.session_info.session_id # The anonymous dataset is used by BigQuery to write query results and # session tables. BigQuery DataFrames also writes temp tables directly @@ -235,17 +228,6 @@ def _create_and_bind_bq_session(self): query_destination.dataset_id, ) - self.bqclient.default_query_job_config = bigquery.QueryJobConfig( - connection_properties=[ - bigquery.ConnectionProperty("session_id", self._session_id) - ] - ) - self.bqclient.default_load_job_config = bigquery.LoadJobConfig( - connection_properties=[ - bigquery.ConnectionProperty("session_id", self._session_id) - ] - ) - # Dataset for storing remote functions, which don't yet # support proper session temporary storage yet self._session_dataset = bigquery.Dataset( @@ -254,28 +236,7 @@ def _create_and_bind_bq_session(self): self._session_dataset.location = self._location def close(self): - """Terminated the BQ session, otherwises the session will be terminated automatically after - 24 hours of inactivity or after 7 days.""" - if self._session_id is not None and self.bqclient is not None: - abort_session_query = "CALL BQ.ABORT_SESSION('{}')".format(self._session_id) - try: - query_job = self.bqclient.query(abort_session_query) - query_job.result() # blocks until finished - except google.api_core.exceptions.BadRequest as exc: - # Ignore the exception when the BQ session itself has expired - # https://cloud.google.com/bigquery/docs/sessions-terminating#auto-terminate_a_session - if not exc.message.startswith( - f"Session {self._session_id} has expired and is no longer available." - ): - raise - except google.auth.exceptions.RefreshError: - # The refresh token may itself have been invalidated or expired - # https://developers.google.com/identity/protocols/oauth2#expiration - # Don't raise the exception in this case while closing the - # BigFrames session, so that the end user has a path for getting - # out of a bad session due to unusable credentials. - pass - self._session_id = None + """No-op. Temporary resources are deleted after 7 days.""" def read_gbq( self, diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index c6eee820530..bd13ac22401 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -90,9 +90,6 @@ def assert_loading_msg_exist(capystOut: str, pattern=job_load_message_regex): def test_query_job_repr_html(penguins_df_default_index: bf.dataframe.DataFrame): bf.options.display.progress_bar = "terminal" - penguins_df_default_index._block._expr.session.bqclient.default_query_job_config.use_query_cache = ( - False - ) penguins_df_default_index.to_pandas() query_job_repr = formatting_helpers.repr_query_job_html( penguins_df_default_index.query_job @@ -109,9 +106,6 @@ def test_query_job_repr_html(penguins_df_default_index: bf.dataframe.DataFrame): def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): - penguins_df_default_index._block._expr.session.bqclient.default_query_job_config.use_query_cache = ( - False - ) penguins_df_default_index.to_pandas() query_job_repr = formatting_helpers.repr_query_job( penguins_df_default_index.query_job diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index bf72e444eba..7cd9f1dd598 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -19,7 +19,6 @@ import typing from typing import List -import google.api_core.exceptions import google.cloud.bigquery as bigquery import numpy as np import pandas as pd @@ -985,26 +984,3 @@ def test_read_json_gcs_default_engine(session, scalars_dfs, gcs_folder): assert df.shape[0] == scalars_df.shape[0] pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) - - -def test_session_id(session): - assert session._session_id is not None - - # BQ client always runs query within the opened session. - query_job = session.bqclient.query("SELECT 1") - assert query_job.session_info.session_id == session._session_id - - # TODO(chelsealin): Verify the session id can be binded with a load job. - - -@pytest.mark.flaky(retries=2) -def test_to_close_session(): - session = bigframes.Session() - assert session._session_id is not None - session.close() - assert session._session_id is None - - # Session has expired and is no longer available. - with pytest.raises(google.api_core.exceptions.BadRequest): - query_job = session.bqclient.query("SELECT 1") - query_job.result() # blocks until finished diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 700eb500ffd..017c96d46d2 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -23,17 +23,41 @@ from bigframes.ml import core, linear_model import bigframes.pandas as bpd +TEMP_MODEL_ID = bigquery.ModelReference.from_string( + "test-project._anon123.temp_model_id" +) + @pytest.fixture def mock_session(): mock_session = mock.create_autospec(spec=bigframes.Session) - # return values we don't care about, but need to provide to continue the program when calling session._start_query() - mock_session._start_query.return_value = (None, mock.MagicMock()) + mock_session._anonymous_dataset = bigquery.DatasetReference( + TEMP_MODEL_ID.project, TEMP_MODEL_ID.dataset_id + ) + + query_job = mock.create_autospec(bigquery.QueryJob) + type(query_job).destination = mock.PropertyMock( + return_value=bigquery.TableReference( + mock_session._anonymous_dataset, TEMP_MODEL_ID.model_id + ) + ) + mock_session._start_query.return_value = (None, query_job) return mock_session +@pytest.fixture +def bqml_model_factory(mocker: pytest_mock.MockerFixture): + mocker.patch( + "bigframes.ml.core.BqmlModelFactory._create_model_ref", + return_value=TEMP_MODEL_ID, + ) + bqml_model_factory = core.BqmlModelFactory() + + return bqml_model_factory + + @pytest.fixture def mock_y(): mock_y = mock.create_autospec(spec=bpd.DataFrame) @@ -63,17 +87,6 @@ def mock_X(mock_y, mock_session): return mock_X -@pytest.fixture -def bqml_model_factory(mocker: pytest_mock.MockerFixture): - mocker.patch( - "bigframes.ml.core.BqmlModelFactory._create_temp_model_id", - return_value="temp_model_id", - ) - bqml_model_factory = core.BqmlModelFactory() - - return bqml_model_factory - - @pytest.fixture def bqml_model(mock_session): bqml_model = core.BqmlModel( @@ -91,7 +104,7 @@ def test_linear_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -101,7 +114,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -134,7 +147,7 @@ def test_logistic_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -148,7 +161,7 @@ def test_logistic_regression_params_fit( model.fit(mock_X, mock_y) mock_session._start_query.assert_called_once_with( - 'CREATE TEMP MODEL `temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 34a02edd424..ea16722393d 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -14,6 +14,7 @@ from unittest import mock +import google.cloud.bigquery as bigquery import pytest import bigframes.ml.sql as ml_sql @@ -27,7 +28,7 @@ def base_sql_generator() -> ml_sql.BaseSqlGenerator: @pytest.fixture(scope="session") def model_creation_sql_generator() -> ml_sql.ModelCreationSqlGenerator: - return ml_sql.ModelCreationSqlGenerator(model_id="my_model_id") + return ml_sql.ModelCreationSqlGenerator() @pytest.fixture(scope="session") @@ -126,11 +127,14 @@ def test_create_model_produces_correct_sql( ): sql = model_creation_sql_generator.create_model( source_df=mock_df, + model_ref=bigquery.ModelReference.from_string( + "test-proj._anonXYZ.create_model_correct_sql" + ), options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( sql - == """CREATE TEMP MODEL `my_model_id` + == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_model_correct_sql` OPTIONS( option_key1="option_value1", option_key2=2) @@ -144,6 +148,9 @@ def test_create_model_transform_produces_correct_sql( ): sql = model_creation_sql_generator.create_model( source_df=mock_df, + model_ref=bigquery.ModelReference.from_string( + "test-proj._anonXYZ.create_model_transform" + ), options={"option_key1": "option_value1", "option_key2": 2}, transforms=[ "ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a", @@ -152,7 +159,7 @@ def test_create_model_transform_produces_correct_sql( ) assert ( sql - == """CREATE TEMP MODEL `my_model_id` + == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_model_transform` TRANSFORM( ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a, ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b) @@ -168,11 +175,14 @@ def test_create_remote_model_produces_correct_sql( ): sql = model_creation_sql_generator.create_remote_model( connection_name="my_project.us.my_connection", + model_ref=bigquery.ModelReference.from_string( + "test-proj._anonXYZ.create_remote_model" + ), options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( sql - == """CREATE TEMP MODEL `my_model_id` + == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_remote_model` REMOTE WITH CONNECTION `my_project.us.my_connection` OPTIONS( option_key1="option_value1", @@ -184,11 +194,14 @@ def test_create_imported_model_produces_correct_sql( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): sql = model_creation_sql_generator.create_imported_model( + model_ref=bigquery.ModelReference.from_string( + "test-proj._anonXYZ.create_imported_model" + ), options={"option_key1": "option_value1", "option_key2": 2}, ) assert ( sql - == """CREATE TEMP MODEL `my_model_id` + == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_imported_model` OPTIONS( option_key1="option_value1", option_key2=2)""" diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 8fc8acd1759..8ba321d122c 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -66,7 +66,6 @@ def create_bigquery_session( credentials=credentials, location="test-region" ) session = bigframes.Session(context=bqoptions, clients_provider=clients_provider) - session._session_id = session_id return session diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 70c5441c685..4835a24dc7d 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -17,8 +17,6 @@ import sys import unittest.mock as mock -import google.api_core.exceptions -import google.cloud.bigquery import pandas as pd import pytest @@ -26,8 +24,6 @@ import bigframes.pandas as bpd import bigframes.session -from . import resources - leading_whitespace = re.compile(r"^\s+", flags=re.MULTILINE) @@ -114,37 +110,3 @@ def test_pandas_attribute(): assert bpd.Int64Dtype is pd.Int64Dtype assert bpd.StringDtype is pd.StringDtype assert bpd.ArrowDtype is pd.ArrowDtype - - -def test_close_session_after_bq_session_ended(monkeypatch: pytest.MonkeyPatch): - bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) - bqclient.project = "test-project" - session = resources.create_bigquery_session( - bqclient=bqclient, session_id="JUST_A_TEST" - ) - - # Simulate that the session has already expired. - # Note: this needs to be done after the Session is constructed, as the - # initializer sends a query to start the BigQuery Session. - query_job = mock.create_autospec(google.cloud.bigquery.QueryJob, instance=True) - query_job.result.side_effect = google.api_core.exceptions.BadRequest( - "Session JUST_A_TEST has expired and is no longer available." - ) - bqclient.query.return_value = query_job - - # Simulate that the session has already started. - monkeypatch.setattr(bigframes.core.global_session, "_global_session", session) - bpd.options.bigquery._session_started = True - - # Confirm that as a result bigframes.pandas interface is unusable - with pytest.raises( - google.api_core.exceptions.BadRequest, - match="Session JUST_A_TEST has expired and is no longer available.", - ): - bpd.read_gbq("SELECT 'ABC'") - - # Even though the query to stop the session raises an exception, we should - # still be able to close it without raising an error to the user. - bpd.close_session() - assert "CALL BQ.ABORT_SESSION('JUST_A_TEST')" in bqclient.query.call_args.args[0] - assert bigframes.core.global_session._global_session is None From 3a2761f3c38d0de8b8eda47fffa15b8412aa84b0 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 14 Nov 2023 13:35:49 -0800 Subject: [PATCH 159/921] fix: all sort operation are now stable (#195) --- bigframes/core/__init__.py | 8 ++--- bigframes/core/block_transforms.py | 4 +-- bigframes/core/blocks.py | 4 +-- bigframes/core/compile/compiled.py | 6 ++-- bigframes/core/compile/compiler.py | 2 +- bigframes/core/groupby/__init__.py | 4 --- bigframes/core/nodes.py | 1 - bigframes/core/ordering.py | 47 ++++++++++++++---------------- bigframes/dataframe.py | 4 +-- bigframes/series.py | 7 +---- 10 files changed, 32 insertions(+), 55 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index b640692bc85..b476961bdc6 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -200,12 +200,8 @@ def filter(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: ) ) - def order_by( - self, by: Sequence[OrderingColumnReference], stable: bool = False - ) -> ArrayValue: - return ArrayValue( - nodes.OrderByNode(child=self.node, by=tuple(by), stable=stable) - ) + def order_by(self, by: Sequence[OrderingColumnReference]) -> ArrayValue: + return ArrayValue(nodes.OrderByNode(child=self.node, by=tuple(by))) def reversed(self) -> ArrayValue: return ArrayValue(nodes.ReversedNode(child=self.node)) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index e095f21f6b7..ce0fdd219a4 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -509,7 +509,7 @@ def nsmallest( ) for col_id in column_ids ] - block = block.order_by(order_refs, stable=True) + block = block.order_by(order_refs) if keep in ("first", "last"): return block.slice(0, n) else: # keep == "all": @@ -541,7 +541,7 @@ def nlargest( ) for col_id in column_ids ] - block = block.order_by(order_refs, stable=True) + block = block.order_by(order_refs) if keep in ("first", "last"): return block.slice(0, n) else: # keep == "all": diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 6358d28e2ea..f1113d938ec 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -235,10 +235,9 @@ def cols_matching_label(self, partial_label: Label) -> typing.Sequence[str]: def order_by( self, by: typing.Sequence[ordering.OrderingColumnReference], - stable: bool = False, ) -> Block: return Block( - self._expr.order_by(by, stable=stable), + self._expr.order_by(by), index_columns=self.index_columns, column_labels=self.column_labels, index_labels=self.index.names, @@ -1596,7 +1595,6 @@ def merge( # sort uses coalesced join keys always joined_expr = joined_expr.order_by( [ordering.OrderingColumnReference(col_id) for col_id in coalesced_ids], - stable=True, ) joined_expr = joined_expr.select_columns(result_columns) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 4ba5e6bd080..78050ed4f0c 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -753,11 +753,9 @@ def builder(self) -> OrderedIR.Builder: predicates=self._predicates, ) - def order_by( - self, by: Sequence[OrderingColumnReference], stable: bool = False - ) -> OrderedIR: + def order_by(self, by: Sequence[OrderingColumnReference]) -> OrderedIR: expr_builder = self.builder() - expr_builder.ordering = self._ordering.with_ordering_columns(by, stable=stable) + expr_builder.ordering = self._ordering.with_ordering_columns(by) return expr_builder.build() def reversed(self) -> OrderedIR: diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 662e73a4331..39892635f1e 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -129,7 +129,7 @@ def compile_filter(node: nodes.FilterNode, ordered: bool = True): @_compile_node.register def compile_orderby(node: nodes.OrderByNode, ordered: bool = True): if ordered: - return compile_ordered(node.child).order_by(node.by, node.stable) + return compile_ordered(node.child).order_by(node.by) else: return compile_unordered(node.child) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 2a19a83dd5f..22ef11dd194 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -217,7 +217,6 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: ) block = self._block.order_by( [order.OrderingColumnReference(col) for col in self._by_col_ids], - stable=True, ) return windows.Window( block, window_spec, self._selected_cols, drop_null_groups=self._dropna @@ -231,7 +230,6 @@ def expanding(self, min_periods: int = 1) -> windows.Window: ) block = self._block.order_by( [order.OrderingColumnReference(col) for col in self._by_col_ids], - stable=True, ) return windows.Window( block, window_spec, self._selected_cols, drop_null_groups=self._dropna @@ -552,7 +550,6 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: ) block = self._block.order_by( [order.OrderingColumnReference(col) for col in self._by_col_ids], - stable=True, ) return windows.Window( block, @@ -570,7 +567,6 @@ def expanding(self, min_periods: int = 1) -> windows.Window: ) block = self._block.order_by( [order.OrderingColumnReference(col) for col in self._by_col_ids], - stable=True, ) return windows.Window( block, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 8f1e2e5e736..050d3562393 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -145,7 +145,6 @@ class FilterNode(UnaryNode): @dataclass(frozen=True) class OrderByNode(UnaryNode): by: Tuple[OrderingColumnReference, ...] - stable: bool = False @dataclass(frozen=True) diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 2cecd2fe7b2..3ab89e0213c 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -28,8 +28,6 @@ # Sufficient to store any value up to 2^63 DEFAULT_ORDERING_ID_LENGTH: int = math.ceil(63 * math.log(2, ORDERING_ID_STRING_BASE)) -STABLE_SORTS = ["mergesort", "stable"] - class OrderingDirection(Enum): ASC = 1 @@ -113,17 +111,12 @@ def with_non_sequential(self): def with_ordering_columns( self, ordering_value_columns: Sequence[OrderingColumnReference] = (), - stable: bool = False, ) -> ExpressionOrdering: """Creates a new ordering that reorders by the given columns. Args: ordering_value_columns: In decreasing precedence order, the values used to sort the ordering - stable: - If True, will use apply a stable sorting, using the old ordering where - the new ordering produces ties. Otherwise, ties will be resolved in - a performance maximizing way, Returns: Modified ExpressionOrdering @@ -131,29 +124,33 @@ def with_ordering_columns( col_ids_new = [ ordering_ref.column_id for ordering_ref in ordering_value_columns ] - if stable: - # Only reference each column once, so discard old referenc if there is a new reference - old_ordering_keep = [ - ordering_ref - for ordering_ref in self.ordering_value_columns - if ordering_ref.column_id not in col_ids_new - ] - else: - # New ordering needs to keep all total ordering columns no matter what. - # All other old ordering references can be discarded as does not need - # to be a stable sort. - old_ordering_keep = [ - ordering_ref - for ordering_ref in self.ordering_value_columns - if (ordering_ref.column_id not in col_ids_new) - and (ordering_ref.column_id in self.total_ordering_columns) - ] - new_ordering = (*ordering_value_columns, *old_ordering_keep) + old_ordering_keep = [ + ordering_ref + for ordering_ref in self.ordering_value_columns + if ordering_ref.column_id not in col_ids_new + ] + + # Truncate to remove any unneded col references after all total order cols included + new_ordering = self._truncate_ordering( + (*ordering_value_columns, *old_ordering_keep) + ) return ExpressionOrdering( new_ordering, total_ordering_columns=self.total_ordering_columns, ) + def _truncate_ordering( + self, order_refs: tuple[OrderingColumnReference, ...] + ) -> tuple[OrderingColumnReference, ...]: + total_order_cols_remaining = set(self.total_ordering_columns) + for i in range(len(order_refs)): + column = order_refs[i].column_id + if column in total_order_cols_remaining: + total_order_cols_remaining.remove(column) + if len(total_order_cols_remaining) == 0: + return order_refs[: i + 1] + raise ValueError("Ordering did not contain all total_order_cols") + def with_reverse(self): """Reverses the ordering.""" return ExpressionOrdering( diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index bdbc00e6207..4716de48d6e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1262,9 +1262,7 @@ def sort_values( column_id, direction=direction, na_last=na_last ) ) - return DataFrame( - self._block.order_by(ordering, stable=kind in order.STABLE_SORTS) - ) + return DataFrame(self._block.order_by(ordering)) def value_counts( self, diff --git a/bigframes/series.py b/bigframes/series.py index 28290d591e0..2cffdd5fcec 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -35,11 +35,7 @@ import bigframes.core.groupby as groupby import bigframes.core.indexers import bigframes.core.indexes as indexes -from bigframes.core.ordering import ( - OrderingColumnReference, - OrderingDirection, - STABLE_SORTS, -) +from bigframes.core.ordering import OrderingColumnReference, OrderingDirection import bigframes.core.scalar as scalars import bigframes.core.utils as utils import bigframes.core.window @@ -1067,7 +1063,6 @@ def sort_values( na_last=(na_position == "last"), ) ], - stable=kind in STABLE_SORTS, ) return Series(block) From 4ea33b7433532ae3a386a6ffa9eb57360ea39526 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:28:07 -0800 Subject: [PATCH 160/921] feat: log most recent API calls as `recent-bigframes-api-xx` labels on BigQuery jobs (#145) --- bigframes/core/groupby/__init__.py | 3 + bigframes/core/log_adapter.py | 61 ++++++++++++++ bigframes/core/nodes.py | 1 + bigframes/core/window/__init__.py | 2 + bigframes/dataframe.py | 2 + bigframes/operations/datetimes.py | 2 + bigframes/operations/strings.py | 2 + bigframes/operations/structs.py | 2 + bigframes/series.py | 2 + bigframes/session/__init__.py | 7 ++ bigframes/session/_io/bigquery.py | 21 ++++- tests/unit/core/test_log_adapter.py | 60 ++++++++++++++ tests/unit/session/test_io_bigquery.py | 110 ++++++++++++++++++++++++- tests/unit/test_compute_options.py | 12 +-- 14 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 bigframes/core/log_adapter.py create mode 100644 tests/unit/core/test_log_adapter.py diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 22ef11dd194..18cb83fa186 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -19,6 +19,7 @@ import pandas as pd import bigframes.constants as constants +from bigframes.core import log_adapter import bigframes.core as core import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks @@ -33,6 +34,7 @@ import third_party.bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby +@log_adapter.class_logger class DataFrameGroupBy(vendored_pandas_groupby.DataFrameGroupBy): __doc__ = vendored_pandas_groupby.GroupBy.__doc__ @@ -406,6 +408,7 @@ def _resolve_label(self, label: blocks.Label) -> str: return col_ids[0] +@log_adapter.class_logger class SeriesGroupBy(vendored_pandas_groupby.SeriesGroupBy): __doc__ = vendored_pandas_groupby.GroupBy.__doc__ diff --git a/bigframes/core/log_adapter.py b/bigframes/core/log_adapter.py new file mode 100644 index 00000000000..b790d195623 --- /dev/null +++ b/bigframes/core/log_adapter.py @@ -0,0 +1,61 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import threading +from typing import List + +_lock = threading.Lock() +MAX_LABELS_COUNT = 64 +_api_methods: List = [] + + +def class_logger(decorated_cls): + """Decorator that adds logging functionality to each method of the class.""" + for attr_name, attr_value in decorated_cls.__dict__.items(): + if callable(attr_value): + setattr(decorated_cls, attr_name, method_logger(attr_value)) + return decorated_cls + + +def method_logger(method): + """Decorator that adds logging functionality to a method.""" + + @functools.wraps(method) + def wrapper(*args, **kwargs): + api_method_name = str(method.__name__) + # Track regular and "dunder" methods + if api_method_name.startswith("__") or not api_method_name.startswith("_"): + add_api_method(api_method_name) + return method(*args, **kwargs) + + return wrapper + + +def add_api_method(api_method_name): + global _lock + global _api_methods + with _lock: + # Push the method to the front of the _api_methods list + _api_methods.insert(0, api_method_name) + # Keep the list length within the maximum limit (adjust MAX_LABELS_COUNT as needed) + _api_methods = _api_methods[:MAX_LABELS_COUNT] + + +def get_and_reset_api_methods(): + global _lock + with _lock: + previous_api_methods = list(_api_methods) + _api_methods.clear() + return previous_api_methods diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 050d3562393..44a8d808ff5 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from __future__ import annotations from dataclasses import dataclass, field diff --git a/bigframes/core/window/__init__.py b/bigframes/core/window/__init__.py index d3d081124e9..240715b6df4 100644 --- a/bigframes/core/window/__init__.py +++ b/bigframes/core/window/__init__.py @@ -16,12 +16,14 @@ import typing +from bigframes.core import log_adapter import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.operations.aggregations as agg_ops import third_party.bigframes_vendored.pandas.core.window.rolling as vendored_pandas_rolling +@log_adapter.class_logger class Window(vendored_pandas_rolling.Window): __doc__ = vendored_pandas_rolling.Window.__doc__ diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 4716de48d6e..57b4ca42cfc 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -41,6 +41,7 @@ import bigframes._config.display_options as display_options import bigframes.constants as constants import bigframes.core +from bigframes.core import log_adapter import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.groupby as groupby @@ -81,6 +82,7 @@ # Inherits from pandas DataFrame so that we can use the same docstrings. +@log_adapter.class_logger class DataFrame(vendored_pandas_frame.DataFrame): __doc__ = vendored_pandas_frame.DataFrame.__doc__ diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py index 1b20c2d593d..a8a33beb577 100644 --- a/bigframes/operations/datetimes.py +++ b/bigframes/operations/datetimes.py @@ -14,12 +14,14 @@ from __future__ import annotations +from bigframes.core import log_adapter import bigframes.operations as ops import bigframes.operations.base import bigframes.series as series import third_party.bigframes_vendored.pandas.core.indexes.accessor as vendordt +@log_adapter.class_logger class DatetimeMethods( bigframes.operations.base.SeriesMethods, vendordt.DatetimeProperties ): diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 0545ea34d6a..201b19abe80 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -18,6 +18,7 @@ from typing import cast, Literal, Optional, Union import bigframes.constants as constants +from bigframes.core import log_adapter import bigframes.dataframe as df import bigframes.operations as ops import bigframes.operations.base @@ -32,6 +33,7 @@ } +@log_adapter.class_logger class StringMethods(bigframes.operations.base.SeriesMethods, vendorstr.StringMethods): __doc__ = vendorstr.StringMethods.__doc__ diff --git a/bigframes/operations/structs.py b/bigframes/operations/structs.py index 506a5577094..b2ae98f3789 100644 --- a/bigframes/operations/structs.py +++ b/bigframes/operations/structs.py @@ -18,6 +18,7 @@ import ibis.expr.types as ibis_types +from bigframes.core import log_adapter import bigframes.dataframe import bigframes.operations import bigframes.operations.base @@ -38,6 +39,7 @@ def _as_ibis(self, x: ibis_types.Value): return struct_value[name].name(name) +@log_adapter.class_logger class StructAccessor( bigframes.operations.base.SeriesMethods, vendoracessors.StructAccessor ): diff --git a/bigframes/series.py b/bigframes/series.py index 2cffdd5fcec..c929775a000 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -30,6 +30,7 @@ import bigframes.constants as constants import bigframes.core +from bigframes.core import log_adapter import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.groupby as groupby @@ -55,6 +56,7 @@ LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] +@log_adapter.class_logger class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Series): def __init__(self, *args, **kwargs): self._query_job: Optional[bigquery.QueryJob] = None diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 1b12e5deeb8..069bd5d2602 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -64,6 +64,7 @@ import bigframes._config.bigquery_options as bigquery_options import bigframes.constants as constants +from bigframes.core import log_adapter import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.guid as guid @@ -1347,6 +1348,10 @@ def _start_query( Starts query job and waits for results. """ job_config = self._prepare_job_config(job_config) + api_methods = log_adapter.get_and_reset_api_methods() + job_config.labels = bigframes_io.create_job_configs_labels( + job_configs_labels=job_config.labels, api_methods=api_methods + ) query_job = self.bqclient.query(sql, job_config=job_config) opts = bigframes.options.display @@ -1381,6 +1386,8 @@ def _prepare_job_config( ) -> bigquery.QueryJobConfig: if job_config is None: job_config = self.bqclient.default_query_job_config + if job_config is None: + job_config = bigquery.QueryJobConfig() if bigframes.options.compute.maximum_bytes_billed is not None: job_config.maximum_bytes_billed = ( bigframes.options.compute.maximum_bytes_billed diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index badc91e3565..dae73301e7b 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -17,17 +17,36 @@ from __future__ import annotations import datetime +import itertools import textwrap import types -from typing import Dict, Iterable, Optional, Union +from typing import Dict, Iterable, Optional, Sequence, Union import uuid import google.cloud.bigquery as bigquery IO_ORDERING_ID = "bqdf_row_nums" +MAX_LABELS_COUNT = 64 TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}" +def create_job_configs_labels( + job_configs_labels: Optional[Dict[str, str]], + api_methods: Sequence[str], +) -> Dict[str, str]: + if job_configs_labels is None: + job_configs_labels = {} + + labels = list( + itertools.chain( + job_configs_labels.keys(), + (f"recent-bigframes-api-{i}" for i in range(len(api_methods))), + ) + ) + values = list(itertools.chain(job_configs_labels.values(), api_methods)) + return dict(zip(labels[:MAX_LABELS_COUNT], values[:MAX_LABELS_COUNT])) + + def create_export_csv_statement( table_id: str, uri: str, field_delimiter: str, header: bool ) -> str: diff --git a/tests/unit/core/test_log_adapter.py b/tests/unit/core/test_log_adapter.py new file mode 100644 index 00000000000..376b7f20758 --- /dev/null +++ b/tests/unit/core/test_log_adapter.py @@ -0,0 +1,60 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from bigframes.core import log_adapter + +MAX_LABELS_COUNT = 64 + + +@pytest.fixture +def test_instance(): + # Create a simple class for testing + @log_adapter.class_logger + class TestClass: + def method1(self): + pass + + def method2(self): + pass + + return TestClass() + + +def test_method_logging(test_instance): + test_instance.method1() + test_instance.method2() + + # Check if the methods were added to the _api_methods list + api_methods = log_adapter.get_and_reset_api_methods() + assert api_methods is not None + assert "method1" in api_methods + assert "method2" in api_methods + + +def test_add_api_method_limit(test_instance): + # Ensure that add_api_method correctly adds a method to _api_methods + for i in range(70): + test_instance.method2() + assert len(log_adapter._api_methods) == MAX_LABELS_COUNT + + +def test_get_and_reset_api_methods(test_instance): + # Ensure that get_and_reset_api_methods returns a copy and resets the list + test_instance.method1() + test_instance.method2() + previous_methods = log_adapter.get_and_reset_api_methods() + assert previous_methods is not None + assert log_adapter._api_methods == [] diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 03470208e42..e1481d3f05c 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -19,7 +19,113 @@ import google.cloud.bigquery as bigquery import pytest -import bigframes.session._io.bigquery +import bigframes +from bigframes.core import log_adapter +import bigframes.pandas as bpd +import bigframes.session._io.bigquery as io_bq + + +def test_create_job_configs_labels_is_none(): + api_methods = ["agg", "series-mode"] + labels = io_bq.create_job_configs_labels( + job_configs_labels=None, api_methods=api_methods + ) + expected_dict = { + "recent-bigframes-api-0": "agg", + "recent-bigframes-api-1": "series-mode", + } + assert labels is not None + assert labels == expected_dict + + +def test_create_job_configs_labels_length_limit_not_met(): + cur_labels = { + "bigframes-api": "read_pandas", + "source": "bigquery-dataframes-temp", + } + api_methods = ["agg", "series-mode"] + labels = io_bq.create_job_configs_labels( + job_configs_labels=cur_labels, api_methods=api_methods + ) + expected_dict = { + "bigframes-api": "read_pandas", + "source": "bigquery-dataframes-temp", + "recent-bigframes-api-0": "agg", + "recent-bigframes-api-1": "series-mode", + } + assert labels is not None + assert len(labels) == 4 + assert labels == expected_dict + + +def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): + cur_labels = { + "bigframes-api": "read_pandas", + "source": "bigquery-dataframes-temp", + } + df = bpd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) + # Test running two methods + df.head() + df.max() + api_methods = log_adapter._api_methods + + labels = io_bq.create_job_configs_labels( + job_configs_labels=cur_labels, api_methods=api_methods + ) + expected_dict = { + "bigframes-api": "read_pandas", + "source": "bigquery-dataframes-temp", + "recent-bigframes-api-0": "__init__", + "recent-bigframes-api-1": "max", + "recent-bigframes-api-2": "__init__", + "recent-bigframes-api-3": "head", + "recent-bigframes-api-4": "__init__", + } + assert labels is not None + assert len(labels) == 7 + assert labels == expected_dict + + +def test_create_job_configs_labels_length_limit_met_and_labels_is_none(): + df = bpd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) + # Test running methods more than the labels' length limit + for i in range(66): + df.head() + api_methods = log_adapter._api_methods + + labels = io_bq.create_job_configs_labels( + job_configs_labels=None, api_methods=api_methods + ) + assert labels is not None + assert len(labels) == 64 + assert "head" in labels.values() + + +def test_create_job_configs_labels_length_limit_met(): + cur_labels = { + "bigframes-api": "read_pandas", + "source": "bigquery-dataframes-temp", + } + for i in range(60): + key = f"bigframes-api-test-{i}" + value = f"test{i}" + cur_labels[key] = value + # If cur_labels length is 62, we can only add one label from api_methods + df = bpd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) + # Test running two methods + df.head() + df.max() + api_methods = log_adapter._api_methods + + labels = io_bq.create_job_configs_labels( + job_configs_labels=cur_labels, api_methods=api_methods + ) + assert labels is not None + assert len(labels) == 64 + assert "max" in labels.values() + assert "head" not in labels.values() + assert "bigframes-api" in labels.keys() + assert "source" in labels.keys() def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): @@ -125,5 +231,5 @@ def test_create_temp_table_default_expiration(): ), ) def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str): - sql = bigframes.session._io.bigquery.bq_schema_to_sql(schema) + sql = io_bq.bq_schema_to_sql(schema) assert sql == expected diff --git a/tests/unit/test_compute_options.py b/tests/unit/test_compute_options.py index 499a0a5fefa..a613bca7b90 100644 --- a/tests/unit/test_compute_options.py +++ b/tests/unit/test_compute_options.py @@ -18,13 +18,9 @@ def test_maximum_bytes_option(): session = resources.create_bigquery_session() - num_query_calls = 0 with bf.option_context("compute.maximum_bytes_billed", 10000): - # clear initial method calls - session.bqclient.method_calls = [] + session.bqclient.query.reset_mock() session._start_query("query") - for call in session.bqclient.method_calls: - _, _, kwargs = call - num_query_calls += 1 - assert kwargs["job_config"].maximum_bytes_billed == 10000 - assert num_query_calls > 0 + call = session.bqclient.query.call_args + assert call.kwargs["job_config"].maximum_bytes_billed == 10000 + session.bqclient.query.assert_called_once() From e2b7df5530de53f4b6a7d721a8d226c94cabee34 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 00:26:15 +0000 Subject: [PATCH 161/921] chore(main): release 0.14.0 (#183) :robot: I have created a release *beep* *boop* --- ## [0.14.0](https://togithub.com/googleapis/python-bigquery-dataframes/compare/v0.13.0...v0.14.0) (2023-11-14) ### Features * Add 'cross' join support ([#176](https://togithub.com/googleapis/python-bigquery-dataframes/issues/176)) ([765446a](https://togithub.com/googleapis/python-bigquery-dataframes/commit/765446a929abe1ac076c3037afa7892f64105356)) * Add 'index', 'pad', 'nearest' interpolate methods ([#162](https://togithub.com/googleapis/python-bigquery-dataframes/issues/162)) ([6a28403](https://togithub.com/googleapis/python-bigquery-dataframes/commit/6a2840349a23035bdfdabacd1e231b41bbb5ed7a)) * Add series.sample (identical to existing dataframe.sample) ([#187](https://togithub.com/googleapis/python-bigquery-dataframes/issues/187)) ([37914a4](https://togithub.com/googleapis/python-bigquery-dataframes/commit/37914a4077c681881491f5c36d1a9c9f4255e18f)) * Add unordered sql compilation ([#156](https://togithub.com/googleapis/python-bigquery-dataframes/issues/156)) ([58f420c](https://togithub.com/googleapis/python-bigquery-dataframes/commit/58f420c91d94ca085e9810f36513ffe772bfddcf)) * Log most recent API calls as `recent-bigframes-api-xx` labels on BigQuery jobs ([#145](https://togithub.com/googleapis/python-bigquery-dataframes/issues/145)) ([4ea33b7](https://togithub.com/googleapis/python-bigquery-dataframes/commit/4ea33b7433532ae3a386a6ffa9eb57360ea39526)) * Read_gbq creates order deterministically without table copy ([#191](https://togithub.com/googleapis/python-bigquery-dataframes/issues/191)) ([8ab81de](https://togithub.com/googleapis/python-bigquery-dataframes/commit/8ab81dee4d0eee499094f2dd576550f0c59d7551)) * Support `date_series.astype("string[pyarrow]")` to cast DATE to STRING ([#186](https://togithub.com/googleapis/python-bigquery-dataframes/issues/186)) ([aee0e8e](https://togithub.com/googleapis/python-bigquery-dataframes/commit/aee0e8e2518c59bd1e0b07940c3309871fde8899)) * Support `series.at[row_label] = scalar` ([#173](https://togithub.com/googleapis/python-bigquery-dataframes/issues/173)) ([0c8bd33](https://togithub.com/googleapis/python-bigquery-dataframes/commit/0c8bd33806bb99206b8b12dbdf7d7485c6ffb759)) * Temporary resources no longer use BigQuery Sessions ([#194](https://togithub.com/googleapis/python-bigquery-dataframes/issues/194)) ([4a02cac](https://togithub.com/googleapis/python-bigquery-dataframes/commit/4a02cac88c7d7b46bed1fa813a862fc2ef9ef084)) ### Bug Fixes * All sort operation are now stable ([#195](https://togithub.com/googleapis/python-bigquery-dataframes/issues/195)) ([3a2761f](https://togithub.com/googleapis/python-bigquery-dataframes/commit/3a2761f3c38d0de8b8eda47fffa15b8412aa84b0)) * Default to 7 days expiration for `read_csv`, `read_json`, `read_parquet` ([#193](https://togithub.com/googleapis/python-bigquery-dataframes/issues/193)) ([03606cd](https://togithub.com/googleapis/python-bigquery-dataframes/commit/03606cda30eb7645bfd4534460112dcca56b0ab0)) * Deprecate the `remote_service_type` in llm model ([#180](https://togithub.com/googleapis/python-bigquery-dataframes/issues/180)) ([a8a409a](https://togithub.com/googleapis/python-bigquery-dataframes/commit/a8a409ab0bd1f99dfb442df0703bf8786e0fe58e)) * For reset_index on unnamed multiindex, always use level_[n] label ([#182](https://togithub.com/googleapis/python-bigquery-dataframes/issues/182)) ([f95000d](https://togithub.com/googleapis/python-bigquery-dataframes/commit/f95000d3f88662be4d88c8b0152f1b838e99ec55)) * Match pandas behavior when assigning listlike to empty dfs ([#172](https://togithub.com/googleapis/python-bigquery-dataframes/issues/172)) ([c1d1f42](https://togithub.com/googleapis/python-bigquery-dataframes/commit/c1d1f42a21cc089877f79ebb46a39ddef6958e04)) * Use anonymous dataset instead of session dataset for temp tables ([#181](https://togithub.com/googleapis/python-bigquery-dataframes/issues/181)) ([800d44e](https://togithub.com/googleapis/python-bigquery-dataframes/commit/800d44eb5eb77da5d87b2e005f5a2ed53842e7b5)) * Use random table for `read_pandas` ([#192](https://togithub.com/googleapis/python-bigquery-dataframes/issues/192)) ([741c75e](https://togithub.com/googleapis/python-bigquery-dataframes/commit/741c75e5797e26a1487ff3da76a07953d9537f3f)) * Use random table when loading data for `read_csv`, `read_json`, `read_parquet` ([#175](https://togithub.com/googleapis/python-bigquery-dataframes/issues/175)) ([9d2e6dc](https://togithub.com/googleapis/python-bigquery-dataframes/commit/9d2e6dc1ae4e11e80da4aabe0daa3a6044137cc6)) ### Documentation * Add code samples for `read_gbq_function` using community UDFs ([#188](https://togithub.com/googleapis/python-bigquery-dataframes/issues/188)) ([7506eab](https://togithub.com/googleapis/python-bigquery-dataframes/commit/7506eabf2e58159507809e36abfe90c417dfe92f)) * Add docstring code samples for `Series.apply` and `DataFrame.map` ([#185](https://togithub.com/googleapis/python-bigquery-dataframes/issues/185)) ([c816d84](https://togithub.com/googleapis/python-bigquery-dataframes/commit/c816d843e6f3c5a944cd4395ed0e1e91cec49812)) * Add llm kmeans notebook as an included example ([#177](https://togithub.com/googleapis/python-bigquery-dataframes/issues/177)) ([d49ae42](https://togithub.com/googleapis/python-bigquery-dataframes/commit/d49ae42a379fafd601cc94227e7f8f14b3d5f8c3)) * Use `head()` to get top `n` results, not to preview results ([#190](https://togithub.com/googleapis/python-bigquery-dataframes/issues/190)) ([87f84c9](https://togithub.com/googleapis/python-bigquery-dataframes/commit/87f84c9e58e7d0ea521ac386c9f02791cdddd19f)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc327b2e966..1f76b782728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,41 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.14.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.13.0...v0.14.0) (2023-11-14) + + +### Features + +* Add 'cross' join support ([#176](https://github.com/googleapis/python-bigquery-dataframes/issues/176)) ([765446a](https://github.com/googleapis/python-bigquery-dataframes/commit/765446a929abe1ac076c3037afa7892f64105356)) +* Add 'index', 'pad', 'nearest' interpolate methods ([#162](https://github.com/googleapis/python-bigquery-dataframes/issues/162)) ([6a28403](https://github.com/googleapis/python-bigquery-dataframes/commit/6a2840349a23035bdfdabacd1e231b41bbb5ed7a)) +* Add series.sample (identical to existing dataframe.sample) ([#187](https://github.com/googleapis/python-bigquery-dataframes/issues/187)) ([37914a4](https://github.com/googleapis/python-bigquery-dataframes/commit/37914a4077c681881491f5c36d1a9c9f4255e18f)) +* Add unordered sql compilation ([#156](https://github.com/googleapis/python-bigquery-dataframes/issues/156)) ([58f420c](https://github.com/googleapis/python-bigquery-dataframes/commit/58f420c91d94ca085e9810f36513ffe772bfddcf)) +* Log most recent API calls as `recent-bigframes-api-xx` labels on BigQuery jobs ([#145](https://github.com/googleapis/python-bigquery-dataframes/issues/145)) ([4ea33b7](https://github.com/googleapis/python-bigquery-dataframes/commit/4ea33b7433532ae3a386a6ffa9eb57360ea39526)) +* Read_gbq creates order deterministically without table copy ([#191](https://github.com/googleapis/python-bigquery-dataframes/issues/191)) ([8ab81de](https://github.com/googleapis/python-bigquery-dataframes/commit/8ab81dee4d0eee499094f2dd576550f0c59d7551)) +* Support `date_series.astype("string[pyarrow]")` to cast DATE to STRING ([#186](https://github.com/googleapis/python-bigquery-dataframes/issues/186)) ([aee0e8e](https://github.com/googleapis/python-bigquery-dataframes/commit/aee0e8e2518c59bd1e0b07940c3309871fde8899)) +* Support `series.at[row_label] = scalar` ([#173](https://github.com/googleapis/python-bigquery-dataframes/issues/173)) ([0c8bd33](https://github.com/googleapis/python-bigquery-dataframes/commit/0c8bd33806bb99206b8b12dbdf7d7485c6ffb759)) +* Temporary resources no longer use BigQuery Sessions ([#194](https://github.com/googleapis/python-bigquery-dataframes/issues/194)) ([4a02cac](https://github.com/googleapis/python-bigquery-dataframes/commit/4a02cac88c7d7b46bed1fa813a862fc2ef9ef084)) + + +### Bug Fixes + +* All sort operation are now stable ([#195](https://github.com/googleapis/python-bigquery-dataframes/issues/195)) ([3a2761f](https://github.com/googleapis/python-bigquery-dataframes/commit/3a2761f3c38d0de8b8eda47fffa15b8412aa84b0)) +* Default to 7 days expiration for `read_csv`, `read_json`, `read_parquet` ([#193](https://github.com/googleapis/python-bigquery-dataframes/issues/193)) ([03606cd](https://github.com/googleapis/python-bigquery-dataframes/commit/03606cda30eb7645bfd4534460112dcca56b0ab0)) +* Deprecate the `remote_service_type` in llm model ([#180](https://github.com/googleapis/python-bigquery-dataframes/issues/180)) ([a8a409a](https://github.com/googleapis/python-bigquery-dataframes/commit/a8a409ab0bd1f99dfb442df0703bf8786e0fe58e)) +* For reset_index on unnamed multiindex, always use level_[n] label ([#182](https://github.com/googleapis/python-bigquery-dataframes/issues/182)) ([f95000d](https://github.com/googleapis/python-bigquery-dataframes/commit/f95000d3f88662be4d88c8b0152f1b838e99ec55)) +* Match pandas behavior when assigning listlike to empty dfs ([#172](https://github.com/googleapis/python-bigquery-dataframes/issues/172)) ([c1d1f42](https://github.com/googleapis/python-bigquery-dataframes/commit/c1d1f42a21cc089877f79ebb46a39ddef6958e04)) +* Use anonymous dataset instead of session dataset for temp tables ([#181](https://github.com/googleapis/python-bigquery-dataframes/issues/181)) ([800d44e](https://github.com/googleapis/python-bigquery-dataframes/commit/800d44eb5eb77da5d87b2e005f5a2ed53842e7b5)) +* Use random table for `read_pandas` ([#192](https://github.com/googleapis/python-bigquery-dataframes/issues/192)) ([741c75e](https://github.com/googleapis/python-bigquery-dataframes/commit/741c75e5797e26a1487ff3da76a07953d9537f3f)) +* Use random table when loading data for `read_csv`, `read_json`, `read_parquet` ([#175](https://github.com/googleapis/python-bigquery-dataframes/issues/175)) ([9d2e6dc](https://github.com/googleapis/python-bigquery-dataframes/commit/9d2e6dc1ae4e11e80da4aabe0daa3a6044137cc6)) + + +### Documentation + +* Add code samples for `read_gbq_function` using community UDFs ([#188](https://github.com/googleapis/python-bigquery-dataframes/issues/188)) ([7506eab](https://github.com/googleapis/python-bigquery-dataframes/commit/7506eabf2e58159507809e36abfe90c417dfe92f)) +* Add docstring code samples for `Series.apply` and `DataFrame.map` ([#185](https://github.com/googleapis/python-bigquery-dataframes/issues/185)) ([c816d84](https://github.com/googleapis/python-bigquery-dataframes/commit/c816d843e6f3c5a944cd4395ed0e1e91cec49812)) +* Add llm kmeans notebook as an included example ([#177](https://github.com/googleapis/python-bigquery-dataframes/issues/177)) ([d49ae42](https://github.com/googleapis/python-bigquery-dataframes/commit/d49ae42a379fafd601cc94227e7f8f14b3d5f8c3)) +* Use `head()` to get top `n` results, not to preview results ([#190](https://github.com/googleapis/python-bigquery-dataframes/issues/190)) ([87f84c9](https://github.com/googleapis/python-bigquery-dataframes/commit/87f84c9e58e7d0ea521ac386c9f02791cdddd19f)) + ## [0.13.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.12.0...v0.13.0) (2023-11-07) diff --git a/bigframes/version.py b/bigframes/version.py index 0a5df274799..5a94f72649a 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13.0" +__version__ = "0.14.0" From f957b278b39e0a472a3153e9e1906c2d5f2ac2e5 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 14 Nov 2023 18:52:15 -0800 Subject: [PATCH 162/921] docs: add an example notebook about line graphs (#197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../bq_dataframes_covid_line_graphs.ipynb | 598 ++++++++++++++++++ noxfile.py | 1 + 2 files changed, 599 insertions(+) create mode 100644 notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb diff --git a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb new file mode 100644 index 00000000000..8b18cc89674 --- /dev/null +++ b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb @@ -0,0 +1,598 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9GIt_orUtNvA" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h7AT6h2ItNvD" + }, + "source": [ + "## Use BigQuery DataFrames to visualize COVID-19 data\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + "
" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "n-MFJQxLtNvE" + }, + "source": [ + "## Overview\n", + "\n", + "The goal of this notebook is to demonstrate creating line graphs from a ~20 million-row BigQuery dataset using BigQuery DataFrames. We will first create a plain line graph using matplotlip, then we will downsample and download our data to create a graph with a line of best fit using seaborn.\n", + "\n", + "If you're like me, during 2020 (and/or later years) you often found yourself looking at charts like [these](https://health.google.com/covid-19/open-data/explorer/statistics) visualizing COVID-19 cases over time. For our first graph, we're going to recreate one of those charts by filtering, summing, and then graphing COVID-19 data from the United States. BigQuery DataFrame's default integration with matplotlib will get us a satisfying result for this first graph.\n", + "\n", + "For our second graph, though, we want to use a scatterplot with a line of best fit, something that matplotlib will not do for us automatically. So, we'll demonstrate how to downsample our data and use seaborn to make our plot. Our second graph will be of symptom-related search trends against new cases of COVID-19, so we'll see if searches for things like \"cough\" and \"fever\" are more common in the places and times where more new cases of COVID-19 occur." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "ffqBzbNztNvF" + }, + "source": [ + "### Dataset\n", + "\n", + "This notebook uses the [BigQuery COVID-19 Open Data](https://pantheon.corp.google.com/marketplace/product/bigquery-public-datasets/covid19-open-data). In this dataset, each row represents a new observation of the COVID-19 situation in a particular time and place. We will use the \"new_confirmed\" column, which contains the number of new COVID-19 cases at each observation, along with the \"search_trends_cough\", \"search_trends_fever\", and \"search_trends_bruise\" columns, which are [Google Trends](https://trends.google.com/trends/) data for searches related to cough, fever, and bruises. In the first section of the notebook, we will also use the \"country_code\" and \"date\" columns to compile one data point per day for a particular country." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Nf__tMR-tNvF" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7_rsbkCktNvG" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XZKC6iMFxmMG" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4aooKMmnxrWF" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pv5A8Tm-yC1U" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bk03Rt_HyGx-" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "B9RWxD1btNvK" + }, + "source": [ + "Now we are ready to use BigQuery DataFrames!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wJ0gXezj2w1t" + }, + "source": [ + "## Visualization #1: Cases over time in the US" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "xckgWno6ouHY" + }, + "source": [ + "### Set up project and filter data" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "-uiY0hh4tNvK" + }, + "source": [ + "First, let's do project setup. We use options to tell BigQuery DataFrames what project and what region to use for our cloud computing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R7STCS8xB5d2" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v6FGschEowht" + }, + "source": [ + "Next, we read the data from a publicly available BigQuery dataset. This will take ~1 minute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zDSwoBo1CU3G" + }, + "outputs": [], + "source": [ + "all_data = bf.read_gbq(\"bigquery-public-data.covid19_open_data.covid19_open_data\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9qV2y3iHp13y" + }, + "source": [ + "Using pandas syntax, we will select from our all_data input dataframe only those rows where the country_code is US. This is called row filtering." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UjMT_qhjf8Fu" + }, + "outputs": [], + "source": [ + "usa_data = all_data[all_data[\"country_code\"] == \"US\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IYCUayWkwq8c" + }, + "source": [ + "We're only concerned with the date and the total number of confirmed cases for now, so select just those two columns as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IaoUf57ZwrJ8" + }, + "outputs": [], + "source": [ + "usa_data = usa_data[[\"date\", \"new_confirmed\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "94oqNRnDvGkr" + }, + "source": [ + "### Sum data" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "TNCQWZW83U0b" + }, + "source": [ + "`usa_data.groupby(\"date\")` will give us a groupby object that lets us perform operations on groups of rows with the same date. We call sum on that object to get the sum for each day. This process might be familiar to pandas users." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tYDoaKgJChiq" + }, + "outputs": [], + "source": [ + "# numeric_only = True because we don't want to sum dates\n", + "new_cases_usa = usa_data.groupby(\"date\").sum(numeric_only = True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3jcwFPgK5BLh" + }, + "source": [ + "### Line graph" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8GvJAgnH5Nzi" + }, + "source": [ + "BigQuery DataFrames implements some of the interface required by matplotlib. This means we can pass our DataFrame right into `pyplot.plt` and using the default settings, matplotlib will draw a simple line graph for us." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gFbCgfFC2gHw" + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# matplotlin will draw a line graph by default\n", + "plt.plot(new_cases_usa)\n", + "# Rotate the labels on the x axis so that they don't overlap\n", + "plt.xticks(rotation=45)\n", + "# label the y axis for clarity\n", + "plt.ylabel(\"New Cases\")\n", + "\n", + "# Show the plot\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sM5-HFDx70RG" + }, + "source": [ + "## Visualization #2: Symptom-related searches compared to new cases" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "se1b6Vf4XB9_" + }, + "source": [ + "### Filter data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wl2o-NYMoygb" + }, + "source": [ + "We're curious if searches for symptoms like \"cough\" and \"fever\" went up in the same times and places that new COVID-19 cases occured, compared to non-symptoms like \"bruise.\" Let's plot searches vs. new cases to see if it looks like there's a correlation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "olfnCzyg8jYi" + }, + "source": [ + "First, we select the new cases column and the search trends we're interested in." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LqqHzjty8jk0" + }, + "outputs": [], + "source": [ + "symptom_data = all_data[[\"new_confirmed\", \"search_trends_cough\", \"search_trends_fever\", \"search_trends_bruise\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b3DlJX-k9SPk" + }, + "source": [ + "Not all rows have data for all of these columns, so let's select only the rows that do." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "g4MeM8Oe9Q6X" + }, + "outputs": [], + "source": [ + "symptom_data = symptom_data.dropna()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IlXt__om9QYI" + }, + "source": [ + "We want to use a line of best fit to make the correlation stand out. Matplotlib does not include a feature for lines of best fit, but seaborn, which is built on matplotlib, does.\n", + "\n", + "BigQuery DataFrames does not currently integrate with seaborn by default. So we will demonstrate how to downsample and download a DataFrame, and use seaborn on the downloaded data." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MmfgKMaEXNbL" + }, + "source": [ + "### Downsample and download" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wIuG1JRTPAk9" + }, + "source": [ + "BigQuery DataFrames options let us set up the sampling functionality we need. Calls to `to_pandas()` usually download all the data available in our BigQuery table and store it locally as a pandas DataFrame. `pd.options.sampling.enable_downsampling = True` will make future calls to `to_pandas` use downsampling to download only part of the data, and `pd.options.sampling.max_download_size` allows us to set the amount of data to download." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x95ZgBkyDMP4" + }, + "outputs": [], + "source": [ + "bf.options.sampling.enable_downsampling = True # enable downsampling\n", + "bf.options.sampling.max_download_size = 5 # download only 5 mb of data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C6sCXkrQPJC_" + }, + "source": [ + "Download the data and note the message letting us know that downsampling is being used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V0OK02D7PJSL" + }, + "outputs": [], + "source": [ + "local_symptom_data = symptom_data.to_pandas(sampling_method=\"uniform\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T9Hub_EAXWvY" + }, + "source": [ + "### Graph with lines of best fit" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "hoQ9TPgUPJnN" + }, + "source": [ + "We will now use seaborn to make the plots with the lines of best fit for cough, fever, and bruise. Note that since we're working with a local pandas dataframe, you could use any other Python library or technique you're familiar with, but we'll stick to seaborn for this notebook.\n", + "\n", + "Seaborn will take a few seconds to calculate the lines. Since cough and fever are symptoms of COVID-19, but bruising isn't, we expect the slope of the line of best fit to be positive in the first two graphs, but not the third, indicating that there is a correlation between new COVID-19 cases and cough- and fever-related searches." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EG7qM3R18bOb" + }, + "outputs": [], + "source": [ + "import seaborn as sns\n", + "\n", + "# first, convert to a data type that is suitable for seaborn\n", + "local_symptom_data[\"new_confirmed\"] = \\\n", + " local_symptom_data[\"new_confirmed\"].astype(float)\n", + "local_symptom_data[\"search_trends_cough\"] = \\\n", + " local_symptom_data[\"search_trends_cough\"].astype(float)\n", + "\n", + "# draw the graph. This might take ~30 seconds.\n", + "sns.regplot(x=\"new_confirmed\", y=\"search_trends_cough\", data=local_symptom_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5nVy61rEGaM4" + }, + "outputs": [], + "source": [ + "# similarly, for fever\n", + "\n", + "local_symptom_data[\"search_trends_fever\"] = \\\n", + " local_symptom_data[\"search_trends_fever\"].astype(float)\n", + "sns.regplot(x=\"new_confirmed\", y=\"search_trends_fever\", data=local_symptom_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-S1A9E3WGaYH" + }, + "outputs": [], + "source": [ + "# similarly, for bruise\n", + "local_symptom_data[\"search_trends_bruise\"] = \\\n", + " local_symptom_data[\"search_trends_bruise\"].astype(float)\n", + "sns.regplot(\n", + " x=\"new_confirmed\",\n", + " y=\"search_trends_bruise\",\n", + " data=local_symptom_data\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Hd2A8707Uhz2" + }, + "source": [ + "We see that the slope of the line is positive in the graphs for cough and fever, but flat for bruise. That means that in places with increasing new cases of COVID-19, we saw increasing searches for cough and fever, but we didn't see increasing searches for unrelated symptoms like bruises. Interesting!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recap" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We used matplotlib to draw a line graph of COVID-19 cases over time in the USA. Then, we used downsampling to download only a portion of the available data, and used seaborn locally to plot lines of best fit to observe corellation between COVID-19 cases and searches for related vs. unrelated symptoms.\n", + "\n", + "Thank you for using BigQuery DataFrames!" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "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.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/noxfile.py b/noxfile.py index 3dd23ba04f6..da61232fc79 100644 --- a/noxfile.py +++ b/noxfile.py @@ -615,6 +615,7 @@ def notebook(session): "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", "notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb", "notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb", + "notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb", # The experimental notebooks imagine features that don't yet # exist or only exist as temporary prototypes. "notebooks/experimental/longer_ml_demo.ipynb", From 8324f133547ec35da5eefc0a8b02fe0f3887d81d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 15 Nov 2023 18:58:14 -0800 Subject: [PATCH 163/921] fix: correctly handle null values when initializing fingerprint ordering (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/session/__init__.py | 9 ++++++--- tests/system/small/test_dataframe.py | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 069bd5d2602..928123ce749 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1120,8 +1120,9 @@ def _create_total_ordering( ordering_hash_part = guid.generate_guid("bigframes_ordering_") ordering_rand_part = guid.generate_guid("bigframes_ordering_") + # All inputs into hash must be non-null or resulting hash will be null str_values = list( - map(lambda col: _convert_to_string(table[col]), table.columns) + map(lambda col: _convert_to_nonnull_string(table[col]), table.columns) ) full_row_str = ( str_values[0].concat(*str_values[1:]) @@ -1419,7 +1420,7 @@ def _can_cluster_bq(field: bigquery.SchemaField): ) -def _convert_to_string(column: ibis_types.Column) -> ibis_types.StringColumn: +def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringValue: col_type = column.type() if ( col_type.is_numeric() @@ -1436,4 +1437,6 @@ def _convert_to_string(column: ibis_types.Column) -> ibis_types.StringColumn: # TO_JSON_STRING works with all data types, but isn't the most efficient # Needed for JSON, STRUCT and ARRAY datatypes result = vendored_ibis_ops.ToJsonString(column).to_expr() # type: ignore - return typing.cast(ibis_types.StringColumn, result) + # Escape backslashes and use backslash as delineator + escaped = typing.cast(ibis_types.StringColumn, result.fillna("")).replace("\\", "\\\\") # type: ignore + return typing.cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e522878229a..a0cf25807cf 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2703,6 +2703,14 @@ def test_sample(scalars_dfs, frac, n, random_state): assert bf_result.shape[1] == scalars_df.shape[1] +def test_sample_determinism(penguins_df_default_index): + df = penguins_df_default_index.sample(n=100, random_state=12345).head(15) + bf_result = df.to_pandas() + bf_result2 = df.to_pandas() + + pandas.testing.assert_frame_equal(bf_result, bf_result2) + + def test_sample_raises_value_error(scalars_dfs): scalars_df, _ = scalars_dfs with pytest.raises( From 5ab5059f7db5d0f2be735dca76bc8e5163287c4d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 20:25:53 -0800 Subject: [PATCH 164/921] chore(main): release 0.14.1 (#207) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ bigframes/version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f76b782728..091967513aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.14.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.14.0...v0.14.1) (2023-11-16) + + +### Bug Fixes + +* Correctly handle null values when initializing fingerprint ordering ([#210](https://github.com/googleapis/python-bigquery-dataframes/issues/210)) ([8324f13](https://github.com/googleapis/python-bigquery-dataframes/commit/8324f133547ec35da5eefc0a8b02fe0f3887d81d)) + + +### Documentation + +* Add an example notebook about line graphs ([#197](https://github.com/googleapis/python-bigquery-dataframes/issues/197)) ([f957b27](https://github.com/googleapis/python-bigquery-dataframes/commit/f957b278b39e0a472a3153e9e1906c2d5f2ac2e5)) + ## [0.14.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.13.0...v0.14.0) (2023-11-14) diff --git a/bigframes/version.py b/bigframes/version.py index 5a94f72649a..46e57e5b888 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.0" +__version__ = "0.14.1" From e8532b1d999d26ea1ebdd30efb8f2c0a93a6a28d Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:08:27 -0800 Subject: [PATCH 165/921] fix: polish the llm+kmeans notebook (#208) --- .../bq_dataframes_llm_code_generation.ipynb | 2 +- .../bq_dataframes_llm_kmeans.ipynb | 1181 +++++++++++++++-- 2 files changed, 1057 insertions(+), 126 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index 0f113b84c6d..0a41447a53e 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -34,7 +34,7 @@ "\n", "\n", " \n", diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 46c49552889..ae038136393 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -31,7 +31,7 @@ "
\n", - " \n", + " \n", " \"Colab Run in Colab\n", " \n", "
\n", "\n", " \n", @@ -118,14 +118,10 @@ "\n", "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", "\n", - "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,run.googleapis.com,artifactregistry.googleapis.com,cloudbuild.googleapis.com,cloudresourcemanager.googleapis.com) to enable the following APIs:\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com) to enable the following APIs:\n", "\n", " * BigQuery API\n", " * BigQuery Connection API\n", - " * Cloud Run API\n", - " * Artifact Registry API\n", - " * Cloud Build API\n", - " * Cloud Resource Manager API\n", " * Vertex AI API\n", "\n", "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." @@ -143,9 +139,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Updated property [core/project].\n" + ] + } + ], "source": [ "# set your project ID below\n", "PROJECT_ID = \"\" # @param {type:\"string\"}\n", @@ -166,7 +170,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -232,87 +236,6 @@ "# auth.authenticate_user()" ] }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Connect to Vertex AI\n", - "\n", - "In order to use PaLM2TextGenerator, we will need to set up a [cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from google.cloud import bigquery_connection_v1 as bq_connection\n", - "\n", - "CONN_NAME = \"bqdf-llm\"\n", - "\n", - "client = bq_connection.ConnectionServiceClient()\n", - "new_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}\"\n", - "exists_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", - "cloud_resource_properties = bq_connection.CloudResourceProperties({})\n", - "\n", - "try:\n", - " request = client.get_connection(\n", - " request=bq_connection.GetConnectionRequest(name=exists_conn_parent)\n", - " )\n", - " CONN_SERVICE_ACCOUNT = f\"serviceAccount:{request.cloud_resource.service_account_id}\"\n", - "except Exception:\n", - " connection = bq_connection.types.Connection(\n", - " {\"friendly_name\": CONN_NAME, \"cloud_resource\": cloud_resource_properties}\n", - " )\n", - " request = bq_connection.CreateConnectionRequest(\n", - " {\n", - " \"parent\": new_conn_parent,\n", - " \"connection_id\": CONN_NAME,\n", - " \"connection\": connection,\n", - " }\n", - " )\n", - " response = client.create_connection(request)\n", - " CONN_SERVICE_ACCOUNT = (\n", - " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", - " )\n", - "print(CONN_SERVICE_ACCOUNT)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Set permissions for the service account\n", - "\n", - "The resource connection service account requires certain project-level permissions:\n", - " - `roles/aiplatform.user` and `roles/bigquery.connectionUser`: These roles are required for the connection to create a model definition using the LLM model in Vertex AI ([documentation](https://cloud.google.com/bigquery/docs/generate-text#give_the_service_account_access)).\n", - " - `roles/run.invoker`: This role is required for the connection to have read-only access to Cloud Run services that back custom/remote functions ([documentation](https://cloud.google.com/bigquery/docs/remote-functions#grant_permission_on_function)).\n", - "\n", - "Set these permissions by running the following `gcloud` commands:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'" - ] - }, { "attachments": {}, "cell_type": "markdown", @@ -336,12 +259,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Project Setup" + "BigQuery DataFrames setup" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "id": "R7STCS8xB5d2" }, @@ -353,6 +276,14 @@ "bf.options.bigquery.location = REGION" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -365,7 +296,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "id": "zDSwoBo1CU3G" }, @@ -376,11 +307,101 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "id": "tYDoaKgJChiq" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 9f096761-e3b5-4d58-a9f7-485ced67afca is DONE. 2.3 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ee8fecb1-2e30-407d-9e2e-9e76061da9e7 is DONE. 2.3 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "
\n", - " \n", + " \n", " \"Colab Run in Colab\n", " \n", "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
consumer_complaint_narrative
0I signed a contract as a condition of employme...
1First, I want to disclose that XXXX and XXXX b...
2Frequent calls from Focused Receivables Manage...
3I recently contacted Enhanced Recovery Company...
4This began when I subscribed to XXXX XXXX inte...
\n", + "

5 rows × 1 columns

\n", + "[5 rows x 1 columns in total]" + ], + "text/plain": [ + " consumer_complaint_narrative\n", + "0 I signed a contract as a condition of employme...\n", + "1 First, I want to disclose that XXXX and XXXX b...\n", + "2 Frequent calls from Focused Receivables Manage...\n", + "3 I recently contacted Enhanced Recovery Company...\n", + "4 This began when I subscribed to XXXX XXXX inte...\n", + "\n", + "[5 rows x 1 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "issues_df = input_df[[\"consumer_complaint_narrative\"]].dropna()\n", "issues_df.head(n=5) # View the first five complaints" @@ -391,12 +412,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Download 10000 complaints to use with PaLM2TextEmbeddingGenerator" + "Downsample DataFrame to 10,000 records for model training." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "id": "OltYSUEcsSOW" }, @@ -418,11 +439,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "id": "li38q8FzDDMu" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 52d2e961-7896-497c-8b03-ab7374737679 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from bigframes.ml.llm import PaLM2TextEmbeddingGenerator\n", "\n", @@ -431,11 +465,125 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "id": "cOuSOQ5FDewD" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job d093d51a-8eda-442f-80cd-568cb76e00b3 is DONE. 10.6 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 6419df65-3e96-41a7-a7b5-3d058e18763a is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 917f09ea-c468-4363-a856-b1091e5f775f is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5c9679e7-192c-40b5-a14b-edc0fa113eaa is DONE. 61.5 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", + "
text_embedding
422[-0.012013785541057587, 0.003669967409223318, ...
616[-0.014948881231248379, -0.04672442376613617, ...
833[-0.01951478235423565, -0.027120858430862427, ...
1370[-0.03140445053577423, -0.048797041177749634, ...
1430[-0.02244548313319683, -0.03336532413959503, 0...
\n", + "

5 rows × 1 columns

\n", + "
[5 rows x 1 columns in total]" + ], + "text/plain": [ + " text_embedding\n", + "422 [-0.012013785541057587, 0.003669967409223318, ...\n", + "616 [-0.014948881231248379, -0.04672442376613617, ...\n", + "833 [-0.01951478235423565, -0.027120858430862427, ...\n", + "1370 [-0.03140445053577423, -0.048797041177749634, ...\n", + "1430 [-0.02244548313319683, -0.03336532413959503, 0...\n", + "\n", + "[5 rows x 1 columns]" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Will take ~3 minutes to compute the embeddings\n", "predicted_embeddings = model.predict(downsampled_issues_df)\n", @@ -445,14 +593,263 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "metadata": { "id": "4H_etYfsEOFP" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job ce9cb0f9-4b0d-40a1-81f3-d6e60dd6c684 is DONE. 160.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job aa692a30-5706-46ad-8029-faf2fac66234 is DONE. 72.2 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", + "
consumer_complaint_narrativetext_embedding
2580664Hello, my name is XXXX XXXX, and I am writing ...[0.0003211698785889894, -0.01816680282354355, ...
1806973This is XXXX XXXX and I am submitting this com...[-0.009485247544944286, -0.025846892967820168,...
2055053XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX...[-0.010950954630970955, -0.0249345600605011, 0...
2515231When I reinvestigated my credit report, I real...[-0.009660656563937664, -0.05793113633990288, ...
2633049Checking my credit report XX/XX/2018 with all ...[-0.0022159104701131582, -0.03330004960298538,...
3117273I contacted TransUnion and spoke a credit rep ...[-0.015955328941345215, -0.006488671060651541,...
698814XXXX XXXX XXXX. makes daily calls to me cell c...[0.005397460889071226, -0.01276913657784462, 0...
267826Can we please reopen Case : XXXX? \n", + "\n", + "Wells Farg...[0.004065403249114752, -0.0005381882656365633,...
54019My rights under 15 USC 1681 have been violated...[0.013823015615344048, -0.02010691538453102, 0...
141050To whom it may concern : My personal informati...[0.008104532025754452, -0.01856449618935585, 0...
2962076I have had a CashApp account since last year, ...[-0.0003019514260813594, -0.03750108182430267,...
2481105that some of the information was erroneous. Th...[-0.014868081547319889, -0.0443895161151886, -...
431562I have disputed the referenced accounts to the...[-0.0020524838473647833, -0.04830990731716156,...
1953029On, XX/XX/22, I attempted to complete a transa...[-0.01599179394543171, -0.0074900356121361256,...
2395979Subject : XXXX XXXX XXXX compensation, refund,...[-0.0035950862802565098, -0.014652969315648079...
455524I paid off my mortgage on XX/XX/2019. The comp...[-0.01100730150938034, -0.03495829552412033, 0...
2155924This kind of account is placed as a charged of...[-0.028635455295443535, -0.028604287654161453,...
1069497This is one of many issues I have had with Wel...[0.008871790021657944, -0.028502725064754486, ...
3181689I have disputed this account with MONTEREY FIN...[-0.004721717908978462, -0.03673810139298439, ...
274268Lender is not updating my loan status in the V...[-0.009221495129168034, -0.0289347805082798, 0...
1671305XXXX is a peer to peer lending conmpany that u...[-0.02911308966577053, -0.01850792020559311, -...
886026( DISPUTE CODE - XXXX ) My personal informatio...[-0.007220877334475517, -0.016615957021713257,...
1044431I filed a complaint against PNC this year and ...[0.002848619595170021, -0.035117778927087784, ...
1938481I applied for a modification and was approved....[-0.03114932030439377, -0.0421406552195549, 0....
1987834Ive been Disputting my XXXX XXXX I opened this...[-0.009406660683453083, -0.020967338234186172,...
\n", + "

25 rows × 2 columns

\n", + "
[10000 rows x 2 columns in total]" + ], + "text/plain": [ + " consumer_complaint_narrative \\\n", + "2580664 Hello, my name is XXXX XXXX, and I am writing ... \n", + "1806973 This is XXXX XXXX and I am submitting this com... \n", + "2055053 XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX... \n", + "2515231 When I reinvestigated my credit report, I real... \n", + "2633049 Checking my credit report XX/XX/2018 with all ... \n", + "3117273 I contacted TransUnion and spoke a credit rep ... \n", + "698814 XXXX XXXX XXXX. makes daily calls to me cell c... \n", + "267826 Can we please reopen Case : XXXX? \n", + "\n", + "Wells Farg... \n", + "54019 My rights under 15 USC 1681 have been violated... \n", + "141050 To whom it may concern : My personal informati... \n", + "2962076 I have had a CashApp account since last year, ... \n", + "2481105 that some of the information was erroneous. Th... \n", + "431562 I have disputed the referenced accounts to the... \n", + "1953029 On, XX/XX/22, I attempted to complete a transa... \n", + "2395979 Subject : XXXX XXXX XXXX compensation, refund,... \n", + "455524 I paid off my mortgage on XX/XX/2019. The comp... \n", + "2155924 This kind of account is placed as a charged of... \n", + "1069497 This is one of many issues I have had with Wel... \n", + "3181689 I have disputed this account with MONTEREY FIN... \n", + "274268 Lender is not updating my loan status in the V... \n", + "1671305 XXXX is a peer to peer lending conmpany that u... \n", + "886026 ( DISPUTE CODE - XXXX ) My personal informatio... \n", + "1044431 I filed a complaint against PNC this year and ... \n", + "1938481 I applied for a modification and was approved.... \n", + "1987834 Ive been Disputting my XXXX XXXX I opened this... \n", + "\n", + " text_embedding \n", + "2580664 [0.0003211698785889894, -0.01816680282354355, ... \n", + "1806973 [-0.009485247544944286, -0.025846892967820168,... \n", + "2055053 [-0.010950954630970955, -0.0249345600605011, 0... \n", + "2515231 [-0.009660656563937664, -0.05793113633990288, ... \n", + "2633049 [-0.0022159104701131582, -0.03330004960298538,... \n", + "3117273 [-0.015955328941345215, -0.006488671060651541,... \n", + "698814 [0.005397460889071226, -0.01276913657784462, 0... \n", + "267826 [0.004065403249114752, -0.0005381882656365633,... \n", + "54019 [0.013823015615344048, -0.02010691538453102, 0... \n", + "141050 [0.008104532025754452, -0.01856449618935585, 0... \n", + "2962076 [-0.0003019514260813594, -0.03750108182430267,... \n", + "2481105 [-0.014868081547319889, -0.0443895161151886, -... \n", + "431562 [-0.0020524838473647833, -0.04830990731716156,... \n", + "1953029 [-0.01599179394543171, -0.0074900356121361256,... \n", + "2395979 [-0.0035950862802565098, -0.014652969315648079... \n", + "455524 [-0.01100730150938034, -0.03495829552412033, 0... \n", + "2155924 [-0.028635455295443535, -0.028604287654161453,... \n", + "1069497 [0.008871790021657944, -0.028502725064754486, ... \n", + "3181689 [-0.004721717908978462, -0.03673810139298439, ... \n", + "274268 [-0.009221495129168034, -0.0289347805082798, 0... \n", + "1671305 [-0.02911308966577053, -0.01850792020559311, -... \n", + "886026 [-0.007220877334475517, -0.016615957021713257,... \n", + "1044431 [0.002848619595170021, -0.035117778927087784, ... \n", + "1938481 [-0.03114932030439377, -0.0421406552195549, 0.... \n", + "1987834 [-0.009406660683453083, -0.020967338234186172,... \n", + "...\n", + "\n", + "[10000 rows x 2 columns]" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Join the complaints with their embeddings in the same DataFrame\n", - "combined_df = downsampled_issues_df.join(predicted_embeddings)" + "combined_df = downsampled_issues_df.join(predicted_embeddings, how=\"left\")\n", + "combined_df" ] }, { @@ -470,12 +867,12 @@ "id": "OUZ3NNbzo1Tb" }, "source": [ - "## Step 2: KMeans clustering" + "## Step 2: Create k-means model and predict clusters" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": { "id": "AhNTnEC5FRz2" }, @@ -496,14 +893,152 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": { "id": "6poSxh-fGJF7" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 65eb317d-59f1-4d10-acd1-4b7f3778114c is DONE. 61.7 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 156e445e-cc01-4b30-84cc-ac1c98a69b81 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5befc212-f4a3-4e33-b1b2-01e809acdcbd is DONE. 61.9 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job bd271178-8b8d-45dc-ac57-7f0194d0daac is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job bbfb9cca-622d-4bf5-9fc0-6d9a85287d41 is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a5f30b32-9fb0-42b4-b426-d8484f008bdb is DONE. 160.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CENTROID_ID
4222
6163
8335
13707
14303
\n", + "

5 rows × 1 columns

\n", + "
[5 rows x 1 columns in total]" + ], + "text/plain": [ + " CENTROID_ID\n", + "422 2\n", + "616 3\n", + "833 5\n", + "1370 7\n", + "1430 3\n", + "\n", + "[5 rows x 1 columns]" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", - "cluster_model.fit(combined_df[[\"text_embedding\"]])\n", + "cluster_model.fit(combined_df[\"text_embedding\"])\n", "clustered_result = cluster_model.predict(combined_df[[\"text_embedding\"]])\n", "# Notice the CENTROID_ID column, which is the ID number of the group that\n", "# each complaint belongs to.\n", @@ -512,12 +1047,123 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 7a41196e-ea67-44ac-95a7-7dce620d6d21 is DONE. 320.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8008b482-1a0d-461f-a215-4676d9d918dc is DONE. 72.4 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", + "
consumer_complaint_narrativetext_embeddingCENTROID_ID
2580664Hello, my name is XXXX XXXX, and I am writing ...[0.0003211698785889894, -0.01816680282354355, ...2
1806973This is XXXX XXXX and I am submitting this com...[-0.009485247544944286, -0.025846892967820168,...5
2055053XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX...[-0.010950954630970955, -0.0249345600605011, 0...3
2515231When I reinvestigated my credit report, I real...[-0.009660656563937664, -0.05793113633990288, ...5
2633049Checking my credit report XX/XX/2018 with all ...[-0.0022159104701131582, -0.03330004960298538,...3
\n", + "

5 rows × 3 columns

\n", + "
[5 rows x 3 columns in total]" + ], + "text/plain": [ + " consumer_complaint_narrative \\\n", + "2580664 Hello, my name is XXXX XXXX, and I am writing ... \n", + "1806973 This is XXXX XXXX and I am submitting this com... \n", + "2055053 XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX... \n", + "2515231 When I reinvestigated my credit report, I real... \n", + "2633049 Checking my credit report XX/XX/2018 with all ... \n", + "\n", + " text_embedding CENTROID_ID \n", + "2580664 [0.0003211698785889894, -0.01816680282354355, ... 2 \n", + "1806973 [-0.009485247544944286, -0.025846892967820168,... 5 \n", + "2055053 [-0.010950954630970955, -0.0249345600605011, 0... 3 \n", + "2515231 [-0.009660656563937664, -0.05793113633990288, ... 5 \n", + "2633049 [-0.0022159104701131582, -0.03330004960298538,... 3 \n", + "\n", + "[5 rows x 3 columns]" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Join the group number to the complaints and their text embeddings\n", - "combined_clustered_result = combined_df.join(clustered_result)" + "combined_clustered_result = combined_df.join(clustered_result)\n", + "\n", + "combined_clustered_result.head(n=5)" ] }, { @@ -535,7 +1181,7 @@ "id": "21rNsFMHo8hO" }, "source": [ - "## Step 3: Summarize the complaints" + "## Step 3: Use PaLM2 LLM model to summarize complaint clusters" ] }, { @@ -548,11 +1194,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": { "id": "2E7wXM_jGqo6" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 50c7c0dd-94a2-494e-a37f-6a838a518f6c is DONE. 11.0 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d96c847f-c292-4804-bd05-fd643c41c7a5 is DONE. 11.0 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Using bigframes, with syntax identical to pandas,\n", "# filter out the first and second groups\n", @@ -569,11 +1240,100 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "metadata": { "id": "ZNDiueI9IP5e" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "comment list 1:\n", + "1. XXXX is a peer to peer lending conmpany that uses borrowers crypto to collateralize loans from investors ( like myself ). I've been investing with them for almost XXXX years and currently have {$240000.00} tied up in lending products with XXXX. \n", + "As of XXXX days ago we received an email saying all business operations have been ceased and no withdrawals or deposits will be allowed. They said they'll update customers within 10 days, but no one can reach anyone at the company to find out any more details as they are not answering calls nor returning emails. It also appears the company has scrubbed its XXXX page and the XXXX pages of top executives. \n", + "\n", + "All collateral and client 's investment funds are supposedly held at or processed through XXXX XXXX XXXX ( registered SEC company ). XXXX XXXX keeps telling us to contact XXXX and won't give us any information, so we have no way to find out what's happening with our funds/collateral or if everything is gone. We have a XXXX channel up where people are gathering evidence, documentation, etc. This is probably the best place to start to get a broad view of what's happening. Details below. \n", + "\n", + "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX CONST LLC ( Business ID : XXXX ) FoXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 'Cease of Operations ' email received by all investors XXXX XX/XX/2022 at XXXX : \" Dear XXXX Users, Given the collapses of several cryptocurrencies so far this year and the rapidly deteriorating market conditions that have been prompting heavy withdrawals across all XXXX lending and XXXX exchange platforms recently, we are sad to inform you that we are unable to continue to operate our business as usual. As such, we are limiting our business activities, including pausing user withdrawals as allowed under our Terms of XXXX. \n", + "No deposit or investment request will be processed at this time. \n", + "\n", + "Our team is working diligently towards our objective of maximizing value for all of our Users, and our top priority continues to be to protect your interests. As we explore all options available to us, we will provide updates to you as we go. \n", + "\n", + "We hope to communicate with you within the next XXXX business days on the next steps to address the situation. We appreciate your patience in this trying time. \n", + "\n", + "Sincerely yoursXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", + "2. Submitted XX/XX/XXXX\n", + "Typed XX/XX/XXXX:\n", + "\n", + "XX/XX/XXXX\n", + "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX, XXXX XXXX\n", + "PH:. XXXX\n", + "PH: XXXX\n", + "EM:\n", + "XXXX\n", + "XXXX\n", + "XXXX XXXX \n", + "XXXX XXXX\n", + "Date of Birth XX/XX/XXXX\n", + "SS#: XXXX\n", + "TO:\n", + "* Consumer Financial Protection Brueau\n", + "* Department of Veteran Affairs, Office of the Inspector General\n", + "My name is XXXX XXXX XXXX, I've received more than one email from Discover Card in my XXXX XXXX, past emails from Discover Card were unautherized deletions.\n", + "From: Discover Card XXXX\n", + "To: You XXXX\n", + "Date: XX/XX/XXXX, XXXX XXXX XXXX From: Discover Card XXXX>\n", + "To Recipient \n", + "Date Mon, XX/XX/XXXX XXXX XXXX\n", + "I dont and havent ever had a Discover Checking, Savings, Business Accounts nor Loans of any kind through any Bank called Discover. The 1st time I was contacted by Discover Card I resided alone from XX/XX/XXXX to XX/XX/XXXXat XXXX XXXX XXXX at XXXX XXXX XXXX XXXX XXXX in XXXX, XXXX years prior to me moving here to XXXX, XXXX in XX/XX/XXXX. When \n", + "\n", + "\n", + "Discover Card had 1st contacted me in XXXX, XXXX it was associated with my XXXX XXXX XXXX website related online Merchants Account. Not once have I ever applied for or had any Website Merchant Accounts here in XXXX; I only applied for online online Merchant Accounts associated with my XXXX related Accounts I purchased while residing in XXXX, XXXX. Some of my website related information was stolen both in XXXX, XXXX and here in XXXX along with my other property that hasn't been returned to me. I don't and haven't ever had any XXXX XXXX related Agreements,Contracts or Credit Cards offered to Veterans associated with ones businesses. Nor have I ever applied for or had a Business License or Business Permit in any City or State inspite of my diverse interest. Not once have I ever allowed another be it an Paralegal, Payee, Attorney, Employers, Landlords, Veteran Organizations including Vocational Rehabilitation Programs, XXXX( XXXX XXXX XXXX, XXXX XXXX, Entertainment Companies, Banks, Celebrity Personal Assistant Agencies or Celebs, Shelters, Charities, HUD, Housing Arthority, Department of Veteran Affairs, Military, Law Enforcement or anyone else nor their employess to sign any business related Agreements or Contracts on my behalf; not even my family members or friends. \n", + "None of my XXXX XXXX attempts were associated with my Employers, Department of Veteran Affairs,Vocational Rehabilitation Programs Military, Landlords, HUD( Housing Authority),Friends, Family nor did I ever sign related Agreements or Contracts with them. Not once had I ever provided anyone the passwords to be able to sign into my accounts rather were aware of my accounts or not. Yes, my desktop computer that was stolen along with my other property XX/XX/XXXX was registered with my Online Merchant Account. I had paid for my Merchant related Accounts through my same XXXX XXXX XXXX Account I purchased both of my XXXX XXXX XXXX related accounts through. That was 1st once during the Summer of XX/XX/XXXX and 2nd my related website months later, while I resided in XXXX XXXX and I worked for XXXX. I never offered nor did I ever sign any business Contracts or Agreements with XXXX nor my Landlord or their staff associted with any of my online websites or Merchant Accounts. My XXXX XXXX XXXX Compensation was deposited into both of my XXXX XXXX XXXX Accounts at that time. My account was changed during the Summer of XX/XX/XXXXbecause of theft of my Bank Card. None of my Checking,Savings, past Credit Cards or Business related were shared accounts in which others were allowed to \n", + "use to make purchases. I had written checks from my XXXX XXXX XXXX account to pay for my XXXX XXXX XXXX XXXX on the XXXX XXXX here in XXXX in XX/XX/XXXX before it's name changed to XXXX XXXX. Prior to me using my same account open a Checking account in person at XXXX XXXX before it's name was changed to XXXX XXXX. Where my XXXX XXXX XXXX XXXX has been deposited since that time. I had used my XXXX XXXX Checking to pay for my XXXX XXXX XXXX XXXX both before theft of my property XX/XX/XXXX and that was also prior to the theft of my property from my XXXX XXXX XXXX XXXX in XX/XX/XXXX.\n", + "I've stated this many times:\n", + "I paid for my 1st XXXX XXXX XXXX Membership while employed at XXXX using my XXXX XXXX XXXX account XXXX my XXXX XXXX XXXX XXXX was also deposited. That was changed to XXXX because I didn't receive my 1st XXXX XXXX XXXX Card the bank sent to XXXX XXXX residence on XXXX XXXX in XX/XX/XXXX while I was there. In which both my XXXX salary and XXXX XXXX XXXX XXXX were deposited into my account, no money from XXXX XXXX nor anyone else that was at that residence was given to nor were any of my children there. Nor did XXXX or any other person at that residence ever give me my missing Bank Card not even after I moved out and stayed a month at XXXX XXXX XXXX using my replacement card to pay for my Hotel room. Which is the same account I used to pay for XXXX XXXX Membership, XXXX XXXX XXXX, XXXX XXXX Membership fees, and various online Merchant Account activation related fees.\n", + "* XXXX XXXX XXXX.\n", + "XXXX XXXX XXXX XXXX. Membership\n", + "\n", + "# XXXX\n", + "* XXXX XXXX Membership\n", + "# XXXX\n", + "* Total Merchant Services XXXX and XXXX.\n", + "* XXXX XXXX XXXX XXXX XXXX\n", + "* XXXX XXXX changed my $XXXX a month fees to my XXXX XXXX XXXX account #XXXX.\n", + "XX/XX/XXXX - XX/XX/XXXX XXXX XXXX, XXXX.\n", + "\n", + "Rep: XXXX XXXX XXXX, Fl \n", + "XXXX\n", + "XXXX Website \n", + "XXXX\n", + "Software and website owner, I performed Internet advertising and marketing, to promote this software and website. I worked and XXXX from my home XXXX XXXX XXXX XXXX XXXX , XXXX. I purchased XXXX XXXX XXXX-Software Electronic Book CD and was given a website to promote the software on the internet. The XXXX was given a copy of my website owner certificate document submitted to me when I purchased the software marketing program as well copies of my other school transcripts in addition to XXXX XXXX XXXX for example. XXXX, represented the first initials of my children's names. I wasn't ever paid and I'm still owed the money. Nor did my marketing program have anything to do with any schools, college nor university programs nor did I ever offer or sign any agreement to include it such. Nor did my XXXX XXXX XXXX have anything to do with any other employers, Department of Family and Children, Military, Veteran Organizations or Food Stamp programs, Section 8 nor Indianapolis Housing Authority for example; only me.\n", + "Thank you,\n", + "XXXX XXXX\n", + "3. ACCORDING TO 15 U.S. CODE 6803-DISCLOSURE OF INSTITUTION PRIVACY POLICY, AND ACCORDING TO U.S. CODE 6802- OBLIGATIONS WITH RESPECT TO DISCLOSURES OF PERSONAL INFORMATION. ( b ) OPT OUT ( 1 ) IN GENERAL A FINANCIAL INSTITUTION MAY NOT DISCLOSE NONPUBLIC PERSONAL INFORMATION TO A NONAFFILIATED THIRD PARTY ( TRANSUNION, XXXX, AND XXXX. ) UNLESS- ( A ) SUCH FINANCIAL INSTITUTION CLEARLY AND CONSPICUOUSLY DISCLOSES TO THE CONSUMER, IN WRITING OR IN ELECTRONIC FORM OR OTHER FORM PERMITTED BY THE REGULATIONS PRESCRIBED UNDER SECTION 6804 OF THIS TITLE. ALSO ACCORDING TO THE \" XXXX ACT '', FINANCIAL INSTITUTIONS MUST TELL THEIR CUSTOMERS ABOUT THEIR INFORMATION-SHARING PRACTICES AND EXPLAIN TO CUSTOMERS THEIR RIGHT TO \" OPT OUT '' IF THEY DON'T WANT THEIR INFORMATION SHARED WITH CERTAIN THIRD PARTIES. UNDER THE FDCPA, A COLLECTOR MUST PROVIDE YOU WITH INFORMATION ABOUT THE DEBT IN ITS INITIAL COMMUNICATION OR WITHIN FIVE DAYS AFTER THE INITIAL COMMUNICATION. ALSO, THE FDCPA STATES, \" YOU CAN NOT ATTEMPT TO COLLECT AN DEBT WHILE A PERSON ( THE CONSUMER ) SUPRESS VALIDATION. TRANSUNION, XXXX, XXXX, AND THE ACCOUNTS LISTED BELOW HAVE CLEARLY VIOLATED MY RIGHTS : XXXX ACCOUNT # XXXX, XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX ACCOUNT # XXXX, XXXX XXXX XXXX XXXX ACCOUNT # XXXX, AND XXXX ACCOUNT # XXXX. FAILURE TO RESPOND SATISFACTORILY WITH DELETIONS OF ALL THE ABOVE ACCOUNTS WILL RESULT IN LEGAL ACTIONS BEING TAKEN AGAINST, TRANSUNION, XXXX, XXXX, WHICH I'LL BE SEEKING A {$1000.00} PER VIOLATION FOR DEFAMATION OF CHARACTER ( PER SE ) NEGLIGENT ENABLEMENT OF IDENTITY FRAUD. 15 USC 1681 VIOLATIONS FOR WILLFUL NONCOMPLIANCE-616 CIVIL LIABILITY FOR WILLFUL NONCOPLIANCE. THIS IS THE THIRD TIME I'VE SUBMITTED A COMPLAINT, AND THE REPONSE I GET IS \" YOU CAN NOT LOCATE MY CREDIT REPORT! '' THIS IS CLEARLY NEGLIGENCE.\n", + "4. I do not know how this works, but I need it done or somehow corrected. My name is XXXX XXXX, XXXX XXXX XXXX XXXX TN XXXXMy SS XXXX DOB XXXX. I had some issues with my income being affected by the COVID-19PANDEMICSHUTDOWN. I was under the 1 CARESAct, Pub. L. 116-136, section 4021, codified at FCRAsection 623 ( a ) ( 1 ) ( F ) ( i ) ( I ), 15 U.S.C.1681s- 2 ( a ) ( 1 ) ( F ) ( i ) ( I ). I am requesting some accommodations so I care to protect the integrity of my credit file. US DEPT OF ED / XXXX # XXXX, # XXXX accounts are reporting on XXXX, XXXX The was 30,60, 90 DAYS LATEsince requested assistance due to the pandemic. I found a few accounts that I have never done any business with these companies and the accounts do not belong on my report : XXXX XXXX # XXXX, XXXX XXXX XXXX XXXX # XXXX. \n", + "\n", + "I have some issues with the misspelling of my name, my correct spelling is XXXX XXXX. Please remove any other variation of my name they are not correct. The following addresses do not belong to me please delete them : XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXXSC, XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", + "5. I want to know if this is even legal?! How can they disclose information without knowing its a correct email?!\n", + "\n", + "comment list 2:\n", + "1. Hello, my name is XXXX XXXX, and I am writing to delete the following information in my file. The items I need deleted are listed in the report. I am a victim of identity theft and did not make the charge. I ask that the items be deleted to correct my credit report. I reported the theft of my identity to the Federal Trade Commission and I also have enclosed copies of the Federal Trade Commissions Identity Theft Affidavit. Please delete the items as soon as possible. The accounts are being reported currently open and the accounts need to be closed. \n", + "XXXX account number XXXX opened on XX/XX/2022 for the amount {$530.00} XXXX XXXX XXXX account number XXXX opened on XX/XX/2022 for the amount of {$140.00} The accounts are being reported currently open and need to be closed immediately. \n", + "Based on, 15 U.S. Code 1681c2 a consumer reporting agency shall block the reporting of any information in the file of a consumer that the consumer identifies as information that resulted from an alleged identity theft, not later than 4 business days after the date of receipt. This account should not be furnished on my consumer report. As a consumer I am demanding the deletion of the accounts listed IMMEDIATELY.\n", + "2. To whom it may concern : My personal information was breach in the internet as result accounts had been open in my name, I was advise to fill out an Id theft report to help me deal with this situation, I have listed each one of the accounts that do not belong to me. This is my second request to remove unverified items in my report, but XXXX keep rposting these account with out providing any type of original document as the FCRA provide, you need to provide me with original documents or remove these account immediately.\n", + "3. Ive been Disputting my XXXX XXXX I opened this account and someone got my information and used my card, I contacted XXXX over and over, they removed the negative reporting from my XXXX report but still reporting it negative on my XXXX and Expean this is very unfair to me because Im a victim of identity theft\n", + "4. Today, XX/XX/2021, I received three items in the mail, one envelope containing an unsolicited debit card from Navy Federal credit Union and the other two, with a letter each describing The Important Rights on two accounts should these accounts become delinquent under New York law. \n", + "\n", + "First of all, I never applied for these accounts with Navy Federal, not have I authorized anyone to do so on my behalf. I immediately contacted Navy Federal via phone and was told I was most likely a victim of identity theft and that I should monitor my credit and use a credit monitoring service. I was also asked for my email and mailing information in order to receive a letter from them regarding this issue. \n", + "\n", + "My main concern is having someone using my identity to illegally open bank accounts and commit fraud, destroying my credit and finances in the process. This bank is in another state from where I reside. I have not lived in Virginia nor do I intend to do so in the foreseeable future.\n", + "5. My personal information ( including my SSN, Drivers License Info, Addresses, and more ) was stolen from a hacking, and Equifax did n't tell the public about the hack until more than a month after the hacking. During this time, three Equifax executives were caught inside trading. It really shows how Equifax cares about other people!\n", + "\n" + ] + } + ], "source": [ "# Build plain-text prompts to send to PaLM 2. Use only 5 complaints from each group.\n", "prompt1 = 'comment list 1:\\n'\n", @@ -592,11 +1352,100 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": { "id": "BfHGJLirzSvH" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Please highlight the most obvious difference betweenthe two lists of comments:\n", + "comment list 1:\n", + "1. XXXX is a peer to peer lending conmpany that uses borrowers crypto to collateralize loans from investors ( like myself ). I've been investing with them for almost XXXX years and currently have {$240000.00} tied up in lending products with XXXX. \n", + "As of XXXX days ago we received an email saying all business operations have been ceased and no withdrawals or deposits will be allowed. They said they'll update customers within 10 days, but no one can reach anyone at the company to find out any more details as they are not answering calls nor returning emails. It also appears the company has scrubbed its XXXX page and the XXXX pages of top executives. \n", + "\n", + "All collateral and client 's investment funds are supposedly held at or processed through XXXX XXXX XXXX ( registered SEC company ). XXXX XXXX keeps telling us to contact XXXX and won't give us any information, so we have no way to find out what's happening with our funds/collateral or if everything is gone. We have a XXXX channel up where people are gathering evidence, documentation, etc. This is probably the best place to start to get a broad view of what's happening. Details below. \n", + "\n", + "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX CONST LLC ( Business ID : XXXX ) FoXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 'Cease of Operations ' email received by all investors XXXX XX/XX/2022 at XXXX : \" Dear XXXX Users, Given the collapses of several cryptocurrencies so far this year and the rapidly deteriorating market conditions that have been prompting heavy withdrawals across all XXXX lending and XXXX exchange platforms recently, we are sad to inform you that we are unable to continue to operate our business as usual. As such, we are limiting our business activities, including pausing user withdrawals as allowed under our Terms of XXXX. \n", + "No deposit or investment request will be processed at this time. \n", + "\n", + "Our team is working diligently towards our objective of maximizing value for all of our Users, and our top priority continues to be to protect your interests. As we explore all options available to us, we will provide updates to you as we go. \n", + "\n", + "We hope to communicate with you within the next XXXX business days on the next steps to address the situation. We appreciate your patience in this trying time. \n", + "\n", + "Sincerely yoursXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", + "2. Submitted XX/XX/XXXX\n", + "Typed XX/XX/XXXX:\n", + "\n", + "XX/XX/XXXX\n", + "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX, XXXX XXXX\n", + "PH:. XXXX\n", + "PH: XXXX\n", + "EM:\n", + "XXXX\n", + "XXXX\n", + "XXXX XXXX \n", + "XXXX XXXX\n", + "Date of Birth XX/XX/XXXX\n", + "SS#: XXXX\n", + "TO:\n", + "* Consumer Financial Protection Brueau\n", + "* Department of Veteran Affairs, Office of the Inspector General\n", + "My name is XXXX XXXX XXXX, I've received more than one email from Discover Card in my XXXX XXXX, past emails from Discover Card were unautherized deletions.\n", + "From: Discover Card XXXX\n", + "To: You XXXX\n", + "Date: XX/XX/XXXX, XXXX XXXX XXXX From: Discover Card XXXX>\n", + "To Recipient \n", + "Date Mon, XX/XX/XXXX XXXX XXXX\n", + "I dont and havent ever had a Discover Checking, Savings, Business Accounts nor Loans of any kind through any Bank called Discover. The 1st time I was contacted by Discover Card I resided alone from XX/XX/XXXX to XX/XX/XXXXat XXXX XXXX XXXX at XXXX XXXX XXXX XXXX XXXX in XXXX, XXXX years prior to me moving here to XXXX, XXXX in XX/XX/XXXX. When \n", + "\n", + "\n", + "Discover Card had 1st contacted me in XXXX, XXXX it was associated with my XXXX XXXX XXXX website related online Merchants Account. Not once have I ever applied for or had any Website Merchant Accounts here in XXXX; I only applied for online online Merchant Accounts associated with my XXXX related Accounts I purchased while residing in XXXX, XXXX. Some of my website related information was stolen both in XXXX, XXXX and here in XXXX along with my other property that hasn't been returned to me. I don't and haven't ever had any XXXX XXXX related Agreements,Contracts or Credit Cards offered to Veterans associated with ones businesses. Nor have I ever applied for or had a Business License or Business Permit in any City or State inspite of my diverse interest. Not once have I ever allowed another be it an Paralegal, Payee, Attorney, Employers, Landlords, Veteran Organizations including Vocational Rehabilitation Programs, XXXX( XXXX XXXX XXXX, XXXX XXXX, Entertainment Companies, Banks, Celebrity Personal Assistant Agencies or Celebs, Shelters, Charities, HUD, Housing Arthority, Department of Veteran Affairs, Military, Law Enforcement or anyone else nor their employess to sign any business related Agreements or Contracts on my behalf; not even my family members or friends. \n", + "None of my XXXX XXXX attempts were associated with my Employers, Department of Veteran Affairs,Vocational Rehabilitation Programs Military, Landlords, HUD( Housing Authority),Friends, Family nor did I ever sign related Agreements or Contracts with them. Not once had I ever provided anyone the passwords to be able to sign into my accounts rather were aware of my accounts or not. Yes, my desktop computer that was stolen along with my other property XX/XX/XXXX was registered with my Online Merchant Account. I had paid for my Merchant related Accounts through my same XXXX XXXX XXXX Account I purchased both of my XXXX XXXX XXXX related accounts through. That was 1st once during the Summer of XX/XX/XXXX and 2nd my related website months later, while I resided in XXXX XXXX and I worked for XXXX. I never offered nor did I ever sign any business Contracts or Agreements with XXXX nor my Landlord or their staff associted with any of my online websites or Merchant Accounts. My XXXX XXXX XXXX Compensation was deposited into both of my XXXX XXXX XXXX Accounts at that time. My account was changed during the Summer of XX/XX/XXXXbecause of theft of my Bank Card. None of my Checking,Savings, past Credit Cards or Business related were shared accounts in which others were allowed to \n", + "use to make purchases. I had written checks from my XXXX XXXX XXXX account to pay for my XXXX XXXX XXXX XXXX on the XXXX XXXX here in XXXX in XX/XX/XXXX before it's name changed to XXXX XXXX. Prior to me using my same account open a Checking account in person at XXXX XXXX before it's name was changed to XXXX XXXX. Where my XXXX XXXX XXXX XXXX has been deposited since that time. I had used my XXXX XXXX Checking to pay for my XXXX XXXX XXXX XXXX both before theft of my property XX/XX/XXXX and that was also prior to the theft of my property from my XXXX XXXX XXXX XXXX in XX/XX/XXXX.\n", + "I've stated this many times:\n", + "I paid for my 1st XXXX XXXX XXXX Membership while employed at XXXX using my XXXX XXXX XXXX account XXXX my XXXX XXXX XXXX XXXX was also deposited. That was changed to XXXX because I didn't receive my 1st XXXX XXXX XXXX Card the bank sent to XXXX XXXX residence on XXXX XXXX in XX/XX/XXXX while I was there. In which both my XXXX salary and XXXX XXXX XXXX XXXX were deposited into my account, no money from XXXX XXXX nor anyone else that was at that residence was given to nor were any of my children there. Nor did XXXX or any other person at that residence ever give me my missing Bank Card not even after I moved out and stayed a month at XXXX XXXX XXXX using my replacement card to pay for my Hotel room. Which is the same account I used to pay for XXXX XXXX Membership, XXXX XXXX XXXX, XXXX XXXX Membership fees, and various online Merchant Account activation related fees.\n", + "* XXXX XXXX XXXX.\n", + "XXXX XXXX XXXX XXXX. Membership\n", + "\n", + "# XXXX\n", + "* XXXX XXXX Membership\n", + "# XXXX\n", + "* Total Merchant Services XXXX and XXXX.\n", + "* XXXX XXXX XXXX XXXX XXXX\n", + "* XXXX XXXX changed my $XXXX a month fees to my XXXX XXXX XXXX account #XXXX.\n", + "XX/XX/XXXX - XX/XX/XXXX XXXX XXXX, XXXX.\n", + "\n", + "Rep: XXXX XXXX XXXX, Fl \n", + "XXXX\n", + "XXXX Website \n", + "XXXX\n", + "Software and website owner, I performed Internet advertising and marketing, to promote this software and website. I worked and XXXX from my home XXXX XXXX XXXX XXXX XXXX , XXXX. I purchased XXXX XXXX XXXX-Software Electronic Book CD and was given a website to promote the software on the internet. The XXXX was given a copy of my website owner certificate document submitted to me when I purchased the software marketing program as well copies of my other school transcripts in addition to XXXX XXXX XXXX for example. XXXX, represented the first initials of my children's names. I wasn't ever paid and I'm still owed the money. Nor did my marketing program have anything to do with any schools, college nor university programs nor did I ever offer or sign any agreement to include it such. Nor did my XXXX XXXX XXXX have anything to do with any other employers, Department of Family and Children, Military, Veteran Organizations or Food Stamp programs, Section 8 nor Indianapolis Housing Authority for example; only me.\n", + "Thank you,\n", + "XXXX XXXX\n", + "3. ACCORDING TO 15 U.S. CODE 6803-DISCLOSURE OF INSTITUTION PRIVACY POLICY, AND ACCORDING TO U.S. CODE 6802- OBLIGATIONS WITH RESPECT TO DISCLOSURES OF PERSONAL INFORMATION. ( b ) OPT OUT ( 1 ) IN GENERAL A FINANCIAL INSTITUTION MAY NOT DISCLOSE NONPUBLIC PERSONAL INFORMATION TO A NONAFFILIATED THIRD PARTY ( TRANSUNION, XXXX, AND XXXX. ) UNLESS- ( A ) SUCH FINANCIAL INSTITUTION CLEARLY AND CONSPICUOUSLY DISCLOSES TO THE CONSUMER, IN WRITING OR IN ELECTRONIC FORM OR OTHER FORM PERMITTED BY THE REGULATIONS PRESCRIBED UNDER SECTION 6804 OF THIS TITLE. ALSO ACCORDING TO THE \" XXXX ACT '', FINANCIAL INSTITUTIONS MUST TELL THEIR CUSTOMERS ABOUT THEIR INFORMATION-SHARING PRACTICES AND EXPLAIN TO CUSTOMERS THEIR RIGHT TO \" OPT OUT '' IF THEY DON'T WANT THEIR INFORMATION SHARED WITH CERTAIN THIRD PARTIES. UNDER THE FDCPA, A COLLECTOR MUST PROVIDE YOU WITH INFORMATION ABOUT THE DEBT IN ITS INITIAL COMMUNICATION OR WITHIN FIVE DAYS AFTER THE INITIAL COMMUNICATION. ALSO, THE FDCPA STATES, \" YOU CAN NOT ATTEMPT TO COLLECT AN DEBT WHILE A PERSON ( THE CONSUMER ) SUPRESS VALIDATION. TRANSUNION, XXXX, XXXX, AND THE ACCOUNTS LISTED BELOW HAVE CLEARLY VIOLATED MY RIGHTS : XXXX ACCOUNT # XXXX, XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX ACCOUNT # XXXX, XXXX XXXX XXXX XXXX ACCOUNT # XXXX, AND XXXX ACCOUNT # XXXX. FAILURE TO RESPOND SATISFACTORILY WITH DELETIONS OF ALL THE ABOVE ACCOUNTS WILL RESULT IN LEGAL ACTIONS BEING TAKEN AGAINST, TRANSUNION, XXXX, XXXX, WHICH I'LL BE SEEKING A {$1000.00} PER VIOLATION FOR DEFAMATION OF CHARACTER ( PER SE ) NEGLIGENT ENABLEMENT OF IDENTITY FRAUD. 15 USC 1681 VIOLATIONS FOR WILLFUL NONCOMPLIANCE-616 CIVIL LIABILITY FOR WILLFUL NONCOPLIANCE. THIS IS THE THIRD TIME I'VE SUBMITTED A COMPLAINT, AND THE REPONSE I GET IS \" YOU CAN NOT LOCATE MY CREDIT REPORT! '' THIS IS CLEARLY NEGLIGENCE.\n", + "4. I do not know how this works, but I need it done or somehow corrected. My name is XXXX XXXX, XXXX XXXX XXXX XXXX TN XXXXMy SS XXXX DOB XXXX. I had some issues with my income being affected by the COVID-19PANDEMICSHUTDOWN. I was under the 1 CARESAct, Pub. L. 116-136, section 4021, codified at FCRAsection 623 ( a ) ( 1 ) ( F ) ( i ) ( I ), 15 U.S.C.1681s- 2 ( a ) ( 1 ) ( F ) ( i ) ( I ). I am requesting some accommodations so I care to protect the integrity of my credit file. US DEPT OF ED / XXXX # XXXX, # XXXX accounts are reporting on XXXX, XXXX The was 30,60, 90 DAYS LATEsince requested assistance due to the pandemic. I found a few accounts that I have never done any business with these companies and the accounts do not belong on my report : XXXX XXXX # XXXX, XXXX XXXX XXXX XXXX # XXXX. \n", + "\n", + "I have some issues with the misspelling of my name, my correct spelling is XXXX XXXX. Please remove any other variation of my name they are not correct. The following addresses do not belong to me please delete them : XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXXSC, XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", + "5. I want to know if this is even legal?! How can they disclose information without knowing its a correct email?!\n", + "comment list 2:\n", + "1. Hello, my name is XXXX XXXX, and I am writing to delete the following information in my file. The items I need deleted are listed in the report. I am a victim of identity theft and did not make the charge. I ask that the items be deleted to correct my credit report. I reported the theft of my identity to the Federal Trade Commission and I also have enclosed copies of the Federal Trade Commissions Identity Theft Affidavit. Please delete the items as soon as possible. The accounts are being reported currently open and the accounts need to be closed. \n", + "XXXX account number XXXX opened on XX/XX/2022 for the amount {$530.00} XXXX XXXX XXXX account number XXXX opened on XX/XX/2022 for the amount of {$140.00} The accounts are being reported currently open and need to be closed immediately. \n", + "Based on, 15 U.S. Code 1681c2 a consumer reporting agency shall block the reporting of any information in the file of a consumer that the consumer identifies as information that resulted from an alleged identity theft, not later than 4 business days after the date of receipt. This account should not be furnished on my consumer report. As a consumer I am demanding the deletion of the accounts listed IMMEDIATELY.\n", + "2. To whom it may concern : My personal information was breach in the internet as result accounts had been open in my name, I was advise to fill out an Id theft report to help me deal with this situation, I have listed each one of the accounts that do not belong to me. This is my second request to remove unverified items in my report, but XXXX keep rposting these account with out providing any type of original document as the FCRA provide, you need to provide me with original documents or remove these account immediately.\n", + "3. Ive been Disputting my XXXX XXXX I opened this account and someone got my information and used my card, I contacted XXXX over and over, they removed the negative reporting from my XXXX report but still reporting it negative on my XXXX and Expean this is very unfair to me because Im a victim of identity theft\n", + "4. Today, XX/XX/2021, I received three items in the mail, one envelope containing an unsolicited debit card from Navy Federal credit Union and the other two, with a letter each describing The Important Rights on two accounts should these accounts become delinquent under New York law. \n", + "\n", + "First of all, I never applied for these accounts with Navy Federal, not have I authorized anyone to do so on my behalf. I immediately contacted Navy Federal via phone and was told I was most likely a victim of identity theft and that I should monitor my credit and use a credit monitoring service. I was also asked for my email and mailing information in order to receive a letter from them regarding this issue. \n", + "\n", + "My main concern is having someone using my identity to illegally open bank accounts and commit fraud, destroying my credit and finances in the process. This bank is in another state from where I reside. I have not lived in Virginia nor do I intend to do so in the foreseeable future.\n", + "5. My personal information ( including my SSN, Drivers License Info, Addresses, and more ) was stolen from a hacking, and Equifax did n't tell the public about the hack until more than a month after the hacking. During this time, three Equifax executives were caught inside trading. It really shows how Equifax cares about other people!\n", + "\n" + ] + } + ], "source": [ "# The plain English request we will make of PaLM 2\n", "prompt = (\n", @@ -616,22 +1465,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "metadata": { "id": "mL5P0_3X04dE" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 66e3af22-91cb-400a-92c3-69e7cd12ee01 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from bigframes.ml.llm import PaLM2TextGenerator\n", "\n", +<<<<<<< HEAD + "q_a_model = PaLM2TextGenerator()" +======= + "# Create a BigQuery Cloud resource connection\n", + "CONN_NAME = \"bqdf-llm\"\n", "session = bf.get_global_session()\n", + "\n", "connection = f\"{PROJECT_ID}.{REGION}.{CONN_NAME}\"\n", "q_a_model = PaLM2TextGenerator(session=session, connection_name=connection)" +>>>>>>> origin/lmm-kmeans-notebook ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "metadata": { "id": "ICWHsqAW1FNk" }, @@ -643,11 +1512,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "metadata": { "id": "gB7e1LXU1pst" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 653add17-29be-408c-8882-064217f8556e is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8fd16954-853a-45fd-80bc-65b1242429e2 is DONE. 8 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d9929bcb-26ce-4844-b68e-f4a980b90ede is DONE. 171 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "' The first comment list is about people complaining about companies or services, while the second comment list is about people reporting identity theft or fraud.'" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Send the request for PaLM 2 to generate a response to our prompt\n", "major_difference = q_a_model.predict(df)\n", @@ -662,6 +1578,21 @@ "source": [ "We now see PaLM2TextGenerator's characterization of the different comment groups. Thanks for using BigQuery DataFrames!" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Summary and next steps\n", + "\n", +<<<<<<< HEAD + "You've used the ML and LLM capabilities of BigQuery DataFrames to help analyze and understand a large dataset of unstructured feedback.\n", +======= + "You've used BigQuery DataFrames' integration with LLM models (`bigframes.ml.llm`) to generate code samples, and have tranformed LLM output by creating and using a custom function in BigQuery DataFrames.\n", +>>>>>>> origin/lmm-kmeans-notebook + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] } ], "metadata": { @@ -682,7 +1613,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.10.13" } }, "nbformat": 4, From 416171a70d91d4a6b71622ba72685147ab7d6186 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:04:18 -0800 Subject: [PATCH 166/921] feat!: model.predict returns all the columns (#204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/cluster.py | 4 +- bigframes/ml/decomposition.py | 9 +- bigframes/ml/ensemble.py | 49 +- bigframes/ml/forecasting.py | 9 +- bigframes/ml/imported.py | 22 +- bigframes/ml/linear_model.py | 24 +- bigframes/ml/llm.py | 17 +- .../getting_started/ml_fundamentals.ipynb | 3586 ++++++++++------- .../sklearn_linear_regression.ipynb | 1192 +++--- tests/system/large/ml/test_cluster.py | 4 +- tests/system/large/ml/test_ensemble.py | 2 +- tests/system/large/ml/test_pipeline.py | 4 +- tests/system/small/ml/test_cluster.py | 4 +- tests/system/small/ml/test_ensemble.py | 20 +- tests/system/small/ml/test_forecasting.py | 4 +- tests/system/small/ml/test_imported.py | 8 +- tests/system/small/ml/test_linear_model.py | 8 +- tests/system/small/ml/test_llm.py | 22 +- .../sklearn/cluster/_kmeans.py | 16 +- .../sklearn/linear_model/_base.py | 6 +- .../bigframes_vendored/xgboost/sklearn.py | 2 +- 21 files changed, 2737 insertions(+), 2275 deletions(-) diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 772b90f666e..c9f52ba0b6b 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import cast, Dict, List, Optional, Union +from typing import Dict, List, Optional, Union from google.cloud import bigquery @@ -92,7 +92,7 @@ def predict( (X,) = utils.convert_to_dataframe(X) - return cast(bpd.DataFrame, self._bqml_model.predict(X)[["CENTROID_ID"]]) + return self._bqml_model.predict(X) def to_gbq(self, model_name: str, replace: bool = False) -> KMeans: """Save the model to BigQuery. diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 8e6be6d28c3..7cda7a69933 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import cast, List, Optional, Union +from typing import List, Optional, Union from google.cloud import bigquery @@ -106,12 +106,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: (X,) = utils.convert_to_dataframe(X) - return cast( - bpd.DataFrame, - self._bqml_model.predict(X)[ - ["principal_component_" + str(i + 1) for i in range(self.n_components)] - ], - ) + return self._bqml_model.predict(X) def to_gbq(self, model_name: str, replace: bool = False) -> PCA: """Save the model to BigQuery. diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 19ca8608ffb..fcb3fe53438 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import cast, Dict, List, Literal, Optional, Union +from typing import Dict, List, Literal, Optional, Union from google.cloud import bigquery @@ -168,16 +168,7 @@ def predict( raise RuntimeError("A model must be fitted before predict") (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def score( self, @@ -328,19 +319,9 @@ def _fit( def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") - (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def score( self, @@ -486,19 +467,9 @@ def predict( ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") - (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def score( self, @@ -661,19 +632,9 @@ def predict( ) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") - (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def score( self, diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 8e309d5e736..cf23854fa01 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -16,7 +16,7 @@ from __future__ import annotations -from typing import cast, Dict, List, Optional, Union +from typing import Dict, List, Optional, Union from google.cloud import bigquery @@ -24,8 +24,6 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -_PREDICT_OUTPUT_COLUMNS = ["forecast_timestamp", "forecast_value"] - class ARIMAPlus(base.SupervisedTrainablePredictor): """Time Series ARIMA Plus model.""" @@ -100,10 +98,7 @@ def predict(self, X=None) -> bpd.DataFrame: if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") - return cast( - bpd.DataFrame, - self._bqml_model.forecast()[_PREDICT_OUTPUT_COLUMNS], - ) + return self._bqml_model.forecast() def score( self, diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index fb8aa98befd..f6afc9aa382 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -78,16 +78,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def to_gbq(self, model_name: str, replace: bool = False) -> TensorFlowModel: """Save the model to BigQuery. @@ -161,16 +152,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def to_gbq(self, model_name: str, replace: bool = False) -> ONNXModel: """Save the model to BigQuery. diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index f11879500b9..433d9fbc380 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import cast, Dict, List, Literal, Optional, Union +from typing import Dict, List, Literal, Optional, Union from google.cloud import bigquery @@ -145,16 +145,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def score( self, @@ -267,16 +258,7 @@ def predict( (X,) = utils.convert_to_dataframe(X) - df = self._bqml_model.predict(X) - return cast( - bpd.DataFrame, - df[ - [ - cast(str, field.name) - for field in self._bqml_model.model.label_columns - ] - ], - ) + return self._bqml_model.predict(X) def score( self, diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 3cfc28e61f8..93e2ba825f4 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -149,7 +149,8 @@ def predict( Returns: - bigframes.dataframe.DataFrame: Output DataFrame with only 1 column as the output text results.""" + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. + """ # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models if temperature < 0.0 or temperature > 1.0: @@ -181,11 +182,7 @@ def predict( "top_p": top_p, "flatten_json_output": True, } - df = self._bqml_model.generate_text(X, options) - return cast( - bpd.DataFrame, - df[[_TEXT_GENERATE_RESULT_COLUMN]], - ) + return self._bqml_model.generate_text(X, options) class PaLM2TextEmbeddingGenerator(base.Predictor): @@ -269,7 +266,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: Input DataFrame, which needs to contain a column with name "content". Only the column will be used as input. Content can include preamble, questions, suggestions, instructions, or examples. Returns: - bigframes.dataframe.DataFrame: Output DataFrame with only 1 column as the output embedding results + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. """ # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models @@ -287,8 +284,4 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: options = { "flatten_json_output": True, } - df = self._bqml_model.generate_text_embedding(X, options) - return cast( - bpd.DataFrame, - df[[_EMBED_TEXT_RESULT_COLUMN]], - ) + return self._bqml_model.generate_text_embedding(X, options) diff --git a/notebooks/getting_started/ml_fundamentals.ipynb b/notebooks/getting_started/ml_fundamentals.ipynb index 2f566dd7049..165bd90f314 100644 --- a/notebooks/getting_started/ml_fundamentals.ipynb +++ b/notebooks/getting_started/ml_fundamentals.ipynb @@ -14,46 +14,16 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 1, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0c8a8bc0b4d64448aef68d6a98fae666", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 28e903c6-e874-4b99-8f53-0755e0b0c188 is RUNNING. " ] }, "metadata": {}, @@ -61,13 +31,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9680fd748e0546b4a010fda0155c5027", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e8aba858-7660-4274-8d90-8d2b0382f8f6 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 7950d6a7-3747-4454-bba2-9660e830647f is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -117,250 +85,250 @@ " \n", " 0\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 36.6\n", - " 18.4\n", - " 184.0\n", - " 3475.0\n", - " FEMALE\n", + " Biscoe\n", + " 40.1\n", + " 18.9\n", + " 188.0\n", + " 4300.0\n", + " MALE\n", " \n", " \n", " 1\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 39.8\n", - " 19.1\n", - " 184.0\n", - " 4650.0\n", + " Torgersen\n", + " 39.1\n", + " 18.7\n", + " 181.0\n", + " 3750.0\n", " MALE\n", " \n", " \n", " 2\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.9\n", - " 18.9\n", - " 184.0\n", - " 3900.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 47.4\n", + " 14.6\n", + " 212.0\n", + " 4725.0\n", + " FEMALE\n", " \n", " \n", " 3\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 46.5\n", - " 17.9\n", - " 192.0\n", - " 3500.0\n", + " 42.5\n", + " 16.7\n", + " 187.0\n", + " 3350.0\n", " FEMALE\n", " \n", " \n", " 4\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.3\n", - " 16.8\n", - " 192.0\n", - " 3000.0\n", - " FEMALE\n", + " Biscoe\n", + " 43.2\n", + " 19.0\n", + " 197.0\n", + " 4775.0\n", + " MALE\n", " \n", " \n", " 5\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 43.2\n", - " 18.5\n", - " 192.0\n", - " 4100.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.7\n", + " 15.3\n", + " 219.0\n", + " 5200.0\n", " MALE\n", " \n", " \n", " 6\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 46.9\n", - " 16.6\n", - " 192.0\n", - " 2700.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 41.3\n", + " 21.1\n", + " 195.0\n", + " 4400.0\n", + " MALE\n", " \n", " \n", " 7\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 50.5\n", - " 18.4\n", - " 200.0\n", - " 3400.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 45.2\n", + " 13.8\n", + " 215.0\n", + " 4750.0\n", " FEMALE\n", " \n", " \n", " 8\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 49.5\n", - " 19.0\n", - " 200.0\n", - " 3800.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.5\n", + " 13.5\n", + " 210.0\n", + " 4550.0\n", + " FEMALE\n", " \n", " \n", " 9\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.2\n", - " 20.1\n", - " 200.0\n", - " 3975.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 50.5\n", + " 15.2\n", + " 216.0\n", + " 5000.0\n", + " FEMALE\n", " \n", " \n", " 10\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.8\n", - " 18.9\n", - " 208.0\n", - " 4300.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 48.2\n", + " 15.6\n", + " 221.0\n", + " 5100.0\n", " MALE\n", " \n", " \n", " 11\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 39.0\n", - " 18.7\n", - " 185.0\n", - " 3650.0\n", - " MALE\n", + " 38.1\n", + " 18.6\n", + " 190.0\n", + " 3700.0\n", + " FEMALE\n", " \n", " \n", " 12\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.0\n", - " 16.9\n", - " 185.0\n", - " 3000.0\n", - " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 50.7\n", + " 15.0\n", + " 223.0\n", + " 5550.0\n", + " MALE\n", " \n", " \n", " 13\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 47.0\n", - " 17.3\n", - " 185.0\n", - " 3700.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 37.8\n", + " 20.0\n", + " 190.0\n", + " 4250.0\n", + " MALE\n", " \n", " \n", " 14\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 34.0\n", - " 17.1\n", - " 185.0\n", - " 3400.0\n", + " Biscoe\n", + " 35.0\n", + " 17.9\n", + " 190.0\n", + " 3450.0\n", " FEMALE\n", " \n", " \n", " 15\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.0\n", - " 16.5\n", - " 185.0\n", - " 3400.0\n", - " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 48.7\n", + " 15.7\n", + " 208.0\n", + " 5350.0\n", + " MALE\n", " \n", " \n", " 16\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 45.7\n", - " 17.3\n", - " 193.0\n", - " 3600.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Torgersen\n", + " 34.6\n", + " 21.1\n", + " 198.0\n", + " 4400.0\n", + " MALE\n", " \n", " \n", " 17\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 50.6\n", - " 19.4\n", - " 193.0\n", - " 3800.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.8\n", + " 15.4\n", + " 215.0\n", + " 5150.0\n", " MALE\n", " \n", " \n", " 18\n", - " Adelie Penguin (Pygoscelis adeliae)\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 39.7\n", - " 17.9\n", - " 193.0\n", - " 4250.0\n", + " 50.3\n", + " 20.0\n", + " 197.0\n", + " 3300.0\n", " MALE\n", " \n", " \n", " 19\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 37.8\n", + " 37.2\n", " 18.1\n", - " 193.0\n", - " 3750.0\n", + " 178.0\n", + " 3900.0\n", " MALE\n", " \n", " \n", " 20\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 46.6\n", - " 17.8\n", - " 193.0\n", - " 3800.0\n", - " FEMALE\n", + " 51.0\n", + " 18.8\n", + " 203.0\n", + " 4100.0\n", + " MALE\n", " \n", " \n", " 21\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 51.3\n", - " 19.2\n", - " 193.0\n", - " 3650.0\n", - " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 40.5\n", + " 17.9\n", + " 187.0\n", + " 3200.0\n", + " FEMALE\n", " \n", " \n", " 22\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.2\n", - " 17.1\n", - " 193.0\n", - " 3400.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 45.5\n", + " 13.9\n", + " 210.0\n", + " 4200.0\n", " FEMALE\n", " \n", " \n", " 23\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 36.8\n", + " 42.2\n", " 18.5\n", - " 193.0\n", - " 3500.0\n", + " 180.0\n", + " 3550.0\n", " FEMALE\n", " \n", " \n", " 24\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 49.6\n", - " 18.2\n", - " 193.0\n", + " 51.7\n", + " 20.3\n", + " 194.0\n", " 3775.0\n", " MALE\n", " \n", @@ -370,86 +338,86 @@ "[334 rows x 7 columns in total]" ], "text/plain": [ - " species island \\\n", - "penguin_id \n", - "0 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "1 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "2 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "3 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "4 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "5 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "6 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "7 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "8 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "9 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "10 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "11 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "12 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "13 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "14 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "15 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "16 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "17 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "18 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "19 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "20 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "21 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "22 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "23 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "24 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + " species island \\\n", + "penguin_id \n", + "0 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "1 Adelie Penguin (Pygoscelis adeliae) Torgersen \n", + "2 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "4 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "5 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "6 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "7 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "8 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "9 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "10 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "12 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "13 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "14 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "15 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "16 Adelie Penguin (Pygoscelis adeliae) Torgersen \n", + "17 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "18 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream \n", + "21 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "22 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream \n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream \n", "\n", " culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", "penguin_id \n", - "0 36.6 18.4 184.0 3475.0 \n", - "1 39.8 19.1 184.0 4650.0 \n", - "2 40.9 18.9 184.0 3900.0 \n", - "3 46.5 17.9 192.0 3500.0 \n", - "4 37.3 16.8 192.0 3000.0 \n", - "5 43.2 18.5 192.0 4100.0 \n", - "6 46.9 16.6 192.0 2700.0 \n", - "7 50.5 18.4 200.0 3400.0 \n", - "8 49.5 19.0 200.0 3800.0 \n", - "9 40.2 20.1 200.0 3975.0 \n", - "10 40.8 18.9 208.0 4300.0 \n", - "11 39.0 18.7 185.0 3650.0 \n", - "12 37.0 16.9 185.0 3000.0 \n", - "13 47.0 17.3 185.0 3700.0 \n", - "14 34.0 17.1 185.0 3400.0 \n", - "15 37.0 16.5 185.0 3400.0 \n", - "16 45.7 17.3 193.0 3600.0 \n", - "17 50.6 19.4 193.0 3800.0 \n", - "18 39.7 17.9 193.0 4250.0 \n", - "19 37.8 18.1 193.0 3750.0 \n", - "20 46.6 17.8 193.0 3800.0 \n", - "21 51.3 19.2 193.0 3650.0 \n", - "22 40.2 17.1 193.0 3400.0 \n", - "23 36.8 18.5 193.0 3500.0 \n", - "24 49.6 18.2 193.0 3775.0 \n", + "0 40.1 18.9 188.0 4300.0 \n", + "1 39.1 18.7 181.0 3750.0 \n", + "2 47.4 14.6 212.0 4725.0 \n", + "3 42.5 16.7 187.0 3350.0 \n", + "4 43.2 19.0 197.0 4775.0 \n", + "5 46.7 15.3 219.0 5200.0 \n", + "6 41.3 21.1 195.0 4400.0 \n", + "7 45.2 13.8 215.0 4750.0 \n", + "8 46.5 13.5 210.0 4550.0 \n", + "9 50.5 15.2 216.0 5000.0 \n", + "10 48.2 15.6 221.0 5100.0 \n", + "11 38.1 18.6 190.0 3700.0 \n", + "12 50.7 15.0 223.0 5550.0 \n", + "13 37.8 20.0 190.0 4250.0 \n", + "14 35.0 17.9 190.0 3450.0 \n", + "15 48.7 15.7 208.0 5350.0 \n", + "16 34.6 21.1 198.0 4400.0 \n", + "17 46.8 15.4 215.0 5150.0 \n", + "18 50.3 20.0 197.0 3300.0 \n", + "19 37.2 18.1 178.0 3900.0 \n", + "20 51.0 18.8 203.0 4100.0 \n", + "21 40.5 17.9 187.0 3200.0 \n", + "22 45.5 13.9 210.0 4200.0 \n", + "23 42.2 18.5 180.0 3550.0 \n", + "24 51.7 20.3 194.0 3775.0 \n", "\n", " sex \n", "penguin_id \n", - "0 FEMALE \n", + "0 MALE \n", "1 MALE \n", - "2 MALE \n", + "2 FEMALE \n", "3 FEMALE \n", - "4 FEMALE \n", + "4 MALE \n", "5 MALE \n", - "6 FEMALE \n", + "6 MALE \n", "7 FEMALE \n", - "8 MALE \n", - "9 MALE \n", + "8 FEMALE \n", + "9 FEMALE \n", "10 MALE \n", - "11 MALE \n", - "12 FEMALE \n", - "13 FEMALE \n", + "11 FEMALE \n", + "12 MALE \n", + "13 MALE \n", "14 FEMALE \n", - "15 FEMALE \n", - "16 FEMALE \n", + "15 MALE \n", + "16 MALE \n", "17 MALE \n", "18 MALE \n", "19 MALE \n", - "20 FEMALE \n", - "21 MALE \n", + "20 MALE \n", + "21 FEMALE \n", "22 FEMALE \n", "23 FEMALE \n", "24 MALE \n", @@ -458,7 +426,7 @@ "[334 rows x 7 columns]" ] }, - "execution_count": 18, + "execution_count": 1, "metadata": {}, "output_type": "execute_result" } @@ -490,18 +458,16 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 2, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "171160f246eb43d1832aeefb055c0851", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job deda90a8-6ec7-419c-8067-e85777bd916f is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 1408053d-cb80-4870-af28-e94b90a20a6d is DONE. 28.9 kB processed. " ] }, "metadata": {}, @@ -509,13 +475,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "eaffac40f94745728e6bd618bebd2c53", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job efe8fa0a-d450-475a-99d5-36beeb985247 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 262885fe-973c-4338-a853-227f9db4835a is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -523,13 +487,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "68e7ecdc639f4d3ab482830bf6a9da04", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 5022c56d-e605-4cab-be1b-1ecf189588a1 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job fb1dc831-7f6f-42ce-96da-1292d73919b4 is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -537,13 +499,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ebfe197fd88348129ebe2f7d288bf4b9", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 175bd293-d448-4510-b926-1d8cfb4eb5e7 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e79add79-f1e4-4cf0-bb97-04d153222f19 is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -551,13 +511,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2ae69ea7da5247e8a1f7cd0e049629cb", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job a3a2e68c-f5f3-4237-99ad-44974f29d090 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job cb5ee343-f86e-4795-b0ce-d58854e72e5c is RUNNING. " ] }, "metadata": {}, @@ -596,18 +554,16 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5ed4206cd3ad4cd485315605bf033df2", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job db3365fb-67ca-44cc-a117-88a80dc63cca is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e65af31c-feda-468d-89c9-dec033574640 is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -615,13 +571,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ac72db21945542558fdd62093d9dc0c3", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job ab78f7ab-a115-448b-92d0-19c091a831ca is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 0455f252-2b94-457e-bad5-672b91d9b51f is RUNNING. " ] }, "metadata": {}, @@ -667,47 +621,47 @@ " \n", " \n", " \n", - " 156\n", - " Biscoe\n", - " 46.2\n", - " 14.5\n", - " 209.0\n", - " FEMALE\n", - " Gentoo penguin (Pygoscelis papua)\n", + " 249\n", + " Torgersen\n", + " 41.1\n", + " 18.6\n", + " 189.0\n", + " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 189\n", + " 36\n", " Biscoe\n", - " 35.3\n", - " 18.9\n", - " 187.0\n", + " 43.4\n", + " 14.4\n", + " 218.0\n", " FEMALE\n", - " Adelie Penguin (Pygoscelis adeliae)\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 279\n", + " 74\n", " Biscoe\n", - " 45.1\n", - " 14.5\n", - " 215.0\n", + " 42.8\n", + " 14.2\n", + " 209.0\n", " FEMALE\n", " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 245\n", - " Biscoe\n", - " 49.5\n", - " 16.2\n", - " 229.0\n", - " MALE\n", - " Gentoo penguin (Pygoscelis papua)\n", + " 235\n", + " Dream\n", + " 34.0\n", + " 17.1\n", + " 185.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 343\n", - " Torgersen\n", - " 37.3\n", - " 20.5\n", - " 199.0\n", + " 117\n", + " Dream\n", + " 37.8\n", + " 18.1\n", + " 193.0\n", " MALE\n", " Adelie Penguin (Pygoscelis adeliae)\n", " \n", @@ -719,24 +673,24 @@ "text/plain": [ " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", "penguin_id \n", - "156 Biscoe 46.2 14.5 209.0 \n", - "189 Biscoe 35.3 18.9 187.0 \n", - "279 Biscoe 45.1 14.5 215.0 \n", - "245 Biscoe 49.5 16.2 229.0 \n", - "343 Torgersen 37.3 20.5 199.0 \n", + "249 Torgersen 41.1 18.6 189.0 \n", + "36 Biscoe 43.4 14.4 218.0 \n", + "74 Biscoe 42.8 14.2 209.0 \n", + "235 Dream 34.0 17.1 185.0 \n", + "117 Dream 37.8 18.1 193.0 \n", "\n", " sex species \n", "penguin_id \n", - "156 FEMALE Gentoo penguin (Pygoscelis papua) \n", - "189 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", - "279 FEMALE Gentoo penguin (Pygoscelis papua) \n", - "245 MALE Gentoo penguin (Pygoscelis papua) \n", - "343 MALE Adelie Penguin (Pygoscelis adeliae) \n", + "249 MALE Adelie Penguin (Pygoscelis adeliae) \n", + "36 FEMALE Gentoo penguin (Pygoscelis papua) \n", + "74 FEMALE Gentoo penguin (Pygoscelis papua) \n", + "235 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", + "117 MALE Adelie Penguin (Pygoscelis adeliae) \n", "\n", "[5 rows x 6 columns]" ] }, - "execution_count": 20, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -749,18 +703,16 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d6dd794f89724099950dcc927d63d0f5", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 22a72cad-11a6-4f8e-b16d-f92853b8112e is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job d5a173bd-a7dc-42fa-8468-b088d47ccfe0 is RUNNING. " ] }, "metadata": {}, @@ -768,13 +720,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a8ab7ca12e0d43a6803483480e837c6e", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job bc952727-8806-4fe2-abf2-c3a8a2bd9b6d is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job c6b6518b-2689-4dc1-a5b0-2a9ab75301eb is RUNNING. " ] }, "metadata": {}, @@ -810,24 +760,24 @@ " \n", " \n", " \n", - " 156\n", - " 4800.0\n", + " 249\n", + " 3325.0\n", " \n", " \n", - " 189\n", - " 3800.0\n", + " 36\n", + " 4600.0\n", " \n", " \n", - " 279\n", - " 5000.0\n", + " 74\n", + " 4700.0\n", " \n", " \n", - " 245\n", - " 5800.0\n", + " 235\n", + " 3400.0\n", " \n", " \n", - " 343\n", - " 3775.0\n", + " 117\n", + " 3750.0\n", " \n", " \n", "\n", @@ -837,16 +787,16 @@ "text/plain": [ " body_mass_g\n", "penguin_id \n", - "156 4800.0\n", - "189 3800.0\n", - "279 5000.0\n", - "245 5800.0\n", - "343 3775.0\n", + "249 3325.0\n", + "36 4600.0\n", + "74 4700.0\n", + "235 3400.0\n", + "117 3750.0\n", "\n", "[5 rows x 1 columns]" ] }, - "execution_count": 21, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -880,18 +830,16 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "380c57dc3fe54fbd8ad2fb23f1e66e37", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job f239341e-785f-43e1-bfe0-683132d6f15f is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 03a0eb1c-747e-4c2a-b7b5-d3e4e5a78134 is RUNNING. " ] }, "metadata": {}, @@ -899,13 +847,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3db47aadba854beca71960d846838dc4", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 2d5bbbb9-efc4-4f4e-a8dc-2c7b66b0e5e0 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 70608c84-dac8-4e77-8a9e-00d823b24f37 is RUNNING. " ] }, "metadata": {}, @@ -913,13 +859,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1de81f2944a44cbda3f16fa8a1fae813", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 66120e1c-2471-4a0c-8b82-aeb189c8866a is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job d18fdc32-2152-45d3-8c62-bf9b1556ec47 is RUNNING. " ] }, "metadata": {}, @@ -927,13 +871,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b06cae61a4534388a4e9ed26ce442cc2", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 62825fc4-5b77-43e5-a3e4-525ebfd1285b is DONE. 2.1 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 2a022682-535f-4dc0-80ba-1640306ad9ef is RUNNING. " ] }, "metadata": {}, @@ -941,13 +883,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "977c8eae2c9848e98c5478c41af82633", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 656d1d69-b4ff-4db6-9f2d-28dcf91e2fd7 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job c145b39d-7d02-4394-80f0-fc605b2ba256 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -955,13 +895,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aefc3085fee04c438d0327d400b4b72a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 466507c8-1474-4725-93e5-baf8ee292e39 is DONE. 8.5 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job fc156a2b-db95-44a3-9ad1-d95b9d290080 is RUNNING. " ] }, "metadata": {}, @@ -1002,153 +940,153 @@ " \n", " \n", " 0\n", - " -1.344188\n", - " 0.642519\n", - " -1.193942\n", - " \n", - " \n", - " 1\n", - " -0.750047\n", - " 1.005876\n", - " -1.193942\n", + " -0.750505\n", + " 0.84903\n", + " -0.937262\n", " \n", " \n", " 2\n", - " -0.545811\n", - " 0.90206\n", - " -1.193942\n", + " 0.622496\n", + " -1.322402\n", + " 0.804051\n", " \n", " \n", - " 4\n", - " -1.214219\n", - " -0.188011\n", - " -0.619171\n", + " 3\n", + " -0.299107\n", + " -0.261935\n", + " -1.009817\n", " \n", " \n", " 5\n", - " -0.118772\n", - " 0.694427\n", - " -0.619171\n", + " 0.490839\n", + " -0.968913\n", + " 1.311935\n", " \n", " \n", " 6\n", - " 0.568203\n", - " -0.291828\n", - " -0.619171\n", + " -0.524806\n", + " 1.959995\n", + " -0.429379\n", " \n", " \n", " 7\n", - " 1.236611\n", - " 0.642519\n", - " -0.044401\n", + " 0.208715\n", + " -1.726389\n", + " 1.021716\n", " \n", " \n", " 9\n", - " -0.675779\n", - " 1.524957\n", - " -0.044401\n", + " 1.205551\n", + " -1.019412\n", + " 1.09427\n", " \n", " \n", " 10\n", - " -0.564378\n", - " 0.90206\n", - " 0.530369\n", - " \n", - " \n", - " 11\n", - " -0.898582\n", - " 0.798243\n", - " -1.122096\n", + " 0.772962\n", + " -0.817418\n", + " 1.457044\n", " \n", " \n", " 12\n", - " -1.26992\n", - " -0.136103\n", - " -1.122096\n", - " \n", - " \n", - " 13\n", - " 0.58677\n", - " 0.071529\n", - " -1.122096\n", + " 1.243168\n", + " -1.120408\n", + " 1.602153\n", " \n", " \n", " 14\n", - " -1.826927\n", - " -0.032287\n", - " -1.122096\n", + " -1.709725\n", + " 0.344046\n", + " -0.792152\n", " \n", " \n", - " 15\n", - " -1.26992\n", - " -0.343736\n", - " -1.122096\n", - " \n", - " \n", - " 16\n", - " 0.3454\n", - " 0.071529\n", - " -0.547325\n", + " 17\n", + " 0.509647\n", + " -0.918415\n", + " 1.021716\n", " \n", " \n", " 18\n", - " -0.768614\n", - " 0.382978\n", - " -0.547325\n", + " 1.167935\n", + " 1.404513\n", + " -0.284269\n", " \n", " \n", " 19\n", - " -1.121385\n", - " 0.486795\n", - " -0.547325\n", + " -1.295944\n", + " 0.445043\n", + " -1.662809\n", " \n", " \n", " 20\n", - " 0.512502\n", - " 0.33107\n", - " -0.547325\n", + " 1.299593\n", + " 0.798532\n", + " 0.151059\n", " \n", " \n", " 21\n", - " 1.385146\n", - " 1.057784\n", - " -0.547325\n", + " -0.675272\n", + " 0.344046\n", + " -1.009817\n", " \n", " \n", " 22\n", - " -0.675779\n", - " -0.032287\n", - " -0.547325\n", + " 0.26514\n", + " -1.675891\n", + " 0.658942\n", " \n", " \n", " 24\n", - " 1.069509\n", - " 0.538703\n", - " -0.547325\n", + " 1.43125\n", + " 1.556008\n", + " -0.501934\n", + " \n", + " \n", + " 25\n", + " 0.302756\n", + " 0.041055\n", + " -0.574488\n", " \n", " \n", " 26\n", - " -0.43441\n", - " 0.694427\n", - " 0.027445\n", + " 0.302756\n", + " -1.675891\n", + " 0.949161\n", + " \n", + " \n", + " 27\n", + " 0.227523\n", + " -1.776888\n", + " 0.658942\n", " \n", " \n", " 28\n", - " 1.923586\n", - " 1.888314\n", - " 0.027445\n", + " 1.318401\n", + " -0.362932\n", + " 1.747263\n", + " \n", + " \n", + " 29\n", + " 2.202388\n", + " 1.303516\n", + " 0.441278\n", " \n", " \n", " 30\n", - " 1.292312\n", - " 0.694427\n", - " 0.027445\n", + " -0.919779\n", + " 1.959995\n", + " -0.356824\n", " \n", " \n", " 31\n", - " -1.994029\n", - " -0.551368\n", - " -1.62502\n", + " 1.036277\n", + " -0.615424\n", + " 1.747263\n", + " \n", + " \n", + " 32\n", + " -0.223874\n", + " 0.19255\n", + " -0.356824\n", " \n", " \n", "\n", @@ -1158,65 +1096,65 @@ "text/plain": [ " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", "penguin_id \n", - "0 -1.344188 0.642519 \n", - "1 -0.750047 1.005876 \n", - "2 -0.545811 0.90206 \n", - "4 -1.214219 -0.188011 \n", - "5 -0.118772 0.694427 \n", - "6 0.568203 -0.291828 \n", - "7 1.236611 0.642519 \n", - "9 -0.675779 1.524957 \n", - "10 -0.564378 0.90206 \n", - "11 -0.898582 0.798243 \n", - "12 -1.26992 -0.136103 \n", - "13 0.58677 0.071529 \n", - "14 -1.826927 -0.032287 \n", - "15 -1.26992 -0.343736 \n", - "16 0.3454 0.071529 \n", - "18 -0.768614 0.382978 \n", - "19 -1.121385 0.486795 \n", - "20 0.512502 0.33107 \n", - "21 1.385146 1.057784 \n", - "22 -0.675779 -0.032287 \n", - "24 1.069509 0.538703 \n", - "26 -0.43441 0.694427 \n", - "28 1.923586 1.888314 \n", - "30 1.292312 0.694427 \n", - "31 -1.994029 -0.551368 \n", + "0 -0.750505 0.84903 \n", + "2 0.622496 -1.322402 \n", + "3 -0.299107 -0.261935 \n", + "5 0.490839 -0.968913 \n", + "6 -0.524806 1.959995 \n", + "7 0.208715 -1.726389 \n", + "9 1.205551 -1.019412 \n", + "10 0.772962 -0.817418 \n", + "12 1.243168 -1.120408 \n", + "14 -1.709725 0.344046 \n", + "17 0.509647 -0.918415 \n", + "18 1.167935 1.404513 \n", + "19 -1.295944 0.445043 \n", + "20 1.299593 0.798532 \n", + "21 -0.675272 0.344046 \n", + "22 0.26514 -1.675891 \n", + "24 1.43125 1.556008 \n", + "25 0.302756 0.041055 \n", + "26 0.302756 -1.675891 \n", + "27 0.227523 -1.776888 \n", + "28 1.318401 -0.362932 \n", + "29 2.202388 1.303516 \n", + "30 -0.919779 1.959995 \n", + "31 1.036277 -0.615424 \n", + "32 -0.223874 0.19255 \n", "\n", " standard_scaled_flipper_length_mm \n", "penguin_id \n", - "0 -1.193942 \n", - "1 -1.193942 \n", - "2 -1.193942 \n", - "4 -0.619171 \n", - "5 -0.619171 \n", - "6 -0.619171 \n", - "7 -0.044401 \n", - "9 -0.044401 \n", - "10 0.530369 \n", - "11 -1.122096 \n", - "12 -1.122096 \n", - "13 -1.122096 \n", - "14 -1.122096 \n", - "15 -1.122096 \n", - "16 -0.547325 \n", - "18 -0.547325 \n", - "19 -0.547325 \n", - "20 -0.547325 \n", - "21 -0.547325 \n", - "22 -0.547325 \n", - "24 -0.547325 \n", - "26 0.027445 \n", - "28 0.027445 \n", - "30 0.027445 \n", - "31 -1.62502 \n", + "0 -0.937262 \n", + "2 0.804051 \n", + "3 -1.009817 \n", + "5 1.311935 \n", + "6 -0.429379 \n", + "7 1.021716 \n", + "9 1.09427 \n", + "10 1.457044 \n", + "12 1.602153 \n", + "14 -0.792152 \n", + "17 1.021716 \n", + "18 -0.284269 \n", + "19 -1.662809 \n", + "20 0.151059 \n", + "21 -1.009817 \n", + "22 0.658942 \n", + "24 -0.501934 \n", + "25 -0.574488 \n", + "26 0.949161 \n", + "27 0.658942 \n", + "28 1.747263 \n", + "29 0.441278 \n", + "30 -0.356824 \n", + "31 1.747263 \n", + "32 -0.356824 \n", "...\n", "\n", "[267 rows x 3 columns]" ] }, - "execution_count": 22, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -1237,32 +1175,16 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "74f3c24c0a434e12bf6a56dc4809b501", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job c6268b07-0d3d-4fe0-971d-cc99fd98cd7e is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job 31550d88-fc7b-4fcb-9975-9ed24bf2e009 is RUNNING. " ] }, "metadata": {}, @@ -1270,13 +1192,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5a04e46a7d0248b1ae523f2ca6903ee8", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 1e17f5f7-2956-4bdd-baa9-c07591481341 is DONE. 536 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 5ec7c8b1-037c-466c-a51e-963f8274e76b is RUNNING. " ] }, "metadata": {}, @@ -1284,13 +1204,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "62563820bfb245be85bbc1bf3dfb993c", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e2fde7a6-67b4-45a4-91d4-1cb9eff66ae5 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 4e860716-bc41-4ef6-83ff-310d085ed7cc is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -1298,13 +1216,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "98aff3bfded44868bf120451c89df9f5", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e0683619-23c5-44fd-8930-9d3c9d02729a is DONE. 2.1 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 6b96a757-42fe-4b65-92fd-a3ae339fe769 is RUNNING. " ] }, "metadata": {}, @@ -1344,154 +1260,154 @@ " \n", " \n", " \n", - " 3\n", - " 0.493935\n", - " 0.382978\n", - " -0.619171\n", - " \n", - " \n", - " 8\n", - " 1.050942\n", - " 0.953968\n", - " -0.044401\n", + " 1\n", + " -0.938587\n", + " 0.748033\n", + " -1.445145\n", " \n", " \n", - " 17\n", - " 1.255178\n", - " 1.1616\n", - " -0.547325\n", + " 4\n", + " -0.16745\n", + " 0.899528\n", + " -0.284269\n", " \n", " \n", - " 23\n", - " -1.307054\n", - " 0.694427\n", - " -0.547325\n", + " 8\n", + " 0.453222\n", + " -1.877885\n", + " 0.658942\n", " \n", " \n", - " 25\n", - " 1.515114\n", - " 0.486795\n", - " 0.027445\n", + " 11\n", + " -1.12667\n", + " 0.697535\n", + " -0.792152\n", " \n", " \n", - " 27\n", - " 1.236611\n", - " 1.265417\n", - " 0.027445\n", + " 13\n", + " -1.183094\n", + " 1.404513\n", + " -0.792152\n", " \n", " \n", - " 29\n", - " 1.403713\n", - " 0.953968\n", - " 0.027445\n", + " 15\n", + " 0.867003\n", + " -0.766919\n", + " 0.513833\n", " \n", " \n", - " 34\n", - " 0.419668\n", - " 0.538703\n", - " -1.62502\n", + " 16\n", + " -1.784958\n", + " 1.959995\n", + " -0.211715\n", " \n", " \n", - " 35\n", - " -1.455589\n", - " 0.694427\n", - " -1.050249\n", + " 23\n", + " -0.355532\n", + " 0.647036\n", + " -1.5177\n", " \n", " \n", - " 39\n", - " 0.326833\n", - " 1.1616\n", - " -0.475479\n", + " 34\n", + " -0.600039\n", + " -1.776888\n", + " 0.949161\n", " \n", " \n", - " 51\n", - " -1.065684\n", - " 0.227254\n", - " -0.978403\n", + " 36\n", + " -0.129833\n", + " -1.423399\n", + " 1.23938\n", " \n", " \n", - " 52\n", - " -0.248741\n", - " 0.071529\n", - " -0.978403\n", + " 42\n", + " -1.615684\n", + " -0.514427\n", + " -0.429379\n", " \n", " \n", - " 60\n", - " 0.531069\n", - " 0.382978\n", - " -0.403633\n", + " 48\n", + " 0.415606\n", + " -0.716421\n", + " 1.021716\n", " \n", " \n", " 61\n", - " 0.401101\n", - " 0.90206\n", - " -0.403633\n", + " 0.396797\n", + " -1.170907\n", + " 1.457044\n", " \n", " \n", " 64\n", - " -1.455589\n", - " 0.33107\n", - " -0.403633\n", + " 0.434414\n", + " -1.120408\n", + " 1.09427\n", " \n", " \n", " 65\n", - " -0.564378\n", - " 0.642519\n", - " -0.403633\n", + " -1.220711\n", + " 1.051024\n", + " -1.445145\n", " \n", " \n", - " 67\n", - " 1.273745\n", - " 1.317325\n", - " 0.171138\n", + " 68\n", + " -1.484026\n", + " -0.009443\n", + " -1.009817\n", " \n", " \n", - " 83\n", - " 2.629128\n", - " 0.33107\n", - " -1.409481\n", + " 70\n", + " 1.638141\n", + " 1.404513\n", + " 0.296168\n", " \n", " \n", - " 85\n", - " -1.288487\n", - " 0.746335\n", - " -0.83471\n", + " 72\n", + " 0.829387\n", + " 0.142052\n", + " -0.719598\n", " \n", " \n", - " 93\n", - " -0.508677\n", - " 0.486795\n", - " 0.314831\n", + " 74\n", + " -0.242683\n", + " -1.524396\n", + " 0.586387\n", " \n", " \n", - " 104\n", - " 0.382534\n", - " -0.032287\n", - " -0.762864\n", + " 77\n", + " -1.277136\n", + " -0.211437\n", + " -0.647043\n", " \n", " \n", - " 105\n", - " -1.065684\n", - " 0.746335\n", - " -0.762864\n", + " 81\n", + " 0.208715\n", + " -1.221405\n", + " 0.804051\n", + " \n", + " \n", + " 91\n", + " 1.261976\n", + " 0.647036\n", + " 0.005949\n", " \n", " \n", - " 108\n", - " 1.162343\n", - " 0.382978\n", - " -0.762864\n", + " 96\n", + " 0.246331\n", + " -1.322402\n", + " 0.731497\n", " \n", " \n", - " 113\n", - " 1.496547\n", - " 1.213509\n", - " 0.386677\n", + " 105\n", + " -1.803766\n", + " 0.445043\n", + " -1.009817\n", " \n", " \n", - " 130\n", - " -0.341575\n", - " 1.213509\n", - " -0.044401\n", + " 111\n", + " -1.164286\n", + " 0.697535\n", + " -2.098138\n", " \n", " \n", "\n", @@ -1501,65 +1417,65 @@ "text/plain": [ " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", "penguin_id \n", - "3 0.493935 0.382978 \n", - "8 1.050942 0.953968 \n", - "17 1.255178 1.1616 \n", - "23 -1.307054 0.694427 \n", - "25 1.515114 0.486795 \n", - "27 1.236611 1.265417 \n", - "29 1.403713 0.953968 \n", - "34 0.419668 0.538703 \n", - "35 -1.455589 0.694427 \n", - "39 0.326833 1.1616 \n", - "51 -1.065684 0.227254 \n", - "52 -0.248741 0.071529 \n", - "60 0.531069 0.382978 \n", - "61 0.401101 0.90206 \n", - "64 -1.455589 0.33107 \n", - "65 -0.564378 0.642519 \n", - "67 1.273745 1.317325 \n", - "83 2.629128 0.33107 \n", - "85 -1.288487 0.746335 \n", - "93 -0.508677 0.486795 \n", - "104 0.382534 -0.032287 \n", - "105 -1.065684 0.746335 \n", - "108 1.162343 0.382978 \n", - "113 1.496547 1.213509 \n", - "130 -0.341575 1.213509 \n", + "1 -0.938587 0.748033 \n", + "4 -0.16745 0.899528 \n", + "8 0.453222 -1.877885 \n", + "11 -1.12667 0.697535 \n", + "13 -1.183094 1.404513 \n", + "15 0.867003 -0.766919 \n", + "16 -1.784958 1.959995 \n", + "23 -0.355532 0.647036 \n", + "34 -0.600039 -1.776888 \n", + "36 -0.129833 -1.423399 \n", + "42 -1.615684 -0.514427 \n", + "48 0.415606 -0.716421 \n", + "61 0.396797 -1.170907 \n", + "64 0.434414 -1.120408 \n", + "65 -1.220711 1.051024 \n", + "68 -1.484026 -0.009443 \n", + "70 1.638141 1.404513 \n", + "72 0.829387 0.142052 \n", + "74 -0.242683 -1.524396 \n", + "77 -1.277136 -0.211437 \n", + "81 0.208715 -1.221405 \n", + "91 1.261976 0.647036 \n", + "96 0.246331 -1.322402 \n", + "105 -1.803766 0.445043 \n", + "111 -1.164286 0.697535 \n", "\n", " standard_scaled_flipper_length_mm \n", "penguin_id \n", - "3 -0.619171 \n", - "8 -0.044401 \n", - "17 -0.547325 \n", - "23 -0.547325 \n", - "25 0.027445 \n", - "27 0.027445 \n", - "29 0.027445 \n", - "34 -1.62502 \n", - "35 -1.050249 \n", - "39 -0.475479 \n", - "51 -0.978403 \n", - "52 -0.978403 \n", - "60 -0.403633 \n", - "61 -0.403633 \n", - "64 -0.403633 \n", - "65 -0.403633 \n", - "67 0.171138 \n", - "83 -1.409481 \n", - "85 -0.83471 \n", - "93 0.314831 \n", - "104 -0.762864 \n", - "105 -0.762864 \n", - "108 -0.762864 \n", - "113 0.386677 \n", - "130 -0.044401 \n", + "1 -1.445145 \n", + "4 -0.284269 \n", + "8 0.658942 \n", + "11 -0.792152 \n", + "13 -0.792152 \n", + "15 0.513833 \n", + "16 -0.211715 \n", + "23 -1.5177 \n", + "34 0.949161 \n", + "36 1.23938 \n", + "42 -0.429379 \n", + "48 1.021716 \n", + "61 1.457044 \n", + "64 1.09427 \n", + "65 -1.445145 \n", + "68 -1.009817 \n", + "70 0.296168 \n", + "72 -0.719598 \n", + "74 0.586387 \n", + "77 -0.647043 \n", + "81 0.804051 \n", + "91 0.005949 \n", + "96 0.731497 \n", + "105 -1.009817 \n", + "111 -2.098138 \n", "...\n", "\n", "[67 rows x 3 columns]" ] }, - "execution_count": 23, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -1581,32 +1497,16 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d642a617d27f4e2493c80dbdd1686193", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job a8d8afa4-d91e-487e-8709-8727a73ab453 is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job b9afd624-4345-4160-8809-05786563ce35 is RUNNING. " ] }, "metadata": {}, @@ -1614,13 +1514,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "09217776c2294e8b929a56e7a73fbfa8", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 41962e2e-4d14-4053-9297-3ce61699551a is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job c918fc7c-a956-4259-b5c5-09c2eac615cd is RUNNING. " ] }, "metadata": {}, @@ -1628,13 +1526,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9c1581fc9fcb49739d1d81b73506b894", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 5d3c22c9-c972-4213-8557-726c9e0aca37 is DONE. 22.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 1d855341-282f-4d10-9ba9-3ce6683b729a is RUNNING. " ] }, "metadata": {}, @@ -1642,13 +1538,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d7749eb7cf554697a60c90f3718ad582", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 9cb7b33f-ea05-4cf4-9f92-bb3aa4ea8d10 is DONE. 2.1 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job c257ff78-3e15-4296-82f5-ba6c2eb6a6ff is RUNNING. " ] }, "metadata": {}, @@ -1656,13 +1550,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e900465918224249bccc781d992aadbb", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job fe1f35d6-d82c-4aab-a284-637b72554f5b is DONE. 29.2 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job a17eec0c-10d0-4943-95be-60fced57d5cb is RUNNING. " ] }, "metadata": {}, @@ -1670,13 +1562,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b0272ee35c5745a491b7c5883b3fbb1b", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 37bc90ff-59cb-4b0c-8f9d-73bcda43524a is DONE. 536 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 1db53c8a-cf45-4c69-a443-6b7a49fc3a07 is DONE. 536 Bytes processed. " ] }, "metadata": {}, @@ -1684,13 +1574,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "00f9d4b55bb94997aaebdae298cefab3", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e23f4724-fdd8-45a9-8c87-defd8d471035 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job ae870ee3-e633-4556-94e6-6669fa0bfde2 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -1698,13 +1586,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9cd8e791be5844669cba10dc53f862ae", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 257378db-0569-42d7-965a-7757154c710b is DONE. 21.4 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job baa7c70c-eabc-49e1-bae9-fdd4891cdb6e is RUNNING. " ] }, "metadata": {}, @@ -1751,226 +1637,226 @@ " \n", " \n", " 0\n", - " [{'index': 2, 'value': 1.0}]\n", - " -1.344188\n", - " 0.642519\n", - " -1.193942\n", - " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", - " \n", - " \n", - " 1\n", + " -0.750505\n", + " 0.84903\n", + " -0.937262\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.750047\n", - " 1.005876\n", - " -1.193942\n", - " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 2\n", - " [{'index': 2, 'value': 1.0}]\n", - " -0.545811\n", - " 0.90206\n", - " -1.193942\n", - " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", + " 0.622496\n", + " -1.322402\n", + " 0.804051\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 4\n", - " [{'index': 2, 'value': 1.0}]\n", - " -1.214219\n", - " -0.188011\n", - " -0.619171\n", + " 3\n", " [{'index': 2, 'value': 1.0}]\n", + " -0.299107\n", + " -0.261935\n", + " -1.009817\n", " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", " 5\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.490839\n", + " -0.968913\n", + " 1.311935\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.118772\n", - " 0.694427\n", - " -0.619171\n", " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 6\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.524806\n", + " 1.959995\n", + " -0.429379\n", " [{'index': 2, 'value': 1.0}]\n", - " 0.568203\n", - " -0.291828\n", - " -0.619171\n", - " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 7\n", - " [{'index': 2, 'value': 1.0}]\n", - " 1.236611\n", - " 0.642519\n", - " -0.044401\n", - " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.208715\n", + " -1.726389\n", + " 1.021716\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 9\n", - " [{'index': 2, 'value': 1.0}]\n", - " -0.675779\n", - " 1.524957\n", - " -0.044401\n", - " [{'index': 3, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", + " 1.205551\n", + " -1.019412\n", + " 1.09427\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 10\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.772962\n", + " -0.817418\n", + " 1.457044\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.564378\n", - " 0.90206\n", - " 0.530369\n", " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 11\n", + " 12\n", + " [{'index': 1, 'value': 1.0}]\n", + " 1.243168\n", + " -1.120408\n", + " 1.602153\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.898582\n", - " 0.798243\n", - " -1.122096\n", " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 12\n", - " [{'index': 2, 'value': 1.0}]\n", - " -1.26992\n", - " -0.136103\n", - " -1.122096\n", - " [{'index': 2, 'value': 1.0}]\n", + " 14\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.709725\n", + " 0.344046\n", + " -0.792152\n", + " [{'index': 1, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 13\n", - " [{'index': 2, 'value': 1.0}]\n", - " 0.58677\n", - " 0.071529\n", - " -1.122096\n", - " [{'index': 2, 'value': 1.0}]\n", + " 17\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.509647\n", + " -0.918415\n", + " 1.021716\n", " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 14\n", + " 18\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.167935\n", + " 1.404513\n", + " -0.284269\n", " [{'index': 2, 'value': 1.0}]\n", - " -1.826927\n", - " -0.032287\n", - " -1.122096\n", " [{'index': 2, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 15\n", + " 19\n", " [{'index': 2, 'value': 1.0}]\n", - " -1.26992\n", - " -0.343736\n", - " -1.122096\n", + " -1.295944\n", + " 0.445043\n", + " -1.662809\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 16\n", + " 20\n", " [{'index': 2, 'value': 1.0}]\n", - " 0.3454\n", - " 0.071529\n", - " -0.547325\n", + " 1.299593\n", + " 0.798532\n", + " 0.151059\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 18\n", - " [{'index': 2, 'value': 1.0}]\n", - " -0.768614\n", - " 0.382978\n", - " -0.547325\n", - " [{'index': 3, 'value': 1.0}]\n", + " 21\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.675272\n", + " 0.344046\n", + " -1.009817\n", + " [{'index': 1, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 19\n", - " [{'index': 2, 'value': 1.0}]\n", - " -1.121385\n", - " 0.486795\n", - " -0.547325\n", - " [{'index': 3, 'value': 1.0}]\n", + " 22\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.26514\n", + " -1.675891\n", + " 0.658942\n", " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 20\n", + " 24\n", " [{'index': 2, 'value': 1.0}]\n", - " 0.512502\n", - " 0.33107\n", - " -0.547325\n", + " 1.43125\n", + " 1.556008\n", + " -0.501934\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 21\n", + " 25\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.385146\n", - " 1.057784\n", - " -0.547325\n", - " [{'index': 3, 'value': 1.0}]\n", + " 0.302756\n", + " 0.041055\n", + " -0.574488\n", + " [{'index': 1, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 22\n", - " [{'index': 2, 'value': 1.0}]\n", - " -0.675779\n", - " -0.032287\n", - " -0.547325\n", - " [{'index': 2, 'value': 1.0}]\n", + " 26\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.302756\n", + " -1.675891\n", + " 0.949161\n", " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 24\n", - " [{'index': 2, 'value': 1.0}]\n", - " 1.069509\n", - " 0.538703\n", - " -0.547325\n", + " 27\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.227523\n", + " -1.776888\n", + " 0.658942\n", + " [{'index': 1, 'value': 1.0}]\n", " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 26\n", + " 28\n", + " [{'index': 1, 'value': 1.0}]\n", + " 1.318401\n", + " -0.362932\n", + " 1.747263\n", " [{'index': 2, 'value': 1.0}]\n", - " -0.43441\n", - " 0.694427\n", - " 0.027445\n", " [{'index': 3, 'value': 1.0}]\n", - " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 28\n", + " 29\n", + " [{'index': 2, 'value': 1.0}]\n", + " 2.202388\n", + " 1.303516\n", + " 0.441278\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.923586\n", - " 1.888314\n", - " 0.027445\n", - " [{'index': 3, 'value': 1.0}]\n", " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", " 30\n", " [{'index': 2, 'value': 1.0}]\n", - " 1.292312\n", - " 0.694427\n", - " 0.027445\n", - " [{'index': 3, 'value': 1.0}]\n", + " -0.919779\n", + " 1.959995\n", + " -0.356824\n", " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", " 31\n", + " [{'index': 1, 'value': 1.0}]\n", + " 1.036277\n", + " -0.615424\n", + " 1.747263\n", " [{'index': 2, 'value': 1.0}]\n", - " -1.994029\n", - " -0.551368\n", - " -1.62502\n", + " [{'index': 3, 'value': 1.0}]\n", + " \n", + " \n", + " 32\n", + " [{'index': 3, 'value': 1.0}]\n", + " -0.223874\n", + " 0.19255\n", + " -0.356824\n", " [{'index': 2, 'value': 1.0}]\n", " [{'index': 1, 'value': 1.0}]\n", " \n", @@ -1982,121 +1868,121 @@ "text/plain": [ " onehotencoded_island standard_scaled_culmen_length_mm \\\n", "penguin_id \n", - "0 [{'index': 2, 'value': 1.0}] -1.344188 \n", - "1 [{'index': 2, 'value': 1.0}] -0.750047 \n", - "2 [{'index': 2, 'value': 1.0}] -0.545811 \n", - "4 [{'index': 2, 'value': 1.0}] -1.214219 \n", - "5 [{'index': 2, 'value': 1.0}] -0.118772 \n", - "6 [{'index': 2, 'value': 1.0}] 0.568203 \n", - "7 [{'index': 2, 'value': 1.0}] 1.236611 \n", - "9 [{'index': 2, 'value': 1.0}] -0.675779 \n", - "10 [{'index': 2, 'value': 1.0}] -0.564378 \n", - "11 [{'index': 2, 'value': 1.0}] -0.898582 \n", - "12 [{'index': 2, 'value': 1.0}] -1.26992 \n", - "13 [{'index': 2, 'value': 1.0}] 0.58677 \n", - "14 [{'index': 2, 'value': 1.0}] -1.826927 \n", - "15 [{'index': 2, 'value': 1.0}] -1.26992 \n", - "16 [{'index': 2, 'value': 1.0}] 0.3454 \n", - "18 [{'index': 2, 'value': 1.0}] -0.768614 \n", - "19 [{'index': 2, 'value': 1.0}] -1.121385 \n", - "20 [{'index': 2, 'value': 1.0}] 0.512502 \n", - "21 [{'index': 2, 'value': 1.0}] 1.385146 \n", - "22 [{'index': 2, 'value': 1.0}] -0.675779 \n", - "24 [{'index': 2, 'value': 1.0}] 1.069509 \n", - "26 [{'index': 2, 'value': 1.0}] -0.43441 \n", - "28 [{'index': 2, 'value': 1.0}] 1.923586 \n", - "30 [{'index': 2, 'value': 1.0}] 1.292312 \n", - "31 [{'index': 2, 'value': 1.0}] -1.994029 \n", + "0 [{'index': 1, 'value': 1.0}] -0.750505 \n", + "2 [{'index': 1, 'value': 1.0}] 0.622496 \n", + "3 [{'index': 2, 'value': 1.0}] -0.299107 \n", + "5 [{'index': 1, 'value': 1.0}] 0.490839 \n", + "6 [{'index': 1, 'value': 1.0}] -0.524806 \n", + "7 [{'index': 1, 'value': 1.0}] 0.208715 \n", + "9 [{'index': 1, 'value': 1.0}] 1.205551 \n", + "10 [{'index': 1, 'value': 1.0}] 0.772962 \n", + "12 [{'index': 1, 'value': 1.0}] 1.243168 \n", + "14 [{'index': 1, 'value': 1.0}] -1.709725 \n", + "17 [{'index': 1, 'value': 1.0}] 0.509647 \n", + "18 [{'index': 2, 'value': 1.0}] 1.167935 \n", + "19 [{'index': 2, 'value': 1.0}] -1.295944 \n", + "20 [{'index': 2, 'value': 1.0}] 1.299593 \n", + "21 [{'index': 1, 'value': 1.0}] -0.675272 \n", + "22 [{'index': 1, 'value': 1.0}] 0.26514 \n", + "24 [{'index': 2, 'value': 1.0}] 1.43125 \n", + "25 [{'index': 2, 'value': 1.0}] 0.302756 \n", + "26 [{'index': 1, 'value': 1.0}] 0.302756 \n", + "27 [{'index': 1, 'value': 1.0}] 0.227523 \n", + "28 [{'index': 1, 'value': 1.0}] 1.318401 \n", + "29 [{'index': 2, 'value': 1.0}] 2.202388 \n", + "30 [{'index': 2, 'value': 1.0}] -0.919779 \n", + "31 [{'index': 1, 'value': 1.0}] 1.036277 \n", + "32 [{'index': 3, 'value': 1.0}] -0.223874 \n", "\n", " standard_scaled_culmen_depth_mm \\\n", "penguin_id \n", - "0 0.642519 \n", - "1 1.005876 \n", - "2 0.90206 \n", - "4 -0.188011 \n", - "5 0.694427 \n", - "6 -0.291828 \n", - "7 0.642519 \n", - "9 1.524957 \n", - "10 0.90206 \n", - "11 0.798243 \n", - "12 -0.136103 \n", - "13 0.071529 \n", - "14 -0.032287 \n", - "15 -0.343736 \n", - "16 0.071529 \n", - "18 0.382978 \n", - "19 0.486795 \n", - "20 0.33107 \n", - "21 1.057784 \n", - "22 -0.032287 \n", - "24 0.538703 \n", - "26 0.694427 \n", - "28 1.888314 \n", - "30 0.694427 \n", - "31 -0.551368 \n", + "0 0.84903 \n", + "2 -1.322402 \n", + "3 -0.261935 \n", + "5 -0.968913 \n", + "6 1.959995 \n", + "7 -1.726389 \n", + "9 -1.019412 \n", + "10 -0.817418 \n", + "12 -1.120408 \n", + "14 0.344046 \n", + "17 -0.918415 \n", + "18 1.404513 \n", + "19 0.445043 \n", + "20 0.798532 \n", + "21 0.344046 \n", + "22 -1.675891 \n", + "24 1.556008 \n", + "25 0.041055 \n", + "26 -1.675891 \n", + "27 -1.776888 \n", + "28 -0.362932 \n", + "29 1.303516 \n", + "30 1.959995 \n", + "31 -0.615424 \n", + "32 0.19255 \n", "\n", " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", "penguin_id \n", - "0 -1.193942 [{'index': 2, 'value': 1.0}] \n", - "1 -1.193942 [{'index': 3, 'value': 1.0}] \n", - "2 -1.193942 [{'index': 3, 'value': 1.0}] \n", - "4 -0.619171 [{'index': 2, 'value': 1.0}] \n", - "5 -0.619171 [{'index': 3, 'value': 1.0}] \n", - "6 -0.619171 [{'index': 2, 'value': 1.0}] \n", - "7 -0.044401 [{'index': 2, 'value': 1.0}] \n", - "9 -0.044401 [{'index': 3, 'value': 1.0}] \n", - "10 0.530369 [{'index': 3, 'value': 1.0}] \n", - "11 -1.122096 [{'index': 3, 'value': 1.0}] \n", - "12 -1.122096 [{'index': 2, 'value': 1.0}] \n", - "13 -1.122096 [{'index': 2, 'value': 1.0}] \n", - "14 -1.122096 [{'index': 2, 'value': 1.0}] \n", - "15 -1.122096 [{'index': 2, 'value': 1.0}] \n", - "16 -0.547325 [{'index': 2, 'value': 1.0}] \n", - "18 -0.547325 [{'index': 3, 'value': 1.0}] \n", - "19 -0.547325 [{'index': 3, 'value': 1.0}] \n", - "20 -0.547325 [{'index': 2, 'value': 1.0}] \n", - "21 -0.547325 [{'index': 3, 'value': 1.0}] \n", - "22 -0.547325 [{'index': 2, 'value': 1.0}] \n", - "24 -0.547325 [{'index': 3, 'value': 1.0}] \n", - "26 0.027445 [{'index': 3, 'value': 1.0}] \n", - "28 0.027445 [{'index': 3, 'value': 1.0}] \n", - "30 0.027445 [{'index': 3, 'value': 1.0}] \n", - "31 -1.62502 [{'index': 2, 'value': 1.0}] \n", + "0 -0.937262 [{'index': 2, 'value': 1.0}] \n", + "2 0.804051 [{'index': 1, 'value': 1.0}] \n", + "3 -1.009817 [{'index': 1, 'value': 1.0}] \n", + "5 1.311935 [{'index': 2, 'value': 1.0}] \n", + "6 -0.429379 [{'index': 2, 'value': 1.0}] \n", + "7 1.021716 [{'index': 1, 'value': 1.0}] \n", + "9 1.09427 [{'index': 1, 'value': 1.0}] \n", + "10 1.457044 [{'index': 2, 'value': 1.0}] \n", + "12 1.602153 [{'index': 2, 'value': 1.0}] \n", + "14 -0.792152 [{'index': 1, 'value': 1.0}] \n", + "17 1.021716 [{'index': 2, 'value': 1.0}] \n", + "18 -0.284269 [{'index': 2, 'value': 1.0}] \n", + "19 -1.662809 [{'index': 2, 'value': 1.0}] \n", + "20 0.151059 [{'index': 2, 'value': 1.0}] \n", + "21 -1.009817 [{'index': 1, 'value': 1.0}] \n", + "22 0.658942 [{'index': 1, 'value': 1.0}] \n", + "24 -0.501934 [{'index': 2, 'value': 1.0}] \n", + "25 -0.574488 [{'index': 1, 'value': 1.0}] \n", + "26 0.949161 [{'index': 1, 'value': 1.0}] \n", + "27 0.658942 [{'index': 1, 'value': 1.0}] \n", + "28 1.747263 [{'index': 2, 'value': 1.0}] \n", + "29 0.441278 [{'index': 2, 'value': 1.0}] \n", + "30 -0.356824 [{'index': 2, 'value': 1.0}] \n", + "31 1.747263 [{'index': 2, 'value': 1.0}] \n", + "32 -0.356824 [{'index': 2, 'value': 1.0}] \n", "\n", " onehotencoded_species \n", "penguin_id \n", "0 [{'index': 1, 'value': 1.0}] \n", - "1 [{'index': 1, 'value': 1.0}] \n", - "2 [{'index': 1, 'value': 1.0}] \n", - "4 [{'index': 1, 'value': 1.0}] \n", - "5 [{'index': 1, 'value': 1.0}] \n", - "6 [{'index': 2, 'value': 1.0}] \n", - "7 [{'index': 2, 'value': 1.0}] \n", - "9 [{'index': 1, 'value': 1.0}] \n", - "10 [{'index': 1, 'value': 1.0}] \n", - "11 [{'index': 1, 'value': 1.0}] \n", - "12 [{'index': 1, 'value': 1.0}] \n", - "13 [{'index': 2, 'value': 1.0}] \n", + "2 [{'index': 3, 'value': 1.0}] \n", + "3 [{'index': 2, 'value': 1.0}] \n", + "5 [{'index': 3, 'value': 1.0}] \n", + "6 [{'index': 1, 'value': 1.0}] \n", + "7 [{'index': 3, 'value': 1.0}] \n", + "9 [{'index': 3, 'value': 1.0}] \n", + "10 [{'index': 3, 'value': 1.0}] \n", + "12 [{'index': 3, 'value': 1.0}] \n", "14 [{'index': 1, 'value': 1.0}] \n", - "15 [{'index': 1, 'value': 1.0}] \n", - "16 [{'index': 2, 'value': 1.0}] \n", - "18 [{'index': 1, 'value': 1.0}] \n", + "17 [{'index': 3, 'value': 1.0}] \n", + "18 [{'index': 2, 'value': 1.0}] \n", "19 [{'index': 1, 'value': 1.0}] \n", "20 [{'index': 2, 'value': 1.0}] \n", - "21 [{'index': 2, 'value': 1.0}] \n", - "22 [{'index': 1, 'value': 1.0}] \n", + "21 [{'index': 1, 'value': 1.0}] \n", + "22 [{'index': 3, 'value': 1.0}] \n", "24 [{'index': 2, 'value': 1.0}] \n", - "26 [{'index': 1, 'value': 1.0}] \n", - "28 [{'index': 2, 'value': 1.0}] \n", - "30 [{'index': 2, 'value': 1.0}] \n", - "31 [{'index': 1, 'value': 1.0}] \n", + "25 [{'index': 2, 'value': 1.0}] \n", + "26 [{'index': 3, 'value': 1.0}] \n", + "27 [{'index': 3, 'value': 1.0}] \n", + "28 [{'index': 3, 'value': 1.0}] \n", + "29 [{'index': 2, 'value': 1.0}] \n", + "30 [{'index': 1, 'value': 1.0}] \n", + "31 [{'index': 3, 'value': 1.0}] \n", + "32 [{'index': 1, 'value': 1.0}] \n", "...\n", "\n", "[267 rows x 6 columns]" ] }, - "execution_count": 24, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -2138,18 +2024,28 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5db4c5c80ba4417db151aa561dab5ee7", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 7d9c9f8b-6b4c-451f-ae3d-06fb7090d148 is DONE. 21.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job be87ccfa-72ab-4858-9d4a-b2f5f8b2a5e6 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job ceced0cc-13a7-4b14-b42c-4d5f69e7e49a is RUNNING. " ] }, "metadata": {}, @@ -2157,13 +2053,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e6b05d83de0e496d9e47392762046fc5", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 2d651fac-11bf-42da-8c18-bd33207379ca is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job a708b8df-6040-49b1-a6da-d2c0d162f247 is RUNNING. " ] }, "metadata": {}, @@ -2171,13 +2065,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "24d616c24a844abfbfd77ebd9f28486a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 58836ccc-242b-4574-bc48-4c269e74dbf1 is DONE. 5.7 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e9b9cbb5-f6a4-4d85-ba78-1edae77dce94 is RUNNING. " ] }, "metadata": {}, @@ -2185,13 +2077,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ce49b66c6fa0460aa3ee28746765b6ac", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 1bf531f0-0fde-489b-ab36-6040a2a12377 is DONE. 536 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 6c0a41a7-a732-413a-b074-ba82f175eab8 is RUNNING. " ] }, "metadata": {}, @@ -2199,13 +2089,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4a6010d73ca04ea9a133de99aa90da3c", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 4245f4e6-4d5b-404f-81d7-50f0553e2456 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 2d08b79d-9c36-4db7-824a-332fdd02e9fc is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -2213,13 +2101,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ce9cfdca964a4062a52ebaae9d13ae59", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job ed951699-c005-450e-a8b6-0916ec234e7f is DONE. 5.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 7fa0bf53-1022-45ee-b3ac-78fa5c155585 is RUNNING. " ] }, "metadata": {}, @@ -2247,152 +2133,397 @@ " \n", " \n", " predicted_body_mass_g\n", + " onehotencoded_island\n", + " standard_scaled_culmen_length_mm\n", + " standard_scaled_culmen_depth_mm\n", + " standard_scaled_flipper_length_mm\n", + " onehotencoded_sex\n", + " onehotencoded_species\n", " \n", " \n", " penguin_id\n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", - " 3\n", - " 3394.118128\n", + " 1\n", + " 3781.402407\n", + " [{'index': 3, 'value': 1.0}]\n", + " -0.938587\n", + " 0.748033\n", + " -1.445145\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 8\n", - " 4048.685642\n", + " 4\n", + " 4124.107944\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.16745\n", + " 0.899528\n", + " -0.284269\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 17\n", - " 3976.454093\n", + " 8\n", + " 4670.344196\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.453222\n", + " -1.877885\n", + " 0.658942\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 23\n", - " 3541.582194\n", + " 11\n", + " 3529.417214\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.12667\n", + " 0.697535\n", + " -0.792152\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 25\n", - " 4032.844186\n", + " 13\n", + " 4014.101714\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.183094\n", + " 1.404513\n", + " -0.792152\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 27\n", - " 4118.351772\n", + " 15\n", + " 5212.41288\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.867003\n", + " -0.766919\n", + " 0.513833\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 29\n", - " 4087.767826\n", + " 16\n", + " 4163.595615\n", + " [{'index': 3, 'value': 1.0}]\n", + " -1.784958\n", + " 1.959995\n", + " -0.211715\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 34\n", - " 3183.755249\n", + " 23\n", + " 3392.453069\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.355532\n", + " 0.647036\n", + " -1.5177\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 35\n", - " 3418.802274\n", - " \n", - " \n", - " 39\n", - " 3519.186468\n", + " 34\n", + " 4698.305397\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.600039\n", + " -1.776888\n", + " 0.949161\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 51\n", - " 3398.135365\n", + " 36\n", + " 4828.226949\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.129833\n", + " -1.423399\n", + " 1.23938\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 52\n", - " 3223.615957\n", + " 42\n", + " 3430.58866\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.615684\n", + " -0.514427\n", + " -0.429379\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 60\n", - " 3445.014718\n", + " 48\n", + " 5314.260221\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.415606\n", + " -0.716421\n", + " 1.021716\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 61\n", - " 3505.638864\n", + " 5363.205372\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.396797\n", + " -1.170907\n", + " 1.457044\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 64\n", - " 3515.905786\n", + " 4855.908314\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.434414\n", + " -1.120408\n", + " 1.09427\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 65\n", - " 4028.363185\n", + " 3413.100524\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.220711\n", + " 1.051024\n", + " -1.445145\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 67\n", - " 4159.993943\n", + " 68\n", + " 3340.219002\n", + " [{'index': 3, 'value': 1.0}]\n", + " -1.484026\n", + " -0.009443\n", + " -1.009817\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 83\n", - " 3348.16883\n", + " 70\n", + " 4228.73157\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.638141\n", + " 1.404513\n", + " 0.296168\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 85\n", - " 3485.050273\n", + " 72\n", + " 3811.538478\n", + " [{'index': 2, 'value': 1.0}]\n", + " 0.829387\n", + " 0.142052\n", + " -0.719598\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 93\n", - " 4172.874548\n", + " 74\n", + " 4659.770763\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.242683\n", + " -1.524396\n", + " 0.586387\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 104\n", - " 3299.302424\n", + " 77\n", + " 3453.388804\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.277136\n", + " -0.211437\n", + " -0.647043\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 105\n", - " 3515.687917\n", + " 81\n", + " 4766.245033\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.208715\n", + " -1.221405\n", + " 0.804051\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", + " \n", + " \n", + " 91\n", + " 4057.807281\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.261976\n", + " 0.647036\n", + " 0.005949\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 108\n", - " 3405.224618\n", + " 96\n", + " 4739.827445\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.246331\n", + " -1.322402\n", + " 0.731497\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 113\n", - " 4209.140425\n", + " 105\n", + " 3394.891976\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.803766\n", + " 0.445043\n", + " -1.009817\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 130\n", - " 4197.905737\n", + " 111\n", + " 3201.493683\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.164286\n", + " 0.697535\n", + " -2.098138\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", "\n", - "

25 rows × 1 columns

\n", - "[67 rows x 1 columns in total]" - ], - "text/plain": [ - " predicted_body_mass_g\n", - "penguin_id \n", - "3 3394.118128\n", - "8 4048.685642\n", - "17 3976.454093\n", - "23 3541.582194\n", - "25 4032.844186\n", - "27 4118.351772\n", - "29 4087.767826\n", - "34 3183.755249\n", - "35 3418.802274\n", - "39 3519.186468\n", - "51 3398.135365\n", - "52 3223.615957\n", - "60 3445.014718\n", - "61 3505.638864\n", - "64 3515.905786\n", - "65 4028.363185\n", - "67 4159.993943\n", - "83 3348.16883\n", - "85 3485.050273\n", - "93 4172.874548\n", - "104 3299.302424\n", - "105 3515.687917\n", - "108 3405.224618\n", - "113 4209.140425\n", - "130 4197.905737\n", - "...\n", + "

25 rows × 7 columns

\n", + "[67 rows x 7 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g onehotencoded_island \\\n", + "penguin_id \n", + "1 3781.402407 [{'index': 3, 'value': 1.0}] \n", + "4 4124.107944 [{'index': 1, 'value': 1.0}] \n", + "8 4670.344196 [{'index': 1, 'value': 1.0}] \n", + "11 3529.417214 [{'index': 2, 'value': 1.0}] \n", + "13 4014.101714 [{'index': 1, 'value': 1.0}] \n", + "15 5212.41288 [{'index': 1, 'value': 1.0}] \n", + "16 4163.595615 [{'index': 3, 'value': 1.0}] \n", + "23 3392.453069 [{'index': 2, 'value': 1.0}] \n", + "34 4698.305397 [{'index': 1, 'value': 1.0}] \n", + "36 4828.226949 [{'index': 1, 'value': 1.0}] \n", + "42 3430.58866 [{'index': 1, 'value': 1.0}] \n", + "48 5314.260221 [{'index': 1, 'value': 1.0}] \n", + "61 5363.205372 [{'index': 1, 'value': 1.0}] \n", + "64 4855.908314 [{'index': 1, 'value': 1.0}] \n", + "65 3413.100524 [{'index': 2, 'value': 1.0}] \n", + "68 3340.219002 [{'index': 3, 'value': 1.0}] \n", + "70 4228.73157 [{'index': 2, 'value': 1.0}] \n", + "72 3811.538478 [{'index': 2, 'value': 1.0}] \n", + "74 4659.770763 [{'index': 1, 'value': 1.0}] \n", + "77 3453.388804 [{'index': 2, 'value': 1.0}] \n", + "81 4766.245033 [{'index': 1, 'value': 1.0}] \n", + "91 4057.807281 [{'index': 2, 'value': 1.0}] \n", + "96 4739.827445 [{'index': 1, 'value': 1.0}] \n", + "105 3394.891976 [{'index': 1, 'value': 1.0}] \n", + "111 3201.493683 [{'index': 1, 'value': 1.0}] \n", "\n", - "[67 rows x 1 columns]" + " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "1 -0.938587 0.748033 \n", + "4 -0.16745 0.899528 \n", + "8 0.453222 -1.877885 \n", + "11 -1.12667 0.697535 \n", + "13 -1.183094 1.404513 \n", + "15 0.867003 -0.766919 \n", + "16 -1.784958 1.959995 \n", + "23 -0.355532 0.647036 \n", + "34 -0.600039 -1.776888 \n", + "36 -0.129833 -1.423399 \n", + "42 -1.615684 -0.514427 \n", + "48 0.415606 -0.716421 \n", + "61 0.396797 -1.170907 \n", + "64 0.434414 -1.120408 \n", + "65 -1.220711 1.051024 \n", + "68 -1.484026 -0.009443 \n", + "70 1.638141 1.404513 \n", + "72 0.829387 0.142052 \n", + "74 -0.242683 -1.524396 \n", + "77 -1.277136 -0.211437 \n", + "81 0.208715 -1.221405 \n", + "91 1.261976 0.647036 \n", + "96 0.246331 -1.322402 \n", + "105 -1.803766 0.445043 \n", + "111 -1.164286 0.697535 \n", + "\n", + " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", + "penguin_id \n", + "1 -1.445145 [{'index': 2, 'value': 1.0}] \n", + "4 -0.284269 [{'index': 2, 'value': 1.0}] \n", + "8 0.658942 [{'index': 1, 'value': 1.0}] \n", + "11 -0.792152 [{'index': 1, 'value': 1.0}] \n", + "13 -0.792152 [{'index': 2, 'value': 1.0}] \n", + "15 0.513833 [{'index': 2, 'value': 1.0}] \n", + "16 -0.211715 [{'index': 2, 'value': 1.0}] \n", + "23 -1.5177 [{'index': 1, 'value': 1.0}] \n", + "34 0.949161 [{'index': 1, 'value': 1.0}] \n", + "36 1.23938 [{'index': 1, 'value': 1.0}] \n", + "42 -0.429379 [{'index': 1, 'value': 1.0}] \n", + "48 1.021716 [{'index': 2, 'value': 1.0}] \n", + "61 1.457044 [{'index': 2, 'value': 1.0}] \n", + "64 1.09427 [{'index': 1, 'value': 1.0}] \n", + "65 -1.445145 [{'index': 1, 'value': 1.0}] \n", + "68 -1.009817 [{'index': 1, 'value': 1.0}] \n", + "70 0.296168 [{'index': 2, 'value': 1.0}] \n", + "72 -0.719598 [{'index': 2, 'value': 1.0}] \n", + "74 0.586387 [{'index': 1, 'value': 1.0}] \n", + "77 -0.647043 [{'index': 1, 'value': 1.0}] \n", + "81 0.804051 [{'index': 1, 'value': 1.0}] \n", + "91 0.005949 [{'index': 2, 'value': 1.0}] \n", + "96 0.731497 [{'index': 1, 'value': 1.0}] \n", + "105 -1.009817 [{'index': 1, 'value': 1.0}] \n", + "111 -2.098138 [{'index': 1, 'value': 1.0}] \n", + "\n", + " onehotencoded_species \n", + "penguin_id \n", + "1 [{'index': 1, 'value': 1.0}] \n", + "4 [{'index': 1, 'value': 1.0}] \n", + "8 [{'index': 3, 'value': 1.0}] \n", + "11 [{'index': 1, 'value': 1.0}] \n", + "13 [{'index': 1, 'value': 1.0}] \n", + "15 [{'index': 3, 'value': 1.0}] \n", + "16 [{'index': 1, 'value': 1.0}] \n", + "23 [{'index': 1, 'value': 1.0}] \n", + "34 [{'index': 3, 'value': 1.0}] \n", + "36 [{'index': 3, 'value': 1.0}] \n", + "42 [{'index': 1, 'value': 1.0}] \n", + "48 [{'index': 3, 'value': 1.0}] \n", + "61 [{'index': 3, 'value': 1.0}] \n", + "64 [{'index': 3, 'value': 1.0}] \n", + "65 [{'index': 1, 'value': 1.0}] \n", + "68 [{'index': 1, 'value': 1.0}] \n", + "70 [{'index': 2, 'value': 1.0}] \n", + "72 [{'index': 2, 'value': 1.0}] \n", + "74 [{'index': 3, 'value': 1.0}] \n", + "77 [{'index': 1, 'value': 1.0}] \n", + "81 [{'index': 3, 'value': 1.0}] \n", + "91 [{'index': 2, 'value': 1.0}] \n", + "96 [{'index': 3, 'value': 1.0}] \n", + "105 [{'index': 1, 'value': 1.0}] \n", + "111 [{'index': 1, 'value': 1.0}] \n", + "\n", + "[67 rows x 7 columns]" ] }, - "execution_count": 25, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -2423,18 +2554,16 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d7a16e04253a42b7a5ce247d8f63b656", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 027042f1-9a18-43d8-a378-ab9410e395b1 is DONE. 23.5 kB processed.
Open Job" + ], "text/plain": [ - "HTML(value='Query job 6f19614c-82c0-4f8b-b74b-9d91a894efdd is RUNNING. " ] }, "metadata": {}, @@ -2442,13 +2571,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4a99ac15431e433595de1040872a4558", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 6c8484a0-a504-4e50-93d6-3d247c9ff558 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 51899e2d-f6ef-4e62-98b6-c11550f74f4b is RUNNING. " ] }, "metadata": {}, @@ -2456,13 +2583,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "90909b620e084f59b0f9da266257593f", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e81ca2de-df2e-41ec-af86-14f8dcec1b44 is DONE. 6.2 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 44d3fddc-74bc-4de0-a458-2c73b38f74fb is RUNNING. " ] }, "metadata": {}, @@ -2470,13 +2595,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2a9c2c05041a4fb691809bab5310bb05", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 3e6d413c-f8c4-4390-95eb-3a1f5bc59aed is DONE. 536 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 33584475-f02b-4c98-9a51-e29996f4f950 is RUNNING. " ] }, "metadata": {}, @@ -2484,13 +2607,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6b0677c228d54b409c66e5dfa98d7e00", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e448220d-0c50-45b7-bcbe-d1159b3d18ce is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job df25ba49-280e-424d-a357-dde71a9b35dd is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -2498,13 +2619,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "379ae6497fb34f969d21b2cd664e8bfa", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e167a234-828d-4f05-8654-63cf97e50ba3 is DONE. 10.2 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 6f92a04e-af7e-41d6-9303-6366c1751294 is RUNNING. " ] }, "metadata": {}, @@ -2532,152 +2651,452 @@ " \n", " \n", " CENTROID_ID\n", + " NEAREST_CENTROIDS_DISTANCE\n", + " onehotencoded_island\n", + " standard_scaled_culmen_length_mm\n", + " standard_scaled_culmen_depth_mm\n", + " standard_scaled_flipper_length_mm\n", + " onehotencoded_sex\n", + " onehotencoded_species\n", " \n", " \n", " penguin_id\n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", - " 3\n", - " 3\n", - " \n", - " \n", - " 8\n", + " 1\n", " 3\n", + " [{'CENTROID_ID': 3, 'DISTANCE': 1.236380597035...\n", + " [{'index': 3, 'value': 1.0}]\n", + " -0.938587\n", + " 0.748033\n", + " -1.445145\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 17\n", + " 4\n", " 3\n", + " [{'CENTROID_ID': 3, 'DISTANCE': 1.039497631856...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.16745\n", + " 0.899528\n", + " -0.284269\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 23\n", + " 8\n", " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 1.171040485975...\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.453222\n", + " -1.877885\n", + " 0.658942\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 25\n", - " 3\n", + " 11\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 0.969102754012...\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.12667\n", + " 0.697535\n", + " -0.792152\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 27\n", + " 13\n", " 3\n", + " [{'CENTROID_ID': 3, 'DISTANCE': 1.113138945949...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.183094\n", + " 1.404513\n", + " -0.792152\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 29\n", - " 3\n", + " 15\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 1.070996026772...\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.867003\n", + " -0.766919\n", + " 0.513833\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 34\n", + " 16\n", " 3\n", + " [{'CENTROID_ID': 3, 'DISTANCE': 1.780136190720...\n", + " [{'index': 3, 'value': 1.0}]\n", + " -1.784958\n", + " 1.959995\n", + " -0.211715\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 35\n", - " 1\n", + " 23\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 1.382540667483...\n", + " [{'index': 2, 'value': 1.0}]\n", + " -0.355532\n", + " 0.647036\n", + " -1.5177\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 39\n", - " 3\n", + " 34\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 1.598627908302...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.600039\n", + " -1.776888\n", + " 0.949161\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 51\n", + " 36\n", " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 1.095162305190...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.129833\n", + " -1.423399\n", + " 1.23938\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 52\n", - " 3\n", + " 42\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 1.275841743930...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.615684\n", + " -0.514427\n", + " -0.429379\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 60\n", - " 3\n", + " 48\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.882209023196...\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.415606\n", + " -0.716421\n", + " 1.021716\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 61\n", - " 3\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.816202832282...\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.396797\n", + " -1.170907\n", + " 1.457044\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 64\n", " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.735435721625...\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.434414\n", + " -1.120408\n", + " 1.09427\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", " 65\n", - " 1\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 1.292559869148...\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.220711\n", + " 1.051024\n", + " -1.445145\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 67\n", - " 3\n", + " 68\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 0.876430138449...\n", + " [{'index': 3, 'value': 1.0}]\n", + " -1.484026\n", + " -0.009443\n", + " -1.009817\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 83\n", - " 3\n", + " 70\n", + " 4\n", + " [{'CENTROID_ID': 4, 'DISTANCE': 1.314229913955...\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.638141\n", + " 1.404513\n", + " 0.296168\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 85\n", - " 1\n", + " 72\n", + " 4\n", + " [{'CENTROID_ID': 4, 'DISTANCE': 0.938569518009...\n", + " [{'index': 2, 'value': 1.0}]\n", + " 0.829387\n", + " 0.142052\n", + " -0.719598\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 93\n", + " 74\n", " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 1.350320088546...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -0.242683\n", + " -1.524396\n", + " 0.586387\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 104\n", - " 3\n", + " 77\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 0.904806634663...\n", + " [{'index': 2, 'value': 1.0}]\n", + " -1.277136\n", + " -0.211437\n", + " -0.647043\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", - " 105\n", + " 81\n", " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.919082578073...\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.208715\n", + " -1.221405\n", + " 0.804051\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 108\n", - " 3\n", + " 91\n", + " 4\n", + " [{'CENTROID_ID': 4, 'DISTANCE': 0.760360038086...\n", + " [{'index': 2, 'value': 1.0}]\n", + " 1.261976\n", + " 0.647036\n", + " 0.005949\n", + " [{'index': 2, 'value': 1.0}]\n", + " [{'index': 2, 'value': 1.0}]\n", " \n", " \n", - " 113\n", - " 3\n", + " 96\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.950188657227...\n", + " [{'index': 1, 'value': 1.0}]\n", + " 0.246331\n", + " -1.322402\n", + " 0.731497\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 3, 'value': 1.0}]\n", " \n", " \n", - " 130\n", - " 1\n", + " 105\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 1.101316467029...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.803766\n", + " 0.445043\n", + " -1.009817\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", + " \n", + " \n", + " 111\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 1.549061068385...\n", + " [{'index': 1, 'value': 1.0}]\n", + " -1.164286\n", + " 0.697535\n", + " -2.098138\n", + " [{'index': 1, 'value': 1.0}]\n", + " [{'index': 1, 'value': 1.0}]\n", " \n", " \n", "\n", - "

25 rows × 1 columns

\n", - "[67 rows x 1 columns in total]" + "

25 rows × 8 columns

\n", + "[67 rows x 8 columns in total]" ], "text/plain": [ - " CENTROID_ID\n", - "penguin_id \n", - "3 3\n", - "8 3\n", - "17 3\n", - "23 1\n", - "25 3\n", - "27 3\n", - "29 3\n", - "34 3\n", - "35 1\n", - "39 3\n", - "51 1\n", - "52 3\n", - "60 3\n", - "61 3\n", - "64 1\n", - "65 1\n", - "67 3\n", - "83 3\n", - "85 1\n", - "93 1\n", - "104 3\n", - "105 1\n", - "108 3\n", - "113 3\n", - "130 1\n", - "...\n", + " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", + "penguin_id \n", + "1 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.236380597035... \n", + "4 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.039497631856... \n", + "8 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.171040485975... \n", + "11 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.969102754012... \n", + "13 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.113138945949... \n", + "15 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.070996026772... \n", + "16 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.780136190720... \n", + "23 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.382540667483... \n", + "34 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.598627908302... \n", + "36 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.095162305190... \n", + "42 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.275841743930... \n", + "48 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.882209023196... \n", + "61 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.816202832282... \n", + "64 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.735435721625... \n", + "65 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.292559869148... \n", + "68 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.876430138449... \n", + "70 4 [{'CENTROID_ID': 4, 'DISTANCE': 1.314229913955... \n", + "72 4 [{'CENTROID_ID': 4, 'DISTANCE': 0.938569518009... \n", + "74 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.350320088546... \n", + "77 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.904806634663... \n", + "81 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.919082578073... \n", + "91 4 [{'CENTROID_ID': 4, 'DISTANCE': 0.760360038086... \n", + "96 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.950188657227... \n", + "105 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.101316467029... \n", + "111 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.549061068385... \n", "\n", - "[67 rows x 1 columns]" + " onehotencoded_island standard_scaled_culmen_length_mm \\\n", + "penguin_id \n", + "1 [{'index': 3, 'value': 1.0}] -0.938587 \n", + "4 [{'index': 1, 'value': 1.0}] -0.16745 \n", + "8 [{'index': 1, 'value': 1.0}] 0.453222 \n", + "11 [{'index': 2, 'value': 1.0}] -1.12667 \n", + "13 [{'index': 1, 'value': 1.0}] -1.183094 \n", + "15 [{'index': 1, 'value': 1.0}] 0.867003 \n", + "16 [{'index': 3, 'value': 1.0}] -1.784958 \n", + "23 [{'index': 2, 'value': 1.0}] -0.355532 \n", + "34 [{'index': 1, 'value': 1.0}] -0.600039 \n", + "36 [{'index': 1, 'value': 1.0}] -0.129833 \n", + "42 [{'index': 1, 'value': 1.0}] -1.615684 \n", + "48 [{'index': 1, 'value': 1.0}] 0.415606 \n", + "61 [{'index': 1, 'value': 1.0}] 0.396797 \n", + "64 [{'index': 1, 'value': 1.0}] 0.434414 \n", + "65 [{'index': 2, 'value': 1.0}] -1.220711 \n", + "68 [{'index': 3, 'value': 1.0}] -1.484026 \n", + "70 [{'index': 2, 'value': 1.0}] 1.638141 \n", + "72 [{'index': 2, 'value': 1.0}] 0.829387 \n", + "74 [{'index': 1, 'value': 1.0}] -0.242683 \n", + "77 [{'index': 2, 'value': 1.0}] -1.277136 \n", + "81 [{'index': 1, 'value': 1.0}] 0.208715 \n", + "91 [{'index': 2, 'value': 1.0}] 1.261976 \n", + "96 [{'index': 1, 'value': 1.0}] 0.246331 \n", + "105 [{'index': 1, 'value': 1.0}] -1.803766 \n", + "111 [{'index': 1, 'value': 1.0}] -1.164286 \n", + "\n", + " standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "1 0.748033 \n", + "4 0.899528 \n", + "8 -1.877885 \n", + "11 0.697535 \n", + "13 1.404513 \n", + "15 -0.766919 \n", + "16 1.959995 \n", + "23 0.647036 \n", + "34 -1.776888 \n", + "36 -1.423399 \n", + "42 -0.514427 \n", + "48 -0.716421 \n", + "61 -1.170907 \n", + "64 -1.120408 \n", + "65 1.051024 \n", + "68 -0.009443 \n", + "70 1.404513 \n", + "72 0.142052 \n", + "74 -1.524396 \n", + "77 -0.211437 \n", + "81 -1.221405 \n", + "91 0.647036 \n", + "96 -1.322402 \n", + "105 0.445043 \n", + "111 0.697535 \n", + "\n", + " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", + "penguin_id \n", + "1 -1.445145 [{'index': 2, 'value': 1.0}] \n", + "4 -0.284269 [{'index': 2, 'value': 1.0}] \n", + "8 0.658942 [{'index': 1, 'value': 1.0}] \n", + "11 -0.792152 [{'index': 1, 'value': 1.0}] \n", + "13 -0.792152 [{'index': 2, 'value': 1.0}] \n", + "15 0.513833 [{'index': 2, 'value': 1.0}] \n", + "16 -0.211715 [{'index': 2, 'value': 1.0}] \n", + "23 -1.5177 [{'index': 1, 'value': 1.0}] \n", + "34 0.949161 [{'index': 1, 'value': 1.0}] \n", + "36 1.23938 [{'index': 1, 'value': 1.0}] \n", + "42 -0.429379 [{'index': 1, 'value': 1.0}] \n", + "48 1.021716 [{'index': 2, 'value': 1.0}] \n", + "61 1.457044 [{'index': 2, 'value': 1.0}] \n", + "64 1.09427 [{'index': 1, 'value': 1.0}] \n", + "65 -1.445145 [{'index': 1, 'value': 1.0}] \n", + "68 -1.009817 [{'index': 1, 'value': 1.0}] \n", + "70 0.296168 [{'index': 2, 'value': 1.0}] \n", + "72 -0.719598 [{'index': 2, 'value': 1.0}] \n", + "74 0.586387 [{'index': 1, 'value': 1.0}] \n", + "77 -0.647043 [{'index': 1, 'value': 1.0}] \n", + "81 0.804051 [{'index': 1, 'value': 1.0}] \n", + "91 0.005949 [{'index': 2, 'value': 1.0}] \n", + "96 0.731497 [{'index': 1, 'value': 1.0}] \n", + "105 -1.009817 [{'index': 1, 'value': 1.0}] \n", + "111 -2.098138 [{'index': 1, 'value': 1.0}] \n", + "\n", + " onehotencoded_species \n", + "penguin_id \n", + "1 [{'index': 1, 'value': 1.0}] \n", + "4 [{'index': 1, 'value': 1.0}] \n", + "8 [{'index': 3, 'value': 1.0}] \n", + "11 [{'index': 1, 'value': 1.0}] \n", + "13 [{'index': 1, 'value': 1.0}] \n", + "15 [{'index': 3, 'value': 1.0}] \n", + "16 [{'index': 1, 'value': 1.0}] \n", + "23 [{'index': 1, 'value': 1.0}] \n", + "34 [{'index': 3, 'value': 1.0}] \n", + "36 [{'index': 3, 'value': 1.0}] \n", + "42 [{'index': 1, 'value': 1.0}] \n", + "48 [{'index': 3, 'value': 1.0}] \n", + "61 [{'index': 3, 'value': 1.0}] \n", + "64 [{'index': 3, 'value': 1.0}] \n", + "65 [{'index': 1, 'value': 1.0}] \n", + "68 [{'index': 1, 'value': 1.0}] \n", + "70 [{'index': 2, 'value': 1.0}] \n", + "72 [{'index': 2, 'value': 1.0}] \n", + "74 [{'index': 3, 'value': 1.0}] \n", + "77 [{'index': 1, 'value': 1.0}] \n", + "81 [{'index': 3, 'value': 1.0}] \n", + "91 [{'index': 2, 'value': 1.0}] \n", + "96 [{'index': 3, 'value': 1.0}] \n", + "105 [{'index': 1, 'value': 1.0}] \n", + "111 [{'index': 1, 'value': 1.0}] \n", + "\n", + "[67 rows x 8 columns]" ] }, - "execution_count": 26, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -2704,7 +3123,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -2721,7 +3140,7 @@ " ('linreg', LinearRegression())])" ] }, - "execution_count": 27, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -2748,18 +3167,16 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "887bf58cebf14bdba95db828390fd33d", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job b11be0d8-e6f1-41cb-8cb2-25a38e7ef311 is DONE. 24.7 kB processed.
Open Job" + ], "text/plain": [ - "HTML(value='Query job ed42cbb3-3d25-47ca-96c5-71a84e426a8c is RUNNING. " ] }, "metadata": {}, @@ -2767,13 +3184,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "24357055792a4eaaa60997fea0f76921", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job f32ea25c-be39-4726-a8f5-604ae83849a6 is DONE. 8.5 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 3fc74930-03b9-4a49-8ed3-c3edc4dd6e51 is RUNNING. " ] }, "metadata": {}, @@ -2781,13 +3196,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "bba878d6d3e345f1a29aea50f7101e8f", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 86e29b78-76f5-4937-8bde-407b99af04a2 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 38a4ce3b-5c2a-4d44-b826-f24529d6500b is RUNNING. " ] }, "metadata": {}, @@ -2795,13 +3208,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4bc2c53aeb7d4a8280f9fbbe373f4b55", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job ca819734-0d41-4d9e-b743-09edae8c7fee is DONE. 29.6 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job ecad776d-77c8-4d94-8186-d5571b512b62 is RUNNING. " ] }, "metadata": {}, @@ -2809,13 +3220,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f4f695cb0a224102b6e26adeb1827981", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 49bb5bed-cc84-47e0-9a90-08ab01e00548 is DONE. 536 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job c9bfc58f-ce2c-47a9-bbc7-b10d9de9b5a6 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -2823,13 +3232,23 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cb1df595006d485288a1060299970e5e", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 1e40a085-2289-47dd-afd8-820413186b9f is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 8fd8036e-3753-433d-975b-c7b42406f648 is RUNNING. " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 60319296-a480-4f51-b7ad-190ac6de963a is DONE. 6.2 kB processed. Open Job" + ], + "text/plain": [ + "" ] }, "metadata": {}, @@ -2857,152 +3276,369 @@ " \n", " \n", " predicted_body_mass_g\n", + " island\n", + " culmen_length_mm\n", + " culmen_depth_mm\n", + " flipper_length_mm\n", + " sex\n", + " species\n", " \n", " \n", " penguin_id\n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", - " 3\n", - " 3394.116212\n", - " \n", - " \n", - " 8\n", - " 4048.683645\n", + " 1\n", + " 3781.396682\n", + " Torgersen\n", + " 39.1\n", + " 18.7\n", + " 181.0\n", + " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 17\n", - " 3976.452358\n", + " 4\n", + " 4124.102574\n", + " Biscoe\n", + " 43.2\n", + " 19.0\n", + " 197.0\n", + " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 23\n", - " 3541.580346\n", + " 8\n", + " 4670.338389\n", + " Biscoe\n", + " 46.5\n", + " 13.5\n", + " 210.0\n", + " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 25\n", - " 4032.842027\n", + " 11\n", + " 3529.411644\n", + " Dream\n", + " 38.1\n", + " 18.6\n", + " 190.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 27\n", - " 4118.34983\n", + " 13\n", + " 4014.09632\n", + " Biscoe\n", + " 37.8\n", + " 20.0\n", + " 190.0\n", + " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 29\n", - " 4087.765797\n", + " 15\n", + " 5212.407319\n", + " Biscoe\n", + " 48.7\n", + " 15.7\n", + " 208.0\n", + " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 34\n", - " 3183.75379\n", + " 16\n", + " 4163.590502\n", + " Torgersen\n", + " 34.6\n", + " 21.1\n", + " 198.0\n", + " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 35\n", - " 3418.800633\n", + " 23\n", + " 3392.44731\n", + " Dream\n", + " 42.2\n", + " 18.5\n", + " 180.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 39\n", - " 3519.18471\n", + " 34\n", + " 4698.299674\n", + " Biscoe\n", + " 40.9\n", + " 13.7\n", + " 214.0\n", + " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 51\n", - " 3398.133564\n", + " 36\n", + " 4828.221398\n", + " Biscoe\n", + " 43.4\n", + " 14.4\n", + " 218.0\n", + " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 52\n", - " 3223.614107\n", + " 42\n", + " 3430.582874\n", + " Biscoe\n", + " 35.5\n", + " 16.2\n", + " 195.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 60\n", - " 3445.012713\n", + " 48\n", + " 5314.254798\n", + " Biscoe\n", + " 46.3\n", + " 15.8\n", + " 215.0\n", + " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", " 61\n", - " 3505.637004\n", + " 5363.19995\n", + " Biscoe\n", + " 46.2\n", + " 14.9\n", + " 221.0\n", + " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", " 64\n", - " 3515.903779\n", + " 4855.90281\n", + " Biscoe\n", + " 46.4\n", + " 15.0\n", + " 216.0\n", + " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", " 65\n", - " 4028.361259\n", + " 3413.094869\n", + " Dream\n", + " 37.6\n", + " 19.3\n", + " 181.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 67\n", - " 4159.991956\n", + " 68\n", + " 3340.213193\n", + " Torgersen\n", + " 36.2\n", + " 17.2\n", + " 187.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 83\n", - " 3348.167212\n", + " 70\n", + " 4228.726508\n", + " Dream\n", + " 52.8\n", + " 20.0\n", + " 205.0\n", + " MALE\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", " \n", " \n", - " 85\n", - " 3485.048557\n", + " 72\n", + " 3811.532821\n", + " Dream\n", + " 48.5\n", + " 17.5\n", + " 191.0\n", + " MALE\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", " \n", " \n", - " 93\n", - " 4172.872284\n", + " 74\n", + " 4659.765013\n", + " Biscoe\n", + " 42.8\n", + " 14.2\n", + " 209.0\n", + " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 104\n", - " 3299.300454\n", + " 77\n", + " 3453.383042\n", + " Dream\n", + " 37.3\n", + " 16.8\n", + " 192.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 105\n", - " 3515.68617\n", + " 81\n", + " 4766.239424\n", + " Biscoe\n", + " 45.2\n", + " 14.8\n", + " 212.0\n", + " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 108\n", - " 3405.222757\n", + " 91\n", + " 4057.801947\n", + " Dream\n", + " 50.8\n", + " 18.5\n", + " 201.0\n", + " MALE\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", + " \n", + " \n", + " 96\n", + " 4739.821792\n", + " Biscoe\n", + " 45.4\n", + " 14.6\n", + " 211.0\n", + " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", " \n", " \n", - " 113\n", - " 4209.13832\n", + " 105\n", + " 3394.886275\n", + " Biscoe\n", + " 34.5\n", + " 18.1\n", + " 187.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", - " 130\n", - " 4197.90382\n", + " 111\n", + " 3201.48777\n", + " Biscoe\n", + " 37.9\n", + " 18.6\n", + " 172.0\n", + " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " \n", " \n", "\n", - "

25 rows × 1 columns

\n", - "[67 rows x 1 columns in total]" - ], - "text/plain": [ - " predicted_body_mass_g\n", - "penguin_id \n", - "3 3394.116212\n", - "8 4048.683645\n", - "17 3976.452358\n", - "23 3541.580346\n", - "25 4032.842027\n", - "27 4118.34983\n", - "29 4087.765797\n", - "34 3183.75379\n", - "35 3418.800633\n", - "39 3519.18471\n", - "51 3398.133564\n", - "52 3223.614107\n", - "60 3445.012713\n", - "61 3505.637004\n", - "64 3515.903779\n", - "65 4028.361259\n", - "67 4159.991956\n", - "83 3348.167212\n", - "85 3485.048557\n", - "93 4172.872284\n", - "104 3299.300454\n", - "105 3515.68617\n", - "108 3405.222757\n", - "113 4209.13832\n", - "130 4197.90382\n", - "...\n", + "

25 rows × 7 columns

\n", + "[67 rows x 7 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g island culmen_length_mm \\\n", + "penguin_id \n", + "1 3781.396682 Torgersen 39.1 \n", + "4 4124.102574 Biscoe 43.2 \n", + "8 4670.338389 Biscoe 46.5 \n", + "11 3529.411644 Dream 38.1 \n", + "13 4014.09632 Biscoe 37.8 \n", + "15 5212.407319 Biscoe 48.7 \n", + "16 4163.590502 Torgersen 34.6 \n", + "23 3392.44731 Dream 42.2 \n", + "34 4698.299674 Biscoe 40.9 \n", + "36 4828.221398 Biscoe 43.4 \n", + "42 3430.582874 Biscoe 35.5 \n", + "48 5314.254798 Biscoe 46.3 \n", + "61 5363.19995 Biscoe 46.2 \n", + "64 4855.90281 Biscoe 46.4 \n", + "65 3413.094869 Dream 37.6 \n", + "68 3340.213193 Torgersen 36.2 \n", + "70 4228.726508 Dream 52.8 \n", + "72 3811.532821 Dream 48.5 \n", + "74 4659.765013 Biscoe 42.8 \n", + "77 3453.383042 Dream 37.3 \n", + "81 4766.239424 Biscoe 45.2 \n", + "91 4057.801947 Dream 50.8 \n", + "96 4739.821792 Biscoe 45.4 \n", + "105 3394.886275 Biscoe 34.5 \n", + "111 3201.48777 Biscoe 37.9 \n", "\n", - "[67 rows x 1 columns]" + " culmen_depth_mm flipper_length_mm sex \\\n", + "penguin_id \n", + "1 18.7 181.0 MALE \n", + "4 19.0 197.0 MALE \n", + "8 13.5 210.0 FEMALE \n", + "11 18.6 190.0 FEMALE \n", + "13 20.0 190.0 MALE \n", + "15 15.7 208.0 MALE \n", + "16 21.1 198.0 MALE \n", + "23 18.5 180.0 FEMALE \n", + "34 13.7 214.0 FEMALE \n", + "36 14.4 218.0 FEMALE \n", + "42 16.2 195.0 FEMALE \n", + "48 15.8 215.0 MALE \n", + "61 14.9 221.0 MALE \n", + "64 15.0 216.0 FEMALE \n", + "65 19.3 181.0 FEMALE \n", + "68 17.2 187.0 FEMALE \n", + "70 20.0 205.0 MALE \n", + "72 17.5 191.0 MALE \n", + "74 14.2 209.0 FEMALE \n", + "77 16.8 192.0 FEMALE \n", + "81 14.8 212.0 FEMALE \n", + "91 18.5 201.0 MALE \n", + "96 14.6 211.0 FEMALE \n", + "105 18.1 187.0 FEMALE \n", + "111 18.6 172.0 FEMALE \n", + "\n", + " species \n", + "penguin_id \n", + "1 Adelie Penguin (Pygoscelis adeliae) \n", + "4 Adelie Penguin (Pygoscelis adeliae) \n", + "8 Gentoo penguin (Pygoscelis papua) \n", + "11 Adelie Penguin (Pygoscelis adeliae) \n", + "13 Adelie Penguin (Pygoscelis adeliae) \n", + "15 Gentoo penguin (Pygoscelis papua) \n", + "16 Adelie Penguin (Pygoscelis adeliae) \n", + "23 Adelie Penguin (Pygoscelis adeliae) \n", + "34 Gentoo penguin (Pygoscelis papua) \n", + "36 Gentoo penguin (Pygoscelis papua) \n", + "42 Adelie Penguin (Pygoscelis adeliae) \n", + "48 Gentoo penguin (Pygoscelis papua) \n", + "61 Gentoo penguin (Pygoscelis papua) \n", + "64 Gentoo penguin (Pygoscelis papua) \n", + "65 Adelie Penguin (Pygoscelis adeliae) \n", + "68 Adelie Penguin (Pygoscelis adeliae) \n", + "70 Chinstrap penguin (Pygoscelis antarctica) \n", + "72 Chinstrap penguin (Pygoscelis antarctica) \n", + "74 Gentoo penguin (Pygoscelis papua) \n", + "77 Adelie Penguin (Pygoscelis adeliae) \n", + "81 Gentoo penguin (Pygoscelis papua) \n", + "91 Chinstrap penguin (Pygoscelis antarctica) \n", + "96 Gentoo penguin (Pygoscelis papua) \n", + "105 Adelie Penguin (Pygoscelis adeliae) \n", + "111 Adelie Penguin (Pygoscelis adeliae) \n", + "\n", + "[67 rows x 7 columns]" ] }, - "execution_count": 28, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -3034,60 +3670,16 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2d32081be31f44abb8de67e2209d76cd", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 2a043039-670f-4eb8-9cf0-765ee6ed7de6 is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job bc8b2042-1e13-441c-9531-300ed5badb7a is RUNNING. " ] }, "metadata": {}, @@ -3095,13 +3687,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4588ae10de634460bf4026ddd9076351", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 7f1f565b-0f73-4a4e-b33f-8484fa260838 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 5e867182-dd7a-4aff-87a8-f7596e900fd5 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -3109,13 +3699,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8209cf8286a545ebb7b6ef9d002a43a1", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job d4b9d4a6-d75e-46e1-b092-ab58e8aef890 is DONE. 48 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job d4cdb016-8f1e-4960-8ed7-4524ccc5a8a8 is RUNNING. " ] }, "metadata": {}, @@ -3153,12 +3741,12 @@ " \n", " \n", " 0\n", - " 229.48269\n", - " 82962.794947\n", - " 0.004248\n", - " 206.728384\n", - " 0.88633\n", - " 0.892953\n", + " 216.444357\n", + " 72639.698707\n", + " 0.00463\n", + " 170.588356\n", + " 0.896396\n", + " 0.900547\n", " \n", " \n", "\n", @@ -3167,15 +3755,15 @@ ], "text/plain": [ " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - "0 229.48269 82962.794947 0.004248 \n", + "0 216.444357 72639.698707 0.00463 \n", "\n", " median_absolute_error r2_score explained_variance \n", - "0 206.728384 0.88633 0.892953 \n", + "0 170.588356 0.896396 0.900547 \n", "\n", "[1 rows x 6 columns]" ] }, - "execution_count": 29, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -3195,18 +3783,16 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f32692d89f00406499f4ea5aa55268fb", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 73448ee8-698b-435f-b11e-6fe2de3bcd8d is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e57383ef-f043-458b-96c6-893e7c5b0c00 is RUNNING. " ] }, "metadata": {}, @@ -3214,13 +3800,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "72e5f23a99de4a818c8493b8b4f3854d", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e002f59d-a03c-4ec9-a85a-93adbfd7bd17 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 1a9db485-477b-43e2-94eb-dea7dc21d45d is RUNNING. " ] }, "metadata": {}, @@ -3228,13 +3812,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9d5333a91b504dd9be51c997715530ab", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 4ab1febc-fb55-473a-b295-69e4329cc5f0 is DONE. 30.0 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 4570a563-b8e0-4308-b8cb-c4731491d4f7 is RUNNING. " ] }, "metadata": {}, @@ -3243,10 +3825,10 @@ { "data": { "text/plain": [ - "0.8863300923278365" + "0.8963962044533755" ] }, - "execution_count": 30, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -3254,7 +3836,7 @@ "source": [ "from bigframes.ml.metrics import r2_score\n", "\n", - "r2_score(y_test, predicted_y_test)" + "r2_score(y_test, predicted_y_test[\"predicted_body_mass_g\"])" ] }, { @@ -3274,57 +3856,9 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 15, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fbc4a70f31d4465b974a7f7c9cc97731", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Copy job c2413be4-6972-4e36-8234-5063628b6d71 is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job 31a5b656-000e-4238-9fd9-c6e644ca298f is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -67,13 +37,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0f25faa156584cc59dda9b0e60f72534", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 12e0f983-695e-4903-8ff1-2f353d7e8cba is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job d8eed0ca-7ce9-4ed8-a592-e16af9f9db8d is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -113,250 +81,250 @@ " \n", " 0\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 36.6\n", - " 18.4\n", - " 184.0\n", - " 3475.0\n", - " FEMALE\n", + " Biscoe\n", + " 40.1\n", + " 18.9\n", + " 188.0\n", + " 4300.0\n", + " MALE\n", " \n", " \n", " 1\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 39.8\n", - " 19.1\n", - " 184.0\n", - " 4650.0\n", + " Torgersen\n", + " 39.1\n", + " 18.7\n", + " 181.0\n", + " 3750.0\n", " MALE\n", " \n", " \n", " 2\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.9\n", - " 18.9\n", - " 184.0\n", - " 3900.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 47.4\n", + " 14.6\n", + " 212.0\n", + " 4725.0\n", + " FEMALE\n", " \n", " \n", " 3\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 46.5\n", - " 17.9\n", - " 192.0\n", - " 3500.0\n", + " 42.5\n", + " 16.7\n", + " 187.0\n", + " 3350.0\n", " FEMALE\n", " \n", " \n", " 4\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.3\n", - " 16.8\n", - " 192.0\n", - " 3000.0\n", - " FEMALE\n", + " Biscoe\n", + " 43.2\n", + " 19.0\n", + " 197.0\n", + " 4775.0\n", + " MALE\n", " \n", " \n", " 5\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 43.2\n", - " 18.5\n", - " 192.0\n", - " 4100.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.7\n", + " 15.3\n", + " 219.0\n", + " 5200.0\n", " MALE\n", " \n", " \n", " 6\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 46.9\n", - " 16.6\n", - " 192.0\n", - " 2700.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 41.3\n", + " 21.1\n", + " 195.0\n", + " 4400.0\n", + " MALE\n", " \n", " \n", " 7\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 50.5\n", - " 18.4\n", - " 200.0\n", - " 3400.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 45.2\n", + " 13.8\n", + " 215.0\n", + " 4750.0\n", " FEMALE\n", " \n", " \n", " 8\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 49.5\n", - " 19.0\n", - " 200.0\n", - " 3800.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.5\n", + " 13.5\n", + " 210.0\n", + " 4550.0\n", + " FEMALE\n", " \n", " \n", " 9\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.2\n", - " 20.1\n", - " 200.0\n", - " 3975.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 50.5\n", + " 15.2\n", + " 216.0\n", + " 5000.0\n", + " FEMALE\n", " \n", " \n", " 10\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.8\n", - " 18.9\n", - " 208.0\n", - " 4300.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 48.2\n", + " 15.6\n", + " 221.0\n", + " 5100.0\n", " MALE\n", " \n", " \n", " 11\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 39.0\n", - " 18.7\n", - " 185.0\n", - " 3650.0\n", - " MALE\n", + " 38.1\n", + " 18.6\n", + " 190.0\n", + " 3700.0\n", + " FEMALE\n", " \n", " \n", " 12\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.0\n", - " 16.9\n", - " 185.0\n", - " 3000.0\n", - " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 50.7\n", + " 15.0\n", + " 223.0\n", + " 5550.0\n", + " MALE\n", " \n", " \n", " 13\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 47.0\n", - " 17.3\n", - " 185.0\n", - " 3700.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 37.8\n", + " 20.0\n", + " 190.0\n", + " 4250.0\n", + " MALE\n", " \n", " \n", " 14\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 34.0\n", - " 17.1\n", - " 185.0\n", - " 3400.0\n", + " Biscoe\n", + " 35.0\n", + " 17.9\n", + " 190.0\n", + " 3450.0\n", " FEMALE\n", " \n", " \n", " 15\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.0\n", - " 16.5\n", - " 185.0\n", - " 3400.0\n", - " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 48.7\n", + " 15.7\n", + " 208.0\n", + " 5350.0\n", + " MALE\n", " \n", " \n", " 16\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 45.7\n", - " 17.3\n", - " 193.0\n", - " 3600.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Torgersen\n", + " 34.6\n", + " 21.1\n", + " 198.0\n", + " 4400.0\n", + " MALE\n", " \n", " \n", " 17\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 50.6\n", - " 19.4\n", - " 193.0\n", - " 3800.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.8\n", + " 15.4\n", + " 215.0\n", + " 5150.0\n", " MALE\n", " \n", " \n", " 18\n", - " Adelie Penguin (Pygoscelis adeliae)\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 39.7\n", - " 17.9\n", - " 193.0\n", - " 4250.0\n", + " 50.3\n", + " 20.0\n", + " 197.0\n", + " 3300.0\n", " MALE\n", " \n", " \n", " 19\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 37.8\n", + " 37.2\n", " 18.1\n", - " 193.0\n", - " 3750.0\n", + " 178.0\n", + " 3900.0\n", " MALE\n", " \n", " \n", " 20\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 46.6\n", - " 17.8\n", - " 193.0\n", - " 3800.0\n", - " FEMALE\n", + " 51.0\n", + " 18.8\n", + " 203.0\n", + " 4100.0\n", + " MALE\n", " \n", " \n", " 21\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 51.3\n", - " 19.2\n", - " 193.0\n", - " 3650.0\n", - " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 40.5\n", + " 17.9\n", + " 187.0\n", + " 3200.0\n", + " FEMALE\n", " \n", " \n", " 22\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.2\n", - " 17.1\n", - " 193.0\n", - " 3400.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 45.5\n", + " 13.9\n", + " 210.0\n", + " 4200.0\n", " FEMALE\n", " \n", " \n", " 23\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 36.8\n", + " 42.2\n", " 18.5\n", - " 193.0\n", - " 3500.0\n", + " 180.0\n", + " 3550.0\n", " FEMALE\n", " \n", " \n", " 24\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 49.6\n", - " 18.2\n", - " 193.0\n", + " 51.7\n", + " 20.3\n", + " 194.0\n", " 3775.0\n", " MALE\n", " \n", @@ -366,65 +334,65 @@ "[344 rows x 7 columns in total]" ], "text/plain": [ - " species island culmen_length_mm \\\n", - "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", - "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", - "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", - "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", - "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", - "5 Adelie Penguin (Pygoscelis adeliae) Dream 43.2 \n", - "6 Chinstrap penguin (Pygoscelis antarctica) Dream 46.9 \n", - "7 Chinstrap penguin (Pygoscelis antarctica) Dream 50.5 \n", - "8 Chinstrap penguin (Pygoscelis antarctica) Dream 49.5 \n", - "9 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "10 Adelie Penguin (Pygoscelis adeliae) Dream 40.8 \n", - "11 Adelie Penguin (Pygoscelis adeliae) Dream 39.0 \n", - "12 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "13 Chinstrap penguin (Pygoscelis antarctica) Dream 47.0 \n", - "14 Adelie Penguin (Pygoscelis adeliae) Dream 34.0 \n", - "15 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "16 Chinstrap penguin (Pygoscelis antarctica) Dream 45.7 \n", - "17 Chinstrap penguin (Pygoscelis antarctica) Dream 50.6 \n", - "18 Adelie Penguin (Pygoscelis adeliae) Dream 39.7 \n", - "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.8 \n", - "20 Chinstrap penguin (Pygoscelis antarctica) Dream 46.6 \n", - "21 Chinstrap penguin (Pygoscelis antarctica) Dream 51.3 \n", - "22 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "23 Adelie Penguin (Pygoscelis adeliae) Dream 36.8 \n", - "24 Chinstrap penguin (Pygoscelis antarctica) Dream 49.6 \n", + " species island culmen_length_mm \\\n", + "0 Adelie Penguin (Pygoscelis adeliae) Biscoe 40.1 \n", + "1 Adelie Penguin (Pygoscelis adeliae) Torgersen 39.1 \n", + "2 Gentoo penguin (Pygoscelis papua) Biscoe 47.4 \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream 42.5 \n", + "4 Adelie Penguin (Pygoscelis adeliae) Biscoe 43.2 \n", + "5 Gentoo penguin (Pygoscelis papua) Biscoe 46.7 \n", + "6 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.3 \n", + "7 Gentoo penguin (Pygoscelis papua) Biscoe 45.2 \n", + "8 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "9 Gentoo penguin (Pygoscelis papua) Biscoe 50.5 \n", + "10 Gentoo penguin (Pygoscelis papua) Biscoe 48.2 \n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream 38.1 \n", + "12 Gentoo penguin (Pygoscelis papua) Biscoe 50.7 \n", + "13 Adelie Penguin (Pygoscelis adeliae) Biscoe 37.8 \n", + "14 Adelie Penguin (Pygoscelis adeliae) Biscoe 35.0 \n", + "15 Gentoo penguin (Pygoscelis papua) Biscoe 48.7 \n", + "16 Adelie Penguin (Pygoscelis adeliae) Torgersen 34.6 \n", + "17 Gentoo penguin (Pygoscelis papua) Biscoe 46.8 \n", + "18 Chinstrap penguin (Pygoscelis antarctica) Dream 50.3 \n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.2 \n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream 51.0 \n", + "21 Adelie Penguin (Pygoscelis adeliae) Biscoe 40.5 \n", + "22 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream 42.2 \n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream 51.7 \n", "\n", " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "0 18.4 184.0 3475.0 FEMALE \n", - "1 19.1 184.0 4650.0 MALE \n", - "2 18.9 184.0 3900.0 MALE \n", - "3 17.9 192.0 3500.0 FEMALE \n", - "4 16.8 192.0 3000.0 FEMALE \n", - "5 18.5 192.0 4100.0 MALE \n", - "6 16.6 192.0 2700.0 FEMALE \n", - "7 18.4 200.0 3400.0 FEMALE \n", - "8 19.0 200.0 3800.0 MALE \n", - "9 20.1 200.0 3975.0 MALE \n", - "10 18.9 208.0 4300.0 MALE \n", - "11 18.7 185.0 3650.0 MALE \n", - "12 16.9 185.0 3000.0 FEMALE \n", - "13 17.3 185.0 3700.0 FEMALE \n", - "14 17.1 185.0 3400.0 FEMALE \n", - "15 16.5 185.0 3400.0 FEMALE \n", - "16 17.3 193.0 3600.0 FEMALE \n", - "17 19.4 193.0 3800.0 MALE \n", - "18 17.9 193.0 4250.0 MALE \n", - "19 18.1 193.0 3750.0 MALE \n", - "20 17.8 193.0 3800.0 FEMALE \n", - "21 19.2 193.0 3650.0 MALE \n", - "22 17.1 193.0 3400.0 FEMALE \n", - "23 18.5 193.0 3500.0 FEMALE \n", - "24 18.2 193.0 3775.0 MALE \n", + "0 18.9 188.0 4300.0 MALE \n", + "1 18.7 181.0 3750.0 MALE \n", + "2 14.6 212.0 4725.0 FEMALE \n", + "3 16.7 187.0 3350.0 FEMALE \n", + "4 19.0 197.0 4775.0 MALE \n", + "5 15.3 219.0 5200.0 MALE \n", + "6 21.1 195.0 4400.0 MALE \n", + "7 13.8 215.0 4750.0 FEMALE \n", + "8 13.5 210.0 4550.0 FEMALE \n", + "9 15.2 216.0 5000.0 FEMALE \n", + "10 15.6 221.0 5100.0 MALE \n", + "11 18.6 190.0 3700.0 FEMALE \n", + "12 15.0 223.0 5550.0 MALE \n", + "13 20.0 190.0 4250.0 MALE \n", + "14 17.9 190.0 3450.0 FEMALE \n", + "15 15.7 208.0 5350.0 MALE \n", + "16 21.1 198.0 4400.0 MALE \n", + "17 15.4 215.0 5150.0 MALE \n", + "18 20.0 197.0 3300.0 MALE \n", + "19 18.1 178.0 3900.0 MALE \n", + "20 18.8 203.0 4100.0 MALE \n", + "21 17.9 187.0 3200.0 FEMALE \n", + "22 13.9 210.0 4200.0 FEMALE \n", + "23 18.5 180.0 3550.0 FEMALE \n", + "24 20.3 194.0 3775.0 MALE \n", "...\n", "\n", "[344 rows x 7 columns]" ] }, - "execution_count": 12, + "execution_count": 1, "metadata": {}, "output_type": "execute_result" } @@ -450,32 +418,16 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 2, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a9ad907fa6e64a61a9dce420bc7d2beb", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 3537a10a-641a-4d40-ae47-449c641b1bc5 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 34101409-7c65-4045-ad52-c6ba24dc9cbb is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -483,13 +435,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "514e68d5b0b4452a9ccdff947848541a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 2af0b0d6-c11b-499e-8d25-a2c628b2853b is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 74190ac2-21a2-47b0-bc21-ef5373565f17 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -527,294 +477,294 @@ " \n", " \n", " 0\n", - " Dream\n", - " 36.6\n", - " 18.4\n", - " 184.0\n", - " 3475.0\n", - " FEMALE\n", - " \n", - " \n", - " 1\n", - " Dream\n", - " 39.8\n", - " 19.1\n", - " 184.0\n", - " 4650.0\n", + " Biscoe\n", + " 40.1\n", + " 18.9\n", + " 188.0\n", + " 4300.0\n", " MALE\n", " \n", " \n", - " 2\n", - " Dream\n", - " 40.9\n", - " 18.9\n", - " 184.0\n", - " 3900.0\n", + " 1\n", + " Torgersen\n", + " 39.1\n", + " 18.7\n", + " 181.0\n", + " 3750.0\n", " MALE\n", " \n", " \n", " 4\n", - " Dream\n", - " 37.3\n", - " 16.8\n", - " 192.0\n", - " 3000.0\n", - " FEMALE\n", - " \n", - " \n", - " 5\n", - " Dream\n", + " Biscoe\n", " 43.2\n", - " 18.5\n", - " 192.0\n", - " 4100.0\n", - " MALE\n", - " \n", - " \n", - " 9\n", - " Dream\n", - " 40.2\n", - " 20.1\n", - " 200.0\n", - " 3975.0\n", + " 19.0\n", + " 197.0\n", + " 4775.0\n", " MALE\n", " \n", " \n", - " 10\n", - " Dream\n", - " 40.8\n", - " 18.9\n", - " 208.0\n", - " 4300.0\n", + " 6\n", + " Biscoe\n", + " 41.3\n", + " 21.1\n", + " 195.0\n", + " 4400.0\n", " MALE\n", " \n", " \n", " 11\n", " Dream\n", - " 39.0\n", - " 18.7\n", - " 185.0\n", - " 3650.0\n", - " MALE\n", - " \n", - " \n", - " 12\n", - " Dream\n", - " 37.0\n", - " 16.9\n", - " 185.0\n", - " 3000.0\n", + " 38.1\n", + " 18.6\n", + " 190.0\n", + " 3700.0\n", " FEMALE\n", " \n", " \n", - " 14\n", - " Dream\n", - " 34.0\n", - " 17.1\n", - " 185.0\n", - " 3400.0\n", - " FEMALE\n", + " 13\n", + " Biscoe\n", + " 37.8\n", + " 20.0\n", + " 190.0\n", + " 4250.0\n", + " MALE\n", " \n", " \n", - " 15\n", - " Dream\n", - " 37.0\n", - " 16.5\n", - " 185.0\n", - " 3400.0\n", + " 14\n", + " Biscoe\n", + " 35.0\n", + " 17.9\n", + " 190.0\n", + " 3450.0\n", " FEMALE\n", " \n", " \n", - " 18\n", - " Dream\n", - " 39.7\n", - " 17.9\n", - " 193.0\n", - " 4250.0\n", + " 16\n", + " Torgersen\n", + " 34.6\n", + " 21.1\n", + " 198.0\n", + " 4400.0\n", " MALE\n", " \n", " \n", " 19\n", " Dream\n", - " 37.8\n", + " 37.2\n", " 18.1\n", - " 193.0\n", - " 3750.0\n", + " 178.0\n", + " 3900.0\n", " MALE\n", " \n", " \n", - " 22\n", - " Dream\n", - " 40.2\n", - " 17.1\n", - " 193.0\n", - " 3400.0\n", + " 21\n", + " Biscoe\n", + " 40.5\n", + " 17.9\n", + " 187.0\n", + " 3200.0\n", " FEMALE\n", " \n", " \n", " 23\n", " Dream\n", - " 36.8\n", + " 42.2\n", " 18.5\n", - " 193.0\n", - " 3500.0\n", + " 180.0\n", + " 3550.0\n", " FEMALE\n", " \n", " \n", - " 26\n", + " 30\n", " Dream\n", - " 41.5\n", - " 18.5\n", - " 201.0\n", - " 4000.0\n", + " 39.2\n", + " 21.1\n", + " 196.0\n", + " 4150.0\n", " MALE\n", " \n", " \n", - " 31\n", - " Dream\n", - " 33.1\n", - " 16.1\n", - " 178.0\n", - " 2900.0\n", - " FEMALE\n", + " 32\n", + " Torgersen\n", + " 42.9\n", + " 17.6\n", + " 196.0\n", + " 4700.0\n", + " MALE\n", " \n", " \n", - " 32\n", + " 38\n", " Dream\n", - " 37.2\n", - " 18.1\n", - " 178.0\n", + " 41.1\n", + " 17.5\n", + " 190.0\n", " 3900.0\n", " MALE\n", " \n", " \n", - " 33\n", - " Dream\n", - " 39.5\n", - " 16.7\n", - " 178.0\n", - " 3250.0\n", + " 40\n", + " Torgersen\n", + " 38.6\n", + " 21.2\n", + " 191.0\n", + " 3800.0\n", + " MALE\n", + " \n", + " \n", + " 42\n", + " Biscoe\n", + " 35.5\n", + " 16.2\n", + " 195.0\n", + " 3350.0\n", " FEMALE\n", " \n", " \n", - " 35\n", + " 44\n", " Dream\n", - " 36.0\n", - " 18.5\n", + " 39.2\n", + " 18.6\n", + " 190.0\n", + " 4250.0\n", + " MALE\n", + " \n", + " \n", + " 45\n", + " Torgersen\n", + " 35.2\n", + " 15.9\n", " 186.0\n", - " 3100.0\n", + " 3050.0\n", " FEMALE\n", " \n", " \n", - " 36\n", + " 46\n", " Dream\n", + " 43.2\n", + " 18.5\n", + " 192.0\n", + " 4100.0\n", + " MALE\n", + " \n", + " \n", + " 49\n", + " Biscoe\n", " 39.6\n", - " 18.1\n", + " 17.7\n", " 186.0\n", - " 4450.0\n", - " MALE\n", + " 3500.0\n", + " FEMALE\n", " \n", " \n", - " 38\n", - " Dream\n", - " 41.3\n", + " 53\n", + " Biscoe\n", + " 45.6\n", " 20.3\n", - " 194.0\n", - " 3550.0\n", + " 191.0\n", + " 4600.0\n", " MALE\n", " \n", " \n", - " 41\n", - " Dream\n", - " 35.7\n", - " 18.0\n", - " 202.0\n", - " 3550.0\n", + " 58\n", + " Torgersen\n", + " 40.9\n", + " 16.8\n", + " 191.0\n", + " 3700.0\n", " FEMALE\n", " \n", " \n", - " 51\n", - " Dream\n", - " 38.1\n", - " 17.6\n", - " 187.0\n", - " 3425.0\n", + " 60\n", + " Torgersen\n", + " 40.3\n", + " 18.0\n", + " 195.0\n", + " 3250.0\n", " FEMALE\n", " \n", " \n", - " 53\n", + " 62\n", " Dream\n", " 36.0\n", - " 17.1\n", - " 187.0\n", - " 3700.0\n", + " 18.5\n", + " 186.0\n", + " 3100.0\n", " FEMALE\n", " \n", + " \n", + " 63\n", + " Torgersen\n", + " 39.3\n", + " 20.6\n", + " 190.0\n", + " 3650.0\n", + " MALE\n", + " \n", " \n", "\n", "

25 rows × 6 columns

\n", "[146 rows x 6 columns in total]" ], "text/plain": [ - " island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", - "0 Dream 36.6 18.4 184.0 3475.0 \n", - "1 Dream 39.8 19.1 184.0 4650.0 \n", - "2 Dream 40.9 18.9 184.0 3900.0 \n", - "4 Dream 37.3 16.8 192.0 3000.0 \n", - "5 Dream 43.2 18.5 192.0 4100.0 \n", - "9 Dream 40.2 20.1 200.0 3975.0 \n", - "10 Dream 40.8 18.9 208.0 4300.0 \n", - "11 Dream 39.0 18.7 185.0 3650.0 \n", - "12 Dream 37.0 16.9 185.0 3000.0 \n", - "14 Dream 34.0 17.1 185.0 3400.0 \n", - "15 Dream 37.0 16.5 185.0 3400.0 \n", - "18 Dream 39.7 17.9 193.0 4250.0 \n", - "19 Dream 37.8 18.1 193.0 3750.0 \n", - "22 Dream 40.2 17.1 193.0 3400.0 \n", - "23 Dream 36.8 18.5 193.0 3500.0 \n", - "26 Dream 41.5 18.5 201.0 4000.0 \n", - "31 Dream 33.1 16.1 178.0 2900.0 \n", - "32 Dream 37.2 18.1 178.0 3900.0 \n", - "33 Dream 39.5 16.7 178.0 3250.0 \n", - "35 Dream 36.0 18.5 186.0 3100.0 \n", - "36 Dream 39.6 18.1 186.0 4450.0 \n", - "38 Dream 41.3 20.3 194.0 3550.0 \n", - "41 Dream 35.7 18.0 202.0 3550.0 \n", - "51 Dream 38.1 17.6 187.0 3425.0 \n", - "53 Dream 36.0 17.1 187.0 3700.0 \n", + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "0 Biscoe 40.1 18.9 188.0 \n", + "1 Torgersen 39.1 18.7 181.0 \n", + "4 Biscoe 43.2 19.0 197.0 \n", + "6 Biscoe 41.3 21.1 195.0 \n", + "11 Dream 38.1 18.6 190.0 \n", + "13 Biscoe 37.8 20.0 190.0 \n", + "14 Biscoe 35.0 17.9 190.0 \n", + "16 Torgersen 34.6 21.1 198.0 \n", + "19 Dream 37.2 18.1 178.0 \n", + "21 Biscoe 40.5 17.9 187.0 \n", + "23 Dream 42.2 18.5 180.0 \n", + "30 Dream 39.2 21.1 196.0 \n", + "32 Torgersen 42.9 17.6 196.0 \n", + "38 Dream 41.1 17.5 190.0 \n", + "40 Torgersen 38.6 21.2 191.0 \n", + "42 Biscoe 35.5 16.2 195.0 \n", + "44 Dream 39.2 18.6 190.0 \n", + "45 Torgersen 35.2 15.9 186.0 \n", + "46 Dream 43.2 18.5 192.0 \n", + "49 Biscoe 39.6 17.7 186.0 \n", + "53 Biscoe 45.6 20.3 191.0 \n", + "58 Torgersen 40.9 16.8 191.0 \n", + "60 Torgersen 40.3 18.0 195.0 \n", + "62 Dream 36.0 18.5 186.0 \n", + "63 Torgersen 39.3 20.6 190.0 \n", "\n", - " sex \n", - "0 FEMALE \n", - "1 MALE \n", - "2 MALE \n", - "4 FEMALE \n", - "5 MALE \n", - "9 MALE \n", - "10 MALE \n", - "11 MALE \n", - "12 FEMALE \n", - "14 FEMALE \n", - "15 FEMALE \n", - "18 MALE \n", - "19 MALE \n", - "22 FEMALE \n", - "23 FEMALE \n", - "26 MALE \n", - "31 FEMALE \n", - "32 MALE \n", - "33 FEMALE \n", - "35 FEMALE \n", - "36 MALE \n", - "38 MALE \n", - "41 FEMALE \n", - "51 FEMALE \n", - "53 FEMALE \n", + " body_mass_g sex \n", + "0 4300.0 MALE \n", + "1 3750.0 MALE \n", + "4 4775.0 MALE \n", + "6 4400.0 MALE \n", + "11 3700.0 FEMALE \n", + "13 4250.0 MALE \n", + "14 3450.0 FEMALE \n", + "16 4400.0 MALE \n", + "19 3900.0 MALE \n", + "21 3200.0 FEMALE \n", + "23 3550.0 FEMALE \n", + "30 4150.0 MALE \n", + "32 4700.0 MALE \n", + "38 3900.0 MALE \n", + "40 3800.0 MALE \n", + "42 3350.0 FEMALE \n", + "44 4250.0 MALE \n", + "45 3050.0 FEMALE \n", + "46 4100.0 MALE \n", + "49 3500.0 FEMALE \n", + "53 4600.0 MALE \n", + "58 3700.0 FEMALE \n", + "60 3250.0 FEMALE \n", + "62 3100.0 FEMALE \n", + "63 3650.0 MALE \n", "...\n", "\n", "[146 rows x 6 columns]" ] }, - "execution_count": 13, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -843,18 +793,16 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "81f9aa34c7234bd88b6b7a4bc77d4b4e", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 0808457b-a0df-4a37-b7a5-8885f4a4588c is DONE. 28.9 kB processed.
Open Job" + ], "text/plain": [ - "HTML(value='Query job 288f0daa-a51e-45b4-86bf-d054467c4a99 is DONE. 28.9 kB processed. " ] }, "metadata": {}, @@ -881,7 +829,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -897,7 +845,7 @@ " ('linreg', LinearRegression(fit_intercept=False))])" ] }, - "execution_count": 15, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -936,9 +884,63 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job e9bfa6a5-a53f-4d8b-ae8c-cc8cd55d0947 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d8d553cf-3d36-49aa-b18b-9a05576a1fb0 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 75ef0083-9a4f-4ffb-a6c6-d82974a1659f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Pipeline(steps=[('preproc',\n", + " ColumnTransformer(transformers=[('onehot', OneHotEncoder(),\n", + " ['island', 'species', 'sex']),\n", + " ('scaler', StandardScaler(),\n", + " ['culmen_depth_mm',\n", + " 'culmen_length_mm',\n", + " 'flipper_length_mm'])])),\n", + " ('linreg', LinearRegression(fit_intercept=False))])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "pipeline.fit(X_train, y_train)" ] @@ -953,18 +955,16 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fcf406d36c0d4915b318cd30c0f3df25", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 55c5a9ce-8159-4a1a-99a4-af3a906640ba is DONE. 29.3 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 81196f97-304b-4d77-bb0f-8fc8adb8fe75 is RUNNING. " ] }, "metadata": {}, @@ -972,13 +972,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "41399a6b1d4f45328bacc6c868cefdf6", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 3e41c470-de70-4f13-89d9-c5564d0b2836 is DONE. 232 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job b417f27a-387d-4eb2-8d6d-287327ef0471 is DONE. 232 Bytes processed. " ] }, "metadata": {}, @@ -986,13 +984,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e3c17676eab448c0942c0c32689ba4b5", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job ed2f9042-a737-4d13-bd21-8c3d29cd61a2 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job b7f89a61-d76a-47be-8b83-917d69f255a2 is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -1000,13 +996,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6c903861564b412aad9d9decad26560c", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 815d16b5-0a5d-42be-a766-1cff5b8f22f2 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 9619c393-90b3-4fea-a197-d09389e9486c is DONE. 31.7 kB processed. " ] }, "metadata": {}, @@ -1014,13 +1008,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2c2534cd90e64c81be45753b81b1be46", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 37a38dc6-5073-4544-a1e3-da145a843922 is DONE. 29.4 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e5854451-ffb4-4a28-a25f-3bdd68e9edae is DONE. 32.2 kB processed. " ] }, "metadata": {}, @@ -1029,10 +1021,10 @@ { "data": { "text/plain": [ - "0.6757452736197735" + "0.2655729213572775" ] }, - "execution_count": 17, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -1040,9 +1032,9 @@ "source": [ "from bigframes.ml.metrics import r2_score\n", "\n", - "pred_y = pipeline.predict(X_test)\n", + "y_pred = pipeline.predict(X_test)[\"predicted_body_mass_g\"]\n", "\n", - "r2_score(y_test, pred_y)" + "r2_score(y_test, y_pred)" ] }, { @@ -1055,18 +1047,16 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9295d6a3ff834f7a91a43d3f4ef4a61c", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Load job 7b46750c-70b4-468d-87ba-9f84f579f2a6 is DONE. Open Job" + ], "text/plain": [ - "HTML(value='Load job d4c2f933-3514-4901-bcd7-888ee66eba82 is RUNNING. " ] }, "metadata": {}, @@ -1097,32 +1087,16 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b7eb82b3b5fc4a8e97468070a3e76300", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job e4ffd919-6f69-4382-a7e5-db37c7c1fefa is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job 6b3e3285-79e9-4137-bf3b-7b7185ef76a5 is DONE. 24 Bytes processed. " ] }, "metadata": {}, @@ -1130,13 +1104,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "edc7bc6434bd4be4926626a235aab65a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 207cb787-cf8a-43ea-8e73-644d3f58b11a is DONE. 24 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 173c4194-e194-43d2-8359-7bec83d3c861 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -1144,13 +1116,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "55a8cbd9b1ab47eeab6e1c305847630f", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job c5dc5075-cac0-4947-9e9f-06aa9cc5bd2a is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 53ba2332-590c-488d-9505-23aebaaad9cb is DONE. 48 Bytes processed. " ] }, "metadata": {}, @@ -1158,13 +1128,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "463a5b072148474db629b9346fa3a6d1", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 2ca4a569-7186-48ed-b3e4-004dca704798 is DONE. 282 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 66e4a8e0-4cae-4e9d-86e0-17dc24f6cfbb is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -1192,41 +1160,83 @@ " \n", " \n", " predicted_body_mass_g\n", + " species\n", + " island\n", + " culmen_length_mm\n", + " culmen_depth_mm\n", + " flipper_length_mm\n", + " sex\n", " \n", " \n", " tag_number\n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", " 1633\n", - " 3965.994361\n", + " 4017.203152\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Torgersen\n", + " 39.5\n", + " 18.8\n", + " 196.0\n", + " MALE\n", " \n", " \n", " 1672\n", - " 3246.312058\n", + " 3127.601519\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Torgersen\n", + " 38.5\n", + " 17.2\n", + " 181.0\n", + " FEMALE\n", " \n", " \n", " 1690\n", - " 3456.404062\n", + " 3386.101231\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 37.9\n", + " 18.1\n", + " 188.0\n", + " FEMALE\n", " \n", " \n", "\n", - "

3 rows × 1 columns

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

3 rows × 7 columns

\n", + "[3 rows x 7 columns in total]" ], "text/plain": [ - " predicted_body_mass_g\n", - "tag_number \n", - "1633 3965.994361\n", - "1672 3246.312058\n", - "1690 3456.404062\n", + " predicted_body_mass_g species \\\n", + "tag_number \n", + "1633 4017.203152 Adelie Penguin (Pygoscelis adeliae) \n", + "1672 3127.601519 Adelie Penguin (Pygoscelis adeliae) \n", + "1690 3386.101231 Adelie Penguin (Pygoscelis adeliae) \n", + "\n", + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "tag_number \n", + "1633 Torgersen 39.5 18.8 196.0 \n", + "1672 Torgersen 38.5 17.2 181.0 \n", + "1690 Dream 37.9 18.1 188.0 \n", "\n", - "[3 rows x 1 columns]" + " sex \n", + "tag_number \n", + "1633 MALE \n", + "1672 FEMALE \n", + "1690 FEMALE \n", + "\n", + "[3 rows x 7 columns]" ] }, - "execution_count": 19, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -1240,28 +1250,53 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Save in BigQuery" + "## 6. Save in BigQuery" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "Copy job d1def4a4-1da1-43a9-8ae5-4459444d993d is DONE.
Open Job" + ], "text/plain": [ - "Pipeline(steps=[('preproc',\n", - " ColumnTransformer(transformers=[('onehot', OneHotEncoder(),\n", - " ['island', 'species', 'sex']),\n", - " ('scaler', StandardScaler(),\n", - " ['culmen_depth_mm',\n", - " 'culmen_length_mm',\n", - " 'flipper_length_mm'])])),\n", - " ('linreg', LinearRegression(fit_intercept=False))])" + "" ] }, - "execution_count": 20, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Pipeline(steps=[('transform',\n", + " ColumnTransformer(transformers=[('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'island'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_length_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_depth_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'flipper_length_mm'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'sex')])),\n", + " ('estimator',\n", + " LinearRegression(fit_intercept=False,\n", + " optimize_strategy='NORMAL_EQUATION'))])" + ] + }, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -1269,6 +1304,13 @@ "source": [ "pipeline.to_gbq(\"bigframes-dev.bigframes_demo_us.penguin_model\", replace=True)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -1287,7 +1329,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.9" }, "orig_nbformat": 4, "vscode": { diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py index f01116665f1..9244c4b9f19 100644 --- a/tests/system/large/ml/test_cluster.py +++ b/tests/system/large/ml/test_cluster.py @@ -98,7 +98,9 @@ def test_cluster_configure_fit_score_predict( score_result, score_expected, check_exact=False, rtol=0.1 ) - result = model.predict(new_penguins).to_pandas() + predictions = model.predict(new_penguins).to_pandas() + assert predictions.shape == (4, 9) + result = predictions[["CENTROID_ID"]] expected = pd.DataFrame( {"CENTROID_ID": [2, 3, 1, 2]}, dtype="Int64", diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index a8613dfeb9b..b98d7a757ca 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -179,7 +179,7 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): ) -@pytest.mark.flaky(retries=2, delay=120) +# @pytest.mark.flaky(retries=2, delay=120) def test_xgbclassifier_dart_booster_multiple_params( penguins_df_default_index, dataset_id ): diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 3e569540587..2929baf3f7b 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -545,7 +545,9 @@ def test_pipeline_standard_scaler_kmeans_fit_score_predict( score_result, score_expected, check_exact=False, rtol=0.1 ) - result = pl.predict(new_penguins).to_pandas().sort_index() + predictions = pl.predict(new_penguins).to_pandas().sort_index() + assert predictions.shape == (6, 9) + result = predictions[["CENTROID_ID"]] expected = pd.DataFrame( {"CENTROID_ID": [1, 2, 1, 2, 1, 2]}, dtype="Int64", diff --git a/tests/system/small/ml/test_cluster.py b/tests/system/small/ml/test_cluster.py index 266a38e3ee1..a9fec0bbce0 100644 --- a/tests/system/small/ml/test_cluster.py +++ b/tests/system/small/ml/test_cluster.py @@ -62,7 +62,9 @@ def test_kmeans_predict(session, penguins_kmeans_model: cluster.KMeans): new_penguins = session.read_pandas(_PD_NEW_PENGUINS) - result = penguins_kmeans_model.predict(new_penguins).to_pandas() + predictions = penguins_kmeans_model.predict(new_penguins).to_pandas() + assert predictions.shape == (4, 9) + result = predictions[["CENTROID_ID"]] expected = pd.DataFrame( {"CENTROID_ID": [2, 3, 1, 2]}, dtype="Int64", diff --git a/tests/system/small/ml/test_ensemble.py b/tests/system/small/ml/test_ensemble.py index bba083d98d9..55d9fef661a 100644 --- a/tests/system/small/ml/test_ensemble.py +++ b/tests/system/small/ml/test_ensemble.py @@ -98,7 +98,9 @@ def test_xgbregressor_model_score_series( def test_xgbregressor_model_predict( penguins_xgbregressor_model: bigframes.ml.ensemble.XGBRegressor, new_penguins_df ): - result = penguins_xgbregressor_model.predict(new_penguins_df).to_pandas() + predictions = penguins_xgbregressor_model.predict(new_penguins_df).to_pandas() + assert predictions.shape == (3, 8) + result = predictions[["predicted_body_mass_g"]] expected = pandas.DataFrame( {"predicted_body_mass_g": ["4293.1538089", "3410.0271", "3357.944"]}, dtype="Float64", @@ -220,7 +222,9 @@ def test_xgbclassifier_model_score_series( def test_xgbclassifier_model_predict( penguins_xgbclassifier_model: bigframes.ml.ensemble.XGBClassifier, new_penguins_df ): - result = penguins_xgbclassifier_model.predict(new_penguins_df).to_pandas() + predictions = penguins_xgbclassifier_model.predict(new_penguins_df).to_pandas() + assert predictions.shape == (3, 9) + result = predictions[["predicted_sex"]] expected = pandas.DataFrame( {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, dtype="string[pyarrow]", @@ -363,7 +367,11 @@ def test_randomforestregressor_model_predict( penguins_randomforest_regressor_model: bigframes.ml.ensemble.RandomForestRegressor, new_penguins_df, ): - result = penguins_randomforest_regressor_model.predict(new_penguins_df).to_pandas() + predictions = penguins_randomforest_regressor_model.predict( + new_penguins_df + ).to_pandas() + assert predictions.shape == (3, 8) + result = predictions[["predicted_body_mass_g"]] expected = pandas.DataFrame( {"predicted_body_mass_g": ["3897.341797", "3458.385742", "3458.385742"]}, dtype="Float64", @@ -490,7 +498,11 @@ def test_randomforestclassifier_model_predict( penguins_randomforest_classifier_model: bigframes.ml.ensemble.RandomForestClassifier, new_penguins_df, ): - result = penguins_randomforest_classifier_model.predict(new_penguins_df).to_pandas() + predictions = penguins_randomforest_classifier_model.predict( + new_penguins_df + ).to_pandas() + assert predictions.shape == (3, 9) + result = predictions[["predicted_sex"]] expected = pandas.DataFrame( {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, dtype="string[pyarrow]", diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index 55079c94cfd..948db59650d 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -22,6 +22,8 @@ def test_model_predict(time_series_arima_plus_model): utc = pytz.utc predictions = time_series_arima_plus_model.predict().to_pandas() + assert predictions.shape == (3, 8) + result = predictions[["forecast_timestamp", "forecast_value"]] expected = pd.DataFrame( { "forecast_timestamp": [ @@ -38,7 +40,7 @@ def test_model_predict(time_series_arima_plus_model): ) pd.testing.assert_frame_equal( - predictions, + result, expected, rtol=0.1, check_index_type=False, diff --git a/tests/system/small/ml/test_imported.py b/tests/system/small/ml/test_imported.py index d3055670668..9008e85a0b5 100644 --- a/tests/system/small/ml/test_imported.py +++ b/tests/system/small/ml/test_imported.py @@ -32,7 +32,9 @@ def test_tensorflow_create_model_default_session(imported_tensorflow_model_path) def test_tensorflow_model_predict(imported_tensorflow_model, llm_text_df): df = llm_text_df.rename(columns={"prompt": "input"}) - result = imported_tensorflow_model.predict(df).to_pandas() + predictions = imported_tensorflow_model.predict(df).to_pandas() + assert predictions.shape == (3, 2) + result = predictions[["dense_1"]] # The values are non-human-readable. As they are a dense layer of Neural Network. # And since it is pretrained and imported, the model is a opaque-box. # We may want to switch to better test model and cases. @@ -72,7 +74,9 @@ def test_onnx_create_model_default_session(imported_onnx_model_path): def test_onnx_model_predict(imported_onnx_model, onnx_iris_df): - result = imported_onnx_model.predict(onnx_iris_df).to_pandas() + predictions = imported_onnx_model.predict(onnx_iris_df).to_pandas() + assert predictions.shape == (3, 7) + result = predictions[["label", "probabilities"]] value1 = np.array([0.9999993443489075, 0.0, 0.0]) value2 = np.array([0.0, 0.0, 0.9999993443489075]) expected = pd.DataFrame( diff --git a/tests/system/small/ml/test_linear_model.py b/tests/system/small/ml/test_linear_model.py index 3a8232ed9ec..218c1074ab4 100644 --- a/tests/system/small/ml/test_linear_model.py +++ b/tests/system/small/ml/test_linear_model.py @@ -91,13 +91,15 @@ def test_linear_reg_model_score_series( def test_linear_reg_model_predict(penguins_linear_model, new_penguins_df): predictions = penguins_linear_model.predict(new_penguins_df).to_pandas() + assert predictions.shape == (3, 8) + result = predictions[["predicted_body_mass_g"]] expected = pandas.DataFrame( {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, dtype="Float64", index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) pandas.testing.assert_frame_equal( - predictions.sort_index(), + result.sort_index(), expected, check_exact=False, rtol=0.1, @@ -224,13 +226,15 @@ def test_logistic_model_score_series( def test_logsitic_model_predict(penguins_logistic_model, new_penguins_df): predictions = penguins_logistic_model.predict(new_penguins_df).to_pandas() + assert predictions.shape == (3, 9) + result = predictions[["predicted_sex"]] expected = pandas.DataFrame( {"predicted_sex": ["MALE", "MALE", "FEMALE"]}, dtype="string[pyarrow]", index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) pandas.testing.assert_frame_equal( - predictions.sort_index(), + result.sort_index(), expected, check_exact=False, rtol=0.1, diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 79d3c40317d..306098548ec 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest import TestCase - import numpy as np import pytest @@ -48,7 +46,7 @@ def test_create_text_generator_model_default_session(bq_connection, llm_text_pan llm_text_df = bpd.read_pandas(llm_text_pandas_df) df = model.predict(llm_text_df).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) @@ -72,7 +70,7 @@ def test_create_text_generator_32k_model_default_session( llm_text_df = bpd.read_pandas(llm_text_pandas_df) df = model.predict(llm_text_df).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) @@ -97,7 +95,7 @@ def test_create_text_generator_model_default_connection(llm_text_pandas_df): ) df = model.predict(llm_text_df).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) @@ -109,7 +107,7 @@ def test_text_generator_predict_default_params_success( palm2_text_generator_model, llm_text_df ): df = palm2_text_generator_model.predict(llm_text_df).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) @@ -120,7 +118,7 @@ def test_text_generator_predict_series_default_params_success( palm2_text_generator_model, llm_text_df ): df = palm2_text_generator_model.predict(llm_text_df["prompt"]).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) @@ -132,7 +130,7 @@ def test_text_generator_predict_arbitrary_col_label_success( ): llm_text_df = llm_text_df.rename(columns={"prompt": "arbitrary"}) df = palm2_text_generator_model.predict(llm_text_df).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) @@ -145,7 +143,7 @@ def test_text_generator_predict_with_params_success( df = palm2_text_generator_model.predict( llm_text_df, temperature=0.5, max_output_tokens=100, top_k=20, top_p=0.5 ).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) @@ -196,7 +194,7 @@ def test_embedding_generator_predict_success( palm2_embedding_generator_model, llm_text_df ): df = palm2_embedding_generator_model.predict(llm_text_df).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "text_embedding" in df.columns series = df["text_embedding"] value = series[0] @@ -209,7 +207,7 @@ def test_embedding_generator_multilingual_predict_success( palm2_embedding_generator_multilingual_model, llm_text_df ): df = palm2_embedding_generator_multilingual_model.predict(llm_text_df).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "text_embedding" in df.columns series = df["text_embedding"] value = series[0] @@ -222,7 +220,7 @@ def test_embedding_generator_predict_series_success( palm2_embedding_generator_model, llm_text_df ): df = palm2_embedding_generator_model.predict(llm_text_df["prompt"]).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 1)) + assert df.shape == (3, 4) assert "text_embedding" in df.columns series = df["text_embedding"] value = series[0] diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index 5369d3662d7..be6c5e7c521 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -20,19 +20,7 @@ class _BaseKMeans(BaseEstimator, ABC): """Base class for KMeans and MiniBatchKMeans""" - def predict(self, X): - """Predict the closest cluster each sample in X belongs to. - - Args: - X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Series or DataFrame of shape (n_samples, n_features). The data matrix for - which we want to get the predictions. - - Returns: - bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,), containing the - class labels for each sample. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + pass class KMeans(_BaseKMeans): @@ -73,7 +61,7 @@ def predict( DataFrame of shape (n_samples, n_features). New data to predict. Returns: - bigframes.dataframe.DataFrame: DataFrame of the cluster each sample belongs to. + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted labels. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index 8dc3b6280a4..ab946e58610 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -16,7 +16,6 @@ # Original location: https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/linear_model/_base.py from abc import ABCMeta -from typing import List, Optional from bigframes import constants from third_party.bigframes_vendored.sklearn.base import ( @@ -35,7 +34,7 @@ def predict(self, X): Series or DataFrame of shape (n_samples, n_features). Samples. Returns: - bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,). Returns predicted values. + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -50,8 +49,7 @@ def predict(self, X): which we want to get the predictions. Returns: - bigframes.dataframe.DataFrame: DataFrame of shape (n_samples,), containing - the class labels for each sample. + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index b7b43b85a3e..dfd0ba73563 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -18,7 +18,7 @@ def predict(self, X): Series or DataFrame of shape (n_samples, n_features). Samples. Returns: - DataFrame of shape (n_samples,): Returns predicted values. + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From a7298317ea2604faa6ae31817f1f729d7e0b9818 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:44:14 -0800 Subject: [PATCH 167/921] fix: invalid JSON type of the notebook (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../bq_dataframes_llm_kmeans.ipynb | 1064 +---------------- 1 file changed, 33 insertions(+), 1031 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index ae038136393..8d759509254 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -139,17 +139,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Updated property [core/project].\n" - ] - } - ], + "outputs": [], "source": [ "# set your project ID below\n", "PROJECT_ID = \"\" # @param {type:\"string\"}\n", @@ -170,7 +162,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -264,7 +256,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "id": "R7STCS8xB5d2" }, @@ -296,7 +288,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "id": "zDSwoBo1CU3G" }, @@ -307,101 +299,11 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "id": "tYDoaKgJChiq" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 9f096761-e3b5-4d58-a9f7-485ced67afca is DONE. 2.3 GB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job ee8fecb1-2e30-407d-9e2e-9e76061da9e7 is DONE. 2.3 GB 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", - "
consumer_complaint_narrative
0I signed a contract as a condition of employme...
1First, I want to disclose that XXXX and XXXX b...
2Frequent calls from Focused Receivables Manage...
3I recently contacted Enhanced Recovery Company...
4This began when I subscribed to XXXX XXXX inte...
\n", - "

5 rows × 1 columns

\n", - "
[5 rows x 1 columns in total]" - ], - "text/plain": [ - " consumer_complaint_narrative\n", - "0 I signed a contract as a condition of employme...\n", - "1 First, I want to disclose that XXXX and XXXX b...\n", - "2 Frequent calls from Focused Receivables Manage...\n", - "3 I recently contacted Enhanced Recovery Company...\n", - "4 This began when I subscribed to XXXX XXXX inte...\n", - "\n", - "[5 rows x 1 columns]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "issues_df = input_df[[\"consumer_complaint_narrative\"]].dropna()\n", "issues_df.head(n=5) # View the first five complaints" @@ -417,7 +319,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "id": "OltYSUEcsSOW" }, @@ -439,24 +341,11 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { "id": "li38q8FzDDMu" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 52d2e961-7896-497c-8b03-ab7374737679 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from bigframes.ml.llm import PaLM2TextEmbeddingGenerator\n", "\n", @@ -465,125 +354,11 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": { "id": "cOuSOQ5FDewD" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job d093d51a-8eda-442f-80cd-568cb76e00b3 is DONE. 10.6 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 6419df65-3e96-41a7-a7b5-3d058e18763a is DONE. 80.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 917f09ea-c468-4363-a856-b1091e5f775f is DONE. 80.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 5c9679e7-192c-40b5-a14b-edc0fa113eaa is DONE. 61.5 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", - "
text_embedding
422[-0.012013785541057587, 0.003669967409223318, ...
616[-0.014948881231248379, -0.04672442376613617, ...
833[-0.01951478235423565, -0.027120858430862427, ...
1370[-0.03140445053577423, -0.048797041177749634, ...
1430[-0.02244548313319683, -0.03336532413959503, 0...
\n", - "

5 rows × 1 columns

\n", - "
[5 rows x 1 columns in total]" - ], - "text/plain": [ - " text_embedding\n", - "422 [-0.012013785541057587, 0.003669967409223318, ...\n", - "616 [-0.014948881231248379, -0.04672442376613617, ...\n", - "833 [-0.01951478235423565, -0.027120858430862427, ...\n", - "1370 [-0.03140445053577423, -0.048797041177749634, ...\n", - "1430 [-0.02244548313319683, -0.03336532413959503, 0...\n", - "\n", - "[5 rows x 1 columns]" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Will take ~3 minutes to compute the embeddings\n", "predicted_embeddings = model.predict(downsampled_issues_df)\n", @@ -593,263 +368,14 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": { "id": "4H_etYfsEOFP" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job ce9cb0f9-4b0d-40a1-81f3-d6e60dd6c684 is DONE. 160.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job aa692a30-5706-46ad-8029-faf2fac66234 is DONE. 72.2 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", - "
consumer_complaint_narrativetext_embedding
2580664Hello, my name is XXXX XXXX, and I am writing ...[0.0003211698785889894, -0.01816680282354355, ...
1806973This is XXXX XXXX and I am submitting this com...[-0.009485247544944286, -0.025846892967820168,...
2055053XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX...[-0.010950954630970955, -0.0249345600605011, 0...
2515231When I reinvestigated my credit report, I real...[-0.009660656563937664, -0.05793113633990288, ...
2633049Checking my credit report XX/XX/2018 with all ...[-0.0022159104701131582, -0.03330004960298538,...
3117273I contacted TransUnion and spoke a credit rep ...[-0.015955328941345215, -0.006488671060651541,...
698814XXXX XXXX XXXX. makes daily calls to me cell c...[0.005397460889071226, -0.01276913657784462, 0...
267826Can we please reopen Case : XXXX? \n", - "\n", - "Wells Farg...[0.004065403249114752, -0.0005381882656365633,...
54019My rights under 15 USC 1681 have been violated...[0.013823015615344048, -0.02010691538453102, 0...
141050To whom it may concern : My personal informati...[0.008104532025754452, -0.01856449618935585, 0...
2962076I have had a CashApp account since last year, ...[-0.0003019514260813594, -0.03750108182430267,...
2481105that some of the information was erroneous. Th...[-0.014868081547319889, -0.0443895161151886, -...
431562I have disputed the referenced accounts to the...[-0.0020524838473647833, -0.04830990731716156,...
1953029On, XX/XX/22, I attempted to complete a transa...[-0.01599179394543171, -0.0074900356121361256,...
2395979Subject : XXXX XXXX XXXX compensation, refund,...[-0.0035950862802565098, -0.014652969315648079...
455524I paid off my mortgage on XX/XX/2019. The comp...[-0.01100730150938034, -0.03495829552412033, 0...
2155924This kind of account is placed as a charged of...[-0.028635455295443535, -0.028604287654161453,...
1069497This is one of many issues I have had with Wel...[0.008871790021657944, -0.028502725064754486, ...
3181689I have disputed this account with MONTEREY FIN...[-0.004721717908978462, -0.03673810139298439, ...
274268Lender is not updating my loan status in the V...[-0.009221495129168034, -0.0289347805082798, 0...
1671305XXXX is a peer to peer lending conmpany that u...[-0.02911308966577053, -0.01850792020559311, -...
886026( DISPUTE CODE - XXXX ) My personal informatio...[-0.007220877334475517, -0.016615957021713257,...
1044431I filed a complaint against PNC this year and ...[0.002848619595170021, -0.035117778927087784, ...
1938481I applied for a modification and was approved....[-0.03114932030439377, -0.0421406552195549, 0....
1987834Ive been Disputting my XXXX XXXX I opened this...[-0.009406660683453083, -0.020967338234186172,...
\n", - "

25 rows × 2 columns

\n", - "
[10000 rows x 2 columns in total]" - ], - "text/plain": [ - " consumer_complaint_narrative \\\n", - "2580664 Hello, my name is XXXX XXXX, and I am writing ... \n", - "1806973 This is XXXX XXXX and I am submitting this com... \n", - "2055053 XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX... \n", - "2515231 When I reinvestigated my credit report, I real... \n", - "2633049 Checking my credit report XX/XX/2018 with all ... \n", - "3117273 I contacted TransUnion and spoke a credit rep ... \n", - "698814 XXXX XXXX XXXX. makes daily calls to me cell c... \n", - "267826 Can we please reopen Case : XXXX? \n", - "\n", - "Wells Farg... \n", - "54019 My rights under 15 USC 1681 have been violated... \n", - "141050 To whom it may concern : My personal informati... \n", - "2962076 I have had a CashApp account since last year, ... \n", - "2481105 that some of the information was erroneous. Th... \n", - "431562 I have disputed the referenced accounts to the... \n", - "1953029 On, XX/XX/22, I attempted to complete a transa... \n", - "2395979 Subject : XXXX XXXX XXXX compensation, refund,... \n", - "455524 I paid off my mortgage on XX/XX/2019. The comp... \n", - "2155924 This kind of account is placed as a charged of... \n", - "1069497 This is one of many issues I have had with Wel... \n", - "3181689 I have disputed this account with MONTEREY FIN... \n", - "274268 Lender is not updating my loan status in the V... \n", - "1671305 XXXX is a peer to peer lending conmpany that u... \n", - "886026 ( DISPUTE CODE - XXXX ) My personal informatio... \n", - "1044431 I filed a complaint against PNC this year and ... \n", - "1938481 I applied for a modification and was approved.... \n", - "1987834 Ive been Disputting my XXXX XXXX I opened this... \n", - "\n", - " text_embedding \n", - "2580664 [0.0003211698785889894, -0.01816680282354355, ... \n", - "1806973 [-0.009485247544944286, -0.025846892967820168,... \n", - "2055053 [-0.010950954630970955, -0.0249345600605011, 0... \n", - "2515231 [-0.009660656563937664, -0.05793113633990288, ... \n", - "2633049 [-0.0022159104701131582, -0.03330004960298538,... \n", - "3117273 [-0.015955328941345215, -0.006488671060651541,... \n", - "698814 [0.005397460889071226, -0.01276913657784462, 0... \n", - "267826 [0.004065403249114752, -0.0005381882656365633,... \n", - "54019 [0.013823015615344048, -0.02010691538453102, 0... \n", - "141050 [0.008104532025754452, -0.01856449618935585, 0... \n", - "2962076 [-0.0003019514260813594, -0.03750108182430267,... \n", - "2481105 [-0.014868081547319889, -0.0443895161151886, -... \n", - "431562 [-0.0020524838473647833, -0.04830990731716156,... \n", - "1953029 [-0.01599179394543171, -0.0074900356121361256,... \n", - "2395979 [-0.0035950862802565098, -0.014652969315648079... \n", - "455524 [-0.01100730150938034, -0.03495829552412033, 0... \n", - "2155924 [-0.028635455295443535, -0.028604287654161453,... \n", - "1069497 [0.008871790021657944, -0.028502725064754486, ... \n", - "3181689 [-0.004721717908978462, -0.03673810139298439, ... \n", - "274268 [-0.009221495129168034, -0.0289347805082798, 0... \n", - "1671305 [-0.02911308966577053, -0.01850792020559311, -... \n", - "886026 [-0.007220877334475517, -0.016615957021713257,... \n", - "1044431 [0.002848619595170021, -0.035117778927087784, ... \n", - "1938481 [-0.03114932030439377, -0.0421406552195549, 0.... \n", - "1987834 [-0.009406660683453083, -0.020967338234186172,... \n", - "...\n", - "\n", - "[10000 rows x 2 columns]" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Join the complaints with their embeddings in the same DataFrame\n", - "combined_df = downsampled_issues_df.join(predicted_embeddings, how=\"left\")\n", - "combined_df" + "combined_df = downsampled_issues_df.join(predicted_embeddings)" ] }, { @@ -872,7 +398,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": { "id": "AhNTnEC5FRz2" }, @@ -893,152 +419,14 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": { "id": "6poSxh-fGJF7" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 65eb317d-59f1-4d10-acd1-4b7f3778114c is DONE. 61.7 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 156e445e-cc01-4b30-84cc-ac1c98a69b81 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 5befc212-f4a3-4e33-b1b2-01e809acdcbd is DONE. 61.9 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job bd271178-8b8d-45dc-ac57-7f0194d0daac is DONE. 80.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job bbfb9cca-622d-4bf5-9fc0-6d9a85287d41 is DONE. 80.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job a5f30b32-9fb0-42b4-b426-d8484f008bdb is DONE. 160.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
CENTROID_ID
4222
6163
8335
13707
14303
\n", - "

5 rows × 1 columns

\n", - "
[5 rows x 1 columns in total]" - ], - "text/plain": [ - " CENTROID_ID\n", - "422 2\n", - "616 3\n", - "833 5\n", - "1370 7\n", - "1430 3\n", - "\n", - "[5 rows x 1 columns]" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", - "cluster_model.fit(combined_df[\"text_embedding\"])\n", + "cluster_model.fit(combined_df[[\"text_embedding\"]])\n", "clustered_result = cluster_model.predict(combined_df[[\"text_embedding\"]])\n", "# Notice the CENTROID_ID column, which is the ID number of the group that\n", "# each complaint belongs to.\n", @@ -1047,123 +435,13 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 7a41196e-ea67-44ac-95a7-7dce620d6d21 is DONE. 320.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 8008b482-1a0d-461f-a215-4676d9d918dc is DONE. 72.4 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", - "
consumer_complaint_narrativetext_embeddingCENTROID_ID
2580664Hello, my name is XXXX XXXX, and I am writing ...[0.0003211698785889894, -0.01816680282354355, ...2
1806973This is XXXX XXXX and I am submitting this com...[-0.009485247544944286, -0.025846892967820168,...5
2055053XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX...[-0.010950954630970955, -0.0249345600605011, 0...3
2515231When I reinvestigated my credit report, I real...[-0.009660656563937664, -0.05793113633990288, ...5
2633049Checking my credit report XX/XX/2018 with all ...[-0.0022159104701131582, -0.03330004960298538,...3
\n", - "

5 rows × 3 columns

\n", - "
[5 rows x 3 columns in total]" - ], - "text/plain": [ - " consumer_complaint_narrative \\\n", - "2580664 Hello, my name is XXXX XXXX, and I am writing ... \n", - "1806973 This is XXXX XXXX and I am submitting this com... \n", - "2055053 XXXX XXXX XXXX, XXXX. ( address : XXXX XXXX XX... \n", - "2515231 When I reinvestigated my credit report, I real... \n", - "2633049 Checking my credit report XX/XX/2018 with all ... \n", - "\n", - " text_embedding CENTROID_ID \n", - "2580664 [0.0003211698785889894, -0.01816680282354355, ... 2 \n", - "1806973 [-0.009485247544944286, -0.025846892967820168,... 5 \n", - "2055053 [-0.010950954630970955, -0.0249345600605011, 0... 3 \n", - "2515231 [-0.009660656563937664, -0.05793113633990288, ... 5 \n", - "2633049 [-0.0022159104701131582, -0.03330004960298538,... 3 \n", - "\n", - "[5 rows x 3 columns]" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Join the group number to the complaints and their text embeddings\n", "combined_clustered_result = combined_df.join(clustered_result)\n", - "\n", - "combined_clustered_result.head(n=5)" + "combined_clustered_result.head(n=5) " ] }, { @@ -1194,36 +472,11 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": { "id": "2E7wXM_jGqo6" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 50c7c0dd-94a2-494e-a37f-6a838a518f6c is DONE. 11.0 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job d96c847f-c292-4804-bd05-fd643c41c7a5 is DONE. 11.0 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Using bigframes, with syntax identical to pandas,\n", "# filter out the first and second groups\n", @@ -1240,100 +493,11 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": { "id": "ZNDiueI9IP5e" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "comment list 1:\n", - "1. XXXX is a peer to peer lending conmpany that uses borrowers crypto to collateralize loans from investors ( like myself ). I've been investing with them for almost XXXX years and currently have {$240000.00} tied up in lending products with XXXX. \n", - "As of XXXX days ago we received an email saying all business operations have been ceased and no withdrawals or deposits will be allowed. They said they'll update customers within 10 days, but no one can reach anyone at the company to find out any more details as they are not answering calls nor returning emails. It also appears the company has scrubbed its XXXX page and the XXXX pages of top executives. \n", - "\n", - "All collateral and client 's investment funds are supposedly held at or processed through XXXX XXXX XXXX ( registered SEC company ). XXXX XXXX keeps telling us to contact XXXX and won't give us any information, so we have no way to find out what's happening with our funds/collateral or if everything is gone. We have a XXXX channel up where people are gathering evidence, documentation, etc. This is probably the best place to start to get a broad view of what's happening. Details below. \n", - "\n", - "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX CONST LLC ( Business ID : XXXX ) FoXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 'Cease of Operations ' email received by all investors XXXX XX/XX/2022 at XXXX : \" Dear XXXX Users, Given the collapses of several cryptocurrencies so far this year and the rapidly deteriorating market conditions that have been prompting heavy withdrawals across all XXXX lending and XXXX exchange platforms recently, we are sad to inform you that we are unable to continue to operate our business as usual. As such, we are limiting our business activities, including pausing user withdrawals as allowed under our Terms of XXXX. \n", - "No deposit or investment request will be processed at this time. \n", - "\n", - "Our team is working diligently towards our objective of maximizing value for all of our Users, and our top priority continues to be to protect your interests. As we explore all options available to us, we will provide updates to you as we go. \n", - "\n", - "We hope to communicate with you within the next XXXX business days on the next steps to address the situation. We appreciate your patience in this trying time. \n", - "\n", - "Sincerely yoursXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", - "2. Submitted XX/XX/XXXX\n", - "Typed XX/XX/XXXX:\n", - "\n", - "XX/XX/XXXX\n", - "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX, XXXX XXXX\n", - "PH:. XXXX\n", - "PH: XXXX\n", - "EM:\n", - "XXXX\n", - "XXXX\n", - "XXXX XXXX \n", - "XXXX XXXX\n", - "Date of Birth XX/XX/XXXX\n", - "SS#: XXXX\n", - "TO:\n", - "* Consumer Financial Protection Brueau\n", - "* Department of Veteran Affairs, Office of the Inspector General\n", - "My name is XXXX XXXX XXXX, I've received more than one email from Discover Card in my XXXX XXXX, past emails from Discover Card were unautherized deletions.\n", - "From: Discover Card XXXX\n", - "To: You XXXX\n", - "Date: XX/XX/XXXX, XXXX XXXX XXXX From: Discover Card XXXX>\n", - "To Recipient \n", - "Date Mon, XX/XX/XXXX XXXX XXXX\n", - "I dont and havent ever had a Discover Checking, Savings, Business Accounts nor Loans of any kind through any Bank called Discover. The 1st time I was contacted by Discover Card I resided alone from XX/XX/XXXX to XX/XX/XXXXat XXXX XXXX XXXX at XXXX XXXX XXXX XXXX XXXX in XXXX, XXXX years prior to me moving here to XXXX, XXXX in XX/XX/XXXX. When \n", - "\n", - "\n", - "Discover Card had 1st contacted me in XXXX, XXXX it was associated with my XXXX XXXX XXXX website related online Merchants Account. Not once have I ever applied for or had any Website Merchant Accounts here in XXXX; I only applied for online online Merchant Accounts associated with my XXXX related Accounts I purchased while residing in XXXX, XXXX. Some of my website related information was stolen both in XXXX, XXXX and here in XXXX along with my other property that hasn't been returned to me. I don't and haven't ever had any XXXX XXXX related Agreements,Contracts or Credit Cards offered to Veterans associated with ones businesses. Nor have I ever applied for or had a Business License or Business Permit in any City or State inspite of my diverse interest. Not once have I ever allowed another be it an Paralegal, Payee, Attorney, Employers, Landlords, Veteran Organizations including Vocational Rehabilitation Programs, XXXX( XXXX XXXX XXXX, XXXX XXXX, Entertainment Companies, Banks, Celebrity Personal Assistant Agencies or Celebs, Shelters, Charities, HUD, Housing Arthority, Department of Veteran Affairs, Military, Law Enforcement or anyone else nor their employess to sign any business related Agreements or Contracts on my behalf; not even my family members or friends. \n", - "None of my XXXX XXXX attempts were associated with my Employers, Department of Veteran Affairs,Vocational Rehabilitation Programs Military, Landlords, HUD( Housing Authority),Friends, Family nor did I ever sign related Agreements or Contracts with them. Not once had I ever provided anyone the passwords to be able to sign into my accounts rather were aware of my accounts or not. Yes, my desktop computer that was stolen along with my other property XX/XX/XXXX was registered with my Online Merchant Account. I had paid for my Merchant related Accounts through my same XXXX XXXX XXXX Account I purchased both of my XXXX XXXX XXXX related accounts through. That was 1st once during the Summer of XX/XX/XXXX and 2nd my related website months later, while I resided in XXXX XXXX and I worked for XXXX. I never offered nor did I ever sign any business Contracts or Agreements with XXXX nor my Landlord or their staff associted with any of my online websites or Merchant Accounts. My XXXX XXXX XXXX Compensation was deposited into both of my XXXX XXXX XXXX Accounts at that time. My account was changed during the Summer of XX/XX/XXXXbecause of theft of my Bank Card. None of my Checking,Savings, past Credit Cards or Business related were shared accounts in which others were allowed to \n", - "use to make purchases. I had written checks from my XXXX XXXX XXXX account to pay for my XXXX XXXX XXXX XXXX on the XXXX XXXX here in XXXX in XX/XX/XXXX before it's name changed to XXXX XXXX. Prior to me using my same account open a Checking account in person at XXXX XXXX before it's name was changed to XXXX XXXX. Where my XXXX XXXX XXXX XXXX has been deposited since that time. I had used my XXXX XXXX Checking to pay for my XXXX XXXX XXXX XXXX both before theft of my property XX/XX/XXXX and that was also prior to the theft of my property from my XXXX XXXX XXXX XXXX in XX/XX/XXXX.\n", - "I've stated this many times:\n", - "I paid for my 1st XXXX XXXX XXXX Membership while employed at XXXX using my XXXX XXXX XXXX account XXXX my XXXX XXXX XXXX XXXX was also deposited. That was changed to XXXX because I didn't receive my 1st XXXX XXXX XXXX Card the bank sent to XXXX XXXX residence on XXXX XXXX in XX/XX/XXXX while I was there. In which both my XXXX salary and XXXX XXXX XXXX XXXX were deposited into my account, no money from XXXX XXXX nor anyone else that was at that residence was given to nor were any of my children there. Nor did XXXX or any other person at that residence ever give me my missing Bank Card not even after I moved out and stayed a month at XXXX XXXX XXXX using my replacement card to pay for my Hotel room. Which is the same account I used to pay for XXXX XXXX Membership, XXXX XXXX XXXX, XXXX XXXX Membership fees, and various online Merchant Account activation related fees.\n", - "* XXXX XXXX XXXX.\n", - "XXXX XXXX XXXX XXXX. Membership\n", - "\n", - "# XXXX\n", - "* XXXX XXXX Membership\n", - "# XXXX\n", - "* Total Merchant Services XXXX and XXXX.\n", - "* XXXX XXXX XXXX XXXX XXXX\n", - "* XXXX XXXX changed my $XXXX a month fees to my XXXX XXXX XXXX account #XXXX.\n", - "XX/XX/XXXX - XX/XX/XXXX XXXX XXXX, XXXX.\n", - "\n", - "Rep: XXXX XXXX XXXX, Fl \n", - "XXXX\n", - "XXXX Website \n", - "XXXX\n", - "Software and website owner, I performed Internet advertising and marketing, to promote this software and website. I worked and XXXX from my home XXXX XXXX XXXX XXXX XXXX , XXXX. I purchased XXXX XXXX XXXX-Software Electronic Book CD and was given a website to promote the software on the internet. The XXXX was given a copy of my website owner certificate document submitted to me when I purchased the software marketing program as well copies of my other school transcripts in addition to XXXX XXXX XXXX for example. XXXX, represented the first initials of my children's names. I wasn't ever paid and I'm still owed the money. Nor did my marketing program have anything to do with any schools, college nor university programs nor did I ever offer or sign any agreement to include it such. Nor did my XXXX XXXX XXXX have anything to do with any other employers, Department of Family and Children, Military, Veteran Organizations or Food Stamp programs, Section 8 nor Indianapolis Housing Authority for example; only me.\n", - "Thank you,\n", - "XXXX XXXX\n", - "3. ACCORDING TO 15 U.S. CODE 6803-DISCLOSURE OF INSTITUTION PRIVACY POLICY, AND ACCORDING TO U.S. CODE 6802- OBLIGATIONS WITH RESPECT TO DISCLOSURES OF PERSONAL INFORMATION. ( b ) OPT OUT ( 1 ) IN GENERAL A FINANCIAL INSTITUTION MAY NOT DISCLOSE NONPUBLIC PERSONAL INFORMATION TO A NONAFFILIATED THIRD PARTY ( TRANSUNION, XXXX, AND XXXX. ) UNLESS- ( A ) SUCH FINANCIAL INSTITUTION CLEARLY AND CONSPICUOUSLY DISCLOSES TO THE CONSUMER, IN WRITING OR IN ELECTRONIC FORM OR OTHER FORM PERMITTED BY THE REGULATIONS PRESCRIBED UNDER SECTION 6804 OF THIS TITLE. ALSO ACCORDING TO THE \" XXXX ACT '', FINANCIAL INSTITUTIONS MUST TELL THEIR CUSTOMERS ABOUT THEIR INFORMATION-SHARING PRACTICES AND EXPLAIN TO CUSTOMERS THEIR RIGHT TO \" OPT OUT '' IF THEY DON'T WANT THEIR INFORMATION SHARED WITH CERTAIN THIRD PARTIES. UNDER THE FDCPA, A COLLECTOR MUST PROVIDE YOU WITH INFORMATION ABOUT THE DEBT IN ITS INITIAL COMMUNICATION OR WITHIN FIVE DAYS AFTER THE INITIAL COMMUNICATION. ALSO, THE FDCPA STATES, \" YOU CAN NOT ATTEMPT TO COLLECT AN DEBT WHILE A PERSON ( THE CONSUMER ) SUPRESS VALIDATION. TRANSUNION, XXXX, XXXX, AND THE ACCOUNTS LISTED BELOW HAVE CLEARLY VIOLATED MY RIGHTS : XXXX ACCOUNT # XXXX, XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX ACCOUNT # XXXX, XXXX XXXX XXXX XXXX ACCOUNT # XXXX, AND XXXX ACCOUNT # XXXX. FAILURE TO RESPOND SATISFACTORILY WITH DELETIONS OF ALL THE ABOVE ACCOUNTS WILL RESULT IN LEGAL ACTIONS BEING TAKEN AGAINST, TRANSUNION, XXXX, XXXX, WHICH I'LL BE SEEKING A {$1000.00} PER VIOLATION FOR DEFAMATION OF CHARACTER ( PER SE ) NEGLIGENT ENABLEMENT OF IDENTITY FRAUD. 15 USC 1681 VIOLATIONS FOR WILLFUL NONCOMPLIANCE-616 CIVIL LIABILITY FOR WILLFUL NONCOPLIANCE. THIS IS THE THIRD TIME I'VE SUBMITTED A COMPLAINT, AND THE REPONSE I GET IS \" YOU CAN NOT LOCATE MY CREDIT REPORT! '' THIS IS CLEARLY NEGLIGENCE.\n", - "4. I do not know how this works, but I need it done or somehow corrected. My name is XXXX XXXX, XXXX XXXX XXXX XXXX TN XXXXMy SS XXXX DOB XXXX. I had some issues with my income being affected by the COVID-19PANDEMICSHUTDOWN. I was under the 1 CARESAct, Pub. L. 116-136, section 4021, codified at FCRAsection 623 ( a ) ( 1 ) ( F ) ( i ) ( I ), 15 U.S.C.1681s- 2 ( a ) ( 1 ) ( F ) ( i ) ( I ). I am requesting some accommodations so I care to protect the integrity of my credit file. US DEPT OF ED / XXXX # XXXX, # XXXX accounts are reporting on XXXX, XXXX The was 30,60, 90 DAYS LATEsince requested assistance due to the pandemic. I found a few accounts that I have never done any business with these companies and the accounts do not belong on my report : XXXX XXXX # XXXX, XXXX XXXX XXXX XXXX # XXXX. \n", - "\n", - "I have some issues with the misspelling of my name, my correct spelling is XXXX XXXX. Please remove any other variation of my name they are not correct. The following addresses do not belong to me please delete them : XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXXSC, XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", - "5. I want to know if this is even legal?! How can they disclose information without knowing its a correct email?!\n", - "\n", - "comment list 2:\n", - "1. Hello, my name is XXXX XXXX, and I am writing to delete the following information in my file. The items I need deleted are listed in the report. I am a victim of identity theft and did not make the charge. I ask that the items be deleted to correct my credit report. I reported the theft of my identity to the Federal Trade Commission and I also have enclosed copies of the Federal Trade Commissions Identity Theft Affidavit. Please delete the items as soon as possible. The accounts are being reported currently open and the accounts need to be closed. \n", - "XXXX account number XXXX opened on XX/XX/2022 for the amount {$530.00} XXXX XXXX XXXX account number XXXX opened on XX/XX/2022 for the amount of {$140.00} The accounts are being reported currently open and need to be closed immediately. \n", - "Based on, 15 U.S. Code 1681c2 a consumer reporting agency shall block the reporting of any information in the file of a consumer that the consumer identifies as information that resulted from an alleged identity theft, not later than 4 business days after the date of receipt. This account should not be furnished on my consumer report. As a consumer I am demanding the deletion of the accounts listed IMMEDIATELY.\n", - "2. To whom it may concern : My personal information was breach in the internet as result accounts had been open in my name, I was advise to fill out an Id theft report to help me deal with this situation, I have listed each one of the accounts that do not belong to me. This is my second request to remove unverified items in my report, but XXXX keep rposting these account with out providing any type of original document as the FCRA provide, you need to provide me with original documents or remove these account immediately.\n", - "3. Ive been Disputting my XXXX XXXX I opened this account and someone got my information and used my card, I contacted XXXX over and over, they removed the negative reporting from my XXXX report but still reporting it negative on my XXXX and Expean this is very unfair to me because Im a victim of identity theft\n", - "4. Today, XX/XX/2021, I received three items in the mail, one envelope containing an unsolicited debit card from Navy Federal credit Union and the other two, with a letter each describing The Important Rights on two accounts should these accounts become delinquent under New York law. \n", - "\n", - "First of all, I never applied for these accounts with Navy Federal, not have I authorized anyone to do so on my behalf. I immediately contacted Navy Federal via phone and was told I was most likely a victim of identity theft and that I should monitor my credit and use a credit monitoring service. I was also asked for my email and mailing information in order to receive a letter from them regarding this issue. \n", - "\n", - "My main concern is having someone using my identity to illegally open bank accounts and commit fraud, destroying my credit and finances in the process. This bank is in another state from where I reside. I have not lived in Virginia nor do I intend to do so in the foreseeable future.\n", - "5. My personal information ( including my SSN, Drivers License Info, Addresses, and more ) was stolen from a hacking, and Equifax did n't tell the public about the hack until more than a month after the hacking. During this time, three Equifax executives were caught inside trading. It really shows how Equifax cares about other people!\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "# Build plain-text prompts to send to PaLM 2. Use only 5 complaints from each group.\n", "prompt1 = 'comment list 1:\\n'\n", @@ -1352,100 +516,11 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": { "id": "BfHGJLirzSvH" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Please highlight the most obvious difference betweenthe two lists of comments:\n", - "comment list 1:\n", - "1. XXXX is a peer to peer lending conmpany that uses borrowers crypto to collateralize loans from investors ( like myself ). I've been investing with them for almost XXXX years and currently have {$240000.00} tied up in lending products with XXXX. \n", - "As of XXXX days ago we received an email saying all business operations have been ceased and no withdrawals or deposits will be allowed. They said they'll update customers within 10 days, but no one can reach anyone at the company to find out any more details as they are not answering calls nor returning emails. It also appears the company has scrubbed its XXXX page and the XXXX pages of top executives. \n", - "\n", - "All collateral and client 's investment funds are supposedly held at or processed through XXXX XXXX XXXX ( registered SEC company ). XXXX XXXX keeps telling us to contact XXXX and won't give us any information, so we have no way to find out what's happening with our funds/collateral or if everything is gone. We have a XXXX channel up where people are gathering evidence, documentation, etc. This is probably the best place to start to get a broad view of what's happening. Details below. \n", - "\n", - "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX CONST LLC ( Business ID : XXXX ) FoXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 'Cease of Operations ' email received by all investors XXXX XX/XX/2022 at XXXX : \" Dear XXXX Users, Given the collapses of several cryptocurrencies so far this year and the rapidly deteriorating market conditions that have been prompting heavy withdrawals across all XXXX lending and XXXX exchange platforms recently, we are sad to inform you that we are unable to continue to operate our business as usual. As such, we are limiting our business activities, including pausing user withdrawals as allowed under our Terms of XXXX. \n", - "No deposit or investment request will be processed at this time. \n", - "\n", - "Our team is working diligently towards our objective of maximizing value for all of our Users, and our top priority continues to be to protect your interests. As we explore all options available to us, we will provide updates to you as we go. \n", - "\n", - "We hope to communicate with you within the next XXXX business days on the next steps to address the situation. We appreciate your patience in this trying time. \n", - "\n", - "Sincerely yoursXXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", - "2. Submitted XX/XX/XXXX\n", - "Typed XX/XX/XXXX:\n", - "\n", - "XX/XX/XXXX\n", - "XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX, XXXX XXXX\n", - "PH:. XXXX\n", - "PH: XXXX\n", - "EM:\n", - "XXXX\n", - "XXXX\n", - "XXXX XXXX \n", - "XXXX XXXX\n", - "Date of Birth XX/XX/XXXX\n", - "SS#: XXXX\n", - "TO:\n", - "* Consumer Financial Protection Brueau\n", - "* Department of Veteran Affairs, Office of the Inspector General\n", - "My name is XXXX XXXX XXXX, I've received more than one email from Discover Card in my XXXX XXXX, past emails from Discover Card were unautherized deletions.\n", - "From: Discover Card XXXX\n", - "To: You XXXX\n", - "Date: XX/XX/XXXX, XXXX XXXX XXXX From: Discover Card XXXX>\n", - "To Recipient \n", - "Date Mon, XX/XX/XXXX XXXX XXXX\n", - "I dont and havent ever had a Discover Checking, Savings, Business Accounts nor Loans of any kind through any Bank called Discover. The 1st time I was contacted by Discover Card I resided alone from XX/XX/XXXX to XX/XX/XXXXat XXXX XXXX XXXX at XXXX XXXX XXXX XXXX XXXX in XXXX, XXXX years prior to me moving here to XXXX, XXXX in XX/XX/XXXX. When \n", - "\n", - "\n", - "Discover Card had 1st contacted me in XXXX, XXXX it was associated with my XXXX XXXX XXXX website related online Merchants Account. Not once have I ever applied for or had any Website Merchant Accounts here in XXXX; I only applied for online online Merchant Accounts associated with my XXXX related Accounts I purchased while residing in XXXX, XXXX. Some of my website related information was stolen both in XXXX, XXXX and here in XXXX along with my other property that hasn't been returned to me. I don't and haven't ever had any XXXX XXXX related Agreements,Contracts or Credit Cards offered to Veterans associated with ones businesses. Nor have I ever applied for or had a Business License or Business Permit in any City or State inspite of my diverse interest. Not once have I ever allowed another be it an Paralegal, Payee, Attorney, Employers, Landlords, Veteran Organizations including Vocational Rehabilitation Programs, XXXX( XXXX XXXX XXXX, XXXX XXXX, Entertainment Companies, Banks, Celebrity Personal Assistant Agencies or Celebs, Shelters, Charities, HUD, Housing Arthority, Department of Veteran Affairs, Military, Law Enforcement or anyone else nor their employess to sign any business related Agreements or Contracts on my behalf; not even my family members or friends. \n", - "None of my XXXX XXXX attempts were associated with my Employers, Department of Veteran Affairs,Vocational Rehabilitation Programs Military, Landlords, HUD( Housing Authority),Friends, Family nor did I ever sign related Agreements or Contracts with them. Not once had I ever provided anyone the passwords to be able to sign into my accounts rather were aware of my accounts or not. Yes, my desktop computer that was stolen along with my other property XX/XX/XXXX was registered with my Online Merchant Account. I had paid for my Merchant related Accounts through my same XXXX XXXX XXXX Account I purchased both of my XXXX XXXX XXXX related accounts through. That was 1st once during the Summer of XX/XX/XXXX and 2nd my related website months later, while I resided in XXXX XXXX and I worked for XXXX. I never offered nor did I ever sign any business Contracts or Agreements with XXXX nor my Landlord or their staff associted with any of my online websites or Merchant Accounts. My XXXX XXXX XXXX Compensation was deposited into both of my XXXX XXXX XXXX Accounts at that time. My account was changed during the Summer of XX/XX/XXXXbecause of theft of my Bank Card. None of my Checking,Savings, past Credit Cards or Business related were shared accounts in which others were allowed to \n", - "use to make purchases. I had written checks from my XXXX XXXX XXXX account to pay for my XXXX XXXX XXXX XXXX on the XXXX XXXX here in XXXX in XX/XX/XXXX before it's name changed to XXXX XXXX. Prior to me using my same account open a Checking account in person at XXXX XXXX before it's name was changed to XXXX XXXX. Where my XXXX XXXX XXXX XXXX has been deposited since that time. I had used my XXXX XXXX Checking to pay for my XXXX XXXX XXXX XXXX both before theft of my property XX/XX/XXXX and that was also prior to the theft of my property from my XXXX XXXX XXXX XXXX in XX/XX/XXXX.\n", - "I've stated this many times:\n", - "I paid for my 1st XXXX XXXX XXXX Membership while employed at XXXX using my XXXX XXXX XXXX account XXXX my XXXX XXXX XXXX XXXX was also deposited. That was changed to XXXX because I didn't receive my 1st XXXX XXXX XXXX Card the bank sent to XXXX XXXX residence on XXXX XXXX in XX/XX/XXXX while I was there. In which both my XXXX salary and XXXX XXXX XXXX XXXX were deposited into my account, no money from XXXX XXXX nor anyone else that was at that residence was given to nor were any of my children there. Nor did XXXX or any other person at that residence ever give me my missing Bank Card not even after I moved out and stayed a month at XXXX XXXX XXXX using my replacement card to pay for my Hotel room. Which is the same account I used to pay for XXXX XXXX Membership, XXXX XXXX XXXX, XXXX XXXX Membership fees, and various online Merchant Account activation related fees.\n", - "* XXXX XXXX XXXX.\n", - "XXXX XXXX XXXX XXXX. Membership\n", - "\n", - "# XXXX\n", - "* XXXX XXXX Membership\n", - "# XXXX\n", - "* Total Merchant Services XXXX and XXXX.\n", - "* XXXX XXXX XXXX XXXX XXXX\n", - "* XXXX XXXX changed my $XXXX a month fees to my XXXX XXXX XXXX account #XXXX.\n", - "XX/XX/XXXX - XX/XX/XXXX XXXX XXXX, XXXX.\n", - "\n", - "Rep: XXXX XXXX XXXX, Fl \n", - "XXXX\n", - "XXXX Website \n", - "XXXX\n", - "Software and website owner, I performed Internet advertising and marketing, to promote this software and website. I worked and XXXX from my home XXXX XXXX XXXX XXXX XXXX , XXXX. I purchased XXXX XXXX XXXX-Software Electronic Book CD and was given a website to promote the software on the internet. The XXXX was given a copy of my website owner certificate document submitted to me when I purchased the software marketing program as well copies of my other school transcripts in addition to XXXX XXXX XXXX for example. XXXX, represented the first initials of my children's names. I wasn't ever paid and I'm still owed the money. Nor did my marketing program have anything to do with any schools, college nor university programs nor did I ever offer or sign any agreement to include it such. Nor did my XXXX XXXX XXXX have anything to do with any other employers, Department of Family and Children, Military, Veteran Organizations or Food Stamp programs, Section 8 nor Indianapolis Housing Authority for example; only me.\n", - "Thank you,\n", - "XXXX XXXX\n", - "3. ACCORDING TO 15 U.S. CODE 6803-DISCLOSURE OF INSTITUTION PRIVACY POLICY, AND ACCORDING TO U.S. CODE 6802- OBLIGATIONS WITH RESPECT TO DISCLOSURES OF PERSONAL INFORMATION. ( b ) OPT OUT ( 1 ) IN GENERAL A FINANCIAL INSTITUTION MAY NOT DISCLOSE NONPUBLIC PERSONAL INFORMATION TO A NONAFFILIATED THIRD PARTY ( TRANSUNION, XXXX, AND XXXX. ) UNLESS- ( A ) SUCH FINANCIAL INSTITUTION CLEARLY AND CONSPICUOUSLY DISCLOSES TO THE CONSUMER, IN WRITING OR IN ELECTRONIC FORM OR OTHER FORM PERMITTED BY THE REGULATIONS PRESCRIBED UNDER SECTION 6804 OF THIS TITLE. ALSO ACCORDING TO THE \" XXXX ACT '', FINANCIAL INSTITUTIONS MUST TELL THEIR CUSTOMERS ABOUT THEIR INFORMATION-SHARING PRACTICES AND EXPLAIN TO CUSTOMERS THEIR RIGHT TO \" OPT OUT '' IF THEY DON'T WANT THEIR INFORMATION SHARED WITH CERTAIN THIRD PARTIES. UNDER THE FDCPA, A COLLECTOR MUST PROVIDE YOU WITH INFORMATION ABOUT THE DEBT IN ITS INITIAL COMMUNICATION OR WITHIN FIVE DAYS AFTER THE INITIAL COMMUNICATION. ALSO, THE FDCPA STATES, \" YOU CAN NOT ATTEMPT TO COLLECT AN DEBT WHILE A PERSON ( THE CONSUMER ) SUPRESS VALIDATION. TRANSUNION, XXXX, XXXX, AND THE ACCOUNTS LISTED BELOW HAVE CLEARLY VIOLATED MY RIGHTS : XXXX ACCOUNT # XXXX, XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX XXXX ACCOUNT # XXXXXXXX XXXX XXXX XXXX ACCOUNT # XXXX, XXXX XXXX XXXX XXXX ACCOUNT # XXXX, AND XXXX ACCOUNT # XXXX. FAILURE TO RESPOND SATISFACTORILY WITH DELETIONS OF ALL THE ABOVE ACCOUNTS WILL RESULT IN LEGAL ACTIONS BEING TAKEN AGAINST, TRANSUNION, XXXX, XXXX, WHICH I'LL BE SEEKING A {$1000.00} PER VIOLATION FOR DEFAMATION OF CHARACTER ( PER SE ) NEGLIGENT ENABLEMENT OF IDENTITY FRAUD. 15 USC 1681 VIOLATIONS FOR WILLFUL NONCOMPLIANCE-616 CIVIL LIABILITY FOR WILLFUL NONCOPLIANCE. THIS IS THE THIRD TIME I'VE SUBMITTED A COMPLAINT, AND THE REPONSE I GET IS \" YOU CAN NOT LOCATE MY CREDIT REPORT! '' THIS IS CLEARLY NEGLIGENCE.\n", - "4. I do not know how this works, but I need it done or somehow corrected. My name is XXXX XXXX, XXXX XXXX XXXX XXXX TN XXXXMy SS XXXX DOB XXXX. I had some issues with my income being affected by the COVID-19PANDEMICSHUTDOWN. I was under the 1 CARESAct, Pub. L. 116-136, section 4021, codified at FCRAsection 623 ( a ) ( 1 ) ( F ) ( i ) ( I ), 15 U.S.C.1681s- 2 ( a ) ( 1 ) ( F ) ( i ) ( I ). I am requesting some accommodations so I care to protect the integrity of my credit file. US DEPT OF ED / XXXX # XXXX, # XXXX accounts are reporting on XXXX, XXXX The was 30,60, 90 DAYS LATEsince requested assistance due to the pandemic. I found a few accounts that I have never done any business with these companies and the accounts do not belong on my report : XXXX XXXX # XXXX, XXXX XXXX XXXX XXXX # XXXX. \n", - "\n", - "I have some issues with the misspelling of my name, my correct spelling is XXXX XXXX. Please remove any other variation of my name they are not correct. The following addresses do not belong to me please delete them : XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXXSC, XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX\n", - "5. I want to know if this is even legal?! How can they disclose information without knowing its a correct email?!\n", - "comment list 2:\n", - "1. Hello, my name is XXXX XXXX, and I am writing to delete the following information in my file. The items I need deleted are listed in the report. I am a victim of identity theft and did not make the charge. I ask that the items be deleted to correct my credit report. I reported the theft of my identity to the Federal Trade Commission and I also have enclosed copies of the Federal Trade Commissions Identity Theft Affidavit. Please delete the items as soon as possible. The accounts are being reported currently open and the accounts need to be closed. \n", - "XXXX account number XXXX opened on XX/XX/2022 for the amount {$530.00} XXXX XXXX XXXX account number XXXX opened on XX/XX/2022 for the amount of {$140.00} The accounts are being reported currently open and need to be closed immediately. \n", - "Based on, 15 U.S. Code 1681c2 a consumer reporting agency shall block the reporting of any information in the file of a consumer that the consumer identifies as information that resulted from an alleged identity theft, not later than 4 business days after the date of receipt. This account should not be furnished on my consumer report. As a consumer I am demanding the deletion of the accounts listed IMMEDIATELY.\n", - "2. To whom it may concern : My personal information was breach in the internet as result accounts had been open in my name, I was advise to fill out an Id theft report to help me deal with this situation, I have listed each one of the accounts that do not belong to me. This is my second request to remove unverified items in my report, but XXXX keep rposting these account with out providing any type of original document as the FCRA provide, you need to provide me with original documents or remove these account immediately.\n", - "3. Ive been Disputting my XXXX XXXX I opened this account and someone got my information and used my card, I contacted XXXX over and over, they removed the negative reporting from my XXXX report but still reporting it negative on my XXXX and Expean this is very unfair to me because Im a victim of identity theft\n", - "4. Today, XX/XX/2021, I received three items in the mail, one envelope containing an unsolicited debit card from Navy Federal credit Union and the other two, with a letter each describing The Important Rights on two accounts should these accounts become delinquent under New York law. \n", - "\n", - "First of all, I never applied for these accounts with Navy Federal, not have I authorized anyone to do so on my behalf. I immediately contacted Navy Federal via phone and was told I was most likely a victim of identity theft and that I should monitor my credit and use a credit monitoring service. I was also asked for my email and mailing information in order to receive a letter from them regarding this issue. \n", - "\n", - "My main concern is having someone using my identity to illegally open bank accounts and commit fraud, destroying my credit and finances in the process. This bank is in another state from where I reside. I have not lived in Virginia nor do I intend to do so in the foreseeable future.\n", - "5. My personal information ( including my SSN, Drivers License Info, Addresses, and more ) was stolen from a hacking, and Equifax did n't tell the public about the hack until more than a month after the hacking. During this time, three Equifax executives were caught inside trading. It really shows how Equifax cares about other people!\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "# The plain English request we will make of PaLM 2\n", "prompt = (\n", @@ -1465,42 +540,20 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": { "id": "mL5P0_3X04dE" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 66e3af22-91cb-400a-92c3-69e7cd12ee01 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from bigframes.ml.llm import PaLM2TextGenerator\n", "\n", -<<<<<<< HEAD "q_a_model = PaLM2TextGenerator()" -======= - "# Create a BigQuery Cloud resource connection\n", - "CONN_NAME = \"bqdf-llm\"\n", - "session = bf.get_global_session()\n", - "\n", - "connection = f\"{PROJECT_ID}.{REGION}.{CONN_NAME}\"\n", - "q_a_model = PaLM2TextGenerator(session=session, connection_name=connection)" ->>>>>>> origin/lmm-kmeans-notebook ] }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": { "id": "ICWHsqAW1FNk" }, @@ -1512,58 +565,11 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "metadata": { "id": "gB7e1LXU1pst" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 653add17-29be-408c-8882-064217f8556e is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 8fd16954-853a-45fd-80bc-65b1242429e2 is DONE. 8 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job d9929bcb-26ce-4844-b68e-f4a980b90ede is DONE. 171 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "' The first comment list is about people complaining about companies or services, while the second comment list is about people reporting identity theft or fraud.'" - ] - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Send the request for PaLM 2 to generate a response to our prompt\n", "major_difference = q_a_model.predict(df)\n", @@ -1585,11 +591,7 @@ "source": [ "# Summary and next steps\n", "\n", -<<<<<<< HEAD "You've used the ML and LLM capabilities of BigQuery DataFrames to help analyze and understand a large dataset of unstructured feedback.\n", -======= - "You've used BigQuery DataFrames' integration with LLM models (`bigframes.ml.llm`) to generate code samples, and have tranformed LLM output by creating and using a custom function in BigQuery DataFrames.\n", ->>>>>>> origin/lmm-kmeans-notebook "\n", "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." ] From 81125f9505ad98e89939769a8e1fcf30518705f0 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:58:14 -0800 Subject: [PATCH 168/921] feat: send warnings on LLM prediction partial failures (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/llm.py | 78 ++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 93e2ba825f4..78f3369daf6 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -17,6 +17,7 @@ from __future__ import annotations from typing import cast, Literal, Optional, Union +import warnings import bigframes from bigframes import clients, constants @@ -24,15 +25,22 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -_REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT = "text-bison" -_REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT = "text-bison-32k" -_TEXT_GENERATE_RESULT_COLUMN = "ml_generate_text_llm_result" +_TEXT_GENERATOR_BISON_ENDPOINT = "text-bison" +_TEXT_GENERATOR_BISON_32K_ENDPOINT = "text-bison-32k" +_TEXT_GENERATOR_ENDPOINTS = ( + _TEXT_GENERATOR_BISON_ENDPOINT, + _TEXT_GENERATOR_BISON_32K_ENDPOINT, +) -_REMOTE_EMBEDDING_GENERATOR_MODEL_ENDPOINT = "textembedding-gecko" -_REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT = ( - "textembedding-gecko-multilingual" +_EMBEDDING_GENERATOR_GECKO_ENDPOINT = "textembedding-gecko" +_EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT = "textembedding-gecko-multilingual" +_EMBEDDING_GENERATOR_ENDPOINTS = ( + _EMBEDDING_GENERATOR_GECKO_ENDPOINT, + _EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT, ) -_EMBED_TEXT_RESULT_COLUMN = "text_embedding" + +_ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" +_ML_EMBED_TEXT_STATUS = "ml_embed_text_status" class PaLM2TextGenerator(base.Predictor): @@ -90,18 +98,16 @@ def _create_bqml_model(self): connection_id=connection_name_parts[2], iam_role="aiplatform.user", ) - if self.model_name == _REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT: - options = { - "endpoint": _REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT, - } - elif self.model_name == _REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT: - options = { - "endpoint": _REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT, - } - else: + + if self.model_name not in _TEXT_GENERATOR_ENDPOINTS: raise ValueError( - f"Model name {self.model_name} is not supported. We only support {_REMOTE_TEXT_GENERATOR_MODEL_ENDPOINT} and {_REMOTE_TEXT_GENERATOR_32K_MODEL_ENDPOINT}." + f"Model name {self.model_name} is not supported. We only support {', '.join(_TEXT_GENERATOR_ENDPOINTS)}." ) + + options = { + "endpoint": self.model_name, + } + return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options ) @@ -182,7 +188,16 @@ def predict( "top_p": top_p, "flatten_json_output": True, } - return self._bqml_model.generate_text(X, options) + + df = self._bqml_model.generate_text(X, options) + + if (df[_ML_GENERATE_TEXT_STATUS] != "").any(): + warnings.warn( + f"Some predictions failed. Check column {_ML_GENERATE_TEXT_STATUS} for detailed status. You may want to filter the failed rows and retry.", + RuntimeWarning, + ) + + return df class PaLM2TextEmbeddingGenerator(base.Predictor): @@ -241,19 +256,15 @@ def _create_bqml_model(self): connection_id=connection_name_parts[2], iam_role="aiplatform.user", ) - if self.model_name == "textembedding-gecko": - options = { - "endpoint": _REMOTE_EMBEDDING_GENERATOR_MODEL_ENDPOINT, - } - elif self.model_name == _REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT: - options = { - "endpoint": _REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT, - } - else: + + if self.model_name not in _EMBEDDING_GENERATOR_ENDPOINTS: raise ValueError( - f"Model name {self.model_name} is not supported. We only support {_REMOTE_EMBEDDING_GENERATOR_MODEL_ENDPOINT} and {_REMOTE_EMBEDDING_GENERATOR_MUlTILINGUAL_MODEL_ENDPOINT}." + f"Model name {self.model_name} is not supported. We only support {', '.join(_EMBEDDING_GENERATOR_ENDPOINTS)}." ) + options = { + "endpoint": self.model_name, + } return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options ) @@ -284,4 +295,13 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: options = { "flatten_json_output": True, } - return self._bqml_model.generate_text_embedding(X, options) + + df = self._bqml_model.generate_text_embedding(X, options) + + if (df[_ML_EMBED_TEXT_STATUS] != "").any(): + warnings.warn( + f"Some predictions failed. Check column {_ML_EMBED_TEXT_STATUS} for detailed status. You may want to filter the failed rows and retry.", + RuntimeWarning, + ) + + return df From 52dfad281def82548751a276ce42b087dbb09f9a Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 17 Nov 2023 21:42:14 +0000 Subject: [PATCH 169/921] docs: code samples for `Series.where` and `Series.mask` (#217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated - `Series.where`: https://screenshot.googleplex.com/9XWHpMnwrzVPF9G - `Series.mask`: https://screenshot.googleplex.com/4cPvvzoVaVzoCDD Fixes internal issue 310981880 🦕 --- .../bigframes_vendored/pandas/core/series.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index c6d98075f57..01175dc0ef5 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1696,6 +1696,49 @@ def kurt(self): def where(self, cond, other): """Replace values where the condition is False. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([10, 11, 12, 13, 14]) + >>> s + 0 10 + 1 11 + 2 12 + 3 13 + 4 14 + dtype: Int64 + + You can filter the values in the Series based on a condition. The values + matching the condition would be kept, and not matching would be replaced. + The default replacement value is ``NA``. + + >>> s.where(s % 2 == 0) + 0 10 + 1 + 2 12 + 3 + 4 14 + dtype: Int64 + + You can specify a custom replacement value for non-matching values. + + >>> s.where(s % 2 == 0, -1) + 0 10 + 1 -1 + 2 12 + 3 -1 + 4 14 + dtype: Int64 + >>> s.where(s % 2 == 0, 100*s) + 0 10 + 1 1100 + 2 12 + 3 1300 + 4 14 + dtype: Int64 + Args: cond (bool Series/DataFrame, array-like, or callable): Where cond is True, keep the original value. Where False, replace @@ -1720,6 +1763,77 @@ def where(self, cond, other): def mask(self, cond, other): """Replace values where the condition is True. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([10, 11, 12, 13, 14]) + >>> s + 0 10 + 1 11 + 2 12 + 3 13 + 4 14 + dtype: Int64 + + You can mask the values in the Series based on a condition. The values + matching the condition would be masked. + + >>> s.mask(s % 2 == 0) + 0 + 1 11 + 2 + 3 13 + 4 + dtype: Int64 + + You can specify a custom mask value. + + >>> s.mask(s % 2 == 0, -1) + 0 -1 + 1 11 + 2 -1 + 3 13 + 4 -1 + dtype: Int64 + >>> s.mask(s % 2 == 0, 100*s) + 0 1000 + 1 11 + 2 1200 + 3 13 + 4 1400 + dtype: Int64 + + You can also use a remote function to evaluate the mask condition. This + is useful in situation such as the following, where the mask + condition is evaluated based on a complicated business logic which cannot + be expressed in form of a Series. + + >>> @bpd.remote_function([str], bool, reuse=False) + ... def should_mask(name): + ... hash = 0 + ... for char_ in name: + ... hash += ord(char_) + ... return hash % 2 == 0 + + >>> s = bpd.Series(["Alice", "Bob", "Caroline"]) + >>> s + 0 Alice + 1 Bob + 2 Caroline + dtype: string + >>> s.mask(should_mask) + 0 + 1 Bob + 2 Caroline + dtype: string + >>> s.mask(should_mask, "REDACTED") + 0 REDACTED + 1 Bob + 2 Caroline + dtype: string + Args: cond (bool Series/DataFrame, array-like, or callable): Where cond is False, keep the original value. Where True, replace From a18d40e808ee0822d21715cc3e8f794c418aeebc Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 17 Nov 2023 14:42:15 -0800 Subject: [PATCH 170/921] fix: avoid unnecessary row_number() on sort key for io (#211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 12 ++++++++---- bigframes/core/compile/compiled.py | 29 ++++++++++++++++++++--------- bigframes/dataframe.py | 12 ++++-------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index b476961bdc6..e19fec8f3f7 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -125,14 +125,18 @@ def to_sql( col_id_overrides: typing.Mapping[str, str] = {}, sorted: bool = False, ) -> str: - if sorted or offset_column: - return self._compile_ordered().to_sql( - offset_column=offset_column, + array_value = self + if offset_column: + array_value = self.promote_offsets(offset_column) + if sorted: + return array_value._compile_ordered().to_sql( col_id_overrides=col_id_overrides, sorted=sorted, ) else: - return self._compile_unordered().to_sql(col_id_overrides=col_id_overrides) + return array_value._compile_unordered().to_sql( + col_id_overrides=col_id_overrides + ) def start_query( self, diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 78050ed4f0c..461c2c005a6 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -1031,31 +1031,42 @@ def _reproject_to_table(self) -> OrderedIR: def to_sql( self, - offset_column: typing.Optional[str] = None, col_id_overrides: typing.Mapping[str, str] = {}, sorted: bool = False, ) -> str: - offsets_id = offset_column or ORDER_ID_COLUMN - sql = ibis_bigquery.Backend().compile( self._to_ibis_expr( - ordering_mode="offset_col" - if (offset_column or sorted) - else "unordered", - order_col_name=offsets_id, + ordering_mode="unordered", col_id_overrides=col_id_overrides, + expose_hidden_cols=sorted, ) ) if sorted: + output_columns = [ + col_id_overrides.get(col) if (col in col_id_overrides) else col + for col in self.column_ids + ] + selection = ", ".join(map(lambda col_id: f"`{col_id}`", output_columns)) + order_by_clause = self._ordering_clause(self._ordering.all_ordering_columns) + sql = textwrap.dedent( - f"SELECT * EXCEPT (`{offsets_id}`)\n" + f"SELECT {selection}\n" "FROM (\n" f"{sql}\n" ")\n" - f"ORDER BY `{offsets_id}`\n" + f"{order_by_clause}\n" ) return typing.cast(str, sql) + def _ordering_clause(self, ordering: Iterable[OrderingColumnReference]) -> str: + parts = [] + for col_ref in ordering: + asc_desc = "ASC" if col_ref.direction.is_ascending else "DESC" + null_clause = "NULLS LAST" if col_ref.na_last else "NULLS FIRST" + part = f"`{col_ref.column_id}` {asc_desc} {null_clause}" + parts.append(part) + return f"ORDER BY {' ,'.join(parts)}" + def _to_ibis_expr( self, *, diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 57b4ca42cfc..1f1275e217b 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2577,14 +2577,10 @@ def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: } if ordering_id is not None: - return array_value.to_sql( - offset_column=ordering_id, - col_id_overrides=id_overrides, - ) - else: - return array_value.to_sql( - col_id_overrides=id_overrides, - ) + array_value = array_value.promote_offsets(ordering_id) + return array_value.to_sql( + col_id_overrides=id_overrides, + ) def _run_io_query( self, From 010486c3494e05d714da6cc7d51514518d9ae1ea Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:38:14 -0800 Subject: [PATCH 171/921] docs: add code samples for df reshaping, function, merge, and join methods (#203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #310245117 --
[310245117](https://b.corp.google.com/issues/310245117) 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 218 +++++++++++++++++- 1 file changed, 217 insertions(+), 1 deletion(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index b35d0f3b2e8..8033c064d7b 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2121,6 +2121,59 @@ def groupby( used to group large amounts of data and compute operations on these groups. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'Animal': ['Falcon', 'Falcon', + ... 'Parrot', 'Parrot'], + ... 'Max Speed': [380., 370., 24., 26.]}) + >>> df + Animal Max Speed + 0 Falcon 380.0 + 1 Falcon 370.0 + 2 Parrot 24.0 + 3 Parrot 26.0 + + [4 rows x 2 columns] + + >>> df.groupby(['Animal'])['Max Speed'].mean() + Animal + Falcon 375.0 + Parrot 25.0 + Name: Max Speed, dtype: Float64 + + We can also choose to include NA in group keys or not by setting `dropna`: + + >>> df = bpd.DataFrame([[1, 2, 3],[1, None, 4], [2, 1, 3], [1, 2, 2]], + ... columns=["a", "b", "c"]) + >>> df.groupby(by=["b"]).sum() + a c + b + 1.0 2 3 + 2.0 2 5 + + [2 rows x 2 columns] + + >>> df.groupby(by=["b"], dropna=False).sum() + a c + b + 1.0 2 3 + 2.0 2 5 + 1 4 + + [3 rows x 2 columns] + + We can also choose to return object with group labels or not by setting `as_index`: + + >>> df.groupby(by=["b"], as_index=False).sum() + b a c + 0 1.0 2 3 + 1 2.0 2 5 + + [2 rows x 3 columns] + Args: by (str, Sequence[str]): A label or list of labels may be passed to group by the columns @@ -2224,7 +2277,7 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: Python function wrapped by ``remote_function`` decorator, returns a single value from a single value. na_action (Optional[str], default None): - ``{None, 'ignore'}``, default None. If ‘ignore’, propagate NaN + ``{None, 'ignore'}``, default None. If `ignore`, propagate NaN values, without passing them to func. Returns: @@ -2240,6 +2293,74 @@ def join(self, other, *, on: Optional[str] = None, how: str) -> DataFrame: Join columns with `other` DataFrame on index + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Join two DataFrames by specifying how to handle the operation: + + >>> df1 = bpd.DataFrame({'col1': ['foo', 'bar'], 'col2': [1, 2]}, index=[10, 11]) + >>> df1 + col1 col2 + 10 foo 1 + 11 bar 2 + + [2 rows x 2 columns] + + >>> df2 = bpd.DataFrame({'col3': ['foo', 'baz'], 'col4': [3, 4]}, index=[11, 22]) + >>> df2 + col3 col4 + 11 foo 3 + 22 baz 4 + + [2 rows x 2 columns] + + >>> df1.join(df2) + col1 col2 col3 col4 + 10 foo 1 + 11 bar 2 foo 3 + + [2 rows x 4 columns] + + >>> df1.join(df2, how="left") + col1 col2 col3 col4 + 10 foo 1 + 11 bar 2 foo 3 + + [2 rows x 4 columns] + + >>> df1.join(df2, how="right") + col1 col2 col3 col4 + 11 bar 2 foo 3 + 22 baz 4 + + [2 rows x 4 columns] + + >>> df1.join(df2, how="outer") + col1 col2 col3 col4 + 10 foo 1 + 11 bar 2 foo 3 + 22 baz 4 + + [3 rows x 4 columns] + + >>> df1.join(df2, how="inner") + col1 col2 col3 col4 + 11 bar 2 foo 3 + + [1 rows x 4 columns] + + + Another option to join using the key columns is to use the on parameter: + + >>> df1.join(df2, on="col1", how="right") + col1 col2 col3 col4 + 11 foo 3 + 22 baz 4 + + [2 rows x 4 columns] + Args: other: DataFrame with an Index similar to the Index of this one. @@ -2292,6 +2413,78 @@ def merge( rows will be matched against each other. This is different from usual SQL join behaviour and can lead to unexpected results. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Merge DataFrames df1 and df2 by specifiying type of merge: + + >>> df1 = bpd.DataFrame({'a': ['foo', 'bar'], 'b': [1, 2]}) + >>> df1 + a b + 0 foo 1 + 1 bar 2 + + [2 rows x 2 columns] + + >>> df2 = bpd.DataFrame({'a': ['foo', 'baz'], 'c': [3, 4]}) + >>> df2 + a c + 0 foo 3 + 1 baz 4 + + [2 rows x 2 columns] + + >>> df1.merge(df2, how="inner", on="a") + a b c + 0 foo 1 3 + + [1 rows x 3 columns] + + >>> df1.merge(df2, how='left', on='a') + a b c + 0 foo 1 3 + 1 bar 2 + + [2 rows x 3 columns] + + Merge df1 and df2 on the lkey and rkey columns. The value columns have + the default suffixes, _x and _y, appended. + + >>> df1 = bpd.DataFrame({'lkey': ['foo', 'bar', 'baz', 'foo'], + ... 'value': [1, 2, 3, 5]}) + >>> df1 + lkey value + 0 foo 1 + 1 bar 2 + 2 baz 3 + 3 foo 5 + + [4 rows x 2 columns] + + >>> df2 = bpd.DataFrame({'rkey': ['foo', 'bar', 'baz', 'foo'], + ... 'value': [5, 6, 7, 8]}) + >>> df2 + rkey value + 0 foo 5 + 1 bar 6 + 2 baz 7 + 3 foo 8 + + [4 rows x 2 columns] + + >>> df1.merge(df2, left_on='lkey', right_on='rkey') + lkey value_x rkey value_y + 0 foo 1 foo 5 + 1 foo 1 foo 8 + 2 bar 2 bar 6 + 3 baz 3 baz 7 + 4 foo 5 foo 5 + 5 foo 5 foo 8 + + [6 rows x 4 columns] + Args: right: Object to merge with. @@ -2342,6 +2535,29 @@ def apply(self, func, *, args=(), **kwargs): the DataFrame's index (``axis=0``) the final return type is inferred from the return type of the applied function. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df + col1 col2 + 0 1 3 + 1 2 4 + + [2 rows x 2 columns] + + >>> def sqaure(x): + ... return x * x + >>> df1 = df.apply(sqaure) + >>> df + col1 col2 + 0 1 3 + 1 2 4 + + [2 rows x 2 columns] + Args: func (function): Function to apply to each column or row. From dd78acb174545ba292776a642afcec46f8ee4a2a Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:36:14 -0800 Subject: [PATCH 172/921] fix: dedup special character (#209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: dedup special character * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 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 --- tests/unit/core/test_bf_utils.py | 6 +++--- third_party/bigframes_vendored/pandas/io/common.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/core/test_bf_utils.py b/tests/unit/core/test_bf_utils.py index fc34f35d9c2..10ce1fd09e3 100644 --- a/tests/unit/core/test_bf_utils.py +++ b/tests/unit/core/test_bf_utils.py @@ -25,7 +25,7 @@ def test_get_standardized_ids_columns(): "0", utils.UNNAMED_COLUMN_ID, "duplicate", - "duplicate.1", + "duplicate_1", "with_space", ] assert idx_ids == [] @@ -37,13 +37,13 @@ def test_get_standardized_ids_indexes(): col_ids, idx_ids = utils.get_standardized_ids(col_labels, idx_labels) - assert col_ids == ["duplicate.2"] + assert col_ids == ["duplicate_2"] assert idx_ids == [ "string", "0", utils.UNNAMED_INDEX_ID, "duplicate", - "duplicate.1", + "duplicate_1", "with_space", ] diff --git a/third_party/bigframes_vendored/pandas/io/common.py b/third_party/bigframes_vendored/pandas/io/common.py index 506984e64da..e186f02b5bd 100644 --- a/third_party/bigframes_vendored/pandas/io/common.py +++ b/third_party/bigframes_vendored/pandas/io/common.py @@ -13,13 +13,13 @@ def dedup_names( """ Rename column names if duplicates exist. - Currently the renaming is done by appending a period and an autonumeric, - but a custom pattern may be supported in the future. + Currently the renaming is done by appending a underscore and an + autonumeric, but a custom pattern may be supported in the future. Examples ``` dedup_names(["x", "y", "x", "x"], is_potential_multiindex=False) - ['x', 'y', 'x.1', 'x.2'] + ['x', 'y', 'x_1', 'x_2'] ``` """ names = list(names) # so we can index @@ -34,9 +34,9 @@ def dedup_names( if is_potential_multiindex: # for mypy assert isinstance(col, tuple) - col = col[:-1] + (f"{col[-1]}.{cur_count}",) + col = col[:-1] + (f"{col[-1]}_{cur_count}",) else: - col = f"{col}.{cur_count}" + col = f"{col}_{cur_count}" cur_count = counts[col] names[i] = col From c88d38e69682f4c620174086b8f16f4780c04811 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 20 Nov 2023 23:02:15 +0000 Subject: [PATCH 173/921] docs: add code samples for `index` and `column` properties (#212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated - `DataFrame.columns`: https://screenshot.googleplex.com/3Bwdb482FBfEsi2 - `DataFrame.index`: https://screenshot.googleplex.com/4iJymH3FxMn8Hhb - `Series.index`: https://screenshot.googleplex.com/7MXQcuASbQ3c8s5 Fixes internal issue 310260952 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 79 ++++++++++++++++++- .../bigframes_vendored/pandas/core/series.py | 49 +++++++++++- 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 8033c064d7b..f448ad79394 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3106,6 +3106,47 @@ def index(self): index is used for label-based access and alignment, and can be accessed or modified using this attribute. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can access the index of a DataFrame via ``index`` property. + + >>> df = bpd.DataFrame({'Name': ['Alice', 'Bob', 'Aritra'], + ... 'Age': [25, 30, 35], + ... 'Location': ['Seattle', 'New York', 'Kona']}, + ... index=([10, 20, 30])) + >>> df + Name Age Location + 10 Alice 25 Seattle + 20 Bob 30 New York + 30 Aritra 35 Kona + + [3 rows x 3 columns] + >>> df.index # doctest: +ELLIPSIS + + >>> df.index.values + array([10, 20, 30], dtype=object) + + Let's try setting a new index for the dataframe and see that reflect via + ``index`` property. + + >>> df1 = df.set_index(["Name", "Location"]) + >>> df1 + Age + Name Location + Alice Seattle 25 + Bob New York 30 + Aritra Kona 35 + + [3 rows x 1 columns] + >>> df1.index # doctest: +ELLIPSIS + + >>> df1.index.values + array([('Alice', 'Seattle'), ('Bob', 'New York'), ('Aritra', 'Kona')], + dtype=object) + Returns: The index labels of the DataFrame. """ @@ -3113,7 +3154,43 @@ def index(self): @property def columns(self): - "The column labels of the DataFrame." + """The column labels of the DataFrame. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can access the column labels of a DataFrame via ``columns`` property. + + >>> df = bpd.DataFrame({'Name': ['Alice', 'Bob', 'Aritra'], + ... 'Age': [25, 30, 35], + ... 'Location': ['Seattle', 'New York', 'Kona']}, + ... index=([10, 20, 30])) + >>> df + Name Age Location + 10 Alice 25 Seattle + 20 Bob 30 New York + 30 Aritra 35 Kona + + [3 rows x 3 columns] + >>> df.columns + Index(['Name', 'Age', 'Location'], dtype='object') + + You can also set new labels for columns. + + >>> df.columns = ["NewName", "NewAge", "NewLocation"] + >>> df + NewName NewAge NewLocation + 10 Alice 25 Seattle + 20 Bob 30 New York + 30 Aritra 35 Kona + + [3 rows x 3 columns] + >>> df.columns + Index(['NewName', 'NewAge', 'NewLocation'], dtype='object') + + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def value_counts( diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 01175dc0ef5..a86765a4125 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -44,7 +44,54 @@ def struct(self): @property def index(self): - """The index (axis labels) of the Series.""" + """The index (axis labels) of the Series. + + The index of a Series is used to label and identify each element of the + underlying data. The index can be thought of as an immutable ordered set + (technically a multi-set, as it may contain duplicate labels), and is + used to index and align data. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can access the index of a Series via ``index`` property. + + >>> df = bpd.DataFrame({'Name': ['Alice', 'Bob', 'Aritra'], + ... 'Age': [25, 30, 35], + ... 'Location': ['Seattle', 'New York', 'Kona']}, + ... index=([10, 20, 30])) + >>> s = df["Age"] + >>> s + 10 25 + 20 30 + 30 35 + Name: Age, dtype: Int64 + >>> s.index # doctest: +ELLIPSIS + + >>> s.index.values + array([10, 20, 30], dtype=object) + + Let's try setting a multi-index case reflect via ``index`` property. + + >>> df1 = df.set_index(["Name", "Location"]) + >>> s1 = df1["Age"] + >>> s1 + Name Location + Alice Seattle 25 + Bob New York 30 + Aritra Kona 35 + Name: Age, dtype: Int64 + >>> s1.index # doctest: +ELLIPSIS + + >>> s1.index.values + array([('Alice', 'Seattle'), ('Bob', 'New York'), ('Aritra', 'Kona')], + dtype=object) + + Returns: + The index labels of the Series. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property From 1d1477158b5a8e84d099e07c6f566182a1abd7fc Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 21 Nov 2023 16:16:25 +0000 Subject: [PATCH 174/921] test: re-enable `system_prerelease` tests (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: re-enable `system_prerelease` tests * exclude ibis from prerelease install list * install explicit 6.2.0 version for ibis in pre prelease * add unit_prerelease to pre and post submit e2e tests * Update noxfile.py * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Tim Swast Co-authored-by: Owl Bot --- .kokoro/continuous/e2e.cfg | 2 +- .kokoro/presubmit/e2e.cfg | 2 +- noxfile.py | 50 ++++++++++++++++++++++++++++++++------ 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.kokoro/continuous/e2e.cfg b/.kokoro/continuous/e2e.cfg index d875f360603..2f93a582124 100644 --- a/.kokoro/continuous/e2e.cfg +++ b/.kokoro/continuous/e2e.cfg @@ -3,5 +3,5 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "system_noextras e2e notebook samples" + value: "unit_prerelease system_prerelease system_noextras e2e notebook samples" } diff --git a/.kokoro/presubmit/e2e.cfg b/.kokoro/presubmit/e2e.cfg index d875f360603..2f93a582124 100644 --- a/.kokoro/presubmit/e2e.cfg +++ b/.kokoro/presubmit/e2e.cfg @@ -3,5 +3,5 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "system_noextras e2e notebook samples" + value: "unit_prerelease system_prerelease system_noextras e2e notebook samples" } diff --git a/noxfile.py b/noxfile.py index da61232fc79..8d6d641fc1e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -494,6 +494,11 @@ def prerelease(session: nox.sessions.Session, tests_path): CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) + # Ignore officially released versions of certain packages specified in + # testing/constraints-*.txt and install a more recent, pre-release versions + # directly + already_installed = set() + # PyArrow prerelease packages are published to an alternative PyPI host. # https://arrow.apache.org/docs/python/install.html#installing-nightly-packages session.install( @@ -504,6 +509,8 @@ def prerelease(session: nox.sessions.Session, tests_path): "--upgrade", "pyarrow", ) + already_installed.add("pyarrow") + session.install( "--extra-index-url", "https://pypi.anaconda.org/scipy-wheels-nightly/simple", @@ -512,16 +519,48 @@ def prerelease(session: nox.sessions.Session, tests_path): "--upgrade", "pandas", ) + already_installed.add("pandas") + + # TODO(shobs): + # Commit https://github.com/ibis-project/ibis/commit/c20ba7feab6bdea6c299721310e04dbc10551cc2 + # introduced breaking change that removed the following: + # ibis.expr.rules.column + # ibis.expr.rules.value + # ibis.expr.rules.any + # Let's exclude ibis head from prerelease install list for now. Instead, use + # a working ibis-framework version resolved via setup.by (currently resolves + # to version 6.2.0 due to version requirement "6.2.0,<7.0.0dev"). + # We should enable the head back once bigframes support a version that + # includes the above commit. + # session.install( + # "--upgrade", + # "-e", # Use -e so that py.typed file is included. + # "git+https://github.com/ibis-project/ibis.git#egg=ibis-framework", + # ) + session.install("--no-deps", "ibis-framework==6.2.0") + already_installed.add("ibis-framework") + + # Workaround https://github.com/googleapis/python-db-dtypes-pandas/issues/178 + session.install("--no-deps", "db-dtypes") + already_installed.add("db-dtypes") + + # Ensure we catch breaking changes in the client libraries early. + session.install( + "--upgrade", + "-e", + "git+https://github.com/googleapis/python-bigquery.git#egg=google-cloud-bigquery", + ) + already_installed.add("google-cloud-bigquery") session.install( "--upgrade", - "-e", # Use -e so that py.typed file is included. - "git+https://github.com/ibis-project/ibis.git#egg=ibis-framework", + "-e", + "git+https://github.com/googleapis/python-bigquery-storage.git#egg=google-cloud-bigquery-storage", ) - # Workaround https://github.com/googleapis/python-db-dtypes-pandas/issues/178 - session.install("--no-deps", "db-dtypes") + already_installed.add("google-cloud-bigquery-storage") # Workaround to install pandas-gbq >=0.15.0, which is required by test only. session.install("--no-deps", "pandas-gbq") + already_installed.add("pandas-gbq") session.install( *set(UNIT_TEST_STANDARD_DEPENDENCIES + SYSTEM_TEST_STANDARD_DEPENDENCIES), @@ -541,9 +580,6 @@ def prerelease(session: nox.sessions.Session, tests_path): constraints_text = constraints_file.read() # Ignore leading whitespace and comment lines. - already_installed = frozenset( - ("db-dtypes", "pandas", "pyarrow", "ibis-framework", "pandas-gbq") - ) deps = [ match.group(1) for match in re.finditer( From ed8876d3439a3b45b65e8789737c3c2e3a7f1adb Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:02:31 -0800 Subject: [PATCH 175/921] feat: add the recent api method for ML component (#225) * feat: add the recent api method for ML component --- bigframes/ml/cluster.py | 2 ++ bigframes/ml/compose.py | 2 ++ bigframes/ml/decomposition.py | 2 ++ bigframes/ml/ensemble.py | 5 +++++ bigframes/ml/forecasting.py | 2 ++ bigframes/ml/imported.py | 3 +++ bigframes/ml/linear_model.py | 3 +++ bigframes/ml/llm.py | 4 +++- bigframes/ml/pipeline.py | 2 ++ bigframes/ml/preprocessing.py | 7 +++++++ tests/unit/session/test_io_bigquery.py | 3 +++ 11 files changed, 34 insertions(+), 1 deletion(-) diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index c9f52ba0b6b..6b79d356a23 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -22,11 +22,13 @@ from google.cloud import bigquery import bigframes +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.cluster._kmeans +@log_adapter.class_logger class KMeans( base.UnsupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans, diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index bf046ff6914..ace876dd2df 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -22,6 +22,7 @@ from typing import List, Optional, Tuple, Union from bigframes import constants +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, preprocessing, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.compose._column_transformer @@ -36,6 +37,7 @@ ] +@log_adapter.class_logger class ColumnTransformer( base.Transformer, third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 7cda7a69933..ef777cb33ac 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -22,11 +22,13 @@ from google.cloud import bigquery import bigframes +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.decomposition._pca +@log_adapter.class_logger class PCA( base.UnsupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.decomposition._pca.PCA, diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index fcb3fe53438..1cc9fb37395 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -22,6 +22,7 @@ from google.cloud import bigquery import bigframes +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.ensemble._forest @@ -47,6 +48,7 @@ } +@log_adapter.class_logger class XGBRegressor( base.SupervisedTrainablePredictor, third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor, @@ -202,6 +204,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: return new_model.session.read_gbq_model(model_name) +@log_adapter.class_logger class XGBClassifier( base.SupervisedTrainablePredictor, third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier, @@ -356,6 +359,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: return new_model.session.read_gbq_model(model_name) +@log_adapter.class_logger class RandomForestRegressor( base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor, @@ -521,6 +525,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegresso return new_model.session.read_gbq_model(model_name) +@log_adapter.class_logger class RandomForestClassifier( base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier, diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index cf23854fa01..995201062bc 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -21,10 +21,12 @@ from google.cloud import bigquery import bigframes +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +@log_adapter.class_logger class ARIMAPlus(base.SupervisedTrainablePredictor): """Time Series ARIMA Plus model.""" diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index f6afc9aa382..4ae0a8ea4d0 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -21,10 +21,12 @@ from google.cloud import bigquery import bigframes +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +@log_adapter.class_logger class TensorFlowModel(base.Predictor): """Imported TensorFlow model. @@ -101,6 +103,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> TensorFlowModel: return new_model.session.read_gbq_model(model_name) +@log_adapter.class_logger class ONNXModel(base.Predictor): """Imported Open Neural Network Exchange (ONNX) model. diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 433d9fbc380..5ee87b88505 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -23,6 +23,7 @@ import bigframes import bigframes.constants as constants +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.linear_model._base @@ -46,6 +47,7 @@ } +@log_adapter.class_logger class LinearRegression( base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression, @@ -178,6 +180,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: return new_model.session.read_gbq_model(model_name) +@log_adapter.class_logger class LogisticRegression( base.SupervisedTrainablePredictor, third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression, diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 78f3369daf6..5beb54a32d4 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -21,7 +21,7 @@ import bigframes from bigframes import clients, constants -from bigframes.core import blocks +from bigframes.core import blocks, log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd @@ -43,6 +43,7 @@ _ML_EMBED_TEXT_STATUS = "ml_embed_text_status" +@log_adapter.class_logger class PaLM2TextGenerator(base.Predictor): """PaLM2 text generator LLM model. @@ -200,6 +201,7 @@ def predict( return df +@log_adapter.class_logger class PaLM2TextEmbeddingGenerator(base.Predictor): """PaLM2 text embedding generator LLM model. diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index ad0b3fae111..4ae2bfe5552 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -24,11 +24,13 @@ import bigframes import bigframes.constants as constants +from bigframes.core import log_adapter from bigframes.ml import base, compose, forecasting, loader, preprocessing, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.pipeline +@log_adapter.class_logger class Pipeline( base.BaseEstimator, third_party.bigframes_vendored.sklearn.pipeline.Pipeline, diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 5f44d402184..a403e57e715 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -20,6 +20,7 @@ import typing from typing import Any, cast, List, Literal, Optional, Tuple, Union +from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd import third_party.bigframes_vendored.sklearn.preprocessing._data @@ -28,6 +29,7 @@ import third_party.bigframes_vendored.sklearn.preprocessing._label +@log_adapter.class_logger class StandardScaler( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler, @@ -111,6 +113,7 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +@log_adapter.class_logger class MaxAbsScaler( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._data.MaxAbsScaler, @@ -194,6 +197,7 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +@log_adapter.class_logger class MinMaxScaler( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._data.MinMaxScaler, @@ -277,6 +281,7 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +@log_adapter.class_logger class KBinsDiscretizer( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._discretization.KBinsDiscretizer, @@ -395,6 +400,7 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +@log_adapter.class_logger class OneHotEncoder( base.Transformer, third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, @@ -524,6 +530,7 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +@log_adapter.class_logger class LabelEncoder( base.LabelTransformer, third_party.bigframes_vendored.sklearn.preprocessing._label.LabelEncoder, diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index e1481d3f05c..c87835c4126 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -59,6 +59,7 @@ def test_create_job_configs_labels_length_limit_not_met(): def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): + log_adapter.get_and_reset_api_methods() cur_labels = { "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", @@ -87,6 +88,7 @@ def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): def test_create_job_configs_labels_length_limit_met_and_labels_is_none(): + log_adapter.get_and_reset_api_methods() df = bpd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) # Test running methods more than the labels' length limit for i in range(66): @@ -102,6 +104,7 @@ def test_create_job_configs_labels_length_limit_met_and_labels_is_none(): def test_create_job_configs_labels_length_limit_met(): + log_adapter.get_and_reset_api_methods() cur_labels = { "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", From d7957fad071d223ef8f6fb8f3de395c865ff60aa Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 21 Nov 2023 21:26:52 -0800 Subject: [PATCH 176/921] docs: code samples for dataframe.any, dataframe.all and dataframe.prod (#223) * docs: code samples for dataframe.any, dataframe.all and dataframe.prod * Update examples * update example output --- .../bigframes_vendored/pandas/core/frame.py | 85 ++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index f448ad79394..b771be30418 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2584,6 +2584,33 @@ def any(self, *, axis=0, bool_only: bool = False): along a Dataframe axis that is True or equivalent (e.g. non-zero or non-empty). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [True, True], "B": [False, False]}) + >>> df + A B + 0 True False + 1 True False + + [2 rows x 2 columns] + + Checking if each column contains at least one True element(the default behavior without an explicit axis parameter). + + >>> df.any() + A True + B False + dtype: boolean + + Checking if each row contains at least one True element. + + >>> df.any(axis=1) + 0 True + 1 True + dtype: boolean + Args: axis ({index (0), columns (1)}): Axis for the function to be applied on. @@ -2604,6 +2631,33 @@ def all(self, axis=0, *, bool_only: bool = False): along a DataFrame axis that is False or equivalent (e.g. zero or empty). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [True, True], "B": [False, False]}) + >>> df + A B + 0 True False + 1 True False + + [2 rows x 2 columns] + + Checking if all values in each column are True(the default behavior without an explicit axis parameter). + + >>> df.all() + A True + B False + dtype: boolean + + Checking across rows to see if all values are True. + + >>> df.all(axis=1) + 0 False + 1 False + dtype: boolean + Args: axis ({index (0), columns (1)}): Axis for the function to be applied on. @@ -2620,8 +2674,37 @@ def prod(self, axis=0, *, numeric_only: bool = False): """ Return the product of the values over the requested axis. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 2, 3], "B": [4.5, 5.5, 6.5]}) + >>> df + A B + 0 1 4.5 + 1 2 5.5 + 2 3 6.5 + + [3 rows x 2 columns] + + Calculating the product of each column(the default behavior without an explicit axis parameter). + + >>> df.prod() + A 6.0 + B 160.875 + dtype: Float64 + + Calculating the product of each row. + + >>> df.prod(axis=1) + 0 4.5 + 1 11.0 + 2 19.5 + dtype: Float64 + Args: - aßxis ({index (0), columns (1)}): + axis ({index (0), columns (1)}): Axis for the function to be applied on. For Series this parameter is unused and defaults to 0. numeric_only (bool. default False): From 71844b03cdbfe684320c186a0488c8c7fb4fcd6e Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 22 Nov 2023 23:46:14 +0000 Subject: [PATCH 177/921] docs: make the code samples reflect default bq connection usage (#206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 305136837 🦕 --- .../getting_started_bq_dataframes.ipynb | 4 +- .../remote_functions/remote_function.ipynb | 514 ++++++++---------- samples/snippets/remote_function.py | 6 +- 3 files changed, 241 insertions(+), 283 deletions(-) diff --git a/notebooks/getting_started/getting_started_bq_dataframes.ipynb b/notebooks/getting_started/getting_started_bq_dataframes.ipynb index 6cc6acc9935..18be5e48fd1 100644 --- a/notebooks/getting_started/getting_started_bq_dataframes.ipynb +++ b/notebooks/getting_started/getting_started_bq_dataframes.ipynb @@ -802,7 +802,7 @@ "source": [ "Running the cell below creates a custom function using the `remote_function` method. This function categorizes a value into one of two buckets: >= 4000 or <4000.\n", "\n", - "> Note: Creating a function requires a [BigQuery connection](https://cloud.google.com/bigquery/docs/remote-functions#create_a_remote_function). This code assumes a pre-created connection named `bigframes-rf-conn`. If\n", + "> Note: Creating a function requires a [BigQuery connection](https://cloud.google.com/bigquery/docs/remote-functions#create_a_remote_function). This code assumes a pre-created connection named `bigframes-default-connection`. If\n", "the connection is not already created, BigQuery DataFrames attempts to create one assuming the [necessary APIs\n", "and IAM permissions](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.pandas#bigframes_pandas_remote_function) are set up in the project.\n", "\n", @@ -817,7 +817,7 @@ }, "outputs": [], "source": [ - "@bf.remote_function([float], str, bigquery_connection='bigframes-rf-conn')\n", + "@bf.remote_function([float], str)\n", "def get_bucket(num):\n", " if not num: return \"NA\"\n", " boundary = 4000\n", diff --git a/notebooks/remote_functions/remote_function.ipynb b/notebooks/remote_functions/remote_function.ipynb index 06be0e7293e..063c1738b4d 100644 --- a/notebooks/remote_functions/remote_function.ipynb +++ b/notebooks/remote_functions/remote_function.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 19, "id": "3613b1cd", "metadata": {}, "outputs": [], @@ -16,24 +16,16 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 20, "id": "f1175247", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", - " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n" - ] - }, { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 25.4 s, sys: 2.5 s, total: 27.9 s\n", - "Wall time: 2min 31s\n" + "CPU times: user 2.34 s, sys: 307 ms, total: 2.65 s\n", + "Wall time: 17.8 s\n" ] }, { @@ -141,7 +133,7 @@ "9 154 Sure, but what about a solution using O(1) mem... 8" ] }, - "execution_count": 3, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -160,7 +152,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 21, "id": "fd8a04a3", "metadata": {}, "outputs": [], @@ -191,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 22, "id": "2b5e4568", "metadata": {}, "outputs": [ @@ -199,8 +191,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 4.22 s, sys: 18.2 ms, total: 4.24 s\n", - "Wall time: 4.26 s\n" + "CPU times: user 3.32 s, sys: 0 ns, total: 3.32 s\n", + "Wall time: 3.32 s\n" ] }, { @@ -319,7 +311,7 @@ "9 154 Sure, but what about a solution using O(1) mem... 8 19" ] }, - "execution_count": 5, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -333,65 +325,25 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 23, "id": "b81feaef", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", - " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n", - "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/google/auth/_default.py:78: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. You might receive a \"quota exceeded\" or \"API not enabled\" error. See the following page for troubleshooting: https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \n", - " warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2b1c9d671db14d2ca3be6a0b0c698430", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 6b0a39de-40a0-4dd4-be88-248bd8ebcd77 is RUNNING. " ] }, "metadata": {}, @@ -399,13 +351,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "09706700e8dd4cf39f65a0d58371c1eb", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job a283cb39-41b1-44cd-a6c3-f2a2c6a55b25 is DONE. 17.2 GB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 4c1d9d3e-be25-4818-b74d-6214164d99ab is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -440,62 +390,62 @@ " \n", " \n", " 0\n", - " 11012908\n", - " you're welcome! according to the docs it shoul...\n", + " 11231597\n", + " In your update, why are some of the system fun...\n", " 0\n", " \n", " \n", " 1\n", - " 11013760\n", - " You *should* be concerned with the disk being ...\n", - " 0\n", + " 49684807\n", + " what you have tried so far . ??\n", + " 1\n", " \n", " \n", " 2\n", - " 11013784\n", - " have you looked at `Integrate` or `NIntegrate`?\n", + " 7623925\n", + " @Michael: It should work. Perhaps you looked i...\n", " 0\n", " \n", " \n", " 3\n", - " 11015512\n", - " sorry, is a typo. The variable name is dist. (...\n", + " 34046685\n", + " Will it work with SQL compact? Please excuse m...\n", " 0\n", " \n", " \n", " 4\n", - " 11016238\n", - " Pfff, I'm having trouble with that formula too...\n", + " 6426146\n", + " do you know the equation to your pdf?\n", " 0\n", " \n", " \n", " 5\n", - " 11016276\n", - " Thanks thinksteep! Does this mean that by usin...\n", + " 60686114\n", + " m sorry but at least you have to think about it.\n", " 0\n", " \n", " \n", " 6\n", - " 11016551\n", - " Jason, thanks for the reply. I've been workin...\n", + " 16631986\n", + " i think also making disable this by only jquer...\n", " 0\n", " \n", " \n", " 7\n", - " 11017973\n", - " I assume an `off` of 0.5 would put be exactly ...\n", + " 16498565\n", + " I am including these files on my header of the...\n", " 0\n", " \n", " \n", " 8\n", - " 11018225\n", - " Thank you very much. I do worry too much abou...\n", + " 26601001\n", + " wrong answer, you didn't understand the logic\n", " 0\n", " \n", " \n", " 9\n", - " 11018370\n", - " @IanClelland, I edited my question a bit. The ...\n", + " 73255842\n", + " Call the setOnClickListener before return row.\n", " 0\n", " \n", " \n", @@ -505,21 +455,21 @@ ], "text/plain": [ " id text score\n", - "0 11012908 you're welcome! according to the docs it shoul... 0\n", - "1 11013760 You *should* be concerned with the disk being ... 0\n", - "2 11013784 have you looked at `Integrate` or `NIntegrate`? 0\n", - "3 11015512 sorry, is a typo. The variable name is dist. (... 0\n", - "4 11016238 Pfff, I'm having trouble with that formula too... 0\n", - "5 11016276 Thanks thinksteep! Does this mean that by usin... 0\n", - "6 11016551 Jason, thanks for the reply. I've been workin... 0\n", - "7 11017973 I assume an `off` of 0.5 would put be exactly ... 0\n", - "8 11018225 Thank you very much. I do worry too much abou... 0\n", - "9 11018370 @IanClelland, I edited my question a bit. The ... 0\n", + "0 11231597 In your update, why are some of the system fun... 0\n", + "1 49684807 what you have tried so far . ?? 1\n", + "2 7623925 @Michael: It should work. Perhaps you looked i... 0\n", + "3 34046685 Will it work with SQL compact? Please excuse m... 0\n", + "4 6426146 do you know the equation to your pdf? 0\n", + "5 60686114 m sorry but at least you have to think about it. 0\n", + "6 16631986 i think also making disable this by only jquer... 0\n", + "7 16498565 I am including these files on my header of the... 0\n", + "8 26601001 wrong answer, you didn't understand the logic 0\n", + "9 73255842 Call the setOnClickListener before return row. 0\n", "\n", "[10 rows x 3 columns]" ] }, - "execution_count": 6, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -539,7 +489,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 24, "id": "55ed241e", "metadata": {}, "outputs": [ @@ -549,8 +499,9 @@ "text": [ "Help on function remote_function in module bigframes.pandas:\n", "\n", - "remote_function(input_types: 'List[type]', output_type: 'type', dataset: 'Optional[str]' = None, bigquery_connection: 'Optional[str]' = None, reuse: 'bool' = True)\n", - " Decorator to turn a user defined function into a BigQuery remote function.\n", + "remote_function(input_types: 'List[type]', output_type: 'type', dataset: 'Optional[str]' = None, bigquery_connection: 'Optional[str]' = None, reuse: 'bool' = True, name: 'Optional[str]' = None, packages: 'Optional[Sequence[str]]' = None)\n", + " Decorator to turn a user defined function into a BigQuery remote function. Check out\n", + " the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes.\n", " \n", " .. note::\n", " Please make sure following is setup before using this API:\n", @@ -576,7 +527,7 @@ " * BigQuery Data Editor (roles/bigquery.dataEditor)\n", " * BigQuery Connection Admin (roles/bigquery.connectionAdmin)\n", " * Cloud Functions Developer (roles/cloudfunctions.developer)\n", - " * Service Account User (roles/iam.serviceAccountUser)\n", + " * Service Account User (roles/iam.serviceAccountUser) on the service account `PROJECT_NUMBER-compute@developer.gserviceaccount.com`\n", " * Storage Object Viewer (roles/storage.objectViewer)\n", " * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.)\n", " \n", @@ -602,15 +553,25 @@ " Name of the BigQuery connection. You should either have the\n", " connection already created in the `location` you have chosen, or\n", " you should have the Project IAM Admin role to enable the service\n", - " to create the connection for you if you need it.If this parameter is\n", + " to create the connection for you if you need it. If this parameter is\n", " not provided then the BigQuery connection from the session is used.\n", " reuse (bool, Optional):\n", " Reuse the remote function if already exists.\n", " `True` by default, which will result in reusing an existing remote\n", - " function (if any) that was previously created for the same udf.\n", - " Setting it to false would force creating a unique remote function.\n", + " function and corresponding cloud function (if any) that was\n", + " previously created for the same udf.\n", + " Setting it to `False` would force creating a unique remote function.\n", " If the required remote function does not exist then it would be\n", " created irrespective of this param.\n", + " name (str, Optional):\n", + " Explicit name of the persisted BigQuery remote function. Use it with\n", + " caution, because two users working in the same project and dataset\n", + " could overwrite each other's remote functions if they use the same\n", + " persistent name.\n", + " packages (str[], Optional):\n", + " Explicit name of the external package dependencies. Each dependency\n", + " is added to the `requirements.txt` as is, and can be of the form\n", + " supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/.\n", " Returns:\n", " callable: A remote function object pointing to the cloud assets created\n", " in the background to support the remote execution. The cloud assets can be\n", @@ -631,49 +592,16 @@ }, { "cell_type": "code", - "execution_count": 8, - "id": "c9a8d03d", - "metadata": {}, - "outputs": [], - "source": [ - "# BigQuery DataFrames user is a data scientist and may not have privileges to\n", - "# create a BQ connector and set it up for invoking a cloud function. They\n", - "# should get such a connector created from their cloud admin and use it with\n", - "# BigQuery DataFrames remote functions. If the provided connection name does not\n", - "# exist, BigQuery DataFrames will try to create it on the fly assuming the user\n", - "# has sufficient privileges.\n", - "bq_connection_name = 'bigframes-rf-conn'" - ] - }, - { - "cell_type": "code", - "execution_count": 9, + "execution_count": 25, "id": "fbc27f81", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO][2023-08-18 21:23:29,687][bigframes.remote_function] Creating new cloud function: gcloud functions deploy bigframes-b0feb1fbaf8188b64d7e70118d93c5d4 --gen2 --runtime=python310 --project=bigframes-dev --region=us-central1 --source=/tmp/tmpl2ewfnue --entry-point=udf_http --trigger-http --no-allow-unauthenticated\n", - "[INFO][2023-08-18 21:24:43,689][bigframes.remote_function] Successfully created cloud function bigframes-b0feb1fbaf8188b64d7e70118d93c5d4 with uri (https://bigframes-b0feb1fbaf8188b64d7e70118d93c5d4-7krlje3eoq-uc.a.run.app)\n", - "[INFO][2023-08-18 21:24:57,348][bigframes.remote_function] Connector bigframes-rf-conn already exists\n", - "[INFO][2023-08-18 21:24:57,351][bigframes.remote_function] Creating BQ remote function: \n", - " CREATE OR REPLACE FUNCTION `bigframes-dev.bigframes_temp_us`.bigframes_b0feb1fbaf8188b64d7e70118d93c5d4(n INT64)\n", - " RETURNS INT64\n", - " REMOTE WITH CONNECTION `bigframes-dev.us.bigframes-rf-conn`\n", - " OPTIONS (\n", - " endpoint = \"https://bigframes-b0feb1fbaf8188b64d7e70118d93c5d4-7krlje3eoq-uc.a.run.app\"\n", - " )\n", - "[INFO][2023-08-18 21:24:58,300][bigframes.remote_function] Created remote function bigframes-dev.bigframes_temp_us.bigframes_b0feb1fbaf8188b64d7e70118d93c5d4\n" - ] - }, { "name": "stdout", "output_type": "stream", "text": [ "\n", - "Wall time: 89.0601 s\n" + "Wall time: 76.2628 s\n" ] } ], @@ -684,7 +612,7 @@ "\n", "# User defined function\n", "# https://www.codespeedy.com/find-nth-prime-number-in-python/\n", - "@pd.remote_function([int], int, bigquery_connection=bq_connection_name)\n", + "@pd.remote_function([int], int, reuse=False)\n", "def nth_prime(n):\n", " prime_numbers = [2,3]\n", " i=3\n", @@ -712,7 +640,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 26, "id": "c1c9355f", "metadata": {}, "outputs": [ @@ -720,33 +648,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 16.8 ms, sys: 61 µs, total: 16.8 ms\n", - "Wall time: 17 ms\n" + "CPU times: user 55.8 ms, sys: 182 µs, total: 56 ms\n", + "Wall time: 54.5 ms\n" ] }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2f840ad27c514ed19c759a004b32de33", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 0f421233-9d02-4746-bb39-86a3b0880aba is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job 4f8d5734-8070-4630-8a59-c05a31d60476 is RUNNING. " ] }, "metadata": {}, @@ -754,13 +666,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "116d6ef3d6b247d3aaafef5fe6b970de", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job c0a2c187-364d-4978-97bc-30352828f624 is DONE. 17.2 GB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job ec057f9e-726b-44f0-a5c0-24c05c7ecfeb is RUNNING. " ] }, "metadata": {}, @@ -796,71 +706,71 @@ " \n", " \n", " 0\n", - " 11012908\n", - " you're welcome! according to the docs it shoul...\n", + " 11231597\n", + " In your update, why are some of the system fun...\n", " 0\n", " -1\n", " \n", " \n", " 1\n", - " 11013760\n", - " You *should* be concerned with the disk being ...\n", - " 0\n", - " -1\n", + " 49684807\n", + " what you have tried so far . ??\n", + " 1\n", + " 2\n", " \n", " \n", " 2\n", - " 11013784\n", - " have you looked at `Integrate` or `NIntegrate`?\n", + " 7623925\n", + " @Michael: It should work. Perhaps you looked i...\n", " 0\n", " -1\n", " \n", " \n", " 3\n", - " 11015512\n", - " sorry, is a typo. The variable name is dist. (...\n", + " 34046685\n", + " Will it work with SQL compact? Please excuse m...\n", " 0\n", " -1\n", " \n", " \n", " 4\n", - " 11016238\n", - " Pfff, I'm having trouble with that formula too...\n", + " 6426146\n", + " do you know the equation to your pdf?\n", " 0\n", " -1\n", " \n", " \n", " 5\n", - " 11016276\n", - " Thanks thinksteep! Does this mean that by usin...\n", + " 60686114\n", + " m sorry but at least you have to think about it.\n", " 0\n", " -1\n", " \n", " \n", " 6\n", - " 11016551\n", - " Jason, thanks for the reply. I've been workin...\n", + " 16631986\n", + " i think also making disable this by only jquer...\n", " 0\n", " -1\n", " \n", " \n", " 7\n", - " 11017973\n", - " I assume an `off` of 0.5 would put be exactly ...\n", + " 16498565\n", + " I am including these files on my header of the...\n", " 0\n", " -1\n", " \n", " \n", " 8\n", - " 11018225\n", - " Thank you very much. I do worry too much abou...\n", + " 26601001\n", + " wrong answer, you didn't understand the logic\n", " 0\n", " -1\n", " \n", " \n", " 9\n", - " 11018370\n", - " @IanClelland, I edited my question a bit. The ...\n", + " 73255842\n", + " Call the setOnClickListener before return row.\n", " 0\n", " -1\n", " \n", @@ -871,21 +781,21 @@ ], "text/plain": [ " id text score n_prime\n", - "0 11012908 you're welcome! according to the docs it shoul... 0 -1\n", - "1 11013760 You *should* be concerned with the disk being ... 0 -1\n", - "2 11013784 have you looked at `Integrate` or `NIntegrate`? 0 -1\n", - "3 11015512 sorry, is a typo. The variable name is dist. (... 0 -1\n", - "4 11016238 Pfff, I'm having trouble with that formula too... 0 -1\n", - "5 11016276 Thanks thinksteep! Does this mean that by usin... 0 -1\n", - "6 11016551 Jason, thanks for the reply. I've been workin... 0 -1\n", - "7 11017973 I assume an `off` of 0.5 would put be exactly ... 0 -1\n", - "8 11018225 Thank you very much. I do worry too much abou... 0 -1\n", - "9 11018370 @IanClelland, I edited my question a bit. The ... 0 -1\n", + "0 11231597 In your update, why are some of the system fun... 0 -1\n", + "1 49684807 what you have tried so far . ?? 1 2\n", + "2 7623925 @Michael: It should work. Perhaps you looked i... 0 -1\n", + "3 34046685 Will it work with SQL compact? Please excuse m... 0 -1\n", + "4 6426146 do you know the equation to your pdf? 0 -1\n", + "5 60686114 m sorry but at least you have to think about it. 0 -1\n", + "6 16631986 i think also making disable this by only jquer... 0 -1\n", + "7 16498565 I am including these files on my header of the... 0 -1\n", + "8 26601001 wrong answer, you didn't understand the logic 0 -1\n", + "9 73255842 Call the setOnClickListener before return row. 0 -1\n", "\n", "[10 rows x 4 columns]" ] }, - "execution_count": 10, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -900,7 +810,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 27, "id": "2701cb81", "metadata": {}, "outputs": [ @@ -908,8 +818,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "bigframes-dev.bigframes_temp_us.bigframes_b0feb1fbaf8188b64d7e70118d93c5d4\n", - "projects/bigframes-dev/locations/us-central1/functions/bigframes-b0feb1fbaf8188b64d7e70118d93c5d4\n" + "shobs-test.bigframes_temp_us.bigframes_343b7b4bb93ca8747dae20c22bdaec8b_p27heyce\n", + "projects/shobs-test/locations/us-central1/functions/bigframes-343b7b4bb93ca8747dae20c22bdaec8b-p27heyce\n" ] } ], @@ -922,7 +832,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 28, "id": "920fa18e", "metadata": {}, "outputs": [ @@ -937,6 +847,42 @@ " \n", " Then it can be applied to a DataFrame or Series.\n", " \n", + " .. note::\n", + " The return type of the function must be explicitly specified in the\n", + " function's original definition even if not otherwise required.\n", + " \n", + " BigQuery Utils provides many public functions under the ``bqutil`` project on Google Cloud Platform project\n", + " (See: https://github.com/GoogleCloudPlatform/bigquery-utils/tree/master/udfs#using-the-udfs).\n", + " You can checkout Community UDFs to use community-contributed functions.\n", + " (See: https://github.com/GoogleCloudPlatform/bigquery-utils/tree/master/udfs/community#community-udfs).\n", + " \n", + " **Examples:**\n", + " \n", + " Use the ``cw_lower_case_ascii_only`` function from Community UDFs.\n", + " (https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/cw_lower_case_ascii_only.sqlx)\n", + " \n", + " >>> import bigframes.pandas as bpd\n", + " >>> bpd.options.display.progress_bar = None\n", + " \n", + " >>> df = bpd.DataFrame({'id': [1, 2, 3], 'name': ['AURÉLIE', 'CÉLESTINE', 'DAPHNÉ']})\n", + " >>> df\n", + " id name\n", + " 0 1 AURÉLIE\n", + " 1 2 CÉLESTINE\n", + " 2 3 DAPHNÉ\n", + " \n", + " [3 rows x 2 columns]\n", + " \n", + " >>> func = bpd.read_gbq_function(\"bqutil.fn.cw_lower_case_ascii_only\")\n", + " >>> df1 = df.assign(new_name=df['name'].apply(func))\n", + " >>> df1\n", + " id name new_name\n", + " 0 1 AURÉLIE aurÉlie\n", + " 1 2 CÉLESTINE cÉlestine\n", + " 2 3 DAPHNÉ daphnÉ\n", + " \n", + " [3 rows x 3 columns]\n", + " \n", " Args:\n", " function_name (str):\n", " the function's name in BigQuery in the format\n", @@ -965,7 +911,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 29, "id": "a6c9da0a", "metadata": {}, "outputs": [], @@ -978,7 +924,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 30, "id": "d7e7de7f", "metadata": {}, "outputs": [ @@ -986,33 +932,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 10.9 ms, sys: 0 ns, total: 10.9 ms\n", - "Wall time: 11.4 ms\n" + "CPU times: user 70.8 ms, sys: 3.49 ms, total: 74.3 ms\n", + "Wall time: 75.2 ms\n" ] }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "73d1a73593cb4115821ab128c221a48d", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job bec5f7d1-3df1-4292-8c68-c396bce7dc5d is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job 02e3bf43-a387-41c7-85c7-4a5366251de7 is RUNNING. " ] }, "metadata": {}, @@ -1020,13 +950,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "faf93766ce1e489183c86a9daf5ce7d1", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 4d3da7ed-42e6-4b2b-b656-ac9ef6d2e871 is DONE. 17.2 GB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job fa4329e8-2918-44c4-96c5-d8591364abc9 is RUNNING. " ] }, "metadata": {}, @@ -1063,80 +991,80 @@ " \n", " \n", " 0\n", - " 11012908\n", - " you're welcome! according to the docs it shoul...\n", + " 11231597\n", + " In your update, why are some of the system fun...\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 1\n", - " 11013760\n", - " You *should* be concerned with the disk being ...\n", - " 0\n", - " -1\n", - " -1\n", + " 49684807\n", + " what you have tried so far . ??\n", + " 1\n", + " 2\n", + " 2\n", " \n", " \n", " 2\n", - " 11013784\n", - " have you looked at `Integrate` or `NIntegrate`?\n", + " 7623925\n", + " @Michael: It should work. Perhaps you looked i...\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 3\n", - " 11015512\n", - " sorry, is a typo. The variable name is dist. (...\n", + " 34046685\n", + " Will it work with SQL compact? Please excuse m...\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 4\n", - " 11016238\n", - " Pfff, I'm having trouble with that formula too...\n", + " 6426146\n", + " do you know the equation to your pdf?\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 5\n", - " 11016276\n", - " Thanks thinksteep! Does this mean that by usin...\n", + " 60686114\n", + " m sorry but at least you have to think about it.\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 6\n", - " 11016551\n", - " Jason, thanks for the reply. I've been workin...\n", + " 16631986\n", + " i think also making disable this by only jquer...\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 7\n", - " 11017973\n", - " I assume an `off` of 0.5 would put be exactly ...\n", + " 16498565\n", + " I am including these files on my header of the...\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 8\n", - " 11018225\n", - " Thank you very much. I do worry too much abou...\n", + " 26601001\n", + " wrong answer, you didn't understand the logic\n", " 0\n", " -1\n", " -1\n", " \n", " \n", " 9\n", - " 11018370\n", - " @IanClelland, I edited my question a bit. The ...\n", + " 73255842\n", + " Call the setOnClickListener before return row.\n", " 0\n", " -1\n", " -1\n", @@ -1148,20 +1076,20 @@ ], "text/plain": [ " id text score \\\n", - "0 11012908 you're welcome! according to the docs it shoul... 0 \n", - "1 11013760 You *should* be concerned with the disk being ... 0 \n", - "2 11013784 have you looked at `Integrate` or `NIntegrate`? 0 \n", - "3 11015512 sorry, is a typo. The variable name is dist. (... 0 \n", - "4 11016238 Pfff, I'm having trouble with that formula too... 0 \n", - "5 11016276 Thanks thinksteep! Does this mean that by usin... 0 \n", - "6 11016551 Jason, thanks for the reply. I've been workin... 0 \n", - "7 11017973 I assume an `off` of 0.5 would put be exactly ... 0 \n", - "8 11018225 Thank you very much. I do worry too much abou... 0 \n", - "9 11018370 @IanClelland, I edited my question a bit. The ... 0 \n", + "0 11231597 In your update, why are some of the system fun... 0 \n", + "1 49684807 what you have tried so far . ?? 1 \n", + "2 7623925 @Michael: It should work. Perhaps you looked i... 0 \n", + "3 34046685 Will it work with SQL compact? Please excuse m... 0 \n", + "4 6426146 do you know the equation to your pdf? 0 \n", + "5 60686114 m sorry but at least you have to think about it. 0 \n", + "6 16631986 i think also making disable this by only jquer... 0 \n", + "7 16498565 I am including these files on my header of the... 0 \n", + "8 26601001 wrong answer, you didn't understand the logic 0 \n", + "9 73255842 Call the setOnClickListener before return row. 0 \n", "\n", " n_prime n_prime_again \n", "0 -1 -1 \n", - "1 -1 -1 \n", + "1 2 2 \n", "2 -1 -1 \n", "3 -1 -1 \n", "4 -1 -1 \n", @@ -1174,7 +1102,7 @@ "[10 rows x 5 columns]" ] }, - "execution_count": 15, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -1186,6 +1114,38 @@ "df = df.assign(n_prime_again=df['score'].apply(nth_prime_existing))\n", "df.head(10)" ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "bafab950", + "metadata": {}, + "outputs": [], + "source": [ + "# Clean up GCP assets created as part of bigframes remote_function\n", + "def cleanup_remote_function_assets(remote_udf, ignore_failures=False):\n", + " \"\"\"Clean up the GCP assets behind a bigframes remote function.\"\"\"\n", + "\n", + " session = pd.get_global_session()\n", + "\n", + " # Clean up BQ remote function\n", + " try:\n", + " session.bqclient.delete_routine(remote_udf.bigframes_remote_function)\n", + " except Exception:\n", + " # By default don't raise exception in cleanup\n", + " if not ignore_failures:\n", + " raise\n", + "\n", + " # Clean up cloud function\n", + " try:\n", + " session.cloudfunctionsclient.delete_function(name=remote_udf.bigframes_cloud_function)\n", + " except Exception:\n", + " # By default don't raise exception in cleanup\n", + " if not ignore_failures:\n", + " raise\n", + "\n", + "cleanup_remote_function_assets(nth_prime)" + ] } ], "metadata": { diff --git a/samples/snippets/remote_function.py b/samples/snippets/remote_function.py index 646d7b0c307..61b7dc092ad 100644 --- a/samples/snippets/remote_function.py +++ b/samples/snippets/remote_function.py @@ -38,8 +38,8 @@ def run_remote_function_and_read_gbq_function(project_id: str): # function. It requires a BigQuery connection. If the connection is not # already created, BigQuery DataFrames will attempt to create one assuming # the necessary APIs and IAM permissions are setup in the project. In our - # examples we would be using a pre-created connection named - # `bigframes-rf-conn`. We will also set `reuse=False` to make sure we don't + # examples we will be letting the default connection `bigframes-default-connection` + # be used. We will also set `reuse=False` to make sure we don't # step over someone else creating remote function in the same project from # the exact same source code at the same time. Let's try a `pandas`-like use # case in which we want to apply a user defined scalar function to every @@ -49,7 +49,6 @@ def run_remote_function_and_read_gbq_function(project_id: str): @bpd.remote_function( [float], str, - bigquery_connection="bigframes-rf-conn", reuse=False, ) def get_bucket(num): @@ -94,7 +93,6 @@ def get_bucket(num): @bpd.remote_function( [str], str, - bigquery_connection="bigframes-rf-conn", reuse=False, packages=["cryptography"], ) From 3a375e87b64b8fb51370bfec8f2cfdbcd8fe960a Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:52:22 -0800 Subject: [PATCH 178/921] docs: add examples for dataframe.min, dataframe.max and dataframe.sum (#227) * docs: add examples for dataframe.min, dataframe.max and dataframe.sum * update spacing --- .../bigframes_vendored/pandas/core/frame.py | 87 ++++++++++++++++++- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index b771be30418..e41ac905aa7 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2597,7 +2597,7 @@ def any(self, *, axis=0, bool_only: bool = False): [2 rows x 2 columns] - Checking if each column contains at least one True element(the default behavior without an explicit axis parameter). + Checking if each column contains at least one True element (the default behavior without an explicit axis parameter). >>> df.any() A True @@ -2644,7 +2644,7 @@ def all(self, axis=0, *, bool_only: bool = False): [2 rows x 2 columns] - Checking if all values in each column are True(the default behavior without an explicit axis parameter). + Checking if all values in each column are True (the default behavior without an explicit axis parameter). >>> df.all() A True @@ -2688,7 +2688,7 @@ def prod(self, axis=0, *, numeric_only: bool = False): [3 rows x 2 columns] - Calculating the product of each column(the default behavior without an explicit axis parameter). + Calculating the product of each column (the default behavior without an explicit axis parameter). >>> df.prod() A 6.0 @@ -2721,6 +2721,33 @@ def min(self, axis=0, *, numeric_only: bool = False): If you want the *index* of the minimum, use ``idxmin``. This is the equivalent of the ``numpy.ndarray`` method ``argmin``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 3], "B": [2, 4]}) + >>> df + A B + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + Finding the minimum value in each column (the default behavior without an explicit axis parameter). + + >>> df.min() + A 1.0 + B 2.0 + dtype: Float64 + + Finding the minimum value in each row. + + >>> df.min(axis=1) + 0 1.0 + 1 3.0 + dtype: Float64 + Args: axis ({index (0), columns (1)}): Axis for the function to be applied on. @@ -2739,6 +2766,33 @@ def max(self, axis=0, *, numeric_only: bool = False): If you want the *index* of the maximum, use ``idxmax``. This is the equivalent of the ``numpy.ndarray`` method ``argmax``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 3], "B": [2, 4]}) + >>> df + A B + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + Finding the maximum value in each column (the default behavior without an explicit axis parameter). + + >>> df.max() + A 3.0 + B 4.0 + dtype: Float64 + + Finding the maximum value in each row. + + >>> df.max(axis=1) + 0 2.0 + 1 4.0 + dtype: Float64 + Args: axis ({index (0), columns (1)}): Axis for the function to be applied on. @@ -2756,6 +2810,33 @@ def sum(self, axis=0, *, numeric_only: bool = False): This is equivalent to the method ``numpy.sum``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 3], "B": [2, 4]}) + >>> df + A B + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + Calculating the sum of each column (the default behavior without an explicit axis parameter). + + >>> df.sum() + A 4.0 + B 6.0 + dtype: Float64 + + Calculating the sum of each row. + + >>> df.sum(axis=1) + 0 3.0 + 1 7.0 + dtype: Float64 + Args: axis ({index (0), columns (1)}): Axis for the function to be applied on. From b62a07a95cd60f995a48825c9874822d0eb02483 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 24 Nov 2023 00:10:18 +0000 Subject: [PATCH 179/921] docs: code samples for `Series.dot` and `DataFrame.dot` (#226) --- bigframes/dataframe.py | 3 +- bigframes/operations/base.py | 2 +- tests/system/small/test_dataframe.py | 23 ++++++ .../bigframes_vendored/pandas/core/frame.py | 71 +++++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 15 ++++ 5 files changed, 112 insertions(+), 2 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1f1275e217b..8567296e29e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2797,7 +2797,8 @@ def get_right_id(id): result = result[other_frame.columns] if isinstance(other, bf_series.Series): - result = result[other.name].rename() + # There should be exactly one column in the result + result = result[result.columns[0]].rename() return result diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index d33befe4da6..85ce1dd9e6d 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -141,7 +141,7 @@ def _apply_binary_op( if isinstance(other, pd.Series): # TODO: Convert to BigQuery DataFrames series raise NotImplementedError( - f"Pandas series not supported supported as operand. {constants.FEEDBACK_LINK}" + f"Pandas series not supported as operand. {constants.FEEDBACK_LINK}" ) if isinstance(other, series.Series): (left, right, block) = self._align(other, how=alignment) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index a0cf25807cf..e25e9ce501c 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3493,6 +3493,29 @@ def test_df_dot_operator( ) +def test_df_dot_series_inline(): + left = [[1, 2, 3], [2, 5, 7]] + right = [2, 1, 3] + + bf1 = dataframe.DataFrame(left) + bf2 = series.Series(right) + bf_result = bf1.dot(bf2).to_pandas() + + df1 = pd.DataFrame(left) + df2 = pd.Series(right) + pd_result = df1.dot(df2) + + # Patch pandas dtypes for testing parity + # Pandas result is int64 instead of Int64 (nullable) dtype. + pd_result = pd_result.astype(pd.Int64Dtype()) + pd_result.index = pd_result.index.astype(pd.Int64Dtype()) + + pd.testing.assert_series_equal( + bf_result, + pd_result, + ) + + def test_df_dot_series( matrix_2by3_df, matrix_2by3_pandas_df, matrix_3by4_df, matrix_3by4_pandas_df ): diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index e41ac905aa7..a1aac5d2b54 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3485,6 +3485,77 @@ def dot(self, other): The dot method for Series computes the inner product, instead of the matrix product here. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> left = bpd.DataFrame([[0, 1, -2, -1], [1, 1, 1, 1]]) + >>> left + 0 1 2 3 + 0 0 1 -2 -1 + 1 1 1 1 1 + + [2 rows x 4 columns] + >>> right = bpd.DataFrame([[0, 1], [1, 2], [-1, -1], [2, 0]]) + >>> right + 0 1 + 0 0 1 + 1 1 2 + 2 -1 -1 + 3 2 0 + + [4 rows x 2 columns] + >>> left.dot(right) + 0 1 + 0 1 4 + 1 2 2 + + [2 rows x 2 columns] + + You can also use the operator ``@`` for the dot product: + + >>> left @ right + 0 1 + 0 1 4 + 1 2 2 + + [2 rows x 2 columns] + + The right input can be a Series, in which case the result will also be a + Series: + + >>> right = bpd.Series([1, 2, -1,0]) + >>> left @ right + 0 4 + 1 2 + dtype: Int64 + + Any user defined index of the left matrix and columns of the right + matrix will reflect in the result. + + >>> left = bpd.DataFrame([[1, 2, 3], [2, 5, 7]], index=["alpha", "beta"]) + >>> left + 0 1 2 + alpha 1 2 3 + beta 2 5 7 + + [2 rows x 3 columns] + >>> right = bpd.DataFrame([[2, 4, 8], [1, 5, 10], [3, 6, 9]], columns=["red", "green", "blue"]) + >>> right + red green blue + 0 2 4 8 + 1 1 5 10 + 2 3 6 9 + + [3 rows x 3 columns] + >>> left.dot(right) + red green blue + alpha 13 32 55 + beta 30 75 129 + + [2 rows x 3 columns] + Args: other (Series or DataFrame): The other object to compute the matrix product with. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index a86765a4125..1b751ed83b6 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -631,6 +631,21 @@ def dot(self, other) -> Series | np.ndarray: BigQuery Dataframes does not validate this property and will produce incorrect results if indices are not equal. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([0, 1, 2, 3]) + >>> other = bpd.Series([-1, 2, -3, 4]) + >>> s.dot(other) + 8 + + You can also use the operator ``@`` for the dot product: + + >>> s @ other + 8 + Args: other (Series): The other object to compute the dot product with its columns. From f9c6e727e2b901310bb5301da449d616ea85e135 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:48:21 -0800 Subject: [PATCH 180/921] docs: add examples for dataframe.kurt, dataframe.std, dataframe.count (#232) * docs: add examples for dataframe.kurt, dataframe.std, dataframe.count * update count example * update count example * update examples * update . to : --- .../bigframes_vendored/pandas/core/frame.py | 96 +++++++++++++++++-- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index a1aac5d2b54..a7018ed3a24 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2597,14 +2597,14 @@ def any(self, *, axis=0, bool_only: bool = False): [2 rows x 2 columns] - Checking if each column contains at least one True element (the default behavior without an explicit axis parameter). + Checking if each column contains at least one True element(the default behavior without an explicit axis parameter): >>> df.any() A True B False dtype: boolean - Checking if each row contains at least one True element. + Checking if each row contains at least one True element: >>> df.any(axis=1) 0 True @@ -2644,14 +2644,14 @@ def all(self, axis=0, *, bool_only: bool = False): [2 rows x 2 columns] - Checking if all values in each column are True (the default behavior without an explicit axis parameter). + Checking if all values in each column are True(the default behavior without an explicit axis parameter): >>> df.all() A True B False dtype: boolean - Checking across rows to see if all values are True. + Checking across rows to see if all values are True: >>> df.all(axis=1) 0 False @@ -2688,14 +2688,14 @@ def prod(self, axis=0, *, numeric_only: bool = False): [3 rows x 2 columns] - Calculating the product of each column (the default behavior without an explicit axis parameter). + Calculating the product of each column(the default behavior without an explicit axis parameter): >>> df.prod() A 6.0 B 160.875 dtype: Float64 - Calculating the product of each row. + Calculating the product of each row: >>> df.prod(axis=1) 0 4.5 @@ -2911,11 +2911,37 @@ def skew(self, *, numeric_only: bool = False): raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def kurt(self, *, numeric_only: bool = False): - """Return unbiased kurtosis over requested axis. + """Return unbiased kurtosis over columns. Kurtosis obtained using Fisher's definition of kurtosis (kurtosis of normal == 0.0). Normalized by N-1. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 2, 3, 4, 5], + ... "B": [3, 4, 3, 2, 1], + ... "C": [2, 2, 3, 2, 2]}) + >>> df + A B C + 0 1 3 2 + 1 2 4 2 + 2 3 3 3 + 3 4 2 2 + 4 5 1 2 + + [5 rows x 3 columns] + + Calculating the kurtosis value of each column: + + >>> df.kurt() + A -1.2 + B -0.177515 + C 5.0 + dtype: Float64 + Args: numeric_only (bool, default False): Include only float, int, boolean columns. @@ -2926,10 +2952,36 @@ def kurt(self, *, numeric_only: bool = False): raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def std(self, *, numeric_only: bool = False): - """Return sample standard deviation over requested axis. + """Return sample standard deviation over columns. Normalized by N-1 by default. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 2, 3, 4, 5], + ... "B": [3, 4, 3, 2, 1], + ... "C": [2, 2, 3, 2, 2]}) + >>> df + A B C + 0 1 3 2 + 1 2 4 2 + 2 3 3 3 + 3 4 2 2 + 4 5 1 2 + + [5 rows x 3 columns] + + Calculating the standard deviation of each column: + + >>> df.std() + A 1.581139 + B 1.140175 + C 0.447214 + dtype: Float64 + Args: numeric_only (bool. default False): Default False. Include only float, int, boolean columns. @@ -2941,11 +2993,37 @@ def std(self, *, numeric_only: bool = False): def count(self, *, numeric_only: bool = False): """ - Count non-NA cells for each column or row. + Count non-NA cells for each column. The values `None`, `NaN`, `NaT`, and optionally `numpy.inf` (depending on `pandas.options.mode.use_inf_as_na`) are considered NA. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, None, 3, 4, 5], + ... "B": [1, 2, 3, 4, 5], + ... "C": [None, 3.5, None, 4.5, 5.0]}) + >>> df + A B C + 0 1.0 1 + 1 2 3.5 + 2 3.0 3 + 3 4.0 4 4.5 + 4 5.0 5 5.0 + + [5 rows x 3 columns] + + Counting non-NA values for each column: + + >>> df.count() + A 4.0 + B 5.0 + C 3.0 + dtype: Float64 + Args: numeric_only (bool, default False): Include only `float`, `int` or `boolean` data. From edd0522747eadb74780124fb18ed7face251441d Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:40:28 -0800 Subject: [PATCH 181/921] =?UTF-8?q?docs:=20add=20examples=20for=20datafram?= =?UTF-8?q?e.mean,=20dataframe.median,=20dataframe.va=E2=80=A6=20(#228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add examples for dataframe.mean, dataframe.median, dataframe.var and dataframe.skew * column to columns * update var example --- .../bigframes_vendored/pandas/core/frame.py | 105 +++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index a7018ed3a24..2a8972f2e58 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2852,6 +2852,33 @@ def sum(self, axis=0, *, numeric_only: bool = False): def mean(self, axis=0, *, numeric_only: bool = False): """Return the mean of the values over the requested axis. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 3], "B": [2, 4]}) + >>> df + A B + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + Calculating the mean of each column (the default behavior without an explicit axis parameter). + + >>> df.mean() + A 2.0 + B 3.0 + dtype: Float64 + + Calculating the mean of each row. + + >>> df.mean(axis=1) + 0 1.5 + 1 3.5 + dtype: Float64 + Args: axis ({index (0), columns (1)}): Axis for the function to be applied on. @@ -2865,7 +2892,27 @@ def mean(self, axis=0, *, numeric_only: bool = False): raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def median(self, *, numeric_only: bool = False, exact: bool = False): - """Return the median of the values over the requested axis. + """Return the median of the values over colunms. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 3], "B": [2, 4]}) + >>> df + A B + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + Finding the median value of each column. + + >>> df.median() + A 1.0 + B 2.0 + dtype: Float64 Args: numeric_only (bool. default False): @@ -2884,6 +2931,34 @@ def var(self, axis=0, *, numeric_only: bool = False): Normalized by N-1 by default. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 3], "B": [2, 4]}) + >>> df + A B + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + Calculating the variance of each column (the default behavior without an explicit axis parameter). + + >>> df.var() + A 2.0 + B 2.0 + dtype: Float64 + + Calculating the variance of each row. + + >>> df.var(axis=1) + 0 0.5 + 1 0.5 + dtype: Float64 + + Args: axis ({index (0), columns (1)}): Axis for the function to be applied on. @@ -2897,10 +2972,36 @@ def var(self, axis=0, *, numeric_only: bool = False): raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def skew(self, *, numeric_only: bool = False): - """Return unbiased skew over requested axis. + """Return unbiased skew over columns. Normalized by N-1. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': [1, 2, 3, 4, 5], + ... 'B': [5, 4, 3, 2, 1], + ... 'C': [2, 2, 3, 2, 2]}) + >>> df + A B C + 0 1 5 2 + 1 2 4 2 + 2 3 3 3 + 3 4 2 2 + 4 5 1 2 + + [5 rows x 3 columns] + + Calculating the skewness of each column. + + >>> df.skew() + A 0.0 + B 0.0 + C 2.236068 + dtype: Float64 + Args: numeric_only (bool, default False): Include only float, int, boolean columns. From ae03756f5ee45e0e74e0c0bdd4777e018eba2273 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:53:51 -0800 Subject: [PATCH 182/921] fix: make to_pandas override enable_downsampling when sampling_method is manually set. (#200) * fix: make to_pandas override enable_downsampling when sampling_method is manually set. * fix: make to_pandas override enable_downsampling when sampling_method is manually set. * fix: make to_pandas override enable_downsampling when sampling_method is manually set. --- bigframes/core/blocks.py | 41 ++++++++++++++++------------ tests/system/small/test_dataframe.py | 11 ++++++++ 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index f1113d938ec..34913872e70 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -389,23 +389,6 @@ def to_pandas( ordered: bool = True, ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame.""" - if max_download_size is None: - max_download_size = bigframes.options.sampling.max_download_size - if sampling_method is None: - sampling_method = ( - bigframes.options.sampling.sampling_method - if bigframes.options.sampling.sampling_method is not None - else _UNIFORM - ) - if random_state is None: - random_state = bigframes.options.sampling.random_state - - sampling_method = sampling_method.lower() - if sampling_method not in _SAMPLING_METHODS: - raise NotImplementedError( - f"The downsampling method {sampling_method} is not implemented, " - f"please choose from {','.join(_SAMPLING_METHODS)}." - ) df, _, query_job = self._compute_and_count( value_keys=value_keys, @@ -453,6 +436,28 @@ def _compute_and_count( ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. + enable_downsampling = ( + True + if sampling_method is not None + else bigframes.options.sampling.enable_downsampling + ) + + max_download_size = ( + max_download_size or bigframes.options.sampling.max_download_size + ) + + random_state = random_state or bigframes.options.sampling.random_state + + if sampling_method is None: + sampling_method = bigframes.options.sampling.sampling_method or _UNIFORM + sampling_method = sampling_method.lower() + + if sampling_method not in _SAMPLING_METHODS: + raise NotImplementedError( + f"The downsampling method {sampling_method} is not implemented, " + f"please choose from {','.join(_SAMPLING_METHODS)}." + ) + expr = self._apply_value_keys_to_expr(value_keys=value_keys) results_iterator, query_job = expr.start_query( @@ -469,7 +474,7 @@ def _compute_and_count( ) if fraction < 1: - if not bigframes.options.sampling.enable_downsampling: + if not enable_downsampling: raise RuntimeError( f"The data size ({table_size:.2f} MB) exceeds the maximum download limit of " f"{max_download_size} MB. You can:\n\t* Enable downsampling in global options:\n" diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e25e9ce501c..9b9567418b7 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3546,3 +3546,14 @@ def test_df_dot_operator_series( bf_result, pd_result, ) + + +def test_to_pandas_downsampling_option_override(session): + df = session.read_gbq("bigframes-dev.bigframes_tests_sys.batting") + download_size = 1 + + df = df.to_pandas(max_download_size=download_size, sampling_method="head") + + total_memory_bytes = df.memory_usage(deep=True).sum() + total_memory_mb = total_memory_bytes / (1024 * 1024) + assert total_memory_mb == pytest.approx(download_size, rel=0.3) From 9d6613d318b558722b7bab12773efdea4bbe9931 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 28 Nov 2023 13:46:18 -0800 Subject: [PATCH 183/921] feat: add info and memory_usage methods to dataframe (#219) --- .pre-commit-config.yaml | 2 +- bigframes/_config/display_options.py | 4 + bigframes/core/indexes/index.py | 13 ++- bigframes/dataframe.py | 84 +++++++++++++++++++ bigframes/dtypes.py | 13 +++ noxfile.py | 1 + setup.py | 1 + tests/system/small/test_dataframe.py | 42 ++++++++++ .../pandas/core/config_init.py | 11 +++ .../bigframes_vendored/pandas/core/frame.py | 66 +++++++++++++++ 10 files changed, 235 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e0fd8b98fb..517176da892 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,4 +38,4 @@ repos: rev: v1.1.1 hooks: - id: mypy - additional_dependencies: [types-requests] + additional_dependencies: [types-requests, types-tabulate] diff --git a/bigframes/_config/display_options.py b/bigframes/_config/display_options.py index ad3ea3f68cc..afa36aa84c2 100644 --- a/bigframes/_config/display_options.py +++ b/bigframes/_config/display_options.py @@ -32,6 +32,10 @@ class DisplayOptions: progress_bar: Optional[str] = "auto" repr_mode: Literal["head", "deferred"] = "head" + max_info_columns: int = 100 + max_info_rows: Optional[int] = 200000 + memory_usage: bool = True + @contextlib.contextmanager def pandas_repr(display_options: DisplayOptions): diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 6c66c36062a..fc7cf167d49 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -155,6 +155,14 @@ def _block(self) -> blocks.Block: def T(self) -> Index: return self.transpose() + def _memory_usage(self) -> int: + (n_rows,) = self.shape + return sum( + self.dtypes.map( + lambda dtype: bigframes.dtypes.DTYPE_BYTE_SIZES.get(dtype, 8) * n_rows + ) + ) + def transpose(self) -> Index: return self @@ -326,7 +334,10 @@ def _apply_aggregation(self, op: agg_ops.AggregateOp) -> typing.Any: def __getitem__(self, key: int) -> typing.Any: if isinstance(key, int): - result_pd_df, _ = self._block.slice(key, key + 1, 1).to_pandas() + if key != -1: + result_pd_df, _ = self._block.slice(key, key + 1, 1).to_pandas() + else: # special case, want [-1:] instead of [-1:0] + result_pd_df, _ = self._block.slice(key).to_pandas() if result_pd_df.empty: raise IndexError("single positional indexer is out-of-bounds") return result_pd_df.index[0] diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 8567296e29e..f7796291b9a 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -18,6 +18,7 @@ import datetime import re +import sys import textwrap import typing from typing import ( @@ -36,6 +37,7 @@ import google.cloud.bigquery as bigquery import numpy import pandas +import tabulate import bigframes import bigframes._config.display_options as display_options @@ -350,6 +352,88 @@ def query_job(self) -> Optional[bigquery.QueryJob]: self._set_internal_query_job(self._compute_dry_run()) return self._query_job + def memory_usage(self, index: bool = True): + n_rows, _ = self.shape + # like pandas, treat all variable-size objects as just 8-byte pointers, ignoring actual object + column_sizes = self.dtypes.map( + lambda dtype: bigframes.dtypes.DTYPE_BYTE_SIZES.get(dtype, 8) * n_rows + ) + if index: + index_size = pandas.Series([self.index._memory_usage()], index=["Index"]) + column_sizes = pandas.concat([index_size, column_sizes]) + return column_sizes + + def info( + self, + verbose: Optional[bool] = None, + buf=None, + max_cols: Optional[int] = None, + memory_usage: Optional[bool] = None, + show_counts: Optional[bool] = None, + ): + obuf = buf or sys.stdout + + n_rows, n_columns = self.shape + + max_cols = ( + max_cols + if max_cols is not None + else bigframes.options.display.max_info_columns + ) + + show_all_columns = verbose if verbose is not None else (n_columns < max_cols) + + obuf.write(f"{type(self)}\n") + + index_type = "MultiIndex" if self.index.nlevels > 1 else "Index" + + # These accessses are kind of expensive, maybe should try to skip? + first_indice = self.index[0] + last_indice = self.index[-1] + obuf.write(f"{index_type}: {n_rows} entries, {first_indice} to {last_indice}\n") + + dtype_strings = self.dtypes.astype("string") + if show_all_columns: + obuf.write(f"Data columns (total {n_columns} columns):\n") + column_info = self.columns.to_frame(name="Column") + + max_rows = bigframes.options.display.max_info_rows + too_many_rows = n_rows > max_rows if max_rows is not None else False + + if show_counts if show_counts is not None else (not too_many_rows): + non_null_counts = self.count().to_pandas() + column_info["Non-Null Count"] = non_null_counts.map( + lambda x: f"{int(x)} non-null" + ) + + column_info["Dtype"] = dtype_strings + + column_info = column_info.reset_index(drop=True) + column_info.index.name = "#" + + column_info_formatted = tabulate.tabulate(column_info, headers="keys") # type: ignore + obuf.write(column_info_formatted) + obuf.write("\n") + + else: # Just number of columns and first, last + obuf.write( + f"Columns: {n_columns} entries, {self.columns[0]} to {self.columns[-1]}\n" + ) + dtype_counts = dtype_strings.value_counts().sort_index(ascending=True).items() + dtype_counts_formatted = ", ".join( + f"{dtype}({count})" for dtype, count in dtype_counts + ) + obuf.write(f"dtypes: {dtype_counts_formatted}\n") + + show_memory = ( + memory_usage + if memory_usage is not None + else bigframes.options.display.memory_usage + ) + if show_memory: + # TODO: Convert to different units (kb, mb, etc.) + obuf.write(f"memory usage: {self.memory_usage().sum()} bytes\n") + def _set_internal_query_job(self, query_job: bigquery.QueryJob): self._query_job = query_job diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index cd35e380c02..774eb74d06a 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -143,6 +143,19 @@ # "string" and "string[pyarrow] are accepted" BIGFRAMES_STRING_TO_BIGFRAMES["string[pyarrow]"] = pd.StringDtype(storage="pyarrow") +# For the purposes of dataframe.memory_usage +# https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#data_type_sizes +DTYPE_BYTE_SIZES = { + pd.BooleanDtype(): 1, + pd.Int64Dtype(): 8, + pd.Float32Dtype(): 8, + pd.StringDtype(): 8, + pd.ArrowDtype(pa.time64("us")): 8, + pd.ArrowDtype(pa.timestamp("us")): 8, + pd.ArrowDtype(pa.timestamp("us", tz="UTC")): 8, + pd.ArrowDtype(pa.date32()): 8, +} + def ibis_dtype_to_bigframes_dtype( ibis_dtype: ibis_dtypes.DataType, diff --git a/noxfile.py b/noxfile.py index 8d6d641fc1e..c1fb53f7947 100644 --- a/noxfile.py +++ b/noxfile.py @@ -228,6 +228,7 @@ def mypy(session): "types-python-dateutil", "types-requests", "types-setuptools", + "types-tabulate", ] ) | set(SYSTEM_TEST_STANDARD_DEPENDENCIES) diff --git a/setup.py b/setup.py index 29eacb74a9a..abf165b3df3 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ "requests >=2.27.1", "scikit-learn >=1.2.2", "sqlalchemy >=1.4,<3.0dev", + "tabulate >= 0.9", "ipywidgets >=7.7.1", "humanize >= 4.6.0", ] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 9b9567418b7..9744d3f6e91 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import io import operator import tempfile import typing @@ -255,6 +256,47 @@ def test_drop_with_custom_column_labels(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) +def test_df_memory_usage(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = scalars_pandas_df.memory_usage() + bf_result = scalars_df.memory_usage() + + pd.testing.assert_series_equal(pd_result, bf_result, rtol=1.5) + + +def test_df_info(scalars_dfs): + expected = ( + "\n" + "Index: 9 entries, 0 to 8\n" + "Data columns (total 13 columns):\n" + " # Column Non-Null Count Dtype\n" + "--- ------------- ---------------- ------------------------------\n" + " 0 bool_col 8 non-null boolean\n" + " 1 bytes_col 6 non-null object\n" + " 2 date_col 7 non-null date32[day][pyarrow]\n" + " 3 datetime_col 6 non-null timestamp[us][pyarrow]\n" + " 4 geography_col 4 non-null geometry\n" + " 5 int64_col 8 non-null Int64\n" + " 6 int64_too 9 non-null Int64\n" + " 7 numeric_col 6 non-null object\n" + " 8 float64_col 7 non-null Float64\n" + " 9 rowindex_2 9 non-null Int64\n" + " 10 string_col 8 non-null string\n" + " 11 time_col 6 non-null time64[us][pyarrow]\n" + " 12 timestamp_col 6 non-null timestamp[us, tz=UTC][pyarrow]\n" + "dtypes: Float64(1), Int64(3), boolean(1), date32[day][pyarrow](1), geometry(1), object(2), string(1), time64[us][pyarrow](1), timestamp[us, tz=UTC][pyarrow](1), timestamp[us][pyarrow](1)\n" + "memory usage: 945 bytes\n" + ) + + scalars_df, _ = scalars_dfs + bf_result = io.StringIO() + + scalars_df.info(buf=bf_result) + + assert expected == bf_result.getvalue() + + def test_drop_index(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index 198654015e0..dfb91dfeb81 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -33,6 +33,17 @@ Instead estimated bytes processed will be shown. Dataframe and Series objects can still be computed with methods that explicitly execute and download results. + max_info_columns (int): + max_info_columns is used in DataFrame.info method to decide if + per column information will be printed. + max_info_rows (int or None): + df.info() will usually show null-counts for each column. + For large frames this can be quite slow. max_info_rows and max_info_cols + limit this null check only to frames with smaller dimensions than + specified. + memory_usage (bool): + This specifies if the memory usage of a DataFrame should be displayed when + df.info() is called. Valid values True,False, """ sampling_options_doc = """ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 2a8972f2e58..099d8b8e663 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -92,6 +92,72 @@ def values(self) -> np.ndarray: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def info( + self, + verbose: bool | None = None, + buf=None, + max_cols: int | None = None, + memory_usage: bool | None = None, + show_counts: bool | None = None, + ) -> None: + """ + Print a concise summary of a DataFrame. + + This method prints information about a DataFrame including + the index dtypeand columns, non-null values and memory usage. + + Args: + verbose (bool, optional): + Whether to print the full summary. By default, the setting in + ``pandas.options.display.max_info_columns`` is followed. + buf (writable buffer, defaults to sys.stdout): + Where to send the output. By default, the output is printed to + sys.stdout. Pass a writable buffer if you need to further process + the output. + max_cols (int, optional): + When to switch from the verbose to the truncated output. If the + DataFrame has more than `max_cols` columns, the truncated output + is used. By default, the setting in + ``pandas.options.display.max_info_columns`` is used. + memory_usage (bool, optional): + Specifies whether total memory usage of the DataFrame + elements (including the index) should be displayed. By default, + this follows the ``pandas.options.display.memory_usage`` setting. + True always show memory usage. False never shows memory usage. + Memory estimation is made based in column dtype and number of rows + assuming values consume the same memory amount for corresponding dtypes. + show_counts (bool, optional): + Whether to show the non-null counts. By default, this is shown + only if the DataFrame is smaller than + ``pandas.options.display.max_info_rows`` and + ``pandas.options.display.max_info_columns``. A value of True always + shows the counts, and False never shows the counts. + + Returns: + None: This method prints a summary of a DataFrame and returns None.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def memory_usage(self, index: bool = True): + """ + Return the memory usage of each column in bytes. + + The memory usage can optionally include the contribution of + the index and elements of `object` dtype. + + This value is displayed in `DataFrame.info` by default. This can be + suppressed by setting ``pandas.options.display.memory_usage`` to False. + + Args: + index (bool, default True): + Specifies whether to include the memory usage of the DataFrame's + index in returned Series. If ``index=True``, the memory usage of + the index is the first item in the output. + + Returns: + Series: A Series whose index is the original column names and whose values is the memory usage of each column in bytes. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # IO methods (to / from other formats) def to_numpy(self, dtype=None, copy=False, na_value=None, **kwargs) -> np.ndarray: From 69b016eae7ea97d84ceeb22ba09f5472841db072 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 28 Nov 2023 23:42:58 +0000 Subject: [PATCH 184/921] fix: use anonymous dataset to create `remote_function` (#205) * fix: use anonymous dataset to create `remote_function` * update README about anonymous dataset instead of bigframes_temp_location * remove dataset creation step from remote function This is because now the dataset is an anonymous dataset that must have been created previously as part of bigframes session creation. * restore create_dataset, guarded by get_dataset --- README.rst | 7 ++-- bigframes/remote_function.py | 19 ++++++--- bigframes/session/__init__.py | 14 ------- tests/system/large/test_remote_function.py | 47 +++++++++++++++++++++- tests/system/small/test_remote_function.py | 37 +++++------------ 5 files changed, 73 insertions(+), 51 deletions(-) diff --git a/README.rst b/README.rst index 5ddb4a7639a..91dac127518 100644 --- a/README.rst +++ b/README.rst @@ -267,10 +267,9 @@ definition. To view and manage connections, do the following: 3. In the Explorer pane, expand that project and then expand External connections. BigQuery remote functions are created in the dataset you specify, or -in a dataset with the name ``bigframes_temp_location``, where location is -the location used by the BigQuery DataFrames session. For example, -``bigframes_temp_us_central1``. To view and manage remote functions, do -the following: +in a special type of `hidden dataset `__ +referred to as an anonymous dataset. To view and manage remote functions created +in a user provided dataset, do the following: 1. Go to `BigQuery in the Google Cloud Console `__. 2. Select the project in which you created the remote function. diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index a39cd033f69..7280ac7d425 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -188,6 +188,7 @@ def create_bq_remote_function( # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 bq_function_args = [] bq_function_return_type = BigQueryType.from_ibis(output_type) + # We are expecting the input type annotations to be 1:1 with the input args for idx, name in enumerate(input_args): bq_function_args.append( @@ -204,14 +205,22 @@ def create_bq_remote_function( logger.info(f"Creating BQ remote function: {create_function_ddl}") - # Make sure the dataset exists + # Make sure the dataset exists. I.e. if it doesn't exist, go ahead and + # create it dataset = bigquery.Dataset( bigquery.DatasetReference.from_string( self._bq_dataset, default_project=self._gcp_project_id ) ) dataset.location = self._bq_location - self._bq_client.create_dataset(dataset, exists_ok=True) + try: + # This check does not require bigquery.datasets.create IAM + # permission. So, if the data set already exists, then user can work + # without having that permission. + self._bq_client.get_dataset(dataset) + except google.api_core.exceptions.NotFound: + # This requires bigquery.datasets.create IAM permission + self._bq_client.create_dataset(dataset, exists_ok=True) # TODO: Use session._start_query() so we get progress bar query_job = self._bq_client.query(create_function_ddl) # Make an API request. @@ -610,7 +619,7 @@ def get_routine_reference( raise DatasetMissingError dataset_ref = bigquery.DatasetReference( - bigquery_client.project, session._session_dataset_id + bigquery_client.project, session._anonymous_dataset.dataset_id ) return dataset_ref.routine(routine_ref_str) @@ -778,9 +787,7 @@ def remote_function( dataset, default_project=bigquery_client.project ) else: - dataset_ref = bigquery.DatasetReference.from_string( - session._session_dataset_id, default_project=bigquery_client.project - ) + dataset_ref = session._anonymous_dataset bq_location, cloud_function_region = get_remote_function_locations( bigquery_client.location diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 928123ce749..d2f6137883c 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -198,13 +198,6 @@ def cloudfunctionsclient(self): def resourcemanagerclient(self): return self._clients_provider.resourcemanagerclient - @property - def _session_dataset_id(self): - """A dataset for storing temporary objects local to the session - This is a workaround for remote functions that do not - yet support session-temporary instances.""" - return self._session_dataset.dataset_id - @property def _project(self): return self.bqclient.project @@ -229,13 +222,6 @@ def _create_bq_datasets(self): query_destination.dataset_id, ) - # Dataset for storing remote functions, which don't yet - # support proper session temporary storage yet - self._session_dataset = bigquery.Dataset( - f"{self.bqclient.project}.bigframes_temp_{self._location.lower().replace('-', '_')}" - ) - self._session_dataset.location = self._location - def close(self): """No-op. Temporary resources are deleted after 7 days.""" diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 6ed3e6511a2..5cb4df188ca 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -22,7 +22,7 @@ import textwrap from google.api_core.exceptions import NotFound, ResourceExhausted -from google.cloud import functions_v2 +from google.cloud import bigquery, functions_v2 import pandas import pytest import test_utils.prefixer @@ -1210,3 +1210,48 @@ def square(x): cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, square ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_anonymous_dataset(session, scalars_dfs): + try: + # This usage of remote_function is expected to create the remote + # function in the bigframes session's anonymous dataset. Use reuse=False + # param to make sure parallel instances of the test don't step over each + # other due to the common anonymous dataset. + @session.remote_function([int], int, reuse=False) + def square(x): + return x * x + + assert ( + bigquery.Routine(square.bigframes_remote_function).dataset_id + == session._anonymous_dataset.dataset_id + ) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_int64_col_filter = bf_int64_col.notnull() + bf_int64_col_filtered = bf_int64_col[bf_int64_col_filter] + bf_result_col = bf_int64_col_filtered.apply(square) + bf_result = ( + bf_int64_col_filtered.to_frame().assign(result=bf_result_col).to_pandas() + ) + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_int64_col_filter = pd_int64_col.notnull() + pd_int64_col_filtered = pd_int64_col[pd_int64_col_filter] + pd_result_col = pd_int64_col_filtered.apply(lambda x: x * x) + # TODO(shobs): Figure why pandas .apply() changes the dtype, i.e. + # pd_int64_col_filtered.dtype is Int64Dtype() + # pd_int64_col_filtered.apply(lambda x: x * x).dtype is int64. + # For this test let's force the pandas dtype to be same as bigframes' dtype. + pd_result_col = pd_result_col.astype(pandas.Int64Dtype()) + pd_result = pd_int64_col_filtered.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square + ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 3d8532a13b6..960a3841261 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -62,13 +62,12 @@ def bq_cf_connection_location_project_mismatched() -> str: @pytest.fixture(scope="module") -def session_with_bq_connection_and_permanent_dataset( +def session_with_bq_connection( bq_cf_connection, dataset_id_permanent ) -> bigframes.Session: session = bigframes.Session( bigframes.BigQueryOptions(bq_connection=bq_cf_connection) ) - session._session_dataset = bigquery.Dataset(dataset_id_permanent) return session @@ -277,13 +276,11 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_direct_session_param( - session_with_bq_connection_and_permanent_dataset, scalars_dfs -): +def test_remote_function_direct_session_param(session_with_bq_connection, scalars_dfs): @rf.remote_function( [int], int, - session=session_with_bq_connection_and_permanent_dataset, + session=session_with_bq_connection, ) def square(x): return x * x @@ -313,9 +310,7 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_via_session_default( - session_with_bq_connection_and_permanent_dataset, scalars_dfs -): +def test_remote_function_via_session_default(session_with_bq_connection, scalars_dfs): # Session has bigquery connection initialized via context. Without an # explicit dataset the default dataset from the session would be used. # Without an explicit bigquery connection, the one present in Session set @@ -323,7 +318,7 @@ def test_remote_function_via_session_default( # the default behavior of reuse=True will take effect. Please note that the # udf is same as the one used in other tests in this file so the underlying # cloud function would be common and quickly reused. - @session_with_bq_connection_and_permanent_dataset.remote_function([int], int) + @session_with_bq_connection.remote_function([int], int) def square(x): return x * x @@ -391,15 +386,11 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_dataframe_applymap( - session_with_bq_connection_and_permanent_dataset, scalars_dfs -): +def test_dataframe_applymap(session_with_bq_connection, scalars_dfs): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection_and_permanent_dataset.remote_function( - [int], int - )(add_one) + remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) scalars_df, scalars_pandas_df = scalars_dfs int64_cols = ["int64_col", "int64_too"] @@ -422,15 +413,11 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_dataframe_applymap_na_ignore( - session_with_bq_connection_and_permanent_dataset, scalars_dfs -): +def test_dataframe_applymap_na_ignore(session_with_bq_connection, scalars_dfs): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection_and_permanent_dataset.remote_function( - [int], int - )(add_one) + remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) scalars_df, scalars_pandas_df = scalars_dfs int64_cols = ["int64_col", "int64_too"] @@ -451,13 +438,11 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_series_map(session_with_bq_connection_and_permanent_dataset, scalars_dfs): +def test_series_map(session_with_bq_connection, scalars_dfs): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection_and_permanent_dataset.remote_function( - [int], int - )(add_one) + remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) scalars_df, scalars_pandas_df = scalars_dfs From f8917abc094e222e0435891d4d184b77bfe67722 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 28 Nov 2023 18:24:16 -0800 Subject: [PATCH 185/921] fix: update the llm+kmeans notebook with recent change (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 313682530 🦕 --- .../bq_dataframes_llm_kmeans.ipynb | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 8d759509254..5f74046fc0e 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -366,18 +366,6 @@ "predicted_embeddings.head() " ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "4H_etYfsEOFP" - }, - "outputs": [], - "source": [ - "# Join the complaints with their embeddings in the same DataFrame\n", - "combined_df = downsampled_issues_df.join(predicted_embeddings)" - ] - }, { "attachments": {}, "cell_type": "markdown", @@ -426,30 +414,19 @@ "outputs": [], "source": [ "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", - "cluster_model.fit(combined_df[[\"text_embedding\"]])\n", - "clustered_result = cluster_model.predict(combined_df[[\"text_embedding\"]])\n", + "cluster_model.fit(predicted_embeddings[[\"text_embedding\"]])\n", + "clustered_result = cluster_model.predict(predicted_embeddings)\n", "# Notice the CENTROID_ID column, which is the ID number of the group that\n", "# each complaint belongs to.\n", "clustered_result.head(n=5)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Join the group number to the complaints and their text embeddings\n", - "combined_clustered_result = combined_df.join(clustered_result)\n", - "combined_clustered_result.head(n=5) " - ] - }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Our dataframe combined_clustered_result now has three columns: the complaints, their text embeddings, and an ID from 1-10 (inclusive) indicating which semantically similar group they belong to." + "Our dataframe combined_clustered_result now has three complaint columns: the content, their text embeddings, and an ID from 1-10 (inclusive) indicating which semantically similar group they belong to." ] }, { @@ -480,14 +457,14 @@ "source": [ "# Using bigframes, with syntax identical to pandas,\n", "# filter out the first and second groups\n", - "cluster_1_result = combined_clustered_result[\n", - " combined_clustered_result[\"CENTROID_ID\"] == 1\n", - "][[\"consumer_complaint_narrative\"]]\n", + "cluster_1_result = clustered_result[\n", + " clustered_result[\"CENTROID_ID\"] == 1\n", + "][[\"content\"]]\n", "cluster_1_result_pandas = cluster_1_result.head(5).to_pandas()\n", "\n", - "cluster_2_result = combined_clustered_result[\n", - " combined_clustered_result[\"CENTROID_ID\"] == 2\n", - "][[\"consumer_complaint_narrative\"]]\n", + "cluster_2_result = clustered_result[\n", + " clustered_result[\"CENTROID_ID\"] == 2\n", + "][[\"content\"]]\n", "cluster_2_result_pandas = cluster_2_result.head(5).to_pandas()" ] }, @@ -503,15 +480,15 @@ "prompt1 = 'comment list 1:\\n'\n", "for i in range(5):\n", " prompt1 += str(i + 1) + '. ' + \\\n", - " cluster_1_result_pandas[\"consumer_complaint_narrative\"].iloc[i] + '\\n'\n", + " cluster_1_result_pandas[\"content\"].iloc[i] + '\\n'\n", "\n", "prompt2 = 'comment list 2:\\n'\n", "for i in range(5):\n", " prompt2 += str(i + 1) + '. ' + \\\n", - " cluster_2_result_pandas[\"consumer_complaint_narrative\"].iloc[i] + '\\n'\n", + " cluster_2_result_pandas[\"content\"].iloc[i] + '\\n'\n", "\n", "print(prompt1)\n", - "print(prompt2)\n" + "print(prompt2)" ] }, { From fbc31ab0fb166e6ffab48b3507614fef566ec64c Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 29 Nov 2023 03:52:20 +0000 Subject: [PATCH 186/921] test: fix prerelease tests (#239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heads of google-cloud-bigquery and google-cloud-bigquery-storage packages are installed with -e. This is leading to `from google.cloud import bigquery` looking into the folder of the latter and running into "ImportError: cannot import name 'bigquery' from 'google.cloud' (.../google-cloud-bigquery-storage/google/cloud/__init__.py). Removing -e from google-cloud-bigquery installation gets rid of this error. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 313701211 🦕 --- noxfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index c1fb53f7947..3b10a37fc7d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -548,7 +548,6 @@ def prerelease(session: nox.sessions.Session, tests_path): # Ensure we catch breaking changes in the client libraries early. session.install( "--upgrade", - "-e", "git+https://github.com/googleapis/python-bigquery.git#egg=google-cloud-bigquery", ) already_installed.add("google-cloud-bigquery") From d0d9b84b101eb03c499d85e74dcfc900dedd4137 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:15:41 -0800 Subject: [PATCH 187/921] fix: add df snapshots lookup for `read_gbq` (#229) --- bigframes/pandas/__init__.py | 6 ++ bigframes/session/__init__.py | 56 +++++++++++-------- bigframes/session/_io/bigquery.py | 5 -- tests/system/small/test_session.py | 18 ++++++ tests/unit/session/test_io_bigquery.py | 14 ----- .../bigframes_vendored/pandas/io/gbq.py | 3 + 6 files changed, 59 insertions(+), 43 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index d35f8383665..0c2c1f87aa6 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -486,6 +486,7 @@ def read_gbq( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + use_cache: bool = True, ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query_or_table) return global_session.with_default_session( @@ -494,6 +495,7 @@ def read_gbq( index_col=index_col, col_order=col_order, max_results=max_results, + use_cache=use_cache, ) @@ -516,6 +518,7 @@ def read_gbq_query( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + use_cache: bool = True, ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) return global_session.with_default_session( @@ -524,6 +527,7 @@ def read_gbq_query( index_col=index_col, col_order=col_order, max_results=max_results, + use_cache=use_cache, ) @@ -536,6 +540,7 @@ def read_gbq_table( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + use_cache: bool = True, ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) return global_session.with_default_session( @@ -544,6 +549,7 @@ def read_gbq_table( index_col=index_col, col_order=col_order, max_results=max_results, + use_cache=use_cache, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index d2f6137883c..84a6eb56381 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -177,6 +177,7 @@ def __init__( # Now that we're starting the session, don't allow the options to be # changed. context._session_started = True + self._df_snapshot: Dict[bigquery.TableReference, datetime.datetime] = {} @property def bqclient(self): @@ -232,6 +233,7 @@ def read_gbq( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + use_cache: bool = True, # Add a verify index argument that fails if the index is not unique. ) -> dataframe.DataFrame: # TODO(b/281571214): Generate prompt to show the progress of read_gbq. @@ -242,6 +244,7 @@ def read_gbq( col_order=col_order, max_results=max_results, api_name="read_gbq", + use_cache=use_cache, ) else: # TODO(swast): Query the snapshot table but mark it as a @@ -253,6 +256,7 @@ def read_gbq( col_order=col_order, max_results=max_results, api_name="read_gbq", + use_cache=use_cache, ) def _query_to_destination( @@ -260,6 +264,7 @@ def _query_to_destination( query: str, index_cols: List[str], api_name: str, + use_cache: bool = True, ) -> Tuple[Optional[bigquery.TableReference], Optional[bigquery.QueryJob]]: # If a dry_run indicates this is not a query type job, then don't # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. @@ -284,6 +289,7 @@ def _query_to_destination( job_config = bigquery.QueryJobConfig() job_config.labels["bigframes-api"] = api_name job_config.destination = temp_table + job_config.use_query_cache = use_cache try: # Write to temp table to workaround BigQuery 10 GB query results @@ -305,6 +311,7 @@ def read_gbq_query( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + use_cache: bool = True, ) -> dataframe.DataFrame: """Turn a SQL query into a DataFrame. @@ -362,6 +369,7 @@ def read_gbq_query( col_order=col_order, max_results=max_results, api_name="read_gbq_query", + use_cache=use_cache, ) def _read_gbq_query( @@ -372,6 +380,7 @@ def _read_gbq_query( col_order: Iterable[str] = (), max_results: Optional[int] = None, api_name: str = "read_gbq_query", + use_cache: bool = True, ) -> dataframe.DataFrame: if isinstance(index_col, str): index_cols = [index_col] @@ -379,7 +388,10 @@ def _read_gbq_query( index_cols = list(index_col) destination, query_job = self._query_to_destination( - query, index_cols, api_name=api_name + query, + index_cols, + api_name=api_name, + use_cache=use_cache, ) # If there was no destination table, that means the query must have @@ -403,6 +415,7 @@ def _read_gbq_query( index_col=index_cols, col_order=col_order, max_results=max_results, + use_cache=use_cache, ) def read_gbq_table( @@ -412,6 +425,7 @@ def read_gbq_table( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + use_cache: bool = True, ) -> dataframe.DataFrame: """Turn a BigQuery table into a DataFrame. @@ -434,6 +448,7 @@ def read_gbq_table( col_order=col_order, max_results=max_results, api_name="read_gbq_table", + use_cache=use_cache, ) def _get_snapshot_sql_and_primary_key( @@ -441,6 +456,7 @@ def _get_snapshot_sql_and_primary_key( table_ref: bigquery.table.TableReference, *, api_name: str, + use_cache: bool = True, ) -> Tuple[ibis_types.Table, Optional[Sequence[str]]]: """Create a read-only Ibis table expression representing a table. @@ -448,19 +464,6 @@ def _get_snapshot_sql_and_primary_key( column(s), then return those too so that ordering generation can be avoided. """ - if table_ref.dataset_id.upper() == "_SESSION": - # _SESSION tables aren't supported by the tables.get REST API. - return ( - self.ibis_client.sql( - f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" - ), - None, - ) - table_expression = self.ibis_client.table( - table_ref.table_id, - database=f"{table_ref.project}.{table_ref.dataset_id}", - ) - # If there are primary keys defined, the query engine assumes these # columns are unique, even if the constraint is not enforced. We make # the same assumption and use these columns as the total ordering keys. @@ -481,14 +484,18 @@ def _get_snapshot_sql_and_primary_key( job_config = bigquery.QueryJobConfig() job_config.labels["bigframes-api"] = api_name - current_timestamp = list( - self.bqclient.query( - "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", - job_config=job_config, - ).result() - )[0][0] + if use_cache and table_ref in self._df_snapshot.keys(): + snapshot_timestamp = self._df_snapshot[table_ref] + else: + snapshot_timestamp = list( + self.bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + self._df_snapshot[table_ref] = snapshot_timestamp table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table_ref, current_timestamp) + bigframes_io.create_snapshot_sql(table_ref, snapshot_timestamp) ) return table_expression, primary_keys @@ -500,12 +507,11 @@ def _read_gbq_table( col_order: Iterable[str] = (), max_results: Optional[int] = None, api_name: str, + use_cache: bool = True, ) -> dataframe.DataFrame: if max_results and max_results <= 0: raise ValueError("`max_results` should be a positive number.") - # TODO(swast): Can we re-use the temp table from other reads in the - # session, if the original table wasn't modified? table_ref = bigquery.table.TableReference.from_string( query, default_project=self.bqclient.project ) @@ -513,7 +519,9 @@ def _read_gbq_table( ( table_expression, total_ordering_cols, - ) = self._get_snapshot_sql_and_primary_key(table_ref, api_name=api_name) + ) = self._get_snapshot_sql_and_primary_key( + table_ref, api_name=api_name, use_cache=use_cache + ) for key in col_order: if key not in table_expression.columns: diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index dae73301e7b..4770f120894 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -117,11 +117,6 @@ def create_snapshot_sql( table_ref: bigquery.TableReference, current_timestamp: datetime.datetime ) -> str: """Query a table via 'time travel' for consistent reads.""" - - # If we have a _SESSION table, assume that it's already a copy. Nothing to do here. - if table_ref.dataset_id.upper() == "_SESSION": - return f"SELECT * FROM `_SESSION`.`{table_ref.table_id}`" - # If we have an anonymous query results table, it can't be modified and # there isn't any BigQuery time travel. if table_ref.dataset_id.startswith("_"): diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 7cd9f1dd598..26c5093b352 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -16,6 +16,7 @@ import random import tempfile import textwrap +import time import typing from typing import List @@ -308,6 +309,23 @@ def test_read_gbq_w_script_no_select(session, dataset_id: str): assert df["statement_type"][0] == "SCRIPT" +def test_read_gbq_twice_with_same_timestamp(session, penguins_table_id): + df1 = session.read_gbq(penguins_table_id) + time.sleep(1) + df2 = session.read_gbq(penguins_table_id) + df1.columns = [ + "species1", + "island1", + "culmen_length_mm1", + "culmen_depth_mm1", + "flipper_length_mm1", + "body_mass_g1", + "sex1", + ] + df3 = df1.join(df2) + assert df3 is not None + + 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/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index c87835c4126..3f3bfbe7d36 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -147,20 +147,6 @@ def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql -def test_create_snapshot_sql_doesnt_timetravel_session_tables(): - table_ref = bigquery.TableReference.from_string("my-test-project._session.abcdefg") - - sql = bigframes.session._io.bigquery.create_snapshot_sql( - table_ref, datetime.datetime.now(datetime.timezone.utc) - ) - - # We aren't modifying _SESSION tables, so don't use time travel. - assert "SYSTEM_TIME" not in sql - - # Don't need the project ID for _SESSION tables. - assert "my-test-project" not in sql - - def test_create_temp_table_default_expiration(): """Make sure the created table has an expiration.""" bqclient = mock.create_autospec(bigquery.Client) diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 2161310b07c..eabb48e600a 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -16,6 +16,7 @@ def read_gbq( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + use_cache: bool = True, ): """Loads a DataFrame from BigQuery. @@ -83,6 +84,8 @@ def read_gbq( max_results (Optional[int], default None): If set, limit the maximum number of rows to fetch from the query results. + use_cache (bool, default True): + Whether to cache the query inputs. Default to True. Returns: bigframes.dataframe.DataFrame: A DataFrame representing results of the query or table. From 0bfc4fb117686c734d4a2503d5a6de0e64e9f9b9 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:44:16 -0800 Subject: [PATCH 188/921] feat: add remote vertex model support (#237) b/299356085 --- bigframes/ml/__init__.py | 1 + bigframes/ml/core.py | 8 ++ bigframes/ml/remote.py | 157 +++++++++++++++++++++++++ bigframes/ml/sql.py | 26 +++- docs/reference/bigframes.ml/index.rst | 2 + docs/reference/bigframes.ml/remote.rst | 7 ++ docs/templates/toc.yml | 6 + tests/system/small/ml/conftest.py | 41 +++++++ tests/system/small/ml/test_core.py | 16 +++ tests/system/small/ml/test_remote.py | 33 ++++++ tests/unit/ml/test_sql.py | 26 ++++ 11 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 bigframes/ml/remote.py create mode 100644 docs/reference/bigframes.ml/remote.rst create mode 100644 tests/system/small/ml/test_remote.py diff --git a/bigframes/ml/__init__.py b/bigframes/ml/__init__.py index 55c8709d8d8..b2c62ff9612 100644 --- a/bigframes/ml/__init__.py +++ b/bigframes/ml/__init__.py @@ -26,4 +26,5 @@ "llm", "forecasting", "imported", + "remote", ] diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index d8135f7085f..5aad77a3942 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -294,6 +294,8 @@ def create_remote_model( self, session: bigframes.Session, connection_name: str, + input: Mapping[str, str] = {}, + output: Mapping[str, str] = {}, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: """Create a session-temporary BQML remote model with the CREATE OR REPLACE MODEL statement @@ -301,6 +303,10 @@ def create_remote_model( Args: connection_name: a BQ connection to talk with Vertex AI, of the format ... https://cloud.google.com/bigquery/docs/create-cloud-resource-connection + input: + input schema for general remote models + output: + output schema for general remote models options: a dict of options to configure the model. Generates a BQML OPTIONS clause @@ -311,6 +317,8 @@ def create_remote_model( sql = self._model_creation_sql_generator.create_remote_model( connection_name=connection_name, model_ref=model_ref, + input=input, + output=output, options=options, ) diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py new file mode 100644 index 00000000000..d4c34bbd0db --- /dev/null +++ b/bigframes/ml/remote.py @@ -0,0 +1,157 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BigFrames general remote models.""" + +from __future__ import annotations + +from typing import Mapping, Optional, Union +import warnings + +import bigframes +from bigframes import clients +from bigframes.core import log_adapter +from bigframes.ml import base, core, globals, utils +import bigframes.pandas as bpd + +_SUPPORTED_DTYPES = ( + "bool", + "string", + "int64", + "float64", + "array", + "array", + "array", + "array", +) + +_REMOTE_MODEL_STATUS = "remote_model_status" + + +@log_adapter.class_logger +class VertexAIModel(base.BaseEstimator): + """Remote model from a Vertex AI https endpoint. User must specify https endpoint, input schema and output schema. + How to deploy a model in Vertex AI https://cloud.google.com/bigquery/docs/bigquery-ml-remote-model-tutorial#Deploy-Model-on-Vertex-AI. + + Args: + endpoint (str): + Vertex AI https endpoint. + input ({column_name: column_type}): + Input schema. Supported types are "bool", "string", "int64", "float64", "array", "array", "array", "array". + output ({column_name: column_type}): + Output label schema. Supported the same types as the input. + session (bigframes.Session or None): + BQ session to create the model. If None, use the global default session. + connection_name (str or None): + Connection to connect with remote service. str of the format ... + if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach + permission if the connection isn't fully setup. + """ + + def __init__( + self, + endpoint: str, + input: Mapping[str, str], + output: Mapping[str, str], + session: Optional[bigframes.Session] = None, + connection_name: Optional[str] = None, + ): + self.endpoint = endpoint + self.input = input + self.output = output + self.session = session or bpd.get_global_session() + + self._bq_connection_manager = clients.BqConnectionManager( + self.session.bqconnectionclient, self.session.resourcemanagerclient + ) + connection_name = connection_name or self.session._bq_connection + self.connection_name = self._bq_connection_manager.resolve_full_connection_name( + connection_name, + default_project=self.session._project, + default_location=self.session._location, + ) + + self._bqml_model_factory = globals.bqml_model_factory() + self._bqml_model: core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + # Parse and create connection if needed. + if not self.connection_name: + raise ValueError( + "Must provide connection_name, either in constructor or through session options." + ) + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", + ) + + options = { + "endpoint": self.endpoint, + } + + def standardize_type(v: str): + v = v.lower() + v = v.replace("boolean", "bool") + + if v not in _SUPPORTED_DTYPES: + raise ValueError( + f"Data type {v} is not supported. We only support {', '.join(_SUPPORTED_DTYPES)}." + ) + + return v + + self.input = {k: standardize_type(v) for k, v in self.input.items()} + self.output = {k: standardize_type(v) for k, v in self.output.items()} + + return self._bqml_model_factory.create_remote_model( + session=self.session, + connection_name=self.connection_name, + input=self.input, + output=self.output, + options=options, + ) + + def predict( + self, + X: Union[bpd.DataFrame, bpd.Series], + ) -> bpd.DataFrame: + """Predict the result from the input DataFrame. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame or Series, which needs to comply with the input parameter of the model. + + Returns: + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. + """ + + (X,) = utils.convert_to_dataframe(X) + + df = self._bqml_model.predict(X) + + # unlike LLM models, the general remote model status is null for successful runs. + if (df[_REMOTE_MODEL_STATUS].notna()).any(): + warnings.warn( + f"Some predictions failed. Check column {_REMOTE_MODEL_STATUS} for detailed status. You may want to filter the failed rows and retry.", + RuntimeWarning, + ) + + return df diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index ab051231fb0..1c88eda4ab3 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -57,6 +57,12 @@ def build_expressions(self, *expr_sqls: str) -> str: indent_str = " " return "\n" + indent_str + f",\n{indent_str}".join(expr_sqls) + def build_schema(self, **kwargs: str) -> str: + """Encode a dict of values into a formatted schema type items for SQL""" + indent_str = " " + param_strs = [f"{k} {v}" for k, v in kwargs.items()] + return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + def options(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: """Encode the OPTIONS clause for BQML""" return f"OPTIONS({self.build_parameters(**kwargs)})" @@ -65,6 +71,14 @@ def struct_options(self, **kwargs: Union[int, float]) -> str: """Encode a BQ STRUCT as options.""" return f"STRUCT({self.build_structs(**kwargs)})" + def input(self, **kwargs: str) -> str: + """Encode a BQML INPUT clause.""" + return f"INPUT({self.build_schema(**kwargs)})" + + def output(self, **kwargs: str) -> str: + """Encode a BQML OUTPUT clause.""" + return f"OUTPUT({self.build_schema(**kwargs)})" + # Connection def connection(self, conn_name: str) -> str: """Encode the REMOTE WITH CONNECTION clause for BQML. conn_name is of the format ...""" @@ -154,15 +168,19 @@ def create_remote_model( self, connection_name: str, model_ref: google.cloud.bigquery.ModelReference, + input: Mapping[str, str] = {}, + output: Mapping[str, str] = {}, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: """Encode the CREATE OR REPLACE MODEL statement for BQML remote model.""" - options_sql = self.options(**options) - parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] + if input: + parts.append(self.input(**input)) + if output: + parts.append(self.output(**output)) parts.append(self.connection(connection_name)) - if options_sql: - parts.append(options_sql) + if options: + parts.append(self.options(**options)) return "\n".join(parts) def create_imported_model( diff --git a/docs/reference/bigframes.ml/index.rst b/docs/reference/bigframes.ml/index.rst index f3cbe1174a7..1975d62e6d9 100644 --- a/docs/reference/bigframes.ml/index.rst +++ b/docs/reference/bigframes.ml/index.rst @@ -30,3 +30,5 @@ API Reference pipeline preprocessing + + remote diff --git a/docs/reference/bigframes.ml/remote.rst b/docs/reference/bigframes.ml/remote.rst new file mode 100644 index 00000000000..7827acfe923 --- /dev/null +++ b/docs/reference/bigframes.ml/remote.rst @@ -0,0 +1,7 @@ +bigframes.ml.remote +=================== + +.. automodule:: bigframes.ml.remote + :members: + :inherited-members: + :undoc-members: diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 9879721d286..58ac1c0efe6 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -108,6 +108,12 @@ - name: PaLM2TextEmbeddingGenerator uid: bigframes.ml.llm.PaLM2TextEmbeddingGenerator name: llm + - items: + - name: Overview + uid: bigframes.ml.remote + - name: VertexAIModel + uid: bigframes.ml.remote.VertexAIModel + name: remote - items: - name: metrics uid: bigframes.ml.metrics diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index c11445b79a5..c4a1272e441 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -29,6 +29,7 @@ imported, linear_model, llm, + remote, ) @@ -247,6 +248,46 @@ def palm2_embedding_generator_multilingual_model( ) +@pytest.fixture(scope="session") +def linear_remote_model_params() -> dict: + # Pre-deployed endpoint of linear reg model in Vertex. + # bigframes-test-linreg2 -> bigframes-test-linreg-endpoint2 + return { + "input": {"culmen_length_mm": "float64"}, + "output": {"predicted_body_mass_g": "array"}, + "endpoint": "https://us-central1-aiplatform.googleapis.com/v1/projects/1084210331973/locations/us-central1/endpoints/3193318217619603456", + } + + +@pytest.fixture(scope="session") +def bqml_linear_remote_model( + session, bq_connection, linear_remote_model_params +) -> core.BqmlModel: + options = { + "endpoint": linear_remote_model_params["endpoint"], + } + return globals.bqml_model_factory().create_remote_model( + session=session, + input=linear_remote_model_params["input"], + output=linear_remote_model_params["output"], + connection_name=bq_connection, + options=options, + ) + + +@pytest.fixture(scope="session") +def linear_remote_vertex_model( + session, bq_connection, linear_remote_model_params +) -> remote.VertexAIModel: + return remote.VertexAIModel( + endpoint=linear_remote_model_params["endpoint"], + input=linear_remote_model_params["input"], + output=linear_remote_model_params["output"], + session=session, + connection_name=bq_connection, + ) + + @pytest.fixture(scope="session") def time_series_bqml_arima_plus_model( session, time_series_arima_plus_model_name diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index be34a4871c6..22cbbb1932f 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -289,6 +289,22 @@ def test_model_predict_with_unnamed_index( ) +def test_remote_model_predict( + bqml_linear_remote_model: core.BqmlModel, new_penguins_df +): + predictions = bqml_linear_remote_model.predict(new_penguins_df).to_pandas() + expected = pd.DataFrame( + {"predicted_body_mass_g": [[3739.54], [3675.79], [3619.54]]}, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]].sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_model_generate_text( bqml_palm2_text_generator_model: core.BqmlModel, llm_text_df diff --git a/tests/system/small/ml/test_remote.py b/tests/system/small/ml/test_remote.py new file mode 100644 index 00000000000..e8eb1c85e86 --- /dev/null +++ b/tests/system/small/ml/test_remote.py @@ -0,0 +1,33 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +from bigframes.ml import remote + + +def test_remote_linear_vertex_model_predict( + linear_remote_vertex_model: remote.VertexAIModel, new_penguins_df +): + predictions = linear_remote_vertex_model.predict(new_penguins_df).to_pandas() + expected = pd.DataFrame( + {"predicted_body_mass_g": [[3739.54], [3675.79], [3619.54]]}, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + predictions[["predicted_body_mass_g"]].sort_index(), + expected, + check_exact=False, + rtol=0.1, + ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index ea16722393d..92230585405 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -190,6 +190,32 @@ def test_create_remote_model_produces_correct_sql( ) +def test_create_remote_model_with_params_produces_correct_sql( + model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, +): + sql = model_creation_sql_generator.create_remote_model( + connection_name="my_project.us.my_connection", + model_ref=bigquery.ModelReference.from_string( + "test-proj._anonXYZ.create_remote_model" + ), + input={"column1": "int64"}, + output={"result": "array"}, + options={"option_key1": "option_value1", "option_key2": 2}, + ) + assert ( + sql + == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_remote_model` +INPUT( + column1 int64) +OUTPUT( + result array) +REMOTE WITH CONNECTION `my_project.us.my_connection` +OPTIONS( + option_key1="option_value1", + option_key2=2)""" + ) + + def test_create_imported_model_produces_correct_sql( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): From 6c899be2989e24f697d72fe1bb92ebbf7dec84cb Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:12:15 -0800 Subject: [PATCH 189/921] chore: release 0.15.0 (#241) Release-As: 0.15.0 From 8089b15feddaeb9c56a8f976b439315fcfed0301 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:46:09 -0800 Subject: [PATCH 190/921] chore(main): release 0.15.0 (#214) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 45 ++++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091967513aa..ef75a017e01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,51 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.15.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.14.1...v0.15.0) (2023-11-29) + + +### ⚠ BREAKING CHANGES + +* model.predict returns all the columns ([#204](https://github.com/googleapis/python-bigquery-dataframes/issues/204)) + +### Features + +* Add info and memory_usage methods to dataframe ([#219](https://github.com/googleapis/python-bigquery-dataframes/issues/219)) ([9d6613d](https://github.com/googleapis/python-bigquery-dataframes/commit/9d6613d318b558722b7bab12773efdea4bbe9931)) +* Add remote vertex model support ([#237](https://github.com/googleapis/python-bigquery-dataframes/issues/237)) ([0bfc4fb](https://github.com/googleapis/python-bigquery-dataframes/commit/0bfc4fb117686c734d4a2503d5a6de0e64e9f9b9)) +* Add the recent api method for ML component ([#225](https://github.com/googleapis/python-bigquery-dataframes/issues/225)) ([ed8876d](https://github.com/googleapis/python-bigquery-dataframes/commit/ed8876d3439a3b45b65e8789737c3c2e3a7f1adb)) +* Model.predict returns all the columns ([#204](https://github.com/googleapis/python-bigquery-dataframes/issues/204)) ([416171a](https://github.com/googleapis/python-bigquery-dataframes/commit/416171a70d91d4a6b71622ba72685147ab7d6186)) +* Send warnings on LLM prediction partial failures ([#216](https://github.com/googleapis/python-bigquery-dataframes/issues/216)) ([81125f9](https://github.com/googleapis/python-bigquery-dataframes/commit/81125f9505ad98e89939769a8e1fcf30518705f0)) + + +### Bug Fixes + +* Add df snapshots lookup for `read_gbq` ([#229](https://github.com/googleapis/python-bigquery-dataframes/issues/229)) ([d0d9b84](https://github.com/googleapis/python-bigquery-dataframes/commit/d0d9b84b101eb03c499d85e74dcfc900dedd4137)) +* Avoid unnecessary row_number() on sort key for io ([#211](https://github.com/googleapis/python-bigquery-dataframes/issues/211)) ([a18d40e](https://github.com/googleapis/python-bigquery-dataframes/commit/a18d40e808ee0822d21715cc3e8f794c418aeebc)) +* Dedup special character ([#209](https://github.com/googleapis/python-bigquery-dataframes/issues/209)) ([dd78acb](https://github.com/googleapis/python-bigquery-dataframes/commit/dd78acb174545ba292776a642afcec46f8ee4a2a)) +* Invalid JSON type of the notebook ([#215](https://github.com/googleapis/python-bigquery-dataframes/issues/215)) ([a729831](https://github.com/googleapis/python-bigquery-dataframes/commit/a7298317ea2604faa6ae31817f1f729d7e0b9818)) +* Make to_pandas override enable_downsampling when sampling_method is manually set. ([#200](https://github.com/googleapis/python-bigquery-dataframes/issues/200)) ([ae03756](https://github.com/googleapis/python-bigquery-dataframes/commit/ae03756f5ee45e0e74e0c0bdd4777e018eba2273)) +* Polish the llm+kmeans notebook ([#208](https://github.com/googleapis/python-bigquery-dataframes/issues/208)) ([e8532b1](https://github.com/googleapis/python-bigquery-dataframes/commit/e8532b1d999d26ea1ebdd30efb8f2c0a93a6a28d)) +* Update the llm+kmeans notebook with recent change ([#236](https://github.com/googleapis/python-bigquery-dataframes/issues/236)) ([f8917ab](https://github.com/googleapis/python-bigquery-dataframes/commit/f8917abc094e222e0435891d4d184b77bfe67722)) +* Use anonymous dataset to create `remote_function` ([#205](https://github.com/googleapis/python-bigquery-dataframes/issues/205)) ([69b016e](https://github.com/googleapis/python-bigquery-dataframes/commit/69b016eae7ea97d84ceeb22ba09f5472841db072)) + + +### Documentation + +* Add code samples for `index` and `column` properties ([#212](https://github.com/googleapis/python-bigquery-dataframes/issues/212)) ([c88d38e](https://github.com/googleapis/python-bigquery-dataframes/commit/c88d38e69682f4c620174086b8f16f4780c04811)) +* Add code samples for df reshaping, function, merge, and join methods ([#203](https://github.com/googleapis/python-bigquery-dataframes/issues/203)) ([010486c](https://github.com/googleapis/python-bigquery-dataframes/commit/010486c3494e05d714da6cc7d51514518d9ae1ea)) +* Add examples for dataframe.kurt, dataframe.std, dataframe.count ([#232](https://github.com/googleapis/python-bigquery-dataframes/issues/232)) ([f9c6e72](https://github.com/googleapis/python-bigquery-dataframes/commit/f9c6e727e2b901310bb5301da449d616ea85e135)) +* Add examples for dataframe.mean, dataframe.median, dataframe.va… ([#228](https://github.com/googleapis/python-bigquery-dataframes/issues/228)) ([edd0522](https://github.com/googleapis/python-bigquery-dataframes/commit/edd0522747eadb74780124fb18ed7face251441d)) +* Add examples for dataframe.min, dataframe.max and dataframe.sum ([#227](https://github.com/googleapis/python-bigquery-dataframes/issues/227)) ([3a375e8](https://github.com/googleapis/python-bigquery-dataframes/commit/3a375e87b64b8fb51370bfec8f2cfdbcd8fe960a)) +* Code samples for `Series.dot` and `DataFrame.dot` ([#226](https://github.com/googleapis/python-bigquery-dataframes/issues/226)) ([b62a07a](https://github.com/googleapis/python-bigquery-dataframes/commit/b62a07a95cd60f995a48825c9874822d0eb02483)) +* Code samples for `Series.where` and `Series.mask` ([#217](https://github.com/googleapis/python-bigquery-dataframes/issues/217)) ([52dfad2](https://github.com/googleapis/python-bigquery-dataframes/commit/52dfad281def82548751a276ce42b087dbb09f9a)) +* Code samples for dataframe.any, dataframe.all and dataframe.prod ([#223](https://github.com/googleapis/python-bigquery-dataframes/issues/223)) ([d7957fa](https://github.com/googleapis/python-bigquery-dataframes/commit/d7957fad071d223ef8f6fb8f3de395c865ff60aa)) +* Make the code samples reflect default bq connection usage ([#206](https://github.com/googleapis/python-bigquery-dataframes/issues/206)) ([71844b0](https://github.com/googleapis/python-bigquery-dataframes/commit/71844b03cdbfe684320c186a0488c8c7fb4fcd6e)) + + +### Miscellaneous Chores + +* Release 0.15.0 ([#241](https://github.com/googleapis/python-bigquery-dataframes/issues/241)) ([6c899be](https://github.com/googleapis/python-bigquery-dataframes/commit/6c899be2989e24f697d72fe1bb92ebbf7dec84cb)) + ## [0.14.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.14.0...v0.14.1) (2023-11-16) diff --git a/bigframes/version.py b/bigframes/version.py index 46e57e5b888..920cb95c3d2 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.1" +__version__ = "0.15.0" From 66d1839c3e9a3011c7feb13a59d966b64cf8313f Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 30 Nov 2023 11:39:47 -0800 Subject: [PATCH 191/921] fix: update the llm_kmeans notebook (#247) --- notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 5f74046fc0e..69efb11018a 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -371,7 +371,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We now have the complaints and their text embeddings as two columns in our combined_df. Recall that complaints with numerically similar text embeddings should have similar meanings semantically. We will now group similar complaints together." + "We now have the complaints and their text embeddings as two columns in our predicted_embeddings DataFrame." ] }, { @@ -426,7 +426,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Our dataframe combined_clustered_result now has three complaint columns: the content, their text embeddings, and an ID from 1-10 (inclusive) indicating which semantically similar group they belong to." + "Our DataFrame clustered_result now has an additional column that includes an ID from 1-10 (inclusive) indicating which semantically similar group they belong to." ] }, { @@ -501,7 +501,7 @@ "source": [ "# The plain English request we will make of PaLM 2\n", "prompt = (\n", - " \"Please highlight the most obvious difference between\"\n", + " \"Please highlight the most obvious difference between \"\n", " \"the two lists of comments:\\n\" + prompt1 + prompt2\n", ")\n", "print(prompt)" From 1737acc51b4fdd9b385bbf91a758efd2e7ead11a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 30 Nov 2023 20:22:16 -0800 Subject: [PATCH 192/921] feat: add DataFrame.select_dtypes method (#242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 13 +++++++ tests/system/small/test_dataframe.py | 20 +++++++++++ .../bigframes_vendored/pandas/core/frame.py | 36 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index f7796291b9a..c6b28f1b012 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -434,6 +434,19 @@ def info( # TODO: Convert to different units (kb, mb, etc.) obuf.write(f"memory usage: {self.memory_usage().sum()} bytes\n") + def select_dtypes(self, include=None, exclude=None) -> DataFrame: + # Create empty pandas dataframe with same schema and then leverage actual pandas implementation + as_pandas = pandas.DataFrame( + { + col_id: pandas.Series([], dtype=dtype) + for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) + } + ) + selected_columns = tuple( + as_pandas.select_dtypes(include=include, exclude=exclude).columns + ) + return DataFrame(self._block.select_columns(selected_columns)) + def _set_internal_query_job(self, query_job: bigquery.QueryJob): self._query_job = query_job diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 9744d3f6e91..5940df590c3 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -297,6 +297,26 @@ def test_df_info(scalars_dfs): assert expected == bf_result.getvalue() +@pytest.mark.parametrize( + ("include", "exclude"), + [ + ("Int64", None), + (["int"], None), + ("number", None), + ([pd.Int64Dtype(), pd.BooleanDtype()], None), + (None, [pd.Int64Dtype(), pd.BooleanDtype()]), + ("Int64", ["boolean"]), + ], +) +def test_select_dtypes(scalars_dfs, include, exclude): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = scalars_pandas_df.select_dtypes(include=include, exclude=exclude) + bf_result = scalars_df.select_dtypes(include=include, exclude=exclude).to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result) + + def test_drop_index(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 099d8b8e663..3bd90be2e4f 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -158,6 +158,42 @@ def memory_usage(self, index: bool = True): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def select_dtypes(self, include=None, exclude=None) -> DataFrame: + """ + Return a subset of the DataFrame's columns based on the column dtypes. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': ["hello", "world"], 'col3': [True, False]}) + >>> df.select_dtypes(include=['Int64']) + col1 + 0 1 + 1 2 + + [2 rows x 1 columns] + + >>> df.select_dtypes(exclude=['Int64']) + col2 col3 + 0 hello True + 1 world False + + [2 rows x 2 columns] + + + Args: + include (scalar or list-like): + A selection of dtypes or strings to be included. + exclude (scalar or list-like): + A selection of dtypes or strings to be excluded. + + Returns: + DataFrame: The subset of the frame including the dtypes in ``include`` and excluding the dtypes in ``exclude``. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + # ---------------------------------------------------------------------- # IO methods (to / from other formats) def to_numpy(self, dtype=None, copy=False, na_value=None, **kwargs) -> np.ndarray: From 0523a31fa0b589f88afe0ad5b447634409ddeb86 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:06:23 -0800 Subject: [PATCH 193/921] docs: add examples for dataframe.cummin, dataframe.cummax, dataframe.cumsum, dataframe.cumprod (#243) --- .../bigframes_vendored/pandas/core/frame.py | 96 ++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 3bd90be2e4f..6b5a580e992 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3367,40 +3367,128 @@ def nunique(self): raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummin(self) -> DataFrame: - """Return cumulative minimum over a DataFrame axis. + """Return cumulative minimum over columns. Returns a DataFrame of the same size containing the cumulative minimum. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + >>> df.cummin() + A B + 0 3 1 + 1 1 1 + 2 1 1 + + [3 rows x 2 columns] + Returns: bigframes.dataframe.DataFrame: Return cumulative minimum of DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cummax(self) -> DataFrame: - """Return cumulative maximum over a DataFrame axis. + """Return cumulative maximum over columns. Returns a DataFrame of the same size containing the cumulative maximum. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + >>> df.cummax() + A B + 0 3 1 + 1 3 2 + 2 3 3 + + [3 rows x 2 columns] + Returns: bigframes.dataframe.DataFrame: Return cumulative maximum of DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumsum(self) -> DataFrame: - """Return cumulative sum over a DataFrame axis. + """Return cumulative sum over columns. Returns a DataFrame of the same size containing the cumulative sum. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + >>> df.cumsum() + A B + 0 3 1 + 1 4 3 + 2 6 6 + + [3 rows x 2 columns] + Returns: bigframes.dataframe.DataFrame: Return cumulative sum of DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cumprod(self) -> DataFrame: - """Return cumulative product over a DataFrame axis. + """Return cumulative product over columns. Returns a DataFrame of the same size containing the cumulative product. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + >>> df.cumprod() + A B + 0 3 1 + 1 3 2 + 2 6 6 + + [3 rows x 2 columns] + Returns: bigframes.dataframe.DataFrame: Return cumulative product of DataFrame. """ From 8d81e24677613dcf4d275c27a327384b8c17bc85 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 1 Dec 2023 11:58:10 -0800 Subject: [PATCH 194/921] feat: add DataFrame from_dict and from_records methods (#244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 26 +++++++ tests/system/small/test_dataframe.py | 48 ++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 75 +++++++++++++++++++ 3 files changed, 149 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index c6b28f1b012..3b0fd7008ab 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2381,6 +2381,32 @@ def _split( blocks = self._block._split(ns=ns, fracs=fracs, random_state=random_state) return [DataFrame(block) for block in blocks] + @classmethod + def from_dict( + cls, + data: dict, + orient: str = "columns", + dtype=None, + columns=None, + ) -> DataFrame: + return cls(pandas.DataFrame.from_dict(data, orient, dtype, columns)) # type: ignore + + @classmethod + def from_records( + cls, + data, + index=None, + exclude=None, + columns=None, + coerce_float: bool = False, + nrows: int | None = None, + ) -> DataFrame: + return cls( + pandas.DataFrame.from_records( + data, index, exclude, columns, coerce_float, nrows + ) + ) + def to_csv( self, path_or_buf: str, sep=",", *, header: bool = True, index: bool = True ) -> None: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 5940df590c3..9318a5d9d2f 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3309,6 +3309,54 @@ def test_df_duplicated(scalars_df_index, scalars_pandas_df_index, keep, subset): pd.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) +def test_df_from_dict_columns_orient(): + data = {"a": [1, 2], "b": [3.3, 2.4]} + bf_result = dataframe.DataFrame.from_dict(data, orient="columns").to_pandas() + pd_result = pd.DataFrame.from_dict(data, orient="columns") + assert_pandas_df_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + +def test_df_from_dict_index_orient(): + data = {"a": [1, 2], "b": [3.3, 2.4]} + bf_result = dataframe.DataFrame.from_dict( + data, orient="index", columns=["col1", "col2"] + ).to_pandas() + pd_result = pd.DataFrame.from_dict(data, orient="index", columns=["col1", "col2"]) + assert_pandas_df_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + +def test_df_from_dict_tight_orient(): + data = { + "index": [("i1", "i2"), ("i3", "i4")], + "columns": ["col1", "col2"], + "data": [[1, 2.6], [3, 4.5]], + "index_names": ["in1", "in2"], + "column_names": ["column_axis"], + } + + bf_result = dataframe.DataFrame.from_dict(data, orient="tight").to_pandas() + pd_result = pd.DataFrame.from_dict(data, orient="tight") + assert_pandas_df_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + +def test_df_from_records(): + records = ((1, "a"), (2.5, "b"), (3.3, "c"), (4.9, "d")) + + bf_result = dataframe.DataFrame.from_records( + records, columns=["c1", "c2"] + ).to_pandas() + pd_result = pd.DataFrame.from_records(records, columns=["c1", "c2"]) + assert_pandas_df_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + def test_df_to_dict(scalars_df_index, scalars_pandas_df_index): unsupported = ["numeric_col"] # formatted differently bf_result = scalars_df_index.drop(columns=unsupported).to_dict() diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 6b5a580e992..08fe8e2de0b 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -196,6 +196,81 @@ def select_dtypes(self, include=None, exclude=None) -> DataFrame: # ---------------------------------------------------------------------- # IO methods (to / from other formats) + @classmethod + def from_dict( + cls, + data: dict, + orient="columns", + dtype=None, + columns=None, + ) -> DataFrame: + """ + Construct DataFrame from dict of array-like or dicts. + + Creates DataFrame object from dictionary by columns or by index + allowing dtype specification. + + Args: + data (dict): + Of the form {field : array-like} or {field : dict}. + orient ({'columns', 'index', 'tight'}, default 'columns'): + The "orientation" of the data. If the keys of the passed dict + should be the columns of the resulting DataFrame, pass 'columns' + (default). Otherwise if the keys should be rows, pass 'index'. + If 'tight', assume a dict with keys ['index', 'columns', 'data', + 'index_names', 'column_names']. + dtype (dtype, default None): + Data type to force after DataFrame construction, otherwise infer. + columns (list, default None): + Column labels to use when ``orient='index'``. Raises a ValueError + if used with ``orient='columns'`` or ``orient='tight'``. + + Returns: + DataFrame + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @classmethod + def from_records( + cls, + data, + index=None, + exclude=None, + columns=None, + coerce_float: bool = False, + nrows: int | None = None, + ) -> DataFrame: + """ + Convert structured or record ndarray to DataFrame. + + Creates a DataFrame object from a structured ndarray, sequence of + tuples or dicts, or DataFrame. + + Args: + data (structured ndarray, sequence of tuples or dicts): + Structured input data. + index (str, list of fields, array-like): + Field of array to use as the index, alternately a specific set of + input labels to use. + exclude (sequence, default None): + Columns or fields to exclude. + columns (sequence, default None): + Column names to use. If the passed data do not have names + associated with them, this argument provides names for the + columns. Otherwise this argument indicates the order of the columns + in the result (any names not found in the data will become all-NA + columns). + coerce_float (bool, default False): + Attempt to convert values of non-string, non-numeric objects (like + decimal.Decimal) to floating point, useful for SQL result sets. + nrows (int, default None): + Number of rows to read if data is an iterator. + + Returns: + DataFrame + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def to_numpy(self, dtype=None, copy=False, na_value=None, **kwargs) -> np.ndarray: """ Convert the DataFrame to a NumPy array. From c2829e3d976a43c53251c9288266e3a8ec5304c5 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:18:16 -0800 Subject: [PATCH 195/921] docs: correct the params rendering for `ml.remote` and `ml.ensemble` modules (#248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - `ensemble.RandomForestClassifier`: https://screenshot.googleplex.com/4Q88xgdm5hkaYXu - `ensemble.RandomForestRegressor`: https://screenshot.googleplex.com/3CU6pJBjYHQvnDo - `remote.VertexAIModel`: https://screenshot.googleplex.com/8SL2max6GfPMwFe Fixes internal issue 314150462 🦕 --- bigframes/ml/remote.py | 8 +-- docs/templates/toc.yml | 12 ++-- .../sklearn/ensemble/_forest.py | 72 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index d4c34bbd0db..8da073802d7 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -47,10 +47,10 @@ class VertexAIModel(base.BaseEstimator): Args: endpoint (str): Vertex AI https endpoint. - input ({column_name: column_type}): - Input schema. Supported types are "bool", "string", "int64", "float64", "array", "array", "array", "array". - output ({column_name: column_type}): - Output label schema. Supported the same types as the input. + input (Mapping): + Input schema: `{column_name: column_type}`. Supported types are "bool", "string", "int64", "float64", "array", "array", "array", "array". + output (Mapping): + Output label schema: `{column_name: column_type}`. Supported the same types as the input. session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. connection_name (str or None): diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 58ac1c0efe6..b680a5fc1ad 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -108,12 +108,6 @@ - name: PaLM2TextEmbeddingGenerator uid: bigframes.ml.llm.PaLM2TextEmbeddingGenerator name: llm - - items: - - name: Overview - uid: bigframes.ml.remote - - name: VertexAIModel - uid: bigframes.ml.remote.VertexAIModel - name: remote - items: - name: metrics uid: bigframes.ml.metrics @@ -144,6 +138,12 @@ - name: OneHotEncoder uid: bigframes.ml.preprocessing.OneHotEncoder name: preprocessing + - items: + - name: Overview + uid: bigframes.ml.remote + - name: VertexAIModel + uid: bigframes.ml.remote.VertexAIModel + name: remote name: bigframes.ml name: BigQuery DataFrames status: beta diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py index 6be41bf9aa5..63c62274fd1 100644 --- a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -47,16 +47,16 @@ def fit(self, X, y): """Build a forest of trees from the training set (X, y). Args: - X: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): Series or DataFrame of shape (n_samples, n_features). Training data. - y: + y (bigframes.dataframe.DataFrame or bigframes.series.Series): Series or DataFrame of shape (n_samples,) or (n_samples, n_targets). Target values. Will be cast to X's dtype if necessary. Returns: - Fitted Estimator. + ForestModel: Fitted Estimator. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -73,12 +73,12 @@ def predict(self, X): mean predicted regression targets of the trees in the forest. Args: - X: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): Series or DataFrame of shape (n_samples, n_features). The data matrix for which we want to get the predictions. Returns: - The predicted values. + bigframes.dataframe.DataFrame: The predicted values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -91,38 +91,38 @@ class RandomForestRegressor(ForestRegressor): to improve the predictive accuracy and control over-fitting. Args: - num_parallel_tree: Optional[int] + num_parallel_tree (Optional[int]): Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. - tree_method: Optional[str] + tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", "hist". - min_child_weight : Optional[float] + min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. - colsample_bytree : Optional[float] + colsample_bytree (Optional[float]): Subsample ratio of columns when constructing each tree. Default to 1.0. The value should be between 0 and 1. - colsample_bylevel : Optional[float] + colsample_bylevel (Optional[float]): Subsample ratio of columns for each level. Default to 1.0. The value should be between 0 and 1. - colsample_bynode : Optional[float] + colsample_bynode (Optional[float]): Subsample ratio of columns for each split. Default to 0.8. The value should be between 0 and 1. - gamma : Optional[float] + gamma (Optional[float]): (min_split_loss) Minimum loss reduction required to make a further partition on a leaf node of the tree. Default to 0.0. - max_depth : Optional[int] + max_depth (Optional[int]): Maximum tree depth for base learners. Default to 15. The value should be greater than 0 and less than 1. - subsample : Optional[float] + subsample (Optional[float]: Subsample ratio of the training instance. Default to 0.8. The value should be greater than 0 and less than 1. - reg_alpha : Optional[float] + reg_alpha (Optional[float]): L1 regularization term on weights (xgb's alpha). Default to 0.0. - reg_lambda : Optional[float] + reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop: Optional[bool] + early_stop (Optional[bool]): Whether training should stop after the first iteration. Default to True. - min_rel_progress: Optional[float] + min_rel_progress (Optional[float]): Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. - enable_global_explain: Optional[bool] + enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. - xgboost_version: Optional[str] + xgboost_version (Optional[str]): Specifies the Xgboost version for model training. Default to "0.9". Possible values: "0.9", "1.1". """ @@ -144,7 +144,7 @@ def predict(self, X): which we want to get the predictions. Returns: - The predicted values. + bigframes.dataframe.DataFrame: The predicted values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -158,37 +158,37 @@ class RandomForestClassifier(ForestClassifier): improve the predictive accuracy and control over-fitting. Args: - num_parallel_tree: Optional[int] + num_parallel_tree (Optional[int]): Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. - tree_method: Optional[str] + tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", "hist". - min_child_weight : Optional[float] + min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. - colsample_bytree : Optional[float] + colsample_bytree (Optional[float]): Subsample ratio of columns when constructing each tree. Default to 1.0. The value should be between 0 and 1. - colsample_bylevel : Optional[float] + colsample_bylevel (Optional[float]): Subsample ratio of columns for each level. Default to 1.0. The value should be between 0 and 1. - colsample_bynode : Optional[float] + colsample_bynode (Optional[float]): Subsample ratio of columns for each split. Default to 0.8. The value should be between 0 and 1. - gamma : Optional[float] + gamma (Optional[float]): (min_split_loss) Minimum loss reduction required to make a further partition on a leaf node of the tree. Default to 0.0. - max_depth : Optional[int] + max_depth (Optional[int]): Maximum tree depth for base learners. Default to 15. The value should be greater than 0 and less than 1. - subsample : Optional[float] + subsample (Optional[float]): Subsample ratio of the training instance. Default to 0.8. The value should be greater than 0 and less than 1. - reg_alpha : Optional[float] + reg_alpha (Optional[float]): L1 regularization term on weights (xgb's alpha). Default to 0.0. - reg_lambda : Optional[float] + reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop: Optional[bool] + early_stop (Optional[bool]): Whether training should stop after the first iteration. Default to True. - min_rel_progress: Optional[float] + min_rel_progress (Optional[float]): Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. - enable_global_explain: Optional[bool] + enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. - xgboost_version: Optional[str] + xgboost_version (Optional[str]): Specifies the Xgboost version for model training. Default to "0.9". Possible values: "0.9", "1.1".ß """ From 77074ecbe7f52d1d7d1d1dc537fbe4062b407672 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:15:14 -0800 Subject: [PATCH 196/921] =?UTF-8?q?docs:=20add=20examples=20for=20datafram?= =?UTF-8?q?e.nunique,=20dataframe.diff,=20dataframe.a=E2=80=A6=20(#251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add examples for dataframe.nunique, dataframe.diff, dataframe.agg, dataframe.describe * update spacing * update ordering --- .../bigframes_vendored/pandas/core/frame.py | 114 +++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 08fe8e2de0b..174ab069f68 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3434,7 +3434,26 @@ def melt(self, id_vars, value_vars, var_name, value_name): def nunique(self): """ - Count number of distinct elements in specified axis. + Count number of distinct elements in each column. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 2]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 2 + + [3 rows x 2 columns] + + >>> df.nunique() + A 3.0 + B 2.0 + dtype: Float64 Returns: bigframes.series.Series: Series with number of distinct elements. @@ -3578,6 +3597,40 @@ def diff( Calculates the difference of a DataFrame element compared with another element in the DataFrame (default is element in previous row). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + Calculating difference with default periods=1: + + >>> df.diff() + A B + 0 + 1 -2 1 + 2 1 1 + + [3 rows x 2 columns] + + Calculating difference with periods=-1: + + >>> df.diff(periods=-1) + A B + 0 2 -1 + 1 -1 -1 + 2 + + [3 rows x 2 columns] + Args: periods (int, default 1): Periods to shift for calculating difference, accepts negative @@ -3590,7 +3643,37 @@ def diff( def agg(self, func): """ - Aggregate using one or more operations over the specified axis. + Aggregate using one or more operations over columns. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + Using a single function: + + >>> df.agg('sum') + A 6.0 + B 6.0 + dtype: Float64 + + Using a list of functions: + + >>> df.agg(['sum', 'mean']) + A B + sum 6.0 6.0 + mean 2.0 2.0 + + [2 rows x 2 columns] Args: func (function): @@ -3623,6 +3706,33 @@ def describe(self): upper percentile is ``75``. The ``50`` percentile is the same as the median. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [0, 2, 8]}) + >>> df + A B + 0 3 0 + 1 1 2 + 2 2 8 + + [3 rows x 2 columns] + + >>> df.describe() + A B + count 3.0 3.0 + mean 2.0 3.333333 + std 1.0 4.163332 + min 1.0 0.0 + 25% 1.0 0.0 + 50% 2.0 2.0 + 75% 3.0 8.0 + max 3.0 8.0 + + [8 rows x 2 columns] + Returns: bigframes.dataframe.DataFrame: Summary statistics of the Series or Dataframe provided. """ From 89a1c67fa5cbb76c1cc6ae24d5f919e22514705c Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 5 Dec 2023 02:14:14 +0000 Subject: [PATCH 197/921] docs: Fix return annotation in API docstrings (#253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 314367409 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 30 +++++++-------- .../bigframes_vendored/pandas/core/series.py | 38 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 174ab069f68..7168572705a 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -226,7 +226,7 @@ def from_dict( if used with ``orient='columns'`` or ``orient='tight'``. Returns: - DataFrame + DataFrame: DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -267,7 +267,7 @@ def from_records( Number of rows to read if data is an iterator. Returns: - DataFrame + DataFrame: DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -717,7 +717,7 @@ def to_markdown( These parameters will be passed to `tabulate `_. Returns: - DataFrame in Markdown-friendly format. + DataFrame: DataFrame in Markdown-friendly format. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1272,7 +1272,7 @@ def sort_values( if `first`; `last` puts NaNs at the end. Returns: - DataFrame with sorted values. + DataFrame: DataFrame with sorted values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1282,7 +1282,7 @@ def sort_index( """Sort object by labels (along an axis). Returns: - The original DataFrame sorted by the labels. + DataFrame: The original DataFrame sorted by the labels. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1330,7 +1330,7 @@ def eq(self, other, axis: str | int = "columns") -> DataFrame: (1 or 'columns'). Returns: - Result of the comparison. + DataFrame: Result of the comparison. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1861,7 +1861,7 @@ def rtruediv(self, other, axis: str | int = "columns") -> DataFrame: (1 or 'columns'). For Series input, axis to match Series index on. Returns: - DataFrame result of the arithmetic operation. + DataFrame: DataFrame result of the arithmetic operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2796,7 +2796,7 @@ def any(self, *, axis=0, bool_only: bool = False): Include only boolean columns. Returns: - Series + bigframes.series.Series: Series indicating if any element is True per column. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2843,7 +2843,7 @@ def all(self, axis=0, *, bool_only: bool = False): Include only boolean columns. Returns: - bigframes.series.Series: Series if all elements are True. + bigframes.series.Series: Series indicating if all elements are True per column. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3184,7 +3184,7 @@ def skew(self, *, numeric_only: bool = False): Include only float, int, boolean columns. Returns: - Series + Series: Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3225,7 +3225,7 @@ def kurt(self, *, numeric_only: bool = False): Include only float, int, boolean columns. Returns: - Series + Series: Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3770,7 +3770,7 @@ def pivot(self, *, columns, index=None, values=None): have hierarchically indexed columns. Returns: - Returns reshaped DataFrame. + DataFrame: Returns reshaped DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3808,7 +3808,7 @@ def unstack(self): (the analogue of stack when the columns are not a MultiIndex). Returns: - DataFrame or Series + DataFrame or Series: DataFrame or Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3866,7 +3866,7 @@ def index(self): dtype=object) Returns: - The index labels of the DataFrame. + Index: The index object of the DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4115,7 +4115,7 @@ def dot(self, other): The other object to compute the matrix product with. Returns: - Series or DataFrame + Series or DataFrame: If `other` is a Series, return the matrix product between self and other as a Series. If other is a DataFrame, return the matrix product of self and other in a DataFrame. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 1b751ed83b6..6b8dd1d64d6 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -90,7 +90,7 @@ def index(self): dtype=object) Returns: - The index labels of the Series. + Index: The index object of the Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -138,7 +138,7 @@ def transpose(self) -> Series: Return the transpose, which is by definition self. Returns: - Series + Series: Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -593,7 +593,7 @@ def corr(self, other, method="pearson", min_periods=None) -> float: are not yet supported, so a result will be returned for at least two observations. Returns: - float; Will return NaN if there are fewer than two numeric pairs, either series has a + float: Will return NaN if there are fewer than two numeric pairs, either series has a variance or covariance of zero, or any input value is infinite. """ raise NotImplementedError("abstract method") @@ -611,7 +611,7 @@ def diff(self) -> Series: values. Returns: - {klass}: First differences of the Series. + Series: First differences of the Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1317,7 +1317,7 @@ def le(self, other) -> Series: other: Series, or scalar value Returns: - bigframes.series.Series. The result of the comparison. + bigframes.series.Series: The result of the comparison. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1331,7 +1331,7 @@ def lt(self, other) -> Series: Args: other (Series, or scalar value): - Returns: + Returns: bigframes.series.Series: The result of the operation. """ @@ -1588,7 +1588,7 @@ def divmod(self, other) -> Series: other: Series, or scalar value Returns: - 2-Tuple of Series. The result of the operation. The result is always + 2-Tuple of Series: The result of the operation. The result is always consistent with (floordiv, mod) (though pandas may not). """ @@ -1603,7 +1603,7 @@ def rdivmod(self, other) -> Series: other: Series, or scalar value Returns: - 2-Tuple of Series. The result of the operation. The result is always + 2-Tuple of Series: The result of the operation. The result is always consistent with (rfloordiv, rmod) (though pandas may not). """ @@ -1650,7 +1650,7 @@ def max( Returns: - scalar or scalar + scalar: Scalar. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1664,7 +1664,7 @@ def min( of the ``numpy.ndarray`` method ``argmin``. Returns: - scalar or scalar + scalar: Scalar. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1692,7 +1692,7 @@ def var( Normalized by N-1 by default. Returns: - scalar or Series (if level specified) + scalar or Series (if level specified): Variance. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1702,7 +1702,7 @@ def sum(self): This is equivalent to the method ``numpy.sum``. Returns: - scalar + scalar: Scalar. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1710,7 +1710,7 @@ def mean(self): """Return the mean of the values over the requested axis. Returns: - scalar + scalar: Scalar. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1723,7 +1723,7 @@ def median(self, *, exact: bool = False): one. Note: ``exact=True`` not yet supported. Returns: - scalar + scalar: Scalar. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1731,7 +1731,7 @@ def prod(self): """Return the product of the values over the requested axis. Returns: - scalar + scalar: Scalar. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1741,7 +1741,7 @@ def skew(self): Normalized by N-1. Returns: - scalar + scalar: Scalar. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1932,7 +1932,7 @@ def clip(self): Maximum threshold value. All values above this threshold will be set to it. A missing threshold (e.g NA) will not clip the value. Returns: - Series. + Series: Series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2083,7 +2083,7 @@ def is_monotonic_increasing(self) -> bool: Return boolean if values in the object are monotonically increasing. Returns: - bool + bool: Boolean. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2093,7 +2093,7 @@ def is_monotonic_decreasing(self) -> bool: Return boolean if values in the object are monotonically decreasing. Returns: - bool + bool: Boolean. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From c8ec245070402aa0770bc9b2375693de674ca925 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 5 Dec 2023 11:34:15 -0800 Subject: [PATCH 198/921] feat: add nunique method to Series/DataFrameGroupby (#256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/groupby/__init__.py | 6 ++++++ tests/system/small/test_groupby.py | 2 ++ .../pandas/core/groupby/__init__.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 18cb83fa186..a8b8afdae7b 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -179,6 +179,9 @@ def any(self) -> df.DataFrame: def count(self) -> df.DataFrame: return self._aggregate_all(agg_ops.count_op) + def nunique(self) -> df.DataFrame: + return self._aggregate_all(agg_ops.nunique_op) + def cumsum(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("cumsum") @@ -442,6 +445,9 @@ def max(self, *args) -> series.Series: def count(self) -> series.Series: return self._aggregate(agg_ops.count_op) + def nunique(self) -> series.Series: + return self._aggregate(agg_ops.nunique_op) + def sum(self, *args) -> series.Series: return self._aggregate(agg_ops.sum_op) diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index a24713c2b3f..52149051860 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -69,11 +69,13 @@ def test_dataframe_groupby_median(scalars_df_index, scalars_pandas_df_index): ("operator"), [ (lambda x: x.count()), + (lambda x: x.nunique()), (lambda x: x.any()), (lambda x: x.all()), ], ids=[ "count", + "nunique", "any", "all", ], diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index b05319b4f7d..8730cf0007a 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -363,6 +363,15 @@ def agg(self, func): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def nunique(self): + """ + Return number of unique elements in the group. + + Returns: + Series: Number of unique values within each group. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + class DataFrameGroupBy(GroupBy): def agg(self, func, **kwargs): @@ -391,3 +400,12 @@ def agg(self, func, **kwargs): DataFrame """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def nunique(self): + """ + Return DataFrame with counts of unique elements in each position. + + Returns: + DataFrame + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From d3fa6f26931d5d0f0ae3fa49baccfc148f870417 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 6 Dec 2023 11:04:15 -0800 Subject: [PATCH 199/921] fix: fix value_counts column label for normalize=True (#245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/block_transforms.py | 4 +++- tests/system/small/test_dataframe.py | 6 ++---- tests/system/small/test_series.py | 11 ++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index ce0fdd219a4..df84f70859b 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -353,7 +353,9 @@ def value_counts( ) ] ) - return block.select_column(count_id).with_column_labels(["count"]) + return block.select_column(count_id).with_column_labels( + ["proportion" if normalize else "count"] + ) def pct_change(block: blocks.Block, periods: int = 1) -> blocks.Block: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 9318a5d9d2f..45490e00ca8 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3453,6 +3453,8 @@ def test_df_to_orc(scalars_df_index, scalars_pandas_df_index): ], ) def test_df_value_counts(scalars_dfs, subset, normalize, ascending, dropna): + if pd.__version__.startswith("1."): + pytest.skip("pandas 1.x produces different column labels.") scalars_df, scalars_pandas_df = scalars_dfs bf_result = ( @@ -3464,10 +3466,6 @@ def test_df_value_counts(scalars_dfs, subset, normalize, ascending, dropna): subset, normalize=normalize, ascending=ascending, dropna=dropna ) - # Older pandas version may not have these values, bigframes tries to emulate 2.0+ - pd_result.name = "count" - pd_result.index.names = bf_result.index.names - pd.testing.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index d9fc23fad01..92a7b6f099b 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1940,16 +1940,14 @@ def test_cummax_int(scalars_df_index, scalars_pandas_df_index): def test_value_counts(scalars_dfs): + if pd.__version__.startswith("1."): + pytest.skip("pandas 1.x produces different column labels.") scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" bf_result = scalars_df[col_name].value_counts().to_pandas() pd_result = scalars_pandas_df[col_name].value_counts() - # Older pandas version may not have these values, bigframes tries to emulate 2.0+ - pd_result.name = "count" - pd_result.index.name = col_name - pd.testing.assert_series_equal( bf_result, pd_result, @@ -1957,6 +1955,8 @@ def test_value_counts(scalars_dfs): def test_value_counts_w_cut(scalars_dfs): + if pd.__version__.startswith("1."): + pytest.skip("value_counts results different in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" @@ -1965,9 +1965,6 @@ def test_value_counts_w_cut(scalars_dfs): bf_result = bf_cut.value_counts().to_pandas() pd_result = pd_cut.value_counts() - # Older pandas version may not have these values, bigframes tries to emulate 2.0+ - pd_result.name = "count" - pd_result.index.name = col_name pd_result.index = pd_result.index.astype(pd.Int64Dtype()) pd.testing.assert_series_equal( From 5bdcc6594ef2e99e96636341d286ea70420858fe Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 7 Dec 2023 08:24:14 +0000 Subject: [PATCH 200/921] docs: add code samples for `shape` and `head` (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - `DataFrame.head`: https://screenshot.googleplex.com/BmM7jPxCk3iLuay - `Series.head`: https://screenshot.googleplex.com/7hANtzZCw8SbEKL - `Series.shape`: https://screenshot.googleplex.com/8AJ2xvLY6dmQUZe Fixes internal issue 314875595 🦕 --- .../bigframes_vendored/pandas/core/generic.py | 60 ++++++++++++++++++- .../bigframes_vendored/pandas/core/series.py | 15 ++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 127efe6a3d6..607243f8447 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -272,17 +272,73 @@ def head(self, n: int = 5): on position. It is useful for quickly testing if your object has the right type of data in it. - **Not yet supported** For negative values of `n`, this function returns + For negative values of `n`, this function returns all rows except the last `|n|` rows, equivalent to ``df[:n]``. If n is larger than the number of rows, this function returns all rows. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'animal': ['alligator', 'bee', 'falcon', 'lion', + ... 'monkey', 'parrot', 'shark', 'whale', 'zebra']}) + >>> df + animal + 0 alligator + 1 bee + 2 falcon + 3 lion + 4 monkey + 5 parrot + 6 shark + 7 whale + 8 zebra + + [9 rows x 1 columns] + + Viewing the first 5 lines: + + >>> df.head() + animal + 0 alligator + 1 bee + 2 falcon + 3 lion + 4 monkey + + [5 rows x 1 columns] + + Viewing the first `n` lines (three in this case): + + >>> df.head(3) + animal + 0 alligator + 1 bee + 2 falcon + + [3 rows x 1 columns] + + For negative values of `n`: + + >>> df.head(-3) + animal + 0 alligator + 1 bee + 2 falcon + 3 lion + 4 monkey + 5 parrot + + [6 rows x 1 columns] + Args: n (int, default 5): Default 5. Number of rows to select. Returns: - The first `n` rows of the caller object. + same type as caller: The first ``n`` rows of the caller object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 6b8dd1d64d6..e6af1648fd6 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -96,7 +96,20 @@ def index(self): @property def shape(self): - """Return a tuple of the shape of the underlying data.""" + """Return a tuple of the shape of the underlying data. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 4, 9, 16]) + >>> s.shape + (4,) + >>> s = bpd.Series(['Alice', 'Bob', bpd.NA]) + >>> s.shape + (3,) + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property From 68c6fdf78af8b87fa4ef4f832631f24d7433a4d8 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:30:15 -0800 Subject: [PATCH 201/921] fix: ml.sql logic (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/sql.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 1c88eda4ab3..5fb40624dd2 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -153,14 +153,12 @@ def create_model( ) -> str: """Encode the CREATE OR REPLACE MODEL statement for BQML""" source_sql = source_df.sql - transform_sql = self.transform(*transforms) if transforms is not None else None - options_sql = self.options(**options) parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] - if transform_sql: - parts.append(transform_sql) - if options_sql: - parts.append(options_sql) + if transforms: + parts.append(self.transform(*transforms)) + if options: + parts.append(self.options(**options)) parts.append(f"AS {source_sql}") return "\n".join(parts) @@ -189,11 +187,10 @@ def create_imported_model( options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: """Encode the CREATE OR REPLACE MODEL statement for BQML remote model.""" - options_sql = self.options(**options) parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] - if options_sql: - parts.append(options_sql) + if options: + parts.append(self.options(**options)) return "\n".join(parts) From d21c6dd26eadd64c526b0fd35b977a74b8334562 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:52:17 -0800 Subject: [PATCH 202/921] docs: correct the docs for `option_context` (#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue #315382764 🦕 --- .../bigframes_vendored/pandas/_config/config.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/_config/config.py b/third_party/bigframes_vendored/pandas/_config/config.py index 8abaca76c78..1b73e649c85 100644 --- a/third_party/bigframes_vendored/pandas/_config/config.py +++ b/third_party/bigframes_vendored/pandas/_config/config.py @@ -11,11 +11,12 @@ class option_context(contextlib.ContextDecorator): You need to invoke as ``option_context(pat, val, [(pat, val), ...])``. - Examples - -------- - >>> import bigframes - >>> with bigframes.option_context('display.max_rows', 10, 'display.max_columns', 5): - ... pass + **Examples:** + + >>> import bigframes + + >>> with bigframes.option_context('display.max_rows', 10, 'display.max_columns', 5): + ... pass """ def __init__(self, *args) -> None: From 9dd63f6dcb6234e1f3aebd63c59e1e5c717099dc Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Sat, 9 Dec 2023 02:10:15 +0000 Subject: [PATCH 203/921] fix: enfore pandas version requirement <2.1.4 (#265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There seem to be a breaking change in pandas release 2.1.4 that is failing tests using `pandas.read_json`. This change is pinning pandas dependency version to <2.1.4 until the proper fix. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 315539920 🦕 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index abf165b3df3..33515429852 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ "google-cloud-storage >=2.0.0", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. "ibis-framework[bigquery] >=6.2.0,<7.0.0dev", - "pandas >=1.5.0", + "pandas >=1.5.0,<2.1.4", "pydata-google-auth >=1.8.2", "requests >=2.27.1", "scikit-learn >=1.2.2", From 99598c7d359f1d1e0671dcf27a5c77094f3c7f67 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:02:15 -0800 Subject: [PATCH 204/921] feat: add ARIMAPlus.predict parameters (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/core.py | 4 +-- bigframes/ml/forecasting.py | 21 +++++++++-- bigframes/ml/sql.py | 6 ++-- tests/system/small/ml/test_core.py | 9 ++--- tests/system/small/ml/test_forecasting.py | 43 +++++++++++++++++++++-- tests/unit/ml/test_sql.py | 16 +++++++++ 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 5aad77a3942..1e2224c9bc0 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -126,8 +126,8 @@ def generate_text_embedding( ), ) - def forecast(self) -> bpd.DataFrame: - sql = self._model_manipulation_sql_generator.ml_forecast() + def forecast(self, options: Mapping[str, int | float]) -> bpd.DataFrame: + sql = self._model_manipulation_sql_generator.ml_forecast(struct_options=options) return self._session.read_gbq(sql, index_col="forecast_timestamp").reset_index() def evaluate(self, input_data: Optional[bpd.DataFrame] = None): diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 995201062bc..03b9857cc5f 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -86,21 +86,38 @@ def _fit( options=self._bqml_options, ) - def predict(self, X=None) -> bpd.DataFrame: + def predict( + self, X=None, horizon: int = 3, confidence_level: float = 0.95 + ) -> bpd.DataFrame: """Predict the closest cluster for each sample in X. Args: X (default None): ignored, to be compatible with other APIs. + horizon (int, default: 3): + an int value that specifies the number of time points to forecast. + The default value is 3, and the maximum value is 1000. + confidence_level (float, default 0.95): + a float value that specifies percentage of the future values that fall in the prediction interval. + The valid input range is [0.0, 1.0). Returns: bigframes.dataframe.DataFrame: The predicted DataFrames. Which contains 2 columns "forecast_timestamp" and "forecast_value". """ + if horizon < 1 or horizon > 1000: + raise ValueError(f"horizon must be [1, 1000], but is {horizon}.") + if confidence_level < 0.0 or confidence_level >= 1.0: + raise ValueError( + f"confidence_level must be [0.0, 1.0), but is {confidence_level}." + ) + if not self._bqml_model: raise RuntimeError("A model must be fitted before predict") - return self._bqml_model.forecast() + return self._bqml_model.forecast( + options={"horizon": horizon, "confidence_level": confidence_level} + ) def score( self, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 5fb40624dd2..25caaf1ac6b 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -223,9 +223,11 @@ def ml_predict(self, source_df: bpd.DataFrame) -> str: return f"""SELECT * FROM ML.PREDICT(MODEL `{self._model_name}`, ({self._source_sql(source_df)}))""" - def ml_forecast(self) -> str: + def ml_forecast(self, struct_options: Mapping[str, Union[int, float]]) -> str: """Encode ML.FORECAST for BQML""" - return f"""SELECT * FROM ML.FORECAST(MODEL `{self._model_name}`)""" + struct_options_sql = self.struct_options(**struct_options) + return f"""SELECT * FROM ML.FORECAST(MODEL `{self._model_name}`, + {struct_options_sql})""" def ml_generate_text( self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 22cbbb1932f..915c4aa444a 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -336,17 +336,18 @@ def test_model_generate_text( def test_model_forecast(time_series_bqml_arima_plus_model: core.BqmlModel): utc = pytz.utc - forecast = time_series_bqml_arima_plus_model.forecast().to_pandas()[ - ["forecast_timestamp", "forecast_value"] - ] + forecast = time_series_bqml_arima_plus_model.forecast( + {"horizon": 4, "confidence_level": 0.8} + ).to_pandas()[["forecast_timestamp", "forecast_value"]] expected = pd.DataFrame( { "forecast_timestamp": [ datetime(2017, 8, 2, tzinfo=utc), datetime(2017, 8, 3, tzinfo=utc), datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), ], - "forecast_value": [2724.472284, 2593.368389, 2353.613034], + "forecast_value": [2724.472284, 2593.368389, 2353.613034, 1781.623071], } ) expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index 948db59650d..be8d9c2bac8 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -18,8 +18,10 @@ import pyarrow as pa import pytz +from bigframes.ml import forecasting -def test_model_predict(time_series_arima_plus_model): + +def test_model_predict_default(time_series_arima_plus_model: forecasting.ARIMAPlus): utc = pytz.utc predictions = time_series_arima_plus_model.predict().to_pandas() assert predictions.shape == (3, 8) @@ -47,7 +49,40 @@ def test_model_predict(time_series_arima_plus_model): ) -def test_model_score(time_series_arima_plus_model, new_time_series_df): +def test_model_predict_params(time_series_arima_plus_model: forecasting.ARIMAPlus): + utc = pytz.utc + predictions = time_series_arima_plus_model.predict( + horizon=4, confidence_level=0.9 + ).to_pandas() + assert predictions.shape == (4, 8) + result = predictions[["forecast_timestamp", "forecast_value"]] + expected = pd.DataFrame( + { + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), + ], + "forecast_value": [2724.472284, 2593.368389, 2353.613034, 1781.623071], + } + ) + expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) + expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( + pd.ArrowDtype(pa.timestamp("us", tz="UTC")) + ) + + pd.testing.assert_frame_equal( + result, + expected, + rtol=0.1, + check_index_type=False, + ) + + +def test_model_score( + time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +): result = time_series_arima_plus_model.score( new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] ).to_pandas() @@ -69,7 +104,9 @@ def test_model_score(time_series_arima_plus_model, new_time_series_df): ) -def test_model_score_series(time_series_arima_plus_model, new_time_series_df): +def test_model_score_series( + time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +): result = time_series_arima_plus_model.score( new_time_series_df["parsed_date"], new_time_series_df["total_visits"] ).to_pandas() diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 92230585405..73d19cc0bb0 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -293,6 +293,22 @@ def test_ml_centroids_produces_correct_sql( ) +def test_forecast_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, + mock_df: bpd.DataFrame, +): + sql = model_manipulation_sql_generator.ml_forecast( + struct_options={"option_key1": 1, "option_key2": 2.2}, + ) + assert ( + sql + == """SELECT * FROM ML.FORECAST(MODEL `my_project_id.my_dataset_id.my_model_id`, + STRUCT( + 1 AS option_key1, + 2.2 AS option_key2))""" + ) + + def test_ml_generate_text_produces_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, From 3febea99358d10f823d43c3af83ea30458e579a2 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:57:30 -0800 Subject: [PATCH 205/921] feat: support dataframe.loc with conditional columns selection (#233) Co-authored-by: Tim Swast --- bigframes/core/indexers.py | 7 ++++++- tests/system/small/test_dataframe.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 69048b6845a..12a1303d29d 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -156,7 +156,12 @@ def __getitem__(self, key): bigframes.dataframe.DataFrame, _loc_getitem_series_or_dataframe(self._dataframe, key[0]), ) - return df[key[1]] + + columns = key[1] + if isinstance(columns, pd.Series) and columns.dtype == "bool": + columns = df.columns[columns] + + return df[columns] return typing.cast( bigframes.dataframe.DataFrame, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 45490e00ca8..57115335dc3 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2474,6 +2474,17 @@ def test_loc_select_column(scalars_df_index, scalars_pandas_df_index): ) +def test_loc_select_with_column_condition(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.loc[:, scalars_df_index.dtypes == "Int64"].to_pandas() + pd_result = scalars_pandas_df_index.loc[ + :, scalars_pandas_df_index.dtypes == "Int64" + ] + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + def test_loc_single_index_with_duplicate(scalars_df_index, scalars_pandas_df_index): scalars_df_index = scalars_df_index.set_index("string_col", drop=False) scalars_pandas_df_index = scalars_pandas_df_index.set_index( From 8c636978f4a21eda2856862100b7a8272797fe42 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:34:31 -0800 Subject: [PATCH 206/921] =?UTF-8?q?docs:=20add=20example=20for=20dataframe?= =?UTF-8?q?.melt,=20dataframe.pivot,=20dataframe.stac=E2=80=A6=20(#252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add example for dataframe.melt, dataframe.pivot, dataframe.stack, dataframe.unstack * remove empty line * docstring fix * spacing update * docs: correct the params rendering for `ml.remote` and `ml.ensemble` modules (#248) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - `ensemble.RandomForestClassifier`: https://screenshot.googleplex.com/4Q88xgdm5hkaYXu - `ensemble.RandomForestRegressor`: https://screenshot.googleplex.com/3CU6pJBjYHQvnDo - `remote.VertexAIModel`: https://screenshot.googleplex.com/8SL2max6GfPMwFe Fixes internal issue 314150462 🦕 * docs: add examples for dataframe.nunique, dataframe.diff, dataframe.a… (#251) * docs: add examples for dataframe.nunique, dataframe.diff, dataframe.agg, dataframe.describe * update spacing * update ordering * docs: Fix return annotation in API docstrings (#253) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 314367409 🦕 * feat: add nunique method to Series/DataFrameGroupby (#256) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 * docs: add example for dataframe.melt, dataframe.pivot, dataframe.stack, dataframe.unstack * docstring fix --------- Co-authored-by: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Co-authored-by: Shobhit Singh Co-authored-by: TrevorBergeron --- .../bigframes_vendored/pandas/core/frame.py | 179 ++++++++++++++++-- 1 file changed, 165 insertions(+), 14 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 7168572705a..5b00385eb8b 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3414,18 +3414,75 @@ def melt(self, id_vars, value_vars, var_name, value_name): the row axis, leaving just two non-identifier columns, 'variable' and 'value'. - Parameters - ---------- - id_vars (tuple, list, or ndarray, optional): - Column(s) to use as identifier variables. - value_vars (tuple, list, or ndarray, optional): - Column(s) to unpivot. If not specified, uses all columns that - are not set as `id_vars`. - var_name (scalar): - Name to use for the 'variable' column. If None it uses - ``frame.columns.name`` or 'variable'. - value_name (scalar, default 'value'): - Name to use for the 'value' column. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, None, 3, 4, 5], + ... "B": [1, 2, 3, 4, 5], + ... "C": [None, 3.5, None, 4.5, 5.0]}) + >>> df + A B C + 0 1.0 1 + 1 2 3.5 + 2 3.0 3 + 3 4.0 4 4.5 + 4 5.0 5 5.0 + + [5 rows x 3 columns] + + Using `melt` without optional arguments: + + >>> df.melt() + variable value + 0 A 1.0 + 1 A + 2 A 3.0 + 3 A 4.0 + 4 A 5.0 + 5 B 1.0 + 6 B 2.0 + 7 B 3.0 + 8 B 4.0 + 9 B 5.0 + 10 C + 11 C 3.5 + 12 C + 13 C 4.5 + 14 C 5.0 + + [15 rows x 2 columns] + + Using `melt` with `id_vars` and `value_vars`: + + >>> df.melt(id_vars='A', value_vars=['B', 'C']) + A variable value + 0 1.0 B 1 + 1 B 2 + 2 3.0 B 3 + 3 4.0 B 4 + 4 5.0 B 5 + 5 1.0 C + 6 C 3 + 7 3.0 C + 8 4.0 C 4 + 9 5.0 C 5 + + [10 rows x 3 columns] + + + Args: + id_vars (tuple, list, or ndarray, optional): + Column(s) to use as identifier variables. + value_vars (tuple, list, or ndarray, optional): + Column(s) to unpivot. If not specified, uses all columns that + are not set as `id_vars`. + var_name (scalar): + Name to use for the 'variable' column. If None it uses + ``frame.columns.name`` or 'variable'. + value_name (scalar, default 'value'): + Name to use for the 'value' column. Returns: DataFrame: Unpivoted DataFrame. @@ -3757,6 +3814,52 @@ def pivot(self, *, columns, index=None, values=None): do not together uniquely identify input rows, the output will be silently non-deterministic. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... "foo": ["one", "one", "one", "two", "two"], + ... "bar": ["A", "B", "C", "A", "B"], + ... "baz": [1, 2, 3, 4, 5], + ... "zoo": ['x', 'y', 'z', 'q', 'w'] + ... }) + + >>> df + foo bar baz zoo + 0 one A 1 x + 1 one B 2 y + 2 one C 3 z + 3 two A 4 q + 4 two B 5 w + + [5 rows x 4 columns] + + Using `pivot` without optional arguments: + + >>> df.pivot(columns='foo') + bar baz zoo + foo one two one two one two + 0 A 1 x + 1 B 2 y + 2 C 3 z + 3 A 4 q + 4 B 5 w + + [5 rows x 6 columns] + + Using `pivot` with `index` and `values`: + + >>> df.pivot(columns='foo', index='bar', values='baz') + foo one two + bar + A 1 4 + B 2 5 + C 3 + + [3 rows x 2 columns] + Args: columns (str or object or a list of str): Column to use to make new frame's columns. @@ -3774,7 +3877,7 @@ def pivot(self, *, columns, index=None, values=None): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def stack(self): + def stack(self, level=-1): """ Stack the prescribed level(s) from columns to index. @@ -3792,12 +3895,36 @@ def stack(self): BigQuery DataFrames does not support stack operations that would combine columns of different dtypes. + **Example:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': [1, 3], 'B': [2, 4]}, index=['foo', 'bar']) + >>> df + A B + foo 1 2 + bar 3 4 + + [2 rows x 2 columns] + + >>> df.stack() + foo A 1 + B 2 + bar A 3 + B 4 + dtype: Int64 + + Args: + level (int, str, or list of these, default -1 (last level)): + Level(s) to stack from the column axis onto the index axis. + Returns: DataFrame or Series: Stacked dataframe or series. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def unstack(self): + def unstack(self, level=-1): """ Pivot a level of the (necessarily hierarchical) index labels. @@ -3807,6 +3934,30 @@ def unstack(self): If the index is not a MultiIndex, the output will be a Series (the analogue of stack when the columns are not a MultiIndex). + **Example:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': [1, 3], 'B': [2, 4]}, index=['foo', 'bar']) + >>> df + A B + foo 1 2 + bar 3 4 + + [2 rows x 2 columns] + + >>> df.unstack() + A foo 1 + bar 3 + B foo 2 + bar 4 + dtype: Int64 + + Args: + level (int, str, or list of these, default -1 (last level)): + Level(s) of index to unstack, can pass level name. + Returns: DataFrame or Series: DataFrame or Series. """ From e735412fdc52d034df92dd5462d6956bdc0167be Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:54:55 -0800 Subject: [PATCH 207/921] =?UTF-8?q?docs:=20add=20example=20to=20dataframe.?= =?UTF-8?q?nlargest,=20dataframe.nsmallest,=20datafra=E2=80=A6=20(#234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add example to dataframe.nlargest, dataframe.nsmallest, dataframe.idxmin, dataframe .idxmax * update example output --- .../bigframes_vendored/pandas/core/frame.py | 151 +++++++++++++++++- 1 file changed, 147 insertions(+), 4 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 5b00385eb8b..4753bfc5892 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3324,6 +3324,58 @@ def nlargest(self, n: int, columns, keep: str = "first"): ``df.sort_values(columns, ascending=False).head(n)``, but more performant. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 1, 3, 3, 5, 5], + ... "B": [5, 6, 3, 4, 1, 2], + ... "C": ['a', 'b', 'a', 'b', 'a', 'b']}) + >>> df + A B C + 0 1 5 a + 1 1 6 b + 2 3 3 a + 3 3 4 b + 4 5 1 a + 5 5 2 b + + [6 rows x 3 columns] + + Returns rows with the largest value in 'A', including all ties: + + >>> df.nlargest(1, 'A', keep = "all") + A B C + 4 5 1 a + 5 5 2 b + + [2 rows x 3 columns] + + Returns the first row with the largest value in 'A', default behavior in case of ties: + + >>> df.nlargest(1, 'A') + A B C + 4 5 1 a + + [1 rows x 3 columns] + + Returns the last row with the largest value in 'A' in case of ties: + + >>> df.nlargest(1, 'A', keep = "last") + A B C + 5 5 2 b + + [1 rows x 3 columns] + + Returns the row with the largest combined values in both 'A' and 'C': + + >>> df.nlargest(1, ['A', 'C']) + A B C + 5 5 2 b + + [1 rows x 3 columns] + Args: n (int): Number of rows to return. @@ -3359,6 +3411,59 @@ def nsmallest(self, n: int, columns, keep: str = "first"): ``df.sort_values(columns, ascending=True).head(n)``, but more performant. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 1, 3, 3, 5, 5], + ... "B": [5, 6, 3, 4, 1, 2], + ... "C": ['a', 'b', 'a', 'b', 'a', 'b']}) + >>> df + A B C + 0 1 5 a + 1 1 6 b + 2 3 3 a + 3 3 4 b + 4 5 1 a + 5 5 2 b + + [6 rows x 3 columns] + + Returns rows with the smallest value in 'A', including all ties: + + >>> df.nsmallest(1, 'A', keep = "all") + A B C + 0 1 5 a + 1 1 6 b + + [2 rows x 3 columns] + + Returns the first row with the smallest value in 'A', default behavior in case of ties: + + >>> df.nsmallest(1, 'A') + A B C + 0 1 5 a + + [1 rows x 3 columns] + + Returns the last row with the smallest value in 'A' in case of ties: + + >>> df.nsmallest(1, 'A', keep = "last") + A B C + 1 1 6 b + + [1 rows x 3 columns] + + Returns rows with the smallest values in 'A' and 'C' + + >>> df.nsmallest(1, ['A', 'C']) + A B C + 0 1 5 a + + [1 rows x 3 columns] + + Args: n (int): Number of rows to return. @@ -3384,23 +3489,61 @@ def nsmallest(self, n: int, columns, keep: str = "first"): def idxmin(self): """ - Return index of first occurrence of minimum over requested axis. + Return index of first occurrence of minimum over columns. NA/null values are excluded. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + >>> df.idxmin() + A 1 + B 0 + dtype: Int64 + Returns: - Series: Indexes of minima along the specified axis. + Series: Indexes of minima along the columns. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def idxmax(self): """ - Return index of first occurrence of maximum over requested axis. + Return index of first occurrence of maximum over columns. NA/null values are excluded. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [3, 1, 2], "B": [1, 2, 3]}) + >>> df + A B + 0 3 1 + 1 1 2 + 2 2 3 + + [3 rows x 2 columns] + + >>> df.idxmax() + A 0 + B 2 + dtype: Int64 + Returns: - Series: Indexes of maxima along the specified axis. + Series: Indexes of maxima along the columns. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From b02fc2c1843e18d3a8d6894c64763f53e6af1b73 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 12 Dec 2023 02:34:27 +0000 Subject: [PATCH 208/921] fix: pin prerelease tests to pandas 2.1.3 to unblock e2e tests (#268) * fix: pin prerelease tests to pandas 2.1.3 to unblock e2e tests * specify excluded pandas version differently, to automatically test on a release --- noxfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 3b10a37fc7d..2174e27529f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -518,7 +518,9 @@ def prerelease(session: nox.sessions.Session, tests_path): "--prefer-binary", "--pre", "--upgrade", - "pandas", + # TODO(shobs): Remove tying to version 2.1.3 after + # https://github.com/pandas-dev/pandas/issues/56463 is resolved + "pandas!=2.1.4", ) already_installed.add("pandas") From 8766ac63f501929577f71e6bd2b523e92c43ba66 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 12 Dec 2023 03:22:15 +0000 Subject: [PATCH 209/921] test: migrate e2e presubmit tests to bigframes-load-testing project (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BEGIN_COMMIT_OVERRIDE fix: migrate e2e tests to bigframes-load-testing project END_COMMIT_OVERRIDE Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 307809767 🦕 --- .kokoro/continuous/e2e.cfg | 10 + .kokoro/presubmit/e2e.cfg | 10 + CONTRIBUTING.rst | 39 +- bigframes/remote_function.py | 24 +- .../generative_ai/large_language_models.ipynb | 6 +- .../regression/easy_linear_regression.ipynb | 1116 ++++++++--------- owlbot.py | 1 + samples/snippets/gen_ai_model_test.py | 9 +- .../load_data_from_biquery_job_test.py | 11 +- samples/snippets/quickstart_test.py | 9 +- samples/snippets/remote_function_test.py | 9 +- scripts/create_test_model_vertex.py | 71 ++ scripts/setup-project-for-testing.sh | 256 ++++ tests/system/conftest.py | 1 - tests/system/large/ml/test_decomposition.py | 8 +- tests/system/large/ml/test_pipeline.py | 21 +- tests/system/large/test_remote_function.py | 6 +- tests/system/small/ml/conftest.py | 12 +- tests/system/small/ml/test_core.py | 4 +- tests/system/small/ml/test_decomposition.py | 11 +- tests/system/small/ml/test_llm.py | 28 +- tests/system/small/test_remote_function.py | 34 +- tests/system/utils.py | 74 ++ 23 files changed, 1130 insertions(+), 640 deletions(-) create mode 100644 scripts/create_test_model_vertex.py create mode 100755 scripts/setup-project-for-testing.sh diff --git a/.kokoro/continuous/e2e.cfg b/.kokoro/continuous/e2e.cfg index 2f93a582124..74793465907 100644 --- a/.kokoro/continuous/e2e.cfg +++ b/.kokoro/continuous/e2e.cfg @@ -5,3 +5,13 @@ env_vars: { key: "NOX_SESSION" value: "unit_prerelease system_prerelease system_noextras e2e notebook samples" } + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/.kokoro/presubmit/e2e.cfg b/.kokoro/presubmit/e2e.cfg index 2f93a582124..74793465907 100644 --- a/.kokoro/presubmit/e2e.cfg +++ b/.kokoro/presubmit/e2e.cfg @@ -5,3 +5,13 @@ env_vars: { key: "NOX_SESSION" value: "unit_prerelease system_prerelease system_noextras e2e notebook samples" } + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f9103bfa72c..5146b4bc7e0 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -155,7 +155,44 @@ Running System Tests auth settings and change some configuration in your project to run all the tests. -- System tests will be run against an actual project. You should use local credentials from gcloud when possible. See `Best practices for application authentication `__. Some tests require a service account. For those tests see `Authenticating as a service account `__. +- System tests will be run against an actual project. A project can be set in + the environment variable ``$GOOGLE_CLOUD_PROJECT``. If not, the project property + set in the `Google Cloud CLI `__ + will be effective, which can be peeked into via ``gcloud config get project``, + or set via ``gcloud config set project ``. The following roles + carry the permissions to run the system tests in the project: + + - `BigQuery User `__ + to be able to create test datasets and run BigQuery jobs in the project. + + - `BigQuery Connection Admin `__ + to be able to use BigQuery connections in the project. + + - `BigQuery Data Editor `__ + to be able to create BigQuery remote functions in the project. + + - `Browser `__ + to be able to get current IAM policy for the service accounts of the BigQuery connections in the project. + + - `Cloud Functions Developer `__ + to be able to create cloud functions to support BigQuery DataFrames remote functions. + + - `Service Account User `__ + to be able to use the project's service accounts. + + - `Vertex AI User `__ + to be able to use the BigQuery DataFrames' ML integration with Vertex AI. + +- You can run the script ``scripts/setup-project-for-testing.sh []`` + to set up a project for running system tests and optionally set up necessary + IAM roles for a principal (user/group/service-account). You need to have the following + IAM permission to be able to run the set up script successfully: + + - ``serviceusage.services.enable`` + - ``bigquery.connections.create`` + - ``resourcemanager.projects.setIamPolicy`` + +- You should use local credentials from gcloud when possible. See `Best practices for application authentication `__. Some tests require a service account. For those tests see `Authenticating as a service account `__. ************* Test Coverage diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index 7280ac7d425..a899ebd371b 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -411,13 +411,23 @@ def create_cloud_function(self, def_, cf_name, package_requirements=None): create_function_request.function = function # Create the cloud function and wait for it to be ready to use - operation = self._cloud_functions_client.create_function( - request=create_function_request - ) - operation.result() - - # Cleanup - os.remove(archive_path) + try: + operation = self._cloud_functions_client.create_function( + request=create_function_request + ) + operation.result() + + # Cleanup + os.remove(archive_path) + except google.api_core.exceptions.AlreadyExists: + # If a cloud function with the same name already exists, let's + # update it + update_function_request = functions_v2.UpdateFunctionRequest() + update_function_request.function = function + operation = self._cloud_functions_client.update_function( + request=update_function_request + ) + operation.result() # Fetch the endpoint of the just created function endpoint = self.get_cloud_function_endpoint(cf_name) diff --git a/notebooks/generative_ai/large_language_models.ipynb b/notebooks/generative_ai/large_language_models.ipynb index 45a46c44af9..2695ee9dc0f 100644 --- a/notebooks/generative_ai/large_language_models.ipynb +++ b/notebooks/generative_ai/large_language_models.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -22,12 +22,12 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "session = bigframes.pandas.get_global_session()\n", - "connection = \"bigframes-dev.us.bigframes-ml\"" + "connection = f\"{session.bqclient.project}.us.bigframes-default-connection\"" ] }, { diff --git a/notebooks/regression/easy_linear_regression.ipynb b/notebooks/regression/easy_linear_regression.ipynb index c441a966ecf..fdabd82a4b9 100644 --- a/notebooks/regression/easy_linear_regression.ipynb +++ b/notebooks/regression/easy_linear_regression.ipynb @@ -26,48 +26,86 @@ "## 1. Init & load data" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import `bigframes.pandas` module and get the default session" + ] + }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas\n", + "session = bigframes.pandas.get_global_session()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define a dataset for storing BQML model, and create it if it does not exist." + ] + }, + { + "cell_type": "code", + "execution_count": 23, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ffc6d6c7815a4a92903a08a11af6db11", - "version_major": 2, - "version_minor": 0 - }, "text/plain": [ - "HTML(value='Query job d1e085ba-66d8-4631-bb51-50a17d0a6e51 is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job 8fe1dc50-9d32-4466-9c2b-76d32cbde7c5 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -75,13 +113,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "042c351aa0944eeeab8b36254f88c072", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 91aa1b30-2b0e-41eb-9bfb-4f6232913b31 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e40d99ae-1b3a-4a12-b4be-e264af8b22e5 is RUNNING. " ] }, "metadata": {}, @@ -121,250 +157,250 @@ " \n", " 0\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 36.6\n", - " 18.4\n", - " 184.0\n", - " 3475.0\n", - " FEMALE\n", + " Biscoe\n", + " 40.1\n", + " 18.9\n", + " 188.0\n", + " 4300.0\n", + " MALE\n", " \n", " \n", " 1\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 39.8\n", - " 19.1\n", - " 184.0\n", - " 4650.0\n", + " Torgersen\n", + " 39.1\n", + " 18.7\n", + " 181.0\n", + " 3750.0\n", " MALE\n", " \n", " \n", " 2\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.9\n", - " 18.9\n", - " 184.0\n", - " 3900.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 47.4\n", + " 14.6\n", + " 212.0\n", + " 4725.0\n", + " FEMALE\n", " \n", " \n", " 3\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 46.5\n", - " 17.9\n", - " 192.0\n", - " 3500.0\n", + " 42.5\n", + " 16.7\n", + " 187.0\n", + " 3350.0\n", " FEMALE\n", " \n", " \n", " 4\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.3\n", - " 16.8\n", - " 192.0\n", - " 3000.0\n", - " FEMALE\n", + " Biscoe\n", + " 43.2\n", + " 19.0\n", + " 197.0\n", + " 4775.0\n", + " MALE\n", " \n", " \n", " 5\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 43.2\n", - " 18.5\n", - " 192.0\n", - " 4100.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.7\n", + " 15.3\n", + " 219.0\n", + " 5200.0\n", " MALE\n", " \n", " \n", " 6\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 46.9\n", - " 16.6\n", - " 192.0\n", - " 2700.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 41.3\n", + " 21.1\n", + " 195.0\n", + " 4400.0\n", + " MALE\n", " \n", " \n", " 7\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 50.5\n", - " 18.4\n", - " 200.0\n", - " 3400.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 45.2\n", + " 13.8\n", + " 215.0\n", + " 4750.0\n", " FEMALE\n", " \n", " \n", " 8\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 49.5\n", - " 19.0\n", - " 200.0\n", - " 3800.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.5\n", + " 13.5\n", + " 210.0\n", + " 4550.0\n", + " FEMALE\n", " \n", " \n", " 9\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.2\n", - " 20.1\n", - " 200.0\n", - " 3975.0\n", - " MALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 50.5\n", + " 15.2\n", + " 216.0\n", + " 5000.0\n", + " FEMALE\n", " \n", " \n", " 10\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.8\n", - " 18.9\n", - " 208.0\n", - " 4300.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 48.2\n", + " 15.6\n", + " 221.0\n", + " 5100.0\n", " MALE\n", " \n", " \n", " 11\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 39.0\n", - " 18.7\n", - " 185.0\n", - " 3650.0\n", - " MALE\n", + " 38.1\n", + " 18.6\n", + " 190.0\n", + " 3700.0\n", + " FEMALE\n", " \n", " \n", " 12\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.0\n", - " 16.9\n", - " 185.0\n", - " 3000.0\n", - " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 50.7\n", + " 15.0\n", + " 223.0\n", + " 5550.0\n", + " MALE\n", " \n", " \n", " 13\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 47.0\n", - " 17.3\n", - " 185.0\n", - " 3700.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 37.8\n", + " 20.0\n", + " 190.0\n", + " 4250.0\n", + " MALE\n", " \n", " \n", " 14\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 34.0\n", - " 17.1\n", - " 185.0\n", - " 3400.0\n", + " Biscoe\n", + " 35.0\n", + " 17.9\n", + " 190.0\n", + " 3450.0\n", " FEMALE\n", " \n", " \n", " 15\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 37.0\n", - " 16.5\n", - " 185.0\n", - " 3400.0\n", - " FEMALE\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 48.7\n", + " 15.7\n", + " 208.0\n", + " 5350.0\n", + " MALE\n", " \n", " \n", " 16\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 45.7\n", - " 17.3\n", - " 193.0\n", - " 3600.0\n", - " FEMALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Torgersen\n", + " 34.6\n", + " 21.1\n", + " 198.0\n", + " 4400.0\n", + " MALE\n", " \n", " \n", " 17\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 50.6\n", - " 19.4\n", - " 193.0\n", - " 3800.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 46.8\n", + " 15.4\n", + " 215.0\n", + " 5150.0\n", " MALE\n", " \n", " \n", " 18\n", - " Adelie Penguin (Pygoscelis adeliae)\n", + " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 39.7\n", - " 17.9\n", - " 193.0\n", - " 4250.0\n", + " 50.3\n", + " 20.0\n", + " 197.0\n", + " 3300.0\n", " MALE\n", " \n", " \n", " 19\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 37.8\n", + " 37.2\n", " 18.1\n", - " 193.0\n", - " 3750.0\n", + " 178.0\n", + " 3900.0\n", " MALE\n", " \n", " \n", " 20\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 46.6\n", - " 17.8\n", - " 193.0\n", - " 3800.0\n", - " FEMALE\n", + " 51.0\n", + " 18.8\n", + " 203.0\n", + " 4100.0\n", + " MALE\n", " \n", " \n", " 21\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", - " Dream\n", - " 51.3\n", - " 19.2\n", - " 193.0\n", - " 3650.0\n", - " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Biscoe\n", + " 40.5\n", + " 17.9\n", + " 187.0\n", + " 3200.0\n", + " FEMALE\n", " \n", " \n", " 22\n", - " Adelie Penguin (Pygoscelis adeliae)\n", - " Dream\n", - " 40.2\n", - " 17.1\n", - " 193.0\n", - " 3400.0\n", + " Gentoo penguin (Pygoscelis papua)\n", + " Biscoe\n", + " 45.5\n", + " 13.9\n", + " 210.0\n", + " 4200.0\n", " FEMALE\n", " \n", " \n", " 23\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 36.8\n", + " 42.2\n", " 18.5\n", - " 193.0\n", - " 3500.0\n", + " 180.0\n", + " 3550.0\n", " FEMALE\n", " \n", " \n", " 24\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 49.6\n", - " 18.2\n", - " 193.0\n", + " 51.7\n", + " 20.3\n", + " 194.0\n", " 3775.0\n", " MALE\n", " \n", @@ -374,74 +410,72 @@ "[344 rows x 7 columns in total]" ], "text/plain": [ - " species island culmen_length_mm \\\n", - "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", - "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", - "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", - "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", - "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", - "5 Adelie Penguin (Pygoscelis adeliae) Dream 43.2 \n", - "6 Chinstrap penguin (Pygoscelis antarctica) Dream 46.9 \n", - "7 Chinstrap penguin (Pygoscelis antarctica) Dream 50.5 \n", - "8 Chinstrap penguin (Pygoscelis antarctica) Dream 49.5 \n", - "9 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "10 Adelie Penguin (Pygoscelis adeliae) Dream 40.8 \n", - "11 Adelie Penguin (Pygoscelis adeliae) Dream 39.0 \n", - "12 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "13 Chinstrap penguin (Pygoscelis antarctica) Dream 47.0 \n", - "14 Adelie Penguin (Pygoscelis adeliae) Dream 34.0 \n", - "15 Adelie Penguin (Pygoscelis adeliae) Dream 37.0 \n", - "16 Chinstrap penguin (Pygoscelis antarctica) Dream 45.7 \n", - "17 Chinstrap penguin (Pygoscelis antarctica) Dream 50.6 \n", - "18 Adelie Penguin (Pygoscelis adeliae) Dream 39.7 \n", - "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.8 \n", - "20 Chinstrap penguin (Pygoscelis antarctica) Dream 46.6 \n", - "21 Chinstrap penguin (Pygoscelis antarctica) Dream 51.3 \n", - "22 Adelie Penguin (Pygoscelis adeliae) Dream 40.2 \n", - "23 Adelie Penguin (Pygoscelis adeliae) Dream 36.8 \n", - "24 Chinstrap penguin (Pygoscelis antarctica) Dream 49.6 \n", + " species island culmen_length_mm \\\n", + "0 Adelie Penguin (Pygoscelis adeliae) Biscoe 40.1 \n", + "1 Adelie Penguin (Pygoscelis adeliae) Torgersen 39.1 \n", + "2 Gentoo penguin (Pygoscelis papua) Biscoe 47.4 \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream 42.5 \n", + "4 Adelie Penguin (Pygoscelis adeliae) Biscoe 43.2 \n", + "5 Gentoo penguin (Pygoscelis papua) Biscoe 46.7 \n", + "6 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.3 \n", + "7 Gentoo penguin (Pygoscelis papua) Biscoe 45.2 \n", + "8 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "9 Gentoo penguin (Pygoscelis papua) Biscoe 50.5 \n", + "10 Gentoo penguin (Pygoscelis papua) Biscoe 48.2 \n", + "11 Adelie Penguin (Pygoscelis adeliae) Dream 38.1 \n", + "12 Gentoo penguin (Pygoscelis papua) Biscoe 50.7 \n", + "13 Adelie Penguin (Pygoscelis adeliae) Biscoe 37.8 \n", + "14 Adelie Penguin (Pygoscelis adeliae) Biscoe 35.0 \n", + "15 Gentoo penguin (Pygoscelis papua) Biscoe 48.7 \n", + "16 Adelie Penguin (Pygoscelis adeliae) Torgersen 34.6 \n", + "17 Gentoo penguin (Pygoscelis papua) Biscoe 46.8 \n", + "18 Chinstrap penguin (Pygoscelis antarctica) Dream 50.3 \n", + "19 Adelie Penguin (Pygoscelis adeliae) Dream 37.2 \n", + "20 Chinstrap penguin (Pygoscelis antarctica) Dream 51.0 \n", + "21 Adelie Penguin (Pygoscelis adeliae) Biscoe 40.5 \n", + "22 Gentoo penguin (Pygoscelis papua) Biscoe 45.5 \n", + "23 Adelie Penguin (Pygoscelis adeliae) Dream 42.2 \n", + "24 Chinstrap penguin (Pygoscelis antarctica) Dream 51.7 \n", "\n", " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "0 18.4 184.0 3475.0 FEMALE \n", - "1 19.1 184.0 4650.0 MALE \n", - "2 18.9 184.0 3900.0 MALE \n", - "3 17.9 192.0 3500.0 FEMALE \n", - "4 16.8 192.0 3000.0 FEMALE \n", - "5 18.5 192.0 4100.0 MALE \n", - "6 16.6 192.0 2700.0 FEMALE \n", - "7 18.4 200.0 3400.0 FEMALE \n", - "8 19.0 200.0 3800.0 MALE \n", - "9 20.1 200.0 3975.0 MALE \n", - "10 18.9 208.0 4300.0 MALE \n", - "11 18.7 185.0 3650.0 MALE \n", - "12 16.9 185.0 3000.0 FEMALE \n", - "13 17.3 185.0 3700.0 FEMALE \n", - "14 17.1 185.0 3400.0 FEMALE \n", - "15 16.5 185.0 3400.0 FEMALE \n", - "16 17.3 193.0 3600.0 FEMALE \n", - "17 19.4 193.0 3800.0 MALE \n", - "18 17.9 193.0 4250.0 MALE \n", - "19 18.1 193.0 3750.0 MALE \n", - "20 17.8 193.0 3800.0 FEMALE \n", - "21 19.2 193.0 3650.0 MALE \n", - "22 17.1 193.0 3400.0 FEMALE \n", - "23 18.5 193.0 3500.0 FEMALE \n", - "24 18.2 193.0 3775.0 MALE \n", + "0 18.9 188.0 4300.0 MALE \n", + "1 18.7 181.0 3750.0 MALE \n", + "2 14.6 212.0 4725.0 FEMALE \n", + "3 16.7 187.0 3350.0 FEMALE \n", + "4 19.0 197.0 4775.0 MALE \n", + "5 15.3 219.0 5200.0 MALE \n", + "6 21.1 195.0 4400.0 MALE \n", + "7 13.8 215.0 4750.0 FEMALE \n", + "8 13.5 210.0 4550.0 FEMALE \n", + "9 15.2 216.0 5000.0 FEMALE \n", + "10 15.6 221.0 5100.0 MALE \n", + "11 18.6 190.0 3700.0 FEMALE \n", + "12 15.0 223.0 5550.0 MALE \n", + "13 20.0 190.0 4250.0 MALE \n", + "14 17.9 190.0 3450.0 FEMALE \n", + "15 15.7 208.0 5350.0 MALE \n", + "16 21.1 198.0 4400.0 MALE \n", + "17 15.4 215.0 5150.0 MALE \n", + "18 20.0 197.0 3300.0 MALE \n", + "19 18.1 178.0 3900.0 MALE \n", + "20 18.8 203.0 4100.0 MALE \n", + "21 17.9 187.0 3200.0 FEMALE \n", + "22 13.9 210.0 4200.0 FEMALE \n", + "23 18.5 180.0 3550.0 FEMALE \n", + "24 20.3 194.0 3775.0 MALE \n", "...\n", "\n", "[344 rows x 7 columns]" ] }, - "execution_count": 20, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import bigframes.pandas\n", - "\n", "# read a BigQuery table to a BigQuery DataFrame\n", - "df = bigframes.pandas.read_gbq(\"bigframes-dev.bqml_tutorial.penguins\")\n", + "df = bigframes.pandas.read_gbq(f\"bigquery-public-data.ml_datasets.penguins\")\n", "\n", "# take a peek at the dataframe\n", "df" @@ -457,18 +491,16 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 26, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0ddb322731fe4b80b2904e1610862c31", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job d2bd7c5e-2652-4c0d-8495-8ef65e89031b is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 7d289291-5c60-4d8f-b476-e46cb2ab06a7 is DONE. 28.9 kB processed. " ] }, "metadata": {}, @@ -476,13 +508,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9f91e7a3d7ed416096d7660a110e0eab", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 92f0a5e5-bc61-426f-a9ef-213a1c376851 is DONE. 28.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 8411db98-9ec3-4655-a40f-f9bf272e2403 is RUNNING. " ] }, "metadata": {}, @@ -520,294 +550,294 @@ " \n", " \n", " 0\n", - " Dream\n", - " 36.6\n", - " 18.4\n", - " 184.0\n", - " 3475.0\n", - " FEMALE\n", - " \n", - " \n", - " 1\n", - " Dream\n", - " 39.8\n", - " 19.1\n", - " 184.0\n", - " 4650.0\n", + " Biscoe\n", + " 40.1\n", + " 18.9\n", + " 188.0\n", + " 4300.0\n", " MALE\n", " \n", " \n", - " 2\n", - " Dream\n", - " 40.9\n", - " 18.9\n", - " 184.0\n", - " 3900.0\n", + " 1\n", + " Torgersen\n", + " 39.1\n", + " 18.7\n", + " 181.0\n", + " 3750.0\n", " MALE\n", " \n", " \n", " 4\n", - " Dream\n", - " 37.3\n", - " 16.8\n", - " 192.0\n", - " 3000.0\n", - " FEMALE\n", - " \n", - " \n", - " 5\n", - " Dream\n", + " Biscoe\n", " 43.2\n", - " 18.5\n", - " 192.0\n", - " 4100.0\n", - " MALE\n", - " \n", - " \n", - " 9\n", - " Dream\n", - " 40.2\n", - " 20.1\n", - " 200.0\n", - " 3975.0\n", + " 19.0\n", + " 197.0\n", + " 4775.0\n", " MALE\n", " \n", " \n", - " 10\n", - " Dream\n", - " 40.8\n", - " 18.9\n", - " 208.0\n", - " 4300.0\n", + " 6\n", + " Biscoe\n", + " 41.3\n", + " 21.1\n", + " 195.0\n", + " 4400.0\n", " MALE\n", " \n", " \n", " 11\n", " Dream\n", - " 39.0\n", - " 18.7\n", - " 185.0\n", - " 3650.0\n", - " MALE\n", - " \n", - " \n", - " 12\n", - " Dream\n", - " 37.0\n", - " 16.9\n", - " 185.0\n", - " 3000.0\n", + " 38.1\n", + " 18.6\n", + " 190.0\n", + " 3700.0\n", " FEMALE\n", " \n", " \n", - " 14\n", - " Dream\n", - " 34.0\n", - " 17.1\n", - " 185.0\n", - " 3400.0\n", - " FEMALE\n", + " 13\n", + " Biscoe\n", + " 37.8\n", + " 20.0\n", + " 190.0\n", + " 4250.0\n", + " MALE\n", " \n", " \n", - " 15\n", - " Dream\n", - " 37.0\n", - " 16.5\n", - " 185.0\n", - " 3400.0\n", + " 14\n", + " Biscoe\n", + " 35.0\n", + " 17.9\n", + " 190.0\n", + " 3450.0\n", " FEMALE\n", " \n", " \n", - " 18\n", - " Dream\n", - " 39.7\n", - " 17.9\n", - " 193.0\n", - " 4250.0\n", + " 16\n", + " Torgersen\n", + " 34.6\n", + " 21.1\n", + " 198.0\n", + " 4400.0\n", " MALE\n", " \n", " \n", " 19\n", " Dream\n", - " 37.8\n", + " 37.2\n", " 18.1\n", - " 193.0\n", - " 3750.0\n", + " 178.0\n", + " 3900.0\n", " MALE\n", " \n", " \n", - " 22\n", - " Dream\n", - " 40.2\n", - " 17.1\n", - " 193.0\n", - " 3400.0\n", + " 21\n", + " Biscoe\n", + " 40.5\n", + " 17.9\n", + " 187.0\n", + " 3200.0\n", " FEMALE\n", " \n", " \n", " 23\n", " Dream\n", - " 36.8\n", + " 42.2\n", " 18.5\n", - " 193.0\n", - " 3500.0\n", + " 180.0\n", + " 3550.0\n", " FEMALE\n", " \n", " \n", - " 26\n", + " 30\n", " Dream\n", - " 41.5\n", - " 18.5\n", - " 201.0\n", - " 4000.0\n", + " 39.2\n", + " 21.1\n", + " 196.0\n", + " 4150.0\n", " MALE\n", " \n", " \n", - " 31\n", - " Dream\n", - " 33.1\n", - " 16.1\n", - " 178.0\n", - " 2900.0\n", - " FEMALE\n", + " 32\n", + " Torgersen\n", + " 42.9\n", + " 17.6\n", + " 196.0\n", + " 4700.0\n", + " MALE\n", " \n", " \n", - " 32\n", + " 38\n", " Dream\n", - " 37.2\n", - " 18.1\n", - " 178.0\n", + " 41.1\n", + " 17.5\n", + " 190.0\n", " 3900.0\n", " MALE\n", " \n", " \n", - " 33\n", - " Dream\n", - " 39.5\n", - " 16.7\n", - " 178.0\n", - " 3250.0\n", + " 40\n", + " Torgersen\n", + " 38.6\n", + " 21.2\n", + " 191.0\n", + " 3800.0\n", + " MALE\n", + " \n", + " \n", + " 42\n", + " Biscoe\n", + " 35.5\n", + " 16.2\n", + " 195.0\n", + " 3350.0\n", " FEMALE\n", " \n", " \n", - " 35\n", + " 44\n", " Dream\n", - " 36.0\n", - " 18.5\n", + " 39.2\n", + " 18.6\n", + " 190.0\n", + " 4250.0\n", + " MALE\n", + " \n", + " \n", + " 45\n", + " Torgersen\n", + " 35.2\n", + " 15.9\n", " 186.0\n", - " 3100.0\n", + " 3050.0\n", " FEMALE\n", " \n", " \n", - " 36\n", + " 46\n", " Dream\n", + " 43.2\n", + " 18.5\n", + " 192.0\n", + " 4100.0\n", + " MALE\n", + " \n", + " \n", + " 49\n", + " Biscoe\n", " 39.6\n", - " 18.1\n", + " 17.7\n", " 186.0\n", - " 4450.0\n", - " MALE\n", + " 3500.0\n", + " FEMALE\n", " \n", " \n", - " 38\n", - " Dream\n", - " 41.3\n", + " 53\n", + " Biscoe\n", + " 45.6\n", " 20.3\n", - " 194.0\n", - " 3550.0\n", + " 191.0\n", + " 4600.0\n", " MALE\n", " \n", " \n", - " 41\n", - " Dream\n", - " 35.7\n", - " 18.0\n", - " 202.0\n", - " 3550.0\n", + " 58\n", + " Torgersen\n", + " 40.9\n", + " 16.8\n", + " 191.0\n", + " 3700.0\n", " FEMALE\n", " \n", " \n", - " 51\n", - " Dream\n", - " 38.1\n", - " 17.6\n", - " 187.0\n", - " 3425.0\n", + " 60\n", + " Torgersen\n", + " 40.3\n", + " 18.0\n", + " 195.0\n", + " 3250.0\n", " FEMALE\n", " \n", " \n", - " 53\n", + " 62\n", " Dream\n", " 36.0\n", - " 17.1\n", - " 187.0\n", - " 3700.0\n", + " 18.5\n", + " 186.0\n", + " 3100.0\n", " FEMALE\n", " \n", + " \n", + " 63\n", + " Torgersen\n", + " 39.3\n", + " 20.6\n", + " 190.0\n", + " 3650.0\n", + " MALE\n", + " \n", " \n", "\n", "

25 rows × 6 columns

\n", "[146 rows x 6 columns in total]" ], "text/plain": [ - " island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", - "0 Dream 36.6 18.4 184.0 3475.0 \n", - "1 Dream 39.8 19.1 184.0 4650.0 \n", - "2 Dream 40.9 18.9 184.0 3900.0 \n", - "4 Dream 37.3 16.8 192.0 3000.0 \n", - "5 Dream 43.2 18.5 192.0 4100.0 \n", - "9 Dream 40.2 20.1 200.0 3975.0 \n", - "10 Dream 40.8 18.9 208.0 4300.0 \n", - "11 Dream 39.0 18.7 185.0 3650.0 \n", - "12 Dream 37.0 16.9 185.0 3000.0 \n", - "14 Dream 34.0 17.1 185.0 3400.0 \n", - "15 Dream 37.0 16.5 185.0 3400.0 \n", - "18 Dream 39.7 17.9 193.0 4250.0 \n", - "19 Dream 37.8 18.1 193.0 3750.0 \n", - "22 Dream 40.2 17.1 193.0 3400.0 \n", - "23 Dream 36.8 18.5 193.0 3500.0 \n", - "26 Dream 41.5 18.5 201.0 4000.0 \n", - "31 Dream 33.1 16.1 178.0 2900.0 \n", - "32 Dream 37.2 18.1 178.0 3900.0 \n", - "33 Dream 39.5 16.7 178.0 3250.0 \n", - "35 Dream 36.0 18.5 186.0 3100.0 \n", - "36 Dream 39.6 18.1 186.0 4450.0 \n", - "38 Dream 41.3 20.3 194.0 3550.0 \n", - "41 Dream 35.7 18.0 202.0 3550.0 \n", - "51 Dream 38.1 17.6 187.0 3425.0 \n", - "53 Dream 36.0 17.1 187.0 3700.0 \n", + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "0 Biscoe 40.1 18.9 188.0 \n", + "1 Torgersen 39.1 18.7 181.0 \n", + "4 Biscoe 43.2 19.0 197.0 \n", + "6 Biscoe 41.3 21.1 195.0 \n", + "11 Dream 38.1 18.6 190.0 \n", + "13 Biscoe 37.8 20.0 190.0 \n", + "14 Biscoe 35.0 17.9 190.0 \n", + "16 Torgersen 34.6 21.1 198.0 \n", + "19 Dream 37.2 18.1 178.0 \n", + "21 Biscoe 40.5 17.9 187.0 \n", + "23 Dream 42.2 18.5 180.0 \n", + "30 Dream 39.2 21.1 196.0 \n", + "32 Torgersen 42.9 17.6 196.0 \n", + "38 Dream 41.1 17.5 190.0 \n", + "40 Torgersen 38.6 21.2 191.0 \n", + "42 Biscoe 35.5 16.2 195.0 \n", + "44 Dream 39.2 18.6 190.0 \n", + "45 Torgersen 35.2 15.9 186.0 \n", + "46 Dream 43.2 18.5 192.0 \n", + "49 Biscoe 39.6 17.7 186.0 \n", + "53 Biscoe 45.6 20.3 191.0 \n", + "58 Torgersen 40.9 16.8 191.0 \n", + "60 Torgersen 40.3 18.0 195.0 \n", + "62 Dream 36.0 18.5 186.0 \n", + "63 Torgersen 39.3 20.6 190.0 \n", "\n", - " sex \n", - "0 FEMALE \n", - "1 MALE \n", - "2 MALE \n", - "4 FEMALE \n", - "5 MALE \n", - "9 MALE \n", - "10 MALE \n", - "11 MALE \n", - "12 FEMALE \n", - "14 FEMALE \n", - "15 FEMALE \n", - "18 MALE \n", - "19 MALE \n", - "22 FEMALE \n", - "23 FEMALE \n", - "26 MALE \n", - "31 FEMALE \n", - "32 MALE \n", - "33 FEMALE \n", - "35 FEMALE \n", - "36 MALE \n", - "38 MALE \n", - "41 FEMALE \n", - "51 FEMALE \n", - "53 FEMALE \n", + " body_mass_g sex \n", + "0 4300.0 MALE \n", + "1 3750.0 MALE \n", + "4 4775.0 MALE \n", + "6 4400.0 MALE \n", + "11 3700.0 FEMALE \n", + "13 4250.0 MALE \n", + "14 3450.0 FEMALE \n", + "16 4400.0 MALE \n", + "19 3900.0 MALE \n", + "21 3200.0 FEMALE \n", + "23 3550.0 FEMALE \n", + "30 4150.0 MALE \n", + "32 4700.0 MALE \n", + "38 3900.0 MALE \n", + "40 3800.0 MALE \n", + "42 3350.0 FEMALE \n", + "44 4250.0 MALE \n", + "45 3050.0 FEMALE \n", + "46 4100.0 MALE \n", + "49 3500.0 FEMALE \n", + "53 4600.0 MALE \n", + "58 3700.0 FEMALE \n", + "60 3250.0 FEMALE \n", + "62 3100.0 FEMALE \n", + "63 3650.0 MALE \n", "...\n", "\n", "[146 rows x 6 columns]" ] }, - "execution_count": 21, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -828,7 +858,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -850,18 +880,40 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 28, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cf14ebed505a4a92b4c72f51c82efe55", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 43c8fdc2-0bc3-4607-a36d-5bee87c894d8 is DONE. 28.9 kB processed.
Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 97e0c84d-aa6a-4197-9377-740d973ea44d is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 726b9a5e-48a1-4ced-ac34-fa028dcb2bf4 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job dcef36e5-4bd6-40f8-88c6-72e84360533f is RUNNING. " ] }, "metadata": {}, @@ -873,7 +925,7 @@ "LinearRegression()" ] }, - "execution_count": 23, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -890,60 +942,16 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 29, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "66af192d9a784994b9d4a48a49c70721", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job 87895ee3-81d0-4267-8a50-ab00e04664a7 is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job 2e3a6603-9f0e-44ff-9086-2e14ad50bd25 is RUNNING. " ] }, "metadata": {}, @@ -951,13 +959,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "163d87d9a2274142b31f5aafa145357a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 5c71d3d9-0e1c-45bd-866f-1f98f056260d is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 357878f9-b705-4a03-aeeb-818a51873724 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -965,13 +971,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3d307bbbd60e431a8d5bbd2ef7c41e2b", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 890767f7-a83b-469a-9f3e-abd5667f8202 is DONE. 48 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 7d6c2e32-56e7-43ef-9b21-ccd2a25930ea is RUNNING. " ] }, "metadata": {}, @@ -1031,7 +1035,7 @@ "[1 rows x 6 columns]" ] }, - "execution_count": 24, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1043,32 +1047,16 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 30, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3966d9ee16b346cf943305112ce60fb6", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job a25c445d-9b60-4a8d-a325-1bfacd32bc8d is RUNNING. Open Job" + ], "text/plain": [ - "HTML(value='Query job b881b602-abfa-4c19-a385-2480b3e8b2bd is RUNNING. " ] }, "metadata": {}, @@ -1076,13 +1064,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2084e1cd66ba449081eda92350f72fd0", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 5af493aa-96f9-434f-a101-ec855f4de694 is DONE. 8 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 03249520-93d3-4b2e-8976-f49cc4efe520 is RUNNING. " ] }, "metadata": {}, @@ -1090,13 +1076,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "68049943e6ad477988b9e65a962ecdf2", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e2076bc3-3966-4c45-8265-c461756a7782 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 31094013-70ea-415f-8b96-85c1af7ee9c8 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -1104,13 +1088,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "341e6796def340cb9e0681ddeb40ff9d", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e9cdfca7-30f6-4e93-95fb-244896e7c2ab is DONE. 16 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 0e456f33-4cb7-45a0-88e6-29324175b5a6 is RUNNING. " ] }, "metadata": {}, @@ -1142,8 +1124,8 @@ " \n", " \n", " \n", - " 292\n", - " 3459.735118\n", + " 334\n", + " 5891.735118\n", " \n", " \n", "\n", @@ -1152,12 +1134,12 @@ ], "text/plain": [ " predicted_body_mass_g\n", - "292 3459.735118\n", + "334 5891.735118\n", "\n", "[1 rows x 1 columns]" ] }, - "execution_count": 25, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -1177,18 +1159,16 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 31, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "256ff43296a9405f890e78511acc38e5", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Copy job cb4ef454-10df-4325-b9cb-6084df3ac9d5 is DONE. Open Job" + ], "text/plain": [ - "HTML(value='Copy job 1a273ccd-212a-4750-a3c1-615256af6d48 is RUNNING. " ] }, "metadata": {}, @@ -1200,14 +1180,14 @@ "LinearRegression(optimize_strategy='NORMAL_EQUATION')" ] }, - "execution_count": 26, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# save the model to a permanent location in BigQuery, so we can use it in future sessions (and elsewhere in BQ)\n", - "model.to_gbq(\"bigframes-dev.bqml_tutorial.penguins_model\", replace=True)" + "model.to_gbq(penguins_model, replace=True)" ] }, { @@ -1219,7 +1199,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -1228,7 +1208,7 @@ "LinearRegression(optimize_strategy='NORMAL_EQUATION')" ] }, - "execution_count": 27, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -1236,7 +1216,7 @@ "source": [ "# WARNING - until b/281709360 is fixed & pipeline is updated, pipelines will load as models,\n", "# and details of their transform steps will be lost (the loaded model will behave the same)\n", - "bigframes.pandas.read_gbq_model(\"bigframes-dev.bqml_tutorial.penguins_model\")" + "bigframes.pandas.read_gbq_model(penguins_model)" ] } ], diff --git a/owlbot.py b/owlbot.py index 082970018dc..dc84de7d8ff 100644 --- a/owlbot.py +++ b/owlbot.py @@ -46,6 +46,7 @@ "noxfile.py", ".pre-commit-config.yaml", "README.rst", + "CONTRIBUTING.rst", ".github/release-trigger.yml", # BigQuery DataFrames manages its own Kokoro cluster for presubmit & continuous tests. ".kokoro/build.sh", diff --git a/samples/snippets/gen_ai_model_test.py b/samples/snippets/gen_ai_model_test.py index 7cbc90d4c03..e4bead0e463 100644 --- a/samples/snippets/gen_ai_model_test.py +++ b/samples/snippets/gen_ai_model_test.py @@ -14,9 +14,14 @@ def test_llm_model(): - PROJECT_ID = "bigframes-dev" + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") REGION = "us" - CONN_NAME = "bigframes-ml" + CONN_NAME = "bigframes-default-connection" + # [START bigquery_dataframes_gen_ai_model] from bigframes.ml.llm import PaLM2TextGenerator import bigframes.pandas as bpd diff --git a/samples/snippets/load_data_from_biquery_job_test.py b/samples/snippets/load_data_from_biquery_job_test.py index 5271574a492..9a7793a7e53 100644 --- a/samples/snippets/load_data_from_biquery_job_test.py +++ b/samples/snippets/load_data_from_biquery_job_test.py @@ -14,10 +14,16 @@ def test_bigquery_dataframes_load_data_from_bigquery_job(): - from google.cloud import bigquery + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + your_project_id = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") # Construct a BigQuery client object. - client = bigquery.Client(project="bigframes-dev", location="us") + from google.cloud import bigquery + + client = bigquery.Client(project=your_project_id, location="us") query = """ SELECT * @@ -26,7 +32,6 @@ def test_bigquery_dataframes_load_data_from_bigquery_job(): """ query_job = client.query(query) JOB_ID = query_job.job_id - your_project_id = "bigframes-dev" # [START bigquery_dataframes_load_data_from_bigquery_job] from google.cloud import bigquery diff --git a/samples/snippets/quickstart_test.py b/samples/snippets/quickstart_test.py index bbe4a8b3c49..4abc87d0118 100644 --- a/samples/snippets/quickstart_test.py +++ b/samples/snippets/quickstart_test.py @@ -25,7 +25,12 @@ def test_quickstart( # We need a fresh session since we're modifying connection options. bigframes.pandas.close_session() - # TODO(swast): Get project from environment so contributors can run tests. - quickstart.run_quickstart("bigframes-dev") + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + your_project_id = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") + + quickstart.run_quickstart(your_project_id) out, _ = capsys.readouterr() assert "average_body_mass (df_session):" in out diff --git a/samples/snippets/remote_function_test.py b/samples/snippets/remote_function_test.py index e1317c6ac08..8f891274dec 100644 --- a/samples/snippets/remote_function_test.py +++ b/samples/snippets/remote_function_test.py @@ -25,8 +25,13 @@ def test_remote_function_and_read_gbq_function( # We need a fresh session since we're modifying connection options. bigframes.pandas.close_session() - # TODO(swast): Get project from environment so contributors can run tests. - remote_function.run_remote_function_and_read_gbq_function("bigframes-dev") + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + your_project_id = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") + + remote_function.run_remote_function_and_read_gbq_function(your_project_id) out, _ = capsys.readouterr() assert "Created BQ remote function:" in out assert "Created cloud function:" in out diff --git a/scripts/create_test_model_vertex.py b/scripts/create_test_model_vertex.py new file mode 100644 index 00000000000..946e54773e6 --- /dev/null +++ b/scripts/create_test_model_vertex.py @@ -0,0 +1,71 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import sys + +import bigframes.ml.linear_model +import bigframes.pandas + + +def create_vertex_model(vertex_model_name): + df = bigframes.pandas.read_gbq("bigquery-public-data.ml_datasets.penguins") + + # filter down to the data we want to analyze + adelie_data = df[df.species == "Adelie Penguin (Pygoscelis adeliae)"] + + # drop the columns we don't care about + adelie_data = adelie_data.drop(columns=["species"]) + + # drop rows with nulls to get our training data + training_data = adelie_data.dropna() + + feature_columns = training_data["culmen_length_mm"] + label_columns = training_data[["body_mass_g"]] + + # create model + model = bigframes.ml.linear_model.LinearRegression() + model.fit(feature_columns, label_columns) + + # register to Vertex Registry + model.register(vertex_model_name) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Get top APIs for which there are no code samples in the docstring." + ) + parser.add_argument( + "-m", + "--model-name", + type=str, + required=True, + action="store", + help="Name of the model in Vertex.", + ) + parser.add_argument( + "-p", + "--project-id", + type=str, + required=False, + action="store", + help="Project id in which the model should be created. " + "By default, a project will be resolved as per https://cloud.google.com/python/docs/reference/google-cloud-core/latest/config#overview.", + ) + + args = parser.parse_args(sys.argv[1:]) + if args.project_id: + bigframes.pandas.options.bigquery.project = args.project_id + + create_vertex_model(args.model_name) diff --git a/scripts/setup-project-for-testing.sh b/scripts/setup-project-for-testing.sh new file mode 100755 index 00000000000..a160784c120 --- /dev/null +++ b/scripts/setup-project-for-testing.sh @@ -0,0 +1,256 @@ +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +if [ $# -lt 1 ]; then + echo "USAGE: `basename $0` []" + echo "EXAMPLES:" + echo " `basename $0` my-project" + echo " `basename $0` my-project user:user_id@example.com" + echo " `basename $0` my-project group:group_id@example.com" + echo " `basename $0` my-project serviceAccount:service_account_id@example.com" + exit 1 +fi + +PROJECT_ID=$1 +PRINCIPAL=$2 +BIGFRAMES_DEFAULT_CONNECTION_NAME=bigframes-default-connection +BIGFRAMES_RF_CONNECTION_NAME=bigframes-rf-conn + +if [ "$PRINCIPAL" != "" ]; then + echo $PRINCIPAL | grep -E "(user|group|serviceAccount):" >/dev/null + if [ $? -ne 0 ]; then + echo "principal must have prefix 'user:', 'group:' or 'serviceAccount:'" + exit 1 + fi +fi + +if ! test `which gcloud`; then + echo "gcloud CLI is not installed. Install it from https://cloud.google.com/sdk/docs/install." >&2 + exit 1 +fi + +################################################################################ +# Log and execute a command +################################################################################ +function log_and_execute() { + echo Running command: $* + $* +} + + +################################################################################ +# Enable APIs +################################################################################ +function enable_apis() { + for service in aiplatform.googleapis.com \ + bigquery.googleapis.com \ + bigqueryconnection.googleapis.com \ + bigquerystorage.googleapis.com \ + cloudbuild.googleapis.com \ + cloudfunctions.googleapis.com \ + cloudresourcemanager.googleapis.com \ + run.googleapis.com \ + ; do + log_and_execute gcloud --project=$PROJECT_ID services enable $service + if [ $? -ne 0 ]; then + echo "Failed to enable service $service, exiting..." + exit 1 + fi + done +} + + +################################################################################ +# Ensure a BQ connection exists with desired IAM rols +################################################################################ +function ensure_bq_connection_with_iam() { + if [ $# -ne 2 ]; then + echo "USAGE: `basename $0` " + echo "EXAMPLES:" + echo " `basename $0` my-project my-connection" + exit 1 + fi + + location=$1 + connection_name=$2 + + log_and_execute bq show \ + --connection \ + --project_id=$PROJECT_ID \ + --location=$location \ + $connection_name 2>&1 >/dev/null + if [ $? -ne 0 ]; then + echo "Connection $connection_name doesn't exists in location \"$location\", creating..." + log_and_execute bq mk \ + --connection \ + --project_id=$PROJECT_ID \ + --location=$location \ + --connection_type=CLOUD_RESOURCE \ + $connection_name + if [ $? -ne 0 ]; then + echo "Failed creating connection, exiting." + exit 1 + fi + else + echo "Connection $connection_name already exists in location $location." + fi + + compact_json_info_cmd="bq show --connection \ + --project_id=$PROJECT_ID \ + --location=$location \ + --format=json \ + $connection_name" + compact_json_info_cmd_output=`$compact_json_info_cmd` + if [ $? -ne 0 ]; then + echo "Failed to fetch connection info: $compact_json_info_cmd_output" + exit 1 + fi + + connection_service_account=`echo $compact_json_info_cmd_output | sed -e 's/.*"cloudResource":{"serviceAccountId":"//' -e 's/".*//'` + + # Configure roles for the service accounts associated with the connection + for role in run.invoker aiplatform.user; do + log_and_execute gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member=serviceAccount:$connection_service_account \ + --role=roles/$role + if [ $? -ne 0 ]; then + echo "Failed to set IAM, exiting..." + exit 1 + fi + done +} + + +################################################################################ +# Create the default BQ connection in US location +################################################################################ +function ensure_bq_connections_with_iam() { + ensure_bq_connection_with_iam "us" "$BIGFRAMES_DEFAULT_CONNECTION_NAME" + + # Create commonly used BQ connection in various locations + for location in asia-southeast1 \ + eu \ + europe-west4 \ + southamerica-west1 \ + us \ + us-central1 \ + ; do + ensure_bq_connection_with_iam "$location" "$BIGFRAMES_RF_CONNECTION_NAME" + done +} + + +################################################################################ +# Set up IAM roles for principal +################################################################################ +function setup_iam_roles () { + if [ "$PRINCIPAL" != "" ]; then + for role in aiplatform.user \ + bigquery.user \ + bigquery.connectionAdmin \ + bigquery.dataEditor \ + browser \ + cloudfunctions.developer \ + iam.serviceAccountUser \ + ; do + log_and_execute gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member=$PRINCIPAL \ + --role=roles/$role + if [ $? -ne 0 ]; then + echo "Failed to set IAM, exiting..." + exit 1 + fi + done + fi +} + + +################################################################################ +# Create vertex endpoint for test ML model +################################################################################ +function create_bq_model_vertex_endpoint () { + vertex_region=us-central1 + model_name=bigframes-test-linreg2 + endpoint_name=$model_name-endpoint + + # Create vertex model + log_and_execute python scripts/create_test_model_vertex.py \ + -m $model_name \ + -p $PROJECT_ID + if [ $? -ne 0 ]; then + echo "Failed to create model, exiting..." + exit 1 + fi + + # Create vertex endpoint + log_and_execute gcloud ai endpoints create \ + --project=$PROJECT_ID \ + --region=$vertex_region \ + --display-name=$endpoint_name + if [ $? -ne 0 ]; then + echo "Failed to create vertex endpoint, exiting..." + exit 1 + fi + + # Fetch endpoint id + endpoint_id=`gcloud ai endpoints list \ + --project=$PROJECT_ID \ + --region=$vertex_region \ + --filter=display_name=$endpoint_name 2>/dev/null \ + | tail -n1 | cut -d' ' -f 1` + if [ "$endpoint_id" = "" ]; then + echo "Failed to fetch vertex endpoint id, exiting..." + exit 1 + fi + + # Deploy the model to the vertex endpoint + log_and_execute gcloud ai endpoints deploy-model $endpoint_id \ + --project=$PROJECT_ID \ + --region=$vertex_region \ + --model=$model_name \ + --display-name=$model_name + if [ $? -ne 0 ]; then + echo "Failed to deploy model to vertex endpoint, exiting..." + exit 1 + fi + + # Form the endpoint + endpoint_rel_path=`gcloud ai endpoints describe \ + --project=$PROJECT_ID \ + --region=us-central1 \ + $endpoint_id 2>/dev/null \ + | grep "^name:" | cut -d' ' -f2` + if [ "$endpoint_rel_path" = "" ]; then + echo "Failed to fetch vertex endpoint relativr path, exiting..." + exit 1 + fi + endpoint_path=https://$vertex_region-aiplatform.googleapis.com/v1/$endpoint_rel_path + + # Print the endpoint configuration to be used in tests + echo + echo Run following command to set test model vertex endpoint: + echo export BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT=$endpoint_path +} + + +################################################################################ +# Set the things up +################################################################################ +enable_apis +ensure_bq_connections_with_iam +setup_iam_roles +create_bq_model_vertex_endpoint diff --git a/tests/system/conftest.py b/tests/system/conftest.py index f9f69c6c8ee..0ad4280497f 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -120,7 +120,6 @@ def session() -> bigframes.Session: def session_tokyo(tokyo_location: str) -> bigframes.Session: context = bigframes.BigQueryOptions( location=tokyo_location, - use_regional_endpoints=True, ) return bigframes.Session(context=context) diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index a7049d4c18e..953287def2b 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -15,6 +15,7 @@ import pandas as pd from bigframes.ml import decomposition +import tests.system.utils def test_decomposition_configure_fit_score_predict( @@ -66,9 +67,10 @@ def test_decomposition_configure_fit_score_predict( dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal( - abs(result.sort_index()), # results may differ by a minus sign - abs(expected), + + tests.system.utils.assert_pandas_df_equal_pca( + result, + expected, check_exact=False, rtol=0.1, ) diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 2929baf3f7b..c128469bd20 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -24,7 +24,7 @@ pipeline, preprocessing, ) -from tests.system.utils import assert_pandas_df_equal +from tests.system.utils import assert_pandas_df_equal, assert_pandas_df_equal_pca def test_pipeline_linear_regression_fit_score_predict( @@ -430,17 +430,16 @@ def test_pipeline_PCA_fit_score_predict(session, penguins_df_default_index): dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal( - abs( # results may differ by a minus sign - predictions[ - [ - "principal_component_1", - "principal_component_2", - "principal_component_3", - ] + + assert_pandas_df_equal_pca( + predictions[ + [ + "principal_component_1", + "principal_component_2", + "principal_component_3", ] - ), - abs(expected), + ], + expected, check_exact=False, rtol=0.1, ) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 5cb4df188ca..4b4c794a050 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -161,8 +161,10 @@ def make_uniq_udf(udf): @pytest.fixture(scope="module") def bq_cf_connection() -> str: - """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn + """Pre-created BQ connection in the test project in US location, used to + invoke cloud function. + + $ bq show --connection --location=us --project_id=PROJECT_ID bigframes-rf-conn """ return "bigframes-rf-conn" diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index c4a1272e441..e3180d28921 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from typing import cast import uuid @@ -34,8 +35,8 @@ @pytest.fixture(scope="session") -def bq_connection() -> str: - return "bigframes-dev.us.bigframes-rf-conn" +def bq_connection(bigquery_client) -> str: + return f"{bigquery_client.project}.us.bigframes-rf-conn" @pytest.fixture(scope="session") @@ -252,10 +253,15 @@ def palm2_embedding_generator_multilingual_model( def linear_remote_model_params() -> dict: # Pre-deployed endpoint of linear reg model in Vertex. # bigframes-test-linreg2 -> bigframes-test-linreg-endpoint2 + model_vertex_endpoint = os.environ.get( + "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT", + "https://us-central1-aiplatform.googleapis.com/v1/projects/1084210331973/locations/us-central1/endpoints/3193318217619603456", + ) + return { "input": {"culmen_length_mm": "float64"}, "output": {"predicted_body_mass_g": "array"}, - "endpoint": "https://us-central1-aiplatform.googleapis.com/v1/projects/1084210331973/locations/us-central1/endpoints/3193318217619603456", + "endpoint": model_vertex_endpoint, } diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 915c4aa444a..eece5ef21d3 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -210,12 +210,12 @@ def test_pca_model_principal_components(penguins_bqml_pca_model: core.BqmlModel) .sort_values(["principal_component_id", "feature"]) .reset_index(drop=True) ) - pd.testing.assert_frame_equal( + + tests.system.utils.assert_pandas_df_equal_pca_components( result, expected, check_exact=False, rtol=0.1, - # int64 Index by default in pandas versus Int64 (nullable) Index in BigQuery DataFrame check_index_type=False, check_dtype=False, ) diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index 42fea66cf86..9565b8f7a85 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -29,11 +29,9 @@ def test_pca_predict(penguins_pca_model, new_penguins_df): dtype="Float64", index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal( - predictions.sort_index(), - expected, - check_exact=False, - rtol=0.1, + + tests.system.utils.assert_pandas_df_equal_pca( + predictions, expected, check_exact=False, rtol=0.1 ) @@ -115,7 +113,8 @@ def test_pca_components_(penguins_pca_model: decomposition.PCA): .sort_values(["principal_component_id", "feature"]) .reset_index(drop=True) ) - pd.testing.assert_frame_equal( + + tests.system.utils.assert_pandas_df_equal_pca_components( result, expected, check_exact=False, diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 306098548ec..267a2ed9c18 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -24,14 +24,10 @@ def test_create_text_generator_model(palm2_text_generator_model): assert palm2_text_generator_model._bqml_model is not None -def test_create_text_generator_32k_model(palm2_text_generator_32k_model): - # Model creation doesn't return error - assert palm2_text_generator_32k_model is not None - assert palm2_text_generator_32k_model._bqml_model is not None - - @pytest.mark.flaky(retries=2, delay=120) -def test_create_text_generator_model_default_session(bq_connection, llm_text_pandas_df): +def test_create_text_generator_model_default_session( + bq_connection, llm_text_pandas_df, bigquery_client +): import bigframes.pandas as bpd bpd.close_session() @@ -41,7 +37,10 @@ def test_create_text_generator_model_default_session(bq_connection, llm_text_pan model = llm.PaLM2TextGenerator() assert model is not None assert model._bqml_model is not None - assert model.connection_name.casefold() == "bigframes-dev.us.bigframes-rf-conn" + assert ( + model.connection_name.casefold() + == f"{bigquery_client.project}.us.bigframes-rf-conn" + ) llm_text_df = bpd.read_pandas(llm_text_pandas_df) @@ -54,7 +53,7 @@ def test_create_text_generator_model_default_session(bq_connection, llm_text_pan @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_32k_model_default_session( - bq_connection, llm_text_pandas_df + bq_connection, llm_text_pandas_df, bigquery_client ): import bigframes.pandas as bpd @@ -65,7 +64,10 @@ def test_create_text_generator_32k_model_default_session( model = llm.PaLM2TextGenerator(model_name="text-bison-32k") assert model is not None assert model._bqml_model is not None - assert model.connection_name.casefold() == "bigframes-dev.us.bigframes-rf-conn" + assert ( + model.connection_name.casefold() + == f"{bigquery_client.project}.us.bigframes-rf-conn" + ) llm_text_df = bpd.read_pandas(llm_text_pandas_df) @@ -77,7 +79,9 @@ def test_create_text_generator_32k_model_default_session( @pytest.mark.flaky(retries=2, delay=120) -def test_create_text_generator_model_default_connection(llm_text_pandas_df): +def test_create_text_generator_model_default_connection( + llm_text_pandas_df, bigquery_client +): from bigframes import _config import bigframes.pandas as bpd @@ -91,7 +95,7 @@ def test_create_text_generator_model_default_connection(llm_text_pandas_df): assert model._bqml_model is not None assert ( model.connection_name.casefold() - == "bigframes-dev.us.bigframes-default-connection" + == f"{bigquery_client.project}.us.bigframes-default-connection" ) df = model.predict(llm_text_df).to_pandas() diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 960a3841261..a98056d82af 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -23,40 +23,50 @@ @pytest.fixture(scope="module") def bq_cf_connection() -> str: - """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn + """Pre-created BQ connection in the test project in US location, used to + invoke cloud function. + + $ bq show --connection --location=us --project_id=PROJECT_ID bigframes-rf-conn """ return "bigframes-rf-conn" @pytest.fixture(scope="module") def bq_cf_connection_location() -> str: - """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn + """Pre-created BQ connection in the test project in US location, in format + PROJECT_ID.LOCATION.CONNECTION_NAME, used to invoke cloud function. + + $ bq show --connection --location=us --project_id=PROJECT_ID bigframes-rf-conn """ return "us.bigframes-rf-conn" @pytest.fixture(scope="module") def bq_cf_connection_location_mismatched() -> str: - """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=eu --project_id=bigframes-dev bigframes-rf-conn + """Pre-created BQ connection in the test project in EU location, in format + LOCATION.CONNECTION_NAME, used to invoke cloud function. + + $ bq show --connection --location=us --project_id=PROJECT_ID bigframes-rf-conn """ return "eu.bigframes-rf-conn" @pytest.fixture(scope="module") -def bq_cf_connection_location_project() -> str: - """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=us --project_id=bigframes-dev bigframes-rf-conn +def bq_cf_connection_location_project(bigquery_client) -> str: + """Pre-created BQ connection in the test project in US location, in format + PROJECT_ID.LOCATION.CONNECTION_NAME, used to invoke cloud function. + + $ bq show --connection --location=us --project_id=PROJECT_ID bigframes-rf-conn """ - return "bigframes-dev.us.bigframes-rf-conn" + return f"{bigquery_client.project}.us.bigframes-rf-conn" @pytest.fixture(scope="module") def bq_cf_connection_location_project_mismatched() -> str: - """Pre-created BQ connection to invoke cloud function for bigframes-dev - $ bq show --connection --location=eu --project_id=bigframes-metrics bigframes-rf-conn + """Pre-created BQ connection in the migframes-metrics project in US location, + in format PROJECT_ID.LOCATION.CONNECTION_NAME, used to invoke cloud function. + + $ bq show --connection --location=us --project_id=PROJECT_ID bigframes-rf-conn """ return "bigframes-metrics.eu.bigframes-rf-conn" diff --git a/tests/system/utils.py b/tests/system/utils.py index f7831972b83..f49b5ece313 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -143,3 +143,77 @@ def convert_pandas_dtypes(df: pd.DataFrame, bytes_col: bool): df["numeric_col"] = df["numeric_col"].apply( lambda value: decimal.Decimal(str(value)) if value else None # type: ignore ) + + +def assert_pandas_df_equal_pca_components(actual, expected, **kwargs): + """Compare two pandas dataframes representing PCA components. The columns + required to be present in the dataframes are: + numerical_value: numeric, + categorical_value: List[object(category, value)] + + The index types of `actual` and `expected` are ignored in the comparison. + + Args: + actual: Actual Pandas DataFrame + + expected: Expected Pandas DataFrame + + kwargs: kwargs to use in `pandas.testing.assert_series_equal` per column + """ + # Compare the index, columns and values separately, as the polarity of the + # PCA vectors can be arbitrary + pd.testing.assert_index_equal( + actual.index, expected.index.astype(actual.index.dtype) + ) # dtype agnostic index comparison + pd.testing.assert_index_equal(actual.columns, expected.columns) + for column in expected.columns: + try: + pd.testing.assert_series_equal(actual[column], expected[column], **kwargs) + except AssertionError: + if column not in {"numerical_value", "categorical_value"}: + raise + + # Allow for sign difference per numeric/categorical column + if column == "numerical_value": + actual_ = -actual[column] + expected_ = expected[column] + else: + # In this column each element is an array of objects, where the + # object has attributes "category" and "value". For the sake of + # comparison let's normalize by flipping the polarity of "value". + def normalize_array_of_objects(arr, reverse_polarity=False): + newarr = [] + for element in arr: + newelement = dict(element) + if reverse_polarity: + newelement["value"] = -newelement["value"] + newarr.append(newelement) + return sorted(newarr, key=lambda d: d["category"]) + + actual_ = actual[column].apply(normalize_array_of_objects, args=(True,)) + expected_ = expected[column].apply(normalize_array_of_objects) + + pd.testing.assert_series_equal(actual_, expected_, **kwargs) + + +def assert_pandas_df_equal_pca(actual, expected, **kwargs): + """Compare two pandas dataframes representing PCA predictions. The columns + in the dataframes are expected to be numeric. + + Args: + actual: Actual Pandas DataFrame + + expected: Expected Pandas DataFrame + + kwargs: kwargs to use in `pandas.testing.assert_series_equal` per column + """ + # Compare the index, columns and values separately, as the polarity of the + # PCA vector can be arbitrary + pd.testing.assert_index_equal(actual.index, expected.index) + pd.testing.assert_index_equal(actual.columns, expected.columns) + for column in expected.columns: + try: + pd.testing.assert_series_equal(actual[column], expected[column], **kwargs) + except AssertionError: + # Allow for sign difference per column + pd.testing.assert_series_equal(-actual[column], expected[column], **kwargs) From 9cde708bb4a94d3ba35ecdf298cc80bc5680e7b4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 04:59:17 +0000 Subject: [PATCH 210/921] chore(main): release 0.16.0 (#250) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef75a017e01..68ea51707c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.16.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.15.0...v0.16.0) (2023-12-12) + + +### Features + +* Add ARIMAPlus.predict parameters ([#264](https://github.com/googleapis/python-bigquery-dataframes/issues/264)) ([99598c7](https://github.com/googleapis/python-bigquery-dataframes/commit/99598c7d359f1d1e0671dcf27a5c77094f3c7f67)) +* Add DataFrame from_dict and from_records methods ([#244](https://github.com/googleapis/python-bigquery-dataframes/issues/244)) ([8d81e24](https://github.com/googleapis/python-bigquery-dataframes/commit/8d81e24677613dcf4d275c27a327384b8c17bc85)) +* Add DataFrame.select_dtypes method ([#242](https://github.com/googleapis/python-bigquery-dataframes/issues/242)) ([1737acc](https://github.com/googleapis/python-bigquery-dataframes/commit/1737acc51b4fdd9b385bbf91a758efd2e7ead11a)) +* Add nunique method to Series/DataFrameGroupby ([#256](https://github.com/googleapis/python-bigquery-dataframes/issues/256)) ([c8ec245](https://github.com/googleapis/python-bigquery-dataframes/commit/c8ec245070402aa0770bc9b2375693de674ca925)) +* Support dataframe.loc with conditional columns selection ([#233](https://github.com/googleapis/python-bigquery-dataframes/issues/233)) ([3febea9](https://github.com/googleapis/python-bigquery-dataframes/commit/3febea99358d10f823d43c3af83ea30458e579a2)) + + +### Bug Fixes + +* Enfore pandas version requirement <2.1.4 ([#265](https://github.com/googleapis/python-bigquery-dataframes/issues/265)) ([9dd63f6](https://github.com/googleapis/python-bigquery-dataframes/commit/9dd63f6dcb6234e1f3aebd63c59e1e5c717099dc)) +* Exclude pandas 2.1.4 from prerelease tests to unblock e2e tests ([b02fc2c](https://github.com/googleapis/python-bigquery-dataframes/commit/b02fc2c1843e18d3a8d6894c64763f53e6af1b73)) +* Fix value_counts column label for normalize=True ([#245](https://github.com/googleapis/python-bigquery-dataframes/issues/245)) ([d3fa6f2](https://github.com/googleapis/python-bigquery-dataframes/commit/d3fa6f26931d5d0f0ae3fa49baccfc148f870417)) +* Migrate e2e tests to bigframes-load-testing project ([8766ac6](https://github.com/googleapis/python-bigquery-dataframes/commit/8766ac63f501929577f71e6bd2b523e92c43ba66)) +* Ml.sql logic ([#262](https://github.com/googleapis/python-bigquery-dataframes/issues/262)) ([68c6fdf](https://github.com/googleapis/python-bigquery-dataframes/commit/68c6fdf78af8b87fa4ef4f832631f24d7433a4d8)) +* Update the llm_kmeans notebook ([#247](https://github.com/googleapis/python-bigquery-dataframes/issues/247)) ([66d1839](https://github.com/googleapis/python-bigquery-dataframes/commit/66d1839c3e9a3011c7feb13a59d966b64cf8313f)) + + +### Documentation + +* Add code samples for `shape` and `head` ([#257](https://github.com/googleapis/python-bigquery-dataframes/issues/257)) ([5bdcc65](https://github.com/googleapis/python-bigquery-dataframes/commit/5bdcc6594ef2e99e96636341d286ea70420858fe)) +* Add example for dataframe.melt, dataframe.pivot, dataframe.stac… ([#252](https://github.com/googleapis/python-bigquery-dataframes/issues/252)) ([8c63697](https://github.com/googleapis/python-bigquery-dataframes/commit/8c636978f4a21eda2856862100b7a8272797fe42)) +* Add example to dataframe.nlargest, dataframe.nsmallest, datafra… ([#234](https://github.com/googleapis/python-bigquery-dataframes/issues/234)) ([e735412](https://github.com/googleapis/python-bigquery-dataframes/commit/e735412fdc52d034df92dd5462d6956bdc0167be)) +* Add examples for dataframe.cummin, dataframe.cummax, dataframe.cumsum, dataframe.cumprod ([#243](https://github.com/googleapis/python-bigquery-dataframes/issues/243)) ([0523a31](https://github.com/googleapis/python-bigquery-dataframes/commit/0523a31fa0b589f88afe0ad5b447634409ddeb86)) +* Add examples for dataframe.nunique, dataframe.diff, dataframe.a… ([#251](https://github.com/googleapis/python-bigquery-dataframes/issues/251)) ([77074ec](https://github.com/googleapis/python-bigquery-dataframes/commit/77074ecbe7f52d1d7d1d1dc537fbe4062b407672)) +* Correct the docs for `option_context` ([#263](https://github.com/googleapis/python-bigquery-dataframes/issues/263)) ([d21c6dd](https://github.com/googleapis/python-bigquery-dataframes/commit/d21c6dd26eadd64c526b0fd35b977a74b8334562)) +* Correct the params rendering for `ml.remote` and `ml.ensemble` modules ([#248](https://github.com/googleapis/python-bigquery-dataframes/issues/248)) ([c2829e3](https://github.com/googleapis/python-bigquery-dataframes/commit/c2829e3d976a43c53251c9288266e3a8ec5304c5)) +* Fix return annotation in API docstrings ([#253](https://github.com/googleapis/python-bigquery-dataframes/issues/253)) ([89a1c67](https://github.com/googleapis/python-bigquery-dataframes/commit/89a1c67fa5cbb76c1cc6ae24d5f919e22514705c)) + ## [0.15.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.14.1...v0.15.0) (2023-11-29) diff --git a/bigframes/version.py b/bigframes/version.py index 920cb95c3d2..3ddf7e0f79a 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.15.0" +__version__ = "0.16.0" From a133822974229f70529a414a682b6d98770d1846 Mon Sep 17 00:00:00 2001 From: Ataf Fazledin Ahamed Date: Wed, 13 Dec 2023 02:32:54 +0600 Subject: [PATCH 211/921] fix: replaced raise `NotImplementedError` with return `NotImplemented` (#258) Signed-off-by: fazledyn-or Co-authored-by: Tim Swast --- third_party/bigframes_vendored/pandas/core/series.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index e6af1648fd6..48bcca4ad8e 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -677,13 +677,13 @@ def __matmul__(self, other): """ Matrix multiplication using binary `@` operator in Python>=3.5. """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + return NotImplemented def __rmatmul__(self, other): """ Matrix multiplication using binary `@` operator in Python>=3.5. """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + return NotImplemented def sort_values( self, From fb14f54548e988c6c226753fcca162cf15b5c8d7 Mon Sep 17 00:00:00 2001 From: Stephanie A <129541811+DevStephanie@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:25:00 -0600 Subject: [PATCH 212/921] docs: add sample for getting started with BQML (#141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add sample for getting started with BQML * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Creating clarifying comments * Merging comments with this branch * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * corrections on comments * Correcting code comments from BQ docs * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Fixing code comments to reflect BQML documentation * Correcting code comments * Correcting documentation code * Correcting documentation errors * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Correcting documentation comments and correcting features * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Correcting documention comments for code samples * Apply suggestions from code review Correcting documentation comments Co-authored-by: Tim Swast * Correcting documentation comments * Correcting documentation comments * Apply suggestions from code review * Apply suggestions from code review * Fixtures for temporary resources * Deleting files --------- Co-authored-by: Your Name Co-authored-by: Owl Bot Co-authored-by: Tim Swast --- samples/snippets/bqml_getting_started_test.py | 93 +++++++++++++++++++ samples/snippets/conftest.py | 66 +++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 samples/snippets/bqml_getting_started_test.py create mode 100644 samples/snippets/conftest.py diff --git a/samples/snippets/bqml_getting_started_test.py b/samples/snippets/bqml_getting_started_test.py new file mode 100644 index 00000000000..783f963feb5 --- /dev/null +++ b/samples/snippets/bqml_getting_started_test.py @@ -0,0 +1,93 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_bqml_getting_started(random_model_id): + your_model_id = random_model_id + + # [START bigquery_dataframes_bqml_getting_started_tutorial] + from bigframes.ml.linear_model import LogisticRegression + import bigframes.pandas as bpd + + # Start by selecting the data you'll use for training. `read_gbq` accepts + # either a SQL query or a table ID. Since this example selects from multiple + # tables via a wildcard, use SQL to define this data. Watch issue + # https://github.com/googleapis/python-bigquery-dataframes/issues/169 + # for updates to `read_gbq` to support wildcard tables. + + df = bpd.read_gbq( + """ + -- Since the order of rows isn't useful for the model training, + -- generate a random ID to use as the index for the DataFrame. + SELECT GENERATE_UUID() AS rowindex, * + FROM + `bigquery-public-data.google_analytics_sample.ga_sessions_*` + WHERE + _TABLE_SUFFIX BETWEEN '20160801' AND '20170630' + """, + index_col="rowindex", + ) + + # Extract the total number of transactions within + # the Google Analytics session. + # + # Because the totals column is a STRUCT data type, call + # Series.struct.field("transactions") to extract the transactions field. + # See the reference documentation below: + # https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.operations.structs.StructAccessor#bigframes_operations_structs_StructAccessor_field + transactions = df["totals"].struct.field("transactions") + + # The "label" values represent the outcome of the model's + # prediction. In this case, the model predicts if there are any + # ecommerce transactions within the Google Analytics session. + # If the number of transactions is NULL, the value in the label + # column is set to 0. Otherwise, it is set to 1. + label = transactions.notnull().map({True: 1, False: 0}) + + # Extract the operating system of the visitor's device. + operatingSystem = df["device"].struct.field("operatingSystem") + operatingSystem = operatingSystem.fillna("") + + # Extract whether the visitor's device is a mobile device. + isMobile = df["device"].struct.field("isMobile") + + # Extract the country from which the sessions originated, based on the IP address. + country = df["geoNetwork"].struct.field("country").fillna("") + + # Extract the total number of page views within the session. + pageviews = df["totals"].struct.field("pageviews").fillna(0) + + # Combine all the feature columns into a single DataFrame + # to use as training data. + features = bpd.DataFrame( + { + "os": operatingSystem, + "is_mobile": isMobile, + "country": country, + "pageviews": pageviews, + } + ) + + # Logistic Regression model splits data into two classes, giving the + # a confidence score that the data is in one of the classes. + model = LogisticRegression() + model.fit(features, label) + + # The model.fit() call above created a temporary model. + # Use the to_gbq() method to write to a permanent location. + model.to_gbq( + your_model_id, # For example: "bqml_tutorial.sample_model", + replace=True, + ) + # [END bigquery_dataframes_bqml_getting_started_tutorial] diff --git a/samples/snippets/conftest.py b/samples/snippets/conftest.py new file mode 100644 index 00000000000..1ce54b3c0c8 --- /dev/null +++ b/samples/snippets/conftest.py @@ -0,0 +1,66 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Iterator + +from google.cloud import bigquery +import pytest +import test_utils.prefixer + +prefixer = test_utils.prefixer.Prefixer( + "python-bigquery-dataframes", "samples/snippets" +) + + +@pytest.fixture(scope="session", autouse=True) +def cleanup_datasets(bigquery_client: bigquery.Client) -> None: + for dataset in bigquery_client.list_datasets(): + if prefixer.should_cleanup(dataset.dataset_id): + bigquery_client.delete_dataset( + dataset, delete_contents=True, not_found_ok=True + ) + + +@pytest.fixture(scope="session") +def bigquery_client() -> bigquery.Client: + bigquery_client = bigquery.Client() + return bigquery_client + + +@pytest.fixture(scope="session") +def project_id(bigquery_client: bigquery.Client) -> str: + return bigquery_client.project + + +@pytest.fixture(scope="session") +def dataset_id(bigquery_client: bigquery.Client, project_id: str) -> Iterator[str]: + dataset_id = prefixer.create_prefix() + full_dataset_id = f"{project_id}.{dataset_id}" + dataset = bigquery.Dataset(full_dataset_id) + bigquery_client.create_dataset(dataset) + yield dataset_id + bigquery_client.delete_dataset(dataset, delete_contents=True, not_found_ok=True) + + +@pytest.fixture +def random_model_id( + bigquery_client: bigquery.Client, project_id: str, dataset_id: str +) -> Iterator[str]: + """Create a new table ID each time, so random_model_id can be used as + target for load jobs. + """ + random_model_id = prefixer.create_prefix() + full_model_id = f"{project_id}.{dataset_id}.{random_model_id}" + yield full_model_id + bigquery_client.delete_model(full_model_id, not_found_ok=True) From f247d957a12a119ce8a263df215e8a9ef7310ef6 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 12 Dec 2023 23:55:50 +0000 Subject: [PATCH 213/921] docs: add code samples for `values` and `value_counts` (#249) * docs: add code samples for `values` and `value_counts` * add newline in docstring for correct rendering * fix warning and return rendering * remove differentiation docstring for values with other methods * adjust series name and whitespaces in the docstrings --- tests/system/small/test_series.py | 38 +++++++++- .../bigframes_vendored/pandas/core/frame.py | 56 ++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 75 +++++++++++++++++++ 3 files changed, 166 insertions(+), 3 deletions(-) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 92a7b6f099b..623da74aa44 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1939,14 +1939,27 @@ def test_cummax_int(scalars_df_index, scalars_pandas_df_index): ) -def test_value_counts(scalars_dfs): +@pytest.mark.parametrize( + ("kwargs"), + [ + {}, + {"normalize": True}, + {"ascending": True}, + ], + ids=[ + "default", + "normalize", + "ascending", + ], +) +def test_value_counts(scalars_dfs, kwargs): if pd.__version__.startswith("1."): pytest.skip("pandas 1.x produces different column labels.") scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_result = scalars_df[col_name].value_counts().to_pandas() - pd_result = scalars_pandas_df[col_name].value_counts() + bf_result = scalars_df[col_name].value_counts(**kwargs).to_pandas() + pd_result = scalars_pandas_df[col_name].value_counts(**kwargs) pd.testing.assert_series_equal( bf_result, @@ -1954,6 +1967,25 @@ def test_value_counts(scalars_dfs): ) +def test_value_counts_with_na(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + + bf_result = scalars_df[col_name].value_counts(dropna=False).to_pandas() + pd_result = scalars_pandas_df[col_name].value_counts(dropna=False) + + # Older pandas version may not have these values, bigframes tries to emulate 2.0+ + pd_result.name = "count" + pd_result.index.name = col_name + + assert_series_equal( + bf_result, + pd_result, + # bigframes values_counts does not honor ordering in the original data + ignore_order=True, + ) + + def test_value_counts_w_cut(scalars_dfs): if pd.__version__.startswith("1."): pytest.skip("value_counts results different in pandas 1.x.") diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 4753bfc5892..c082b873367 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4216,6 +4216,62 @@ def value_counts( """ Return a Series containing counts of unique rows in the DataFrame. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'num_legs': [2, 4, 4, 6, 7], + ... 'num_wings': [2, 0, 0, 0, bpd.NA]}, + ... index=['falcon', 'dog', 'cat', 'ant', 'octopus'], + ... dtype='Int64') + >>> df + num_legs num_wings + falcon 2 2 + dog 4 0 + cat 4 0 + ant 6 0 + octopus 7 + + [5 rows x 2 columns] + + ``value_counts`` sorts the result by counts in a descending order by default: + + >>> df.value_counts() + num_legs num_wings + 4 0 2 + 2 2 1 + 6 0 1 + Name: count, dtype: Int64 + + You can normalize the counts to return relative frequencies by setting ``normalize=True``: + + >>> df.value_counts(normalize=True) + num_legs num_wings + 4 0 0.5 + 2 2 0.25 + 6 0 0.25 + Name: proportion, dtype: Float64 + + You can get the rows in the ascending order of the counts by setting ``ascending=True``: + + >>> df.value_counts(ascending=True) + num_legs num_wings + 2 2 1 + 6 0 1 + 4 0 2 + Name: count, dtype: Int64 + + You can include the counts of the rows with ``NA`` values by setting ``dropna=False``: + + >>> df.value_counts(dropna=False) + num_legs num_wings + 4 0 2 + 2 2 1 + 6 0 1 + 7 1 + Name: count, dtype: Int64 + Args: subset (label or list of labels, optional): Columns to use when counting unique combinations. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 48bcca4ad8e..8303df5ef4c 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -2035,6 +2035,59 @@ def value_counts( first element is the most frequently-occurring element. Excludes NA values by default. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([3, 1, 2, 3, 4, bpd.NA], dtype="Int64") + + >>> s + 0 3 + 1 1 + 2 2 + 3 3 + 4 4 + 5 + dtype: Int64 + + ``value_counts`` sorts the result by counts in a descending order by default: + + >>> s.value_counts() + 3 2 + 1 1 + 2 1 + 4 1 + Name: count, dtype: Int64 + + You can normalize the counts to return relative frequencies by setting ``normalize=True``: + + >>> s.value_counts(normalize=True) + 3 0.4 + 1 0.2 + 2 0.2 + 4 0.2 + Name: proportion, dtype: Float64 + + You can get the values in the ascending order of the counts by setting ``ascending=True``: + + >>> s.value_counts(ascending=True) + 1 1 + 2 1 + 4 1 + 3 2 + Name: count, dtype: Int64 + + You can include the counts of the ``NA`` values by setting ``dropna=False``: + + >>> s.value_counts(dropna=False) + 3 2 + 1 1 + 2 1 + 4 1 + 1 + Name: count, dtype: Int64 + Args: normalize (bool, default False): If True then the object returned will contain the relative @@ -2167,3 +2220,25 @@ def iat(self): def at(self): """Access a single value for a row/column label pair.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def values(self): + """ + Return Series as ndarray or ndarray-like depending on the dtype. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> bpd.Series([1, 2, 3]).values + array([1, 2, 3], dtype=object) + + >>> bpd.Series(list('aabc')).values + array(['a', 'a', 'b', 'c'], dtype=object) + + Returns: + numpy.ndarray or ndarray-like: Values in the Series. + + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 80b8bbeec9e3a53866fd02f58224648d97c88644 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:56:15 -0800 Subject: [PATCH 214/921] chore: filter errors explicitly caught in BF dashboard (#270) plx script change https://plx.corp.google.com/scripts2/script_68._e922fb_55ab_4c4d_9046_85fc1c4c7467 Line 43 - 47: + AND ( SELECT l.value FROM UNNEST(configuration.labels.label) AS l WHERE l.key = "error_caught" ) != "True" --- bigframes/session/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 84a6eb56381..cebef532ad6 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -294,6 +294,7 @@ def _query_to_destination( try: # Write to temp table to workaround BigQuery 10 GB query results # limit. See: internal issue 303057336. + job_config.labels["error_caught"] = "True" _, query_job = self._start_query(query, job_config=job_config) return query_job.destination, query_job except google.api_core.exceptions.BadRequest: From 319a1f27be5bd96ebbe29f11a00a5a62d2b4237f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 13 Dec 2023 06:42:33 +0000 Subject: [PATCH 215/921] chore: deprecate `use_regional_endpoints` (#199) * chore: deprecate `use_regional_endpoints` * don't enable `use_regional_endpoints` in system tests * reword the regional endpoints deprecation message --------- Co-authored-by: Tim Swast --- bigframes/_config/bigquery_options.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index d0cce9492bb..2875a11de37 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -17,6 +17,7 @@ from __future__ import annotations from typing import Optional +import warnings import google.api_core.exceptions import google.auth.credentials @@ -123,6 +124,11 @@ def bq_connection(self, value: Optional[str]): def use_regional_endpoints(self) -> bool: """Flag to connect to regional API endpoints. + .. deprecated:: 0.13.0 + BigQuery regional endpoints is a feature in preview and + available only to selected projects. + Enable it only if your project has regional endpoints access. + Requires ``location`` to also be set. For example, set ``location='asia-northeast1'`` and ``use_regional_endpoints=True`` to connect to asia-northeast1-bigquery.googleapis.com. @@ -135,4 +141,12 @@ def use_regional_endpoints(self, value: bool): raise ValueError( SESSION_STARTED_MESSAGE.format(attribute="use_regional_endpoints") ) + + if value: + warnings.warn( + "BigQuery regional endpoints is a feature in preview and " + "available only to selected projects. " + "Enable it only if your project has regional endpoints access." + ) + self._use_regional_endpoints = value From 034f71f113235f2218223e43f129507c1ec3f6ff Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:34:47 -0800 Subject: [PATCH 216/921] feat: Add `filters` argument to `read_gbq` for enhanced data querying (#198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add filters argument to read_gbq for enhanced data querying * feat: Add filters argument to read_gbq for enhanced data querying * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: Add filters and columns arguments to read_gbq for enhanced data querying See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * update docstring * remove columns input * make filter_to_query run only when there are filters * remove named input --------- Co-authored-by: Owl Bot Co-authored-by: Tim Swast --- bigframes/pandas/__init__.py | 3 + bigframes/session/__init__.py | 77 +++++++++++++++++++ tests/unit/session/test_session.py | 57 ++++++++++++++ .../bigframes_vendored/pandas/io/gbq.py | 29 ++++++- 4 files changed, 165 insertions(+), 1 deletion(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 0c2c1f87aa6..c9640abb23f 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -59,6 +59,7 @@ import third_party.bigframes_vendored.pandas.core.reshape.encoding as vendored_pandas_encoding import third_party.bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile +import third_party.bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq # Include method definition so that the method appears in our docs for @@ -486,6 +487,7 @@ def read_gbq( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + filters: vendored_pandas_gbq.FiltersType = (), use_cache: bool = True, ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query_or_table) @@ -495,6 +497,7 @@ def read_gbq( index_col=index_col, col_order=col_order, max_results=max_results, + filters=filters, use_cache=use_cache, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index cebef532ad6..5364060d1c8 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -233,10 +233,13 @@ def read_gbq( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + filters: third_party_pandas_gbq.FiltersType = (), use_cache: bool = True, # Add a verify index argument that fails if the index is not unique. ) -> dataframe.DataFrame: # TODO(b/281571214): Generate prompt to show the progress of read_gbq. + query_or_table = self._filters_to_query(query_or_table, col_order, filters) + if _is_query(query_or_table): return self._read_gbq_query( query_or_table, @@ -259,6 +262,80 @@ def read_gbq( use_cache=use_cache, ) + def _filters_to_query(self, query_or_table, columns, filters): + """Convert filters to query""" + if len(filters) == 0: + return query_or_table + + sub_query = ( + f"({query_or_table})" if _is_query(query_or_table) else query_or_table + ) + + select_clause = "SELECT " + ( + ", ".join(f"`{column}`" for column in columns) if columns else "*" + ) + + where_clause = "" + if filters: + valid_operators = { + "in": "IN", + "not in": "NOT IN", + "==": "=", + ">": ">", + "<": "<", + ">=": ">=", + "<=": "<=", + "!=": "!=", + } + + if ( + isinstance(filters, Iterable) + and isinstance(filters[0], Tuple) + and (len(filters[0]) == 0 or not isinstance(filters[0][0], Tuple)) + ): + filters = [filters] + + or_expressions = [] + for group in filters: + if not isinstance(group, Iterable): + raise ValueError( + f"Filter group should be a iterable, {group} is not valid." + ) + + and_expressions = [] + for filter_item in group: + if not isinstance(filter_item, tuple) or (len(filter_item) != 3): + raise ValueError( + f"Filter condition should be a tuple of length 3, {filter_item} is not valid." + ) + + column, operator, value = filter_item + + if not isinstance(column, str): + raise ValueError( + f"Column name should be a string, but received '{column}' of type {type(column).__name__}." + ) + + if operator not in valid_operators: + raise ValueError(f"Operator {operator} is not valid.") + + operator = valid_operators[operator] + + if operator in ["IN", "NOT IN"]: + value_list = ", ".join([repr(v) for v in value]) + expression = f"`{column}` {operator} ({value_list})" + else: + expression = f"`{column}` {operator} {repr(value)}" + and_expressions.append(expression) + + or_expressions.append(" AND ".join(and_expressions)) + + if or_expressions: + where_clause = " WHERE " + " OR ".join(or_expressions) + + full_query = f"{select_clause} FROM {sub_query} AS sub{where_clause}" + return full_query + def _query_to_destination( self, query: str, diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 18fd42e0f31..d38a393f270 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -57,3 +57,60 @@ def test_session_init_fails_with_no_project(): credentials=mock.Mock(spec=google.auth.credentials.Credentials) ) ) + + +@pytest.mark.parametrize( + ("query_or_table", "columns", "filters", "expected_output"), + [ + pytest.param( + """SELECT + rowindex, + string_col, + FROM `test_table` AS t + """, + [], + [("rowindex", "<", 4), ("string_col", "==", "Hello, World!")], + """SELECT * FROM (SELECT + rowindex, + string_col, + FROM `test_table` AS t + ) AS sub WHERE `rowindex` < 4 AND `string_col` = 'Hello, World!'""", + id="query_input", + ), + pytest.param( + "test_table", + [], + [("date_col", ">", "2022-10-20")], + "SELECT * FROM test_table AS sub WHERE `date_col` > '2022-10-20'", + id="table_input", + ), + pytest.param( + "test_table", + ["row_index", "string_col"], + [ + (("rowindex", "not in", [0, 6]),), + (("string_col", "in", ["Hello, World!", "こんにちは"]),), + ], + ( + "SELECT `row_index`, `string_col` FROM test_table AS sub WHERE " + "`rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " + "'こんにちは')" + ), + id="or_operation", + ), + pytest.param( + "test_table", + [], + ["date_col", ">", "2022-10-20"], + None, + marks=pytest.mark.xfail( + raises=ValueError, + ), + id="raise_error", + ), + ], +) +def test_read_gbq_with_filters(query_or_table, columns, filters, expected_output): + session = resources.create_bigquery_session() + query = session._filters_to_query(query_or_table, columns, filters) + assert query == expected_output diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index eabb48e600a..dc8bcc1f774 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -3,10 +3,13 @@ from __future__ import annotations -from typing import Iterable, Optional +from typing import Any, Iterable, Literal, Optional, Tuple, Union from bigframes import constants +FilterType = Tuple[str, Literal["in", "not in", "<", "<=", "==", "!=", ">=", ">"], Any] +FiltersType = Iterable[Union[FilterType, Iterable[FilterType]]] + class GBQIOMixin: def read_gbq( @@ -16,6 +19,7 @@ def read_gbq( index_col: Iterable[str] | str = (), col_order: Iterable[str] = (), max_results: Optional[int] = None, + filters: FiltersType = (), use_cache: bool = True, ): """Loads a DataFrame from BigQuery. @@ -71,6 +75,21 @@ def read_gbq( [2 rows x 3 columns] + Reading data with `columns` and `filters` parameters: + + >>> col_order = ['pitcherFirstName', 'pitcherLastName', 'year', 'pitchSpeed'] + >>> filters = [('year', '==', 2016), ('pitcherFirstName', 'in', ['John', 'Doe']), ('pitcherLastName', 'in', ['Gant'])] + >>> df = bpd.read_gbq( + ... "bigquery-public-data.baseball.games_wide", + ... col_order=col_order, + ... filters=filters, + ... ) + >>> df.head(1) + pitcherFirstName pitcherLastName year pitchSpeed + 0 John Gant 2016 82 + + [1 rows x 4 columns] + Args: query_or_table (str): A SQL string to be executed or a BigQuery table to be read. The @@ -84,6 +103,14 @@ def read_gbq( max_results (Optional[int], default None): If set, limit the maximum number of rows to fetch from the query results. + filters (Iterable[Union[Tuple, Iterable[Tuple]]], default ()): To + filter out data. Filter syntax: [[(column, op, val), …],…] where + op is [==, >, >=, <, <=, !=, in, not in]. The innermost tuples + are transposed into a set of filters applied through an AND + operation. The outer Iterable combines these sets of filters + through an OR operation. A single Iterable of tuples can also + be used, meaning that no OR operation between set of filters + is to be conducted. use_cache (bool, default True): Whether to cache the query inputs. Default to True. From b54791c820f56c578a0bd9883489de9b9c7eb3a2 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 13 Dec 2023 16:44:44 -0800 Subject: [PATCH 217/921] fix: increase recursion limit, cache compilation tree hashes (#184) * fix: increase recursion limit, cache compilation tree hashes * don't decrease recursion limit * add comment explaining _node_hash method --- bigframes/core/nodes.py | 75 ++++++++++++++++++++++++++-- bigframes/pandas/__init__.py | 4 ++ tests/system/small/test_dataframe.py | 7 +++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 44a8d808ff5..82a869dac20 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -14,7 +14,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields import functools import typing from typing import Optional, Tuple @@ -66,6 +66,13 @@ def session(self): return sessions[0] return None + # BigFrameNode trees can be very deep so its important avoid recalculating the hash from scratch + # Each subclass of BigFrameNode should use this property to implement __hash__ + # The default dataclass-generated __hash__ method is not cached + @functools.cached_property + def _node_hash(self): + return hash(tuple(hash(getattr(self, field.name)) for field in fields(self))) + @dataclass(frozen=True) class UnaryNode(BigFrameNode): @@ -95,6 +102,9 @@ class JoinNode(BigFrameNode): def child_nodes(self) -> typing.Sequence[BigFrameNode]: return (self.left_child, self.right_child) + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class ConcatNode(BigFrameNode): @@ -104,6 +114,9 @@ class ConcatNode(BigFrameNode): def child_nodes(self) -> typing.Sequence[BigFrameNode]: return self.children + def __hash__(self): + return self._node_hash + # Input Nodex @dataclass(frozen=True) @@ -111,6 +124,9 @@ class ReadLocalNode(BigFrameNode): feather_bytes: bytes column_ids: typing.Tuple[str, ...] + def __hash__(self): + return self._node_hash + # TODO: Refactor to take raw gbq object reference @dataclass(frozen=True) @@ -125,38 +141,60 @@ class ReadGbqNode(BigFrameNode): def session(self): return (self.table_session,) + def __hash__(self): + return self._node_hash + # Unary nodes @dataclass(frozen=True) class DropColumnsNode(UnaryNode): columns: Tuple[str, ...] + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class PromoteOffsetsNode(UnaryNode): col_id: str + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class FilterNode(UnaryNode): predicate_id: str keep_null: bool = False + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class OrderByNode(UnaryNode): by: Tuple[OrderingColumnReference, ...] + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class ReversedNode(UnaryNode): - pass + # useless field to make sure has distinct hash + reversed: bool = True + + def __hash__(self): + return self._node_hash @dataclass(frozen=True) class SelectNode(UnaryNode): column_ids: typing.Tuple[str, ...] + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class ProjectUnaryOpNode(UnaryNode): @@ -164,6 +202,9 @@ class ProjectUnaryOpNode(UnaryNode): op: ops.UnaryOp output_id: Optional[str] = None + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class ProjectBinaryOpNode(UnaryNode): @@ -172,6 +213,9 @@ class ProjectBinaryOpNode(UnaryNode): op: ops.BinaryOp output_id: str + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class ProjectTernaryOpNode(UnaryNode): @@ -181,6 +225,9 @@ class ProjectTernaryOpNode(UnaryNode): op: ops.TernaryOp output_id: str + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class AggregateNode(UnaryNode): @@ -188,12 +235,18 @@ class AggregateNode(UnaryNode): by_column_ids: typing.Tuple[str, ...] = tuple([]) dropna: bool = True + def __hash__(self): + return self._node_hash + # TODO: Unify into aggregate @dataclass(frozen=True) class CorrNode(UnaryNode): corr_aggregations: typing.Tuple[typing.Tuple[str, str, str], ...] + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class WindowOpNode(UnaryNode): @@ -204,10 +257,14 @@ class WindowOpNode(UnaryNode): never_skip_nulls: bool = False skip_reproject_unsafe: bool = False + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class ReprojectOpNode(UnaryNode): - pass + def __hash__(self): + return self._node_hash @dataclass(frozen=True) @@ -223,12 +280,18 @@ class UnpivotNode(UnaryNode): ] = (pandas.Float64Dtype(),) how: typing.Literal["left", "right"] = "left" + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class AssignNode(UnaryNode): source_id: str destination_id: str + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class AssignConstantNode(UnaryNode): @@ -236,6 +299,9 @@ class AssignConstantNode(UnaryNode): value: typing.Hashable dtype: typing.Optional[bigframes.dtypes.Dtype] + def __hash__(self): + return self._node_hash + @dataclass(frozen=True) class RandomSampleNode(UnaryNode): @@ -244,3 +310,6 @@ class RandomSampleNode(UnaryNode): @property def deterministic(self) -> bool: return False + + def __hash__(self): + return self._node_hash diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index c9640abb23f..7386c4a2e73 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -18,6 +18,7 @@ from collections import namedtuple import inspect +import sys import typing from typing import ( Any, @@ -657,6 +658,9 @@ def read_gbq_function(function_name: str): close_session = global_session.close_session reset_session = global_session.close_session +# SQL Compilation uses recursive algorithms on deep trees +# 10M tree depth should be sufficient to generate any sql that is under bigquery limit +sys.setrecursionlimit(max(10000000, sys.getrecursionlimit())) # Use __all__ to let type checkers know what is part of the public API. __all___ = [ diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 57115335dc3..663a7ceb496 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3667,6 +3667,13 @@ def test_df_dot_operator_series( ) +def test_recursion_limit(scalars_df_index): + scalars_df_index = scalars_df_index[["int64_too", "int64_col", "float64_col"]] + for i in range(400): + scalars_df_index = scalars_df_index + 4 + scalars_df_index.to_pandas() + + def test_to_pandas_downsampling_option_override(session): df = session.read_gbq("bigframes-dev.bigframes_tests_sys.batting") download_size = 1 From 4f3db3d50fb782dbe03051ed024d03e19944d775 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:46:55 -0800 Subject: [PATCH 218/921] feat: add module/class level api tracking (#272) * feat: add module/class level api tracking * fix the failing unit test * fix: follow the labels requirement --- bigframes/core/log_adapter.py | 8 +++++--- tests/unit/core/test_log_adapter.py | 4 ++-- tests/unit/session/test_io_bigquery.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/bigframes/core/log_adapter.py b/bigframes/core/log_adapter.py index b790d195623..556851fa1b7 100644 --- a/bigframes/core/log_adapter.py +++ b/bigframes/core/log_adapter.py @@ -25,19 +25,21 @@ def class_logger(decorated_cls): """Decorator that adds logging functionality to each method of the class.""" for attr_name, attr_value in decorated_cls.__dict__.items(): if callable(attr_value): - setattr(decorated_cls, attr_name, method_logger(attr_value)) + setattr(decorated_cls, attr_name, method_logger(attr_value, decorated_cls)) return decorated_cls -def method_logger(method): +def method_logger(method, decorated_cls): """Decorator that adds logging functionality to a method.""" @functools.wraps(method) def wrapper(*args, **kwargs): + class_name = decorated_cls.__name__ # Access decorated class name api_method_name = str(method.__name__) + full_method_name = f"{class_name.lower()}-{api_method_name}" # Track regular and "dunder" methods if api_method_name.startswith("__") or not api_method_name.startswith("_"): - add_api_method(api_method_name) + add_api_method(full_method_name) return method(*args, **kwargs) return wrapper diff --git a/tests/unit/core/test_log_adapter.py b/tests/unit/core/test_log_adapter.py index 376b7f20758..7033369dd52 100644 --- a/tests/unit/core/test_log_adapter.py +++ b/tests/unit/core/test_log_adapter.py @@ -40,8 +40,8 @@ def test_method_logging(test_instance): # Check if the methods were added to the _api_methods list api_methods = log_adapter.get_and_reset_api_methods() assert api_methods is not None - assert "method1" in api_methods - assert "method2" in api_methods + assert "testclass-method1" in api_methods + assert "testclass-method2" in api_methods def test_add_api_method_limit(test_instance): diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 3f3bfbe7d36..96bb7bf67fc 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -76,11 +76,11 @@ def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): expected_dict = { "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", - "recent-bigframes-api-0": "__init__", - "recent-bigframes-api-1": "max", - "recent-bigframes-api-2": "__init__", - "recent-bigframes-api-3": "head", - "recent-bigframes-api-4": "__init__", + "recent-bigframes-api-0": "series-__init__", + "recent-bigframes-api-1": "dataframe-max", + "recent-bigframes-api-2": "dataframe-__init__", + "recent-bigframes-api-3": "dataframe-head", + "recent-bigframes-api-4": "dataframe-__init__", } assert labels is not None assert len(labels) == 7 @@ -100,7 +100,7 @@ def test_create_job_configs_labels_length_limit_met_and_labels_is_none(): ) assert labels is not None assert len(labels) == 64 - assert "head" in labels.values() + assert "dataframe-head" in labels.values() def test_create_job_configs_labels_length_limit_met(): @@ -125,8 +125,8 @@ def test_create_job_configs_labels_length_limit_met(): ) assert labels is not None assert len(labels) == 64 - assert "max" in labels.values() - assert "head" not in labels.values() + assert "dataframe-max" in labels.values() + assert "dataframe-head" not in labels.values() assert "bigframes-api" in labels.keys() assert "source" in labels.keys() From 31e17411f294924032df47018e3b9f547808f998 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 06:21:25 +0000 Subject: [PATCH 219/921] chore(main): release 0.17.0 (#269) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 21 +++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ea51707c4..8c49c5b63bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.17.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.16.0...v0.17.0) (2023-12-14) + + +### Features + +* Add `filters` argument to `read_gbq` for enhanced data querying ([#198](https://github.com/googleapis/python-bigquery-dataframes/issues/198)) ([034f71f](https://github.com/googleapis/python-bigquery-dataframes/commit/034f71f113235f2218223e43f129507c1ec3f6ff)) +* Add module/class level api tracking ([#272](https://github.com/googleapis/python-bigquery-dataframes/issues/272)) ([4f3db3d](https://github.com/googleapis/python-bigquery-dataframes/commit/4f3db3d50fb782dbe03051ed024d03e19944d775)) +* Deprecate `use_regional_endpoints` ([#199](https://github.com/googleapis/python-bigquery-dataframes/issues/199)) ([319a1f2](https://github.com/googleapis/python-bigquery-dataframes/commit/319a1f27be5bd96ebbe29f11a00a5a62d2b4237f)) + + +### Bug Fixes + +* Increase recursion limit, cache compilation tree hashes ([#184](https://github.com/googleapis/python-bigquery-dataframes/issues/184)) ([b54791c](https://github.com/googleapis/python-bigquery-dataframes/commit/b54791c820f56c578a0bd9883489de9b9c7eb3a2)) +* Replaced raise `NotImplementedError` with return `NotImplemented` ([#258](https://github.com/googleapis/python-bigquery-dataframes/issues/258)) ([a133822](https://github.com/googleapis/python-bigquery-dataframes/commit/a133822974229f70529a414a682b6d98770d1846)) + + +### Documentation + +* Add code samples for `values` and `value_counts` ([#249](https://github.com/googleapis/python-bigquery-dataframes/issues/249)) ([f247d95](https://github.com/googleapis/python-bigquery-dataframes/commit/f247d957a12a119ce8a263df215e8a9ef7310ef6)) +* Add sample for getting started with BQML ([#141](https://github.com/googleapis/python-bigquery-dataframes/issues/141)) ([fb14f54](https://github.com/googleapis/python-bigquery-dataframes/commit/fb14f54548e988c6c226753fcca162cf15b5c8d7)) + ## [0.16.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.15.0...v0.16.0) (2023-12-12) diff --git a/bigframes/version.py b/bigframes/version.py index 3ddf7e0f79a..04eac385f67 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.16.0" +__version__ = "0.17.0" From 9798a2b14dffb20432f732343cac92341e42fe09 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 15 Dec 2023 14:42:07 -0600 Subject: [PATCH 220/921] deps: migrate to `ibis-framework >= "7.1.0"` (#53) * deps: migrate to `ibis-framework >= "7.0.0"` This should unlock some bug fixes as well as potential `UNNEST` support in a future change. * use dtype instead of output_dtype in custom ops * adjust type annotations * Update noxfile.py * update type annotations * fix for deferred values * fix prerelease * minimum 7.1.0 * mypy * revert presubmit changes * fix minimum sqlglot * fix custom op * hack InMemoryTable formatter back in * use ops module to avoid breaking changes if ops move around * workaround nullscalar issue * update usage of percent_rank to explicitly order by the value * disable ibis prerelease tests for now * fix unit_prerelease --- bigframes/core/compile/compiled.py | 25 ++++++- bigframes/core/reshape/__init__.py | 6 +- bigframes/operations/__init__.py | 70 ++++++++++++++++--- bigframes/operations/aggregations.py | 8 +-- bigframes/remote_function.py | 9 +-- bigframes/session/__init__.py | 7 +- mypy.ini | 3 + noxfile.py | 26 +++---- setup.py | 2 +- testing/constraints-3.9.txt | 4 +- tests/system/small/test_ibis.py | 13 ++-- tests/unit/resources.py | 2 +- .../ibis/backends/bigquery/__init__.py | 3 + .../ibis/backends/bigquery/compiler.py | 59 ++++++++++++++++ .../ibis/expr/operations/analytic.py | 14 ++-- .../ibis/expr/operations/json.py | 2 +- .../ibis/expr/operations/reductions.py | 8 +-- 17 files changed, 198 insertions(+), 63 deletions(-) create mode 100644 third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 461c2c005a6..537d9c8b527 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -21,6 +21,7 @@ import ibis import ibis.backends.bigquery as ibis_bigquery +import ibis.common.deferred # type: ignore import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types import pandas @@ -62,7 +63,16 @@ def __init__( self._columns = tuple(columns) # To allow for more efficient lookup by column name, create a # dictionary mapping names to column values. - self._column_names = {column.get_name(): column for column in self._columns} + self._column_names = { + ( + column.resolve(table) + # TODO(https://github.com/ibis-project/ibis/issues/7613): use + # public API to refer to Deferred type. + if isinstance(column, ibis.common.deferred.Deferred) + else column + ).get_name(): column + for column in self._columns + } @property def columns(self) -> typing.Tuple[ibis_types.Value, ...]: @@ -643,7 +653,16 @@ def __init__( # To allow for more efficient lookup by column name, create a # dictionary mapping names to column values. - self._column_names = {column.get_name(): column for column in self._columns} + self._column_names = { + ( + column.resolve(table) + # TODO(https://github.com/ibis-project/ibis/issues/7613): use + # public API to refer to Deferred type. + if isinstance(column, ibis.common.deferred.Deferred) + else column + ).get_name(): column + for column in self._columns + } self._hidden_ordering_column_names = { column.get_name(): column for column in self._hidden_ordering_columns } @@ -860,7 +879,7 @@ def project_window_op( case_statement = ibis.case() for clause in clauses: case_statement = case_statement.when(clause[0], clause[1]) - case_statement = case_statement.else_(window_op).end() + case_statement = case_statement.else_(window_op).end() # type: ignore window_op = case_statement result = self._set_or_replace_by_id(output_name or column_name, window_op) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index dc61c3baada..24c1bff309d 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -18,6 +18,7 @@ import bigframes.constants as constants import bigframes.core as core +import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.dataframe import bigframes.operations as ops @@ -145,7 +146,10 @@ def qcut( block, result = block.apply_window_op( x._value_column, agg_ops.QcutOp(q), - window_spec=core.WindowSpec(grouping_keys=(nullity_id,)), + window_spec=core.WindowSpec( + grouping_keys=(nullity_id,), + ordering=(order.OrderingColumnReference(x._value_column),), + ), ) block, result = block.apply_binary_op( result, nullity_id, ops.partial_arg3(ops.where_op, None), result_label=label diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index a29dd36c72a..0655aafdb37 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -18,6 +18,7 @@ import typing import ibis +import ibis.common.annotations import ibis.common.exceptions import ibis.expr.datatypes as ibis_dtypes import ibis.expr.operations.generic @@ -352,14 +353,23 @@ def _as_ibis(self, x: ibis_types.Value): str_val = typing.cast(ibis_types.StringValue, x) # SQL pad operations will truncate, we do not want to truncate though. - pad_length = ibis.greatest(str_val.length(), self._length) + pad_length = typing.cast( + ibis_types.IntegerValue, ibis.greatest(str_val.length(), self._length) + ) if self._side == "left": return str_val.lpad(pad_length, self._fillchar) elif self._side == "right": return str_val.rpad(pad_length, self._fillchar) else: # side == both # Pad more on right side if can't pad both sides equally - lpad_amount = ((pad_length - str_val.length()) // 2) + str_val.length() + lpad_amount = typing.cast( + ibis_types.IntegerValue, + ( + (pad_length - str_val.length()) + // typing.cast(ibis_types.NumericValue, ibis.literal(2)) + ) + + str_val.length(), + ) return str_val.lpad(lpad_amount, self._fillchar).rpad( pad_length, self._fillchar ) @@ -722,10 +732,29 @@ def ne_op( return x != y +def _null_or_value(value: ibis_types.Value, where_value: ibis_types.BooleanValue): + return ibis.where( + where_value, + value, + ibis.null(), + ) + + def and_op( x: ibis_types.Value, y: ibis_types.Value, ): + # Workaround issue https://github.com/ibis-project/ibis/issues/7775 by + # implementing three-valued logic ourselves. For AND, when we encounter a + # NULL value, we only know when the result is FALSE, otherwise the result + # is unknown (NULL). See: truth table at + # https://en.wikibooks.org/wiki/Structured_Query_Language/NULLs_and_the_Three_Valued_Logic#AND,_OR + if isinstance(x, ibis_types.NullScalar): + return _null_or_value(y, y == ibis.literal(False)) + + if isinstance(y, ibis_types.NullScalar): + return _null_or_value(x, x == ibis.literal(False)) + return typing.cast(ibis_types.BooleanValue, x) & typing.cast( ibis_types.BooleanValue, y ) @@ -735,6 +764,17 @@ def or_op( x: ibis_types.Value, y: ibis_types.Value, ): + # Workaround issue https://github.com/ibis-project/ibis/issues/7775 by + # implementing three-valued logic ourselves. For OR, when we encounter a + # NULL value, we only know when the result is TRUE, otherwise the result + # is unknown (NULL). See: truth table at + # https://en.wikibooks.org/wiki/Structured_Query_Language/NULLs_and_the_Three_Valued_Logic#AND,_OR + if isinstance(x, ibis_types.NullScalar): + return _null_or_value(y, y == ibis.literal(True)) + + if isinstance(y, ibis_types.NullScalar): + return _null_or_value(x, x == ibis.literal(True)) + return typing.cast(ibis_types.BooleanValue, x) | typing.cast( ibis_types.BooleanValue, y ) @@ -746,10 +786,16 @@ def add_op( y: ibis_types.Value, ): if isinstance(x, ibis_types.NullScalar) or isinstance(x, ibis_types.NullScalar): - return - return typing.cast(ibis_types.NumericValue, x) + typing.cast( - ibis_types.NumericValue, y - ) + return ibis.null() + try: + # Could be string concatenation or numeric addition. + return x + y # type: ignore + except ibis.common.annotations.SignatureValidationError as exc: + left_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(x.type()) + right_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(y.type()) + raise TypeError( + f"Cannot add {repr(left_type)} and {repr(right_type)}. {constants.FEEDBACK_LINK}" + ) from exc @short_circuit_nulls() @@ -1047,7 +1093,7 @@ def where_op( replacement: ibis_types.Value, ) -> ibis_types.Value: """Returns x if y is true, otherwise returns z.""" - return ibis.case().when(condition, original).else_(replacement).end() + return ibis.case().when(condition, original).else_(replacement).end() # type: ignore def clip_op( @@ -1060,7 +1106,7 @@ def clip_op( not isinstance(upper, ibis_types.NullScalar) ): return ( - ibis.case() + ibis.case() # type: ignore .when(upper.isnull() | (original > upper), upper) .else_(original) .end() @@ -1069,7 +1115,7 @@ def clip_op( upper, ibis_types.NullScalar ): return ( - ibis.case() + ibis.case() # type: ignore .when(lower.isnull() | (original < lower), lower) .else_(original) .end() @@ -1079,9 +1125,11 @@ def clip_op( ): return original else: - # Note: Pandas has unchanged behavior when upper bound and lower bound are flipped. This implementation requires that lower_bound < upper_bound + # Note: Pandas has unchanged behavior when upper bound and lower bound + # are flipped. + # This implementation requires that lower_bound < upper_bound. return ( - ibis.case() + ibis.case() # type: ignore .when(lower.isnull() | (original < lower), lower) .when(upper.isnull() | (original > upper), upper) .else_(original) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 465d1887247..363dfe819d0 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -74,7 +74,7 @@ def _as_ibis( # Will be null if all inputs are null. Pandas defaults to zero sum though. bq_sum = _apply_window_if_present(column.sum(), window) return ( - ibis.case().when(bq_sum.isnull(), ibis_types.literal(0)).else_(bq_sum).end() + ibis.case().when(bq_sum.isnull(), ibis_types.literal(0)).else_(bq_sum).end() # type: ignore ) @@ -167,7 +167,7 @@ def _as_ibis( .else_(magnitude * pow(-1, negative_count_parity)) .end() ) - return float_result.cast(column.type()) + return float_result.cast(column.type()) # type: ignore class MaxOp(AggregateOp): @@ -290,7 +290,7 @@ def _as_ibis( dtypes.literal_to_ibis_scalar(bucket_n, force_dtype=Int64Dtype()), ) out = out.else_(None) - return out.end() + return out.end() # type: ignore @property def skips_nulls(self): @@ -482,7 +482,7 @@ def _map_to_literal( original: ibis_types.Value, literal: ibis_types.Scalar ) -> ibis_types.Column: # Hack required to perform aggregations on literals in ibis, even though bigquery will let you directly aggregate literals (eg. 'SELECT COUNT(1) from table1') - return ibis.ifelse(original.isnull(), literal, literal) + return ibis.ifelse(original.isnull(), literal, literal) # type: ignore sum_op = SumOp() diff --git a/bigframes/remote_function.py b/bigframes/remote_function.py index a899ebd371b..f54c26fa566 100644 --- a/bigframes/remote_function.py +++ b/bigframes/remote_function.py @@ -535,17 +535,14 @@ def remote_function_node( """Creates an Ibis node representing a remote function call.""" fields = { - name: rlz.value(type_) if type_ else rlz.any + name: rlz.ValueOf(None if type_ == "ANY TYPE" else type_) for name, type_ in zip( ibis_signature.parameter_names, ibis_signature.input_types ) } - try: - fields["output_type"] = rlz.shape_like("args", dtype=ibis_signature.output_type) # type: ignore - except TypeError: - fields["output_dtype"] = property(lambda _: ibis_signature.output_type) - fields["output_shape"] = rlz.shape_like("args") + fields["dtype"] = ibis_signature.output_type # type: ignore + fields["shape"] = rlz.shape_like("args") node = type(routine_ref_to_string_for_query(routine_ref), (ops.ValueOp,), fields) # type: ignore diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 5364060d1c8..fb5fab86ce3 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -79,9 +79,9 @@ import bigframes.session.clients import bigframes.version -# Even though the ibis.backends.bigquery.registry import is unused, it's needed +# Even though the ibis.backends.bigquery import is unused, it's needed # to register new and replacement ops with the Ibis BigQuery backend. -import third_party.bigframes_vendored.ibis.backends.bigquery.registry # noqa +import third_party.bigframes_vendored.ibis.backends.bigquery # noqa import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import third_party.bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import third_party.bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet @@ -873,8 +873,9 @@ def _read_pandas( total_ordering_columns=frozenset([ordering_col]), integer_encoding=IntegerEncoding(True, is_sequential=True), ) - table_expression = self.ibis_client.table( + table_expression = self.ibis_client.table( # type: ignore load_table_destination.table_id, + # TODO: use "dataset_id" as the "schema" database=f"{load_table_destination.project}.{load_table_destination.dataset_id}", ) diff --git a/mypy.ini b/mypy.ini index 901394813aa..3809f8e241d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -24,5 +24,8 @@ ignore_missing_imports = True [mypy-pyarrow] ignore_missing_imports = True +[mypy-ibis.*] +ignore_missing_imports = True + [mypy-ipywidgets] ignore_missing_imports = True diff --git a/noxfile.py b/noxfile.py index 2174e27529f..c0ec3b0c543 100644 --- a/noxfile.py +++ b/noxfile.py @@ -524,23 +524,19 @@ def prerelease(session: nox.sessions.Session, tests_path): ) already_installed.add("pandas") - # TODO(shobs): - # Commit https://github.com/ibis-project/ibis/commit/c20ba7feab6bdea6c299721310e04dbc10551cc2 - # introduced breaking change that removed the following: - # ibis.expr.rules.column - # ibis.expr.rules.value - # ibis.expr.rules.any - # Let's exclude ibis head from prerelease install list for now. Instead, use - # a working ibis-framework version resolved via setup.by (currently resolves - # to version 6.2.0 due to version requirement "6.2.0,<7.0.0dev"). - # We should enable the head back once bigframes support a version that - # includes the above commit. + # Ibis has introduced breaking changes. Let's exclude ibis head + # from prerelease install list for now. We should enable the head back + # once bigframes supports the version at HEAD. # session.install( - # "--upgrade", - # "-e", # Use -e so that py.typed file is included. - # "git+https://github.com/ibis-project/ibis.git#egg=ibis-framework", + # "--upgrade", + # "-e", # Use -e so that py.typed file is included. + # "git+https://github.com/ibis-project/ibis.git@7.x.x#egg=ibis-framework", # ) - session.install("--no-deps", "ibis-framework==6.2.0") + session.install( + "--upgrade", + # "--pre", + "ibis-framework>=7.1.0,<8.0.0dev", + ) already_installed.add("ibis-framework") # Workaround https://github.com/googleapis/python-db-dtypes-pandas/issues/178 diff --git a/setup.py b/setup.py index 33515429852..1ad4bbd3eb1 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,8 @@ "google-cloud-iam >=2.12.1", "google-cloud-resource-manager >=1.10.3", "google-cloud-storage >=2.0.0", + "ibis-framework[bigquery] >=7.1.0,<8.0.0dev", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. - "ibis-framework[bigquery] >=6.2.0,<7.0.0dev", "pandas >=1.5.0,<2.1.4", "pydata-google-auth >=1.8.2", "requests >=2.27.1", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index f43d3b4ca01..218255c77e6 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -45,7 +45,7 @@ greenlet==2.0.2 grpc-google-iam-v1==0.12.6 grpcio==1.53.0 grpcio-status==1.48.2 -ibis-framework==6.2.0 +ibis-framework==7.1.0 humanize==4.6.0 identify==2.5.22 idna==3.4 @@ -107,7 +107,7 @@ scikit-learn==1.2.2 SecretStorage==3.3.3 six==1.16.0 SQLAlchemy==1.4.0 -sqlglot==10.6.4 +sqlglot==18.12.0 tomli==2.0.1 toolz==0.12.0 tqdm==4.65.0 diff --git a/tests/system/small/test_ibis.py b/tests/system/small/test_ibis.py index 58b78e00481..9fe1176068b 100644 --- a/tests/system/small/test_ibis.py +++ b/tests/system/small/test_ibis.py @@ -23,11 +23,16 @@ def test_approximate_quantiles(session: bigframes.Session, scalars_table_id: str): num_bins = 3 ibis_client = session.ibis_client - _, dataset, table_id = scalars_table_id.split(".") - ibis_table: ibis_types.Table = ibis_client.table(table_id, database=dataset) + project, dataset, table_id = scalars_table_id.split(".") + ibis_table: ibis_types.Table = ibis_client.table( # type: ignore + table_id, + schema=dataset, + database=project, + ) ibis_column: ibis_types.NumericColumn = ibis_table["int64_col"] - quantiles: ibis_types.ArrayScalar = vendored_ibis_ops.ApproximateMultiQuantile( # type: ignore - ibis_column, num_bins=num_bins + quantiles: ibis_types.ArrayScalar = vendored_ibis_ops.ApproximateMultiQuantile( + ibis_column, # type: ignore + num_bins=num_bins, # type: ignore ).to_expr() value = quantiles[1] num_edges = quantiles.length() diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 8ba321d122c..b239b04671f 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -79,7 +79,7 @@ def create_dataframe( # might not actually be used. Mock out the global session, too. monkeypatch.setattr(bigframes.core.global_session, "_global_session", session) bigframes.options.bigquery._session_started = True - return bigframes.dataframe.DataFrame({}, session=session) + return bigframes.dataframe.DataFrame({"col": []}, session=session) def create_pandas_session(tables: Dict[str, pandas.DataFrame]) -> bigframes.Session: diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py b/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py index e69de29bb2d..43508fab11b 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py @@ -0,0 +1,3 @@ +# Import all sub-modules to monkeypatch everything. +import third_party.bigframes_vendored.ibis.backends.bigquery.compiler # noqa +import third_party.bigframes_vendored.ibis.backends.bigquery.registry # noqa diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py b/third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py new file mode 100644 index 00000000000..414f0a7c81e --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py @@ -0,0 +1,59 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/compiler.py +"""Module to convert from Ibis expression to SQL string.""" + +from __future__ import annotations + +import re + +from ibis.backends.base.sql import compiler as sql_compiler +import ibis.backends.bigquery.compiler +from ibis.backends.bigquery.datatypes import BigQueryType +import ibis.expr.datatypes as dt +import ibis.expr.operations as ops + +_NAME_REGEX = re.compile(r'[^!"$()*,./;?@[\\\]^`{}~\n]+') +_EXACT_NAME_REGEX = re.compile(f"^{_NAME_REGEX.pattern}$") + + +class BigQueryTableSetFormatter(sql_compiler.TableSetFormatter): + def _quote_identifier(self, name): + """Restore 6.x version of identifier quoting. + + 7.x uses sqlglot which as of December 2023 doesn't know about the + extended unicode names for BigQuery yet. + """ + if _EXACT_NAME_REGEX.match(name) is not None: + return name + return f"`{name}`" + + def _format_in_memory_table(self, op): + """Restore 6.x version of InMemoryTable. + + BigQuery DataFrames explicitly uses InMemoryTable only when we know + the data is small enough to embed in SQL. + """ + schema = op.schema + names = schema.names + types = schema.types + + raw_rows = [] + for row in op.data.to_frame().itertuples(index=False): + raw_row = ", ".join( + f"{self._translate(lit)} AS {name}" + for lit, name in zip( + map(ops.Literal, row, types), map(self._quote_identifier, names) + ) + ) + raw_rows.append(f"STRUCT({raw_row})") + array_type = BigQueryType.from_ibis(dt.Array(op.schema.as_struct())) + + return f"UNNEST({array_type}[{', '.join(raw_rows)}])" + + +# Override implementation. +ibis.backends.bigquery.compiler.BigQueryTableSetFormatter._quote_identifier = ( + BigQueryTableSetFormatter._quote_identifier +) +ibis.backends.bigquery.compiler.BigQueryTableSetFormatter._format_in_memory_table = ( + BigQueryTableSetFormatter._format_in_memory_table +) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/analytic.py b/third_party/bigframes_vendored/ibis/expr/operations/analytic.py index 038987cac93..3d6a3b37b1b 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/analytic.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/analytic.py @@ -2,22 +2,22 @@ from __future__ import annotations -from ibis.expr.operations.analytic import Analytic +import ibis.expr.operations as ops import ibis.expr.rules as rlz -class FirstNonNullValue(Analytic): +class FirstNonNullValue(ops.Analytic): """Retrieve the first element.""" - arg = rlz.column(rlz.any) - output_dtype = rlz.dtype_like("arg") + arg: ops.Column + dtype = rlz.dtype_like("arg") -class LastNonNullValue(Analytic): +class LastNonNullValue(ops.Analytic): """Retrieve the last element.""" - arg = rlz.column(rlz.any) - output_dtype = rlz.dtype_like("arg") + arg: ops.Column + dtype = rlz.dtype_like("arg") __all__ = [ diff --git a/third_party/bigframes_vendored/ibis/expr/operations/json.py b/third_party/bigframes_vendored/ibis/expr/operations/json.py index dbb3fa3066c..772c2e8ff44 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/json.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/json.py @@ -6,4 +6,4 @@ class ToJsonString(Unary): - output_dtype = dt.string + dtype = dt.string diff --git a/third_party/bigframes_vendored/ibis/expr/operations/reductions.py b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py index 5e6ad9ecf2b..e6644f477a5 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/reductions.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py @@ -3,8 +3,8 @@ from __future__ import annotations import ibis.expr.datatypes as dt +import ibis.expr.operations.core as ibis_ops_core from ibis.expr.operations.reductions import Filterable, Reduction -import ibis.expr.rules as rlz class ApproximateMultiQuantile(Filterable, Reduction): @@ -13,9 +13,9 @@ class ApproximateMultiQuantile(Filterable, Reduction): See: https://cloud.google.com/bigquery/docs/reference/standard-sql/approximate_aggregate_functions#approx_quantiles """ - arg = rlz.any - num_bins = rlz.value(dt.int64) - output_dtype = dt.Array(dt.float64) + arg: ibis_ops_core.Value + num_bins: ibis_ops_core.Value[dt.Int64] + dtype = dt.Array(dt.float64) __all__ = [ From 7cbbb7d4608d8b7d1a360b2fe2d39d89a52f9546 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:24:15 -0800 Subject: [PATCH 221/921] docs: add code snippets for explore query result page (#278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 316614454 🦕 --- samples/snippets/explore_query_result_test.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 samples/snippets/explore_query_result_test.py diff --git a/samples/snippets/explore_query_result_test.py b/samples/snippets/explore_query_result_test.py new file mode 100644 index 00000000000..5f0ec7d9b61 --- /dev/null +++ b/samples/snippets/explore_query_result_test.py @@ -0,0 +1,70 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_bigquery_dataframes_explore_query_result(): + import bigframes.pandas as bpd + + # [START bigquery_dataframes_explore_query_result] + # Load data from BigQuery + query_or_table = "bigquery-public-data.ml_datasets.penguins" + bq_df = bpd.read_gbq(query_or_table) + + # Inspect one of the columns (or series) of the DataFrame: + bq_df["body_mass_g"] + + # Compute the mean of this series: + average_body_mass = bq_df["body_mass_g"].mean() + print(f"average_body_mass: {average_body_mass}") + + # Find the heaviest species using the groupby operation to calculate the + # mean body_mass_g: + ( + bq_df["body_mass_g"] + .groupby(by=bq_df["species"]) + .mean() + .sort_values(ascending=False) + .head(10) + ) + + # Create the Linear Regression model + from bigframes.ml.linear_model import LinearRegression + + # Filter down to the data we want to analyze + adelie_data = bq_df[bq_df.species == "Adelie Penguin (Pygoscelis adeliae)"] + + # Drop the columns we don't care about + adelie_data = adelie_data.drop(columns=["species"]) + + # Drop rows with nulls to get our training data + training_data = adelie_data.dropna() + + # Pick feature columns and label column + X = training_data[ + [ + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + y = training_data[["body_mass_g"]] + + model = LinearRegression(fit_intercept=False) + model.fit(X, y) + model.score(X, y) + # [END bigquery_dataframes_explore_query_result] + assert average_body_mass is not None + assert model is not None From 02f7ab64bf520f41a0d9a536d4d4880d3a3d401a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 15 Dec 2023 16:18:18 -0800 Subject: [PATCH 222/921] refactor: move query execution from ArrayValue to Session (#255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 110 ++++------------------------- bigframes/core/blocks.py | 37 +++++++--- bigframes/core/compile/compiled.py | 16 +++++ bigframes/core/compile/compiler.py | 6 ++ bigframes/core/indexes/index.py | 6 +- bigframes/core/nodes.py | 8 ++- bigframes/dataframe.py | 3 +- bigframes/session/__init__.py | 76 ++++++++++++++++++++ tests/system/small/test_session.py | 4 +- 9 files changed, 155 insertions(+), 111 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index e19fec8f3f7..e8ac8c1d0f9 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -16,10 +16,8 @@ from dataclasses import dataclass import io import typing -from typing import Iterable, Literal, Optional, Sequence, Tuple +from typing import Iterable, Literal, Sequence -from google.cloud import bigquery -import ibis import ibis.expr.types as ibis_types import pandas @@ -86,7 +84,17 @@ def session(self) -> Session: required_session = self.node.session from bigframes import get_global_session - return self.node.session[0] if required_session else get_global_session() + return ( + required_session if (required_session is not None) else get_global_session() + ) + + def _try_evaluate_local(self): + """Use only for unit testing paths - not fully featured. Will throw exception if fails.""" + import ibis + + return ibis.pandas.connect({}).execute( + self._compile_ordered()._to_ibis_expr(ordering_mode="unordered") + ) def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: return self._compile_ordered().get_column_type(key) @@ -97,97 +105,9 @@ def _compile_ordered(self) -> compiled.OrderedIR: def _compile_unordered(self) -> compiled.UnorderedIR: return compiler.compile_unordered(self.node) - def shape(self) -> typing.Tuple[int, int]: - """Returns dimensions as (length, width) tuple.""" - width = len(self._compile_unordered().columns) - count_expr = self._compile_unordered()._to_ibis_expr().count() - - # Support in-memory engines for hermetic unit tests. - if not self.node.session: - try: - length = ibis.pandas.connect({}).execute(count_expr) - return (length, width) - except Exception: - # Not all cases can be handled by pandas engine - pass - - sql = self.session.ibis_client.compile(count_expr) - row_iterator, _ = self.session._start_query( - sql=sql, - max_results=1, - ) - length = next(row_iterator)[0] - return (length, width) - - def to_sql( - self, - offset_column: typing.Optional[str] = None, - col_id_overrides: typing.Mapping[str, str] = {}, - sorted: bool = False, - ) -> str: - array_value = self - if offset_column: - array_value = self.promote_offsets(offset_column) - if sorted: - return array_value._compile_ordered().to_sql( - col_id_overrides=col_id_overrides, - sorted=sorted, - ) - else: - return array_value._compile_unordered().to_sql( - col_id_overrides=col_id_overrides - ) - - def start_query( - self, - job_config: Optional[bigquery.job.QueryJobConfig] = None, - max_results: Optional[int] = None, - *, - sorted: bool = True, - ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - """Execute a query and return metadata about the results.""" - # TODO(swast): Cache the job ID so we can look it up again if they ask - # for the results? We'd need a way to invalidate the cache if DataFrame - # becomes mutable, though. Or move this method to the immutable - # expression class. - # TODO(swast): We might want to move this method to Session and/or - # provide our own minimal metadata class. Tight coupling to the - # BigQuery client library isn't ideal, especially if we want to support - # a LocalSession for unit testing. - # TODO(swast): Add a timeout here? If the query is taking a long time, - # maybe we just print the job metadata that we have so far? - sql = self.to_sql(sorted=sorted) # type:ignore - return self.session._start_query( - sql=sql, - job_config=job_config, - max_results=max_results, - ) - - def cached(self, cluster_cols: typing.Sequence[str]) -> ArrayValue: - """Write the ArrayValue to a session table and create a new block object that references it.""" - compiled_value = self._compile_ordered() - ibis_expr = compiled_value._to_ibis_expr( - ordering_mode="unordered", expose_hidden_cols=True - ) - tmp_table = self.session._ibis_to_temp_table( - ibis_expr, cluster_cols=cluster_cols, api_name="cached" - ) - - table_expression = self.session.ibis_client.table( - f"{tmp_table.project}.{tmp_table.dataset_id}.{tmp_table.table_id}" - ) - new_columns = [table_expression[column] for column in compiled_value.column_ids] - new_hidden_columns = [ - table_expression[column] - for column in compiled_value._hidden_ordering_column_names - ] - return ArrayValue.from_ibis( - self.session, - table_expression, - columns=new_columns, - hidden_ordering_columns=new_hidden_columns, - ordering=compiled_value._ordering, - ) + def row_count(self) -> ArrayValue: + """Get number of rows in ArrayValue as a single-entry ArrayValue.""" + return ArrayValue(nodes.RowCountNode(child=self.node)) # Operations diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 34913872e70..6542b694d29 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -137,8 +137,19 @@ def index(self) -> indexes.IndexValue: @functools.cached_property def shape(self) -> typing.Tuple[int, int]: """Returns dimensions as (length, width) tuple.""" - impl_length, _ = self._expr.shape() - return (impl_length, len(self.value_columns)) + row_count_expr = self.expr.row_count() + + # Support in-memory engines for hermetic unit tests. + if self.expr.node.session is None: + try: + row_count = row_count_expr._try_evaluate_local().squeeze() + return (row_count, len(self.value_columns)) + except Exception: + pass + + iter, _ = self.session._execute(row_count_expr, sorted=False) + row_count = next(iter)[0] + return (row_count, len(self.value_columns)) @property def index_columns(self) -> Sequence[str]: @@ -182,6 +193,10 @@ def index_dtypes( """Returns the dtypes of the index columns.""" return [self.expr.get_column_type(col) for col in self.index_columns] + @property + def session(self) -> core.Session: + return self._expr.session + @functools.cached_property def col_id_to_label(self) -> typing.Mapping[str, Label]: """Get column label for value columns, or index name for index columns""" @@ -376,7 +391,7 @@ def _to_dataframe(self, result) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" dtypes = dict(zip(self.index_columns, self.index_dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) - return self._expr.session._rows_to_dataframe(result, dtypes) + return self.session._rows_to_dataframe(result, dtypes) def to_pandas( self, @@ -404,9 +419,9 @@ def to_pandas_batches(self): """Download results one message at a time.""" dtypes = dict(zip(self.index_columns, self.index_dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) - results_iterator, _ = self._expr.start_query() + results_iterator, _ = self.session._execute(self.expr, sorted=True) for arrow_table in results_iterator.to_arrow_iterable( - bqstorage_client=self._expr.session.bqstoragereadclient + bqstorage_client=self.session.bqstoragereadclient ): df = bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) self._copy_index_to_pandas(df) @@ -460,12 +475,12 @@ def _compute_and_count( expr = self._apply_value_keys_to_expr(value_keys=value_keys) - results_iterator, query_job = expr.start_query( - max_results=max_results, sorted=ordered + results_iterator, query_job = self.session._execute( + expr, max_results=max_results, sorted=ordered ) table_size = ( - expr.session._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES + self.session._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES ) fraction = ( max_download_size / table_size @@ -607,7 +622,7 @@ def _compute_dry_run( ) -> bigquery.QueryJob: expr = self._apply_value_keys_to_expr(value_keys=value_keys) job_config = bigquery.QueryJobConfig(dry_run=True) - _, query_job = expr.start_query(job_config=job_config) + _, query_job = self.session._execute(expr, job_config=job_config, dry_run=True) return query_job def _apply_value_keys_to_expr(self, value_keys: Optional[Iterable[str]] = None): @@ -1668,7 +1683,7 @@ def to_sql_query( # the BigQuery unicode column name feature? substitutions[old_id] = new_id - sql = array_value.to_sql(col_id_overrides=substitutions) + sql = self.session._to_sql(array_value, col_id_overrides=substitutions) return ( sql, new_ids[: len(idx_labels)], @@ -1678,7 +1693,7 @@ def to_sql_query( def cached(self) -> Block: """Write the block to a session table and create a new block object that references it.""" return Block( - self.expr.cached(cluster_cols=self.index_columns), + self.session._execute_and_cache(self.expr, cluster_cols=self.index_columns), index_columns=self.index_columns, column_labels=self.column_labels, index_labels=self.index_labels, diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 537d9c8b527..d6183228d1e 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -268,6 +268,22 @@ def to_sql( ) return typing.cast(str, sql) + def row_count(self) -> OrderedIR: + original_table = self._to_ibis_expr() + ibis_table = original_table.agg( + [ + original_table.count().name("count"), + ] + ) + return OrderedIR( + ibis_table, + (ibis_table["count"],), + ordering=ExpressionOrdering( + ordering_value_columns=(OrderingColumnReference("count"),), + total_ordering_columns=frozenset(["count"]), + ), + ) + def _to_ibis_expr( self, *, diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 39892635f1e..17dcde638fa 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -173,6 +173,12 @@ def compile_concat(node: nodes.ConcatNode, ordered: bool = True): return concat_impl.concat_unordered(compiled_unordered) +@_compile_node.register +def compile_rowcount(node: nodes.RowCountNode, ordered: bool = True): + result = compile_unordered(node.child).row_count() + return result if ordered else result.to_unordered() + + @_compile_node.register def compile_aggregate(node: nodes.AggregateNode, ordered: bool = True): result = compile_unordered(node.child).aggregate( diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index fc7cf167d49..6fc284403d2 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -396,6 +396,10 @@ def dtypes( ) -> typing.Sequence[typing.Union[bf_dtypes.Dtype, np.dtype[typing.Any]]]: return self._block.index_dtypes + @property + def session(self) -> core.Session: + return self._expr.session + def __repr__(self) -> str: """Converts an Index to a string.""" # TODO(swast): Add a timeout here? If the query is taking a long time, @@ -411,7 +415,7 @@ def to_pandas(self) -> pandas.Index: index_columns = list(self._block.index_columns) dtypes = dict(zip(index_columns, self.dtypes)) expr = self._expr.select_columns(index_columns) - results, _ = expr.start_query() + results, _ = self.session._execute(expr) df = expr.session._rows_to_dataframe(results, dtypes) df = df.set_index(index_columns) index = df.index diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 82a869dac20..30444f55657 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -139,7 +139,7 @@ class ReadGbqNode(BigFrameNode): @property def session(self): - return (self.table_session,) + return self.table_session def __hash__(self): return self._node_hash @@ -229,6 +229,12 @@ def __hash__(self): return self._node_hash +# TODO: Merge RowCount and Corr into Aggregate Node +@dataclass(frozen=True) +class RowCountNode(UnaryNode): + pass + + @dataclass(frozen=True) class AggregateNode(UnaryNode): aggregations: typing.Tuple[typing.Tuple[str, agg_ops.AggregateOp, str], ...] diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 3b0fd7008ab..d777784f644 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2701,7 +2701,8 @@ def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: if ordering_id is not None: array_value = array_value.promote_offsets(ordering_id) - return array_value.to_sql( + return self._block.session._to_sql( + array_value=array_value, col_id_overrides=id_overrides, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index fb5fab86ce3..a57f7b94c5a 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -67,6 +67,7 @@ from bigframes.core import log_adapter import bigframes.core as core import bigframes.core.blocks as blocks +import bigframes.core.compile import bigframes.core.guid as guid from bigframes.core.ordering import IntegerEncoding, OrderingColumnReference import bigframes.core.ordering as orderings @@ -1437,6 +1438,81 @@ def _start_query( results_iterator = query_job.result(max_results=max_results) return results_iterator, query_job + def _execute_and_cache( + self, array_value: core.ArrayValue, cluster_cols: typing.Sequence[str] + ) -> core.ArrayValue: + """Executes the query and uses the resulting table to rewrite future executions.""" + # TODO: Use this for all executions? Problem is that caching materializes extra + # ordering columns + compiled_value = self._compile_ordered(array_value) + + ibis_expr = compiled_value._to_ibis_expr( + ordering_mode="unordered", expose_hidden_cols=True + ) + tmp_table = self._ibis_to_temp_table( + ibis_expr, cluster_cols=cluster_cols, api_name="cached" + ) + table_expression = self.ibis_client.table( + f"{tmp_table.project}.{tmp_table.dataset_id}.{tmp_table.table_id}" + ) + new_columns = [table_expression[column] for column in compiled_value.column_ids] + new_hidden_columns = [ + table_expression[column] + for column in compiled_value._hidden_ordering_column_names + ] + # TODO: Instead, keep session-wide map of cached results and automatically reuse + return core.ArrayValue.from_ibis( + self, + table_expression, + columns=new_columns, + hidden_ordering_columns=new_hidden_columns, + ordering=compiled_value._ordering, + ) + + def _execute( + self, + array_value: core.ArrayValue, + job_config: Optional[bigquery.job.QueryJobConfig] = None, + max_results: Optional[int] = None, + *, + sorted: bool = True, + dry_run=False, + ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + sql = self._to_sql(array_value, sorted=sorted) # type:ignore + job_config = bigquery.QueryJobConfig(dry_run=dry_run) + return self._start_query( + sql=sql, + job_config=job_config, + max_results=max_results, + ) + + def _to_sql( + self, + array_value: core.ArrayValue, + offset_column: typing.Optional[str] = None, + col_id_overrides: typing.Mapping[str, str] = {}, + sorted: bool = False, + ) -> str: + if offset_column: + array_value = array_value.promote_offsets(offset_column) + if sorted: + return self._compile_ordered(array_value).to_sql( + col_id_overrides=col_id_overrides, sorted=True + ) + return self._compile_unordered(array_value).to_sql( + col_id_overrides=col_id_overrides + ) + + def _compile_ordered( + self, array_value: core.ArrayValue + ) -> bigframes.core.compile.OrderedIR: + return bigframes.core.compile.compile_ordered(array_value.node) + + def _compile_unordered( + self, array_value: core.ArrayValue + ) -> bigframes.core.compile.UnorderedIR: + return bigframes.core.compile.compile_unordered(array_value.node) + def _get_table_size(self, destination_table): table = self.bqclient.get_table(destination_table) return table.num_bytes diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 26c5093b352..e6eb40a5fa1 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -44,7 +44,7 @@ def test_read_gbq_tokyo( result = df.sort_index().to_pandas() expected = scalars_pandas_df_index - _, query_job = df._block.expr.start_query() + _, query_job = session_tokyo._execute(df._block.expr) assert query_job.location == tokyo_location pd.testing.assert_frame_equal(result, expected) @@ -379,7 +379,7 @@ def test_read_pandas_tokyo( result = df.to_pandas() expected = scalars_pandas_df_index - _, query_job = df._block.expr.start_query() + _, query_job = session_tokyo._execute(df._block.expr) assert query_job.location == tokyo_location pd.testing.assert_frame_equal(result, expected) From 6c1969a35fe720cf3a804006bcc9046ba554fcc3 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:34:37 -0800 Subject: [PATCH 223/921] feat: Add IntervalIndex support to bigframes.pandas.cut (#254) * feature: Add IntervalIndex support to bigframes.pandas.cut * add bins <= 0 error in CutOp * add type ignore * add type ignore to session --------- Co-authored-by: Shobhit Singh --- bigframes/core/reshape/__init__.py | 20 +++++-- bigframes/operations/aggregations.py | 41 ++++++++++----- bigframes/series.py | 2 +- bigframes/session/__init__.py | 4 +- tests/system/small/test_pandas.py | 34 ++++++++++++ .../pandas/core/reshape/tile.py | 52 +++++++++++++++---- 6 files changed, 124 insertions(+), 29 deletions(-) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index 24c1bff309d..d9cc99a0366 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -14,7 +14,9 @@ from __future__ import annotations import typing -from typing import Iterable, Literal, Optional, Union +from typing import Iterable, Literal, Optional, Tuple, Union + +import pandas as pd import bigframes.constants as constants import bigframes.core as core @@ -108,17 +110,29 @@ def concat( def cut( x: bigframes.series.Series, - bins: int, + bins: Union[ + int, + pd.IntervalIndex, + Iterable[Tuple[Union[int, float], Union[int, float]]], + ], *, labels: Optional[bool] = None, ) -> bigframes.series.Series: - if bins <= 0: + if isinstance(bins, int) and bins <= 0: raise ValueError("`bins` should be a positive integer.") + if isinstance(bins, Iterable): + if not isinstance(bins, pd.IntervalIndex): + bins = pd.IntervalIndex.from_tuples(list(bins)) + + if bins.is_overlapping: + raise ValueError("Overlapping IntervalIndex is not accepted.") + if labels is not False: raise NotImplementedError( f"Only labels=False is supported in BigQuery DataFrames so far. {constants.FEEDBACK_LINK}" ) + return x._apply_window_op(agg_ops.CutOp(bins), window_spec=core.WindowSpec()) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 363dfe819d0..8178ebfaea5 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -20,6 +20,7 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types from pandas import Int64Dtype +import pandas as pd import bigframes.constants as constants import bigframes.dtypes as dtypes @@ -228,21 +229,37 @@ def skips_nulls(self): class CutOp(WindowOp): - def __init__(self, bins: int): - self._bins_ibis = dtypes.literal_to_ibis_scalar(bins, force_dtype=Int64Dtype()) - self._bins_int = bins + def __init__(self, bins: typing.Union[int, pd.IntervalIndex]): + if isinstance(bins, int): + if not bins > 0: + raise ValueError("`bins` should be a positive integer.") + self._bins_int = bins + self._bins = dtypes.literal_to_ibis_scalar(bins, force_dtype=Int64Dtype()) + else: + self._bins_int = 0 + self._bins = bins def _as_ibis(self, x: ibis_types.Column, window=None): - col_min = _apply_window_if_present(x.min(), window) - col_max = _apply_window_if_present(x.max(), window) - bin_width = (col_max - col_min) / self._bins_ibis out = ibis.case() - for this_bin in range(self._bins_int - 1): - out = out.when( - x <= (col_min + (this_bin + 1) * bin_width), - dtypes.literal_to_ibis_scalar(this_bin, force_dtype=Int64Dtype()), - ) - out = out.when(x.notnull(), self._bins_ibis - 1) + + if self._bins_int > 0: + col_min = _apply_window_if_present(x.min(), window) + col_max = _apply_window_if_present(x.max(), window) + bin_width = (col_max - col_min) / self._bins + + for this_bin in range(self._bins_int - 1): + out = out.when( + x <= (col_min + (this_bin + 1) * bin_width), + dtypes.literal_to_ibis_scalar(this_bin, force_dtype=Int64Dtype()), + ) + out = out.when(x.notnull(), self._bins - 1) + else: + for interval in self._bins: + condition = (x > interval.left) & (x <= interval.right) + interval_struct = ibis.struct( + {"left_exclusive": interval.left, "right_inclusive": interval.right} + ) + out = out.when(condition, interval_struct) return out.end() @property diff --git a/bigframes/series.py b/bigframes/series.py index c929775a000..8d8c711c923 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1324,7 +1324,7 @@ def to_csv(self, path_or_buf=None, **kwargs) -> typing.Optional[str]: return self.to_pandas().to_csv(path_or_buf, **kwargs) def to_dict(self, into: type[dict] = dict) -> typing.Mapping: - return typing.cast(dict, self.to_pandas().to_dict(into)) + return typing.cast(dict, self.to_pandas().to_dict(into)) # type: ignore def to_excel(self, excel_writer, sheet_name="Sheet1", **kwargs) -> None: return self.to_pandas().to_excel(excel_writer, sheet_name, **kwargs) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index a57f7b94c5a..fbe900106ab 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1017,13 +1017,13 @@ def read_csv( header=header, names=names, index_col=index_col, - usecols=usecols, + usecols=usecols, # type: ignore dtype=dtype, engine=engine, encoding=encoding, **kwargs, ) - return self.read_pandas(pandas_df) + return self.read_pandas(pandas_df) # type: ignore def read_pickle( self, diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index a1079288cfb..282c0d68ebb 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -365,6 +365,40 @@ def test_cut(scalars_dfs): pd.testing.assert_series_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("bins",), + [ + ([(-5, 2), (2, 3), (-3000, -10)],), + (pd.IntervalIndex.from_tuples([(1, 2), (2, 3), (4, 5)]),), + ], +) +def test_cut_with_interval(scalars_dfs, bins): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = bpd.cut(scalars_df["int64_too"], bins, labels=False).to_pandas() + + if isinstance(bins, list): + bins = pd.IntervalIndex.from_tuples(bins) + pd_result = pd.cut(scalars_pandas_df["int64_too"], bins, labels=False) + + # Convert to match data format + pd_result_converted = pd.Series( + [ + {"left_exclusive": interval.left, "right_inclusive": interval.right} + if pd.notna(val) + else pd.NA + for val, interval in zip( + pd_result, pd_result.cat.categories[pd_result.cat.codes] + ) + ], + name=pd_result.name, + ) + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal( + bf_result, pd_result_converted, check_index=False, check_dtype=False + ) + + @pytest.mark.parametrize( ("q",), [ diff --git a/third_party/bigframes_vendored/pandas/core/reshape/tile.py b/third_party/bigframes_vendored/pandas/core/reshape/tile.py index d4471ed68e4..55975c3fc1c 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/tile.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/tile.py @@ -24,31 +24,61 @@ def cut( ``labels=False`` implies you just want the bins back. - Examples: - - .. code-block:: - - import bigframes.pandas as pd + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series([0, 1, 5, 10]) + >>> s + 0 0 + 1 1 + 2 5 + 3 10 + dtype: Int64 - pd.options.display.progress_bar = None - s = pd.Series([0, 1, 1, 2]) - pd.cut(s, bins=4, labels=False) + Cut with an integer (equal-width bins): + >>> bpd.cut(s, bins=4, labels=False) 0 0 - 1 1 + 1 0 2 1 3 3 dtype: Int64 + Cut with pd.IntervalIndex, requires importing pandas for IntervalIndex: + + >>> import pandas as pd + + >>> interval_index = pd.IntervalIndex.from_tuples([(0, 1), (1, 5), (5, 20)]) + >>> bpd.cut(s, bins=interval_index, labels=False) + 0 + 1 {'left_exclusive': 0, 'right_inclusive': 1} + 2 {'left_exclusive': 1, 'right_inclusive': 5} + 3 {'left_exclusive': 5, 'right_inclusive': 20} + dtype: struct[pyarrow] + + Cut with an iterable of tuples: + + >>> bins_tuples = [(0, 1), (1, 4), (5, 20)] + >>> bpd.cut(s, bins=bins_tuples, labels=False) + 0 + 1 {'left_exclusive': 0, 'right_inclusive': 1} + 2 + 3 {'left_exclusive': 5, 'right_inclusive': 20} + dtype: struct[pyarrow] + Args: x (Series): The input Series to be binned. Must be 1-dimensional. - bins (int): + bins (int, pd.IntervalIndex, Iterable[Tuple[Union[int, float], Union[int, float]]]): The criteria to bin by. - int : Defines the number of equal-width bins in the range of `x`. The + int: Defines the number of equal-width bins in the range of `x`. The range of `x` is extended by .1% on each side to include the minimum and maximum values of `x`. + + pd.IntervalIndex or Iterable of tuples: Defines the exact bins to be used. + It's important to ensure that these bins are non-overlapping. labels (None): Specifies the labels for the returned bins. Must be the same length as the resulting bins. If False, returns only integer indicators of the From bac62f76af1af6ca8834c3690c7c79aeb12dd331 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:47:39 -0800 Subject: [PATCH 224/921] fix: dataframes to_gbq now creates dataset if it doesn't exist (#222) * "fix: dataframes to_gbq now creates dataset if it doesn't exist * fix: dataframes to_gbq now creates dataset if it doesn't exist * fix: dataframes to_gbq now creates dataset if it doesn't exist * update test * update create dataset method. * fix --------- Co-authored-by: Shobhit Singh --- bigframes/dataframe.py | 17 +++++++++++++++-- tests/system/small/test_dataframe.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d777784f644..1251e64fb0f 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -34,6 +34,7 @@ Union, ) +import google.api_core.exceptions import google.cloud.bigquery as bigquery import numpy import pandas @@ -2508,7 +2509,14 @@ def to_gbq( ) if_exists = "replace" - if "." not in destination_table: + table_parts = destination_table.split(".") + default_project = self._block.expr.session.bqclient.project + + if len(table_parts) == 2: + destination_dataset = f"{default_project}.{table_parts[0]}" + elif len(table_parts) == 3: + destination_dataset = f"{table_parts[0]}.{table_parts[1]}" + else: raise ValueError( f"Got invalid value for destination_table {repr(destination_table)}. " "Should be of the form 'datasetId.tableId' or 'projectId.datasetId.tableId'." @@ -2523,11 +2531,16 @@ def to_gbq( f"Valid options include None or one of {dispositions.keys()}." ) + try: + self._session.bqclient.get_dataset(destination_dataset) + except google.api_core.exceptions.NotFound: + self._session.bqclient.create_dataset(destination_dataset, exists_ok=True) + job_config = bigquery.QueryJobConfig( write_disposition=dispositions[if_exists], destination=bigquery.table.TableReference.from_string( destination_table, - default_project=self._block.expr.session.bqclient.project, + default_project=default_project, ), ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 663a7ceb496..ab68543d91f 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3683,3 +3683,18 @@ def test_to_pandas_downsampling_option_override(session): total_memory_bytes = df.memory_usage(deep=True).sum() total_memory_mb = total_memory_bytes / (1024 * 1024) assert total_memory_mb == pytest.approx(download_size, rel=0.3) + + +def test_to_gbq_and_create_dataset(session, scalars_df_index, dataset_id_not_created): + dataset_id = dataset_id_not_created + destination_table = f"{dataset_id}.scalars_df" + + result_table = scalars_df_index.to_gbq(destination_table) + assert ( + result_table == destination_table + if destination_table + else result_table is not None + ) + + loaded_scalars_df_index = session.read_gbq(result_table) + assert not loaded_scalars_df_index.empty From dab2f2cb8720ec68413a71bddaeadc1ec40b6541 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 19 Dec 2023 08:27:16 -0600 Subject: [PATCH 225/921] chore: use latest pip in nightly builds (#281) * chore: use latest pip in nightly builds * restrict ibis version * update prerelease tests too --- .kokoro/release-nightly.sh | 3 +++ noxfile.py | 2 +- setup.py | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh index 0751cf2502c..5624df3b8d9 100755 --- a/.kokoro/release-nightly.sh +++ b/.kokoro/release-nightly.sh @@ -55,6 +55,9 @@ rm -rf build dist # internal issue b/261050975. git config --global --add safe.directory "${PROJECT_ROOT}" +# Workaround for older pip not able to resolve dependencies. See internal +# issue 316909553. +python3.10 -m pip install pip==23.3.2 python3.10 -m pip install --require-hashes -r .kokoro/requirements.txt # Disable buffering, so that the logs stream through. diff --git a/noxfile.py b/noxfile.py index c0ec3b0c543..c4bbd7a65a6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -535,7 +535,7 @@ def prerelease(session: nox.sessions.Session, tests_path): session.install( "--upgrade", # "--pre", - "ibis-framework>=7.1.0,<8.0.0dev", + "ibis-framework>=7.1.0,<7.2.0dev", ) already_installed.add("ibis-framework") diff --git a/setup.py b/setup.py index 1ad4bbd3eb1..9aaaaae04fc 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,8 @@ "google-cloud-iam >=2.12.1", "google-cloud-resource-manager >=1.10.3", "google-cloud-storage >=2.0.0", - "ibis-framework[bigquery] >=7.1.0,<8.0.0dev", + # TODO: Relax upper bound once we have fixed unit tests with 7.2.0. + "ibis-framework[bigquery] >=7.1.0,<7.2.0dev", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. "pandas >=1.5.0,<2.1.4", "pydata-google-auth >=1.8.2", From 5092215767d77c90b132e9cd6b3e3749827ebe09 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 19 Dec 2023 11:47:25 -0800 Subject: [PATCH 226/921] feat: add replace method to DataFrame (#261) * feat: add replace method to DataFrame * remove unwanted change to describe method * better docs * is_patype docstring * docstring fix * mypy fix --- bigframes/dataframe.py | 15 ++++ bigframes/dtypes.py | 48 ++++++++++ bigframes/operations/__init__.py | 14 +++ bigframes/series.py | 87 +++++++++++------- tests/system/small/test_dataframe.py | 44 ++++++++++ .../bigframes_vendored/pandas/core/frame.py | 88 +++++++++++++++++++ 6 files changed, 265 insertions(+), 31 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1251e64fb0f..1d8169960b3 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1562,6 +1562,21 @@ def interpolate(self, method: str = "linear") -> DataFrame: def fillna(self, value=None) -> DataFrame: return self._apply_binop(value, ops.fillna_op, how="left") + def replace( + self, to_replace: typing.Any, value: typing.Any = None, *, regex: bool = False + ): + if utils.is_dict_like(value): + return self.apply( + lambda x: x.replace( + to_replace=to_replace, value=value[x.name], regex=regex + ) + if (x.name in value) + else x + ) + return self.apply( + lambda x: x.replace(to_replace=to_replace, value=value, regex=regex) + ) + def ffill(self, *, limit: typing.Optional[int] = None) -> DataFrame: window = bigframes.core.WindowSpec(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 774eb74d06a..6dfcc17f376 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -14,6 +14,7 @@ """Mappings for Pandas dtypes supported by BigQuery DataFrames package""" +import datetime import textwrap import typing from typing import Any, Dict, Iterable, Literal, Tuple, Union @@ -437,3 +438,50 @@ def to_pandas_dtypes_overrides(schema: Iterable[bigquery.SchemaField]) -> Dict: gcb3p_pandas_helpers.bq_to_arrow_data_type(field) ) return dtypes + + +def is_dtype(scalar: typing.Any, dtype: Dtype) -> bool: + """Captures whether a scalar can be losslessly represented by a dtype.""" + if scalar is None: + return True + if pd.api.types.is_bool_dtype(dtype): + return pd.api.types.is_bool(scalar) + if pd.api.types.is_float_dtype(dtype): + return pd.api.types.is_float(scalar) + if pd.api.types.is_integer_dtype(dtype): + return pd.api.types.is_integer(scalar) + if isinstance(dtype, pd.StringDtype): + return isinstance(scalar, str) + if isinstance(dtype, pd.ArrowDtype): + pa_type = dtype.pyarrow_dtype + return is_patype(scalar, pa_type) + return False + + +def is_patype(scalar: typing.Any, pa_type: pa.DataType) -> bool: + """Determine whether a scalar's type matches a given pyarrow type.""" + if pa_type == pa.time64("us"): + return isinstance(scalar, datetime.time) + if pa_type == pa.timestamp("us"): + if isinstance(scalar, datetime.datetime): + return not scalar.tzinfo + if isinstance(scalar, pd.Timestamp): + return not scalar.tzinfo + if pa_type == pa.timestamp("us", tz="UTC"): + if isinstance(scalar, datetime.datetime): + return scalar.tzinfo == datetime.timezone.utc + if isinstance(scalar, pd.Timestamp): + return scalar.tzinfo == datetime.timezone.utc + if pa_type == pa.date32(): + return isinstance(scalar, datetime.date) + return False + + +def is_comparable(scalar: typing.Any, dtype: Dtype) -> bool: + """Whether scalar can be compare to items of dtype (though maybe requiring coercion)""" + if is_dtype(scalar, dtype): + return True + elif pd.api.types.is_numeric_dtype(dtype): + return pd.api.types.is_number(scalar) + else: + return False diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 0655aafdb37..753870a42d5 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -523,6 +523,20 @@ def _as_ibis(self, x: ibis_types.Value): return bigframes.dtypes.cast_ibis_value(x, self.to_type) +class MapOp(UnaryOp): + def __init__( + self, + mappings: typing.Tuple[typing.Tuple[typing.Hashable, typing.Hashable], ...], + ): + self._mappings = mappings + + def _as_ibis(self, x: ibis_types.Value): + case = ibis.case() + for mapping in self._mappings: + case = case.when(x == mapping[0], mapping[1]) + return case.else_(x).end() + + class FindOp(UnaryOp): def __init__(self, sub, start, end): self._sub = sub diff --git a/bigframes/series.py b/bigframes/series.py index 8d8c711c923..1b9982877a6 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -442,42 +442,67 @@ def replace( self, to_replace: typing.Any, value: typing.Any = None, *, regex: bool = False ): if regex: - if not (isinstance(to_replace, str) and isinstance(value, str)): - raise NotImplementedError( - f"replace regex mode only supports strings for 'to_replace' and 'value'. {constants.FEEDBACK_LINK}" - ) - block, result_col = self._block.apply_unary_op( - self._value_column, - ops.ReplaceRegexOp(to_replace, value), - result_label=self.name, - ) - return Series(block.select_column(result_col)) + # No-op unless to_replace and series dtype are both string type + if not isinstance(to_replace, str) or not isinstance( + self.dtype, pandas.StringDtype + ): + return self + return self._regex_replace(to_replace, value) elif utils.is_dict_like(to_replace): - raise NotImplementedError( - f"Dict 'to_replace' not supported. {constants.FEEDBACK_LINK}" - ) + return self._mapping_replace(to_replace) # type: ignore elif utils.is_list_like(to_replace): - block, cond = self._block.apply_unary_op( - self._value_column, ops.IsInOp(to_replace) - ) - block, result_col = block.apply_binary_op( - cond, - self._value_column, - ops.partial_arg1(ops.where_op, value), - result_label=self.name, - ) - return Series(block.select_column(result_col)) + replace_list = to_replace else: # Scalar - block, cond = self._block.apply_unary_op( - self._value_column, ops.BinopPartialLeft(ops.eq_op, to_replace) + replace_list = [to_replace] + replace_list = [ + i for i in replace_list if bigframes.dtypes.is_comparable(i, self.dtype) + ] + return self._simple_replace(replace_list, value) if replace_list else self + + def _regex_replace(self, to_replace: str, value: str): + if not bigframes.dtypes.is_dtype(value, self.dtype): + raise NotImplementedError( + f"Cannot replace {self.dtype} elements with incompatible item {value} as mixed-type columns not supported. {constants.FEEDBACK_LINK}" ) - block, result_col = block.apply_binary_op( - cond, - self._value_column, - ops.partial_arg1(ops.where_op, value), - result_label=self.name, + block, result_col = self._block.apply_unary_op( + self._value_column, + ops.ReplaceRegexOp(to_replace, value), + result_label=self.name, + ) + return Series(block.select_column(result_col)) + + def _simple_replace(self, to_replace_list: typing.Sequence, value): + if not bigframes.dtypes.is_dtype(value, self.dtype): + raise NotImplementedError( + f"Cannot replace {self.dtype} elements with incompatible item {value} as mixed-type columns not supported. {constants.FEEDBACK_LINK}" ) - return Series(block.select_column(result_col)) + + block, cond = self._block.apply_unary_op( + self._value_column, ops.IsInOp(to_replace_list) + ) + block, result_col = block.apply_binary_op( + cond, + self._value_column, + ops.partial_arg1(ops.where_op, value), + result_label=self.name, + ) + return Series(block.select_column(result_col)) + + def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): + tuples = [] + for key, value in mapping.items(): + if not bigframes.dtypes.is_comparable(key, self.dtype): + continue + if not bigframes.dtypes.is_dtype(value, self.dtype): + raise NotImplementedError( + f"Cannot replace {self.dtype} elements with incompatible item {value} as mixed-type columns not supported. {constants.FEEDBACK_LINK}" + ) + tuples.append((key, value)) + + block, result = self._block.apply_unary_op( + self._value_column, ops.MapOp(tuple(tuples)) + ) + return Series(block.select_column(result)) def interpolate(self, method: str = "linear") -> Series: if method == "pad": diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ab68543d91f..ed78e73e5df 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -839,6 +839,50 @@ def test_df_fillna(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_df_replace_scalar_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df.replace("Hello, World!", "Howdy, Planet!").to_pandas() + pd_result = scalars_pandas_df.replace("Hello, World!", "Howdy, Planet!") + + pd.testing.assert_frame_equal( + pd_result, + bf_result, + ) + + +def test_df_replace_regex_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df.replace("^H.l", "Howdy, Planet!", regex=True).to_pandas() + pd_result = scalars_pandas_df.replace("^H.l", "Howdy, Planet!", regex=True) + + pd.testing.assert_frame_equal( + pd_result, + bf_result, + ) + + +def test_df_replace_list_scalar(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df.replace(["Hello, World!", "T"], "Howdy, Planet!").to_pandas() + pd_result = scalars_pandas_df.replace(["Hello, World!", "T"], "Howdy, Planet!") + + pd.testing.assert_frame_equal( + pd_result, + bf_result, + ) + + +def test_df_replace_value_dict(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df.replace(1, {"int64_col": 100, "int64_too": 200}).to_pandas() + pd_result = scalars_pandas_df.replace(1, {"int64_col": 100, "int64_too": 200}) + + pd.testing.assert_frame_equal( + pd_result, + bf_result, + ) + + def test_df_ffill(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df[["int64_col", "float64_col"]].ffill(limit=1).to_pandas() diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index c082b873367..00be9e5e9ee 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4356,6 +4356,94 @@ def fillna(self, value): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def replace( + self, + to_replace, + value=None, + *, + regex=False, + ): + """ + Replace values given in `to_replace` with `value`. + + Values of the Series/DataFrame are replaced with other values dynamically. + This differs from updating with ``.loc`` or ``.iloc``, which require + you to specify a location to update with some value. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'int_col': [1, 1, 2, 3], + ... 'string_col': ["a", "b", "c", "b"], + ... }) + + Using scalar `to_replace` and `value`: + + >>> df.replace("b", "e") + int_col string_col + 0 1 a + 1 1 e + 2 2 c + 3 3 e + + [4 rows x 2 columns] + + Using dictionary: + + >>> df.replace({"a": "e", 2: 5}) + int_col string_col + 0 1 e + 1 1 b + 2 5 c + 3 3 b + + [4 rows x 2 columns] + + Using regex: + + >>> df.replace("[ab]", "e", regex=True) + int_col string_col + 0 1 e + 1 1 e + 2 2 c + 3 3 e + + [4 rows x 2 columns] + + + Args: + to_replace (str, regex, list, int, float or None): + How to find the values that will be replaced. + numeric: numeric values equal to `to_replace` will be replaced with `value` + str: string exactly matching `to_replace` will be replaced with `value` + regex: regexs matching `to_replace` will be replaced with`value` + list of str, regex, or numeric: + First, if `to_replace` and `value` are both lists, they **must** be the same length. + Second, if ``regex=True`` then all of the strings in **both** + lists will be interpreted as regexs otherwise they will match + directly. This doesn't matter much for `value` since there + are only a few possible substitution regexes you can use. + str, regex and numeric rules apply as above. + + value (scalar, default None): + Value to replace any values matching `to_replace` with. + For a DataFrame a dict of values can be used to specify which + value to use for each column (columns not in the dict will not be + filled). Regular expressions, strings and lists or dicts of such + objects are also allowed. + regex (bool, default False): + Whether to interpret `to_replace` and/or `value` as regular + expressions. If this is ``True`` then `to_replace` *must* be a + string. + + Returns: + Series/DataFrame: Object after replacement. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property def iloc(self): """Purely integer-location based indexing for selection by position.""" From ab493506e71ed8970a11fe2f88b2145150e09291 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 19 Dec 2023 12:32:15 -0800 Subject: [PATCH 227/921] fix: fix DataFrameGroupby.agg() issue with as_index=False (#273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #271 🦕 --- bigframes/core/block_transforms.py | 1 - bigframes/core/blocks.py | 53 ++++++++------------------- bigframes/core/groupby/__init__.py | 30 ++++++++++----- bigframes/dataframe.py | 8 ++-- bigframes/series.py | 16 ++++---- tests/system/small/test_groupby.py | 17 +++++++-- tests/system/small/test_multiindex.py | 11 +++++- 7 files changed, 70 insertions(+), 66 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index df84f70859b..66548922875 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -332,7 +332,6 @@ def value_counts( by_column_ids=columns, aggregations=[(dummy, agg_ops.count_op)], dropna=dropna, - as_index=True, ) count_id = agg_ids[0] if normalize: diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 6542b694d29..3163aa5b096 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -66,7 +66,7 @@ _MONOTONIC_DECREASING = "monotonic_decreasing" -LevelType = typing.Union[str, int] +LevelType = typing.Hashable LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] @@ -941,7 +941,6 @@ def aggregate( by_column_ids: typing.Sequence[str] = (), aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp]] = (), *, - as_index: bool = True, dropna: bool = True, ) -> typing.Tuple[Block, typing.Sequence[str]]: """ @@ -962,40 +961,21 @@ def aggregate( aggregate_labels = self._get_labels_for_columns( [agg[0] for agg in aggregations] ) - if as_index: - names: typing.List[Label] = [] - for by_col_id in by_column_ids: - if by_col_id in self.value_columns: - names.append(self.col_id_to_label[by_col_id]) - else: - names.append(self.col_id_to_index_name[by_col_id]) - return ( - Block( - result_expr, - index_columns=by_column_ids, - column_labels=aggregate_labels, - index_labels=names, - ), - output_col_ids, - ) - else: # as_index = False - # If as_index=False, drop grouping levels, but keep grouping value columns - by_value_columns = [ - col for col in by_column_ids if col in self.value_columns - ] - by_column_labels = self._get_labels_for_columns(by_value_columns) - labels = (*by_column_labels, *aggregate_labels) - offsets_id = guid.generate_guid() - result_expr_pruned = result_expr.select_columns( - [*by_value_columns, *output_col_ids] - ).promote_offsets(offsets_id) - - return ( - Block( - result_expr_pruned, index_columns=[offsets_id], column_labels=labels - ), - output_col_ids, - ) + names: typing.List[Label] = [] + for by_col_id in by_column_ids: + if by_col_id in self.value_columns: + names.append(self.col_id_to_label[by_col_id]) + else: + names.append(self.col_id_to_index_name[by_col_id]) + return ( + Block( + result_expr, + index_columns=by_column_ids, + column_labels=aggregate_labels, + index_labels=names, + ), + output_col_ids, + ) def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): """Gets aggregates immediately, and caches it""" @@ -1324,7 +1304,6 @@ def pivot( result_block, _ = block.aggregate( by_column_ids=self.index_columns, aggregations=aggregations, - as_index=True, dropna=True, ) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index a8b8afdae7b..3ee46ef6758 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -263,10 +263,10 @@ def _agg_string(self, func: str) -> df.DataFrame: agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, - as_index=self._as_index, dropna=self._dropna, ) - return df.DataFrame(agg_block) + dataframe = df.DataFrame(agg_block) + return dataframe if self._as_index else self._convert_index(dataframe) def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: aggregations: typing.List[typing.Tuple[str, agg_ops.AggregateOp]] = [] @@ -285,7 +285,6 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, - as_index=self._as_index, dropna=self._dropna, ) if want_aggfunc_level: @@ -297,7 +296,8 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: ) else: agg_block = agg_block.with_column_labels(pd.Index(column_labels)) - return df.DataFrame(agg_block) + dataframe = df.DataFrame(agg_block) + return dataframe if self._as_index else self._convert_index(dataframe) def _agg_list(self, func: typing.Sequence) -> df.DataFrame: aggregations = [ @@ -311,7 +311,6 @@ def _agg_list(self, func: typing.Sequence) -> df.DataFrame: agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, - as_index=self._as_index, dropna=self._dropna, ) agg_block = agg_block.with_column_labels( @@ -319,7 +318,8 @@ def _agg_list(self, func: typing.Sequence) -> df.DataFrame: column_labels, names=[*self._block.column_labels.names, None] ) ) - return df.DataFrame(agg_block) + dataframe = df.DataFrame(agg_block) + return dataframe if self._as_index else self._convert_index(dataframe) def _agg_named(self, **kwargs) -> df.DataFrame: aggregations = [] @@ -339,11 +339,21 @@ def _agg_named(self, **kwargs) -> df.DataFrame: agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, - as_index=self._as_index, dropna=self._dropna, ) agg_block = agg_block.with_column_labels(column_labels) - return df.DataFrame(agg_block) + dataframe = df.DataFrame(agg_block) + return dataframe if self._as_index else self._convert_index(dataframe) + + def _convert_index(self, dataframe: df.DataFrame): + """Convert index levels to columns except where names conflict.""" + levels_to_drop = [ + level for level in dataframe.index.names if level in dataframe.columns + ] + + if len(levels_to_drop) == dataframe.index.nlevels: + return dataframe.reset_index(drop=True) + return dataframe.droplevel(levels_to_drop).reset_index(drop=False) aggregate = agg @@ -379,10 +389,10 @@ def _aggregate_all( result_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, - as_index=self._as_index, dropna=self._dropna, ) - return df.DataFrame(result_block) + dataframe = df.DataFrame(result_block) + return dataframe if self._as_index else self._convert_index(dataframe) def _apply_window_op( self, diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1d8169960b3..98aa8f1185b 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -72,7 +72,7 @@ # TODO(tbergeron): Convert to bytes-based limit MAX_INLINE_DF_SIZE = 5000 -LevelType = typing.Union[str, int] +LevelType = typing.Hashable LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] SingleItemValue = Union[bigframes.series.Series, int, float, Callable] @@ -1956,7 +1956,7 @@ def _stack_mono(self): def _stack_multi(self, level: LevelsType = -1): n_levels = self.columns.nlevels - if isinstance(level, int) or isinstance(level, str): + if not utils.is_list_like(level): level = [level] level_indices = [] for level_ref in level: @@ -1966,7 +1966,7 @@ def _stack_multi(self, level: LevelsType = -1): else: level_indices.append(level_ref) else: # str - level_indices.append(self.columns.names.index(level_ref)) + level_indices.append(self.columns.names.index(level_ref)) # type: ignore new_order = [ *[i for i in range(n_levels) if i not in level_indices], @@ -1982,7 +1982,7 @@ def _stack_multi(self, level: LevelsType = -1): return DataFrame(block) def unstack(self, level: LevelsType = -1): - if isinstance(level, int) or isinstance(level, str): + if not utils.is_list_like(level): level = [level] block = self._block diff --git a/bigframes/series.py b/bigframes/series.py index 1b9982877a6..6837c1c7f8d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -841,7 +841,6 @@ def mode(self) -> Series: block, agg_ids = block.aggregate( by_column_ids=[self._value_column], aggregations=((self._value_column, agg_ops.count_op),), - as_index=False, ) value_count_col_id = agg_ids[0] block, max_value_count_col_id = block.apply_window_op( @@ -855,14 +854,15 @@ def mode(self) -> Series: ops.eq_op, ) block = block.filter(is_mode_col_id) - mode_values_series = Series( - block.select_column(self._value_column).assign_label( - self._value_column, self.name - ) - ) - return typing.cast( - Series, mode_values_series.sort_values().reset_index(drop=True) + # use temporary name for reset_index to avoid collision, restore after dropping extra columns + block = ( + block.with_index_labels(["mode_temp_internal"]) + .order_by([OrderingColumnReference(self._value_column)]) + .reset_index(drop=False) ) + block = block.select_column(self._value_column).with_column_labels([self.name]) + mode_values_series = Series(block.select_column(self._value_column)) + return typing.cast(Series, mode_values_series) def mean(self) -> float: return typing.cast(float, self._apply_aggregation(agg_ops.mean_op)) diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 52149051860..2919c167efe 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -122,23 +122,32 @@ def test_dataframe_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +@pytest.mark.parametrize( + ("as_index"), + [ + (True), + (False), + ], +) def test_dataframe_groupby_agg_dict_with_list( - scalars_df_index, scalars_pandas_df_index + scalars_df_index, scalars_pandas_df_index, as_index ): col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = ( scalars_df_index[col_names] - .groupby("string_col") + .groupby("string_col", as_index=as_index) .agg({"int64_too": ["mean", "max"], "string_col": "count"}) ) pd_result = ( scalars_pandas_df_index[col_names] - .groupby("string_col") + .groupby("string_col", as_index=as_index) .agg({"int64_too": ["mean", "max"], "string_col": "count"}) ) bf_result_computed = bf_result.to_pandas() - pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + pd.testing.assert_frame_equal( + pd_result, bf_result_computed, check_dtype=False, check_index_type=False + ) def test_dataframe_groupby_agg_dict_no_lists(scalars_df_index, scalars_pandas_df_index): diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index e7e93849c60..1708735f4cc 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -356,17 +356,24 @@ def test_multi_index_dataframe_groupby(scalars_df_index, scalars_pandas_df_index def test_multi_index_dataframe_groupby_level_aggregate( scalars_df_index, scalars_pandas_df_index, level, as_index ): + index_cols = ["int64_too", "bool_col"] bf_result = ( - scalars_df_index.set_index(["int64_too", "bool_col"]) + scalars_df_index.set_index(index_cols) .groupby(level=level, as_index=as_index) .mean(numeric_only=True) .to_pandas() ) pd_result = ( - scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) + scalars_pandas_df_index.set_index(index_cols) .groupby(level=level, as_index=as_index) .mean(numeric_only=True) ) + # For as_index=False, pandas will drop index levels used as groupings + # In the future, it will include this in the result, bigframes already does this behavior + if not as_index: + for col in index_cols: + if col in bf_result.columns: + bf_result = bf_result.drop(col, axis=1) # Pandas will have int64 index, while bigquery will have Int64 when resetting pandas.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) From b36ce472678d2680bc8468d1e589b8bde3fde1fe Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 19 Dec 2023 13:22:15 -0800 Subject: [PATCH 228/921] chore: Remove symlink that breaks local testing (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- docs/samples | 1 - 1 file changed, 1 deletion(-) delete mode 120000 docs/samples diff --git a/docs/samples b/docs/samples deleted file mode 120000 index e804737ed3a..00000000000 --- a/docs/samples +++ /dev/null @@ -1 +0,0 @@ -../samples \ No newline at end of file From e8da3a1ca1adff59b687725d9f367e7b3bdd60cf Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 22:10:15 +0000 Subject: [PATCH 229/921] build: update actions/upload-artifact and actions/download-artifact (#276) Source-Link: https://togithub.com/googleapis/synthtool/commit/280ddaed417057dfe5b1395731de07b7d09f5058 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:346ab2efb51649c5dde7756cbbdc60dd394852ba83b9bbffc292a63549f33c17 --- .github/.OwlBot.lock.yaml | 4 +- .github/workflows/docs.yml | 8 ++-- .github/workflows/lint.yml | 4 +- .github/workflows/unittest.yml | 18 ++++---- .kokoro/requirements.txt | 48 ++++++++++---------- .kokoro/samples/python3.12/common.cfg | 40 ++++++++++++++++ .kokoro/samples/python3.12/continuous.cfg | 6 +++ .kokoro/samples/python3.12/periodic-head.cfg | 11 +++++ .kokoro/samples/python3.12/periodic.cfg | 6 +++ .kokoro/samples/python3.12/presubmit.cfg | 6 +++ 10 files changed, 110 insertions(+), 41 deletions(-) create mode 100644 .kokoro/samples/python3.12/common.cfg create mode 100644 .kokoro/samples/python3.12/continuous.cfg create mode 100644 .kokoro/samples/python3.12/periodic-head.cfg create mode 100644 .kokoro/samples/python3.12/periodic.cfg create mode 100644 .kokoro/samples/python3.12/presubmit.cfg diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 453b540c1e5..9bee2409716 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:caffe0a9277daeccc4d1de5c9b55ebba0901b57c2f713ec9c876b0d4ec064f61 -# created: 2023-11-08T19:46:45.022803742Z + digest: sha256:346ab2efb51649c5dde7756cbbdc60dd394852ba83b9bbffc292a63549f33c17 +# created: 2023-12-14T22:17:57.611773021Z diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 221806cedf5..698fbc5c94d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.9" - name: Install nox @@ -24,9 +24,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install nox diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 16d5a9e90f6..4866193af2a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.8" - name: Install nox diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 465199fc9a9..f059b5548a1 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -11,9 +11,9 @@ jobs: python: ['3.9', '3.10', '3.11'] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install nox @@ -26,9 +26,9 @@ jobs: run: | nox -s unit-${{ matrix.python }} - name: Upload coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage-artifacts + name: coverage-artifact-${{ matrix.python }} path: .coverage-${{ matrix.python }} cover: @@ -37,9 +37,9 @@ jobs: - unit steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.8" - name: Install coverage @@ -47,11 +47,11 @@ jobs: python -m pip install --upgrade setuptools pip wheel python -m pip install coverage - name: Download coverage results - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: coverage-artifacts path: .coverage-results/ - name: Report coverage results run: | - coverage combine .coverage-results/.coverage* + find .coverage-results -type f -name '*.zip' -exec unzip {} \; + coverage combine .coverage-results/**/.coverage* coverage report --show-missing --fail-under=35 diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 8957e21104e..e5c1ffca94b 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -93,30 +93,30 @@ colorlog==6.7.0 \ # via # gcp-docuploader # nox -cryptography==41.0.5 \ - --hash=sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf \ - --hash=sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84 \ - --hash=sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e \ - --hash=sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8 \ - --hash=sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7 \ - --hash=sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1 \ - --hash=sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88 \ - --hash=sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86 \ - --hash=sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179 \ - --hash=sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81 \ - --hash=sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20 \ - --hash=sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548 \ - --hash=sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d \ - --hash=sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d \ - --hash=sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5 \ - --hash=sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1 \ - --hash=sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147 \ - --hash=sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936 \ - --hash=sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797 \ - --hash=sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696 \ - --hash=sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72 \ - --hash=sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da \ - --hash=sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723 +cryptography==41.0.6 \ + --hash=sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596 \ + --hash=sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c \ + --hash=sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660 \ + --hash=sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4 \ + --hash=sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead \ + --hash=sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed \ + --hash=sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3 \ + --hash=sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7 \ + --hash=sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09 \ + --hash=sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c \ + --hash=sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43 \ + --hash=sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65 \ + --hash=sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6 \ + --hash=sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da \ + --hash=sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c \ + --hash=sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b \ + --hash=sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8 \ + --hash=sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c \ + --hash=sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d \ + --hash=sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9 \ + --hash=sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86 \ + --hash=sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36 \ + --hash=sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae # via # gcp-releasetool # secretstorage diff --git a/.kokoro/samples/python3.12/common.cfg b/.kokoro/samples/python3.12/common.cfg new file mode 100644 index 00000000000..abf83e196db --- /dev/null +++ b/.kokoro/samples/python3.12/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.12" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-312" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-bigquery-dataframes/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.12/continuous.cfg b/.kokoro/samples/python3.12/continuous.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.12/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.12/periodic-head.cfg b/.kokoro/samples/python3.12/periodic-head.cfg new file mode 100644 index 00000000000..123a35fbd3d --- /dev/null +++ b/.kokoro/samples/python3.12/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-bigquery-dataframes/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.12/periodic.cfg b/.kokoro/samples/python3.12/periodic.cfg new file mode 100644 index 00000000000..71cd1e597e3 --- /dev/null +++ b/.kokoro/samples/python3.12/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.12/presubmit.cfg b/.kokoro/samples/python3.12/presubmit.cfg new file mode 100644 index 00000000000..a1c8d9759c8 --- /dev/null +++ b/.kokoro/samples/python3.12/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file From 9ec352a338f11d82aee9cd665ffb0e6e97cb391b Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 19 Dec 2023 18:04:15 -0500 Subject: [PATCH 230/921] fix: use setuptools.find_namespace_packages (#246) Similar to googleapis/google-auth-library-python#1205 https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9aaaaae04fc..345d1ea752d 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ # benchmarks, etc. packages = [ package - for package in setuptools.PEP420PackageFinder.find() + for package in setuptools.find_namespace_packages() if package.startswith("bigframes") or package.startswith("third_party") ] From 95b673aeb1545744e4b1a353cf1f4d0202d8a1b2 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 20 Dec 2023 07:13:35 +0000 Subject: [PATCH 231/921] docs: code samples for `Series.{sum, mean, min, max}`, `astype` (#280) * docs: code samples for `Series.{sum, mean, min, max}`, `astype` * insert newlines in code samples to improve readability --------- Co-authored-by: Huan Chen <142538604+Genesis929@users.noreply.github.com> --- bigframes/dtypes.py | 2 +- .../bigframes_vendored/pandas/core/generic.py | 38 +++- .../bigframes_vendored/pandas/core/series.py | 192 ++++++++++++++++++ 3 files changed, 230 insertions(+), 2 deletions(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 6dfcc17f376..891c372a100 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -288,7 +288,7 @@ def bigframes_dtype_to_ibis_dtype( f""" Unexpected data type {bigframes_dtype}. The following str dtypes are supppted: 'boolean','Float64','Int64', 'string', - 'tring[pyarrow]','timestamp[us, tz=UTC][pyarrow]', + 'string[pyarrow]','timestamp[us, tz=UTC][pyarrow]', 'timestamp[us][pyarrow]','date32[day][pyarrow]', 'time64[us][pyarrow]'. The following pandas.ExtensionDtype are supported: pandas.BooleanDtype(), pandas.Float64Dtype(), diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 607243f8447..ca5c6344ce8 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -82,10 +82,46 @@ def astype(self, dtype): """ Cast a pandas object to a specified dtype ``dtype``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Create a DataFrame: + + >>> d = {'col1': [1, 2], 'col2': [3, 4]} + >>> df = bpd.DataFrame(data=d) + >>> df.dtypes + col1 Int64 + col2 Int64 + dtype: object + + Cast all columns to ``Float64``: + + >>> df.astype('Float64').dtypes + col1 Float64 + col2 Float64 + dtype: object + + Create a series of type ``Int64``: + + >>> ser = bpd.Series([1, 2], dtype='Int64') + >>> ser + 0 1 + 1 2 + dtype: Int64 + + Convert to ``Float64`` type: + + >>> ser.astype('Float64') + 0 1.0 + 1 2.0 + dtype: Float64 + Args: dtype (str or pandas.ExtensionDtype): A dtype supported by BigQuery DataFrame include 'boolean','Float64','Int64', - 'string', 'tring[pyarrow]','timestamp[us, tz=UTC][pyarrow]', + 'string', 'string[pyarrow]','timestamp[us, tz=UTC][pyarrow]', 'timestamp[us][pyarrow]','date32[day][pyarrow]','time64[us][pyarrow]' A pandas.ExtensionDtype include pandas.BooleanDtype(), pandas.Float64Dtype(), pandas.Int64Dtype(), pandas.StringDtype(storage="pyarrow"), diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 8303df5ef4c..d0546845987 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -890,6 +890,95 @@ def groupby( used to group large amounts of data and compute operations on these groups. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can group by a named index level. + + >>> s = bpd.Series([380, 370., 24., 26.], + ... index=["Falcon", "Falcon", "Parrot", "Parrot"], + ... name="Max Speed") + >>> s.index.name="Animal" + >>> s + Animal + Falcon 380.0 + Falcon 370.0 + Parrot 24.0 + Parrot 26.0 + Name: Max Speed, dtype: Float64 + >>> s.groupby("Animal").mean() + Animal + Falcon 375.0 + Parrot 25.0 + Name: Max Speed, dtype: Float64 + + You can also group by more than one index levels. + + >>> import pandas as pd + >>> s = bpd.Series([380, 370., 24., 26.], + ... index=pd.MultiIndex.from_tuples( + ... [("Falcon", "Clear"), + ... ("Falcon", "Cloudy"), + ... ("Parrot", "Clear"), + ... ("Parrot", "Clear")], + ... names=["Animal", "Sky"]), + ... name="Max Speed") + >>> s + Animal Sky + Falcon Clear 380.0 + Cloudy 370.0 + Parrot Clear 24.0 + Clear 26.0 + Name: Max Speed, dtype: Float64 + + >>> s.groupby("Animal").mean() + Animal + Falcon 375.0 + Parrot 25.0 + Name: Max Speed, dtype: Float64 + + >>> s.groupby("Sky").mean() + Sky + Clear 143.333333 + Cloudy 370.0 + Name: Max Speed, dtype: Float64 + + >>> s.groupby(["Animal", "Sky"]).mean() + Animal Sky + Falcon Clear 380.0 + Cloudy 370.0 + Parrot Clear 25.0 + Name: Max Speed, dtype: Float64 + + You can also group by values in a Series provided the index matches with + the original series. + + >>> df = bpd.DataFrame({'Animal': ['Falcon', 'Falcon', 'Parrot', 'Parrot'], + ... 'Max Speed': [380., 370., 24., 26.], + ... 'Age': [10., 20., 4., 6.]}) + >>> df + Animal Max Speed Age + 0 Falcon 380.0 10.0 + 1 Falcon 370.0 20.0 + 2 Parrot 24.0 4.0 + 3 Parrot 26.0 6.0 + + [4 rows x 3 columns] + + >>> df['Max Speed'].groupby(df['Animal']).mean() + Animal + Falcon 375.0 + Parrot 25.0 + Name: Max Speed, dtype: Float64 + + >>> df['Age'].groupby(df['Animal']).max() + Animal + Falcon 20.0 + Parrot 6.0 + Name: Age, dtype: Float64 + Args: by (mapping, function, label, pd.Grouper or list of such, default None): Used to determine the groups for the groupby. @@ -1661,6 +1750,31 @@ def max( If you want the index of the maximum, use ``idxmax``. This is the equivalent of the ``numpy.ndarray`` method ``argmax``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Calculating the max of a Series: + + >>> s = bpd.Series([1, 3]) + >>> s + 0 1 + 1 3 + dtype: Int64 + >>> s.max() + 3 + + Calculating the max of a Series containing ``NA`` values: + + >>> s = bpd.Series([1, 3, bpd.NA]) + >>> s + 0 1.0 + 1 3.0 + 2 + dtype: Float64 + >>> s.max() + 3.0 Returns: scalar: Scalar. @@ -1676,6 +1790,32 @@ def min( If you want the index of the minimum, use ``idxmin``. This is the equivalent of the ``numpy.ndarray`` method ``argmin``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Calculating the min of a Series: + + >>> s = bpd.Series([1, 3]) + >>> s + 0 1 + 1 3 + dtype: Int64 + >>> s.min() + 1 + + Calculating the min of a Series containing ``NA`` values: + + >>> s = bpd.Series([1, 3, bpd.NA]) + >>> s + 0 1.0 + 1 3.0 + 2 + dtype: Float64 + >>> s.min() + 1.0 + Returns: scalar: Scalar. """ @@ -1714,6 +1854,32 @@ def sum(self): This is equivalent to the method ``numpy.sum``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Calculating the sum of a Series: + + >>> s = bpd.Series([1, 3]) + >>> s + 0 1 + 1 3 + dtype: Int64 + >>> s.sum() + 4 + + Calculating the sum of a Series containing ``NA`` values: + + >>> s = bpd.Series([1, 3, bpd.NA]) + >>> s + 0 1.0 + 1 3.0 + 2 + dtype: Float64 + >>> s.sum() + 4.0 + Returns: scalar: Scalar. """ @@ -1722,6 +1888,32 @@ def sum(self): def mean(self): """Return the mean of the values over the requested axis. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Calculating the mean of a Series: + + >>> s = bpd.Series([1, 3]) + >>> s + 0 1 + 1 3 + dtype: Int64 + >>> s.mean() + 2.0 + + Calculating the mean of a Series containing ``NA`` values: + + >>> s = bpd.Series([1, 3, bpd.NA]) + >>> s + 0 1.0 + 1 3.0 + 2 + dtype: Float64 + >>> s.mean() + 2.0 + Returns: scalar: Scalar. """ From ad6746569b3af11be9d40805a1449ee1e89288dc Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 21 Dec 2023 00:33:37 +0000 Subject: [PATCH 232/921] fix: make `Series.str.replace` work for simple strings (#285) --- bigframes/operations/__init__.py | 2 +- tests/system/small/operations/test_strings.py | 2 ++ .../bigframes_vendored/pandas/core/series.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 753870a42d5..678774978a1 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -385,7 +385,7 @@ def _as_ibis(self, x: ibis_types.Value): ibis_types.StringValue, ibis_types.literal(self._pat) ) repl_str_value = typing.cast( - ibis_types.StringValue, ibis_types.literal(self._pat) + ibis_types.StringValue, ibis_types.literal(self._repl) ) return typing.cast(ibis_types.StringValue, x).replace( diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 27a35134d4a..79f92c94b43 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -94,6 +94,8 @@ def test_str_extract(scalars_dfs, pat): (".*", "blah", True, 0, True), ("h.l", "blah", False, 0, True), (re.compile("(?i).e.."), "blah", None, 0, True), + ("H", "h", True, 0, False), + (", ", "__", True, 0, False), ], ) def test_str_replace(scalars_dfs, pat, repl, case, flags, regex): diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index d0546845987..366f32c77ef 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -2304,6 +2304,24 @@ def str(self): NAs stay NA unless handled otherwise by a particular method. Patterned after Python’s string methods, with some inspiration from R’s stringr package. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["A_Str_Series"]) + >>> s + 0 A_Str_Series + dtype: string + + >>> s.str.lower() + 0 a_str_series + dtype: string + + >>> s.str.replace("_", "") + 0 AStrSeries + dtype: string + Returns: bigframes.operations.strings.StringMethods: An accessor containing string methods. From a1c06319ab0e3697c3175112490488002bb344c0 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 20 Dec 2023 18:35:18 -0800 Subject: [PATCH 233/921] feat: specific pyarrow mappings for decimal, bytes types (#283) * feat: new bytes, json, decimal type mappings * amend tests to reflect new types * add implicit type conversion for df.replace * more type casting tests * skip pandas 1.x for more tests --------- Co-authored-by: Tim Swast --- bigframes/core/block_transforms.py | 2 +- bigframes/core/blocks.py | 2 +- bigframes/core/compile/compiled.py | 14 +- bigframes/core/groupby/__init__.py | 7 +- bigframes/dataframe.py | 20 +-- bigframes/dtypes.py | 139 ++++++++++++++------ bigframes/series.py | 24 +++- tests/system/large/ml/test_compose.py | 4 +- tests/system/large/ml/test_core.py | 1 + tests/system/small/ml/test_core.py | 3 +- tests/system/small/ml/test_imported.py | 2 + tests/system/small/ml/test_llm.py | 10 +- tests/system/small/ml/test_preprocessing.py | 16 +++ tests/system/small/ml/test_remote.py | 1 + tests/system/small/test_dataframe.py | 37 +++--- tests/system/small/test_dataframe_io.py | 18 ++- tests/system/small/test_multiindex.py | 7 +- tests/system/small/test_series.py | 15 ++- tests/system/small/test_session.py | 3 + tests/system/utils.py | 36 ++++- tests/unit/test_dtypes.py | 7 +- 21 files changed, 267 insertions(+), 101 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 66548922875..c6867c1a33d 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -131,7 +131,7 @@ def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: if len(index_columns) != 1: raise ValueError("only method 'linear' supports multi-index") xvalues = block.index_columns[0] - if block.index_dtypes[0] not in dtypes.NUMERIC_BIGFRAMES_TYPES: + if block.index_dtypes[0] not in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: raise ValueError("Can only interpolate on numeric index.") for column in original_columns: diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 3163aa5b096..779d11b3712 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1063,7 +1063,7 @@ def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.AggregateOp]: stats: list[agg_ops.AggregateOp] = [agg_ops.count_op] if dtype not in bigframes.dtypes.UNORDERED_DTYPES: stats += [agg_ops.min_op, agg_ops.max_op] - if dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES: + if dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: # Notable exclusions: # prod op tends to cause overflows # Also, var_op is redundant as can be derived from std diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index d6183228d1e..199c8db785c 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -220,7 +220,10 @@ def _get_ibis_column(self, key: str) -> ibis_types.Value: raise ValueError( "Column name {} not in set of values: {}".format(key, self.column_ids) ) - return typing.cast(ibis_types.Value, self._column_names[key]) + return typing.cast( + ibis_types.Value, + bigframes.dtypes.ibis_value_to_canonical_type(self._column_names[key]), + ) def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: ibis_type = typing.cast( @@ -1177,7 +1180,14 @@ def _to_ibis_expr( # Make sure all dtypes are the "canonical" ones for BigFrames. This is # important for operations like UNION where the schema must match. table = self._table.select( - bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns + bigframes.dtypes.ibis_value_to_canonical_type( + column.resolve(self._table) + # TODO(https://github.com/ibis-project/ibis/issues/7613): use + # public API to refer to Deferred type. + if isinstance(column, ibis.common.deferred.Deferred) + else column + ) + for column in columns ) base_table = table if self._reduced_predicate is not None: diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 3ee46ef6758..66ba9016491 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -359,7 +359,8 @@ def _convert_index(self, dataframe: df.DataFrame): def _raise_on_non_numeric(self, op: str): if not all( - dtype in dtypes.NUMERIC_BIGFRAMES_TYPES for dtype in self._block.dtypes + dtype in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE + for dtype in self._block.dtypes ): raise NotImplementedError( f"'{op}' does not support non-numeric columns. " @@ -371,7 +372,9 @@ def _raise_on_non_numeric(self, op: str): def _aggregated_columns(self, numeric_only: bool = False) -> typing.Sequence[str]: valid_agg_cols: list[str] = [] for col_id in self._selected_cols: - is_numeric = self._column_type(col_id) in dtypes.NUMERIC_BIGFRAMES_TYPES + is_numeric = ( + self._column_type(col_id) in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE + ) if is_numeric or not numeric_only: valid_agg_cols.append(col_id) return valid_agg_cols diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 98aa8f1185b..423c2bcaac4 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1800,7 +1800,7 @@ def agg( ) -> DataFrame | bigframes.series.Series: if utils.is_list_like(func): if any( - dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES + dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE for dtype in self.dtypes ): raise NotImplementedError( @@ -1867,7 +1867,7 @@ def melt( ) def describe(self) -> DataFrame: - df_numeric = self._drop_non_numeric(keep_bool=False) + df_numeric = self._drop_non_numeric(permissive=False) if len(df_numeric.columns) == 0: raise NotImplementedError( f"df.describe() currently only supports numeric values. {constants.FEEDBACK_LINK}" @@ -2005,10 +2005,12 @@ def unstack(self, level: LevelsType = -1): ) return DataFrame(pivot_block) - def _drop_non_numeric(self, keep_bool=True) -> DataFrame: - types_to_keep = set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) - if not keep_bool: - types_to_keep -= set(bigframes.dtypes.BOOL_BIGFRAMES_TYPES) + def _drop_non_numeric(self, permissive=True) -> DataFrame: + types_to_keep = ( + set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) + if permissive + else set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_RESTRICTIVE) + ) non_numeric_cols = [ col_id for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) @@ -2026,7 +2028,7 @@ def _drop_non_bool(self) -> DataFrame: def _raise_on_non_numeric(self, op: str): if not all( - dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES + dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE for dtype in self._block.dtypes ): raise NotImplementedError( @@ -2301,7 +2303,7 @@ def notna(self) -> DataFrame: def cumsum(self): is_numeric_types = [ - (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) + (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) for _, dtype in self.dtypes.items() ] if not all(is_numeric_types): @@ -2313,7 +2315,7 @@ def cumsum(self): def cumprod(self) -> DataFrame: is_numeric_types = [ - (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES) + (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) for _, dtype in self.dtypes.items() ] if not all(is_numeric_types): diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 891c372a100..b754acea2ea 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -15,6 +15,7 @@ """Mappings for Pandas dtypes supported by BigQuery DataFrames package""" import datetime +import decimal import textwrap import typing from typing import Any, Dict, Iterable, Literal, Tuple, Union @@ -30,6 +31,7 @@ import bigframes.constants as constants import third_party.bigframes_vendored.google_cloud_bigquery._pandas_helpers as gcb3p_pandas_helpers +import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ @@ -40,9 +42,6 @@ pd.ArrowDtype, ] -# Corresponds to the pandas concept of numeric type (such as when 'numeric_only' is specified in an operation) -NUMERIC_BIGFRAMES_TYPES = [pd.BooleanDtype(), pd.Float64Dtype(), pd.Int64Dtype()] - # On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable UNORDERED_DTYPES = [gpd.array.GeometryDtype()] @@ -57,6 +56,9 @@ "timestamp[us][pyarrow]", "date32[day][pyarrow]", "time64[us][pyarrow]", + "decimal128(38, 9)[pyarrow]", + "decimal256(38, 9)[pyarrow]", + "binary[pyarrow]", ] # Type hints for Ibis data types supported by BigQuery DataFrame @@ -72,8 +74,17 @@ BOOL_BIGFRAMES_TYPES = [pd.BooleanDtype()] -# Several operations are restricted to these types. -NUMERIC_BIGFRAMES_TYPES = [pd.BooleanDtype(), pd.Float64Dtype(), pd.Int64Dtype()] +# Corresponds to the pandas concept of numeric type (such as when 'numeric_only' is specified in an operation) +# Pandas is inconsistent, so two definitions are provided, each used in different contexts +NUMERIC_BIGFRAMES_TYPES_RESTRICTIVE = [ + pd.Float64Dtype(), + pd.Int64Dtype(), +] +NUMERIC_BIGFRAMES_TYPES_PERMISSIVE = NUMERIC_BIGFRAMES_TYPES_RESTRICTIVE + [ + pd.BooleanDtype(), + pd.ArrowDtype(pa.decimal128(38, 9)), + pd.ArrowDtype(pa.decimal256(76, 38)), +] # Type hints for Ibis data types that can be read to Python objects by BigQuery DataFrame ReadOnlyIbisDtype = Union[ @@ -97,6 +108,15 @@ ibis_dtypes.Timestamp(timezone="UTC"), pd.ArrowDtype(pa.timestamp("us", tz="UTC")), ), + (ibis_dtypes.binary, pd.ArrowDtype(pa.binary())), + ( + ibis_dtypes.Decimal(precision=38, scale=9, nullable=True), + pd.ArrowDtype(pa.decimal128(38, 9)), + ), + ( + ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), + pd.ArrowDtype(pa.decimal256(76, 38)), + ), ) BIGFRAMES_TO_IBIS: Dict[Dtype, ibis_dtypes.DataType] = { @@ -112,6 +132,9 @@ ibis_dtypes.time: pa.time64("us"), ibis_dtypes.Timestamp(timezone=None): pa.timestamp("us"), ibis_dtypes.Timestamp(timezone="UTC"): pa.timestamp("us", tz="UTC"), + ibis_dtypes.binary: pa.binary(), + ibis_dtypes.Decimal(precision=38, scale=9, nullable=True): pa.decimal128(38, 9), + ibis_dtypes.Decimal(precision=76, scale=38, nullable=True): pa.decimal256(76, 38), } ARROW_TO_IBIS = {arrow: ibis for ibis, arrow in IBIS_TO_ARROW.items()} @@ -125,10 +148,6 @@ ) IBIS_TO_BIGFRAMES.update( { - ibis_dtypes.binary: np.dtype("O"), - ibis_dtypes.json: np.dtype("O"), - ibis_dtypes.Decimal(precision=38, scale=9, nullable=True): np.dtype("O"), - ibis_dtypes.Decimal(precision=76, scale=38, nullable=True): np.dtype("O"), ibis_dtypes.GeoSpatial( geotype="geography", srid=4326, nullable=True ): gpd.array.GeometryDtype(), @@ -178,7 +197,7 @@ def ibis_dtype_to_bigframes_dtype( # our IO returns them as objects. Eventually, we should support them as # ArrowDType (and update the IO accordingly) if isinstance(ibis_dtype, ibis_dtypes.Array): - return np.dtype("O") + return pd.ArrowDtype(ibis_dtype_to_arrow_dtype(ibis_dtype)) if isinstance(ibis_dtype, ibis_dtypes.Struct): return pd.ArrowDtype(ibis_dtype_to_arrow_dtype(ibis_dtype)) @@ -200,7 +219,9 @@ def ibis_dtype_to_bigframes_dtype( def ibis_dtype_to_arrow_dtype(ibis_dtype: ibis_dtypes.DataType) -> pa.DataType: if isinstance(ibis_dtype, ibis_dtypes.Array): - return pa.list_(ibis_dtype_to_arrow_dtype(ibis_dtype.value_type)) + return pa.list_( + ibis_dtype_to_arrow_dtype(ibis_dtype.value_type.copy(nullable=True)) + ) if isinstance(ibis_dtype, ibis_dtypes.Struct): return pa.struct( @@ -224,21 +245,13 @@ def ibis_value_to_canonical_type(value: ibis_types.Value) -> ibis_types.Value: This is useful in cases where multiple types correspond to the same BigFrames dtype. """ ibis_type = value.type() + name = value.get_name() + if ibis_type.is_json(): + value = vendored_ibis_ops.ToJsonString(value).to_expr() + return value.name(name) # Allow REQUIRED fields to be joined with NULLABLE fields. nullable_type = ibis_type.copy(nullable=True) - return value.cast(nullable_type).name(value.get_name()) - - -def ibis_table_to_canonical_types(table: ibis_types.Table) -> ibis_types.Table: - """Converts an Ibis table expression to canonical types. - - This is useful in cases where multiple types correspond to the same BigFrames dtype. - """ - casted_columns = [] - for column_name in table.columns: - column = typing.cast(ibis_types.Value, table[column_name]) - casted_columns.append(ibis_value_to_canonical_type(column)) - return table.select(*casted_columns) + return value.cast(nullable_type).name(name) def arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType: @@ -386,15 +399,35 @@ def cast_ibis_value( ibis_dtypes.bool, ibis_dtypes.float64, ibis_dtypes.string, + ibis_dtypes.Decimal(precision=38, scale=9), + ibis_dtypes.Decimal(precision=76, scale=38), + ), + ibis_dtypes.float64: ( + ibis_dtypes.string, + ibis_dtypes.int64, + ibis_dtypes.Decimal(precision=38, scale=9), + ibis_dtypes.Decimal(precision=76, scale=38), + ), + ibis_dtypes.string: ( + ibis_dtypes.int64, + ibis_dtypes.float64, + ibis_dtypes.Decimal(precision=38, scale=9), + ibis_dtypes.Decimal(precision=76, scale=38), + ibis_dtypes.binary, ), - ibis_dtypes.float64: (ibis_dtypes.string, ibis_dtypes.int64), - ibis_dtypes.string: (ibis_dtypes.int64, ibis_dtypes.float64), ibis_dtypes.date: (ibis_dtypes.string,), - ibis_dtypes.Decimal(precision=38, scale=9): (ibis_dtypes.float64,), - ibis_dtypes.Decimal(precision=76, scale=38): (ibis_dtypes.float64,), + ibis_dtypes.Decimal(precision=38, scale=9): ( + ibis_dtypes.float64, + ibis_dtypes.Decimal(precision=76, scale=38), + ), + ibis_dtypes.Decimal(precision=76, scale=38): ( + ibis_dtypes.float64, + ibis_dtypes.Decimal(precision=38, scale=9), + ), ibis_dtypes.time: (), ibis_dtypes.timestamp: (ibis_dtypes.Timestamp(timezone="UTC"),), ibis_dtypes.Timestamp(timezone="UTC"): (ibis_dtypes.timestamp,), + ibis_dtypes.binary: (ibis_dtypes.string,), } value = ibis_value_to_canonical_type(value) @@ -458,30 +491,62 @@ def is_dtype(scalar: typing.Any, dtype: Dtype) -> bool: return False +# string is binary def is_patype(scalar: typing.Any, pa_type: pa.DataType) -> bool: """Determine whether a scalar's type matches a given pyarrow type.""" if pa_type == pa.time64("us"): return isinstance(scalar, datetime.time) - if pa_type == pa.timestamp("us"): + elif pa_type == pa.timestamp("us"): if isinstance(scalar, datetime.datetime): return not scalar.tzinfo if isinstance(scalar, pd.Timestamp): return not scalar.tzinfo - if pa_type == pa.timestamp("us", tz="UTC"): + elif pa_type == pa.timestamp("us", tz="UTC"): if isinstance(scalar, datetime.datetime): return scalar.tzinfo == datetime.timezone.utc if isinstance(scalar, pd.Timestamp): return scalar.tzinfo == datetime.timezone.utc - if pa_type == pa.date32(): + elif pa_type == pa.date32(): return isinstance(scalar, datetime.date) + elif pa_type == pa.binary(): + return isinstance(scalar, bytes) + elif pa_type == pa.decimal128(38, 9): + # decimal.Decimal is a superset, but ibis performs out-of-bounds and loss-of-precision checks + return isinstance(scalar, decimal.Decimal) + elif pa_type == pa.decimal256(76, 38): + # decimal.Decimal is a superset, but ibis performs out-of-bounds and loss-of-precision checks + return isinstance(scalar, decimal.Decimal) return False -def is_comparable(scalar: typing.Any, dtype: Dtype) -> bool: - """Whether scalar can be compare to items of dtype (though maybe requiring coercion)""" +def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: + """Whether scalar can be compare to items of dtype (though maybe requiring coercion). Returns the datatype that must be used for the comparison""" if is_dtype(scalar, dtype): - return True + return dtype elif pd.api.types.is_numeric_dtype(dtype): - return pd.api.types.is_number(scalar) - else: - return False + # Implicit conversion currently only supported for numeric types + if pd.api.types.is_bool(scalar): + return lcd_type(pd.BooleanDtype(), dtype) + if pd.api.types.is_float(scalar): + return lcd_type(pd.Float64Dtype(), dtype) + if pd.api.types.is_integer(scalar): + return lcd_type(pd.Int64Dtype(), dtype) + if isinstance(scalar, decimal.Decimal): + # TODO: Check context to see if can use NUMERIC instead of BIGNUMERIC + return lcd_type(pd.ArrowDtype(pa.decimal128(76, 38)), dtype) + return None + + +def lcd_type(dtype1: Dtype, dtype2: Dtype) -> typing.Optional[Dtype]: + # Implicit conversion currently only supported for numeric types + hierarchy: list[Dtype] = [ + pd.BooleanDtype(), + pd.Int64Dtype(), + pd.Float64Dtype(), + pd.ArrowDtype(pa.decimal128(38, 9)), + pd.ArrowDtype(pa.decimal256(76, 38)), + ] + if (dtype1 not in hierarchy) or (dtype2 not in hierarchy): + return None + lcd_index = max(hierarchy.index(dtype1), hierarchy.index(dtype2)) + return hierarchy[lcd_index] diff --git a/bigframes/series.py b/bigframes/series.py index 6837c1c7f8d..eefd2b755dd 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -16,6 +16,7 @@ from __future__ import annotations +import functools import itertools import numbers import textwrap @@ -455,7 +456,7 @@ def replace( else: # Scalar replace_list = [to_replace] replace_list = [ - i for i in replace_list if bigframes.dtypes.is_comparable(i, self.dtype) + i for i in replace_list if bigframes.dtypes.is_compatible(i, self.dtype) ] return self._simple_replace(replace_list, value) if replace_list else self @@ -472,11 +473,15 @@ def _regex_replace(self, to_replace: str, value: str): return Series(block.select_column(result_col)) def _simple_replace(self, to_replace_list: typing.Sequence, value): - if not bigframes.dtypes.is_dtype(value, self.dtype): + result_type = bigframes.dtypes.is_compatible(value, self.dtype) + if not result_type: raise NotImplementedError( f"Cannot replace {self.dtype} elements with incompatible item {value} as mixed-type columns not supported. {constants.FEEDBACK_LINK}" ) + if result_type != self.dtype: + return self.astype(result_type)._simple_replace(to_replace_list, value) + block, cond = self._block.apply_unary_op( self._value_column, ops.IsInOp(to_replace_list) ) @@ -490,15 +495,26 @@ def _simple_replace(self, to_replace_list: typing.Sequence, value): def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): tuples = [] + lcd_types: list[typing.Optional[bigframes.dtypes.Dtype]] = [] for key, value in mapping.items(): - if not bigframes.dtypes.is_comparable(key, self.dtype): + lcd_type = bigframes.dtypes.is_compatible(key, self.dtype) + if not lcd_type: continue if not bigframes.dtypes.is_dtype(value, self.dtype): raise NotImplementedError( f"Cannot replace {self.dtype} elements with incompatible item {value} as mixed-type columns not supported. {constants.FEEDBACK_LINK}" ) tuples.append((key, value)) + lcd_types.append(lcd_type) + result_dtype = functools.reduce( + lambda t1, t2: bigframes.dtypes.lcd_type(t1, t2) if (t1 and t2) else None, + lcd_types, + ) + if not result_dtype: + raise NotImplementedError( + f"Cannot replace {self.dtype} elements with incompatible mapping {mapping} as mixed-type columns not supported. {constants.FEEDBACK_LINK}" + ) block, result = self._block.apply_unary_op( self._value_column, ops.MapOp(tuple(tuples)) ) @@ -782,7 +798,7 @@ def _central_moment(self, n: int) -> float: def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: if _is_list_like(func): - if self.dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES: + if self.dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: raise NotImplementedError( f"Multiple aggregations only supported on numeric series. {constants.FEEDBACK_LINK}" ) diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 0c280e5d020..6ea4f724898 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -72,7 +72,7 @@ def test_columntransformer_standalone_fit_and_transform( expected.standard_scaled_flipper_length_mm.astype("Float64") ) - pandas.testing.assert_frame_equal(result, expected, rtol=1e-3) + pandas.testing.assert_frame_equal(result, expected, rtol=1e-3, check_dtype=False) def test_columntransformer_standalone_fit_transform(new_penguins_df): @@ -123,4 +123,4 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): expected.standard_scaled_flipper_length_mm.astype("Float64") ) - pandas.testing.assert_frame_equal(result, expected, rtol=1e-3) + pandas.testing.assert_frame_equal(result, expected, rtol=1e-3, check_dtype=False) diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py index 3b30d7eb1d9..df387e6ee17 100644 --- a/tests/system/large/ml/test_core.py +++ b/tests/system/large/ml/test_core.py @@ -184,4 +184,5 @@ def test_bqml_standalone_transform(penguins_df_default_index, new_penguins_df): expected, check_exact=False, rtol=0.1, + check_dtype=False, ) diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index eece5ef21d3..f39815aec28 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -292,11 +292,12 @@ def test_model_predict_with_unnamed_index( def test_remote_model_predict( bqml_linear_remote_model: core.BqmlModel, new_penguins_df ): - predictions = bqml_linear_remote_model.predict(new_penguins_df).to_pandas() expected = pd.DataFrame( {"predicted_body_mass_g": [[3739.54], [3675.79], [3619.54]]}, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + dtype=pd.ArrowDtype(pa.list_(pa.float64())), ) + predictions = bqml_linear_remote_model.predict(new_penguins_df).to_pandas() pd.testing.assert_frame_equal( predictions[["predicted_body_mass_g"]].sort_index(), expected, diff --git a/tests/system/small/ml/test_imported.py b/tests/system/small/ml/test_imported.py index 9008e85a0b5..8ffd9924e94 100644 --- a/tests/system/small/ml/test_imported.py +++ b/tests/system/small/ml/test_imported.py @@ -51,6 +51,7 @@ def test_tensorflow_model_predict(imported_tensorflow_model, llm_text_df): result, expected, check_exact=False, + check_dtype=False, atol=0.1, ) @@ -90,6 +91,7 @@ def test_onnx_model_predict(imported_onnx_model, onnx_iris_df): result, expected, check_exact=False, + check_dtype=False, atol=0.1, ) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 267a2ed9c18..fd1b803eea2 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import numpy as np import pytest from bigframes.ml import llm @@ -202,8 +201,7 @@ def test_embedding_generator_predict_success( assert "text_embedding" in df.columns series = df["text_embedding"] value = series[0] - assert isinstance(value, np.ndarray) - assert value.size == 768 + assert len(value) == 768 @pytest.mark.flaky(retries=2, delay=120) @@ -215,8 +213,7 @@ def test_embedding_generator_multilingual_predict_success( assert "text_embedding" in df.columns series = df["text_embedding"] value = series[0] - assert isinstance(value, np.ndarray) - assert value.size == 768 + assert len(value) == 768 @pytest.mark.flaky(retries=2, delay=120) @@ -228,5 +225,4 @@ def test_embedding_generator_predict_series_success( assert "text_embedding" in df.columns series = df["text_embedding"] value = series[0] - assert isinstance(value, np.ndarray) - assert value.size == 768 + assert len(value) == 768 diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 45548acca32..c3bd7f3b87f 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -15,6 +15,7 @@ import math import pandas as pd +import pyarrow as pa import bigframes.ml.preprocessing @@ -453,6 +454,9 @@ def test_one_hot_encoder_default_params(new_penguins_df): [{"index": 2, "value": 1.0}], ], }, + dtype=pd.ArrowDtype( + pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) + ), index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -482,6 +486,9 @@ def test_one_hot_encoder_default_params_fit_transform(new_penguins_df): [{"index": 2, "value": 1.0}], ], }, + dtype=pd.ArrowDtype( + pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) + ), index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -507,6 +514,9 @@ def test_one_hot_encoder_series_default_params(new_penguins_df): [{"index": 2, "value": 1.0}], ], }, + dtype=pd.ArrowDtype( + pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) + ), index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -537,6 +547,9 @@ def test_one_hot_encoder_params(new_penguins_df): [{"index": 0, "value": 1.0}], ], }, + dtype=pd.ArrowDtype( + pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) + ), index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -567,6 +580,9 @@ def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_ [{"index": 2, "value": 1.0}], ], }, + dtype=pd.ArrowDtype( + pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) + ), index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) diff --git a/tests/system/small/ml/test_remote.py b/tests/system/small/ml/test_remote.py index e8eb1c85e86..5036cdadfca 100644 --- a/tests/system/small/ml/test_remote.py +++ b/tests/system/small/ml/test_remote.py @@ -29,5 +29,6 @@ def test_remote_linear_vertex_model_predict( predictions[["predicted_body_mass_g"]].sort_index(), expected, check_exact=False, + check_dtype=False, rtol=0.1, ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ed78e73e5df..86b8cfbe669 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -19,7 +19,6 @@ from typing import Tuple import geopandas as gpd # type: ignore -import numpy as np import pandas as pd import pandas.testing import pyarrow as pa # type: ignore @@ -29,7 +28,11 @@ import bigframes._config.display_options as display_options import bigframes.dataframe as dataframe import bigframes.series as series -from tests.system.utils import assert_pandas_df_equal, assert_series_equal +from tests.system.utils import ( + assert_pandas_df_equal, + assert_series_equal, + skip_legacy_pandas, +) def test_df_construct_copy(scalars_dfs): @@ -273,19 +276,19 @@ def test_df_info(scalars_dfs): " # Column Non-Null Count Dtype\n" "--- ------------- ---------------- ------------------------------\n" " 0 bool_col 8 non-null boolean\n" - " 1 bytes_col 6 non-null object\n" + " 1 bytes_col 6 non-null binary[pyarrow]\n" " 2 date_col 7 non-null date32[day][pyarrow]\n" " 3 datetime_col 6 non-null timestamp[us][pyarrow]\n" " 4 geography_col 4 non-null geometry\n" " 5 int64_col 8 non-null Int64\n" " 6 int64_too 9 non-null Int64\n" - " 7 numeric_col 6 non-null object\n" + " 7 numeric_col 6 non-null decimal128(38, 9)[pyarrow]\n" " 8 float64_col 7 non-null Float64\n" " 9 rowindex_2 9 non-null Int64\n" " 10 string_col 8 non-null string\n" " 11 time_col 6 non-null time64[us][pyarrow]\n" " 12 timestamp_col 6 non-null timestamp[us, tz=UTC][pyarrow]\n" - "dtypes: Float64(1), Int64(3), boolean(1), date32[day][pyarrow](1), geometry(1), object(2), string(1), time64[us][pyarrow](1), timestamp[us, tz=UTC][pyarrow](1), timestamp[us][pyarrow](1)\n" + "dtypes: Float64(1), Int64(3), binary[pyarrow](1), boolean(1), date32[day][pyarrow](1), decimal128(38, 9)[pyarrow](1), geometry(1), string(1), time64[us][pyarrow](1), timestamp[us, tz=UTC][pyarrow](1), timestamp[us][pyarrow](1)\n" "memory usage: 945 bytes\n" ) @@ -362,6 +365,7 @@ def test_drop_bigframes_index_with_na(scalars_dfs): pd.testing.assert_frame_equal(pd_result, bf_result) +@skip_legacy_pandas def test_drop_bigframes_multiindex(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs scalars_df = scalars_df.copy() @@ -841,13 +845,11 @@ def test_df_fillna(scalars_dfs): def test_df_replace_scalar_scalar(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df.replace("Hello, World!", "Howdy, Planet!").to_pandas() - pd_result = scalars_pandas_df.replace("Hello, World!", "Howdy, Planet!") + bf_result = scalars_df.replace(555.555, 3).to_pandas() + pd_result = scalars_pandas_df.replace(555.555, 3) - pd.testing.assert_frame_equal( - pd_result, - bf_result, - ) + # pandas has narrower result types as they are determined dynamically + pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) def test_df_replace_regex_scalar(scalars_dfs): @@ -863,12 +865,14 @@ def test_df_replace_regex_scalar(scalars_dfs): def test_df_replace_list_scalar(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df.replace(["Hello, World!", "T"], "Howdy, Planet!").to_pandas() - pd_result = scalars_pandas_df.replace(["Hello, World!", "T"], "Howdy, Planet!") + bf_result = scalars_df.replace([555.555, 3.2], 3).to_pandas() + pd_result = scalars_pandas_df.replace([555.555, 3.2], 3) + # pandas has narrower result types as they are determined dynamically pd.testing.assert_frame_equal( pd_result, bf_result, + check_dtype=False, ) @@ -1198,13 +1202,13 @@ def test_get_dtypes(scalars_df_default_index): pd.Series( { "bool_col": pd.BooleanDtype(), - "bytes_col": np.dtype("O"), + "bytes_col": pd.ArrowDtype(pa.binary()), "date_col": pd.ArrowDtype(pa.date32()), "datetime_col": pd.ArrowDtype(pa.timestamp("us")), "geography_col": gpd.array.GeometryDtype(), "int64_col": pd.Int64Dtype(), "int64_too": pd.Int64Dtype(), - "numeric_col": np.dtype("O"), + "numeric_col": pd.ArrowDtype(pa.decimal128(38, 9)), "float64_col": pd.Float64Dtype(), "rowindex": pd.Int64Dtype(), "rowindex_2": pd.Int64Dtype(), @@ -1232,7 +1236,7 @@ def test_get_dtypes_array_struct(session): dtypes, pd.Series( { - "array_column": np.dtype("O"), + "array_column": pd.ArrowDtype(pa.list_(pa.int64())), "struct_column": pd.ArrowDtype( pa.struct( [ @@ -2138,6 +2142,7 @@ def test_dataframe_agg_multi_string(scalars_dfs): ).all() +@skip_legacy_pandas def test_df_describe(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs # pyarrows time columns fail in pandas diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index fb9fb7bb898..59864e483e4 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -56,7 +56,9 @@ def test_to_pandas_array_struct_correct_result(session): result = df.to_pandas() expected = pd.DataFrame( { - "array_column": [[1, 3, 2]], + "array_column": pd.Series( + [[1, 3, 2]], dtype=pd.ArrowDtype(pa.list_(pa.int64())) + ), "struct_column": pd.Series( [{"string_field": "a", "float_field": 1.2}], dtype=pd.ArrowDtype( @@ -91,7 +93,8 @@ def test_load_json(session): expected = pd.DataFrame( { "json_column": ['{"bar":true,"foo":10}'], - } + }, + dtype=pd.StringDtype(storage="pyarrow"), ) expected.index = expected.index.astype("Int64") pd.testing.assert_series_equal(result.dtypes, expected.dtypes) @@ -137,6 +140,8 @@ def test_to_csv_index( dtype = scalars_df.reset_index().dtypes.to_dict() dtype.pop("geography_col") dtype.pop("rowindex") + # read_csv will decode into bytes inproperly, convert_pandas_dtypes will encode properly from string + dtype.pop("bytes_col") gcs_df = pd.read_csv( path, dtype=dtype, @@ -148,7 +153,6 @@ def test_to_csv_index( scalars_pandas_df = scalars_pandas_df.copy() scalars_pandas_df.index = scalars_pandas_df.index.astype("int64") - # Ordering should be maintained for tables smaller than 1 GB. pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) @@ -174,6 +178,8 @@ def test_to_csv_tabs( dtype = scalars_df.reset_index().dtypes.to_dict() dtype.pop("geography_col") dtype.pop("rowindex") + # read_csv will decode into bytes inproperly, convert_pandas_dtypes will encode properly from string + dtype.pop("bytes_col") gcs_df = pd.read_csv( path, sep="\t", @@ -216,6 +222,8 @@ def test_to_gbq_index(scalars_dfs, dataset_id, index): df_out = df_out.sort_values("rowindex_2").reset_index(drop=True) convert_pandas_dtypes(df_out, bytes_col=False) + # pd.read_gbq interpets bytes_col as object, reconvert to pyarrow binary + df_out["bytes_col"] = df_out["bytes_col"].astype(pd.ArrowDtype(pa.binary())) expected = scalars_pandas_df.copy() expected.index.name = index_col pd.testing.assert_frame_equal(df_out, expected, check_index_type=False) @@ -377,7 +385,9 @@ def test_to_parquet_index(scalars_dfs, gcs_folder, index): scalars_pandas_df.index = scalars_pandas_df.index.astype("Int64") # Ordering should be maintained for tables smaller than 1 GB. - pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) + pd.testing.assert_frame_equal( + gcs_df.drop("bytes_col", axis=1), scalars_pandas_df.drop("bytes_col", axis=1) + ) def test_to_sql_query_unnamed_index_included( diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 1708735f4cc..2d4e1f02043 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -394,14 +394,17 @@ def test_multi_index_dataframe_groupby_level_aggregate( def test_multi_index_dataframe_groupby_level_analytic( scalars_df_index, scalars_pandas_df_index, level, as_index ): + # Drop "numeric_col" as pandas doesn't support numerics for grouped window function bf_result = ( - scalars_df_index.set_index(["int64_too", "bool_col"]) + scalars_df_index.drop("numeric_col", axis=1) + .set_index(["int64_too", "bool_col"]) .groupby(level=level, as_index=as_index, dropna=False) .cumsum(numeric_only=True) .to_pandas() ) pd_result = ( - scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) + scalars_pandas_df_index.drop("numeric_col", axis=1) + .set_index(["int64_too", "bool_col"]) .groupby(level=level, as_index=as_index, dropna=False) .cumsum(numeric_only=True) ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 623da74aa44..6f919f740fe 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -24,7 +24,11 @@ import bigframes.pandas import bigframes.series as series -from tests.system.utils import assert_pandas_df_equal, assert_series_equal +from tests.system.utils import ( + assert_pandas_df_equal, + assert_series_equal, + skip_legacy_pandas, +) def test_series_construct_copy(scalars_dfs): @@ -81,14 +85,14 @@ def test_series_construct_from_list_escaped_strings(): [ ("bool_col", pd.BooleanDtype()), # TODO(swast): Use a more efficient type. - ("bytes_col", numpy.dtype("object")), + ("bytes_col", pd.ArrowDtype(pa.binary())), ("date_col", pd.ArrowDtype(pa.date32())), ("datetime_col", pd.ArrowDtype(pa.timestamp("us"))), ("float64_col", pd.Float64Dtype()), ("geography_col", gpd.array.GeometryDtype()), ("int64_col", pd.Int64Dtype()), # TODO(swast): Use a more efficient type. - ("numeric_col", numpy.dtype("object")), + ("numeric_col", pd.ArrowDtype(pa.decimal128(38, 9))), ("int64_too", pd.Int64Dtype()), ("string_col", pd.StringDtype(storage="pyarrow")), ("time_col", pd.ArrowDtype(pa.time64("us"))), @@ -2519,8 +2523,12 @@ def test_mask_custom_value(scalars_dfs): ("int64_col", pd.Float64Dtype()), ("int64_col", "string[pyarrow]"), ("int64_col", "boolean"), + ("int64_col", pd.ArrowDtype(pa.decimal128(38, 9))), + ("int64_col", pd.ArrowDtype(pa.decimal256(76, 38))), ("bool_col", "Int64"), ("bool_col", "string[pyarrow]"), + ("string_col", "binary[pyarrow]"), + ("bytes_col", "string[pyarrow]"), # pandas actually doesn't let folks convert to/from naive timestamp and # raises a deprecation warning to use tz_localize/tz_convert instead, # but BigQuery always stores values as UTC and doesn't have to deal @@ -2538,6 +2546,7 @@ def test_mask_custom_value(scalars_dfs): # https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions ], ) +@skip_legacy_pandas def test_astype(scalars_df_index, scalars_pandas_df_index, column, to_type): bf_result = scalars_df_index[column].astype(to_type).to_pandas() pd_result = scalars_pandas_df_index[column].astype(to_type) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index e6eb40a5fa1..8ce442376a1 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -30,6 +30,7 @@ import bigframes.dataframe import bigframes.dtypes import bigframes.ml.linear_model +from tests.system.utils import skip_legacy_pandas FIRST_FILE = "000000000000" @@ -385,6 +386,7 @@ def test_read_pandas_tokyo( pd.testing.assert_frame_equal(result, expected) +@skip_legacy_pandas def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs if scalars_df.index.name is not None: @@ -441,6 +443,7 @@ def test_read_csv_gcs_bq_engine(session, scalars_dfs, gcs_folder): pytest.param("\t", id="custom_sep"), ], ) +@skip_legacy_pandas def test_read_csv_local_default_engine(session, scalars_dfs, sep): scalars_df, scalars_pandas_df = scalars_dfs with tempfile.TemporaryDirectory() as dir: diff --git a/tests/system/utils.py b/tests/system/utils.py index f49b5ece313..a4647b4f512 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -14,11 +14,23 @@ import base64 import decimal +import functools import geopandas as gpd # type: ignore import numpy as np import pandas as pd import pyarrow as pa # type: ignore +import pytest + + +def skip_legacy_pandas(test): + @functools.wraps(test) + def wrapper(*args, **kwds): + if pd.__version__.startswith("1."): + pytest.skip("Skips pandas 1.x as not compatible with 2.x behavior.") + return test(*args, **kwds) + + return wrapper def assert_pandas_df_equal(df0, df1, ignore_order: bool = False, **kwargs): @@ -133,16 +145,28 @@ def convert_pandas_dtypes(df: pd.DataFrame, bytes_col: bool): df["geography_col"].replace({np.nan: None}) ) - # Convert bytes types column. - if bytes_col: + if bytes_col and not isinstance(df["bytes_col"].dtype, pd.ArrowDtype): df["bytes_col"] = df["bytes_col"].apply( lambda value: base64.b64decode(value) if not pd.isnull(value) else value ) + arrow_table = pa.Table.from_pandas( + pd.DataFrame(df, columns=["bytes_col"]), + schema=pa.schema([("bytes_col", pa.binary())]), + ) + df["bytes_col"] = arrow_table.to_pandas(types_mapper=pd.ArrowDtype)["bytes_col"] - # Convert numeric types column. - df["numeric_col"] = df["numeric_col"].apply( - lambda value: decimal.Decimal(str(value)) if value else None # type: ignore - ) + if not isinstance(df["numeric_col"].dtype, pd.ArrowDtype): + # Convert numeric types column. + df["numeric_col"] = df["numeric_col"].apply( + lambda value: decimal.Decimal(str(value)) if value else None # type: ignore + ) + arrow_table = pa.Table.from_pandas( + pd.DataFrame(df, columns=["numeric_col"]), + schema=pa.schema([("numeric_col", pa.decimal128(38, 9))]), + ) + df["numeric_col"] = arrow_table.to_pandas(types_mapper=pd.ArrowDtype)[ + "numeric_col" + ] def assert_pandas_df_equal_pca_components(actual, expected, **kwargs): diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index 6ceaaf911b9..e648fd28ccb 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -31,11 +31,11 @@ # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types pytest.param( ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), - np.dtype("O"), + pd.ArrowDtype(pa.decimal256(76, 38)), id="bignumeric", ), pytest.param(ibis_dtypes.boolean, pd.BooleanDtype(), id="bool"), - pytest.param(ibis_dtypes.binary, np.dtype("O"), id="bytes"), + pytest.param(ibis_dtypes.binary, pd.ArrowDtype(pa.binary()), id="bytes"), pytest.param(ibis_dtypes.date, pd.ArrowDtype(pa.date32()), id="date"), pytest.param( ibis_dtypes.Timestamp(), pd.ArrowDtype(pa.timestamp("us")), id="datetime" @@ -49,10 +49,9 @@ pytest.param(ibis_dtypes.int8, pd.Int64Dtype(), id="int8-as-int64"), pytest.param(ibis_dtypes.int64, pd.Int64Dtype(), id="int64"), # TODO(tswast): custom dtype (or at least string dtype) for JSON objects - pytest.param(ibis_dtypes.json, np.dtype("O"), id="json"), pytest.param( ibis_dtypes.Decimal(precision=38, scale=9, nullable=True), - np.dtype("O"), + pd.ArrowDtype(pa.decimal128(38, 9)), id="numeric", ), pytest.param( From 9c5012ec68275db83d1f6f7e743f5edaaaacd8cb Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 21 Dec 2023 23:41:45 +0000 Subject: [PATCH 234/921] docs: code samples for `drop` and `fillna` (#284) --- .../bigframes_vendored/pandas/core/frame.py | 141 ++++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 81 ++++++++++ 2 files changed, 222 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 00be9e5e9ee..427e586c526 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -869,6 +869,97 @@ def drop( Remove columns by directly specifying column names. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame(np.arange(12).reshape(3, 4), + ... columns=['A', 'B', 'C', 'D']) + >>> df + A B C D + 0 0 1 2 3 + 1 4 5 6 7 + 2 8 9 10 11 + + [3 rows x 4 columns] + + Drop columns: + + >>> df.drop(['B', 'C'], axis=1) + A D + 0 0 3 + 1 4 7 + 2 8 11 + + [3 rows x 2 columns] + + >>> df.drop(columns=['B', 'C']) + A D + 0 0 3 + 1 4 7 + 2 8 11 + + [3 rows x 2 columns] + + Drop a row by index: + + >>> df.drop([0, 1]) + A B C D + 2 8 9 10 11 + + [1 rows x 4 columns] + + Drop columns and/or rows of MultiIndex DataFrame: + + >>> import pandas as pd + >>> midx = pd.MultiIndex(levels=[['llama', 'cow', 'falcon'], + ... ['speed', 'weight', 'length']], + ... codes=[[0, 0, 0, 1, 1, 1, 2, 2, 2], + ... [0, 1, 2, 0, 1, 2, 0, 1, 2]]) + >>> df = bpd.DataFrame(index=midx, columns=['big', 'small'], + ... data=[[45, 30], [200, 100], [1.5, 1], [30, 20], + ... [250, 150], [1.5, 0.8], [320, 250], + ... [1, 0.8], [0.3, 0.2]]) + >>> df + big small + llama speed 45.0 30.0 + weight 200.0 100.0 + length 1.5 1.0 + cow speed 30.0 20.0 + weight 250.0 150.0 + length 1.5 0.8 + falcon speed 320.0 250.0 + weight 1.0 0.8 + length 0.3 0.2 + + [9 rows x 2 columns] + + Drop a specific index and column combination from the MultiIndex + DataFrame, i.e., drop the index ``'cow'`` and column ``'small'``: + + >>> df.drop(index='cow', columns='small') + big + llama speed 45.0 + weight 200.0 + length 1.5 + falcon speed 320.0 + weight 1.0 + length 0.3 + + [6 rows x 1 columns] + + >>> df.drop(index='length', level=1) + big small + llama speed 45.0 30.0 + weight 200.0 100.0 + cow speed 30.0 20.0 + weight 250.0 150.0 + falcon speed 320.0 250.0 + weight 1.0 0.8 + + [6 rows x 2 columns] + Args: labels: Index or column labels to drop. @@ -4343,6 +4434,56 @@ def fillna(self, value): """ Fill NA/NaN values using the specified method. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame([[np.nan, 2, np.nan, 0], + ... [3, 4, np.nan, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [np.nan, 3, np.nan, 4]], + ... columns=list("ABCD")).astype("Float64") + >>> df + A B C D + 0 2.0 0.0 + 1 3.0 4.0 1.0 + 2 + 3 3.0 4.0 + + [4 rows x 4 columns] + + Replace all NA elements with 0s. + + >>> df.fillna(0) + A B C D + 0 0.0 2.0 0.0 0.0 + 1 3.0 4.0 0.0 1.0 + 2 0.0 0.0 0.0 0.0 + 3 0.0 3.0 0.0 4.0 + + [4 rows x 4 columns] + + You can use fill values from another DataFrame: + + >>> df_fill = bpd.DataFrame(np.arange(12).reshape(3, 4), + ... columns=['A', 'B', 'C', 'D']) + >>> df_fill + A B C D + 0 0 1 2 3 + 1 4 5 6 7 + 2 8 9 10 11 + + [3 rows x 4 columns] + >>> df.fillna(df_fill) + A B C D + 0 0.0 2.0 2.0 0.0 + 1 3.0 4.0 6.0 1.0 + 2 8.0 9.0 10.0 11.0 + 3 3.0 4.0 + + [4 rows x 4 columns] + Args: value (scalar, Series): Value to use to fill holes (e.g. 0), alternately a diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 366f32c77ef..01cc3a0500a 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1062,6 +1062,55 @@ def drop( When using a multi-index, labels on different levels can be removed by specifying the level. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(data=np.arange(3), index=['A', 'B', 'C']) + >>> s + A 0 + B 1 + C 2 + dtype: Int64 + + Drop labels B and C: + + >>> s.drop(labels=['B', 'C']) + A 0 + dtype: Int64 + + Drop 2nd level label in MultiIndex Series: + + >>> import pandas as pd + >>> midx = pd.MultiIndex(levels=[['llama', 'cow', 'falcon'], + ... ['speed', 'weight', 'length']], + ... codes=[[0, 0, 0, 1, 1, 1, 2, 2, 2], + ... [0, 1, 2, 0, 1, 2, 0, 1, 2]]) + + >>> s = bpd.Series([45, 200, 1.2, 30, 250, 1.5, 320, 1, 0.3], + ... index=midx) + >>> s + llama speed 45.0 + weight 200.0 + length 1.2 + cow speed 30.0 + weight 250.0 + length 1.5 + falcon speed 320.0 + weight 1.0 + length 0.3 + dtype: Float64 + + >>> s.drop(labels='weight', level=1) + llama speed 45.0 + length 1.2 + cow speed 30.0 + length 1.5 + falcon speed 320.0 + length 0.3 + dtype: Float64 + Args: labels (single label or list-like): Index labels to drop. @@ -1193,6 +1242,38 @@ def fillna( """ Fill NA/NaN values using the specified method. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([np.nan, 2, np.nan, -1]) + >>> s + 0 + 1 2.0 + 2 + 3 -1.0 + dtype: Float64 + + Replace all NA elements with 0s. + + >>> s.fillna(0) + 0 0.0 + 1 2.0 + 2 0.0 + 3 -1.0 + dtype: Float64 + + You can use fill values from another Series: + + >>> s_fill = bpd.Series([11, 22, 33]) + >>> s.fillna(s_fill) + 0 11.0 + 1 2.0 + 2 33.0 + 3 -1.0 + dtype: Float64 + Args: value (scalar, dict, Series, or DataFrame, default None): Value to use to fill holes (e.g. 0). From acc0eb7010951c8cfb91aecc45268b041217dd09 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 22 Dec 2023 02:45:36 +0000 Subject: [PATCH 235/921] docs: code samples for `reset_index` and `sort_values` (#282) * docs: code samples for `reset_index` and `sort_values` * fix alignment in dataframe api code samples --- .../bigframes_vendored/pandas/core/frame.py | 161 ++++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 110 ++++++++++++ 2 files changed, 271 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 427e586c526..fb341937108 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1138,6 +1138,93 @@ def reset_index( Reset the index of the DataFrame, and use the default one instead. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> import numpy as np + >>> df = bpd.DataFrame([('bird', 389.0), + ... ('bird', 24.0), + ... ('mammal', 80.5), + ... ('mammal', np.nan)], + ... index=['falcon', 'parrot', 'lion', 'monkey'], + ... columns=('class', 'max_speed')) + >>> df + class max_speed + falcon bird 389.0 + parrot bird 24.0 + lion mammal 80.5 + monkey mammal + + [4 rows x 2 columns] + + When we reset the index, the old index is added as a column, and a new sequential index is used: + + >>> df.reset_index() + index class max_speed + 0 falcon bird 389.0 + 1 parrot bird 24.0 + 2 lion mammal 80.5 + 3 monkey mammal + + [4 rows x 3 columns] + + We can use the ``drop`` parameter to avoid the old index being added as a column: + + >>> df.reset_index(drop=True) + class max_speed + 0 bird 389.0 + 1 bird 24.0 + 2 mammal 80.5 + 3 mammal + + [4 rows x 2 columns] + + You can also use ``reset_index`` with ``MultiIndex``. + + >>> import pandas as pd + >>> index = pd.MultiIndex.from_tuples([('bird', 'falcon'), + ... ('bird', 'parrot'), + ... ('mammal', 'lion'), + ... ('mammal', 'monkey')], + ... names=['class', 'name']) + >>> columns = ['speed', 'max'] + >>> df = bpd.DataFrame([(389.0, 'fly'), + ... (24.0, 'fly'), + ... (80.5, 'run'), + ... (np.nan, 'jump')], + ... index=index, + ... columns=columns) + >>> df + speed max + class name + bird falcon 389.0 fly + parrot 24.0 fly + mammal lion 80.5 run + monkey jump + + [4 rows x 2 columns] + + >>> df.reset_index() + class name speed max + 0 bird falcon 389.0 fly + 1 bird parrot 24.0 fly + 2 mammal lion 80.5 run + 3 mammal monkey jump + + [4 rows x 4 columns] + + >>> df.reset_index(drop=True) + speed max + 0 389.0 fly + 1 24.0 fly + 2 80.5 run + 3 jump + + [4 rows x 2 columns] + + Args: drop (bool, default False): Do not try to insert index into dataframe columns. This resets @@ -1347,6 +1434,80 @@ def sort_values( ) -> DataFrame: """Sort by the values along row axis. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'col1': ['A', 'A', 'B', bpd.NA, 'D', 'C'], + ... 'col2': [2, 1, 9, 8, 7, 4], + ... 'col3': [0, 1, 9, 4, 2, 3], + ... 'col4': ['a', 'B', 'c', 'D', 'e', 'F'] + ... }) + >>> df + col1 col2 col3 col4 + 0 A 2 0 a + 1 A 1 1 B + 2 B 9 9 c + 3 8 4 D + 4 D 7 2 e + 5 C 4 3 F + + [6 rows x 4 columns] + + Sort by col1: + + >>> df.sort_values(by=['col1']) + col1 col2 col3 col4 + 0 A 2 0 a + 1 A 1 1 B + 2 B 9 9 c + 5 C 4 3 F + 4 D 7 2 e + 3 8 4 D + + [6 rows x 4 columns] + + Sort by multiple columns: + + >>> df.sort_values(by=['col1', 'col2']) + col1 col2 col3 col4 + 1 A 1 1 B + 0 A 2 0 a + 2 B 9 9 c + 5 C 4 3 F + 4 D 7 2 e + 3 8 4 D + + [6 rows x 4 columns] + + Sort Descending: + + >>> df.sort_values(by='col1', ascending=False) + col1 col2 col3 col4 + 4 D 7 2 e + 5 C 4 3 F + 2 B 9 9 c + 0 A 2 0 a + 1 A 1 1 B + 3 8 4 D + + [6 rows x 4 columns] + + Putting NAs first: + + >>> df.sort_values(by='col1', ascending=False, na_position='first') + col1 col2 col3 col4 + 3 8 4 D + 4 D 7 2 e + 5 C 4 3 F + 2 B 9 9 c + 0 A 2 0 a + 1 A 1 1 B + + [6 rows x 4 columns] + Args: by (str or Sequence[str]): Name or list of names to sort by. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 01cc3a0500a..778ad68e0eb 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -168,6 +168,53 @@ def reset_index( when the index is meaningless and needs to be reset to the default before another operation. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3, 4], name='foo', + ... index=['a', 'b', 'c', 'd']) + >>> s.index.name = "idx" + >>> s + idx + a 1 + b 2 + c 3 + d 4 + Name: foo, dtype: Int64 + + Generate a DataFrame with default index. + + >>> s.reset_index() + idx foo + 0 a 1 + 1 b 2 + 2 c 3 + 3 d 4 + + [4 rows x 2 columns] + + To specify the name of the new column use ``name`` param. + + >>> s.reset_index(name="bar") + idx bar + 0 a 1 + 1 b 2 + 2 c 3 + 3 d 4 + + [4 rows x 2 columns] + + To generate a new Series with the default index set param ``drop=True``. + + >>> s.reset_index(drop=True) + 0 1 + 1 2 + 2 3 + 3 4 + Name: foo, dtype: Int64 + Args: drop (bool, default False): Just reset the index, without inserting it as a column in @@ -699,6 +746,69 @@ def sort_values( Sort a Series in ascending or descending order by some criterion. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([np.nan, 1, 3, 10, 5]) + >>> s + 0 + 1 1.0 + 2 3.0 + 3 10.0 + 4 5.0 + dtype: Float64 + + Sort values ascending order (default behaviour): + + >>> s.sort_values(ascending=True) + 1 1.0 + 2 3.0 + 4 5.0 + 3 10.0 + 0 + dtype: Float64 + + Sort values descending order: + + >>> s.sort_values(ascending=False) + 3 10.0 + 4 5.0 + 2 3.0 + 1 1.0 + 0 + dtype: Float64 + + Sort values putting NAs first: + + >>> s.sort_values(na_position='first') + 0 + 1 1.0 + 2 3.0 + 4 5.0 + 3 10.0 + dtype: Float64 + + Sort a series of strings: + + >>> s = bpd.Series(['z', 'b', 'd', 'a', 'c']) + >>> s + 0 z + 1 b + 2 d + 3 a + 4 c + dtype: string + + >>> s.sort_values() + 3 a + 1 b + 4 c + 2 d + 0 z + dtype: string + Args: axis (0 or 'index'): Unused. Parameter needed for compatibility with DataFrame. From ad51035bcf80d6a49f134df26624b578010b5b12 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 26 Dec 2023 18:29:00 +0000 Subject: [PATCH 236/921] docs: code samples for `isna`, `isnull`, `dropna`, `isin` (#289) * docs: code samples for `isna`, `isnull`, `dropna`, `isin` * fix header alignment in rendering --- .../bigframes_vendored/pandas/core/frame.py | 81 +++++++++++++++++++ .../bigframes_vendored/pandas/core/generic.py | 65 +++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 70 ++++++++++++++++ 3 files changed, 216 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index fb341937108..2de63b91031 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1289,9 +1289,57 @@ def duplicated(self, subset=None, keep="first"): def dropna( self, + *, + axis: int | str = 0, + how: str = "any", + ignore_index=False, ) -> DataFrame: """Remove missing values. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"name": ['Alfred', 'Batman', 'Catwoman'], + ... "toy": [np.nan, 'Batmobile', 'Bullwhip'], + ... "born": [bpd.NA, "1940-04-25", bpd.NA]}) + >>> df + name toy born + 0 Alfred + 1 Batman Batmobile 1940-04-25 + 2 Catwoman Bullwhip + + [3 rows x 3 columns] + + Drop the rows where at least one element is missing: + + >>> df.dropna() + name toy born + 1 Batman Batmobile 1940-04-25 + + [1 rows x 3 columns] + + Drop the columns where at least one element is missing. + + >>> df.dropna(axis='columns') + name + 0 Alfred + 1 Batman + 2 Catwoman + + [3 rows x 1 columns] + + Drop the rows where all elements are missing: + + >>> df.dropna(how='all') + name toy born + 0 Alfred + 1 Batman Batmobile 1940-04-25 + 2 Catwoman Bullwhip + + [3 rows x 3 columns] + Args: axis ({0 or 'index', 1 or 'columns'}, default 'columns'): Determine if rows or columns which contain missing values are @@ -1318,6 +1366,39 @@ def isin(self, values): """ Whether each element in the DataFrame is contained in values. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'num_legs': [2, 4], 'num_wings': [2, 0]}, + ... index=['falcon', 'dog']) + >>> df + num_legs num_wings + falcon 2 2 + dog 4 0 + + [2 rows x 2 columns] + + When ``values`` is a list check whether every value in the DataFrame is + present in the list (which animals have 0 or 2 legs or wings). + + >>> df.isin([0, 2]) + num_legs num_wings + falcon True True + dog False True + + [2 rows x 2 columns] + + When ``values`` is a dict, we can pass it to check for each column separately: + + >>> df.isin({'num_wings': [0, 3]}) + num_legs num_wings + falcon False False + dog False True + + [2 rows x 2 columns] + Args: values (iterable, or dict): The result will only be true at a location if all the diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index ca5c6344ce8..2885162fd61 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -499,6 +499,71 @@ def isna(self) -> NDFrame: False values. Characters such as empty strings ``''`` or :attr:`numpy.inf` are not considered NA values. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> import numpy as np + + >>> df = bpd.DataFrame(dict( + ... age=[5, 6, np.nan], + ... born=[bpd.NA, "1940-04-25", "1940-04-25"], + ... name=['Alfred', 'Batman', ''], + ... toy=[None, 'Batmobile', 'Joker'], + ... )) + >>> df + age born name toy + 0 5.0 Alfred + 1 6.0 1940-04-25 Batman Batmobile + 2 1940-04-25 Joker + + [3 rows x 4 columns] + + Show which entries in a DataFrame are NA: + + >>> df.isna() + age born name toy + 0 False True False True + 1 False False False False + 2 True False False False + + [3 rows x 4 columns] + + >>> df.isnull() + age born name toy + 0 False True False True + 1 False False False False + 2 True False False False + + [3 rows x 4 columns] + + Show which entries in a Series are NA: + + >>> ser = bpd.Series([5, None, 6, np.nan, bpd.NA]) + >>> ser + 0 5.0 + 1 + 2 6.0 + 3 + 4 + dtype: Float64 + + >>> ser.isna() + 0 False + 1 True + 2 False + 3 True + 4 True + dtype: boolean + + >>> ser.isnull() + 0 False + 1 True + 2 False + 3 True + 4 True + dtype: boolean + Returns: Mask of bool values for each element that indicates whether an element is an NA value. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 778ad68e0eb..cbe09630512 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1460,6 +1460,42 @@ def dropna(self, *, axis=0, inplace: bool = False, how=None) -> Series: """ Return a new Series with missing values removed. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Drop NA values from a Series: + + >>> ser = bpd.Series([1., 2., np.nan]) + >>> ser + 0 1.0 + 1 2.0 + 2 + dtype: Float64 + + >>> ser.dropna() + 0 1.0 + 1 2.0 + dtype: Float64 + + Empty strings are not considered NA values. ``None`` is considered an NA value. + + >>> ser = bpd.Series(['2', bpd.NA, '', None, 'I stay'], dtype='object') + >>> ser + 0 2 + 1 + 2 + 3 + 4 I stay + dtype: string + + >>> ser.dropna() + 0 2 + 2 + 4 I stay + dtype: string + Args: axis (0 or 'index'): Unused. Parameter needed for compatibility with DataFrame. @@ -2531,6 +2567,40 @@ def isin(self, values): the same. That is, if any form of NaN is present in values, all forms of NaN in the series will be considered a match. (though pandas may not) + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['llama', 'cow', 'llama', 'beetle', 'llama', + ... 'hippo'], name='animal') + >>> s + 0 llama + 1 cow + 2 llama + 3 beetle + 4 llama + 5 hippo + Name: animal, dtype: string + + >>> s.isin(['cow', 'llama']) + 0 True + 1 True + 2 True + 3 False + 4 True + 5 False + Name: animal, dtype: boolean + + Strings and integers are distinct and are therefore not comparable: + + >>> bpd.Series([1]).isin(['1']) + 0 False + dtype: boolean + >>> bpd.Series([1.1]).isin(['1.1']) + 0 False + dtype: boolean + Args: values (list-like): The sequence of values to test. Passing in a single string will raise a From 0e1bbfc1055aff9757b5138907c11caab2f3965a Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 26 Dec 2023 19:10:15 +0000 Subject: [PATCH 237/921] docs: code samples for `Series.{add, replace, unique, T, transpose}` (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - `Series.add`: https://screenshot.googleplex.com/763p36yJKKvM5DY - `Series.replace`: https://screenshot.googleplex.com/9MHuQZnKakqjjJn - `Series.unique`: https://screenshot.googleplex.com/7BfuQE3bFcYASqu - `Series.T`: https://screenshot.googleplex.com/8cSYpwKXrYetsEg - `Series.transpose`: https://screenshot.googleplex.com/7mM2zBwxRiqfDUV Fixes internal issue 317297573 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 4 +- .../bigframes_vendored/pandas/core/series.py | 190 +++++++++++++++++- 2 files changed, 191 insertions(+), 3 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 2de63b91031..d7ecae102b9 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4371,7 +4371,7 @@ def stack(self, level=-1): BigQuery DataFrames does not support stack operations that would combine columns of different dtypes. - **Example:** + **Examples:** >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None @@ -4410,7 +4410,7 @@ def unstack(self, level=-1): If the index is not a MultiIndex, the output will be a Series (the analogue of stack when the columns are not a MultiIndex). - **Example:** + **Examples:** >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index cbe09630512..b0a4cb81939 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -143,13 +143,51 @@ def name(self) -> Hashable: @property def T(self) -> Series: - """Return the transpose, which is by definition self.""" + """Return the transpose, which is by definition self. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['Ant', 'Bear', 'Cow']) + >>> s + 0 Ant + 1 Bear + 2 Cow + dtype: string + + >>> s.T + 0 Ant + 1 Bear + 2 Cow + dtype: string + + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def transpose(self) -> Series: """ Return the transpose, which is by definition self. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['Ant', 'Bear', 'Cow']) + >>> s + 0 Ant + 1 Bear + 2 Cow + dtype: string + + >>> s.transpose() + 0 Ant + 1 Bear + 2 Cow + dtype: string + Returns: Series: Series. """ @@ -539,6 +577,36 @@ def nunique(self) -> int: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def unique(self) -> Series: + """ + Return unique values of Series object. + + Uniques are returned in order of appearance. Hash table-based unique, + therefore does NOT sort. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([2, 1, 3, 3], name='A') + >>> s + 0 2 + 1 1 + 2 3 + 3 3 + Name: A, dtype: Int64 + >>> s.unique() + 0 2 + 1 1 + 2 3 + Name: A, dtype: Int64 + + Returns: + Series: The unique values returned as a Series. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def mode(self) -> Series: """ Return the mode(s) of the Series. @@ -1405,6 +1473,77 @@ def replace( This differs from updating with ``.loc`` or ``.iloc``, which require you to specify a location to update with some value. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3, 4, 5]) + >>> s + 0 1 + 1 2 + 2 3 + 3 4 + 4 5 + dtype: Int64 + + >>> s.replace(1, 5) + 0 5 + 1 2 + 2 3 + 3 4 + 4 5 + dtype: Int64 + + You can replace a list of values: + + >>> s.replace([1, 3, 5], -1) + 0 -1 + 1 2 + 2 -1 + 3 4 + 4 -1 + dtype: Int64 + + You can use a replacement mapping: + + >>> s.replace({1: 5, 3: 10}) + 0 5 + 1 2 + 2 10 + 3 4 + 4 5 + dtype: Int64 + + With a string Series you can use a simple string replacement or a regex + replacement: + + >>> s = bpd.Series(["Hello", "Another Hello"]) + >>> s.replace("Hello", "Hi") + 0 Hi + 1 Another Hello + dtype: string + + >>> s.replace("Hello", "Hi", regex=True) + 0 Hi + 1 Another Hi + dtype: string + + >>> s.replace("^Hello", "Hi", regex=True) + 0 Hi + 1 Another Hello + dtype: string + + >>> s.replace("Hello$", "Hi", regex=True) + 0 Hi + 1 Another Hi + dtype: string + + >>> s.replace("[Hh]e", "__", regex=True) + 0 __llo + 1 Anot__r __llo + dtype: string + Args: to_replace (str, regex, list, int, float or None): How to find the values that will be replaced. @@ -1702,6 +1841,55 @@ def add(self, other) -> Series: Equivalent to ``series + other``, but with support to substitute a fill_value for missing data in either one of the inputs. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> a = bpd.Series([1, 2, 3, bpd.NA]) + >>> a + 0 1.0 + 1 2.0 + 2 3.0 + 3 + dtype: Float64 + + >>> b = bpd.Series([10, 20, 30, 40]) + >>> b + 0 10 + 1 20 + 2 30 + 3 40 + dtype: Int64 + + >>> a.add(b) + 0 11.0 + 1 22.0 + 2 33.0 + 3 + dtype: Float64 + + You can also use the mathematical operator ``+``: + + >>> a + b + 0 11.0 + 1 22.0 + 2 33.0 + 3 + dtype: Float64 + + Adding two Series with explicit indexes: + + >>> a = bpd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd']) + >>> b = bpd.Series([10, 20, 30, 40], index=['a', 'b', 'd', 'e']) + >>> a.add(b) + a 11 + b 22 + c + d 34 + e + dtype: Int64 + Args: other (Series, or scalar value): From 2cd64891170dcd4f2a709024a2993e36db210976 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:36:15 -0800 Subject: [PATCH 238/921] feat: Add dataframe.to_html (#259) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes b/296945119 --- bigframes/dataframe.py | 52 ++++++++ tests/system/small/test_dataframe.py | 9 ++ .../bigframes_vendored/pandas/core/frame.py | 124 ++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 423c2bcaac4..ab0006ea20c 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2682,6 +2682,58 @@ def to_string( encoding, ) + def to_html( + self, + buf=None, + columns: Sequence[str] | None = None, + col_space=None, + header: bool = True, + index: bool = True, + na_rep: str = "NaN", + formatters=None, + float_format=None, + sparsify: bool | None = None, + index_names: bool = True, + justify: str | None = None, + max_rows: int | None = None, + max_cols: int | None = None, + show_dimensions: bool = False, + decimal: str = ".", + bold_rows: bool = True, + classes: str | list | tuple | None = None, + escape: bool = True, + notebook: bool = False, + border: int | None = None, + table_id: str | None = None, + render_links: bool = False, + encoding: str | None = None, + ) -> str: + return self.to_pandas().to_html( + buf, + columns, # type: ignore + col_space, + header, + index, + na_rep, + formatters, + float_format, + sparsify, + index_names, + justify, # type: ignore + max_rows, + max_cols, + show_dimensions, + decimal, + bold_rows, + classes, + escape, + notebook, + border, + table_id, + render_links, + encoding, + ) + def to_markdown( self, buf=None, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 86b8cfbe669..cb2e4f94fad 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3463,6 +3463,15 @@ def test_df_to_string(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result +def test_df_to_html(scalars_df_index, scalars_pandas_df_index): + unsupported = ["numeric_col"] # formatted differently + + bf_result = scalars_df_index.drop(columns=unsupported).to_html() + pd_result = scalars_pandas_df_index.drop(columns=unsupported).to_html() + + assert bf_result == pd_result + + def test_df_to_markdown(scalars_df_index, scalars_pandas_df_index): # Nulls have bug from tabulate https://github.com/astanin/python-tabulate/issues/231 bf_result = scalars_df_index.dropna().to_markdown() diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index d7ecae102b9..f2de8fcb6ab 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -685,6 +685,130 @@ def to_string( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def to_html( + self, + buf=None, + columns: Sequence[str] | None = None, + col_space=None, + header: bool = True, + index: bool = True, + na_rep: str = "NaN", + formatters=None, + float_format=None, + sparsify: bool | None = None, + index_names: bool = True, + justify: str | None = None, + max_rows: int | None = None, + max_cols: int | None = None, + show_dimensions: bool = False, + decimal: str = ".", + bold_rows: bool = True, + classes: str | list | tuple | None = None, + escape: bool = True, + notebook: bool = False, + border: int | None = None, + table_id: str | None = None, + render_links: bool = False, + encoding: str | None = None, + ): + """Render a DataFrame as an HTML table. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> print(df.to_html()) + + + + + + + + + + + + + + + + + + + + +
col1col2
013
124
+ + Args: + buf (str, Path or StringIO-like, optional, default None): + Buffer to write to. If None, the output is returned as a string. + columns (sequence, optional, default None): + The subset of columns to write. Writes all columns by default. + col_space (str or int, list or dict of int or str, optional): + The minimum width of each column in CSS length units. An int is + assumed to be px units. + header (bool, optional): + Whether to print column labels, default True. + index (bool, optional, default True): + Whether to print index (row) labels. + na_rep (str, optional, default 'NaN'): + String representation of NAN to use. + formatters (list, tuple or dict of one-param. functions, optional): + Formatter functions to apply to columns' elements by position or + name. + The result of each function must be a unicode string. + List/tuple must be of length equal to the number of columns. + float_format (one-parameter function, optional, default None): + Formatter function to apply to columns' elements if they are + floats. This function must return a unicode string and will + be applied only to the non-NaN elements, with NaN being + handled by na_rep. + sparsify (bool, optional, default True): + Set to False for a DataFrame with a hierarchical index to print + every multiindex key at each row. + index_names (bool, optional, default True): + Prints the names of the indexes. + justify (str, default None): + How to justify the column labels. If None uses the option from + the print configuration (controlled by set_option), 'right' out + of the box. Valid values are, 'left', 'right', 'center', 'justify', + 'justify-all', 'start', 'end', 'inherit', 'match-parent', 'initial', + 'unset'. + max_rows (int, optional): + Maximum number of rows to display in the console. + max_cols (int, optional): + Maximum number of columns to display in the console. + show_dimensions (bool, default False): + Display DataFrame dimensions (number of rows by number of columns). + decimal (str, default '.'): + Character recognized as decimal separator, e.g. ',' in Europe. + bold_rows (bool, default True): + Make the row labels bold in the output. + classes (str or list or tuple, default None): + CSS class(es) to apply to the resulting html table. + escape (bool, default True): + Convert the characters <, >, and & to HTML-safe sequences. + notebook (bool, default False): + Whether the generated HTML is for IPython Notebook. + border (int): + A border=border attribute is included in the opening + tag. Default pd.options.display.html.border. + table_id (str, optional): + A css id is included in the opening
tag if specified. + render_links (bool, default False): + Convert URLs to HTML links. + encoding (str, default "utf-8"): + Set character encoding. + + Returns: + str or None: If buf is None, returns the result as a string. Otherwise + returns None. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def to_markdown( self, buf=None, From ac1a745ddce9865f4585777b43c2234b9bf2841d Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 28 Dec 2023 18:00:22 +0000 Subject: [PATCH 239/921] fix: exclude pandas 2.2.0rc0 to unblock prerelease tests (#292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 317908521 🦕 --- noxfile.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index c4bbd7a65a6..1d3624005a7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -518,9 +518,13 @@ def prerelease(session: nox.sessions.Session, tests_path): "--prefer-binary", "--pre", "--upgrade", - # TODO(shobs): Remove tying to version 2.1.3 after - # https://github.com/pandas-dev/pandas/issues/56463 is resolved - "pandas!=2.1.4", + # TODO(shobs): Remove excluding version 2.1.4 after + # https://github.com/pandas-dev/pandas/issues/56463 is resolved. + # + # TODO(shobs): Remove excluding version 2.2.0rc0 after + # https://github.com/pandas-dev/pandas/issues/56646 and + # https://github.com/pandas-dev/pandas/issues/56651 are resolved. + "pandas!=2.1.4,!=2.2.0rc0", ) already_installed.add("pandas") From 252f3a2a0e1296c7d786acdc0bdebe9e4a9ae1be Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:54:15 -0800 Subject: [PATCH 240/921] docs: fix the rendering for `get_dummies` (#291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - docs: https://screenshot.googleplex.com/8X53mhLdQb2dQsd Fixes internal issue 317915956 🦕 --- third_party/bigframes_vendored/pandas/core/reshape/encoding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/encoding.py b/third_party/bigframes_vendored/pandas/core/reshape/encoding.py index da92b58f505..b7f67473ea1 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/encoding.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/encoding.py @@ -25,6 +25,7 @@ def get_dummies( prepended to the value. **Examples:** + >>> import bigframes.pandas as pd >>> pd.options.display.progress_bar = None >>> s = pd.Series(list('abca')) From 746115d5564c95bc3c4a5309c99e7a29e535e6fe Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:42:15 -0800 Subject: [PATCH 241/921] Fix: Update dataframe.to_gbq to dedup column names. (#286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 14 ++++---- tests/system/small/test_dataframe_io.py | 44 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ab0006ea20c..595670b0b6e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2759,26 +2759,28 @@ def _apply_unary_op(self, operation: ops.UnaryOp) -> DataFrame: def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: """Create query text representing this dataframe for I/O.""" array_value = self._block.expr + + new_col_labels, new_idx_labels = utils.get_standardized_ids( + self._block.column_labels, self.index.names + ) + columns = list(self._block.value_columns) - column_labels = list(self._block.column_labels) + column_labels = new_col_labels # This code drops unnamed indexes to keep consistent with the behavior of # most pandas write APIs. The exception is `pandas.to_csv`, which keeps # unnamed indexes as `Unnamed: 0`. # TODO(chelsealin): check if works for multiple indexes. if index and self.index.name is not None: columns.extend(self._block.index_columns) - column_labels.extend(self.index.names) + column_labels.extend(new_idx_labels) else: array_value = array_value.drop_columns(self._block.index_columns) # Make columns in SQL reflect _labels_ not _ids_. Note: This may use # the arbitrary unicode column labels feature in BigQuery, which is # currently (June 2023) in preview. - # TODO(swast): Handle duplicate and NULL labels. id_overrides = { - col_id: col_label - for col_id, col_label in zip(columns, column_labels) - if col_label and isinstance(col_label, str) + col_id: col_label for col_id, col_label in zip(columns, column_labels) } if ordering_id is not None: diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 59864e483e4..6f1b31b48ea 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -273,6 +273,50 @@ def test_to_gbq_if_exists( ) +def test_to_gbq_w_duplicate_column_names( + scalars_df_index, scalars_pandas_df_index, dataset_id +): + """Test the `to_gbq` API when dealing with duplicate column names.""" + destination_table = f"{dataset_id}.test_to_gbq_w_duplicate_column_names" + + # Renaming 'int64_too' to 'int64_col', which will result in 'int64_too' + # becoming 'int64_col_1' after deduplication. + scalars_df_index = scalars_df_index.rename(columns={"int64_too": "int64_col"}) + scalars_df_index.to_gbq(destination_table, if_exists="replace") + + bf_result = bpd.read_gbq(destination_table, index_col="rowindex").to_pandas() + + pd.testing.assert_series_equal( + scalars_pandas_df_index["int64_col"], bf_result["int64_col"] + ) + pd.testing.assert_series_equal( + scalars_pandas_df_index["int64_too"], + bf_result["int64_col_1"], + check_names=False, + ) + + +def test_to_gbq_w_None_column_names( + scalars_df_index, scalars_pandas_df_index, dataset_id +): + """Test the `to_gbq` API with None as a column name.""" + destination_table = f"{dataset_id}.test_to_gbq_w_none_column_names" + + scalars_df_index = scalars_df_index.rename(columns={"int64_too": None}) + scalars_df_index.to_gbq(destination_table, if_exists="replace") + + bf_result = bpd.read_gbq(destination_table, index_col="rowindex").to_pandas() + + pd.testing.assert_series_equal( + scalars_pandas_df_index["int64_col"], bf_result["int64_col"] + ) + pd.testing.assert_series_equal( + scalars_pandas_df_index["int64_too"], + bf_result["bigframes_unnamed_column"], + check_names=False, + ) + + def test_to_gbq_w_invalid_destination_table(scalars_df_index): with pytest.raises(ValueError): scalars_df_index.to_gbq("table_id") From 7cbc2b0ba572d11778ba7caf7c95b7fb8f3a31a7 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 29 Dec 2023 00:06:19 +0000 Subject: [PATCH 242/921] docs: code samples for `Series.{map, to_list, count}` (#290) docs: code samples for `DataFrame.copy` and `Series.copy` --- .../bigframes_vendored/pandas/core/generic.py | 61 ++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 70 +++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 2885162fd61..c079cbff7f9 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -448,6 +448,67 @@ def copy(self): and indices. Modifications to the data or indices of the copy will not be reflected in the original object. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Modification in the original Series will not affect the copy Series: + + >>> s = bpd.Series([1, 2], index=["a", "b"]) + >>> s + a 1 + b 2 + dtype: Int64 + + >>> s_copy = s.copy() + >>> s_copy + a 1 + b 2 + dtype: Int64 + + >>> s.loc['b'] = 22 + >>> s + a 1 + b 22 + dtype: Int64 + >>> s_copy + a 1 + b 2 + dtype: Int64 + + Modification in the original DataFrame will not affect the copy DataFrame: + + >>> df = bpd.DataFrame({'a': [1, 3], 'b': [2, 4]}) + >>> df + a b + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + >>> df_copy = df.copy() + >>> df_copy + a b + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + + >>> df.loc[df["b"] == 2, "b"] = 22 + >>> df + a b + 0 1 22.0 + 1 3 4.0 + + [2 rows x 2 columns] + >>> df_copy + a b + 0 1 2 + 1 3 4 + + [2 rows x 2 columns] + Returns: Object type matches caller. """ diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index b0a4cb81939..b97f9018dda 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -433,6 +433,21 @@ def tolist(self) -> list: (for str, int, float) or a pandas scalar (for Timestamp/Timedelta/Interval/Period). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3]) + >>> s + 0 1 + 1 2 + 2 3 + dtype: Int64 + + >>> s.to_list() + [1, 2, 3] + Returns: list: list of the values """ @@ -560,6 +575,20 @@ def count(self): """ Return number of non-NA/null observations in the Series. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([0.0, 1.0, bpd.NA]) + >>> s + 0 0.0 + 1 1.0 + 2 + dtype: Float64 + >>> s.count() + 2 + Returns: int or Series (if level specified): Number of non-null values in the Series. @@ -2845,6 +2874,47 @@ def map( ``__missing__`` (i.e. provide a method for default values). These are treated the same as ``dict``. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['cat', 'dog', bpd.NA, 'rabbit']) + >>> s + 0 cat + 1 dog + 2 + 3 rabbit + dtype: string + + `map` can accepts a `dict`. Values that are not found in the `dict` are + converted to `NA`: + + >>> s.map({'cat': 'kitten', 'dog': 'puppy'}) + 0 kitten + 1 puppy + 2 + 3 + dtype: string + + It also accepts a remote function: + + >>> @bpd.remote_function([str], str) + ... def my_mapper(val): + ... vowels = ["a", "e", "i", "o", "u"] + ... if val: + ... return "".join([ + ... ch.upper() if ch in vowels else ch for ch in val + ... ]) + ... return "N/A" + + >>> s.map(my_mapper) + 0 cAt + 1 dOg + 2 N/A + 3 rAbbIt + dtype: string + Args: arg (function, Mapping, Series): remote function, collections.abc.Mapping subclass or Series From 64bdf7622f3b5a5b5ec9176b1558a9cd4b7a756a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 2 Jan 2024 11:20:16 -0800 Subject: [PATCH 243/921] chore: stop using deprecated ibis relabel method (#297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/compile/compiled.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 199c8db785c..524699290b9 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -346,7 +346,9 @@ def _to_ibis_expr( table = table.filter(base_table[PREDICATE_COLUMN]) table = table.drop(*columns_to_drop) if col_id_overrides: - table = table.relabel(col_id_overrides) + table = table.rename( + {value: key for key, value in col_id_overrides.items()} + ) if fraction is not None: table = table.filter(ibis.random() < ibis.literal(fraction)) return table @@ -1194,7 +1196,9 @@ def _to_ibis_expr( table = table.filter(base_table[PREDICATE_COLUMN]) table = table.drop(*columns_to_drop) if col_id_overrides: - table = table.relabel(col_id_overrides) + table = table.rename( + {value: key for key, value in col_id_overrides.items()} + ) if fraction is not None: table = table.filter(ibis.random() < ibis.literal(fraction)) return table From c2b1892825545a34ce4ed5b0ef99e99348466108 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 2 Jan 2024 20:28:16 +0000 Subject: [PATCH 244/921] docs: code samples for `sample`, `get`, `Series.round` (#295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BEGIN_COMMIT_OVERRIDE docs: code samples for `sample`, `get`, `Series.round` (#295) docs: code samples for DataFrame `set_index`, `items` (#295) END_COMMIT_OVERRIDE Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - `DataFrame.sample`, `Series.sample`: https://screenshot.googleplex.com/kPy5swVACMeBhSo - `DataFrame.get`, `Series.get`: https://screenshot.googleplex.com/7hirn5oz2b4L6B3 - `DataFrame.set_index`: https://screenshot.googleplex.com/3CXARrp5hwV6gau - `DataFrame.items`: https://screenshot.googleplex.com/bk3HAiXZQq3TYD9 - `Series.round`: https://screenshot.googleplex.com/C9c4m84NWNMnAwS Fixes internal issues 318011542 and 318011745 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 78 ++++++++++++++- .../bigframes_vendored/pandas/core/generic.py | 94 +++++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 19 ++++ 3 files changed, 189 insertions(+), 2 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index f2de8fcb6ab..9259d14bab6 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1187,6 +1187,47 @@ def set_index( Set the DataFrame index (row labels) using one existing column. The index can replace the existing index. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'month': [1, 4, 7, 10], + ... 'year': [2012, 2014, 2013, 2014], + ... 'sale': [55, 40, 84, 31]}) + >>> df + month year sale + 0 1 2012 55 + 1 4 2014 40 + 2 7 2013 84 + 3 10 2014 31 + + [4 rows x 3 columns] + + Set the 'month' column to become the index: + + >>> df.set_index('month') + year sale + month + 1 2012 55 + 4 2014 40 + 7 2013 84 + 10 2014 31 + + [4 rows x 2 columns] + + Create a MultiIndex using columns 'year' and 'month': + + >>> df.set_index(['year', 'month']) + sale + year month + 2012 1 55 + 2014 4 40 + 2013 7 84 + 2014 10 31 + + [4 rows x 1 columns] + Args: keys: A label. This parameter can be a single column key. @@ -1621,6 +1662,39 @@ def items(self): Iterates over the DataFrame columns, returning a tuple with the column name and the content as a Series. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'species': ['bear', 'bear', 'marsupial'], + ... 'population': [1864, 22000, 80000]}, + ... index=['panda', 'polar', 'koala']) + >>> df + species population + panda bear 1864 + polar bear 22000 + koala marsupial 80000 + + [3 rows x 2 columns] + + >>> for label, content in df.items(): + ... print(f'--> label: {label}') + ... print(f'--> content:\\n{content}') + ... + --> label: species + --> content: + panda bear + polar bear + koala marsupial + Name: species, dtype: string + --> label: population + --> content: + panda 1864 + polar 22000 + koala 80000 + Name: population, dtype: Int64 + Returns: Iterator: Iterator of label, Series for each column. """ @@ -4587,7 +4661,7 @@ def index(self): ... 'Location': ['Seattle', 'New York', 'Kona']}, ... index=([10, 20, 30])) >>> df - Name Age Location + Name Age Location 10 Alice 25 Seattle 20 Bob 30 New York 30 Aritra 35 Kona @@ -4603,7 +4677,7 @@ def index(self): >>> df1 = df.set_index(["Name", "Location"]) >>> df1 - Age + Age Name Location Alice Seattle 25 Bob New York 30 diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index c079cbff7f9..bc31e022635 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -254,6 +254,55 @@ def get(self, key, default=None): Returns default value if not found. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame( + ... [ + ... [24.3, 75.7, "high"], + ... [31, 87.8, "high"], + ... [22, 71.6, "medium"], + ... [35, 95, "medium"], + ... ], + ... columns=["temp_celsius", "temp_fahrenheit", "windspeed"], + ... index=["2014-02-12", "2014-02-13", "2014-02-14", "2014-02-15"], + ... ) + >>> df + temp_celsius temp_fahrenheit windspeed + 2014-02-12 24.3 75.7 high + 2014-02-13 31.0 87.8 high + 2014-02-14 22.0 71.6 medium + 2014-02-15 35.0 95.0 medium + + [4 rows x 3 columns] + + >>> df.get(["temp_celsius", "windspeed"]) + temp_celsius windspeed + 2014-02-12 24.3 high + 2014-02-13 31.0 high + 2014-02-14 22.0 medium + 2014-02-15 35.0 medium + + [4 rows x 2 columns] + + >>> ser = df['windspeed'] + >>> ser + 2014-02-12 high + 2014-02-13 high + 2014-02-14 medium + 2014-02-15 medium + Name: windspeed, dtype: string + >>> ser.get('2014-02-13') + 'high' + + If the key is not found, the default value will be used. + + >>> df.get(["temp_celsius", "temp_kelvin"]) + >>> df.get(["temp_celsius", "temp_kelvin"], default="default_value") + 'default_value' + Args: key: object @@ -410,6 +459,51 @@ def sample( You can use `random_state` for reproducibility. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'num_legs': [2, 4, 8, 0], + ... 'num_wings': [2, 0, 0, 0], + ... 'num_specimen_seen': [10, 2, 1, 8]}, + ... index=['falcon', 'dog', 'spider', 'fish']) + >>> df + num_legs num_wings num_specimen_seen + falcon 2 2 10 + dog 4 0 2 + spider 8 0 1 + fish 0 0 8 + + [4 rows x 3 columns] + + Fetch one random row from the DataFrame (Note that we use `random_state` + to ensure reproducibility of the examples): + + >>> df.sample(random_state=1) + num_legs num_wings num_specimen_seen + dog 4 0 2 + + [1 rows x 3 columns] + + A random 50% sample of the DataFrame: + + >>> df.sample(frac=0.5, random_state=1) + num_legs num_wings num_specimen_seen + dog 4 0 2 + fish 0 0 8 + + [2 rows x 3 columns] + + Extract 3 random elements from the Series `df['num_legs']`: + + >>> s = df['num_legs'] + >>> s.sample(n=3, random_state=1) + dog 4 + fish 0 + spider 8 + Name: num_legs, dtype: Int64 + Args: n (Optional[int], default None): Number of items from axis to return. Cannot be used with `frac`. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index b97f9018dda..1ee1a8d5b5b 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -722,6 +722,25 @@ def round(self, decimals: int = 0) -> Series: """ Round each value in a Series to the given number of decimals. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([0.1, 1.3, 2.7]) + >>> s.round() + 0 0.0 + 1 1.0 + 2 3.0 + dtype: Float64 + + >>> s = bpd.Series([0.123, 1.345, 2.789]) + >>> s.round(decimals=2) + 0 0.12 + 1 1.34 + 2 2.79 + dtype: Float64 + Args: decimals (int, default 0): Number of decimal places to round to. If decimals is negative, From eb69f60db52544882fb06c2d5fa0e41226dfe93f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 2 Jan 2024 23:44:39 +0000 Subject: [PATCH 245/921] docs: code samples for `DataFrame.rename`, `Series.rename` (#293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BEGIN_COMMIT_OVERRIDE docs: code samples for `rename` , `size` (#293) docs: code samples for `Series.{name, std, agg}` (#293) END_COMMIT_OVERRIDE Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - `DataFrame.size`: https://screenshot.googleplex.com/55MHXNuAamdfbud - `Series.size`: https://screenshot.googleplex.com/5ve4T8UJq2TUiWb - `DataFrame.rename`: https://screenshot.googleplex.com/7eWsfcz8tmLx4pY - `Series.rename`: https://screenshot.googleplex.com/3HbXTxCaJVsbEzs - `Series.name`: https://screenshot.googleplex.com/7FpNDWJEyiqGLpN - `Series.std`: https://screenshot.googleplex.com/4RSTC8s2tYYK5cW - `Series.agg`: https://screenshot.googleplex.com/63TmACx23TPJu2K Fixes internal issues 317997641 and 317998300 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 24 ++++ .../bigframes_vendored/pandas/core/generic.py | 13 ++ .../bigframes_vendored/pandas/core/series.py | 127 ++++++++++++++++++ 3 files changed, 164 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 9259d14bab6..c3794c550e8 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1146,6 +1146,30 @@ def rename( Dict values must be unique (1-to-1). Labels not contained in a dict will be left as-is. Extra labels listed don't throw an error. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + >>> df + A B + 0 1 4 + 1 2 5 + 2 3 6 + + [3 rows x 2 columns] + + Rename columns using a mapping: + + >>> df.rename(columns={"A": "col1", "B": "col2"}) + col1 col2 + 0 1 4 + 1 2 5 + 2 3 6 + + [3 rows x 2 columns] + Args: columns (Mapping): Dict-like from old column labels to new column labels. diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index bc31e022635..72b947f96ca 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -29,6 +29,19 @@ def ndim(self) -> int: def size(self) -> int: """Return an int representing the number of elements in this object. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series({'a': 1, 'b': 2, 'c': 3}) + >>> s.size + 3 + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df.size + 4 + Returns: int: Return the number of rows if Series. Otherwise return the number of rows times number of columns if DataFrame. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 1ee1a8d5b5b..98c4fcdd447 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -135,6 +135,35 @@ def name(self) -> Hashable: to form a DataFrame. It is also used whenever displaying the Series using the interpreter. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + For a Series: + + >>> s = bpd.Series([1, 2, 3], dtype="Int64", name='Numbers') + >>> s + 0 1 + 1 2 + 2 3 + Name: Numbers, dtype: Int64 + >>> s.name + 'Numbers' + + If the Series is part of a DataFrame: + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df + col1 col2 + 0 1 3 + 1 2 4 + + [2 rows x 2 columns] + >>> s = df["col1"] + >>> s.name + 'col1' + Returns: hashable object: The name of the Series, also the column name if part of a DataFrame. @@ -560,6 +589,27 @@ def agg(self, func): """ Aggregate using one or more operations over the specified axis. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3, 4]) + >>> s + 0 1 + 1 2 + 2 3 + 3 4 + dtype: Int64 + + >>> s.agg('min') + 1 + + >>> s.agg(['min', 'max']) + min 1.0 + max 4.0 + dtype: Float64 + Args: func (function): Function to use for aggregating the data. @@ -2292,6 +2342,29 @@ def std( Normalized by N-1 by default. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'person_id': [0, 1, 2, 3], + ... 'age': [21, 25, 62, 43], + ... 'height': [1.61, 1.87, 1.49, 2.01]} + ... ).set_index('person_id') + >>> df + age height + person_id + 0 21 1.61 + 1 25 1.87 + 2 62 1.49 + 3 43 2.01 + + [4 rows x 2 columns] + + >>> df.std() + age 18.786076 + height 0.237417 + dtype: Float64 Returns ------- @@ -2649,6 +2722,34 @@ def rename(self, index, **kwargs) -> Series | None: Alternatively, change ``Series.name`` with a scalar value. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3]) + >>> s + 0 1 + 1 2 + 2 3 + dtype: Int64 + + You can changes the Series name by specifying a string scalar: + + >>> s.rename("my_name") + 0 1 + 1 2 + 2 3 + Name: my_name, dtype: Int64 + + You can change the labels by specifying a mapping: + + >>> s.rename({1: 3, 2: 5}) + 0 1 + 3 2 + 5 3 + dtype: Int64 + Args: index (scalar, hashable sequence, dict-like or function optional): Functions or dict-like are transformations to apply to @@ -2990,3 +3091,29 @@ def values(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def size(self) -> int: + """Return the number of elements in the underlying data. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + For Series: + + >>> s = bpd.Series({'a': 1, 'b': 2, 'c': 3}) + >>> s.size + 3 + + For Index: + + >>> idx = bpd.Index(bpd.Series([1, 2, 3])) + >>> idx.size + 3 + + Returns: + int: Return the number of elements in the underlying data. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 655178ad71c2b64f720d0d195813a97889c38f5a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:29:36 -0800 Subject: [PATCH 246/921] chore(main): release 0.18.0 (#279) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c49c5b63bd..77a6576ee05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,49 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.18.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.17.0...v0.18.0) (2024-01-02) + + +### Features + +* Add dataframe.to_html ([#259](https://github.com/googleapis/python-bigquery-dataframes/issues/259)) ([2cd6489](https://github.com/googleapis/python-bigquery-dataframes/commit/2cd64891170dcd4f2a709024a2993e36db210976)) +* Add IntervalIndex support to bigframes.pandas.cut ([#254](https://github.com/googleapis/python-bigquery-dataframes/issues/254)) ([6c1969a](https://github.com/googleapis/python-bigquery-dataframes/commit/6c1969a35fe720cf3a804006bcc9046ba554fcc3)) +* Add replace method to DataFrame ([#261](https://github.com/googleapis/python-bigquery-dataframes/issues/261)) ([5092215](https://github.com/googleapis/python-bigquery-dataframes/commit/5092215767d77c90b132e9cd6b3e3749827ebe09)) +* Specific pyarrow mappings for decimal, bytes types ([#283](https://github.com/googleapis/python-bigquery-dataframes/issues/283)) ([a1c0631](https://github.com/googleapis/python-bigquery-dataframes/commit/a1c06319ab0e3697c3175112490488002bb344c0)) + + +### Bug Fixes + +* Dataframes to_gbq now creates dataset if it doesn't exist ([#222](https://github.com/googleapis/python-bigquery-dataframes/issues/222)) ([bac62f7](https://github.com/googleapis/python-bigquery-dataframes/commit/bac62f76af1af6ca8834c3690c7c79aeb12dd331)) +* Exclude pandas 2.2.0rc0 to unblock prerelease tests ([#292](https://github.com/googleapis/python-bigquery-dataframes/issues/292)) ([ac1a745](https://github.com/googleapis/python-bigquery-dataframes/commit/ac1a745ddce9865f4585777b43c2234b9bf2841d)) +* Fix DataFrameGroupby.agg() issue with as_index=False ([#273](https://github.com/googleapis/python-bigquery-dataframes/issues/273)) ([ab49350](https://github.com/googleapis/python-bigquery-dataframes/commit/ab493506e71ed8970a11fe2f88b2145150e09291)) +* Make `Series.str.replace` work for simple strings ([#285](https://github.com/googleapis/python-bigquery-dataframes/issues/285)) ([ad67465](https://github.com/googleapis/python-bigquery-dataframes/commit/ad6746569b3af11be9d40805a1449ee1e89288dc)) +* Update dataframe.to_gbq to dedup column names. ([#286](https://github.com/googleapis/python-bigquery-dataframes/issues/286)) ([746115d](https://github.com/googleapis/python-bigquery-dataframes/commit/746115d5564c95bc3c4a5309c99e7a29e535e6fe)) +* Use setuptools.find_namespace_packages ([#246](https://github.com/googleapis/python-bigquery-dataframes/issues/246)) ([9ec352a](https://github.com/googleapis/python-bigquery-dataframes/commit/9ec352a338f11d82aee9cd665ffb0e6e97cb391b)) + + +### Dependencies + +* Migrate to `ibis-framework >= "7.1.0"` ([#53](https://github.com/googleapis/python-bigquery-dataframes/issues/53)) ([9798a2b](https://github.com/googleapis/python-bigquery-dataframes/commit/9798a2b14dffb20432f732343cac92341e42fe09)) + + +### Documentation + +* Add code snippets for explore query result page ([#278](https://github.com/googleapis/python-bigquery-dataframes/issues/278)) ([7cbbb7d](https://github.com/googleapis/python-bigquery-dataframes/commit/7cbbb7d4608d8b7d1a360b2fe2d39d89a52f9546)) +* Code samples for `astype` common to DataFrame and Series ([#280](https://github.com/googleapis/python-bigquery-dataframes/issues/280)) ([95b673a](https://github.com/googleapis/python-bigquery-dataframes/commit/95b673aeb1545744e4b1a353cf1f4d0202d8a1b2)) +* Code samples for `DataFrame.copy` and `Series.copy` ([#290](https://github.com/googleapis/python-bigquery-dataframes/issues/290)) ([7cbc2b0](https://github.com/googleapis/python-bigquery-dataframes/commit/7cbc2b0ba572d11778ba7caf7c95b7fb8f3a31a7)) +* Code samples for `drop` and `fillna` ([#284](https://github.com/googleapis/python-bigquery-dataframes/issues/284)) ([9c5012e](https://github.com/googleapis/python-bigquery-dataframes/commit/9c5012ec68275db83d1f6f7e743f5edaaaacd8cb)) +* Code samples for `isna`, `isnull`, `dropna`, `isin` ([#289](https://github.com/googleapis/python-bigquery-dataframes/issues/289)) ([ad51035](https://github.com/googleapis/python-bigquery-dataframes/commit/ad51035bcf80d6a49f134df26624b578010b5b12)) +* Code samples for `rename` , `size` ([#293](https://github.com/googleapis/python-bigquery-dataframes/issues/293)) ([eb69f60](https://github.com/googleapis/python-bigquery-dataframes/commit/eb69f60db52544882fb06c2d5fa0e41226dfe93f)) +* Code samples for `reset_index` and `sort_values` ([#282](https://github.com/googleapis/python-bigquery-dataframes/issues/282)) ([acc0eb7](https://github.com/googleapis/python-bigquery-dataframes/commit/acc0eb7010951c8cfb91aecc45268b041217dd09)) +* Code samples for `sample`, `get`, `Series.round` ([#295](https://github.com/googleapis/python-bigquery-dataframes/issues/295)) ([c2b1892](https://github.com/googleapis/python-bigquery-dataframes/commit/c2b1892825545a34ce4ed5b0ef99e99348466108)) +* Code samples for `Series.{add, replace, unique, T, transpose}` ([#287](https://github.com/googleapis/python-bigquery-dataframes/issues/287)) ([0e1bbfc](https://github.com/googleapis/python-bigquery-dataframes/commit/0e1bbfc1055aff9757b5138907c11caab2f3965a)) +* Code samples for `Series.{map, to_list, count}` ([#290](https://github.com/googleapis/python-bigquery-dataframes/issues/290)) ([7cbc2b0](https://github.com/googleapis/python-bigquery-dataframes/commit/7cbc2b0ba572d11778ba7caf7c95b7fb8f3a31a7)) +* Code samples for `Series.{name, std, agg}` ([#293](https://github.com/googleapis/python-bigquery-dataframes/issues/293)) ([eb69f60](https://github.com/googleapis/python-bigquery-dataframes/commit/eb69f60db52544882fb06c2d5fa0e41226dfe93f)) +* Code samples for `Series.groupby` and `Series.{sum,mean,min,max}` ([#280](https://github.com/googleapis/python-bigquery-dataframes/issues/280)) ([95b673a](https://github.com/googleapis/python-bigquery-dataframes/commit/95b673aeb1545744e4b1a353cf1f4d0202d8a1b2)) +* Code samples for DataFrame `set_index`, `items` ([#295](https://github.com/googleapis/python-bigquery-dataframes/issues/295)) ([c2b1892](https://github.com/googleapis/python-bigquery-dataframes/commit/c2b1892825545a34ce4ed5b0ef99e99348466108)) +* Fix the rendering for `get_dummies` ([#291](https://github.com/googleapis/python-bigquery-dataframes/issues/291)) ([252f3a2](https://github.com/googleapis/python-bigquery-dataframes/commit/252f3a2a0e1296c7d786acdc0bdebe9e4a9ae1be)) + ## [0.17.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.16.0...v0.17.0) (2023-12-14) diff --git a/bigframes/version.py b/bigframes/version.py index 04eac385f67..494335acd71 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.17.0" +__version__ = "0.18.0" From e3a056a301e99c4c3d2a2ecdcbcaf8804be8089f Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 3 Jan 2024 13:33:09 -0800 Subject: [PATCH 247/921] feat: support upcasting numeric columns in concat (#294) --- bigframes/core/blocks.py | 83 ++++++++++++++++++++-------- bigframes/dtypes.py | 21 ++++++- bigframes/operations/aggregations.py | 5 ++ tests/system/small/test_dataframe.py | 4 +- tests/system/small/test_pandas.py | 32 +++++++++++ 5 files changed, 118 insertions(+), 27 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 779d11b3712..e88326795c2 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1506,8 +1506,10 @@ def concat( blocks: typing.List[Block] = [self, *other] if ignore_index: blocks = [block.reset_index() for block in blocks] - - result_labels = _align_indices(blocks) + level_names = None + else: + level_names, level_types = _align_indices(blocks) + blocks = [_cast_index(block, level_types) for block in blocks] index_nlevels = blocks[0].index.nlevels @@ -1522,7 +1524,7 @@ def concat( result_expr, index_columns=list(result_expr.column_ids)[:index_nlevels], column_labels=aligned_blocks[0].column_labels, - index_labels=result_labels, + index_labels=level_names, ) if ignore_index: result_block = result_block.reset_index() @@ -1783,16 +1785,40 @@ def block_from_local(data) -> Block: ) +def _cast_index(block: Block, dtypes: typing.Sequence[bigframes.dtypes.Dtype]): + original_block = block + result_ids = [] + for idx_id, idx_dtype, target_dtype in zip( + block.index_columns, block.index_dtypes, dtypes + ): + if idx_dtype != target_dtype: + block, result_id = block.apply_unary_op(idx_id, ops.AsTypeOp(target_dtype)) + result_ids.append(result_id) + else: + result_ids.append(idx_id) + + expr = block.expr.select_columns((*result_ids, *original_block.value_columns)) + return Block( + expr, + index_columns=result_ids, + column_labels=original_block.column_labels, + index_labels=original_block.index_labels, + ) + + def _align_block_to_schema( block: Block, schema: dict[Label, bigframes.dtypes.Dtype] ) -> Block: - """For a given schema, remap block to schema by reordering columns and inserting nulls.""" + """For a given schema, remap block to schema by reordering columns, and inserting nulls.""" col_ids: typing.Tuple[str, ...] = () for label, dtype in schema.items(): - # TODO: Support casting to lcd type - requires mixed type support matching_ids: typing.Sequence[str] = block.label_to_col_id.get(label, ()) if len(matching_ids) > 0: col_id = matching_ids[-1] + col_dtype = block.expr.get_column_type(col_id) + if dtype != col_dtype: + # If _align_schema worked properly, this should always be an upcast + block, col_id = block.apply_unary_op(col_id, ops.AsTypeOp(dtype)) col_ids = (*col_ids, col_id) else: block, null_column = block.create_constant(None, dtype=dtype) @@ -1810,24 +1836,28 @@ def _align_schema( return functools.reduce(reduction, schemas) -def _align_indices(blocks: typing.Sequence[Block]) -> typing.Sequence[Label]: - """Validates that the blocks have compatible indices and returns the resulting label names.""" +def _align_indices( + blocks: typing.Sequence[Block], +) -> typing.Tuple[typing.Sequence[Label], typing.Sequence[bigframes.dtypes.Dtype]]: + """Validates that the blocks have compatible indices and returns the resulting label names and dtypes.""" names = blocks[0].index.names types = blocks[0].index.dtypes + for block in blocks[1:]: if len(names) != block.index.nlevels: raise NotImplementedError( f"Cannot combine indices with different number of levels. Use 'ignore_index'=True. {constants.FEEDBACK_LINK}" ) - if block.index.dtypes != types: - raise NotImplementedError( - f"Cannot combine different index dtypes. Use 'ignore_index'=True. {constants.FEEDBACK_LINK}" - ) names = [ lname if lname == rname else None for lname, rname in zip(names, block.index.names) ] - return names + types = [ + bigframes.dtypes.lcd_type_or_throw(ltype, rtype) + for ltype, rtype in zip(types, block.index.dtypes) + ] + types = typing.cast(typing.Sequence[bigframes.dtypes.Dtype], types) + return names, types def _combine_schema_inner( @@ -1835,13 +1865,15 @@ def _combine_schema_inner( right: typing.Dict[Label, bigframes.dtypes.Dtype], ) -> typing.Dict[Label, bigframes.dtypes.Dtype]: result = dict() - for label, type in left.items(): + for label, left_type in left.items(): if label in right: - if type != right[label]: + right_type = right[label] + output_type = bigframes.dtypes.lcd_type(left_type, right_type) + if output_type is None: raise ValueError( f"Cannot concat rows with label {label} due to mismatched types. {constants.FEEDBACK_LINK}" ) - result[label] = type + result[label] = output_type return result @@ -1850,15 +1882,20 @@ def _combine_schema_outer( right: typing.Dict[Label, bigframes.dtypes.Dtype], ) -> typing.Dict[Label, bigframes.dtypes.Dtype]: result = dict() - for label, type in left.items(): - if (label in right) and (type != right[label]): - raise ValueError( - f"Cannot concat rows with label {label} due to mismatched types. {constants.FEEDBACK_LINK}" - ) - result[label] = type - for label, type in right.items(): + for label, left_type in left.items(): + if label not in right: + result[label] = left_type + else: + right_type = right[label] + output_type = bigframes.dtypes.lcd_type(left_type, right_type) + if output_type is None: + raise NotImplementedError( + f"Cannot concat rows with label {label} due to mismatched types. {constants.FEEDBACK_LINK}" + ) + result[label] = output_type + for label, right_type in right.items(): if label not in left: - result[label] = type + result[label] = right_type return result diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index b754acea2ea..608885dec42 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -40,6 +40,7 @@ pd.Int64Dtype, pd.StringDtype, pd.ArrowDtype, + gpd.array.GeometryDtype, ] # On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable @@ -139,7 +140,7 @@ ARROW_TO_IBIS = {arrow: ibis for ibis, arrow in IBIS_TO_ARROW.items()} -IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, Union[Dtype, np.dtype[Any]]] = { +IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, Dtype] = { ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS } # Allow REQUIRED fields to map correctly. @@ -179,7 +180,7 @@ def ibis_dtype_to_bigframes_dtype( ibis_dtype: ibis_dtypes.DataType, -) -> Union[Dtype, np.dtype[Any]]: +) -> Dtype: """Converts an Ibis dtype to a BigQuery DataFrames dtype Args: @@ -340,6 +341,11 @@ def literal_to_ibis_scalar( ValueError: if passed literal cannot be coerced to a BigQuery DataFrames compatible scalar """ + # Special case: Can create nulls for non-bidirectional types + if (force_dtype == gpd.array.GeometryDtype()) and pd.isna(literal): + # Ibis has bug for casting nulltype to geospatial, so we perform intermediate cast first + geotype = ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True) + return ibis.literal(None, geotype) ibis_dtype = BIGFRAMES_TO_IBIS[force_dtype] if force_dtype else None if pd.api.types.is_list_like(literal): @@ -538,6 +544,8 @@ def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: def lcd_type(dtype1: Dtype, dtype2: Dtype) -> typing.Optional[Dtype]: + if dtype1 == dtype2: + return dtype1 # Implicit conversion currently only supported for numeric types hierarchy: list[Dtype] = [ pd.BooleanDtype(), @@ -550,3 +558,12 @@ def lcd_type(dtype1: Dtype, dtype2: Dtype) -> typing.Optional[Dtype]: return None lcd_index = max(hierarchy.index(dtype1), hierarchy.index(dtype2)) return hierarchy[lcd_index] + + +def lcd_type_or_throw(dtype1: Dtype, dtype2: Dtype) -> Dtype: + result = lcd_type(dtype1, dtype2) + if result is None: + raise NotImplementedError( + f"BigFrames cannot upcast {dtype1} and {dtype2} to common type. {constants.FEEDBACK_LINK}" + ) + return result diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 8178ebfaea5..452abf047c6 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -396,6 +396,11 @@ def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: ) +class LastOp(WindowOp): + def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.last(), window) + + class LastNonNullOp(WindowOp): @property def skips_nulls(self): diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index cb2e4f94fad..fa3d5148a82 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3141,9 +3141,9 @@ def test_df___array__(scalars_df_index, scalars_pandas_df_index): def test_getattr_attribute_error_when_pandas_has(scalars_df_index): - # asof is implemented in pandas but not in bigframes + # swapaxes is implemented in pandas but not in bigframes with pytest.raises(AttributeError): - scalars_df_index.asof() + scalars_df_index.swapaxes() def test_getattr_attribute_error(scalars_df_index): diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index 282c0d68ebb..a79ddb64cd3 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -185,6 +185,38 @@ def test_concat_dataframe_mismatched_columns(scalars_dfs, how): pd.testing.assert_frame_equal(bf_result, pd_result) +def test_concat_dataframe_upcasting(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_input1 = scalars_df[["int64_col", "float64_col", "int64_too"]].set_index( + "int64_col", drop=True + ) + bf_input1.columns = ["a", "b"] + bf_input2 = scalars_df[["int64_too", "int64_col", "float64_col"]].set_index( + "float64_col", drop=True + ) + bf_input2.columns = ["a", "b"] + bf_result = bpd.concat([bf_input1, bf_input2], join="outer") + bf_result = bf_result.to_pandas() + + bf_input1 = ( + scalars_pandas_df[["int64_col", "float64_col", "int64_too"]] + .set_index("int64_col", drop=True) + .set_axis(["a", "b"], axis=1) + ) + bf_input2 = ( + scalars_pandas_df[["int64_too", "int64_col", "float64_col"]] + .set_index("float64_col", drop=True) + .set_axis(["a", "b"], axis=1) + ) + pd_result = pd.concat( + [bf_input1, bf_input2], + join="outer", + ) + + pd.testing.assert_frame_equal(bf_result, pd_result) + + @pytest.mark.parametrize( ("how",), [ From a61c5fef1e3b88f38269ee5bfd50886b8d2908ae Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 4 Jan 2024 13:14:15 -0800 Subject: [PATCH 248/921] fix: Fix bug converting non-string labels to sql ids (#296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 13 +++++++---- bigframes/core/nodes.py | 1 - bigframes/core/utils.py | 33 ++++++++++++++++++++------- tests/system/small/test_multiindex.py | 32 +++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index e8ac8c1d0f9..13b05620927 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -27,6 +27,7 @@ import bigframes.core.nodes as nodes from bigframes.core.ordering import OrderingColumnReference import bigframes.core.ordering as orderings +import bigframes.core.utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes import bigframes.operations as ops @@ -69,10 +70,14 @@ def from_ibis( @classmethod def from_pandas(cls, pd_df: pandas.DataFrame): iobytes = io.BytesIO() - # Discard row labels and use simple string ids for columns - column_ids = tuple(str(label) for label in pd_df.columns) - pd_df.reset_index(drop=True).set_axis(column_ids, axis=1).to_feather(iobytes) - node = nodes.ReadLocalNode(iobytes.getvalue(), column_ids=column_ids) + # Use alphanumeric identifiers, to avoid downstream problems with escaping. + as_ids = [ + bigframes.core.utils.label_to_identifier(label, strict=True) + for label in pd_df.columns + ] + unique_ids = tuple(bigframes.core.utils.disambiguate_ids(as_ids)) + pd_df.reset_index(drop=True).set_axis(unique_ids, axis=1).to_feather(iobytes) + node = nodes.ReadLocalNode(iobytes.getvalue()) return cls(node) @property diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 30444f55657..62cd7373d07 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -122,7 +122,6 @@ def __hash__(self): @dataclass(frozen=True) class ReadLocalNode(BigFrameNode): feather_bytes: bytes - column_ids: typing.Tuple[str, ...] def __hash__(self): return self._node_hash diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index dc7c7090117..4331999dd6b 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -11,6 +11,7 @@ # 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 Hashable, Iterable, List @@ -84,26 +85,42 @@ def get_standardized_ids( Tuple of (standardized_column_ids, standardized_index_ids) """ col_ids = [ - UNNAMED_COLUMN_ID if col_label is None else str(col_label) + UNNAMED_COLUMN_ID if col_label is None else label_to_identifier(col_label) for col_label in col_labels ] idx_ids = [ - UNNAMED_INDEX_ID if idx_label is None else str(idx_label) + UNNAMED_INDEX_ID if idx_label is None else label_to_identifier(idx_label) for idx_label in idx_labels ] - ids = idx_ids + col_ids + ids = disambiguate_ids(idx_ids + col_ids) + + idx_ids, col_ids = ids[: len(idx_ids)], ids[len(idx_ids) :] + + return col_ids, idx_ids + + +def label_to_identifier(label: typing.Hashable, strict: bool = False) -> str: + """ + Convert pandas label to make legal bigquery identifier. May create collisions (should deduplicate after). + Strict mode might not be necessary, but ibis seems to escape non-alphanumeric characters inconsistently. + """ # Column values will be loaded as null if the column name has spaces. # https://github.com/googleapis/python-bigquery/issues/1566 - ids = [id.replace(" ", "_") for id in ids] + identifier = str(label).replace(" ", "_") + if strict: + identifier = re.sub(r"[^a-zA-Z0-9_]", "", identifier) + if not identifier: + identifier = "id" + return identifier + - ids = typing.cast( +def disambiguate_ids(ids: typing.Sequence[str]) -> typing.List[str]: + """Disambiguate list of ids by adding suffixes where needed. If inputs are legal sql ids, outputs should be as well.""" + return typing.cast( List[str], vendored_pandas_io_common.dedup_names(ids, is_potential_multiindex=False), ) - idx_ids, col_ids = ids[: len(idx_ids)], ids[len(idx_ids) :] - - return col_ids, idx_ids def merge_column_labels( diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 2d4e1f02043..058adb93908 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -16,7 +16,37 @@ import pytest import bigframes.pandas as bpd -from tests.system.utils import assert_pandas_df_equal +from tests.system.utils import assert_pandas_df_equal, skip_legacy_pandas + + +@skip_legacy_pandas +def test_read_pandas_multi_index_axes(): + index = pandas.MultiIndex.from_arrays( + [ + pandas.Index([4, 99], dtype=pandas.Int64Dtype()), + pandas.Index( + [" Hello, World!", "_some_new_string"], + dtype=pandas.StringDtype(storage="pyarrow"), + ), + ], + names=[" 1index 1", "_1index 2"], + ) + columns = pandas.MultiIndex.from_arrays( + [ + pandas.Index([6, 87], dtype=pandas.Int64Dtype()), + pandas.Index( + [" Bonjour le monde!", "_une_chaîne_de_caractères"], + dtype=pandas.StringDtype(storage="pyarrow"), + ), + ], + names=[" 1columns 1", "_1new_index 2"], + ) + pandas_df = pandas.DataFrame( + [[1, 2], [3, 4]], index=index, columns=columns, dtype=pandas.Int64Dtype() + ) + bf_df = bpd.DataFrame(pandas_df) + + pandas.testing.assert_frame_equal(bf_df.to_pandas(), pandas_df) # Row Multi-index tests From a01b271e76d05459f531cd83c6e93a2d13bfa061 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:41:18 -0800 Subject: [PATCH 249/921] feat: add 'columns' as an alias for 'col_order' (#298) --- bigframes/pandas/__init__.py | 18 +++-- bigframes/session/__init__.py | 66 +++++++++++++------ ...q_dataframes_ml_drug_name_generation.ipynb | 4 +- tests/system/small/test_session.py | 10 +-- .../bigframes_vendored/pandas/io/gbq.py | 11 ++-- 5 files changed, 71 insertions(+), 38 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 7386c4a2e73..76b30e6680c 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -486,20 +486,22 @@ def read_gbq( query_or_table: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, filters: vendored_pandas_gbq.FiltersType = (), use_cache: bool = True, + col_order: Iterable[str] = (), ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query_or_table) return global_session.with_default_session( bigframes.session.Session.read_gbq, query_or_table, index_col=index_col, - col_order=col_order, + columns=columns, max_results=max_results, filters=filters, use_cache=use_cache, + col_order=col_order, ) @@ -520,18 +522,20 @@ def read_gbq_query( query: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, use_cache: bool = True, + col_order: Iterable[str] = (), ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) return global_session.with_default_session( bigframes.session.Session.read_gbq_query, query, index_col=index_col, - col_order=col_order, + columns=columns, max_results=max_results, use_cache=use_cache, + col_order=col_order, ) @@ -542,18 +546,20 @@ def read_gbq_table( query: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, use_cache: bool = True, + col_order: Iterable[str] = (), ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) return global_session.with_default_session( bigframes.session.Session.read_gbq_table, query, index_col=index_col, - col_order=col_order, + columns=columns, max_results=max_results, use_cache=use_cache, + col_order=col_order, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index fbe900106ab..15c262afa70 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -232,20 +232,28 @@ def read_gbq( query_or_table: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, filters: third_party_pandas_gbq.FiltersType = (), use_cache: bool = True, + col_order: Iterable[str] = (), # Add a verify index argument that fails if the index is not unique. ) -> dataframe.DataFrame: # TODO(b/281571214): Generate prompt to show the progress of read_gbq. - query_or_table = self._filters_to_query(query_or_table, col_order, filters) + if columns and col_order: + raise ValueError( + "Must specify either columns (preferred) or col_order, not both" + ) + elif col_order: + columns = col_order + + query_or_table = self._filters_to_query(query_or_table, columns, filters) if _is_query(query_or_table): return self._read_gbq_query( query_or_table, index_col=index_col, - col_order=col_order, + columns=columns, max_results=max_results, api_name="read_gbq", use_cache=use_cache, @@ -257,7 +265,7 @@ def read_gbq( return self._read_gbq_table( query_or_table, index_col=index_col, - col_order=col_order, + columns=columns, max_results=max_results, api_name="read_gbq", use_cache=use_cache, @@ -388,9 +396,10 @@ def read_gbq_query( query: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, use_cache: bool = True, + col_order: Iterable[str] = (), ) -> dataframe.DataFrame: """Turn a SQL query into a DataFrame. @@ -442,10 +451,17 @@ def read_gbq_query( """ # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so # these docstrings are inline. + if columns and col_order: + raise ValueError( + "Must specify either columns (preferred) or col_order, not both" + ) + elif col_order: + columns = col_order + return self._read_gbq_query( query=query, index_col=index_col, - col_order=col_order, + columns=columns, max_results=max_results, api_name="read_gbq_query", use_cache=use_cache, @@ -456,7 +472,7 @@ def _read_gbq_query( query: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, api_name: str = "read_gbq_query", use_cache: bool = True, @@ -492,7 +508,7 @@ def _read_gbq_query( return self.read_gbq_table( f"{destination.project}.{destination.dataset_id}.{destination.table_id}", index_col=index_cols, - col_order=col_order, + columns=columns, max_results=max_results, use_cache=use_cache, ) @@ -502,9 +518,10 @@ def read_gbq_table( query: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, use_cache: bool = True, + col_order: Iterable[str] = (), ) -> dataframe.DataFrame: """Turn a BigQuery table into a DataFrame. @@ -521,10 +538,17 @@ def read_gbq_table( """ # NOTE: This method doesn't (yet) exist in pandas or pandas-gbq, so # these docstrings are inline. + if columns and col_order: + raise ValueError( + "Must specify either columns (preferred) or col_order, not both" + ) + elif col_order: + columns = col_order + return self._read_gbq_table( query=query, index_col=index_col, - col_order=col_order, + columns=columns, max_results=max_results, api_name="read_gbq_table", use_cache=use_cache, @@ -583,7 +607,7 @@ def _read_gbq_table( query: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, api_name: str, use_cache: bool = True, @@ -602,10 +626,10 @@ def _read_gbq_table( table_ref, api_name=api_name, use_cache=use_cache ) - for key in col_order: + for key in columns: if key not in table_expression.columns: raise ValueError( - f"Column '{key}' of `col_order` not found in this table." + f"Column '{key}' of `columns` not found in this table." ) if isinstance(index_col, str): @@ -619,8 +643,8 @@ def _read_gbq_table( f"Column `{key}` of `index_col` not found in this table." ) - if col_order: - table_expression = table_expression.select([*index_cols, *col_order]) + if columns: + table_expression = table_expression.select([*index_cols, *columns]) # If the index is unique and sortable, then we don't need to generate # an ordering column. @@ -719,7 +743,7 @@ def _read_bigquery_load_job( *, job_config: bigquery.LoadJobConfig, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), ) -> dataframe.DataFrame: if isinstance(index_col, str): index_cols = [index_col] @@ -760,7 +784,7 @@ def _read_bigquery_load_job( return self.read_gbq_table( table_id, index_col=index_col, - col_order=col_order, + columns=columns, ) def read_gbq_model(self, model_name: str): @@ -959,13 +983,13 @@ def read_csv( if index_col is None: index_col = () - # usecols should only be an iterable of strings (column names) for use as col_order in read_gbq. - col_order: Tuple[Any, ...] = tuple() + # usecols should only be an iterable of strings (column names) for use as columns in read_gbq. + columns: Tuple[Any, ...] = tuple() if usecols is not None: if isinstance(usecols, Iterable) and all( isinstance(col, str) for col in usecols ): - col_order = tuple(col for col in usecols) + columns = tuple(col for col in usecols) else: raise NotImplementedError( "BigQuery engine only supports an iterable of strings for `usecols`. " @@ -1000,7 +1024,7 @@ def read_csv( table, job_config=job_config, index_col=index_col, - col_order=col_order, + columns=columns, ) else: if any(arg in kwargs for arg in ("chunksize", "iterator")): diff --git a/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb b/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb index 56d7bd13558..52a1c4e7683 100644 --- a/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb @@ -613,7 +613,7 @@ "source": [ "# Query 3 columns of interest from drug label dataset\n", "df = bpd.read_gbq(\"bigquery-public-data.fda_drug.drug_label\",\n", - " col_order=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"])\n", + " columns=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"])\n", "\n", "# Exclude any rows with missing data\n", "df = df.dropna()\n", @@ -825,7 +825,7 @@ "source": [ "# Query 3 columns of interest from drug label dataset\n", "df_missing = bpd.read_gbq(\"bigquery-public-data.fda_drug.drug_label\",\n", - " col_order=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"])\n", + " columns=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"])\n", "\n", "# Exclude any rows with missing data\n", "df_missing = df_missing.dropna()\n", diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 8ce442376a1..2d9c332de1d 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -52,7 +52,7 @@ def test_read_gbq_tokyo( @pytest.mark.parametrize( - ("query_or_table", "col_order"), + ("query_or_table", "columns"), [ pytest.param( "{scalars_table_id}", ["bool_col", "int64_col"], id="two_cols_in_table" @@ -79,16 +79,16 @@ def test_read_gbq_tokyo( ), ], ) -def test_read_gbq_w_col_order( +def test_read_gbq_w_columns( session: bigframes.Session, scalars_table_id: str, query_or_table: str, - col_order: List[str], + columns: List[str], ): df = session.read_gbq( - query_or_table.format(scalars_table_id=scalars_table_id), col_order=col_order + query_or_table.format(scalars_table_id=scalars_table_id), columns=columns ) - assert df.columns.tolist() == col_order + assert df.columns.tolist() == columns @pytest.mark.parametrize( diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index dc8bcc1f774..8e2c9f092da 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -17,10 +17,11 @@ def read_gbq( query_or_table: str, *, index_col: Iterable[str] | str = (), - col_order: Iterable[str] = (), + columns: Iterable[str] = (), max_results: Optional[int] = None, filters: FiltersType = (), use_cache: bool = True, + col_order: Iterable[str] = (), ): """Loads a DataFrame from BigQuery. @@ -77,11 +78,11 @@ def read_gbq( Reading data with `columns` and `filters` parameters: - >>> col_order = ['pitcherFirstName', 'pitcherLastName', 'year', 'pitchSpeed'] + >>> columns = ['pitcherFirstName', 'pitcherLastName', 'year', 'pitchSpeed'] >>> filters = [('year', '==', 2016), ('pitcherFirstName', 'in', ['John', 'Doe']), ('pitcherLastName', 'in', ['Gant'])] >>> df = bpd.read_gbq( ... "bigquery-public-data.baseball.games_wide", - ... col_order=col_order, + ... columns=columns, ... filters=filters, ... ) >>> df.head(1) @@ -97,7 +98,7 @@ def read_gbq( `project.dataset.tablename` or `dataset.tablename`. index_col (Iterable[str] or str): Name of result column(s) to use for index in results DataFrame. - col_order (Iterable[str]): + columns (Iterable[str]): List of BigQuery column names in the desired order for results DataFrame. max_results (Optional[int], default None): @@ -113,6 +114,8 @@ def read_gbq( is to be conducted. use_cache (bool, default True): Whether to cache the query inputs. Default to True. + col_order (Iterable[str]): + Alias for columns, retained for backwards compatibility. Returns: bigframes.dataframe.DataFrame: A DataFrame representing results of the query or table. From 9c213239a73b5cd0ca7b647a86238263d3947431 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:59:06 -0800 Subject: [PATCH 250/921] feat: Allow manually set clustering_columns in dataframe.to_gbq (#302) * feat: Allow manually set clustering_columns in dataframe.to_gbq * Update if_exists check. * Update test. --- bigframes/dataframe.py | 79 ++++++++++++++++--- bigframes/session/_io/bigquery.py | 11 +++ tests/system/small/test_dataframe_io.py | 49 ++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 23 +++++- 4 files changed, 150 insertions(+), 12 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 595670b0b6e..9c90ad5b5fc 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2499,6 +2499,7 @@ def to_gbq( if_exists: Optional[Literal["fail", "replace", "append"]] = None, index: bool = True, ordering_id: Optional[str] = None, + clustering_columns: Union[pandas.Index, Iterable[typing.Hashable]] = (), ) -> str: dispositions = { "fail": bigquery.WriteDisposition.WRITE_EMPTY, @@ -2506,18 +2507,9 @@ def to_gbq( "append": bigquery.WriteDisposition.WRITE_APPEND, } - if destination_table is None: - # TODO(swast): If there have been no modifications to the DataFrame - # since the last time it was written (cached), then return that. - # For `read_gbq` nodes, return the underlying table clone. - destination_table = bigframes.session._io.bigquery.create_temp_table( - self._session.bqclient, - self._session._anonymous_dataset, - # TODO(swast): allow custom expiration times, probably via session configuration. - datetime.datetime.now(datetime.timezone.utc) - + constants.DEFAULT_EXPIRATION, - ) + temp_table_ref = None + if destination_table is None: if if_exists is not None and if_exists != "replace": raise ValueError( f"Got invalid value {repr(if_exists)} for if_exists. " @@ -2526,6 +2518,11 @@ def to_gbq( ) if_exists = "replace" + temp_table_ref = bigframes.session._io.bigquery.random_table( + self._session._anonymous_dataset + ) + destination_table = f"{temp_table_ref.project}.{temp_table_ref.dataset_id}.{temp_table_ref.table_id}" + table_parts = destination_table.split(".") default_project = self._block.expr.session.bqclient.project @@ -2553,15 +2550,29 @@ def to_gbq( except google.api_core.exceptions.NotFound: self._session.bqclient.create_dataset(destination_dataset, exists_ok=True) + clustering_fields = self._map_clustering_columns( + clustering_columns, index=index + ) + job_config = bigquery.QueryJobConfig( write_disposition=dispositions[if_exists], destination=bigquery.table.TableReference.from_string( destination_table, default_project=default_project, ), + clustering_fields=clustering_fields if clustering_fields else None, ) self._run_io_query(index=index, ordering_id=ordering_id, job_config=job_config) + + if temp_table_ref: + bigframes.session._io.bigquery.set_table_expiration( + self._session.bqclient, + temp_table_ref, + datetime.datetime.now(datetime.timezone.utc) + + constants.DEFAULT_EXPIRATION, + ) + return destination_table def to_numpy( @@ -2756,6 +2767,52 @@ def _apply_unary_op(self, operation: ops.UnaryOp) -> DataFrame: block = self._block.multi_apply_unary_op(self._block.value_columns, operation) return DataFrame(block) + def _map_clustering_columns( + self, + clustering_columns: Union[pandas.Index, Iterable[typing.Hashable]], + index: bool, + ) -> List[str]: + """Maps the provided clustering columns to the existing columns in the DataFrame.""" + + def map_columns_on_occurrence(columns): + mapped_columns = [] + for col in clustering_columns: + if col in columns: + count = columns.count(col) + mapped_columns.extend([col] * count) + return mapped_columns + + if not clustering_columns: + return [] + + if len(list(clustering_columns)) != len(set(clustering_columns)): + raise ValueError("Duplicates are not supported in clustering_columns") + + all_possible_columns = ( + (set(self.columns) | set(self.index.names)) if index else set(self.columns) + ) + missing_columns = set(clustering_columns) - all_possible_columns + if missing_columns: + raise ValueError( + f"Clustering columns not found in DataFrame: {missing_columns}" + ) + + clustering_columns_for_df = map_columns_on_occurrence( + list(self._block.column_labels) + ) + clustering_columns_for_index = ( + map_columns_on_occurrence(list(self.index.names)) if index else [] + ) + + ( + clustering_columns_for_df, + clustering_columns_for_index, + ) = utils.get_standardized_ids( + clustering_columns_for_df, clustering_columns_for_index + ) + + return clustering_columns_for_index + clustering_columns_for_df + def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: """Create query text representing this dataframe for I/O.""" array_value = self._block.expr diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index 4770f120894..3695fc98e85 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -150,6 +150,17 @@ def create_temp_table( return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" +def set_table_expiration( + bqclient: bigquery.Client, + table_ref: bigquery.TableReference, + expiration: datetime.datetime, +) -> None: + """Set an expiration time for an existing BigQuery table.""" + table = bqclient.get_table(table_ref) + table.expires = expiration + bqclient.update_table(table, ["expires"]) + + # BigQuery REST API returns types in Legacy SQL format # https://cloud.google.com/bigquery/docs/data-types but we use Standard SQL # names diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 6f1b31b48ea..05c9d223728 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -317,6 +317,55 @@ def test_to_gbq_w_None_column_names( ) +@pytest.mark.parametrize( + "clustering_columns", + [ + pytest.param(["int64_col", "geography_col"]), + pytest.param( + ["float64_col"], + marks=pytest.mark.xfail(raises=google.api_core.exceptions.BadRequest), + ), + pytest.param( + ["int64_col", "int64_col"], + marks=pytest.mark.xfail(raises=ValueError), + ), + ], +) +def test_to_gbq_w_clustering( + scalars_df_default_index, + dataset_id, + bigquery_client, + clustering_columns, +): + """Test the `to_gbq` API for creating clustered tables.""" + destination_table = ( + f"{dataset_id}.test_to_gbq_clustering_{'_'.join(clustering_columns)}" + ) + + scalars_df_default_index.to_gbq( + destination_table, clustering_columns=clustering_columns + ) + table = bigquery_client.get_table(destination_table) + + assert list(table.clustering_fields) == clustering_columns + assert table.expires is None + + +def test_to_gbq_w_clustering_no_destination( + scalars_df_default_index, + bigquery_client, +): + """Test the `to_gbq` API for creating clustered tables without destination.""" + clustering_columns = ["int64_col", "geography_col"] + destination_table = scalars_df_default_index.to_gbq( + clustering_columns=clustering_columns + ) + table = bigquery_client.get_table(destination_table) + + assert list(table.clustering_fields) == clustering_columns + assert table.expires is not None + + def test_to_gbq_w_invalid_destination_table(scalars_df_index): with pytest.raises(ValueError): scalars_df_index.to_gbq("table_id") diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index c3794c550e8..66ed2088e46 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -11,9 +11,10 @@ """ from __future__ import annotations -from typing import Literal, Mapping, Optional, Sequence, Union +from typing import Hashable, Iterable, Literal, Mapping, Optional, Sequence, Union import numpy as np +import pandas as pd from bigframes import constants from third_party.bigframes_vendored.pandas.core.generic import NDFrame @@ -307,6 +308,7 @@ def to_gbq( if_exists: Optional[Literal["fail", "replace", "append"]] = None, index: bool = True, ordering_id: Optional[str] = None, + clustering_columns: Union[pd.Index, Iterable[Hashable]] = (), ) -> str: """Write a DataFrame to a BigQuery table. @@ -336,6 +338,16 @@ def to_gbq( [2 rows x 2 columns] + Write a DataFrame to a BigQuery table with clustering columns: + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4], 'col3': [5, 6]}) + >>> clustering_cols = ['col1', 'col3'] + >>> df.to_gbq( + ... "bigframes-dev.birds.test-clusters", + ... if_exists="replace", + ... clustering_columns=clustering_cols, + ... ) + 'bigframes-dev.birds.test-clusters' + Args: destination_table (Optional[str]): Name of table to be written, in the form ``dataset.tablename`` @@ -364,6 +376,15 @@ def to_gbq( If set, write the ordering of the DataFrame as a column in the result table with this name. + clustering_columns (Union[pd.Index, Iterable[Hashable]], default ()): + Specifies the columns for clustering in the BigQuery table. The order + of columns in this list is significant for clustering hierarchy. Index + columns may be included in clustering if the `index` parameter is set + to True, and their names are specified in this. These index columns, + if included, precede DataFrame columns in the clustering order. The + clustering order within the Index/DataFrame columns follows the order + specified in `clustering_columns`. + Returns: str: The fully-qualified ID for the written table, in the form From 21391a9d07bb0dc6b6f900f1b069350d6232bd92 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 4 Jan 2024 17:00:27 -0800 Subject: [PATCH 251/921] fix: DF.drop tuple input as multi-index (#301) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes b/317131784 --- bigframes/dataframe.py | 31 +++++++++++++++---- tests/system/small/test_multiindex.py | 1 + .../bigframes_vendored/pandas/core/frame.py | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9c90ad5b5fc..21310087414 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1062,12 +1062,31 @@ def drop( level_id = self._resolve_levels(level or 0)[0] if utils.is_list_like(index): - block, inverse_condition_id = block.apply_unary_op( - level_id, ops.IsInOp(index, match_nulls=True) - ) - block, condition_id = block.apply_unary_op( - inverse_condition_id, ops.invert_op - ) + # Only tuple is treated as multi-index value combinations + if isinstance(index, tuple): + if level is not None: + raise ValueError("Multi-index tuple can't specify level.") + condition_id = None + for i, idx in enumerate(index): + level_id = self._resolve_levels(i)[0] + block, condition_id_cur = block.apply_unary_op( + level_id, ops.partial_right(ops.ne_op, idx) + ) + if condition_id: + block, condition_id = block.apply_binary_op( + condition_id, condition_id_cur, ops.or_op + ) + else: + condition_id = condition_id_cur + + condition_id = typing.cast(str, condition_id) + else: + block, inverse_condition_id = block.apply_unary_op( + level_id, ops.IsInOp(index, match_nulls=True) + ) + block, condition_id = block.apply_unary_op( + inverse_condition_id, ops.invert_op + ) elif isinstance(index, indexes.Index): return self._drop_by_index(index) else: diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 058adb93908..d767b30bd67 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -234,6 +234,7 @@ def test_series_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, (1, 0), ([0, 1], 0), ([True, None], 1), + ((0, True), None), ], ) def test_multi_index_drop(scalars_df_index, scalars_pandas_df_index, labels, level): diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 66ed2088e46..bca18bd0b79 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1107,7 +1107,7 @@ def drop( Args: labels: - Index or column labels to drop. + Index or column labels to drop. A tuple will be used as a single label and not treated as a list-like. axis: Whether to drop labels from the index (0 or 'index') or columns (1 or 'columns'). From 1c63b451bb057e5b6470d63d4b44c090d7172aa5 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:55:08 -0800 Subject: [PATCH 252/921] docs: add code samples for `Series.ffill` and `DataFrame.ffill` (#307) * docs: add code samples for Series.ffill and DataFrame.ffill * address comments --- .../bigframes_vendored/pandas/core/generic.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 72b947f96ca..2ca51f64937 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -627,6 +627,48 @@ def copy(self): def ffill(self, *, limit: Optional[int] = None): """Fill NA/NaN values by propagating the last valid observation to next valid. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame([[np.nan, 2, np.nan, 0], + ... [3, 4, np.nan, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [np.nan, 3, np.nan, 4]], + ... columns=list("ABCD")).astype("Float64") + >>> df + A B C D + 0 2.0 0.0 + 1 3.0 4.0 1.0 + 2 + 3 3.0 4.0 + + [4 rows x 4 columns] + + Fill NA/NaN values in DataFrames: + + >>> df.ffill() + A B C D + 0 2.0 0.0 + 1 3.0 4.0 1.0 + 2 3.0 4.0 1.0 + 3 3.0 3.0 4.0 + + [4 rows x 4 columns] + + + Fill NA/NaN values in Series: + + >>> series = bpd.Series([1, np.nan, 2, 3]) + >>> series.ffill() + 0 1.0 + 1 1.0 + 2 2.0 + 3 3.0 + dtype: Float64 + Args: limit : int, default None If method is specified, this is the maximum number of consecutive From f645c56e5436adb100018afbf9ef18003a1a6ed9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 8 Jan 2024 11:22:15 -0800 Subject: [PATCH 253/921] feat: support assigning to columns like a property (#304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/log_adapter.py | 3 +- bigframes/dataframe.py | 41 ++++++++++++++++++++++ bigframes/series.py | 4 +++ tests/system/small/test_dataframe.py | 52 ++++++++++++++++++++++++++-- 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/bigframes/core/log_adapter.py b/bigframes/core/log_adapter.py index 556851fa1b7..860d394cd20 100644 --- a/bigframes/core/log_adapter.py +++ b/bigframes/core/log_adapter.py @@ -19,12 +19,13 @@ _lock = threading.Lock() MAX_LABELS_COUNT = 64 _api_methods: List = [] +_excluded_methods = ["__setattr__", "__getattr__"] def class_logger(decorated_cls): """Decorator that adds logging functionality to each method of the class.""" for attr_name, attr_value in decorated_cls.__dict__.items(): - if callable(attr_value): + if callable(attr_value) and (attr_name not in _excluded_methods): setattr(decorated_cls, attr_name, method_logger(attr_value, decorated_cls)) return decorated_cls diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 21310087414..cd3dacd2eeb 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -251,6 +251,15 @@ def index( ) -> indexes.Index: return indexes.Index(self) + @index.setter + def index(self, value): + # TODO: Handle assigning MultiIndex + result = self._assign_single_item("_new_bf_index", value).set_index( + "_new_bf_index" + ) + self._set_block(result._get_block()) + self.index.name = value.name if hasattr(value, "name") else None + @property def loc(self) -> indexers.LocDataFrameIndexer: return indexers.LocDataFrameIndexer(self) @@ -545,6 +554,29 @@ def __getattr__(self, key: str): else: raise AttributeError(key) + def __setattr__(self, key: str, value): + if key in ["_block", "_query_job"]: + object.__setattr__(self, key, value) + return + # Can this be removed??? + try: + # boring attributes go through boring old path + object.__getattribute__(self, key) + return object.__setattr__(self, key, value) + except AttributeError: + pass + + # if this fails, go on to more involved attribute setting + # (note that this matches __getattr__, above). + try: + if key in self.columns: + self[key] = value + else: + object.__setattr__(self, key, value) + # Can this be removed? + except (AttributeError, TypeError): + object.__setattr__(self, key, value) + def __repr__(self) -> str: """Converts a DataFrame to a string. Calls to_pandas. @@ -1265,6 +1297,15 @@ def _assign_single_item_listlike(self, k: str, v: Sequence) -> DataFrame: [get_column_left[col_id] for col_id in original_index_column_ids], index_labels=self._block.index_labels, ) + src_col = get_column_right[new_column_block.value_columns[0]] + # Check to see if key exists, and modify in place + col_ids = self._block.cols_matching_label(k) + for col_id in col_ids: + result_block = result_block.copy_values( + src_col, get_column_left[col_id] + ) + if len(col_ids) > 0: + result_block = result_block.drop_columns([src_col]) return DataFrame(result_block) def _assign_scalar(self, label: str, value: Union[int, float]) -> DataFrame: diff --git a/bigframes/series.py b/bigframes/series.py index eefd2b755dd..1247883aa50 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -139,6 +139,10 @@ def struct(self) -> structs.StructAccessor: def T(self) -> Series: return self.transpose() + @property + def _info_axis(self) -> indexes.Index: + return self.index + def transpose(self) -> Series: return self diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index fa3d5148a82..9557475b46d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3140,17 +3140,65 @@ def test_df___array__(scalars_df_index, scalars_pandas_df_index): ) -def test_getattr_attribute_error_when_pandas_has(scalars_df_index): +def test_df_getattr_attribute_error_when_pandas_has(scalars_df_index): # swapaxes is implemented in pandas but not in bigframes with pytest.raises(AttributeError): scalars_df_index.swapaxes() -def test_getattr_attribute_error(scalars_df_index): +def test_df_getattr_attribute_error(scalars_df_index): with pytest.raises(AttributeError): scalars_df_index.not_a_method() +def test_df_getattr_axes(): + df = dataframe.DataFrame( + [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] + ) + assert isinstance(df.index, bigframes.core.indexes.Index) + assert isinstance(df.columns, pandas.Index) + assert isinstance(df.my_column, series.Series) + + +def test_df_setattr_index(): + pd_df = pandas.DataFrame( + [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] + ) + bf_df = dataframe.DataFrame(pd_df) + pd_df.index = [4, 5] + bf_df.index = [4, 5] + + assert_pandas_df_equal( + pd_df, bf_df.to_pandas(), check_index_type=False, check_dtype=False + ) + + +def test_df_setattr_columns(): + pd_df = pandas.DataFrame( + [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] + ) + bf_df = dataframe.DataFrame(pd_df) + pd_df.columns = [4, 5, 6] + bf_df.columns = [4, 5, 6] + + assert_pandas_df_equal( + pd_df, bf_df.to_pandas(), check_index_type=False, check_dtype=False + ) + + +def test_df_setattr_modify_column(): + pd_df = pandas.DataFrame( + [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] + ) + bf_df = dataframe.DataFrame(pd_df) + pd_df.my_column = [4, 5] + bf_df.my_column = [4, 5] + + assert_pandas_df_equal( + pd_df, bf_df.to_pandas(), check_index_type=False, check_dtype=False + ) + + def test_loc_list_string_index(scalars_df_index, scalars_pandas_df_index): index_list = scalars_pandas_df_index.string_col.iloc[[0, 1, 1, 5]].values From 2e1a4036e58fb6b35aa68ac6d121cb0d04f4f369 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 8 Jan 2024 13:06:15 -0800 Subject: [PATCH 254/921] feat: add Series dt.tz and dt.unit properties (#303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/operations/datetimes.py | 37 ++++++-- .../system/small/operations/test_datetimes.py | 95 ++++++++++++------- .../pandas/core/indexes/accessor.py | 19 ++++ 3 files changed, 107 insertions(+), 44 deletions(-) diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py index a8a33beb577..3165e6f0035 100644 --- a/bigframes/operations/datetimes.py +++ b/bigframes/operations/datetimes.py @@ -14,6 +14,9 @@ from __future__ import annotations +import datetime as dt +from typing import Optional + from bigframes.core import log_adapter import bigframes.operations as ops import bigframes.operations.base @@ -27,6 +30,7 @@ class DatetimeMethods( ): __doc__ = vendordt.DatetimeProperties.__doc__ + # Date accessors @property def day(self) -> series.Series: return self._apply_unary_op(ops.day_op) @@ -40,17 +44,26 @@ def date(self) -> series.Series: return self._apply_unary_op(ops.date_op) @property - def hour(self) -> series.Series: - return self._apply_unary_op(ops.hour_op) + def quarter(self) -> series.Series: + return self._apply_unary_op(ops.quarter_op) @property - def minute(self) -> series.Series: - return self._apply_unary_op(ops.minute_op) + def year(self) -> series.Series: + return self._apply_unary_op(ops.year_op) @property def month(self) -> series.Series: return self._apply_unary_op(ops.month_op) + # Time accessors + @property + def hour(self) -> series.Series: + return self._apply_unary_op(ops.hour_op) + + @property + def minute(self) -> series.Series: + return self._apply_unary_op(ops.minute_op) + @property def second(self) -> series.Series: return self._apply_unary_op(ops.second_op) @@ -60,9 +73,17 @@ def time(self) -> series.Series: return self._apply_unary_op(ops.time_op) @property - def quarter(self) -> series.Series: - return self._apply_unary_op(ops.quarter_op) + def tz(self) -> Optional[dt.timezone]: + # Assumption: pyarrow dtype + tz_string = self._dtype.pyarrow_dtype.tz + if tz_string == "UTC": + return dt.timezone.utc + elif tz_string is None: + return None + else: + raise ValueError(f"Unexpected timezone {tz_string}") @property - def year(self) -> series.Series: - return self._apply_unary_op(ops.year_op) + def unit(self) -> str: + # Assumption: pyarrow dtype + return self._dtype.pyarrow_dtype.unit diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 177194c7a8a..3882491ecb7 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -16,18 +16,22 @@ import pytest import bigframes.series -from tests.system.utils import assert_series_equal +from tests.system.utils import assert_series_equal, skip_legacy_pandas DATETIME_COL_NAMES = [("datetime_col",), ("timestamp_col",)] +DATE_COLUMNS = [ + ("datetime_col",), + ("timestamp_col",), + ("date_col",), +] @pytest.mark.parametrize( ("col_name",), - DATETIME_COL_NAMES, + DATE_COLUMNS, ) -def test_day(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_day(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.day.to_pandas() @@ -43,9 +47,8 @@ def test_day(scalars_dfs, col_name): ("col_name",), DATETIME_COL_NAMES, ) -def test_date(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_date(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.date.to_pandas() @@ -59,11 +62,10 @@ def test_date(scalars_dfs, col_name): @pytest.mark.parametrize( ("col_name",), - DATETIME_COL_NAMES, + DATE_COLUMNS, ) -def test_dayofweek(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_dayofweek(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.dayofweek.to_pandas() @@ -76,9 +78,8 @@ def test_dayofweek(scalars_dfs, col_name): ("col_name",), DATETIME_COL_NAMES, ) -def test_hour(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_hour(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.hour.to_pandas() @@ -94,9 +95,8 @@ def test_hour(scalars_dfs, col_name): ("col_name",), DATETIME_COL_NAMES, ) -def test_minute(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_minute(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.minute.to_pandas() @@ -110,11 +110,10 @@ def test_minute(scalars_dfs, col_name): @pytest.mark.parametrize( ("col_name",), - DATETIME_COL_NAMES, + DATE_COLUMNS, ) -def test_month(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_month(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.month.to_pandas() @@ -128,11 +127,10 @@ def test_month(scalars_dfs, col_name): @pytest.mark.parametrize( ("col_name",), - DATETIME_COL_NAMES, + DATE_COLUMNS, ) -def test_quarter(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_quarter(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.quarter.to_pandas() @@ -148,9 +146,8 @@ def test_quarter(scalars_dfs, col_name): ("col_name",), DATETIME_COL_NAMES, ) -def test_second(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_second(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.second.to_pandas() @@ -166,9 +163,8 @@ def test_second(scalars_dfs, col_name): ("col_name",), DATETIME_COL_NAMES, ) -def test_time(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_time(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.time.to_pandas() @@ -182,11 +178,10 @@ def test_time(scalars_dfs, col_name): @pytest.mark.parametrize( ("col_name",), - DATETIME_COL_NAMES, + DATE_COLUMNS, ) -def test_year(scalars_dfs, col_name): - if pd.__version__.startswith("1."): - pytest.skip("Pyarrow datetime objects not support in pandas 1.x.") +@skip_legacy_pandas +def test_dt_year(scalars_dfs, col_name): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df[col_name] bf_result = bf_series.dt.year.to_pandas() @@ -196,3 +191,31 @@ def test_year(scalars_dfs, col_name): pd_result.astype(pd.Int64Dtype()), bf_result, ) + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +@skip_legacy_pandas +def test_dt_tz(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.tz + pd_result = scalars_pandas_df[col_name].dt.tz + + assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +@skip_legacy_pandas +def test_dt_unit(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df[col_name] + bf_result = bf_series.dt.unit + pd_result = scalars_pandas_df[col_name].dt.unit + + assert bf_result == pd_result diff --git a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py index 2b4a3263175..9490f4608b2 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py @@ -94,3 +94,22 @@ def year(self): """The year of the datetime.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def tz(self): + """Return the timezone. + + Returns: + datetime.tzinfo, pytz.tzinfo.BaseTZInfo, dateutil.tz.tz.tzfile, or None + """ + + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def unit(self) -> str: + """Returns the unit of time precision. + + Returns: + Unit as string (eg. "us"). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From dafbc1bdb225c7132cdf7191792fde785947c7a1 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:42:30 -0800 Subject: [PATCH 255/921] feat: add to_gbq() method for LLM models (#299) --- bigframes/ml/llm.py | 74 +++++++++++++++++++++++++++++++ bigframes/ml/loader.py | 23 ++++++++++ tests/system/small/ml/test_llm.py | 50 ++++++++++++++++++++- 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 5beb54a32d4..8c011591138 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -19,6 +19,8 @@ from typing import cast, Literal, Optional, Union import warnings +from google.cloud import bigquery + import bigframes from bigframes import clients, constants from bigframes.core import blocks, log_adapter @@ -113,6 +115,26 @@ def _create_bqml_model(self): session=self.session, connection_name=self.connection_name, options=options ) + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> PaLM2TextGenerator: + assert model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in model._properties + assert "endpoint" in model._properties["remoteModelInfo"] + assert "connection" in model._properties["remoteModelInfo"] + + # Parse the remote model endpoint + bqml_endpoint = model._properties["remoteModelInfo"]["endpoint"] + model_connection = model._properties["remoteModelInfo"]["connection"] + model_endpoint = bqml_endpoint.split("/")[-1] + + text_generator_model = cls( + session=session, model_name=model_endpoint, connection_name=model_connection + ) + text_generator_model._bqml_model = core.BqmlModel(session, model) + return text_generator_model + def predict( self, X: Union[bpd.DataFrame, bpd.Series], @@ -200,6 +222,21 @@ def predict( return df + def to_gbq(self, model_name: str, replace: bool = False) -> PaLM2TextGenerator: + """Save the model to BigQuery. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + PaLM2TextGenerator: saved model.""" + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) + @log_adapter.class_logger class PaLM2TextEmbeddingGenerator(base.Predictor): @@ -271,6 +308,26 @@ def _create_bqml_model(self): session=self.session, connection_name=self.connection_name, options=options ) + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> PaLM2TextEmbeddingGenerator: + assert model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in model._properties + assert "endpoint" in model._properties["remoteModelInfo"] + assert "connection" in model._properties["remoteModelInfo"] + + # Parse the remote model endpoint + bqml_endpoint = model._properties["remoteModelInfo"]["endpoint"] + model_connection = model._properties["remoteModelInfo"]["connection"] + model_endpoint = bqml_endpoint.split("/")[-1] + + embedding_generator_model = cls( + session=session, model_name=model_endpoint, connection_name=model_connection + ) + embedding_generator_model._bqml_model = core.BqmlModel(session, model) + return embedding_generator_model + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. @@ -307,3 +364,20 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) return df + + def to_gbq( + self, model_name: str, replace: bool = False + ) -> PaLM2TextEmbeddingGenerator: + """Save the model to BigQuery. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + PaLM2TextEmbeddingGenerator: saved model.""" + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 805747c49ba..4ffde43543a 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -28,6 +28,7 @@ forecasting, imported, linear_model, + llm, pipeline, ) @@ -47,6 +48,15 @@ } ) +_BQML_ENDPOINT_TYPE_MAPPING = MappingProxyType( + { + llm._TEXT_GENERATOR_BISON_ENDPOINT: llm.PaLM2TextGenerator, + llm._TEXT_GENERATOR_BISON_32K_ENDPOINT: llm.PaLM2TextGenerator, + llm._EMBEDDING_GENERATOR_GECKO_ENDPOINT: llm.PaLM2TextEmbeddingGenerator, + llm._EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT: llm.PaLM2TextEmbeddingGenerator, + } +) + def from_bq( session: bigframes.Session, bq_model: bigquery.Model @@ -62,6 +72,8 @@ def from_bq( ensemble.RandomForestClassifier, imported.TensorFlowModel, imported.ONNXModel, + llm.PaLM2TextGenerator, + llm.PaLM2TextEmbeddingGenerator, pipeline.Pipeline, ]: """Load a BQML model to BigQuery DataFrames ML. @@ -84,6 +96,17 @@ def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): return _BQML_MODEL_TYPE_MAPPING[bq_model.model_type]._from_bq( # type: ignore session=session, model=bq_model ) + if ( + bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + and "remoteModelInfo" in bq_model._properties + and "endpoint" in bq_model._properties["remoteModelInfo"] + ): + # Parse the remote model endpoint + bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] + endpoint_model = bqml_endpoint.split("/")[-1] + return _BQML_ENDPOINT_TYPE_MAPPING[endpoint_model]._from_bq( # type: ignore + session=session, model=bq_model + ) raise NotImplementedError( f"Model type {bq_model.model_type} is not yet supported by BigQuery DataFrames. {constants.FEEDBACK_LINK}" diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index fd1b803eea2..805cee4fecb 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -17,11 +17,37 @@ from bigframes.ml import llm -def test_create_text_generator_model(palm2_text_generator_model): +def test_create_text_generator_model( + palm2_text_generator_model, dataset_id, bq_connection +): # Model creation doesn't return error assert palm2_text_generator_model is not None assert palm2_text_generator_model._bqml_model is not None + # save, load to ensure configuration was kept + reloaded_model = palm2_text_generator_model.to_gbq( + f"{dataset_id}.temp_text_model", replace=True + ) + assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.model_name == "text-bison" + assert reloaded_model.connection_name == bq_connection + + +def test_create_text_generator_32k_model( + palm2_text_generator_32k_model, dataset_id, bq_connection +): + # Model creation doesn't return error + assert palm2_text_generator_32k_model is not None + assert palm2_text_generator_32k_model._bqml_model is not None + + # save, load to ensure configuration was kept + reloaded_model = palm2_text_generator_32k_model.to_gbq( + f"{dataset_id}.temp_text_model", replace=True + ) + assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.model_name == "text-bison-32k" + assert reloaded_model.connection_name == bq_connection + @pytest.mark.flaky(retries=2, delay=120) def test_create_text_generator_model_default_session( @@ -152,19 +178,39 @@ def test_text_generator_predict_with_params_success( assert all(series.str.len() > 20) -def test_create_embedding_generator_model(palm2_embedding_generator_model): +def test_create_embedding_generator_model( + palm2_embedding_generator_model, dataset_id, bq_connection +): # Model creation doesn't return error assert palm2_embedding_generator_model is not None assert palm2_embedding_generator_model._bqml_model is not None + # save, load to ensure configuration was kept + reloaded_model = palm2_embedding_generator_model.to_gbq( + f"{dataset_id}.temp_embedding_model", replace=True + ) + assert f"{dataset_id}.temp_embedding_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.model_name == "textembedding-gecko" + assert reloaded_model.connection_name == bq_connection + def test_create_embedding_generator_multilingual_model( palm2_embedding_generator_multilingual_model, + dataset_id, + bq_connection, ): # Model creation doesn't return error assert palm2_embedding_generator_multilingual_model is not None assert palm2_embedding_generator_multilingual_model._bqml_model is not None + # save, load to ensure configuration was kept + reloaded_model = palm2_embedding_generator_multilingual_model.to_gbq( + f"{dataset_id}.temp_embedding_model", replace=True + ) + assert f"{dataset_id}.temp_embedding_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.model_name == "textembedding-gecko-multilingual" + assert reloaded_model.connection_name == bq_connection + def test_create_text_embedding_generator_model_defaults(bq_connection): import bigframes.pandas as bpd From 2d98a5ae3efb9876991b8b8e3c240cf183ee3565 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 8 Jan 2024 17:34:18 -0800 Subject: [PATCH 256/921] refactor: unify row operators to same interface (#100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 15 +- bigframes/core/block_transforms.py | 2 +- bigframes/core/blocks.py | 12 +- bigframes/core/compile/compiled.py | 57 +- bigframes/core/compile/compiler.py | 20 +- bigframes/core/compile/scalar_op_compiler.py | 1134 +++++++++++++++ bigframes/core/indexes/index.py | 10 +- bigframes/core/nodes.py | 29 +- bigframes/dataframe.py | 28 +- bigframes/operations/__init__.py | 1360 ++++-------------- bigframes/operations/base.py | 4 +- bigframes/operations/strings.py | 50 +- bigframes/operations/structs.py | 19 +- bigframes/pandas/__init__.py | 4 +- bigframes/series.py | 32 +- 15 files changed, 1534 insertions(+), 1242 deletions(-) create mode 100644 bigframes/core/compile/scalar_op_compiler.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 13b05620927..7ff23efad3d 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -157,8 +157,8 @@ def project_unary_op( ) -> ArrayValue: """Creates a new expression based on this expression with unary operation applied to one column.""" return ArrayValue( - nodes.ProjectUnaryOpNode( - child=self.node, input_id=column_name, op=op, output_id=output_name + nodes.ProjectRowOpNode( + child=self.node, input_ids=(column_name,), op=op, output_id=output_name ) ) @@ -171,10 +171,9 @@ def project_binary_op( ) -> ArrayValue: """Creates a new expression based on this expression with binary operation applied to two columns.""" return ArrayValue( - nodes.ProjectBinaryOpNode( + nodes.ProjectRowOpNode( child=self.node, - left_input_id=left_column_id, - right_input_id=right_column_id, + input_ids=(left_column_id, right_column_id), op=op, output_id=output_column_id, ) @@ -190,11 +189,9 @@ def project_ternary_op( ) -> ArrayValue: """Creates a new expression based on this expression with ternary operation applied to three columns.""" return ArrayValue( - nodes.ProjectTernaryOpNode( + nodes.ProjectRowOpNode( child=self.node, - input_id1=col_id_1, - input_id2=col_id_2, - input_id3=col_id_3, + input_ids=(col_id_1, col_id_2, col_id_3), op=op, output_id=output_column_id, ) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index c6867c1a33d..0b6886562e0 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -45,7 +45,7 @@ def equals(block1: blocks.Block, block2: blocks.Block) -> bool: lcolmapped = lmap[lcol] rcolmapped = rmap[rcol] joined_block, result_id = joined_block.apply_binary_op( - lcolmapped, rcolmapped, ops.eq_nulls_match_op + lcolmapped, rcolmapped, ops.eq_null_match_op ) joined_block, result_id = joined_block.apply_unary_op( result_id, ops.partial_right(ops.fillna_op, False) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index e88326795c2..9688f439b1a 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -581,12 +581,12 @@ def _split( # Create an ordering col and convert to string block, ordering_col = block.promote_offsets() block, string_ordering_col = block.apply_unary_op( - ordering_col, ops.AsTypeOp("string[pyarrow]") + ordering_col, ops.AsTypeOp(to_type="string[pyarrow]") ) # Apply hash method to sum col and order by it. block, string_sum_col = block.apply_binary_op( - string_ordering_col, random_state_col, ops.concat_op + string_ordering_col, random_state_col, ops.strconcat_op ) block, hash_string_sum_col = block.apply_unary_op(string_sum_col, ops.hash_op) block = block.order_by([ordering.OrderingColumnReference(hash_string_sum_col)]) @@ -1232,8 +1232,8 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - expr = expr.project_unary_op(index_col, ops.AsTypeOp("string")) - prefix_op = ops.BinopPartialLeft(ops.add_op, prefix) + expr = expr.project_unary_op(index_col, ops.AsTypeOp(to_type="string")) + prefix_op = ops.ApplyLeft(base_op=ops.add_op, left_scalar=prefix) expr = expr.project_unary_op(index_col, prefix_op) return Block( expr, @@ -1251,8 +1251,8 @@ def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - expr = expr.project_unary_op(index_col, ops.AsTypeOp("string")) - prefix_op = ops.BinopPartialRight(ops.add_op, suffix) + expr = expr.project_unary_op(index_col, ops.AsTypeOp(to_type="string")) + prefix_op = ops.ApplyRight(base_op=ops.add_op, right_scalar=suffix) expr = expr.project_unary_op(index_col, prefix_op) return Block( expr, diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 524699290b9..c1e8f1ea486 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -27,6 +27,7 @@ import pandas import bigframes.constants as constants +import bigframes.core.compile.scalar_op_compiler as op_compilers import bigframes.core.guid from bigframes.core.ordering import ( encode_order_string, @@ -43,8 +44,11 @@ ORDER_ID_COLUMN = "bigframes_ordering_id" PREDICATE_COLUMN = "bigframes_predicate" + T = typing.TypeVar("T", bound="BaseIbisIR") +op_compiler = op_compilers.scalar_op_compiler + class BaseIbisIR(abc.ABC): """Implementation detail, contains common logic between ordered and unordered IR""" @@ -147,49 +151,20 @@ def _reproject_to_table(self: T) -> T: """ ... - def project_unary_op( + def project_row_op( self: T, - input_column_id: str, - op: ops.UnaryOp, + input_column_ids: typing.Sequence[str], + op: ops.RowOp, output_column_id: typing.Optional[str] = None, ) -> T: """Creates a new expression based on this expression with unary operation applied to one column.""" result_id = ( - output_column_id or input_column_id + output_column_id or input_column_ids[0] ) # overwrite input if not output id provided - value = op._as_ibis(self._get_ibis_column(input_column_id)).name(result_id) + inputs = tuple(self._get_ibis_column(col) for col in input_column_ids) + value = op_compiler.compile_row_op(op, inputs).name(result_id) return self._set_or_replace_by_id(result_id, value) - def project_binary_op( - self: T, - left_column_id: str, - right_column_id: str, - op: ops.BinaryOp, - output_column_id: str, - ) -> T: - """Creates a new expression based on this expression with binary operation applied to two columns.""" - value = op( - self._get_ibis_column(left_column_id), - self._get_ibis_column(right_column_id), - ).name(output_column_id) - return self._set_or_replace_by_id(output_column_id, value) - - def project_ternary_op( - self: T, - col_id_1: str, - col_id_2: str, - col_id_3: str, - op: ops.TernaryOp, - output_column_id: str, - ) -> T: - """Creates a new expression based on this expression with ternary operation applied to three columns.""" - value = op( - self._get_ibis_column(col_id_1), - self._get_ibis_column(col_id_2), - self._get_ibis_column(col_id_3), - ).name(output_column_id) - return self._set_or_replace_by_id(output_column_id, value) - def assign(self: T, source_id: str, destination_id: str) -> T: return self._set_or_replace_by_id( destination_id, self._get_ibis_column(source_id) @@ -454,7 +429,9 @@ def unpivot( None, force_dtype=col_dtype ) ibis_values = [ - ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) + op_compiler.compile_row_op( + ops.AsTypeOp(col_dtype), (unpivot_table[col],) + ) if col is not None else null_value for col in source_cols @@ -521,9 +498,7 @@ def aggregate( expr = OrderedIR(result, columns=columns, ordering=ordering) if dropna: for column_id in by_column_ids: - expr = expr._filter( - ops.notnull_op._as_ibis(expr._get_ibis_column(column_id)) - ) + expr = expr._filter(expr._get_ibis_column(column_id).notnull()) # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation return expr._project_offsets() else: @@ -982,7 +957,9 @@ def unpivot( None, force_dtype=col_dtype ) ibis_values = [ - ops.AsTypeOp(col_dtype)._as_ibis(unpivot_table[col]) + op_compiler.compile_row_op( + ops.AsTypeOp(col_dtype), (unpivot_table[col],) + ) if col is not None else null_value for col in source_cols diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 17dcde638fa..c28958a8612 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -143,23 +143,9 @@ def compile_reversed(node: nodes.ReversedNode, ordered: bool = True): @_compile_node.register -def compile_project_unary(node: nodes.ProjectUnaryOpNode, ordered: bool = True): - return compile_node(node.child, ordered).project_unary_op( - node.input_id, node.op, node.output_id - ) - - -@_compile_node.register -def compile_project_binary(node: nodes.ProjectBinaryOpNode, ordered: bool = True): - return compile_node(node.child, ordered).project_binary_op( - node.left_input_id, node.right_input_id, node.op, node.output_id - ) - - -@_compile_node.register -def compile_project_ternary(node: nodes.ProjectTernaryOpNode, ordered: bool = True): - return compile_node(node.child, ordered).project_ternary_op( - node.input_id1, node.input_id2, node.input_id3, node.op, node.output_id +def compile_project(node: nodes.ProjectRowOpNode, ordered: bool = True): + return compile_node(node.child, ordered).project_row_op( + node.input_ids, node.op, node.output_id ) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py new file mode 100644 index 00000000000..d711dbf4565 --- /dev/null +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -0,0 +1,1134 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import functools +import typing + +import ibis +import ibis.common.exceptions +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.operations.generic +import ibis.expr.types as ibis_types +import numpy as np +import pandas as pd + +import bigframes.constants as constants +import bigframes.dtypes +import bigframes.dtypes as dtypes +import bigframes.operations as ops + +_ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) +_NAN = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.nan)) +_INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.inf)) +_NEG_INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(-np.inf)) + +# Approx Highest number you can pass in to EXP function and get a valid FLOAT64 result +# FLOAT64 has 11 exponent bits, so max values is about 2**(2**10) +# ln(2**(2**10)) == (2**10)*ln(2) ~= 709.78, so EXP(x) for x>709.78 will overflow. +_FLOAT64_EXP_BOUND = typing.cast(ibis_types.NumericValue, ibis_types.literal(709.78)) + + +class ScalarOpCompiler: + # Mapping of operation name to implemenations + _registry: dict[ + str, + typing.Callable[ + [typing.Sequence[ibis_types.Value], ops.RowOp], ibis_types.Value + ], + ] = {} + + def compile_row_op( + self, op: ops.RowOp, inputs: typing.Sequence[ibis_types.Value] + ) -> ibis_types.Value: + impl = self._registry[op.name] + return impl(inputs, op) + + def register_unary_op( + self, + op_ref: typing.Union[ops.UnaryOp, type[ops.UnaryOp]], + pass_op: bool = False, + ): + """ + Decorator to register a unary op implementation. + + Args: + op_ref (UnaryOp or UnaryOp type): + Class or instance of operator that is implemented by the decorated function. + pass_op (bool): + Set to true if implementation takes the operator object as the last argument. + This is needed for parameterized ops where parameters are part of op object. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + if pass_op: + return impl(args[0], op) + else: + return impl(args[0]) + + self._register(key, normalized_impl) + return impl + + return decorator + + def register_binary_op( + self, + op_ref: typing.Union[ops.BinaryOp, type[ops.BinaryOp]], + pass_op: bool = False, + ): + """ + Decorator to register a binary op implementation. + + Args: + op_ref (BinaryOp or BinaryOp type): + Class or instance of operator that is implemented by the decorated function. + pass_op (bool): + Set to true if implementation takes the operator object as the last argument. + This is needed for parameterized ops where parameters are part of op object. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + if pass_op: + return impl(args[0], args[1], op) + else: + return impl(args[0], args[1]) + + self._register(key, normalized_impl) + return impl + + return decorator + + def register_ternary_op( + self, op_ref: typing.Union[ops.TernaryOp, type[ops.TernaryOp]] + ): + """ + Decorator to register a ternary op implementation. + + Args: + op_ref (TernaryOp or TernaryOp type): + Class or instance of operator that is implemented by the decorated function. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + return impl(args[0], args[1], args[2]) + + self._register(key, normalized_impl) + return impl + + return decorator + + def _register( + self, + op_name: str, + impl: typing.Callable[ + [typing.Sequence[ibis_types.Value], ops.RowOp], ibis_types.Value + ], + ): + if op_name in self._registry: + raise ValueError(f"Operation name {op_name} already registered") + self._registry[op_name] = impl + + +# Singleton compiler +scalar_op_compiler = ScalarOpCompiler() + + +### Unary Ops +@scalar_op_compiler.register_unary_op(ops.isnull_op) +def isnull_op_impl(x: ibis_types.Value): + return x.isnull() + + +@scalar_op_compiler.register_unary_op(ops.notnull_op) +def notnull_op_impl(x: ibis_types.Value): + return x.notnull() + + +@scalar_op_compiler.register_unary_op(ops.hash_op) +def hash_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.IntegerValue, x).hash() + + +# Trig Functions +@scalar_op_compiler.register_unary_op(ops.sin_op) +def sin_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).sin() + + +@scalar_op_compiler.register_unary_op(ops.cos_op) +def cos_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).cos() + + +@scalar_op_compiler.register_unary_op(ops.tan_op) +def tan_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).tan() + + +# Inverse trig functions +@scalar_op_compiler.register_unary_op(ops.arcsin_op) +def arcsin_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value.abs() <= _ibis_num(1) + return (~domain).ifelse(_NAN, numeric_value.asin()) + + +@scalar_op_compiler.register_unary_op(ops.arccos_op) +def arccos_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value.abs() <= _ibis_num(1) + return (~domain).ifelse(_NAN, numeric_value.acos()) + + +@scalar_op_compiler.register_unary_op(ops.arctan_op) +def arctan_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).atan() + + +# Hyperbolic trig functions +# BQ has these functions, but Ibis doesn't +@scalar_op_compiler.register_unary_op(ops.sinh_op) +def sinh_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + sinh_result = (numeric_value.exp() - (numeric_value.negate()).exp()) / _ibis_num(2) + domain = numeric_value.abs() < _FLOAT64_EXP_BOUND + return (~domain).ifelse(_INF * numeric_value.sign(), sinh_result) + + +@scalar_op_compiler.register_unary_op(ops.cosh_op) +def cosh_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + cosh_result = (numeric_value.exp() + (numeric_value.negate()).exp()) / _ibis_num(2) + domain = numeric_value.abs() < _FLOAT64_EXP_BOUND + return (~domain).ifelse(_INF, cosh_result) + + +@scalar_op_compiler.register_unary_op(ops.tanh_op) +def tanh_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + tanh_result = (numeric_value.exp() - (numeric_value.negate()).exp()) / ( + numeric_value.exp() + (numeric_value.negate()).exp() + ) + # Beyond +-20, is effectively just the sign function + domain = numeric_value.abs() < _ibis_num(20) + return (~domain).ifelse(numeric_value.sign(), tanh_result) + + +@scalar_op_compiler.register_unary_op(ops.arcsinh_op) +def arcsinh_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + sqrt_part = ((numeric_value * numeric_value) + _ibis_num(1)).sqrt() + return (numeric_value.abs() + sqrt_part).ln() * numeric_value.sign() + + +@scalar_op_compiler.register_unary_op(ops.arccosh_op) +def arccosh_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + sqrt_part = ((numeric_value * numeric_value) - _ibis_num(1)).sqrt() + acosh_result = (numeric_value + sqrt_part).ln() + domain = numeric_value >= _ibis_num(1) + return (~domain).ifelse(_NAN, acosh_result) + + +@scalar_op_compiler.register_unary_op(ops.arctanh_op) +def arctanh_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value.abs() < _ibis_num(1) + numerator = numeric_value + _ibis_num(1) + denominator = _ibis_num(1) - numeric_value + ln_input = typing.cast(ibis_types.NumericValue, numerator.div(denominator)) + atanh_result = ln_input.ln().div(2) + + out_of_domain = (numeric_value.abs() == _ibis_num(1)).ifelse( + _INF * numeric_value, _NAN + ) + + return (~domain).ifelse(out_of_domain, atanh_result) + + +# Numeric Ops +@scalar_op_compiler.register_unary_op(ops.abs_op) +def abs_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).abs() + + +@scalar_op_compiler.register_unary_op(ops.sqrt_op) +def sqrt_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value >= _ZERO + return (~domain).ifelse(_NAN, numeric_value.sqrt()) + + +@scalar_op_compiler.register_unary_op(ops.log10_op) +def log10_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value > _ZERO + out_of_domain = (numeric_value == _ZERO).ifelse(_NEG_INF, _NAN) + return (~domain).ifelse(out_of_domain, numeric_value.log10()) + + +@scalar_op_compiler.register_unary_op(ops.ln_op) +def ln_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value > _ZERO + out_of_domain = (numeric_value == _ZERO).ifelse(_NEG_INF, _NAN) + return (~domain).ifelse(out_of_domain, numeric_value.ln()) + + +@scalar_op_compiler.register_unary_op(ops.exp_op) +def exp_op_impl(x: ibis_types.Value): + numeric_value = typing.cast(ibis_types.NumericValue, x) + domain = numeric_value < _FLOAT64_EXP_BOUND + return (~domain).ifelse(_INF, numeric_value.exp()) + + +@scalar_op_compiler.register_unary_op(ops.invert_op) +def invert_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).negate() + + +## String Operation +@scalar_op_compiler.register_unary_op(ops.len_op) +def len_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).length().cast(ibis_dtypes.int64) + + +@scalar_op_compiler.register_unary_op(ops.reverse_op) +def reverse_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).reverse() + + +@scalar_op_compiler.register_unary_op(ops.lower_op) +def lower_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).lower() + + +@scalar_op_compiler.register_unary_op(ops.upper_op) +def upper_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).upper() + + +@scalar_op_compiler.register_unary_op(ops.strip_op) +def strip_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).strip() + + +@scalar_op_compiler.register_unary_op(ops.isnumeric_op) +def isnumeric_op_impl(x: ibis_types.Value): + # catches all members of the Unicode number class, which matches pandas isnumeric + # see https://cloud.google.com/bigquery/docs/reference/standard-sql/string_functions#regexp_contains + # TODO: Validate correctness, my miss eg ⅕ character + return typing.cast(ibis_types.StringValue, x).re_search(r"^(\pN+)$") + + +@scalar_op_compiler.register_unary_op(ops.isalpha_op) +def isalpha_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_search( + r"^(\p{Lm}|\p{Lt}|\p{Lu}|\p{Ll}|\p{Lo})+$" + ) + + +@scalar_op_compiler.register_unary_op(ops.isdigit_op) +def isdigit_op_impl(x: ibis_types.Value): + # Based on docs, should include superscript/subscript-ed numbers + # Tests however pass only when set to Nd unicode class + return typing.cast(ibis_types.StringValue, x).re_search(r"^(\p{Nd})+$") + + +@scalar_op_compiler.register_unary_op(ops.isdecimal_op) +def isdecimal_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_search(r"^(\p{Nd})+$") + + +@scalar_op_compiler.register_unary_op(ops.isalnum_op) +def isalnum_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).re_search( + r"^(\p{N}|\p{Lm}|\p{Lt}|\p{Lu}|\p{Ll}|\p{Lo})+$" + ) + + +@scalar_op_compiler.register_unary_op(ops.isspace_op) +def isspace_op_impl(x: ibis_types.Value): + # All characters are whitespace characters, False for empty string + return typing.cast(ibis_types.StringValue, x).re_search(r"^\s+$") + + +@scalar_op_compiler.register_unary_op(ops.islower_op) +def islower_op_impl(x: ibis_types.Value): + # No upper case characters, min one cased character + # See: https://docs.python.org/3/library/stdtypes.html#str + return typing.cast(ibis_types.StringValue, x).re_search(r"\p{Ll}") & ~typing.cast( + ibis_types.StringValue, x + ).re_search(r"\p{Lu}|\p{Lt}") + + +@scalar_op_compiler.register_unary_op(ops.isupper_op) +def isupper_op_impl(x: ibis_types.Value): + # No lower case characters, min one cased character + # See: https://docs.python.org/3/library/stdtypes.html#str + return typing.cast(ibis_types.StringValue, x).re_search(r"\p{Lu}") & ~typing.cast( + ibis_types.StringValue, x + ).re_search(r"\p{Ll}|\p{Lt}") + + +@scalar_op_compiler.register_unary_op(ops.rstrip_op) +def rstrip_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).rstrip() + + +@scalar_op_compiler.register_unary_op(ops.lstrip_op) +def lstrip_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).lstrip() + + +@scalar_op_compiler.register_unary_op(ops.capitalize_op) +def capitalize_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.StringValue, x).capitalize() + + +@scalar_op_compiler.register_unary_op(ops.StrContainsOp, pass_op=True) +def strcontains_op(x: ibis_types.Value, op: ops.StrContainsOp): + return typing.cast(ibis_types.StringValue, x).contains(op.pat) + + +@scalar_op_compiler.register_unary_op(ops.StrContainsRegexOp, pass_op=True) +def contains_regex_op_impl(x: ibis_types.Value, op: ops.StrContainsRegexOp): + return typing.cast(ibis_types.StringValue, x).re_search(op.pat) + + +@scalar_op_compiler.register_unary_op(ops.StrGetOp, pass_op=True) +def strget_op_impl(x: ibis_types.Value, op: ops.StrGetOp): + substr = typing.cast( + ibis_types.StringValue, typing.cast(ibis_types.StringValue, x)[op.i] + ) + return substr.nullif(ibis_types.literal("")) + + +@scalar_op_compiler.register_unary_op(ops.StrPadOp, pass_op=True) +def strpad_op_impl(x: ibis_types.Value, op: ops.StrPadOp): + str_val = typing.cast(ibis_types.StringValue, x) + + # SQL pad operations will truncate, we do not want to truncate though. + pad_length = ibis.greatest(str_val.length(), op.length) + if op.side == "left": + return str_val.lpad(pad_length, op.fillchar) + elif op.side == "right": + return str_val.rpad(pad_length, op.fillchar) + else: # side == both + # Pad more on right side if can't pad both sides equally + lpad_amount = ((pad_length - str_val.length()) // 2) + str_val.length() + return str_val.lpad(lpad_amount, op.fillchar).rpad(pad_length, op.fillchar) + + +@scalar_op_compiler.register_unary_op(ops.ReplaceStrOp, pass_op=True) +def replacestring_op_impl(x: ibis_types.Value, op: ops.ReplaceStrOp): + pat_str_value = typing.cast(ibis_types.StringValue, ibis_types.literal(op.pat)) + repl_str_value = typing.cast(ibis_types.StringValue, ibis_types.literal(op.repl)) + return typing.cast(ibis_types.StringValue, x).replace(pat_str_value, repl_str_value) + + +@scalar_op_compiler.register_unary_op(ops.RegexReplaceStrOp, pass_op=True) +def replaceregex_op_impl(x: ibis_types.Value, op: ops.RegexReplaceStrOp): + return typing.cast(ibis_types.StringValue, x).re_replace(op.pat, op.repl) + + +@scalar_op_compiler.register_unary_op(ops.StartsWithOp, pass_op=True) +def startswith_op_impl(x: ibis_types.Value, op: ops.StartsWithOp): + any_match = None + for pat in op.pat: + pat_match = typing.cast(ibis_types.StringValue, x).startswith(pat) + if any_match is not None: + any_match = any_match | pat_match + else: + any_match = pat_match + return any_match if any_match is not None else ibis_types.literal(False) + + +@scalar_op_compiler.register_unary_op(ops.EndsWithOp, pass_op=True) +def endswith_op_impl(x: ibis_types.Value, op: ops.EndsWithOp): + any_match = None + for pat in op.pat: + pat_match = typing.cast(ibis_types.StringValue, x).endswith(pat) + if any_match is not None: + any_match = any_match | pat_match + else: + any_match = pat_match + return any_match if any_match is not None else ibis_types.literal(False) + + +@scalar_op_compiler.register_unary_op(ops.ZfillOp, pass_op=True) +def zfill_op_impl(x: ibis_types.Value, op: ops.ZfillOp): + str_value = typing.cast(ibis_types.StringValue, x) + return ( + ibis.case() + .when( + str_value[0] == "-", + "-" + + strpad_op_impl( + str_value.substr(1), + ops.StrPadOp(length=op.width - 1, fillchar="0", side="left"), + ), + ) + .else_( + strpad_op_impl( + str_value, ops.StrPadOp(length=op.width, fillchar="0", side="left") + ) + ) + .end() + ) + + +@scalar_op_compiler.register_unary_op(ops.StrFindOp, pass_op=True) +def find_op_impl(x: ibis_types.Value, op: ops.StrFindOp): + return typing.cast(ibis_types.StringValue, x).find(op.substr, op.start, op.end) + + +@scalar_op_compiler.register_unary_op(ops.StrExtractOp, pass_op=True) +def extract_op_impl(x: ibis_types.Value, op: ops.StrExtractOp): + return typing.cast(ibis_types.StringValue, x).re_extract(op.pat, op.n) + + +@scalar_op_compiler.register_unary_op(ops.StrSliceOp, pass_op=True) +def slice_op_impl(x: ibis_types.Value, op: ops.StrSliceOp): + return typing.cast(ibis_types.StringValue, x)[op.start : op.end] + + +@scalar_op_compiler.register_unary_op(ops.StrRepeatOp, pass_op=True) +def repeat_op_impl(x: ibis_types.Value, op: ops.StrRepeatOp): + return typing.cast(ibis_types.StringValue, x).repeat(op.repeats) + + +## Datetime Ops +@scalar_op_compiler.register_unary_op(ops.day_op) +def day_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).day().cast(ibis_dtypes.int64) + + +@scalar_op_compiler.register_unary_op(ops.date_op) +def date_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).date() + + +@scalar_op_compiler.register_unary_op(ops.dayofweek_op) +def dayofweek_op_impl(x: ibis_types.Value): + return ( + typing.cast(ibis_types.TimestampValue, x) + .day_of_week.index() + .cast(ibis_dtypes.int64) + ) + + +@scalar_op_compiler.register_unary_op(ops.hour_op) +def hour_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).hour().cast(ibis_dtypes.int64) + + +@scalar_op_compiler.register_unary_op(ops.minute_op) +def minute_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).minute().cast(ibis_dtypes.int64) + + +@scalar_op_compiler.register_unary_op(ops.month_op) +def month_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).month().cast(ibis_dtypes.int64) + + +@scalar_op_compiler.register_unary_op(ops.quarter_op) +def quarter_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).quarter().cast(ibis_dtypes.int64) + + +@scalar_op_compiler.register_unary_op(ops.second_op) +def second_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).second().cast(ibis_dtypes.int64) + + +@scalar_op_compiler.register_unary_op(ops.time_op) +def time_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).time() + + +@scalar_op_compiler.register_unary_op(ops.year_op) +def year_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.TimestampValue, x).year().cast(ibis_dtypes.int64) + + +# Parameterized ops +@scalar_op_compiler.register_unary_op(ops.StructFieldOp, pass_op=True) +def struct_field_op_impl(x: ibis_types.Value, op: ops.StructFieldOp): + struct_value = typing.cast(ibis_types.StructValue, x) + if isinstance(op.name_or_index, str): + name = op.name_or_index + else: + name = struct_value.names[op.name_or_index] + return struct_value[name].name(name) + + +@scalar_op_compiler.register_unary_op(ops.AsTypeOp, pass_op=True) +def astype_op_impl(x: ibis_types.Value, op: ops.AsTypeOp): + to_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(op.to_type) + if isinstance(x, ibis_types.NullScalar): + return ibis_types.null().cast(to_type) + return bigframes.dtypes.cast_ibis_value(x, to_type) + + +@scalar_op_compiler.register_unary_op(ops.IsInOp, pass_op=True) +def isin_op_impl(x: ibis_types.Value, op: ops.IsInOp): + contains_nulls = any(is_null(value) for value in op.values) + matchable_ibis_values = [] + for item in op.values: + if not is_null(item): + try: + # we want values that *could* be cast to the dtype, but we don't want + # to actually cast it, as that could be lossy (eg float -> int) + item_inferred_type = ibis.literal(item).type() + if ( + x.type() == item_inferred_type + or x.type().is_numeric() + and item_inferred_type.is_numeric() + ): + matchable_ibis_values.append(item) + except TypeError: + pass + + if op.match_nulls and contains_nulls: + return x.isnull() | x.isin(matchable_ibis_values) + else: + return x.isin(matchable_ibis_values) + + +@scalar_op_compiler.register_unary_op(ops.RemoteFunctionOp, pass_op=True) +def remote_function_op_impl(x: ibis_types.Value, op: ops.RemoteFunctionOp): + if not hasattr(op.func, "bigframes_remote_function"): + raise TypeError( + f"only a bigframes remote function is supported as a callable. {constants.FEEDBACK_LINK}" + ) + x_transformed = op.func(x) + if not op.apply_on_null: + x_transformed = ibis.case().when(x.isnull(), x).else_(x_transformed).end() + return x_transformed + + +@scalar_op_compiler.register_unary_op(ops.MapOp, pass_op=True) +def map_op_impl(x: ibis_types.Value, op: ops.MapOp): + case = ibis.case() + for mapping in op.mappings: + case = case.when(x == mapping[0], mapping[1]) + return case.else_(x).end() + + +### Binary Ops +def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): + """Wraps a binary operator to generate nulls of the expected type if either input is a null scalar.""" + + def short_circuit_nulls_inner(binop): + @functools.wraps(binop) + def wrapped_binop(x: ibis_types.Value, y: ibis_types.Value): + if isinstance(x, ibis_types.NullScalar): + return ibis_types.null().cast(type_override or y.type()) + elif isinstance(y, ibis_types.NullScalar): + return ibis_types.null().cast(type_override or x.type()) + else: + return binop(x, y) + + return wrapped_binop + + return short_circuit_nulls_inner + + +@scalar_op_compiler.register_binary_op(ops.strconcat_op) +def concat_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + x_string = typing.cast(ibis_types.StringValue, x) + y_string = typing.cast(ibis_types.StringValue, y) + return x_string.concat(y_string) + + +@scalar_op_compiler.register_binary_op(ops.eq_op) +def eq_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x == y + + +@scalar_op_compiler.register_binary_op(ops.eq_null_match_op) +def eq_nulls_match_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + """Variant of eq_op where nulls match each other. Only use where dtypes are known to be same.""" + left = x.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) + right = y.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) + return left == right + + +@scalar_op_compiler.register_binary_op(ops.ne_op) +def ne_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x != y + + +def _null_or_value(value: ibis_types.Value, where_value: ibis_types.BooleanValue): + return ibis.where( + where_value, + value, + ibis.null(), + ) + + +@scalar_op_compiler.register_binary_op(ops.and_op) +def and_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + # Workaround issue https://github.com/ibis-project/ibis/issues/7775 by + # implementing three-valued logic ourselves. For AND, when we encounter a + # NULL value, we only know when the result is FALSE, otherwise the result + # is unknown (NULL). See: truth table at + # https://en.wikibooks.org/wiki/Structured_Query_Language/NULLs_and_the_Three_Valued_Logic#AND,_OR + if isinstance(x, ibis_types.NullScalar): + return _null_or_value(y, y == ibis.literal(False)) + + if isinstance(y, ibis_types.NullScalar): + return _null_or_value(x, x == ibis.literal(False)) + return typing.cast(ibis_types.BooleanValue, x) & typing.cast( + ibis_types.BooleanValue, y + ) + + +@scalar_op_compiler.register_binary_op(ops.or_op) +def or_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + # Workaround issue https://github.com/ibis-project/ibis/issues/7775 by + # implementing three-valued logic ourselves. For OR, when we encounter a + # NULL value, we only know when the result is TRUE, otherwise the result + # is unknown (NULL). See: truth table at + # https://en.wikibooks.org/wiki/Structured_Query_Language/NULLs_and_the_Three_Valued_Logic#AND,_OR + if isinstance(x, ibis_types.NullScalar): + return _null_or_value(y, y == ibis.literal(True)) + + if isinstance(y, ibis_types.NullScalar): + return _null_or_value(x, x == ibis.literal(True)) + return typing.cast(ibis_types.BooleanValue, x) | typing.cast( + ibis_types.BooleanValue, y + ) + + +@scalar_op_compiler.register_binary_op(ops.add_op) +@short_circuit_nulls() +def add_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + if isinstance(x, ibis_types.NullScalar) or isinstance(x, ibis_types.NullScalar): + return ibis.null() + try: + # Could be string concatenation or numeric addition. + return x + y # type: ignore + except ibis.common.annotations.SignatureValidationError as exc: + left_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(x.type()) + right_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(y.type()) + raise TypeError( + f"Cannot add {repr(left_type)} and {repr(right_type)}. {constants.FEEDBACK_LINK}" + ) from exc + + +@scalar_op_compiler.register_binary_op(ops.sub_op) +@short_circuit_nulls() +def sub_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.NumericValue, x) - typing.cast( + ibis_types.NumericValue, y + ) + + +@scalar_op_compiler.register_binary_op(ops.mul_op) +@short_circuit_nulls() +def mul_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.NumericValue, x) * typing.cast( + ibis_types.NumericValue, y + ) + + +@scalar_op_compiler.register_binary_op(ops.div_op) +@short_circuit_nulls(ibis_dtypes.float) +def div_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.NumericValue, x) / typing.cast( + ibis_types.NumericValue, y + ) + + +@scalar_op_compiler.register_binary_op(ops.pow_op) +@short_circuit_nulls(ibis_dtypes.float) +def pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + if x.type().is_integer() and y.type().is_integer(): + return _int_pow_op(x, y) + else: + return _float_pow_op(x, y) + + +@scalar_op_compiler.register_binary_op(ops.unsafe_pow_op) +@short_circuit_nulls(ibis_dtypes.float) +def unsafe_pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + """For internal use only - where domain and overflow checks are not needed.""" + return typing.cast(ibis_types.NumericValue, x) ** typing.cast( + ibis_types.NumericValue, y + ) + + +def _int_pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + # Need to avoid any error cases - should produce NaN instead + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions#pow + x_as_decimal = typing.cast( + ibis_types.NumericValue, + x.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)), + ) + y_val = typing.cast(ibis_types.NumericValue, y) + + # BQ POW() function outputs FLOAT64, which can lose precision. + # Therefore, we do math in NUMERIC and cast back down after. + # Also, explicit bounds checks, pandas will silently overflow. + pow_result = x_as_decimal**y_val + overflow_cond = (pow_result > _ibis_num((2**63) - 1)) | ( + pow_result < _ibis_num(-(2**63)) + ) + + return ( + ibis.case() + .when((overflow_cond), ibis.null()) + .else_(pow_result.cast(ibis_dtypes.int64)) + .end() + ) + + +def _float_pow_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + # Most conditions here seek to prevent calling BQ POW with inputs that would generate errors. + # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions#pow + x_val = typing.cast(ibis_types.NumericValue, x) + y_val = typing.cast(ibis_types.NumericValue, y) + + overflow_cond = (x_val != _ZERO) & ((y_val * x_val.abs().ln()) > _FLOAT64_EXP_BOUND) + + # Float64 lose integer precision beyond 2**53, beyond this insufficient precision to get parity + exp_too_big = y_val.abs() > _ibis_num(2**53) + # Treat very large exponents as +=INF + norm_exp = exp_too_big.ifelse(_INF * y_val.sign(), y_val) + + pow_result = x_val**norm_exp + + # This cast is dangerous, need to only excuted where y_val has been bounds-checked + # Ibis needs try_cast binding to bq safe_cast + exponent_is_whole = y_val.cast(ibis_dtypes.int64) == y_val + odd_exponent = (x_val < _ZERO) & ( + y_val.cast(ibis_dtypes.int64) % _ibis_num(2) == _ibis_num(1) + ) + infinite_base = x_val.abs() == _INF + + return ( + ibis.case() + # Might be able to do something more clever with x_val==0 case + .when(y_val == _ZERO, _ibis_num(1)) + .when( + x_val == _ibis_num(1), _ibis_num(1) + ) # Need to ignore exponent, even if it is NA + .when( + (x_val == _ZERO) & (y_val < _ZERO), _INF + ) # This case would error POW function in BQ + .when(infinite_base, pow_result) + .when( + exp_too_big, pow_result + ) # Bigquery can actually handle the +-inf cases gracefully + .when((x_val < _ZERO) & (~exponent_is_whole), _NAN) + .when( + overflow_cond, _INF * odd_exponent.ifelse(_ibis_num(-1), _ibis_num(1)) + ) # finite overflows would cause bq to error + .else_(pow_result) + .end() + ) + + +@scalar_op_compiler.register_binary_op(ops.lt_op) +@short_circuit_nulls(ibis_dtypes.bool) +def lt_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x < y + + +@scalar_op_compiler.register_binary_op(ops.le_op) +@short_circuit_nulls(ibis_dtypes.bool) +def le_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x <= y + + +@scalar_op_compiler.register_binary_op(ops.gt_op) +@short_circuit_nulls(ibis_dtypes.bool) +def gt_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x > y + + +@scalar_op_compiler.register_binary_op(ops.ge_op) +@short_circuit_nulls(ibis_dtypes.bool) +def ge_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x >= y + + +@scalar_op_compiler.register_binary_op(ops.floordiv_op) +@short_circuit_nulls(ibis_dtypes.int) +def floordiv_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + x_numeric = typing.cast(ibis_types.NumericValue, x) + y_numeric = typing.cast(ibis_types.NumericValue, y) + floordiv_expr = x_numeric // y_numeric + + # DIV(N, 0) will error in bigquery, but needs to return 0 for int, and inf for float in BQ so we short-circuit in this case. + # Multiplying left by zero propogates nulls. + zero_result = _INF if (x.type().is_floating() or y.type().is_floating()) else _ZERO + return ( + ibis.case() + .when(y_numeric == _ZERO, zero_result * x_numeric) + .else_(floordiv_expr) + .end() + ) + + +def _is_float(x: ibis_types.Value): + return isinstance(x, (ibis_types.FloatingColumn, ibis_types.FloatingScalar)) + + +@scalar_op_compiler.register_binary_op(ops.mod_op) +@short_circuit_nulls() +def mod_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + is_result_float = _is_float(x) | _is_float(y) + x_numeric = typing.cast( + ibis_types.NumericValue, + x.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) + if is_result_float + else x, + ) + y_numeric = typing.cast( + ibis_types.NumericValue, + y.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) + if is_result_float + else y, + ) + # Hacky short-circuit to avoid passing zero-literal to sql backend, evaluate locally instead to null. + op = y.op() + if isinstance(op, ibis.expr.operations.generic.Literal) and op.value == 0: + return ibis_types.null().cast(x.type()) + + bq_mod = x_numeric % y_numeric # Bigquery will maintain x sign here + if is_result_float: + bq_mod = typing.cast(ibis_types.NumericValue, bq_mod.cast(ibis_dtypes.float64)) + + # In BigQuery returned value has the same sign as X. In pandas, the sign of y is used, so we need to flip the result if sign(x) != sign(y) + return ( + ibis.case() + .when( + y_numeric == _ZERO, + _NAN * x_numeric if is_result_float else _ZERO * x_numeric, + ) # Dummy op to propogate nulls and type from x arg + .when( + (y_numeric < _ZERO) & (bq_mod > _ZERO), (y_numeric + bq_mod) + ) # Convert positive result to negative + .when( + (y_numeric > _ZERO) & (bq_mod < _ZERO), (y_numeric + bq_mod) + ) # Convert negative result to positive + .else_(bq_mod) + .end() + ) + + +@scalar_op_compiler.register_binary_op(ops.fillna_op) +def fillna_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return x.fillna(typing.cast(ibis_types.Scalar, y)) + + +@scalar_op_compiler.register_binary_op(ops.round_op) +def round_op(x: ibis_types.Value, y: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).round( + digits=typing.cast(ibis_types.IntegerValue, y) + ) + + +@scalar_op_compiler.register_binary_op(ops.coalesce_op) +def coalesce_impl( + x: ibis_types.Value, + y: ibis_types.Value, +): + if x.name("name").equals(y.name("name")): + return x + else: + return ibis.coalesce(x, y) + + +@scalar_op_compiler.register_binary_op(ops.cliplower_op) +def clip_lower( + value: ibis_types.Value, + lower: ibis_types.Value, +): + return ibis.case().when(lower.isnull() | (value < lower), lower).else_(value).end() + + +@scalar_op_compiler.register_binary_op(ops.clipupper_op) +def clip_upper( + value: ibis_types.Value, + upper: ibis_types.Value, +): + return ibis.case().when(upper.isnull() | (value > upper), upper).else_(value).end() + + +# Ternary Operations +@scalar_op_compiler.register_ternary_op(ops.where_op) +def where_op( + original: ibis_types.Value, + condition: ibis_types.Value, + replacement: ibis_types.Value, +) -> ibis_types.Value: + """Returns x if y is true, otherwise returns z.""" + return ibis.case().when(condition, original).else_(replacement).end() + + +@scalar_op_compiler.register_ternary_op(ops.clip_op) +def clip_op( + original: ibis_types.Value, + lower: ibis_types.Value, + upper: ibis_types.Value, +) -> ibis_types.Value: + """Clips value to lower and upper bounds.""" + if isinstance(lower, ibis_types.NullScalar) and ( + not isinstance(upper, ibis_types.NullScalar) + ): + return ( + ibis.case() + .when(upper.isnull() | (original > upper), upper) + .else_(original) + .end() + ) + elif (not isinstance(lower, ibis_types.NullScalar)) and isinstance( + upper, ibis_types.NullScalar + ): + return ( + ibis.case() + .when(lower.isnull() | (original < lower), lower) + .else_(original) + .end() + ) + elif isinstance(lower, ibis_types.NullScalar) and ( + isinstance(upper, ibis_types.NullScalar) + ): + return original + else: + # Note: Pandas has unchanged behavior when upper bound and lower bound are flipped. This implementation requires that lower_bound < upper_bound + return ( + ibis.case() + .when(lower.isnull() | (original < lower), lower) + .when(upper.isnull() | (original > upper), upper) + .else_(original) + .end() + ) + + +# Composition Ops +@scalar_op_compiler.register_unary_op(ops.ApplyRight, pass_op=True) +def apply_right(input: ibis_types.Value, op: ops.ApplyRight): + right = dtypes.literal_to_ibis_scalar(op.right_scalar, validate=False) + return scalar_op_compiler.compile_row_op(op.base_op, (input, right)) + + +@scalar_op_compiler.register_unary_op(ops.ApplyLeft, pass_op=True) +def apply_left(input: ibis_types.Value, op: ops.ApplyLeft): + left = dtypes.literal_to_ibis_scalar(op.left_scalar, validate=False) + return scalar_op_compiler.compile_row_op(op.base_op, (left, input)) + + +@scalar_op_compiler.register_binary_op(ops.ReverseArgsOp, pass_op=True) +def apply_reversed( + input1: ibis_types.Value, input2: ibis_types.Value, op: ops.ReverseArgsOp +): + return scalar_op_compiler.compile_row_op(op.base_op, (input2, input1)) + + +@scalar_op_compiler.register_binary_op(ops.ApplyArg1, pass_op=True) +def apply_arg1(input1: ibis_types.Value, input2: ibis_types.Value, op: ops.ApplyArg1): + arg1 = dtypes.literal_to_ibis_scalar(op.scalar, validate=False) + return scalar_op_compiler.compile_row_op(op.base_op, (arg1, input1, input2)) + + +@scalar_op_compiler.register_binary_op(ops.ApplyArg3, pass_op=True) +def apply_arg3(input1: ibis_types.Value, input2: ibis_types.Value, op: ops.ApplyArg3): + arg3 = dtypes.literal_to_ibis_scalar(op.scalar, validate=False) + return scalar_op_compiler.compile_row_op(op.base_op, (input1, input2, arg3)) + + +# Helpers +def is_null(value) -> bool: + # float NaN/inf should be treated as distinct from 'true' null values + return typing.cast(bool, pd.isna(value)) and not isinstance(value, float) + + +def _ibis_num(number: float): + return typing.cast(ibis_types.NumericValue, ibis_types.literal(number)) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 6fc284403d2..f148759f619 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -186,7 +186,7 @@ def astype( ) -> Index: if self.nlevels > 1: raise TypeError("Multiindex does not support 'astype'") - return self._apply_unary_op(ops.AsTypeOp(dtype)) + return self._apply_unary_op(ops.AsTypeOp(to_type=dtype)) def all(self) -> bool: if self.nlevels > 1: @@ -278,7 +278,7 @@ def drop( level_id = self._block.index_columns[0] if utils.is_list_like(labels): block, inverse_condition_id = block.apply_unary_op( - level_id, ops.IsInOp(labels, match_nulls=True) + level_id, ops.IsInOp(values=tuple(labels), match_nulls=True) ) block, condition_id = block.apply_unary_op( inverse_condition_id, ops.invert_op @@ -308,9 +308,9 @@ def isin(self, values) -> Index: f"isin(), you passed a [{type(values).__name__}]" ) - return self._apply_unary_op(ops.IsInOp(values, match_nulls=True)).fillna( - value=False - ) + return self._apply_unary_op( + ops.IsInOp(values=tuple(values), match_nulls=True) + ).fillna(value=False) def _apply_unary_op( self, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 62cd7373d07..5385852432f 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -196,38 +196,15 @@ def __hash__(self): @dataclass(frozen=True) -class ProjectUnaryOpNode(UnaryNode): - input_id: str - op: ops.UnaryOp +class ProjectRowOpNode(UnaryNode): + input_ids: typing.Tuple[str, ...] + op: ops.RowOp output_id: Optional[str] = None def __hash__(self): return self._node_hash -@dataclass(frozen=True) -class ProjectBinaryOpNode(UnaryNode): - left_input_id: str - right_input_id: str - op: ops.BinaryOp - output_id: str - - def __hash__(self): - return self._node_hash - - -@dataclass(frozen=True) -class ProjectTernaryOpNode(UnaryNode): - input_id1: str - input_id2: str - input_id3: str - op: ops.TernaryOp - output_id: str - - def __hash__(self): - return self._node_hash - - # TODO: Merge RowCount and Corr into Aggregate Node @dataclass(frozen=True) class RowCountNode(UnaryNode): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index cd3dacd2eeb..1f039904f0d 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -154,7 +154,7 @@ def __init__( block = block.select_columns(list(columns)) # type:ignore if dtype: block = block.multi_apply_unary_op( - block.value_columns, ops.AsTypeOp(dtype) + block.value_columns, ops.AsTypeOp(to_type=dtype) ) self._block = block @@ -325,7 +325,7 @@ def astype( self, dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], ) -> DataFrame: - return self._apply_unary_op(ops.AsTypeOp(dtype)) + return self._apply_unary_op(ops.AsTypeOp(to_type=dtype)) def _to_sql_query( self, include_index: bool @@ -670,7 +670,7 @@ def _apply_binop( def _apply_scalar_binop(self, other: float | int, op: ops.BinaryOp) -> DataFrame: block = self._block - partial_op = ops.BinopPartialRight(op, other) + partial_op = ops.ApplyRight(base_op=op, right_scalar=other) for column_id, label in zip( self._block.value_columns, self._block.column_labels ): @@ -1114,7 +1114,7 @@ def drop( condition_id = typing.cast(str, condition_id) else: block, inverse_condition_id = block.apply_unary_op( - level_id, ops.IsInOp(index, match_nulls=True) + level_id, ops.IsInOp(values=tuple(index), match_nulls=True) ) block, condition_id = block.apply_unary_op( inverse_condition_id, ops.invert_op @@ -1483,16 +1483,16 @@ def _filter_rows( block = self._block block, label_string_id = block.apply_unary_op( self._block.index_columns[0], - ops.AsTypeOp(pandas.StringDtype(storage="pyarrow")), + ops.AsTypeOp(to_type=pandas.StringDtype(storage="pyarrow")), ) if like is not None: block, mask_id = block.apply_unary_op( - label_string_id, ops.ContainsStringOp(pat=like) + label_string_id, ops.StrContainsOp(pat=like) ) else: # regex assert regex is not None block, mask_id = block.apply_unary_op( - label_string_id, ops.ContainsRegexOp(pat=regex) + label_string_id, ops.StrContainsRegexOp(pat=regex) ) block = block.filter(mask_id) @@ -1502,7 +1502,7 @@ def _filter_rows( # Behavior matches pandas 2.1+, older pandas versions would reindex block = self._block block, mask_id = block.apply_unary_op( - self._block.index_columns[0], ops.IsInOp(values=list(items)) + self._block.index_columns[0], ops.IsInOp(values=tuple(items)) ) block = block.filter(mask_id) block = block.select_columns(self._block.value_columns) @@ -1653,7 +1653,9 @@ def isin(self, values) -> DataFrame: if label in values.keys(): value_for_key = values[label] block, result_id = block.apply_unary_op( - col, ops.IsInOp(value_for_key, match_nulls=True), label + col, + ops.IsInOp(values=tuple(value_for_key), match_nulls=True), + label, ) result_ids.append(result_id) else: @@ -1663,9 +1665,9 @@ def isin(self, values) -> DataFrame: result_ids.append(result_id) return DataFrame(block.select_columns(result_ids)).fillna(value=False) elif utils.is_list_like(values): - return self._apply_unary_op(ops.IsInOp(values, match_nulls=True)).fillna( - value=False - ) + return self._apply_unary_op( + ops.IsInOp(values=tuple(values), match_nulls=True) + ).fillna(value=False) else: raise TypeError( "only list-like objects are allowed to be passed to " @@ -2941,7 +2943,7 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: # inputs causing errors. reprojected_df = DataFrame(self._block._force_reproject()) return reprojected_df._apply_unary_op( - ops.RemoteFunctionOp(func, apply_on_null=(na_action is None)) + ops.RemoteFunctionOp(func=func, apply_on_null=(na_action is None)) ) def apply(self, func, *, args: typing.Tuple = (), **kwargs): diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 678774978a1..3ef551e4539 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -14,1083 +14,379 @@ from __future__ import annotations -import functools +import dataclasses import typing -import ibis -import ibis.common.annotations -import ibis.common.exceptions -import ibis.expr.datatypes as ibis_dtypes -import ibis.expr.operations.generic -import ibis.expr.types as ibis_types import numpy as np -import pandas as pd -import bigframes.constants as constants -import bigframes.dtypes import bigframes.dtypes as dtypes -_ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) -_NAN = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.nan)) -_INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.inf)) -_NEG_INF = typing.cast(ibis_types.NumericValue, ibis_types.literal(-np.inf)) -# Approx Highest number you can pass in to EXP function and get a valid FLOAT64 result -# FLOAT64 has 11 exponent bits, so max values is about 2**(2**10) -# ln(2**(2**10)) == (2**10)*ln(2) ~= 709.78, so EXP(x) for x>709.78 will overflow. -_FLOAT64_EXP_BOUND = typing.cast(ibis_types.NumericValue, ibis_types.literal(709.78)) -_INT64_EXP_BOUND = typing.cast(ibis_types.NumericValue, ibis_types.literal(43.6)) +class RowOp(typing.Protocol): + @property + def name(self) -> str: + raise NotImplementedError("RowOp abstract base class has no implementation") -BinaryOp = typing.Callable[[ibis_types.Value, ibis_types.Value], ibis_types.Value] -TernaryOp = typing.Callable[ - [ibis_types.Value, ibis_types.Value, ibis_types.Value], ibis_types.Value -] + @property + def arguments(self) -> int: + """The number of column argument the operation takes""" + raise NotImplementedError("RowOp abstract base class has no implementation") -### Unary Ops +# These classes can be used to create simple ops that don't take local parameters +# All is needed is a unique name, and to register an implementation in ibis_mappings.py +@dataclasses.dataclass(frozen=True) class UnaryOp: - def _as_ibis(self, x): - raise NotImplementedError( - f"Base class UnaryOp has no implementation. {constants.FEEDBACK_LINK}" - ) - @property - def is_windowed(self): - return False - - -# Trig Functions -class AbsOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).abs() - - -class SinOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).sin() - - -class CosOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).cos() - - -class TanOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).tan() - - -# Inverse trig functions -class ArcsinOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - domain = numeric_value.abs() <= _ibis_num(1) - return (~domain).ifelse(_NAN, numeric_value.asin()) - - -class ArccosOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - domain = numeric_value.abs() <= _ibis_num(1) - return (~domain).ifelse(_NAN, numeric_value.acos()) - - -class ArctanOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).atan() - - -# Hyperbolic trig functions -# BQ has these functions, but Ibis doesn't -class SinhOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - sinh_result = ( - numeric_value.exp() - (numeric_value.negate()).exp() - ) / _ibis_num(2) - domain = numeric_value.abs() < _FLOAT64_EXP_BOUND - return (~domain).ifelse(_INF * numeric_value.sign(), sinh_result) - - -class CoshOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - cosh_result = ( - numeric_value.exp() + (numeric_value.negate()).exp() - ) / _ibis_num(2) - domain = numeric_value.abs() < _FLOAT64_EXP_BOUND - return (~domain).ifelse(_INF, cosh_result) - - -class TanhOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - tanh_result = (numeric_value.exp() - (numeric_value.negate()).exp()) / ( - numeric_value.exp() + (numeric_value.negate()).exp() - ) - # Beyond +-20, is effectively just the sign function - domain = numeric_value.abs() < _ibis_num(20) - return (~domain).ifelse(numeric_value.sign(), tanh_result) - - -class ArcsinhOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - sqrt_part = ((numeric_value * numeric_value) + _ibis_num(1)).sqrt() - return (numeric_value.abs() + sqrt_part).ln() * numeric_value.sign() - - -class ArccoshOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - sqrt_part = ((numeric_value * numeric_value) - _ibis_num(1)).sqrt() - acosh_result = (numeric_value + sqrt_part).ln() - domain = numeric_value >= _ibis_num(1) - return (~domain).ifelse(_NAN, acosh_result) - - -class ArctanhOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - domain = numeric_value.abs() < _ibis_num(1) - numerator = numeric_value + _ibis_num(1) - denominator = _ibis_num(1) - numeric_value - ln_input = typing.cast(ibis_types.NumericValue, numerator.div(denominator)) - atanh_result = ln_input.ln().div(2) - - out_of_domain = (numeric_value.abs() == _ibis_num(1)).ifelse( - _INF * numeric_value, _NAN - ) - - return (~domain).ifelse(out_of_domain, atanh_result) - - -class SqrtOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - domain = numeric_value >= _ZERO - return (~domain).ifelse(_NAN, numeric_value.sqrt()) - - -class Log10Op(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - domain = numeric_value > _ZERO - out_of_domain = (numeric_value == _ZERO).ifelse(_NEG_INF, _NAN) - return (~domain).ifelse(out_of_domain, numeric_value.log10()) - - -class LnOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - domain = numeric_value > _ZERO - out_of_domain = (numeric_value == _ZERO).ifelse(_NEG_INF, _NAN) - return (~domain).ifelse(out_of_domain, numeric_value.ln()) - - -class ExpOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - numeric_value = typing.cast(ibis_types.NumericValue, x) - domain = numeric_value < _FLOAT64_EXP_BOUND - return (~domain).ifelse(_INF, numeric_value.exp()) - - -class InvertOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).negate() - - -class IsNullOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return x.isnull() - - -class LenOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).length().cast(ibis_dtypes.int64) - - -class NotNullOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return x.notnull() - - -class HashOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.IntegerValue, x).hash() - - -## String Operation -class ReverseOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).reverse() - - -class LowerOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).lower() - - -class UpperOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).upper() - - -class StripOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).strip() - - -class IsNumericOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - # catches all members of the Unicode number class, which matches pandas isnumeric - # see https://cloud.google.com/bigquery/docs/reference/standard-sql/string_functions#regexp_contains - # TODO: Validate correctness, my miss eg ⅕ character - return typing.cast(ibis_types.StringValue, x).re_search(r"^(\pN+)$") - - -class IsAlphaOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).re_search( - r"^(\p{Lm}|\p{Lt}|\p{Lu}|\p{Ll}|\p{Lo})+$" - ) - - -class IsDigitOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - # Based on docs, should include superscript/subscript-ed numbers - # Tests however pass only when set to Nd unicode class - return typing.cast(ibis_types.StringValue, x).re_search(r"^(\p{Nd})+$") - - -class IsDecimalOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).re_search(r"^(\p{Nd})+$") - - -class IsAlnumOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).re_search( - r"^(\p{N}|\p{Lm}|\p{Lt}|\p{Lu}|\p{Ll}|\p{Lo})+$" - ) - - -class IsSpaceOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - # All characters are whitespace characters, False for empty string - return typing.cast(ibis_types.StringValue, x).re_search(r"^\s+$") - - -class IsLowerOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - # No upper case characters, min one cased character - # See: https://docs.python.org/3/library/stdtypes.html#str - return typing.cast(ibis_types.StringValue, x).re_search( - r"\p{Ll}" - ) & ~typing.cast(ibis_types.StringValue, x).re_search(r"\p{Lu}|\p{Lt}") - - -class IsUpperOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - # No lower case characters, min one cased character - # See: https://docs.python.org/3/library/stdtypes.html#str - return typing.cast(ibis_types.StringValue, x).re_search( - r"\p{Lu}" - ) & ~typing.cast(ibis_types.StringValue, x).re_search(r"\p{Ll}|\p{Lt}") - - -class RstripOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).rstrip() - - -class LstripOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).lstrip() + def name(self) -> str: + raise NotImplementedError("RowOp abstract base class has no implementation") + @property + def arguments(self) -> int: + return 1 -class CapitalizeOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).capitalize() +@dataclasses.dataclass(frozen=True) +class BinaryOp: + @property + def name(self) -> str: + raise NotImplementedError("RowOp abstract base class has no implementation") -class ContainsStringOp(UnaryOp): - def __init__(self, pat: str, case: bool = True): - self._pat = pat + @property + def arguments(self) -> int: + return 2 - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).contains(self._pat) +@dataclasses.dataclass(frozen=True) +class TernaryOp: + @property + def name(self) -> str: + raise NotImplementedError("RowOp abstract base class has no implementation") -class ContainsRegexOp(UnaryOp): - def __init__(self, pat: str): - self._pat = pat + @property + def arguments(self) -> int: + return 3 + + +# Operation Factories +def create_unary_op(name: str) -> UnaryOp: + return dataclasses.make_dataclass( + name, + [("name", typing.ClassVar[str], name)], # type: ignore + bases=(UnaryOp,), + frozen=True, + )() + + +def create_binary_op(name: str) -> BinaryOp: + return dataclasses.make_dataclass( + name, + [("name", typing.ClassVar[str], name)], # type: ignore + bases=(BinaryOp,), + frozen=True, + )() + + +def create_ternary_op(name: str) -> TernaryOp: + return dataclasses.make_dataclass( + name, + [("name", typing.ClassVar[str], name)], # type: ignore + bases=(TernaryOp,), + frozen=True, + )() + + +# Unary Ops +## Generic Ops +invert_op = create_unary_op(name="invert") +isnull_op = create_unary_op(name="isnull") +notnull_op = create_unary_op(name="notnull") +hash_op = create_unary_op(name="hash") +## String Ops +len_op = create_unary_op(name="len") +reverse_op = create_unary_op(name="reverse") +lower_op = create_unary_op(name="lower") +upper_op = create_unary_op(name="upper") +strip_op = create_unary_op(name="strip") +isalnum_op = create_unary_op(name="isalnum") +isalpha_op = create_unary_op(name="isalpha") +isdecimal_op = create_unary_op(name="isdecimal") +isdigit_op = create_unary_op(name="isdigit") +isnumeric_op = create_unary_op(name="isnumeric") +isspace_op = create_unary_op(name="isspace") +islower_op = create_unary_op(name="islower") +isupper_op = create_unary_op(name="isupper") +rstrip_op = create_unary_op(name="rstrip") +lstrip_op = create_unary_op(name="lstrip") +capitalize_op = create_unary_op(name="capitalize") +## DateTime Ops +day_op = create_unary_op(name="day") +dayofweek_op = create_unary_op(name="dayofweek") +date_op = create_unary_op(name="date") +hour_op = create_unary_op(name="hour") +minute_op = create_unary_op(name="minute") +month_op = create_unary_op(name="month") +quarter_op = create_unary_op(name="quarter") +second_op = create_unary_op(name="second") +time_op = create_unary_op(name="time") +year_op = create_unary_op(name="year") +## Trigonometry Ops +sin_op = create_unary_op(name="sin") +cos_op = create_unary_op(name="cos") +tan_op = create_unary_op(name="tan") +arcsin_op = create_unary_op(name="arcsin") +arccos_op = create_unary_op(name="arccos") +arctan_op = create_unary_op(name="arctan") +sinh_op = create_unary_op(name="sinh") +cosh_op = create_unary_op(name="cosh") +tanh_op = create_unary_op(name="tanh") +arcsinh_op = create_unary_op(name="arcsinh") +arccosh_op = create_unary_op(name="arccosh") +arctanh_op = create_unary_op(name="arctanh") +## Numeric Ops +abs_op = create_unary_op(name="abs") +exp_op = create_unary_op(name="exp") +ln_op = create_unary_op(name="log") +log10_op = create_unary_op(name="log10") +sqrt_op = create_unary_op(name="sqrt") + + +# Parameterized unary ops +@dataclasses.dataclass(frozen=True) +class StrContainsOp(UnaryOp): + name: typing.ClassVar[str] = "str_contains" + pat: str + + +@dataclasses.dataclass(frozen=True) +class StrContainsRegexOp(UnaryOp): + name: typing.ClassVar[str] = "str_contains_regex" + pat: str + + +@dataclasses.dataclass(frozen=True) +class StrGetOp(UnaryOp): + name: typing.ClassVar[str] = "str_get" + i: int - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).re_search(self._pat) +@dataclasses.dataclass(frozen=True) +class StrPadOp(UnaryOp): + name: typing.ClassVar[str] = "str_pad" + length: int + fillchar: str + side: typing.Literal["both", "left", "right"] -class StrGetOp(UnaryOp): - def __init__(self, i: int): - self._i = i - def _as_ibis(self, x: ibis_types.Value): - substr = typing.cast( - ibis_types.StringValue, typing.cast(ibis_types.StringValue, x)[self._i] - ) - return substr.nullif(ibis_types.literal("")) +@dataclasses.dataclass(frozen=True) +class ReplaceStrOp(UnaryOp): + name: typing.ClassVar[str] = "str_replace" + pat: str + repl: str -class StrPadOp(UnaryOp): - def __init__( - self, length: int, fillchar: str, side: typing.Literal["both", "left", "right"] - ): - self._length = length - self._fillchar = fillchar - self._side = side - - def _as_ibis(self, x: ibis_types.Value): - str_val = typing.cast(ibis_types.StringValue, x) - - # SQL pad operations will truncate, we do not want to truncate though. - pad_length = typing.cast( - ibis_types.IntegerValue, ibis.greatest(str_val.length(), self._length) - ) - if self._side == "left": - return str_val.lpad(pad_length, self._fillchar) - elif self._side == "right": - return str_val.rpad(pad_length, self._fillchar) - else: # side == both - # Pad more on right side if can't pad both sides equally - lpad_amount = typing.cast( - ibis_types.IntegerValue, - ( - (pad_length - str_val.length()) - // typing.cast(ibis_types.NumericValue, ibis.literal(2)) - ) - + str_val.length(), - ) - return str_val.lpad(lpad_amount, self._fillchar).rpad( - pad_length, self._fillchar - ) - - -class ReplaceStringOp(UnaryOp): - def __init__(self, pat: str, repl: str): - self._pat = pat - self._repl = repl - - def _as_ibis(self, x: ibis_types.Value): - pat_str_value = typing.cast( - ibis_types.StringValue, ibis_types.literal(self._pat) - ) - repl_str_value = typing.cast( - ibis_types.StringValue, ibis_types.literal(self._repl) - ) - - return typing.cast(ibis_types.StringValue, x).replace( - pat_str_value, repl_str_value - ) - - -class ReplaceRegexOp(UnaryOp): - def __init__(self, pat: str, repl: str): - self._pat = pat - self._repl = repl - - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).re_replace(self._pat, self._repl) +@dataclasses.dataclass(frozen=True) +class RegexReplaceStrOp(UnaryOp): + name: typing.ClassVar[str] = "str_rereplace" + pat: str + repl: str +@dataclasses.dataclass(frozen=True) class StartsWithOp(UnaryOp): - def __init__(self, pat: typing.Sequence[str]): - self._pat = pat - - def _as_ibis(self, x: ibis_types.Value): - any_match = None - for pat in self._pat: - pat_match = typing.cast(ibis_types.StringValue, x).startswith(pat) - if any_match is not None: - any_match = any_match | pat_match - else: - any_match = pat_match - return any_match if any_match is not None else ibis_types.literal(False) + name: typing.ClassVar[str] = "str_startswith" + pat: typing.Sequence[str] +@dataclasses.dataclass(frozen=True) class EndsWithOp(UnaryOp): - def __init__(self, pat: typing.Sequence[str]): - self._pat = pat - - def _as_ibis(self, x: ibis_types.Value): - any_match = None - for pat in self._pat: - pat_match = typing.cast(ibis_types.StringValue, x).endswith(pat) - if any_match is not None: - any_match = any_match | pat_match - else: - any_match = pat_match - return any_match if any_match is not None else ibis_types.literal(False) + name: typing.ClassVar[str] = "str_endswith" + pat: typing.Sequence[str] +@dataclasses.dataclass(frozen=True) class ZfillOp(UnaryOp): - def __init__(self, width: int): - self._width = width - - def _as_ibis(self, x: ibis_types.Value): - str_value = typing.cast(ibis_types.StringValue, x) - return ( - ibis.case() - .when( - str_value[0] == "-", - "-" - + StrPadOp(self._width - 1, "0", "left")._as_ibis(str_value.substr(1)), - ) - .else_(StrPadOp(self._width, "0", "left")._as_ibis(str_value)) - .end() - ) - - -## Datetime Ops -class DayOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).day().cast(ibis_dtypes.int64) - - -class DateOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).date() - - -class DayofweekOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return ( - typing.cast(ibis_types.TimestampValue, x) - .day_of_week.index() - .cast(ibis_dtypes.int64) - ) + name: typing.ClassVar[str] = "str_zfill" + width: int -class HourOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).hour().cast(ibis_dtypes.int64) +@dataclasses.dataclass(frozen=True) +class StrFindOp(UnaryOp): + name: typing.ClassVar[str] = "str_find" + substr: str + start: typing.Optional[int] + end: typing.Optional[int] -class MinuteOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return ( - typing.cast(ibis_types.TimestampValue, x).minute().cast(ibis_dtypes.int64) - ) +@dataclasses.dataclass(frozen=True) +class StrExtractOp(UnaryOp): + name: typing.ClassVar[str] = "str_extract" + pat: str + n: int = 1 -class MonthOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).month().cast(ibis_dtypes.int64) +@dataclasses.dataclass(frozen=True) +class StrSliceOp(UnaryOp): + name: typing.ClassVar[str] = "str_slice" + start: typing.Optional[int] + end: typing.Optional[int] -class QuarterOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return ( - typing.cast(ibis_types.TimestampValue, x).quarter().cast(ibis_dtypes.int64) - ) +@dataclasses.dataclass(frozen=True) +class StrRepeatOp(UnaryOp): + name: typing.ClassVar[str] = "str_repeat" + repeats: int -class SecondOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return ( - typing.cast(ibis_types.TimestampValue, x).second().cast(ibis_dtypes.int64) - ) +# Other parameterized unary operations +@dataclasses.dataclass(frozen=True) +class StructFieldOp(UnaryOp): + name: typing.ClassVar[str] = "struct_field" + name_or_index: str | int -class TimeOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).time() - - -class YearOp(UnaryOp): - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.TimestampValue, x).year().cast(ibis_dtypes.int64) - - -# Parameterized ops +@dataclasses.dataclass(frozen=True) class AsTypeOp(UnaryOp): - def __init__(self, to_type: dtypes.DtypeString | dtypes.Dtype): - self.to_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(to_type) + name: typing.ClassVar[str] = "astype" + to_type: dtypes.DtypeString | dtypes.Dtype - def _as_ibis(self, x: ibis_types.Value): - if isinstance(x, ibis_types.NullScalar): - return ibis_types.null().cast(self.to_type) - return bigframes.dtypes.cast_ibis_value(x, self.to_type) - - -class MapOp(UnaryOp): - def __init__( - self, - mappings: typing.Tuple[typing.Tuple[typing.Hashable, typing.Hashable], ...], - ): - self._mappings = mappings +@dataclasses.dataclass(frozen=True) +class IsInOp(UnaryOp): + name: typing.ClassVar[str] = "is_in" + values: typing.Tuple + match_nulls: bool = True - def _as_ibis(self, x: ibis_types.Value): - case = ibis.case() - for mapping in self._mappings: - case = case.when(x == mapping[0], mapping[1]) - return case.else_(x).end() +@dataclasses.dataclass(frozen=True) +class RemoteFunctionOp(UnaryOp): + name: typing.ClassVar[str] = "remote_function" + func: typing.Callable + apply_on_null: bool -class FindOp(UnaryOp): - def __init__(self, sub, start, end): - self._sub = sub - self._start = start - self._end = end - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).find( - self._sub, self._start, self._end - ) +@dataclasses.dataclass(frozen=True) +class MapOp(UnaryOp): + name = "map_values" + mappings: typing.Tuple[typing.Tuple[typing.Hashable, typing.Hashable], ...] -class ExtractOp(UnaryOp): - def __init__(self, pat: str, n: int = 1): - self._pat = pat - self._n = n +# Operation Composition +# Meta-ops that do partial application or parameter remapping +# Subject to change, may convert to explicit tree +@dataclasses.dataclass(frozen=True) +class ApplyRight(UnaryOp): + name: typing.ClassVar[str] = "apply_right" + base_op: BinaryOp + right_scalar: typing.Any - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).re_extract(self._pat, self._n) +@dataclasses.dataclass(frozen=True) +class ApplyLeft(UnaryOp): + name: typing.ClassVar[str] = "apply_left" + base_op: BinaryOp + left_scalar: typing.Any -class SliceOp(UnaryOp): - def __init__(self, start, stop): - self._start = start - self._stop = stop - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x)[self._start : self._stop] +@dataclasses.dataclass(frozen=True) +class ApplyArg1(BinaryOp): + name: typing.ClassVar[str] = "apply_arg1" + base_op: TernaryOp + scalar: typing.Any -class IsInOp(UnaryOp): - def __init__(self, values, match_nulls: bool = True): - self._values = values - self._match_nulls = match_nulls +@dataclasses.dataclass(frozen=True) +class ApplyArg3(BinaryOp): + name: typing.ClassVar[str] = "apply_arg3" + base_op: TernaryOp + scalar: typing.Any - def _as_ibis(self, x: ibis_types.Value): - contains_nulls = any(is_null(value) for value in self._values) - matchable_ibis_values = [] - for item in self._values: - if not is_null(item): - try: - # we want values that *could* be cast to the dtype, but we don't want - # to actually cast it, as that could be lossy (eg float -> int) - item_inferred_type = ibis.literal(item).type() - if ( - x.type() == item_inferred_type - or x.type().is_numeric() - and item_inferred_type.is_numeric() - ): - matchable_ibis_values.append(item) - except TypeError: - pass - if self._match_nulls and contains_nulls: - return x.isnull() | x.isin(matchable_ibis_values) - else: - return x.isin(matchable_ibis_values) +@dataclasses.dataclass(frozen=True) +class ReverseArgsOp(BinaryOp): + name: typing.ClassVar[str] = "apply_reverse" + base_op: BinaryOp -class BinopPartialRight(UnaryOp): - def __init__(self, binop: BinaryOp, right_scalar: typing.Any): - self._binop = binop - self._right = dtypes.literal_to_ibis_scalar(right_scalar, validate=False) +def partial_left(op: BinaryOp, scalar: typing.Any) -> UnaryOp: + return ApplyLeft(base_op=op, left_scalar=scalar) - def _as_ibis(self, x): - return self._binop(x, self._right) +def partial_right(op: BinaryOp, scalar: typing.Any) -> UnaryOp: + return ApplyRight(base_op=op, right_scalar=scalar) -class BinopPartialLeft(UnaryOp): - def __init__(self, binop: BinaryOp, left_scalar: typing.Any): - self._binop = binop - self._left = dtypes.literal_to_ibis_scalar(left_scalar, validate=False) - def _as_ibis(self, x): - return self._binop(self._left, x) +def partial_arg1(op: TernaryOp, scalar: typing.Any) -> BinaryOp: + return ApplyArg1(base_op=op, scalar=scalar) -class RepeatOp(UnaryOp): - def __init__(self, repeats): - self._repeats = repeats +def partial_arg3(op: TernaryOp, scalar: typing.Any) -> BinaryOp: + return ApplyArg3(base_op=op, scalar=scalar) - def _as_ibis(self, x: ibis_types.Value): - return typing.cast(ibis_types.StringValue, x).repeat(self._repeats) +def reverse(op: BinaryOp) -> BinaryOp: + return ReverseArgsOp(base_op=op) + + +# Binary Ops +fillna_op = create_binary_op(name="fillna") +cliplower_op = create_binary_op(name="clip_lower") +clipupper_op = create_binary_op(name="clip_upper") +coalesce_op = create_binary_op(name="coalesce") +## Math Ops +add_op = create_binary_op(name="add") +sub_op = create_binary_op(name="sub") +mul_op = create_binary_op(name="mul") +div_op = create_binary_op(name="div") +floordiv_op = create_binary_op(name="floordiv") +pow_op = create_binary_op(name="pow") +mod_op = create_binary_op(name="mod") +round_op = create_binary_op(name="round") +unsafe_pow_op = create_binary_op(name="unsafe_pow_op") +# Logical Ops +and_op = create_binary_op(name="and") +or_op = create_binary_op(name="or") + +## Comparison Ops +eq_op = create_binary_op(name="eq") +eq_null_match_op = create_binary_op(name="eq_nulls_match") +ne_op = create_binary_op(name="ne") +lt_op = create_binary_op(name="lt") +gt_op = create_binary_op(name="gt") +le_op = create_binary_op(name="le") +ge_op = create_binary_op(name="ge") + +## String Ops +strconcat_op = create_binary_op(name="strconcat") + +# Ternary Ops +where_op = create_ternary_op(name="where") +clip_op = create_ternary_op(name="clip") -class RemoteFunctionOp(UnaryOp): - def __init__(self, func: typing.Callable, apply_on_null=True): - if not hasattr(func, "bigframes_remote_function"): - raise TypeError( - f"only a bigframes remote function is supported as a callable. {constants.FEEDBACK_LINK}" - ) - - self._func = func - self._apply_on_null = apply_on_null - - def _as_ibis(self, x: ibis_types.Value): - x_transformed = self._func(x) - if not self._apply_on_null: - x_transformed = where_op(x, x.isnull(), x_transformed) - return x_transformed - - -abs_op = AbsOp() -invert_op = InvertOp() -isnull_op = IsNullOp() -len_op = LenOp() -notnull_op = NotNullOp() -reverse_op = ReverseOp() -lower_op = LowerOp() -upper_op = UpperOp() -strip_op = StripOp() -isalnum_op = IsAlnumOp() -isalpha_op = IsAlphaOp() -isdecimal_op = IsDecimalOp() -isdigit_op = IsDigitOp() -isnumeric_op = IsNumericOp() -isspace_op = IsSpaceOp() -islower_op = IsLowerOp() -isupper_op = IsUpperOp() -rstrip_op = RstripOp() -lstrip_op = LstripOp() -hash_op = HashOp() -day_op = DayOp() -dayofweek_op = DayofweekOp() -date_op = DateOp() -hour_op = HourOp() -minute_op = MinuteOp() -month_op = MonthOp() -quarter_op = QuarterOp() -second_op = SecondOp() -time_op = TimeOp() -year_op = YearOp() -capitalize_op = CapitalizeOp() # Just parameterless unary ops for now # TODO: Parameter mappings NUMPY_TO_OP: typing.Final = { - np.sin: SinOp(), - np.cos: CosOp(), - np.tan: TanOp(), - np.arcsin: ArcsinOp(), - np.arccos: ArccosOp(), - np.arctan: ArctanOp(), - np.sinh: SinhOp(), - np.cosh: CoshOp(), - np.tanh: TanhOp(), - np.arcsinh: ArcsinhOp(), - np.arccosh: ArccoshOp(), - np.arctanh: ArctanhOp(), - np.exp: ExpOp(), - np.log: LnOp(), - np.log10: Log10Op(), - np.sqrt: SqrtOp(), - np.abs: AbsOp(), + np.sin: sin_op, + np.cos: cos_op, + np.tan: tan_op, + np.arcsin: arcsin_op, + np.arccos: arccos_op, + np.arctan: arctan_op, + np.sinh: sinh_op, + np.cosh: cosh_op, + np.tanh: tanh_op, + np.arcsinh: arcsinh_op, + np.arccosh: arccosh_op, + np.arctanh: arctanh_op, + np.exp: exp_op, + np.log: ln_op, + np.log10: log10_op, + np.sqrt: sqrt_op, + np.abs: abs_op, } -### Binary Ops -def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): - """Wraps a binary operator to generate nulls of the expected type if either input is a null scalar.""" - - def short_circuit_nulls_inner(binop): - @functools.wraps(binop) - def wrapped_binop(x: ibis_types.Value, y: ibis_types.Value): - if isinstance(x, ibis_types.NullScalar): - return ibis_types.null().cast(type_override or y.type()) - elif isinstance(y, ibis_types.NullScalar): - return ibis_types.null().cast(type_override or x.type()) - else: - return binop(x, y) - - return wrapped_binop - - return short_circuit_nulls_inner - - -def concat_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - x_string = typing.cast(ibis_types.StringValue, x) - y_string = typing.cast(ibis_types.StringValue, y) - return x_string.concat(y_string) - - -def eq_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x == y - - -def eq_nulls_match_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - """Variant of eq_op where nulls match each other. Only use where dtypes are known to be same.""" - left = x.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) - right = y.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) - return left == right - - -def ne_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x != y - - -def _null_or_value(value: ibis_types.Value, where_value: ibis_types.BooleanValue): - return ibis.where( - where_value, - value, - ibis.null(), - ) - - -def and_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - # Workaround issue https://github.com/ibis-project/ibis/issues/7775 by - # implementing three-valued logic ourselves. For AND, when we encounter a - # NULL value, we only know when the result is FALSE, otherwise the result - # is unknown (NULL). See: truth table at - # https://en.wikibooks.org/wiki/Structured_Query_Language/NULLs_and_the_Three_Valued_Logic#AND,_OR - if isinstance(x, ibis_types.NullScalar): - return _null_or_value(y, y == ibis.literal(False)) - - if isinstance(y, ibis_types.NullScalar): - return _null_or_value(x, x == ibis.literal(False)) - - return typing.cast(ibis_types.BooleanValue, x) & typing.cast( - ibis_types.BooleanValue, y - ) - - -def or_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - # Workaround issue https://github.com/ibis-project/ibis/issues/7775 by - # implementing three-valued logic ourselves. For OR, when we encounter a - # NULL value, we only know when the result is TRUE, otherwise the result - # is unknown (NULL). See: truth table at - # https://en.wikibooks.org/wiki/Structured_Query_Language/NULLs_and_the_Three_Valued_Logic#AND,_OR - if isinstance(x, ibis_types.NullScalar): - return _null_or_value(y, y == ibis.literal(True)) - - if isinstance(y, ibis_types.NullScalar): - return _null_or_value(x, x == ibis.literal(True)) - - return typing.cast(ibis_types.BooleanValue, x) | typing.cast( - ibis_types.BooleanValue, y - ) - - -@short_circuit_nulls() -def add_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - if isinstance(x, ibis_types.NullScalar) or isinstance(x, ibis_types.NullScalar): - return ibis.null() - try: - # Could be string concatenation or numeric addition. - return x + y # type: ignore - except ibis.common.annotations.SignatureValidationError as exc: - left_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(x.type()) - right_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(y.type()) - raise TypeError( - f"Cannot add {repr(left_type)} and {repr(right_type)}. {constants.FEEDBACK_LINK}" - ) from exc - - -@short_circuit_nulls() -def sub_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return typing.cast(ibis_types.NumericValue, x) - typing.cast( - ibis_types.NumericValue, y - ) - - -@short_circuit_nulls() -def mul_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return typing.cast(ibis_types.NumericValue, x) * typing.cast( - ibis_types.NumericValue, y - ) - - -@short_circuit_nulls(ibis_dtypes.float) -def div_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return typing.cast(ibis_types.NumericValue, x) / typing.cast( - ibis_types.NumericValue, y - ) - - -@short_circuit_nulls(ibis_dtypes.float) -def pow_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - if x.type().is_integer() and y.type().is_integer(): - return _int_pow_op(x, y) - else: - return _float_pow_op(x, y) - - -@short_circuit_nulls(ibis_dtypes.float) -def unsafe_pow_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - """For internal use only - where domain and overflow checks are not needed.""" - return typing.cast(ibis_types.NumericValue, x) ** typing.cast( - ibis_types.NumericValue, y - ) - - -def _int_pow_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - # Need to avoid any error cases - should produce NaN instead - # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions#pow - x_as_decimal = typing.cast( - ibis_types.NumericValue, - x.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)), - ) - y_val = typing.cast(ibis_types.NumericValue, y) - - # BQ POW() function outputs FLOAT64, which can lose precision. - # Therefore, we do math in NUMERIC and cast back down after. - # Also, explicit bounds checks, pandas will silently overflow. - pow_result = x_as_decimal**y_val - overflow_cond = (pow_result > _ibis_num((2**63) - 1)) | ( - pow_result < _ibis_num(-(2**63)) - ) - - return ( - ibis.case() - .when((overflow_cond), ibis.null()) - .else_(pow_result.cast(ibis_dtypes.int64)) - .end() - ) - - -def _float_pow_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - # Most conditions here seek to prevent calling BQ POW with inputs that would generate errors. - # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions#pow - x_val = typing.cast(ibis_types.NumericValue, x) - y_val = typing.cast(ibis_types.NumericValue, y) - - overflow_cond = (x_val != _ZERO) & ((y_val * x_val.abs().ln()) > _FLOAT64_EXP_BOUND) - - # Float64 lose integer precision beyond 2**53, beyond this insufficient precision to get parity - exp_too_big = y_val.abs() > _ibis_num(2**53) - # Treat very large exponents as +=INF - norm_exp = exp_too_big.ifelse(_INF * y_val.sign(), y_val) - - pow_result = x_val**norm_exp - - # This cast is dangerous, need to only excuted where y_val has been bounds-checked - # Ibis needs try_cast binding to bq safe_cast - exponent_is_whole = y_val.cast(ibis_dtypes.int64) == y_val - odd_exponent = (x_val < _ZERO) & ( - y_val.cast(ibis_dtypes.int64) % _ibis_num(2) == _ibis_num(1) - ) - infinite_base = x_val.abs() == _INF - - return ( - ibis.case() - # Might be able to do something more clever with x_val==0 case - .when(y_val == _ZERO, _ibis_num(1)) - .when( - x_val == _ibis_num(1), _ibis_num(1) - ) # Need to ignore exponent, even if it is NA - .when( - (x_val == _ZERO) & (y_val < _ZERO), _INF - ) # This case would error POW function in BQ - .when(infinite_base, pow_result) - .when( - exp_too_big, pow_result - ) # Bigquery can actually handle the +-inf cases gracefully - .when((x_val < _ZERO) & (~exponent_is_whole), _NAN) - .when( - overflow_cond, _INF * odd_exponent.ifelse(_ibis_num(-1), _ibis_num(1)) - ) # finite overflows would cause bq to error - .else_(pow_result) - .end() - ) - - -@short_circuit_nulls(ibis_dtypes.bool) -def lt_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x < y - - -@short_circuit_nulls(ibis_dtypes.bool) -def le_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x <= y - - -@short_circuit_nulls(ibis_dtypes.bool) -def gt_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x > y - - -@short_circuit_nulls(ibis_dtypes.bool) -def ge_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x >= y - - -def coalesce_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - if x.name("name").equals(y.name("name")): - return x - else: - return ibis.coalesce(x, y) - - -@short_circuit_nulls(ibis_dtypes.int) -def floordiv_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - x_numeric = typing.cast(ibis_types.NumericValue, x) - y_numeric = typing.cast(ibis_types.NumericValue, y) - floordiv_expr = x_numeric // y_numeric - - # DIV(N, 0) will error in bigquery, but needs to return 0 for int, and inf for float in BQ so we short-circuit in this case. - # Multiplying left by zero propogates nulls. - zero_result = _INF if (x.type().is_floating() or y.type().is_floating()) else _ZERO - return ( - ibis.case() - .when(y_numeric == _ZERO, zero_result * x_numeric) - .else_(floordiv_expr) - .end() - ) - - -def _is_float(x: ibis_types.Value): - return isinstance(x, (ibis_types.FloatingColumn, ibis_types.FloatingScalar)) - - -@short_circuit_nulls() -def mod_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - is_result_float = _is_float(x) | _is_float(y) - x_numeric = typing.cast( - ibis_types.NumericValue, - x.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) - if is_result_float - else x, - ) - y_numeric = typing.cast( - ibis_types.NumericValue, - y.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) - if is_result_float - else y, - ) - # Hacky short-circuit to avoid passing zero-literal to sql backend, evaluate locally instead to null. - op = y.op() - if isinstance(op, ibis.expr.operations.generic.Literal) and op.value == 0: - return ibis_types.null().cast(x.type()) - - bq_mod = x_numeric % y_numeric # Bigquery will maintain x sign here - if is_result_float: - bq_mod = typing.cast(ibis_types.NumericValue, bq_mod.cast(ibis_dtypes.float64)) - - # In BigQuery returned value has the same sign as X. In pandas, the sign of y is used, so we need to flip the result if sign(x) != sign(y) - return ( - ibis.case() - .when( - y_numeric == _ZERO, - _NAN * x_numeric if is_result_float else _ZERO * x_numeric, - ) # Dummy op to propogate nulls and type from x arg - .when( - (y_numeric < _ZERO) & (bq_mod > _ZERO), (y_numeric + bq_mod) - ) # Convert positive result to negative - .when( - (y_numeric > _ZERO) & (bq_mod < _ZERO), (y_numeric + bq_mod) - ) # Convert negative result to positive - .else_(bq_mod) - .end() - ) - - -def fillna_op( - x: ibis_types.Value, - y: ibis_types.Value, -): - return x.fillna(typing.cast(ibis_types.Scalar, y)) - - -def round_op(x: ibis_types.Value, y: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).round( - digits=typing.cast(ibis_types.IntegerValue, y) - ) - - -def clip_lower( - value: ibis_types.Value, - lower: ibis_types.Value, -): - return ibis.case().when(lower.isnull() | (value < lower), lower).else_(value).end() - - -def clip_upper( - value: ibis_types.Value, - upper: ibis_types.Value, -): - return ibis.case().when(upper.isnull() | (value > upper), upper).else_(value).end() - - -def reverse(op: BinaryOp) -> BinaryOp: - return lambda x, y: op(y, x) - - -def partial_left(op: BinaryOp, scalar: typing.Any) -> UnaryOp: - return BinopPartialLeft(op, scalar) - - -def partial_right(op: BinaryOp, scalar: typing.Any) -> UnaryOp: - return BinopPartialRight(op, scalar) - - NUMPY_TO_BINOP: typing.Final = { np.add: add_op, np.subtract: sub_op, @@ -1098,75 +394,3 @@ def partial_right(op: BinaryOp, scalar: typing.Any) -> UnaryOp: np.divide: div_op, np.power: pow_op, } - - -# Ternary ops -def where_op( - original: ibis_types.Value, - condition: ibis_types.Value, - replacement: ibis_types.Value, -) -> ibis_types.Value: - """Returns x if y is true, otherwise returns z.""" - return ibis.case().when(condition, original).else_(replacement).end() # type: ignore - - -def clip_op( - original: ibis_types.Value, - lower: ibis_types.Value, - upper: ibis_types.Value, -) -> ibis_types.Value: - """Clips value to lower and upper bounds.""" - if isinstance(lower, ibis_types.NullScalar) and ( - not isinstance(upper, ibis_types.NullScalar) - ): - return ( - ibis.case() # type: ignore - .when(upper.isnull() | (original > upper), upper) - .else_(original) - .end() - ) - elif (not isinstance(lower, ibis_types.NullScalar)) and isinstance( - upper, ibis_types.NullScalar - ): - return ( - ibis.case() # type: ignore - .when(lower.isnull() | (original < lower), lower) - .else_(original) - .end() - ) - elif isinstance(lower, ibis_types.NullScalar) and ( - isinstance(upper, ibis_types.NullScalar) - ): - return original - else: - # Note: Pandas has unchanged behavior when upper bound and lower bound - # are flipped. - # This implementation requires that lower_bound < upper_bound. - return ( - ibis.case() # type: ignore - .when(lower.isnull() | (original < lower), lower) - .when(upper.isnull() | (original > upper), upper) - .else_(original) - .end() - ) - - -def partial_arg1(op: TernaryOp, scalar: typing.Any) -> BinaryOp: - return lambda x, y: op(dtypes.literal_to_ibis_scalar(scalar, validate=False), x, y) - - -def partial_arg2(op: TernaryOp, scalar: typing.Any) -> BinaryOp: - return lambda x, y: op(x, dtypes.literal_to_ibis_scalar(scalar, validate=False), y) - - -def partial_arg3(op: TernaryOp, scalar: typing.Any) -> BinaryOp: - return lambda x, y: op(x, y, dtypes.literal_to_ibis_scalar(scalar, validate=False)) - - -def is_null(value) -> bool: - # float NaN/inf should be treated as distinct from 'true' null values - return typing.cast(bool, pd.isna(value)) and not isinstance(value, float) - - -def _ibis_num(number: float): - return typing.cast(ibis_types.NumericValue, ibis_types.literal(number)) diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 85ce1dd9e6d..8989255f7ed 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -71,7 +71,7 @@ def __init__( ) if dtype: block = block.multi_apply_unary_op( - block.value_columns, ops.AsTypeOp(dtype) + block.value_columns, ops.AsTypeOp(to_type=dtype) ) self._block = block @@ -162,7 +162,7 @@ def _apply_binary_op( block.select_column(result_id).assign_label(result_id, name) ) else: - partial_op = ops.BinopPartialRight(op, other) + partial_op = ops.ApplyRight(base_op=op, right_scalar=other) return self._apply_unary_op(partial_op) def _apply_corr_aggregation(self, other: series.Series) -> float: diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 201b19abe80..2798f18b38e 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -43,7 +43,7 @@ def find( start: Optional[int] = None, end: Optional[int] = None, ) -> series.Series: - return self._apply_unary_op(ops.FindOp(sub, start, end)) + return self._apply_unary_op(ops.StrFindOp(substr=sub, start=start, end=end)) def len(self) -> series.Series: return self._apply_unary_op(ops.len_op) @@ -61,7 +61,7 @@ def slice( start: Optional[int] = None, stop: Optional[int] = None, ) -> series.Series: - return self._apply_unary_op(ops.SliceOp(start, stop)) + return self._apply_unary_op(ops.StrSliceOp(start=start, end=stop)) def strip(self) -> series.Series: return self._apply_unary_op(ops.strip_op) @@ -114,7 +114,7 @@ def lstrip(self) -> series.Series: return self._apply_unary_op(ops.lstrip_op) def repeat(self, repeats: int) -> series.Series: - return self._apply_unary_op(ops.RepeatOp(repeats)) + return self._apply_unary_op(ops.StrRepeatOp(repeats=repeats)) def capitalize(self) -> series.Series: return self._apply_unary_op(ops.capitalize_op) @@ -122,38 +122,44 @@ def capitalize(self) -> series.Series: def match(self, pat, case=True, flags=0) -> series.Series: # \A anchors start of entire string rather than start of any line in multiline mode adj_pat = rf"\A{pat}" - return self.contains(adj_pat, case=case, flags=flags) + return self.contains(pat=adj_pat, case=case, flags=flags) def fullmatch(self, pat, case=True, flags=0) -> series.Series: # \A anchors start of entire string rather than start of any line in multiline mode # \z likewise anchors to the end of the entire multiline string adj_pat = rf"\A{pat}\z" - return self.contains(adj_pat, case=case, flags=flags) + return self.contains(pat=adj_pat, case=case, flags=flags) def get(self, i: int) -> series.Series: - return self._apply_unary_op(ops.StrGetOp(i)) + return self._apply_unary_op(ops.StrGetOp(i=i)) def pad(self, width, side="left", fillchar=" ") -> series.Series: - return self._apply_unary_op(ops.StrPadOp(width, fillchar, side)) + return self._apply_unary_op( + ops.StrPadOp(length=width, fillchar=fillchar, side=side) + ) def ljust(self, width, fillchar=" ") -> series.Series: - return self._apply_unary_op(ops.StrPadOp(width, fillchar, "right")) + return self._apply_unary_op( + ops.StrPadOp(length=width, fillchar=fillchar, side="right") + ) def rjust(self, width, fillchar=" ") -> series.Series: - return self._apply_unary_op(ops.StrPadOp(width, fillchar, "left")) + return self._apply_unary_op( + ops.StrPadOp(length=width, fillchar=fillchar, side="left") + ) def contains( self, pat, case: bool = True, flags: int = 0, *, regex: bool = True ) -> series.Series: if not case: - return self.contains(pat, flags=flags | re.IGNORECASE, regex=True) + return self.contains(pat=pat, flags=flags | re.IGNORECASE, regex=True) if regex: re2flags = _parse_flags(flags) if re2flags: pat = re2flags + pat - return self._apply_unary_op(ops.ContainsRegexOp(pat)) + return self._apply_unary_op(ops.StrContainsRegexOp(pat=pat)) else: - return self._apply_unary_op(ops.ContainsStringOp(pat)) + return self._apply_unary_op(ops.StrContainsOp(pat=pat)) def extract(self, pat: str, flags: int = 0) -> df.DataFrame: re2flags = _parse_flags(flags) @@ -173,7 +179,9 @@ def extract(self, pat: str, flags: int = 0) -> df.DataFrame: ] label = labels[0] if labels else str(i) block, id = block.apply_unary_op( - self._value_column, ops.ExtractOp(pat, i + 1), result_label=label + self._value_column, + ops.StrExtractOp(pat=pat, n=i + 1), + result_label=label, ) results.append(id) block = block.select_columns(results) @@ -196,13 +204,13 @@ def replace( re2flags = _parse_flags(flags) if re2flags: patstr = re2flags + patstr - return self._apply_unary_op(ops.ReplaceRegexOp(patstr, repl)) + return self._apply_unary_op(ops.RegexReplaceStrOp(pat=patstr, repl=repl)) else: if is_compiled: raise ValueError( "Must set 'regex'=True if using compiled regex pattern." ) - return self._apply_unary_op(ops.ReplaceStringOp(patstr, repl)) + return self._apply_unary_op(ops.ReplaceStrOp(pat=patstr, repl=repl)) def startswith( self, @@ -210,7 +218,7 @@ def startswith( ) -> series.Series: if not isinstance(pat, tuple): pat = (pat,) - return self._apply_unary_op(ops.StartsWithOp(pat)) + return self._apply_unary_op(ops.StartsWithOp(pat=pat)) def endswith( self, @@ -218,13 +226,15 @@ def endswith( ) -> series.Series: if not isinstance(pat, tuple): pat = (pat,) - return self._apply_unary_op(ops.EndsWithOp(pat)) + return self._apply_unary_op(ops.EndsWithOp(pat=pat)) def zfill(self, width: int) -> series.Series: - return self._apply_unary_op(ops.ZfillOp(width)) + return self._apply_unary_op(ops.ZfillOp(width=width)) def center(self, width: int, fillchar: str = " ") -> series.Series: - return self._apply_unary_op(ops.StrPadOp(width, fillchar, "both")) + return self._apply_unary_op( + ops.StrPadOp(length=width, fillchar=fillchar, side="both") + ) def cat( self, @@ -232,7 +242,7 @@ def cat( *, join: Literal["outer", "left"] = "left", ) -> series.Series: - return self._apply_binary_op(others, ops.concat_op, alignment=join) + return self._apply_binary_op(others, ops.strconcat_op, alignment=join) def _parse_flags(flags: int) -> Optional[str]: diff --git a/bigframes/operations/structs.py b/bigframes/operations/structs.py index b2ae98f3789..0e00b781c94 100644 --- a/bigframes/operations/structs.py +++ b/bigframes/operations/structs.py @@ -14,10 +14,6 @@ from __future__ import annotations -import typing - -import ibis.expr.types as ibis_types - from bigframes.core import log_adapter import bigframes.dataframe import bigframes.operations @@ -26,19 +22,6 @@ import third_party.bigframes_vendored.pandas.core.arrays.arrow.accessors as vendoracessors -class _StructField(bigframes.operations.UnaryOp): - def __init__(self, name_or_index: str | int): - self._name_or_index = name_or_index - - def _as_ibis(self, x: ibis_types.Value): - struct_value = typing.cast(ibis_types.StructValue, x) - if isinstance(self._name_or_index, str): - name = self._name_or_index - else: - name = struct_value.names[self._name_or_index] - return struct_value[name].name(name) - - @log_adapter.class_logger class StructAccessor( bigframes.operations.base.SeriesMethods, vendoracessors.StructAccessor @@ -46,7 +29,7 @@ class StructAccessor( __doc__ = vendoracessors.StructAccessor.__doc__ def field(self, name_or_index: str | int) -> bigframes.series.Series: - series = self._apply_unary_op(_StructField(name_or_index)) + series = self._apply_unary_op(bigframes.operations.StructFieldOp(name_or_index)) if isinstance(name_or_index, str): name = name_or_index else: diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 76b30e6680c..0f060a23e8b 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -295,12 +295,12 @@ def _perform_get_dummies_block_operations( if column_label == "": new_column_label = value new_block, new_id = block.apply_unary_op( - column_id, ops.BinopPartialLeft(ops.eq_op, value) + column_id, ops.ApplyLeft(ops.eq_op, value) ) intermediate_col_ids.append(new_id) block, _ = new_block.apply_unary_op( new_id, - ops.BinopPartialRight(ops.fillna_op, False), + ops.ApplyRight(ops.fillna_op, False), result_label=new_column_label, ) if dummy_na: diff --git a/bigframes/series.py b/bigframes/series.py index 1247883aa50..8f564423fc4 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -189,7 +189,7 @@ def rename( # Will throw if value type isn't compatible with index type. block, const_id = block.create_constant(v, dtype=idx_dtype) block, cond_id = block.apply_unary_op( - idx_id, ops.BinopPartialRight(ops.ne_op, k) + idx_id, ops.ApplyRight(base_op=ops.ne_op, right_scalar=k) ) block, new_idx_id = block.apply_ternary_op( idx_id, cond_id, const_id, ops.where_op @@ -267,7 +267,7 @@ def astype( self, dtype: Union[bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype], ) -> Series: - return self._apply_unary_op(bigframes.operations.AsTypeOp(dtype)) + return self._apply_unary_op(bigframes.operations.AsTypeOp(to_type=dtype)) def to_pandas( self, @@ -337,7 +337,7 @@ def drop( level_id = self._resolve_levels(level or 0)[0] if _is_list_like(index): block, inverse_condition_id = block.apply_unary_op( - level_id, ops.IsInOp(index, match_nulls=True) + level_id, ops.IsInOp(values=tuple(index), match_nulls=True) ) block, condition_id = block.apply_unary_op( inverse_condition_id, ops.invert_op @@ -471,7 +471,7 @@ def _regex_replace(self, to_replace: str, value: str): ) block, result_col = self._block.apply_unary_op( self._value_column, - ops.ReplaceRegexOp(to_replace, value), + ops.RegexReplaceStrOp(to_replace, value), result_label=self.name, ) return Series(block.select_column(result_col)) @@ -487,7 +487,7 @@ def _simple_replace(self, to_replace_list: typing.Sequence, value): return self.astype(result_type)._simple_replace(to_replace_list, value) block, cond = self._block.apply_unary_op( - self._value_column, ops.IsInOp(to_replace_list) + self._value_column, ops.IsInOp(tuple(to_replace_list)) ) block, result_col = block.apply_binary_op( cond, @@ -572,9 +572,9 @@ def isin(self, values) -> "Series" | None: f"isin(), you passed a [{type(values).__name__}]" ) - return self._apply_unary_op(ops.IsInOp(values, match_nulls=True)).fillna( - value=False - ) + return self._apply_unary_op( + ops.IsInOp(values=tuple(values), match_nulls=True) + ).fillna(value=False) def isna(self) -> "Series": return self._apply_unary_op(ops.isnull_op) @@ -930,9 +930,9 @@ def clip(self, lower, upper): if lower is None and upper is None: return self if lower is None: - return self._apply_binary_op(upper, ops.clip_upper, alignment="left") + return self._apply_binary_op(upper, ops.clipupper_op, alignment="left") if upper is None: - return self._apply_binary_op(lower, ops.clip_lower, alignment="left") + return self._apply_binary_op(lower, ops.cliplower_op, alignment="left") value_id, lower_id, upper_id, block = self._align3(lower, upper) block, result_id = block.apply_ternary_op( value_id, lower_id, upper_id, ops.clip_op @@ -1239,7 +1239,9 @@ def apply(self, func) -> Series: # to be applied before passing data to remote function, protecting from bad # inputs causing errors. reprojected_series = Series(self._block._force_reproject()) - return reprojected_series._apply_unary_op(ops.RemoteFunctionOp(func)) + return reprojected_series._apply_unary_op( + ops.RemoteFunctionOp(func=func, apply_on_null=True) + ) def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_prefix(prefix)) @@ -1268,16 +1270,16 @@ def filter( block = self._block block, label_string_id = block.apply_unary_op( self._block.index_columns[0], - ops.AsTypeOp(pandas.StringDtype(storage="pyarrow")), + ops.AsTypeOp(to_type=pandas.StringDtype(storage="pyarrow")), ) if like is not None: block, mask_id = block.apply_unary_op( - label_string_id, ops.ContainsStringOp(pat=like) + label_string_id, ops.StrContainsOp(pat=like) ) else: # regex assert regex is not None block, mask_id = block.apply_unary_op( - label_string_id, ops.ContainsRegexOp(pat=regex) + label_string_id, ops.StrContainsRegexOp(pat=regex) ) block = block.filter(mask_id) @@ -1287,7 +1289,7 @@ def filter( # Behavior matches pandas 2.1+, older pandas versions would reindex block = self._block block, mask_id = block.apply_unary_op( - self._block.index_columns[0], ops.IsInOp(values=list(items)) + self._block.index_columns[0], ops.IsInOp(values=tuple(items)) ) block = block.filter(mask_id) block = block.select_columns([self._value_column]) From aac35a3368c7d65b002cc977caf3a13df6bf40da Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:29:32 -0800 Subject: [PATCH 257/921] chore(main): release 0.19.0 (#300) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> --- CHANGELOG.md | 23 +++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a6576ee05..c1691e1f64c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.19.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.18.0...v0.19.0) (2024-01-09) + + +### Features + +* Add 'columns' as an alias for 'col_order' ([#298](https://github.com/googleapis/python-bigquery-dataframes/issues/298)) ([a01b271](https://github.com/googleapis/python-bigquery-dataframes/commit/a01b271e76d05459f531cd83c6e93a2d13bfa061)) +* Add Series dt.tz and dt.unit properties ([#303](https://github.com/googleapis/python-bigquery-dataframes/issues/303)) ([2e1a403](https://github.com/googleapis/python-bigquery-dataframes/commit/2e1a4036e58fb6b35aa68ac6d121cb0d04f4f369)) +* Add to_gbq() method for LLM models ([#299](https://github.com/googleapis/python-bigquery-dataframes/issues/299)) ([dafbc1b](https://github.com/googleapis/python-bigquery-dataframes/commit/dafbc1bdb225c7132cdf7191792fde785947c7a1)) +* Allow manually set clustering_columns in dataframe.to_gbq ([#302](https://github.com/googleapis/python-bigquery-dataframes/issues/302)) ([9c21323](https://github.com/googleapis/python-bigquery-dataframes/commit/9c213239a73b5cd0ca7b647a86238263d3947431)) +* Support assigning to columns like a property ([#304](https://github.com/googleapis/python-bigquery-dataframes/issues/304)) ([f645c56](https://github.com/googleapis/python-bigquery-dataframes/commit/f645c56e5436adb100018afbf9ef18003a1a6ed9)) +* Support upcasting numeric columns in concat ([#294](https://github.com/googleapis/python-bigquery-dataframes/issues/294)) ([e3a056a](https://github.com/googleapis/python-bigquery-dataframes/commit/e3a056a301e99c4c3d2a2ecdcbcaf8804be8089f)) + + +### Bug Fixes + +* DF.drop tuple input as multi-index ([#301](https://github.com/googleapis/python-bigquery-dataframes/issues/301)) ([21391a9](https://github.com/googleapis/python-bigquery-dataframes/commit/21391a9d07bb0dc6b6f900f1b069350d6232bd92)) +* Fix bug converting non-string labels to sql ids ([#296](https://github.com/googleapis/python-bigquery-dataframes/issues/296)) ([a61c5fe](https://github.com/googleapis/python-bigquery-dataframes/commit/a61c5fef1e3b88f38269ee5bfd50886b8d2908ae)) + + +### Documentation + +* Add code samples for `Series.ffill` and `DataFrame.ffill` ([#307](https://github.com/googleapis/python-bigquery-dataframes/issues/307)) ([1c63b45](https://github.com/googleapis/python-bigquery-dataframes/commit/1c63b451bb057e5b6470d63d4b44c090d7172aa5)) + ## [0.18.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.17.0...v0.18.0) (2024-01-02) diff --git a/bigframes/version.py b/bigframes/version.py index 494335acd71..aeefff7a4b1 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.18.0" +__version__ = "0.19.0" From 5bb45ba5560f178438d490a62520ccd36fd2f284 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 9 Jan 2024 17:20:16 -0800 Subject: [PATCH 258/921] fix: handle multi-level columns for df aggregates properly (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/blocks.py | 12 ++++++++++-- tests/system/small/test_multiindex.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 9688f439b1a..e97e8fd01c9 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -855,13 +855,21 @@ def aggregate_all_and_stack( aggregations = [ (col_id, operation, col_id) for col_id in self.value_columns ] + index_col_ids = [ + guid.generate_guid() for i in range(self.column_labels.nlevels) + ] result_expr = self.expr.aggregate(aggregations, dropna=dropna).unpivot( row_labels=self.column_labels.to_list(), - index_col_ids=["index"], + index_col_ids=index_col_ids, unpivot_columns=tuple([(value_col_id, tuple(self.value_columns))]), dtype=dtype, ) - return Block(result_expr, index_columns=["index"], column_labels=[None]) + return Block( + result_expr, + index_columns=index_col_ids, + column_labels=[None], + index_labels=self.column_labels.names, + ) else: # axis_n == 1 # using offsets as identity to group on. # TODO: Allow to promote identity/total_order columns instead for better perf diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index d767b30bd67..c5e8b45b8ee 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -713,6 +713,26 @@ def test_column_multi_index_binary_op(scalars_df_index, scalars_pandas_df_index) pandas.testing.assert_series_equal(bf_result, pd_result) +@skip_legacy_pandas +def test_column_multi_index_any(): + columns = pandas.MultiIndex.from_tuples( + [("col0", "col00"), ("col0", "col00"), ("col1", "col11")] + ) + pd_df = pandas.DataFrame( + [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]], columns=columns + ) + bf_df = bpd.DataFrame(pd_df) + + pd_result = pd_df.isna().any() + bf_result = bf_df.isna().any().to_pandas() + + pandas.testing.assert_frame_equal( + bf_result.reset_index(drop=False), + pd_result.reset_index(drop=False), + check_dtype=False, + ) + + def test_column_multi_index_agg(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "int64_col", "float64_col"] multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "a"], ["a", "b", "b"])) From 5cccd36fd2081becd741541c4ac8d5cf53c076f2 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 10 Jan 2024 22:47:20 -0800 Subject: [PATCH 259/921] fix: update max_output_token limitation. (#308) * fix: update max_output_token limitation. * update format * update if-else condition. * update else * logic update. --------- Co-authored-by: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> --- bigframes/ml/llm.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 8c011591138..36070003238 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -160,7 +160,8 @@ def predict( max_output_tokens (int, default 128): Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. A token may be smaller than a word. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. - Default 128. Possible values [1, 1024]. + Default 128. For the 'text-bison' model, possible values are in the range [1, 1024]. For the 'text-bison-32k' model, possible values are in the range [1, 8196]. + Please ensure that the specified value for max_output_tokens is within the appropriate range for the model being used. top_k (int, default 40): Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens @@ -184,12 +185,26 @@ def predict( # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models if temperature < 0.0 or temperature > 1.0: raise ValueError(f"temperature must be [0.0, 1.0], but is {temperature}.") - if max_output_tokens not in range(1, 1025): + + if ( + self.model_name == _TEXT_GENERATOR_BISON_ENDPOINT + and max_output_tokens not in range(1, 1025) + ): + raise ValueError( + f"max_output_token must be [1, 1024] for TextBison model, but is {max_output_tokens}." + ) + + if ( + self.model_name == _TEXT_GENERATOR_BISON_32K_ENDPOINT + and max_output_tokens not in range(1, 8197) + ): raise ValueError( - f"max_output_token must be [1, 1024], but is {max_output_tokens}." + f"max_output_token must be [1, 8196] for TextBison 32k model, but is {max_output_tokens}." ) + if top_k not in range(1, 41): raise ValueError(f"top_k must be [1, 40], but is {top_k}.") + if top_p < 0.0 or top_p > 1.0: raise ValueError(f"top_p must be [0.0, 1.0], but is {top_p}.") From 8922e5e6f7bd3113bd39608910aac7b006d1f5e6 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 11 Jan 2024 00:06:15 -0800 Subject: [PATCH 260/921] refactor: define scalar expression structs (#309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 46 ++----------- bigframes/core/blocks.py | 36 ++++++---- bigframes/core/compile/compiled.py | 16 +++-- bigframes/core/compile/compiler.py | 9 +-- bigframes/core/compile/scalar_op_compiler.py | 40 ++++++++++++ bigframes/core/expression.py | 69 ++++++++++++++++++++ bigframes/core/indexes/index.py | 4 +- bigframes/core/nodes.py | 10 ++- bigframes/operations/__init__.py | 38 +++++++++++ tests/unit/test_core.py | 12 ++-- 10 files changed, 199 insertions(+), 81 deletions(-) create mode 100644 bigframes/core/expression.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 7ff23efad3d..489afdcb51b 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -23,6 +23,7 @@ import bigframes.core.compile.compiled as compiled import bigframes.core.compile.compiler as compiler +import bigframes.core.expression as expressions import bigframes.core.guid import bigframes.core.nodes as nodes from bigframes.core.ordering import OrderingColumnReference @@ -30,7 +31,6 @@ import bigframes.core.utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes -import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.session._io.bigquery @@ -152,48 +152,10 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: nodes.ConcatNode(children=tuple([self.node, *[val.node for val in other]])) ) - def project_unary_op( - self, column_name: str, op: ops.UnaryOp, output_name=None - ) -> ArrayValue: - """Creates a new expression based on this expression with unary operation applied to one column.""" + def project(self, expression: expressions.Expression, output_id: str): return ArrayValue( - nodes.ProjectRowOpNode( - child=self.node, input_ids=(column_name,), op=op, output_id=output_name - ) - ) - - def project_binary_op( - self, - left_column_id: str, - right_column_id: str, - op: ops.BinaryOp, - output_column_id: str, - ) -> ArrayValue: - """Creates a new expression based on this expression with binary operation applied to two columns.""" - return ArrayValue( - nodes.ProjectRowOpNode( - child=self.node, - input_ids=(left_column_id, right_column_id), - op=op, - output_id=output_column_id, - ) - ) - - def project_ternary_op( - self, - col_id_1: str, - col_id_2: str, - col_id_3: str, - op: ops.TernaryOp, - output_column_id: str, - ) -> ArrayValue: - """Creates a new expression based on this expression with ternary operation applied to three columns.""" - return ArrayValue( - nodes.ProjectRowOpNode( - child=self.node, - input_ids=(col_id_1, col_id_2, col_id_3), - op=op, - output_id=output_column_id, + nodes.ProjectionNode( + child=self.node, assignments=((expression, output_id),) ) ) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index e97e8fd01c9..57a497d2cb9 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -669,7 +669,7 @@ def apply_unary_op( """ # TODO(tbergeron): handle labels safely so callers don't need to result_id = guid.generate_guid() - expr = self._expr.project_unary_op(column, op, result_id) + expr = self._expr.project(op.as_expr(column), result_id) block = Block( expr, index_columns=self.index_columns, @@ -686,8 +686,8 @@ def apply_binary_op( result_label: Label = None, ) -> typing.Tuple[Block, str]: result_id = guid.generate_guid() - expr = self._expr.project_binary_op( - left_column_id, right_column_id, op, result_id + expr = self._expr.project( + op.as_expr(left_column_id, right_column_id), result_id ) block = Block( expr, @@ -706,9 +706,7 @@ def apply_ternary_op( result_label: Label = None, ) -> typing.Tuple[Block, str]: result_id = guid.generate_guid() - expr = self._expr.project_ternary_op( - col_id_1, col_id_2, col_id_3, op, result_id - ) + expr = self._expr.project(op.as_expr(col_id_1, col_id_2, col_id_3), result_id) block = Block( expr, index_columns=self.index_columns, @@ -1240,9 +1238,14 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - expr = expr.project_unary_op(index_col, ops.AsTypeOp(to_type="string")) + expr = expr.project( + expression=ops.AsTypeOp(to_type="string").as_expr(index_col), + output_id=index_col, + ) prefix_op = ops.ApplyLeft(base_op=ops.add_op, left_scalar=prefix) - expr = expr.project_unary_op(index_col, prefix_op) + expr = expr.project( + expression=prefix_op.as_expr(index_col), output_id=index_col + ) return Block( expr, index_columns=self.index_columns, @@ -1259,9 +1262,14 @@ def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - expr = expr.project_unary_op(index_col, ops.AsTypeOp(to_type="string")) + expr = expr.project( + expression=ops.AsTypeOp(to_type="string").as_expr(index_col), + output_id=index_col, + ) prefix_op = ops.ApplyRight(base_op=ops.add_op, right_scalar=suffix) - expr = expr.project_unary_op(index_col, prefix_op) + expr = expr.project( + expression=prefix_op.as_expr(index_col), output_id=index_col + ) return Block( expr, index_columns=self.index_columns, @@ -1568,10 +1576,10 @@ def merge( coalesced_ids = [] for left_id, right_id in zip(left_join_ids, right_join_ids): coalesced_id = guid.generate_guid() - joined_expr = joined_expr.project_binary_op( - get_column_left[left_id], - get_column_right[right_id], - ops.coalesce_op, + joined_expr = joined_expr.project( + ops.coalesce_op.as_expr( + get_column_left[left_id], get_column_right[right_id] + ), coalesced_id, ) coalesced_ids.append(coalesced_id) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index c1e8f1ea486..6a3db3f2bdf 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -28,6 +28,7 @@ import bigframes.constants as constants import bigframes.core.compile.scalar_op_compiler as op_compilers +import bigframes.core.expression as expressions import bigframes.core.guid from bigframes.core.ordering import ( encode_order_string, @@ -151,18 +152,19 @@ def _reproject_to_table(self: T) -> T: """ ... - def project_row_op( + def project_expression( self: T, - input_column_ids: typing.Sequence[str], - op: ops.RowOp, + expression: expressions.Expression, output_column_id: typing.Optional[str] = None, ) -> T: - """Creates a new expression based on this expression with unary operation applied to one column.""" + """Apply an expression to the ArrayValue and assign the output to a column.""" result_id = ( - output_column_id or input_column_ids[0] + output_column_id or expression.unbound_variables[0] ) # overwrite input if not output id provided - inputs = tuple(self._get_ibis_column(col) for col in input_column_ids) - value = op_compiler.compile_row_op(op, inputs).name(result_id) + bindings = { + col: self._get_ibis_column(col) for col in expression.unbound_variables + } + value = op_compiler.compile_expression(expression, bindings).name(result_id) return self._set_or_replace_by_id(result_id, value) def assign(self: T, source_id: str, destination_id: str) -> T: diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index c28958a8612..eacee26bcb7 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -143,10 +143,11 @@ def compile_reversed(node: nodes.ReversedNode, ordered: bool = True): @_compile_node.register -def compile_project(node: nodes.ProjectRowOpNode, ordered: bool = True): - return compile_node(node.child, ordered).project_row_op( - node.input_ids, node.op, node.output_id - ) +def compile_projection(node: nodes.ProjectionNode, ordered: bool = True): + result = compile_node(node.child, ordered) + for expr, id in node.assignments: + result = result.project_expression(expr, id) + return result @_compile_node.register diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index d711dbf4565..2331d3aa289 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -26,6 +26,7 @@ import pandas as pd import bigframes.constants as constants +import bigframes.core.expression as expressions import bigframes.dtypes import bigframes.dtypes as dtypes import bigframes.operations as ops @@ -50,6 +51,45 @@ class ScalarOpCompiler: ], ] = {} + @functools.singledispatchmethod + def compile_expression( + self, + expression: expressions.Expression, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + raise NotImplementedError(f"Unrecognized expression: {expression}") + + @compile_expression.register + def _( + self, + expression: expressions.ScalarConstantExpression, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + return ibis.literal(expression.value) + + @compile_expression.register + def _( + self, + expression: expressions.UnboundVariableExpression, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + if expression.id not in bindings: + raise ValueError(f"Could not resolve unbound variable {expression.id}") + else: + return bindings[expression.id] + + @compile_expression.register + def _( + self, + expression: expressions.OpExpression, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + inputs = [ + self.compile_expression(sub_expr, bindings) + for sub_expr in expression.inputs + ] + return self.compile_row_op(expression.op, inputs) + def compile_row_op( self, op: ops.RowOp, inputs: typing.Sequence[ibis_types.Value] ) -> ibis_types.Value: diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py new file mode 100644 index 00000000000..2fb1ccb9883 --- /dev/null +++ b/bigframes/core/expression.py @@ -0,0 +1,69 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import abc +import dataclasses +import itertools +import typing + +import bigframes.operations + + +@dataclasses.dataclass(frozen=True) +class Expression(abc.ABC): + """An expression represents a computation taking N scalar inputs and producing a single output scalar.""" + + @property + def unbound_variables(self) -> typing.Tuple[str, ...]: + return () + + +@dataclasses.dataclass(frozen=True) +class ScalarConstantExpression(Expression): + """An expression representing a scalar constant.""" + + # TODO: Further constrain? + value: typing.Hashable + + +@dataclasses.dataclass(frozen=True) +class UnboundVariableExpression(Expression): + """A variable expression representing an unbound variable.""" + + id: str + + @property + def unbound_variables(self) -> typing.Tuple[str, ...]: + return (self.id,) + + +@dataclasses.dataclass(frozen=True) +class OpExpression(Expression): + """An expression representing a scalar operation applied to 1 or more argument sub-expressions.""" + + op: bigframes.operations.RowOp + inputs: typing.Tuple[Expression, ...] + + def __post_init__(self): + assert self.op.arguments == len(self.inputs) + + @property + def unbound_variables(self) -> typing.Tuple[str, ...]: + return tuple( + itertools.chain.from_iterable( + map(lambda x: x.unbound_variables, self.inputs) + ) + ) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index f148759f619..6602170b5f3 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -604,8 +604,8 @@ def coalesce_columns( expr = expr.drop_columns([left_id]) elif how == "outer": coalesced_id = bigframes.core.guid.generate_guid() - expr = expr.project_binary_op( - left_id, right_id, ops.coalesce_op, coalesced_id + expr = expr.project( + ops.coalesce_op.as_expr(left_id, right_id), coalesced_id ) expr = expr.drop_columns([left_id, right_id]) result_ids.append(coalesced_id) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 5385852432f..360f853e3ed 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -17,15 +17,15 @@ from dataclasses import dataclass, field, fields import functools import typing -from typing import Optional, Tuple +from typing import Tuple import pandas +import bigframes.core.expression as expressions import bigframes.core.guid from bigframes.core.ordering import OrderingColumnReference import bigframes.core.window_spec as window import bigframes.dtypes -import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops if typing.TYPE_CHECKING: @@ -196,10 +196,8 @@ def __hash__(self): @dataclass(frozen=True) -class ProjectRowOpNode(UnaryNode): - input_ids: typing.Tuple[str, ...] - op: ops.RowOp - output_id: Optional[str] = None +class ProjectionNode(UnaryNode): + assignments: typing.Tuple[typing.Tuple[expressions.Expression, str], ...] def __hash__(self): return self._node_hash diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 3ef551e4539..39ca52394ea 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -21,6 +21,10 @@ import bigframes.dtypes as dtypes +if typing.TYPE_CHECKING: + # Avoids circular dependency + import bigframes.core.expression + class RowOp(typing.Protocol): @property @@ -45,6 +49,13 @@ def name(self) -> str: def arguments(self) -> int: return 1 + def as_expr(self, input_id: str) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + return bigframes.core.expression.OpExpression( + self, (bigframes.core.expression.UnboundVariableExpression(input_id),) + ) + @dataclasses.dataclass(frozen=True) class BinaryOp: @@ -56,6 +67,19 @@ def name(self) -> str: def arguments(self) -> int: return 2 + def as_expr( + self, left_input: str, right_input: str + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + return bigframes.core.expression.OpExpression( + self, + ( + bigframes.core.expression.UnboundVariableExpression(left_input), + bigframes.core.expression.UnboundVariableExpression(right_input), + ), + ) + @dataclasses.dataclass(frozen=True) class TernaryOp: @@ -67,6 +91,20 @@ def name(self) -> str: def arguments(self) -> int: return 3 + def as_expr( + self, input1: str, input2: str, input3: str + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + return bigframes.core.expression.OpExpression( + self, + ( + bigframes.core.expression.UnboundVariableExpression(input1), + bigframes.core.expression.UnboundVariableExpression(input2), + bigframes.core.expression.UnboundVariableExpression(input3), + ), + ) + # Operation Factories def create_unary_op(name: str) -> UnaryOp: diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 623448b3aa0..8032093b626 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -136,7 +136,9 @@ def test_arrayvalues_to_ibis_expr_with_project_unary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_unary_op("col1", ops.AsTypeOp("string"))._compile_ordered() + expr = value.project( + ops.AsTypeOp("string").as_expr("col1"), output_id="col1" + )._compile_ordered() assert value._compile_ordered().columns[0].type().is_int64() assert expr.columns[0].type().is_string() @@ -152,9 +154,7 @@ def test_arrayvalues_to_ibis_expr_with_project_binary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_binary_op( - "col2", "col3", ops.add_op, "col4" - )._compile_ordered() + expr = value.project(ops.add_op.as_expr("col2", "col3"), "col4")._compile_ordered() assert expr.columns[3].type().is_float64() actual = expr._to_ibis_expr(ordering_mode="unordered") assert len(expr.columns) == 4 @@ -173,8 +173,8 @@ def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project_ternary_op( - "col2", "col3", "col4", ops.where_op, "col5" + expr = value.project( + ops.where_op.as_expr("col2", "col3", "col4"), "col5" )._compile_ordered() assert expr.columns[4].type().is_float64() actual = expr._to_ibis_expr(ordering_mode="unordered") From 4a1a1e08014d0f2588f85aa0f3db037aef4791bf Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 11 Jan 2024 16:04:15 -0800 Subject: [PATCH 261/921] refactor: refactor block materialization (#306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/_config/sampling_options.py | 24 ++++ bigframes/core/__init__.py | 11 +- bigframes/core/blocks.py | 158 ++++++++++++------------- bigframes/core/compile/row_identity.py | 8 +- bigframes/series.py | 3 +- bigframes/session/__init__.py | 2 - 6 files changed, 113 insertions(+), 93 deletions(-) diff --git a/bigframes/_config/sampling_options.py b/bigframes/_config/sampling_options.py index 1742dabe17a..a80b9601ca3 100644 --- a/bigframes/_config/sampling_options.py +++ b/bigframes/_config/sampling_options.py @@ -14,6 +14,8 @@ """Options for downsampling.""" +from __future__ import annotations + import dataclasses from typing import Literal, Optional @@ -25,6 +27,28 @@ class SamplingOptions: __doc__ = vendored_pandas_config.sampling_options_doc max_download_size: Optional[int] = 500 + # Enable downsampling enable_downsampling: bool = False sampling_method: Literal["head", "uniform"] = "uniform" random_state: Optional[int] = None + + def with_max_download_size(self, max_rows: Optional[int]) -> SamplingOptions: + return SamplingOptions( + max_rows, self.enable_downsampling, self.sampling_method, self.random_state + ) + + def with_method(self, method: Literal["head", "uniform"]) -> SamplingOptions: + return SamplingOptions(self.max_download_size, True, method, self.random_state) + + def with_random_state(self, state: Optional[int]) -> SamplingOptions: + return SamplingOptions( + self.max_download_size, + self.enable_downsampling, + self.sampling_method, + state, + ) + + def with_disabled(self) -> SamplingOptions: + return SamplingOptions( + self.max_download_size, False, self.sampling_method, self.random_state + ) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 489afdcb51b..e32977fbce5 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -21,8 +21,7 @@ import ibis.expr.types as ibis_types import pandas -import bigframes.core.compile.compiled as compiled -import bigframes.core.compile.compiler as compiler +import bigframes.core.compile as compiling import bigframes.core.expression as expressions import bigframes.core.guid import bigframes.core.nodes as nodes @@ -104,11 +103,11 @@ def _try_evaluate_local(self): def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: return self._compile_ordered().get_column_type(key) - def _compile_ordered(self) -> compiled.OrderedIR: - return compiler.compile_ordered(self.node) + def _compile_ordered(self) -> compiling.OrderedIR: + return compiling.compile_ordered(self.node) - def _compile_unordered(self) -> compiled.UnorderedIR: - return compiler.compile_unordered(self.node) + def _compile_unordered(self) -> compiling.UnorderedIR: + return compiling.compile_unordered(self.node) def row_count(self) -> ArrayValue: """Get number of rows in ArrayValue as a single-entry ArrayValue.""" diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 57a497d2cb9..cf1a8cb7c0f 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -21,6 +21,7 @@ from __future__ import annotations +import dataclasses import functools import itertools import random @@ -31,6 +32,7 @@ import google.cloud.bigquery as bigquery import pandas as pd +import bigframes._config.sampling_options as sampling_options import bigframes.constants as constants import bigframes.core as core import bigframes.core.guid as guid @@ -80,6 +82,14 @@ def _get_block(self) -> Block: """Get the underlying block value of the object""" +@dataclasses.dataclass() +class MaterializationOptions: + downsampling: sampling_options.SamplingOptions = dataclasses.field( + default_factory=sampling_options.SamplingOptions + ) + ordered: bool = True + + class Block: """A immutable 2D data structure.""" @@ -395,8 +405,6 @@ def _to_dataframe(self, result) -> pd.DataFrame: def to_pandas( self, - value_keys: Optional[Iterable[str]] = None, - max_results: Optional[int] = None, max_download_size: Optional[int] = None, sampling_method: Optional[str] = None, random_state: Optional[int] = None, @@ -404,14 +412,24 @@ def to_pandas( ordered: bool = True, ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame.""" + 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)}." + ) - df, _, query_job = self._compute_and_count( - value_keys=value_keys, - max_results=max_results, - max_download_size=max_download_size, - sampling_method=sampling_method, - random_state=random_state, - ordered=ordered, + 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() + + df, query_job = self._materialize_local( + materialize_options=MaterializationOptions( + downsampling=sampling, ordered=ordered + ) ) return df, query_job @@ -439,57 +457,29 @@ def _copy_index_to_pandas(self, df: pd.DataFrame): # See: https://github.com/pandas-dev/pandas-stubs/issues/804 df.index.names = self.index.names # type: ignore - def _compute_and_count( - self, - value_keys: Optional[Iterable[str]] = None, - max_results: Optional[int] = None, - max_download_size: Optional[int] = None, - sampling_method: Optional[str] = None, - random_state: Optional[int] = None, - *, - ordered: bool = True, - ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: + def _materialize_local( + self, materialize_options: MaterializationOptions = MaterializationOptions() + ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. - enable_downsampling = ( - True - if sampling_method is not None - else bigframes.options.sampling.enable_downsampling - ) - - max_download_size = ( - max_download_size or bigframes.options.sampling.max_download_size - ) - - random_state = random_state or bigframes.options.sampling.random_state - - if sampling_method is None: - sampling_method = bigframes.options.sampling.sampling_method or _UNIFORM - sampling_method = sampling_method.lower() - - if sampling_method not in _SAMPLING_METHODS: - raise NotImplementedError( - f"The downsampling method {sampling_method} is not implemented, " - f"please choose from {','.join(_SAMPLING_METHODS)}." - ) - - expr = self._apply_value_keys_to_expr(value_keys=value_keys) - results_iterator, query_job = self.session._execute( - expr, max_results=max_results, sorted=ordered + self.expr, sorted=materialize_options.ordered ) - table_size = ( self.session._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES ) + sample_config = materialize_options.downsampling + max_download_size = sample_config.max_download_size fraction = ( max_download_size / table_size if (max_download_size is not None) and (table_size != 0) else 2 ) + # TODO: Maybe materialize before downsampling + # Some downsampling methods if fraction < 1: - if not enable_downsampling: + if not sample_config.enable_downsampling: raise RuntimeError( f"The data size ({table_size:.2f} MB) exceeds the maximum download limit of " f"{max_download_size} MB. You can:\n\t* Enable downsampling in global options:\n" @@ -507,42 +497,53 @@ def _compute_and_count( "\nPlease refer to the documentation for configuring the downloading limit.", UserWarning, ) - if sampling_method == _HEAD: - total_rows = int(results_iterator.total_rows * fraction) - results_iterator.max_results = total_rows - df = self._to_dataframe(results_iterator) - - if self.index_columns: - df.set_index(list(self.index_columns), inplace=True) - df.index.names = self.index.names # type: ignore - elif (sampling_method == _UNIFORM) and (random_state is None): - filtered_expr = self.expr._uniform_sampling(fraction) - block = Block( - filtered_expr, - index_columns=self.index_columns, - column_labels=self.column_labels, - index_labels=self.index.names, - ) - df, total_rows, _ = block._compute_and_count(max_download_size=None) - elif sampling_method == _UNIFORM: - block = self._split( - fracs=(max_download_size / table_size,), - random_state=random_state, - preserve_order=True, - )[0] - df, total_rows, _ = block._compute_and_count(max_download_size=None) - else: - # This part should never be called, just in case. - raise NotImplementedError( - f"The downsampling method {sampling_method} is not implemented, " - f"please choose from {','.join(_SAMPLING_METHODS)}." - ) + total_rows = results_iterator.total_rows + # Remove downsampling config from subsequent invocations, as otherwise could result in many + # iterations if downsampling undershoots + return self._downsample( + total_rows=total_rows, + sampling_method=sample_config.sampling_method, + fraction=fraction, + random_state=sample_config.random_state, + )._materialize_local( + MaterializationOptions(ordered=materialize_options.ordered) + ) else: total_rows = results_iterator.total_rows df = self._to_dataframe(results_iterator) self._copy_index_to_pandas(df) - return df, total_rows, query_job + return df, query_job + + def _downsample( + self, total_rows: int, sampling_method: str, fraction: float, random_state + ) -> Block: + # either selecting fraction or number of rows + if sampling_method == _HEAD: + filtered_block = self.slice(stop=int(total_rows * fraction)) + return filtered_block + elif (sampling_method == _UNIFORM) and (random_state is None): + filtered_expr = self.expr._uniform_sampling(fraction) + block = Block( + filtered_expr, + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + return block + elif sampling_method == _UNIFORM: + block = self._split( + fracs=(fraction,), + random_state=random_state, + preserve_order=True, + )[0] + return block + else: + # This part should never be called, just in case. + raise NotImplementedError( + f"The downsampling method {sampling_method} is not implemented, " + f"please choose from {','.join(_SAMPLING_METHODS)}." + ) def _split( self, @@ -1209,10 +1210,9 @@ def retrieve_repr_request_results( count = self.shape[0] if count > max_results: head_block = self.slice(0, max_results) - computed_df, query_job = head_block.to_pandas(max_results=max_results) else: head_block = self - computed_df, query_job = head_block.to_pandas() + computed_df, query_job = head_block.to_pandas() formatted_df = computed_df.set_axis(self.column_labels, axis=1) # we reset the axis and substitute the bf index name for the default formatted_df.index.name = self.index.name diff --git a/bigframes/core/compile/row_identity.py b/bigframes/core/compile/row_identity.py index 71d53f90dc4..7a87a435fea 100644 --- a/bigframes/core/compile/row_identity.py +++ b/bigframes/core/compile/row_identity.py @@ -24,7 +24,7 @@ import bigframes.constants as constants import bigframes.core.compile.compiled as compiled -import bigframes.core.joins.name_resolution as naming +import bigframes.core.joins as joining import bigframes.core.ordering as orderings SUPPORTED_ROW_IDENTITY_HOW = {"outer", "left", "inner"} @@ -68,7 +68,7 @@ def join_by_row_identity_unordered( right_mask = right_relative_predicates if how in ["left", "outer"] else None # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - map_left_id, map_right_id = naming.JOIN_NAME_REMAPPER( + map_left_id, map_right_id = joining.JOIN_NAME_REMAPPER( left.column_ids, right.column_ids ) joined_columns = [ @@ -125,10 +125,10 @@ def join_by_row_identity_ordered( right_mask = right_relative_predicates if how in ["left", "outer"] else None # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - lpublicmapping, rpublicmapping = naming.JOIN_NAME_REMAPPER( + lpublicmapping, rpublicmapping = joining.JOIN_NAME_REMAPPER( left.column_ids, right.column_ids ) - lhiddenmapping, rhiddenmapping = naming.JoinNameRemapper(namespace="hidden")( + lhiddenmapping, rhiddenmapping = joining.JoinNameRemapper(namespace="hidden")( left._hidden_column_ids, right._hidden_column_ids ) map_left_id = {**lpublicmapping, **lhiddenmapping} diff --git a/bigframes/series.py b/bigframes/series.py index 8f564423fc4..093fc0bf0c1 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -305,14 +305,13 @@ def to_pandas( is not exceeded; otherwise, a pandas Series with downsampled rows of the DataFrame. """ df, query_job = self._block.to_pandas( - (self._value_column,), max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, ordered=ordered, ) self._set_internal_query_job(query_job) - series = df[self._value_column] + series = df.squeeze(axis=1) series.name = self._name return series diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 15c262afa70..d503b844aa8 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1497,7 +1497,6 @@ def _execute( self, array_value: core.ArrayValue, job_config: Optional[bigquery.job.QueryJobConfig] = None, - max_results: Optional[int] = None, *, sorted: bool = True, dry_run=False, @@ -1507,7 +1506,6 @@ def _execute( return self._start_query( sql=sql, job_config=job_config, - max_results=max_results, ) def _to_sql( From 9150c16e951fb757547721e0003910c7c49e3d27 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:59:57 -0800 Subject: [PATCH 262/921] docs: add code samples for Series.corr (#316) --- bigframes/series.py | 20 ------------------- .../bigframes_vendored/pandas/core/series.py | 15 ++++++++++++++ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/bigframes/series.py b/bigframes/series.py index 093fc0bf0c1..7a4600a3245 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -735,26 +735,6 @@ def round(self, decimals=0) -> "Series": return self._apply_binary_op(decimals, ops.round_op) def corr(self, other: Series, method="pearson", min_periods=None) -> float: - """ - Compute the correlation with the other Series. Non-number values are ignored in the - computation. - - Uses the "Pearson" method of correlation. Numbers are converted to float before - calculation, so the result may be unstable. - - Args: - other (Series): - The series with which this is to be correlated. - method (string, default "pearson"): - Correlation method to use - currently only "pearson" is supported. - min_periods (int, default None): - The minimum number of observations needed to return a result. Non-default values - are not yet supported, so a result will be returned for at least two observations. - - Returns: - float; Will return NaN if there are fewer than two numeric pairs, either series has a - variance or covariance of zero, or any input value is infinite. - """ # TODO(kemppeterson): Validate early that both are numeric # TODO(kemppeterson): Handle partially-numeric columns if method != "pearson": diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 98c4fcdd447..10a4c195abe 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -809,6 +809,21 @@ def corr(self, other, method="pearson", min_periods=None) -> float: Uses the "Pearson" method of correlation. Numbers are converted to float before calculation, so the result may be unstable. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s1 = bpd.Series([.2, .0, .6, .2]) + >>> s2 = bpd.Series([.3, .6, .0, .1]) + >>> s1.corr(s2) + -0.8510644963469901 + + >>> s1 = bpd.Series([1, 2, 3], index=[0, 1, 2]) + >>> s2 = bpd.Series([1, 2, 3], index=[2, 1, 0]) + >>> s1.corr(s2) + -1.0 + Args: other (Series): The series with which this is to be correlated. From 4eb64f666ecfcb36b55325b7b6dd4e19457e8938 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 12 Jan 2024 13:55:37 -0800 Subject: [PATCH 263/921] refactor: remove 'partial' ops and replace with expressions (#314) --- bigframes/core/block_transforms.py | 155 ++++++++----------- bigframes/core/blocks.py | 125 +++++++-------- bigframes/core/compile/scalar_op_compiler.py | 35 +---- bigframes/core/expression.py | 22 +++ bigframes/core/groupby/__init__.py | 12 +- bigframes/core/indexers.py | 18 +-- bigframes/core/indexes/index.py | 23 +-- bigframes/core/reshape/__init__.py | 5 +- bigframes/dataframe.py | 95 +++++++----- bigframes/operations/__init__.py | 94 ++++------- bigframes/operations/base.py | 25 +-- bigframes/pandas/__init__.py | 12 +- bigframes/series.py | 32 ++-- 13 files changed, 295 insertions(+), 358 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 0b6886562e0..345adb6be3b 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -20,6 +20,7 @@ import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks +import bigframes.core.expression as ex import bigframes.core.ordering as ordering import bigframes.core.window_spec as windows import bigframes.dtypes as dtypes @@ -44,11 +45,10 @@ def equals(block1: blocks.Block, block2: blocks.Block) -> bool: for lcol, rcol in zip(block1.value_columns, block2.value_columns): lcolmapped = lmap[lcol] rcolmapped = rmap[rcol] - joined_block, result_id = joined_block.apply_binary_op( - lcolmapped, rcolmapped, ops.eq_null_match_op - ) - joined_block, result_id = joined_block.apply_unary_op( - result_id, ops.partial_right(ops.fillna_op, False) + joined_block, result_id = joined_block.project_expr( + ops.fillna_op.as_expr( + ops.eq_null_match_op.as_expr(lcolmapped, rcolmapped), ex.const(False) + ) ) equality_ids.append(result_id) @@ -91,9 +91,8 @@ def indicate_duplicates( agg_ops.count_op, window_spec=window_spec, ) - block, duplicate_indicator = block.apply_unary_op( - val_count_col_id, - ops.partial_right(ops.gt_op, 1), + block, duplicate_indicator = block.project_expr( + ops.gt_op.as_expr(val_count_col_id, ex.const(1)) ) return ( block.drop_columns( @@ -183,8 +182,8 @@ def _interpolate_column( # Note, this method may block, notnull = block.apply_unary_op(column, ops.notnull_op) - block, masked_offsets = block.apply_binary_op( - x_values, notnull, ops.partial_arg3(ops.where_op, None) + block, masked_offsets = block.project_expr( + ops.where_op.as_expr(x_values, notnull, ex.const(None)) ) block, previous_value = block.apply_window_op( @@ -271,25 +270,22 @@ def _interpolate_points_nearest( xpredict_id: str, ) -> typing.Tuple[blocks.Block, str]: """Interpolate by taking the y value of the nearest x value""" - block, left_diff = block.apply_binary_op(xpredict_id, x0_id, ops.sub_op) - block, right_diff = block.apply_binary_op(x1_id, xpredict_id, ops.sub_op) + left_diff = ops.sub_op.as_expr(xpredict_id, x0_id) + right_diff = ops.sub_op.as_expr(x1_id, xpredict_id) # If diffs equal, choose left - block, choose_left = block.apply_binary_op(left_diff, right_diff, ops.le_op) - block, choose_left = block.apply_unary_op( - choose_left, ops.partial_right(ops.fillna_op, False) + choose_left = ops.fillna_op.as_expr( + ops.le_op.as_expr(left_diff, right_diff), ex.const(False) ) - block, nearest = block.apply_ternary_op(y0_id, choose_left, y1_id, ops.where_op) - - block, y0_exists = block.apply_unary_op(y0_id, ops.notnull_op) - block, y1_exists = block.apply_unary_op(y1_id, ops.notnull_op) - block, is_interpolation = block.apply_binary_op(y0_exists, y1_exists, ops.and_op) + nearest = ops.where_op.as_expr(y0_id, choose_left, y1_id) - block, prediction_id = block.apply_binary_op( - nearest, is_interpolation, ops.partial_arg3(ops.where_op, None) + is_interpolation = ops.and_op.as_expr( + ops.notnull_op.as_expr(y0_id), ops.notnull_op.as_expr(y1_id) ) - return block, prediction_id + return block.project_expr( + ops.where_op.as_expr(nearest, is_interpolation, ex.const(None)) + ) def _interpolate_points_ffill( @@ -302,11 +298,9 @@ def _interpolate_points_ffill( ) -> typing.Tuple[blocks.Block, str]: """Interpolates by using the preceding values""" # check for existance of y1, otherwise we are extrapolating instead of interpolating - block, y1_exists = block.apply_unary_op(y1_id, ops.notnull_op) - block, prediction_id = block.apply_binary_op( - y0_id, y1_exists, ops.partial_arg3(ops.where_op, None) + return block.project_expr( + ops.where_op.as_expr(y0_id, ops.notnull_op.as_expr(y1_id), ex.const(None)) ) - return block, prediction_id def drop_duplicates( @@ -519,9 +513,7 @@ def nsmallest( agg_ops.rank_op, window_spec=windows.WindowSpec(ordering=tuple(order_refs)), ) - block, condition = block.apply_unary_op( - counter, ops.partial_right(ops.le_op, n) - ) + block, condition = block.project_expr(ops.le_op.as_expr(counter, ex.const(n))) block = block.filter(condition) return block.drop_columns([counter, condition]) @@ -551,9 +543,7 @@ def nlargest( agg_ops.rank_op, window_spec=windows.WindowSpec(ordering=tuple(order_refs)), ) - block, condition = block.apply_unary_op( - counter, ops.partial_right(ops.le_op, n) - ) + block, condition = block.project_expr(ops.le_op.as_expr(counter, ex.const(n))) block = block.filter(condition) return block.drop_columns([counter, condition]) @@ -641,7 +631,7 @@ def kurt( def _mean_delta_to_power( block: blocks.Block, - n_power, + n_power: int, column_ids: typing.Sequence[str], grouping_column_ids: typing.Sequence[str], ) -> typing.Tuple[blocks.Block, typing.Sequence[str]]: @@ -649,11 +639,10 @@ def _mean_delta_to_power( window = windows.WindowSpec(grouping_keys=tuple(grouping_column_ids)) block, mean_ids = block.multi_apply_window_op(column_ids, agg_ops.mean_op, window) delta_ids = [] - cube_op = ops.partial_right(ops.pow_op, n_power) for val_id, mean_val_id in zip(column_ids, mean_ids): - block, delta_id = block.apply_binary_op(val_id, mean_val_id, ops.sub_op) - block, delta_power_id = block.apply_unary_op(delta_id, cube_op) - block = block.drop_columns([delta_id]) + delta = ops.sub_op.as_expr(val_id, mean_val_id) + delta_power = ops.pow_op.as_expr(delta, ex.const(n_power)) + block, delta_power_id = block.project_expr(delta_power) delta_ids.append(delta_power_id) return block, delta_ids @@ -664,31 +653,26 @@ def _skew_from_moments_and_count( # Calculate skew using count, third moment and population variance # See G1 estimator: # https://en.wikipedia.org/wiki/Skewness#Sample_skewness - block, denominator_id = block.apply_unary_op( - moment2_id, ops.partial_right(ops.unsafe_pow_op, 3 / 2) - ) - block, base_id = block.apply_binary_op(moment3_id, denominator_id, ops.div_op) - block, countminus1_id = block.apply_unary_op( - count_id, ops.partial_right(ops.sub_op, 1) - ) - block, countminus2_id = block.apply_unary_op( - count_id, ops.partial_right(ops.sub_op, 2) - ) - block, adjustment_id = block.apply_binary_op(count_id, countminus1_id, ops.mul_op) - block, adjustment_id = block.apply_unary_op( - adjustment_id, ops.partial_right(ops.unsafe_pow_op, 1 / 2) + moments_estimator = ops.div_op.as_expr( + moment3_id, ops.pow_op.as_expr(moment2_id, ex.const(3 / 2)) ) - block, adjustment_id = block.apply_binary_op( - adjustment_id, countminus2_id, ops.div_op + + countminus1 = ops.sub_op.as_expr(count_id, ex.const(1)) + countminus2 = ops.sub_op.as_expr(count_id, ex.const(2)) + adjustment = ops.div_op.as_expr( + ops.unsafe_pow_op.as_expr( + ops.mul_op.as_expr(count_id, countminus1), ex.const(1 / 2) + ), + countminus2, ) - block, skew_id = block.apply_binary_op(base_id, adjustment_id, ops.mul_op) + + skew = ops.mul_op.as_expr(moments_estimator, adjustment) # Need to produce NA if have less than 3 data points - block, na_cond_id = block.apply_unary_op(count_id, ops.partial_right(ops.ge_op, 3)) - block, skew_id = block.apply_binary_op( - skew_id, na_cond_id, ops.partial_arg3(ops.where_op, None) + cleaned_skew = ops.where_op.as_expr( + skew, ops.ge_op.as_expr(count_id, ex.const(3)), ex.const(None) ) - return block, skew_id + return block.project_expr(cleaned_skew) def _kurt_from_moments_and_count( @@ -701,49 +685,42 @@ def _kurt_from_moments_and_count( # adjustment = 3 * (count - 1) ** 2 / ((count - 2) * (count - 3)) # kurtosis = (numerator / denominator) - adjustment - # Numerator - block, countminus1_id = block.apply_unary_op( - count_id, ops.partial_right(ops.sub_op, 1) - ) - block, countplus1_id = block.apply_unary_op( - count_id, ops.partial_right(ops.add_op, 1) + numerator = ops.mul_op.as_expr( + moment4_id, + ops.mul_op.as_expr( + ops.sub_op.as_expr(count_id, ex.const(1)), + ops.add_op.as_expr(count_id, ex.const(1)), + ), ) - block, num_adj = block.apply_binary_op(countplus1_id, countminus1_id, ops.mul_op) - block, numerator_id = block.apply_binary_op(moment4_id, num_adj, ops.mul_op) # Denominator - block, countminus2_id = block.apply_unary_op( - count_id, ops.partial_right(ops.sub_op, 2) - ) - block, countminus3_id = block.apply_unary_op( - count_id, ops.partial_right(ops.sub_op, 3) - ) - block, denom_adj = block.apply_binary_op(countminus2_id, countminus3_id, ops.mul_op) - block, popvar_squared = block.apply_unary_op( - moment2_id, ops.partial_right(ops.unsafe_pow_op, 2) + countminus2 = ops.sub_op.as_expr(count_id, ex.const(2)) + countminus3 = ops.sub_op.as_expr(count_id, ex.const(3)) + + # Denominator + denominator = ops.mul_op.as_expr( + ops.unsafe_pow_op.as_expr(moment2_id, ex.const(2)), + ops.mul_op.as_expr(countminus2, countminus3), ) - block, denominator_id = block.apply_binary_op(popvar_squared, denom_adj, ops.mul_op) # Adjustment - block, countminus1_square = block.apply_unary_op( - countminus1_id, ops.partial_right(ops.unsafe_pow_op, 2) - ) - block, adj_num = block.apply_unary_op( - countminus1_square, ops.partial_right(ops.mul_op, 3) + adj_num = ops.mul_op.as_expr( + ops.unsafe_pow_op.as_expr( + ops.sub_op.as_expr(count_id, ex.const(1)), ex.const(2) + ), + ex.const(3), ) - block, adj_denom = block.apply_binary_op(countminus2_id, countminus3_id, ops.mul_op) - block, adjustment_id = block.apply_binary_op(adj_num, adj_denom, ops.div_op) + adj_denom = ops.mul_op.as_expr(countminus2, countminus3) + adjustment = ops.div_op.as_expr(adj_num, adj_denom) # Combine - block, base_id = block.apply_binary_op(numerator_id, denominator_id, ops.div_op) - block, kurt_id = block.apply_binary_op(base_id, adjustment_id, ops.sub_op) + kurt = ops.sub_op.as_expr(ops.div_op.as_expr(numerator, denominator), adjustment) # Need to produce NA if have less than 4 data points - block, na_cond_id = block.apply_unary_op(count_id, ops.partial_right(ops.ge_op, 4)) - block, kurt_id = block.apply_binary_op( - kurt_id, na_cond_id, ops.partial_arg3(ops.where_op, None) + cleaned_kurt = ops.where_op.as_expr( + kurt, ops.ge_op.as_expr(count_id, ex.const(4)), ex.const(None) ) - return block, kurt_id + return block.project_expr(cleaned_kurt) def align( diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index cf1a8cb7c0f..1960def0d5c 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -35,6 +35,7 @@ import bigframes._config.sampling_options as sampling_options import bigframes.constants as constants import bigframes.core as core +import bigframes.core.expression as ex import bigframes.core.guid as guid import bigframes.core.indexes as indexes import bigframes.core.joins.name_resolution as join_names @@ -662,23 +663,32 @@ def with_index_labels(self, value: typing.Sequence[Label]) -> Block: index_labels=tuple(value), ) - def apply_unary_op( - self, column: str, op: ops.UnaryOp, result_label: Label = None + def project_expr( + self, expr: ex.Expression, label: Label = None ) -> typing.Tuple[Block, str]: """ - Apply a unary op to the block. Creates a new column to store the result. + Apply a scalar expression to the block. Creates a new column to store the result. """ # TODO(tbergeron): handle labels safely so callers don't need to result_id = guid.generate_guid() - expr = self._expr.project(op.as_expr(column), result_id) + array_val = self._expr.project(expr, result_id) block = Block( - expr, + array_val, index_columns=self.index_columns, - column_labels=[*self.column_labels, result_label], + column_labels=[*self.column_labels, label], index_labels=self.index.names, ) return (block, result_id) + def apply_unary_op( + self, column: str, op: ops.UnaryOp, result_label: Label = None + ) -> typing.Tuple[Block, str]: + """ + Apply a unary op to the block. Creates a new column to store the result. + """ + expr = op.as_expr(column) + return self.project_expr(expr, result_label) + def apply_binary_op( self, left_column_id: str, @@ -686,17 +696,8 @@ def apply_binary_op( op: ops.BinaryOp, result_label: Label = None, ) -> typing.Tuple[Block, str]: - result_id = guid.generate_guid() - expr = self._expr.project( - op.as_expr(left_column_id, right_column_id), result_id - ) - block = Block( - expr, - index_columns=self.index_columns, - column_labels=[*self.column_labels, result_label], - index_labels=self.index.names, - ) - return (block, result_id) + expr = op.as_expr(left_column_id, right_column_id) + return self.project_expr(expr, result_label) def apply_ternary_op( self, @@ -706,15 +707,8 @@ def apply_ternary_op( op: ops.TernaryOp, result_label: Label = None, ) -> typing.Tuple[Block, str]: - result_id = guid.generate_guid() - expr = self._expr.project(op.as_expr(col_id_1, col_id_2, col_id_3), result_id) - block = Block( - expr, - index_columns=self.index_columns, - column_labels=[*self.column_labels, result_label], - index_labels=self.index.names, - ) - return (block, result_id) + expr = op.as_expr(col_id_1, col_id_2, col_id_3) + return self.project_expr(expr, result_label) def multi_apply_window_op( self, @@ -1152,43 +1146,37 @@ def _forward_slice(self, start: int = 0, stop=None, step: int = 1): conditions = [] if start != 0: if start > 0: - op = ops.partial_right(ops.ge_op, start) assert positive_offsets - block, start_cond = block.apply_unary_op(positive_offsets, op) + conditions.append(ops.ge_op.as_expr(positive_offsets, ex.const(start))) else: - op = ops.partial_right(ops.le_op, -start - 1) assert negative_offsets - block, start_cond = block.apply_unary_op(negative_offsets, op) - conditions.append(start_cond) + conditions.append( + ops.le_op.as_expr(negative_offsets, ex.const(-start - 1)) + ) if stop is not None: if stop >= 0: - op = ops.partial_right(ops.lt_op, stop) assert positive_offsets - block, stop_cond = block.apply_unary_op(positive_offsets, op) + conditions.append(ops.lt_op.as_expr(positive_offsets, ex.const(stop))) else: - op = ops.partial_right(ops.gt_op, -stop - 1) assert negative_offsets - block, stop_cond = block.apply_unary_op(negative_offsets, op) - conditions.append(stop_cond) - + conditions.append( + ops.gt_op.as_expr(negative_offsets, ex.const(-stop - 1)) + ) if step > 1: - op = ops.partial_right(ops.mod_op, step) if start >= 0: - op = ops.partial_right(ops.sub_op, start) assert positive_offsets - block, start_diff = block.apply_unary_op(positive_offsets, op) + start_diff = ops.sub_op.as_expr(positive_offsets, ex.const(start)) else: - op = ops.partial_right(ops.sub_op, -start + 1) assert negative_offsets - block, start_diff = block.apply_unary_op(negative_offsets, op) - modulo_op = ops.partial_right(ops.mod_op, step) - block, mod = block.apply_unary_op(start_diff, modulo_op) - is_zero_op = ops.partial_right(ops.eq_op, 0) - block, step_cond = block.apply_unary_op(mod, is_zero_op) + start_diff = ops.sub_op.as_expr(negative_offsets, ex.const(-start + 1)) + step_cond = ops.eq_op.as_expr( + ops.mod_op.as_expr(start_diff, ex.const(step)), ex.const(0) + ) conditions.append(step_cond) for cond in conditions: - block = block.filter(cond) + block, cond_id = block.project_expr(cond) + block = block.filter(cond_id) return block.select_columns(self.value_columns) @@ -1238,13 +1226,12 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - expr = expr.project( - expression=ops.AsTypeOp(to_type="string").as_expr(index_col), - output_id=index_col, + add_prefix = ops.add_op.as_expr( + ex.const(prefix), ops.AsTypeOp(to_type="string").as_expr(index_col) ) - prefix_op = ops.ApplyLeft(base_op=ops.add_op, left_scalar=prefix) expr = expr.project( - expression=prefix_op.as_expr(index_col), output_id=index_col + expression=add_prefix, + output_id=index_col, ) return Block( expr, @@ -1262,13 +1249,12 @@ def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - expr = expr.project( - expression=ops.AsTypeOp(to_type="string").as_expr(index_col), - output_id=index_col, + add_suffix = ops.add_op.as_expr( + ops.AsTypeOp(to_type="string").as_expr(index_col), ex.const(suffix) ) - prefix_op = ops.ApplyRight(base_op=ops.add_op, right_scalar=suffix) expr = expr.project( - expression=prefix_op.as_expr(index_col), output_id=index_col + expression=add_suffix, + output_id=index_col, ) return Block( expr, @@ -1468,28 +1454,23 @@ def _create_pivot_column_index( def _create_pivot_col( block: Block, columns: typing.Sequence[str], value_col: str, value ) -> typing.Tuple[Block, str]: - cond_id = "" + condition: typing.Optional[ex.Expression] = None nlevels = len(columns) for i in range(len(columns)): uvalue_level = value[i] if nlevels > 1 else value if pd.isna(uvalue_level): - block, eq_id = block.apply_unary_op( - columns[i], - ops.isnull_op, - ) + equality = ops.isnull_op.as_expr(columns[i]) else: - block, eq_id = block.apply_unary_op( - columns[i], ops.partial_right(ops.eq_op, uvalue_level) - ) - if cond_id: - block, cond_id = block.apply_binary_op(eq_id, cond_id, ops.and_op) + equality = ops.eq_op.as_expr(columns[i], ex.const(uvalue_level)) + if condition is not None: + condition = ops.and_op.as_expr(equality, condition) else: - cond_id = eq_id - block, masked_id = block.apply_binary_op( - value_col, cond_id, ops.partial_arg3(ops.where_op, None) - ) + condition = equality - return block, masked_id + assert condition is not None + return block.project_expr( + ops.where_op.as_expr(value_col, condition, ex.const(None)) + ) def _get_unique_values( self, columns: Sequence[str], max_unique_values: int diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 2331d3aa289..115242e7223 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -28,7 +28,6 @@ import bigframes.constants as constants import bigframes.core.expression as expressions import bigframes.dtypes -import bigframes.dtypes as dtypes import bigframes.operations as ops _ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) @@ -65,6 +64,8 @@ def _( expression: expressions.ScalarConstantExpression, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: + if pd.isnull(expression.value): # type: ignore + return ibis.null() return ibis.literal(expression.value) @compile_expression.register @@ -1132,38 +1133,6 @@ def clip_op( ) -# Composition Ops -@scalar_op_compiler.register_unary_op(ops.ApplyRight, pass_op=True) -def apply_right(input: ibis_types.Value, op: ops.ApplyRight): - right = dtypes.literal_to_ibis_scalar(op.right_scalar, validate=False) - return scalar_op_compiler.compile_row_op(op.base_op, (input, right)) - - -@scalar_op_compiler.register_unary_op(ops.ApplyLeft, pass_op=True) -def apply_left(input: ibis_types.Value, op: ops.ApplyLeft): - left = dtypes.literal_to_ibis_scalar(op.left_scalar, validate=False) - return scalar_op_compiler.compile_row_op(op.base_op, (left, input)) - - -@scalar_op_compiler.register_binary_op(ops.ReverseArgsOp, pass_op=True) -def apply_reversed( - input1: ibis_types.Value, input2: ibis_types.Value, op: ops.ReverseArgsOp -): - return scalar_op_compiler.compile_row_op(op.base_op, (input2, input1)) - - -@scalar_op_compiler.register_binary_op(ops.ApplyArg1, pass_op=True) -def apply_arg1(input1: ibis_types.Value, input2: ibis_types.Value, op: ops.ApplyArg1): - arg1 = dtypes.literal_to_ibis_scalar(op.scalar, validate=False) - return scalar_op_compiler.compile_row_op(op.base_op, (arg1, input1, input2)) - - -@scalar_op_compiler.register_binary_op(ops.ApplyArg3, pass_op=True) -def apply_arg3(input1: ibis_types.Value, input2: ibis_types.Value, op: ops.ApplyArg3): - arg3 = dtypes.literal_to_ibis_scalar(op.scalar, validate=False) - return scalar_op_compiler.compile_row_op(op.base_op, (input1, input2, arg3)) - - # Helpers def is_null(value) -> bool: # float NaN/inf should be treated as distinct from 'true' null values diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 2fb1ccb9883..59c3e595d9b 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -22,6 +22,14 @@ import bigframes.operations +def const(value: typing.Hashable) -> Expression: + return ScalarConstantExpression(value) + + +def free_var(id: str) -> Expression: + return UnboundVariableExpression(id) + + @dataclasses.dataclass(frozen=True) class Expression(abc.ABC): """An expression represents a computation taking N scalar inputs and producing a single output scalar.""" @@ -30,6 +38,9 @@ class Expression(abc.ABC): def unbound_variables(self) -> typing.Tuple[str, ...]: return () + def rename(self, name_mapping: dict[str, str]) -> Expression: + return self + @dataclasses.dataclass(frozen=True) class ScalarConstantExpression(Expression): @@ -49,6 +60,12 @@ class UnboundVariableExpression(Expression): def unbound_variables(self) -> typing.Tuple[str, ...]: return (self.id,) + def rename(self, name_mapping: dict[str, str]) -> Expression: + if self.id in name_mapping: + return UnboundVariableExpression(name_mapping[self.id]) + else: + return self + @dataclasses.dataclass(frozen=True) class OpExpression(Expression): @@ -67,3 +84,8 @@ def unbound_variables(self) -> typing.Tuple[str, ...]: map(lambda x: x.unbound_variables, self.inputs) ) ) + + def rename(self, name_mapping: dict[str, str]) -> Expression: + return OpExpression( + self.op, tuple(input.rename(name_mapping) for input in self.inputs) + ) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 66ba9016491..ab6b15e7b94 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -28,7 +28,6 @@ import bigframes.core.window as windows import bigframes.dataframe as df import bigframes.dtypes as dtypes -import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.series as series import third_party.bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby @@ -540,10 +539,13 @@ def cummin(self, *args, **kwargs) -> series.Series: ) def cumcount(self, *args, **kwargs) -> series.Series: - return self._apply_window_op( - agg_ops.rank_op, - discard_name=True, - )._apply_unary_op(ops.partial_right(ops.sub_op, 1)) + return ( + self._apply_window_op( + agg_ops.rank_op, + discard_name=True, + ) + - 1 + ) def shift(self, periods=1) -> series.Series: """Shift index by desired number of periods.""" diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 12a1303d29d..6998d0e974d 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -22,6 +22,7 @@ import bigframes.constants as constants import bigframes.core.blocks +import bigframes.core.expression as ex import bigframes.core.guid as guid import bigframes.core.indexes as indexes import bigframes.core.scalar @@ -63,17 +64,14 @@ def __setitem__(self, key, value) -> None: index_column = block.index_columns[0] # if index == key return value else value_colum - block, insert_cond = block.apply_unary_op( - index_column, ops.partial_right(ops.eq_op, key) - ) - block, result_id = block.apply_binary_op( - insert_cond, - self._series._value_column, - ops.partial_arg1(ops.where_op, value), - ) - block = block.copy_values(result_id, value_column).drop_columns( - [insert_cond, result_id] + block, result_id = block.project_expr( + ops.where_op.as_expr( + ex.const(value), + ops.eq_op.as_expr(index_column, ex.const(key)), + self._series._value_column, + ) ) + block = block.copy_values(result_id, value_column).drop_columns([result_id]) self._series._set_block(block) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 6602170b5f3..c02fdbb12dc 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -26,6 +26,7 @@ import bigframes.core as core import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks +import bigframes.core.expression as ex import bigframes.core.joins as joining import bigframes.core.ordering as order import bigframes.core.utils as utils @@ -186,7 +187,7 @@ def astype( ) -> Index: if self.nlevels > 1: raise TypeError("Multiindex does not support 'astype'") - return self._apply_unary_op(ops.AsTypeOp(to_type=dtype)) + return self._apply_unary_expr(ops.AsTypeOp(to_type=dtype).as_expr("arg")) def all(self) -> bool: if self.nlevels > 1: @@ -261,7 +262,7 @@ def value_counts( def fillna(self, value=None) -> Index: if self.nlevels > 1: raise TypeError("Multiindex does not support 'fillna'") - return self._apply_unary_op(ops.partial_right(ops.fillna_op, value)) + return self._apply_unary_expr(ops.fillna_op.as_expr("arg", ex.const(value))) def rename(self, name: Union[str, Sequence[str]]) -> Index: names = [name] if isinstance(name, str) else list(name) @@ -284,8 +285,8 @@ def drop( inverse_condition_id, ops.invert_op ) else: - block, condition_id = block.apply_unary_op( - level_id, ops.partial_right(ops.ne_op, labels) + block, condition_id = block.project_expr( + ops.ne_op.as_expr(level_id, ex.const(labels)) ) block = block.filter(condition_id, keep_null=True) block = block.drop_columns([condition_id]) @@ -308,19 +309,23 @@ def isin(self, values) -> Index: f"isin(), you passed a [{type(values).__name__}]" ) - return self._apply_unary_op( - ops.IsInOp(values=tuple(values), match_nulls=True) + return self._apply_unary_expr( + ops.IsInOp(values=tuple(values), match_nulls=True).as_expr("arg") ).fillna(value=False) - def _apply_unary_op( + def _apply_unary_expr( self, - op: ops.UnaryOp, + op: ex.Expression, ) -> Index: """Applies a unary operator to the index.""" + if len(op.unbound_variables) != 1: + raise ValueError("Expression must have exactly 1 unbound variable.") + unbound_variable = op.unbound_variables[0] + block = self._block result_ids = [] for col in self._block.index_columns: - block, result_id = block.apply_unary_op(col, op) + block, result_id = block.project_expr(op.rename({unbound_variable: col})) result_ids.append(result_id) block = block.set_index(result_ids, index_labels=self._block.index_labels) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index d9cc99a0366..cadd8e51453 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -20,6 +20,7 @@ import bigframes.constants as constants import bigframes.core as core +import bigframes.core.expression as ex import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.dataframe @@ -165,7 +166,7 @@ def qcut( ordering=(order.OrderingColumnReference(x._value_column),), ), ) - block, result = block.apply_binary_op( - result, nullity_id, ops.partial_arg3(ops.where_op, None), result_label=label + block, result = block.project_expr( + ops.where_op.as_expr(result, nullity_id, ex.const(None)), label=label ) return bigframes.series.Series(block.select_column(result)) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1f039904f0d..1288117395b 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -47,6 +47,7 @@ from bigframes.core import log_adapter import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks +import bigframes.core.expression as ex import bigframes.core.groupby as groupby import bigframes.core.guid import bigframes.core.indexers as indexers @@ -656,25 +657,34 @@ def _apply_binop( op, axis: str | int = "columns", how: str = "outer", + reverse: bool = False, ): if isinstance(other, (float, int)): - return self._apply_scalar_binop(other, op) + return self._apply_scalar_binop(other, op, reverse=reverse) elif isinstance(other, bigframes.series.Series): - return self._apply_series_binop(other, op, axis=axis, how=how) + return self._apply_series_binop( + other, op, axis=axis, how=how, reverse=reverse + ) elif isinstance(other, DataFrame): - return self._apply_dataframe_binop(other, op, how=how) + return self._apply_dataframe_binop(other, op, how=how, reverse=reverse) raise NotImplementedError( f"binary operation is not implemented on the second operand of type {type(other).__name__}." f"{constants.FEEDBACK_LINK}" ) - def _apply_scalar_binop(self, other: float | int, op: ops.BinaryOp) -> DataFrame: + def _apply_scalar_binop( + self, other: float | int, op: ops.BinaryOp, reverse: bool = False + ) -> DataFrame: block = self._block - partial_op = ops.ApplyRight(base_op=op, right_scalar=other) for column_id, label in zip( self._block.value_columns, self._block.column_labels ): - block, _ = block.apply_unary_op(column_id, partial_op, result_label=label) + expr = ( + op.as_expr(ex.const(other), column_id) + if reverse + else op.as_expr(column_id, ex.const(other)) + ) + block, _ = block.project_expr(expr, label) block = block.drop_columns([column_id]) return DataFrame(block) @@ -684,6 +694,7 @@ def _apply_series_binop( op: ops.BinaryOp, axis: str | int = "columns", how: str = "outer", + reverse: bool = False, ) -> DataFrame: if axis not in ("columns", "index", 0, 1): raise ValueError(f"Invalid input: axis {axis}.") @@ -703,12 +714,13 @@ def _apply_series_binop( for column_id, label in zip( self._block.value_columns, self._block.column_labels ): - block, _ = block.apply_binary_op( - get_column_left[column_id], - series_col, - op, - result_label=label, + self_col = get_column_left[column_id] + expr = ( + op.as_expr(series_col, self_col) + if reverse + else op.as_expr(self_col, series_col) ) + block, _ = block.project_expr(expr, label) block = block.drop_columns([get_column_left[column_id]]) block = block.drop_columns([series_col]) @@ -716,7 +728,11 @@ def _apply_series_binop( return DataFrame(block) def _apply_dataframe_binop( - self, other: DataFrame, op: ops.BinaryOp, how: str = "outer" + self, + other: DataFrame, + op: ops.BinaryOp, + how: str = "outer", + reverse: bool = False, ) -> DataFrame: # Join rows joined_index, (get_column_left, get_column_right) = self._block.index.join( @@ -738,31 +754,32 @@ def _apply_dataframe_binop( for left_index, right_index in column_indices: if left_index >= 0 and right_index >= 0: # -1 indices indicate missing - left_col_id = self._block.value_columns[left_index] - right_col_id = other._block.value_columns[right_index] - block, result_col_id = block.apply_binary_op( - get_column_left[left_col_id], - get_column_right[right_col_id], - op, + self_col_id = get_column_left[self._block.value_columns[left_index]] + other_col_id = get_column_right[other._block.value_columns[right_index]] + expr = ( + op.as_expr(other_col_id, self_col_id) + if reverse + else op.as_expr(self_col_id, other_col_id) ) - binop_result_ids.append(result_col_id) elif left_index >= 0: - left_col_id = self._block.value_columns[left_index] - block, result_col_id = block.apply_unary_op( - get_column_left[left_col_id], - ops.partial_right(op, None), + self_col_id = get_column_left[self._block.value_columns[left_index]] + expr = ( + op.as_expr(ex.const(None), self_col_id) + if reverse + else op.as_expr(self_col_id, ex.const(None)) ) - binop_result_ids.append(result_col_id) elif right_index >= 0: - right_col_id = other._block.value_columns[right_index] - block, result_col_id = block.apply_unary_op( - get_column_right[right_col_id], - ops.partial_left(op, None), + other_col_id = get_column_right[other._block.value_columns[right_index]] + expr = ( + op.as_expr(other_col_id, ex.const(None)) + if reverse + else op.as_expr(ex.const(None), other_col_id) ) - binop_result_ids.append(result_col_id) else: # Should not be possible raise ValueError("No right or left index.") + block, result_col_id = block.project_expr(expr) + binop_result_ids.append(result_col_id) block = block.select_columns(binop_result_ids).with_column_labels(columns) return DataFrame(block) @@ -822,7 +839,7 @@ def rsub( other: float | int | bigframes.series.Series | DataFrame, axis: str | int = "columns", ) -> DataFrame: - return self._apply_binop(other, ops.reverse(ops.sub_op), axis=axis) + return self._apply_binop(other, ops.sub_op, axis=axis, reverse=True) __rsub__ = rsub @@ -849,7 +866,7 @@ def rtruediv( other: float | int | bigframes.series.Series | DataFrame, axis: str | int = "columns", ) -> DataFrame: - return self._apply_binop(other, ops.reverse(ops.div_op), axis=axis) + return self._apply_binop(other, ops.div_op, axis=axis, reverse=True) __rtruediv__ = rdiv = rtruediv @@ -867,7 +884,7 @@ def rfloordiv( other: float | int | bigframes.series.Series | DataFrame, axis: str | int = "columns", ) -> DataFrame: - return self._apply_binop(other, ops.reverse(ops.floordiv_op), axis=axis) + return self._apply_binop(other, ops.floordiv_op, axis=axis, reverse=True) __rfloordiv__ = rfloordiv @@ -875,7 +892,7 @@ def mod(self, other: int | bigframes.series.Series | DataFrame, axis: str | int return self._apply_binop(other, ops.mod_op, axis=axis) def rmod(self, other: int | bigframes.series.Series | DataFrame, axis: str | int = "columns") -> DataFrame: # type: ignore - return self._apply_binop(other, ops.reverse(ops.mod_op), axis=axis) + return self._apply_binop(other, ops.mod_op, axis=axis, reverse=True) __mod__ = mod @@ -889,7 +906,7 @@ def pow( def rpow( self, other: int | bigframes.series.Series, axis: str | int = "columns" ) -> DataFrame: - return self._apply_binop(other, ops.reverse(ops.pow_op), axis=axis) + return self._apply_binop(other, ops.pow_op, axis=axis, reverse=True) __pow__ = pow @@ -1101,8 +1118,8 @@ def drop( condition_id = None for i, idx in enumerate(index): level_id = self._resolve_levels(i)[0] - block, condition_id_cur = block.apply_unary_op( - level_id, ops.partial_right(ops.ne_op, idx) + block, condition_id_cur = block.project_expr( + ops.ne_op.as_expr(level_id, ex.const(idx)) ) if condition_id: block, condition_id = block.apply_binary_op( @@ -1122,8 +1139,8 @@ def drop( elif isinstance(index, indexes.Index): return self._drop_by_index(index) else: - block, condition_id = block.apply_unary_op( - level_id, ops.partial_right(ops.ne_op, index) + block, condition_id = block.project_expr( + ops.ne_op.as_expr(level_id, ex.const(index)) ) block = block.filter(condition_id, keep_null=True).select_columns( self._block.value_columns @@ -3031,7 +3048,7 @@ def __array_ufunc__( if inputs[0] is self: return self._apply_binop(inputs[1], binop) else: - return self._apply_binop(inputs[0], ops.reverse(binop)) + return self._apply_binop(inputs[0], binop, reverse=True) return NotImplemented diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 39ca52394ea..9737df94f9e 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -49,11 +49,13 @@ def name(self) -> str: def arguments(self) -> int: return 1 - def as_expr(self, input_id: str) -> bigframes.core.expression.Expression: + def as_expr( + self, input_id: typing.Union[str, bigframes.core.expression.Expression] = "arg" + ) -> bigframes.core.expression.Expression: import bigframes.core.expression return bigframes.core.expression.OpExpression( - self, (bigframes.core.expression.UnboundVariableExpression(input_id),) + self, (_convert_expr_input(input_id),) ) @@ -68,15 +70,17 @@ def arguments(self) -> int: return 2 def as_expr( - self, left_input: str, right_input: str + self, + left_input: typing.Union[str, bigframes.core.expression.Expression] = "arg1", + right_input: typing.Union[str, bigframes.core.expression.Expression] = "arg2", ) -> bigframes.core.expression.Expression: import bigframes.core.expression return bigframes.core.expression.OpExpression( self, ( - bigframes.core.expression.UnboundVariableExpression(left_input), - bigframes.core.expression.UnboundVariableExpression(right_input), + _convert_expr_input(left_input), + _convert_expr_input(right_input), ), ) @@ -92,20 +96,35 @@ def arguments(self) -> int: return 3 def as_expr( - self, input1: str, input2: str, input3: str + self, + input1: typing.Union[str, bigframes.core.expression.Expression] = "arg1", + input2: typing.Union[str, bigframes.core.expression.Expression] = "arg2", + input3: typing.Union[str, bigframes.core.expression.Expression] = "arg3", ) -> bigframes.core.expression.Expression: import bigframes.core.expression return bigframes.core.expression.OpExpression( self, ( - bigframes.core.expression.UnboundVariableExpression(input1), - bigframes.core.expression.UnboundVariableExpression(input2), - bigframes.core.expression.UnboundVariableExpression(input3), + _convert_expr_input(input1), + _convert_expr_input(input2), + _convert_expr_input(input3), ), ) +def _convert_expr_input( + input: typing.Union[str, bigframes.core.expression.Expression] +) -> bigframes.core.expression.Expression: + """Allows creating free variables with just a string""" + import bigframes.core.expression + + if isinstance(input, str): + return bigframes.core.expression.UnboundVariableExpression(input) + else: + return input + + # Operation Factories def create_unary_op(name: str) -> UnaryOp: return dataclasses.make_dataclass( @@ -309,63 +328,6 @@ class MapOp(UnaryOp): mappings: typing.Tuple[typing.Tuple[typing.Hashable, typing.Hashable], ...] -# Operation Composition -# Meta-ops that do partial application or parameter remapping -# Subject to change, may convert to explicit tree -@dataclasses.dataclass(frozen=True) -class ApplyRight(UnaryOp): - name: typing.ClassVar[str] = "apply_right" - base_op: BinaryOp - right_scalar: typing.Any - - -@dataclasses.dataclass(frozen=True) -class ApplyLeft(UnaryOp): - name: typing.ClassVar[str] = "apply_left" - base_op: BinaryOp - left_scalar: typing.Any - - -@dataclasses.dataclass(frozen=True) -class ApplyArg1(BinaryOp): - name: typing.ClassVar[str] = "apply_arg1" - base_op: TernaryOp - scalar: typing.Any - - -@dataclasses.dataclass(frozen=True) -class ApplyArg3(BinaryOp): - name: typing.ClassVar[str] = "apply_arg3" - base_op: TernaryOp - scalar: typing.Any - - -@dataclasses.dataclass(frozen=True) -class ReverseArgsOp(BinaryOp): - name: typing.ClassVar[str] = "apply_reverse" - base_op: BinaryOp - - -def partial_left(op: BinaryOp, scalar: typing.Any) -> UnaryOp: - return ApplyLeft(base_op=op, left_scalar=scalar) - - -def partial_right(op: BinaryOp, scalar: typing.Any) -> UnaryOp: - return ApplyRight(base_op=op, right_scalar=scalar) - - -def partial_arg1(op: TernaryOp, scalar: typing.Any) -> BinaryOp: - return ApplyArg1(base_op=op, scalar=scalar) - - -def partial_arg3(op: TernaryOp, scalar: typing.Any) -> BinaryOp: - return ApplyArg3(base_op=op, scalar=scalar) - - -def reverse(op: BinaryOp) -> BinaryOp: - return ReverseArgsOp(base_op=op) - - # Binary Ops fillna_op = create_binary_op(name="fillna") cliplower_op = create_binary_op(name="clip_lower") diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 8989255f7ed..077815a9d65 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -20,6 +20,7 @@ import bigframes.constants as constants import bigframes.core.blocks as blocks +import bigframes.core.expression as ex import bigframes.core.scalar as scalars import bigframes.dtypes import bigframes.operations as ops @@ -136,6 +137,7 @@ def _apply_binary_op( other: typing.Any, op: ops.BinaryOp, alignment: typing.Literal["outer", "left"] = "outer", + reverse: bool = False, ) -> series.Series: """Applies a binary operator to the series and other.""" if isinstance(other, pd.Series): @@ -144,11 +146,7 @@ def _apply_binary_op( f"Pandas series not supported as operand. {constants.FEEDBACK_LINK}" ) if isinstance(other, series.Series): - (left, right, block) = self._align(other, how=alignment) - - block, result_id = block.apply_binary_op( - left, right, op, self._value_column - ) + (self_col, other_col, block) = self._align(other, how=alignment) name = self._name if ( @@ -157,13 +155,20 @@ def _apply_binary_op( and alignment == "outer" ): name = None - - return series.Series( - block.select_column(result_id).assign_label(result_id, name) + expr = op.as_expr( + other_col if reverse else self_col, self_col if reverse else other_col ) + block, result_id = block.project_expr(expr, name) + return series.Series(block.select_column(result_id)) + else: - partial_op = ops.ApplyRight(base_op=op, right_scalar=other) - return self._apply_unary_op(partial_op) + name = self._name + expr = op.as_expr( + ex.const(other) if reverse else self._value_column, + self._value_column if reverse else ex.const(other), + ) + block, result_id = self._block.project_expr(expr, name) + return series.Series(block.select_column(result_id)) def _apply_corr_aggregation(self, other: series.Series) -> float: (left, right, block) = self._align(other, how="outer") diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 0f060a23e8b..554acda202e 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -48,6 +48,7 @@ import bigframes._config as config import bigframes.constants as constants import bigframes.core.blocks +import bigframes.core.expression as ex import bigframes.core.global_session as global_session import bigframes.core.indexes import bigframes.core.reshape @@ -294,14 +295,13 @@ def _perform_get_dummies_block_operations( new_column_label = f"{column_label}{value}" if column_label == "": new_column_label = value - new_block, new_id = block.apply_unary_op( - column_id, ops.ApplyLeft(ops.eq_op, value) + new_block, new_id = block.project_expr( + ops.eq_op.as_expr(column_id, ex.const(value)) ) intermediate_col_ids.append(new_id) - block, _ = new_block.apply_unary_op( - new_id, - ops.ApplyRight(ops.fillna_op, False), - result_label=new_column_label, + block, _ = new_block.project_expr( + ops.fillna_op.as_expr(new_id, ex.const(False)), + label=new_column_label, ) if dummy_na: # dummy column name for na depends on the dtype diff --git a/bigframes/series.py b/bigframes/series.py index 7a4600a3245..2371aad780b 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -34,6 +34,7 @@ from bigframes.core import log_adapter import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks +import bigframes.core.expression as ex import bigframes.core.groupby as groupby import bigframes.core.indexers import bigframes.core.indexes as indexes @@ -188,8 +189,8 @@ def rename( # Will throw if value type isn't compatible with index type. block, const_id = block.create_constant(v, dtype=idx_dtype) - block, cond_id = block.apply_unary_op( - idx_id, ops.ApplyRight(base_op=ops.ne_op, right_scalar=k) + block, cond_id = block.project_expr( + ops.ne_op.as_expr(idx_id, ex.const(k)) ) block, new_idx_id = block.apply_ternary_op( idx_id, cond_id, const_id, ops.where_op @@ -342,8 +343,8 @@ def drop( inverse_condition_id, ops.invert_op ) else: - block, condition_id = block.apply_unary_op( - level_id, ops.partial_right(ops.ne_op, index) + block, condition_id = block.project_expr( + ops.ne_op.as_expr(level_id, ex.const(index)) ) block = block.filter(condition_id, keep_null=True) block = block.drop_columns([condition_id]) @@ -488,11 +489,8 @@ def _simple_replace(self, to_replace_list: typing.Sequence, value): block, cond = self._block.apply_unary_op( self._value_column, ops.IsInOp(tuple(to_replace_list)) ) - block, result_col = block.apply_binary_op( - cond, - self._value_column, - ops.partial_arg1(ops.where_op, value), - result_label=self.name, + block, result_col = block.project_expr( + ops.where_op.as_expr(ex.const(value), cond, self._value_column), self.name ) return Series(block.select_column(result_col)) @@ -605,7 +603,7 @@ def add(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.add_op) def radd(self, other: float | int | Series) -> Series: - return self._apply_binary_op(other, ops.reverse(ops.add_op)) + return self._apply_binary_op(other, ops.add_op, reverse=True) def __sub__(self, other: float | int | Series) -> Series: return self.sub(other) @@ -617,7 +615,7 @@ def sub(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.sub_op) def rsub(self, other: float | int | Series) -> Series: - return self._apply_binary_op(other, ops.reverse(ops.sub_op)) + return self._apply_binary_op(other, ops.sub_op, reverse=True) subtract = sub @@ -631,7 +629,7 @@ def mul(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.mul_op) def rmul(self, other: float | int | Series) -> Series: - return self._apply_binary_op(other, ops.reverse(ops.mul_op)) + return self._apply_binary_op(other, ops.mul_op, reverse=True) multiply = mul @@ -645,7 +643,7 @@ def truediv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.div_op) def rtruediv(self, other: float | int | Series) -> Series: - return self._apply_binary_op(other, ops.reverse(ops.div_op)) + return self._apply_binary_op(other, ops.div_op, reverse=True) div = truediv @@ -663,7 +661,7 @@ def floordiv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.floordiv_op) def rfloordiv(self, other: float | int | Series) -> Series: - return self._apply_binary_op(other, ops.reverse(ops.floordiv_op)) + return self._apply_binary_op(other, ops.floordiv_op, reverse=True) def __pow__(self, other: float | int | Series) -> Series: return self.pow(other) @@ -675,7 +673,7 @@ def pow(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.pow_op) def rpow(self, other: float | int | Series) -> Series: - return self._apply_binary_op(other, ops.reverse(ops.pow_op)) + return self._apply_binary_op(other, ops.pow_op, reverse=True) def __lt__(self, other: float | int | Series) -> Series: # type: ignore return self.lt(other) @@ -711,7 +709,7 @@ def mod(self, other) -> Series: # type: ignore return self._apply_binary_op(other, ops.mod_op) def rmod(self, other) -> Series: # type: ignore - return self._apply_binary_op(other, ops.reverse(ops.mod_op)) + return self._apply_binary_op(other, ops.mod_op, reverse=True) def divmod(self, other) -> Tuple[Series, Series]: # type: ignore # TODO(huanc): when self and other both has dtype int and other contains zeros, @@ -1503,7 +1501,7 @@ def __array_ufunc__( if inputs[0] is self: return self._apply_binary_op(inputs[1], binop) else: - return self._apply_binary_op(inputs[0], ops.reverse(binop)) + return self._apply_binary_op(inputs[0], binop, reverse=True) return NotImplemented From cd2c729ee8eaa538cac05773f0c4240fb9298570 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 16 Jan 2024 11:25:56 -0800 Subject: [PATCH 264/921] refactor: combine all projection nodes into single node type (#317) --- bigframes/core/__init__.py | 124 ++++++++++++++----- bigframes/core/blocks.py | 24 ++-- bigframes/core/compile/compiled.py | 107 +++++++--------- bigframes/core/compile/compiler.py | 26 +--- bigframes/core/compile/scalar_op_compiler.py | 16 +-- bigframes/core/expression.py | 25 +++- bigframes/core/indexes/index.py | 2 +- bigframes/core/nodes.py | 39 +----- tests/unit/test_core.py | 8 +- 9 files changed, 187 insertions(+), 184 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index e32977fbce5..8c08698b93e 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -22,7 +22,7 @@ import pandas import bigframes.core.compile as compiling -import bigframes.core.expression as expressions +import bigframes.core.expression as ex import bigframes.core.guid import bigframes.core.nodes as nodes from bigframes.core.ordering import OrderingColumnReference @@ -114,12 +114,6 @@ def row_count(self) -> ArrayValue: return ArrayValue(nodes.RowCountNode(child=self.node)) # Operations - - def drop_columns(self, columns: Iterable[str]) -> ArrayValue: - return ArrayValue( - nodes.DropColumnsNode(child=self.node, columns=tuple(columns)) - ) - def filter(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" return ArrayValue( @@ -140,21 +134,104 @@ def promote_offsets(self, col_id: str) -> ArrayValue: """ return ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)) - def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: - return ArrayValue( - nodes.SelectNode(child=self.node, column_ids=tuple(column_ids)) - ) - def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: """Append together multiple ArrayValue objects.""" return ArrayValue( nodes.ConcatNode(children=tuple([self.node, *[val.node for val in other]])) ) - def project(self, expression: expressions.Expression, output_id: str): + def project_to_id(self, expression: ex.Expression, output_id: str): + if output_id in self.column_ids: # Mutate case + exprs = [ + ((expression if (col_id == output_id) else ex.free_var(col_id)), col_id) + for col_id in self.column_ids + ] + else: # append case + self_projection = ( + (ex.free_var(col_id), col_id) for col_id in self.column_ids + ) + exprs = [*self_projection, (expression, output_id)] + return ArrayValue( + nodes.ProjectionNode( + child=self.node, + assignments=tuple(exprs), + ) + ) + + def assign(self, source_id: str, destination_id: str) -> ArrayValue: + if destination_id in self.column_ids: # Mutate case + exprs = [ + ( + ( + ex.free_var(source_id) + if (col_id == destination_id) + else ex.free_var(col_id) + ), + col_id, + ) + for col_id in self.column_ids + ] + else: # append case + self_projection = ( + (ex.free_var(col_id), col_id) for col_id in self.column_ids + ) + exprs = [*self_projection, (ex.free_var(source_id), destination_id)] + return ArrayValue( + nodes.ProjectionNode( + child=self.node, + assignments=tuple(exprs), + ) + ) + + def assign_constant( + self, + destination_id: str, + value: typing.Any, + dtype: typing.Optional[bigframes.dtypes.Dtype], + ) -> ArrayValue: + if destination_id in self.column_ids: # Mutate case + exprs = [ + ( + ( + ex.const(value, dtype) + if (col_id == destination_id) + else ex.free_var(col_id) + ), + col_id, + ) + for col_id in self.column_ids + ] + else: # append case + self_projection = ( + (ex.free_var(col_id), col_id) for col_id in self.column_ids + ) + exprs = [*self_projection, (ex.const(value, dtype), destination_id)] + return ArrayValue( + nodes.ProjectionNode( + child=self.node, + assignments=tuple(exprs), + ) + ) + + def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: + selections = ((ex.free_var(col_id), col_id) for col_id in column_ids) + return ArrayValue( + nodes.ProjectionNode( + child=self.node, + assignments=tuple(selections), + ) + ) + + def drop_columns(self, columns: Iterable[str]) -> ArrayValue: + new_projection = ( + (ex.free_var(col_id), col_id) + for col_id in self.column_ids + if col_id not in columns + ) return ArrayValue( nodes.ProjectionNode( - child=self.node, assignments=((expression, output_id),) + child=self.node, + assignments=tuple(new_projection), ) ) @@ -277,25 +354,6 @@ def unpivot( ) ) - def assign(self, source_id: str, destination_id: str) -> ArrayValue: - return ArrayValue( - nodes.AssignNode( - child=self.node, source_id=source_id, destination_id=destination_id - ) - ) - - def assign_constant( - self, - destination_id: str, - value: typing.Any, - dtype: typing.Optional[bigframes.dtypes.Dtype], - ) -> ArrayValue: - return ArrayValue( - nodes.AssignConstantNode( - child=self.node, destination_id=destination_id, value=value, dtype=dtype - ) - ) - def join( self, self_column_ids: typing.Sequence[str], diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 1960def0d5c..8c59f8106b1 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -671,7 +671,7 @@ def project_expr( """ # TODO(tbergeron): handle labels safely so callers don't need to result_id = guid.generate_guid() - array_val = self._expr.project(expr, result_id) + array_val = self._expr.project_to_id(expr, result_id) block = Block( array_val, index_columns=self.index_columns, @@ -1226,11 +1226,11 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - add_prefix = ops.add_op.as_expr( - ex.const(prefix), ops.AsTypeOp(to_type="string").as_expr(index_col) - ) - expr = expr.project( - expression=add_prefix, + expr = expr.project_to_id( + expression=ops.add_op.as_expr( + ex.const(prefix), + ops.AsTypeOp(to_type="string").as_expr(index_col), + ), output_id=index_col, ) return Block( @@ -1249,11 +1249,11 @@ def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: if axis_number == 0: expr = self._expr for index_col in self._index_columns: - add_suffix = ops.add_op.as_expr( - ops.AsTypeOp(to_type="string").as_expr(index_col), ex.const(suffix) - ) - expr = expr.project( - expression=add_suffix, + expr = expr.project_to_id( + expression=ops.add_op.as_expr( + ops.AsTypeOp(to_type="string").as_expr(index_col), + ex.const(suffix), + ), output_id=index_col, ) return Block( @@ -1557,7 +1557,7 @@ def merge( coalesced_ids = [] for left_id, right_id in zip(left_join_ids, right_join_ids): coalesced_id = guid.generate_guid() - joined_expr = joined_expr.project( + joined_expr = joined_expr.project_to_id( ops.coalesce_op.as_expr( get_column_left[left_id], get_column_right[right_id] ), diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 6a3db3f2bdf..2cab6fb95d5 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -26,9 +26,8 @@ import ibis.expr.types as ibis_types import pandas -import bigframes.constants as constants import bigframes.core.compile.scalar_op_compiler as op_compilers -import bigframes.core.expression as expressions +import bigframes.core.expression as ex import bigframes.core.guid from bigframes.core.ordering import ( encode_order_string, @@ -96,16 +95,6 @@ def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: else None ) - @abc.abstractmethod - def select_columns(self: T, column_ids: typing.Sequence[str]) -> T: - """Creates a new expression based on this expression with new columns.""" - ... - - def drop_columns(self: T, columns: Iterable[str]) -> T: - return self.select_columns( - [col for col in self.column_ids if col not in columns] - ) - @abc.abstractmethod def filter(self: T, predicate_id: str, keep_null: bool = False) -> T: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" @@ -152,40 +141,26 @@ def _reproject_to_table(self: T) -> T: """ ... - def project_expression( + def projection( self: T, - expression: expressions.Expression, - output_column_id: typing.Optional[str] = None, + expression_id_pairs: typing.Tuple[typing.Tuple[ex.Expression, str], ...], ) -> T: """Apply an expression to the ArrayValue and assign the output to a column.""" - result_id = ( - output_column_id or expression.unbound_variables[0] - ) # overwrite input if not output id provided - bindings = { - col: self._get_ibis_column(col) for col in expression.unbound_variables - } - value = op_compiler.compile_expression(expression, bindings).name(result_id) - return self._set_or_replace_by_id(result_id, value) + bindings = {col: self._get_ibis_column(col) for col in self.column_ids} + values = [ + op_compiler.compile_expression(expression, bindings).name(id) + for expression, id in expression_id_pairs + ] + result = self._select(tuple(values)) # type: ignore - def assign(self: T, source_id: str, destination_id: str) -> T: - return self._set_or_replace_by_id( - destination_id, self._get_ibis_column(source_id) - ) + # Need to reproject to convert ibis Scalar to ibis Column object + if any(exp_id[0].is_const for exp_id in expression_id_pairs): + result = result._reproject_to_table() + return result - def assign_constant( - self: T, - destination_id: str, - value: typing.Any, - dtype: typing.Optional[bigframes.dtypes.Dtype], - ) -> T: - # TODO(b/281587571): Solve scalar constant aggregation problem w/Ibis. - ibis_value = bigframes.dtypes.literal_to_ibis_scalar(value, dtype) - if ibis_value is None: - raise NotImplementedError( - f"Type not supported as scalar value {type(value)}. {constants.FEEDBACK_LINK}" - ) - expr = self._set_or_replace_by_id(destination_id, ibis_value) - return expr._reproject_to_table() + @abc.abstractmethod + def _select(self: T, values: typing.Tuple[ibis_types.Value]) -> T: + ... @abc.abstractmethod def _set_or_replace_by_id(self: T, id: str, new_value: ibis_types.Value) -> T: @@ -330,14 +305,6 @@ def _to_ibis_expr( table = table.filter(ibis.random() < ibis.literal(fraction)) return table - def select_columns(self, column_ids: typing.Sequence[str]) -> UnorderedIR: - """Creates a new expression based on this expression with new columns.""" - columns = [self._get_ibis_column(col_id) for col_id in column_ids] - builder = self.builder() - builder.columns = list(columns) - new_expr = builder.build() - return new_expr - def filter(self, predicate_id: str, keep_null: bool = False) -> UnorderedIR: condition = typing.cast( ibis_types.BooleanValue, self._get_ibis_column(predicate_id) @@ -577,6 +544,11 @@ def _set_or_replace_by_id( builder.columns = [*self.columns, new_value.name(id)] return builder.build() + def _select(self, values: typing.Tuple[ibis_types.Value]) -> UnorderedIR: + builder = self.builder() + builder.columns = values + return builder.build() + def _reproject_to_table(self) -> UnorderedIR: """ Internal operators that projects the internal representation into a @@ -816,20 +788,6 @@ def promote_offsets(self, col_id: str) -> OrderedIR: ] return expr_builder.build() - def select_columns(self, column_ids: typing.Sequence[str]) -> OrderedIR: - """Creates a new expression based on this expression with new columns.""" - columns = [self._get_ibis_column(col_id) for col_id in column_ids] - expr = self - for ordering_column in set(self.column_ids).intersection( - [col_ref.column_id for col_ref in self._ordering.ordering_value_columns] - ): - # Need to hide ordering columns that are being dropped. Alternatively, could project offsets - expr = expr._hide_column(ordering_column) - builder = expr.builder() - builder.columns = list(columns) - new_expr = builder.build() - return new_expr - ## Methods that only work with ordering def project_window_op( self, @@ -1221,6 +1179,29 @@ def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> Ordered builder.columns = [*self.columns, new_value.name(id)] return builder.build() + def _select(self, values: typing.Tuple[ibis_types.Value]) -> OrderedIR: + """Safely assign by id while maintaining ordering integrity.""" + # TODO: Split into explicit set and replace methods + ordering_col_ids = [ + col_ref.column_id for col_ref in self._ordering.ordering_value_columns + ] + ir = self + mappings = {value.name: value for value in values} + for ordering_id in ordering_col_ids: + # Drop case + if (ordering_id not in mappings) and (ordering_id in ir.column_ids): + # id is being dropped, hide it first + ir = ir._hide_column(ordering_id) + # Mutate case + elif (ordering_id in mappings) and not mappings[ordering_id].equals( + ir._get_any_column(ordering_id) + ): + ir = ir._hide_column(ordering_id) + + builder = ir.builder() + builder.columns = list(values) + return builder.build() + ## Ordering specific helpers def _get_any_column(self, key: str) -> ibis_types.Value: """Gets the Ibis expression for a given column. Will also get hidden columns.""" diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index eacee26bcb7..18fcd73d193 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -79,16 +79,6 @@ def compile_join(node: nodes.JoinNode, ordered: bool = True): ) -@_compile_node.register -def compile_select(node: nodes.SelectNode, ordered: bool = True): - return compile_node(node.child, ordered).select_columns(node.column_ids) - - -@_compile_node.register -def compile_drop(node: nodes.DropColumnsNode, ordered: bool = True): - return compile_node(node.child, ordered).drop_columns(node.columns) - - @_compile_node.register def compile_readlocal(node: nodes.ReadLocalNode, ordered: bool = True): array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) @@ -145,9 +135,7 @@ def compile_reversed(node: nodes.ReversedNode, ordered: bool = True): @_compile_node.register def compile_projection(node: nodes.ProjectionNode, ordered: bool = True): result = compile_node(node.child, ordered) - for expr, id in node.assignments: - result = result.project_expression(expr, id) - return result + return result.projection(node.assignments) @_compile_node.register @@ -210,18 +198,6 @@ def compile_unpivot(node: nodes.UnpivotNode, ordered: bool = True): ) -@_compile_node.register -def compile_assign(node: nodes.AssignNode, ordered: bool = True): - return compile_node(node.child, ordered).assign(node.source_id, node.destination_id) - - -@_compile_node.register -def compile_assign_constant(node: nodes.AssignConstantNode, ordered: bool = True): - return compile_node(node.child, ordered).assign_constant( - node.destination_id, node.value, node.dtype - ) - - @_compile_node.register def compiler_random_sample(node: nodes.RandomSampleNode, ordered: bool = True): return compile_node(node.child, ordered)._uniform_sampling(node.fraction) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 115242e7223..bf0755acc75 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -26,7 +26,7 @@ import pandas as pd import bigframes.constants as constants -import bigframes.core.expression as expressions +import bigframes.core.expression as ex import bigframes.dtypes import bigframes.operations as ops @@ -53,7 +53,7 @@ class ScalarOpCompiler: @functools.singledispatchmethod def compile_expression( self, - expression: expressions.Expression, + expression: ex.Expression, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: raise NotImplementedError(f"Unrecognized expression: {expression}") @@ -61,17 +61,17 @@ def compile_expression( @compile_expression.register def _( self, - expression: expressions.ScalarConstantExpression, + expression: ex.ScalarConstantExpression, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: - if pd.isnull(expression.value): # type: ignore - return ibis.null() - return ibis.literal(expression.value) + return bigframes.dtypes.literal_to_ibis_scalar( + expression.value, expression.dtype + ) @compile_expression.register def _( self, - expression: expressions.UnboundVariableExpression, + expression: ex.UnboundVariableExpression, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: if expression.id not in bindings: @@ -82,7 +82,7 @@ def _( @compile_expression.register def _( self, - expression: expressions.OpExpression, + expression: ex.OpExpression, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: inputs = [ diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 59c3e595d9b..540f9b6e5a2 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -18,12 +18,16 @@ import dataclasses import itertools import typing +from typing import Optional +import bigframes.dtypes import bigframes.operations -def const(value: typing.Hashable) -> Expression: - return ScalarConstantExpression(value) +def const( + value: typing.Hashable, dtype: Optional[bigframes.dtypes.Dtype] = None +) -> Expression: + return ScalarConstantExpression(value, dtype) def free_var(id: str) -> Expression: @@ -41,6 +45,10 @@ def unbound_variables(self) -> typing.Tuple[str, ...]: def rename(self, name_mapping: dict[str, str]) -> Expression: return self + @abc.abstractproperty + def is_const(self) -> bool: + return False + @dataclasses.dataclass(frozen=True) class ScalarConstantExpression(Expression): @@ -48,6 +56,11 @@ class ScalarConstantExpression(Expression): # TODO: Further constrain? value: typing.Hashable + dtype: Optional[bigframes.dtypes.Dtype] = None + + @property + def is_const(self) -> bool: + return True @dataclasses.dataclass(frozen=True) @@ -66,6 +79,10 @@ def rename(self, name_mapping: dict[str, str]) -> Expression: else: return self + @property + def is_const(self) -> bool: + return False + @dataclasses.dataclass(frozen=True) class OpExpression(Expression): @@ -89,3 +106,7 @@ def rename(self, name_mapping: dict[str, str]) -> Expression: return OpExpression( self.op, tuple(input.rename(name_mapping) for input in self.inputs) ) + + @property + def is_const(self) -> bool: + return all(child.is_const for child in self.inputs) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index c02fdbb12dc..4ec11cb163e 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -609,7 +609,7 @@ def coalesce_columns( expr = expr.drop_columns([left_id]) elif how == "outer": coalesced_id = bigframes.core.guid.generate_guid() - expr = expr.project( + expr = expr.project_to_id( ops.coalesce_op.as_expr(left_id, right_id), coalesced_id ) expr = expr.drop_columns([left_id, right_id]) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 360f853e3ed..d30db9a7f7f 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -21,7 +21,7 @@ import pandas -import bigframes.core.expression as expressions +import bigframes.core.expression as ex import bigframes.core.guid from bigframes.core.ordering import OrderingColumnReference import bigframes.core.window_spec as window @@ -145,14 +145,6 @@ def __hash__(self): # Unary nodes -@dataclass(frozen=True) -class DropColumnsNode(UnaryNode): - columns: Tuple[str, ...] - - def __hash__(self): - return self._node_hash - - @dataclass(frozen=True) class PromoteOffsetsNode(UnaryNode): col_id: str @@ -187,17 +179,9 @@ def __hash__(self): return self._node_hash -@dataclass(frozen=True) -class SelectNode(UnaryNode): - column_ids: typing.Tuple[str, ...] - - def __hash__(self): - return self._node_hash - - @dataclass(frozen=True) class ProjectionNode(UnaryNode): - assignments: typing.Tuple[typing.Tuple[expressions.Expression, str], ...] + assignments: typing.Tuple[typing.Tuple[ex.Expression, str], ...] def __hash__(self): return self._node_hash @@ -264,25 +248,6 @@ def __hash__(self): return self._node_hash -@dataclass(frozen=True) -class AssignNode(UnaryNode): - source_id: str - destination_id: str - - def __hash__(self): - return self._node_hash - - -@dataclass(frozen=True) -class AssignConstantNode(UnaryNode): - destination_id: str - value: typing.Hashable - dtype: typing.Optional[bigframes.dtypes.Dtype] - - def __hash__(self): - return self._node_hash - - @dataclass(frozen=True) class RandomSampleNode(UnaryNode): fraction: float diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 8032093b626..9f415f3bc45 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -136,7 +136,7 @@ def test_arrayvalues_to_ibis_expr_with_project_unary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project( + expr = value.project_to_id( ops.AsTypeOp("string").as_expr("col1"), output_id="col1" )._compile_ordered() assert value._compile_ordered().columns[0].type().is_int64() @@ -154,7 +154,9 @@ def test_arrayvalues_to_ibis_expr_with_project_binary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project(ops.add_op.as_expr("col2", "col3"), "col4")._compile_ordered() + expr = value.project_to_id( + ops.add_op.as_expr("col2", "col3"), "col4" + )._compile_ordered() assert expr.columns[3].type().is_float64() actual = expr._to_ibis_expr(ordering_mode="unordered") assert len(expr.columns) == 4 @@ -173,7 +175,7 @@ def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): ), total_ordering_columns=["col1"], ) - expr = value.project( + expr = value.project_to_id( ops.where_op.as_expr("col2", "col3", "col4"), "col5" )._compile_ordered() assert expr.columns[4].type().is_float64() From c057658a52cf92abd5c9ca736833bb91018e2b7d Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:40:22 -0800 Subject: [PATCH 265/921] build(python): fix `docs` and `docfx` builds (#319) * build(python): fix `docs` and `docfx` builds Source-Link: https://github.com/googleapis/synthtool/commit/fac8444edd5f5526e804c306b766a271772a3e2f Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:5ea6d0ab82c956b50962f91d94e206d3921537ae5fe1549ec5326381d8905cfa * Update noxfile. * update noxfile docfx --------- Co-authored-by: Owl Bot Co-authored-by: Huan Chen Co-authored-by: Huan Chen <142538604+Genesis929@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 6 +++--- .kokoro/requirements.txt | 6 +++--- noxfile.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9bee2409716..d8a1bbca717 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:346ab2efb51649c5dde7756cbbdc60dd394852ba83b9bbffc292a63549f33c17 -# created: 2023-12-14T22:17:57.611773021Z + digest: sha256:5ea6d0ab82c956b50962f91d94e206d3921537ae5fe1549ec5326381d8905cfa +# created: 2024-01-15T16:32:08.142785673Z diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index e5c1ffca94b..bb3d6ca38b1 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -263,9 +263,9 @@ jeepney==0.8.0 \ # via # keyring # secretstorage -jinja2==3.1.2 \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 +jinja2==3.1.3 \ + --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ + --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 # via gcp-releasetool keyring==24.2.0 \ --hash=sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6 \ diff --git a/noxfile.py b/noxfile.py index 1d3624005a7..f25cb9fa10b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -432,6 +432,15 @@ def docs(session): session.install("-e", ".") session.install( + # We need to pin to specific versions of the `sphinxcontrib-*` packages + # which still support sphinx 4.x. + # See https://github.com/googleapis/sphinx-docfx-yaml/issues/344 + # and https://github.com/googleapis/sphinx-docfx-yaml/issues/345. + "sphinxcontrib-applehelp==1.0.4", + "sphinxcontrib-devhelp==1.0.2", + "sphinxcontrib-htmlhelp==2.0.1", + "sphinxcontrib-qthelp==1.0.3", + "sphinxcontrib-serializinghtml==1.1.5", SPHINX_VERSION, "alabaster", "recommonmark", @@ -458,6 +467,15 @@ def docfx(session): session.install("-e", ".") session.install( + # We need to pin to specific versions of the `sphinxcontrib-*` packages + # which still support sphinx 4.x. + # See https://github.com/googleapis/sphinx-docfx-yaml/issues/344 + # and https://github.com/googleapis/sphinx-docfx-yaml/issues/345. + "sphinxcontrib-applehelp==1.0.4", + "sphinxcontrib-devhelp==1.0.2", + "sphinxcontrib-htmlhelp==2.0.1", + "sphinxcontrib-qthelp==1.0.3", + "sphinxcontrib-serializinghtml==1.1.5", SPHINX_VERSION, "alabaster", "recommonmark", From e165ba815950ffca496827767a53eb61fefe6d72 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:52:15 -0700 Subject: [PATCH 266/921] chore: add polished ml fundamental notebooks and retire the old one (#310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue #303708466 🦕 --- .../getting_started/ml_fundamentals.ipynb | 3908 ----------------- .../ml_fundamentals_bq_dataframes.ipynb | 970 ++++ 2 files changed, 970 insertions(+), 3908 deletions(-) delete mode 100644 notebooks/getting_started/ml_fundamentals.ipynb create mode 100644 notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb diff --git a/notebooks/getting_started/ml_fundamentals.ipynb b/notebooks/getting_started/ml_fundamentals.ipynb deleted file mode 100644 index 165bd90f314..00000000000 --- a/notebooks/getting_started/ml_fundamentals.ipynb +++ /dev/null @@ -1,3908 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using ML - ML fundamentals\n", - "\n", - "The `bigframes.ml` module implements Scikit-Learn's machine learning API in\n", - "BigQuery DataFrames. It exposes BigQuery's ML capabilities in a simple, popular\n", - "API that works seamlessly with the rest of the BigQuery DataFrames API." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 7ddb1bda-402a-4e8e-8476-7904010fb4ef is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e8aba858-7660-4274-8d90-8d2b0382f8f6 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
penguin_id
0Adelie Penguin (Pygoscelis adeliae)Biscoe40.118.9188.04300.0MALE
1Adelie Penguin (Pygoscelis adeliae)Torgersen39.118.7181.03750.0MALE
2Gentoo penguin (Pygoscelis papua)Biscoe47.414.6212.04725.0FEMALE
3Chinstrap penguin (Pygoscelis antarctica)Dream42.516.7187.03350.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Biscoe43.219.0197.04775.0MALE
5Gentoo penguin (Pygoscelis papua)Biscoe46.715.3219.05200.0MALE
6Adelie Penguin (Pygoscelis adeliae)Biscoe41.321.1195.04400.0MALE
7Gentoo penguin (Pygoscelis papua)Biscoe45.213.8215.04750.0FEMALE
8Gentoo penguin (Pygoscelis papua)Biscoe46.513.5210.04550.0FEMALE
9Gentoo penguin (Pygoscelis papua)Biscoe50.515.2216.05000.0FEMALE
10Gentoo penguin (Pygoscelis papua)Biscoe48.215.6221.05100.0MALE
11Adelie Penguin (Pygoscelis adeliae)Dream38.118.6190.03700.0FEMALE
12Gentoo penguin (Pygoscelis papua)Biscoe50.715.0223.05550.0MALE
13Adelie Penguin (Pygoscelis adeliae)Biscoe37.820.0190.04250.0MALE
14Adelie Penguin (Pygoscelis adeliae)Biscoe35.017.9190.03450.0FEMALE
15Gentoo penguin (Pygoscelis papua)Biscoe48.715.7208.05350.0MALE
16Adelie Penguin (Pygoscelis adeliae)Torgersen34.621.1198.04400.0MALE
17Gentoo penguin (Pygoscelis papua)Biscoe46.815.4215.05150.0MALE
18Chinstrap penguin (Pygoscelis antarctica)Dream50.320.0197.03300.0MALE
19Adelie Penguin (Pygoscelis adeliae)Dream37.218.1178.03900.0MALE
20Chinstrap penguin (Pygoscelis antarctica)Dream51.018.8203.04100.0MALE
21Adelie Penguin (Pygoscelis adeliae)Biscoe40.517.9187.03200.0FEMALE
22Gentoo penguin (Pygoscelis papua)Biscoe45.513.9210.04200.0FEMALE
23Adelie Penguin (Pygoscelis adeliae)Dream42.218.5180.03550.0FEMALE
24Chinstrap penguin (Pygoscelis antarctica)Dream51.720.3194.03775.0MALE
\n", - "

25 rows × 7 columns

\n", - "[334 rows x 7 columns in total]" - ], - "text/plain": [ - " species island \\\n", - "penguin_id \n", - "0 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "1 Adelie Penguin (Pygoscelis adeliae) Torgersen \n", - "2 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "3 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "4 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "5 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "6 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "7 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "8 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "9 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "10 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "11 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "12 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "13 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "14 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "15 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "16 Adelie Penguin (Pygoscelis adeliae) Torgersen \n", - "17 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "18 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "19 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "20 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "21 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "22 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "23 Adelie Penguin (Pygoscelis adeliae) Dream \n", - "24 Chinstrap penguin (Pygoscelis antarctica) Dream \n", - "\n", - " culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", - "penguin_id \n", - "0 40.1 18.9 188.0 4300.0 \n", - "1 39.1 18.7 181.0 3750.0 \n", - "2 47.4 14.6 212.0 4725.0 \n", - "3 42.5 16.7 187.0 3350.0 \n", - "4 43.2 19.0 197.0 4775.0 \n", - "5 46.7 15.3 219.0 5200.0 \n", - "6 41.3 21.1 195.0 4400.0 \n", - "7 45.2 13.8 215.0 4750.0 \n", - "8 46.5 13.5 210.0 4550.0 \n", - "9 50.5 15.2 216.0 5000.0 \n", - "10 48.2 15.6 221.0 5100.0 \n", - "11 38.1 18.6 190.0 3700.0 \n", - "12 50.7 15.0 223.0 5550.0 \n", - "13 37.8 20.0 190.0 4250.0 \n", - "14 35.0 17.9 190.0 3450.0 \n", - "15 48.7 15.7 208.0 5350.0 \n", - "16 34.6 21.1 198.0 4400.0 \n", - "17 46.8 15.4 215.0 5150.0 \n", - "18 50.3 20.0 197.0 3300.0 \n", - "19 37.2 18.1 178.0 3900.0 \n", - "20 51.0 18.8 203.0 4100.0 \n", - "21 40.5 17.9 187.0 3200.0 \n", - "22 45.5 13.9 210.0 4200.0 \n", - "23 42.2 18.5 180.0 3550.0 \n", - "24 51.7 20.3 194.0 3775.0 \n", - "\n", - " sex \n", - "penguin_id \n", - "0 MALE \n", - "1 MALE \n", - "2 FEMALE \n", - "3 FEMALE \n", - "4 MALE \n", - "5 MALE \n", - "6 MALE \n", - "7 FEMALE \n", - "8 FEMALE \n", - "9 FEMALE \n", - "10 MALE \n", - "11 FEMALE \n", - "12 MALE \n", - "13 MALE \n", - "14 FEMALE \n", - "15 MALE \n", - "16 MALE \n", - "17 MALE \n", - "18 MALE \n", - "19 MALE \n", - "20 MALE \n", - "21 FEMALE \n", - "22 FEMALE \n", - "23 FEMALE \n", - "24 MALE \n", - "...\n", - "\n", - "[334 rows x 7 columns]" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Lets load some test data to use in this tutorial\n", - "import bigframes.pandas\n", - "\n", - "df = bigframes.pandas.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", - "df = df.dropna()\n", - "\n", - "# Temporary workaround: lets name our index so it isn't lost BigQuery DataFrame\n", - "# currently drops unnamed indexes when round-tripping through pandas, which\n", - "# some ML APIs do to route around missing functionality\n", - "df.index.name = \"penguin_id\"\n", - "\n", - "df" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data split\n", - "\n", - "Part of preparing data for a machine learning task is splitting it into subsets for training and testing, to ensure that the solution is not overfitting. Most commonly this is done with `bigframes.ml.model_selection.train_test_split` like so:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job deda90a8-6ec7-419c-8067-e85777bd916f is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job efe8fa0a-d450-475a-99d5-36beeb985247 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 5022c56d-e605-4cab-be1b-1ecf189588a1 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 175bd293-d448-4510-b926-1d8cfb4eb5e7 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job a3a2e68c-f5f3-4237-99ad-44974f29d090 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "X_train shape: (267, 6)\n", - "X_test shape: (67, 6)\n", - "y_train shape: (267, 1)\n", - "y_test shape: (67, 1)\n" - ] - } - ], - "source": [ - "# In this example, we're doing supervised learning, where we will learn to predict\n", - "# output variable `y` from input features `X`\n", - "X = df[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex', 'species']]\n", - "y = df[['body_mass_g']] \n", - "\n", - "from bigframes.ml.model_selection import train_test_split\n", - "\n", - "# This will split X and y into test and training sets, with 20% of the rows in the test set,\n", - "# and the rest in the training set\n", - "X_train, X_test, y_train, y_test = train_test_split(\n", - " X, y, test_size=0.2)\n", - "\n", - "# Show the shape of the data after the split\n", - "print(f\"\"\"X_train shape: {X_train.shape}\n", - "X_test shape: {X_test.shape}\n", - "y_train shape: {y_train.shape}\n", - "y_test shape: {y_test.shape}\"\"\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job db3365fb-67ca-44cc-a117-88a80dc63cca is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job ab78f7ab-a115-448b-92d0-19c091a831ca is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
islandculmen_length_mmculmen_depth_mmflipper_length_mmsexspecies
penguin_id
249Torgersen41.118.6189.0MALEAdelie Penguin (Pygoscelis adeliae)
36Biscoe43.414.4218.0FEMALEGentoo penguin (Pygoscelis papua)
74Biscoe42.814.2209.0FEMALEGentoo penguin (Pygoscelis papua)
235Dream34.017.1185.0FEMALEAdelie Penguin (Pygoscelis adeliae)
117Dream37.818.1193.0MALEAdelie Penguin (Pygoscelis adeliae)
\n", - "

5 rows × 6 columns

\n", - "
[5 rows x 6 columns in total]" - ], - "text/plain": [ - " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", - "penguin_id \n", - "249 Torgersen 41.1 18.6 189.0 \n", - "36 Biscoe 43.4 14.4 218.0 \n", - "74 Biscoe 42.8 14.2 209.0 \n", - "235 Dream 34.0 17.1 185.0 \n", - "117 Dream 37.8 18.1 193.0 \n", - "\n", - " sex species \n", - "penguin_id \n", - "249 MALE Adelie Penguin (Pygoscelis adeliae) \n", - "36 FEMALE Gentoo penguin (Pygoscelis papua) \n", - "74 FEMALE Gentoo penguin (Pygoscelis papua) \n", - "235 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", - "117 MALE Adelie Penguin (Pygoscelis adeliae) \n", - "\n", - "[5 rows x 6 columns]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# If we look at the data, we can see that random rows were selected for\n", - "# each side of the split\n", - "X_test.head(5)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 22a72cad-11a6-4f8e-b16d-f92853b8112e is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job bc952727-8806-4fe2-abf2-c3a8a2bd9b6d is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
body_mass_g
penguin_id
2493325.0
364600.0
744700.0
2353400.0
1173750.0
\n", - "

5 rows × 1 columns

\n", - "
[5 rows x 1 columns in total]" - ], - "text/plain": [ - " body_mass_g\n", - "penguin_id \n", - "249 3325.0\n", - "36 4600.0\n", - "74 4700.0\n", - "235 3400.0\n", - "117 3750.0\n", - "\n", - "[5 rows x 1 columns]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Note that this matches the rows in X_test\n", - "y_test.head(5)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Estimators\n", - "\n", - "Following Scikit-Learn, all learning components are \"estimators\"; objects that can learn from training data and then apply themselves to new data. Estimators share the following patterns:\n", - "\n", - "- a constructor that takes a list of parameters\n", - "- a standard string representation that shows the class name and all non-default parameters, e.g. `LinearRegression(fit_intercept=False)`\n", - "- a `.fit(..)` method to fit the estimator to training data\n", - "\n", - "There estimators can be further broken down into two main subtypes:\n", - "\n", - "### Transformers\n", - "\n", - "Transformers are estimators that are used to prepare data for consumption by other estimators ('preprocessing'). In addition to `.fit(...)`, the transformer implements a `.transform(...)` method, which will apply a transformation based on what was computed during `.fit(..)`. With this pattern dynamic preprocessing steps can be applied to both training and test/production data consistently.\n", - "\n", - "An example of a transformer is `bigframes.ml.preprocessing.StandardScaler`, which rescales a dataset to have a mean of zero and a standard deviation of one:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job f239341e-785f-43e1-bfe0-683132d6f15f is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 2d5bbbb9-efc4-4f4e-a8dc-2c7b66b0e5e0 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 66120e1c-2471-4a0c-8b82-aeb189c8866a is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 62825fc4-5b77-43e5-a3e4-525ebfd1285b is DONE. 2.1 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 656d1d69-b4ff-4db6-9f2d-28dcf91e2fd7 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 466507c8-1474-4725-93e5-baf8ee292e39 is DONE. 8.5 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
standard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mm
penguin_id
0-0.7505050.84903-0.937262
20.622496-1.3224020.804051
3-0.299107-0.261935-1.009817
50.490839-0.9689131.311935
6-0.5248061.959995-0.429379
70.208715-1.7263891.021716
91.205551-1.0194121.09427
100.772962-0.8174181.457044
121.243168-1.1204081.602153
14-1.7097250.344046-0.792152
170.509647-0.9184151.021716
181.1679351.404513-0.284269
19-1.2959440.445043-1.662809
201.2995930.7985320.151059
21-0.6752720.344046-1.009817
220.26514-1.6758910.658942
241.431251.556008-0.501934
250.3027560.041055-0.574488
260.302756-1.6758910.949161
270.227523-1.7768880.658942
281.318401-0.3629321.747263
292.2023881.3035160.441278
30-0.9197791.959995-0.356824
311.036277-0.6154241.747263
32-0.2238740.19255-0.356824
\n", - "

25 rows × 3 columns

\n", - "
[267 rows x 3 columns in total]" - ], - "text/plain": [ - " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", - "penguin_id \n", - "0 -0.750505 0.84903 \n", - "2 0.622496 -1.322402 \n", - "3 -0.299107 -0.261935 \n", - "5 0.490839 -0.968913 \n", - "6 -0.524806 1.959995 \n", - "7 0.208715 -1.726389 \n", - "9 1.205551 -1.019412 \n", - "10 0.772962 -0.817418 \n", - "12 1.243168 -1.120408 \n", - "14 -1.709725 0.344046 \n", - "17 0.509647 -0.918415 \n", - "18 1.167935 1.404513 \n", - "19 -1.295944 0.445043 \n", - "20 1.299593 0.798532 \n", - "21 -0.675272 0.344046 \n", - "22 0.26514 -1.675891 \n", - "24 1.43125 1.556008 \n", - "25 0.302756 0.041055 \n", - "26 0.302756 -1.675891 \n", - "27 0.227523 -1.776888 \n", - "28 1.318401 -0.362932 \n", - "29 2.202388 1.303516 \n", - "30 -0.919779 1.959995 \n", - "31 1.036277 -0.615424 \n", - "32 -0.223874 0.19255 \n", - "\n", - " standard_scaled_flipper_length_mm \n", - "penguin_id \n", - "0 -0.937262 \n", - "2 0.804051 \n", - "3 -1.009817 \n", - "5 1.311935 \n", - "6 -0.429379 \n", - "7 1.021716 \n", - "9 1.09427 \n", - "10 1.457044 \n", - "12 1.602153 \n", - "14 -0.792152 \n", - "17 1.021716 \n", - "18 -0.284269 \n", - "19 -1.662809 \n", - "20 0.151059 \n", - "21 -1.009817 \n", - "22 0.658942 \n", - "24 -0.501934 \n", - "25 -0.574488 \n", - "26 0.949161 \n", - "27 0.658942 \n", - "28 1.747263 \n", - "29 0.441278 \n", - "30 -0.356824 \n", - "31 1.747263 \n", - "32 -0.356824 \n", - "...\n", - "\n", - "[267 rows x 3 columns]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from bigframes.ml.preprocessing import StandardScaler\n", - "\n", - "# StandardScaler will only work on numeric columns\n", - "numeric_columns = [\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]\n", - "\n", - "scaler = StandardScaler()\n", - "scaler.fit(X_train[numeric_columns])\n", - "\n", - "# Now, standardscaler should transform the numbers to have mean of zero\n", - "# and standard deviation of one:\n", - "scaler.transform(X_train[numeric_columns])" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 845c6cff-ac6c-46c1-8e9b-061519f1fa1a is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 1e17f5f7-2956-4bdd-baa9-c07591481341 is DONE. 536 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e2fde7a6-67b4-45a4-91d4-1cb9eff66ae5 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e0683619-23c5-44fd-8930-9d3c9d02729a is DONE. 2.1 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
standard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mm
penguin_id
1-0.9385870.748033-1.445145
4-0.167450.899528-0.284269
80.453222-1.8778850.658942
11-1.126670.697535-0.792152
13-1.1830941.404513-0.792152
150.867003-0.7669190.513833
16-1.7849581.959995-0.211715
23-0.3555320.647036-1.5177
34-0.600039-1.7768880.949161
36-0.129833-1.4233991.23938
42-1.615684-0.514427-0.429379
480.415606-0.7164211.021716
610.396797-1.1709071.457044
640.434414-1.1204081.09427
65-1.2207111.051024-1.445145
68-1.484026-0.009443-1.009817
701.6381411.4045130.296168
720.8293870.142052-0.719598
74-0.242683-1.5243960.586387
77-1.277136-0.211437-0.647043
810.208715-1.2214050.804051
911.2619760.6470360.005949
960.246331-1.3224020.731497
105-1.8037660.445043-1.009817
111-1.1642860.697535-2.098138
\n", - "

25 rows × 3 columns

\n", - "
[67 rows x 3 columns in total]" - ], - "text/plain": [ - " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", - "penguin_id \n", - "1 -0.938587 0.748033 \n", - "4 -0.16745 0.899528 \n", - "8 0.453222 -1.877885 \n", - "11 -1.12667 0.697535 \n", - "13 -1.183094 1.404513 \n", - "15 0.867003 -0.766919 \n", - "16 -1.784958 1.959995 \n", - "23 -0.355532 0.647036 \n", - "34 -0.600039 -1.776888 \n", - "36 -0.129833 -1.423399 \n", - "42 -1.615684 -0.514427 \n", - "48 0.415606 -0.716421 \n", - "61 0.396797 -1.170907 \n", - "64 0.434414 -1.120408 \n", - "65 -1.220711 1.051024 \n", - "68 -1.484026 -0.009443 \n", - "70 1.638141 1.404513 \n", - "72 0.829387 0.142052 \n", - "74 -0.242683 -1.524396 \n", - "77 -1.277136 -0.211437 \n", - "81 0.208715 -1.221405 \n", - "91 1.261976 0.647036 \n", - "96 0.246331 -1.322402 \n", - "105 -1.803766 0.445043 \n", - "111 -1.164286 0.697535 \n", - "\n", - " standard_scaled_flipper_length_mm \n", - "penguin_id \n", - "1 -1.445145 \n", - "4 -0.284269 \n", - "8 0.658942 \n", - "11 -0.792152 \n", - "13 -0.792152 \n", - "15 0.513833 \n", - "16 -0.211715 \n", - "23 -1.5177 \n", - "34 0.949161 \n", - "36 1.23938 \n", - "42 -0.429379 \n", - "48 1.021716 \n", - "61 1.457044 \n", - "64 1.09427 \n", - "65 -1.445145 \n", - "68 -1.009817 \n", - "70 0.296168 \n", - "72 -0.719598 \n", - "74 0.586387 \n", - "77 -0.647043 \n", - "81 0.804051 \n", - "91 0.005949 \n", - "96 0.731497 \n", - "105 -1.009817 \n", - "111 -2.098138 \n", - "...\n", - "\n", - "[67 rows x 3 columns]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# We can then repeat this transformation on new data\n", - "scaler.transform(X_test[numeric_columns])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Composing transformers\n", - "\n", - "To process data where different columns need different preprocessors, `bigframes.composition.ColumnTransformer` can be employed:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 75c1ce67-e5d7-4f4c-947e-381fc5298236 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 41962e2e-4d14-4053-9297-3ce61699551a is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 5d3c22c9-c972-4213-8557-726c9e0aca37 is DONE. 22.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 9cb7b33f-ea05-4cf4-9f92-bb3aa4ea8d10 is DONE. 2.1 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job fe1f35d6-d82c-4aab-a284-637b72554f5b is DONE. 29.2 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 37bc90ff-59cb-4b0c-8f9d-73bcda43524a is DONE. 536 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e23f4724-fdd8-45a9-8c87-defd8d471035 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 257378db-0569-42d7-965a-7757154c710b is DONE. 21.4 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
onehotencoded_islandstandard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mmonehotencoded_sexonehotencoded_species
penguin_id
0[{'index': 1, 'value': 1.0}]-0.7505050.84903-0.937262[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
2[{'index': 1, 'value': 1.0}]0.622496-1.3224020.804051[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
3[{'index': 2, 'value': 1.0}]-0.299107-0.261935-1.009817[{'index': 1, 'value': 1.0}][{'index': 2, 'value': 1.0}]
5[{'index': 1, 'value': 1.0}]0.490839-0.9689131.311935[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
6[{'index': 1, 'value': 1.0}]-0.5248061.959995-0.429379[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
7[{'index': 1, 'value': 1.0}]0.208715-1.7263891.021716[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
9[{'index': 1, 'value': 1.0}]1.205551-1.0194121.09427[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
10[{'index': 1, 'value': 1.0}]0.772962-0.8174181.457044[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
12[{'index': 1, 'value': 1.0}]1.243168-1.1204081.602153[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
14[{'index': 1, 'value': 1.0}]-1.7097250.344046-0.792152[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
17[{'index': 1, 'value': 1.0}]0.509647-0.9184151.021716[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
18[{'index': 2, 'value': 1.0}]1.1679351.404513-0.284269[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
19[{'index': 2, 'value': 1.0}]-1.2959440.445043-1.662809[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
20[{'index': 2, 'value': 1.0}]1.2995930.7985320.151059[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
21[{'index': 1, 'value': 1.0}]-0.6752720.344046-1.009817[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
22[{'index': 1, 'value': 1.0}]0.26514-1.6758910.658942[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
24[{'index': 2, 'value': 1.0}]1.431251.556008-0.501934[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
25[{'index': 2, 'value': 1.0}]0.3027560.041055-0.574488[{'index': 1, 'value': 1.0}][{'index': 2, 'value': 1.0}]
26[{'index': 1, 'value': 1.0}]0.302756-1.6758910.949161[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
27[{'index': 1, 'value': 1.0}]0.227523-1.7768880.658942[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
28[{'index': 1, 'value': 1.0}]1.318401-0.3629321.747263[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
29[{'index': 2, 'value': 1.0}]2.2023881.3035160.441278[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
30[{'index': 2, 'value': 1.0}]-0.9197791.959995-0.356824[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
31[{'index': 1, 'value': 1.0}]1.036277-0.6154241.747263[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
32[{'index': 3, 'value': 1.0}]-0.2238740.19255-0.356824[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
\n", - "

25 rows × 6 columns

\n", - "
[267 rows x 6 columns in total]" - ], - "text/plain": [ - " onehotencoded_island standard_scaled_culmen_length_mm \\\n", - "penguin_id \n", - "0 [{'index': 1, 'value': 1.0}] -0.750505 \n", - "2 [{'index': 1, 'value': 1.0}] 0.622496 \n", - "3 [{'index': 2, 'value': 1.0}] -0.299107 \n", - "5 [{'index': 1, 'value': 1.0}] 0.490839 \n", - "6 [{'index': 1, 'value': 1.0}] -0.524806 \n", - "7 [{'index': 1, 'value': 1.0}] 0.208715 \n", - "9 [{'index': 1, 'value': 1.0}] 1.205551 \n", - "10 [{'index': 1, 'value': 1.0}] 0.772962 \n", - "12 [{'index': 1, 'value': 1.0}] 1.243168 \n", - "14 [{'index': 1, 'value': 1.0}] -1.709725 \n", - "17 [{'index': 1, 'value': 1.0}] 0.509647 \n", - "18 [{'index': 2, 'value': 1.0}] 1.167935 \n", - "19 [{'index': 2, 'value': 1.0}] -1.295944 \n", - "20 [{'index': 2, 'value': 1.0}] 1.299593 \n", - "21 [{'index': 1, 'value': 1.0}] -0.675272 \n", - "22 [{'index': 1, 'value': 1.0}] 0.26514 \n", - "24 [{'index': 2, 'value': 1.0}] 1.43125 \n", - "25 [{'index': 2, 'value': 1.0}] 0.302756 \n", - "26 [{'index': 1, 'value': 1.0}] 0.302756 \n", - "27 [{'index': 1, 'value': 1.0}] 0.227523 \n", - "28 [{'index': 1, 'value': 1.0}] 1.318401 \n", - "29 [{'index': 2, 'value': 1.0}] 2.202388 \n", - "30 [{'index': 2, 'value': 1.0}] -0.919779 \n", - "31 [{'index': 1, 'value': 1.0}] 1.036277 \n", - "32 [{'index': 3, 'value': 1.0}] -0.223874 \n", - "\n", - " standard_scaled_culmen_depth_mm \\\n", - "penguin_id \n", - "0 0.84903 \n", - "2 -1.322402 \n", - "3 -0.261935 \n", - "5 -0.968913 \n", - "6 1.959995 \n", - "7 -1.726389 \n", - "9 -1.019412 \n", - "10 -0.817418 \n", - "12 -1.120408 \n", - "14 0.344046 \n", - "17 -0.918415 \n", - "18 1.404513 \n", - "19 0.445043 \n", - "20 0.798532 \n", - "21 0.344046 \n", - "22 -1.675891 \n", - "24 1.556008 \n", - "25 0.041055 \n", - "26 -1.675891 \n", - "27 -1.776888 \n", - "28 -0.362932 \n", - "29 1.303516 \n", - "30 1.959995 \n", - "31 -0.615424 \n", - "32 0.19255 \n", - "\n", - " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", - "penguin_id \n", - "0 -0.937262 [{'index': 2, 'value': 1.0}] \n", - "2 0.804051 [{'index': 1, 'value': 1.0}] \n", - "3 -1.009817 [{'index': 1, 'value': 1.0}] \n", - "5 1.311935 [{'index': 2, 'value': 1.0}] \n", - "6 -0.429379 [{'index': 2, 'value': 1.0}] \n", - "7 1.021716 [{'index': 1, 'value': 1.0}] \n", - "9 1.09427 [{'index': 1, 'value': 1.0}] \n", - "10 1.457044 [{'index': 2, 'value': 1.0}] \n", - "12 1.602153 [{'index': 2, 'value': 1.0}] \n", - "14 -0.792152 [{'index': 1, 'value': 1.0}] \n", - "17 1.021716 [{'index': 2, 'value': 1.0}] \n", - "18 -0.284269 [{'index': 2, 'value': 1.0}] \n", - "19 -1.662809 [{'index': 2, 'value': 1.0}] \n", - "20 0.151059 [{'index': 2, 'value': 1.0}] \n", - "21 -1.009817 [{'index': 1, 'value': 1.0}] \n", - "22 0.658942 [{'index': 1, 'value': 1.0}] \n", - "24 -0.501934 [{'index': 2, 'value': 1.0}] \n", - "25 -0.574488 [{'index': 1, 'value': 1.0}] \n", - "26 0.949161 [{'index': 1, 'value': 1.0}] \n", - "27 0.658942 [{'index': 1, 'value': 1.0}] \n", - "28 1.747263 [{'index': 2, 'value': 1.0}] \n", - "29 0.441278 [{'index': 2, 'value': 1.0}] \n", - "30 -0.356824 [{'index': 2, 'value': 1.0}] \n", - "31 1.747263 [{'index': 2, 'value': 1.0}] \n", - "32 -0.356824 [{'index': 2, 'value': 1.0}] \n", - "\n", - " onehotencoded_species \n", - "penguin_id \n", - "0 [{'index': 1, 'value': 1.0}] \n", - "2 [{'index': 3, 'value': 1.0}] \n", - "3 [{'index': 2, 'value': 1.0}] \n", - "5 [{'index': 3, 'value': 1.0}] \n", - "6 [{'index': 1, 'value': 1.0}] \n", - "7 [{'index': 3, 'value': 1.0}] \n", - "9 [{'index': 3, 'value': 1.0}] \n", - "10 [{'index': 3, 'value': 1.0}] \n", - "12 [{'index': 3, 'value': 1.0}] \n", - "14 [{'index': 1, 'value': 1.0}] \n", - "17 [{'index': 3, 'value': 1.0}] \n", - "18 [{'index': 2, 'value': 1.0}] \n", - "19 [{'index': 1, 'value': 1.0}] \n", - "20 [{'index': 2, 'value': 1.0}] \n", - "21 [{'index': 1, 'value': 1.0}] \n", - "22 [{'index': 3, 'value': 1.0}] \n", - "24 [{'index': 2, 'value': 1.0}] \n", - "25 [{'index': 2, 'value': 1.0}] \n", - "26 [{'index': 3, 'value': 1.0}] \n", - "27 [{'index': 3, 'value': 1.0}] \n", - "28 [{'index': 3, 'value': 1.0}] \n", - "29 [{'index': 2, 'value': 1.0}] \n", - "30 [{'index': 1, 'value': 1.0}] \n", - "31 [{'index': 3, 'value': 1.0}] \n", - "32 [{'index': 1, 'value': 1.0}] \n", - "...\n", - "\n", - "[267 rows x 6 columns]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from bigframes.ml.compose import ColumnTransformer\n", - "from bigframes.ml.preprocessing import OneHotEncoder\n", - "\n", - "# Create an aggregate transform that applies StandardScaler to the numeric columns,\n", - "# and OneHotEncoder to the string columns\n", - "preproc = ColumnTransformer([\n", - " (\"scale\", StandardScaler(), [\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]),\n", - " (\"encode\", OneHotEncoder(), [\"species\", \"sex\", \"island\"])])\n", - "\n", - "# Now we can fit all columns of the training data\n", - "preproc.fit(X_train)\n", - "\n", - "processed_X_train = preproc.transform(X_train)\n", - "processed_X_test = preproc.transform(X_test)\n", - "\n", - "processed_X_train" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Predictors\n", - "\n", - "Predictors are estimators that learn and make predictions. In addition to `.fit(...)`, the predictor implements a `.predict(...)` method, which will use what was learned during `.fit(...)` to predict some output.\n", - "\n", - "Predictors can be further broken down into two categories:\n", - "\n", - "#### Supervised predictors\n", - "\n", - "Supervised learning is when we train a model on input-output pairs, and then ask it to predict the output for new inputs. An example of such a predictor is `bigframes.ml.linear_models.LinearRegression`." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 7d9c9f8b-6b4c-451f-ae3d-06fb7090d148 is DONE. 21.4 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job be87ccfa-72ab-4858-9d4a-b2f5f8b2a5e6 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 2d651fac-11bf-42da-8c18-bd33207379ca is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 58836ccc-242b-4574-bc48-4c269e74dbf1 is DONE. 5.7 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 1bf531f0-0fde-489b-ab36-6040a2a12377 is DONE. 536 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 4245f4e6-4d5b-404f-81d7-50f0553e2456 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job ed951699-c005-450e-a8b6-0916ec234e7f is DONE. 5.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
predicted_body_mass_gonehotencoded_islandstandard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mmonehotencoded_sexonehotencoded_species
penguin_id
13781.402407[{'index': 3, 'value': 1.0}]-0.9385870.748033-1.445145[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
44124.107944[{'index': 1, 'value': 1.0}]-0.167450.899528-0.284269[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
84670.344196[{'index': 1, 'value': 1.0}]0.453222-1.8778850.658942[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
113529.417214[{'index': 2, 'value': 1.0}]-1.126670.697535-0.792152[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
134014.101714[{'index': 1, 'value': 1.0}]-1.1830941.404513-0.792152[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
155212.41288[{'index': 1, 'value': 1.0}]0.867003-0.7669190.513833[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
164163.595615[{'index': 3, 'value': 1.0}]-1.7849581.959995-0.211715[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
233392.453069[{'index': 2, 'value': 1.0}]-0.3555320.647036-1.5177[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
344698.305397[{'index': 1, 'value': 1.0}]-0.600039-1.7768880.949161[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
364828.226949[{'index': 1, 'value': 1.0}]-0.129833-1.4233991.23938[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
423430.58866[{'index': 1, 'value': 1.0}]-1.615684-0.514427-0.429379[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
485314.260221[{'index': 1, 'value': 1.0}]0.415606-0.7164211.021716[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
615363.205372[{'index': 1, 'value': 1.0}]0.396797-1.1709071.457044[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
644855.908314[{'index': 1, 'value': 1.0}]0.434414-1.1204081.09427[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
653413.100524[{'index': 2, 'value': 1.0}]-1.2207111.051024-1.445145[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
683340.219002[{'index': 3, 'value': 1.0}]-1.484026-0.009443-1.009817[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
704228.73157[{'index': 2, 'value': 1.0}]1.6381411.4045130.296168[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
723811.538478[{'index': 2, 'value': 1.0}]0.8293870.142052-0.719598[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
744659.770763[{'index': 1, 'value': 1.0}]-0.242683-1.5243960.586387[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
773453.388804[{'index': 2, 'value': 1.0}]-1.277136-0.211437-0.647043[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
814766.245033[{'index': 1, 'value': 1.0}]0.208715-1.2214050.804051[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
914057.807281[{'index': 2, 'value': 1.0}]1.2619760.6470360.005949[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
964739.827445[{'index': 1, 'value': 1.0}]0.246331-1.3224020.731497[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
1053394.891976[{'index': 1, 'value': 1.0}]-1.8037660.445043-1.009817[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1113201.493683[{'index': 1, 'value': 1.0}]-1.1642860.697535-2.098138[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
\n", - "

25 rows × 7 columns

\n", - "
[67 rows x 7 columns in total]" - ], - "text/plain": [ - " predicted_body_mass_g onehotencoded_island \\\n", - "penguin_id \n", - "1 3781.402407 [{'index': 3, 'value': 1.0}] \n", - "4 4124.107944 [{'index': 1, 'value': 1.0}] \n", - "8 4670.344196 [{'index': 1, 'value': 1.0}] \n", - "11 3529.417214 [{'index': 2, 'value': 1.0}] \n", - "13 4014.101714 [{'index': 1, 'value': 1.0}] \n", - "15 5212.41288 [{'index': 1, 'value': 1.0}] \n", - "16 4163.595615 [{'index': 3, 'value': 1.0}] \n", - "23 3392.453069 [{'index': 2, 'value': 1.0}] \n", - "34 4698.305397 [{'index': 1, 'value': 1.0}] \n", - "36 4828.226949 [{'index': 1, 'value': 1.0}] \n", - "42 3430.58866 [{'index': 1, 'value': 1.0}] \n", - "48 5314.260221 [{'index': 1, 'value': 1.0}] \n", - "61 5363.205372 [{'index': 1, 'value': 1.0}] \n", - "64 4855.908314 [{'index': 1, 'value': 1.0}] \n", - "65 3413.100524 [{'index': 2, 'value': 1.0}] \n", - "68 3340.219002 [{'index': 3, 'value': 1.0}] \n", - "70 4228.73157 [{'index': 2, 'value': 1.0}] \n", - "72 3811.538478 [{'index': 2, 'value': 1.0}] \n", - "74 4659.770763 [{'index': 1, 'value': 1.0}] \n", - "77 3453.388804 [{'index': 2, 'value': 1.0}] \n", - "81 4766.245033 [{'index': 1, 'value': 1.0}] \n", - "91 4057.807281 [{'index': 2, 'value': 1.0}] \n", - "96 4739.827445 [{'index': 1, 'value': 1.0}] \n", - "105 3394.891976 [{'index': 1, 'value': 1.0}] \n", - "111 3201.493683 [{'index': 1, 'value': 1.0}] \n", - "\n", - " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", - "penguin_id \n", - "1 -0.938587 0.748033 \n", - "4 -0.16745 0.899528 \n", - "8 0.453222 -1.877885 \n", - "11 -1.12667 0.697535 \n", - "13 -1.183094 1.404513 \n", - "15 0.867003 -0.766919 \n", - "16 -1.784958 1.959995 \n", - "23 -0.355532 0.647036 \n", - "34 -0.600039 -1.776888 \n", - "36 -0.129833 -1.423399 \n", - "42 -1.615684 -0.514427 \n", - "48 0.415606 -0.716421 \n", - "61 0.396797 -1.170907 \n", - "64 0.434414 -1.120408 \n", - "65 -1.220711 1.051024 \n", - "68 -1.484026 -0.009443 \n", - "70 1.638141 1.404513 \n", - "72 0.829387 0.142052 \n", - "74 -0.242683 -1.524396 \n", - "77 -1.277136 -0.211437 \n", - "81 0.208715 -1.221405 \n", - "91 1.261976 0.647036 \n", - "96 0.246331 -1.322402 \n", - "105 -1.803766 0.445043 \n", - "111 -1.164286 0.697535 \n", - "\n", - " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", - "penguin_id \n", - "1 -1.445145 [{'index': 2, 'value': 1.0}] \n", - "4 -0.284269 [{'index': 2, 'value': 1.0}] \n", - "8 0.658942 [{'index': 1, 'value': 1.0}] \n", - "11 -0.792152 [{'index': 1, 'value': 1.0}] \n", - "13 -0.792152 [{'index': 2, 'value': 1.0}] \n", - "15 0.513833 [{'index': 2, 'value': 1.0}] \n", - "16 -0.211715 [{'index': 2, 'value': 1.0}] \n", - "23 -1.5177 [{'index': 1, 'value': 1.0}] \n", - "34 0.949161 [{'index': 1, 'value': 1.0}] \n", - "36 1.23938 [{'index': 1, 'value': 1.0}] \n", - "42 -0.429379 [{'index': 1, 'value': 1.0}] \n", - "48 1.021716 [{'index': 2, 'value': 1.0}] \n", - "61 1.457044 [{'index': 2, 'value': 1.0}] \n", - "64 1.09427 [{'index': 1, 'value': 1.0}] \n", - "65 -1.445145 [{'index': 1, 'value': 1.0}] \n", - "68 -1.009817 [{'index': 1, 'value': 1.0}] \n", - "70 0.296168 [{'index': 2, 'value': 1.0}] \n", - "72 -0.719598 [{'index': 2, 'value': 1.0}] \n", - "74 0.586387 [{'index': 1, 'value': 1.0}] \n", - "77 -0.647043 [{'index': 1, 'value': 1.0}] \n", - "81 0.804051 [{'index': 1, 'value': 1.0}] \n", - "91 0.005949 [{'index': 2, 'value': 1.0}] \n", - "96 0.731497 [{'index': 1, 'value': 1.0}] \n", - "105 -1.009817 [{'index': 1, 'value': 1.0}] \n", - "111 -2.098138 [{'index': 1, 'value': 1.0}] \n", - "\n", - " onehotencoded_species \n", - "penguin_id \n", - "1 [{'index': 1, 'value': 1.0}] \n", - "4 [{'index': 1, 'value': 1.0}] \n", - "8 [{'index': 3, 'value': 1.0}] \n", - "11 [{'index': 1, 'value': 1.0}] \n", - "13 [{'index': 1, 'value': 1.0}] \n", - "15 [{'index': 3, 'value': 1.0}] \n", - "16 [{'index': 1, 'value': 1.0}] \n", - "23 [{'index': 1, 'value': 1.0}] \n", - "34 [{'index': 3, 'value': 1.0}] \n", - "36 [{'index': 3, 'value': 1.0}] \n", - "42 [{'index': 1, 'value': 1.0}] \n", - "48 [{'index': 3, 'value': 1.0}] \n", - "61 [{'index': 3, 'value': 1.0}] \n", - "64 [{'index': 3, 'value': 1.0}] \n", - "65 [{'index': 1, 'value': 1.0}] \n", - "68 [{'index': 1, 'value': 1.0}] \n", - "70 [{'index': 2, 'value': 1.0}] \n", - "72 [{'index': 2, 'value': 1.0}] \n", - "74 [{'index': 3, 'value': 1.0}] \n", - "77 [{'index': 1, 'value': 1.0}] \n", - "81 [{'index': 3, 'value': 1.0}] \n", - "91 [{'index': 2, 'value': 1.0}] \n", - "96 [{'index': 3, 'value': 1.0}] \n", - "105 [{'index': 1, 'value': 1.0}] \n", - "111 [{'index': 1, 'value': 1.0}] \n", - "\n", - "[67 rows x 7 columns]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from bigframes.ml.linear_model import LinearRegression\n", - "\n", - "linreg = LinearRegression()\n", - "\n", - "# Learn from the training data how to predict output y\n", - "linreg.fit(processed_X_train, y_train)\n", - "\n", - "# Predict y for the test data\n", - "predicted_y_test = linreg.predict(processed_X_test)\n", - "\n", - "predicted_y_test" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Unsupervised predictors\n", - "\n", - "In unsupervised learning, there are no known outputs in the training data, instead the model learns on input data alone and predicts something else. An example of an unsupervised predictor is `bigframes.ml.cluster.KMeans`, which learns how to fit input data to a target number of clusters." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 027042f1-9a18-43d8-a378-ab9410e395b1 is DONE. 23.5 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 6c8484a0-a504-4e50-93d6-3d247c9ff558 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e81ca2de-df2e-41ec-af86-14f8dcec1b44 is DONE. 6.2 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 3e6d413c-f8c4-4390-95eb-3a1f5bc59aed is DONE. 536 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e448220d-0c50-45b7-bcbe-d1159b3d18ce is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e167a234-828d-4f05-8654-63cf97e50ba3 is DONE. 10.2 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
CENTROID_IDNEAREST_CENTROIDS_DISTANCEonehotencoded_islandstandard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mmonehotencoded_sexonehotencoded_species
penguin_id
13[{'CENTROID_ID': 3, 'DISTANCE': 1.236380597035...[{'index': 3, 'value': 1.0}]-0.9385870.748033-1.445145[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
43[{'CENTROID_ID': 3, 'DISTANCE': 1.039497631856...[{'index': 1, 'value': 1.0}]-0.167450.899528-0.284269[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
81[{'CENTROID_ID': 1, 'DISTANCE': 1.171040485975...[{'index': 1, 'value': 1.0}]0.453222-1.8778850.658942[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
112[{'CENTROID_ID': 2, 'DISTANCE': 0.969102754012...[{'index': 2, 'value': 1.0}]-1.126670.697535-0.792152[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
133[{'CENTROID_ID': 3, 'DISTANCE': 1.113138945949...[{'index': 1, 'value': 1.0}]-1.1830941.404513-0.792152[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
151[{'CENTROID_ID': 1, 'DISTANCE': 1.070996026772...[{'index': 1, 'value': 1.0}]0.867003-0.7669190.513833[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
163[{'CENTROID_ID': 3, 'DISTANCE': 1.780136190720...[{'index': 3, 'value': 1.0}]-1.7849581.959995-0.211715[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
232[{'CENTROID_ID': 2, 'DISTANCE': 1.382540667483...[{'index': 2, 'value': 1.0}]-0.3555320.647036-1.5177[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
341[{'CENTROID_ID': 1, 'DISTANCE': 1.598627908302...[{'index': 1, 'value': 1.0}]-0.600039-1.7768880.949161[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
361[{'CENTROID_ID': 1, 'DISTANCE': 1.095162305190...[{'index': 1, 'value': 1.0}]-0.129833-1.4233991.23938[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
422[{'CENTROID_ID': 2, 'DISTANCE': 1.275841743930...[{'index': 1, 'value': 1.0}]-1.615684-0.514427-0.429379[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
481[{'CENTROID_ID': 1, 'DISTANCE': 0.882209023196...[{'index': 1, 'value': 1.0}]0.415606-0.7164211.021716[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
611[{'CENTROID_ID': 1, 'DISTANCE': 0.816202832282...[{'index': 1, 'value': 1.0}]0.396797-1.1709071.457044[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
641[{'CENTROID_ID': 1, 'DISTANCE': 0.735435721625...[{'index': 1, 'value': 1.0}]0.434414-1.1204081.09427[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
652[{'CENTROID_ID': 2, 'DISTANCE': 1.292559869148...[{'index': 2, 'value': 1.0}]-1.2207111.051024-1.445145[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
682[{'CENTROID_ID': 2, 'DISTANCE': 0.876430138449...[{'index': 3, 'value': 1.0}]-1.484026-0.009443-1.009817[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
704[{'CENTROID_ID': 4, 'DISTANCE': 1.314229913955...[{'index': 2, 'value': 1.0}]1.6381411.4045130.296168[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
724[{'CENTROID_ID': 4, 'DISTANCE': 0.938569518009...[{'index': 2, 'value': 1.0}]0.8293870.142052-0.719598[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
741[{'CENTROID_ID': 1, 'DISTANCE': 1.350320088546...[{'index': 1, 'value': 1.0}]-0.242683-1.5243960.586387[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
772[{'CENTROID_ID': 2, 'DISTANCE': 0.904806634663...[{'index': 2, 'value': 1.0}]-1.277136-0.211437-0.647043[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
811[{'CENTROID_ID': 1, 'DISTANCE': 0.919082578073...[{'index': 1, 'value': 1.0}]0.208715-1.2214050.804051[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
914[{'CENTROID_ID': 4, 'DISTANCE': 0.760360038086...[{'index': 2, 'value': 1.0}]1.2619760.6470360.005949[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
961[{'CENTROID_ID': 1, 'DISTANCE': 0.950188657227...[{'index': 1, 'value': 1.0}]0.246331-1.3224020.731497[{'index': 1, 'value': 1.0}][{'index': 3, 'value': 1.0}]
1052[{'CENTROID_ID': 2, 'DISTANCE': 1.101316467029...[{'index': 1, 'value': 1.0}]-1.8037660.445043-1.009817[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1112[{'CENTROID_ID': 2, 'DISTANCE': 1.549061068385...[{'index': 1, 'value': 1.0}]-1.1642860.697535-2.098138[{'index': 1, 'value': 1.0}][{'index': 1, 'value': 1.0}]
\n", - "

25 rows × 8 columns

\n", - "
[67 rows x 8 columns in total]" - ], - "text/plain": [ - " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", - "penguin_id \n", - "1 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.236380597035... \n", - "4 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.039497631856... \n", - "8 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.171040485975... \n", - "11 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.969102754012... \n", - "13 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.113138945949... \n", - "15 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.070996026772... \n", - "16 3 [{'CENTROID_ID': 3, 'DISTANCE': 1.780136190720... \n", - "23 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.382540667483... \n", - "34 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.598627908302... \n", - "36 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.095162305190... \n", - "42 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.275841743930... \n", - "48 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.882209023196... \n", - "61 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.816202832282... \n", - "64 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.735435721625... \n", - "65 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.292559869148... \n", - "68 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.876430138449... \n", - "70 4 [{'CENTROID_ID': 4, 'DISTANCE': 1.314229913955... \n", - "72 4 [{'CENTROID_ID': 4, 'DISTANCE': 0.938569518009... \n", - "74 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.350320088546... \n", - "77 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.904806634663... \n", - "81 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.919082578073... \n", - "91 4 [{'CENTROID_ID': 4, 'DISTANCE': 0.760360038086... \n", - "96 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.950188657227... \n", - "105 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.101316467029... \n", - "111 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.549061068385... \n", - "\n", - " onehotencoded_island standard_scaled_culmen_length_mm \\\n", - "penguin_id \n", - "1 [{'index': 3, 'value': 1.0}] -0.938587 \n", - "4 [{'index': 1, 'value': 1.0}] -0.16745 \n", - "8 [{'index': 1, 'value': 1.0}] 0.453222 \n", - "11 [{'index': 2, 'value': 1.0}] -1.12667 \n", - "13 [{'index': 1, 'value': 1.0}] -1.183094 \n", - "15 [{'index': 1, 'value': 1.0}] 0.867003 \n", - "16 [{'index': 3, 'value': 1.0}] -1.784958 \n", - "23 [{'index': 2, 'value': 1.0}] -0.355532 \n", - "34 [{'index': 1, 'value': 1.0}] -0.600039 \n", - "36 [{'index': 1, 'value': 1.0}] -0.129833 \n", - "42 [{'index': 1, 'value': 1.0}] -1.615684 \n", - "48 [{'index': 1, 'value': 1.0}] 0.415606 \n", - "61 [{'index': 1, 'value': 1.0}] 0.396797 \n", - "64 [{'index': 1, 'value': 1.0}] 0.434414 \n", - "65 [{'index': 2, 'value': 1.0}] -1.220711 \n", - "68 [{'index': 3, 'value': 1.0}] -1.484026 \n", - "70 [{'index': 2, 'value': 1.0}] 1.638141 \n", - "72 [{'index': 2, 'value': 1.0}] 0.829387 \n", - "74 [{'index': 1, 'value': 1.0}] -0.242683 \n", - "77 [{'index': 2, 'value': 1.0}] -1.277136 \n", - "81 [{'index': 1, 'value': 1.0}] 0.208715 \n", - "91 [{'index': 2, 'value': 1.0}] 1.261976 \n", - "96 [{'index': 1, 'value': 1.0}] 0.246331 \n", - "105 [{'index': 1, 'value': 1.0}] -1.803766 \n", - "111 [{'index': 1, 'value': 1.0}] -1.164286 \n", - "\n", - " standard_scaled_culmen_depth_mm \\\n", - "penguin_id \n", - "1 0.748033 \n", - "4 0.899528 \n", - "8 -1.877885 \n", - "11 0.697535 \n", - "13 1.404513 \n", - "15 -0.766919 \n", - "16 1.959995 \n", - "23 0.647036 \n", - "34 -1.776888 \n", - "36 -1.423399 \n", - "42 -0.514427 \n", - "48 -0.716421 \n", - "61 -1.170907 \n", - "64 -1.120408 \n", - "65 1.051024 \n", - "68 -0.009443 \n", - "70 1.404513 \n", - "72 0.142052 \n", - "74 -1.524396 \n", - "77 -0.211437 \n", - "81 -1.221405 \n", - "91 0.647036 \n", - "96 -1.322402 \n", - "105 0.445043 \n", - "111 0.697535 \n", - "\n", - " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", - "penguin_id \n", - "1 -1.445145 [{'index': 2, 'value': 1.0}] \n", - "4 -0.284269 [{'index': 2, 'value': 1.0}] \n", - "8 0.658942 [{'index': 1, 'value': 1.0}] \n", - "11 -0.792152 [{'index': 1, 'value': 1.0}] \n", - "13 -0.792152 [{'index': 2, 'value': 1.0}] \n", - "15 0.513833 [{'index': 2, 'value': 1.0}] \n", - "16 -0.211715 [{'index': 2, 'value': 1.0}] \n", - "23 -1.5177 [{'index': 1, 'value': 1.0}] \n", - "34 0.949161 [{'index': 1, 'value': 1.0}] \n", - "36 1.23938 [{'index': 1, 'value': 1.0}] \n", - "42 -0.429379 [{'index': 1, 'value': 1.0}] \n", - "48 1.021716 [{'index': 2, 'value': 1.0}] \n", - "61 1.457044 [{'index': 2, 'value': 1.0}] \n", - "64 1.09427 [{'index': 1, 'value': 1.0}] \n", - "65 -1.445145 [{'index': 1, 'value': 1.0}] \n", - "68 -1.009817 [{'index': 1, 'value': 1.0}] \n", - "70 0.296168 [{'index': 2, 'value': 1.0}] \n", - "72 -0.719598 [{'index': 2, 'value': 1.0}] \n", - "74 0.586387 [{'index': 1, 'value': 1.0}] \n", - "77 -0.647043 [{'index': 1, 'value': 1.0}] \n", - "81 0.804051 [{'index': 1, 'value': 1.0}] \n", - "91 0.005949 [{'index': 2, 'value': 1.0}] \n", - "96 0.731497 [{'index': 1, 'value': 1.0}] \n", - "105 -1.009817 [{'index': 1, 'value': 1.0}] \n", - "111 -2.098138 [{'index': 1, 'value': 1.0}] \n", - "\n", - " onehotencoded_species \n", - "penguin_id \n", - "1 [{'index': 1, 'value': 1.0}] \n", - "4 [{'index': 1, 'value': 1.0}] \n", - "8 [{'index': 3, 'value': 1.0}] \n", - "11 [{'index': 1, 'value': 1.0}] \n", - "13 [{'index': 1, 'value': 1.0}] \n", - "15 [{'index': 3, 'value': 1.0}] \n", - "16 [{'index': 1, 'value': 1.0}] \n", - "23 [{'index': 1, 'value': 1.0}] \n", - "34 [{'index': 3, 'value': 1.0}] \n", - "36 [{'index': 3, 'value': 1.0}] \n", - "42 [{'index': 1, 'value': 1.0}] \n", - "48 [{'index': 3, 'value': 1.0}] \n", - "61 [{'index': 3, 'value': 1.0}] \n", - "64 [{'index': 3, 'value': 1.0}] \n", - "65 [{'index': 1, 'value': 1.0}] \n", - "68 [{'index': 1, 'value': 1.0}] \n", - "70 [{'index': 2, 'value': 1.0}] \n", - "72 [{'index': 2, 'value': 1.0}] \n", - "74 [{'index': 3, 'value': 1.0}] \n", - "77 [{'index': 1, 'value': 1.0}] \n", - "81 [{'index': 3, 'value': 1.0}] \n", - "91 [{'index': 2, 'value': 1.0}] \n", - "96 [{'index': 3, 'value': 1.0}] \n", - "105 [{'index': 1, 'value': 1.0}] \n", - "111 [{'index': 1, 'value': 1.0}] \n", - "\n", - "[67 rows x 8 columns]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from bigframes.ml.cluster import KMeans\n", - "\n", - "kmeans = KMeans(n_clusters=4)\n", - "\n", - "kmeans.fit(processed_X_train)\n", - "\n", - "kmeans.predict(processed_X_test)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Pipelines\n", - "\n", - "Transfomers and predictors can be chained into a single estimator component using `bigframes.ml.pipeline.Pipeline`:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Pipeline(steps=[('preproc',\n", - " ColumnTransformer(transformers=[('scale', StandardScaler(),\n", - " ['culmen_length_mm',\n", - " 'culmen_depth_mm',\n", - " 'flipper_length_mm']),\n", - " ('encode', OneHotEncoder(),\n", - " ['species', 'sex',\n", - " 'island'])])),\n", - " ('linreg', LinearRegression())])" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from bigframes.ml.pipeline import Pipeline\n", - "\n", - "pipeline = Pipeline([\n", - " ('preproc', preproc),\n", - " ('linreg', linreg)\n", - "])\n", - "\n", - "# Print our pipeline\n", - "pipeline" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The pipeline simplifies the workflow by applying each of its component steps automatically:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job b11be0d8-e6f1-41cb-8cb2-25a38e7ef311 is DONE. 24.7 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job f32ea25c-be39-4726-a8f5-604ae83849a6 is DONE. 8.5 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 86e29b78-76f5-4937-8bde-407b99af04a2 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job ca819734-0d41-4d9e-b743-09edae8c7fee is DONE. 29.6 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 49bb5bed-cc84-47e0-9a90-08ab01e00548 is DONE. 536 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 1e40a085-2289-47dd-afd8-820413186b9f is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 60319296-a480-4f51-b7ad-190ac6de963a is DONE. 6.2 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
predicted_body_mass_gislandculmen_length_mmculmen_depth_mmflipper_length_mmsexspecies
penguin_id
13781.396682Torgersen39.118.7181.0MALEAdelie Penguin (Pygoscelis adeliae)
44124.102574Biscoe43.219.0197.0MALEAdelie Penguin (Pygoscelis adeliae)
84670.338389Biscoe46.513.5210.0FEMALEGentoo penguin (Pygoscelis papua)
113529.411644Dream38.118.6190.0FEMALEAdelie Penguin (Pygoscelis adeliae)
134014.09632Biscoe37.820.0190.0MALEAdelie Penguin (Pygoscelis adeliae)
155212.407319Biscoe48.715.7208.0MALEGentoo penguin (Pygoscelis papua)
164163.590502Torgersen34.621.1198.0MALEAdelie Penguin (Pygoscelis adeliae)
233392.44731Dream42.218.5180.0FEMALEAdelie Penguin (Pygoscelis adeliae)
344698.299674Biscoe40.913.7214.0FEMALEGentoo penguin (Pygoscelis papua)
364828.221398Biscoe43.414.4218.0FEMALEGentoo penguin (Pygoscelis papua)
423430.582874Biscoe35.516.2195.0FEMALEAdelie Penguin (Pygoscelis adeliae)
485314.254798Biscoe46.315.8215.0MALEGentoo penguin (Pygoscelis papua)
615363.19995Biscoe46.214.9221.0MALEGentoo penguin (Pygoscelis papua)
644855.90281Biscoe46.415.0216.0FEMALEGentoo penguin (Pygoscelis papua)
653413.094869Dream37.619.3181.0FEMALEAdelie Penguin (Pygoscelis adeliae)
683340.213193Torgersen36.217.2187.0FEMALEAdelie Penguin (Pygoscelis adeliae)
704228.726508Dream52.820.0205.0MALEChinstrap penguin (Pygoscelis antarctica)
723811.532821Dream48.517.5191.0MALEChinstrap penguin (Pygoscelis antarctica)
744659.765013Biscoe42.814.2209.0FEMALEGentoo penguin (Pygoscelis papua)
773453.383042Dream37.316.8192.0FEMALEAdelie Penguin (Pygoscelis adeliae)
814766.239424Biscoe45.214.8212.0FEMALEGentoo penguin (Pygoscelis papua)
914057.801947Dream50.818.5201.0MALEChinstrap penguin (Pygoscelis antarctica)
964739.821792Biscoe45.414.6211.0FEMALEGentoo penguin (Pygoscelis papua)
1053394.886275Biscoe34.518.1187.0FEMALEAdelie Penguin (Pygoscelis adeliae)
1113201.48777Biscoe37.918.6172.0FEMALEAdelie Penguin (Pygoscelis adeliae)
\n", - "

25 rows × 7 columns

\n", - "
[67 rows x 7 columns in total]" - ], - "text/plain": [ - " predicted_body_mass_g island culmen_length_mm \\\n", - "penguin_id \n", - "1 3781.396682 Torgersen 39.1 \n", - "4 4124.102574 Biscoe 43.2 \n", - "8 4670.338389 Biscoe 46.5 \n", - "11 3529.411644 Dream 38.1 \n", - "13 4014.09632 Biscoe 37.8 \n", - "15 5212.407319 Biscoe 48.7 \n", - "16 4163.590502 Torgersen 34.6 \n", - "23 3392.44731 Dream 42.2 \n", - "34 4698.299674 Biscoe 40.9 \n", - "36 4828.221398 Biscoe 43.4 \n", - "42 3430.582874 Biscoe 35.5 \n", - "48 5314.254798 Biscoe 46.3 \n", - "61 5363.19995 Biscoe 46.2 \n", - "64 4855.90281 Biscoe 46.4 \n", - "65 3413.094869 Dream 37.6 \n", - "68 3340.213193 Torgersen 36.2 \n", - "70 4228.726508 Dream 52.8 \n", - "72 3811.532821 Dream 48.5 \n", - "74 4659.765013 Biscoe 42.8 \n", - "77 3453.383042 Dream 37.3 \n", - "81 4766.239424 Biscoe 45.2 \n", - "91 4057.801947 Dream 50.8 \n", - "96 4739.821792 Biscoe 45.4 \n", - "105 3394.886275 Biscoe 34.5 \n", - "111 3201.48777 Biscoe 37.9 \n", - "\n", - " culmen_depth_mm flipper_length_mm sex \\\n", - "penguin_id \n", - "1 18.7 181.0 MALE \n", - "4 19.0 197.0 MALE \n", - "8 13.5 210.0 FEMALE \n", - "11 18.6 190.0 FEMALE \n", - "13 20.0 190.0 MALE \n", - "15 15.7 208.0 MALE \n", - "16 21.1 198.0 MALE \n", - "23 18.5 180.0 FEMALE \n", - "34 13.7 214.0 FEMALE \n", - "36 14.4 218.0 FEMALE \n", - "42 16.2 195.0 FEMALE \n", - "48 15.8 215.0 MALE \n", - "61 14.9 221.0 MALE \n", - "64 15.0 216.0 FEMALE \n", - "65 19.3 181.0 FEMALE \n", - "68 17.2 187.0 FEMALE \n", - "70 20.0 205.0 MALE \n", - "72 17.5 191.0 MALE \n", - "74 14.2 209.0 FEMALE \n", - "77 16.8 192.0 FEMALE \n", - "81 14.8 212.0 FEMALE \n", - "91 18.5 201.0 MALE \n", - "96 14.6 211.0 FEMALE \n", - "105 18.1 187.0 FEMALE \n", - "111 18.6 172.0 FEMALE \n", - "\n", - " species \n", - "penguin_id \n", - "1 Adelie Penguin (Pygoscelis adeliae) \n", - "4 Adelie Penguin (Pygoscelis adeliae) \n", - "8 Gentoo penguin (Pygoscelis papua) \n", - "11 Adelie Penguin (Pygoscelis adeliae) \n", - "13 Adelie Penguin (Pygoscelis adeliae) \n", - "15 Gentoo penguin (Pygoscelis papua) \n", - "16 Adelie Penguin (Pygoscelis adeliae) \n", - "23 Adelie Penguin (Pygoscelis adeliae) \n", - "34 Gentoo penguin (Pygoscelis papua) \n", - "36 Gentoo penguin (Pygoscelis papua) \n", - "42 Adelie Penguin (Pygoscelis adeliae) \n", - "48 Gentoo penguin (Pygoscelis papua) \n", - "61 Gentoo penguin (Pygoscelis papua) \n", - "64 Gentoo penguin (Pygoscelis papua) \n", - "65 Adelie Penguin (Pygoscelis adeliae) \n", - "68 Adelie Penguin (Pygoscelis adeliae) \n", - "70 Chinstrap penguin (Pygoscelis antarctica) \n", - "72 Chinstrap penguin (Pygoscelis antarctica) \n", - "74 Gentoo penguin (Pygoscelis papua) \n", - "77 Adelie Penguin (Pygoscelis adeliae) \n", - "81 Gentoo penguin (Pygoscelis papua) \n", - "91 Chinstrap penguin (Pygoscelis antarctica) \n", - "96 Gentoo penguin (Pygoscelis papua) \n", - "105 Adelie Penguin (Pygoscelis adeliae) \n", - "111 Adelie Penguin (Pygoscelis adeliae) \n", - "\n", - "[67 rows x 7 columns]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pipeline.fit(X_train, y_train)\n", - "\n", - "predicted_y_test = pipeline.predict(X_test)\n", - "predicted_y_test" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the backend, a pipeline will actually be compiled into a single model with an embedded TRANSFORM step." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Evaluating results\n", - "\n", - "Some models include a convenient `.score(X, y)` method for evaulation with a preset accuracy metric:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job c02fb597-8d5a-42ca-9185-03b59c5ef2f9 is DONE. 29.6 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 7f1f565b-0f73-4a4e-b33f-8484fa260838 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job d4b9d4a6-d75e-46e1-b092-ab58e8aef890 is DONE. 48 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0216.44435772639.6987070.00463170.5883560.8963960.900547
\n", - "

1 rows × 6 columns

\n", - "
[1 rows x 6 columns in total]" - ], - "text/plain": [ - " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - "0 216.444357 72639.698707 0.00463 \n", - "\n", - " median_absolute_error r2_score explained_variance \n", - "0 170.588356 0.896396 0.900547 \n", - "\n", - "[1 rows x 6 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# In the case of a pipeline, this will be equivalent to calling .score on the contained LinearRegression\n", - "pipeline.score(X_test, y_test)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For a more general approach, the library `bigframes.ml.metrics` is provided:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 73448ee8-698b-435f-b11e-6fe2de3bcd8d is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e002f59d-a03c-4ec9-a85a-93adbfd7bd17 is DONE. 28.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 4ab1febc-fb55-473a-b295-69e4329cc5f0 is DONE. 30.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "0.8963962044533755" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from bigframes.ml.metrics import r2_score\n", - "\n", - "r2_score(y_test, predicted_y_test[\"predicted_body_mass_g\"])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Save/Load to BigQuery\n", - "\n", - "Estimators can be saved to BigQuery as BQML models, and loaded again in future.\n", - "\n", - "Saving requires `bigquery.tables.create` permission, and loading requires `bigquery.models.getMetadata` permission.\n", - "These permissions can be at project level or the dataset level.\n", - "\n", - "If you have those permissions, please go ahead and uncomment the code in the following cells and run." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "# # Replace with a path where you have permission to save a model\n", - "# model_name = \"bigframes-dev.bqml_tutorial.penguins_model\"\n", - "\n", - "# linreg.to_gbq(model_name, replace=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "# # WARNING - until b/281709360 is fixed & pipeline is updated, pipelines will load as models,\n", - "# # and details of their transform steps will be lost (the loaded model will behave the same)\n", - "# bigframes.pandas.read_gbq_model(model_name)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "a850322d07d9bdc9ec5f301d307e048bcab2390ae395e1cbce9335f4e081e5e2" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb b/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb new file mode 100644 index 00000000000..089c167d397 --- /dev/null +++ b/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb @@ -0,0 +1,970 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Machine Learning Fundamentals with BigQuery DataFrames\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "The `bigframes.ml` module implements Scikit-Learn's machine learning API in\n", + "BigQuery DataFrames. It exposes BigQuery's ML capabilities in a simple, popular\n", + "API that works seamlessly with the rest of the BigQuery DataFrames API.\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you will walk through an end-to-end machine learning workflow using BigQuery DataFrames. You will load data, manipulate and prepare it for model training, build supervised and unsupervised models, and evaluate and save a model for future use; all using built-in BigQuery DataFrames functionality." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) (a BigQuery public dataset), which contains data on a set of penguins including species, island of residence, weight, culmen length and depth, flipper length, and sex." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (storage and compute)\n", + "* BigQuery ML\n", + "\n", + "Learn about [BigQuery storage pricing](https://cloud.google.com/bigquery/pricing#storage),\n", + "[BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Depending on your Jupyter environment, you might have to install packages." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NRTcBQPZpKWd" + }, + "source": [ + "**Vertex AI Workbench or Colab**\n", + "\n", + "Do nothing, BigQuery DataFrames package is already installed." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bdOJtFo1pRnc" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mfPoOwPLGpSr" + }, + "outputs": [], + "source": [ + "# !pip install bigframes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yq7zKYWelRQP" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com) to enable the BigQuery API.\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XcW9adriUQRc" + }, + "source": [ + "#### Set the dataset ID\n", + "\n", + "As part of this notebook, you will save BigQuery ML models to your Google Cloud project, which requires a dataset. Create the dataset, if needed, and provide the ID here as the `DATASET` variable used by BigQuery. Learn how to create a [BigQuery dataset](https://cloud.google.com/bigquery/docs/datasets)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BbMh9JHvUHAn" + }, + "outputs": [], + "source": [ + "DATASET = \"\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NwxfWoR5UGwO" + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "\n", + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pDfrKwMKE_dK" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LjfRpSruzg5j" + }, + "source": [ + "## Import data into BigQuery DataFrames\n", + "\n", + "You can create a DataFrame by reading data from a BigQuery table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d86W4hNqzZJb" + }, + "outputs": [], + "source": [ + "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", + "df = df.dropna()\n", + "\n", + "# BigQuery DataFrames creates a default numbered index, which we can give a name\n", + "df.index.name = \"penguin_id\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pDfCJ6-LkRB1" + }, + "source": [ + "Take a look at a few rows of the DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "arGaUZVWkSwT" + }, + "outputs": [], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WkUIcMXPkahu" + }, + "source": [ + "## Clean and prepare data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DScncEoDkiTG" + }, + "source": [ + "We're are going to start with supervised learning, where a Linear Regression model will learn to predict the body mass (output variable `y`) using input features such as flipper length, sex, species, and more (features `X`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "B9mW93o9z_-L" + }, + "outputs": [], + "source": [ + "# Isolate input features and output variable into DataFrames\n", + "X = df[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex', 'species']]\n", + "y = df[['body_mass_g']]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wkw0Cs62k_cl" + }, + "source": [ + "Part of preparing data for a machine learning task is splitting it into subsets for training and testing to ensure that the solution is not overfitting. By default, BQML will automatically manage splitting the data for you. However, BQML also supports manually splitting out your training data.\n", + "\n", + "Performing a manual data split can be done with `bigframes.ml.model_selection.train_test_split` like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NysWAWmvlAxB" + }, + "outputs": [], + "source": [ + "from bigframes.ml.model_selection import train_test_split\n", + "\n", + "# This will split X and y into test and training sets, with 20% of the rows in the test set,\n", + "# and the rest in the training set\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.2)\n", + "\n", + "# Show the shape of the data after the split\n", + "print(f\"\"\"X_train shape: {X_train.shape}\n", + "X_test shape: {X_test.shape}\n", + "y_train shape: {y_train.shape}\n", + "y_test shape: {y_test.shape}\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "faFnVnNolydu" + }, + "source": [ + "If we look at the data, we can see that random rows were selected for\n", + "each side of the split:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f8bz1HwLlyLP" + }, + "outputs": [], + "source": [ + "X_test.head(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v4ic7GQEl67Y" + }, + "source": [ + "Note that the `y_test` data matches the same rows in `X_test`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PflbhKGkl8v2" + }, + "outputs": [], + "source": [ + "y_test.head(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Dkf52IdvmSaj" + }, + "source": [ + "## Estimators\n", + "\n", + "Following Scikit-Learn, all learning components are \"estimators\"; objects that can learn from training data and then apply themselves to new data. Estimators share the following patterns:\n", + "\n", + "- a constructor that takes a list of parameters\n", + "- a standard string representation that shows the class name and all non-default parameters, e.g. `LinearRegression(fit_intercept=False)`\n", + "- a `.fit(..)` method to fit the estimator to training data\n", + "\n", + "There estimators can be further broken down into two main subtypes:\n", + " 1. Transformers\n", + " 2. Predictors\n", + "\n", + "Let's walk through each of these with our example model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "55oNSWQ2Q5te" + }, + "source": [ + "### Transformers\n", + "\n", + "Transformers are estimators that are used to prepare data for consumption by other estimators ('preprocessing'). In addition to `.fit(...)`, the transformer implements a `.transform(...)` method, which will apply a transformation based on what was computed during `.fit(..)`. With this pattern dynamic preprocessing steps can be applied to both training and test/production data consistently.\n", + "\n", + "An example of a transformer is `bigframes.ml.preprocessing.StandardScaler`, which rescales a dataset to have a mean of zero and a standard deviation of one:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yhATDMR-mkdF" + }, + "outputs": [], + "source": [ + "from bigframes.ml.preprocessing import StandardScaler\n", + "\n", + "# StandardScaler will only work on numeric columns\n", + "numeric_columns = [\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]\n", + "\n", + "scaler = StandardScaler()\n", + "scaler.fit(X_train[numeric_columns])\n", + "\n", + "# Now, standardscaler should transform the numbers to have mean of zero\n", + "# and standard deviation of one:\n", + "scaler.transform(X_train[numeric_columns])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vhywHzH-ml-W" + }, + "source": [ + "We can then repeat this transformation on the test data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TfwSLOTXmspI" + }, + "outputs": [], + "source": [ + "scaler.transform(X_test[numeric_columns])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9enAdjzPmwmv" + }, + "source": [ + "#### Composing transformers\n", + "\n", + "To process data where different columns need different preprocessors, `bigframes.composition.ColumnTransformer` can be employed.\n", + "\n", + "Let's create an aggregate transform that applies `StandardScalar` to the numeric columns and `OneHotEncoder` to the string columns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "I8Wwx3emmz2J" + }, + "outputs": [], + "source": [ + "from bigframes.ml.compose import ColumnTransformer\n", + "from bigframes.ml.preprocessing import OneHotEncoder\n", + "\n", + "# Create an aggregate transform that applies StandardScaler to the numeric columns,\n", + "# and OneHotEncoder to the string columns\n", + "preproc = ColumnTransformer([\n", + " (\"scale\", StandardScaler(), [\"culmen_length_mm\", \"culmen_depth_mm\", \"flipper_length_mm\"]),\n", + " (\"encode\", OneHotEncoder(), [\"species\", \"sex\", \"island\"])])\n", + "\n", + "# Now we can fit all columns of the training data\n", + "preproc.fit(X_train)\n", + "\n", + "processed_X_train = preproc.transform(X_train)\n", + "processed_X_test = preproc.transform(X_test)\n", + "\n", + "# View the processed training data\n", + "processed_X_train" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JhoO4fctm4Q5" + }, + "source": [ + "### Predictors\n", + "\n", + "Predictors are estimators that learn and make predictions. In addition to `.fit(...)`, the predictor implements a `.predict(...)` method, which will use what was learned during `.fit(...)` to predict some output.\n", + "\n", + "Predictors can be further broken down into two categories:\n", + "* Supervised predictors\n", + "* Unsupervised predictors" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TqLItVyjslP8" + }, + "source": [ + "#### Supervised predictors\n", + "\n", + "Supervised learning is when we train a model on input-output pairs, and then ask it to predict the output for new inputs. An example of such a predictor is `bigframes.ml.linear_models.LinearRegression`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZeloMmopm8KI" + }, + "outputs": [], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "linreg = LinearRegression()\n", + "\n", + "# Learn from the training data how to predict output y\n", + "linreg.fit(processed_X_train, y_train)\n", + "\n", + "# Predict y for the test data\n", + "predicted_y_test = linreg.predict(processed_X_test)\n", + "\n", + "# View predictions\n", + "predicted_y_test" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "z42qesW_nAIf" + }, + "source": [ + "#### Unsupervised predictors\n", + "\n", + "In unsupervised learning, there are no known outputs in the training data, instead the model learns on input data alone and predicts something else. An example of an unsupervised predictor is `bigframes.ml.cluster.KMeans`, which learns how to fit input data to a target number of clusters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "M13zd02znCIg" + }, + "outputs": [], + "source": [ + "from bigframes.ml.cluster import KMeans\n", + "\n", + "# Specify KMeans with four clusters\n", + "kmeans = KMeans(n_clusters=4)\n", + "\n", + "# Fit data\n", + "kmeans.fit(processed_X_train)\n", + "\n", + "# View predictions\n", + "kmeans.predict(processed_X_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DFwsIbscnEvh" + }, + "source": [ + "## Pipelines\n", + "\n", + "Transfomers and predictors can be chained into a single estimator component using `bigframes.ml.pipeline.Pipeline`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ku2OXqgJnEeR" + }, + "outputs": [], + "source": [ + "from bigframes.ml.pipeline import Pipeline\n", + "\n", + "pipeline = Pipeline([\n", + " ('preproc', preproc),\n", + " ('linreg', linreg)\n", + "])\n", + "\n", + "# Print our pipeline\n", + "pipeline" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cCQCY_6wnKz_" + }, + "source": [ + "The pipeline simplifies the workflow by applying each of its component steps automatically:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hsF7FYagnMko" + }, + "outputs": [], + "source": [ + "pipeline.fit(X_train, y_train)\n", + "\n", + "predicted_y_test = pipeline.predict(X_test)\n", + "predicted_y_test" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SiLzpsg8nRXn" + }, + "source": [ + "In the backend, a pipeline will actually be compiled into a single model with an embedded TRANSFORM step." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sTzAxTv1nUKZ" + }, + "source": [ + "## Evaluating results\n", + "\n", + "Some models include a convenient `.score(X, y)` method for evaulation with a preset accuracy metric:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Q8nR1ZqznU-B" + }, + "outputs": [], + "source": [ + "# In the case of a pipeline, this will be equivalent to calling .score on the contained LinearRegression\n", + "pipeline.score(X_test, y_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UHM7jls6nY8A" + }, + "source": [ + "For a more general approach, the library `bigframes.ml.metrics` is provided:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vdEN4Ob9nan4" + }, + "outputs": [], + "source": [ + "from bigframes.ml.metrics import r2_score\n", + "\n", + "r2_score(y_test, predicted_y_test[\"predicted_body_mass_g\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "opn4ycPyneVh" + }, + "source": [ + "## Save to BigQuery\n", + "\n", + "Estimators can be saved to BigQuery as BQML models, and loaded again in future.\n", + "\n", + "Saving requires `bigquery.tables.create` permission, and loading requires `bigquery.models.getMetadata` permission.\n", + "These permissions can be at project level or the dataset level.\n", + "\n", + "If you have those permissions, please go ahead and uncomment the code in the following cells and run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fb0HpkdpnigJ" + }, + "outputs": [], + "source": [ + "linreg.to_gbq(f\"{DATASET}.penguins_model\", replace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_zNOBlHdnkII" + }, + "outputs": [], + "source": [ + "bf.read_gbq_model(f\"{DATASET}.penguins_model\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RfV-du5uTcBB" + }, + "source": [ + "We can also save the pipeline to BigQuery. BigQuery will save this as a single model, with the pre-processing steps embedded in the TRANSFORM property:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P76_TQ3IR6nB" + }, + "outputs": [], + "source": [ + "pipeline.to_gbq(f\"{DATASET}.penguins_pipeline\", replace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GKvlKFjAbToJ" + }, + "outputs": [], + "source": [ + "bf.read_gbq_model(f\"{DATASET}.penguins_pipeline\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wCsmt0IwFkDy" + }, + "source": [ + "## Summary and next steps\n", + "\n", + "You've completed an end-to-end machine learning workflow using the built-in capabilities of BigQuery DataFrames.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "### Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QwumLUKmVpuH" + }, + "outputs": [], + "source": [ + "# # Delete the BQML models\n", + "# MODEL_NAME = f\"{PROJECT_ID}:{DATASET}.penguins_model\"\n", + "# ! bq rm -f --model {MODEL_NAME}\n", + "# PIPELINE_NAME = f\"{PROJECT_ID}:{DATASET}.penguins_pipeline\"\n", + "# ! bq rm -f --model {PIPELINE_NAME}" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From f3cd2f455586c14dd48056ca7b3ca2417b69689a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:20:11 -0800 Subject: [PATCH 267/921] chore(main): release 0.19.1 (#311) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ bigframes/version.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1691e1f64c..14c8050d80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.19.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.19.0...v0.19.1) (2024-01-17) + + +### Bug Fixes + +* Handle multi-level columns for df aggregates properly ([#305](https://github.com/googleapis/python-bigquery-dataframes/issues/305)) ([5bb45ba](https://github.com/googleapis/python-bigquery-dataframes/commit/5bb45ba5560f178438d490a62520ccd36fd2f284)) +* Update max_output_token limitation. ([#308](https://github.com/googleapis/python-bigquery-dataframes/issues/308)) ([5cccd36](https://github.com/googleapis/python-bigquery-dataframes/commit/5cccd36fd2081becd741541c4ac8d5cf53c076f2)) + + +### Documentation + +* Add code samples for Series.corr ([#316](https://github.com/googleapis/python-bigquery-dataframes/issues/316)) ([9150c16](https://github.com/googleapis/python-bigquery-dataframes/commit/9150c16e951fb757547721e0003910c7c49e3d27)) + ## [0.19.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.18.0...v0.19.0) (2024-01-09) diff --git a/bigframes/version.py b/bigframes/version.py index aeefff7a4b1..043d22fd117 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.0" +__version__ = "0.19.1" From 57bc9ab7fb9eb33d2aaac504b025b5ef5bcd2771 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 17 Jan 2024 14:20:47 -0600 Subject: [PATCH 268/921] refactor: move remote_function logic to make a space for other types (#322) --- bigframes/functions/__init__.py | 13 +++++++++++++ bigframes/{ => functions}/remote_function.py | 0 bigframes/session/__init__.py | 4 ++-- tests/system/large/test_remote_function.py | 2 +- tests/system/small/test_remote_function.py | 2 +- tests/unit/test_dtypes.py | 2 +- tests/unit/test_remote_function.py | 2 +- 7 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 bigframes/functions/__init__.py rename bigframes/{ => functions}/remote_function.py (100%) diff --git a/bigframes/functions/__init__.py b/bigframes/functions/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/bigframes/functions/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/bigframes/remote_function.py b/bigframes/functions/remote_function.py similarity index 100% rename from bigframes/remote_function.py rename to bigframes/functions/remote_function.py diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index d503b844aa8..2e630e4fadb 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -74,8 +74,8 @@ import bigframes.core.utils as utils import bigframes.dataframe as dataframe import bigframes.formatting_helpers as formatting_helpers -from bigframes.remote_function import read_gbq_function as bigframes_rgf -from bigframes.remote_function import remote_function as bigframes_rf +from bigframes.functions.remote_function import read_gbq_function as bigframes_rgf +from bigframes.functions.remote_function import remote_function as bigframes_rf import bigframes.session._io.bigquery as bigframes_io import bigframes.session.clients import bigframes.version diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 4b4c794a050..773de48adf1 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -28,7 +28,7 @@ import test_utils.prefixer import bigframes -from bigframes.remote_function import ( +from bigframes.functions.remote_function import ( get_cloud_function_name, get_remote_function_locations, ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index a98056d82af..1cf494ea6b0 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -17,7 +17,7 @@ import pytest import bigframes -from bigframes import remote_function as rf +from bigframes.functions import remote_function as rf from tests.system.utils import assert_pandas_df_equal diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index e648fd28ccb..047ad5638dd 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -241,7 +241,7 @@ def test_literal_to_ibis_scalar_throws_on_incompatible_literal(): def test_remote_function_io_types_are_supported_bigframes_types(): from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type - from bigframes.remote_function import ( + from bigframes.functions.remote_function import ( SUPPORTED_IO_PYTHON_TYPES as rf_supported_io_types, ) diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py index 540f4020d36..ed24ada6c70 100644 --- a/tests/unit/test_remote_function.py +++ b/tests/unit/test_remote_function.py @@ -15,7 +15,7 @@ from ibis.backends.bigquery import datatypes as bq_types from ibis.expr import datatypes as ibis_types -from bigframes import remote_function as rf +from bigframes.functions import remote_function as rf def test_supported_types_correspond(): From f45c078f5086a7098fb0ba3fbd1447ccf637a3e1 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:43:55 -0800 Subject: [PATCH 269/921] chore: add ml_fundamentals_bq_dataframes.ipynb to denylist. (#325) --- noxfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/noxfile.py b/noxfile.py index f25cb9fa10b..62fbee2650e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -664,6 +664,7 @@ def notebook(session): # appropriate values and omitting cleanup logic that may break # our test infrastructure. "notebooks/getting_started/getting_started_bq_dataframes.ipynb", + "notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb", "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", "notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb", "notebooks/regression/bq_dataframes_ml_linear_regression.ipynb", From bd531a1557c08bcee6a0d275747f0939cdd33e81 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 18 Jan 2024 18:29:12 +0000 Subject: [PATCH 270/921] docs: Add `DataFrame.applymap` documentation (#326) * docs: Add `DataFrame.applymap` documentation * fix rendering in the remote function requirements section --- README.rst | 2 +- third_party/bigframes_vendored/pandas/core/frame.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 91dac127518..246b9270fab 100644 --- a/README.rst +++ b/README.rst @@ -304,7 +304,7 @@ following IAM roles: * BigQuery Connection Admin (roles/bigquery.connectionAdmin) * Cloud Functions Developer (roles/cloudfunctions.developer) * Service Account User (roles/iam.serviceAccountUser) on the - `service account ` + `service account `__ ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` * Storage Object Viewer (roles/storage.objectViewer) * Project IAM Admin (roles/resourcemanager.projectIamAdmin) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index bca18bd0b79..10cdbf8f7cf 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2961,7 +2961,6 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: to every element of a DataFrame. .. note:: - In pandas 2.1.0, DataFrame.applymap is deprecated and renamed to DataFrame.map. @@ -3038,6 +3037,8 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + applymap = map + # ---------------------------------------------------------------------- # Merging / joining methods From 32cc6fa73dea80e31985d380d550d8042e5f5566 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:20:57 -0800 Subject: [PATCH 271/921] docs: add code samples for series methods (#323) --- .../bigframes_vendored/pandas/core/series.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 10a4c195abe..1aa4ffffbb5 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -2711,6 +2711,31 @@ def argmax(self): If the minimum is achieved in multiple locations, the first row position is returned. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Consider dataset containing cereal calories. + + >>> s = bpd.Series({'Corn Flakes': 100.0, 'Almond Delight': 110.0, + ... 'Cinnamon Toast Crunch': 120.0, 'Cocoa Puff': 110.0}) + >>> s + Corn Flakes 100.0 + Almond Delight 110.0 + Cinnamon Toast Crunch 120.0 + Cocoa Puff 110.0 + dtype: Float64 + + >>> s.argmax() + 2 + + >>> s.argmin() + 0 + + The maximum cereal calories is the third element and the minimum cereal + calories is the first element, since series is zero-indexed. + Returns: Series: Row position of the maximum value. """ @@ -2722,6 +2747,31 @@ def argmin(self): If the maximum is achieved in multiple locations, the first row position is returned. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Consider dataset containing cereal calories. + + >>> s = bpd.Series({'Corn Flakes': 100.0, 'Almond Delight': 110.0, + ... 'Cinnamon Toast Crunch': 120.0, 'Cocoa Puff': 110.0}) + >>> s + Corn Flakes 100.0 + Almond Delight 110.0 + Cinnamon Toast Crunch 120.0 + Cocoa Puff 110.0 + dtype: Float64 + + >>> s.argmax() + 2 + + >>> s.argmin() + 0 + + The maximum cereal calories is the third element and the minimum cereal + calories is the first element, since series is zero-indexed. + Returns: Series: Row position of the minimum value. """ @@ -2971,6 +3021,19 @@ def is_monotonic_increasing(self) -> bool: """ Return boolean if values in the object are monotonically increasing. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 2]) + >>> s.is_monotonic_increasing + True + + >>> s = bpd.Series([3, 2, 1]) + >>> s.is_monotonic_increasing + False + Returns: bool: Boolean. """ @@ -2981,6 +3044,19 @@ def is_monotonic_decreasing(self) -> bool: """ Return boolean if values in the object are monotonically decreasing. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([3, 2, 2, 1]) + >>> s.is_monotonic_decreasing + True + + >>> s = bpd.Series([1, 2, 3]) + >>> s.is_monotonic_decreasing + False + Returns: bool: Boolean. """ From 972e92492d23ef309930cdcd50600651e226c9dd Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:50:38 -0800 Subject: [PATCH 272/921] chore: delete the connection requirement in llm (#330) --- .../bq_dataframes_llm_code_generation.ipynb | 83 +------------------ 1 file changed, 2 insertions(+), 81 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index 0a41447a53e..b2966c404c0 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -370,83 +370,6 @@ "This section walks through a few steps required in order to use the model in your notebook." ] }, - { - "cell_type": "markdown", - "metadata": { - "id": "rS4VO1TGiO4G" - }, - "source": [ - "## Create a BigQuery Cloud resource connection\n", - "\n", - "You need to create a [Cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection) to enable BigQuery DataFrames to interact with Vertex AI services." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "KFPjDM4LVh96" - }, - "outputs": [], - "source": [ - "CONN_NAME = \"bqdf-llm\"\n", - "\n", - "client = bq_connection.ConnectionServiceClient()\n", - "new_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}\"\n", - "exists_conn_parent = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", - "cloud_resource_properties = bq_connection.CloudResourceProperties({})\n", - "\n", - "try:\n", - " request = client.get_connection(\n", - " request=bq_connection.GetConnectionRequest(name=exists_conn_parent)\n", - " )\n", - " CONN_SERVICE_ACCOUNT = f\"serviceAccount:{request.cloud_resource.service_account_id}\"\n", - "except Exception:\n", - " connection = bq_connection.types.Connection(\n", - " {\"friendly_name\": CONN_NAME, \"cloud_resource\": cloud_resource_properties}\n", - " )\n", - " request = bq_connection.CreateConnectionRequest(\n", - " {\n", - " \"parent\": new_conn_parent,\n", - " \"connection_id\": CONN_NAME,\n", - " \"connection\": connection,\n", - " }\n", - " )\n", - " response = client.create_connection(request)\n", - " CONN_SERVICE_ACCOUNT = (\n", - " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", - " )\n", - "print(CONN_SERVICE_ACCOUNT)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "W6l6Ol2biU9h" - }, - "source": [ - "## Set permissions for the service account\n", - "\n", - "The resource connection service account requires certain project-level permissions:\n", - " - `roles/aiplatform.user` and `roles/bigquery.connectionUser`: These roles are required for the connection to create a model definition using the LLM model in Vertex AI ([documentation](https://cloud.google.com/bigquery/docs/generate-text#give_the_service_account_access)).\n", - " - `roles/run.invoker`: This role is required for the connection to have read-only access to Cloud Run services that back custom/remote functions ([documentation](https://cloud.google.com/bigquery/docs/remote-functions#grant_permission_on_function)).\n", - "\n", - "Set these permissions by running the following `gcloud` commands:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "d8wja24SVq6s" - }, - "outputs": [], - "source": [ - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'" - ] - }, { "cell_type": "markdown", "metadata": { @@ -468,9 +391,7 @@ "source": [ "from bigframes.ml.llm import PaLM2TextGenerator\n", "\n", - "session = bf.get_global_session()\n", - "connection = f\"{PROJECT_ID}.{REGION}.{CONN_NAME}\"\n", - "model = PaLM2TextGenerator(session=session, connection_name=connection)" + "model = PaLM2TextGenerator()" ] }, { @@ -651,7 +572,7 @@ }, "outputs": [], "source": [ - "@bf.remote_function([str], str, bigquery_connection=CONN_NAME)\n", + "@bf.remote_function([str], str)\n", "def extract_code(text: str):\n", " try:\n", " res = text[text.find('\\n')+1:text.find('```', 3)]\n", From b8178b9a47958d9176d99dfd8833556a64d9724d Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:53:04 -0800 Subject: [PATCH 273/921] fix: read_gbq large response issue (#332) * fix: read_gbq large size return issue * delete temp test --- bigframes/session/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 2e630e4fadb..9e1e6b560ab 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -380,7 +380,7 @@ def _query_to_destination( try: # Write to temp table to workaround BigQuery 10 GB query results # limit. See: internal issue 303057336. - job_config.labels["error_caught"] = "True" + job_config.labels["error_caught"] = "true" _, query_job = self._start_query(query, job_config=job_config) return query_job.destination, query_job except google.api_core.exceptions.BadRequest: From c7aa1af0f845a375da94a1a3393c6ebfe6dcf22b Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 19 Jan 2024 14:20:19 -0800 Subject: [PATCH 274/921] refactor: simplify filter and join nodes (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 33 +++++------ bigframes/core/blocks.py | 39 +++++++++---- bigframes/core/compile/compiled.py | 32 +++-------- bigframes/core/compile/compiler.py | 18 +++--- bigframes/core/compile/row_identity.py | 41 ++++++++------ bigframes/core/compile/single_column.py | 61 +++++++-------------- bigframes/core/indexes/index.py | 73 ++++++++++++++++++++----- bigframes/core/join_def.py | 59 ++++++++++++++++++++ bigframes/core/joins/__init__.py | 4 +- bigframes/core/joins/name_resolution.py | 5 -- bigframes/core/nodes.py | 14 +---- 11 files changed, 223 insertions(+), 156 deletions(-) create mode 100644 bigframes/core/join_def.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 8c08698b93e..8c08d073d7e 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -16,7 +16,7 @@ from dataclasses import dataclass import io import typing -from typing import Iterable, Literal, Sequence +from typing import Iterable, Sequence import ibis.expr.types as ibis_types import pandas @@ -24,12 +24,14 @@ import bigframes.core.compile as compiling import bigframes.core.expression as ex import bigframes.core.guid +import bigframes.core.join_def as join_def import bigframes.core.nodes as nodes from bigframes.core.ordering import OrderingColumnReference import bigframes.core.ordering as orderings import bigframes.core.utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes +import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.session._io.bigquery @@ -114,13 +116,15 @@ def row_count(self) -> ArrayValue: return ArrayValue(nodes.RowCountNode(child=self.node)) # Operations - def filter(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: + def filter_by_id(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - return ArrayValue( - nodes.FilterNode( - child=self.node, predicate_id=predicate_id, keep_null=keep_null - ) - ) + predicate = ex.free_var(predicate_id) + if keep_null: + predicate = ops.fillna_op.as_expr(predicate, ex.const(True)) + return self.filter(predicate) + + def filter(self, predicate: ex.Expression): + return ArrayValue(nodes.FilterNode(child=self.node, predicate=predicate)) def order_by(self, by: Sequence[OrderingColumnReference]) -> ArrayValue: return ArrayValue(nodes.OrderByNode(child=self.node, by=tuple(by))) @@ -356,26 +360,15 @@ def unpivot( def join( self, - self_column_ids: typing.Sequence[str], other: ArrayValue, - other_column_ids: typing.Sequence[str], - *, - how: Literal[ - "inner", - "left", - "outer", - "right", - "cross", - ], + join_def: join_def.JoinDefinition, allow_row_identity_join: bool = True, ): return ArrayValue( nodes.JoinNode( left_child=self.node, right_child=other.node, - left_column_ids=tuple(self_column_ids), - right_column_ids=tuple(other_column_ids), - how=how, + join=join_def, allow_row_identity_join=allow_row_identity_join, ) ) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 8c59f8106b1..3ec0419c6dc 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -38,7 +38,7 @@ import bigframes.core.expression as ex import bigframes.core.guid as guid import bigframes.core.indexes as indexes -import bigframes.core.joins.name_resolution as join_names +import bigframes.core.join_def as join_defs import bigframes.core.ordering as ordering import bigframes.core.utils import bigframes.core.utils as utils @@ -826,7 +826,7 @@ def assign_label(self, column_id: str, new_label: Label) -> Block: def filter(self, column_id: str, keep_null: bool = False): return Block( - self._expr.filter(column_id, keep_null), + self._expr.filter_by_id(column_id, keep_null), index_columns=self.index_columns, column_labels=self.column_labels, index_labels=self.index.names, @@ -1542,19 +1542,38 @@ def merge( sort: bool, suffixes: tuple[str, str] = ("_x", "_y"), ) -> Block: - joined_expr = self.expr.join( - left_join_ids, - other.expr, - right_join_ids, - how=how, - ) - get_column_left, get_column_right = join_names.JOIN_NAME_REMAPPER( - self.expr.column_ids, other.expr.column_ids + left_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.LEFT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in self.expr.column_ids + ] + right_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.RIGHT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in other.expr.column_ids + ] + + join_def = join_defs.JoinDefinition( + conditions=tuple( + join_defs.JoinCondition(left, right) + for left, right in zip(left_join_ids, right_join_ids) + ), + mappings=(*left_mappings, *right_mappings), + type=how, ) + joined_expr = self.expr.join(other.expr, join_def=join_def) result_columns = [] matching_join_labels = [] coalesced_ids = [] + get_column_left = join_def.get_left_mapping() + get_column_right = join_def.get_right_mapping() for left_id, right_id in zip(left_join_ids, right_join_ids): coalesced_id = guid.generate_guid() joined_expr = joined_expr.project_to_id( diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 2cab6fb95d5..eaaf692a170 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -96,8 +96,8 @@ def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: ) @abc.abstractmethod - def filter(self: T, predicate_id: str, keep_null: bool = False) -> T: - """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" + def filter(self: T, predicate: ex.Expression) -> T: + """Filter the table on a given expression, the predicate must be a boolean expression.""" ... @abc.abstractmethod @@ -305,17 +305,9 @@ def _to_ibis_expr( table = table.filter(ibis.random() < ibis.literal(fraction)) return table - def filter(self, predicate_id: str, keep_null: bool = False) -> UnorderedIR: - condition = typing.cast( - ibis_types.BooleanValue, self._get_ibis_column(predicate_id) - ) - if keep_null: - condition = typing.cast( - ibis_types.BooleanValue, - condition.fillna( - typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) - ), - ) + def filter(self, predicate: ex.Expression) -> UnorderedIR: + bindings = {col: self._get_ibis_column(col) for col in self.column_ids} + condition = op_compiler.compile_expression(predicate, bindings) return self._filter(condition) def _filter(self, predicate_value: ibis_types.BooleanValue) -> UnorderedIR: @@ -1140,17 +1132,9 @@ def _to_ibis_expr( table = table.filter(ibis.random() < ibis.literal(fraction)) return table - def filter(self, predicate_id: str, keep_null: bool = False) -> OrderedIR: - condition = typing.cast( - ibis_types.BooleanValue, self._get_ibis_column(predicate_id) - ) - if keep_null: - condition = typing.cast( - ibis_types.BooleanValue, - condition.fillna( - typing.cast(ibis_types.BooleanScalar, ibis_types.literal(True)) - ), - ) + def filter(self, predicate: ex.Expression) -> OrderedIR: + bindings = {col: self._get_ibis_column(col) for col in self.column_ids} + condition = op_compiler.compile_expression(predicate, bindings) return self._filter(condition) def _filter(self, predicate_value: ibis_types.BooleanValue) -> OrderedIR: diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 18fcd73d193..c948f0bdef2 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -59,22 +59,18 @@ def compile_join(node: nodes.JoinNode, ordered: bool = True): left_ordered = compile_ordered(node.left_child) right_ordered = compile_ordered(node.right_child) return bigframes.core.compile.single_column.join_by_column_ordered( - left_ordered, - node.left_column_ids, - right_ordered, - node.right_column_ids, - how=node.how, + left=left_ordered, + right=right_ordered, + join=node.join, allow_row_identity_join=node.allow_row_identity_join, ) else: left_unordered = compile_unordered(node.left_child) right_unordered = compile_unordered(node.right_child) return bigframes.core.compile.single_column.join_by_column_unordered( - left_unordered, - node.left_column_ids, - right_unordered, - node.right_column_ids, - how=node.how, + left=left_unordered, + right=right_unordered, + join=node.join, allow_row_identity_join=node.allow_row_identity_join, ) @@ -113,7 +109,7 @@ def compile_promote_offsets(node: nodes.PromoteOffsetsNode, ordered: bool = True @_compile_node.register def compile_filter(node: nodes.FilterNode, ordered: bool = True): - return compile_node(node.child, ordered).filter(node.predicate_id, node.keep_null) + return compile_node(node.child, ordered).filter(node.predicate) @_compile_node.register diff --git a/bigframes/core/compile/row_identity.py b/bigframes/core/compile/row_identity.py index 7a87a435fea..f46e2f94638 100644 --- a/bigframes/core/compile/row_identity.py +++ b/bigframes/core/compile/row_identity.py @@ -24,6 +24,7 @@ import bigframes.constants as constants import bigframes.core.compile.compiled as compiled +import bigframes.core.join_def as join_def import bigframes.core.joins as joining import bigframes.core.ordering as orderings @@ -33,11 +34,10 @@ def join_by_row_identity_unordered( left: compiled.UnorderedIR, right: compiled.UnorderedIR, - *, - how: str, + join_def: join_def.JoinDefinition, ) -> compiled.UnorderedIR: """Compute join when we are joining by row identity not a specific column.""" - if how not in SUPPORTED_ROW_IDENTITY_HOW: + if join_def.type not in SUPPORTED_ROW_IDENTITY_HOW: raise NotImplementedError( f"Only how='outer','left','inner' currently supported. {constants.FEEDBACK_LINK}" ) @@ -60,17 +60,20 @@ def join_by_row_identity_unordered( combined_predicates = [] if left_predicates or right_predicates: joined_predicates = _join_predicates( - left_predicates, right_predicates, join_type=how + left_predicates, right_predicates, join_type=join_def.type ) combined_predicates = list(joined_predicates) # builder expects mutable list - left_mask = left_relative_predicates if how in ["right", "outer"] else None - right_mask = right_relative_predicates if how in ["left", "outer"] else None + left_mask = ( + left_relative_predicates if join_def.type in ["right", "outer"] else None + ) + right_mask = ( + right_relative_predicates if join_def.type in ["left", "outer"] else None + ) # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - map_left_id, map_right_id = joining.JOIN_NAME_REMAPPER( - left.column_ids, right.column_ids - ) + map_left_id = join_def.get_left_mapping() + map_right_id = join_def.get_right_mapping() joined_columns = [ _mask_value(left._get_ibis_column(key), left_mask).name(map_left_id[key]) for key in left.column_ids @@ -90,11 +93,10 @@ def join_by_row_identity_unordered( def join_by_row_identity_ordered( left: compiled.OrderedIR, right: compiled.OrderedIR, - *, - how: str, + join_def: join_def.JoinDefinition, ) -> compiled.OrderedIR: """Compute join when we are joining by row identity not a specific column.""" - if how not in SUPPORTED_ROW_IDENTITY_HOW: + if join_def.type not in SUPPORTED_ROW_IDENTITY_HOW: raise NotImplementedError( f"Only how='outer','left','inner' currently supported. {constants.FEEDBACK_LINK}" ) @@ -117,17 +119,20 @@ def join_by_row_identity_ordered( combined_predicates = [] if left_predicates or right_predicates: joined_predicates = _join_predicates( - left_predicates, right_predicates, join_type=how + left_predicates, right_predicates, join_type=join_def.type ) combined_predicates = list(joined_predicates) # builder expects mutable list - left_mask = left_relative_predicates if how in ["right", "outer"] else None - right_mask = right_relative_predicates if how in ["left", "outer"] else None + left_mask = ( + left_relative_predicates if join_def.type in ["right", "outer"] else None + ) + right_mask = ( + right_relative_predicates if join_def.type in ["left", "outer"] else None + ) # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - lpublicmapping, rpublicmapping = joining.JOIN_NAME_REMAPPER( - left.column_ids, right.column_ids - ) + lpublicmapping = join_def.get_left_mapping() + rpublicmapping = join_def.get_right_mapping() lhiddenmapping, rhiddenmapping = joining.JoinNameRemapper(namespace="hidden")( left._hidden_column_ids, right._hidden_column_ids ) diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index a9088feb49b..d26e71d1b43 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -16,8 +16,7 @@ from __future__ import annotations -import typing -from typing import Literal, Mapping +from typing import Mapping import ibis import ibis.expr.datatypes as ibis_dtypes @@ -25,23 +24,15 @@ import bigframes.core.compile.compiled as compiled import bigframes.core.compile.row_identity +import bigframes.core.join_def as join_defs import bigframes.core.joins as joining import bigframes.core.ordering as orderings def join_by_column_ordered( left: compiled.OrderedIR, - left_column_ids: typing.Sequence[str], right: compiled.OrderedIR, - right_column_ids: typing.Sequence[str], - *, - how: Literal[ - "inner", - "left", - "outer", - "right", - "cross", - ], + join: join_defs.JoinDefinition, allow_row_identity_join: bool = True, ) -> compiled.OrderedIR: """Join two expressions by column equality. @@ -62,7 +53,7 @@ def join_by_column_ordered( """ if ( allow_row_identity_join - and how in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW + and join.type in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW and left._table.equals(right._table) # Make sure we're joining on exactly the same column(s), at least with # regards to value its possible that they both have the same names but @@ -71,22 +62,18 @@ def join_by_column_ordered( left._get_ibis_column(lcol) .name("index") .equals(right._get_ibis_column(rcol).name("index")) - for lcol, rcol in zip(left_column_ids, right_column_ids) + for lcol, rcol in join.conditions ) ): return bigframes.core.compile.row_identity.join_by_row_identity_ordered( - left, right, how=how + left, right, join_def=join ) else: - # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - l_public_mapping, r_public_mapping = joining.JOIN_NAME_REMAPPER( - left.column_ids, right.column_ids - ) l_hidden_mapping, r_hidden_mapping = joining.JoinNameRemapper( namespace="hidden" )(left._hidden_column_ids, right._hidden_column_ids) - l_mapping = {**l_public_mapping, **l_hidden_mapping} - r_mapping = {**r_public_mapping, **r_hidden_mapping} + l_mapping = {**join.get_left_mapping(), **l_hidden_mapping} + r_mapping = {**join.get_right_mapping(), **r_hidden_mapping} left_table = left._to_ibis_expr( ordering_mode="unordered", @@ -101,14 +88,14 @@ def join_by_column_ordered( join_conditions = [ value_to_join_key(left_table[l_mapping[left_index]]) == value_to_join_key(right_table[r_mapping[right_index]]) - for left_index, right_index in zip(left_column_ids, right_column_ids) + for left_index, right_index in join.conditions ] combined_table = ibis.join( left_table, right_table, predicates=join_conditions, - how=how, # type: ignore + how=join.type, # type: ignore ) # Preserve ordering accross joins. @@ -117,7 +104,7 @@ def join_by_column_ordered( right._ordering, l_mapping, r_mapping, - left_order_dominates=(how != "right"), + left_order_dominates=(join.type != "right"), ) # We could filter out the original join columns, but predicates/ordering @@ -145,17 +132,8 @@ def join_by_column_ordered( def join_by_column_unordered( left: compiled.UnorderedIR, - left_column_ids: typing.Sequence[str], right: compiled.UnorderedIR, - right_column_ids: typing.Sequence[str], - *, - how: Literal[ - "inner", - "left", - "outer", - "right", - "cross", - ], + join: join_defs.JoinDefinition, allow_row_identity_join: bool = True, ) -> compiled.UnorderedIR: """Join two expressions by column equality. @@ -176,7 +154,7 @@ def join_by_column_unordered( """ if ( allow_row_identity_join - and how in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW + and join.type in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW and left._table.equals(right._table) # Make sure we're joining on exactly the same column(s), at least with # regards to value its possible that they both have the same names but @@ -185,17 +163,16 @@ def join_by_column_unordered( left._get_ibis_column(lcol) .name("index") .equals(right._get_ibis_column(rcol).name("index")) - for lcol, rcol in zip(left_column_ids, right_column_ids) + for lcol, rcol in join.conditions ) ): return bigframes.core.compile.row_identity.join_by_row_identity_unordered( - left, right, how=how + left, right, join_def=join ) else: # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - l_mapping, r_mapping = joining.JOIN_NAME_REMAPPER( - left.column_ids, right.column_ids - ) + l_mapping = join.get_left_mapping() + r_mapping = join.get_right_mapping() left_table = left._to_ibis_expr( col_id_overrides=l_mapping, ) @@ -205,14 +182,14 @@ def join_by_column_unordered( join_conditions = [ value_to_join_key(left_table[l_mapping[left_index]]) == value_to_join_key(right_table[r_mapping[right_index]]) - for left_index, right_index in zip(left_column_ids, right_column_ids) + for left_index, right_index in join.conditions ] combined_table = ibis.join( left_table, right_table, predicates=join_conditions, - how=how, # type: ignore + how=join.type, # type: ignore ) # We could filter out the original join columns, but predicates/ordering # might still reference them in implicit joins. diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 4ec11cb163e..8b3613d82cf 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -27,7 +27,8 @@ import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.expression as ex -import bigframes.core.joins as joining +import bigframes.core.guid +import bigframes.core.join_def as join_defs import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.dtypes @@ -480,16 +481,39 @@ def join_mono_indexed( ) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: left_expr = left._block.expr right_expr = right._block.expr - get_column_left, get_column_right = joining.JOIN_NAME_REMAPPER( - left_expr.column_ids, right_expr.column_ids + left_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.LEFT, + source_id=id, + destination_id=bigframes.core.guid.generate_guid(), + ) + for id in left_expr.column_ids + ] + right_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.RIGHT, + source_id=id, + destination_id=bigframes.core.guid.generate_guid(), + ) + for id in right_expr.column_ids + ] + + join_def = join_defs.JoinDefinition( + conditions=( + join_defs.JoinCondition( + left._block.index_columns[0], right._block.index_columns[0] + ), + ), + mappings=(*left_mappings, *right_mappings), + type=how, ) - combined_expr = left._block.expr.join( - left._block.index_columns, - right._block.expr, - right._block.index_columns, - how=how, + combined_expr = left_expr.join( + right_expr, + join_def=join_def, allow_row_identity_join=(not block_identity_join), ) + get_column_left = join_def.get_left_mapping() + get_column_right = join_def.get_right_mapping() # Drop original indices from each side. and used the coalesced combination generated by the join. left_index = get_column_left[left._block.index_columns[0]] right_index = get_column_right[right._block.index_columns[0]] @@ -538,19 +562,42 @@ def join_multi_indexed( left_expr = left._block.expr right_expr = right._block.expr - get_column_left, get_column_right = joining.JOIN_NAME_REMAPPER( - left_expr.column_ids, right_expr.column_ids + + left_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.LEFT, + source_id=id, + destination_id=bigframes.core.guid.generate_guid(), + ) + for id in left_expr.column_ids + ] + right_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.RIGHT, + source_id=id, + destination_id=bigframes.core.guid.generate_guid(), + ) + for id in right_expr.column_ids + ] + + join_def = join_defs.JoinDefinition( + conditions=tuple( + join_defs.JoinCondition(left, right) + for left, right in zip(left_join_ids, right_join_ids) + ), + mappings=(*left_mappings, *right_mappings), + type=how, ) combined_expr = left_expr.join( - left_join_ids, right_expr, - right_join_ids, - how=how, + join_def=join_def, # If we're only joining on a subset of the index columns, we need to # perform a true join. allow_row_identity_join=(names_fully_match and not block_identity_join), ) + get_column_left = join_def.get_left_mapping() + get_column_right = join_def.get_right_mapping() left_ids_post_join = [get_column_left[id] for id in left_join_ids] right_ids_post_join = [get_column_right[id] for id in right_join_ids] # Drop original indices from each side. and used the coalesced combination generated by the join. diff --git a/bigframes/core/join_def.py b/bigframes/core/join_def.py new file mode 100644 index 00000000000..4646a0d6ae8 --- /dev/null +++ b/bigframes/core/join_def.py @@ -0,0 +1,59 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import dataclasses +import enum +from typing import Literal, Mapping, NamedTuple, Tuple + + +class JoinSide(enum.Enum): + LEFT = 0 + RIGHT = 1 + + +JoinType = Literal["inner", "outer", "left", "right", "cross"] + + +class JoinCondition(NamedTuple): + left_id: str + right_id: str + + +@dataclasses.dataclass(frozen=True) +class JoinColumnMapping: + source_table: JoinSide + source_id: str + destination_id: str + + +@dataclasses.dataclass(frozen=True) +class JoinDefinition: + conditions: Tuple[JoinCondition, ...] + mappings: Tuple[JoinColumnMapping, ...] + type: JoinType + + def get_left_mapping(self) -> Mapping[str, str]: + return { + i.source_id: i.destination_id + for i in self.mappings + if i.source_table == JoinSide.LEFT + } + + def get_right_mapping(self) -> Mapping[str, str]: + return { + i.source_id: i.destination_id + for i in self.mappings + if i.source_table == JoinSide.RIGHT + } diff --git a/bigframes/core/joins/__init__.py b/bigframes/core/joins/__init__.py index 5d407ec22b2..415ee4e49d6 100644 --- a/bigframes/core/joins/__init__.py +++ b/bigframes/core/joins/__init__.py @@ -15,6 +15,6 @@ """Helpers to join ArrayValue objects.""" from bigframes.core.joins.merge import merge -from bigframes.core.joins.name_resolution import JOIN_NAME_REMAPPER, JoinNameRemapper +from bigframes.core.joins.name_resolution import JoinNameRemapper -__all__ = ("merge", "JoinNameRemapper", "JOIN_NAME_REMAPPER") +__all__ = ("merge", "JoinNameRemapper") diff --git a/bigframes/core/joins/name_resolution.py b/bigframes/core/joins/name_resolution.py index df946b3a590..f648d28ad20 100644 --- a/bigframes/core/joins/name_resolution.py +++ b/bigframes/core/joins/name_resolution.py @@ -39,8 +39,3 @@ def __call__( col: f"{self._namespace}_r_{i}" for i, col in enumerate(right_column_ids) } return new_left_ids, new_right_ids - - -# Defines how column ids are remapped, regardless of join strategy or ordering mode -# Use this remapper for all value column remappings. -JOIN_NAME_REMAPPER = JoinNameRemapper("bfjoin") diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index d30db9a7f7f..bf261b62f4e 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -23,6 +23,7 @@ import bigframes.core.expression as ex import bigframes.core.guid +from bigframes.core.join_def import JoinDefinition from bigframes.core.ordering import OrderingColumnReference import bigframes.core.window_spec as window import bigframes.dtypes @@ -87,15 +88,7 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: class JoinNode(BigFrameNode): left_child: BigFrameNode right_child: BigFrameNode - left_column_ids: typing.Tuple[str, ...] - right_column_ids: typing.Tuple[str, ...] - how: typing.Literal[ - "inner", - "left", - "outer", - "right", - "cross", - ] + join: JoinDefinition allow_row_identity_join: bool = True @property @@ -155,8 +148,7 @@ def __hash__(self): @dataclass(frozen=True) class FilterNode(UnaryNode): - predicate_id: str - keep_null: bool = False + predicate: ex.Expression def __hash__(self): return self._node_hash From c91f70ca7b9793cc62578d7845c3aa31cf8a4507 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:20:15 -0800 Subject: [PATCH 275/921] docs: add remote model requirements (#333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #320775961 🦕 --- README.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.rst b/README.rst index 246b9270fab..38f7d0ab7af 100644 --- a/README.rst +++ b/README.rst @@ -309,6 +309,23 @@ following IAM roles: * Storage Object Viewer (roles/storage.objectViewer) * Project IAM Admin (roles/resourcemanager.projectIamAdmin) +To use BigQuery DataFrames ML remote models(bigframes.ml.remote or bigframes.ml.llm), you must enable the following APIs: + +* The BigQuery API (bigquery.googleapis.com) +* The BigQuery Connection API (bigqueryconnection.googleapis.com) +* The Vertex AI API (aiplatform.googleapis.com) + +To use BigQuery DataFrames ML remote models(bigframes.ml.remote or bigframes.ml.llm), you must be granted the +following IAM roles: + +* BigQuery Data Editor (roles/bigquery.dataEditor) +* BigQuery Connection Admin (roles/bigquery.connectionAdmin) +* Service Account User (roles/iam.serviceAccountUser) on the + `service account `__ + ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` +* Vertex AI User (roles/aiplatform.user) +* Project IAM Admin (roles/resourcemanager.projectIamAdmin) + **Limitations** * Remote functions take about 90 seconds to become available when you first create them. From 374ddb534777895d93a1e2ae2f9c6dbe5f10bf8c Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 22 Jan 2024 12:05:28 -0600 Subject: [PATCH 276/921] fix: use object dtype for ARRAY columns in `to_pandas()` with pandas 1.x (#329) * fix: use object dtype for ARRAY columns in `to_pandas()` with pandas 1.x * fix tests * simplify dtype logic --- bigframes/features.py | 42 + bigframes/session/_io/pandas.py | 32 +- .../bq_dataframes_llm_kmeans.ipynb | 1710 ++++++++++++----- tests/system/small/ml/test_core.py | 8 +- tests/system/small/ml/test_preprocessing.py | 27 +- tests/system/small/test_dataframe_io.py | 8 +- tests/unit/session/test_io_pandas.py | 125 ++ tests/unit/test_features.py | 49 + 8 files changed, 1451 insertions(+), 550 deletions(-) create mode 100644 bigframes/features.py create mode 100644 tests/unit/test_features.py diff --git a/bigframes/features.py b/bigframes/features.py new file mode 100644 index 00000000000..287dbcb0a4e --- /dev/null +++ b/bigframes/features.py @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Tuple + + +class PandasVersions: + """Version comparisons for pandas package""" + + def __init__(self): + self._installed_version = None + + @property + def installed_version(self) -> Tuple[str, ...]: + """pandas version""" + if self._installed_version is None: + import pandas + + self._installed_version = tuple(pandas.__version__.split(".")) + return self._installed_version + + @property + def is_arrow_list_dtype_usable(self): + """True if pandas.ArrowDtype is usable.""" + version = self.installed_version + return version[0] != "1" + + +PANDAS_VERSIONS = PandasVersions() diff --git a/bigframes/session/_io/pandas.py b/bigframes/session/_io/pandas.py index 1af00a2d015..789426a6e31 100644 --- a/bigframes/session/_io/pandas.py +++ b/bigframes/session/_io/pandas.py @@ -19,8 +19,33 @@ import pandas.arrays import pyarrow # type: ignore import pyarrow.compute # type: ignore +import pyarrow.types # type: ignore import bigframes.constants +import bigframes.features + + +def _arrow_to_pandas_arrowdtype( + column: pyarrow.Array, dtype: pandas.ArrowDtype +) -> pandas.Series: + if ( + pyarrow.types.is_list(dtype.pyarrow_dtype) + and not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable + ): + # This version of pandas doesn't really support ArrowDtype + # well. See internal issue 321013333 where array type has + # several problems converting a string. + return pandas.Series( + column.to_pylist(), # type: ignore + dtype="object", + ) + + # Avoid conversion logic if we are backing the pandas Series by the + # arrow array. + return pandas.Series( + pandas.arrays.ArrowExtensionArray(column), # type: ignore + dtype=dtype, + ) def arrow_to_pandas( @@ -75,12 +100,7 @@ def arrow_to_pandas( ) series = pandas.Series(pd_array, dtype=dtype) elif isinstance(dtype, pandas.ArrowDtype): - # Avoid conversion logic if we are backing the pandas Series by the - # arrow array. - series = pandas.Series( - pandas.arrays.ArrowExtensionArray(column), # type: ignore - dtype=dtype, - ) + series = _arrow_to_pandas_arrowdtype(column, dtype) else: series = column.to_pandas(types_mapper=lambda _: dtype) diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 69efb11018a..14a681a6933 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -1,600 +1,1258 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Copyright 2023 Google LLC\n", - "#\n", - "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", - "# you may not use this file except in compliance with the License.\n", - "# You may obtain a copy of the License at\n", - "#\n", - "# https://www.apache.org/licenses/LICENSE-2.0\n", - "#\n", - "# Unless required by applicable law or agreed to in writing, software\n", - "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", - "# See the License for the specific language governing permissions and\n", - "# limitations under the License." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Use BigQuery DataFrames to cluster and characterize complaints\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"Vertex\n", - " Open in Vertex AI Workbench\n", - " \n", - "
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Overview\n", - "\n", - "The goal of this notebook is to demonstrate a comment characterization algorithm for an online business. We will accomplish this using [Google's PaLM 2](https://ai.google/discover/palm2/) and [KMeans clustering](https://en.wikipedia.org/wiki/K-means_clustering) in three steps:\n", - "\n", - "1. Use PaLM2TextEmbeddingGenerator to [generate text embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) for each of 10000 complaints sent to an online bank. If you're not familiar with what a text embedding is, it's a list of numbers that are like coordinates in an imaginary \"meaning space\" for sentences. (It's like [word embeddings](https://en.wikipedia.org/wiki/Word_embedding), but for more general text.) The important point for our purposes is that similar sentences are close to each other in this imaginary space.\n", - "2. Use KMeans clustering to group together complaints whose text embeddings are near to eachother. This will give us sets of similar complaints, but we don't yet know _why_ these complaints are similar.\n", - "3. Prompt PaLM2TextGenerator in English asking what the difference is between the groups of complaints that we got. Thanks to the power of modern LLMs, the response might give us a very good idea of what these complaints are all about, but remember to [\"understand the limits of your dataset and model.\"](https://ai.google/responsibility/responsible-ai-practices/#:~:text=Understand%20the%20limitations%20of%20your%20dataset%20and%20model)\n", - "\n", - "We will tie these pieces together in Python using BigQuery DataFrames. [Click here](https://cloud.google.com/bigquery/docs/dataframes-quickstart) to learn more about BigQuery DataFrames!" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Dataset\n", - "\n", - "This notebook uses the [CFPB Consumer Complaint Database](https://console.cloud.google.com/marketplace/product/cfpb/complaint-database)." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* BigQuery (compute)\n", - "* BigQuery ML\n", - "* Generative AI support on Vertex AI\n", - "\n", - "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models), [Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models),\n", - "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Before you begin\n", - "\n", - "Complete the tasks in this section to set up your environment." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com) to enable the following APIs:\n", - "\n", - " * BigQuery API\n", - " * BigQuery Connection API\n", - " * Vertex AI API\n", - "\n", - "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Set your project ID\n", - "\n", - "**If you don't know your project ID**, see the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set your project ID below\n", - "PROJECT_ID = \"\" # @param {type:\"string\"}\n", - "\n", - "# Set the project id in gcloud\n", - "! gcloud config set project {PROJECT_ID}" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Set the region\n", - "\n", - "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "REGION = \"US\" # @param {type: \"string\"}" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Vertex AI Workbench**\n", - "\n", - "Do nothing, you are already authenticated." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Local JupyterLab instance**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# ! gcloud auth login" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Colab**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we are ready to use BigQuery DataFrames!" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "xckgWno6ouHY" - }, - "source": [ - "## Step 1: Text embedding " - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "BigQuery DataFrames setup" - ] - }, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use BigQuery DataFrames to cluster and characterize complaints\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + "
" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "The goal of this notebook is to demonstrate a comment characterization algorithm for an online business. We will accomplish this using [Google's PaLM 2](https://ai.google/discover/palm2/) and [KMeans clustering](https://en.wikipedia.org/wiki/K-means_clustering) in three steps:\n", + "\n", + "1. Use PaLM2TextEmbeddingGenerator to [generate text embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) for each of 10000 complaints sent to an online bank. If you're not familiar with what a text embedding is, it's a list of numbers that are like coordinates in an imaginary \"meaning space\" for sentences. (It's like [word embeddings](https://en.wikipedia.org/wiki/Word_embedding), but for more general text.) The important point for our purposes is that similar sentences are close to each other in this imaginary space.\n", + "2. Use KMeans clustering to group together complaints whose text embeddings are near to eachother. This will give us sets of similar complaints, but we don't yet know _why_ these complaints are similar.\n", + "3. Prompt PaLM2TextGenerator in English asking what the difference is between the groups of complaints that we got. Thanks to the power of modern LLMs, the response might give us a very good idea of what these complaints are all about, but remember to [\"understand the limits of your dataset and model.\"](https://ai.google/responsibility/responsible-ai-practices/#:~:text=Understand%20the%20limitations%20of%20your%20dataset%20and%20model)\n", + "\n", + "We will tie these pieces together in Python using BigQuery DataFrames. [Click here](https://cloud.google.com/bigquery/docs/dataframes-quickstart) to learn more about BigQuery DataFrames!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dataset\n", + "\n", + "This notebook uses the [CFPB Consumer Complaint Database](https://console.cloud.google.com/marketplace/product/cfpb/complaint-database)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "* Generative AI support on Vertex AI\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models), [Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models),\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com) to enable the following APIs:\n", + "\n", + " * BigQuery API\n", + " * BigQuery Connection API\n", + " * Vertex AI API\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, see the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# set your project ID below\n", + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id in gcloud\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we are ready to use BigQuery DataFrames!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "xckgWno6ouHY" + }, + "source": [ + "## Step 1: Text embedding " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "BigQuery DataFrames setup" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "R7STCS8xB5d2" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "v6FGschEowht" + }, + "source": [ + "Data Input - read the data from a publicly available BigQuery dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "zDSwoBo1CU3G" + }, + "outputs": [], + "source": [ + "input_df = bf.read_gbq(\"bigquery-public-data.cfpb_complaints.complaint_database\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "tYDoaKgJChiq" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "R7STCS8xB5d2" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bf\n", - "\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "bf.options.bigquery.location = REGION" + "data": { + "text/html": [ + "Query job 4423e883-bd7e-41c9-86b3-52d296a36b00 is DONE. 2.3 GB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "data": { + "text/html": [ + "Query job 4ddbf855-a48e-4d98-80d3-290b06d55e98 is DONE. 2.3 GB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "v6FGschEowht" - }, - "source": [ - "Data Input - read the data from a publicly available BigQuery dataset" + "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", + "
consumer_complaint_narrative
2COLLECTION BUREAU OF AMERICA ACCOUNT NO. XXXX...
3Despite multiple written requests, the unverif...
6Once again you guys have not provided me with ...
9XX/XX/XXXX {$350.00} I received a outstating d...
10Im am unable to withdraw money from my account...
\n", + "

5 rows × 1 columns

\n", + "
[5 rows x 1 columns in total]" + ], + "text/plain": [ + " consumer_complaint_narrative\n", + "2 COLLECTION BUREAU OF AMERICA ACCOUNT NO. XXXX...\n", + "3 Despite multiple written requests, the unverif...\n", + "6 Once again you guys have not provided me with ...\n", + "9 XX/XX/XXXX {$350.00} I received a outstating d...\n", + "10 Im am unable to withdraw money from my account...\n", + "\n", + "[5 rows x 1 columns]" ] - }, + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "issues_df = input_df[[\"consumer_complaint_narrative\"]].dropna()\n", + "issues_df.head(n=5) # View the first five complaints" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Downsample DataFrame to 10,000 records for model training." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "OltYSUEcsSOW" + }, + "outputs": [], + "source": [ + "# Choose 10,000 complaints randomly and store them in a column in a DataFrame\n", + "downsampled_issues_df = issues_df.sample(n=10000)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "Wl2o-NYMoygb" + }, + "source": [ + "Generate the text embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "li38q8FzDDMu" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "zDSwoBo1CU3G" - }, - "outputs": [], - "source": [ - "input_df = bf.read_gbq(\"bigquery-public-data.cfpb_complaints.complaint_database\")" + "data": { + "text/html": [ + "Query job 1f75a254-c8c5-468a-a8c0-76f342a2822a is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] - }, + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from bigframes.ml.llm import PaLM2TextEmbeddingGenerator\n", + "\n", + "model = PaLM2TextEmbeddingGenerator() # No connection id needed" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "cOuSOQ5FDewD" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "tYDoaKgJChiq" - }, - "outputs": [], - "source": [ - "issues_df = input_df[[\"consumer_complaint_narrative\"]].dropna()\n", - "issues_df.head(n=5) # View the first five complaints" + "data": { + "text/html": [ + "Query job 15508c20-760e-4137-b312-f010913d13f5 is DONE. 2.3 GB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Downsample DataFrame to 10,000 records for model training." + "data": { + "text/html": [ + "Query job 1e23baf5-8523-4fc5-9a73-d059c0b51c33 is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "OltYSUEcsSOW" - }, - "outputs": [], - "source": [ - "# Choose 10,000 complaints randomly and store them in a column in a DataFrame\n", - "downsampled_issues_df = issues_df.sample(n=10000)" + "data": { + "text/html": [ + "Query job 12bf5383-fa0d-4806-a4a9-d7ee65a0dd01 is DONE. 20.0 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "Wl2o-NYMoygb" - }, - "source": [ - "Generate the text embeddings" + "data": { + "text/html": [ + "Query job cafcd732-727b-4100-ac47-e6d98024fd16 is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "li38q8FzDDMu" - }, - "outputs": [], - "source": [ - "from bigframes.ml.llm import PaLM2TextEmbeddingGenerator\n", - "\n", - "model = PaLM2TextEmbeddingGenerator() # No connection id needed" + "data": { + "text/html": [ + "Query job 216b3d31-4159-4faf-93e6-820f82ca8f31 is DONE. 72.0 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "cOuSOQ5FDewD" - }, - "outputs": [], - "source": [ - "# Will take ~3 minutes to compute the embeddings\n", - "predicted_embeddings = model.predict(downsampled_issues_df)\n", - "# Notice the lists of numbers that are our text embeddings for each complaint\n", - "predicted_embeddings.head() " + "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", + "
text_embeddingstatisticsml_embed_text_statuscontent
3[0.02506784163415432, -0.03947214409708977, -0...{\"token_count\":273,\"truncated\":false}Despite multiple written requests, the unverif...
102[0.029197776690125465, -0.028653817251324654, ...{\"token_count\":859,\"truncated\":false}To Whom It May Concern, Please be advised that...
660[0.034257132560014725, -0.05359702184796333, -...{\"token_count\":282,\"truncated\":false}Transunion has failed to remove a XXXX XXXX b...
1500[-0.008016454987227917, -0.05421802029013634, ...{\"token_count\":39,\"truncated\":false}FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR...
1601[-0.005403461866080761, -0.024799197912216187,...{\"token_count\":94,\"truncated\":false}I am a victim of identity theft/fraud. I have ...
\n", + "

5 rows × 4 columns

\n", + "
[5 rows x 4 columns in total]" + ], + "text/plain": [ + " text_embedding \\\n", + "3 [0.02506784163415432, -0.03947214409708977, -0... \n", + "102 [0.029197776690125465, -0.028653817251324654, ... \n", + "660 [0.034257132560014725, -0.05359702184796333, -... \n", + "1500 [-0.008016454987227917, -0.05421802029013634, ... \n", + "1601 [-0.005403461866080761, -0.024799197912216187,... \n", + "\n", + " statistics ml_embed_text_status \\\n", + "3 {\"token_count\":273,\"truncated\":false} \n", + "102 {\"token_count\":859,\"truncated\":false} \n", + "660 {\"token_count\":282,\"truncated\":false} \n", + "1500 {\"token_count\":39,\"truncated\":false} \n", + "1601 {\"token_count\":94,\"truncated\":false} \n", + "\n", + " content \n", + "3 Despite multiple written requests, the unverif... \n", + "102 To Whom It May Concern, Please be advised that... \n", + "660 Transunion has failed to remove a XXXX XXXX b... \n", + "1500 FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR... \n", + "1601 I am a victim of identity theft/fraud. I have ... \n", + "\n", + "[5 rows x 4 columns]" ] - }, + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Will take ~3 minutes to compute the embeddings\n", + "predicted_embeddings = model.predict(downsampled_issues_df)\n", + "# Notice the lists of numbers that are our text embeddings for each complaint\n", + "predicted_embeddings.head() " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now have the complaints and their text embeddings as two columns in our predicted_embeddings DataFrame." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "OUZ3NNbzo1Tb" + }, + "source": [ + "## Step 2: Create k-means model and predict clusters" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "AhNTnEC5FRz2" + }, + "outputs": [], + "source": [ + "from bigframes.ml.cluster import KMeans\n", + "\n", + "cluster_model = KMeans(n_clusters=10) # We will divide our complaints into 10 groups" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perform KMeans clustering" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "6poSxh-fGJF7" + }, + "outputs": [ { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now have the complaints and their text embeddings as two columns in our predicted_embeddings DataFrame." + "data": { + "text/html": [ + "Query job ed22ef18-cbdf-4c7a-9244-38f65e2bbefb is DONE. 61.5 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "OUZ3NNbzo1Tb" - }, - "source": [ - "## Step 2: Create k-means model and predict clusters" + "data": { + "text/html": [ + "Query job 5e94bc4b-44d6-4c20-b907-cee9795d5b54 is DONE. 61.4 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "AhNTnEC5FRz2" - }, - "outputs": [], - "source": [ - "from bigframes.ml.cluster import KMeans\n", - "\n", - "cluster_model = KMeans(n_clusters=10) # We will divide our complaints into 10 groups" + "data": { + "text/html": [ + "Query job 915d58d8-403b-44bf-8cf2-38ad5a87be9d is DONE. 72.3 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Perform KMeans clustering" + "data": { + "text/html": [ + "Query job fa241e23-d153-49ea-8dff-6d28906bcb1f is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "6poSxh-fGJF7" - }, - "outputs": [], - "source": [ - "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", - "cluster_model.fit(predicted_embeddings[[\"text_embedding\"]])\n", - "clustered_result = cluster_model.predict(predicted_embeddings)\n", - "# Notice the CENTROID_ID column, which is the ID number of the group that\n", - "# each complaint belongs to.\n", - "clustered_result.head(n=5)" + "data": { + "text/html": [ + "Query job a3b00aa3-d6d6-4e30-ac96-816b81fcd202 is DONE. 80.0 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Our DataFrame clustered_result now has an additional column that includes an ID from 1-10 (inclusive) indicating which semantically similar group they belong to." + "data": { + "text/html": [ + "Query job 38277ed2-bcad-4de1-b508-7bb44b3158dc is DONE. 73.2 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "21rNsFMHo8hO" - }, - "source": [ - "## Step 3: Use PaLM2 LLM model to summarize complaint clusters" + "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", + "
CENTROID_IDNEAREST_CENTROIDS_DISTANCEtext_embeddingstatisticsml_embed_text_statuscontent
38[{'CENTROID_ID': 8, 'DISTANCE': 0.399505154607...[0.02506784163415432, -0.03947214409708977, -0...{\"token_count\":273,\"truncated\":false}Despite multiple written requests, the unverif...
1028[{'CENTROID_ID': 8, 'DISTANCE': 0.403332660368...[0.029197776690125465, -0.028653817251324654, ...{\"token_count\":859,\"truncated\":false}To Whom It May Concern, Please be advised that...
66010[{'CENTROID_ID': 10, 'DISTANCE': 0.42615208239...[0.034257132560014725, -0.05359702184796333, -...{\"token_count\":282,\"truncated\":false}Transunion has failed to remove a XXXX XXXX b...
15006[{'CENTROID_ID': 6, 'DISTANCE': 0.500031509322...[-0.008016454987227917, -0.05421802029013634, ...{\"token_count\":39,\"truncated\":false}FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR...
16016[{'CENTROID_ID': 6, 'DISTANCE': 0.361769337067...[-0.005403461866080761, -0.024799197912216187,...{\"token_count\":94,\"truncated\":false}I am a victim of identity theft/fraud. I have ...
\n", + "

5 rows × 6 columns

\n", + "
[5 rows x 6 columns in total]" + ], + "text/plain": [ + " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", + "3 8 [{'CENTROID_ID': 8, 'DISTANCE': 0.399505154607... \n", + "102 8 [{'CENTROID_ID': 8, 'DISTANCE': 0.403332660368... \n", + "660 10 [{'CENTROID_ID': 10, 'DISTANCE': 0.42615208239... \n", + "1500 6 [{'CENTROID_ID': 6, 'DISTANCE': 0.500031509322... \n", + "1601 6 [{'CENTROID_ID': 6, 'DISTANCE': 0.361769337067... \n", + "\n", + " text_embedding \\\n", + "3 [0.02506784163415432, -0.03947214409708977, -0... \n", + "102 [0.029197776690125465, -0.028653817251324654, ... \n", + "660 [0.034257132560014725, -0.05359702184796333, -... \n", + "1500 [-0.008016454987227917, -0.05421802029013634, ... \n", + "1601 [-0.005403461866080761, -0.024799197912216187,... \n", + "\n", + " statistics ml_embed_text_status \\\n", + "3 {\"token_count\":273,\"truncated\":false} \n", + "102 {\"token_count\":859,\"truncated\":false} \n", + "660 {\"token_count\":282,\"truncated\":false} \n", + "1500 {\"token_count\":39,\"truncated\":false} \n", + "1601 {\"token_count\":94,\"truncated\":false} \n", + "\n", + " content \n", + "3 Despite multiple written requests, the unverif... \n", + "102 To Whom It May Concern, Please be advised that... \n", + "660 Transunion has failed to remove a XXXX XXXX b... \n", + "1500 FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR... \n", + "1601 I am a victim of identity theft/fraud. I have ... \n", + "\n", + "[5 rows x 6 columns]" ] - }, + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", + "cluster_model.fit(predicted_embeddings[[\"text_embedding\"]])\n", + "clustered_result = cluster_model.predict(predicted_embeddings)\n", + "# Notice the CENTROID_ID column, which is the ID number of the group that\n", + "# each complaint belongs to.\n", + "clustered_result.head(n=5)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our DataFrame clustered_result now has an additional column that includes an ID from 1-10 (inclusive) indicating which semantically similar group they belong to." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "21rNsFMHo8hO" + }, + "source": [ + "## Step 3: Use PaLM2 LLM model to summarize complaint clusters" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Build prompts - we will choose just two of our categories and prompt PaLM2TextGenerator to identify their salient characteristics. The prompt is natural language in a python string." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "2E7wXM_jGqo6" + }, + "outputs": [ { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Build prompts - we will choose just two of our categories and prompt PaLM2TextGenerator to identify their salient characteristics. The prompt is natural language in a python string." + "data": { + "text/html": [ + "Query job 8aec7d59-aaf5-4c55-85d3-1c96ee904558 is DONE. 10.5 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "2E7wXM_jGqo6" - }, - "outputs": [], - "source": [ - "# Using bigframes, with syntax identical to pandas,\n", - "# filter out the first and second groups\n", - "cluster_1_result = clustered_result[\n", - " clustered_result[\"CENTROID_ID\"] == 1\n", - "][[\"content\"]]\n", - "cluster_1_result_pandas = cluster_1_result.head(5).to_pandas()\n", - "\n", - "cluster_2_result = clustered_result[\n", - " clustered_result[\"CENTROID_ID\"] == 2\n", - "][[\"content\"]]\n", - "cluster_2_result_pandas = cluster_2_result.head(5).to_pandas()" + "data": { + "text/html": [ + "Query job f269725a-da1c-46d4-aa3f-3525bbbceaef is DONE. 10.5 MB processed. Open Job" + ], + "text/plain": [ + "" ] - }, + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Using bigframes, with syntax identical to pandas,\n", + "# filter out the first and second groups\n", + "cluster_1_result = clustered_result[\n", + " clustered_result[\"CENTROID_ID\"] == 1\n", + "][[\"content\"]]\n", + "cluster_1_result_pandas = cluster_1_result.head(5).to_pandas()\n", + "\n", + "cluster_2_result = clustered_result[\n", + " clustered_result[\"CENTROID_ID\"] == 2\n", + "][[\"content\"]]\n", + "cluster_2_result_pandas = cluster_2_result.head(5).to_pandas()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "ZNDiueI9IP5e" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ZNDiueI9IP5e" - }, - "outputs": [], - "source": [ - "# Build plain-text prompts to send to PaLM 2. Use only 5 complaints from each group.\n", - "prompt1 = 'comment list 1:\\n'\n", - "for i in range(5):\n", - " prompt1 += str(i + 1) + '. ' + \\\n", - " cluster_1_result_pandas[\"content\"].iloc[i] + '\\n'\n", - "\n", - "prompt2 = 'comment list 2:\\n'\n", - "for i in range(5):\n", - " prompt2 += str(i + 1) + '. ' + \\\n", - " cluster_2_result_pandas[\"content\"].iloc[i] + '\\n'\n", - "\n", - "print(prompt1)\n", - "print(prompt2)" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "comment list 1:\n", + "1. Out of the blue I received a debt collection notice saying they were collecting debt of {$710.00} for a XXXX XXXX which I have no idea what it was and was never notified of. I replied with written notification disputing the debt and demand of supporting documentation ( as the debt collection simply stated a total due with no supporting documentation ) but received no supporting documentation or verification.\n", + "2. I am referring a case to the Consumer Protection Bureau as follows. \n", + "\n", + "First, please see the most recent document, 'New_Debt_Collector_Account.jpg '. This was sent to me on XX/XX/2021 and arrived on XX/XX/2021. This is a letter from \" I.C. System '' telling me I must pay a delinquent account for \" XXXX ''. An account I thought that was already settled by \" Offices of the XXXX XXXX XXXX ''. Or so I believed. Until now... \n", + "\n", + "See attachment 'dispute.rtf '. This is the original letter I sent to \" Offices of the XXXX XXXX XXXX ''. Within 'dispute.rtf ' is a very detailed, and extremely well articulated ( if I do say so myself ) dispute letter, delineating the abuses and reasons to \" XXXX '' of why I would not pay the bill. In a response, sent back to me by \" Offices of the XXXX XXXX XXXX '', 'dispute_response.jpg ', they said they would discontinue their collection efforts and no information regarding this account would be referred to the credit bureau XXXX \n", + "\n", + "THEN, 7 months later, from \" I.C. System '' I receive, 'New_Debt_Collector_Account.jpg ', as first seent above, saying I still owe this debt.\n", + "\n", + "My question, to \" I.C. System '' \" XXXX XXXX '' and the \" Consumer Protection Bureau '' are : How can I dispute and then remove a debt, that I legitimatlly do not owe, and be exonerated from it, if \" Charter/Spectrum '' can simply refer it to another debt collecting company to try and collect it? And then tarnish my credit? Is misleading a consumer like this 'legal '?\n", + "\n", + "As 'dispute.rtf ' indicates, I even said I would contact the Consumer Protection Bureau and the XXXX. I did not originally do this and regret that decision, but I am doing so now!! \n", + "\n", + "Furthermore, due to the unparalleled absolute deceptive malfeasance on the part of \" XXXX '' and these debt XXXX, I am considering approaching media outlets with my story. Consumers can not and should not be ripped of this way and their credit tarnished, while a corrupt telecommunications company and their debt collecting cronies attempt to charge them for several months of internet they never had. \n", + "\n", + "This is fraud, it is illegal, and it needs to stop. And I wish to be exonerated from this debt as I was originally led to believe!!! \n", + "\n", + "Finally, if \" XXXX '' are part of any class action lawsuits involving this behaviour ( and I'm sure they are ), then I wish to testify on the behalf on any government, individual, and/or private institutions as to the corrput nature of entities such as \" XXXX '' and their debt collectors, and hereby consent to providing all the following statements, including all attachments herein, and any testimony they want me to give as evidence. \n", + "\n", + "-XXXX XXXX XXXX XXXX XXXX\n", + "3. XXXX called on XX/XX/XXXX at about XXXXXXXX XXXX claiming to be from Sherloq financial, about collecting medical debt from XXXX, he was XXXX and XXXX and said there were four other accounts but did not specify which or how much. I have been contacted by my insurance that I have paid everything in relation to this. I have talked to my doctor and my endocrinologist and they said I don't owe anything either. ( I am XXXX and have XXXX and XXXX that pays for my doctor visits. ) I feel that they are just trying to collect money they are not owed and its predatory.\n", + "4. While moving out of my long time residence in XXXX, CA I began the process to cut off my services in XXXX of XXXX which included the phone, internet and XXXX, which were all included in a single bill. Initially the final date I relayed to XXXX XXXX XXXX was the XXXX of XXXX but because of delays in the move was extended to XX/XX/XXXX. Because XXXX ended up mistakenly cutting off the satellite service on the XXXX I had to call them directly to extend it to the XXXX. I left the house for the last time on XXXX XX/XX/XXXX and turn in all the equipment as required to the local XXXX office. Although I assumed all the services were ended properly I continued to get bills from XXXX ( not from XXXX XXXX XXXX ) so I called several times to rectify the mistake and finally sent a detailed escalation letter to them in XX/XX/XXXX. I felt at this point they finally realized their mistake and closed the bill but in XXXX of XXXX I then received a collection notice from Afni, Inc. for the disputed amount. I recently forwarded them a dispute letter along with other pertinent details of the situation. I have no idea why XXXX is unable to verify the fact I no longer lived at the XXXX residence, nor retained any of the receivers or remotes after the XXXX of XX/XX/XXXX. This should be an easy task on their end especially since I went above and beyond due diligence to work with them to correct this.\n", + "5. Merchants and Medical Credit began reporting on a debt they had acquired on XXXX XXXX 2014 and never sent written notice despite it being required under section 806 of the FDCPA. For 3 years they have been holding my financial life hostage and only recently came to my attention. Despite many disputes with the big three credit reporting agencies section 806 has been ignored and they have continued to break and ignore the proper procedures set forth by the FDCPA.\n", + "\n", + "comment list 2:\n", + "1. I have a PayPal account and a PayPal credit card and a XXXX XXXX Checking account. \n", + "If I use PayPal, or the PayPal credit card the charges are then debited from my XXXX XXXX Checking account.. \n", + "\n", + "Recently there were two fraudulent and unauthorized charges I was alerted to by PayPal.\n", + "\n", + "One was a debit from XXXX for {$240.00}. I contacted XXXX and they said there was no record for that amount and date for purchases or charges anywhere on XXXX. They said it was most likely fraudulent and I never authorized it. \n", + "\n", + "The other was from XXXX XXXX for {$50.00}. I did make political donations to politicians during the election cycle and I noted each in my checking account. I STOPPED making any further contributions after the last elections in Georgia. This charge was not one I had authorized and it was dated AFTER the last election and was not one of the ones I had checked against my checking account. \n", + "\n", + "I disputed both with XXXX XXXX on my checking account and they researched both of them and determined they were fraudulent and XXXX XXXX noted in writing \" We have completed our research of your inquiry and returned the charges unpaid. As a result no money was deducted from your account '' I have filed two claims disputing both charges to PayPal and each time they have denied my fraudulent/unauthorized claims and I even spoke to PayPal people several times and quoted XXXX XXXX to them and then filed a dispute with them on their denial. \n", + "\n", + "They are still denying my dispute and claiming that I owe them {$290.00} for both fraudulent charges. Please help me get this resolved. They keep sending me notices and phone calls. I should not have to pay them since XXXX XXXX returned them as unpaid and determined they were fraudulent and even XXXX says there was no purchase or credit so it was fraudulent and unauthorized.\n", + "2. Banked with truist for almost XXXX months now and never had an issue until i decided to start funding my account and using my debit card to pay my bills, they locked my debit card which is fine, called to have it unblocked no problem and they end up locking it again but this time they are saying i have to come into a branch to do so which IS a problem. Truist bank is nowhere near me in my state, its an out of state bank so thats literally not an option. Bank locked my online banking with money still in my account lol, i tried an ACH transfer and set up bill pay payments and i tried to log in just to find out my account is restricted for security reasons. This has been XXXX of the worst banking experiences i've ever had and im not being treated like a customer at all. its almost like truist isnt allowing me to use my money the way i want or access my funds properly. Ive had no problem up until i made a large deposit into my account and started using those funds. \n", + "\n", + "i have no problenm verifying myself any other way but going out of my current state just to visit truist bank is not an option.\n", + "3. Navy Exchange ( NEX ), the \" XXXX '' onboard US XXXX bases, offers currency exchange for personnel stationed overseas ( in XXXX ) to pay their off-base rent. However, the exchange rate offered is always 2-3 % less than the Bank Rate for US dollar to XXXX XXXX exchange. \n", + "\n", + "To offset this, the Navy Exchange provides a {$10.00} gift card for use at the NEX, for each month paid. However, this is inequitable because the gift card is always for the same amount, regardless of the monthly rent amount ... so a single military member with an $ XXXX/month ( equivalent ) rent payment derives more proportional benefit from this {$10.00} gift card than a military family whose rent could be closer to $ XXXX ( equivalent ).\n", + "4. In XX/XX/XXXX, based on a friend 's recommendation, I visited a vacation rental home website to rent her neighbor 's property in XXXX, California, USA. The vacation rental home website is called : XXXX. I used the right side of the website called \" Contact Owner '' to reach out to the owner. I received an email from the owner, XXXX, who let me know that the property was only available for 26 days at a time, as such, I did not move forward. I also received another email from the email address : XXXX. The contact was named XXXX XXXX who asked for desired check-in/out dates, number of bedrooms, and preferred location. He also asked about preferred budget and stated that he manages a lot of properties and could offer an attractive discount. I responded with my preferences and he sent an offer for a vacation rental property. He included a link to view the property via XXXX. I asked for the address of the property and he provided the following address : XXXX XXXX XXXX XXXX, California. He let me know that 50 % of the total was due as a deposit and the remaining amount was due 30 days prior to the arrival date. He also let me know that the owners of the property live in XXXX and require payment with the company XXXX ( formerly TransferWise ). I recognize XXXX as I have used the company in the past and already had an account. Therefore, on XX/XX/XXXX, I sent the money through XXXX to the stated \" Owners '' of the property : XXXX XXXX and XXXX XXXX. \n", + "Eight days later, I realized that the transaction was a scam after Googling the email address ( XXXX ). There were at least 20 reviewers with similar experiences that claimed this entity was a scammer for rental properties. At this point ( on XX/XX/XXXX ), I reached back out to XXXX via email to ask for the money back, and he agreed to send the money via check to my mailing address. However, the check did not arrive. I let him know that the money did not arrive and asked him to reverse the XXXX transaction. However, I did not receive a response from XXXX. After this, I contacted my bank. My bank provided a temporary reimbursement while an investigation was performed. After the investigation was complete 60 days later ( XX/XX/XXXX ), the reimbursement was removed since my bank determined that XXXX is the company responsible in this matter. As such, I am looking to get a refund for the amount with XXXX.\n", + "5. I can not withdraw money or pay my rent from my Way to go card from Florida reemployment. It keeps declining me. It was working fine in XXXX, I went to pay my rent for XXXX, and it declined ; I went to an atm again declined. So I ordered a new card and paid expedited shipping ; thinking this one was damaged, I activated it, and it again declined me. There is absolutely no way to contact anyone. All they have is an automatic response which doesn't provide any help\n", + "\n" + ] + } + ], + "source": [ + "# Build plain-text prompts to send to PaLM 2. Use only 5 complaints from each group.\n", + "prompt1 = 'comment list 1:\\n'\n", + "for i in range(5):\n", + " prompt1 += str(i + 1) + '. ' + \\\n", + " cluster_1_result_pandas[\"content\"].iloc[i] + '\\n'\n", + "\n", + "prompt2 = 'comment list 2:\\n'\n", + "for i in range(5):\n", + " prompt2 += str(i + 1) + '. ' + \\\n", + " cluster_2_result_pandas[\"content\"].iloc[i] + '\\n'\n", + "\n", + "print(prompt1)\n", + "print(prompt2)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "BfHGJLirzSvH" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "BfHGJLirzSvH" - }, - "outputs": [], - "source": [ - "# The plain English request we will make of PaLM 2\n", - "prompt = (\n", - " \"Please highlight the most obvious difference between \"\n", - " \"the two lists of comments:\\n\" + prompt1 + prompt2\n", - ")\n", - "print(prompt)" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Please highlight the most obvious difference between the two lists of comments:\n", + "comment list 1:\n", + "1. Out of the blue I received a debt collection notice saying they were collecting debt of {$710.00} for a XXXX XXXX which I have no idea what it was and was never notified of. I replied with written notification disputing the debt and demand of supporting documentation ( as the debt collection simply stated a total due with no supporting documentation ) but received no supporting documentation or verification.\n", + "2. I am referring a case to the Consumer Protection Bureau as follows. \n", + "\n", + "First, please see the most recent document, 'New_Debt_Collector_Account.jpg '. This was sent to me on XX/XX/2021 and arrived on XX/XX/2021. This is a letter from \" I.C. System '' telling me I must pay a delinquent account for \" XXXX ''. An account I thought that was already settled by \" Offices of the XXXX XXXX XXXX ''. Or so I believed. Until now... \n", + "\n", + "See attachment 'dispute.rtf '. This is the original letter I sent to \" Offices of the XXXX XXXX XXXX ''. Within 'dispute.rtf ' is a very detailed, and extremely well articulated ( if I do say so myself ) dispute letter, delineating the abuses and reasons to \" XXXX '' of why I would not pay the bill. In a response, sent back to me by \" Offices of the XXXX XXXX XXXX '', 'dispute_response.jpg ', they said they would discontinue their collection efforts and no information regarding this account would be referred to the credit bureau XXXX \n", + "\n", + "THEN, 7 months later, from \" I.C. System '' I receive, 'New_Debt_Collector_Account.jpg ', as first seent above, saying I still owe this debt.\n", + "\n", + "My question, to \" I.C. System '' \" XXXX XXXX '' and the \" Consumer Protection Bureau '' are : How can I dispute and then remove a debt, that I legitimatlly do not owe, and be exonerated from it, if \" Charter/Spectrum '' can simply refer it to another debt collecting company to try and collect it? And then tarnish my credit? Is misleading a consumer like this 'legal '?\n", + "\n", + "As 'dispute.rtf ' indicates, I even said I would contact the Consumer Protection Bureau and the XXXX. I did not originally do this and regret that decision, but I am doing so now!! \n", + "\n", + "Furthermore, due to the unparalleled absolute deceptive malfeasance on the part of \" XXXX '' and these debt XXXX, I am considering approaching media outlets with my story. Consumers can not and should not be ripped of this way and their credit tarnished, while a corrupt telecommunications company and their debt collecting cronies attempt to charge them for several months of internet they never had. \n", + "\n", + "This is fraud, it is illegal, and it needs to stop. And I wish to be exonerated from this debt as I was originally led to believe!!! \n", + "\n", + "Finally, if \" XXXX '' are part of any class action lawsuits involving this behaviour ( and I'm sure they are ), then I wish to testify on the behalf on any government, individual, and/or private institutions as to the corrput nature of entities such as \" XXXX '' and their debt collectors, and hereby consent to providing all the following statements, including all attachments herein, and any testimony they want me to give as evidence. \n", + "\n", + "-XXXX XXXX XXXX XXXX XXXX\n", + "3. XXXX called on XX/XX/XXXX at about XXXXXXXX XXXX claiming to be from Sherloq financial, about collecting medical debt from XXXX, he was XXXX and XXXX and said there were four other accounts but did not specify which or how much. I have been contacted by my insurance that I have paid everything in relation to this. I have talked to my doctor and my endocrinologist and they said I don't owe anything either. ( I am XXXX and have XXXX and XXXX that pays for my doctor visits. ) I feel that they are just trying to collect money they are not owed and its predatory.\n", + "4. While moving out of my long time residence in XXXX, CA I began the process to cut off my services in XXXX of XXXX which included the phone, internet and XXXX, which were all included in a single bill. Initially the final date I relayed to XXXX XXXX XXXX was the XXXX of XXXX but because of delays in the move was extended to XX/XX/XXXX. Because XXXX ended up mistakenly cutting off the satellite service on the XXXX I had to call them directly to extend it to the XXXX. I left the house for the last time on XXXX XX/XX/XXXX and turn in all the equipment as required to the local XXXX office. Although I assumed all the services were ended properly I continued to get bills from XXXX ( not from XXXX XXXX XXXX ) so I called several times to rectify the mistake and finally sent a detailed escalation letter to them in XX/XX/XXXX. I felt at this point they finally realized their mistake and closed the bill but in XXXX of XXXX I then received a collection notice from Afni, Inc. for the disputed amount. I recently forwarded them a dispute letter along with other pertinent details of the situation. I have no idea why XXXX is unable to verify the fact I no longer lived at the XXXX residence, nor retained any of the receivers or remotes after the XXXX of XX/XX/XXXX. This should be an easy task on their end especially since I went above and beyond due diligence to work with them to correct this.\n", + "5. Merchants and Medical Credit began reporting on a debt they had acquired on XXXX XXXX 2014 and never sent written notice despite it being required under section 806 of the FDCPA. For 3 years they have been holding my financial life hostage and only recently came to my attention. Despite many disputes with the big three credit reporting agencies section 806 has been ignored and they have continued to break and ignore the proper procedures set forth by the FDCPA.\n", + "comment list 2:\n", + "1. I have a PayPal account and a PayPal credit card and a XXXX XXXX Checking account. \n", + "If I use PayPal, or the PayPal credit card the charges are then debited from my XXXX XXXX Checking account.. \n", + "\n", + "Recently there were two fraudulent and unauthorized charges I was alerted to by PayPal.\n", + "\n", + "One was a debit from XXXX for {$240.00}. I contacted XXXX and they said there was no record for that amount and date for purchases or charges anywhere on XXXX. They said it was most likely fraudulent and I never authorized it. \n", + "\n", + "The other was from XXXX XXXX for {$50.00}. I did make political donations to politicians during the election cycle and I noted each in my checking account. I STOPPED making any further contributions after the last elections in Georgia. This charge was not one I had authorized and it was dated AFTER the last election and was not one of the ones I had checked against my checking account. \n", + "\n", + "I disputed both with XXXX XXXX on my checking account and they researched both of them and determined they were fraudulent and XXXX XXXX noted in writing \" We have completed our research of your inquiry and returned the charges unpaid. As a result no money was deducted from your account '' I have filed two claims disputing both charges to PayPal and each time they have denied my fraudulent/unauthorized claims and I even spoke to PayPal people several times and quoted XXXX XXXX to them and then filed a dispute with them on their denial. \n", + "\n", + "They are still denying my dispute and claiming that I owe them {$290.00} for both fraudulent charges. Please help me get this resolved. They keep sending me notices and phone calls. I should not have to pay them since XXXX XXXX returned them as unpaid and determined they were fraudulent and even XXXX says there was no purchase or credit so it was fraudulent and unauthorized.\n", + "2. Banked with truist for almost XXXX months now and never had an issue until i decided to start funding my account and using my debit card to pay my bills, they locked my debit card which is fine, called to have it unblocked no problem and they end up locking it again but this time they are saying i have to come into a branch to do so which IS a problem. Truist bank is nowhere near me in my state, its an out of state bank so thats literally not an option. Bank locked my online banking with money still in my account lol, i tried an ACH transfer and set up bill pay payments and i tried to log in just to find out my account is restricted for security reasons. This has been XXXX of the worst banking experiences i've ever had and im not being treated like a customer at all. its almost like truist isnt allowing me to use my money the way i want or access my funds properly. Ive had no problem up until i made a large deposit into my account and started using those funds. \n", + "\n", + "i have no problenm verifying myself any other way but going out of my current state just to visit truist bank is not an option.\n", + "3. Navy Exchange ( NEX ), the \" XXXX '' onboard US XXXX bases, offers currency exchange for personnel stationed overseas ( in XXXX ) to pay their off-base rent. However, the exchange rate offered is always 2-3 % less than the Bank Rate for US dollar to XXXX XXXX exchange. \n", + "\n", + "To offset this, the Navy Exchange provides a {$10.00} gift card for use at the NEX, for each month paid. However, this is inequitable because the gift card is always for the same amount, regardless of the monthly rent amount ... so a single military member with an $ XXXX/month ( equivalent ) rent payment derives more proportional benefit from this {$10.00} gift card than a military family whose rent could be closer to $ XXXX ( equivalent ).\n", + "4. In XX/XX/XXXX, based on a friend 's recommendation, I visited a vacation rental home website to rent her neighbor 's property in XXXX, California, USA. The vacation rental home website is called : XXXX. I used the right side of the website called \" Contact Owner '' to reach out to the owner. I received an email from the owner, XXXX, who let me know that the property was only available for 26 days at a time, as such, I did not move forward. I also received another email from the email address : XXXX. The contact was named XXXX XXXX who asked for desired check-in/out dates, number of bedrooms, and preferred location. He also asked about preferred budget and stated that he manages a lot of properties and could offer an attractive discount. I responded with my preferences and he sent an offer for a vacation rental property. He included a link to view the property via XXXX. I asked for the address of the property and he provided the following address : XXXX XXXX XXXX XXXX, California. He let me know that 50 % of the total was due as a deposit and the remaining amount was due 30 days prior to the arrival date. He also let me know that the owners of the property live in XXXX and require payment with the company XXXX ( formerly TransferWise ). I recognize XXXX as I have used the company in the past and already had an account. Therefore, on XX/XX/XXXX, I sent the money through XXXX to the stated \" Owners '' of the property : XXXX XXXX and XXXX XXXX. \n", + "Eight days later, I realized that the transaction was a scam after Googling the email address ( XXXX ). There were at least 20 reviewers with similar experiences that claimed this entity was a scammer for rental properties. At this point ( on XX/XX/XXXX ), I reached back out to XXXX via email to ask for the money back, and he agreed to send the money via check to my mailing address. However, the check did not arrive. I let him know that the money did not arrive and asked him to reverse the XXXX transaction. However, I did not receive a response from XXXX. After this, I contacted my bank. My bank provided a temporary reimbursement while an investigation was performed. After the investigation was complete 60 days later ( XX/XX/XXXX ), the reimbursement was removed since my bank determined that XXXX is the company responsible in this matter. As such, I am looking to get a refund for the amount with XXXX.\n", + "5. I can not withdraw money or pay my rent from my Way to go card from Florida reemployment. It keeps declining me. It was working fine in XXXX, I went to pay my rent for XXXX, and it declined ; I went to an atm again declined. So I ordered a new card and paid expedited shipping ; thinking this one was damaged, I activated it, and it again declined me. There is absolutely no way to contact anyone. All they have is an automatic response which doesn't provide any help\n", + "\n" + ] + } + ], + "source": [ + "# The plain English request we will make of PaLM 2\n", + "prompt = (\n", + " \"Please highlight the most obvious difference between \"\n", + " \"the two lists of comments:\\n\" + prompt1 + prompt2\n", + ")\n", + "print(prompt)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get a response from PaLM 2 LLM by making a call to Vertex AI using our connection." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "mL5P0_3X04dE" + }, + "outputs": [ { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Get a response from PaLM 2 LLM by making a call to Vertex AI using our connection." + "data": { + "text/html": [ + "Query job 48da679e-d991-4af6-939f-8c45b3a262d8 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] - }, + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from bigframes.ml.llm import PaLM2TextGenerator\n", + "\n", + "q_a_model = PaLM2TextGenerator()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "ICWHsqAW1FNk" + }, + "outputs": [], + "source": [ + "# Make a DataFrame containing only a single row with our prompt for PaLM 2\n", + "df = bf.DataFrame({\"prompt\": [prompt]})" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "gB7e1LXU1pst" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "mL5P0_3X04dE" - }, - "outputs": [], - "source": [ - "from bigframes.ml.llm import PaLM2TextGenerator\n", - "\n", - "q_a_model = PaLM2TextGenerator()" + "data": { + "text/html": [ + "Query job 8877d0e9-a41e-4344-8dea-bf57c93a505e is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ICWHsqAW1FNk" - }, - "outputs": [], - "source": [ - "# Make a DataFrame containing only a single row with our prompt for PaLM 2\n", - "df = bf.DataFrame({\"prompt\": [prompt]})" + "data": { + "text/html": [ + "Query job fa3de153-d6b8-421f-84f6-69e401a36cc8 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "gB7e1LXU1pst" - }, - "outputs": [], - "source": [ - "# Send the request for PaLM 2 to generate a response to our prompt\n", - "major_difference = q_a_model.predict(df)\n", - "# PaLM 2's response is the only row in the dataframe result \n", - "major_difference[\"ml_generate_text_llm_result\"].iloc[0]" + "data": { + "text/html": [ + "Query job 749fbc0b-5da2-4089-ac80-d7a2ac8325f2 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now see PaLM2TextGenerator's characterization of the different comment groups. Thanks for using BigQuery DataFrames!" + "data": { + "text/html": [ + "Query job 6a8c8c0c-894f-41f1-a20f-adfb67349281 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Summary and next steps\n", - "\n", - "You've used the ML and LLM capabilities of BigQuery DataFrames to help analyze and understand a large dataset of unstructured feedback.\n", - "\n", - "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + "data": { + "text/plain": [ + "' The most obvious difference between the two lists of comments is the nature of the issues being raised.\\n\\n**Comment list 1 primarily focuses on disputes related to debt collection practices.** \\n\\n- Commenters express concerns about receiving debt collection notices for debts they believe they do not owe, lack of supporting documentation, and the transfer of debt between debt collection agencies without proper resolution. \\n\\n- They highlight the impact on their credit scores and the frustration of dealing with misleading or deceptive practices.\\n\\n\\n**Comment list 2, on the other hand, covers a wider range of issues related to financial services.** \\n\\n- It includes complaints about fraudulent charges'" ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" } - ], - "metadata": { - "colab": { - "provenance": [] - }, - "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.13" - } + ], + "source": [ + "# Send the request for PaLM 2 to generate a response to our prompt\n", + "major_difference = q_a_model.predict(df)\n", + "# PaLM 2's response is the only row in the dataframe result \n", + "major_difference[\"ml_generate_text_llm_result\"].iloc[0]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now see PaLM2TextGenerator's characterization of the different comment groups. Thanks for using BigQuery DataFrames!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Summary and next steps\n", + "\n", + "You've used the ML and LLM capabilities of BigQuery DataFrames to help analyze and understand a large dataset of unstructured feedback.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index f39815aec28..d20867a2d77 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -22,6 +22,7 @@ import pytz import bigframes +import bigframes.features from bigframes.ml import core import tests.system.utils @@ -263,7 +264,6 @@ def test_model_predict(penguins_bqml_linear_model: core.BqmlModel, new_penguins_ def test_model_predict_with_unnamed_index( penguins_bqml_linear_model: core.BqmlModel, new_penguins_df ): - # This will result in an index that lacks a name, which the ML library will # need to persist through the call to ML.PREDICT new_penguins_df = new_penguins_df.reset_index() @@ -295,7 +295,11 @@ def test_remote_model_predict( expected = pd.DataFrame( {"predicted_body_mass_g": [[3739.54], [3675.79], [3619.54]]}, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - dtype=pd.ArrowDtype(pa.list_(pa.float64())), + dtype=( + pd.ArrowDtype(pa.list_(pa.float64())) + if bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable + else "object" + ), ) predictions = bqml_linear_remote_model.predict(new_penguins_df).to_pandas() pd.testing.assert_frame_equal( diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index c3bd7f3b87f..990795da3b0 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -17,8 +17,15 @@ import pandas as pd import pyarrow as pa +import bigframes.features import bigframes.ml.preprocessing +ONE_HOT_ENCODED_DTYPE = ( + pd.ArrowDtype(pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())]))) + if bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable + else "object" +) + def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. @@ -454,9 +461,7 @@ def test_one_hot_encoder_default_params(new_penguins_df): [{"index": 2, "value": 1.0}], ], }, - dtype=pd.ArrowDtype( - pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) - ), + dtype=ONE_HOT_ENCODED_DTYPE, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -486,9 +491,7 @@ def test_one_hot_encoder_default_params_fit_transform(new_penguins_df): [{"index": 2, "value": 1.0}], ], }, - dtype=pd.ArrowDtype( - pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) - ), + dtype=ONE_HOT_ENCODED_DTYPE, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -514,9 +517,7 @@ def test_one_hot_encoder_series_default_params(new_penguins_df): [{"index": 2, "value": 1.0}], ], }, - dtype=pd.ArrowDtype( - pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) - ), + dtype=ONE_HOT_ENCODED_DTYPE, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -547,9 +548,7 @@ def test_one_hot_encoder_params(new_penguins_df): [{"index": 0, "value": 1.0}], ], }, - dtype=pd.ArrowDtype( - pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) - ), + dtype=ONE_HOT_ENCODED_DTYPE, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) @@ -580,9 +579,7 @@ def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_ [{"index": 2, "value": 1.0}], ], }, - dtype=pd.ArrowDtype( - pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())])) - ), + dtype=ONE_HOT_ENCODED_DTYPE, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 05c9d223728..f9a0d2481b0 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -30,6 +30,7 @@ import bigframes import bigframes.dataframe +import bigframes.features import bigframes.pandas as bpd @@ -57,7 +58,12 @@ def test_to_pandas_array_struct_correct_result(session): expected = pd.DataFrame( { "array_column": pd.Series( - [[1, 3, 2]], dtype=pd.ArrowDtype(pa.list_(pa.int64())) + [[1, 3, 2]], + dtype=( + pd.ArrowDtype(pa.list_(pa.int64())) + if bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable + else "object" + ), ), "struct_column": pd.Series( [{"string_field": "a", "float_field": 1.2}], diff --git a/tests/unit/session/test_io_pandas.py b/tests/unit/session/test_io_pandas.py index 0f6f5dae03b..959cccd80ec 100644 --- a/tests/unit/session/test_io_pandas.py +++ b/tests/unit/session/test_io_pandas.py @@ -23,8 +23,28 @@ import pyarrow # type: ignore import pytest +import bigframes.features import bigframes.session._io.pandas +_LIST_OF_SCALARS = [ + [1, 2, 3], + [], + [4, 5, 6], +] +_LIST_OF_STRUCTS = [ + [ + {"version": 1, "package": "numpy"}, + {"version": 2, "package": "pandas"}, + {"version": 3, "package": "pyarrow"}, + ], + [], + [ + {"version": 4, "package": "awkward-pandas"}, + {"version": 5, "package": "cyberpandas"}, + {"version": 6, "package": "geopandas"}, + ], +] + @pytest.mark.parametrize( ("arrow_table", "dtypes", "expected"), @@ -184,6 +204,111 @@ ), id="arrow-dtypes", ), + pytest.param( + pyarrow.Table.from_pydict( + { + "listofscalars": pyarrow.array( + _LIST_OF_SCALARS, + type=pyarrow.list_(pyarrow.int64()), + ), + "listofstructs": pyarrow.array( + _LIST_OF_STRUCTS, + type=pyarrow.list_( + pyarrow.struct( + [ + ("version", pyarrow.int64()), + ("package", pyarrow.string()), + ] + ) + ), + ), + }, + ), + { + "listofscalars": pandas.ArrowDtype(pyarrow.list_(pyarrow.int64())), + "listofstructs": pandas.ArrowDtype( + pyarrow.list_( + pyarrow.struct( + [ + ("version", pyarrow.int64()), + ("package", pyarrow.string()), + ] + ) + ) + ), + }, + pandas.DataFrame( + { + "listofscalars": pandas.Series(_LIST_OF_SCALARS, dtype="object"), + "listofstructs": pandas.Series(_LIST_OF_STRUCTS, dtype="object"), + }, + ), + marks=pytest.mark.skipif( + bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable, + reason="no need to use object dtype for ARRAY in pandas 2.x", + ), + id="nested-dtypes-pandas-1-x", + ), + pytest.param( + pyarrow.Table.from_pydict( + { + "listofscalars": pyarrow.array( + _LIST_OF_SCALARS, + type=pyarrow.list_(pyarrow.int64()), + ), + "listofstructs": pyarrow.array( + _LIST_OF_STRUCTS, + type=pyarrow.list_( + pyarrow.struct( + [ + ("version", pyarrow.int64()), + ("package", pyarrow.string()), + ] + ) + ), + ), + }, + ), + { + "listofscalars": pandas.ArrowDtype(pyarrow.list_(pyarrow.int64())), + "listofstructs": pandas.ArrowDtype( + pyarrow.list_( + pyarrow.struct( + [ + ("version", pyarrow.int64()), + ("package", pyarrow.string()), + ] + ) + ) + ), + }, + pandas.DataFrame( + { + "listofscalars": pandas.Series( + _LIST_OF_SCALARS, + dtype=pandas.ArrowDtype(pyarrow.list_(pyarrow.int64())), + ), + "listofstructs": pandas.Series( + _LIST_OF_STRUCTS, + dtype=pandas.ArrowDtype( + pyarrow.list_( + pyarrow.struct( + [ + ("version", pyarrow.int64()), + ("package", pyarrow.string()), + ] + ) + ) + ), + ), + }, + ), + marks=pytest.mark.skipif( + not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable, + reason="Arrow list type broken in pandas 1.x", + ), + id="nested-dtypes-pandas-2-x", + ), pytest.param( pyarrow.Table.from_pydict( { diff --git a/tests/unit/test_features.py b/tests/unit/test_features.py new file mode 100644 index 00000000000..20642aec343 --- /dev/null +++ b/tests/unit/test_features.py @@ -0,0 +1,49 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas +import pytest + +import bigframes.features + + +def test_pandas_installed_version_returns_cached(): + versions = bigframes.features.PandasVersions() + versions._installed_version = object() + assert versions.installed_version is versions._installed_version + + +def test_pandas_installed_version_returns_parsed_version(monkeypatch): + versions = bigframes.features.PandasVersions() + monkeypatch.setattr(pandas, "__version__", "1.2.3") + major, minor, micro = versions.installed_version + assert major == "1" + assert minor == "2" + assert micro == "3" + + +@pytest.mark.parametrize( + ("version", "expected"), + ( + ("1.2.3", False), + ("1.5.3", False), + ("2.0.0", True), + ("2.2.3", True), + ("3.0.0", True), + ), +) +def test_pandas_is_arrow_list_dtype_usable(version, expected): + versions = bigframes.features.PandasVersions() + versions._installed_version = version.split(".") + assert versions.is_arrow_list_dtype_usable == expected From ae439054f9c74f2021a3830395a1f4908530403c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:08:12 -0600 Subject: [PATCH 277/921] chore(main): release 0.19.2 (#328) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 15 +++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c8050d80b..29f99ecc43c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.19.2](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.19.1...v0.19.2) (2024-01-22) + + +### Bug Fixes + +* Read_gbq large response issue ([#332](https://github.com/googleapis/python-bigquery-dataframes/issues/332)) ([b8178b9](https://github.com/googleapis/python-bigquery-dataframes/commit/b8178b9a47958d9176d99dfd8833556a64d9724d)) +* Use object dtype for ARRAY columns in `to_pandas()` with pandas 1.x ([#329](https://github.com/googleapis/python-bigquery-dataframes/issues/329)) ([374ddb5](https://github.com/googleapis/python-bigquery-dataframes/commit/374ddb534777895d93a1e2ae2f9c6dbe5f10bf8c)) + + +### Documentation + +* Add `DataFrame.applymap` documentation ([#326](https://github.com/googleapis/python-bigquery-dataframes/issues/326)) ([bd531a1](https://github.com/googleapis/python-bigquery-dataframes/commit/bd531a1557c08bcee6a0d275747f0939cdd33e81)) +* Add code samples for series methods ([#323](https://github.com/googleapis/python-bigquery-dataframes/issues/323)) ([32cc6fa](https://github.com/googleapis/python-bigquery-dataframes/commit/32cc6fa73dea80e31985d380d550d8042e5f5566)) +* Add remote model requirements ([#333](https://github.com/googleapis/python-bigquery-dataframes/issues/333)) ([c91f70c](https://github.com/googleapis/python-bigquery-dataframes/commit/c91f70ca7b9793cc62578d7845c3aa31cf8a4507)) + ## [0.19.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.19.0...v0.19.1) (2024-01-17) diff --git a/bigframes/version.py b/bigframes/version.py index 043d22fd117..4d71f17d710 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.1" +__version__ = "0.19.2" From 4ff53db48133b817bec5f123b634690244a610d3 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:19:04 -0800 Subject: [PATCH 278/921] feat: update cut to work without labels = False and show intervals as dict (#335) * test ver. * add test and adjustment * update test and docstring. * remove unused import. * update code examples. * COde formatted. * Update error and unittest. * Update labels selections. --- bigframes/core/reshape/__init__.py | 9 ++-- bigframes/operations/aggregations.py | 41 +++++++++++++++---- tests/system/small/test_pandas.py | 25 ++++++++++- tests/unit/test_pandas.py | 5 ++- .../pandas/core/reshape/tile.py | 17 +++++--- 5 files changed, 80 insertions(+), 17 deletions(-) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index cadd8e51453..4a3bb16a392 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -129,12 +129,15 @@ def cut( if bins.is_overlapping: raise ValueError("Overlapping IntervalIndex is not accepted.") - if labels is not False: + if labels is not None and labels is not False: raise NotImplementedError( - f"Only labels=False is supported in BigQuery DataFrames so far. {constants.FEEDBACK_LINK}" + "The 'labels' parameter must be either False or None. " + "Please provide a valid value for 'labels'." ) - return x._apply_window_op(agg_ops.CutOp(bins), window_spec=core.WindowSpec()) + return x._apply_window_op( + agg_ops.CutOp(bins, labels=labels), window_spec=core.WindowSpec() + ) def qcut( diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 452abf047c6..cc2e79c1003 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -229,7 +229,7 @@ def skips_nulls(self): class CutOp(WindowOp): - def __init__(self, bins: typing.Union[int, pd.IntervalIndex]): + def __init__(self, bins: typing.Union[int, pd.IntervalIndex], labels=None): if isinstance(bins, int): if not bins > 0: raise ValueError("`bins` should be a positive integer.") @@ -239,6 +239,8 @@ def __init__(self, bins: typing.Union[int, pd.IntervalIndex]): self._bins_int = 0 self._bins = bins + self._labels = labels + def _as_ibis(self, x: ibis_types.Column, window=None): out = ibis.case() @@ -247,12 +249,37 @@ def _as_ibis(self, x: ibis_types.Column, window=None): col_max = _apply_window_if_present(x.max(), window) bin_width = (col_max - col_min) / self._bins - for this_bin in range(self._bins_int - 1): - out = out.when( - x <= (col_min + (this_bin + 1) * bin_width), - dtypes.literal_to_ibis_scalar(this_bin, force_dtype=Int64Dtype()), - ) - out = out.when(x.notnull(), self._bins - 1) + if self._labels is False: + for this_bin in range(self._bins_int - 1): + out = out.when( + x <= (col_min + (this_bin + 1) * bin_width), + dtypes.literal_to_ibis_scalar( + this_bin, force_dtype=Int64Dtype() + ), + ) + out = out.when(x.notnull(), self._bins - 1) + else: + interval_struct = None + adj = (col_max - col_min) * 0.001 + for this_bin in range(self._bins_int): + left_edge = ( + col_min + this_bin * bin_width - (0 if this_bin > 0 else adj) + ) + right_edge = col_min + (this_bin + 1) * bin_width + interval_struct = ibis.struct( + { + "left_exclusive": left_edge, + "right_inclusive": right_edge, + } + ) + + if this_bin < self._bins_int - 1: + out = out.when( + x <= (col_min + (this_bin + 1) * bin_width), + interval_struct, + ) + else: + out = out.when(x.notnull(), interval_struct) else: for interval in self._bins: condition = (x > interval.left) & (x <= interval.right) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index a79ddb64cd3..0910c0b7e26 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -397,6 +397,30 @@ def test_cut(scalars_dfs): pd.testing.assert_series_equal(bf_result, pd_result) +def test_cut_default_labels(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = pd.cut(scalars_pandas_df["float64_col"], 5) + bf_result = bpd.cut(scalars_df["float64_col"], 5).to_pandas() + + # Convert to match data format + pd_result_converted = pd.Series( + [ + {"left_exclusive": interval.left, "right_inclusive": interval.right} + if pd.notna(val) + else pd.NA + for val, interval in zip( + pd_result, pd_result.cat.categories[pd_result.cat.codes] + ) + ], + name=pd_result.name, + ) + + pd.testing.assert_series_equal( + bf_result, pd_result_converted, check_index=False, check_dtype=False + ) + + @pytest.mark.parametrize( ("bins",), [ @@ -424,7 +448,6 @@ def test_cut_with_interval(scalars_dfs, bins): ], name=pd_result.name, ) - pd_result.index = pd_result.index.astype("Int64") pd.testing.assert_series_equal( bf_result, pd_result_converted, check_index=False, check_dtype=False diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 4835a24dc7d..d6af223456f 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -85,7 +85,10 @@ def test_method_matches_session(method_name: str): def test_cut_raises_with_labels(): - with pytest.raises(NotImplementedError, match="Only labels=False"): + with pytest.raises( + NotImplementedError, + match="The 'labels' parameter must be either False or None.", + ): mock_series = mock.create_autospec(bigframes.pandas.Series, instance=True) bigframes.pandas.cut(mock_series, 4, labels=["a", "b", "c", "d"]) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/tile.py b/third_party/bigframes_vendored/pandas/core/reshape/tile.py index 55975c3fc1c..fbd1d2d0527 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/tile.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/tile.py @@ -38,6 +38,15 @@ def cut( Cut with an integer (equal-width bins): + >>> bpd.cut(s, bins=4) + 0 {'left_exclusive': -0.01, 'right_inclusive': 2.5} + 1 {'left_exclusive': -0.01, 'right_inclusive': 2.5} + 2 {'left_exclusive': 2.5, 'right_inclusive': 5.0} + 3 {'left_exclusive': 7.5, 'right_inclusive': 10.0} + dtype: struct[pyarrow] + + Cut with an integer (equal-width bins) and labels=False: + >>> bpd.cut(s, bins=4, labels=False) 0 0 1 0 @@ -50,7 +59,7 @@ def cut( >>> import pandas as pd >>> interval_index = pd.IntervalIndex.from_tuples([(0, 1), (1, 5), (5, 20)]) - >>> bpd.cut(s, bins=interval_index, labels=False) + >>> bpd.cut(s, bins=interval_index) 0 1 {'left_exclusive': 0, 'right_inclusive': 1} 2 {'left_exclusive': 1, 'right_inclusive': 5} @@ -60,7 +69,7 @@ def cut( Cut with an iterable of tuples: >>> bins_tuples = [(0, 1), (1, 4), (5, 20)] - >>> bpd.cut(s, bins=bins_tuples, labels=False) + >>> bpd.cut(s, bins=bins_tuples) 0 1 {'left_exclusive': 0, 'right_inclusive': 1} 2 @@ -82,9 +91,7 @@ def cut( labels (None): Specifies the labels for the returned bins. Must be the same length as the resulting bins. If False, returns only integer indicators of the - bins. This affects the type of the output container (see below). - If True, raises an error. When `ordered=False`, labels must be - provided. + bins. This affects the type of the output container. Returns: Series: A Series representing the respective bin for each value From 2c6af9ba8b362dae39a6e082cdc816c955c73517 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 23 Jan 2024 15:34:02 -0800 Subject: [PATCH 279/921] fix: Series iteration correctly returns values instead of index (#339) * fix: Series iteration correctly returns values instead of index * Update iter docstring --- bigframes/series.py | 2 +- tests/system/small/test_series.py | 10 ++++++++++ .../bigframes_vendored/pandas/core/generic.py | 12 ++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/bigframes/series.py b/bigframes/series.py index 2371aad780b..e049b414614 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -155,7 +155,7 @@ def __len__(self): def __iter__(self) -> typing.Iterator: return itertools.chain.from_iterable( - map(lambda x: x.index, self._block.to_pandas_batches()) + map(lambda x: x.squeeze(axis=1), self._block.to_pandas_batches()) ) def copy(self) -> Series: diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 6f919f740fe..69b35d102c5 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2987,3 +2987,13 @@ def test_sample(scalars_dfs, frac, n, random_state): n = 1 if n is None else n expected_sample_size = round(frac * scalars_df.shape[0]) if frac is not None else n assert bf_result.shape[0] == expected_sample_size + + +def test_series_iter( + scalars_df_index, + scalars_pandas_df_index, +): + for bf_i, pd_i in zip( + scalars_df_index["int64_too"], scalars_pandas_df_index["int64_too"] + ): + assert bf_i == pd_i diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 2ca51f64937..b55c7e23d82 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -50,10 +50,10 @@ def size(self) -> int: def __iter__(self) -> Iterator: """ - Iterate over info axis. + Iterate over column axis for DataFrame, or values for Series. - Returns - iterator: Info axis as iterator. + Returns: + iterator **Examples:** >>> import bigframes.pandas as bpd @@ -71,9 +71,9 @@ def __iter__(self) -> Iterator: >>> series = bpd.Series(["a", "b", "c"], index=[10, 20, 30]) >>> for x in series: ... print(x) - 10 - 20 - 30 + a + b + c """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 75dc9e60d1b431dd7b25b4ed6984da0ee852b83f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 24 Jan 2024 02:40:45 +0000 Subject: [PATCH 280/921] chore: exclude pandas 2.2.0 to unblock prerelease tests (#343) --- noxfile.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 62fbee2650e..873b60ce912 100644 --- a/noxfile.py +++ b/noxfile.py @@ -542,7 +542,12 @@ def prerelease(session: nox.sessions.Session, tests_path): # TODO(shobs): Remove excluding version 2.2.0rc0 after # https://github.com/pandas-dev/pandas/issues/56646 and # https://github.com/pandas-dev/pandas/issues/56651 are resolved. - "pandas!=2.1.4,!=2.2.0rc0", + # + # TODO(shobs): Remove excluding version 2.2.0 after + # https://github.com/googleapis/python-bigquery-dataframes/issues/341 + # https://github.com/googleapis/python-bigquery-dataframes/issues/337 + # are resolved + "pandas!=2.1.4, !=2.2.0rc0, !=2.2.0", ) already_installed.add("pandas") From 47c32853017a7557538f4f17cdcd90fc3a9c9ae1 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 24 Jan 2024 20:40:15 +0000 Subject: [PATCH 281/921] chore: Script to inspect and clean up stale GCFs (#331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 319307783 🦕 ### Usage: ```bash $ python scripts/manage_cloud_functions.py --help usage: manage_cloud_functions.py [-h] -p PROJECT_ID [-r REGIONS] {summary,cleanup} ... Manage cloud functions created to serve bigframes remote functions. options: -h, --help show this help message and exit -p PROJECT_ID, --project-id PROJECT_ID GCP project-id. -r REGIONS, --regions REGIONS Cloud functions region(s). If multiple regions, Specify comma separated (e.g. region1,region2) subcommands: {summary,cleanup} summary BigFrames cloud functions summary. cleanup BigFrames cloud functions clean up. $ python scripts/manage_cloud_functions.py summary --help usage: manage_cloud_functions.py summary [-h] Show the bigframes cloud functions summary. options: -h, --help show this help message and exit $ python scripts/manage_cloud_functions.py cleanup --help usage: manage_cloud_functions.py cleanup [-h] [-n NUMBER] Delete the stale bigframes cloud functions. options: -h, --help show this help message and exit -n NUMBER, --number NUMBER Number of stale (more than a day old) cloud functions to clean up. (venv) shobs@shobs-ct-3:~/code/bigframes1$ ``` ### Example: ```bash $ python scripts/manage_cloud_functions.py -p bigframes-dev summary us-central1: Total=1412, Recent=86, OlderThanADay=1326 europe-west4: Total=270, Recent=24, OlderThanADay=246 southamerica-west1: Total=269, Recent=23, OlderThanADay=246 europe-west1: Total=262, Recent=23, OlderThanADay=239 asia-southeast1: Total=260, Recent=18, OlderThanADay=242 us-east1: Total=1, Recent=0, OlderThanADay=1 $ python scripts/manage_cloud_functions.py -p bigframes-dev -r us-central1,europe-west4 summary us-central1: Total=1412, Recent=85, OlderThanADay=1327 europe-west4: Total=270, Recent=24, OlderThanADay=246 $ python scripts/manage_cloud_functions.py -p bigframes-dev -r us-central1,europe-west4 cleanup -n 2 [us-central1]: deleted [1] projects/bigframes-dev/locations/us-central1/functions/bigframes-597cc02ef5ce0525e4f51697b5a83b6c-3pfpu6gu last updated on 2024-01-08 21:47:58.503628+00:00 [us-central1]: deleted [2] projects/bigframes-dev/locations/us-central1/functions/bigframes-68f796a13666bb3bfe354dd1adaeef71 last updated on 2024-01-09 21:52:49.620259+00:00 [europe-west4]: deleted [1] projects/bigframes-dev/locations/europe-west4/functions/bigframes-558d0ca6649537a9e45896faf08b0a7a last updated on 2024-01-12 21:15:04.379198+00:00 [europe-west4]: deleted [2] projects/bigframes-dev/locations/europe-west4/functions/bigframes-4b7705561ec336ed80722a8e6e56ac41 last updated on 2024-01-08 05:34:59.331828+00:00 $ python scripts/manage_cloud_functions.py -p bigframes-dev -r us-central1,europe-west4 summary us-central1: Total=1410, Recent=85, OlderThanADay=1325 europe-west4: Total=269, Recent=25, OlderThanADay=244 ``` --- scripts/manage_cloud_functions.py | 195 ++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 scripts/manage_cloud_functions.py diff --git a/scripts/manage_cloud_functions.py b/scripts/manage_cloud_functions.py new file mode 100644 index 00000000000..6b690890892 --- /dev/null +++ b/scripts/manage_cloud_functions.py @@ -0,0 +1,195 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +from datetime import datetime +import sys +import time + +import google.api_core.exceptions +from google.cloud import functions_v2 + +GCF_REGIONS_ALL = [ + "asia-east1", + "asia-east2", + "asia-northeast1", + "asia-northeast2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west2", + "europe-west4", + "europe-west8", + "europe-west9", + "us-central1", + "us-east1", + "us-east4", + "us-east5", + "us-south1", + "us-west1", + "asia-east2", + "asia-northeast3", + "asia-southeast1", + "asia-southeast2", + "asia-south1", + "asia-south2", + "australia-southeast1", + "australia-southeast2", + "europe-central2", + "europe-west2", + "europe-west3", + "europe-west6", + "northamerica-northeast1", + "northamerica-northeast2", + "southamerica-east1", + "southamerica-west1", + "us-west2", + "us-west3", + "us-west4", +] + +GCF_CLIENT = functions_v2.FunctionServiceClient() + + +def get_bigframes_functions(project, region): + parent = f"projects/{args.project_id}/locations/{region}" + functions = GCF_CLIENT.list_functions( + functions_v2.ListFunctionsRequest(parent=parent) + ) + # Filter bigframes created functions + functions = [ + function + for function in functions + if function.name.startswith( + f"projects/{args.project_id}/locations/{region}/functions/bigframes-" + ) + ] + + return functions + + +def summarize_gcfs(args): + """Summarize number of bigframes cloud functions in various regions.""" + + region_counts = {} + for region in args.regions: + functions = get_bigframes_functions(args.project_id, region) + functions_count = len(functions) + + # Exclude reporting regions with 0 bigframes GCFs + if functions_count == 0: + continue + + # Count how many GCFs are newer than a day + recent = 0 + for f in functions: + age = datetime.now() - datetime.fromtimestamp(f.update_time.timestamp()) + if age.days <= 0: + recent += 1 + + region_counts[region] = (functions_count, recent) + + for item in sorted( + region_counts.items(), key=lambda item: item[1][0], reverse=True + ): + region = item[0] + count, recent = item[1] + print( + "{}: Total={}, Recent={}, OlderThanADay={}".format( + region, count, recent, count - recent + ) + ) + + +def cleanup_gcfs(args): + """Clean-up bigframes cloud functions in the given regions.""" + max_delete_per_region = args.number + + for region in args.regions: + functions = get_bigframes_functions(args.project_id, region) + count = 0 + for f in functions: + age = datetime.now() - datetime.fromtimestamp(f.update_time.timestamp()) + if age.days > 0: + try: + count += 1 + GCF_CLIENT.delete_function(name=f.name) + print( + f"[{region}]: deleted [{count}] {f.name} last updated on {f.update_time}" + ) + if count >= max_delete_per_region: + break + # Mostly there is a 60 mutations per minute quota, we want to use 10% of + # that for this clean-up, i.e. 6 mutations per minute. So wait for + # 60/6 = 10 seconds + time.sleep(10) + except google.api_core.exceptions.ResourceExhausted: + # Stop deleting in this region for now + print( + f"Cannot delete any more functions in region {region} due to quota exhaustion. Please try again later." + ) + break + + +def list_str(values): + return [val for val in values.split(",") if val] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Manage cloud functions created to serve bigframes remote functions." + ) + parser.add_argument( + "-p", + "--project-id", + type=str, + required=True, + action="store", + help="GCP project-id.", + ) + parser.add_argument( + "-r", + "--regions", + type=list_str, + required=False, + default=GCF_REGIONS_ALL, + action="store", + help="Cloud functions region(s). If multiple regions, Specify comma separated (e.g. region1,region2)", + ) + + subparsers = parser.add_subparsers(title="subcommands", required=True) + parser_summary = subparsers.add_parser( + "summary", + help="BigFrames cloud functions summary.", + description="Show the bigframes cloud functions summary.", + ) + parser_summary.set_defaults(func=summarize_gcfs) + parser_cleanup = subparsers.add_parser( + "cleanup", + help="BigFrames cloud functions clean up.", + description="Delete the stale bigframes cloud functions.", + ) + parser_cleanup.add_argument( + "-n", + "--number", + type=int, + required=False, + default=100, + action="store", + help="Number of stale (more than a day old) cloud functions to clean up.", + ) + parser_cleanup.set_defaults(func=cleanup_gcfs) + + args = parser.parse_args(sys.argv[1:]) + args.func(args) From 73e997b3e80f844a8120b52ed2ece8b046cf4ca9 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:02:53 -0800 Subject: [PATCH 282/921] feat: add ARIMA_EVAULATE options in forecasting models (#336) * feat: add ARIMA_EVAULATE options in forecasting models * feat: add summary method * fix minor errors * fix failed tests * address comments --- bigframes/ml/core.py | 7 ++++ bigframes/ml/forecasting.py | 25 ++++++++++++++ bigframes/ml/sql.py | 6 ++++ tests/system/large/ml/test_forecasting.py | 35 ++++++++++++++++++-- tests/system/small/ml/test_forecasting.py | 40 +++++++++++++++++++++++ tests/unit/ml/test_sql.py | 13 ++++++++ 6 files changed, 124 insertions(+), 2 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 1e2224c9bc0..7c156b4cb79 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -136,6 +136,13 @@ def evaluate(self, input_data: Optional[bpd.DataFrame] = None): return self._session.read_gbq(sql) + def arima_evaluate(self, show_all_candidate_models: bool = False): + sql = self._model_manipulation_sql_generator.ml_arima_evaluate( + show_all_candidate_models + ) + + return self._session.read_gbq(sql) + def centroids(self) -> bpd.DataFrame: assert self._model.model_type == "KMEANS" diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 03b9857cc5f..8d448fbace2 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -151,6 +151,31 @@ def score( input_data = X.join(y, how="outer") return self._bqml_model.evaluate(input_data) + def summary( + self, + show_all_candidate_models: bool = False, + ) -> bpd.DataFrame: + """Summary of the evaluation metrics of the time series model. + + .. note:: + + Output matches that of the BigQuery ML.ARIMA_EVALUATE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-arima-evaluate + for the outputs relevant to this model type. + + Args: + show_all_candidate_models (bool, default to False): + Whether to show evaluation metrics or an error message for either + all candidate models or for only the best model with the lowest + AIC. Default to False. + + Returns: + bigframes.dataframe.DataFrame: A DataFrame as evaluation result. + """ + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + return self._bqml_model.arima_evaluate(show_all_candidate_models) + def to_gbq(self, model_name: str, replace: bool = False) -> ARIMAPlus: """Save the model to BigQuery. diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 25caaf1ac6b..152f881ec04 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -260,6 +260,12 @@ def ml_evaluate(self, source_df: Optional[bpd.DataFrame] = None) -> str: return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, ({source_sql}))""" + # ML evaluation TVFs + def ml_arima_evaluate(self, show_all_candidate_models: bool = False) -> str: + """Encode ML.ARMIA_EVALUATE for BQML""" + return f"""SELECT * FROM ML.ARIMA_EVALUATE(MODEL `{self._model_name}`, + STRUCT({show_all_candidate_models} AS show_all_candidate_models))""" + def ml_centroids(self) -> str: """Encode ML.CENTROIDS for BQML""" return f"""SELECT * FROM ML.CENTROIDS(MODEL `{self._model_name}`)""" diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index 33b835e8522..2bb136b0f2d 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -16,6 +16,20 @@ from bigframes.ml import forecasting +ARIMA_EVALUATE_OUTPUT_COL = [ + "non_seasonal_p", + "non_seasonal_d", + "non_seasonal_q", + "log_likelihood", + "AIC", + "variance", + "seasonal_periods", + "has_holiday_effect", + "has_spikes_and_dips", + "has_step_changes", + "error_message", +] + def test_arima_plus_model_fit_score( time_series_df_default_index, dataset_id, new_time_series_df @@ -42,7 +56,24 @@ def test_arima_plus_model_fit_score( pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load to ensure configuration was kept - reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) + reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) + assert ( + f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name + ) + + +def test_arima_plus_model_fit_summary(time_series_df_default_index, dataset_id): + model = forecasting.ARIMAPlus() + X_train = time_series_df_default_index[["parsed_date"]] + y_train = time_series_df_default_index[["total_visits"]] + model.fit(X_train, y_train) + + result = model.summary() + assert result.shape == (1, 12) + assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) + + # save, load to ensure configuration was kept + reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) assert ( - f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name + f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name ) diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index be8d9c2bac8..4726d5ab217 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -20,6 +20,20 @@ from bigframes.ml import forecasting +ARIMA_EVALUATE_OUTPUT_COL = [ + "non_seasonal_p", + "non_seasonal_d", + "non_seasonal_q", + "log_likelihood", + "AIC", + "variance", + "seasonal_periods", + "has_holiday_effect", + "has_spikes_and_dips", + "has_step_changes", + "error_message", +] + def test_model_predict_default(time_series_arima_plus_model: forecasting.ARIMAPlus): utc = pytz.utc @@ -104,6 +118,24 @@ def test_model_score( ) +def test_model_summary( + time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +): + result = time_series_arima_plus_model.summary() + assert result.shape == (1, 12) + assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) + + +def test_model_summary_show_all_candidates( + time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +): + result = time_series_arima_plus_model.summary( + show_all_candidate_models=True, + ) + assert result.shape[0] > 1 + assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) + + def test_model_score_series( time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df ): @@ -126,3 +158,11 @@ def test_model_score_series( rtol=0.1, check_index_type=False, ) + + +def test_model_summary_series( + time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +): + result = time_series_arima_plus_model.summary() + assert result.shape == (1, 12) + assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 73d19cc0bb0..37cc33d33e2 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -273,6 +273,19 @@ def test_ml_evaluate_produces_correct_sql( ) +def test_ml_arima_evaluate_produces_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_arima_evaluate( + show_all_candidate_models=True + ) + assert ( + sql + == """SELECT * FROM ML.ARIMA_EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, + STRUCT(True AS show_all_candidate_models))""" + ) + + def test_ml_evaluate_no_source_produces_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): From d88c5623e4eb3259384d82852179bb586c9eeeb5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 24 Jan 2024 18:00:39 -0800 Subject: [PATCH 283/921] refactor: add output type annotations to scalar ops (#338) * refactor: add output type annotations to scalar ops * use same expression type annotation everywhere * pr comments --- bigframes/core/expression.py | 43 ++++- bigframes/dtypes.py | 65 ++++++- bigframes/functions/remote_function.py | 50 ++---- bigframes/operations/__init__.py | 236 +++++++++++++++++-------- bigframes/operations/type.py | 80 +++++++++ tests/unit/core/test_expression.py | 49 +++++ tests/unit/test_dtypes.py | 4 +- tests/unit/test_remote_function.py | 9 +- 8 files changed, 407 insertions(+), 129 deletions(-) create mode 100644 bigframes/operations/type.py create mode 100644 tests/unit/core/test_expression.py diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 540f9b6e5a2..d1be6444395 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -18,16 +18,13 @@ import dataclasses import itertools import typing -from typing import Optional -import bigframes.dtypes +import bigframes.dtypes as dtypes import bigframes.operations -def const( - value: typing.Hashable, dtype: Optional[bigframes.dtypes.Dtype] = None -) -> Expression: - return ScalarConstantExpression(value, dtype) +def const(value: typing.Hashable, dtype: dtypes.ExpressionType = None) -> Expression: + return ScalarConstantExpression(value, dtype or dtypes.infer_literal_type(value)) def free_var(id: str) -> Expression: @@ -45,9 +42,16 @@ def unbound_variables(self) -> typing.Tuple[str, ...]: def rename(self, name_mapping: dict[str, str]) -> Expression: return self - @abc.abstractproperty + @property + @abc.abstractmethod def is_const(self) -> bool: - return False + ... + + @abc.abstractmethod + def output_type( + self, input_types: dict[str, dtypes.ExpressionType] + ) -> dtypes.ExpressionType: + ... @dataclasses.dataclass(frozen=True) @@ -56,12 +60,17 @@ class ScalarConstantExpression(Expression): # TODO: Further constrain? value: typing.Hashable - dtype: Optional[bigframes.dtypes.Dtype] = None + dtype: dtypes.ExpressionType = None @property def is_const(self) -> bool: return True + def output_type( + self, input_types: dict[str, bigframes.dtypes.Dtype] + ) -> dtypes.ExpressionType: + return self.dtype + @dataclasses.dataclass(frozen=True) class UnboundVariableExpression(Expression): @@ -83,6 +92,14 @@ def rename(self, name_mapping: dict[str, str]) -> Expression: def is_const(self) -> bool: return False + def output_type( + self, input_types: dict[str, bigframes.dtypes.Dtype] + ) -> dtypes.ExpressionType: + if self.id in input_types: + return input_types[self.id] + else: + raise ValueError("Type of variable has not been fixed.") + @dataclasses.dataclass(frozen=True) class OpExpression(Expression): @@ -110,3 +127,11 @@ def rename(self, name_mapping: dict[str, str]) -> Expression: @property def is_const(self) -> bool: return all(child.is_const for child in self.inputs) + + def output_type( + self, input_types: dict[str, dtypes.ExpressionType] + ) -> dtypes.ExpressionType: + operand_types = tuple( + map(lambda x: x.output_type(input_types=input_types), self.inputs) + ) + return self.op.output_type(*operand_types) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 608885dec42..cb2210bec64 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -23,7 +23,9 @@ import geopandas as gpd # type: ignore import google.cloud.bigquery as bigquery import ibis +from ibis.backends.bigquery.datatypes import BigQueryType import ibis.expr.datatypes as ibis_dtypes +from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type import ibis.expr.types as ibis_types import numpy as np import pandas as pd @@ -42,6 +44,14 @@ pd.ArrowDtype, gpd.array.GeometryDtype, ] +# Represents both column types (dtypes) and local-only types +# None represents the type of a None scalar. +ExpressionType = typing.Optional[Dtype] + +INT_DTYPE = pd.Int64Dtype() +FLOAT_DTYPE = pd.Float64Dtype() +BOOL_DTYPE = pd.BooleanDtype() +STRING_DTYPE = pd.StringDtype(storage="pyarrow") # On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable UNORDERED_DTYPES = [gpd.array.GeometryDtype()] @@ -539,20 +549,20 @@ def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: return lcd_type(pd.Int64Dtype(), dtype) if isinstance(scalar, decimal.Decimal): # TODO: Check context to see if can use NUMERIC instead of BIGNUMERIC - return lcd_type(pd.ArrowDtype(pa.decimal128(76, 38)), dtype) + return lcd_type(pd.ArrowDtype(pa.decimal256(76, 38)), dtype) return None -def lcd_type(dtype1: Dtype, dtype2: Dtype) -> typing.Optional[Dtype]: +def lcd_type(dtype1: Dtype, dtype2: Dtype) -> Dtype: if dtype1 == dtype2: return dtype1 # Implicit conversion currently only supported for numeric types hierarchy: list[Dtype] = [ pd.BooleanDtype(), pd.Int64Dtype(), - pd.Float64Dtype(), pd.ArrowDtype(pa.decimal128(38, 9)), pd.ArrowDtype(pa.decimal256(76, 38)), + pd.Float64Dtype(), ] if (dtype1 not in hierarchy) or (dtype2 not in hierarchy): return None @@ -560,6 +570,14 @@ def lcd_type(dtype1: Dtype, dtype2: Dtype) -> typing.Optional[Dtype]: return hierarchy[lcd_index] +def lcd_etype(etype1: ExpressionType, etype2: ExpressionType) -> ExpressionType: + if etype1 is None: + return etype2 + if etype2 is None: + return etype1 + return lcd_type_or_throw(etype1, etype2) + + def lcd_type_or_throw(dtype1: Dtype, dtype2: Dtype) -> Dtype: result = lcd_type(dtype1, dtype2) if result is None: @@ -567,3 +585,44 @@ def lcd_type_or_throw(dtype1: Dtype, dtype2: Dtype) -> Dtype: f"BigFrames cannot upcast {dtype1} and {dtype2} to common type. {constants.FEEDBACK_LINK}" ) return result + + +def infer_literal_type(literal) -> typing.Optional[Dtype]: + if pd.isna(literal): + return None # Null value without a definite type + # Temporary logic, use ibis inferred type + ibis_literal = literal_to_ibis_scalar(literal) + return ibis_dtype_to_bigframes_dtype(ibis_literal.type()) + + +# Input and output types supported by BigQuery DataFrames remote functions. +# TODO(shobs): Extend the support to all types supported by BQ remote functions +# https://cloud.google.com/bigquery/docs/remote-functions#limitations +SUPPORTED_IO_PYTHON_TYPES = {bool, float, int, str} +SUPPORTED_IO_BIGQUERY_TYPEKINDS = { + "BOOLEAN", + "BOOL", + "FLOAT", + "FLOAT64", + "INT64", + "INTEGER", + "STRING", +} + + +class UnsupportedTypeError(ValueError): + def __init__(self, type_, supported_types): + self.type = type_ + self.supported_types = supported_types + + +def ibis_type_from_python_type(t: type) -> ibis_dtypes.DataType: + if t not in SUPPORTED_IO_PYTHON_TYPES: + raise UnsupportedTypeError(t, SUPPORTED_IO_PYTHON_TYPES) + return python_type_to_bigquery_type(t) + + +def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> ibis_dtypes.DataType: + if tk not in SUPPORTED_IO_BIGQUERY_TYPEKINDS: + raise UnsupportedTypeError(tk, SUPPORTED_IO_BIGQUERY_TYPEKINDS) + return BigQueryType.to_ibis(tk) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index f54c26fa566..dfffbe65ac1 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -46,12 +46,12 @@ from ibis.backends.bigquery.compiler import compiles from ibis.backends.bigquery.datatypes import BigQueryType from ibis.expr.datatypes.core import DataType as IbisDataType -from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type import ibis.expr.operations as ops import ibis.expr.rules as rlz from bigframes import clients import bigframes.constants as constants +import bigframes.dtypes logger = logging.getLogger(__name__) @@ -59,20 +59,6 @@ # https://docs.python.org/3/library/pickle.html#data-stream-format _pickle_protocol_version = 4 -# Input and output types supported by BigQuery DataFrames remote functions. -# TODO(shobs): Extend the support to all types supported by BQ remote functions -# https://cloud.google.com/bigquery/docs/remote-functions#limitations -SUPPORTED_IO_PYTHON_TYPES = {bool, float, int, str} -SUPPORTED_IO_BIGQUERY_TYPEKINDS = { - "BOOLEAN", - "BOOL", - "FLOAT", - "FLOAT64", - "INT64", - "INTEGER", - "STRING", -} - def get_remote_function_locations(bq_location): """Get BQ location and cloud functions region given a BQ client.""" @@ -558,24 +544,6 @@ def f(*args, **kwargs): return f -class UnsupportedTypeError(ValueError): - def __init__(self, type_, supported_types): - self.type = type_ - self.supported_types = supported_types - - -def ibis_type_from_python_type(t: type) -> IbisDataType: - if t not in SUPPORTED_IO_PYTHON_TYPES: - raise UnsupportedTypeError(t, SUPPORTED_IO_PYTHON_TYPES) - return python_type_to_bigquery_type(t) - - -def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> IbisDataType: - if tk not in SUPPORTED_IO_BIGQUERY_TYPEKINDS: - raise UnsupportedTypeError(tk, SUPPORTED_IO_BIGQUERY_TYPEKINDS) - return BigQueryType.to_ibis(tk) - - def ibis_signature_from_python_signature( signature: inspect.Signature, input_types: Sequence[type], @@ -583,8 +551,10 @@ def ibis_signature_from_python_signature( ) -> IbisSignature: return IbisSignature( parameter_names=list(signature.parameters.keys()), - input_types=[ibis_type_from_python_type(t) for t in input_types], - output_type=ibis_type_from_python_type(output_type), + input_types=[ + bigframes.dtypes.ibis_type_from_python_type(t) for t in input_types + ], + output_type=bigframes.dtypes.ibis_type_from_python_type(output_type), ) @@ -599,10 +569,14 @@ def ibis_signature_from_routine(routine: bigquery.Routine) -> IbisSignature: return IbisSignature( parameter_names=[arg.name for arg in routine.arguments], input_types=[ - ibis_type_from_type_kind(arg.data_type.type_kind) if arg.data_type else None + bigframes.dtypes.ibis_type_from_type_kind(arg.data_type.type_kind) + if arg.data_type + else None for arg in routine.arguments ], - output_type=ibis_type_from_type_kind(routine.return_type.type_kind), + output_type=bigframes.dtypes.ibis_type_from_type_kind( + routine.return_type.type_kind + ), ) @@ -908,7 +882,7 @@ def read_gbq_function( raise ValueError( "Function return type must be specified. {constants.FEEDBACK_LINK}" ) - except UnsupportedTypeError as e: + except bigframes.dtypes.UnsupportedTypeError as e: raise ValueError( f"Type {e.type} not supported, supported types are {e.supported_types}. " f"{constants.FEEDBACK_LINK}" diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 9737df94f9e..b40f42a3e80 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -20,6 +20,7 @@ import numpy as np import bigframes.dtypes as dtypes +import bigframes.operations.type as op_typing if typing.TYPE_CHECKING: # Avoids circular dependency @@ -36,6 +37,9 @@ def arguments(self) -> int: """The number of column argument the operation takes""" raise NotImplementedError("RowOp abstract base class has no implementation") + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + raise NotImplementedError("Abstract typing rule has no output type") + # These classes can be used to create simple ops that don't take local parameters # All is needed is a unique name, and to register an implementation in ibis_mappings.py @@ -49,6 +53,9 @@ def name(self) -> str: def arguments(self) -> int: return 1 + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + raise NotImplementedError("Abstract operation has no output type") + def as_expr( self, input_id: typing.Union[str, bigframes.core.expression.Expression] = "arg" ) -> bigframes.core.expression.Expression: @@ -69,6 +76,9 @@ def name(self) -> str: def arguments(self) -> int: return 2 + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + raise NotImplementedError("Abstract operation has no output type") + def as_expr( self, left_input: typing.Union[str, bigframes.core.expression.Expression] = "arg1", @@ -95,6 +105,9 @@ def name(self) -> str: def arguments(self) -> int: return 3 + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + raise NotImplementedError("Abstract operation has no output type") + def as_expr( self, input1: typing.Union[str, bigframes.core.expression.Expression] = "arg1", @@ -126,28 +139,34 @@ def _convert_expr_input( # Operation Factories -def create_unary_op(name: str) -> UnaryOp: +def create_unary_op( + name: str, type_rule: op_typing.OpTypeRule = op_typing.INPUT_TYPE +) -> UnaryOp: return dataclasses.make_dataclass( name, - [("name", typing.ClassVar[str], name)], # type: ignore + [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_rule.as_method)], # type: ignore bases=(UnaryOp,), frozen=True, )() -def create_binary_op(name: str) -> BinaryOp: +def create_binary_op( + name: str, type_rule: op_typing.OpTypeRule = op_typing.Supertype() +) -> BinaryOp: return dataclasses.make_dataclass( name, - [("name", typing.ClassVar[str], name)], # type: ignore + [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_rule.as_method)], # type: ignore bases=(BinaryOp,), frozen=True, )() -def create_ternary_op(name: str) -> TernaryOp: +def create_ternary_op( + name: str, type_rule: op_typing.OpTypeRule = op_typing.Supertype() +) -> TernaryOp: return dataclasses.make_dataclass( name, - [("name", typing.ClassVar[str], name)], # type: ignore + [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_rule.as_method)], # type: ignore bases=(TernaryOp,), frozen=True, )() @@ -155,57 +174,57 @@ def create_ternary_op(name: str) -> TernaryOp: # Unary Ops ## Generic Ops -invert_op = create_unary_op(name="invert") -isnull_op = create_unary_op(name="isnull") -notnull_op = create_unary_op(name="notnull") -hash_op = create_unary_op(name="hash") +invert_op = create_unary_op(name="invert", type_rule=op_typing.INPUT_TYPE) +isnull_op = create_unary_op(name="isnull", type_rule=op_typing.PREDICATE) +notnull_op = create_unary_op(name="notnull", type_rule=op_typing.PREDICATE) +hash_op = create_unary_op(name="hash", type_rule=op_typing.INTEGER) ## String Ops -len_op = create_unary_op(name="len") -reverse_op = create_unary_op(name="reverse") -lower_op = create_unary_op(name="lower") -upper_op = create_unary_op(name="upper") -strip_op = create_unary_op(name="strip") -isalnum_op = create_unary_op(name="isalnum") -isalpha_op = create_unary_op(name="isalpha") -isdecimal_op = create_unary_op(name="isdecimal") -isdigit_op = create_unary_op(name="isdigit") -isnumeric_op = create_unary_op(name="isnumeric") -isspace_op = create_unary_op(name="isspace") -islower_op = create_unary_op(name="islower") -isupper_op = create_unary_op(name="isupper") -rstrip_op = create_unary_op(name="rstrip") -lstrip_op = create_unary_op(name="lstrip") -capitalize_op = create_unary_op(name="capitalize") +len_op = create_unary_op(name="len", type_rule=op_typing.INTEGER) +reverse_op = create_unary_op(name="reverse", type_rule=op_typing.STRING) +lower_op = create_unary_op(name="lower", type_rule=op_typing.STRING) +upper_op = create_unary_op(name="upper", type_rule=op_typing.STRING) +strip_op = create_unary_op(name="strip", type_rule=op_typing.STRING) +isalnum_op = create_unary_op(name="isalnum", type_rule=op_typing.PREDICATE) +isalpha_op = create_unary_op(name="isalpha", type_rule=op_typing.PREDICATE) +isdecimal_op = create_unary_op(name="isdecimal", type_rule=op_typing.PREDICATE) +isdigit_op = create_unary_op(name="isdigit", type_rule=op_typing.PREDICATE) +isnumeric_op = create_unary_op(name="isnumeric", type_rule=op_typing.PREDICATE) +isspace_op = create_unary_op(name="isspace", type_rule=op_typing.PREDICATE) +islower_op = create_unary_op(name="islower", type_rule=op_typing.PREDICATE) +isupper_op = create_unary_op(name="isupper", type_rule=op_typing.PREDICATE) +rstrip_op = create_unary_op(name="rstrip", type_rule=op_typing.STRING) +lstrip_op = create_unary_op(name="lstrip", type_rule=op_typing.STRING) +capitalize_op = create_unary_op(name="capitalize", type_rule=op_typing.STRING) ## DateTime Ops -day_op = create_unary_op(name="day") -dayofweek_op = create_unary_op(name="dayofweek") +day_op = create_unary_op(name="day", type_rule=op_typing.INTEGER) +dayofweek_op = create_unary_op(name="dayofweek", type_rule=op_typing.INTEGER) date_op = create_unary_op(name="date") -hour_op = create_unary_op(name="hour") -minute_op = create_unary_op(name="minute") -month_op = create_unary_op(name="month") -quarter_op = create_unary_op(name="quarter") -second_op = create_unary_op(name="second") -time_op = create_unary_op(name="time") -year_op = create_unary_op(name="year") +hour_op = create_unary_op(name="hour", type_rule=op_typing.INTEGER) +minute_op = create_unary_op(name="minute", type_rule=op_typing.INTEGER) +month_op = create_unary_op(name="month", type_rule=op_typing.INTEGER) +quarter_op = create_unary_op(name="quarter", type_rule=op_typing.INTEGER) +second_op = create_unary_op(name="second", type_rule=op_typing.INTEGER) +time_op = create_unary_op(name="time", type_rule=op_typing.INTEGER) +year_op = create_unary_op(name="year", type_rule=op_typing.INTEGER) ## Trigonometry Ops -sin_op = create_unary_op(name="sin") -cos_op = create_unary_op(name="cos") -tan_op = create_unary_op(name="tan") -arcsin_op = create_unary_op(name="arcsin") -arccos_op = create_unary_op(name="arccos") -arctan_op = create_unary_op(name="arctan") -sinh_op = create_unary_op(name="sinh") -cosh_op = create_unary_op(name="cosh") -tanh_op = create_unary_op(name="tanh") -arcsinh_op = create_unary_op(name="arcsinh") -arccosh_op = create_unary_op(name="arccosh") -arctanh_op = create_unary_op(name="arctanh") +sin_op = create_unary_op(name="sin", type_rule=op_typing.REAL_NUMERIC) +cos_op = create_unary_op(name="cos", type_rule=op_typing.REAL_NUMERIC) +tan_op = create_unary_op(name="tan", type_rule=op_typing.REAL_NUMERIC) +arcsin_op = create_unary_op(name="arcsin", type_rule=op_typing.REAL_NUMERIC) +arccos_op = create_unary_op(name="arccos", type_rule=op_typing.REAL_NUMERIC) +arctan_op = create_unary_op(name="arctan", type_rule=op_typing.REAL_NUMERIC) +sinh_op = create_unary_op(name="sinh", type_rule=op_typing.REAL_NUMERIC) +cosh_op = create_unary_op(name="cosh", type_rule=op_typing.REAL_NUMERIC) +tanh_op = create_unary_op(name="tanh", type_rule=op_typing.REAL_NUMERIC) +arcsinh_op = create_unary_op(name="arcsinh", type_rule=op_typing.REAL_NUMERIC) +arccosh_op = create_unary_op(name="arccosh", type_rule=op_typing.REAL_NUMERIC) +arctanh_op = create_unary_op(name="arctanh", type_rule=op_typing.REAL_NUMERIC) ## Numeric Ops -abs_op = create_unary_op(name="abs") -exp_op = create_unary_op(name="exp") -ln_op = create_unary_op(name="log") -log10_op = create_unary_op(name="log10") -sqrt_op = create_unary_op(name="sqrt") +abs_op = create_unary_op(name="abs", type_rule=op_typing.INPUT_TYPE) +exp_op = create_unary_op(name="exp", type_rule=op_typing.REAL_NUMERIC) +ln_op = create_unary_op(name="log", type_rule=op_typing.REAL_NUMERIC) +log10_op = create_unary_op(name="log10", type_rule=op_typing.REAL_NUMERIC) +sqrt_op = create_unary_op(name="sqrt", type_rule=op_typing.REAL_NUMERIC) # Parameterized unary ops @@ -214,18 +233,27 @@ class StrContainsOp(UnaryOp): name: typing.ClassVar[str] = "str_contains" pat: str + def output_type(self, *input_types): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class StrContainsRegexOp(UnaryOp): name: typing.ClassVar[str] = "str_contains_regex" pat: str + def output_type(self, *input_types): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class StrGetOp(UnaryOp): name: typing.ClassVar[str] = "str_get" i: int + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + @dataclasses.dataclass(frozen=True) class StrPadOp(UnaryOp): @@ -234,6 +262,9 @@ class StrPadOp(UnaryOp): fillchar: str side: typing.Literal["both", "left", "right"] + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + @dataclasses.dataclass(frozen=True) class ReplaceStrOp(UnaryOp): @@ -241,6 +272,9 @@ class ReplaceStrOp(UnaryOp): pat: str repl: str + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + @dataclasses.dataclass(frozen=True) class RegexReplaceStrOp(UnaryOp): @@ -248,24 +282,36 @@ class RegexReplaceStrOp(UnaryOp): pat: str repl: str + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + @dataclasses.dataclass(frozen=True) class StartsWithOp(UnaryOp): name: typing.ClassVar[str] = "str_startswith" pat: typing.Sequence[str] + def output_type(self, *input_types): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class EndsWithOp(UnaryOp): name: typing.ClassVar[str] = "str_endswith" pat: typing.Sequence[str] + def output_type(self, *input_types): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class ZfillOp(UnaryOp): name: typing.ClassVar[str] = "str_zfill" width: int + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + @dataclasses.dataclass(frozen=True) class StrFindOp(UnaryOp): @@ -274,6 +320,9 @@ class StrFindOp(UnaryOp): start: typing.Optional[int] end: typing.Optional[int] + def output_type(self, *input_types): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class StrExtractOp(UnaryOp): @@ -281,6 +330,9 @@ class StrExtractOp(UnaryOp): pat: str n: int = 1 + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + @dataclasses.dataclass(frozen=True) class StrSliceOp(UnaryOp): @@ -288,12 +340,18 @@ class StrSliceOp(UnaryOp): start: typing.Optional[int] end: typing.Optional[int] + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + @dataclasses.dataclass(frozen=True) class StrRepeatOp(UnaryOp): name: typing.ClassVar[str] = "str_repeat" repeats: int + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + # Other parameterized unary operations @dataclasses.dataclass(frozen=True) @@ -305,8 +363,14 @@ class StructFieldOp(UnaryOp): @dataclasses.dataclass(frozen=True) class AsTypeOp(UnaryOp): name: typing.ClassVar[str] = "astype" + # TODO: Convert strings to dtype earlier to_type: dtypes.DtypeString | dtypes.Dtype + def output_type(self, *input_types): + if isinstance(self.to_type, str): + return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[self.to_type] + return self.to_type + @dataclasses.dataclass(frozen=True) class IsInOp(UnaryOp): @@ -314,6 +378,9 @@ class IsInOp(UnaryOp): values: typing.Tuple match_nulls: bool = True + def output_type(self, *input_types): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class RemoteFunctionOp(UnaryOp): @@ -321,12 +388,21 @@ class RemoteFunctionOp(UnaryOp): func: typing.Callable apply_on_null: bool + def output_type(self, *input_types): + python_type = self.func.__signature__.output_type + ibis_type = dtypes.ibis_type_from_python_type(python_type) + dtype = dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) + return dtype + @dataclasses.dataclass(frozen=True) class MapOp(UnaryOp): name = "map_values" mappings: typing.Tuple[typing.Tuple[typing.Hashable, typing.Hashable], ...] + def output_type(self, *input_types): + return input_types[0] + # Binary Ops fillna_op = create_binary_op(name="fillna") @@ -334,34 +410,48 @@ class MapOp(UnaryOp): clipupper_op = create_binary_op(name="clip_upper") coalesce_op = create_binary_op(name="coalesce") ## Math Ops -add_op = create_binary_op(name="add") -sub_op = create_binary_op(name="sub") -mul_op = create_binary_op(name="mul") -div_op = create_binary_op(name="div") -floordiv_op = create_binary_op(name="floordiv") -pow_op = create_binary_op(name="pow") -mod_op = create_binary_op(name="mod") -round_op = create_binary_op(name="round") -unsafe_pow_op = create_binary_op(name="unsafe_pow_op") +add_op = create_binary_op(name="add", type_rule=op_typing.NUMERIC) +sub_op = create_binary_op(name="sub", type_rule=op_typing.NUMERIC) +mul_op = create_binary_op(name="mul", type_rule=op_typing.NUMERIC) +div_op = create_binary_op(name="div", type_rule=op_typing.REAL_NUMERIC) +floordiv_op = create_binary_op(name="floordiv", type_rule=op_typing.REAL_NUMERIC) +pow_op = create_binary_op(name="pow", type_rule=op_typing.REAL_NUMERIC) +mod_op = create_binary_op(name="mod", type_rule=op_typing.NUMERIC) +round_op = create_binary_op(name="round", type_rule=op_typing.REAL_NUMERIC) +unsafe_pow_op = create_binary_op(name="unsafe_pow_op", type_rule=op_typing.REAL_NUMERIC) # Logical Ops -and_op = create_binary_op(name="and") -or_op = create_binary_op(name="or") +and_op = create_binary_op(name="and", type_rule=op_typing.PREDICATE) +or_op = create_binary_op(name="or", type_rule=op_typing.PREDICATE) ## Comparison Ops -eq_op = create_binary_op(name="eq") -eq_null_match_op = create_binary_op(name="eq_nulls_match") -ne_op = create_binary_op(name="ne") -lt_op = create_binary_op(name="lt") -gt_op = create_binary_op(name="gt") -le_op = create_binary_op(name="le") -ge_op = create_binary_op(name="ge") +eq_op = create_binary_op(name="eq", type_rule=op_typing.PREDICATE) +eq_null_match_op = create_binary_op( + name="eq_nulls_match", type_rule=op_typing.PREDICATE +) +ne_op = create_binary_op(name="ne", type_rule=op_typing.PREDICATE) +lt_op = create_binary_op(name="lt", type_rule=op_typing.PREDICATE) +gt_op = create_binary_op(name="gt", type_rule=op_typing.PREDICATE) +le_op = create_binary_op(name="le", type_rule=op_typing.PREDICATE) +ge_op = create_binary_op(name="ge", type_rule=op_typing.PREDICATE) ## String Ops -strconcat_op = create_binary_op(name="strconcat") +strconcat_op = create_binary_op(name="strconcat", type_rule=op_typing.STRING) + # Ternary Ops -where_op = create_ternary_op(name="where") -clip_op = create_ternary_op(name="clip") +@dataclasses.dataclass(frozen=True) +class WhereOp(TernaryOp): + name: typing.ClassVar[str] = "where" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + # Second input is boolean and doesn't affect output type + return dtypes.lcd_etype(input_types[0], input_types[2]) + + +where_op = WhereOp() + + +clip_op = create_ternary_op(name="clip", type_rule=op_typing.Supertype()) # Just parameterless unary ops for now diff --git a/bigframes/operations/type.py b/bigframes/operations/type.py new file mode 100644 index 00000000000..3c16f0cbe9c --- /dev/null +++ b/bigframes/operations/type.py @@ -0,0 +1,80 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import functools + +import pandas as pd + +import bigframes.dtypes +from bigframes.dtypes import ExpressionType + +# TODO: Apply input type constraints to help pre-empt invalid expression construction + + +@dataclasses.dataclass +class OpTypeRule: + def output_type(self, *input_types: ExpressionType) -> ExpressionType: + raise NotImplementedError("Abstract typing rule has no output type") + + @property + def as_method(self): + def meth(_, *input_types: ExpressionType) -> ExpressionType: + return self.output_type(*input_types) + + return meth + + +@dataclasses.dataclass +class InputType(OpTypeRule): + def output_type(self, *input_types: ExpressionType) -> ExpressionType: + assert len(input_types) == 1 + return input_types[0] + + +@dataclasses.dataclass +class RealNumeric(OpTypeRule): + def output_type(self, *input_types: ExpressionType) -> ExpressionType: + all_ints = all(pd.api.types.is_integer(input) for input in input_types) + if all_ints: + return bigframes.dtypes.FLOAT_DTYPE + else: + return functools.reduce( + lambda t1, t2: bigframes.dtypes.lcd_etype(t1, t2), input_types + ) + + +@dataclasses.dataclass +class Supertype(OpTypeRule): + def output_type(self, *input_types: ExpressionType) -> ExpressionType: + return functools.reduce( + lambda t1, t2: bigframes.dtypes.lcd_etype(t1, t2), input_types + ) + + +@dataclasses.dataclass +class Fixed(OpTypeRule): + out_type: ExpressionType + + def output_type(self, *input_types: ExpressionType) -> ExpressionType: + return self.out_type + + +# Common type rules +NUMERIC = Supertype() +REAL_NUMERIC = RealNumeric() +PREDICATE = Fixed(bigframes.dtypes.BOOL_DTYPE) +INTEGER = Fixed(bigframes.dtypes.INT_DTYPE) +STRING = Fixed(bigframes.dtypes.STRING_DTYPE) +INPUT_TYPE = InputType() diff --git a/tests/unit/core/test_expression.py b/tests/unit/core/test_expression.py new file mode 100644 index 00000000000..f46c47a5827 --- /dev/null +++ b/tests/unit/core/test_expression.py @@ -0,0 +1,49 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.core.expression as ex +import bigframes.dtypes as dtypes +import bigframes.operations as ops + + +def test_expression_dtype_simple(): + expression = ops.add_op.as_expr("a", "b") + result = expression.output_type({"a": dtypes.INT_DTYPE, "b": dtypes.INT_DTYPE}) + assert result == dtypes.INT_DTYPE + + +def test_expression_dtype_nested(): + expression = ops.add_op.as_expr( + "a", ops.abs_op.as_expr(ops.sub_op.as_expr("b", ex.const(3.14))) + ) + + result = expression.output_type({"a": dtypes.INT_DTYPE, "b": dtypes.INT_DTYPE}) + + assert result == dtypes.FLOAT_DTYPE + + +def test_expression_dtype_where(): + expression = ops.where_op.as_expr(ex.const(3), ex.const(True), ex.const(None)) + + result = expression.output_type({}) + + assert result == dtypes.INT_DTYPE + + +def test_expression_dtype_astype(): + expression = ops.AsTypeOp("Int64").as_expr(ex.const(3.14159)) + + result = expression.output_type({}) + + assert result == dtypes.INT_DTYPE diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index 047ad5638dd..dabbf11c6c7 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -241,9 +241,7 @@ def test_literal_to_ibis_scalar_throws_on_incompatible_literal(): def test_remote_function_io_types_are_supported_bigframes_types(): from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type - from bigframes.functions.remote_function import ( - SUPPORTED_IO_PYTHON_TYPES as rf_supported_io_types, - ) + from bigframes.dtypes import SUPPORTED_IO_PYTHON_TYPES as rf_supported_io_types for python_type in rf_supported_io_types: ibis_type = python_type_to_bigquery_type(python_type) diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py index ed24ada6c70..392872a7bea 100644 --- a/tests/unit/test_remote_function.py +++ b/tests/unit/test_remote_function.py @@ -15,14 +15,17 @@ from ibis.backends.bigquery import datatypes as bq_types from ibis.expr import datatypes as ibis_types -from bigframes.functions import remote_function as rf +import bigframes.dtypes def test_supported_types_correspond(): # The same types should be representable by the supported Python and BigQuery types. - ibis_types_from_python = {ibis_types.dtype(t) for t in rf.SUPPORTED_IO_PYTHON_TYPES} + ibis_types_from_python = { + ibis_types.dtype(t) for t in bigframes.dtypes.SUPPORTED_IO_PYTHON_TYPES + } ibis_types_from_bigquery = { - bq_types.BigQueryType.to_ibis(tk) for tk in rf.SUPPORTED_IO_BIGQUERY_TYPEKINDS + bq_types.BigQueryType.to_ibis(tk) + for tk in bigframes.dtypes.SUPPORTED_IO_BIGQUERY_TYPEKINDS } assert ibis_types_from_python == ibis_types_from_bigquery From 9c34d834e83ca5514bee723ebb9a7ad1ad50e88d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 25 Jan 2024 14:08:03 -0800 Subject: [PATCH 284/921] feat: add `DataFrame.peek()` as an efficient alternative to `head()` results preview (#318) * feat: add efficient peek dataframe preview * add force parameter to peek to cache full dataframe * add df.peek docstring * set peek to default force=False * update peek docstring and error type --------- Co-authored-by: Tim Swast --- bigframes/core/__init__.py | 4 +- bigframes/core/blocks.py | 10 +++++ bigframes/core/compile/__init__.py | 6 +-- bigframes/core/compile/compiled.py | 7 ++++ bigframes/core/compile/compiler.py | 38 ++++++++++--------- bigframes/core/nodes.py | 55 ++++++++++++++++++++++++++++ bigframes/dataframe.py | 31 ++++++++++++++++ bigframes/session/__init__.py | 15 +++++++- tests/system/small/test_dataframe.py | 31 ++++++++++++++++ 9 files changed, 173 insertions(+), 24 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 8c08d073d7e..8c399e34ab4 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -106,10 +106,10 @@ def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: return self._compile_ordered().get_column_type(key) def _compile_ordered(self) -> compiling.OrderedIR: - return compiling.compile_ordered(self.node) + return compiling.compile_ordered_ir(self.node) def _compile_unordered(self) -> compiling.UnorderedIR: - return compiling.compile_unordered(self.node) + return compiling.compile_unordered_ir(self.node) def row_count(self) -> ArrayValue: """Get number of rows in ArrayValue as a single-entry ArrayValue.""" diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 3ec0419c6dc..aa9649f2726 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -432,8 +432,18 @@ def to_pandas( downsampling=sampling, ordered=ordered ) ) + df.set_axis(self.column_labels, axis=1, copy=False) return df, query_job + def try_peek(self, n: int = 20) -> typing.Optional[pd.DataFrame]: + if self.expr.node.peekable: + iterator, _ = self.session._peek(self.expr, n) + df = self._to_dataframe(iterator) + self._copy_index_to_pandas(df) + return df + else: + return None + def to_pandas_batches(self): """Download results one message at a time.""" dtypes = dict(zip(self.index_columns, self.index_dtypes)) diff --git a/bigframes/core/compile/__init__.py b/bigframes/core/compile/__init__.py index 761fd9a4650..c3e2bd832a0 100644 --- a/bigframes/core/compile/__init__.py +++ b/bigframes/core/compile/__init__.py @@ -13,11 +13,11 @@ # limitations under the License. from bigframes.core.compile.compiled import OrderedIR, UnorderedIR -from bigframes.core.compile.compiler import compile_ordered, compile_unordered +from bigframes.core.compile.compiler import compile_ordered_ir, compile_unordered_ir __all__ = [ - "compile_ordered", - "compile_unordered", + "compile_ordered_ir", + "compile_unordered_ir", "OrderedIR", "UnorderedIR", ] diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index eaaf692a170..9d213c6bed3 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -208,6 +208,13 @@ def builder(self): predicates=self._predicates, ) + def peek_sql(self, n: int): + # Peek currently implemented as top level LIMIT op. + # Execution engine handles limit pushdown. + # In future, may push down limit/filters in compilation. + sql = ibis_bigquery.Backend().compile(self._to_ibis_expr().limit(n)) + return typing.cast(str, sql) + def to_sql( self, offset_column: typing.Optional[str] = None, diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index c948f0bdef2..2ec00f70738 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -29,14 +29,18 @@ import bigframes.session -def compile_ordered(node: nodes.BigFrameNode) -> compiled.OrderedIR: +def compile_ordered_ir(node: nodes.BigFrameNode) -> compiled.OrderedIR: return typing.cast(compiled.OrderedIR, compile_node(node, True)) -def compile_unordered(node: nodes.BigFrameNode) -> compiled.UnorderedIR: +def compile_unordered_ir(node: nodes.BigFrameNode) -> compiled.UnorderedIR: return typing.cast(compiled.UnorderedIR, compile_node(node, False)) +def compile_peak_sql(node: nodes.BigFrameNode, n_rows: int) -> typing.Optional[str]: + return compile_unordered_ir(node).peek_sql(n_rows) + + @functools.cache def compile_node( node: nodes.BigFrameNode, ordered: bool = True @@ -56,8 +60,8 @@ def _compile_node( @_compile_node.register def compile_join(node: nodes.JoinNode, ordered: bool = True): if ordered: - left_ordered = compile_ordered(node.left_child) - right_ordered = compile_ordered(node.right_child) + left_ordered = compile_ordered_ir(node.left_child) + right_ordered = compile_ordered_ir(node.right_child) return bigframes.core.compile.single_column.join_by_column_ordered( left=left_ordered, right=right_ordered, @@ -65,8 +69,8 @@ def compile_join(node: nodes.JoinNode, ordered: bool = True): allow_row_identity_join=node.allow_row_identity_join, ) else: - left_unordered = compile_unordered(node.left_child) - right_unordered = compile_unordered(node.right_child) + left_unordered = compile_unordered_ir(node.left_child) + right_unordered = compile_unordered_ir(node.right_child) return bigframes.core.compile.single_column.join_by_column_unordered( left=left_unordered, right=right_unordered, @@ -103,7 +107,7 @@ def compile_readgbq(node: nodes.ReadGbqNode, ordered: bool = True): @_compile_node.register def compile_promote_offsets(node: nodes.PromoteOffsetsNode, ordered: bool = True): - result = compile_ordered(node.child).promote_offsets(node.col_id) + result = compile_ordered_ir(node.child).promote_offsets(node.col_id) return result if ordered else result.to_unordered() @@ -115,17 +119,17 @@ def compile_filter(node: nodes.FilterNode, ordered: bool = True): @_compile_node.register def compile_orderby(node: nodes.OrderByNode, ordered: bool = True): if ordered: - return compile_ordered(node.child).order_by(node.by) + return compile_ordered_ir(node.child).order_by(node.by) else: - return compile_unordered(node.child) + return compile_unordered_ir(node.child) @_compile_node.register def compile_reversed(node: nodes.ReversedNode, ordered: bool = True): if ordered: - return compile_ordered(node.child).reversed() + return compile_ordered_ir(node.child).reversed() else: - return compile_unordered(node.child) + return compile_unordered_ir(node.child) @_compile_node.register @@ -137,22 +141,22 @@ def compile_projection(node: nodes.ProjectionNode, ordered: bool = True): @_compile_node.register def compile_concat(node: nodes.ConcatNode, ordered: bool = True): if ordered: - compiled_ordered = [compile_ordered(node) for node in node.children] + compiled_ordered = [compile_ordered_ir(node) for node in node.children] return concat_impl.concat_ordered(compiled_ordered) else: - compiled_unordered = [compile_unordered(node) for node in node.children] + compiled_unordered = [compile_unordered_ir(node) for node in node.children] return concat_impl.concat_unordered(compiled_unordered) @_compile_node.register def compile_rowcount(node: nodes.RowCountNode, ordered: bool = True): - result = compile_unordered(node.child).row_count() + result = compile_unordered_ir(node.child).row_count() return result if ordered else result.to_unordered() @_compile_node.register def compile_aggregate(node: nodes.AggregateNode, ordered: bool = True): - result = compile_unordered(node.child).aggregate( + result = compile_unordered_ir(node.child).aggregate( node.aggregations, node.by_column_ids, node.dropna ) return result if ordered else result.to_unordered() @@ -160,13 +164,13 @@ def compile_aggregate(node: nodes.AggregateNode, ordered: bool = True): @_compile_node.register def compile_corr(node: nodes.CorrNode, ordered: bool = True): - result = compile_unordered(node.child).corr_aggregate(node.corr_aggregations) + result = compile_unordered_ir(node.child).corr_aggregate(node.corr_aggregations) return result if ordered else result.to_unordered() @_compile_node.register def compile_window(node: nodes.WindowOpNode, ordered: bool = True): - result = compile_ordered(node.child).project_window_op( + result = compile_ordered_ir(node.child).project_window_op( node.column_name, node.op, node.window_spec, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index bf261b62f4e..e1882c36848 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -16,6 +16,7 @@ from dataclasses import dataclass, field, fields import functools +import itertools import typing from typing import Tuple @@ -74,6 +75,18 @@ def session(self): def _node_hash(self): return hash(tuple(hash(getattr(self, field.name)) for field in fields(self))) + @property + def peekable(self) -> bool: + """Indicates whether the node can be sampled efficiently""" + return all(child.peekable for child in self.child_nodes) + + @property + def roots(self) -> typing.Set[BigFrameNode]: + roots = itertools.chain.from_iterable( + map(lambda child: child.roots, self.child_nodes) + ) + return set(roots) + @dataclass(frozen=True) class UnaryNode(BigFrameNode): @@ -98,6 +111,12 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + children_peekable = all(child.peekable for child in self.child_nodes) + single_root = len(self.roots) == 1 + return children_peekable and single_root + @dataclass(frozen=True) class ConcatNode(BigFrameNode): @@ -119,6 +138,14 @@ class ReadLocalNode(BigFrameNode): def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + return True + + @property + def roots(self) -> typing.Set[BigFrameNode]: + return {self} + # TODO: Refactor to take raw gbq object reference @dataclass(frozen=True) @@ -136,6 +163,14 @@ def session(self): def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + return True + + @property + def roots(self) -> typing.Set[BigFrameNode]: + return {self} + # Unary nodes @dataclass(frozen=True) @@ -145,6 +180,10 @@ class PromoteOffsetsNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + return False + @dataclass(frozen=True) class FilterNode(UnaryNode): @@ -194,6 +233,10 @@ class AggregateNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + return False + # TODO: Unify into aggregate @dataclass(frozen=True) @@ -203,6 +246,10 @@ class CorrNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + return False + @dataclass(frozen=True) class WindowOpNode(UnaryNode): @@ -216,6 +263,10 @@ class WindowOpNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + return False + @dataclass(frozen=True) class ReprojectOpNode(UnaryNode): @@ -239,6 +290,10 @@ class UnpivotNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def peekable(self) -> bool: + return False + @dataclass(frozen=True) class RandomSampleNode(UnaryNode): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1288117395b..9989831e1b2 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1066,6 +1066,37 @@ def head(self, n: int = 5) -> DataFrame: def tail(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[-n:]) + def peek(self, n: int = 5, *, force: bool = False) -> pandas.DataFrame: + """ + Preview n arbitrary rows from the dataframe. No guarantees about row selection or ordering. + DataFrame.peek(force=False) will always be very fast, but will not succeed if data requires + full data scanning. Using force=True will always succeed, but may be perform expensive + computations. + + Args: + n (int, default 5): + The number of rows to select from the dataframe. Which N rows are returned is non-deterministic. + force (bool, default False): + If the data cannot be peeked efficiently, the dataframe will instead be fully materialized as part + of the operation if force=True. If force=False, the operation will throw a ValueError. + Returns: + pandas.DataFrame: A pandas DataFrame with n rows. + + Raises: + ValueError: If force=False and data cannot be efficiently peeked. + """ + maybe_result = self._block.try_peek(n) + if maybe_result is None: + if force: + self._cached() + maybe_result = self._block.try_peek(n) + assert maybe_result is not None + else: + raise ValueError( + "Cannot peek efficiently when data has aggregates, joins or window functions applied. Use force=True to fully compute dataframe." + ) + return maybe_result.set_axis(self._block.column_labels, axis=1, copy=False) + def nlargest( self, n: int, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 9e1e6b560ab..da39ab43ce3 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1508,6 +1508,17 @@ def _execute( job_config=job_config, ) + def _peek( + self, array_value: core.ArrayValue, n_rows: int + ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """A 'peek' efficiently accesses a small number of rows in the dataframe.""" + if not array_value.node.peekable: + raise NotImplementedError("cannot efficient peek this dataframe") + sql = self._compile_unordered(array_value).peek_sql(n_rows) + return self._start_query( + sql=sql, + ) + def _to_sql( self, array_value: core.ArrayValue, @@ -1528,12 +1539,12 @@ def _to_sql( def _compile_ordered( self, array_value: core.ArrayValue ) -> bigframes.core.compile.OrderedIR: - return bigframes.core.compile.compile_ordered(array_value.node) + return bigframes.core.compile.compile_ordered_ir(array_value.node) def _compile_unordered( self, array_value: core.ArrayValue ) -> bigframes.core.compile.UnorderedIR: - return bigframes.core.compile.compile_unordered(array_value.node) + return bigframes.core.compile.compile_unordered_ir(array_value.node) def _get_table_size(self, destination_table): table = self.bqclient.get_table(destination_table) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 9557475b46d..4ae31fa4a0a 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -412,6 +412,37 @@ def test_rename(scalars_dfs): ) +def test_df_peek(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + peek_result = scalars_df.peek(n=3) + pd.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) + assert len(peek_result) == 3 + + +def test_df_peek_filtered(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + peek_result = scalars_df[scalars_df.int64_col != 0].peek(n=3) + pd.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) + assert len(peek_result) == 3 + + +def test_df_peek_exception(scalars_dfs): + scalars_df, _ = scalars_dfs + + with pytest.raises(ValueError): + # Window ops aren't compatible with efficient peeking + scalars_df[["int64_col", "int64_too"]].cumsum().peek(n=3, force=False) + + +def test_df_peek_force(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + peek_result = scalars_df[["int64_col", "int64_too"]].cumsum().peek(n=3, force=True) + pd.testing.assert_index_equal( + scalars_pandas_df[["int64_col", "int64_too"]].columns, peek_result.columns + ) + assert len(peek_result) == 3 + + def test_repr_w_all_rows(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs From 0794788a2d232d795d803cd0c5b3f7d51c562cf1 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 26 Jan 2024 01:59:16 +0000 Subject: [PATCH 285/921] feat: Improve error message for drive based BQ table reads (#344) * feat: Improve error message for drive based BQ table reads * move exception handling deeper to apply to read_gbq* * add unit tests --- bigframes/session/__init__.py | 21 +++++++++++++++++---- tests/unit/session/test_session.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index da39ab43ce3..bd813c8c6b3 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -597,9 +597,16 @@ def _get_snapshot_sql_and_primary_key( ).result() )[0][0] self._df_snapshot[table_ref] = snapshot_timestamp - table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table_ref, snapshot_timestamp) - ) + + try: + table_expression = self.ibis_client.sql( + bigframes_io.create_snapshot_sql(table_ref, snapshot_timestamp) + ) + except google.api_core.exceptions.Forbidden as ex: + if "Drive credentials" in ex.message: + ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." + raise + return table_expression, primary_keys def _read_gbq_table( @@ -1451,7 +1458,13 @@ def _start_query( job_config.labels = bigframes_io.create_job_configs_labels( job_configs_labels=job_config.labels, api_methods=api_methods ) - query_job = self.bqclient.query(sql, job_config=job_config) + + try: + query_job = self.bqclient.query(sql, job_config=job_config) + except google.api_core.exceptions.Forbidden as ex: + if "Drive credentials" in ex.message: + ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." + raise opts = bigframes.options.display if opts.progress_bar is not None and not query_job.configuration.dry_run: diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index d38a393f270..5fc8996993e 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -35,7 +35,7 @@ def test_read_gbq_missing_parts(missing_parts_table_id): "not_found_table_id", [("unknown.dataset.table"), ("project.unknown.table"), ("project.dataset.unknown")], ) -def test_read_gdb_not_found_tables(not_found_table_id): +def test_read_gbq_not_found_tables(not_found_table_id): bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" bqclient.get_table.side_effect = google.api_core.exceptions.NotFound( @@ -47,6 +47,34 @@ def test_read_gdb_not_found_tables(not_found_table_id): session.read_gbq(not_found_table_id) +@pytest.mark.parametrize( + ("api_name", "query_or_table"), + [ + ("read_gbq", "project.dataset.table"), + ("read_gbq_table", "project.dataset.table"), + ("read_gbq", "SELECT * FROM project.dataset.table"), + ("read_gbq_query", "SELECT * FROM project.dataset.table"), + ], + ids=[ + "read_gbq_on_table", + "read_gbq_table", + "read_gbq_on_query", + "read_gbq_query", + ], +) +def test_read_gbq_external_table_no_drive_access(api_name, query_or_table): + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + bqclient.get_table.side_effect = google.api_core.exceptions.Forbidden( + "Access Denied: BigQuery BigQuery: Permission denied while getting Drive credentials." + ) + session = resources.create_bigquery_session(bqclient=bqclient) + + api = getattr(session, api_name) + with pytest.raises(google.api_core.exceptions.Forbidden): + api(query_or_table) + + @mock.patch.dict(os.environ, {}, clear=True) def test_session_init_fails_with_no_project(): with pytest.raises( From 677f0146acf19def88fddbeb0527a078458948ae Mon Sep 17 00:00:00 2001 From: Shane Glass <23001651+shanecglass@users.noreply.github.com> Date: Fri, 26 Jan 2024 18:10:15 +0000 Subject: [PATCH 286/921] fix: chance default connection name in getting_started.ipnyb (#347) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [X ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [X ] Ensure the tests and linter pass - [ X] Code coverage does not decrease (if any source code was changed) - [X ] Appropriate docs were updated (if necessary) Fixes #346 The existing notebook references an incorrect default connection name. This PR corrects that so that users can more easily cleanup after they use the [getting started notebook](https://togithub.com/googleapis/python-bigquery-dataframes/blob/main/notebooks/getting_started/getting_started_bq_dataframes.ipynb) --- notebooks/getting_started/getting_started_bq_dataframes.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/getting_started/getting_started_bq_dataframes.ipynb b/notebooks/getting_started/getting_started_bq_dataframes.ipynb index 18be5e48fd1..a70e2f65ffe 100644 --- a/notebooks/getting_started/getting_started_bq_dataframes.ipynb +++ b/notebooks/getting_started/getting_started_bq_dataframes.ipynb @@ -925,7 +925,7 @@ "# # Delete the BigQuery Connection\n", "# from google.cloud import bigquery_connection_v1 as bq_connection\n", "# client = bq_connection.ConnectionServiceClient()\n", - "# CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/bigframes-rf-conn\"\n", + "# CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/bigframes-default-connection\"\n", "# client.delete_connection(name=CONNECTION_ID)\n", "# print(\"Deleted connection '{}'.\".format(CONNECTION_ID))" ] From e5d054e93a05f5c504e8db57b954c07d33e5f5b9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 26 Jan 2024 15:47:06 -0800 Subject: [PATCH 287/921] feat: Add Index constructor, repr, copy, get_level_values, to_series (#334) * feat: Add Index constructor, copy, get_level_values, to_series fix mypy error * fix constructor bug * fix error with index name mutation * refactor index to make mutation clearer * fix index bugs * give index custom repr --------- Co-authored-by: Huan Chen <142538604+Genesis929@users.noreply.github.com> --- bigframes/core/blocks.py | 6 +- bigframes/core/indexers.py | 2 +- bigframes/core/indexes/index.py | 183 ++++++++++++++---- bigframes/dataframe.py | 31 ++- bigframes/operations/base.py | 42 ++-- bigframes/series.py | 16 +- tests/system/small/test_index.py | 72 +++++++ tests/system/small/test_series.py | 34 ++++ .../bigframes_vendored/pandas/core/frame.py | 7 +- .../pandas/core/indexes/base.py | 51 +++++ .../bigframes_vendored/pandas/core/series.py | 7 +- 11 files changed, 375 insertions(+), 76 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index aa9649f2726..9e17dc2752e 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -287,7 +287,6 @@ def reset_index(self, drop: bool = True) -> Block: A new Block because dropping index columns can break references from Index classes that point to this block. """ - block = self new_index_col_id = guid.generate_guid() expr = self._expr.promote_offsets(new_index_col_id) if drop: @@ -295,7 +294,7 @@ def reset_index(self, drop: bool = True) -> Block: # ordering expression as reset_index shouldn't change the row # order. expr = expr.drop_columns(self.index_columns) - block = Block( + return Block( expr, index_columns=[new_index_col_id], column_labels=self.column_labels, @@ -321,13 +320,12 @@ def reset_index(self, drop: bool = True) -> Block: # See: https://pandas.pydata.org/docs/reference/api/pandas.Index.insert.html column_labels_modified = column_labels_modified.insert(level, label) - block = Block( + return Block( expr, index_columns=[new_index_col_id], column_labels=column_labels_modified, index_labels=[None], ) - return block def set_index( self, diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 6998d0e974d..0a47c3a78e8 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -294,7 +294,7 @@ def _loc_getitem_series_or_dataframe( keys_df = keys_df.set_index(temp_name, drop=True) return _perform_loc_list_join(series_or_dataframe, keys_df) elif isinstance(key, bigframes.core.indexes.Index): - block = key._data._get_block() + block = key._block block = block.select_columns(()) keys_df = bigframes.dataframe.DataFrame(block) return _perform_loc_list_join(series_or_dataframe, keys_df) diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 8b3613d82cf..78a4fc6f0bd 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -17,8 +17,9 @@ from __future__ import annotations import typing -from typing import Mapping, Sequence, Tuple, Union +from typing import Hashable, Mapping, Optional, Sequence, Tuple, Union +import google.cloud.bigquery as bigquery import numpy as np import pandas @@ -33,16 +34,60 @@ import bigframes.core.utils as utils import bigframes.dtypes import bigframes.dtypes as bf_dtypes +import bigframes.formatting_helpers as formatter import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import third_party.bigframes_vendored.pandas.core.indexes.base as vendored_pandas_index +if typing.TYPE_CHECKING: + import bigframes.dataframe + import bigframes.series + class Index(vendored_pandas_index.Index): __doc__ = vendored_pandas_index.Index.__doc__ - def __init__(self, data: blocks.BlockHolder): - self._data = data + def __init__( + self, + data=None, + dtype=None, + *, + name=None, + ): + import bigframes.dataframe as df + import bigframes.series as series + + if isinstance(data, blocks.Block): + block = data.select_columns([]) + elif isinstance(data, df.DataFrame): + raise ValueError("Cannot construct index from dataframe.") + elif isinstance(data, series.Series) or isinstance(data, Index): + if isinstance(data, series.Series): + block = data._block + block = block.set_index( + col_ids=[data._value_column], + ) + elif isinstance(data, Index): + block = data._block + index = Index(data=block) + name = data.name if name is None else name + if name is not None: + index.name = name + if dtype is not None: + index = index.astype(dtype) + block = index._block + else: + pd_index = pandas.Index(data=data, dtype=dtype, name=name) + pd_df = pandas.DataFrame(index=pd_index) + block = df.DataFrame(pd_df)._block + self._query_job = None + self._block: blocks.Block = block + + @classmethod + def from_frame( + cls, frame: Union[bigframes.series.Series, bigframes.dataframe.DataFrame] + ) -> Index: + return FrameIndex(frame) @property def name(self) -> blocks.Label: @@ -55,15 +100,16 @@ def name(self, value: blocks.Label): @property def names(self) -> typing.Sequence[blocks.Label]: """Returns the names of the Index.""" - return self._data._get_block()._index_labels + return self._block._index_labels @names.setter def names(self, values: typing.Sequence[blocks.Label]): - return self._data._set_block(self._block.with_index_labels(values)) + new_block = self._block.with_index_labels(values) + self._block = new_block @property def nlevels(self) -> int: - return len(self._data._get_block().index_columns) + return len(self._block.index_columns) @property def values(self) -> np.ndarray: @@ -75,7 +121,7 @@ def ndim(self) -> int: @property def shape(self) -> typing.Tuple[int]: - return (self._data._get_block().shape[0],) + return (self._block.shape[0],) @property def dtype(self): @@ -107,9 +153,7 @@ def is_monotonic_increasing(self) -> bool: """ return typing.cast( bool, - self._data._get_block().is_monotonic_increasing( - self._data._get_block().index_columns - ), + self._block.is_monotonic_increasing(self._block.index_columns), ) @property @@ -122,9 +166,7 @@ def is_monotonic_decreasing(self) -> bool: """ return typing.cast( bool, - self._data._get_block().is_monotonic_decreasing( - self._data._get_block().index_columns - ), + self._block.is_monotonic_decreasing(self._block.index_columns), ) @property @@ -149,14 +191,65 @@ def has_duplicates(self) -> bool: duplicates_df = df.DataFrame(duplicates_block) return duplicates_df["is_duplicate"].any() - @property - def _block(self) -> blocks.Block: - return self._data._get_block() - @property def T(self) -> Index: return self.transpose() + @property + def query_job(self) -> Optional[bigquery.QueryJob]: + """BigQuery job metadata for the most recent query. + + Returns: + The most recent `QueryJob + `_. + """ + if self._query_job is None: + self._query_job = self._block._compute_dry_run() + return self._query_job + + def __repr__(self) -> str: + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + # TODO(swast): Avoid downloading the whole series by using job + # metadata, like we do with DataFrame. + opts = bigframes.options.display + max_results = opts.max_rows + if opts.repr_mode == "deferred": + return formatter.repr_query_job(self.query_job) + + pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) + self._query_job = query_job + return repr(pandas_df.index) + + def copy(self, name: Optional[Hashable] = None): + copy_index = Index(self._block) + if name is not None: + copy_index.name = name + return copy_index + + def to_series( + self, index: Optional[Index] = None, name: Optional[Hashable] = None + ) -> bigframes.series.Series: + if self.nlevels != 1: + NotImplementedError( + f"Converting multi-index to series is not yet supported. {constants.FEEDBACK_LINK}" + ) + + import bigframes.series + + name = self.name if name is None else name + if index is None: + return bigframes.series.Series(data=self, index=self, name=name) + else: + return bigframes.series.Series(data=self, index=Index(index), name=name) + + def get_level_values(self, level) -> Index: + level_n = level if isinstance(level, int) else self.names.index(level) + block = self._block.drop_levels( + [self._block.index_columns[i] for i in range(self.nlevels) if i != level_n] + ) + return Index(block) + def _memory_usage(self) -> int: (n_rows,) = self.shape return sum( @@ -180,7 +273,7 @@ def sort_values(self, *, ascending: bool = True, na_position: str = "last"): order.OrderingColumnReference(column, direction=direction, na_last=na_last) for column in index_columns ] - return Index._from_block(self._block.order_by(ordering)) + return Index(self._block.order_by(ordering)) def astype( self, @@ -269,7 +362,7 @@ def rename(self, name: Union[str, Sequence[str]]) -> Index: names = [name] if isinstance(name, str) else list(name) if len(names) != self.nlevels: raise ValueError("'name' must be same length as levels") - return Index._from_block(self._block.with_index_labels(names)) + return Index(self._block.with_index_labels(names)) def drop( self, @@ -291,17 +384,17 @@ def drop( ) block = block.filter(condition_id, keep_null=True) block = block.drop_columns([condition_id]) - return Index._from_block(block) + return Index(block) def dropna(self, how: str = "any") -> Index: if how not in ("any", "all"): raise ValueError("'how' must be one of 'any', 'all'") result = block_ops.dropna(self._block, self._block.index_columns, how=how) # type: ignore - return Index._from_block(result) + return Index(result) def drop_duplicates(self, *, keep: str = "first") -> Index: block = block_ops.drop_duplicates(self._block, self._block.index_columns, keep) - return Index._from_block(block) + return Index(block) def isin(self, values) -> Index: if not utils.is_list_like(values): @@ -330,7 +423,7 @@ def _apply_unary_expr( result_ids.append(result_id) block = block.set_index(result_ids, index_labels=self._block.index_labels) - return Index._from_block(block) + return Index(block) def _apply_aggregation(self, op: agg_ops.AggregateOp) -> typing.Any: if self.nlevels > 1: @@ -344,7 +437,7 @@ def __getitem__(self, key: int) -> typing.Any: result_pd_df, _ = self._block.slice(key, key + 1, 1).to_pandas() else: # special case, want [-1:] instead of [-1:0] result_pd_df, _ = self._block.slice(key).to_pandas() - if result_pd_df.empty: + if result_pd_df.index.empty: raise IndexError("single positional indexer is out-of-bounds") return result_pd_df.index[0] else: @@ -367,11 +460,36 @@ def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: def __len__(self): return self.shape[0] - @classmethod - def _from_block(cls, block: blocks.Block) -> Index: - import bigframes.dataframe as df - return Index(df.DataFrame(block)) +# Index that mutates the originating dataframe/series +class FrameIndex(Index): + def __init__( + self, + series_or_dataframe: typing.Union[ + bigframes.series.Series, bigframes.dataframe.DataFrame + ], + ): + super().__init__(series_or_dataframe._block) + self._whole_frame = series_or_dataframe + + @property + def name(self) -> blocks.Label: + return self.names[0] + + @name.setter + def name(self, value: blocks.Label): + self.names = [value] + + @property + def names(self) -> typing.Sequence[blocks.Label]: + """Returns the names of the Index.""" + return self._block._index_labels + + @names.setter + def names(self, values: typing.Sequence[blocks.Label]): + new_block = self._whole_frame._get_block().with_index_labels(values) + self._whole_frame._set_block(new_block) + self._block = new_block class IndexValue: @@ -406,15 +524,6 @@ def dtypes( def session(self) -> core.Session: return self._expr.session - def __repr__(self) -> str: - """Converts an Index to a string.""" - # TODO(swast): Add a timeout here? If the query is taking a long time, - # maybe we just print the job metadata that we have so far? - # TODO(swast): Avoid downloading the whole index by using job - # metadata, like we do with DataFrame. - preview = self.to_pandas() - return repr(preview) - def to_pandas(self) -> pandas.Index: """Executes deferred operations and downloads the results.""" # Project down to only the index column. So the query can be cached to visualize other data. diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9989831e1b2..2a20a4aabb4 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -146,10 +146,15 @@ def __init__( block = result_index._block if block: - if index: - raise NotImplementedError( - "DataFrame 'index' constructor parameter not supported " - f"when passing BigQuery-backed objects. {constants.FEEDBACK_LINK}" + if index is not None: + bf_index = indexes.Index(index) + idx_block = bf_index._block + idx_cols = idx_block.index_columns + join_idx, (_, r_mapping) = block.reset_index().index.join( + bf_index._block.reset_index().index, how="inner" + ) + block = join_idx._block.set_index( + [r_mapping[idx_col] for idx_col in idx_cols] ) if columns: block = block.select_columns(list(columns)) # type:ignore @@ -250,7 +255,7 @@ def _sql_names( def index( self, ) -> indexes.Index: - return indexes.Index(self) + return indexes.Index.from_frame(self) @index.setter def index(self, value): @@ -661,6 +666,14 @@ def _apply_binop( ): if isinstance(other, (float, int)): return self._apply_scalar_binop(other, op, reverse=reverse) + elif isinstance(other, indexes.Index): + return self._apply_series_binop( + other.to_series(index=self.index), + op, + axis=axis, + how=how, + reverse=reverse, + ) elif isinstance(other, bigframes.series.Series): return self._apply_series_binop( other, op, axis=axis, how=how, reverse=reverse @@ -1183,7 +1196,7 @@ def drop( return DataFrame(block) def _drop_by_index(self, index: indexes.Index) -> DataFrame: - block = index._data._get_block() + block = index._block block, ordering_col = block.promote_offsets() joined_index, (get_column_left, get_column_right) = self._block.index.join( block.index @@ -1319,9 +1332,7 @@ def _assign_single_item_listlike(self, k: str, v: Sequence) -> DataFrame: f"Length of values ({given_rows}) does not match length of index ({actual_rows})" ) - local_df = bigframes.dataframe.DataFrame( - {k: v}, session=self._get_block().expr.session - ) + local_df = DataFrame({k: v}, session=self._get_block().expr.session) # local_df is likely (but not guaranteed) to be cached locally # since the original list came from memory and so is probably < MAX_INLINE_DF_SIZE @@ -1622,7 +1633,7 @@ def _reindex_rows( raise ValueError("Original index must be unique to reindex") keep_original_names = False if isinstance(index, indexes.Index): - new_indexer = DataFrame(data=index._data._get_block())[[]] + new_indexer = DataFrame(data=index._block)[[]] else: if not isinstance(index, pandas.Index): keep_original_names = True diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 077815a9d65..6829d3faab5 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -21,6 +21,7 @@ import bigframes.constants as constants import bigframes.core.blocks as blocks import bigframes.core.expression as ex +import bigframes.core.indexes as indexes import bigframes.core.scalar as scalars import bigframes.dtypes import bigframes.operations as ops @@ -54,10 +55,34 @@ def __init__( if isinstance(data, blocks.Block): assert len(data.value_columns) == 1 assert len(data.column_labels) == 1 + assert index is None block = data elif isinstance(data, SeriesMethods): - block = data._get_block() + block = data._block + if index is not None: + # reindex + bf_index = indexes.Index(index) + idx_block = bf_index._block + idx_cols = idx_block.value_columns + block_idx, _ = idx_block.index.join(block.index, how="left") + block = block_idx._block.with_index_labels(bf_index.names) + + elif isinstance(data, indexes.Index): + if data.nlevels != 1: + raise NotImplementedError("Cannot interpret multi-index as Series.") + # Reset index to promote index columns to value columns, set default index + block = data._block.reset_index(drop=False) + if index is not None: + # Align by offset + bf_index = indexes.Index(index) + idx_block = bf_index._block.reset_index(drop=False) + idx_cols = idx_block.value_columns + block_idx, (l_mapping, _) = idx_block.index.join( + block.index, how="left" + ) + block = block_idx._block.set_index([l_mapping[col] for col in idx_cols]) + block = block.with_index_labels(bf_index.names) if block: if name: @@ -66,16 +91,10 @@ def __init__( f"BigQuery DataFrames only supports hashable series names. {constants.FEEDBACK_LINK}" ) block = block.with_column_labels([name]) - if index: - raise NotImplementedError( - f"Series 'index' constructor parameter not supported when passing BigQuery-backed objects. {constants.FEEDBACK_LINK}" - ) if dtype: block = block.multi_apply_unary_op( block.value_columns, ops.AsTypeOp(to_type=dtype) ) - self._block = block - else: import bigframes.pandas @@ -95,14 +114,15 @@ def __init__( if isinstance(dt, pd.ArrowDtype) ) ): - self._block = blocks.block_from_local(pd_dataframe) + block = blocks.block_from_local(pd_dataframe) elif session: - self._block = session.read_pandas(pd_dataframe)._get_block() + block = session.read_pandas(pd_dataframe)._get_block() else: # Uses default global session - self._block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() + block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() if pd_series.name is None: - self._block = self._block.with_column_labels([None]) + block = block.with_column_labels([None]) + self._block: blocks.Block = block @property def _value_column(self) -> str: diff --git a/bigframes/series.py b/bigframes/series.py index e049b414614..6a217279759 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -76,10 +76,6 @@ def dtype(self): def dtypes(self): return self._dtype - @property - def index(self) -> indexes.Index: - return indexes.Index(self) - @property def loc(self) -> bigframes.core.indexers.LocSeriesIndexer: return bigframes.core.indexers.LocSeriesIndexer(self) @@ -120,6 +116,10 @@ def empty(self) -> bool: def values(self) -> numpy.ndarray: return self.to_numpy() + @property + def index(self) -> indexes.Index: + return indexes.Index.from_frame(self) + @property def query_job(self) -> Optional[bigquery.QueryJob]: """BigQuery job metadata for the most recent query. @@ -978,7 +978,7 @@ def idxmax(self) -> blocks.Label: ] ) block = block.slice(0, 1) - return indexes.Index._from_block(block).to_pandas()[0] + return indexes.Index(block).to_pandas()[0] def idxmin(self) -> blocks.Label: block = self._block.order_by( @@ -991,7 +991,7 @@ def idxmin(self) -> blocks.Label: ] ) block = block.slice(0, 1) - return indexes.Index._from_block(block).to_pandas()[0] + return indexes.Index(block).to_pandas()[0] @property def is_monotonic_increasing(self) -> bool: @@ -1279,9 +1279,7 @@ def reindex(self, index=None, *, validate: typing.Optional[bool] = None): raise ValueError("Original index must be unique to reindex") keep_original_names = False if isinstance(index, indexes.Index): - new_indexer = bigframes.dataframe.DataFrame(data=index._data._get_block())[ - [] - ] + new_indexer = bigframes.dataframe.DataFrame(data=index._block)[[]] else: if not isinstance(index, pandas.Index): keep_original_names = True diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index f7fa0f0855e..2961884ebfd 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -16,9 +16,44 @@ import pandas as pd import pytest +import bigframes.pandas as bpd from tests.system.utils import assert_pandas_index_equal_ignore_index_type +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([3, 14, 159], dtype=pd.Int64Dtype(), name="my_index") + pd.testing.assert_index_equal(bf_result, pd_result) + + +def test_index_construct_from_series(): + bf_result = bpd.Index( + bpd.Series([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name"), + name="index_name", + dtype=pd.Int64Dtype(), + ).to_pandas() + pd_result = pd.Index( + pd.Series([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name"), + name="index_name", + dtype=pd.Int64Dtype(), + ) + pd.testing.assert_index_equal(bf_result, pd_result) + + +def test_index_construct_from_index(): + bf_index_input = bpd.Index( + [3, 14, 159], dtype=pd.Float64Dtype(), name="series_name" + ) + bf_result = bpd.Index( + bf_index_input, dtype=pd.Int64Dtype(), name="index_name" + ).to_pandas() + pd_index_input = pd.Index([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name") + pd_result = pd.Index(pd_index_input, dtype=pd.Int64Dtype(), name="index_name") + pd.testing.assert_index_equal(bf_result, pd_result) + + def test_get_index(scalars_df_index, scalars_pandas_df_index): index = scalars_df_index.index bf_result = index.to_pandas() @@ -240,6 +275,43 @@ def test_index_value_counts(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) +@pytest.mark.parametrize( + ("level",), + [ + ("int64_too",), + ("rowindex_2",), + (1,), + ], +) +def test_index_get_level_values(scalars_df_index, scalars_pandas_df_index, level): + bf_result = ( + scalars_df_index.set_index(["int64_too", "rowindex_2"]) + .index.get_level_values(level) + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index( + ["int64_too", "rowindex_2"] + ).index.get_level_values(level) + + pd.testing.assert_index_equal(bf_result, pd_result) + + +def test_index_to_series( + scalars_df_index, + scalars_pandas_df_index, +): + bf_result = ( + scalars_df_index.set_index(["int64_too"]) + .index.to_series(index=scalars_df_index["float64_col"], name="new_name") + .to_pandas() + ) + pd_result = scalars_pandas_df_index.set_index(["int64_too"]).index.to_series( + index=scalars_pandas_df_index["float64_col"], name="new_name" + ) + + pd.testing.assert_series_equal(bf_result, pd_result) + + @pytest.mark.parametrize( ("how",), [ diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 69b35d102c5..5d8fb0b29c7 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -42,6 +42,40 @@ def test_series_construct_copy(scalars_dfs): pd.testing.assert_series_equal(bf_result, pd_result) +def test_series_construct_copy_with_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = series.Series( + scalars_df["int64_col"], + name="test_series", + dtype="Float64", + index=scalars_df["int64_too"], + ).to_pandas() + pd_result = pd.Series( + scalars_pandas_df["int64_col"], + name="test_series", + dtype="Float64", + index=scalars_pandas_df["int64_too"], + ) + pd.testing.assert_series_equal(bf_result, pd_result) + + +def test_series_construct_copy_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = series.Series( + scalars_df.index, + name="test_series", + dtype="Float64", + index=scalars_df["int64_too"], + ).to_pandas() + pd_result = pd.Series( + scalars_pandas_df.index, + name="test_series", + dtype="Float64", + index=scalars_pandas_df["int64_too"], + ) + pd.testing.assert_series_equal(bf_result, pd_result) + + def test_series_construct_pandas(scalars_dfs): _, scalars_pandas_df = scalars_dfs bf_result = series.Series( diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 10cdbf8f7cf..93fba9f3aa2 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4714,7 +4714,7 @@ def index(self): [3 rows x 3 columns] >>> df.index # doctest: +ELLIPSIS - + Index([10, 20, 30], dtype='Int64') >>> df.index.values array([10, 20, 30], dtype=object) @@ -4731,7 +4731,10 @@ def index(self): [3 rows x 1 columns] >>> df1.index # doctest: +ELLIPSIS - + MultiIndex([( 'Alice', 'Seattle'), + ( 'Bob', 'New York'), + ('Aritra', 'Kona')], + name='Name') >>> df1.index.values array([('Alice', 'Seattle'), ('Bob', 'New York'), ('Aritra', 'Kona')], dtype=object) diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index e8737341a34..3ad87292719 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -58,6 +58,23 @@ def T(self) -> Index: """Return the transpose, which is by definition self.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def copy( + self, + name=None, + ) -> Index: + """ + Make a copy of this object. + + Name is set on the new object. + + Args: + name (Label, optional): + Set name for new object. + Returns: + Index: Index refer to new object which is a copy of this object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def transpose(self) -> Index: """ Return the transpose, which is by definition self. @@ -81,6 +98,40 @@ def astype(self, dtype): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def get_level_values(self, level) -> Index: + """ + Return an Index of values for requested level. + + This is primarily useful to get an individual level of values from a + MultiIndex, but is provided on Index as well for compatibility. + + Args: + level (int or str): + It is either the integer position or the name of the level. + + Returns: + Index: Calling object, as there is only one level in the Index. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def to_series(self): + """ + Create a Series with both index and values equal to the index keys. + + Useful with map for returning an indexer based on an index. + + Args: + index (Index, optional): + Index of resulting Series. If None, defaults to original index. + name (str, optional): + Name of resulting Series. If None, defaults to name of original + index. + + Returns: + Series: The dtype will be based on the type of the Index values. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def isin(self, values): """ Return a boolean array where the index values are in `values`. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 1aa4ffffbb5..33f03572f12 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -69,7 +69,7 @@ def index(self): 30 35 Name: Age, dtype: Int64 >>> s.index # doctest: +ELLIPSIS - + Index([10, 20, 30], dtype='Int64') >>> s.index.values array([10, 20, 30], dtype=object) @@ -84,7 +84,10 @@ def index(self): Aritra Kona 35 Name: Age, dtype: Int64 >>> s1.index # doctest: +ELLIPSIS - + MultiIndex([( 'Alice', 'Seattle'), + ( 'Bob', 'New York'), + ('Aritra', 'Kona')], + name='Name') >>> s1.index.values array([('Alice', 'Seattle'), ('Bob', 'New York'), ('Aritra', 'Kona')], dtype=object) From 99ed6c321b2727b0c6b5101f902f08885f3529c8 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 29 Jan 2024 10:12:15 -0800 Subject: [PATCH 288/921] refactor: Split aggregate ops from implementation (#354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/compile/aggregate_compiler.py | 413 +++++++++++++++++++ bigframes/core/compile/analytic_compiler.py | 0 bigframes/core/compile/compiled.py | 13 +- bigframes/operations/aggregations.py | 334 +-------------- 4 files changed, 425 insertions(+), 335 deletions(-) create mode 100644 bigframes/core/compile/aggregate_compiler.py create mode 100644 bigframes/core/compile/analytic_compiler.py diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py new file mode 100644 index 00000000000..044c33799ed --- /dev/null +++ b/bigframes/core/compile/aggregate_compiler.py @@ -0,0 +1,413 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools +import typing +from typing import cast, Optional + +import ibis +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types +import pandas as pd + +import bigframes.constants as constants +import bigframes.core.window_spec as window_spec +import bigframes.dtypes as dtypes +import bigframes.operations.aggregations as agg_ops +import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops + + +def compile_unary_aggregate( + op: agg_ops.AggregateOp, input: ibis_types.Column +) -> ibis_types.Value: + return compile_agg(op, input) + + +def compile_unary_analytic( + op: agg_ops.WindowOp, input: ibis_types.Column, window: window_spec.WindowSpec +) -> ibis_types.Value: + return compile_agg(op, input, window) + + +@functools.singledispatch +def compile_agg( + op: agg_ops.WindowOp, + input: ibis_types.Column, + window: Optional[window_spec.WindowSpec] = None, +) -> ibis_types.Value: + """Defines transformation but isn't cached, always use compile_node instead""" + raise ValueError(f"Can't compile unrecognized operation: {op}") + + +def numeric_op(operation): + @functools.wraps(operation) + def constrained_op(op, column: ibis_types.Column, window=None): + if column.type().is_boolean(): + column = typing.cast( + ibis_types.NumericColumn, column.cast(ibis_dtypes.int64) + ) + if column.type().is_numeric(): + return operation(op, column, window) + else: + raise ValueError( + f"Numeric operation cannot be applied to type {column.type()}. {constants.FEEDBACK_LINK}" + ) + + return constrained_op + + +@compile_agg.register +@numeric_op +def _( + op: agg_ops.SumOp, column: ibis_types.NumericColumn, window=None +) -> ibis_types.NumericValue: + # Will be null if all inputs are null. Pandas defaults to zero sum though. + bq_sum = _apply_window_if_present(column.sum(), window) + return ( + ibis.case().when(bq_sum.isnull(), ibis_types.literal(0)).else_(bq_sum).end() # type: ignore + ) + + +@compile_agg.register +@numeric_op +def _( + op: agg_ops.MedianOp, column: ibis_types.NumericColumn, window=None +) -> ibis_types.NumericValue: + # PERCENTILE_CONT has very few allowed windows. For example, "window + # framing clause is not allowed for analytic function percentile_cont". + if window is not None: + raise NotImplementedError( + f"Median with windowing is not supported. {constants.FEEDBACK_LINK}" + ) + + # TODO(swast): Allow switching between exact and approximate median. + # For now, the best we can do is an approximate median when we're doing + # an aggregation, as PERCENTILE_CONT is only an analytic function. + return cast(ibis_types.NumericValue, column.approx_median()) + + +@compile_agg.register +@numeric_op +def _( + op: agg_ops.ApproxQuartilesOp, column: ibis_types.NumericColumn, window=None +) -> ibis_types.NumericValue: + # PERCENTILE_CONT has very few allowed windows. For example, "window + # framing clause is not allowed for analytic function percentile_cont". + if window is not None: + raise NotImplementedError( + f"Approx Quartiles with windowing is not supported. {constants.FEEDBACK_LINK}" + ) + value = vendored_ibis_ops.ApproximateMultiQuantile( + column, num_bins=4 # type: ignore + ).to_expr()[op._quartile] + return cast(ibis_types.NumericValue, value) + + +@compile_agg.register +@numeric_op +def _( + op: agg_ops.MeanOp, column: ibis_types.NumericColumn, window=None +) -> ibis_types.NumericValue: + return _apply_window_if_present(column.mean(), window) + + +@compile_agg.register +@numeric_op +def _( + op: agg_ops.ProductOp, column: ibis_types.NumericColumn, window=None +) -> ibis_types.NumericValue: + # Need to short-circuit as log with zeroes is illegal sql + is_zero = cast(ibis_types.BooleanColumn, (column == 0)) + + # There is no product sql aggregate function, so must implement as a sum of logs, and then + # apply power after. Note, log and power base must be equal! This impl uses base 2. + logs = cast( + ibis_types.NumericColumn, + ibis.case().when(is_zero, 0).else_(column.abs().log2()).end(), + ) + logs_sum = _apply_window_if_present(logs.sum(), window) + magnitude = cast(ibis_types.NumericValue, ibis_types.literal(2)).pow(logs_sum) + + # Can't determine sign from logs, so have to determine parity of count of negative inputs + is_negative = cast( + ibis_types.NumericColumn, + ibis.case().when(column.sign() == -1, 1).else_(0).end(), + ) + negative_count = _apply_window_if_present(is_negative.sum(), window) + negative_count_parity = negative_count % cast( + ibis_types.NumericValue, ibis.literal(2) + ) # 1 if result should be negative, otherwise 0 + + any_zeroes = _apply_window_if_present(is_zero.any(), window) + float_result = ( + ibis.case() + .when(any_zeroes, ibis_types.literal(0)) + .else_(magnitude * pow(-1, negative_count_parity)) + .end() + ) + return float_result.cast(column.type()) # type: ignore + + +@compile_agg.register +def _(op: agg_ops.MaxOp, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.max(), window) + + +@compile_agg.register +def _(op: agg_ops.MinOp, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.min(), window) + + +@compile_agg.register +@numeric_op +def _(op: agg_ops.StdOp, x: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(cast(ibis_types.NumericColumn, x).std(), window) + + +@compile_agg.register +@numeric_op +def _(op: agg_ops.VarOp, x: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(cast(ibis_types.NumericColumn, x).var(), window) + + +@compile_agg.register +@numeric_op +def _(op: agg_ops.PopVarOp, x: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present( + cast(ibis_types.NumericColumn, x).var(how="pop"), window + ) + + +@compile_agg.register +def _( + op: agg_ops.CountOp, column: ibis_types.Column, window=None +) -> ibis_types.IntegerValue: + return _apply_window_if_present(column.count(), window) + + +@compile_agg.register +def _(op: agg_ops.CutOp, x: ibis_types.Column, window=None): + out = ibis.case() + + if op._bins_int > 0: + col_min = _apply_window_if_present(x.min(), window) + col_max = _apply_window_if_present(x.max(), window) + bin_width = (col_max - col_min) / op._bins + + if op._labels is False: + for this_bin in range(op._bins_int - 1): + out = out.when( + x <= (col_min + (this_bin + 1) * bin_width), + dtypes.literal_to_ibis_scalar( + this_bin, force_dtype=pd.Int64Dtype() + ), + ) + out = out.when(x.notnull(), op._bins - 1) + else: + interval_struct = None + adj = (col_max - col_min) * 0.001 + for this_bin in range(op._bins_int): + left_edge = ( + col_min + this_bin * bin_width - (0 if this_bin > 0 else adj) + ) + right_edge = col_min + (this_bin + 1) * bin_width + interval_struct = ibis.struct( + { + "left_exclusive": left_edge, + "right_inclusive": right_edge, + } + ) + + if this_bin < op._bins_int - 1: + out = out.when( + x <= (col_min + (this_bin + 1) * bin_width), + interval_struct, + ) + else: + out = out.when(x.notnull(), interval_struct) + else: + for interval in op._bins: + condition = (x > interval.left) & (x <= interval.right) + interval_struct = ibis.struct( + {"left_exclusive": interval.left, "right_inclusive": interval.right} + ) + out = out.when(condition, interval_struct) + return out.end() + + +@compile_agg.register +@numeric_op +def _( + self: agg_ops.QcutOp, column: ibis_types.Column, window=None +) -> ibis_types.IntegerValue: + if isinstance(self._quantiles, int): + quantiles_ibis = dtypes.literal_to_ibis_scalar(self._quantiles) + percent_ranks = cast( + ibis_types.FloatingColumn, + _apply_window_if_present(column.percent_rank(), window), + ) + float_bucket = cast(ibis_types.FloatingColumn, (percent_ranks * quantiles_ibis)) + return float_bucket.ceil().clip(lower=_ibis_num(1)) - _ibis_num(1) + else: + percent_ranks = cast( + ibis_types.FloatingColumn, + _apply_window_if_present(column.percent_rank(), window), + ) + out = ibis.case() + first_ibis_quantile = dtypes.literal_to_ibis_scalar(self._quantiles[0]) + out = out.when(percent_ranks < first_ibis_quantile, None) + for bucket_n in range(len(self._quantiles) - 1): + ibis_quantile = dtypes.literal_to_ibis_scalar(self._quantiles[bucket_n + 1]) + out = out.when( + percent_ranks <= ibis_quantile, + dtypes.literal_to_ibis_scalar(bucket_n, force_dtype=pd.Int64Dtype()), + ) + out = out.else_(None) + return out.end() # type: ignore + + +@compile_agg.register +def _( + op: agg_ops.NuniqueOp, column: ibis_types.Column, window=None +) -> ibis_types.IntegerValue: + return _apply_window_if_present(column.nunique(), window) + + +@compile_agg.register +def _( + op: agg_ops.AnyValueOp, column: ibis_types.Column, window=None +) -> ibis_types.IntegerValue: + return _apply_window_if_present(column.arbitrary(), window) + + +@compile_agg.register +def _( + op: agg_ops.RankOp, column: ibis_types.Column, window=None +) -> ibis_types.IntegerValue: + # Ibis produces 0-based ranks, while pandas creates 1-based ranks + return _apply_window_if_present(column.rank(), window) + 1 + + +@compile_agg.register +def _( + op: agg_ops.DenseRankOp, column: ibis_types.Column, window=None +) -> ibis_types.IntegerValue: + # Ibis produces 0-based ranks, while pandas creates 1-based ranks + return _apply_window_if_present(column.dense_rank(), window) + 1 + + +@compile_agg.register +def _(op: agg_ops.FirstOp, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.first(), window) + + +@compile_agg.register +def _( + op: agg_ops.FirstNonNullOp, column: ibis_types.Column, window=None +) -> ibis_types.Value: + return _apply_window_if_present( + vendored_ibis_ops.FirstNonNullValue(column).to_expr(), window # type: ignore + ) + + +@compile_agg.register +def _(op: agg_ops.LastOp, column: ibis_types.Column, window=None) -> ibis_types.Value: + return _apply_window_if_present(column.last(), window) + + +@compile_agg.register +def _( + op: agg_ops.LastNonNullOp, column: ibis_types.Column, window=None +) -> ibis_types.Value: + return _apply_window_if_present( + vendored_ibis_ops.LastNonNullValue(column).to_expr(), window # type: ignore + ) + + +@compile_agg.register +def _(op: agg_ops.ShiftOp, column: ibis_types.Column, window=None) -> ibis_types.Value: + if op._periods == 0: # No-op + return column + if op._periods > 0: + return _apply_window_if_present(column.lag(op._periods), window) + return _apply_window_if_present(column.lead(-op._periods), window) + + +@compile_agg.register +def _(op: agg_ops.DiffOp, column: ibis_types.Column, window=None) -> ibis_types.Value: + shifted = compile_agg(agg_ops.ShiftOp(op._periods), column, window) + if column.type().is_boolean(): + return cast(ibis_types.BooleanColumn, column) != cast( + ibis_types.BooleanColumn, shifted + ) + elif column.type().is_numeric(): + return cast(ibis_types.NumericColumn, column) - cast( + ibis_types.NumericColumn, shifted + ) + else: + raise TypeError(f"Cannot perform diff on type{column.type()}") + + +@compile_agg.register +def _( + op: agg_ops.AllOp, column: ibis_types.Column, window=None +) -> ibis_types.BooleanValue: + # BQ will return null for empty column, result would be true in pandas. + result = _is_true(column).all() + return cast( + ibis_types.BooleanScalar, + _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), + ) + + +@compile_agg.register +def _( + op: agg_ops.AnyOp, column: ibis_types.Column, window=None +) -> ibis_types.BooleanValue: + # BQ will return null for empty column, result would be false in pandas. + result = _is_true(column).any() + return cast( + ibis_types.BooleanScalar, + _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), + ) + + +def _apply_window_if_present(value: ibis_types.Value, window): + return value.over(window) if (window is not None) else value + + +def _map_to_literal( + original: ibis_types.Value, literal: ibis_types.Scalar +) -> ibis_types.Column: + # Hack required to perform aggregations on literals in ibis, even though bigquery will let you directly aggregate literals (eg. 'SELECT COUNT(1) from table1') + return ibis.ifelse(original.isnull(), literal, literal) # type: ignore + + +def _ibis_num(number: float): + return typing.cast(ibis_types.NumericValue, ibis_types.literal(number)) + + +def _is_true(column: ibis_types.Column) -> ibis_types.BooleanColumn: + if column.type().is_boolean(): + return cast(ibis_types.BooleanColumn, column) + elif column.type().is_numeric(): + result = cast(ibis_types.NumericColumn, column).__ne__(ibis_types.literal(0)) + return cast(ibis_types.BooleanColumn, result) + elif column.type().is_string(): + result = cast(ibis_types.StringValue, column).length() > ibis_types.literal(0) + return cast(ibis_types.BooleanColumn, result) + else: + # Time and geo values don't have a 'False' value + return cast( + ibis_types.BooleanColumn, _map_to_literal(column, ibis_types.literal(True)) + ) diff --git a/bigframes/core/compile/analytic_compiler.py b/bigframes/core/compile/analytic_compiler.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 9d213c6bed3..c867eaf6809 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -26,6 +26,7 @@ import ibis.expr.types as ibis_types import pandas +import bigframes.core.compile.aggregate_compiler as agg_compiler import bigframes.core.compile.scalar_op_compiler as op_compilers import bigframes.core.expression as ex import bigframes.core.guid @@ -447,7 +448,7 @@ def aggregate( """ table = self._to_ibis_expr() stats = { - col_out: agg_op._as_ibis(table[col_in]) + col_out: agg_compiler.compile_agg(agg_op, table[col_in]) for col_in, agg_op, col_out in aggregations } if by_column_ids: @@ -810,7 +811,7 @@ def project_window_op( column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) window = self._ibis_window_from_spec(window_spec, allow_ties=op.handles_ties) - window_op = op._as_ibis(column, window) + window_op = agg_compiler.compile_unary_analytic(op, column, window) clauses = [] if op.skips_nulls and not never_skip_nulls: @@ -818,12 +819,16 @@ def project_window_op( if window_spec.min_periods: if op.skips_nulls: # Most operations do not count NULL values towards min_periods - observation_count = agg_ops.count_op._as_ibis(column, window) + observation_count = agg_compiler.compile_unary_analytic( + agg_ops.count_op, column, window + ) else: # Operations like count treat even NULLs as valid observations for the sake of min_periods # notnull is just used to convert null values to non-null (FALSE) values to be counted denulled_value = typing.cast(ibis_types.BooleanColumn, column.notnull()) - observation_count = agg_ops.count_op._as_ibis(denulled_value, window) + observation_count = agg_compiler.compile_unary_analytic( + agg_ops.count_op, denulled_value, window + ) clauses.append( ( observation_count < ibis_types.literal(window_spec.min_periods), diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index cc2e79c1003..ba62ae28d2d 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -16,21 +16,13 @@ import typing -import ibis -import ibis.expr.datatypes as ibis_dtypes -import ibis.expr.types as ibis_types from pandas import Int64Dtype import pandas as pd -import bigframes.constants as constants import bigframes.dtypes as dtypes -import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops class WindowOp: - def _as_ibis(self, value: ibis_types.Column, window=None): - raise NotImplementedError("Base class WindowOp has no implementaiton.") - @property def skips_nulls(self): """Whether the window op skips null rows.""" @@ -45,184 +37,52 @@ def handles_ties(self): class AggregateOp(WindowOp): name = "abstract_aggregate" - def _as_ibis(self, value: ibis_types.Column, window=None): - raise NotImplementedError("Base class AggregateOp has no implementaiton.") - - -def numeric_op(operation): - def constrained_op(op, column: ibis_types.Column, window=None): - if column.type().is_boolean(): - column = typing.cast( - ibis_types.NumericColumn, column.cast(ibis_dtypes.int64) - ) - if column.type().is_numeric(): - return operation(op, column, window) - else: - raise ValueError( - f"Numeric operation cannot be applied to type {column.type()}. {constants.FEEDBACK_LINK}" - ) - - return constrained_op - class SumOp(AggregateOp): name = "sum" - @numeric_op - def _as_ibis( - self, column: ibis_types.NumericColumn, window=None - ) -> ibis_types.NumericValue: - # Will be null if all inputs are null. Pandas defaults to zero sum though. - bq_sum = _apply_window_if_present(column.sum(), window) - return ( - ibis.case().when(bq_sum.isnull(), ibis_types.literal(0)).else_(bq_sum).end() # type: ignore - ) - class MedianOp(AggregateOp): name = "median" - @numeric_op - def _as_ibis( - self, column: ibis_types.NumericColumn, window=None - ) -> ibis_types.NumericValue: - # PERCENTILE_CONT has very few allowed windows. For example, "window - # framing clause is not allowed for analytic function percentile_cont". - if window is not None: - raise NotImplementedError( - f"Median with windowing is not supported. {constants.FEEDBACK_LINK}" - ) - - # TODO(swast): Allow switching between exact and approximate median. - # For now, the best we can do is an approximate median when we're doing - # an aggregation, as PERCENTILE_CONT is only an analytic function. - return typing.cast(ibis_types.NumericValue, column.approx_median()) - class ApproxQuartilesOp(AggregateOp): def __init__(self, quartile: int): self.name = f"{quartile*25}%" self._quartile = quartile - @numeric_op - def _as_ibis( - self, column: ibis_types.NumericColumn, window=None - ) -> ibis_types.NumericValue: - # PERCENTILE_CONT has very few allowed windows. For example, "window - # framing clause is not allowed for analytic function percentile_cont". - if window is not None: - raise NotImplementedError( - f"Approx Quartiles with windowing is not supported. {constants.FEEDBACK_LINK}" - ) - value = vendored_ibis_ops.ApproximateMultiQuantile( - column, num_bins=4 # type: ignore - ).to_expr()[self._quartile] - return typing.cast(ibis_types.NumericValue, value) - class MeanOp(AggregateOp): name = "mean" - @numeric_op - def _as_ibis( - self, column: ibis_types.NumericColumn, window=None - ) -> ibis_types.NumericValue: - return _apply_window_if_present(column.mean(), window) - class ProductOp(AggregateOp): name = "product" - @numeric_op - def _as_ibis( - self, column: ibis_types.NumericColumn, window=None - ) -> ibis_types.NumericValue: - # Need to short-circuit as log with zeroes is illegal sql - is_zero = typing.cast(ibis_types.BooleanColumn, (column == 0)) - - # There is no product sql aggregate function, so must implement as a sum of logs, and then - # apply power after. Note, log and power base must be equal! This impl uses base 2. - logs = typing.cast( - ibis_types.NumericColumn, - ibis.case().when(is_zero, 0).else_(column.abs().log2()).end(), - ) - logs_sum = _apply_window_if_present(logs.sum(), window) - magnitude = typing.cast(ibis_types.NumericValue, ibis_types.literal(2)).pow( - logs_sum - ) - - # Can't determine sign from logs, so have to determine parity of count of negative inputs - is_negative = typing.cast( - ibis_types.NumericColumn, - ibis.case().when(column.sign() == -1, 1).else_(0).end(), - ) - negative_count = _apply_window_if_present(is_negative.sum(), window) - negative_count_parity = negative_count % typing.cast( - ibis_types.NumericValue, ibis.literal(2) - ) # 1 if result should be negative, otherwise 0 - - any_zeroes = _apply_window_if_present(is_zero.any(), window) - float_result = ( - ibis.case() - .when(any_zeroes, ibis_types.literal(0)) - .else_(magnitude * pow(-1, negative_count_parity)) - .end() - ) - return float_result.cast(column.type()) # type: ignore - class MaxOp(AggregateOp): name = "max" - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present(column.max(), window) - class MinOp(AggregateOp): name = "min" - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present(column.min(), window) - class StdOp(AggregateOp): name = "std" - @numeric_op - def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present( - typing.cast(ibis_types.NumericColumn, x).std(), window - ) - class VarOp(AggregateOp): name = "var" - @numeric_op - def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present( - typing.cast(ibis_types.NumericColumn, x).var(), window - ) - class PopVarOp(AggregateOp): name = "popvar" - @numeric_op - def _as_ibis(self, x: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present( - typing.cast(ibis_types.NumericColumn, x).var(how="pop"), window - ) - class CountOp(AggregateOp): name = "count" - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.IntegerValue: - return _apply_window_if_present(column.count(), window) - @property def skips_nulls(self): return False @@ -241,54 +101,6 @@ def __init__(self, bins: typing.Union[int, pd.IntervalIndex], labels=None): self._labels = labels - def _as_ibis(self, x: ibis_types.Column, window=None): - out = ibis.case() - - if self._bins_int > 0: - col_min = _apply_window_if_present(x.min(), window) - col_max = _apply_window_if_present(x.max(), window) - bin_width = (col_max - col_min) / self._bins - - if self._labels is False: - for this_bin in range(self._bins_int - 1): - out = out.when( - x <= (col_min + (this_bin + 1) * bin_width), - dtypes.literal_to_ibis_scalar( - this_bin, force_dtype=Int64Dtype() - ), - ) - out = out.when(x.notnull(), self._bins - 1) - else: - interval_struct = None - adj = (col_max - col_min) * 0.001 - for this_bin in range(self._bins_int): - left_edge = ( - col_min + this_bin * bin_width - (0 if this_bin > 0 else adj) - ) - right_edge = col_min + (this_bin + 1) * bin_width - interval_struct = ibis.struct( - { - "left_exclusive": left_edge, - "right_inclusive": right_edge, - } - ) - - if this_bin < self._bins_int - 1: - out = out.when( - x <= (col_min + (this_bin + 1) * bin_width), - interval_struct, - ) - else: - out = out.when(x.notnull(), interval_struct) - else: - for interval in self._bins: - condition = (x > interval.left) & (x <= interval.right) - interval_struct = ibis.struct( - {"left_exclusive": interval.left, "right_inclusive": interval.right} - ) - out = out.when(condition, interval_struct) - return out.end() - @property def skips_nulls(self): return False @@ -303,39 +115,6 @@ def __init__(self, quantiles: typing.Union[int, typing.Sequence[float]]): self.name = f"qcut-{quantiles}" self._quantiles = quantiles - @numeric_op - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.IntegerValue: - if isinstance(self._quantiles, int): - quantiles_ibis = dtypes.literal_to_ibis_scalar(self._quantiles) - percent_ranks = typing.cast( - ibis_types.FloatingColumn, - _apply_window_if_present(column.percent_rank(), window), - ) - float_bucket = typing.cast( - ibis_types.FloatingColumn, (percent_ranks * quantiles_ibis) - ) - return float_bucket.ceil().clip(lower=_ibis_num(1)) - _ibis_num(1) - else: - percent_ranks = typing.cast( - ibis_types.FloatingColumn, - _apply_window_if_present(column.percent_rank(), window), - ) - out = ibis.case() - first_ibis_quantile = dtypes.literal_to_ibis_scalar(self._quantiles[0]) - out = out.when(percent_ranks < first_ibis_quantile, None) - for bucket_n in range(len(self._quantiles) - 1): - ibis_quantile = dtypes.literal_to_ibis_scalar( - self._quantiles[bucket_n + 1] - ) - out = out.when( - percent_ranks <= ibis_quantile, - dtypes.literal_to_ibis_scalar(bucket_n, force_dtype=Int64Dtype()), - ) - out = out.else_(None) - return out.end() # type: ignore - @property def skips_nulls(self): return False @@ -348,11 +127,6 @@ def handles_ties(self): class NuniqueOp(AggregateOp): name = "nunique" - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.IntegerValue: - return _apply_window_if_present(column.nunique(), window) - @property def skips_nulls(self): return False @@ -363,11 +137,6 @@ class AnyValueOp(AggregateOp): # Do not expose to users. For special cases only (e.g. pivot). name = "any_value" - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.IntegerValue: - return _apply_window_if_present(column.arbitrary(), window) - @property def skips_nulls(self): return True @@ -376,12 +145,6 @@ def skips_nulls(self): class RankOp(WindowOp): name = "rank" - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.IntegerValue: - # Ibis produces 0-based ranks, while pandas creates 1-based ranks - return _apply_window_if_present(column.rank(), window) + 1 - @property def skips_nulls(self): return False @@ -392,12 +155,6 @@ def handles_ties(self): class DenseRankOp(WindowOp): - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.IntegerValue: - # Ibis produces 0-based ranks, while pandas creates 1-based ranks - return _apply_window_if_present(column.dense_rank(), window) + 1 - @property def skips_nulls(self): return False @@ -408,8 +165,7 @@ def handles_ties(self): class FirstOp(WindowOp): - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present(column.first(), window) + name = "first" class FirstNonNullOp(WindowOp): @@ -417,15 +173,9 @@ class FirstNonNullOp(WindowOp): def skips_nulls(self): return False - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present( - vendored_ibis_ops.FirstNonNullValue(column).to_expr(), window # type: ignore - ) - class LastOp(WindowOp): - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present(column.last(), window) + name = "last" class LastNonNullOp(WindowOp): @@ -433,23 +183,11 @@ class LastNonNullOp(WindowOp): def skips_nulls(self): return False - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - return _apply_window_if_present( - vendored_ibis_ops.LastNonNullValue(column).to_expr(), window # type: ignore - ) - class ShiftOp(WindowOp): def __init__(self, periods: int): self._periods = periods - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - if self._periods == 0: # No-op - return column - if self._periods > 0: - return _apply_window_if_present(column.lag(self._periods), window) - return _apply_window_if_present(column.lead(-self._periods), window) - @property def skips_nulls(self): return False @@ -459,80 +197,18 @@ class DiffOp(WindowOp): def __init__(self, periods: int): self._periods = periods - def _as_ibis(self, column: ibis_types.Column, window=None) -> ibis_types.Value: - shifted = ShiftOp(self._periods)._as_ibis(column, window) - if column.type().is_boolean(): - return typing.cast(ibis_types.BooleanColumn, column) != typing.cast( - ibis_types.BooleanColumn, shifted - ) - elif column.type().is_numeric(): - return typing.cast(ibis_types.NumericColumn, column) - typing.cast( - ibis_types.NumericColumn, shifted - ) - else: - raise TypeError(f"Cannot perform diff on type{column.type()}") - @property def skips_nulls(self): return False class AllOp(AggregateOp): - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.BooleanValue: - # BQ will return null for empty column, result would be true in pandas. - result = _is_true(column).all() - return typing.cast( - ibis_types.BooleanScalar, - _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), - ) + name = "all" class AnyOp(AggregateOp): name = "any" - def _as_ibis( - self, column: ibis_types.Column, window=None - ) -> ibis_types.BooleanValue: - # BQ will return null for empty column, result would be false in pandas. - result = _is_true(column).any() - return typing.cast( - ibis_types.BooleanScalar, - _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), - ) - - -def _is_true(column: ibis_types.Column) -> ibis_types.BooleanColumn: - if column.type().is_boolean(): - return typing.cast(ibis_types.BooleanColumn, column) - elif column.type().is_numeric(): - result = typing.cast(ibis_types.NumericColumn, column).__ne__( - ibis_types.literal(0) - ) - return typing.cast(ibis_types.BooleanColumn, result) - elif column.type().is_string(): - result = typing.cast( - ibis_types.StringValue, column - ).length() > ibis_types.literal(0) - return typing.cast(ibis_types.BooleanColumn, result) - else: - # Time and geo values don't have a 'False' value - return typing.cast( - ibis_types.BooleanColumn, _map_to_literal(column, ibis_types.literal(True)) - ) - - -def _apply_window_if_present(value: ibis_types.Value, window): - return value.over(window) if (window is not None) else value - - -def _map_to_literal( - original: ibis_types.Value, literal: ibis_types.Scalar -) -> ibis_types.Column: - # Hack required to perform aggregations on literals in ibis, even though bigquery will let you directly aggregate literals (eg. 'SELECT COUNT(1) from table1') - return ibis.ifelse(original.isnull(), literal, literal) # type: ignore - sum_op = SumOp() mean_op = MeanOp() @@ -587,7 +263,3 @@ def lookup_agg_func(key: str) -> AggregateOp: return _AGGREGATIONS_LOOKUP[key] else: raise ValueError(f"Unrecognize aggregate function: {key}") - - -def _ibis_num(number: float): - return typing.cast(ibis_types.NumericValue, ibis_types.literal(number)) From 6795ed2b6e745eb1504770e7f518fb9b0eb5713a Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 29 Jan 2024 16:30:16 -0600 Subject: [PATCH 289/921] chore: pin pytest version (#358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- noxfile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 873b60ce912..b79f6f11821 100644 --- a/noxfile.py +++ b/noxfile.py @@ -30,6 +30,10 @@ BLACK_VERSION = "black==22.3.0" ISORT_VERSION = "isort==5.12.0" + +# pytest-retry is not yet compatible with pytest 8.x. +# https://github.com/str0zzapreti/pytest-retry/issues/32 +PYTEST_VERSION = "pytest<8.0.0dev" SPHINX_VERSION = "sphinx==4.5.0" LINT_PATHS = ["docs", "bigframes", "tests", "third_party", "noxfile.py", "setup.py"] @@ -39,7 +43,7 @@ UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", "asyncmock", - "pytest", + PYTEST_VERSION, "pytest-cov", "pytest-asyncio", "pytest-mock", @@ -55,7 +59,7 @@ "jinja2", "mock", "openpyxl", - "pytest", + PYTEST_VERSION, "pytest-cov", "pytest-retry", "pytest-timeout", From 5aad3a1152453f503fe36c413f97c5975b55b017 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 30 Jan 2024 10:09:43 -0600 Subject: [PATCH 290/921] chore: ensure colab sample notebooks are tested (#351) * chore: ensure colab sample notebooks are tested * make restore from backup robust to when the backup doesn't exist * fix path to notebook params scripts * exclude notebooks that need parameters other than project_id * add missing dependencies * notebook testing fixes * add sleep to avoid some bucket flakiness * Revert "add sleep to avoid some bucket flakiness" This reverts commit dfee838b5c41ed6df51861a023454c4b1bbfc689. * exclude bq_dataframes_llm_code_generation sample --- .../getting_started_bq_dataframes.ipynb | 15 +++- noxfile.py | 78 +++++++++++++------ scripts/notebooks_fill_params.py | 65 ++++++++++++++++ scripts/notebooks_restore_from_backup.py | 35 +++++++++ 4 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 scripts/notebooks_fill_params.py create mode 100644 scripts/notebooks_restore_from_backup.py diff --git a/notebooks/getting_started/getting_started_bq_dataframes.ipynb b/notebooks/getting_started/getting_started_bq_dataframes.ipynb index a70e2f65ffe..a9b6aefe30a 100644 --- a/notebooks/getting_started/getting_started_bq_dataframes.ipynb +++ b/notebooks/getting_started/getting_started_bq_dataframes.ipynb @@ -532,6 +532,10 @@ }, "outputs": [], "source": [ + "# BigQuery DataFrames can read directly from GCS.\n", + "fn = 'gs://cloud-samples-data/vertex-ai/bigframe/penguins.csv'\n", + "\n", + "# Or from a local file.\n", "# fn = 'penguins.csv'" ] }, @@ -580,7 +584,9 @@ }, "outputs": [], "source": [ - "df_from_local = bf.read_csv(fn)" + "# If order is not important, use the \"bigquery\" engine to\n", + "# allow BigQuery DataFrames to read directly from GCS.\n", + "df_from_local = bf.read_csv(fn, engine=\"bigquery\")" ] }, { @@ -658,7 +664,10 @@ }, "outputs": [], "source": [ - "df_from_local.to_gbq(PROJECT_ID + \".\" + DATASET_ID + \".penguins\")" + "df_from_local.to_gbq(\n", + " PROJECT_ID + \".\" + DATASET_ID + \".penguins\",\n", + " if_exists=\"replace\",\n", + ")" ] }, { @@ -771,7 +780,7 @@ }, "outputs": [], "source": [ - "bq_df[\"species\", \"body_mass_g\"].groupby(by=bq_df[\"species\"]).mean(numeric_only=True).head()" + "bq_df[[\"species\", \"body_mass_g\"]].groupby(by=bq_df[\"species\"]).mean(numeric_only=True).head()" ] }, { diff --git a/noxfile.py b/noxfile.py index b79f6f11821..7cf9faf685f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -657,9 +657,23 @@ def system_prerelease(session: nox.sessions.Session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) -def notebook(session): +def notebook(session: nox.Session): + GOOGLE_CLOUD_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT") + if not GOOGLE_CLOUD_PROJECT: + session.error( + "Set GOOGLE_CLOUD_PROJECT environment variable to run notebook session." + ) + session.install("-e", ".[all]") - session.install("pytest", "pytest-xdist", "pytest-retry", "nbmake") + session.install( + "pytest", + "pytest-xdist", + "pytest-retry", + "nbmake", + "google-cloud-aiplatform", + "matplotlib", + "seaborn", + ) notebooks_list = list(Path("notebooks/").glob("*/*.ipynb")) @@ -669,19 +683,22 @@ def notebook(session): # These notebooks contain special colab `param {type:"string"}` # comments, which make it easy for customers to fill in their # own information. + # + # With the notebooks_fill_params.py script, we are able to find and + # replace the PROJECT_ID parameter, but not the others. + # # TODO(ashleyxu): Test these notebooks by replacing parameters with # appropriate values and omitting cleanup logic that may break # our test infrastructure. - "notebooks/getting_started/getting_started_bq_dataframes.ipynb", - "notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb", - "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", - "notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb", - "notebooks/regression/bq_dataframes_ml_linear_regression.ipynb", - "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", - "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", - "notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb", - "notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb", - "notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb", + "notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb", # Needs DATASET. + "notebooks/regression/bq_dataframes_ml_linear_regression.ipynb", # Needs DATASET_ID. + "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", # Needs CONNECTION. + # TODO(swast): investigate why we get 404 errors, even though + # bq_dataframes_llm_code_generation creates a bucket in the sample. + "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", # Needs BUCKET_URI. + "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", # Needs BUCKET_URI. + "notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb", # Needs BUCKET_URI. + "notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb", # Needs BUCKET_URI. # The experimental notebooks imagine features that don't yet # exist or only exist as temporary prototypes. "notebooks/experimental/longer_ml_demo.ipynb", @@ -709,9 +726,9 @@ def notebook(session): for nb, regions in notebooks_reg.items() } - # For some reason nbmake exits silently with "no tests ran" message if + # The pytest --nbmake exits silently with "no tests ran" message if # one of the notebook paths supplied does not exist. Let's make sure that - # each path exists + # each path exists. for nb in notebooks + list(notebooks_reg): assert os.path.exists(nb), nb @@ -723,16 +740,33 @@ def notebook(session): pytest_command = [ "py.test", "--nbmake", - "--nbmake-timeout=600", + "--nbmake-timeout=900", # 15 minutes ] - # Run self-contained notebooks in single session.run - # achieve parallelization via -n - session.run( - *pytest_command, - "-nauto", - *notebooks, - ) + try: + # Populate notebook parameters and make a backup so that the notebooks + # are runnable. + session.run( + "python", + CURRENT_DIRECTORY / "scripts" / "notebooks_fill_params.py", + *notebooks, + ) + + # Run self-contained notebooks in single session.run + # achieve parallelization via -n + session.run( + *pytest_command, + "-nauto", + *notebooks, + ) + finally: + # Prevent our notebook changes from getting checked in to git + # accidentally. + session.run( + "python", + CURRENT_DIRECTORY / "scripts" / "notebooks_restore_from_backup.py", + *notebooks, + ) # Run regionalized notebooks in parallel session.run's, since each notebook # takes a different region via env param. diff --git a/scripts/notebooks_fill_params.py b/scripts/notebooks_fill_params.py new file mode 100644 index 00000000000..e0f7c8d687a --- /dev/null +++ b/scripts/notebooks_fill_params.py @@ -0,0 +1,65 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +import re +import shutil +import sys + +GOOGLE_CLOUD_PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] + + +def make_backup(notebook_path: str): + shutil.copy( + notebook_path, + f"{notebook_path}.backup", + ) + + +def replace_project(line): + """ + Notebooks contain special colab `param {type:"string"}` + comments, which make it easy for customers to fill in their + own information. + """ + # Make sure we're robust to whitespace differences. + cleaned = re.sub(r"\s", "", line) + if cleaned == 'PROJECT_ID=""#@param{type:"string"}': + return f'PROJECT_ID = "{GOOGLE_CLOUD_PROJECT}" # @param {{type:"string"}}\n' + else: + return line + + +def replace_params(notebook_path: str): + with open(notebook_path, "r", encoding="utf-8") as notebook_file: + notebook_json = json.load(notebook_file) + + for cell in notebook_json["cells"]: + lines = cell.get("source", []) + new_lines = [replace_project(line) for line in lines] + cell["source"] = new_lines + + with open(notebook_path, "w", encoding="utf-8") as notebook_file: + json.dump(notebook_json, notebook_file, indent=2, ensure_ascii=False) + + +def main(notebook_paths): + for notebook_path in notebook_paths: + make_backup(notebook_path) + replace_params(notebook_path) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/scripts/notebooks_restore_from_backup.py b/scripts/notebooks_restore_from_backup.py new file mode 100644 index 00000000000..4d3e0333e39 --- /dev/null +++ b/scripts/notebooks_restore_from_backup.py @@ -0,0 +1,35 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +import shutil +import sys + + +def restore_from_backup(notebook_path): + backup_path = pathlib.Path(f"{notebook_path}.backup") + if backup_path.exists(): + shutil.move( + backup_path, + notebook_path, + ) + + +def main(notebook_paths): + for notebook_path in notebook_paths: + restore_from_backup(notebook_path) + + +if __name__ == "__main__": + main(sys.argv[1:]) From 09a52fda19cde8efa6b20731d5b8e21f50b18a9a Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:36:15 -0800 Subject: [PATCH 291/921] docs: add code samples for `Series.{between, cumprod}` (#353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - [x] `Series.between()`: https://screenshot.googleplex.com/BhHpZsL7S9d3FsG - [x] `Series.cumprod()`: https://screenshot.googleplex.com/7o7gDNwJZEWst84 Fixes # 🦕 --- .../bigframes_vendored/pandas/core/series.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 33f03572f12..9e376c713e6 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1777,6 +1777,42 @@ def between( corresponding Series element is between the boundary values `left` and `right`. NA values are treated as `False`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Boundary values are included by default: + + >>> s = bpd.Series([2, 0, 4, 8, np.nan]) + >>> s.between(1, 4) + 0 True + 1 False + 2 True + 3 False + 4 + dtype: boolean + + With inclusive set to "neither" boundary values are excluded: + + >>> s.between(1, 4, inclusive="neither") + 0 True + 1 False + 2 False + 3 False + 4 + dtype: boolean + + left and right can be any scalar value: + + >>> s = bpd.Series(['Alice', 'Bob', 'Carol', 'Eve']) + >>> s.between('Anna', 'Daniel') + 0 False + 1 True + 2 True + 3 False + dtype: boolean + Args: left (scalar or list-like): Left boundary. @@ -1799,6 +1835,30 @@ def cumprod(self): Returns a DataFrame or Series of the same size containing the cumulative product. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([2, np.nan, 5, -1, 0]) + >>> s + 0 2.0 + 1 + 2 5.0 + 3 -1.0 + 4 0.0 + dtype: Float64 + + By default, NA values are ignored. + + >>> s.cumprod() + 0 2.0 + 1 + 2 10.0 + 3 -10.0 + 4 0.0 + dtype: Float64 + Returns: bigframes.series.Series: Return cumulative sum of scalar or Series. """ From 18efb83e49d0a252c0fcfdd854c4500517d529b7 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:58:08 -0800 Subject: [PATCH 292/921] chore(main): release 0.20.0 (#342) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 22 ++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29f99ecc43c..771f04776ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.20.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.19.2...v0.20.0) (2024-01-30) + + +### Features + +* Add `DataFrame.peek()` as an efficient alternative to `head()` results preview ([#318](https://github.com/googleapis/python-bigquery-dataframes/issues/318)) ([9c34d83](https://github.com/googleapis/python-bigquery-dataframes/commit/9c34d834e83ca5514bee723ebb9a7ad1ad50e88d)) +* Add ARIMA_EVAULATE options in forecasting models ([#336](https://github.com/googleapis/python-bigquery-dataframes/issues/336)) ([73e997b](https://github.com/googleapis/python-bigquery-dataframes/commit/73e997b3e80f844a8120b52ed2ece8b046cf4ca9)) +* Add Index constructor, repr, copy, get_level_values, to_series ([#334](https://github.com/googleapis/python-bigquery-dataframes/issues/334)) ([e5d054e](https://github.com/googleapis/python-bigquery-dataframes/commit/e5d054e93a05f5c504e8db57b954c07d33e5f5b9)) +* Improve error message for drive based BQ table reads ([#344](https://github.com/googleapis/python-bigquery-dataframes/issues/344)) ([0794788](https://github.com/googleapis/python-bigquery-dataframes/commit/0794788a2d232d795d803cd0c5b3f7d51c562cf1)) +* Update cut to work without labels = False and show intervals as dict ([#335](https://github.com/googleapis/python-bigquery-dataframes/issues/335)) ([4ff53db](https://github.com/googleapis/python-bigquery-dataframes/commit/4ff53db48133b817bec5f123b634690244a610d3)) + + +### Bug Fixes + +* Chance default connection name in getting_started.ipnyb ([#347](https://github.com/googleapis/python-bigquery-dataframes/issues/347)) ([677f014](https://github.com/googleapis/python-bigquery-dataframes/commit/677f0146acf19def88fddbeb0527a078458948ae)) +* Series iteration correctly returns values instead of index ([#339](https://github.com/googleapis/python-bigquery-dataframes/issues/339)) ([2c6af9b](https://github.com/googleapis/python-bigquery-dataframes/commit/2c6af9ba8b362dae39a6e082cdc816c955c73517)) + + +### Documentation + +* Add code samples for `Series.{between, cumprod}` ([#353](https://github.com/googleapis/python-bigquery-dataframes/issues/353)) ([09a52fd](https://github.com/googleapis/python-bigquery-dataframes/commit/09a52fda19cde8efa6b20731d5b8e21f50b18a9a)) + ## [0.19.2](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.19.1...v0.19.2) (2024-01-22) diff --git a/bigframes/version.py b/bigframes/version.py index 4d71f17d710..131f820e7d5 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.19.2" +__version__ = "0.20.0" From 068879f97fb1626aca081106150803f832a0cf81 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 30 Jan 2024 14:22:16 -0800 Subject: [PATCH 293/921] perf: Make repr cache the block where appropriate (#350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/blocks.py | 13 ++++++- bigframes/core/nodes.py | 69 ++++++++++++++++++++++++++++++++++- bigframes/core/ordering.py | 8 ++++ bigframes/core/traversal.py | 27 ++++++++++++++ bigframes/dataframe.py | 12 +++++- bigframes/ml/core.py | 10 +++-- bigframes/series.py | 5 ++- bigframes/session/__init__.py | 38 ++++++++++++++++++- 8 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 bigframes/core/traversal.py diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 9e17dc2752e..8e9ceb83f76 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1695,10 +1695,19 @@ def to_sql_query( idx_labels, ) - def cached(self) -> Block: + def cached(self, *, optimize_offsets=False, force: bool = False) -> Block: """Write the block to a session table and create a new block object that references it.""" + # use a heuristic for whether something needs to be cached + if (not force) and self.session._is_trivially_executable(self.expr): + return self + if optimize_offsets: + expr = self.session._cache_with_offsets(self.expr) + else: + expr = self.session._cache_with_cluster_cols( + self.expr, cluster_cols=self.index_columns + ) return Block( - self.session._execute_and_cache(self.expr, cluster_cols=self.index_columns), + expr, index_columns=self.index_columns, column_labels=self.column_labels, index_labels=self.index_labels, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index e1882c36848..99ea229a44a 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -50,6 +50,19 @@ def deterministic(self) -> bool: """Whether this node will evaluates deterministically.""" return True + @property + def row_preserving(self) -> bool: + """Whether this node preserves input rows.""" + return True + + @property + def non_local(self) -> bool: + """ + Whether this node combines information across multiple rows instead of processing rows independently. + Used as an approximation for whether the expression may require shuffling to execute (and therefore be expensive). + """ + return False + @property def child_nodes(self) -> typing.Sequence[BigFrameNode]: """Direct children of this node""" @@ -104,6 +117,14 @@ class JoinNode(BigFrameNode): join: JoinDefinition allow_row_identity_join: bool = True + @property + def row_preserving(self) -> bool: + return False + + @property + def non_local(self) -> bool: + return True + @property def child_nodes(self) -> typing.Sequence[BigFrameNode]: return (self.left_child, self.right_child) @@ -184,11 +205,19 @@ def __hash__(self): def peekable(self) -> bool: return False + @property + def non_local(self) -> bool: + return False + @dataclass(frozen=True) class FilterNode(UnaryNode): predicate: ex.Expression + @property + def row_preserving(self) -> bool: + return False + def __hash__(self): return self._node_hash @@ -221,7 +250,13 @@ def __hash__(self): # TODO: Merge RowCount and Corr into Aggregate Node @dataclass(frozen=True) class RowCountNode(UnaryNode): - pass + @property + def row_preserving(self) -> bool: + return False + + @property + def non_local(self) -> bool: + return True @dataclass(frozen=True) @@ -230,6 +265,10 @@ class AggregateNode(UnaryNode): by_column_ids: typing.Tuple[str, ...] = tuple([]) dropna: bool = True + @property + def row_preserving(self) -> bool: + return False + def __hash__(self): return self._node_hash @@ -237,6 +276,10 @@ def __hash__(self): def peekable(self) -> bool: return False + @property + def non_local(self) -> bool: + return True + # TODO: Unify into aggregate @dataclass(frozen=True) @@ -246,10 +289,18 @@ class CorrNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def row_preserving(self) -> bool: + return False + @property def peekable(self) -> bool: return False + @property + def non_local(self) -> bool: + return True + @dataclass(frozen=True) class WindowOpNode(UnaryNode): @@ -267,6 +318,10 @@ def __hash__(self): def peekable(self) -> bool: return False + @property + def non_local(self) -> bool: + return True + @dataclass(frozen=True) class ReprojectOpNode(UnaryNode): @@ -290,6 +345,14 @@ class UnpivotNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def row_preserving(self) -> bool: + return False + + @property + def non_local(self) -> bool: + return True + @property def peekable(self) -> bool: return False @@ -303,5 +366,9 @@ class RandomSampleNode(UnaryNode): def deterministic(self) -> bool: return False + @property + def row_preserving(self) -> bool: + return False + def __hash__(self): return self._node_hash diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 3ab89e0213c..1fd5ab4e37b 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -92,6 +92,14 @@ class ExpressionOrdering: # Therefore, any modifications(or drops) done to these columns must result in hidden copies being made. total_ordering_columns: frozenset[str] = field(default_factory=frozenset) + @classmethod + def from_offset_col(cls, col: str) -> ExpressionOrdering: + return ExpressionOrdering( + (OrderingColumnReference(col),), + integer_encoding=IntegerEncoding(True, is_sequential=True), + total_ordering_columns=frozenset({col}), + ) + def with_non_sequential(self): """Create a copy that is marked as non-sequential. diff --git a/bigframes/core/traversal.py b/bigframes/core/traversal.py new file mode 100644 index 00000000000..b038ee6599c --- /dev/null +++ b/bigframes/core/traversal.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.core.nodes as nodes + + +def is_trivially_executable(node: nodes.BigFrameNode) -> bool: + if local_only(node): + return True + children_trivial = all(is_trivially_executable(child) for child in node.child_nodes) + self_trivial = (not node.non_local) and (node.row_preserving) + return children_trivial and self_trivial + + +def local_only(node: nodes.BigFrameNode) -> bool: + return all(isinstance(node, nodes.ReadLocalNode) for node in node.roots) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 2a20a4aabb4..a728a55d174 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -592,6 +592,8 @@ def __repr__(self) -> str: max_results = opts.max_rows if opts.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) + + self._cached() # TODO(swast): pass max_columns and get the true column count back. Maybe # get 1 more column than we have requested so that pandas can add the # ... for us? @@ -629,6 +631,8 @@ def _repr_html_(self) -> str: max_results = bigframes.options.display.max_rows if opts.repr_mode == "deferred": return formatter.repr_query_job_html(self.query_job) + + self._cached() # TODO(swast): pass max_columns and get the true column count back. Maybe # get 1 more column than we have requested so that pandas can add the # ... for us? @@ -3100,8 +3104,12 @@ def _set_block(self, block: blocks.Block): def _get_block(self) -> blocks.Block: return self._block - def _cached(self) -> DataFrame: - self._set_block(self._block.cached()) + def _cached(self, *, force: bool = False) -> DataFrame: + """Materialize dataframe to a temporary table. + No-op if the dataframe represents a trivial transformation of an existing materialization. + Force=True is used for BQML integration where need to copy data rather than use snapshot. + """ + self._set_block(self._block.cached(force=force)) return self _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 7c156b4cb79..266ab1b058d 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -247,9 +247,11 @@ def create_model( # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot if y_train is None: - input_data = X_train._cached() + input_data = X_train._cached(force=True) else: - input_data = X_train._cached().join(y_train._cached(), how="outer") + input_data = X_train._cached(force=True).join( + y_train._cached(force=True), how="outer" + ) options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session @@ -281,7 +283,9 @@ def create_time_series_model( options = dict(options) # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot - input_data = X_train._cached().join(y_train._cached(), how="outer") + input_data = X_train._cached(force=True).join( + y_train._cached(force=True), how="outer" + ) options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) diff --git a/bigframes/series.py b/bigframes/series.py index 6a217279759..c802fd24678 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -259,6 +259,7 @@ def __repr__(self) -> str: if opts.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) + self._cached() pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) self._set_internal_query_job(query_job) @@ -1521,8 +1522,8 @@ def _slice( ), ) - def _cached(self) -> Series: - self._set_block(self._block.cached()) + def _cached(self, *, force: bool = True) -> Series: + self._set_block(self._block.cached(force=force)) return self diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index bd813c8c6b3..15d4b3577b1 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -71,6 +71,7 @@ import bigframes.core.guid as guid from bigframes.core.ordering import IntegerEncoding, OrderingColumnReference import bigframes.core.ordering as orderings +import bigframes.core.traversal as traversals import bigframes.core.utils as utils import bigframes.dataframe as dataframe import bigframes.formatting_helpers as formatting_helpers @@ -1475,7 +1476,7 @@ def _start_query( results_iterator = query_job.result(max_results=max_results) return results_iterator, query_job - def _execute_and_cache( + def _cache_with_cluster_cols( self, array_value: core.ArrayValue, cluster_cols: typing.Sequence[str] ) -> core.ArrayValue: """Executes the query and uses the resulting table to rewrite future executions.""" @@ -1506,6 +1507,41 @@ def _execute_and_cache( ordering=compiled_value._ordering, ) + def _cache_with_offsets(self, array_value: core.ArrayValue) -> core.ArrayValue: + """Executes the query and uses the resulting table to rewrite future executions.""" + # TODO: Use this for all executions? Problem is that caching materializes extra + # ordering columns + compiled_value = self._compile_ordered(array_value) + + ibis_expr = compiled_value._to_ibis_expr( + ordering_mode="offset_col", order_col_name="bigframes_offsets" + ) + tmp_table = self._ibis_to_temp_table( + ibis_expr, cluster_cols=["bigframes_offsets"], api_name="cached" + ) + table_expression = self.ibis_client.table( + f"{tmp_table.project}.{tmp_table.dataset_id}.{tmp_table.table_id}" + ) + new_columns = [table_expression[column] for column in compiled_value.column_ids] + new_hidden_columns = [table_expression["bigframes_offsets"]] + # TODO: Instead, keep session-wide map of cached results and automatically reuse + return core.ArrayValue.from_ibis( + self, + table_expression, + columns=new_columns, + hidden_ordering_columns=new_hidden_columns, + ordering=orderings.ExpressionOrdering.from_offset_col("bigframes_offsets"), + ) + + def _is_trivially_executable(self, array_value: core.ArrayValue): + """ + Can the block be evaluated very cheaply? + If True, the array_value probably is not worth caching. + """ + # Once rewriting is available, will want to rewrite before + # evaluating execution cost. + return traversals.is_trivially_executable(array_value.node) + def _execute( self, array_value: core.ArrayValue, From b90241e6db8fa7b5dce29c5fc5e0d6d81d6595e6 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 31 Jan 2024 21:27:39 -0800 Subject: [PATCH 294/921] refactor: refactor block index properties (#312) * refactor: refactor block index properties * fix concat --- bigframes/core/block_transforms.py | 13 +- bigframes/core/blocks.py | 416 ++++++++++++++++++++++++----- bigframes/core/indexes/__init__.py | 3 +- bigframes/core/indexes/index.py | 297 +------------------- bigframes/core/reshape/__init__.py | 3 +- bigframes/dataframe.py | 67 ++--- bigframes/operations/base.py | 17 +- bigframes/series.py | 13 +- tests/unit/core/test_blocks.py | 4 +- 9 files changed, 408 insertions(+), 425 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 345adb6be3b..9cc0a05680c 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -38,8 +38,7 @@ def equals(block1: blocks.Block, block2: blocks.Block) -> bool: block1 = block1.reset_index(drop=False) block2 = block2.reset_index(drop=False) - joined, (lmap, rmap) = block1.index.join(block2.index, how="outer") - joined_block = joined._block + joined_block, (lmap, rmap) = block1.join(block2, how="outer") equality_ids = [] for lcol, rcol in zip(block1.value_columns, block2.value_columns): @@ -130,7 +129,7 @@ def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: if len(index_columns) != 1: raise ValueError("only method 'linear' supports multi-index") xvalues = block.index_columns[0] - if block.index_dtypes[0] not in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: + if block.index.dtypes[0] not in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: raise ValueError("Can only interpolate on numeric index.") for column in original_columns: @@ -743,14 +742,14 @@ def align_rows( right_block: blocks.Block, join: str = "outer", ): - joined_index, (get_column_left, get_column_right) = left_block.index.join( - right_block.index, how=join + joined_block, (get_column_left, get_column_right) = left_block.join( + right_block, how=join ) left_columns = [get_column_left[col] for col in left_block.value_columns] right_columns = [get_column_right[col] for col in right_block.value_columns] - left_block = joined_index._block.select_columns(left_columns) - right_block = joined_index._block.select_columns(right_columns) + left_block = joined_block.select_columns(left_columns) + right_block = joined_block.select_columns(right_columns) return left_block, right_block diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 8e9ceb83f76..49c4979a841 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -26,7 +26,7 @@ import itertools import random import typing -from typing import Iterable, List, Optional, Sequence, Tuple +from typing import Iterable, List, Mapping, Optional, Sequence, Tuple import warnings import google.cloud.bigquery as bigquery @@ -37,7 +37,6 @@ import bigframes.core as core import bigframes.core.expression as ex import bigframes.core.guid as guid -import bigframes.core.indexes as indexes import bigframes.core.join_def as join_defs import bigframes.core.ordering as ordering import bigframes.core.utils @@ -140,10 +139,41 @@ def __init__( self._stats_cache[" ".join(self.index_columns)] = {} + @classmethod + def from_local(cls, data) -> Block: + pd_data = pd.DataFrame(data) + columns = pd_data.columns + + # Make a flattened version to treat as a table. + if len(pd_data.columns.names) > 1: + pd_data.columns = columns.to_flat_index() + + index_labels = list(pd_data.index.names) + # The ArrayValue layer doesn't know about indexes, so make sure indexes + # are real columns with unique IDs. + pd_data = pd_data.reset_index( + names=[f"level_{level}" for level in range(len(index_labels))] + ) + pd_data = pd_data.set_axis( + vendored_pandas_io_common.dedup_names( + list(pd_data.columns), is_potential_multiindex=False + ), + axis="columns", + ) + index_ids = pd_data.columns[: len(index_labels)] + + keys_expr = core.ArrayValue.from_pandas(pd_data) + return cls( + keys_expr, + column_labels=columns, + index_columns=index_ids, + index_labels=index_labels, + ) + @property - def index(self) -> indexes.IndexValue: + def index(self) -> BlockIndexProperties: """Row identities for values in the Block.""" - return indexes.IndexValue(self) + return BlockIndexProperties(self) @functools.cached_property def shape(self) -> typing.Tuple[int, int]: @@ -167,11 +197,6 @@ def index_columns(self) -> Sequence[str]: """Column(s) to use as row labels.""" return self._index_columns - @property - def index_labels(self) -> Sequence[Label]: - """Name of column(s) to use as row labels.""" - return self._index_labels - @property def value_columns(self) -> Sequence[str]: """All value columns, mutually exclusive with index columns.""" @@ -197,13 +222,6 @@ def dtypes( """Returns the dtypes of the value columns.""" return [self.expr.get_column_type(col) for col in self.value_columns] - @property - def index_dtypes( - self, - ) -> Sequence[bigframes.dtypes.Dtype]: - """Returns the dtypes of the index columns.""" - return [self.expr.get_column_type(col) for col in self.index_columns] - @property def session(self) -> core.Session: return self._expr.session @@ -398,7 +416,7 @@ def reorder_levels(self, ids: typing.Sequence[str]): def _to_dataframe(self, result) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" - dtypes = dict(zip(self.index_columns, self.index_dtypes)) + dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) return self.session._rows_to_dataframe(result, dtypes) @@ -444,7 +462,7 @@ def try_peek(self, n: int = 20) -> typing.Optional[pd.DataFrame]: def to_pandas_batches(self): """Download results one message at a time.""" - dtypes = dict(zip(self.index_columns, self.index_dtypes)) + dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) results_iterator, _ = self.session._execute(self.expr, sorted=True) for arrow_table in results_iterator.to_arrow_iterable( @@ -897,7 +915,7 @@ def aggregate_all_and_stack( result_expr.drop_columns([offset_col]), self.index_columns, column_labels=[None], - index_labels=self.index_labels, + index_labels=self.index.names, ) def select_column(self, id: str) -> Block: @@ -1634,6 +1652,37 @@ def merge( expr = joined_expr.promote_offsets(offset_index_id) return Block(expr, index_columns=[offset_index_id], column_labels=labels) + def join( + self, + other: Block, + *, + how="left", + sort=False, + block_identity_join: bool = False, + ) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: + if not isinstance(other, Block): + # TODO(swast): We need to improve this error message to be more + # actionable for the user. For example, it's possible they + # could call set_index and try again to resolve this error. + raise ValueError( + f"Tried to join with an unexpected type: {type(other)}. {constants.FEEDBACK_LINK}" + ) + + # TODO(swast): Support cross-joins (requires reindexing). + if how not in {"outer", "left", "right", "inner"}: + raise NotImplementedError( + f"Only how='outer','left','right','inner' currently supported. {constants.FEEDBACK_LINK}" + ) + if self.index.nlevels == other.index.nlevels == 1: + return join_mono_indexed( + self, other, how=how, sort=sort, block_identity_join=block_identity_join + ) + else: + # Always sort mult-index join + return join_multi_indexed( + self, other, how=how, sort=sort, block_identity_join=block_identity_join + ) + def _force_reproject(self) -> Block: """Forces a reprojection of the underlying tables expression. Used to force predicate/order application before subsequent operations.""" return Block( @@ -1670,7 +1719,7 @@ def to_sql_query( return empty lists. """ array_value = self._expr - col_labels, idx_labels = list(self.column_labels), list(self.index_labels) + col_labels, idx_labels = list(self.column_labels), list(self.index.names) old_col_ids, old_idx_ids = list(self.value_columns), list(self.index_columns) if not include_index: @@ -1710,27 +1759,9 @@ def cached(self, *, optimize_offsets=False, force: bool = False) -> Block: expr, index_columns=self.index_columns, column_labels=self.column_labels, - index_labels=self.index_labels, + index_labels=self.index.names, ) - def resolve_index_level(self, level: LevelsType) -> typing.Sequence[str]: - if utils.is_list_like(level): - levels = list(level) - else: - levels = [level] - resolved_level_ids = [] - for level_ref in levels: - if isinstance(level_ref, int): - resolved_level_ids.append(self.index_columns[level_ref]) - elif isinstance(level_ref, typing.Hashable): - matching_ids = self.index_name_to_col_id.get(level_ref, []) - if len(matching_ids) != 1: - raise ValueError("level name cannot be found or is ambiguous") - resolved_level_ids.append(matching_ids[0]) - else: - raise ValueError(f"Unexpected level: {level_ref}") - return resolved_level_ids - def _is_monotonic( self, column_ids: typing.Union[str, Sequence[str]], increasing: bool ) -> bool: @@ -1787,42 +1818,301 @@ def _is_monotonic( return result -def block_from_local(data) -> Block: - pd_data = pd.DataFrame(data) - columns = pd_data.columns +class BlockIndexProperties: + """Accessor for the index-related block properties.""" - # Make a flattened version to treat as a table. - if len(pd_data.columns.names) > 1: - pd_data.columns = columns.to_flat_index() + def __init__(self, block: Block): + self._block = block - index_labels = list(pd_data.index.names) - # The ArrayValue layer doesn't know about indexes, so make sure indexes - # are real columns with unique IDs. - pd_data = pd_data.reset_index( - names=[f"level_{level}" for level in range(len(index_labels))] + @property + def _expr(self) -> core.ArrayValue: + return self._block.expr + + @property + def name(self) -> Label: + return self._block._index_labels[0] + + @property + def names(self) -> typing.Sequence[Label]: + return self._block._index_labels + + @property + def nlevels(self) -> int: + return len(self._block._index_columns) + + @property + def dtypes( + self, + ) -> typing.Sequence[bigframes.dtypes.Dtype]: + return [ + self._block.expr.get_column_type(col) for col in self._block.index_columns + ] + + @property + def session(self) -> core.Session: + return self._expr.session + + @property + def column_ids(self) -> Sequence[str]: + """Column(s) to use as row labels.""" + return self._block._index_columns + + def __repr__(self) -> str: + """Converts an Index to a string.""" + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + # TODO(swast): Avoid downloading the whole index by using job + # metadata, like we do with DataFrame. + preview = self.to_pandas() + return repr(preview) + + def to_pandas(self) -> pd.Index: + """Executes deferred operations and downloads the results.""" + # Project down to only the index column. So the query can be cached to visualize other data. + index_columns = list(self._block.index_columns) + dtypes = dict(zip(index_columns, self.dtypes)) + expr = self._expr.select_columns(index_columns) + results, _ = self.session._execute(expr) + df = expr.session._rows_to_dataframe(results, dtypes) + df = df.set_index(index_columns) + index = df.index + index.names = list(self._block._index_labels) + return index + + def resolve_level(self, level: LevelsType) -> typing.Sequence[str]: + if utils.is_list_like(level): + levels = list(level) + else: + levels = [level] + resolved_level_ids = [] + for level_ref in levels: + if isinstance(level_ref, int): + resolved_level_ids.append(self._block.index_columns[level_ref]) + elif isinstance(level_ref, typing.Hashable): + matching_ids = self._block.index_name_to_col_id.get(level_ref, []) + if len(matching_ids) != 1: + raise ValueError("level name cannot be found or is ambiguous") + resolved_level_ids.append(matching_ids[0]) + else: + raise ValueError(f"Unexpected level: {level_ref}") + return resolved_level_ids + + def resolve_level_exact(self: BlockIndexProperties, label: Label) -> str: + matches = self._block.index_name_to_col_id.get(label, []) + if len(matches) > 1: + raise ValueError(f"Ambiguous index level name {label}") + if len(matches) == 0: + raise ValueError(f"Cannot resolve index level name {label}") + return matches[0] + + def is_uniquely_named(self: BlockIndexProperties): + return len(set(self.names)) == len(self.names) + + +def join_mono_indexed( + left: Block, + right: Block, + *, + how="left", + sort=False, + block_identity_join: bool = False, +) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: + left_expr = left.expr + right_expr = right.expr + left_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.LEFT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in left_expr.column_ids + ] + right_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.RIGHT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in right_expr.column_ids + ] + + join_def = join_defs.JoinDefinition( + conditions=( + join_defs.JoinCondition(left.index_columns[0], right.index_columns[0]), + ), + mappings=(*left_mappings, *right_mappings), + type=how, ) - pd_data = pd_data.set_axis( - vendored_pandas_io_common.dedup_names( - list(pd_data.columns), is_potential_multiindex=False + combined_expr = left_expr.join( + right_expr, + join_def=join_def, + allow_row_identity_join=(not block_identity_join), + ) + get_column_left = join_def.get_left_mapping() + get_column_right = join_def.get_right_mapping() + # Drop original indices from each side. and used the coalesced combination generated by the join. + left_index = get_column_left[left.index_columns[0]] + right_index = get_column_right[right.index_columns[0]] + # Drop original indices from each side. and used the coalesced combination generated by the join. + combined_expr, coalesced_join_cols = coalesce_columns( + combined_expr, [left_index], [right_index], how=how + ) + if sort: + combined_expr = combined_expr.order_by( + [ordering.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] + ) + block = Block( + combined_expr, + index_columns=coalesced_join_cols, + column_labels=[*left.column_labels, *right.column_labels], + index_labels=[left.index.name] + if left.index.name == right.index.name + else [None], + ) + return ( + block, + (get_column_left, get_column_right), + ) + + +def join_multi_indexed( + left: Block, + right: Block, + *, + how="left", + sort=False, + block_identity_join: bool = False, +) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: + if not (left.index.is_uniquely_named() and right.index.is_uniquely_named()): + raise ValueError("Joins not supported on indices with non-unique level names") + + common_names = [name for name in left.index.names if name in right.index.names] + if len(common_names) == 0: + raise ValueError("Cannot join without a index level in common.") + + left_only_names = [ + name for name in left.index.names if name not in right.index.names + ] + right_only_names = [ + name for name in right.index.names if name not in left.index.names + ] + + left_join_ids = [left.index.resolve_level_exact(name) for name in common_names] + right_join_ids = [right.index.resolve_level_exact(name) for name in common_names] + + names_fully_match = len(left_only_names) == 0 and len(right_only_names) == 0 + + left_expr = left.expr + right_expr = right.expr + + left_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.LEFT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in left_expr.column_ids + ] + right_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.RIGHT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in right_expr.column_ids + ] + + join_def = join_defs.JoinDefinition( + conditions=tuple( + join_defs.JoinCondition(left, right) + for left, right in zip(left_join_ids, right_join_ids) ), - axis="columns", + mappings=(*left_mappings, *right_mappings), + type=how, ) - index_ids = pd_data.columns[: len(index_labels)] - keys_expr = core.ArrayValue.from_pandas(pd_data) - return Block( - keys_expr, - column_labels=columns, - index_columns=index_ids, + combined_expr = left_expr.join( + right_expr, + join_def=join_def, + # If we're only joining on a subset of the index columns, we need to + # perform a true join. + allow_row_identity_join=(names_fully_match and not block_identity_join), + ) + get_column_left = join_def.get_left_mapping() + get_column_right = join_def.get_right_mapping() + left_ids_post_join = [get_column_left[id] for id in left_join_ids] + right_ids_post_join = [get_column_right[id] for id in right_join_ids] + # Drop original indices from each side. and used the coalesced combination generated by the join. + combined_expr, coalesced_join_cols = coalesce_columns( + combined_expr, left_ids_post_join, right_ids_post_join, how=how + ) + if sort: + combined_expr = combined_expr.order_by( + [ordering.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] + ) + + if left.index.nlevels == 1: + index_labels = right.index.names + elif right.index.nlevels == 1: + index_labels = left.index.names + else: + index_labels = [*common_names, *left_only_names, *right_only_names] + + def resolve_label_id(label: Label) -> str: + # if name is shared between both blocks, coalesce the values + if label in common_names: + return coalesced_join_cols[common_names.index(label)] + if label in left_only_names: + return get_column_left[left.index.resolve_level_exact(label)] + if label in right_only_names: + return get_column_right[right.index.resolve_level_exact(label)] + raise ValueError(f"Unexpected label: {label}") + + index_columns = [resolve_label_id(label) for label in index_labels] + + block = Block( + combined_expr, + index_columns=index_columns, + column_labels=[*left.column_labels, *right.column_labels], index_labels=index_labels, ) + return ( + block, + (get_column_left, get_column_right), + ) + + +def coalesce_columns( + expr: core.ArrayValue, + left_ids: typing.Sequence[str], + right_ids: typing.Sequence[str], + how: str, +) -> Tuple[core.ArrayValue, Sequence[str]]: + result_ids = [] + for left_id, right_id in zip(left_ids, right_ids): + if how == "left" or how == "inner": + result_ids.append(left_id) + expr = expr.drop_columns([right_id]) + elif how == "right": + result_ids.append(right_id) + expr = expr.drop_columns([left_id]) + elif how == "outer": + coalesced_id = guid.generate_guid() + expr = expr.project_to_id( + ops.coalesce_op.as_expr(left_id, right_id), coalesced_id + ) + expr = expr.drop_columns([left_id, right_id]) + result_ids.append(coalesced_id) + else: + raise ValueError(f"Unexpected join type: {how}. {constants.FEEDBACK_LINK}") + return expr, result_ids def _cast_index(block: Block, dtypes: typing.Sequence[bigframes.dtypes.Dtype]): original_block = block result_ids = [] for idx_id, idx_dtype, target_dtype in zip( - block.index_columns, block.index_dtypes, dtypes + block.index_columns, block.index.dtypes, dtypes ): if idx_dtype != target_dtype: block, result_id = block.apply_unary_op(idx_id, ops.AsTypeOp(target_dtype)) @@ -1835,10 +2125,12 @@ def _cast_index(block: Block, dtypes: typing.Sequence[bigframes.dtypes.Dtype]): expr, index_columns=result_ids, column_labels=original_block.column_labels, - index_labels=original_block.index_labels, + index_labels=original_block.index.names, ) +### Schema alignment Utils +### TODO: Pull out to separate module? def _align_block_to_schema( block: Block, schema: dict[Label, bigframes.dtypes.Dtype] ) -> Block: diff --git a/bigframes/core/indexes/__init__.py b/bigframes/core/indexes/__init__.py index 184a9ce262f..6419d0985af 100644 --- a/bigframes/core/indexes/__init__.py +++ b/bigframes/core/indexes/__init__.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from bigframes.core.indexes.index import Index, IndexValue +from bigframes.core.indexes.index import Index __all__ = [ "Index", - "IndexValue", ] diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 78a4fc6f0bd..5b2fa4ebb7a 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -17,23 +17,19 @@ from __future__ import annotations import typing -from typing import Hashable, Mapping, Optional, Sequence, Tuple, Union +from typing import Hashable, Optional, Sequence, Union import google.cloud.bigquery as bigquery import numpy as np import pandas import bigframes.constants as constants -import bigframes.core as core import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.expression as ex -import bigframes.core.guid -import bigframes.core.join_def as join_defs import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.dtypes -import bigframes.dtypes as bf_dtypes import bigframes.formatting_helpers as formatter import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -125,12 +121,12 @@ def shape(self) -> typing.Tuple[int]: @property def dtype(self): - return self._block.index_dtypes[0] if self.nlevels == 1 else np.dtype("O") + return self._block.index.dtypes[0] if self.nlevels == 1 else np.dtype("O") @property def dtypes(self) -> pandas.Series: return pandas.Series( - data=self._block.index_dtypes, index=self._block.index_labels # type:ignore + data=self._block.index.dtypes, index=self._block.index.names # type:ignore ) @property @@ -422,7 +418,7 @@ def _apply_unary_expr( block, result_id = block.project_expr(op.rename({unbound_variable: col})) result_ids.append(result_id) - block = block.set_index(result_ids, index_labels=self._block.index_labels) + block = block.set_index(result_ids, index_labels=self._block.index.names) return Index(block) def _apply_aggregation(self, op: agg_ops.AggregateOp) -> typing.Any: @@ -450,7 +446,7 @@ def to_pandas(self) -> pandas.Index: pandas.Index: A pandas Index with all of the labels from this Index. """ - return IndexValue(self._block).to_pandas() + return self._block.index.to_pandas() def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: return self.to_pandas().to_numpy(dtype, **kwargs) @@ -490,286 +486,3 @@ def names(self, values: typing.Sequence[blocks.Label]): new_block = self._whole_frame._get_block().with_index_labels(values) self._whole_frame._set_block(new_block) self._block = new_block - - -class IndexValue: - """An immutable index.""" - - def __init__(self, block: blocks.Block): - self._block = block - - @property - def _expr(self) -> core.ArrayValue: - return self._block.expr - - @property - def name(self) -> blocks.Label: - return self._block._index_labels[0] - - @property - def names(self) -> typing.Sequence[blocks.Label]: - return self._block._index_labels - - @property - def nlevels(self) -> int: - return len(self._block._index_columns) - - @property - def dtypes( - self, - ) -> typing.Sequence[typing.Union[bf_dtypes.Dtype, np.dtype[typing.Any]]]: - return self._block.index_dtypes - - @property - def session(self) -> core.Session: - return self._expr.session - - def to_pandas(self) -> pandas.Index: - """Executes deferred operations and downloads the results.""" - # Project down to only the index column. So the query can be cached to visualize other data. - index_columns = list(self._block.index_columns) - dtypes = dict(zip(index_columns, self.dtypes)) - expr = self._expr.select_columns(index_columns) - results, _ = self.session._execute(expr) - df = expr.session._rows_to_dataframe(results, dtypes) - df = df.set_index(index_columns) - index = df.index - index.names = list(self._block._index_labels) - return index - - def join( - self, - other: IndexValue, - *, - how="left", - sort=False, - block_identity_join: bool = False, - ) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: - if not isinstance(other, IndexValue): - # TODO(swast): We need to improve this error message to be more - # actionable for the user. For example, it's possible they - # could call set_index and try again to resolve this error. - raise ValueError( - f"Tried to join with an unexpected type: {type(other)}. {constants.FEEDBACK_LINK}" - ) - - # TODO(swast): Support cross-joins (requires reindexing). - if how not in {"outer", "left", "right", "inner"}: - raise NotImplementedError( - f"Only how='outer','left','right','inner' currently supported. {constants.FEEDBACK_LINK}" - ) - if self.nlevels == other.nlevels == 1: - return join_mono_indexed( - self, other, how=how, sort=sort, block_identity_join=block_identity_join - ) - else: - # Always sort mult-index join - return join_multi_indexed( - self, other, how=how, sort=sort, block_identity_join=block_identity_join - ) - - def resolve_level_name(self: IndexValue, label: blocks.Label) -> str: - matches = self._block.index_name_to_col_id.get(label, []) - if len(matches) > 1: - raise ValueError(f"Ambiguous index level name {label}") - if len(matches) == 0: - raise ValueError(f"Cannot resolve index level name {label}") - return matches[0] - - def is_uniquely_named(self: IndexValue): - return len(set(self.names)) == len(self.names) - - -def join_mono_indexed( - left: IndexValue, - right: IndexValue, - *, - how="left", - sort=False, - block_identity_join: bool = False, -) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: - left_expr = left._block.expr - right_expr = right._block.expr - left_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.LEFT, - source_id=id, - destination_id=bigframes.core.guid.generate_guid(), - ) - for id in left_expr.column_ids - ] - right_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.RIGHT, - source_id=id, - destination_id=bigframes.core.guid.generate_guid(), - ) - for id in right_expr.column_ids - ] - - join_def = join_defs.JoinDefinition( - conditions=( - join_defs.JoinCondition( - left._block.index_columns[0], right._block.index_columns[0] - ), - ), - mappings=(*left_mappings, *right_mappings), - type=how, - ) - combined_expr = left_expr.join( - right_expr, - join_def=join_def, - allow_row_identity_join=(not block_identity_join), - ) - get_column_left = join_def.get_left_mapping() - get_column_right = join_def.get_right_mapping() - # Drop original indices from each side. and used the coalesced combination generated by the join. - left_index = get_column_left[left._block.index_columns[0]] - right_index = get_column_right[right._block.index_columns[0]] - # Drop original indices from each side. and used the coalesced combination generated by the join. - combined_expr, coalesced_join_cols = coalesce_columns( - combined_expr, [left_index], [right_index], how=how - ) - if sort: - combined_expr = combined_expr.order_by( - [order.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] - ) - block = blocks.Block( - combined_expr, - index_columns=coalesced_join_cols, - column_labels=[*left._block.column_labels, *right._block.column_labels], - index_labels=[left.name] if left.name == right.name else [None], - ) - return ( - typing.cast(IndexValue, block.index), - (get_column_left, get_column_right), - ) - - -def join_multi_indexed( - left: IndexValue, - right: IndexValue, - *, - how="left", - sort=False, - block_identity_join: bool = False, -) -> Tuple[IndexValue, Tuple[Mapping[str, str], Mapping[str, str]],]: - if not (left.is_uniquely_named() and right.is_uniquely_named()): - raise ValueError("Joins not supported on indices with non-unique level names") - - common_names = [name for name in left.names if name in right.names] - if len(common_names) == 0: - raise ValueError("Cannot join without a index level in common.") - - left_only_names = [name for name in left.names if name not in right.names] - right_only_names = [name for name in right.names if name not in left.names] - - left_join_ids = [left.resolve_level_name(name) for name in common_names] - right_join_ids = [right.resolve_level_name(name) for name in common_names] - - names_fully_match = len(left_only_names) == 0 and len(right_only_names) == 0 - - left_expr = left._block.expr - right_expr = right._block.expr - - left_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.LEFT, - source_id=id, - destination_id=bigframes.core.guid.generate_guid(), - ) - for id in left_expr.column_ids - ] - right_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.RIGHT, - source_id=id, - destination_id=bigframes.core.guid.generate_guid(), - ) - for id in right_expr.column_ids - ] - - join_def = join_defs.JoinDefinition( - conditions=tuple( - join_defs.JoinCondition(left, right) - for left, right in zip(left_join_ids, right_join_ids) - ), - mappings=(*left_mappings, *right_mappings), - type=how, - ) - - combined_expr = left_expr.join( - right_expr, - join_def=join_def, - # If we're only joining on a subset of the index columns, we need to - # perform a true join. - allow_row_identity_join=(names_fully_match and not block_identity_join), - ) - get_column_left = join_def.get_left_mapping() - get_column_right = join_def.get_right_mapping() - left_ids_post_join = [get_column_left[id] for id in left_join_ids] - right_ids_post_join = [get_column_right[id] for id in right_join_ids] - # Drop original indices from each side. and used the coalesced combination generated by the join. - combined_expr, coalesced_join_cols = coalesce_columns( - combined_expr, left_ids_post_join, right_ids_post_join, how=how - ) - if sort: - combined_expr = combined_expr.order_by( - [order.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] - ) - - if left.nlevels == 1: - index_labels = right.names - elif right.nlevels == 1: - index_labels = left.names - else: - index_labels = [*common_names, *left_only_names, *right_only_names] - - def resolve_label_id(label: blocks.Label) -> str: - # if name is shared between both blocks, coalesce the values - if label in common_names: - return coalesced_join_cols[common_names.index(label)] - if label in left_only_names: - return get_column_left[left.resolve_level_name(label)] - if label in right_only_names: - return get_column_right[right.resolve_level_name(label)] - raise ValueError(f"Unexpected label: {label}") - - index_columns = [resolve_label_id(label) for label in index_labels] - - block = blocks.Block( - combined_expr, - index_columns=index_columns, - column_labels=[*left._block.column_labels, *right._block.column_labels], - index_labels=index_labels, - ) - return ( - typing.cast(IndexValue, block.index), - (get_column_left, get_column_right), - ) - - -def coalesce_columns( - expr: core.ArrayValue, - left_ids: typing.Sequence[str], - right_ids: typing.Sequence[str], - how: str, -) -> Tuple[core.ArrayValue, Sequence[str]]: - result_ids = [] - for left_id, right_id in zip(left_ids, right_ids): - if how == "left" or how == "inner": - result_ids.append(left_id) - expr = expr.drop_columns([right_id]) - elif how == "right": - result_ids.append(right_id) - expr = expr.drop_columns([left_id]) - elif how == "outer": - coalesced_id = bigframes.core.guid.generate_guid() - expr = expr.project_to_id( - ops.coalesce_op.as_expr(left_id, right_id), coalesced_id - ) - expr = expr.drop_columns([left_id, right_id]) - result_ids.append(coalesced_id) - else: - raise ValueError(f"Unexpected join type: {how}. {constants.FEEDBACK_LINK}") - return expr, result_ids diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index 4a3bb16a392..22e7d40c81e 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -104,8 +104,7 @@ def concat( block_list = [obj._block for obj in objs] block = block_list[0] for rblock in block_list[1:]: - combined_index, _ = block.index.join(rblock.index, how=join) - block = combined_index._block + block, _ = block.join(rblock, how=join) return bigframes.dataframe.DataFrame(block) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a728a55d174..2729d237017 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -140,22 +140,19 @@ def __init__( other_block = other._block.with_column_labels([key]) # Pandas will keep original sorting if all indices are aligned. # We cannot detect this easily however, and so always sort on index - result_index, _ = block.index.join( # type:ignore - other_block.index, how="outer", sort=True + block, _ = block.join( # type:ignore + other_block, how="outer", sort=True ) - block = result_index._block if block: if index is not None: bf_index = indexes.Index(index) idx_block = bf_index._block idx_cols = idx_block.index_columns - join_idx, (_, r_mapping) = block.reset_index().index.join( - bf_index._block.reset_index().index, how="inner" - ) - block = join_idx._block.set_index( - [r_mapping[idx_col] for idx_col in idx_cols] + block, (_, r_mapping) = block.reset_index().join( + bf_index._block.reset_index(), how="inner" ) + block = block.set_index([r_mapping[idx_col] for idx_col in idx_cols]) if columns: block = block.select_columns(list(columns)) # type:ignore if dtype: @@ -182,7 +179,7 @@ def __init__( if isinstance(dt, pandas.ArrowDtype) ) ): - self._block = blocks.block_from_local(pd_dataframe) + self._block = blocks.Block.from_local(pd_dataframe) elif session: self._block = session.read_pandas(pd_dataframe)._get_block() else: @@ -538,8 +535,8 @@ def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: combined_index, ( get_column_left, get_column_right, - ) = self._block.index.join(key._block.index, how="left") - block = combined_index._block + ) = self._block.join(key._block, how="left") + block = combined_index filter_col_id = get_column_right[key._value_column] block = block.filter(filter_col_id) block = block.drop_columns([filter_col_id]) @@ -721,13 +718,12 @@ def _apply_series_binop( f"Row Series operations haven't been supported. {constants.FEEDBACK_LINK}" ) - joined_index, (get_column_left, get_column_right) = self._block.index.join( - other._block.index, how=how + block, (get_column_left, get_column_right) = self._block.join( + other._block, how=how ) series_column_id = other._value_column series_col = get_column_right[series_column_id] - block = joined_index._block for column_id, label in zip( self._block.value_columns, self._block.column_labels ): @@ -752,8 +748,8 @@ def _apply_dataframe_binop( reverse: bool = False, ) -> DataFrame: # Join rows - joined_index, (get_column_left, get_column_right) = self._block.index.join( - other._block.index, how=how + block, (get_column_left, get_column_right) = self._block.join( + other._block, how=how ) # join columns schema # indexers will be none for exact match @@ -762,7 +758,6 @@ def _apply_dataframe_binop( ) binop_result_ids = [] - block = joined_index._block column_indices = zip( lcol_indexer if (lcol_indexer is not None) else range(len(columns)), @@ -1202,12 +1197,10 @@ def drop( def _drop_by_index(self, index: indexes.Index) -> DataFrame: block = index._block block, ordering_col = block.promote_offsets() - joined_index, (get_column_left, get_column_right) = self._block.index.join( - block.index - ) + joined_index, (get_column_left, get_column_right) = self._block.join(block) new_ordering_col = get_column_right[ordering_col] - drop_block = joined_index._block + drop_block = joined_index drop_block, drop_col = drop_block.apply_unary_op( new_ordering_col, ops.isnull_op, @@ -1266,7 +1259,7 @@ def reorder_levels(self, order: LevelsType, axis: int | str = 0): raise ValueError("Columns must be a multiindex to reorder levels.") def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: - return self._block.resolve_index_level(level) + return self._block.index.resolve_level(level) def rename(self, *, columns: Mapping[blocks.Label, blocks.Label]) -> DataFrame: block = self._block.rename(columns=columns) @@ -1349,16 +1342,16 @@ def _assign_single_item_listlike(self, k: str, v: Sequence) -> DataFrame: raise ValueError( "Assigning listlike to a first column under multiindex is not supported." ) - result_block = new_column_block.with_index_labels(self._block.index_labels) + result_block = new_column_block.with_index_labels(self._block.index.names) result_block = result_block.with_column_labels([k]) else: - result_index, (get_column_left, get_column_right,) = self_block.index.join( - new_column_block.index, how="left", block_identity_join=True - ) - result_block = result_index._block + result_block, ( + get_column_left, + get_column_right, + ) = self_block.join(new_column_block, how="left", block_identity_join=True) result_block = result_block.set_index( [get_column_left[col_id] for col_id in original_index_column_ids], - index_labels=self._block.index_labels, + index_labels=self._block.index.names, ) src_col = get_column_right[new_column_block.value_columns[0]] # Check to see if key exists, and modify in place @@ -1386,14 +1379,13 @@ def _assign_scalar(self, label: str, value: Union[int, float]) -> DataFrame: def _assign_series_join_on_index( self, label: str, series: bigframes.series.Series ) -> DataFrame: - joined_index, (get_column_left, get_column_right) = self._block.index.join( - series._block.index, how="left" + block, (get_column_left, get_column_right) = self._block.join( + series._block, how="left" ) column_ids = [ get_column_left[col_id] for col_id in self._block.cols_matching_label(label) ] - block = joined_index._block source_column = get_column_right[series._value_column] # Replace each column matching the label @@ -2311,10 +2303,8 @@ def join( return left._perform_join_by_index(right, how=how) def _perform_join_by_index(self, other: DataFrame, *, how: str = "left"): - combined_index, _ = self._block.index.join( - other._block.index, how=how, block_identity_join=True - ) - return DataFrame(combined_index._block) + block, _ = self._block.join(other._block, how=how, block_identity_join=True) + return DataFrame(block) 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. @@ -2384,17 +2374,14 @@ def _groupby_series( col_ids: typing.Sequence[str] = [] for key in by: if isinstance(key, bigframes.series.Series): - combined_index, ( + block, ( get_column_left, get_column_right, - ) = block.index.join( - key._block.index, how="inner" if dropna else "left" - ) + ) = block.join(key._block, how="inner" if dropna else "left") col_ids = [ *[get_column_left[value] for value in col_ids], get_column_right[key._value_column], ] - block = combined_index._block else: # Interpret as index level or column name col_matches = block.label_to_col_id.get(key, []) diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 6829d3faab5..4aad9479e7a 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -65,8 +65,8 @@ def __init__( bf_index = indexes.Index(index) idx_block = bf_index._block idx_cols = idx_block.value_columns - block_idx, _ = idx_block.index.join(block.index, how="left") - block = block_idx._block.with_index_labels(bf_index.names) + block_idx, _ = idx_block.join(block, how="left") + block = block_idx.with_index_labels(bf_index.names) elif isinstance(data, indexes.Index): if data.nlevels != 1: @@ -78,10 +78,8 @@ def __init__( bf_index = indexes.Index(index) idx_block = bf_index._block.reset_index(drop=False) idx_cols = idx_block.value_columns - block_idx, (l_mapping, _) = idx_block.index.join( - block.index, how="left" - ) - block = block_idx._block.set_index([l_mapping[col] for col in idx_cols]) + block, (l_mapping, _) = idx_block.join(block, how="left") + block = block.set_index([l_mapping[col] for col in idx_cols]) block = block.with_index_labels(bf_index.names) if block: @@ -114,7 +112,7 @@ def __init__( if isinstance(dt, pd.ArrowDtype) ) ): - block = blocks.block_from_local(pd_dataframe) + block = blocks.Block.from_local(pd_dataframe) elif session: block = session.read_pandas(pd_dataframe)._get_block() else: @@ -214,15 +212,14 @@ def _align_n( block = self._block for other in others: if isinstance(other, series.Series): - combined_index, ( + block, ( get_column_left, get_column_right, - ) = block.index.join(other._block.index, how=how) + ) = block.join(other._block, how=how) value_ids = [ *[get_column_left[value] for value in value_ids], get_column_right[other._value_column], ] - block = combined_index._block else: # Will throw if can't interpret as scalar. dtype = typing.cast(bigframes.dtypes.Dtype, self._dtype) diff --git a/bigframes/series.py b/bigframes/series.py index c802fd24678..649851e716e 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -183,7 +183,7 @@ def rename( block = self._block for k, v in index.items(): new_idx_ids = [] - for idx_id, idx_dtype in zip(block.index_columns, block.index_dtypes): + for idx_id, idx_dtype in zip(block.index_columns, block.index.dtypes): # Will throw if key type isn't compatible with index type, which leads to invalid SQL. block.create_constant(k, dtype=idx_dtype) @@ -199,7 +199,7 @@ def rename( new_idx_ids.append(new_idx_id) block = block.drop_columns([const_id, cond_id]) - block = block.set_index(new_idx_ids, index_labels=block.index_labels) + block = block.set_index(new_idx_ids, index_labels=block.index.names) return Series(block) @@ -369,7 +369,7 @@ def reorder_levels(self, order: LevelsType, axis: int | str = 0): return Series(self._block.reorder_levels(resolved_level_ids)) def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]: - return self._block.resolve_index_level(level) + return self._block.index.resolve_level(level) def between(self, left, right, inclusive="both"): if inclusive not in ["both", "neither", "left", "right"]: @@ -1180,19 +1180,16 @@ def _groupby_values( value_col = self._value_column for key in by: if isinstance(key, Series): - combined_index, ( + block, ( get_column_left, get_column_right, - ) = block.index.join( - key._block.index, how="inner" if dropna else "left" - ) + ) = block.join(key._block, how="inner" if dropna else "left") value_col = get_column_left[self._value_column] grouping_cols = [ *[get_column_left[value] for value in grouping_cols], get_column_right[key._value_column], ] - block = combined_index._block else: # Interpret as index level matches = block.index_name_to_col_id.get(key, []) diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py index 86715d090cb..5a4f0951d3d 100644 --- a/tests/unit/core/test_blocks.py +++ b/tests/unit/core/test_blocks.py @@ -75,8 +75,8 @@ def test_block_from_local(data): expected = pandas.DataFrame(data) - block = blocks.block_from_local(data) + block = blocks.Block.from_local(data) pandas.testing.assert_index_equal(block.column_labels, expected.columns) - assert tuple(block.index_labels) == tuple(expected.index.names) + assert tuple(block.index.names) == tuple(expected.index.names) assert block.shape == expected.shape From 04ce3be37fd3a06e956f39ff6ff50b27aa79cb1a Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 2 Feb 2024 11:18:41 -0600 Subject: [PATCH 295/921] chore: add test data in preparation for `explode()` and similar methods (#367) --- tests/data/nested.jsonl | 100 +++++++++++++++++++++++++++ tests/data/nested_schema.json | 56 +++++++++++++++ tests/system/conftest.py | 28 ++++++++ tests/system/small/test_dataframe.py | 39 ++++++++++- 4 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 tests/data/nested.jsonl create mode 100644 tests/data/nested_schema.json diff --git a/tests/data/nested.jsonl b/tests/data/nested.jsonl new file mode 100644 index 00000000000..a71e9b1db1a --- /dev/null +++ b/tests/data/nested.jsonl @@ -0,0 +1,100 @@ +{"rowindex":0,"customer_id":"jkl","day":"2023-12-18","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-18 03:43:58","data":[{"key":"x","value":20.2533015856},{"key":"y","value":42.8363462389}]},{"category":"D","timestamp":"2023-12-18 07:15:37","data":[{"key":"x","value":62.0762664928},{"key":"z","value":83.6655402432}]}]} +{"rowindex":1,"customer_id":"def","day":"2023-12-18","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-18 23:11:11","data":[{"key":"w","value":36.1388065179}]},{"category":"B","timestamp":"2023-12-18 07:12:50","data":[{"key":"z","value":68.7673488304}]},{"category":"D","timestamp":"2023-12-18 09:09:03","data":[{"key":"x","value":57.4139647019}]},{"category":"C","timestamp":"2023-12-18 13:05:30","data":[{"key":"z","value":36.087871201}]}]} +{"rowindex":2,"customer_id":"abc","day":"2023-12-6","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-06 10:37:11","data":[]},{"category":"A","timestamp":"2023-12-06 03:35:44","data":[]},{"category":"D","timestamp":"2023-12-06 13:10:57","data":[{"key":"z","value":21.8487807658}]},{"category":"B","timestamp":"2023-12-06 01:39:16","data":[{"key":"y","value":1.6380505139}]}]} +{"rowindex":3,"customer_id":"mno","day":"2023-12-16","flag":2,"event_sequence":[]} +{"rowindex":4,"customer_id":"jkl","day":"2023-12-1","flag":1,"event_sequence":[{"category":"C","timestamp":"2023-12-01 22:29:35","data":[]}]} +{"rowindex":5,"customer_id":"mno","day":"2023-12-8","flag":2,"event_sequence":[{"category":"C","timestamp":"2023-12-08 19:56:43","data":[{"key":"z","value":64.0025360397}]},{"category":"A","timestamp":"2023-12-08 00:43:53","data":[{"key":"z","value":62.5030923507},{"key":"y","value":67.4517590972}]}]} +{"rowindex":6,"customer_id":"abc","day":"2023-12-3","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-03 10:04:48","data":[{"key":"x","value":73.0494929425},{"key":"z","value":81.1761568104}]}]} +{"rowindex":7,"customer_id":"abc","day":"2023-12-6","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-06 16:50:15","data":[{"key":"w","value":46.395435162},{"key":"y","value":7.8421775851}]},{"category":"A","timestamp":"2023-12-06 05:55:01","data":[]},{"category":"B","timestamp":"2023-12-06 15:24:08","data":[{"key":"x","value":37.5351196265},{"key":"w","value":65.4896295524}]}]} +{"rowindex":8,"customer_id":"jkl","day":"2023-12-8","flag":2,"event_sequence":[{"category":"A","timestamp":"2023-12-08 00:21:23","data":[{"key":"w","value":42.4467608939},{"key":"x","value":81.083558253}]},{"category":"A","timestamp":"2023-12-08 09:31:05","data":[]},{"category":"C","timestamp":"2023-12-08 01:42:37","data":[{"key":"y","value":55.1881250973}]},{"category":"C","timestamp":"2023-12-08 21:14:46","data":[{"key":"z","value":12.0833253151}]},{"category":"D","timestamp":"2023-12-08 21:38:25","data":[{"key":"y","value":59.9482432021}]}]} +{"rowindex":9,"customer_id":"jkl","day":"2023-12-5","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-05 09:46:09","data":[{"key":"w","value":48.5204042398}]},{"category":"C","timestamp":"2023-12-05 03:44:30","data":[{"key":"y","value":49.3712140658}]}]} +{"rowindex":10,"customer_id":"mno","day":"2023-12-1","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-01 00:53:03","data":[{"key":"w","value":19.1753301515},{"key":"z","value":90.1966084522}]},{"category":"B","timestamp":"2023-12-01 15:18:15","data":[{"key":"w","value":28.4831052842},{"key":"y","value":74.3676328239}]},{"category":"D","timestamp":"2023-12-01 18:35:06","data":[{"key":"w","value":50.9000130431}]},{"category":"A","timestamp":"2023-12-01 19:10:15","data":[{"key":"x","value":36.4073472229},{"key":"y","value":2.5800142072}]}]} +{"rowindex":11,"customer_id":"abc","day":"2023-12-7","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-07 03:28:37","data":[]},{"category":"D","timestamp":"2023-12-07 03:00:47","data":[{"key":"z","value":42.5078083149},{"key":"w","value":0.3430387149}]}]} +{"rowindex":12,"customer_id":"jkl","day":"2023-12-16","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-16 20:28:48","data":[{"key":"y","value":99.4511527722}]}]} +{"rowindex":13,"customer_id":"ghi","day":"2023-12-18","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-18 00:35:24","data":[{"key":"w","value":30.3520969504}]},{"category":"B","timestamp":"2023-12-18 10:45:35","data":[]},{"category":"C","timestamp":"2023-12-18 18:39:11","data":[{"key":"z","value":93.486287241}]},{"category":"C","timestamp":"2023-12-18 18:55:30","data":[{"key":"y","value":20.2247125873}]}]} +{"rowindex":14,"customer_id":"abc","day":"2023-12-14","flag":2,"event_sequence":[{"category":"A","timestamp":"2023-12-14 04:48:13","data":[]},{"category":"B","timestamp":"2023-12-14 07:39:40","data":[]},{"category":"D","timestamp":"2023-12-14 22:08:13","data":[{"key":"x","value":31.3054147446},{"key":"y","value":32.9881809276}]},{"category":"A","timestamp":"2023-12-14 23:02:18","data":[{"key":"x","value":41.4514710087},{"key":"w","value":71.0759384863}]}]} +{"rowindex":15,"customer_id":"def","day":"2023-12-14","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-14 18:34:07","data":[{"key":"w","value":82.4015077053},{"key":"x","value":80.8508070787}]},{"category":"B","timestamp":"2023-12-14 10:08:52","data":[{"key":"y","value":91.3558143519},{"key":"w","value":42.8103570355}]}]} +{"rowindex":16,"customer_id":"mno","day":"2023-12-7","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-07 07:07:38","data":[]},{"category":"A","timestamp":"2023-12-07 03:39:27","data":[{"key":"w","value":25.6141348288}]}]} +{"rowindex":17,"customer_id":"mno","day":"2023-12-18","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-18 22:24:48","data":[{"key":"y","value":81.207759202}]}]} +{"rowindex":18,"customer_id":"ghi","day":"2023-12-13","flag":2,"event_sequence":[{"category":"A","timestamp":"2023-12-13 16:26:05","data":[{"key":"y","value":30.6921921236}]},{"category":"C","timestamp":"2023-12-13 15:00:10","data":[{"key":"x","value":73.8609954622}]}]} +{"rowindex":19,"customer_id":"abc","day":"2023-12-7","flag":2,"event_sequence":[]} +{"rowindex":20,"customer_id":"jkl","day":"2023-12-17","flag":0,"event_sequence":[]} +{"rowindex":21,"customer_id":"mno","day":"2023-12-14","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-14 04:13:58","data":[{"key":"w","value":86.1548312989}]},{"category":"D","timestamp":"2023-12-14 15:39:43","data":[{"key":"w","value":40.0214161212}]},{"category":"B","timestamp":"2023-12-14 19:35:33","data":[{"key":"z","value":67.4152417129}]},{"category":"D","timestamp":"2023-12-14 17:20:20","data":[]},{"category":"C","timestamp":"2023-12-14 00:10:29","data":[{"key":"z","value":56.6529579965},{"key":"y","value":52.1273353535}]}]} +{"rowindex":22,"customer_id":"mno","day":"2023-12-8","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-08 13:34:42","data":[{"key":"w","value":95.9950956489},{"key":"y","value":73.9478601628}]}]} +{"rowindex":23,"customer_id":"def","day":"2023-12-17","flag":1,"event_sequence":[{"category":"A","timestamp":"2023-12-17 10:07:16","data":[{"key":"x","value":66.1044798274}]},{"category":"B","timestamp":"2023-12-17 14:33:42","data":[{"key":"z","value":77.4267396836}]},{"category":"B","timestamp":"2023-12-17 11:54:45","data":[]}]} +{"rowindex":24,"customer_id":"def","day":"2023-12-17","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-17 21:02:08","data":[{"key":"y","value":70.9945354474}]}]} +{"rowindex":25,"customer_id":"ghi","day":"2023-12-2","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-02 01:17:39","data":[]},{"category":"B","timestamp":"2023-12-02 13:54:33","data":[{"key":"w","value":49.7485944905},{"key":"x","value":12.3938168348}]},{"category":"B","timestamp":"2023-12-02 02:30:14","data":[]},{"category":"C","timestamp":"2023-12-02 13:16:54","data":[{"key":"x","value":52.0455905555},{"key":"y","value":13.1107332474}]},{"category":"A","timestamp":"2023-12-02 23:10:23","data":[{"key":"w","value":73.5827155332}]}]} +{"rowindex":26,"customer_id":"def","day":"2023-12-1","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-01 10:01:13","data":[]}]} +{"rowindex":27,"customer_id":"mno","day":"2023-12-10","flag":1,"event_sequence":[{"category":"C","timestamp":"2023-12-10 11:07:58","data":[{"key":"y","value":41.8327013256},{"key":"w","value":59.4445826737}]},{"category":"C","timestamp":"2023-12-10 01:35:25","data":[{"key":"z","value":98.4395840749}]}]} +{"rowindex":28,"customer_id":"def","day":"2023-12-4","flag":0,"event_sequence":[{"category":"B","timestamp":"2023-12-04 13:27:56","data":[]},{"category":"D","timestamp":"2023-12-04 07:29:29","data":[]},{"category":"C","timestamp":"2023-12-04 15:50:42","data":[]},{"category":"C","timestamp":"2023-12-04 21:14:39","data":[{"key":"x","value":87.2090409333},{"key":"z","value":67.873124445}]},{"category":"A","timestamp":"2023-12-04 10:22:07","data":[]}]} +{"rowindex":29,"customer_id":"abc","day":"2023-12-6","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-06 13:03:19","data":[{"key":"y","value":64.2584716378},{"key":"w","value":17.4653120122}]},{"category":"A","timestamp":"2023-12-06 06:10:03","data":[{"key":"w","value":93.696003482},{"key":"y","value":0.675474038}]},{"category":"B","timestamp":"2023-12-06 10:10:08","data":[]},{"category":"C","timestamp":"2023-12-06 06:48:30","data":[]},{"category":"B","timestamp":"2023-12-06 23:00:42","data":[{"key":"x","value":65.1766190228}]}]} +{"rowindex":30,"customer_id":"abc","day":"2023-12-1","flag":1,"event_sequence":[{"category":"C","timestamp":"2023-12-01 03:17:48","data":[]},{"category":"A","timestamp":"2023-12-01 19:59:32","data":[]},{"category":"C","timestamp":"2023-12-01 02:16:52","data":[]}]} +{"rowindex":31,"customer_id":"jkl","day":"2023-12-2","flag":0,"event_sequence":[{"category":"A","timestamp":"2023-12-02 13:13:21","data":[{"key":"y","value":85.6919195342}]},{"category":"C","timestamp":"2023-12-02 06:32:12","data":[{"key":"y","value":72.2526437761},{"key":"x","value":62.1668944755}]},{"category":"D","timestamp":"2023-12-02 01:49:25","data":[{"key":"z","value":13.5820871569}]},{"category":"A","timestamp":"2023-12-02 21:30:07","data":[{"key":"x","value":33.6063239173},{"key":"z","value":93.896859174}]},{"category":"C","timestamp":"2023-12-02 07:03:10","data":[{"key":"w","value":95.2222323306},{"key":"x","value":8.4438153156}]}]} +{"rowindex":32,"customer_id":"def","day":"2023-12-9","flag":1,"event_sequence":[]} +{"rowindex":33,"customer_id":"def","day":"2023-12-4","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-04 18:03:13","data":[{"key":"x","value":87.3759936085}]},{"category":"C","timestamp":"2023-12-04 12:23:33","data":[{"key":"x","value":7.6663438235}]},{"category":"D","timestamp":"2023-12-04 23:16:12","data":[{"key":"x","value":42.6682526335}]}]} +{"rowindex":34,"customer_id":"ghi","day":"2023-12-2","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-02 22:48:04","data":[]},{"category":"A","timestamp":"2023-12-02 06:52:49","data":[{"key":"x","value":53.7008853605}]},{"category":"D","timestamp":"2023-12-02 21:35:43","data":[{"key":"w","value":65.7972882681}]},{"category":"D","timestamp":"2023-12-02 04:22:32","data":[{"key":"x","value":8.0812633272}]},{"category":"D","timestamp":"2023-12-02 04:53:36","data":[]}]} +{"rowindex":35,"customer_id":"ghi","day":"2023-12-18","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-18 07:51:07","data":[]},{"category":"C","timestamp":"2023-12-18 23:09:23","data":[{"key":"x","value":36.7126625188},{"key":"z","value":7.3234058497}]}]} +{"rowindex":36,"customer_id":"ghi","day":"2023-12-11","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-11 17:36:44","data":[{"key":"y","value":72.5462499934},{"key":"x","value":40.7042156894}]},{"category":"C","timestamp":"2023-12-11 19:58:01","data":[{"key":"x","value":88.553115143},{"key":"w","value":16.5083749137}]},{"category":"C","timestamp":"2023-12-11 00:22:58","data":[{"key":"y","value":13.7684351079}]},{"category":"A","timestamp":"2023-12-11 06:52:46","data":[{"key":"x","value":82.6970048317}]}]} +{"rowindex":37,"customer_id":"jkl","day":"2023-12-1","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-01 12:41:41","data":[]},{"category":"D","timestamp":"2023-12-01 05:37:51","data":[]},{"category":"C","timestamp":"2023-12-01 07:50:54","data":[{"key":"y","value":79.7821140254},{"key":"w","value":55.1183743775}]},{"category":"A","timestamp":"2023-12-01 16:23:25","data":[]}]} +{"rowindex":38,"customer_id":"abc","day":"2023-12-15","flag":0,"event_sequence":[{"category":"B","timestamp":"2023-12-15 15:45:21","data":[]},{"category":"D","timestamp":"2023-12-15 05:40:05","data":[{"key":"z","value":84.4372711239}]},{"category":"C","timestamp":"2023-12-15 18:54:07","data":[]},{"category":"C","timestamp":"2023-12-15 01:34:35","data":[{"key":"x","value":57.6043137776},{"key":"y","value":2.0915421039}]}]} +{"rowindex":39,"customer_id":"ghi","day":"2023-12-2","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-02 17:31:07","data":[]},{"category":"A","timestamp":"2023-12-02 14:09:19","data":[]},{"category":"A","timestamp":"2023-12-02 19:47:26","data":[{"key":"y","value":40.4981578761}]}]} +{"rowindex":40,"customer_id":"abc","day":"2023-12-17","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-17 14:54:26","data":[]}]} +{"rowindex":41,"customer_id":"def","day":"2023-12-8","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-08 03:29:13","data":[{"key":"w","value":22.3551385464}]},{"category":"A","timestamp":"2023-12-08 18:11:55","data":[]}]} +{"rowindex":42,"customer_id":"ghi","day":"2023-12-2","flag":1,"event_sequence":[{"category":"C","timestamp":"2023-12-02 13:34:00","data":[{"key":"w","value":32.2914731904},{"key":"z","value":1.667821995}]},{"category":"C","timestamp":"2023-12-02 16:27:30","data":[]},{"category":"D","timestamp":"2023-12-02 05:53:11","data":[]},{"category":"C","timestamp":"2023-12-02 06:36:55","data":[{"key":"z","value":17.1648556861},{"key":"y","value":68.34850499}]}]} +{"rowindex":43,"customer_id":"ghi","day":"2023-12-11","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-11 08:23:53","data":[{"key":"x","value":44.2005886027}]},{"category":"B","timestamp":"2023-12-11 07:45:41","data":[{"key":"w","value":77.6941452877},{"key":"z","value":51.1968046092}]},{"category":"B","timestamp":"2023-12-11 11:58:25","data":[{"key":"y","value":68.1363704094}]},{"category":"C","timestamp":"2023-12-11 22:13:57","data":[{"key":"z","value":58.1763854177}]},{"category":"C","timestamp":"2023-12-11 09:13:08","data":[]}]} +{"rowindex":44,"customer_id":"def","day":"2023-12-12","flag":2,"event_sequence":[{"category":"C","timestamp":"2023-12-12 11:38:27","data":[{"key":"y","value":89.3301425129},{"key":"w","value":39.419946238}]}]} +{"rowindex":45,"customer_id":"mno","day":"2023-12-14","flag":2,"event_sequence":[{"category":"A","timestamp":"2023-12-14 13:26:53","data":[{"key":"z","value":76.4355996198}]},{"category":"D","timestamp":"2023-12-14 02:51:25","data":[]},{"category":"D","timestamp":"2023-12-14 16:06:20","data":[]}]} +{"rowindex":46,"customer_id":"mno","day":"2023-12-18","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-18 16:52:35","data":[{"key":"y","value":92.8314533492}]},{"category":"A","timestamp":"2023-12-18 18:55:16","data":[]},{"category":"A","timestamp":"2023-12-18 11:48:11","data":[]}]} +{"rowindex":47,"customer_id":"ghi","day":"2023-12-5","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-05 18:00:29","data":[{"key":"w","value":4.1194443596},{"key":"y","value":90.9907980881}]},{"category":"C","timestamp":"2023-12-05 18:28:30","data":[]},{"category":"C","timestamp":"2023-12-05 01:23:53","data":[]},{"category":"B","timestamp":"2023-12-05 09:30:53","data":[]}]} +{"rowindex":48,"customer_id":"jkl","day":"2023-12-4","flag":1,"event_sequence":[{"category":"C","timestamp":"2023-12-04 00:00:57","data":[{"key":"x","value":54.1860622721},{"key":"z","value":21.9039040875}]},{"category":"C","timestamp":"2023-12-04 03:47:29","data":[{"key":"z","value":10.1626962952},{"key":"y","value":80.2137857017}]},{"category":"C","timestamp":"2023-12-04 09:38:59","data":[{"key":"y","value":41.4002343854},{"key":"x","value":2.5915025309}]},{"category":"D","timestamp":"2023-12-04 10:26:10","data":[{"key":"y","value":78.3790791291},{"key":"z","value":21.0205345948}]}]} +{"rowindex":49,"customer_id":"jkl","day":"2023-12-11","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-11 00:56:27","data":[]},{"category":"C","timestamp":"2023-12-11 00:00:49","data":[]},{"category":"A","timestamp":"2023-12-11 06:51:01","data":[]},{"category":"B","timestamp":"2023-12-11 15:03:31","data":[{"key":"w","value":11.4068443366}]},{"category":"A","timestamp":"2023-12-11 06:51:26","data":[{"key":"x","value":16.6716464506},{"key":"w","value":12.3375298466}]}]} +{"rowindex":50,"customer_id":"jkl","day":"2023-12-7","flag":0,"event_sequence":[]} +{"rowindex":51,"customer_id":"jkl","day":"2023-12-16","flag":1,"event_sequence":[]} +{"rowindex":52,"customer_id":"mno","day":"2023-12-8","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-08 13:38:34","data":[{"key":"y","value":89.16823262}]},{"category":"B","timestamp":"2023-12-08 21:42:37","data":[{"key":"z","value":49.2264719354},{"key":"w","value":71.3471924749}]},{"category":"B","timestamp":"2023-12-08 11:20:22","data":[]}]} +{"rowindex":53,"customer_id":"ghi","day":"2023-12-18","flag":0,"event_sequence":[]} +{"rowindex":54,"customer_id":"def","day":"2023-12-14","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-14 15:18:52","data":[{"key":"x","value":10.7255724898}]},{"category":"C","timestamp":"2023-12-14 00:16:13","data":[{"key":"x","value":81.6578442509},{"key":"z","value":97.6343706241}]},{"category":"A","timestamp":"2023-12-14 15:17:47","data":[{"key":"z","value":61.0727156569},{"key":"y","value":68.5047229429}]}]} +{"rowindex":55,"customer_id":"def","day":"2023-12-17","flag":0,"event_sequence":[{"category":"A","timestamp":"2023-12-17 08:09:37","data":[{"key":"x","value":96.7880530276}]},{"category":"C","timestamp":"2023-12-17 17:45:03","data":[{"key":"x","value":89.261752039}]},{"category":"B","timestamp":"2023-12-17 23:34:55","data":[{"key":"x","value":56.6947696032},{"key":"y","value":39.2160698568}]}]} +{"rowindex":56,"customer_id":"abc","day":"2023-12-3","flag":1,"event_sequence":[{"category":"C","timestamp":"2023-12-03 16:36:33","data":[{"key":"w","value":31.3842474288},{"key":"y","value":70.0883222713}]},{"category":"A","timestamp":"2023-12-03 23:14:03","data":[{"key":"z","value":2.241181478},{"key":"x","value":33.4155024672}]},{"category":"C","timestamp":"2023-12-03 02:59:20","data":[{"key":"w","value":30.325598456},{"key":"y","value":43.6801994079}]},{"category":"A","timestamp":"2023-12-03 17:25:12","data":[]}]} +{"rowindex":57,"customer_id":"jkl","day":"2023-12-18","flag":0,"event_sequence":[{"category":"B","timestamp":"2023-12-18 02:36:06","data":[{"key":"y","value":59.5978119693},{"key":"w","value":50.0596752663}]},{"category":"A","timestamp":"2023-12-18 22:15:26","data":[{"key":"y","value":46.7811589523},{"key":"z","value":17.5305458954}]},{"category":"B","timestamp":"2023-12-18 10:46:35","data":[{"key":"y","value":17.5499211188}]}]} +{"rowindex":58,"customer_id":"jkl","day":"2023-12-11","flag":1,"event_sequence":[{"category":"A","timestamp":"2023-12-11 08:08:24","data":[]},{"category":"A","timestamp":"2023-12-11 14:37:12","data":[{"key":"z","value":85.2678327892}]},{"category":"A","timestamp":"2023-12-11 14:11:26","data":[]},{"category":"A","timestamp":"2023-12-11 09:15:19","data":[]},{"category":"A","timestamp":"2023-12-11 13:29:27","data":[]}]} +{"rowindex":59,"customer_id":"mno","day":"2023-12-18","flag":2,"event_sequence":[]} +{"rowindex":60,"customer_id":"def","day":"2023-12-15","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-15 22:31:56","data":[{"key":"x","value":69.3286635086},{"key":"z","value":41.2999550449}]},{"category":"D","timestamp":"2023-12-15 22:30:05","data":[]},{"category":"B","timestamp":"2023-12-15 13:52:17","data":[{"key":"z","value":37.8991532333},{"key":"y","value":69.1381526165}]}]} +{"rowindex":61,"customer_id":"jkl","day":"2023-12-6","flag":2,"event_sequence":[{"category":"C","timestamp":"2023-12-06 16:19:12","data":[{"key":"w","value":83.7533903572},{"key":"x","value":72.0796689391}]}]} +{"rowindex":62,"customer_id":"ghi","day":"2023-12-13","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-13 19:35:45","data":[{"key":"y","value":9.7338091747}]},{"category":"B","timestamp":"2023-12-13 04:27:13","data":[{"key":"x","value":77.5851696223},{"key":"y","value":44.6396928116}]},{"category":"B","timestamp":"2023-12-13 14:21:37","data":[{"key":"z","value":62.6243288556}]},{"category":"C","timestamp":"2023-12-13 09:43:52","data":[{"key":"y","value":96.4384908625}]}]} +{"rowindex":63,"customer_id":"def","day":"2023-12-14","flag":1,"event_sequence":[{"category":"A","timestamp":"2023-12-14 10:49:52","data":[{"key":"x","value":47.2768901655},{"key":"y","value":31.4990167429}]},{"category":"B","timestamp":"2023-12-14 13:00:17","data":[{"key":"y","value":47.1290340032},{"key":"x","value":63.4631919376}]},{"category":"A","timestamp":"2023-12-14 22:12:52","data":[]},{"category":"A","timestamp":"2023-12-14 06:31:57","data":[]},{"category":"A","timestamp":"2023-12-14 03:46:03","data":[]}]} +{"rowindex":64,"customer_id":"mno","day":"2023-12-9","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-09 10:04:27","data":[{"key":"y","value":67.6773976982},{"key":"w","value":30.3681543638}]},{"category":"D","timestamp":"2023-12-09 06:31:47","data":[]}]} +{"rowindex":65,"customer_id":"mno","day":"2023-12-4","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-04 03:30:32","data":[]},{"category":"B","timestamp":"2023-12-04 05:04:06","data":[{"key":"x","value":21.382181381}]}]} +{"rowindex":66,"customer_id":"mno","day":"2023-12-9","flag":0,"event_sequence":[]} +{"rowindex":67,"customer_id":"def","day":"2023-12-18","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-18 15:06:18","data":[{"key":"w","value":22.8608042274}]}]} +{"rowindex":68,"customer_id":"mno","day":"2023-12-2","flag":0,"event_sequence":[{"category":"A","timestamp":"2023-12-02 20:31:02","data":[{"key":"z","value":91.6471682783}]}]} +{"rowindex":69,"customer_id":"def","day":"2023-12-1","flag":2,"event_sequence":[{"category":"A","timestamp":"2023-12-01 18:23:24","data":[]},{"category":"B","timestamp":"2023-12-01 03:38:19","data":[{"key":"z","value":77.6426948721}]},{"category":"D","timestamp":"2023-12-01 02:53:39","data":[]},{"category":"D","timestamp":"2023-12-01 01:16:05","data":[{"key":"x","value":4.1829224252}]}]} +{"rowindex":70,"customer_id":"ghi","day":"2023-12-2","flag":1,"event_sequence":[{"category":"A","timestamp":"2023-12-02 18:53:51","data":[]},{"category":"A","timestamp":"2023-12-02 11:05:50","data":[{"key":"z","value":41.8070964998}]},{"category":"B","timestamp":"2023-12-02 06:32:35","data":[]},{"category":"B","timestamp":"2023-12-02 07:03:09","data":[{"key":"x","value":73.1611243111}]}]} +{"rowindex":71,"customer_id":"ghi","day":"2023-12-9","flag":2,"event_sequence":[{"category":"C","timestamp":"2023-12-09 04:54:59","data":[{"key":"x","value":85.2320581103}]},{"category":"B","timestamp":"2023-12-09 15:11:55","data":[]},{"category":"D","timestamp":"2023-12-09 16:21:45","data":[]},{"category":"B","timestamp":"2023-12-09 06:03:32","data":[{"key":"w","value":69.0663696235}]},{"category":"C","timestamp":"2023-12-09 02:48:41","data":[{"key":"y","value":13.3980977494}]}]} +{"rowindex":72,"customer_id":"abc","day":"2023-12-14","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-14 15:56:22","data":[]},{"category":"D","timestamp":"2023-12-14 06:48:33","data":[{"key":"y","value":36.2141968443},{"key":"z","value":95.4467019984}]}]} +{"rowindex":73,"customer_id":"mno","day":"2023-12-13","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-13 10:19:12","data":[]}]} +{"rowindex":74,"customer_id":"def","day":"2023-12-3","flag":0,"event_sequence":[]} +{"rowindex":75,"customer_id":"abc","day":"2023-12-13","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-13 19:07:09","data":[{"key":"w","value":18.8470628926},{"key":"z","value":88.20939594}]}]} +{"rowindex":76,"customer_id":"ghi","day":"2023-12-8","flag":0,"event_sequence":[{"category":"D","timestamp":"2023-12-08 15:22:08","data":[]},{"category":"C","timestamp":"2023-12-08 16:51:43","data":[{"key":"w","value":79.5244986146}]},{"category":"C","timestamp":"2023-12-08 03:12:25","data":[{"key":"w","value":56.6377952915},{"key":"z","value":42.3533060413}]}]} +{"rowindex":77,"customer_id":"jkl","day":"2023-12-13","flag":2,"event_sequence":[{"category":"C","timestamp":"2023-12-13 12:14:14","data":[{"key":"w","value":35.2592201371},{"key":"y","value":13.5684896571}]}]} +{"rowindex":78,"customer_id":"abc","day":"2023-12-5","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-05 19:22:58","data":[{"key":"z","value":66.2843566224}]},{"category":"B","timestamp":"2023-12-05 19:39:08","data":[{"key":"w","value":34.080531438}]},{"category":"C","timestamp":"2023-12-05 02:53:05","data":[{"key":"z","value":33.991374759},{"key":"x","value":80.0208062703}]},{"category":"D","timestamp":"2023-12-05 13:30:43","data":[{"key":"y","value":67.1306733907}]},{"category":"A","timestamp":"2023-12-05 00:51:36","data":[{"key":"w","value":17.3844088301}]}]} +{"rowindex":79,"customer_id":"mno","day":"2023-12-9","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-09 10:36:18","data":[{"key":"y","value":17.9861379377},{"key":"x","value":31.1422706226}]},{"category":"A","timestamp":"2023-12-09 19:04:16","data":[]},{"category":"C","timestamp":"2023-12-09 23:46:25","data":[]},{"category":"B","timestamp":"2023-12-09 15:08:37","data":[]}]} +{"rowindex":80,"customer_id":"mno","day":"2023-12-11","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-11 23:50:20","data":[]},{"category":"A","timestamp":"2023-12-11 13:45:37","data":[{"key":"y","value":34.1896555846},{"key":"z","value":54.8455987136}]},{"category":"D","timestamp":"2023-12-11 05:27:06","data":[{"key":"z","value":8.6439113664},{"key":"w","value":57.8679152847}]},{"category":"A","timestamp":"2023-12-11 22:56:07","data":[]},{"category":"D","timestamp":"2023-12-11 01:09:13","data":[{"key":"x","value":94.8088772326},{"key":"y","value":92.9817038325}]}]} +{"rowindex":81,"customer_id":"mno","day":"2023-12-2","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-02 19:13:55","data":[{"key":"x","value":92.6140550812},{"key":"y","value":21.6844233156}]},{"category":"A","timestamp":"2023-12-02 10:19:54","data":[{"key":"z","value":96.1332346043},{"key":"y","value":12.3365763983}]},{"category":"C","timestamp":"2023-12-02 23:15:36","data":[]}]} +{"rowindex":82,"customer_id":"def","day":"2023-12-8","flag":2,"event_sequence":[{"category":"C","timestamp":"2023-12-08 03:43:45","data":[{"key":"z","value":39.7558930693}]},{"category":"A","timestamp":"2023-12-08 01:35:47","data":[]},{"category":"D","timestamp":"2023-12-08 04:53:02","data":[{"key":"x","value":3.1323563783}]},{"category":"B","timestamp":"2023-12-08 01:12:21","data":[{"key":"w","value":21.6503102051},{"key":"y","value":43.4536696853}]},{"category":"B","timestamp":"2023-12-08 01:57:25","data":[{"key":"z","value":11.3705979892},{"key":"y","value":85.3671308445}]}]} +{"rowindex":83,"customer_id":"mno","day":"2023-12-16","flag":0,"event_sequence":[]} +{"rowindex":84,"customer_id":"def","day":"2023-12-13","flag":0,"event_sequence":[]} +{"rowindex":85,"customer_id":"jkl","day":"2023-12-6","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-06 18:06:14","data":[{"key":"w","value":75.6475285669},{"key":"y","value":92.2341481081}]},{"category":"B","timestamp":"2023-12-06 15:28:32","data":[]},{"category":"B","timestamp":"2023-12-06 19:45:52","data":[]},{"category":"C","timestamp":"2023-12-06 08:32:52","data":[]},{"category":"A","timestamp":"2023-12-06 17:32:37","data":[{"key":"y","value":80.2305875735}]}]} +{"rowindex":86,"customer_id":"abc","day":"2023-12-10","flag":2,"event_sequence":[{"category":"A","timestamp":"2023-12-10 09:34:20","data":[{"key":"y","value":10.7693525828},{"key":"w","value":81.4922282197}]},{"category":"C","timestamp":"2023-12-10 03:58:48","data":[{"key":"y","value":75.2926863125},{"key":"x","value":14.3834415502}]},{"category":"A","timestamp":"2023-12-10 09:09:24","data":[{"key":"z","value":26.1964055176},{"key":"w","value":33.2590307936}]},{"category":"C","timestamp":"2023-12-10 07:53:33","data":[{"key":"z","value":23.2141532358}]}]} +{"rowindex":87,"customer_id":"ghi","day":"2023-12-6","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-06 17:59:34","data":[]},{"category":"B","timestamp":"2023-12-06 15:30:32","data":[{"key":"z","value":65.5093670838}]},{"category":"C","timestamp":"2023-12-06 11:40:36","data":[{"key":"z","value":19.0969232242}]},{"category":"C","timestamp":"2023-12-06 23:24:48","data":[{"key":"w","value":41.7328593069}]}]} +{"rowindex":88,"customer_id":"mno","day":"2023-12-4","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-04 09:37:49","data":[{"key":"z","value":73.115183578},{"key":"w","value":55.409641057}]},{"category":"A","timestamp":"2023-12-04 20:25:06","data":[{"key":"x","value":68.225517069}]},{"category":"C","timestamp":"2023-12-04 02:46:08","data":[]},{"category":"A","timestamp":"2023-12-04 06:18:04","data":[{"key":"x","value":95.7957065313},{"key":"y","value":68.2634789529}]}]} +{"rowindex":89,"customer_id":"ghi","day":"2023-12-3","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-03 07:02:27","data":[{"key":"w","value":17.0165951832}]},{"category":"D","timestamp":"2023-12-03 19:06:20","data":[]},{"category":"A","timestamp":"2023-12-03 17:50:14","data":[{"key":"x","value":4.3834633659},{"key":"z","value":84.6024255445}]},{"category":"C","timestamp":"2023-12-03 06:51:03","data":[]}]} +{"rowindex":90,"customer_id":"mno","day":"2023-12-12","flag":2,"event_sequence":[{"category":"B","timestamp":"2023-12-12 21:57:25","data":[{"key":"y","value":95.5058021347}]},{"category":"C","timestamp":"2023-12-12 07:24:27","data":[{"key":"z","value":17.9587475242}]},{"category":"A","timestamp":"2023-12-12 10:35:52","data":[{"key":"z","value":55.194876676}]},{"category":"D","timestamp":"2023-12-12 23:44:14","data":[{"key":"w","value":24.6177835891}]},{"category":"D","timestamp":"2023-12-12 16:09:40","data":[{"key":"y","value":32.2627525342},{"key":"x","value":77.4276051497}]}]} +{"rowindex":91,"customer_id":"abc","day":"2023-12-18","flag":0,"event_sequence":[]} +{"rowindex":92,"customer_id":"jkl","day":"2023-12-7","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-07 11:36:31","data":[{"key":"w","value":70.4689420724}]},{"category":"A","timestamp":"2023-12-07 09:18:26","data":[{"key":"z","value":31.0551928628},{"key":"y","value":4.7472634353}]},{"category":"D","timestamp":"2023-12-07 05:44:09","data":[{"key":"z","value":37.7906214595},{"key":"w","value":38.618192046}]},{"category":"B","timestamp":"2023-12-07 16:30:31","data":[{"key":"y","value":92.4389663402}]},{"category":"A","timestamp":"2023-12-07 16:35:58","data":[{"key":"x","value":63.8398372162},{"key":"z","value":90.1325261576}]}]} +{"rowindex":93,"customer_id":"abc","day":"2023-12-15","flag":0,"event_sequence":[]} +{"rowindex":94,"customer_id":"mno","day":"2023-12-7","flag":0,"event_sequence":[]} +{"rowindex":95,"customer_id":"ghi","day":"2023-12-14","flag":2,"event_sequence":[{"category":"C","timestamp":"2023-12-14 22:37:13","data":[{"key":"x","value":55.3895966386}]},{"category":"B","timestamp":"2023-12-14 15:56:30","data":[{"key":"y","value":87.7140820119},{"key":"x","value":48.3079555774}]},{"category":"D","timestamp":"2023-12-14 06:35:41","data":[{"key":"y","value":60.4608873685},{"key":"x","value":74.6169412477}]}]} +{"rowindex":96,"customer_id":"def","day":"2023-12-1","flag":1,"event_sequence":[{"category":"D","timestamp":"2023-12-01 07:57:31","data":[{"key":"w","value":83.8985453363},{"key":"x","value":37.6937609678}]},{"category":"A","timestamp":"2023-12-01 00:44:54","data":[{"key":"w","value":65.3980461559}]},{"category":"D","timestamp":"2023-12-01 17:43:00","data":[]},{"category":"A","timestamp":"2023-12-01 02:48:33","data":[{"key":"z","value":23.8579933054}]},{"category":"B","timestamp":"2023-12-01 07:36:21","data":[{"key":"y","value":53.0811307247}]}]} +{"rowindex":97,"customer_id":"ghi","day":"2023-12-5","flag":0,"event_sequence":[{"category":"B","timestamp":"2023-12-05 09:23:03","data":[]},{"category":"C","timestamp":"2023-12-05 01:22:08","data":[]}]} +{"rowindex":98,"customer_id":"ghi","day":"2023-12-14","flag":2,"event_sequence":[]} +{"rowindex":99,"customer_id":"ghi","day":"2023-12-14","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-14 15:46:06","data":[{"key":"z","value":48.2733214833}]},{"category":"D","timestamp":"2023-12-14 15:39:56","data":[]},{"category":"D","timestamp":"2023-12-14 17:18:14","data":[]},{"category":"D","timestamp":"2023-12-14 02:41:54","data":[{"key":"z","value":98.7008514491},{"key":"x","value":55.3757151027}]},{"category":"C","timestamp":"2023-12-14 07:54:49","data":[{"key":"z","value":69.8181005179}]}]} diff --git a/tests/data/nested_schema.json b/tests/data/nested_schema.json new file mode 100644 index 00000000000..c3fa39b36d2 --- /dev/null +++ b/tests/data/nested_schema.json @@ -0,0 +1,56 @@ +[ + { + "mode": "REQUIRED", + "name": "rowindex", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "customer_id", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "day", + "type": "DATE" + }, + { + "mode": "NULLABLE", + "name": "flag", + "type": "INTEGER" + }, + { + "fields": [ + { + "fields": [ + { + "mode": "NULLABLE", + "name": "value", + "type": "FLOAT" + }, + { + "mode": "NULLABLE", + "name": "key", + "type": "STRING" + } + ], + "mode": "REPEATED", + "name": "data", + "type": "RECORD" + }, + { + "mode": "NULLABLE", + "name": "timestamp", + "type": "TIMESTAMP" + }, + { + "mode": "NULLABLE", + "name": "category", + "type": "STRING" + } + ], + "mode": "REPEATED", + "name": "event_sequence", + "type": "RECORD" + } +] diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 0ad4280497f..4aa27d6a19c 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -231,6 +231,7 @@ def load_test_data_tables( for table_name, schema_filename, data_filename in [ ("scalars", "scalars_schema.json", "scalars.jsonl"), ("scalars_too", "scalars_schema.json", "scalars.jsonl"), + ("nested", "nested_schema.json", "nested.jsonl"), ("penguins", "penguins_schema.json", "penguins.jsonl"), ("time_series", "time_series_schema.json", "time_series.jsonl"), ("hockey_players", "hockey_players.json", "hockey_players.jsonl"), @@ -295,6 +296,11 @@ def scalars_table_tokyo(test_data_tables_tokyo) -> str: return test_data_tables_tokyo["scalars"] +@pytest.fixture(scope="session") +def nested_table_id(test_data_tables) -> str: + return test_data_tables["nested"] + + @pytest.fixture(scope="session") def penguins_table_id(test_data_tables) -> str: return test_data_tables["penguins"] @@ -315,6 +321,28 @@ def matrix_3by4_table_id(test_data_tables) -> str: return test_data_tables["matrix_3by4"] +@pytest.fixture(scope="session") +def nested_df( + nested_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq(nested_table_id, index_col="rowindex") + + +@pytest.fixture(scope="session") +def nested_pandas_df() -> pd.DataFrame: + """pd.DataFrame pointing at test data.""" + + df = pd.read_json( + DATA_DIR / "nested.jsonl", + lines=True, + ) + convert_pandas_dtypes(df, bytes_col=True) + + df = df.set_index("rowindex") + return df + + @pytest.fixture(scope="session") def scalars_df_default_index( scalars_df_index: bigframes.dataframe.DataFrame, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 4ae31fa4a0a..3d312530213 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1251,9 +1251,7 @@ def test_get_dtypes(scalars_df_default_index): ) -def test_get_dtypes_array_struct(session): - """We may upgrade struct and array to proper arrow dtype support in future. For now, - we return python objects""" +def test_get_dtypes_array_struct_query(session): df = session.read_gbq( """SELECT [1, 3, 2] AS array_column, @@ -1281,6 +1279,41 @@ def test_get_dtypes_array_struct(session): ) +def test_get_dtypes_array_struct_table(nested_df): + dtypes = nested_df.dtypes + pd.testing.assert_series_equal( + dtypes, + pd.Series( + { + "customer_id": pd.StringDtype(storage="pyarrow"), + "day": pd.ArrowDtype(pa.date32()), + "flag": pd.Int64Dtype(), + "event_sequence": pd.ArrowDtype( + pa.list_( + pa.struct( + [ + ( + "data", + pa.list_( + pa.struct( + [ + ("value", pa.float64()), + ("key", pa.string()), + ], + ), + ), + ), + ("timestamp", pa.timestamp("us", "UTC")), + ("category", pa.string()), + ] + ), + ), + ), + } + ), + ) + + def test_shape(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df.shape From 1866a266f0fa40882b589579654c1ad428b036d8 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 2 Feb 2024 18:52:15 +0000 Subject: [PATCH 296/921] docs: Fix the `DataFrame.apply` code sample (#366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 93fba9f3aa2..05f41678384 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3296,19 +3296,19 @@ def apply(self, func, *, args=(), **kwargs): >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) >>> df - col1 col2 - 0 1 3 - 1 2 4 + col1 col2 + 0 1 3 + 1 2 4 [2 rows x 2 columns] - >>> def sqaure(x): + >>> def square(x): ... return x * x - >>> df1 = df.apply(sqaure) - >>> df + + >>> df.apply(square) col1 col2 - 0 1 3 - 1 2 4 + 0 1 9 + 1 4 16 [2 rows x 2 columns] From 93835ef27463e5369e201693aa65bb80447570cd Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 5 Feb 2024 14:40:16 -0800 Subject: [PATCH 297/921] refactor: generalize aggregation to handle 0,1, or 2 inputs (#360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 17 +- bigframes/core/blocks.py | 36 ++-- bigframes/core/compile/aggregate_compiler.py | 161 +++++++++------ bigframes/core/compile/analytic_compiler.py | 0 bigframes/core/compile/compiled.py | 55 ++---- bigframes/core/compile/compiler.py | 6 - bigframes/core/expression.py | 30 ++- bigframes/core/groupby/__init__.py | 6 +- bigframes/core/indexes/index.py | 2 +- bigframes/core/nodes.py | 28 +-- bigframes/core/reshape/__init__.py | 16 +- bigframes/core/window/__init__.py | 2 +- bigframes/operations/aggregations.py | 197 ++++++++++++------- bigframes/series.py | 2 +- tests/unit/test_core.py | 5 +- 15 files changed, 338 insertions(+), 225 deletions(-) delete mode 100644 bigframes/core/compile/analytic_compiler.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 8c399e34ab4..ea169dbb743 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -118,7 +118,7 @@ def row_count(self) -> ArrayValue: # Operations def filter_by_id(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - predicate = ex.free_var(predicate_id) + predicate: ex.Expression = ex.free_var(predicate_id) if keep_null: predicate = ops.fillna_op.as_expr(predicate, ex.const(True)) return self.filter(predicate) @@ -241,7 +241,7 @@ def drop_columns(self, columns: Iterable[str]) -> ArrayValue: def aggregate( self, - aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp, str]], + aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]], by_column_ids: typing.Sequence[str] = (), dropna: bool = True, ) -> ArrayValue: @@ -270,14 +270,23 @@ def corr_aggregate( Arguments: corr_aggregations: left_column_id, right_column_id, output_column_id tuples """ + aggregations = tuple( + ( + ex.BinaryAggregation( + agg_ops.CorrOp(), ex.free_var(agg[0]), ex.free_var(agg[1]) + ), + agg[2], + ) + for agg in corr_aggregations + ) return ArrayValue( - nodes.CorrNode(child=self.node, corr_aggregations=tuple(corr_aggregations)) + nodes.AggregateNode(child=self.node, aggregations=aggregations) ) def project_window_op( self, column_name: str, - op: agg_ops.WindowOp, + op: agg_ops.UnaryWindowOp, window_spec: WindowSpec, output_name=None, *, diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 49c4979a841..e758e203353 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -860,7 +860,7 @@ def filter(self, column_id: str, keep_null: bool = False): def aggregate_all_and_stack( self, - operation: agg_ops.AggregateOp, + operation: agg_ops.UnaryAggregateOp, *, axis: int | str = 0, value_col_id: str = "values", @@ -872,7 +872,8 @@ def aggregate_all_and_stack( axis_n = utils.get_axis_number(axis) if axis_n == 0: aggregations = [ - (col_id, operation, col_id) for col_id in self.value_columns + (ex.UnaryAggregation(operation, ex.free_var(col_id)), col_id) + for col_id in self.value_columns ] index_col_ids = [ guid.generate_guid() for i in range(self.column_labels.nlevels) @@ -902,10 +903,13 @@ def aggregate_all_and_stack( dtype=dtype, ) index_aggregations = [ - (col_id, agg_ops.AnyValueOp(), col_id) + (ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.free_var(col_id)), col_id) for col_id in [*self.index_columns] ] - main_aggregation = (value_col_id, operation, value_col_id) + main_aggregation = ( + ex.UnaryAggregation(operation, ex.free_var(value_col_id)), + value_col_id, + ) result_expr = stacked_expr.aggregate( [*index_aggregations, main_aggregation], by_column_ids=[offset_col], @@ -966,7 +970,7 @@ def remap_f(x): def aggregate( self, by_column_ids: typing.Sequence[str] = (), - aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp]] = (), + aggregations: typing.Sequence[typing.Tuple[str, agg_ops.UnaryAggregateOp]] = (), *, dropna: bool = True, ) -> typing.Tuple[Block, typing.Sequence[str]]: @@ -979,10 +983,13 @@ def aggregate( dropna: whether null keys should be dropped """ agg_specs = [ - (input_id, operation, guid.generate_guid()) + ( + ex.UnaryAggregation(operation, ex.free_var(input_id)), + guid.generate_guid(), + ) for input_id, operation in aggregations ] - output_col_ids = [agg_spec[2] for agg_spec in agg_specs] + output_col_ids = [agg_spec[1] for agg_spec in agg_specs] result_expr = self.expr.aggregate(agg_specs, by_column_ids, dropna=dropna) aggregate_labels = self._get_labels_for_columns( @@ -1004,7 +1011,7 @@ def aggregate( output_col_ids, ) - def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): + def get_stat(self, column_id: str, stat: agg_ops.UnaryAggregateOp): """Gets aggregates immediately, and caches it""" if stat.name in self._stats_cache[column_id]: return self._stats_cache[column_id][stat.name] @@ -1014,7 +1021,10 @@ def get_stat(self, column_id: str, stat: agg_ops.AggregateOp): standard_stats = self._standard_stats(column_id) stats_to_fetch = standard_stats if stat in standard_stats else [stat] - aggregations = [(column_id, stat, stat.name) for stat in stats_to_fetch] + aggregations = [ + (ex.UnaryAggregation(stat, ex.free_var(column_id)), stat.name) + for stat in stats_to_fetch + ] expr = self.expr.aggregate(aggregations) offset_index_id = guid.generate_guid() expr = expr.promote_offsets(offset_index_id) @@ -1054,13 +1064,13 @@ def get_corr_stat(self, column_id_left: str, column_id_right: str): def summarize( self, column_ids: typing.Sequence[str], - stats: typing.Sequence[agg_ops.AggregateOp], + stats: typing.Sequence[agg_ops.UnaryAggregateOp], ): """Get a list of stats as a deferred block object.""" label_col_id = guid.generate_guid() labels = [stat.name for stat in stats] aggregations = [ - (col_id, stat, f"{col_id}-{stat.name}") + (ex.UnaryAggregation(stat, ex.free_var(col_id)), f"{col_id}-{stat.name}") for stat in stats for col_id in column_ids ] @@ -1076,7 +1086,7 @@ def summarize( labels = self._get_labels_for_columns(column_ids) return Block(expr, column_labels=labels, index_columns=[label_col_id]) - def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.AggregateOp]: + def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.UnaryAggregateOp]: """ Gets a standard set of stats to preemptively fetch for a column if any other stat is fetched. @@ -1087,7 +1097,7 @@ def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.AggregateOp]: """ # TODO: annotate aggregations themself with this information dtype = self.expr.get_column_type(column_id) - stats: list[agg_ops.AggregateOp] = [agg_ops.count_op] + stats: list[agg_ops.UnaryAggregateOp] = [agg_ops.count_op] if dtype not in bigframes.dtypes.UNORDERED_DTYPES: stats += [agg_ops.min_op, agg_ops.max_op] if dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 044c33799ed..0dbc0e73107 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -21,31 +21,63 @@ import pandas as pd import bigframes.constants as constants +import bigframes.core.compile.scalar_op_compiler as scalar_compilers +import bigframes.core.expression as ex import bigframes.core.window_spec as window_spec import bigframes.dtypes as dtypes import bigframes.operations.aggregations as agg_ops import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +scalar_compiler = scalar_compilers.scalar_op_compiler -def compile_unary_aggregate( - op: agg_ops.AggregateOp, input: ibis_types.Column + +def compile_aggregate( + aggregate: ex.Aggregation, + bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: - return compile_agg(op, input) + if isinstance(aggregate, ex.UnaryAggregation): + input = scalar_compiler.compile_expression(aggregate.arg, bindings=bindings) + return compile_unary_agg( + aggregate.op, + input, + ) + elif isinstance(aggregate, ex.BinaryAggregation): + left = scalar_compiler.compile_expression(aggregate.left, bindings=bindings) + right = scalar_compiler.compile_expression(aggregate.right, bindings=bindings) + return compile_binary_agg(aggregate.op, left, right) + else: + raise ValueError(f"Unexpected aggregation: {aggregate}") -def compile_unary_analytic( - op: agg_ops.WindowOp, input: ibis_types.Column, window: window_spec.WindowSpec +def compile_analytic( + aggregate: ex.Aggregation, + window: window_spec.WindowSpec, + bindings: typing.Dict[str, ibis_types.Value], +) -> ibis_types.Value: + if isinstance(aggregate, ex.UnaryAggregation): + input = scalar_compiler.compile_expression(aggregate.arg, bindings=bindings) + return compile_unary_agg(aggregate.op, input, window) + elif isinstance(aggregate, ex.BinaryAggregation): + raise NotImplementedError("binary analytic operations not yet supported") + else: + raise ValueError(f"Unexpected analytic operation: {aggregate}") + + +@functools.singledispatch +def compile_binary_agg( + op: agg_ops.WindowOp, + input: ibis_types.Column, + window: Optional[window_spec.WindowSpec] = None, ) -> ibis_types.Value: - return compile_agg(op, input, window) + raise ValueError(f"Can't compile unrecognized operation: {op}") @functools.singledispatch -def compile_agg( +def compile_unary_agg( op: agg_ops.WindowOp, input: ibis_types.Column, window: Optional[window_spec.WindowSpec] = None, ) -> ibis_types.Value: - """Defines transformation but isn't cached, always use compile_node instead""" raise ValueError(f"Can't compile unrecognized operation: {op}") @@ -66,7 +98,10 @@ def constrained_op(op, column: ibis_types.Column, window=None): return constrained_op -@compile_agg.register +### Specific Op implementations Below + + +@compile_unary_agg.register @numeric_op def _( op: agg_ops.SumOp, column: ibis_types.NumericColumn, window=None @@ -78,7 +113,7 @@ def _( ) -@compile_agg.register +@compile_unary_agg.register @numeric_op def _( op: agg_ops.MedianOp, column: ibis_types.NumericColumn, window=None @@ -96,7 +131,7 @@ def _( return cast(ibis_types.NumericValue, column.approx_median()) -@compile_agg.register +@compile_unary_agg.register @numeric_op def _( op: agg_ops.ApproxQuartilesOp, column: ibis_types.NumericColumn, window=None @@ -109,11 +144,11 @@ def _( ) value = vendored_ibis_ops.ApproximateMultiQuantile( column, num_bins=4 # type: ignore - ).to_expr()[op._quartile] + ).to_expr()[op.quartile] return cast(ibis_types.NumericValue, value) -@compile_agg.register +@compile_unary_agg.register @numeric_op def _( op: agg_ops.MeanOp, column: ibis_types.NumericColumn, window=None @@ -121,7 +156,7 @@ def _( return _apply_window_if_present(column.mean(), window) -@compile_agg.register +@compile_unary_agg.register @numeric_op def _( op: agg_ops.ProductOp, column: ibis_types.NumericColumn, window=None @@ -158,29 +193,29 @@ def _( return float_result.cast(column.type()) # type: ignore -@compile_agg.register +@compile_unary_agg.register def _(op: agg_ops.MaxOp, column: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(column.max(), window) -@compile_agg.register +@compile_unary_agg.register def _(op: agg_ops.MinOp, column: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(column.min(), window) -@compile_agg.register +@compile_unary_agg.register @numeric_op def _(op: agg_ops.StdOp, x: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(cast(ibis_types.NumericColumn, x).std(), window) -@compile_agg.register +@compile_unary_agg.register @numeric_op def _(op: agg_ops.VarOp, x: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(cast(ibis_types.NumericColumn, x).var(), window) -@compile_agg.register +@compile_unary_agg.register @numeric_op def _(op: agg_ops.PopVarOp, x: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present( @@ -188,35 +223,34 @@ def _(op: agg_ops.PopVarOp, x: ibis_types.Column, window=None) -> ibis_types.Val ) -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.CountOp, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: return _apply_window_if_present(column.count(), window) -@compile_agg.register +@compile_unary_agg.register def _(op: agg_ops.CutOp, x: ibis_types.Column, window=None): out = ibis.case() - - if op._bins_int > 0: + if isinstance(op.bins, int): col_min = _apply_window_if_present(x.min(), window) col_max = _apply_window_if_present(x.max(), window) - bin_width = (col_max - col_min) / op._bins + bin_width = (col_max - col_min) / op.bins - if op._labels is False: - for this_bin in range(op._bins_int - 1): + if op.labels is False: + for this_bin in range(op.bins - 1): out = out.when( x <= (col_min + (this_bin + 1) * bin_width), dtypes.literal_to_ibis_scalar( this_bin, force_dtype=pd.Int64Dtype() ), ) - out = out.when(x.notnull(), op._bins - 1) + out = out.when(x.notnull(), op.bins - 1) else: interval_struct = None adj = (col_max - col_min) * 0.001 - for this_bin in range(op._bins_int): + for this_bin in range(op.bins): left_edge = ( col_min + this_bin * bin_width - (0 if this_bin > 0 else adj) ) @@ -228,30 +262,32 @@ def _(op: agg_ops.CutOp, x: ibis_types.Column, window=None): } ) - if this_bin < op._bins_int - 1: + if this_bin < op.bins - 1: out = out.when( x <= (col_min + (this_bin + 1) * bin_width), interval_struct, ) else: out = out.when(x.notnull(), interval_struct) - else: - for interval in op._bins: - condition = (x > interval.left) & (x <= interval.right) + else: # Interpret as intervals + for interval in op.bins: + left = dtypes.literal_to_ibis_scalar(interval[0]) + right = dtypes.literal_to_ibis_scalar(interval[1]) + condition = (x > left) & (x <= right) interval_struct = ibis.struct( - {"left_exclusive": interval.left, "right_inclusive": interval.right} + {"left_exclusive": left, "right_inclusive": right} ) out = out.when(condition, interval_struct) return out.end() -@compile_agg.register +@compile_unary_agg.register @numeric_op def _( self: agg_ops.QcutOp, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: - if isinstance(self._quantiles, int): - quantiles_ibis = dtypes.literal_to_ibis_scalar(self._quantiles) + if isinstance(self.quantiles, int): + quantiles_ibis = dtypes.literal_to_ibis_scalar(self.quantiles) percent_ranks = cast( ibis_types.FloatingColumn, _apply_window_if_present(column.percent_rank(), window), @@ -264,10 +300,10 @@ def _( _apply_window_if_present(column.percent_rank(), window), ) out = ibis.case() - first_ibis_quantile = dtypes.literal_to_ibis_scalar(self._quantiles[0]) + first_ibis_quantile = dtypes.literal_to_ibis_scalar(self.quantiles[0]) out = out.when(percent_ranks < first_ibis_quantile, None) - for bucket_n in range(len(self._quantiles) - 1): - ibis_quantile = dtypes.literal_to_ibis_scalar(self._quantiles[bucket_n + 1]) + for bucket_n in range(len(self.quantiles) - 1): + ibis_quantile = dtypes.literal_to_ibis_scalar(self.quantiles[bucket_n + 1]) out = out.when( percent_ranks <= ibis_quantile, dtypes.literal_to_ibis_scalar(bucket_n, force_dtype=pd.Int64Dtype()), @@ -276,21 +312,21 @@ def _( return out.end() # type: ignore -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.NuniqueOp, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: return _apply_window_if_present(column.nunique(), window) -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.AnyValueOp, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: return _apply_window_if_present(column.arbitrary(), window) -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.RankOp, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: @@ -298,7 +334,7 @@ def _( return _apply_window_if_present(column.rank(), window) + 1 -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.DenseRankOp, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: @@ -306,12 +342,12 @@ def _( return _apply_window_if_present(column.dense_rank(), window) + 1 -@compile_agg.register +@compile_unary_agg.register def _(op: agg_ops.FirstOp, column: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(column.first(), window) -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.FirstNonNullOp, column: ibis_types.Column, window=None ) -> ibis_types.Value: @@ -320,12 +356,12 @@ def _( ) -@compile_agg.register +@compile_unary_agg.register def _(op: agg_ops.LastOp, column: ibis_types.Column, window=None) -> ibis_types.Value: return _apply_window_if_present(column.last(), window) -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.LastNonNullOp, column: ibis_types.Column, window=None ) -> ibis_types.Value: @@ -334,18 +370,18 @@ def _( ) -@compile_agg.register +@compile_unary_agg.register def _(op: agg_ops.ShiftOp, column: ibis_types.Column, window=None) -> ibis_types.Value: - if op._periods == 0: # No-op + if op.periods == 0: # No-op return column - if op._periods > 0: - return _apply_window_if_present(column.lag(op._periods), window) - return _apply_window_if_present(column.lead(-op._periods), window) + if op.periods > 0: + return _apply_window_if_present(column.lag(op.periods), window) + return _apply_window_if_present(column.lead(-op.periods), window) -@compile_agg.register +@compile_unary_agg.register def _(op: agg_ops.DiffOp, column: ibis_types.Column, window=None) -> ibis_types.Value: - shifted = compile_agg(agg_ops.ShiftOp(op._periods), column, window) + shifted = compile_unary_agg(agg_ops.ShiftOp(op.periods), column, window) if column.type().is_boolean(): return cast(ibis_types.BooleanColumn, column) != cast( ibis_types.BooleanColumn, shifted @@ -358,7 +394,7 @@ def _(op: agg_ops.DiffOp, column: ibis_types.Column, window=None) -> ibis_types. raise TypeError(f"Cannot perform diff on type{column.type()}") -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.AllOp, column: ibis_types.Column, window=None ) -> ibis_types.BooleanValue: @@ -370,7 +406,7 @@ def _( ) -@compile_agg.register +@compile_unary_agg.register def _( op: agg_ops.AnyOp, column: ibis_types.Column, window=None ) -> ibis_types.BooleanValue: @@ -382,6 +418,19 @@ def _( ) +@compile_binary_agg.register +def _( + op: agg_ops.CorrOp, left: ibis_types.Column, right: ibis_types.Column, window=None +) -> ibis_types.NumericValue: + # Will be null if all inputs are null. Pandas defaults to zero sum though. + left_numeric = cast(ibis_types.NumericColumn, left) + right_numeric = cast(ibis_types.NumericColumn, right) + bq_corr = _apply_window_if_present( + left_numeric.corr(right_numeric, how="pop"), window + ) + return cast(ibis_types.NumericColumn, bq_corr) + + def _apply_window_if_present(value: ibis_types.Value, window): return value.over(window) if (window is not None) else value diff --git a/bigframes/core/compile/analytic_compiler.py b/bigframes/core/compile/analytic_compiler.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index c867eaf6809..969437939fa 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -435,7 +435,7 @@ def unpivot( def aggregate( self, - aggregations: typing.Sequence[typing.Tuple[str, agg_ops.AggregateOp, str]], + aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]], by_column_ids: typing.Sequence[str] = (), dropna: bool = True, ) -> OrderedIR: @@ -447,9 +447,10 @@ def aggregate( dropna: whether null keys should be dropped """ table = self._to_ibis_expr() + bindings = {col: table[col] for col in self.column_ids} stats = { - col_out: agg_compiler.compile_agg(agg_op, table[col_in]) - for col_in, agg_op, col_out in aggregations + col_out: agg_compiler.compile_aggregate(aggregate, bindings) + for aggregate, col_out in aggregations } if by_column_ids: result = table.group_by(by_column_ids).aggregate(**stats) @@ -488,35 +489,6 @@ def aggregate( ordering=ordering, ) - def corr_aggregate( - self, corr_aggregations: typing.Sequence[typing.Tuple[str, str, str]] - ) -> OrderedIR: - """ - Get correlations between each lef_column_id and right_column_id, stored in the respective output_column_id. - This uses BigQuery's CORR under the hood, and thus only Pearson's method is used. - Arguments: - corr_aggregations: left_column_id, right_column_id, output_column_id tuples - """ - table = self._to_ibis_expr() - stats = { - col_out: table[col_left].corr(table[col_right], how="pop") - for col_left, col_right, col_out in corr_aggregations - } - aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} - result = table.aggregate(**aggregates) - # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. - ordering = ExpressionOrdering( - ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), - ) - return OrderedIR( - result, - columns=[result[col_id] for col_id in [*stats.keys()]], - hidden_ordering_columns=[result[ORDER_ID_COLUMN]], - ordering=ordering, - ) - def _uniform_sampling(self, fraction: float) -> UnorderedIR: """Sampling the table on given fraction. @@ -792,7 +764,7 @@ def promote_offsets(self, col_id: str) -> OrderedIR: def project_window_op( self, column_name: str, - op: agg_ops.WindowOp, + op: agg_ops.UnaryWindowOp, window_spec: WindowSpec, output_name=None, *, @@ -810,8 +782,11 @@ def project_window_op( """ column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) window = self._ibis_window_from_spec(window_spec, allow_ties=op.handles_ties) + bindings = {col: self._get_ibis_column(col) for col in self.column_ids} - window_op = agg_compiler.compile_unary_analytic(op, column, window) + window_op = agg_compiler.compile_analytic( + ex.UnaryAggregation(op, ex.free_var(column_name)), window, bindings=bindings + ) clauses = [] if op.skips_nulls and not never_skip_nulls: @@ -819,15 +794,19 @@ def project_window_op( if window_spec.min_periods: if op.skips_nulls: # Most operations do not count NULL values towards min_periods - observation_count = agg_compiler.compile_unary_analytic( - agg_ops.count_op, column, window + observation_count = agg_compiler.compile_analytic( + ex.UnaryAggregation(agg_ops.count_op, ex.free_var(column_name)), + window, + bindings=bindings, ) else: # Operations like count treat even NULLs as valid observations for the sake of min_periods # notnull is just used to convert null values to non-null (FALSE) values to be counted denulled_value = typing.cast(ibis_types.BooleanColumn, column.notnull()) - observation_count = agg_compiler.compile_unary_analytic( - agg_ops.count_op, denulled_value, window + observation_count = agg_compiler.compile_analytic( + ex.UnaryAggregation(agg_ops.count_op, ex.free_var("_denulled")), + window, + bindings={**bindings, "_denulled": denulled_value}, ) clauses.append( ( diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 2ec00f70738..4ced85352c5 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -162,12 +162,6 @@ def compile_aggregate(node: nodes.AggregateNode, ordered: bool = True): return result if ordered else result.to_unordered() -@_compile_node.register -def compile_corr(node: nodes.CorrNode, ordered: bool = True): - result = compile_unordered_ir(node.child).corr_aggregate(node.corr_aggregations) - return result if ordered else result.to_unordered() - - @_compile_node.register def compile_window(node: nodes.WindowOpNode, ordered: bool = True): result = compile_ordered_ir(node.child).project_window_op( diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index d1be6444395..ec9e698412c 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -18,19 +18,47 @@ import dataclasses import itertools import typing +from typing import Union import bigframes.dtypes as dtypes import bigframes.operations +import bigframes.operations.aggregations as agg_ops def const(value: typing.Hashable, dtype: dtypes.ExpressionType = None) -> Expression: return ScalarConstantExpression(value, dtype or dtypes.infer_literal_type(value)) -def free_var(id: str) -> Expression: +def free_var(id: str) -> UnboundVariableExpression: return UnboundVariableExpression(id) +@dataclasses.dataclass(frozen=True) +class Aggregation(abc.ABC): + """Represents windowing or aggregation over a column.""" + + op: agg_ops.WindowOp = dataclasses.field() + + +@dataclasses.dataclass(frozen=True) +class UnaryAggregation(Aggregation): + op: agg_ops.UnaryWindowOp = dataclasses.field() + arg: Union[ + UnboundVariableExpression, ScalarConstantExpression + ] = dataclasses.field() + + +@dataclasses.dataclass(frozen=True) +class BinaryAggregation(Aggregation): + op: agg_ops.BinaryAggregateOp = dataclasses.field() + left: Union[ + UnboundVariableExpression, ScalarConstantExpression + ] = dataclasses.field() + right: Union[ + UnboundVariableExpression, ScalarConstantExpression + ] = dataclasses.field() + + @dataclasses.dataclass(frozen=True) class Expression(abc.ABC): """An expression represents a computation taking N scalar inputs and producing a single output scalar.""" diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index ab6b15e7b94..9a0889b0413 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -268,7 +268,7 @@ def _agg_string(self, func: str) -> df.DataFrame: return dataframe if self._as_index else self._convert_index(dataframe) def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: - aggregations: typing.List[typing.Tuple[str, agg_ops.AggregateOp]] = [] + aggregations: typing.List[typing.Tuple[str, agg_ops.UnaryAggregateOp]] = [] column_labels = [] want_aggfunc_level = any(utils.is_list_like(aggs) for aggs in func.values()) @@ -384,7 +384,7 @@ def _column_type(self, col_id: str) -> dtypes.Dtype: return dtype def _aggregate_all( - self, aggregate_op: agg_ops.AggregateOp, numeric_only: bool = False + self, aggregate_op: agg_ops.UnaryAggregateOp, numeric_only: bool = False ) -> df.DataFrame: aggregated_col_ids = self._aggregated_columns(numeric_only=numeric_only) aggregations = [(col_id, aggregate_op) for col_id in aggregated_col_ids] @@ -600,7 +600,7 @@ def expanding(self, min_periods: int = 1) -> windows.Window: is_series=True, ) - def _aggregate(self, aggregate_op: agg_ops.AggregateOp) -> series.Series: + def _aggregate(self, aggregate_op: agg_ops.UnaryAggregateOp) -> series.Series: result_block, _ = self._block.aggregate( self._by_col_ids, ((self._value_column, aggregate_op),), diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 5b2fa4ebb7a..3ae4fbe24a5 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -421,7 +421,7 @@ def _apply_unary_expr( block = block.set_index(result_ids, index_labels=self._block.index.names) return Index(block) - def _apply_aggregation(self, op: agg_ops.AggregateOp) -> typing.Any: + def _apply_aggregation(self, op: agg_ops.UnaryAggregateOp) -> typing.Any: if self.nlevels > 1: raise NotImplementedError(f"Multiindex does not yet support {op.name}") column_id = self._block.index_columns[0] diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 99ea229a44a..f637177a948 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -247,7 +247,8 @@ def __hash__(self): return self._node_hash -# TODO: Merge RowCount and Corr into Aggregate Node +# TODO: Merge RowCount into Aggregate Node? +# Row count can be compute from table metadata sometimes, so it is a bit special. @dataclass(frozen=True) class RowCountNode(UnaryNode): @property @@ -261,7 +262,7 @@ def non_local(self) -> bool: @dataclass(frozen=True) class AggregateNode(UnaryNode): - aggregations: typing.Tuple[typing.Tuple[str, agg_ops.AggregateOp, str], ...] + aggregations: typing.Tuple[typing.Tuple[ex.Aggregation, str], ...] by_column_ids: typing.Tuple[str, ...] = tuple([]) dropna: bool = True @@ -281,31 +282,10 @@ def non_local(self) -> bool: return True -# TODO: Unify into aggregate -@dataclass(frozen=True) -class CorrNode(UnaryNode): - corr_aggregations: typing.Tuple[typing.Tuple[str, str, str], ...] - - def __hash__(self): - return self._node_hash - - @property - def row_preserving(self) -> bool: - return False - - @property - def peekable(self) -> bool: - return False - - @property - def non_local(self) -> bool: - return True - - @dataclass(frozen=True) class WindowOpNode(UnaryNode): column_name: str - op: agg_ops.WindowOp + op: agg_ops.UnaryWindowOp window_spec: window.WindowSpec output_name: typing.Optional[str] = None never_skip_nulls: bool = False diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index 22e7d40c81e..ffbba109366 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -122,10 +122,14 @@ def cut( raise ValueError("`bins` should be a positive integer.") if isinstance(bins, Iterable): - if not isinstance(bins, pd.IntervalIndex): - bins = pd.IntervalIndex.from_tuples(list(bins)) - - if bins.is_overlapping: + if isinstance(bins, pd.IntervalIndex): + as_index: pd.IntervalIndex = bins + bins = tuple((bin.left.item(), bin.right.item()) for bin in bins) + else: + as_index = pd.IntervalIndex.from_tuples(list(bins)) + bins = tuple(bins) + + if as_index.is_overlapping: raise ValueError("Overlapping IntervalIndex is not accepted.") if labels is not None and labels is not False: @@ -148,6 +152,8 @@ def qcut( ) -> bigframes.series.Series: if isinstance(q, int) and q <= 0: raise ValueError("`q` should be a positive integer.") + if utils.is_list_like(q): + q = tuple(q) if labels is not False: raise NotImplementedError( @@ -162,7 +168,7 @@ def qcut( block, nullity_id = block.apply_unary_op(x._value_column, ops.notnull_op) block, result = block.apply_window_op( x._value_column, - agg_ops.QcutOp(q), + agg_ops.QcutOp(q), # type: ignore window_spec=core.WindowSpec( grouping_keys=(nullity_id,), ordering=(order.OrderingColumnReference(x._value_column),), diff --git a/bigframes/core/window/__init__.py b/bigframes/core/window/__init__.py index 240715b6df4..8711625f88a 100644 --- a/bigframes/core/window/__init__.py +++ b/bigframes/core/window/__init__.py @@ -64,7 +64,7 @@ def min(self): def _apply_aggregate( self, - op: agg_ops.AggregateOp, + op: agg_ops.UnaryAggregateOp, ): block = self._block labels = [block.col_id_to_label[col] for col in self._value_column_ids] diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index ba62ae28d2d..aed05e287bc 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -14,14 +14,13 @@ from __future__ import annotations +import abc +import dataclasses import typing - -from pandas import Int64Dtype -import pandas as pd - -import bigframes.dtypes as dtypes +from typing import ClassVar, Hashable, Optional, Tuple +@dataclasses.dataclass(frozen=True) class WindowOp: @property def skips_nulls(self): @@ -34,72 +33,110 @@ def handles_ties(self): return False +@dataclasses.dataclass(frozen=True) +class UnaryWindowOp(WindowOp): + @property + def arguments(self) -> int: + return 1 + + +@dataclasses.dataclass(frozen=True) class AggregateOp(WindowOp): - name = "abstract_aggregate" + """Aggregate ops can be applied with or without a window clause.""" + + @property + @abc.abstractmethod + def name(self) -> str: + ... + + @property + @abc.abstractmethod + def arguments(self) -> int: + ... + + +@dataclasses.dataclass(frozen=True) +class UnaryAggregateOp(AggregateOp, UnaryWindowOp): + @property + def arguments(self) -> int: + return 1 + + +@dataclasses.dataclass(frozen=True) +class BinaryAggregateOp(AggregateOp): + @property + def arguments(self) -> int: + return 2 + +@dataclasses.dataclass(frozen=True) +class SumOp(UnaryAggregateOp): + name: ClassVar[str] = "sum" -class SumOp(AggregateOp): - name = "sum" +@dataclasses.dataclass(frozen=True) +class MedianOp(UnaryAggregateOp): + name: ClassVar[str] = "median" -class MedianOp(AggregateOp): - name = "median" +@dataclasses.dataclass(frozen=True) +class ApproxQuartilesOp(UnaryAggregateOp): + quartile: int -class ApproxQuartilesOp(AggregateOp): - def __init__(self, quartile: int): - self.name = f"{quartile*25}%" - self._quartile = quartile + @property + def name(self): + return f"{self.quartile*25}%" -class MeanOp(AggregateOp): - name = "mean" +@dataclasses.dataclass(frozen=True) +class MeanOp(UnaryAggregateOp): + name: ClassVar[str] = "mean" -class ProductOp(AggregateOp): - name = "product" +@dataclasses.dataclass(frozen=True) +class ProductOp(UnaryAggregateOp): + name: ClassVar[str] = "product" -class MaxOp(AggregateOp): - name = "max" +@dataclasses.dataclass(frozen=True) +class MaxOp(UnaryAggregateOp): + name: ClassVar[str] = "max" -class MinOp(AggregateOp): - name = "min" +@dataclasses.dataclass(frozen=True) +class MinOp(UnaryAggregateOp): + name: ClassVar[str] = "min" -class StdOp(AggregateOp): - name = "std" +@dataclasses.dataclass(frozen=True) +class StdOp(UnaryAggregateOp): + name: ClassVar[str] = "std" -class VarOp(AggregateOp): - name = "var" +@dataclasses.dataclass(frozen=True) +class VarOp(UnaryAggregateOp): + name: ClassVar[str] = "var" -class PopVarOp(AggregateOp): - name = "popvar" +@dataclasses.dataclass(frozen=True) +class PopVarOp(UnaryAggregateOp): + name: ClassVar[str] = "popvar" -class CountOp(AggregateOp): - name = "count" +@dataclasses.dataclass(frozen=True) +class CountOp(UnaryAggregateOp): + name: ClassVar[str] = "count" @property def skips_nulls(self): return False -class CutOp(WindowOp): - def __init__(self, bins: typing.Union[int, pd.IntervalIndex], labels=None): - if isinstance(bins, int): - if not bins > 0: - raise ValueError("`bins` should be a positive integer.") - self._bins_int = bins - self._bins = dtypes.literal_to_ibis_scalar(bins, force_dtype=Int64Dtype()) - else: - self._bins_int = 0 - self._bins = bins - - self._labels = labels +@dataclasses.dataclass(frozen=True) +class CutOp(UnaryWindowOp): + # TODO: Unintuitive, refactor into multiple ops? + bins: typing.Union[int, Tuple[Tuple[Hashable, Hashable], ...]] + labels: Optional[bool] @property def skips_nulls(self): @@ -110,10 +147,13 @@ def handles_ties(self): return True -class QcutOp(WindowOp): - def __init__(self, quantiles: typing.Union[int, typing.Sequence[float]]): - self.name = f"qcut-{quantiles}" - self._quantiles = quantiles +@dataclasses.dataclass(frozen=True) +class QcutOp(UnaryWindowOp): + quantiles: typing.Union[int, typing.Tuple[float, ...]] + + @property + def name(self): + return f"qcut-{self.quantiles}" @property def skips_nulls(self): @@ -124,26 +164,29 @@ def handles_ties(self): return True -class NuniqueOp(AggregateOp): - name = "nunique" +@dataclasses.dataclass(frozen=True) +class NuniqueOp(UnaryAggregateOp): + name: ClassVar[str] = "nunique" @property def skips_nulls(self): return False -class AnyValueOp(AggregateOp): +@dataclasses.dataclass(frozen=True) +class AnyValueOp(UnaryAggregateOp): # Warning: only use if all values are equal. Non-deterministic otherwise. # Do not expose to users. For special cases only (e.g. pivot). - name = "any_value" + name: ClassVar[str] = "any_value" @property def skips_nulls(self): return True -class RankOp(WindowOp): - name = "rank" +@dataclasses.dataclass(frozen=True) +class RankOp(UnaryWindowOp): + name: ClassVar[str] = "rank" @property def skips_nulls(self): @@ -154,7 +197,8 @@ def handles_ties(self): return True -class DenseRankOp(WindowOp): +@dataclasses.dataclass(frozen=True) +class DenseRankOp(UnaryWindowOp): @property def skips_nulls(self): return False @@ -164,50 +208,61 @@ def handles_ties(self): return True -class FirstOp(WindowOp): - name = "first" +@dataclasses.dataclass(frozen=True) +class FirstOp(UnaryWindowOp): + name: ClassVar[str] = "first" -class FirstNonNullOp(WindowOp): +@dataclasses.dataclass(frozen=True) +class FirstNonNullOp(UnaryWindowOp): @property def skips_nulls(self): return False -class LastOp(WindowOp): - name = "last" +@dataclasses.dataclass(frozen=True) +class LastOp(UnaryWindowOp): + name: ClassVar[str] = "last" -class LastNonNullOp(WindowOp): +@dataclasses.dataclass(frozen=True) +class LastNonNullOp(UnaryWindowOp): @property def skips_nulls(self): return False -class ShiftOp(WindowOp): - def __init__(self, periods: int): - self._periods = periods +@dataclasses.dataclass(frozen=True) +class ShiftOp(UnaryWindowOp): + periods: int @property def skips_nulls(self): return False -class DiffOp(WindowOp): - def __init__(self, periods: int): - self._periods = periods +@dataclasses.dataclass(frozen=True) +class DiffOp(UnaryWindowOp): + periods: int @property def skips_nulls(self): return False -class AllOp(AggregateOp): - name = "all" +@dataclasses.dataclass(frozen=True) +class AllOp(UnaryAggregateOp): + name: ClassVar[str] = "all" + + +@dataclasses.dataclass(frozen=True) +class AnyOp(UnaryAggregateOp): + name: ClassVar[str] = "any" -class AnyOp(AggregateOp): - name = "any" +@dataclasses.dataclass(frozen=True) +class CorrOp(BinaryAggregateOp): + name: ClassVar[str] = "corr" sum_op = SumOp() @@ -228,7 +283,7 @@ class AnyOp(AggregateOp): # TODO: Alternative names and lookup from numpy function objects -_AGGREGATIONS_LOOKUP: dict[str, AggregateOp] = { +_AGGREGATIONS_LOOKUP: dict[str, UnaryAggregateOp] = { op.name: op for op in [ sum_op, @@ -250,7 +305,7 @@ class AnyOp(AggregateOp): } -def lookup_agg_func(key: str) -> AggregateOp: +def lookup_agg_func(key: str) -> UnaryAggregateOp: if callable(key): raise NotImplementedError( "Aggregating with callable object not supported, pass method name as string instead (eg. 'sum' instead of np.sum)." diff --git a/bigframes/series.py b/bigframes/series.py index 649851e716e..6167ce0966a 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1045,7 +1045,7 @@ def _align3(self, other1: Series | scalars.Scalar, other2: Series | scalars.Scal values, index = self._align_n([other1, other2], how) return (values[0], values[1], values[2], index) - def _apply_aggregation(self, op: agg_ops.AggregateOp) -> Any: + def _apply_aggregation(self, op: agg_ops.UnaryAggregateOp) -> Any: return self._block.get_stat(self._value_column, op) def _apply_window_op( diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 9f415f3bc45..a7f45efc858 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -16,6 +16,7 @@ import pandas import bigframes.core as core +import bigframes.core.expression as ex import bigframes.core.ordering import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -196,7 +197,9 @@ def test_arrayvalue_to_ibis_expr_with_aggregate(): total_ordering_columns=["col1"], ) expr = value.aggregate( - aggregations=(("col1", agg_ops.sum_op, "col4"),), + aggregations=( + (ex.UnaryAggregation(agg_ops.sum_op, ex.free_var("col1")), "col4"), + ), by_column_ids=["col1"], dropna=False, )._compile_ordered() From 44bfe46dd638a4e3c4ffcf02135a04a3eec9154d Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 6 Feb 2024 05:21:19 +0000 Subject: [PATCH 298/921] test: fix unit tests for handling drive access issues (#352) * test: fix unit tests for handling drive access issues * refactor test logic * adapt new create_bigquery_session in one failing test --- tests/unit/resources.py | 24 ++++++++++++++++-------- tests/unit/session/test_session.py | 30 +++++++++++++++++++++++------- tests/unit/test_compute_options.py | 4 +++- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/tests/unit/resources.py b/tests/unit/resources.py index b239b04671f..967e42548f1 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime from typing import Dict, List, Optional import unittest.mock as mock @@ -49,14 +50,21 @@ def create_bigquery_session( "test_dataset", ) - query_job = mock.create_autospec(google.cloud.bigquery.QueryJob) - type(query_job).destination = mock.PropertyMock( - return_value=anonymous_dataset.table("test_table"), - ) - type(query_job).session_info = google.cloud.bigquery.SessionInfo( - {"sessionInfo": {"sessionId": session_id}}, - ) - bqclient.query.return_value = query_job + def query_mock(query, *args, **kwargs): + query_job = mock.create_autospec(google.cloud.bigquery.QueryJob) + type(query_job).destination = mock.PropertyMock( + return_value=anonymous_dataset.table("test_table"), + ) + type(query_job).session_info = google.cloud.bigquery.SessionInfo( + {"sessionInfo": {"sessionId": session_id}}, + ) + + if query.startswith("SELECT CURRENT_TIMESTAMP()"): + query_job.result = mock.MagicMock(return_value=[[datetime.datetime.now()]]) + + return query_job + + bqclient.query = query_mock clients_provider = mock.create_autospec(bigframes.session.clients.ClientsProvider) type(clients_provider).bqclient = mock.PropertyMock(return_value=bqclient) diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 5fc8996993e..ea8d0882ae8 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -63,15 +63,31 @@ def test_read_gbq_not_found_tables(not_found_table_id): ], ) def test_read_gbq_external_table_no_drive_access(api_name, query_or_table): - bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) - bqclient.project = "test-project" - bqclient.get_table.side_effect = google.api_core.exceptions.Forbidden( - "Access Denied: BigQuery BigQuery: Permission denied while getting Drive credentials." - ) - session = resources.create_bigquery_session(bqclient=bqclient) + session = resources.create_bigquery_session() + session_query_mock = session.bqclient.query + + def query_mock(query, *args, **kwargs): + if query.lstrip().startswith("SELECT *"): + raise google.api_core.exceptions.Forbidden( + "Access Denied: BigQuery BigQuery: Permission denied while getting Drive credentials." + ) + + return session_query_mock(query, *args, **kwargs) + + session.bqclient.query = query_mock + + def get_table_mock(dataset_ref): + dataset = google.cloud.bigquery.Dataset(dataset_ref) + dataset.location = session._location + return dataset + + session.bqclient.get_table = get_table_mock api = getattr(session, api_name) - with pytest.raises(google.api_core.exceptions.Forbidden): + with pytest.raises( + google.api_core.exceptions.Forbidden, + match="Check https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions.", + ): api(query_or_table) diff --git a/tests/unit/test_compute_options.py b/tests/unit/test_compute_options.py index a613bca7b90..2de715a40ec 100644 --- a/tests/unit/test_compute_options.py +++ b/tests/unit/test_compute_options.py @@ -11,6 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock + import bigframes as bf from . import resources @@ -18,8 +20,8 @@ def test_maximum_bytes_option(): session = resources.create_bigquery_session() + session.bqclient.query = mock.MagicMock() with bf.option_context("compute.maximum_bytes_billed", 10000): - session.bqclient.query.reset_mock() session._start_query("query") call = session.bqclient.query.call_args assert call.kwargs["job_config"].maximum_bytes_billed == 10000 From cff09194b2c3a96a1f50e86a38ee59783c2a343b Mon Sep 17 00:00:00 2001 From: Stephanie A <129541811+DevStephanie@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:43:19 -0600 Subject: [PATCH 299/921] docs: Add a sample to demonstrate the evaluation results (#364) * docs: Add a sample to demonstrate the evaluation results * Adding comments explaining logistic regression results * editing read_gbd explanation * Creating link for ML course * Formatting metrics * Update samples/snippets/bqml_getting_started_test.py --------- Co-authored-by: Tim Swast --- samples/snippets/bqml_getting_started_test.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/samples/snippets/bqml_getting_started_test.py b/samples/snippets/bqml_getting_started_test.py index 783f963feb5..bb282fa5637 100644 --- a/samples/snippets/bqml_getting_started_test.py +++ b/samples/snippets/bqml_getting_started_test.py @@ -91,3 +91,78 @@ def test_bqml_getting_started(random_model_id): replace=True, ) # [END bigquery_dataframes_bqml_getting_started_tutorial] + + # [START bigquery_dataframes_bqml_getting_started_tutorial_evaluate] + import bigframes.pandas as bpd + + # Select model you'll use for training. `read_gbq_model` loads model data from a + # BigQuery, but you could also use the `model` object from the previous steps. + model = bpd.read_gbq_model( + your_model_id, # For example: "bqml_tutorial.sample_model", + ) + + # The WHERE clause — _TABLE_SUFFIX BETWEEN '20170701' AND '20170801' — + # limits the number of tables scanned by the query. The date range scanned is + # July 1, 2017 to August 1, 2017. This is the data you're using to evaluate the predictive performance + # of the model. It was collected in the month immediately following the time + # period spanned by the training data. + + df = bpd.read_gbq( + """ + SELECT GENERATE_UUID() AS rowindex, * + FROM + `bigquery-public-data.google_analytics_sample.ga_sessions_*` + WHERE + _TABLE_SUFFIX BETWEEN '20170701' AND '20170801' + """, + index_col="rowindex", + ) + transactions = df["totals"].struct.field("transactions") + label = transactions.notnull().map({True: 1, False: 0}) + operatingSystem = df["device"].struct.field("operatingSystem") + operatingSystem = operatingSystem.fillna("") + isMobile = df["device"].struct.field("isMobile") + country = df["geoNetwork"].struct.field("country").fillna("") + pageviews = df["totals"].struct.field("pageviews").fillna(0) + features = bpd.DataFrame( + { + "os": operatingSystem, + "is_mobile": isMobile, + "country": country, + "pageviews": pageviews, + } + ) + + # Some models include a convenient .score(X, y) method for evaluation with a preset accuracy metric: + + # Because you performed a logistic regression, the results include the following columns: + + # - precision — A metric for classification models. Precision identifies the frequency with + # which a model was correct when predicting the positive class. + + # - recall — A metric for classification models that answers the following question: + # Out of all the possible positive labels, how many did the model correctly identify? + + # - accuracy — Accuracy is the fraction of predictions that a classification model got right. + + # - f1_score — A measure of the accuracy of the model. The f1 score is the harmonic average of + # the precision and recall. An f1 score's best value is 1. The worst value is 0. + + # - log_loss — The loss function used in a logistic regression. This is the measure of how far the + # model's predictions are from the correct labels. + + # - roc_auc — The area under the ROC curve. This is the probability that a classifier is more confident that + # a randomly chosen positive example + # is actually positive than that a randomly chosen negative example is positive. For more information, + # see ['Classification']('https://developers.google.com/machine-learning/crash-course/classification/video-lecture') + # in the Machine Learning Crash Course. + + model.score(features, label) + # precision recall accuracy f1_score log_loss roc_auc + # 0 0.412621 0.079143 0.985074 0.132812 0.049764 0.974285 + # [1 rows x 6 columns] + # [END bigquery_dataframes_bqml_getting_started_tutorial_evaluate] + + # [START bigquery_dataframes_bqml_getting_started_tutorial_predict] + + # [END bigquery_dataframes_bqml_getting_started_tutorial_predict] From 91596b866b0e028e1b81196e98d3ebcde474eb17 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:21:10 -0600 Subject: [PATCH 300/921] chore(main): release 0.20.1 (#362) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ bigframes/version.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 771f04776ef..6e2ab05f477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.20.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.20.0...v0.20.1) (2024-02-06) + + +### Performance Improvements + +* Make repr cache the block where appropriate ([#350](https://github.com/googleapis/python-bigquery-dataframes/issues/350)) ([068879f](https://github.com/googleapis/python-bigquery-dataframes/commit/068879f97fb1626aca081106150803f832a0cf81)) + + +### Documentation + +* Add a sample to demonstrate the evaluation results ([#364](https://github.com/googleapis/python-bigquery-dataframes/issues/364)) ([cff0919](https://github.com/googleapis/python-bigquery-dataframes/commit/cff09194b2c3a96a1f50e86a38ee59783c2a343b)) +* Fix the `DataFrame.apply` code sample ([#366](https://github.com/googleapis/python-bigquery-dataframes/issues/366)) ([1866a26](https://github.com/googleapis/python-bigquery-dataframes/commit/1866a266f0fa40882b589579654c1ad428b036d8)) + ## [0.20.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.19.2...v0.20.0) (2024-01-30) diff --git a/bigframes/version.py b/bigframes/version.py index 131f820e7d5..fca3eec0237 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.0" +__version__ = "0.20.1" From 443db228375da9b232376140c9d5b0db14895eae Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 7 Feb 2024 13:18:15 -0800 Subject: [PATCH 301/921] feat: add `Series.cov` method (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 22 ------------------- bigframes/core/blocks.py | 19 +++++++++------- bigframes/core/compile/aggregate_compiler.py | 13 +++++++++++ bigframes/operations/aggregations.py | 5 +++++ bigframes/operations/base.py | 7 ++++-- bigframes/series.py | 9 +++++--- tests/system/small/test_series.py | 13 ++++++++++- tests/unit/test_core.py | 20 ----------------- .../bigframes_vendored/pandas/core/series.py | 21 ++++++++++++++++++ 9 files changed, 73 insertions(+), 56 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index ea169dbb743..4dc2e4d7af0 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -261,28 +261,6 @@ def aggregate( ) ) - def corr_aggregate( - self, corr_aggregations: typing.Sequence[typing.Tuple[str, str, str]] - ) -> ArrayValue: - """ - Get correlations between each lef_column_id and right_column_id, stored in the respective output_column_id. - This uses BigQuery's CORR under the hood, and thus only Pearson's method is used. - Arguments: - corr_aggregations: left_column_id, right_column_id, output_column_id tuples - """ - aggregations = tuple( - ( - ex.BinaryAggregation( - agg_ops.CorrOp(), ex.free_var(agg[0]), ex.free_var(agg[1]) - ), - agg[2], - ) - for agg in corr_aggregations - ) - return ArrayValue( - nodes.AggregateNode(child=self.node, aggregations=aggregations) - ) - def project_window_op( self, column_name: str, diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index e758e203353..34df7231cc2 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1040,26 +1040,29 @@ def get_stat(self, column_id: str, stat: agg_ops.UnaryAggregateOp): self._stats_cache[column_id].update(stats_map) return stats_map[stat.name] - def get_corr_stat(self, column_id_left: str, column_id_right: str): + def get_binary_stat( + self, column_id_left: str, column_id_right: str, stat: agg_ops.BinaryAggregateOp + ): # TODO(kemppeterson): Clean up the column names for DataFrames.corr support # TODO(kemppeterson): Add a cache here. - corr_aggregations = [ + aggregations = [ ( - column_id_left, - column_id_right, - "corr_" + column_id_left + column_id_right, + ex.BinaryAggregation( + stat, ex.free_var(column_id_left), ex.free_var(column_id_right) + ), + f"{stat.name}_{column_id_left}{column_id_right}", ) ] - expr = self.expr.corr_aggregate(corr_aggregations) + expr = self.expr.aggregate(aggregations) offset_index_id = guid.generate_guid() expr = expr.promote_offsets(offset_index_id) block = Block( expr, index_columns=[offset_index_id], - column_labels=[a[2] for a in corr_aggregations], + column_labels=[a[1] for a in aggregations], ) df, _ = block.to_pandas() - return df.loc[0, "corr_" + column_id_left + column_id_right] + return df.loc[0, f"{stat.name}_{column_id_left}{column_id_right}"] def summarize( self, diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 0dbc0e73107..1dad128599c 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -431,6 +431,19 @@ def _( return cast(ibis_types.NumericColumn, bq_corr) +@compile_binary_agg.register +def _( + op: agg_ops.CovOp, left: ibis_types.Column, right: ibis_types.Column, window=None +) -> ibis_types.NumericValue: + # Will be null if all inputs are null. Pandas defaults to zero sum though. + left_numeric = cast(ibis_types.NumericColumn, left) + right_numeric = cast(ibis_types.NumericColumn, right) + bq_cov = _apply_window_if_present( + left_numeric.cov(right_numeric, how="sample"), window + ) + return cast(ibis_types.NumericColumn, bq_cov) + + def _apply_window_if_present(value: ibis_types.Value, window): return value.over(window) if (window is not None) else value diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index aed05e287bc..6301ece865c 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -265,6 +265,11 @@ class CorrOp(BinaryAggregateOp): name: ClassVar[str] = "corr" +@dataclasses.dataclass(frozen=True) +class CovOp(BinaryAggregateOp): + name: ClassVar[str] = "cov" + + sum_op = SumOp() mean_op = MeanOp() median_op = MedianOp() diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 4aad9479e7a..04114b43cb3 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -25,6 +25,7 @@ import bigframes.core.scalar as scalars import bigframes.dtypes import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops import bigframes.series as series import bigframes.session import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing @@ -188,10 +189,12 @@ def _apply_binary_op( block, result_id = self._block.project_expr(expr, name) return series.Series(block.select_column(result_id)) - def _apply_corr_aggregation(self, other: series.Series) -> float: + def _apply_binary_aggregation( + self, other: series.Series, stat: agg_ops.BinaryAggregateOp + ) -> float: (left, right, block) = self._align(other, how="outer") - return block.get_corr_stat(left, right) + return block.get_binary_stat(left, right, stat) def _align(self, other: series.Series, how="outer") -> tuple[str, str, blocks.Block]: # type: ignore """Aligns the series value with another scalar or series object. Returns new left column id, right column id and joined tabled expression.""" diff --git a/bigframes/series.py b/bigframes/series.py index 6167ce0966a..753e195e0ae 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -734,8 +734,8 @@ def round(self, decimals=0) -> "Series": return self._apply_binary_op(decimals, ops.round_op) def corr(self, other: Series, method="pearson", min_periods=None) -> float: - # TODO(kemppeterson): Validate early that both are numeric - # TODO(kemppeterson): Handle partially-numeric columns + # TODO(tbergeron): Validate early that both are numeric + # TODO(tbergeron): Handle partially-numeric columns if method != "pearson": raise NotImplementedError( f"Only Pearson correlation is currently supported. {constants.FEEDBACK_LINK}" @@ -744,7 +744,10 @@ def corr(self, other: Series, method="pearson", min_periods=None) -> float: raise NotImplementedError( f"min_periods not yet supported. {constants.FEEDBACK_LINK}" ) - return self._apply_corr_aggregation(other) + return self._apply_binary_aggregation(other, agg_ops.CorrOp()) + + def cov(self, other: Series) -> float: + return self._apply_binary_aggregation(other, agg_ops.CovOp()) def all(self) -> bool: return typing.cast(bool, self._apply_aggregation(agg_ops.all_op)) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 5d8fb0b29c7..f2790d190aa 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -656,7 +656,7 @@ def test_mods(scalars_dfs, col_x, col_y, method): # We work around a pandas bug that doesn't handle correlating nullable dtypes by doing this # manually with dumb self-correlation instead of parameterized as test_mods is above. -def test_corr(scalars_dfs): +def test_series_corr(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df["int64_too"].corr(scalars_df["int64_too"]) pd_result = ( @@ -667,6 +667,17 @@ def test_corr(scalars_dfs): assert math.isclose(pd_result, bf_result) +def test_series_cov(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["int64_too"].cov(scalars_df["int64_too"]) + pd_result = ( + scalars_pandas_df["int64_too"] + .astype("int64") + .cov(scalars_pandas_df["int64_too"].astype("int64")) + ) + assert math.isclose(pd_result, bf_result) + + @pytest.mark.parametrize( ("col_x",), [ diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index a7f45efc858..42cbcbbc9f9 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -208,23 +208,3 @@ def test_arrayvalue_to_ibis_expr_with_aggregate(): assert actual.columns[0] == "col1" assert actual.columns[1] == "col4" assert expr.columns[1].type().is_int64() - - -def test_arrayvalue_to_ibis_expr_with_corr_aggregate(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value.corr_aggregate( - corr_aggregations=[("col1", "col3", "col4")] - )._compile_ordered() - actual = expr._to_ibis_expr(ordering_mode="unordered") - assert len(expr.columns) == 1 - assert actual.columns[0] == "col4" - assert expr.columns[0].type().is_float64() diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 9e376c713e6..4232d3ec2a4 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -842,6 +842,27 @@ def corr(self, other, method="pearson", min_periods=None) -> float: """ raise NotImplementedError("abstract method") + def cov( + self, + other, + ) -> float: + """ + Compute covariance with Series, excluding missing values. + + The two `Series` objects are not required to be the same length and + will be aligned internally before the covariance is calculated. + + Args: + other (Series): + Series with which to compute the covariance. + + Returns: + float: + Covariance between Series and other normalized by N-1 + (unbiased estimator). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def diff(self) -> Series: """ First discrete difference of element. From d5518b28509be0ce070b22d9134a6a662412010a Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:10:15 -0800 Subject: [PATCH 302/921] feat: add XGBoostModel (#363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #321809936 🦕 --- README.rst | 2 + bigframes/ml/core.py | 30 +++++++ bigframes/ml/globals.py | 11 +++ bigframes/ml/imported.py | 117 ++++++++++++++++++++++++- bigframes/ml/loader.py | 2 + bigframes/ml/remote.py | 12 +-- bigframes/ml/sql.py | 18 ++++ docs/templates/toc.yml | 2 + tests/system/small/ml/conftest.py | 40 +++++++++ tests/system/small/ml/test_imported.py | 42 ++++++++- tests/unit/ml/test_sql.py | 24 +++++ 11 files changed, 287 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 38f7d0ab7af..a977a9f31ec 100644 --- a/README.rst +++ b/README.rst @@ -172,6 +172,8 @@ Create estimators for imported models by using the `bigframes.ml.imported module to import Open Neural Network Exchange (ONNX) models. * Use the `TensorFlowModel class `_ to import TensorFlow models. +* Use the `XGBoostModel class `_ + to import XGBoostModel models. **Linear models** diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 266ab1b058d..51fed059010 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -355,3 +355,33 @@ def create_imported_model( ) return self._create_model_with_sql(session=session, sql=sql) + + def create_xgboost_imported_model( + self, + session: bigframes.Session, + input: Mapping[str, str] = {}, + output: Mapping[str, str] = {}, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> BqmlModel: + """Create a session-temporary BQML imported model with the CREATE OR REPLACE MODEL statement + + Args: + input: + input schema for imported xgboost models + output: + output schema for imported xgboost models + options: a dict of options to configure the model. Generates a BQML OPTIONS + clause + + Returns: a BqmlModel, wrapping a trained model in BigQuery + """ + model_ref = self._create_model_ref(session._anonymous_dataset) + + sql = self._model_creation_sql_generator.create_xgboost_imported_model( + model_ref=model_ref, + input=input, + output=output, + options=options, + ) + + return self._create_model_with_sql(session=session, sql=sql) diff --git a/bigframes/ml/globals.py b/bigframes/ml/globals.py index c139476daaa..44e94637276 100644 --- a/bigframes/ml/globals.py +++ b/bigframes/ml/globals.py @@ -19,6 +19,17 @@ _BASE_SQL_GENERATOR = sql.BaseSqlGenerator() _BQML_MODEL_FACTORY = core.BqmlModelFactory() +_SUPPORTED_DTYPES = ( + "bool", + "string", + "int64", + "float64", + "array", + "array", + "array", + "array", +) + def base_sql_generator() -> sql.BaseSqlGenerator: """Base SQL Generator.""" diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index 4ae0a8ea4d0..e2be1547035 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -16,13 +16,14 @@ from __future__ import annotations -from typing import cast, Optional, Union +from typing import cast, Mapping, Optional, Union from google.cloud import bigquery import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils +from bigframes.ml.globals import _SUPPORTED_DTYPES import bigframes.pandas as bpd @@ -176,3 +177,117 @@ def to_gbq(self, model_name: str, replace: bool = False) -> ONNXModel: new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) + + +@log_adapter.class_logger +class XGBoostModel(base.Predictor): + """Imported XGBoost model. + + .. warning:: + + Imported XGBoost models have the several limitations. See: + https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-xgboost#limitations + + Args: + session (BigQuery Session): + BQ session to create the model + input (Dict, default None): + Specify the model input schema information when you + create the XGBoost model. The input should be the format of + {field_name: field_type}. Input is optional only if feature_names + and feature_types are both specified in the model file. Supported types + are "bool", "string", "int64", "float64", "array", "array", "array", "array". + output (Dict, default None): + Specify the model output schema information when you + create the XGBoost model. The input should be the format of + {field_name: field_type}. Output is optional only if feature_names + and feature_types are both specified in the model file. Supported types + are "bool", "string", "int64", "float64", "array", "array", "array", "array". + model_path (str): + Cloud Storage path that holds the model files.""" + + def __init__( + self, + session: Optional[bigframes.Session] = None, + input: Mapping[str, str] = {}, + output: Mapping[str, str] = {}, + model_path: Optional[str] = None, + ): + self.session = session or bpd.get_global_session() + self.model_path = model_path + self.input = input + self.output = output + self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + + def _create_bqml_model(self): + options = {"model_type": "XGBOOST", "model_path": self.model_path} + + if not self.input and not self.output: + return self._bqml_model_factory.create_imported_model( + session=self.session, options=options + ) + else: + for io in (self.input, self.output): + for v in io.values(): + if v not in _SUPPORTED_DTYPES: + raise ValueError( + f"field_type {v} is not supported. We only support {', '.join(_SUPPORTED_DTYPES)}." + ) + + return self._bqml_model_factory.create_xgboost_imported_model( + session=self.session, + input=self.input, + output=self.output, + options=options, + ) + + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> XGBoostModel: + assert model.model_type == "XGBOOST" + + xgboost_model = cls(session=session, model_path=None) + xgboost_model._bqml_model = core.BqmlModel(session, model) + return xgboost_model + + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + """Predict the result from input DataFrame. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame or Series, schema is defined by the model. + + Returns: + bigframes.dataframe.DataFrame: Output DataFrame, schema is defined by the model.""" + + if not self._bqml_model: + if self.model_path is None: + raise ValueError("Model GCS path must be provided.") + self._bqml_model = self._create_bqml_model() + self._bqml_model = cast(core.BqmlModel, self._bqml_model) + + (X,) = utils.convert_to_dataframe(X) + + return self._bqml_model.predict(X) + + def to_gbq(self, model_name: str, replace: bool = False) -> XGBoostModel: + """Save the model to BigQuery. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + XGBoostModel: saved model.""" + if not self._bqml_model: + if self.model_path is None: + raise ValueError("Model GCS path must be provided.") + self._bqml_model = self._create_bqml_model() + self._bqml_model = cast(core.BqmlModel, self._bqml_model) + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 4ffde43543a..99a31922d87 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -45,6 +45,7 @@ "RANDOM_FOREST_CLASSIFIER": ensemble.RandomForestClassifier, "TENSORFLOW": imported.TensorFlowModel, "ONNX": imported.ONNXModel, + "XGBOOST": imported.XGBoostModel, } ) @@ -72,6 +73,7 @@ def from_bq( ensemble.RandomForestClassifier, imported.TensorFlowModel, imported.ONNXModel, + imported.XGBoostModel, llm.PaLM2TextGenerator, llm.PaLM2TextEmbeddingGenerator, pipeline.Pipeline, diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index 8da073802d7..44fde4f32f7 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -23,19 +23,9 @@ from bigframes import clients from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils +from bigframes.ml.globals import _SUPPORTED_DTYPES import bigframes.pandas as bpd -_SUPPORTED_DTYPES = ( - "bool", - "string", - "int64", - "float64", - "array", - "array", - "array", - "array", -) - _REMOTE_MODEL_STATUS = "remote_model_status" diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 152f881ec04..f726317bb4a 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -193,6 +193,24 @@ def create_imported_model( parts.append(self.options(**options)) return "\n".join(parts) + def create_xgboost_imported_model( + self, + model_ref: google.cloud.bigquery.ModelReference, + input: Mapping[str, str] = {}, + output: Mapping[str, str] = {}, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> str: + """Encode the CREATE OR REPLACE MODEL statement for BQML remote model.""" + + parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] + if input: + parts.append(self.input(**input)) + if output: + parts.append(self.output(**output)) + if options: + parts.append(self.options(**options)) + return "\n".join(parts) + class ModelManipulationSqlGenerator(BaseSqlGenerator): """Sql generator for manipulating a model entity. Model name is the full model path of project_id.dataset_id.model_id.""" diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index b680a5fc1ad..d387a513a12 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -91,6 +91,8 @@ uid: bigframes.ml.imported.ONNXModel - name: TensorFlowModel uid: bigframes.ml.imported.TensorFlowModel + - name: XGBoostModel + uid: bigframes.ml.imported.XGBoostModel name: imported - items: - name: Overview diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index e3180d28921..422ea6f1f4a 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -190,11 +190,29 @@ def onnx_iris_pandas_df(): ) +@pytest.fixture(scope="session") +def xgboost_iris_pandas_df(): + """Data matching the iris dataset.""" + return pd.DataFrame( + { + "sepal_length": [4.9, 5.1, 34.7], + "sepal_width": [3.0, 5.1, 24.7], + "petal_length": [1.4, 1.5, 13.3], + "petal_width": [0.4, 0.2, 18.3], + } + ) + + @pytest.fixture(scope="session") def onnx_iris_df(session, onnx_iris_pandas_df): return session.read_pandas(onnx_iris_pandas_df) +@pytest.fixture(scope="session") +def xgboost_iris_df(session, xgboost_iris_pandas_df): + return session.read_pandas(xgboost_iris_pandas_df) + + @pytest.fixture(scope="session") def llm_text_df(session, llm_text_pandas_df): return session.read_pandas(llm_text_pandas_df) @@ -322,6 +340,11 @@ def imported_onnx_model_path() -> str: return "gs://cloud-samples-data/bigquery/ml/onnx/pipeline_rf.onnx" +@pytest.fixture(scope="session") +def imported_xgboost_array_model_path() -> str: + return "gs://bigframes-dev-testing/xgboost-testdata/model.bst" + + @pytest.fixture(scope="session") def imported_tensorflow_model( session, imported_tensorflow_model_path @@ -346,3 +369,20 @@ def imported_onnx_model(session, imported_onnx_model_path) -> imported.ONNXModel session=session, model_path=imported_onnx_model_path, ) + + +@pytest.fixture(scope="session") +def imported_xgboost_model( + session, imported_xgboost_array_model_path +) -> imported.XGBoostModel: + return imported.XGBoostModel( + session=session, + input={ + "petal_length": "float64", + "petal_width": "float64", + "sepal_length": "float64", + "sepal_width": "float64", + }, + output={"predicted_label": "float64"}, + model_path=imported_xgboost_array_model_path, + ) diff --git a/tests/system/small/ml/test_imported.py b/tests/system/small/ml/test_imported.py index 8ffd9924e94..2b8d04c3aef 100644 --- a/tests/system/small/ml/test_imported.py +++ b/tests/system/small/ml/test_imported.py @@ -70,7 +70,7 @@ def test_onnx_create_model(imported_onnx_model): def test_onnx_create_model_default_session(imported_onnx_model_path): - model = imported.TensorFlowModel(model_path=imported_onnx_model_path) + model = imported.ONNXModel(model_path=imported_onnx_model_path) assert model is not None @@ -100,3 +100,43 @@ def test_onnx_model_to_gbq(imported_onnx_model: imported.ONNXModel, dataset_id: imported_onnx_model.to_gbq(f"{dataset_id}.test_onnx_model", replace=True) with pytest.raises(google.api_core.exceptions.Conflict): imported_onnx_model.to_gbq(f"{dataset_id}.test_onnx_model") + + +def test_xgboost_create_model(imported_xgboost_model): + # Model creation doesn't return error + assert imported_xgboost_model is not None + + +def test_xgboost_create_model_default_session(imported_xgboost_array_model_path): + model = imported.XGBoostModel(model_path=imported_xgboost_array_model_path) + assert model is not None + + +def test_xgboost_model_predict(imported_xgboost_model, xgboost_iris_df): + predictions = imported_xgboost_model.predict(xgboost_iris_df).to_pandas() + assert predictions.shape == (3, 5) + result = predictions[["predicted_label"]] + value1 = np.array([0.00362173, 0.01580198, 0.98057634]) + value2 = np.array([0.00349651, 0.00999565, 0.98650789]) + value3 = np.array([0.00561748, 0.0108124, 0.98357016]) + expected = pd.DataFrame( + { + "predicted_label": [value1, value2, value3], + }, + index=pd.Index([0, 1, 2], dtype="Int64"), + ) + pd.testing.assert_frame_equal( + result, + expected, + check_exact=False, + check_dtype=False, + atol=0.1, + ) + + +def test_xgboost_model_to_gbq( + imported_xgboost_model: imported.XGBoostModel, dataset_id: str +): + imported_xgboost_model.to_gbq(f"{dataset_id}.test_xgboost_model", replace=True) + with pytest.raises(google.api_core.exceptions.Conflict): + imported_xgboost_model.to_gbq(f"{dataset_id}.test_xgboost_model") diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 37cc33d33e2..de80dad710d 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -234,6 +234,30 @@ def test_create_imported_model_produces_correct_sql( ) +def test_create_xgboost_imported_model_produces_correct_sql( + model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, +): + sql = model_creation_sql_generator.create_xgboost_imported_model( + model_ref=bigquery.ModelReference.from_string( + "test-proj._anonXYZ.create_xgboost_imported_model" + ), + input={"column1": "int64"}, + output={"result": "array"}, + options={"option_key1": "option_value1", "option_key2": 2}, + ) + assert ( + sql + == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_xgboost_imported_model` +INPUT( + column1 int64) +OUTPUT( + result array) +OPTIONS( + option_key1="option_value1", + option_key2=2)""" + ) + + def test_alter_model_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): From 930cf6b9ae8a48f422586dbd21b52e15c9ef9492 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:10:18 -0800 Subject: [PATCH 303/921] fix: error message fix. (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 4 ++-- bigframes/functions/remote_function.py | 2 +- bigframes/series.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 2729d237017..9db567a4979 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1532,7 +1532,7 @@ def _filter_rows( ) -> DataFrame: if len(self._block.index_columns) > 1: raise NotImplementedError( - "Method filter does not support rows multiindex. {constants.FEEDBACK_LINK}" + f"Method filter does not support rows multiindex. {constants.FEEDBACK_LINK}" ) if (like is not None) or (regex is not None): block = self._block @@ -1760,7 +1760,7 @@ def dropna( ) -> DataFrame: if inplace: raise NotImplementedError( - "'inplace'=True not supported. {constants.FEEDBACK_LINK}" + f"'inplace'=True not supported. {constants.FEEDBACK_LINK}" ) if how not in ("any", "all"): raise ValueError("'how' must be one of 'any', 'all'") diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index dfffbe65ac1..c7bb5d92c69 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -880,7 +880,7 @@ def read_gbq_function( ibis_signature = ibis_signature_from_routine(routine) except ReturnTypeMissingError: raise ValueError( - "Function return type must be specified. {constants.FEEDBACK_LINK}" + f"Function return type must be specified. {constants.FEEDBACK_LINK}" ) except bigframes.dtypes.UnsupportedTypeError as e: raise ValueError( diff --git a/bigframes/series.py b/bigframes/series.py index 753e195e0ae..74580989f30 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1242,7 +1242,7 @@ def filter( ) if len(self._block.index_columns) > 1: raise NotImplementedError( - "Method filter does not support rows multiindex. {constants.FEEDBACK_LINK}" + f"Method filter does not support rows multiindex. {constants.FEEDBACK_LINK}" ) if (like is not None) or (regex is not None): block = self._block From 99a9e6e15c6eef4297035ce89bb619f8e4ca54ff Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 8 Feb 2024 21:24:16 +0000 Subject: [PATCH 304/921] docs: clarify ADC pre-auth in a non-interactive environment (#348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 321859738 🦕 --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index a977a9f31ec..08322f51f2c 100644 --- a/README.rst +++ b/README.rst @@ -27,10 +27,13 @@ Prerequisites * Install the ``bigframes`` package. * Create a Google Cloud project and billing account. -* When running locally, authenticate with application default credentials. See - the `gcloud auth application-default login - `_ - reference. +* In an interactive environment (like Notebook, Python REPL or command line), + ``bigframes`` will do the authentication on-the-fly if needed. Otherwise, see + `how to set up application default credentials `_ + for various environments. For example, to pre-authenticate on your laptop you can + `install and initialize the gcloud CLI `_, + and then generate the application default credentials by doing + `gcloud auth application-default login `_. Code sample ^^^^^^^^^^^ @@ -286,9 +289,6 @@ created by BigQuery DataFrames are prefixed by ``bigframes``. **Requirements** -BigQuery DataFrames uses the ``gcloud`` command-line interface internally, -so you must run ``gcloud auth login`` before using remote functions. - To use BigQuery DataFrames remote functions, you must enable the following APIs: * The BigQuery API (bigquery.googleapis.com) From de1e0a451785e679f37b083be6d58c267319f56a Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:56:18 -0800 Subject: [PATCH 305/921] feat: add ml.llm.GeminiTextGenerator model (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- README.rst | 2 + bigframes/ml/llm.py | 174 ++++++++++++++++++++++++++++++ bigframes/ml/loader.py | 1 + docs/templates/toc.yml | 2 + tests/system/small/ml/conftest.py | 5 + tests/system/small/ml/test_llm.py | 39 +++++++ 6 files changed, 223 insertions(+) diff --git a/README.rst b/README.rst index 08322f51f2c..1323a065d87 100644 --- a/README.rst +++ b/README.rst @@ -194,6 +194,8 @@ Create estimators for linear models by using the `bigframes.ml.linear_model modu Create estimators for LLMs by using the `bigframes.ml.llm module `_. +* Use the `GeminiTextGenerator class `_ to create Gemini text generator models. Use these models + for text generation tasks. * Use the `PaLM2TextGenerator class `_ to create PaLM2 text generator models. Use these models for text generation tasks. * Use the `PaLM2TextEmbeddingGenerator class `_ to create PaLM2 text embedding generator models. diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 36070003238..f44310f6a59 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -41,6 +41,8 @@ _EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT, ) +_GEMINI_PRO_ENDPOINT = "gemini-pro" + _ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" _ML_EMBED_TEXT_STATUS = "ml_embed_text_status" @@ -396,3 +398,175 @@ def to_gbq( new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) + + +@log_adapter.class_logger +class GeminiTextGenerator(base.Predictor): + """Gemini text generator LLM model. + + Args: + session (bigframes.Session or None): + BQ session to create the model. If None, use the global default session. + connection_name (str or None): + Connection to connect with remote service. str of the format ... + if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach + permission if the connection isn't fully setup. + """ + + def __init__( + self, + session: Optional[bigframes.Session] = None, + connection_name: Optional[str] = None, + ): + self.session = session or bpd.get_global_session() + self._bq_connection_manager = clients.BqConnectionManager( + self.session.bqconnectionclient, self.session.resourcemanagerclient + ) + + connection_name = connection_name or self.session._bq_connection + self.connection_name = self._bq_connection_manager.resolve_full_connection_name( + connection_name, + default_project=self.session._project, + default_location=self.session._location, + ) + + self._bqml_model_factory = globals.bqml_model_factory() + self._bqml_model: core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + # Parse and create connection if needed. + if not self.connection_name: + raise ValueError( + "Must provide connection_name, either in constructor or through session options." + ) + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", + ) + + options = {"endpoint": _GEMINI_PRO_ENDPOINT} + + return self._bqml_model_factory.create_remote_model( + session=self.session, connection_name=self.connection_name, options=options + ) + + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> GeminiTextGenerator: + assert model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in model._properties + assert "connection" in model._properties["remoteModelInfo"] + + # Parse the remote model endpoint + model_connection = model._properties["remoteModelInfo"]["connection"] + + text_generator_model = cls(session=session, connection_name=model_connection) + text_generator_model._bqml_model = core.BqmlModel(session, model) + return text_generator_model + + def predict( + self, + X: Union[bpd.DataFrame, bpd.Series], + temperature: float = 0.9, + max_output_tokens: int = 8192, + top_k: int = 40, + top_p: float = 1.0, + ) -> bpd.DataFrame: + """Predict the result from input DataFrame. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame or Series, which contains only one column of prompts. + Prompts can include preamble, questions, suggestions, instructions, or examples. + + temperature (float, default 0.9): + The temperature is used for sampling during the response generation, which occurs when topP and topK are applied. Temperature controls the degree of randomness in token selection. Lower temperatures are good for prompts that require a more deterministic and less open-ended or creative response, while higher temperatures can lead to more diverse or creative results. A temperature of 0 is deterministic: the highest probability response is always selected. + Default 0.9. Possible values [0.0, 1.0]. + + max_output_tokens (int, default 8192): + Maximum number of tokens that can be generated in the response. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. + Specify a lower value for shorter responses and a higher value for potentially longer responses. + Default 8192. Possible values are in the range [1, 8192]. + + top_k (int, default 40): + Top-K changes how the model selects tokens for output. A top-K of 1 means the next selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-K of 3 means that the next token is selected from among the three most probable tokens by using temperature. + For each token selection step, the top-K tokens with the highest probabilities are sampled. Then tokens are further filtered based on top-P with the final token selected using temperature sampling. + Specify a lower value for less random responses and a higher value for more random responses. + Default 40. Possible values [1, 40]. + + top_p (float, default 0.95):: + Top-P changes how the model selects tokens for output. Tokens are selected from the most (see top-K) to least probable until the sum of their probabilities equals the top-P value. For example, if tokens A, B, and C have a probability of 0.3, 0.2, and 0.1 and the top-P value is 0.5, then the model will select either A or B as the next token by using temperature and excludes C as a candidate. + Specify a lower value for less random responses and a higher value for more random responses. + Default 1.0. Possible values [0.0, 1.0]. + + + Returns: + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. + """ + + # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models + if temperature < 0.0 or temperature > 1.0: + raise ValueError(f"temperature must be [0.0, 1.0], but is {temperature}.") + + if max_output_tokens not in range(1, 8193): + raise ValueError( + f"max_output_token must be [1, 8192] for Gemini model, but is {max_output_tokens}." + ) + + if top_k not in range(1, 41): + raise ValueError(f"top_k must be [1, 40], but is {top_k}.") + + if top_p < 0.0 or top_p > 1.0: + raise ValueError(f"top_p must be [0.0, 1.0], but is {top_p}.") + + (X,) = utils.convert_to_dataframe(X) + + if len(X.columns) != 1: + raise ValueError( + f"Only support one column as input. {constants.FEEDBACK_LINK}" + ) + + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "prompt"}) + + options = { + "temperature": temperature, + "max_output_tokens": max_output_tokens, + "top_k": top_k, + "top_p": top_p, + "flatten_json_output": True, + } + + df = self._bqml_model.generate_text(X, options) + + if (df[_ML_GENERATE_TEXT_STATUS] != "").any(): + warnings.warn( + f"Some predictions failed. Check column {_ML_GENERATE_TEXT_STATUS} for detailed status. You may want to filter the failed rows and retry.", + RuntimeWarning, + ) + + return df + + def to_gbq(self, model_name: str, replace: bool = False) -> GeminiTextGenerator: + """Save the model to BigQuery. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + GeminiTextGenerator: saved model.""" + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 99a31922d87..dafda43e9dd 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -55,6 +55,7 @@ llm._TEXT_GENERATOR_BISON_32K_ENDPOINT: llm.PaLM2TextGenerator, llm._EMBEDDING_GENERATOR_GECKO_ENDPOINT: llm.PaLM2TextEmbeddingGenerator, llm._EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT: llm.PaLM2TextEmbeddingGenerator, + llm._GEMINI_PRO_ENDPOINT: llm.GeminiTextGenerator, } ) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index d387a513a12..e921279418b 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -105,6 +105,8 @@ - items: - name: Overview uid: bigframes.ml.llm + - name: GeminiTextGenerator + uid: bigframes.ml.llm.GeminiTextGenerator - name: PaLM2TextGenerator uid: bigframes.ml.llm.PaLM2TextGenerator - name: PaLM2TextEmbeddingGenerator diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 422ea6f1f4a..8bf08906f9e 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -267,6 +267,11 @@ def palm2_embedding_generator_multilingual_model( ) +@pytest.fixture(scope="session") +def gemini_text_generator_model(session, bq_connection) -> llm.GeminiTextGenerator: + return llm.GeminiTextGenerator(session=session, connection_name=bq_connection) + + @pytest.fixture(scope="session") def linear_remote_model_params() -> dict: # Pre-deployed endpoint of linear reg model in Vertex. diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 805cee4fecb..db959b854e3 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -272,3 +272,42 @@ def test_embedding_generator_predict_series_success( series = df["text_embedding"] value = series[0] assert len(value) == 768 + + +def test_create_gemini_text_generator_model( + gemini_text_generator_model, dataset_id, bq_connection +): + # Model creation doesn't return error + assert gemini_text_generator_model is not None + assert gemini_text_generator_model._bqml_model is not None + + # save, load to ensure configuration was kept + reloaded_model = gemini_text_generator_model.to_gbq( + f"{dataset_id}.temp_text_model", replace=True + ) + assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.connection_name == bq_connection + + +@pytest.mark.flaky(retries=2, delay=120) +def test_gemini_text_generator_predict_default_params_success( + gemini_text_generator_model, llm_text_df +): + df = gemini_text_generator_model.predict(llm_text_df).to_pandas() + assert df.shape == (3, 4) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_gemini_text_generator_predict_with_params_success( + gemini_text_generator_model, llm_text_df +): + df = gemini_text_generator_model.predict( + llm_text_df, temperature=0.5, max_output_tokens=100, top_k=20, top_p=0.5 + ).to_pandas() + assert df.shape == (3, 4) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) From ffb0d15602fe4d86e7a1aad72bba0a7049193a14 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 12 Feb 2024 10:12:10 -0800 Subject: [PATCH 306/921] feat: Support bigframes.pandas.to_datetime for scalars, iterables and series. (#372) * feat: Support pd.to_datetime for scalars, iterables and series. * update test and docstring * update types * format update * remove import. * update docstring * update arg conversion * update examples * update format * update code examples, and working logic. * docstring update. * type update. * format update. * Update docstring format * remove import * remove empty line * Remove extra code * remove prints. * Code logic updates. * Add constants. * Update comments * Move datetime helpers to the end of file. * Update helper * update format * String process logic updated. * update import * remove print * update docstring * update docstring * update docstring * update note * update docstring * Update code examples --- bigframes/core/compile/scalar_op_compiler.py | 43 ++++++++++ bigframes/core/tools/__init__.py | 19 +++++ bigframes/core/tools/datetimes.py | 82 +++++++++++++++++++ bigframes/operations/__init__.py | 11 +++ bigframes/pandas/__init__.py | 28 +++++++ tests/system/small/test_pandas.py | 62 ++++++++++++++ .../pandas/core/tools/__init__.py | 0 .../pandas/core/tools/datetimes.py | 77 +++++++++++++++++ 8 files changed, 322 insertions(+) create mode 100644 bigframes/core/tools/__init__.py create mode 100644 bigframes/core/tools/datetimes.py create mode 100644 third_party/bigframes_vendored/pandas/core/tools/__init__.py create mode 100644 third_party/bigframes_vendored/pandas/core/tools/datetimes.py diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index bf0755acc75..3bcdd70581a 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -40,6 +40,17 @@ # ln(2**(2**10)) == (2**10)*ln(2) ~= 709.78, so EXP(x) for x>709.78 will overflow. _FLOAT64_EXP_BOUND = typing.cast(ibis_types.NumericValue, ibis_types.literal(709.78)) +# Datetime constants +UNIT_TO_US_CONVERSION_FACTORS = { + "D": 24 * 60 * 60 * 1000 * 1000, + "h": 60 * 60 * 1000 * 1000, + "m": 60 * 1000 * 1000, + "s": 1000 * 1000, + "ms": 1000, + "us": 1, + "ns": 1e-3, +} + class ScalarOpCompiler: # Mapping of operation name to implemenations @@ -656,6 +667,33 @@ def isin_op_impl(x: ibis_types.Value, op: ops.IsInOp): return x.isin(matchable_ibis_values) +@scalar_op_compiler.register_unary_op(ops.ToDatetimeOp, pass_op=True) +def to_datetime_op_impl(x: ibis_types.Value, op: ops.ToDatetimeOp): + if x.type() == ibis_dtypes.str: + x = x.to_timestamp(op.format) if op.format else timestamp(x) + elif x.type() == ibis_dtypes.Timestamp(timezone="UTC"): + return x + elif x.type() != ibis_dtypes.timestamp: + # The default unit is set to "ns" (nanoseconds) for consistency + # with pandas, where "ns" is the default unit for datetime operations. + unit = op.unit or "ns" + if unit not in UNIT_TO_US_CONVERSION_FACTORS: + raise ValueError(f"Cannot convert input with unit '{unit}'.") + x_converted = x * UNIT_TO_US_CONVERSION_FACTORS[unit] + x_converted = x_converted.cast(ibis_dtypes.int64) + + # Note: Due to an issue where casting directly to a timestamp + # without a timezone does not work, we first cast to UTC. This + # approach appears to bypass a potential bug in Ibis's cast function, + # allowing for subsequent casting to a timestamp type without timezone + # information. Further investigation is needed to confirm this behavior. + x = x_converted.to_timestamp(unit="us").cast( + ibis_dtypes.Timestamp(timezone="UTC") + ) + + return x.cast(ibis_dtypes.Timestamp(timezone="UTC" if op.utc else None)) + + @scalar_op_compiler.register_unary_op(ops.RemoteFunctionOp, pass_op=True) def remote_function_op_impl(x: ibis_types.Value, op: ops.RemoteFunctionOp): if not hasattr(op.func, "bigframes_remote_function"): @@ -1141,3 +1179,8 @@ def is_null(value) -> bool: def _ibis_num(number: float): return typing.cast(ibis_types.NumericValue, ibis_types.literal(number)) + + +@ibis.udf.scalar.builtin +def timestamp(a: str) -> ibis_dtypes.timestamp: + """Convert string to timestamp.""" diff --git a/bigframes/core/tools/__init__.py b/bigframes/core/tools/__init__.py new file mode 100644 index 00000000000..38563510a79 --- /dev/null +++ b/bigframes/core/tools/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes.core.tools.datetimes import to_datetime + +__all__ = [ + "to_datetime", +] diff --git a/bigframes/core/tools/datetimes.py b/bigframes/core/tools/datetimes.py new file mode 100644 index 00000000000..093fa0a6705 --- /dev/null +++ b/bigframes/core/tools/datetimes.py @@ -0,0 +1,82 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Mapping +from datetime import datetime +from typing import Optional, Union + +import pandas as pd + +import bigframes.constants as constants +import bigframes.core.global_session as global_session +import bigframes.dataframe +import bigframes.operations as ops +import bigframes.series +import third_party.bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes + + +def to_datetime( + arg: Union[ + vendored_pandas_datetimes.local_scalars, + vendored_pandas_datetimes.local_iterables, + bigframes.series.Series, + bigframes.dataframe.DataFrame, + ], + *, + utc: bool = False, + format: Optional[str] = None, + unit: Optional[str] = None, +) -> Union[pd.Timestamp, datetime, bigframes.series.Series]: + if isinstance(arg, (int, float, str, datetime)): + return pd.to_datetime( + arg, + utc=utc, + format=format, + unit=unit, + ) + + if isinstance(arg, (Mapping, pd.DataFrame, bigframes.dataframe.DataFrame)): + raise NotImplementedError( + "Conversion of Mapping, pandas.DataFrame, or bigframes.dataframe.DataFrame " + f"to datetime is not implemented. {constants.FEEDBACK_LINK}" + ) + + if not isinstance(arg, bigframes.series.Series): + # This block ensures compatibility with local data formats, including + # iterables and pandas.Series + # TODO: Currently, data upload is performed using pandas DataFrames + # combined with the `read_pandas` method due to the BigFrames DataFrame + # constructor's limitations in handling various data types. Plan to update + # the upload process to utilize the BigFrames DataFrame constructor directly + # once it is enhanced for more related datatypes. + arg = global_session.with_default_session( + bigframes.session.Session.read_pandas, pd.DataFrame(arg) + ) + if len(arg.columns) != 1: + raise ValueError("Input must be 1-dimensional.") + + arg = arg[arg.columns[0]] + + if not utc and arg.dtype not in ("Int64", "Float64"): # type: ignore + raise NotImplementedError( + f"String and Timestamp requires utc=True. {constants.FEEDBACK_LINK}" + ) + + return arg._apply_unary_op( # type: ignore + ops.ToDatetimeOp( + utc=utc, + format=format, + unit=unit, + ) + ) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index b40f42a3e80..37188e490ee 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -404,6 +404,17 @@ def output_type(self, *input_types): return input_types[0] +@dataclasses.dataclass(frozen=True) +class ToDatetimeOp(UnaryOp): + name: typing.ClassVar[str] = "to_datetime" + utc: bool = False + format: typing.Optional[str] = None + unit: typing.Optional[str] = None + + def output_type(self, *input_types): + return input_types[0] + + # Binary Ops fillna_op = create_binary_op(name="fillna") cliplower_op = create_binary_op(name="clip_lower") diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 554acda202e..5320e84e211 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -17,6 +17,7 @@ from __future__ import annotations from collections import namedtuple +from datetime import datetime import inspect import sys import typing @@ -52,6 +53,7 @@ import bigframes.core.global_session as global_session import bigframes.core.indexes import bigframes.core.reshape +import bigframes.core.tools import bigframes.dataframe import bigframes.operations as ops import bigframes.series @@ -61,6 +63,7 @@ import third_party.bigframes_vendored.pandas.core.reshape.encoding as vendored_pandas_encoding import third_party.bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile +import third_party.bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes import third_party.bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq @@ -635,6 +638,30 @@ def read_gbq_function(function_name: str): read_gbq_function.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_function) + +def to_datetime( + arg: Union[ + vendored_pandas_datetimes.local_scalars, + vendored_pandas_datetimes.local_iterables, + bigframes.series.Series, + bigframes.dataframe.DataFrame, + ], + *, + utc: bool = False, + format: Optional[str] = None, + unit: Optional[str] = None, +) -> Union[pandas.Timestamp, datetime, bigframes.series.Series]: + return bigframes.core.tools.to_datetime( + arg, + utc=utc, + format=format, + unit=unit, + ) + + +to_datetime.__doc__ = vendored_pandas_datetimes.to_datetime.__doc__ + + # pandas dtype attributes NA = pandas.NA BooleanDtype = pandas.BooleanDtype @@ -680,6 +707,7 @@ def read_gbq_function(function_name: str): "read_pandas", "read_pickle", "remote_function", + "to_datetime", # pandas dtype attributes "NA", "BooleanDtype", diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index 0910c0b7e26..ec61329aa57 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime + import pandas as pd import pytest +import pytz import bigframes.pandas as bpd from tests.system.utils import assert_pandas_df_equal @@ -477,3 +480,62 @@ def test_qcut(scalars_dfs, q): pd_result = pd_result.astype("Int64") pd.testing.assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("arg", "utc", "unit", "format"), + [ + (173872738, False, None, None), + (32787983.23, True, "s", None), + ("2023-01-01", False, None, "%Y-%m-%d"), + (datetime(2023, 1, 1, 12, 0), False, None, None), + ], +) +def test_to_datetime_scalar(arg, utc, unit, format): + bf_result = bpd.to_datetime(arg, utc=utc, unit=unit, format=format) + pd_result = pd.to_datetime(arg, utc=utc, unit=unit, format=format) + + assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("arg", "utc", "unit", "format"), + [ + ([173872738], False, None, None), + ([32787983.23], True, "s", None), + ( + [datetime(2023, 1, 1, 12, 0, tzinfo=pytz.timezone("America/New_York"))], + True, + None, + None, + ), + (["2023-01-01"], True, None, "%Y-%m-%d"), + (["2023-02-01T15:00:00+07:22"], True, None, None), + (["01-31-2023 14:30 -0800"], True, None, "%m-%d-%Y %H:%M %z"), + (["01-31-2023 14:00", "02-01-2023 15:00"], True, None, "%m-%d-%Y %H:%M"), + ], +) +def test_to_datetime_iterable(arg, utc, unit, format): + bf_result = ( + bpd.to_datetime(arg, utc=utc, unit=unit, format=format) + .to_pandas() + .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") + ) + pd_result = pd.Series( + pd.to_datetime(arg, utc=utc, unit=unit, format=format) + ).dt.floor("us") + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_names=False + ) + + +def test_to_datetime_series(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col = "int64_too" + bf_result = ( + bpd.to_datetime(scalars_df[col], unit="s").to_pandas().astype("datetime64[s]") + ) + pd_result = pd.Series(pd.to_datetime(scalars_pandas_df[col], unit="s")) + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_names=False + ) diff --git a/third_party/bigframes_vendored/pandas/core/tools/__init__.py b/third_party/bigframes_vendored/pandas/core/tools/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py new file mode 100644 index 00000000000..8a8a562baeb --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py @@ -0,0 +1,77 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/tools/datetimes.py + +from datetime import datetime +from typing import Iterable, Mapping, Union + +import pandas as pd + +from bigframes import constants, series + +local_scalars = Union[int, float, str, datetime] +local_iterables = Union[Iterable, pd.Series, pd.DataFrame, Mapping] + + +def to_datetime( + arg, + *, + utc=False, + format=None, + unit=None, +) -> Union[pd.Timestamp, datetime, series.Series]: + """ + This function converts a scalar, array-like or Series to a datetime object. + + .. note:: + BigQuery only supports precision up to microseconds (us). Therefore, when working + with timestamps that have a finer granularity than microseconds, be aware that + the additional precision will not be represented in BigQuery. + + .. note:: + The format strings for specifying datetime representations in BigQuery and pandas + are not completely identical. Ensure that the format string provided is compatible + with BigQuery. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Converting a Scalar to datetime: + + >>> scalar = 123456.789 + >>> bpd.to_datetime(scalar, unit = 's') + Timestamp('1970-01-02 10:17:36.789000') + + Converting a List of Strings without Timezone Information: + + >>> list_str = ["01-31-2021 14:30", "02-28-2021 15:45"] + >>> bpd.to_datetime(list_str, format="%m-%d-%Y %H:%M", utc=True) + 0 2021-01-31 14:30:00+00:00 + 1 2021-02-28 15:45:00+00:00 + Name: 0, dtype: timestamp[us, tz=UTC][pyarrow] + + Converting a Series of Strings with Timezone Information: + + >>> series_str = bpd.Series(["01-31-2021 14:30+08:00", "02-28-2021 15:45+00:00"]) + >>> bpd.to_datetime(series_str, format="%m-%d-%Y %H:%M%Z", utc=True) + 0 2021-01-31 06:30:00+00:00 + 1 2021-02-28 15:45:00+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + + Args: + arg (int, float, str, datetime, list, tuple, 1-d array, Series): + The object to convert to a datetime. + utc (bool, default False): + Control timezone-related parsing, localization and conversion. If True, the + function always returns a timezone-aware UTC-localized timestamp or series. + If False (default), inputs will not be coerced to UTC. + format (str, default None): + The strftime to parse time, e.g. "%d/%m/%Y". + unit (str, default 'ns'): + The unit of the arg (D,s,ms,us,ns) denote the unit, which is an integer or + float number. + + Returns: + Timestamp, datetime.datetime or bigframes.series.Series: Return type depends on input. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 208e081fa99e17b8085e83c111c07eb6fc5c4730 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 12 Feb 2024 23:16:16 +0000 Subject: [PATCH 307/921] feat: limited support of lamdas in `Series.apply` (#345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BEGIN_COMMIT_OVERRIDE feat: limited support of lambdas in `Series.apply` (#345) END_COMMIT_OVERRIDE Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated https://screenshot.googleplex.com/6ZEiKXPz8LWMTRf Partially fixes internal issue 295964341 🦕 --- bigframes/series.py | 45 +++++- tests/system/small/test_series.py | 151 ++++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 82 +++++++++- 3 files changed, 270 insertions(+), 8 deletions(-) diff --git a/bigframes/series.py b/bigframes/series.py index 74580989f30..4aef959a76a 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -58,6 +58,12 @@ LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] +_remote_function_recommendation_message = ( + "Your functions could not be applied directly to the Series." + " Try converting it to a remote function." +) + + @log_adapter.class_logger class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Series): def __init__(self, *args, **kwargs): @@ -1210,12 +1216,43 @@ def _groupby_values( dropna=dropna, ) - def apply(self, func) -> Series: + def apply( + self, func, by_row: typing.Union[typing.Literal["compat"], bool] = "compat" + ) -> Series: # TODO(shobs, b/274645634): Support convert_dtype, args, **kwargs # is actually a ternary op # Reproject as workaround to applying filter too late. This forces the filter # to be applied before passing data to remote function, protecting from bad # inputs causing errors. + + if by_row not in ["compat", False]: + raise ValueError("Param by_row must be one of 'compat' or False") + + if not callable(func): + raise ValueError( + "Only a ufunc (a function that applies to the entire Series) or a remote function that only works on single values are supported." + ) + + if not hasattr(func, "bigframes_remote_function"): + # It is not a remote function + # Then it must be a vectorized function that applies to the Series + # as a whole + if by_row: + raise ValueError( + "A vectorized non-remote function can be provided only with by_row=False." + " For element-wise operation it must be a remote function." + ) + + try: + return func(self) + except Exception as ex: + # This could happen if any of the operators in func is not + # supported on a Series. Let's guide the customer to use a + # remote function instead + if hasattr(ex, "message"): + ex.message += f"\n{_remote_function_recommendation_message}" + raise + reprojected_series = Series(self._block._force_reproject()) return reprojected_series._apply_unary_op( ops.RemoteFunctionOp(func=func, apply_on_null=True) @@ -1325,7 +1362,11 @@ def duplicated(self, keep: str = "first") -> Series: def mask(self, cond, other=None) -> Series: if callable(cond): - cond = self.apply(cond) + if hasattr(cond, "bigframes_remote_function"): + cond = self.apply(cond) + else: + # For non-remote function assume that it is applicable on Series + cond = self.apply(cond, by_row=False) if not isinstance(cond, Series): raise TypeError( diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index f2790d190aa..42651ed96f5 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2560,6 +2560,51 @@ def test_mask_custom_value(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("lambda_",), + [ + pytest.param(lambda x: x > 0), + pytest.param( + lambda x: True if x > 0 else False, + marks=pytest.mark.xfail( + raises=ValueError, + ), + ), + ], + ids=[ + "lambda_arithmatic", + "lambda_arbitrary", + ], +) +def test_mask_lambda(scalars_dfs, lambda_): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_col = scalars_df["int64_col"] + bf_result = bf_col.mask(lambda_).to_pandas() + + pd_col = scalars_pandas_df["int64_col"] + pd_result = pd_col.mask(lambda_) + + # ignore dtype check, which are Int64 and object respectively + assert_series_equal(bf_result, pd_result, check_dtype=False) + + +def test_mask_simple_udf(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + def foo(x): + return x < 1000000 + + bf_col = scalars_df["int64_col"] + bf_result = bf_col.mask(foo).to_pandas() + + pd_col = scalars_pandas_df["int64_col"] + pd_result = pd_col.mask(foo) + + # ignore dtype check, which are Int64 and object respectively + assert_series_equal(bf_result, pd_result, check_dtype=False) + + @pytest.mark.parametrize( ("column", "to_type"), [ @@ -3042,3 +3087,109 @@ def test_series_iter( scalars_df_index["int64_too"], scalars_pandas_df_index["int64_too"] ): assert bf_i == pd_i + + +@pytest.mark.parametrize( + ( + "col", + "lambda_", + ), + [ + pytest.param("int64_col", lambda x: x * x + x + 1), + pytest.param("int64_col", lambda x: x % 2 == 1), + pytest.param("string_col", lambda x: x + "_suffix"), + ], + ids=[ + "lambda_int_int", + "lambda_int_bool", + "lambda_str_str", + ], +) +def test_apply_lambda(scalars_dfs, col, lambda_): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_col = scalars_df[col] + + # Can't be applied to BigFrames Series without by_row=False + with pytest.raises(ValueError, match="by_row=False"): + bf_col.apply(lambda_) + + bf_result = bf_col.apply(lambda_, by_row=False).to_pandas() + + pd_col = scalars_pandas_df[col] + pd_result = pd_col.apply(lambda_) + + # ignore dtype check, which are Int64 and object respectively + assert_series_equal(bf_result, pd_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("ufunc",), + [ + pytest.param(numpy.log), + pytest.param(numpy.sqrt), + pytest.param(numpy.sin), + ], + ids=[ + "log", + "sqrt", + "sin", + ], +) +def test_apply_numpy_ufunc(scalars_dfs, ufunc): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_col = scalars_df["int64_col"] + + # Can't be applied to BigFrames Series without by_row=False + with pytest.raises(ValueError, match="by_row=False"): + bf_col.apply(ufunc) + + bf_result = bf_col.apply(ufunc, by_row=False).to_pandas() + + pd_col = scalars_pandas_df["int64_col"] + pd_result = pd_col.apply(ufunc) + + assert_series_equal(bf_result, pd_result) + + +def test_apply_simple_udf(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + def foo(x): + return x * x + 2 * x + 3 + + bf_col = scalars_df["int64_col"] + + # Can't be applied to BigFrames Series without by_row=False + with pytest.raises(ValueError, match="by_row=False"): + bf_col.apply(foo) + + bf_result = bf_col.apply(foo, by_row=False).to_pandas() + + pd_col = scalars_pandas_df["int64_col"] + pd_result = pd_col.apply(foo) + + # ignore dtype check, which are Int64 and object respectively + assert_series_equal(bf_result, pd_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("col", "lambda_", "exception"), + [ + pytest.param("int64_col", {1: 2, 3: 4}, ValueError), + pytest.param("int64_col", numpy.square, TypeError), + pytest.param("string_col", lambda x: x.capitalize(), AttributeError), + ], + ids=[ + "not_callable", + "numpy_ufunc", + "custom_lambda", + ], +) +def test_apply_not_supported(scalars_dfs, col, lambda_, exception): + scalars_df, _ = scalars_dfs + + bf_col = scalars_df[col] + with pytest.raises(exception): + bf_col.apply(lambda_, by_row=False) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 4232d3ec2a4..b2034716067 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1116,18 +1116,24 @@ def nsmallest(self, n: int = 5, keep: str = "first") -> Series: def apply( self, func, + by_row="compat", ) -> DataFrame | Series: """ Invoke function on values of a Series. + Can be ufunc (a NumPy function that applies to the entire Series) or a + Python function that only works on single values. If it is an arbitrary + python function then converting it into a `remote_function` is recommended. + **Examples:** >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None - Let's use ``reuse=False`` flag to make sure a new ``remote_function`` + For applying arbitrary python function a `remote_funciton` is recommended. + Let's use ``reuse=False`` flag to make sure a new `remote_function` is created every time we run the following code, but you can skip it - to potentially reuse a previously deployed ``remote_function`` from + to potentially reuse a previously deployed `remote_function` from the same user defined function. >>> @bpd.remote_function([int], float, reuse=False) @@ -1152,9 +1158,9 @@ def apply( 4 2.0 dtype: Float64 - You could turn a user defined function with external package - dependencies into a BigQuery DataFrames remote function. You would - provide the names of the packages via ``packages`` param. + To turn a user defined function with external package dependencies into + a `remote_function`, you would provide the names of the packages via + `packages` param. >>> @bpd.remote_function( ... [str], @@ -1176,11 +1182,48 @@ def apply( >>> names = bpd.Series(["Alice", "Bob"]) >>> hashes = names.apply(get_hash) + Simple vectorized functions, lambdas or ufuncs can be applied directly + with `by_row=False`. + + >>> nums = bpd.Series([1, 2, 3, 4]) + >>> nums + 0 1 + 1 2 + 2 3 + 3 4 + dtype: Int64 + >>> nums.apply(lambda x: x*x + 2*x + 1, by_row=False) + 0 4 + 1 9 + 2 16 + 3 25 + dtype: Int64 + + >>> def is_odd(num): + ... return num % 2 == 1 + >>> nums.apply(is_odd, by_row=False) + 0 True + 1 False + 2 True + 3 False + dtype: boolean + + >>> nums.apply(np.log, by_row=False) + 0 0.0 + 1 0.693147 + 2 1.098612 + 3 1.386294 + dtype: Float64 + Args: func (function): BigFrames DataFrames ``remote_function`` to apply. The function should take a scalar and return a scalar. It will be applied to every element in the ``Series``. + by_row (False or "compat", default "compat"): + If `"compat"` , func must be a remote function which will be + passed each element of the Series, like `Series.map`. If False, + the func will be passed the whole Series at once. Returns: bigframes.series.Series: A new Series with values representing the @@ -2680,7 +2723,8 @@ def mask(self, cond, other): dtype: Int64 You can mask the values in the Series based on a condition. The values - matching the condition would be masked. + matching the condition would be masked. The condition can be provided in + formm of a Series. >>> s.mask(s % 2 == 0) 0 @@ -2736,6 +2780,32 @@ def mask(self, cond, other): 2 Caroline dtype: string + Simple vectorized (i.e. they only perform operations supported on a + Series) lambdas or python functions can be used directly. + + >>> nums = bpd.Series([1, 2, 3, 4], name="nums") + >>> nums + 0 1 + 1 2 + 2 3 + 3 4 + Name: nums, dtype: Int64 + >>> nums.mask(lambda x: (x+1) % 2 == 1) + 0 1 + 1 + 2 3 + 3 + Name: nums, dtype: Int64 + + >>> def is_odd(num): + ... return num % 2 == 1 + >>> nums.mask(is_odd) + 0 + 1 2 + 2 + 3 4 + Name: nums, dtype: Int64 + Args: cond (bool Series/DataFrame, array-like, or callable): Where cond is False, keep the original value. Where True, replace From 90caf865efc940f94e16643bda7ba261c2f2e473 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:40:20 -0800 Subject: [PATCH 308/921] feat: support read_gbq wildcard table path (#377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/pandas/__init__.py | 2 + bigframes/session/__init__.py | 64 +++++++++++++------ tests/system/small/test_session.py | 26 ++++++++ tests/unit/session/test_session.py | 31 ++++++++- .../bigframes_vendored/pandas/io/gbq.py | 15 ++++- 5 files changed, 112 insertions(+), 26 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 5320e84e211..110978a7f10 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -551,6 +551,7 @@ def read_gbq_table( index_col: Iterable[str] | str = (), columns: Iterable[str] = (), max_results: Optional[int] = None, + filters: vendored_pandas_gbq.FiltersType = (), use_cache: bool = True, col_order: Iterable[str] = (), ) -> bigframes.dataframe.DataFrame: @@ -561,6 +562,7 @@ def read_gbq_table( index_col=index_col, columns=columns, max_results=max_results, + filters=filters, use_cache=use_cache, col_order=col_order, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 15d4b3577b1..df0cd6e947a 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -30,6 +30,7 @@ Iterable, List, Literal, + Mapping, MutableSequence, Optional, Sequence, @@ -115,6 +116,11 @@ def _is_query(query_or_table: str) -> bool: return re.search(r"\s", query_or_table.strip(), re.MULTILINE) is not None +def _is_table_with_wildcard_suffix(query_or_table: str) -> bool: + """Determine if `query_or_table` is a table and contains a wildcard suffix.""" + return not _is_query(query_or_table) and query_or_table.endswith("*") + + class Session( third_party_pandas_gbq.GBQIOMixin, third_party_pandas_parquet.ParquetIOMixin, @@ -248,7 +254,9 @@ def read_gbq( elif col_order: columns = col_order - query_or_table = self._filters_to_query(query_or_table, columns, filters) + filters = list(filters) + if len(filters) != 0 or _is_table_with_wildcard_suffix(query_or_table): + query_or_table = self._to_query(query_or_table, columns, filters) if _is_query(query_or_table): return self._read_gbq_query( @@ -272,13 +280,18 @@ def read_gbq( use_cache=use_cache, ) - def _filters_to_query(self, query_or_table, columns, filters): - """Convert filters to query""" - if len(filters) == 0: - return query_or_table - + def _to_query( + self, + query_or_table: str, + columns: Iterable[str], + filters: third_party_pandas_gbq.FiltersType, + ) -> str: + """Compile query_or_table with conditions(filters, wildcards) to query.""" + filters = list(filters) sub_query = ( - f"({query_or_table})" if _is_query(query_or_table) else query_or_table + f"({query_or_table})" + if _is_query(query_or_table) + else f"`{query_or_table}`" ) select_clause = "SELECT " + ( @@ -287,7 +300,7 @@ def _filters_to_query(self, query_or_table, columns, filters): where_clause = "" if filters: - valid_operators = { + valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { "in": "IN", "not in": "NOT IN", "==": "=", @@ -298,19 +311,16 @@ def _filters_to_query(self, query_or_table, columns, filters): "!=": "!=", } - if ( - isinstance(filters, Iterable) - and isinstance(filters[0], Tuple) - and (len(filters[0]) == 0 or not isinstance(filters[0][0], Tuple)) + # If single layer filter, add another pseudo layer. So the single layer represents "and" logic. + if isinstance(filters[0], tuple) and ( + len(filters[0]) == 0 or not isinstance(list(filters[0])[0], tuple) ): - filters = [filters] + filters = typing.cast(third_party_pandas_gbq.FiltersType, [filters]) or_expressions = [] for group in filters: if not isinstance(group, Iterable): - raise ValueError( - f"Filter group should be a iterable, {group} is not valid." - ) + group = [group] and_expressions = [] for filter_item in group: @@ -329,13 +339,13 @@ def _filters_to_query(self, query_or_table, columns, filters): if operator not in valid_operators: raise ValueError(f"Operator {operator} is not valid.") - operator = valid_operators[operator] + operator_str = valid_operators[operator] - if operator in ["IN", "NOT IN"]: + if operator_str in ["IN", "NOT IN"]: value_list = ", ".join([repr(v) for v in value]) - expression = f"`{column}` {operator} ({value_list})" + expression = f"`{column}` {operator_str} ({value_list})" else: - expression = f"`{column}` {operator} {repr(value)}" + expression = f"`{column}` {operator_str} {repr(value)}" and_expressions.append(expression) or_expressions.append(" AND ".join(and_expressions)) @@ -521,6 +531,7 @@ def read_gbq_table( index_col: Iterable[str] | str = (), columns: Iterable[str] = (), max_results: Optional[int] = None, + filters: third_party_pandas_gbq.FiltersType = (), use_cache: bool = True, col_order: Iterable[str] = (), ) -> dataframe.DataFrame: @@ -546,6 +557,19 @@ def read_gbq_table( elif col_order: columns = col_order + filters = list(filters) + if len(filters) != 0 or _is_table_with_wildcard_suffix(query): + query = self._to_query(query, columns, filters) + + return self._read_gbq_query( + query, + index_col=index_col, + columns=columns, + max_results=max_results, + api_name="read_gbq_table", + use_cache=use_cache, + ) + return self._read_gbq_table( query=query, index_col=index_col, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 2d9c332de1d..85573472b90 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -327,6 +327,32 @@ def test_read_gbq_twice_with_same_timestamp(session, penguins_table_id): assert df3 is not None +def test_read_gbq_wildcard(session: bigframes.Session): + df = session.read_gbq("bigquery-public-data.noaa_gsod.gsod193*") + assert df.shape == (348485, 32) + + +def test_read_gbq_wildcard_with_filter(session: bigframes.Session): + df = session.read_gbq( + "bigquery-public-data.noaa_gsod.gsod19*", + filters=[("_table_suffix", ">=", "30"), ("_table_suffix", "<=", "39")], # type: ignore + ) + assert df.shape == (348485, 32) + + +def test_read_gbq_table_wildcard(session: bigframes.Session): + df = session.read_gbq_table("bigquery-public-data.noaa_gsod.gsod193*") + assert df.shape == (348485, 32) + + +def test_read_gbq_table_wildcard_with_filter(session: bigframes.Session): + df = session.read_gbq_table( + "bigquery-public-data.noaa_gsod.gsod19*", + filters=[("_table_suffix", ">=", "30"), ("_table_suffix", "<=", "39")], # type: ignore + ) + assert df.shape == (348485, 32) + + 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/session/test_session.py b/tests/unit/session/test_session.py index ea8d0882ae8..b474c9f63e7 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -125,7 +125,7 @@ def test_session_init_fails_with_no_project(): "test_table", [], [("date_col", ">", "2022-10-20")], - "SELECT * FROM test_table AS sub WHERE `date_col` > '2022-10-20'", + "SELECT * FROM `test_table` AS sub WHERE `date_col` > '2022-10-20'", id="table_input", ), pytest.param( @@ -136,7 +136,7 @@ def test_session_init_fails_with_no_project(): (("string_col", "in", ["Hello, World!", "こんにちは"]),), ], ( - "SELECT `row_index`, `string_col` FROM test_table AS sub WHERE " + "SELECT `row_index`, `string_col` FROM `test_table` AS sub WHERE " "`rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " "'こんにちは')" ), @@ -156,5 +156,30 @@ def test_session_init_fails_with_no_project(): ) def test_read_gbq_with_filters(query_or_table, columns, filters, expected_output): session = resources.create_bigquery_session() - query = session._filters_to_query(query_or_table, columns, filters) + query = session._to_query(query_or_table, columns, filters) + assert query == expected_output + + +@pytest.mark.parametrize( + ("query_or_table", "columns", "filters", "expected_output"), + [ + pytest.param( + "test_table*", + [], + [], + "SELECT * FROM `test_table*` AS sub", + id="wildcard_table_input", + ), + pytest.param( + "test_table*", + [], + [("_TABLE_SUFFIX", ">", "2022-10-20")], + "SELECT * FROM `test_table*` AS sub WHERE `_TABLE_SUFFIX` > '2022-10-20'", + id="wildcard_table_input_with_filter", + ), + ], +) +def test_read_gbq_wildcard(query_or_table, columns, filters, expected_output): + session = resources.create_bigquery_session() + query = session._to_query(query_or_table, columns, filters) assert query == expected_output diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 8e2c9f092da..1f31c530d27 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -7,8 +7,9 @@ from bigframes import constants -FilterType = Tuple[str, Literal["in", "not in", "<", "<=", "==", "!=", ">=", ">"], Any] -FiltersType = Iterable[Union[FilterType, Iterable[FilterType]]] +FilterOps = Literal["in", "not in", "<", "<=", "==", "!=", ">=", ">"] +FilterType = Tuple[str, FilterOps, Any] +FiltersType = Union[Iterable[FilterType], Iterable[Iterable[FilterType]]] class GBQIOMixin: @@ -52,6 +53,9 @@ def read_gbq( >>> df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + Read table path with wildcard suffix and filters: + >>> df = bpd.read_gbq_table("bigquery-public-data.noaa_gsod.gsod19*", filters=[("_table_suffix", ">=", "30"), ("_table_suffix", "<=", "39")]) + Preserve ordering in a query input. >>> df = bpd.read_gbq(''' @@ -96,6 +100,8 @@ def read_gbq( A SQL string to be executed or a BigQuery table to be read. The table must be specified in the format of `project.dataset.tablename` or `dataset.tablename`. + Can also take wildcard table name, such as `project.dataset.table_prefix*`. + In tha case, will read all the matched table as one DataFrame. index_col (Iterable[str] or str): Name of result column(s) to use for index in results DataFrame. columns (Iterable[str]): @@ -104,7 +110,7 @@ def read_gbq( max_results (Optional[int], default None): If set, limit the maximum number of rows to fetch from the query results. - filters (Iterable[Union[Tuple, Iterable[Tuple]]], default ()): To + filters (Union[Iterable[FilterType], Iterable[Iterable[FilterType]]], default ()): To filter out data. Filter syntax: [[(column, op, val), …],…] where op is [==, >, >=, <, <=, !=, in, not in]. The innermost tuples are transposed into a set of filters applied through an AND @@ -112,6 +118,9 @@ def read_gbq( through an OR operation. A single Iterable of tuples can also be used, meaning that no OR operation between set of filters is to be conducted. + If using wildcard table suffix in query_or_table, can specify + '_table_suffix' pseudo column to filter the tables to be read + into the DataFrame. use_cache (bool, default True): Whether to cache the query inputs. Default to True. col_order (Iterable[str]): From 126f5660bd61bd8998e5f17ca0cbd39959590367 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 13 Feb 2024 11:00:21 -0800 Subject: [PATCH 309/921] feat: add ml.metrics.pairwise.cosine_similarity function (#374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/core.py | 97 +++++++++++++------ bigframes/ml/metrics/__init__.py | 39 ++++++++ .../ml/{metrics.py => metrics/_metrics.py} | 0 bigframes/ml/metrics/pairwise.py | 34 +++++++ bigframes/ml/sql.py | 15 ++- docs/templates/toc.yml | 4 + .../system/small/ml/test_metrics_pairwise.py | 35 +++++++ tests/unit/ml/test_sql.py | 55 ++++++----- .../sklearn/metrics/pairwise.py | 31 ++++++ 9 files changed, 260 insertions(+), 50 deletions(-) create mode 100644 bigframes/ml/metrics/__init__.py rename bigframes/ml/{metrics.py => metrics/_metrics.py} (100%) create mode 100644 bigframes/ml/metrics/pairwise.py create mode 100644 tests/system/small/ml/test_metrics_pairwise.py create mode 100644 third_party/bigframes_vendored/sklearn/metrics/pairwise.py diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 51fed059010..c496133aa72 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -17,7 +17,7 @@ from __future__ import annotations import datetime -from typing import Callable, cast, Iterable, Mapping, Optional, Union +from typing import Callable, cast, Iterable, Literal, Mapping, Optional, Union import uuid from google.cloud import bigquery @@ -28,34 +28,12 @@ import bigframes.pandas as bpd -class BqmlModel: - """Represents an existing BQML model in BigQuery. - - Wraps the BQML API and SQL interface to expose the functionality needed for - BigQuery DataFrames ML. - """ +class BaseBqml: + """Base class for BQML functionalities.""" - def __init__(self, session: bigframes.Session, model: bigquery.Model): + def __init__(self, session: bigframes.Session): self._session = session - self._model = model - self._model_manipulation_sql_generator = ml_sql.ModelManipulationSqlGenerator( - self.model_name - ) - - @property - def session(self) -> bigframes.Session: - """Get the BigQuery DataFrames session that this BQML model wrapper is tied to""" - return self._session - - @property - def model_name(self) -> str: - """Get the fully qualified name of the model, i.e. project_id.dataset_id.model_id""" - return f"{self._model.project}.{self._model.dataset_id}.{self._model.model_id}" - - @property - def model(self) -> bigquery.Model: - """Get the BQML model associated with this wrapper""" - return self._model + self._base_sql_generator = ml_sql.BaseSqlGenerator() def _apply_sql( self, @@ -84,6 +62,71 @@ def _apply_sql( return df + def distance( + self, + x: bpd.DataFrame, + y: bpd.DataFrame, + type: Literal["EUCLIDEAN", "MANHATTAN", "COSINE"], + name: str, + ) -> bpd.DataFrame: + """Calculate ML.DISTANCE from DataFrame inputs. + + Args: + x: + input DataFrame + y: + input DataFrame + type: + Distance types, accept values are "EUCLIDEAN", "MANHATTAN", "COSINE". + name: + name of the output result column + """ + assert len(x.columns) == 1 and len(y.columns) == 1 + + input_data = x._cached().join(y._cached(), how="outer") + x_column_id, y_column_id = x._block.value_columns[0], y._block.value_columns[0] + + return self._apply_sql( + input_data, + lambda source_df: self._base_sql_generator.ml_distance( + x_column_id, + y_column_id, + type=type, + source_df=source_df, + name=name, + ), + ) + + +class BqmlModel(BaseBqml): + """Represents an existing BQML model in BigQuery. + + Wraps the BQML API and SQL interface to expose the functionality needed for + BigQuery DataFrames ML. + """ + + def __init__(self, session: bigframes.Session, model: bigquery.Model): + self._session = session + self._model = model + self._model_manipulation_sql_generator = ml_sql.ModelManipulationSqlGenerator( + self.model_name + ) + + @property + def session(self) -> bigframes.Session: + """Get the BigQuery DataFrames session that this BQML model wrapper is tied to""" + return self._session + + @property + def model_name(self) -> str: + """Get the fully qualified name of the model, i.e. project_id.dataset_id.model_id""" + return f"{self._model.project}.{self._model.dataset_id}.{self._model.model_id}" + + @property + def model(self) -> bigquery.Model: + """Get the BQML model associated with this wrapper""" + return self._model + def predict(self, input_data: bpd.DataFrame) -> bpd.DataFrame: # TODO: validate input data schema return self._apply_sql( diff --git a/bigframes/ml/metrics/__init__.py b/bigframes/ml/metrics/__init__.py new file mode 100644 index 00000000000..6b0a243426e --- /dev/null +++ b/bigframes/ml/metrics/__init__.py @@ -0,0 +1,39 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bigframes.ml.metrics import pairwise +from bigframes.ml.metrics._metrics import ( + accuracy_score, + auc, + confusion_matrix, + f1_score, + precision_score, + r2_score, + recall_score, + roc_auc_score, + roc_curve, +) + +__all__ = [ + "r2_score", + "recall_score", + "accuracy_score", + "roc_curve", + "roc_auc_score", + "auc", + "confusion_matrix", + "precision_score", + "f1_score", + "pairwise", +] diff --git a/bigframes/ml/metrics.py b/bigframes/ml/metrics/_metrics.py similarity index 100% rename from bigframes/ml/metrics.py rename to bigframes/ml/metrics/_metrics.py diff --git a/bigframes/ml/metrics/pairwise.py b/bigframes/ml/metrics/pairwise.py new file mode 100644 index 00000000000..04577c89d35 --- /dev/null +++ b/bigframes/ml/metrics/pairwise.py @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from typing import Union + +from bigframes.ml import core, utils +import bigframes.pandas as bpd +import third_party.bigframes_vendored.sklearn.metrics.pairwise as vendored_metrics_pairwise + + +def cosine_similarity( + X: Union[bpd.DataFrame, bpd.Series], Y: Union[bpd.DataFrame, bpd.Series] +) -> bpd.DataFrame: + X, Y = utils.convert_to_dataframe(X, Y) + if len(X.columns) != 1 or len(Y.columns) != 1: + raise ValueError("Inputs X and Y can only contain 1 column.") + + base_bqml = core.BaseBqml(session=X._session) + return base_bqml.distance(X, Y, type="COSINE", name="cosine_similarity") + + +cosine_similarity.__doc__ = inspect.getdoc(vendored_metrics_pairwise.cosine_similarity) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index f726317bb4a..7999cb90a3e 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -16,7 +16,7 @@ Generates SQL queries needed for BigQuery DataFrames ML """ -from typing import Iterable, Mapping, Optional, Union +from typing import Iterable, Literal, Mapping, Optional, Union import google.cloud.bigquery @@ -133,6 +133,19 @@ def ml_label_encoder( https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-label-encoder for params.""" return f"""ML.LABEL_ENCODER({numeric_expr_sql}, {top_k}, {frequency_threshold}) OVER() AS {name}""" + def ml_distance( + self, + col_x: str, + col_y: str, + type: Literal["EUCLIDEAN", "MANHATTAN", "COSINE"], + source_df: bpd.DataFrame, + name: str, + ) -> str: + """Encode ML.DISTANCE for BQML. + https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-distance""" + source_sql, _, _ = source_df._to_sql_query(include_index=True) + return f"""SELECT *, ML.DISTANCE({col_x}, {col_y}, '{type}') AS {name} FROM ({source_sql})""" + class ModelCreationSqlGenerator(BaseSqlGenerator): """Sql generator for creating a model entity. Model id is the standalone id without project id and dataset id.""" diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index e921279418b..90dbc504b0a 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -116,6 +116,10 @@ - name: metrics uid: bigframes.ml.metrics name: metrics + - items: + - name: metrics.pairwise + uid: bigframes.ml.metrics.pairwise + name: metrics.pairwise - items: - name: model_selection uid: bigframes.ml.model_selection diff --git a/tests/system/small/ml/test_metrics_pairwise.py b/tests/system/small/ml/test_metrics_pairwise.py new file mode 100644 index 00000000000..c02a36abbc3 --- /dev/null +++ b/tests/system/small/ml/test_metrics_pairwise.py @@ -0,0 +1,35 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd + +from bigframes.ml import metrics +import bigframes.pandas as bpd + + +def test_cosine_similarity(): + x_col = [np.array([4.1, 0.5, 1.0])] + y_col = [np.array([3.0, 0.0, 2.5])] + X = bpd.read_pandas(pd.DataFrame({"X": x_col})) + Y = bpd.read_pandas(pd.DataFrame({"Y": y_col})) + + result = metrics.pairwise.cosine_similarity(X, Y) + expected_pd_df = pd.DataFrame( + {"X": x_col, "Y": y_col, "cosine_similarity": [0.108199]} + ) + + pd.testing.assert_frame_equal( + result.to_pandas(), expected_pd_df, check_dtype=False, check_index_type=False + ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index de80dad710d..52c10f31444 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -47,7 +47,7 @@ def mock_df(): return mock_df -def test_options_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenerator): +def test_options_correct(base_sql_generator: ml_sql.BaseSqlGenerator): sql = base_sql_generator.options( model_type="lin_reg", input_label_cols=["col_a"], l1_reg=0.6 ) @@ -60,7 +60,7 @@ def test_options_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenerato ) -def test_transform_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenerator): +def test_transform_correct(base_sql_generator: ml_sql.BaseSqlGenerator): sql = base_sql_generator.transform( "ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a", "ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b", @@ -75,35 +75,35 @@ def test_transform_produces_correct_sql(base_sql_generator: ml_sql.BaseSqlGenera ) -def test_standard_scaler_produces_correct_sql( +def test_standard_scaler_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_standard_scaler("col_a", "scaled_col_a") assert sql == "ML.STANDARD_SCALER(col_a) OVER() AS scaled_col_a" -def test_max_abs_scaler_produces_correct_sql( +def test_max_abs_scaler_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_max_abs_scaler("col_a", "scaled_col_a") assert sql == "ML.MAX_ABS_SCALER(col_a) OVER() AS scaled_col_a" -def test_min_max_scaler_produces_correct_sql( +def test_min_max_scaler_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_min_max_scaler("col_a", "scaled_col_a") assert sql == "ML.MIN_MAX_SCALER(col_a) OVER() AS scaled_col_a" -def test_k_bins_discretizer_produces_correct_sql( +def test_k_bins_discretizer_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_bucketize("col_a", [1, 2, 3, 4], "scaled_col_a") assert sql == "ML.BUCKETIZE(col_a, [1, 2, 3, 4], FALSE) AS scaled_col_a" -def test_one_hot_encoder_produces_correct_sql( +def test_one_hot_encoder_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_one_hot_encoder( @@ -114,14 +114,25 @@ def test_one_hot_encoder_produces_correct_sql( ) -def test_label_encoder_produces_correct_sql( +def test_label_encoder_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_label_encoder("col_a", 1000000, 0, "encoded_col_a") assert sql == "ML.LABEL_ENCODER(col_a, 1000000, 0) OVER() AS encoded_col_a" -def test_create_model_produces_correct_sql( +def test_distance_correct( + base_sql_generator: ml_sql.BaseSqlGenerator, + mock_df: bpd.DataFrame, +): + sql = base_sql_generator.ml_distance("col_a", "col_b", "COSINE", mock_df, "cosine") + assert ( + sql + == "SELECT *, ML.DISTANCE(col_a, col_b, 'COSINE') AS cosine FROM (input_X_sql)" + ) + + +def test_create_model_correct( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, mock_df: bpd.DataFrame, ): @@ -142,7 +153,7 @@ def test_create_model_produces_correct_sql( ) -def test_create_model_transform_produces_correct_sql( +def test_create_model_transform_correct( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, mock_df: bpd.DataFrame, ): @@ -170,7 +181,7 @@ def test_create_model_transform_produces_correct_sql( ) -def test_create_remote_model_produces_correct_sql( +def test_create_remote_model_correct( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): sql = model_creation_sql_generator.create_remote_model( @@ -190,7 +201,7 @@ def test_create_remote_model_produces_correct_sql( ) -def test_create_remote_model_with_params_produces_correct_sql( +def test_create_remote_model_with_params_correct( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): sql = model_creation_sql_generator.create_remote_model( @@ -216,7 +227,7 @@ def test_create_remote_model_with_params_produces_correct_sql( ) -def test_create_imported_model_produces_correct_sql( +def test_create_imported_model_correct( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): sql = model_creation_sql_generator.create_imported_model( @@ -273,7 +284,7 @@ def test_alter_model_correct_sql( ) -def test_ml_predict_produces_correct_sql( +def test_ml_predict_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, ): @@ -285,7 +296,7 @@ def test_ml_predict_produces_correct_sql( ) -def test_ml_evaluate_produces_correct_sql( +def test_ml_evaluate_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, ): @@ -297,7 +308,7 @@ def test_ml_evaluate_produces_correct_sql( ) -def test_ml_arima_evaluate_produces_correct_sql( +def test_ml_arima_evaluate_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): sql = model_manipulation_sql_generator.ml_arima_evaluate( @@ -310,7 +321,7 @@ def test_ml_arima_evaluate_produces_correct_sql( ) -def test_ml_evaluate_no_source_produces_correct_sql( +def test_ml_evaluate_no_source_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): sql = model_manipulation_sql_generator.ml_evaluate() @@ -320,7 +331,7 @@ def test_ml_evaluate_no_source_produces_correct_sql( ) -def test_ml_centroids_produces_correct_sql( +def test_ml_centroids_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): sql = model_manipulation_sql_generator.ml_centroids() @@ -346,7 +357,7 @@ def test_forecast_correct_sql( ) -def test_ml_generate_text_produces_correct_sql( +def test_ml_generate_text_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, ): @@ -363,7 +374,7 @@ def test_ml_generate_text_produces_correct_sql( ) -def test_ml_generate_text_embedding_produces_correct_sql( +def test_ml_generate_text_embedding_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, ): @@ -380,7 +391,7 @@ def test_ml_generate_text_embedding_produces_correct_sql( ) -def test_ml_principal_components_produces_correct_sql( +def test_ml_principal_components_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): sql = model_manipulation_sql_generator.ml_principal_components() @@ -390,7 +401,7 @@ def test_ml_principal_components_produces_correct_sql( ) -def test_ml_principal_component_info_produces_correct_sql( +def test_ml_principal_component_info_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): sql = model_manipulation_sql_generator.ml_principal_component_info() diff --git a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py new file mode 100644 index 00000000000..3ef54311787 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py @@ -0,0 +1,31 @@ +# Authors: Alexandre Gramfort +# Mathieu Blondel +# Robert Layton +# Andreas Mueller +# Philippe Gervais +# Lars Buitinck +# Joel Nothman +# License: BSD 3 clause + +from bigframes import constants +import bigframes.pandas as bpd + + +def cosine_similarity(X, Y) -> bpd.DataFrame: + """Compute cosine similarity between samples in X and Y. + + Cosine similarity, or the cosine kernel, computes similarity as the + normalized dot product of X and Y: + + K(X, Y) = / (||X||*||Y||) + + Args: + X (Series or single column DataFrame of array of numeric type): + Input data. + Y (Series or single column DataFrame of array of numeric type): + Input data. X and Y are mapped by indexes, must have the same index. + + Returns: + bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and cosine_similarity + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 747e5f64fa5e8d0b9af4d75c293590705b447cf1 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:09:17 -0800 Subject: [PATCH 310/921] chore(main): release 0.21.0 (#373) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 23 +++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2ab05f477..0091fb4a114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.21.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.20.1...v0.21.0) (2024-02-13) + + +### Features + +* Add `Series.cov` method ([#368](https://github.com/googleapis/python-bigquery-dataframes/issues/368)) ([443db22](https://github.com/googleapis/python-bigquery-dataframes/commit/443db228375da9b232376140c9d5b0db14895eae)) +* Add ml.llm.GeminiTextGenerator model ([#370](https://github.com/googleapis/python-bigquery-dataframes/issues/370)) ([de1e0a4](https://github.com/googleapis/python-bigquery-dataframes/commit/de1e0a451785e679f37b083be6d58c267319f56a)) +* Add ml.metrics.pairwise.cosine_similarity function ([#374](https://github.com/googleapis/python-bigquery-dataframes/issues/374)) ([126f566](https://github.com/googleapis/python-bigquery-dataframes/commit/126f5660bd61bd8998e5f17ca0cbd39959590367)) +* Add XGBoostModel ([#363](https://github.com/googleapis/python-bigquery-dataframes/issues/363)) ([d5518b2](https://github.com/googleapis/python-bigquery-dataframes/commit/d5518b28509be0ce070b22d9134a6a662412010a)) +* Limited support of lambdas in `Series.apply` ([#345](https://github.com/googleapis/python-bigquery-dataframes/issues/345)) ([208e081](https://github.com/googleapis/python-bigquery-dataframes/commit/208e081fa99e17b8085e83c111c07eb6fc5c4730)) +* Support bigframes.pandas.to_datetime for scalars, iterables and series. ([#372](https://github.com/googleapis/python-bigquery-dataframes/issues/372)) ([ffb0d15](https://github.com/googleapis/python-bigquery-dataframes/commit/ffb0d15602fe4d86e7a1aad72bba0a7049193a14)) +* Support read_gbq wildcard table path ([#377](https://github.com/googleapis/python-bigquery-dataframes/issues/377)) ([90caf86](https://github.com/googleapis/python-bigquery-dataframes/commit/90caf865efc940f94e16643bda7ba261c2f2e473)) + + +### Bug Fixes + +* Error message fix. ([#375](https://github.com/googleapis/python-bigquery-dataframes/issues/375)) ([930cf6b](https://github.com/googleapis/python-bigquery-dataframes/commit/930cf6b9ae8a48f422586dbd21b52e15c9ef9492)) + + +### Documentation + +* Clarify ADC pre-auth in a non-interactive environment ([#348](https://github.com/googleapis/python-bigquery-dataframes/issues/348)) ([99a9e6e](https://github.com/googleapis/python-bigquery-dataframes/commit/99a9e6e15c6eef4297035ce89bb619f8e4ca54ff)) + ## [0.20.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.20.0...v0.20.1) (2024-02-06) diff --git a/bigframes/version.py b/bigframes/version.py index fca3eec0237..a713192ada0 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.20.1" +__version__ = "0.21.0" From 93b02cd8bc620823563f8214b43bc5f2f35c155b Mon Sep 17 00:00:00 2001 From: chelsea-lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:45:39 -0800 Subject: [PATCH 311/921] fix: correct the numeric literal dtype (#365) --- bigframes/dtypes.py | 20 +++++++++++++++++++- tests/system/small/test_series.py | 10 ++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index cb2210bec64..6e3bc25c477 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -378,11 +378,29 @@ def literal_to_ibis_scalar( scalar_expr = ibis.literal(literal, ibis_dtypes.float64) elif scalar_expr.type().is_integer(): scalar_expr = ibis.literal(literal, ibis_dtypes.int64) + elif scalar_expr.type().is_decimal(): + precision = scalar_expr.type().precision + scale = scalar_expr.type().scale + if (not precision and not scale) or ( + precision and scale and scale <= 9 and precision + (9 - scale) <= 38 + ): + scalar_expr = ibis.literal( + literal, ibis_dtypes.decimal(precision=38, scale=9) + ) + elif precision and scale and scale <= 38 and precision + (38 - scale) <= 76: + scalar_expr = ibis.literal( + literal, ibis_dtypes.decimal(precision=76, scale=38) + ) + else: + raise TypeError( + "BigQuery's decimal data type supports a maximum precision of 76 and a maximum scale of 38." + f"Current precision: {precision}. Current scale: {scale}" + ) # TODO(bmil): support other literals that can be coerced to compatible types if validate and (scalar_expr.type() not in BIGFRAMES_TO_IBIS.values()): raise ValueError( - f"Literal did not coerce to a supported data type: {literal}. {constants.FEEDBACK_LINK}" + f"Literal did not coerce to a supported data type: {scalar_expr.type()}. {constants.FEEDBACK_LINK}" ) return scalar_expr diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 42651ed96f5..37b4f8c1dea 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1228,6 +1228,16 @@ def test_median(scalars_dfs): assert pd_min < bf_result < pd_max +def test_numeric_literal(scalars_dfs): + scalars_df, _ = scalars_dfs + col_name = "numeric_col" + assert scalars_df[col_name].dtype == pd.ArrowDtype(pa.decimal128(38, 9)) + bf_result = scalars_df[col_name] - scalars_df[col_name].median() + assert bf_result.size == scalars_df[col_name].size + # TODO(b/323387826): The precision increased by 1 unexpectedly. + # assert bf_result.dtype == pd.ArrowDtype(pa.decimal128(38, 9)) + + def test_repr(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs if scalars_pandas_df.index.name != "rowindex": From 234b61cdfe75b402adf1b56f53b5f06934777f95 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 16 Feb 2024 01:59:47 +0000 Subject: [PATCH 312/921] docs: Fix `bigframes.pandas.concat` documentation (#382) --- .../pandas/core/reshape/concat.py | 189 +++++++++--------- 1 file changed, 93 insertions(+), 96 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/reshape/concat.py b/third_party/bigframes_vendored/pandas/core/reshape/concat.py index b0472c524ab..685a73abc20 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/concat.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/concat.py @@ -14,8 +14,7 @@ def concat( join: str = "outer", ignore_index: bool = False, ): - """ - Concatenate BigQuery DataFrames objects along a particular axis. + """Concatenate BigQuery DataFrames objects along a particular axis. Allows optional set logic along the other axes. @@ -23,118 +22,116 @@ def concat( which may be useful if the labels are the same (or overlapping) on the passed axis number. - Parameters - ---------- - objs: - Objects to concatenate. Any None objects will be dropped silently unless - they are all None in which case a ValueError will be raised. - axis : {0/'index', 1/'columns'}, default 0 - The axis to concatenate along. - join: {'inner', 'outer'}, default 'outer' - How to handle indexes on other axis (or axes). - ignore_index : bool, default False - If True, do not use the index values along the concatenation axis. The - resulting axis will be labeled 0, ..., n - 1. This is useful if you are - concatenating objects where the concatenation axis does not have - meaningful indexing information. Note the index values on the other - axes are still respected in the join. - - Returns - ------- - object, type of objs - When concatenating all ``Series`` along the index (axis=0), a - ``Series`` is returned. When ``objs`` contains at least one - ``DataFrame``, a ``DataFrame`` is returned. - - Notes - ----- - It is not recommended to build DataFrames by adding single rows in a - for loop. Build a list of rows and make a DataFrame in a single concat. - - Examples - -------- + .. note:: + It is not recommended to build DataFrames by adding single rows in a + for loop. Build a list of rows and make a DataFrame in a single concat. + + **Examples:** + + >>> import bigframes.pandas as pd + >>> pd.options.display.progress_bar = None + Combine two ``Series``. - >>> import bigframes.pandas as pd - >>> pd.options.display.progress_bar = None - >>> s1 = pd.Series(['a', 'b']) - >>> s2 = pd.Series(['c', 'd']) - >>> pd.concat([s1, s2]) - 0 a - 1 b - 0 c - 1 d - dtype: string + >>> s1 = pd.Series(['a', 'b']) + >>> s2 = pd.Series(['c', 'd']) + >>> pd.concat([s1, s2]) + 0 a + 1 b + 0 c + 1 d + dtype: string Clear the existing index and reset it in the result by setting the ``ignore_index`` option to ``True``. - >>> pd.concat([s1, s2], ignore_index=True) - 0 a - 1 b - 2 c - 3 d - dtype: string + >>> pd.concat([s1, s2], ignore_index=True) + 0 a + 1 b + 2 c + 3 d + dtype: string Combine two ``DataFrame`` objects with identical columns. - >>> df1 = pd.DataFrame([['a', 1], ['b', 2]], - ... columns=['letter', 'number']) - >>> df1 - letter number - 0 a 1 - 1 b 2 - - [2 rows x 2 columns] - >>> df2 = pd.DataFrame([['c', 3], ['d', 4]], - ... columns=['letter', 'number']) - >>> df2 - letter number - 0 c 3 - 1 d 4 - - [2 rows x 2 columns] - >>> pd.concat([df1, df2]) - letter number - 0 a 1 - 1 b 2 - 0 c 3 - 1 d 4 - - [4 rows x 2 columns] + >>> df1 = pd.DataFrame([['a', 1], ['b', 2]], + ... columns=['letter', 'number']) + >>> df1 + letter number + 0 a 1 + 1 b 2 + + [2 rows x 2 columns] + >>> df2 = pd.DataFrame([['c', 3], ['d', 4]], + ... columns=['letter', 'number']) + >>> df2 + letter number + 0 c 3 + 1 d 4 + + [2 rows x 2 columns] + >>> pd.concat([df1, df2]) + letter number + 0 a 1 + 1 b 2 + 0 c 3 + 1 d 4 + + [4 rows x 2 columns] Combine ``DataFrame`` objects with overlapping columns and return everything. Columns outside the intersection will be filled with ``NaN`` values. - >>> df3 = pd.DataFrame([['c', 3, 'cat'], ['d', 4, 'dog']], - ... columns=['letter', 'number', 'animal']) - >>> df3 - letter number animal - 0 c 3 cat - 1 d 4 dog - - [2 rows x 3 columns] - >>> pd.concat([df1, df3]) - letter number animal - 0 a 1 - 1 b 2 - 0 c 3 cat - 1 d 4 dog - - [4 rows x 3 columns] + >>> df3 = pd.DataFrame([['c', 3, 'cat'], ['d', 4, 'dog']], + ... columns=['letter', 'number', 'animal']) + >>> df3 + letter number animal + 0 c 3 cat + 1 d 4 dog + + [2 rows x 3 columns] + >>> pd.concat([df1, df3]) + letter number animal + 0 a 1 + 1 b 2 + 0 c 3 cat + 1 d 4 dog + + [4 rows x 3 columns] Combine ``DataFrame`` objects with overlapping columns and return only those that are shared by passing ``inner`` to the ``join`` keyword argument. - >>> pd.concat([df1, df3], join="inner") - letter number - 0 a 1 - 1 b 2 - 0 c 3 - 1 d 4 - - [4 rows x 2 columns] + >>> pd.concat([df1, df3], join="inner") + letter number + 0 a 1 + 1 b 2 + 0 c 3 + 1 d 4 + + [4 rows x 2 columns] + + Args: + objs (list of objects): + Objects to concatenate. Any None objects will be dropped silently unless + they are all None in which case a ValueError will be raised. + axis ({0 or 'index', 1 or 'columns'}, default 0): + The axis to concatenate along. + join ({'inner', 'outer'}, default 'outer'): + How to handle indexes on other axis (or axes). + ignore_index (bool, default False): + If True, do not use the index values along the concatenation axis. The + resulting axis will be labeled 0, ..., n - 1. This is useful if you are + concatenating objects where the concatenation axis does not have + meaningful indexing information. Note the index values on the other + axes are still respected in the join. + + Returns: + object, type of objs: + When concatenating all ``Series`` along the index (axis=0), a + ``Series`` is returned. When ``objs`` contains at least one + ``DataFrame``, a ``DataFrame`` is returned. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 67fd434bbb1c73f9013f65252d1ecc8da79542f6 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:13:27 -0800 Subject: [PATCH 313/921] feat: add `DataFrames.corr()` method (#379) * feat: add `DataFrames.corr()` method * support multi-indices * fix mypy * support non-numeric col * fix doc * fix system 3.9 * fix doctest --- bigframes/core/blocks.py | 46 +++++++++++++++++-- bigframes/dataframe.py | 21 +++++++++ tests/system/small/test_dataframe.py | 40 ++++++++++++++++ tests/system/small/test_multiindex.py | 21 +++++++++ .../bigframes_vendored/pandas/core/frame.py | 34 ++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 2 +- 6 files changed, 160 insertions(+), 4 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 34df7231cc2..993f2caa474 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -102,11 +102,11 @@ def __init__( ): """Construct a block object, will create default index if no index columns specified.""" index_columns = list(index_columns) - if index_labels: + if index_labels is not None: index_labels = list(index_labels) if len(index_labels) != len(index_columns): raise ValueError( - "'index_columns' and 'index_labels' must have equal length" + f"'index_columns' (size {len(index_columns)}) and 'index_labels' (size {len(index_labels)}) must have equal length" ) if len(index_columns) == 0: new_index_col_id = guid.generate_guid() @@ -1089,6 +1089,46 @@ def summarize( labels = self._get_labels_for_columns(column_ids) return Block(expr, column_labels=labels, index_columns=[label_col_id]) + def corr(self): + """Returns a block object to compute the self-correlation on this block.""" + aggregations = [ + ( + ex.BinaryAggregation( + agg_ops.CorrOp(), ex.free_var(left_col), ex.free_var(right_col) + ), + f"{left_col}-{right_col}", + ) + for left_col in self.value_columns + for right_col in self.value_columns + ] + expr = self.expr.aggregate(aggregations) + + index_col_ids = [ + guid.generate_guid() for i in range(self.column_labels.nlevels) + ] + input_count = len(self.value_columns) + unpivot_columns = tuple( + ( + guid.generate_guid(), + tuple(expr.column_ids[input_count * i : input_count * (i + 1)]), + ) + for i in range(input_count) + ) + labels = self._get_labels_for_columns(self.value_columns) + + expr = expr.unpivot( + row_labels=labels, + index_col_ids=index_col_ids, + unpivot_columns=unpivot_columns, + ) + + return Block( + expr, + column_labels=self.column_labels, + index_columns=index_col_ids, + index_labels=self.column_labels.names, + ) + def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.UnaryAggregateOp]: """ Gets a standard set of stats to preemptively fetch for a column if @@ -1889,7 +1929,7 @@ def to_pandas(self) -> pd.Index: df = expr.session._rows_to_dataframe(results, dtypes) df = df.set_index(index_columns) index = df.index - index.names = list(self._block._index_labels) + index.names = list(self._block._index_labels) # type:ignore return index def resolve_level(self, level: LevelsType) -> typing.Sequence[str]: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9db567a4979..ccbf68ebb58 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1017,6 +1017,27 @@ def combine( def combine_first(self, other: DataFrame): return self._apply_dataframe_binop(other, ops.fillna_op) + def corr(self, method="pearson", min_periods=None, numeric_only=False) -> DataFrame: + if method != "pearson": + raise NotImplementedError( + f"Only Pearson correlation is currently supported. {constants.FEEDBACK_LINK}" + ) + if min_periods: + raise NotImplementedError( + f"min_periods not yet supported. {constants.FEEDBACK_LINK}" + ) + if len(self.columns) > 30: + raise NotImplementedError( + f"Only work with dataframes containing fewer than 30 columns. Current: {len(self.columns)}. {constants.FEEDBACK_LINK}" + ) + + if not numeric_only: + frame = self._raise_on_non_numeric("corr") + else: + frame = self._drop_non_numeric() + + return DataFrame(frame._block.corr()) + def to_pandas( self, max_download_size: Optional[int] = None, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 3d312530213..8f75534fc61 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1783,6 +1783,46 @@ def test_combine_first( pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) +@pytest.mark.parametrize( + ("columns", "numeric_only"), + [ + (["bool_col", "int64_col", "float64_col"], True), + (["bool_col", "int64_col", "float64_col"], False), + (["bool_col", "int64_col", "float64_col", "string_col"], True), + pytest.param( + ["bool_col", "int64_col", "float64_col", "string_col"], + False, + marks=pytest.mark.xfail( + raises=NotImplementedError, + ), + ), + ], +) +def test_corr_w_numeric_only(scalars_dfs, columns, numeric_only): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df[columns].corr(numeric_only=numeric_only).to_pandas() + pd_result = scalars_pandas_df[columns].corr(numeric_only=numeric_only) + + # BigFrames and Pandas differ in their data type handling: + # - Column types: BigFrames uses Float64, Pandas uses float64. + # - Index types: BigFrames uses strign, Pandas uses object. + pd.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + +def test_corr_w_invalid_parameters(scalars_dfs): + columns = ["int64_too", "int64_col", "float64_col"] + scalars_df, _ = scalars_dfs + + with pytest.raises(NotImplementedError): + scalars_df[columns].corr(method="kendall") + + with pytest.raises(NotImplementedError): + scalars_df[columns].corr(min_periods=1) + + @pytest.mark.parametrize( ("op"), [ diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index c5e8b45b8ee..e0b91643157 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -880,6 +880,27 @@ def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) +def test_corr_w_multi_index(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "float64_col", "int64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "b"], [1, 2, 2])) + + bf = scalars_df_index[columns].copy() + bf.columns = multi_columns + + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf.corr(numeric_only=True).to_pandas() + pd_result = pd_df.corr(numeric_only=True) + + # BigFrames and Pandas differ in their data type handling: + # - Column types: BigFrames uses Float64, Pandas uses float64. + # - Index types: BigFrames uses strign, Pandas uses object. + pandas.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + @pytest.mark.parametrize( ("index_names",), [ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 05f41678384..84d2aa7fcb1 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2805,6 +2805,40 @@ def combine_first(self, other) -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def corr(self, method, min_periods, numeric_only) -> DataFrame: + """ + Compute pairwise correlation of columns, excluding NA/null values. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': [1, 2, 3], + ... 'B': [400, 500, 600], + ... 'C': [0.8, 0.4, 0.9]}) + >>> df.corr(numeric_only=True) + A B C + A 1.0 1.0 0.188982 + B 1.0 1.0 0.188982 + C 0.188982 0.188982 1.0 + + [3 rows x 3 columns] + + Args: + method (string, default "pearson"): + Correlation method to use - currently only "pearson" is supported. + min_periods (int, default None): + The minimum number of observations needed to return a result. Non-default values + are not yet supported, so a result will be returned for at least two observations. + numeric_only(bool, default False): + Include only float, int, boolean, decimal data. + + Returns: + DataFrame: Correlation matrix. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def update( self, other, join: str = "left", overwrite: bool = True, filter_func=None ) -> DataFrame: diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index b2034716067..6c01a6dd0cb 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -840,7 +840,7 @@ def corr(self, other, method="pearson", min_periods=None) -> float: float: Will return NaN if there are fewer than two numeric pairs, either series has a variance or covariance of zero, or any input value is infinite. """ - raise NotImplementedError("abstract method") + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def cov( self, From 59b446bad8d2c5fca791c384616cfa7e54d54c09 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:48:15 -0800 Subject: [PATCH 314/921] perf: inline read_pandas for small data (#383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #312729021 🦕 --- bigframes/dataframe.py | 16 +----------- bigframes/operations/base.py | 16 +----------- bigframes/session/__init__.py | 33 ++++++++++++++++++++++--- tests/system/small/test_progress_bar.py | 9 ++++++- tests/unit/session/test_io_bigquery.py | 16 ++++++++---- 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ccbf68ebb58..d467239ea62 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -69,10 +69,6 @@ import bigframes.session -# BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. -# TODO(tbergeron): Convert to bytes-based limit -MAX_INLINE_DF_SIZE = 5000 - LevelType = typing.Hashable LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] SingleItemValue = Union[bigframes.series.Series, int, float, Callable] @@ -170,17 +166,7 @@ def __init__( columns=columns, # type:ignore dtype=dtype, # type:ignore ) - if ( - pd_dataframe.size < MAX_INLINE_DF_SIZE - # TODO(swast): Workaround data types limitation in inline data. - and not any( - dt.pyarrow_dtype - for dt in pd_dataframe.dtypes - if isinstance(dt, pandas.ArrowDtype) - ) - ): - self._block = blocks.Block.from_local(pd_dataframe) - elif session: + if session: self._block = session.read_pandas(pd_dataframe)._get_block() else: self._block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 04114b43cb3..154247c0332 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -30,10 +30,6 @@ import bigframes.session import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing -# BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. -# TODO(tbergeron): Convert to bytes-based limit -MAX_INLINE_SERIES_SIZE = 5000 - class SeriesMethods: def __init__( @@ -104,17 +100,7 @@ def __init__( if pd_series.name is None: # to_frame will set default numeric column label if unnamed, but we do not support int column label, so must rename pd_dataframe = pd_dataframe.set_axis(["unnamed_col"], axis=1) - if ( - pd_dataframe.size < MAX_INLINE_SERIES_SIZE - # TODO(swast): Workaround data types limitation in inline data. - and not any( - dt.pyarrow_dtype - for dt in pd_dataframe.dtypes - if isinstance(dt, pd.ArrowDtype) - ) - ): - block = blocks.Block.from_local(pd_dataframe) - elif session: + if session: block = session.read_pandas(pd_dataframe)._get_block() else: # Uses default global session diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index df0cd6e947a..20dd39c0fa5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -108,6 +108,10 @@ "UTF-32LE", } +# BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. +# TODO(tbergeron): Convert to bytes-based limit +MAX_INLINE_DF_SIZE = 5000 + logger = logging.getLogger(__name__) @@ -882,6 +886,29 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame def _read_pandas( self, pandas_dataframe: pandas.DataFrame, api_name: str + ) -> dataframe.DataFrame: + if ( + pandas_dataframe.size < MAX_INLINE_DF_SIZE + # TODO(swast): Workaround data types limitation in inline data. + and not any( + ( + isinstance(s.dtype, pandas.ArrowDtype) + or (len(s) > 0 and pandas.api.types.is_list_like(s.iloc[0])) + or pandas.api.types.is_datetime64_any_dtype(s) + ) + for _, s in pandas_dataframe.items() + ) + ): + return self._read_pandas_inline(pandas_dataframe) + return self._read_pandas_load_job(pandas_dataframe, api_name) + + def _read_pandas_inline( + self, pandas_dataframe: pandas.DataFrame + ) -> dataframe.DataFrame: + return dataframe.DataFrame(blocks.Block.from_local(pandas_dataframe)) + + def _read_pandas_load_job( + self, pandas_dataframe: pandas.DataFrame, api_name: str ) -> dataframe.DataFrame: col_labels, idx_labels = ( pandas_dataframe.columns.to_list(), @@ -1079,7 +1106,7 @@ def read_csv( encoding=encoding, **kwargs, ) - return self.read_pandas(pandas_df) # type: ignore + return self._read_pandas(pandas_df, "read_csv") # type: ignore def read_pickle( self, @@ -1096,7 +1123,7 @@ def read_pickle( if isinstance(pandas_obj, pandas.Series): if pandas_obj.name is None: pandas_obj.name = "0" - bigframes_df = self.read_pandas(pandas_obj.to_frame()) + bigframes_df = self._read_pandas(pandas_obj.to_frame(), "read_pickle") return bigframes_df[bigframes_df.columns[0]] return self._read_pandas(pandas_obj, "read_pickle") @@ -1196,7 +1223,7 @@ def read_json( engine=engine, **kwargs, ) - return self.read_pandas(pandas_df) + return self._read_pandas(pandas_df, "read_json") def _check_file_size(self, filepath: str): max_size = 1024 * 1024 * 1024 # 1 GB in bytes diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index bd13ac22401..1c04b580fc1 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -15,10 +15,12 @@ import re import tempfile +import numpy as np import pandas as pd import bigframes as bf import bigframes.formatting_helpers as formatting_helpers +from bigframes.session import MAX_INLINE_DF_SIZE job_load_message_regex = r"\w+ job [\w-]+ is \w+\." @@ -66,10 +68,15 @@ def test_progress_bar_extract_jobs( def test_progress_bar_load_jobs( session: bf.Session, penguins_pandas_df_default_index: pd.DataFrame, capsys ): + # repeat the DF to be big enough to trigger the load job. + df = penguins_pandas_df_default_index + while len(df) < MAX_INLINE_DF_SIZE: + df = pd.DataFrame(np.repeat(df.values, 2, axis=0)) + bf.options.display.progress_bar = "terminal" with tempfile.TemporaryDirectory() as dir: path = dir + "/test_read_csv_progress_bar*.csv" - penguins_pandas_df_default_index.to_csv(path, index=False) + df.to_csv(path, index=False) capsys.readouterr() # clear output session.read_csv(path) diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 96bb7bf67fc..406de2b88e6 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -23,6 +23,7 @@ from bigframes.core import log_adapter import bigframes.pandas as bpd import bigframes.session._io.bigquery as io_bq +from tests.unit import resources def test_create_job_configs_labels_is_none(): @@ -64,7 +65,9 @@ def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", } - df = bpd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) + df = bpd.DataFrame( + {"col1": [1, 2], "col2": [3, 4]}, session=resources.create_bigquery_session() + ) # Test running two methods df.head() df.max() @@ -81,15 +84,16 @@ def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): "recent-bigframes-api-2": "dataframe-__init__", "recent-bigframes-api-3": "dataframe-head", "recent-bigframes-api-4": "dataframe-__init__", + "recent-bigframes-api-5": "dataframe-__init__", } - assert labels is not None - assert len(labels) == 7 assert labels == expected_dict def test_create_job_configs_labels_length_limit_met_and_labels_is_none(): log_adapter.get_and_reset_api_methods() - df = bpd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) + df = bpd.DataFrame( + {"col1": [1, 2], "col2": [3, 4]}, session=resources.create_bigquery_session() + ) # Test running methods more than the labels' length limit for i in range(66): df.head() @@ -114,7 +118,9 @@ def test_create_job_configs_labels_length_limit_met(): value = f"test{i}" cur_labels[key] = value # If cur_labels length is 62, we can only add one label from api_methods - df = bpd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) + df = bpd.DataFrame( + {"col1": [1, 2], "col2": [3, 4]}, session=resources.create_bigquery_session() + ) # Test running two methods df.head() df.max() From 4037992b61ff352320d5dfb87dcf5f274791ace1 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:42:15 -0800 Subject: [PATCH 315/921] refactor!: move model optional args to kwargs (#381) To be more like sklearn, and make API more accurate. Those param shouldn't be called through positions. --- bigframes/ml/base.py | 11 +++++----- bigframes/ml/ensemble.py | 4 ++++ bigframes/ml/forecasting.py | 2 +- bigframes/ml/imported.py | 36 +++++++++++++++++++------------- bigframes/ml/linear_model.py | 2 ++ bigframes/ml/llm.py | 5 +++++ bigframes/ml/metrics/_metrics.py | 6 ++++++ bigframes/ml/remote.py | 1 + 8 files changed, 45 insertions(+), 22 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index f2478b1ce23..845b64caf1c 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -127,6 +127,10 @@ def register(self: _T, vertex_ai_model_id: Optional[str] = None) -> _T: self._bqml_model.register(vertex_ai_model_id) return self + @abc.abstractmethod + def to_gbq(self, model_name, replace): + pass + class TrainablePredictor(Predictor): """A BigQuery DataFrames ML Model base class that can be used to fit and predict outputs. @@ -141,11 +145,6 @@ def _fit(self, X, y, transforms=None): def score(self, X, y): pass - # TODO(b/291812029): move to Predictor after implement in LLM and imported models - @abc.abstractmethod - def to_gbq(self, model_name, replace): - pass - class SupervisedTrainablePredictor(TrainablePredictor): """A BigQuery DataFrames ML Supervised Model base class that can be used to fit and predict outputs. @@ -165,7 +164,7 @@ def fit( class UnsupervisedTrainablePredictor(TrainablePredictor): """A BigQuery DataFrames ML Unsupervised Model base class that can be used to fit and predict outputs. - Only need to provide both X (y is optional and ignored) in unsupervised tasks.""" + Only need to provide X (y is optional and ignored) in unsupervised tasks.""" _T = TypeVar("_T", bound="UnsupervisedTrainablePredictor") diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 1cc9fb37395..7fcaa926edb 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -58,6 +58,7 @@ class XGBRegressor( def __init__( self, num_parallel_tree: int = 1, + *, booster: Literal["gbtree", "dart"] = "gbtree", dart_normalized_type: Literal["tree", "forest"] = "tree", tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", @@ -215,6 +216,7 @@ class XGBClassifier( def __init__( self, num_parallel_tree: int = 1, + *, booster: Literal["gbtree", "dart"] = "gbtree", dart_normalized_type: Literal["tree", "forest"] = "tree", tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", @@ -372,6 +374,7 @@ class RandomForestRegressor( def __init__( self, num_parallel_tree: int = 100, + *, tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", min_tree_child_weight: int = 1, colsample_bytree=1.0, @@ -538,6 +541,7 @@ class RandomForestClassifier( def __init__( self, num_parallel_tree: int = 100, + *, tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", min_tree_child_weight: int = 1, colsample_bytree: float = 1.0, diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 8d448fbace2..0c33660475e 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -87,7 +87,7 @@ def _fit( ) def predict( - self, X=None, horizon: int = 3, confidence_level: float = 0.95 + self, X=None, *, horizon: int = 3, confidence_level: float = 0.95 ) -> bpd.DataFrame: """Predict the closest cluster for each sample in X. diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index e2be1547035..98b23931f3d 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -32,15 +32,17 @@ class TensorFlowModel(base.Predictor): """Imported TensorFlow model. Args: + model_path (str): + GCS path that holds the model files. session (BigQuery Session): BQ session to create the model - model_path (str): - GCS path that holds the model files.""" + """ def __init__( self, + model_path: str, + *, session: Optional[bigframes.Session] = None, - model_path: Optional[str] = None, ): self.session = session or bpd.get_global_session() self.model_path = model_path @@ -59,7 +61,7 @@ def _from_bq( ) -> TensorFlowModel: assert model.model_type == "TENSORFLOW" - tf_model = cls(session=session, model_path=None) + tf_model = cls(session=session, model_path="") tf_model._bqml_model = core.BqmlModel(session, model) return tf_model @@ -109,15 +111,17 @@ class ONNXModel(base.Predictor): """Imported Open Neural Network Exchange (ONNX) model. Args: + model_path (str): + Cloud Storage path that holds the model files. session (BigQuery Session): BQ session to create the model - model_path (str): - Cloud Storage path that holds the model files.""" + """ def __init__( self, + model_path: str, + *, session: Optional[bigframes.Session] = None, - model_path: Optional[str] = None, ): self.session = session or bpd.get_global_session() self.model_path = model_path @@ -134,7 +138,7 @@ def _create_bqml_model(self): def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ONNXModel: assert model.model_type == "ONNX" - onnx_model = cls(session=session, model_path=None) + onnx_model = cls(session=session, model_path="") onnx_model._bqml_model = core.BqmlModel(session, model) return onnx_model @@ -189,8 +193,8 @@ class XGBoostModel(base.Predictor): https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-xgboost#limitations Args: - session (BigQuery Session): - BQ session to create the model + model_path (str): + Cloud Storage path that holds the model files. input (Dict, default None): Specify the model input schema information when you create the XGBoost model. The input should be the format of @@ -203,15 +207,17 @@ class XGBoostModel(base.Predictor): {field_name: field_type}. Output is optional only if feature_names and feature_types are both specified in the model file. Supported types are "bool", "string", "int64", "float64", "array", "array", "array", "array". - model_path (str): - Cloud Storage path that holds the model files.""" + session (BigQuery Session): + BQ session to create the model + """ def __init__( self, - session: Optional[bigframes.Session] = None, + model_path: str, + *, input: Mapping[str, str] = {}, output: Mapping[str, str] = {}, - model_path: Optional[str] = None, + session: Optional[bigframes.Session] = None, ): self.session = session or bpd.get_global_session() self.model_path = model_path @@ -248,7 +254,7 @@ def _from_bq( ) -> XGBoostModel: assert model.model_type == "XGBOOST" - xgboost_model = cls(session=session, model_path=None) + xgboost_model = cls(session=session, model_path="") xgboost_model._bqml_model = core.BqmlModel(session, model) return xgboost_model diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 5ee87b88505..b0c40693528 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -58,6 +58,7 @@ class LinearRegression( def __init__( self, + *, optimize_strategy: Literal[ "auto_strategy", "batch_gradient_descent", "normal_equation" ] = "normal_equation", @@ -192,6 +193,7 @@ class LogisticRegression( # TODO(ashleyxu) support class_weights in the constructor. def __init__( self, + *, fit_intercept: bool = True, class_weights: Optional[Union[Literal["balanced"], Dict[str, float]]] = None, ): diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index f44310f6a59..dfe0af2f25a 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -66,6 +66,7 @@ class PaLM2TextGenerator(base.Predictor): def __init__( self, + *, model_name: Literal["text-bison", "text-bison-32k"] = "text-bison", session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, @@ -140,6 +141,7 @@ def _from_bq( def predict( self, X: Union[bpd.DataFrame, bpd.Series], + *, temperature: float = 0.0, max_output_tokens: int = 128, top_k: int = 40, @@ -273,6 +275,7 @@ class PaLM2TextEmbeddingGenerator(base.Predictor): def __init__( self, + *, model_name: Literal[ "textembedding-gecko", "textembedding-gecko-multilingual" ] = "textembedding-gecko", @@ -415,6 +418,7 @@ class GeminiTextGenerator(base.Predictor): def __init__( self, + *, session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, ): @@ -475,6 +479,7 @@ def _from_bq( def predict( self, X: Union[bpd.DataFrame, bpd.Series], + *, temperature: float = 0.9, max_output_tokens: int = 8192, top_k: int = 40, diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index 5731b946ca9..5c81f16e31a 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -34,6 +34,7 @@ def r2_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], + *, force_finite=True, ) -> float: y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) @@ -61,6 +62,7 @@ def r2_score( def accuracy_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], + *, normalize=True, ) -> float: # TODO(ashleyxu): support sample_weight as the parameter @@ -83,6 +85,7 @@ def accuracy_score( def roc_curve( y_true: Union[bpd.DataFrame, bpd.Series], y_score: Union[bpd.DataFrame, bpd.Series], + *, drop_intermediate: bool = True, ) -> Tuple[bpd.Series, bpd.Series, bpd.Series]: # TODO(bmil): Add multi-class support @@ -227,6 +230,7 @@ def confusion_matrix( def recall_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], + *, average: str = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" @@ -263,6 +267,7 @@ def recall_score( def precision_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], + *, average: str = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" @@ -301,6 +306,7 @@ def precision_score( def f1_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], + *, average: str = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index 44fde4f32f7..a4a95b39d11 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -54,6 +54,7 @@ def __init__( endpoint: str, input: Mapping[str, str], output: Mapping[str, str], + *, session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, ): From 8b62d77d8274cff2842c98b032bf98d69c483482 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 23 Feb 2024 13:20:08 -0600 Subject: [PATCH 316/921] deps: add minimum version constraint for sqlglot to 19.9.0 (#389) * deps: add minimum version constraint for sqlglot to 19.9.0 This shouldn't be necessary, as sqlglot is only used as a transitive dependency through ibis-framework, but in setting the constraints, we can have greater assurance that sqlglot is upgraded alongside bigframes in existing environments. * formatting --- setup.py | 7 ++- testing/constraints-3.9.txt | 117 +++--------------------------------- 2 files changed, 16 insertions(+), 108 deletions(-) diff --git a/setup.py b/setup.py index 345d1ea752d..4aa07904f77 100644 --- a/setup.py +++ b/setup.py @@ -32,11 +32,12 @@ # 'Development Status :: 5 - Production/Stable' release_status = "Development Status :: 3 - Alpha" dependencies = [ + # please keep these in sync with the minimum versions in testing/constraints-3.9.txt "cloudpickle >= 2.0.0", "fsspec >=2023.3.0", "gcsfs >=2023.3.0", "geopandas >=0.12.2", - "google-auth >2.14.1,<3.0dev", + "google-auth >=2.15.0,<3.0dev", "google-cloud-bigquery[bqstorage,pandas] >=3.10.0", "google-cloud-functions >=1.10.1", "google-cloud-bigquery-connection >=1.12.0", @@ -51,6 +52,10 @@ "requests >=2.27.1", "scikit-learn >=1.2.2", "sqlalchemy >=1.4,<3.0dev", + # Keep sqlglot versions in sync with ibis-framework. This avoids problems + # where the incorrect version of sqlglot is installed, such as + # https://github.com/googleapis/python-bigquery-dataframes/issues/315 + "sqlglot >=19.9.0,<20", "tabulate >= 0.9", "ipywidgets >=7.7.1", "humanize >= 4.6.0", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 218255c77e6..42cc68eb045 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -1,121 +1,24 @@ -argcomplete==2.1.2 -asyncmock==0.4.2 -atpublic==3.1.1 -attrs==22.2.0 -bidict==0.22.1 -black==23.3.0 -bleach==6.0.0 -cachetools==5.3.0 -certifi==2022.12.7 -cffi==1.15.1 -cfgv==3.3.1 -charset-normalizer==2.0.0 -click==8.1.3 +# please keep these in sync with the minimum versions in setup.py cloudpickle==2.0.0 -colorlog==6.7.0 -coverage==7.2.2 -cryptography==40.0.1 -distlib==0.3.6 -docstring-inheritance==2.0.0 -docutils==0.19 -exceptiongroup==1.1.1 -execnet==1.9.0 -filelock==3.10.7 fsspec==2023.3.0 -gcp-docuploader==0.6.5 -gcp-releasetool==1.11.0 gcsfs==2023.3.0 geopandas==0.12.2 -google-api-core==2.11.0 -google-auth==2.17.0 -google-auth-oauthlib==1.0.0 +google-auth==2.15.0 google-cloud-bigquery==3.10.0 -google-cloud-bigquery-connection==1.12.0 -google-cloud-bigquery-storage==2.19.1 -google-cloud-core==2.3.2 google-cloud-functions==1.10.1 +google-cloud-bigquery-connection==1.12.0 google-cloud-iam==2.12.1 google-cloud-resource-manager==1.10.3 google-cloud-storage==2.0.0 -google-cloud-testutils==1.3.3 -google-crc32c==1.5.0 -google-resumable-media==2.4.1 -googleapis-common-protos==1.59.0 -greenlet==2.0.2 -grpc-google-iam-v1==0.12.6 -grpcio==1.53.0 -grpcio-status==1.48.2 ibis-framework==7.1.0 -humanize==4.6.0 -identify==2.5.22 -idna==3.4 -importlib-metadata==6.1.0 -iniconfig==2.0.0 -ipywidgets==7.7.1 -jaraco.classes==3.2.3 -jeepney==0.8.0 -Jinja2==3.1.2 -keyring==23.13.1 -markdown-it-py==2.2.0 -MarkupSafe==2.1.2 -mdurl==0.1.2 -mock==5.0.1 -more-itertools==9.1.0 -multipledispatch==0.6.0 -mypy-extensions==1.0.0 -nodeenv==1.7.0 -nox==2022.11.21 -numpy==1.24.2 -oauthlib==3.2.2 -packaging==23.0 pandas==1.5.0 -pandas-gbq==0.19.0 -parsy==2.1 -pathspec==0.11.1 -pkginfo==1.9.6 -platformdirs==3.2.0 -pluggy==1.0.0 -pooch==1.7.0 -pre-commit==3.2.1 -proto-plus==1.22.2 -protobuf==3.20.3 -pyarrow==11.0.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycparser==2.21 pydata-google-auth==1.8.2 -Pygments==2.14.0 -PyJWT==2.6.0 -pyperclip==1.8.2 -pytest==7.2.2 -pytest-asyncio==0.21.0 -pytest-cov==4.0.0 -pytest-mock==3.11.1 -pytest-retry==1.1.0 -pytest-xdist==3.2.1 -python-dateutil==2.8.2 -pytz==2023.3 -PyYAML==6.0 -readme-renderer==37.3 requests==2.27.1 -requests-oauthlib==1.3.1 -requests-toolbelt==0.10.1 -rfc3986==2.0.0 -rich==13.3.3 -rsa==4.9 scikit-learn==1.2.2 -SecretStorage==3.3.3 -six==1.16.0 -SQLAlchemy==1.4.0 -sqlglot==18.12.0 -tomli==2.0.1 -toolz==0.12.0 -tqdm==4.65.0 -twine==4.0.2 -typing_extensions==4.5.0 -tzdata==2023.3 -urllib3==1.26.15 -virtualenv==20.21.0 -webencodings==0.5.1 -xxhash==3.2.0 -zipp==3.15.0 +sqlalchemy==1.4 +sqlglot==19.9.0 +tabulate==0.9 +ipywidgets==7.7.1 +humanize==4.6.0 +# extras +pandas-gbq==0.19.0 From 1040dff69d35fedb58be465068beacaa37edcfb3 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 23 Feb 2024 14:50:17 -0600 Subject: [PATCH 317/921] chore: exclude pandas 2.2.x from testing (#391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Towards internal issue 322845272 🦕 --- noxfile.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 7cf9faf685f..259943aaa48 100644 --- a/noxfile.py +++ b/noxfile.py @@ -551,7 +551,11 @@ def prerelease(session: nox.sessions.Session, tests_path): # https://github.com/googleapis/python-bigquery-dataframes/issues/341 # https://github.com/googleapis/python-bigquery-dataframes/issues/337 # are resolved - "pandas!=2.1.4, !=2.2.0rc0, !=2.2.0", + # + # We exclude each version individually so that we can continue to test + # some prerelease packages. See: + # https://github.com/googleapis/python-bigquery-dataframes/pull/268#discussion_r1423205172 + "pandas!=2.1.4, !=2.2.0rc0, !=2.2.0, !=2.2.1", ) already_installed.add("pandas") From 469674d64f6ad5dac0f24ad450a7b8b6998fdf68 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Sat, 24 Feb 2024 05:56:16 +0000 Subject: [PATCH 318/921] feat: Enable regional endpoints for me-central2 (#386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 312304785 🦕 --- bigframes/_config/bigquery_options.py | 10 ++++----- bigframes/session/__init__.py | 2 +- bigframes/session/clients.py | 32 ++++++++++++++++++--------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 2875a11de37..74b83429d07 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -125,9 +125,8 @@ def use_regional_endpoints(self) -> bool: """Flag to connect to regional API endpoints. .. deprecated:: 0.13.0 - BigQuery regional endpoints is a feature in preview and - available only to selected projects. - Enable it only if your project has regional endpoints access. + Use of regional endpoints is a feature in preview and + available only in selected regions and projects. Requires ``location`` to also be set. For example, set ``location='asia-northeast1'`` and ``use_regional_endpoints=True`` to @@ -144,9 +143,8 @@ def use_regional_endpoints(self, value: bool): if value: warnings.warn( - "BigQuery regional endpoints is a feature in preview and " - "available only to selected projects. " - "Enable it only if your project has regional endpoints access." + "Use of regional endpoints is a feature in preview and " + "available only in selected regions and projects. " ) self._use_regional_endpoints = value diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 20dd39c0fa5..3aac1b5541c 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -152,7 +152,7 @@ def __init__( context = bigquery_options.BigQueryOptions() # TODO(swast): Get location from the environment. - if context is None or context.location is None: + if context.location is None: self._location = "US" warnings.warn( f"No explicit location is set, so using location {self._location} for the session.", diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index e33413002fe..627c9258a60 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -37,13 +37,21 @@ _APPLICATION_NAME = f"bigframes/{bigframes.version.__version__} ibis/{ibis.__version__}" _SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] +# Regions for which Regional Endpoints (REPs) are supported +_REP_SUPPORTED_REGIONS = {"me-central2"} + + # BigQuery is a REST API, which requires the protocol as part of the URL. -_BIGQUERY_REGIONAL_ENDPOINT = "https://{location}-bigquery.googleapis.com" +_BIGQUERY_LOCATIONAL_ENDPOINT = "https://{location}-bigquery.googleapis.com" +_BIGQUERY_REGIONAL_ENDPOINT = "https://bigquery.{location}.rep.googleapis.com" # BigQuery Connection and Storage are gRPC APIs, which don't support the # https:// protocol in the API endpoint URL. -_BIGQUERYCONNECTION_REGIONAL_ENDPOINT = "{location}-bigqueryconnection.googleapis.com" -_BIGQUERYSTORAGE_REGIONAL_ENDPOINT = "{location}-bigquerystorage.googleapis.com" +_BIGQUERYCONNECTION_LOCATIONAL_ENDPOINT = "{location}-bigqueryconnection.googleapis.com" +_BIGQUERYSTORAGE_LOCATIONAL_ENDPOINT = "{location}-bigquerystorage.googleapis.com" +_BIGQUERYSTORAGE_REGIONAL_ENDPOINT = ( + "https://bigquerystorage.{location}.rep.googleapis.com" +) def _get_default_credentials_with_project(): @@ -104,9 +112,11 @@ def bqclient(self): bq_options = None if self._use_regional_endpoints: bq_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERY_REGIONAL_ENDPOINT.format( - location=self._location - ), + api_endpoint=( + _BIGQUERY_REGIONAL_ENDPOINT + if self._location.lower() in _REP_SUPPORTED_REGIONS + else _BIGQUERY_LOCATIONAL_ENDPOINT + ).format(location=self._location), ) bq_info = google.api_core.client_info.ClientInfo( user_agent=self._application_name @@ -127,7 +137,7 @@ def bqconnectionclient(self): bqconnection_options = None if self._use_regional_endpoints: bqconnection_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERYCONNECTION_REGIONAL_ENDPOINT.format( + api_endpoint=_BIGQUERYCONNECTION_LOCATIONAL_ENDPOINT.format( location=self._location ) ) @@ -150,9 +160,11 @@ def bqstoragereadclient(self): bqstorage_options = None if self._use_regional_endpoints: bqstorage_options = google.api_core.client_options.ClientOptions( - api_endpoint=_BIGQUERYSTORAGE_REGIONAL_ENDPOINT.format( - location=self._location - ) + api_endpoint=( + _BIGQUERYSTORAGE_REGIONAL_ENDPOINT + if self._location.lower() in _REP_SUPPORTED_REGIONS + else _BIGQUERYSTORAGE_LOCATIONAL_ENDPOINT + ).format(location=self._location), ) bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name From a0490a492a43db24a314b3f42bfac61da7683151 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 26 Feb 2024 12:48:11 -0600 Subject: [PATCH 319/921] fix: avoid ibis warning for "database" table() method argument (#390) --- bigframes/session/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 3aac1b5541c..4bd205afea1 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -959,8 +959,8 @@ def _read_pandas_load_job( ) table_expression = self.ibis_client.table( # type: ignore load_table_destination.table_id, - # TODO: use "dataset_id" as the "schema" - database=f"{load_table_destination.project}.{load_table_destination.dataset_id}", + schema=load_table_destination.dataset_id, + database=load_table_destination.project, ) # b/297590178 Potentially a bug in bqclient.load_table_from_dataframe(), that only when the DF is empty, the index columns disappear in table_expression. @@ -1542,7 +1542,9 @@ def _cache_with_cluster_cols( ibis_expr, cluster_cols=cluster_cols, api_name="cached" ) table_expression = self.ibis_client.table( - f"{tmp_table.project}.{tmp_table.dataset_id}.{tmp_table.table_id}" + tmp_table.table_id, + schema=tmp_table.dataset_id, + database=tmp_table.project, ) new_columns = [table_expression[column] for column in compiled_value.column_ids] new_hidden_columns = [ @@ -1571,7 +1573,9 @@ def _cache_with_offsets(self, array_value: core.ArrayValue) -> core.ArrayValue: ibis_expr, cluster_cols=["bigframes_offsets"], api_name="cached" ) table_expression = self.ibis_client.table( - f"{tmp_table.project}.{tmp_table.dataset_id}.{tmp_table.table_id}" + tmp_table.table_id, + schema=tmp_table.dataset_id, + database=tmp_table.project, ) new_columns = [table_expression[column] for column in compiled_value.column_ids] new_hidden_columns = [table_expression["bigframes_offsets"]] From 81ece463b69765b0f93585d6b866fb642ddc65dc Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:36:16 -0800 Subject: [PATCH 320/921] fix!: rename cosine_similarity to paired_cosine_distances (#393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/metrics/pairwise.py | 8 +++++--- tests/system/small/ml/test_metrics_pairwise.py | 6 +++--- .../bigframes_vendored/sklearn/metrics/pairwise.py | 11 +++-------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/bigframes/ml/metrics/pairwise.py b/bigframes/ml/metrics/pairwise.py index 04577c89d35..35b64c7850b 100644 --- a/bigframes/ml/metrics/pairwise.py +++ b/bigframes/ml/metrics/pairwise.py @@ -20,7 +20,7 @@ import third_party.bigframes_vendored.sklearn.metrics.pairwise as vendored_metrics_pairwise -def cosine_similarity( +def paired_cosine_distances( X: Union[bpd.DataFrame, bpd.Series], Y: Union[bpd.DataFrame, bpd.Series] ) -> bpd.DataFrame: X, Y = utils.convert_to_dataframe(X, Y) @@ -28,7 +28,9 @@ def cosine_similarity( raise ValueError("Inputs X and Y can only contain 1 column.") base_bqml = core.BaseBqml(session=X._session) - return base_bqml.distance(X, Y, type="COSINE", name="cosine_similarity") + return base_bqml.distance(X, Y, type="COSINE", name="cosine_distance") -cosine_similarity.__doc__ = inspect.getdoc(vendored_metrics_pairwise.cosine_similarity) +paired_cosine_distances.__doc__ = inspect.getdoc( + vendored_metrics_pairwise.paired_cosine_distances +) diff --git a/tests/system/small/ml/test_metrics_pairwise.py b/tests/system/small/ml/test_metrics_pairwise.py index c02a36abbc3..47bd1e18d05 100644 --- a/tests/system/small/ml/test_metrics_pairwise.py +++ b/tests/system/small/ml/test_metrics_pairwise.py @@ -19,15 +19,15 @@ import bigframes.pandas as bpd -def test_cosine_similarity(): +def test_paired_cosine_distances(): x_col = [np.array([4.1, 0.5, 1.0])] y_col = [np.array([3.0, 0.0, 2.5])] X = bpd.read_pandas(pd.DataFrame({"X": x_col})) Y = bpd.read_pandas(pd.DataFrame({"Y": y_col})) - result = metrics.pairwise.cosine_similarity(X, Y) + result = metrics.pairwise.paired_cosine_distances(X, Y) expected_pd_df = pd.DataFrame( - {"X": x_col, "Y": y_col, "cosine_similarity": [0.108199]} + {"X": x_col, "Y": y_col, "cosine_distance": [0.108199]} ) pd.testing.assert_frame_equal( diff --git a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py index 3ef54311787..c309b08d889 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py +++ b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py @@ -11,13 +11,8 @@ import bigframes.pandas as bpd -def cosine_similarity(X, Y) -> bpd.DataFrame: - """Compute cosine similarity between samples in X and Y. - - Cosine similarity, or the cosine kernel, computes similarity as the - normalized dot product of X and Y: - - K(X, Y) = / (||X||*||Y||) +def paired_cosine_distances(X, Y) -> bpd.DataFrame: + """Compute the paired cosine distances between X and Y. Args: X (Series or single column DataFrame of array of numeric type): @@ -26,6 +21,6 @@ def cosine_similarity(X, Y) -> bpd.DataFrame: Input data. X and Y are mapped by indexes, must have the same index. Returns: - bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and cosine_similarity + bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and cosine_distance """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 9d318653c001287bcc8ae9d8e09d0187413cbed6 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 27 Feb 2024 07:30:58 -0800 Subject: [PATCH 321/921] feat: Add ml.metrics.pairwise.manhattan_distance (#392) --- bigframes/ml/metrics/pairwise.py | 16 ++++++++++++++++ tests/system/small/ml/test_metrics_pairwise.py | 14 ++++++++++++++ .../sklearn/metrics/pairwise.py | 15 +++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/bigframes/ml/metrics/pairwise.py b/bigframes/ml/metrics/pairwise.py index 35b64c7850b..9ebea4ef42d 100644 --- a/bigframes/ml/metrics/pairwise.py +++ b/bigframes/ml/metrics/pairwise.py @@ -34,3 +34,19 @@ def paired_cosine_distances( paired_cosine_distances.__doc__ = inspect.getdoc( vendored_metrics_pairwise.paired_cosine_distances ) + + +def paired_manhattan_distance( + X: Union[bpd.DataFrame, bpd.Series], Y: Union[bpd.DataFrame, bpd.Series] +) -> bpd.DataFrame: + X, Y = utils.convert_to_dataframe(X, Y) + if len(X.columns) != 1 or len(Y.columns) != 1: + raise ValueError("Inputs X and Y can only contain 1 column.") + + base_bqml = core.BaseBqml(session=X._session) + return base_bqml.distance(X, Y, type="MANHATTAN", name="manhattan_distance") + + +paired_manhattan_distance.__doc__ = inspect.getdoc( + vendored_metrics_pairwise.paired_manhattan_distance +) diff --git a/tests/system/small/ml/test_metrics_pairwise.py b/tests/system/small/ml/test_metrics_pairwise.py index 47bd1e18d05..e2aee971eef 100644 --- a/tests/system/small/ml/test_metrics_pairwise.py +++ b/tests/system/small/ml/test_metrics_pairwise.py @@ -33,3 +33,17 @@ def test_paired_cosine_distances(): pd.testing.assert_frame_equal( result.to_pandas(), expected_pd_df, check_dtype=False, check_index_type=False ) + + +def test_paired_manhattan_distance(): + x_col = [np.array([4.1, 0.5, 1.0])] + y_col = [np.array([3.0, 0.0, 2.5])] + X = bpd.read_pandas(pd.DataFrame({"X": x_col})) + Y = bpd.read_pandas(pd.DataFrame({"Y": y_col})) + + result = metrics.pairwise.paired_manhattan_distance(X, Y) + expected_pd_df = pd.DataFrame({"X": x_col, "Y": y_col, "manhattan_distance": [3.1]}) + + pd.testing.assert_frame_equal( + result.to_pandas(), expected_pd_df, check_dtype=False, check_index_type=False + ) diff --git a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py index c309b08d889..5791d850ffd 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py +++ b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py @@ -24,3 +24,18 @@ def paired_cosine_distances(X, Y) -> bpd.DataFrame: bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and cosine_distance """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +def paired_manhattan_distance(X, Y) -> bpd.DataFrame: + """Compute the L1 distances between the vectors in X and Y. + + Args: + X (Series or single column DataFrame of array of numeric type): + Input data. + Y (Series or single column DataFrame of array of numeric type): + Input data. X and Y are mapped by indexes, must have the same index. + + Returns: + bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and manhattan_distance + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 8f73d9e37827ecdc90683313000364922ae61dab Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:34:16 -0800 Subject: [PATCH 322/921] chore: release 0.22.0 (#396) Release-As: 0.22.0 From 4291d656f30dc50b8ffcdd10ccbfa7f327711100 Mon Sep 17 00:00:00 2001 From: Salem Jorden <115185670+SalemJorden@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:14:00 -0600 Subject: [PATCH 323/921] docs: add a code sample for creating a kmeans model (#267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * k-means code sample * formatting * added test * docs: add code sampke for creating kmeans model * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * license header + region tags added * Update samples/snippets/create_kmeans_model_test.py Co-authored-by: Tim Swast * code corrections resolved * code corrections commit 1 * descriptions of geospatial analysis functions * explantions revised for clarity * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Update samples/snippets/create_kmeans_model_test.py Co-authored-by: Tim Swast * code corrections * code revision * code changes * revisions * expected output previews * revisions * tests passing, expected output characters >80 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * column wrapping * reset session before running code smaples * Update samples/snippets/create_kmeans_model_test.py Co-authored-by: Tim Swast * predict function added to tutorial * replaced project_id with model_id * reformatting * reformat --------- Co-authored-by: Salem Boyland Co-authored-by: Owl Bot Co-authored-by: Tim Swast --- samples/snippets/conftest.py | 37 +++++ samples/snippets/create_kmeans_model_test.py | 152 +++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 samples/snippets/create_kmeans_model_test.py diff --git a/samples/snippets/conftest.py b/samples/snippets/conftest.py index 1ce54b3c0c8..d34837b3e28 100644 --- a/samples/snippets/conftest.py +++ b/samples/snippets/conftest.py @@ -18,6 +18,8 @@ import pytest import test_utils.prefixer +import bigframes.pandas as bpd + prefixer = test_utils.prefixer.Prefixer( "python-bigquery-dataframes", "samples/snippets" ) @@ -43,6 +45,16 @@ def project_id(bigquery_client: bigquery.Client) -> str: return bigquery_client.project +@pytest.fixture(autouse=True) +def reset_session(): + """An autouse fixture ensuring each sample runs in a fresh session. + + This allows us to have samples that query data in different locations. + """ + bpd.reset_session() + bpd.options.bigquery.location = None + + @pytest.fixture(scope="session") def dataset_id(bigquery_client: bigquery.Client, project_id: str) -> Iterator[str]: dataset_id = prefixer.create_prefix() @@ -53,6 +65,17 @@ def dataset_id(bigquery_client: bigquery.Client, project_id: str) -> Iterator[st bigquery_client.delete_dataset(dataset, delete_contents=True, not_found_ok=True) +@pytest.fixture(scope="session") +def dataset_id_eu(bigquery_client: bigquery.Client, project_id: str) -> Iterator[str]: + dataset_id = prefixer.create_prefix() + full_dataset_id = f"{project_id}.{dataset_id}" + dataset = bigquery.Dataset(full_dataset_id) + dataset.location = "EU" + bigquery_client.create_dataset(dataset) + yield dataset_id + bigquery_client.delete_dataset(dataset, delete_contents=True, not_found_ok=True) + + @pytest.fixture def random_model_id( bigquery_client: bigquery.Client, project_id: str, dataset_id: str @@ -64,3 +87,17 @@ def random_model_id( full_model_id = f"{project_id}.{dataset_id}.{random_model_id}" yield full_model_id bigquery_client.delete_model(full_model_id, not_found_ok=True) + + +@pytest.fixture +def random_model_id_eu( + bigquery_client: bigquery.Client, project_id: str, dataset_id_eu: str +) -> Iterator[str]: + """ + Create a new table ID each time, so random_model_id_eu can be used + as a target for load jobs. + """ + random_model_id_eu = prefixer.create_prefix() + full_model_id = f"{project_id}.{dataset_id_eu}.{random_model_id_eu}" + yield full_model_id + bigquery_client.delete_model(full_model_id, not_found_ok=True) diff --git a/samples/snippets/create_kmeans_model_test.py b/samples/snippets/create_kmeans_model_test.py new file mode 100644 index 00000000000..2429060d09f --- /dev/null +++ b/samples/snippets/create_kmeans_model_test.py @@ -0,0 +1,152 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_kmeans_sample(project_id: str, random_model_id_eu: str): + your_gcp_project_id = project_id + your_model_id = random_model_id_eu + # [START bigquery_dataframes_bqml_kmeans] + import datetime + + import bigframes + import bigframes.pandas as bpd + + bigframes.options.bigquery.project = your_gcp_project_id + # Compute in the EU multi-region to query the London bicycles dataset. + bigframes.options.bigquery.location = "EU" + + # Extract the information you'll need to train the k-means model in this + # tutorial. Use the read_gbq function to represent cycle hires + # data as a DataFrame. + h = bpd.read_gbq( + "bigquery-public-data.london_bicycles.cycle_hire", + col_order=["start_station_name", "start_station_id", "start_date", "duration"], + ).rename( + columns={ + "start_station_name": "station_name", + "start_station_id": "station_id", + } + ) + + s = bpd.read_gbq( + # Use ST_GEOPOINT and ST_DISTANCE to analyze geographical + # data. These functions determine spatial relationships between + # geographical features. + """ + SELECT + id, + ST_DISTANCE( + ST_GEOGPOINT(s.longitude, s.latitude), + ST_GEOGPOINT(-0.1, 51.5) + ) / 1000 AS distance_from_city_center + FROM + `bigquery-public-data.london_bicycles.cycle_stations` s + """ + ) + + # Define Python datetime objects in the UTC timezone for range comparison, + # because BigQuery stores timestamp data in the UTC timezone. + sample_time = datetime.datetime(2015, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + sample_time2 = datetime.datetime(2016, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + + h = h.loc[(h["start_date"] >= sample_time) & (h["start_date"] <= sample_time2)] + + # Replace each day-of-the-week number with the corresponding "weekday" or + # "weekend" label by using the Series.map method. + h = h.assign( + isweekday=h.start_date.dt.dayofweek.map( + { + 0: "weekday", + 1: "weekday", + 2: "weekday", + 3: "weekday", + 4: "weekday", + 5: "weekend", + 6: "weekend", + } + ) + ) + + # Supplement each trip in "h" with the station distance information from + # "s" by merging the two DataFrames by station ID. + merged_df = h.merge( + right=s, + how="inner", + left_on="station_id", + right_on="id", + ) + + # Engineer features to cluster the stations. For each station, find the + # average trip duration, number of trips, and distance from city center. + stationstats = merged_df.groupby(["station_name", "isweekday"]).agg( + {"duration": ["mean", "count"], "distance_from_city_center": "max"} + ) + stationstats.columns = ["duration", "num_trips", "distance_from_city_center"] + stationstats = stationstats.sort_values( + by="distance_from_city_center", ascending=True + ).reset_index() + + # Expected output results: >>> stationstats.head(3) + # station_name isweekday duration num_trips distance_from_city_center + # Borough Road... weekday 1110 5749 0.12624 + # Borough Road... weekend 2125 1774 0.12624 + # Webber Street... weekday 795 6517 0.164021 + # 3 rows × 5 columns + + # [END bigquery_dataframes_bqml_kmeans] + + # [START bigquery_dataframes_bqml_kmeans_fit] + + from bigframes.ml.cluster import KMeans + + # To determine an optimal number of clusters, construct and fit several + # K-Means objects with different values of num_clusters, find the error + # measure, and pick the point at which the error measure is at its minimum + # value. + cluster_model = KMeans(n_clusters=4) + cluster_model.fit(stationstats) + cluster_model.to_gbq( + your_model_id, # For example: "bqml_tutorial.london_station_clusters" + replace=True, + ) + # [END bigquery_dataframes_bqml_kmeans_fit] + + # [START bigquery_dataframes_bqml_kmeans_predict] + + # Select model you'll use for predictions. `read_gbq_model` loads model + # data from BigQuery, but you could also use the `cluster_model` object + # from previous steps. + cluster_model = bpd.read_gbq_model( + your_model_id, + # For example: "bqml_tutorial.london_station_clusters", + ) + + # Use 'contains' function to filter by stations containing the string + # "Kennington". + stationstats = stationstats.loc[ + stationstats["station_name"].str.contains("Kennington") + ] + + result = cluster_model.predict(stationstats) + + # Expected output results: >>>results.peek(3) + # CENTROID... NEAREST... station_name isweekday duration num_trips dist... + # 1 [{'CENTROID_ID'... Borough... weekday 1110 5749 0.13 + # 2 [{'CENTROID_ID'... Borough... weekend 2125 1774 0.13 + # 1 [{'CENTROID_ID'... Webber... weekday 795 6517 0.16 + # 3 rows × 7 columns + + # [END bigquery_dataframes_bqml_kmeans_predict] + + assert result is not None From 20de7f4428aac798049e52fc893458ea143fc6e8 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:00:39 -0800 Subject: [PATCH 324/921] chore(main): release 0.22.0 (#380) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 47 ++++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0091fb4a114..d8e11d47e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,53 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.22.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.21.0...v0.22.0) (2024-02-27) + + +### ⚠ BREAKING CHANGES + +* rename cosine_similarity to paired_cosine_distances ([#393](https://github.com/googleapis/python-bigquery-dataframes/issues/393)) +* move model optional args to kwargs ([#381](https://github.com/googleapis/python-bigquery-dataframes/issues/381)) + +### Features + +* Add `DataFrames.corr()` method ([#379](https://github.com/googleapis/python-bigquery-dataframes/issues/379)) ([67fd434](https://github.com/googleapis/python-bigquery-dataframes/commit/67fd434bbb1c73f9013f65252d1ecc8da79542f6)) +* Add ml.metrics.pairwise.manhattan_distance ([#392](https://github.com/googleapis/python-bigquery-dataframes/issues/392)) ([9d31865](https://github.com/googleapis/python-bigquery-dataframes/commit/9d318653c001287bcc8ae9d8e09d0187413cbed6)) +* Enable regional endpoints for me-central2 ([#386](https://github.com/googleapis/python-bigquery-dataframes/issues/386)) ([469674d](https://github.com/googleapis/python-bigquery-dataframes/commit/469674d64f6ad5dac0f24ad450a7b8b6998fdf68)) + + +### Bug Fixes + +* Avoid ibis warning for "database" table() method argument ([#390](https://github.com/googleapis/python-bigquery-dataframes/issues/390)) ([a0490a4](https://github.com/googleapis/python-bigquery-dataframes/commit/a0490a492a43db24a314b3f42bfac61da7683151)) +* Correct the numeric literal dtype ([#365](https://github.com/googleapis/python-bigquery-dataframes/issues/365)) ([93b02cd](https://github.com/googleapis/python-bigquery-dataframes/commit/93b02cd8bc620823563f8214b43bc5f2f35c155b)) +* Rename cosine_similarity to paired_cosine_distances ([#393](https://github.com/googleapis/python-bigquery-dataframes/issues/393)) ([81ece46](https://github.com/googleapis/python-bigquery-dataframes/commit/81ece463b69765b0f93585d6b866fb642ddc65dc)) + + +### Performance Improvements + +* Inline read_pandas for small data ([#383](https://github.com/googleapis/python-bigquery-dataframes/issues/383)) ([59b446b](https://github.com/googleapis/python-bigquery-dataframes/commit/59b446bad8d2c5fca791c384616cfa7e54d54c09)) + + +### Dependencies + +* Add minimum version constraint for sqlglot to 19.9.0 ([#389](https://github.com/googleapis/python-bigquery-dataframes/issues/389)) ([8b62d77](https://github.com/googleapis/python-bigquery-dataframes/commit/8b62d77d8274cff2842c98b032bf98d69c483482)) + + +### Documentation + +* Add a code sample for creating a kmeans model ([#267](https://github.com/googleapis/python-bigquery-dataframes/issues/267)) ([4291d65](https://github.com/googleapis/python-bigquery-dataframes/commit/4291d656f30dc50b8ffcdd10ccbfa7f327711100)) +* Fix `bigframes.pandas.concat` documentation ([#382](https://github.com/googleapis/python-bigquery-dataframes/issues/382)) ([234b61c](https://github.com/googleapis/python-bigquery-dataframes/commit/234b61cdfe75b402adf1b56f53b5f06934777f95)) + + +### Miscellaneous Chores + +* Release 0.22.0 ([#396](https://github.com/googleapis/python-bigquery-dataframes/issues/396)) ([8f73d9e](https://github.com/googleapis/python-bigquery-dataframes/commit/8f73d9e37827ecdc90683313000364922ae61dab)) + + +### Code Refactoring + +* Move model optional args to kwargs ([#381](https://github.com/googleapis/python-bigquery-dataframes/issues/381)) ([4037992](https://github.com/googleapis/python-bigquery-dataframes/commit/4037992b61ff352320d5dfb87dcf5f274791ace1)) + ## [0.21.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.20.1...v0.21.0) (2024-02-13) diff --git a/bigframes/version.py b/bigframes/version.py index a713192ada0..387b7663f25 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.21.0" +__version__ = "0.22.0" From 350499bccb62e22169ab2f2e1400175b2179ef85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=2C=20formerly=29?= Date: Wed, 28 Feb 2024 10:34:16 -0600 Subject: [PATCH 325/921] deps: update ibis to version 8.0.0 and refactor `remote_function` to use ibis UDF method (#277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Follow-up to https://togithub.com/googleapis/python-bigquery-dataframes/pull/53#discussion_r1427224630 🦕 --- bigframes/core/compile/aggregate_compiler.py | 2 +- bigframes/core/compile/compiled.py | 15 +- bigframes/dtypes.py | 4 +- bigframes/functions/remote_function.py | 71 +++---- noxfile.py | 6 +- setup.py | 5 +- testing/constraints-3.9.txt | 4 +- tests/system/small/test_dataframe.py | 10 +- tests/unit/test_core.py | 2 +- tests/unit/test_remote_function.py | 4 +- .../ibis/backends/bigquery/datatypes.py | 176 ++++++++++++++++++ 11 files changed, 230 insertions(+), 69 deletions(-) create mode 100644 third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 1dad128599c..86ba16e347a 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -331,7 +331,7 @@ def _( op: agg_ops.RankOp, column: ibis_types.Column, window=None ) -> ibis_types.IntegerValue: # Ibis produces 0-based ranks, while pandas creates 1-based ranks - return _apply_window_if_present(column.rank(), window) + 1 + return _apply_window_if_present(ibis.rank(), window) + 1 @compile_unary_agg.register diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 969437939fa..7245689aae2 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -1099,17 +1099,14 @@ def _to_ibis_expr( if not columns: return ibis.memtable([]) + # Make sure we don't have any unbound (deferred) columns. + table = self._table.select(columns) + # Make sure all dtypes are the "canonical" ones for BigFrames. This is # important for operations like UNION where the schema must match. - table = self._table.select( - bigframes.dtypes.ibis_value_to_canonical_type( - column.resolve(self._table) - # TODO(https://github.com/ibis-project/ibis/issues/7613): use - # public API to refer to Deferred type. - if isinstance(column, ibis.common.deferred.Deferred) - else column - ) - for column in columns + table = table.select( + bigframes.dtypes.ibis_value_to_canonical_type(table[column]) + for column in table.columns ) base_table = table if self._reduced_predicate is not None: diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 6e3bc25c477..8a2055ef7f6 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -23,7 +23,6 @@ import geopandas as gpd # type: ignore import google.cloud.bigquery as bigquery import ibis -from ibis.backends.bigquery.datatypes import BigQueryType import ibis.expr.datatypes as ibis_dtypes from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type import ibis.expr.types as ibis_types @@ -33,6 +32,7 @@ import bigframes.constants as constants import third_party.bigframes_vendored.google_cloud_bigquery._pandas_helpers as gcb3p_pandas_helpers +import third_party.bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops # Type hints for Pandas dtypes supported by BigQuery DataFrame @@ -643,4 +643,4 @@ def ibis_type_from_python_type(t: type) -> ibis_dtypes.DataType: def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> ibis_dtypes.DataType: if tk not in SUPPORTED_IO_BIGQUERY_TYPEKINDS: raise UnsupportedTypeError(tk, SUPPORTED_IO_BIGQUERY_TYPEKINDS) - return BigQueryType.to_ibis(tk) + return third_party_ibis_bqtypes.BigQueryType.to_ibis(tk) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index c7bb5d92c69..af4c4b138ac 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -14,7 +14,6 @@ from __future__ import annotations -import functools import hashlib import inspect import logging @@ -28,6 +27,7 @@ import textwrap from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING +import ibis import requests if TYPE_CHECKING: @@ -43,15 +43,12 @@ resourcemanager_v3, ) import google.iam.v1 -from ibis.backends.bigquery.compiler import compiles -from ibis.backends.bigquery.datatypes import BigQueryType from ibis.expr.datatypes.core import DataType as IbisDataType -import ibis.expr.operations as ops -import ibis.expr.rules as rlz from bigframes import clients import bigframes.constants as constants import bigframes.dtypes +import third_party.bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes logger = logging.getLogger(__name__) @@ -173,12 +170,14 @@ def create_bq_remote_function( # Create BQ function # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 bq_function_args = [] - bq_function_return_type = BigQueryType.from_ibis(output_type) + bq_function_return_type = third_party_ibis_bqtypes.BigQueryType.from_ibis( + output_type + ) # We are expecting the input type annotations to be 1:1 with the input args for idx, name in enumerate(input_args): bq_function_args.append( - f"{name} {BigQueryType.from_ibis(input_types[idx])}" + f"{name} {third_party_ibis_bqtypes.BigQueryType.from_ibis(input_types[idx])}" ) create_function_ddl = f""" CREATE OR REPLACE FUNCTION `{self._gcp_project_id}.{self._bq_dataset}`.{bq_function_name}({','.join(bq_function_args)}) @@ -515,33 +514,10 @@ def get_remote_function_specs(self, remote_function_name): return (http_endpoint, bq_connection) -def remote_function_node( - routine_ref: bigquery.RoutineReference, ibis_signature: IbisSignature -): - """Creates an Ibis node representing a remote function call.""" - - fields = { - name: rlz.ValueOf(None if type_ == "ANY TYPE" else type_) - for name, type_ in zip( - ibis_signature.parameter_names, ibis_signature.input_types - ) - } - - fields["dtype"] = ibis_signature.output_type # type: ignore - fields["shape"] = rlz.shape_like("args") - - node = type(routine_ref_to_string_for_query(routine_ref), (ops.ValueOp,), fields) # type: ignore - - @compiles(node) - def compile_node(t, op): - return "{}({})".format(node.__name__, ", ".join(map(t.translate, op.args))) - - def f(*args, **kwargs): - return node(*args, **kwargs).to_expr() - - f.bigframes_remote_function = str(routine_ref) # type: ignore - - return f +class UnsupportedTypeError(ValueError): + def __init__(self, type_, supported_types): + self.type = type_ + self.supported_types = supported_types def ibis_signature_from_python_signature( @@ -831,14 +807,16 @@ def wrapper(f): packages, ) - node = remote_function_node(dataset_ref.routine(rf_name), ibis_signature) - - node = functools.wraps(f)(node) - node.__signature__ = signature + node = ibis.udf.scalar.builtin( + f, + name=rf_name, + schema=f"{dataset_ref.project}.{dataset_ref.dataset_id}", + signature=(ibis_signature.input_types, ibis_signature.output_type), + ) node.bigframes_cloud_function = ( remote_function_client.get_cloud_function_fully_qualified_name(cf_name) ) - + node.bigframes_remote_function = str(dataset_ref.routine(rf_name)) # type: ignore return node return wrapper @@ -888,4 +866,17 @@ def read_gbq_function( f"{constants.FEEDBACK_LINK}" ) - return remote_function_node(routine_ref, ibis_signature) + # The name "args" conflicts with the Ibis operator, so we use + # non-standard names for the arguments here. + def node(*ignored_args, **ignored_kwargs): + f"""Remote function {str(routine_ref)}.""" + + node.__name__ = routine_ref.routine_id + node = ibis.udf.scalar.builtin( + node, + name=routine_ref.routine_id, + schema=f"{routine_ref.project}.{routine_ref.dataset_id}", + signature=(ibis_signature.input_types, ibis_signature.output_type), + ) + node.bigframes_remote_function = str(routine_ref) # type: ignore + return node diff --git a/noxfile.py b/noxfile.py index 259943aaa48..91d26cf6954 100644 --- a/noxfile.py +++ b/noxfile.py @@ -565,12 +565,12 @@ def prerelease(session: nox.sessions.Session, tests_path): # session.install( # "--upgrade", # "-e", # Use -e so that py.typed file is included. - # "git+https://github.com/ibis-project/ibis.git@7.x.x#egg=ibis-framework", + # "git+https://github.com/ibis-project/ibis.git#egg=ibis-framework", # ) session.install( "--upgrade", - # "--pre", - "ibis-framework>=7.1.0,<7.2.0dev", + "--pre", + "ibis-framework>=8.0.0,<9.0.0dev", ) already_installed.add("ibis-framework") diff --git a/setup.py b/setup.py index 4aa07904f77..516d5b8a19f 100644 --- a/setup.py +++ b/setup.py @@ -44,8 +44,7 @@ "google-cloud-iam >=2.12.1", "google-cloud-resource-manager >=1.10.3", "google-cloud-storage >=2.0.0", - # TODO: Relax upper bound once we have fixed unit tests with 7.2.0. - "ibis-framework[bigquery] >=7.1.0,<7.2.0dev", + "ibis-framework[bigquery] >=8.0.0,<9.0.0dev", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. "pandas >=1.5.0,<2.1.4", "pydata-google-auth >=1.8.2", @@ -55,7 +54,7 @@ # Keep sqlglot versions in sync with ibis-framework. This avoids problems # where the incorrect version of sqlglot is installed, such as # https://github.com/googleapis/python-bigquery-dataframes/issues/315 - "sqlglot >=19.9.0,<20", + "sqlglot >=20.8.0,<=20.11", "tabulate >= 0.9", "ipywidgets >=7.7.1", "humanize >= 4.6.0", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 42cc68eb045..c4fed64fbd5 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -10,13 +10,13 @@ google-cloud-bigquery-connection==1.12.0 google-cloud-iam==2.12.1 google-cloud-resource-manager==1.10.3 google-cloud-storage==2.0.0 -ibis-framework==7.1.0 +ibis-framework==8.0.0 pandas==1.5.0 pydata-google-auth==1.8.2 requests==2.27.1 scikit-learn==1.2.2 sqlalchemy==1.4 -sqlglot==19.9.0 +sqlglot==20.8.0 tabulate==0.9 ipywidgets==7.7.1 humanize==4.6.0 diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 8f75534fc61..9f4e138b736 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -157,15 +157,13 @@ def test_tail_with_custom_column_labels(scalars_df_index, scalars_pandas_df_inde ], ) def test_df_nlargest(scalars_df_index, scalars_pandas_df_index, keep): - bf_result = scalars_df_index.nlargest( - 3, ["bool_col", "int64_too"], keep=keep - ).to_pandas() + bf_result = scalars_df_index.nlargest(3, ["bool_col", "int64_too"], keep=keep) pd_result = scalars_pandas_df_index.nlargest( 3, ["bool_col", "int64_too"], keep=keep ) pd.testing.assert_frame_equal( - bf_result, + bf_result.to_pandas(), pd_result, ) @@ -179,11 +177,11 @@ def test_df_nlargest(scalars_df_index, scalars_pandas_df_index, keep): ], ) def test_df_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): - bf_result = scalars_df_index.nsmallest(6, ["bool_col"], keep=keep).to_pandas() + bf_result = scalars_df_index.nsmallest(6, ["bool_col"], keep=keep) pd_result = scalars_pandas_df_index.nsmallest(6, ["bool_col"], keep=keep) pd.testing.assert_frame_equal( - bf_result, + bf_result.to_pandas(), pd_result, ) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 42cbcbbc9f9..5f940fd7a51 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -118,7 +118,7 @@ def test_arrayvalues_to_ibis_expr_with_concat(): total_ordering_columns=["col1"], ) expr = value.concat([value]) - actual = expr._compile_ordered()._to_ibis_expr(ordering_mode="unordered") + actual = expr._compile_unordered()._to_ibis_expr() assert len(actual.columns) == 3 # TODO(ashleyxu, b/299631930): test out the union expression assert actual.columns[0] == "column_0" diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py index 392872a7bea..629bc5326a1 100644 --- a/tests/unit/test_remote_function.py +++ b/tests/unit/test_remote_function.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ibis.backends.bigquery import datatypes as bq_types from ibis.expr import datatypes as ibis_types import bigframes.dtypes +import third_party.bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes def test_supported_types_correspond(): @@ -24,7 +24,7 @@ def test_supported_types_correspond(): ibis_types.dtype(t) for t in bigframes.dtypes.SUPPORTED_IO_PYTHON_TYPES } ibis_types_from_bigquery = { - bq_types.BigQueryType.to_ibis(tk) + third_party_ibis_bqtypes.BigQueryType.to_ibis(tk) for tk in bigframes.dtypes.SUPPORTED_IO_BIGQUERY_TYPEKINDS } diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py b/third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py new file mode 100644 index 00000000000..e7200cbf2a6 --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py @@ -0,0 +1,176 @@ +# Contains code from +# https://github.com/ibis-project/ibis/blob/697d325f13bdf2746a50e86204eb8834b1710bd6/ibis/backends/bigquery/datatypes.py + +from __future__ import annotations + +import google.cloud.bigquery as bq +import ibis +import ibis.expr.datatypes as dt +import ibis.expr.schema as sch +from ibis.formats import SchemaMapper, TypeMapper +import sqlglot as sg + +_from_bigquery_types = { + "INT64": dt.Int64, + "INTEGER": dt.Int64, + "FLOAT": dt.Float64, + "FLOAT64": dt.Float64, + "BOOL": dt.Boolean, + "BOOLEAN": dt.Boolean, + "STRING": dt.String, + "DATE": dt.Date, + "TIME": dt.Time, + "BYTES": dt.Binary, + "JSON": dt.JSON, +} + + +class BigQueryType(TypeMapper): + @classmethod + def to_ibis(cls, typ: str, nullable: bool = True) -> dt.DataType: + if typ == "DATETIME": + return dt.Timestamp(timezone=None, nullable=nullable) + elif typ == "TIMESTAMP": + return dt.Timestamp(timezone="UTC", nullable=nullable) + elif typ == "NUMERIC": + return dt.Decimal(38, 9, nullable=nullable) + elif typ == "BIGNUMERIC": + return dt.Decimal(76, 38, nullable=nullable) + elif typ == "GEOGRAPHY": + return dt.GeoSpatial(geotype="geography", srid=4326, nullable=nullable) + else: + try: + return _from_bigquery_types[typ](nullable=nullable) + except KeyError: + raise TypeError(f"Unable to convert BigQuery type to ibis: {typ}") + + @classmethod + def from_ibis(cls, dtype: dt.DataType) -> str: + if dtype.is_floating(): + return "FLOAT64" + elif dtype.is_uint64(): + raise TypeError( + "Conversion from uint64 to BigQuery integer type (int64) is lossy" + ) + elif dtype.is_integer(): + return "INT64" + elif dtype.is_binary(): + return "BYTES" + elif dtype.is_date(): + return "DATE" + elif dtype.is_timestamp(): + if dtype.timezone is None: + return "DATETIME" + elif dtype.timezone == "UTC": + return "TIMESTAMP" + else: + raise TypeError( + "BigQuery does not support timestamps with timezones other than 'UTC'" + ) + elif dtype.is_decimal(): + if (dtype.precision, dtype.scale) == (76, 38): + return "BIGNUMERIC" + if (dtype.precision, dtype.scale) in [(38, 9), (None, None)]: + return "NUMERIC" + raise TypeError( + "BigQuery only supports decimal types with precision of 38 and " + f"scale of 9 (NUMERIC) or precision of 76 and scale of 38 (BIGNUMERIC). " + f"Current precision: {dtype.precision}. Current scale: {dtype.scale}" + ) + elif dtype.is_array(): + return f"ARRAY<{cls.from_ibis(dtype.value_type)}>" + elif dtype.is_struct(): + fields = ( + f"{sg.to_identifier(k).sql('bigquery')} {cls.from_ibis(v)}" + for k, v in dtype.fields.items() + ) + return "STRUCT<{}>".format(", ".join(fields)) + elif dtype.is_json(): + return "JSON" + elif dtype.is_geospatial(): + if (dtype.geotype, dtype.srid) == ("geography", 4326): + return "GEOGRAPHY" + raise TypeError( + "BigQuery geography uses points on WGS84 reference ellipsoid." + f"Current geotype: {dtype.geotype}, Current srid: {dtype.srid}" + ) + elif dtype.is_map(): + raise NotImplementedError("Maps are not supported in BigQuery") + else: + return str(dtype).upper() + + +class BigQuerySchema(SchemaMapper): + @classmethod + def from_ibis(cls, schema: sch.Schema) -> list[bq.SchemaField]: + schema_fields = [] + + for name, typ in ibis.schema(schema).items(): + if typ.is_array(): + value_type = typ.value_type + if value_type.is_array(): + raise TypeError("Nested arrays are not supported in BigQuery") + + is_struct = value_type.is_struct() + + field_type = ( + "RECORD" if is_struct else BigQueryType.from_ibis(typ.value_type) + ) + mode = "REPEATED" + fields = cls.from_ibis(ibis.schema(getattr(value_type, "fields", {}))) + elif typ.is_struct(): + field_type = "RECORD" + mode = "NULLABLE" if typ.nullable else "REQUIRED" + fields = cls.from_ibis(ibis.schema(typ.fields)) + else: + field_type = BigQueryType.from_ibis(typ) + mode = "NULLABLE" if typ.nullable else "REQUIRED" + fields = [] + + schema_fields.append( + bq.SchemaField(name, field_type=field_type, mode=mode, fields=fields) + ) + return schema_fields + + @classmethod + def _dtype_from_bigquery_field(cls, field: bq.SchemaField) -> dt.DataType: + typ = field.field_type + if typ == "RECORD": + assert field.fields, "RECORD fields are empty" + fields = {f.name: cls._dtype_from_bigquery_field(f) for f in field.fields} + dtype = dt.Struct(fields) + else: + dtype = BigQueryType.to_ibis(typ) + + mode = field.mode + if mode == "NULLABLE": + return dtype.copy(nullable=True) + elif mode == "REQUIRED": + return dtype.copy(nullable=False) + elif mode == "REPEATED": + # arrays with NULL elements aren't supported + return dt.Array(dtype.copy(nullable=False)) + else: + raise TypeError(f"Unknown BigQuery field.mode: {mode}") + + @classmethod + def to_ibis(cls, fields: list[bq.SchemaField]) -> sch.Schema: + return sch.Schema({f.name: cls._dtype_from_bigquery_field(f) for f in fields}) + + +# TODO(kszucs): we can eliminate this function by making dt.DataType traversible +# using ibis.common.graph.Node, similarly to how we traverse ops.Node instances: +# node.find(types) +def spread_type(dt: dt.DataType): + """Returns a generator that contains all the types in the given type. + + For complex types like set and array, it returns the types of the elements. + """ + if dt.is_array(): + yield from spread_type(dt.value_type) + elif dt.is_struct(): + for type_ in dt.types: + yield from spread_type(type_) + elif dt.is_map(): + raise NotImplementedError("Maps are not supported in BigQuery") + yield dt From 1726588beb8894bc08c272d718ca8e3a9451d0c2 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 29 Feb 2024 07:18:52 -0800 Subject: [PATCH 326/921] feat: Add ml.metrics.pairwise.euclidean_distance (#397) --- bigframes/ml/metrics/pairwise.py | 16 ++++++++++++++++ tests/system/small/ml/test_metrics_pairwise.py | 16 ++++++++++++++++ .../sklearn/metrics/pairwise.py | 15 +++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/bigframes/ml/metrics/pairwise.py b/bigframes/ml/metrics/pairwise.py index 9ebea4ef42d..ef2c08d4719 100644 --- a/bigframes/ml/metrics/pairwise.py +++ b/bigframes/ml/metrics/pairwise.py @@ -50,3 +50,19 @@ def paired_manhattan_distance( paired_manhattan_distance.__doc__ = inspect.getdoc( vendored_metrics_pairwise.paired_manhattan_distance ) + + +def paired_euclidean_distances( + X: Union[bpd.DataFrame, bpd.Series], Y: Union[bpd.DataFrame, bpd.Series] +) -> bpd.DataFrame: + X, Y = utils.convert_to_dataframe(X, Y) + if len(X.columns) != 1 or len(Y.columns) != 1: + raise ValueError("Inputs X and Y can only contain 1 column.") + + base_bqml = core.BaseBqml(session=X._session) + return base_bqml.distance(X, Y, type="EUCLIDEAN", name="euclidean_distance") + + +paired_euclidean_distances.__doc__ = inspect.getdoc( + vendored_metrics_pairwise.paired_euclidean_distances +) diff --git a/tests/system/small/ml/test_metrics_pairwise.py b/tests/system/small/ml/test_metrics_pairwise.py index e2aee971eef..717f32667f9 100644 --- a/tests/system/small/ml/test_metrics_pairwise.py +++ b/tests/system/small/ml/test_metrics_pairwise.py @@ -47,3 +47,19 @@ def test_paired_manhattan_distance(): pd.testing.assert_frame_equal( result.to_pandas(), expected_pd_df, check_dtype=False, check_index_type=False ) + + +def test_paired_euclidean_distances(): + x_col = [np.array([4.1, 0.5, 1.0])] + y_col = [np.array([3.0, 0.0, 2.5])] + X = bpd.read_pandas(pd.DataFrame({"X": x_col})) + Y = bpd.read_pandas(pd.DataFrame({"Y": y_col})) + + result = metrics.pairwise.paired_euclidean_distances(X, Y) + expected_pd_df = pd.DataFrame( + {"X": x_col, "Y": y_col, "euclidean_distance": [1.926136]} + ) + + pd.testing.assert_frame_equal( + result.to_pandas(), expected_pd_df, check_dtype=False, check_index_type=False + ) diff --git a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py index 5791d850ffd..be3d6753a70 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py +++ b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py @@ -39,3 +39,18 @@ def paired_manhattan_distance(X, Y) -> bpd.DataFrame: bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and manhattan_distance """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +def paired_euclidean_distances(X, Y) -> bpd.DataFrame: + """Compute the paired euclidean distances between X and Y. + + Args: + X (Series or single column DataFrame of array of numeric type): + Input data. + Y (Series or single column DataFrame of array of numeric type): + Input data. X and Y are mapped by indexes, must have the same index. + + Returns: + bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and euclidean_distance + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From e0f1ab07cbc81034e24767baff54560561950e67 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:28:15 -0800 Subject: [PATCH 327/921] feat: add TextEmbedding model version support (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/llm.py | 18 ++++++++++++++++-- bigframes/ml/loader.py | 7 +++++-- bigframes/ml/utils.py | 15 ++++++++++++++- tests/system/small/ml/conftest.py | 9 +++++++++ tests/system/small/ml/test_llm.py | 17 +++++++++++++++++ 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index dfe0af2f25a..79f6b90bfdd 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -266,6 +266,9 @@ class PaLM2TextEmbeddingGenerator(base.Predictor): The model for text embedding. “textembedding-gecko” returns model embeddings for text inputs. "textembedding-gecko-multilingual" returns model embeddings for text inputs which support over 100 languages Default to "textembedding-gecko". + version (str or None): + Model version. Accepted values are "001", "002", "003", "latest" etc. Will use the default version if unset. + See https://cloud.google.com/vertex-ai/docs/generative-ai/learn/model-versioning for details. session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. connection_name (str or None): @@ -279,10 +282,12 @@ def __init__( model_name: Literal[ "textembedding-gecko", "textembedding-gecko-multilingual" ] = "textembedding-gecko", + version: Optional[str] = None, session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, ): self.model_name = model_name + self.version = version self.session = session or bpd.get_global_session() self._bq_connection_manager = clients.BqConnectionManager( self.session.bqconnectionclient, self.session.resourcemanagerclient @@ -321,8 +326,11 @@ def _create_bqml_model(self): f"Model name {self.model_name} is not supported. We only support {', '.join(_EMBEDDING_GENERATOR_ENDPOINTS)}." ) + endpoint = ( + self.model_name + "@" + self.version if self.version else self.model_name + ) options = { - "endpoint": self.model_name, + "endpoint": endpoint, } return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options @@ -342,8 +350,14 @@ def _from_bq( model_connection = model._properties["remoteModelInfo"]["connection"] model_endpoint = bqml_endpoint.split("/")[-1] + model_name, version = utils.parse_model_endpoint(model_endpoint) + embedding_generator_model = cls( - session=session, model_name=model_endpoint, connection_name=model_connection + session=session, + # str to literals + model_name=model_name, # type: ignore + version=version, + connection_name=model_connection, ) embedding_generator_model._bqml_model = core.BqmlModel(session, model) return embedding_generator_model diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index dafda43e9dd..31912a01294 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -30,6 +30,7 @@ linear_model, llm, pipeline, + utils, ) _BQML_MODEL_TYPE_MAPPING = MappingProxyType( @@ -106,8 +107,10 @@ def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): ): # Parse the remote model endpoint bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] - endpoint_model = bqml_endpoint.split("/")[-1] - return _BQML_ENDPOINT_TYPE_MAPPING[endpoint_model]._from_bq( # type: ignore + model_endpoint = bqml_endpoint.split("/")[-1] + model_name, _ = utils.parse_model_endpoint(model_endpoint) + + return _BQML_ENDPOINT_TYPE_MAPPING[model_name]._from_bq( # type: ignore session=session, model=bq_model ) diff --git a/bigframes/ml/utils.py b/bigframes/ml/utils.py index 299282d3337..364fb5e88d6 100644 --- a/bigframes/ml/utils.py +++ b/bigframes/ml/utils.py @@ -13,7 +13,7 @@ # limitations under the License. import typing -from typing import Iterable, Union +from typing import Iterable, Optional, Union import bigframes.constants as constants from bigframes.core import blocks @@ -56,3 +56,16 @@ def _convert_to_series(frame: ArrayType) -> bpd.Series: raise ValueError( f"Unsupported type {type(frame)} to convert to Series. {constants.FEEDBACK_LINK}" ) + + +def parse_model_endpoint(model_endpoint: str) -> tuple[str, Optional[str]]: + """Parse model endpoint string to model_name and version.""" + model_name = model_endpoint + version = None + + at_idx = model_endpoint.find("@") + if at_idx != -1: + version = model_endpoint[at_idx + 1 :] + model_name = model_endpoint[:at_idx] + + return model_name, version diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 8bf08906f9e..c9100f36f39 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -256,6 +256,15 @@ def palm2_embedding_generator_model( ) +@pytest.fixture(scope="session") +def palm2_embedding_generator_model_002( + session, bq_connection +) -> llm.PaLM2TextEmbeddingGenerator: + return llm.PaLM2TextEmbeddingGenerator( + version="002", session=session, connection_name=bq_connection + ) + + @pytest.fixture(scope="session") def palm2_embedding_generator_multilingual_model( session, bq_connection diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index db959b854e3..4d2ddfe5133 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -194,6 +194,23 @@ def test_create_embedding_generator_model( assert reloaded_model.connection_name == bq_connection +def test_create_embedding_generator_model_002( + palm2_embedding_generator_model_002, dataset_id, bq_connection +): + # Model creation doesn't return error + assert palm2_embedding_generator_model_002 is not None + assert palm2_embedding_generator_model_002._bqml_model is not None + + # save, load to ensure configuration was kept + reloaded_model = palm2_embedding_generator_model_002.to_gbq( + f"{dataset_id}.temp_embedding_model", replace=True + ) + assert f"{dataset_id}.temp_embedding_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.model_name == "textembedding-gecko" + assert reloaded_model.version == "002" + assert reloaded_model.connection_name == bq_connection + + def test_create_embedding_generator_multilingual_model( palm2_embedding_generator_multilingual_model, dataset_id, From bfe2b23e2dea0cdf1e1b6ff5b17f6759d73c3e24 Mon Sep 17 00:00:00 2001 From: Dan Lee <71398022+dandhlee@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:06:09 -0500 Subject: [PATCH 328/921] docs: update README to point to new summary pages (#402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) Fixes b/263399076 🦕 Updates the link to the client library reference docs page. The page doesn't exist yet but will be added in a future CL, and mentioned in the TOC from #378. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1323a065d87..f6d6f93e36c 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ Documentation * `BigQuery DataFrames source code (GitHub) `_ * `BigQuery DataFrames sample notebooks `_ -* `BigQuery DataFrames API reference `_ +* `BigQuery DataFrames API reference `_ * `BigQuery documentation `_ From dd3643d3733ca1c2a18352bafac7d32fbdfa2a25 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 29 Feb 2024 23:10:16 +0000 Subject: [PATCH 329/921] fix: exceptions raised in `apply` from a `remote_function` now surface in the client (#387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 309699263 🦕 --- bigframes/functions/remote_function.py | 35 ++++++++-------------- tests/system/large/test_remote_function.py | 24 ++++++++++++++- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index af4c4b138ac..c31105a0216 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -21,7 +21,6 @@ import random import shutil import string -import subprocess import sys import tempfile import textwrap @@ -87,19 +86,6 @@ def _get_hash(def_, package_requirements=None): return hashlib.md5(def_repr).hexdigest() -def _run_system_command(command): - program = subprocess.Popen( - [command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True - ) - stdout, stderr = program.communicate() - exit_code = program.wait() - if exit_code: - raise RuntimeError( - f"Command: {command}\nOutput: {stdout.decode()}\nError: {stderr.decode()}" - f"{constants.FEEDBACK_LINK}" - ) - - def routine_ref_to_string_for_query(routine_ref: bigquery.RoutineReference) -> str: return f"`{routine_ref.project}.{routine_ref.dataset_id}`.{routine_ref.routine_id}" @@ -281,6 +267,8 @@ def generate_cloud_function_main_code(self, def_, dir): code_template = textwrap.dedent( """\ import cloudpickle + import functions_framework + from flask import jsonify import json # original udf code is in {udf_code_file} @@ -289,14 +277,17 @@ def generate_cloud_function_main_code(self, def_, dir): udf = cloudpickle.load(f) def {handler_func_name}(request): - request_json = request.get_json(silent=True) - calls = request_json["calls"] - replies = [] - for call in calls: - reply = udf(*call) - replies.append(reply) - return_json = json.dumps({{"replies" : replies}}) - return return_json + try: + request_json = request.get_json(silent=True) + calls = request_json["calls"] + replies = [] + for call in calls: + reply = udf(*call) + replies.append(reply) + return_json = json.dumps({{"replies" : replies}}) + return return_json + except Exception as e: + return jsonify( {{ "errorMessage": str(e) }} ), 400 """ ) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 773de48adf1..b33298ae019 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -21,7 +21,7 @@ import tempfile import textwrap -from google.api_core.exceptions import NotFound, ResourceExhausted +from google.api_core.exceptions import BadRequest, NotFound, ResourceExhausted from google.cloud import bigquery, functions_v2 import pandas import pytest @@ -1214,6 +1214,28 @@ def square(x): ) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_runtime_error(session, scalars_dfs, dataset_id): + try: + + @session.remote_function([int], int, dataset=dataset_id) + def square(x): + return x * x + + scalars_df, _ = scalars_dfs + + with pytest.raises( + BadRequest, match="400.*errorMessage.*unsupported operand type" + ): + # int64_col has nulls which should cause error in square + scalars_df["int64_col"].apply(square).to_pandas() + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_anonymous_dataset(session, scalars_dfs): try: From 67c2bc949c9809e020c27fa9f8207294bfd5dbad Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:16:16 -0800 Subject: [PATCH 330/921] chore: add the toc template file for entry page (#378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #325083413 🦕 --- docs/templates/toc.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 90dbc504b0a..0d6bec55346 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -3,6 +3,16 @@ name: Overview - href: changelog.md name: Changelog + - items: + - href: summary_overview.yml + name: Overview + - href: summary_class.yml + name: Classes + - href: summary_method.yml + name: Methods + - href: summary_property.yml + name: Properties and Attributes + name: BigQuery DataFrames API - items: - items: - name: Options From a60aba712576e2e4e14cfcfffe9349d6972716a5 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:00:27 -0800 Subject: [PATCH 331/921] fix: docs link for metrics.pairwise (#400) --- docs/reference/bigframes.ml/index.rst | 2 ++ docs/reference/bigframes.ml/metrics.pairwise.rst | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 docs/reference/bigframes.ml/metrics.pairwise.rst diff --git a/docs/reference/bigframes.ml/index.rst b/docs/reference/bigframes.ml/index.rst index 1975d62e6d9..37504b0830d 100644 --- a/docs/reference/bigframes.ml/index.rst +++ b/docs/reference/bigframes.ml/index.rst @@ -25,6 +25,8 @@ API Reference metrics + metrics.pairwise + model_selection pipeline diff --git a/docs/reference/bigframes.ml/metrics.pairwise.rst b/docs/reference/bigframes.ml/metrics.pairwise.rst new file mode 100644 index 00000000000..c20772ef07f --- /dev/null +++ b/docs/reference/bigframes.ml/metrics.pairwise.rst @@ -0,0 +1,7 @@ +bigframes.ml.metrics.pairwise +============================= + +.. automodule:: bigframes.ml.metrics.pairwise + :members: + :inherited-members: + :undoc-members: From 45e6229da392b8a9cea4dc86b83632fed55843ee Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:21:49 -0800 Subject: [PATCH 332/921] chore(main): release 0.23.0 (#399) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 24 ++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e11d47e3c..35eaa3688d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.23.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.22.0...v0.23.0) (2024-03-05) + + +### Features + +* Add ml.metrics.pairwise.euclidean_distance ([#397](https://github.com/googleapis/python-bigquery-dataframes/issues/397)) ([1726588](https://github.com/googleapis/python-bigquery-dataframes/commit/1726588beb8894bc08c272d718ca8e3a9451d0c2)) +* Add TextEmbedding model version support ([#394](https://github.com/googleapis/python-bigquery-dataframes/issues/394)) ([e0f1ab0](https://github.com/googleapis/python-bigquery-dataframes/commit/e0f1ab07cbc81034e24767baff54560561950e67)) + + +### Bug Fixes + +* Code exception in `remote_function` now prevents retry and surfaces in the client ([#387](https://github.com/googleapis/python-bigquery-dataframes/issues/387)) ([dd3643d](https://github.com/googleapis/python-bigquery-dataframes/commit/dd3643d3733ca1c2a18352bafac7d32fbdfa2a25)) +* Docs link for metrics.pairwise ([#400](https://github.com/googleapis/python-bigquery-dataframes/issues/400)) ([a60aba7](https://github.com/googleapis/python-bigquery-dataframes/commit/a60aba712576e2e4e14cfcfffe9349d6972716a5)) + + +### Dependencies + +* Update ibis to version 8.0.0 and refactor `remote_function` to use ibis UDF method ([#277](https://github.com/googleapis/python-bigquery-dataframes/issues/277)) ([350499b](https://github.com/googleapis/python-bigquery-dataframes/commit/350499bccb62e22169ab2f2e1400175b2179ef85)) + + +### Documentation + +* Update README to point to new summary pages ([#402](https://github.com/googleapis/python-bigquery-dataframes/issues/402)) ([bfe2b23](https://github.com/googleapis/python-bigquery-dataframes/commit/bfe2b23e2dea0cdf1e1b6ff5b17f6759d73c3e24)) + ## [0.22.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.21.0...v0.22.0) (2024-02-27) diff --git a/bigframes/version.py b/bigframes/version.py index 387b7663f25..a50b0b86fd2 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.22.0" +__version__ = "0.23.0" From d92ced2adaa30a0405ace9ca6cd70a8e217f13d0 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 5 Mar 2024 20:56:16 +0000 Subject: [PATCH 333/921] feat: Support BYOSA in `remote_function` (#407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 328138730 🦕 --- bigframes/functions/remote_function.py | 21 +++++++++-- bigframes/pandas/__init__.py | 2 + bigframes/session/__init__.py | 9 +++++ tests/system/large/test_remote_function.py | 43 ++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index c31105a0216..5bc8291f590 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -129,6 +129,7 @@ def __init__( bq_connection_client, bq_connection_id, cloud_resource_manager_client, + cloud_function_service_account, ): self._gcp_project_id = gcp_project_id self._cloud_function_region = cloud_function_region @@ -140,6 +141,7 @@ def __init__( self._bq_connection_manager = clients.BqConnectionManager( bq_connection_client, cloud_resource_manager_client ) + self._cloud_function_service_account = cloud_function_service_account def create_bq_remote_function( self, input_args, input_types, output_type, endpoint, bq_function_name @@ -384,6 +386,9 @@ def create_cloud_function(self, def_, cf_name, package_requirements=None): function.service_config = functions_v2.ServiceConfig() function.service_config.available_memory = "1024M" function.service_config.timeout_seconds = 600 + function.service_config.service_account_email = ( + self._cloud_function_service_account + ) create_function_request.function = function # Create the cloud function and wait for it to be ready to use @@ -591,6 +596,7 @@ def remote_function( reuse: bool = True, name: Optional[str] = None, packages: Optional[Sequence[str]] = None, + cloud_function_service_account: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -646,12 +652,12 @@ def remote_function( Client to use for BigQuery operations. If this param is not provided then bigquery client from the session would be used. bigquery_connection_client (google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional): - Client to use for cloud functions operations. If this param is not - provided then functions client from the session would be used. - cloud_functions_client (google.cloud.functions_v2.FunctionServiceClient, Optional): Client to use for BigQuery connection operations. If this param is not provided then bigquery connection client from the session would be used. + cloud_functions_client (google.cloud.functions_v2.FunctionServiceClient, Optional): + Client to use for cloud functions operations. If this param is not + provided then the functions client from the session would be used. resource_manager_client (google.cloud.resourcemanager_v3.ProjectsClient, Optional): Client to use for cloud resource management operations, e.g. for getting and setting IAM roles on cloud resources. If this param is @@ -686,7 +692,13 @@ def remote_function( Explicit name of the external package dependencies. Each dependency is added to the `requirements.txt` as is, and can be of the form supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. - + cloud_function_service_account (str, Optional): + Service account to use for the cloud functions. If not provided then + the default service account would be used. See + https://cloud.google.com/functions/docs/securing/function-identity + for more details. Please make sure the service account has the + necessary IAM permissions configured as described in + https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. """ import bigframes.pandas as bpd @@ -787,6 +799,7 @@ def wrapper(f): bigquery_connection_client, bq_connection_id, resource_manager_client, + cloud_function_service_account, ) rf_name, cf_name = remote_function_client.provision_bq_remote_function( diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 110978a7f10..3c9bb003cca 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -615,6 +615,7 @@ def remote_function( reuse: bool = True, name: Optional[str] = None, packages: Optional[Sequence[str]] = None, + cloud_function_service_account: Optional[str] = None, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -625,6 +626,7 @@ def remote_function( reuse=reuse, name=name, packages=packages, + cloud_function_service_account=cloud_function_service_account, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 4bd205afea1..ef4a3492444 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1337,6 +1337,7 @@ def remote_function( reuse: bool = True, name: Optional[str] = None, packages: Optional[Sequence[str]] = None, + cloud_function_service_account: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1410,6 +1411,13 @@ def remote_function( Explicit name of the external package dependencies. Each dependency is added to the `requirements.txt` as is, and can be of the form supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. + cloud_function_service_account (str, Optional): + Service account to use for the cloud functions. If not provided + then the default service account would be used. See + https://cloud.google.com/functions/docs/securing/function-identity + for more details. Please make sure the service account has the + necessary IAM permissions configured as described in + https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1428,6 +1436,7 @@ def remote_function( reuse=reuse, name=name, packages=packages, + cloud_function_service_account=cloud_function_service_account, ) def read_gbq_function( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index b33298ae019..77aa3c76031 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1279,3 +1279,46 @@ def square(x): cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, square ) + + +@pytest.mark.skip("This requires additional project config.") +def test_remote_function_via_session_custom_sa(scalars_dfs): + # Set these values to run the test locally + # TODO(shobs): Automate and enable this test + PROJECT = "" + GCF_SERVICE_ACCOUNT = "" + + rf_session = bigframes.Session(context=bigframes.BigQueryOptions(project=PROJECT)) + + try: + + @rf_session.remote_function( + [int], int, reuse=False, cloud_function_service_account=GCF_SERVICE_ACCOUNT + ) + def square_num(x): + if x is None: + return x + return x * x + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_result_col = bf_int64_col.apply(square_num) + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_result_col = pd_int64_col.apply(lambda x: x if x is None else x * x) + pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal(bf_result, pd_result, check_dtype=False) + + # Assert that the GCF is created with the intended SA + gcf = rf_session.cloudfunctionsclient.get_function( + name=square_num.bigframes_cloud_function + ) + assert gcf.service_config.service_account_email == GCF_SERVICE_ACCOUNT + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + rf_session.bqclient, rf_session.cloudfunctionsclient, square_num + ) From 6478ad75a98fcd3841ab701fd1f5ae0ddc49f761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=2C=20formerly=29?= Date: Tue, 5 Mar 2024 16:45:31 -0600 Subject: [PATCH 334/921] chore: add load tests session for reading large tables (#410) * chore: add load tests session for reading large tables * update junit prefix * xfail for to_pandas_batches * use smaller table but still beyond query results limit --- .kokoro/load/common.cfg | 10 +++ .kokoro/load/load.cfg | 17 ++++ noxfile.py | 11 +++ scripts/create_load_test_tables.py | 109 +++++++++++++++++++++++++ tests/system/conftest.py | 8 +- tests/system/load/test_large_tables.py | 96 ++++++++++++++++++++++ 6 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 .kokoro/load/common.cfg create mode 100644 .kokoro/load/load.cfg create mode 100644 scripts/create_load_test_tables.py create mode 100644 tests/system/load/test_large_tables.py diff --git a/.kokoro/load/common.cfg b/.kokoro/load/common.cfg new file mode 100644 index 00000000000..97e0651aa92 --- /dev/null +++ b/.kokoro/load/common.cfg @@ -0,0 +1,10 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +build_file: "python-bigquery-dataframes/.kokoro/build.sh" diff --git a/.kokoro/load/load.cfg b/.kokoro/load/load.cfg new file mode 100644 index 00000000000..656614cf735 --- /dev/null +++ b/.kokoro/load/load.cfg @@ -0,0 +1,17 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "load" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/noxfile.py b/noxfile.py index 91d26cf6954..bcd39f961f6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -387,6 +387,17 @@ def e2e(session: nox.sessions.Session): ) +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS[-1]) +def load(session: nox.sessions.Session): + """Run the very large tests in system test suite.""" + run_system( + session=session, + prefix_name="load", + test_folder=os.path.join("tests", "system", "load"), + print_duration=True, + ) + + @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def samples(session): """Run the samples test suite.""" diff --git a/scripts/create_load_test_tables.py b/scripts/create_load_test_tables.py new file mode 100644 index 00000000000..d94a33aa5cc --- /dev/null +++ b/scripts/create_load_test_tables.py @@ -0,0 +1,109 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 math +import os +import pathlib +import sys + +import google.cloud.bigquery as bigquery + +REPO_ROOT = pathlib.Path(__file__).parent.parent + +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") + +if not PROJECT_ID: + print( + "Please set GOOGLE_CLOUD_PROJECT environment variable before running.", + file=sys.stderr, + ) + sys.exit(1) + +DATASET_ID = f"{PROJECT_ID}.load_testing" +TABLE_ID = f"{DATASET_ID}.scalars" +TABLE_ID_FORMAT = f"{DATASET_ID}.scalars_{{size}}" + +KB_BYTES = 1000 +MB_BYTES = 1000 * KB_BYTES +GB_BYTES = 1000 * MB_BYTES +TB_BYTES = 1000 * GB_BYTES +SIZES = ( + ("1mb", MB_BYTES), + ("10mb", 10 * MB_BYTES), + ("100mb", 100 * MB_BYTES), + ("1gb", GB_BYTES), + ("10gb", 10 * GB_BYTES), + ("100gb", 100 * GB_BYTES), + ("1tb", TB_BYTES), +) +SCHEMA_PATH = REPO_ROOT / "tests" / "data" / "scalars_schema.json" +DATA_PATH = REPO_ROOT / "tests" / "data" / "scalars.jsonl" +BQCLIENT = bigquery.Client() + + +def create_dataset(): + dataset = bigquery.Dataset(DATASET_ID) + BQCLIENT.create_dataset(dataset, exists_ok=True) + + +def load_scalars_table(): + schema = BQCLIENT.schema_from_json(SCHEMA_PATH) + job_config = bigquery.LoadJobConfig() + job_config.schema = schema + job_config.write_disposition = bigquery.WriteDisposition.WRITE_TRUNCATE + job_config.source_format = bigquery.SourceFormat.NEWLINE_DELIMITED_JSON + + print(f"Creating {TABLE_ID}") + with open(DATA_PATH, "rb") as data_file: + BQCLIENT.load_table_from_file( + data_file, + TABLE_ID, + job_config=job_config, + ).result() + + +def multiply_table(previous_table_id, target_table_id, multiplier): + clauses = [f"SELECT * FROM `{previous_table_id}`"] * multiplier + query = " UNION ALL ".join(clauses) + job_config = bigquery.QueryJobConfig() + job_config.destination = target_table_id + job_config.write_disposition = bigquery.WriteDisposition.WRITE_TRUNCATE + print(f"Creating {target_table_id}, {multiplier} x {previous_table_id}") + BQCLIENT.query_and_wait(query, job_config=job_config) + + +def create_tables(): + base_table = BQCLIENT.get_table(TABLE_ID) + previous_bytes = base_table.num_bytes + previous_table_id = TABLE_ID + + for table_suffix, target_bytes in SIZES: + # Make sure we exceed the desired bytes by adding to the multiplier. + multiplier = math.ceil(target_bytes / previous_bytes) + 1 + target_table_id = TABLE_ID_FORMAT.format(size=table_suffix) + multiply_table(previous_table_id, target_table_id, multiplier) + + table = BQCLIENT.get_table(target_table_id) + previous_bytes = table.num_bytes + previous_table_id = target_table_id + + +def main(): + create_dataset() + load_scalars_table() + create_tables() + + +if __name__ == "__main__": + main() diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 4aa27d6a19c..7ca1882fe0e 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -104,6 +104,11 @@ def cloudfunctions_client( return session.cloudfunctionsclient +@pytest.fixture(scope="session") +def project_id(bigquery_client: bigquery.Client) -> str: + return bigquery_client.project + + @pytest.fixture(scope="session") def resourcemanager_client( session: bigframes.Session, @@ -159,9 +164,8 @@ def dataset_id_not_created(bigquery_client: bigquery.Client): @pytest.fixture(scope="session") -def dataset_id_permanent(bigquery_client: bigquery.Client) -> str: +def dataset_id_permanent(bigquery_client: bigquery.Client, project_id: str) -> str: """Create a dataset if it doesn't exist.""" - project_id = bigquery_client.project dataset_id = f"{project_id}.{PERMANENT_DATASET}" dataset = bigquery.Dataset(dataset_id) bigquery_client.create_dataset(dataset, exists_ok=True) diff --git a/tests/system/load/test_large_tables.py b/tests/system/load/test_large_tables.py new file mode 100644 index 00000000000..1d4a6b0a5b8 --- /dev/null +++ b/tests/system/load/test_large_tables.py @@ -0,0 +1,96 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +"""Load test for query (SQL) inputs with large results sizes.""" + +import pytest + +import bigframes.pandas as bpd + +KB_BYTES = 1000 +MB_BYTES = 1000 * KB_BYTES +GB_BYTES = 1000 * MB_BYTES +TB_BYTES = 1000 * GB_BYTES + + +@pytest.mark.parametrize( + ("sql", "expected_bytes"), + ( + pytest.param( + "SELECT * FROM load_testing.scalars_1gb", + GB_BYTES, + id="1gb", + ), + pytest.param( + "SELECT * FROM load_testing.scalars_10gb", + 10 * GB_BYTES, + id="10gb", + ), + pytest.param( + "SELECT * FROM load_testing.scalars_100gb", + 100 * GB_BYTES, + id="100gb", + ), + pytest.param( + "SELECT * FROM load_testing.scalars_1tb", + TB_BYTES, + id="1tb", + ), + ), +) +def test_read_gbq_sql_large_results(sql, expected_bytes): + df = bpd.read_gbq(sql) + assert df.memory_usage().sum() >= expected_bytes + + +def test_df_repr_large_table(): + df = bpd.read_gbq("load_testing.scalars_100gb") + row_count, column_count = df.shape + expected = f"[{row_count} rows x {column_count} columns]" + actual = repr(df) + assert expected in actual + + +def test_series_repr_large_table(): + df = bpd.read_gbq("load_testing.scalars_1tb") + actual = repr(df["string_col"]) + assert actual is not None + + +def test_index_repr_large_table(): + df = bpd.read_gbq("load_testing.scalars_1tb") + actual = repr(df.index) + assert actual is not None + + +# FAILED +# tests/system/load/test_large_tables.py::test_to_pandas_batches_large_table +# google.api_core.exceptions.Forbidden: 403 Response too large to return. +# Consider specifying a destination table in your job... +@pytest.mark.xfail +def test_to_pandas_batches_large_table(): + df = bpd.read_gbq("load_testing.scalars_100gb") + expected_row_count, expected_column_count = df.shape + + row_count = 0 + for df in df.to_pandas_batches(): + batch_row_count, batch_column_count = df.shape + assert batch_column_count == expected_column_count + row_count += batch_row_count + + # Attempt to save on memory by manually removing the batch df + # from local memory after finishing with processing. + del df + + assert row_count == expected_row_count From 1dd0f3eb88251203b94a894556867f4197e73343 Mon Sep 17 00:00:00 2001 From: Dan Lee <71398022+dandhlee@users.noreply.github.com> Date: Tue, 5 Mar 2024 19:20:20 -0500 Subject: [PATCH 335/921] chore: update toc entry to properly include summary pages (#415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the files aren't present, docfx does not convert the files from `.yml` extension to `.html`. I've tested this locally to ensure docfx keeps the file extension to `.html` as needed, which we'll need for the new files added for summary pages. The only entry that should change in the future would be `summary_overview.html` to `summary_overview.md`. Filed #414 to keep track of that. 🦕 --- docs/templates/toc.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 0d6bec55346..66973fc5a2d 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -4,13 +4,13 @@ - href: changelog.md name: Changelog - items: - - href: summary_overview.yml + - href: summary_overview.html name: Overview - - href: summary_class.yml + - href: summary_class.html name: Classes - - href: summary_method.yml + - href: summary_method.html name: Methods - - href: summary_property.yml + - href: summary_property.html name: Properties and Attributes name: BigQuery DataFrames API - items: From 5cde3990fac7b527a35c734d143f8b320b896eb8 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:44:02 -0500 Subject: [PATCH 336/921] build(deps): bump cryptography from 42.0.2 to 42.0.4 in .kokoro (#395) Source-Link: https://github.com/googleapis/synthtool/commit/d895aec3679ad22aa120481f746bf9f2f325f26f Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:98f3afd11308259de6e828e37376d18867fd321aba07826e29e4f8d9cab56bad Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 +-- .kokoro/requirements.txt | 57 ++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index d8a1bbca717..e4e943e0259 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:5ea6d0ab82c956b50962f91d94e206d3921537ae5fe1549ec5326381d8905cfa -# created: 2024-01-15T16:32:08.142785673Z + digest: sha256:98f3afd11308259de6e828e37376d18867fd321aba07826e29e4f8d9cab56bad +# created: 2024-02-27T15:56:18.442440378Z diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index bb3d6ca38b1..bda8e38c4f3 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -93,30 +93,39 @@ colorlog==6.7.0 \ # via # gcp-docuploader # nox -cryptography==41.0.6 \ - --hash=sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596 \ - --hash=sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c \ - --hash=sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660 \ - --hash=sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4 \ - --hash=sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead \ - --hash=sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed \ - --hash=sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3 \ - --hash=sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7 \ - --hash=sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09 \ - --hash=sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c \ - --hash=sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43 \ - --hash=sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65 \ - --hash=sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6 \ - --hash=sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da \ - --hash=sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c \ - --hash=sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b \ - --hash=sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8 \ - --hash=sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c \ - --hash=sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d \ - --hash=sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9 \ - --hash=sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86 \ - --hash=sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36 \ - --hash=sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae +cryptography==42.0.4 \ + --hash=sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b \ + --hash=sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce \ + --hash=sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88 \ + --hash=sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7 \ + --hash=sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20 \ + --hash=sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9 \ + --hash=sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff \ + --hash=sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1 \ + --hash=sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764 \ + --hash=sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b \ + --hash=sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298 \ + --hash=sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1 \ + --hash=sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824 \ + --hash=sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257 \ + --hash=sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a \ + --hash=sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129 \ + --hash=sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb \ + --hash=sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929 \ + --hash=sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854 \ + --hash=sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52 \ + --hash=sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923 \ + --hash=sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885 \ + --hash=sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0 \ + --hash=sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd \ + --hash=sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2 \ + --hash=sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18 \ + --hash=sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b \ + --hash=sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992 \ + --hash=sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74 \ + --hash=sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660 \ + --hash=sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925 \ + --hash=sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449 # via # gcp-releasetool # secretstorage From 31325a190320bf01ced53d9f4cdb94462daaa06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=2C=20formerly=29?= Date: Wed, 6 Mar 2024 14:58:16 -0600 Subject: [PATCH 337/921] feat: add engine parameter to `read_parquet` (#413) This makes the default behavior in `read_parquet()` consistent with the other `read_XYZ()` methods. Pandas is used to parse the file and ordering is preserved by default. Use `engine="bigquery"` for the previous behavior (not order preserving). --- bigframes/pandas/__init__.py | 5 ++- bigframes/session/__init__.py | 31 +++++++++++++------ tests/system/small/test_session.py | 21 ++++++++++--- .../bigframes_vendored/pandas/io/parquet.py | 8 ++++- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 3c9bb003cca..3120e96b1ae 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -597,10 +597,13 @@ def read_pickle( read_pickle.__doc__ = inspect.getdoc(bigframes.session.Session.read_pickle) -def read_parquet(path: str | IO["bytes"]) -> bigframes.dataframe.DataFrame: +def read_parquet( + path: str | IO["bytes"], *, engine: str = "auto" +) -> bigframes.dataframe.DataFrame: return global_session.with_default_session( bigframes.session.Session.read_parquet, path, + engine=engine, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index ef4a3492444..4b30a3a9d10 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1130,19 +1130,32 @@ def read_pickle( def read_parquet( self, path: str | IO["bytes"], + *, + engine: str = "auto", ) -> dataframe.DataFrame: - # Note: "engine" is omitted because it is redundant. Loading a table - # from a pandas DataFrame will just create another parquet file + load - # job anyway. table = bigframes_io.random_table(self._anonymous_dataset) - job_config = bigquery.LoadJobConfig() - job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED - job_config.source_format = bigquery.SourceFormat.PARQUET - job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY - job_config.labels = {"bigframes-api": "read_parquet"} + if engine == "bigquery": + job_config = bigquery.LoadJobConfig() + job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED + job_config.source_format = bigquery.SourceFormat.PARQUET + job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY + job_config.labels = {"bigframes-api": "read_parquet"} - return self._read_bigquery_load_job(path, table, job_config=job_config) + return self._read_bigquery_load_job(path, table, job_config=job_config) + else: + read_parquet_kwargs: Dict[str, Any] = {} + if pandas.__version__.startswith("1."): + read_parquet_kwargs["use_nullable_dtypes"] = True + else: + read_parquet_kwargs["dtype_backend"] = "pyarrow" + + pandas_obj = pandas.read_parquet( + path, + engine=engine, # type: ignore + **read_parquet_kwargs, + ) + return self._read_pandas(pandas_obj, "read_parquet") def read_json( self, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 85573472b90..2e2252be065 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -856,11 +856,19 @@ def test_read_pickle_gcs(session, penguins_pandas_df_default_index, gcs_folder): pd.testing.assert_frame_equal(penguins_pandas_df_default_index, df.to_pandas()) -def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): +@pytest.mark.parametrize( + ("engine",), + ( + ("auto",), + ("bigquery",), + ), +) +def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder, engine): scalars_df, _ = scalars_dfs # Include wildcard so that multiple files can be written/read if > 1 GB. # https://cloud.google.com/bigquery/docs/exporting-data#exporting_data_into_one_or_more_files path = gcs_folder + test_read_parquet_gcs.__name__ + "*.parquet" + df_in: bigframes.dataframe.DataFrame = scalars_df.copy() # GEOGRAPHY not supported in parquet export. df_in = df_in.drop(columns="geography_col") @@ -869,8 +877,12 @@ def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): df_write.index.name = f"ordering_id_{random.randrange(1_000_000)}" df_write.to_parquet(path, index=True) + # Only bigquery engine for reads supports wildcards in path name. + if engine != "bigquery": + path = path.replace("*", "000000000000") + df_out = ( - session.read_parquet(path) + session.read_parquet(path, engine=engine) # Restore order. .set_index(df_write.index.name).sort_index() # Restore index. @@ -880,7 +892,8 @@ def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder): # DATETIME gets loaded as TIMESTAMP in parquet. See: # https://cloud.google.com/bigquery/docs/exporting-data#parquet_export_details df_out = df_out.assign( - datetime_col=df_out["datetime_col"].astype("timestamp[us][pyarrow]") + datetime_col=df_out["datetime_col"].astype("timestamp[us][pyarrow]"), + timestamp_col=df_out["timestamp_col"].astype("timestamp[us, tz=UTC][pyarrow]"), ) # Make sure we actually have at least some values before comparing. @@ -919,7 +932,7 @@ def test_read_parquet_gcs_compressed( df_write.to_parquet(path, compression=compression, index=True) df_out = ( - session.read_parquet(path) + session.read_parquet(path, engine="bigquery") # Restore order. .set_index(df_write.index.name).sort_index() # Restore index. diff --git a/third_party/bigframes_vendored/pandas/io/parquet.py b/third_party/bigframes_vendored/pandas/io/parquet.py index 0f664e70fcb..877a384b6d1 100644 --- a/third_party/bigframes_vendored/pandas/io/parquet.py +++ b/third_party/bigframes_vendored/pandas/io/parquet.py @@ -9,6 +9,8 @@ class ParquetIOMixin: def read_parquet( self, path: str, + *, + engine: str = "auto", ): r"""Load a Parquet object from the file path (local or Cloud Storage), returning a DataFrame. @@ -23,11 +25,15 @@ def read_parquet( >>> bpd.options.display.progress_bar = None >>> gcs_path = "gs://cloud-samples-data/bigquery/us-states/us-states.parquet" - >>> df = bpd.read_parquet(path=gcs_path) + >>> df = bpd.read_parquet(path=gcs_path, engine="bigquery") Args: path (str): Local or Cloud Storage path to Parquet file. + engine (str): + One of ``'auto', 'pyarrow', 'fastparquet'``, or ``'bigquery'``. + Parquet library to parse the file. If set to ``'bigquery'``, + order is not preserved. Default, ``'auto'``. Returns: bigframes.dataframe.DataFrame: A BigQuery DataFrames. From 0b344023fe71384e49af9893e363a086f19e5258 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 7 Mar 2024 08:20:17 +0000 Subject: [PATCH 338/921] chore: materialize result of `remote_function` early (#408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 327662690 🦕 --- bigframes/series.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bigframes/series.py b/bigframes/series.py index 4aef959a76a..dfa6fa4b0d3 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1253,11 +1253,17 @@ def apply( ex.message += f"\n{_remote_function_recommendation_message}" raise + # We are working with remote function at this point reprojected_series = Series(self._block._force_reproject()) - return reprojected_series._apply_unary_op( + result_series = reprojected_series._apply_unary_op( ops.RemoteFunctionOp(func=func, apply_on_null=True) ) + # return Series with materialized result so that any error in the remote + # function is caught early + materialized_series = result_series._cached() + return materialized_series + def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_prefix(prefix)) From 76b252f907055d72556e3e95f6cb5ee41de5b1c2 Mon Sep 17 00:00:00 2001 From: Duc Le Tu Date: Fri, 8 Mar 2024 00:36:16 +0700 Subject: [PATCH 339/921] fix: only do row identity based joins when joining by index (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) Fixes #355 🦕 --- bigframes/core/__init__.py | 2 +- bigframes/core/compile/single_column.py | 4 +- bigframes/core/nodes.py | 2 +- tests/system/conftest.py | 7 ++ .../test_issue355_merge_after_filter.py | 70 +++++++++++++++++++ 5 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 tests/system/small/regression/test_issue355_merge_after_filter.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 4dc2e4d7af0..9032993452d 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -349,7 +349,7 @@ def join( self, other: ArrayValue, join_def: join_def.JoinDefinition, - allow_row_identity_join: bool = True, + allow_row_identity_join: bool = False, ): return ArrayValue( nodes.JoinNode( diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index d26e71d1b43..7beebfcb66c 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -33,7 +33,7 @@ def join_by_column_ordered( left: compiled.OrderedIR, right: compiled.OrderedIR, join: join_defs.JoinDefinition, - allow_row_identity_join: bool = True, + allow_row_identity_join: bool = False, ) -> compiled.OrderedIR: """Join two expressions by column equality. @@ -134,7 +134,7 @@ def join_by_column_unordered( left: compiled.UnorderedIR, right: compiled.UnorderedIR, join: join_defs.JoinDefinition, - allow_row_identity_join: bool = True, + allow_row_identity_join: bool = False, ) -> compiled.UnorderedIR: """Join two expressions by column equality. diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index f637177a948..1cd3277cbcf 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -115,7 +115,7 @@ class JoinNode(BigFrameNode): left_child: BigFrameNode right_child: BigFrameNode join: JoinDefinition - allow_row_identity_join: bool = True + allow_row_identity_join: bool = False @property def row_preserving(self) -> bool: diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 7ca1882fe0e..4b5ebc9d439 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -285,6 +285,13 @@ def scalars_table_id(test_data_tables) -> str: return test_data_tables["scalars"] +@pytest.fixture(scope="session") +def baseball_schedules_df(session: bigframes.Session) -> bigframes.dataframe.DataFrame: + """Public BQ table""" + df = session.read_gbq("bigquery-public-data.baseball.schedules") + return df + + @pytest.fixture(scope="session") def hockey_table_id(test_data_tables) -> str: return test_data_tables["hockey_players"] diff --git a/tests/system/small/regression/test_issue355_merge_after_filter.py b/tests/system/small/regression/test_issue355_merge_after_filter.py new file mode 100644 index 00000000000..24ee01cb7f2 --- /dev/null +++ b/tests/system/small/regression/test_issue355_merge_after_filter.py @@ -0,0 +1,70 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +from tests.system.utils import assert_pandas_df_equal + + +@pytest.mark.parametrize( + ("merge_how",), + [ + ("inner",), + ("outer",), + ("left",), + ("right",), + ], +) +def test_merge_after_filter(baseball_schedules_df, merge_how): + on = ["awayTeamName"] + left_columns = [ + "gameId", + "year", + "homeTeamName", + "awayTeamName", + "duration_minutes", + ] + right_columns = [ + "gameId", + "year", + "homeTeamName", + "awayTeamName", + "duration_minutes", + ] + + left = baseball_schedules_df[left_columns] + left = left[left["homeTeamName"] == "Rays"] + # Offset the rows somewhat so that outer join can have an effect. + right = baseball_schedules_df[right_columns] + right = right[right["homeTeamName"] == "White Sox"] + + df = left.merge(right, on=on, how=merge_how) + bf_result = df.to_pandas() + + left_pandas = baseball_schedules_df.to_pandas()[left_columns] + left_pandas = left_pandas[left_pandas["homeTeamName"] == "Rays"] + + right_pandas = baseball_schedules_df.to_pandas()[right_columns] + right_pandas = right_pandas[right_pandas["homeTeamName"] == "White Sox"] + + pd_result = pd.merge( + left_pandas, + right_pandas, + merge_how, + on, + sort=True, + ) + + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) From 38bd2ba21bc1a3222635de22eecd97930bf5b1de Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:38:40 -0800 Subject: [PATCH 340/921] docs: fix the note rendering for DataFrames methods: nlargest, nsmallest (#417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue #328445384 🦕 --- .../bigframes_vendored/pandas/core/frame.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 84d2aa7fcb1..f88649ca13b 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3935,6 +3935,11 @@ def nlargest(self, n: int, columns, keep: str = "first"): ``df.sort_values(columns, ascending=False).head(n)``, but more performant. + .. note:: + This function cannot be used with all column types. For example, when + specifying columns with `object` or `category` dtypes, ``TypeError`` is + raised. + **Examples:** >>> import bigframes.pandas as bpd @@ -4002,11 +4007,6 @@ def nlargest(self, n: int, columns, keep: str = "first"): Returns: DataFrame: The first `n` rows ordered by the given columns in descending order. - - .. note:: - This function cannot be used with all column types. For example, when - specifying columns with `object` or `category` dtypes, ``TypeError`` is - raised. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4022,6 +4022,12 @@ def nsmallest(self, n: int, columns, keep: str = "first"): ``df.sort_values(columns, ascending=True).head(n)``, but more performant. + .. note:: + + This function cannot be used with all column types. For example, when + specifying columns with `object` or `category` dtypes, ``TypeError`` is + raised. + **Examples:** >>> import bigframes.pandas as bpd @@ -4090,11 +4096,6 @@ def nsmallest(self, n: int, columns, keep: str = "first"): Returns: DataFrame: The first `n` rows ordered by the given columns in ascending order. - - .. note:: - This function cannot be used with all column types. For example, when - specifying columns with `object` or `category` dtypes, ``TypeError`` is - raised. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 36173b0c14747fb52909bbedd93249024bae9ac1 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 7 Mar 2024 22:06:15 +0000 Subject: [PATCH 341/921] docs: Document minimum IAM requirement (#416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - https://screenshot.googleplex.com/BPPQ6YVWYykCSus - https://screenshot.googleplex.com/8v3JYwcJJemSKRd Fixes internal issue #328086566 🦕 --- README.rst | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index f6d6f93e36c..ad96382df88 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,11 @@ Prerequisites `install and initialize the gcloud CLI `_, and then generate the application default credentials by doing `gcloud auth application-default login `_. +* The user must have + `BigQuery Job User `_ and + `BigQuery Read Session User `_ + roles for the minimum usage. Additional IAM requirements apply for using remote + functions and ML. Code sample ^^^^^^^^^^^ @@ -215,6 +220,30 @@ steps and an estimator together. to create a pipeline of transforms with a final estimator. +ML remote models +---------------- + +**Requirements** + +To use BigQuery DataFrames ML remote models (`bigframes.ml.remote` or `bigframes.ml.llm`), +you must enable the following APIs: + +* The BigQuery API (bigquery.googleapis.com) +* The BigQuery Connection API (bigqueryconnection.googleapis.com) +* The Vertex AI API (aiplatform.googleapis.com) + +and you must be granted the following IAM roles: + +* BigQuery Data Editor (roles/bigquery.dataEditor) +* BigQuery Connection Admin (roles/bigquery.connectionAdmin) +* Service Account User (roles/iam.serviceAccountUser) on the + `service account `__ + ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` +* Vertex AI User (roles/aiplatform.user) +* Project IAM Admin (roles/resourcemanager.projectIamAdmin) if using default + BigQuery connection, or Browser (roles/browser) if using a pre-created connection + + ML locations ------------ @@ -311,24 +340,8 @@ following IAM roles: `service account `__ ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` * Storage Object Viewer (roles/storage.objectViewer) -* Project IAM Admin (roles/resourcemanager.projectIamAdmin) - -To use BigQuery DataFrames ML remote models(bigframes.ml.remote or bigframes.ml.llm), you must enable the following APIs: - -* The BigQuery API (bigquery.googleapis.com) -* The BigQuery Connection API (bigqueryconnection.googleapis.com) -* The Vertex AI API (aiplatform.googleapis.com) - -To use BigQuery DataFrames ML remote models(bigframes.ml.remote or bigframes.ml.llm), you must be granted the -following IAM roles: - -* BigQuery Data Editor (roles/bigquery.dataEditor) -* BigQuery Connection Admin (roles/bigquery.connectionAdmin) -* Service Account User (roles/iam.serviceAccountUser) on the - `service account `__ - ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` -* Vertex AI User (roles/aiplatform.user) -* Project IAM Admin (roles/resourcemanager.projectIamAdmin) +* Project IAM Admin (roles/resourcemanager.projectIamAdmin) if using default + BigQuery connection, or Browser (roles/browser) if using a pre-created connection **Limitations** From 11a37433f99603e645499d9c1762ed7e65d364fb Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 7 Mar 2024 18:48:17 -0800 Subject: [PATCH 342/921] refactor: export operations use sesssion.execute path (#418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dataframe.py | 25 ++++++++++++++----------- bigframes/session/__init__.py | 10 ++++++++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d467239ea62..24c46994739 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2932,8 +2932,9 @@ def map_columns_on_occurrence(columns): return clustering_columns_for_index + clustering_columns_for_df - def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: - """Create query text representing this dataframe for I/O.""" + def _prepare_export( + self, index: bool, ordering_id: Optional[str] + ) -> Tuple[bigframes.core.ArrayValue, Dict[str, str]]: array_value = self._block.expr new_col_labels, new_idx_labels = utils.get_standardized_ids( @@ -2961,10 +2962,7 @@ def _create_io_query(self, index: bool, ordering_id: Optional[str]) -> str: if ordering_id is not None: array_value = array_value.promote_offsets(ordering_id) - return self._block.session._to_sql( - array_value=array_value, - col_id_overrides=id_overrides, - ) + return array_value, id_overrides def _run_io_query( self, @@ -2974,11 +2972,16 @@ def _run_io_query( ) -> bigquery.TableReference: """Executes a query job presenting this dataframe and returns the destination table.""" - expr = self._block.expr - session = expr.session - sql = self._create_io_query(index=index, ordering_id=ordering_id) - _, query_job = session._start_query( - sql=sql, job_config=job_config # type: ignore + session = self._block.expr.session + export_array, id_overrides = self._prepare_export( + index=index, ordering_id=ordering_id + ) + + _, query_job = session._execute( + export_array, + job_config=job_config, + sorted=False, + col_id_overrides=id_overrides, ) self._set_internal_query_job(query_job) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 4b30a3a9d10..190ce17ee1b 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1626,9 +1626,15 @@ def _execute( *, sorted: bool = True, dry_run=False, + col_id_overrides: Mapping[str, str] = {}, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - sql = self._to_sql(array_value, sorted=sorted) # type:ignore - job_config = bigquery.QueryJobConfig(dry_run=dry_run) + sql = self._to_sql( + array_value, sorted=sorted, col_id_overrides=col_id_overrides + ) # type:ignore + if job_config is None: + job_config = bigquery.QueryJobConfig(dry_run=dry_run) + else: + job_config.dry_run = dry_run return self._start_query( sql=sql, job_config=job_config, From 6a3b0cc7f84120fc5978ce11b6b7c55e89654304 Mon Sep 17 00:00:00 2001 From: Stephanie A <129541811+DevStephanie@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:19:52 -0600 Subject: [PATCH 343/921] docs: add predict sample to samples/snippets/bqml_getting_started_test.py (#388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Add a sample to demonstrate the evaluation results * Adding comments explaining logistic regression results * editing read_gbd explanation * docs: add predict sample to samples/snippets/bqml_getting_started_test.py * correcting variable names * Correcting python variables * feat: add predict by visit to samples/snippets/bqml_getting_started_test.py * file * file * file --------- Co-authored-by: Tim Sweña (Swast, formerly) --- samples/snippets/bqml_getting_started_test.py | 213 ++++++++++++++---- 1 file changed, 171 insertions(+), 42 deletions(-) diff --git a/samples/snippets/bqml_getting_started_test.py b/samples/snippets/bqml_getting_started_test.py index bb282fa5637..d9f9135faa1 100644 --- a/samples/snippets/bqml_getting_started_test.py +++ b/samples/snippets/bqml_getting_started_test.py @@ -14,7 +14,7 @@ def test_bqml_getting_started(random_model_id): - your_model_id = random_model_id + your_model_id = random_model_id # for example: bqml_tutorial.sample_model # [START bigquery_dataframes_bqml_getting_started_tutorial] from bigframes.ml.linear_model import LogisticRegression @@ -26,17 +26,12 @@ def test_bqml_getting_started(random_model_id): # https://github.com/googleapis/python-bigquery-dataframes/issues/169 # for updates to `read_gbq` to support wildcard tables. - df = bpd.read_gbq( - """ - -- Since the order of rows isn't useful for the model training, - -- generate a random ID to use as the index for the DataFrame. - SELECT GENERATE_UUID() AS rowindex, * - FROM - `bigquery-public-data.google_analytics_sample.ga_sessions_*` - WHERE - _TABLE_SUFFIX BETWEEN '20160801' AND '20170630' - """, - index_col="rowindex", + df = bpd.read_gbq_table( + "bigquery-public-data.google_analytics_sample.ga_sessions_*", + filters=[ + ("_table_suffix", ">=", "20160801"), + ("_table_suffix", "<=", "20170630"), + ], ) # Extract the total number of transactions within @@ -53,14 +48,14 @@ def test_bqml_getting_started(random_model_id): # ecommerce transactions within the Google Analytics session. # If the number of transactions is NULL, the value in the label # column is set to 0. Otherwise, it is set to 1. - label = transactions.notnull().map({True: 1, False: 0}) + label = transactions.notnull().map({True: 1, False: 0}).rename("label") # Extract the operating system of the visitor's device. - operatingSystem = df["device"].struct.field("operatingSystem") - operatingSystem = operatingSystem.fillna("") + operating_system = df["device"].struct.field("operatingSystem") + operating_system = operating_system.fillna("") # Extract whether the visitor's device is a mobile device. - isMobile = df["device"].struct.field("isMobile") + is_mobile = df["device"].struct.field("isMobile") # Extract the country from which the sessions originated, based on the IP address. country = df["geoNetwork"].struct.field("country").fillna("") @@ -72,8 +67,8 @@ def test_bqml_getting_started(random_model_id): # to use as training data. features = bpd.DataFrame( { - "os": operatingSystem, - "is_mobile": isMobile, + "os": operating_system, + "is_mobile": is_mobile, "country": country, "pageviews": pageviews, } @@ -95,39 +90,36 @@ def test_bqml_getting_started(random_model_id): # [START bigquery_dataframes_bqml_getting_started_tutorial_evaluate] import bigframes.pandas as bpd - # Select model you'll use for training. `read_gbq_model` loads model data from a + # Select model you'll use for evaluating. `read_gbq_model` loads model data from a # BigQuery, but you could also use the `model` object from the previous steps. model = bpd.read_gbq_model( your_model_id, # For example: "bqml_tutorial.sample_model", ) - # The WHERE clause — _TABLE_SUFFIX BETWEEN '20170701' AND '20170801' — - # limits the number of tables scanned by the query. The date range scanned is - # July 1, 2017 to August 1, 2017. This is the data you're using to evaluate the predictive performance - # of the model. It was collected in the month immediately following the time - # period spanned by the training data. - - df = bpd.read_gbq( - """ - SELECT GENERATE_UUID() AS rowindex, * - FROM - `bigquery-public-data.google_analytics_sample.ga_sessions_*` - WHERE - _TABLE_SUFFIX BETWEEN '20170701' AND '20170801' - """, - index_col="rowindex", + # The filters parameter limits the number of tables scanned by the query. + # The date range scanned is July 1, 2017 to August 1, 2017. This is the + # data you're using to evaluate the predictive performance of the model. + # It was collected in the month immediately following the time period + # spanned by the training data. + df = bpd.read_gbq_table( + "bigquery-public-data.google_analytics_sample.ga_sessions_*", + filters=[ + ("_table_suffix", ">=", "20170701"), + ("_table_suffix", "<=", "20170801"), + ], ) + transactions = df["totals"].struct.field("transactions") - label = transactions.notnull().map({True: 1, False: 0}) - operatingSystem = df["device"].struct.field("operatingSystem") - operatingSystem = operatingSystem.fillna("") - isMobile = df["device"].struct.field("isMobile") + label = transactions.notnull().map({True: 1, False: 0}).rename("label") + operating_system = df["device"].struct.field("operatingSystem") + operating_system = operating_system.fillna("") + is_mobile = df["device"].struct.field("isMobile") country = df["geoNetwork"].struct.field("country").fillna("") pageviews = df["totals"].struct.field("pageviews").fillna(0) features = bpd.DataFrame( { - "os": operatingSystem, - "is_mobile": isMobile, + "os": operating_system, + "is_mobile": is_mobile, "country": country, "pageviews": pageviews, } @@ -163,6 +155,143 @@ def test_bqml_getting_started(random_model_id): # [1 rows x 6 columns] # [END bigquery_dataframes_bqml_getting_started_tutorial_evaluate] - # [START bigquery_dataframes_bqml_getting_started_tutorial_predict] + # [START bigquery_dataframes_bqml_getting_started_tutorial_predict_by_country] + import bigframes.pandas as bpd + + # Select model you'll use for predicting. + # `read_gbq_model` loads model data from + # BigQuery, but you could also use the `model` + # object from the previous steps. + model = bpd.read_gbq_model( + your_model_id, # For example: "bqml_tutorial.sample_model", + ) + + # The filters parameter limits the number of tables scanned by the query. + # The date range scanned is July 1, 2017 to August 1, 2017. This is the + # data you're using to make the prediction. + # It was collected in the month immediately following the time period + # spanned by the training data. + df = bpd.read_gbq_table( + "bigquery-public-data.google_analytics_sample.ga_sessions_*", + filters=[ + ("_table_suffix", ">=", "20170701"), + ("_table_suffix", "<=", "20170801"), + ], + ) + + operating_system = df["device"].struct.field("operatingSystem") + operating_system = operating_system.fillna("") + is_mobile = df["device"].struct.field("isMobile") + country = df["geoNetwork"].struct.field("country").fillna("") + pageviews = df["totals"].struct.field("pageviews").fillna(0) + features = bpd.DataFrame( + { + "os": operating_system, + "is_mobile": is_mobile, + "country": country, + "pageviews": pageviews, + } + ) + # Use Logistic Regression predict method to predict results + # using your model. + # Find more information here in + # [BigFrames](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.linear_model.LogisticRegression#bigframes_ml_linear_model_LogisticRegression_predict) + + predictions = model.predict(features) + + # Call groupby method to group predicted_label by country. + # Call sum method to get the total_predicted_label by country. + total_predicted_purchases = predictions.groupby(["country"])[ + ["predicted_label"] + ].sum() + + # Call the sort_values method with the parameter + # ascending = False to get the highest values. + # Call head method to limit to the 10 highest values. + total_predicted_purchases.sort_values(ascending=False).head(10) + + # country + # United States 220 + # Taiwan 8 + # Canada 7 + # India 2 + # Japan 2 + # Turkey 2 + # Australia 1 + # Brazil 1 + # Germany 1 + # Guyana 1 + # Name: predicted_label, dtype: Int64 + + # [END bigquery_dataframes_bqml_getting_started_tutorial_predict_by_country] + + # [START bigquery_dataframes_bqml_getting_started_tutorial_predict_by_visitor] + + import bigframes.pandas as bpd + + # Select model you'll use for predicting. + # `read_gbq_model` loads model data from + # BigQuery, but you could also use the `model` + # object from the previous steps. + model = bpd.read_gbq_model( + your_model_id, # For example: "bqml_tutorial.sample_model", + ) + + # The filters parameter limits the number of tables scanned by the query. + # The date range scanned is July 1, 2017 to August 1, 2017. This is the + # data you're using to make the prediction. + # It was collected in the month immediately following the time period + # spanned by the training data. + df = bpd.read_gbq_table( + "bigquery-public-data.google_analytics_sample.ga_sessions_*", + filters=[ + ("_table_suffix", ">=", "20170701"), + ("_table_suffix", "<=", "20170801"), + ], + ) + + operating_system = df["device"].struct.field("operatingSystem") + operating_system = operating_system.fillna("") + is_mobile = df["device"].struct.field("isMobile") + country = df["geoNetwork"].struct.field("country").fillna("") + pageviews = df["totals"].struct.field("pageviews").fillna(0) + full_visitor_id = df["fullVisitorId"] + + features = bpd.DataFrame( + { + "os": operating_system, + "is_mobile": is_mobile, + "country": country, + "pageviews": pageviews, + "fullVisitorId": full_visitor_id, + } + ) + + predictions = model.predict(features) + + # Call groupby method to group predicted_label by visitor. + # Call sum method to get the total_predicted_label by visitor. + total_predicted_purchases = predictions.groupby(["fullVisitorId"])[ + ["predicted_label"] + ].sum() + + # Call the sort_values method with the parameter + # ascending = False to get the highest values. + # Call head method to limit to the 10 highest values. + total_predicted_purchases.sort_values(ascending=False).head(10) + + # fullVisitorId + # 9417857471295131045 4 + # 0376394056092189113 2 + # 0456807427403774085 2 + # 057693500927581077 2 + # 112288330928895942 2 + # 1280993661204347450 2 + # 2105122376016897629 2 + # 2158257269735455737 2 + # 2969418676126258798 2 + # 489038402765684003 2 + # Name: predicted_label, dtype: Int64 + - # [END bigquery_dataframes_bqml_getting_started_tutorial_predict] +# [END bigquery_dataframes_bqml_getting_started_tutorial_predict_by_visitor] From 815f578533fb1340296aca2be2083897f899a926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=2C=20formerly=29?= Date: Fri, 8 Mar 2024 15:18:13 -0600 Subject: [PATCH 344/921] chore: increase timeout on load tests (#419) * chore: increase timeout on load tests * increase vm timeout too --- .kokoro/load/common.cfg | 1 + noxfile.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.kokoro/load/common.cfg b/.kokoro/load/common.cfg index 97e0651aa92..7f6fa7e0d9b 100644 --- a/.kokoro/load/common.cfg +++ b/.kokoro/load/common.cfg @@ -8,3 +8,4 @@ action { } build_file: "python-bigquery-dataframes/.kokoro/build.sh" +timeout_mins: 360 diff --git a/noxfile.py b/noxfile.py index bcd39f961f6..db503c43fd7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -290,6 +290,7 @@ def run_system( install_test_extra=True, print_duration=False, extra_pytest_options=(), + timeout_seconds=900, ): """Run the system test suite.""" constraints_path = str( @@ -311,7 +312,7 @@ def run_system( "--quiet", "-n=20", # Any individual test taking longer than 15 mins will be terminated. - "--timeout=900", + f"--timeout={timeout_seconds}", # Log 20 slowest tests "--durations=20", f"--junitxml={prefix_name}_{session.python}_sponge_log.xml", @@ -395,6 +396,7 @@ def load(session: nox.sessions.Session): prefix_name="load", test_folder=os.path.join("tests", "system", "load"), print_duration=True, + timeout_seconds=60 * 60, ) From 9a678e35201d935e1d93875429005033cfe7cff6 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Sat, 9 Mar 2024 02:14:50 +0000 Subject: [PATCH 345/921] feat: Support CMEK for BQ tables (#403) * feat: Support CMEK for BQ tables * add more tests * add unit tests * add more tests, fix broken tests * separate bqml client to send kms_key_name via OPTIONS instead of job config * fix unit tests * fix mypy * skip cmek test for empty cmek * move staticmethods to helper module * revert bqmlclient, pass cmek through call time job config * revert bqmlclient unit test * fix mypy failure * use better named key, disable use_query_cache in test * rename bqml create model internal method * fix renamed methods's reference in unit tests * remove stray bqmlclient variable --- bigframes/_config/bigquery_options.py | 25 ++ bigframes/ml/core.py | 10 +- bigframes/pandas/__init__.py | 1 + bigframes/session/__init__.py | 126 +++++++--- bigframes/session/_io/bigquery.py | 39 ++- bigframes/session/clients.py | 46 ++-- tests/system/small/test_encryption.py | 256 ++++++++++++++++++++ tests/unit/_config/test_bigquery_options.py | 2 + tests/unit/ml/test_golden_sql.py | 11 +- tests/unit/session/test_clients.py | 1 + 10 files changed, 450 insertions(+), 67 deletions(-) create mode 100644 tests/system/small/test_encryption.py diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 74b83429d07..34701740f6e 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -39,6 +39,7 @@ def __init__( bq_connection: Optional[str] = None, use_regional_endpoints: bool = False, application_name: Optional[str] = None, + kms_key_name: Optional[str] = None, ): self._credentials = credentials self._project = project @@ -46,6 +47,7 @@ def __init__( self._bq_connection = bq_connection self._use_regional_endpoints = use_regional_endpoints self._application_name = application_name + self._kms_key_name = kms_key_name self._session_started = False @property @@ -148,3 +150,26 @@ def use_regional_endpoints(self, value: bool): ) self._use_regional_endpoints = value + + @property + def kms_key_name(self) -> Optional[str]: + """Customer managed encryption key used to control encryption of the + data-at-rest in BigQuery. This is of the format + projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY + + See https://cloud.google.com/bigquery/docs/customer-managed-encryption + for more details. + + Please make sure the project used for Bigquery DataFrames has "Cloud KMS + CryptoKey Encrypter/Decrypter" role in the key's project, See + https://cloud.google.com/bigquery/docs/customer-managed-encryption#assign_role + for steps on how to ensure that. + """ + return self._kms_key_name + + @kms_key_name.setter + def kms_key_name(self, value: str): + if self._session_started and self._kms_key_name != value: + raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="kms_key_name")) + + self._kms_key_name = value diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index c496133aa72..24997708fb4 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -212,7 +212,8 @@ def principal_component_info(self) -> bpd.DataFrame: return self._session.read_gbq(sql) def copy(self, new_model_name: str, replace: bool = False) -> BqmlModel: - job_config = bigquery.job.CopyJobConfig() + job_config = self._session._prepare_copy_job_config() + if replace: job_config.write_disposition = "WRITE_TRUNCATE" @@ -236,7 +237,7 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: options={"vertex_ai_model_id": vertex_ai_model_id} ) # Register the model and wait it to finish - self._session._start_query(sql) + self._session._start_query_create_model(sql) self._model = self._session.bqclient.get_model(self.model_name) return self @@ -255,7 +256,7 @@ def _create_model_ref( def _create_model_with_sql(self, session: bigframes.Session, sql: str) -> BqmlModel: # fit the model, synchronously - _, job = session._start_query(sql) + _, job = session._start_query_create_model(sql) # real model path in the session specific hidden dataset and table prefix model_name_full = f"{job.destination.project}.{job.destination.dataset_id}.{job.destination.table_id}" @@ -298,6 +299,9 @@ def create_model( options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session + if session._bq_kms_key_name: + options.update({"kms_key_name": session._bq_kms_key_name}) + model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_model( diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 3120e96b1ae..195d7eabfa7 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -383,6 +383,7 @@ def _set_default_session_location_if_possible(query): use_regional_endpoints=options.bigquery.use_regional_endpoints, credentials=options.bigquery.credentials, application_name=options.bigquery.application_name, + bq_kms_key_name=options.bigquery.kms_key_name, ) bqclient = clients_provider.bqclient diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 190ce17ee1b..b553865ea9f 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -65,7 +65,6 @@ import bigframes._config.bigquery_options as bigquery_options import bigframes.constants as constants -from bigframes.core import log_adapter import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.compile @@ -84,7 +83,6 @@ # Even though the ibis.backends.bigquery import is unused, it's needed # to register new and replacement ops with the Ibis BigQuery backend. -import third_party.bigframes_vendored.ibis.backends.bigquery # noqa import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import third_party.bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import third_party.bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet @@ -161,6 +159,8 @@ def __init__( else: self._location = context.location + self._bq_kms_key_name = context.kms_key_name + # Instantiate a clients provider to help with cloud clients that will be # used in the future operations in the session if clients_provider: @@ -172,9 +172,17 @@ def __init__( use_regional_endpoints=context.use_regional_endpoints, credentials=context.credentials, application_name=context.application_name, + bq_kms_key_name=self._bq_kms_key_name, ) self._create_bq_datasets() + + # TODO(shobs): Remove this logic after https://github.com/ibis-project/ibis/issues/8494 + # has been fixed. The ibis client changes the default query job config + # so we are going to remember the current config and restore it after + # the ibis client has been created + original_default_query_job_config = self.bqclient.default_query_job_config + self.ibis_client = typing.cast( ibis_bigquery.Backend, ibis.bigquery.connect( @@ -184,6 +192,9 @@ def __init__( ), ) + self.bqclient.default_query_job_config = original_default_query_job_config + + # Resolve the BQ connection for remote function and Vertex AI integration self._bq_connection = context.bq_connection or _BIGFRAMES_DEFAULT_CONNECTION_ID # Now that we're starting the session, don't allow the options to be @@ -929,6 +940,8 @@ def _read_pandas_load_job( pandas_dataframe_copy.columns = pandas.Index(new_col_ids) pandas_dataframe_copy[ordering_col] = np.arange(pandas_dataframe_copy.shape[0]) + job_config = self._prepare_load_job_config() + # Specify the datetime dtypes, which is auto-detected as timestamp types. schema: list[bigquery.SchemaField] = [] for column, dtype in zip(pandas_dataframe.columns, pandas_dataframe.dtypes): @@ -936,12 +949,12 @@ def _read_pandas_load_job( schema.append( bigquery.SchemaField(column, bigquery.enums.SqlTypeNames.DATETIME) ) + job_config.schema = schema # Clustering probably not needed anyways as pandas tables are small cluster_cols = [ordering_col] - - job_config = bigquery.LoadJobConfig(schema=schema) job_config.clustering_fields = cluster_cols + job_config.labels = {"bigframes-api": api_name} load_table_destination = bigframes_io.random_table(self._anonymous_dataset) @@ -1061,7 +1074,7 @@ def read_csv( f"{constants.FEEDBACK_LINK}" ) - job_config = bigquery.LoadJobConfig() + job_config = self._prepare_load_job_config() job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED job_config.source_format = bigquery.SourceFormat.CSV job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY @@ -1136,7 +1149,7 @@ def read_parquet( table = bigframes_io.random_table(self._anonymous_dataset) if engine == "bigquery": - job_config = bigquery.LoadJobConfig() + job_config = self._prepare_load_job_config() job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED job_config.source_format = bigquery.SourceFormat.PARQUET job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY @@ -1194,7 +1207,7 @@ def read_json( "'lines' keyword is only valid when 'orient' is 'records'." ) - job_config = bigquery.LoadJobConfig() + job_config = self._prepare_load_job_config() job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED job_config.source_format = bigquery.SourceFormat.NEWLINE_DELIMITED_JSON job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY @@ -1518,6 +1531,53 @@ def read_gbq_function( session=self, ) + def _prepare_query_job_config( + self, + job_config: Optional[bigquery.QueryJobConfig] = None, + ) -> bigquery.QueryJobConfig: + if job_config is None: + job_config = bigquery.QueryJobConfig() + else: + # Create a copy so that we don't mutate the original config passed + job_config = typing.cast( + bigquery.QueryJobConfig, + bigquery.QueryJobConfig.from_api_repr(job_config.to_api_repr()), + ) + + if bigframes.options.compute.maximum_bytes_billed is not None: + job_config.maximum_bytes_billed = ( + bigframes.options.compute.maximum_bytes_billed + ) + + if self._bq_kms_key_name: + job_config.destination_encryption_configuration = ( + bigquery.EncryptionConfiguration(kms_key_name=self._bq_kms_key_name) + ) + + return job_config + + def _prepare_load_job_config(self) -> bigquery.LoadJobConfig: + # Create a copy so that we don't mutate the original config passed + job_config = bigquery.LoadJobConfig() + + if self._bq_kms_key_name: + job_config.destination_encryption_configuration = ( + bigquery.EncryptionConfiguration(kms_key_name=self._bq_kms_key_name) + ) + + return job_config + + def _prepare_copy_job_config(self) -> bigquery.CopyJobConfig: + # Create a copy so that we don't mutate the original config passed + job_config = bigquery.CopyJobConfig() + + if self._bq_kms_key_name: + job_config.destination_encryption_configuration = ( + bigquery.EncryptionConfiguration(kms_key_name=self._bq_kms_key_name) + ) + + return job_config + def _start_query( self, sql: str, @@ -1525,29 +1585,30 @@ def _start_query( max_results: Optional[int] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ - Starts query job and waits for results. + Starts BigQuery query job and waits for results. """ - job_config = self._prepare_job_config(job_config) - api_methods = log_adapter.get_and_reset_api_methods() - job_config.labels = bigframes_io.create_job_configs_labels( - job_configs_labels=job_config.labels, api_methods=api_methods + job_config = self._prepare_query_job_config(job_config) + return bigframes.session._io.bigquery.start_query_with_client( + self.bqclient, sql, job_config, max_results ) - try: - query_job = self.bqclient.query(sql, job_config=job_config) - except google.api_core.exceptions.Forbidden as ex: - if "Drive credentials" in ex.message: - ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." - raise + def _start_query_create_model( + self, + sql: str, + ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """ + Starts BigQuery ML CREATE MODEL query job and waits for results. + """ + job_config = self._prepare_query_job_config() - opts = bigframes.options.display - if opts.progress_bar is not None and not query_job.configuration.dry_run: - results_iterator = formatting_helpers.wait_for_query_job( - query_job, max_results, opts.progress_bar - ) - else: - results_iterator = query_job.result(max_results=max_results) - return results_iterator, query_job + # BQML expects kms_key_name through OPTIONS and not through job config, + # so we must reset any encryption set in the job config + # https://cloud.google.com/bigquery/docs/customer-managed-encryption#encrypt-model + job_config.destination_encryption_configuration = None + + return bigframes.session._io.bigquery.start_query_with_client( + self.bqclient, sql, job_config + ) def _cache_with_cluster_cols( self, array_value: core.ArrayValue, cluster_cols: typing.Sequence[str] @@ -1696,19 +1757,6 @@ def _start_generic_job(self, job: formatting_helpers.GenericJob): else: job.result() - def _prepare_job_config( - self, job_config: Optional[bigquery.QueryJobConfig] = None - ) -> bigquery.QueryJobConfig: - if job_config is None: - job_config = self.bqclient.default_query_job_config - if job_config is None: - job_config = bigquery.QueryJobConfig() - if bigframes.options.compute.maximum_bytes_billed is not None: - job_config.maximum_bytes_billed = ( - bigframes.options.compute.maximum_bytes_billed - ) - return job_config - def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: return Session(context) diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index 3695fc98e85..67820bbbcb5 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -20,11 +20,17 @@ import itertools import textwrap import types -from typing import Dict, Iterable, Optional, Sequence, Union +from typing import Dict, Iterable, Optional, Sequence, Tuple, Union import uuid +import google.api_core.exceptions import google.cloud.bigquery as bigquery +import bigframes +from bigframes.core import log_adapter +import bigframes.formatting_helpers as formatting_helpers +import bigframes.session._io.bigquery as bigframes_io + IO_ORDERING_ID = "bqdf_row_nums" MAX_LABELS_COUNT = 64 TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}" @@ -207,3 +213,34 @@ def format_option(key: str, value: Union[bool, str]) -> str: if isinstance(value, bool): return f"{key}=true" if value else f"{key}=false" return f"{key}={repr(value)}" + + +def start_query_with_client( + bq_client: bigquery.Client, + sql: str, + job_config: bigquery.job.QueryJobConfig, + max_results: Optional[int] = None, +) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """ + Starts query job and waits for results. + """ + api_methods = log_adapter.get_and_reset_api_methods() + job_config.labels = bigframes_io.create_job_configs_labels( + job_configs_labels=job_config.labels, api_methods=api_methods + ) + + try: + query_job = bq_client.query(sql, job_config=job_config) + except google.api_core.exceptions.Forbidden as ex: + if "Drive credentials" in ex.message: + ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." + raise + + opts = bigframes.options.display + if opts.progress_bar is not None and not query_job.configuration.dry_run: + results_iterator = formatting_helpers.wait_for_query_job( + query_job, max_results, opts.progress_bar + ) + else: + results_iterator = query_job.result(max_results=max_results) + return results_iterator, query_job diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index 627c9258a60..7574aa44548 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -68,6 +68,7 @@ def __init__( use_regional_endpoints: Optional[bool], credentials: Optional[google.auth.credentials.Credentials], application_name: Optional[str], + bq_kms_key_name: Optional[str], ): credentials_project = None if credentials is None: @@ -98,6 +99,7 @@ def __init__( self._location = location self._use_regional_endpoints = use_regional_endpoints self._credentials = credentials + self._bq_kms_key_name = bq_kms_key_name # cloud clients initialized for lazy load self._bqclient = None @@ -106,28 +108,34 @@ def __init__( self._cloudfunctionsclient = None self._resourcemanagerclient = None + def _create_bigquery_client(self): + bq_options = None + if self._use_regional_endpoints: + bq_options = google.api_core.client_options.ClientOptions( + api_endpoint=( + _BIGQUERY_REGIONAL_ENDPOINT + if self._location.lower() in _REP_SUPPORTED_REGIONS + else _BIGQUERY_LOCATIONAL_ENDPOINT + ).format(location=self._location), + ) + bq_info = google.api_core.client_info.ClientInfo( + user_agent=self._application_name + ) + + bq_client = bigquery.Client( + client_info=bq_info, + client_options=bq_options, + credentials=self._credentials, + project=self._project, + location=self._location, + ) + + return bq_client + @property def bqclient(self): if not self._bqclient: - bq_options = None - if self._use_regional_endpoints: - bq_options = google.api_core.client_options.ClientOptions( - api_endpoint=( - _BIGQUERY_REGIONAL_ENDPOINT - if self._location.lower() in _REP_SUPPORTED_REGIONS - else _BIGQUERY_LOCATIONAL_ENDPOINT - ).format(location=self._location), - ) - bq_info = google.api_core.client_info.ClientInfo( - user_agent=self._application_name - ) - self._bqclient = bigquery.Client( - client_info=bq_info, - client_options=bq_options, - credentials=self._credentials, - project=self._project, - location=self._location, - ) + self._bqclient = self._create_bigquery_client() return self._bqclient diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py new file mode 100644 index 00000000000..0ce9d881fd6 --- /dev/null +++ b/tests/system/small/test_encryption.py @@ -0,0 +1,256 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from google.cloud import bigquery +import pandas +import pytest + +import bigframes +import bigframes.ml.linear_model + + +@pytest.fixture(scope="module") +def bq_cmek() -> str: + """Customer managed encryption key to encrypt BigQuery data at rest. + + This is of the form projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY + + See https://cloud.google.com/bigquery/docs/customer-managed-encryption for steps. + """ + + # NOTE: This key is manually set up through the cloud console + # TODO(shobs): Automate the the key creation during the test. This will + # require extra IAM privileges for the test runner. + return "projects/bigframes-dev-perf/locations/us/keyRings/bigframesKeyRing/cryptoKeys/bigframesKey" + + +@pytest.fixture(scope="module") +def session_with_bq_cmek(bq_cmek) -> bigframes.Session: + session = bigframes.Session(bigframes.BigQueryOptions(kms_key_name=bq_cmek)) + + return session + + +def _assert_bq_table_is_encrypted( + df: bigframes.dataframe.DataFrame, + cmek: str, + session: bigframes.Session, +): + # Materialize the data in BQ + repr(df) + + # The df should be backed by a query job with intended encryption on the result table + assert df.query_job is not None + assert df.query_job.destination_encryption_configuration.kms_key_name.startswith( + cmek + ) + + # The result table should exist with the intended encryption + table = session.bqclient.get_table(df.query_job.destination) + assert table.encryption_configuration.kms_key_name == cmek + + +def test_session_query_job(bq_cmek, session_with_bq_cmek): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + _, query_job = session_with_bq_cmek._start_query( + "SELECT 123", job_config=bigquery.QueryJobConfig(use_query_cache=False) + ) + query_job.result() + + assert query_job.destination_encryption_configuration.kms_key_name.startswith( + bq_cmek + ) + + # The result table should exist with the intended encryption + table = session_with_bq_cmek.bqclient.get_table(query_job.destination) + assert table.encryption_configuration.kms_key_name == bq_cmek + + +def test_session_load_job(bq_cmek, session_with_bq_cmek): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + # Session should have cmek set in the default query and load job configs + load_table = bigframes.session._io.bigquery.random_table( + session_with_bq_cmek._anonymous_dataset + ) + + df = pandas.DataFrame({"col0": [1, 2, 3]}) + load_job_config = session_with_bq_cmek._prepare_load_job_config() + load_job_config.schema = [ + bigquery.SchemaField(df.columns[0], bigquery.enums.SqlTypeNames.INT64) + ] + + load_job = session_with_bq_cmek.bqclient.load_table_from_dataframe( + df, + load_table, + job_config=load_job_config, + ) + load_job.result() + + assert load_job.destination == load_table + assert load_job.destination_encryption_configuration.kms_key_name.startswith( + bq_cmek + ) + + # The load destination table should be created with the intended encryption + table = session_with_bq_cmek.bqclient.get_table(load_job.destination) + assert table.encryption_configuration.kms_key_name == bq_cmek + + +def test_read_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + # Read the BQ table + df = session_with_bq_cmek.read_gbq(scalars_table_id) + + # Assert encryption + _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) + + +def test_df_apis(bq_cmek, session_with_bq_cmek, scalars_table_id): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + # Read a BQ table and assert encryption + df = session_with_bq_cmek.read_gbq(scalars_table_id) + + # Perform a few dataframe operations and assert assertion + df1 = df.dropna() + _assert_bq_table_is_encrypted(df1, bq_cmek, session_with_bq_cmek) + + df2 = df1.head() + _assert_bq_table_is_encrypted(df2, bq_cmek, session_with_bq_cmek) + + +@pytest.mark.parametrize( + "engine", + [ + pytest.param("bigquery", id="bq_engine"), + pytest.param( + None, + id="default_engine", + marks=pytest.mark.skip( + reason="Internal issue 327544164, cmek does not propagate to the dataframe." + ), + ), + ], +) +def test_read_csv_gcs( + bq_cmek, session_with_bq_cmek, scalars_df_index, gcs_folder, engine +): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + # Create a csv in gcs + write_path = gcs_folder + "test_read_csv_gcs_bigquery_engine*.csv" + read_path = ( + write_path.replace("*", "000000000000") if engine is None else write_path + ) + scalars_df_index.to_csv(write_path) + + # Read the BQ table + df = session_with_bq_cmek.read_csv(read_path, engine=engine) + + # Assert encryption + _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) + + +def test_to_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + # Read a BQ table and assert encryption + df = session_with_bq_cmek.read_gbq(scalars_table_id) + _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) + + # Modify the dataframe and assert assertion + df = df.dropna().head() + _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) + + # Write the result to BQ and assert assertion + output_table_id = df.to_gbq() + output_table = session_with_bq_cmek.bqclient.get_table(output_table_id) + assert output_table.encryption_configuration.kms_key_name == bq_cmek + + +@pytest.mark.skip( + reason="Internal issue 327544164, cmek does not propagate to the dataframe." +) +def test_read_pandas(bq_cmek, session_with_bq_cmek): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + # Read a pandas dataframe + df = session_with_bq_cmek.read_pandas(pandas.DataFrame([1])) + + # Assert encryption + _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) + + +def test_read_pandas_large(bq_cmek, session_with_bq_cmek): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + # Read a pandas dataframe large enough to trigger a BQ load job + df = session_with_bq_cmek.read_pandas(pandas.DataFrame(range(10_000))) + + # Assert encryption + _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) + + +def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): + if not bq_cmek: + pytest.skip("no cmek set for testing") + + model = bigframes.ml.linear_model.LinearRegression() + df = session_with_bq_cmek.read_gbq(penguins_table_id).dropna() + X_train = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) + + assert model is not None + assert model._bqml_model.model.encryption_configuration is not None + assert model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek + + # Assert that model exists in BQ with intended encryption + model_bq = session_with_bq_cmek.bqclient.get_model(model._bqml_model.model_name) + assert model_bq.encryption_configuration.kms_key_name == bq_cmek + + # Explicitly save the model to a destination and assert that encryption holds + model_ref = model._bqml_model_factory._create_model_ref( + session_with_bq_cmek._anonymous_dataset + ) + model_ref_full_name = ( + f"{model_ref.project}.{model_ref.dataset_id}.{model_ref.model_id}" + ) + new_model = model.to_gbq(model_ref_full_name) + assert new_model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek + + # Assert that model exists in BQ with intended encryption + model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) + assert model_bq.encryption_configuration.kms_key_name == bq_cmek diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index e5b6cfe2f1b..1ce70e3da2f 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -29,6 +29,7 @@ ("project", "my-project", "my-other-project"), ("bq_connection", "path/to/connection/1", "path/to/connection/2"), ("use_regional_endpoints", False, True), + ("kms_key_name", "kms/key/name/1", "kms/key/name/2"), ], ) def test_setter_raises_if_session_started(attribute, original_value, new_value): @@ -61,6 +62,7 @@ def test_setter_raises_if_session_started(attribute, original_value, new_value): "project", "bq_connection", "use_regional_endpoints", + "bq_kms_key_name", ] ], ) diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 017c96d46d2..25e12d87bf8 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -35,6 +35,7 @@ def mock_session(): mock_session._anonymous_dataset = bigquery.DatasetReference( TEMP_MODEL_ID.project, TEMP_MODEL_ID.dataset_id ) + mock_session._bq_kms_key_name = None query_job = mock.create_autospec(bigquery.QueryJob) type(query_job).destination = mock.PropertyMock( @@ -42,7 +43,7 @@ def mock_session(): mock_session._anonymous_dataset, TEMP_MODEL_ID.model_id ) ) - mock_session._start_query.return_value = (None, query_job) + mock_session._start_query_create_model.return_value = (None, query_job) return mock_session @@ -103,7 +104,7 @@ def test_linear_regression_default_fit( model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query.assert_called_once_with( + mock_session._start_query_create_model.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -113,7 +114,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query.assert_called_once_with( + mock_session._start_query_create_model.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -146,7 +147,7 @@ def test_logistic_regression_default_fit( model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query.assert_called_once_with( + mock_session._start_query_create_model.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -160,7 +161,7 @@ def test_logistic_regression_params_fit( model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query.assert_called_once_with( + mock_session._start_query_create_model.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) diff --git a/tests/unit/session/test_clients.py b/tests/unit/session/test_clients.py index f1b2a5045a3..30ba2f90915 100644 --- a/tests/unit/session/test_clients.py +++ b/tests/unit/session/test_clients.py @@ -38,6 +38,7 @@ def create_clients_provider(application_name: Optional[str] = None): use_regional_endpoints=False, credentials=credentials, application_name=application_name, + bq_kms_key_name="projects/my-project/locations/us/keyRings/myKeyRing/cryptoKeys/myKey", ) From 60594f4011ff72617932f37c1e53d4d3ba683ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 11 Mar 2024 09:52:16 -0500 Subject: [PATCH 346/921] chore: don't require branch to be synced with main to merge (#425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many recent changes (e.g. docs) don't need to be synced with `main` to be pretty confident they are safe to merge. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .github/sync-repo-settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index cfa62f787c9..80c73d991c5 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -5,7 +5,7 @@ branchProtectionRules: # Defaults to `main` - pattern: main requiresCodeOwnerReviews: true - requiresStrictStatusChecks: true + requiresStrictStatusChecks: false requiredStatusCheckContexts: - 'conventionalcommits.org' - 'cla/google' From 4aadff4db59243b4510a874fef2bdb17402d1674 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:44:20 -0700 Subject: [PATCH 347/921] feat: (Series|Dataframe).plot.hist() (#420) * feat: (Series|Dataframe).plot.hist() --- bigframes/dataframe.py | 5 + bigframes/operations/_matplotlib/__init__.py | 30 +++ bigframes/operations/_matplotlib/core.py | 30 +++ bigframes/operations/_matplotlib/hist.py | 172 ++++++++++++++++++ bigframes/operations/plotting.py | 34 ++++ bigframes/series.py | 5 + docs/reference/bigframes.pandas/frame.rst | 11 ++ docs/reference/bigframes.pandas/series.rst | 9 + setup.py | 1 + testing/constraints-3.9.txt | 1 + tests/system/small/operations/test_plot.py | 168 +++++++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 11 ++ .../bigframes_vendored/pandas/core/series.py | 11 ++ .../pandas/plotting/_core.py | 48 +++++ 14 files changed, 536 insertions(+) create mode 100644 bigframes/operations/_matplotlib/__init__.py create mode 100644 bigframes/operations/_matplotlib/core.py create mode 100644 bigframes/operations/_matplotlib/hist.py create mode 100644 bigframes/operations/plotting.py create mode 100644 tests/system/small/operations/test_plot.py create mode 100644 third_party/bigframes_vendored/pandas/plotting/_core.py diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 24c46994739..a122212d04f 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -59,6 +59,7 @@ import bigframes.formatting_helpers as formatter import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops +import bigframes.operations.plotting as plotting import bigframes.series import bigframes.series as bf_series import bigframes.session._io.bigquery @@ -3193,4 +3194,8 @@ def get_right_id(id): return result + @property + def plot(self): + return plotting.PlotAccessor(self) + __matmul__ = dot diff --git a/bigframes/operations/_matplotlib/__init__.py b/bigframes/operations/_matplotlib/__init__.py new file mode 100644 index 00000000000..f8770a9ef83 --- /dev/null +++ b/bigframes/operations/_matplotlib/__init__.py @@ -0,0 +1,30 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.operations._matplotlib.core as core +import bigframes.operations._matplotlib.hist as hist + +PLOT_CLASSES: dict[str, type[core.MPLPlot]] = { + "hist": hist.HistPlot, +} + + +def plot(data, kind, **kwargs): + plot_obj = PLOT_CLASSES[kind](data, **kwargs) + plot_obj.generate() + plot_obj.draw() + return plot_obj.result + + +__all__ = ["plot"] diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py new file mode 100644 index 00000000000..4b15d6f4dd0 --- /dev/null +++ b/bigframes/operations/_matplotlib/core.py @@ -0,0 +1,30 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc + +import matplotlib.pyplot as plt + + +class MPLPlot(abc.ABC): + @abc.abstractmethod + def generate(self): + pass + + def draw(self) -> None: + plt.draw_if_interactive() + + @property + def result(self): + return self.axes diff --git a/bigframes/operations/_matplotlib/hist.py b/bigframes/operations/_matplotlib/hist.py new file mode 100644 index 00000000000..720b94d7da3 --- /dev/null +++ b/bigframes/operations/_matplotlib/hist.py @@ -0,0 +1,172 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +from typing import Literal + +import numpy as np +import pandas as pd + +import bigframes.constants as constants +import bigframes.operations._matplotlib.core as bfplt + + +class HistPlot(bfplt.MPLPlot): + @property + def _kind(self) -> Literal["hist"]: + return "hist" + + def __init__( + self, + data, + bins: int = 10, + **kwargs, + ) -> None: + self.bins = bins + self.label = kwargs.get("label", None) + self.by = kwargs.pop("by", None) + self.kwargs = kwargs + + if self.by is not None: + raise NotImplementedError( + f"Non-none `by` argument is not yet supported. {constants.FEEDBACK_LINK}" + ) + if not isinstance(self.bins, int): + raise NotImplementedError( + f"Only integer values are supported for the `bins` argument. {constants.FEEDBACK_LINK}" + ) + if kwargs.get("weight", None) is not None: + raise NotImplementedError( + f"Non-none `weight` argument is not yet supported. {constants.FEEDBACK_LINK}" + ) + + self.data = self._compute_plot_data(data) + + def generate(self) -> None: + """ + Calculates weighted histograms through BigQuery and plots them through pandas + native histogram plot. + """ + hist_bars = self._calculate_hist_bars(self.data, self.bins) + bin_edges = self._calculate_bin_edges( + hist_bars, self.bins, self.kwargs.get("range", None) + ) + + weights = { + col_name: hist_bar.values for col_name, hist_bar in hist_bars.items() + } + hist_x = { + col_name: pd.Series( + ( + hist_bar.index.get_level_values("left_exclusive") + + hist_bar.index.get_level_values("right_inclusive") + ) + / 2.0 + ) + for col_name, hist_bar in hist_bars.items() + } + + # Align DataFrames for plotting despite potential differences in column + # lengths, filling shorter columns with zeros. + hist_x_pd = pd.DataFrame( + list(itertools.zip_longest(*hist_x.values())), columns=list(hist_x.keys()) + ).sort_index(axis=1)[self.data.columns.values] + weights_pd = pd.DataFrame( + list(itertools.zip_longest(*weights.values())), columns=list(weights.keys()) + ).sort_index(axis=1)[self.data.columns.values] + + # Prevents pandas from dropping NA values and causing length mismatches by + # filling them with zeros. + hist_x_pd.fillna(0, inplace=True) + weights_pd.fillna(0, inplace=True) + + self.axes = hist_x_pd.plot.hist( + bins=bin_edges, + weights=np.array(weights_pd.values), + **self.kwargs, + ) # type: ignore + + def _compute_plot_data(self, data): + """ + Prepares data for plotting, focusing on numeric data types. + + Raises: + TypeError: If the input data contains no numeric columns. + """ + # Importing at the top of the file causes a circular import. + import bigframes.series as series + + if isinstance(data, series.Series): + label = self.label + if label is None and data.name is None: + label = "" + if label is None: + data = data.to_frame() + else: + data = data.to_frame(name=label) + + # TODO(chelsealin): Support timestamp/date types here. + include_type = ["number"] + numeric_data = data.select_dtypes(include=include_type) + try: + is_empty = numeric_data.columns.empty + except AttributeError: + is_empty = not len(numeric_data) + + if is_empty: + raise TypeError("no numeric data to plot") + + return numeric_data + + @staticmethod + def _calculate_hist_bars(data, bins): + """ + Calculates histogram bars for each column in a BigFrames DataFrame, and + returns a dictionary where keys are column names and values are pandas + Series. The series values are the histogram bins' heights with a + multi-index defining 'left_exclusive' and 'right_inclusive' bin edges. + """ + import bigframes.pandas as bpd + + # TODO: Optimize this by batching multiple jobs into one. + hist_bar = {} + for _, col in enumerate(data.columns): + cutted_data = bpd.cut(data[col], bins=bins, labels=None) + hist_bar[col] = ( + cutted_data.struct.explode() + .value_counts() + .to_pandas() + .sort_index(level="left_exclusive") + ) + return hist_bar + + @staticmethod + def _calculate_bin_edges(hist_bars, bins, range): + """ + Calculate bin edges from the histogram bars. + """ + bin_edges = None + for _, hist_bar in hist_bars.items(): + left = hist_bar.index.get_level_values("left_exclusive") + right = hist_bar.index.get_level_values("right_inclusive") + if bin_edges is None: + bin_edges = left.union(right) + else: + bin_edges = left.union(right).union(bin_edges) + + if bin_edges is None: + return None + + _, bins = np.histogram(bin_edges, bins=bins, range=range) + return bins diff --git a/bigframes/operations/plotting.py b/bigframes/operations/plotting.py new file mode 100644 index 00000000000..ef36e9383af --- /dev/null +++ b/bigframes/operations/plotting.py @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Sequence + +import bigframes.constants as constants +import bigframes.operations._matplotlib as bfplt +import third_party.bigframes_vendored.pandas.plotting._core as vendordt + + +class PlotAccessor: + __doc__ = vendordt.PlotAccessor.__doc__ + + def __init__(self, data) -> None: + self._parent = data + + def hist(self, by: Optional[Sequence[str]] = None, bins: int = 10, **kwargs): + if kwargs.pop("backend", None) is not None: + raise NotImplementedError( + f"Only support matplotlib backend for now. {constants.FEEDBACK_LINK}" + ) + # Calls matplotlib backend to plot the data. + return bfplt.plot(self._parent.copy(), kind="hist", by=by, bins=bins, **kwargs) diff --git a/bigframes/series.py b/bigframes/series.py index dfa6fa4b0d3..21f1f3b4e4d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -50,6 +50,7 @@ import bigframes.operations.aggregations as agg_ops import bigframes.operations.base import bigframes.operations.datetimes as dt +import bigframes.operations.plotting as plotting import bigframes.operations.strings as strings import bigframes.operations.structs as structs import third_party.bigframes_vendored.pandas.core.series as vendored_pandas_series @@ -1557,6 +1558,10 @@ def __array_ufunc__( def str(self) -> strings.StringMethods: return strings.StringMethods(self._block) + @property + def plot(self): + return plotting.PlotAccessor(self) + def _slice( self, start: typing.Optional[int] = None, diff --git a/docs/reference/bigframes.pandas/frame.rst b/docs/reference/bigframes.pandas/frame.rst index a49bcc8f7cb..d1610accddd 100644 --- a/docs/reference/bigframes.pandas/frame.rst +++ b/docs/reference/bigframes.pandas/frame.rst @@ -7,3 +7,14 @@ DataFrame :members: :inherited-members: :undoc-members: + +Accessors +--------- + +Plotting handling +^^^^^^^^^^^^^^^^^ + +.. automodule:: bigframes.operations.plotting + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.pandas/series.rst b/docs/reference/bigframes.pandas/series.rst index e212904f3ff..f14eb8e8625 100644 --- a/docs/reference/bigframes.pandas/series.rst +++ b/docs/reference/bigframes.pandas/series.rst @@ -42,3 +42,12 @@ Struct handling :members: :inherited-members: :undoc-members: + +Plotting handling +^^^^^^^^^^^^^^^^^ + +.. automodule:: bigframes.operations.plotting + :members: + :inherited-members: + :undoc-members: + :noindex: diff --git a/setup.py b/setup.py index 516d5b8a19f..027c1b76afc 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ "tabulate >= 0.9", "ipywidgets >=7.7.1", "humanize >= 4.6.0", + "matplotlib >= 3.7.1", ] extras = { # Optional test dependencies packages. If they're missed, may skip some tests. diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index c4fed64fbd5..07c8b763f3e 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -20,5 +20,6 @@ sqlglot==20.8.0 tabulate==0.9 ipywidgets==7.7.1 humanize==4.6.0 +matplotlib==3.7.1 # extras pandas-gbq==0.19.0 diff --git a/tests/system/small/operations/test_plot.py b/tests/system/small/operations/test_plot.py new file mode 100644 index 00000000000..44f31ec071a --- /dev/null +++ b/tests/system/small/operations/test_plot.py @@ -0,0 +1,168 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas._testing as tm +import pytest + + +def _check_legend_labels(ax, labels): + """ + Check the ax has expected legend label + """ + assert ax.get_legend() is not None + texts = ax.get_legend().get_texts() + if not isinstance(texts, list): + assert texts.get_text() == labels + else: + actual_labels = [t.get_text() for t in texts] + assert len(actual_labels) == len(labels) + for label, e in zip(actual_labels, labels): + assert label == e + + +def test_series_hist_bins(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bins = 5 + ax = scalars_df["int64_col"].plot.hist(bins=bins) + pd_ax = scalars_pandas_df["int64_col"].plot.hist(bins=bins) + + # Compares axis values and height between bigframes and pandas histograms. + # Note: Due to potential float rounding by matplotlib, this test may not + # be applied to all cases. + assert len(ax.patches) == len(pd_ax.patches) + for i in range(len(ax.patches)): + assert ax.patches[i].xy == pd_ax.patches[i].xy + assert ax.patches[i]._height == pd_ax.patches[i]._height + + +def test_dataframes_hist_bins(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bins = 7 + columns = ["int64_col", "int64_too", "float64_col"] + ax = scalars_df[columns].plot.hist(bins=bins) + pd_ax = scalars_pandas_df[columns].plot.hist(bins=bins) + + # Compares axis values and height between bigframes and pandas histograms. + # Note: Due to potential float rounding by matplotlib, this test may not + # be applied to all cases. + assert len(ax.patches) == len(pd_ax.patches) + for i in range(len(ax.patches)): + assert ax.patches[i]._height == pd_ax.patches[i]._height + + +@pytest.mark.parametrize( + ("col_names"), + [ + pytest.param(["int64_col"]), + pytest.param(["float64_col"]), + pytest.param(["int64_too", "bool_col"]), + pytest.param(["bool_col"], marks=pytest.mark.xfail(raises=TypeError)), + pytest.param(["date_col"], marks=pytest.mark.xfail(raises=TypeError)), + pytest.param(["datetime_col"], marks=pytest.mark.xfail(raises=TypeError)), + pytest.param(["time_col"], marks=pytest.mark.xfail(raises=TypeError)), + pytest.param(["timestamp_col"], marks=pytest.mark.xfail(raises=TypeError)), + ], +) +def test_hist_include_types(scalars_dfs, col_names): + scalars_df, _ = scalars_dfs + ax = scalars_df[col_names].plot.hist() + assert len(ax.patches) == 10 + + +@pytest.mark.parametrize( + ("arg_name", "arg_value"), + [ + pytest.param( + "by", ["int64_col"], marks=pytest.mark.xfail(raises=NotImplementedError) + ), + pytest.param( + "bins", [1, 3, 5], marks=pytest.mark.xfail(raises=NotImplementedError) + ), + pytest.param( + "weight", [2, 3], marks=pytest.mark.xfail(raises=NotImplementedError) + ), + pytest.param( + "backend", + "backend.module", + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + ], +) +def test_hist_not_implemented_error(scalars_dfs, arg_name, arg_value): + scalars_df, _ = scalars_dfs + kwargs = {arg_name: arg_value} + scalars_df.plot.hist(**kwargs) + + +def test_hist_kwargs_true_subplots(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + columns = ["int64_col", "int64_too", "float64_col"] + axes = scalars_df[columns].plot.hist(subplots=True) + pd_axes = scalars_pandas_df[columns].plot.hist(subplots=True) + assert len(axes) == len(pd_axes) + + expected_labels = (["int64_col"], ["int64_too"], ["float64_col"]) + for ax, labels in zip(axes, expected_labels): + _check_legend_labels(ax, labels) + + +def test_hist_kwargs_list_subplots(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + columns = ["int64_col", "int64_too", "float64_col"] + subplots = [["int64_col", "int64_too"]] + axes = scalars_df[columns].plot.hist(subplots=subplots) + pd_axes = scalars_pandas_df[columns].plot.hist(subplots=subplots) + assert len(axes) == len(pd_axes) + + expected_labels = (["int64_col", "int64_too"], ["float64_col"]) + for ax, labels in zip(axes, expected_labels): + _check_legend_labels(ax, labels=labels) + + +@pytest.mark.parametrize( + ("orientation"), + [ + pytest.param("horizontal"), + pytest.param("vertical"), + ], +) +def test_hist_kwargs_orientation(scalars_dfs, orientation): + scalars_df, scalars_pandas_df = scalars_dfs + ax = scalars_df["int64_col"].plot.hist(orientation=orientation) + pd_ax = scalars_pandas_df["int64_col"].plot.hist(orientation=orientation) + assert ax.xaxis.get_label().get_text() == pd_ax.xaxis.get_label().get_text() + assert ax.yaxis.get_label().get_text() == pd_ax.yaxis.get_label().get_text() + + +def test_hist_kwargs_ticks_props(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + xticks = [20, 18] + yticks = [30, 40] + + ax = scalars_df["float64_col"].plot.hist(xticks=xticks, yticks=yticks) + pd_ax = scalars_pandas_df["float64_col"].plot.hist(xticks=xticks, yticks=yticks) + xlabels = ax.get_xticklabels() + pd_xlables = pd_ax.get_xticklabels() + assert len(xlabels) == len(pd_xlables) + for i in range(len(pd_xlables)): + tm.assert_almost_equal(xlabels[i].get_fontsize(), pd_xlables[i].get_fontsize()) + tm.assert_almost_equal(xlabels[i].get_rotation(), pd_xlables[i].get_rotation()) + + ylabels = ax.get_yticklabels() + pd_ylables = pd_ax.get_yticklabels() + assert len(xlabels) == len(pd_xlables) + for i in range(len(pd_xlables)): + tm.assert_almost_equal(ylabels[i].get_fontsize(), pd_ylables[i].get_fontsize()) + tm.assert_almost_equal(ylabels[i].get_rotation(), pd_ylables[i].get_rotation()) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index f88649ca13b..0399d9c5b90 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -5224,3 +5224,14 @@ def dot(self, other): the matrix product of self and other in a DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def plot(self): + """ + Make plots of Dataframes. + + Returns: + bigframes.operations.plotting.PlotAccessor: + An accessor making plots. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 6c01a6dd0cb..2c4f2aaa8f7 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3111,6 +3111,17 @@ def str(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def plot(self): + """ + Make plots of Series. + + Returns: + bigframes.operations.plotting.PlotAccessor: + An accessor making plots. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def isin(self, values): """ Whether elements in Series are contained in values. diff --git a/third_party/bigframes_vendored/pandas/plotting/_core.py b/third_party/bigframes_vendored/pandas/plotting/_core.py new file mode 100644 index 00000000000..d0425737eec --- /dev/null +++ b/third_party/bigframes_vendored/pandas/plotting/_core.py @@ -0,0 +1,48 @@ +from typing import Optional, Sequence + +from bigframes import constants + + +class PlotAccessor: + """ + Make plots of Series or DataFrame with the `matplotlib` backend. + """ + + def hist(self, by: Optional[Sequence[str]] = None, bins: int = 10, **kwargs): + """ + Draw one histogram of the DataFrame’s columns. + + A histogram is a representation of the distribution of data. + This function groups the values of all given Series in the DataFrame + into bins and draws all bins in one :class:`matplotlib.axes.Axes`. + This is useful when the DataFrame's Series are in a similar scale. + + Parameters + ---------- + by : str or sequence, optional + Column in the DataFrame to group by. It is not supported yet. + bins : int, default 10 + Number of histogram bins to be used. + **kwargs + Additional keyword arguments are documented in + :meth:`DataFrame.plot`. + + Returns + ------- + class:`matplotlib.AxesSubplot` + Return a histogram plot. + + Examples + -------- + For Series: + + .. plot:: + :context: close-figs + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> df = bpd.DataFrame(np.random.randint(1, 7, 6000), columns=['one']) + >>> df['two'] = np.random.randint(1, 7, 6000) + np.random.randint(1, 7, 6000) + >>> ax = df.plot.hist(bins=12, alpha=0.5) + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 763edeb4f4e8bc4b8bb05a992dae80c49c245e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 12 Mar 2024 12:46:16 -0500 Subject: [PATCH 348/921] fix: move `third_party.bigframes_vendored` to `bigframes_vendored` (#424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will avoid potential conflicts when run from a location that contains the commonly named `third_party` directory. Note: I followed the instructions here: https://stackoverflow.com/a/17179022/101923 but I also had to add a `pyproject.toml` file to support editable installations (see: https://togithub.com/pypa/setuptools/issues/230#issuecomment-1473278299). Fixes internal issue 328781348. 🦕 --- MANIFEST.in | 2 +- bigframes/_config/__init__.py | 3 +- bigframes/_config/display_options.py | 3 +- bigframes/_config/sampling_options.py | 2 +- bigframes/core/blocks.py | 2 +- bigframes/core/compile/aggregate_compiler.py | 2 +- bigframes/core/groupby/__init__.py | 2 +- bigframes/core/indexes/index.py | 2 +- bigframes/core/tools/datetimes.py | 2 +- bigframes/core/utils.py | 3 +- bigframes/core/window/__init__.py | 3 +- bigframes/dataframe.py | 4 +- bigframes/dtypes.py | 6 +-- bigframes/functions/remote_function.py | 2 +- bigframes/ml/base.py | 7 +-- bigframes/ml/cluster.py | 6 +-- bigframes/ml/compose.py | 7 +-- bigframes/ml/decomposition.py | 6 +-- bigframes/ml/ensemble.py | 24 +++++------ bigframes/ml/linear_model.py | 14 +++--- bigframes/ml/metrics/_metrics.py | 6 +-- bigframes/ml/metrics/pairwise.py | 3 +- bigframes/ml/pipeline.py | 6 +-- bigframes/ml/preprocessing.py | 43 ++++++++----------- bigframes/operations/base.py | 2 +- bigframes/operations/datetimes.py | 3 +- bigframes/operations/plotting.py | 3 +- bigframes/operations/strings.py | 3 +- bigframes/operations/structs.py | 3 +- bigframes/pandas/__init__.py | 12 +++--- bigframes/py.typed | 0 bigframes/series.py | 2 +- bigframes/session/__init__.py | 16 +++---- noxfile.py | 5 ++- owlbot.py | 2 +- pyproject.toml | 3 ++ setup.py | 10 ++++- tests/system/small/test_ibis.py | 2 +- tests/unit/test_remote_function.py | 2 +- .../tests/unit/test_pandas_helpers.py | 2 +- .../ibis/backends/bigquery/__init__.py | 4 +- .../ibis/backends/bigquery/registry.py | 3 +- .../ibis/expr/operations/__init__.py | 6 +-- .../bigframes_vendored/pandas/core/frame.py | 2 +- .../bigframes_vendored/pandas/core/generic.py | 3 +- .../bigframes_vendored/pandas/core/series.py | 6 +-- third_party/bigframes_vendored/py.typed | 0 .../sklearn/cluster/_kmeans.py | 3 +- .../sklearn/compose/_column_transformer.py | 3 +- .../sklearn/decomposition/_pca.py | 3 +- .../sklearn/linear_model/_base.py | 5 ++- .../sklearn/linear_model/_logistic.py | 7 ++- .../bigframes_vendored/sklearn/pipeline.py | 3 +- .../sklearn/preprocessing/_data.py | 3 +- .../sklearn/preprocessing/_discretization.py | 3 +- .../sklearn/preprocessing/_encoder.py | 3 +- .../sklearn/preprocessing/_label.py | 3 +- 57 files changed, 152 insertions(+), 138 deletions(-) create mode 100644 bigframes/py.typed create mode 100644 pyproject.toml create mode 100644 third_party/bigframes_vendored/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index b422266a96a..02b1f4ba4bd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,7 +16,7 @@ # Generated by synthtool. DO NOT EDIT! include README.rst LICENSE -recursive-include third_party * +recursive-include third_party/bigframes_vendored * recursive-include bigframes *.json *.proto py.typed recursive-include tests * global-exclude *.py[co] diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index 8dcebfce6a2..bdd7a8f2d63 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -17,11 +17,12 @@ DataFrames from this package. """ +import bigframes_vendored.pandas._config.config as pandas_config + import bigframes._config.bigquery_options as bigquery_options import bigframes._config.compute_options as compute_options import bigframes._config.display_options as display_options import bigframes._config.sampling_options as sampling_options -import third_party.bigframes_vendored.pandas._config.config as pandas_config class Options: diff --git a/bigframes/_config/display_options.py b/bigframes/_config/display_options.py index afa36aa84c2..2af07d30a8d 100644 --- a/bigframes/_config/display_options.py +++ b/bigframes/_config/display_options.py @@ -18,10 +18,9 @@ import dataclasses from typing import Literal, Optional +import bigframes_vendored.pandas.core.config_init as vendored_pandas_config import pandas as pd -import third_party.bigframes_vendored.pandas.core.config_init as vendored_pandas_config - @dataclasses.dataclass class DisplayOptions: diff --git a/bigframes/_config/sampling_options.py b/bigframes/_config/sampling_options.py index a80b9601ca3..f4fa0928e15 100644 --- a/bigframes/_config/sampling_options.py +++ b/bigframes/_config/sampling_options.py @@ -19,7 +19,7 @@ import dataclasses from typing import Literal, Optional -import third_party.bigframes_vendored.pandas.core.config_init as vendored_pandas_config +import bigframes_vendored.pandas.core.config_init as vendored_pandas_config @dataclasses.dataclass diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 993f2caa474..93dcd1d6916 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -29,6 +29,7 @@ from typing import Iterable, List, Mapping, Optional, Sequence, Tuple import warnings +import bigframes_vendored.pandas.io.common as vendored_pandas_io_common import google.cloud.bigquery as bigquery import pandas as pd @@ -45,7 +46,6 @@ import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.session._io.pandas -import third_party.bigframes_vendored.pandas.io.common as vendored_pandas_io_common # Type constraint for wherever column labels are used Label = typing.Hashable diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 86ba16e347a..7059c4fdc19 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -15,6 +15,7 @@ import typing from typing import cast, Optional +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types @@ -26,7 +27,6 @@ import bigframes.core.window_spec as window_spec import bigframes.dtypes as dtypes import bigframes.operations.aggregations as agg_ops -import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops scalar_compiler = scalar_compilers.scalar_op_compiler diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 9a0889b0413..837eb28f688 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -16,6 +16,7 @@ import typing +import bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby import pandas as pd import bigframes.constants as constants @@ -30,7 +31,6 @@ import bigframes.dtypes as dtypes import bigframes.operations.aggregations as agg_ops import bigframes.series as series -import third_party.bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby @log_adapter.class_logger diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 3ae4fbe24a5..328dd493972 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -19,6 +19,7 @@ import typing from typing import Hashable, Optional, Sequence, Union +import bigframes_vendored.pandas.core.indexes.base as vendored_pandas_index import google.cloud.bigquery as bigquery import numpy as np import pandas @@ -33,7 +34,6 @@ import bigframes.formatting_helpers as formatter import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops -import third_party.bigframes_vendored.pandas.core.indexes.base as vendored_pandas_index if typing.TYPE_CHECKING: import bigframes.dataframe diff --git a/bigframes/core/tools/datetimes.py b/bigframes/core/tools/datetimes.py index 093fa0a6705..4aaf320c7ad 100644 --- a/bigframes/core/tools/datetimes.py +++ b/bigframes/core/tools/datetimes.py @@ -16,6 +16,7 @@ from datetime import datetime from typing import Optional, Union +import bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes import pandas as pd import bigframes.constants as constants @@ -23,7 +24,6 @@ import bigframes.dataframe import bigframes.operations as ops import bigframes.series -import third_party.bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes def to_datetime( diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 4331999dd6b..1976ec1e393 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -15,11 +15,10 @@ import typing from typing import Hashable, Iterable, List +import bigframes_vendored.pandas.io.common as vendored_pandas_io_common import pandas as pd import typing_extensions -import third_party.bigframes_vendored.pandas.io.common as vendored_pandas_io_common - UNNAMED_COLUMN_ID = "bigframes_unnamed_column" UNNAMED_INDEX_ID = "bigframes_unnamed_index" diff --git a/bigframes/core/window/__init__.py b/bigframes/core/window/__init__.py index 8711625f88a..fb682c950e1 100644 --- a/bigframes/core/window/__init__.py +++ b/bigframes/core/window/__init__.py @@ -16,11 +16,12 @@ import typing +import bigframes_vendored.pandas.core.window.rolling as vendored_pandas_rolling + from bigframes.core import log_adapter import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.operations.aggregations as agg_ops -import third_party.bigframes_vendored.pandas.core.window.rolling as vendored_pandas_rolling @log_adapter.class_logger diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a122212d04f..6ed882987ce 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -34,6 +34,8 @@ Union, ) +import bigframes_vendored.pandas.core.frame as vendored_pandas_frame +import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing import google.api_core.exceptions import google.cloud.bigquery as bigquery import numpy @@ -63,8 +65,6 @@ import bigframes.series import bigframes.series as bf_series import bigframes.session._io.bigquery -import third_party.bigframes_vendored.pandas.core.frame as vendored_pandas_frame -import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing if typing.TYPE_CHECKING: import bigframes.session diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 8a2055ef7f6..f29d653d4f9 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -20,6 +20,9 @@ import typing from typing import Any, Dict, Iterable, Literal, Tuple, Union +import bigframes_vendored.google_cloud_bigquery._pandas_helpers as gcb3p_pandas_helpers +import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import geopandas as gpd # type: ignore import google.cloud.bigquery as bigquery import ibis @@ -31,9 +34,6 @@ import pyarrow as pa import bigframes.constants as constants -import third_party.bigframes_vendored.google_cloud_bigquery._pandas_helpers as gcb3p_pandas_helpers -import third_party.bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes -import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 5bc8291f590..29c1c68e7cb 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -32,6 +32,7 @@ if TYPE_CHECKING: from bigframes.session import Session +import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes import cloudpickle import google.api_core.exceptions import google.api_core.retry @@ -47,7 +48,6 @@ from bigframes import clients import bigframes.constants as constants import bigframes.dtypes -import third_party.bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes logger = logging.getLogger(__name__) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 845b64caf1c..9001987e9a9 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -24,12 +24,13 @@ import abc from typing import cast, Optional, TypeVar, Union +import bigframes_vendored.sklearn.base + from bigframes.ml import core import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.base -class BaseEstimator(third_party.bigframes_vendored.sklearn.base.BaseEstimator, abc.ABC): +class BaseEstimator(bigframes_vendored.sklearn.base.BaseEstimator, abc.ABC): """ A BigQuery DataFrames machine learning component following the SKLearn API design Ref: https://bit.ly/3NyhKjN @@ -80,7 +81,7 @@ def __repr__(self): # Estimator pretty printer adapted from Sklearn's, which is in turn an adaption of # the inbuilt pretty-printer in CPython - import third_party.bigframes_vendored.cpython._pprint as adapted_pprint + import bigframes_vendored.cpython._pprint as adapted_pprint prettyprinter = adapted_pprint._EstimatorPrettyPrinter( compact=True, indent=1, indent_at_name=True, n_max_elements_to_show=30 diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 6b79d356a23..360ab014532 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -19,22 +19,22 @@ from typing import Dict, List, Optional, Union +import bigframes_vendored.sklearn.cluster._kmeans from google.cloud import bigquery import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.cluster._kmeans @log_adapter.class_logger class KMeans( base.UnsupervisedTrainablePredictor, - third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans, + bigframes_vendored.sklearn.cluster._kmeans.KMeans, ): - __doc__ = third_party.bigframes_vendored.sklearn.cluster._kmeans.KMeans.__doc__ + __doc__ = bigframes_vendored.sklearn.cluster._kmeans.KMeans.__doc__ def __init__(self, n_clusters: int = 8): self.n_clusters = n_clusters diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index ace876dd2df..d35941b338b 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -21,11 +21,12 @@ import typing from typing import List, Optional, Tuple, Union +import bigframes_vendored.sklearn.compose._column_transformer + from bigframes import constants from bigframes.core import log_adapter from bigframes.ml import base, core, globals, preprocessing, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.compose._column_transformer CompilablePreprocessorType = Union[ preprocessing.OneHotEncoder, @@ -40,10 +41,10 @@ @log_adapter.class_logger class ColumnTransformer( base.Transformer, - third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, + bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer, ): __doc__ = ( - third_party.bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer.__doc__ + bigframes_vendored.sklearn.compose._column_transformer.ColumnTransformer.__doc__ ) def __init__( diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index ef777cb33ac..f2b7c97994d 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -19,21 +19,21 @@ from typing import List, Optional, Union +import bigframes_vendored.sklearn.decomposition._pca from google.cloud import bigquery import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.decomposition._pca @log_adapter.class_logger class PCA( base.UnsupervisedTrainablePredictor, - third_party.bigframes_vendored.sklearn.decomposition._pca.PCA, + bigframes_vendored.sklearn.decomposition._pca.PCA, ): - __doc__ = third_party.bigframes_vendored.sklearn.decomposition._pca.PCA.__doc__ + __doc__ = bigframes_vendored.sklearn.decomposition._pca.PCA.__doc__ def __init__(self, n_components: int = 3): self.n_components = n_components diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 7fcaa926edb..23b227de67f 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -19,14 +19,14 @@ from typing import Dict, List, Literal, Optional, Union +import bigframes_vendored.sklearn.ensemble._forest +import bigframes_vendored.xgboost.sklearn from google.cloud import bigquery import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.ensemble._forest -import third_party.bigframes_vendored.xgboost.sklearn _BQML_PARAMS_MAPPING = { "booster": "boosterType", @@ -51,9 +51,9 @@ @log_adapter.class_logger class XGBRegressor( base.SupervisedTrainablePredictor, - third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor, + bigframes_vendored.xgboost.sklearn.XGBRegressor, ): - __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBRegressor.__doc__ + __doc__ = bigframes_vendored.xgboost.sklearn.XGBRegressor.__doc__ def __init__( self, @@ -208,10 +208,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: @log_adapter.class_logger class XGBClassifier( base.SupervisedTrainablePredictor, - third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier, + bigframes_vendored.xgboost.sklearn.XGBClassifier, ): - __doc__ = third_party.bigframes_vendored.xgboost.sklearn.XGBClassifier.__doc__ + __doc__ = bigframes_vendored.xgboost.sklearn.XGBClassifier.__doc__ def __init__( self, @@ -364,12 +364,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: @log_adapter.class_logger class RandomForestRegressor( base.SupervisedTrainablePredictor, - third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor, + bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor, ): - __doc__ = ( - third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.ensemble._forest.RandomForestRegressor.__doc__ def __init__( self, @@ -531,12 +529,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegresso @log_adapter.class_logger class RandomForestClassifier( base.SupervisedTrainablePredictor, - third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier, + bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier, ): - __doc__ = ( - third_party.bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.ensemble._forest.RandomForestClassifier.__doc__ def __init__( self, diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index b0c40693528..68d1e126764 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -19,6 +19,8 @@ from typing import Dict, List, Literal, Optional, Union +import bigframes_vendored.sklearn.linear_model._base +import bigframes_vendored.sklearn.linear_model._logistic from google.cloud import bigquery import bigframes @@ -26,8 +28,6 @@ from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.linear_model._base -import third_party.bigframes_vendored.sklearn.linear_model._logistic _BQML_PARAMS_MAPPING = { "optimize_strategy": "optimizationStrategy", @@ -50,11 +50,9 @@ @log_adapter.class_logger class LinearRegression( base.SupervisedTrainablePredictor, - third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression, + bigframes_vendored.sklearn.linear_model._base.LinearRegression, ): - __doc__ = ( - third_party.bigframes_vendored.sklearn.linear_model._base.LinearRegression.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.linear_model._base.LinearRegression.__doc__ def __init__( self, @@ -184,10 +182,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: @log_adapter.class_logger class LogisticRegression( base.SupervisedTrainablePredictor, - third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression, + bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression, ): __doc__ = ( - third_party.bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression.__doc__ + bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression.__doc__ ) # TODO(ashleyxu) support class_weights in the constructor. diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index 5c81f16e31a..e8c7400f35f 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -19,6 +19,9 @@ import typing from typing import Tuple, Union +import bigframes_vendored.sklearn.metrics._classification as vendored_mertics_classification +import bigframes_vendored.sklearn.metrics._ranking as vendored_mertics_ranking +import bigframes_vendored.sklearn.metrics._regression as vendored_metrics_regression import numpy as np import pandas as pd import sklearn.metrics as sklearn_metrics # type: ignore @@ -26,9 +29,6 @@ import bigframes.constants as constants from bigframes.ml import utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.metrics._classification as vendored_mertics_classification -import third_party.bigframes_vendored.sklearn.metrics._ranking as vendored_mertics_ranking -import third_party.bigframes_vendored.sklearn.metrics._regression as vendored_metrics_regression def r2_score( diff --git a/bigframes/ml/metrics/pairwise.py b/bigframes/ml/metrics/pairwise.py index ef2c08d4719..bdbe4a682d0 100644 --- a/bigframes/ml/metrics/pairwise.py +++ b/bigframes/ml/metrics/pairwise.py @@ -15,9 +15,10 @@ import inspect from typing import Union +import bigframes_vendored.sklearn.metrics.pairwise as vendored_metrics_pairwise + from bigframes.ml import core, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.metrics.pairwise as vendored_metrics_pairwise def paired_cosine_distances( diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 4ae2bfe5552..9289b613b87 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -20,6 +20,7 @@ from typing import cast, List, Optional, Tuple, Union +import bigframes_vendored.sklearn.pipeline from google.cloud import bigquery import bigframes @@ -27,15 +28,14 @@ from bigframes.core import log_adapter from bigframes.ml import base, compose, forecasting, loader, preprocessing, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.pipeline @log_adapter.class_logger class Pipeline( base.BaseEstimator, - third_party.bigframes_vendored.sklearn.pipeline.Pipeline, + bigframes_vendored.sklearn.pipeline.Pipeline, ): - __doc__ = third_party.bigframes_vendored.sklearn.pipeline.Pipeline.__doc__ + __doc__ = bigframes_vendored.sklearn.pipeline.Pipeline.__doc__ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): self.steps = steps diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index a403e57e715..23eab429784 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -20,23 +20,22 @@ import typing from typing import Any, cast, List, Literal, Optional, Tuple, Union +import bigframes_vendored.sklearn.preprocessing._data +import bigframes_vendored.sklearn.preprocessing._discretization +import bigframes_vendored.sklearn.preprocessing._encoder +import bigframes_vendored.sklearn.preprocessing._label + from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd -import third_party.bigframes_vendored.sklearn.preprocessing._data -import third_party.bigframes_vendored.sklearn.preprocessing._discretization -import third_party.bigframes_vendored.sklearn.preprocessing._encoder -import third_party.bigframes_vendored.sklearn.preprocessing._label @log_adapter.class_logger class StandardScaler( base.Transformer, - third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler, + bigframes_vendored.sklearn.preprocessing._data.StandardScaler, ): - __doc__ = ( - third_party.bigframes_vendored.sklearn.preprocessing._data.StandardScaler.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.preprocessing._data.StandardScaler.__doc__ def __init__(self): self._bqml_model: Optional[core.BqmlModel] = None @@ -116,11 +115,9 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @log_adapter.class_logger class MaxAbsScaler( base.Transformer, - third_party.bigframes_vendored.sklearn.preprocessing._data.MaxAbsScaler, + bigframes_vendored.sklearn.preprocessing._data.MaxAbsScaler, ): - __doc__ = ( - third_party.bigframes_vendored.sklearn.preprocessing._data.MaxAbsScaler.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.preprocessing._data.MaxAbsScaler.__doc__ def __init__(self): self._bqml_model: Optional[core.BqmlModel] = None @@ -200,11 +197,9 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @log_adapter.class_logger class MinMaxScaler( base.Transformer, - third_party.bigframes_vendored.sklearn.preprocessing._data.MinMaxScaler, + bigframes_vendored.sklearn.preprocessing._data.MinMaxScaler, ): - __doc__ = ( - third_party.bigframes_vendored.sklearn.preprocessing._data.MinMaxScaler.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.preprocessing._data.MinMaxScaler.__doc__ def __init__(self): self._bqml_model: Optional[core.BqmlModel] = None @@ -284,10 +279,10 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @log_adapter.class_logger class KBinsDiscretizer( base.Transformer, - third_party.bigframes_vendored.sklearn.preprocessing._discretization.KBinsDiscretizer, + bigframes_vendored.sklearn.preprocessing._discretization.KBinsDiscretizer, ): __doc__ = ( - third_party.bigframes_vendored.sklearn.preprocessing._discretization.KBinsDiscretizer.__doc__ + bigframes_vendored.sklearn.preprocessing._discretization.KBinsDiscretizer.__doc__ ) def __init__( @@ -403,15 +398,13 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @log_adapter.class_logger class OneHotEncoder( base.Transformer, - third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, + bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder, ): # BQML max value https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder#syntax TOP_K_DEFAULT = 1000000 FREQUENCY_THRESHOLD_DEFAULT = 0 - __doc__ = ( - third_party.bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.preprocessing._encoder.OneHotEncoder.__doc__ # All estimators must implement __init__ to document their parameters, even # if they don't have any @@ -533,15 +526,13 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @log_adapter.class_logger class LabelEncoder( base.LabelTransformer, - third_party.bigframes_vendored.sklearn.preprocessing._label.LabelEncoder, + bigframes_vendored.sklearn.preprocessing._label.LabelEncoder, ): # BQML max value https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder#syntax TOP_K_DEFAULT = 1000000 FREQUENCY_THRESHOLD_DEFAULT = 0 - __doc__ = ( - third_party.bigframes_vendored.sklearn.preprocessing._label.LabelEncoder.__doc__ - ) + __doc__ = bigframes_vendored.sklearn.preprocessing._label.LabelEncoder.__doc__ # All estimators must implement __init__ to document their parameters, even # if they don't have any diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 154247c0332..9bfa0500b5f 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -16,6 +16,7 @@ import typing +import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing import pandas as pd import bigframes.constants as constants @@ -28,7 +29,6 @@ import bigframes.operations.aggregations as agg_ops import bigframes.series as series import bigframes.session -import third_party.bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing class SeriesMethods: diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py index 3165e6f0035..66ec347add9 100644 --- a/bigframes/operations/datetimes.py +++ b/bigframes/operations/datetimes.py @@ -17,11 +17,12 @@ import datetime as dt from typing import Optional +import bigframes_vendored.pandas.core.indexes.accessor as vendordt + from bigframes.core import log_adapter import bigframes.operations as ops import bigframes.operations.base import bigframes.series as series -import third_party.bigframes_vendored.pandas.core.indexes.accessor as vendordt @log_adapter.class_logger diff --git a/bigframes/operations/plotting.py b/bigframes/operations/plotting.py index ef36e9383af..d19485e65ef 100644 --- a/bigframes/operations/plotting.py +++ b/bigframes/operations/plotting.py @@ -14,9 +14,10 @@ from typing import Optional, Sequence +import bigframes_vendored.pandas.plotting._core as vendordt + import bigframes.constants as constants import bigframes.operations._matplotlib as bfplt -import third_party.bigframes_vendored.pandas.plotting._core as vendordt class PlotAccessor: diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 2798f18b38e..abd45a14531 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -17,13 +17,14 @@ import re from typing import cast, Literal, Optional, Union +import bigframes_vendored.pandas.core.strings.accessor as vendorstr + import bigframes.constants as constants from bigframes.core import log_adapter import bigframes.dataframe as df import bigframes.operations as ops import bigframes.operations.base import bigframes.series as series -import third_party.bigframes_vendored.pandas.core.strings.accessor as vendorstr # Maps from python to re2 REGEXP_FLAGS = { diff --git a/bigframes/operations/structs.py b/bigframes/operations/structs.py index 0e00b781c94..e8a1af96026 100644 --- a/bigframes/operations/structs.py +++ b/bigframes/operations/structs.py @@ -14,12 +14,13 @@ from __future__ import annotations +import bigframes_vendored.pandas.core.arrays.arrow.accessors as vendoracessors + from bigframes.core import log_adapter import bigframes.dataframe import bigframes.operations import bigframes.operations.base import bigframes.series -import third_party.bigframes_vendored.pandas.core.arrays.arrow.accessors as vendoracessors @log_adapter.class_logger diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 195d7eabfa7..03c8412907b 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -36,6 +36,12 @@ Union, ) +import bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat +import bigframes_vendored.pandas.core.reshape.encoding as vendored_pandas_encoding +import bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge +import bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile +import bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes +import bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq from google.cloud import bigquery import numpy import pandas @@ -59,12 +65,6 @@ import bigframes.series import bigframes.session import bigframes.session.clients -import third_party.bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat -import third_party.bigframes_vendored.pandas.core.reshape.encoding as vendored_pandas_encoding -import third_party.bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge -import third_party.bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile -import third_party.bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes -import third_party.bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq # Include method definition so that the method appears in our docs for diff --git a/bigframes/py.typed b/bigframes/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bigframes/series.py b/bigframes/series.py index 21f1f3b4e4d..ef2feb4f92d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -23,6 +23,7 @@ import typing from typing import Any, Mapping, Optional, Tuple, Union +import bigframes_vendored.pandas.core.series as vendored_pandas_series import google.cloud.bigquery as bigquery import numpy import pandas @@ -53,7 +54,6 @@ import bigframes.operations.plotting as plotting import bigframes.operations.strings as strings import bigframes.operations.structs as structs -import third_party.bigframes_vendored.pandas.core.series as vendored_pandas_series LevelType = typing.Union[str, int] LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index b553865ea9f..e3c392cd2f9 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -39,6 +39,14 @@ ) import warnings +# Even though the ibis.backends.bigquery import is unused, it's needed +# to register new and replacement ops with the Ibis BigQuery backend. +import bigframes_vendored.ibis.backends.bigquery # noqa +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq +import bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet +import bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers +import bigframes_vendored.pandas.io.pickle as third_party_pandas_pickle import google.api_core.client_info import google.api_core.client_options import google.api_core.exceptions @@ -81,14 +89,6 @@ import bigframes.session.clients import bigframes.version -# Even though the ibis.backends.bigquery import is unused, it's needed -# to register new and replacement ops with the Ibis BigQuery backend. -import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops -import third_party.bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq -import third_party.bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet -import third_party.bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers -import third_party.bigframes_vendored.pandas.io.pickle as third_party_pandas_pickle - _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" _MAX_CLUSTER_COLUMNS = 4 diff --git a/noxfile.py b/noxfile.py index db503c43fd7..e7f238c01f9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -219,7 +219,10 @@ def unit_noextras(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def mypy(session): """Run type checks with mypy.""" - session.install("-e", ".") + # Editable mode is not compatible with mypy when there are multiple + # package directories. See: + # https://github.com/python/mypy/issues/10564#issuecomment-851687749 + session.install(".") # Just install the dependencies' type info directly, since "mypy --install-types" # might require an additional pass. diff --git a/owlbot.py b/owlbot.py index dc84de7d8ff..4dc6d1aca30 100644 --- a/owlbot.py +++ b/owlbot.py @@ -63,7 +63,7 @@ s.replace( ["MANIFEST.in"], re.escape("recursive-include google"), - "recursive-include third_party *\nrecursive-include bigframes", + "recursive-include third_party/bigframes_vendored *\nrecursive-include bigframes", ) # Even though BigQuery DataFrames isn't technically a client library, we are diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..fed528d4a7a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 027c1b76afc..a626fd4b347 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,11 @@ packages = [ package for package in setuptools.find_namespace_packages() - if package.startswith("bigframes") or package.startswith("third_party") + if package.startswith("bigframes") +] + [ + package + for package in setuptools.find_namespace_packages("third_party") + if package.startswith("bigframes_vendored") ] setuptools.setup( @@ -115,6 +119,10 @@ install_requires=dependencies, extras_require=extras, platforms="Posix; MacOS X; Windows", + package_dir={ + "bigframes": "bigframes", + "bigframes_vendored": "third_party/bigframes_vendored", + }, packages=packages, python_requires=">=3.9", include_package_data=True, diff --git a/tests/system/small/test_ibis.py b/tests/system/small/test_ibis.py index 9fe1176068b..e2648d1eba4 100644 --- a/tests/system/small/test_ibis.py +++ b/tests/system/small/test_ibis.py @@ -14,10 +14,10 @@ """Tests for monkeypatched ibis code.""" +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis.expr.types as ibis_types import bigframes -import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops def test_approximate_quantiles(session: bigframes.Session, scalars_table_id: str): diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py index 629bc5326a1..1acff27c7f0 100644 --- a/tests/unit/test_remote_function.py +++ b/tests/unit/test_remote_function.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes from ibis.expr import datatypes as ibis_types import bigframes.dtypes -import third_party.bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes def test_supported_types_correspond(): diff --git a/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py b/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py index dc4a09cc541..c798b0d1695 100644 --- a/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py +++ b/third_party/bigframes_vendored/google_cloud_bigquery/tests/unit/test_pandas_helpers.py @@ -25,7 +25,7 @@ @pytest.fixture def module_under_test(): - from third_party.bigframes_vendored.google_cloud_bigquery import _pandas_helpers + from bigframes_vendored.google_cloud_bigquery import _pandas_helpers return _pandas_helpers diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py b/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py index 43508fab11b..1d2d05a741a 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py @@ -1,3 +1,3 @@ # Import all sub-modules to monkeypatch everything. -import third_party.bigframes_vendored.ibis.backends.bigquery.compiler # noqa -import third_party.bigframes_vendored.ibis.backends.bigquery.registry # noqa +import bigframes_vendored.ibis.backends.bigquery.compiler # noqa +import bigframes_vendored.ibis.backends.bigquery.registry # noqa diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py index e1b28690d7c..3f89feaa34e 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -1,10 +1,9 @@ # Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/registry.py """Module to convert from Ibis expression to SQL string.""" +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops from ibis.backends.bigquery.registry import OPERATION_REGISTRY -import third_party.bigframes_vendored.ibis.expr.operations as vendored_ibis_ops - def _approx_quantiles(translator, op: vendored_ibis_ops.ApproximateMultiQuantile): arg = translator.translate(op.arg) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py index 82197013923..2c2efe528db 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py @@ -1,6 +1,6 @@ # Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/__init__.py from __future__ import annotations -from third_party.bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F401 F403 -from third_party.bigframes_vendored.ibis.expr.operations.json import * # noqa: F401 F403 -from third_party.bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F401 F403 +from bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F401 F403 +from bigframes_vendored.ibis.expr.operations.json import * # noqa: F401 F403 +from bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F401 F403 diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 0399d9c5b90..313c6663c8b 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -13,11 +13,11 @@ from typing import Hashable, Iterable, Literal, Mapping, Optional, Sequence, Union +from bigframes_vendored.pandas.core.generic import NDFrame import numpy as np import pandas as pd from bigframes import constants -from third_party.bigframes_vendored.pandas.core.generic import NDFrame # ----------------------------------------------------------------------- # DataFrame class diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index b55c7e23d82..01d8f7a174f 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -3,8 +3,9 @@ from typing import Iterator, Literal, Optional +from bigframes_vendored.pandas.core import indexing + from bigframes import constants -from third_party.bigframes_vendored.pandas.core import indexing class NDFrame(indexing.IndexingMixin): diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 2c4f2aaa8f7..beaf8aedb10 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -5,16 +5,16 @@ from typing import Hashable, IO, Literal, Mapping, Sequence, TYPE_CHECKING +from bigframes_vendored.pandas.core.generic import NDFrame import numpy as np from pandas._libs import lib from pandas._typing import Axis, FilePath, NaPosition, WriteBuffer from bigframes import constants -from third_party.bigframes_vendored.pandas.core.generic import NDFrame if TYPE_CHECKING: - from third_party.bigframes_vendored.pandas.core.frame import DataFrame - from third_party.bigframes_vendored.pandas.core.groupby import SeriesGroupBy + from bigframes_vendored.pandas.core.frame import DataFrame + from bigframes_vendored.pandas.core.groupby import SeriesGroupBy class Series(NDFrame): # type: ignore[misc] diff --git a/third_party/bigframes_vendored/py.typed b/third_party/bigframes_vendored/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index be6c5e7c521..d72b9b7bd53 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -13,8 +13,9 @@ from abc import ABC +from bigframes_vendored.sklearn.base import BaseEstimator + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator class _BaseKMeans(BaseEstimator, ABC): diff --git a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py index dead173b2df..b08eb104924 100644 --- a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py +++ b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py @@ -6,8 +6,9 @@ from abc import ABCMeta +from bigframes_vendored.sklearn.base import BaseEstimator + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator class _BaseComposition(BaseEstimator, metaclass=ABCMeta): diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 011ecc06dd3..30c9c3b0b61 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -12,8 +12,9 @@ from abc import ABCMeta +from bigframes_vendored.sklearn.base import BaseEstimator + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator class PCA(BaseEstimator, metaclass=ABCMeta): diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index ab946e58610..ad2c8724688 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -17,13 +17,14 @@ from abc import ABCMeta -from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import ( +from bigframes_vendored.sklearn.base import ( BaseEstimator, ClassifierMixin, RegressorMixin, ) +from bigframes import constants + class LinearModel(BaseEstimator, metaclass=ABCMeta): def predict(self, X): diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 989ca03c827..621c78d551f 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -12,14 +12,13 @@ # Original location: https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/linear_model/_logistic.py -from typing import List, Optional - -from bigframes import constants -from third_party.bigframes_vendored.sklearn.linear_model._base import ( +from bigframes_vendored.sklearn.linear_model._base import ( BaseEstimator, LinearClassifierMixin, ) +from bigframes import constants + class LogisticRegression(LinearClassifierMixin, BaseEstimator): """Logistic Regression (aka logit, MaxEnt) classifier. diff --git a/third_party/bigframes_vendored/sklearn/pipeline.py b/third_party/bigframes_vendored/sklearn/pipeline.py index 4b8eb25a973..aed15659601 100644 --- a/third_party/bigframes_vendored/sklearn/pipeline.py +++ b/third_party/bigframes_vendored/sklearn/pipeline.py @@ -11,8 +11,9 @@ from abc import ABCMeta +from bigframes_vendored.sklearn.base import BaseEstimator + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator class Pipeline(BaseEstimator, metaclass=ABCMeta): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py index 5ce102d573f..1ff83aa640b 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_data.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_data.py @@ -7,8 +7,9 @@ # Eric Chang # License: BSD 3 clause +from bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin class StandardScaler(BaseEstimator, TransformerMixin): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py index 0236558dd4b..5fcc4815730 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py @@ -3,8 +3,9 @@ # License: BSD +from bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin class KBinsDiscretizer(TransformerMixin, BaseEstimator): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index 8da9a98c53f..5e5e8ac0429 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -2,8 +2,9 @@ # Joris Van den Bossche # License: BSD 3 clause +from bigframes_vendored.sklearn.base import BaseEstimator + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator class OneHotEncoder(BaseEstimator): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_label.py b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py index 83f8eb0f9c2..cc6b995c8cf 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_label.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py @@ -6,8 +6,9 @@ # Hamzeh Alsalhi # License: BSD 3 clause +from bigframes_vendored.sklearn.base import BaseEstimator + from bigframes import constants -from third_party.bigframes_vendored.sklearn.base import BaseEstimator class LabelEncoder(BaseEstimator): From ae0e3eaca49171fd449de4d43ddc3e3ce9fdc2ce Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:50:16 -0700 Subject: [PATCH 349/921] fix: read_pandas inline respects location (#412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes b/327544164 🦕 --- bigframes/core/__init__.py | 4 ++-- bigframes/core/blocks.py | 4 ++-- bigframes/core/nodes.py | 1 + bigframes/dataframe.py | 2 +- bigframes/session/__init__.py | 2 +- notebooks/location/regionalized.ipynb | 2 +- tests/system/small/test_dataframe.py | 17 +++++++++++++++++ tests/system/small/test_session.py | 11 +++++++++++ tests/unit/core/test_blocks.py | 9 ++++++++- 9 files changed, 44 insertions(+), 8 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 9032993452d..e4a60e08e1e 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -69,7 +69,7 @@ def from_ibis( return cls(node) @classmethod - def from_pandas(cls, pd_df: pandas.DataFrame): + def from_pandas(cls, pd_df: pandas.DataFrame, session: bigframes.Session): iobytes = io.BytesIO() # Use alphanumeric identifiers, to avoid downstream problems with escaping. as_ids = [ @@ -78,7 +78,7 @@ def from_pandas(cls, pd_df: pandas.DataFrame): ] unique_ids = tuple(bigframes.core.utils.disambiguate_ids(as_ids)) pd_df.reset_index(drop=True).set_axis(unique_ids, axis=1).to_feather(iobytes) - node = nodes.ReadLocalNode(iobytes.getvalue()) + node = nodes.ReadLocalNode(feather_bytes=iobytes.getvalue(), session=session) return cls(node) @property diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 93dcd1d6916..375ce7e7e0e 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -140,7 +140,7 @@ def __init__( self._stats_cache[" ".join(self.index_columns)] = {} @classmethod - def from_local(cls, data) -> Block: + def from_local(cls, data, session: bigframes.Session) -> Block: pd_data = pd.DataFrame(data) columns = pd_data.columns @@ -162,7 +162,7 @@ def from_local(cls, data) -> Block: ) index_ids = pd_data.columns[: len(index_labels)] - keys_expr = core.ArrayValue.from_pandas(pd_data) + keys_expr = core.ArrayValue.from_pandas(pd_data, session) return cls( keys_expr, column_labels=columns, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 1cd3277cbcf..9da535e15f0 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -155,6 +155,7 @@ def __hash__(self): @dataclass(frozen=True) class ReadLocalNode(BigFrameNode): feather_bytes: bytes + session: typing.Optional[bigframes.session.Session] = None def __hash__(self): return self._node_hash diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 6ed882987ce..5dae7a82f93 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1646,7 +1646,7 @@ def _reindex_rows( raise NotImplementedError( "Cannot reindex with index with different nlevels" ) - new_indexer = DataFrame(index=index)[[]] + new_indexer = DataFrame(index=index, session=self._session)[[]] # multiindex join is senstive to index names, so we will set all these result = new_indexer.rename_axis(range(new_indexer.index.nlevels)).join( self.rename_axis(range(self.index.nlevels)), diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index e3c392cd2f9..5266267a222 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -916,7 +916,7 @@ def _read_pandas( def _read_pandas_inline( self, pandas_dataframe: pandas.DataFrame ) -> dataframe.DataFrame: - return dataframe.DataFrame(blocks.Block.from_local(pandas_dataframe)) + return dataframe.DataFrame(blocks.Block.from_local(pandas_dataframe, self)) def _read_pandas_load_job( self, pandas_dataframe: pandas.DataFrame, api_name: str diff --git a/notebooks/location/regionalized.ipynb b/notebooks/location/regionalized.ipynb index a7ff5db84e3..86f43b1dd65 100644 --- a/notebooks/location/regionalized.ipynb +++ b/notebooks/location/regionalized.ipynb @@ -2791,7 +2791,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.9" }, "orig_nbformat": 4 }, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 9f4e138b736..61dcd778eff 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -93,6 +93,23 @@ def test_df_construct_from_dict(): ) +def test_df_construct_inline_respects_location(): + import bigframes.pandas as bpd + + bpd.close_session() + bpd.options.bigquery.location = "europe-west1" + + df = bpd.DataFrame([[1, 2, 3], [4, 5, 6]]) + repr(df) + + table = bpd.get_global_session().bqclient.get_table(df.query_job.destination) + assert table.location == "europe-west1" + + # Reset global session + bpd.close_session() + bpd.options.bigquery.location = "us" + + def test_get_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 2e2252be065..aba4a52c437 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -369,6 +369,17 @@ def test_read_pandas(session, scalars_dfs): pd.testing.assert_frame_equal(result, expected) +def test_read_pandas_inline_respects_location(): + options = bigframes.BigQueryOptions(location="europe-west1") + session = bigframes.Session(options) + + df = session.read_pandas(pd.DataFrame([[1, 2, 3], [4, 5, 6]])) + repr(df) + + table = session.bqclient.get_table(df.query_job.destination) + assert table.location == "europe-west1" + + def test_read_pandas_col_label_w_space(session: bigframes.Session): expected = pd.DataFrame( { diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py index 5a4f0951d3d..0bb5e0101a4 100644 --- a/tests/unit/core/test_blocks.py +++ b/tests/unit/core/test_blocks.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest import mock + import pandas import pandas.testing import pytest +import bigframes import bigframes.core.blocks as blocks @@ -74,8 +77,12 @@ ) def test_block_from_local(data): expected = pandas.DataFrame(data) + mock_session = mock.create_autospec(spec=bigframes.Session) + + # hard-coded the returned dimension of the session for that each of the test case contains 3 rows. + mock_session._execute.return_value = (iter([[3]]), None) - block = blocks.Block.from_local(data) + block = blocks.Block.from_local(data, mock_session) pandas.testing.assert_index_equal(block.column_labels, expected.columns) assert tuple(block.index.names) == tuple(expected.index.names) From 8d8294544ac7fedaca753c5473e3ca2a27868420 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:33:08 -0700 Subject: [PATCH 350/921] feat: add ml PCA.detect_anomalies method (#422) * feat: add ml detect_anomalies * add PCA.detect_anomalies * fix mypy --- bigframes/ml/core.py | 18 +++++++++---- bigframes/ml/decomposition.py | 28 +++++++++++++++++++ bigframes/ml/imported.py | 5 ++-- bigframes/ml/remote.py | 5 ++-- bigframes/ml/sql.py | 8 ++++++ tests/system/small/ml/test_core.py | 30 +++++++++++++++++++-- tests/system/small/ml/test_decomposition.py | 26 +++++++++++++++++- tests/unit/ml/test_sql.py | 20 ++++++++++++-- 8 files changed, 124 insertions(+), 16 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 24997708fb4..43a882ecac3 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -128,14 +128,12 @@ def model(self) -> bigquery.Model: return self._model def predict(self, input_data: bpd.DataFrame) -> bpd.DataFrame: - # TODO: validate input data schema return self._apply_sql( input_data, self._model_manipulation_sql_generator.ml_predict, ) def transform(self, input_data: bpd.DataFrame) -> bpd.DataFrame: - # TODO: validate input data schema return self._apply_sql( input_data, self._model_manipulation_sql_generator.ml_transform, @@ -146,7 +144,6 @@ def generate_text( input_data: bpd.DataFrame, options: Mapping[str, int | float], ) -> bpd.DataFrame: - # TODO: validate input data schema return self._apply_sql( input_data, lambda source_df: self._model_manipulation_sql_generator.ml_generate_text( @@ -160,7 +157,6 @@ def generate_text_embedding( input_data: bpd.DataFrame, options: Mapping[str, int | float], ) -> bpd.DataFrame: - # TODO: validate input data schema return self._apply_sql( input_data, lambda source_df: self._model_manipulation_sql_generator.ml_generate_text_embedding( @@ -169,12 +165,24 @@ def generate_text_embedding( ), ) + def detect_anomalies( + self, input_data: bpd.DataFrame, options: Mapping[str, int | float] + ) -> bpd.DataFrame: + assert self._model.model_type in ("PCA", "KMEANS", "ARIMA_PLUS") + + return self._apply_sql( + input_data, + lambda source_df: self._model_manipulation_sql_generator.ml_detect_anomalies( + source_df=source_df, + struct_options=options, + ), + ) + def forecast(self, options: Mapping[str, int | float]) -> bpd.DataFrame: sql = self._model_manipulation_sql_generator.ml_forecast(struct_options=options) return self._session.read_gbq(sql, index_col="forecast_timestamp").reset_index() def evaluate(self, input_data: Optional[bpd.DataFrame] = None): - # TODO: validate input data schema sql = self._model_manipulation_sql_generator.ml_evaluate(input_data) return self._session.read_gbq(sql) diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index f2b7c97994d..2714664dcec 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -110,6 +110,34 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: return self._bqml_model.predict(X) + def detect_anomalies( + self, X: Union[bpd.DataFrame, bpd.Series], *, contamination=0.1 + ) -> bpd.DataFrame: + """Detect the anomaly data points of the input. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or a DataFrame to detect anomalies. + contamination (float, default 0.1): + Identifies the proportion of anomalies in the training dataset that are used to create the model. + The value must be in the range [0, 0.5]. + + Returns: + bigframes.dataframe.DataFrame: detected DataFrame.""" + if contamination < 0.0 or contamination > 0.5: + raise ValueError( + f"contamination must be [0.0, 0.5], but is {contamination}." + ) + + if not self._bqml_model: + raise RuntimeError("A model must be fitted before detect_anomalies") + + (X,) = utils.convert_to_dataframe(X) + + return self._bqml_model.detect_anomalies( + X, options={"contamination": contamination} + ) + def to_gbq(self, model_name: str, replace: bool = False) -> PCA: """Save the model to BigQuery. diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index 98b23931f3d..7f758270830 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -23,7 +23,6 @@ import bigframes from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils -from bigframes.ml.globals import _SUPPORTED_DTYPES import bigframes.pandas as bpd @@ -236,9 +235,9 @@ def _create_bqml_model(self): else: for io in (self.input, self.output): for v in io.values(): - if v not in _SUPPORTED_DTYPES: + if v not in globals._SUPPORTED_DTYPES: raise ValueError( - f"field_type {v} is not supported. We only support {', '.join(_SUPPORTED_DTYPES)}." + f"field_type {v} is not supported. We only support {', '.join(globals._SUPPORTED_DTYPES)}." ) return self._bqml_model_factory.create_xgboost_imported_model( diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index a4a95b39d11..2b83382e68e 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -23,7 +23,6 @@ from bigframes import clients from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils -from bigframes.ml.globals import _SUPPORTED_DTYPES import bigframes.pandas as bpd _REMOTE_MODEL_STATUS = "remote_model_status" @@ -102,9 +101,9 @@ def standardize_type(v: str): v = v.lower() v = v.replace("boolean", "bool") - if v not in _SUPPORTED_DTYPES: + if v not in globals._SUPPORTED_DTYPES: raise ValueError( - f"Data type {v} is not supported. We only support {', '.join(_SUPPORTED_DTYPES)}." + f"Data type {v} is not supported. We only support {', '.join(globals._SUPPORTED_DTYPES)}." ) return v diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 7999cb90a3e..fa74458e775 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -276,6 +276,14 @@ def ml_generate_text_embedding( return f"""SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `{self._model_name}`, ({self._source_sql(source_df)}), {struct_options_sql})""" + def ml_detect_anomalies( + self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] + ) -> str: + """Encode ML.DETECT_ANOMALIES for BQML""" + struct_options_sql = self.struct_options(**struct_options) + return f"""SELECT * FROM ML.DETECT_ANOMALIES(MODEL `{self._model_name}`, + {struct_options_sql}, ({self._source_sql(source_df)}))""" + # ML evaluation TVFs def ml_evaluate(self, source_df: Optional[bpd.DataFrame] = None) -> str: """Encode ML.EVALUATE for BQML""" diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index d20867a2d77..02030cd31e7 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -289,6 +289,29 @@ def test_model_predict_with_unnamed_index( ) +def test_model_detect_anomalies( + penguins_bqml_pca_model: core.BqmlModel, new_penguins_df +): + options = {"contamination": 0.25} + anomalies = penguins_bqml_pca_model.detect_anomalies( + new_penguins_df, options + ).to_pandas() + expected = pd.DataFrame( + { + "is_anomaly": [True, True, True], + "mean_squared_error": [0.254188, 0.731243, 0.298889], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + pd.testing.assert_frame_equal( + anomalies[["is_anomaly", "mean_squared_error"]].sort_index(), + expected, + check_exact=False, + check_dtype=False, + rtol=0.1, + ) + + def test_remote_model_predict( bqml_linear_remote_model: core.BqmlModel, new_penguins_df ): @@ -367,16 +390,19 @@ def test_model_forecast(time_series_bqml_arima_plus_model: core.BqmlModel): ) -def test_model_register(ephemera_penguins_bqml_linear_model): +def test_model_register(ephemera_penguins_bqml_linear_model: core.BqmlModel): model = ephemera_penguins_bqml_linear_model model.register() + assert model.model.model_id is not None model_name = "bigframes_" + model.model.model_id # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. assert model_name in model.model.training_runs[-1]["vertexAiModelId"] -def test_model_register_with_params(ephemera_penguins_bqml_linear_model): +def test_model_register_with_params( + ephemera_penguins_bqml_linear_model: core.BqmlModel, +): model_name = "bigframes_system_test_model" model = ephemera_penguins_bqml_linear_model model.register(model_name) diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index 9565b8f7a85..72fdc6d951c 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -15,10 +15,13 @@ import pandas as pd from bigframes.ml import decomposition +import bigframes.pandas as bpd import tests.system.utils -def test_pca_predict(penguins_pca_model, new_penguins_df): +def test_pca_predict( + penguins_pca_model: decomposition.PCA, new_penguins_df: bpd.DataFrame +): predictions = penguins_pca_model.predict(new_penguins_df).to_pandas() expected = pd.DataFrame( { @@ -35,6 +38,27 @@ def test_pca_predict(penguins_pca_model, new_penguins_df): ) +def test_pca_detect_anomalies( + penguins_pca_model: decomposition.PCA, new_penguins_df: bpd.DataFrame +): + anomalies = penguins_pca_model.detect_anomalies(new_penguins_df).to_pandas() + expected = pd.DataFrame( + { + "is_anomaly": [False, True, False], + "mean_squared_error": [0.254188, 0.731243, 0.298889], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal( + anomalies[["is_anomaly", "mean_squared_error"]].sort_index(), + expected, + check_exact=False, + check_dtype=False, + rtol=0.1, + ) + + def test_pca_score(penguins_pca_model: decomposition.PCA): result = penguins_pca_model.score().to_pandas() expected = pd.DataFrame( diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 52c10f31444..913bab0379d 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -341,9 +341,8 @@ def test_ml_centroids_correct( ) -def test_forecast_correct_sql( +def test_ml_forecast_correct_sql( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, - mock_df: bpd.DataFrame, ): sql = model_manipulation_sql_generator.ml_forecast( struct_options={"option_key1": 1, "option_key2": 2.2}, @@ -391,6 +390,23 @@ def test_ml_generate_text_embedding_correct( ) +def test_ml_detect_anomalies_correct_sql( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, + mock_df: bpd.DataFrame, +): + sql = model_manipulation_sql_generator.ml_detect_anomalies( + source_df=mock_df, + struct_options={"option_key1": 1, "option_key2": 2.2}, + ) + assert ( + sql + == """SELECT * FROM ML.DETECT_ANOMALIES(MODEL `my_project_id.my_dataset_id.my_model_id`, + STRUCT( + 1 AS option_key1, + 2.2 AS option_key2), (input_X_sql))""" + ) + + def test_ml_principal_components_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, ): From 6df28ed704552ebec7869e1f2034614cb6407098 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:26:16 -0700 Subject: [PATCH 351/921] feat: add detect_anomalies to ml ARIMAPlus and KMeans models (#426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/cluster.py | 28 +++++++++ bigframes/ml/decomposition.py | 2 +- bigframes/ml/forecasting.py | 30 +++++++++ tests/system/small/ml/test_cluster.py | 45 +++++++++++++ tests/system/small/ml/test_decomposition.py | 23 +++++++ tests/system/small/ml/test_forecasting.py | 70 +++++++++++++++++---- 6 files changed, 185 insertions(+), 13 deletions(-) diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 360ab014532..c294d1f4249 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -96,6 +96,34 @@ def predict( return self._bqml_model.predict(X) + def detect_anomalies( + self, X: Union[bpd.DataFrame, bpd.Series], *, contamination: float = 0.1 + ) -> bpd.DataFrame: + """Detect the anomaly data points of the input. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or a DataFrame to detect anomalies. + contamination (float, default 0.1): + Identifies the proportion of anomalies in the training dataset that are used to create the model. + The value must be in the range [0, 0.5]. + + Returns: + bigframes.dataframe.DataFrame: detected DataFrame.""" + if contamination < 0.0 or contamination > 0.5: + raise ValueError( + f"contamination must be [0.0, 0.5], but is {contamination}." + ) + + if not self._bqml_model: + raise RuntimeError("A model must be fitted before detect_anomalies") + + (X,) = utils.convert_to_dataframe(X) + + return self._bqml_model.detect_anomalies( + X, options={"contamination": contamination} + ) + def to_gbq(self, model_name: str, replace: bool = False) -> KMeans: """Save the model to BigQuery. diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 2714664dcec..9dc60be78f4 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -111,7 +111,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: return self._bqml_model.predict(X) def detect_anomalies( - self, X: Union[bpd.DataFrame, bpd.Series], *, contamination=0.1 + self, X: Union[bpd.DataFrame, bpd.Series], *, contamination: float = 0.1 ) -> bpd.DataFrame: """Detect the anomaly data points of the input. diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 0c33660475e..18380328c72 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -119,6 +119,36 @@ def predict( options={"horizon": horizon, "confidence_level": confidence_level} ) + def detect_anomalies( + self, + X: Union[bpd.DataFrame, bpd.Series], + *, + anomaly_prob_threshold: float = 0.95, + ) -> bpd.DataFrame: + """Detect the anomaly data points of the input. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Series or a DataFrame to detect anomalies. + anomaly_prob_threshold (float, default 0.95): + Identifies the custom threshold to use for anomaly detection. The value must be in the range [0, 1), with a default value of 0.95. + + Returns: + bigframes.dataframe.DataFrame: detected DataFrame.""" + if anomaly_prob_threshold < 0.0 or anomaly_prob_threshold >= 1.0: + raise ValueError( + f"anomaly_prob_threshold must be [0.0, 1.0), but is {anomaly_prob_threshold}." + ) + + if not self._bqml_model: + raise RuntimeError("A model must be fitted before detect_anomalies") + + (X,) = utils.convert_to_dataframe(X) + + return self._bqml_model.detect_anomalies( + X, options={"anomaly_prob_threshold": anomaly_prob_threshold} + ) + def score( self, X: Union[bpd.DataFrame, bpd.Series], diff --git a/tests/system/small/ml/test_cluster.py b/tests/system/small/ml/test_cluster.py index a9fec0bbce0..96066e5fbea 100644 --- a/tests/system/small/ml/test_cluster.py +++ b/tests/system/small/ml/test_cluster.py @@ -15,6 +15,7 @@ import pandas as pd from bigframes.ml import cluster +import bigframes.pandas as bpd from tests.system.utils import assert_pandas_df_equal _PD_NEW_PENGUINS = pd.DataFrame.from_dict( @@ -73,6 +74,50 @@ def test_kmeans_predict(session, penguins_kmeans_model: cluster.KMeans): assert_pandas_df_equal(result, expected, ignore_order=True) +def test_kmeans_detect_anomalies( + penguins_kmeans_model: cluster.KMeans, new_penguins_df: bpd.DataFrame +): + anomalies = penguins_kmeans_model.detect_anomalies(new_penguins_df).to_pandas() + expected = pd.DataFrame( + { + "is_anomaly": [False, False, False], + "normalized_distance": [1.082937, 0.77139, 0.478304], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal( + anomalies[["is_anomaly", "normalized_distance"]].sort_index(), + expected, + check_exact=False, + check_dtype=False, + rtol=0.1, + ) + + +def test_kmeans_detect_anomalies_params( + penguins_kmeans_model: cluster.KMeans, new_penguins_df: bpd.DataFrame +): + anomalies = penguins_kmeans_model.detect_anomalies( + new_penguins_df, contamination=0.4 + ).to_pandas() + expected = pd.DataFrame( + { + "is_anomaly": [True, False, False], + "normalized_distance": [1.082937, 0.77139, 0.478304], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal( + anomalies[["is_anomaly", "normalized_distance"]].sort_index(), + expected, + check_exact=False, + check_dtype=False, + rtol=0.1, + ) + + def test_kmeans_score(session, penguins_kmeans_model: cluster.KMeans): new_penguins = session.read_pandas(_PD_NEW_PENGUINS) result = penguins_kmeans_model.score(new_penguins).to_pandas() diff --git a/tests/system/small/ml/test_decomposition.py b/tests/system/small/ml/test_decomposition.py index 72fdc6d951c..9eb9b25ea17 100644 --- a/tests/system/small/ml/test_decomposition.py +++ b/tests/system/small/ml/test_decomposition.py @@ -59,6 +59,29 @@ def test_pca_detect_anomalies( ) +def test_pca_detect_anomalies_params( + penguins_pca_model: decomposition.PCA, new_penguins_df: bpd.DataFrame +): + anomalies = penguins_pca_model.detect_anomalies( + new_penguins_df, contamination=0.2 + ).to_pandas() + expected = pd.DataFrame( + { + "is_anomaly": [False, True, True], + "mean_squared_error": [0.254188, 0.731243, 0.298889], + }, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal( + anomalies[["is_anomaly", "mean_squared_error"]].sort_index(), + expected, + check_exact=False, + check_dtype=False, + rtol=0.1, + ) + + def test_pca_score(penguins_pca_model: decomposition.PCA): result = penguins_pca_model.score().to_pandas() expected = pd.DataFrame( diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index 4726d5ab217..7fef1895502 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -35,7 +35,9 @@ ] -def test_model_predict_default(time_series_arima_plus_model: forecasting.ARIMAPlus): +def test_arima_plus_predict_default( + time_series_arima_plus_model: forecasting.ARIMAPlus, +): utc = pytz.utc predictions = time_series_arima_plus_model.predict().to_pandas() assert predictions.shape == (3, 8) @@ -63,7 +65,7 @@ def test_model_predict_default(time_series_arima_plus_model: forecasting.ARIMAPl ) -def test_model_predict_params(time_series_arima_plus_model: forecasting.ARIMAPlus): +def test_arima_plus_predict_params(time_series_arima_plus_model: forecasting.ARIMAPlus): utc = pytz.utc predictions = time_series_arima_plus_model.predict( horizon=4, confidence_level=0.9 @@ -94,7 +96,55 @@ def test_model_predict_params(time_series_arima_plus_model: forecasting.ARIMAPlu ) -def test_model_score( +def test_arima_plus_detect_anomalies( + time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +): + anomalies = time_series_arima_plus_model.detect_anomalies( + new_time_series_df + ).to_pandas() + + expected = pd.DataFrame( + { + "is_anomaly": [False, False, False], + "lower_bound": [2349.301736, 2153.614829, 1849.040192], + "upper_bound": [3099.642833, 3033.12195, 2858.185876], + "anomaly_probability": [0.757824, 0.322559, 0.43011], + }, + ) + pd.testing.assert_frame_equal( + anomalies[["is_anomaly", "lower_bound", "upper_bound", "anomaly_probability"]], + expected, + rtol=0.1, + check_index_type=False, + check_dtype=False, + ) + + +def test_arima_plus_detect_anomalies_params( + time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +): + anomalies = time_series_arima_plus_model.detect_anomalies( + new_time_series_df, anomaly_prob_threshold=0.7 + ).to_pandas() + + expected = pd.DataFrame( + { + "is_anomaly": [True, False, False], + "lower_bound": [2525.5363, 2360.1870, 2086.0609], + "upper_bound": [2923.408256, 2826.54981, 2621.165188], + "anomaly_probability": [0.757824, 0.322559, 0.43011], + }, + ) + pd.testing.assert_frame_equal( + anomalies[["is_anomaly", "lower_bound", "upper_bound", "anomaly_probability"]], + expected, + rtol=0.1, + check_index_type=False, + check_dtype=False, + ) + + +def test_arima_plus_score( time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df ): result = time_series_arima_plus_model.score( @@ -118,16 +168,14 @@ def test_model_score( ) -def test_model_summary( - time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df -): +def test_arima_plus_summary(time_series_arima_plus_model: forecasting.ARIMAPlus): result = time_series_arima_plus_model.summary() assert result.shape == (1, 12) assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) -def test_model_summary_show_all_candidates( - time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df +def test_arima_plus_summary_show_all_candidates( + time_series_arima_plus_model: forecasting.ARIMAPlus, ): result = time_series_arima_plus_model.summary( show_all_candidate_models=True, @@ -136,7 +184,7 @@ def test_model_summary_show_all_candidates( assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) -def test_model_score_series( +def test_arima_plus_score_series( time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df ): result = time_series_arima_plus_model.score( @@ -160,9 +208,7 @@ def test_model_score_series( ) -def test_model_summary_series( - time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df -): +def test_arima_plus_summary_series(time_series_arima_plus_model: forecasting.ARIMAPlus): result = time_series_arima_plus_model.summary() assert result.shape == (1, 12) assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) From a9a5e6af52f41c4662fba16ca1875855cea67d3a Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:18:26 -0700 Subject: [PATCH 352/921] chore: update genai notebooks to Gemini (#429) --- .../bq_dataframes_llm_code_generation.ipynb | 531 ++++++++++- .../bq_dataframes_llm_kmeans.ipynb | 10 +- ...q_dataframes_ml_drug_name_generation.ipynb | 881 +++++++++++++++--- .../generative_ai/large_language_models.ipynb | 132 ++- noxfile.py | 1 + 5 files changed, 1356 insertions(+), 199 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index b2966c404c0..74a0d7b2068 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "id": "ur8xi4C7S06n" }, @@ -140,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "id": "2b4ef9b72d43" }, @@ -204,11 +204,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "id": "oM1iC_MfAts1" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1;31mERROR:\u001b[0m (gcloud.config.set) argument VALUE: Must be specified.\n", + "Usage: gcloud config set SECTION/PROPERTY VALUE [optional flags]\n", + " optional flags may be --help | --installation\n", + "\n", + "For detailed information on this command and its flags, run:\n", + " gcloud config set --help\n" + ] + } + ], "source": [ "PROJECT_ID = \"\" # @param {type:\"string\"}\n", "\n", @@ -229,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "id": "eF-Twtc4XGem" }, @@ -273,7 +286,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "id": "254614fa0c46" }, @@ -295,7 +308,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "id": "603adbbf0532" }, @@ -316,7 +329,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "id": "PyQmSRbKA8r-" }, @@ -338,7 +351,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "id": "NPPMuw2PXGeo" }, @@ -383,15 +396,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "id": "sdjeXFwcHfl7" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job a3897125-4272-4817-a0e6-8e1a9e022b93 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "from bigframes.ml.llm import PaLM2TextGenerator\n", + "from bigframes.ml.llm import GeminiTextGenerator\n", "\n", - "model = PaLM2TextGenerator()" + "model = GeminiTextGenerator()" ] }, { @@ -414,7 +440,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "id": "SchiTkQGIJog" }, @@ -435,22 +461,172 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "id": "QCqgVCIsGGuv" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job e186a7bf-813c-4c46-80c8-ae079c829841 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 868ef0e0-ef33-4f0c-8b47-401a82bfc288 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
API
0values
1dtypes
\n", + "

2 rows × 1 columns

\n", + "
[2 rows x 1 columns in total]" + ], + "text/plain": [ + " API\n", + "0 values\n", + "1 dtypes\n", + "\n", + "[2 rows x 1 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df_api.head(2)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "id": "BGJnZbgEGS5-" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job fcf9e1e9-cd3f-4a34-ba42-450c818bd6c7 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job c7b5f6a2-a5ca-4a4a-bcf3-9ddaa0a3777c is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
API
0shape
1size
\n", + "

2 rows × 1 columns

\n", + "
[2 rows x 1 columns in total]" + ], + "text/plain": [ + " API\n", + "0 shape\n", + "1 size\n", + "\n", + "[2 rows x 1 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "series_api.head(2)" ] @@ -481,11 +657,60 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "id": "EDAaIwHpQCDZ" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 67d4d2d9-dd57-4886-8bcb-68e9eb6e11e2 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 26124cec-8753-4b48-b467-5e17c2c3591e is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 9bdb0d90-60ec-4eec-96f4-990c3e1adef5 is DONE. 132 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 Generate Pandas sample code for DataFrame.values\n", + "1 Generate Pandas sample code for DataFrame.dtypes\n", + "Name: API, dtype: string" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df_prompt_prefix = \"Generate Pandas sample code for DataFrame.\"\n", "series_prompt_prefix = \"Generate Pandas sample code for Series.\"\n", @@ -511,11 +736,84 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": { "id": "6i6HkFJZa8na" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job f0199b1e-5524-48ba-81ec-89d70c28b5d0 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f1816195-25fa-4180-96ce-7917e9729428 is DONE. 584 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ce1ad8d4-3fcd-4ca9-9f9b-4be0cfdabde5 is DONE. 146 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 3b245a41-a86e-4773-aa14-8edaa821c6b7 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d3ceafe9-888d-4f5e-b7f3-c2218dae0736 is DONE. 904 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 930c2334-60ac-4ec1-8a06-2a4cf2d9dc1e is DONE. 226 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024)\n", "series_pred = model.predict(series_prompt.to_frame(), max_output_tokens=1024)" @@ -532,11 +830,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": { "id": "9A2gw6hP_2nX" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 10a766a8-7368-4a82-b239-764e1c13ed64 is DONE. 21.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "```python\n", + "import pandas as pd\n", + "\n", + "# Create a DataFrame\n", + "df = pd.DataFrame({\n", + " \"Name\": [\"John\", \"Mary\", \"Peter\"],\n", + " \"Age\": [20, 25, 30],\n", + " \"City\": [\"New York\", \"London\", \"Paris\"]\n", + "})\n", + "\n", + "# Get the values as a NumPy array\n", + "values = df.values\n", + "\n", + "# Print the values\n", + "print(values)\n", + "```\n" + ] + } + ], "source": [ "print(df_pred['ml_generate_text_llm_result'].iloc[0])" ] @@ -566,7 +899,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": { "id": "GskyyUQPowBT" }, @@ -595,11 +928,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": { "id": "PBlp-C-DOHRO" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cloud Function Name projects/bigframes-dev/locations/us-central1/functions/bigframes-3a8781216c4ccdded9eecfdbd72c63f2\n", + "Remote Function Name bigframes-dev._76f0f906c2e04e83c3496619541347a5922c80ee.bigframes_3a8781216c4ccdded9eecfdbd72c63f2\n" + ] + } + ], "source": [ "CLOUD_FUNCTION_NAME = format(extract_code.bigframes_cloud_function)\n", "print(\"Cloud Function Name \" + CLOUD_FUNCTION_NAME)\n", @@ -618,7 +960,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { "id": "bsQ9cmoWo0Ps" }, @@ -639,11 +981,45 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": { "id": "7yWzjhGy_zcy" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 96bea1ea-9c98-42e9-8f6d-a2b6cdeaf17a is DONE. 21.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "import bigframes.pandas as bf\n", + "\n", + "# Create a DataFrame\n", + "df = pd.DataFrame({\n", + " \"Name\": [\"John\", \"Mary\", \"Peter\"],\n", + " \"Age\": [20, 25, 30],\n", + " \"City\": [\"New York\", \"London\", \"Paris\"]\n", + "})\n", + "\n", + "# Get the values as a NumPy array\n", + "values = df.values\n", + "\n", + "# Print the values\n", + "print(values)\n", + "\n" + ] + } + ], "source": [ "print(df_code['code'].iloc[0])" ] @@ -670,11 +1046,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "id": "-J5LHgS6LLZ0" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating gs://code-samples-d1d466b7-dfe6-11ee-b86e-4201c0a82d52/...\n" + ] + } + ], "source": [ "import uuid\n", "BUCKET_ID = \"code-samples-\" + str(uuid.uuid1())\n", @@ -693,11 +1077,60 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": { "id": "Zs_b5L-4IvER" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 72fe0ca6-2f37-457f-9705-ce89b2a4c324 is DONE. 21.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 1b21be65-8761-4694-932e-8fa634569e56 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 510566f8-05f2-4455-8daa-f24feea0344e is DONE. 27.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 82c0e7f6-ce99-462c-a7d3-e760391f6677 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "df_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/df_code*.csv\")\n", "series_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/series_code*.csv\")" @@ -716,11 +1149,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": { "id": "PspCXu-qu_ND" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://console.developers.google.com/storage/browser/code-samples-d1d466b7-dfe6-11ee-b86e-4201c0a82d52/\n" + ] + } + ], "source": [ "print(f'https://console.developers.google.com/storage/browser/{BUCKET_ID}/')" ] @@ -754,7 +1195,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "id": "yw7A461XLjvW" }, @@ -770,7 +1211,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": { "id": "sx_vKniMq9ZX" }, @@ -785,7 +1226,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": { "id": "iQFo6OUBLmi3" }, @@ -805,6 +1246,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.9" } }, "nbformat": 4, diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 14a681a6933..221933c2f81 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -61,7 +61,7 @@ "\n", "1. Use PaLM2TextEmbeddingGenerator to [generate text embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) for each of 10000 complaints sent to an online bank. If you're not familiar with what a text embedding is, it's a list of numbers that are like coordinates in an imaginary \"meaning space\" for sentences. (It's like [word embeddings](https://en.wikipedia.org/wiki/Word_embedding), but for more general text.) The important point for our purposes is that similar sentences are close to each other in this imaginary space.\n", "2. Use KMeans clustering to group together complaints whose text embeddings are near to eachother. This will give us sets of similar complaints, but we don't yet know _why_ these complaints are similar.\n", - "3. Prompt PaLM2TextGenerator in English asking what the difference is between the groups of complaints that we got. Thanks to the power of modern LLMs, the response might give us a very good idea of what these complaints are all about, but remember to [\"understand the limits of your dataset and model.\"](https://ai.google/responsibility/responsible-ai-practices/#:~:text=Understand%20the%20limitations%20of%20your%20dataset%20and%20model)\n", + "3. Prompt GeminiTextGenerator in English asking what the difference is between the groups of complaints that we got. Thanks to the power of modern LLMs, the response might give us a very good idea of what these complaints are all about, but remember to [\"understand the limits of your dataset and model.\"](https://ai.google/responsibility/responsible-ai-practices/#:~:text=Understand%20the%20limitations%20of%20your%20dataset%20and%20model)\n", "\n", "We will tie these pieces together in Python using BigQuery DataFrames. [Click here](https://cloud.google.com/bigquery/docs/dataframes-quickstart) to learn more about BigQuery DataFrames!" ] @@ -894,7 +894,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Build prompts - we will choose just two of our categories and prompt PaLM2TextGenerator to identify their salient characteristics. The prompt is natural language in a python string." + "Build prompts - we will choose just two of our categories and prompt GeminiTextGenerator to identify their salient characteristics. The prompt is natural language in a python string." ] }, { @@ -1121,9 +1121,9 @@ } ], "source": [ - "from bigframes.ml.llm import PaLM2TextGenerator\n", + "from bigframes.ml.llm import GeminiTextGenerator\n", "\n", - "q_a_model = PaLM2TextGenerator()" + "q_a_model = GeminiTextGenerator()" ] }, { @@ -1216,7 +1216,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We now see PaLM2TextGenerator's characterization of the different comment groups. Thanks for using BigQuery DataFrames!" + "We now see GeminiTextGenerator's characterization of the different comment groups. Thanks for using BigQuery DataFrames!" ] }, { diff --git a/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb b/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb index 52a1c4e7683..8c0b1b00387 100644 --- a/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "id": "ur8xi4C7S06n" }, @@ -92,7 +92,7 @@ "1. Use `bigframes` to query the FDA dataset of over 100,000 drugs, filtered on the brand name, generic name, and indications & usage columns.\n", "1. Filter this dataset to find prototypical brand names that can be used as examples in prompt tuning.\n", "1. Create a prompt with the user input, general instructions, examples and counter-examples for the desired brand name.\n", - "1. Use the `bigframes.ml.llm.PaLM2TextGenerator` to generate choices of brand names." + "1. Use the `bigframes.ml.llm.GeminiTextGenerator` to generate choices of brand names." ] }, { @@ -138,13 +138,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "id": "2b4ef9b72d43" }, "outputs": [], "source": [ - "!pip install -U --quiet bigframes" + "# !pip install -U --quiet bigframes" ] }, { @@ -158,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "id": "f200f10a1da3" }, @@ -182,15 +182,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "id": "PyQmSRbKA8r-" }, "outputs": [], "source": [ "import bigframes.pandas as bpd\n", - "from google.cloud import bigquery_connection_v1 as bq_connection\n", - "from bigframes.ml.llm import PaLM2TextGenerator\n", + "from bigframes.ml.llm import GeminiTextGenerator\n", "from IPython.display import Markdown" ] }, @@ -226,7 +225,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "id": "254614fa0c46" }, @@ -246,7 +245,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "id": "603adbbf0532" }, @@ -294,13 +293,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "id": "oM1iC_MfAts1" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1;31mERROR:\u001b[0m (gcloud.config.set) argument VALUE: Must be specified.\n", + "Usage: gcloud config set SECTION/PROPERTY VALUE [optional flags]\n", + " optional flags may be --help | --installation\n", + "\n", + "For detailed information on this command and its flags, run:\n", + " gcloud config set --help\n" + ] + } + ], "source": [ - "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "# Please fill in these values.\n", + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", "\n", "# Set the project id\n", "! gcloud config set project {PROJECT_ID}" @@ -320,17 +333,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "id": "G1vVsPiMsL2X" }, "outputs": [], "source": [ "# Please fill in these values.\n", - "LOCATION = \"us\" # @param {type:\"string\"}\n", - "CONNECTION = \"\" # @param {type:\"string\"}\n", - "\n", - "connection_name = f\"{PROJECT_ID}.{LOCATION}.{CONNECTION}\"" + "LOCATION = \"us\" # @param {type:\"string\"}" ] }, { @@ -342,50 +352,6 @@ "We will now try to use the provided connection, and if it doesn't exist, create a new one. We will also print the service account used." ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "56Hw42m6kFrj" - }, - "outputs": [], - "source": [ - "# Initialize client and set request parameters\n", - "client = bq_connection.ConnectionServiceClient()\n", - "new_conn_parent = f\"projects/{PROJECT_ID}/locations/{LOCATION}\"\n", - "exists_conn_parent = f\"projects/{PROJECT_ID}/locations/{LOCATION}/connections/{CONNECTION}\"\n", - "cloud_resource_properties = bq_connection.CloudResourceProperties({})\n", - "\n", - "# Try to connect using provided connection\n", - "try:\n", - " request = client.get_connection(\n", - " request=bq_connection.GetConnectionRequest(name=exists_conn_parent)\n", - " )\n", - " CONN_SERVICE_ACCOUNT = f\"serviceAccount:{request.cloud_resource.service_account_id}\"\n", - "# Create a new connection on error\n", - "except Exception:\n", - " connection = bq_connection.types.Connection(\n", - " {\"friendly_name\": CONNECTION, \"cloud_resource\": cloud_resource_properties}\n", - " )\n", - " request = bq_connection.CreateConnectionRequest(\n", - " {\n", - " \"parent\": new_conn_parent,\n", - " \"connection_id\": CONNECTION,\n", - " \"connection\": connection,\n", - " }\n", - " )\n", - " response = client.create_connection(request)\n", - " CONN_SERVICE_ACCOUNT = (\n", - " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", - " )\n", - "# Set service account permissions\n", - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", - "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'\n", - "\n", - "print(CONN_SERVICE_ACCOUNT)" - ] - }, { "cell_type": "markdown", "metadata": { @@ -399,7 +365,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "id": "OCccLirpkSRz" }, @@ -422,7 +388,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "id": "oxphj2gnuKou" }, @@ -445,11 +411,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "id": "0knz5ZWMzed-" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Provide 10 unique and modern brand names in Markdown bullet point format. Do not provide any additional explanation.\n", + "\n", + "Be creative with the brand names. Don't use English words directly; use variants or invented words.\n", + "\n", + "The generic name is: Entropofloxacin\n", + "\n", + "The indications and usage are: Entropofloxacin is a fluoroquinolone antibiotic that is used to treat a variety of bacterial infections, including: pneumonia, streptococcus infections, salmonella infections, escherichia coli infections, and pseudomonas aeruginosa infections It is taken by mouth or by injection. The dosage and frequency of administration will vary depending on the type of infection being treated. It should be taken for the full course of treatment, even if symptoms improve after a few days. Stopping the medication early may increase the risk of the infection coming back..\n" + ] + } + ], "source": [ "zero_shot_prompt = f\"\"\"Provide {NUM_NAMES} unique and modern brand names in Markdown bullet point format. Do not provide any additional explanation.\n", "\n", @@ -464,19 +444,15 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "LCRE2L720f5y" - }, + "metadata": {}, "source": [ - "Next, let's create a helper function to predict with our model. It will take a string input, and add it to a temporary BigFrames `DataFrame`. It will also return the string extracted from the response `DataFrame`." + "Next, let's create a helper function to predict with our model. It will take a string input, and add it to a temporary BigFrames DataFrame. It will also return the string extracted from the response DataFrame." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "LB3xgDroIxlx" - }, + "execution_count": 12, + "metadata": {}, "outputs": [], "source": [ "def predict(prompt: str, temperature: float = TEMPERATURE) -> str:\n", @@ -488,7 +464,7 @@ " )\n", "\n", " # Return response\n", - " return model.predict(input, temperature).ml_generate_text_llm_result.iloc[0]" + " return model.predict(input, temperature=temperature).ml_generate_text_llm_result.iloc[0]" ] }, { @@ -502,20 +478,100 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": { "id": "UW2fQ2k5Hsic" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 25b47284-2b28-4cd9-ac9a-90379f818c84 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 0efa6f42-6569-4274-ac21-667c7eecefc7 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job c5e98170-7d58-4aa2-a3a3-6680cd9a54c0 is DONE. 8 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5fd9d5bf-c731-4b21-b7c9-9b6244ffb412 is DONE. 2 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 36f7e8ec-ee42-4f94-8e38-bdf18b371517 is DONE. 118 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "- Etherealox\n", + "- Zenithrox\n", + "- Aureox\n", + "- Lucentrox\n", + "- Aethrox\n", + "- Luminex\n", + "- Elysirox\n", + "- Quasarox\n", + "- Novaflux\n", + "- Arcanox" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Get BigFrames session\n", - "session = bpd.get_global_session()\n", - "\n", "# Define the model\n", - "model = PaLM2TextGenerator(session=session, connection_name=connection_name)\n", + "model = GeminiTextGenerator()\n", "\n", "# Invoke LLM with prompt\n", - "response = predict(zero_shot_prompt)\n", + "response = predict(zero_shot_prompt, temperature = TEMPERATURE)\n", "\n", "# Print results as Markdown\n", "Markdown(response)" @@ -552,7 +608,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "id": "MXdI78SOElyt" }, @@ -574,11 +630,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": { "id": "aQ2iscnhF2cx" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Provide 10 unique and modern brand names in Markdown bullet point format, related to the drug at the bottom of this prompt.\n", + "\n", + "Be creative with the brand names. Don't use English words directly; use variants or invented words.\n", + "\n", + "First, we will provide 3 examples to help with your thought process.\n", + "\n", + "Then, we will provide the generic name and usage for the drug we'd like you to generate brand names for.\n", + "\n" + ] + } + ], "source": [ "prefix_prompt = f\"\"\"Provide {NUM_NAMES} unique and modern brand names in Markdown bullet point format, related to the drug at the bottom of this prompt.\n", "\n", @@ -605,11 +676,139 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": { "id": "IoO_Bp8wA07N" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 542b0ce1-9d56-456f-bcd3-d24a6f0c825a is DONE. 84.4 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2405ba41-b263-46d3-a0e5-3b5e7ecef6ab is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job b24663ec-8d81-4295-84df-ffb65a6a0f1b is DONE. 3.1 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openfda_generic_nameopenfda_brand_nameindications_and_usage
0BENZALKONIUM CHLORIDEmeijer kidsUse - hand washing to decrease bacteria on skin
3OCTINOXATE, TITANIUM DIOXIDECD DIORSKIN STAR Studio Makeup Spectacular Bri...Uses Helps prevent sunburn. If used as directe...
4TRIAMCINOLONE ACETONIDETriamcinolone AcetonideINDICATIONS AND USAGE Triamcinolone Acetonide ...
5BACITRACIN ZINC, NEOMYCIN SULFATE, POLYMYXIN B...Triple AntibioticFirst aid to help prevent infection in minor c...
6RISPERIDONERisperidone1. INDICATIONS AND USAGE Risperidone is an aty...
\n", + "

5 rows × 3 columns

\n", + "
[5 rows x 3 columns in total]" + ], + "text/plain": [ + " openfda_generic_name \\\n", + "0 BENZALKONIUM CHLORIDE \n", + "3 OCTINOXATE, TITANIUM DIOXIDE \n", + "4 TRIAMCINOLONE ACETONIDE \n", + "5 BACITRACIN ZINC, NEOMYCIN SULFATE, POLYMYXIN B... \n", + "6 RISPERIDONE \n", + "\n", + " openfda_brand_name \\\n", + "0 meijer kids \n", + "3 CD DIORSKIN STAR Studio Makeup Spectacular Bri... \n", + "4 Triamcinolone Acetonide \n", + "5 Triple Antibiotic \n", + "6 Risperidone \n", + "\n", + " indications_and_usage \n", + "0 Use - hand washing to decrease bacteria on skin \n", + "3 Uses Helps prevent sunburn. If used as directe... \n", + "4 INDICATIONS AND USAGE Triamcinolone Acetonide ... \n", + "5 First aid to help prevent infection in minor c... \n", + "6 1. INDICATIONS AND USAGE Risperidone is an aty... \n", + "\n", + "[5 rows x 3 columns]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Query 3 columns of interest from drug label dataset\n", "df = bpd.read_gbq(\"bigquery-public-data.fda_drug.drug_label\",\n", @@ -636,7 +835,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": { "id": "95WDe2eCCeLx" }, @@ -663,11 +862,89 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": { "id": "2ohZYg7QEyJV" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 293c90e0-7fdf-4769-9d8e-f222f35d368e is DONE. 84.4 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", + "
openfda_generic_nameopenfda_brand_nameindications_and_usage
81748AMPICILLIN SODIUMAmpicillinINDICATIONS AND USAGE Ampicillin for Injection...
730AZTREONAMCayston1 INDICATIONS AND USAGE CAYSTON® is indicated ...
71763TERAZOSIN HYDROCHLORIDETerazosinINDICATIONS AND USAGE Terazosin capsules are i...
\n", + "
" + ], + "text/plain": [ + " openfda_generic_name openfda_brand_name \\\n", + "81748 AMPICILLIN SODIUM Ampicillin \n", + "730 AZTREONAM Cayston \n", + "71763 TERAZOSIN HYDROCHLORIDE Terazosin \n", + "\n", + " indications_and_usage \n", + "81748 INDICATIONS AND USAGE Ampicillin for Injection... \n", + "730 1 INDICATIONS AND USAGE CAYSTON® is indicated ... \n", + "71763 INDICATIONS AND USAGE Terazosin capsules are i... " + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Take a sample and convert to a Pandas dataframe for local usage.\n", "df_examples = df.sample(NUM_EXAMPLES, random_state=3).to_pandas()\n", @@ -686,11 +963,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": { "id": "PcJdSaw0EGcW" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'brand_name': 'Ampicillin', 'generic_name': 'AMPICILLIN SODIUM', 'usage': 'INDICATIONS AND USAGE Ampicillin for Injection, USP is indicated in the treatment of infections caused by susceptible strains of the designated organisms in the following conditions: Respiratory Tract Infections caused by Streptococcus pneumoniae. Staphylococcus aureus (penicillinase and nonpenicillinase-producing), H. influenzae, and Group A beta-hemolytic streptococci. Bacterial Meningitis caused by E. coli, Group B streptococci, and other Gram-negative bacteria (Listeria monocytogenes, N. meningitidis). The addition of an aminoglycoside with ampicillin may increase its effectiveness against Gram-negative bacteria. Septicemia and Endocarditis caused by susceptible Gram-positive organisms including Streptococcus spp., penicillin G-susceptible staphylococci, and enterococci. Gram-negative sepsis caused by E. coli, Proteus mirabilis and Salmonella spp. responds to ampicillin. Endocarditis due to enterococcal strains usually respond to intravenous therapy. The addition of an aminoglycoside may enhance the effectiveness of ampicillin when treating streptococcal endocarditis. Urinary Tract Infections caused by sensitive strains of E. coli and Proteus mirabilis. Gastrointestinal Infections caused by Salmonella typhi (typhoid fever), other Salmonella spp., and Shigella spp. (dysentery) usually respond to oral or intravenous therapy. Bacteriology studies to determine the causative organisms and their susceptibility to ampicillin should be performed. Therapy may be instituted prior to obtaining results of susceptibility testing. It is advisable to reserve the parenteral form of this drug for moderately severe and severe infections and for patients who are unable to take the oral forms. A change to oral ampicillin may be made as soon as appropriate. To reduce the development of drug-resistant bacteria and maintain the effectiveness of Ampicillin for Injection, USP and other antibacterial drugs, Ampicillin for Injection, USP should be used only to treat or prevent infections that are proven or strongly suspected to be caused by susceptible bacteria. When culture and susceptibility information are available, they should be considered in selecting or modifying antibacterial therapy. In the absence of such data, local epidemiology and susceptibility patterns may contribute to the empiric selection of therapy. Indicated surgical procedures should be performed.'}, {'brand_name': 'Cayston', 'generic_name': 'AZTREONAM', 'usage': '1 INDICATIONS AND USAGE CAYSTON® is indicated to improve respiratory symptoms in cystic fibrosis (CF) patients with Pseudomonas aeruginosa. Safety and effectiveness have not been established in pediatric patients below the age of 7 years, patients with FEV1 <25% or >75% predicted, or patients colonized with Burkholderia cepacia [see Clinical Studies (14) ]. To reduce the development of drug-resistant bacteria and maintain the effectiveness of CAYSTON and other antibacterial drugs, CAYSTON should be used only to treat patients with CF known to have Pseudomonas aeruginosa in the lungs. CAYSTON is a monobactam antibacterial indicated to improve respiratory symptoms in cystic fibrosis (CF) patients with Pseudomonas aeruginosa. Safety and effectiveness have not been established in pediatric patients below the age of 7 years, patients with FEV1 <25% or >75% predicted, or patients colonized with Burkholderia cepacia. (1)'}, {'brand_name': 'Terazosin', 'generic_name': 'TERAZOSIN HYDROCHLORIDE', 'usage': 'INDICATIONS AND USAGE Terazosin capsules are indicated for the treatment of symptomatic benign prostatic hyperplasia (BPH). There is a rapid response, with approximately 70% of patients experiencing an increase in urinary flow and improvement in symptoms of BPH when treated with terazosin capsules. The long-term effects of terazosin capsules on the incidence of surgery, acute urinary obstruction or other complications of BPH are yet to be determined. Terazosin capsules are also indicated for the treatment of hypertension. Terazosin capsules can be used alone or in combination with other antihypertensive agents such as diuretics or beta-adrenergic blocking agents.'}]\n" + ] + } + ], "source": [ "examples = [\n", " {\n", @@ -719,11 +1004,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "id": "kzAVsF6wJ93S" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'Generic name: AMPICILLIN SODIUM\\nUsage: INDICATIONS AND USAGE Ampicillin for Injection, USP is indicated in the treatment of infections caused by susceptible strains of the designated organisms in the following conditions: Respiratory Tract Infections caused by Streptococcus pneumoniae. Staphylococcus aureus (penicillinase and nonpenicillinase-producing), H. influenzae, and Group A beta-hemolytic streptococci. Bacterial Meningitis caused by E. coli, Group B streptococci, and other Gram-negative bacteria (Listeria monocytogenes, N. meningitidis). The addition of an aminoglycoside with ampicillin may increase its effectiveness against Gram-negative bacteria. Septicemia and Endocarditis caused by susceptible Gram-positive organisms including Streptococcus spp., penicillin G-susceptible staphylococci, and enterococci. Gram-negative sepsis caused by E. coli, Proteus mirabilis and Salmonella spp. responds to ampicillin. Endocarditis due to enterococcal strains usually respond to intravenous therapy. The addition of an aminoglycoside may enhance the effectiveness of ampicillin when treating streptococcal endocarditis. Urinary Tract Infections caused by sensitive strains of E. coli and Proteus mirabilis. Gastrointestinal Infections caused by Salmonella typhi (typhoid fever), other Salmonella spp., and Shigella spp. (dysentery) usually respond to oral or intravenous therapy. Bacteriology studies to determine the causative organisms and their susceptibility to ampicillin should be performed. Therapy may be instituted prior to obtaining results of susceptibility testing. It is advisable to reserve the parenteral form of this drug for moderately severe and severe infections and for patients who are unable to take the oral forms. A change to oral ampicillin may be made as soon as appropriate. To reduce the development of drug-resistant bacteria and maintain the effectiveness of Ampicillin for Injection, USP and other antibacterial drugs, Ampicillin for Injection, USP should be used only to treat or prevent infections that are proven or strongly suspected to be caused by susceptible bacteria. When culture and susceptibility information are available, they should be considered in selecting or modifying antibacterial therapy. In the absence of such data, local epidemiology and susceptibility patterns may contribute to the empiric selection of therapy. Indicated surgical procedures should be performed.\\nBrand name: Ampicillin\\n\\nGeneric name: AZTREONAM\\nUsage: 1 INDICATIONS AND USAGE CAYSTON® is indicated to improve respiratory symptoms in cystic fibrosis (CF) patients with Pseudomonas aeruginosa. Safety and effectiveness have not been established in pediatric patients below the age of 7 years, patients with FEV1 <25% or >75% predicted, or patients colonized with Burkholderia cepacia [see Clinical Studies (14) ]. To reduce the development of drug-resistant bacteria and maintain the effectiveness of CAYSTON and other antibacterial drugs, CAYSTON should be used only to treat patients with CF known to have Pseudomonas aeruginosa in the lungs. CAYSTON is a monobactam antibacterial indicated to improve respiratory symptoms in cystic fibrosis (CF) patients with Pseudomonas aeruginosa. Safety and effectiveness have not been established in pediatric patients below the age of 7 years, patients with FEV1 <25% or >75% predicted, or patients colonized with Burkholderia cepacia. (1)\\nBrand name: Cayston\\n\\nGeneric name: TERAZOSIN HYDROCHLORIDE\\nUsage: INDICATIONS AND USAGE Terazosin capsules are indicated for the treatment of symptomatic benign prostatic hyperplasia (BPH). There is a rapid response, with approximately 70% of patients experiencing an increase in urinary flow and improvement in symptoms of BPH when treated with terazosin capsules. The long-term effects of terazosin capsules on the incidence of surgery, acute urinary obstruction or other complications of BPH are yet to be determined. Terazosin capsules are also indicated for the treatment of hypertension. Terazosin capsules can be used alone or in combination with other antihypertensive agents such as diuretics or beta-adrenergic blocking agents.\\nBrand name: Terazosin\\n\\n'" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "example_prompt = \"\"\n", "for example in examples:\n", @@ -743,11 +1039,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "metadata": { "id": "OYp6W_XfHTlo" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generic name: Entropofloxacin\n", + "Usage: Entropofloxacin is a fluoroquinolone antibiotic that is used to treat a variety of bacterial infections, including: pneumonia, streptococcus infections, salmonella infections, escherichia coli infections, and pseudomonas aeruginosa infections It is taken by mouth or by injection. The dosage and frequency of administration will vary depending on the type of infection being treated. It should be taken for the full course of treatment, even if symptoms improve after a few days. Stopping the medication early may increase the risk of the infection coming back.\n", + "Brand names:\n" + ] + } + ], "source": [ "suffix_prompt = f\"\"\"Generic name: {GENERIC_NAME}\n", "Usage: {USAGE}\n", @@ -767,11 +1073,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": { "id": "99xdU7l8C1h8" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Provide 10 unique and modern brand names in Markdown bullet point format, related to the drug at the bottom of this prompt.\n", + "\n", + "Be creative with the brand names. Don't use English words directly; use variants or invented words.\n", + "\n", + "First, we will provide 3 examples to help with your thought process.\n", + "\n", + "Then, we will provide the generic name and usage for the drug we'd like you to generate brand names for.\n", + "Generic name: AMPICILLIN SODIUM\n", + "Usage: INDICATIONS AND USAGE Ampicillin for Injection, USP is indicated in the treatment of infections caused by susceptible strains of the designated organisms in the following conditions: Respiratory Tract Infections caused by Streptococcus pneumoniae. Staphylococcus aureus (penicillinase and nonpenicillinase-producing), H. influenzae, and Group A beta-hemolytic streptococci. Bacterial Meningitis caused by E. coli, Group B streptococci, and other Gram-negative bacteria (Listeria monocytogenes, N. meningitidis). The addition of an aminoglycoside with ampicillin may increase its effectiveness against Gram-negative bacteria. Septicemia and Endocarditis caused by susceptible Gram-positive organisms including Streptococcus spp., penicillin G-susceptible staphylococci, and enterococci. Gram-negative sepsis caused by E. coli, Proteus mirabilis and Salmonella spp. responds to ampicillin. Endocarditis due to enterococcal strains usually respond to intravenous therapy. The addition of an aminoglycoside may enhance the effectiveness of ampicillin when treating streptococcal endocarditis. Urinary Tract Infections caused by sensitive strains of E. coli and Proteus mirabilis. Gastrointestinal Infections caused by Salmonella typhi (typhoid fever), other Salmonella spp., and Shigella spp. (dysentery) usually respond to oral or intravenous therapy. Bacteriology studies to determine the causative organisms and their susceptibility to ampicillin should be performed. Therapy may be instituted prior to obtaining results of susceptibility testing. It is advisable to reserve the parenteral form of this drug for moderately severe and severe infections and for patients who are unable to take the oral forms. A change to oral ampicillin may be made as soon as appropriate. To reduce the development of drug-resistant bacteria and maintain the effectiveness of Ampicillin for Injection, USP and other antibacterial drugs, Ampicillin for Injection, USP should be used only to treat or prevent infections that are proven or strongly suspected to be caused by susceptible bacteria. When culture and susceptibility information are available, they should be considered in selecting or modifying antibacterial therapy. In the absence of such data, local epidemiology and susceptibility patterns may contribute to the empiric selection of therapy. Indicated surgical procedures should be performed.\n", + "Brand name: Ampicillin\n", + "\n", + "Generic name: AZTREONAM\n", + "Usage: 1 INDICATIONS AND USAGE CAYSTON® is indicated to improve respiratory symptoms in cystic fibrosis (CF) patients with Pseudomonas aeruginosa. Safety and effectiveness have not been established in pediatric patients below the age of 7 years, patients with FEV1 <25% or >75% predicted, or patients colonized with Burkholderia cepacia [see Clinical Studies (14) ]. To reduce the development of drug-resistant bacteria and maintain the effectiveness of CAYSTON and other antibacterial drugs, CAYSTON should be used only to treat patients with CF known to have Pseudomonas aeruginosa in the lungs. CAYSTON is a monobactam antibacterial indicated to improve respiratory symptoms in cystic fibrosis (CF) patients with Pseudomonas aeruginosa. Safety and effectiveness have not been established in pediatric patients below the age of 7 years, patients with FEV1 <25% or >75% predicted, or patients colonized with Burkholderia cepacia. (1)\n", + "Brand name: Cayston\n", + "\n", + "Generic name: TERAZOSIN HYDROCHLORIDE\n", + "Usage: INDICATIONS AND USAGE Terazosin capsules are indicated for the treatment of symptomatic benign prostatic hyperplasia (BPH). There is a rapid response, with approximately 70% of patients experiencing an increase in urinary flow and improvement in symptoms of BPH when treated with terazosin capsules. The long-term effects of terazosin capsules on the incidence of surgery, acute urinary obstruction or other complications of BPH are yet to be determined. Terazosin capsules are also indicated for the treatment of hypertension. Terazosin capsules can be used alone or in combination with other antihypertensive agents such as diuretics or beta-adrenergic blocking agents.\n", + "Brand name: Terazosin\n", + "\n", + "Generic name: Entropofloxacin\n", + "Usage: Entropofloxacin is a fluoroquinolone antibiotic that is used to treat a variety of bacterial infections, including: pneumonia, streptococcus infections, salmonella infections, escherichia coli infections, and pseudomonas aeruginosa infections It is taken by mouth or by injection. The dosage and frequency of administration will vary depending on the type of infection being treated. It should be taken for the full course of treatment, even if symptoms improve after a few days. Stopping the medication early may increase the risk of the infection coming back.\n", + "Brand names:\n" + ] + } + ], "source": [ "# Define the prompt\n", "few_shot_prompt = prefix_prompt + example_prompt + suffix_prompt\n", @@ -791,11 +1126,82 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 42, "metadata": { "id": "d4ODRJdvLhlQ" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 5c6c3b79-812c-4a6e-876e-ca1ff6230a6e is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 168d5859-5edb-4702-8192-838ac2c7bc17 is DONE. 8 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 72f07348-4bcd-4042-84ca-396e7651ad03 is DONE. 2 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 70863a3b-8c63-423c-84cd-2804139daf5f is DONE. 679 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "- **Aerion:** (Derived from \"aer\" meaning air)\n", + "- **Aquazone:** (Combining \"aqua\" for water and \"zone\" for area)\n", + "- **Biosphere:** (Inspired by the concept of a self-contained ecosystem)\n", + "- **Celestial:** (Evoking the vastness and healing power of the universe)\n", + "- **Ethereal:** (Conveying a sense of lightness and transcendence)\n", + "- **Luminary:** (From \"lumen\" meaning light, symbolizing hope and healing)\n", + "- **Quasar:** (Inspired by the powerful and distant cosmic objects)\n", + "- **Sanctuary:** (Creating a sense of safety and refuge)\n", + "- **Zenith:** (Reaching the highest point or peak)\n", + "- **Zephyr:** (Named after the gentle west wind, representing a calming and soothing effect)" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "response = predict(few_shot_prompt)\n", "\n", @@ -817,11 +1223,139 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "metadata": { "id": "8eAutS41mx6U" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job b73f92bb-0e58-4fe4-adfb-b948fc5f4647 is DONE. 84.4 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 392dae36-aacb-4753-b28c-dad8291cb153 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7c6ff6ee-db64-4629-a417-846dcecac127 is DONE. 6.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openfda_generic_nameopenfda_brand_nameindications_and_usage
89MEPHITIS MEPHITICAMEPHITIS MEPHITICAINDICATIONS Condition listed above or as direc...
105ONDANSETRONONDANSETRON1 INDICATIONS AND USAGE Ondansetron Injection,...
124CLOFARABINECLOFARABINE1 INDICATIONS AND USAGE Clofarabine injection ...
273ACETAMINOPHEN AND DIPHENHYDRAMINE HYDROCHLORIDEACETAMINOPHEN AND DIPHENHYDRAMINE HYDROCHLORIDEUses Temporary relief of occasional headaches ...
284OFLOXACINOFLOXACININDICATIONS AND USAGE To reduce the developmen...
\n", + "

5 rows × 3 columns

\n", + "
[5 rows x 3 columns in total]" + ], + "text/plain": [ + " openfda_generic_name \\\n", + "89 MEPHITIS MEPHITICA \n", + "105 ONDANSETRON \n", + "124 CLOFARABINE \n", + "273 ACETAMINOPHEN AND DIPHENHYDRAMINE HYDROCHLORIDE \n", + "284 OFLOXACIN \n", + "\n", + " openfda_brand_name \\\n", + "89 MEPHITIS MEPHITICA \n", + "105 ONDANSETRON \n", + "124 CLOFARABINE \n", + "273 ACETAMINOPHEN AND DIPHENHYDRAMINE HYDROCHLORIDE \n", + "284 OFLOXACIN \n", + "\n", + " indications_and_usage \n", + "89 INDICATIONS Condition listed above or as direc... \n", + "105 1 INDICATIONS AND USAGE Ondansetron Injection,... \n", + "124 1 INDICATIONS AND USAGE Clofarabine injection ... \n", + "273 Uses Temporary relief of occasional headaches ... \n", + "284 INDICATIONS AND USAGE To reduce the developmen... \n", + "\n", + "[5 rows x 3 columns]" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Query 3 columns of interest from drug label dataset\n", "df_missing = bpd.read_gbq(\"bigquery-public-data.fda_drug.drug_label\",\n", @@ -851,7 +1385,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 44, "metadata": { "id": "19TvGN1PVmVX" }, @@ -878,16 +1412,53 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 46, "metadata": { "id": "tiSHa5B4aFhw" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job d216bea6-9b9c-4918-9194-40de2745beca is DONE. 84.4 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 37d88636-b1fb-44da-9504-44144af9624d is DONE. 800 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 0b35db83-5bac-47b4-8a2c-b46a816c0e3e is DONE. 200 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "def batch_predict(\n", " input: bpd.DataFrame, temperature: float = TEMPERATURE\n", ") -> bpd.DataFrame:\n", - " return model.predict(input, temperature).ml_generate_text_llm_result\n", + " return model.predict(input, temperature=temperature).ml_generate_text_llm_result\n", "\n", "\n", "response = batch_predict(df_missing[\"prompt\"])" @@ -904,19 +1475,73 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 50, "metadata": { "id": "TnizdeqBdbZj" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 4397b5f3-5058-409c-a361-c9fa715e46ee is DONE. 84.4 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 147ea301-e249-49fb-8280-d61948d5df7f is DONE. 84.4 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 067a2a73-0f36-42a6-973e-074ab8be631a is DONE. 56.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generic name: MEPHITIS MEPHITICA\n", + "Brand name: INDICATIONS Condition listed above or as directed by the physician\n", + "Response: **Ephemeral** (Latin root: \"ephemerus,\" meaning \"lasting for a day\")\n", + "\n", + "**Aetheria** (Greek root: \"aither,\" meaning \"upper air, sky\")\n", + "\n", + "**Zenithar** (Combination of \"zenith\" and \"pharma\")\n", + "\n", + "**Celestian** (Latin root: \"celestial,\" meaning \"heavenly\")\n", + "\n", + "**Astralux** (Combination of \"astral\" and \"lux,\" meaning \"light\")\n" + ] + } + ], "source": [ "# Pick a sample\n", "k = 0\n", "\n", "# Gather the prompt and response details\n", - "prompt_generic = df_missing[\"openfda_generic_name\"][k].iloc[0]\n", - "prompt_usage = df_missing[\"indications_and_usage\"][k].iloc[0]\n", - "response_str = response[k].iloc[0]\n", + "prompt_generic = df_missing[\"openfda_generic_name\"].iloc[k]\n", + "prompt_usage = df_missing[\"indications_and_usage\"].iloc[k]\n", + "response_str = response.iloc[k]\n", "\n", "# Print details\n", "print(f\"Generic name: {prompt_generic}\")\n", @@ -934,36 +1559,6 @@ "\n", "You've also seen how BigFrames can manage each step of the process, including gathering data, data manipulation, and querying the LLM." ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Bys6--dVmq7R" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "cIODjOLump_-" - }, - "outputs": [], - "source": [ - "# Delete the BigQuery Connection\n", - "from google.cloud import bigquery_connection_v1 as bq_connection\n", - "client = bq_connection.ConnectionServiceClient()\n", - "CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{LOCATION}/connections/{CONNECTION}\"\n", - "client.delete_connection(name=CONNECTION_ID)\n", - "print(f\"Deleted connection {CONNECTION_ID}.\")" - ] } ], "metadata": { @@ -973,6 +1568,10 @@ "kernelspec": { "display_name": "Python 3", "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.9" } }, "nbformat": 4, diff --git a/notebooks/generative_ai/large_language_models.ipynb b/notebooks/generative_ai/large_language_models.ipynb index 2695ee9dc0f..08ef52b544f 100644 --- a/notebooks/generative_ai/large_language_models.ipynb +++ b/notebooks/generative_ai/large_language_models.ipynb @@ -2,13 +2,13 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import bigframes.pandas\n", "import pandas as pd\n", - "from bigframes.ml.llm import PaLM2TextGenerator" + "from bigframes.ml.llm import GeminiTextGenerator" ] }, { @@ -22,9 +22,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/session/__init__.py:1762: UserWarning: No explicit location is set, so using location US for the session.\n", + " return Session(context)\n" + ] + } + ], "source": [ "session = bigframes.pandas.get_global_session()\n", "connection = f\"{session.bqclient.project}.us.bigframes-default-connection\"" @@ -42,9 +51,22 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 12bcd690-ca99-4001-bf26-032f50e77d62 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "model = PaLM2TextGenerator(session=session, connection_name=connection)" + "model = GeminiTextGenerator(session=session, connection_name=connection)" ] }, { @@ -83,6 +105,54 @@ "execution_count": 5, "metadata": {}, "outputs": [ + { + "data": { + "text/html": [ + "Query job f8fe31c6-7d8a-4919-9492-8304a0083cca is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 28bab71f-e218-4d92-9a50-dab41bb0c71f is DONE. 24 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 01d66b61-459f-474e-9f66-d519f9c2f23d is DONE. 6 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job af606ca7-4bcf-4bd1-95fd-c516542b5a4f is DONE. 5.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, { "data": { "text/html": [ @@ -105,30 +175,64 @@ " \n", " \n", " ml_generate_text_llm_result\n", + " ml_generate_text_rai_result\n", + " ml_generate_text_status\n", + " prompt\n", " \n", " \n", " \n", " \n", " 0\n", - " BigQuery is a fully managed, petabyte-scale an...\n", + " **BigQuery**\n", + "\n", + "**Definition:**\n", + "\n", + "BigQuery is a s...\n", + " null\n", + " \n", + " What is BigQuery?\n", " \n", " \n", " 1\n", - " BQML stands for BigQuery Machine Learning. It ...\n", + " **BigQuery Machine Learning (BQML)**\n", + "\n", + "BQML is ...\n", + " null\n", + " \n", + " What is BQML?\n", " \n", " \n", " 2\n", - " A BigQuery DataFrames is a distributed collecti...\n", + " BigQuery DataFrame is a Python DataFrame imple...\n", + " null\n", + " \n", + " What is BigQuery DataFrame?\n", " \n", " \n", "\n", "" ], "text/plain": [ - " ml_generate_text_llm_result\n", - "0 BigQuery is a fully managed, petabyte-scale an...\n", - "1 BQML stands for BigQuery Machine Learning. It ...\n", - "2 A BigQuery DataFrames is a distributed collecti..." + " ml_generate_text_llm_result \\\n", + "0 **BigQuery**\n", + "\n", + "**Definition:**\n", + "\n", + "BigQuery is a s... \n", + "1 **BigQuery Machine Learning (BQML)**\n", + "\n", + "BQML is ... \n", + "2 BigQuery DataFrame is a Python DataFrame imple... \n", + "\n", + " ml_generate_text_rai_result ml_generate_text_status \\\n", + "0 null \n", + "1 null \n", + "2 null \n", + "\n", + " prompt \n", + "0 What is BigQuery? \n", + "1 What is BQML? \n", + "2 What is BigQuery DataFrame? " ] }, "execution_count": 5, @@ -157,7 +261,7 @@ { "data": { "text/plain": [ - "'BigQuery is a fully managed, petabyte-scale analytics data warehouse that enables businesses to analyze all their data very quickly. It is a cloud-based service that offers a pay-as-you-go pricing model. BigQuery is designed to handle large amounts of data and provide fast performance. It is a good choice for businesses that need to analyze large amounts of data quickly and easily.'" + "'**BigQuery**\\n\\n**Definition:**\\n\\nBigQuery is a serverless, highly scalable, cloud-based data warehouse and analytics platform offered by Google Cloud.\\n\\n**Key Features:**\\n\\n* **Massive Scalability:** Can handle large datasets (petabytes or more) with fast query execution.\\n* **Elastic:** Automatically scales compute resources based on workload requirements.\\n* **Serverless:** Users do not need to manage infrastructure or provision resources.\\n* **Flexible Data Loading:** Supports a wide range of data sources, including files, databases, and streaming data.\\n* **SQL-Based Querying:** Uses standard SQL syntax for querying and analyzing data.\\n* **Machine Learning Integration:** Provides built-in machine learning capabilities for predictive analytics and data exploration.\\n* **Real-Time Analysis:** Supports streaming data analysis and interactive dashboards.\\n* **Collaboration and Sharing:** Allows multiple users to access and analyze data in a collaborative environment.\\n* **Cost-Effective:** Pay-as-you-go pricing based on data scanned and compute resources used.\\n\\n**Applications:**\\n\\n* Data warehousing and analytics\\n* Business intelligence and reporting\\n* Data science and machine learning\\n* Data exploration and visualization\\n* Marketing analytics\\n* Fraud detection and risk management\\n\\n**Benefits:**\\n\\n* Rapid data analysis on large datasets\\n* Reduced infrastructure management overhead\\n* Increased agility and flexibility\\n* Enhanced collaboration and data sharing\\n* Cost-effective data storage and analytics'" ] }, "execution_count": 6, diff --git a/noxfile.py b/noxfile.py index e7f238c01f9..1d8ab6c1fd6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -716,6 +716,7 @@ def notebook(session: nox.Session): # TODO(swast): investigate why we get 404 errors, even though # bq_dataframes_llm_code_generation creates a bucket in the sample. "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", # Needs BUCKET_URI. + "notebooks/generative_ai/sentiment_analysis.ipynb", # Too slow "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", # Needs BUCKET_URI. "notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb", # Needs BUCKET_URI. "notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb", # Needs BUCKET_URI. From cba21ba8533835cd2286c8b89ac4b7bf144bfce4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:55:09 -0700 Subject: [PATCH 353/921] chore(main): release 0.24.0 (#411) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35eaa3688d7..565fe432415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.24.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.23.0...v0.24.0) (2024-03-12) + + +### ⚠ BREAKING CHANGES + +* `read_parquet` uses a "pandas" engine to parse files by default. Use `engine="bigquery"` for the previous behavior + +### Features + +* (Series|Dataframe).plot.hist() ([#420](https://github.com/googleapis/python-bigquery-dataframes/issues/420)) ([4aadff4](https://github.com/googleapis/python-bigquery-dataframes/commit/4aadff4db59243b4510a874fef2bdb17402d1674)) +* Add detect_anomalies to ml ARIMAPlus and KMeans models ([#426](https://github.com/googleapis/python-bigquery-dataframes/issues/426)) ([6df28ed](https://github.com/googleapis/python-bigquery-dataframes/commit/6df28ed704552ebec7869e1f2034614cb6407098)) +* Add engine parameter to `read_parquet` ([#413](https://github.com/googleapis/python-bigquery-dataframes/issues/413)) ([31325a1](https://github.com/googleapis/python-bigquery-dataframes/commit/31325a190320bf01ced53d9f4cdb94462daaa06b)) +* Add ml PCA.detect_anomalies method ([#422](https://github.com/googleapis/python-bigquery-dataframes/issues/422)) ([8d82945](https://github.com/googleapis/python-bigquery-dataframes/commit/8d8294544ac7fedaca753c5473e3ca2a27868420)) +* Support BYOSA in `remote_function` ([#407](https://github.com/googleapis/python-bigquery-dataframes/issues/407)) ([d92ced2](https://github.com/googleapis/python-bigquery-dataframes/commit/d92ced2adaa30a0405ace9ca6cd70a8e217f13d0)) +* Support CMEK for BQ tables ([#403](https://github.com/googleapis/python-bigquery-dataframes/issues/403)) ([9a678e3](https://github.com/googleapis/python-bigquery-dataframes/commit/9a678e35201d935e1d93875429005033cfe7cff6)) + + +### Bug Fixes + +* Move `third_party.bigframes_vendored` to `bigframes_vendored` ([#424](https://github.com/googleapis/python-bigquery-dataframes/issues/424)) ([763edeb](https://github.com/googleapis/python-bigquery-dataframes/commit/763edeb4f4e8bc4b8bb05a992dae80c49c245e25)) +* Only do row identity based joins when joining by index ([#356](https://github.com/googleapis/python-bigquery-dataframes/issues/356)) ([76b252f](https://github.com/googleapis/python-bigquery-dataframes/commit/76b252f907055d72556e3e95f6cb5ee41de5b1c2)) +* Read_pandas inline respects location ([#412](https://github.com/googleapis/python-bigquery-dataframes/issues/412)) ([ae0e3ea](https://github.com/googleapis/python-bigquery-dataframes/commit/ae0e3eaca49171fd449de4d43ddc3e3ce9fdc2ce)) + + +### Documentation + +* Add predict sample to samples/snippets/bqml_getting_started_test.py ([#388](https://github.com/googleapis/python-bigquery-dataframes/issues/388)) ([6a3b0cc](https://github.com/googleapis/python-bigquery-dataframes/commit/6a3b0cc7f84120fc5978ce11b6b7c55e89654304)) +* Document minimum IAM requirement ([#416](https://github.com/googleapis/python-bigquery-dataframes/issues/416)) ([36173b0](https://github.com/googleapis/python-bigquery-dataframes/commit/36173b0c14747fb52909bbedd93249024bae9ac1)) +* Fix the note rendering for DataFrames methods: nlargest, nsmallest ([#417](https://github.com/googleapis/python-bigquery-dataframes/issues/417)) ([38bd2ba](https://github.com/googleapis/python-bigquery-dataframes/commit/38bd2ba21bc1a3222635de22eecd97930bf5b1de)) + ## [0.23.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.22.0...v0.23.0) (2024-03-05) diff --git a/bigframes/version.py b/bigframes/version.py index a50b0b86fd2..ae18e113efe 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.23.0" +__version__ = "0.24.0" From fb29e560d466ddebb425119dc5a481f9ac948b40 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 13 Mar 2024 03:05:46 +0000 Subject: [PATCH 354/921] chore: add script to compute code samples coverage (#428) * chore: add script to compute code samples coverage * include Index, Session and ml.ensemble --- scripts/get_code_sample_coverage.py | 147 ++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100755 scripts/get_code_sample_coverage.py diff --git a/scripts/get_code_sample_coverage.py b/scripts/get_code_sample_coverage.py new file mode 100755 index 00000000000..d81023394ff --- /dev/null +++ b/scripts/get_code_sample_coverage.py @@ -0,0 +1,147 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import importlib +import inspect +import sys +from typing import Dict, List + +import bigframes +import bigframes.pandas as bpd + +PRESENT = "present" +NOT_PRESENT = "not_present" + +CLASSES = [ + bpd.DataFrame, + bpd.Series, + bpd.Index, + bigframes.session.Session, + bigframes.operations.strings.StringMethods, + bigframes.operations.datetimes.DatetimeMethods, + bigframes.operations.structs.StructAccessor, +] + +ML_MODULE_NAMES = [ + "cluster", + "compose", + "decomposition", + "ensemble", + "linear_model", + "metrics", + "model_selection", + "pipeline", + "preprocessing", + "llm", + "forecasting", + "imported", + "remote", +] + +for module_name in ML_MODULE_NAMES: + module = importlib.import_module(f"bigframes.ml.{module_name}") + classes_ = [ + class_ for _, class_ in inspect.getmembers(module, predicate=inspect.isclass) + ] + CLASSES.extend(classes_) + + +def get_code_samples_summary() -> Dict[str, Dict[str, List[str]]]: + """Get Summary of the code samples coverage in BigFrames APIs. + + Returns: + Summary: A dictionary of the format + { + class_1: { + "present": [method1, method2, ...], + "not_present": [method3, method4, ...] + }, + class_2: { + ... + } + } + """ + summary: Dict[str, Dict[str, List[str]]] = dict() + + for class_ in CLASSES: + class_key = f"{class_.__module__}.{class_.__name__}" + summary[class_key] = {PRESENT: [], NOT_PRESENT: []} + + members = inspect.getmembers(class_) + + for name, obj in members: + # ignore private methods + if name.startswith("_") and not name.startswith("__"): + continue + + def predicate(impl): + return ( + # This includes class methods like `from_dict`, `from_records` + inspect.ismethod(impl) + # This includes instance methods like `dropna`, join` + or inspect.isfunction(impl) + # This includes properties like `shape`, `values` but not + # generic properties like `__weakref__` + or (inspect.isdatadescriptor(impl) and not name.startswith("__")) + ) + + if not predicate(obj): + continue + + # At this point we have a property or a public method + impl = getattr(class_, name) + + docstr = inspect.getdoc(impl) + code_samples_present = docstr and "**Examples:**" in docstr + key = PRESENT if code_samples_present else NOT_PRESENT + summary[class_key][key].append(name) + + return summary + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Get a summary of code samples coverage in BigFrames APIs." + ) + parser.add_argument( + "-d", + "--details", + type=bool, + action=argparse.BooleanOptionalAction, + default=False, + help="Whether to print APIs with and without code samples.", + ) + + args = parser.parse_args(sys.argv[1:]) + + summary = get_code_samples_summary() + + total_with_code_samples = 0 + total = 0 + for class_, class_summary in summary.items(): + apis_with_code_samples = len(class_summary[PRESENT]) + total_with_code_samples += apis_with_code_samples + + apis_total = len(class_summary[PRESENT]) + len(class_summary[NOT_PRESENT]) + total += apis_total + + coverage = 100 * apis_with_code_samples / apis_total + print(f"{class_}: {coverage:.1f}% ({apis_with_code_samples}/{apis_total})") + if args.details: + print(f"===> APIs WITH code samples: {class_summary[PRESENT]}") + print(f"===> APIs WITHOUT code samples: {class_summary[NOT_PRESENT]}") + + coverage = 100 * total_with_code_samples / total + print(f"Total: {coverage:.1f}% ({total_with_code_samples}/{total})") From b6211eee9e6363b6a487e39dbd22b2f136b38009 Mon Sep 17 00:00:00 2001 From: Salem Jorden <115185670+SalemJorden@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:09:01 -0500 Subject: [PATCH 355/921] chore: remove `to_pandas_dtypes_overrides` dead code (#404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dtypes.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index f29d653d4f9..cf140c7d229 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -20,7 +20,6 @@ import typing from typing import Any, Dict, Iterable, Literal, Tuple, Union -import bigframes_vendored.google_cloud_bigquery._pandas_helpers as gcb3p_pandas_helpers import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import geopandas as gpd # type: ignore @@ -492,21 +491,6 @@ def cast_ibis_value( ) -def to_pandas_dtypes_overrides(schema: Iterable[bigquery.SchemaField]) -> Dict: - """For each STRUCT field, make sure we specify the full type to use.""" - # TODO(swast): Also override ARRAY fields. - dtypes = {} - for field in schema: - if field.field_type == "RECORD" and field.mode != "REPEATED": - # TODO(swast): We're using a private API here. Would likely be - # better if we called `to_arrow()` and converted to a pandas - # DataFrame ourselves from that. - dtypes[field.name] = pd.ArrowDtype( - gcb3p_pandas_helpers.bq_to_arrow_data_type(field) - ) - return dtypes - - def is_dtype(scalar: typing.Any, dtype: Dtype) -> bool: """Captures whether a scalar can be losslessly represented by a dtype.""" if scalar is None: From e502e6bd3ff882d9cc491e2f6751dfbb0ea36e43 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 13 Mar 2024 23:51:16 +0000 Subject: [PATCH 356/921] test: include model.register test for BQML CMEK (#433) * test: include model.register test for BQML CMEK * propagate _start_query_create_model renaming in unit test * add a side assert in to_gbq tes --- bigframes/ml/core.py | 4 +-- bigframes/session/__init__.py | 5 ++-- tests/system/small/test_encryption.py | 41 +++++++++++++++++++++++++-- tests/unit/ml/test_golden_sql.py | 10 +++---- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 43a882ecac3..03d9b806b97 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -245,7 +245,7 @@ def register(self, vertex_ai_model_id: Optional[str] = None) -> BqmlModel: options={"vertex_ai_model_id": vertex_ai_model_id} ) # Register the model and wait it to finish - self._session._start_query_create_model(sql) + self._session._start_query_ml_ddl(sql) self._model = self._session.bqclient.get_model(self.model_name) return self @@ -264,7 +264,7 @@ def _create_model_ref( def _create_model_with_sql(self, session: bigframes.Session, sql: str) -> BqmlModel: # fit the model, synchronously - _, job = session._start_query_create_model(sql) + _, job = session._start_query_ml_ddl(sql) # real model path in the session specific hidden dataset and table prefix model_name_full = f"{job.destination.project}.{job.destination.dataset_id}.{job.destination.table_id}" diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 5266267a222..218531c3a7d 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1592,12 +1592,13 @@ def _start_query( self.bqclient, sql, job_config, max_results ) - def _start_query_create_model( + def _start_query_ml_ddl( self, sql: str, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ - Starts BigQuery ML CREATE MODEL query job and waits for results. + Starts BigQuery ML DDL query job (CREATE MODEL/ALTER MODEL/...) and + waits for results. """ job_config = self._prepare_query_job_config() diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 0ce9d881fd6..f13d2b9e1a3 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -130,7 +130,7 @@ def test_df_apis(bq_cmek, session_with_bq_cmek, scalars_table_id): # Read a BQ table and assert encryption df = session_with_bq_cmek.read_gbq(scalars_table_id) - # Perform a few dataframe operations and assert assertion + # Perform a few dataframe operations and assert encryption df1 = df.dropna() _assert_bq_table_is_encrypted(df1, bq_cmek, session_with_bq_cmek) @@ -179,15 +179,32 @@ def test_to_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): df = session_with_bq_cmek.read_gbq(scalars_table_id) _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) - # Modify the dataframe and assert assertion + # Modify the dataframe and assert encryption df = df.dropna().head() _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) - # Write the result to BQ and assert assertion + # Write the result to BQ and assert encryption output_table_id = df.to_gbq() output_table = session_with_bq_cmek.bqclient.get_table(output_table_id) assert output_table.encryption_configuration.kms_key_name == bq_cmek + # Write the result to BQ custom table and assert encryption + session_with_bq_cmek.bqclient.get_table(output_table_id) + output_table_ref = bigframes.session._io.bigquery.random_table( + session_with_bq_cmek._anonymous_dataset + ) + 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) + assert output_table.encryption_configuration.kms_key_name == bq_cmek + + # Lastly, assert that the encryption is not because of any default set at + # the dataset level + output_table_dataset = session_with_bq_cmek.bqclient.get_dataset( + output_table.dataset_id + ) + assert output_table_dataset.default_encryption_configuration is None + @pytest.mark.skip( reason="Internal issue 327544164, cmek does not propagate to the dataframe." @@ -254,3 +271,21 @@ def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): # Assert that model exists in BQ with intended encryption model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) assert model_bq.encryption_configuration.kms_key_name == bq_cmek + + # Assert that model registration keeps the encryption + # Note that model registration only creates an entry (metadata) to be + # included in the Vertex AI Model Registry. See for more details + # https://cloud.google.com/bigquery/docs/update_vertex#add-existing. + # When use deploys the model to an endpoint from the Model Registry then + # they can specify an encryption key to further protect the artifacts at + # rest on the Vertex AI side. See for more details: + # https://cloud.google.com/vertex-ai/docs/general/deployment#deploy_a_model_to_an_endpoint, + # https://cloud.google.com/vertex-ai/docs/general/cmek#create_resources_with_the_kms_key. + # bigframes.ml does not provide any API for the model deployment. + model_registered = new_model.register() + assert ( + model_registered._bqml_model.model.encryption_configuration.kms_key_name + == bq_cmek + ) + model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) + assert model_bq.encryption_configuration.kms_key_name == bq_cmek diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 25e12d87bf8..d63bc7aaa10 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -43,7 +43,7 @@ def mock_session(): mock_session._anonymous_dataset, TEMP_MODEL_ID.model_id ) ) - mock_session._start_query_create_model.return_value = (None, query_job) + mock_session._start_query_ml_ddl.return_value = (None, query_job) return mock_session @@ -104,7 +104,7 @@ def test_linear_regression_default_fit( model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query_create_model.assert_called_once_with( + mock_session._start_query_ml_ddl.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -114,7 +114,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query_create_model.assert_called_once_with( + mock_session._start_query_ml_ddl.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -147,7 +147,7 @@ def test_logistic_regression_default_fit( model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query_create_model.assert_called_once_with( + mock_session._start_query_ml_ddl.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -161,7 +161,7 @@ def test_logistic_regression_params_fit( model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) - mock_session._start_query_create_model.assert_called_once_with( + mock_session._start_query_ml_ddl.assert_called_once_with( 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) From 2fd69f4bed143fc8c040dac1c55288c1cb660f6e Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 14 Mar 2024 02:28:23 +0000 Subject: [PATCH 357/921] feat: support CMEK for `remote_function` cloud functions (#430) * feat: support CMEK for `remote_function` cloud functions * add retry in test * bump up min version of google-python-functions for CMEK compliance --- bigframes/functions/remote_function.py | 40 ++++++++++++- bigframes/pandas/__init__.py | 4 ++ bigframes/session/__init__.py | 18 ++++++ setup.py | 2 +- testing/constraints-3.9.txt | 2 +- tests/system/large/test_remote_function.py | 67 +++++++++++++++++++++- 6 files changed, 129 insertions(+), 4 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 29c1c68e7cb..09a9d978698 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -130,6 +130,8 @@ def __init__( bq_connection_id, cloud_resource_manager_client, cloud_function_service_account, + cloud_function_kms_key_name, + cloud_function_docker_repository, ): self._gcp_project_id = gcp_project_id self._cloud_function_region = cloud_function_region @@ -142,6 +144,8 @@ def __init__( bq_connection_client, cloud_resource_manager_client ) self._cloud_function_service_account = cloud_function_service_account + self._cloud_function_kms_key_name = cloud_function_kms_key_name + self._cloud_function_docker_repository = cloud_function_docker_repository def create_bq_remote_function( self, input_args, input_types, output_type, endpoint, bq_function_name @@ -344,7 +348,9 @@ def create_cloud_function(self, def_, cf_name, package_requirements=None): ) # Determine an upload URL for user code - upload_url_request = functions_v2.GenerateUploadUrlRequest() + upload_url_request = functions_v2.GenerateUploadUrlRequest( + kms_key_name=self._cloud_function_kms_key_name + ) upload_url_request.parent = self.get_cloud_function_fully_qualified_parent() upload_url_response = self._cloud_functions_client.generate_upload_url( request=upload_url_request @@ -383,12 +389,16 @@ def create_cloud_function(self, def_, cf_name, package_requirements=None): function.build_config.source.storage_source.object_ = ( upload_url_response.storage_source.object_ ) + function.build_config.docker_repository = ( + self._cloud_function_docker_repository + ) function.service_config = functions_v2.ServiceConfig() function.service_config.available_memory = "1024M" function.service_config.timeout_seconds = 600 function.service_config.service_account_email = ( self._cloud_function_service_account ) + function.kms_key_name = self._cloud_function_kms_key_name create_function_request.function = function # Create the cloud function and wait for it to be ready to use @@ -597,6 +607,8 @@ def remote_function( name: Optional[str] = None, packages: Optional[Sequence[str]] = None, cloud_function_service_account: Optional[str] = None, + cloud_function_kms_key_name: Optional[str] = None, + cloud_function_docker_repository: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -699,6 +711,20 @@ def remote_function( for more details. Please make sure the service account has the necessary IAM permissions configured as described in https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. + cloud_function_kms_key_name (str, Optional): + Customer managed encryption key to protect cloud functions and + related data at rest. This is of the format + projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY. + Read https://cloud.google.com/functions/docs/securing/cmek for + more details including granting necessary service accounts + access to the key. + cloud_function_docker_repository (str, Optional): + Docker repository created with the same encryption key as + `cloud_function_kms_key_name` to store encrypted artifacts + created to support the cloud function. This is of the format + projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. + For more details see + https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. """ import bigframes.pandas as bpd @@ -780,6 +806,16 @@ def remote_function( f"{bq_location}." ) + # If any CMEK is intended then check that a docker repository is also specified + if ( + cloud_function_kms_key_name is not None + and cloud_function_docker_repository is None + ): + raise ValueError( + "cloud_function_docker_repository must be specified with cloud_function_kms_key_name." + " For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin" + ) + def wrapper(f): if not callable(f): raise TypeError("f must be callable, got {}".format(f)) @@ -800,6 +836,8 @@ def wrapper(f): bq_connection_id, resource_manager_client, cloud_function_service_account, + cloud_function_kms_key_name, + cloud_function_docker_repository, ) rf_name, cf_name = remote_function_client.provision_bq_remote_function( diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 03c8412907b..10caf17b790 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -620,6 +620,8 @@ def remote_function( name: Optional[str] = None, packages: Optional[Sequence[str]] = None, cloud_function_service_account: Optional[str] = None, + cloud_function_kms_key_name: Optional[str] = None, + cloud_function_docker_repository: Optional[str] = None, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -631,6 +633,8 @@ def remote_function( name=name, packages=packages, cloud_function_service_account=cloud_function_service_account, + cloud_function_kms_key_name=cloud_function_kms_key_name, + cloud_function_docker_repository=cloud_function_docker_repository, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 218531c3a7d..656c62ef193 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1364,6 +1364,8 @@ def remote_function( name: Optional[str] = None, packages: Optional[Sequence[str]] = None, cloud_function_service_account: Optional[str] = None, + cloud_function_kms_key_name: Optional[str] = None, + cloud_function_docker_repository: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1444,6 +1446,20 @@ def remote_function( for more details. Please make sure the service account has the necessary IAM permissions configured as described in https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. + cloud_function_kms_key_name (str, Optional): + Customer managed encryption key to protect cloud functions and + related data at rest. This is of the format + projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY. + Read https://cloud.google.com/functions/docs/securing/cmek for + more details including granting necessary service accounts + access to the key. + cloud_function_docker_repository (str, Optional): + Docker repository created with the same encryption key as + `cloud_function_kms_key_name` to store encrypted artifacts + created to support the cloud function. This is of the format + projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. + For more details see + https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1463,6 +1479,8 @@ def remote_function( name=name, packages=packages, cloud_function_service_account=cloud_function_service_account, + cloud_function_kms_key_name=cloud_function_kms_key_name, + cloud_function_docker_repository=cloud_function_docker_repository, ) def read_gbq_function( diff --git a/setup.py b/setup.py index a626fd4b347..5258a7d6f93 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ "geopandas >=0.12.2", "google-auth >=2.15.0,<3.0dev", "google-cloud-bigquery[bqstorage,pandas] >=3.10.0", - "google-cloud-functions >=1.10.1", + "google-cloud-functions >=1.12.0", "google-cloud-bigquery-connection >=1.12.0", "google-cloud-iam >=2.12.1", "google-cloud-resource-manager >=1.10.3", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 07c8b763f3e..0aeb15eab86 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -5,7 +5,7 @@ gcsfs==2023.3.0 geopandas==0.12.2 google-auth==2.15.0 google-cloud-bigquery==3.10.0 -google-cloud-functions==1.10.1 +google-cloud-functions==1.12.0 google-cloud-bigquery-connection==1.12.0 google-cloud-iam==2.12.1 google-cloud-resource-manager==1.10.3 diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 77aa3c76031..f8c5e98f1d8 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -22,7 +22,7 @@ import textwrap from google.api_core.exceptions import BadRequest, NotFound, ResourceExhausted -from google.cloud import bigquery, functions_v2 +from google.cloud import bigquery, functions_v2, storage import pandas import pytest import test_utils.prefixer @@ -1322,3 +1322,68 @@ def square_num(x): cleanup_remote_function_assets( rf_session.bqclient, rf_session.cloudfunctionsclient, square_num ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_with_gcf_cmek(): + # TODO(shobs): Automate the following set-up during testing in the test project. + # + # For upfront convenience, the following set up has been statically created + # in the project bigfrmames-dev-perf via cloud console: + # + # 1. Created an encryption key and granting the necessary service accounts + # the required IAM permissions as per https://cloud.google.com/kms/docs/create-key + # 2. Created a docker repository with CMEK (created in step 1) enabled as per + # https://cloud.google.com/artifact-registry/docs/repositories/create-repos#overview + # + project = "bigframes-dev-perf" + cmek = "projects/bigframes-dev-perf/locations/us-central1/keyRings/bigframesKeyRing/cryptoKeys/bigframesKey" + docker_repository = ( + "projects/bigframes-dev-perf/locations/us-central1/repositories/rf-artifacts" + ) + + session = bigframes.Session(context=bigframes.BigQueryOptions(project=project)) + try: + + @session.remote_function( + [int], + int, + reuse=False, + cloud_function_kms_key_name=cmek, + cloud_function_docker_repository=docker_repository, + ) + def square_num(x): + if x is None: + return x + return x * x + + df = pandas.DataFrame({"num": [-1, 0, None, 1]}, dtype="Int64") + bf = session.read_pandas(df) + + bf_result_col = bf["num"].apply(square_num) + bf_result = bf.assign(result=bf_result_col).to_pandas() + + pd_result_col = df["num"].apply(lambda x: x if x is None else x * x) + pd_result = df.assign(result=pd_result_col) + + assert_pandas_df_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + # Assert that the GCF is created with the intended SA + gcf = session.cloudfunctionsclient.get_function( + name=square_num.bigframes_cloud_function + ) + assert gcf.kms_key_name == cmek + + # Assert that GCS artifact has CMEK applied + storage_client = storage.Client() + bucket = storage_client.bucket(gcf.build_config.source.storage_source.bucket) + blob = bucket.get_blob(gcf.build_config.source.storage_source.object_) + assert blob.kms_key_name.startswith(cmek) + + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square_num + ) From 7f3d41c9a1dc57335bbc3c62f74d3ba7760b324c Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Wed, 13 Mar 2024 21:48:38 -0700 Subject: [PATCH 358/921] chore: fix two typos in dtypes.py (#406) --- bigframes/dtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index cf140c7d229..366820f9f6c 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -67,7 +67,7 @@ "date32[day][pyarrow]", "time64[us][pyarrow]", "decimal128(38, 9)[pyarrow]", - "decimal256(38, 9)[pyarrow]", + "decimal256(76, 38)[pyarrow]", "binary[pyarrow]", ] @@ -170,7 +170,7 @@ } # special case - string[pyarrow] doesn't include the storage in its name, and both -# "string" and "string[pyarrow] are accepted" +# "string" and "string[pyarrow]" are accepted BIGFRAMES_STRING_TO_BIGFRAMES["string[pyarrow]"] = pd.StringDtype(storage="pyarrow") # For the purposes of dataframe.memory_usage From 077251084e3121019c56e5d6c16aebab16be8dc7 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:11:27 -0700 Subject: [PATCH 359/921] feat: (Series|DataFrame).plot.(line|area|scatter) (#431) Fixing internal bugs: line: b/322177942 scatter: b/322178336 area: b/322178394 --- bigframes/operations/_matplotlib/__init__.py | 3 + bigframes/operations/_matplotlib/core.py | 42 +++ bigframes/operations/plotting.py | 57 +++- .../{test_plot.py => test_plotting.py} | 67 +++++ .../pandas/plotting/_core.py | 257 ++++++++++++++++-- 5 files changed, 396 insertions(+), 30 deletions(-) rename tests/system/small/operations/{test_plot.py => test_plotting.py} (69%) diff --git a/bigframes/operations/_matplotlib/__init__.py b/bigframes/operations/_matplotlib/__init__.py index f8770a9ef83..02aca8cf5d4 100644 --- a/bigframes/operations/_matplotlib/__init__.py +++ b/bigframes/operations/_matplotlib/__init__.py @@ -17,6 +17,9 @@ PLOT_CLASSES: dict[str, type[core.MPLPlot]] = { "hist": hist.HistPlot, + "line": core.LinePlot, + "area": core.AreaPlot, + "scatter": core.ScatterPlot, } diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index 4b15d6f4dd0..5c9d771f61e 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +import typing import matplotlib.pyplot as plt @@ -28,3 +29,44 @@ def draw(self) -> None: @property def result(self): return self.axes + + +class SamplingPlot(MPLPlot): + @abc.abstractproperty + def _kind(self): + pass + + def __init__(self, data, **kwargs) -> None: + self.kwargs = kwargs + self.data = self._compute_plot_data(data) + + def generate(self) -> None: + self.axes = self.data.plot(kind=self._kind, **self.kwargs) + + def _compute_plot_data(self, data): + # TODO: Cache the sampling data in the PlotAccessor. + sampling_n = self.kwargs.pop("sampling_n", 100) + sampling_random_state = self.kwargs.pop("sampling_random_state", 0) + return ( + data.sample(n=sampling_n, random_state=sampling_random_state) + .to_pandas() + .sort_index() + ) + + +class LinePlot(SamplingPlot): + @property + def _kind(self) -> typing.Literal["line"]: + return "line" + + +class AreaPlot(SamplingPlot): + @property + def _kind(self) -> typing.Literal["area"]: + return "area" + + +class ScatterPlot(SamplingPlot): + @property + def _kind(self) -> typing.Literal["scatter"]: + return "scatter" diff --git a/bigframes/operations/plotting.py b/bigframes/operations/plotting.py index d19485e65ef..cc9f71e5d17 100644 --- a/bigframes/operations/plotting.py +++ b/bigframes/operations/plotting.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, Sequence +import typing import bigframes_vendored.pandas.plotting._core as vendordt @@ -20,16 +20,65 @@ import bigframes.operations._matplotlib as bfplt -class PlotAccessor: +class PlotAccessor(vendordt.PlotAccessor): __doc__ = vendordt.PlotAccessor.__doc__ def __init__(self, data) -> None: self._parent = data - def hist(self, by: Optional[Sequence[str]] = None, bins: int = 10, **kwargs): + def hist( + self, by: typing.Optional[typing.Sequence[str]] = None, bins: int = 10, **kwargs + ): if kwargs.pop("backend", None) is not None: raise NotImplementedError( f"Only support matplotlib backend for now. {constants.FEEDBACK_LINK}" ) - # Calls matplotlib backend to plot the data. return bfplt.plot(self._parent.copy(), kind="hist", by=by, bins=bins, **kwargs) + + def line( + self, + x: typing.Optional[typing.Hashable] = None, + y: typing.Optional[typing.Hashable] = None, + **kwargs, + ): + return bfplt.plot( + self._parent.copy(), + kind="line", + x=x, + y=y, + **kwargs, + ) + + def area( + self, + x: typing.Optional[typing.Hashable] = None, + y: typing.Optional[typing.Hashable] = None, + stacked: bool = True, + **kwargs, + ): + return bfplt.plot( + self._parent.copy(), + kind="area", + x=x, + y=y, + stacked=stacked, + **kwargs, + ) + + def scatter( + self, + x: typing.Optional[typing.Hashable] = None, + y: typing.Optional[typing.Hashable] = None, + s: typing.Union[typing.Hashable, typing.Sequence[typing.Hashable]] = None, + c: typing.Union[typing.Hashable, typing.Sequence[typing.Hashable]] = None, + **kwargs, + ): + return bfplt.plot( + self._parent.copy(), + kind="scatter", + x=x, + y=y, + s=s, + c=c, + **kwargs, + ) diff --git a/tests/system/small/operations/test_plot.py b/tests/system/small/operations/test_plotting.py similarity index 69% rename from tests/system/small/operations/test_plot.py rename to tests/system/small/operations/test_plotting.py index 44f31ec071a..ce320b6f57e 100644 --- a/tests/system/small/operations/test_plot.py +++ b/tests/system/small/operations/test_plotting.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import numpy as np import pandas._testing as tm import pytest +import bigframes.pandas as bpd + def _check_legend_labels(ax, labels): """ @@ -166,3 +169,67 @@ def test_hist_kwargs_ticks_props(scalars_dfs): for i in range(len(pd_xlables)): tm.assert_almost_equal(ylabels[i].get_fontsize(), pd_ylables[i].get_fontsize()) tm.assert_almost_equal(ylabels[i].get_rotation(), pd_ylables[i].get_rotation()) + + +def test_line(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_names = ["int64_col", "float64_col", "int64_too", "bool_col"] + ax = scalars_df[col_names].plot.line() + pd_ax = scalars_pandas_df[col_names].plot.line() + tm.assert_almost_equal(ax.get_xticks(), pd_ax.get_xticks()) + tm.assert_almost_equal(ax.get_yticks(), pd_ax.get_yticks()) + for line, pd_line in zip(ax.lines, pd_ax.lines): + # Compare y coordinates between the lines + tm.assert_almost_equal(line.get_data()[1], pd_line.get_data()[1]) + + +def test_area(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_names = ["int64_col", "float64_col", "int64_too"] + ax = scalars_df[col_names].plot.area(stacked=False) + pd_ax = scalars_pandas_df[col_names].plot.area(stacked=False) + tm.assert_almost_equal(ax.get_xticks(), pd_ax.get_xticks()) + tm.assert_almost_equal(ax.get_yticks(), pd_ax.get_yticks()) + for line, pd_line in zip(ax.lines, pd_ax.lines): + # Compare y coordinates between the lines + tm.assert_almost_equal(line.get_data()[1], pd_line.get_data()[1]) + + +def test_scatter(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_names = ["int64_col", "float64_col", "int64_too", "bool_col"] + ax = scalars_df[col_names].plot.scatter(x="int64_col", y="float64_col") + pd_ax = scalars_pandas_df[col_names].plot.scatter(x="int64_col", y="float64_col") + tm.assert_almost_equal(ax.get_xticks(), pd_ax.get_xticks()) + tm.assert_almost_equal(ax.get_yticks(), pd_ax.get_yticks()) + tm.assert_almost_equal( + ax.collections[0].get_sizes(), pd_ax.collections[0].get_sizes() + ) + + +def test_sampling_plot_args_n(): + df = bpd.DataFrame(np.arange(1000), columns=["one"]) + ax = df.plot.line() + assert len(ax.lines) == 1 + # Default sampling_n is 100 + assert len(ax.lines[0].get_data()[1]) == 100 + + ax = df.plot.line(sampling_n=2) + assert len(ax.lines) == 1 + assert len(ax.lines[0].get_data()[1]) == 2 + + +def test_sampling_plot_args_random_state(): + df = bpd.DataFrame(np.arange(1000), columns=["one"]) + ax_0 = df.plot.line() + ax_1 = df.plot.line() + ax_2 = df.plot.line(sampling_random_state=100) + ax_3 = df.plot.line(sampling_random_state=100) + + # Setting a fixed sampling_random_state guarantees reproducible plotted sampling. + tm.assert_almost_equal(ax_0.lines[0].get_data()[1], ax_1.lines[0].get_data()[1]) + tm.assert_almost_equal(ax_2.lines[0].get_data()[1], ax_3.lines[0].get_data()[1]) + + msg = "numpy array are different" + with pytest.raises(AssertionError, match=msg): + tm.assert_almost_equal(ax_0.lines[0].get_data()[1], ax_2.lines[0].get_data()[1]) diff --git a/third_party/bigframes_vendored/pandas/plotting/_core.py b/third_party/bigframes_vendored/pandas/plotting/_core.py index d0425737eec..2b0f0776956 100644 --- a/third_party/bigframes_vendored/pandas/plotting/_core.py +++ b/third_party/bigframes_vendored/pandas/plotting/_core.py @@ -1,14 +1,14 @@ -from typing import Optional, Sequence +import typing from bigframes import constants class PlotAccessor: - """ - Make plots of Series or DataFrame with the `matplotlib` backend. - """ + """Make plots of Series or DataFrame with the `matplotlib` backend.""" - def hist(self, by: Optional[Sequence[str]] = None, bins: int = 10, **kwargs): + def hist( + self, by: typing.Optional[typing.Sequence[str]] = None, bins: int = 10, **kwargs + ): """ Draw one histogram of the DataFrame’s columns. @@ -17,32 +17,237 @@ def hist(self, by: Optional[Sequence[str]] = None, bins: int = 10, **kwargs): into bins and draws all bins in one :class:`matplotlib.axes.Axes`. This is useful when the DataFrame's Series are in a similar scale. - Parameters - ---------- - by : str or sequence, optional - Column in the DataFrame to group by. It is not supported yet. - bins : int, default 10 - Number of histogram bins to be used. - **kwargs - Additional keyword arguments are documented in - :meth:`DataFrame.plot`. - - Returns - ------- - class:`matplotlib.AxesSubplot` - Return a histogram plot. - - Examples - -------- - For Series: - - .. plot:: - :context: close-figs + **Examples:** >>> import bigframes.pandas as bpd >>> import numpy as np >>> df = bpd.DataFrame(np.random.randint(1, 7, 6000), columns=['one']) >>> df['two'] = np.random.randint(1, 7, 6000) + np.random.randint(1, 7, 6000) >>> ax = df.plot.hist(bins=12, alpha=0.5) + + Args: + by (str or sequence, optional): + Column in the DataFrame to group by. It is not supported yet. + bins (int, default 10): + Number of histogram bins to be used. + **kwargs: + Additional keyword arguments are documented in + :meth:`DataFrame.plot`. + + Returns: + class:`matplotlib.AxesSubplot`: A histogram plot. + + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def line( + self, + x: typing.Optional[typing.Hashable] = None, + y: typing.Optional[typing.Hashable] = None, + **kwargs, + ): + """ + Plot Series or DataFrame as lines. This function is useful to plot lines + using DataFrame's values as coordinates. + + This function calls `pandas.plot` to generate a plot with a random sample + of items. For consistent results, the random sampling is reproducible. + Use the `sampling_random_state` parameter to modify the sampling seed. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> df = bpd.DataFrame( + ... { + ... 'one': [1, 2, 3, 4], + ... 'three': [3, 6, 9, 12], + ... 'reverse_ten': [40, 30, 20, 10], + ... } + ... ) + >>> ax = df.plot.line(x='one') + + Args: + x (label or position, optional): + Allows plotting of one column versus another. If not specified, + the index of the DataFrame is used. + y (label or position, optional): + Allows plotting of one column versus another. If not specified, + all numerical columns are used. + color (str, array-like, or dict, optional): + The color for each of the DataFrame's columns. Possible values are: + + - A single color string referred to by name, RGB or RGBA code, + for instance 'red' or '#a98d19'. + + - A sequence of color strings referred to by name, RGB or RGBA + code, which will be used for each column recursively. For + instance ['green','yellow'] each column's %(kind)s will be filled in + green or yellow, alternatively. If there is only a single column to + be plotted, then only the first color from the color list will be + used. + + - A dict of the form {column name : color}, so that each column will be + colored accordingly. For example, if your columns are called `a` and + `b`, then passing {'a': 'green', 'b': 'red'} will color %(kind)ss for + column `a` in green and %(kind)ss for column `b` in red. + sampling_n (int, default 100): + Number of random items for plotting. + sampling_random_state (int, default 0): + Seed for random number generator. + **kwargs: + Additional keyword arguments are documented in + :meth:`DataFrame.plot`. + + Returns: + matplotlib.axes.Axes or np.ndarray of them: + An ndarray is returned with one :class:`matplotlib.axes.Axes` + per column when ``subplots=True``. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def area( + self, + x: typing.Optional[typing.Hashable] = None, + y: typing.Optional[typing.Hashable] = None, + stacked: bool = True, + **kwargs, + ): + """ + Draw a stacked area plot. An area plot displays quantitative data visually. + + This function calls `pandas.plot` to generate a plot with a random sample + of items. For consistent results, the random sampling is reproducible. + Use the `sampling_random_state` parameter to modify the sampling seed. + + **Examples:** + + Draw an area plot based on basic business metrics: + + >>> import bigframes.pandas as bpd + >>> df = bpd.DataFrame( + ... { + ... 'sales': [3, 2, 3, 9, 10, 6], + ... 'signups': [5, 5, 6, 12, 14, 13], + ... 'visits': [20, 42, 28, 62, 81, 50], + ... }, + ... index=["01-31", "02-28", "03-31", "04-30", "05-31", "06-30"] + ... ) + >>> ax = df.plot.area() + + Area plots are stacked by default. To produce an unstacked plot, + pass ``stacked=False``: + + >>> ax = df.plot.area(stacked=False) + + Draw an area plot for a single column: + + >>> ax = df.plot.area(y='sales') + + Draw with a different `x`: + + >>> df = bpd.DataFrame({ + ... 'sales': [3, 2, 3], + ... 'visits': [20, 42, 28], + ... 'day': [1, 2, 3], + ... }) + >>> ax = df.plot.area(x='day') + + Args: + x (label or position, optional): + Coordinates for the X axis. By default uses the index. + y (label or position, optional): + Column to plot. By default uses all columns. + stacked (bool, default True): + Area plots are stacked by default. Set to False to create a + unstacked plot. + sampling_n (int, default 100): + Number of random items for plotting. + sampling_random_state (int, default 0): + Seed for random number generator. + **kwargs: + Additional keyword arguments are documented in + :meth:`DataFrame.plot`. + + Returns: + matplotlib.axes.Axes or numpy.ndarray: + Area plot, or array of area plots if subplots is True. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def scatter( + self, + x: typing.Optional[typing.Hashable] = None, + y: typing.Optional[typing.Hashable] = None, + s: typing.Union[typing.Hashable, typing.Sequence[typing.Hashable]] = None, + c: typing.Union[typing.Hashable, typing.Sequence[typing.Hashable]] = None, + **kwargs, + ): + """ + Create a scatter plot with varying marker point size and color. + + This function calls `pandas.plot` to generate a plot with a random sample + of items. For consistent results, the random sampling is reproducible. + Use the `sampling_random_state` parameter to modify the sampling seed. + + **Examples:** + + Let's see how to draw a scatter plot using coordinates from the values + in a DataFrame's columns. + + >>> import bigframes.pandas as bpd + >>> df = bpd.DataFrame([[5.1, 3.5, 0], [4.9, 3.0, 0], [7.0, 3.2, 1], + ... [6.4, 3.2, 1], [5.9, 3.0, 2]], + ... columns=['length', 'width', 'species']) + >>> ax1 = df.plot.scatter(x='length', + ... y='width', + ... c='DarkBlue') + + And now with the color determined by a column as well. + + >>> ax2 = df.plot.scatter(x='length', + ... y='width', + ... c='species', + ... colormap='viridis') + + Args: + x (int or str): + The column name or column position to be used as horizontal + coordinates for each point. + y (int or str): + The column name or column position to be used as vertical + coordinates for each point. + s (str, scalar or array-like, optional): + The size of each point. Possible values are: + + - A string with the name of the column to be used for marker's size. + - A single scalar so all points have the same size. + - A sequence of scalars, which will be used for each point's size + recursively. For instance, when passing [2,14] all points size + will be either 2 or 14, alternatively. + + c (str, int or array-like, optional): + The color of each point. Possible values are: + + - A single color string referred to by name, RGB or RGBA code, + for instance 'red' or '#a98d19'. + - A sequence of color strings referred to by name, RGB or RGBA + code, which will be used for each point's color recursively. For + instance ['green','yellow'] all points will be filled in green or + yellow, alternatively. + - A column name or position whose values will be used to color the + marker points according to a colormap. + + sampling_n (int, default 100): + Number of random items for plotting. + sampling_random_state (int, default 0): + Seed for random number generator. + **kwargs: + Additional keyword arguments are documented in + :meth:`DataFrame.plot`. + + Returns: + matplotlib.axes.Axes or np.ndarray of them: + An ndarray is returned with one :class:`matplotlib.axes.Axes` + per column when ``subplots=True``. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 56de98290a41ffdeb85be2750fd45b8825b75381 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:22:56 -0700 Subject: [PATCH 360/921] chore(main): release 0.25.0 (#440) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ bigframes/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 565fe432415..4edd37bed39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.25.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.24.0...v0.25.0) (2024-03-14) + + +### Features + +* (Series|DataFrame).plot.(line|area|scatter) ([#431](https://github.com/googleapis/python-bigquery-dataframes/issues/431)) ([0772510](https://github.com/googleapis/python-bigquery-dataframes/commit/077251084e3121019c56e5d6c16aebab16be8dc7)) +* Support CMEK for `remote_function` cloud functions ([#430](https://github.com/googleapis/python-bigquery-dataframes/issues/430)) ([2fd69f4](https://github.com/googleapis/python-bigquery-dataframes/commit/2fd69f4bed143fc8c040dac1c55288c1cb660f6e)) + ## [0.24.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.23.0...v0.24.0) (2024-03-12) diff --git a/bigframes/version.py b/bigframes/version.py index ae18e113efe..708390a7cd1 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.0" +__version__ = "0.25.0" From 456fb32b1799aef467506487c5c4808a8a9d7db2 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:22:52 -0700 Subject: [PATCH 361/921] chore: add deferred exec code samples (#439) * chore: add deferred exec code samples * fix tests * fix tests --- bigframes/_config/compute_options.py | 11 ++++++++++ .../pandas/core/config_init.py | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/bigframes/_config/compute_options.py b/bigframes/_config/compute_options.py index 20c31d39066..fb708b844cb 100644 --- a/bigframes/_config/compute_options.py +++ b/bigframes/_config/compute_options.py @@ -23,6 +23,17 @@ class ComputeOptions: """ Encapsulates configuration for compute options. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + + >>> bpd.options.compute.maximum_bytes_billed = 500 + >>> # df.to_pandas() # this should fail + google.api_core.exceptions.InternalServerError: 500 Query exceeded limit for bytes billed: 500. 10485760 or higher required. + + >>> bpd.options.compute.maximum_bytes_billed = None # reset option + Attributes: maximum_bytes_billed (int, Options): Limits the bytes billed for query jobs. Queries that will have diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index dfb91dfeb81..33c6b3e093a 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -15,6 +15,26 @@ display_options_doc = """ Encapsulates configuration for displaying objects. +**Examples:** + +Define Repr mode to "deferred" will prevent job execution in repr. + >>> import bigframes.pandas as bpd + >>> df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + + >>> bpd.options.display.repr_mode = "deferred" + >>> df.head(20) # will no longer run the job + Computation deferred. Computation will process 28.9 kB + +Users can also get a dry run of the job by accessing the query_job property before they've run the job. This will return a dry run instance of the job they can inspect. + >>> df.query_job.total_bytes_processed + 28947 + +User can execute the job by calling .to_pandas() + >>> # df.to_pandas() + +Reset option + >>> bpd.options.display.repr_mode = "head" + Attributes: max_columns (int, default 20): If `max_columns` is exceeded, switch to truncate view. From 95f5a6e749468743af65062e559bc35ac56f3c24 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 14 Mar 2024 15:14:29 -0700 Subject: [PATCH 362/921] feat: add DataFrame.pipe() method (#421) --- tests/system/small/test_dataframe.py | 25 +++++ tests/system/small/test_series.py | 25 +++++ .../bigframes_vendored/pandas/core/common.py | 42 +++++++ .../bigframes_vendored/pandas/core/generic.py | 105 +++++++++++++++++- 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 third_party/bigframes_vendored/pandas/core/common.py diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 61dcd778eff..be4211a2fc7 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1000,6 +1000,31 @@ def test_apply_series_scalar_callable( pandas.testing.assert_series_equal(bf_result, pd_result) +def test_df_pipe( + scalars_df_index, + scalars_pandas_df_index, +): + columns = ["int64_too", "int64_col"] + + def foo(x: int, y: int, df): + return (df + x) % y + + bf_result = ( + scalars_df_index[columns] + .pipe((foo, "df"), x=7, y=9) + .pipe(lambda x: x**2) + .to_pandas() + ) + + pd_result = ( + scalars_pandas_df_index[columns] + .pipe((foo, "df"), x=7, y=9) + .pipe(lambda x: x**2) + ) + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + def test_df_keys( scalars_df_index, scalars_pandas_df_index, diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 37b4f8c1dea..f5c5b1c2162 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -3203,3 +3203,28 @@ def test_apply_not_supported(scalars_dfs, col, lambda_, exception): bf_col = scalars_df[col] with pytest.raises(exception): bf_col.apply(lambda_, by_row=False) + + +def test_series_pipe( + scalars_df_index, + scalars_pandas_df_index, +): + column = "int64_too" + + def foo(x: int, y: int, df): + return (df + x) % y + + bf_result = ( + scalars_df_index[column] + .pipe((foo, "df"), x=7, y=9) + .pipe(lambda x: x**2) + .to_pandas() + ) + + pd_result = ( + scalars_pandas_df_index[column] + .pipe((foo, "df"), x=7, y=9) + .pipe(lambda x: x**2) + ) + + assert_series_equal(bf_result, pd_result) diff --git a/third_party/bigframes_vendored/pandas/core/common.py b/third_party/bigframes_vendored/pandas/core/common.py new file mode 100644 index 00000000000..ded5a22b8f7 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/common.py @@ -0,0 +1,42 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/common.py +from __future__ import annotations + +from typing import Callable, TYPE_CHECKING + +if TYPE_CHECKING: + from bigframes_vendored.pandas.pandas._typing import T + + +def pipe( + obj, func: Callable[..., T] | tuple[Callable[..., T], str], *args, **kwargs +) -> T: + """ + Apply a function ``func`` to object ``obj`` either by passing obj as the + first argument to the function or, in the case that the func is a tuple, + interpret the first element of the tuple as a function and pass the obj to + that function as a keyword argument whose key is the value of the second + element of the tuple. + + Args: + func (callable or tuple of (callable, str)): + Function to apply to this object or, alternatively, a + ``(callable, data_keyword)`` tuple where ``data_keyword`` is a + string indicating the keyword of ``callable`` that expects the + object. + args (iterable, optional): + Positional arguments passed into ``func``. + kwargs (dict, optional): + A dictionary of keyword arguments passed into ``func``. + + Returns: + object: the return type of ``func``. + """ + if isinstance(func, tuple): + func, target = func + if target in kwargs: + msg = f"{target} is both the pipe target and a keyword argument" + raise ValueError(msg) + kwargs[target] = obj + return func(*args, **kwargs) + else: + return func(obj, *args, **kwargs) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 01d8f7a174f..7f8e1f7b53e 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -1,12 +1,16 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/generic.py from __future__ import annotations -from typing import Iterator, Literal, Optional +from typing import Callable, Iterator, Literal, Optional, TYPE_CHECKING from bigframes_vendored.pandas.core import indexing +import bigframes_vendored.pandas.core.common as common from bigframes import constants +if TYPE_CHECKING: + from bigframes_vendored.pandas.pandas._typing import T + class NDFrame(indexing.IndexingMixin): """ @@ -963,6 +967,105 @@ def expanding(self, min_periods=1): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def pipe( + self, + func: Callable[..., T] | tuple[Callable[..., T], str], + *args, + **kwargs, + ) -> T: + """ + Apply chainable functions that expect Series or DataFrames. + + **Examples:** + + Constructing a income DataFrame from a dictionary. + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> data = [[8000, 1000], [9500, np.nan], [5000, 2000]] + >>> df = bpd.DataFrame(data, columns=['Salary', 'Others']) + >>> df + Salary Others + 0 8000 1000.0 + 1 9500 + 2 5000 2000.0 + + [3 rows x 2 columns] + + Functions that perform tax reductions on an income DataFrame. + + >>> def subtract_federal_tax(df): + ... return df * 0.9 + >>> def subtract_state_tax(df, rate): + ... return df * (1 - rate) + >>> def subtract_national_insurance(df, rate, rate_increase): + ... new_rate = rate + rate_increase + ... return df * (1 - new_rate) + + Instead of writing + + >>> subtract_national_insurance( + ... subtract_state_tax(subtract_federal_tax(df), rate=0.12), + ... rate=0.05, + ... rate_increase=0.02) # doctest: +SKIP + + You can write + + >>> ( + ... df.pipe(subtract_federal_tax) + ... .pipe(subtract_state_tax, rate=0.12) + ... .pipe(subtract_national_insurance, rate=0.05, rate_increase=0.02) + ... ) + Salary Others + 0 5892.48 736.56 + 1 6997.32 + 2 3682.8 1473.12 + + [3 rows x 2 columns] + + If you have a function that takes the data as (say) the second + argument, pass a tuple indicating which keyword expects the + data. For example, suppose ``national_insurance`` takes its data as ``df`` + in the second argument: + + >>> def subtract_national_insurance(rate, df, rate_increase): + ... new_rate = rate + rate_increase + ... return df * (1 - new_rate) + >>> ( + ... df.pipe(subtract_federal_tax) + ... .pipe(subtract_state_tax, rate=0.12) + ... .pipe( + ... (subtract_national_insurance, 'df'), + ... rate=0.05, + ... rate_increase=0.02 + ... ) + ... ) + Salary Others + 0 5892.48 736.56 + 1 6997.32 + 2 3682.8 1473.12 + + [3 rows x 2 columns] + + Args: + func (function): + Function to apply to this object. + ``args``, and ``kwargs`` are passed into ``func``. + Alternatively a ``(callable, data_keyword)`` tuple where + ``data_keyword`` is a string indicating the keyword of + ``callable`` that expects this object. + args (iterable, optional): + Positional arguments passed into ``func``. + kwargs (mapping, optional): + A dictionary of keyword arguments passed into ``func``. + + Returns: + same type as caller + """ + return common.pipe(self, func, *args, **kwargs) + def __nonzero__(self): raise ValueError( f"The truth value of a {type(self).__name__} is ambiguous. " From fde339b71c754e617c61052940215b77890b59e4 Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:06:09 -0700 Subject: [PATCH 363/921] feat: support datetime related casting in (Series|DataFrame|Index).astype (#442) * feat: support datetime related casting in (Series|DataFrame|Index).astype * chore: add deferred exec code samples (#439) * chore: add deferred exec code samples * fix tests * fix tests * feat: add DataFrame.pipe() method (#421) * addressed comments --------- Co-authored-by: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Co-authored-by: TrevorBergeron --- bigframes/core/compile/scalar_op_compiler.py | 59 +++++++++--- bigframes/dtypes.py | 48 ++++++++-- tests/system/small/test_series.py | 98 ++++++++++++++++++++ 3 files changed, 183 insertions(+), 22 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 3bcdd70581a..67761c03302 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -634,11 +634,56 @@ def struct_field_op_impl(x: ibis_types.Value, op: ops.StructFieldOp): return struct_value[name].name(name) +def numeric_to_datatime(x: ibis_types.Value, unit: str) -> ibis_types.TimestampValue: + if not isinstance(x, ibis_types.IntegerValue) and not isinstance( + x, ibis_types.FloatingValue + ): + raise TypeError("Non-numerical types are not supposed to reach this function.") + + if unit not in UNIT_TO_US_CONVERSION_FACTORS: + raise ValueError(f"Cannot convert input with unit '{unit}'.") + x_converted = x * UNIT_TO_US_CONVERSION_FACTORS[unit] + x_converted = x_converted.cast(ibis_dtypes.int64) + + # Note: Due to an issue where casting directly to a timestamp + # without a timezone does not work, we first cast to UTC. This + # approach appears to bypass a potential bug in Ibis's cast function, + # allowing for subsequent casting to a timestamp type without timezone + # information. Further investigation is needed to confirm this behavior. + return x_converted.to_timestamp(unit="us").cast( + ibis_dtypes.Timestamp(timezone="UTC") + ) + + @scalar_op_compiler.register_unary_op(ops.AsTypeOp, pass_op=True) def astype_op_impl(x: ibis_types.Value, op: ops.AsTypeOp): to_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(op.to_type) if isinstance(x, ibis_types.NullScalar): return ibis_types.null().cast(to_type) + + # When casting DATETIME column into INT column, we need to convert the column into TIMESTAMP first. + if to_type == ibis_dtypes.int64 and x.type() == ibis_dtypes.timestamp: + x_converted = x.cast(ibis_dtypes.Timestamp(timezone="UTC")) + return bigframes.dtypes.cast_ibis_value(x_converted, to_type) + + if to_type == ibis_dtypes.int64 and x.type() == ibis_dtypes.time: + # The conversion unit is set to "us" (microseconds) for consistency + # with pandas converting time64[us][pyarrow] to int64[pyarrow]. + return x.delta(ibis.time("00:00:00"), part="microsecond") + + if x.type() == ibis_dtypes.int64: + # The conversion unit is set to "us" (microseconds) for consistency + # with pandas converting int64[pyarrow] to timestamp[us][pyarrow], + # timestamp[us, tz=UTC][pyarrow], and time64[us][pyarrow]. + unit = "us" + x_converted = numeric_to_datatime(x, unit) + if to_type == ibis_dtypes.timestamp: + return x_converted.cast(ibis_dtypes.Timestamp()) + elif to_type == ibis_dtypes.Timestamp(timezone="UTC"): + return x_converted + elif to_type == ibis_dtypes.time: + return x_converted.time() + return bigframes.dtypes.cast_ibis_value(x, to_type) @@ -677,19 +722,7 @@ def to_datetime_op_impl(x: ibis_types.Value, op: ops.ToDatetimeOp): # The default unit is set to "ns" (nanoseconds) for consistency # with pandas, where "ns" is the default unit for datetime operations. unit = op.unit or "ns" - if unit not in UNIT_TO_US_CONVERSION_FACTORS: - raise ValueError(f"Cannot convert input with unit '{unit}'.") - x_converted = x * UNIT_TO_US_CONVERSION_FACTORS[unit] - x_converted = x_converted.cast(ibis_dtypes.int64) - - # Note: Due to an issue where casting directly to a timestamp - # without a timezone does not work, we first cast to UTC. This - # approach appears to bypass a potential bug in Ibis's cast function, - # allowing for subsequent casting to a timestamp type without timezone - # information. Further investigation is needed to confirm this behavior. - x = x_converted.to_timestamp(unit="us").cast( - ibis_dtypes.Timestamp(timezone="UTC") - ) + x = numeric_to_datatime(x, unit) return x.cast(ibis_dtypes.Timestamp(timezone="UTC" if op.utc else None)) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 366820f9f6c..d78a88dfebb 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -60,6 +60,7 @@ "boolean", "Float64", "Int64", + "int64[pyarrow]", "string", "string[pyarrow]", "timestamp[us, tz=UTC][pyarrow]", @@ -173,6 +174,9 @@ # "string" and "string[pyarrow]" are accepted BIGFRAMES_STRING_TO_BIGFRAMES["string[pyarrow]"] = pd.StringDtype(storage="pyarrow") +# special case - both "Int64" and "int64[pyarrow]" are accepted +BIGFRAMES_STRING_TO_BIGFRAMES["int64[pyarrow]"] = pd.Int64Dtype() + # For the purposes of dataframe.memory_usage # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#data_type_sizes DTYPE_BYTE_SIZES = { @@ -310,11 +314,12 @@ def bigframes_dtype_to_ibis_dtype( textwrap.dedent( f""" Unexpected data type {bigframes_dtype}. The following - str dtypes are supppted: 'boolean','Float64','Int64', 'string', - 'string[pyarrow]','timestamp[us, tz=UTC][pyarrow]', - 'timestamp[us][pyarrow]','date32[day][pyarrow]', - 'time64[us][pyarrow]'. The following pandas.ExtensionDtype are - supported: pandas.BooleanDtype(), pandas.Float64Dtype(), + str dtypes are supppted: 'boolean','Float64','Int64', + 'int64[pyarrow]','string','string[pyarrow]', + 'timestamp[us, tz=UTC][pyarrow]','timestamp[us][pyarrow]', + 'date32[day][pyarrow]','time64[us][pyarrow]'. + The following pandas.ExtensionDtype are supported: + pandas.BooleanDtype(), pandas.Float64Dtype(), pandas.Int64Dtype(), pandas.StringDtype(storage="pyarrow"), pd.ArrowDtype(pa.date32()), pd.ArrowDtype(pa.time64("us")), pd.ArrowDtype(pa.timestamp("us")), @@ -434,6 +439,9 @@ def cast_ibis_value( ibis_dtypes.string, ibis_dtypes.Decimal(precision=38, scale=9), ibis_dtypes.Decimal(precision=76, scale=38), + ibis_dtypes.time, + ibis_dtypes.timestamp, + ibis_dtypes.Timestamp(timezone="UTC"), ), ibis_dtypes.float64: ( ibis_dtypes.string, @@ -447,8 +455,15 @@ def cast_ibis_value( ibis_dtypes.Decimal(precision=38, scale=9), ibis_dtypes.Decimal(precision=76, scale=38), ibis_dtypes.binary, + ibis_dtypes.date, + ibis_dtypes.timestamp, + ibis_dtypes.Timestamp(timezone="UTC"), + ), + ibis_dtypes.date: ( + ibis_dtypes.string, + ibis_dtypes.timestamp, + ibis_dtypes.Timestamp(timezone="UTC"), ), - ibis_dtypes.date: (ibis_dtypes.string,), ibis_dtypes.Decimal(precision=38, scale=9): ( ibis_dtypes.float64, ibis_dtypes.Decimal(precision=76, scale=38), @@ -457,9 +472,24 @@ def cast_ibis_value( ibis_dtypes.float64, ibis_dtypes.Decimal(precision=38, scale=9), ), - ibis_dtypes.time: (), - ibis_dtypes.timestamp: (ibis_dtypes.Timestamp(timezone="UTC"),), - ibis_dtypes.Timestamp(timezone="UTC"): (ibis_dtypes.timestamp,), + ibis_dtypes.time: ( + ibis_dtypes.int64, + ibis_dtypes.string, + ), + ibis_dtypes.timestamp: ( + ibis_dtypes.date, + ibis_dtypes.int64, + ibis_dtypes.string, + ibis_dtypes.time, + ibis_dtypes.Timestamp(timezone="UTC"), + ), + ibis_dtypes.Timestamp(timezone="UTC"): ( + ibis_dtypes.date, + ibis_dtypes.int64, + ibis_dtypes.string, + ibis_dtypes.time, + ibis_dtypes.timestamp, + ), ibis_dtypes.binary: (ibis_dtypes.string,), } diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index f5c5b1c2162..e22037a1ce0 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2625,6 +2625,9 @@ def foo(x): ("int64_col", "boolean"), ("int64_col", pd.ArrowDtype(pa.decimal128(38, 9))), ("int64_col", pd.ArrowDtype(pa.decimal256(76, 38))), + ("int64_col", pd.ArrowDtype(pa.timestamp("us"))), + ("int64_col", pd.ArrowDtype(pa.timestamp("us", tz="UTC"))), + ("int64_col", "time64[us][pyarrow]"), ("bool_col", "Int64"), ("bool_col", "string[pyarrow]"), ("string_col", "binary[pyarrow]"), @@ -2633,9 +2636,17 @@ def foo(x): # raises a deprecation warning to use tz_localize/tz_convert instead, # but BigQuery always stores values as UTC and doesn't have to deal # with timezone conversions, so we'll allow it. + ("timestamp_col", "date32[day][pyarrow]"), + ("timestamp_col", "time64[us][pyarrow]"), ("timestamp_col", pd.ArrowDtype(pa.timestamp("us"))), + ("datetime_col", "date32[day][pyarrow]"), + ("datetime_col", "string[pyarrow]"), + ("datetime_col", "time64[us][pyarrow]"), ("datetime_col", pd.ArrowDtype(pa.timestamp("us", tz="UTC"))), ("date_col", "string[pyarrow]"), + ("date_col", pd.ArrowDtype(pa.timestamp("us"))), + ("date_col", pd.ArrowDtype(pa.timestamp("us", tz="UTC"))), + ("time_col", "string[pyarrow]"), # TODO(bmil): fix Ibis bug: BigQuery backend rounds to nearest int # ("float64_col", "Int64"), # TODO(bmil): decide whether to fix Ibis bug: BigQuery backend @@ -2653,6 +2664,24 @@ def test_astype(scalars_df_index, scalars_pandas_df_index, column, to_type): pd.testing.assert_series_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("column", "to_type"), + [ + ("timestamp_col", "int64[pyarrow]"), + ("datetime_col", "int64[pyarrow]"), + ("time_col", "int64[pyarrow]"), + ], +) +@skip_legacy_pandas +def test_date_time_astype_int( + scalars_df_index, scalars_pandas_df_index, column, to_type +): + bf_result = scalars_df_index[column].astype(to_type).to_pandas() + pd_result = scalars_pandas_df_index[column].astype(to_type) + pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + assert bf_result.dtype == "Int64" + + def test_string_astype_int(): pd_series = pd.Series(["4", "-7", "0", " -03"]) bf_series = series.Series(pd_series) @@ -2676,6 +2705,75 @@ def test_string_astype_float(): pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) +def test_string_astype_date(): + pd_series = pd.Series(["2014-08-15", "2215-08-15", "2016-02-29"]).astype( + pd.ArrowDtype(pa.string()) + ) + + bf_series = series.Series(pd_series) + + pd_result = pd_series.astype("date32[day][pyarrow]") + bf_result = bf_series.astype("date32[day][pyarrow]").to_pandas() + + pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + + +def test_string_astype_datetime(): + pd_series = pd.Series( + ["2014-08-15 08:15:12", "2015-08-15 08:15:12.654754", "2016-02-29 00:00:00"] + ).astype(pd.ArrowDtype(pa.string())) + + bf_series = series.Series(pd_series) + + pd_result = pd_series.astype(pd.ArrowDtype(pa.timestamp("us"))) + bf_result = bf_series.astype(pd.ArrowDtype(pa.timestamp("us"))).to_pandas() + + pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + + +def test_string_astype_timestamp(): + pd_series = pd.Series( + [ + "2014-08-15 08:15:12+00:00", + "2015-08-15 08:15:12.654754+05:00", + "2016-02-29 00:00:00+08:00", + ] + ).astype(pd.ArrowDtype(pa.string())) + + bf_series = series.Series(pd_series) + + pd_result = pd_series.astype(pd.ArrowDtype(pa.timestamp("us", tz="UTC"))) + bf_result = bf_series.astype( + pd.ArrowDtype(pa.timestamp("us", tz="UTC")) + ).to_pandas() + + pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + + +def test_timestamp_astype_string(): + bf_series = series.Series( + [ + "2014-08-15 08:15:12+00:00", + "2015-08-15 08:15:12.654754+05:00", + "2016-02-29 00:00:00+08:00", + ] + ).astype(pd.ArrowDtype(pa.timestamp("us", tz="UTC"))) + + expected_result = pd.Series( + [ + "2014-08-15 08:15:12+00", + "2015-08-15 03:15:12.654754+00", + "2016-02-28 16:00:00+00", + ] + ) + bf_result = bf_series.astype(pa.string()).to_pandas() + + pd.testing.assert_series_equal( + bf_result, expected_result, check_index_type=False, check_dtype=False + ) + assert bf_result.dtype == "string[pyarrow]" + + @pytest.mark.parametrize( "index", [0, 5, -2], From e16a8c0a6fb46cf1a7be12eec9471ae95d6f2c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 15 Mar 2024 09:53:18 -0500 Subject: [PATCH 364/921] fix: warn when `read_gbq` / `read_gbq_table` uses the snapshot time cache (#441) --- bigframes/session/__init__.py | 18 ++++++++++++++++++ tests/unit/resources.py | 12 ++++++++++++ tests/unit/session/test_session.py | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 656c62ef193..2bc612bdbe4 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -629,6 +629,24 @@ def _get_snapshot_sql_and_primary_key( job_config.labels["bigframes-api"] = api_name if use_cache and table_ref in self._df_snapshot.keys(): snapshot_timestamp = self._df_snapshot[table_ref] + + # Cache hit could be unexpected. See internal issue 329545805. + # Raise a warning with more information about how to avoid the + # problems with the cache. + warnings.warn( + f"Reading cached table from {snapshot_timestamp} to avoid " + "incompatibilies with previous reads of this table. To read " + "the latest version, set `use_cache=False` or close the " + "current session with Session.close() or " + "bigframes.pandas.close_session().", + # There are many layers before we get to (possibly) the user's code: + # pandas.read_gbq_table + # -> with_default_session + # -> Session.read_gbq_table + # -> _read_gbq_table + # -> _get_snapshot_sql_and_primary_key + stacklevel=6, + ) else: snapshot_timestamp = list( self.bqclient.query( diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 967e42548f1..b57cd853600 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -31,6 +31,9 @@ """Utilities for creating test resources.""" +TEST_SCHEMA = (google.cloud.bigquery.SchemaField("col", "INTEGER"),) + + def create_bigquery_session( bqclient: Optional[mock.Mock] = None, session_id: str = "abcxyz", @@ -44,6 +47,13 @@ def create_bigquery_session( bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" + # Mock the location. + table = mock.create_autospec(google.cloud.bigquery.Table, instance=True) + table._properties = {} + type(table).location = mock.PropertyMock(return_value="test-region") + type(table).schema = mock.PropertyMock(return_value=TEST_SCHEMA) + bqclient.get_table.return_value = table + if anonymous_dataset is None: anonymous_dataset = google.cloud.bigquery.DatasetReference( "test-project", @@ -61,6 +71,8 @@ def query_mock(query, *args, **kwargs): if query.startswith("SELECT CURRENT_TIMESTAMP()"): query_job.result = mock.MagicMock(return_value=[[datetime.datetime.now()]]) + else: + type(query_job).schema = mock.PropertyMock(return_value=TEST_SCHEMA) return query_job diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index b474c9f63e7..3e2b28c2004 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import os +import re from unittest import mock import google.api_core.exceptions +import google.cloud.bigquery import pytest import bigframes @@ -31,6 +34,22 @@ def test_read_gbq_missing_parts(missing_parts_table_id): session.read_gbq(missing_parts_table_id) +def test_read_gbq_cached_table(): + session = resources.create_bigquery_session() + table_ref = google.cloud.bigquery.TableReference( + google.cloud.bigquery.DatasetReference("my-project", "my_dataset"), + "my_table", + ) + session._df_snapshot[table_ref] = datetime.datetime( + 1999, 1, 2, 3, 4, 5, 678901, tzinfo=datetime.timezone.utc + ) + + with pytest.warns(UserWarning, match=re.escape("use_cache=False")): + df = session.read_gbq("my-project.my_dataset.my_table") + + assert "1999-01-02T03:04:05.678901" in df.sql + + @pytest.mark.parametrize( "not_found_table_id", [("unknown.dataset.table"), ("project.unknown.table"), ("project.dataset.unknown")], From 91bd39e8b194ddad09d53fca96201eee58063bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 15 Mar 2024 09:54:06 -0500 Subject: [PATCH 365/921] docs: add version information to bug template (#437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add version information to bug template * 🦉 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/ISSUE_TEMPLATE/bug_report.md | 19 ++++++++++++++++++ owlbot.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7b0900728e4..8bc1d5f787a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -22,6 +22,25 @@ If you are still having issues, please be sure to include as much information as - pip version: `pip --version` - `bigframes` version: `pip show bigframes` + +```python +import sys +import bigframes +import google.cloud.bigquery +import ibis +import pandas +import pyarrow +import sqlglot + +print(f"Python: {sys.version}") +print(f"bigframes=={bigframes.__version__}") +print(f"google-cloud-bigquery=={google.cloud.bigquery.__version__}") +print(f"ibis=={ibis.__version__}") +print(f"pandas=={pandas.__version__}") +print(f"pyarrow=={pyarrow.__version__}") +print(f"sqlglot=={sqlglot.__version__}") +``` + #### Steps to reproduce 1. ? diff --git a/owlbot.py b/owlbot.py index 4dc6d1aca30..77479401d50 100644 --- a/owlbot.py +++ b/owlbot.py @@ -16,6 +16,7 @@ import pathlib import re +import textwrap from synthtool import gcp import synthtool as s @@ -59,6 +60,35 @@ # Fixup files # ---------------------------------------------------------------------------- +# Encourage sharring all relevant versions in bug reports. +s.replace( + [".github/ISSUE_TEMPLATE/bug_report.md"], + re.escape("#### Steps to reproduce\n"), + textwrap.dedent( + """ + ```python + import sys + import bigframes + import google.cloud.bigquery + import ibis + import pandas + import pyarrow + import sqlglot + + print(f"Python: {sys.version}") + print(f"bigframes=={bigframes.__version__}") + print(f"google-cloud-bigquery=={google.cloud.bigquery.__version__}") + print(f"ibis=={ibis.__version__}") + print(f"pandas=={pandas.__version__}") + print(f"pyarrow=={pyarrow.__version__}") + print(f"sqlglot=={sqlglot.__version__}") + ``` + + #### Steps to reproduce + """, + ), +) + # Make sure build includes all necessary files. s.replace( ["MANIFEST.in"], From 1c3e668ceb26fd0f1377acbf6b95e8f4bcef40d6 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:38:54 -0700 Subject: [PATCH 366/921] feat: (Series|DataFrame).plot (#438) --- bigframes/operations/plotting.py | 57 +++++++++---------- .../system/small/operations/test_plotting.py | 28 +++++++++ .../pandas/plotting/_core.py | 38 ++++++++++++- 3 files changed, 93 insertions(+), 30 deletions(-) diff --git a/bigframes/operations/plotting.py b/bigframes/operations/plotting.py index cc9f71e5d17..ff748069937 100644 --- a/bigframes/operations/plotting.py +++ b/bigframes/operations/plotting.py @@ -23,17 +23,37 @@ class PlotAccessor(vendordt.PlotAccessor): __doc__ = vendordt.PlotAccessor.__doc__ + _common_kinds = ("line", "area", "hist") + _dataframe_kinds = ("scatter",) + _all_kinds = _common_kinds + _dataframe_kinds + + def __call__(self, **kwargs): + import bigframes.series as series + + if kwargs.pop("backend", None) is not None: + raise NotImplementedError( + f"Only support matplotlib backend for now. {constants.FEEDBACK_LINK}" + ) + + kind = kwargs.pop("kind", "line") + if kind not in self._all_kinds: + raise NotImplementedError( + f"{kind} is not a valid plot kind supported for now. {constants.FEEDBACK_LINK}" + ) + + data = self._parent.copy() + if kind in self._dataframe_kinds and isinstance(data, series.Series): + raise ValueError(f"plot kind {kind} can only be used for data frames") + + return bfplt.plot(data, kind=kind, **kwargs) + def __init__(self, data) -> None: self._parent = data def hist( self, by: typing.Optional[typing.Sequence[str]] = None, bins: int = 10, **kwargs ): - if kwargs.pop("backend", None) is not None: - raise NotImplementedError( - f"Only support matplotlib backend for now. {constants.FEEDBACK_LINK}" - ) - return bfplt.plot(self._parent.copy(), kind="hist", by=by, bins=bins, **kwargs) + return self(kind="hist", by=by, bins=bins, **kwargs) def line( self, @@ -41,13 +61,7 @@ def line( y: typing.Optional[typing.Hashable] = None, **kwargs, ): - return bfplt.plot( - self._parent.copy(), - kind="line", - x=x, - y=y, - **kwargs, - ) + return self(kind="line", x=x, y=y, **kwargs) def area( self, @@ -56,14 +70,7 @@ def area( stacked: bool = True, **kwargs, ): - return bfplt.plot( - self._parent.copy(), - kind="area", - x=x, - y=y, - stacked=stacked, - **kwargs, - ) + return self(kind="area", x=x, y=y, stacked=stacked, **kwargs) def scatter( self, @@ -73,12 +80,4 @@ def scatter( c: typing.Union[typing.Hashable, typing.Sequence[typing.Hashable]] = None, **kwargs, ): - return bfplt.plot( - self._parent.copy(), - kind="scatter", - x=x, - y=y, - s=s, - c=c, - **kwargs, - ) + return self(kind="scatter", x=x, y=y, s=s, c=c, **kwargs) diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index ce320b6f57e..876c8f7d04b 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -233,3 +233,31 @@ def test_sampling_plot_args_random_state(): msg = "numpy array are different" with pytest.raises(AssertionError, match=msg): tm.assert_almost_equal(ax_0.lines[0].get_data()[1], ax_2.lines[0].get_data()[1]) + + +@pytest.mark.parametrize( + ("kind", "col_names", "kwargs"), + [ + pytest.param("hist", ["int64_col", "int64_too"], {}), + pytest.param("line", ["int64_col", "int64_too"], {}), + pytest.param("area", ["int64_col", "int64_too"], {"stacked": False}), + pytest.param( + "scatter", ["int64_col", "int64_too"], {"x": "int64_col", "y": "int64_too"} + ), + pytest.param( + "scatter", + ["int64_col"], + {}, + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "uknown", + ["int64_col", "int64_too"], + {}, + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + ], +) +def test_plot_call(scalars_dfs, kind, col_names, kwargs): + scalars_df, _ = scalars_dfs + scalars_df[col_names].plot(kind=kind, **kwargs) diff --git a/third_party/bigframes_vendored/pandas/plotting/_core.py b/third_party/bigframes_vendored/pandas/plotting/_core.py index 2b0f0776956..d901f41ef87 100644 --- a/third_party/bigframes_vendored/pandas/plotting/_core.py +++ b/third_party/bigframes_vendored/pandas/plotting/_core.py @@ -4,7 +4,43 @@ class PlotAccessor: - """Make plots of Series or DataFrame with the `matplotlib` backend.""" + """ + Make plots of Series or DataFrame with the `matplotlib` backend. + + **Examples:** + For Series: + + >>> import bigframes.pandas as bpd + >>> ser = bpd.Series([1, 2, 3, 3]) + >>> plot = ser.plot(kind='hist', title="My plot") + + For DataFrame: + + >>> df = bpd.DataFrame({'length': [1.5, 0.5, 1.2, 0.9, 3], + ... 'width': [0.7, 0.2, 0.15, 0.2, 1.1]}, + ... index=['pig', 'rabbit', 'duck', 'chicken', 'horse']) + >>> plot = df.plot(title="DataFrame Plot") + + Args: + data (Series or DataFrame): + The object for which the method is called. + kind (str): + The kind of plot to produce: + + - 'line' : line plot (default) + - 'hist' : histogram + - 'area' : area plot + - 'scatter' : scatter plot (DataFrame only) + + **kwargs: + Options to pass to `pandas.DataFrame.plot` method. See pandas + documentation online for more on these arguments. + + Returns: + matplotlib.axes.Axes or np.ndarray of them: + An ndarray is returned with one :class:`matplotlib.axes.Axes` + per column when ``subplots=True``. + """ def hist( self, by: typing.Optional[typing.Sequence[str]] = None, bins: int = 10, **kwargs From 40ddb694b26d4dea4a05845a9007bc26c3991ff5 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 15 Mar 2024 18:04:18 +0000 Subject: [PATCH 367/921] test: enable BYOSA test for `remote_function` cloud function (#432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This support was added in [PR#407](https://togithub.com/googleapis/python-bigquery-dataframes/pull/407) but the test was only verified locally since the project `bigframes-load-testing` is latchkey managed and would require some extra configuration to set-up. This change does one step better by enabling the test in the automated pipelines by targeting it to a different project `bigframes-dev-perf` which is easier to set up through cloud console. Eventually it should be moved to run entirely in `bigframes-load-testing` after the necessary configuration is done through latchkey (created internal issue 329339908 to track the work). Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/large/test_remote_function.py | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index f8c5e98f1d8..c0a1f6c4ece 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1281,19 +1281,29 @@ def square(x): ) -@pytest.mark.skip("This requires additional project config.") +@pytest.mark.flaky(retries=2, delay=120) def test_remote_function_via_session_custom_sa(scalars_dfs): - # Set these values to run the test locally - # TODO(shobs): Automate and enable this test - PROJECT = "" - GCF_SERVICE_ACCOUNT = "" + # TODO(shobs): Automate the following set-up during testing in the test project. + # + # For upfront convenience, the following set up has been statically created + # in the project bigfrmames-dev-perf via cloud console: + # + # 1. Create a service account as per + # https://cloud.google.com/iam/docs/service-accounts-create#iam-service-accounts-create-console + # 2. Give necessary roles as per + # https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration + # + project = "bigframes-dev-perf" + gcf_service_account = ( + "bigframes-dev-perf-1@bigframes-dev-perf.iam.gserviceaccount.com" + ) - rf_session = bigframes.Session(context=bigframes.BigQueryOptions(project=PROJECT)) + rf_session = bigframes.Session(context=bigframes.BigQueryOptions(project=project)) try: @rf_session.remote_function( - [int], int, reuse=False, cloud_function_service_account=GCF_SERVICE_ACCOUNT + [int], int, reuse=False, cloud_function_service_account=gcf_service_account ) def square_num(x): if x is None: @@ -1316,7 +1326,7 @@ def square_num(x): gcf = rf_session.cloudfunctionsclient.get_function( name=square_num.bigframes_cloud_function ) - assert gcf.service_config.service_account_email == GCF_SERVICE_ACCOUNT + assert gcf.service_config.service_account_email == gcf_service_account finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( From b28f9fdd9681b3c9783a6e52322b70093e0283ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 15 Mar 2024 16:11:38 -0500 Subject: [PATCH 368/921] fix: raise `ValueError` when `read_pandas()` receives a bigframes `DataFrame` (#447) * fix: raise `ValueError` when `read_pandas()` receives a bigframes `DataFrame` * make actually a unit test --- bigframes/session/__init__.py | 6 ++++++ tests/unit/session/test_io_pandas.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 2bc612bdbe4..40831292de8 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -916,6 +916,12 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame def _read_pandas( self, pandas_dataframe: pandas.DataFrame, api_name: str ) -> dataframe.DataFrame: + if isinstance(pandas_dataframe, dataframe.DataFrame): + raise ValueError( + "read_pandas() expects a pandas.DataFrame, but got a " + "bigframes.pandas.DataFrame." + ) + if ( pandas_dataframe.size < MAX_INLINE_DF_SIZE # TODO(swast): Workaround data types limitation in inline data. diff --git a/tests/unit/session/test_io_pandas.py b/tests/unit/session/test_io_pandas.py index 959cccd80ec..81d02466ef0 100644 --- a/tests/unit/session/test_io_pandas.py +++ b/tests/unit/session/test_io_pandas.py @@ -13,7 +13,9 @@ # limitations under the License. import datetime +import re from typing import Dict, Union +import unittest.mock as mock import geopandas # type: ignore import numpy @@ -24,8 +26,11 @@ import pytest import bigframes.features +import bigframes.pandas import bigframes.session._io.pandas +from .. import resources + _LIST_OF_SCALARS = [ [1, 2, 3], [], @@ -475,3 +480,13 @@ def test_arrow_to_pandas_wrong_size_dtypes( ): with pytest.raises(ValueError, match=f"Number of types {len(dtypes)}"): bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) + + +def test_read_pandas_with_bigframes_dataframe(): + session = resources.create_bigquery_session() + df = mock.create_autospec(bigframes.pandas.DataFrame, instance=True) + + with pytest.raises( + ValueError, match=re.escape("read_pandas() expects a pandas.DataFrame") + ): + session.read_pandas(df) From adadb0658c35142fed228abbd9baa42f9372f44b Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Fri, 15 Mar 2024 14:37:31 -0700 Subject: [PATCH 369/921] fix: fix broken link in covid notebook (#450) * fix: fix broken link in covid notebook * Update bq_dataframes_covid_line_graphs.ipynb --------- Co-authored-by: Henry J Solberg --- notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb index 8b18cc89674..cbbcc57aec9 100644 --- a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb +++ b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb @@ -34,7 +34,7 @@ "\n", "\n", " \n", From 14ab8d834d793ac7644f066145912e6d50966881 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:58:16 -0700 Subject: [PATCH 370/921] docs: add the docs for loc and iloc indexers (#446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- docs/reference/bigframes.pandas/index.rst | 1 + docs/reference/bigframes.pandas/indexers.rst | 32 +++++++++++++++++++ docs/templates/toc.yml | 12 +++++-- .../bigframes_vendored/pandas/core/frame.py | 15 ++++++++- .../bigframes_vendored/pandas/core/series.py | 15 ++++++++- 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 docs/reference/bigframes.pandas/indexers.rst diff --git a/docs/reference/bigframes.pandas/index.rst b/docs/reference/bigframes.pandas/index.rst index c7ff586884e..3492f236ee9 100644 --- a/docs/reference/bigframes.pandas/index.rst +++ b/docs/reference/bigframes.pandas/index.rst @@ -9,6 +9,7 @@ BigQuery DataFrames (pandas) general_functions series frame + indexers indexing window groupby diff --git a/docs/reference/bigframes.pandas/indexers.rst b/docs/reference/bigframes.pandas/indexers.rst new file mode 100644 index 00000000000..a7388bcb6b7 --- /dev/null +++ b/docs/reference/bigframes.pandas/indexers.rst @@ -0,0 +1,32 @@ + +========= +Indexers +========= + +ILocDataFrameIndexer +-------------------- +.. autoclass:: bigframes.core.indexers.ILocDataFrameIndexer + :members: + :inherited-members: + :undoc-members: + +IlocSeriesIndexer +----------------- +.. autoclass:: bigframes.core.indexers.IlocSeriesIndexer + :members: + :inherited-members: + :undoc-members: + +LocDataFrameIndexer +------------------- +.. autoclass:: bigframes.core.indexers.LocDataFrameIndexer + :members: + :inherited-members: + :undoc-members: + +LocSeriesIndexer +---------------- +.. autoclass:: bigframes.core.indexers.LocSeriesIndexer + :members: + :inherited-members: + :undoc-members: diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 66973fc5a2d..224b5354160 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -40,8 +40,16 @@ - name: SeriesGroupBy uid: bigframes.core.groupby.SeriesGroupBy name: Groupby - - name: Indexes - uid: bigframes.core.indexes.index.Index + - items: + - name: ILocDataFrameIndexer + uid: bigframes.core.indexers.ILocDataFrameIndexer + - name: IlocSeriesIndexer + uid: bigframes.core.indexers.IlocSeriesIndexer + - name: LocDataFrameIndexer + uid: bigframes.core.indexers.LocDataFrameIndexer + - name: LocSeriesIndexer + uid: bigframes.core.indexers.LocSeriesIndexer + name: Indexers - name: pandas uid: bigframes.pandas - items: diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 313c6663c8b..7793b31a210 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -5111,7 +5111,20 @@ def replace( @property def iloc(self): - """Purely integer-location based indexing for selection by position.""" + """Purely integer-location based indexing for selection by position. + + Returns: + bigframes.core.indexers.ILocDataFrameIndexer: Purely integer-location Indexers. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def loc(self): + """Access a group of rows and columns by label(s) or a boolean array. + + Returns: + bigframes.core.indexers.ILocDataFrameIndexer: Indexers object. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index beaf8aedb10..ab96e731b9f 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3313,7 +3313,20 @@ def map( @property def iloc(self): - """Purely integer-location based indexing for selection by position.""" + """Purely integer-location based indexing for selection by position. + + Returns: + bigframes.core.indexers.IlocSeriesIndexer: Purely integer-location Indexers. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @property + def loc(self): + """Access a group of rows and columns by label(s) or a boolean array. + + Returns: + bigframes.core.indexers.LocSeriesIndexer: Indexers object. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property From 1df014010652e7827a2720a906d0afe482a30ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 18 Mar 2024 13:18:35 -0500 Subject: [PATCH 371/921] docs: indicate that project and location are optional in example notebooks (#451) --- notebooks/dataframes/dataframe.ipynb | 4442 +++++++++-------- .../bq_dataframes_llm_code_generation.ipynb | 152 +- .../bq_dataframes_llm_kmeans.ipynb | 462 +- ...q_dataframes_ml_drug_name_generation.ipynb | 7 + .../getting_started_bq_dataframes.ipynb | 906 +++- .../ml_fundamentals_bq_dataframes.ipynb | 3574 ++++++++++++- notebooks/location/regionalized.ipynb | 7 + .../bq_dataframes_ml_linear_regression.ipynb | 17 +- .../bq_dataframes_covid_line_graphs.ipynb | 174 +- 9 files changed, 7119 insertions(+), 2622 deletions(-) diff --git a/notebooks/dataframes/dataframe.ipynb b/notebooks/dataframes/dataframe.ipynb index c6b276af877..15da0755520 100644 --- a/notebooks/dataframes/dataframe.ipynb +++ b/notebooks/dataframes/dataframe.ipynb @@ -12,56 +12,12 @@ { "cell_type": "code", "execution_count": 1, - "id": "72ebb083-f06b-4408-b24d-f349bd0851e3", + "id": "96757c59-fc22-420e-a42f-c6cb956110ec", "metadata": {}, "outputs": [], "source": [ - "# On the instance where you are running jupyter,\n", - "# authenticate with gcloud first:\n", - "#\n", - "# gcloud auth application-default login\n", - "\n", "import bigframes.pandas as bpd\n", "\n", - "bpd.options.bigquery.location = \"US\"" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "96757c59-fc22-420e-a42f-c6cb956110ec", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "11c27813da5c4d2e8108bf4bd9e7e55d", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='Query job ccb31707-38d2-4d93-8502-e39352f322a3 is RUNNING. " ] }, "metadata": {}, @@ -106,13 +60,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d1fce57541264fa1b61e1acbc99393d7", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 1280ea98-5503-4b32-899b-65ce4b4ad50f is DONE. 582.8 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job fdc644e2-c008-485a-90b2-dc64e5c81f3b is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -149,203 +101,203 @@ " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", "
\n", - " \n", + " \n", " \"Colab Run in Colab\n", " \n", "
0e14b6493-9e7f-404f-840a-8a680cc364bfd60c6036-0ce1-4c90-8dd9-de3b403c92a82016MarlinsCubs187NationalsBrewers167
11f32b347-cbcb-4c31-a145-0e685306d168af72a0b9-65f7-49fb-9b30-d505068bdf6d2016MarlinsCubs189RedsBrewers172
20c2292d1-7398-48be-bf8e-b41dad5e1a43f57e1271-d217-400a-aea6-2e2d7d6a59a02016BravesCubs165OriolesRays166
38fbec734-a15a-42ab-8d51-60790de7750b198f4eed-a29f-41e2-8623-cb261e5ab3702016BravesCubs222RockiesGiants182
489e514d5-fbf5-4b9d-bdac-6ca45bfd18ddcb3ef033-dd57-41fd-b206-cdd3bc12c74f2016PhilliesCubs164TwinsIndians204
56a83e76c-dc0d-4924-9d3d-a2e7e0ab5b524be9f735-a98e-4689-87ce-852cc3a1e79d2016DiamondbacksCubs201Blue JaysOrioles184
676ea8662-c7e6-4c38-8f2a-efe373e428ce0b2de8c3-11d9-4f0f-a186-25b59f34a5d82016AthleticsCubs173YankeesMets182
766fad23d-6e89-4f99-be29-d49b6e94f95d60d80663-6ced-44aa-aad9-0f4bf8d3b4d22016AthleticsCubs176Red SoxRays191
8d977367c-cf0c-4687-95a0-eb4542efcb017e1c2095-4fea-454c-8773-096ceb6fb05c2016RockiesCubs180CardinalsPirates201
9a87070ff-1084-43ca-a7ba-69278f93ecbaf7f24ce3-7f9d-4e8a-986e-095db847c4c12016CardinalsCubs157RaysTwins189
10ea6b350d-3c1d-4737-878d-4465f66999f65c26e7fc-c99f-48b4-92c1-4a7208c8cfe92016CardinalsCubs218RaysTwins177
1146463c50-0f5c-4dca-a661-dd194464e7916d2cab13-dd85-477a-8769-669069f858362016CardinalsCubs160RoyalsRays183
1259134e6d-9d13-49aa-978e-c3c2300eb90fbca90342-7ddc-468e-b189-d43fad7528ec2016PiratesCubs178AstrosRays194
13387630a3-a894-4327-baa1-b24ec1a654d9630f4f78-03cc-43c1-9e57-ababb9c114182016PiratesCubs205DodgersGiants178
145d084e13-94fd-4995-b95a-4801ea3ed556c0cf1376-1115-4a2f-b457-3f82bbc41a892016GiantsCubs197TigersWhite Sox193
1534444c94-03ec-4d12-96af-68b8f399a22f46463c50-0f5c-4dca-a661-dd194464e7912016RedsCardinalsCubs198160
169580bffe-22e1-4975-978b-1b13e7505193392ad56d-972e-4f77-98e2-5f8577931cf82016RedsCubs188GiantsCardinals169
17645e6a08-afd6-4677-a5c9-01ef446b0cf3307730fa-bbed-4221-b4e6-a2492f546fd52016RedsCubs188Red SoxTwins251
1808981bd8-d1d7-48e1-8668-9098b8f7fe901cbc558f-7615-4fa9-bf97-7ccd62040d6f2016RedsCubs194MetsBraves151
19303703bb-b55f-476d-8faf-bf582169fb1d723348ba-1645-43fc-9e22-92994f7a63bd2016PadresCubs175AthleticsTwins153
2071ab82a4-6e07-430a-b695-1af3bc42ea61ffbd6ecc-82e1-4e5d-9bd1-4ea210be59922016NationalsCubs257TwinsMarlins185
21d1a110c2-f6c8-4029-bcd8-2f8a01e1561cf2747230-7df5-4535-a475-a1c823d0d6542016BrewersCubs178TwinsYankees180
226d111b57-fa0b-4f24-82df-ff33a26f0252db3b6f35-a7a4-430a-8703-2b2f25103e172016BrewersCubs171White SoxOrioles199
23a97e9539-bbbd-4e03-bf15-f25ea2c1d9235fc8c6f0-a70e-4d1b-877f-eb1ec8e6f6362016BrewersCubs248DiamondbacksGiants175
24dc0c9218-505c-4725-8c0c-40b72cca095695d548b6-2da8-4644-812e-b277fec5b91f2016AstrosCubs174BravesMets201
\n", @@ -354,64 +306,64 @@ ], "text/plain": [ " gameId year homeTeamName awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", " duration_minutes \n", - "0 187 \n", - "1 189 \n", - "2 165 \n", - "3 222 \n", - "4 164 \n", - "5 201 \n", - "6 173 \n", - "7 176 \n", - "8 180 \n", - "9 157 \n", - "10 218 \n", - "11 160 \n", - "12 178 \n", - "13 205 \n", - "14 197 \n", - "15 198 \n", - "16 188 \n", - "17 188 \n", - "18 194 \n", - "19 175 \n", - "20 257 \n", - "21 178 \n", - "22 171 \n", - "23 248 \n", - "24 174 \n", + "0 167 \n", + "1 172 \n", + "2 166 \n", + "3 182 \n", + "4 204 \n", + "5 184 \n", + "6 182 \n", + "7 191 \n", + "8 201 \n", + "9 189 \n", + "10 177 \n", + "11 183 \n", + "12 194 \n", + "13 178 \n", + "14 193 \n", + "15 160 \n", + "16 169 \n", + "17 251 \n", + "18 151 \n", + "19 153 \n", + "20 185 \n", + "21 180 \n", + "22 199 \n", + "23 175 \n", + "24 201 \n", "...\n", "\n", "[2431 rows x 5 columns]" ] }, - "execution_count": 4, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -423,7 +375,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "a6b8b3ac-1df8-46ff-ac4f-d6e7657fc80c", "metadata": {}, "outputs": [ @@ -433,7 +385,7 @@ "(2431, 5)" ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -453,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "34457cc7-e734-4e3f-9f2b-34cdd4e2aba4", "metadata": { "tags": [] @@ -470,7 +422,7 @@ "dtype: object" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -481,7 +433,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "b4f4383f-f596-41d8-aad2-2fd68d261cfd", "metadata": {}, "outputs": [ @@ -491,7 +443,7 @@ "Index(['gameId', 'year', 'homeTeamName', 'awayTeamName', 'duration_minutes'], dtype='object')" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -511,7 +463,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "c7017f3d-869d-42e3-bbd8-b3fbc408c2d0", "metadata": { "tags": [] @@ -519,13 +471,11 @@ "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "653525aaa4394009ae97f54ba868dcf8", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e8a94ab7-7833-43ac-bf14-bfd4310260b9 is DONE. 582.8 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job c4b8deed-0c47-4ce7-b013-d8b24997851a is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -533,13 +483,23 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "298f5e9b7b094a4992ae304aba43a479", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 8b1e4a6c-9f93-4588-9c34-ae324a42fd57 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e6830838-99ae-4162-a47f-2185bb9c1f27 is DONE. 193.8 kB processed. " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 18db85e7-c94f-46ec-b981-5c582b5ce22a is DONE. 261.3 kB processed. Open Job" + ], + "text/plain": [ + "" ] }, "metadata": {}, @@ -577,228 +537,228 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", - " Marlins vs Cubs\n", + " Nationals\n", + " Brewers\n", + " 167\n", + " Nationals vs Brewers\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", - " Marlins vs Cubs\n", + " Reds\n", + " Brewers\n", + " 172\n", + " Reds vs Brewers\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", - " Braves vs Cubs\n", + " Orioles\n", + " Rays\n", + " 166\n", + " Orioles vs Rays\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", - " Braves vs Cubs\n", + " Rockies\n", + " Giants\n", + " 182\n", + " Rockies vs Giants\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", - " Phillies vs Cubs\n", + " Twins\n", + " Indians\n", + " 204\n", + " Twins vs Indians\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", - " Diamondbacks vs Cubs\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", + " Blue Jays vs Orioles\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", - " Athletics vs Cubs\n", + " Yankees\n", + " Mets\n", + " 182\n", + " Yankees vs Mets\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", - " Athletics vs Cubs\n", + " Red Sox\n", + " Rays\n", + " 191\n", + " Red Sox vs Rays\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", - " Rockies vs Cubs\n", + " Cardinals\n", + " Pirates\n", + " 201\n", + " Cardinals vs Pirates\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", - " Cardinals vs Cubs\n", + " Rays\n", + " Twins\n", + " 189\n", + " Rays vs Twins\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", - " Cardinals vs Cubs\n", + " Rays\n", + " Twins\n", + " 177\n", + " Rays vs Twins\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", - " Cardinals vs Cubs\n", + " Royals\n", + " Rays\n", + " 183\n", + " Royals vs Rays\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", - " Pirates vs Cubs\n", + " Astros\n", + " Rays\n", + " 194\n", + " Astros vs Rays\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", - " Pirates vs Cubs\n", + " Dodgers\n", + " Giants\n", + " 178\n", + " Dodgers vs Giants\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", - " Giants vs Cubs\n", + " Tigers\n", + " White Sox\n", + " 193\n", + " Tigers vs White Sox\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", - " Reds vs Cubs\n", + " 160\n", + " Cardinals vs Cubs\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", - " Reds vs Cubs\n", + " Giants\n", + " Cardinals\n", + " 169\n", + " Giants vs Cardinals\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", - " Reds vs Cubs\n", + " Red Sox\n", + " Twins\n", + " 251\n", + " Red Sox vs Twins\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", - " Reds vs Cubs\n", + " Mets\n", + " Braves\n", + " 151\n", + " Mets vs Braves\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", - " Padres vs Cubs\n", + " Athletics\n", + " Twins\n", + " 153\n", + " Athletics vs Twins\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", - " Nationals vs Cubs\n", + " Twins\n", + " Marlins\n", + " 185\n", + " Twins vs Marlins\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", - " Brewers vs Cubs\n", + " Twins\n", + " Yankees\n", + " 180\n", + " Twins vs Yankees\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", - " Brewers vs Cubs\n", + " White Sox\n", + " Orioles\n", + " 199\n", + " White Sox vs Orioles\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", - " Brewers vs Cubs\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", + " Diamondbacks vs Giants\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", - " Astros vs Cubs\n", + " Braves\n", + " Mets\n", + " 201\n", + " Braves vs Mets\n", " \n", " \n", "\n", @@ -807,64 +767,64 @@ ], "text/plain": [ " gameId year homeTeamName awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", - " duration_minutes title \n", - "0 187 Marlins vs Cubs \n", - "1 189 Marlins vs Cubs \n", - "2 165 Braves vs Cubs \n", - "3 222 Braves vs Cubs \n", - "4 164 Phillies vs Cubs \n", - "5 201 Diamondbacks vs Cubs \n", - "6 173 Athletics vs Cubs \n", - "7 176 Athletics vs Cubs \n", - "8 180 Rockies vs Cubs \n", - "9 157 Cardinals vs Cubs \n", - "10 218 Cardinals vs Cubs \n", - "11 160 Cardinals vs Cubs \n", - "12 178 Pirates vs Cubs \n", - "13 205 Pirates vs Cubs \n", - "14 197 Giants vs Cubs \n", - "15 198 Reds vs Cubs \n", - "16 188 Reds vs Cubs \n", - "17 188 Reds vs Cubs \n", - "18 194 Reds vs Cubs \n", - "19 175 Padres vs Cubs \n", - "20 257 Nationals vs Cubs \n", - "21 178 Brewers vs Cubs \n", - "22 171 Brewers vs Cubs \n", - "23 248 Brewers vs Cubs \n", - "24 174 Astros vs Cubs \n", + " duration_minutes title \n", + "0 167 Nationals vs Brewers \n", + "1 172 Reds vs Brewers \n", + "2 166 Orioles vs Rays \n", + "3 182 Rockies vs Giants \n", + "4 204 Twins vs Indians \n", + "5 184 Blue Jays vs Orioles \n", + "6 182 Yankees vs Mets \n", + "7 191 Red Sox vs Rays \n", + "8 201 Cardinals vs Pirates \n", + "9 189 Rays vs Twins \n", + "10 177 Rays vs Twins \n", + "11 183 Royals vs Rays \n", + "12 194 Astros vs Rays \n", + "13 178 Dodgers vs Giants \n", + "14 193 Tigers vs White Sox \n", + "15 160 Cardinals vs Cubs \n", + "16 169 Giants vs Cardinals \n", + "17 251 Red Sox vs Twins \n", + "18 151 Mets vs Braves \n", + "19 153 Athletics vs Twins \n", + "20 185 Twins vs Marlins \n", + "21 180 Twins vs Yankees \n", + "22 199 White Sox vs Orioles \n", + "23 175 Diamondbacks vs Giants \n", + "24 201 Braves vs Mets \n", "...\n", "\n", "[2431 rows x 6 columns]" ] }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -885,19 +845,17 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "8bbe000a-36f0-4b6f-b403-b9ec28dd608b", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ea2f330fcba44a8ca8c9919641e6a881", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job ef76c434-c4bc-4b4c-bb06-61521fc85b15 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 2062db30-30ae-42cf-8afa-9b8f3493fd98 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -905,13 +863,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3f6c6bb6171c40129d023e08d73a75ad", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job d3e413ee-c0c3-49fe-a2ad-f61d671593eb is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 9c36c84f-e672-46e1-a134-7ef2c2e60b4e is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -949,228 +905,228 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", - " Marlins vs Cubs\n", + " Nationals\n", + " Brewers\n", + " 167\n", + " Nationals vs Brewers\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", - " Marlins vs Cubs\n", + " Reds\n", + " Brewers\n", + " 172\n", + " Reds vs Brewers\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", - " Braves vs Cubs\n", + " Orioles\n", + " Rays\n", + " 166\n", + " Orioles vs Rays\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", - " Braves vs Cubs\n", + " Rockies\n", + " Giants\n", + " 182\n", + " Rockies vs Giants\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", - " Phillies vs Cubs\n", + " Twins\n", + " Indians\n", + " 204\n", + " Twins vs Indians\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", - " Diamondbacks vs Cubs\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", + " Blue Jays vs Orioles\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", - " Athletics vs Cubs\n", + " Yankees\n", + " Mets\n", + " 182\n", + " Yankees vs Mets\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", - " Athletics vs Cubs\n", + " Red Sox\n", + " Rays\n", + " 191\n", + " Red Sox vs Rays\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", - " Rockies vs Cubs\n", + " Cardinals\n", + " Pirates\n", + " 201\n", + " Cardinals vs Pirates\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", - " Cardinals vs Cubs\n", + " Rays\n", + " Twins\n", + " 189\n", + " Rays vs Twins\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", - " Cardinals vs Cubs\n", + " Rays\n", + " Twins\n", + " 177\n", + " Rays vs Twins\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", - " Cardinals vs Cubs\n", + " Royals\n", + " Rays\n", + " 183\n", + " Royals vs Rays\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", - " Pirates vs Cubs\n", + " Astros\n", + " Rays\n", + " 194\n", + " Astros vs Rays\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", - " Pirates vs Cubs\n", + " Dodgers\n", + " Giants\n", + " 178\n", + " Dodgers vs Giants\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", - " Giants vs Cubs\n", + " Tigers\n", + " White Sox\n", + " 193\n", + " Tigers vs White Sox\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", - " Reds vs Cubs\n", + " 160\n", + " Cardinals vs Cubs\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", - " Reds vs Cubs\n", + " Giants\n", + " Cardinals\n", + " 169\n", + " Giants vs Cardinals\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", - " Reds vs Cubs\n", + " Red Sox\n", + " Twins\n", + " 251\n", + " Red Sox vs Twins\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", - " Reds vs Cubs\n", + " Mets\n", + " Braves\n", + " 151\n", + " Mets vs Braves\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", - " Padres vs Cubs\n", + " Athletics\n", + " Twins\n", + " 153\n", + " Athletics vs Twins\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", - " Nationals vs Cubs\n", + " Twins\n", + " Marlins\n", + " 185\n", + " Twins vs Marlins\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", - " Brewers vs Cubs\n", + " Twins\n", + " Yankees\n", + " 180\n", + " Twins vs Yankees\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", - " Brewers vs Cubs\n", + " White Sox\n", + " Orioles\n", + " 199\n", + " White Sox vs Orioles\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", - " Brewers vs Cubs\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", + " Diamondbacks vs Giants\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", - " Astros vs Cubs\n", + " Braves\n", + " Mets\n", + " 201\n", + " Braves vs Mets\n", " \n", " \n", "\n", @@ -1179,64 +1135,64 @@ ], "text/plain": [ " gameId year homeTeamName awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", - " duration_minutes headline \n", - "0 187 Marlins vs Cubs \n", - "1 189 Marlins vs Cubs \n", - "2 165 Braves vs Cubs \n", - "3 222 Braves vs Cubs \n", - "4 164 Phillies vs Cubs \n", - "5 201 Diamondbacks vs Cubs \n", - "6 173 Athletics vs Cubs \n", - "7 176 Athletics vs Cubs \n", - "8 180 Rockies vs Cubs \n", - "9 157 Cardinals vs Cubs \n", - "10 218 Cardinals vs Cubs \n", - "11 160 Cardinals vs Cubs \n", - "12 178 Pirates vs Cubs \n", - "13 205 Pirates vs Cubs \n", - "14 197 Giants vs Cubs \n", - "15 198 Reds vs Cubs \n", - "16 188 Reds vs Cubs \n", - "17 188 Reds vs Cubs \n", - "18 194 Reds vs Cubs \n", - "19 175 Padres vs Cubs \n", - "20 257 Nationals vs Cubs \n", - "21 178 Brewers vs Cubs \n", - "22 171 Brewers vs Cubs \n", - "23 248 Brewers vs Cubs \n", - "24 174 Astros vs Cubs \n", + " duration_minutes headline \n", + "0 167 Nationals vs Brewers \n", + "1 172 Reds vs Brewers \n", + "2 166 Orioles vs Rays \n", + "3 182 Rockies vs Giants \n", + "4 204 Twins vs Indians \n", + "5 184 Blue Jays vs Orioles \n", + "6 182 Yankees vs Mets \n", + "7 191 Red Sox vs Rays \n", + "8 201 Cardinals vs Pirates \n", + "9 189 Rays vs Twins \n", + "10 177 Rays vs Twins \n", + "11 183 Royals vs Rays \n", + "12 194 Astros vs Rays \n", + "13 178 Dodgers vs Giants \n", + "14 193 Tigers vs White Sox \n", + "15 160 Cardinals vs Cubs \n", + "16 169 Giants vs Cardinals \n", + "17 251 Red Sox vs Twins \n", + "18 151 Mets vs Braves \n", + "19 153 Athletics vs Twins \n", + "20 185 Twins vs Marlins \n", + "21 180 Twins vs Yankees \n", + "22 199 White Sox vs Orioles \n", + "23 175 Diamondbacks vs Giants \n", + "24 201 Braves vs Mets \n", "...\n", "\n", "[2431 rows x 6 columns]" ] }, - "execution_count": 9, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -1248,7 +1204,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "87eee643-28ac-4f4b-ac61-1f3de9c08a9d", "metadata": {}, "outputs": [], @@ -1258,19 +1214,17 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "fad6d3da-1f40-4c5f-94ec-0bdfe21ca5b6", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fae17c24b2be4a47a72cc067e7b38e8c", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 051b3d23-5ab2-4022-adfc-f6553eb8532d is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job f0ed19be-b1f5-4333-a51f-3c7872a2bbc6 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -1278,13 +1232,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5b93b75abff04a36b186e3894bc9e957", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job f7356669-04f8-46f9-bf9b-f8cd997d6162 is DONE. 213.3 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 863e72d3-b421-4e53-98bb-9bf634fe9a71 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -1321,203 +1273,203 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", + " Nationals\n", + " Brewers\n", + " 167\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", + " Reds\n", + " Brewers\n", + " 172\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", + " Orioles\n", + " Rays\n", + " 166\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", + " Rockies\n", + " Giants\n", + " 182\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", + " Twins\n", + " Indians\n", + " 204\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", + " Yankees\n", + " Mets\n", + " 182\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", + " Red Sox\n", + " Rays\n", + " 191\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", + " Cardinals\n", + " Pirates\n", + " 201\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", + " Rays\n", + " Twins\n", + " 189\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", + " Rays\n", + " Twins\n", + " 177\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", + " Royals\n", + " Rays\n", + " 183\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", + " Astros\n", + " Rays\n", + " 194\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", + " Dodgers\n", + " Giants\n", + " 178\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", + " Tigers\n", + " White Sox\n", + " 193\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", + " 160\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Giants\n", + " Cardinals\n", + " 169\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Red Sox\n", + " Twins\n", + " 251\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", + " Mets\n", + " Braves\n", + " 151\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", + " Athletics\n", + " Twins\n", + " 153\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", + " Twins\n", + " Marlins\n", + " 185\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", + " Twins\n", + " Yankees\n", + " 180\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", + " White Sox\n", + " Orioles\n", + " 199\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", + " Braves\n", + " Mets\n", + " 201\n", " \n", " \n", "\n", @@ -1526,64 +1478,64 @@ ], "text/plain": [ " gameId year homeTeamName awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", " duration_minutes \n", - "0 187 \n", - "1 189 \n", - "2 165 \n", - "3 222 \n", - "4 164 \n", - "5 201 \n", - "6 173 \n", - "7 176 \n", - "8 180 \n", - "9 157 \n", - "10 218 \n", - "11 160 \n", - "12 178 \n", - "13 205 \n", - "14 197 \n", - "15 198 \n", - "16 188 \n", - "17 188 \n", - "18 194 \n", - "19 175 \n", - "20 257 \n", - "21 178 \n", - "22 171 \n", - "23 248 \n", - "24 174 \n", + "0 167 \n", + "1 172 \n", + "2 166 \n", + "3 182 \n", + "4 204 \n", + "5 184 \n", + "6 182 \n", + "7 191 \n", + "8 201 \n", + "9 189 \n", + "10 177 \n", + "11 183 \n", + "12 194 \n", + "13 178 \n", + "14 193 \n", + "15 160 \n", + "16 169 \n", + "17 251 \n", + "18 151 \n", + "19 153 \n", + "20 185 \n", + "21 180 \n", + "22 199 \n", + "23 175 \n", + "24 201 \n", "...\n", "\n", "[2431 rows x 5 columns]" ] }, - "execution_count": 11, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -1603,19 +1555,29 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "id": "67a7c35f-80cf-4482-80f9-7f01c7743807", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b5b2da9ef7864a51adfbc7d3c85c46b7", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job bd315bd7-1f10-4f1b-9997-10a294b1f464 is DONE. 232.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 972bf072-22c2-49ef-8764-1c1109dfc0a3 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job e387be31-99fc-46a9-9de7-3bb83ff1f4fe is DONE. 174.4 kB processed. " ] }, "metadata": {}, @@ -1623,13 +1585,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "32a12de102694ac8bbf0dfa16d17be72", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 42c0cf8a-4276-479f-b8de-dcfce94ae42a is DONE. 213.3 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job af23a9a2-151d-469a-a5c7-588ea60d1602 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -1666,203 +1626,203 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", + " Nationals\n", + " Brewers\n", + " 167\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", + " Reds\n", + " Brewers\n", + " 172\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", + " Orioles\n", + " Rays\n", + " 166\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", + " Rockies\n", + " Giants\n", + " 182\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", + " Twins\n", + " Indians\n", + " 204\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", + " Yankees\n", + " Mets\n", + " 182\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", + " Red Sox\n", + " Rays\n", + " 191\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", + " Cardinals\n", + " Pirates\n", + " 201\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", + " Rays\n", + " Twins\n", + " 189\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", + " Rays\n", + " Twins\n", + " 177\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", + " Royals\n", + " Rays\n", + " 183\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", + " Astros\n", + " Rays\n", + " 194\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", + " Dodgers\n", + " Giants\n", + " 178\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", + " Tigers\n", + " White Sox\n", + " 193\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", + " 160\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Giants\n", + " Cardinals\n", + " 169\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Red Sox\n", + " Twins\n", + " 251\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", + " Mets\n", + " Braves\n", + " 151\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", + " Athletics\n", + " Twins\n", + " 153\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", + " Twins\n", + " Marlins\n", + " 185\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", + " Twins\n", + " Yankees\n", + " 180\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", + " White Sox\n", + " Orioles\n", + " 199\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", + " Braves\n", + " Mets\n", + " 201\n", " \n", " \n", "\n", @@ -1871,64 +1831,64 @@ ], "text/plain": [ " gameId year homeTeamName awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", " duration_minutes \n", - "0 187 \n", - "1 189 \n", - "2 165 \n", - "3 222 \n", - "4 164 \n", - "5 201 \n", - "6 173 \n", - "7 176 \n", - "8 180 \n", - "9 157 \n", - "10 218 \n", - "11 160 \n", - "12 178 \n", - "13 205 \n", - "14 197 \n", - "15 198 \n", - "16 188 \n", - "17 188 \n", - "18 194 \n", - "19 175 \n", - "20 257 \n", - "21 178 \n", - "22 171 \n", - "23 248 \n", - "24 174 \n", + "0 167 \n", + "1 172 \n", + "2 166 \n", + "3 182 \n", + "4 204 \n", + "5 184 \n", + "6 182 \n", + "7 191 \n", + "8 201 \n", + "9 189 \n", + "10 177 \n", + "11 183 \n", + "12 194 \n", + "13 178 \n", + "14 193 \n", + "15 160 \n", + "16 169 \n", + "17 251 \n", + "18 151 \n", + "19 153 \n", + "20 185 \n", + "21 180 \n", + "22 199 \n", + "23 175 \n", + "24 201 \n", "...\n", "\n", "[2431 rows x 5 columns]" ] }, - "execution_count": 12, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -1949,19 +1909,17 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "3f09ff32-ef43-4fab-a86b-8868afc34363", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "30a9e3bd880a4c718ec3a581e0139e21", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 3c859587-582d-4b68-8b35-7072b9a42346 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job b14b9796-f94d-48c0-a477-13f6b865e11d is DONE. 174.4 kB processed. " ] }, "metadata": {}, @@ -1969,13 +1927,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7a1e5045b4ae4567b7be9f6f7bf39e3a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 15bf1d87-152a-45a5-b000-e8e72ce6a982 is DONE. 152.8 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 20f025c3-4d84-49e9-9fbd-d77eeb2c6e04 is RUNNING. " ] }, "metadata": {}, @@ -2009,128 +1965,128 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", - " Marlins\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", + " Nationals\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", - " Marlins\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", + " Reds\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", - " Braves\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", + " Orioles\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", - " Braves\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", + " Rockies\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", - " Phillies\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", + " Twins\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", - " Diamondbacks\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", + " Blue Jays\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", - " Athletics\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", + " Yankees\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", - " Athletics\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", + " Red Sox\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", - " Rockies\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", + " Cardinals\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", - " Cardinals\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", + " Rays\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", - " Cardinals\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", + " Rays\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", - " Cardinals\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", + " Royals\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", - " Pirates\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", + " Astros\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", - " Pirates\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", + " Dodgers\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", - " Giants\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", + " Tigers\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", - " Reds\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " Cardinals\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", - " Reds\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", + " Giants\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", - " Reds\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", + " Red Sox\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", - " Reds\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", + " Mets\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", - " Padres\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", + " Athletics\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", - " Nationals\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", + " Twins\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", - " Brewers\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", + " Twins\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", - " Brewers\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", + " White Sox\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", - " Brewers\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", + " Diamondbacks\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", - " Astros\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", + " Braves\n", " \n", " \n", "\n", @@ -2139,37 +2095,37 @@ ], "text/plain": [ " gameId homeTeamName\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins\n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins\n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 Braves\n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b Braves\n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd Phillies\n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 Diamondbacks\n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce Athletics\n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d Athletics\n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 Rockies\n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba Cardinals\n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 Cardinals\n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals\n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f Pirates\n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 Pirates\n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 Giants\n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f Reds\n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 Reds\n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 Reds\n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 Reds\n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d Padres\n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 Nationals\n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c Brewers\n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 Brewers\n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 Brewers\n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 Astros\n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals\n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds\n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 Orioles\n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 Rockies\n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f Twins\n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d Blue Jays\n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 Yankees\n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 Red Sox\n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c Cardinals\n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 Rays\n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 Rays\n", + "11 6d2cab13-dd85-477a-8769-669069f85836 Royals\n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec Astros\n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 Dodgers\n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 Tigers\n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals\n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 Giants\n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 Red Sox\n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f Mets\n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd Athletics\n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 Twins\n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 Twins\n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 White Sox\n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 Diamondbacks\n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f Braves\n", "...\n", "\n", "[2431 rows x 2 columns]" ] }, - "execution_count": 13, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -2181,19 +2137,29 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "id": "5331d2c8-7912-4d96-8da1-f64b57374df3", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2b06ade302254b7399d74edca095140c", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 262a8d65-8eb7-4769-b26d-4a1d93f19950 is DONE. 152.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a18f6c86-dbff-4846-8d21-8f8c1d700a80 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job fbd5deef-4c7f-4345-ab6c-28c3e24bd918 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2201,13 +2167,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e7413e47e1344d498851383a077917ed", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 9cc89303-be7a-4c34-b4c0-d1d75837a1e4 is DONE. 126 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job ba580b64-ca65-4245-b17f-12fd382b2e2b is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2241,13 +2205,13 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", - " Cubs\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", + " Brewers\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", - " Cubs\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", + " Brewers\n", " \n", " \n", "\n", @@ -2256,13 +2220,13 @@ ], "text/plain": [ " gameId awayTeamName\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf Cubs\n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 Cubs\n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Brewers\n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d Brewers\n", "\n", "[2 rows x 2 columns]" ] }, - "execution_count": 14, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -2274,19 +2238,29 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "id": "a574ad3e-a219-454c-8bb5-c5ed6627f2c6", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0e076fa03b6b41878386abcaa9aeb757", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 1edf3455-802d-4b93-900b-9677cb43955a is DONE. 133.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 98cccaa5-e630-4edf-bc15-2823e89aecb6 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 3f54256f-7189-400b-8d47-5ce1f6fe92c0 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2294,13 +2268,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3a87a0d2d7cc4d429191b6e7ceeb8a0b", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 91af749e-6afa-488a-83da-1257667460f0 is DONE. 143 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 57211b59-73c2-42d6-88bd-a614a8baf779 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2335,15 +2307,15 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", - " Marlins\n", - " Cubs\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", + " Nationals\n", + " Brewers\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", - " Marlins\n", - " Cubs\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", + " Reds\n", + " Brewers\n", " \n", " \n", "\n", @@ -2352,13 +2324,13 @@ ], "text/plain": [ " gameId homeTeamName awayTeamName\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals Brewers\n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds Brewers\n", "\n", "[2 rows x 3 columns]" ] }, - "execution_count": 15, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -2369,19 +2341,29 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "id": "288e7a95-a077-46c4-8fe6-802474c01f8b", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e974466302964b7785881ed1ce96ec75", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job dfe4d1ec-9a3d-4877-ab39-bb6f1c38d070 is DONE. 133.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 6261a857-d256-4051-8af5-c6b04fb2795f is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 07ee6beb-b805-4ba9-8cb2-174d4e62ddfb is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2389,13 +2371,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4f07980adb1f4f9eb2e2f80de5f0a174", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job df400799-b054-4969-83a0-089fb2b25fdd is DONE. 152.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job ced45fc3-cfdb-4cd0-96ef-16a29d8d8f0b is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2430,152 +2410,152 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", - " Marlins\n", - " Cubs\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", + " Nationals\n", + " Brewers\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", - " Marlins\n", - " Cubs\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", + " Reds\n", + " Brewers\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", - " Braves\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", + " Orioles\n", " <NA>\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", - " Braves\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", + " Rockies\n", " <NA>\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", - " Phillies\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", + " Twins\n", " <NA>\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", - " Diamondbacks\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", + " Blue Jays\n", " <NA>\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", - " Athletics\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", + " Yankees\n", " <NA>\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", - " Athletics\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", + " Red Sox\n", " <NA>\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", - " Rockies\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", + " Cardinals\n", " <NA>\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", - " Cardinals\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", + " Rays\n", " <NA>\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", - " Cardinals\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", + " Rays\n", " <NA>\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", - " Cardinals\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", + " Royals\n", " <NA>\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", - " Pirates\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", + " Astros\n", " <NA>\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", - " Pirates\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", + " Dodgers\n", " <NA>\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", - " Giants\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", + " Tigers\n", " <NA>\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", - " Reds\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " Cardinals\n", " <NA>\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", - " Reds\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", + " Giants\n", " <NA>\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", - " Reds\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", + " Red Sox\n", " <NA>\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", - " Reds\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", + " Mets\n", " <NA>\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", - " Padres\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", + " Athletics\n", " <NA>\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", - " Nationals\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", + " Twins\n", " <NA>\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", - " Brewers\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", + " Twins\n", " <NA>\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", - " Brewers\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", + " White Sox\n", " <NA>\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", - " Brewers\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", + " Diamondbacks\n", " <NA>\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", - " Astros\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", + " Braves\n", " <NA>\n", " \n", " \n", @@ -2585,37 +2565,37 @@ ], "text/plain": [ " gameId homeTeamName awayTeamName\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 Braves \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b Braves \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd Phillies \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 Diamondbacks \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce Athletics \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d Athletics \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 Rockies \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba Cardinals \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 Cardinals \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f Pirates \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 Pirates \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 Giants \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f Reds \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 Reds \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 Reds \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 Reds \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d Padres \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 Nationals \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c Brewers \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 Brewers \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 Brewers \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 Astros \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals Brewers\n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds Brewers\n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 Orioles \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 Rockies \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f Twins \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d Blue Jays \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 Yankees \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 Red Sox \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c Cardinals \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 Rays \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 Rays \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 Royals \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec Astros \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 Dodgers \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 Tigers \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 Giants \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 Red Sox \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f Mets \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd Athletics \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 Twins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 Twins \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 White Sox \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 Diamondbacks \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f Braves \n", "...\n", "\n", "[2431 rows x 3 columns]" ] }, - "execution_count": 16, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -2626,19 +2606,29 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "id": "7ee87a01-2ff5-4021-855d-44b71cf2a225", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c83cdc3e86ff4b3694acb25b1eda845a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 9ae1e55b-36d0-4aef-ae39-67a3ad5fdb4d is DONE. 133.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7a1822eb-7db9-4c54-abd5-74cb1cde6121 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 60335c33-acc9-4a4a-9e08-190fe67ad60e is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2646,13 +2636,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "14f1635a78d5414b9533f31436096e6e", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 783d092c-5f79-4601-9365-633e48fac610 is DONE. 152.9 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 8c4e016f-429d-4591-8956-56fb18676334 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2687,152 +2675,152 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", - " Marlins\n", - " Cubs\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", + " Nationals\n", + " Brewers\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", - " Marlins\n", - " Cubs\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", + " Reds\n", + " Brewers\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", - " Braves\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", + " Orioles\n", " <NA>\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", - " Braves\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", + " Rockies\n", " <NA>\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", - " Phillies\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", + " Twins\n", " <NA>\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", - " Diamondbacks\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", + " Blue Jays\n", " <NA>\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", - " Athletics\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", + " Yankees\n", " <NA>\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", - " Athletics\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", + " Red Sox\n", " <NA>\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", - " Rockies\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", + " Cardinals\n", " <NA>\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", - " Cardinals\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", + " Rays\n", " <NA>\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", - " Cardinals\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", + " Rays\n", " <NA>\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", - " Cardinals\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", + " Royals\n", " <NA>\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", - " Pirates\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", + " Astros\n", " <NA>\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", - " Pirates\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", + " Dodgers\n", " <NA>\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", - " Giants\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", + " Tigers\n", " <NA>\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", - " Reds\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " Cardinals\n", " <NA>\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", - " Reds\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", + " Giants\n", " <NA>\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", - " Reds\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", + " Red Sox\n", " <NA>\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", - " Reds\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", + " Mets\n", " <NA>\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", - " Padres\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", + " Athletics\n", " <NA>\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", - " Nationals\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", + " Twins\n", " <NA>\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", - " Brewers\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", + " Twins\n", " <NA>\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", - " Brewers\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", + " White Sox\n", " <NA>\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", - " Brewers\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", + " Diamondbacks\n", " <NA>\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", - " Astros\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", + " Braves\n", " <NA>\n", " \n", " \n", @@ -2842,37 +2830,37 @@ ], "text/plain": [ " gameId homeTeamName awayTeamName\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 Braves \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b Braves \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd Phillies \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 Diamondbacks \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce Athletics \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d Athletics \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 Rockies \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba Cardinals \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 Cardinals \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f Pirates \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 Pirates \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 Giants \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f Reds \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 Reds \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 Reds \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 Reds \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d Padres \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 Nationals \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c Brewers \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 Brewers \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 Brewers \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 Astros \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals Brewers\n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds Brewers\n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 Orioles \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 Rockies \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f Twins \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d Blue Jays \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 Yankees \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 Red Sox \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c Cardinals \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 Rays \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 Rays \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 Royals \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec Astros \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 Dodgers \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 Tigers \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 Cardinals \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 Giants \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 Red Sox \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f Mets \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd Athletics \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 Twins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 Twins \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 White Sox \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 Diamondbacks \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f Braves \n", "...\n", "\n", "[2431 rows x 3 columns]" ] }, - "execution_count": 17, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -2883,19 +2871,29 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "id": "330ed69c-f122-4af9-bf5e-96e309d3fa0c", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d97026197292402daa7176d5aac8c583", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job ec1c442e-6ea1-461c-ada7-e3dd0454b0ca is DONE. 133.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 4ababa83-ad57-4520-b49d-e613256ae2f3 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 948a3d0b-1c3d-479b-b54f-9a2b2062380e is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2903,13 +2901,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9b987688bfdd49989e0dcae375a33740", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job f3db2d14-b877-46ea-8858-5cdb3706e26a is DONE. 143 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 20d2a8bb-a876-4729-a439-8c8bbf591051 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -2944,15 +2940,15 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", - " Marlins\n", - " Cubs\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", + " Nationals\n", + " Brewers\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", - " Marlins\n", - " Cubs\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", + " Reds\n", + " Brewers\n", " \n", " \n", "\n", @@ -2961,13 +2957,13 @@ ], "text/plain": [ " gameId homeTeamName awayTeamName\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins Cubs\n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins Cubs\n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals Brewers\n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds Brewers\n", "\n", "[2 rows x 3 columns]" ] }, - "execution_count": 18, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -2987,19 +2983,17 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "id": "5181231e-8a2a-4ac5-a379-6aa5ad4fee89", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "862fd15acf82434fb153121c74164b5f", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job f0e1bda5-34f5-46e2-a396-289340074f82 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job ea340371-7874-4590-bb5e-f747f81397de is DONE. 174.4 kB processed. " ] }, "metadata": {}, @@ -3007,13 +3001,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6f9e64af012140619d1f4190b06862e6", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e7195b4a-f1ea-4bef-a4db-fa817144d249 is DONE. 213.3 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 5c35116b-1c4a-4cc1-9ddd-172083d09490 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -3050,203 +3042,203 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", + " Nationals\n", + " Brewers\n", + " 167\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", + " Reds\n", + " Brewers\n", + " 172\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", + " Orioles\n", + " Rays\n", + " 166\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", + " Rockies\n", + " Giants\n", + " 182\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", + " Twins\n", + " Indians\n", + " 204\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", + " Yankees\n", + " Mets\n", + " 182\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", + " Red Sox\n", + " Rays\n", + " 191\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", + " Cardinals\n", + " Pirates\n", + " 201\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", + " Rays\n", + " Twins\n", + " 189\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", + " Rays\n", + " Twins\n", + " 177\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", + " Royals\n", + " Rays\n", + " 183\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", + " Astros\n", + " Rays\n", + " 194\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", + " Dodgers\n", + " Giants\n", + " 178\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", + " Tigers\n", + " White Sox\n", + " 193\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", + " 160\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", - " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", + " 2016\n", + " Giants\n", + " Cardinals\n", + " 169\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Red Sox\n", + " Twins\n", + " 251\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", + " Mets\n", + " Braves\n", + " 151\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", + " Athletics\n", + " Twins\n", + " 153\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", + " Twins\n", + " Marlins\n", + " 185\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", + " Twins\n", + " Yankees\n", + " 180\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", + " White Sox\n", + " Orioles\n", + " 199\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", + " Braves\n", + " Mets\n", + " 201\n", " \n", " \n", "\n", @@ -3255,64 +3247,64 @@ ], "text/plain": [ " gameId year homeTeamName awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", " duration_minutes \n", - "0 187 \n", - "1 189 \n", - "2 165 \n", - "3 222 \n", - "4 164 \n", - "5 201 \n", - "6 173 \n", - "7 176 \n", - "8 180 \n", - "9 157 \n", - "10 218 \n", - "11 160 \n", - "12 178 \n", - "13 205 \n", - "14 197 \n", - "15 198 \n", - "16 188 \n", - "17 188 \n", - "18 194 \n", - "19 175 \n", - "20 257 \n", - "21 178 \n", - "22 171 \n", - "23 248 \n", - "24 174 \n", + "0 167 \n", + "1 172 \n", + "2 166 \n", + "3 182 \n", + "4 204 \n", + "5 184 \n", + "6 182 \n", + "7 191 \n", + "8 201 \n", + "9 189 \n", + "10 177 \n", + "11 183 \n", + "12 194 \n", + "13 178 \n", + "14 193 \n", + "15 160 \n", + "16 169 \n", + "17 251 \n", + "18 151 \n", + "19 153 \n", + "20 185 \n", + "21 180 \n", + "22 199 \n", + "23 175 \n", + "24 201 \n", "...\n", "\n", "[4862 rows x 5 columns]" ] }, - "execution_count": 19, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -3332,19 +3324,29 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "id": "ad1f86f1-890b-462b-b408-b94c073371ff", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2d064a62c8424a93a0359eea715b4969", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 0745cde9-9175-4e11-9721-f0c58fae90a2 is DONE. 79.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 4c7a65d6-63b7-44b6-8249-171139f907f5 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 79bc2f65-5c7e-470d-b30e-a959838f0ed9 is DONE. 174.4 kB processed. " ] }, "metadata": {}, @@ -3352,13 +3354,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5136bbb1905d4d8d9d953d770f6dadf9", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job cee24fe1-cb33-4836-b158-90e293cbc057 is DONE. 60.5 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 9de0da3d-b43c-4381-8ecb-5b8c6d9d2c8b is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -3367,35 +3367,35 @@ { "data": { "text/plain": [ - "0 Marlins\n", - "1 Marlins\n", - "2 Braves\n", - "3 Braves\n", - "4 Phillies\n", - "5 Diamondbacks\n", - "6 Athletics\n", - "7 Athletics\n", - "8 Rockies\n", - "9 Cardinals\n", - "10 Cardinals\n", - "11 Cardinals\n", - "12 Pirates\n", - "13 Pirates\n", - "14 Giants\n", - "15 Reds\n", - "16 Reds\n", - "17 Reds\n", - "18 Reds\n", - "19 Padres\n", - "20 Nationals\n", - "21 Brewers\n", - "22 Brewers\n", - "23 Brewers\n", - "24 Astros\n", + "0 Nationals\n", + "1 Reds\n", + "2 Orioles\n", + "3 Rockies\n", + "4 Twins\n", + "5 Blue Jays\n", + "6 Yankees\n", + "7 Red Sox\n", + "8 Cardinals\n", + "9 Rays\n", + "10 Rays\n", + "11 Royals\n", + "12 Astros\n", + "13 Dodgers\n", + "14 Tigers\n", + "15 Cardinals\n", + "16 Giants\n", + "17 Red Sox\n", + "18 Mets\n", + "19 Athletics\n", + "20 Twins\n", + "21 Twins\n", + "22 White Sox\n", + "23 Diamondbacks\n", + "24 Braves\n", "Name: homeTeamName, dtype: string" ] }, - "execution_count": 20, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -3415,7 +3415,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 28, "id": "2ae4924b-37a9-4327-81d4-4f2afb66cb37", "metadata": {}, "outputs": [ @@ -3423,12 +3423,86 @@ "name": "stdout", "output_type": "stream", "text": [ - "W\n" + "WITH t0 AS (\n", + " SELECT\n", + " t9.`col_13`,\n", + " t9.`col_14`,\n", + " t9.`col_17`,\n", + " t9.`bigframes_ordering_id`\n", + " FROM `swast-scratch`._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240315_0f214503ed3e408abae057064ac2b4c2 AS t9\n", + "), t1 AS (\n", + " SELECT\n", + " t9.`col_13`,\n", + " t9.`col_14`,\n", + " t9.`col_16`,\n", + " t9.`hidden_l_0`,\n", + " t9.`hidden_r_0`\n", + " FROM `swast-scratch`._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240315_22c51834c2b94bbc93da3d0ff27f980f AS t9\n", + "), t2 AS (\n", + " SELECT\n", + " t0.`col_13`,\n", + " t0.`col_14`,\n", + " t0.`col_17`,\n", + " t0.`bigframes_ordering_id`\n", + " FROM t0\n", + "), t3 AS (\n", + " SELECT\n", + " t1.`col_13`,\n", + " t1.`col_14`,\n", + " t1.`col_16`,\n", + " t1.`hidden_l_0`,\n", + " t1.`hidden_r_0`\n", + " FROM t1\n", + "), t4 AS (\n", + " SELECT\n", + " t2.`col_13` AS `col_103`,\n", + " t2.`col_14` AS `col_104`,\n", + " t2.`col_17` AS `col_105`,\n", + " t2.`bigframes_ordering_id` AS `hidden_r_0`\n", + " FROM t2\n", + "), t5 AS (\n", + " SELECT\n", + " t3.`col_13` AS `col_100`,\n", + " t3.`col_14` AS `col_101`,\n", + " t3.`col_16` AS `col_102`,\n", + " t3.`hidden_l_0`,\n", + " t3.`hidden_r_0` AS `hidden_l_1`\n", + " FROM t3\n", + "), t6 AS (\n", + " SELECT\n", + " coalesce(`col_101`, `col_104`) AS `col_106`,\n", + " `col_102`,\n", + " `col_105`,\n", + " (\n", + " row_number() OVER (ORDER BY `hidden_l_0` IS NULL ASC, `hidden_l_0` ASC, `hidden_r_0` IS NULL ASC, `hidden_r_0` ASC) - 1\n", + " ) AS `bigframes_ordering_id`\n", + " FROM t5\n", + " INNER JOIN t4\n", + " ON coalesce(t5.`col_101`, '$NULL_SENTINEL$') = coalesce(t4.`col_104`, '$NULL_SENTINEL$')\n", + "), t7 AS (\n", + " SELECT\n", + " t6.`col_106`,\n", + " t6.`col_102`,\n", + " t6.`col_105`,\n", + " t6.`bigframes_ordering_id`\n", + " FROM t6\n", + ")\n", + "SELECT\n", + " t8.`col_106` AS `gameId`,\n", + " t8.`col_102` AS `homeTeamName`,\n", + " t8.`col_105` AS `awayTeamName`\n", + "FROM (\n", + " SELECT\n", + " t7.`col_106`,\n", + " t7.`col_102`,\n", + " t7.`col_105`\n", + " FROM t7\n", + ") AS t8\n" ] } ], "source": [ - "print(df1.merge(df2, on=\"gameId\", how=\"inner\").sql[0])" + "print(df1.merge(df2, on=\"gameId\", how=\"inner\").sql)" ] }, { @@ -3442,19 +3516,17 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "id": "5adc7bbf-2c58-4924-964c-ed1b18dc9268", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3823a6fb05e84f8986962d044559accb", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 9fcba646-219f-40ee-9792-d74af0ff7e22 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 15cdbf31-e68a-41f0-9c5b-ea4ce49345f0 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -3462,13 +3534,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6e3bbb5866d244cda7418890ca99766a", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 657cdf62-f71e-482c-97f7-b66e4ac20e10 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 1e56a960-3fcd-421a-8500-bc1c099ae7a1 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -3505,203 +3575,203 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", + " Nationals\n", + " Brewers\n", + " 167\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", + " Reds\n", + " Brewers\n", + " 172\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", + " Orioles\n", + " Rays\n", + " 166\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", + " Rockies\n", + " Giants\n", + " 182\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", + " Twins\n", + " Indians\n", + " 204\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", + " Yankees\n", + " Mets\n", + " 182\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", + " Red Sox\n", + " Rays\n", + " 191\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", + " Cardinals\n", + " Pirates\n", + " 201\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", + " Rays\n", + " Twins\n", + " 189\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", + " Rays\n", + " Twins\n", + " 177\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", + " Royals\n", + " Rays\n", + " 183\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", + " Astros\n", + " Rays\n", + " 194\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", + " Dodgers\n", + " Giants\n", + " 178\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", + " Tigers\n", + " White Sox\n", + " 193\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", + " 160\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Giants\n", + " Cardinals\n", + " 169\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Red Sox\n", + " Twins\n", + " 251\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", + " Mets\n", + " Braves\n", + " 151\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", + " Athletics\n", + " Twins\n", + " 153\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", + " Twins\n", + " Marlins\n", + " 185\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", + " Twins\n", + " Yankees\n", + " 180\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", + " White Sox\n", + " Orioles\n", + " 199\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", + " Braves\n", + " Mets\n", + " 201\n", " \n", " \n", "\n", @@ -3710,64 +3780,64 @@ ], "text/plain": [ " gameId year HOME TEAM awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", " duration_minutes \n", - "0 187 \n", - "1 189 \n", - "2 165 \n", - "3 222 \n", - "4 164 \n", - "5 201 \n", - "6 173 \n", - "7 176 \n", - "8 180 \n", - "9 157 \n", - "10 218 \n", - "11 160 \n", - "12 178 \n", - "13 205 \n", - "14 197 \n", - "15 198 \n", - "16 188 \n", - "17 188 \n", - "18 194 \n", - "19 175 \n", - "20 257 \n", - "21 178 \n", - "22 171 \n", - "23 248 \n", - "24 174 \n", + "0 167 \n", + "1 172 \n", + "2 166 \n", + "3 182 \n", + "4 204 \n", + "5 184 \n", + "6 182 \n", + "7 191 \n", + "8 201 \n", + "9 189 \n", + "10 177 \n", + "11 183 \n", + "12 194 \n", + "13 178 \n", + "14 193 \n", + "15 160 \n", + "16 169 \n", + "17 251 \n", + "18 151 \n", + "19 153 \n", + "20 185 \n", + "21 180 \n", + "22 199 \n", + "23 175 \n", + "24 201 \n", "...\n", "\n", "[2431 rows x 5 columns]" ] }, - "execution_count": 22, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -3778,19 +3848,17 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 22, "id": "ac3ceabe-4317-453c-9418-826de5094454", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0c8aa06c869446f09a41c5dff15dc682", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job c90e0cd4-30e5-427c-8f5f-a0a8c778bc62 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job f21ef830-99b1-4fce-ab9a-378e12a04587 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -3798,13 +3866,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2c03c2a860c847b5a27d1c3f2188e323", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 23bfdb6d-9411-484c-9766-93f76dfc1adc is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 88f63cc1-9691-4e6c-8acf-650f31ab8560 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -3841,203 +3907,203 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", + " Nationals\n", + " Brewers\n", + " 167\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", + " Reds\n", + " Brewers\n", + " 172\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", + " Orioles\n", + " Rays\n", + " 166\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", + " Rockies\n", + " Giants\n", + " 182\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", + " Twins\n", + " Indians\n", + " 204\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", + " Yankees\n", + " Mets\n", + " 182\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", + " Red Sox\n", + " Rays\n", + " 191\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", + " Cardinals\n", + " Pirates\n", + " 201\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", + " Rays\n", + " Twins\n", + " 189\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", + " Rays\n", + " Twins\n", + " 177\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", + " Royals\n", + " Rays\n", + " 183\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", + " Astros\n", + " Rays\n", + " 194\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", + " Dodgers\n", + " Giants\n", + " 178\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", + " Tigers\n", + " White Sox\n", + " 193\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", + " 160\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Giants\n", + " Cardinals\n", + " 169\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Red Sox\n", + " Twins\n", + " 251\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", + " Mets\n", + " Braves\n", + " 151\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", + " Athletics\n", + " Twins\n", + " 153\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", + " Twins\n", + " Marlins\n", + " 185\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", + " Twins\n", + " Yankees\n", + " 180\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", + " White Sox\n", + " Orioles\n", + " 199\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", + " Braves\n", + " Mets\n", + " 201\n", " \n", " \n", "\n", @@ -4046,64 +4112,64 @@ ], "text/plain": [ " gameId year homeTeam!@#$%col awayTeamName \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", " duration_minutes \n", - "0 187 \n", - "1 189 \n", - "2 165 \n", - "3 222 \n", - "4 164 \n", - "5 201 \n", - "6 173 \n", - "7 176 \n", - "8 180 \n", - "9 157 \n", - "10 218 \n", - "11 160 \n", - "12 178 \n", - "13 205 \n", - "14 197 \n", - "15 198 \n", - "16 188 \n", - "17 188 \n", - "18 194 \n", - "19 175 \n", - "20 257 \n", - "21 178 \n", - "22 171 \n", - "23 248 \n", - "24 174 \n", + "0 167 \n", + "1 172 \n", + "2 166 \n", + "3 182 \n", + "4 204 \n", + "5 184 \n", + "6 182 \n", + "7 191 \n", + "8 201 \n", + "9 189 \n", + "10 177 \n", + "11 183 \n", + "12 194 \n", + "13 178 \n", + "14 193 \n", + "15 160 \n", + "16 169 \n", + "17 251 \n", + "18 151 \n", + "19 153 \n", + "20 185 \n", + "21 180 \n", + "22 199 \n", + "23 175 \n", + "24 201 \n", "...\n", "\n", "[2431 rows x 5 columns]" ] }, - "execution_count": 23, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -4114,19 +4180,17 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 23, "id": "e73704c9-7aa9-4f10-b414-3417c3ad9eb8", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4eba676d1e7b4ead892828e33baa8534", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 63c2d27f-382c-4a43-8fc1-135d9fd66a54 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 6f6e1d12-4202-434a-908a-3d0b34e70656 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -4134,13 +4198,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6c1c07d67cc74b768664575511eb2a7f", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job cc64185b-98e7-40d8-bf8e-2a3ea355ef67 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 05ec231e-a7f0-4e41-ad60-5d8136a2e148 is DONE. 0 Bytes processed. " ] }, "metadata": {}, @@ -4177,203 +4239,203 @@ " \n", " \n", " 0\n", - " e14b6493-9e7f-404f-840a-8a680cc364bf\n", + " d60c6036-0ce1-4c90-8dd9-de3b403c92a8\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 187\n", + " Nationals\n", + " Brewers\n", + " 167\n", " \n", " \n", " 1\n", - " 1f32b347-cbcb-4c31-a145-0e685306d168\n", + " af72a0b9-65f7-49fb-9b30-d505068bdf6d\n", " 2016\n", - " Marlins\n", - " Cubs\n", - " 189\n", + " Reds\n", + " Brewers\n", + " 172\n", " \n", " \n", " 2\n", - " 0c2292d1-7398-48be-bf8e-b41dad5e1a43\n", + " f57e1271-d217-400a-aea6-2e2d7d6a59a0\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 165\n", + " Orioles\n", + " Rays\n", + " 166\n", " \n", " \n", " 3\n", - " 8fbec734-a15a-42ab-8d51-60790de7750b\n", + " 198f4eed-a29f-41e2-8623-cb261e5ab370\n", " 2016\n", - " Braves\n", - " Cubs\n", - " 222\n", + " Rockies\n", + " Giants\n", + " 182\n", " \n", " \n", " 4\n", - " 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd\n", + " cb3ef033-dd57-41fd-b206-cdd3bc12c74f\n", " 2016\n", - " Phillies\n", - " Cubs\n", - " 164\n", + " Twins\n", + " Indians\n", + " 204\n", " \n", " \n", " 5\n", - " 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52\n", + " 4be9f735-a98e-4689-87ce-852cc3a1e79d\n", " 2016\n", - " Diamondbacks\n", - " Cubs\n", - " 201\n", + " Blue Jays\n", + " Orioles\n", + " 184\n", " \n", " \n", " 6\n", - " 76ea8662-c7e6-4c38-8f2a-efe373e428ce\n", + " 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 173\n", + " Yankees\n", + " Mets\n", + " 182\n", " \n", " \n", " 7\n", - " 66fad23d-6e89-4f99-be29-d49b6e94f95d\n", + " 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2\n", " 2016\n", - " Athletics\n", - " Cubs\n", - " 176\n", + " Red Sox\n", + " Rays\n", + " 191\n", " \n", " \n", " 8\n", - " d977367c-cf0c-4687-95a0-eb4542efcb01\n", + " 7e1c2095-4fea-454c-8773-096ceb6fb05c\n", " 2016\n", - " Rockies\n", - " Cubs\n", - " 180\n", + " Cardinals\n", + " Pirates\n", + " 201\n", " \n", " \n", " 9\n", - " a87070ff-1084-43ca-a7ba-69278f93ecba\n", + " f7f24ce3-7f9d-4e8a-986e-095db847c4c1\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 157\n", + " Rays\n", + " Twins\n", + " 189\n", " \n", " \n", " 10\n", - " ea6b350d-3c1d-4737-878d-4465f66999f6\n", + " 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 218\n", + " Rays\n", + " Twins\n", + " 177\n", " \n", " \n", " 11\n", - " 46463c50-0f5c-4dca-a661-dd194464e791\n", + " 6d2cab13-dd85-477a-8769-669069f85836\n", " 2016\n", - " Cardinals\n", - " Cubs\n", - " 160\n", + " Royals\n", + " Rays\n", + " 183\n", " \n", " \n", " 12\n", - " 59134e6d-9d13-49aa-978e-c3c2300eb90f\n", + " bca90342-7ddc-468e-b189-d43fad7528ec\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 178\n", + " Astros\n", + " Rays\n", + " 194\n", " \n", " \n", " 13\n", - " 387630a3-a894-4327-baa1-b24ec1a654d9\n", + " 630f4f78-03cc-43c1-9e57-ababb9c11418\n", " 2016\n", - " Pirates\n", - " Cubs\n", - " 205\n", + " Dodgers\n", + " Giants\n", + " 178\n", " \n", " \n", " 14\n", - " 5d084e13-94fd-4995-b95a-4801ea3ed556\n", + " c0cf1376-1115-4a2f-b457-3f82bbc41a89\n", " 2016\n", - " Giants\n", - " Cubs\n", - " 197\n", + " Tigers\n", + " White Sox\n", + " 193\n", " \n", " \n", " 15\n", - " 34444c94-03ec-4d12-96af-68b8f399a22f\n", + " 46463c50-0f5c-4dca-a661-dd194464e791\n", " 2016\n", - " Reds\n", + " Cardinals\n", " Cubs\n", - " 198\n", + " 160\n", " \n", " \n", " 16\n", - " 9580bffe-22e1-4975-978b-1b13e7505193\n", + " 392ad56d-972e-4f77-98e2-5f8577931cf8\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Giants\n", + " Cardinals\n", + " 169\n", " \n", " \n", " 17\n", - " 645e6a08-afd6-4677-a5c9-01ef446b0cf3\n", + " 307730fa-bbed-4221-b4e6-a2492f546fd5\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 188\n", + " Red Sox\n", + " Twins\n", + " 251\n", " \n", " \n", " 18\n", - " 08981bd8-d1d7-48e1-8668-9098b8f7fe90\n", + " 1cbc558f-7615-4fa9-bf97-7ccd62040d6f\n", " 2016\n", - " Reds\n", - " Cubs\n", - " 194\n", + " Mets\n", + " Braves\n", + " 151\n", " \n", " \n", " 19\n", - " 303703bb-b55f-476d-8faf-bf582169fb1d\n", + " 723348ba-1645-43fc-9e22-92994f7a63bd\n", " 2016\n", - " Padres\n", - " Cubs\n", - " 175\n", + " Athletics\n", + " Twins\n", + " 153\n", " \n", " \n", " 20\n", - " 71ab82a4-6e07-430a-b695-1af3bc42ea61\n", + " ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992\n", " 2016\n", - " Nationals\n", - " Cubs\n", - " 257\n", + " Twins\n", + " Marlins\n", + " 185\n", " \n", " \n", " 21\n", - " d1a110c2-f6c8-4029-bcd8-2f8a01e1561c\n", + " f2747230-7df5-4535-a475-a1c823d0d654\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 178\n", + " Twins\n", + " Yankees\n", + " 180\n", " \n", " \n", " 22\n", - " 6d111b57-fa0b-4f24-82df-ff33a26f0252\n", + " db3b6f35-a7a4-430a-8703-2b2f25103e17\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 171\n", + " White Sox\n", + " Orioles\n", + " 199\n", " \n", " \n", " 23\n", - " a97e9539-bbbd-4e03-bf15-f25ea2c1d923\n", + " 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636\n", " 2016\n", - " Brewers\n", - " Cubs\n", - " 248\n", + " Diamondbacks\n", + " Giants\n", + " 175\n", " \n", " \n", " 24\n", - " dc0c9218-505c-4725-8c0c-40b72cca0956\n", + " 95d548b6-2da8-4644-812e-b277fec5b91f\n", " 2016\n", - " Astros\n", - " Cubs\n", - " 174\n", + " Braves\n", + " Mets\n", + " 201\n", " \n", " \n", "\n", @@ -4381,65 +4443,65 @@ "[2431 rows x 5 columns in total]" ], "text/plain": [ - " gameId year team team \\\n", - "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", - "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", - "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", - "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", - "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", - "5 6a83e76c-dc0d-4924-9d3d-a2e7e0ab5b52 2016 Diamondbacks Cubs \n", - "6 76ea8662-c7e6-4c38-8f2a-efe373e428ce 2016 Athletics Cubs \n", - "7 66fad23d-6e89-4f99-be29-d49b6e94f95d 2016 Athletics Cubs \n", - "8 d977367c-cf0c-4687-95a0-eb4542efcb01 2016 Rockies Cubs \n", - "9 a87070ff-1084-43ca-a7ba-69278f93ecba 2016 Cardinals Cubs \n", - "10 ea6b350d-3c1d-4737-878d-4465f66999f6 2016 Cardinals Cubs \n", - "11 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", - "12 59134e6d-9d13-49aa-978e-c3c2300eb90f 2016 Pirates Cubs \n", - "13 387630a3-a894-4327-baa1-b24ec1a654d9 2016 Pirates Cubs \n", - "14 5d084e13-94fd-4995-b95a-4801ea3ed556 2016 Giants Cubs \n", - "15 34444c94-03ec-4d12-96af-68b8f399a22f 2016 Reds Cubs \n", - "16 9580bffe-22e1-4975-978b-1b13e7505193 2016 Reds Cubs \n", - "17 645e6a08-afd6-4677-a5c9-01ef446b0cf3 2016 Reds Cubs \n", - "18 08981bd8-d1d7-48e1-8668-9098b8f7fe90 2016 Reds Cubs \n", - "19 303703bb-b55f-476d-8faf-bf582169fb1d 2016 Padres Cubs \n", - "20 71ab82a4-6e07-430a-b695-1af3bc42ea61 2016 Nationals Cubs \n", - "21 d1a110c2-f6c8-4029-bcd8-2f8a01e1561c 2016 Brewers Cubs \n", - "22 6d111b57-fa0b-4f24-82df-ff33a26f0252 2016 Brewers Cubs \n", - "23 a97e9539-bbbd-4e03-bf15-f25ea2c1d923 2016 Brewers Cubs \n", - "24 dc0c9218-505c-4725-8c0c-40b72cca0956 2016 Astros Cubs \n", + " gameId year team team \\\n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 2016 Nationals Brewers \n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d 2016 Reds Brewers \n", + "2 f57e1271-d217-400a-aea6-2e2d7d6a59a0 2016 Orioles Rays \n", + "3 198f4eed-a29f-41e2-8623-cb261e5ab370 2016 Rockies Giants \n", + "4 cb3ef033-dd57-41fd-b206-cdd3bc12c74f 2016 Twins Indians \n", + "5 4be9f735-a98e-4689-87ce-852cc3a1e79d 2016 Blue Jays Orioles \n", + "6 0b2de8c3-11d9-4f0f-a186-25b59f34a5d8 2016 Yankees Mets \n", + "7 60d80663-6ced-44aa-aad9-0f4bf8d3b4d2 2016 Red Sox Rays \n", + "8 7e1c2095-4fea-454c-8773-096ceb6fb05c 2016 Cardinals Pirates \n", + "9 f7f24ce3-7f9d-4e8a-986e-095db847c4c1 2016 Rays Twins \n", + "10 5c26e7fc-c99f-48b4-92c1-4a7208c8cfe9 2016 Rays Twins \n", + "11 6d2cab13-dd85-477a-8769-669069f85836 2016 Royals Rays \n", + "12 bca90342-7ddc-468e-b189-d43fad7528ec 2016 Astros Rays \n", + "13 630f4f78-03cc-43c1-9e57-ababb9c11418 2016 Dodgers Giants \n", + "14 c0cf1376-1115-4a2f-b457-3f82bbc41a89 2016 Tigers White Sox \n", + "15 46463c50-0f5c-4dca-a661-dd194464e791 2016 Cardinals Cubs \n", + "16 392ad56d-972e-4f77-98e2-5f8577931cf8 2016 Giants Cardinals \n", + "17 307730fa-bbed-4221-b4e6-a2492f546fd5 2016 Red Sox Twins \n", + "18 1cbc558f-7615-4fa9-bf97-7ccd62040d6f 2016 Mets Braves \n", + "19 723348ba-1645-43fc-9e22-92994f7a63bd 2016 Athletics Twins \n", + "20 ffbd6ecc-82e1-4e5d-9bd1-4ea210be5992 2016 Twins Marlins \n", + "21 f2747230-7df5-4535-a475-a1c823d0d654 2016 Twins Yankees \n", + "22 db3b6f35-a7a4-430a-8703-2b2f25103e17 2016 White Sox Orioles \n", + "23 5fc8c6f0-a70e-4d1b-877f-eb1ec8e6f636 2016 Diamondbacks Giants \n", + "24 95d548b6-2da8-4644-812e-b277fec5b91f 2016 Braves Mets \n", "\n", " duration_minutes \n", - "0 187 \n", - "1 189 \n", - "2 165 \n", - "3 222 \n", - "4 164 \n", - "5 201 \n", - "6 173 \n", - "7 176 \n", - "8 180 \n", - "9 157 \n", - "10 218 \n", - "11 160 \n", - "12 178 \n", - "13 205 \n", - "14 197 \n", - "15 198 \n", - "16 188 \n", - "17 188 \n", - "18 194 \n", - "19 175 \n", - "20 257 \n", - "21 178 \n", - "22 171 \n", - "23 248 \n", - "24 174 \n", + "0 167 \n", + "1 172 \n", + "2 166 \n", + "3 182 \n", + "4 204 \n", + "5 184 \n", + "6 182 \n", + "7 191 \n", + "8 201 \n", + "9 189 \n", + "10 177 \n", + "11 183 \n", + "12 194 \n", + "13 178 \n", + "14 193 \n", + "15 160 \n", + "16 169 \n", + "17 251 \n", + "18 151 \n", + "19 153 \n", + "20 185 \n", + "21 180 \n", + "22 199 \n", + "23 175 \n", + "24 201 \n", "...\n", "\n", "[2431 rows x 5 columns]" ] }, - "execution_count": 24, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -4451,19 +4513,17 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 24, "id": "1a80f6f8-a172-4d7d-a2f5-e10871da7224", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "62abc80873ca4f96843de16b70ff0724", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 089a657b-e651-4b17-a4ce-4d7be682a49c is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job f4b90f3c-381e-470d-8416-54f31f1fbb3a is DONE. 174.4 kB processed. " ] }, "metadata": {}, @@ -4471,13 +4531,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0a92c634fa774082a476c56ac0097adc", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job 740ad61e-cf14-41f1-ae4e-23d1fb8ed155 is DONE. 82.0 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 5cd5e48d-8b02-4500-94cd-cbd2ee91957e is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -4511,128 +4569,128 @@ " \n", " \n", " 0\n", - " Marlins\n", - " Cubs\n", + " Nationals\n", + " Brewers\n", " \n", " \n", " 1\n", - " Marlins\n", - " Cubs\n", + " Reds\n", + " Brewers\n", " \n", " \n", " 2\n", - " Braves\n", - " Cubs\n", + " Orioles\n", + " Rays\n", " \n", " \n", " 3\n", - " Braves\n", - " Cubs\n", + " Rockies\n", + " Giants\n", " \n", " \n", " 4\n", - " Phillies\n", - " Cubs\n", + " Twins\n", + " Indians\n", " \n", " \n", " 5\n", - " Diamondbacks\n", - " Cubs\n", + " Blue Jays\n", + " Orioles\n", " \n", " \n", " 6\n", - " Athletics\n", - " Cubs\n", + " Yankees\n", + " Mets\n", " \n", " \n", " 7\n", - " Athletics\n", - " Cubs\n", + " Red Sox\n", + " Rays\n", " \n", " \n", " 8\n", - " Rockies\n", - " Cubs\n", + " Cardinals\n", + " Pirates\n", " \n", " \n", " 9\n", - " Cardinals\n", - " Cubs\n", + " Rays\n", + " Twins\n", " \n", " \n", " 10\n", - " Cardinals\n", - " Cubs\n", + " Rays\n", + " Twins\n", " \n", " \n", " 11\n", - " Cardinals\n", - " Cubs\n", + " Royals\n", + " Rays\n", " \n", " \n", " 12\n", - " Pirates\n", - " Cubs\n", + " Astros\n", + " Rays\n", " \n", " \n", " 13\n", - " Pirates\n", - " Cubs\n", + " Dodgers\n", + " Giants\n", " \n", " \n", " 14\n", - " Giants\n", - " Cubs\n", + " Tigers\n", + " White Sox\n", " \n", " \n", " 15\n", - " Reds\n", + " Cardinals\n", " Cubs\n", " \n", " \n", " 16\n", - " Reds\n", - " Cubs\n", + " Giants\n", + " Cardinals\n", " \n", " \n", " 17\n", - " Reds\n", - " Cubs\n", + " Red Sox\n", + " Twins\n", " \n", " \n", " 18\n", - " Reds\n", - " Cubs\n", + " Mets\n", + " Braves\n", " \n", " \n", " 19\n", - " Padres\n", - " Cubs\n", + " Athletics\n", + " Twins\n", " \n", " \n", " 20\n", - " Nationals\n", - " Cubs\n", + " Twins\n", + " Marlins\n", " \n", " \n", " 21\n", - " Brewers\n", - " Cubs\n", + " Twins\n", + " Yankees\n", " \n", " \n", " 22\n", - " Brewers\n", - " Cubs\n", + " White Sox\n", + " Orioles\n", " \n", " \n", " 23\n", - " Brewers\n", - " Cubs\n", + " Diamondbacks\n", + " Giants\n", " \n", " \n", " 24\n", - " Astros\n", - " Cubs\n", + " Braves\n", + " Mets\n", " \n", " \n", "\n", @@ -4640,38 +4698,38 @@ "[2431 rows x 2 columns in total]" ], "text/plain": [ - " team team\n", - "0 Marlins Cubs\n", - "1 Marlins Cubs\n", - "2 Braves Cubs\n", - "3 Braves Cubs\n", - "4 Phillies Cubs\n", - "5 Diamondbacks Cubs\n", - "6 Athletics Cubs\n", - "7 Athletics Cubs\n", - "8 Rockies Cubs\n", - "9 Cardinals Cubs\n", - "10 Cardinals Cubs\n", - "11 Cardinals Cubs\n", - "12 Pirates Cubs\n", - "13 Pirates Cubs\n", - "14 Giants Cubs\n", - "15 Reds Cubs\n", - "16 Reds Cubs\n", - "17 Reds Cubs\n", - "18 Reds Cubs\n", - "19 Padres Cubs\n", - "20 Nationals Cubs\n", - "21 Brewers Cubs\n", - "22 Brewers Cubs\n", - "23 Brewers Cubs\n", - "24 Astros Cubs\n", + " team team\n", + "0 Nationals Brewers\n", + "1 Reds Brewers\n", + "2 Orioles Rays\n", + "3 Rockies Giants\n", + "4 Twins Indians\n", + "5 Blue Jays Orioles\n", + "6 Yankees Mets\n", + "7 Red Sox Rays\n", + "8 Cardinals Pirates\n", + "9 Rays Twins\n", + "10 Rays Twins\n", + "11 Royals Rays\n", + "12 Astros Rays\n", + "13 Dodgers Giants\n", + "14 Tigers White Sox\n", + "15 Cardinals Cubs\n", + "16 Giants Cardinals\n", + "17 Red Sox Twins\n", + "18 Mets Braves\n", + "19 Athletics Twins\n", + "20 Twins Marlins\n", + "21 Twins Yankees\n", + "22 White Sox Orioles\n", + "23 Diamondbacks Giants\n", + "24 Braves Mets\n", "...\n", "\n", "[2431 rows x 2 columns]" ] }, - "execution_count": 25, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -4691,19 +4749,17 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 25, "id": "2414a095-37df-4755-b86c-2031a6cb9d4a", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "27bab406ba024805b35effa7e01def3d", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job d3b3cd83-d9cf-4c5b-9015-e3979e0857f3 is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 6b1cf632-7764-49ba-bd5d-cdf31c47e430 is DONE. 174.4 kB processed. " ] }, "metadata": {}, @@ -4711,13 +4767,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fb3f85b83da34242963b478328db9662", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e4d99a58-738d-42af-863e-d1262a32c93c is DONE. 77.8 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job baa31010-2426-4ea5-9527-8f55473a3f41 is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -4752,127 +4806,127 @@ " \n", " 0\n", " 2016\n", - " 187\n", + " 167\n", " \n", " \n", " 1\n", " 2016\n", - " 189\n", + " 172\n", " \n", " \n", " 2\n", " 2016\n", - " 165\n", + " 166\n", " \n", " \n", " 3\n", " 2016\n", - " 222\n", + " 182\n", " \n", " \n", " 4\n", " 2016\n", - " 164\n", + " 204\n", " \n", " \n", " 5\n", " 2016\n", - " 201\n", + " 184\n", " \n", " \n", " 6\n", " 2016\n", - " 173\n", + " 182\n", " \n", " \n", " 7\n", " 2016\n", - " 176\n", + " 191\n", " \n", " \n", " 8\n", " 2016\n", - " 180\n", + " 201\n", " \n", " \n", " 9\n", " 2016\n", - " 157\n", + " 189\n", " \n", " \n", " 10\n", " 2016\n", - " 218\n", + " 177\n", " \n", " \n", " 11\n", " 2016\n", - " 160\n", + " 183\n", " \n", " \n", " 12\n", " 2016\n", - " 178\n", + " 194\n", " \n", " \n", " 13\n", " 2016\n", - " 205\n", + " 178\n", " \n", " \n", " 14\n", " 2016\n", - " 197\n", + " 193\n", " \n", " \n", " 15\n", " 2016\n", - " 198\n", + " 160\n", " \n", " \n", " 16\n", " 2016\n", - " 188\n", + " 169\n", " \n", " \n", " 17\n", " 2016\n", - " 188\n", + " 251\n", " \n", " \n", " 18\n", " 2016\n", - " 194\n", + " 151\n", " \n", " \n", " 19\n", " 2016\n", - " 175\n", + " 153\n", " \n", " \n", " 20\n", " 2016\n", - " 257\n", + " 185\n", " \n", " \n", " 21\n", " 2016\n", - " 178\n", + " 180\n", " \n", " \n", " 22\n", " 2016\n", - " 171\n", + " 199\n", " \n", " \n", " 23\n", " 2016\n", - " 248\n", + " 175\n", " \n", " \n", " 24\n", " 2016\n", - " 174\n", + " 201\n", " \n", " \n", "\n", @@ -4881,37 +4935,37 @@ ], "text/plain": [ " year duration_minutes\n", - "0 2016 187\n", - "1 2016 189\n", - "2 2016 165\n", - "3 2016 222\n", - "4 2016 164\n", - "5 2016 201\n", - "6 2016 173\n", - "7 2016 176\n", - "8 2016 180\n", - "9 2016 157\n", - "10 2016 218\n", - "11 2016 160\n", - "12 2016 178\n", - "13 2016 205\n", - "14 2016 197\n", - "15 2016 198\n", - "16 2016 188\n", - "17 2016 188\n", - "18 2016 194\n", - "19 2016 175\n", - "20 2016 257\n", - "21 2016 178\n", - "22 2016 171\n", - "23 2016 248\n", - "24 2016 174\n", + "0 2016 167\n", + "1 2016 172\n", + "2 2016 166\n", + "3 2016 182\n", + "4 2016 204\n", + "5 2016 184\n", + "6 2016 182\n", + "7 2016 191\n", + "8 2016 201\n", + "9 2016 189\n", + "10 2016 177\n", + "11 2016 183\n", + "12 2016 194\n", + "13 2016 178\n", + "14 2016 193\n", + "15 2016 160\n", + "16 2016 169\n", + "17 2016 251\n", + "18 2016 151\n", + "19 2016 153\n", + "20 2016 185\n", + "21 2016 180\n", + "22 2016 199\n", + "23 2016 175\n", + "24 2016 201\n", "...\n", "\n", "[2431 rows x 2 columns]" ] }, - "execution_count": 26, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -4923,19 +4977,17 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 26, "id": "7d437c7c-ae74-4f0d-a4f8-10a133f4b61e", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0836e9729f75465e8eb1e73c1071a22d", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job e5c2b908-c539-4349-8368-50e61d8e19cd is DONE. 0 Bytes processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 62ffb914-e1fb-4b12-adb5-09b431e06acf is DONE. 174.4 kB processed. " ] }, "metadata": {}, @@ -4943,13 +4995,11 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "69e853a1f0e44d39adacfefbfee86156", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job c5a563d2-9780-4c51-996b-bf0242b96e39 is DONE. 77.8 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job 2eaceff3-b00c-42d0-883f-fbe85a70f49b is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -4984,127 +5034,127 @@ " \n", " 0\n", " 2017\n", - " 188\n", + " 168\n", " \n", " \n", " 1\n", " 2017\n", - " 190\n", + " 173\n", " \n", " \n", " 2\n", " 2017\n", - " 166\n", + " 167\n", " \n", " \n", " 3\n", " 2017\n", - " 223\n", + " 183\n", " \n", " \n", " 4\n", " 2017\n", - " 165\n", + " 205\n", " \n", " \n", " 5\n", " 2017\n", - " 202\n", + " 185\n", " \n", " \n", " 6\n", " 2017\n", - " 174\n", + " 183\n", " \n", " \n", " 7\n", " 2017\n", - " 177\n", + " 192\n", " \n", " \n", " 8\n", " 2017\n", - " 181\n", + " 202\n", " \n", " \n", " 9\n", " 2017\n", - " 158\n", + " 190\n", " \n", " \n", " 10\n", " 2017\n", - " 219\n", + " 178\n", " \n", " \n", " 11\n", " 2017\n", - " 161\n", + " 184\n", " \n", " \n", " 12\n", " 2017\n", - " 179\n", + " 195\n", " \n", " \n", " 13\n", " 2017\n", - " 206\n", + " 179\n", " \n", " \n", " 14\n", " 2017\n", - " 198\n", + " 194\n", " \n", " \n", " 15\n", " 2017\n", - " 199\n", + " 161\n", " \n", " \n", " 16\n", " 2017\n", - " 189\n", + " 170\n", " \n", " \n", " 17\n", " 2017\n", - " 189\n", + " 252\n", " \n", " \n", " 18\n", " 2017\n", - " 195\n", + " 152\n", " \n", " \n", " 19\n", " 2017\n", - " 176\n", + " 154\n", " \n", " \n", " 20\n", " 2017\n", - " 258\n", + " 186\n", " \n", " \n", " 21\n", " 2017\n", - " 179\n", + " 181\n", " \n", " \n", " 22\n", " 2017\n", - " 172\n", + " 200\n", " \n", " \n", " 23\n", " 2017\n", - " 249\n", + " 176\n", " \n", " \n", " 24\n", " 2017\n", - " 175\n", + " 202\n", " \n", " \n", "\n", @@ -5113,37 +5163,37 @@ ], "text/plain": [ " year duration_minutes\n", - "0 2017 188\n", - "1 2017 190\n", - "2 2017 166\n", - "3 2017 223\n", - "4 2017 165\n", - "5 2017 202\n", - "6 2017 174\n", - "7 2017 177\n", - "8 2017 181\n", - "9 2017 158\n", - "10 2017 219\n", - "11 2017 161\n", - "12 2017 179\n", - "13 2017 206\n", - "14 2017 198\n", - "15 2017 199\n", - "16 2017 189\n", - "17 2017 189\n", - "18 2017 195\n", - "19 2017 176\n", - "20 2017 258\n", - "21 2017 179\n", - "22 2017 172\n", - "23 2017 249\n", - "24 2017 175\n", + "0 2017 168\n", + "1 2017 173\n", + "2 2017 167\n", + "3 2017 183\n", + "4 2017 205\n", + "5 2017 185\n", + "6 2017 183\n", + "7 2017 192\n", + "8 2017 202\n", + "9 2017 190\n", + "10 2017 178\n", + "11 2017 184\n", + "12 2017 195\n", + "13 2017 179\n", + "14 2017 194\n", + "15 2017 161\n", + "16 2017 170\n", + "17 2017 252\n", + "18 2017 152\n", + "19 2017 154\n", + "20 2017 186\n", + "21 2017 181\n", + "22 2017 200\n", + "23 2017 176\n", + "24 2017 202\n", "...\n", "\n", "[2431 rows x 2 columns]" ] }, - "execution_count": 27, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -5162,19 +5212,17 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 27, "id": "ab429fa5", "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "39eda5e2b7984f8f90a441c31b62a675", - "version_major": 2, - "version_minor": 0 - }, + "text/html": [ + "Query job b6495f3d-619c-429e-8904-5cdc4957d09f is DONE. 77.8 kB processed. Open Job" + ], "text/plain": [ - "HTML(value='Query job f3b7ff8a-ffdc-4f4c-91d3-6c2c702c373d is DONE. 193.8 kB processed. " ] }, "metadata": {}, @@ -5209,27 +5257,27 @@ " \n", " 0\n", " 2016\n", - " 187\n", + " 167\n", " \n", " \n", " 1\n", " 2016\n", - " 189\n", + " 172\n", " \n", " \n", " 2\n", " 2016\n", - " 165\n", + " 166\n", " \n", " \n", " 3\n", " 2016\n", - " 222\n", + " 182\n", " \n", " \n", " 4\n", " 2016\n", - " 164\n", + " 204\n", " \n", " \n", " ...\n", @@ -5239,27 +5287,27 @@ " \n", " 2426\n", " 2016\n", - " 156\n", + " 199\n", " \n", " \n", " 2427\n", " 2016\n", - " 185\n", + " 181\n", " \n", " \n", " 2428\n", " 2016\n", - " 243\n", + " 205\n", " \n", " \n", " 2429\n", " 2016\n", - " 184\n", + " 203\n", " \n", " \n", " 2430\n", " 2016\n", - " 185\n", + " 182\n", " \n", " \n", "\n", @@ -5268,22 +5316,22 @@ ], "text/plain": [ " year duration_minutes\n", - "0 2016 187\n", - "1 2016 189\n", - "2 2016 165\n", - "3 2016 222\n", - "4 2016 164\n", + "0 2016 167\n", + "1 2016 172\n", + "2 2016 166\n", + "3 2016 182\n", + "4 2016 204\n", "... ... ...\n", - "2426 2016 156\n", - "2427 2016 185\n", - "2428 2016 243\n", - "2429 2016 184\n", - "2430 2016 185\n", + "2426 2016 199\n", + "2427 2016 181\n", + "2428 2016 205\n", + "2429 2016 203\n", + "2430 2016 182\n", "\n", "[2431 rows x 2 columns]" ] }, - "execution_count": 28, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -5310,7 +5358,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.1" } }, "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 74a0d7b2068..4ea766604de 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": { "id": "ur8xi4C7S06n" }, @@ -140,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": { "id": "2b4ef9b72d43" }, @@ -204,7 +204,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 27, "metadata": { "id": "oM1iC_MfAts1" }, @@ -242,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": { "id": "eF-Twtc4XGem" }, @@ -286,7 +286,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": { "id": "254614fa0c46" }, @@ -308,7 +308,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": { "id": "603adbbf0532" }, @@ -329,7 +329,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": { "id": "PyQmSRbKA8r-" }, @@ -351,13 +351,20 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": { "id": "NPPMuw2PXGeo" }, "outputs": [], "source": [ + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", "bf.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", "bf.options.bigquery.location = REGION" ] }, @@ -396,7 +403,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": { "id": "sdjeXFwcHfl7" }, @@ -404,7 +411,7 @@ { "data": { "text/html": [ - "Query job a3897125-4272-4817-a0e6-8e1a9e022b93 is DONE. 0 Bytes processed. Open Job" + "Query job 0ee1a08e-788e-4fc7-b061-52c23ab25d5a is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -440,7 +447,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": { "id": "SchiTkQGIJog" }, @@ -461,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": { "id": "QCqgVCIsGGuv" }, @@ -469,7 +476,7 @@ { "data": { "text/html": [ - "Query job e186a7bf-813c-4c46-80c8-ae079c829841 is DONE. 0 Bytes processed. Open Job" + "Query job 48be241c-ee93-4dfa-a9e3-66b64c4b5150 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -481,7 +488,7 @@ { "data": { "text/html": [ - "Query job 868ef0e0-ef33-4f0c-8b47-401a82bfc288 is DONE. 0 Bytes processed. Open Job" + "Query job 6af9caa5-4f7a-48f0-a7df-d692ee063b7e is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -536,7 +543,7 @@ "[2 rows x 1 columns]" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -547,7 +554,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": { "id": "BGJnZbgEGS5-" }, @@ -555,7 +562,7 @@ { "data": { "text/html": [ - "Query job fcf9e1e9-cd3f-4a34-ba42-450c818bd6c7 is DONE. 0 Bytes processed. Open Job" + "Query job 41e4f2e7-689a-45d9-bf92-4416f5560b81 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -567,7 +574,7 @@ { "data": { "text/html": [ - "Query job c7b5f6a2-a5ca-4a4a-bcf3-9ddaa0a3777c is DONE. 0 Bytes processed. Open Job" + "Query job aae0b164-f786-4734-8c79-2af9805af0cf is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -622,7 +629,7 @@ "[2 rows x 1 columns]" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -657,7 +664,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": { "id": "EDAaIwHpQCDZ" }, @@ -665,7 +672,7 @@ { "data": { "text/html": [ - "Query job 67d4d2d9-dd57-4886-8bcb-68e9eb6e11e2 is DONE. 0 Bytes processed. Open Job" + "Query job 17f50c10-aa81-4023-b206-4ba59ddf2269 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -677,7 +684,7 @@ { "data": { "text/html": [ - "Query job 26124cec-8753-4b48-b467-5e17c2c3591e is DONE. 0 Bytes processed. Open Job" + "Query job d6d217aa-a623-4ea4-83fb-8f1b8bfb8e68 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -689,7 +696,7 @@ { "data": { "text/html": [ - "Query job 9bdb0d90-60ec-4eec-96f4-990c3e1adef5 is DONE. 132 Bytes processed. Open Job" + "Query job a275a107-752e-46f8-be9f-9cb35eb6b0b9 is DONE. 132 Bytes processed. Open Job" ], "text/plain": [ "" @@ -706,7 +713,7 @@ "Name: API, dtype: string" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -736,7 +743,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": { "id": "6i6HkFJZa8na" }, @@ -744,7 +751,7 @@ { "data": { "text/html": [ - "Query job f0199b1e-5524-48ba-81ec-89d70c28b5d0 is DONE. 0 Bytes processed. Open Job" + "Query job 01f95d2d-901d-4edf-bd3a-245d17c31ef6 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -756,7 +763,7 @@ { "data": { "text/html": [ - "Query job f1816195-25fa-4180-96ce-7917e9729428 is DONE. 584 Bytes processed. Open Job" + "Query job 55927a6f-b023-479a-b9bf-826abde77111 is DONE. 584 Bytes processed. Open Job" ], "text/plain": [ "" @@ -768,7 +775,7 @@ { "data": { "text/html": [ - "Query job ce1ad8d4-3fcd-4ca9-9f9b-4be0cfdabde5 is DONE. 146 Bytes processed. Open Job" + "Query job 445eb0af-f643-40c5-9c1e-25aa3db8374a is DONE. 146 Bytes processed. Open Job" ], "text/plain": [ "" @@ -780,7 +787,7 @@ { "data": { "text/html": [ - "Query job 3b245a41-a86e-4773-aa14-8edaa821c6b7 is DONE. 0 Bytes processed. Open Job" + "Query job ddee268c-773a-4dcc-b14c-ebdd90c2c347 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -792,7 +799,7 @@ { "data": { "text/html": [ - "Query job d3ceafe9-888d-4f5e-b7f3-c2218dae0736 is DONE. 904 Bytes processed. Open Job" + "Query job d7f1eb26-28b2-44ba-8858-5cd4df8621bd is DONE. 904 Bytes processed. Open Job" ], "text/plain": [ "" @@ -804,7 +811,7 @@ { "data": { "text/html": [ - "Query job 930c2334-60ac-4ec1-8a06-2a4cf2d9dc1e is DONE. 226 Bytes processed. Open Job" + "Query job f24d27a5-0e36-4fb5-953b-d09298f83af6 is DONE. 226 Bytes processed. Open Job" ], "text/plain": [ "" @@ -830,7 +837,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": { "id": "9A2gw6hP_2nX" }, @@ -838,7 +845,7 @@ { "data": { "text/html": [ - "Query job 10a766a8-7368-4a82-b239-764e1c13ed64 is DONE. 21.0 kB processed. Open Job" + "Query job 65599c98-72ad-4088-8b09-f29bf05c164b is DONE. 21.8 kB processed. Open Job" ], "text/plain": [ "" @@ -855,11 +862,7 @@ "import pandas as pd\n", "\n", "# Create a DataFrame\n", - "df = pd.DataFrame({\n", - " \"Name\": [\"John\", \"Mary\", \"Peter\"],\n", - " \"Age\": [20, 25, 30],\n", - " \"City\": [\"New York\", \"London\", \"Paris\"]\n", - "})\n", + "df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n", "\n", "# Get the values as a NumPy array\n", "values = df.values\n", @@ -899,7 +902,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": { "id": "GskyyUQPowBT" }, @@ -928,7 +931,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "metadata": { "id": "PBlp-C-DOHRO" }, @@ -937,8 +940,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Cloud Function Name projects/bigframes-dev/locations/us-central1/functions/bigframes-3a8781216c4ccdded9eecfdbd72c63f2\n", - "Remote Function Name bigframes-dev._76f0f906c2e04e83c3496619541347a5922c80ee.bigframes_3a8781216c4ccdded9eecfdbd72c63f2\n" + "Cloud Function Name projects/swast-scratch/locations/us-central1/functions/bigframes-6e7606963c3f06b8181b3cb9449a4363\n", + "Remote Function Name swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bigframes_6e7606963c3f06b8181b3cb9449a4363\n" ] } ], @@ -960,11 +963,36 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "metadata": { "id": "bsQ9cmoWo0Ps" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 047903f8-ea67-430a-8281-8fb5a119b779 is DONE. 21.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 793df956-0b1a-46ba-bb5e-e428171f3bd0 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "df_code = df_pred.assign(code=df_pred['ml_generate_text_llm_result'].apply(extract_code))\n", "series_code = series_pred.assign(code=series_pred['ml_generate_text_llm_result'].apply(extract_code))" @@ -981,7 +1009,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "metadata": { "id": "7yWzjhGy_zcy" }, @@ -989,7 +1017,7 @@ { "data": { "text/html": [ - "Query job 96bea1ea-9c98-42e9-8f6d-a2b6cdeaf17a is DONE. 21.0 kB processed. Open Job" + "Query job 6974c2b7-2ed9-4564-a80b-57aef6959e19 is DONE. 22.8 kB processed. Open Job" ], "text/plain": [ "" @@ -1005,11 +1033,7 @@ "import bigframes.pandas as bf\n", "\n", "# Create a DataFrame\n", - "df = pd.DataFrame({\n", - " \"Name\": [\"John\", \"Mary\", \"Peter\"],\n", - " \"Age\": [20, 25, 30],\n", - " \"City\": [\"New York\", \"London\", \"Paris\"]\n", - "})\n", + "df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n", "\n", "# Get the values as a NumPy array\n", "values = df.values\n", @@ -1046,7 +1070,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "metadata": { "id": "-J5LHgS6LLZ0" }, @@ -1055,7 +1079,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Creating gs://code-samples-d1d466b7-dfe6-11ee-b86e-4201c0a82d52/...\n" + "Creating gs://code-samples-773ee0f2-e302-11ee-8298-4201c0a8181f/...\n" ] } ], @@ -1077,7 +1101,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "metadata": { "id": "Zs_b5L-4IvER" }, @@ -1085,7 +1109,7 @@ { "data": { "text/html": [ - "Query job 72fe0ca6-2f37-457f-9705-ce89b2a4c324 is DONE. 21.0 kB processed. Open Job" + "Query job 81277037-032f-4557-a46e-1d39702f33d5 is DONE. 22.8 kB processed. Open Job" ], "text/plain": [ "" @@ -1097,7 +1121,7 @@ { "data": { "text/html": [ - "Query job 1b21be65-8761-4694-932e-8fa634569e56 is DONE. 0 Bytes processed. Open Job" + "Query job 8dc5a38c-ac16-44e7-83dd-4187380f780f is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1109,7 +1133,7 @@ { "data": { "text/html": [ - "Query job 510566f8-05f2-4455-8daa-f24feea0344e is DONE. 27.0 kB processed. Open Job" + "Query job 9087a758-b1f9-4be7-889b-7761ef0ad966 is DONE. 27.7 kB processed. Open Job" ], "text/plain": [ "" @@ -1121,7 +1145,7 @@ { "data": { "text/html": [ - "Query job 82c0e7f6-ce99-462c-a7d3-e760391f6677 is DONE. 0 Bytes processed. Open Job" + "Query job 6126ea72-c6f7-43f0-8888-e1c2a464a8a4 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1149,7 +1173,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "metadata": { "id": "PspCXu-qu_ND" }, @@ -1158,7 +1182,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "https://console.developers.google.com/storage/browser/code-samples-d1d466b7-dfe6-11ee-b86e-4201c0a82d52/\n" + "https://console.developers.google.com/storage/browser/code-samples-773ee0f2-e302-11ee-8298-4201c0a8181f/\n" ] } ], @@ -1195,7 +1219,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "metadata": { "id": "yw7A461XLjvW" }, @@ -1211,7 +1235,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "metadata": { "id": "sx_vKniMq9ZX" }, @@ -1226,7 +1250,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "metadata": { "id": "iQFo6OUBLmi3" }, @@ -1257,7 +1281,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.1" } }, "nbformat": 4, diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 221933c2f81..d6d819f9e36 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -139,7 +139,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -147,26 +147,7 @@ "PROJECT_ID = \"\" # @param {type:\"string\"}\n", "\n", "# Set the project id in gcloud\n", - "! gcloud config set project {PROJECT_ID}" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Set the region\n", - "\n", - "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "REGION = \"US\" # @param {type: \"string\"}" + "#! gcloud config set project {PROJECT_ID}" ] }, { @@ -201,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -220,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -256,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": { "id": "R7STCS8xB5d2" }, @@ -264,8 +245,9 @@ "source": [ "import bigframes.pandas as bf\n", "\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "bf.options.bigquery.location = REGION" + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bf.options.bigquery.project = PROJECT_ID" ] }, { @@ -288,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": { "id": "zDSwoBo1CU3G" }, @@ -299,7 +281,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": { "id": "tYDoaKgJChiq" }, @@ -307,7 +289,19 @@ { "data": { "text/html": [ - "Query job 4423e883-bd7e-41c9-86b3-52d296a36b00 is DONE. 2.3 GB processed. Open Job" + "Query job 313ed696-37fc-46b3-806e-6041403080d3 is DONE. 2.3 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f149ff34-4807-4cba-841f-fb7bf51bbbd6 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -319,7 +313,7 @@ { "data": { "text/html": [ - "Query job 4ddbf855-a48e-4d98-80d3-290b06d55e98 is DONE. 2.3 GB processed. Open Job" + "Query job 8ed05179-52b2-437d-a709-f651a80de307 is DONE. 4.6 kB processed. Open Job" ], "text/plain": [ "" @@ -389,7 +383,7 @@ "[5 rows x 1 columns]" ] }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -409,7 +403,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": { "id": "OltYSUEcsSOW" }, @@ -431,7 +425,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": { "id": "li38q8FzDDMu" }, @@ -439,7 +433,7 @@ { "data": { "text/html": [ - "Query job 1f75a254-c8c5-468a-a8c0-76f342a2822a is DONE. 0 Bytes processed. Open Job" + "Query job bd6b88fc-6e05-4d71-acb1-d5befaced079 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -457,7 +451,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": { "id": "cOuSOQ5FDewD" }, @@ -465,7 +459,19 @@ { "data": { "text/html": [ - "Query job 15508c20-760e-4137-b312-f010913d13f5 is DONE. 2.3 GB processed. Open Job" + "Query job a4d2983a-7967-4ffb-b2b7-2a387f58776b is DONE. 2.3 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 619b0cc2-4162-44ab-a085-e7cc5c48a02b is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -477,7 +483,7 @@ { "data": { "text/html": [ - "Query job 1e23baf5-8523-4fc5-9a73-d059c0b51c33 is DONE. 80.0 kB processed. Open Job" + "Query job 6dc5b3cf-efa6-4350-907e-ab40e3de80aa is DONE. 20.0 kB processed. Open Job" ], "text/plain": [ "" @@ -489,7 +495,7 @@ { "data": { "text/html": [ - "Query job 12bf5383-fa0d-4806-a4a9-d7ee65a0dd01 is DONE. 20.0 kB processed. Open Job" + "Query job 70f25b9e-2d26-4dc2-9d1f-c24d36e58856 is DONE. 72.0 MB processed. Open Job" ], "text/plain": [ "" @@ -501,7 +507,7 @@ { "data": { "text/html": [ - "Query job cafcd732-727b-4100-ac47-e6d98024fd16 is DONE. 80.0 kB processed. Open Job" + "Query job 2b2f366b-b398-4817-a610-cf71c64a8349 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -513,7 +519,7 @@ { "data": { "text/html": [ - "Query job 216b3d31-4159-4faf-93e6-820f82ca8f31 is DONE. 72.0 MB processed. Open Job" + "Query job b0e71a63-1365-4fc5-a764-b25b62387fd1 is DONE. 36.1 kB processed. Open Job" ], "text/plain": [ "" @@ -551,39 +557,39 @@ " \n", " \n", " \n", - " 3\n", - " [0.02506784163415432, -0.03947214409708977, -0...\n", - " {\"token_count\":273,\"truncated\":false}\n", + " 251\n", + " [ 2.20562406e-02 -3.51827666e-02 7.63384486e-...\n", + " {\"token_count\":145,\"truncated\":false}\n", " \n", - " Despite multiple written requests, the unverif...\n", + " A purse was purchased from XXXX XXXX on XX/XX/...\n", " \n", " \n", - " 102\n", - " [0.029197776690125465, -0.028653817251324654, ...\n", - " {\"token_count\":859,\"truncated\":false}\n", + " 300\n", + " [ 0.01977486 -0.04289974 -0.05289588 -0.027267...\n", + " {\"token_count\":498,\"truncated\":false}\n", " \n", - " To Whom It May Concern, Please be advised that...\n", + " XXXX XXXX XXXXXXXX has reported on my credit r...\n", " \n", " \n", - " 660\n", - " [0.034257132560014725, -0.05359702184796333, -...\n", - " {\"token_count\":282,\"truncated\":false}\n", + " 414\n", + " [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-...\n", + " {\"token_count\":263,\"truncated\":false}\n", " \n", - " Transunion has failed to remove a XXXX XXXX b...\n", + " I have tried to dispute US BKPT CT TX XXXXXXXX...\n", " \n", " \n", - " 1500\n", - " [-0.008016454987227917, -0.05421802029013634, ...\n", - " {\"token_count\":39,\"truncated\":false}\n", + " 493\n", + " [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-...\n", + " {\"token_count\":395,\"truncated\":false}\n", " \n", - " FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR...\n", + " Discover Student Loan has been holding onto {$...\n", " \n", " \n", - " 1601\n", - " [-0.005403461866080761, -0.024799197912216187,...\n", - " {\"token_count\":94,\"truncated\":false}\n", + " 545\n", + " [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...\n", + " {\"token_count\":178,\"truncated\":false}\n", " \n", - " I am a victim of identity theft/fraud. I have ...\n", + " My payments have been approximately {$89.00} w...\n", " \n", " \n", "\n", @@ -591,31 +597,31 @@ "[5 rows x 4 columns in total]" ], "text/plain": [ - " text_embedding \\\n", - "3 [0.02506784163415432, -0.03947214409708977, -0... \n", - "102 [0.029197776690125465, -0.028653817251324654, ... \n", - "660 [0.034257132560014725, -0.05359702184796333, -... \n", - "1500 [-0.008016454987227917, -0.05421802029013634, ... \n", - "1601 [-0.005403461866080761, -0.024799197912216187,... \n", + " text_embedding \\\n", + "251 [ 2.20562406e-02 -3.51827666e-02 7.63384486e-... \n", + "300 [ 0.01977486 -0.04289974 -0.05289588 -0.027267... \n", + "414 [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-... \n", + "493 [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-... \n", + "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "3 {\"token_count\":273,\"truncated\":false} \n", - "102 {\"token_count\":859,\"truncated\":false} \n", - "660 {\"token_count\":282,\"truncated\":false} \n", - "1500 {\"token_count\":39,\"truncated\":false} \n", - "1601 {\"token_count\":94,\"truncated\":false} \n", + " statistics ml_embed_text_status \\\n", + "251 {\"token_count\":145,\"truncated\":false} \n", + "300 {\"token_count\":498,\"truncated\":false} \n", + "414 {\"token_count\":263,\"truncated\":false} \n", + "493 {\"token_count\":395,\"truncated\":false} \n", + "545 {\"token_count\":178,\"truncated\":false} \n", "\n", - " content \n", - "3 Despite multiple written requests, the unverif... \n", - "102 To Whom It May Concern, Please be advised that... \n", - "660 Transunion has failed to remove a XXXX XXXX b... \n", - "1500 FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR... \n", - "1601 I am a victim of identity theft/fraud. I have ... \n", + " content \n", + "251 A purse was purchased from XXXX XXXX on XX/XX/... \n", + "300 XXXX XXXX XXXXXXXX has reported on my credit r... \n", + "414 I have tried to dispute US BKPT CT TX XXXXXXXX... \n", + "493 Discover Student Loan has been holding onto {$... \n", + "545 My payments have been approximately {$89.00} w... \n", "\n", "[5 rows x 4 columns]" ] }, - "execution_count": 11, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -647,7 +653,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": { "id": "AhNTnEC5FRz2" }, @@ -668,7 +674,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": { "id": "6poSxh-fGJF7" }, @@ -676,7 +682,7 @@ { "data": { "text/html": [ - "Query job ed22ef18-cbdf-4c7a-9244-38f65e2bbefb is DONE. 61.5 MB processed. Open Job" + "Query job 37f432dd-9ed7-4bbd-adc1-f33b8cbab33a is DONE. 61.5 MB processed. Open Job" ], "text/plain": [ "" @@ -688,7 +694,7 @@ { "data": { "text/html": [ - "Query job 5e94bc4b-44d6-4c20-b907-cee9795d5b54 is DONE. 61.4 MB processed. Open Job" + "Query job 8ca9cc5a-091a-4d4e-bcf8-04d4bfec7b6b is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -700,7 +706,7 @@ { "data": { "text/html": [ - "Query job 915d58d8-403b-44bf-8cf2-38ad5a87be9d is DONE. 72.3 MB processed. Open Job" + "Query job cdc11d15-fa78-4190-841f-18642ddb53f8 is DONE. 72.3 MB processed. Open Job" ], "text/plain": [ "" @@ -712,7 +718,7 @@ { "data": { "text/html": [ - "Query job fa241e23-d153-49ea-8dff-6d28906bcb1f is DONE. 80.0 kB processed. Open Job" + "Query job 39a6ea59-0e3d-4d69-bf8a-1502b9f1a48f is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -724,7 +730,7 @@ { "data": { "text/html": [ - "Query job a3b00aa3-d6d6-4e30-ac96-816b81fcd202 is DONE. 80.0 kB processed. Open Job" + "Query job 324ab354-ecbd-4bde-8f73-806856a53a19 is DONE. 73.2 MB processed. Open Job" ], "text/plain": [ "" @@ -736,7 +742,19 @@ { "data": { "text/html": [ - "Query job 38277ed2-bcad-4de1-b508-7bb44b3158dc is DONE. 73.2 MB processed. Open Job" + "Query job cb7558b2-a967-491c-82db-e11116f1fba4 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 0cfc2298-e67b-4a03-804d-f4abd8d56da0 is DONE. 36.6 kB processed. Open Job" ], "text/plain": [ "" @@ -776,49 +794,49 @@ " \n", " \n", " \n", - " 3\n", - " 8\n", - " [{'CENTROID_ID': 8, 'DISTANCE': 0.399505154607...\n", - " [0.02506784163415432, -0.03947214409708977, -0...\n", - " {\"token_count\":273,\"truncated\":false}\n", + " 251\n", + " 2\n", + " [{'CENTROID_ID': 2, 'DISTANCE': 0.534540549592...\n", + " [ 2.20562406e-02 -3.51827666e-02 7.63384486e-...\n", + " {\"token_count\":145,\"truncated\":false}\n", " \n", - " Despite multiple written requests, the unverif...\n", + " A purse was purchased from XXXX XXXX on XX/XX/...\n", " \n", " \n", - " 102\n", - " 8\n", - " [{'CENTROID_ID': 8, 'DISTANCE': 0.403332660368...\n", - " [0.029197776690125465, -0.028653817251324654, ...\n", - " {\"token_count\":859,\"truncated\":false}\n", + " 300\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.437379245910...\n", + " [ 0.01977486 -0.04289974 -0.05289588 -0.027267...\n", + " {\"token_count\":498,\"truncated\":false}\n", " \n", - " To Whom It May Concern, Please be advised that...\n", + " XXXX XXXX XXXXXXXX has reported on my credit r...\n", " \n", " \n", - " 660\n", - " 10\n", - " [{'CENTROID_ID': 10, 'DISTANCE': 0.42615208239...\n", - " [0.034257132560014725, -0.05359702184796333, -...\n", - " {\"token_count\":282,\"truncated\":false}\n", + " 414\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.482813493921...\n", + " [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-...\n", + " {\"token_count\":263,\"truncated\":false}\n", " \n", - " Transunion has failed to remove a XXXX XXXX b...\n", + " I have tried to dispute US BKPT CT TX XXXXXXXX...\n", " \n", " \n", - " 1500\n", - " 6\n", - " [{'CENTROID_ID': 6, 'DISTANCE': 0.500031509322...\n", - " [-0.008016454987227917, -0.05421802029013634, ...\n", - " {\"token_count\":39,\"truncated\":false}\n", + " 493\n", + " 9\n", + " [{'CENTROID_ID': 9, 'DISTANCE': 0.561752335987...\n", + " [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-...\n", + " {\"token_count\":395,\"truncated\":false}\n", " \n", - " FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR...\n", + " Discover Student Loan has been holding onto {$...\n", " \n", " \n", - " 1601\n", - " 6\n", - " [{'CENTROID_ID': 6, 'DISTANCE': 0.361769337067...\n", - " [-0.005403461866080761, -0.024799197912216187,...\n", - " {\"token_count\":94,\"truncated\":false}\n", + " 545\n", + " 9\n", + " [{'CENTROID_ID': 9, 'DISTANCE': 0.540487926907...\n", + " [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...\n", + " {\"token_count\":178,\"truncated\":false}\n", " \n", - " I am a victim of identity theft/fraud. I have ...\n", + " My payments have been approximately {$89.00} w...\n", " \n", " \n", "\n", @@ -826,38 +844,38 @@ "[5 rows x 6 columns in total]" ], "text/plain": [ - " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", - "3 8 [{'CENTROID_ID': 8, 'DISTANCE': 0.399505154607... \n", - "102 8 [{'CENTROID_ID': 8, 'DISTANCE': 0.403332660368... \n", - "660 10 [{'CENTROID_ID': 10, 'DISTANCE': 0.42615208239... \n", - "1500 6 [{'CENTROID_ID': 6, 'DISTANCE': 0.500031509322... \n", - "1601 6 [{'CENTROID_ID': 6, 'DISTANCE': 0.361769337067... \n", + " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", + "251 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.534540549592... \n", + "300 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.437379245910... \n", + "414 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.482813493921... \n", + "493 9 [{'CENTROID_ID': 9, 'DISTANCE': 0.561752335987... \n", + "545 9 [{'CENTROID_ID': 9, 'DISTANCE': 0.540487926907... \n", "\n", - " text_embedding \\\n", - "3 [0.02506784163415432, -0.03947214409708977, -0... \n", - "102 [0.029197776690125465, -0.028653817251324654, ... \n", - "660 [0.034257132560014725, -0.05359702184796333, -... \n", - "1500 [-0.008016454987227917, -0.05421802029013634, ... \n", - "1601 [-0.005403461866080761, -0.024799197912216187,... \n", + " text_embedding \\\n", + "251 [ 2.20562406e-02 -3.51827666e-02 7.63384486e-... \n", + "300 [ 0.01977486 -0.04289974 -0.05289588 -0.027267... \n", + "414 [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-... \n", + "493 [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-... \n", + "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "3 {\"token_count\":273,\"truncated\":false} \n", - "102 {\"token_count\":859,\"truncated\":false} \n", - "660 {\"token_count\":282,\"truncated\":false} \n", - "1500 {\"token_count\":39,\"truncated\":false} \n", - "1601 {\"token_count\":94,\"truncated\":false} \n", + " statistics ml_embed_text_status \\\n", + "251 {\"token_count\":145,\"truncated\":false} \n", + "300 {\"token_count\":498,\"truncated\":false} \n", + "414 {\"token_count\":263,\"truncated\":false} \n", + "493 {\"token_count\":395,\"truncated\":false} \n", + "545 {\"token_count\":178,\"truncated\":false} \n", "\n", - " content \n", - "3 Despite multiple written requests, the unverif... \n", - "102 To Whom It May Concern, Please be advised that... \n", - "660 Transunion has failed to remove a XXXX XXXX b... \n", - "1500 FRAUDULENT TRADELINES WERE ATTRIBUTED TO MY CR... \n", - "1601 I am a victim of identity theft/fraud. I have ... \n", + " content \n", + "251 A purse was purchased from XXXX XXXX on XX/XX/... \n", + "300 XXXX XXXX XXXXXXXX has reported on my credit r... \n", + "414 I have tried to dispute US BKPT CT TX XXXXXXXX... \n", + "493 Discover Student Loan has been holding onto {$... \n", + "545 My payments have been approximately {$89.00} w... \n", "\n", "[5 rows x 6 columns]" ] }, - "execution_count": 13, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -899,7 +917,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": { "id": "2E7wXM_jGqo6" }, @@ -907,7 +925,7 @@ { "data": { "text/html": [ - "Query job 8aec7d59-aaf5-4c55-85d3-1c96ee904558 is DONE. 10.5 MB processed. Open Job" + "Query job 84f95981-01c7-49ca-a10c-5842f07d867f is DONE. 10.6 MB processed. Open Job" ], "text/plain": [ "" @@ -919,7 +937,7 @@ { "data": { "text/html": [ - "Query job f269725a-da1c-46d4-aa3f-3525bbbceaef is DONE. 10.5 MB processed. Open Job" + "Query job 0872869a-94f0-4c3f-9f92-da7272f95cd0 is DONE. 10.6 MB processed. Open Job" ], "text/plain": [ "" @@ -945,7 +963,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": { "id": "ZNDiueI9IP5e" }, @@ -955,52 +973,48 @@ "output_type": "stream", "text": [ "comment list 1:\n", - "1. Out of the blue I received a debt collection notice saying they were collecting debt of {$710.00} for a XXXX XXXX which I have no idea what it was and was never notified of. I replied with written notification disputing the debt and demand of supporting documentation ( as the debt collection simply stated a total due with no supporting documentation ) but received no supporting documentation or verification.\n", - "2. I am referring a case to the Consumer Protection Bureau as follows. \n", - "\n", - "First, please see the most recent document, 'New_Debt_Collector_Account.jpg '. This was sent to me on XX/XX/2021 and arrived on XX/XX/2021. This is a letter from \" I.C. System '' telling me I must pay a delinquent account for \" XXXX ''. An account I thought that was already settled by \" Offices of the XXXX XXXX XXXX ''. Or so I believed. Until now... \n", - "\n", - "See attachment 'dispute.rtf '. This is the original letter I sent to \" Offices of the XXXX XXXX XXXX ''. Within 'dispute.rtf ' is a very detailed, and extremely well articulated ( if I do say so myself ) dispute letter, delineating the abuses and reasons to \" XXXX '' of why I would not pay the bill. In a response, sent back to me by \" Offices of the XXXX XXXX XXXX '', 'dispute_response.jpg ', they said they would discontinue their collection efforts and no information regarding this account would be referred to the credit bureau XXXX \n", - "\n", - "THEN, 7 months later, from \" I.C. System '' I receive, 'New_Debt_Collector_Account.jpg ', as first seent above, saying I still owe this debt.\n", - "\n", - "My question, to \" I.C. System '' \" XXXX XXXX '' and the \" Consumer Protection Bureau '' are : How can I dispute and then remove a debt, that I legitimatlly do not owe, and be exonerated from it, if \" Charter/Spectrum '' can simply refer it to another debt collecting company to try and collect it? And then tarnish my credit? Is misleading a consumer like this 'legal '?\n", + "1. XXXX XXXX XXXXXXXX has reported on my credit report for XXXX that I had 3 payments 30 or more days past due this information was and is incorrect i have sent numerous letters and responses to them asking for information regarding this reporting the bank continues to send me responces stating that my credit report is accurat but no documentation supporting the reporting of negative payments i have sent the XXXX XXXX XXXX numerous document showing past mistake of my payment that were misapplied by thier associates yet they still will not correct my credit report the XXXX XXXX XXXX has the wrong infor mation on my payment, balance last payments amount made & ect i have recently sent them a letter ( This is the exact letter ) and other information im having problems with the problem im having Date BLANK BLANK BLANK Complaint Dispute on credit report reporting XXXX XXXX XXXX Address Mail XXXX fl XXXX XXXX XXXX XXXX FL Zip Code XXXX Regarding Account Number ( Blank ) & Account Number ( Blank ) I ( blank ) on this XXXX Day of XXXX XXXX do hereby request a complete copy of my payment history from XXXX XXXX XXXX to XXXX XXXX XXXX this request is to include dates of all payments that was made on these accounts dates of all payments that were past due on these accounts and dates payments was paid to these account s after due date. I ( Blank ) am also requesting all letters and correspondence advising me / making me aware of my past due payments I also request all payments showing my account were paid 30 or more days late I ( Blank ) also request all payment dates and how payment was made ( Ex XXXX XXXX XXXX XXXXXXXX. XXXX Please Mail this information ASAP\n", + "2. I have tried to dispute US BKPT CT TX XXXXXXXX XXXX XXXX Account number XXXX on my credit report several times as inaccurate ( XX/XX/XXXX XX/XX/XXXX XX/XX/XXXX and XX/XX/XXXX ) as not mine but the credit bureaus have verified that it is accurate. I mailed a letter to the county records office ( XX/XX/XXXX ) pertaining to US BKPT CT TX XXXX Account number XXXX where this occurred and they replied on XX/XX/XXXX stating that they do not send information to the credit bureaus. This is where I am confused because after reading the law the FCRA if the original creditor or institution can not verify the information then the credit bureaus reporting it have to delete it. I sent this into the credit bureaus as well along with the paperwork from the county record office and it seems like they are not doing ANY sort of investigation which they are required to\n", + "3. Unauthorized hard inquires on my credit report and bureau refuses to remove them after they placed them on my credit report with my authorization XXXX Inquiry XX/XX/XXXX XXXX XXXX Inquiry XX/XX/XXXX XXXX XXXX inquiry XX/XX/XXXX and XX/XX/XXXX XXXX XX/XX/XXXX XXXX\n", + "4. XXXX - XX/XX/2020 Equifax Hello, I have reviewed a copy of my current credit report and it shows the inquiries above are fraudulent and I have sent a copy of FTC report to request removal. I have not been successful and need these removed due to attempting to purchase a home.\n", + "5. I have tried to remedy the issue that XXXX XXXX caused as XXXX XXXX indicated to me that Equifax could not remove a discharge debt from my report because of two social security numbers. In fact it was not two ssn 's but rather my deceased husbands DOB XXXX was on my report. XXXX XXXX is notorious for reporting incorrect data so i am going to get this resolved through CEPB. Attached please find the bankruptcy discharge notification and a copy of the requested drivers license with my DOB, XXXX. I expect that the debt for XXXX placed by XXXX XXXX to be removed as all the requested documentation is included in the correspondence.\n", "\n", - "As 'dispute.rtf ' indicates, I even said I would contact the Consumer Protection Bureau and the XXXX. I did not originally do this and regret that decision, but I am doing so now!! \n", - "\n", - "Furthermore, due to the unparalleled absolute deceptive malfeasance on the part of \" XXXX '' and these debt XXXX, I am considering approaching media outlets with my story. Consumers can not and should not be ripped of this way and their credit tarnished, while a corrupt telecommunications company and their debt collecting cronies attempt to charge them for several months of internet they never had. \n", + "comment list 2:\n", + "1. A purse was purchased from XXXX XXXX on XX/XX/2021. As they stated my package was delivered and signed for. Indeed it was, I also still have it. My package was damaged and only XXXX can file a claim with XXXX. I have tried to contact XXXX and was told to contact the shipper. I have contacted XXXX and was told that a chargeback was filed with TCF so there is nothing that they can do. I have provided all of these documents to you all including a police report. I will continue to escalate this if needed.\n", + "2. On Saturday XX/XX/XXXX I submitted a dispute for several fraudulent transactions with chime. I had stopped using chime back in XXXX of 2022 because I believed they were scamming me because of the money that was being taken out of my account without my knowledge and i had also had my phone stolen at work with my chime card and ssi & ID in the back of my phone case. I told them I believed thats how i was scammed for all of those fraudulent transactions that I disputed and also that money from my job had still been posting in my chime account after i had stopped using it because i was unable to change my direct deposit information with my job because i couldnt access my chime account anymore so I didnt have access to my routing and account number but i still had my card connected to my apple pay which I eventually stopped using because i was getting paid but most time my card on apple pay was declined because i was still being scammed out my money. Ive tried ordering new cards several time before I completely stopped using chime however the same thing was happening with each card and chime couldnt figure out how to stop it. \n", + "A few days ago i tried to create a new chime bank account because i had completely forgotten about my old scammed chime account but that app prompted me to sign into my old account Chime told me that they would do a complete investigation and let me know of the results. \n", + "However, on Sunday XX/XX/XXXX at XXXX i got an email from chime stating Hello XXXX XXXX XXXX This letter is to inform you that we have made a final determination regarding the claim referenced above. \n", + "Based on our investigation, we have concluded no error occurred. Therefore, no funds will be credited to your account and this claim is considered closed. \n", "\n", - "This is fraud, it is illegal, and it needs to stop. And I wish to be exonerated from this debt as I was originally led to believe!!! \n", + "I emailed them back for the documentation on how they did the dispute and verified the information was inaccurate. I also asked how is it possible that a investigation for 20+ transactions had been completed in less than 1 day. \n", + "I truly believe chime did not do any investigation at all and that they lied about my investigation/dispute They have been unable to provide me with the documents and dispute information. In fact, they said id have to wait 10+ business days to receive that information in mail because they arent going to email it to me I found this very unfortunate. \n", "\n", - "Finally, if \" XXXX '' are part of any class action lawsuits involving this behaviour ( and I'm sure they are ), then I wish to testify on the behalf on any government, individual, and/or private institutions as to the corrput nature of entities such as \" XXXX '' and their debt collectors, and hereby consent to providing all the following statements, including all attachments herein, and any testimony they want me to give as evidence. \n", + "Here are my reference numbers for my disputes XXXX XXXX\n", + "3. My sister and I attempted to close our aging father 's professional account at Wells Fargo. My sister worked closely with the local branch and followed their directives. But, even after 6 months the bank continued to reject the paperwork presented. Anticipating our father 's further decline, we hired an estate attorney to help us close the account. Even after appointing me as an officer of my dad 's company and presenting new documentation which followed the bank 's directive, the bank again refused to move the money to my mother and to close the account. The entire time the balance of the account was depleted through bank fees from around {$1800.00} to around {$1000.00}. In the end, Wells Fargo refunded the entire original balance of around {$1800.00} plus some a \" consumer satisfaction credit '' and transferred the funds to my mother 's account.\n", + "4. On XXXX XX/XX/2020 a sale of three Tahitian pearl necklaces was made by my online shop. This amounted to approximately {$2800.00}. Some of the funds were withdrawn by me leaving a total of {$220000.00} in the Paypal account. \n", "\n", - "-XXXX XXXX XXXX XXXX XXXX\n", - "3. XXXX called on XX/XX/XXXX at about XXXXXXXX XXXX claiming to be from Sherloq financial, about collecting medical debt from XXXX, he was XXXX and XXXX and said there were four other accounts but did not specify which or how much. I have been contacted by my insurance that I have paid everything in relation to this. I have talked to my doctor and my endocrinologist and they said I don't owe anything either. ( I am XXXX and have XXXX and XXXX that pays for my doctor visits. ) I feel that they are just trying to collect money they are not owed and its predatory.\n", - "4. While moving out of my long time residence in XXXX, CA I began the process to cut off my services in XXXX of XXXX which included the phone, internet and XXXX, which were all included in a single bill. Initially the final date I relayed to XXXX XXXX XXXX was the XXXX of XXXX but because of delays in the move was extended to XX/XX/XXXX. Because XXXX ended up mistakenly cutting off the satellite service on the XXXX I had to call them directly to extend it to the XXXX. I left the house for the last time on XXXX XX/XX/XXXX and turn in all the equipment as required to the local XXXX office. Although I assumed all the services were ended properly I continued to get bills from XXXX ( not from XXXX XXXX XXXX ) so I called several times to rectify the mistake and finally sent a detailed escalation letter to them in XX/XX/XXXX. I felt at this point they finally realized their mistake and closed the bill but in XXXX of XXXX I then received a collection notice from Afni, Inc. for the disputed amount. I recently forwarded them a dispute letter along with other pertinent details of the situation. I have no idea why XXXX is unable to verify the fact I no longer lived at the XXXX residence, nor retained any of the receivers or remotes after the XXXX of XX/XX/XXXX. This should be an easy task on their end especially since I went above and beyond due diligence to work with them to correct this.\n", - "5. Merchants and Medical Credit began reporting on a debt they had acquired on XXXX XXXX 2014 and never sent written notice despite it being required under section 806 of the FDCPA. For 3 years they have been holding my financial life hostage and only recently came to my attention. Despite many disputes with the big three credit reporting agencies section 806 has been ignored and they have continued to break and ignore the proper procedures set forth by the FDCPA.\n", + "Suddenly the balance was frozen for 180 days by Paypal. A telephone call to their customer service on about XXXX XXXX resulted in a rude and abrupt lady telling me that Paypal was parting ways with me. I was given no reasonable explanation why the account was limited or the funds frozen other than to infer that I was busy with fraudulent activity. The rude person refused to give me more details. \n", "\n", - "comment list 2:\n", - "1. I have a PayPal account and a PayPal credit card and a XXXX XXXX Checking account. \n", - "If I use PayPal, or the PayPal credit card the charges are then debited from my XXXX XXXX Checking account.. \n", + "I submitted all the details/documents they requested from then on. My appeals were refused, On XXXX XX/XX/2020 the 180 frozen days expired. I was requested by Paypal to submit all my personal details/documents again. I did. I have the XXXX XXXX XXXX XXXX ID Document. It is perfectly legal and valid in XXXX XXXX as is the new XXXX XXXXXXXX which is now being issued. But Paypal is not accepting my ID Document. So my funds are not being released.They send computer generated emails which offer no explanation as to why my ID document is not being accepted or any solution to the problem. \n", "\n", - "Recently there were two fraudulent and unauthorized charges I was alerted to by PayPal.\n", + "This matter is most frustrating and I'm sure not in line with any financial practice.\n", + "5. On XX/XX/XXXX I received a text from Chase showing -- -- -- -- - Chase Fraud : Did you attempt a {$1700.00} withdrawal on XX/XX/XXXX with card XXXX? \n", + "replay yes or no. Msg & data rates may apply. \n", + "-- -- -- -- - Then I replied no, Then chase sent a text they will close my account, give them a call. \n", "\n", - "One was a debit from XXXX for {$240.00}. I contacted XXXX and they said there was no record for that amount and date for purchases or charges anywhere on XXXX. They said it was most likely fraudulent and I never authorized it. \n", + "So I called chase to report, they are saying because it is pending transaction I have to wait until pending is gone, 2 days later they accepted my claim. \n", "\n", - "The other was from XXXX XXXX for {$50.00}. I did make political donations to politicians during the election cycle and I noted each in my checking account. I STOPPED making any further contributions after the last elections in Georgia. This charge was not one I had authorized and it was dated AFTER the last election and was not one of the ones I had checked against my checking account. \n", + "Today XX/XX/XXXX they refused to credit my money {$1700.00} because my pin number and debit card were used. So I told them I never received card. \n", + "Still their answer is same. \n", "\n", - "I disputed both with XXXX XXXX on my checking account and they researched both of them and determined they were fraudulent and XXXX XXXX noted in writing \" We have completed our research of your inquiry and returned the charges unpaid. As a result no money was deducted from your account '' I have filed two claims disputing both charges to PayPal and each time they have denied my fraudulent/unauthorized claims and I even spoke to PayPal people several times and quoted XXXX XXXX to them and then filed a dispute with them on their denial. \n", + "Then I asked what should I do? file small claim against chase? go to police office? \n", + "They told me I can go to police office to file a claim. \n", "\n", - "They are still denying my dispute and claiming that I owe them {$290.00} for both fraudulent charges. Please help me get this resolved. They keep sending me notices and phone calls. I should not have to pay them since XXXX XXXX returned them as unpaid and determined they were fraudulent and even XXXX says there was no purchase or credit so it was fraudulent and unauthorized.\n", - "2. Banked with truist for almost XXXX months now and never had an issue until i decided to start funding my account and using my debit card to pay my bills, they locked my debit card which is fine, called to have it unblocked no problem and they end up locking it again but this time they are saying i have to come into a branch to do so which IS a problem. Truist bank is nowhere near me in my state, its an out of state bank so thats literally not an option. Bank locked my online banking with money still in my account lol, i tried an ACH transfer and set up bill pay payments and i tried to log in just to find out my account is restricted for security reasons. This has been XXXX of the worst banking experiences i've ever had and im not being treated like a customer at all. its almost like truist isnt allowing me to use my money the way i want or access my funds properly. Ive had no problem up until i made a large deposit into my account and started using those funds. \n", + "I will go to police office after my work. \n", "\n", - "i have no problenm verifying myself any other way but going out of my current state just to visit truist bank is not an option.\n", - "3. Navy Exchange ( NEX ), the \" XXXX '' onboard US XXXX bases, offers currency exchange for personnel stationed overseas ( in XXXX ) to pay their off-base rent. However, the exchange rate offered is always 2-3 % less than the Bank Rate for US dollar to XXXX XXXX exchange. \n", + "Before I go to police office, I am asking your help about this situation. \n", "\n", - "To offset this, the Navy Exchange provides a {$10.00} gift card for use at the NEX, for each month paid. However, this is inequitable because the gift card is always for the same amount, regardless of the monthly rent amount ... so a single military member with an $ XXXX/month ( equivalent ) rent payment derives more proportional benefit from this {$10.00} gift card than a military family whose rent could be closer to $ XXXX ( equivalent ).\n", - "4. In XX/XX/XXXX, based on a friend 's recommendation, I visited a vacation rental home website to rent her neighbor 's property in XXXX, California, USA. The vacation rental home website is called : XXXX. I used the right side of the website called \" Contact Owner '' to reach out to the owner. I received an email from the owner, XXXX, who let me know that the property was only available for 26 days at a time, as such, I did not move forward. I also received another email from the email address : XXXX. The contact was named XXXX XXXX who asked for desired check-in/out dates, number of bedrooms, and preferred location. He also asked about preferred budget and stated that he manages a lot of properties and could offer an attractive discount. I responded with my preferences and he sent an offer for a vacation rental property. He included a link to view the property via XXXX. I asked for the address of the property and he provided the following address : XXXX XXXX XXXX XXXX, California. He let me know that 50 % of the total was due as a deposit and the remaining amount was due 30 days prior to the arrival date. He also let me know that the owners of the property live in XXXX and require payment with the company XXXX ( formerly TransferWise ). I recognize XXXX as I have used the company in the past and already had an account. Therefore, on XX/XX/XXXX, I sent the money through XXXX to the stated \" Owners '' of the property : XXXX XXXX and XXXX XXXX. \n", - "Eight days later, I realized that the transaction was a scam after Googling the email address ( XXXX ). There were at least 20 reviewers with similar experiences that claimed this entity was a scammer for rental properties. At this point ( on XX/XX/XXXX ), I reached back out to XXXX via email to ask for the money back, and he agreed to send the money via check to my mailing address. However, the check did not arrive. I let him know that the money did not arrive and asked him to reverse the XXXX transaction. However, I did not receive a response from XXXX. After this, I contacted my bank. My bank provided a temporary reimbursement while an investigation was performed. After the investigation was complete 60 days later ( XX/XX/XXXX ), the reimbursement was removed since my bank determined that XXXX is the company responsible in this matter. As such, I am looking to get a refund for the amount with XXXX.\n", - "5. I can not withdraw money or pay my rent from my Way to go card from Florida reemployment. It keeps declining me. It was working fine in XXXX, I went to pay my rent for XXXX, and it declined ; I went to an atm again declined. So I ordered a new card and paid expedited shipping ; thinking this one was damaged, I activated it, and it again declined me. There is absolutely no way to contact anyone. All they have is an automatic response which doesn't provide any help\n", + "Please help me.\n", "\n" ] } @@ -1023,7 +1037,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": { "id": "BfHGJLirzSvH" }, @@ -1034,51 +1048,47 @@ "text": [ "Please highlight the most obvious difference between the two lists of comments:\n", "comment list 1:\n", - "1. Out of the blue I received a debt collection notice saying they were collecting debt of {$710.00} for a XXXX XXXX which I have no idea what it was and was never notified of. I replied with written notification disputing the debt and demand of supporting documentation ( as the debt collection simply stated a total due with no supporting documentation ) but received no supporting documentation or verification.\n", - "2. I am referring a case to the Consumer Protection Bureau as follows. \n", - "\n", - "First, please see the most recent document, 'New_Debt_Collector_Account.jpg '. This was sent to me on XX/XX/2021 and arrived on XX/XX/2021. This is a letter from \" I.C. System '' telling me I must pay a delinquent account for \" XXXX ''. An account I thought that was already settled by \" Offices of the XXXX XXXX XXXX ''. Or so I believed. Until now... \n", - "\n", - "See attachment 'dispute.rtf '. This is the original letter I sent to \" Offices of the XXXX XXXX XXXX ''. Within 'dispute.rtf ' is a very detailed, and extremely well articulated ( if I do say so myself ) dispute letter, delineating the abuses and reasons to \" XXXX '' of why I would not pay the bill. In a response, sent back to me by \" Offices of the XXXX XXXX XXXX '', 'dispute_response.jpg ', they said they would discontinue their collection efforts and no information regarding this account would be referred to the credit bureau XXXX \n", - "\n", - "THEN, 7 months later, from \" I.C. System '' I receive, 'New_Debt_Collector_Account.jpg ', as first seent above, saying I still owe this debt.\n", - "\n", - "My question, to \" I.C. System '' \" XXXX XXXX '' and the \" Consumer Protection Bureau '' are : How can I dispute and then remove a debt, that I legitimatlly do not owe, and be exonerated from it, if \" Charter/Spectrum '' can simply refer it to another debt collecting company to try and collect it? And then tarnish my credit? Is misleading a consumer like this 'legal '?\n", - "\n", - "As 'dispute.rtf ' indicates, I even said I would contact the Consumer Protection Bureau and the XXXX. I did not originally do this and regret that decision, but I am doing so now!! \n", + "1. XXXX XXXX XXXXXXXX has reported on my credit report for XXXX that I had 3 payments 30 or more days past due this information was and is incorrect i have sent numerous letters and responses to them asking for information regarding this reporting the bank continues to send me responces stating that my credit report is accurat but no documentation supporting the reporting of negative payments i have sent the XXXX XXXX XXXX numerous document showing past mistake of my payment that were misapplied by thier associates yet they still will not correct my credit report the XXXX XXXX XXXX has the wrong infor mation on my payment, balance last payments amount made & ect i have recently sent them a letter ( This is the exact letter ) and other information im having problems with the problem im having Date BLANK BLANK BLANK Complaint Dispute on credit report reporting XXXX XXXX XXXX Address Mail XXXX fl XXXX XXXX XXXX XXXX FL Zip Code XXXX Regarding Account Number ( Blank ) & Account Number ( Blank ) I ( blank ) on this XXXX Day of XXXX XXXX do hereby request a complete copy of my payment history from XXXX XXXX XXXX to XXXX XXXX XXXX this request is to include dates of all payments that was made on these accounts dates of all payments that were past due on these accounts and dates payments was paid to these account s after due date. I ( Blank ) am also requesting all letters and correspondence advising me / making me aware of my past due payments I also request all payments showing my account were paid 30 or more days late I ( Blank ) also request all payment dates and how payment was made ( Ex XXXX XXXX XXXX XXXXXXXX. XXXX Please Mail this information ASAP\n", + "2. I have tried to dispute US BKPT CT TX XXXXXXXX XXXX XXXX Account number XXXX on my credit report several times as inaccurate ( XX/XX/XXXX XX/XX/XXXX XX/XX/XXXX and XX/XX/XXXX ) as not mine but the credit bureaus have verified that it is accurate. I mailed a letter to the county records office ( XX/XX/XXXX ) pertaining to US BKPT CT TX XXXX Account number XXXX where this occurred and they replied on XX/XX/XXXX stating that they do not send information to the credit bureaus. This is where I am confused because after reading the law the FCRA if the original creditor or institution can not verify the information then the credit bureaus reporting it have to delete it. I sent this into the credit bureaus as well along with the paperwork from the county record office and it seems like they are not doing ANY sort of investigation which they are required to\n", + "3. Unauthorized hard inquires on my credit report and bureau refuses to remove them after they placed them on my credit report with my authorization XXXX Inquiry XX/XX/XXXX XXXX XXXX Inquiry XX/XX/XXXX XXXX XXXX inquiry XX/XX/XXXX and XX/XX/XXXX XXXX XX/XX/XXXX XXXX\n", + "4. XXXX - XX/XX/2020 Equifax Hello, I have reviewed a copy of my current credit report and it shows the inquiries above are fraudulent and I have sent a copy of FTC report to request removal. I have not been successful and need these removed due to attempting to purchase a home.\n", + "5. I have tried to remedy the issue that XXXX XXXX caused as XXXX XXXX indicated to me that Equifax could not remove a discharge debt from my report because of two social security numbers. In fact it was not two ssn 's but rather my deceased husbands DOB XXXX was on my report. XXXX XXXX is notorious for reporting incorrect data so i am going to get this resolved through CEPB. Attached please find the bankruptcy discharge notification and a copy of the requested drivers license with my DOB, XXXX. I expect that the debt for XXXX placed by XXXX XXXX to be removed as all the requested documentation is included in the correspondence.\n", + "comment list 2:\n", + "1. A purse was purchased from XXXX XXXX on XX/XX/2021. As they stated my package was delivered and signed for. Indeed it was, I also still have it. My package was damaged and only XXXX can file a claim with XXXX. I have tried to contact XXXX and was told to contact the shipper. I have contacted XXXX and was told that a chargeback was filed with TCF so there is nothing that they can do. I have provided all of these documents to you all including a police report. I will continue to escalate this if needed.\n", + "2. On Saturday XX/XX/XXXX I submitted a dispute for several fraudulent transactions with chime. I had stopped using chime back in XXXX of 2022 because I believed they were scamming me because of the money that was being taken out of my account without my knowledge and i had also had my phone stolen at work with my chime card and ssi & ID in the back of my phone case. I told them I believed thats how i was scammed for all of those fraudulent transactions that I disputed and also that money from my job had still been posting in my chime account after i had stopped using it because i was unable to change my direct deposit information with my job because i couldnt access my chime account anymore so I didnt have access to my routing and account number but i still had my card connected to my apple pay which I eventually stopped using because i was getting paid but most time my card on apple pay was declined because i was still being scammed out my money. Ive tried ordering new cards several time before I completely stopped using chime however the same thing was happening with each card and chime couldnt figure out how to stop it. \n", + "A few days ago i tried to create a new chime bank account because i had completely forgotten about my old scammed chime account but that app prompted me to sign into my old account Chime told me that they would do a complete investigation and let me know of the results. \n", + "However, on Sunday XX/XX/XXXX at XXXX i got an email from chime stating Hello XXXX XXXX XXXX This letter is to inform you that we have made a final determination regarding the claim referenced above. \n", + "Based on our investigation, we have concluded no error occurred. Therefore, no funds will be credited to your account and this claim is considered closed. \n", "\n", - "Furthermore, due to the unparalleled absolute deceptive malfeasance on the part of \" XXXX '' and these debt XXXX, I am considering approaching media outlets with my story. Consumers can not and should not be ripped of this way and their credit tarnished, while a corrupt telecommunications company and their debt collecting cronies attempt to charge them for several months of internet they never had. \n", + "I emailed them back for the documentation on how they did the dispute and verified the information was inaccurate. I also asked how is it possible that a investigation for 20+ transactions had been completed in less than 1 day. \n", + "I truly believe chime did not do any investigation at all and that they lied about my investigation/dispute They have been unable to provide me with the documents and dispute information. In fact, they said id have to wait 10+ business days to receive that information in mail because they arent going to email it to me I found this very unfortunate. \n", "\n", - "This is fraud, it is illegal, and it needs to stop. And I wish to be exonerated from this debt as I was originally led to believe!!! \n", + "Here are my reference numbers for my disputes XXXX XXXX\n", + "3. My sister and I attempted to close our aging father 's professional account at Wells Fargo. My sister worked closely with the local branch and followed their directives. But, even after 6 months the bank continued to reject the paperwork presented. Anticipating our father 's further decline, we hired an estate attorney to help us close the account. Even after appointing me as an officer of my dad 's company and presenting new documentation which followed the bank 's directive, the bank again refused to move the money to my mother and to close the account. The entire time the balance of the account was depleted through bank fees from around {$1800.00} to around {$1000.00}. In the end, Wells Fargo refunded the entire original balance of around {$1800.00} plus some a \" consumer satisfaction credit '' and transferred the funds to my mother 's account.\n", + "4. On XXXX XX/XX/2020 a sale of three Tahitian pearl necklaces was made by my online shop. This amounted to approximately {$2800.00}. Some of the funds were withdrawn by me leaving a total of {$220000.00} in the Paypal account. \n", "\n", - "Finally, if \" XXXX '' are part of any class action lawsuits involving this behaviour ( and I'm sure they are ), then I wish to testify on the behalf on any government, individual, and/or private institutions as to the corrput nature of entities such as \" XXXX '' and their debt collectors, and hereby consent to providing all the following statements, including all attachments herein, and any testimony they want me to give as evidence. \n", + "Suddenly the balance was frozen for 180 days by Paypal. A telephone call to their customer service on about XXXX XXXX resulted in a rude and abrupt lady telling me that Paypal was parting ways with me. I was given no reasonable explanation why the account was limited or the funds frozen other than to infer that I was busy with fraudulent activity. The rude person refused to give me more details. \n", "\n", - "-XXXX XXXX XXXX XXXX XXXX\n", - "3. XXXX called on XX/XX/XXXX at about XXXXXXXX XXXX claiming to be from Sherloq financial, about collecting medical debt from XXXX, he was XXXX and XXXX and said there were four other accounts but did not specify which or how much. I have been contacted by my insurance that I have paid everything in relation to this. I have talked to my doctor and my endocrinologist and they said I don't owe anything either. ( I am XXXX and have XXXX and XXXX that pays for my doctor visits. ) I feel that they are just trying to collect money they are not owed and its predatory.\n", - "4. While moving out of my long time residence in XXXX, CA I began the process to cut off my services in XXXX of XXXX which included the phone, internet and XXXX, which were all included in a single bill. Initially the final date I relayed to XXXX XXXX XXXX was the XXXX of XXXX but because of delays in the move was extended to XX/XX/XXXX. Because XXXX ended up mistakenly cutting off the satellite service on the XXXX I had to call them directly to extend it to the XXXX. I left the house for the last time on XXXX XX/XX/XXXX and turn in all the equipment as required to the local XXXX office. Although I assumed all the services were ended properly I continued to get bills from XXXX ( not from XXXX XXXX XXXX ) so I called several times to rectify the mistake and finally sent a detailed escalation letter to them in XX/XX/XXXX. I felt at this point they finally realized their mistake and closed the bill but in XXXX of XXXX I then received a collection notice from Afni, Inc. for the disputed amount. I recently forwarded them a dispute letter along with other pertinent details of the situation. I have no idea why XXXX is unable to verify the fact I no longer lived at the XXXX residence, nor retained any of the receivers or remotes after the XXXX of XX/XX/XXXX. This should be an easy task on their end especially since I went above and beyond due diligence to work with them to correct this.\n", - "5. Merchants and Medical Credit began reporting on a debt they had acquired on XXXX XXXX 2014 and never sent written notice despite it being required under section 806 of the FDCPA. For 3 years they have been holding my financial life hostage and only recently came to my attention. Despite many disputes with the big three credit reporting agencies section 806 has been ignored and they have continued to break and ignore the proper procedures set forth by the FDCPA.\n", - "comment list 2:\n", - "1. I have a PayPal account and a PayPal credit card and a XXXX XXXX Checking account. \n", - "If I use PayPal, or the PayPal credit card the charges are then debited from my XXXX XXXX Checking account.. \n", + "I submitted all the details/documents they requested from then on. My appeals were refused, On XXXX XX/XX/2020 the 180 frozen days expired. I was requested by Paypal to submit all my personal details/documents again. I did. I have the XXXX XXXX XXXX XXXX ID Document. It is perfectly legal and valid in XXXX XXXX as is the new XXXX XXXXXXXX which is now being issued. But Paypal is not accepting my ID Document. So my funds are not being released.They send computer generated emails which offer no explanation as to why my ID document is not being accepted or any solution to the problem. \n", "\n", - "Recently there were two fraudulent and unauthorized charges I was alerted to by PayPal.\n", + "This matter is most frustrating and I'm sure not in line with any financial practice.\n", + "5. On XX/XX/XXXX I received a text from Chase showing -- -- -- -- - Chase Fraud : Did you attempt a {$1700.00} withdrawal on XX/XX/XXXX with card XXXX? \n", + "replay yes or no. Msg & data rates may apply. \n", + "-- -- -- -- - Then I replied no, Then chase sent a text they will close my account, give them a call. \n", "\n", - "One was a debit from XXXX for {$240.00}. I contacted XXXX and they said there was no record for that amount and date for purchases or charges anywhere on XXXX. They said it was most likely fraudulent and I never authorized it. \n", + "So I called chase to report, they are saying because it is pending transaction I have to wait until pending is gone, 2 days later they accepted my claim. \n", "\n", - "The other was from XXXX XXXX for {$50.00}. I did make political donations to politicians during the election cycle and I noted each in my checking account. I STOPPED making any further contributions after the last elections in Georgia. This charge was not one I had authorized and it was dated AFTER the last election and was not one of the ones I had checked against my checking account. \n", + "Today XX/XX/XXXX they refused to credit my money {$1700.00} because my pin number and debit card were used. So I told them I never received card. \n", + "Still their answer is same. \n", "\n", - "I disputed both with XXXX XXXX on my checking account and they researched both of them and determined they were fraudulent and XXXX XXXX noted in writing \" We have completed our research of your inquiry and returned the charges unpaid. As a result no money was deducted from your account '' I have filed two claims disputing both charges to PayPal and each time they have denied my fraudulent/unauthorized claims and I even spoke to PayPal people several times and quoted XXXX XXXX to them and then filed a dispute with them on their denial. \n", + "Then I asked what should I do? file small claim against chase? go to police office? \n", + "They told me I can go to police office to file a claim. \n", "\n", - "They are still denying my dispute and claiming that I owe them {$290.00} for both fraudulent charges. Please help me get this resolved. They keep sending me notices and phone calls. I should not have to pay them since XXXX XXXX returned them as unpaid and determined they were fraudulent and even XXXX says there was no purchase or credit so it was fraudulent and unauthorized.\n", - "2. Banked with truist for almost XXXX months now and never had an issue until i decided to start funding my account and using my debit card to pay my bills, they locked my debit card which is fine, called to have it unblocked no problem and they end up locking it again but this time they are saying i have to come into a branch to do so which IS a problem. Truist bank is nowhere near me in my state, its an out of state bank so thats literally not an option. Bank locked my online banking with money still in my account lol, i tried an ACH transfer and set up bill pay payments and i tried to log in just to find out my account is restricted for security reasons. This has been XXXX of the worst banking experiences i've ever had and im not being treated like a customer at all. its almost like truist isnt allowing me to use my money the way i want or access my funds properly. Ive had no problem up until i made a large deposit into my account and started using those funds. \n", + "I will go to police office after my work. \n", "\n", - "i have no problenm verifying myself any other way but going out of my current state just to visit truist bank is not an option.\n", - "3. Navy Exchange ( NEX ), the \" XXXX '' onboard US XXXX bases, offers currency exchange for personnel stationed overseas ( in XXXX ) to pay their off-base rent. However, the exchange rate offered is always 2-3 % less than the Bank Rate for US dollar to XXXX XXXX exchange. \n", + "Before I go to police office, I am asking your help about this situation. \n", "\n", - "To offset this, the Navy Exchange provides a {$10.00} gift card for use at the NEX, for each month paid. However, this is inequitable because the gift card is always for the same amount, regardless of the monthly rent amount ... so a single military member with an $ XXXX/month ( equivalent ) rent payment derives more proportional benefit from this {$10.00} gift card than a military family whose rent could be closer to $ XXXX ( equivalent ).\n", - "4. In XX/XX/XXXX, based on a friend 's recommendation, I visited a vacation rental home website to rent her neighbor 's property in XXXX, California, USA. The vacation rental home website is called : XXXX. I used the right side of the website called \" Contact Owner '' to reach out to the owner. I received an email from the owner, XXXX, who let me know that the property was only available for 26 days at a time, as such, I did not move forward. I also received another email from the email address : XXXX. The contact was named XXXX XXXX who asked for desired check-in/out dates, number of bedrooms, and preferred location. He also asked about preferred budget and stated that he manages a lot of properties and could offer an attractive discount. I responded with my preferences and he sent an offer for a vacation rental property. He included a link to view the property via XXXX. I asked for the address of the property and he provided the following address : XXXX XXXX XXXX XXXX, California. He let me know that 50 % of the total was due as a deposit and the remaining amount was due 30 days prior to the arrival date. He also let me know that the owners of the property live in XXXX and require payment with the company XXXX ( formerly TransferWise ). I recognize XXXX as I have used the company in the past and already had an account. Therefore, on XX/XX/XXXX, I sent the money through XXXX to the stated \" Owners '' of the property : XXXX XXXX and XXXX XXXX. \n", - "Eight days later, I realized that the transaction was a scam after Googling the email address ( XXXX ). There were at least 20 reviewers with similar experiences that claimed this entity was a scammer for rental properties. At this point ( on XX/XX/XXXX ), I reached back out to XXXX via email to ask for the money back, and he agreed to send the money via check to my mailing address. However, the check did not arrive. I let him know that the money did not arrive and asked him to reverse the XXXX transaction. However, I did not receive a response from XXXX. After this, I contacted my bank. My bank provided a temporary reimbursement while an investigation was performed. After the investigation was complete 60 days later ( XX/XX/XXXX ), the reimbursement was removed since my bank determined that XXXX is the company responsible in this matter. As such, I am looking to get a refund for the amount with XXXX.\n", - "5. I can not withdraw money or pay my rent from my Way to go card from Florida reemployment. It keeps declining me. It was working fine in XXXX, I went to pay my rent for XXXX, and it declined ; I went to an atm again declined. So I ordered a new card and paid expedited shipping ; thinking this one was damaged, I activated it, and it again declined me. There is absolutely no way to contact anyone. All they have is an automatic response which doesn't provide any help\n", + "Please help me.\n", "\n" ] } @@ -1102,7 +1112,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": { "id": "mL5P0_3X04dE" }, @@ -1110,7 +1120,7 @@ { "data": { "text/html": [ - "Query job 48da679e-d991-4af6-939f-8c45b3a262d8 is DONE. 0 Bytes processed. Open Job" + "Query job d3965d90-8af9-46cb-9129-40e1d2866efe is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1128,7 +1138,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": { "id": "ICWHsqAW1FNk" }, @@ -1140,7 +1150,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "metadata": { "id": "gB7e1LXU1pst" }, @@ -1148,7 +1158,7 @@ { "data": { "text/html": [ - "Query job 8877d0e9-a41e-4344-8dea-bf57c93a505e is DONE. 0 Bytes processed. Open Job" + "Query job 29a26018-027a-4c70-a795-841b5ace87d6 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1160,7 +1170,7 @@ { "data": { "text/html": [ - "Query job fa3de153-d6b8-421f-84f6-69e401a36cc8 is DONE. 0 Bytes processed. Open Job" + "Query job 3abcc8cd-fa9f-4a93-b6be-6e22c8cdaceb is DONE. 8 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1172,7 +1182,7 @@ { "data": { "text/html": [ - "Query job 749fbc0b-5da2-4089-ac80-d7a2ac8325f2 is DONE. 0 Bytes processed. Open Job" + "Query job ab5dfd41-98cb-4f24-a9c7-11399fcb2e47 is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1184,7 +1194,7 @@ { "data": { "text/html": [ - "Query job 6a8c8c0c-894f-41f1-a20f-adfb67349281 is DONE. 0 Bytes processed. Open Job" + "Query job c6c7bce4-81a0-4c4d-a515-2d5dfffc08a2 is DONE. 299 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1196,10 +1206,10 @@ { "data": { "text/plain": [ - "' The most obvious difference between the two lists of comments is the nature of the issues being raised.\\n\\n**Comment list 1 primarily focuses on disputes related to debt collection practices.** \\n\\n- Commenters express concerns about receiving debt collection notices for debts they believe they do not owe, lack of supporting documentation, and the transfer of debt between debt collection agencies without proper resolution. \\n\\n- They highlight the impact on their credit scores and the frustration of dealing with misleading or deceptive practices.\\n\\n\\n**Comment list 2, on the other hand, covers a wider range of issues related to financial services.** \\n\\n- It includes complaints about fraudulent charges'" + "'The most obvious difference between the two lists of comments is their subject matter. Comment list 1 primarily deals with issues related to credit reporting and identity theft, while comment list 2 focuses on issues related to bank accounts, fraudulent transactions, and customer service.'" ] }, - "execution_count": 19, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -1250,7 +1260,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.1" } }, "nbformat": 4, diff --git a/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb b/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb index 8c0b1b00387..32e6cb7924c 100644 --- a/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb @@ -371,7 +371,14 @@ }, "outputs": [], "source": [ + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", "bpd.options.bigquery.location = LOCATION" ] }, diff --git a/notebooks/getting_started/getting_started_bq_dataframes.ipynb b/notebooks/getting_started/getting_started_bq_dataframes.ipynb index a9b6aefe30a..b59ccbb8ac8 100644 --- a/notebooks/getting_started/getting_started_bq_dataframes.ipynb +++ b/notebooks/getting_started/getting_started_bq_dataframes.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "id": "ur8xi4C7S06n" }, @@ -145,11 +145,122 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "id": "mfPoOwPLGpSr" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: bigframes in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (0.25.0)\n", + "Requirement already satisfied: cloudpickle>=2.0.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (3.0.0)\n", + "Requirement already satisfied: fsspec>=2023.3.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2024.2.0)\n", + "Requirement already satisfied: gcsfs>=2023.3.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2024.2.0)\n", + "Requirement already satisfied: geopandas>=0.12.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (0.14.3)\n", + "Requirement already satisfied: google-auth<3.0dev,>=2.15.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2.28.2)\n", + "Requirement already satisfied: google-cloud-bigquery>=3.10.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (3.19.0)\n", + "Requirement already satisfied: google-cloud-functions>=1.12.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (1.16.3)\n", + "Requirement already satisfied: google-cloud-bigquery-connection>=1.12.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (1.15.3)\n", + "Requirement already satisfied: google-cloud-iam>=2.12.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2.14.3)\n", + "Requirement already satisfied: google-cloud-resource-manager>=1.10.3 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (1.12.3)\n", + "Requirement already satisfied: google-cloud-storage>=2.0.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2.15.0)\n", + "Requirement already satisfied: ibis-framework<9.0.0dev,>=8.0.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (8.0.0)\n", + "Requirement already satisfied: pandas<2.1.4,>=1.5.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2.1.3)\n", + "Requirement already satisfied: pydata-google-auth>=1.8.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (1.8.2)\n", + "Requirement already satisfied: requests>=2.27.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2.31.0)\n", + "Requirement already satisfied: scikit-learn>=1.2.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (1.4.1.post1)\n", + "Requirement already satisfied: sqlalchemy<3.0dev,>=1.4 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (2.0.28)\n", + "Requirement already satisfied: sqlglot<=20.11,>=20.8.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (20.11.0)\n", + "Requirement already satisfied: tabulate>=0.9 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (0.9.0)\n", + "Requirement already satisfied: ipywidgets>=7.7.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (8.1.2)\n", + "Requirement already satisfied: humanize>=4.6.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (4.9.0)\n", + "Requirement already satisfied: matplotlib>=3.7.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from bigframes) (3.8.3)\n", + "Requirement already satisfied: aiohttp!=4.0.0a0,!=4.0.0a1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from gcsfs>=2023.3.0->bigframes) (3.9.3)\n", + "Requirement already satisfied: decorator>4.1.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from gcsfs>=2023.3.0->bigframes) (5.1.1)\n", + "Requirement already satisfied: google-auth-oauthlib in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from gcsfs>=2023.3.0->bigframes) (1.2.0)\n", + "Requirement already satisfied: fiona>=1.8.21 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from geopandas>=0.12.2->bigframes) (1.9.6)\n", + "Requirement already satisfied: packaging in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from geopandas>=0.12.2->bigframes) (24.0)\n", + "Requirement already satisfied: pyproj>=3.3.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from geopandas>=0.12.2->bigframes) (3.6.1)\n", + "Requirement already satisfied: shapely>=1.8.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from geopandas>=0.12.2->bigframes) (2.0.3)\n", + "Requirement already satisfied: cachetools<6.0,>=2.0.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-auth<3.0dev,>=2.15.0->bigframes) (5.3.3)\n", + "Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-auth<3.0dev,>=2.15.0->bigframes) (0.3.0)\n", + "Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-auth<3.0dev,>=2.15.0->bigframes) (4.9)\n", + "Requirement already satisfied: google-api-core!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-api-core[grpc]!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1->google-cloud-bigquery>=3.10.0->google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (2.17.1)\n", + "Requirement already satisfied: google-cloud-core<3.0.0dev,>=1.6.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery>=3.10.0->google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (2.4.1)\n", + "Requirement already satisfied: google-resumable-media<3.0dev,>=0.6.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery>=3.10.0->google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (2.7.0)\n", + "Requirement already satisfied: python-dateutil<3.0dev,>=2.7.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery>=3.10.0->google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (2.9.0.post0)\n", + "Requirement already satisfied: proto-plus<2.0.0dev,>=1.22.3 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery-connection>=1.12.0->bigframes) (1.23.0)\n", + "Requirement already satisfied: protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev,>=3.19.5 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery-connection>=1.12.0->bigframes) (4.25.3)\n", + "Requirement already satisfied: grpc-google-iam-v1<1.0.0dev,>=0.12.4 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery-connection>=1.12.0->bigframes) (0.13.0)\n", + "Requirement already satisfied: google-cloud-bigquery-storage<3.0.0dev,>=2.6.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (2.24.0)\n", + "Requirement already satisfied: grpcio<2.0dev,>=1.47.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (1.62.1)\n", + "Requirement already satisfied: pyarrow>=3.0.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (15.0.1)\n", + "Requirement already satisfied: db-dtypes<2.0.0dev,>=0.3.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (1.2.0)\n", + "Requirement already satisfied: google-crc32c<2.0dev,>=1.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-cloud-storage>=2.0.0->bigframes) (1.5.0)\n", + "Requirement already satisfied: atpublic<5,>=2.3 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (4.0)\n", + "Requirement already satisfied: bidict<1,>=0.22.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (0.23.1)\n", + "Requirement already satisfied: multipledispatch<2,>=0.6 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (1.0.0)\n", + "Requirement already satisfied: numpy<2,>=1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (1.26.4)\n", + "Requirement already satisfied: parsy<3,>=2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (2.1)\n", + "Requirement already satisfied: pyarrow-hotfix<1,>=0.4 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (0.6)\n", + "Requirement already satisfied: pytz>=2022.7 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (2024.1)\n", + "Requirement already satisfied: rich<14,>=12.4.4 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (13.7.1)\n", + "Requirement already satisfied: toolz<1,>=0.11 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (0.12.1)\n", + "Requirement already satisfied: typing-extensions<5,>=4.3.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (4.10.0)\n", + "Requirement already satisfied: comm>=0.1.3 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipywidgets>=7.7.1->bigframes) (0.2.2)\n", + "Requirement already satisfied: ipython>=6.1.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipywidgets>=7.7.1->bigframes) (8.22.2)\n", + "Requirement already satisfied: traitlets>=4.3.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipywidgets>=7.7.1->bigframes) (5.14.2)\n", + "Requirement already satisfied: widgetsnbextension~=4.0.10 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipywidgets>=7.7.1->bigframes) (4.0.10)\n", + "Requirement already satisfied: jupyterlab-widgets~=3.0.10 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipywidgets>=7.7.1->bigframes) (3.0.10)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from matplotlib>=3.7.1->bigframes) (1.2.0)\n", + "Requirement already satisfied: cycler>=0.10 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from matplotlib>=3.7.1->bigframes) (0.12.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from matplotlib>=3.7.1->bigframes) (4.49.0)\n", + "Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from matplotlib>=3.7.1->bigframes) (1.4.5)\n", + "Requirement already satisfied: pillow>=8 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from matplotlib>=3.7.1->bigframes) (10.2.0)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from matplotlib>=3.7.1->bigframes) (3.1.2)\n", + "Requirement already satisfied: tzdata>=2022.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from pandas<2.1.4,>=1.5.0->bigframes) (2024.1)\n", + "Requirement already satisfied: setuptools in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from pydata-google-auth>=1.8.2->bigframes) (69.2.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from requests>=2.27.1->bigframes) (3.3.2)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from requests>=2.27.1->bigframes) (3.6)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from requests>=2.27.1->bigframes) (2.2.1)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from requests>=2.27.1->bigframes) (2024.2.2)\n", + "Requirement already satisfied: scipy>=1.6.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from scikit-learn>=1.2.2->bigframes) (1.12.0)\n", + "Requirement already satisfied: joblib>=1.2.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from scikit-learn>=1.2.2->bigframes) (1.3.2)\n", + "Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from scikit-learn>=1.2.2->bigframes) (3.3.0)\n", + "Requirement already satisfied: greenlet!=0.4.17 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from sqlalchemy<3.0dev,>=1.4->bigframes) (3.0.3)\n", + "Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->gcsfs>=2023.3.0->bigframes) (1.3.1)\n", + "Requirement already satisfied: attrs>=17.3.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->gcsfs>=2023.3.0->bigframes) (23.2.0)\n", + "Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->gcsfs>=2023.3.0->bigframes) (1.4.1)\n", + "Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->gcsfs>=2023.3.0->bigframes) (6.0.5)\n", + "Requirement already satisfied: yarl<2.0,>=1.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->gcsfs>=2023.3.0->bigframes) (1.9.4)\n", + "Requirement already satisfied: click~=8.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas>=0.12.2->bigframes) (8.1.7)\n", + "Requirement already satisfied: click-plugins>=1.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas>=0.12.2->bigframes) (1.1.1)\n", + "Requirement already satisfied: cligj>=0.5 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas>=0.12.2->bigframes) (0.7.2)\n", + "Requirement already satisfied: six in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas>=0.12.2->bigframes) (1.16.0)\n", + "Requirement already satisfied: googleapis-common-protos<2.0.dev0,>=1.56.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-api-core!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1->google-api-core[grpc]!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1->google-cloud-bigquery>=3.10.0->google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (1.63.0)\n", + "Requirement already satisfied: grpcio-status<2.0.dev0,>=1.33.2 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-api-core[grpc]!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1->google-cloud-bigquery>=3.10.0->google-cloud-bigquery[bqstorage,pandas]>=3.10.0->bigframes) (1.62.1)\n", + "Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from google-auth-oauthlib->gcsfs>=2023.3.0->bigframes) (1.4.0)\n", + "Requirement already satisfied: jedi>=0.16 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (0.19.1)\n", + "Requirement already satisfied: matplotlib-inline in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (0.1.6)\n", + "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (3.0.43)\n", + "Requirement already satisfied: pygments>=2.4.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (2.17.2)\n", + "Requirement already satisfied: stack-data in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (0.6.3)\n", + "Requirement already satisfied: pexpect>4.3 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (4.9.0)\n", + "Requirement already satisfied: pyasn1<0.6.0,>=0.4.6 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from pyasn1-modules>=0.2.1->google-auth<3.0dev,>=2.15.0->bigframes) (0.5.1)\n", + "Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from rich<14,>=12.4.4->ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (3.0.0)\n", + "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (0.8.3)\n", + "Requirement already satisfied: mdurl~=0.1 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from markdown-it-py>=2.2.0->rich<14,>=12.4.4->ibis-framework<9.0.0dev,>=8.0.0->ibis-framework[bigquery]<9.0.0dev,>=8.0.0->bigframes) (0.1.2)\n", + "Requirement already satisfied: ptyprocess>=0.5 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (0.7.0)\n", + "Requirement already satisfied: wcwidth in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (0.2.13)\n", + "Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib->gcsfs>=2023.3.0->bigframes) (3.2.2)\n", + "Requirement already satisfied: executing>=1.2.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (2.0.1)\n", + "Requirement already satisfied: asttokens>=2.1.0 in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (2.4.1)\n", + "Requirement already satisfied: pure-eval in /usr/local/google/home/swast/envs/bigframes/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets>=7.7.1->bigframes) (0.2.2)\n" + ] + } + ], "source": [ "!pip install bigframes" ] @@ -167,7 +278,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "id": "f200f10a1da3" }, @@ -234,11 +345,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "id": "oM1iC_MfAts1" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Updated property [core/project].\n", + "\n", + "\n", + "To take a quick anonymous survey, run:\n", + " $ gcloud survey\n", + "\n" + ] + } + ], "source": [ "PROJECT_ID = \"\" # @param {type:\"string\"}\n", "\n", @@ -259,7 +383,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "id": "eF-Twtc4XGem" }, @@ -303,7 +427,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "id": "254614fa0c46" }, @@ -325,7 +449,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "id": "603adbbf0532" }, @@ -346,13 +470,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "id": "PyQmSRbKA8r-" }, "outputs": [], "source": [ - "import bigframes.pandas as bf" + "import bigframes.pandas as bpd" ] }, { @@ -367,14 +491,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "id": "NPPMuw2PXGeo" }, "outputs": [], "source": [ - "bf.options.bigquery.project = PROJECT_ID\n", - "bf.options.bigquery.location = REGION" + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = REGION" ] }, { @@ -383,7 +514,7 @@ "id": "pDfrKwMKE_dK" }, "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." ] }, { @@ -421,13 +552,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "id": "Vyex9BQI-BNa" }, "outputs": [], "source": [ - "# bq_df_sample = bf.read_gbq(\"bigquery-samples.wikipedia_pageviews.200809h\")" + "# bq_df_sample = bpd.read_gbq(\"bigquery-samples.wikipedia_pageviews.200809h\")" ] }, { @@ -452,7 +583,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "id": "XfGq5apK-D_e" }, @@ -526,7 +657,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "id": "SvyXzkRl783u" }, @@ -552,7 +683,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "id": "3QHQYlnoBLpt" }, @@ -578,15 +709,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": { "id": "EDAaIwHpQCDZ" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Load job d578c399-e2e5-4f6b-ba28-59d0686a91e7 is DONE. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# If order is not important, use the \"bigquery\" engine to\n", "# allow BigQuery DataFrames to read directly from GCS.\n", - "df_from_local = bf.read_csv(fn, engine=\"bigquery\")" + "df_from_local = bpd.read_csv(fn, engine=\"bigquery\")" ] }, { @@ -600,11 +744,156 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": { "id": "_gPD0Zn1Stdb" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job f50a129b-4a51-4c21-b155-ab1e85c1403e is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job b0d65008-f9f1-4fec-8620-42f307390049 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a83d72e8-0cb8-44e9-ad0b-6fe3726ed1e9 is DONE. 501 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Gentoo penguin (Pygoscelis papua)Biscoe50.515.92255400MALE
1Gentoo penguin (Pygoscelis papua)Biscoe45.114.52155000FEMALE
2Adelie Penguin (Pygoscelis adeliae)Torgersen41.418.52023875MALE
3Adelie Penguin (Pygoscelis adeliae)Torgersen38.617.01882900FEMALE
4Gentoo penguin (Pygoscelis papua)Biscoe46.514.82175200FEMALE
\n", + "

5 rows × 7 columns

\n", + "
[5 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "0 Gentoo penguin (Pygoscelis papua) Biscoe 50.5 \n", + "1 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Torgersen 41.4 \n", + "3 Adelie Penguin (Pygoscelis adeliae) Torgersen 38.6 \n", + "4 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 15.9 225 5400 MALE \n", + "1 14.5 215 5000 FEMALE \n", + "2 18.5 202 3875 MALE \n", + "3 17.0 188 2900 FEMALE \n", + "4 14.8 217 5200 FEMALE \n", + "\n", + "[5 rows x 7 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df_from_local.head()" ] @@ -631,11 +920,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": { "id": "ZSP7gt13QrQt" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset birds created.\n" + ] + } + ], "source": [ "DATASET_ID = \"birds\" # @param {type:\"string\"}\n", "\n", @@ -658,11 +955,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": { "id": "oP1NIAmUBjop" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 49702108-948c-4a60-a66e-16a3ed6bc102 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'swast-scratch.birds.penguins'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df_from_local.to_gbq(\n", " PROJECT_ID + \".\" + DATASET_ID + \".penguins\",\n", @@ -691,14 +1011,159 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { "id": "IBuo-d6dWfsA" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 05a6288d-3774-41d0-9884-6bbb5af28942 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 45383ce0-0ca1-4c16-9832-739e9d325673 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2f672140-ddc6-43b6-b79a-318f29bb9239 is DONE. 501 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Gentoo penguin (Pygoscelis papua)Biscoe50.515.92255400MALE
1Gentoo penguin (Pygoscelis papua)Biscoe45.114.52155000FEMALE
2Adelie Penguin (Pygoscelis adeliae)Torgersen41.418.52023875MALE
3Adelie Penguin (Pygoscelis adeliae)Torgersen38.617.01882900FEMALE
4Gentoo penguin (Pygoscelis papua)Biscoe46.514.82175200FEMALE
\n", + "

5 rows × 7 columns

\n", + "
[5 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "0 Gentoo penguin (Pygoscelis papua) Biscoe 50.5 \n", + "1 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Torgersen 41.4 \n", + "3 Adelie Penguin (Pygoscelis adeliae) Torgersen 38.6 \n", + "4 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 15.9 225 5400 MALE \n", + "1 14.5 215 5000 FEMALE \n", + "2 18.5 202 3875 MALE \n", + "3 17.0 188 2900 FEMALE \n", + "4 14.8 217 5200 FEMALE \n", + "\n", + "[5 rows x 7 columns]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "query_or_table = f\"\"\"{PROJECT_ID}.{DATASET_ID}.penguins\"\"\"\n", - "bq_df = bf.read_gbq(query_or_table)\n", + "bq_df = bpd.read_gbq(query_or_table)\n", "bq_df.head()" ] }, @@ -733,11 +1198,68 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": { "id": "6i6HkFJZa8na" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 5c454fa1-a01b-4e95-b947-6f02554a8461 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2cffe5c7-c0c6-4495-ad67-1f5fb55654fd is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 96b4dead-f526-4be3-b24d-5d7aec99eeeb is DONE. 240 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 5400\n", + "1 5000\n", + "2 3875\n", + "3 2900\n", + "4 5200\n", + "5 3725\n", + "6 2975\n", + "7 4150\n", + "8 5300\n", + "9 4150\n", + "Name: body_mass_g, dtype: Int64" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "bq_df[\"body_mass_g\"].head(10)" ] @@ -753,11 +1275,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "id": "YKwCW7Nsavap" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 635d000c-14ca-4ecf-bc32-1527821cba28 is DONE. 2.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "average_body_mass: 4201.754385964917\n" + ] + } + ], "source": [ "average_body_mass = bq_df[\"body_mass_g\"].mean()\n", "print(f\"average_body_mass: {average_body_mass}\")" @@ -774,11 +1316,108 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": { "id": "4PyKMR61-Mjy" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job d22d8e48-26a0-4cfb-83fc-3e52b834f487 is DONE. 15.6 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 64fff5f3-7106-4003-9241-a9b09afed781 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job c3d566cc-bed1-4361-96ef-f06956982916 is DONE. 163 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
body_mass_g
species
Adelie Penguin (Pygoscelis adeliae)3700.662252
Chinstrap penguin (Pygoscelis antarctica)3733.088235
Gentoo penguin (Pygoscelis papua)5076.01626
\n", + "

3 rows × 1 columns

\n", + "
[3 rows x 1 columns in total]" + ], + "text/plain": [ + " body_mass_g\n", + "species \n", + "Adelie Penguin (Pygoscelis adeliae) 3700.662252\n", + "Chinstrap penguin (Pygoscelis antarctica) 3733.088235\n", + "Gentoo penguin (Pygoscelis papua) 5076.01626\n", + "\n", + "[3 rows x 1 columns]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "bq_df[[\"species\", \"body_mass_g\"]].groupby(by=bq_df[\"species\"]).mean(numeric_only=True).head()" ] @@ -820,13 +1459,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": { "id": "rSWTOG-vb2Fc" }, "outputs": [], "source": [ - "@bf.remote_function([float], str)\n", + "@bpd.remote_function([float], str)\n", "def get_bucket(num):\n", " if not num: return \"NA\"\n", " boundary = 4000\n", @@ -846,11 +1485,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "id": "6ejPXoyEQpWE" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cloud Function Name projects/swast-scratch/locations/us-central1/functions/bigframes-71a76285da23f28be467ed16826f7276\n", + "Remote Function Name swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bigframes_71a76285da23f28be467ed16826f7276\n" + ] + } + ], "source": [ "CLOUD_FUNCTION_NAME = format(get_bucket.bigframes_cloud_function)\n", "print(\"Cloud Function Name \" + CLOUD_FUNCTION_NAME)\n", @@ -869,11 +1517,161 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": { "id": "NxSd9WZFcIji" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 9925acd1-d1e7-4746-90d6-4ce8c2ca30a8 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2f10b5cd-80bb-4697-9c61-b7848ce15c81 is DONE. 39.6 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 29266b33-3945-44c0-943b-3d6365b9cc7a is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 19ecf156-8940-4c02-b20e-3e52e18c7239 is DONE. 396 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
body_mass_gbody_mass_bucket
05400at_or_above_4000
15000at_or_above_4000
23875below_4000
32900below_4000
45200at_or_above_4000
53725below_4000
62975below_4000
74150at_or_above_4000
85300at_or_above_4000
94150at_or_above_4000
\n", + "

10 rows × 2 columns

\n", + "
[10 rows x 2 columns in total]" + ], + "text/plain": [ + " body_mass_g body_mass_bucket\n", + "0 5400 at_or_above_4000\n", + "1 5000 at_or_above_4000\n", + "2 3875 below_4000\n", + "3 2900 below_4000\n", + "4 5200 at_or_above_4000\n", + "5 3725 below_4000\n", + "6 2975 below_4000\n", + "7 4150 at_or_above_4000\n", + "8 5300 at_or_above_4000\n", + "9 4150 at_or_above_4000\n", + "\n", + "[10 rows x 2 columns]" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "bq_df = bq_df.assign(body_mass_bucket=bq_df['body_mass_g'].apply(get_bucket))\n", "bq_df[['body_mass_g', 'body_mass_bucket']].head(10)" @@ -908,7 +1706,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": { "id": "sx_vKniMq9ZX" }, @@ -925,7 +1723,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": { "id": "_dTCXvCxtPw9" }, @@ -941,7 +1739,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": { "id": "EDAIIfcpwNOF" }, @@ -953,7 +1751,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": { "id": "QwumLUKmVpuH" }, @@ -973,6 +1771,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.11.1" } }, "nbformat": 4, diff --git a/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb b/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb index 089c167d397..b3c965aded5 100644 --- a/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb +++ b/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "id": "ur8xi4C7S06n" }, @@ -156,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "id": "mfPoOwPLGpSr" }, @@ -211,11 +211,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "id": "oM1iC_MfAts1" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Updated property [core/project].\n" + ] + } + ], "source": [ "PROJECT_ID = \"\" # @param {type:\"string\"}\n", "\n", @@ -236,7 +244,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "id": "eF-Twtc4XGem" }, @@ -258,7 +266,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "id": "BbMh9JHvUHAn" }, @@ -309,7 +317,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "id": "254614fa0c46" }, @@ -331,7 +339,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "id": "603adbbf0532" }, @@ -352,13 +360,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "id": "PyQmSRbKA8r-" }, "outputs": [], "source": [ - "import bigframes.pandas as bf" + "import bigframes.pandas as bpd" ] }, { @@ -373,14 +381,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "id": "NPPMuw2PXGeo" }, "outputs": [], "source": [ - "bf.options.bigquery.project = PROJECT_ID\n", - "bf.options.bigquery.location = REGION" + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = REGION" ] }, { @@ -389,7 +404,7 @@ "id": "pDfrKwMKE_dK" }, "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.reset_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.reset_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." ] }, { @@ -405,13 +420,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "id": "d86W4hNqzZJb" }, "outputs": [], "source": [ - "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", + "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", "df = df.dropna()\n", "\n", "# BigQuery DataFrames creates a default numbered index, which we can give a name\n", @@ -429,11 +444,168 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "id": "arGaUZVWkSwT" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job d3acda60-1059-4bb0-9912-ed374491c5c3 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 51c6aa1c-ff98-4805-921e-00830e125e56 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 01e2cb6d-604b-4cdd-afb0-8f515a9da951 is DONE. 501 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
penguin_id
0Gentoo penguin (Pygoscelis papua)Biscoe50.515.9225.05400.0MALE
1Gentoo penguin (Pygoscelis papua)Biscoe45.114.5215.05000.0FEMALE
2Adelie Penguin (Pygoscelis adeliae)Torgersen41.418.5202.03875.0MALE
3Adelie Penguin (Pygoscelis adeliae)Torgersen38.617.0188.02900.0FEMALE
4Gentoo penguin (Pygoscelis papua)Biscoe46.514.8217.05200.0FEMALE
\n", + "

5 rows × 7 columns

\n", + "
[5 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "penguin_id \n", + "0 Gentoo penguin (Pygoscelis papua) Biscoe 50.5 \n", + "1 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Torgersen 41.4 \n", + "3 Adelie Penguin (Pygoscelis adeliae) Torgersen 38.6 \n", + "4 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "penguin_id \n", + "0 15.9 225.0 5400.0 MALE \n", + "1 14.5 215.0 5000.0 FEMALE \n", + "2 18.5 202.0 3875.0 MALE \n", + "3 17.0 188.0 2900.0 FEMALE \n", + "4 14.8 217.0 5200.0 FEMALE \n", + "\n", + "[5 rows x 7 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df.head()" ] @@ -458,7 +630,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "id": "B9mW93o9z_-L" }, @@ -482,11 +654,82 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "id": "NysWAWmvlAxB" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 7bd14e04-b3b4-4281-b5be-187f7baad62f is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 240cc7db-19ac-4bd3-8e76-a79f75ded077 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 91194fee-d9b9-4cb9-a469-e49e9d77c624 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 84c71647-956b-4385-8dce-c8bc70a917c8 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 9c94600b-2231-4d04-8e3a-fb46f8892b6a is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "X_train shape: (267, 6)\n", + "X_test shape: (67, 6)\n", + "y_train shape: (267, 1)\n", + "y_test shape: (67, 1)\n" + ] + } + ], "source": [ "from bigframes.ml.model_selection import train_test_split\n", "\n", @@ -514,11 +757,161 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": { "id": "f8bz1HwLlyLP" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 8ad534c1-eb49-4616-b7a6-f7d8b044b8bf is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 3793de66-fb3c-4ca4-a337-aa708c718cc5 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 66524afb-4509-4927-8902-4a72826e83c4 is DONE. 456 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmsexspecies
penguin_id
188Dream51.518.7187.0MALEChinstrap penguin (Pygoscelis antarctica)
251Biscoe49.516.1224.0MALEGentoo penguin (Pygoscelis papua)
231Biscoe45.713.9214.0FEMALEGentoo penguin (Pygoscelis papua)
271Biscoe59.617.0230.0MALEGentoo penguin (Pygoscelis papua)
128Biscoe38.817.2180.0MALEAdelie Penguin (Pygoscelis adeliae)
\n", + "

5 rows × 6 columns

\n", + "
[5 rows x 6 columns in total]" + ], + "text/plain": [ + " island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "penguin_id \n", + "188 Dream 51.5 18.7 187.0 \n", + "251 Biscoe 49.5 16.1 224.0 \n", + "231 Biscoe 45.7 13.9 214.0 \n", + "271 Biscoe 59.6 17.0 230.0 \n", + "128 Biscoe 38.8 17.2 180.0 \n", + "\n", + " sex species \n", + "penguin_id \n", + "188 MALE Chinstrap penguin (Pygoscelis antarctica) \n", + "251 MALE Gentoo penguin (Pygoscelis papua) \n", + "231 FEMALE Gentoo penguin (Pygoscelis papua) \n", + "271 MALE Gentoo penguin (Pygoscelis papua) \n", + "128 MALE Adelie Penguin (Pygoscelis adeliae) \n", + "\n", + "[5 rows x 6 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "X_test.head(5)" ] @@ -534,11 +927,118 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": { "id": "PflbhKGkl8v2" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 6a87fcc2-f2d0-44f5-8ab2-08f109c2b70d is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ed8e49f8-0f4c-4ef2-bbc2-b8c5ef9fd064 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 97fea642-03aa-49fd-943e-f4efa5a87f0f is DONE. 120 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
body_mass_g
penguin_id
1883250.0
2515650.0
2314400.0
2716050.0
1283800.0
\n", + "

5 rows × 1 columns

\n", + "
[5 rows x 1 columns in total]" + ], + "text/plain": [ + " body_mass_g\n", + "penguin_id \n", + "188 3250.0\n", + "251 5650.0\n", + "231 4400.0\n", + "271 6050.0\n", + "128 3800.0\n", + "\n", + "[5 rows x 1 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "y_test.head(5)" ] @@ -579,11 +1079,337 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": { "id": "yhATDMR-mkdF" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job aee64759-42bb-44d6-b8c7-1c737cdd6eed is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job acb29d04-a20d-4f1c-8d90-51c7e8ac9922 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2bd034db-7d9b-467c-be17-49bca094cceb is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5dfb583a-1ced-4f2a-94b9-f1282263134d is DONE. 2.1 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8fe87288-4a95-49f4-9895-7c41c1004901 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7ebcecee-beff-402d-ac71-6384014a54da is DONE. 8.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
standard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mm
penguin_id
01.20778-0.6515311.772656
2-0.4556020.6628550.100476
3-0.967412-0.095445-0.917372
40.476623-1.2076171.191028
5-1.6254540.359535-0.626559
7-0.345929-1.864810.682104
80.842202-1.5614911.409139
90.3486710.865068-0.263041
100.9335961.2189410.827511
11-1.460943-0.297658-0.771966
121.317454-0.4493181.409139
13-0.236255-1.7637040.900214
140.549739-0.297658-0.626559
160.970154-1.0054041.481842
17-1.058807-0.348211-0.190338
181.354012-1.5109371.263732
19-0.053466-1.6625971.191028
20-0.199697-1.5109370.609401
211.1529430.763962-0.190338
22-1.2050380.308982-0.699262
24-0.7846231.775028-0.699262
25-0.839461.724474-0.771966
26-0.6201130.359535-0.990076
270.330392-0.095445-0.408448
292.194842-0.0954451.990767
\n", + "

25 rows × 3 columns

\n", + "
[267 rows x 3 columns in total]" + ], + "text/plain": [ + " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "0 1.20778 -0.651531 \n", + "2 -0.455602 0.662855 \n", + "3 -0.967412 -0.095445 \n", + "4 0.476623 -1.207617 \n", + "5 -1.625454 0.359535 \n", + "7 -0.345929 -1.86481 \n", + "8 0.842202 -1.561491 \n", + "9 0.348671 0.865068 \n", + "10 0.933596 1.218941 \n", + "11 -1.460943 -0.297658 \n", + "12 1.317454 -0.449318 \n", + "13 -0.236255 -1.763704 \n", + "14 0.549739 -0.297658 \n", + "16 0.970154 -1.005404 \n", + "17 -1.058807 -0.348211 \n", + "18 1.354012 -1.510937 \n", + "19 -0.053466 -1.662597 \n", + "20 -0.199697 -1.510937 \n", + "21 1.152943 0.763962 \n", + "22 -1.205038 0.308982 \n", + "24 -0.784623 1.775028 \n", + "25 -0.83946 1.724474 \n", + "26 -0.620113 0.359535 \n", + "27 0.330392 -0.095445 \n", + "29 2.194842 -0.095445 \n", + "\n", + " standard_scaled_flipper_length_mm \n", + "penguin_id \n", + "0 1.772656 \n", + "2 0.100476 \n", + "3 -0.917372 \n", + "4 1.191028 \n", + "5 -0.626559 \n", + "7 0.682104 \n", + "8 1.409139 \n", + "9 -0.263041 \n", + "10 0.827511 \n", + "11 -0.771966 \n", + "12 1.409139 \n", + "13 0.900214 \n", + "14 -0.626559 \n", + "16 1.481842 \n", + "17 -0.190338 \n", + "18 1.263732 \n", + "19 1.191028 \n", + "20 0.609401 \n", + "21 -0.190338 \n", + "22 -0.699262 \n", + "24 -0.699262 \n", + "25 -0.771966 \n", + "26 -0.990076 \n", + "27 -0.408448 \n", + "29 1.990767 \n", + "...\n", + "\n", + "[267 rows x 3 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from bigframes.ml.preprocessing import StandardScaler\n", "\n", @@ -609,11 +1435,313 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": { "id": "TfwSLOTXmspI" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 6639e06d-3920-4c64-84d8-b40ce042188c is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 579dfb14-6d39-44c0-9b92-eb6a40c46df8 is DONE. 536 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7f613d94-a68c-42d5-8afe-0413b32de3a0 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 140e8b5f-a24b-43a3-831f-30a29a4bd7ea is DONE. 2.1 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
standard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mm
penguin_id
10.220718-1.3592771.045621
15-0.5104390.157322-0.771966
28-1.0588070.713408-0.771966
321.4636851.1683880.39129
33-0.2545340.056215-0.990076
34-0.5104390.4606420.318587
371.3540120.511195-0.263041
41-0.674949-0.095445-1.789814
47-1.1684810.662855-0.117634
520.4583440.308982-0.699262
56-1.0405280.460642-1.135483
57-0.9674120.005662-0.117634
620.988433-0.7526381.191028
651.7561481.3706010.318587
670.677691-1.3592771.045621
75-1.1136441.421155-0.771966
810.6776910.561748-0.408448
89-0.8577390.713408-0.771966
92-0.8029020.308982-0.917372
93-0.3093711.168388-0.263041
96-0.3093710.662855-1.499
100-0.9125760.814515-0.771966
1010.549739-1.3087241.554546
102-0.1265820.662855-0.626559
1071.20778-1.0054041.118325
\n", + "

25 rows × 3 columns

\n", + "
[67 rows x 3 columns in total]" + ], + "text/plain": [ + " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "1 0.220718 -1.359277 \n", + "15 -0.510439 0.157322 \n", + "28 -1.058807 0.713408 \n", + "32 1.463685 1.168388 \n", + "33 -0.254534 0.056215 \n", + "34 -0.510439 0.460642 \n", + "37 1.354012 0.511195 \n", + "41 -0.674949 -0.095445 \n", + "47 -1.168481 0.662855 \n", + "52 0.458344 0.308982 \n", + "56 -1.040528 0.460642 \n", + "57 -0.967412 0.005662 \n", + "62 0.988433 -0.752638 \n", + "65 1.756148 1.370601 \n", + "67 0.677691 -1.359277 \n", + "75 -1.113644 1.421155 \n", + "81 0.677691 0.561748 \n", + "89 -0.857739 0.713408 \n", + "92 -0.802902 0.308982 \n", + "93 -0.309371 1.168388 \n", + "96 -0.309371 0.662855 \n", + "100 -0.912576 0.814515 \n", + "101 0.549739 -1.308724 \n", + "102 -0.126582 0.662855 \n", + "107 1.20778 -1.005404 \n", + "\n", + " standard_scaled_flipper_length_mm \n", + "penguin_id \n", + "1 1.045621 \n", + "15 -0.771966 \n", + "28 -0.771966 \n", + "32 0.39129 \n", + "33 -0.990076 \n", + "34 0.318587 \n", + "37 -0.263041 \n", + "41 -1.789814 \n", + "47 -0.117634 \n", + "52 -0.699262 \n", + "56 -1.135483 \n", + "57 -0.117634 \n", + "62 1.191028 \n", + "65 0.318587 \n", + "67 1.045621 \n", + "75 -0.771966 \n", + "81 -0.408448 \n", + "89 -0.771966 \n", + "92 -0.917372 \n", + "93 -0.263041 \n", + "96 -1.499 \n", + "100 -0.771966 \n", + "101 1.554546 \n", + "102 -0.626559 \n", + "107 1.118325 \n", + "...\n", + "\n", + "[67 rows x 3 columns]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "scaler.transform(X_test[numeric_columns])" ] @@ -633,11 +1761,498 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { "id": "I8Wwx3emmz2J" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job c16fdb5d-3f18-4f85-8a31-705ef4680be5 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8c94a7c1-7f12-44be-b389-7c854ceead4b is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 1287628d-1380-4495-a5e9-6806440206bc is DONE. 22.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 03163e1a-c789-4046-b71a-b4b4e7bbc043 is DONE. 2.1 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 86f39b30-00db-4ada-8699-0fe49c94eb2d is DONE. 29.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d5b0e8b0-12cd-47f6-85d2-806b2c252d37 is DONE. 536 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 459cdc90-d1f3-4580-9137-9b93d44ca991 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 80d10913-7263-44e6-89f7-719eac4158a3 is DONE. 21.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
onehotencoded_islandstandard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mmonehotencoded_sexonehotencoded_species
penguin_id
0[{'index': 1, 'value': 1.0}]1.20778-0.6515311.772656[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
2[{'index': 3, 'value': 1.0}]-0.4556020.6628550.100476[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
3[{'index': 3, 'value': 1.0}]-0.967412-0.095445-0.917372[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
4[{'index': 1, 'value': 1.0}]0.476623-1.2076171.191028[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
5[{'index': 1, 'value': 1.0}]-1.6254540.359535-0.626559[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
7[{'index': 1, 'value': 1.0}]-0.345929-1.864810.682104[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
8[{'index': 1, 'value': 1.0}]0.842202-1.5614911.409139[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
9[{'index': 3, 'value': 1.0}]0.3486710.865068-0.263041[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
10[{'index': 2, 'value': 1.0}]0.9335961.2189410.827511[{'index': 3, 'value': 1.0}][{'index': 2, 'value': 1.0}]
11[{'index': 3, 'value': 1.0}]-1.460943-0.297658-0.771966[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
12[{'index': 1, 'value': 1.0}]1.317454-0.4493181.409139[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
13[{'index': 1, 'value': 1.0}]-0.236255-1.7637040.900214[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
14[{'index': 2, 'value': 1.0}]0.549739-0.297658-0.626559[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
16[{'index': 1, 'value': 1.0}]0.970154-1.0054041.481842[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
17[{'index': 1, 'value': 1.0}]-1.058807-0.348211-0.190338[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
18[{'index': 1, 'value': 1.0}]1.354012-1.5109371.263732[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
19[{'index': 1, 'value': 1.0}]-0.053466-1.6625971.191028[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
20[{'index': 1, 'value': 1.0}]-0.199697-1.5109370.609401[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
21[{'index': 2, 'value': 1.0}]1.1529430.763962-0.190338[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
22[{'index': 2, 'value': 1.0}]-1.2050380.308982-0.699262[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
24[{'index': 1, 'value': 1.0}]-0.7846231.775028-0.699262[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
25[{'index': 3, 'value': 1.0}]-0.839461.724474-0.771966[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
26[{'index': 1, 'value': 1.0}]-0.6201130.359535-0.990076[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
27[{'index': 2, 'value': 1.0}]0.330392-0.095445-0.408448[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
29[{'index': 1, 'value': 1.0}]2.194842-0.0954451.990767[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
\n", + "

25 rows × 6 columns

\n", + "
[267 rows x 6 columns in total]" + ], + "text/plain": [ + " onehotencoded_island standard_scaled_culmen_length_mm \\\n", + "penguin_id \n", + "0 [{'index': 1, 'value': 1.0}] 1.20778 \n", + "2 [{'index': 3, 'value': 1.0}] -0.455602 \n", + "3 [{'index': 3, 'value': 1.0}] -0.967412 \n", + "4 [{'index': 1, 'value': 1.0}] 0.476623 \n", + "5 [{'index': 1, 'value': 1.0}] -1.625454 \n", + "7 [{'index': 1, 'value': 1.0}] -0.345929 \n", + "8 [{'index': 1, 'value': 1.0}] 0.842202 \n", + "9 [{'index': 3, 'value': 1.0}] 0.348671 \n", + "10 [{'index': 2, 'value': 1.0}] 0.933596 \n", + "11 [{'index': 3, 'value': 1.0}] -1.460943 \n", + "12 [{'index': 1, 'value': 1.0}] 1.317454 \n", + "13 [{'index': 1, 'value': 1.0}] -0.236255 \n", + "14 [{'index': 2, 'value': 1.0}] 0.549739 \n", + "16 [{'index': 1, 'value': 1.0}] 0.970154 \n", + "17 [{'index': 1, 'value': 1.0}] -1.058807 \n", + "18 [{'index': 1, 'value': 1.0}] 1.354012 \n", + "19 [{'index': 1, 'value': 1.0}] -0.053466 \n", + "20 [{'index': 1, 'value': 1.0}] -0.199697 \n", + "21 [{'index': 2, 'value': 1.0}] 1.152943 \n", + "22 [{'index': 2, 'value': 1.0}] -1.205038 \n", + "24 [{'index': 1, 'value': 1.0}] -0.784623 \n", + "25 [{'index': 3, 'value': 1.0}] -0.83946 \n", + "26 [{'index': 1, 'value': 1.0}] -0.620113 \n", + "27 [{'index': 2, 'value': 1.0}] 0.330392 \n", + "29 [{'index': 1, 'value': 1.0}] 2.194842 \n", + "\n", + " standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "0 -0.651531 \n", + "2 0.662855 \n", + "3 -0.095445 \n", + "4 -1.207617 \n", + "5 0.359535 \n", + "7 -1.86481 \n", + "8 -1.561491 \n", + "9 0.865068 \n", + "10 1.218941 \n", + "11 -0.297658 \n", + "12 -0.449318 \n", + "13 -1.763704 \n", + "14 -0.297658 \n", + "16 -1.005404 \n", + "17 -0.348211 \n", + "18 -1.510937 \n", + "19 -1.662597 \n", + "20 -1.510937 \n", + "21 0.763962 \n", + "22 0.308982 \n", + "24 1.775028 \n", + "25 1.724474 \n", + "26 0.359535 \n", + "27 -0.095445 \n", + "29 -0.095445 \n", + "\n", + " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", + "penguin_id \n", + "0 1.772656 [{'index': 3, 'value': 1.0}] \n", + "2 0.100476 [{'index': 3, 'value': 1.0}] \n", + "3 -0.917372 [{'index': 2, 'value': 1.0}] \n", + "4 1.191028 [{'index': 2, 'value': 1.0}] \n", + "5 -0.626559 [{'index': 2, 'value': 1.0}] \n", + "7 0.682104 [{'index': 2, 'value': 1.0}] \n", + "8 1.409139 [{'index': 3, 'value': 1.0}] \n", + "9 -0.263041 [{'index': 3, 'value': 1.0}] \n", + "10 0.827511 [{'index': 3, 'value': 1.0}] \n", + "11 -0.771966 [{'index': 2, 'value': 1.0}] \n", + "12 1.409139 [{'index': 3, 'value': 1.0}] \n", + "13 0.900214 [{'index': 2, 'value': 1.0}] \n", + "14 -0.626559 [{'index': 2, 'value': 1.0}] \n", + "16 1.481842 [{'index': 3, 'value': 1.0}] \n", + "17 -0.190338 [{'index': 2, 'value': 1.0}] \n", + "18 1.263732 [{'index': 3, 'value': 1.0}] \n", + "19 1.191028 [{'index': 2, 'value': 1.0}] \n", + "20 0.609401 [{'index': 2, 'value': 1.0}] \n", + "21 -0.190338 [{'index': 2, 'value': 1.0}] \n", + "22 -0.699262 [{'index': 2, 'value': 1.0}] \n", + "24 -0.699262 [{'index': 2, 'value': 1.0}] \n", + "25 -0.771966 [{'index': 3, 'value': 1.0}] \n", + "26 -0.990076 [{'index': 2, 'value': 1.0}] \n", + "27 -0.408448 [{'index': 2, 'value': 1.0}] \n", + "29 1.990767 [{'index': 3, 'value': 1.0}] \n", + "\n", + " onehotencoded_species \n", + "penguin_id \n", + "0 [{'index': 3, 'value': 1.0}] \n", + "2 [{'index': 1, 'value': 1.0}] \n", + "3 [{'index': 1, 'value': 1.0}] \n", + "4 [{'index': 3, 'value': 1.0}] \n", + "5 [{'index': 1, 'value': 1.0}] \n", + "7 [{'index': 3, 'value': 1.0}] \n", + "8 [{'index': 3, 'value': 1.0}] \n", + "9 [{'index': 1, 'value': 1.0}] \n", + "10 [{'index': 2, 'value': 1.0}] \n", + "11 [{'index': 1, 'value': 1.0}] \n", + "12 [{'index': 3, 'value': 1.0}] \n", + "13 [{'index': 3, 'value': 1.0}] \n", + "14 [{'index': 2, 'value': 1.0}] \n", + "16 [{'index': 3, 'value': 1.0}] \n", + "17 [{'index': 1, 'value': 1.0}] \n", + "18 [{'index': 3, 'value': 1.0}] \n", + "19 [{'index': 3, 'value': 1.0}] \n", + "20 [{'index': 3, 'value': 1.0}] \n", + "21 [{'index': 2, 'value': 1.0}] \n", + "22 [{'index': 1, 'value': 1.0}] \n", + "24 [{'index': 1, 'value': 1.0}] \n", + "25 [{'index': 1, 'value': 1.0}] \n", + "26 [{'index': 1, 'value': 1.0}] \n", + "27 [{'index': 2, 'value': 1.0}] \n", + "29 [{'index': 3, 'value': 1.0}] \n", + "...\n", + "\n", + "[267 rows x 6 columns]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from bigframes.ml.compose import ColumnTransformer\n", "from bigframes.ml.preprocessing import OneHotEncoder\n", @@ -686,11 +2301,512 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": { "id": "ZeloMmopm8KI" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job a59bf4cc-4c92-4a68-96b1-7465fbcb3ed0 is DONE. 21.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 6860c534-a218-4a55-866d-a6e011399cd9 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 1b3e8da6-2d64-4337-872e-55b874f00596 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job fc118469-8dd7-4187-a3c1-7c5c2f1c5e36 is DONE. 5.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 544c5453-cd10-4a08-a338-601d85142df8 is DONE. 536 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 41c82cc9-7268-40ae-a736-f7a5f2c8b413 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job e9836f6b-160d-4ce4-88b6-0b04f40a1549 is DONE. 5.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_gonehotencoded_islandstandard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mmonehotencoded_sexonehotencoded_species
penguin_id
14772.376044[{'index': 1, 'value': 1.0}]0.220718-1.3592771.045621[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
153883.373922[{'index': 2, 'value': 1.0}]-0.5104390.157322-0.771966[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
283479.709088[{'index': 2, 'value': 1.0}]-1.0588070.713408-0.771966[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
324223.853626[{'index': 2, 'value': 1.0}]1.4636851.1683880.39129[{'index': 3, 'value': 1.0}][{'index': 2, 'value': 1.0}]
333197.623474[{'index': 2, 'value': 1.0}]-0.2545340.056215-0.990076[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
344155.26742[{'index': 2, 'value': 1.0}]-0.5104390.4606420.318587[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
373991.314095[{'index': 2, 'value': 1.0}]1.3540120.511195-0.263041[{'index': 3, 'value': 1.0}][{'index': 2, 'value': 1.0}]
413232.648242[{'index': 3, 'value': 1.0}]-0.674949-0.095445-1.789814[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
474017.740788[{'index': 2, 'value': 1.0}]-1.1684810.662855-0.117634[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
523365.080596[{'index': 2, 'value': 1.0}]0.4583440.308982-0.699262[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
563791.332002[{'index': 1, 'value': 1.0}]-1.0405280.460642-1.135483[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
573547.892992[{'index': 1, 'value': 1.0}]-0.9674120.005662-0.117634[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
625372.087702[{'index': 1, 'value': 1.0}]0.988433-0.7526381.191028[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
654263.232169[{'index': 2, 'value': 1.0}]1.7561481.3706010.318587[{'index': 3, 'value': 1.0}][{'index': 2, 'value': 1.0}]
675234.45894[{'index': 1, 'value': 1.0}]0.677691-1.3592771.045621[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
753979.314516[{'index': 1, 'value': 1.0}]-1.1136441.421155-0.771966[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
813481.331391[{'index': 2, 'value': 1.0}]0.6776910.561748-0.408448[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
893915.240555[{'index': 2, 'value': 1.0}]-0.8577390.713408-0.771966[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
923425.563946[{'index': 2, 'value': 1.0}]-0.8029020.308982-0.917372[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
934141.497717[{'index': 1, 'value': 1.0}]-0.3093711.168388-0.263041[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
963394.72289[{'index': 2, 'value': 1.0}]-0.3093710.662855-1.499[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1003507.226918[{'index': 2, 'value': 1.0}]-0.9125760.814515-0.771966[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1014922.286202[{'index': 1, 'value': 1.0}]0.549739-1.3087241.554546[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
1024016.243221[{'index': 2, 'value': 1.0}]-0.1265820.662855-0.626559[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1074933.655362[{'index': 1, 'value': 1.0}]1.20778-1.0054041.118325[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
\n", + "

25 rows × 7 columns

\n", + "
[67 rows x 7 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g onehotencoded_island \\\n", + "penguin_id \n", + "1 4772.376044 [{'index': 1, 'value': 1.0}] \n", + "15 3883.373922 [{'index': 2, 'value': 1.0}] \n", + "28 3479.709088 [{'index': 2, 'value': 1.0}] \n", + "32 4223.853626 [{'index': 2, 'value': 1.0}] \n", + "33 3197.623474 [{'index': 2, 'value': 1.0}] \n", + "34 4155.26742 [{'index': 2, 'value': 1.0}] \n", + "37 3991.314095 [{'index': 2, 'value': 1.0}] \n", + "41 3232.648242 [{'index': 3, 'value': 1.0}] \n", + "47 4017.740788 [{'index': 2, 'value': 1.0}] \n", + "52 3365.080596 [{'index': 2, 'value': 1.0}] \n", + "56 3791.332002 [{'index': 1, 'value': 1.0}] \n", + "57 3547.892992 [{'index': 1, 'value': 1.0}] \n", + "62 5372.087702 [{'index': 1, 'value': 1.0}] \n", + "65 4263.232169 [{'index': 2, 'value': 1.0}] \n", + "67 5234.45894 [{'index': 1, 'value': 1.0}] \n", + "75 3979.314516 [{'index': 1, 'value': 1.0}] \n", + "81 3481.331391 [{'index': 2, 'value': 1.0}] \n", + "89 3915.240555 [{'index': 2, 'value': 1.0}] \n", + "92 3425.563946 [{'index': 2, 'value': 1.0}] \n", + "93 4141.497717 [{'index': 1, 'value': 1.0}] \n", + "96 3394.72289 [{'index': 2, 'value': 1.0}] \n", + "100 3507.226918 [{'index': 2, 'value': 1.0}] \n", + "101 4922.286202 [{'index': 1, 'value': 1.0}] \n", + "102 4016.243221 [{'index': 2, 'value': 1.0}] \n", + "107 4933.655362 [{'index': 1, 'value': 1.0}] \n", + "\n", + " standard_scaled_culmen_length_mm standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "1 0.220718 -1.359277 \n", + "15 -0.510439 0.157322 \n", + "28 -1.058807 0.713408 \n", + "32 1.463685 1.168388 \n", + "33 -0.254534 0.056215 \n", + "34 -0.510439 0.460642 \n", + "37 1.354012 0.511195 \n", + "41 -0.674949 -0.095445 \n", + "47 -1.168481 0.662855 \n", + "52 0.458344 0.308982 \n", + "56 -1.040528 0.460642 \n", + "57 -0.967412 0.005662 \n", + "62 0.988433 -0.752638 \n", + "65 1.756148 1.370601 \n", + "67 0.677691 -1.359277 \n", + "75 -1.113644 1.421155 \n", + "81 0.677691 0.561748 \n", + "89 -0.857739 0.713408 \n", + "92 -0.802902 0.308982 \n", + "93 -0.309371 1.168388 \n", + "96 -0.309371 0.662855 \n", + "100 -0.912576 0.814515 \n", + "101 0.549739 -1.308724 \n", + "102 -0.126582 0.662855 \n", + "107 1.20778 -1.005404 \n", + "\n", + " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", + "penguin_id \n", + "1 1.045621 [{'index': 2, 'value': 1.0}] \n", + "15 -0.771966 [{'index': 3, 'value': 1.0}] \n", + "28 -0.771966 [{'index': 2, 'value': 1.0}] \n", + "32 0.39129 [{'index': 3, 'value': 1.0}] \n", + "33 -0.990076 [{'index': 2, 'value': 1.0}] \n", + "34 0.318587 [{'index': 3, 'value': 1.0}] \n", + "37 -0.263041 [{'index': 3, 'value': 1.0}] \n", + "41 -1.789814 [{'index': 2, 'value': 1.0}] \n", + "47 -0.117634 [{'index': 3, 'value': 1.0}] \n", + "52 -0.699262 [{'index': 2, 'value': 1.0}] \n", + "56 -1.135483 [{'index': 3, 'value': 1.0}] \n", + "57 -0.117634 [{'index': 2, 'value': 1.0}] \n", + "62 1.191028 [{'index': 3, 'value': 1.0}] \n", + "65 0.318587 [{'index': 3, 'value': 1.0}] \n", + "67 1.045621 [{'index': 3, 'value': 1.0}] \n", + "75 -0.771966 [{'index': 3, 'value': 1.0}] \n", + "81 -0.408448 [{'index': 2, 'value': 1.0}] \n", + "89 -0.771966 [{'index': 3, 'value': 1.0}] \n", + "92 -0.917372 [{'index': 2, 'value': 1.0}] \n", + "93 -0.263041 [{'index': 3, 'value': 1.0}] \n", + "96 -1.499 [{'index': 2, 'value': 1.0}] \n", + "100 -0.771966 [{'index': 2, 'value': 1.0}] \n", + "101 1.554546 [{'index': 2, 'value': 1.0}] \n", + "102 -0.626559 [{'index': 3, 'value': 1.0}] \n", + "107 1.118325 [{'index': 2, 'value': 1.0}] \n", + "\n", + " onehotencoded_species \n", + "penguin_id \n", + "1 [{'index': 3, 'value': 1.0}] \n", + "15 [{'index': 1, 'value': 1.0}] \n", + "28 [{'index': 1, 'value': 1.0}] \n", + "32 [{'index': 2, 'value': 1.0}] \n", + "33 [{'index': 2, 'value': 1.0}] \n", + "34 [{'index': 1, 'value': 1.0}] \n", + "37 [{'index': 2, 'value': 1.0}] \n", + "41 [{'index': 1, 'value': 1.0}] \n", + "47 [{'index': 1, 'value': 1.0}] \n", + "52 [{'index': 2, 'value': 1.0}] \n", + "56 [{'index': 1, 'value': 1.0}] \n", + "57 [{'index': 1, 'value': 1.0}] \n", + "62 [{'index': 3, 'value': 1.0}] \n", + "65 [{'index': 2, 'value': 1.0}] \n", + "67 [{'index': 3, 'value': 1.0}] \n", + "75 [{'index': 1, 'value': 1.0}] \n", + "81 [{'index': 2, 'value': 1.0}] \n", + "89 [{'index': 1, 'value': 1.0}] \n", + "92 [{'index': 1, 'value': 1.0}] \n", + "93 [{'index': 1, 'value': 1.0}] \n", + "96 [{'index': 1, 'value': 1.0}] \n", + "100 [{'index': 1, 'value': 1.0}] \n", + "101 [{'index': 3, 'value': 1.0}] \n", + "102 [{'index': 1, 'value': 1.0}] \n", + "107 [{'index': 3, 'value': 1.0}] \n", + "\n", + "[67 rows x 7 columns]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from bigframes.ml.linear_model import LinearRegression\n", "\n", @@ -719,11 +2835,555 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "id": "M13zd02znCIg" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 728068d3-2349-4636-a030-016b500a9812 is DONE. 23.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 37bac685-2afa-4ece-b3a3-e0b84a92c65f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 38416629-4615-45f5-9e27-d9164124f755 is DONE. 6.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 0241ea1c-8d96-418a-b3d6-08d819854954 is DONE. 536 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 405bcf9b-d652-42f3-931e-12ca0310fe4f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 21ca6f31-2ea2-4f71-b030-c738bf5afe27 is DONE. 10.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CENTROID_IDNEAREST_CENTROIDS_DISTANCEonehotencoded_islandstandard_scaled_culmen_length_mmstandard_scaled_culmen_depth_mmstandard_scaled_flipper_length_mmonehotencoded_sexonehotencoded_species
penguin_id
13[{'CENTROID_ID': 3, 'DISTANCE': 0.857057881337...[{'index': 1, 'value': 1.0}]0.220718-1.3592771.045621[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
154[{'CENTROID_ID': 4, 'DISTANCE': 1.181613302004...[{'index': 2, 'value': 1.0}]-0.5104390.157322-0.771966[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
281[{'CENTROID_ID': 1, 'DISTANCE': 1.006856853050...[{'index': 2, 'value': 1.0}]-1.0588070.713408-0.771966[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
322[{'CENTROID_ID': 2, 'DISTANCE': 1.237504384283...[{'index': 2, 'value': 1.0}]1.4636851.1683880.39129[{'index': 3, 'value': 1.0}][{'index': 2, 'value': 1.0}]
332[{'CENTROID_ID': 2, 'DISTANCE': 1.656439702919...[{'index': 2, 'value': 1.0}]-0.2545340.056215-0.990076[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
344[{'CENTROID_ID': 4, 'DISTANCE': 1.343792119214...[{'index': 2, 'value': 1.0}]-0.5104390.4606420.318587[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
372[{'CENTROID_ID': 2, 'DISTANCE': 0.816670297369...[{'index': 2, 'value': 1.0}]1.3540120.511195-0.263041[{'index': 3, 'value': 1.0}][{'index': 2, 'value': 1.0}]
411[{'CENTROID_ID': 1, 'DISTANCE': 1.317560921596...[{'index': 3, 'value': 1.0}]-0.674949-0.095445-1.789814[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
474[{'CENTROID_ID': 4, 'DISTANCE': 1.135112005343...[{'index': 2, 'value': 1.0}]-1.1684810.662855-0.117634[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
522[{'CENTROID_ID': 2, 'DISTANCE': 1.004096945181...[{'index': 2, 'value': 1.0}]0.4583440.308982-0.699262[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
564[{'CENTROID_ID': 4, 'DISTANCE': 1.218648668822...[{'index': 1, 'value': 1.0}]-1.0405280.460642-1.135483[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
571[{'CENTROID_ID': 1, 'DISTANCE': 1.238466630273...[{'index': 1, 'value': 1.0}]-0.9674120.005662-0.117634[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
623[{'CENTROID_ID': 3, 'DISTANCE': 0.876984617451...[{'index': 1, 'value': 1.0}]0.988433-0.7526381.191028[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
652[{'CENTROID_ID': 2, 'DISTANCE': 1.439604004538...[{'index': 2, 'value': 1.0}]1.7561481.3706010.318587[{'index': 3, 'value': 1.0}][{'index': 2, 'value': 1.0}]
673[{'CENTROID_ID': 3, 'DISTANCE': 0.763112987694...[{'index': 1, 'value': 1.0}]0.677691-1.3592771.045621[{'index': 3, 'value': 1.0}][{'index': 3, 'value': 1.0}]
754[{'CENTROID_ID': 4, 'DISTANCE': 1.075788925734...[{'index': 1, 'value': 1.0}]-1.1136441.421155-0.771966[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
812[{'CENTROID_ID': 2, 'DISTANCE': 0.777307801541...[{'index': 2, 'value': 1.0}]0.6776910.561748-0.408448[{'index': 2, 'value': 1.0}][{'index': 2, 'value': 1.0}]
894[{'CENTROID_ID': 4, 'DISTANCE': 0.891303183824...[{'index': 2, 'value': 1.0}]-0.8577390.713408-0.771966[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
921[{'CENTROID_ID': 1, 'DISTANCE': 0.934676470689...[{'index': 2, 'value': 1.0}]-0.8029020.308982-0.917372[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
934[{'CENTROID_ID': 4, 'DISTANCE': 0.984620018517...[{'index': 1, 'value': 1.0}]-0.3093711.168388-0.263041[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
961[{'CENTROID_ID': 1, 'DISTANCE': 1.446939975674...[{'index': 2, 'value': 1.0}]-0.3093710.662855-1.499[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1001[{'CENTROID_ID': 1, 'DISTANCE': 1.101117711572...[{'index': 2, 'value': 1.0}]-0.9125760.814515-0.771966[{'index': 2, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1013[{'CENTROID_ID': 3, 'DISTANCE': 0.823832007899...[{'index': 1, 'value': 1.0}]0.549739-1.3087241.554546[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
1024[{'CENTROID_ID': 4, 'DISTANCE': 0.995348310182...[{'index': 2, 'value': 1.0}]-0.1265820.662855-0.626559[{'index': 3, 'value': 1.0}][{'index': 1, 'value': 1.0}]
1073[{'CENTROID_ID': 3, 'DISTANCE': 0.930021405831...[{'index': 1, 'value': 1.0}]1.20778-1.0054041.118325[{'index': 2, 'value': 1.0}][{'index': 3, 'value': 1.0}]
\n", + "

25 rows × 8 columns

\n", + "
[67 rows x 8 columns in total]" + ], + "text/plain": [ + " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", + "penguin_id \n", + "1 3 [{'CENTROID_ID': 3, 'DISTANCE': 0.857057881337... \n", + "15 4 [{'CENTROID_ID': 4, 'DISTANCE': 1.181613302004... \n", + "28 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.006856853050... \n", + "32 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.237504384283... \n", + "33 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.656439702919... \n", + "34 4 [{'CENTROID_ID': 4, 'DISTANCE': 1.343792119214... \n", + "37 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.816670297369... \n", + "41 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.317560921596... \n", + "47 4 [{'CENTROID_ID': 4, 'DISTANCE': 1.135112005343... \n", + "52 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.004096945181... \n", + "56 4 [{'CENTROID_ID': 4, 'DISTANCE': 1.218648668822... \n", + "57 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.238466630273... \n", + "62 3 [{'CENTROID_ID': 3, 'DISTANCE': 0.876984617451... \n", + "65 2 [{'CENTROID_ID': 2, 'DISTANCE': 1.439604004538... \n", + "67 3 [{'CENTROID_ID': 3, 'DISTANCE': 0.763112987694... \n", + "75 4 [{'CENTROID_ID': 4, 'DISTANCE': 1.075788925734... \n", + "81 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.777307801541... \n", + "89 4 [{'CENTROID_ID': 4, 'DISTANCE': 0.891303183824... \n", + "92 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.934676470689... \n", + "93 4 [{'CENTROID_ID': 4, 'DISTANCE': 0.984620018517... \n", + "96 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.446939975674... \n", + "100 1 [{'CENTROID_ID': 1, 'DISTANCE': 1.101117711572... \n", + "101 3 [{'CENTROID_ID': 3, 'DISTANCE': 0.823832007899... \n", + "102 4 [{'CENTROID_ID': 4, 'DISTANCE': 0.995348310182... \n", + "107 3 [{'CENTROID_ID': 3, 'DISTANCE': 0.930021405831... \n", + "\n", + " onehotencoded_island standard_scaled_culmen_length_mm \\\n", + "penguin_id \n", + "1 [{'index': 1, 'value': 1.0}] 0.220718 \n", + "15 [{'index': 2, 'value': 1.0}] -0.510439 \n", + "28 [{'index': 2, 'value': 1.0}] -1.058807 \n", + "32 [{'index': 2, 'value': 1.0}] 1.463685 \n", + "33 [{'index': 2, 'value': 1.0}] -0.254534 \n", + "34 [{'index': 2, 'value': 1.0}] -0.510439 \n", + "37 [{'index': 2, 'value': 1.0}] 1.354012 \n", + "41 [{'index': 3, 'value': 1.0}] -0.674949 \n", + "47 [{'index': 2, 'value': 1.0}] -1.168481 \n", + "52 [{'index': 2, 'value': 1.0}] 0.458344 \n", + "56 [{'index': 1, 'value': 1.0}] -1.040528 \n", + "57 [{'index': 1, 'value': 1.0}] -0.967412 \n", + "62 [{'index': 1, 'value': 1.0}] 0.988433 \n", + "65 [{'index': 2, 'value': 1.0}] 1.756148 \n", + "67 [{'index': 1, 'value': 1.0}] 0.677691 \n", + "75 [{'index': 1, 'value': 1.0}] -1.113644 \n", + "81 [{'index': 2, 'value': 1.0}] 0.677691 \n", + "89 [{'index': 2, 'value': 1.0}] -0.857739 \n", + "92 [{'index': 2, 'value': 1.0}] -0.802902 \n", + "93 [{'index': 1, 'value': 1.0}] -0.309371 \n", + "96 [{'index': 2, 'value': 1.0}] -0.309371 \n", + "100 [{'index': 2, 'value': 1.0}] -0.912576 \n", + "101 [{'index': 1, 'value': 1.0}] 0.549739 \n", + "102 [{'index': 2, 'value': 1.0}] -0.126582 \n", + "107 [{'index': 1, 'value': 1.0}] 1.20778 \n", + "\n", + " standard_scaled_culmen_depth_mm \\\n", + "penguin_id \n", + "1 -1.359277 \n", + "15 0.157322 \n", + "28 0.713408 \n", + "32 1.168388 \n", + "33 0.056215 \n", + "34 0.460642 \n", + "37 0.511195 \n", + "41 -0.095445 \n", + "47 0.662855 \n", + "52 0.308982 \n", + "56 0.460642 \n", + "57 0.005662 \n", + "62 -0.752638 \n", + "65 1.370601 \n", + "67 -1.359277 \n", + "75 1.421155 \n", + "81 0.561748 \n", + "89 0.713408 \n", + "92 0.308982 \n", + "93 1.168388 \n", + "96 0.662855 \n", + "100 0.814515 \n", + "101 -1.308724 \n", + "102 0.662855 \n", + "107 -1.005404 \n", + "\n", + " standard_scaled_flipper_length_mm onehotencoded_sex \\\n", + "penguin_id \n", + "1 1.045621 [{'index': 2, 'value': 1.0}] \n", + "15 -0.771966 [{'index': 3, 'value': 1.0}] \n", + "28 -0.771966 [{'index': 2, 'value': 1.0}] \n", + "32 0.39129 [{'index': 3, 'value': 1.0}] \n", + "33 -0.990076 [{'index': 2, 'value': 1.0}] \n", + "34 0.318587 [{'index': 3, 'value': 1.0}] \n", + "37 -0.263041 [{'index': 3, 'value': 1.0}] \n", + "41 -1.789814 [{'index': 2, 'value': 1.0}] \n", + "47 -0.117634 [{'index': 3, 'value': 1.0}] \n", + "52 -0.699262 [{'index': 2, 'value': 1.0}] \n", + "56 -1.135483 [{'index': 3, 'value': 1.0}] \n", + "57 -0.117634 [{'index': 2, 'value': 1.0}] \n", + "62 1.191028 [{'index': 3, 'value': 1.0}] \n", + "65 0.318587 [{'index': 3, 'value': 1.0}] \n", + "67 1.045621 [{'index': 3, 'value': 1.0}] \n", + "75 -0.771966 [{'index': 3, 'value': 1.0}] \n", + "81 -0.408448 [{'index': 2, 'value': 1.0}] \n", + "89 -0.771966 [{'index': 3, 'value': 1.0}] \n", + "92 -0.917372 [{'index': 2, 'value': 1.0}] \n", + "93 -0.263041 [{'index': 3, 'value': 1.0}] \n", + "96 -1.499 [{'index': 2, 'value': 1.0}] \n", + "100 -0.771966 [{'index': 2, 'value': 1.0}] \n", + "101 1.554546 [{'index': 2, 'value': 1.0}] \n", + "102 -0.626559 [{'index': 3, 'value': 1.0}] \n", + "107 1.118325 [{'index': 2, 'value': 1.0}] \n", + "\n", + " onehotencoded_species \n", + "penguin_id \n", + "1 [{'index': 3, 'value': 1.0}] \n", + "15 [{'index': 1, 'value': 1.0}] \n", + "28 [{'index': 1, 'value': 1.0}] \n", + "32 [{'index': 2, 'value': 1.0}] \n", + "33 [{'index': 2, 'value': 1.0}] \n", + "34 [{'index': 1, 'value': 1.0}] \n", + "37 [{'index': 2, 'value': 1.0}] \n", + "41 [{'index': 1, 'value': 1.0}] \n", + "47 [{'index': 1, 'value': 1.0}] \n", + "52 [{'index': 2, 'value': 1.0}] \n", + "56 [{'index': 1, 'value': 1.0}] \n", + "57 [{'index': 1, 'value': 1.0}] \n", + "62 [{'index': 3, 'value': 1.0}] \n", + "65 [{'index': 2, 'value': 1.0}] \n", + "67 [{'index': 3, 'value': 1.0}] \n", + "75 [{'index': 1, 'value': 1.0}] \n", + "81 [{'index': 2, 'value': 1.0}] \n", + "89 [{'index': 1, 'value': 1.0}] \n", + "92 [{'index': 1, 'value': 1.0}] \n", + "93 [{'index': 1, 'value': 1.0}] \n", + "96 [{'index': 1, 'value': 1.0}] \n", + "100 [{'index': 1, 'value': 1.0}] \n", + "101 [{'index': 3, 'value': 1.0}] \n", + "102 [{'index': 1, 'value': 1.0}] \n", + "107 [{'index': 3, 'value': 1.0}] \n", + "\n", + "[67 rows x 8 columns]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from bigframes.ml.cluster import KMeans\n", "\n", @@ -750,11 +3410,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": { "id": "Ku2OXqgJnEeR" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('preproc',\n", + " ColumnTransformer(transformers=[('scale', StandardScaler(),\n", + " ['culmen_length_mm',\n", + " 'culmen_depth_mm',\n", + " 'flipper_length_mm']),\n", + " ('encode', OneHotEncoder(),\n", + " ['species', 'sex',\n", + " 'island'])])),\n", + " ('linreg', LinearRegression())])" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from bigframes.ml.pipeline import Pipeline\n", "\n", @@ -778,11 +3457,484 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": { "id": "hsF7FYagnMko" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 95b43592-b198-4f9e-a990-4e837b82121f is DONE. 24.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 615b2afb-0c76-45d6-82c7-bde7c8b2b3a4 is DONE. 8.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job cf2ed3ca-01bf-4cb6-a71a-d6e30a8428f6 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d9780763-1d2b-494d-a778-20364c52bd08 is DONE. 29.6 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f01296ba-7cd0-4d06-b25a-b5697e46bbf7 is DONE. 536 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5b6fe451-2f8e-471e-a6a0-00b9bffaa826 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 6a81883b-0514-4251-9f63-490b6346bb8b is DONE. 6.1 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_gislandculmen_length_mmculmen_depth_mmflipper_length_mmsexspecies
penguin_id
14772.374547Biscoe45.114.5215.0FEMALEGentoo penguin (Pygoscelis papua)
153883.371052Dream41.117.5190.0MALEAdelie Penguin (Pygoscelis adeliae)
283479.706166Dream38.118.6190.0FEMALEAdelie Penguin (Pygoscelis adeliae)
324223.851137Dream51.919.5206.0MALEChinstrap penguin (Pygoscelis antarctica)
333197.620461Dream42.517.3187.0FEMALEChinstrap penguin (Pygoscelis antarctica)
344155.265191Dream41.118.1205.0MALEAdelie Penguin (Pygoscelis adeliae)
373991.311319Dream51.318.2197.0MALEChinstrap penguin (Pygoscelis antarctica)
413232.644783Torgersen40.217.0176.0FEMALEAdelie Penguin (Pygoscelis adeliae)
474017.738303Dream37.518.5199.0MALEAdelie Penguin (Pygoscelis adeliae)
523365.077659Dream46.417.8191.0FEMALEChinstrap penguin (Pygoscelis antarctica)
563791.328893Biscoe38.218.1185.0MALEAdelie Penguin (Pygoscelis adeliae)
573547.890609Biscoe38.617.2199.0FEMALEAdelie Penguin (Pygoscelis adeliae)
625372.086117Biscoe49.315.7217.0MALEGentoo penguin (Pygoscelis papua)
654263.229571Dream53.519.9205.0MALEChinstrap penguin (Pygoscelis antarctica)
675234.457401Biscoe47.614.5215.0MALEGentoo penguin (Pygoscelis papua)
753979.311469Biscoe37.820.0190.0MALEAdelie Penguin (Pygoscelis adeliae)
813481.328573Dream47.618.3195.0FEMALEChinstrap penguin (Pygoscelis antarctica)
893915.237615Dream39.218.6190.0MALEAdelie Penguin (Pygoscelis adeliae)
923425.560982Dream39.517.8188.0FEMALEAdelie Penguin (Pygoscelis adeliae)
934141.494969Biscoe42.219.5197.0MALEAdelie Penguin (Pygoscelis adeliae)
963394.719445Dream42.218.5180.0FEMALEAdelie Penguin (Pygoscelis adeliae)
1003507.223965Dream38.918.8190.0FEMALEAdelie Penguin (Pygoscelis adeliae)
1014922.284991Biscoe46.914.6222.0FEMALEGentoo penguin (Pygoscelis papua)
1024016.240318Dream43.218.5192.0MALEAdelie Penguin (Pygoscelis adeliae)
1074933.653758Biscoe50.515.2216.0FEMALEGentoo penguin (Pygoscelis papua)
\n", + "

25 rows × 7 columns

\n", + "
[67 rows x 7 columns in total]" + ], + "text/plain": [ + " predicted_body_mass_g island culmen_length_mm \\\n", + "penguin_id \n", + "1 4772.374547 Biscoe 45.1 \n", + "15 3883.371052 Dream 41.1 \n", + "28 3479.706166 Dream 38.1 \n", + "32 4223.851137 Dream 51.9 \n", + "33 3197.620461 Dream 42.5 \n", + "34 4155.265191 Dream 41.1 \n", + "37 3991.311319 Dream 51.3 \n", + "41 3232.644783 Torgersen 40.2 \n", + "47 4017.738303 Dream 37.5 \n", + "52 3365.077659 Dream 46.4 \n", + "56 3791.328893 Biscoe 38.2 \n", + "57 3547.890609 Biscoe 38.6 \n", + "62 5372.086117 Biscoe 49.3 \n", + "65 4263.229571 Dream 53.5 \n", + "67 5234.457401 Biscoe 47.6 \n", + "75 3979.311469 Biscoe 37.8 \n", + "81 3481.328573 Dream 47.6 \n", + "89 3915.237615 Dream 39.2 \n", + "92 3425.560982 Dream 39.5 \n", + "93 4141.494969 Biscoe 42.2 \n", + "96 3394.719445 Dream 42.2 \n", + "100 3507.223965 Dream 38.9 \n", + "101 4922.284991 Biscoe 46.9 \n", + "102 4016.240318 Dream 43.2 \n", + "107 4933.653758 Biscoe 50.5 \n", + "\n", + " culmen_depth_mm flipper_length_mm sex \\\n", + "penguin_id \n", + "1 14.5 215.0 FEMALE \n", + "15 17.5 190.0 MALE \n", + "28 18.6 190.0 FEMALE \n", + "32 19.5 206.0 MALE \n", + "33 17.3 187.0 FEMALE \n", + "34 18.1 205.0 MALE \n", + "37 18.2 197.0 MALE \n", + "41 17.0 176.0 FEMALE \n", + "47 18.5 199.0 MALE \n", + "52 17.8 191.0 FEMALE \n", + "56 18.1 185.0 MALE \n", + "57 17.2 199.0 FEMALE \n", + "62 15.7 217.0 MALE \n", + "65 19.9 205.0 MALE \n", + "67 14.5 215.0 MALE \n", + "75 20.0 190.0 MALE \n", + "81 18.3 195.0 FEMALE \n", + "89 18.6 190.0 MALE \n", + "92 17.8 188.0 FEMALE \n", + "93 19.5 197.0 MALE \n", + "96 18.5 180.0 FEMALE \n", + "100 18.8 190.0 FEMALE \n", + "101 14.6 222.0 FEMALE \n", + "102 18.5 192.0 MALE \n", + "107 15.2 216.0 FEMALE \n", + "\n", + " species \n", + "penguin_id \n", + "1 Gentoo penguin (Pygoscelis papua) \n", + "15 Adelie Penguin (Pygoscelis adeliae) \n", + "28 Adelie Penguin (Pygoscelis adeliae) \n", + "32 Chinstrap penguin (Pygoscelis antarctica) \n", + "33 Chinstrap penguin (Pygoscelis antarctica) \n", + "34 Adelie Penguin (Pygoscelis adeliae) \n", + "37 Chinstrap penguin (Pygoscelis antarctica) \n", + "41 Adelie Penguin (Pygoscelis adeliae) \n", + "47 Adelie Penguin (Pygoscelis adeliae) \n", + "52 Chinstrap penguin (Pygoscelis antarctica) \n", + "56 Adelie Penguin (Pygoscelis adeliae) \n", + "57 Adelie Penguin (Pygoscelis adeliae) \n", + "62 Gentoo penguin (Pygoscelis papua) \n", + "65 Chinstrap penguin (Pygoscelis antarctica) \n", + "67 Gentoo penguin (Pygoscelis papua) \n", + "75 Adelie Penguin (Pygoscelis adeliae) \n", + "81 Chinstrap penguin (Pygoscelis antarctica) \n", + "89 Adelie Penguin (Pygoscelis adeliae) \n", + "92 Adelie Penguin (Pygoscelis adeliae) \n", + "93 Adelie Penguin (Pygoscelis adeliae) \n", + "96 Adelie Penguin (Pygoscelis adeliae) \n", + "100 Adelie Penguin (Pygoscelis adeliae) \n", + "101 Gentoo penguin (Pygoscelis papua) \n", + "102 Adelie Penguin (Pygoscelis adeliae) \n", + "107 Gentoo penguin (Pygoscelis papua) \n", + "\n", + "[67 rows x 7 columns]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "pipeline.fit(X_train, y_train)\n", "\n", @@ -812,11 +3964,106 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "id": "Q8nR1ZqznU-B" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job c098e1d1-b3ed-4ec5-94c7-6ba3b2b59e3f is DONE. 29.6 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 035234b0-537a-44ce-adff-bb51c40b4ffa is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job b4a2a367-3e06-4fa3-9f00-bdbca884cfdd is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0225.88351277765.9892810.004457179.5480410.8731660.873315
\n", + "

1 rows × 6 columns

\n", + "
[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 225.883512 77765.989281 0.004457 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 179.548041 0.873166 0.873315 \n", + "\n", + "[1 rows x 6 columns]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# In the case of a pipeline, this will be equivalent to calling .score on the contained LinearRegression\n", "pipeline.score(X_test, y_test)" @@ -833,11 +4080,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": { "id": "vdEN4Ob9nan4" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 20ec1716-3e8e-4d3f-ba08-1f7b9970ce3f is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 6f628f3b-62df-4a5a-8e05-0b313db0ed07 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job c4eee1e5-146f-4a52-8499-83fe5f701f53 is DONE. 30.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0.8731660699616813" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from bigframes.ml.metrics import r2_score\n", "\n", @@ -862,24 +4156,104 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": { "id": "fb0HpkdpnigJ" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Copy job 06c2b62d-a7aa-46a5-a04a-2f189bafc5ee is DONE. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Pipeline(steps=[('transform',\n", + " ColumnTransformer(transformers=[('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'island'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_length_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_depth_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'flipper_length_mm'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'sex'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'species')])),\n", + " ('estimator',\n", + " LinearRegression(optimize_strategy='NORMAL_EQUATION'))])" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "linreg.to_gbq(f\"{DATASET}.penguins_model\", replace=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": { "id": "_zNOBlHdnkII" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('transform',\n", + " ColumnTransformer(transformers=[('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'island'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_length_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_depth_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'flipper_length_mm'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'sex'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'species')])),\n", + " ('estimator',\n", + " LinearRegression(optimize_strategy='NORMAL_EQUATION'))])" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "bf.read_gbq_model(f\"{DATASET}.penguins_model\")" + "bpd.read_gbq_model(f\"{DATASET}.penguins_model\")" ] }, { @@ -893,24 +4267,104 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": { "id": "P76_TQ3IR6nB" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Copy job a0ed8c1b-3a3f-4995-853c-e151d41560d7 is DONE. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Pipeline(steps=[('transform',\n", + " ColumnTransformer(transformers=[('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'island'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_length_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_depth_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'flipper_length_mm'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'sex'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'species')])),\n", + " ('estimator',\n", + " LinearRegression(optimize_strategy='NORMAL_EQUATION'))])" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "pipeline.to_gbq(f\"{DATASET}.penguins_pipeline\", replace=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": { "id": "GKvlKFjAbToJ" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('transform',\n", + " ColumnTransformer(transformers=[('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'island'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_length_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'culmen_depth_mm'),\n", + " ('standard_scaler',\n", + " StandardScaler(),\n", + " 'flipper_length_mm'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'sex'),\n", + " ('ont_hot_encoder',\n", + " OneHotEncoder(max_categories=1000001,\n", + " min_frequency=0),\n", + " 'species')])),\n", + " ('estimator',\n", + " LinearRegression(optimize_strategy='NORMAL_EQUATION'))])" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "bf.read_gbq_model(f\"{DATASET}.penguins_pipeline\")" + "bpd.read_gbq_model(f\"{DATASET}.penguins_pipeline\")" ] }, { @@ -942,7 +4396,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "id": "QwumLUKmVpuH" }, @@ -963,6 +4417,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.11.1" } }, "nbformat": 4, diff --git a/notebooks/location/regionalized.ipynb b/notebooks/location/regionalized.ipynb index 86f43b1dd65..c05d27c24e8 100644 --- a/notebooks/location/regionalized.ipynb +++ b/notebooks/location/regionalized.ipynb @@ -132,7 +132,14 @@ "source": [ "import bigframes.pandas\n", "\n", + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", "bigframes.pandas.options.bigquery.project = PROJECT\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", "bigframes.pandas.options.bigquery.location = BQ_LOCATION" ] }, diff --git a/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb b/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb index 675416f6ea8..347a3e8cffc 100644 --- a/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb +++ b/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb @@ -340,7 +340,7 @@ }, "outputs": [], "source": [ - "import bigframes.pandas as bf" + "import bigframes.pandas as bpd" ] }, { @@ -360,8 +360,15 @@ }, "outputs": [], "source": [ - "bf.options.bigquery.project = PROJECT_ID\n", - "bf.options.bigquery.location = REGION" + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = REGION" ] }, { @@ -370,7 +377,7 @@ "id": "D21CoOlfFTYI" }, "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." ] }, { @@ -392,7 +399,7 @@ }, "outputs": [], "source": [ - "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" + "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" ] }, { diff --git a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb index cbbcc57aec9..87b8f9c0b60 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": null, + "execution_count": 1, "metadata": { "id": "9GIt_orUtNvA" }, @@ -129,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "id": "4aooKMmnxrWF" }, @@ -151,7 +151,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "id": "bk03Rt_HyGx-" }, @@ -200,16 +200,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "id": "R7STCS8xB5d2" }, "outputs": [], "source": [ - "import bigframes.pandas as bf\n", + "import bigframes.pandas as bpd\n", "\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "bf.options.bigquery.location = REGION" + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = REGION" ] }, { @@ -223,13 +230,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "id": "zDSwoBo1CU3G" }, "outputs": [], "source": [ - "all_data = bf.read_gbq(\"bigquery-public-data.covid19_open_data.covid19_open_data\")" + "all_data = bpd.read_gbq(\"bigquery-public-data.covid19_open_data.covid19_open_data\")" ] }, { @@ -243,7 +250,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "id": "UjMT_qhjf8Fu" }, @@ -263,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "id": "IaoUf57ZwrJ8" }, @@ -293,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "id": "tYDoaKgJChiq" }, @@ -323,11 +330,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "id": "gFbCgfFC2gHw" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 38e28079-9a84-4c28-a04c-cdc0afbb74b1 is DONE. 273.1 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job b1df794f-6d3f-4f05-8bcd-2da29f4eb402 is DONE. 372.9 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHRCAYAAACW3ZisAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB0BklEQVR4nO3deVyU1f4H8M/MAAOyCSqggPuuiLiDmpp71tVr2Z51S+8trVxatbJfy402U9u03SzNsptaVi6pWCq5L4hLuYKyiCCL7Myc3x/I8MwwM8zADM/Mw+f9evFyeOaZmXME5vnO93zPOSohhAARERGRQqjlbgARERGRIzG4ISIiIkVhcENERESKwuCGiIiIFIXBDRERESkKgxsiIiJSFAY3REREpCgMboiIiEhRGNwQERGRojC4ISIiIkVp1MHN77//jltuuQWtWrWCSqXCunXr7H4OIQTefvttdO7cGVqtFuHh4fjvf//r+MYSERGRTTzkboCcCgsLER0djQcffBCTJ0+u03PMmjULmzdvxttvv42oqCjk5OQgJyfHwS0lIiIiW6m4cWYllUqFtWvXYtKkSYZjpaWleO655/DNN98gNzcXPXv2xBtvvIHhw4cDAE6cOIFevXrh2LFj6NKlizwNJyIiIiONeliqNo8++igSExOxevVqHD16FFOmTMG4cePw999/AwB++ukntG/fHhs2bEC7du3Qtm1bTJs2jZkbIiIiGTG4sSAlJQVffPEF1qxZg6FDh6JDhw548sknMWTIEHzxxRcAgLNnz+LChQtYs2YNVqxYgeXLl+PAgQO47bbbZG49ERFR49Woa26sSUpKgk6nQ+fOnY2Ol5aWolmzZgAAvV6P0tJSrFixwnDeZ599hr59++LUqVMcqiIiIpIBgxsLrl27Bo1GgwMHDkCj0Rjd5+fnBwBo2bIlPDw8jAKgbt26AajM/DC4ISIiangMbiyIiYmBTqfD5cuXMXToULPnDB48GBUVFThz5gw6dOgAAPjrr78AAG3atGmwthIREVG1Rj1b6tq1azh9+jSAymDmnXfewYgRIxAcHIzWrVvj3nvvxa5du7Bw4ULExMQgKysLW7duRa9evTBhwgTo9Xr0798ffn5+WLx4MfR6PWbOnImAgABs3rxZ5t4RERE1To06uElISMCIESNqHL///vuxfPlylJeX49VXX8WKFStw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T09PxMTE4PTp02bvnzdvHvLy8gxfqampjmgyERHVExM35CyyZm6EEHjsscewdu1aJCQkoF27dnY/h06nQ1JSEm666Saz92u1Wmi12vo2lYiIHMze2VIsKCZbyRrczJw5E6tWrcL69evh7++PjIwMAEBgYCB8fHwAAFOnTkV4eDji4+MBAC+//DIGDRqEjh07Ijc3F2+99RYuXLiAadOmydYPIiKyH2tuyFlkDW6WLl0KABg+fLjR8S+++AIPPPAAACAlJQVqdfXo2dWrVzF9+nRkZGQgKCgIffv2xe7du9G9e/eGajYRETmAsDMVw1iIbCX7sFRtEhISjL5ftGgRFi1a5KQWERGRq+KwFNnKIQXFubm5jngaIiJqRBiskLPYHdy88cYb+Pbbbw3f33777WjWrBnCw8Nx5MgRhzaOiIiUy97YhrEQ2cru4GbZsmWIjIwEAGzZsgVbtmzBr7/+ivHjx+Opp55yeAOJiEiZmLkhZ7G75iYjI8MQ3GzYsAG33347xowZg7Zt22LgwIEObyARERGRPezO3AQFBRkWwtu4cSNGjRoFoLI4WKfTObZ1RESkWPauOMxMD9nK7szN5MmTcffdd6NTp07Izs7G+PHjAQCHDh1Cx44dHd5AIiJSJgYr5Cx2BzeLFi1C27ZtkZqaijfffBN+fn4AgPT0dMyYMcPhDSQiIiKyh93BjaenJ5588skax+fMmeOQBhERUePAxA05S53Wufnqq68wZMgQtGrVChcuXAAALF68GOvXr3do44iISME4LkVOYndws3TpUsydOxfjx49Hbm6uoYi4adOmWLx4saPbR0REBMD+AmRqvOwObt577z188skneO6556DRaAzH+/Xrh6SkJIc2joiIlIuhCjmL3cHNuXPnEBMTU+O4VqtFYWGhQxpFRETKx1Epcha7g5t27drh8OHDNY5v3LgR3bp1c0SbiIiIamIwRDaye7bU3LlzMXPmTJSUlEAIgb179+Kbb75BfHw8Pv30U2e0kYiIFIg1NOQsdgc306ZNg4+PD55//nkUFRXh7rvvRqtWrbBkyRLceeedzmgjEREpEIelyFnsDm4A4J577sE999yDoqIiXLt2DSEhIY5uFxERKRxjG3IWu2tuiouLUVRUBABo0qQJiouLsXjxYmzevNnhjSMiosald2RTuZtACmB3cDNx4kSsWLECAJCbm4sBAwZg4cKFmDhxIpYuXerwBhIRkTKZG5ZSq6yc77ymkMLYHdwcPHgQQ4cOBQB8//33CAsLw4ULF7BixQq8++67Dm8gEREpk7mCYpXKSnRDZCO7g5uioiL4+/sDADZv3ozJkydDrVZj0KBBhq0YiIiI6sJaaCNYgUw2sju46dixI9atW4fU1FRs2rQJY8aMAQBcvnwZAQEBDm8gEREplJlYhYkbcgS7g5sFCxbgySefRNu2bTFw4EDExsYCqMzimFu5mIiIyBxzeRiV1dwNkW3sngp+2223YciQIUhPT0d0dLTh+MiRI/HPf/7ToY0jIqLGxVrmhoNSZKs6rXMTFhaGsLAwo2MDBgxwSIOIiKhxMFdDw2EpcoQ6BTf79+/Hd999h5SUFJSVlRnd98MPPzikYUREpGzm6oM5LEWOYHfNzerVqxEXF4cTJ05g7dq1KC8vR3JyMrZt24bAwEBntJGIiBSsTbMmhtvM3JAj2B3cvPbaa1i0aBF++ukneHl5YcmSJTh58iRuv/12tG7d2hltJCIiBapK3KglEY2a0Q05gN3BzZkzZzBhwgQAgJeXFwoLC6FSqTBnzhx8/PHHDm8gEREpU9WwlDScsVpQzIpispHdwU1QUBAKCgoAAOHh4Th27BiAyq0YqvacIiIiqo1hhWIbkzWMbchWdgc3N9xwA7Zs2QIAmDJlCmbNmoXp06fjrrvuwsiRIx3eQCIiUjbpUJS17Rd+OpKGa6UVDdEkcnN2z5Z6//33UVJSAgB47rnn4Onpid27d+PWW2/F888/7/AGEhGRMpkblrK2cSYA/Hw0DXf0Z30nWWd35iY4OBitWrWqfLBajWeffRY//vgjFi5ciKCgILueKz4+Hv3794e/vz9CQkIwadIknDp1qtbHrVmzBl27doW3tzeioqLwyy+/2NsNIiJyEUaZm1rOZd0N2cLm4CYtLQ1PPvkk8vPza9yXl5eHp556CpmZmXa9+I4dOzBz5kz8+eef2LJlC8rLyzFmzBgUFhZafMzu3btx11134aGHHsKhQ4cwadIkTJo0yVD7Q0RE7kU6ElXbruCMbcgWNgc377zzDvLz881ujhkYGIiCggK88847dr34xo0b8cADD6BHjx6Ijo7G8uXLkZKSggMHDlh8zJIlSzBu3Dg89dRT6NatG1555RX06dMH77//vl2vTURE8qpaodhS5sZcnMPMDdnC5uBm48aNmDp1qsX7p06dig0bNtSrMXl5eQAqh74sSUxMxKhRo4yOjR07FomJifV6bSIialhVcYqlzI25HI5g7oZsYHNB8blz56wu0hcREYHz58/XuSF6vR6zZ8/G4MGD0bNnT4vnZWRkIDQ01OhYaGgoMjIyzJ5fWlqK0tJSw/fmhtWIiEg+xsGN9LaKqRqqE5szNz4+PlaDl/Pnz8PHx6fODZk5cyaOHTuG1atX1/k5zImPj0dgYKDhKzIy0qHPT0REdVMVt1haldhs5oaxDtnA5uBm4MCB+Oqrryzev2LFijrvDP7oo49iw4YN2L59OyIiIqyeGxYWVqNwOTMzs8Yu5VXmzZuHvLw8w1dqamqd2khERI5VNcRkaSq4SlWz7oaxDdnC5uDmySefxBdffIEnn3zSKLjIzMzEE088geXLl+PJJ5+068WFEHj00Uexdu1abNu2De3atav1MbGxsdi6davRsS1btiA2Ntbs+VqtFgEBAUZfRETkOozrbIyjmRrZG6ZuyAY219yMGDECH3zwAWbNmoVFixYhICAAKpUKeXl58PT0xHvvvYcbb7zRrhefOXMmVq1ahfXr18Pf399QNxMYGGgY4po6dSrCw8MRHx8PAJg1axaGDRuGhQsXYsKECVi9ejX279/Pfa2IiNyMYRE/SzU3UME0V8PQhmxh1wrF//nPf3DzzTfju+++w+nTpyGEQOfOnXHbbbfVOpxkztKlSwEAw4cPNzr+xRdf4IEHHgAApKSkQK2uTjDFxcVh1apVeP755zF//nx06tQJ69ats1qETERErscwW0pyzGgYihuEUx3Zvf1CeHg45syZ45AXFzakFxMSEmocmzJlCqZMmeKQNhARkTzMFRTXtogfkS3s3n6BiIjIkbj9AjkagxsiIpJJzZ0zTTM3pt/bkvEnYnBDRESyqB6Wqj5WW8kNQxuyBYMbIiKSlXT6t7qWcSkmbsgWdgc3CxYswPbt21FSUuKM9hARUSNhfio4dwWn+rM7uElMTMQtt9yCpk2bYujQoXj++efx22+/obi42BntIyIihapaodiegmIAuFZa4aQWkVLYHdxs2bIFubm52Lp1K2666Sbs378fkydPRtOmTTFkyBBntJGIiBTM0to2KlXNYOfD7afR88VN2HA0rSGaRm7K7nVuAMDDwwODBw9GixYtEBwcDH9/f6xbtw4nT550dPuIiEihqoelLG+/YCq7sAwAMO9/Sbi5VyuntY3cm92Zm48//hh33303wsPDERcXh40bN2LIkCHYv38/srKynNFGIiJSoKr6GdPNMu15LJE5dmduHn74YbRo0QJPPPEEZsyYAT8/P2e0i4iIGglL07/N7S1VhevdkDV2Z25++OEH3HPPPVi9ejVatGiBuLg4zJ8/H5s3b0ZRUZEz2khERApkbvsFNbdfIAewO3MzadIkTJo0CQCQl5eHP/74A2vWrMHNN98MtVrNKeJERGSTqtlSFncFV1kepmLehqypU0FxdnY2duzYgYSEBCQkJCA5ORlBQUEYOnSoo9tHRERKZa6gmIkbcgC7g5uoqCicOHECQUFBuOGGGzB9+nQMGzYMvXr1ckb7iIhI4YxXJbZvzRsic+pUUDxs2DD07NnTGe0hIqJGompoydr2C5aKillPTNbYHdzMnDkTAFBWVoZz586hQ4cO8PCo0+gWERE1YlUznizV3ADVdTlE9rB7tlRxcTEeeughNGnSBD169EBKSgoA4LHHHsPrr7/u8AYSEZGyqe1YxK8Kgx6yxu7g5tlnn8WRI0eQkJAAb29vw/FRo0bh22+/dWjjiIhIeYQQeGvTSfySlFF5wOJsKZXNwQ6RlN3jSevWrcO3336LQYMGGVW49+jRA2fOnHFo44iISHl2n8nGB9urrxdc54Ycze7MTVZWFkJCQmocLywsrHWreiIiorzicqPvTYuIjVha54ajUmSF3cFNv3798PPPPxu+rwpoPv30U8TGxjquZUREpEg+nhqj7422XDANZhjEUB3YPSz12muvYfz48Th+/DgqKiqwZMkSHD9+HLt378aOHTuc0UYiIlIQT43x52pLu4JbS+gw5iFr7M7cDBkyBIcPH0ZFRQWioqKwefNmhISEIDExEX379nVGG4mISEEq9Hqj761NBWc9MdVFnRao6dChAz755BNHt4WIiBqBcp1x3sXWbA2RrezO3BAREdVHuc44c6O2lLlRWQl2OC5FVticuVGr1bXOhlKpVKioqKh3o4iISLlqBjfmry3mN14gqp3Nwc3atWst3peYmIh3330XepNxVCIiIlNlFdZqbrhCMdWfzcHNxIkTaxw7deoUnn32Wfz000+455578PLLLzu0cUREpDw1am6sxDOW7uI6N2RNnWpu0tLS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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import matplotlib.pyplot as plt\n", "\n", @@ -380,7 +422,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "id": "LqqHzjty8jk0" }, @@ -400,7 +442,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "id": "g4MeM8Oe9Q6X" }, @@ -440,14 +482,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "id": "x95ZgBkyDMP4" }, "outputs": [], "source": [ - "bf.options.sampling.enable_downsampling = True # enable downsampling\n", - "bf.options.sampling.max_download_size = 5 # download only 5 mb of data" + "bpd.options.sampling.enable_downsampling = True # enable downsampling\n", + "bpd.options.sampling.max_download_size = 5 # download only 5 mb of data" ] }, { @@ -461,11 +503,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "id": "V0OK02D7PJSL" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 120a989f-4ce0-47e9-b051-a1a570ecd0e3 is DONE. 12.6 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "local_symptom_data = symptom_data.to_pandas(sampling_method=\"uniform\")" ] @@ -493,11 +548,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": { "id": "EG7qM3R18bOb" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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kJeXhwceeABTp07F6tWrAQAnTpzAyZMn8c477+Bf//oX3njjDezZswfXXntt15+wW8tUEkJizowZM5jBYGB/+MMf2LFjx9ibb77JOI7zrYI8a9YsdvXVV7Pvv/+eHTt2jN1///0sKSmJNTQ0MMYYKywsZM888wxjjLGSkhKWmJjIlEqlb3X222+/nd18883nbYfFYmGDBw9m06ZNY19//TU7fvw4++9//8t27NjBGGNs/fr1TKFQsJdeeokdPXqU/fWvf2UymYx9+eWXvnMAYNnZ2WzNmjXs+PHj7De/+Q3T6/WsoaGBeb1eVl1dzQwGA1u5ciWrrq5mdrvdtwpwU1MTY4yx1atXM4VCwaZMmcK2b9/Ojhw5wmw2GxswYABLTExkL7/8Mjt27Bi76667mMFgYHPmzGHr1q1jR48eZfPnz2cjR45koigyxhgrLS1lOp2OPffcc+zYsWNs+/btrKCggN16662+Ns+dO5eNGzeO7dy5k+3evZtNmTKFaTQa9txzz3XvD0tIBAFgGzZs8D3+6KOPGACm0+n8fuRyObvuuusYY4zdcccdDIDfKvN79uxhANiRI0e69vxheRWEkJgxY8YMNnXqVL9tF154IXvwwQfZ119/zQwGA3M6nX778/Ly2CuvvMIYY2zJkiXsqquuYowxtnLlSnb99dezcePGsU8//ZQxxtiQIUPY//3f/523Ha+88gqLi4vzBUnnmjJlCrvjjjv8tv34xz9mV155pe8xAPboo4/6HlutVgbA1xbGGDMajWz16tW+x8GCGQCspKTE77kGDBjAfvKTn/geV1dXMwDsscce823buXMnA8Cqq6sZY4zddttt7Je//KXfeb7++mvG8zxzOBzs6NGjDAD77rvvfPsPHz7MAFAwQ2LaucHM2rVrmUwmY0eOHGHHjx/3+2n99/L4448zuVzudx673c4A+G6uOosSgAnph/Lz8/0eZ2RkoK6uDnv37oXVakVSUpLffofDgbKyMgDAjBkz8Prrr0MQBGzbtg1XXHEF0tPTsXXrVuTn56O0tNSX4NeRkpISFBQUIDExMej+w4cP45e//KXftosvvhjPP/98u69Fp9PBYDCgrq7uvM/fllKpDHhPzj13WloaAGDs2LEB2+rq6pCeno69e/di3759fkNHjDGIoojy8nIcO3YMcrkcEyZM8O0fMWIE4uPju9ReQqJdQUEBBEFAXV0dpk2bFvSYiy++GF6vF2VlZcjLywMAHDt2DAAwYMCALj0fBTOE9EMKhcLvMcdxEEURVqsVGRkZ2Lp1a8DvtF5wp0+fDovFgqKiInz11Vd46qmnkJ6ejhUrVmDcuHHIzMzE0KFDz9sGjUYTjpfS7mvpCo1GEzQhuO25W/cH29b6fFarFb/61a/wm9/8JuBcubm5vi9qQvoCq9WK0tJS3+Py8nKUlJQgMTERw4YNw80334yf/exn+Otf/4qCggKcOXMGW7ZsQX5+Pq666irMmjULhYWF+MUvfoGVK1dCFEUsXrwYl19+OYYNG9altlACMCHEp7CwEDU1NZDL5RgyZIjfT3JyMgApqMnPz8eLL74IhUKBESNGYPr06SguLsZHH32EGTNmdOq58vPzUVJS0u7MhZEjR2L79u1+27Zv345Ro0Z170X2oMLCQhw6dCjgvRsyZAiUSiVGjBgBr9eLPXv2+H7n6NGjUVXzhpDO2r17NwoKClBQUABAKoNQUFCAxx9/HACwevVq/OxnP8P999+P4cOHY/78+fj++++Rm5sLAOB5Hh9++CGSk5Mxffp0XHXVVRg5ciTWrl3b5bZQzwwhxGfWrFmYPHky5s+fj7/85S8YNmwYqqqq8PHHH2PBggW44IILAEgzol544QXfrIPExESMHDkS//3vfztdQOvGG2/EU089hfnz52P58uXIyMhAcXExMjMzMXnyZPzud7/Dddddh4KCAsyaNQsffvgh1q9fjy+++KLHXn93Pfjgg7joootw99134/bbb4dOp8OhQ4fw+eef48UXX8Tw4cMxZ84c/OpXv8KqVasgl8tx7733hq2XipDeNHPmTEjpMsEpFAosW7YMy5Yta/eYzMxMvPfee91uC/XMEEJ8OI7DJ598gunTp+PnP/85hg0bhhtuuAEnT5705YcAUt6MIAh+uTEzZ84M2NYRpVKJzz77DKmpqbjyyisxduxYrFixAjKZDAAwf/58PP/883j22WcxevRovPLKK1i9enWnzx8J+fn52LZtG44dO4Zp06b57lIzMzN9x6xevRqZmZmYMWMGFi5ciF/+8pdITU2NYKsJiX0c6yisIoQQQgiJctQzQwghhJCYRsEMIaRHPPXUU9Dr9UF/OlrPhRBCuoqGmQghPaKxsbHdmUoajQZZWVm93CJCSF9FwQwhhBBCYhoNMxFCCCEkplEwQwghhJCYRsEMIYQQQmIaBTOEEEIIiWkUzBBCCCEkplEwQwghhJCYRsEMIYQQQmLa/wNhCyDCMi8TLQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import seaborn as sns\n", "\n", @@ -513,11 +589,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": { "id": "5nVy61rEGaM4" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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aW1tjZ9z8803izVr1ojdu3eLjz76SMyfP18UFRWJxsbGlMfJYGaAaW5uFhdffLHIy8sTdrtdXHHFFfELkRBC7N69WwAQ//nPf4QQQvznP/8RALr82r17txAiUt593HHHiby8PGG1WsW0adPEH//4R6GqagYe4ZE98sgjYuTIkcJgMIgTTzxRfPzxx/Hb5s2bJy677LKE81988UUxceJEYTAYxOTJk8U///nPhNs1TRN33nmnKCkpEUajUZx++uli+/btR+OhpEVvno9Ro0Z1+Vq4++67hRBCeL1eccYZZ4ihQ4cKvV4vRo0aJa6++uqceWMWonfPx4033hg/t6SkRJx11lkJ/TKEGFyvDyGE2LZtmwAg3n333U73leuvj+7eD2PPwWWXXSbmzZvX6WeOO+44YTAYxNixYxN67sT09Bxnu94+J/PmzevxfCEi5etlZWXCYDCIYcOGiQsvvFBUV1f3aZzcNZuIiIhyGnNmiIiIKKcxmCEiIqKcxmCGiIiIchqDGSIiIsppDGaIiIgopzGYISIiopzGYIaIiIhyGoMZIiIi6rX3338f55xzDsrLyyFJEl599dVe34cQAg8++CAmTpwIo9GIYcOG4b777uv1/TCYIaIB7aOPPsKUKVOg1+uxaNEirFmzBpIkobW1NdNDixs9ejRWrFiR6WEQ9UpbWxumTZuGRx99NOX7+PnPf44nn3wSDz74ILZt24bXX3+9007sydClPAIiohywZMkSHHfccXjrrbeQl5cHi8WCuro6OByOTA+NKKedeeaZOPPMM7u9PRAI4Pbbb8dzzz2H1tZWHHvssfjNb36DU089FQBQVVWFlStX4uuvv8akSZMARDZvTQVnZohoQKupqcFpp52G4cOHIz8/HwaDAaWlpZAkqcvzVVWFpmlHeZREA8/PfvYzrF+/Hs8//zy++uor/Nd//RcWLlyInTt3AgDeeOMNjB07Fv/4xz8wZswYjB49GldddRVaWlp6/bsYzBANMqeeeipuuOEG/PKXv0RhYSFKS0txzz33xG9vbW3FVVddhaFDh8Jut+O0007Dl19+CQBwOp1QFAWff/45AEDTNBQWFmLWrFnxn/+///s/jBgxIqmx7N+/HxdffDEKCwthtVoxY8YMfPLJJ/HbV65ciXHjxsFgMGDSpEn461//mvDzkiThySefxHnnnQeLxYIJEybg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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# similarly, for fever\n", "\n", @@ -528,11 +625,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": { "id": "-S1A9E3WGaYH" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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RMwWoZGkxm8cq1atwpw5cwD4J81btGgR3nzzTbhcLkybNg1/+MMfmmxmOpvdbofVaoXNZmOTExERkUa05v4d02SmIzCZISIi0p7W3L87RQdgIiIiokgxmSEiIiJNYzJDREREmsZkhoiIiDSNyQwRERFpGpMZIiIi0jQmM0RERKRpTGaIiIhI05jMEBERkaYxmSEiIiJNYzJDREREmhZxMlNZWYlXX30VixcvRkVFBQBgx44dOH78eNSCIyIiImqJLpIXffvtt5gyZQqsVit++OEHzJs3D8nJyXjnnXdw5MgRvPbaa9GOk4iIiKhREdXMLFy4EHPmzMGhQ4dgMpmC2y+77DJ89tlnUQuOiIiIqCURJTPffPMN7rjjjgbbe/fujdLS0jYHRURERBSuiJIZo9EIu93eYPvBgwfRs2fPNgdFREREFK6Ikpkrr7wSjz76KDweDwBAkiQcOXIE9913H6655pqoBkhERETUnIiSmWeffRYOhwNpaWmoqanBpEmTMGjQICQmJuJ///d/ox0jERERUZMiGs1ktVrxySefYPPmzdi9ezccDgfOO+88TJkyJdrxERERETUromQmYPz48Rg/fjwA/7wzRERERB0tomamJ598En//+9+Dz6+99lqkpKSgd+/e2L17d9SCIyIiImpJRMnMH//4R2RlZQEAPvnkE3zyySf48MMPMWPGDPz617+OaoBEREREzYmomam0tDSYzLz//vu49tprMXXqVPTv3x9jxoyJaoBEREREzYmoZqZHjx44evQoAGD9+vXBjr9CCPh8vuhFR0RERNSCiGpmrr76atxwww0YPHgwysvLMWPGDADAzp07MWjQoKgGSERERNSciJKZ559/Hv3798fRo0fx1FNPISEhAQBQUlKCX/7yl1ENkIiIiKg5khBCxDqI9mS322G1WmGz2WCxWGIdDhEREYWhNffvsGtm1q1bhxkzZkCv12PdunXNlr3yyivDPSwRERFRm4RdMyPLMkpLS5GWlgZZbrrfsCRJnaoTMGtmiIiItKddamZUVW3070RERESxFNHQbCIiIqLOIqLRTI8++miz+x9++OGIgiEiIiJqrYiSmbVr14Y893g8KCoqgk6nQ3Z2NpMZIiIi6jARJTM7d+5ssM1ut2POnDmYNWtWm4MiIiIiClfU+sxYLBYsW7YMDz30ULQOSURERNSiqHYA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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# similarly, for bruise\n", "local_symptom_data[\"search_trends_bruise\"] = \\\n", @@ -590,7 +708,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.11.1" } }, "nbformat": 4, From 8f6e955fc946db97c95ea012659432355b0cd12c Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:56:50 -0700 Subject: [PATCH 372/921] feat: support Series.dt.strftime (#453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support Series.dt.strftime * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * address comments * fix imports * 🦉 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 --- bigframes/core/compile/scalar_op_compiler.py | 9 ++++ bigframes/operations/__init__.py | 9 ++++ bigframes/operations/datetimes.py | 8 +++- .../system/small/operations/test_datetimes.py | 47 +++++++++++++++++++ .../pandas/core/arrays/datetimelike.py | 38 +++++++++++++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 67761c03302..c95d1ca45e0 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -613,6 +613,15 @@ def second_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.TimestampValue, x).second().cast(ibis_dtypes.int64) +@scalar_op_compiler.register_unary_op(ops.StrftimeOp, pass_op=True) +def strftime_op_impl(x: ibis_types.Value, op: ops.StrftimeOp): + return ( + typing.cast(ibis_types.TimestampValue, x) + .strftime(op.date_format) + .cast(ibis_dtypes.str) + ) + + @scalar_op_compiler.register_unary_op(ops.time_op) def time_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.TimestampValue, x).time() diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 37188e490ee..c358d46ee16 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -415,6 +415,15 @@ def output_type(self, *input_types): return input_types[0] +@dataclasses.dataclass(frozen=True) +class StrftimeOp(UnaryOp): + name: typing.ClassVar[str] = "strftime" + date_format: str + + def output_type(self, *input_types): + return dtypes.STRING_DTYPE + + # Binary Ops fillna_op = create_binary_op(name="fillna") cliplower_op = create_binary_op(name="clip_lower") diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py index 66ec347add9..eb91bc0b201 100644 --- a/bigframes/operations/datetimes.py +++ b/bigframes/operations/datetimes.py @@ -17,6 +17,7 @@ import datetime as dt from typing import Optional +import bigframes_vendored.pandas.core.arrays.datetimelike as vendored_pandas_datetimelike import bigframes_vendored.pandas.core.indexes.accessor as vendordt from bigframes.core import log_adapter @@ -27,7 +28,9 @@ @log_adapter.class_logger class DatetimeMethods( - bigframes.operations.base.SeriesMethods, vendordt.DatetimeProperties + bigframes.operations.base.SeriesMethods, + vendordt.DatetimeProperties, + vendored_pandas_datetimelike.DatelikeOps, ): __doc__ = vendordt.DatetimeProperties.__doc__ @@ -88,3 +91,6 @@ def tz(self) -> Optional[dt.timezone]: def unit(self) -> str: # Assumption: pyarrow dtype return self._dtype.pyarrow_dtype.unit + + def strftime(self, date_format: str) -> series.Series: + return self._apply_unary_op(ops.StrftimeOp(date_format=date_format)) diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 3882491ecb7..854672585de 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -219,3 +219,50 @@ def test_dt_unit(scalars_dfs, col_name): pd_result = scalars_pandas_df[col_name].dt.unit assert bf_result == pd_result + + +@pytest.mark.parametrize( + ("column", "date_format"), + [ + ("timestamp_col", "%B %d, %Y, %r"), + ("timestamp_col", "%m-%d-%Y %H:%M"), + ("datetime_col", "%m-%d-%Y %H:%M"), + ("datetime_col", "%H:%M"), + ], +) +@skip_legacy_pandas +def test_dt_strftime(scalars_df_index, scalars_pandas_df_index, column, date_format): + bf_result = scalars_df_index[column].dt.strftime(date_format).to_pandas() + pd_result = scalars_pandas_df_index[column].dt.strftime(date_format) + pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + assert bf_result.dtype == "string[pyarrow]" + + +def test_dt_strftime_date(): + bf_series = bigframes.series.Series( + ["2014-08-15", "2215-08-15", "2016-02-29"] + ).astype("date32[day][pyarrow]") + + expected_result = pd.Series(["08/15/2014", "08/15/2215", "02/29/2016"]) + bf_result = bf_series.dt.strftime("%m/%d/%Y").to_pandas() + + pd.testing.assert_series_equal( + bf_result, expected_result, check_index_type=False, check_dtype=False + ) + assert bf_result.dtype == "string[pyarrow]" + + +def test_dt_strftime_time(): + bf_series = bigframes.series.Series( + [143542314, 345234512341, 75543252344, 626546437654754, 8543523452345234] + ).astype("time64[us][pyarrow]") + + expected_result = pd.Series( + ["00:02:23", "23:53:54", "20:59:03", "16:40:37", "08:57:32"] + ) + bf_result = bf_series.dt.strftime("%X").to_pandas() + + pd.testing.assert_series_equal( + bf_result, expected_result, check_index_type=False, check_dtype=False + ) + assert bf_result.dtype == "string[pyarrow]" diff --git a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py new file mode 100644 index 00000000000..4f7e33909e4 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py @@ -0,0 +1,38 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/arrays/datetimelike.py + +from bigframes import constants + + +class DatelikeOps: + def strftime(self, date_format: str): + """ + Convert to string Series using specified date_format. + + Return a Series of formatted strings specified by date_format. Details + of the string format can be found in `BigQuery format elements doc + <%(https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements)s>`__. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.to_datetime( + ... ['2014-08-15 08:15:12', '2012-02-29 08:15:12+06:00', '2015-08-15 08:15:12+05:00'], + ... utc=True + ... ).astype("timestamp[us, tz=UTC][pyarrow]") + + >>> s.dt.strftime("%B %d, %Y, %r") + 0 August 15, 2014, 08:15:12 AM + 1 February 29, 2012, 02:15:12 AM + 2 August 15, 2015, 03:15:12 AM + Name: 0, dtype: string + + Args: + date_format (str): + Date format string (e.g. "%Y-%m-%d"). + + Returns: + bigframes.series.Series of formatted strings. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 718a00c1fa8ac44b0d3a79a2217e5b12690785fb Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:04:53 -0700 Subject: [PATCH 373/921] fix: series.(to_csv|to_json) leverages bq export (#452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Sweña (Swast) --- bigframes/dataframe.py | 12 ++--- bigframes/series.py | 20 ++++--- tests/system/small/test_series.py | 28 +++++++--- .../bigframes_vendored/pandas/core/generic.py | 4 +- .../bigframes_vendored/pandas/core/series.py | 53 ------------------- 5 files changed, 40 insertions(+), 77 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 5dae7a82f93..e8328b6047c 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2588,16 +2588,16 @@ def to_json( if "*" not in path_or_buf: raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) - if lines is True and orient != "records": - raise ValueError( - "'lines' keyword is only valid when 'orient' is 'records'." - ) - # TODO(ashleyxu) Support lines=False for small tables with arrays and TO_JSON_STRING. # See: https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#to_json_string if lines is False: raise NotImplementedError( - f"Only newline delimited JSON format is supported. {constants.FEEDBACK_LINK}" + f"Only newline-delimited JSON is supported. Add `lines=True` to your function call. {constants.FEEDBACK_LINK}" + ) + + if lines is True and orient != "records": + raise ValueError( + "'lines' keyword is only valid when 'orient' is 'records'." ) result_table = self._run_io_query( diff --git a/bigframes/series.py b/bigframes/series.py index ef2feb4f92d..86afdd047c0 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1390,9 +1390,10 @@ def to_frame(self, name: blocks.Label = None) -> bigframes.dataframe.DataFrame: ) return bigframes.dataframe.DataFrame(block) - def to_csv(self, path_or_buf=None, **kwargs) -> typing.Optional[str]: - # TODO(b/280651142): Implement version that leverages bq export native csv support to bypass local pandas step. - return self.to_pandas().to_csv(path_or_buf, **kwargs) + def to_csv( + self, path_or_buf: str, sep=",", *, header: bool = True, index: bool = True + ) -> None: + return self.to_frame().to_csv(path_or_buf, sep=sep, header=header, index=index) def to_dict(self, into: type[dict] = dict) -> typing.Mapping: return typing.cast(dict, self.to_pandas().to_dict(into)) # type: ignore @@ -1402,14 +1403,17 @@ def to_excel(self, excel_writer, sheet_name="Sheet1", **kwargs) -> None: def to_json( self, - path_or_buf=None, + path_or_buf: str, orient: typing.Literal[ "split", "records", "index", "columns", "values", "table" ] = "columns", - **kwargs, - ) -> typing.Optional[str]: - # TODO(b/280651142): Implement version that leverages bq export native csv support to bypass local pandas step. - return self.to_pandas().to_json(path_or_buf, **kwargs) + *, + lines: bool = False, + index: bool = True, + ) -> None: + return self.to_frame().to_json( + path_or_buf=path_or_buf, orient=orient, lines=lines, index=index + ) def to_latex( self, buf=None, columns=None, header=True, index=True, **kwargs diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index e22037a1ce0..584dc219560 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2384,18 +2384,30 @@ def test_to_frame(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) -def test_to_json(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index["int64_col"].to_json() - pd_result = scalars_pandas_df_index["int64_col"].to_json() +def test_to_json(gcs_folder, scalars_df_index, scalars_pandas_df_index): + path = gcs_folder + "test_series_to_json*.jsonl" + scalars_df_index["int64_col"].to_json(path, lines=True, orient="records") + gcs_df = pd.read_json(path, lines=True) - assert bf_result == pd_result + pd.testing.assert_series_equal( + gcs_df["int64_col"].astype(pd.Int64Dtype()), + scalars_pandas_df_index["int64_col"], + check_dtype=False, + check_index=False, + ) -def test_to_csv(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index["int64_col"].to_csv() - pd_result = scalars_pandas_df_index["int64_col"].to_csv() +def test_to_csv(gcs_folder, scalars_df_index, scalars_pandas_df_index): + path = gcs_folder + "test_series_to_csv*.csv" + scalars_df_index["int64_col"].to_csv(path) + gcs_df = pd.read_csv(path) - assert bf_result == pd_result + pd.testing.assert_series_equal( + gcs_df["int64_col"].astype(pd.Int64Dtype()), + scalars_pandas_df_index["int64_col"], + check_dtype=False, + check_index=False, + ) def test_to_latex(scalars_df_index, scalars_pandas_df_index): diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 7f8e1f7b53e..9358dca17b0 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -183,7 +183,7 @@ def to_json( *, index: bool = True, lines: bool = False, - ) -> str | None: + ) -> None: """Convert the object to a JSON string, written to Cloud Storage. Note NaN's and None will be converted to null and datetime objects @@ -241,7 +241,7 @@ def to_json( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: + def to_csv(self, path_or_buf: str, *, index: bool = True) -> None: """Write object to a comma-separated values (csv) file on Cloud Storage. Args: diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index ab96e731b9f..6a4c7f0ad57 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -535,59 +535,6 @@ def to_xarray(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def to_json( - self, - path_or_buf=None, - orient: Literal[ - "split", "records", "index", "columns", "values", "table" - ] = "columns", - **kwarg, - ) -> str | None: - """ - Convert the object to a JSON string. - - Note NaN's and None will be converted to null and datetime objects - will be converted to UNIX timestamps. - - Args: - path_or_buf (str, path object, file-like object, or None, default None): - String, path object (implementing os.PathLike[str]), or file-like - object implementing a write() function. If None, the result is - returned as a string. - orient ({"split", "records", "index", "columns", "values", "table"}, default "columns"): - Indication of expected JSON string format. - 'split' : dict like {{'index' -> [index], 'columns' -> [columns],'data' -> [values]}} - 'records' : list like [{{column -> value}}, ... , {{column -> value}}] - 'index' : dict like {{index -> {{column -> value}}}} - 'columns' : dict like {{column -> {{index -> value}}}} - 'values' : just the values array - 'table' : dict like {{'schema': {{schema}}, 'data': {{data}}}} - Describing the data, where data component is like ``orient='records'``. - - Returns: - None or str: If path_or_buf is None, returns the resulting json format as a - string. Otherwise returns None. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - - def to_csv(self, path_or_buf: str, *, index: bool = True) -> str | None: - """ - Write object to a comma-separated values (csv) file. - - Args: - path_or_buf (str, path object, file-like object, or None, default None): - String, path object (implementing os.PathLike[str]), or file-like - object implementing a write() function. If None, the result is - returned as a string. If a non-binary file object is passed, it should - be opened with `newline=''`, disabling universal newlines. If a binary - file object is passed, `mode` might need to contain a `'b'`. - - Returns: - None or str: If path_or_buf is None, returns the resulting csv format - as a string. Otherwise returns None. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def agg(self, func): """ Aggregate using one or more operations over the specified axis. From d2d425a93aa9e96f3b71c3ca3b185f4b5eaf32ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 18 Mar 2024 15:12:21 -0500 Subject: [PATCH 374/921] feat: `read_gbq_table` supports `LIKE` as a operator in `filters` (#454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 330149095 🦕 --- bigframes/session/__init__.py | 1 + tests/system/small/test_session.py | 12 ++++++++++++ third_party/bigframes_vendored/pandas/io/gbq.py | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 40831292de8..4cb3c11859a 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -318,6 +318,7 @@ def _to_query( valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { "in": "IN", "not in": "NOT IN", + "LIKE": "LIKE", "==": "=", ">": ">", "<": "<", diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index aba4a52c437..d0cd24e2beb 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -327,6 +327,18 @@ def test_read_gbq_twice_with_same_timestamp(session, penguins_table_id): assert df3 is not None +def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): + df = session.read_gbq_table( + "bigquery-public-data.cloud_storage_geo_index.landsat_index", + filters=[[("sensor_id", "LIKE", "OLI%")], [("sensor_id", "LIKE", "%TIRS")]], # type: ignore + columns=["sensor_id"], + ) + sensors = df.groupby(["sensor_id"]).agg("count").to_pandas(ordered=False) + assert "OLI" in sensors.index + assert "TIRS" in sensors.index + assert "OLI_TIRS" in sensors.index + + def test_read_gbq_wildcard(session: bigframes.Session): df = session.read_gbq("bigquery-public-data.noaa_gsod.gsod193*") assert df.shape == (348485, 32) diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 1f31c530d27..74602b5af15 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -7,7 +7,7 @@ from bigframes import constants -FilterOps = Literal["in", "not in", "<", "<=", "==", "!=", ">=", ">"] +FilterOps = Literal["in", "not in", "<", "<=", "==", "!=", ">=", ">", "LIKE"] FilterType = Tuple[str, FilterOps, Any] FiltersType = Union[Iterable[FilterType], Iterable[Iterable[FilterType]]] @@ -112,7 +112,7 @@ def read_gbq( query results. filters (Union[Iterable[FilterType], Iterable[Iterable[FilterType]]], default ()): To filter out data. Filter syntax: [[(column, op, val), …],…] where - op is [==, >, >=, <, <=, !=, in, not in]. The innermost tuples + op is [==, >, >=, <, <=, !=, in, not in, LIKE]. The innermost tuples are transposed into a set of filters applied through an AND operation. The outer Iterable combines these sets of filters through an OR operation. A single Iterable of tuples can also From 874026da612bf08fbaf6d7dbfaa3325dc8a61500 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:50:07 -0700 Subject: [PATCH 375/921] fix: disable to_json and to_csv related tests (#462) * fix: disable series to_json and to_csv tests * disable the rest ones --- tests/system/small/test_dataframe_io.py | 3 +++ tests/system/small/test_series.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index f9a0d2481b0..adc729565e6 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -115,6 +115,7 @@ def test_to_pandas_batches_w_correct_dtypes(scalars_df_default_index): pd.testing.assert_series_equal(actual, expected) +@pytest.mark.skip(reason="Disable to unblock kokoro tests") @pytest.mark.parametrize( ("index"), [True, False], @@ -163,6 +164,7 @@ def test_to_csv_index( pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) +@pytest.mark.skip(reason="Disable to unblock kokoro tests") def test_to_csv_tabs( scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], gcs_folder: str, @@ -413,6 +415,7 @@ def test_to_json_index_invalid_lines( scalars_df.to_json(path, index=index) +@pytest.mark.skip(reason="Disable to unblock kokoro tests") @pytest.mark.parametrize( ("index"), [True, False], diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 584dc219560..4ce3dcfe2c4 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2384,6 +2384,7 @@ def test_to_frame(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) +@pytest.mark.skip(reason="Disable to unblock kokoro tests") def test_to_json(gcs_folder, scalars_df_index, scalars_pandas_df_index): path = gcs_folder + "test_series_to_json*.jsonl" scalars_df_index["int64_col"].to_json(path, lines=True, orient="records") @@ -2397,6 +2398,7 @@ def test_to_json(gcs_folder, scalars_df_index, scalars_pandas_df_index): ) +@pytest.mark.skip(reason="Disable to unblock kokoro tests") def test_to_csv(gcs_folder, scalars_df_index, scalars_pandas_df_index): path = gcs_folder + "test_series_to_csv*.csv" scalars_df_index["int64_col"].to_csv(path) From 6611c2803f174edcf11c5606c56f3ec36c4ae3e5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 18 Mar 2024 18:50:20 -0700 Subject: [PATCH 376/921] refactor: Reimplement implicit joiner at expression layer (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 16 +- bigframes/core/compile/compiler.py | 2 - bigframes/core/compile/row_identity.py | 265 --------------------- bigframes/core/compile/single_column.py | 218 +++++++---------- bigframes/core/expression.py | 22 +- bigframes/core/ordering.py | 29 ++- bigframes/core/rewrite.py | 299 ++++++++++++++++++++++++ bigframes/operations/__init__.py | 6 +- tests/system/small/test_dataframe.py | 14 +- 9 files changed, 462 insertions(+), 409 deletions(-) delete mode 100644 bigframes/core/compile/row_identity.py create mode 100644 bigframes/core/rewrite.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index e4a60e08e1e..02582b17ba6 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -28,6 +28,7 @@ import bigframes.core.nodes as nodes from bigframes.core.ordering import OrderingColumnReference import bigframes.core.ordering as orderings +import bigframes.core.rewrite import bigframes.core.utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes @@ -351,14 +352,15 @@ def join( join_def: join_def.JoinDefinition, allow_row_identity_join: bool = False, ): - return ArrayValue( - nodes.JoinNode( - left_child=self.node, - right_child=other.node, - join=join_def, - allow_row_identity_join=allow_row_identity_join, - ) + join_node = nodes.JoinNode( + left_child=self.node, + right_child=other.node, + join=join_def, + allow_row_identity_join=allow_row_identity_join, ) + if allow_row_identity_join: + return ArrayValue(bigframes.core.rewrite.maybe_rewrite_join(join_node)) + return ArrayValue(join_node) def _uniform_sampling(self, fraction: float) -> ArrayValue: """Sampling the table on given fraction. diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 4ced85352c5..ec6c79db5f5 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -66,7 +66,6 @@ def compile_join(node: nodes.JoinNode, ordered: bool = True): left=left_ordered, right=right_ordered, join=node.join, - allow_row_identity_join=node.allow_row_identity_join, ) else: left_unordered = compile_unordered_ir(node.left_child) @@ -75,7 +74,6 @@ def compile_join(node: nodes.JoinNode, ordered: bool = True): left=left_unordered, right=right_unordered, join=node.join, - allow_row_identity_join=node.allow_row_identity_join, ) diff --git a/bigframes/core/compile/row_identity.py b/bigframes/core/compile/row_identity.py deleted file mode 100644 index f46e2f94638..00000000000 --- a/bigframes/core/compile/row_identity.py +++ /dev/null @@ -1,265 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Helpers to join ArrayValue objects.""" - -from __future__ import annotations - -import functools -import typing - -import ibis -import ibis.expr.types as ibis_types - -import bigframes.constants as constants -import bigframes.core.compile.compiled as compiled -import bigframes.core.join_def as join_def -import bigframes.core.joins as joining -import bigframes.core.ordering as orderings - -SUPPORTED_ROW_IDENTITY_HOW = {"outer", "left", "inner"} - - -def join_by_row_identity_unordered( - left: compiled.UnorderedIR, - right: compiled.UnorderedIR, - join_def: join_def.JoinDefinition, -) -> compiled.UnorderedIR: - """Compute join when we are joining by row identity not a specific column.""" - if join_def.type not in SUPPORTED_ROW_IDENTITY_HOW: - raise NotImplementedError( - f"Only how='outer','left','inner' currently supported. {constants.FEEDBACK_LINK}" - ) - - if not left._table.equals(right._table): - raise ValueError( - "Cannot combine objects without an explicit join/merge key. " - f"Left based on: {left._table.compile()}, but " - f"right based on: {right._table.compile()}" - ) - - left_predicates = left._predicates - right_predicates = right._predicates - # TODO(tbergeron): Skip generating these for inner part of join - ( - left_relative_predicates, - right_relative_predicates, - ) = _get_relative_predicates(left_predicates, right_predicates) - - combined_predicates = [] - if left_predicates or right_predicates: - joined_predicates = _join_predicates( - left_predicates, right_predicates, join_type=join_def.type - ) - combined_predicates = list(joined_predicates) # builder expects mutable list - - left_mask = ( - left_relative_predicates if join_def.type in ["right", "outer"] else None - ) - right_mask = ( - right_relative_predicates if join_def.type in ["left", "outer"] else None - ) - - # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - map_left_id = join_def.get_left_mapping() - map_right_id = join_def.get_right_mapping() - joined_columns = [ - _mask_value(left._get_ibis_column(key), left_mask).name(map_left_id[key]) - for key in left.column_ids - ] + [ - _mask_value(right._get_ibis_column(key), right_mask).name(map_right_id[key]) - for key in right.column_ids - ] - - joined_expr = compiled.UnorderedIR( - left._table, - columns=joined_columns, - predicates=combined_predicates, - ) - return joined_expr - - -def join_by_row_identity_ordered( - left: compiled.OrderedIR, - right: compiled.OrderedIR, - join_def: join_def.JoinDefinition, -) -> compiled.OrderedIR: - """Compute join when we are joining by row identity not a specific column.""" - if join_def.type not in SUPPORTED_ROW_IDENTITY_HOW: - raise NotImplementedError( - f"Only how='outer','left','inner' currently supported. {constants.FEEDBACK_LINK}" - ) - - if not left._table.equals(right._table): - raise ValueError( - "Cannot combine objects without an explicit join/merge key. " - f"Left based on: {left._table.compile()}, but " - f"right based on: {right._table.compile()}" - ) - - left_predicates = left._predicates - right_predicates = right._predicates - # TODO(tbergeron): Skip generating these for inner part of join - ( - left_relative_predicates, - right_relative_predicates, - ) = _get_relative_predicates(left_predicates, right_predicates) - - combined_predicates = [] - if left_predicates or right_predicates: - joined_predicates = _join_predicates( - left_predicates, right_predicates, join_type=join_def.type - ) - combined_predicates = list(joined_predicates) # builder expects mutable list - - left_mask = ( - left_relative_predicates if join_def.type in ["right", "outer"] else None - ) - right_mask = ( - right_relative_predicates if join_def.type in ["left", "outer"] else None - ) - - # Public mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - lpublicmapping = join_def.get_left_mapping() - rpublicmapping = join_def.get_right_mapping() - lhiddenmapping, rhiddenmapping = joining.JoinNameRemapper(namespace="hidden")( - left._hidden_column_ids, right._hidden_column_ids - ) - map_left_id = {**lpublicmapping, **lhiddenmapping} - map_right_id = {**rpublicmapping, **rhiddenmapping} - - joined_columns = [ - _mask_value(left._get_ibis_column(key), left_mask).name(map_left_id[key]) - for key in left.column_ids - ] + [ - _mask_value(right._get_ibis_column(key), right_mask).name(map_right_id[key]) - for key in right.column_ids - ] - - # If left isn't being masked, can just use left ordering - if not left_mask: - col_mapping = { - order_ref.column_id: map_left_id[order_ref.column_id] - for order_ref in left._ordering.ordering_value_columns - } - new_ordering = left._ordering.with_column_remap(col_mapping) - else: - ordering_columns = [ - col_ref.with_name(map_left_id[col_ref.column_id]) - for col_ref in left._ordering.ordering_value_columns - ] + [ - col_ref.with_name(map_right_id[col_ref.column_id]) - for col_ref in right._ordering.ordering_value_columns - ] - left_total_order_cols = frozenset( - map_left_id[col] for col in left._ordering.total_ordering_columns - ) - # Assume that left ordering is sufficient since 1:1 join over same base table - join_total_order_cols = left_total_order_cols - new_ordering = orderings.ExpressionOrdering( - tuple(ordering_columns), total_ordering_columns=join_total_order_cols - ) - - hidden_ordering_columns = [ - left._get_hidden_ordering_column(key.column_id).name(map_left_id[key.column_id]) - for key in left._ordering.ordering_value_columns - if key.column_id in left._hidden_ordering_column_names.keys() - ] + [ - right._get_hidden_ordering_column(key.column_id).name( - map_right_id[key.column_id] - ) - for key in right._ordering.ordering_value_columns - if key.column_id in right._hidden_ordering_column_names.keys() - ] - - joined_expr = compiled.OrderedIR( - left._table, - columns=joined_columns, - hidden_ordering_columns=hidden_ordering_columns, - ordering=new_ordering, - predicates=combined_predicates, - ) - return joined_expr - - -def _mask_value( - value: ibis_types.Value, - predicates: typing.Optional[typing.Sequence[ibis_types.BooleanValue]] = None, -): - if predicates: - return ( - ibis.case() - .when(_reduce_predicate_list(predicates), value) - .else_(ibis.null()) - .end() - ) - return value - - -def _join_predicates( - left_predicates: typing.Collection[ibis_types.BooleanValue], - right_predicates: typing.Collection[ibis_types.BooleanValue], - join_type: str = "outer", -) -> typing.Tuple[ibis_types.BooleanValue, ...]: - """Combines predicates lists for each side of a join.""" - if join_type == "outer": - if not left_predicates: - return () - if not right_predicates: - return () - # TODO(tbergeron): Investigate factoring out common predicates - joined_predicates = _reduce_predicate_list(left_predicates).__or__( - _reduce_predicate_list(right_predicates) - ) - return (joined_predicates,) - if join_type == "left": - return tuple(left_predicates) - if join_type == "inner": - _, right_relative_predicates = _get_relative_predicates( - left_predicates, right_predicates - ) - return (*left_predicates, *right_relative_predicates) - else: - raise ValueError( - f"Unsupported join_type: {join_type}. {constants.FEEDBACK_LINK}" - ) - - -def _get_relative_predicates( - left_predicates: typing.Collection[ibis_types.BooleanValue], - right_predicates: typing.Collection[ibis_types.BooleanValue], -) -> tuple[ - typing.Tuple[ibis_types.BooleanValue, ...], - typing.Tuple[ibis_types.BooleanValue, ...], -]: - """Get predicates that apply to only one side of the join. Not strictly necessary but simplifies resulting query.""" - left_relative_predicates = tuple(left_predicates) or () - right_relative_predicates = tuple(right_predicates) or () - if left_predicates and right_predicates: - # Factor out common predicates needed for left/right column masking - left_relative_predicates = tuple(set(left_predicates) - set(right_predicates)) - right_relative_predicates = tuple(set(right_predicates) - set(left_predicates)) - return (left_relative_predicates, right_relative_predicates) - - -def _reduce_predicate_list( - predicate_list: typing.Collection[ibis_types.BooleanValue], -) -> ibis_types.BooleanValue: - """Converts a list of predicates BooleanValues into a single BooleanValue.""" - if len(predicate_list) == 0: - raise ValueError("Cannot reduce empty list of predicates") - if len(predicate_list) == 1: - (item,) = predicate_list - return item - return functools.reduce(lambda acc, pred: acc.__and__(pred), predicate_list) diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index 7beebfcb66c..31ebf87d176 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -23,7 +23,6 @@ import ibis.expr.types as ibis_types import bigframes.core.compile.compiled as compiled -import bigframes.core.compile.row_identity import bigframes.core.join_def as join_defs import bigframes.core.joins as joining import bigframes.core.ordering as orderings @@ -33,7 +32,6 @@ def join_by_column_ordered( left: compiled.OrderedIR, right: compiled.OrderedIR, join: join_defs.JoinDefinition, - allow_row_identity_join: bool = False, ) -> compiled.OrderedIR: """Join two expressions by column equality. @@ -51,90 +49,72 @@ def join_by_column_ordered( first the coalesced join keys, then, all the left columns, and finally, all the right columns. """ - if ( - allow_row_identity_join - and join.type in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW - and left._table.equals(right._table) - # Make sure we're joining on exactly the same column(s), at least with - # regards to value its possible that they both have the same names but - # were modified in different ways. Ignore differences in the names. - and all( - left._get_ibis_column(lcol) - .name("index") - .equals(right._get_ibis_column(rcol).name("index")) - for lcol, rcol in join.conditions - ) - ): - return bigframes.core.compile.row_identity.join_by_row_identity_ordered( - left, right, join_def=join - ) - else: - l_hidden_mapping, r_hidden_mapping = joining.JoinNameRemapper( - namespace="hidden" - )(left._hidden_column_ids, right._hidden_column_ids) - l_mapping = {**join.get_left_mapping(), **l_hidden_mapping} - r_mapping = {**join.get_right_mapping(), **r_hidden_mapping} - - left_table = left._to_ibis_expr( - ordering_mode="unordered", - expose_hidden_cols=True, - col_id_overrides=l_mapping, - ) - right_table = right._to_ibis_expr( - ordering_mode="unordered", - expose_hidden_cols=True, - col_id_overrides=r_mapping, - ) - join_conditions = [ - value_to_join_key(left_table[l_mapping[left_index]]) - == value_to_join_key(right_table[r_mapping[right_index]]) - for left_index, right_index in join.conditions - ] - - combined_table = ibis.join( - left_table, - right_table, - predicates=join_conditions, - how=join.type, # type: ignore - ) - - # Preserve ordering accross joins. - ordering = join_orderings( - left._ordering, - right._ordering, - l_mapping, - r_mapping, - left_order_dominates=(join.type != "right"), - ) - - # We could filter out the original join columns, but predicates/ordering - # might still reference them in implicit joins. - columns = [ - combined_table[l_mapping[col.get_name()]] for col in left.columns - ] + [combined_table[r_mapping[col.get_name()]] for col in right.columns] - hidden_ordering_columns = [ - *[ - combined_table[l_hidden_mapping[col.get_name()]] - for col in left._hidden_ordering_columns - ], - *[ - combined_table[r_hidden_mapping[col.get_name()]] - for col in right._hidden_ordering_columns - ], - ] - return compiled.OrderedIR( - combined_table, - columns=columns, - hidden_ordering_columns=hidden_ordering_columns, - ordering=ordering, - ) + + l_hidden_mapping, r_hidden_mapping = joining.JoinNameRemapper(namespace="hidden")( + left._hidden_column_ids, right._hidden_column_ids + ) + l_mapping = {**join.get_left_mapping(), **l_hidden_mapping} + r_mapping = {**join.get_right_mapping(), **r_hidden_mapping} + + left_table = left._to_ibis_expr( + ordering_mode="unordered", + expose_hidden_cols=True, + col_id_overrides=l_mapping, + ) + right_table = right._to_ibis_expr( + ordering_mode="unordered", + expose_hidden_cols=True, + col_id_overrides=r_mapping, + ) + join_conditions = [ + value_to_join_key(left_table[l_mapping[left_index]]) + == value_to_join_key(right_table[r_mapping[right_index]]) + for left_index, right_index in join.conditions + ] + + combined_table = ibis.join( + left_table, + right_table, + predicates=join_conditions, + how=join.type, # type: ignore + ) + + # Preserve ordering accross joins. + ordering = join_orderings( + left._ordering, + right._ordering, + l_mapping, + r_mapping, + left_order_dominates=(join.type != "right"), + ) + + # We could filter out the original join columns, but predicates/ordering + # might still reference them in implicit joins. + columns = [combined_table[l_mapping[col.get_name()]] for col in left.columns] + [ + combined_table[r_mapping[col.get_name()]] for col in right.columns + ] + hidden_ordering_columns = [ + *[ + combined_table[l_hidden_mapping[col.get_name()]] + for col in left._hidden_ordering_columns + ], + *[ + combined_table[r_hidden_mapping[col.get_name()]] + for col in right._hidden_ordering_columns + ], + ] + return compiled.OrderedIR( + combined_table, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=ordering, + ) def join_by_column_unordered( left: compiled.UnorderedIR, right: compiled.UnorderedIR, join: join_defs.JoinDefinition, - allow_row_identity_join: bool = False, ) -> compiled.UnorderedIR: """Join two expressions by column equality. @@ -152,54 +132,36 @@ def join_by_column_unordered( first the coalesced join keys, then, all the left columns, and finally, all the right columns. """ - if ( - allow_row_identity_join - and join.type in bigframes.core.compile.row_identity.SUPPORTED_ROW_IDENTITY_HOW - and left._table.equals(right._table) - # Make sure we're joining on exactly the same column(s), at least with - # regards to value its possible that they both have the same names but - # were modified in different ways. Ignore differences in the names. - and all( - left._get_ibis_column(lcol) - .name("index") - .equals(right._get_ibis_column(rcol).name("index")) - for lcol, rcol in join.conditions - ) - ): - return bigframes.core.compile.row_identity.join_by_row_identity_unordered( - left, right, join_def=join - ) - else: - # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - l_mapping = join.get_left_mapping() - r_mapping = join.get_right_mapping() - left_table = left._to_ibis_expr( - col_id_overrides=l_mapping, - ) - right_table = right._to_ibis_expr( - col_id_overrides=r_mapping, - ) - join_conditions = [ - value_to_join_key(left_table[l_mapping[left_index]]) - == value_to_join_key(right_table[r_mapping[right_index]]) - for left_index, right_index in join.conditions - ] - - combined_table = ibis.join( - left_table, - right_table, - predicates=join_conditions, - how=join.type, # type: ignore - ) - # We could filter out the original join columns, but predicates/ordering - # might still reference them in implicit joins. - columns = [ - combined_table[l_mapping[col.get_name()]] for col in left.columns - ] + [combined_table[r_mapping[col.get_name()]] for col in right.columns] - return compiled.UnorderedIR( - combined_table, - columns=columns, - ) + # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result + l_mapping = join.get_left_mapping() + r_mapping = join.get_right_mapping() + left_table = left._to_ibis_expr( + col_id_overrides=l_mapping, + ) + right_table = right._to_ibis_expr( + col_id_overrides=r_mapping, + ) + join_conditions = [ + value_to_join_key(left_table[l_mapping[left_index]]) + == value_to_join_key(right_table[r_mapping[right_index]]) + for left_index, right_index in join.conditions + ] + + combined_table = ibis.join( + left_table, + right_table, + predicates=join_conditions, + how=join.type, # type: ignore + ) + # We could filter out the original join columns, but predicates/ordering + # might still reference them in implicit joins. + columns = [combined_table[l_mapping[col.get_name()]] for col in left.columns] + [ + combined_table[r_mapping[col.get_name()]] for col in right.columns + ] + return compiled.UnorderedIR( + combined_table, + columns=columns, + ) def value_to_join_key(value: ibis_types.Value): diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index ec9e698412c..4c2ae461fd4 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -18,7 +18,7 @@ import dataclasses import itertools import typing -from typing import Union +from typing import Mapping, Union import bigframes.dtypes as dtypes import bigframes.operations @@ -81,6 +81,11 @@ def output_type( ) -> dtypes.ExpressionType: ... + @abc.abstractmethod + def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: + """Replace all variables with expression given in `bindings`.""" + ... + @dataclasses.dataclass(frozen=True) class ScalarConstantExpression(Expression): @@ -99,6 +104,9 @@ def output_type( ) -> dtypes.ExpressionType: return self.dtype + def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: + return self + @dataclasses.dataclass(frozen=True) class UnboundVariableExpression(Expression): @@ -128,6 +136,12 @@ def output_type( else: raise ValueError("Type of variable has not been fixed.") + def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: + if self.id in bindings.keys(): + return bindings[self.id] + else: + raise ValueError(f"Variable {self.id} remains unbound") + @dataclasses.dataclass(frozen=True) class OpExpression(Expression): @@ -163,3 +177,9 @@ def output_type( map(lambda x: x.output_type(input_types=input_types), self.inputs) ) return self.op.output_type(*operand_types) + + def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: + return OpExpression( + self.op, + tuple(input.bind_all_variables(bindings) for input in self.inputs), + ) diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 1fd5ab4e37b..bbfc7cf9d86 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -23,6 +23,8 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types +import bigframes.core.expression as expression + # TODO(tbergeron): Encode more efficiently ORDERING_ID_STRING_BASE: int = 10 # Sufficient to store any value up to 2^63 @@ -52,15 +54,38 @@ class OrderingColumnReference: direction: OrderingDirection = OrderingDirection.ASC na_last: bool = True - def with_name(self, name: str): + def with_name(self, name: str) -> OrderingColumnReference: return OrderingColumnReference(name, self.direction, self.na_last) - def with_reverse(self): + def with_reverse(self) -> OrderingColumnReference: return OrderingColumnReference( self.column_id, self.direction.reverse(), not self.na_last ) +@dataclass(frozen=True) +class OrderingExpression: + """ + An expression that defines a scalar value to order, a direction and a null behavior. Maps directly to ORDER BY expressions in GoogleSQL. + This is more of OrderingColumnReference which order on a previously projected column id instead of any scalar expression. + """ + + # TODO: Right now, expression trees requires projecting a value before it can be sorted on. If OrderByNode used this instead, we could avoid some such projections and simplify the tree. + scalar_expression: expression.Expression + direction: OrderingDirection = OrderingDirection.ASC + na_last: bool = True + + def remap_names(self, mapping: dict[str, str]) -> OrderingExpression: + return OrderingExpression( + self.scalar_expression.rename(mapping), self.direction, self.na_last + ) + + def with_reverse(self) -> OrderingExpression: + return OrderingExpression( + self.scalar_expression, self.direction.reverse(), not self.na_last + ) + + # Encoding classes specify additional properties for some ordering representations @dataclass(frozen=True) class StringEncoding: diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py new file mode 100644 index 00000000000..a518108f4ab --- /dev/null +++ b/bigframes/core/rewrite.py @@ -0,0 +1,299 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import dataclasses +import functools +import itertools +from typing import Optional, Sequence, Tuple + +import bigframes.core.expression as scalar_exprs +import bigframes.core.guid as guids +import bigframes.core.join_def as join_defs +import bigframes.core.nodes as nodes +import bigframes.core.ordering as order +import bigframes.operations as ops + +Selection = Tuple[Tuple[scalar_exprs.Expression, str], ...] + + +@dataclasses.dataclass(frozen=True) +class SquashedSelect: + """Squash together as many nodes as possible, separating out the projection, filter and reordering expressions.""" + + root: nodes.BigFrameNode + columns: Tuple[Tuple[scalar_exprs.Expression, str], ...] + predicate: Optional[scalar_exprs.Expression] + ordering: Tuple[order.OrderingExpression, ...] + + @classmethod + def from_node(cls, node: nodes.BigFrameNode) -> SquashedSelect: + if isinstance(node, nodes.ProjectionNode): + return cls.from_node(node.child).project(node.assignments) + elif isinstance(node, nodes.FilterNode): + return cls.from_node(node.child).filter(node.predicate) + elif isinstance(node, nodes.ReversedNode): + return cls.from_node(node.child).reverse() + elif isinstance(node, nodes.OrderByNode): + return cls.from_node(node.child).order_with(node.by) + else: + selection = tuple( + (scalar_exprs.UnboundVariableExpression(id), id) + for id in get_node_column_ids(node) + ) + return cls(node, selection, None, ()) + + def project( + self, projection: Tuple[Tuple[scalar_exprs.Expression, str], ...] + ) -> SquashedSelect: + lookup = {id: expr for expr, id in self.columns} + new_columns = tuple( + (expr.bind_all_variables(lookup), id) for expr, id in projection + ) + return SquashedSelect(self.root, new_columns, self.predicate, self.ordering) + + def filter(self, predicate: scalar_exprs.Expression) -> SquashedSelect: + lookup = {id: expr for expr, id in self.columns} + if self.predicate is None: + new_predicate = predicate.bind_all_variables(lookup) + else: + new_predicate = ops.and_op.as_expr( + self.predicate, predicate.bind_all_variables(lookup) + ) + return SquashedSelect(self.root, self.columns, new_predicate, self.ordering) + + def reverse(self) -> SquashedSelect: + new_ordering = tuple(expr.with_reverse() for expr in self.ordering) + return SquashedSelect(self.root, self.columns, self.predicate, new_ordering) + + def order_with(self, by: Tuple[order.OrderingColumnReference, ...]): + exprs_by_id = {id: expr for expr, id in self.columns} + as_order_exprs = [ + order.OrderingExpression( + exprs_by_id[ref.column_id], ref.direction, ref.na_last + ) + for ref in by + ] + new_ordering = (*as_order_exprs, *self.ordering) + return SquashedSelect(self.root, self.columns, self.predicate, new_ordering) + + def maybe_join( + self, right: SquashedSelect, join_def: join_defs.JoinDefinition + ) -> Optional[SquashedSelect]: + if join_def.type == "cross": + # Cannot convert cross join to projection + return None + + r_exprs_by_id = {id: expr for expr, id in right.columns} + l_exprs_by_id = {id: expr for expr, id in self.columns} + l_join_exprs = [l_exprs_by_id[cond.left_id] for cond in join_def.conditions] + r_join_exprs = [r_exprs_by_id[cond.right_id] for cond in join_def.conditions] + + if (self.root != right.root) or any( + l_expr != r_expr for l_expr, r_expr in zip(l_join_exprs, r_join_exprs) + ): + return None + + join_type = join_def.type + + # Mask columns and remap names to expected schema + lselection = self.columns + rselection = right.columns + if join_type == "inner": + new_predicate = and_predicates(self.predicate, right.predicate) + elif join_type == "outer": + new_predicate = or_predicates(self.predicate, right.predicate) + elif join_type == "left": + new_predicate = self.predicate + elif join_type == "right": + new_predicate = right.predicate + + l_relative, r_relative = relative_predicates(self.predicate, right.predicate) + lmask = l_relative if join_type in {"right", "outer"} else None + rmask = r_relative if join_type in {"left", "outer"} else None + if lmask is not None: + lselection = tuple((apply_mask(expr, lmask), id) for expr, id in lselection) + if rmask is not None: + rselection = tuple((apply_mask(expr, rmask), id) for expr, id in rselection) + new_columns = remap_names(join_def, lselection, rselection) + + # Reconstruct ordering + if join_type == "right": + new_ordering = right.ordering + elif join_type == "outer": + if lmask is not None: + prefix = order.OrderingExpression(lmask, order.OrderingDirection.DESC) + left_ordering = tuple( + order.OrderingExpression( + apply_mask(ref.scalar_expression, lmask), + ref.direction, + ref.na_last, + ) + for ref in self.ordering + ) + right_ordering = ( + tuple( + order.OrderingExpression( + apply_mask(ref.scalar_expression, rmask), + ref.direction, + ref.na_last, + ) + for ref in right.ordering + ) + if rmask + else right.ordering + ) + new_ordering = (prefix, *left_ordering, *right_ordering) + else: + new_ordering = self.ordering + elif join_type in {"inner", "left"}: + new_ordering = self.ordering + else: + raise ValueError(f"Unexpected join type {join_type}") + return SquashedSelect(self.root, new_columns, new_predicate, new_ordering) + + def expand(self) -> nodes.BigFrameNode: + # Safest to apply predicates first, as it may filter out inputs that cannot be handled by other expressions + root = ( + nodes.FilterNode(child=self.root, predicate=self.predicate) + if self.predicate + else self.root + ) + if self.ordering: + # Need this clumsy 3-node expansion as OrderByNode doesn't support expressions (yet?) + # Could also directly compile this whole class directly + ordering_assignments = [ + (ref.scalar_expression, guids.generate_guid()) for ref in self.ordering + ] + as_ordering_refs = tuple( + order.OrderingColumnReference(id, ref.direction, ref.na_last) + for ref, (_, id) in zip(self.ordering, ordering_assignments) + ) + extended_projection = nodes.ProjectionNode( + child=root, assignments=(*self.columns, *ordering_assignments) + ) + ordered_node = nodes.OrderByNode( + child=extended_projection, by=as_ordering_refs + ) + drop_ordering_selection = tuple( + (scalar_exprs.UnboundVariableExpression(id), id) + for _, id in self.columns + ) + pruned_node = nodes.ProjectionNode( + child=ordered_node, assignments=drop_ordering_selection + ) + return pruned_node + else: + return nodes.ProjectionNode(child=root, assignments=self.columns) + + +def maybe_rewrite_join(join_node: nodes.JoinNode) -> nodes.BigFrameNode: + left_side = SquashedSelect.from_node(join_node.left_child) + right_side = SquashedSelect.from_node(join_node.right_child) + joined = left_side.maybe_join(right_side, join_node.join) + if joined is not None: + return joined.expand() + else: + return join_node + + +def remap_names( + join: join_defs.JoinDefinition, lselection: Selection, rselection: Selection +) -> Selection: + new_selection: Selection = tuple() + l_exprs_by_id = {id: expr for expr, id in lselection} + r_exprs_by_id = {id: expr for expr, id in rselection} + for mapping in join.mappings: + if mapping.source_table == join_defs.JoinSide.LEFT: + expr = l_exprs_by_id[mapping.source_id] + else: # Right + expr = r_exprs_by_id[mapping.source_id] + id = mapping.destination_id + new_selection = (*new_selection, (expr, id)) + return new_selection + + +def and_predicates( + expr1: Optional[scalar_exprs.Expression], expr2: Optional[scalar_exprs.Expression] +) -> Optional[scalar_exprs.Expression]: + if expr1 is None: + return expr2 + if expr2 is None: + return expr1 + left_predicates = decompose_conjunction(expr1) + right_predicates = decompose_conjunction(expr2) + # remove common predicates + all_predicates = itertools.chain( + left_predicates, [p for p in right_predicates if p not in left_predicates] + ) + return merge_predicates(list(all_predicates)) + + +def or_predicates( + expr1: Optional[scalar_exprs.Expression], expr2: Optional[scalar_exprs.Expression] +) -> Optional[scalar_exprs.Expression]: + if (expr1 is None) or (expr2 is None): + return None + # TODO(tbergeron): Factor out common predicates + return ops.or_op.as_expr(expr1, expr2) + + +def relative_predicates( + expr1: Optional[scalar_exprs.Expression], expr2: Optional[scalar_exprs.Expression] +) -> Tuple[Optional[scalar_exprs.Expression], Optional[scalar_exprs.Expression]]: + left_predicates = decompose_conjunction(expr1) if expr1 else () + right_predicates = decompose_conjunction(expr2) if expr2 else () + left_relative = tuple( + pred for pred in left_predicates if pred not in right_predicates + ) + right_relative = tuple( + pred for pred in right_predicates if pred not in left_predicates + ) + return merge_predicates(left_relative), merge_predicates(right_relative) + + +def apply_mask( + expr: scalar_exprs.Expression, mask: scalar_exprs.Expression +) -> scalar_exprs.Expression: + return ops.where_op.as_expr(expr, mask, scalar_exprs.const(None)) + + +def merge_predicates( + predicates: Sequence[scalar_exprs.Expression], +) -> Optional[scalar_exprs.Expression]: + if len(predicates) == 0: + return None + + return functools.reduce(ops.and_op.as_expr, predicates) + + +def decompose_conjunction( + expr: scalar_exprs.Expression, +) -> Tuple[scalar_exprs.Expression, ...]: + if isinstance(expr, scalar_exprs.OpExpression) and isinstance( + expr.op, type(ops.and_op) + ): + return tuple( + itertools.chain.from_iterable(decompose_conjunction(i) for i in expr.inputs) + ) + else: + return (expr,) + + +def get_node_column_ids(node: nodes.BigFrameNode) -> Tuple[str, ...]: + # TODO: Convert to use node.schema once that has been merged + # Note: this actually compiles the node to get the schema + import bigframes.core + + return tuple(bigframes.core.ArrayValue(node).column_ids) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index c358d46ee16..b122f1fe7c0 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -30,15 +30,15 @@ class RowOp(typing.Protocol): @property def name(self) -> str: - raise NotImplementedError("RowOp abstract base class has no implementation") + ... @property def arguments(self) -> int: """The number of column argument the operation takes""" - raise NotImplementedError("RowOp abstract base class has no implementation") + ... def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - raise NotImplementedError("Abstract typing rule has no output type") + ... # These classes can be used to create simple ops that don't take local parameters diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index be4211a2fc7..3714106860b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2056,11 +2056,23 @@ def test_join_same_table(scalars_dfs, how): bf_df, pd_df = scalars_dfs bf_df_a = bf_df.set_index("int64_too")[["string_col", "int64_col"]] + bf_df_a = bf_df_a.sort_index() + bf_df_b = bf_df.set_index("int64_too")[["float64_col"]] + bf_df_b = bf_df_b[bf_df_b.float64_col > 0] + bf_df_b = bf_df_b.sort_values("float64_col") + bf_result = bf_df_a.join(bf_df_b, how=how).to_pandas() - pd_df_a = pd_df.set_index("int64_too")[["string_col", "int64_col"]] + + pd_df_a = pd_df.set_index("int64_too")[["string_col", "int64_col"]].sort_index() + pd_df_a = pd_df_a.sort_index() + pd_df_b = pd_df.set_index("int64_too")[["float64_col"]] + pd_df_b = pd_df_b[pd_df_b.float64_col > 0] + pd_df_b = pd_df_b.sort_values("float64_col") + pd_result = pd_df_a.join(pd_df_b, how=how) + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) From 3bab1a917a5833bd58b20071a229ee95cf86a251 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 18 Mar 2024 20:18:17 -0700 Subject: [PATCH 377/921] fix: df.drop_na preserves columns dtype (#457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/block_transforms.py | 37 +++++++---------------- bigframes/core/blocks.py | 45 ++++++++++++++++++---------- bigframes/core/indexes/index.py | 2 +- bigframes/dataframe.py | 10 +++---- bigframes/series.py | 10 +++---- tests/system/small/test_dataframe.py | 18 +++++++++-- 6 files changed, 66 insertions(+), 56 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 9cc0a05680c..6b9a367f55d 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -13,6 +13,7 @@ # limitations under the License. from __future__ import annotations +import functools import typing import pandas as pd @@ -307,7 +308,7 @@ def drop_duplicates( ) -> blocks.Block: block, dupe_indicator_id = indicate_duplicates(block, columns, keep) block, keep_indicator_id = block.apply_unary_op(dupe_indicator_id, ops.invert_op) - return block.filter(keep_indicator_id).drop_columns( + return block.filter_by_id(keep_indicator_id).drop_columns( (dupe_indicator_id, keep_indicator_id) ) @@ -459,32 +460,14 @@ def dropna( """ Drop na entries from block """ + predicates = [ops.notnull_op.as_expr(column_id) for column_id in column_ids] + if len(predicates) == 0: + return block if how == "any": - filtered_block = block - for column in column_ids: - filtered_block, result_id = filtered_block.apply_unary_op( - column, ops.notnull_op - ) - filtered_block = filtered_block.filter(result_id) - filtered_block = filtered_block.drop_columns([result_id]) - return filtered_block + predicate = functools.reduce(ops.and_op.as_expr, predicates) else: # "all" - filtered_block = block - predicate = None - for column in column_ids: - filtered_block, partial_predicate = filtered_block.apply_unary_op( - column, ops.notnull_op - ) - if predicate: - filtered_block, predicate = filtered_block.apply_binary_op( - partial_predicate, predicate, ops.or_op - ) - else: - predicate = partial_predicate - if predicate: - filtered_block = filtered_block.filter(predicate) - filtered_block = filtered_block.select_columns(block.value_columns) - return filtered_block + predicate = functools.reduce(ops.or_op.as_expr, predicates) + return block.filter(predicate) def nsmallest( @@ -513,7 +496,7 @@ def nsmallest( window_spec=windows.WindowSpec(ordering=tuple(order_refs)), ) block, condition = block.project_expr(ops.le_op.as_expr(counter, ex.const(n))) - block = block.filter(condition) + block = block.filter_by_id(condition) return block.drop_columns([counter, condition]) @@ -543,7 +526,7 @@ def nlargest( window_spec=windows.WindowSpec(ordering=tuple(order_refs)), ) block, condition = block.project_expr(ops.le_op.as_expr(counter, ex.const(n))) - block = block.filter(condition) + block = block.filter_by_id(condition) return block.drop_columns([counter, condition]) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 375ce7e7e0e..0ebbe48cc40 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -37,6 +37,7 @@ import bigframes.constants as constants import bigframes.core as core import bigframes.core.expression as ex +import bigframes.core.expression as scalars import bigframes.core.guid as guid import bigframes.core.join_def as join_defs import bigframes.core.ordering as ordering @@ -701,7 +702,7 @@ def project_expr( block = Block( array_val, index_columns=self.index_columns, - column_labels=[*self.column_labels, label], + column_labels=self.column_labels.insert(len(self.column_labels), label), index_labels=self.index.names, ) return (block, result_id) @@ -793,7 +794,7 @@ def apply_window_op( if skip_null_groups: for key in window_spec.grouping_keys: block, not_null_id = block.apply_unary_op(key, ops.notnull_op) - block = block.filter(not_null_id).drop_columns([not_null_id]) + block = block.filter_by_id(not_null_id).drop_columns([not_null_id]) result_id = guid.generate_guid() expr = block._expr.project_window_op( column, @@ -806,7 +807,9 @@ def apply_window_op( block = Block( expr, index_columns=self.index_columns, - column_labels=[*self.column_labels, result_label], + column_labels=self.column_labels.insert( + len(self.column_labels), result_label + ), index_labels=self._index_labels, ) return (block, result_id) @@ -850,7 +853,7 @@ def assign_label(self, column_id: str, new_label: Label) -> Block: ) return self.with_column_labels(new_labels) - def filter(self, column_id: str, keep_null: bool = False): + def filter_by_id(self, column_id: str, keep_null: bool = False): return Block( self._expr.filter_by_id(column_id, keep_null), index_columns=self.index_columns, @@ -858,6 +861,14 @@ def filter(self, column_id: str, keep_null: bool = False): index_labels=self.index.names, ) + def filter(self, predicate: scalars.Expression): + return Block( + self._expr.filter(predicate), + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self.index.names, + ) + def aggregate_all_and_stack( self, operation: agg_ops.UnaryAggregateOp, @@ -1086,8 +1097,11 @@ def summarize( unpivot_columns=tuple(columns), index_col_ids=tuple([label_col_id]), ) - labels = self._get_labels_for_columns(column_ids) - return Block(expr, column_labels=labels, index_columns=[label_col_id]) + return Block( + expr, + column_labels=self._get_labels_for_columns(column_ids), + index_columns=[label_col_id], + ) def corr(self): """Returns a block object to compute the self-correlation on this block.""" @@ -1156,10 +1170,10 @@ def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.UnaryAggregateOp return stats - def _get_labels_for_columns(self, column_ids: typing.Sequence[str]): + def _get_labels_for_columns(self, column_ids: typing.Sequence[str]) -> pd.Index: """Get column label for value columns, or index name for index columns""" - lookup = self.col_id_to_label - return [lookup.get(col_id, None) for col_id in column_ids] + indices = [self.value_columns.index(col_id) for col_id in column_ids] + return self.column_labels.take(indices, allow_fill=False) def _normalize_expression( self, @@ -1255,7 +1269,7 @@ def _forward_slice(self, start: int = 0, stop=None, step: int = 1): for cond in conditions: block, cond_id = block.project_expr(cond) - block = block.filter(cond_id) + block = block.filter_by_id(cond_id) return block.select_columns(self.value_columns) @@ -1292,7 +1306,7 @@ def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: Block( expr, index_columns=self.index_columns, - column_labels=[label, *self.column_labels], + column_labels=self.column_labels.insert(0, label), index_labels=self._index_labels, ), result_id, @@ -1391,10 +1405,9 @@ def pivot( if values_in_index or len(values) > 1: value_labels = self._get_labels_for_columns(values) column_index = self._create_pivot_column_index(value_labels, columns_values) + return result_block.with_column_labels(column_index) else: - column_index = columns_values - - return result_block.with_column_labels(column_index) + return result_block.with_column_labels(columns_values) def stack(self, how="left", levels: int = 1): """Unpivot last column axis level into row axis""" @@ -1517,8 +1530,8 @@ def _column_type(self, col_id: str) -> bigframes.dtypes.Dtype: @staticmethod def _create_pivot_column_index( - value_labels: Sequence[typing.Hashable], columns_values: pd.Index - ): + value_labels: pd.Index, columns_values: pd.Index + ) -> pd.Index: index_parts = [] for value in value_labels: as_frame = columns_values.to_frame() diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 328dd493972..c8cb07d3392 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -378,7 +378,7 @@ def drop( block, condition_id = block.project_expr( ops.ne_op.as_expr(level_id, ex.const(labels)) ) - block = block.filter(condition_id, keep_null=True) + block = block.filter_by_id(condition_id, keep_null=True) block = block.drop_columns([condition_id]) return Index(block) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index e8328b6047c..ad71c9b6e47 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -525,7 +525,7 @@ def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: ) = self._block.join(key._block, how="left") block = combined_index filter_col_id = get_column_right[key._value_column] - block = block.filter(filter_col_id) + block = block.filter_by_id(filter_col_id) block = block.drop_columns([filter_col_id]) return DataFrame(block) @@ -1193,7 +1193,7 @@ def drop( block, condition_id = block.project_expr( ops.ne_op.as_expr(level_id, ex.const(index)) ) - block = block.filter(condition_id, keep_null=True).select_columns( + block = block.filter_by_id(condition_id, keep_null=True).select_columns( self._block.value_columns ) if columns: @@ -1214,7 +1214,7 @@ def _drop_by_index(self, index: indexes.Index) -> DataFrame: ops.isnull_op, ) - drop_block = drop_block.filter(drop_col) + drop_block = drop_block.filter_by_id(drop_col) original_columns = [ get_column_left[column] for column in self._block.value_columns ] @@ -1558,7 +1558,7 @@ def _filter_rows( label_string_id, ops.StrContainsRegexOp(pat=regex) ) - block = block.filter(mask_id) + block = block.filter_by_id(mask_id) block = block.select_columns(self._block.value_columns) return DataFrame(block) elif items is not None: @@ -1567,7 +1567,7 @@ def _filter_rows( block, mask_id = block.apply_unary_op( self._block.index_columns[0], ops.IsInOp(values=tuple(items)) ) - block = block.filter(mask_id) + block = block.filter_by_id(mask_id) block = block.select_columns(self._block.value_columns) return DataFrame(block) else: diff --git a/bigframes/series.py b/bigframes/series.py index 86afdd047c0..8c3f1352f2d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -354,7 +354,7 @@ def drop( block, condition_id = block.project_expr( ops.ne_op.as_expr(level_id, ex.const(index)) ) - block = block.filter(condition_id, keep_null=True) + block = block.filter_by_id(condition_id, keep_null=True) block = block.drop_columns([condition_id]) return Series(block.select_column(self._value_column)) @@ -861,7 +861,7 @@ def mode(self) -> Series: max_value_count_col_id, ops.eq_op, ) - block = block.filter(is_mode_col_id) + block = block.filter_by_id(is_mode_col_id) # use temporary name for reset_index to avoid collision, restore after dropping extra columns block = ( block.with_index_labels(["mode_temp_internal"]) @@ -1032,7 +1032,7 @@ def __getitem__(self, indexer): return self.iloc[indexer] if isinstance(indexer, Series): (left, right, block) = self._align(indexer, "left") - block = block.filter(right) + block = block.filter_by_id(right) block = block.select_column(left) return Series(block) return self.loc[indexer] @@ -1304,7 +1304,7 @@ def filter( label_string_id, ops.StrContainsRegexOp(pat=regex) ) - block = block.filter(mask_id) + block = block.filter_by_id(mask_id) block = block.select_columns([self._value_column]) return Series(block) elif items is not None: @@ -1313,7 +1313,7 @@ def filter( block, mask_id = block.apply_unary_op( self._block.index_columns[0], ops.IsInOp(values=tuple(items)) ) - block = block.filter(mask_id) + block = block.filter_by_id(mask_id) block = block.select_columns([self._value_column]) return Series(block) else: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 3714106860b..2048ada44f8 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -842,6 +842,7 @@ def test_assign_callable_lambda(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) +@skip_legacy_pandas @pytest.mark.parametrize( ("axis", "how", "ignore_index"), [ @@ -852,8 +853,6 @@ def test_assign_callable_lambda(scalars_dfs): ], ) def test_df_dropna(scalars_dfs, axis, how, ignore_index): - if pd.__version__.startswith("1."): - pytest.skip("ignore_index parameter not supported in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs df = scalars_df.dropna(axis=axis, how=how, ignore_index=ignore_index) bf_result = df.to_pandas() @@ -864,6 +863,21 @@ def test_df_dropna(scalars_dfs, axis, how, ignore_index): pandas.testing.assert_frame_equal(bf_result, pd_result) +@skip_legacy_pandas +def test_df_dropna_range_columns(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + scalars_df = scalars_df.copy() + scalars_pandas_df = scalars_pandas_df.copy() + scalars_df.columns = pandas.RangeIndex(0, len(scalars_df.columns)) + scalars_pandas_df.columns = pandas.RangeIndex(0, len(scalars_pandas_df.columns)) + + df = scalars_df.dropna() + bf_result = df.to_pandas() + pd_result = scalars_pandas_df.dropna() + + pandas.testing.assert_frame_equal(bf_result, pd_result) + + def test_df_interpolate(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs columns = ["int64_col", "int64_too", "float64_col"] From 5dd9e6e23e3ffd07abbde70a43129238ff07d056 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 19 Mar 2024 14:11:58 +0000 Subject: [PATCH 378/921] test: do GCF cleanup in both presubmit and e2e tests (#423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: do GCF cleanup in both presubmit and e2e tests * use functions client from session * address review comments --------- Co-authored-by: Tim Sweña (Swast) --- tests/system/conftest.py | 67 ++++++- tests/system/large/test_remote_function.py | 219 ++++++--------------- tests/system/utils.py | 63 ++++++ 3 files changed, 185 insertions(+), 164 deletions(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 4b5ebc9d439..e6b241c9a3e 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -21,6 +21,7 @@ import typing from typing import Dict, Optional +import google.api_core.exceptions import google.cloud.bigquery as bigquery import google.cloud.bigquery_connection_v1 as bigquery_connection_v1 import google.cloud.exceptions @@ -34,7 +35,15 @@ import test_utils.prefixer import bigframes -from tests.system.utils import convert_pandas_dtypes +import tests.system.utils + +# Use this to control the number of cloud functions being deleted in a single +# test session. This should help soften the spike of the number of mutations per +# minute tracked against a quota limit (default 60, increased to 120 for +# bigframes-dev project) by the Cloud Functions API +# We are running pytest with "-n 20". Let's say each session lasts about a +# minute, so we are setting a limit of 120/20 = 6 deletions per session. +MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION = 6 CURRENT_DIR = pathlib.Path(__file__).parent DATA_DIR = CURRENT_DIR.parent / "data" @@ -348,7 +357,7 @@ def nested_pandas_df() -> pd.DataFrame: DATA_DIR / "nested.jsonl", lines=True, ) - convert_pandas_dtypes(df, bytes_col=True) + tests.system.utils.convert_pandas_dtypes(df, bytes_col=True) df = df.set_index("rowindex") return df @@ -400,7 +409,7 @@ def scalars_pandas_df_default_index() -> pd.DataFrame: DATA_DIR / "scalars.jsonl", lines=True, ) - convert_pandas_dtypes(df, bytes_col=True) + tests.system.utils.convert_pandas_dtypes(df, bytes_col=True) df = df.set_index("rowindex", drop=False) df.index.name = None @@ -1040,3 +1049,55 @@ def floats_bf(session, floats_pd): @pytest.fixture() def floats_product_bf(session, floats_product_pd): return session.read_pandas(floats_product_pd) + + +@pytest.fixture(scope="session", autouse=True) +def cleanup_cloud_functions(session, cloudfunctions_client, dataset_id_permanent): + """Clean up stale cloud functions.""" + permanent_endpoints = tests.system.utils.get_remote_function_endpoints( + session.bqclient, dataset_id_permanent + ) + delete_count = 0 + for cloud_function in tests.system.utils.get_cloud_functions( + cloudfunctions_client, + session.bqclient.project, + session.bqclient.location, + name_prefix="bigframes-", + ): + # Ignore bigframes cloud functions referred by the remote functions in + # the permanent dataset + if cloud_function.service_config.uri in permanent_endpoints: + continue + + # Ignore the functions less than one day old + age = datetime.now() - datetime.fromtimestamp( + cloud_function.update_time.timestamp() + ) + if age.days <= 0: + continue + + # Go ahead and delete + try: + tests.system.utils.delete_cloud_function( + cloudfunctions_client, cloud_function.name + ) + delete_count += 1 + if delete_count >= MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION: + break + except google.api_core.exceptions.NotFound: + # This can happen when multiple pytest sessions are running in + # parallel. Two or more sessions may discover the same cloud + # function, but only one of them would be able to delete it + # successfully, while the other instance will run into this + # exception. Ignore this exception. + pass + except google.api_core.exceptions.ResourceExhausted: + # This can happen if we are hitting GCP limits, e.g. + # google.api_core.exceptions.ResourceExhausted: 429 Quota exceeded + # for quota metric 'Per project mutation requests' and limit + # 'Per project mutation requests per minute per region' of service + # 'cloudfunctions.googleapis.com' for consumer + # 'project_number:1084210331973'. + # [reason: "RATE_LIMIT_EXCEEDED" domain: "googleapis.com" ... + # Let's stop further clean up and leave it to later. + break diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index c0a1f6c4ece..f0b138c110e 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime import importlib.util import inspect import math # must keep this at top level to test udf referring global import @@ -21,26 +20,19 @@ import tempfile import textwrap -from google.api_core.exceptions import BadRequest, NotFound, ResourceExhausted -from google.cloud import bigquery, functions_v2, storage +from google.api_core.exceptions import BadRequest, NotFound +from google.cloud import bigquery, storage import pandas import pytest import test_utils.prefixer import bigframes -from bigframes.functions.remote_function import ( - get_cloud_function_name, - get_remote_function_locations, +from bigframes.functions.remote_function import get_cloud_function_name +from tests.system.utils import ( + assert_pandas_df_equal, + delete_cloud_function, + get_cloud_functions, ) -from tests.system.utils import assert_pandas_df_equal - -# Use this to control the number of cloud functions being deleted in a single -# test session. This should help soften the spike of the number of mutations per -# minute tracked against a quota limit (default 60, increased to 120 for -# bigframes-dev project) by the Cloud Functions API -# We are running pytest with "-n 20". Let's say each session lasts about a -# minute, so we are setting a limit of 120/20 = 6 deletions per session. -_MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION = 6 # NOTE: Keep this import at the top level to test global var behavior with # remote functions @@ -48,57 +40,8 @@ _team_euler = "Team Euler" -def get_remote_function_endpoints(bigquery_client, dataset_id): - """Get endpoints used by the remote functions in a datset""" - endpoints = set() - routines = bigquery_client.list_routines(dataset=dataset_id) - for routine in routines: - rf_options = routine._properties.get("remoteFunctionOptions") - if not rf_options: - continue - rf_endpoint = rf_options.get("endpoint") - if rf_endpoint: - endpoints.add(rf_endpoint) - return endpoints - - -def get_cloud_functions( - functions_client, project, location, name=None, name_prefix=None -): - """Get the cloud functions in the given project and location.""" - - assert ( - not name or not name_prefix - ), f"At most one of the {name.__name__} or {name_prefix.__name__} can be passed." - - _, location = get_remote_function_locations(location) - parent = f"projects/{project}/locations/{location}" - request = functions_v2.ListFunctionsRequest(parent=parent) - page_result = functions_client.list_functions(request=request) - for response in page_result: - # If name is provided and it does not match then skip - if bool(name): - full_name = parent + f"/functions/{name}" - if response.name != full_name: - continue - # If name prefix is provided and it does not match then skip - elif bool(name_prefix): - full_name_prefix = parent + f"/functions/{name_prefix}" - if not response.name.startswith(full_name_prefix): - continue - - yield response - - -def delete_cloud_function(functions_client, full_name): - """Delete a cloud function with the given fully qualified name.""" - request = functions_v2.DeleteFunctionRequest(name=full_name) - operation = functions_client.delete_function(request=request) - return operation - - def cleanup_remote_function_assets( - bigquery_client, functions_client, remote_udf, ignore_failures=True + bigquery_client, cloudfunctions_client, remote_udf, ignore_failures=True ): """Clean up the GCP assets behind a bigframes remote function.""" @@ -112,7 +55,9 @@ def cleanup_remote_function_assets( # Clean up cloud function try: - delete_cloud_function(functions_client, remote_udf.bigframes_cloud_function) + delete_cloud_function( + cloudfunctions_client, remote_udf.bigframes_cloud_function + ) except Exception: # By default don't raise exception in cleanup if not ignore_failures: @@ -169,62 +114,6 @@ def bq_cf_connection() -> str: return "bigframes-rf-conn" -@pytest.fixture(scope="module") -def functions_client() -> functions_v2.FunctionServiceClient: - """Cloud Functions client""" - return functions_v2.FunctionServiceClient() - - -@pytest.fixture(scope="module", autouse=True) -def cleanup_cloud_functions(session, functions_client, dataset_id_permanent): - """Clean up stale cloud functions.""" - permanent_endpoints = get_remote_function_endpoints( - session.bqclient, dataset_id_permanent - ) - delete_count = 0 - for cloud_function in get_cloud_functions( - functions_client, - session.bqclient.project, - session.bqclient.location, - name_prefix="bigframes-", - ): - # Ignore bigframes cloud functions referred by the remote functions in - # the permanent dataset - if cloud_function.service_config.uri in permanent_endpoints: - continue - - # Ignore the functions less than one day old - age = datetime.now() - datetime.fromtimestamp( - cloud_function.update_time.timestamp() - ) - if age.days <= 0: - continue - - # Go ahead and delete - try: - delete_cloud_function(functions_client, cloud_function.name) - delete_count += 1 - if delete_count >= _MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION: - break - except NotFound: - # This can happen when multiple pytest sessions are running in - # parallel. Two or more sessions may discover the same cloud - # function, but only one of them would be able to delete it - # successfully, while the other instance will run into this - # exception. Ignore this exception. - pass - except ResourceExhausted: - # This can happen if we are hitting GCP limits, e.g. - # google.api_core.exceptions.ResourceExhausted: 429 Quota exceeded - # for quota metric 'Per project mutation requests' and limit - # 'Per project mutation requests per minute per region' of service - # 'cloudfunctions.googleapis.com' for consumer - # 'project_number:1084210331973'. - # [reason: "RATE_LIMIT_EXCEEDED" domain: "googleapis.com" ... - # Let's stop further clean up and leave it to later. - break - - @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_multiply_with_ibis( session, @@ -232,7 +121,6 @@ def test_remote_function_multiply_with_ibis( ibis_client, dataset_id, bq_cf_connection, - functions_client, ): try: @@ -274,7 +162,9 @@ def multiply(x, y): ) finally: # clean up the gcp assets created for the remote function - cleanup_remote_function_assets(session.bqclient, functions_client, multiply) + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, multiply + ) @pytest.mark.flaky(retries=2, delay=120) @@ -284,7 +174,6 @@ def test_remote_function_stringify_with_ibis( ibis_client, dataset_id, bq_cf_connection, - functions_client, ): try: @@ -319,12 +208,14 @@ def stringify(x): ) finally: # clean up the gcp assets created for the remote function - cleanup_remote_function_assets(session.bqclient, functions_client, stringify) + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, stringify + ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_decorator_with_bigframes_series( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -362,12 +253,14 @@ def square(x): assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function - cleanup_remote_function_assets(session.bqclient, functions_client, square) + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square + ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_explicit_with_bigframes_series( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -407,13 +300,16 @@ def add_one(x): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, remote_add_one + session.bqclient, session.cloudfunctionsclient, remote_add_one ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_explicit_dataset_not_created( - session, scalars_dfs, dataset_id_not_created, bq_cf_connection, functions_client + session, + scalars_dfs, + dataset_id_not_created, + bq_cf_connection, ): try: @@ -451,12 +347,14 @@ def square(x): assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function - cleanup_remote_function_assets(session.bqclient, functions_client, square) + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square + ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_udf_referring_outside_var( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: POSITIVE_SIGN = 1 @@ -502,12 +400,14 @@ def sign(num): assert_pandas_df_equal(bf_result, pd_result) finally: # clean up the gcp assets created for the remote function - cleanup_remote_function_assets(session.bqclient, functions_client, remote_sign) + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, remote_sign + ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_udf_referring_outside_import( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: import math as mymath @@ -548,13 +448,13 @@ def circumference(radius): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, remote_circumference + session.bqclient, session.cloudfunctionsclient, remote_circumference ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_udf_referring_global_var_and_import( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -597,7 +497,7 @@ def find_team(num): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, remote_find_team + session.bqclient, session.cloudfunctionsclient, remote_find_team ) @@ -607,7 +507,6 @@ def test_remote_function_restore_with_bigframes_series( scalars_dfs, dataset_id, bq_cf_connection, - functions_client, ): try: @@ -623,7 +522,7 @@ def add_one(x): # There should be no cloud function yet for the unique udf cloud_functions = list( get_cloud_functions( - functions_client, + session.cloudfunctionsclient, session.bqclient.project, session.bqclient.location, name=add_one_uniq_cf_name, @@ -644,7 +543,7 @@ def add_one(x): # There should have been excactly one cloud function created at this point cloud_functions = list( get_cloud_functions( - functions_client, + session.cloudfunctionsclient, session.bqclient.project, session.bqclient.location, name=add_one_uniq_cf_name, @@ -684,7 +583,7 @@ def inner_test(): # Let's delete the cloud function while not touching the bq remote function delete_operation = delete_cloud_function( - functions_client, cloud_functions[0].name + session.cloudfunctionsclient, cloud_functions[0].name ) delete_operation.result() assert delete_operation.done() @@ -692,7 +591,7 @@ def inner_test(): # There should be no cloud functions at this point for the uniq udf cloud_functions = list( get_cloud_functions( - functions_client, + session.cloudfunctionsclient, session.bqclient.project, session.bqclient.location, name=add_one_uniq_cf_name, @@ -714,7 +613,7 @@ def inner_test(): # There should be excactly one cloud function again cloud_functions = list( get_cloud_functions( - functions_client, + session.cloudfunctionsclient, session.bqclient.project, session.bqclient.location, name=add_one_uniq_cf_name, @@ -731,13 +630,13 @@ def inner_test(): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, remote_add_one + session.bqclient, session.cloudfunctionsclient, remote_add_one ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_udf_mask_default_value( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -771,13 +670,13 @@ def is_odd(num): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, is_odd_remote + session.bqclient, session.cloudfunctionsclient, is_odd_remote ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_udf_mask_custom_value( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -814,14 +713,12 @@ def is_odd(num): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, is_odd_remote + session.bqclient, session.cloudfunctionsclient, is_odd_remote ) @pytest.mark.flaky(retries=2, delay=120) -def test_remote_udf_lambda( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client -): +def test_remote_udf_lambda(session, scalars_dfs, dataset_id, bq_cf_connection): try: add_one_lambda = lambda x: x + 1 # noqa: E731 @@ -858,13 +755,13 @@ def test_remote_udf_lambda( finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, add_one_lambda_remote + session.bqclient, session.cloudfunctionsclient, add_one_lambda_remote ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_with_explicit_name( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -915,13 +812,13 @@ def square(x): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, square_remote + session.bqclient, session.cloudfunctionsclient, square_remote ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_with_external_package_dependencies( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -960,13 +857,13 @@ def pd_np_foo(x): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, pd_np_foo_remote + session.bqclient, session.cloudfunctionsclient, pd_np_foo_remote ) @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_with_explicit_name_reuse( - session, scalars_dfs, dataset_id, bq_cf_connection, functions_client + session, scalars_dfs, dataset_id, bq_cf_connection ): try: @@ -1113,13 +1010,13 @@ def plusone(x): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, functions_client, square_remote1 + session.bqclient, session.cloudfunctionsclient, square_remote1 ) cleanup_remote_function_assets( - session.bqclient, functions_client, square_remote2 + session.bqclient, session.cloudfunctionsclient, square_remote2 ) cleanup_remote_function_assets( - session.bqclient, functions_client, plusone_remote + session.bqclient, session.cloudfunctionsclient, plusone_remote ) for dir_ in dirs_to_cleanup: shutil.rmtree(dir_) diff --git a/tests/system/utils.py b/tests/system/utils.py index a4647b4f512..8ea49ed7e29 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -15,13 +15,19 @@ import base64 import decimal import functools +from typing import Iterable, Optional, Set import geopandas as gpd # type: ignore +import google.api_core.operation +from google.cloud import bigquery, functions_v2 +from google.cloud.functions_v2.types import functions import numpy as np import pandas as pd import pyarrow as pa # type: ignore import pytest +from bigframes.functions import remote_function + def skip_legacy_pandas(test): @functools.wraps(test) @@ -241,3 +247,60 @@ def assert_pandas_df_equal_pca(actual, expected, **kwargs): except AssertionError: # Allow for sign difference per column pd.testing.assert_series_equal(-actual[column], expected[column], **kwargs) + + +def get_remote_function_endpoints( + bigquery_client: bigquery.Client, dataset_id: str +) -> Set[str]: + """Get endpoints used by the remote functions in a datset""" + endpoints = set() + routines = bigquery_client.list_routines(dataset=dataset_id) + for routine in routines: + rf_options = routine._properties.get("remoteFunctionOptions") + if not rf_options: + continue + rf_endpoint = rf_options.get("endpoint") + if rf_endpoint: + endpoints.add(rf_endpoint) + return endpoints + + +def get_cloud_functions( + functions_client: functions_v2.FunctionServiceClient, + project: str, + location: str, + name: Optional[str] = None, + name_prefix: Optional[str] = None, +) -> Iterable[functions.ListFunctionsResponse]: + """Get the cloud functions in the given project and location.""" + + assert ( + not name or not name_prefix + ), "Either 'name' or 'name_prefix' can be passed but not both." + + _, location = remote_function.get_remote_function_locations(location) + parent = f"projects/{project}/locations/{location}" + request = functions_v2.ListFunctionsRequest(parent=parent) + page_result = functions_client.list_functions(request=request) + for response in page_result: + # If name is provided and it does not match then skip + if bool(name): + full_name = parent + f"/functions/{name}" + if response.name != full_name: + continue + # If name prefix is provided and it does not match then skip + elif bool(name_prefix): + full_name_prefix = parent + f"/functions/{name_prefix}" + if not response.name.startswith(full_name_prefix): + continue + + yield response + + +def delete_cloud_function( + functions_client: functions_v2.FunctionServiceClient, full_name: str +) -> google.api_core.operation.Operation: + """Delete a cloud function with the given fully qualified name.""" + request = functions_v2.DeleteFunctionRequest(name=full_name) + operation = functions_client.delete_function(request=request) + return operation From 340f0b5b41fc5150d73890c7f27ae68dc308e160 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:03:25 -0700 Subject: [PATCH 379/921] docs: add the pages for at and iat indexers (#456) --- docs/reference/bigframes.pandas/indexers.rst | 28 +++++++++++++++++++ docs/templates/toc.yml | 8 ++++++ .../bigframes_vendored/pandas/core/frame.py | 12 ++++++-- .../bigframes_vendored/pandas/core/series.py | 12 ++++++-- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/docs/reference/bigframes.pandas/indexers.rst b/docs/reference/bigframes.pandas/indexers.rst index a7388bcb6b7..602b6de8372 100644 --- a/docs/reference/bigframes.pandas/indexers.rst +++ b/docs/reference/bigframes.pandas/indexers.rst @@ -3,6 +3,34 @@ Indexers ========= +AtDataFrameIndexer +-------------------- +.. autoclass:: bigframes.core.indexers.AtDataFrameIndexer + :members: + :inherited-members: + :undoc-members: + +AtSeriesIndexer +-------------------- +.. autoclass:: bigframes.core.indexers.AtSeriesIndexer + :members: + :inherited-members: + :undoc-members: + +IatDataFrameIndexer +-------------------- +.. autoclass:: bigframes.core.indexers.IatDataFrameIndexer + :members: + :inherited-members: + :undoc-members: + +IatSeriesIndexer +-------------------- +.. autoclass:: bigframes.core.indexers.IatSeriesIndexer + :members: + :inherited-members: + :undoc-members: + ILocDataFrameIndexer -------------------- .. autoclass:: bigframes.core.indexers.ILocDataFrameIndexer diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 224b5354160..c07e6141f1b 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -41,6 +41,14 @@ uid: bigframes.core.groupby.SeriesGroupBy name: Groupby - items: + - name: AtDataFrameIndexer + uid: bigframes.core.indexers.AtDataFrameIndexer + - name: AtSeriesIndexer + uid: bigframes.core.indexers.AtSeriesIndexer + - name: IatDataFrameIndexer + uid: bigframes.core.indexers.IatDataFrameIndexer + - name: IatSeriesIndexer + uid: bigframes.core.indexers.IatSeriesIndexer - name: ILocDataFrameIndexer uid: bigframes.core.indexers.ILocDataFrameIndexer - name: IlocSeriesIndexer diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 7793b31a210..4eceb8a2f18 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -5129,12 +5129,20 @@ def loc(self): @property def iat(self): - """Access a single value for a row/column pair by integer position.""" + """Access a single value for a row/column pair by integer position. + + Returns: + bigframes.core.indexers.IatDataFrameIndexer: Indexers object. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def at(self): - """Access a single value for a row/column label pair.""" + """Access a single value for a row/column label pair. + + Returns: + bigframes.core.indexers.AtDataFrameIndexer: Indexers object. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def dot(self, other): diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 6a4c7f0ad57..7120c4d155e 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3278,12 +3278,20 @@ def loc(self): @property def iat(self): - """Access a single value for a row/column pair by integer position.""" + """Access a single value for a row/column pair by integer position. + + Returns: + bigframes.core.indexers.IatSeriesIndexer: Indexers object. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def at(self): - """Access a single value for a row/column label pair.""" + """Access a single value for a row/column label pair. + + Returns: + bigframes.core.indexers.AtSeriesIndexer: Indexers object. + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property From 85fefa2f1d4dbe3e0c9d4ab8124cea88eb5df38f Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:06:10 -0700 Subject: [PATCH 380/921] docs: add code samples for `ml.metrics.r2_score` (#459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #330220122 🦕 --- .../sklearn/metrics/_regression.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_regression.py b/third_party/bigframes_vendored/sklearn/metrics/_regression.py index 9740c540e90..be531a9b1cd 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_regression.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_regression.py @@ -42,6 +42,18 @@ def r2_score(y_true, y_pred, force_finite=True) -> float: these cases are replaced with 1.0 (perfect predictions) or 0.0 (imperfect predictions) respectively. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([3, -0.5, 2, 7]) + >>> y_pred = bpd.DataFrame([2.5, 0.0, 2, 8]) + >>> r2_score = bigframes.ml.metrics.r2_score(y_true, y_pred) + >>> r2_score + 0.9486081370449679 + Args: y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) target values. From 3971bd27c96b68b859399564dbb6abdb93de5f14 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 19 Mar 2024 10:31:34 -0700 Subject: [PATCH 381/921] fix: fix grouping series on multiple other series (#455) --- bigframes/series.py | 2 +- tests/system/small/test_series.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bigframes/series.py b/bigframes/series.py index 8c3f1352f2d..d01ee88cf5e 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1195,7 +1195,7 @@ def _groupby_values( get_column_right, ) = block.join(key._block, how="inner" if dropna else "left") - value_col = get_column_left[self._value_column] + value_col = get_column_left[value_col] grouping_cols = [ *[get_column_left[value] for value in grouping_cols], get_column_right[key._value_column], diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 4ce3dcfe2c4..f63ea977ffc 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1313,9 +1313,15 @@ def test_any(scalars_dfs): def test_groupby_sum(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_series = scalars_df[col_name].groupby(scalars_df["string_col"]).sum() + bf_series = ( + scalars_df[col_name] + .groupby([scalars_df["bool_col"], ~scalars_df["bool_col"]]) + .sum() + ) pd_series = ( - scalars_pandas_df[col_name].groupby(scalars_pandas_df["string_col"]).sum() + scalars_pandas_df[col_name] + .groupby([scalars_pandas_df["bool_col"], ~scalars_pandas_df["bool_col"]]) + .sum() ) # TODO(swast): Update groupby to use index based on group by key(s). bf_result = bf_series.to_pandas() From 73fe0f89a96557afc4225521654978b96a2291b3 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:11:16 -0700 Subject: [PATCH 382/921] fix!: exclude remote models for .register() (#465) * fix: exclude remote models for .register() * fix mypy --- bigframes/ml/base.py | 1 + bigframes/ml/llm.py | 6 +++--- tests/system/small/ml/test_register.py | 17 ++++------------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 9001987e9a9..e58ed4feef6 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -90,6 +90,7 @@ def __repr__(self): return prettyprinter.pformat(self) +# TODO(garrettwu): refactor to reflect the actual property. Now the class contains .register() method. class Predictor(BaseEstimator): """A BigQuery DataFrames ML Model base class that can be used to predict outputs.""" diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 79f6b90bfdd..10c3cc51b23 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -48,7 +48,7 @@ @log_adapter.class_logger -class PaLM2TextGenerator(base.Predictor): +class PaLM2TextGenerator(base.BaseEstimator): """PaLM2 text generator LLM model. Args: @@ -258,7 +258,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> PaLM2TextGenerator: @log_adapter.class_logger -class PaLM2TextEmbeddingGenerator(base.Predictor): +class PaLM2TextEmbeddingGenerator(base.BaseEstimator): """PaLM2 text embedding generator LLM model. Args: @@ -418,7 +418,7 @@ def to_gbq( @log_adapter.class_logger -class GeminiTextGenerator(base.Predictor): +class GeminiTextGenerator(base.BaseEstimator): """Gemini text generator LLM model. Args: diff --git a/tests/system/small/ml/test_register.py b/tests/system/small/ml/test_register.py index bcf1f4a5b0e..6d8ff0a712c 100644 --- a/tests/system/small/ml/test_register.py +++ b/tests/system/small/ml/test_register.py @@ -14,6 +14,8 @@ from typing import cast +import pytest + from bigframes.ml import core, imported, linear_model, llm @@ -54,19 +56,8 @@ def test_linear_reg_register_with_params( def test_palm2_text_generator_register( ephemera_palm2_text_generator_model: llm.PaLM2TextGenerator, ): - model = ephemera_palm2_text_generator_model - model.register() - - model_name = "bigframes_" + cast( - str, cast(core.BqmlModel, model._bqml_model).model.model_id - ) - # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. - assert ( - model_name[:63] # truncated - in cast(core.BqmlModel, model._bqml_model).model.training_runs[-1][ - "vertexAiModelId" - ] - ) + with pytest.raises(AttributeError): + ephemera_palm2_text_generator_model.register() # type: ignore def test_imported_tensorflow_register( From 4e8e97d661078ed38d77be93b0bc1ad0fd52949c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 19 Mar 2024 13:30:45 -0500 Subject: [PATCH 383/921] feat: set `force=True` by default in `DataFrame.peek()` (#469) --- bigframes/dataframe.py | 12 ++++++------ tests/system/small/test_dataframe.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ad71c9b6e47..c10a0c2456a 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1086,19 +1086,19 @@ def head(self, n: int = 5) -> DataFrame: def tail(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[-n:]) - def peek(self, n: int = 5, *, force: bool = False) -> pandas.DataFrame: + def peek(self, n: int = 5, *, force: bool = True) -> pandas.DataFrame: """ Preview n arbitrary rows from the dataframe. No guarantees about row selection or ordering. - DataFrame.peek(force=False) will always be very fast, but will not succeed if data requires - full data scanning. Using force=True will always succeed, but may be perform expensive - computations. + ``DataFrame.peek(force=False)`` will always be very fast, but will not succeed if data requires + full data scanning. Using ``force=True`` will always succeed, but may be perform queries. + Query results will be cached so that future steps will benefit from these queries. Args: n (int, default 5): The number of rows to select from the dataframe. Which N rows are returned is non-deterministic. - force (bool, default False): + force (bool, default True): If the data cannot be peeked efficiently, the dataframe will instead be fully materialized as part - of the operation if force=True. If force=False, the operation will throw a ValueError. + of the operation if ``force=True``. If ``force=False``, the operation will throw a ValueError. Returns: pandas.DataFrame: A pandas DataFrame with n rows. diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 2048ada44f8..ee32fb25ac9 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -429,14 +429,14 @@ def test_rename(scalars_dfs): def test_df_peek(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - peek_result = scalars_df.peek(n=3) + peek_result = scalars_df.peek(n=3, force=False) pd.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) assert len(peek_result) == 3 def test_df_peek_filtered(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - peek_result = scalars_df[scalars_df.int64_col != 0].peek(n=3) + peek_result = scalars_df[scalars_df.int64_col != 0].peek(n=3, force=False) pd.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) assert len(peek_result) == 3 @@ -449,9 +449,9 @@ def test_df_peek_exception(scalars_dfs): scalars_df[["int64_col", "int64_too"]].cumsum().peek(n=3, force=False) -def test_df_peek_force(scalars_dfs): +def test_df_peek_force_default(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - peek_result = scalars_df[["int64_col", "int64_too"]].cumsum().peek(n=3, force=True) + peek_result = scalars_df[["int64_col", "int64_too"]].cumsum().peek(n=3) pd.testing.assert_index_equal( scalars_pandas_df[["int64_col", "int64_too"]].columns, peek_result.columns ) From f55680cd0eed46ee06cd9baf658de792f4a27f31 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 19 Mar 2024 12:46:17 -0700 Subject: [PATCH 384/921] fix: any() on empty set now correctly returns False (#471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/compile/aggregate_compiler.py | 2 +- tests/system/small/test_index.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 7059c4fdc19..9c1db0f1622 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -414,7 +414,7 @@ def _( result = _is_true(column).any() return cast( ibis_types.BooleanScalar, - _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), + _apply_window_if_present(result, window).fillna(ibis_types.literal(False)), ) diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index 2961884ebfd..1f39ba25fe1 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -68,6 +68,10 @@ def test_index_has_duplicates(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result +def test_index_empty_has_duplicates(): + assert not bpd.Index([]).has_duplicates + + def test_index_values(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.index.values pd_result = scalars_pandas_df_index.index.values From b519197d51cc098ac4981a9a57a9d6988ba07d03 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 19 Mar 2024 12:54:16 -0700 Subject: [PATCH 385/921] fix: fix broken multiindex loc cases (#467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/indexers.py | 157 ++++++++++---------------- bigframes/core/indexes/index.py | 3 +- bigframes/dataframe.py | 4 +- bigframes/series.py | 4 + tests/system/small/test_multiindex.py | 25 +++- 5 files changed, 88 insertions(+), 105 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 0a47c3a78e8..8d6a1cbdfed 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -15,7 +15,7 @@ from __future__ import annotations import typing -from typing import List, Tuple, Union +from typing import Tuple, Union import ibis import pandas as pd @@ -147,19 +147,22 @@ def __getitem__( ... def __getitem__(self, key): - # TODO(swast): If the DataFrame has a MultiIndex, we'll need to - # disambiguate this from a single row selection. + # TODO(tbergeron): Pandas will try both splitting 2-tuple into row, index or as 2-part + # row key. We must choose one, so bias towards treating as multi-part row label if isinstance(key, tuple) and len(key) == 2: - df = typing.cast( - bigframes.dataframe.DataFrame, - _loc_getitem_series_or_dataframe(self._dataframe, key[0]), - ) + is_row_multi_index = self._dataframe.index.nlevels > 1 + is_first_item_tuple = isinstance(key[0], tuple) + if not is_row_multi_index or is_first_item_tuple: + df = typing.cast( + bigframes.dataframe.DataFrame, + _loc_getitem_series_or_dataframe(self._dataframe, key[0]), + ) - columns = key[1] - if isinstance(columns, pd.Series) and columns.dtype == "bool": - columns = df.columns[columns] + columns = key[1] + if isinstance(columns, pd.Series) and columns.dtype == "bool": + columns = df.columns[columns] - return df[columns] + return df[columns] return typing.cast( bigframes.dataframe.DataFrame, @@ -283,94 +286,40 @@ def _loc_getitem_series_or_dataframe( pd.Series, bigframes.core.scalar.Scalar, ]: - if isinstance(key, bigframes.series.Series) and key.dtype == "boolean": - return series_or_dataframe[key] - elif isinstance(key, bigframes.series.Series): - temp_name = guid.generate_guid(prefix="temp_series_name_") - if len(series_or_dataframe.index.names) > 1: - temp_name = series_or_dataframe.index.names[0] - key = key.rename(temp_name) - keys_df = key.to_frame() - keys_df = keys_df.set_index(temp_name, drop=True) - return _perform_loc_list_join(series_or_dataframe, keys_df) - elif isinstance(key, bigframes.core.indexes.Index): - block = key._block - block = block.select_columns(()) - keys_df = bigframes.dataframe.DataFrame(block) - return _perform_loc_list_join(series_or_dataframe, keys_df) - elif pd.api.types.is_list_like(key): - key = typing.cast(List, key) - if len(key) == 0: - return typing.cast( - Union[bigframes.dataframe.DataFrame, bigframes.series.Series], - series_or_dataframe.iloc[0:0], - ) - if pd.api.types.is_list_like(key[0]): - original_index_names = series_or_dataframe.index.names - num_index_cols = len(original_index_names) - - entry_col_count_correct = [len(entry) == num_index_cols for entry in key] - if not all(entry_col_count_correct): - # pandas usually throws TypeError in these cases- tuple causes IndexError, but that - # seems like unintended behavior - raise TypeError( - "All entries must be of equal length when indexing by list of listlikes" - ) - temporary_index_names = [ - guid.generate_guid(prefix="temp_loc_index_") - for _ in range(len(original_index_names)) - ] - index_cols_dict = {} - for i in range(num_index_cols): - index_name = temporary_index_names[i] - values = [entry[i] for entry in key] - index_cols_dict[index_name] = values - keys_df = bigframes.dataframe.DataFrame( - index_cols_dict, session=series_or_dataframe._get_block().expr.session - ) - keys_df = keys_df.set_index(temporary_index_names, drop=True) - keys_df = keys_df.rename_axis(original_index_names) - else: - # We can't upload a DataFrame with None as the column name, so set it - # an arbitrary string. - index_name = series_or_dataframe.index.name - index_name_is_none = index_name is None - if index_name_is_none: - index_name = "unnamed_col" - keys_df = bigframes.dataframe.DataFrame( - {index_name: key}, - session=series_or_dataframe._get_block().expr.session, - ) - keys_df = keys_df.set_index(index_name, drop=True) - if index_name_is_none: - keys_df.index.name = None - return _perform_loc_list_join(series_or_dataframe, keys_df) - elif isinstance(key, slice): + if isinstance(key, slice): if (key.start is None) and (key.stop is None) and (key.step is None): return series_or_dataframe.copy() raise NotImplementedError( f"loc does not yet support indexing with a slice. {constants.FEEDBACK_LINK}" ) - elif callable(key): + if callable(key): raise NotImplementedError( f"loc does not yet support indexing with a callable. {constants.FEEDBACK_LINK}" ) - elif pd.api.types.is_scalar(key): - index_name = "unnamed_col" - keys_df = bigframes.dataframe.DataFrame( - {index_name: [key]}, session=series_or_dataframe._get_block().expr.session - ) - keys_df = keys_df.set_index(index_name, drop=True) - keys_df.index.name = None - result = _perform_loc_list_join(series_or_dataframe, keys_df) - pandas_result = result.to_pandas() - # although loc[scalar_key] returns multiple results when scalar_key - # is not unique, we download the results here and return the computed - # individual result (as a scalar or pandas series) when the key is unique, - # since we expect unique index keys to be more common. loc[[scalar_key]] - # can be used to retrieve one-item DataFrames or Series. - if len(pandas_result) == 1: - return pandas_result.iloc[0] + elif isinstance(key, bigframes.series.Series) and key.dtype == "boolean": + return series_or_dataframe[key] + elif ( + isinstance(key, bigframes.series.Series) + or isinstance(key, indexes.Index) + or (pd.api.types.is_list_like(key) and not isinstance(key, tuple)) + ): + index = indexes.Index(key, session=series_or_dataframe._session) + index.names = series_or_dataframe.index.names[: index.nlevels] + return _perform_loc_list_join(series_or_dataframe, index) + elif pd.api.types.is_scalar(key) or isinstance(key, tuple): + index = indexes.Index([key], session=series_or_dataframe._session) + index.names = series_or_dataframe.index.names[: index.nlevels] + result = _perform_loc_list_join(series_or_dataframe, index, drop_levels=True) + + if index.nlevels == series_or_dataframe.index.nlevels: + pandas_result = result.to_pandas() + # although loc[scalar_key] returns multiple results when scalar_key + # is not unique, we download the results here and return the computed + # individual result (as a scalar or pandas series) when the key is unique, + # since we expect unique index keys to be more common. loc[[scalar_key]] + # can be used to retrieve one-item DataFrames or Series. + if len(pandas_result) == 1: + return pandas_result.iloc[0] # when the key is not unique, we return a bigframes data type # as usual for methods that return dataframes/series return result @@ -385,7 +334,8 @@ def _loc_getitem_series_or_dataframe( @typing.overload def _perform_loc_list_join( series_or_dataframe: bigframes.series.Series, - keys_df: bigframes.dataframe.DataFrame, + keys_index: indexes.Index, + drop_levels: bool = False, ) -> bigframes.series.Series: ... @@ -393,31 +343,38 @@ def _perform_loc_list_join( @typing.overload def _perform_loc_list_join( series_or_dataframe: bigframes.dataframe.DataFrame, - keys_df: bigframes.dataframe.DataFrame, + keys_index: indexes.Index, + drop_levels: bool = False, ) -> bigframes.dataframe.DataFrame: ... def _perform_loc_list_join( series_or_dataframe: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], - keys_df: bigframes.dataframe.DataFrame, + keys_index: indexes.Index, + drop_levels: bool = False, ) -> Union[bigframes.series.Series, bigframes.dataframe.DataFrame]: # right join based on the old index so that the matching rows from the user's # original dataframe will be duplicated and reordered appropriately - original_index_names = series_or_dataframe.index.names if isinstance(series_or_dataframe, bigframes.series.Series): original_name = series_or_dataframe.name name = series_or_dataframe.name if series_or_dataframe.name is not None else "0" result = typing.cast( bigframes.series.Series, - series_or_dataframe.to_frame()._perform_join_by_index(keys_df, how="right")[ - name - ], + series_or_dataframe.to_frame()._perform_join_by_index( + keys_index, how="right" + )[name], ) result = result.rename(original_name) else: - result = series_or_dataframe._perform_join_by_index(keys_df, how="right") # type: ignore - result = result.rename_axis(original_index_names) + result = series_or_dataframe._perform_join_by_index(keys_index, how="right") # type: ignore + + if drop_levels and series_or_dataframe.index.nlevels > keys_index.nlevels: + # drop common levels + levels_to_drop = [ + name for name in series_or_dataframe.index.names if name in keys_index.names + ] + result = result.droplevel(levels_to_drop) # type: ignore return result diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index c8cb07d3392..958b742636f 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -49,6 +49,7 @@ def __init__( dtype=None, *, name=None, + session=None, ): import bigframes.dataframe as df import bigframes.series as series @@ -75,7 +76,7 @@ def __init__( else: pd_index = pandas.Index(data=data, dtype=dtype, name=name) pd_df = pandas.DataFrame(index=pd_index) - block = df.DataFrame(pd_df)._block + block = df.DataFrame(pd_df, session=session)._block self._query_job = None self._block: blocks.Block = block diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index c10a0c2456a..c0f602a5984 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2310,7 +2310,9 @@ def join( return left._perform_join_by_index(right, how=how) - def _perform_join_by_index(self, other: DataFrame, *, how: str = "left"): + def _perform_join_by_index( + self, other: Union[DataFrame, indexes.Index], *, how: str = "left" + ): block, _ = self._block.join(other._block, how=how, block_identity_join=True) return DataFrame(block) diff --git a/bigframes/series.py b/bigframes/series.py index d01ee88cf5e..6128238057a 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -151,6 +151,10 @@ def T(self) -> Series: def _info_axis(self) -> indexes.Index: return self.index + @property + def _session(self) -> bigframes.Session: + return self._get_block().expr.session + def transpose(self) -> Series: return self diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index e0b91643157..4a293526df8 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -169,15 +169,34 @@ def test_concat_multi_indices_ignore_index(scalars_df_index, scalars_pandas_df_i pandas.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) -def test_multi_index_loc(scalars_df_index, scalars_pandas_df_index): +@pytest.mark.parametrize( + ("key"), + [ + (2), + ([2, 0]), + ([(2, "capitalize, This "), (-2345, "Hello, World!")]), + ], +) +def test_multi_index_loc_multi_row(scalars_df_index, scalars_pandas_df_index, key): bf_result = ( - scalars_df_index.set_index(["int64_too", "bool_col"]).loc[[2, 0]].to_pandas() + scalars_df_index.set_index(["int64_too", "string_col"]).loc[key].to_pandas() ) - pd_result = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]).loc[[2, 0]] + pd_result = scalars_pandas_df_index.set_index(["int64_too", "string_col"]).loc[key] pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_multi_index_loc_single_row(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.set_index(["int64_too", "string_col"]).loc[ + (2, "capitalize, This ") + ] + pd_result = scalars_pandas_df_index.set_index(["int64_too", "string_col"]).loc[ + (2, "capitalize, This ") + ] + + pandas.testing.assert_series_equal(bf_result, pd_result) + + def test_multi_index_getitem_bool(scalars_df_index, scalars_pandas_df_index): bf_frame = scalars_df_index.set_index(["int64_too", "bool_col"]) pd_frame = scalars_pandas_df_index.set_index(["int64_too", "bool_col"]) From 4fbf938c200a3e0e6b592aa4a4e18b59f2f34082 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 19 Mar 2024 16:08:16 -0700 Subject: [PATCH 386/921] fix: groupby aggregates no longer check if grouping keys are numeric (#472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/groupby/__init__.py | 4 ++-- bigframes/dataframe.py | 1 + tests/system/small/test_groupby.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 837eb28f688..2b447a0190d 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -358,8 +358,8 @@ def _convert_index(self, dataframe: df.DataFrame): def _raise_on_non_numeric(self, op: str): if not all( - dtype in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE - for dtype in self._block.dtypes + self._column_type(col) in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE + for col in self._selected_cols ): raise NotImplementedError( f"'{op}' does not support non-numeric columns. " diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index c0f602a5984..0f99a3e4db5 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2337,6 +2337,7 @@ def groupby( blocks.Label, bigframes.series.Series, typing.Sequence[typing.Union[blocks.Label, bigframes.series.Series]], + None, ] = None, *, level: typing.Optional[LevelsType] = None, diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 2919c167efe..b38dcaf5d1a 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -371,3 +371,20 @@ def test_series_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False, check_names=False ) + + +def test_dataframe_groupby_nonnumeric_with_mean(): + df = pd.DataFrame( + { + "key1": ["a", "a", "a", "b"], + "key2": ["a", "a", "c", "c"], + "key3": [1, 2, 3, 4], + "key4": [1.6, 2, 3, 4], + } + ) + pd_result = df.groupby(["key1", "key2"]).mean() + bf_result = bpd.DataFrame(df).groupby(["key1", "key2"]).mean().to_pandas() + + pd.testing.assert_frame_equal( + pd_result, bf_result, check_index_type=False, check_dtype=False + ) From 4727563c3c619e9336b6138b1ec54794b40fd44a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:32:26 -0700 Subject: [PATCH 387/921] chore(main): release 0.26.0 (#445) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4edd37bed39..3bca26e361e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,46 @@ [1]: https://pypi.org/project/bigframes/#history +## [0.26.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.25.0...v0.26.0) (2024-03-20) + + +### ⚠ BREAKING CHANGES + +* exclude remote models for .register() ([#465](https://github.com/googleapis/python-bigquery-dataframes/issues/465)) + +### Features + +* (Series|DataFrame).plot ([#438](https://github.com/googleapis/python-bigquery-dataframes/issues/438)) ([1c3e668](https://github.com/googleapis/python-bigquery-dataframes/commit/1c3e668ceb26fd0f1377acbf6b95e8f4bcef40d6)) +* `read_gbq_table` supports `LIKE` as a operator in `filters` ([#454](https://github.com/googleapis/python-bigquery-dataframes/issues/454)) ([d2d425a](https://github.com/googleapis/python-bigquery-dataframes/commit/d2d425a93aa9e96f3b71c3ca3b185f4b5eaf32ef)) +* Add DataFrame.pipe() method ([#421](https://github.com/googleapis/python-bigquery-dataframes/issues/421)) ([95f5a6e](https://github.com/googleapis/python-bigquery-dataframes/commit/95f5a6e749468743af65062e559bc35ac56f3c24)) +* Set `force=True` by default in `DataFrame.peek()` ([#469](https://github.com/googleapis/python-bigquery-dataframes/issues/469)) ([4e8e97d](https://github.com/googleapis/python-bigquery-dataframes/commit/4e8e97d661078ed38d77be93b0bc1ad0fd52949c)) +* Support datetime related casting in (Series|DataFrame|Index).astype ([#442](https://github.com/googleapis/python-bigquery-dataframes/issues/442)) ([fde339b](https://github.com/googleapis/python-bigquery-dataframes/commit/fde339b71c754e617c61052940215b77890b59e4)) +* Support Series.dt.strftime ([#453](https://github.com/googleapis/python-bigquery-dataframes/issues/453)) ([8f6e955](https://github.com/googleapis/python-bigquery-dataframes/commit/8f6e955fc946db97c95ea012659432355b0cd12c)) + + +### Bug Fixes + +* Any() on empty set now correctly returns False ([#471](https://github.com/googleapis/python-bigquery-dataframes/issues/471)) ([f55680c](https://github.com/googleapis/python-bigquery-dataframes/commit/f55680cd0eed46ee06cd9baf658de792f4a27f31)) +* Df.drop_na preserves columns dtype ([#457](https://github.com/googleapis/python-bigquery-dataframes/issues/457)) ([3bab1a9](https://github.com/googleapis/python-bigquery-dataframes/commit/3bab1a917a5833bd58b20071a229ee95cf86a251)) +* Disable to_json and to_csv related tests ([#462](https://github.com/googleapis/python-bigquery-dataframes/issues/462)) ([874026d](https://github.com/googleapis/python-bigquery-dataframes/commit/874026da612bf08fbaf6d7dbfaa3325dc8a61500)) +* Exclude remote models for .register() ([#465](https://github.com/googleapis/python-bigquery-dataframes/issues/465)) ([73fe0f8](https://github.com/googleapis/python-bigquery-dataframes/commit/73fe0f89a96557afc4225521654978b96a2291b3)) +* Fix broken link in covid notebook ([#450](https://github.com/googleapis/python-bigquery-dataframes/issues/450)) ([adadb06](https://github.com/googleapis/python-bigquery-dataframes/commit/adadb0658c35142fed228abbd9baa42f9372f44b)) +* Fix broken multiindex loc cases ([#467](https://github.com/googleapis/python-bigquery-dataframes/issues/467)) ([b519197](https://github.com/googleapis/python-bigquery-dataframes/commit/b519197d51cc098ac4981a9a57a9d6988ba07d03)) +* Fix grouping series on multiple other series ([#455](https://github.com/googleapis/python-bigquery-dataframes/issues/455)) ([3971bd2](https://github.com/googleapis/python-bigquery-dataframes/commit/3971bd27c96b68b859399564dbb6abdb93de5f14)) +* Groupby aggregates no longer check if grouping keys are numeric ([#472](https://github.com/googleapis/python-bigquery-dataframes/issues/472)) ([4fbf938](https://github.com/googleapis/python-bigquery-dataframes/commit/4fbf938c200a3e0e6b592aa4a4e18b59f2f34082)) +* Raise `ValueError` when `read_pandas()` receives a bigframes `DataFrame` ([#447](https://github.com/googleapis/python-bigquery-dataframes/issues/447)) ([b28f9fd](https://github.com/googleapis/python-bigquery-dataframes/commit/b28f9fdd9681b3c9783a6e52322b70093e0283ec)) +* Series.(to_csv|to_json) leverages bq export ([#452](https://github.com/googleapis/python-bigquery-dataframes/issues/452)) ([718a00c](https://github.com/googleapis/python-bigquery-dataframes/commit/718a00c1fa8ac44b0d3a79a2217e5b12690785fb)) +* Warn when `read_gbq` / `read_gbq_table` uses the snapshot time cache ([#441](https://github.com/googleapis/python-bigquery-dataframes/issues/441)) ([e16a8c0](https://github.com/googleapis/python-bigquery-dataframes/commit/e16a8c0a6fb46cf1a7be12eec9471ae95d6f2c44)) + + +### Documentation + +* Add code samples for `ml.metrics.r2_score` ([#459](https://github.com/googleapis/python-bigquery-dataframes/issues/459)) ([85fefa2](https://github.com/googleapis/python-bigquery-dataframes/commit/85fefa2f1d4dbe3e0c9d4ab8124cea88eb5df38f)) +* Add the docs for loc and iloc indexers ([#446](https://github.com/googleapis/python-bigquery-dataframes/issues/446)) ([14ab8d8](https://github.com/googleapis/python-bigquery-dataframes/commit/14ab8d834d793ac7644f066145912e6d50966881)) +* Add the pages for at and iat indexers ([#456](https://github.com/googleapis/python-bigquery-dataframes/issues/456)) ([340f0b5](https://github.com/googleapis/python-bigquery-dataframes/commit/340f0b5b41fc5150d73890c7f27ae68dc308e160)) +* Add version information to bug template ([#437](https://github.com/googleapis/python-bigquery-dataframes/issues/437)) ([91bd39e](https://github.com/googleapis/python-bigquery-dataframes/commit/91bd39e8b194ddad09d53fca96201eee58063bb9)) +* Indicate that project and location are optional in example notebooks ([#451](https://github.com/googleapis/python-bigquery-dataframes/issues/451)) ([1df0140](https://github.com/googleapis/python-bigquery-dataframes/commit/1df014010652e7827a2720a906d0afe482a30ca9)) + ## [0.25.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.24.0...v0.25.0) (2024-03-14) diff --git a/bigframes/version.py b/bigframes/version.py index 708390a7cd1..8066f4353ac 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.25.0" +__version__ = "0.26.0" From fb5d83b1e35c465cff486e6cf7862e5b32e3c65a Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:37:50 -0700 Subject: [PATCH 388/921] feat: add ml PCA model params (#474) --- bigframes/ml/decomposition.py | 15 +++- tests/system/large/ml/test_decomposition.py | 71 +++++++++++++++++++ .../sklearn/decomposition/_pca.py | 4 +- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 9dc60be78f4..36fa28e141c 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import List, Optional, Union +from typing import List, Literal, Optional, Union import bigframes_vendored.sklearn.decomposition._pca from google.cloud import bigquery @@ -35,8 +35,14 @@ class PCA( ): __doc__ = bigframes_vendored.sklearn.decomposition._pca.PCA.__doc__ - def __init__(self, n_components: int = 3): + def __init__( + self, + n_components: int = 3, + *, + svd_solver: Literal["full", "randomized", "auto"] = "auto", + ): self.n_components = n_components + self.svd_solver = svd_solver self._bqml_model: Optional[core.BqmlModel] = None self._bqml_model_factory = globals.bqml_model_factory() @@ -44,12 +50,14 @@ def __init__(self, n_components: int = 3): def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> PCA: assert model.model_type == "PCA" - kwargs = {} + kwargs: dict = {} # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun last_fitting = model.training_runs[-1]["trainingOptions"] if "numPrincipalComponents" in last_fitting: kwargs["n_components"] = int(last_fitting["numPrincipalComponents"]) + if "pcaSolver" in last_fitting: + kwargs["svd_solver"] = str(last_fitting["pcaSolver"]) new_pca = cls(**kwargs) new_pca._bqml_model = core.BqmlModel(session, model) @@ -69,6 +77,7 @@ def _fit( options={ "model_type": "PCA", "num_principal_components": self.n_components, + "pca_solver": self.svd_solver, }, ) return self diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index 953287def2b..7932536e0c6 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -84,3 +84,74 @@ def test_decomposition_configure_fit_score_predict( in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_components == 3 + + +def test_decomposition_configure_fit_score_predict_params( + session, penguins_df_default_index, dataset_id +): + model = decomposition.PCA(n_components=5, svd_solver="randomized") + model.fit(penguins_df_default_index) + + new_penguins = session.read_pandas( + pd.DataFrame( + { + "tag_number": [1633, 1672, 1690], + "species": [ + "Adelie Penguin (Pygoscelis adeliae)", + "Gentoo penguin (Pygoscelis papua)", + "Adelie Penguin (Pygoscelis adeliae)", + ], + "island": ["Dream", "Biscoe", "Torgersen"], + "culmen_length_mm": [37.8, 46.5, 41.1], + "culmen_depth_mm": [18.1, 14.8, 18.6], + "flipper_length_mm": [193.0, 217.0, 189.0], + "body_mass_g": [3750.0, 5200.0, 3325.0], + "sex": ["MALE", "FEMALE", "MALE"], + } + ).set_index("tag_number") + ) + + # Check score to ensure the model was fitted + score_result = model.score(new_penguins).to_pandas() + score_expected = pd.DataFrame( + { + "total_explained_variance_ratio": [0.932897], + }, + dtype="Float64", + ) + score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) + + pd.testing.assert_frame_equal( + score_result, score_expected, check_exact=False, rtol=0.1 + ) + + result = model.predict(new_penguins).to_pandas() + expected = pd.DataFrame( + { + "principal_component_1": [-1.459, 2.258, -1.685], + "principal_component_2": [-1.120, -1.351, -0.874], + "principal_component_3": [-0.646, 0.443, -0.704], + "principal_component_4": [-0.539, 0.234, -0.571], + "principal_component_5": [-0.876, 0.122, 0.609], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + tests.system.utils.assert_pandas_df_equal_pca( + result, + expected, + check_exact=False, + rtol=0.1, + ) + + # save, load, check n_components to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_pca_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_pca_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.n_components == 5 + assert reloaded_model.svd_solver == "RANDOMIZED" diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 30c9c3b0b61..25d67f64c44 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -32,9 +32,11 @@ class PCA(BaseEstimator, metaclass=ABCMeta): truncated SVD. Args: - n_components (Optional[int], default 3): + n_components (Optional[int], default 3): Number of components to keep. if n_components is not set all components are kept. + svd_solver ("full", "randomized" or "auto", default "auto"): + The solver to use to calculate the principal components. Details: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-pca#pca_solver. """ From 21b2188cd0ca85485b5171ee9e46da4c924e2ff8 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:04:15 -0700 Subject: [PATCH 389/921] feat: add params for LinearRegression model (#464) * feat: add params for LinearRegression model * fix tests * update docs --- bigframes/ml/linear_model.py | 32 +++++++++++--- bigframes/ml/sql.py | 4 +- tests/system/large/ml/test_linear_model.py | 42 ++++++++++++------- tests/unit/ml/test_golden_sql.py | 4 +- .../sklearn/linear_model/_base.py | 10 ++++- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 68d1e126764..62855bae902 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -61,19 +61,25 @@ def __init__( "auto_strategy", "batch_gradient_descent", "normal_equation" ] = "normal_equation", fit_intercept: bool = True, + l1_reg: Optional[float] = None, l2_reg: float = 0.0, max_iterations: int = 20, + warm_start: bool = False, + learn_rate: Optional[float] = None, learn_rate_strategy: Literal["line_search", "constant"] = "line_search", early_stop: bool = True, min_rel_progress: float = 0.01, - ls_init_learn_rate: float = 0.1, + ls_init_learn_rate: Optional[float] = None, calculate_p_values: bool = False, enable_global_explain: bool = False, ): self.optimize_strategy = optimize_strategy self.fit_intercept = fit_intercept + self.l1_reg = l1_reg self.l2_reg = l2_reg self.max_iterations = max_iterations + self.warm_start = warm_start + self.learn_rate = learn_rate self.learn_rate_strategy = learn_rate_strategy self.early_stop = early_stop self.min_rel_progress = min_rel_progress @@ -99,17 +105,21 @@ def _from_bq( for bf_param, bf_value in dummy_linear.__dict__.items(): bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) if bqml_param in last_fitting: - kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + # Convert types + kwargs[bf_param] = ( + float(last_fitting[bqml_param]) + if bf_param in ["l1_reg", "learn_rate", "ls_init_learn_rate"] + else type(bf_value)(last_fitting[bqml_param]) + ) new_linear_regression = cls(**kwargs) new_linear_regression._bqml_model = core.BqmlModel(session, model) return new_linear_regression @property - def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + def _bqml_options(self) -> dict: """The model options as they will be set for BQML""" - # TODO: Support l1_reg, warm_start, and learn_rate with error catching. - return { + options = { "model_type": "LINEAR_REG", "data_split_method": "NO_SPLIT", "optimize_strategy": self.optimize_strategy, @@ -119,10 +129,20 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "learn_rate_strategy": self.learn_rate_strategy, "early_stop": self.early_stop, "min_rel_progress": self.min_rel_progress, - "ls_init_learn_rate": self.ls_init_learn_rate, "calculate_p_values": self.calculate_p_values, "enable_global_explain": self.enable_global_explain, } + if self.l1_reg is not None: + options["l1_reg"] = self.l1_reg + if self.learn_rate is not None: + options["learn_rate"] = self.learn_rate + if self.ls_init_learn_rate is not None: + options["ls_init_learn_rate"] = self.ls_init_learn_rate + # Even presenting warm_start returns error for NORMAL_EQUATION optimizer + if self.warm_start is True: + options["warm_start"] = self.warm_start + + return options def _fit( self, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index fa74458e775..807fadc06a9 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -38,7 +38,9 @@ def encode_value(self, v: Union[str, int, float, Iterable[str]]) -> str: inner = ", ".join([self.encode_value(x) for x in v]) return f"[{inner}]" else: - raise ValueError(f"Unexpected value type. {constants.FEEDBACK_LINK}") + raise ValueError( + f"Unexpected value type {type(v)}. {constants.FEEDBACK_LINK}" + ) def build_parameters(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: """Encode a dict of values into a formatted Iterable of key-value pairs for SQL""" diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index a0f4182e6fb..03fed007700 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -60,9 +60,11 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase assert reloaded_model.calculate_p_values is False assert reloaded_model.early_stop is True assert reloaded_model.enable_global_explain is False + assert reloaded_model.l1_reg is None assert reloaded_model.l2_reg == 0.0 + assert reloaded_model.learn_rate is None assert reloaded_model.learn_rate_strategy == "line_search" - assert reloaded_model.ls_init_learn_rate == 0.1 + assert reloaded_model.ls_init_learn_rate is None assert reloaded_model.max_iterations == 20 assert reloaded_model.min_rel_progress == 0.01 @@ -71,7 +73,14 @@ def test_linear_regression_customized_params_fit_score( penguins_df_default_index, dataset_id ): model = bigframes.ml.linear_model.LinearRegression( - fit_intercept=False, l2_reg=0.1, min_rel_progress=0.01 + fit_intercept=False, + l2_reg=0.2, + min_rel_progress=0.02, + l1_reg=0.2, + max_iterations=30, + optimize_strategy="batch_gradient_descent", + learn_rate_strategy="constant", + learn_rate=0.2, ) df = penguins_df_default_index.dropna() @@ -92,12 +101,12 @@ def test_linear_regression_customized_params_fit_score( result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { - "mean_absolute_error": [226.108411], - "mean_squared_error": [80459.668456], - "mean_squared_log_error": [0.00497], - "median_absolute_error": [171.618872], - "r2_score": [0.875415], - "explained_variance": [0.875417], + "mean_absolute_error": [240], + "mean_squared_error": [91197], + "mean_squared_log_error": [0.00573], + "median_absolute_error": [197], + "r2_score": [0.858], + "explained_variance": [0.8588], }, dtype="Float64", ) @@ -109,16 +118,21 @@ def test_linear_regression_customized_params_fit_score( assert ( f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name ) - assert reloaded_model.optimize_strategy == "NORMAL_EQUATION" + assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" assert reloaded_model.fit_intercept is False assert reloaded_model.calculate_p_values is False assert reloaded_model.early_stop is True assert reloaded_model.enable_global_explain is False - assert reloaded_model.l2_reg == 0.1 - assert reloaded_model.learn_rate_strategy == "line_search" - assert reloaded_model.ls_init_learn_rate == 0.1 - assert reloaded_model.max_iterations == 20 - assert reloaded_model.min_rel_progress == 0.01 + assert reloaded_model.l1_reg == 0.2 + assert reloaded_model.l2_reg == 0.2 + assert reloaded_model.ls_init_learn_rate is None + assert reloaded_model.max_iterations == 30 + assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.learn_rate_strategy == "CONSTANT" + assert reloaded_model.learn_rate == 0.2 + + +# TODO(garrettwu): add tests for param warm_start. Requires a trained model. def test_logistic_regression_configure_fit_score(penguins_df_default_index, dataset_id): diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index d63bc7aaa10..8996a9c77f1 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -105,7 +105,7 @@ def test_linear_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -115,7 +115,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n ls_init_learn_rate=0.1,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index ad2c8724688..39012cbe088 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -71,18 +71,24 @@ class LinearRegression(RegressorMixin, LinearModel): Default ``True``. Whether to calculate the intercept for this model. If set to False, no intercept will be used in calculations (i.e. data is expected to be centered). + l1_reg (float or None, default None): + The amount of L1 regularization applied. Default to None. Can't be set in "normal_equation" mode. If unset, value 0 is used. l2_reg (float, default 0.0): The amount of L2 regularization applied. Default to 0. max_iterations (int, default 20): The maximum number of training iterations or steps. Default to 20. + warm_start (bool, default False): + Determines whether to train a model with new training data, new model options, or both. Unless you explicitly override them, the initial options used to train the model are used for the warm start run. Default to False. + learn_rate (float or None, default None): + The learn rate for gradient descent when learn_rate_strategy='constant'. If unset, value 0.1 is used. If learn_rate_strategy='line_search', an error is returned. learn_rate_strategy (str, default "line_search"): The strategy for specifying the learning rate during training. Default to "line_search". early_stop (bool, default True): Whether training should stop after the first iteration in which the relative loss improvement is less than the value specified for min_rel_progress. Default to True. min_rel_progress (float, default 0.01): The minimum relative loss improvement that is necessary to continue training when EARLY_STOP is set to true. For example, a value of 0.01 specifies that each iteration must reduce the loss by 1% for training to continue. Default to 0.01. - ls_init_learn_rate (float, default 0.1): - Sets the initial learning rate that learn_rate_strategy='line_search' uses. This option can only be used if line_search is specified. Default to 0.1. + ls_init_learn_rate (float or None, default None): + Sets the initial learning rate that learn_rate_strategy='line_search' uses. This option can only be used if line_search is specified. If unset, value 0.1 is used. calculate_p_values (bool, default False): Specifies whether to compute p-values and standard errors during training. Default to False. enable_global_explain (bool, default False): From 2b9a01de0adb8d41fbe73ce94b1acc8d22f507b5 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:06:39 -0700 Subject: [PATCH 390/921] fix: renable to_csv and to_json related tests (#468) * fix: renable to_csv and to_json related tests * fix gcs file path * add global FIRST_GCS_FILE_SUFFIX * trying to avoid import functions --- tests/system/small/test_dataframe_io.py | 35 +++++++++++++------------ tests/system/small/test_encryption.py | 3 ++- tests/system/small/test_series.py | 7 +++-- tests/system/small/test_session.py | 26 +++++++++--------- tests/system/utils.py | 4 +++ 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index adc729565e6..10d74087903 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -19,7 +19,7 @@ import pyarrow as pa import pytest -from tests.system.utils import assert_pandas_df_equal, convert_pandas_dtypes +from tests.system import utils try: import pandas_gbq # type: ignore @@ -115,7 +115,6 @@ def test_to_pandas_batches_w_correct_dtypes(scalars_df_default_index): pd.testing.assert_series_equal(actual, expected) -@pytest.mark.skip(reason="Disable to unblock kokoro tests") @pytest.mark.parametrize( ("index"), [True, False], @@ -150,12 +149,12 @@ def test_to_csv_index( # read_csv will decode into bytes inproperly, convert_pandas_dtypes will encode properly from string dtype.pop("bytes_col") gcs_df = pd.read_csv( - path, + utils.get_first_file_from_wildcard(path), dtype=dtype, date_format={"timestamp_col": "YYYY-MM-DD HH:MM:SS Z"}, index_col=index_col, ) - convert_pandas_dtypes(gcs_df, bytes_col=True) + utils.convert_pandas_dtypes(gcs_df, bytes_col=True) gcs_df.index.name = scalars_df.index.name scalars_pandas_df = scalars_pandas_df.copy() @@ -164,7 +163,6 @@ def test_to_csv_index( pd.testing.assert_frame_equal(gcs_df, scalars_pandas_df) -@pytest.mark.skip(reason="Disable to unblock kokoro tests") def test_to_csv_tabs( scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], gcs_folder: str, @@ -189,13 +187,13 @@ def test_to_csv_tabs( # read_csv will decode into bytes inproperly, convert_pandas_dtypes will encode properly from string dtype.pop("bytes_col") gcs_df = pd.read_csv( - path, + utils.get_first_file_from_wildcard(path), sep="\t", dtype=dtype, date_format={"timestamp_col": "YYYY-MM-DD HH:MM:SS Z"}, index_col=index_col, ) - convert_pandas_dtypes(gcs_df, bytes_col=True) + utils.convert_pandas_dtypes(gcs_df, bytes_col=True) gcs_df.index.name = scalars_df.index.name scalars_pandas_df = scalars_pandas_df.copy() @@ -229,7 +227,7 @@ def test_to_gbq_index(scalars_dfs, dataset_id, index): else: df_out = df_out.sort_values("rowindex_2").reset_index(drop=True) - convert_pandas_dtypes(df_out, bytes_col=False) + utils.convert_pandas_dtypes(df_out, bytes_col=False) # pd.read_gbq interpets bytes_col as object, reconvert to pyarrow binary df_out["bytes_col"] = df_out["bytes_col"].astype(pd.ArrowDtype(pa.binary())) expected = scalars_pandas_df.copy() @@ -415,7 +413,6 @@ def test_to_json_index_invalid_lines( scalars_df.to_json(path, index=index) -@pytest.mark.skip(reason="Disable to unblock kokoro tests") @pytest.mark.parametrize( ("index"), [True, False], @@ -435,8 +432,12 @@ def test_to_json_index_records_orient( """ Test the `to_json` API with `orient` is `records` and `lines` is True""" scalars_df.to_json(path, index=index, orient="records", lines=True) - gcs_df = pd.read_json(path, lines=True, convert_dates=["datetime_col"]) - convert_pandas_dtypes(gcs_df, bytes_col=True) + gcs_df = pd.read_json( + utils.get_first_file_from_wildcard(path), + lines=True, + convert_dates=["datetime_col"], + ) + utils.convert_pandas_dtypes(gcs_df, bytes_col=True) if index and scalars_df.index.name is not None: gcs_df = gcs_df.set_index(scalars_df.index.name) @@ -474,8 +475,8 @@ def test_to_parquet_index(scalars_dfs, gcs_folder, index): # table. scalars_df.to_parquet(path, index=index) - gcs_df = pd.read_parquet(path.replace("*", "000000000000")) - convert_pandas_dtypes(gcs_df, bytes_col=False) + gcs_df = pd.read_parquet(utils.get_first_file_from_wildcard(path)) + utils.convert_pandas_dtypes(gcs_df, bytes_col=False) if index and scalars_df.index.name is not None: gcs_df = gcs_df.set_index(scalars_df.index.name) @@ -507,7 +508,7 @@ def test_to_sql_query_unnamed_index_included( pd_df = scalars_pandas_df_default_index.reset_index(drop=True) roundtrip = session.read_gbq(sql, index_col=idx_ids) roundtrip.index.names = [None] - assert_pandas_df_equal(roundtrip.to_pandas(), pd_df, check_index_type=False) + utils.assert_pandas_df_equal(roundtrip.to_pandas(), pd_df, check_index_type=False) def test_to_sql_query_named_index_included( @@ -524,7 +525,7 @@ def test_to_sql_query_named_index_included( pd_df = scalars_pandas_df_default_index.set_index("rowindex_2", drop=True) roundtrip = session.read_gbq(sql, index_col=idx_ids) - assert_pandas_df_equal(roundtrip.to_pandas(), pd_df) + utils.assert_pandas_df_equal(roundtrip.to_pandas(), pd_df) def test_to_sql_query_unnamed_index_excluded( @@ -539,7 +540,7 @@ def test_to_sql_query_unnamed_index_excluded( pd_df = scalars_pandas_df_default_index.reset_index(drop=True) roundtrip = session.read_gbq(sql) - assert_pandas_df_equal( + utils.assert_pandas_df_equal( roundtrip.to_pandas(), pd_df, check_index_type=False, ignore_order=True ) @@ -558,6 +559,6 @@ def test_to_sql_query_named_index_excluded( "rowindex_2", drop=True ).reset_index(drop=True) roundtrip = session.read_gbq(sql) - assert_pandas_df_equal( + utils.assert_pandas_df_equal( roundtrip.to_pandas(), pd_df, check_index_type=False, ignore_order=True ) diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index f13d2b9e1a3..70d2ce381fc 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -19,6 +19,7 @@ import bigframes import bigframes.ml.linear_model +from tests.system import utils @pytest.fixture(scope="module") @@ -160,7 +161,7 @@ def test_read_csv_gcs( # Create a csv in gcs write_path = gcs_folder + "test_read_csv_gcs_bigquery_engine*.csv" read_path = ( - write_path.replace("*", "000000000000") if engine is None else write_path + utils.get_first_file_from_wildcard(write_path) if engine is None else write_path ) scalars_df_index.to_csv(write_path) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index f63ea977ffc..dcb47d8c60b 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -27,6 +27,7 @@ from tests.system.utils import ( assert_pandas_df_equal, assert_series_equal, + get_first_file_from_wildcard, skip_legacy_pandas, ) @@ -2390,11 +2391,10 @@ def test_to_frame(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) -@pytest.mark.skip(reason="Disable to unblock kokoro tests") def test_to_json(gcs_folder, scalars_df_index, scalars_pandas_df_index): path = gcs_folder + "test_series_to_json*.jsonl" scalars_df_index["int64_col"].to_json(path, lines=True, orient="records") - gcs_df = pd.read_json(path, lines=True) + gcs_df = pd.read_json(get_first_file_from_wildcard(path), lines=True) pd.testing.assert_series_equal( gcs_df["int64_col"].astype(pd.Int64Dtype()), @@ -2404,11 +2404,10 @@ def test_to_json(gcs_folder, scalars_df_index, scalars_pandas_df_index): ) -@pytest.mark.skip(reason="Disable to unblock kokoro tests") def test_to_csv(gcs_folder, scalars_df_index, scalars_pandas_df_index): path = gcs_folder + "test_series_to_csv*.csv" scalars_df_index["int64_col"].to_csv(path) - gcs_df = pd.read_csv(path) + gcs_df = pd.read_csv(get_first_file_from_wildcard(path)) pd.testing.assert_series_equal( gcs_df["int64_col"].astype(pd.Int64Dtype()), diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index d0cd24e2beb..c6702aa032b 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -30,9 +30,7 @@ import bigframes.dataframe import bigframes.dtypes import bigframes.ml.linear_model -from tests.system.utils import skip_legacy_pandas - -FIRST_FILE = "000000000000" +from tests.system import utils def test_read_gbq_tokyo( @@ -435,14 +433,14 @@ def test_read_pandas_tokyo( pd.testing.assert_frame_equal(result, expected) -@skip_legacy_pandas +@utils.skip_legacy_pandas def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs if scalars_df.index.name is not None: path = gcs_folder + "test_read_csv_gcs_default_engine_w_index*.csv" else: path = gcs_folder + "test_read_csv_gcs_default_engine_wo_index*.csv" - read_path = path.replace("*", FIRST_FILE) + read_path = utils.get_first_file_from_wildcard(path) scalars_df.to_csv(path, index=False) dtype = scalars_df.dtypes.to_dict() dtype.pop("geography_col") @@ -492,7 +490,7 @@ def test_read_csv_gcs_bq_engine(session, scalars_dfs, gcs_folder): pytest.param("\t", id="custom_sep"), ], ) -@skip_legacy_pandas +@utils.skip_legacy_pandas def test_read_csv_local_default_engine(session, scalars_dfs, sep): scalars_df, scalars_pandas_df = scalars_dfs with tempfile.TemporaryDirectory() as dir: @@ -641,7 +639,7 @@ def test_read_csv_default_engine_throws_not_implemented_error( gcs_folder + "test_read_csv_gcs_default_engine_throws_not_implemented_error*.csv" ) - read_path = path.replace("*", FIRST_FILE) + read_path = utils.get_first_file_from_wildcard(path) scalars_df_index.to_csv(path) with pytest.raises(NotImplementedError, match=match): session.read_csv(read_path, **kwargs) @@ -649,7 +647,7 @@ def test_read_csv_default_engine_throws_not_implemented_error( def test_read_csv_gcs_default_engine_w_header(session, scalars_df_index, gcs_folder): path = gcs_folder + "test_read_csv_gcs_default_engine_w_header*.csv" - read_path = path.replace("*", FIRST_FILE) + read_path = utils.get_first_file_from_wildcard(path) scalars_df_index.to_csv(path) # Skips header=N rows, normally considers the N+1th row as the header, but overridden by @@ -716,7 +714,7 @@ def test_read_csv_gcs_default_engine_w_index_col_name( session, scalars_df_default_index, gcs_folder ): path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_name*.csv" - read_path = path.replace("*", FIRST_FILE) + read_path = utils.get_first_file_from_wildcard(path) scalars_df_default_index.to_csv(path) df = session.read_csv(read_path, index_col="rowindex") @@ -731,7 +729,7 @@ def test_read_csv_gcs_default_engine_w_index_col_index( session, scalars_df_default_index, gcs_folder ): path = gcs_folder + "test_read_csv_gcs_default_engine_w_index_col_index*.csv" - read_path = path.replace("*", FIRST_FILE) + read_path = utils.get_first_file_from_wildcard(path) scalars_df_default_index.to_csv(path) index_col = scalars_df_default_index.columns.to_list().index("rowindex") @@ -790,7 +788,7 @@ def test_read_csv_local_default_engine_w_index_col_index( def test_read_csv_gcs_w_usecols(session, scalars_df_index, gcs_folder, engine): path = gcs_folder + "test_read_csv_gcs_w_usecols" path = path + "_default_engine*.csv" if engine is None else path + "_bq_engine*.csv" - read_path = path.replace("*", FIRST_FILE) if engine is None else path + read_path = utils.get_first_file_from_wildcard(path) if engine is None else path scalars_df_index.to_csv(path) # df should only have 1 column which is bool_col. @@ -902,7 +900,7 @@ def test_read_parquet_gcs(session: bigframes.Session, scalars_dfs, gcs_folder, e # Only bigquery engine for reads supports wildcards in path name. if engine != "bigquery": - path = path.replace("*", "000000000000") + path = utils.get_first_file_from_wildcard(path) df_out = ( session.read_parquet(path, engine=engine) @@ -1012,7 +1010,7 @@ def test_read_parquet_gcs_compression_not_supported( def test_read_json_gcs_bq_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs path = gcs_folder + "test_read_json_gcs_bq_engine_w_index*.json" - read_path = path.replace("*", FIRST_FILE) + read_path = utils.get_first_file_from_wildcard(path) scalars_df.to_json(path, index=False, lines=True, orient="records") df = session.read_json(read_path, lines=True, orient="records", engine="bigquery") @@ -1036,7 +1034,7 @@ def test_read_json_gcs_bq_engine(session, scalars_dfs, gcs_folder): def test_read_json_gcs_default_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs path = gcs_folder + "test_read_json_gcs_default_engine_w_index*.json" - read_path = path.replace("*", FIRST_FILE) + read_path = utils.get_first_file_from_wildcard(path) scalars_df.to_json( path, index=False, diff --git a/tests/system/utils.py b/tests/system/utils.py index 8ea49ed7e29..e40502e6f2a 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -304,3 +304,7 @@ def delete_cloud_function( request = functions_v2.DeleteFunctionRequest(name=full_name) operation = functions_client.delete_function(request=request) return operation + + +def get_first_file_from_wildcard(path): + return path.replace("*", "000000000000") From 43d0864143419a3569148027e6f195de9cdccb55 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 20 Mar 2024 15:04:46 -0700 Subject: [PATCH 391/921] refactor: complete dtype rules for expression tree transformations (#376) --- bigframes/core/__init__.py | 59 +++++-- bigframes/core/blocks.py | 40 ++--- bigframes/core/compile/compiled.py | 57 ++----- bigframes/core/compile/compiler.py | 5 +- bigframes/core/compile/scalar_op_compiler.py | 98 ++++++++--- bigframes/core/expression.py | 20 ++- bigframes/core/local_data.py | 63 ++++++++ bigframes/core/nodes.py | 153 +++++++++++++++++- bigframes/core/schema.py | 71 ++++++++ bigframes/dataframe.py | 7 + bigframes/dtypes.py | 27 ++++ bigframes/functions/remote_function.py | 8 + bigframes/operations/__init__.py | 40 +++-- bigframes/operations/aggregations.py | 101 ++++++++++++ bigframes/operations/type.py | 13 +- bigframes/pandas/__init__.py | 4 + bigframes/series.py | 6 + bigframes/session/__init__.py | 52 ++++-- mypy.ini | 3 + tests/system/small/test_dataframe.py | 52 +++++- tests/system/small/test_series.py | 31 +++- tests/unit/core/test_blocks.py | 2 +- .../bigframes_vendored/pandas/core/generic.py | 6 +- .../bigframes_vendored/pandas/core/series.py | 54 +++---- 24 files changed, 794 insertions(+), 178 deletions(-) create mode 100644 bigframes/core/local_data.py create mode 100644 bigframes/core/schema.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 02582b17ba6..c37cd14b77b 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -14,21 +14,26 @@ from __future__ import annotations from dataclasses import dataclass +import functools import io import typing from typing import Iterable, Sequence import ibis.expr.types as ibis_types import pandas +import pyarrow as pa +import pyarrow.feather as pa_feather import bigframes.core.compile as compiling import bigframes.core.expression as ex import bigframes.core.guid import bigframes.core.join_def as join_def +import bigframes.core.local_data as local_data import bigframes.core.nodes as nodes from bigframes.core.ordering import OrderingColumnReference import bigframes.core.ordering as orderings import bigframes.core.rewrite +import bigframes.core.schema as schemata import bigframes.core.utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes @@ -63,28 +68,32 @@ def from_ibis( node = nodes.ReadGbqNode( table=table, table_session=session, - columns=tuple(columns), + columns=tuple( + bigframes.dtypes.ibis_value_to_canonical_type(column) + for column in columns + ), hidden_ordering_columns=tuple(hidden_ordering_columns), ordering=ordering, ) return cls(node) @classmethod - def from_pandas(cls, pd_df: pandas.DataFrame, session: bigframes.Session): + def from_pyarrow(cls, arrow_table: pa.Table, session: Session): + adapted_table = local_data.adapt_pa_table(arrow_table) + schema = local_data.arrow_schema_to_bigframes(adapted_table.schema) + iobytes = io.BytesIO() - # Use alphanumeric identifiers, to avoid downstream problems with escaping. - as_ids = [ - bigframes.core.utils.label_to_identifier(label, strict=True) - for label in pd_df.columns - ] - unique_ids = tuple(bigframes.core.utils.disambiguate_ids(as_ids)) - pd_df.reset_index(drop=True).set_axis(unique_ids, axis=1).to_feather(iobytes) - node = nodes.ReadLocalNode(feather_bytes=iobytes.getvalue(), session=session) + pa_feather.write_feather(adapted_table, iobytes) + node = nodes.ReadLocalNode( + iobytes.getvalue(), + data_schema=schema, + session=session, + ) return cls(node) @property def column_ids(self) -> typing.Sequence[str]: - return self._compile_ordered().column_ids + return self.schema.names @property def session(self) -> Session: @@ -95,6 +104,32 @@ def session(self) -> Session: required_session if (required_session is not None) else get_global_session() ) + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + # TODO: switch to use self.node.schema + return self._compiled_schema + + @functools.cached_property + def _compiled_schema(self) -> schemata.ArraySchema: + compiled = self._compile_unordered() + items = tuple( + schemata.SchemaItem(id, compiled.get_column_type(id)) + for id in compiled.column_ids + ) + return schemata.ArraySchema(items) + + def validate_schema(self): + tree_derived = self.node.schema + ibis_derived = self._compiled_schema + if tree_derived.names != ibis_derived.names: + raise ValueError( + f"Unexpected names internal {tree_derived.names} vs compiled {ibis_derived.names}" + ) + if tree_derived.dtypes != ibis_derived.dtypes: + raise ValueError( + f"Unexpected types internal {tree_derived.dtypes} vs compiled {ibis_derived.dtypes}" + ) + def _try_evaluate_local(self): """Use only for unit testing paths - not fully featured. Will throw exception if fails.""" import ibis @@ -104,7 +139,7 @@ def _try_evaluate_local(self): ) def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: - return self._compile_ordered().get_column_type(key) + return self.schema.get_type(key) def _compile_ordered(self) -> compiling.OrderedIR: return compiling.compile_ordered_ir(self.node) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 0ebbe48cc40..074b17762fb 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -29,9 +29,9 @@ from typing import Iterable, List, Mapping, Optional, Sequence, Tuple import warnings -import bigframes_vendored.pandas.io.common as vendored_pandas_io_common import google.cloud.bigquery as bigquery import pandas as pd +import pyarrow as pa import bigframes._config.sampling_options as sampling_options import bigframes.constants as constants @@ -141,32 +141,23 @@ def __init__( self._stats_cache[" ".join(self.index_columns)] = {} @classmethod - def from_local(cls, data, session: bigframes.Session) -> Block: - pd_data = pd.DataFrame(data) - columns = pd_data.columns - - # Make a flattened version to treat as a table. - if len(pd_data.columns.names) > 1: - pd_data.columns = columns.to_flat_index() - + def from_local(cls, data: pd.DataFrame, session: bigframes.Session) -> Block: + # Assumes caller has already converted datatypes to bigframes ones. + pd_data = data + column_labels = pd_data.columns index_labels = list(pd_data.index.names) - # The ArrayValue layer doesn't know about indexes, so make sure indexes - # are real columns with unique IDs. - pd_data = pd_data.reset_index( - names=[f"level_{level}" for level in range(len(index_labels))] - ) - pd_data = pd_data.set_axis( - vendored_pandas_io_common.dedup_names( - list(pd_data.columns), is_potential_multiindex=False - ), - axis="columns", - ) - index_ids = pd_data.columns[: len(index_labels)] - keys_expr = core.ArrayValue.from_pandas(pd_data, session) + # unique internal ids + column_ids = [f"column_{i}" for i in range(len(pd_data.columns))] + index_ids = [f"level_{level}" for level in range(pd_data.index.nlevels)] + + pd_data = pd_data.set_axis(column_ids, axis=1) + pd_data = pd_data.reset_index(names=index_ids) + as_pyarrow = pa.Table.from_pandas(pd_data, preserve_index=False) + array_value = core.ArrayValue.from_pyarrow(as_pyarrow, session=session) return cls( - keys_expr, - column_labels=columns, + array_value, + column_labels=column_labels, index_columns=index_ids, index_labels=index_labels, ) @@ -484,6 +475,7 @@ def _copy_index_to_pandas(self, df: pd.DataFrame): # general Sequence[Label] that BigQuery DataFrames has. # See: https://github.com/pandas-dev/pandas-stubs/issues/804 df.index.names = self.index.names # type: ignore + df.columns = self.column_labels def _materialize_local( self, materialize_options: MaterializationOptions = MaterializationOptions() diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 7245689aae2..34e7bf1ce2b 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -36,6 +36,7 @@ IntegerEncoding, OrderingColumnReference, ) +import bigframes.core.schema as schemata import bigframes.core.utils as utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes @@ -627,56 +628,30 @@ def __init__( def from_pandas( cls, pd_df: pandas.DataFrame, + schema: schemata.ArraySchema, ) -> OrderedIR: """ Builds an in-memory only (SQL only) expr from a pandas dataframe. + + Assumed that the dataframe has unique string column names and bigframes-suppported dtypes. """ - # We can't include any hidden columns in the ArrayValue constructor, so - # grab the column names before we add the hidden ordering column. - column_names = [str(column) for column in pd_df.columns] - # Make sure column names are all strings. - pd_df = pd_df.set_axis(column_names, axis="columns") - pd_df = pd_df.assign(**{ORDER_ID_COLUMN: range(len(pd_df))}) # ibis memtable cannot handle NA, must convert to None - pd_df = pd_df.astype("object") # type: ignore - pd_df = pd_df.where(pandas.notnull(pd_df), None) - - # NULL type isn't valid in BigQuery, so retry with an explicit schema in these cases. - keys_memtable = ibis.memtable(pd_df) - schema = keys_memtable.schema() - new_schema = [] - for column_index, column in enumerate(schema): - if column == ORDER_ID_COLUMN: - new_type: ibis_dtypes.DataType = ibis_dtypes.int64 - else: - column_type = schema[column] - # The autodetected type might not be one we can support, such - # as NULL type for empty rows, so convert to a type we do - # support. - new_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype( - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(column_type) - ) - # TODO(swast): Ibis memtable doesn't use backticks in struct - # field names, so spaces and other characters aren't allowed in - # the memtable context. Blocked by - # https://github.com/ibis-project/ibis/issues/7187 - column = f"col_{column_index}" - new_schema.append((column, new_type)) - - # must set non-null column labels. these are not the user-facing labels - pd_df = pd_df.set_axis( - [column for column, _ in new_schema], - axis="columns", - ) - keys_memtable = ibis.memtable(pd_df, schema=ibis.schema(new_schema)) + # this destroys the schema however + ibis_values = pd_df.astype("object").where(pandas.notnull(pd_df), None) # type: ignore + ibis_values = ibis_values.assign(**{ORDER_ID_COLUMN: range(len(pd_df))}) + # derive the ibis schema from the original pandas schema + ibis_schema = [ + (name, bigframes.dtypes.bigframes_dtype_to_ibis_dtype(dtype)) + for name, dtype in zip(schema.names, schema.dtypes) + ] + ibis_schema.append((ORDER_ID_COLUMN, ibis_dtypes.int64)) + + keys_memtable = ibis.memtable(ibis_values, schema=ibis.schema(ibis_schema)) return cls( keys_memtable, - columns=[ - keys_memtable[f"col_{column_index}"].name(column) - for column_index, column in enumerate(column_names) - ], + columns=[keys_memtable[column].name(column) for column in pd_df.columns], ordering=ExpressionOrdering( ordering_value_columns=tuple( [OrderingColumnReference(ORDER_ID_COLUMN)] diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index ec6c79db5f5..6f10d85f31c 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -41,7 +41,8 @@ def compile_peak_sql(node: nodes.BigFrameNode, n_rows: int) -> typing.Optional[s return compile_unordered_ir(node).peek_sql(n_rows) -@functools.cache +# TODO: Remove cache when schema no longer requires compilation to derive schema (and therefor only compiles for execution) +@functools.lru_cache(maxsize=5000) def compile_node( node: nodes.BigFrameNode, ordered: bool = True ) -> compiled.UnorderedIR | compiled.OrderedIR: @@ -80,7 +81,7 @@ def compile_join(node: nodes.JoinNode, ordered: bool = True): @_compile_node.register def compile_readlocal(node: nodes.ReadLocalNode, ordered: bool = True): array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) - ordered_ir = compiled.OrderedIR.from_pandas(array_as_pd) + ordered_ir = compiled.OrderedIR.from_pandas(array_as_pd, node.schema) if ordered: return ordered_ir else: diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index c95d1ca45e0..2a93f08e90b 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -1070,8 +1070,16 @@ def floordiv_op( ) -def _is_float(x: ibis_types.Value): - return isinstance(x, (ibis_types.FloatingColumn, ibis_types.FloatingScalar)) +def _is_bignumeric(x: ibis_types.Value): + if not isinstance(x, ibis_types.DecimalValue): + return False + # Should be exactly 76 for bignumeric + return x.precision > 70 + + +def _is_numeric(x: ibis_types.Value): + # either big-numeric or numeric + return isinstance(x, ibis_types.DecimalValue) @scalar_op_compiler.register_binary_op(ops.mod_op) @@ -1080,40 +1088,88 @@ def mod_op( x: ibis_types.Value, y: ibis_types.Value, ): - is_result_float = _is_float(x) | _is_float(y) - x_numeric = typing.cast( - ibis_types.NumericValue, - x.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) - if is_result_float - else x, - ) - y_numeric = typing.cast( - ibis_types.NumericValue, - y.cast(ibis_dtypes.Decimal(precision=38, scale=9, nullable=True)) - if is_result_float - else y, + # Hacky short-circuit to avoid passing zero-literal to sql backend, evaluate locally instead to null. + op = y.op() + if isinstance(op, ibis.expr.operations.generic.Literal) and op.value == 0: + return ibis_types.null().cast(x.type()) + + if x.type().is_integer() and y.type().is_integer(): + # both are ints, no casting necessary + return _int_mod(x, y) + + else: + # bigquery doens't support float mod, so just cast to bignumeric and hope for the best + x_numeric = typing.cast( + ibis_types.DecimalValue, + x.cast(ibis_dtypes.Decimal(precision=76, scale=38, nullable=True)), + ) + y_numeric = typing.cast( + ibis_types.DecimalValue, + y.cast(ibis_dtypes.Decimal(precision=76, scale=38, nullable=True)), + ) + mod_numeric = _bignumeric_mod(x_numeric, y_numeric) + + # Cast back down based on original types + if _is_bignumeric(x) or _is_bignumeric(y): + return mod_numeric + if _is_numeric(x) or _is_numeric(y): + return mod_numeric.cast(ibis_dtypes.Decimal(38, 9)) + else: + return mod_numeric.cast(ibis_dtypes.float64) + + +def _bignumeric_mod( + x: ibis_types.IntegerValue, + y: ibis_types.IntegerValue, +): + # Hacky short-circuit to avoid passing zero-literal to sql backend, evaluate locally instead to null. + op = y.op() + if isinstance(op, ibis.expr.operations.generic.Literal) and op.value == 0: + return ibis_types.null().cast(x.type()) + + bq_mod = x % y # Bigquery will maintain x sign here + + # In BigQuery returned value has the same sign as X. In pandas, the sign of y is used, so we need to flip the result if sign(x) != sign(y) + return ( + ibis.case() + .when( + y == _ZERO, + _NAN * x, + ) # Dummy op to propogate nulls and type from x arg + .when( + (y < _ZERO) & (bq_mod > _ZERO), (y + bq_mod) + ) # Convert positive result to negative + .when( + (y > _ZERO) & (bq_mod < _ZERO), (y + bq_mod) + ) # Convert negative result to positive + .else_(bq_mod) + .end() ) + + +def _int_mod( + x: ibis_types.IntegerValue, + y: ibis_types.IntegerValue, +): # Hacky short-circuit to avoid passing zero-literal to sql backend, evaluate locally instead to null. op = y.op() if isinstance(op, ibis.expr.operations.generic.Literal) and op.value == 0: return ibis_types.null().cast(x.type()) - bq_mod = x_numeric % y_numeric # Bigquery will maintain x sign here - if is_result_float: - bq_mod = typing.cast(ibis_types.NumericValue, bq_mod.cast(ibis_dtypes.float64)) + bq_mod = x % y # Bigquery will maintain x sign here # In BigQuery returned value has the same sign as X. In pandas, the sign of y is used, so we need to flip the result if sign(x) != sign(y) return ( ibis.case() .when( - y_numeric == _ZERO, - _NAN * x_numeric if is_result_float else _ZERO * x_numeric, + y == _ZERO, + _ZERO * x, ) # Dummy op to propogate nulls and type from x arg .when( - (y_numeric < _ZERO) & (bq_mod > _ZERO), (y_numeric + bq_mod) + (y < _ZERO) & (bq_mod > _ZERO), (y + bq_mod) ) # Convert positive result to negative .when( - (y_numeric > _ZERO) & (bq_mod < _ZERO), (y_numeric + bq_mod) + (y > _ZERO) & (bq_mod < _ZERO), (y + bq_mod) ) # Convert negative result to positive .else_(bq_mod) .end() diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 4c2ae461fd4..7379bafa7ec 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -39,6 +39,12 @@ class Aggregation(abc.ABC): op: agg_ops.WindowOp = dataclasses.field() + @abc.abstractmethod + def output_type( + self, input_types: dict[str, dtypes.ExpressionType] + ) -> dtypes.ExpressionType: + ... + @dataclasses.dataclass(frozen=True) class UnaryAggregation(Aggregation): @@ -47,6 +53,11 @@ class UnaryAggregation(Aggregation): UnboundVariableExpression, ScalarConstantExpression ] = dataclasses.field() + def output_type( + self, input_types: dict[str, bigframes.dtypes.Dtype] + ) -> dtypes.ExpressionType: + return self.op.output_type(self.arg.output_type(input_types)) + @dataclasses.dataclass(frozen=True) class BinaryAggregation(Aggregation): @@ -58,6 +69,13 @@ class BinaryAggregation(Aggregation): UnboundVariableExpression, ScalarConstantExpression ] = dataclasses.field() + def output_type( + self, input_types: dict[str, bigframes.dtypes.Dtype] + ) -> dtypes.ExpressionType: + return self.op.output_type( + self.left.output_type(input_types), self.right.output_type(input_types) + ) + @dataclasses.dataclass(frozen=True) class Expression(abc.ABC): @@ -134,7 +152,7 @@ def output_type( if self.id in input_types: return input_types[self.id] else: - raise ValueError("Type of variable has not been fixed.") + raise ValueError(f"Type of variable {self.id} has not been fixed.") def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: if self.id in bindings.keys(): diff --git a/bigframes/core/local_data.py b/bigframes/core/local_data.py new file mode 100644 index 00000000000..8b256be6d2b --- /dev/null +++ b/bigframes/core/local_data.py @@ -0,0 +1,63 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Methods that deal with local pandas/pyarrow dataframes.""" + +from __future__ import annotations + +import pyarrow as pa + +import bigframes.core.schema as schemata +import bigframes.dtypes + + +def arrow_schema_to_bigframes(arrow_schema: pa.Schema) -> schemata.ArraySchema: + """Infer the corresponding bigframes schema given a pyarrow schema.""" + schema_items = tuple( + schemata.SchemaItem( + field.name, + bigframes_type_for_arrow_type(field.type), + ) + for field in arrow_schema + ) + return schemata.ArraySchema(schema_items) + + +def adapt_pa_table(arrow_table: pa.Table) -> pa.Table: + """Adapt a pyarrow table to one that can be handled by bigframes. Converts tz to UTC and unit to us for temporal types.""" + new_schema = pa.schema( + [ + pa.field(field.name, arrow_type_replacements(field.type)) + for field in arrow_table.schema + ] + ) + return arrow_table.cast(new_schema) + + +def bigframes_type_for_arrow_type(pa_type: pa.DataType) -> bigframes.dtypes.Dtype: + return bigframes.dtypes.ibis_dtype_to_bigframes_dtype( + bigframes.dtypes.arrow_dtype_to_ibis_dtype(arrow_type_replacements(pa_type)) + ) + + +def arrow_type_replacements(type: pa.DataType) -> pa.DataType: + if pa.types.is_timestamp(type): + # This is potentially lossy, but BigFrames doesn't support ns + new_tz = "UTC" if (type.tz is not None) else None + return pa.timestamp(unit="us", tz=new_tz) + if pa.types.is_time64(type): + # This is potentially lossy, but BigFrames doesn't support ns + return pa.time64("us") + else: + return type diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 9da535e15f0..540b7cae928 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -14,6 +14,7 @@ from __future__ import annotations +import abc from dataclasses import dataclass, field, fields import functools import itertools @@ -24,8 +25,9 @@ import bigframes.core.expression as ex import bigframes.core.guid -from bigframes.core.join_def import JoinDefinition +from bigframes.core.join_def import JoinColumnMapping, JoinDefinition, JoinSide from bigframes.core.ordering import OrderingColumnReference +import bigframes.core.schema as schemata import bigframes.core.window_spec as window import bigframes.dtypes import bigframes.operations.aggregations as agg_ops @@ -100,6 +102,11 @@ def roots(self) -> typing.Set[BigFrameNode]: ) return set(roots) + @property + @abc.abstractmethod + def schema(self) -> schemata.ArraySchema: + ... + @dataclass(frozen=True) class UnaryNode(BigFrameNode): @@ -109,6 +116,10 @@ class UnaryNode(BigFrameNode): def child_nodes(self) -> typing.Sequence[BigFrameNode]: return (self.child,) + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + return self.child.schema + @dataclass(frozen=True) class JoinNode(BigFrameNode): @@ -138,11 +149,34 @@ def peekable(self) -> bool: single_root = len(self.roots) == 1 return children_peekable and single_root + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + def join_mapping_to_schema_item(mapping: JoinColumnMapping): + result_id = mapping.destination_id + result_dtype = ( + self.left_child.schema.get_type(mapping.source_id) + if mapping.source_table == JoinSide.LEFT + else self.right_child.schema.get_type(mapping.source_id) + ) + return schemata.SchemaItem(result_id, result_dtype) + + items = tuple( + join_mapping_to_schema_item(mapping) for mapping in self.join.mappings + ) + return schemata.ArraySchema(items) + @dataclass(frozen=True) class ConcatNode(BigFrameNode): children: Tuple[BigFrameNode, ...] + def __post_init__(self): + if len(self.children) == 0: + raise ValueError("Concat requires at least one input table. Zero provided.") + child_schemas = [child.schema.dtypes for child in self.children] + if not len(set(child_schemas)) == 1: + raise ValueError("All inputs must have identical dtypes. {child_schemas}") + @property def child_nodes(self) -> typing.Sequence[BigFrameNode]: return self.children @@ -150,11 +184,21 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: def __hash__(self): return self._node_hash + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + # TODO: Output names should probably be aligned beforehand or be part of concat definition + items = tuple( + schemata.SchemaItem(f"column_{i}", dtype) + for i, dtype in enumerate(self.children[0].schema.dtypes) + ) + return schemata.ArraySchema(items) + # Input Nodex @dataclass(frozen=True) class ReadLocalNode(BigFrameNode): feather_bytes: bytes + data_schema: schemata.ArraySchema session: typing.Optional[bigframes.session.Session] = None def __hash__(self): @@ -168,6 +212,10 @@ def peekable(self) -> bool: def roots(self) -> typing.Set[BigFrameNode]: return {self} + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + return self.data_schema + # TODO: Refactor to take raw gbq object reference @dataclass(frozen=True) @@ -193,6 +241,17 @@ def peekable(self) -> bool: def roots(self) -> typing.Set[BigFrameNode]: return {self} + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + items = tuple( + schemata.SchemaItem( + value.get_name(), + bigframes.dtypes.ibis_dtype_to_bigframes_dtype(value.type()), + ) + for value in self.columns + ) + return schemata.ArraySchema(items) + # Unary nodes @dataclass(frozen=True) @@ -210,6 +269,12 @@ def peekable(self) -> bool: def non_local(self) -> bool: return False + @property + def schema(self) -> schemata.ArraySchema: + return self.child.schema.prepend( + schemata.SchemaItem(self.col_id, bigframes.dtypes.INT_DTYPE) + ) + @dataclass(frozen=True) class FilterNode(UnaryNode): @@ -247,6 +312,17 @@ class ProjectionNode(UnaryNode): def __hash__(self): return self._node_hash + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + input_types = self.child.schema._mapping + items = tuple( + schemata.SchemaItem( + id, bigframes.dtypes.dtype_for_etype(ex.output_type(input_types)) + ) + for ex, id in self.assignments + ) + return schemata.ArraySchema(items) + # TODO: Merge RowCount into Aggregate Node? # Row count can be compute from table metadata sometimes, so it is a bit special. @@ -260,6 +336,12 @@ def row_preserving(self) -> bool: def non_local(self) -> bool: return True + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + return schemata.ArraySchema( + (schemata.SchemaItem("count", bigframes.dtypes.INT_DTYPE),) + ) + @dataclass(frozen=True) class AggregateNode(UnaryNode): @@ -282,6 +364,21 @@ def peekable(self) -> bool: def non_local(self) -> bool: return True + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + by_items = tuple( + schemata.SchemaItem(id, self.child.schema.get_type(id)) + for id in self.by_column_ids + ) + input_types = self.child.schema._mapping + agg_items = tuple( + schemata.SchemaItem( + id, bigframes.dtypes.dtype_for_etype(agg.output_type(input_types)) + ) + for agg, id in self.aggregations + ) + return schemata.ArraySchema(tuple([*by_items, *agg_items])) + @dataclass(frozen=True) class WindowOpNode(UnaryNode): @@ -303,6 +400,18 @@ def peekable(self) -> bool: def non_local(self) -> bool: return True + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + input_type = self.child.schema.get_type(self.column_name) + new_item_dtype = self.op.output_type(input_type) + if self.output_name is None: + return self.child.schema.update_dtype(self.column_name, new_item_dtype) + if self.output_name in self.child.schema.names: + return self.child.schema.update_dtype(self.output_name, new_item_dtype) + return self.child.schema.append( + schemata.SchemaItem(self.output_name, new_item_dtype) + ) + @dataclass(frozen=True) class ReprojectOpNode(UnaryNode): @@ -312,6 +421,7 @@ def __hash__(self): @dataclass(frozen=True) class UnpivotNode(UnaryNode): + # TODO: Refactor unpivot row_labels: typing.Tuple[typing.Hashable, ...] unpivot_columns: typing.Tuple[ typing.Tuple[str, typing.Tuple[typing.Optional[str], ...]], ... @@ -338,6 +448,47 @@ def non_local(self) -> bool: def peekable(self) -> bool: return False + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + def infer_dtype( + values: typing.Iterable[typing.Hashable], + ) -> bigframes.dtypes.Dtype: + item_types = map(lambda x: bigframes.dtypes.infer_literal_type(x), values) + etype = functools.reduce( + lambda t1, t2: bigframes.dtypes.lcd_type(t1, t2) + if (t1 and t2) + else None, + item_types, + ) + return bigframes.dtypes.dtype_for_etype(etype) + + label_tuples = [ + label if isinstance(label, tuple) else (label,) for label in self.row_labels + ] + idx_dtypes = [ + infer_dtype(map(lambda x: typing.cast(tuple, x)[i], label_tuples)) + for i in range(len(self.index_col_ids)) + ] + + index_items = [ + schemata.SchemaItem(id, dtype) + for id, dtype in zip(self.index_col_ids, idx_dtypes) + ] + value_dtypes = ( + self.dtype + if isinstance(self.dtype, tuple) + else (self.dtype,) * len(self.unpivot_columns) + ) + value_items = [ + schemata.SchemaItem(col[0], dtype) + for col, dtype in zip(self.unpivot_columns, value_dtypes) + ] + passthrough_items = [ + schemata.SchemaItem(id, self.child.schema.get_type(id)) + for id in self.passthrough_columns + ] + return schemata.ArraySchema((*index_items, *value_items, *passthrough_items)) + @dataclass(frozen=True) class RandomSampleNode(UnaryNode): diff --git a/bigframes/core/schema.py b/bigframes/core/schema.py new file mode 100644 index 00000000000..3629778aaf4 --- /dev/null +++ b/bigframes/core/schema.py @@ -0,0 +1,71 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +import functools +import typing + +import bigframes.core.guid +import bigframes.dtypes + +ColumnIdentifierType = str + + +@dataclass(frozen=True) +class SchemaItem: + column: ColumnIdentifierType + dtype: bigframes.dtypes.Dtype + + +@dataclass(frozen=True) +class ArraySchema: + items: typing.Tuple[SchemaItem, ...] + + @property + def names(self) -> typing.Tuple[str, ...]: + return tuple(item.column for item in self.items) + + @property + def dtypes(self) -> typing.Tuple[bigframes.dtypes.Dtype, ...]: + return tuple(item.dtype for item in self.items) + + @functools.cached_property + def _mapping(self) -> typing.Dict[ColumnIdentifierType, bigframes.dtypes.Dtype]: + return {item.column: item.dtype for item in self.items} + + def drop(self, columns: typing.Iterable[str]) -> ArraySchema: + return ArraySchema( + tuple(item for item in self.items if item.column not in columns) + ) + + def append(self, item: SchemaItem): + return ArraySchema(tuple([*self.items, item])) + + def prepend(self, item: SchemaItem): + return ArraySchema(tuple([item, *self.items])) + + def update_dtype( + self, id: ColumnIdentifierType, dtype: bigframes.dtypes.Dtype + ) -> ArraySchema: + return ArraySchema( + tuple( + SchemaItem(id, dtype) if item.column == id else item + for item in self.items + ) + ) + + def get_type(self, id: ColumnIdentifierType): + return self._mapping[id] diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0f99a3e4db5..ee7d78d984f 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -17,6 +17,7 @@ from __future__ import annotations import datetime +import os import re import sys import textwrap @@ -173,6 +174,11 @@ def __init__( self._block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() self._query_job: Optional[bigquery.QueryJob] = None + # Runs strict validations to ensure internal type predictions and ibis are completely in sync + # Do not execute these validations outside of testing suite. + if "PYTEST_CURRENT_TEST" in os.environ: + self._block.expr.validate_schema() + def __dir__(self): return dir(type(self)) + [ label @@ -1061,6 +1067,7 @@ def to_pandas( downsampled rows and all columns of this DataFrame. """ # TODO(orrbradford): Optimize this in future. Potentially some cases where we can return the stored query job + df, query_job = self._block.to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index d78a88dfebb..954dd270eea 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -47,6 +47,9 @@ # None represents the type of a None scalar. ExpressionType = typing.Optional[Dtype] +# Used when storing Null expressions +DEFAULT_DTYPE = pd.Float64Dtype() + INT_DTYPE = pd.Int64Dtype() FLOAT_DTYPE = pd.Float64Dtype() BOOL_DTYPE = pd.BooleanDtype() @@ -191,6 +194,13 @@ } +def dtype_for_etype(etype: ExpressionType) -> Dtype: + if etype is None: + return DEFAULT_DTYPE + else: + return etype + + def ibis_dtype_to_bigframes_dtype( ibis_dtype: ibis_dtypes.DataType, ) -> Dtype: @@ -222,6 +232,12 @@ def ibis_dtype_to_bigframes_dtype( if ibis_dtype in IBIS_TO_BIGFRAMES: return IBIS_TO_BIGFRAMES[ibis_dtype] + elif isinstance(ibis_dtype, ibis_dtypes.Decimal): + # Temporary workaround for ibis decimal issue (b/323387826) + if ibis_dtype.precision >= 76: + return pd.ArrowDtype(pa.decimal256(76, 38)) + else: + return pd.ArrowDtype(pa.decimal128(38, 9)) elif isinstance(ibis_dtype, ibis_dtypes.Null): # Fallback to STRING for NULL values for most flexibility in SQL. return IBIS_TO_BIGFRAMES[ibis_dtypes.string] @@ -280,6 +296,9 @@ def arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType: if arrow_dtype in ARROW_TO_IBIS: return ARROW_TO_IBIS[arrow_dtype] + if arrow_dtype == pa.null(): + # Used for empty local dataframes where pyarrow has null type + return ibis_dtypes.float64 else: raise ValueError( f"Unexpected Arrow data type {arrow_dtype}. {constants.FEEDBACK_LINK}" @@ -627,6 +646,14 @@ def infer_literal_type(literal) -> typing.Optional[Dtype]: return ibis_dtype_to_bigframes_dtype(ibis_literal.type()) +def infer_literal_arrow_type(literal) -> typing.Optional[pa.DataType]: + if pd.isna(literal): + return None # Null value without a definite type + # Temporary logic, use ibis inferred type + ibis_literal = literal_to_ibis_scalar(literal) + return ibis_dtype_to_arrow_dtype(ibis_literal.type()) + + # Input and output types supported by BigQuery DataFrames remote functions. # TODO(shobs): Extend the support to all types supported by BQ remote functions # https://cloud.google.com/bigquery/docs/remote-functions#limitations diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 09a9d978698..bfb272d9922 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -849,6 +849,7 @@ def wrapper(f): packages, ) + # TODO: Move ibis logic to compiler step node = ibis.udf.scalar.builtin( f, name=rf_name, @@ -859,6 +860,9 @@ def wrapper(f): remote_function_client.get_cloud_function_fully_qualified_name(cf_name) ) node.bigframes_remote_function = str(dataset_ref.routine(rf_name)) # type: ignore + node.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( + ibis_signature.output_type + ) return node return wrapper @@ -913,6 +917,7 @@ def read_gbq_function( def node(*ignored_args, **ignored_kwargs): f"""Remote function {str(routine_ref)}.""" + # TODO: Move ibis logic to compiler step node.__name__ = routine_ref.routine_id node = ibis.udf.scalar.builtin( node, @@ -921,4 +926,7 @@ def node(*ignored_args, **ignored_kwargs): signature=(ibis_signature.input_types, ibis_signature.output_type), ) node.bigframes_remote_function = str(routine_ref) # type: ignore + node.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( # type: ignore + ibis_signature.output_type + ) return node diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index b122f1fe7c0..1268c56162d 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -18,6 +18,8 @@ import typing import numpy as np +import pandas as pd +import pyarrow as pa import bigframes.dtypes as dtypes import bigframes.operations.type as op_typing @@ -198,13 +200,17 @@ def create_ternary_op( ## DateTime Ops day_op = create_unary_op(name="day", type_rule=op_typing.INTEGER) dayofweek_op = create_unary_op(name="dayofweek", type_rule=op_typing.INTEGER) -date_op = create_unary_op(name="date") +date_op = create_unary_op( + name="date", type_rule=op_typing.Fixed(pd.ArrowDtype(pa.date32())) +) hour_op = create_unary_op(name="hour", type_rule=op_typing.INTEGER) minute_op = create_unary_op(name="minute", type_rule=op_typing.INTEGER) month_op = create_unary_op(name="month", type_rule=op_typing.INTEGER) quarter_op = create_unary_op(name="quarter", type_rule=op_typing.INTEGER) second_op = create_unary_op(name="second", type_rule=op_typing.INTEGER) -time_op = create_unary_op(name="time", type_rule=op_typing.INTEGER) +time_op = create_unary_op( + name="time", type_rule=op_typing.Fixed(pd.ArrowDtype(pa.time64("us"))) +) year_op = create_unary_op(name="year", type_rule=op_typing.INTEGER) ## Trigonometry Ops sin_op = create_unary_op(name="sin", type_rule=op_typing.REAL_NUMERIC) @@ -321,7 +327,7 @@ class StrFindOp(UnaryOp): end: typing.Optional[int] def output_type(self, *input_types): - return dtypes.BOOL_DTYPE + return dtypes.INT_DTYPE @dataclasses.dataclass(frozen=True) @@ -359,6 +365,14 @@ class StructFieldOp(UnaryOp): name: typing.ClassVar[str] = "struct_field" name_or_index: str | int + def output_type(self, *input_types): + pd_type = typing.cast(pd.ArrowDtype, input_types[0]) + pa_struct_t = typing.cast(pa.StructType, pd_type.pyarrow_dtype) + pa_result_type = pa_struct_t[self.name_or_index].type + # TODO: Directly convert from arrow to pandas type + ibis_result_type = dtypes.arrow_dtype_to_ibis_dtype(pa_result_type) + return dtypes.ibis_dtype_to_bigframes_dtype(ibis_result_type) + @dataclasses.dataclass(frozen=True) class AsTypeOp(UnaryOp): @@ -367,6 +381,9 @@ class AsTypeOp(UnaryOp): to_type: dtypes.DtypeString | dtypes.Dtype def output_type(self, *input_types): + # TODO: We should do this conversion earlier + if self.to_type == pa.string(): + return dtypes.STRING_DTYPE if isinstance(self.to_type, str): return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[self.to_type] return self.to_type @@ -389,10 +406,8 @@ class RemoteFunctionOp(UnaryOp): apply_on_null: bool def output_type(self, *input_types): - python_type = self.func.__signature__.output_type - ibis_type = dtypes.ibis_type_from_python_type(python_type) - dtype = dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) - return dtype + # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method + return self.func.output_dtype @dataclasses.dataclass(frozen=True) @@ -412,7 +427,8 @@ class ToDatetimeOp(UnaryOp): unit: typing.Optional[str] = None def output_type(self, *input_types): - return input_types[0] + timezone = "UTC" if self.utc else None + return pd.ArrowDtype(pa.timestamp("us", tz=timezone)) @dataclasses.dataclass(frozen=True) @@ -434,14 +450,14 @@ def output_type(self, *input_types): sub_op = create_binary_op(name="sub", type_rule=op_typing.NUMERIC) mul_op = create_binary_op(name="mul", type_rule=op_typing.NUMERIC) div_op = create_binary_op(name="div", type_rule=op_typing.REAL_NUMERIC) -floordiv_op = create_binary_op(name="floordiv", type_rule=op_typing.REAL_NUMERIC) -pow_op = create_binary_op(name="pow", type_rule=op_typing.REAL_NUMERIC) +floordiv_op = create_binary_op(name="floordiv", type_rule=op_typing.NUMERIC) +pow_op = create_binary_op(name="pow", type_rule=op_typing.NUMERIC) mod_op = create_binary_op(name="mod", type_rule=op_typing.NUMERIC) round_op = create_binary_op(name="round", type_rule=op_typing.REAL_NUMERIC) unsafe_pow_op = create_binary_op(name="unsafe_pow_op", type_rule=op_typing.REAL_NUMERIC) # Logical Ops -and_op = create_binary_op(name="and", type_rule=op_typing.PREDICATE) -or_op = create_binary_op(name="or", type_rule=op_typing.PREDICATE) +and_op = create_binary_op(name="and") +or_op = create_binary_op(name="or") ## Comparison Ops eq_op = create_binary_op(name="eq", type_rule=op_typing.PREDICATE) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 6301ece865c..9a270f1ce71 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -19,6 +19,11 @@ import typing from typing import ClassVar, Hashable, Optional, Tuple +import pandas as pd +import pyarrow as pa + +import bigframes.dtypes as dtypes + @dataclasses.dataclass(frozen=True) class WindowOp: @@ -32,6 +37,10 @@ def handles_ties(self): """Whether the operator can handle ties without nondeterministic output. (eg. rank operator can handle ties but not the count operator)""" return False + @abc.abstractmethod + def output_type(self, *input_types: dtypes.ExpressionType): + ... + @dataclasses.dataclass(frozen=True) class UnaryWindowOp(WindowOp): @@ -39,6 +48,9 @@ class UnaryWindowOp(WindowOp): def arguments(self) -> int: return 1 + def output_type(self, *input_types: dtypes.ExpressionType): + return input_types[0] + @dataclasses.dataclass(frozen=True) class AggregateOp(WindowOp): @@ -73,11 +85,24 @@ def arguments(self) -> int: class SumOp(UnaryAggregateOp): name: ClassVar[str] = "sum" + def output_type(self, *input_types: dtypes.ExpressionType): + if pd.api.types.is_bool_dtype(input_types[0]): + return dtypes.INT_DTYPE + else: + return input_types[0] + @dataclasses.dataclass(frozen=True) class MedianOp(UnaryAggregateOp): name: ClassVar[str] = "median" + def output_type(self, *input_types: dtypes.ExpressionType): + # These will change if median is changed to exact implementation. + if pd.api.types.is_bool_dtype(input_types[0]): + return dtypes.INT_DTYPE + else: + return input_types[0] + @dataclasses.dataclass(frozen=True) class ApproxQuartilesOp(UnaryAggregateOp): @@ -87,16 +112,38 @@ class ApproxQuartilesOp(UnaryAggregateOp): def name(self): return f"{self.quartile*25}%" + def output_type(self, *input_types: dtypes.ExpressionType): + if pd.api.types.is_bool_dtype(input_types[0]) or pd.api.types.is_integer_dtype( + input_types[0] + ): + return dtypes.FLOAT_DTYPE + else: + return input_types[0] + @dataclasses.dataclass(frozen=True) class MeanOp(UnaryAggregateOp): name: ClassVar[str] = "mean" + def output_type(self, *input_types: dtypes.ExpressionType): + if pd.api.types.is_bool_dtype(input_types[0]) or pd.api.types.is_integer_dtype( + input_types[0] + ): + return dtypes.FLOAT_DTYPE + else: + return input_types[0] + @dataclasses.dataclass(frozen=True) class ProductOp(UnaryAggregateOp): name: ClassVar[str] = "product" + def output_type(self, *input_types: dtypes.ExpressionType): + if pd.api.types.is_bool_dtype(input_types[0]): + return dtypes.INT_DTYPE + else: + return input_types[0] + @dataclasses.dataclass(frozen=True) class MaxOp(UnaryAggregateOp): @@ -112,16 +159,25 @@ class MinOp(UnaryAggregateOp): class StdOp(UnaryAggregateOp): name: ClassVar[str] = "std" + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.FLOAT_DTYPE + @dataclasses.dataclass(frozen=True) class VarOp(UnaryAggregateOp): name: ClassVar[str] = "var" + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.FLOAT_DTYPE + @dataclasses.dataclass(frozen=True) class PopVarOp(UnaryAggregateOp): name: ClassVar[str] = "popvar" + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.FLOAT_DTYPE + @dataclasses.dataclass(frozen=True) class CountOp(UnaryAggregateOp): @@ -131,6 +187,9 @@ class CountOp(UnaryAggregateOp): def skips_nulls(self): return False + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.INT_DTYPE + @dataclasses.dataclass(frozen=True) class CutOp(UnaryWindowOp): @@ -146,6 +205,24 @@ def skips_nulls(self): def handles_ties(self): return True + def output_type(self, *input_types: dtypes.ExpressionType): + if isinstance(self.bins, int) and (self.labels is False): + return dtypes.INT_DTYPE + else: + # Assumption: buckets use same numeric type + interval_dtype = ( + pa.float64() + if isinstance(self.bins, int) + else dtypes.infer_literal_arrow_type(self.bins[0][0]) + ) + pa_type = pa.struct( + [ + ("left_exclusive", interval_dtype), + ("right_inclusive", interval_dtype), + ] + ) + return pd.ArrowDtype(pa_type) + @dataclasses.dataclass(frozen=True) class QcutOp(UnaryWindowOp): @@ -163,6 +240,9 @@ def skips_nulls(self): def handles_ties(self): return True + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.INT_DTYPE + @dataclasses.dataclass(frozen=True) class NuniqueOp(UnaryAggregateOp): @@ -172,6 +252,9 @@ class NuniqueOp(UnaryAggregateOp): def skips_nulls(self): return False + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.INT_DTYPE + @dataclasses.dataclass(frozen=True) class AnyValueOp(UnaryAggregateOp): @@ -196,6 +279,9 @@ def skips_nulls(self): def handles_ties(self): return True + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.INT_DTYPE + @dataclasses.dataclass(frozen=True) class DenseRankOp(UnaryWindowOp): @@ -207,6 +293,9 @@ def skips_nulls(self): def handles_ties(self): return True + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.INT_DTYPE + @dataclasses.dataclass(frozen=True) class FirstOp(UnaryWindowOp): @@ -254,21 +343,33 @@ def skips_nulls(self): class AllOp(UnaryAggregateOp): name: ClassVar[str] = "all" + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class AnyOp(UnaryAggregateOp): name: ClassVar[str] = "any" + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.BOOL_DTYPE + @dataclasses.dataclass(frozen=True) class CorrOp(BinaryAggregateOp): name: ClassVar[str] = "corr" + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.FLOAT_DTYPE + @dataclasses.dataclass(frozen=True) class CovOp(BinaryAggregateOp): name: ClassVar[str] = "cov" + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.FLOAT_DTYPE + sum_op = SumOp() mean_op = MeanOp() diff --git a/bigframes/operations/type.py b/bigframes/operations/type.py index 3c16f0cbe9c..30e0c1e7458 100644 --- a/bigframes/operations/type.py +++ b/bigframes/operations/type.py @@ -15,8 +15,6 @@ import dataclasses import functools -import pandas as pd - import bigframes.dtypes from bigframes.dtypes import ExpressionType @@ -46,13 +44,10 @@ def output_type(self, *input_types: ExpressionType) -> ExpressionType: @dataclasses.dataclass class RealNumeric(OpTypeRule): def output_type(self, *input_types: ExpressionType) -> ExpressionType: - all_ints = all(pd.api.types.is_integer(input) for input in input_types) - if all_ints: - return bigframes.dtypes.FLOAT_DTYPE - else: - return functools.reduce( - lambda t1, t2: bigframes.dtypes.lcd_etype(t1, t2), input_types - ) + return functools.reduce( + lambda t1, t2: bigframes.dtypes.lcd_etype(t1, t2), + [*input_types, bigframes.dtypes.FLOAT_DTYPE], + ) @dataclasses.dataclass diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 10caf17b790..77f1af9dd43 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -19,6 +19,7 @@ from collections import namedtuple from datetime import datetime import inspect +import resource import sys import typing from typing import ( @@ -706,6 +707,9 @@ def to_datetime( # SQL Compilation uses recursive algorithms on deep trees # 10M tree depth should be sufficient to generate any sql that is under bigquery limit sys.setrecursionlimit(max(10000000, sys.getrecursionlimit())) +resource.setrlimit( + resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) +) # Use __all__ to let type checkers know what is part of the public API. __all___ = [ diff --git a/bigframes/series.py b/bigframes/series.py index 6128238057a..f1eabc18fea 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -19,6 +19,7 @@ import functools import itertools import numbers +import os import textwrap import typing from typing import Any, Mapping, Optional, Tuple, Union @@ -71,6 +72,11 @@ def __init__(self, *args, **kwargs): self._query_job: Optional[bigquery.QueryJob] = None super().__init__(*args, **kwargs) + # Runs strict validations to ensure internal type predictions and ibis are completely in sync + # Do not execute these validations outside of testing suite. + if "PYTEST_CURRENT_TEST" in os.environ: + self._block.expr.validate_schema() + @property def dt(self) -> dt.DatetimeMethods: return dt.DatetimeMethods(self._block) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 4cb3c11859a..3f59e58df2b 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -70,6 +70,7 @@ ReadPickleBuffer, StorageOptions, ) +import pyarrow as pa import bigframes._config.bigquery_options as bigquery_options import bigframes.constants as constants @@ -82,6 +83,7 @@ import bigframes.core.traversal as traversals import bigframes.core.utils as utils import bigframes.dataframe as dataframe +import bigframes.dtypes import bigframes.formatting_helpers as formatting_helpers from bigframes.functions.remote_function import read_gbq_function as bigframes_rgf from bigframes.functions.remote_function import remote_function as bigframes_rf @@ -112,6 +114,20 @@ logger = logging.getLogger(__name__) +# Excludes geography, bytes, and nested (array, struct) datatypes +INLINABLE_DTYPES: Sequence[bigframes.dtypes.Dtype] = ( + pandas.BooleanDtype(), + pandas.Float64Dtype(), + pandas.Int64Dtype(), + pandas.StringDtype(storage="pyarrow"), + pandas.ArrowDtype(pa.date32()), + pandas.ArrowDtype(pa.time64("us")), + pandas.ArrowDtype(pa.timestamp("us")), + pandas.ArrowDtype(pa.timestamp("us", tz="UTC")), + pandas.ArrowDtype(pa.decimal128(38, 9)), + pandas.ArrowDtype(pa.decimal256(76, 38)), +) + def _is_query(query_or_table: str) -> bool: """Determine if `query_or_table` is a table ID or a SQL string""" @@ -923,25 +939,31 @@ def _read_pandas( "bigframes.pandas.DataFrame." ) - if ( - pandas_dataframe.size < MAX_INLINE_DF_SIZE - # TODO(swast): Workaround data types limitation in inline data. - and not any( - ( - isinstance(s.dtype, pandas.ArrowDtype) - or (len(s) > 0 and pandas.api.types.is_list_like(s.iloc[0])) - or pandas.api.types.is_datetime64_any_dtype(s) - ) - for _, s in pandas_dataframe.items() - ) - ): - return self._read_pandas_inline(pandas_dataframe) + inline_df = self._read_pandas_inline(pandas_dataframe) + if inline_df is not None: + return inline_df return self._read_pandas_load_job(pandas_dataframe, api_name) def _read_pandas_inline( self, pandas_dataframe: pandas.DataFrame - ) -> dataframe.DataFrame: - return dataframe.DataFrame(blocks.Block.from_local(pandas_dataframe, self)) + ) -> Optional[dataframe.DataFrame]: + if pandas_dataframe.size > MAX_INLINE_DF_SIZE: + return None + + try: + inline_df = dataframe.DataFrame( + blocks.Block.from_local(pandas_dataframe, self) + ) + except ValueError: # Thrown by ibis for some unhandled types + return None + except pa.ArrowTypeError: # Thrown by arrow for types without mapping (geo). + return None + + inline_types = inline_df._block.expr.schema.dtypes + # Ibis has problems escaping bytes literals, which will cause syntax errors server-side. + if all(dtype in INLINABLE_DTYPES for dtype in inline_types): + return inline_df + return None def _read_pandas_load_job( self, pandas_dataframe: pandas.DataFrame, api_name: str diff --git a/mypy.ini b/mypy.ini index 3809f8e241d..5707f141540 100644 --- a/mypy.ini +++ b/mypy.ini @@ -29,3 +29,6 @@ ignore_missing_imports = True [mypy-ipywidgets] ignore_missing_imports = True + +[mypy-pyarrow.feather] +ignore_missing_imports = True diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ee32fb25ac9..54df35c333d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -43,8 +43,43 @@ def test_df_construct_copy(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result) -def test_df_construct_pandas(scalars_dfs): - columns = ["int64_too", "int64_col", "float64_col", "bool_col", "string_col"] +def test_df_construct_pandas_default(scalars_dfs): + # This should trigger the inlined codepath + columns = [ + "int64_too", + "int64_col", + "float64_col", + "bool_col", + "string_col", + "date_col", + "datetime_col", + "numeric_col", + "float64_col", + "time_col", + "timestamp_col", + ] + _, scalars_pandas_df = scalars_dfs + bf_result = dataframe.DataFrame(scalars_pandas_df, columns=columns).to_pandas() + pd_result = pd.DataFrame(scalars_pandas_df, columns=columns) + pandas.testing.assert_frame_equal(bf_result, pd_result) + + +def test_df_construct_pandas_load_job(scalars_dfs): + # This should trigger the inlined codepath + columns = [ + "int64_too", + "int64_col", + "float64_col", + "bool_col", + "string_col", + "date_col", + "datetime_col", + "numeric_col", + "float64_col", + "time_col", + "timestamp_col", + "geography_col", + ] _, scalars_pandas_df = scalars_dfs bf_result = dataframe.DataFrame(scalars_pandas_df, columns=columns).to_pandas() pd_result = pd.DataFrame(scalars_pandas_df, columns=columns) @@ -1056,10 +1091,13 @@ def test_df_iter( assert bf_i == df_i +@skip_legacy_pandas def test_iterrows( scalars_df_index, scalars_pandas_df_index, ): + scalars_df_index = scalars_df_index.add_suffix("_suffix", axis=1) + scalars_pandas_df_index = scalars_pandas_df_index.add_suffix("_suffix", axis=1) for (bf_index, bf_series), (pd_index, pd_series) in zip( scalars_df_index.iterrows(), scalars_pandas_df_index.iterrows() ): @@ -1936,7 +1974,7 @@ def test_mod(scalars_dfs, other_scalar): def test_scalar_binop_str_exception(scalars_dfs): scalars_df, _ = scalars_dfs columns = ["string_col"] - with pytest.raises(TypeError): + with pytest.raises(Exception): (scalars_df[columns] + 1).to_pandas() @@ -2760,7 +2798,7 @@ def test_loc_setitem_bool_series_scalar_new_col(scalars_dfs): bf_df.loc[bf_df["int64_too"] == 0, "new_col"] = 99 pd_df.loc[pd_df["int64_too"] == 0, "new_col"] = 99 - # pandas type difference + # pandas uses float64 instead pd_df["new_col"] = pd_df["new_col"].astype("Float64") pd.testing.assert_frame_equal( @@ -2785,7 +2823,7 @@ def test_loc_setitem_bool_series_scalar_existing_col(scalars_dfs): ) -def test_loc_setitem_bool_series_scalar_type_error(scalars_dfs): +def test_loc_setitem_bool_series_scalar_error(scalars_dfs): if pd.__version__.startswith("1."): pytest.skip("this loc overload not supported in pandas 1.x.") @@ -2793,9 +2831,9 @@ def test_loc_setitem_bool_series_scalar_type_error(scalars_dfs): bf_df = scalars_df.copy() pd_df = scalars_pandas_df.copy() - with pytest.raises(TypeError): + with pytest.raises(Exception): bf_df.loc[bf_df["int64_too"] == 1, "string_col"] = 99 - with pytest.raises(TypeError): + with pytest.raises(Exception): pd_df.loc[pd_df["int64_too"] == 1, "string_col"] = 99 diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index dcb47d8c60b..0c160a1dc05 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime as dt import math import re import tempfile @@ -43,6 +44,32 @@ def test_series_construct_copy(scalars_dfs): pd.testing.assert_series_equal(bf_result, pd_result) +def test_series_construct_nullable_ints(): + bf_result = series.Series( + [1, 3, bigframes.pandas.NA], index=[0, 4, bigframes.pandas.NA] + ).to_pandas() + + expected_index = pd.Index( + [0, 4, None], + dtype=pd.Int64Dtype(), + ) + expected = pd.Series([1, 3, pd.NA], dtype=pd.Int64Dtype(), index=expected_index) + + pd.testing.assert_series_equal(bf_result, expected) + + +def test_series_construct_timestamps(): + datetimes = [ + dt.datetime(2020, 1, 20, 20, 20, 20, 20), + dt.datetime(2019, 1, 20, 20, 20, 20, 20), + None, + ] + bf_result = series.Series(datetimes).to_pandas() + pd_result = pd.Series(datetimes, dtype=pd.ArrowDtype(pa.timestamp("us"))) + + pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + + def test_series_construct_copy_with_index(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = series.Series( @@ -3099,8 +3126,8 @@ def test_query_job_setters(scalars_dfs): ], ) def test_is_monotonic_increasing(series_input): - scalars_df = series.Series(series_input) - scalars_pandas_df = pd.Series(series_input) + scalars_df = series.Series(series_input, dtype=pd.Int64Dtype()) + scalars_pandas_df = pd.Series(series_input, dtype=pd.Int64Dtype()) assert ( scalars_df.is_monotonic_increasing == scalars_pandas_df.is_monotonic_increasing ) diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py index 0bb5e0101a4..8cde187cb35 100644 --- a/tests/unit/core/test_blocks.py +++ b/tests/unit/core/test_blocks.py @@ -82,7 +82,7 @@ def test_block_from_local(data): # hard-coded the returned dimension of the session for that each of the test case contains 3 rows. mock_session._execute.return_value = (iter([[3]]), None) - block = blocks.Block.from_local(data, mock_session) + block = blocks.Block.from_local(pandas.DataFrame(data), mock_session) pandas.testing.assert_index_equal(block.column_labels, expected.columns) assert tuple(block.index.names) == tuple(expected.index.names) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 9358dca17b0..4f91c1b19aa 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -756,12 +756,12 @@ def isna(self) -> NDFrame: >>> ser = bpd.Series([5, None, 6, np.nan, bpd.NA]) >>> ser - 0 5.0 + 0 5 1 - 2 6.0 + 2 6 3 4 - dtype: Float64 + dtype: Int64 >>> ser.isna() 0 False diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 7120c4d155e..d087df0ef98 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -2035,11 +2035,11 @@ def add(self, other) -> Series: >>> a = bpd.Series([1, 2, 3, bpd.NA]) >>> a - 0 1.0 - 1 2.0 - 2 3.0 + 0 1 + 1 2 + 2 3 3 - dtype: Float64 + dtype: Int64 >>> b = bpd.Series([10, 20, 30, 40]) >>> b @@ -2050,20 +2050,20 @@ def add(self, other) -> Series: dtype: Int64 >>> a.add(b) - 0 11.0 - 1 22.0 - 2 33.0 + 0 11 + 1 22 + 2 33 3 - dtype: Float64 + dtype: Int64 You can also use the mathematical operator ``+``: >>> a + b - 0 11.0 - 1 22.0 - 2 33.0 + 0 11 + 1 22 + 2 33 3 - dtype: Float64 + dtype: Int64 Adding two Series with explicit indexes: @@ -2371,12 +2371,12 @@ def max( >>> s = bpd.Series([1, 3, bpd.NA]) >>> s - 0 1.0 - 1 3.0 + 0 1 + 1 3 2 - dtype: Float64 + dtype: Int64 >>> s.max() - 3.0 + 3 Returns: scalar: Scalar. @@ -2411,12 +2411,12 @@ def min( >>> s = bpd.Series([1, 3, bpd.NA]) >>> s - 0 1.0 - 1 3.0 + 0 1 + 1 3 2 - dtype: Float64 + dtype: Int64 >>> s.min() - 1.0 + 1 Returns: scalar: Scalar. @@ -2498,12 +2498,12 @@ def sum(self): >>> s = bpd.Series([1, 3, bpd.NA]) >>> s - 0 1.0 - 1 3.0 + 0 1 + 1 3 2 - dtype: Float64 + dtype: Int64 >>> s.sum() - 4.0 + 4 Returns: scalar: Scalar. @@ -2532,10 +2532,10 @@ def mean(self): >>> s = bpd.Series([1, 3, bpd.NA]) >>> s - 0 1.0 - 1 3.0 + 0 1 + 1 3 2 - dtype: Float64 + dtype: Int64 >>> s.mean() 2.0 From a5345fe8943667a89fcba48ce31aa8ecfc283f92 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:51:08 -0700 Subject: [PATCH 392/921] fix: sampling plot cannot preserve ordering if index is not ordered (#475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: sampling plot cannot preserve ordering if index is not ordered * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * change sort type --------- Co-authored-by: Owl Bot --- bigframes/core/blocks.py | 19 ++++++++++++---- bigframes/dataframe.py | 5 ++++- bigframes/operations/_matplotlib/core.py | 10 ++++----- bigframes/series.py | 7 ++++-- .../system/small/operations/test_plotting.py | 15 ++++++++++++- tests/system/small/test_dataframe.py | 22 +++++++++++++++++++ .../bigframes_vendored/pandas/core/generic.py | 7 ++++++ 7 files changed, 72 insertions(+), 13 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 074b17762fb..178d698f8d5 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -26,7 +26,7 @@ import itertools import random import typing -from typing import Iterable, List, Mapping, Optional, Sequence, Tuple +from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple import warnings import google.cloud.bigquery as bigquery @@ -555,7 +555,7 @@ def _downsample( block = self._split( fracs=(fraction,), random_state=random_state, - preserve_order=True, + sort=False, )[0] return block else: @@ -571,7 +571,7 @@ def _split( fracs: Iterable[float] = (), *, random_state: Optional[int] = None, - preserve_order: Optional[bool] = False, + sort: Optional[bool | Literal["random"]] = "random", ) -> List[Block]: """Internal function to support splitting Block to multiple parts along index axis. @@ -623,7 +623,18 @@ def _split( typing.cast(Block, block.slice(start=lower, stop=upper)) for lower, upper in intervals ] - if preserve_order: + + if sort is True: + sliced_blocks = [ + sliced_block.order_by( + [ + ordering.OrderingColumnReference(idx_col) + for idx_col in sliced_block.index_columns + ] + ) + for sliced_block in sliced_blocks + ] + elif sort is False: sliced_blocks = [ sliced_block.order_by([ordering.OrderingColumnReference(ordering_col)]) for sliced_block in sliced_blocks diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ee7d78d984f..4e447c547f5 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2504,6 +2504,7 @@ def sample( frac: Optional[float] = None, *, random_state: Optional[int] = None, + sort: Optional[bool | Literal["random"]] = "random", ) -> DataFrame: if n is not None and frac is not None: raise ValueError("Only one of 'n' or 'frac' parameter can be specified.") @@ -2511,7 +2512,9 @@ def sample( ns = (n,) if n is not None else () fracs = (frac,) if frac is not None else () return DataFrame( - self._block._split(ns=ns, fracs=fracs, random_state=random_state)[0] + self._block._split( + ns=ns, fracs=fracs, random_state=random_state, sort=sort + )[0] ) def _split( diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index 5c9d771f61e..7cbeb3df4fb 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -47,11 +47,11 @@ def _compute_plot_data(self, data): # TODO: Cache the sampling data in the PlotAccessor. sampling_n = self.kwargs.pop("sampling_n", 100) sampling_random_state = self.kwargs.pop("sampling_random_state", 0) - return ( - data.sample(n=sampling_n, random_state=sampling_random_state) - .to_pandas() - .sort_index() - ) + return data.sample( + n=sampling_n, + random_state=sampling_random_state, + sort=False, + ).to_pandas() class LinePlot(SamplingPlot): diff --git a/bigframes/series.py b/bigframes/series.py index f1eabc18fea..5f6cfe98930 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -22,7 +22,7 @@ import os import textwrap import typing -from typing import Any, Mapping, Optional, Tuple, Union +from typing import Any, Literal, Mapping, Optional, Tuple, Union import bigframes_vendored.pandas.core.series as vendored_pandas_series import google.cloud.bigquery as bigquery @@ -1535,6 +1535,7 @@ def sample( frac: Optional[float] = None, *, random_state: Optional[int] = None, + sort: Optional[bool | Literal["random"]] = "random", ) -> Series: if n is not None and frac is not None: raise ValueError("Only one of 'n' or 'frac' parameter can be specified.") @@ -1542,7 +1543,9 @@ def sample( ns = (n,) if n is not None else () fracs = (frac,) if frac is not None else () return Series( - self._block._split(ns=ns, fracs=fracs, random_state=random_state)[0] + self._block._split( + ns=ns, fracs=fracs, random_state=random_state, sort=sort + )[0] ) def __array_ufunc__( diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index 876c8f7d04b..47491cdadad 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -13,6 +13,7 @@ # limitations under the License. import numpy as np +import pandas as pd import pandas._testing as tm import pytest @@ -235,6 +236,18 @@ def test_sampling_plot_args_random_state(): tm.assert_almost_equal(ax_0.lines[0].get_data()[1], ax_2.lines[0].get_data()[1]) +def test_sampling_preserve_ordering(): + df = bpd.DataFrame([0.0, 1.0, 2.0, 3.0, 4.0], index=[1, 3, 4, 2, 0]) + pd_df = pd.DataFrame([0.0, 1.0, 2.0, 3.0, 4.0], index=[1, 3, 4, 2, 0]) + ax = df.plot.line() + pd_ax = pd_df.plot.line() + tm.assert_almost_equal(ax.get_xticks(), pd_ax.get_xticks()) + tm.assert_almost_equal(ax.get_yticks(), pd_ax.get_yticks()) + for line, pd_line in zip(ax.lines, pd_ax.lines): + # Compare y coordinates between the lines + tm.assert_almost_equal(line.get_data()[1], pd_line.get_data()[1]) + + @pytest.mark.parametrize( ("kind", "col_names", "kwargs"), [ @@ -251,7 +264,7 @@ def test_sampling_plot_args_random_state(): marks=pytest.mark.xfail(raises=ValueError), ), pytest.param( - "uknown", + "bar", ["int64_col", "int64_too"], {}, marks=pytest.mark.xfail(raises=NotImplementedError), diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 54df35c333d..3b6cd8c05fd 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3049,6 +3049,28 @@ def test_sample_raises_value_error(scalars_dfs): scalars_df.sample(frac=0.5, n=4) +def test_sample_args_sort(scalars_dfs): + scalars_df, _ = scalars_dfs + index = [4, 3, 2, 5, 1, 0] + scalars_df = scalars_df.iloc[index] + + kwargs = {"frac": 1.0, "random_state": 333} + + df = scalars_df.sample(**kwargs).to_pandas() + assert df.index.values != index + assert df.index.values != sorted(index) + + df = scalars_df.sample(sort="random", **kwargs).to_pandas() + assert df.index.values != index + assert df.index.values != sorted(index) + + df = scalars_df.sample(sort=True, **kwargs).to_pandas() + assert df.index.values == sorted(index) + + df = scalars_df.sample(sort=False, **kwargs).to_pandas() + assert df.index.values == index + + @pytest.mark.parametrize( ("axis",), [ diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 4f91c1b19aa..d1cf55c95bb 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -472,6 +472,7 @@ def sample( frac: Optional[float] = None, *, random_state: Optional[int] = None, + sort: Optional[bool | Literal["random"]] = "random", ): """Return a random sample of items from an axis of object. @@ -530,6 +531,12 @@ def sample( Fraction of axis items to return. Cannot be used with `n`. random_state (Optional[int], default None): Seed for random number generator. + sort (Optional[bool|Literal["random"]], default "random"): + + - 'random' (default): No specific ordering will be applied after + sampling. + - 'True' : Index columns will determine the sample's order. + - 'False': The sample will retain the original object's order. Returns: A new object of same type as caller containing `n` items randomly From 0b3f8e5ce63f75ba99ee8cf29226a0fd38bef99f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 21 Mar 2024 01:47:20 +0000 Subject: [PATCH 393/921] feat: option to use bq connection without check (#460) * feat: option to use bq connection without check * revert breaking signature change, centralize connection manager skipping * fix bad referencing * use public property from session * revert unintended test_iam_permissions change * fix couple of more unwanted changes --- bigframes/_config/bigquery_options.py | 28 ++++++- bigframes/clients.py | 40 +++++----- bigframes/functions/remote_function.py | 27 ++++--- bigframes/ml/llm.py | 84 ++++++++++----------- bigframes/ml/remote.py | 28 +++---- bigframes/session/__init__.py | 14 +++- tests/system/small/test_remote_function.py | 34 +++++++++ tests/unit/_config/test_bigquery_options.py | 1 + tests/unit/test_clients.py | 8 +- 9 files changed, 164 insertions(+), 100 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 34701740f6e..d035fe5df1e 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -40,6 +40,7 @@ def __init__( use_regional_endpoints: bool = False, application_name: Optional[str] = None, kms_key_name: Optional[str] = None, + skip_bq_connection_check: bool = False, ): self._credentials = credentials self._project = project @@ -48,6 +49,7 @@ def __init__( self._use_regional_endpoints = use_regional_endpoints self._application_name = application_name self._kms_key_name = kms_key_name + self._skip_bq_connection_check = skip_bq_connection_check self._session_started = False @property @@ -105,14 +107,16 @@ def project(self, value: Optional[str]): @property def bq_connection(self) -> Optional[str]: - """Name of the BigQuery connection to use. Should be of the form ... + """Name of the BigQuery connection to use. Should be of the form + ... You should either have the connection already created in the location you have chosen, or you should have the Project IAM Admin role to enable the service to create the connection for you if you need it. - If this option isn't provided, or project or location aren't provided, session will use its default project/location/connection_id as default connection. + If this option isn't provided, or project or location aren't provided, + session will use its default project/location/connection_id as default connection. """ return self._bq_connection @@ -122,6 +126,26 @@ def bq_connection(self, value: Optional[str]): raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="bq_connection")) self._bq_connection = value + @property + def skip_bq_connection_check(self) -> bool: + """Forcibly use the BigQuery connection. + + Setting this flag to True would avoid creating the BigQuery connection + and checking or setting IAM permissions on it. So if the BigQuery + connection (default or user-provided) does not exist, or it does not have + necessary permissions set up to support BigQuery DataFrames operations, + then a runtime error will be reported. + """ + return self._skip_bq_connection_check + + @skip_bq_connection_check.setter + def skip_bq_connection_check(self, value: bool): + if self._session_started and self._skip_bq_connection_check != value: + raise ValueError( + SESSION_STARTED_MESSAGE.format(attribute="skip_bq_connection_check") + ) + self._skip_bq_connection_check = value + @property def use_regional_endpoints(self) -> bool: """Flag to connect to regional API endpoints. diff --git a/bigframes/clients.py b/bigframes/clients.py index de2421e499f..8a2dbfed6cc 100644 --- a/bigframes/clients.py +++ b/bigframes/clients.py @@ -27,6 +27,23 @@ logger = logging.getLogger(__name__) +def resolve_full_bq_connection_name( + connection_name: str, default_project: str, default_location: str +) -> str: + """Retrieve the full connection name of the form ... + Use default project, location or connection_id when any of them are missing.""" + if connection_name.count(".") == 2: + return connection_name + + if connection_name.count(".") == 1: + return f"{default_project}.{connection_name}" + + if connection_name.count(".") == 0: + return f"{default_project}.{default_location}.{connection_name}" + + raise ValueError(f"Invalid connection name format: {connection_name}.") + + class BqConnectionManager: """Manager to handle operations with BQ connections.""" @@ -41,23 +58,6 @@ def __init__( self._bq_connection_client = bq_connection_client self._cloud_resource_manager_client = cloud_resource_manager_client - @classmethod - def resolve_full_connection_name( - cls, connection_name: str, default_project: str, default_location: str - ) -> str: - """Retrieve the full connection name of the form ... - Use default project, location or connection_id when any of them are missing.""" - if connection_name.count(".") == 2: - return connection_name - - if connection_name.count(".") == 1: - return f"{default_project}.{connection_name}" - - if connection_name.count(".") == 0: - return f"{default_project}.{default_location}.{connection_name}" - - raise ValueError(f"Invalid connection name format: {connection_name}.") - def create_bq_connection( self, project_id: str, location: str, connection_id: str, iam_role: str ): @@ -73,12 +73,6 @@ def create_bq_connection( iam_role: str of the IAM role that the service account of the created connection needs to aquire. E.g. 'run.invoker', 'aiplatform.user' """ - # TODO(shobs): The below command to enable BigQuery Connection API needs - # to be automated. Disabling for now since most target users would not - # have the privilege to enable API in a project. - # log("Making sure BigQuery Connection API is enabled") - # if os.system("gcloud services enable bigqueryconnection.googleapis.com"): - # raise ValueError("Failed to enable BigQuery Connection API") # If the intended connection does not exist then create it service_account_id = self._get_service_account_if_connection_exists( project_id, location, connection_id diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index bfb272d9922..178c911591d 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -126,9 +126,8 @@ def __init__( bq_location, bq_dataset, bq_client, - bq_connection_client, bq_connection_id, - cloud_resource_manager_client, + bq_connection_manager, cloud_function_service_account, cloud_function_kms_key_name, cloud_function_docker_repository, @@ -140,9 +139,7 @@ def __init__( self._bq_dataset = bq_dataset self._bq_client = bq_client self._bq_connection_id = bq_connection_id - self._bq_connection_manager = clients.BqConnectionManager( - bq_connection_client, cloud_resource_manager_client - ) + self._bq_connection_manager = bq_connection_manager self._cloud_function_service_account = cloud_function_service_account self._cloud_function_kms_key_name = cloud_function_kms_key_name self._cloud_function_docker_repository = cloud_function_docker_repository @@ -152,12 +149,13 @@ def create_bq_remote_function( ): """Create a BigQuery remote function given the artifacts of a user defined function and the http endpoint of a corresponding cloud function.""" - self._bq_connection_manager.create_bq_connection( - self._gcp_project_id, - self._bq_location, - self._bq_connection_id, - "run.invoker", - ) + if self._bq_connection_manager: + self._bq_connection_manager.create_bq_connection( + self._gcp_project_id, + self._bq_location, + self._bq_connection_id, + "run.invoker", + ) # Create BQ function # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 @@ -784,7 +782,7 @@ def remote_function( if not bigquery_connection: bigquery_connection = session._bq_connection # type: ignore - bigquery_connection = clients.BqConnectionManager.resolve_full_connection_name( + bigquery_connection = clients.resolve_full_bq_connection_name( bigquery_connection, default_project=dataset_ref.project, default_location=bq_location, @@ -816,6 +814,8 @@ def remote_function( " For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin" ) + bq_connection_manager = None if session is None else session.bqconnectionmanager + def wrapper(f): if not callable(f): raise TypeError("f must be callable, got {}".format(f)) @@ -832,9 +832,8 @@ def wrapper(f): bq_location, dataset_ref.dataset_id, bigquery_client, - bigquery_connection_client, bq_connection_id, - resource_manager_client, + bq_connection_manager, cloud_function_service_account, cloud_function_kms_key_name, cloud_function_docker_repository, diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 10c3cc51b23..6c4ae2ea434 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -73,12 +73,10 @@ def __init__( ): self.model_name = model_name self.session = session or bpd.get_global_session() - self._bq_connection_manager = clients.BqConnectionManager( - self.session.bqconnectionclient, self.session.resourcemanagerclient - ) + self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection - self.connection_name = self._bq_connection_manager.resolve_full_connection_name( + self.connection_name = clients.resolve_full_bq_connection_name( connection_name, default_project=self.session._project, default_location=self.session._location, @@ -93,17 +91,19 @@ def _create_bqml_model(self): raise ValueError( "Must provide connection_name, either in constructor or through session options." ) - connection_name_parts = self.connection_name.split(".") - if len(connection_name_parts) != 3: - raise ValueError( - f"connection_name must be of the format .., got {self.connection_name}." + + if self._bq_connection_manager: + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", ) - self._bq_connection_manager.create_bq_connection( - project_id=connection_name_parts[0], - location=connection_name_parts[1], - connection_id=connection_name_parts[2], - iam_role="aiplatform.user", - ) if self.model_name not in _TEXT_GENERATOR_ENDPOINTS: raise ValueError( @@ -289,12 +289,10 @@ def __init__( self.model_name = model_name self.version = version self.session = session or bpd.get_global_session() - self._bq_connection_manager = clients.BqConnectionManager( - self.session.bqconnectionclient, self.session.resourcemanagerclient - ) + self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection - self.connection_name = self._bq_connection_manager.resolve_full_connection_name( + self.connection_name = clients.resolve_full_bq_connection_name( connection_name, default_project=self.session._project, default_location=self.session._location, @@ -309,17 +307,19 @@ def _create_bqml_model(self): raise ValueError( "Must provide connection_name, either in constructor or through session options." ) - connection_name_parts = self.connection_name.split(".") - if len(connection_name_parts) != 3: - raise ValueError( - f"connection_name must be of the format .., got {self.connection_name}." + + if self._bq_connection_manager: + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", ) - self._bq_connection_manager.create_bq_connection( - project_id=connection_name_parts[0], - location=connection_name_parts[1], - connection_id=connection_name_parts[2], - iam_role="aiplatform.user", - ) if self.model_name not in _EMBEDDING_GENERATOR_ENDPOINTS: raise ValueError( @@ -437,12 +437,10 @@ def __init__( connection_name: Optional[str] = None, ): self.session = session or bpd.get_global_session() - self._bq_connection_manager = clients.BqConnectionManager( - self.session.bqconnectionclient, self.session.resourcemanagerclient - ) + self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection - self.connection_name = self._bq_connection_manager.resolve_full_connection_name( + self.connection_name = clients.resolve_full_bq_connection_name( connection_name, default_project=self.session._project, default_location=self.session._location, @@ -457,17 +455,19 @@ def _create_bqml_model(self): raise ValueError( "Must provide connection_name, either in constructor or through session options." ) - connection_name_parts = self.connection_name.split(".") - if len(connection_name_parts) != 3: - raise ValueError( - f"connection_name must be of the format .., got {self.connection_name}." + + if self._bq_connection_manager: + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", ) - self._bq_connection_manager.create_bq_connection( - project_id=connection_name_parts[0], - location=connection_name_parts[1], - connection_id=connection_name_parts[2], - iam_role="aiplatform.user", - ) options = {"endpoint": _GEMINI_PRO_ENDPOINT} diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index 2b83382e68e..8cf892f5362 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -62,11 +62,9 @@ def __init__( self.output = output self.session = session or bpd.get_global_session() - self._bq_connection_manager = clients.BqConnectionManager( - self.session.bqconnectionclient, self.session.resourcemanagerclient - ) + self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection - self.connection_name = self._bq_connection_manager.resolve_full_connection_name( + self.connection_name = clients.resolve_full_bq_connection_name( connection_name, default_project=self.session._project, default_location=self.session._location, @@ -81,17 +79,19 @@ def _create_bqml_model(self): raise ValueError( "Must provide connection_name, either in constructor or through session options." ) - connection_name_parts = self.connection_name.split(".") - if len(connection_name_parts) != 3: - raise ValueError( - f"connection_name must be of the format .., got {self.connection_name}." + + if self._bq_connection_manager: + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", ) - self._bq_connection_manager.create_bq_connection( - project_id=connection_name_parts[0], - location=connection_name_parts[1], - connection_id=connection_name_parts[2], - iam_role="aiplatform.user", - ) options = { "endpoint": self.endpoint, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 3f59e58df2b..5732d4b08e2 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -73,6 +73,7 @@ import pyarrow as pa import bigframes._config.bigquery_options as bigquery_options +import bigframes.clients import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks @@ -153,7 +154,7 @@ class Session( Configuration adjusting how to connect to BigQuery and related APIs. Note that some options are ignored if ``clients_provider`` is set. - clients_provider (bigframes.session.bigframes.session.clients.ClientsProvider): + clients_provider (bigframes.session.clients.ClientsProvider): An object providing client library objects. """ @@ -212,6 +213,7 @@ def __init__( # Resolve the BQ connection for remote function and Vertex AI integration self._bq_connection = context.bq_connection or _BIGFRAMES_DEFAULT_CONNECTION_ID + self._skip_bq_connection_check = context._skip_bq_connection_check # Now that we're starting the session, don't allow the options to be # changed. @@ -238,6 +240,16 @@ def cloudfunctionsclient(self): def resourcemanagerclient(self): return self._clients_provider.resourcemanagerclient + _bq_connection_manager: Optional[bigframes.clients.BqConnectionManager] = None + + @property + def bqconnectionmanager(self): + if not self._skip_bq_connection_check and not self._bq_connection_manager: + self._bq_connection_manager = bigframes.clients.BqConnectionManager( + self.bqconnectionclient, self.resourcemanagerclient + ) + return self._bq_connection_manager + @property def _project(self): return self.bqclient.project diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 1cf494ea6b0..e7e434dbd05 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import google.api_core.exceptions from google.cloud import bigquery import pandas as pd import pytest @@ -466,6 +467,39 @@ def add_one(x): ) +def test_skip_bq_connection_check(dataset_id_permanent): + connection_name = "connection_does_not_exist" + session = bigframes.Session( + context=bigframes.BigQueryOptions( + bq_connection=connection_name, skip_bq_connection_check=True + ) + ) + + # Make sure that the connection does not exist + with pytest.raises(google.api_core.exceptions.NotFound): + session.bqconnectionclient.get_connection( + name=session.bqconnectionclient.connection_path( + session._project, session._location, connection_name + ) + ) + + # Make sure that an attempt to create a remote function routine with + # non-existent connection would result in an exception thrown by the BQ + # service. + # This is different from the exception throw by the BQ Connection service + # if it was not able to create the connection because of lack of permission + # when skip_bq_connection_check was not set to True: + # google.api_core.exceptions.PermissionDenied: 403 Permission 'resourcemanager.projects.setIamPolicy' denied on resource + with pytest.raises( + google.api_core.exceptions.NotFound, + match=f"Not found: Connection {connection_name}", + ): + + @session.remote_function([int], int, dataset=dataset_id_permanent) + def add_one(x): + return x + 1 + + @pytest.mark.flaky(retries=2, delay=120) def test_read_gbq_function_detects_invalid_function(bigquery_client, dataset_id): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index 1ce70e3da2f..cf130846101 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -30,6 +30,7 @@ ("bq_connection", "path/to/connection/1", "path/to/connection/2"), ("use_regional_endpoints", False, True), ("kms_key_name", "kms/key/name/1", "kms/key/name/2"), + ("skip_bq_connection_check", False, True), ], ) def test_setter_raises_if_session_started(attribute, original_value, new_value): diff --git a/tests/unit/test_clients.py b/tests/unit/test_clients.py index f89cc21397c..37450ececbf 100644 --- a/tests/unit/test_clients.py +++ b/tests/unit/test_clients.py @@ -18,21 +18,21 @@ def test_get_connection_name_full_connection_id(): - connection_name = clients.BqConnectionManager.resolve_full_connection_name( + connection_name = clients.resolve_full_bq_connection_name( "connection-id", default_project="default-project", default_location="us" ) assert connection_name == "default-project.us.connection-id" def test_get_connection_name_full_location_connection_id(): - connection_name = clients.BqConnectionManager.resolve_full_connection_name( + connection_name = clients.resolve_full_bq_connection_name( "eu.connection-id", default_project="default-project", default_location="us" ) assert connection_name == "default-project.eu.connection-id" def test_get_connection_name_full_all(): - connection_name = clients.BqConnectionManager.resolve_full_connection_name( + connection_name = clients.resolve_full_bq_connection_name( "my-project.eu.connection-id", default_project="default-project", default_location="us", @@ -42,7 +42,7 @@ def test_get_connection_name_full_all(): def test_get_connection_name_full_raise_value_error(): with pytest.raises(ValueError): - clients.BqConnectionManager.resolve_full_connection_name( + clients.resolve_full_bq_connection_name( "my-project.eu.connection-id.extra_field", default_project="default-project", default_location="us", From 23a8d9a32e1619aff92c8dfabb7bcdd54c314bd5 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 20 Mar 2024 19:02:18 -0700 Subject: [PATCH 394/921] feat: add ml KMeans model params (#477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/cluster.py | 61 ++++++++++++++++--- tests/system/large/ml/test_cluster.py | 48 ++++++++++++++- .../sklearn/cluster/_kmeans.py | 28 +++++++++ 3 files changed, 128 insertions(+), 9 deletions(-) diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index c294d1f4249..ac5b6f1e3bb 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import Dict, List, Optional, Union +from typing import List, Literal, Optional, Union import bigframes_vendored.sklearn.cluster._kmeans from google.cloud import bigquery @@ -27,6 +27,16 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +_BQML_PARAMS_MAPPING = { + "n_clusters": "numClusters", + "init": "kmeansInitializationMethod", + "init_col": "kmeansInitializationColumn", + "distance_type": "distanceType", + "max_iter": "maxIterations", + "early_stop": "earlyStop", + "tol": "minRelativeProgress", +} + @log_adapter.class_logger class KMeans( @@ -36,8 +46,24 @@ class KMeans( __doc__ = bigframes_vendored.sklearn.cluster._kmeans.KMeans.__doc__ - def __init__(self, n_clusters: int = 8): + def __init__( + self, + n_clusters: int = 8, + *, + init: Literal["kmeans++", "random", "custom"] = "kmeans++", + init_col: Optional[str] = None, + distance_type: Literal["euclidean", "cosine"] = "euclidean", + max_iter: int = 20, + tol: float = 0.01, + warm_start: bool = False, + ): self.n_clusters = n_clusters + self.init = init + self.init_col = init_col + self.distance_type = distance_type + self.max_iter = max_iter + self.tol = tol + self.warm_start = warm_start self._bqml_model: Optional[core.BqmlModel] = None self._bqml_model_factory = globals.bqml_model_factory() @@ -45,21 +71,42 @@ def __init__(self, n_clusters: int = 8): def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> KMeans: assert model.model_type == "KMEANS" - kwargs = {} + kwargs: dict = {} # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun last_fitting = model.training_runs[-1]["trainingOptions"] - if "numClusters" in last_fitting: - kwargs["n_clusters"] = int(last_fitting["numClusters"]) + dummy_kmeans = cls() + for bf_param, bf_value in dummy_kmeans.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param in last_fitting: + # Convert types + kwargs[bf_param] = ( + str(last_fitting[bqml_param]) + if bf_param in ["init"] + else type(bf_value)(last_fitting[bqml_param]) + ) new_kmeans = cls(**kwargs) new_kmeans._bqml_model = core.BqmlModel(session, model) return new_kmeans @property - def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: + def _bqml_options(self) -> dict: """The model options as they will be set for BQML""" - return {"model_type": "KMEANS", "num_clusters": self.n_clusters} + options = { + "model_type": "KMEANS", + "num_clusters": self.n_clusters, + "KMEANS_INIT_METHOD": self.init, + "DISTANCE_TYPE": self.distance_type, + "MAX_ITERATIONS": self.max_iter, + "MIN_REL_PROGRESS": self.tol, + "WARM_START": self.warm_start, + } + + if self.init_col is not None: + options["KMEANS_INIT_COL"] = self.init_col + + return options def _fit( self, diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py index 9244c4b9f19..b633ca4ea2b 100644 --- a/tests/system/large/ml/test_cluster.py +++ b/tests/system/large/ml/test_cluster.py @@ -19,11 +19,11 @@ from tests.system.utils import assert_pandas_df_equal -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_cluster_configure_fit_score_predict( session, penguins_df_default_index, dataset_id ): - model = cluster.KMeans(n_clusters=3) + model = cluster.KMeans(n_clusters=3, init="random") df = penguins_df_default_index.dropna()[ [ @@ -118,3 +118,47 @@ def test_cluster_configure_fit_score_predict( in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_clusters == 3 + assert reloaded_model.init == "RANDOM" + assert reloaded_model.distance_type == "EUCLIDEAN" + assert reloaded_model.max_iter == 20 + assert reloaded_model.tol == 0.01 + + +def test_cluster_configure_fit_load_params(penguins_df_default_index, dataset_id): + model = cluster.KMeans( + n_clusters=4, + init="random", + distance_type="cosine", + max_iter=30, + tol=0.001, + ) + + df = penguins_df_default_index.dropna()[ + [ + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + + # TODO(swast): How should we handle the default index? Currently, we get: + # "Column bigframes_index_0_z is not found in the input data to the + # EVALUATE function." + df = df.reset_index(drop=True) + + model.fit(df) + + # save, load, check n_clusters to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_cluster_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_cluster_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.n_clusters == 4 + assert reloaded_model.init == "RANDOM" + assert reloaded_model.distance_type == "COSINE" + assert reloaded_model.max_iter == 30 + assert reloaded_model.tol == 0.001 diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index d72b9b7bd53..2a0acc8cfe1 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -31,6 +31,34 @@ class KMeans(_BaseKMeans): n_clusters (int, default 8): The number of clusters to form as well as the number of centroids to generate. Default to 8. + + init ("kmeans++", "random" or "custom", default "kmeans++"): + The method of initializing the clusters. Default to "kmeans++" + + kmeas++: Initializes a number of centroids equal to the n_clusters value by using the k-means++ algorithm. Using this approach usually trains a better model than using random cluster initialization. + random: Initializes the centroids by randomly selecting a number of data points equal to the n_clusters value from the input data. + custom: Initializes the centroids using a provided column of type bool. Uses the rows with a value of True as the initial centroids. You specify the column to use by using the init_col option. + + init_col (str or None, default None): + The name of the column to use to initialize the centroids. This column must have a type of bool. If this column contains a value of True for a given row, then uses that row as an initial centroid. The number of True rows in this column must be equal to the value you have specified for the n_clusters option. + Only works with init method "custom". Default to None. + + distance_type ("euclidean" or "cosine", default "euclidean"): + The type of metric to use to compute the distance between two points. + Default to "euclidean". + + max_iter (int, default 20): + The maximum number of training iterations, where one iteration represents a single pass of the entire training data. Default to 20. + + tol (float, default 0.01): + The minimum relative loss improvement that is necessary to continue training. For example, a value of 0.01 specifies that each iteration must reduce the loss by 1% for training to continue. + Default to 0.01. + + warm_start (bool, default False): + Determines whether to train a model with new training data, new model options, or both. Unless you explicitly override them, the initial options used to train the model are used for the warm start run. + Default to False. + + """ def fit( From 2dfb9c24d07841d785e41b33573c5f3a218efeea Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 20 Mar 2024 19:21:14 -0700 Subject: [PATCH 395/921] fix: fix -1 offset lookups failing (#463) * fix: fix -1 offset lookups failing * parameterize series getitem int key test --- bigframes/core/indexers.py | 3 ++- tests/system/small/test_series.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 8d6a1cbdfed..da6f3f3740b 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -402,7 +402,8 @@ def _iloc_getitem_series_or_dataframe( pd.Series, ]: if isinstance(key, int): - internal_slice_result = series_or_dataframe._slice(key, key + 1, 1) + stop_key = key + 1 if key != -1 else None + internal_slice_result = series_or_dataframe._slice(key, stop_key, 1) result_pd_df = internal_slice_result.to_pandas() if result_pd_df.empty: raise IndexError("single positional indexer is out-of-bounds") diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 0c160a1dc05..e35d2f33774 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -240,10 +240,18 @@ def test_series___getitem__(scalars_dfs, index_col, key): pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) -def test_series___getitem___with_int_key(scalars_dfs): +@pytest.mark.parametrize( + ("key",), + ( + (-2,), + (-1,), + (0,), + (1,), + ), +) +def test_series___getitem___with_int_key(scalars_dfs, key): col_name = "int64_too" index_col = "string_col" - key = 2 scalars_df, scalars_pandas_df = scalars_dfs scalars_df = scalars_df.set_index(index_col, drop=False) scalars_pandas_df = scalars_pandas_df.set_index(index_col, drop=False) From 0bf1e916c2b636ec02ac010190e89d38e88fce4b Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 20 Mar 2024 23:20:19 -0700 Subject: [PATCH 396/921] feat: support Series.dt.normalize (#483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/compile/scalar_op_compiler.py | 7 +++++ bigframes/operations/__init__.py | 1 + bigframes/operations/datetimes.py | 3 ++ .../system/small/operations/test_datetimes.py | 16 ++++++++++ .../pandas/core/arrays/datetimelike.py | 31 +++++++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 2a93f08e90b..a52264be17b 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -632,6 +632,13 @@ def year_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.TimestampValue, x).year().cast(ibis_dtypes.int64) +@scalar_op_compiler.register_unary_op(ops.normalize_op) +def normalize_op_impl(x: ibis_types.Value): + result_type = x.type() + result = x.truncate("D") + return result.cast(result_type) + + # Parameterized ops @scalar_op_compiler.register_unary_op(ops.StructFieldOp, pass_op=True) def struct_field_op_impl(x: ibis_types.Value, op: ops.StructFieldOp): diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 1268c56162d..dbea6145e05 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -212,6 +212,7 @@ def create_ternary_op( name="time", type_rule=op_typing.Fixed(pd.ArrowDtype(pa.time64("us"))) ) year_op = create_unary_op(name="year", type_rule=op_typing.INTEGER) +normalize_op = create_unary_op(name="normalize") ## Trigonometry Ops sin_op = create_unary_op(name="sin", type_rule=op_typing.REAL_NUMERIC) cos_op = create_unary_op(name="cos", type_rule=op_typing.REAL_NUMERIC) diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py index eb91bc0b201..1b4a2fe0e67 100644 --- a/bigframes/operations/datetimes.py +++ b/bigframes/operations/datetimes.py @@ -94,3 +94,6 @@ def unit(self) -> str: def strftime(self, date_format: str) -> series.Series: return self._apply_unary_op(ops.StrftimeOp(date_format=date_format)) + + def normalize(self) -> series.Series: + return self._apply_unary_op(ops.normalize_op) diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 854672585de..d5100e7dc23 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -266,3 +266,19 @@ def test_dt_strftime_time(): bf_result, expected_result, check_index_type=False, check_dtype=False ) assert bf_result.dtype == "string[pyarrow]" + + +@pytest.mark.parametrize( + ("col_name",), + DATETIME_COL_NAMES, +) +@skip_legacy_pandas +def test_dt_normalize(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].dt.normalize().to_pandas() + pd_result = scalars_pandas_df[col_name].dt.normalize() + + assert_series_equal( + pd_result.astype(scalars_df[col_name].dtype), # normalize preserves type + bf_result, + ) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py index 4f7e33909e4..60ac19b818e 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py @@ -36,3 +36,34 @@ def strftime(self, date_format: str): bigframes.series.Series of formatted strings. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def normalize(self): + """ + Convert times to midnight. + + The time component of the date-time is converted to midnight i.e. + 00:00:00. This is useful in cases when the time does not matter. + The return dtype will match the source series. + + This method is available on Series with datetime values under the + .dt accessor. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> s = bpd.Series(pd.date_range( + ... start='2014-08-01 10:00', + ... freq='h', + ... periods=3, + ... tz='Asia/Calcutta')) # note timezones will be converted to UTC here + >>> s.dt.normalize() + 0 2014-08-01 00:00:00+00:00 + 1 2014-08-01 00:00:00+00:00 + 2 2014-08-01 00:00:00+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + + Returns: + bigframes.series.Series of the same dtype as the data. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 3e3329a37c1020bd3e6d4d5e980103c63ab0c337 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:54:16 -0700 Subject: [PATCH 397/921] docs: add code samples for metrics.{accuracy_score, confusion_matrix} (#478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../sklearn/metrics/_classification.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_classification.py b/third_party/bigframes_vendored/sklearn/metrics/_classification.py index a9d8038e59b..35c22f4cd06 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_classification.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_classification.py @@ -26,6 +26,24 @@ def accuracy_score(y_true, y_pred, normalize=True) -> float: """Accuracy classification score. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([0, 2, 1, 3]) + >>> y_pred = bpd.DataFrame([0, 1, 2, 3]) + >>> accuracy_score = bigframes.ml.metrics.accuracy_score(y_true, y_pred) + >>> accuracy_score + 0.5 + + If False, return the number of correctly classified samples: + + >>> accuracy_score = bigframes.ml.metrics.accuracy_score(y_true, y_pred, normalize=False) + >>> accuracy_score + 2 + Args: y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) labels. @@ -58,6 +76,30 @@ def confusion_matrix( :math:`C_{0,0}`, false negatives is :math:`C_{1,0}`, true positives is :math:`C_{1,1}` and false positives is :math:`C_{0,1}`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([2, 0, 2, 2, 0, 1]) + >>> y_pred = bpd.DataFrame([0, 0, 2, 2, 0, 2]) + >>> confusion_matrix = bigframes.ml.metrics.confusion_matrix(y_true, y_pred) + >>> confusion_matrix + 0 1 2 + 0 2 0 0 + 1 0 0 1 + 2 1 0 2 + + >>> y_true = bpd.DataFrame(["cat", "ant", "cat", "cat", "ant", "bird"]) + >>> y_pred = bpd.DataFrame(["ant", "ant", "cat", "cat", "ant", "cat"]) + >>> confusion_matrix = bigframes.ml.metrics.confusion_matrix(y_true, y_pred) + >>> confusion_matrix + ant bird cat + ant 2 0 0 + bird 0 0 1 + cat 1 0 2 + Args: y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) target values. From 2dd01c25e9f01c03979c61e71d3c5cd9f0bd4c96 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 21 Mar 2024 13:48:16 -0700 Subject: [PATCH 398/921] feat: support Series.dt.floor (#493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/compile/scalar_op_compiler.py | 20 +++++++++++++ bigframes/operations/__init__.py | 9 ++++++ bigframes/operations/datetimes.py | 3 ++ .../system/small/operations/test_datetimes.py | 21 +++++++++++++ .../pandas/core/arrays/datetimelike.py | 30 +++++++++++++++++++ 5 files changed, 83 insertions(+) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index a52264be17b..4d43545efe2 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -622,6 +622,26 @@ def strftime_op_impl(x: ibis_types.Value, op: ops.StrftimeOp): ) +@scalar_op_compiler.register_unary_op(ops.FloorDtOp, pass_op=True) +def floor_dt_op_impl(x: ibis_types.Value, op: ops.FloorDtOp): + supported_freqs = ["Y", "Q", "M", "W", "D", "h", "min", "s", "ms", "us", "ns"] + pandas_to_ibis_freqs = {"min": "m"} + if op.freq not in supported_freqs: + raise NotImplementedError( + f"Unsupported freq paramater: {op.freq}" + + " Supported freq parameters are: " + + ",".join(supported_freqs) + ) + if op.freq in pandas_to_ibis_freqs: + ibis_freq = pandas_to_ibis_freqs[op.freq] + else: + ibis_freq = op.freq + result_type = x.type() + result = typing.cast(ibis_types.TimestampValue, x) + result = result.truncate(ibis_freq) + return result.cast(result_type) + + @scalar_op_compiler.register_unary_op(ops.time_op) def time_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.TimestampValue, x).time() diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index dbea6145e05..f2bcbd894a8 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -441,6 +441,15 @@ def output_type(self, *input_types): return dtypes.STRING_DTYPE +@dataclasses.dataclass(frozen=True) +class FloorDtOp(UnaryOp): + name: typing.ClassVar[str] = "floor_dt" + freq: str + + def output_type(self, *input_types): + return input_types[0] + + # Binary Ops fillna_op = create_binary_op(name="fillna") cliplower_op = create_binary_op(name="clip_lower") diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py index 1b4a2fe0e67..7d25ac3622a 100644 --- a/bigframes/operations/datetimes.py +++ b/bigframes/operations/datetimes.py @@ -97,3 +97,6 @@ def strftime(self, date_format: str) -> series.Series: def normalize(self) -> series.Series: return self._apply_unary_op(ops.normalize_op) + + def floor(self, freq: str) -> series.Series: + return self._apply_unary_op(ops.FloorDtOp(freq=freq)) diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index d5100e7dc23..b952289a72d 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -282,3 +282,24 @@ def test_dt_normalize(scalars_dfs, col_name): pd_result.astype(scalars_df[col_name].dtype), # normalize preserves type bf_result, ) + + +@pytest.mark.parametrize( + ("col_name", "freq"), + [ + ("timestamp_col", "D"), + ("timestamp_col", "min"), + ("datetime_col", "s"), + ("datetime_col", "us"), + ], +) +@skip_legacy_pandas +def test_dt_floor(scalars_dfs, col_name, freq): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[col_name].dt.floor(freq).to_pandas() + pd_result = scalars_pandas_df[col_name].dt.floor(freq) + + assert_series_equal( + pd_result.astype(scalars_df[col_name].dtype), # floor preserves type + bf_result, + ) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py index 60ac19b818e..f706ae25603 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py @@ -67,3 +67,33 @@ def normalize(self): bigframes.series.Series of the same dtype as the data. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def floor(self, freq: str): + """ + Perform floor operation on the data to the specified freq. + + Supported freq arguments are: 'Y' (year), 'Q' (quarter), 'M' + (month), 'W' (week), 'D' (day), 'h' (hour), 'min' (minute), 's' + (second), 'ms' (microsecond), 'us' (nanosecond), 'ns' (nanosecond) + + Behavior around clock changes (i.e. daylight savings) is determined + by the SQL engine, so "ambiguous" and "nonexistent" parameters are not + supported. Y, Q, M, and W freqs are not supported by pandas as of + version 2.2, but have been added here due to backend support. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> rng = pd.date_range('1/1/2018 11:59:00', periods=3, freq='min') + >>> bpd.Series(rng).dt.floor("h") + 0 2018-01-01 11:00:00 + 1 2018-01-01 12:00:00 + 2 2018-01-01 12:00:00 + dtype: timestamp[us][pyarrow] + + Args: + freq (str): + Frequency string (e.g. "D", "min", "s"). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From ae20c3583d5526777548b5d594ecca6034bb49ec Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:16:38 -0700 Subject: [PATCH 399/921] fix: Properly support format param for numerical input. (#486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Properly support format param for numerical input. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix lint --------- Co-authored-by: Owl Bot --- bigframes/core/compile/scalar_op_compiler.py | 17 ++++++-- bigframes/core/tools/datetimes.py | 8 ++++ tests/system/small/test_pandas.py | 43 ++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 4d43545efe2..d2fc4538354 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -42,6 +42,8 @@ # Datetime constants UNIT_TO_US_CONVERSION_FACTORS = { + "W": 7 * 24 * 60 * 60 * 1000 * 1000, + "d": 24 * 60 * 60 * 1000 * 1000, "D": 24 * 60 * 60 * 1000 * 1000, "h": 60 * 60 * 1000 * 1000, "m": 60 * 1000 * 1000, @@ -753,12 +755,19 @@ def to_datetime_op_impl(x: ibis_types.Value, op: ops.ToDatetimeOp): if x.type() == ibis_dtypes.str: x = x.to_timestamp(op.format) if op.format else timestamp(x) elif x.type() == ibis_dtypes.Timestamp(timezone="UTC"): + if op.format: + raise NotImplementedError( + f"Format parameter is not supported for Timestamp input types. {constants.FEEDBACK_LINK}" + ) return x elif x.type() != ibis_dtypes.timestamp: - # The default unit is set to "ns" (nanoseconds) for consistency - # with pandas, where "ns" is the default unit for datetime operations. - unit = op.unit or "ns" - x = numeric_to_datatime(x, unit) + if op.format: + x = x.cast(ibis_dtypes.str).to_timestamp(op.format) + else: + # The default unit is set to "ns" (nanoseconds) for consistency + # with pandas, where "ns" is the default unit for datetime operations. + unit = op.unit or "ns" + x = numeric_to_datatime(x, unit) return x.cast(ibis_dtypes.Timestamp(timezone="UTC" if op.utc else None)) diff --git a/bigframes/core/tools/datetimes.py b/bigframes/core/tools/datetimes.py index 4aaf320c7ad..96bf556101d 100644 --- a/bigframes/core/tools/datetimes.py +++ b/bigframes/core/tools/datetimes.py @@ -73,6 +73,14 @@ def to_datetime( f"String and Timestamp requires utc=True. {constants.FEEDBACK_LINK}" ) + if format and unit and arg.dtype in ("Int64", "Float64"): # type: ignore + raise ValueError("cannot specify both format and unit") + + if unit and arg.dtype not in ("Int64", "Float64"): # type: ignore + raise NotImplementedError( + f"Unit parameter is not supported for non-numerical input types. {constants.FEEDBACK_LINK}" + ) + return arg._apply_unary_op( # type: ignore ops.ToDatetimeOp( utc=utc, diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index ec61329aa57..a080a969c87 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -539,3 +539,46 @@ def test_to_datetime_series(scalars_dfs): pd.testing.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) + + +@pytest.mark.parametrize( + ("arg", "unit"), + [ + ([1, 2, 3], "W"), + ([1, 2, 3], "d"), + ([1, 2, 3], "D"), + ([1, 2, 3], "h"), + ([1, 2, 3], "m"), + ([20242330, 25244685, 34324234], "s"), + ([20242330000, 25244685000, 34324234000], "ms"), + ([20242330000000, 25244685000000, 34324234000000], "us"), + ([20242330000000000, 25244685000000000, 34324234000000000], "ns"), + ], +) +def test_to_datetime_unit_param(arg, unit): + bf_result = bpd.to_datetime(arg, unit=unit).to_pandas().astype("datetime64[ns]") + pd_result = pd.Series(pd.to_datetime(arg, unit=unit)).dt.floor("us") + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_names=False + ) + + +@pytest.mark.parametrize( + ("arg", "utc", "format"), + [ + ([20230110, 20230101, 20230101], False, "%Y%m%d"), + ([201301.01], False, "%Y%m.%d"), + (["2023-01-10", "2023-01-20", "2023-01-01"], True, "%Y-%m-%d"), + (["2014-08-15 07:19"], True, "%Y-%m-%d %H:%M"), + ], +) +def test_to_datetime_format_param(arg, utc, format): + bf_result = ( + bpd.to_datetime(arg, utc=utc, format=format) + .to_pandas() + .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") + ) + pd_result = pd.Series(pd.to_datetime(arg, utc=utc, format=format)).dt.floor("us") + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_names=False + ) From 429a4a5d0f30eecdffec37de9a879c634991ac5c Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:21:49 -0700 Subject: [PATCH 400/921] chore: update docstring for astype (#485) * chore: update docstring for astype * minor fix * minor fixes to pass doc test --- .../bigframes_vendored/pandas/core/generic.py | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index d1cf55c95bb..1477640c299 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -123,28 +123,55 @@ def astype(self, dtype): Create a series of type ``Int64``: - >>> ser = bpd.Series([1, 2], dtype='Int64') + >>> ser = bpd.Series([2023010000246789, 1624123244123101, 1054834234120101], dtype='Int64') >>> ser - 0 1 - 1 2 + 0 2023010000246789 + 1 1624123244123101 + 2 1054834234120101 dtype: Int64 Convert to ``Float64`` type: >>> ser.astype('Float64') - 0 1.0 - 1 2.0 + 0 2023010000246789.0 + 1 1624123244123101.0 + 2 1054834234120101.0 dtype: Float64 + Convert to ``pd.ArrowDtype(pa.timestamp("us", tz="UTC"))`` type: + + >>> ser.astype("timestamp[us, tz=UTC][pyarrow]") + 0 2034-02-08 11:13:20.246789+00:00 + 1 2021-06-19 17:20:44.123101+00:00 + 2 2003-06-05 17:30:34.120101+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + + Note that this is equivalent of using ``to_datetime`` with ``unit='us'``: + + >>> bpd.to_datetime(ser, unit='us', utc=True) + 0 2034-02-08 11:13:20.246789+00:00 + 1 2021-06-19 17:20:44.123101+00:00 + 2 2003-06-05 17:30:34.120101+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + + Convert ``pd.ArrowDtype(pa.timestamp("us", tz="UTC"))`` type to ``Int64`` type: + + >>> timestamp_ser = ser.astype("timestamp[us, tz=UTC][pyarrow]") + >>> timestamp_ser.astype('Int64') + 0 2023010000246789 + 1 1624123244123101 + 2 1054834234120101 + dtype: Int64 + Args: dtype (str or pandas.ExtensionDtype): - A dtype supported by BigQuery DataFrame include 'boolean','Float64','Int64', - 'string', 'string[pyarrow]','timestamp[us, tz=UTC][pyarrow]', - 'timestamp[us][pyarrow]','date32[day][pyarrow]','time64[us][pyarrow]' - A pandas.ExtensionDtype include pandas.BooleanDtype(), pandas.Float64Dtype(), - pandas.Int64Dtype(), pandas.StringDtype(storage="pyarrow"), - pd.ArrowDtype(pa.date32()), pd.ArrowDtype(pa.time64("us")), - pd.ArrowDtype(pa.timestamp("us")), pd.ArrowDtype(pa.timestamp("us", tz="UTC")). + A dtype supported by BigQuery DataFrame include ``'boolean'``, ``'Float64'``, ``'Int64'``, + ``'int64[pyarrow]'``, ``'string'``, ``'string[pyarrow]'``, ``'timestamp[us, tz=UTC][pyarrow]'``, + ``'timestamp\[us\]\[pyarrow\]'``, ``'date32\[day\]\[pyarrow\]'``, ``'time64\[us\]\[pyarrow\]'``. + A pandas.ExtensionDtype include ``pandas.BooleanDtype()``, ``pandas.Float64Dtype()``, + ``pandas.Int64Dtype()``, ``pandas.StringDtype(storage="pyarrow")``, + ``pd.ArrowDtype(pa.date32())``, ``pd.ArrowDtype(pa.time64("us"))``, + ``pd.ArrowDtype(pa.timestamp("us"))``, ``pd.ArrowDtype(pa.timestamp("us", tz="UTC"))``. Returns: same type as caller From ae5fc8fe8ac375e07df4f9644cef3d135dd617e7 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 21 Mar 2024 15:48:16 -0700 Subject: [PATCH 401/921] =?UTF-8?q?refactor:=20convert=20ordering=20defini?= =?UTF-8?q?tions=20to=20use=20flexible=20scalar=20express=E2=80=A6=20(#490?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ions Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/__init__.py | 4 +- bigframes/core/block_transforms.py | 22 +-- bigframes/core/blocks.py | 27 +++- bigframes/core/compile/compiled.py | 179 ++++++++++++++++-------- bigframes/core/compile/concat.py | 4 +- bigframes/core/compile/single_column.py | 6 +- bigframes/core/expression.py | 24 +++- bigframes/core/groupby/__init__.py | 8 +- bigframes/core/indexes/index.py | 23 +-- bigframes/core/nodes.py | 13 +- bigframes/core/ordering.py | 93 ++++++------ bigframes/core/reshape/__init__.py | 2 +- bigframes/core/rewrite.py | 62 +++----- bigframes/core/window_spec.py | 2 +- bigframes/dataframe.py | 19 +-- bigframes/operations/__init__.py | 20 +++ bigframes/series.py | 38 +++-- bigframes/session/__init__.py | 28 ++-- tests/unit/resources.py | 2 +- tests/unit/test_core.py | 6 +- 20 files changed, 324 insertions(+), 258 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index c37cd14b77b..6fd6fc23c2f 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -30,7 +30,7 @@ import bigframes.core.join_def as join_def import bigframes.core.local_data as local_data import bigframes.core.nodes as nodes -from bigframes.core.ordering import OrderingColumnReference +from bigframes.core.ordering import OrderingExpression import bigframes.core.ordering as orderings import bigframes.core.rewrite import bigframes.core.schema as schemata @@ -162,7 +162,7 @@ def filter_by_id(self, predicate_id: str, keep_null: bool = False) -> ArrayValue def filter(self, predicate: ex.Expression): return ArrayValue(nodes.FilterNode(child=self.node, predicate=predicate)) - def order_by(self, by: Sequence[OrderingColumnReference]) -> ArrayValue: + def order_by(self, by: Sequence[OrderingExpression]) -> ArrayValue: return ArrayValue(nodes.OrderByNode(child=self.node, by=tuple(by))) def reversed(self) -> ArrayValue: diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 6b9a367f55d..c789b2a69cf 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -176,7 +176,7 @@ def _interpolate_column( ) -> typing.Tuple[blocks.Block, str]: if interpolate_method not in ["linear", "nearest", "ffill"]: raise ValueError("interpolate method not supported") - window_ordering = (ordering.OrderingColumnReference(x_values),) + window_ordering = (ordering.OrderingExpression(ex.free_var(x_values)),) backwards_window = windows.WindowSpec(following=0, ordering=window_ordering) forwards_window = windows.WindowSpec(preceding=0, ordering=window_ordering) @@ -338,8 +338,8 @@ def value_counts( if sort: block = block.order_by( [ - ordering.OrderingColumnReference( - count_id, + ordering.OrderingExpression( + ex.free_var(count_id), direction=ordering.OrderingDirection.ASC if ascending else ordering.OrderingDirection.DESC, @@ -398,8 +398,8 @@ def rank( window = windows.WindowSpec( # BigQuery has syntax to reorder nulls with "NULLS FIRST/LAST", but that is unavailable through ibis presently, so must order on a separate nullity expression first. ordering=( - ordering.OrderingColumnReference( - col, + ordering.OrderingExpression( + ex.free_var(col), ordering.OrderingDirection.ASC if ascending else ordering.OrderingDirection.DESC, @@ -481,8 +481,8 @@ def nsmallest( if keep == "last": block = block.reversed() order_refs = [ - ordering.OrderingColumnReference( - col_id, direction=ordering.OrderingDirection.ASC + ordering.OrderingExpression( + ex.free_var(col_id), direction=ordering.OrderingDirection.ASC ) for col_id in column_ids ] @@ -511,8 +511,8 @@ def nlargest( if keep == "last": block = block.reversed() order_refs = [ - ordering.OrderingColumnReference( - col_id, direction=ordering.OrderingDirection.DESC + ordering.OrderingExpression( + ex.free_var(col_id), direction=ordering.OrderingDirection.DESC ) for col_id in column_ids ] @@ -804,9 +804,9 @@ def _idx_extrema( ) # Have to find the min for each order_refs = [ - ordering.OrderingColumnReference(value_col, direction), + ordering.OrderingExpression(ex.free_var(value_col), direction), *[ - ordering.OrderingColumnReference(idx_col) + ordering.OrderingExpression(ex.free_var(idx_col)) for idx_col in original_block.index_columns ], ] diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 178d698f8d5..64a0df9d863 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -270,7 +270,7 @@ def cols_matching_label(self, partial_label: Label) -> typing.Sequence[str]: def order_by( self, - by: typing.Sequence[ordering.OrderingColumnReference], + by: typing.Sequence[ordering.OrderingExpression], ) -> Block: return Block( self._expr.order_by(by), @@ -610,7 +610,9 @@ def _split( string_ordering_col, random_state_col, ops.strconcat_op ) block, hash_string_sum_col = block.apply_unary_op(string_sum_col, ops.hash_op) - block = block.order_by([ordering.OrderingColumnReference(hash_string_sum_col)]) + block = block.order_by( + [ordering.OrderingExpression(ex.free_var(hash_string_sum_col))] + ) intervals = [] cur = 0 @@ -628,7 +630,7 @@ def _split( sliced_blocks = [ sliced_block.order_by( [ - ordering.OrderingColumnReference(idx_col) + ordering.OrderingExpression(ex.free_var(idx_col)) for idx_col in sliced_block.index_columns ] ) @@ -636,7 +638,9 @@ def _split( ] elif sort is False: sliced_blocks = [ - sliced_block.order_by([ordering.OrderingColumnReference(ordering_col)]) + sliced_block.order_by( + [ordering.OrderingExpression(ex.free_var(ordering_col))] + ) for sliced_block in sliced_blocks ] @@ -1706,7 +1710,10 @@ def merge( if sort: # sort uses coalesced join keys always joined_expr = joined_expr.order_by( - [ordering.OrderingColumnReference(col_id) for col_id in coalesced_ids], + [ + ordering.OrderingExpression(ex.free_var(col_id)) + for col_id in coalesced_ids + ], ) joined_expr = joined_expr.select_columns(result_columns) @@ -2028,7 +2035,10 @@ def join_mono_indexed( ) if sort: combined_expr = combined_expr.order_by( - [ordering.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] + [ + ordering.OrderingExpression(ex.free_var(col_id)) + for col_id in coalesced_join_cols + ] ) block = Block( combined_expr, @@ -2117,7 +2127,10 @@ def join_multi_indexed( ) if sort: combined_expr = combined_expr.order_by( - [ordering.OrderingColumnReference(col_id) for col_id in coalesced_join_cols] + [ + ordering.OrderingExpression(ex.free_var(col_id)) + for col_id in coalesced_join_cols + ] ) if left.index.nlevels == 1: diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 34e7bf1ce2b..af2d69275aa 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -15,6 +15,7 @@ import abc import functools +import itertools import textwrap import typing from typing import Collection, Iterable, Literal, Optional, Sequence @@ -31,10 +32,11 @@ import bigframes.core.expression as ex import bigframes.core.guid from bigframes.core.ordering import ( + ascending_over, encode_order_string, ExpressionOrdering, IntegerEncoding, - OrderingColumnReference, + OrderingExpression, ) import bigframes.core.schema as schemata import bigframes.core.utils as utils @@ -97,6 +99,10 @@ def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: else None ) + @property + def _ibis_bindings(self) -> dict[str, ibis_types.Value]: + return {col: self._get_ibis_column(col) for col in self.column_ids} + @abc.abstractmethod def filter(self: T, predicate: ex.Expression) -> T: """Filter the table on a given expression, the predicate must be a boolean expression.""" @@ -243,7 +249,7 @@ def row_count(self) -> OrderedIR: ibis_table, (ibis_table["count"],), ordering=ExpressionOrdering( - ordering_value_columns=(OrderingColumnReference("count"),), + ordering_value_columns=(ascending_over("count"),), total_ordering_columns=frozenset(["count"]), ), ) @@ -457,12 +463,7 @@ def aggregate( result = table.group_by(by_column_ids).aggregate(**stats) # Must have deterministic ordering, so order by the unique "by" column ordering = ExpressionOrdering( - tuple( - [ - OrderingColumnReference(column_id=column_id) - for column_id in by_column_ids - ] - ), + tuple([ascending_over(column_id) for column_id in by_column_ids]), total_ordering_columns=frozenset(by_column_ids), ) columns = tuple(result[key] for key in result.columns) @@ -476,12 +477,10 @@ def aggregate( aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} result = table.aggregate(**aggregates) # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. + # TODO: Maybe can make completely empty ordering = ExpressionOrdering( - ordering_value_columns=tuple( - [OrderingColumnReference(ORDER_ID_COLUMN)] - ), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(is_encoded=True, is_sequential=True), + ordering_value_columns=tuple([]), + total_ordering_columns=frozenset([]), ) return OrderedIR( result, @@ -583,9 +582,6 @@ def __init__( predicates: Optional[Collection[ibis_types.BooleanValue]] = None, ): super().__init__(table, columns, predicates) - # TODO: Validate ordering - if not ordering.total_ordering_columns: - raise ValueError("Must have total ordering defined by one or more columns") self._ordering = ordering # Meta columns store ordering, or other data that doesn't correspond to dataframe columns self._hidden_ordering_columns = ( @@ -615,7 +611,8 @@ def __init__( all_columns = value_col_ids | hidden_col_ids ordering_valid = all( - col.column_id in all_columns for col in ordering.all_ordering_columns + set(col.scalar_expression.unbound_variables).issubset(all_columns) + for col in ordering.all_ordering_columns ) if value_col_ids & hidden_col_ids: raise ValueError( @@ -653,14 +650,17 @@ def from_pandas( keys_memtable, columns=[keys_memtable[column].name(column) for column in pd_df.columns], ordering=ExpressionOrdering( - ordering_value_columns=tuple( - [OrderingColumnReference(ORDER_ID_COLUMN)] - ), + ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), total_ordering_columns=frozenset([ORDER_ID_COLUMN]), ), hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), ) + @property + def _ibis_bindings(self) -> dict[str, ibis_types.Value]: + all_keys = itertools.chain(self.column_ids, self._hidden_column_ids) + return {col: self._get_any_column(col) for col in all_keys} + @property def _hidden_column_ids(self) -> typing.Sequence[str]: return tuple(self._hidden_ordering_column_names.keys()) @@ -689,7 +689,7 @@ def builder(self) -> OrderedIR.Builder: predicates=self._predicates, ) - def order_by(self, by: Sequence[OrderingColumnReference]) -> OrderedIR: + def order_by(self, by: Sequence[OrderingExpression]) -> OrderedIR: expr_builder = self.builder() expr_builder.ordering = self._ordering.with_ordering_columns(by) return expr_builder.build() @@ -730,7 +730,9 @@ def promote_offsets(self, col_id: str) -> OrderedIR: return self._project_offsets().promote_offsets(col_id) expr_builder = self.builder() expr_builder.columns = [ - self._get_any_column(ordering.total_order_col.column_id).name(col_id), + self._compile_expression(ordering.total_order_col.scalar_expression).name( + col_id + ), *self.columns, ] return expr_builder.build() @@ -905,7 +907,7 @@ def unpivot( ordering_value_columns=tuple( [ *old_ordering.ordering_value_columns, - OrderingColumnReference(unpivot_offset_id), + ascending_over(unpivot_offset_id), ] ), total_ordering_columns=frozenset( @@ -916,7 +918,7 @@ def unpivot( new_ordering = ExpressionOrdering( ordering_value_columns=tuple( [ - OrderingColumnReference(unpivot_offset_id), + ascending_over(unpivot_offset_id), *old_ordering.ordering_value_columns, ] ), @@ -949,9 +951,12 @@ def _reproject_to_table(self) -> OrderedIR: expose_hidden_cols=True, ) columns = [table[column_name] for column_name in self._column_names] - ordering_col_ids = [ - ref.column_id for ref in self._ordering.all_ordering_columns - ] + ordering_col_ids = list( + itertools.chain.from_iterable( + ref.scalar_expression.unbound_variables + for ref in self._ordering.all_ordering_columns + ) + ) hidden_ordering_columns = [ table[column_name] for column_name in self._hidden_ordering_column_names @@ -969,20 +974,24 @@ def to_sql( col_id_overrides: typing.Mapping[str, str] = {}, sorted: bool = False, ) -> str: - sql = ibis_bigquery.Backend().compile( - self._to_ibis_expr( - ordering_mode="unordered", - col_id_overrides=col_id_overrides, - expose_hidden_cols=sorted, - ) - ) if sorted: + # Need to bake ordering expressions into the selected column in order for our ordering clause builder to work. + baked_ir = self._bake_ordering() + sql = ibis_bigquery.Backend().compile( + baked_ir._to_ibis_expr( + ordering_mode="unordered", + col_id_overrides=col_id_overrides, + expose_hidden_cols=True, + ) + ) output_columns = [ col_id_overrides.get(col) if (col in col_id_overrides) else col - for col in self.column_ids + for col in baked_ir.column_ids ] selection = ", ".join(map(lambda col_id: f"`{col_id}`", output_columns)) - order_by_clause = self._ordering_clause(self._ordering.all_ordering_columns) + order_by_clause = baked_ir._ordering_clause( + baked_ir._ordering.all_ordering_columns + ) sql = textwrap.dedent( f"SELECT {selection}\n" @@ -991,14 +1000,29 @@ def to_sql( ")\n" f"{order_by_clause}\n" ) + else: + sql = ibis_bigquery.Backend().compile( + self._to_ibis_expr( + ordering_mode="unordered", + col_id_overrides=col_id_overrides, + expose_hidden_cols=False, + ) + ) return typing.cast(str, sql) - def _ordering_clause(self, ordering: Iterable[OrderingColumnReference]) -> str: + def _ordering_clause(self, ordering: Iterable[OrderingExpression]) -> str: parts = [] for col_ref in ordering: asc_desc = "ASC" if col_ref.direction.is_ascending else "DESC" null_clause = "NULLS LAST" if col_ref.na_last else "NULLS FIRST" - part = f"`{col_ref.column_id}` {asc_desc} {null_clause}" + ordering_expr = col_ref.scalar_expression + # We don't know how to compile scalar expressions in isolation + if ordering_expr.is_const: + # Probably shouldn't have constants in ordering definition, but best to ignore if somehow they end up here. + continue + if not isinstance(ordering_expr, ex.UnboundVariableExpression): + raise ValueError("Expected direct column reference.") + part = f"`{ordering_expr.id}` {asc_desc} {null_clause}" parts.append(part) return f"ORDER BY {' ,'.join(parts)}" @@ -1110,9 +1134,12 @@ def _filter(self, predicate_value: ibis_types.BooleanValue) -> OrderedIR: def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> OrderedIR: """Safely assign by id while maintaining ordering integrity.""" # TODO: Split into explicit set and replace methods - ordering_col_ids = [ - col_ref.column_id for col_ref in self._ordering.ordering_value_columns - ] + ordering_col_ids = set( + itertools.chain.from_iterable( + col_ref.scalar_expression.unbound_variables + for col_ref in self._ordering.ordering_value_columns + ) + ) if id in ordering_col_ids: return self._hide_column(id)._set_or_replace_by_id(id, new_value) @@ -1129,9 +1156,14 @@ def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> Ordered def _select(self, values: typing.Tuple[ibis_types.Value]) -> OrderedIR: """Safely assign by id while maintaining ordering integrity.""" # TODO: Split into explicit set and replace methods - ordering_col_ids = [ - col_ref.column_id for col_ref in self._ordering.ordering_value_columns - ] + ordering_col_ids = set( + itertools.chain.from_iterable( + [ + col_ref.scalar_expression.unbound_variables + for col_ref in self._ordering.ordering_value_columns + ] + ) + ) ir = self mappings = {value.name: value for value in values} for ordering_id in ordering_col_ids: @@ -1184,17 +1216,43 @@ def _hide_column(self, column_id) -> OrderedIR: expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) return expr_builder.build() + def _bake_ordering(self) -> OrderedIR: + """Bakes ordering expression into the selection, maybe creating hidden columns.""" + ordering_expressions = self._ordering.all_ordering_columns + new_exprs = [] + new_baked_cols = [] + for expr in ordering_expressions: + if isinstance(expr.scalar_expression, ex.OpExpression): + baked_column = self._compile_expression(expr.scalar_expression).name( + bigframes.core.guid.generate_guid() + ) + new_baked_cols.append(baked_column) + new_expr = OrderingExpression( + ex.free_var(baked_column.name), expr.direction, expr.na_last + ) + new_exprs.append(new_expr) + else: + new_exprs.append(expr) + + ordering = self._ordering.with_ordering_columns(new_exprs) + return OrderedIR( + self._table, + columns=self.columns, + hidden_ordering_columns=[*self._hidden_ordering_columns, *new_baked_cols], + ordering=ordering, + predicates=self._predicates, + ) + def _project_offsets(self) -> OrderedIR: """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" if self._ordering.is_sequential: return self - # TODO(tbergeron): Enforce total ordering table = self._to_ibis_expr( ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN ) columns = [table[column_name] for column_name in self._column_names] ordering = ExpressionOrdering( - ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), total_ordering_columns=frozenset([ORDER_ID_COLUMN]), integer_encoding=IntegerEncoding(True, is_sequential=True), ) @@ -1222,7 +1280,9 @@ def _create_order_columns( def _create_offset_column(self) -> ibis_types.IntegerColumn: if self._ordering.total_order_col and self._ordering.is_sequential: - offsets = self._get_any_column(self._ordering.total_order_col.column_id) + offsets = self._compile_expression( + self._ordering.total_order_col.scalar_expression + ) return typing.cast(ibis_types.IntegerColumn, offsets) else: window = ibis.window(order_by=self._ibis_order) @@ -1233,8 +1293,8 @@ def _create_offset_column(self) -> ibis_types.IntegerColumn: def _create_string_ordering_column(self) -> ibis_types.StringColumn: if self._ordering.total_order_col and self._ordering.is_string_encoded: - string_order_ids = self._get_any_column( - self._ordering.total_order_col.column_id + string_order_ids = op_compiler.compile_expression( + self._ordering.total_order_col.scalar_expression, self._ibis_bindings ) return typing.cast(ibis_types.StringColumn, string_order_ids) if ( @@ -1242,7 +1302,9 @@ def _create_string_ordering_column(self) -> ibis_types.StringColumn: and self._ordering.integer_encoding.is_encoded ): # Special case: non-negative integer ordering id can be converted directly to string without regenerating row numbers - int_values = self._get_any_column(self._ordering.total_order_col.column_id) + int_values = self._compile_expression( + self._ordering.total_order_col.scalar_expression + ) return encode_order_string( typing.cast(ibis_types.IntegerColumn, int_values), ) @@ -1256,6 +1318,9 @@ def _create_string_ordering_column(self) -> ibis_types.StringColumn: ) return encode_order_string(row_nums) + def _compile_expression(self, expr: ex.Expression): + return op_compiler.compile_expression(expr, self._ibis_bindings) + def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = False): group_by: typing.List[ibis_types.Value] = ( [ @@ -1329,25 +1394,25 @@ def _reduce_predicate_list( def _convert_ordering_to_table_values( value_lookup: typing.Mapping[str, ibis_types.Value], - ordering_columns: typing.Sequence[OrderingColumnReference], + ordering_columns: typing.Sequence[OrderingExpression], ) -> typing.Sequence[ibis_types.Value]: column_refs = ordering_columns ordering_values = [] for ordering_col in column_refs: - column = typing.cast(ibis_types.Column, value_lookup[ordering_col.column_id]) + expr = op_compiler.compile_expression( + ordering_col.scalar_expression, value_lookup + ) ordering_value = ( - ibis.asc(column) - if ordering_col.direction.is_ascending - else ibis.desc(column) + ibis.asc(expr) if ordering_col.direction.is_ascending else ibis.desc(expr) ) # Bigquery SQL considers NULLS to be "smallest" values, but we need to override in these cases. if (not ordering_col.na_last) and (not ordering_col.direction.is_ascending): # Force nulls to be first - is_null_val = typing.cast(ibis_types.Column, column.isnull()) + is_null_val = typing.cast(ibis_types.Column, expr.isnull()) ordering_values.append(ibis.desc(is_null_val)) elif (ordering_col.na_last) and (ordering_col.direction.is_ascending): # Force nulls to be last - is_null_val = typing.cast(ibis_types.Column, column.isnull()) + is_null_val = typing.cast(ibis_types.Column, expr.isnull()) ordering_values.append(ibis.asc(is_null_val)) ordering_values.append(ordering_value) return ordering_values diff --git a/bigframes/core/compile/concat.py b/bigframes/core/compile/concat.py index d39569370e8..41a8f97821b 100644 --- a/bigframes/core/compile/concat.py +++ b/bigframes/core/compile/concat.py @@ -20,8 +20,8 @@ import bigframes.core.compile.compiled as compiled from bigframes.core.ordering import ( + ascending_over, ExpressionOrdering, - OrderingColumnReference, reencode_order_string, StringEncoding, ) @@ -84,7 +84,7 @@ def concat_ordered( tables.append(table) combined_table = ibis.union(*tables) ordering = ExpressionOrdering( - ordering_value_columns=tuple([OrderingColumnReference(ORDER_ID_COLUMN)]), + ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), total_ordering_columns=frozenset([ORDER_ID_COLUMN]), string_encoding=StringEncoding(True, prefix_size + max_encoding_size), ) diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index 31ebf87d176..f1a3d723ac4 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -179,12 +179,10 @@ def join_orderings( left_order_dominates: bool = True, ) -> orderings.ExpressionOrdering: left_ordering_refs = [ - ref.with_name(left_id_mapping[ref.column_id]) - for ref in left.all_ordering_columns + ref.remap_names(left_id_mapping) for ref in left.all_ordering_columns ] right_ordering_refs = [ - ref.with_name(right_id_mapping[ref.column_id]) - for ref in right.all_ordering_columns + ref.remap_names(right_id_mapping) for ref in right.all_ordering_columns ] if left_order_dominates: joined_refs = [*left_ordering_refs, *right_ordering_refs] diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 7379bafa7ec..8c3f52d22bd 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -85,7 +85,7 @@ class Expression(abc.ABC): def unbound_variables(self) -> typing.Tuple[str, ...]: return () - def rename(self, name_mapping: dict[str, str]) -> Expression: + def rename(self, name_mapping: Mapping[str, str]) -> Expression: return self @property @@ -104,6 +104,10 @@ def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: """Replace all variables with expression given in `bindings`.""" ... + @property + def is_bijective(self) -> bool: + return False + @dataclasses.dataclass(frozen=True) class ScalarConstantExpression(Expression): @@ -125,6 +129,11 @@ def output_type( def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: return self + @property + def is_bijective(self) -> bool: + # () <-> value + return True + @dataclasses.dataclass(frozen=True) class UnboundVariableExpression(Expression): @@ -136,7 +145,7 @@ class UnboundVariableExpression(Expression): def unbound_variables(self) -> typing.Tuple[str, ...]: return (self.id,) - def rename(self, name_mapping: dict[str, str]) -> Expression: + def rename(self, name_mapping: Mapping[str, str]) -> Expression: if self.id in name_mapping: return UnboundVariableExpression(name_mapping[self.id]) else: @@ -160,6 +169,10 @@ def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: else: raise ValueError(f"Variable {self.id} remains unbound") + @property + def is_bijective(self) -> bool: + return True + @dataclasses.dataclass(frozen=True) class OpExpression(Expression): @@ -179,7 +192,7 @@ def unbound_variables(self) -> typing.Tuple[str, ...]: ) ) - def rename(self, name_mapping: dict[str, str]) -> Expression: + def rename(self, name_mapping: Mapping[str, str]) -> Expression: return OpExpression( self.op, tuple(input.rename(name_mapping) for input in self.inputs) ) @@ -201,3 +214,8 @@ def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: self.op, tuple(input.bind_all_variables(bindings) for input in self.inputs), ) + + @property + def is_bijective(self) -> bool: + # TODO: Mark individual functions as bijective? + return False diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 2b447a0190d..e2b28553c6b 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -220,7 +220,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: min_periods=min_periods or window, ) block = self._block.order_by( - [order.OrderingColumnReference(col) for col in self._by_col_ids], + [order.ascending_over(col) for col in self._by_col_ids], ) return windows.Window( block, window_spec, self._selected_cols, drop_null_groups=self._dropna @@ -233,7 +233,7 @@ def expanding(self, min_periods: int = 1) -> windows.Window: min_periods=min_periods, ) block = self._block.order_by( - [order.OrderingColumnReference(col) for col in self._by_col_ids], + [order.ascending_over(col) for col in self._by_col_ids], ) return windows.Window( block, window_spec, self._selected_cols, drop_null_groups=self._dropna @@ -573,7 +573,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: min_periods=min_periods or window, ) block = self._block.order_by( - [order.OrderingColumnReference(col) for col in self._by_col_ids], + [order.ascending_over(col) for col in self._by_col_ids], ) return windows.Window( block, @@ -590,7 +590,7 @@ def expanding(self, min_periods: int = 1) -> windows.Window: min_periods=min_periods, ) block = self._block.order_by( - [order.OrderingColumnReference(col) for col in self._by_col_ids], + [order.ascending_over(col) for col in self._by_col_ids], ) return windows.Window( block, diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 958b742636f..c818b68711c 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -261,13 +261,12 @@ def transpose(self) -> Index: def sort_values(self, *, ascending: bool = True, na_position: str = "last"): if na_position not in ["first", "last"]: raise ValueError("Param na_position must be one of 'first' or 'last'") - direction = ( - order.OrderingDirection.ASC if ascending else order.OrderingDirection.DESC - ) na_last = na_position == "last" index_columns = self._block.index_columns ordering = [ - order.OrderingColumnReference(column, direction=direction, na_last=na_last) + order.ascending_over(column, na_last) + if ascending + else order.descending_over(column, na_last) for column in index_columns ] return Index(self._block.order_by(ordering)) @@ -303,13 +302,8 @@ def argmax(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( [ - *[ - order.OrderingColumnReference( - col, direction=order.OrderingDirection.DESC - ) - for col in self._block.index_columns - ], - order.OrderingColumnReference(row_nums), + *[order.descending_over(col) for col in self._block.index_columns], + order.ascending_over(row_nums), ] ) import bigframes.series as series @@ -320,11 +314,8 @@ def argmin(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( [ - *[ - order.OrderingColumnReference(col) - for col in self._block.index_columns - ], - order.OrderingColumnReference(row_nums), + *[order.ascending_over(col) for col in self._block.index_columns], + order.ascending_over(row_nums), ] ) import bigframes.series as series diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 540b7cae928..5ebd2a5997c 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -26,7 +26,7 @@ import bigframes.core.expression as ex import bigframes.core.guid from bigframes.core.join_def import JoinColumnMapping, JoinDefinition, JoinSide -from bigframes.core.ordering import OrderingColumnReference +from bigframes.core.ordering import OrderingExpression import bigframes.core.schema as schemata import bigframes.core.window_spec as window import bigframes.dtypes @@ -290,7 +290,16 @@ def __hash__(self): @dataclass(frozen=True) class OrderByNode(UnaryNode): - by: Tuple[OrderingColumnReference, ...] + by: Tuple[OrderingExpression, ...] + + def __post_init__(self): + available_variables = self.child.schema.names + for order_expr in self.by: + for variable in order_expr.scalar_expression.unbound_variables: + if variable not in available_variables: + raise ValueError( + f"Cannot over unknown id:{variable}, columns are {available_variables}" + ) def __hash__(self): return self._node_hash diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index bbfc7cf9d86..2543a3b722b 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -18,7 +18,7 @@ from enum import Enum import math import typing -from typing import Optional, Sequence +from typing import Mapping, Optional, Sequence, Set import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types @@ -46,40 +46,28 @@ def is_ascending(self) -> bool: return self == OrderingDirection.ASC -@dataclass(frozen=True) -class OrderingColumnReference: - """References a column and how to order with respect to values in that column.""" - - column_id: str - direction: OrderingDirection = OrderingDirection.ASC - na_last: bool = True - - def with_name(self, name: str) -> OrderingColumnReference: - return OrderingColumnReference(name, self.direction, self.na_last) - - def with_reverse(self) -> OrderingColumnReference: - return OrderingColumnReference( - self.column_id, self.direction.reverse(), not self.na_last - ) - - @dataclass(frozen=True) class OrderingExpression: - """ - An expression that defines a scalar value to order, a direction and a null behavior. Maps directly to ORDER BY expressions in GoogleSQL. - This is more of OrderingColumnReference which order on a previously projected column id instead of any scalar expression. - """ + """References a column and how to order with respect to values in that column.""" - # TODO: Right now, expression trees requires projecting a value before it can be sorted on. If OrderByNode used this instead, we could avoid some such projections and simplify the tree. scalar_expression: expression.Expression direction: OrderingDirection = OrderingDirection.ASC na_last: bool = True - def remap_names(self, mapping: dict[str, str]) -> OrderingExpression: + def remap_names(self, mapping: Mapping[str, str]) -> OrderingExpression: return OrderingExpression( self.scalar_expression.rename(mapping), self.direction, self.na_last ) + def bind_variables( + self, mapping: Mapping[str, expression.Expression] + ) -> OrderingExpression: + return OrderingExpression( + self.scalar_expression.bind_all_variables(mapping), + self.direction, + self.na_last, + ) + def with_reverse(self) -> OrderingExpression: return OrderingExpression( self.scalar_expression, self.direction.reverse(), not self.na_last @@ -109,7 +97,7 @@ class IntegerEncoding: class ExpressionOrdering: """Immutable object that holds information about the ordering of rows in a ArrayValue object.""" - ordering_value_columns: typing.Tuple[OrderingColumnReference, ...] = () + ordering_value_columns: typing.Tuple[OrderingExpression, ...] = () integer_encoding: IntegerEncoding = IntegerEncoding(False) string_encoding: StringEncoding = StringEncoding(False) # A table has a total ordering defined by the identities of a set of 1 or more columns. @@ -120,7 +108,7 @@ class ExpressionOrdering: @classmethod def from_offset_col(cls, col: str) -> ExpressionOrdering: return ExpressionOrdering( - (OrderingColumnReference(col),), + (ascending_over(col),), integer_encoding=IntegerEncoding(True, is_sequential=True), total_ordering_columns=frozenset({col}), ) @@ -143,7 +131,7 @@ def with_non_sequential(self): def with_ordering_columns( self, - ordering_value_columns: Sequence[OrderingColumnReference] = (), + ordering_value_columns: Sequence[OrderingExpression] = (), ) -> ExpressionOrdering: """Creates a new ordering that reorders by the given columns. @@ -154,18 +142,10 @@ def with_ordering_columns( Returns: Modified ExpressionOrdering """ - col_ids_new = [ - ordering_ref.column_id for ordering_ref in ordering_value_columns - ] - old_ordering_keep = [ - ordering_ref - for ordering_ref in self.ordering_value_columns - if ordering_ref.column_id not in col_ids_new - ] # Truncate to remove any unneded col references after all total order cols included new_ordering = self._truncate_ordering( - (*ordering_value_columns, *old_ordering_keep) + (*ordering_value_columns, *self.ordering_value_columns) ) return ExpressionOrdering( new_ordering, @@ -173,15 +153,20 @@ def with_ordering_columns( ) def _truncate_ordering( - self, order_refs: tuple[OrderingColumnReference, ...] - ) -> tuple[OrderingColumnReference, ...]: - total_order_cols_remaining = set(self.total_ordering_columns) - for i in range(len(order_refs)): - column = order_refs[i].column_id - if column in total_order_cols_remaining: - total_order_cols_remaining.remove(column) - if len(total_order_cols_remaining) == 0: - return order_refs[: i + 1] + self, order_refs: tuple[OrderingExpression, ...] + ) -> tuple[OrderingExpression, ...]: + # Truncate once we refer to a full key in bijective operations + must_see = set(self.total_ordering_columns) + columns_seen: Set[str] = set() + truncated_refs = [] + for order_part in order_refs: + expr = order_part.scalar_expression + if not set(expr.unbound_variables).issubset(columns_seen): + if expr.is_bijective: + columns_seen.update(expr.unbound_variables) + truncated_refs.append(order_part) + if columns_seen.issuperset(must_see): + return tuple(truncated_refs) raise ValueError("Ordering did not contain all total_order_cols") def with_reverse(self): @@ -193,8 +178,7 @@ def with_reverse(self): def with_column_remap(self, mapping: typing.Mapping[str, str]): new_value_columns = [ - col.with_name(mapping.get(col.column_id, col.column_id)) - for col in self.ordering_value_columns + col.remap_names(mapping) for col in self.all_ordering_columns ] new_total_order = frozenset( mapping.get(col_id, col_id) for col_id in self.total_ordering_columns @@ -207,7 +191,7 @@ def with_column_remap(self, mapping: typing.Mapping[str, str]): ) @property - def total_order_col(self) -> Optional[OrderingColumnReference]: + def total_order_col(self) -> Optional[OrderingExpression]: """Returns column id of columns that defines total ordering, if such as column exists""" if len(self.ordering_value_columns) != 1: return None @@ -226,7 +210,7 @@ def is_sequential(self) -> bool: return self.integer_encoding.is_encoded and self.integer_encoding.is_sequential @property - def all_ordering_columns(self) -> Sequence[OrderingColumnReference]: + def all_ordering_columns(self) -> Sequence[OrderingExpression]: return list(self.ordering_value_columns) @@ -252,3 +236,14 @@ def reencode_order_string( ibis_types.StringColumn, (typing.cast(ibis_types.StringValue, order_id).lpad(length, "0")), ) + + +# Convenience functions +def ascending_over(id: str, nulls_last: bool = True) -> OrderingExpression: + return OrderingExpression(expression.free_var(id), na_last=nulls_last) + + +def descending_over(id: str, nulls_last: bool = True) -> OrderingExpression: + return OrderingExpression( + expression.free_var(id), direction=OrderingDirection.DESC, na_last=nulls_last + ) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index ffbba109366..e3ed8edd21c 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -171,7 +171,7 @@ def qcut( agg_ops.QcutOp(q), # type: ignore window_spec=core.WindowSpec( grouping_keys=(nullity_id,), - ordering=(order.OrderingColumnReference(x._value_column),), + ordering=(order.ascending_over(x._value_column),), ), ) block, result = block.project_expr( diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index a518108f4ab..61fe28b7b53 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -16,10 +16,9 @@ import dataclasses import functools import itertools -from typing import Optional, Sequence, Tuple +from typing import Mapping, Optional, Sequence, Tuple import bigframes.core.expression as scalar_exprs -import bigframes.core.guid as guids import bigframes.core.join_def as join_defs import bigframes.core.nodes as nodes import bigframes.core.ordering as order @@ -54,22 +53,24 @@ def from_node(cls, node: nodes.BigFrameNode) -> SquashedSelect: ) return cls(node, selection, None, ()) + @property + def column_lookup(self) -> Mapping[str, scalar_exprs.Expression]: + return {col_id: expr for expr, col_id in self.columns} + def project( self, projection: Tuple[Tuple[scalar_exprs.Expression, str], ...] ) -> SquashedSelect: - lookup = {id: expr for expr, id in self.columns} new_columns = tuple( - (expr.bind_all_variables(lookup), id) for expr, id in projection + (expr.bind_all_variables(self.column_lookup), id) for expr, id in projection ) return SquashedSelect(self.root, new_columns, self.predicate, self.ordering) def filter(self, predicate: scalar_exprs.Expression) -> SquashedSelect: - lookup = {id: expr for expr, id in self.columns} if self.predicate is None: - new_predicate = predicate.bind_all_variables(lookup) + new_predicate = predicate.bind_all_variables(self.column_lookup) else: new_predicate = ops.and_op.as_expr( - self.predicate, predicate.bind_all_variables(lookup) + self.predicate, predicate.bind_all_variables(self.column_lookup) ) return SquashedSelect(self.root, self.columns, new_predicate, self.ordering) @@ -77,15 +78,11 @@ def reverse(self) -> SquashedSelect: new_ordering = tuple(expr.with_reverse() for expr in self.ordering) return SquashedSelect(self.root, self.columns, self.predicate, new_ordering) - def order_with(self, by: Tuple[order.OrderingColumnReference, ...]): - exprs_by_id = {id: expr for expr, id in self.columns} - as_order_exprs = [ - order.OrderingExpression( - exprs_by_id[ref.column_id], ref.direction, ref.na_last - ) - for ref in by + def order_with(self, by: Tuple[order.OrderingExpression, ...]): + adjusted_orderings = [ + order_part.bind_variables(self.column_lookup) for order_part in by ] - new_ordering = (*as_order_exprs, *self.ordering) + new_ordering = (*adjusted_orderings, *self.ordering) return SquashedSelect(self.root, self.columns, self.predicate, new_ordering) def maybe_join( @@ -165,37 +162,12 @@ def maybe_join( def expand(self) -> nodes.BigFrameNode: # Safest to apply predicates first, as it may filter out inputs that cannot be handled by other expressions - root = ( - nodes.FilterNode(child=self.root, predicate=self.predicate) - if self.predicate - else self.root - ) + root = self.root + if self.predicate: + root = nodes.FilterNode(child=root, predicate=self.predicate) if self.ordering: - # Need this clumsy 3-node expansion as OrderByNode doesn't support expressions (yet?) - # Could also directly compile this whole class directly - ordering_assignments = [ - (ref.scalar_expression, guids.generate_guid()) for ref in self.ordering - ] - as_ordering_refs = tuple( - order.OrderingColumnReference(id, ref.direction, ref.na_last) - for ref, (_, id) in zip(self.ordering, ordering_assignments) - ) - extended_projection = nodes.ProjectionNode( - child=root, assignments=(*self.columns, *ordering_assignments) - ) - ordered_node = nodes.OrderByNode( - child=extended_projection, by=as_ordering_refs - ) - drop_ordering_selection = tuple( - (scalar_exprs.UnboundVariableExpression(id), id) - for _, id in self.columns - ) - pruned_node = nodes.ProjectionNode( - child=ordered_node, assignments=drop_ordering_selection - ) - return pruned_node - else: - return nodes.ProjectionNode(child=root, assignments=self.columns) + root = nodes.OrderByNode(child=root, by=self.ordering) + return nodes.ProjectionNode(child=root, assignments=self.columns) def maybe_rewrite_join(join_node: nodes.JoinNode) -> nodes.BigFrameNode: diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py index 3458bfb1b8c..b02f13d333e 100644 --- a/bigframes/core/window_spec.py +++ b/bigframes/core/window_spec.py @@ -29,7 +29,7 @@ class WindowSpec: """ grouping_keys: typing.Tuple[str, ...] = tuple() - ordering: typing.Tuple[orderings.OrderingColumnReference, ...] = tuple() + ordering: typing.Tuple[orderings.OrderingExpression, ...] = tuple() preceding: typing.Optional[int] = None following: typing.Optional[int] = None min_periods: int = 0 diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 4e447c547f5..6a073ba9f26 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1445,13 +1445,12 @@ def sort_index( ) -> DataFrame: if na_position not in ["first", "last"]: raise ValueError("Param na_position must be one of 'first' or 'last'") - direction = ( - order.OrderingDirection.ASC if ascending else order.OrderingDirection.DESC - ) na_last = na_position == "last" index_columns = self._block.index_columns ordering = [ - order.OrderingColumnReference(column, direction=direction, na_last=na_last) + order.ascending_over(column, na_last) + if ascending + else order.descending_over(column, na_last) for column in index_columns ] return DataFrame(self._block.order_by(ordering)) @@ -1481,16 +1480,12 @@ def sort_values( ordering = [] for i in range(len(sort_labels)): column_id = sort_column_ids[i] - direction = ( - order.OrderingDirection.ASC - if sort_directions[i] - else order.OrderingDirection.DESC - ) + is_ascending = sort_directions[i] na_last = na_position == "last" ordering.append( - order.OrderingColumnReference( - column_id, direction=direction, na_last=na_last - ) + order.ascending_over(column_id, na_last) + if is_ascending + else order.descending_over(column_id, na_last) ) return DataFrame(self._block.order_by(ordering)) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index f2bcbd894a8..4ecb8dca5a5 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -42,6 +42,11 @@ def arguments(self) -> int: def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: ... + @property + def order_preserving(self) -> bool: + """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" + ... + # These classes can be used to create simple ops that don't take local parameters # All is needed is a unique name, and to register an implementation in ibis_mappings.py @@ -67,6 +72,11 @@ def as_expr( self, (_convert_expr_input(input_id),) ) + @property + def order_preserving(self) -> bool: + """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" + return False + @dataclasses.dataclass(frozen=True) class BinaryOp: @@ -96,6 +106,11 @@ def as_expr( ), ) + @property + def order_preserving(self) -> bool: + """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" + return False + @dataclasses.dataclass(frozen=True) class TernaryOp: @@ -127,6 +142,11 @@ def as_expr( ), ) + @property + def order_preserving(self) -> bool: + """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" + return False + def _convert_expr_input( input: typing.Union[str, bigframes.core.expression.Expression] diff --git a/bigframes/series.py b/bigframes/series.py index 5f6cfe98930..f9aa59d6ecb 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -40,7 +40,7 @@ import bigframes.core.groupby as groupby import bigframes.core.indexers import bigframes.core.indexes as indexes -from bigframes.core.ordering import OrderingColumnReference, OrderingDirection +import bigframes.core.ordering as order import bigframes.core.scalar as scalars import bigframes.core.utils as utils import bigframes.core.window @@ -875,7 +875,7 @@ def mode(self) -> Series: # use temporary name for reset_index to avoid collision, restore after dropping extra columns block = ( block.with_index_labels(["mode_temp_internal"]) - .order_by([OrderingColumnReference(self._value_column)]) + .order_by([order.ascending_over(self._value_column)]) .reset_index(drop=False) ) block = block.select_column(self._value_column).with_column_labels([self.name]) @@ -941,10 +941,8 @@ def argmax(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( [ - OrderingColumnReference( - self._value_column, direction=OrderingDirection.DESC - ), - OrderingColumnReference(row_nums), + order.descending_over(self._value_column), + order.ascending_over(row_nums), ] ) return typing.cast( @@ -955,8 +953,8 @@ def argmin(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( [ - OrderingColumnReference(self._value_column), - OrderingColumnReference(row_nums), + order.ascending_over(self._value_column), + order.ascending_over(row_nums), ] ) return typing.cast( @@ -989,11 +987,9 @@ def unstack(self, level: LevelsType = -1): def idxmax(self) -> blocks.Label: block = self._block.order_by( [ - OrderingColumnReference( - self._value_column, direction=OrderingDirection.DESC - ), + order.descending_over(self._value_column), *[ - OrderingColumnReference(idx_col) + order.ascending_over(idx_col) for idx_col in self._block.index_columns ], ] @@ -1004,9 +1000,9 @@ def idxmax(self) -> blocks.Label: def idxmin(self) -> blocks.Label: block = self._block.order_by( [ - OrderingColumnReference(self._value_column), + order.ascending_over(self._value_column), *[ - OrderingColumnReference(idx_col) + order.ascending_over(idx_col) for idx_col in self._block.index_columns ], ] @@ -1099,14 +1095,11 @@ def sort_values( ) -> Series: if na_position not in ["first", "last"]: raise ValueError("Param na_position must be one of 'first' or 'last'") - direction = OrderingDirection.ASC if ascending else OrderingDirection.DESC block = self._block.order_by( [ - OrderingColumnReference( - self._value_column, - direction=direction, - na_last=(na_position == "last"), - ) + order.ascending_over(self._value_column, (na_position == "last")) + if ascending + else order.descending_over(self._value_column, (na_position == "last")) ], ) return Series(block) @@ -1116,10 +1109,11 @@ def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: if na_position not in ["first", "last"]: raise ValueError("Param na_position must be one of 'first' or 'last'") block = self._block - direction = OrderingDirection.ASC if ascending else OrderingDirection.DESC na_last = na_position == "last" ordering = [ - OrderingColumnReference(column, direction=direction, na_last=na_last) + order.ascending_over(column, na_last) + if ascending + else order.descending_over(column, na_last) for column in block.index_columns ] block = block.order_by(ordering) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 5732d4b08e2..b826d429230 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -79,8 +79,8 @@ import bigframes.core.blocks as blocks import bigframes.core.compile import bigframes.core.guid as guid -from bigframes.core.ordering import IntegerEncoding, OrderingColumnReference -import bigframes.core.ordering as orderings +from bigframes.core.ordering import IntegerEncoding +import bigframes.core.ordering as order import bigframes.core.traversal as traversals import bigframes.core.utils as utils import bigframes.dataframe as dataframe @@ -747,10 +747,9 @@ def _read_gbq_table( # Note: currently, a table has a total ordering only when the # primary key(s) are set on a table. The query engine assumes such # columns are unique, even if not enforced. - ordering = orderings.ExpressionOrdering( + ordering = order.ExpressionOrdering( ordering_value_columns=tuple( - core.OrderingColumnReference(column_id) - for column_id in total_ordering_cols + order.ascending_over(column_id) for column_id in total_ordering_cols ), total_ordering_columns=frozenset(total_ordering_cols), ) @@ -765,12 +764,9 @@ def _read_gbq_table( elif len(index_cols) != 0: # We have index columns, lets see if those are actually total_order_columns - ordering = orderings.ExpressionOrdering( + ordering = order.ExpressionOrdering( ordering_value_columns=tuple( - [ - core.OrderingColumnReference(column_id) - for column_id in index_cols - ] + [order.ascending_over(column_id) for column_id in index_cols] ), total_ordering_columns=frozenset(index_cols), ) @@ -1024,8 +1020,8 @@ def _read_pandas_load_job( ) self._start_generic_job(load_job) - ordering = orderings.ExpressionOrdering( - ordering_value_columns=tuple([OrderingColumnReference(ordering_col)]), + ordering = order.ExpressionOrdering( + ordering_value_columns=tuple([order.ascending_over(ordering_col)]), total_ordering_columns=frozenset([ordering_col]), integer_encoding=IntegerEncoding(True, is_sequential=True), ) @@ -1380,9 +1376,9 @@ def _create_total_ordering( itertools.chain(original_column_ids, [full_row_hash, random_value]) ) - ordering_ref1 = core.OrderingColumnReference(ordering_hash_part) - ordering_ref2 = core.OrderingColumnReference(ordering_rand_part) - ordering = orderings.ExpressionOrdering( + ordering_ref1 = order.ascending_over(ordering_hash_part) + ordering_ref2 = order.ascending_over(ordering_rand_part) + ordering = order.ExpressionOrdering( ordering_value_columns=(ordering_ref1, ordering_ref2), total_ordering_columns=frozenset([ordering_hash_part, ordering_rand_part]), ) @@ -1746,7 +1742,7 @@ def _cache_with_offsets(self, array_value: core.ArrayValue) -> core.ArrayValue: table_expression, columns=new_columns, hidden_ordering_columns=new_hidden_columns, - ordering=orderings.ExpressionOrdering.from_offset_col("bigframes_offsets"), + ordering=order.ExpressionOrdering.from_offset_col("bigframes_offsets"), ) def _is_trivially_executable(self, array_value: core.ArrayValue): diff --git a/tests/unit/resources.py b/tests/unit/resources.py index b57cd853600..6846659930a 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -119,7 +119,7 @@ def create_arrayvalue( columns = tuple(ibis_table[key] for key in ibis_table.columns) ordering = bigframes.core.ordering.ExpressionOrdering( tuple( - [core.OrderingColumnReference(column) for column in total_ordering_columns] + [core.orderings.ascending_over(column) for column in total_ordering_columns] ), total_ordering_columns=frozenset(total_ordering_columns), ) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 5f940fd7a51..0a2fc61418f 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -17,7 +17,7 @@ import bigframes.core as core import bigframes.core.expression as ex -import bigframes.core.ordering +import bigframes.core.ordering as order import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -39,8 +39,8 @@ def test_arrayvalue_constructor_from_ibis_table_adds_all_columns(): ) ibis_table = session.ibis_client.table("test_table") columns = (ibis_table["col1"], ibis_table["col2"], ibis_table["col3"]) - ordering = bigframes.core.ordering.ExpressionOrdering( - tuple([core.OrderingColumnReference("col1")]), + ordering = order.ExpressionOrdering( + tuple([order.ascending_over("col1")]), total_ordering_columns=frozenset(["col1"]), ) actual = core.ArrayValue.from_ibis( From 60d4a7bbac867256f8bbfd3053c7dd2645c1b062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 21 Mar 2024 18:02:16 -0500 Subject: [PATCH 402/921] feat: update plot sample to 1000 rows (#458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In making a line plot sample with Salem, I noticed that 100 rows loses some important shape information. Most screens are > 1000 pixels wide, so this seems a reasonable default. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/operations/_matplotlib/core.py | 9 +++++++-- tests/system/small/operations/test_plotting.py | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index 7cbeb3df4fb..b4beea75fd3 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -17,6 +17,9 @@ import matplotlib.pyplot as plt +DEFAULT_SAMPLING_N = 1000 +DEFAULT_SAMPLING_STATE = 0 + class MPLPlot(abc.ABC): @abc.abstractmethod @@ -45,8 +48,10 @@ def generate(self) -> None: def _compute_plot_data(self, data): # TODO: Cache the sampling data in the PlotAccessor. - sampling_n = self.kwargs.pop("sampling_n", 100) - sampling_random_state = self.kwargs.pop("sampling_random_state", 0) + sampling_n = self.kwargs.pop("sampling_n", DEFAULT_SAMPLING_N) + sampling_random_state = self.kwargs.pop( + "sampling_random_state", DEFAULT_SAMPLING_STATE + ) return data.sample( n=sampling_n, random_state=sampling_random_state, diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index 47491cdadad..5ca3382e2a1 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -17,6 +17,7 @@ import pandas._testing as tm import pytest +import bigframes.operations._matplotlib.core as bf_mpl import bigframes.pandas as bpd @@ -209,11 +210,10 @@ def test_scatter(scalars_dfs): def test_sampling_plot_args_n(): - df = bpd.DataFrame(np.arange(1000), columns=["one"]) + df = bpd.DataFrame(np.arange(bf_mpl.DEFAULT_SAMPLING_N * 10), columns=["one"]) ax = df.plot.line() assert len(ax.lines) == 1 - # Default sampling_n is 100 - assert len(ax.lines[0].get_data()[1]) == 100 + assert len(ax.lines[0].get_data()[1]) == bf_mpl.DEFAULT_SAMPLING_N ax = df.plot.line(sampling_n=2) assert len(ax.lines) == 1 @@ -221,7 +221,7 @@ def test_sampling_plot_args_n(): def test_sampling_plot_args_random_state(): - df = bpd.DataFrame(np.arange(1000), columns=["one"]) + df = bpd.DataFrame(np.arange(bf_mpl.DEFAULT_SAMPLING_N * 10), columns=["one"]) ax_0 = df.plot.line() ax_1 = df.plot.line() ax_2 = df.plot.line(sampling_random_state=100) From 352cb850d23e41a2278edf0df584b89ee9619aab Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:18:26 -0700 Subject: [PATCH 403/921] feat: add ml ARIMAPlus model params (#488) --- bigframes/ml/forecasting.py | 157 +++++++++++++++++++++- tests/system/large/ml/test_forecasting.py | 41 ++++++ 2 files changed, 192 insertions(+), 6 deletions(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 18380328c72..292389dcbba 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -16,7 +16,7 @@ from __future__ import annotations -from typing import Dict, List, Optional, Union +from typing import List, Optional, Union from google.cloud import bigquery @@ -25,12 +25,108 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +_BQML_PARAMS_MAPPING = { + "horizon": "horizon", + "auto_arima": "autoArima", + "auto_arima_max_order": "autoArimaMaxOrder", + "auto_arima_min_order": "autoArimaMinOrder", + "order": "nonSeasonalOrder", + "data_frequency": "dataFrequency", + "holiday_region": "holidayRegion", + "clean_spikes_and_dips": "cleanSpikesAndDips", + "adjust_step_changes": "adjustStepChanges", + "time_series_length_fraction": "timeSeriesLengthFraction", + "min_time_series_length": "minTimeSeriesLength", + "max_time_series_length": "maxTimeSeriesLength", + "decompose_time_series": "decomposeTimeSeries", + "trend_smoothing_window_size": "trendSmoothingWindowSize", +} + @log_adapter.class_logger class ARIMAPlus(base.SupervisedTrainablePredictor): - """Time Series ARIMA Plus model.""" + """Time Series ARIMA Plus model. + + Args: + horizon (int, default 1,000): + The number of time points to forecast. Default to 1,000, max value 10,000. + + auto_arima (bool, default True): + Determines whether the training process uses auto.ARIMA or not. If True, training automatically finds the best non-seasonal order (that is, the p, d, q tuple) and decides whether or not to include a linear drift term when d is 1. + + auto_arima_max_order (int or None, default None): + The maximum value for the sum of non-seasonal p and q. + + auto_arima_min_order (int or None, default None): + The minimum value for the sum of non-seasonal p and q. + + data_frequency (str, default "auto_frequency"): + The data frequency of the input time series. + Possible values are "auto_frequency", "per_minute", "hourly", "daily", "weekly", "monthly", "quarterly", "yearly" + + include_drift (bool, defalut False): + Determines whether the model should include a linear drift term or not. The drift term is applicable when non-seasonal d is 1. + + holiday_region (str or None, default None): + The geographical region based on which the holiday effect is applied in modeling. By default, holiday effect modeling isn't used. + Possible values see https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-time-series#holiday_region. + + clean_spikes_and_dips (bool, default True): + Determines whether or not to perform automatic spikes and dips detection and cleanup in the model training pipeline. The spikes and dips are replaced with local linear interpolated values when they're detected. + + adjust_step_changes (bool, default True): + Determines whether or not to perform automatic step change detection and adjustment in the model training pipeline. + + time_series_length_fraction (float or None, default None): + The fraction of the interpolated length of the time series that's used to model the time series trend component. All of the time points of the time series are used to model the non-trend component. + + min_time_series_length (int or None, default None): + The minimum number of time points that are used in modeling the trend component of the time series. + + max_time_series_length (int or None, default None): + The maximum number of time points in a time series that can be used in modeling the trend component of the time series. + + trend_smoothing_window_size (int or None, default None): + The smoothing window size for the trend component. + + decompose_time_series (bool, default True): + Determines whether the separate components of both the history and forecast parts of the time series (such as holiday effect and seasonal components) are saved in the model. + """ + + def __init__( + self, + *, + horizon: int = 1000, + auto_arima: bool = True, + auto_arima_max_order: Optional[int] = None, + auto_arima_min_order: Optional[int] = None, + data_frequency: str = "auto_frequency", + include_drift: bool = False, + holiday_region: Optional[str] = None, + clean_spikes_and_dips: bool = True, + adjust_step_changes: bool = True, + time_series_length_fraction: Optional[float] = None, + min_time_series_length: Optional[int] = None, + max_time_series_length: Optional[int] = None, + trend_smoothing_window_size: Optional[int] = None, + decompose_time_series: bool = True, + ): + self.horizon = horizon + self.auto_arima = auto_arima + self.auto_arima_max_order = auto_arima_max_order + self.auto_arima_min_order = auto_arima_min_order + self.data_frequency = data_frequency + self.include_drift = include_drift + self.holiday_region = holiday_region + self.clean_spikes_and_dips = clean_spikes_and_dips + self.adjust_step_changes = adjust_step_changes + self.time_series_length_fraction = time_series_length_fraction + self.min_time_series_length = min_time_series_length + self.max_time_series_length = max_time_series_length + self.trend_smoothing_window_size = trend_smoothing_window_size + self.decompose_time_series = decompose_time_series + # TODO(garrettwu) add order and seasonalities params, which need struct/array - def __init__(self): self._bqml_model: Optional[core.BqmlModel] = None self._bqml_model_factory = globals.bqml_model_factory() @@ -38,16 +134,65 @@ def __init__(self): def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ARIMAPlus: assert model.model_type == "ARIMA_PLUS" - kwargs: Dict[str, str | int | bool | float | List[str]] = {} + kwargs: dict = {} + last_fitting = model.training_runs[-1]["trainingOptions"] + + dummy_arima = cls() + for bf_param, bf_value in dummy_arima.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param in last_fitting: + # Convert types + if bf_param in ["time_series_length_fraction"]: + kwargs[bf_param] = float(last_fitting[bqml_param]) + elif bf_param in [ + "auto_arima_max_order", + "auto_arima_min_order", + "min_time_series_length", + "max_time_series_length", + "trend_smoothing_window_size", + ]: + kwargs[bf_param] = int(last_fitting[bqml_param]) + elif bf_param in ["holiday_region"]: + kwargs[bf_param] = str(last_fitting[bqml_param]) + else: + kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) new_arima_plus = cls(**kwargs) new_arima_plus._bqml_model = core.BqmlModel(session, model) return new_arima_plus @property - def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: + def _bqml_options(self) -> dict: """The model options as they will be set for BQML.""" - return {"model_type": "ARIMA_PLUS"} + options = { + "model_type": "ARIMA_PLUS", + "horizon": self.horizon, + "auto_arima": self.auto_arima, + "data_frequency": self.data_frequency, + "clean_spikes_and_dips": self.clean_spikes_and_dips, + "adjust_step_changes": self.adjust_step_changes, + "decompose_time_series": self.decompose_time_series, + } + + if self.auto_arima_max_order is not None: + options["auto_arima_max_order"] = self.auto_arima_max_order + if self.auto_arima_min_order is not None: + options["auto_arima_min_order"] = self.auto_arima_min_order + if self.holiday_region is not None: + options["holiday_region"] = self.holiday_region + if self.time_series_length_fraction is not None: + options["time_series_length_fraction"] = self.time_series_length_fraction + if self.min_time_series_length is not None: + options["min_time_series_length"] = self.min_time_series_length + if self.max_time_series_length is not None: + options["max_time_series_length"] = self.max_time_series_length + if self.trend_smoothing_window_size is not None: + options["trend_smoothing_window_size"] = self.trend_smoothing_window_size + + if self.include_drift: + options["include_drift"] = True + + return options def _fit( self, diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index 2bb136b0f2d..b333839e2ea 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -77,3 +77,44 @@ def test_arima_plus_model_fit_summary(time_series_df_default_index, dataset_id): assert ( f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name ) + + +def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): + model = forecasting.ARIMAPlus( + horizon=100, + auto_arima=True, + auto_arima_max_order=4, + auto_arima_min_order=1, + data_frequency="daily", + holiday_region="US", + clean_spikes_and_dips=False, + adjust_step_changes=False, + time_series_length_fraction=0.5, + min_time_series_length=10, + trend_smoothing_window_size=5, + decompose_time_series=False, + ) + + X_train = time_series_df_default_index[["parsed_date"]] + y_train = time_series_df_default_index[["total_visits"]] + model.fit(X_train, y_train) + + # save, load to ensure configuration was kept + reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) + assert ( + f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name + ) + + assert reloaded_model.horizon == 100 + assert reloaded_model.auto_arima is True + assert reloaded_model.auto_arima_max_order == 4 + # TODO(garrettwu): now BQML doesn't populate auto_arima_min_order + # assert reloaded_model.auto_arima_min_order == 1 + assert reloaded_model.data_frequency == "DAILY" + assert reloaded_model.holiday_region == "US" + assert reloaded_model.clean_spikes_and_dips is False + assert reloaded_model.adjust_step_changes is False + assert reloaded_model.time_series_length_fraction == 0.5 + assert reloaded_model.min_time_series_length == 10 + assert reloaded_model.trend_smoothing_window_size == 5 + assert reloaded_model.decompose_time_series is False From f959b653a0e82b5bfd21f9e994031cf6d25c281a Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:19:13 -0700 Subject: [PATCH 404/921] feat: add ml LogisticRegression model params (#481) * feat: add ml LogisticRegression model params * fix tests * fix tests --- bigframes/ml/linear_model.py | 64 +++++++++++++++++-- tests/system/large/ml/test_linear_model.py | 34 +++++++--- tests/unit/ml/test_golden_sql.py | 18 ++++-- .../sklearn/linear_model/_base.py | 4 +- .../sklearn/linear_model/_logistic.py | 24 +++++++ 5 files changed, 123 insertions(+), 21 deletions(-) diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 62855bae902..b3db04df052 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -38,7 +38,9 @@ "learn_rate_strategy": "learnRateStrategy", "learn_rate": "learnRate", "early_stop": "earlyStop", + # To rename to tol. "min_rel_progress": "minRelativeProgress", + "tol": "minRelativeProgress", "ls_init_learn_rate": "initialLearnRate", "warm_start": "warmStart", "calculate_p_values": "calculatePValues", @@ -59,7 +61,7 @@ def __init__( *, optimize_strategy: Literal[ "auto_strategy", "batch_gradient_descent", "normal_equation" - ] = "normal_equation", + ] = "auto_strategy", fit_intercept: bool = True, l1_reg: Optional[float] = None, l2_reg: float = 0.0, @@ -139,7 +141,7 @@ def _bqml_options(self) -> dict: if self.ls_init_learn_rate is not None: options["ls_init_learn_rate"] = self.ls_init_learn_rate # Even presenting warm_start returns error for NORMAL_EQUATION optimizer - if self.warm_start is True: + if self.warm_start: options["warm_start"] = self.warm_start return options @@ -212,10 +214,34 @@ class LogisticRegression( def __init__( self, *, + optimize_strategy: Literal[ + "auto_strategy", "batch_gradient_descent", "normal_equation" + ] = "auto_strategy", fit_intercept: bool = True, + l1_reg: Optional[float] = None, + l2_reg: float = 0.0, + max_iterations: int = 20, + warm_start: bool = False, + learn_rate: Optional[float] = None, + learn_rate_strategy: Literal["line_search", "constant"] = "line_search", + tol: float = 0.01, + ls_init_learn_rate: Optional[float] = None, + calculate_p_values: bool = False, + enable_global_explain: bool = False, class_weights: Optional[Union[Literal["balanced"], Dict[str, float]]] = None, ): + self.optimize_strategy = optimize_strategy self.fit_intercept = fit_intercept + self.l1_reg = l1_reg + self.l2_reg = l2_reg + self.max_iterations = max_iterations + self.warm_start = warm_start + self.learn_rate = learn_rate + self.learn_rate_strategy = learn_rate_strategy + self.tol = tol + self.ls_init_learn_rate = ls_init_learn_rate + self.calculate_p_values = calculate_p_values + self.enable_global_explain = enable_global_explain self.class_weights = class_weights self._auto_class_weight = class_weights == "balanced" self._bqml_model: Optional[core.BqmlModel] = None @@ -231,8 +257,16 @@ def _from_bq( # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun last_fitting = model.training_runs[-1]["trainingOptions"] - if "fitIntercept" in last_fitting: - kwargs["fit_intercept"] = last_fitting["fitIntercept"] + dummy_logistic = cls() + for bf_param, bf_value in dummy_logistic.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param in last_fitting: + # Convert types + kwargs[bf_param] = ( + float(last_fitting[bqml_param]) + if bf_param in ["l1_reg", "learn_rate", "ls_init_learn_rate"] + else type(bf_value)(last_fitting[bqml_param]) + ) if last_fitting["autoClassWeights"]: kwargs["class_weights"] = "balanced" # TODO(ashleyxu) support class_weights in the constructor. @@ -244,16 +278,34 @@ def _from_bq( return new_logistic_regression @property - def _bqml_options(self) -> Dict[str, str | int | float | List[str]]: + def _bqml_options(self) -> dict: """The model options as they will be set for BQML""" - return { + options = { "model_type": "LOGISTIC_REG", "data_split_method": "NO_SPLIT", "fit_intercept": self.fit_intercept, "auto_class_weights": self._auto_class_weight, + "optimize_strategy": self.optimize_strategy, + "l2_reg": self.l2_reg, + "max_iterations": self.max_iterations, + "learn_rate_strategy": self.learn_rate_strategy, + "min_rel_progress": self.tol, + "calculate_p_values": self.calculate_p_values, + "enable_global_explain": self.enable_global_explain, # TODO(ashleyxu): support class_weights (struct array as dict in our API) # "class_weights": self.class_weights, } + if self.l1_reg is not None: + options["l1_reg"] = self.l1_reg + if self.learn_rate is not None: + options["learn_rate"] = self.learn_rate + if self.ls_init_learn_rate is not None: + options["ls_init_learn_rate"] = self.ls_init_learn_rate + # Even presenting warm_start returns error for NORMAL_EQUATION optimizer + if self.warm_start: + options["warm_start"] = self.warm_start + + return options def _fit( self, diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 03fed007700..3616cc4dd0c 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -184,7 +184,15 @@ def test_logistic_regression_customized_params_fit_score( penguins_df_default_index, dataset_id ): model = bigframes.ml.linear_model.LogisticRegression( - fit_intercept=False, class_weights="balanced" + fit_intercept=False, + class_weights="balanced", + l2_reg=0.2, + tol=0.02, + l1_reg=0.2, + max_iterations=30, + optimize_strategy="batch_gradient_descent", + learn_rate_strategy="constant", + learn_rate=0.2, ) df = penguins_df_default_index.dropna() X_train = df[ @@ -203,12 +211,12 @@ def test_logistic_regression_customized_params_fit_score( result = model.score(X_train, y_train).to_pandas() expected = pd.DataFrame( { - "precision": [0.58483], - "recall": [0.586616], - "accuracy": [0.877246], - "f1_score": [0.58571], - "log_loss": [1.032699], - "roc_auc": [0.924132], + "precision": [0.487], + "recall": [0.602], + "accuracy": [0.464], + "f1_score": [0.379], + "log_loss": [0.972], + "roc_auc": [0.700], }, dtype="Float64", ) @@ -223,5 +231,15 @@ def test_logistic_regression_customized_params_fit_score( f"{dataset_id}.temp_configured_logistic_reg_model" in reloaded_model._bqml_model.model_name ) + # TODO(garrettwu) optimize_strategy isn't logged in BQML + # assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" assert reloaded_model.fit_intercept is False - assert reloaded_model.class_weights == "balanced" + assert reloaded_model.calculate_p_values is False + assert reloaded_model.enable_global_explain is False + assert reloaded_model.l1_reg == 0.2 + assert reloaded_model.l2_reg == 0.2 + assert reloaded_model.ls_init_learn_rate is None + assert reloaded_model.max_iterations == 30 + assert reloaded_model.tol == 0.02 + assert reloaded_model.learn_rate_strategy == "CONSTANT" + assert reloaded_model.learn_rate == 0.2 diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 8996a9c77f1..c7c4437a6ea 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -105,7 +105,7 @@ def test_linear_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -115,7 +115,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="normal_equation",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -148,7 +148,7 @@ def test_logistic_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n optimize_strategy="auto_strategy",\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -156,13 +156,21 @@ def test_logistic_regression_params_fit( bqml_model_factory, mock_session, mock_X, mock_y ): model = linear_model.LogisticRegression( - fit_intercept=False, class_weights="balanced" + fit_intercept=False, + class_weights="balanced", + l2_reg=0.2, + tol=0.02, + l1_reg=0.2, + max_iterations=30, + optimize_strategy="batch_gradient_descent", + learn_rate_strategy="constant", + learn_rate=0.2, ) model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n optimize_strategy="batch_gradient_descent",\n l2_reg=0.2,\n max_iterations=30,\n learn_rate_strategy="constant",\n min_rel_progress=0.02,\n calculate_p_values=False,\n enable_global_explain=False,\n l1_reg=0.2,\n learn_rate=0.2,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index 39012cbe088..7c7473e7135 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -63,10 +63,10 @@ class LinearRegression(RegressorMixin, LinearModel): the dataset, and the targets predicted by the linear approximation. Args: - optimize_strategy (str, default "normal_equation"): + optimize_strategy (str, default "auto_strategy"): The strategy to train linear regression models. Possible values are "auto_strategy", "batch_gradient_descent", "normal_equation". Default - to "normal_equation". + to "auto_strategy". fit_intercept (bool, default True): Default ``True``. Whether to calculate the intercept for this model. If set to False, no intercept will be used in calculations diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 621c78d551f..4cca3b136bf 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -24,6 +24,10 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): """Logistic Regression (aka logit, MaxEnt) classifier. Args: + optimize_strategy (str, default "auto_strategy"): + The strategy to train logistic regression models. Possible values are + "auto_strategy", "batch_gradient_descent", "normal_equation". Default + to "auto_strategy". fit_intercept (default True): Default True. Specifies if a constant (a.k.a. bias or intercept) should be added to the decision function. @@ -35,6 +39,26 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): frequencies in the input data as ``n_samples / (n_classes * np.bincount(y))``. Dict isn't supported now. + l1_reg (float or None, default None): + The amount of L1 regularization applied. Default to None. Can't be set in "normal_equation" mode. If unset, value 0 is used. + l2_reg (float, default 0.0): + The amount of L2 regularization applied. Default to 0. + max_iterations (int, default 20): + The maximum number of training iterations or steps. Default to 20. + warm_start (bool, default False): + Determines whether to train a model with new training data, new model options, or both. Unless you explicitly override them, the initial options used to train the model are used for the warm start run. Default to False. + learn_rate (float or None, default None): + The learn rate for gradient descent when learn_rate_strategy='constant'. If unset, value 0.1 is used. If learn_rate_strategy='line_search', an error is returned. + learn_rate_strategy (str, default "line_search"): + The strategy for specifying the learning rate during training. Default to "line_search". + tol (float, default 0.01): + The minimum relative loss improvement that is necessary to continue training when EARLY_STOP is set to true. For example, a value of 0.01 specifies that each iteration must reduce the loss by 1% for training to continue. Default to 0.01. + ls_init_learn_rate (float or None, default None): + Sets the initial learning rate that learn_rate_strategy='line_search' uses. This option can only be used if line_search is specified. If unset, value 0.1 is used. + calculate_p_values (bool, default False): + Specifies whether to compute p-values and standard errors during training. Default to False. + enable_global_explain (bool, default False): + Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. """ def fit( From 2ad1e4674b808bedf26c252077cd188c5416f9f7 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 03:38:15 -0400 Subject: [PATCH 405/921] chore(python): add requirements for docs build (#449) Source-Link: https://github.com/googleapis/synthtool/commit/85c23b6bc4352c1b0674848eaeb4e48645aeda6b Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:3741fd1f5f5150378563c76afa06bcc12777b5fe54c5ee01115218f83872134f Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 +-- .kokoro/docker/docs/Dockerfile | 4 +++ .kokoro/docker/docs/requirements.in | 1 + .kokoro/docker/docs/requirements.txt | 38 ++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 .kokoro/docker/docs/requirements.in create mode 100644 .kokoro/docker/docs/requirements.txt diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index e4e943e0259..5d9542b1cb2 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:98f3afd11308259de6e828e37376d18867fd321aba07826e29e4f8d9cab56bad -# created: 2024-02-27T15:56:18.442440378Z + digest: sha256:3741fd1f5f5150378563c76afa06bcc12777b5fe54c5ee01115218f83872134f +# created: 2024-03-15T16:26:15.743347415Z diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index 8e39a2cc438..bdaf39fe22d 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -80,4 +80,8 @@ RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ # Test pip RUN python3 -m pip +# Install build requirements +COPY requirements.txt /requirements.txt +RUN python3 -m pip install --require-hashes -r requirements.txt + CMD ["python3.8"] diff --git a/.kokoro/docker/docs/requirements.in b/.kokoro/docker/docs/requirements.in new file mode 100644 index 00000000000..816817c672a --- /dev/null +++ b/.kokoro/docker/docs/requirements.in @@ -0,0 +1 @@ +nox diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt new file mode 100644 index 00000000000..0e5d70f20f8 --- /dev/null +++ b/.kokoro/docker/docs/requirements.txt @@ -0,0 +1,38 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --allow-unsafe --generate-hashes requirements.in +# +argcomplete==3.2.3 \ + --hash=sha256:bf7900329262e481be5a15f56f19736b376df6f82ed27576fa893652c5de6c23 \ + --hash=sha256:c12355e0494c76a2a7b73e3a59b09024ca0ba1e279fb9ed6c1b82d5b74b6a70c + # via nox +colorlog==6.8.2 \ + --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ + --hash=sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33 + # via nox +distlib==0.3.8 \ + --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ + --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 + # via virtualenv +filelock==3.13.1 \ + --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ + --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c + # via virtualenv +nox==2024.3.2 \ + --hash=sha256:e53514173ac0b98dd47585096a55572fe504fecede58ced708979184d05440be \ + --hash=sha256:f521ae08a15adbf5e11f16cb34e8d0e6ea521e0b92868f684e91677deb974553 + # via -r requirements.in +packaging==24.0 \ + --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ + --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 + # via nox +platformdirs==4.2.0 \ + --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ + --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 + # via virtualenv +virtualenv==20.25.1 \ + --hash=sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a \ + --hash=sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197 + # via nox From df2976fa9fd0319b824128d0ccf2ebb20f381caa Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 03:54:53 -0400 Subject: [PATCH 406/921] feat: Add support for Python 3.12 (#231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(python): Add Python 3.12 Source-Link: https://github.com/googleapis/synthtool/commit/af16e6d4672cc7b400f144de2fc3068b54ff47d2 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:bacc3af03bff793a03add584537b36b5644342931ad989e3ba1171d3bd5399f5 * add python 3.12 to noxfile and setup.py * update system test to use python 3.12 * add constraints file for python 3.12 * add python 3.12 to owlbot.py * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * add python 3.12 as a required check * skip recursion test * avoid matplotlib error * 🦉 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 Co-authored-by: Anthonios Partheniou Co-authored-by: Tim Swast --- .github/sync-repo-settings.yaml | 1 + .github/workflows/unittest.yml | 2 +- CONTRIBUTING.rst | 10 ++++++---- bigframes/operations/_matplotlib/core.py | 7 +++++-- bigframes/pandas/__init__.py | 3 +++ noxfile.py | 4 ++-- owlbot.py | 4 ++-- setup.py | 1 + testing/constraints-3.12.txt | 0 tests/system/small/test_dataframe.py | 8 ++++++++ 10 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 testing/constraints-3.12.txt diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 80c73d991c5..b98d68799ad 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -15,6 +15,7 @@ branchProtectionRules: - 'unit (3.9)' - 'unit (3.10)' - 'unit (3.11)' + - 'unit (3.12)' - 'cover' - 'Kokoro presubmit' permissionRules: diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index f059b5548a1..132369f3edf 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12'] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5146b4bc7e0..8d68e4fc278 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,7 +22,7 @@ In order to add a feature: documentation. - The feature must work fully on the following CPython versions: - 3.9, 3.10 and 3.11 on both UNIX and Windows. + 3.9, 3.10, 3.11 and 3.12 on both UNIX and Windows. - The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, but new dependencies should @@ -72,7 +72,7 @@ We use `nox `__ to instrument our tests. - To run a single unit test:: - $ nox -s unit-3.11 -- -k + $ nox -s unit-3.12 -- -k .. note:: @@ -143,12 +143,12 @@ Running System Tests $ nox -s system # Run a single system test - $ nox -s system-3.11 -- -k + $ nox -s system-3.12 -- -k .. note:: - System tests are only configured to run under Python 3.9 and 3.11. + System tests are only configured to run under Python 3.9, 3.11 and 3.12. For expediency, we do not run them in older versions of Python 3. This alone will not run the tests. You'll need to change some local @@ -261,10 +261,12 @@ We support: - `Python 3.9`_ - `Python 3.10`_ - `Python 3.11`_ +- `Python 3.12`_ .. _Python 3.9: https://docs.python.org/3.9/ .. _Python 3.10: https://docs.python.org/3.10/ .. _Python 3.11: https://docs.python.org/3.11/ +.. _Python 3.12: https://docs.python.org/3.12/ Supported versions can be found in our ``noxfile.py`` `config`_. diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index b4beea75fd3..663e7a789f4 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -15,8 +15,6 @@ import abc import typing -import matplotlib.pyplot as plt - DEFAULT_SAMPLING_N = 1000 DEFAULT_SAMPLING_STATE = 0 @@ -27,6 +25,11 @@ def generate(self): pass def draw(self) -> None: + # This import can fail with "Matplotlib failed to acquire the + # following lock file" so import here to reduce the chance of + # our parallel test suite from triggering this. + import matplotlib.pyplot as plt + plt.draw_if_interactive() @property diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 77f1af9dd43..067c1b1ebe3 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -706,6 +706,9 @@ def to_datetime( # SQL Compilation uses recursive algorithms on deep trees # 10M tree depth should be sufficient to generate any sql that is under bigquery limit +# Note: This limit does not have the desired effect on Python 3.12 in +# which the applicable limit is now hard coded. See: +# https://github.com/python/cpython/issues/112282 sys.setrecursionlimit(max(10000000, sys.getrecursionlimit())) resource.setrlimit( resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) diff --git a/noxfile.py b/noxfile.py index 1d8ab6c1fd6..506e935c7d3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -39,7 +39,7 @@ DEFAULT_PYTHON_VERSION = "3.10" -UNIT_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] +UNIT_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12"] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", "asyncmock", @@ -54,7 +54,7 @@ UNIT_TEST_EXTRAS: List[str] = [] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} -SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.11"] +SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.12"] SYSTEM_TEST_STANDARD_DEPENDENCIES = [ "jinja2", "mock", diff --git a/owlbot.py b/owlbot.py index 77479401d50..f804859689a 100644 --- a/owlbot.py +++ b/owlbot.py @@ -30,8 +30,8 @@ # Add templated files # ---------------------------------------------------------------------------- templated_files = common.py_library( - unit_test_python_versions=["3.9", "3.10", "3.11"], - system_test_python_versions=["3.9", "3.11"], + unit_test_python_versions=["3.9", "3.10", "3.11", "3.12"], + system_test_python_versions=["3.9", "3.11", "3.12"], cov_level=35, intersphinx_dependencies={ "pandas": "https://pandas.pydata.org/pandas-docs/stable/", diff --git a/setup.py b/setup.py index 5258a7d6f93..3b2dc53bd64 100644 --- a/setup.py +++ b/setup.py @@ -113,6 +113,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Operating System :: OS Independent", "Topic :: Internet", ], diff --git a/testing/constraints-3.12.txt b/testing/constraints-3.12.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 3b6cd8c05fd..5e034e8e836 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -14,6 +14,7 @@ import io import operator +import sys import tempfile import typing from typing import Tuple @@ -4003,6 +4004,13 @@ def test_df_dot_operator_series( ) +# TODO(tswast): We may be able to re-enable this test after we break large +# queries up in https://github.com/googleapis/python-bigquery-dataframes/pull/427 +@pytest.mark.skipif( + sys.version_info >= (3, 12), + # See: https://github.com/python/cpython/issues/112282 + reason="setrecursionlimit has no effect on the Python C stack since Python 3.12.", +) def test_recursion_limit(scalars_df_index): scalars_df_index = scalars_df_index[["int64_too", "int64_col", "float64_col"]] for i in range(400): From ad0e99eddb1dddd3d439cea7db1e4f222b45c6b9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 22 Mar 2024 11:08:50 -0700 Subject: [PATCH 407/921] feat: allow assigning directly to Series.name property (#495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Sweña (Swast) --- bigframes/series.py | 5 +++++ tests/system/small/test_series.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/bigframes/series.py b/bigframes/series.py index f9aa59d6ecb..e7b358c2feb 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -109,6 +109,11 @@ def at(self) -> bigframes.core.indexers.AtSeriesIndexer: def name(self) -> blocks.Label: return self._name + @name.setter + def name(self, label: blocks.Label): + new_block = self._block.with_column_labels([label]) + self._set_block(new_block) + @property def shape(self) -> typing.Tuple[int]: return (self._block.shape[0],) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index e35d2f33774..010ad323e78 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1667,6 +1667,21 @@ def test_empty_true_memtable(session: bigframes.Session): assert bf_result == pd_result +def test_series_names(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"].copy() + bf_result.index.name = "new index name" + bf_result.name = "new series name" + + pd_result = scalars_pandas_df["string_col"].copy() + pd_result.index.name = "new index name" + pd_result.name = "new series name" + + assert pd_result.name == bf_result.name + assert pd_result.index.name == bf_result.index.name + + def test_dtype(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs From 85cede22587a9fe1dae888721492f9390dc46d70 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:04:16 -0700 Subject: [PATCH 408/921] feat: add configuration option to read_gbq (#401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes https://togithub.com/googleapis/python-bigquery-dataframes/issues/384 🦕 --- bigframes/pandas/__init__.py | 8 +- bigframes/session/__init__.py | 95 ++++++++++++++++--- bigframes/session/_io/bigquery.py | 3 +- tests/system/small/test_session.py | 42 ++++++++ .../bigframes_vendored/pandas/io/gbq.py | 16 +++- 5 files changed, 145 insertions(+), 19 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 067c1b1ebe3..b6476c5eb88 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -492,9 +492,10 @@ def read_gbq( *, index_col: Iterable[str] | str = (), columns: Iterable[str] = (), + configuration: Optional[Dict] = None, max_results: Optional[int] = None, filters: vendored_pandas_gbq.FiltersType = (), - use_cache: bool = True, + use_cache: Optional[bool] = None, col_order: Iterable[str] = (), ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query_or_table) @@ -503,6 +504,7 @@ def read_gbq( query_or_table, index_col=index_col, columns=columns, + configuration=configuration, max_results=max_results, filters=filters, use_cache=use_cache, @@ -528,8 +530,9 @@ def read_gbq_query( *, index_col: Iterable[str] | str = (), columns: Iterable[str] = (), + configuration: Optional[Dict] = None, max_results: Optional[int] = None, - use_cache: bool = True, + use_cache: Optional[bool] = None, col_order: Iterable[str] = (), ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) @@ -538,6 +541,7 @@ def read_gbq_query( query, index_col=index_col, columns=columns, + configuration=configuration, max_results=max_results, use_cache=use_cache, col_order=col_order, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index b826d429230..14df7edeb2e 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,6 +16,7 @@ from __future__ import annotations +import copy import datetime import itertools import logging @@ -283,9 +284,10 @@ def read_gbq( *, index_col: Iterable[str] | str = (), columns: Iterable[str] = (), + configuration: Optional[Dict] = None, max_results: Optional[int] = None, filters: third_party_pandas_gbq.FiltersType = (), - use_cache: bool = True, + use_cache: Optional[bool] = None, col_order: Iterable[str] = (), # Add a verify index argument that fails if the index is not unique. ) -> dataframe.DataFrame: @@ -306,6 +308,7 @@ def read_gbq( query_or_table, index_col=index_col, columns=columns, + configuration=configuration, max_results=max_results, api_name="read_gbq", use_cache=use_cache, @@ -314,13 +317,20 @@ def read_gbq( # TODO(swast): Query the snapshot table but mark it as a # deterministic query so we can avoid serializing if we have a # unique index. + if configuration is not None: + raise ValueError( + "The 'configuration' argument is not allowed when " + "directly reading from a table. Please remove " + "'configuration' or use a query." + ) + return self._read_gbq_table( query_or_table, index_col=index_col, columns=columns, max_results=max_results, api_name="read_gbq", - use_cache=use_cache, + use_cache=use_cache if use_cache is not None else True, ) def _to_query( @@ -405,7 +415,7 @@ def _query_to_destination( query: str, index_cols: List[str], api_name: str, - use_cache: bool = True, + configuration: dict = {"query": {"useQueryCache": True}}, ) -> Tuple[Optional[bigquery.TableReference], Optional[bigquery.QueryJob]]: # If a dry_run indicates this is not a query type job, then don't # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. @@ -427,23 +437,35 @@ def _query_to_destination( ][:_MAX_CLUSTER_COLUMNS] temp_table = self._create_empty_temp_table(schema, cluster_cols) - job_config = bigquery.QueryJobConfig() + timeout_ms = configuration.get("jobTimeoutMs") or configuration["query"].get( + "timeoutMs" + ) + + # Convert timeout_ms to seconds, ensuring a minimum of 0.1 seconds to avoid + # the program getting stuck on too-short timeouts. + timeout = max(int(timeout_ms) * 1e-3, 0.1) if timeout_ms else None + + job_config = typing.cast( + bigquery.QueryJobConfig, + bigquery.QueryJobConfig.from_api_repr(configuration), + ) job_config.labels["bigframes-api"] = api_name job_config.destination = temp_table - job_config.use_query_cache = use_cache try: # Write to temp table to workaround BigQuery 10 GB query results # limit. See: internal issue 303057336. job_config.labels["error_caught"] = "true" - _, query_job = self._start_query(query, job_config=job_config) + _, query_job = self._start_query( + query, job_config=job_config, timeout=timeout + ) return query_job.destination, query_job except google.api_core.exceptions.BadRequest: # Some SELECT statements still aren't compatible with cluster # tables as the destination. For example, if the query has a # top-level ORDER BY, this conflicts with our ability to cluster # the table by the index column(s). - _, query_job = self._start_query(query) + _, query_job = self._start_query(query, timeout=timeout) return query_job.destination, query_job def read_gbq_query( @@ -452,8 +474,9 @@ def read_gbq_query( *, index_col: Iterable[str] | str = (), columns: Iterable[str] = (), + configuration: Optional[Dict] = None, max_results: Optional[int] = None, - use_cache: bool = True, + use_cache: Optional[bool] = None, col_order: Iterable[str] = (), ) -> dataframe.DataFrame: """Turn a SQL query into a DataFrame. @@ -517,6 +540,7 @@ def read_gbq_query( query=query, index_col=index_col, columns=columns, + configuration=configuration, max_results=max_results, api_name="read_gbq_query", use_cache=use_cache, @@ -528,10 +552,34 @@ def _read_gbq_query( *, index_col: Iterable[str] | str = (), columns: Iterable[str] = (), + configuration: Optional[Dict] = None, max_results: Optional[int] = None, api_name: str = "read_gbq_query", - use_cache: bool = True, + use_cache: Optional[bool] = None, ) -> dataframe.DataFrame: + configuration = _transform_read_gbq_configuration(configuration) + + if "query" not in configuration: + configuration["query"] = {} + + if "query" in configuration["query"]: + raise ValueError( + "The query statement must not be included in the ", + "'configuration' because it is already provided as", + " a separate parameter.", + ) + + if "useQueryCache" in configuration["query"]: + if use_cache is not None: + raise ValueError( + "'useQueryCache' in 'configuration' conflicts with" + " 'use_cache' parameter. Please specify only one." + ) + else: + configuration["query"]["useQueryCache"] = ( + True if use_cache is None else use_cache + ) + if isinstance(index_col, str): index_cols = [index_col] else: @@ -541,7 +589,7 @@ def _read_gbq_query( query, index_cols, api_name=api_name, - use_cache=use_cache, + configuration=configuration, ) # If there was no destination table, that means the query must have @@ -565,7 +613,7 @@ def _read_gbq_query( index_col=index_cols, columns=columns, max_results=max_results, - use_cache=use_cache, + use_cache=configuration["query"]["useQueryCache"], ) def read_gbq_table( @@ -1656,13 +1704,14 @@ def _start_query( sql: str, job_config: Optional[bigquery.job.QueryJobConfig] = None, max_results: Optional[int] = None, + timeout: Optional[float] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ Starts BigQuery query job and waits for results. """ job_config = self._prepare_query_job_config(job_config) return bigframes.session._io.bigquery.start_query_with_client( - self.bqclient, sql, job_config, max_results + self.bqclient, sql, job_config, max_results, timeout ) def _start_query_ml_ddl( @@ -1876,3 +1925,25 @@ def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringVa # Escape backslashes and use backslash as delineator escaped = typing.cast(ibis_types.StringColumn, result.fillna("")).replace("\\", "\\\\") # type: ignore return typing.cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) + + +def _transform_read_gbq_configuration(configuration: Optional[dict]) -> dict: + """ + For backwards-compatibility, convert any previously client-side only + parameters such as timeoutMs to the property name expected by the REST API. + + Makes a copy of configuration if changes are needed. + """ + + if configuration is None: + return {} + + timeout_ms = configuration.get("query", {}).get("timeoutMs") + if timeout_ms is not None: + # Transform timeoutMs to an actual server-side configuration. + # https://github.com/googleapis/python-bigquery-pandas/issues/479 + configuration = copy.deepcopy(configuration) + del configuration["query"]["timeoutMs"] + configuration["jobTimeoutMs"] = timeout_ms + + return configuration diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index 67820bbbcb5..38ff7429ec0 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -220,6 +220,7 @@ def start_query_with_client( sql: str, job_config: bigquery.job.QueryJobConfig, max_results: Optional[int] = None, + timeout: Optional[float] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ Starts query job and waits for results. @@ -230,7 +231,7 @@ def start_query_with_client( ) try: - query_job = bq_client.query(sql, job_config=job_config) + query_job = bq_client.query(sql, job_config=job_config, timeout=timeout) except google.api_core.exceptions.Forbidden as ex: if "Drive credentials" in ex.message: ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index c6702aa032b..d0c20f3839b 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -20,6 +20,7 @@ import typing from typing import List +import google import google.cloud.bigquery as bigquery import numpy as np import pandas as pd @@ -363,6 +364,47 @@ def test_read_gbq_table_wildcard_with_filter(session: bigframes.Session): assert df.shape == (348485, 32) +@pytest.mark.parametrize( + ("config"), + [ + { + "query": { + "useQueryCache": True, + "maximumBytesBilled": "1000000000", + "timeoutMs": 10000, + } + }, + pytest.param( + {"query": {"useQueryCache": True, "timeoutMs": 50}}, + marks=pytest.mark.xfail( + raises=google.api_core.exceptions.BadRequest, + reason="Expected failure due to timeout being set too short.", + ), + ), + pytest.param( + {"query": {"useQueryCache": False, "maximumBytesBilled": "100"}}, + marks=pytest.mark.xfail( + raises=google.api_core.exceptions.InternalServerError, + reason="Expected failure when the query exceeds the maximum bytes billed limit.", + ), + ), + ], +) +def test_read_gbq_with_configuration( + session: bigframes.Session, scalars_table_id: str, config: dict +): + query = f"""SELECT + t.float64_col * 2 AS my_floats, + CONCAT(t.string_col, "_2") AS my_strings, + t.int64_col > 0 AS my_bools, + FROM `{scalars_table_id}` AS t + """ + + df = session.read_gbq(query, configuration=config) + + assert df.shape == (9, 3) + + 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/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 74602b5af15..b5feeb13c5c 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Any, Iterable, Literal, Optional, Tuple, Union +from typing import Any, Dict, Iterable, Literal, Optional, Tuple, Union from bigframes import constants @@ -19,9 +19,10 @@ def read_gbq( *, index_col: Iterable[str] | str = (), columns: Iterable[str] = (), + configuration: Optional[Dict] = None, max_results: Optional[int] = None, filters: FiltersType = (), - use_cache: bool = True, + use_cache: Optional[bool] = None, col_order: Iterable[str] = (), ): """Loads a DataFrame from BigQuery. @@ -107,6 +108,11 @@ def read_gbq( columns (Iterable[str]): List of BigQuery column names in the desired order for results DataFrame. + configuration (dict, optional): + Query config parameters for job processing. + For example: configuration = {'query': {'useQueryCache': False}}. + For more information see `BigQuery REST API Reference + `__. max_results (Optional[int], default None): If set, limit the maximum number of rows to fetch from the query results. @@ -121,8 +127,10 @@ def read_gbq( If using wildcard table suffix in query_or_table, can specify '_table_suffix' pseudo column to filter the tables to be read into the DataFrame. - use_cache (bool, default True): - Whether to cache the query inputs. Default to True. + use_cache (Optional[bool], default None): + Caches query results if set to `True`. When `None`, it behaves + as `True`, but should not be combined with `useQueryCache` in + `configuration` to avoid conflicts. col_order (Iterable[str]): Alias for columns, retained for backwards compatibility. From 751266e056ac566ef5b6e40fbbca84ed95e7a7a9 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 22 Mar 2024 19:16:17 +0000 Subject: [PATCH 409/921] docs: improve API documentation (#489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a bunch of miscellaneous doc rendering issues I came across. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../pandas/core/arrays/datetimelike.py | 2 +- .../bigframes_vendored/pandas/core/frame.py | 33 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py index f706ae25603..bd5f055ece0 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py @@ -33,7 +33,7 @@ def strftime(self, date_format: str): Date format string (e.g. "%Y-%m-%d"). Returns: - bigframes.series.Series of formatted strings. + bigframes.series.Series: Series of formatted strings. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 4eceb8a2f18..bdc220868ab 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1141,7 +1141,7 @@ def align( Args: other (DataFrame or Series): - join ({{'outer', 'inner', 'left', 'right'}}, default 'outer'): + join ({'outer', 'inner', 'left', 'right'}, default 'outer'): Type of alignment to be performed. left: use only keys from left frame, preserve key order. right: use only keys from right frame, preserve key order. @@ -1627,9 +1627,6 @@ def keys(self): This is index for Series, columns for DataFrame. - Returns: - Index: Info axis. - **Examples:** >>> import bigframes.pandas as bpd @@ -1641,6 +1638,9 @@ def keys(self): ... }) >>> df.keys() Index(['A', 'B'], dtype='object') + + Returns: + Index: Info axis. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -1673,6 +1673,17 @@ def itertuples(self, index: bool = True, name: str | None = "Pandas"): """ Iterate over DataFrame rows as namedtuples. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + >>> next(df.itertuples(name="Pair")) + Pair(Index=0, A=1, B=4) + Args: index (bool, default True): If True, return the index as the first element of the tuple. @@ -1685,18 +1696,6 @@ def itertuples(self, index: bool = True, name: str | None = "Pandas"): An object to iterate over namedtuples for each row in the DataFrame with the first field possibly being the index and following fields being the column values. - - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> bpd.options.display.progress_bar = None - >>> df = bpd.DataFrame({ - ... 'A': [1, 2, 3], - ... 'B': [4, 5, 6], - ... }) - >>> next(df.itertuples(name="Pair")) - Pair(Index=0, A=1, B=4) """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -3155,7 +3154,7 @@ def join(self, other, *, on: Optional[str] = None, how: str) -> DataFrame: on: Column in the caller to join on the index in other, otherwise joins index-on-index. Like an Excel VLOOKUP operation. - how ({'left', 'right', 'outer', 'inner'}, default 'left'`): + how ({'left', 'right', 'outer', 'inner'}, default 'left'): How to handle the operation of the two objects. ``left``: use calling frame's index (or column if on is specified) ``right``: use `other`'s index. ``outer``: form union of calling From d51fa84003f0bd56739a9fd051bdec2e5f58cbb6 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:56:33 -0700 Subject: [PATCH 410/921] chore(python): update dependencies in /.kokoro (#448) Source-Link: https://github.com/googleapis/synthtool/commit/db94845da69ccdfefd7ce55c84e6cfa74829747e Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:a8a80fc6456e433df53fc2a0d72ca0345db0ddefb409f1b75b118dfd1babd952 Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 +- .kokoro/requirements.in | 3 +- .kokoro/requirements.txt | 114 +++++++++++++++++--------------------- 3 files changed, 56 insertions(+), 65 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 5d9542b1cb2..4bdeef3904e 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:3741fd1f5f5150378563c76afa06bcc12777b5fe54c5ee01115218f83872134f -# created: 2024-03-15T16:26:15.743347415Z + digest: sha256:a8a80fc6456e433df53fc2a0d72ca0345db0ddefb409f1b75b118dfd1babd952 +# created: 2024-03-15T16:25:47.905264637Z diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in index ec867d9fd65..fff4d9ce0d0 100644 --- a/.kokoro/requirements.in +++ b/.kokoro/requirements.in @@ -1,5 +1,5 @@ gcp-docuploader -gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x +gcp-releasetool>=2 # required for compatibility with cryptography>=42.x importlib-metadata typing-extensions twine @@ -8,3 +8,4 @@ setuptools nox>=2022.11.21 # required to remove dependency on py charset-normalizer<3 click<8.1.0 +cryptography>=42.0.5 diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index bda8e38c4f3..dd61f5f3201 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -93,40 +93,41 @@ colorlog==6.7.0 \ # via # gcp-docuploader # nox -cryptography==42.0.4 \ - --hash=sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b \ - --hash=sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce \ - --hash=sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88 \ - --hash=sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7 \ - --hash=sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20 \ - --hash=sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9 \ - --hash=sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff \ - --hash=sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1 \ - --hash=sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764 \ - --hash=sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b \ - --hash=sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298 \ - --hash=sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1 \ - --hash=sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824 \ - --hash=sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257 \ - --hash=sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a \ - --hash=sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129 \ - --hash=sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb \ - --hash=sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929 \ - --hash=sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854 \ - --hash=sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52 \ - --hash=sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923 \ - --hash=sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885 \ - --hash=sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0 \ - --hash=sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd \ - --hash=sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2 \ - --hash=sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18 \ - --hash=sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b \ - --hash=sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992 \ - --hash=sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74 \ - --hash=sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660 \ - --hash=sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925 \ - --hash=sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449 +cryptography==42.0.5 \ + --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ + --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ + --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ + --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ + --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ + --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ + --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ + --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ + --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ + --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ + --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ + --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ + --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ + --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ + --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ + --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ + --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ + --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ + --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ + --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ + --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ + --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ + --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ + --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ + --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ + --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ + --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ + --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ + --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ + --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ + --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ + --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 # via + # -r requirements.in # gcp-releasetool # secretstorage distlib==0.3.7 \ @@ -145,9 +146,9 @@ gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==1.16.0 \ - --hash=sha256:27bf19d2e87aaa884096ff941aa3c592c482be3d6a2bfe6f06afafa6af2353e3 \ - --hash=sha256:a316b197a543fd036209d0caba7a8eb4d236d8e65381c80cbc6d7efaa7606d63 +gcp-releasetool==2.0.0 \ + --hash=sha256:3d73480b50ba243f22d7c7ec08b115a30e1c7817c4899781840c26f9c55b8277 \ + --hash=sha256:7aa9fd935ec61e581eb8458ad00823786d91756c25e492f372b2b30962f3c28f # via -r requirements.in google-api-core==2.12.0 \ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \ @@ -392,29 +393,18 @@ platformdirs==3.11.0 \ --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e # via virtualenv -protobuf==3.20.3 \ - --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ - --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \ - --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \ - --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \ - --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \ - --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \ - --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \ - --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \ - --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \ - --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \ - --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \ - --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \ - --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \ - --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \ - --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \ - --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \ - --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \ - --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \ - --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \ - --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \ - --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \ - --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee +protobuf==4.25.3 \ + --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ + --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ + --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ + --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ + --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ + --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ + --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ + --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ + --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ + --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ + --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 # via # gcp-docuploader # gcp-releasetool @@ -518,7 +508,7 @@ zipp==3.17.0 \ # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==68.2.2 \ - --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \ - --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a +setuptools==69.2.0 \ + --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ + --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c # via -r requirements.in From 10c044686228e5c6f3868c1eb10454f6a086ac8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 22 Mar 2024 15:00:28 -0500 Subject: [PATCH 411/921] feat: ensure `Series.str.len()` can get length of array columns (#497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/conftest.py | 2 -- tests/system/small/operations/test_strings.py | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index e6b241c9a3e..6d8e9abe5b6 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -357,8 +357,6 @@ def nested_pandas_df() -> pd.DataFrame: DATA_DIR / "nested.jsonl", lines=True, ) - tests.system.utils.convert_pandas_dtypes(df, bytes_col=True) - df = df.set_index("rowindex") return df diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 79f92c94b43..9654c77ec44 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -181,6 +181,26 @@ def test_len(scalars_dfs): ) +def test_len_with_array_column(nested_df, nested_pandas_df): + """ + Series.str.len() is expected to work on columns containing lists as well as strings. + + See: https://stackoverflow.com/a/41340543/101923 + """ + col_name = "event_sequence" + bf_series: bigframes.series.Series = nested_df[col_name] + bf_result = bf_series.str.len().to_pandas() + pd_result = nested_pandas_df[col_name].str.len() + + # One of dtype mismatches to be documented. Here, the `bf_result.dtype` is `Int64` but + # the `pd_result.dtype` is `float64`: https://github.com/pandas-dev/pandas/issues/51948 + assert_series_equal( + pd_result.astype(pd.Int64Dtype()), + bf_result, + check_index_type=False, + ) + + def test_lower(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" From e2cf50e053f7163d1654c4b5621cc93e922d5148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 22 Mar 2024 17:00:37 -0500 Subject: [PATCH 412/921] deps: support pandas 2.2 (#492) * deps: support pandas 2.2 * fix tests and loading multiindex * fix doctests * avoid circular imports * missing import * allow pandas in prerelease * avoid table.execute in ibis tests * update notebook to use native plotting --- bigframes/core/joins/merge.py | 28 +++++++++----- bigframes/core/utils.py | 13 +++++-- bigframes/dataframe.py | 2 +- bigframes/session/__init__.py | 30 ++++++++++++--- bigframes/session/_io/bigquery.py | 3 +- .../bq_dataframes_covid_line_graphs.ipynb | 37 +++++++------------ noxfile.py | 15 +------- setup.py | 2 +- tests/system/conftest.py | 1 + tests/system/large/test_remote_function.py | 22 +++++++---- tests/system/small/test_dataframe.py | 4 +- tests/system/small/test_groupby.py | 4 +- tests/system/small/test_multiindex.py | 3 +- tests/system/small/test_series.py | 25 ++++++++++--- tests/unit/test_pandas.py | 7 +++- .../bigframes_vendored/pandas/core/frame.py | 10 ++--- .../bigframes_vendored/pandas/core/generic.py | 2 +- .../pandas/core/indexing.py | 2 +- .../bigframes_vendored/pandas/core/series.py | 4 +- 19 files changed, 130 insertions(+), 84 deletions(-) diff --git a/bigframes/core/joins/merge.py b/bigframes/core/joins/merge.py index c65e1bdd54d..1542cda0afa 100644 --- a/bigframes/core/joins/merge.py +++ b/bigframes/core/joins/merge.py @@ -18,15 +18,18 @@ from __future__ import annotations +import typing from typing import Literal, Optional -from bigframes.dataframe import DataFrame -from bigframes.series import Series +# Avoid cirular imports. +if typing.TYPE_CHECKING: + import bigframes.dataframe + import bigframes.series def merge( - left: DataFrame, - right: DataFrame, + left: bigframes.dataframe.DataFrame, + right: bigframes.dataframe.DataFrame, how: Literal[ "inner", "left", @@ -40,7 +43,7 @@ def merge( right_on: Optional[str] = None, sort: bool = False, suffixes: tuple[str, str] = ("_x", "_y"), -) -> DataFrame: +) -> bigframes.dataframe.DataFrame: left = _validate_operand(left) right = _validate_operand(right) @@ -55,14 +58,19 @@ def merge( ) -def _validate_operand(obj: DataFrame | Series) -> DataFrame: - if isinstance(obj, DataFrame): +def _validate_operand( + obj: bigframes.dataframe.DataFrame | bigframes.series.Series, +) -> bigframes.dataframe.DataFrame: + import bigframes.dataframe + import bigframes.series + + if isinstance(obj, bigframes.dataframe.DataFrame): return obj - elif isinstance(obj, Series): + elif isinstance(obj, bigframes.series.Series): if obj.name is None: - raise ValueError("Cannot merge a Series without a name") + raise ValueError("Cannot merge a bigframes.series.Series without a name") return obj.to_frame() else: raise TypeError( - f"Can only merge Series or DataFrame objects, a {type(obj)} was passed" + f"Can only merge bigframes.series.Series or bigframes.dataframe.DataFrame objects, a {type(obj)} was passed" ) diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 1976ec1e393..97c5ef03e58 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -70,7 +70,9 @@ def split_index( def get_standardized_ids( - col_labels: Iterable[Hashable], idx_labels: Iterable[Hashable] = () + col_labels: Iterable[Hashable], + idx_labels: Iterable[Hashable] = (), + strict: bool = False, ) -> tuple[list[str], list[str]]: """Get stardardized column ids as column_ids_list, index_ids_list. The standardized_column_id must be valid BQ SQL schema column names, can only be string type and unique. @@ -84,11 +86,15 @@ def get_standardized_ids( Tuple of (standardized_column_ids, standardized_index_ids) """ col_ids = [ - UNNAMED_COLUMN_ID if col_label is None else label_to_identifier(col_label) + UNNAMED_COLUMN_ID + if col_label is None + else label_to_identifier(col_label, strict=strict) for col_label in col_labels ] idx_ids = [ - UNNAMED_INDEX_ID if idx_label is None else label_to_identifier(idx_label) + UNNAMED_INDEX_ID + if idx_label is None + else label_to_identifier(idx_label, strict=strict) for idx_label in idx_labels ] @@ -107,6 +113,7 @@ def label_to_identifier(label: typing.Hashable, strict: bool = False) -> str: # Column values will be loaded as null if the column name has spaces. # https://github.com/googleapis/python-bigquery/issues/1566 identifier = str(label).replace(" ", "_") + if strict: identifier = re.sub(r"[^a-zA-Z0-9_]", "", identifier) if not identifier: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 6a073ba9f26..051796535b1 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -70,10 +70,10 @@ if typing.TYPE_CHECKING: import bigframes.session + SingleItemValue = Union[bigframes.series.Series, int, float, Callable] LevelType = typing.Hashable LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]] -SingleItemValue = Union[bigframes.series.Series, int, float, Callable] ERROR_IO_ONLY_GS_PATHS = f"Only Google Cloud Storage (gs://...) paths are supported. {constants.FEEDBACK_LINK}" ERROR_IO_REQUIRES_WILDCARD = ( diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 14df7edeb2e..74a8325dac4 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -84,7 +84,6 @@ import bigframes.core.ordering as order import bigframes.core.traversal as traversals import bigframes.core.utils as utils -import bigframes.dataframe as dataframe import bigframes.dtypes import bigframes.formatting_helpers as formatting_helpers from bigframes.functions.remote_function import read_gbq_function as bigframes_rgf @@ -93,6 +92,10 @@ import bigframes.session.clients import bigframes.version +# Avoid circular imports. +if typing.TYPE_CHECKING: + import bigframes.dataframe as dataframe + _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" _MAX_CLUSTER_COLUMNS = 4 @@ -557,6 +560,8 @@ def _read_gbq_query( api_name: str = "read_gbq_query", use_cache: Optional[bool] = None, ) -> dataframe.DataFrame: + import bigframes.dataframe as dataframe + configuration = _transform_read_gbq_configuration(configuration) if "query" not in configuration: @@ -754,6 +759,8 @@ def _read_gbq_table( api_name: str, use_cache: bool = True, ) -> dataframe.DataFrame: + import bigframes.dataframe as dataframe + if max_results and max_results <= 0: raise ValueError("`max_results` should be a positive number.") @@ -989,6 +996,8 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame def _read_pandas( self, pandas_dataframe: pandas.DataFrame, api_name: str ) -> dataframe.DataFrame: + import bigframes.dataframe as dataframe + if isinstance(pandas_dataframe, dataframe.DataFrame): raise ValueError( "read_pandas() expects a pandas.DataFrame, but got a " @@ -1003,6 +1012,8 @@ def _read_pandas( def _read_pandas_inline( self, pandas_dataframe: pandas.DataFrame ) -> Optional[dataframe.DataFrame]: + import bigframes.dataframe as dataframe + if pandas_dataframe.size > MAX_INLINE_DF_SIZE: return None @@ -1024,11 +1035,20 @@ def _read_pandas_inline( def _read_pandas_load_job( self, pandas_dataframe: pandas.DataFrame, api_name: str ) -> dataframe.DataFrame: + import bigframes.dataframe as dataframe + + col_index = pandas_dataframe.columns.copy() col_labels, idx_labels = ( - pandas_dataframe.columns.to_list(), + col_index.to_list(), pandas_dataframe.index.names, ) - new_col_ids, new_idx_ids = utils.get_standardized_ids(col_labels, idx_labels) + new_col_ids, new_idx_ids = utils.get_standardized_ids( + col_labels, + idx_labels, + # Loading parquet files into BigQuery with special column names + # is only supported under an allowlist. + strict=True, + ) # Add order column to pandas DataFrame to preserve order in BigQuery ordering_col = "rowid" @@ -1047,7 +1067,7 @@ def _read_pandas_load_job( # Specify the datetime dtypes, which is auto-detected as timestamp types. schema: list[bigquery.SchemaField] = [] - for column, dtype in zip(pandas_dataframe.columns, pandas_dataframe.dtypes): + for column, dtype in zip(new_col_ids, pandas_dataframe.dtypes): if dtype == "timestamp[us][pyarrow]": schema.append( bigquery.SchemaField(column, bigquery.enums.SqlTypeNames.DATETIME) @@ -1101,7 +1121,7 @@ def _read_pandas_load_job( block = blocks.Block( array_value, index_columns=new_idx_ids, - column_labels=col_labels, + column_labels=col_index, index_labels=idx_labels, ) return dataframe.DataFrame(block) diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index 38ff7429ec0..75283a060aa 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -29,7 +29,6 @@ import bigframes from bigframes.core import log_adapter import bigframes.formatting_helpers as formatting_helpers -import bigframes.session._io.bigquery as bigframes_io IO_ORDERING_ID = "bqdf_row_nums" MAX_LABELS_COUNT = 64 @@ -226,7 +225,7 @@ def start_query_with_client( Starts query job and waits for results. """ api_methods = log_adapter.get_and_reset_api_methods() - job_config.labels = bigframes_io.create_job_configs_labels( + job_config.labels = create_job_configs_labels( job_configs_labels=job_config.labels, api_methods=api_methods ) diff --git a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb index 87b8f9c0b60..40efe9d18c4 100644 --- a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb +++ b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb @@ -325,7 +325,7 @@ "id": "8GvJAgnH5Nzi" }, "source": [ - "BigQuery DataFrames implements some of the interface required by matplotlib. This means we can pass our DataFrame right into `pyplot.plt` and using the default settings, matplotlib will draw a simple line graph for us." + "BigQuery DataFrames implements some plotting methods with the matplotlib backend. Use `DataFrame.plot.line()` to draw a simple line graph." ] }, { @@ -338,7 +338,7 @@ { "data": { "text/html": [ - "Query job 38e28079-9a84-4c28-a04c-cdc0afbb74b1 is DONE. 273.1 MB processed. Open Job" + "Query job 307ec006-490f-435d-b3e3-74eb1d73fe0f is DONE. 372.9 MB processed. Open Job" ], "text/plain": [ "" @@ -349,19 +349,17 @@ }, { "data": { - "text/html": [ - "Query job b1df794f-6d3f-4f05-8bcd-2da29f4eb402 is DONE. 372.9 MB processed. Open Job" - ], "text/plain": [ - "" + "" ] }, + "execution_count": 9, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHRCAYAAACW3ZisAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB0BklEQVR4nO3deVyU1f4H8M/MAAOyCSqggPuuiLiDmpp71tVr2Z51S+8trVxatbJfy402U9u03SzNsptaVi6pWCq5L4hLuYKyiCCL7Myc3x/I8MwwM8zADM/Mw+f9evFyeOaZmXME5vnO93zPOSohhAARERGRQqjlbgARERGRIzG4ISIiIkVhcENERESKwuCGiIiIFIXBDRERESkKgxsiIiJSFAY3REREpCgMboiIiEhRGNwQERGRojC4ISIiIkVp1MHN77//jltuuQWtWrWCSqXCunXr7H4OIQTefvttdO7cGVqtFuHh4fjvf//r+MYSERGRTTzkboCcCgsLER0djQcffBCTJ0+u03PMmjULmzdvxttvv42oqCjk5OQgJyfHwS0lIiIiW6m4cWYllUqFtWvXYtKkSYZjpaWleO655/DNN98gNzcXPXv2xBtvvIHhw4cDAE6cOIFevXrh2LFj6NKlizwNJyIiIiONeliqNo8++igSExOxevVqHD16FFOmTMG4cePw999/AwB++ukntG/fHhs2bEC7du3Qtm1bTJs2jZkbIiIiGTG4sSAlJQVffPEF1qxZg6FDh6JDhw548sknMWTIEHzxxRcAgLNnz+LChQtYs2YNVqxYgeXLl+PAgQO47bbbZG49ERFR49Woa26sSUpKgk6nQ+fOnY2Ol5aWolmzZgAAvV6P0tJSrFixwnDeZ599hr59++LUqVMcqiIiIpIBgxsLrl27Bo1GgwMHDkCj0Rjd5+fnBwBo2bIlPDw8jAKgbt26AajM/DC4ISIiangMbiyIiYmBTqfD5cuXMXToULPnDB48GBUVFThz5gw6dOgAAPjrr78AAG3atGmwthIREVG1Rj1b6tq1azh9+jSAymDmnXfewYgRIxAcHIzWrVvj3nvvxa5du7Bw4ULExMQgKysLW7duRa9evTBhwgTo9Xr0798ffn5+WLx4MfR6PWbOnImAgABs3rxZ5t4RERE1To06uElISMCIESNqHL///vuxfPlylJeX49VXX8WKFStw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T09PxMTE4PTp02bvnzdvHvLy8gxfqampjmgyERHVExM35CyyZm6EEHjsscewdu1aJCQkoF27dnY/h06nQ1JSEm666Saz92u1Wmi12vo2lYiIHMze2VIsKCZbyRrczJw5E6tWrcL69evh7++PjIwMAEBgYCB8fHwAAFOnTkV4eDji4+MBAC+//DIGDRqEjh07Ijc3F2+99RYuXLiAadOmydYPIiKyH2tuyFlkDW6WLl0KABg+fLjR8S+++AIPPPAAACAlJQVqdfXo2dWrVzF9+nRkZGQgKCgIffv2xe7du9G9e/eGajYRETmAsDMVw1iIbCX7sFRtEhISjL5ftGgRFi1a5KQWERGRq+KwFNnKIQXFubm5jngaIiJqRBiskLPYHdy88cYb+Pbbbw3f33777WjWrBnCw8Nx5MgRhzaOiIiUy97YhrEQ2cru4GbZsmWIjIwEAGzZsgVbtmzBr7/+ivHjx+Opp55yeAOJiEiZmLkhZ7G75iYjI8MQ3GzYsAG33347xowZg7Zt22LgwIEObyARERGRPezO3AQFBRkWwtu4cSNGjRoFoLI4WKfTObZ1RESkWPauOMxMD9nK7szN5MmTcffdd6NTp07Izs7G+PHjAQCHDh1Cx44dHd5AIiJSJgYr5Cx2BzeLFi1C27ZtkZqaijfffBN+fn4AgPT0dMyYMcPhDSQiIiKyh93BjaenJ5588skax+fMmeOQBhERUePAxA05S53Wufnqq68wZMgQtGrVChcuXAAALF68GOvXr3do44iISME4LkVOYndws3TpUsydOxfjx49Hbm6uoYi4adOmWLx4saPbR0REBMD+AmRqvOwObt577z188skneO6556DRaAzH+/Xrh6SkJIc2joiIlIuhCjmL3cHNuXPnEBMTU+O4VqtFYWGhQxpFRETKx1Epcha7g5t27drh8OHDNY5v3LgR3bp1c0SbiIiIamIwRDaye7bU3LlzMXPmTJSUlEAIgb179+Kbb75BfHw8Pv30U2e0kYiIFIg1NOQsdgc306ZNg4+PD55//nkUFRXh7rvvRqtWrbBkyRLceeedzmgjEREpEIelyFnsDm4A4J577sE999yDoqIiXLt2DSEhIY5uFxERKRxjG3IWu2tuiouLUVRUBABo0qQJiouLsXjxYmzevNnhjSMiosald2RTuZtACmB3cDNx4kSsWLECAJCb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DH9/f6xbtw4nT550dPuIiEihqoelLG+/YCq7sAwAMO9/Sbi5VyuntY3cm92Zm48//hh33303wsPDERcXh40bN2LIkCHYv38/srKynNFGIiJSoKr6GdPNMu15LJE5dmduHn74YbRo0QJPPPEEZsyYAT8/P2e0i4iIGglL07/N7S1VhevdkDV2Z25++OEH3HPPPVi9ejVatGiBuLg4zJ8/H5s3b0ZRUZEz2khERApkbvsFNbdfIAewO3MzadIkTJo0CQCQl5eHP/74A2vWrMHNN98MtVrNKeJERGSTqtlSFncFV1kepmLehqypU0FxdnY2duzYgYSEBCQkJCA5ORlBQUEYOnSoo9tHRERKZa6gmIkbcgC7g5uoqCicOHECQUFBuOGGGzB9+nQMGzYMvXr1ckb7iIhI4YxXJbZvzRsic+pUUDxs2DD07NnTGe0hIqJGompoydr2C5aKillPTNbYHdzMnDkTAFBWVoZz586hQ4cO8PCo0+gWERE1YlUznizV3ADVdTlE9rB7tlRxcTEeeughNGnSBD169EBKSgoA4LHHHsPrr7/u8AYSEZGyqe1YxK8Kgx6yxu7g5tlnn8WRI0eQkJAAb29vw/FRo0bh22+/dWjjiIhIeYQQeGvTSfySlFF5wOJsKZXNwQ6RlN3jSevWrcO3336LQYMGGVW49+jRA2fOnHFo44iISHl2n8nGB9urrxdc54Ycze7MTVZWFkJCQmocLywsrHWreiIiorzicqPvTYuIjVha54ajUmSF3cFNv3798PPPPxu+rwpoPv30U8TGxjquZUREpEg+nhqj7422XDANZhjEUB3YPSz12muvYfz48Th+/DgqKiqwZMkSHD9+HLt378aOHTuc0UYiIlIQT43x52pLu4JbS+gw5iFr7M7cDBkyBIcPH0ZFRQWioqKwefNmhISEIDExEX379nVGG4mISEEq9Hqj761NBWc9MdVFnRao6dChAz755BNHt4WIiBqBcp1x3sXWbA2RrezO3BAREdVHuc44c6O2lLlRWQl2OC5FVticuVGr1bXOhlKpVKioqKh3o4iISLlqBjfmry3mN14gqp3Nwc3atWst3peYmIh3330XepNxVCIiIlNlFdZqbrhCMdWfzcHNxIkTaxw7deoUnn32Wfz000+455578PLLLzu0cUREpDw1am6sxDOW7uI6N2RNnWpu0tLS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", + "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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", "text/plain": [ "
" ] @@ -371,17 +369,10 @@ } ], "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "# matplotlin will draw a line graph by default\n", - "plt.plot(new_cases_usa)\n", - "# Rotate the labels on the x axis so that they don't overlap\n", - "plt.xticks(rotation=45)\n", - "# label the y axis for clarity\n", - "plt.ylabel(\"New Cases\")\n", - "\n", - "# Show the plot\n", - "plt.show()" + "new_cases_usa.plot.line(\n", + " rot=45,\n", + " ylabel=\"New Cases\",\n", + ")" ] }, { @@ -511,7 +502,7 @@ { "data": { "text/html": [ - "Query job 120a989f-4ce0-47e9-b051-a1a570ecd0e3 is DONE. 12.6 GB processed. Open Job" + "Query job 44159a16-cab9-4ffa-be68-2228387a48c2 is DONE. 12.6 GB processed. Open Job" ], "text/plain": [ "" @@ -565,7 +556,7 @@ }, { "data": { - "image/png": "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kJeXhwceeABTp07F6tWrAQAnTpzAyZMn8c477+Bf//oX3njjDezZswfXXntt15+wW8tUEkJizowZM5jBYGB/+MMf2LFjx9ibb77JOI7zrYI8a9YsdvXVV7Pvv/+eHTt2jN1///0sKSmJNTQ0MMYYKywsZM888wxjjLGSkhKWmJjIlEqlb3X222+/nd18883nbYfFYmGDBw9m06ZNY19//TU7fvw4++9//8t27NjBGGNs/fr1TKFQsJdeeokdPXqU/fWvf2UymYx9+eWXvnMAYNnZ2WzNmjXs+PHj7De/+Q3T6/WsoaGBeb1eVl1dzQwGA1u5ciWrrq5mdrvdtwpwU1MTY4yx1atXM4VCwaZMmcK2b9/Ojhw5wmw2GxswYABLTExkL7/8Mjt27Bi76667mMFgYHPmzGHr1q1jR48eZfPnz2cjR45koigyxhgrLS1lOp2OPffcc+zYsWNs+/btrKCggN16662+Ns+dO5eNGzeO7dy5k+3evZtNmTKFaTQa9txzz3XvD0tIBAFgGzZs8D3+6KOPGACm0+n8fuRyObvuuusYY4zdcccdDIDfKvN79uxhANiRI0e69vxheRWEkJgxY8YMNnXqVL9tF154IXvwwQfZ119/zQwGA3M6nX778/Ly2CuvvMIYY2zJkiXsqquuYowxtnLlSnb99dezcePGsU8//ZQxxtiQIUPY//3f/523Ha+88gqLi4vzBUnnmjJlCrvjjjv8tv34xz9mV155pe8xAPboo4/6HlutVgbA1xbGGDMajWz16tW+x8GCGQCspKTE77kGDBjAfvKTn/geV1dXMwDsscce823buXMnA8Cqq6sZY4zddttt7Je//KXfeb7++mvG8zxzOBzs6NGjDAD77rvvfPsPHz7MAFAwQ2LaucHM2rVrmUwmY0eOHGHHjx/3+2n99/L4448zuVzudx673c4A+G6uOosSgAnph/Lz8/0eZ2RkoK6uDnv37oXVakVSUpLffofDgbKyMgDAjBkz8Prrr0MQBGzbtg1XXHEF0tPTsXXrVuTn56O0tNSX4NeRkpISFBQUIDExMej+w4cP45e//KXftosvvhjPP/98u69Fp9PBYDCgrq7uvM/fllKpDHhPzj13WloaAGDs2LEB2+rq6pCeno69e/di3759fkNHjDGIoojy8nIcO3YMcrkcEyZM8O0fMWIE4uPju9ReQqJdQUEBBEFAXV0dpk2bFvSYiy++GF6vF2VlZcjLywMAHDt2DAAwYMCALj0fBTOE9EMKhcLvMcdxEEURVqsVGRkZ2Lp1a8DvtF5wp0+fDovFgqKiInz11Vd46qmnkJ6ejhUrVmDcuHHIzMzE0KFDz9sGjUYTjpfS7mvpCo1GEzQhuO25W/cH29b6fFarFb/61a/wm9/8JuBcubm5vi9qQvoCq9WK0tJS3+Py8nKUlJQgMTERw4YNw80334yf/exn+Otf/4qCggKcOXMGW7ZsQX5+Pq666irMmjULhYWF+MUvfoGVK1dCFEUsXrwYl19+OYYNG9altlACMCHEp7CwEDU1NZDL5RgyZIjfT3JyMgApqMnPz8eLL74IhUKBESNGYPr06SguLsZHH32EGTNmdOq58vPzUVJS0u7MhZEjR2L79u1+27Zv345Ro0Z170X2oMLCQhw6dCjgvRsyZAiUSiVGjBgBr9eLPXv2+H7n6NGjUVXzhpDO2r17NwoKClBQUABAKoNQUFCAxx9/HACwevVq/OxnP8P999+P4cOHY/78+fj++++Rm5sLAOB5Hh9++CGSk5Mxffp0XHXVVRg5ciTWrl3b5bZQzwwhxGfWrFmYPHky5s+fj7/85S8YNmwYqqqq8PHHH2PBggW44IILAEgzol544QXfrIPExESMHDkS//3vfztdQOvGG2/EU089hfnz52P58uXIyMhAcXExMjMzMXnyZPzud7/Dddddh4KCAsyaNQsffvgh1q9fjy+++KLHXn93Pfjgg7joootw99134/bbb4dOp8OhQ4fw+eef48UXX8Tw4cMxZ84c/OpXv8KqVasgl8tx7733hq2XipDeNHPmTEjpMsEpFAosW7YMy5Yta/eYzMxMvPfee91uC/XMEEJ8OI7DJ598gunTp+PnP/85hg0bhhtuuAEnT5705YcAUt6MIAh+uTEzZ84M2NYRpVKJzz77DKmpqbjyyisxduxYrFixAjKZDAAwf/58PP/883j22WcxevRovPLKK1i9enWnzx8J+fn52LZtG44dO4Zp06b57lIzMzN9x6xevRqZmZmYMWMGFi5ciF/+8pdITU2NYKsJiX0c6yisIoQQQgiJctQzQwghhJCYRsEMIaRHPPXUU9Dr9UF/OlrPhRBCuoqGmQghPaKxsbHdmUoajQZZWVm93CJCSF9FwQwhhBBCYhoNMxFCCCEkplEwQwghhJCYRsEMIYQQQmIaBTOEEEIIiWkUzBBCCCEkplEwQwghhJCYRsEMIYQQQmLa/wNhCyDCMi8TLQAAAABJRU5ErkJggg==", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjMAAAG1CAYAAAAMU3WaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAACFAElEQVR4nO3deXxU5dUH8N+9s08mM9lXEpawL4EEZFMWFQWkyubr2qp1t1SraK1Yl9LagtVW3EqtWrS+InUBwQ1QfEEFBCWEfUtAIPs++3rv8/5xM0OGmYRkMsnMJOf7+dCSuTczzwzj3DPPc55zOMYYAyGEEEJIjOIjPQBCCCGEkM6gYIYQQgghMY2CGUIIIYTENApmCCGEEBLTKJghhBBCSEyjYIYQQgghMY2CGUIIIYTENApmCCGEEBLTKJghhBBCSEyjYIYQQgghMS2qgpnly5eD4zg8+OCDvtscDgcWLVqE5ORk6HQ6LFy4ENXV1ZEbJCGEEEKiStQEMz/88ANee+015Ofn+93+0EMP4ZNPPsEHH3yAbdu2oaKiAgsWLIjQKAkhhBASbeSRHgAAWCwW3HzzzXj99dfxzDPP+G43Go148803sXr1alx22WUAgFWrVmHYsGH4/vvvMXHixAvetyiKqKioQHx8PDiO67LnQAghhJDwYYzBbDYjKysLPN/23EtUBDOLFi3CnDlzMGPGDL9gZs+ePXC73ZgxY4bvtqFDhyI3Nxc7d+4MGsw4nU44nU7fz+Xl5Rg+fHjXPgFCCCGEdImzZ8+iT58+bZ4T8WBmzZo1KCoqwg8//BBwrKqqCkqlEgkJCX63p6eno6qqKuj9LVu2DEuXLg24/ezZs9Dr9WEZMyGEEEK6lslkQk5ODuLj4y94bkSDmbNnz+I3v/kNvvzyS6jV6rDc55IlS7B48WLfz94XQ6/XUzBDCCGExJj2pIhENAF4z549qKmpQWFhIeRyOeRyObZt24aXXnoJcrkc6enpcLlcaGpq8vu96upqZGRkBL1PlUrlC1wogCGEEEJ6vojOzFx++eU4cOCA322//OUvMXToUPzud79DTk4OFAoFtmzZgoULFwIAjh07hjNnzmDSpEmRGDIhhBBCokxEg5n4+HiMHDnS77a4uDgkJyf7br/jjjuwePFiJCUlQa/X4/7778ekSZPatZOJEEIIIT1fxBOAL+SFF14Az/NYuHAhnE4nZs6ciX/84x+RHhYhhBBCogTHGGORHkRXMplMMBgMMBqNlD9DCCGExIiOXL+jpgIwIYQQQkgoKJghhBBCSEyjYIYQQgghMY2CGUIIIYTEtKjfzURIVxFFhkMVJjTYXEjSKjEiSw+ep2akhBASayiYIb3SjpI6rNxWitIaC9wCg0LGIS9Nh/um5WHywJRID48QQkgH0DIT6XV2lNTh8XUHcKTShDiVHGnxKsSp5DhSacbj6w5gR0ldpIdICCGkAyiYIb2KKDKs3FYKi9ODDL0aaoUMPM9BrZAhQ6+CxSlg5bZSiGKPLr9ECCE9CgUzpFc5VGFCaY0FiVplQCdWjuOQoFWgtMaCQxWmCI2QEEJIR1EwQ3qVBpsLboFBKQv+1lfJeLhFhgabq5tHRgghJFQUzJBeJUmrhELGwSWIQY87BREKnkOSVtnNIyOEEBIqCmZIrzIiS4+8NB0abW6c35aMMYYmmxt5aTqMyKI+XoQQEisomCG9Cs9zuG9aHnQqGapMTtjdAkSRwe4WUGVyQqeS4b5peVRvhhBCYggFM6TXmTwwBX+ZPwrDMuNhc3pQY3HC5vRgWGY8/jJ/FNWZIYSQGENF80ivNHlgCiYOSKYKwIQQ0gNQMEN6LZ7nMKqPIdLDIIQQ0km0zEQIIYSQmEbBDCGEEEJiGgUzhBBCCIlpFMwQQgghJKZRMEMIIYSQmEbBDCGEEEJiGgUzhBBCCIlpFMwQQgghJKZRMEMIIYSQmEbBDCGEEEJiGgUzhBBCCIlpFMwQQgghJKZRMEMIIYSQmEbBDCGEEEJiGgUzhBBCCIlpEQ1mVq5cifz8fOj1euj1ekyaNAlffPGF7/j06dPBcZzfn3vvvTeCIyaEEEJItJFH8sH79OmD5cuXY9CgQWCM4e2338bcuXOxd+9ejBgxAgBw11134Y9//KPvd7RabaSGSwghhJAoFNFg5uqrr/b7+c9//jNWrlyJ77//3hfMaLVaZGRktPs+nU4nnE6n72eTyRSewRJCCCEkKkVNzowgCFizZg2sVismTZrku/3dd99FSkoKRo4ciSVLlsBms7V5P8uWLYPBYPD9ycnJ6eqhE0IIISSCOMYYi+QADhw4gEmTJsHhcECn02H16tW46qqrAAD/+te/0LdvX2RlZWH//v343e9+h/Hjx2Pt2rWt3l+wmZmcnBwYjUbo9foufz6EEEII6TyTyQSDwdCu63fEgxmXy4UzZ87AaDTiww8/xBtvvIFt27Zh+PDhAed+/fXXuPzyy1FSUoK8vLx23X9HXgxCCCGERIeOXL8jvsykVCoxcOBAjB07FsuWLcPo0aPx4osvBj13woQJAICSkpLuHCIhhBBColjEg5nziaLot0zUUnFxMQAgMzOzG0dECCGEkGgW0d1MS5YswezZs5Gbmwuz2YzVq1dj69at2LRpE0pLS335M8nJydi/fz8eeughTJ06Ffn5+ZEcNiGEEEKiSESDmZqaGtxyyy2orKyEwWBAfn4+Nm3ahCuuuAJnz57FV199hRUrVsBqtSInJwcLFy7EE088EckhE0IIISTKRDwBuKtRAjAhhBASe2IqAZgQQgghpDMomCGEEEJITKNghhBCCCExjYIZQgghhMQ0CmYIIYQQEtMomCGEEEJITKNghhBCCCExjYIZQgghhMQ0CmYIIYQQEtMomCGEEEJITKNghhBCCCExjYIZQgghhMQ0CmYIIYQQEtMomCGEEEJITKNghhBCCCExjYIZQgghhMQ0CmYIIYQQEtMomCGEEEJITKNghhBCCCExjYIZQgghhMQ0CmYIIYQQEtMomCGEEEJITKNghhBCCCExjYIZQgghhMQ0CmYIIYQQEtMomCGEEEJITKNghhBCCCExjYIZQgghhMQ0CmYIIYQQEtMomCGEEEJITItoMLNy5Urk5+dDr9dDr9dj0qRJ+OKLL3zHHQ4HFi1ahOTkZOh0OixcuBDV1dURHDEhhBBCok1Eg5k+ffpg+fLl2LNnD3788UdcdtllmDt3Lg4dOgQAeOihh/DJJ5/ggw8+wLZt21BRUYEFCxZEcsiEEEIIiTIcY4xFehAtJSUl4bnnnsO1116L1NRUrF69Gtdeey0A4OjRoxg2bBh27tyJiRMntuv+TCYTDAYDjEYj9Hp9Vw6dEEIIIWHSket31OTMCIKANWvWwGq1YtKkSdizZw/cbjdmzJjhO2fo0KHIzc3Fzp07W70fp9MJk8nk94cQQgghPVfEg5kDBw5Ap9NBpVLh3nvvxbp16zB8+HBUVVVBqVQiISHB7/z09HRUVVW1en/Lli2DwWDw/cnJyeniZ0AIIYSQSIp4MDNkyBAUFxdj165duO+++3Drrbfi8OHDId/fkiVLYDQafX/Onj0bxtESQgghJNrIIz0ApVKJgQMHAgDGjh2LH374AS+++CKuv/56uFwuNDU1+c3OVFdXIyMjo9X7U6lUUKlUXT1sQgghhESJiM/MnE8URTidTowdOxYKhQJbtmzxHTt27BjOnDmDSZMmRXCEhBBCCIkmEZ2ZWbJkCWbPno3c3FyYzWasXr0aW7duxaZNm2AwGHDHHXdg8eLFSEpKgl6vx/33349Jkya1eycTIYQQQnq+iAYzNTU1uOWWW1BZWQmDwYD8/Hxs2rQJV1xxBQDghRdeAM/zWLhwIZxOJ2bOnIl//OMfkRwyIYQQQqJM1NWZCTeqM0MIIYTEnpisM0MIIYQQEgoKZgghhBAS0yiYIYQQQkhMo2CGEEIIITGNghlCCCGExDQKZgghhBAS0yiYIYQQQkhMo2CGEEIIITGNghlCCCGExDQKZgghhBAS0yiYIYQQQkhMo2CGEEIIITGNghlCCCGExDQKZgghhBAS0yiYIYQQQkhMo2CGEEIIITGNghlCCCGExDQKZgghhBAS0yiYIYQQQkhMo2CGEEIIITGNghlCCCGExDQKZgghhBAS0yiYIYQQQkhMo2CGEEIIITFNHsovCYKAt956C1u2bEFNTQ1EUfQ7/vXXX4dlcIQQQgghFxJSMPOb3/wGb731FubMmYORI0eC47hwj4sQQgghpF1CCmbWrFmD999/H1dddVW4x0MIIYQQ0iEh5cwolUoMHDgw3GMhhBBCCOmwkIKZhx9+GC+++CIYY+EeDyGEEEJigEcQUWdxosnmivRQ2r/MtGDBAr+fv/76a3zxxRcYMWIEFAqF37G1a9eGZ3SEEEIIiSqCyNBkc8Hk8IAxhkStMtJDan8wYzAY/H6eP39+2AdDSHcSRYZDFSY02FxI0ioxIksPnqdkdkIICUYUGYx2N4x2N8QoW5lpdzCzatWqsD/4smXLsHbtWhw9ehQajQaTJ0/Gs88+iyFDhvjOmT59OrZt2+b3e/fccw/++c9/hn08pPfYUVKHf2wtxbEqM1yCCKWMx5CMePxqeh4mD0yJ9PAIISRqMHYuiBHE6ApivELazRQu27Ztw6JFi3DRRRfB4/Hg8ccfx5VXXonDhw8jLi7Od95dd92FP/7xj76ftVptJIZLeogdJXV46P1iNFhdYIyBMYDjgF2nXDhRY8YL142hgIYQ0usxxmByeGC0ueE5r55ctAkpmCkoKAhaW4bjOKjVagwcOBC33XYbLr300jbvZ+PGjX4/v/XWW0hLS8OePXswdepU3+1arRYZGRmhDJUQP6LIsOyLI6g1OwEAMp4Dz3FgYBBEhlqzE8u+OIL1iy6hJSdCSK/EGIPZ6UGTNfqDGK+QdjPNmjULJ0+eRFxcHC699FJceuml0Ol0KC0txUUXXYTKykrMmDED69ev79D9Go1GAEBSUpLf7e+++y5SUlIwcuRILFmyBDabrdX7cDqdMJlMfn8I8TpQbsTxagu8y71ugcEliHAL0g2MAcerLThQbozgKAkhJDIsTg/KGu2oMztjJpABQpyZqaurw8MPP4wnn3zS7/ZnnnkGp0+fxubNm/H000/jT3/6E+bOnduu+xRFEQ8++CAuvvhijBw50nf7TTfdhL59+yIrKwv79+/H7373Oxw7dqzVHVPLli3D0qVLQ3lapBcoPtMElyCCATg/f827FOwSRBSfacLonITuHh4hhESE1elBo80Flyd2ApiWOBZCsRiDwYA9e/YEFM4rKSnB2LFjYTQacfToUVx00UUwm83tus/77rsPX3zxBb777jv06dOn1fO+/vprXH755SgpKUFeXl7AcafTCafT6fvZZDIhJycHRqMRer2+nc+Q9FSrtp/C0k8OX/C8p68ejl9e3L8bRkQIIZFjdwlosLngdAsh30eiVonEuPBvzzaZTDAYDO26foc0M6NWq7Fjx46AYGbHjh1Qq9UApJkW798v5Ne//jU+/fRTfPPNN20GMgAwYcIEAGg1mFGpVFCpVO16XNL76FTte8u39zxCCIlFDreABqsLjk4EMdEkpE/s+++/H/feey/27NmDiy66CADwww8/4I033sDjjz8OANi0aRPGjBnT5v0wxnD//fdj3bp12Lp1K/r3v/A34eLiYgBAZmZmKEMnvZzV4QnreYQQEkscbgFNNjdsrp71GRdSMPPEE0+gf//+eOWVV/DOO+8AAIYMGYLXX38dN910EwDg3nvvxX333dfm/SxatAirV6/G+vXrER8fj6qqKgDSMpZGo0FpaSlWr16Nq666CsnJydi/fz8eeughTJ06Ffn5+aEMnfRyHMeB4wLzZfzPAXWCJ4T0KE6PFMRYnT0riPEKKWcmbA/eygVj1apVuO2223D27Fn8/Oc/x8GDB2G1WpGTk4P58+fjiSeeaHf+S0fW3EjPt+9sE657bSfcnuYk4BbHuOY/CjmP9++ZRAnAhJCY5xZENFpdsHRhEBOzOTPhcqE4KicnJ6D6LyGdMSrbgMHpOhwsN+H8dx+DFMwMTtdhVLYhyG8TQkhs8AgiGm1uWJyeXtEUOqRghuf5NqfhBaFnJBSRnofnOVwzOguHK0wQgvz3zXHANaOzqGAeISQmCSJDo80Fs6N3BDFeIQUz69at8/vZ7XZj7969ePvtt6nGC4lqosjwzYk6qOQ8HB4RLduM8BygksvwzYk63HHJAApoCCExQ2jRBLI3BTFeIQUzwQrhXXvttRgxYgT++9//4o477uj0wAjpCocqTDhcYYS7OYpRyDjf+pIgMrhFEYcrjDhUYcKoPrTURAiJbtHcybo7hdTOoDUTJ07Eli1bwnmXhIRVndUJk8MDkTEoZTzkPA958/8rZTzE5sZqdVbnhe+MEEIihDEGo82Ns402NNpcvTqQAcKYAGy32/HSSy8hOzs7XHdJSNg1Wd0QRQaeP9dc0ts1m+cAnuMgigxNVnekh0oIIQFiqZN1dwopmElMTPRLAGaMwWw2Q6vV4n//93/DNjhCwi1RqwDPc77GksEoZBwStYpuHBUhhFyYyeGG0eaGW6Ag5nwhBTMrVqzw+5nneaSmpmLChAlITEwMx7gI6RLJOhUUFwpmeA7JOmqJQQiJDhanB41WFwUxbQgpmLn11lvDPQ5CusWQNB0cF+gK6/CIGJKm66YREUJIcLHeybo7hZwz09TUhDfffBNHjhwBAIwYMQK33347DAbaAUKi1ycHKv22YwcjMum8hWPbbnpKCCFdIRydrHubkHYz/fjjj8jLy8MLL7yAhoYGNDQ04O9//zvy8vJQVFQU7jESEjZ7zzaG9TxCCAkXh1tARZMdlUY7BTIdFNLMzEMPPYRrrrkGr7/+OuRy6S48Hg/uvPNOPPjgg/jmm2/COkhCwkWjkIX1PEII6aye2sm6O4UUzPz4449+gQwAyOVyPProoxg3blzYBkdIuA1Jjw/reYQQEiqXR0SjzdVjO1l3p5CWmfR6Pc6cORNw+9mzZxEfTxcBEr2S41W4UJcCnpPOI4SQruAWRNSYHShrtPWIQOZAuRF7Tkd2aT6kmZnrr78ed9xxB55//nlMnjwZALB9+3b89re/xY033hjWARISTilxKiRqFWiwugO6ZgNS1+xErQIpcRTMEELCqyd1svYIIrYdr8PavWU4UmnG2L6J+Oi+yREbT0jBzPPPPw+O43DLLbfA45GiSoVCgfvuuw/Lly8P6wAJCacRWXroNQrUt1LhlwHQaxQYkaXv3oERQnosQWRosrlg6gGdrJtsLny6vxLr91Wg3uLy3b7ndCP2nW3C6JyEiIwrpGBGqVTixRdfxLJly1BaWgoAyMvLg1arDevgCAk3UWSoNbfdd6nW7PS1PCCEkFB5O1mbekATyNJaC9YWleOrI9VBi45e1C8RQgSfY0jBjNFohCAISEpKwqhRo3y3NzQ0QC6XQ6+nb7UkOn2yvxJWZ9tbHq1OAZ/sr8T8QuozRgjpuJ7SyVoQGXaW1mPt3jIUnzUGHFfIOFw6JA23Te6HyQNTIjDCc0IKZm644QZcffXV+NWvfuV3+/vvv48NGzbg888/D8vgCAm3sw2WoLkyLbHm8wghpCMYYzDZPWiyuyBcqDpnFLM4PfjiYBU+3luOSqMj4HiiVoFrRmfh6tFZSIpTIlGrjMAo/YUUzOzatQt///vfA26fPn06fv/733d6UIR0lWqT68IndeA8QgjpKZ2szzbYsG5vOTYdqoY9SNG+wek6LCjsg+mDU6GUh7QZusuEFMw4nU5f4m9Lbrcbdru904MipKuk69u3S6m95xFCerdY72TNGMOPpxuxtqgcu041BBznOeCSQSm4trAPRmTpwXHRmUsYUjAzfvx4/Otf/8LLL7/sd/s///lPjB07NiwDI6Qr5CTFhfU8QkjvFOudrO1uAV8ersa6onKcbrAFHI9XyzFnVCbmjslCul4dgRF2TEjBzDPPPIMZM2Zg3759uPzyywEAW7ZswQ8//IDNmzeHdYCEhNPs4el4qJ3nEULI+WK9k3W1yYH1xRX47EAlzI7AFZa+SVosKMzGjOHpMdXWJaRg5uKLL8bOnTvx3HPP4f3334dGo0F+fj7efPNNDBo0KNxjJCRs1u8vb/d514/v28WjIYTEiljuZM0Yw8FyEz7aW4bvTtQhWG7yxAFJWFCQjbF9E6N2KaktIQUzADBmzBi8++67bZ6zfPly3HvvvUhISAj1YQgJq3d3BbbhCOa1b05SMEMIgcMtoNHmgt0Ve0GMyyNi67EafFRUjhM1gTs0NQoZZo3MwPyCLPRJjO06cSEHM+3xl7/8Bddddx0FMyRq1FvaLpjndarOhh0ldRGvnUAIiQynR0CjNTY7WTdYXdiwrwKf7KtAoy2w2nmmQY35BdmYNTIDOlWXhgHdpkufRayXbSY9D8e17z3JACz74gjWL7qEKgET0ovEcifr49VmfFRUjv87WgNPkLWkMTkJWFiYjYkDkiHrYZ9rPSMkI6SdZHz7ayMcr7bgQLkxYr1GCCHdxy1IQYwlSFJsNBNEhu9K6vDRnjIcrDAFHFfIOFwxLB3zC7ORl6qLwAi7BwUzpFfhOvBtxCWIKD4TucZphJCuF6udrE12Nz47UIn1xRWoCdJvLlmnxLwxWfjZqCwYtIoIjLB7UTBDepXRWQb8VNe+wo6M0VIpIT1VrHay/qneinVF5dh8uBrOINvDh2XGY2FhH0wdlAK5LLqq9HYlCmZIrzJxYArW769q9/nxmp7/jYaQ3iQWO1mLjGH3qQZ8VFSOPacbA47LeA7TBqdiYWE2hmX2zkbPXRrMTJkyBRqNpisfgpAOsdoDM/tbwwFI0kW+gRohpPNisZO1zeXBxoPV+Li4HGWNgTPKBo0CP8vPxDWjs5Aa37tbsIQ0B1VUVIQDBw74fl6/fj3mzZuHxx9/HC7XuQZ9n3/+OTIzM1u9n2XLluGiiy5CfHw80tLSMG/ePBw7dszvHIfDgUWLFiE5ORk6nQ4LFy5EdXV1KMMmBN+V1rf7XAagLEiZb0JI7GCMwWhz42yjDY02V0wEMhVNdvxjawmuf+17vPJ/JQGBzIDUOPz2ysFYc9cE3HFJ/14fyAAhBjP33HMPjh8/DgA4efIkbrjhBmi1WnzwwQd49NFH230/27Ztw6JFi/D999/jyy+/hNvtxpVXXgmr1eo756GHHsInn3yCDz74ANu2bUNFRQUWLFgQyrAJgdnR/pkZAHjl6xPYUVLXRaMhhHQVxqSZmLMNdtRbnRCClb2NIowx7D3TiCc/PohfvLkbH+4ph7VFoT4OwMUDk/H360bj9V+MxexRmVDFULuBrsaxEDKfDAYDioqKkJeXh2effRZff/01Nm3ahO3bt+OGG27A2bNnQxpMbW0t0tLSsG3bNkydOhVGoxGpqalYvXo1rr32WgDA0aNHMWzYMOzcuRMTJ0684H2aTCYYDAYYjUbo9b1zLZGcM/+Vb7G3LHD7YmvUCh4X9UvC278cT/VmCIkRZocbTTHSydrpFrDlaA3W7i3HyVprwPE4lQxXjczEvIIsZBqiM20jUatEYlz4l+Q7cv0OKWeGMQZRlN4kX331FX72s58BAHJyclBXF/q3WKPRCABISkoCAOzZswdutxszZszwnTN06FDk5ua2Gsw4nU44nee2qZlM7b9wkZ6vqQM5MwCglPEorbHgUIUJo/oYumhUhJBwiKVO1rVmp69KrylIbZs+iRosKMjGzBEZ0ChpBuZCQgpmxo0b5+ucvW3bNqxcuRIAcOrUKaSnh9ZtWBRFPPjgg7j44osxcuRIAEBVVRWUSmVAO4T09HRUVQXfkbJs2TIsXbo0pDGQnq/R6rrwSS3EqWRwiwwNto79HiGk+9hcHjRYY6OT9eEKEz4qKsM3J+qCLn1d1C8RCwqzcVG/JPAx2PAxUkIKZlasWIGbb74ZH3/8MX7/+99j4MCBAIAPP/wQkydPDmkgixYtwsGDB/Hdd9+F9PteS5YsweLFi30/m0wm5OTkdOo+Sc/R0aUiHoCC55CkpV1NhESbWOlk7RZEfHO8Fh8VleNolTnguFrO48oRUsPHvslxERhhaDiOQ5xSBq0q8jNHIQUz+fn5fruZvJ577jnIZB1/Ur/+9a/x6aef4ptvvkGfPn18t2dkZMDlcqGpqclvdqa6uhoZGRlB70ulUkGlosxuEpxa3rGc9yaHB4W5iRiRRflWhESLWOlk3WRz4ZP9ldhQXIH6ILPCafEqzCvIxpxRGYhXx05NK7VCBp1aDp1SHjW5hGGtM6NWqzt0PmMM999/P9atW4etW7eif//+fsfHjh0LhUKBLVu2YOHChQCAY8eO4cyZM5g0aVLYxk16j45uaFDKONw3LS9q/oMlpDeLlU7WpTUWfFRUji1Hq+EWAj908vsYsKAwGxfnpcRMw0eFjIdOJYdOLYciCisLtzuYSUxMBNfO9buGhoZ2nbdo0SKsXr0a69evR3x8vC8PxmAwQKPRwGAw4I477sDixYuRlJQEvV6P+++/H5MmTWrXTiZCzuf0dOyb3KVD0jB5YEoXjYYQ0h6x0MlaEBl2lNZjbVEZ9pUZA44rZBwuG5qGBQXZGJQeH4ERdhzPcYhTyRGvlkMd5dvA2x3MrFixwvf3+vp6PPPMM5g5c6ZvhmTnzp3YtGkTnnzyyXY/uDdxePr06X63r1q1CrfddhsA4IUXXgDP81i4cCGcTidmzpyJf/zjH+1+DEJaCtbLpC0//tSAHSV1FNAQEgGx0Mna4vDgi4OVWLe3AlUmR8DxpDglrhmdiZ/lZyGpC7YvdwWtUpqBiVPK2j2JEWkh1ZlZuHAhLr30Uvz617/2u/2VV17BV199hY8//jhc4+s0qjNDWhrw2GfoSDij4IDsJC3+Mn8UBTSEdJNY6GR9psGGdXvLselQFRzuwE+Vwek6LCzsg+lDUqNyWeZ8SjmPeJUCcSpZ1DSo7PI6M5s2bcKzzz4bcPusWbPw2GOPhXKXhHSLjm7cFBhgdniwclspJg5IptwZQrpQtHeyZozhx9ON+KioHLtPBaZT8BwwZZDU8HFElj7qZzVkPOfLg1HJo3sZ6UJCCmaSk5Oxfv16PPzww363r1+/HsnJyWEZGCHRQASgUcqocB4hXUgUGZqiuJO13S1g86FqrNtbjjNB+rXFq+WYMyoT88ZkIU3fsY0w3c27nVqnlkOjiJ1lpAsJKZhZunQp7rzzTmzduhUTJkwAAOzatQsbN27E66+/HtYBEhJpHGNUOI+QLiCKDKbm1gPRGMRUmRxYv7ccnx2ogiVI8nHfZC0WFmZjxrD0qE+QVSlkiI+y7dThFFIwc9ttt2HYsGF46aWXsHbtWgDAsGHD8N133/mCG0J6CrPTA61SToXzCAkTxhhMdg+a7K6oawDJGMOBciPWFpXju5K6gHIOHIAJA5KwsLAPCnMTonpmQyHjEaeSQ6eSQ9nBGluxJuQ6MxMmTMC7774bzrEQEpXsLhGj+uiocB4hncQYg8nhgdHmhkeMrtYDLo+I/ztWg4+KylFSYwk4rlHIMHtkBuYXZCM7MTobPgLSdmqtSoZ4laJX9XQKOZgRRRElJSWoqanxNZ30mjp1aqcHRki0UCl4KpxHSCdFayfrBqsLG4or8Mn+CjTaAhvRZhrUmF+QjVkjM6BThbXObFhplDIpmVclj+rZoq4S0r/M999/j5tuugmnT58OyDjnOA6CEN0lpgnpiIevHEzbsgkJUbR2sj5ebcaHe8qw9VgtPEGWugpyE7CgIBsTByRHbZVehYyX8mBU8qjZTh0pIQUz9957L8aNG4fPPvsMmZmZvTIKJL0DB+Dm8X0jPQxCYk40drIWRIZvT9RhbVEZDlaYAo4r5TxmDJOq9A5I1UVghBcm4zlfHky0Jx13p5CCmRMnTuDDDz/0dcsmpKdiAA5VmjA6JyHSQyEkJkRjJ2uT3Y3PDlRifXEFaszOgOMpOiXmjcnGnFGZMGijr+Ejx3HQNi8jaWOoKm93CimYmTBhAkpKSiiYIb3Cjz/VUzBDyAVEYyfrU3VWrNtbji8PVwdtZTI8Mx4LCvtg6qCUqFymUSnO5cFE61JXtAgpmLn//vvx8MMPo6qqCqNGjYJC4R/J5ufnh2VwhESDb0vqcceUvEgPg5CoFG2drEXGsPtUAz4qKsee040Bx2U8h+mDU7GgMBvDMqNvh6Kc56FT947t1OEUUjCzcOFCAMDtt9/uu43jODDGKAGY9Dh8dJXBICQquDwimmyuoMXkIsHm8mDjQalKb3mTPeC4QaPA1aMzcc3oLKToVBEYYet663bqcAopmDl16lS4x0FI1MrPoRYGhHhFWyfriiY71u0tx8aDVbAGWeLKS43DgsI+uHxoWtTNdHi3U8f10Kq83SmkYKZvX9rdQXqPKYNTIz0EQiIumjpZM8aw92wT1haVY2dpPc4fDc8Bk/NSsKAwG6P7GKIqYZa2U3eNkCsAvfPOO/jnP/+JU6dOYefOnejbty9WrFiB/v37Y+7cueEcIyERZYqSb6CEREI0dbJ2ugVsOVqDtUXlOFlnDTgep5LhqpGZmFeQhUxD9FTplfEctEo54tW0nbqrhBTMrFy5Ek899RQefPBB/PnPf/blyCQkJGDFihUUzJAepckaWBWUkJ4umjpZ15qdWF9cjk/3Vwb9ctEnUYMFBdmYOSIjanJOOI6DRiF1p46j7dRdLqRg5uWXX8brr7+OefPmYfny5b7bx40bh0ceeSRsgyMkGlQabZEeAiHdJlo6WTPGcLjShLVF5dh2vDag4SMAjO+XiAWFfTCuXyL4KAkWlHIe8SoFdGraTt2dQk4ALigoCLhdpVLBag2c+iMklm0+XIN7pw2kBD3So0VLJ2u3IGLb8Vp8VFSOY1XmgONqOY+ZI6SGj7nJ2giMMJCc5xGnkmZhVPLomBnqbUIKZvr374/i4uKAROCNGzdi2LBhYRkYIdGiqsmGQxUmjOpDu5pIz8MYg9npQZM1sp2sG20ufLqvEhv2VaDe6go4nq5XYX5BNq4amQmdOvINHzmOQ5xSCmC0ysiPp7cL6V9g8eLFWLRoERwOBxhj2L17N9577z0sW7YMb7zxRrjHSEhEOQSGBlvghyshsS4aOlmX1ljwUVE5thythlsInBHK72PAgsJsXJyXEhXLNurmPBgdbaeOKiEFM3feeSc0Gg2eeOIJ2Gw23HTTTcjKysKLL76IG264IdxjJCSiBIEhSauM9DAICZtId7IWRIYdpfVYW1SGfWXGgOMKGYfLhqZhYWEfDEyLfMNHhYyX2gqo5VDQduqo1OFgxuPxYPXq1Zg5cyZuvvlm2Gw2WCwWpKWldcX4CIk4tyBiWEZ8pIdBSKdFupO1xeHB5wcr8fHeClSZHAHHk+KUmDs6Cz8bnYnECH+B4DmpOzVtp44NHQ5m5HI57r33Xhw5cgQAoNVqodVGRxIWIV3B4RGx5sez+PlEKhZJYpPdJTWBdESok/WZBhvWFZVj0+EqONyBgdSQjHgsLMzGtMGpEZ/50CrltJ06BoW0zDR+/Hjs3buXKgGTXmP1rtO4aXwueJ6DKDIcqjChweZCklaJEVl6WjsnUSmSnaxFxvDjT41YW1SG3T8FNnzkOWDa4FTML8jGiCx9RAMH73bqOJWMqvLGqJCCmV/96ld4+OGHUVZWhrFjxyIuLs7vOHXNJj1NRZMDhypMMDvc+MfWEhytMsPtYVDIOQzNiMevpg/E5IEpkR4mIQAi28na7haw+ZDU8PFMQ2CNJr1ajjn5mZg7OgtpenW3j89LxnO+PBjaTh37OBZCfWqeD4xco7VrtslkgsFggNFohF4ffe3eSffq99hnIf2eVinD/ZcNwls7TqHB6kLL/2o4Tlrrf+G6MRTQkIiKZCfrKpMDH+8tx+cHqoI+fr9kLRYU9sGMYWkRy0FpuZ1ao6BlpGjXkes3dc0mpB1cHgEf7jmLWrMTHCcVyeIAMAAeUUSt2YllXxzB+kWX0JIT6XaR6mTNGMP+ciPWFpVje0ldQJVeDsDEAclYWJiNgtyEiAUPKoXUnVqnoqq8PVVIwczp06cxefJkyOX+v+7xeLBjxw7KpSE9jihKSYwcAAXP+z6UvT+7BRHHqiw4UG7E6JyESA6V9CKR6mTt8oj4urnhY0mtJeC4VinDrOYqvdmJkWn4KOd5qR6MSg6lnPJgerqQgplLL70UlZWVAduxjUYjLr300qhaZiIkHBgAjyDlyJz/7ZLjOMhkHDyCiL1nmyiYIV0uUp2sG6wubCiuwIZ9FWiyBzZgzUpQY35BNmaNyECcqvur4vIcB61KhniVImoaTpLuEdK7zZsbc776+vqAZGBCegLv5UIUGWTBPiObT+Ai21yY9HCR6mR9rMqMj4rKsPVYLTxB+jYV5iZgQWE2JvRPjsgyjkYpLSPFUVXeXqtDwcyCBQsASN9Eb7vtNqhUKt8xQRCwf/9+TJ48ObwjJCQKKHjALQIeEeB5EWAcOE5aZgInfVNWyHiMyU2I8EhJT+TtZG20u7utCaQgMnx7Qmr4eKjCFHBcKedxxbB0LCjMRv+U7v8Sq5DxiG9eRqLt1KRDwYzBIDXaY4whPj4eGs25tVClUomJEyfirrvuavf9ffPNN3juueewZ88eVFZWYt26dZg3b57v+G233Ya3337b73dmzpyJjRs3dmTYhHSajOcggkEQAZeHwTsVw+FcQDM4XYdR2dSMkoRPJDpZG+1ufLa/EuuLK1BrcQYcT9WpMHdMFubkZ8KgUXTLmLxkvFSVV6eiqrzEX4eCmVWrVgEA+vXrh0ceeeSCS0rbt2/HuHHj/GZwWrJarRg9ejRuv/1236zP+WbNmuV7XACt3hchXcnhCX4h8YY1iRoFlsweRlPcJCwi0cn6VJ0Va4vK8dWRajiDtDsYkaXHwsJsXDIwpVtnQjiOg7Z5GUlLVXlJK0LKmXn66afbdd7s2bNRXFyMAQMGtHp89uzZbd6HSqVCRkZGu8fmdDrhdJ77NmEyBU6PEhIO3rhFxnPok6jBxAHJkR0Q6RG6s5O1yBh2nWzAR0VlKDrTFHBcznOYPiQVCwqzMTSje+t00XZq0hFdmm4ejiz7rVu3Ii0tDYmJibjsssvwzDPPIDm59YvGsmXLsHTp0k4/LiEXIjLpw17Oc74KwaP60DITCU13drK2Oj3YdKgK6/ZWoLzJHnA8QaPA1aMzcc3oLCTrum82nLZTk1B1/965Dpg1axYWLFiA/v37o7S0FI8//jhmz56NnTt3QhZ0SwmwZMkSLF682PezyWRCTk5Odw2Z9DoMLg+Dy+PCdyW1FMyQDuvOTtblTXas21uOjQerYAvSr2lgqg4LCrNx2dC0bgsmOI5DHG2nJp0U1cHMDTfc4Pv7qFGjkJ+fj7y8PGzduhWXX3550N9RqVSUV0O6jUcElHIOHoFh06Fq3DM1j/JmSLt0Vydrxhj2nmnCR0Xl+P5kPc6fL+c5YHJeChaOzUZ+tqHbclJoOzUJp6gOZs43YMAApKSkoKSkpNVghpDu5vEwqBQyVBvttNRELqi7Olk73QK+PFKDdXvLcarOGnA8TiXDVSMzMb8gGxmG7mn46N1OHaeSQ0HbqUkYdWkwE+4Iv6ysDPX19cjMzAzr/RLSGSKANJ0SNo+IBpsr0sMhUaq7OlnXmp1YX1yOT/dXwhSkV1NOogYLCrNx5fCMblnW4TlpO3W8mrZTk64T0QRgi8WCkpIS38+nTp1CcXExkpKSkJSUhKVLl2LhwoXIyMhAaWkpHn30UQwcOBAzZ87symET0mECAAXPIUmrjPRQSJTpjk7WjDEcqjBhbVE5vjlRG9DwEQDG90vEgsI+GNcvEXwXLyVxHAeNQupOHUfbqUk36NJgxmw2t3n8xx9/xKWXXur72Zu4e+utt2LlypXYv38/3n77bTQ1NSErKwtXXnkl/vSnP1FODAmJ2IVFx4w2N8bkJmBEVvduXyXRqzs6WbsFEVuP1WJtUTmOVQd+3qoVPGYOlxo+5iZru2wcXko5j3iVAjo1bacm3SukYKa6uhqPPPIItmzZgpqamoAZmPY2mpw+fXqbszebNm0KZXiEBBWsJHvYcAz3TaPkXyJ1sm6yu2HuwiaQjTYXPtlXgQ37KtFgDVzazNCrMa8gC1eNzIRO3bWpkXKeR5xKmoVRyWkZiURGSO/y2267DWfOnMGTTz6JzMxMmkIkMaEr81kcXZzMSaJfd3SyPlFtxtq95fj6aA3cQuBjjO5jwILCPpic17UNHzmOQ5xSCmC0ypjaR0J6qJDehd999x2+/fZbjBkzJszDIaTrdGU+i1sE/vL5EWz49SU0O9PLiCKD0S41geyKTtaCyLC9tA5ri8qxv8wYcFwh43D5UKnh48A0XdgfvyV1cx6MjrZTkygTUjCTk5PTZd88COkKosi65ELT0qEKE1bvPoOfT+zbpY9DokNXd7I2O9z4/EAVPi4uR7UpsOFjcpwS14zJwtX5mUjowkBdIeOltgJq2k5NoldIwcyKFSvw2GOP4bXXXkO/fv3CPCRCwmtHSR1WbitFSZAEyXBiAF75+gRuGp9L31p7sK7uZH2m3oa1e8ux+VAVHEGqAg/NiMfCwmxMHZzaZcEFbacmsabdwUxiYqJfbozVakVeXh60Wi0UCv828A0NDeEbISGdsKOkDo+vOwCL0wNNN3woV5uc2H+2CWP6Jnb5Y5Hu1ZWdrEXG8MNPDVhbVI4ffmoMOM5zwLTBqVhY2AfDu3DHnFYpp+3UJCa1O5hZsWJFFw6DkPATRYaV20phcXqQoVd3aZ0PLwbgi0NVXRbMiKJUT6TB5kKSVokRWXqaBeoGXdXJ2u4SsPlwFdYWleNsY2DDR71ajp/lZ2LumGykxndNSQrvduo4lQxyWkYiMardwcytt97aleMgJOwOVZhQWmNBolYJjuMg57vng9oahgqvwYKW70/WY+W2UpTWWOAWGBQyDnlpOtw3LQ+TB6aEYeTkfFan1AQy3EFMldGBdXvL8fnBSlidgTvh+qfEYUFBNmYMS4OqC2YUZTzny4Oh7dSkJwgpZ+bzzz+HTCYLqMS7efNmCIKA2bNnh2VwhHRGg80Ft8CgbP62qVZ0TzBTkNO5WRlvjk/LoCVZp0SN2QlBZEjUKqGU8XAJIo5UmvH4ugP4y/xRFNCEUVd0smaMYX+ZER8VlWNHaV1AlV4OwKS8ZCwozEZBTkLYl3k4joO2ubmjlpaRSA8TUjDz2GOPYfny5QG3i6KIxx57jIIZEhWStEooZBxcggg1330f3tXGwOWC9mqZ4+MNWpweAUerzBBEhtwkrS8hU83LkKHnUWVyYuW2UkwckExLTp3kcAtosIa3k7XLI2LL0RqsKypHSa0l4LhWKcPskRmYV5CN7ARN2B7XS6WQAhidiqrykp4rpGDmxIkTGD58eMDtQ4cO9eu1REgkjcjSIy9NhyOVZmTo+W4LZl76ugRDM/VIjVd3KKfl/Byfc+PlfP9bZ3FBp5aD897GcUjQKlBaY6GO3Z3QFZ2s6yxObNhXgU/3VaLJ7g44np2gwfyCLMwckYE4VXgLz8l5XqoHo5JDKac8GNLzhfRfkMFgwMmTJwO2ZZeUlCAuLi4c4yKk03iew33T8vD4ugOoMjmRoFVc+JfCwOER8Zv/FkMtlyFNr8KN43Nxw7gcHKkyt5m4e36Oj5dHFMGYlOfg9AhwuES/bscqGQ+jyKhjdwicHgFNNjesYUwOP1olNXzceqwWniBbt8fmJmBBYR9MGJAU1oaPPMdBq5IhXqXolm7YhESTkIKZuXPn4sEHH8S6deuQl5cHQApkHn74YVxzzTVhHSAhnTF5YAr+Mn+ULwelu9icHtidHtRZnHhq/UEs++II1HIePMe3mrh7fo6Pl5znwXEAOICJaN4WfO5i5RRE6tjdQeHuZO0RRHx7og4fFZXjcGVgDzCVnMcVw9MxvyAb/VPC+4VP05wHE0dVeUkvFlIw89e//hWzZs3C0KFD0adPHwBAWVkZpkyZgueffz6sAySksyYPTMHEAck4VGHC1a981y2PKTCpNgjPSX+3OgU43QKyE7RQyvmgibvn5/h4qRU8VHIedpcAjoPfrizGGJpsbgzLjMeILD1t3b4AtyCiyeaG2RG47BMKo92Nz/ZXYn1xBWotgVV6U3UqqeHjqEwYNOGbGVTIeMQ3LyPRdmpCOrHMtGPHDnz55ZfYt28fNBoN8vPzMXXq1HCPj5Cw4HmuW/NJOCBgt4rIgHqrE32TtTCo5aizOPHXTcfwYb8kyOV8qzk+HMchRafCmQabtCzBMYgig7P5wqxTyXDftDzaut0Gbydro82NE9UWGB0uGNRKDEyPC2mp51SdFR8VleGrIzVBdzyNyNJjYWEfTBmUErakWxkvVeXVqagqLyHn41gHmyy53W5oNBoUFxdj5MiRXTWusDGZTDAYDDAajdDru65yJokN/R77rFseh+MAMKmInpeC5yACUMp4eEQRYnO0M6pPAh6dOQSTB6a02M0kIEGrgErG+4IWGQ+kxatQb3HBLTIo+HPBCoCAXVAuQURjc7ATzVu3u3I2qWUn66LTDVi9+yzO1lt9r19OchxuGp+DgtwLb6cXGcP3J+uxtqgcRWeaAo7LeQ7Th0hVeodkxIdl/LSdmvRmHbl+d3hmRqFQIDc3F4IQvqx/QnqcIF8RGBgEEXAyAQoZD14GeETgZK3Vb8mpZY6PsfmiOywzHvdNy/Mtl7W88APArat2B+yCioWt28Fq6oRjNun8TtZ7zzTi718eh80lQK9WQC/j4BYYTtZa8Pcvj2PxFYNbDWisTg82HqrCur3lqGhyBBxP1CpwdX4Wrh6diWRdeKr00nZqQjompGWm3//+93j88cfxzjvvICkpKdxjIiTmMXg3VEs4SIELAMhlHHiOg8gAnmNI1SlhdHh8AUfLHJ9gsxXnL5cdKDMG3QUFRPfW7WA1dTpbCJCxc0GMtwmkyBhW7z4Lm0tAik7p29auknNI0SlRZ3Fh9e6zGJ2T4LfkVN5ox7q95dh4qAq2IFu2B6bpsLAwG5cOSQu6/VlkDCXV1nYvadF2akJCF1Iw88orr6CkpARZWVno27dvwHbsoqKisAyOkFjWcnKG4wDGpKCGBwfGGDwig0bBQ6OUgeM5v4CjIzk+re2C8orGrdut1dQJdTaJMQaTw4MmW2An65JqK87WW6FXK3yBjBcHDvFqBc7WW1FSbcWg9DgUnWnCR0Vl2HWyIWCCjeeASwamYEFhNkZlG1pd9tl7prFdS1ocxyFOKYNOLYdWGd5aM4T0JiH91zNv3rwwD4OQns17fZXzUpDjERlkHIfUeOlC3pmAo7VdUF7RuHW7tZo6QMdmk9rTydrokHKM9LLggYdSxsEkith8uArLNzbip3pbwDk6lRxXjZKq9Gbo1W0+t/YsaU3KS5FmYWg7NSFhEVIw8/TTT4d7HIT0WFoFDxnPw+r0QIQ0RaNR8EiNV0PXXPm1MwFHW5WOz9+6HS3CMZvU3k7WBrUSCl4KKFRy/8DBLYiot7pgdniwdm95wO/mJmkxvyAbV45Ih6YdO4jaWtJK1alQZ3Vh7d5yzC/oQ0EMIWFE85qEdBEOgErGYXCGHmvumIAb3tyFk7VWpOqU0tJSc9DR2YAjWKXjlrugvFu3o+ni2ZnZJKvTg0Zb+5tADkyPQ05yHE7WWpCiUwJMqtLcaHO3WjRvfP8kLCzMxti+iR3auh2wpMVJlXllvJQnlRSnxKlaa9TlLxES60IKZgRBwAsvvID3338fZ86cgcvl/+2poaEhLIMjJJYxAG4GnG2w4kSdFY/OHILH1x2A0eEBx3NhDTgutAsq2rZlhzKbZHN50GhzwxmkCWRbybY8x+Gm8Tn42+ZjKG9ywCMwuILM5qgVPGaOyMD8gmzkJmlDel7eJa0EOQ+5jAfPwe+5RTp/iYoqkp4qpGBm6dKleOONN/Dwww/jiSeewO9//3v89NNP+Pjjj/HUU0+Fe4yExCxBZGiye1BndeLSIWldGnBcaBeUVzRc0Doym3ShTtYXSrZtsLpwoNwIs1MIuispKU6J68f1weyRmdCpQ5+sVsp55CbGQS3nwYCgW6ojmb/UVdvgCYkGHS6aBwB5eXl46aWXMGfOHMTHx6O4uNh32/fff4/Vq1d3xVhDQkXzSEvdVTTvfH/7n9FYOFZq/eFyCfjnNydxusGKvklxuHfqACibGwN2daARbRc0v/GcVwiwsG/iBTtZn59sq2hOtjU53JDzHPomx2FfWRPcQuDH3OB0HW6ekIvJeaFX6ZXxnFQPRi2HSi6DKDLcump384yTKmDGqcrkxLDMeLz9y/HdGkC2tg0+Fooqkt6rS4vmAUBVVRVGjRoFANDpdDAajQCAn/3sZ3jyySdDuUtCerTisiYsHNsHr39Tile3lsJsd0MEwAP4945TWDQ9DyOyDF0aaHRFXZfOCjabNDAtDiaHBxVN9jZ/N1iyLWMMLo8Iu1uAwy2i1uK/nKOQcZgxLB0LCrKRl6YLacwtt1NrFP5VeaMxfynYNnjGGBgD4pQyNNnc+MfWkqgsqkhIe4UUzPTp0weVlZXIzc1FXl4eNm/ejMLCQvzwww9QqcJTAZOQnmT7iVq8tq0Uz206BkFkkMs4yDlpy7bR5sbyL45Cr1FAxnNdEmiEu65LOHlr6ng7WVcaA6vsBtMy2VYUAaPdhSa7G57zm2IBSNYpMXd0Fn6Wn4mEEJd41ApZu7ZTR1v+0vnb4C1OD2rNDjg9Irzz8rtPNWL17jP4+cS+3Tq2UEXDUimJLiEFM/Pnz8eWLVswYcIE3H///fj5z3+ON998E2fOnMFDDz0U7jESEhZikItcd6kzOfDK1yUQRAalnAPPSVuSOU4ak1tgaLS5MSw9DnK5tOQUzkAjXHVd2hLqBSbUTtZGhwsOjwiHR9paHexfV8ZzuG5cH9w2uR9kPIeSaitO1Fja3WRSIeN9y0iKDnSnbm/+UndouQ3e4vSgvNEOgTHIeQ4cB4hgcHtEvPz1CQxIiYv65aZoWyol0SGkYGb58uW+v19//fXIzc3Fzp07MWjQIFx99dVhGxwh4XSowhSxx7Z7RAhMbG5lIF0UBcbgEUS/7ton6+3IStD46s+0DDQOlBvBc1xIF8eurhIcygXG28na7PCgI6l7ImP44acGvLPzDEyO4Fur41VyaFUyMJFh2qA0HCw3BiYJJ2kxZXAqMg1qv+CG5zhoVTLo1YpOdafu7k7trfFug3d6BNSaHRCY9Px9XdkZIOMZnB4xant4eUXjUimJDmGpMzNp0iRMmjQpHHdFSJepNbdv+aIreERpq7ai+RohMOnb8PmXcJdHRHmjHdmJ5wIalYxHrUvA4+sOoNHq6vC3UVFkaLC4IIgiTA43DBpFwOxMZ3bZdPQC07KTdUeCGLtLwKZDVVi7txxljYH5NDwHJGgUMGgUkMs41FlcGJCqg9npwoqvTvhV5DU53NhX1oS9Z5ugVcqhVfDolxKHe6bm4bJhaT2qO7V3G/yBMiOcHrF5Rqa5xhEYBJFBrZAjRaeMyh5eXtG8VEoiL+RuZu+88w4uvvhiZGVl4fTp0wCAFStWYP369WEbXDQTRYYDZUZsO16LA2XGiC5hkAvbUVKH5RuPRezxve8Ot8DAIM3IeJtRtvzYlfFSoFNrdvgu9I12FywON840l9mPV0uzDt5gYUdJXauPu6OkDreu2o3nNh2F2elBeZMdp+qsfsXivHVd8tJ0HS7a573AmB1uGNQKuAURTo8IlZxHhl4Fi1PAym2lEEUGUWRotLpwtsEGo93d7kCm0mjHyq2luO5fO/HS1yUBgYyc56BVypCVoEFSnBICA+osLmiVMtxwUR+s+aHMlySskvNSwTyrW5oRY4AoiojXyHGyzoY/fXYYO0vrO/QaRDtvUrJKzsMjMin5FwwiY/AIDDzHITVeBZVMBncHZ+e683OwI0ulpPcJaWZm5cqVeOqpp/Dggw/iz3/+MwRB2jqZkJCAFStWYO7cue26n2+++QbPPfcc9uzZg8rKSqxbt86v7xNjDE8//TRef/11NDU14eKLL8bKlSsxaNCgUIYdNrRmG1u8MwcN1sg3WhSZNPvibTrZ3N3AR9oizMHpEeFwi1DJOVSbnBAZYHd7YHcL4DhAJZchRaf0BQvBvo2eP2OikPMob7TD5hJQ1mhDlkEDhZzv1C6bQxUmHK4wwu4SYXLYpOfFASq51K4hQatASbUZ35+sR3aiJqAJZGsYY9hXZsRHRWXYWVqP83+NAzA5LxkLCrPBGMN7P5ThbL0VVpcHCo7DgFQdbhqfgzil1EQyXiWH083gET2ot7p9Sy3gOLhFBg48MvTyHvvtfvLAFNx/+SD86dPDEEQRoiD9O6kVMqTGq6BTyWF3Cx2anevuz8FYbKhKuk9IwczLL7+M119/HfPmzfPLnxk3bhweeeSRdt+P1WrF6NGjcfvtt2PBggUBx//617/ipZdewttvv43+/fvjySefxMyZM3H48GGo1W03e+sqtGYbW1pOTafqVK3mWHQHDtIMjd+FucXfeQ4QRIDnpG2zVpcHlUYPBJGBByDjpWJsjDHYXQIqmuxIjVcHXRoINiWvVsjAJ3GoMTlgdwuoMNqREqds1y6b1pJ7vyupQ6PNDY4D5DwvdQcHYHeLKGuwIUOvhlNgONtoQ4bhwv/Nujwithypxtq95SittQYcj1PKMHtUBuaNyUZWgsZ3+5jcxKAVgH/4qQE2lwiz0wO3IEIUAdH7b8Fx4DmAiYBHFMFxsrAkQkerm8bnYuPBKhysMMKglkMhk0Gt5H1b2jvSUiMSn4Ox2FCVdJ+QgplTp06hoKAg4HaVSgWrNfADqDWzZ8/G7Nmzgx5jjGHFihV44oknfDM9//nPf5Ceno6PP/4YN9xwQyhD7xRas409LaemVYqQV1XDxhvQtCTjgNR4NTRKGWrNTthdHogA7E4PAAYO0oyNW5BmdLzLU6IA1FsciFcrfd9GvUHHnjONOFppRoLWPz9Gp5IjLjUORpsbNpeAR2cNw9wxWW2+X1v7Bn7P1AHYdKiq+Tlw53YGMQY5B7hFhhqLE3qVDAZ12xeYOosT64sr8On+ShjtgbuashM0mF+QjVkj06FVBn5s8RyHwRmBdWNqzU7Y3B6AAQo5Dw7SchcD4PZICdneQAzo2d/ueZ7Dr6bnNQchAhK0MjARcAhCh2bnIvU5OCJLjwGpcThYboJBo4BCxkOt4H11c6KxoSrpPiEFM/3790dxcTH69vWvSbBx40YMGzYsLAM7deoUqqqqMGPGDN9tBoMBEyZMwM6dO1sNZpxOJ5xOp+9nkyl866fdsb2VhJff1HSE05oYAIUMcAtAsk4Bu1NAnEoOrVIGhUwGgYlgTNrdxPMcGBicggg0Bwbn3xcAODwMKkFEklaJHSV1+MfWUhyrMsPqkpakbC4P0vTnunMDAAcOerUCDo+IRK2ize3DbX0D/+2H++HyiFDJZXALInhIkRYDAI6DjJcK2CWnxGFgelzQ1+RIpQlri8qx9Xht0CWosX0TsbAwG+P7J7W74aOc56FTy6FVyPD9yQbIeA6iyMBx0uvKNQczDIBHYIhTSTMUQM//dh+OGjiR+hz8/mQ9jHY3zE4PjA43ZBwHlZyHQauAy8OisqEq6T4hBTOLFy/GokWL4HBISYq7d+/Ge++9h2XLluGNN94Iy8CqqqRvfOnp6X63p6en+44Fs2zZMixdujQsYzgfrdnGHt+2VEGAw9W+LstdSRClmZiLB6Rg85Fq1FtdaLD6Lz/JeQ7ZBjUsLgEeR+ul/L08gogmqwuPfLQPDVYXGGMQm3dPWV0CzjbYkJOk9QtonIIIUWRYseUEakyOoDkPF/oGfqbBDrvbg6wENSoaHXB7RClYaM4DEgRpBmnKoBS/QMQjiPjmRB3WFpXhcKU54PkoZBwKcxMxc0QGpg5OaVcQ491OHa9SQNPcGuJAmREnay1Ii1ej1uxsTnaFb3yA9BrpNYqQllpiVWdr4ETic7BlUJ2hV6PJ5oLTI/XacnhEDM+Mx5LZw2iJvxcLKZi58847odFo8MQTT8Bms+Gmm25CdnY2XnzxxYgs/7S0ZMkSLF682PezyWRCTk5OWO6b1mxjz4gsPZJ1ShytMrc7+bQriQyIU8mwr8wIpYwHD84XWJw7h6Ha4oTb077gizGGP3x6CLVmJzgAchnfPCsizUB4RIYqox15aTrfRdtbAfZsgxVJcSq/GZcl6w7grikD4BFZ0KUqQPoGrtfIYXG44fKISDeo0WBxwSUIEEUpYFDIeWgUMozNTQIgVTr+9EAF1hdXoM4SeKFL0CgQp5LD6fbgeLUZp2ot+OxApa9hZDAapUxaOgtSldd70U2LV0Ip51FrdsLp8Q8OeUjBo93dsaWWWNeZGjjd/TkYLKhOjFPA4RLhFgQYHR4YNEpMHJAclscjsSmkYMZut2P+/Pm4+eabYbPZcPDgQWzfvh19+vQJ28AyMjIAANXV1cjMzPTdXl1djTFjxrT6eyqVqstaKnjrNUhN5PiAJnK94VtdrPn+ZD1qzE4pkIl8LAMASItXwezwIDdJC4vTg7JGm99xxgCHu/2zSC6B4XS9FRy8eSHSjiiFjEl5NpAScq1OD2Q8jyabC3a3CDkHJGikXCIOHNS8DDqViPImO/74yWGo5TwsLk/QpSqRMWjkPHgeMNs9yExQIztRDaebQWAi+Oay+QNSdeB54PlNx/DV0Rq4ggRoI7P0KMxNwFdHqmFzefwaRp6steDvXx7H4isG+wIahYxHvFoOnUoOeRtVeVtedHUqOeJUMjhcIjyiCJdHRKPNBZdHhNnhgUbBItZuINZ09+dgsGUtDhw0Shk0kEEhl+FkLS3v93YhZUTOnTsX//nPfwAALpcL11xzDf7+979j3rx5WLlyZVgG1r9/f2RkZGDLli2+20wmE3bt2hWxAn3eeg06lQxVJifsbgGiyGB3C6gyOXvNt7pY4f1GJ4gMuUlaaJSRTwBO0Slhskv5JwB8264BaTYD6FjMJeelInQeUUoS5lpUrZHxHBQy3ndLrdmFRqsLNpcAd/PW79MNVvxUZ4PF6YHF6UFFk0OqCcMY4jVy8BwHh1tAeaMdFqcHIpMCJLdHhFNg0Knk0ChlqLO44PRIrRpkPA+zQ9rhZLS7cfc7e/D5wSq/QEbOc7hieDr++fNCrLhhDA5VmmF3i75aMHxzPkSKTgmbS8B7u88iTilHVoIGOUlaJGiVbQYywLmLbqNNqmnjvQDGqxVIilNCq5RjVJ8EvHDdGLz2i3F4+5fjKZBph+7+HGzPslZH6+OQniekT/eioiJMmTIFAPDhhx8iPT0dp0+fxn/+8x+89NJL7b4fi8WC4uJiFBcXA5CSfouLi3HmzBlwHIcHH3wQzzzzDDZs2IADBw7glltuQVZWll8tmu7mTaAblhkPm9ODGosTNqcHwzLjaVt2lGn5jS5erUD/lNC6JIeLUsbD6ZaaKZrsbpTWWqULgTd/o4MzR3KeA2Pw5ZSITApCWItwSMZzkMuk3JXB6TqYndIuJgZpi7IgAjaXB+WNNlQZ7RCZ1ATTe/9qBQ9wUp5LtckBl0do3g3EYHa4kZcWj0dnDcGAVB0cLg/qrE40WJywuUU0WN0oqbH4jTlRq8Atk/pizd0TsWT2UAxOj/drGNkyGAMnbUdP1CpR3mhDjdnZofYCF7roxqvleHTmEEwfmoZRfQz0JaQD2vM5GK6Cei1n2IKh5f3IiabisSEtM9lsNsTHxwMANm/ejAULFoDneUycONFXDbg9fvzxR1x66aW+n725LrfeeiveeustPProo7Barbj77rvR1NSESy65BBs3boxYjRmvaGoiR1p3/je6SJeodwkilHIeIgOqzU543y7Btmu3h+93mu/A46uXIi3FyJq3rIrNszYHyo3wNCf6ugXmuw/GpMrEruZj3sJ3CpkMKTqV1JgQDE63ALtbWkIyO9zQKmW+fJbUeDXe3vETtpfWB11KGpimw7WF2Zg+JA1Kuf93KKPDBbfIoG8OojiOg4znmpN1Ocg4DmanJ6Rv3tHWwbonaetzsKMF9dpqUkrL+9Ep2orHcqwjzVGa5efn484778T8+fMxcuRIbNy4EZMmTcKePXswZ86cNncbdTeTyQSDwQCj0Qi9nt7svcmBMiPueedHxKnkvm/0B8qNER7VOW0FMecf41v87K0zo5RzcHmkHTpCkDuSNfdKYEyqyOvty8PzHFweMaCqbksqOYf+yXEQAdhcAurMDjg8DGqFlNQ7ICUON47PgciAtXvLsetkQ8Bz4TngkkEpWFjQByOz9a0Gk8erLHhqwwHEqRTQKmUBu5fsbgE2pwev/WJct3f0Jh3X2nb+xuYE6/NnsNtzUTx3nwIStAqoZDyczd3Wg90n6Vod/TcOVUeu3yHNzDz11FO46aab8NBDD+Hyyy/35bBs3rw5aDE9QiKhrW900aA93yI4AIlxClgcUiVgaVkJSIpTosnuBpq3GvNAQHAiMCmgSdAqpJmX5q3T7cmF9ggMFpcAbXMQ6A2geO9uKIsLz20+jmqTM+B349VyzBmVibljspCub30WleM4xClluGRQMoZk6HGk0ow4pf8yUri+eUdLB+uerqMF9dpbSbg9M2wUsHaPaC0eG1Iwc+211+KSSy5BZWUlRo8e7bv98ssvx/z588M2OEI6w5sz8fi6A6gyOZGgVUR6SO2mlPPQqeTINKjRYHVBUEpdo739dNCcXaLkeXgYg1ImBSne3UteGrkMd17SH69/ewoc15wsLLA2gxk5J+XTNFicYHFKqQ6NCChlHJRyHiaHG032wLYQfZO0mF+YjSuGp0PTRm6LWiGDTi2HrsV26vP/nc7/5k2J9bGhIwX1RmTpO3RRDOeyFgldtBaPDSmYAaSt097t017jx4/v9IAICafzv9HFCgXP4cUbxmByXorvwztBIwVjTXY3TtVZ8cLmY7C5BMh5rjm3BOA5vrkAH4MgMCjlPDINWmgUMjjcIhxu4YKzMgzS7I/TLSX9eqQixHAJDK4grQbG90/CwsJsjOub2Orsl0ImBWc6tRyKILtSKLelZ+hIQb1QLorBZtioX173itbisSEHM4TEipbf6K5+5btID6dd5DIO8WpFq8sjSVqlVKYfQMs5EI47tx+I8dIMTEKcAnlpOvzwU2BeS0syDr5WCr7Zm+Zc3vN/j+OAOKUcShmH2yf3D9oXiec4xKnkiFfL27ULKVKJ9bQ8ET4dKagXjotitC559GTRWjyWghnSK8RazoTR7sED7+3FXVMH4KbxuQEfxCOy9MhNjsP+sqbmgEbKZfHOyogig0ImJeumxKkwdVAKdpTUtfp4HODLpXEHyyZupuClb8x6tQIcB9TbXDA6/C82WqU0AxOnlHU4T6m7/51oeSK8OrLz6FCFqV0XxQaLC9uO1wYNNKN1yaMni9bdZZGvIkYICcAAnG6w4ekNhzD31e98gYi3rsO3JXVYWJgtbbP2MLgFAS5BhNMjwi0wCAxwiyKSdUoMy4jHNyfqoJK3/p+7t/FiazQKGbIMavRL1iKxeVbIJTAoOA4GtdQuIDlOhdwkLTIMUrXgaEu4Pp93eeJIpQlxKjnS4lWIU8l9yxNtBX8kuI4U1Du/qGFL3pYbNreAv248gkfe34d73vkRt67a7ffvQgX1ul+0Fo+lmRlCopRSJiXsHq0yY8na/fj5xL745kSd3yxCVoIGZxpsOL+0Cw9pS3aN2Ym3d/6EY1UmpMSrpP5EbhE8DzARuFAbSw5S/kx2ohp8i4J2UtE8Dwan6zBtSAo0ytj6KKHlia7T3vynYAn63sTvWrMDVqcArVIGnVrRah5MtC559HTRmOMWW59AhPQScl5qCwCIEBlDlcmJ5zYfQ7xKjqQ4FRS8VEiuzuwCBw4yTlpi4jip2JxKLkNSnAJmh4D3fjgDt4dBr+aRrFOhqskOQWy7xk28Wo5UnQpmpxt1FheqTc7m5EoOHlEKZPRqGX5z+aBOBzKRyFmh5Ymu1d78p9YuihzHQauUITdJ22agGe4lD8qfar9oKx5LwQwhUYhrbiEgiAwipMq+AGDjBChkHlicbjg9UrdtgUkzMRkGNZTNfY3kMk6KVtQc6i0ugAOsLqn4nNBKnRmpJo0SiVo5eE6atterFbC5RGTo1TA73LC5pCn94Vn6sHwDi1TOSrTuyOhJ2pv/dP5FscHiwl83HoFOLe3es7sEeEQRcp6HWsEHBJrh2tZP+VMdF025iBTMEBKFWkvCdbhF2N0O8M0tC8ABApMCnhqzA5kGDZQK3hetKHip9owoMjTaArdVeylkQL/kOL/eSBzHQRAZ4lUyPP8/o8FzXFi/gUVySy0tT0SXlhfFbcdr4REBl0dEpdEOp0f0tdhQNedmtcyDCceSB23vjn0UzBASQ1iLv3CQLgLe0r+iCNRbXMhOVIOJgNHhRqPNDU+QvgU8BylJF4DdI0DhvaBz0pZqGcdJu5WsUvPAUdnhbcQY6ZyVaN2REcvCtUSTpFVCZCIqjC6IDM11lKT3vt0tosJoh14t9ws0O7PkEen3IgkPCmYIiRG++jHNf0TGWvaYBscBTo+AKqMTVpcnaO8ljUIGOQ/IZByUPI+c5Dhc1DcRn+6vQL3VjUStAgp511ffjXTOSlvJp1R1uOPCuUQzLCMeApNaaijlnK9XFwdAzjO4PNLS6rCMeL/fC3XJI9LvRRIeFMwQEiO4c5Mwvm7XLYsreFemzE7/VgMcgMkDk7GwsA9GZetRWmOD0eFCcpwKhbkJ0GsUmDo41XcxMjk8Xb4zIRpyVqJxR0YsCvcSzZEqM2TNndMFEQDPpJkZBgjNHeBlHIcjVeawBBfR8F4knUfBDCEx4vz+9iJjEANbJPnwHHDJwBTcPXUAshI0AKQLwbj+idCp/KvydvfOhGjJWYm2HRmxRhQZ/rG1BE02FwwapS+3Ra0IfYmmweYCz3HITtSg3uKC0yOAiefuN1mnhM0lhC24iJb3IukcCmYIiVGtFepNiVPi8mHpuHliDnQqhW+bq04lh7aNqrzduTMhmnJWomlHRqxZvfsMdp9qhMgYLE67L0k3NV4qnBjKEo03uFDKePRL0cLhEs/tZlLycLhFKHgxbMFFNL0XSeioAjAhMeJCDSJlHIdpg1Pw7p0TcM+0AUjWqZGsk6rypupUOFlrxTcn6nCgzAgxWEJNN4rWKqKk/XaU1OHlr0/ALUhFGOW8lN9id4sob7TD4vSEVIG3ZWVgMECjlCFerYBGKQMY0GRzIy9NF7bggt6LPQPNzBDSQwiM4dvjdfifszvxq2l5uHf6QADRUz/j/N0uEwckU85KjPLuAHK6RUgVArjmGQ0GGSfVRaoyOpCVoOrwEk0kkrMpfyr2cez8phg9jMlkgsFggNFohF5P04S9Xb/HPov0ELqEnJdyZBikjtcynsPvZg3BiCxD0OTMRpsLChmPWyb1wyUDU7o8T6StgIpyVmLPgTIj7nnnR2hVMlQZnXC4BfAcB48o1YTxXlTkPDAsU4/1iy7p8L+p33umObjo6iCcKgBHl45cv2lmhpAYJIN/XyUZz/u2sPKcCJeH4dX/K8WIrPiA+hkeN4PdJaDB7cLfvzyG/+w4hYHpHf8G2t4PfipI1vN4dwCpZDKkxqtwtsEGlyBVqW75DhBEqT/Y9yfrO/xvHInkbMqfil0UzBASg85vEMkYfFcRnuMhl4kwOdw4VGFCavy5QMbi9KC80Q6RSbM3YIBcxnc4sGjv0hUVJOuZWu4AilPJoJBJ1aKBc7MyHKQWG26BhfxvHAvBBc3mRAcKZgiJcRykbast8RzgYYDLc65+BgNDrdkJkTFf7yZPc1CToVe1O7DoyExLrBYkowtU21ruADKo5fCIDEq59D4TRQaRMWkbdZwSDo8Ylf/G4RAt+WiEghlCYh7HBQYzYnO9D6X8XP0Mh0uE0yNAxnPgwEGEVIxMzvPtDiw6OtMSiwXJ6AJ1YS2TdOssTogig1SihYMIadkzrfn9EY3/xuFAy6fRhbZmExJjOEgzL14yjvNrECkyER6BQa9WYESWHo02NxhjvuRMqZoqg0dkUMmlTsQA2rWNtiMzLYD/ckQw0VaQzHuBOlJpQpxKjrR4FeJUct8FakdJXau/K4oMB8qM2Ha8Niq2v3c17w6g/qk6AIBHlAo5ahQ8shM10Kmk78rR9m8cDucH9WqFDDzPNRcLVMHiFLByW2mPfw9EE5qZISTGeLsYeAMaj8gAToS356R3N9OiS/N8u5mqTE5oFDwABkGUzpNxnF8+TXsuOh2daYmlgmSdye/prbM5kwem4MN+Sbj2tZ04WWtFqk4JTYvCjNH2bxwusbp82pPRzAwhMUhgQLZBhVsn9YVeo4AgNjfgExkMGgV+N2sI7pqa5/v2PCwzHoLIwHFSoqZa7v/t2XvRuVAxso7OtMRSQbKOzjp5tWc2pyfP2sjlPB6dOQRJcQoYHR44PGLU/huHS3uC+o4WCySdQzMzhMSos01OvLPzNHgOUMh48ABUCh4jsuIxIuvct8GWW1y/K6nFf3aehssjQsZzEEXWoWJkocy0xEpBslDye9ozm7PsiyMwaBQ4WWvtsbM2sfJvHC7Uzyn6UDBDSAzzMEjrTqIIDoBCzuFghQmLP9iHWyf1xSUDU307cUb1MWBElh7xagXe230GNSYnAEAha/9FJ9TqrLHQ0LGjFyhRZFhfXIHDFSZolTL/AiuQZnNUch6HK82IV8mQGq/u0UmisfBvHC6xtHzaW1AwQ3qNnjS1HwwDYHJIFWiMdg/+tvk43tl52jcLAMD3zdnlEQEOyNCrccP4XNw0PrfdF51Qv4VHe82QjlygvDkyh8qNaLS7YbQDjTY3UuNV55buwNBkc0FkDAaN0telvCfX2In2f+NwiUTLBdI2amdAeo0DZUZc/cp3kR5Gt0qNV4IxDt6VE0Fk57U1kD54Q5kh6Im1WM5ttxWCXqD+Mn8UAPi25GoUMlQZHQAnFS7kOc6Xi2R3Cfip3gKAQ7/kOKgVUsdnbwdoBqkS82u/GNcrAoCeKBItF3oTamdASBC9MRnP7PAgLyUOx2ssAIDBaTrwvBTZdHaGoCd+C7/QrNPEAcm4ddVuX44MADTaXLC7Rch5KVisNTsRp5LBLQgQRECr5OERRfxUb4fTc257vFLGQy7je+X7sqfoTUtr0Y6CGRLTOjI70BuT8TwCg9HhAWvud+D0MGhavAyd3Ubqff3rrE40Wd1I1CqQrFPF9Ad6WxeoA2XGgB1PqfFqlDfa4REZeA5wuD0w2twwOz3geQ4aBY+KJgcExiDnOanODwCHWwA8Is422CL7hEmn9MSgPhZFfTDzhz/8AUuXLvW7bciQITh69GiERkSiRbDaHgNS4zBrZCZykrQBwc2wjPgIj7j7CYzB4vRAFAGeBzyiCKlN5TmhVmj1vv6HK4wwOTxSFVieg16twPAsfUxPtbd2gQq240mnkiM7UYNaswNOtwiRATaXgJFZBjTZXDhWbYbIGBR8yzwcaXWf54CNB6s6lLNECAkU9cEMAIwYMQJfffWV72e5PCaGTbpQsFLiTXYXdp1qwM6TDdCp5IhTyvzWrw9UGCM97G7HGGCyuyECYCLz1YdhjPnyNzwig5yD3y6dC8127Sipw5K1+1FnccLhYQCTAhlRZDDa3dhf1nTB3TqxmHPT2o4n6f0Whya7GzanB0/MGY65Y7KwevcZPL3hEMC84QsDY9JylIznkRqvwslaKq5GSGfFRFQgl8uRkZER6WGQKBGstofF6UGtWdo5AgZ4BBFaldJvC+y7u85EeugR4d3ExQDUmhwQRcDidPvyN0TGoNcoYLS7gs52penVmDkiA5cMTPFtNV32xRGUN9khiOe6JDORQc7zEBmDR5BmhHpaxdy2djwBgMMtYkS2AXPHZIHnOeQkaaFTyeERRLgEEUyU8mXUChlS41XQKmSosTgpb4aQToqJYObEiRPIysqCWq3GpEmTsGzZMuTm5gY91+l0wul0+n42mUxBzyOx6/xKrYwx1JqlnAQFz4MB0gwEO9cN+h9bS1FSbY700CPOLQJVJoevv5M3GdXtEfHAmmLIeSn4SdQq4RJE1JicqDI5sL+sCW98q8DwLAMGpulwuNIM1iKQ4SD9nlsQIW+euUhSKIPm4sRyg76ObslN0ioRp5RBq1ICjPPtZFIreXDgYHcLVFyNkDCI+nYGEyZMwFtvvYWNGzdi5cqVOHXqFKZMmQKzOfiFadmyZTAYDL4/OTk53Txi0tXOz1twuEU4PWJzcmVzgiWT8kO8Ca7HqsywuIQIjzx6MEgtEcTm/7e7BDRYXag1uxCnlMEjMlQ2OaTgpDlp1e4ScbjCiHe/P928TNLiDjkpoGGQllB8QdIFKubGYoO+li0ibE4PaixO2JweDMuMDwjEvDM5TTYP1Aoe8WqF1LsIXLtbSBBCLizqZ2Zmz57t+3t+fj4mTJiAvn374v3338cdd9wRcP6SJUuwePFi388mk4kCmh7m/LyFlt2ggXOzDXL+XDdop0eAw+2J4Kijm7fTEgNQ1miHQi4tF8llUkdujjG4RRFJCgUarG7fud4ABpB+4Jg0Q+Od9Tl/1qGnNOhr75ZcKq5GSPeI+mDmfAkJCRg8eDBKSkqCHlepVFCpVN08KtKdzs9bkPO8b7ur1BWaQa2QQa3k4RZEVJscMDs8iN7v+tFFYIDgFsEDEKVG2wAYRJHBLbAWMzDnfqdlMAkAMh6wuwUMy9T7zTqE0v8oWrV3S25v61tESCTEXDBjsVhQWlqKX/ziF5EeComQ87/tGjRyKGW8VLcDgIznEa+W42yDHUa7O8KjjV0iAJev+ZM0C2N3C36zOC21rCWukPHQqeQBsw69tUEfFVcjpGtFfc7MI488gm3btuGnn37Cjh07MH/+fMhkMtx4442RHhqJoJZ5C3aXALmMP7erBgyVRkdAIEOXjc6Rej+dW6qT88FfU4WMw5icxKCJvN5ZtUabG+d3UunpOSTemZxpg1Mxqo+BAhlCwijqZ2bKyspw4403or6+Hqmpqbjkkkvw/fffIzU1NdJDIxHm/ba7o7Qea/eW4cvD1TA7PBAE/4tkik6Jq/Oz8NmBCtSYo3/5IlYwcFDKOd9WbJ4DkuOUeGDG4FaLwFEOCSGkK1CjSRKzDleYsGr7KawvrvAVg2tpeKYeiy7Nw8wRGdheWo8H1+xFo42WncIhOU4Jp0eE0yM0Ly9JReCe/Nlw/Hxi3wv+PjXoI4RcCDWaJD2WIDJ8daQab357Crt/agg4rpBxuGZ0Fn55cX+MzD6XnJmkVcJOW7M7TapPwyFeLUemWg6HS6oizHMczA4PcpK07bofyiEhhIQTBTMkJhjtbqzZfQb/2Xka5U32gOMpOiV+PqEvbp7YF6nxgbvZhqTpgs7ekPbhAXA8B2+sIeelom8apQyATCr+JutY4i416COEhAsFMySqldSY8ca30lKS3R04szI8U487L+mPn43OglLeej77Zwer0LMXVLuWRskD4GBzCdAqZVArzr3W3sTdYZnxPTJxlxAS/SiYIVGHMYYvD1fjrR0/YUdpfcBxngMuG5qGu6YOwPh+SQHF14Ipb7KdV+GNtJeMAxK0Klicbsh4DjKeh8MjUuIuISRqUDBDokajzYn//lCG93afwel6W8DxeLUcCwqzccfF/ZGbHNeh+85O0IIHQFkz7ccBkPMctCo5GGPI75OAqYNS8M2JOir+RgiJKhTMkIgSRIbjVWb85/uf8Mm+SlicgS0H+iZrcdP4XNw4Phd6jSKkx7k6PxNLPzmEpl5WRM87T9LahJRBI4dBo4DIALPdDZmMw+QBKRjXNxGjcxPAcxya7G6/BN07LhlAibuEkKhCwQzpdowxWJwebC+pw+pdZ/BdSR2C9RWcOCAJN0/IxZXDM6BSBFaL7Qi5nMf8wmys2v5Tp+4nlvAckKCRIzdZhyuHpyEtXo3jVWbYPCJ0KhkOlBlxqs4Ku1uqupufk9CuGRZK3CWERBsKZki3cbgF1Ftc+GRfBT4sKkNJjSXgHI1ChlkjM3Dj+FyMzjFAJe9cENPS9CFpPSqYSdQqcN+0AfjvD2X4qcHq65WklPPISdTg2rE5uGRgSqszJ6LIaIaFENIjUDBDupRbEGFxeHC63oq1ReX4ZH9F0MJ1mQY15hdkY35BNnKStFB3ciYmmGjs98OjucO3jG/OT5FhVLYBo7INMKgV+KnBhq+PVqOyyQGBNeexyDgMSdfh8auGY/LAFNw5JQ8Hyo3Ye7YJHAPG5CZgVPaFy+XTDAshpKegYIaEnSgyWFweWBwe7C9rwod7yrD1WC08QdaSCnITsKAgG9OGpCFVp2quW9I1omXbsErBI14lx+0X98fFzUs65+eltPSHq0e0GazwPIfROQkYnZPQ3U+FEEKiAgUzJGzsLgFmhxsmhxvfHK/D2qIyHKwwBZynlPOYMSwNCwqyMTRTj6Q4JbTKrn8rfn8ycJt3d+E5QMZz0KsVGJ6l79DuHwpWCCGkbRTMkE5xeURYnNIsTIPVic8OVGJ9cQVqzM6Ac1N0Sswbk405ozKREq9CYpwSOlX3vAVFkWHlttJueSwvngN+NioT8wqyYbR7kKhVIFmnotwUQggJMwpmSIcJorQbyeL0wOkWcKrOinV7y/Hl4Wo4PcEbPi4szMaUQSnQKOVI0CoQrw5ti3WoDlWYUBok4bgrqOQchmbo8duZQ3DJIOruTgghXY2CGdIujDHYXAIsTg9sLgGCKGLXyQasLSrDnjNNAefLeQ7Th6RiQWE2hmboIed5JMQpEK+St6tib7g12FxwBQm0wknBAw/OGIypg9No9oUQQroRBTOkTQ63FMBYnR4IIoPV6cGmQ1VYt7ciaMPHBI0CV4/OxDWjs5CsU0HGc0jQKKHXRCaI8UrSKs9VkOsCGXoV/n7dGKqCSwghEUDBDAngEaQ8GLPDA3dz8ZLyJjvW7S3HxoNVsLkCmwIMTNVhQWE2LhuaBqWcB89xSNAqoFcromKGYkSWHhl6NeosrrDdp5wHUuNVuHdaHn4xsV9UPE9CCOmNKJghAKQEWatLyoOxNwcrjDHsPdOEj4rK8f3J+oCS+DwHTM5LwcKx2cjPNoDjOPAcB71GgQRNdAQxXjzP4YbxuXji44Oduh+tgsO7d02CyeGhQnOEEBIlKJjp5ewuAWanGzanAJFJ4YrTLeDLIzVYt7ccp+qsAb8Tp5LhqpGZmF+QjQyDGgDAcRz0ajkStErIovTiflMngxkOwENXDEFBbmL4BkUIIaTTKJjphVpup/aI55Jia81OfFxcjs/2V8LkCGz4mJOowYLCbFw5PMNX3I7jOOhUciRqFZDL+G57DqHozAyKWsHj4SsG466peWEcESGEkHCgYKaXOH87tRdjDIcrTVhbVI5tx2uDNnwc3y8RCwr7YFy/RPAtknh1ajkStUooojyICdWVw1Ihk8lw2dA0zB+TDbm8Zz5PQgiJdRTM9GCMMdjdAswOaTs1Y+ciFbcgYtvxWnxUVI5jVeaA31XLecwckYH5BdnITdb6HdOppOUkZQ+/uP/r1vGRHgIhhJB2oGCmB3J6pADGu526pUabC5/uq8SGfRWotwbu7EnXqzBvTDauGpURUNhOq5QjMU4R1k7WhBBCSGdRMNNDeAQRVqeUzBusOFxJjQUfFZXh66M1cAuBa0n5fQxYUJiNi/NSAhJ4NUoZErXKLulkTQghhHQWBTMxjDEGq0uAxeGBzRWYsCuIDDtK6/FRURn2lxkDjitkHC4bKjV8HJQeH3BcpZAhSavs0k7WhBBCSGdRMBODHO5zy0giC5xlsTg8+PxgJT7eW4EqkyPgeFKcEnNHZ+FnozORqFUGHFfK+W7rZE0IIYR0Fl2tYoRbEGFxSLuRvFV5z3emwYZ1ReXYdLgKDnfgOUPS47GgMBvTh6QG3YGkkPHd2smaEEIICQe6akUxUWSwuKR6MA53YAsBABAZw48/NWJtURl2/9QYcJzngKmDpIaPI7L0QfsjKWR8RDpZE0IIIeFAwUwUsjUHMNbztlO3ZHcJ2HxYavh4psEWcFyvlmNOfibmjs5Cml4d9D4i3cmaEEIICQcKZqKE0yMl8lqdgl9V3vNVmRz4eG85Pj9QBYszMOm3X7IWCwr7YMawtFZ3H0VLJ2tCCCEkHCiYiSBBZLA4PK1up/ZijGF/uRFri8qxvaQuoEovB2DigGQsLMxGQW5CqwFKtHWyJoQQQsKBgplu1nI7td3d+jISIPVQ+vpoDdYWlaOk1hJwXKuUYdbIDMwfk43sRE2r9+PtZG3QKKK2CWR36RPPo8zceuDY8jxCCCGxISaCmVdffRXPPfccqqqqMHr0aLz88ssYPz62Ss1faDt1S/UWJzbsq8An+yrRZHcHHM9KUGNBQTZmjshAXBs7j2Khk3V3+9t1F+H6N3e16zxCCCGxIeqDmf/+979YvHgx/vnPf2LChAlYsWIFZs6ciWPHjiEtLS3Sw2uTWxBhdXpgdrS+nbqlo1VSw8etx2rhCdLxsTA3AQsL+2DCgCS/ho/ni6VO1t3torxkaBQ87EG2rntpFDwuykvuxlERQgjpDI61tc4RBSZMmICLLroIr7zyCgBAFEXk5OTg/vvvx2OPPXbB3zeZTDAYDDAajdDr9V09XIgig9UlBTCtbaduySOI+K6kDh8VleNQhSnguFLO44ph6VhQmI3+KXEXvL+e3sk6HHaU1OHWf++GO0jAqOA5vH37eEwemBKBkRFCCPHqyPU7qmdmXC4X9uzZgyVLlvhu43keM2bMwM6dO4P+jtPphNPp9P1sMgUGCF3B7pL6IlmdbefBeBntbny2vxLriytQa3EGHE/RKTFvTDbmjMqEQXvh+i9xKimI6emdrMNh8sAUvH37eLy85Tj2nG2CR2CQyziMzUnA/ZcPpkCGEEJiTFQHM3V1dRAEAenp6X63p6en4+jRo0F/Z9myZVi6dGl3DA8eQYTJIdWEaWs7dUun6qxYW1SOr45UwxlkB9PwTD0WFmZjyqCUdi0RUSfr0EwemIKJA5JxqMKEBpsLSVolRmTpaZcXIYTEoKgOZkKxZMkSLF682PezyWRCTk5OlzyW1SWgyea64HkiY/j+ZD3WFpWj6ExTwHE5z2H6EKlK79CM9i2FUSfrzuN5DqP6GCI9DEIIIZ0U1cFMSkoKZDIZqqur/W6vrq5GRkZG0N9RqVRQqVTdMbwLsjo92HioCuv2lqOiKbDhY4JGgatHZ+Ka0VlI1rVvzNTJmhBCCPEX1cGMUqnE2LFjsWXLFsybNw+AlAC8ZcsW/PrXv47s4NpQ3mjHur3l2HioCjZXYBLwwFQdFo7NxqVD0tqd40KdrAkhhJDgov7KuHjxYtx6660YN24cxo8fjxUrVsBqteKXv/xlpIfmhzGGvWea8GFRGXadbMD5KcA8B1w8MAULCrORn21odxsB6mRNCCGEtC3qr5DXX389amtr8dRTT6GqqgpjxozBxo0bA5KCI8XhFvDVkRqs21uOU3XWgOM6lRxXjcrAvDHZyDAEb/gYDHWyJoQQQton6uvMdFZX1ZmpNNrx+jen8OGeszA5Ahs+5iZpMb8gG1eOSIemA0m6cp6HQauAXk1NIAkhhPRePabOTLTaXlKHW/69G0KQomvj+ydhYWE2xvZNbLNK7/mokzUhhBASGgpmQlCYm4h4tRxNNqlvklrBY+aIDMwvyEZukrZD98VzHAzNTSCpxgkhhBDScRTMhECjlOHG8blYX1yOa0Zn4aqRmdCpO/ZSUidrQgghJDwoZyZENpcHDrfYrqJ5LVEna0IIIeTCKGemG2iVcrgFd7vPp07WhBBCSNegYKYbUCdrQgghpOtQMNOFqJM1IYQQ0vUomOkC1MmaEEII6T4UzISRWiFDUhx1siaEEEK6EwUzYUCdrAkhhJDIoWCmE5QyHul6NeKoCSQhhBASMXQV7gSaiSGEEEIij7bZEEIIISSmUTBDCCGEkJhGwQwhhBBCYhoFM4QQQgiJaRTMEEIIISSmUTBDCCGEkJhGwQwhhBBCYhoFM4QQQgiJaRTMEEIIISSmUTBDCCGEkJhGwQwhhBBCYhoFM4QQQgiJaRTMEEIIISSmUTBDCCGEkJhGwQwhhBBCYpo80gPoaowxAIDJZIrwSAghhBDSXt7rtvc63pYeH8yYzWYAQE5OToRHQgghhJCOMpvNMBgMbZ7DsfaEPDFMFEVUVFQgPj4eHMdFejgRZzKZkJOTg7Nnz0Kv10d6OBFHr4c/ej0C0Wvij14Pf/R6+Avn68EYg9lsRlZWFni+7ayYHj8zw/M8+vTpE+lhRB29Xk//4bVAr4c/ej0C0Wvij14Pf/R6+AvX63GhGRkvSgAmhBBCSEyjYIYQQgghMY2CmV5GpVLh6aefhkqlivRQogK9Hv7o9QhEr4k/ej380evhL1KvR49PACaEEEJIz0YzM4QQQgiJaRTMEEIIISSmUTBDCCGEkJhGwQwhhBBCYhoFMz1MQ0MDbr75Zuj1eiQkJOCOO+6AxWJp8/z7778fQ4YMgUajQW5uLh544AEYjUa/8ziOC/izZs2arn46IXn11VfRr18/qNVqTJgwAbt3727z/A8++ABDhw6FWq3GqFGj8Pnnn/sdZ4zhqaeeQmZmJjQaDWbMmIETJ0505VMIq468Hq+//jqmTJmCxMREJCYmYsaMGQHn33bbbQHvhVmzZnX10wibjrweb731VsBzVavVfuf0pvfH9OnTg34WzJkzx3dOLL8/vvnmG1x99dXIysoCx3H4+OOPL/g7W7duRWFhIVQqFQYOHIi33nor4JyOfiZFk46+JmvXrsUVV1yB1NRU6PV6TJo0CZs2bfI75w9/+EPAe2To0KGdGygjPcqsWbPY6NGj2ffff8++/fZbNnDgQHbjjTe2ev6BAwfYggUL2IYNG1hJSQnbsmULGzRoEFu4cKHfeQDYqlWrWGVlpe+P3W7v6qfTYWvWrGFKpZL9+9//ZocOHWJ33XUXS0hIYNXV1UHP3759O5PJZOyvf/0rO3z4MHviiSeYQqFgBw4c8J2zfPlyZjAY2Mcff8z27dvHrrnmGta/f/+ofP7n6+jrcdNNN7FXX32V7d27lx05coTddtttzGAwsLKyMt85t956K5s1a5bfe6GhoaG7nlKndPT1WLVqFdPr9X7Ptaqqyu+c3vT+qK+v93stDh48yGQyGVu1apXvnFh+f3z++efs97//PVu7di0DwNatW9fm+SdPnmRarZYtXryYHT58mL388stMJpOxjRs3+s7p6GscbTr6mvzmN79hzz77LNu9ezc7fvw4W7JkCVMoFKyoqMh3ztNPP81GjBjh9x6pra3t1DgpmOlBDh8+zACwH374wXfbF198wTiOY+Xl5e2+n/fff58plUrmdrt9t7XnTRwNxo8fzxYtWuT7WRAElpWVxZYtWxb0/Ouuu47NmTPH77YJEyawe+65hzHGmCiKLCMjgz333HO+401NTUylUrH33nuvC55BeHX09Tifx+Nh8fHx7O233/bdduutt7K5c+eGe6jdoqOvx6pVq5jBYGj1/nr7++OFF15g8fHxzGKx+G6L5fdHS+35zHv00UfZiBEj/G67/vrr2cyZM30/d/Y1jiahXgeGDx/Oli5d6vv56aefZqNHjw7fwBhjtMzUg+zcuRMJCQkYN26c77YZM2aA53ns2rWr3fdjNBqh1+shl/u37lq0aBFSUlIwfvx4/Pvf/25XW/bu5HK5sGfPHsyYMcN3G8/zmDFjBnbu3Bn0d3bu3Ol3PgDMnDnTd/6pU6dQVVXld47BYMCECRNavc9oEcrrcT6bzQa3242kpCS/27du3Yq0tDQMGTIE9913H+rr68M69q4Q6uthsVjQt29f5OTkYO7cuTh06JDvWG9/f7z55pu44YYbEBcX53d7LL4/QnGhz49wvMaxThRFmM3mgM+QEydOICsrCwMGDMDNN9+MM2fOdOpxKJjpQaqqqpCWluZ3m1wuR1JSEqqqqtp1H3V1dfjTn/6Eu+++2+/2P/7xj3j//ffx5ZdfYuHChfjVr36Fl19+OWxjD4e6ujoIgoD09HS/29PT01t9/lVVVW2e7/3/jtxntAjl9Tjf7373O2RlZfl9GM+aNQv/+c9/sGXLFjz77LPYtm0bZs+eDUEQwjr+cAvl9RgyZAj+/e9/Y/369fjf//1fiKKIyZMno6ysDEDvfn/s3r0bBw8exJ133ul3e6y+P0LR2ueHyWSC3W4Py3+Dse7555+HxWLBdddd57ttwoQJeOutt7Bx40asXLkSp06dwpQpU2A2m0N+nB7fNbsneOyxx/Dss8+2ec6RI0c6/Tgmkwlz5szB8OHD8Yc//MHv2JNPPun7e0FBAaxWK5577jk88MADnX5cEp2WL1+ONWvWYOvWrX5JrzfccIPv76NGjUJ+fj7y8vKwdetWXH755ZEYapeZNGkSJk2a5Pt58uTJGDZsGF577TX86U9/iuDIIu/NN9/EqFGjMH78eL/be9P7g7Rt9erVWLp0KdavX+/3RXv27Nm+v+fn52PChAno27cv3n//fdxxxx0hPRbNzMSAhx9+GEeOHGnzz4ABA5CRkYGamhq/3/V4PGhoaEBGRkabj2E2mzFr1izEx8dj3bp1UCgUbZ4/YcIElJWVwel0dvr5hUtKSgpkMhmqq6v9bq+urm71+WdkZLR5vvf/O3Kf0SKU18Pr+eefx/Lly7F582bk5+e3ee6AAQOQkpKCkpKSTo+5K3Xm9fBSKBQoKCjwPdfe+v6wWq1Ys2ZNuy48sfL+CEVrnx96vR4ajSYs77lYtWbNGtx55514//33A5bizpeQkIDBgwd36j1CwUwMSE1NxdChQ9v8o1QqMWnSJDQ1NWHPnj2+3/36668hiiImTJjQ6v2bTCZceeWVUCqV2LBhQ8DW02CKi4uRmJgYVc3VlEolxo4diy1btvhuE0URW7Zs8ft23dKkSZP8zgeAL7/80nd+//79kZGR4XeOyWTCrl27Wr3PaBHK6wEAf/3rX/GnP/0JGzdu9Mu/ak1ZWRnq6+uRmZkZlnF3lVBfj5YEQcCBAwd8z7U3vj8AqZyB0+nEz3/+8ws+Tqy8P0Jxoc+PcLznYtF7772HX/7yl3jvvff8tu23xmKxoLS0tHPvkbCmE5OImzVrFisoKGC7du1i3333HRs0aJDf1uyysjI2ZMgQtmvXLsYYY0ajkU2YMIGNGjWKlZSU+G2V83g8jDHGNmzYwF5//XV24MABduLECfaPf/yDabVa9tRTT0XkObZlzZo1TKVSsbfeeosdPnyY3X333SwhIcG3nfYXv/gFe+yxx3znb9++ncnlcvb888+zI0eOsKeffjro1uyEhAS2fv16tn//fjZ37tyY2nrbkddj+fLlTKlUsg8//NDvvWA2mxljjJnNZvbII4+wnTt3slOnTrGvvvqKFRYWskGDBjGHwxGR59gRHX09li5dyjZt2sRKS0vZnj172A033MDUajU7dOiQ75ze9P7wuuSSS9j1118fcHusvz/MZjPbu3cv27t3LwPA/v73v7O9e/ey06dPM8YYe+yxx9gvfvEL3/nerdm//e1v2ZEjR9irr74adGt2W69xtOvoa/Luu+8yuVzOXn31Vb/PkKamJt85Dz/8MNu6dSs7deoU2759O5sxYwZLSUlhNTU1IY+Tgpkepr6+nt14441Mp9MxvV7PfvnLX/ouRIwxdurUKQaA/d///R9jjLH/+7//YwCC/jl16hRjTNrePWbMGKbT6VhcXBwbPXo0++c//8kEQYjAM7ywl19+meXm5jKlUsnGjx/Pvv/+e9+xadOmsVtvvdXv/Pfff58NHjyYKZVKNmLECPbZZ5/5HRdFkT355JMsPT2dqVQqdvnll7Njx451x1MJi468Hn379g36Xnj66acZY4zZbDZ25ZVXstTUVKZQKFjfvn3ZXXfdFTMfzIx17PV48MEHfeemp6ezq666yq9eBmO96/3BGGNHjx5lANjmzZsD7ivW3x+tfR56X4Nbb72VTZs2LeB3xowZw5RKJRswYIBfzR2vtl7jaNfR12TatGltns+YtH09MzOTKZVKlp2dza6//npWUlLSqXFyjEXZ/lpCCCGEkA6gnBlCCCGExDQKZgghhBAS0yiYIYQQQkhMo2CGEEIIITGNghlCCCGExDQKZgghhBAS0yiYIYQQQkhMo2CGEEIIIR32zTff4Oqrr0ZWVhY4jsPHH3/c4ftgjOH555/H4MGDoVKpkJ2djT//+c8dvh8KZgghPdr27dsxatQoKBQKzJs3D1u3bgXHcWhqaor00Hz69euHFStWRHoYhHSI1WrF6NGj8eqrr4Z8H7/5zW/wxhtv4Pnnn8fRo0exYcOGgE7s7SEPeQSEEBIDFi9ejDFjxuCLL76ATqeDVqtFZWUlDAZDpIdGSEybPXs2Zs+e3epxp9OJ3//+93jvvffQ1NSEkSNH4tlnn8X06dMBAEeOHMHKlStx8OBBDBkyBIDUvDUUNDNDCOnRSktLcdlll6FPnz5ISEiAUqlERkYGOI4Ler4gCBBFsZtHSUjP8+tf/xo7d+7EmjVrsH//fvzP//wPZs2ahRMnTgAAPvnkEwwYMACffvop+vfvj379+uHOO+9EQ0NDhx+LghlCepnp06fjgQcewKOPPoqkpCRkZGTgD3/4g+94U1MT7rzzTqSmpkKv1+Oyyy7Dvn37AABGoxEymQw//vgjAEAURSQlJWHixIm+3//f//1f5OTktGssZWVluPHGG5GUlIS4uDiMGzcOu3bt8h1fuXIl8vLyoFQqMWTIELzzzjt+v89xHN544w3Mnz8fWq0WgwYNwoYNGwAAP/30EziOQ319PW6//XZwHIe33norYJnprbfeQkJCAjZs2IDhw4dDpVLhzJkz6NevH5555hnccsst0Ol06Nu3LzZs2IDa2lrMnTsXOp0O+fn5vtfC67vvvsOUKVOg0WiQk5ODBx54AFar1Xe8pqYGV199NTQaDfr374933323Xa8VIbHkzJkzWLVqFT744ANMmTIFeXl5eOSRR3DJJZdg1apVAICTJ0/i9OnT+OCDD/Cf//wHb731Fvbs2YNrr7224w/YqTaVhJCYM23aNKbX69kf/vAHdvz4cfb2228zjuN8XZBnzJjBrr76avbDDz+w48ePs4cffpglJyez+vp6xhhjhYWF7LnnnmOMMVZcXMySkpKYUqn0dWe/88472c0333zBcZjNZjZgwAA2ZcoU9u2337ITJ06w//73v2zHjh2MMcbWrl3LFAoFe/XVV9mxY8fY3/72NyaTydjXX3/tuw8ArE+fPmz16tXsxIkT7IEHHmA6nY7V19czj8fDKisrmV6vZytWrGCVlZXMZrP5ugA3NjYyxhhbtWoVUygUbPLkyWz79u3s6NGjzGq1sr59+7KkpCT2z3/+kx0/fpzdd999TK/Xs1mzZrH333+fHTt2jM2bN48NGzaMiaLIGGOspKSExcXFsRdeeIEdP36cbd++nRUUFLDbbrvNN+bZs2ez0aNHs507d7Iff/yRTZ48mWk0GvbCCy907h+WkAgCwNatW+f7+dNPP2UAWFxcnN8fuVzOrrvuOsYYY3fddRcD4Ndlfs+ePQwAO3r0aMcePyzPghASM6ZNm8YuueQSv9suuugi9rvf/Y59++23TK/XM4fD4Xc8Ly+Pvfbaa4wxxhYvXszmzJnDGGNsxYoV7Prrr2ejR49mX3zxBWOMsYEDB7J//etfFxzHa6+9xuLj431B0vkmT57M7rrrLr/b/ud//oddddVVvp8BsCeeeML3s8ViYQB8Y2GMMYPBwFatWuX7OVgwA4AVFxf7PVbfvn3Zz3/+c9/PlZWVDAB78sknfbft3LmTAWCVlZWMMcbuuOMOdvfdd/vdz7fffst4nmd2u50dO3aMAWC7d+/2HT9y5AgDQMEMiWnnBzNr1qxhMpmMHT16lJ04ccLvj/e/l6eeeorJ5XK/+7HZbAyA78tVe1ECMCG9UH5+vt/PmZmZqKmpwb59+2CxWJCcnOx33G63o7S0FAAwbdo0vPnmmxAEAdu2bcOVV16JjIwMbN26Ffn5+SgpKfEl+LWluLgYBQUFSEpKCnr8yJEjuPvuu/1uu/jii/Hiiy+2+lzi4uKg1+tRU1NzwcdvSalUBrwm5993eno6AGDUqFEBt9XU1CAjIwP79u3D/v37/ZaOGGMQRRGnTp3C8ePHIZfLMXbsWN/xoUOHIiEhoUPjJSTaFRQUQBAE1NTUYMqUKUHPufjii+HxeFBaWoq8vDwAwPHjxwEAffv27dDjUTBDSC+kUCj8fuY4DqIowmKxIDMzE1u3bg34He8Fd+rUqTCbzSgqKsI333yDv/zlL8jIyMDy5csxevRoZGVlYdCgQRccg0ajCcdTafW5dIRGowmaENzyvr3Hg93mfTyLxYJ77rkHDzzwQMB95ebm+j6oCekJLBYLSkpKfD+fOnUKxcXFSEpKwuDBg3HzzTfjlltuwd/+9jcUFBSgtrYWW7ZsQX5+PubMmYMZM2agsLAQt99+O1asWAFRFLFo0SJcccUVGDx4cIfGQgnAhBCfwsJCVFVVQS6XY+DAgX5/UlJSAEhBTX5+Pl555RUoFAoMHToUU6dOxd69e/Hpp59i2rRp7Xqs/Px8FBcXt7pzYdiwYdi+fbvfbdu3b8fw4cM79yS7UGFhIQ4fPhzw2g0cOBBKpRJDhw6Fx+PBnj17fL9z7NixqKp5Q0h7/fjjjygoKEBBQQEAqQxCQUEBnnrqKQDAqlWrcMstt+Dhhx/GkCFDMG/ePPzwww/Izc0FAPA8j08++QQpKSmYOnUq5syZg2HDhmHNmjUdHgvNzBBCfGbMmIFJkyZh3rx5+Otf/4rBgwejoqICn332GebPn49x48YBkHZEvfzyy75dB0lJSRg2bBj++9//truA1o033oi//OUvmDdvHpYtW4bMzEzs3bsXWVlZmDRpEn7729/iuuuuQ0FBAWbMmIFPPvkEa9euxVdffdVlz7+zfve732HixIn49a9/jTvvvBNxcXE4fPgwvvzyS7zyyisYMmQIZs2ahXvuuQcrV66EXC7Hgw8+GLZZKkK60/Tp0yGlywSnUCiwdOlSLF26tNVzsrKy8NFHH3V6LDQzQwjx4TgOn3/+OaZOnYpf/vKXGDx4MG644QacPn3alx8CSHkzgiD45cZMnz494La2KJVKbN68GWlpabjqqqswatQoLF++HDKZDAAwb948vPjii3j++ecxYsQIvPbaa1i1alW77z8S8vPzsW3bNhw/fhxTpkzxfUvNysrynbNq1SpkZWVh2rRpWLBgAe6++26kpaVFcNSExD6OtRVWEUIIIYREOZqZIYQQQkhMo2CGENIl/vKXv0Cn0wX901Y/F0II6ShaZiKEdImGhoZWdyppNBpkZ2d384gIIT0VBTOEEEIIiWm0zEQIIYSQmEbBDCGEEEJiGgUzhBBCCIlpFMwQQgghJKZRMEMIIYSQmEbBDCGEEEJiGgUzhBBCCIlp/w8OnJknZSEmoQAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -606,7 +597,7 @@ }, { "data": { - "image/png": "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aW1tjZ9z8803izVr1ojdu3eLjz76SMyfP18UFRWJxsbGlMfJYGaAaW5uFhdffLHIy8sTdrtdXHHFFfELkRBC7N69WwAQ//nPf4QQQvznP/8RALr82r17txAiUt593HHHiby8PGG1WsW0adPEH//4R6GqagYe4ZE98sgjYuTIkcJgMIgTTzxRfPzxx/Hb5s2bJy677LKE81988UUxceJEYTAYxOTJk8U///nPhNs1TRN33nmnKCkpEUajUZx++uli+/btR+OhpEVvno9Ro0Z1+Vq4++67hRBCeL1eccYZZ4ihQ4cKvV4vRo0aJa6++uqceWMWonfPx4033hg/t6SkRJx11lkJ/TKEGFyvDyGE2LZtmwAg3n333U73leuvj+7eD2PPwWWXXSbmzZvX6WeOO+44YTAYxNixYxN67sT09Bxnu94+J/PmzevxfCEi5etlZWXCYDCIYcOGiQsvvFBUV1f3aZzcNZuIiIhyGnNmiIiIKKcxmCEiIqKcxmCGiIiIchqDGSIiIsppDGaIiIgopzGYISIiopzGYIaIiIhyGoMZIiIi6rX3338f55xzDsrLyyFJEl599dVe34cQAg8++CAmTpwIo9GIYcOG4b777uv1/TCYIaIB7aOPPsKUKVOg1+uxaNEirFmzBpIkobW1NdNDixs9ejRWrFiR6WEQ9UpbWxumTZuGRx99NOX7+PnPf44nn3wSDz74ILZt24bXX3+9007sydClPAIiohywZMkSHHfccXjrrbeQl5cHi8WCuro6OByOTA+NKKedeeaZOPPMM7u9PRAI4Pbbb8dzzz2H1tZWHHvssfjNb36DU089FQBQVVWFlStX4uuvv8akSZMARDZvTQVnZohoQKupqcFpp52G4cOHIz8/HwaDAaWlpZAkqcvzVVWFpmlHeZREA8/PfvYzrF+/Hs8//zy++uor/Nd//RcWLlyInTt3AgDeeOMNjB07Fv/4xz8wZswYjB49GldddRVaWlp6/bsYzBANMqeeeipuuOEG/PKXv0RhYSFKS0txzz33xG9vbW3FVVddhaFDh8Jut+O0007Dl19+CQBwOp1QFAWff/45AEDTNBQWFmLWrFnxn/+///s/jBgxIqmx7N+/HxdffDEKCwthtVoxY8YMfPLJJ/HbV65ciXHjxsFgMGDSpEn461//mvDzkiThySefxHnnnQeLxYIJEybg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", + "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", "text/plain": [ "
" ] @@ -642,7 +633,7 @@ }, { "data": { - "image/png": "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RMwWoZGkxm8cq1atwpw5cwD4J81btGgR3nzzTbhcLkybNg1/+MMfmmxmOpvdbofVaoXNZmOTExERkUa05v4d02SmIzCZISIi0p7W3L87RQdgIiIiokgxmSEiIiJNYzJDREREmsZkhoiIiDSNyQwRERFpGpMZIiIi0jQmM0RERKRpTGaIiIhI05jMEBERkaYxmSEiIiJNYzJDREREmhZxMlNZWYlXX30VixcvRkVFBQBgx44dOH78eNSCIyIiImqJLpIXffvtt5gyZQqsVit++OEHzJs3D8nJyXjnnXdw5MgRvPbaa9GOk4iIiKhREdXMLFy4EHPmzMGhQ4dgMpmC2y+77DJ89tlnUQuOiIiIqCURJTPffPMN7rjjjgbbe/fujdLS0jYHRURERBSuiJIZo9EIu93eYPvBgwfRs2fPNgdFREREFK6Ikpkrr7wSjz76KDweDwBAkiQcOXIE9913H6655pqoBkhERETUnIiSmWeffRYOhwNpaWmoqanBpEmTMGjQICQmJuJ///d/ox0jERERUZMiGs1ktVrxySefYPPmzdi9ezccDgfOO+88TJkyJdrxERERETUromQmYPz48Rg/fjwA/7wzRERERB0tomamJ598En//+9+Dz6+99lqkpKSgd+/e2L17d9SCIyIiImpJRMnMH//4R2RlZQEAPvnkE3zyySf48MMPMWPGDPz617+OaoBEREREzYmomam0tDSYzLz//vu49tprMXXqVPTv3x9jxoyJaoBEREREzYmoZqZHjx44evQoAGD9+vXBjr9CCPh8vuhFR0RERNSCiGpmrr76atxwww0YPHgwysvLMWPGDADAzp07MWjQoKgGSERERNSciJKZ559/Hv3798fRo0fx1FNPISEhAQBQUlKCX/7yl1ENkIiIiKg5khBCxDqI9mS322G1WmGz2WCxWGIdDhEREYWhNffvsGtm1q1bhxkzZkCv12PdunXNlr3yyivDPSwRERFRm4RdMyPLMkpLS5GWlgZZbrrfsCRJnaoTMGtmiIiItKddamZUVW3070RERESxFNHQbCIiIqLOIqLRTI8++miz+x9++OGIgiEiIiJqrYiSmbVr14Y893g8KCoqgk6nQ3Z2NpMZIiIi6jARJTM7d+5ssM1ut2POnDmYNWtWm4MiIiIiClfU+sxYLBYsW7YMDz30ULQOSURERNSiqHYA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", + "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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", "text/plain": [ "
" ] diff --git a/noxfile.py b/noxfile.py index 506e935c7d3..6b369954807 100644 --- a/noxfile.py +++ b/noxfile.py @@ -556,22 +556,11 @@ def prerelease(session: nox.sessions.Session, tests_path): "--prefer-binary", "--pre", "--upgrade", - # TODO(shobs): Remove excluding version 2.1.4 after - # https://github.com/pandas-dev/pandas/issues/56463 is resolved. - # - # TODO(shobs): Remove excluding version 2.2.0rc0 after - # https://github.com/pandas-dev/pandas/issues/56646 and - # https://github.com/pandas-dev/pandas/issues/56651 are resolved. - # - # TODO(shobs): Remove excluding version 2.2.0 after - # https://github.com/googleapis/python-bigquery-dataframes/issues/341 - # https://github.com/googleapis/python-bigquery-dataframes/issues/337 - # are resolved - # # We exclude each version individually so that we can continue to test # some prerelease packages. See: # https://github.com/googleapis/python-bigquery-dataframes/pull/268#discussion_r1423205172 - "pandas!=2.1.4, !=2.2.0rc0, !=2.2.0, !=2.2.1", + # "pandas!=2.1.4, !=2.2.0rc0, !=2.2.0, !=2.2.1", + "pandas", ) already_installed.add("pandas") diff --git a/setup.py b/setup.py index 3b2dc53bd64..768fac530cc 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ "google-cloud-storage >=2.0.0", "ibis-framework[bigquery] >=8.0.0,<9.0.0dev", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. - "pandas >=1.5.0,<2.1.4", + "pandas >=1.5.0", "pydata-google-auth >=1.8.2", "requests >=2.27.1", "scikit-learn >=1.2.2", diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 6d8e9abe5b6..a108ff4a8e9 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -35,6 +35,7 @@ import test_utils.prefixer import bigframes +import bigframes.dataframe import tests.system.utils # Use this to control the number of cloud functions being deleted in a single diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index f0b138c110e..f1ae56cb3ca 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -118,6 +118,7 @@ def bq_cf_connection() -> str: def test_remote_function_multiply_with_ibis( session, scalars_table_id, + bigquery_client, ibis_client, dataset_id, bq_cf_connection, @@ -134,20 +135,22 @@ def test_remote_function_multiply_with_ibis( def multiply(x, y): return x * y - project_id, dataset_name, table_name = scalars_table_id.split(".") + _, dataset_name, table_name = scalars_table_id.split(".") if not ibis_client.dataset: ibis_client.dataset = dataset_name col_name = "int64_col" table = ibis_client.tables[table_name] table = table.filter(table[col_name].notnull()).order_by("rowindex").head(10) - pandas_df_orig = table.execute() + sql = table.compile() + pandas_df_orig = bigquery_client.query(sql).to_dataframe() col = table[col_name] col_2x = multiply(col, 2).name("int64_col_2x") col_square = multiply(col, col).name("int64_col_square") table = table.mutate([col_2x, col_square]) - pandas_df_new = table.execute() + sql = table.compile() + pandas_df_new = bigquery_client.query(sql).to_dataframe() pandas.testing.assert_series_equal( pandas_df_orig[col_name] * 2, @@ -163,7 +166,7 @@ def multiply(x, y): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, session.cloudfunctionsclient, multiply + bigquery_client, session.cloudfunctionsclient, multiply ) @@ -171,6 +174,7 @@ def multiply(x, y): def test_remote_function_stringify_with_ibis( session, scalars_table_id, + bigquery_client, ibis_client, dataset_id, bq_cf_connection, @@ -187,19 +191,21 @@ def test_remote_function_stringify_with_ibis( def stringify(x): return f"I got {x}" - project_id, dataset_name, table_name = scalars_table_id.split(".") + _, dataset_name, table_name = scalars_table_id.split(".") if not ibis_client.dataset: ibis_client.dataset = dataset_name col_name = "int64_col" table = ibis_client.tables[table_name] table = table.filter(table[col_name].notnull()).order_by("rowindex").head(10) - pandas_df_orig = table.execute() + sql = table.compile() + pandas_df_orig = bigquery_client.query(sql).to_dataframe() col = table[col_name] col_2x = stringify(col).name("int64_str_col") table = table.mutate([col_2x]) - pandas_df_new = table.execute() + sql = table.compile() + pandas_df_new = bigquery_client.query(sql).to_dataframe() pandas.testing.assert_series_equal( pandas_df_orig[col_name].apply(lambda x: f"I got {x}"), @@ -209,7 +215,7 @@ def stringify(x): finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( - session.bqclient, session.cloudfunctionsclient, stringify + bigquery_client, session.cloudfunctionsclient, stringify ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 5e034e8e836..93bfd8d35c6 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2500,6 +2500,8 @@ def test_df_pivot(scalars_dfs, values, index, columns): pd_result = scalars_pandas_df.pivot(values=values, index=index, columns=columns) # Pandas produces NaN, where bq dataframes produces pd.NA + bf_result = bf_result.fillna(float("nan")) + pd_result = pd_result.fillna(float("nan")) pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) @@ -4026,7 +4028,7 @@ def test_to_pandas_downsampling_option_override(session): total_memory_bytes = df.memory_usage(deep=True).sum() total_memory_mb = total_memory_bytes / (1024 * 1024) - assert total_memory_mb == pytest.approx(download_size, rel=0.3) + assert total_memory_mb == pytest.approx(download_size, rel=0.5) def test_to_gbq_and_create_dataset(session, scalars_df_index, dataset_id_not_created): diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index b38dcaf5d1a..e7ecbedfc2e 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -228,7 +228,9 @@ def test_dataframe_groupby_multi_sum( (lambda x: x.cumsum(numeric_only=True)), (lambda x: x.cummax(numeric_only=True)), (lambda x: x.cummin(numeric_only=True)), - (lambda x: x.cumprod()), + # pandas 2.2 uses floating point for cumulative product even for + # integer inputs. + (lambda x: x.cumprod().astype("Float64")), (lambda x: x.shift(periods=2)), ], ids=[ diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 4a293526df8..f348758c935 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -45,8 +45,9 @@ def test_read_pandas_multi_index_axes(): [[1, 2], [3, 4]], index=index, columns=columns, dtype=pandas.Int64Dtype() ) bf_df = bpd.DataFrame(pandas_df) + bf_df_computed = bf_df.to_pandas() - pandas.testing.assert_frame_equal(bf_df.to_pandas(), pandas_df) + pandas.testing.assert_frame_equal(bf_df_computed, pandas_df) # Row Multi-index tests diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 010ad323e78..8847753e881 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1497,7 +1497,8 @@ def test_groupby_prod(scalars_dfs): (lambda x: x.cumcount()), (lambda x: x.cummin()), (lambda x: x.cummax()), - (lambda x: x.cumprod()), + # Pandas 2.2 casts to cumprod to float. + (lambda x: x.cumprod().astype("Float64")), (lambda x: x.diff()), (lambda x: x.shift(2)), (lambda x: x.shift(-2)), @@ -1521,7 +1522,7 @@ def test_groupby_window_ops(scalars_df_index, scalars_pandas_df_index, operator) ).to_pandas() pd_series = operator( scalars_pandas_df_index[col_name].groupby(scalars_pandas_df_index[group_key]) - ).astype(pd.Int64Dtype()) + ).astype(bf_series.dtype) pd.testing.assert_series_equal( pd_series, bf_series, @@ -2709,7 +2710,14 @@ def foo(x): ("timestamp_col", "time64[us][pyarrow]"), ("timestamp_col", pd.ArrowDtype(pa.timestamp("us"))), ("datetime_col", "date32[day][pyarrow]"), - ("datetime_col", "string[pyarrow]"), + pytest.param( + "datetime_col", + "string[pyarrow]", + marks=pytest.mark.skipif( + pd.__version__.startswith("2.2"), + reason="pandas 2.2 uses T as date/time separator whereas earlier versions use space", + ), + ), ("datetime_col", "time64[us][pyarrow]"), ("datetime_col", pd.ArrowDtype(pa.timestamp("us", tz="UTC"))), ("date_col", "string[pyarrow]"), @@ -3294,7 +3302,10 @@ def test_apply_lambda(scalars_dfs, col, lambda_): bf_result = bf_col.apply(lambda_, by_row=False).to_pandas() pd_col = scalars_pandas_df[col] - pd_result = pd_col.apply(lambda_) + if pd.__version__.startswith("2.2"): + pd_result = pd_col.apply(lambda_, by_row=False) + else: + pd_result = pd_col.apply(lambda_) # ignore dtype check, which are Int64 and object respectively assert_series_equal(bf_result, pd_result, check_dtype=False) @@ -3345,7 +3356,11 @@ def foo(x): bf_result = bf_col.apply(foo, by_row=False).to_pandas() pd_col = scalars_pandas_df["int64_col"] - pd_result = pd_col.apply(foo) + + if pd.__version__.startswith("2.2"): + pd_result = pd_col.apply(foo, by_row=False) + else: + pd_result = pd_col.apply(foo) # ignore dtype check, which are Int64 and object respectively assert_series_equal(bf_result, pd_result, check_dtype=False) diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index d6af223456f..535b748345e 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -21,6 +21,7 @@ import pytest import bigframes.core.global_session +import bigframes.dataframe import bigframes.pandas as bpd import bigframes.session @@ -67,7 +68,11 @@ def test_method_matches_session(method_name: str): # Add `eval_str = True` so that deferred annotations are turned into their # corresponding type objects. Need Python 3.10 for eval_str parameter. - session_signature = inspect.signature(session_method, eval_str=True) + session_signature = inspect.signature( + session_method, + eval_str=True, + globals={**vars(bigframes.session), **{"dataframe": bigframes.dataframe}}, + ) pandas_signature = inspect.signature(pandas_method, eval_str=True) assert [ # Kind includes position, which will be an offset. diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index bdc220868ab..876a85f1a51 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -13,7 +13,7 @@ from typing import Hashable, Iterable, Literal, Mapping, Optional, Sequence, Union -from bigframes_vendored.pandas.core.generic import NDFrame +import bigframes_vendored.pandas.core.generic as generic import numpy as np import pandas as pd @@ -23,7 +23,7 @@ # DataFrame class -class DataFrame(NDFrame): +class DataFrame(generic.NDFrame): """Two-dimensional, size-mutable, potentially heterogeneous tabular data. Data structure also contains labeled axes (rows and columns). @@ -592,7 +592,7 @@ def to_records( >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) >>> df.to_records() rec.array([(0, 1, 3), (1, 2, 4)], - dtype=[('index', 'O'), ('col1', 'O'), ('col2', 'O')]) + dtype=[('index', ' DataFrame: def diff( self, periods: int = 1, - ) -> NDFrame: + ) -> generic.NDFrame: """First discrete difference of element. Calculates the difference of a DataFrame element compared with another @@ -4750,7 +4750,7 @@ def index(self): >>> df.index # doctest: +ELLIPSIS Index([10, 20, 30], dtype='Int64') >>> df.index.values - array([10, 20, 30], dtype=object) + array([10, 20, 30]) Let's try setting a new index for the dataframe and see that reflect via ``index`` property. diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 1477640c299..04cc3990a4e 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -6,7 +6,7 @@ from bigframes_vendored.pandas.core import indexing import bigframes_vendored.pandas.core.common as common -from bigframes import constants +import bigframes.constants as constants if TYPE_CHECKING: from bigframes_vendored.pandas.pandas._typing import T diff --git a/third_party/bigframes_vendored/pandas/core/indexing.py b/third_party/bigframes_vendored/pandas/core/indexing.py index fae5d6261f2..3c7f8a6c9fb 100644 --- a/third_party/bigframes_vendored/pandas/core/indexing.py +++ b/third_party/bigframes_vendored/pandas/core/indexing.py @@ -1,6 +1,6 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexing.py -from bigframes import constants +import bigframes.constants as constants class IndexingMixin: diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index d087df0ef98..0aebd0660f7 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -71,7 +71,7 @@ def index(self): >>> s.index # doctest: +ELLIPSIS Index([10, 20, 30], dtype='Int64') >>> s.index.values - array([10, 20, 30], dtype=object) + array([10, 20, 30]) Let's try setting a multi-index case reflect via ``index`` property. @@ -3305,7 +3305,7 @@ def values(self): >>> bpd.options.display.progress_bar = None >>> bpd.Series([1, 2, 3]).values - array([1, 2, 3], dtype=object) + array([1, 2, 3]) >>> bpd.Series(list('aabc')).values array(['a', 'a', 'b', 'c'], dtype=object) From 97afad96f80c1815db8ad34f0ff62095631036c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 22 Mar 2024 17:00:50 -0500 Subject: [PATCH 413/921] docs: update LLM + K-means notebook to handle partial failures (#496) * docs: update LLM + K-means notebook to handle partial failures * filter empty arrays --- .../bq_dataframes_llm_kmeans.ipynb | 1003 ++++++++++++----- 1 file changed, 748 insertions(+), 255 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index d6d819f9e36..61445d85c5b 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -139,7 +139,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -289,7 +289,7 @@ { "data": { "text/html": [ - "Query job 313ed696-37fc-46b3-806e-6041403080d3 is DONE. 2.3 GB processed. Open Job" + "Query job d5778724-6966-42ba-b8a6-2a1865a1184c is DONE. 2.3 GB processed. Open Job" ], "text/plain": [ "" @@ -301,19 +301,7 @@ { "data": { "text/html": [ - "Query job f149ff34-4807-4cba-841f-fb7bf51bbbd6 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 8ed05179-52b2-437d-a709-f651a80de307 is DONE. 4.6 kB processed. Open Job" + "Query job 4d48bf69-571c-4773-8486-0232840597d5 is DONE. 55.1 MB processed. Open Job" ], "text/plain": [ "" @@ -348,39 +336,36 @@ " \n", " \n", " \n", - " 2\n", - " COLLECTION BUREAU OF AMERICA ACCOUNT NO. XXXX...\n", + " 24\n", + " I sent disputed to Transunion, XXXX and XXXX f...\n", " \n", " \n", - " 3\n", - " Despite multiple written requests, the unverif...\n", + " 942\n", + " on XX/XX/2017 I sent XXXX, transunion, XXXX pr...\n", " \n", " \n", - " 6\n", - " Once again you guys have not provided me with ...\n", + " 1193\n", + " On Wednesday, XXXX XXXX , I initiated a wir...\n", " \n", " \n", - " 9\n", - " XX/XX/XXXX {$350.00} I received a outstating d...\n", + " 1292\n", + " Dear Sir or Madam, I am a victim of identity t...\n", " \n", " \n", - " 10\n", - " Im am unable to withdraw money from my account...\n", + " 1377\n", + " For the purpose of this complaint, I will refe...\n", " \n", " \n", "\n", - "

5 rows × 1 columns

\n", - "[5 rows x 1 columns in total]" + "" ], "text/plain": [ - " consumer_complaint_narrative\n", - "2 COLLECTION BUREAU OF AMERICA ACCOUNT NO. XXXX...\n", - "3 Despite multiple written requests, the unverif...\n", - "6 Once again you guys have not provided me with ...\n", - "9 XX/XX/XXXX {$350.00} I received a outstating d...\n", - "10 Im am unable to withdraw money from my account...\n", - "\n", - "[5 rows x 1 columns]" + " consumer_complaint_narrative\n", + "24 I sent disputed to Transunion, XXXX and XXXX f...\n", + "942 on XX/XX/2017 I sent XXXX, transunion, XXXX pr...\n", + "1193 On Wednesday, XXXX XXXX , I initiated a wir...\n", + "1292 Dear Sir or Madam, I am a victim of identity t...\n", + "1377 For the purpose of this complaint, I will refe..." ] }, "execution_count": 7, @@ -390,7 +375,7 @@ ], "source": [ "issues_df = input_df[[\"consumer_complaint_narrative\"]].dropna()\n", - "issues_df.head(n=5) # View the first five complaints" + "issues_df.peek(n=5) # View an arbitrary five complaints" ] }, { @@ -433,7 +418,7 @@ { "data": { "text/html": [ - "Query job bd6b88fc-6e05-4d71-acb1-d5befaced079 is DONE. 0 Bytes processed. Open Job" + "Query job 15b352c2-783c-42b1-bc03-e5772f00381a is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -459,7 +444,7 @@ { "data": { "text/html": [ - "Query job a4d2983a-7967-4ffb-b2b7-2a387f58776b is DONE. 2.3 GB processed. Open Job" + "Query job e2152e81-b736-4a68-a25a-c5eb2b03d734 is DONE. 1.3 GB processed. Open Job" ], "text/plain": [ "" @@ -471,7 +456,7 @@ { "data": { "text/html": [ - "Query job 619b0cc2-4162-44ab-a085-e7cc5c48a02b is DONE. 80.0 kB processed. Open Job" + "Query job b1a3d20b-aee3-424c-a0c5-5b36f1177709 is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -483,7 +468,7 @@ { "data": { "text/html": [ - "Query job 6dc5b3cf-efa6-4350-907e-ab40e3de80aa is DONE. 20.0 kB processed. Open Job" + "Query job 6b2fad50-cbc8-42ea-83c1-b5d3eaac10b9 is DONE. 20.0 kB processed. Open Job" ], "text/plain": [ "" @@ -495,7 +480,349 @@ { "data": { "text/html": [ - "Query job 70f25b9e-2d26-4dc2-9d1f-c24d36e58856 is DONE. 72.0 MB processed. Open Job" + "Query job 31896ae6-fbb5-42fb-98c4-13bd19d1adfa is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 43f04543-f59b-4f1b-8598-c529324904be is DONE. 72.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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
text_embeddingstatisticsml_embed_text_statuscontent
545[ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...{\"token_count\":178,\"truncated\":false}My payments have been approximately {$89.00} w...
614[ 5.40032536e-02 -5.28502129e-02 -5.33268750e-...{\"token_count\":399,\"truncated\":false}Hi, I have contacted Trans Union XXXX XXXX abo...
1236[-5.32836001e-03 -5.84292673e-02 -5.86670786e-...{\"token_count\":129,\"truncated\":false}I have a XXXX XXXX XXXX credit card on my Exp...
1477[ 3.02605387e-02 -4.37121317e-02 -2.70802993e-...{\"token_count\":16,\"truncated\":false}Wrongs information, selling my information to ...
2261[ 2.35723313e-02 -3.73509154e-02 -6.44604117e-...{\"token_count\":33,\"truncated\":false}Please investigate and delete disputed item th...
2361[ 1.04440488e-02 -9.37070698e-03 -7.36323372e-...{\"token_count\":45,\"truncated\":false}By the provisions of the Fair Credit Reporting...
2378[ 3.04989032e-02 -4.08191867e-02 -6.18648790e-...{\"token_count\":892,\"truncated\":false}Since XX/XX/XXXX I have been trying to dispute...
3133[ 0.00152804 -0.04189068 -0.04220504 -0.053740...{\"token_count\":90,\"truncated\":false}Out of the blue I received a debt collection n...
3140[ 3.11435573e-02 -4.44000624e-02 -2.10917685e-...{\"token_count\":372,\"truncated\":false}My wife and I have been sending money to XXXX ...
3322[ 2.75927987e-02 -6.23729872e-03 -3.83295454e-...{\"token_count\":36,\"truncated\":false}Phone calls from Convergent Outsourcing XXXX. ...
3583[ 9.20385588e-03 -3.83387171e-02 -6.46291822e-...{\"token_count\":52,\"truncated\":false}I recently received a copy of my credit report...
4134[-7.04960374e-04 -3.52595337e-02 -1.65264793e-...{\"token_count\":412,\"truncated\":false}I have been sending the creditor what they hav...
4496[ 3.67735326e-02 1.21120387e-03 -5.20942472e-...{\"token_count\":182,\"truncated\":false}This is my second complaint. Their response to...
5260[ 2.07133405e-02 -1.69602726e-02 -5.07124476e-...{\"token_count\":103,\"truncated\":false}XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte...
5400[ 1.44114876e-02 -2.34710164e-02 -6.58538565e-...{\"token_count\":60,\"truncated\":false}Upon checking my XXXX credit report I noticed ...
5425[ 3.10326386e-02 -2.19427086e-02 -6.56386837e-...{\"token_count\":87,\"truncated\":false}Follow up to previous complaint XXXX XXXX XXXX...
6014[ 1.90773793e-02 -2.27493346e-02 -3.27166244e-...{\"token_count\":175,\"truncated\":false}My new XXXX lease was over always paid on time...
8192[ 0.01937891 -0.05466933 -0.06070872 -0.059028...{\"token_count\":131,\"truncated\":false}I have no idea where this account cane from. B...
8240[ 4.34123818e-03 -3.40953320e-02 -4.06381376e-...{\"token_count\":87,\"truncated\":false}I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F...
8720[ 0.03133732 -0.03972461 -0.00178199 -0.035876...{\"token_count\":645,\"truncated\":false}XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum...
8914[ 1.75969116e-02 -2.25022305e-02 -5.70390299e-...{\"token_count\":180,\"truncated\":false}On XX/XX/21 I sent a letter regarding inaccura...
10021[ 5.02460636e-02 -5.25112189e-02 -4.12914790e-...{\"token_count\":30,\"truncated\":false}XX/XX/XXXX and XX/XX/XXXX inaccurate informati...
10327[-0.00979626 -0.04912931 -0.08654705 -0.021063...{\"token_count\":194,\"truncated\":false}When I reviewed my credit report, I discovered...
10345[-0.04292191 -0.02636929 -0.06177032 -0.076520...{\"token_count\":262,\"truncated\":false}U.S. Bank sent two letters containing Visa Deb...
10369[ 2.16020197e-02 -5.62509745e-02 -5.93873672e-...{\"token_count\":77,\"truncated\":false}I requested from XXXX that they reverse the la...
\n", + "

25 rows × 4 columns

\n", + "
[10000 rows x 4 columns in total]" + ], + "text/plain": [ + " text_embedding \\\n", + "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", + "614 [ 5.40032536e-02 -5.28502129e-02 -5.33268750e-... \n", + "1236 [-5.32836001e-03 -5.84292673e-02 -5.86670786e-... \n", + "1477 [ 3.02605387e-02 -4.37121317e-02 -2.70802993e-... \n", + "2261 [ 2.35723313e-02 -3.73509154e-02 -6.44604117e-... \n", + "2361 [ 1.04440488e-02 -9.37070698e-03 -7.36323372e-... \n", + "2378 [ 3.04989032e-02 -4.08191867e-02 -6.18648790e-... \n", + "3133 [ 0.00152804 -0.04189068 -0.04220504 -0.053740... \n", + "3140 [ 3.11435573e-02 -4.44000624e-02 -2.10917685e-... \n", + "3322 [ 2.75927987e-02 -6.23729872e-03 -3.83295454e-... \n", + "3583 [ 9.20385588e-03 -3.83387171e-02 -6.46291822e-... \n", + "4134 [-7.04960374e-04 -3.52595337e-02 -1.65264793e-... \n", + "4496 [ 3.67735326e-02 1.21120387e-03 -5.20942472e-... \n", + "5260 [ 2.07133405e-02 -1.69602726e-02 -5.07124476e-... \n", + "5400 [ 1.44114876e-02 -2.34710164e-02 -6.58538565e-... \n", + "5425 [ 3.10326386e-02 -2.19427086e-02 -6.56386837e-... \n", + "6014 [ 1.90773793e-02 -2.27493346e-02 -3.27166244e-... \n", + "8192 [ 0.01937891 -0.05466933 -0.06070872 -0.059028... \n", + "8240 [ 4.34123818e-03 -3.40953320e-02 -4.06381376e-... \n", + "8720 [ 0.03133732 -0.03972461 -0.00178199 -0.035876... \n", + "8914 [ 1.75969116e-02 -2.25022305e-02 -5.70390299e-... \n", + "10021 [ 5.02460636e-02 -5.25112189e-02 -4.12914790e-... \n", + "10327 [-0.00979626 -0.04912931 -0.08654705 -0.021063... \n", + "10345 [-0.04292191 -0.02636929 -0.06177032 -0.076520... \n", + "10369 [ 2.16020197e-02 -5.62509745e-02 -5.93873672e-... \n", + "\n", + " statistics ml_embed_text_status \\\n", + "545 {\"token_count\":178,\"truncated\":false} \n", + "614 {\"token_count\":399,\"truncated\":false} \n", + "1236 {\"token_count\":129,\"truncated\":false} \n", + "1477 {\"token_count\":16,\"truncated\":false} \n", + "2261 {\"token_count\":33,\"truncated\":false} \n", + "2361 {\"token_count\":45,\"truncated\":false} \n", + "2378 {\"token_count\":892,\"truncated\":false} \n", + "3133 {\"token_count\":90,\"truncated\":false} \n", + "3140 {\"token_count\":372,\"truncated\":false} \n", + "3322 {\"token_count\":36,\"truncated\":false} \n", + "3583 {\"token_count\":52,\"truncated\":false} \n", + "4134 {\"token_count\":412,\"truncated\":false} \n", + "4496 {\"token_count\":182,\"truncated\":false} \n", + "5260 {\"token_count\":103,\"truncated\":false} \n", + "5400 {\"token_count\":60,\"truncated\":false} \n", + "5425 {\"token_count\":87,\"truncated\":false} \n", + "6014 {\"token_count\":175,\"truncated\":false} \n", + "8192 {\"token_count\":131,\"truncated\":false} \n", + "8240 {\"token_count\":87,\"truncated\":false} \n", + "8720 {\"token_count\":645,\"truncated\":false} \n", + "8914 {\"token_count\":180,\"truncated\":false} \n", + "10021 {\"token_count\":30,\"truncated\":false} \n", + "10327 {\"token_count\":194,\"truncated\":false} \n", + "10345 {\"token_count\":262,\"truncated\":false} \n", + "10369 {\"token_count\":77,\"truncated\":false} \n", + "\n", + " content \n", + "545 My payments have been approximately {$89.00} w... \n", + "614 Hi, I have contacted Trans Union XXXX XXXX abo... \n", + "1236 I have a XXXX XXXX XXXX credit card on my Exp... \n", + "1477 Wrongs information, selling my information to ... \n", + "2261 Please investigate and delete disputed item th... \n", + "2361 By the provisions of the Fair Credit Reporting... \n", + "2378 Since XX/XX/XXXX I have been trying to dispute... \n", + "3133 Out of the blue I received a debt collection n... \n", + "3140 My wife and I have been sending money to XXXX ... \n", + "3322 Phone calls from Convergent Outsourcing XXXX. ... \n", + "3583 I recently received a copy of my credit report... \n", + "4134 I have been sending the creditor what they hav... \n", + "4496 This is my second complaint. Their response to... \n", + "5260 XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte... \n", + "5400 Upon checking my XXXX credit report I noticed ... \n", + "5425 Follow up to previous complaint XXXX XXXX XXXX... \n", + "6014 My new XXXX lease was over always paid on time... \n", + "8192 I have no idea where this account cane from. B... \n", + "8240 I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F... \n", + "8720 XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum... \n", + "8914 On XX/XX/21 I sent a letter regarding inaccura... \n", + "10021 XX/XX/XXXX and XX/XX/XXXX inaccurate informati... \n", + "10327 When I reviewed my credit report, I discovered... \n", + "10345 U.S. Bank sent two letters containing Visa Deb... \n", + "10369 I requested from XXXX that they reverse the la... \n", + "...\n", + "\n", + "[10000 rows x 4 columns]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Will take ~3 minutes to compute the embeddings\n", + "predicted_embeddings = model.predict(downsampled_issues_df)\n", + "# Notice the lists of numbers that are our text embeddings for each complaint\n", + "predicted_embeddings" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model may have encountered errors while calculating embeddings for some rows. Filter out the errored rows before training the model. Alternatively, select these rows and retry the embeddings." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job c78e1040-2a57-42f6-8fdb-5b9524846259 is DONE. 72.1 MB processed. Open Job" ], "text/plain": [ "" @@ -507,7 +834,7 @@ { "data": { "text/html": [ - "Query job 2b2f366b-b398-4817-a610-cf71c64a8349 is DONE. 0 Bytes processed. Open Job" + "Query job 0986541b-3941-4387-b813-8888f53d149e is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -519,7 +846,7 @@ { "data": { "text/html": [ - "Query job b0e71a63-1365-4fc5-a764-b25b62387fd1 is DONE. 36.1 kB processed. Open Job" + "Query job 754aadd2-fee6-495c-acef-506f4e13c062 is DONE. 72.6 MB processed. Open Job" ], "text/plain": [ "" @@ -557,80 +884,285 @@ " \n", " \n", " \n", - " 251\n", - " [ 2.20562406e-02 -3.51827666e-02 7.63384486e-...\n", - " {\"token_count\":145,\"truncated\":false}\n", + " 545\n", + " [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...\n", + " {\"token_count\":178,\"truncated\":false}\n", " \n", - " A purse was purchased from XXXX XXXX on XX/XX/...\n", + " My payments have been approximately {$89.00} w...\n", " \n", " \n", - " 300\n", - " [ 0.01977486 -0.04289974 -0.05289588 -0.027267...\n", - " {\"token_count\":498,\"truncated\":false}\n", + " 614\n", + " [ 5.40032536e-02 -5.28502129e-02 -5.33268750e-...\n", + " {\"token_count\":399,\"truncated\":false}\n", " \n", - " XXXX XXXX XXXXXXXX has reported on my credit r...\n", + " Hi, I have contacted Trans Union XXXX XXXX abo...\n", " \n", " \n", - " 414\n", - " [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-...\n", - " {\"token_count\":263,\"truncated\":false}\n", + " 1236\n", + " [-5.32836001e-03 -5.84292673e-02 -5.86670786e-...\n", + " {\"token_count\":129,\"truncated\":false}\n", " \n", - " I have tried to dispute US BKPT CT TX XXXXXXXX...\n", + " I have a XXXX XXXX XXXX credit card on my Exp...\n", " \n", " \n", - " 493\n", - " [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-...\n", - " {\"token_count\":395,\"truncated\":false}\n", + " 1477\n", + " [ 3.02605387e-02 -4.37121317e-02 -2.70802993e-...\n", + " {\"token_count\":16,\"truncated\":false}\n", " \n", - " Discover Student Loan has been holding onto {$...\n", + " Wrongs information, selling my information to ...\n", " \n", " \n", - " 545\n", - " [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...\n", - " {\"token_count\":178,\"truncated\":false}\n", + " 2261\n", + " [ 2.35723313e-02 -3.73509154e-02 -6.44604117e-...\n", + " {\"token_count\":33,\"truncated\":false}\n", " \n", - " My payments have been approximately {$89.00} w...\n", + " Please investigate and delete disputed item th...\n", + " \n", + " \n", + " 2361\n", + " [ 1.04440488e-02 -9.37070698e-03 -7.36323372e-...\n", + " {\"token_count\":45,\"truncated\":false}\n", + " \n", + " By the provisions of the Fair Credit Reporting...\n", + " \n", + " \n", + " 2378\n", + " [ 3.04989032e-02 -4.08191867e-02 -6.18648790e-...\n", + " {\"token_count\":892,\"truncated\":false}\n", + " \n", + " Since XX/XX/XXXX I have been trying to dispute...\n", + " \n", + " \n", + " 3133\n", + " [ 0.00152804 -0.04189068 -0.04220504 -0.053740...\n", + " {\"token_count\":90,\"truncated\":false}\n", + " \n", + " Out of the blue I received a debt collection n...\n", + " \n", + " \n", + " 3140\n", + " [ 3.11435573e-02 -4.44000624e-02 -2.10917685e-...\n", + " {\"token_count\":372,\"truncated\":false}\n", + " \n", + " My wife and I have been sending money to XXXX ...\n", + " \n", + " \n", + " 3322\n", + " [ 2.75927987e-02 -6.23729872e-03 -3.83295454e-...\n", + " {\"token_count\":36,\"truncated\":false}\n", + " \n", + " Phone calls from Convergent Outsourcing XXXX. ...\n", + " \n", + " \n", + " 3583\n", + " [ 9.20385588e-03 -3.83387171e-02 -6.46291822e-...\n", + " {\"token_count\":52,\"truncated\":false}\n", + " \n", + " I recently received a copy of my credit report...\n", + " \n", + " \n", + " 4134\n", + " [-7.04960374e-04 -3.52595337e-02 -1.65264793e-...\n", + " {\"token_count\":412,\"truncated\":false}\n", + " \n", + " I have been sending the creditor what they hav...\n", + " \n", + " \n", + " 4496\n", + " [ 3.67735326e-02 1.21120387e-03 -5.20942472e-...\n", + " {\"token_count\":182,\"truncated\":false}\n", + " \n", + " This is my second complaint. Their response to...\n", + " \n", + " \n", + " 5260\n", + " [ 2.07133405e-02 -1.69602726e-02 -5.07124476e-...\n", + " {\"token_count\":103,\"truncated\":false}\n", + " \n", + " XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte...\n", + " \n", + " \n", + " 5400\n", + " [ 1.44114876e-02 -2.34710164e-02 -6.58538565e-...\n", + " {\"token_count\":60,\"truncated\":false}\n", + " \n", + " Upon checking my XXXX credit report I noticed ...\n", + " \n", + " \n", + " 5425\n", + " [ 3.10326386e-02 -2.19427086e-02 -6.56386837e-...\n", + " {\"token_count\":87,\"truncated\":false}\n", + " \n", + " Follow up to previous complaint XXXX XXXX XXXX...\n", + " \n", + " \n", + " 6014\n", + " [ 1.90773793e-02 -2.27493346e-02 -3.27166244e-...\n", + " {\"token_count\":175,\"truncated\":false}\n", + " \n", + " My new XXXX lease was over always paid on time...\n", + " \n", + " \n", + " 8192\n", + " [ 0.01937891 -0.05466933 -0.06070872 -0.059028...\n", + " {\"token_count\":131,\"truncated\":false}\n", + " \n", + " I have no idea where this account cane from. B...\n", + " \n", + " \n", + " 8240\n", + " [ 4.34123818e-03 -3.40953320e-02 -4.06381376e-...\n", + " {\"token_count\":87,\"truncated\":false}\n", + " \n", + " I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F...\n", + " \n", + " \n", + " 8720\n", + " [ 0.03133732 -0.03972461 -0.00178199 -0.035876...\n", + " {\"token_count\":645,\"truncated\":false}\n", + " \n", + " XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum...\n", + " \n", + " \n", + " 8914\n", + " [ 1.75969116e-02 -2.25022305e-02 -5.70390299e-...\n", + " {\"token_count\":180,\"truncated\":false}\n", + " \n", + " On XX/XX/21 I sent a letter regarding inaccura...\n", + " \n", + " \n", + " 10021\n", + " [ 5.02460636e-02 -5.25112189e-02 -4.12914790e-...\n", + " {\"token_count\":30,\"truncated\":false}\n", + " \n", + " XX/XX/XXXX and XX/XX/XXXX inaccurate informati...\n", + " \n", + " \n", + " 10327\n", + " [-0.00979626 -0.04912931 -0.08654705 -0.021063...\n", + " {\"token_count\":194,\"truncated\":false}\n", + " \n", + " When I reviewed my credit report, I discovered...\n", + " \n", + " \n", + " 10345\n", + " [-0.04292191 -0.02636929 -0.06177032 -0.076520...\n", + " {\"token_count\":262,\"truncated\":false}\n", + " \n", + " U.S. Bank sent two letters containing Visa Deb...\n", + " \n", + " \n", + " 10369\n", + " [ 2.16020197e-02 -5.62509745e-02 -5.93873672e-...\n", + " {\"token_count\":77,\"truncated\":false}\n", + " \n", + " I requested from XXXX that they reverse the la...\n", " \n", " \n", "\n", - "

5 rows × 4 columns

\n", - "[5 rows x 4 columns in total]" + "

25 rows × 4 columns

\n", + "[10000 rows x 4 columns in total]" ], "text/plain": [ - " text_embedding \\\n", - "251 [ 2.20562406e-02 -3.51827666e-02 7.63384486e-... \n", - "300 [ 0.01977486 -0.04289974 -0.05289588 -0.027267... \n", - "414 [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-... \n", - "493 [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-... \n", - "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", + " text_embedding \\\n", + "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", + "614 [ 5.40032536e-02 -5.28502129e-02 -5.33268750e-... \n", + "1236 [-5.32836001e-03 -5.84292673e-02 -5.86670786e-... \n", + "1477 [ 3.02605387e-02 -4.37121317e-02 -2.70802993e-... \n", + "2261 [ 2.35723313e-02 -3.73509154e-02 -6.44604117e-... \n", + "2361 [ 1.04440488e-02 -9.37070698e-03 -7.36323372e-... \n", + "2378 [ 3.04989032e-02 -4.08191867e-02 -6.18648790e-... \n", + "3133 [ 0.00152804 -0.04189068 -0.04220504 -0.053740... \n", + "3140 [ 3.11435573e-02 -4.44000624e-02 -2.10917685e-... \n", + "3322 [ 2.75927987e-02 -6.23729872e-03 -3.83295454e-... \n", + "3583 [ 9.20385588e-03 -3.83387171e-02 -6.46291822e-... \n", + "4134 [-7.04960374e-04 -3.52595337e-02 -1.65264793e-... \n", + "4496 [ 3.67735326e-02 1.21120387e-03 -5.20942472e-... \n", + "5260 [ 2.07133405e-02 -1.69602726e-02 -5.07124476e-... \n", + "5400 [ 1.44114876e-02 -2.34710164e-02 -6.58538565e-... \n", + "5425 [ 3.10326386e-02 -2.19427086e-02 -6.56386837e-... \n", + "6014 [ 1.90773793e-02 -2.27493346e-02 -3.27166244e-... \n", + "8192 [ 0.01937891 -0.05466933 -0.06070872 -0.059028... \n", + "8240 [ 4.34123818e-03 -3.40953320e-02 -4.06381376e-... \n", + "8720 [ 0.03133732 -0.03972461 -0.00178199 -0.035876... \n", + "8914 [ 1.75969116e-02 -2.25022305e-02 -5.70390299e-... \n", + "10021 [ 5.02460636e-02 -5.25112189e-02 -4.12914790e-... \n", + "10327 [-0.00979626 -0.04912931 -0.08654705 -0.021063... \n", + "10345 [-0.04292191 -0.02636929 -0.06177032 -0.076520... \n", + "10369 [ 2.16020197e-02 -5.62509745e-02 -5.93873672e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "251 {\"token_count\":145,\"truncated\":false} \n", - "300 {\"token_count\":498,\"truncated\":false} \n", - "414 {\"token_count\":263,\"truncated\":false} \n", - "493 {\"token_count\":395,\"truncated\":false} \n", - "545 {\"token_count\":178,\"truncated\":false} \n", + " statistics ml_embed_text_status \\\n", + "545 {\"token_count\":178,\"truncated\":false} \n", + "614 {\"token_count\":399,\"truncated\":false} \n", + "1236 {\"token_count\":129,\"truncated\":false} \n", + "1477 {\"token_count\":16,\"truncated\":false} \n", + "2261 {\"token_count\":33,\"truncated\":false} \n", + "2361 {\"token_count\":45,\"truncated\":false} \n", + "2378 {\"token_count\":892,\"truncated\":false} \n", + "3133 {\"token_count\":90,\"truncated\":false} \n", + "3140 {\"token_count\":372,\"truncated\":false} \n", + "3322 {\"token_count\":36,\"truncated\":false} \n", + "3583 {\"token_count\":52,\"truncated\":false} \n", + "4134 {\"token_count\":412,\"truncated\":false} \n", + "4496 {\"token_count\":182,\"truncated\":false} \n", + "5260 {\"token_count\":103,\"truncated\":false} \n", + "5400 {\"token_count\":60,\"truncated\":false} \n", + "5425 {\"token_count\":87,\"truncated\":false} \n", + "6014 {\"token_count\":175,\"truncated\":false} \n", + "8192 {\"token_count\":131,\"truncated\":false} \n", + "8240 {\"token_count\":87,\"truncated\":false} \n", + "8720 {\"token_count\":645,\"truncated\":false} \n", + "8914 {\"token_count\":180,\"truncated\":false} \n", + "10021 {\"token_count\":30,\"truncated\":false} \n", + "10327 {\"token_count\":194,\"truncated\":false} \n", + "10345 {\"token_count\":262,\"truncated\":false} \n", + "10369 {\"token_count\":77,\"truncated\":false} \n", "\n", - " content \n", - "251 A purse was purchased from XXXX XXXX on XX/XX/... \n", - "300 XXXX XXXX XXXXXXXX has reported on my credit r... \n", - "414 I have tried to dispute US BKPT CT TX XXXXXXXX... \n", - "493 Discover Student Loan has been holding onto {$... \n", - "545 My payments have been approximately {$89.00} w... \n", + " content \n", + "545 My payments have been approximately {$89.00} w... \n", + "614 Hi, I have contacted Trans Union XXXX XXXX abo... \n", + "1236 I have a XXXX XXXX XXXX credit card on my Exp... \n", + "1477 Wrongs information, selling my information to ... \n", + "2261 Please investigate and delete disputed item th... \n", + "2361 By the provisions of the Fair Credit Reporting... \n", + "2378 Since XX/XX/XXXX I have been trying to dispute... \n", + "3133 Out of the blue I received a debt collection n... \n", + "3140 My wife and I have been sending money to XXXX ... \n", + "3322 Phone calls from Convergent Outsourcing XXXX. ... \n", + "3583 I recently received a copy of my credit report... \n", + "4134 I have been sending the creditor what they hav... \n", + "4496 This is my second complaint. Their response to... \n", + "5260 XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte... \n", + "5400 Upon checking my XXXX credit report I noticed ... \n", + "5425 Follow up to previous complaint XXXX XXXX XXXX... \n", + "6014 My new XXXX lease was over always paid on time... \n", + "8192 I have no idea where this account cane from. B... \n", + "8240 I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F... \n", + "8720 XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum... \n", + "8914 On XX/XX/21 I sent a letter regarding inaccura... \n", + "10021 XX/XX/XXXX and XX/XX/XXXX inaccurate informati... \n", + "10327 When I reviewed my credit report, I discovered... \n", + "10345 U.S. Bank sent two letters containing Visa Deb... \n", + "10369 I requested from XXXX that they reverse the la... \n", + "...\n", "\n", - "[5 rows x 4 columns]" + "[10000 rows x 4 columns]" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Will take ~3 minutes to compute the embeddings\n", - "predicted_embeddings = model.predict(downsampled_issues_df)\n", - "# Notice the lists of numbers that are our text embeddings for each complaint\n", - "predicted_embeddings.head() " + "successful_rows = (\n", + " (predicted_embeddings[\"ml_embed_text_status\"] == \"\")\n", + " # Series.str.len() gives the length of an array.\n", + " # See: https://stackoverflow.com/a/41340543/101923\n", + " & (predicted_embeddings[\"text_embedding\"].str.len() != 0)\n", + ")\n", + "predicted_embeddings = predicted_embeddings[successful_rows]\n", + "predicted_embeddings\n" ] }, { @@ -653,7 +1185,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": { "id": "AhNTnEC5FRz2" }, @@ -674,7 +1206,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": { "id": "6poSxh-fGJF7" }, @@ -682,7 +1214,7 @@ { "data": { "text/html": [ - "Query job 37f432dd-9ed7-4bbd-adc1-f33b8cbab33a is DONE. 61.5 MB processed. Open Job" + "Query job fa4bbc13-3831-4c80-9b59-9939e605ed58 is DONE. 61.7 MB processed. Open Job" ], "text/plain": [ "" @@ -694,7 +1226,7 @@ { "data": { "text/html": [ - "Query job 8ca9cc5a-091a-4d4e-bcf8-04d4bfec7b6b is DONE. 0 Bytes processed. Open Job" + "Query job d2d681aa-e49a-4fda-89fd-60cf906d3aec is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -706,7 +1238,7 @@ { "data": { "text/html": [ - "Query job cdc11d15-fa78-4190-841f-18642ddb53f8 is DONE. 72.3 MB processed. Open Job" + "Query job 234bb6be-625c-4c96-baea-c37c33410114 is DONE. 72.7 MB processed. Open Job" ], "text/plain": [ "" @@ -718,7 +1250,7 @@ { "data": { "text/html": [ - "Query job 39a6ea59-0e3d-4d69-bf8a-1502b9f1a48f is DONE. 80.0 kB processed. Open Job" + "Query job 285817cb-99d3-426f-82c3-89d36119e8db is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -730,31 +1262,7 @@ { "data": { "text/html": [ - "Query job 324ab354-ecbd-4bde-8f73-806856a53a19 is DONE. 73.2 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job cb7558b2-a967-491c-82db-e11116f1fba4 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 0cfc2298-e67b-4a03-804d-f4abd8d56da0 is DONE. 36.6 kB processed. Open Job" + "Query job 3a39d2b0-55a1-4922-972a-8806b387f877 is DONE. 73.3 MB processed. Open Job" ], "text/plain": [ "" @@ -794,88 +1302,85 @@ " \n", " \n", " \n", - " 251\n", - " 2\n", - " [{'CENTROID_ID': 2, 'DISTANCE': 0.534540549592...\n", - " [ 2.20562406e-02 -3.51827666e-02 7.63384486e-...\n", - " {\"token_count\":145,\"truncated\":false}\n", + " 182250\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.570560301900...\n", + " [ 4.70298417e-02 -4.08669300e-02 -2.99868709e-...\n", + " {\"token_count\":10,\"truncated\":false}\n", " \n", - " A purse was purchased from XXXX XXXX on XX/XX/...\n", + " These are not my accounts. Please remove them.\n", " \n", " \n", - " 300\n", + " 3023485\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.437379245910...\n", - " [ 0.01977486 -0.04289974 -0.05289588 -0.027267...\n", - " {\"token_count\":498,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.523572693768...\n", + " [ 1.55437263e-02 -1.93240177e-02 -2.48466972e-...\n", + " {\"token_count\":10,\"truncated\":false}\n", " \n", - " XXXX XXXX XXXXXXXX has reported on my credit r...\n", + " This debt is not mine due to identity theft.\n", " \n", " \n", - " 414\n", + " 407254\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.482813493921...\n", - " [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-...\n", - " {\"token_count\":263,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.515173566816...\n", + " [-0.01293471 -0.01959546 -0.02238463 -0.066214...\n", + " {\"token_count\":10,\"truncated\":false}\n", " \n", - " I have tried to dispute US BKPT CT TX XXXXXXXX...\n", + " I do not owe this company money!!!!!\n", " \n", " \n", - " 493\n", - " 9\n", - " [{'CENTROID_ID': 9, 'DISTANCE': 0.561752335987...\n", - " [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-...\n", - " {\"token_count\":395,\"truncated\":false}\n", + " 1509454\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.645342721754...\n", + " [ 3.21860723e-02 -2.67103072e-02 -4.78175096e-...\n", + " {\"token_count\":10,\"truncated\":false}\n", " \n", - " Discover Student Loan has been holding onto {$...\n", + " VIOLATES HIPPA AND CRA\n", " \n", " \n", - " 545\n", - " 9\n", - " [{'CENTROID_ID': 9, 'DISTANCE': 0.540487926907...\n", - " [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...\n", - " {\"token_count\":178,\"truncated\":false}\n", + " 2357848\n", + " 1\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.519872186251...\n", + " [-1.88122243e-02 -2.68064123e-02 -4.69480827e-...\n", + " {\"token_count\":10,\"truncated\":false}\n", " \n", - " My payments have been approximately {$89.00} w...\n", + " Receive numerous phone calls. I have no debt.\n", " \n", " \n", "\n", - "

5 rows × 6 columns

\n", - "[5 rows x 6 columns in total]" + "" ], "text/plain": [ - " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", - "251 2 [{'CENTROID_ID': 2, 'DISTANCE': 0.534540549592... \n", - "300 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.437379245910... \n", - "414 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.482813493921... \n", - "493 9 [{'CENTROID_ID': 9, 'DISTANCE': 0.561752335987... \n", - "545 9 [{'CENTROID_ID': 9, 'DISTANCE': 0.540487926907... \n", + " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", + "182250 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.570560301900... \n", + "3023485 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.523572693768... \n", + "407254 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.515173566816... \n", + "1509454 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.645342721754... \n", + "2357848 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.519872186251... \n", "\n", - " text_embedding \\\n", - "251 [ 2.20562406e-02 -3.51827666e-02 7.63384486e-... \n", - "300 [ 0.01977486 -0.04289974 -0.05289588 -0.027267... \n", - "414 [ 1.37719307e-02 -4.15441953e-02 -7.81692266e-... \n", - "493 [ 4.48844060e-02 -1.40293539e-02 -3.46709713e-... \n", - "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", + " text_embedding \\\n", + "182250 [ 4.70298417e-02 -4.08669300e-02 -2.99868709e-... \n", + "3023485 [ 1.55437263e-02 -1.93240177e-02 -2.48466972e-... \n", + "407254 [-0.01293471 -0.01959546 -0.02238463 -0.066214... \n", + "1509454 [ 3.21860723e-02 -2.67103072e-02 -4.78175096e-... \n", + "2357848 [-1.88122243e-02 -2.68064123e-02 -4.69480827e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "251 {\"token_count\":145,\"truncated\":false} \n", - "300 {\"token_count\":498,\"truncated\":false} \n", - "414 {\"token_count\":263,\"truncated\":false} \n", - "493 {\"token_count\":395,\"truncated\":false} \n", - "545 {\"token_count\":178,\"truncated\":false} \n", + " statistics ml_embed_text_status \\\n", + "182250 {\"token_count\":10,\"truncated\":false} \n", + "3023485 {\"token_count\":10,\"truncated\":false} \n", + "407254 {\"token_count\":10,\"truncated\":false} \n", + "1509454 {\"token_count\":10,\"truncated\":false} \n", + "2357848 {\"token_count\":10,\"truncated\":false} \n", "\n", - " content \n", - "251 A purse was purchased from XXXX XXXX on XX/XX/... \n", - "300 XXXX XXXX XXXXXXXX has reported on my credit r... \n", - "414 I have tried to dispute US BKPT CT TX XXXXXXXX... \n", - "493 Discover Student Loan has been holding onto {$... \n", - "545 My payments have been approximately {$89.00} w... \n", - "\n", - "[5 rows x 6 columns]" + " content \n", + "182250 These are not my accounts. Please remove them. \n", + "3023485 This debt is not mine due to identity theft. \n", + "407254 I do not owe this company money!!!!! \n", + "1509454 VIOLATES HIPPA AND CRA \n", + "2357848 Receive numerous phone calls. I have no debt. " ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -886,7 +1391,7 @@ "clustered_result = cluster_model.predict(predicted_embeddings)\n", "# Notice the CENTROID_ID column, which is the ID number of the group that\n", "# each complaint belongs to.\n", - "clustered_result.head(n=5)" + "clustered_result.peek(n=5)" ] }, { @@ -904,7 +1409,7 @@ "id": "21rNsFMHo8hO" }, "source": [ - "## Step 3: Use PaLM2 LLM model to summarize complaint clusters" + "## Step 3: Use Gemini to summarize complaint clusters" ] }, { @@ -917,7 +1422,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": { "id": "2E7wXM_jGqo6" }, @@ -925,7 +1430,7 @@ { "data": { "text/html": [ - "Query job 84f95981-01c7-49ca-a10c-5842f07d867f is DONE. 10.6 MB processed. Open Job" + "Query job 85ead687-4ba9-44bf-88da-23a066f45960 is DONE. 10.7 MB processed. Open Job" ], "text/plain": [ "" @@ -937,7 +1442,7 @@ { "data": { "text/html": [ - "Query job 0872869a-94f0-4c3f-9f92-da7272f95cd0 is DONE. 10.6 MB processed. Open Job" + "Query job 68ef20cd-220d-40a9-bb42-63ed3d6f5d3f is DONE. 10.7 MB processed. Open Job" ], "text/plain": [ "" @@ -963,7 +1468,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": { "id": "ZNDiueI9IP5e" }, @@ -973,48 +1478,42 @@ "output_type": "stream", "text": [ "comment list 1:\n", - "1. XXXX XXXX XXXXXXXX has reported on my credit report for XXXX that I had 3 payments 30 or more days past due this information was and is incorrect i have sent numerous letters and responses to them asking for information regarding this reporting the bank continues to send me responces stating that my credit report is accurat but no documentation supporting the reporting of negative payments i have sent the XXXX XXXX XXXX numerous document showing past mistake of my payment that were misapplied by thier associates yet they still will not correct my credit report the XXXX XXXX XXXX has the wrong infor mation on my payment, balance last payments amount made & ect i have recently sent them a letter ( This is the exact letter ) and other information im having problems with the problem im having Date BLANK BLANK BLANK Complaint Dispute on credit report reporting XXXX XXXX XXXX Address Mail XXXX fl XXXX XXXX XXXX XXXX FL Zip Code XXXX Regarding Account Number ( Blank ) & Account Number ( Blank ) I ( blank ) on this XXXX Day of XXXX XXXX do hereby request a complete copy of my payment history from XXXX XXXX XXXX to XXXX XXXX XXXX this request is to include dates of all payments that was made on these accounts dates of all payments that were past due on these accounts and dates payments was paid to these account s after due date. I ( Blank ) am also requesting all letters and correspondence advising me / making me aware of my past due payments I also request all payments showing my account were paid 30 or more days late I ( Blank ) also request all payment dates and how payment was made ( Ex XXXX XXXX XXXX XXXXXXXX. XXXX Please Mail this information ASAP\n", - "2. I have tried to dispute US BKPT CT TX XXXXXXXX XXXX XXXX Account number XXXX on my credit report several times as inaccurate ( XX/XX/XXXX XX/XX/XXXX XX/XX/XXXX and XX/XX/XXXX ) as not mine but the credit bureaus have verified that it is accurate. I mailed a letter to the county records office ( XX/XX/XXXX ) pertaining to US BKPT CT TX XXXX Account number XXXX where this occurred and they replied on XX/XX/XXXX stating that they do not send information to the credit bureaus. This is where I am confused because after reading the law the FCRA if the original creditor or institution can not verify the information then the credit bureaus reporting it have to delete it. I sent this into the credit bureaus as well along with the paperwork from the county record office and it seems like they are not doing ANY sort of investigation which they are required to\n", - "3. Unauthorized hard inquires on my credit report and bureau refuses to remove them after they placed them on my credit report with my authorization XXXX Inquiry XX/XX/XXXX XXXX XXXX Inquiry XX/XX/XXXX XXXX XXXX inquiry XX/XX/XXXX and XX/XX/XXXX XXXX XX/XX/XXXX XXXX\n", - "4. XXXX - XX/XX/2020 Equifax Hello, I have reviewed a copy of my current credit report and it shows the inquiries above are fraudulent and I have sent a copy of FTC report to request removal. I have not been successful and need these removed due to attempting to purchase a home.\n", - "5. I have tried to remedy the issue that XXXX XXXX caused as XXXX XXXX indicated to me that Equifax could not remove a discharge debt from my report because of two social security numbers. In fact it was not two ssn 's but rather my deceased husbands DOB XXXX was on my report. XXXX XXXX is notorious for reporting incorrect data so i am going to get this resolved through CEPB. Attached please find the bankruptcy discharge notification and a copy of the requested drivers license with my DOB, XXXX. I expect that the debt for XXXX placed by XXXX XXXX to be removed as all the requested documentation is included in the correspondence.\n", + "1. Wrongs information, selling my information to third party. Incorrect reporting\n", + "2. I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL FROM XXXX XXXX XXXX XXXX WHICH ENDED A A LIE. THEY ALSO, PULLED MANY OTHERS I DID NT AGREED TO. SOLF PULLS ONLY\n", + "3. XX/XX/XXXX and XX/XX/XXXX inaccurate information reported 30 days late.\n", + "4. Im working on myCredit and I see a couple of inquiries that I have no idea where they came from.\n", + "5. I request a copy of all dispute results and documentary evidence from XXXX, and XXXX mailed to me\n", "\n", "comment list 2:\n", - "1. A purse was purchased from XXXX XXXX on XX/XX/2021. As they stated my package was delivered and signed for. Indeed it was, I also still have it. My package was damaged and only XXXX can file a claim with XXXX. I have tried to contact XXXX and was told to contact the shipper. I have contacted XXXX and was told that a chargeback was filed with TCF so there is nothing that they can do. I have provided all of these documents to you all including a police report. I will continue to escalate this if needed.\n", - "2. On Saturday XX/XX/XXXX I submitted a dispute for several fraudulent transactions with chime. I had stopped using chime back in XXXX of 2022 because I believed they were scamming me because of the money that was being taken out of my account without my knowledge and i had also had my phone stolen at work with my chime card and ssi & ID in the back of my phone case. I told them I believed thats how i was scammed for all of those fraudulent transactions that I disputed and also that money from my job had still been posting in my chime account after i had stopped using it because i was unable to change my direct deposit information with my job because i couldnt access my chime account anymore so I didnt have access to my routing and account number but i still had my card connected to my apple pay which I eventually stopped using because i was getting paid but most time my card on apple pay was declined because i was still being scammed out my money. Ive tried ordering new cards several time before I completely stopped using chime however the same thing was happening with each card and chime couldnt figure out how to stop it. \n", - "A few days ago i tried to create a new chime bank account because i had completely forgotten about my old scammed chime account but that app prompted me to sign into my old account Chime told me that they would do a complete investigation and let me know of the results. \n", - "However, on Sunday XX/XX/XXXX at XXXX i got an email from chime stating Hello XXXX XXXX XXXX This letter is to inform you that we have made a final determination regarding the claim referenced above. \n", - "Based on our investigation, we have concluded no error occurred. Therefore, no funds will be credited to your account and this claim is considered closed. \n", + "1. My wife and I have been sending money to XXXX via my brother-in-law to finish a building project we have been working on since XXXX with target date of completion by XX/XX/XXXX. In XXXX XXXX my brother-in-law in was contacted by his bank to confirm he was not defrauding my wife. My brother-in-law confirmed he was helping to handle the building project by organizing and paying the workers. In XXXX XXXX Bank of America reach out to my wife to update her profile to avoid account restrictions. My wife 's account was eventually restricted until she called and confirmed her employment and other personal information. My wife 's full account activities were then restored and we continued sending wire transfers to XXXX via her checking account. \n", + "Then I received a letter dated XXXX XXXX XXXX from Bank of America stating the money market account I share with my wife which has been opened since XXXX will be will be restricted from use in 21 days and closed in 30 days with no reason. I strongly believe this is a result of the legal international wires because there was no reason to close the Savings account which had with hardly any activity. \n", + "I agree that Bank of America has a right to close accounts but I do not agree with Bank of America closing accounts because of international transactions unless they can prove fraud, criminal activity or support for terrorism, this is discriminatory towards foreign nationals. How are foreign nationals suppose to make investments or support their family/community if they are excluded from the banking system?\n", + "2. XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consumer Financial Protection Bureau XXXX XXXX XXXX XXXX, IA XXXX Dear Sir or Madam : In XX/XX/XXXX Out of the blue JP Morgan Chase arbitrarily closed my account. This was after my mother is a XXXX survivor who is over XXXX years old and for whom I have a general power of attorney and take care of her bill paying was questioned about a transaction. She is also XXXX XXXX. \n", "\n", - "I emailed them back for the documentation on how they did the dispute and verified the information was inaccurate. I also asked how is it possible that a investigation for 20+ transactions had been completed in less than 1 day. \n", - "I truly believe chime did not do any investigation at all and that they lied about my investigation/dispute They have been unable to provide me with the documents and dispute information. In fact, they said id have to wait 10+ business days to receive that information in mail because they arent going to email it to me I found this very unfortunate. \n", + "I have reason to belief that a mentally disturbed family member for whom I have an order of protection initiated this situation. This individual has ben harassing me and other members of my family for a considerable amount of time. \n", "\n", - "Here are my reference numbers for my disputes XXXX XXXX\n", - "3. My sister and I attempted to close our aging father 's professional account at Wells Fargo. My sister worked closely with the local branch and followed their directives. But, even after 6 months the bank continued to reject the paperwork presented. Anticipating our father 's further decline, we hired an estate attorney to help us close the account. Even after appointing me as an officer of my dad 's company and presenting new documentation which followed the bank 's directive, the bank again refused to move the money to my mother and to close the account. The entire time the balance of the account was depleted through bank fees from around {$1800.00} to around {$1000.00}. In the end, Wells Fargo refunded the entire original balance of around {$1800.00} plus some a \" consumer satisfaction credit '' and transferred the funds to my mother 's account.\n", - "4. On XXXX XX/XX/2020 a sale of three Tahitian pearl necklaces was made by my online shop. This amounted to approximately {$2800.00}. Some of the funds were withdrawn by me leaving a total of {$220000.00} in the Paypal account. \n", + "The bank initially was satisfied with her response. However within 2 days they closed the account of a XXXX year old XXXX XXXX person. \n", "\n", - "Suddenly the balance was frozen for 180 days by Paypal. A telephone call to their customer service on about XXXX XXXX resulted in a rude and abrupt lady telling me that Paypal was parting ways with me. I was given no reasonable explanation why the account was limited or the funds frozen other than to infer that I was busy with fraudulent activity. The rude person refused to give me more details. \n", + "Soon after for no reason my account was closed as well. I tried to reach out to the corporate offices of Chase and make great effort to find out what happened and to restore my account as well as my mothers but I was unsuccessful. In addition the people I spoke to were not only unhelpful bu exceedingly rude. \n", "\n", - "I submitted all the details/documents they requested from then on. My appeals were refused, On XXXX XX/XX/2020 the 180 frozen days expired. I was requested by Paypal to submit all my personal details/documents again. I did. I have the XXXX XXXX XXXX XXXX ID Document. It is perfectly legal and valid in XXXX XXXX as is the new XXXX XXXXXXXX which is now being issued. But Paypal is not accepting my ID Document. So my funds are not being released.They send computer generated emails which offer no explanation as to why my ID document is not being accepted or any solution to the problem. \n", + "I should add that I have had an account with Chase since XX/XX/XXXX and took care of my ailing father before he passed away for over 25 years as well. I am now taking care of my mother for over 28 years. \n", "\n", - "This matter is most frustrating and I'm sure not in line with any financial practice.\n", - "5. On XX/XX/XXXX I received a text from Chase showing -- -- -- -- - Chase Fraud : Did you attempt a {$1700.00} withdrawal on XX/XX/XXXX with card XXXX? \n", - "replay yes or no. Msg & data rates may apply. \n", - "-- -- -- -- - Then I replied no, Then chase sent a text they will close my account, give them a call. \n", + "I went so far as contacting a prominent Television reporter who was interested in doing a report on what happened. \n", "\n", - "So I called chase to report, they are saying because it is pending transaction I have to wait until pending is gone, 2 days later they accepted my claim. \n", + "I have since managed to open an account at another bank but this week I had reason to go to a branch of Chase regarding another issue and a manager using my That is a very serious unsubstantiated accusation and given this information I have no choice but to submit this complaint. \n", "\n", - "Today XX/XX/XXXX they refused to credit my money {$1700.00} because my pin number and debit card were used. So I told them I never received card. \n", - "Still their answer is same. \n", + "I have no interest in having an account again at a disreputable bank like Chase but I can not and will not accept or tolerate a derogatory accusation be associated with my name. \n", "\n", - "Then I asked what should I do? file small claim against chase? go to police office? \n", - "They told me I can go to police office to file a claim. \n", + "I hope that my complaint will hAve the desired effect of removing this derogatory unsubstantiated accusation be removed from my name. However. I will not let this unfair matter stand and Chase ought to know that I have already retained an attorney and will if necessary hold Chase responsible and liable all damage i have incurred now And in the future Enclosed, please find the letter from Chase stating that they were closing my mothers account and a similar letter was received by me too. \n", "\n", - "I will go to police office after my work. \n", + "Also please find a letter from her Doctor stating that she is XXXX XXXX. \n", "\n", - "Before I go to police office, I am asking your help about this situation. \n", + "Thank you. \n", "\n", - "Please help me.\n", + "XXXX XXXX\n", + "3. U.S. Bank sent two letters containing Visa Debit Cards to our address on XX/XX/2021. One Visa Debit Card is in the name of XXXX XXXX and one Visa Debit Card is in the name of XXXX XXXX. These cards supposedly link to existing checking accounts at U.S. Bank. However : ( 1 ) Neither of us have existing checking accounts at U.S. Bank, ( 2 ) Neither of us solicited a bank account at U.S. Bank, and ( 3 ) Neither of us solicited a Visa Debit Card. We have attempted to call U.S. Bank at the phone numbers provided in the letters but are only able to access an automated system which will not proceed without us establishing accounts and activating these cards. We are concerned here that one of two things has happened : either ( 1 ) we are victims of identity theft and some third party is trying to establish accounts in our name, or ( 2 ) U.S. Bank is engaged in bank fraud. In either case, we request the assistance of the Consumer Financial Protection Bureau. Thank you.\n", + "4. I contacted my bank over 3 times about this amount, the first two times I spoke to gentleman that agreed with me that I didnt get back a certain amount of dollars back, I did the math and they refuse to see that I do not owe this amount because I never had it in the first place. I wrote out all my charges and connected it to the charges made back from the consumer and I was missing XXXX, I called XXXX they said they gave it all back which is not their fault because they showed me proof. Along the lines Capital One does not want to take responsibility for the missing money. I have wrote everything out and then its not adding up, they keep saying that they did a charge back twice which is incorrect. My balance was at XXXX before I made this purchase and it shouldve been returned back to XXXX because I return all the items and nothing is in my possession. I have proof that I returned everything.\n", + "5. CB INDIGO ( Bank ) XX/XX/2022 I just recently got off the phone with the company and they wont put in a request of removal of a fraudulent hard inquiry from Insigo Mastercard to XXXX. They dont even have my information on file, I called 3 times most of them are lazy and was giving me a hard time.\n", "\n" ] } @@ -1037,7 +1536,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": { "id": "BfHGJLirzSvH" }, @@ -1048,47 +1547,41 @@ "text": [ "Please highlight the most obvious difference between the two lists of comments:\n", "comment list 1:\n", - "1. XXXX XXXX XXXXXXXX has reported on my credit report for XXXX that I had 3 payments 30 or more days past due this information was and is incorrect i have sent numerous letters and responses to them asking for information regarding this reporting the bank continues to send me responces stating that my credit report is accurat but no documentation supporting the reporting of negative payments i have sent the XXXX XXXX XXXX numerous document showing past mistake of my payment that were misapplied by thier associates yet they still will not correct my credit report the XXXX XXXX XXXX has the wrong infor mation on my payment, balance last payments amount made & ect i have recently sent them a letter ( This is the exact letter ) and other information im having problems with the problem im having Date BLANK BLANK BLANK Complaint Dispute on credit report reporting XXXX XXXX XXXX Address Mail XXXX fl XXXX XXXX XXXX XXXX FL Zip Code XXXX Regarding Account Number ( Blank ) & Account Number ( Blank ) I ( blank ) on this XXXX Day of XXXX XXXX do hereby request a complete copy of my payment history from XXXX XXXX XXXX to XXXX XXXX XXXX this request is to include dates of all payments that was made on these accounts dates of all payments that were past due on these accounts and dates payments was paid to these account s after due date. I ( Blank ) am also requesting all letters and correspondence advising me / making me aware of my past due payments I also request all payments showing my account were paid 30 or more days late I ( Blank ) also request all payment dates and how payment was made ( Ex XXXX XXXX XXXX XXXXXXXX. XXXX Please Mail this information ASAP\n", - "2. I have tried to dispute US BKPT CT TX XXXXXXXX XXXX XXXX Account number XXXX on my credit report several times as inaccurate ( XX/XX/XXXX XX/XX/XXXX XX/XX/XXXX and XX/XX/XXXX ) as not mine but the credit bureaus have verified that it is accurate. I mailed a letter to the county records office ( XX/XX/XXXX ) pertaining to US BKPT CT TX XXXX Account number XXXX where this occurred and they replied on XX/XX/XXXX stating that they do not send information to the credit bureaus. This is where I am confused because after reading the law the FCRA if the original creditor or institution can not verify the information then the credit bureaus reporting it have to delete it. I sent this into the credit bureaus as well along with the paperwork from the county record office and it seems like they are not doing ANY sort of investigation which they are required to\n", - "3. Unauthorized hard inquires on my credit report and bureau refuses to remove them after they placed them on my credit report with my authorization XXXX Inquiry XX/XX/XXXX XXXX XXXX Inquiry XX/XX/XXXX XXXX XXXX inquiry XX/XX/XXXX and XX/XX/XXXX XXXX XX/XX/XXXX XXXX\n", - "4. XXXX - XX/XX/2020 Equifax Hello, I have reviewed a copy of my current credit report and it shows the inquiries above are fraudulent and I have sent a copy of FTC report to request removal. I have not been successful and need these removed due to attempting to purchase a home.\n", - "5. I have tried to remedy the issue that XXXX XXXX caused as XXXX XXXX indicated to me that Equifax could not remove a discharge debt from my report because of two social security numbers. In fact it was not two ssn 's but rather my deceased husbands DOB XXXX was on my report. XXXX XXXX is notorious for reporting incorrect data so i am going to get this resolved through CEPB. Attached please find the bankruptcy discharge notification and a copy of the requested drivers license with my DOB, XXXX. I expect that the debt for XXXX placed by XXXX XXXX to be removed as all the requested documentation is included in the correspondence.\n", + "1. Wrongs information, selling my information to third party. Incorrect reporting\n", + "2. I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL FROM XXXX XXXX XXXX XXXX WHICH ENDED A A LIE. THEY ALSO, PULLED MANY OTHERS I DID NT AGREED TO. SOLF PULLS ONLY\n", + "3. XX/XX/XXXX and XX/XX/XXXX inaccurate information reported 30 days late.\n", + "4. Im working on myCredit and I see a couple of inquiries that I have no idea where they came from.\n", + "5. I request a copy of all dispute results and documentary evidence from XXXX, and XXXX mailed to me\n", "comment list 2:\n", - "1. A purse was purchased from XXXX XXXX on XX/XX/2021. As they stated my package was delivered and signed for. Indeed it was, I also still have it. My package was damaged and only XXXX can file a claim with XXXX. I have tried to contact XXXX and was told to contact the shipper. I have contacted XXXX and was told that a chargeback was filed with TCF so there is nothing that they can do. I have provided all of these documents to you all including a police report. I will continue to escalate this if needed.\n", - "2. On Saturday XX/XX/XXXX I submitted a dispute for several fraudulent transactions with chime. I had stopped using chime back in XXXX of 2022 because I believed they were scamming me because of the money that was being taken out of my account without my knowledge and i had also had my phone stolen at work with my chime card and ssi & ID in the back of my phone case. I told them I believed thats how i was scammed for all of those fraudulent transactions that I disputed and also that money from my job had still been posting in my chime account after i had stopped using it because i was unable to change my direct deposit information with my job because i couldnt access my chime account anymore so I didnt have access to my routing and account number but i still had my card connected to my apple pay which I eventually stopped using because i was getting paid but most time my card on apple pay was declined because i was still being scammed out my money. Ive tried ordering new cards several time before I completely stopped using chime however the same thing was happening with each card and chime couldnt figure out how to stop it. \n", - "A few days ago i tried to create a new chime bank account because i had completely forgotten about my old scammed chime account but that app prompted me to sign into my old account Chime told me that they would do a complete investigation and let me know of the results. \n", - "However, on Sunday XX/XX/XXXX at XXXX i got an email from chime stating Hello XXXX XXXX XXXX This letter is to inform you that we have made a final determination regarding the claim referenced above. \n", - "Based on our investigation, we have concluded no error occurred. Therefore, no funds will be credited to your account and this claim is considered closed. \n", + "1. My wife and I have been sending money to XXXX via my brother-in-law to finish a building project we have been working on since XXXX with target date of completion by XX/XX/XXXX. In XXXX XXXX my brother-in-law in was contacted by his bank to confirm he was not defrauding my wife. My brother-in-law confirmed he was helping to handle the building project by organizing and paying the workers. In XXXX XXXX Bank of America reach out to my wife to update her profile to avoid account restrictions. My wife 's account was eventually restricted until she called and confirmed her employment and other personal information. My wife 's full account activities were then restored and we continued sending wire transfers to XXXX via her checking account. \n", + "Then I received a letter dated XXXX XXXX XXXX from Bank of America stating the money market account I share with my wife which has been opened since XXXX will be will be restricted from use in 21 days and closed in 30 days with no reason. I strongly believe this is a result of the legal international wires because there was no reason to close the Savings account which had with hardly any activity. \n", + "I agree that Bank of America has a right to close accounts but I do not agree with Bank of America closing accounts because of international transactions unless they can prove fraud, criminal activity or support for terrorism, this is discriminatory towards foreign nationals. How are foreign nationals suppose to make investments or support their family/community if they are excluded from the banking system?\n", + "2. XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consumer Financial Protection Bureau XXXX XXXX XXXX XXXX, IA XXXX Dear Sir or Madam : In XX/XX/XXXX Out of the blue JP Morgan Chase arbitrarily closed my account. This was after my mother is a XXXX survivor who is over XXXX years old and for whom I have a general power of attorney and take care of her bill paying was questioned about a transaction. She is also XXXX XXXX. \n", "\n", - "I emailed them back for the documentation on how they did the dispute and verified the information was inaccurate. I also asked how is it possible that a investigation for 20+ transactions had been completed in less than 1 day. \n", - "I truly believe chime did not do any investigation at all and that they lied about my investigation/dispute They have been unable to provide me with the documents and dispute information. In fact, they said id have to wait 10+ business days to receive that information in mail because they arent going to email it to me I found this very unfortunate. \n", + "I have reason to belief that a mentally disturbed family member for whom I have an order of protection initiated this situation. This individual has ben harassing me and other members of my family for a considerable amount of time. \n", "\n", - "Here are my reference numbers for my disputes XXXX XXXX\n", - "3. My sister and I attempted to close our aging father 's professional account at Wells Fargo. My sister worked closely with the local branch and followed their directives. But, even after 6 months the bank continued to reject the paperwork presented. Anticipating our father 's further decline, we hired an estate attorney to help us close the account. Even after appointing me as an officer of my dad 's company and presenting new documentation which followed the bank 's directive, the bank again refused to move the money to my mother and to close the account. The entire time the balance of the account was depleted through bank fees from around {$1800.00} to around {$1000.00}. In the end, Wells Fargo refunded the entire original balance of around {$1800.00} plus some a \" consumer satisfaction credit '' and transferred the funds to my mother 's account.\n", - "4. On XXXX XX/XX/2020 a sale of three Tahitian pearl necklaces was made by my online shop. This amounted to approximately {$2800.00}. Some of the funds were withdrawn by me leaving a total of {$220000.00} in the Paypal account. \n", + "The bank initially was satisfied with her response. However within 2 days they closed the account of a XXXX year old XXXX XXXX person. \n", "\n", - "Suddenly the balance was frozen for 180 days by Paypal. A telephone call to their customer service on about XXXX XXXX resulted in a rude and abrupt lady telling me that Paypal was parting ways with me. I was given no reasonable explanation why the account was limited or the funds frozen other than to infer that I was busy with fraudulent activity. The rude person refused to give me more details. \n", + "Soon after for no reason my account was closed as well. I tried to reach out to the corporate offices of Chase and make great effort to find out what happened and to restore my account as well as my mothers but I was unsuccessful. In addition the people I spoke to were not only unhelpful bu exceedingly rude. \n", "\n", - "I submitted all the details/documents they requested from then on. My appeals were refused, On XXXX XX/XX/2020 the 180 frozen days expired. I was requested by Paypal to submit all my personal details/documents again. I did. I have the XXXX XXXX XXXX XXXX ID Document. It is perfectly legal and valid in XXXX XXXX as is the new XXXX XXXXXXXX which is now being issued. But Paypal is not accepting my ID Document. So my funds are not being released.They send computer generated emails which offer no explanation as to why my ID document is not being accepted or any solution to the problem. \n", + "I should add that I have had an account with Chase since XX/XX/XXXX and took care of my ailing father before he passed away for over 25 years as well. I am now taking care of my mother for over 28 years. \n", "\n", - "This matter is most frustrating and I'm sure not in line with any financial practice.\n", - "5. On XX/XX/XXXX I received a text from Chase showing -- -- -- -- - Chase Fraud : Did you attempt a {$1700.00} withdrawal on XX/XX/XXXX with card XXXX? \n", - "replay yes or no. Msg & data rates may apply. \n", - "-- -- -- -- - Then I replied no, Then chase sent a text they will close my account, give them a call. \n", + "I went so far as contacting a prominent Television reporter who was interested in doing a report on what happened. \n", "\n", - "So I called chase to report, they are saying because it is pending transaction I have to wait until pending is gone, 2 days later they accepted my claim. \n", + "I have since managed to open an account at another bank but this week I had reason to go to a branch of Chase regarding another issue and a manager using my That is a very serious unsubstantiated accusation and given this information I have no choice but to submit this complaint. \n", "\n", - "Today XX/XX/XXXX they refused to credit my money {$1700.00} because my pin number and debit card were used. So I told them I never received card. \n", - "Still their answer is same. \n", + "I have no interest in having an account again at a disreputable bank like Chase but I can not and will not accept or tolerate a derogatory accusation be associated with my name. \n", "\n", - "Then I asked what should I do? file small claim against chase? go to police office? \n", - "They told me I can go to police office to file a claim. \n", + "I hope that my complaint will hAve the desired effect of removing this derogatory unsubstantiated accusation be removed from my name. However. I will not let this unfair matter stand and Chase ought to know that I have already retained an attorney and will if necessary hold Chase responsible and liable all damage i have incurred now And in the future Enclosed, please find the letter from Chase stating that they were closing my mothers account and a similar letter was received by me too. \n", "\n", - "I will go to police office after my work. \n", + "Also please find a letter from her Doctor stating that she is XXXX XXXX. \n", "\n", - "Before I go to police office, I am asking your help about this situation. \n", + "Thank you. \n", "\n", - "Please help me.\n", + "XXXX XXXX\n", + "3. U.S. Bank sent two letters containing Visa Debit Cards to our address on XX/XX/2021. One Visa Debit Card is in the name of XXXX XXXX and one Visa Debit Card is in the name of XXXX XXXX. These cards supposedly link to existing checking accounts at U.S. Bank. However : ( 1 ) Neither of us have existing checking accounts at U.S. Bank, ( 2 ) Neither of us solicited a bank account at U.S. Bank, and ( 3 ) Neither of us solicited a Visa Debit Card. We have attempted to call U.S. Bank at the phone numbers provided in the letters but are only able to access an automated system which will not proceed without us establishing accounts and activating these cards. We are concerned here that one of two things has happened : either ( 1 ) we are victims of identity theft and some third party is trying to establish accounts in our name, or ( 2 ) U.S. Bank is engaged in bank fraud. In either case, we request the assistance of the Consumer Financial Protection Bureau. Thank you.\n", + "4. I contacted my bank over 3 times about this amount, the first two times I spoke to gentleman that agreed with me that I didnt get back a certain amount of dollars back, I did the math and they refuse to see that I do not owe this amount because I never had it in the first place. I wrote out all my charges and connected it to the charges made back from the consumer and I was missing XXXX, I called XXXX they said they gave it all back which is not their fault because they showed me proof. Along the lines Capital One does not want to take responsibility for the missing money. I have wrote everything out and then its not adding up, they keep saying that they did a charge back twice which is incorrect. My balance was at XXXX before I made this purchase and it shouldve been returned back to XXXX because I return all the items and nothing is in my possession. I have proof that I returned everything.\n", + "5. CB INDIGO ( Bank ) XX/XX/2022 I just recently got off the phone with the company and they wont put in a request of removal of a fraudulent hard inquiry from Insigo Mastercard to XXXX. They dont even have my information on file, I called 3 times most of them are lazy and was giving me a hard time.\n", "\n" ] } @@ -1107,12 +1600,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Get a response from PaLM 2 LLM by making a call to Vertex AI using our connection." + "Get a response from Gemini by making a call to Vertex AI using our connection." ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": { "id": "mL5P0_3X04dE" }, @@ -1120,7 +1613,7 @@ { "data": { "text/html": [ - "Query job d3965d90-8af9-46cb-9129-40e1d2866efe is DONE. 0 Bytes processed. Open Job" + "Query job a7ce86a7-3a18-47b9-a46f-98dbe6a5a339 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1138,19 +1631,19 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "metadata": { "id": "ICWHsqAW1FNk" }, "outputs": [], "source": [ - "# Make a DataFrame containing only a single row with our prompt for PaLM 2\n", + "# Make a DataFrame containing only a single row with our prompt for Gemini\n", "df = bf.DataFrame({\"prompt\": [prompt]})" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "metadata": { "id": "gB7e1LXU1pst" }, @@ -1158,7 +1651,7 @@ { "data": { "text/html": [ - "Query job 29a26018-027a-4c70-a795-841b5ace87d6 is DONE. 0 Bytes processed. Open Job" + "Query job d568c03d-6bbd-4c3e-b087-563b7f5135ed is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1170,7 +1663,7 @@ { "data": { "text/html": [ - "Query job 3abcc8cd-fa9f-4a93-b6be-6e22c8cdaceb is DONE. 8 Bytes processed. Open Job" + "Query job 17eaa806-51a4-4ee9-b219-75455d0095a7 is DONE. 8 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1182,7 +1675,7 @@ { "data": { "text/html": [ - "Query job ab5dfd41-98cb-4f24-a9c7-11399fcb2e47 is DONE. 2 Bytes processed. Open Job" + "Query job e6d40ded-691d-4523-94ea-dd8202bd0220 is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1194,7 +1687,7 @@ { "data": { "text/html": [ - "Query job c6c7bce4-81a0-4c4d-a515-2d5dfffc08a2 is DONE. 299 Bytes processed. Open Job" + "Query job 200f0b88-7b6d-417b-a181-a98138e3bc95 is DONE. 193 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1206,16 +1699,16 @@ { "data": { "text/plain": [ - "'The most obvious difference between the two lists of comments is their subject matter. Comment list 1 primarily deals with issues related to credit reporting and identity theft, while comment list 2 focuses on issues related to bank accounts, fraudulent transactions, and customer service.'" + "'The most obvious difference between the two lists of comments is that list 1 is related to credit reporting disputes and list 2 is a collection of general consumer banking complaints.'" ] }, - "execution_count": 18, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Send the request for PaLM 2 to generate a response to our prompt\n", + "# Send the request for Gemini to generate a response to our prompt\n", "major_difference = q_a_model.predict(df)\n", "# PaLM 2's response is the only row in the dataframe result \n", "major_difference[\"ml_generate_text_llm_result\"].iloc[0]" From c4beafdf0c1ba88b306ca96fa3ca46b86debaa4c Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:58:16 -0700 Subject: [PATCH 414/921] feat: support dataframe.cov (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Document: https://screenshot.googleplex.com/9egi7MsNj2uWHkH Fixes # 🦕 --- bigframes/core/blocks.py | 19 ++++++++---- bigframes/dataframe.py | 14 +++++---- tests/system/small/test_dataframe.py | 28 +++++++++++++++++ tests/system/small/test_multiindex.py | 21 +++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 30 ++++++++++++++++++- 5 files changed, 101 insertions(+), 11 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 64a0df9d863..afa13375b18 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1110,13 +1110,22 @@ def summarize( index_columns=[label_col_id], ) - def corr(self): - """Returns a block object to compute the self-correlation on this block.""" + def calculate_pairwise_metric(self, op=agg_ops.CorrOp()): + """ + Returns a block object to compute pairwise metrics among all value columns in this block. + + The metric to be computed is specified by the `op` parameter, which can be either a + correlation operation (default) or a covariance operation. + """ + if len(self.value_columns) > 30: + raise NotImplementedError( + "This function supports dataframes with 30 columns or fewer. " + f"Provided dataframe has {len(self.value_columns)} columns. {constants.FEEDBACK_LINK}" + ) + aggregations = [ ( - ex.BinaryAggregation( - agg_ops.CorrOp(), ex.free_var(left_col), ex.free_var(right_col) - ), + ex.BinaryAggregation(op, ex.free_var(left_col), ex.free_var(right_col)), f"{left_col}-{right_col}", ) for left_col in self.value_columns diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 051796535b1..07dae2c53b8 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1019,17 +1019,21 @@ def corr(self, method="pearson", min_periods=None, numeric_only=False) -> DataFr raise NotImplementedError( f"min_periods not yet supported. {constants.FEEDBACK_LINK}" ) - if len(self.columns) > 30: - raise NotImplementedError( - f"Only work with dataframes containing fewer than 30 columns. Current: {len(self.columns)}. {constants.FEEDBACK_LINK}" - ) if not numeric_only: frame = self._raise_on_non_numeric("corr") else: frame = self._drop_non_numeric() - return DataFrame(frame._block.corr()) + return DataFrame(frame._block.calculate_pairwise_metric(op=agg_ops.CorrOp())) + + def cov(self, *, numeric_only: bool = False) -> DataFrame: + if not numeric_only: + frame = self._raise_on_non_numeric("corr") + else: + frame = self._drop_non_numeric() + + return DataFrame(frame._block.calculate_pairwise_metric(agg_ops.CovOp())) def to_pandas( self, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 93bfd8d35c6..99ee6680fa9 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1916,6 +1916,34 @@ def test_corr_w_invalid_parameters(scalars_dfs): scalars_df[columns].corr(min_periods=1) +@pytest.mark.parametrize( + ("columns", "numeric_only"), + [ + (["bool_col", "int64_col", "float64_col"], True), + (["bool_col", "int64_col", "float64_col"], False), + (["bool_col", "int64_col", "float64_col", "string_col"], True), + pytest.param( + ["bool_col", "int64_col", "float64_col", "string_col"], + False, + marks=pytest.mark.xfail( + raises=NotImplementedError, + ), + ), + ], +) +def test_cov_w_numeric_only(scalars_dfs, columns, numeric_only): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[columns].cov(numeric_only=numeric_only).to_pandas() + pd_result = scalars_pandas_df[columns].cov(numeric_only=numeric_only) + + # BigFrames and Pandas differ in their data type handling: + # - Column types: BigFrames uses Float64, Pandas uses float64. + # - Index types: BigFrames uses strign, Pandas uses object. + pd.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + @pytest.mark.parametrize( ("op"), [ diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index f348758c935..d585d4f73e3 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -921,6 +921,27 @@ def test_corr_w_multi_index(scalars_df_index, scalars_pandas_df_index): ) +def test_cov_w_multi_index(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "float64_col", "int64_col"] + multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "b"], [1, 2, 2])) + + bf = scalars_df_index[columns].copy() + bf.columns = multi_columns + + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf.cov(numeric_only=True).to_pandas() + pd_result = pd_df.cov(numeric_only=True) + + # BigFrames and Pandas differ in their data type handling: + # - Column types: BigFrames uses Float64, Pandas uses float64. + # - Index types: BigFrames uses string, Pandas uses object. + pandas.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + @pytest.mark.parametrize( ("index_names",), [ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 876a85f1a51..d70d3827e7f 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2834,10 +2834,38 @@ def corr(self, method, min_periods, numeric_only) -> DataFrame: Include only float, int, boolean, decimal data. Returns: - DataFrame: Correlation matrix. + DataFrame: Correlation matrix. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def cov(self, *, numeric_only) -> DataFrame: + """ + Compute pairwise covariance of columns, excluding NA/null values. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': [1, 2, 3], + ... 'B': [400, 500, 600], + ... 'C': [0.8, 0.4, 0.9]}) + >>> df.cov(numeric_only=True) + A B C + A 1.0 100.0 0.05 + B 100.0 10000.0 5.0 + C 0.05 5.0 0.07 + + [3 rows x 3 columns] + + Args: + numeric_only(bool, default False): + Include only float, int, boolean, decimal data. + + Returns: + DataFrame: The covariance matrix of the series of the DataFrame. + """ + def update( self, other, join: str = "left", overwrite: bool = True, filter_func=None ) -> DataFrame: From 370fe9087848862d02f0e5a333fcb4cd37cf5ca0 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 22 Mar 2024 18:06:09 -0700 Subject: [PATCH 415/921] docs: add code samples for metrics.{recall_score, precision_score, f11_score} (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …_score} Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../sklearn/metrics/_classification.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_classification.py b/third_party/bigframes_vendored/sklearn/metrics/_classification.py index 35c22f4cd06..00bbf8cd605 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_classification.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_classification.py @@ -128,6 +128,22 @@ def recall_score( The best value is 1 and the worst value is 0. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([0, 1, 2, 0, 1, 2]) + >>> y_pred = bpd.DataFrame([0, 2, 1, 0, 0, 1]) + >>> recall_score = bigframes.ml.metrics.recall_score(y_true, y_pred, average=None) + >>> recall_score + 0 1 + 1 0 + 2 0 + dtype: int64 + + Args: y_true (Series or DataFrame of shape (n_samples,)): Ground truth (correct) target values. @@ -137,6 +153,7 @@ def recall_score( default='binary'): This parameter is required for multiclass/multilabel targets. Possible values are 'None', 'micro', 'macro', 'samples', 'weighted', 'binary'. + Only average=None is supported. Returns: float (if average is not None) or Series of float of shape n_unique_labels,): Recall @@ -160,6 +177,21 @@ def precision_score( The best value is 1 and the worst value is 0. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([0, 1, 2, 0, 1, 2]) + >>> y_pred = bpd.DataFrame([0, 2, 1, 0, 0, 1]) + >>> precision_score = bigframes.ml.metrics.precision_score(y_true, y_pred, average=None) + >>> precision_score + 0 0.666667 + 1 0.000000 + 2 0.000000 + dtype: float64 + Args: y_true: Series or DataFrame of shape (n_samples,) Ground truth (correct) target values. @@ -169,6 +201,7 @@ def precision_score( default='binary' This parameter is required for multiclass/multilabel targets. Possible values are 'None', 'micro', 'macro', 'samples', 'weighted', 'binary'. + Only average=None is supported. Returns: precision: float (if average is not None) or Series of float of shape \ @@ -195,6 +228,21 @@ def f1_score( the F1 score of each class with weighting depending on the ``average`` parameter. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([0, 1, 2, 0, 1, 2]) + >>> y_pred = bpd.DataFrame([0, 2, 1, 0, 0, 1]) + >>> f1_score = bigframes.ml.metrics.f1_score(y_true, y_pred, average=None) + >>> f1_score + 0 0.8 + 1 0.0 + 2 0.0 + dtype: float64 + Args: y_true: Series or DataFrame of shape (n_samples,) Ground truth (correct) target values. From ae586e0b8065e762d9bd4fb441f1acb65e2fdf25 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Sat, 23 Mar 2024 01:30:17 +0000 Subject: [PATCH 416/921] test: enable back cmek tests on inline data (#461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- tests/system/small/test_encryption.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 70d2ce381fc..3389e5cd68b 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -146,9 +146,6 @@ def test_df_apis(bq_cmek, session_with_bq_cmek, scalars_table_id): pytest.param( None, id="default_engine", - marks=pytest.mark.skip( - reason="Internal issue 327544164, cmek does not propagate to the dataframe." - ), ), ], ) @@ -207,9 +204,6 @@ def test_to_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): assert output_table_dataset.default_encryption_configuration is None -@pytest.mark.skip( - reason="Internal issue 327544164, cmek does not propagate to the dataframe." -) def test_read_pandas(bq_cmek, session_with_bq_cmek): if not bq_cmek: pytest.skip("no cmek set for testing") From 65c6f4736d1a5552835e4cec8b777b2c0f3dd8da Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Sat, 23 Mar 2024 01:42:16 -0700 Subject: [PATCH 417/921] feat!: rename ml model params (#491) Includes following changes: * renaming min_rel_progress -> tol, to be consistent with sklearn * not allowing setting early_stop anymore, always to True * renaming n_parallell_trees -> n_estimators, to be consistent with sklearn * renaming class_weights -> class_weight, to be consistent with sklearn * renaming learn_rate -> learning_rate, to be consistent with sklearn * PCA n_components supports float value and None now, default to None --- bigframes/ml/cluster.py | 1 - bigframes/ml/decomposition.py | 29 ++++-- bigframes/ml/ensemble.py | 69 +++++++------- bigframes/ml/linear_model.py | 90 +++++++++---------- tests/system/large/ml/test_decomposition.py | 34 +++++++ tests/system/large/ml/test_ensemble.py | 52 +++++------ tests/system/large/ml/test_linear_model.py | 39 ++++---- tests/unit/ml/test_golden_sql.py | 10 +-- .../sklearn/decomposition/_pca.py | 18 +--- .../sklearn/ensemble/_forest.py | 16 ++-- .../sklearn/linear_model/_base.py | 14 ++- .../sklearn/linear_model/_logistic.py | 12 +-- .../bigframes_vendored/xgboost/sklearn.py | 16 ++-- 13 files changed, 205 insertions(+), 195 deletions(-) diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index ac5b6f1e3bb..1035def54db 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -33,7 +33,6 @@ "init_col": "kmeansInitializationColumn", "distance_type": "distanceType", "max_iter": "maxIterations", - "early_stop": "earlyStop", "tol": "minRelativeProgress", } diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 36fa28e141c..475b4a046fa 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -37,7 +37,7 @@ class PCA( def __init__( self, - n_components: int = 3, + n_components: Optional[Union[int, float]] = None, *, svd_solver: Literal["full", "randomized", "auto"] = "auto", ): @@ -56,6 +56,8 @@ def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> PCA: last_fitting = model.training_runs[-1]["trainingOptions"] if "numPrincipalComponents" in last_fitting: kwargs["n_components"] = int(last_fitting["numPrincipalComponents"]) + if "pcaExplainedVarianceRatio" in last_fitting: + kwargs["n_components"] = float(last_fitting["pcaExplainedVarianceRatio"]) if "pcaSolver" in last_fitting: kwargs["svd_solver"] = str(last_fitting["pcaSolver"]) @@ -63,6 +65,22 @@ def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> PCA: new_pca._bqml_model = core.BqmlModel(session, model) return new_pca + @property + def _bqml_options(self) -> dict: + """The model options as they will be set for BQML""" + options: dict = { + "model_type": "PCA", + "pca_solver": self.svd_solver, + } + + assert self.n_components is not None + if 0 < self.n_components < 1: + options["pca_explained_variance_ratio"] = float(self.n_components) + elif self.n_components >= 1: + options["num_principal_components"] = int(self.n_components) + + return options + def _fit( self, X: Union[bpd.DataFrame, bpd.Series], @@ -71,14 +89,13 @@ def _fit( ) -> PCA: (X,) = utils.convert_to_dataframe(X) + # To mimic sklearn's behavior + if self.n_components is None: + self.n_components = min(X.shape) self._bqml_model = self._bqml_model_factory.create_model( X_train=X, transforms=transforms, - options={ - "model_type": "PCA", - "num_principal_components": self.n_components, - "pca_solver": self.svd_solver, - }, + options=self._bqml_options, ) return self diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 23b227de67f..72ea600c581 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -31,7 +31,6 @@ _BQML_PARAMS_MAPPING = { "booster": "boosterType", "tree_method": "treeMethod", - "early_stop": "earlyStop", "colsample_bytree": "colsampleBylevel", "colsample_bylevel": "colsampleBytree", "colsample_bynode": "colsampleBynode", @@ -40,8 +39,8 @@ "reg_alpha": "l1Regularization", "reg_lambda": "l2Regularization", "learning_rate": "learnRate", - "min_rel_progress": "minRelativeProgress", - "num_parallel_tree": "numParallelTree", + "tol": "minRelativeProgress", + "n_estimators": "numParallelTree", "min_tree_child_weight": "minTreeChildWeight", "max_depth": "maxTreeDepth", "max_iterations": "maxIterations", @@ -57,7 +56,7 @@ class XGBRegressor( def __init__( self, - num_parallel_tree: int = 1, + n_estimators: int = 1, *, booster: Literal["gbtree", "dart"] = "gbtree", dart_normalized_type: Literal["tree", "forest"] = "tree", @@ -71,14 +70,13 @@ def __init__( subsample: float = 1.0, reg_alpha: float = 0.0, reg_lambda: float = 1.0, - early_stop: float = True, learning_rate: float = 0.3, max_iterations: int = 20, - min_rel_progress: float = 0.01, + tol: float = 0.01, enable_global_explain: bool = False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): - self.num_parallel_tree = num_parallel_tree + self.n_estimators = n_estimators self.booster = booster self.dart_normalized_type = dart_normalized_type self.tree_method = tree_method @@ -91,10 +89,9 @@ def __init__( self.subsample = subsample self.reg_alpha = reg_alpha self.reg_lambda = reg_lambda - self.early_stop = early_stop self.learning_rate = learning_rate self.max_iterations = max_iterations - self.min_rel_progress = min_rel_progress + self.tol = tol self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None @@ -127,7 +124,8 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: return { "model_type": "BOOSTED_TREE_REGRESSOR", "data_split_method": "NO_SPLIT", - "num_parallel_tree": self.num_parallel_tree, + "early_stop": True, + "num_parallel_tree": self.n_estimators, "booster_type": self.booster, "tree_method": self.tree_method, "min_tree_child_weight": self.min_tree_child_weight, @@ -139,10 +137,9 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "subsample": self.subsample, "l1_reg": self.reg_alpha, "l2_reg": self.reg_lambda, - "early_stop": self.early_stop, "learn_rate": self.learning_rate, "max_iterations": self.max_iterations, - "min_rel_progress": self.min_rel_progress, + "min_rel_progress": self.tol, "enable_global_explain": self.enable_global_explain, "xgboost_version": self.xgboost_version, } @@ -215,7 +212,7 @@ class XGBClassifier( def __init__( self, - num_parallel_tree: int = 1, + n_estimators: int = 1, *, booster: Literal["gbtree", "dart"] = "gbtree", dart_normalized_type: Literal["tree", "forest"] = "tree", @@ -229,14 +226,13 @@ def __init__( subsample: float = 1.0, reg_alpha: float = 0.0, reg_lambda: float = 1.0, - early_stop: bool = True, learning_rate: float = 0.3, max_iterations: int = 20, - min_rel_progress: float = 0.01, + tol: float = 0.01, enable_global_explain: bool = False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): - self.num_parallel_tree = num_parallel_tree + self.n_estimators = n_estimators self.booster = booster self.dart_normalized_type = dart_normalized_type self.tree_method = tree_method @@ -249,10 +245,9 @@ def __init__( self.subsample = subsample self.reg_alpha = reg_alpha self.reg_lambda = reg_lambda - self.early_stop = early_stop self.learning_rate = learning_rate self.max_iterations = max_iterations - self.min_rel_progress = min_rel_progress + self.tol = tol self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None @@ -285,7 +280,8 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: return { "model_type": "BOOSTED_TREE_CLASSIFIER", "data_split_method": "NO_SPLIT", - "num_parallel_tree": self.num_parallel_tree, + "early_stop": True, + "num_parallel_tree": self.n_estimators, "booster_type": self.booster, "tree_method": self.tree_method, "min_tree_child_weight": self.min_tree_child_weight, @@ -297,10 +293,9 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "subsample": self.subsample, "l1_reg": self.reg_alpha, "l2_reg": self.reg_lambda, - "early_stop": self.early_stop, "learn_rate": self.learning_rate, "max_iterations": self.max_iterations, - "min_rel_progress": self.min_rel_progress, + "min_rel_progress": self.tol, "enable_global_explain": self.enable_global_explain, "xgboost_version": self.xgboost_version, } @@ -371,7 +366,7 @@ class RandomForestRegressor( def __init__( self, - num_parallel_tree: int = 100, + n_estimators: int = 100, *, tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", min_tree_child_weight: int = 1, @@ -383,12 +378,11 @@ def __init__( subsample=0.8, reg_alpha=0.0, reg_lambda=1.0, - early_stop=True, - min_rel_progress=0.01, + tol=0.01, enable_global_explain=False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): - self.num_parallel_tree = num_parallel_tree + self.n_estimators = n_estimators self.tree_method = tree_method self.min_tree_child_weight = min_tree_child_weight self.colsample_bytree = colsample_bytree @@ -399,8 +393,7 @@ def __init__( self.subsample = subsample self.reg_alpha = reg_alpha self.reg_lambda = reg_lambda - self.early_stop = early_stop - self.min_rel_progress = min_rel_progress + self.tol = tol self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None @@ -432,7 +425,8 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: """The model options as they will be set for BQML""" return { "model_type": "RANDOM_FOREST_REGRESSOR", - "num_parallel_tree": self.num_parallel_tree, + "early_stop": True, + "num_parallel_tree": self.n_estimators, "tree_method": self.tree_method, "min_tree_child_weight": self.min_tree_child_weight, "colsample_bytree": self.colsample_bytree, @@ -443,8 +437,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "subsample": self.subsample, "l1_reg": self.reg_alpha, "l2_reg": self.reg_lambda, - "early_stop": self.early_stop, - "min_rel_progress": self.min_rel_progress, + "min_rel_progress": self.tol, "data_split_method": "NO_SPLIT", "enable_global_explain": self.enable_global_explain, "xgboost_version": self.xgboost_version, @@ -536,7 +529,7 @@ class RandomForestClassifier( def __init__( self, - num_parallel_tree: int = 100, + n_estimators: int = 100, *, tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", min_tree_child_weight: int = 1, @@ -548,12 +541,11 @@ def __init__( subsample: float = 0.8, reg_alpha: float = 0.0, reg_lambda: float = 1.0, - early_stop=True, - min_rel_progress: float = 0.01, + tol: float = 0.01, enable_global_explain=False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): - self.num_parallel_tree = num_parallel_tree + self.n_estimators = n_estimators self.tree_method = tree_method self.min_tree_child_weight = min_tree_child_weight self.colsample_bytree = colsample_bytree @@ -564,8 +556,7 @@ def __init__( self.subsample = subsample self.reg_alpha = reg_alpha self.reg_lambda = reg_lambda - self.early_stop = early_stop - self.min_rel_progress = min_rel_progress + self.tol = tol self.enable_global_explain = enable_global_explain self.xgboost_version = xgboost_version self._bqml_model: Optional[core.BqmlModel] = None @@ -597,7 +588,8 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: """The model options as they will be set for BQML""" return { "model_type": "RANDOM_FOREST_CLASSIFIER", - "num_parallel_tree": self.num_parallel_tree, + "early_stop": True, + "num_parallel_tree": self.n_estimators, "tree_method": self.tree_method, "min_tree_child_weight": self.min_tree_child_weight, "colsample_bytree": self.colsample_bytree, @@ -608,8 +600,7 @@ def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: "subsample": self.subsample, "l1_reg": self.reg_alpha, "l2_reg": self.reg_lambda, - "early_stop": self.early_stop, - "min_rel_progress": self.min_rel_progress, + "min_rel_progress": self.tol, "data_split_method": "NO_SPLIT", "enable_global_explain": self.enable_global_explain, "xgboost_version": self.xgboost_version, diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index b3db04df052..c0abe77b9f7 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -35,13 +35,10 @@ "l1_reg": "l1Regularization", "l2_reg": "l2Regularization", "max_iterations": "maxIterations", - "learn_rate_strategy": "learnRateStrategy", - "learn_rate": "learnRate", - "early_stop": "earlyStop", - # To rename to tol. - "min_rel_progress": "minRelativeProgress", + "learning_rate_strategy": "learnRateStrategy", + "learning_rate": "learnRate", "tol": "minRelativeProgress", - "ls_init_learn_rate": "initialLearnRate", + "ls_init_learning_rate": "initialLearnRate", "warm_start": "warmStart", "calculate_p_values": "calculatePValues", "enable_global_explain": "enableGlobalExplain", @@ -67,11 +64,10 @@ def __init__( l2_reg: float = 0.0, max_iterations: int = 20, warm_start: bool = False, - learn_rate: Optional[float] = None, - learn_rate_strategy: Literal["line_search", "constant"] = "line_search", - early_stop: bool = True, - min_rel_progress: float = 0.01, - ls_init_learn_rate: Optional[float] = None, + learning_rate: Optional[float] = None, + learning_rate_strategy: Literal["line_search", "constant"] = "line_search", + tol: float = 0.01, + ls_init_learning_rate: Optional[float] = None, calculate_p_values: bool = False, enable_global_explain: bool = False, ): @@ -81,11 +77,10 @@ def __init__( self.l2_reg = l2_reg self.max_iterations = max_iterations self.warm_start = warm_start - self.learn_rate = learn_rate - self.learn_rate_strategy = learn_rate_strategy - self.early_stop = early_stop - self.min_rel_progress = min_rel_progress - self.ls_init_learn_rate = ls_init_learn_rate + self.learning_rate = learning_rate + self.learning_rate_strategy = learning_rate_strategy + self.tol = tol + self.ls_init_learning_rate = ls_init_learning_rate self.calculate_p_values = calculate_p_values self.enable_global_explain = enable_global_explain self._bqml_model: Optional[core.BqmlModel] = None @@ -110,7 +105,7 @@ def _from_bq( # Convert types kwargs[bf_param] = ( float(last_fitting[bqml_param]) - if bf_param in ["l1_reg", "learn_rate", "ls_init_learn_rate"] + if bf_param in ["l1_reg", "learning_rate", "ls_init_learning_rate"] else type(bf_value)(last_fitting[bqml_param]) ) @@ -128,18 +123,17 @@ def _bqml_options(self) -> dict: "fit_intercept": self.fit_intercept, "l2_reg": self.l2_reg, "max_iterations": self.max_iterations, - "learn_rate_strategy": self.learn_rate_strategy, - "early_stop": self.early_stop, - "min_rel_progress": self.min_rel_progress, + "learn_rate_strategy": self.learning_rate_strategy, + "min_rel_progress": self.tol, "calculate_p_values": self.calculate_p_values, "enable_global_explain": self.enable_global_explain, } if self.l1_reg is not None: options["l1_reg"] = self.l1_reg - if self.learn_rate is not None: - options["learn_rate"] = self.learn_rate - if self.ls_init_learn_rate is not None: - options["ls_init_learn_rate"] = self.ls_init_learn_rate + if self.learning_rate is not None: + options["learn_rate"] = self.learning_rate + if self.ls_init_learning_rate is not None: + options["ls_init_learn_rate"] = self.ls_init_learning_rate # Even presenting warm_start returns error for NORMAL_EQUATION optimizer if self.warm_start: options["warm_start"] = self.warm_start @@ -210,7 +204,7 @@ class LogisticRegression( bigframes_vendored.sklearn.linear_model._logistic.LogisticRegression.__doc__ ) - # TODO(ashleyxu) support class_weights in the constructor. + # TODO(ashleyxu) support class_weight in the constructor. def __init__( self, *, @@ -222,13 +216,13 @@ def __init__( l2_reg: float = 0.0, max_iterations: int = 20, warm_start: bool = False, - learn_rate: Optional[float] = None, - learn_rate_strategy: Literal["line_search", "constant"] = "line_search", + learning_rate: Optional[float] = None, + learning_rate_strategy: Literal["line_search", "constant"] = "line_search", tol: float = 0.01, - ls_init_learn_rate: Optional[float] = None, + ls_init_learning_rate: Optional[float] = None, calculate_p_values: bool = False, enable_global_explain: bool = False, - class_weights: Optional[Union[Literal["balanced"], Dict[str, float]]] = None, + class_weight: Optional[Union[Literal["balanced"], Dict[str, float]]] = None, ): self.optimize_strategy = optimize_strategy self.fit_intercept = fit_intercept @@ -236,14 +230,14 @@ def __init__( self.l2_reg = l2_reg self.max_iterations = max_iterations self.warm_start = warm_start - self.learn_rate = learn_rate - self.learn_rate_strategy = learn_rate_strategy + self.learning_rate = learning_rate + self.learning_rate_strategy = learning_rate_strategy self.tol = tol - self.ls_init_learn_rate = ls_init_learn_rate + self.ls_init_learning_rate = ls_init_learning_rate self.calculate_p_values = calculate_p_values self.enable_global_explain = enable_global_explain - self.class_weights = class_weights - self._auto_class_weight = class_weights == "balanced" + self.class_weight = class_weight + self._auto_class_weight = class_weight == "balanced" self._bqml_model: Optional[core.BqmlModel] = None self._bqml_model_factory = globals.bqml_model_factory() @@ -264,14 +258,14 @@ def _from_bq( # Convert types kwargs[bf_param] = ( float(last_fitting[bqml_param]) - if bf_param in ["l1_reg", "learn_rate", "ls_init_learn_rate"] + if bf_param in ["l1_reg", "learning_rate", "ls_init_learning_rate"] else type(bf_value)(last_fitting[bqml_param]) ) if last_fitting["autoClassWeights"]: - kwargs["class_weights"] = "balanced" - # TODO(ashleyxu) support class_weights in the constructor. + kwargs["class_weight"] = "balanced" + # TODO(ashleyxu) support class_weight in the constructor. # if "labelClassWeights" in last_fitting: - # kwargs["class_weights"] = last_fitting["labelClassWeights"] + # kwargs["class_weight"] = last_fitting["labelClassWeights"] new_logistic_regression = cls(**kwargs) new_logistic_regression._bqml_model = core.BqmlModel(session, model) @@ -288,19 +282,19 @@ def _bqml_options(self) -> dict: "optimize_strategy": self.optimize_strategy, "l2_reg": self.l2_reg, "max_iterations": self.max_iterations, - "learn_rate_strategy": self.learn_rate_strategy, + "learn_rate_strategy": self.learning_rate_strategy, "min_rel_progress": self.tol, "calculate_p_values": self.calculate_p_values, "enable_global_explain": self.enable_global_explain, - # TODO(ashleyxu): support class_weights (struct array as dict in our API) - # "class_weights": self.class_weights, + # TODO(ashleyxu): support class_weight (struct array as dict in our API) + # "class_weight": self.class_weight, } if self.l1_reg is not None: options["l1_reg"] = self.l1_reg - if self.learn_rate is not None: - options["learn_rate"] = self.learn_rate - if self.ls_init_learn_rate is not None: - options["ls_init_learn_rate"] = self.ls_init_learn_rate + if self.learning_rate is not None: + options["learn_rate"] = self.learning_rate + if self.ls_init_learning_rate is not None: + options["ls_init_learn_rate"] = self.ls_init_learning_rate # Even presenting warm_start returns error for NORMAL_EQUATION optimizer if self.warm_start: options["warm_start"] = self.warm_start @@ -362,10 +356,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LogisticRegression: if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") - # TODO(ashleyxu): support class_weights (struct array as dict in our API) - if self.class_weights not in (None, "balanced"): + # TODO(ashleyxu): support class_weight (struct array as dict in our API) + if self.class_weight not in (None, "balanced"): raise NotImplementedError( - f"class_weights is not supported yet. {constants.FEEDBACK_LINK}" + f"class_weight is not supported yet. {constants.FEEDBACK_LINK}" ) new_model = self._bqml_model.copy(model_name, replace) diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index 7932536e0c6..264b95a92e6 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -155,3 +155,37 @@ def test_decomposition_configure_fit_score_predict_params( ) assert reloaded_model.n_components == 5 assert reloaded_model.svd_solver == "RANDOMIZED" + + +def test_decomposition_configure_fit_load_float_component( + penguins_df_default_index, dataset_id +): + model = decomposition.PCA(n_components=0.2) + model.fit(penguins_df_default_index) + + # save, load, check n_components to ensure configuration was kept + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_pca_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_pca_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.n_components == 0.2 + + +def test_decomposition_configure_fit_load_none_component( + penguins_df_default_index, dataset_id +): + model = decomposition.PCA(n_components=None) + model.fit(penguins_df_default_index) + + # save, load, check n_components. Here n_components is the column size of the training input. + reloaded_model = model.to_gbq( + f"{dataset_id}.temp_configured_pca_model", replace=True + ) + assert ( + f"{dataset_id}.temp_configured_pca_model" + in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.n_components == 7 diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index b98d7a757ca..2403644a423 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -20,7 +20,7 @@ import bigframes.ml.ensemble -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBRegressor() @@ -64,7 +64,7 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): ) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_xgbregressor_dart_booster_multiple_params( penguins_df_default_index, dataset_id ): @@ -75,14 +75,14 @@ def test_xgbregressor_dart_booster_multiple_params( colsample_bytree=0.95, colsample_bylevel=0.95, colsample_bynode=0.95, - num_parallel_tree=2, + n_estimators=2, max_depth=4, subsample=0.95, reg_alpha=0.0001, reg_lambda=0.0001, learning_rate=0.015, max_iterations=4, - min_rel_progress=0.02, + tol=0.02, ) df = penguins_df_default_index.dropna().sample(n=70) @@ -126,20 +126,19 @@ def test_xgbregressor_dart_booster_multiple_params( assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 assert reloaded_model.colsample_bynode == 0.95 - assert reloaded_model.early_stop is True assert reloaded_model.subsample == 0.95 assert reloaded_model.reg_alpha == 0.0001 assert reloaded_model.reg_lambda == 0.0001 assert reloaded_model.learning_rate == 0.015 assert reloaded_model.max_iterations == 4 - assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.tol == 0.02 assert reloaded_model.gamma == 0.0 assert reloaded_model.max_depth == 4 assert reloaded_model.min_tree_child_weight == 2 - assert reloaded_model.num_parallel_tree == 2 + assert reloaded_model.n_estimators == 2 -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBClassifier() @@ -179,7 +178,7 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): ) -# @pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_xgbclassifier_dart_booster_multiple_params( penguins_df_default_index, dataset_id ): @@ -190,14 +189,14 @@ def test_xgbclassifier_dart_booster_multiple_params( colsample_bytree=0.95, colsample_bylevel=0.95, colsample_bynode=0.95, - num_parallel_tree=2, + n_estimators=2, max_depth=4, subsample=0.95, reg_alpha=0.0001, reg_lambda=0.0001, learning_rate=0.015, max_iterations=4, - min_rel_progress=0.02, + tol=0.02, ) df = penguins_df_default_index.dropna().sample(n=70) @@ -240,20 +239,19 @@ def test_xgbclassifier_dart_booster_multiple_params( assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 assert reloaded_model.colsample_bynode == 0.95 - assert reloaded_model.early_stop is True assert reloaded_model.subsample == 0.95 assert reloaded_model.reg_alpha == 0.0001 assert reloaded_model.reg_lambda == 0.0001 assert reloaded_model.learning_rate == 0.015 assert reloaded_model.max_iterations == 4 - assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.tol == 0.02 assert reloaded_model.gamma == 0.0 assert reloaded_model.max_depth == 4 assert reloaded_model.min_tree_child_weight == 2 - assert reloaded_model.num_parallel_tree == 2 + assert reloaded_model.n_estimators == 2 -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_randomforestregressor_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestRegressor() @@ -294,7 +292,7 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset ) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_randomforestregressor_multiple_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestRegressor( tree_method="auto", @@ -302,12 +300,12 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase colsample_bytree=0.95, colsample_bylevel=0.95, colsample_bynode=0.95, - num_parallel_tree=90, + n_estimators=90, max_depth=14, subsample=0.95, reg_alpha=0.0001, reg_lambda=0.0001, - min_rel_progress=0.02, + tol=0.02, ) df = penguins_df_default_index.dropna().sample(n=70) @@ -349,19 +347,18 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 assert reloaded_model.colsample_bynode == 0.95 - assert reloaded_model.early_stop is True assert reloaded_model.subsample == 0.95 assert reloaded_model.reg_alpha == 0.0001 assert reloaded_model.reg_lambda == 0.0001 - assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.tol == 0.02 assert reloaded_model.gamma == 0.0 assert reloaded_model.max_depth == 14 assert reloaded_model.min_tree_child_weight == 2 - assert reloaded_model.num_parallel_tree == 90 + assert reloaded_model.n_estimators == 90 assert reloaded_model.enable_global_explain is False -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_randomforestclassifier_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestClassifier() @@ -401,7 +398,7 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase ) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_randomforestclassifier_multiple_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.RandomForestClassifier( tree_method="AUTO", @@ -409,12 +406,12 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas colsample_bytree=0.95, colsample_bylevel=0.95, colsample_bynode=0.95, - num_parallel_tree=90, + n_estimators=90, max_depth=14, subsample=0.95, reg_alpha=0.0001, reg_lambda=0.0001, - min_rel_progress=0.02, + tol=0.02, ) df = penguins_df_default_index.dropna().sample(n=70) @@ -455,13 +452,12 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 assert reloaded_model.colsample_bynode == 0.95 - assert reloaded_model.early_stop is True assert reloaded_model.subsample == 0.95 assert reloaded_model.reg_alpha == 0.0001 assert reloaded_model.reg_lambda == 0.0001 - assert reloaded_model.min_rel_progress == 0.02 + assert reloaded_model.tol == 0.02 assert reloaded_model.gamma == 0.0 assert reloaded_model.max_depth == 14 assert reloaded_model.min_tree_child_weight == 2 - assert reloaded_model.num_parallel_tree == 90 + assert reloaded_model.n_estimators == 90 assert reloaded_model.enable_global_explain is False diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 3616cc4dd0c..99121e4a31d 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -58,15 +58,14 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase assert reloaded_model.optimize_strategy == "NORMAL_EQUATION" assert reloaded_model.fit_intercept is True assert reloaded_model.calculate_p_values is False - assert reloaded_model.early_stop is True assert reloaded_model.enable_global_explain is False assert reloaded_model.l1_reg is None assert reloaded_model.l2_reg == 0.0 - assert reloaded_model.learn_rate is None - assert reloaded_model.learn_rate_strategy == "line_search" - assert reloaded_model.ls_init_learn_rate is None + assert reloaded_model.learning_rate is None + assert reloaded_model.learning_rate_strategy == "line_search" + assert reloaded_model.ls_init_learning_rate is None assert reloaded_model.max_iterations == 20 - assert reloaded_model.min_rel_progress == 0.01 + assert reloaded_model.tol == 0.01 def test_linear_regression_customized_params_fit_score( @@ -75,12 +74,12 @@ def test_linear_regression_customized_params_fit_score( model = bigframes.ml.linear_model.LinearRegression( fit_intercept=False, l2_reg=0.2, - min_rel_progress=0.02, + tol=0.02, l1_reg=0.2, max_iterations=30, optimize_strategy="batch_gradient_descent", - learn_rate_strategy="constant", - learn_rate=0.2, + learning_rate_strategy="constant", + learning_rate=0.2, ) df = penguins_df_default_index.dropna() @@ -121,15 +120,14 @@ def test_linear_regression_customized_params_fit_score( assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" assert reloaded_model.fit_intercept is False assert reloaded_model.calculate_p_values is False - assert reloaded_model.early_stop is True assert reloaded_model.enable_global_explain is False assert reloaded_model.l1_reg == 0.2 assert reloaded_model.l2_reg == 0.2 - assert reloaded_model.ls_init_learn_rate is None + assert reloaded_model.ls_init_learning_rate is None assert reloaded_model.max_iterations == 30 - assert reloaded_model.min_rel_progress == 0.02 - assert reloaded_model.learn_rate_strategy == "CONSTANT" - assert reloaded_model.learn_rate == 0.2 + assert reloaded_model.tol == 0.02 + assert reloaded_model.learning_rate_strategy == "CONSTANT" + assert reloaded_model.learning_rate == 0.2 # TODO(garrettwu): add tests for param warm_start. Requires a trained model. @@ -177,7 +175,7 @@ def test_logistic_regression_configure_fit_score(penguins_df_default_index, data in reloaded_model._bqml_model.model_name ) assert reloaded_model.fit_intercept is True - assert reloaded_model.class_weights is None + assert reloaded_model.class_weight is None def test_logistic_regression_customized_params_fit_score( @@ -185,14 +183,14 @@ def test_logistic_regression_customized_params_fit_score( ): model = bigframes.ml.linear_model.LogisticRegression( fit_intercept=False, - class_weights="balanced", + class_weight="balanced", l2_reg=0.2, tol=0.02, l1_reg=0.2, max_iterations=30, optimize_strategy="batch_gradient_descent", - learn_rate_strategy="constant", - learn_rate=0.2, + learning_rate_strategy="constant", + learning_rate=0.2, ) df = penguins_df_default_index.dropna() X_train = df[ @@ -234,12 +232,13 @@ def test_logistic_regression_customized_params_fit_score( # TODO(garrettwu) optimize_strategy isn't logged in BQML # assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" assert reloaded_model.fit_intercept is False + assert reloaded_model.class_weight == "balanced" assert reloaded_model.calculate_p_values is False assert reloaded_model.enable_global_explain is False assert reloaded_model.l1_reg == 0.2 assert reloaded_model.l2_reg == 0.2 - assert reloaded_model.ls_init_learn_rate is None + assert reloaded_model.ls_init_learning_rate is None assert reloaded_model.max_iterations == 30 assert reloaded_model.tol == 0.02 - assert reloaded_model.learn_rate_strategy == "CONSTANT" - assert reloaded_model.learn_rate == 0.2 + assert reloaded_model.learning_rate_strategy == "CONSTANT" + assert reloaded_model.learning_rate == 0.2 diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index c7c4437a6ea..bcb220b1071 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -105,7 +105,7 @@ def test_linear_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -115,7 +115,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n early_stop=True,\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' ) @@ -157,14 +157,14 @@ def test_logistic_regression_params_fit( ): model = linear_model.LogisticRegression( fit_intercept=False, - class_weights="balanced", + class_weight="balanced", l2_reg=0.2, tol=0.02, l1_reg=0.2, max_iterations=30, optimize_strategy="batch_gradient_descent", - learn_rate_strategy="constant", - learn_rate=0.2, + learning_rate_strategy="constant", + learning_rate=0.2, ) model._bqml_model_factory = bqml_model_factory model.fit(mock_X, mock_y) diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 25d67f64c44..dcce75d1d90 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -20,21 +20,11 @@ class PCA(BaseEstimator, metaclass=ABCMeta): """Principal component analysis (PCA). - Linear dimensionality reduction using Singular Value Decomposition of the - data to project it to a lower dimensional space. The input data is centered - but not scaled for each feature before applying the SVD. - - It uses the LAPACK implementation of the full SVD or a randomized truncated - SVD by the method of Halko et al. 2009, depending on the shape of the input - data and the number of components to extract. - - It can also use the scipy.sparse.linalg ARPACK implementation of the - truncated SVD. - Args: - n_components (Optional[int], default 3): - Number of components to keep. if n_components is not set all components - are kept. + n_components (int, float or None, default None): + Number of components to keep. + If n_components is not set all components are kept. n_components = min(n_samples, n_features). + If 0 < n_components < 1, select the number of components such that the amount of variance that needs to be explained is greater than the percentage specified by n_components. svd_solver ("full", "randomized" or "auto", default "auto"): The solver to use to calculate the principal components. Details: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-pca#pca_solver. diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py index 63c62274fd1..53a211dd7f2 100644 --- a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -91,7 +91,7 @@ class RandomForestRegressor(ForestRegressor): to improve the predictive accuracy and control over-fitting. Args: - num_parallel_tree (Optional[int]): + n_estimators (Optional[int]): Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to @@ -116,10 +116,8 @@ class RandomForestRegressor(ForestRegressor): L1 regularization term on weights (xgb's alpha). Default to 0.0. reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop (Optional[bool]): - Whether training should stop after the first iteration. Default to True. - min_rel_progress (Optional[float]): - Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + tol (Optional[float]): + Minimum relative loss improvement necessary to continue training. Default to 0.01. enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. xgboost_version (Optional[str]): @@ -158,7 +156,7 @@ class RandomForestClassifier(ForestClassifier): improve the predictive accuracy and control over-fitting. Args: - num_parallel_tree (Optional[int]): + n_estimators (Optional[int]): Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to @@ -183,10 +181,8 @@ class RandomForestClassifier(ForestClassifier): L1 regularization term on weights (xgb's alpha). Default to 0.0. reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop (Optional[bool]): - Whether training should stop after the first iteration. Default to True. - min_rel_progress (Optional[float]): - Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + tol (Optional[float]): + Minimum relative loss improvement necessary to continue training. Default to 0.01. enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. xgboost_version (Optional[str]): diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index 7c7473e7135..a845b782c05 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -79,16 +79,14 @@ class LinearRegression(RegressorMixin, LinearModel): The maximum number of training iterations or steps. Default to 20. warm_start (bool, default False): Determines whether to train a model with new training data, new model options, or both. Unless you explicitly override them, the initial options used to train the model are used for the warm start run. Default to False. - learn_rate (float or None, default None): - The learn rate for gradient descent when learn_rate_strategy='constant'. If unset, value 0.1 is used. If learn_rate_strategy='line_search', an error is returned. - learn_rate_strategy (str, default "line_search"): + learning_rate (float or None, default None): + The learn rate for gradient descent when learning_rate_strategy='constant'. If unset, value 0.1 is used. If learning_rate_strategy='line_search', an error is returned. + learning_rate_strategy (str, default "line_search"): The strategy for specifying the learning rate during training. Default to "line_search". - early_stop (bool, default True): - Whether training should stop after the first iteration in which the relative loss improvement is less than the value specified for min_rel_progress. Default to True. - min_rel_progress (float, default 0.01): + tol (float, default 0.01): The minimum relative loss improvement that is necessary to continue training when EARLY_STOP is set to true. For example, a value of 0.01 specifies that each iteration must reduce the loss by 1% for training to continue. Default to 0.01. - ls_init_learn_rate (float or None, default None): - Sets the initial learning rate that learn_rate_strategy='line_search' uses. This option can only be used if line_search is specified. If unset, value 0.1 is used. + ls_init_learning_rate (float or None, default None): + Sets the initial learning rate that learning_rate_strategy='line_search' uses. This option can only be used if line_search is specified. If unset, value 0.1 is used. calculate_p_values (bool, default False): Specifies whether to compute p-values and standard errors during training. Default to False. enable_global_explain (bool, default False): diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 4cca3b136bf..88ff32ea06d 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -31,7 +31,7 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): fit_intercept (default True): Default True. Specifies if a constant (a.k.a. bias or intercept) should be added to the decision function. - class_weights (dict or 'balanced', default None): + class_weight (dict or 'balanced', default None): Default None. Weights associated with classes in the form ``{class_label: weight}``.If not given, all classes are supposed to have weight one. The "balanced" mode uses the values of y to @@ -47,14 +47,14 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): The maximum number of training iterations or steps. Default to 20. warm_start (bool, default False): Determines whether to train a model with new training data, new model options, or both. Unless you explicitly override them, the initial options used to train the model are used for the warm start run. Default to False. - learn_rate (float or None, default None): - The learn rate for gradient descent when learn_rate_strategy='constant'. If unset, value 0.1 is used. If learn_rate_strategy='line_search', an error is returned. - learn_rate_strategy (str, default "line_search"): + learning_rate (float or None, default None): + The learn rate for gradient descent when learning_rate_strategy='constant'. If unset, value 0.1 is used. If learning_rate_strategy='line_search', an error is returned. + learning_rate_strategy (str, default "line_search"): The strategy for specifying the learning rate during training. Default to "line_search". tol (float, default 0.01): The minimum relative loss improvement that is necessary to continue training when EARLY_STOP is set to true. For example, a value of 0.01 specifies that each iteration must reduce the loss by 1% for training to continue. Default to 0.01. - ls_init_learn_rate (float or None, default None): - Sets the initial learning rate that learn_rate_strategy='line_search' uses. This option can only be used if line_search is specified. If unset, value 0.1 is used. + ls_init_learning_rate (float or None, default None): + Sets the initial learning rate that learning_rate_strategy='line_search' uses. This option can only be used if line_search is specified. If unset, value 0.1 is used. calculate_p_values (bool, default False): Specifies whether to compute p-values and standard errors during training. Default to False. enable_global_explain (bool, default False): diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index dfd0ba73563..250e34dc2ca 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -55,7 +55,7 @@ class XGBRegressor(XGBModel, XGBRegressorBase): XGBoost regression model. Args: - num_parallel_tree (Optional[int]): + n_estimators (Optional[int]): Number of parallel trees constructed during each iteration. Default to 1. booster (Optional[str]): Specify which booster to use: gbtree or dart. Default to "gbtree". @@ -84,14 +84,12 @@ class XGBRegressor(XGBModel, XGBRegressorBase): L1 regularization term on weights (xgb's alpha). Default to 0.0. reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop (Optional[bool]): - Whether training should stop after the first iteration. Default to True. learning_rate (Optional[float]): Boosting learning rate (xgb's "eta"). Default to 0.3. max_iterations (Optional[int]): Maximum number of rounds for boosting. Default to 20. - min_rel_progress (Optional[float]): - Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + tol (Optional[float]): + Minimum relative loss improvement necessary to continue training. Default to 0.01. enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. xgboost_version (Optional[str]): @@ -104,7 +102,7 @@ class XGBClassifier(XGBModel, XGBClassifierMixIn, XGBClassifierBase): XGBoost classifier model. Args: - num_parallel_tree (Optional[int]): + n_estimators (Optional[int]): Number of parallel trees constructed during each iteration. Default to 1. booster (Optional[str]): Specify which booster to use: gbtree or dart. Default to "gbtree". @@ -133,14 +131,12 @@ class XGBClassifier(XGBModel, XGBClassifierMixIn, XGBClassifierBase): L1 regularization term on weights (xgb's alpha). Default to 0.0. reg_lambda (Optional[float]): L2 regularization term on weights (xgb's lambda). Default to 1.0. - early_stop (Optional[bool]): - Whether training should stop after the first iteration. Default to True. learning_rate (Optional[float]): Boosting learning rate (xgb's "eta"). Default to 0.3. max_iterations (Optional[int]): Maximum number of rounds for boosting. Default to 20. - min_rel_progress (Optional[float]): - Minimum relative loss improvement necessary to continue training when early_stop is set to True. Default to 0.01. + tol (Optional[float]): + Minimum relative loss improvement necessary to continue training. Default to 0.01. enable_global_explain (Optional[bool]): Whether to compute global explanations using explainable AI to evaluate global feature importance to the model. Default to False. xgboost_version (Optional[str]): From d6ee994c17e0b1dd6768b09ee81d2c902f601b76 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Sun, 24 Mar 2024 15:10:16 -0700 Subject: [PATCH 418/921] fix: plot.scatter `c` argument functionalities (#494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes internal bug: b/330770901 🦕 --- bigframes/operations/_matplotlib/core.py | 58 ++++++++++++++++++- .../system/small/operations/test_plotting.py | 31 ++++++++++ .../pandas/plotting/_core.py | 4 -- 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index 663e7a789f4..ad5abb4bcad 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -14,6 +14,12 @@ import abc import typing +import uuid + +import pandas as pd + +import bigframes.constants as constants +import bigframes.dtypes as dtypes DEFAULT_SAMPLING_N = 1000 DEFAULT_SAMPLING_STATE = 0 @@ -44,12 +50,13 @@ def _kind(self): def __init__(self, data, **kwargs) -> None: self.kwargs = kwargs - self.data = self._compute_plot_data(data) + self.data = data def generate(self) -> None: - self.axes = self.data.plot(kind=self._kind, **self.kwargs) + plot_data = self._compute_plot_data() + self.axes = plot_data.plot(kind=self._kind, **self.kwargs) - def _compute_plot_data(self, data): + def _compute_sample_data(self, data): # TODO: Cache the sampling data in the PlotAccessor. sampling_n = self.kwargs.pop("sampling_n", DEFAULT_SAMPLING_N) sampling_random_state = self.kwargs.pop( @@ -61,6 +68,9 @@ def _compute_plot_data(self, data): sort=False, ).to_pandas() + def _compute_plot_data(self): + return self._compute_sample_data(self.data) + class LinePlot(SamplingPlot): @property @@ -78,3 +88,45 @@ class ScatterPlot(SamplingPlot): @property def _kind(self) -> typing.Literal["scatter"]: return "scatter" + + def __init__(self, data, **kwargs) -> None: + super().__init__(data, **kwargs) + + c = self.kwargs.get("c", None) + if self._is_sequence_arg(c): + raise NotImplementedError( + f"Only support a single color string or a column name/posision. {constants.FEEDBACK_LINK}" + ) + + def _compute_plot_data(self): + sample = self._compute_sample_data(self.data) + + # Works around a pandas bug: + # https://github.com/pandas-dev/pandas/commit/45b937d64f6b7b6971856a47e379c7c87af7e00a + c = self.kwargs.get("c", None) + if pd.core.dtypes.common.is_integer(c): + c = self.data.columns[c] + if self._is_column_name(c, sample) and sample[c].dtype == dtypes.STRING_DTYPE: + sample[c] = sample[c].astype("object") + + return sample + + def _is_sequence_arg(self, arg): + return ( + arg is not None + and not isinstance(arg, str) + and isinstance(arg, typing.Iterable) + ) + + def _is_column_name(self, arg, data): + return ( + arg is not None + and pd.core.dtypes.common.is_hashable(arg) + and arg in data.columns + ) + + def _generate_new_column_name(self, data): + col_name = None + while col_name is None or col_name in data.columns: + col_name = f"plot_temp_{str(uuid.uuid4())[:8]}" + return col_name diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index 5ca3382e2a1..41ea7d4ebb8 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -209,6 +209,37 @@ def test_scatter(scalars_dfs): ) +@pytest.mark.parametrize( + ("c"), + [ + pytest.param("red", id="red"), + pytest.param("c", id="int_column"), + pytest.param("species", id="color_column"), + pytest.param(3, id="column_index"), + ], +) +def test_scatter_args_c(c): + data = { + "a": [1, 2, 3], + "b": [1, 2, 3], + "c": [1, 2, 3], + "species": ["r", "g", "b"], + } + df = bpd.DataFrame(data) + pd_df = pd.DataFrame(data) + + ax = df.plot.scatter(x="a", y="b", c=c) + pd_ax = pd_df.plot.scatter(x="a", y="b", c=c) + assert len(ax.collections[0].get_facecolor()) == len( + pd_ax.collections[0].get_facecolor() + ) + for idx in range(len(ax.collections[0].get_facecolor())): + tm.assert_numpy_array_equal( + ax.collections[0].get_facecolor()[idx], + pd_ax.collections[0].get_facecolor()[idx], + ) + + def test_sampling_plot_args_n(): df = bpd.DataFrame(np.arange(bf_mpl.DEFAULT_SAMPLING_N * 10), columns=["one"]) ax = df.plot.line() diff --git a/third_party/bigframes_vendored/pandas/plotting/_core.py b/third_party/bigframes_vendored/pandas/plotting/_core.py index d901f41ef87..f8da9efdc00 100644 --- a/third_party/bigframes_vendored/pandas/plotting/_core.py +++ b/third_party/bigframes_vendored/pandas/plotting/_core.py @@ -266,10 +266,6 @@ def scatter( - A single color string referred to by name, RGB or RGBA code, for instance 'red' or '#a98d19'. - - A sequence of color strings referred to by name, RGB or RGBA - code, which will be used for each point's color recursively. For - instance ['green','yellow'] all points will be filled in green or - yellow, alternatively. - A column name or position whose values will be used to color the marker points according to a colormap. From fbada4a70688c5d13fa35d1843b0c4252c5ced72 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 25 Mar 2024 14:22:30 +0000 Subject: [PATCH 419/921] feat: support BQ regional endpoints for europe-west9, europe-west3, us-east4, and us-west1 (#504) --- bigframes/session/clients.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index 7574aa44548..d97e53901d2 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -38,7 +38,13 @@ _SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] # Regions for which Regional Endpoints (REPs) are supported -_REP_SUPPORTED_REGIONS = {"me-central2"} +_REP_SUPPORTED_REGIONS = { + "me-central2", + "europe-west9", + "europe-west3", + "us-east4", + "us-west1", +} # BigQuery is a REST API, which requires the protocol as part of the URL. From 0ac0cd19bd9bad92b84aebc6da94d0e7f2654a28 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 25 Mar 2024 14:23:31 +0000 Subject: [PATCH 420/921] test: use `reuse=False` in more `remote_function` e2e tests (#503) --- tests/system/large/test_remote_function.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index f1ae56cb3ca..cf6b2a01f80 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1047,7 +1047,7 @@ def test_remote_function_via_session_context_connection_setter( # unique dataset_id, even though the cloud function would be reused, the bq # remote function would still be created, making use of the bq connection # set in the BigQueryOptions above. - @session.remote_function([int], int, dataset=dataset_id) + @session.remote_function([int], int, dataset=dataset_id, reuse=False) def square(x): return x * x @@ -1084,7 +1084,7 @@ def square(x): def test_remote_function_default_connection(session, scalars_dfs, dataset_id): try: - @session.remote_function([int], int, dataset=dataset_id) + @session.remote_function([int], int, dataset=dataset_id, reuse=False) def square(x): return x * x @@ -1121,7 +1121,7 @@ def square(x): def test_remote_function_runtime_error(session, scalars_dfs, dataset_id): try: - @session.remote_function([int], int, dataset=dataset_id) + @session.remote_function([int], int, dataset=dataset_id, reuse=False) def square(x): return x * x From 4bfe094fdf2f7e1af72cc939558713a499760129 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 25 Mar 2024 14:25:24 +0000 Subject: [PATCH 421/921] docs: update bigquery connection documentation (#499) * docs: update bigquery connection documentation * be more specific about the service accounts * more rewording * make rst hyperlink labels non-unique * remove confusing resource level IAM docs until we support that --- README.rst | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index ad96382df88..73709641def 100644 --- a/README.rst +++ b/README.rst @@ -232,16 +232,18 @@ you must enable the following APIs: * The BigQuery Connection API (bigqueryconnection.googleapis.com) * The Vertex AI API (aiplatform.googleapis.com) -and you must be granted the following IAM roles: +and you must be granted the following IAM roles in the project: * BigQuery Data Editor (roles/bigquery.dataEditor) * BigQuery Connection Admin (roles/bigquery.connectionAdmin) -* Service Account User (roles/iam.serviceAccountUser) on the - `service account `__ - ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` +* Service Account User (roles/iam.serviceAccountUser) * Vertex AI User (roles/aiplatform.user) * Project IAM Admin (roles/resourcemanager.projectIamAdmin) if using default - BigQuery connection, or Browser (roles/browser) if using a pre-created connection + BigQuery connection, or Browser (roles/browser) if using a pre-configured connection. + This requirement can be avoided by setting + ``bigframes.pandas.options.bigquery.skip_bq_connection_check`` option to ``True``, + in which case the connection (default or pre-configured) would be + used as-is without any existence or permission check. ML locations @@ -288,11 +290,17 @@ into `BigQuery remote functions `_ . Creating a remote function in BigQuery DataFrames (See `code samples `_) -creates a BigQuery remote function, a `BigQuery -connection -`_ , -and a `Cloud Functions (2nd gen) function -`_ . +creates: + +1. A `Cloud Functions (2nd gen) function `_. +2. A `BigQuery connection `_. + If the BigQuery connection is created, the BigQuery service will + create a + `Google Cloud-managed IAM service account `_ + and attach it to the connection. You can use a pre-configured BigQuery + connection if you prefer, in which case the connection creation is skipped. +3. A BigQuery remote function that talks to the cloud function (1) using the BigQuery + connection (2). BigQuery connections are created in the same location as the BigQuery DataFrames session, using the name you provide in the custom function @@ -331,17 +339,19 @@ To use BigQuery DataFrames remote functions, you must enable the following APIs: * The Cloud Resource Manager API (cloudresourcemanager.googleapis.com) To use BigQuery DataFrames remote functions, you must be granted the -following IAM roles: +following IAM roles in the project: * BigQuery Data Editor (roles/bigquery.dataEditor) * BigQuery Connection Admin (roles/bigquery.connectionAdmin) * Cloud Functions Developer (roles/cloudfunctions.developer) -* Service Account User (roles/iam.serviceAccountUser) on the - `service account `__ - ``PROJECT_NUMBER-compute@developer.gserviceaccount.com`` +* Service Account User (roles/iam.serviceAccountUser) * Storage Object Viewer (roles/storage.objectViewer) * Project IAM Admin (roles/resourcemanager.projectIamAdmin) if using default - BigQuery connection, or Browser (roles/browser) if using a pre-created connection + BigQuery connection, or Browser (roles/browser) if using a pre-configured connection. + This requirement can be avoided by setting + ``bigframes.pandas.options.bigquery.skip_bq_connection_check`` option to ``True``, + in which case the connection (default or pre-configured) would be + used as-is without any existence or permission check. **Limitations** From 82b4f91db365fe06d8bd0bf938f880a48091104e Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 25 Mar 2024 07:49:32 -0700 Subject: [PATCH 422/921] fix: Use actual BigQuery types rather than ibis types in to_pandas (#500) * fix: use actual bigframes types rather than ibis types in to_pandas * Use ibis function that properly converts nested bq datatypes to ibis type --- bigframes/dtypes.py | 11 +++++++++++ bigframes/session/__init__.py | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 954dd270eea..63adc059f3c 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -682,6 +682,17 @@ def ibis_type_from_python_type(t: type) -> ibis_dtypes.DataType: def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> ibis_dtypes.DataType: + """Convert bq type to ibis. Only to be used for remote functions, does not handle all types.""" if tk not in SUPPORTED_IO_BIGQUERY_TYPEKINDS: raise UnsupportedTypeError(tk, SUPPORTED_IO_BIGQUERY_TYPEKINDS) return third_party_ibis_bqtypes.BigQueryType.to_ibis(tk) + + +def bf_type_from_type_kind(bf_schema) -> Dict[str, Dtype]: + """Converts bigquery sql type to the default bigframes dtype.""" + ibis_schema: ibis.Schema = third_party_ibis_bqtypes.BigQuerySchema.to_ibis( + bf_schema + ) + return { + name: ibis_dtype_to_bigframes_dtype(type) for name, type in ibis_schema.items() + } diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 74a8325dac4..479b3a7bacc 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1889,8 +1889,10 @@ def _get_table_size(self, destination_table): def _rows_to_dataframe( self, row_iterator: bigquery.table.RowIterator, dtypes: Dict ) -> pandas.DataFrame: + # Can ignore inferred datatype until dtype emulation breaks 1:1 mapping between BQ types and bigframes types + dtypes_from_bq = bigframes.dtypes.bf_type_from_type_kind(row_iterator.schema) arrow_table = row_iterator.to_arrow() - return bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) + return bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes_from_bq) def _start_generic_job(self, job: formatting_helpers.GenericJob): if bigframes.options.display.progress_bar is not None: From 1e2cd74f725c6f7b9cfadb7f59ccb85253151a88 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:16:54 -0500 Subject: [PATCH 423/921] chore(main): release 1.0.0 (#476) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bca26e361e..72d0e833bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,68 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.0.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.26.0...v1.0.0) (2024-03-25) + + +### ⚠ BREAKING CHANGES + +* rename model parameter `min_rel_progress` to `tol` +* `early_stop` setting no longer supported, always uses `True` +* rename model parameter `n_parallell_trees` to `n_estimators` +* rename `class_weights` to `class_weight` +* rename `learn_rate` to `learning_rate` +* PCA `n_components` supports float value and `None`, default to `None` +* rename various ml model parameters for consistency with sklearn (https://github.com/googleapis/python-bigquery-dataframes/pull/491) + +### Features + +* Add configuration option to read_gbq ([#401](https://github.com/googleapis/python-bigquery-dataframes/issues/401)) ([85cede2](https://github.com/googleapis/python-bigquery-dataframes/commit/85cede22587a9fe1dae888721492f9390dc46d70)) +* Add ml ARIMAPlus model params ([#488](https://github.com/googleapis/python-bigquery-dataframes/issues/488)) ([352cb85](https://github.com/googleapis/python-bigquery-dataframes/commit/352cb850d23e41a2278edf0df584b89ee9619aab)) +* Add ml KMeans model params ([#477](https://github.com/googleapis/python-bigquery-dataframes/issues/477)) ([23a8d9a](https://github.com/googleapis/python-bigquery-dataframes/commit/23a8d9a32e1619aff92c8dfabb7bcdd54c314bd5)) +* Add ml LogisticRegression model params ([#481](https://github.com/googleapis/python-bigquery-dataframes/issues/481)) ([f959b65](https://github.com/googleapis/python-bigquery-dataframes/commit/f959b653a0e82b5bfd21f9e994031cf6d25c281a)) +* Add ml PCA model params ([#474](https://github.com/googleapis/python-bigquery-dataframes/issues/474)) ([fb5d83b](https://github.com/googleapis/python-bigquery-dataframes/commit/fb5d83b1e35c465cff486e6cf7862e5b32e3c65a)) +* Add params for LinearRegression model ([#464](https://github.com/googleapis/python-bigquery-dataframes/issues/464)) ([21b2188](https://github.com/googleapis/python-bigquery-dataframes/commit/21b2188cd0ca85485b5171ee9e46da4c924e2ff8)) +* Add support for Python 3.12 ([#231](https://github.com/googleapis/python-bigquery-dataframes/issues/231)) ([df2976f](https://github.com/googleapis/python-bigquery-dataframes/commit/df2976fa9fd0319b824128d0ccf2ebb20f381caa)) +* Allow assigning directly to Series.name property ([#495](https://github.com/googleapis/python-bigquery-dataframes/issues/495)) ([ad0e99e](https://github.com/googleapis/python-bigquery-dataframes/commit/ad0e99eddb1dddd3d439cea7db1e4f222b45c6b9)) +* Ensure `Series.str.len()` can get length of array columns ([#497](https://github.com/googleapis/python-bigquery-dataframes/issues/497)) ([10c0446](https://github.com/googleapis/python-bigquery-dataframes/commit/10c044686228e5c6f3868c1eb10454f6a086ac8b)) +* Option to use bq connection without check ([#460](https://github.com/googleapis/python-bigquery-dataframes/issues/460)) ([0b3f8e5](https://github.com/googleapis/python-bigquery-dataframes/commit/0b3f8e5ce63f75ba99ee8cf29226a0fd38bef99f)) +* PCA `n_components` supports float value and `None`, default to `None` ([65c6f47](https://github.com/googleapis/python-bigquery-dataframes/commit/65c6f4736d1a5552835e4cec8b777b2c0f3dd8da)) +* Rename `class_weights` to `class_weight` ([65c6f47](https://github.com/googleapis/python-bigquery-dataframes/commit/65c6f4736d1a5552835e4cec8b777b2c0f3dd8da)) +* Rename `learn_rate` to `learning_rate` ([65c6f47](https://github.com/googleapis/python-bigquery-dataframes/commit/65c6f4736d1a5552835e4cec8b777b2c0f3dd8da)) +* Rename model parameter `min_rel_progress` to `tol` ([65c6f47](https://github.com/googleapis/python-bigquery-dataframes/commit/65c6f4736d1a5552835e4cec8b777b2c0f3dd8da)) +* Rename model parameter `n_parallell_trees` to `n_estimators` ([65c6f47](https://github.com/googleapis/python-bigquery-dataframes/commit/65c6f4736d1a5552835e4cec8b777b2c0f3dd8da)) +* Rename various ml model parameters for consistency with sklearn (https://github.com/googleapis/python-bigquery-dataframes/pull/491) ([65c6f47](https://github.com/googleapis/python-bigquery-dataframes/commit/65c6f4736d1a5552835e4cec8b777b2c0f3dd8da)) +* Support BQ regional endpoints for europe-west9, europe-west3, us-east4, and us-west1 ([#504](https://github.com/googleapis/python-bigquery-dataframes/issues/504)) ([fbada4a](https://github.com/googleapis/python-bigquery-dataframes/commit/fbada4a70688c5d13fa35d1843b0c4252c5ced72)) +* Support dataframe.cov ([#498](https://github.com/googleapis/python-bigquery-dataframes/issues/498)) ([c4beafd](https://github.com/googleapis/python-bigquery-dataframes/commit/c4beafdf0c1ba88b306ca96fa3ca46b86debaa4c)) +* Support Series.dt.floor ([#493](https://github.com/googleapis/python-bigquery-dataframes/issues/493)) ([2dd01c2](https://github.com/googleapis/python-bigquery-dataframes/commit/2dd01c25e9f01c03979c61e71d3c5cd9f0bd4c96)) +* Support Series.dt.normalize ([#483](https://github.com/googleapis/python-bigquery-dataframes/issues/483)) ([0bf1e91](https://github.com/googleapis/python-bigquery-dataframes/commit/0bf1e916c2b636ec02ac010190e89d38e88fce4b)) +* Update plot sample to 1000 rows ([#458](https://github.com/googleapis/python-bigquery-dataframes/issues/458)) ([60d4a7b](https://github.com/googleapis/python-bigquery-dataframes/commit/60d4a7bbac867256f8bbfd3053c7dd2645c1b062)) + + +### Bug Fixes + +* `early_stop` setting no longer supported, always uses `True` ([65c6f47](https://github.com/googleapis/python-bigquery-dataframes/commit/65c6f4736d1a5552835e4cec8b777b2c0f3dd8da)) +* Fix -1 offset lookups failing ([#463](https://github.com/googleapis/python-bigquery-dataframes/issues/463)) ([2dfb9c2](https://github.com/googleapis/python-bigquery-dataframes/commit/2dfb9c24d07841d785e41b33573c5f3a218efeea)) +* Plot.scatter `c` argument functionalities ([#494](https://github.com/googleapis/python-bigquery-dataframes/issues/494)) ([d6ee994](https://github.com/googleapis/python-bigquery-dataframes/commit/d6ee994c17e0b1dd6768b09ee81d2c902f601b76)) +* Properly support format param for numerical input. ([#486](https://github.com/googleapis/python-bigquery-dataframes/issues/486)) ([ae20c35](https://github.com/googleapis/python-bigquery-dataframes/commit/ae20c3583d5526777548b5d594ecca6034bb49ec)) +* Renable to_csv and to_json related tests ([#468](https://github.com/googleapis/python-bigquery-dataframes/issues/468)) ([2b9a01d](https://github.com/googleapis/python-bigquery-dataframes/commit/2b9a01de0adb8d41fbe73ce94b1acc8d22f507b5)) +* Sampling plot cannot preserve ordering if index is not ordered ([#475](https://github.com/googleapis/python-bigquery-dataframes/issues/475)) ([a5345fe](https://github.com/googleapis/python-bigquery-dataframes/commit/a5345fe8943667a89fcba48ce31aa8ecfc283f92)) +* Use actual BigQuery types rather than ibis types in to_pandas ([#500](https://github.com/googleapis/python-bigquery-dataframes/issues/500)) ([82b4f91](https://github.com/googleapis/python-bigquery-dataframes/commit/82b4f91db365fe06d8bd0bf938f880a48091104e)) + + +### Dependencies + +* Support pandas 2.2 ([#492](https://github.com/googleapis/python-bigquery-dataframes/issues/492)) ([e2cf50e](https://github.com/googleapis/python-bigquery-dataframes/commit/e2cf50e053f7163d1654c4b5621cc93e922d5148)) + + +### Documentation + +* Add code samples for metrics.{accuracy_score, confusion_matrix} ([#478](https://github.com/googleapis/python-bigquery-dataframes/issues/478)) ([3e3329a](https://github.com/googleapis/python-bigquery-dataframes/commit/3e3329a37c1020bd3e6d4d5e980103c63ab0c337)) +* Add code samples for metrics.{recall_score, precision_score, f11_score} ([#502](https://github.com/googleapis/python-bigquery-dataframes/issues/502)) ([370fe90](https://github.com/googleapis/python-bigquery-dataframes/commit/370fe9087848862d02f0e5a333fcb4cd37cf5ca0)) +* Improve API documentation ([#489](https://github.com/googleapis/python-bigquery-dataframes/issues/489)) ([751266e](https://github.com/googleapis/python-bigquery-dataframes/commit/751266e056ac566ef5b6e40fbbca84ed95e7a7a9)) +* Update bigquery connection documentation ([#499](https://github.com/googleapis/python-bigquery-dataframes/issues/499)) ([4bfe094](https://github.com/googleapis/python-bigquery-dataframes/commit/4bfe094fdf2f7e1af72cc939558713a499760129)) +* Update LLM + K-means notebook to handle partial failures ([#496](https://github.com/googleapis/python-bigquery-dataframes/issues/496)) ([97afad9](https://github.com/googleapis/python-bigquery-dataframes/commit/97afad96f80c1815db8ad34f0ff62095631036c2)) + ## [0.26.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.25.0...v0.26.0) (2024-03-20) diff --git a/bigframes/version.py b/bigframes/version.py index 8066f4353ac..8e31592250b 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.26.0" +__version__ = "1.0.0" From 92a1af35b8de4afb6cdb5b5e89facdceb5c151d2 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:10:17 -0700 Subject: [PATCH 424/921] docs: add progress_bar code sample (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../bigframes_vendored/pandas/core/config_init.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index 33c6b3e093a..ecc103d7c86 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -32,9 +32,21 @@ User can execute the job by calling .to_pandas() >>> # df.to_pandas() -Reset option +Reset repr_mode option >>> bpd.options.display.repr_mode = "head" +Can also set the progress_bar option to see the progress bar in terminal, + >>> bpd.options.display.progress_bar = "terminal" + +notebook, + >>> bpd.options.display.progress_bar = "notebook" + +or just remove it. + >>> bpd.options.display.progress_bar = None + +Setting to default value "auto" will detect and show progress bar automatically. + >>> bpd.options.display.progress_bar = "auto" + Attributes: max_columns (int, default 20): If `max_columns` is exceeded, switch to truncate view. From 036649e7edbd8528196a3ce4b64837b554d83ecf Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:36:33 -0700 Subject: [PATCH 425/921] chore: fix model.register test to use anonymous dataset (#510) --- tests/system/small/ml/conftest.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index c9100f36f39..33351afe451 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -47,12 +47,11 @@ def penguins_bqml_linear_model(session, penguins_linear_model_name) -> core.Bqml @pytest.fixture(scope="function") def ephemera_penguins_bqml_linear_model( - penguins_bqml_linear_model, + session: bigframes.Session, + penguins_bqml_linear_model: core.BqmlModel, ) -> core.BqmlModel: model = penguins_bqml_linear_model - return model.copy( - f"{model._model.project}.{model._model.dataset_id}.{uuid.uuid4().hex}" - ) + return model.copy(f"{session._anonymous_dataset}.{uuid.uuid4().hex}") @pytest.fixture(scope="session") From e8e66cf25887f64d2a7cb26081c2ef3cea10827d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 25 Mar 2024 20:40:27 -0700 Subject: [PATCH 426/921] feat: Add support for numpy expm1, log1p, floor, ceil, arctan2 ops (#505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Sweña (Swast) --- bigframes/core/compile/scalar_op_compiler.py | 54 ++++++++++++++++++++ bigframes/operations/__init__.py | 10 ++++ tests/system/small/test_numpy.py | 22 ++++++++ 3 files changed, 86 insertions(+) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index d2fc4538354..5c165fa1df4 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -257,6 +257,13 @@ def arctan_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.NumericValue, x).atan() +@scalar_op_compiler.register_binary_op(ops.arctan2_op) +def arctan2_op_impl(x: ibis_types.Value, y: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).atan2( + typing.cast(ibis_types.NumericValue, y) + ) + + # Hyperbolic trig functions # BQ has these functions, but Ibis doesn't @scalar_op_compiler.register_unary_op(ops.sinh_op) @@ -319,6 +326,30 @@ def arctanh_op_impl(x: ibis_types.Value): # Numeric Ops +@scalar_op_compiler.register_unary_op(ops.floor_op) +def floor_op_impl(x: ibis_types.Value): + x_numeric = typing.cast(ibis_types.NumericValue, x) + if x_numeric.type().is_integer(): + return x_numeric.cast(ibis_dtypes.Float64()) + if x_numeric.type().is_floating(): + # Default ibis impl tries to cast to integer, which doesn't match pandas and can overflow + return float_floor(x_numeric) + else: # numeric + return x_numeric.floor() + + +@scalar_op_compiler.register_unary_op(ops.ceil_op) +def ceil_op_impl(x: ibis_types.Value): + x_numeric = typing.cast(ibis_types.NumericValue, x) + if x_numeric.type().is_integer(): + return x_numeric.cast(ibis_dtypes.Float64()) + if x_numeric.type().is_floating(): + # Default ibis impl tries to cast to integer, which doesn't match pandas and can overflow + return float_ceil(x_numeric) + else: # numeric + return x_numeric.ceil() + + @scalar_op_compiler.register_unary_op(ops.abs_op) def abs_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.NumericValue, x).abs() @@ -347,6 +378,11 @@ def ln_op_impl(x: ibis_types.Value): return (~domain).ifelse(out_of_domain, numeric_value.ln()) +@scalar_op_compiler.register_unary_op(ops.log1p_op) +def log1p_op_impl(x: ibis_types.Value): + return ln_op_impl(_ibis_num(1) + x) + + @scalar_op_compiler.register_unary_op(ops.exp_op) def exp_op_impl(x: ibis_types.Value): numeric_value = typing.cast(ibis_types.NumericValue, x) @@ -354,6 +390,11 @@ def exp_op_impl(x: ibis_types.Value): return (~domain).ifelse(_INF, numeric_value.exp()) +@scalar_op_compiler.register_unary_op(ops.expm1_op) +def expm1_op_impl(x: ibis_types.Value): + return exp_op_impl(x) - _ibis_num(1) + + @scalar_op_compiler.register_unary_op(ops.invert_op) def invert_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.NumericValue, x).negate() @@ -1318,3 +1359,16 @@ def _ibis_num(number: float): @ibis.udf.scalar.builtin def timestamp(a: str) -> ibis_dtypes.timestamp: """Convert string to timestamp.""" + + +# Need these because ibis otherwise tries to do casts to int that can fail +@ibis.udf.scalar.builtin(name="floor") +def float_floor(a: float) -> float: + """Convert string to timestamp.""" + return 0 # pragma: NO COVER + + +@ibis.udf.scalar.builtin(name="ceil") +def float_ceil(a: float) -> float: + """Convert string to timestamp.""" + return 0 # pragma: NO COVER diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 4ecb8dca5a5..2ef71fde7fa 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -246,11 +246,16 @@ def create_ternary_op( arcsinh_op = create_unary_op(name="arcsinh", type_rule=op_typing.REAL_NUMERIC) arccosh_op = create_unary_op(name="arccosh", type_rule=op_typing.REAL_NUMERIC) arctanh_op = create_unary_op(name="arctanh", type_rule=op_typing.REAL_NUMERIC) +arctan2_op = create_binary_op(name="arctan2", type_rule=op_typing.REAL_NUMERIC) ## Numeric Ops +floor_op = create_unary_op(name="floor", type_rule=op_typing.REAL_NUMERIC) +ceil_op = create_unary_op(name="ceil", type_rule=op_typing.REAL_NUMERIC) abs_op = create_unary_op(name="abs", type_rule=op_typing.INPUT_TYPE) exp_op = create_unary_op(name="exp", type_rule=op_typing.REAL_NUMERIC) +expm1_op = create_unary_op(name="expm1", type_rule=op_typing.REAL_NUMERIC) ln_op = create_unary_op(name="log", type_rule=op_typing.REAL_NUMERIC) log10_op = create_unary_op(name="log10", type_rule=op_typing.REAL_NUMERIC) +log1p_op = create_unary_op(name="log1p", type_rule=op_typing.REAL_NUMERIC) sqrt_op = create_unary_op(name="sqrt", type_rule=op_typing.REAL_NUMERIC) @@ -540,6 +545,10 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT np.log10: log10_op, np.sqrt: sqrt_op, np.abs: abs_op, + np.floor: floor_op, + np.ceil: ceil_op, + np.log1p: log1p_op, + np.expm1: expm1_op, } @@ -549,4 +558,5 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT np.multiply: mul_op, np.divide: div_op, np.power: pow_op, + np.arctan2: arctan2_op, } diff --git a/tests/system/small/test_numpy.py b/tests/system/small/test_numpy.py index 5c2a93ec394..8e349e472a1 100644 --- a/tests/system/small/test_numpy.py +++ b/tests/system/small/test_numpy.py @@ -56,6 +56,10 @@ def test_series_ufuncs(floats_pd, floats_bf, opname): ("log10",), ("sqrt",), ("abs",), + ("floor",), + ("ceil",), + ("expm1",), + ("log1p",), ], ) def test_df_ufuncs(scalars_dfs, opname): @@ -77,6 +81,7 @@ def test_df_ufuncs(scalars_dfs, opname): ("multiply",), ("divide",), ("power",), + ("arctan2",), ], ) def test_series_binary_ufuncs(floats_product_pd, floats_product_bf, opname): @@ -112,6 +117,23 @@ def test_df_binary_ufuncs(scalars_dfs, opname): pd.testing.assert_frame_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("x", "y"), + [ + ("int64_col", "int64_col"), + ("float64_col", "int64_col"), + ], +) +def test_series_atan2(scalars_dfs, x, y): + # Test atan2 separately as pandas errors when passing entire df as input, so pass only series + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = np.arctan2(scalars_df[x], scalars_df[y]).to_pandas() + pd_result = np.arctan2(scalars_pandas_df[x], scalars_pandas_df[y]) + + pd.testing.assert_series_equal(bf_result, pd_result) + + def test_series_binary_ufuncs_reverse(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs From 5e28ebd1ba3a5559e093c2ea676c0714c1434ba9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 26 Mar 2024 12:06:47 -0700 Subject: [PATCH 427/921] feat: add `DataFrame.eval` and `DataFrame.query` (#361) * feat: add DataFrame.eval, DataFrame.query * address pr comments * add docstring, disable new tests for legacy pandas * vendor the pandas eval implementation * amend eval docstring * fix doctest expectation * amend doctest * pr comments * Fix doctest for eval --- bigframes/core/eval.py | 71 ++ bigframes/dataframe.py | 11 + tests/system/small/test_dataframe.py | 38 + .../bigframes_vendored/pandas/core/common.py | 26 + .../pandas/core/computation/align.py | 226 +++++ .../pandas/core/computation/common.py | 48 + .../pandas/core/computation/engines.py | 94 ++ .../pandas/core/computation/eval.py | 368 ++++++++ .../pandas/core/computation/expr.py | 828 ++++++++++++++++++ .../pandas/core/computation/ops.py | 605 +++++++++++++ .../pandas/core/computation/parsing.py | 196 +++++ .../pandas/core/computation/scope.py | 355 ++++++++ .../pandas/core/dtypes/inference.py | 31 + .../bigframes_vendored/pandas/core/frame.py | 153 ++++ .../pandas/util/_exceptions.py | 29 + .../pandas/util/_validators.py | 58 ++ 16 files changed, 3137 insertions(+) create mode 100644 bigframes/core/eval.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/align.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/common.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/engines.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/eval.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/expr.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/ops.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/parsing.py create mode 100644 third_party/bigframes_vendored/pandas/core/computation/scope.py create mode 100644 third_party/bigframes_vendored/pandas/core/dtypes/inference.py create mode 100644 third_party/bigframes_vendored/pandas/util/_exceptions.py create mode 100644 third_party/bigframes_vendored/pandas/util/_validators.py diff --git a/bigframes/core/eval.py b/bigframes/core/eval.py new file mode 100644 index 00000000000..692ca1c7bb0 --- /dev/null +++ b/bigframes/core/eval.py @@ -0,0 +1,71 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +from typing import Optional + +import bigframes_vendored.pandas.core.computation.eval as vendored_pandas_eval +import bigframes_vendored.pandas.core.computation.parsing as vendored_pandas_eval_parsing + +import bigframes.dataframe as dataframe +import bigframes.dtypes +import bigframes.series as series + + +def eval(df: dataframe.DataFrame, expr: str, target: Optional[dataframe.DataFrame]): + """ + Evaluate the given python expression + + Args: + df (DataFrame): + Columns of this dataframe will be used to resolve variables in expression. + expr (str): + One or more python expression to evaluate. + target (DataFrame or None): + The evaluation result will be written to the target if provided. + + Returns: + Result of evaluation. + """ + index_resolver = { + vendored_pandas_eval_parsing.clean_column_name(str(name)): EvalSeries( + df.index.get_level_values(level).to_series() + ) + for level, name in enumerate(df.index.names) + } + column_resolver = { + vendored_pandas_eval_parsing.clean_column_name(str(name)): EvalSeries(series) + for name, series in df.items() + } + # 3 Levels: user -> logging wrapper -> dataframe -> eval helper (this) + return vendored_pandas_eval.eval( + expr=expr, level=3, target=target, resolvers=(index_resolver, column_resolver) # type: ignore + ) + + +@dataclasses.dataclass +class FakeNumpyArray: + dtype: bigframes.dtypes.Dtype + + +class EvalSeries(series.Series): + """Slight modified series that works better with pandas.eval""" + + def __init__(self, underlying: series.Series): + super().__init__(data=underlying._block) + + @property + def values(self): + """Returns fake numpy array with only dtype property so that eval can determine schema without actually downloading the data.""" + return FakeNumpyArray(self.dtype) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 07dae2c53b8..7e82ba125ca 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1493,6 +1493,17 @@ def sort_values( ) return DataFrame(self._block.order_by(ordering)) + def eval(self, expr: str) -> DataFrame: + import bigframes.core.eval as bf_eval + + return bf_eval.eval(self, expr, target=self) + + def query(self, expr: str) -> DataFrame: + import bigframes.core.eval as bf_eval + + eval_result = bf_eval.eval(self, expr, target=None) + return self[eval_result] + def value_counts( self, subset: typing.Union[blocks.Label, typing.Sequence[blocks.Label]] = None, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 99ee6680fa9..e58a6667096 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3822,6 +3822,44 @@ def test_df_to_orc(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result +@skip_legacy_pandas +@pytest.mark.parametrize( + ("expr",), + [ + ("new_col = int64_col + int64_too",), + ("new_col = (rowindex > 3) | bool_col",), + ("int64_too = bool_col\nnew_col2 = rowindex",), + ], +) +def test_df_eval(scalars_dfs, expr): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.eval(expr).to_pandas() + pd_result = scalars_pandas_df.eval(expr) + + pd.testing.assert_frame_equal(bf_result, pd_result) + + +@skip_legacy_pandas +@pytest.mark.parametrize( + ("expr",), + [ + ("int64_col > int64_too",), + ("bool_col",), + ("((int64_col - int64_too) % @local_var) == 0",), + ], +) +def test_df_query(scalars_dfs, expr): + # local_var is referenced in expressions + local_var = 3 # NOQA + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.query(expr).to_pandas() + pd_result = scalars_pandas_df.query(expr) + + pd.testing.assert_frame_equal(bf_result, pd_result) + + @pytest.mark.parametrize( ("subset", "normalize", "ascending", "dropna"), [ diff --git a/third_party/bigframes_vendored/pandas/core/common.py b/third_party/bigframes_vendored/pandas/core/common.py index ded5a22b8f7..872a64db6c3 100644 --- a/third_party/bigframes_vendored/pandas/core/common.py +++ b/third_party/bigframes_vendored/pandas/core/common.py @@ -3,6 +3,8 @@ from typing import Callable, TYPE_CHECKING +from bigframes_vendored.pandas.core.dtypes.inference import iterable_not_string + if TYPE_CHECKING: from bigframes_vendored.pandas.pandas._typing import T @@ -40,3 +42,27 @@ def pipe( return func(*args, **kwargs) else: return func(obj, *args, **kwargs) + + +def flatten(line): + """ + Flatten an arbitrarily nested sequence. + + Parameters + ---------- + line : sequence + The non string sequence to flatten + + Notes + ----- + This doesn't consider strings sequences. + + Returns + ------- + flattened : generator + """ + for element in line: + if iterable_not_string(element): + yield from flatten(element) + else: + yield element diff --git a/third_party/bigframes_vendored/pandas/core/computation/align.py b/third_party/bigframes_vendored/pandas/core/computation/align.py new file mode 100644 index 00000000000..2608dabe7ac --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/align.py @@ -0,0 +1,226 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/align.py +""" +Core eval alignment algorithms. +""" +from __future__ import annotations + +from functools import partial, wraps +from typing import Callable, TYPE_CHECKING +import warnings + +import bigframes_vendored.pandas.core.common as com +from bigframes_vendored.pandas.core.computation.common import result_type_many +from bigframes_vendored.pandas.util._exceptions import find_stack_level +import numpy as np +from pandas.errors import PerformanceWarning + +if TYPE_CHECKING: + from collections.abc import Sequence + + from bigframes_vendored.pandas.core.generic import NDFrame + from bigframes_vendored.pandas.core.indexes.base import Index + from pandas._typing import F + + +def _align_core_single_unary_op( + term, +) -> tuple[partial | type[NDFrame], dict[str, Index] | None]: + typ: partial | type[NDFrame] + axes: dict[str, Index] | None = None + + if isinstance(term.value, np.ndarray): + typ = partial(np.asanyarray, dtype=term.value.dtype) + else: + typ = type(term.value) + if hasattr(term.value, "axes"): + axes = _zip_axes_from_type(typ, term.value.axes) + + return typ, axes + + +def _zip_axes_from_type( + typ: type[NDFrame], new_axes: Sequence[Index] +) -> dict[str, Index]: + return {name: new_axes[i] for i, name in enumerate(typ._AXIS_ORDERS)} + + +def _any_pandas_objects(terms) -> bool: + """ + Check a sequence of terms for instances of PandasObject. + """ + return any(is_pandas_object(term.value) for term in terms) + + +def _filter_special_cases(f) -> Callable[[F], F]: + @wraps(f) + def wrapper(terms): + # single unary operand + if len(terms) == 1: + return _align_core_single_unary_op(terms[0]) + + term_values = (term.value for term in terms) + + # we don't have any pandas objects + if not _any_pandas_objects(terms): + return result_type_many(*term_values), None + + return f(terms) + + return wrapper + + +@_filter_special_cases +def _align_core(terms): + term_index = [i for i, term in enumerate(terms) if hasattr(term.value, "axes")] + term_dims = [terms[i].value.ndim for i in term_index] + + from pandas import Series + + ndims = Series(dict(zip(term_index, term_dims))) + + # initial axes are the axes of the largest-axis'd term + biggest = terms[ndims.idxmax()].value + typ = biggest._constructor + axes = biggest.axes + naxes = len(axes) + gt_than_one_axis = naxes > 1 + + for value in (terms[i].value for i in term_index): + value_is_series = is_series(value) + is_series_and_gt_one_axis = value_is_series and gt_than_one_axis + + for axis, items in enumerate(value.axes): + if is_series_and_gt_one_axis: + ax, itm = naxes - 1, value.index + else: + ax, itm = axis, items + + if not axes[ax].is_(itm): + axes[ax] = axes[ax].join(itm, how="outer") + + for i, ndim in ndims.items(): + for axis, items in zip(range(ndim), axes): + ti = terms[i].value + + if hasattr(ti, "reindex"): + transpose = value_is_series(ti) and naxes > 1 + reindexer = axes[naxes - 1] if transpose else items + + term_axis_size = len(ti.axes[axis]) + reindexer_size = len(reindexer) + + ordm = np.log10(max(1, abs(reindexer_size - term_axis_size))) + if ordm >= 1 and reindexer_size >= 10000: + w = ( + f"Alignment difference on axis {axis} is larger " + f"than an order of magnitude on term {repr(terms[i].name)}, " + f"by more than {ordm:.4g}; performance may suffer." + ) + warnings.warn( + w, category=PerformanceWarning, stacklevel=find_stack_level() + ) + + obj = ti.reindex(reindexer, axis=axis, copy=False) + terms[i].update(obj) + + terms[i].update(terms[i].value.values) + + return typ, _zip_axes_from_type(typ, axes) + + +def align_terms(terms): + """ + Align a set of terms. + """ + try: + # flatten the parse tree (a nested list, really) + terms = list(com.flatten(terms)) + except TypeError: + # can't iterate so it must just be a constant or single variable + if is_series_or_dataframe(terms.value): + typ = type(terms.value) + return typ, _zip_axes_from_type(typ, terms.value.axes) + return np.result_type(terms.type), None + + # if all resolved variables are numeric scalars + if all(term.is_scalar for term in terms): + return result_type_many(*(term.value for term in terms)).type, None + + # perform the main alignment + typ, axes = _align_core(terms) + return typ, axes + + +def reconstruct_object(typ, obj, axes, dtype): + """ + Reconstruct an object given its type, raw value, and possibly empty + (None) axes. + + Parameters + ---------- + typ : object + A type + obj : object + The value to use in the type constructor + axes : dict + The axes to use to construct the resulting pandas object + + Returns + ------- + ret : typ + An object of type ``typ`` with the value `obj` and possible axes + `axes`. + """ + try: + typ = typ.type + except AttributeError: + pass + + res_t = np.result_type(obj.dtype, dtype) + + if not isinstance(typ, partial) and is_pandas_type(typ): + return typ(obj, dtype=res_t, **axes) + + # special case for pathological things like ~True/~False + if hasattr(res_t, "type") and typ == np.bool_ and res_t != np.bool_: + ret_value = res_t.type(obj) + else: + ret_value = typ(obj).astype(res_t) + # The condition is to distinguish 0-dim array (returned in case of + # scalar) and 1 element array + # e.g. np.array(0) and np.array([0]) + if ( + len(obj.shape) == 1 + and len(obj) == 1 + and not isinstance(ret_value, np.ndarray) + ): + ret_value = np.array([ret_value]).astype(res_t) + + return ret_value + + +# Custom to recognize BigFrames types +def is_series(obj) -> bool: + from bigframes_vendored.pandas.core.series import Series + + return isinstance(obj, Series) + + +def is_series_or_dataframe(obj) -> bool: + from bigframes_vendored.pandas.core.frame import NDFrame + + return isinstance(obj, NDFrame) + + +def is_pandas_object(obj) -> bool: + from bigframes_vendored.pandas.core.frame import NDFrame + from bigframes_vendored.pandas.core.indexes.base import Index + + return isinstance(obj, NDFrame) or isinstance(obj, Index) + + +def is_pandas_type(type) -> bool: + from bigframes_vendored.pandas.core.frame import NDFrame + from bigframes_vendored.pandas.core.indexes.base import Index + + return issubclass(type, NDFrame) or issubclass(type, Index) diff --git a/third_party/bigframes_vendored/pandas/core/computation/common.py b/third_party/bigframes_vendored/pandas/core/computation/common.py new file mode 100644 index 00000000000..7775489d0df --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/common.py @@ -0,0 +1,48 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/common.py +from __future__ import annotations + +from functools import reduce + +import numpy as np +from pandas._config import get_option + + +def ensure_decoded(s) -> str: + """ + If we have bytes, decode them to unicode. + """ + if isinstance(s, (np.bytes_, bytes)): + s = s.decode(get_option("display.encoding")) + return s + + +def result_type_many(*arrays_and_dtypes): + """ + Wrapper around numpy.result_type which overcomes the NPY_MAXARGS (32) + argument limit. + """ + try: + return np.result_type(*arrays_and_dtypes) + except ValueError: + # we have > NPY_MAXARGS terms in our expression + return reduce(np.result_type, arrays_and_dtypes) + except TypeError: + from pandas.core.dtypes.cast import find_common_type + from pandas.core.dtypes.common import is_extension_array_dtype + + arr_and_dtypes = list(arrays_and_dtypes) + ea_dtypes, non_ea_dtypes = [], [] + for arr_or_dtype in arr_and_dtypes: + if is_extension_array_dtype(arr_or_dtype): + ea_dtypes.append(arr_or_dtype) + else: + non_ea_dtypes.append(arr_or_dtype) + + if non_ea_dtypes: + try: + np_dtype = np.result_type(*non_ea_dtypes) + except ValueError: + np_dtype = reduce(np.result_type, arrays_and_dtypes) + return find_common_type(ea_dtypes + [np_dtype]) + + return find_common_type(ea_dtypes) diff --git a/third_party/bigframes_vendored/pandas/core/computation/engines.py b/third_party/bigframes_vendored/pandas/core/computation/engines.py new file mode 100644 index 00000000000..15fd48b2376 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/engines.py @@ -0,0 +1,94 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/engines.py +""" +Engine classes for :func:`~pandas.eval` +""" +from __future__ import annotations + +import abc + +from bigframes_vendored.pandas.core.computation.align import ( + align_terms, + reconstruct_object, +) +from pandas.io.formats import printing + + +class AbstractEngine(metaclass=abc.ABCMeta): + """Object serving as a base class for all engines.""" + + has_neg_frac = False + + def __init__(self, expr) -> None: + self.expr = expr + self.aligned_axes = None + self.result_type = None + + def convert(self) -> str: + """ + Convert an expression for evaluation. + + Defaults to return the expression as a string. + """ + return printing.pprint_thing(self.expr) + + def evaluate(self) -> object: + """ + Run the engine on the expression. + + This method performs alignment which is necessary no matter what engine + is being used, thus its implementation is in the base class. + + Returns + ------- + object + The result of the passed expression. + """ + if not self._is_aligned: + self.result_type, self.aligned_axes = align_terms(self.expr.terms) + + # make sure no names in resolvers and locals/globals clash + res = self._evaluate() + return reconstruct_object( + self.result_type, res, self.aligned_axes, self.expr.terms.return_type + ) + + @property + def _is_aligned(self) -> bool: + return self.aligned_axes is not None and self.result_type is not None + + @abc.abstractmethod + def _evaluate(self): + """ + Return an evaluated expression. + + Parameters + ---------- + env : Scope + The local and global environment in which to evaluate an + expression. + + Notes + ----- + Must be implemented by subclasses. + """ + + +class PythonEngine(AbstractEngine): + """ + Evaluate an expression in Python space. + + Mostly for testing purposes. + """ + + has_neg_frac = False + + def evaluate(self): + return self.expr() + + def _evaluate(self) -> None: + pass + + +ENGINES: dict[str, type[AbstractEngine]] = { + "python": PythonEngine, +} diff --git a/third_party/bigframes_vendored/pandas/core/computation/eval.py b/third_party/bigframes_vendored/pandas/core/computation/eval.py new file mode 100644 index 00000000000..56d60174a60 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/eval.py @@ -0,0 +1,368 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/eval.py +""" +Top level ``eval`` module. +""" +from __future__ import annotations + +import tokenize +from typing import TYPE_CHECKING +import warnings + +from bigframes_vendored.pandas.core.computation.engines import ENGINES +from bigframes_vendored.pandas.core.computation.expr import Expr, PARSERS +from bigframes_vendored.pandas.core.computation.parsing import tokenize_string +from bigframes_vendored.pandas.core.computation.scope import ensure_scope +from bigframes_vendored.pandas.core.generic import NDFrame +from bigframes_vendored.pandas.util._validators import validate_bool_kwarg +from pandas.io.formats.printing import pprint_thing + +if TYPE_CHECKING: + from pandas.core.computation.ops import BinOp + + +def _check_engine(engine: str | None) -> str: + """ + Make sure a valid engine is passed. + + Parameters + ---------- + engine : str + String to validate. + + Raises + ------ + KeyError + * If an invalid engine is passed. + + Returns + ------- + str + Engine name. + """ + + if engine is None: + engine = "python" + + if engine not in ENGINES: + valid_engines = list(ENGINES.keys()) + raise KeyError( + f"Invalid engine '{engine}' passed, valid engines are {valid_engines}" + ) + + return engine + + +def _check_parser(parser: str): + """ + Make sure a valid parser is passed. + + Parameters + ---------- + parser : str + + Raises + ------ + KeyError + * If an invalid parser is passed + """ + if parser not in PARSERS: + raise KeyError( + f"Invalid parser '{parser}' passed, valid parsers are {PARSERS.keys()}" + ) + + +def _check_resolvers(resolvers): + if resolvers is not None: + for resolver in resolvers: + if not hasattr(resolver, "__getitem__"): + name = type(resolver).__name__ + raise TypeError( + f"Resolver of type '{name}' does not " + "implement the __getitem__ method" + ) + + +def _check_expression(expr): + """ + Make sure an expression is not an empty string + + Parameters + ---------- + expr : object + An object that can be converted to a string + + Raises + ------ + ValueError + * If expr is an empty string + """ + if not expr: + raise ValueError("expr cannot be an empty string") + + +def _convert_expression(expr) -> str: + """ + Convert an object to an expression. + + This function converts an object to an expression (a unicode string) and + checks to make sure it isn't empty after conversion. This is used to + convert operators to their string representation for recursive calls to + :func:`~pandas.eval`. + + Parameters + ---------- + expr : object + The object to be converted to a string. + + Returns + ------- + str + The string representation of an object. + + Raises + ------ + ValueError + * If the expression is empty. + """ + s = pprint_thing(expr) + _check_expression(s) + return s + + +def _check_for_locals(expr: str, stack_level: int, parser: str): + at_top_of_stack = stack_level == 0 + not_pandas_parser = parser != "pandas" + + if not_pandas_parser: + msg = "The '@' prefix is only supported by the pandas parser" + elif at_top_of_stack: + msg = ( + "The '@' prefix is not allowed in top-level eval calls.\n" + "please refer to your variables by name without the '@' prefix." + ) + + if at_top_of_stack or not_pandas_parser: + for toknum, tokval in tokenize_string(expr): + if toknum == tokenize.OP and tokval == "@": + raise SyntaxError(msg) + + +def eval( + expr: str | BinOp, # we leave BinOp out of the docstr bc it isn't for users + parser: str = "pandas", + engine: str | None = None, + local_dict=None, + global_dict=None, + resolvers=(), + level: int = 0, + target=None, + inplace: bool = False, +): + """ + Evaluate a Python expression as a string using various backends. + + The following arithmetic operations are supported: ``+``, ``-``, ``*``, + ``/``, ``**``, ``%``, ``//`` (python engine only) along with the following + boolean operations: ``|`` (or), ``&`` (and), and ``~`` (not). + Additionally, the ``'pandas'`` parser allows the use of :keyword:`and`, + :keyword:`or`, and :keyword:`not` with the same semantics as the + corresponding bitwise operators. :class:`~pandas.Series` and + :class:`~pandas.DataFrame` objects are supported and behave as they would + with plain ol' Python evaluation. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({"animal": ["dog", "pig"], "age": [10, 20]}) + >>> df + animal age + 0 dog 10 + 1 pig 20 + + [2 rows x 2 columns] + + We can add a new column using ``pd.eval``: + + >>> df.eval("double_age = age * 2") + animal age double_age + 0 dog 10 20 + 1 pig 20 40 + + [2 rows x 3 columns] + + Args: + expr (str): + The expression to evaluate. This string cannot contain any Python + `statements + `__, + only Python `expressions + `__. + parser ({'pandas', 'python'}, default 'pandas'): + The parser to use to construct the syntax tree from the expression. The + default of ``'pandas'`` parses code slightly different than standard + Python. Alternatively, you can parse an expression using the + ``'python'`` parser to retain strict Python semantics. See the + :ref:`enhancing performance ` documentation for + more details. + engine ({'python'}, default None): + + The engine used to evaluate the expression. Supported engines are + + - None : defaults to ``python`` + - ``'python'`` : Performs operations as if you had ``eval``'d in top + level python. This engine is generally not that useful. + + More backends may be available in the future. + local_dict (dict or None, optional): + A dictionary of local variables, taken from locals() by default. + global_dict (dict or None, optional): + A dictionary of global variables, taken from globals() by default. + resolvers (list of dict-like or None, optional): + A list of objects implementing the ``__getitem__`` special method that + you can use to inject an additional collection of namespaces to use for + variable lookup. For example, this is used in the + :meth:`~DataFrame.query` method to inject the + ``DataFrame.index`` and ``DataFrame.columns`` + variables that refer to their respective :class:`~pandas.DataFrame` + instance attributes. + level (int, optional): + The number of prior stack frames to traverse and add to the current + scope. Most users will **not** need to change this parameter. + target (object, optional, default None): + This is the target object for assignment. It is used when there is + variable assignment in the expression. If so, then `target` must + support item assignment with string keys, and if a copy is being + returned, it must also support `.copy()`. + inplace (bool, default False): + If `target` is provided, and the expression mutates `target`, whether + to modify `target` inplace. Otherwise, return a copy of `target` with + the mutation. + + Returns: + ndarray, numeric scalar, DataFrame, Series, or None: + The completion value of evaluating the given code or None if ``inplace=True``. + + Raises: + ValueError: + There are many instances where such an error can be raised: + + - `target=None`, but the expression is multiline. + - The expression is multiline, but not all them have item assignment. + An example of such an arrangement is this: + + a = b + 1 + a + 2 + + Here, there are expressions on different lines, making it multiline, + but the last line has no variable assigned to the output of `a + 2`. + - `inplace=True`, but the expression is missing item assignment. + - Item assignment is provided, but the `target` does not support + string item assignment. + - Item assignment is provided and `inplace=False`, but the `target` + does not support the `.copy()` method + + """ + inplace = validate_bool_kwarg(inplace, "inplace") + + exprs: list[str | BinOp] + if isinstance(expr, str): + _check_expression(expr) + exprs = [e.strip() for e in expr.splitlines() if e.strip() != ""] + else: + # ops.BinOp; for internal compat, not intended to be passed by users + exprs = [expr] + multi_line = len(exprs) > 1 + + if multi_line and target is None: + raise ValueError( + "multi-line expressions are only valid in the " + "context of data, use DataFrame.eval" + ) + engine = _check_engine(engine) + _check_parser(parser) + _check_resolvers(resolvers) + + ret = None + first_expr = True + target_modified = False + + for expr in exprs: + expr = _convert_expression(expr) + _check_for_locals(expr, level, parser) + + # get our (possibly passed-in) scope + env = ensure_scope( + level + 1, + global_dict=global_dict, + local_dict=local_dict, + resolvers=resolvers, + target=target, + ) + + parsed_expr = Expr(expr, engine=engine, parser=parser, env=env) + + # construct the engine and evaluate the parsed expression + eng = ENGINES[engine] + eng_inst = eng(parsed_expr) + ret = eng_inst.evaluate() + + if parsed_expr.assigner is None: + if multi_line: + raise ValueError( + "Multi-line expressions are only valid " + "if all expressions contain an assignment" + ) + if inplace: + raise ValueError("Cannot operate inplace if there is no assignment") + + # assign if needed + assigner = parsed_expr.assigner + if env.target is not None and assigner is not None: + target_modified = True + + # if returning a copy, copy only on the first assignment + if not inplace and first_expr: + try: + target = env.target + if isinstance(target, NDFrame): + target = target.copy() + except AttributeError as err: + raise ValueError("Cannot return a copy of the target") from err + else: + target = env.target + + # TypeError is most commonly raised (e.g. int, list), but you + # get IndexError if you try to do this assignment on np.ndarray. + # we will ignore numpy warnings here; e.g. if trying + # to use a non-numeric indexer + try: + with warnings.catch_warnings(record=True): + # TODO: Filter the warnings we actually care about here. + if inplace and isinstance(target, NDFrame): + target.loc[:, assigner] = ret + else: + target[ # pyright: ignore[reportGeneralTypeIssues] + assigner + ] = ret + except (TypeError, IndexError) as err: + raise ValueError("Cannot assign expression output to target") from err + + if not resolvers: + resolvers = ({assigner: ret},) + else: + # existing resolver needs updated to handle + # case of mutating existing column in copy + for resolver in resolvers: + if assigner in resolver: + resolver[assigner] = ret + break + else: + resolvers += ({assigner: ret},) + + ret = None + first_expr = False + + # We want to exclude `inplace=None` as being False. + if inplace is False: + return target if target_modified else ret diff --git a/third_party/bigframes_vendored/pandas/core/computation/expr.py b/third_party/bigframes_vendored/pandas/core/computation/expr.py new file mode 100644 index 00000000000..44f649e59dc --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/expr.py @@ -0,0 +1,828 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/expr.py +""" +:func:`~pandas.eval` parsers. +""" +from __future__ import annotations + +import ast +from functools import partial, reduce +from keyword import iskeyword +import tokenize +from typing import Callable, TypeVar + +import bigframes_vendored.pandas.core.common as com +from bigframes_vendored.pandas.core.computation.ops import ( + ARITH_OPS_SYMS, + BinOp, + BOOL_OPS_SYMS, + CMP_OPS_SYMS, + Constant, + Div, + FuncNode, + is_term, + LOCAL_TAG, + Op, + Term, + UNARY_OPS_SYMS, + UnaryOp, +) +from bigframes_vendored.pandas.core.computation.parsing import ( + clean_backtick_quoted_toks, + tokenize_string, +) +from bigframes_vendored.pandas.core.computation.scope import Scope +import numpy as np +from pandas.errors import UndefinedVariableError +from pandas.io.formats import printing + + +def _rewrite_assign(tok: tuple[int, str]) -> tuple[int, str]: + """ + Rewrite the assignment operator for PyTables expressions that use ``=`` + as a substitute for ``==``. + + Parameters + ---------- + tok : tuple of int, str + ints correspond to the all caps constants in the tokenize module + + Returns + ------- + tuple of int, str + Either the input or token or the replacement values + """ + toknum, tokval = tok + return toknum, "==" if tokval == "=" else tokval + + +def _replace_booleans(tok: tuple[int, str]) -> tuple[int, str]: + """ + Replace ``&`` with ``and`` and ``|`` with ``or`` so that bitwise + precedence is changed to boolean precedence. + + Parameters + ---------- + tok : tuple of int, str + ints correspond to the all caps constants in the tokenize module + + Returns + ------- + tuple of int, str + Either the input or token or the replacement values + """ + toknum, tokval = tok + if toknum == tokenize.OP: + if tokval == "&": + return tokenize.NAME, "and" + elif tokval == "|": + return tokenize.NAME, "or" + return toknum, tokval + return toknum, tokval + + +def _replace_locals(tok: tuple[int, str]) -> tuple[int, str]: + """ + Replace local variables with a syntactically valid name. + + Parameters + ---------- + tok : tuple of int, str + ints correspond to the all caps constants in the tokenize module + + Returns + ------- + tuple of int, str + Either the input or token or the replacement values + + Notes + ----- + This is somewhat of a hack in that we rewrite a string such as ``'@a'`` as + ``'__pd_eval_local_a'`` by telling the tokenizer that ``__pd_eval_local_`` + is a ``tokenize.OP`` and to replace the ``'@'`` symbol with it. + """ + toknum, tokval = tok + if toknum == tokenize.OP and tokval == "@": + return tokenize.OP, LOCAL_TAG + return toknum, tokval + + +def _compose2(f, g): + """ + Compose 2 callables. + """ + return lambda *args, **kwargs: f(g(*args, **kwargs)) + + +def _compose(*funcs): + """ + Compose 2 or more callables. + """ + assert len(funcs) > 1, "At least 2 callables must be passed to compose" + return reduce(_compose2, funcs) + + +def _preparse( + source: str, + f=_compose( + _replace_locals, _replace_booleans, _rewrite_assign, clean_backtick_quoted_toks + ), +) -> str: + """ + Compose a collection of tokenization functions. + + Parameters + ---------- + source : str + A Python source code string + f : callable + This takes a tuple of (toknum, tokval) as its argument and returns a + tuple with the same structure but possibly different elements. Defaults + to the composition of ``_rewrite_assign``, ``_replace_booleans``, and + ``_replace_locals``. + + Returns + ------- + str + Valid Python source code + + Notes + ----- + The `f` parameter can be any callable that takes *and* returns input of the + form ``(toknum, tokval)``, where ``toknum`` is one of the constants from + the ``tokenize`` module and ``tokval`` is a string. + """ + assert callable(f), "f must be callable" + return tokenize.untokenize(f(x) for x in tokenize_string(source)) + + +def _is_type(t): + """ + Factory for a type checking function of type ``t`` or tuple of types. + """ + return lambda x: isinstance(x.value, t) + + +_is_list = _is_type(list) +_is_str = _is_type(str) + + +# partition all AST nodes +_all_nodes = frozenset( + node + for node in (getattr(ast, name) for name in dir(ast)) + if isinstance(node, type) and issubclass(node, ast.AST) +) + + +def _filter_nodes(superclass, all_nodes=_all_nodes): + """ + Filter out AST nodes that are subclasses of ``superclass``. + """ + node_names = (node.__name__ for node in all_nodes if issubclass(node, superclass)) + return frozenset(node_names) + + +_all_node_names = frozenset(x.__name__ for x in _all_nodes) +_mod_nodes = _filter_nodes(ast.mod) +_stmt_nodes = _filter_nodes(ast.stmt) +_expr_nodes = _filter_nodes(ast.expr) +_expr_context_nodes = _filter_nodes(ast.expr_context) +_boolop_nodes = _filter_nodes(ast.boolop) +_operator_nodes = _filter_nodes(ast.operator) +_unary_op_nodes = _filter_nodes(ast.unaryop) +_cmp_op_nodes = _filter_nodes(ast.cmpop) +_comprehension_nodes = _filter_nodes(ast.comprehension) +_handler_nodes = _filter_nodes(ast.excepthandler) +_arguments_nodes = _filter_nodes(ast.arguments) +_keyword_nodes = _filter_nodes(ast.keyword) +_alias_nodes = _filter_nodes(ast.alias) + + +# nodes that we don't support directly but are needed for parsing +_hacked_nodes = frozenset(["Assign", "Module", "Expr"]) + + +_unsupported_expr_nodes = frozenset( + [ + "Yield", + "GeneratorExp", + "IfExp", + "DictComp", + "SetComp", + "Repr", + "Lambda", + "Set", + "AST", + "Is", + "IsNot", + ] +) + +# these nodes are low priority or won't ever be supported (e.g., AST) +_unsupported_nodes = ( + _stmt_nodes + | _mod_nodes + | _handler_nodes + | _arguments_nodes + | _keyword_nodes + | _alias_nodes + | _expr_context_nodes + | _unsupported_expr_nodes +) - _hacked_nodes + +# we're adding a different assignment in some cases to be equality comparison +# and we don't want `stmt` and friends in their so get only the class whose +# names are capitalized +_base_supported_nodes = (_all_node_names - _unsupported_nodes) | _hacked_nodes +intersection = _unsupported_nodes & _base_supported_nodes +_msg = f"cannot both support and not support {intersection}" +assert not intersection, _msg + + +def _node_not_implemented(node_name: str) -> Callable[..., None]: + """ + Return a function that raises a NotImplementedError with a passed node name. + """ + + def f(self, *args, **kwargs): + raise NotImplementedError(f"'{node_name}' nodes are not implemented") + + return f + + +# should be bound by BaseExprVisitor but that creates a circular dependency: +# _T is used in disallow, but disallow is used to define BaseExprVisitor +# https://github.com/microsoft/pyright/issues/2315 +_T = TypeVar("_T") + + +def disallow(nodes: set[str]) -> Callable[[type[_T]], type[_T]]: + """ + Decorator to disallow certain nodes from parsing. Raises a + NotImplementedError instead. + + Returns + ------- + callable + """ + + def disallowed(cls: type[_T]) -> type[_T]: + # error: "Type[_T]" has no attribute "unsupported_nodes" + cls.unsupported_nodes = () # type: ignore[attr-defined] + for node in nodes: + new_method = _node_not_implemented(node) + name = f"visit_{node}" + # error: "Type[_T]" has no attribute "unsupported_nodes" + cls.unsupported_nodes += (name,) # type: ignore[attr-defined] + setattr(cls, name, new_method) + return cls + + return disallowed + + +def _op_maker(op_class, op_symbol): + """ + Return a function to create an op class with its symbol already passed. + + Returns + ------- + callable + """ + + def f(self, node, *args, **kwargs): + """ + Return a partial function with an Op subclass with an operator already passed. + + Returns + ------- + callable + """ + return partial(op_class, op_symbol, *args, **kwargs) + + return f + + +_op_classes = {"binary": BinOp, "unary": UnaryOp} + + +def add_ops(op_classes): + """ + Decorator to add default implementation of ops. + """ + + def f(cls): + for op_attr_name, op_class in op_classes.items(): + ops = getattr(cls, f"{op_attr_name}_ops") + ops_map = getattr(cls, f"{op_attr_name}_op_nodes_map") + for op in ops: + op_node = ops_map[op] + if op_node is not None: + made_op = _op_maker(op_class, op) + setattr(cls, f"visit_{op_node}", made_op) + return cls + + return f + + +@disallow(_unsupported_nodes) +@add_ops(_op_classes) +class BaseExprVisitor(ast.NodeVisitor): + """ + Custom ast walker. Parsers of other engines should subclass this class + if necessary. + + Parameters + ---------- + env : Scope + engine : str + parser : str + preparser : callable + """ + + const_type: type[Term] = Constant + term_type = Term + + binary_ops = CMP_OPS_SYMS + BOOL_OPS_SYMS + ARITH_OPS_SYMS + binary_op_nodes = ( + "Gt", + "Lt", + "GtE", + "LtE", + "Eq", + "NotEq", + "In", + "NotIn", + "BitAnd", + "BitOr", + "And", + "Or", + "Add", + "Sub", + "Mult", + None, + "Pow", + "FloorDiv", + "Mod", + ) + binary_op_nodes_map = dict(zip(binary_ops, binary_op_nodes)) + + unary_ops = UNARY_OPS_SYMS + unary_op_nodes = "UAdd", "USub", "Invert", "Not" + unary_op_nodes_map = dict(zip(unary_ops, unary_op_nodes)) + + rewrite_map = { + ast.Eq: ast.In, + ast.NotEq: ast.NotIn, + ast.In: ast.In, + ast.NotIn: ast.NotIn, + } + + unsupported_nodes: tuple[str, ...] + + def __init__(self, env, engine, parser, preparser=_preparse) -> None: + self.env = env + self.engine = engine + self.parser = parser + self.preparser = preparser + self.assigner = None + + def visit(self, node, **kwargs): + if isinstance(node, str): + clean = self.preparser(node) + try: + node = ast.fix_missing_locations(ast.parse(clean)) + except SyntaxError as e: + if any(iskeyword(x) for x in clean.split()): + e.msg = "Python keyword not valid identifier in numexpr query" + raise e + + method = f"visit_{type(node).__name__}" + visitor = getattr(self, method) + return visitor(node, **kwargs) + + def visit_Module(self, node, **kwargs): + if len(node.body) != 1: + raise SyntaxError("only a single expression is allowed") + expr = node.body[0] + return self.visit(expr, **kwargs) + + def visit_Expr(self, node, **kwargs): + return self.visit(node.value, **kwargs) + + def _rewrite_membership_op(self, node, left, right): + # the kind of the operator (is actually an instance) + op_instance = node.op + op_type = type(op_instance) + + # must be two terms and the comparison operator must be ==/!=/in/not in + if is_term(left) and is_term(right) and op_type in self.rewrite_map: + left_list, right_list = map(_is_list, (left, right)) + left_str, right_str = map(_is_str, (left, right)) + + # if there are any strings or lists in the expression + if left_list or right_list or left_str or right_str: + op_instance = self.rewrite_map[op_type]() + + # pop the string variable out of locals and replace it with a list + # of one string, kind of a hack + if right_str: + name = self.env.add_tmp([right.value]) + right = self.term_type(name, self.env) + + if left_str: + name = self.env.add_tmp([left.value]) + left = self.term_type(name, self.env) + + op = self.visit(op_instance) + return op, op_instance, left, right + + def _maybe_transform_eq_ne(self, node, left=None, right=None): + if left is None: + left = self.visit(node.left, side="left") + if right is None: + right = self.visit(node.right, side="right") + op, op_class, left, right = self._rewrite_membership_op(node, left, right) + return op, op_class, left, right + + def _maybe_downcast_constants(self, left, right): + f32 = np.dtype(np.float32) + if ( + left.is_scalar + and hasattr(left, "value") + and not right.is_scalar + and right.return_type == f32 + ): + # right is a float32 array, left is a scalar + name = self.env.add_tmp(np.float32(left.value)) + left = self.term_type(name, self.env) + if ( + right.is_scalar + and hasattr(right, "value") + and not left.is_scalar + and left.return_type == f32 + ): + # left is a float32 array, right is a scalar + name = self.env.add_tmp(np.float32(right.value)) + right = self.term_type(name, self.env) + + return left, right + + def _maybe_eval(self, binop, eval_in_python): + # eval `in` and `not in` (for now) in "partial" python space + # things that can be evaluated in "eval" space will be turned into + # temporary variables. for example, + # [1,2] in a + 2 * b + # in that case a + 2 * b will be evaluated using numexpr, and the "in" + # call will be evaluated using isin (in python space) + return binop.evaluate( + self.env, self.engine, self.parser, self.term_type, eval_in_python + ) + + def _maybe_evaluate_binop( + self, + op, + op_class, + lhs, + rhs, + eval_in_python=("in", "not in"), + maybe_eval_in_python=("==", "!=", "<", ">", "<=", ">="), + ): + res = op(lhs, rhs) + + if res.has_invalid_return_type: + raise TypeError( + f"unsupported operand type(s) for {res.op}: " + f"'{lhs.type}' and '{rhs.type}'" + ) + + if self.engine != "pytables" and ( + res.op in CMP_OPS_SYMS + and getattr(lhs, "is_datetime", False) + or getattr(rhs, "is_datetime", False) + ): + # all date ops must be done in python bc numexpr doesn't work + # well with NaT + return self._maybe_eval(res, self.binary_ops) + + if res.op in eval_in_python: + # "in"/"not in" ops are always evaluated in python + return self._maybe_eval(res, eval_in_python) + elif self.engine != "pytables": + if ( + getattr(lhs, "return_type", None) == object + or getattr(rhs, "return_type", None) == object + ): + # evaluate "==" and "!=" in python if either of our operands + # has an object return type + return self._maybe_eval(res, eval_in_python + maybe_eval_in_python) + return res + + def visit_BinOp(self, node, **kwargs): + op, op_class, left, right = self._maybe_transform_eq_ne(node) + left, right = self._maybe_downcast_constants(left, right) + return self._maybe_evaluate_binop(op, op_class, left, right) + + def visit_Div(self, node, **kwargs): + return lambda lhs, rhs: Div(lhs, rhs) + + def visit_UnaryOp(self, node, **kwargs): + op = self.visit(node.op) + operand = self.visit(node.operand) + return op(operand) + + def visit_Name(self, node, **kwargs): + return self.term_type(node.id, self.env, **kwargs) + + # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min + def visit_NameConstant(self, node, **kwargs) -> Term: + return self.const_type(node.value, self.env) + + # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min + def visit_Num(self, node, **kwargs) -> Term: + return self.const_type(node.value, self.env) + + def visit_Constant(self, node, **kwargs) -> Term: + return self.const_type(node.value, self.env) + + # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min + def visit_Str(self, node, **kwargs): + name = self.env.add_tmp(node.s) + return self.term_type(name, self.env) + + def visit_List(self, node, **kwargs): + name = self.env.add_tmp([self.visit(e)(self.env) for e in node.elts]) + return self.term_type(name, self.env) + + visit_Tuple = visit_List + + def visit_Index(self, node, **kwargs): + """df.index[4]""" + return self.visit(node.value) + + def visit_Subscript(self, node, **kwargs): + from pandas import eval as pd_eval + + value = self.visit(node.value) + slobj = self.visit(node.slice) + result = pd_eval( + slobj, local_dict=self.env, engine=self.engine, parser=self.parser + ) + try: + # a Term instance + v = value.value[result] + except AttributeError: + # an Op instance + lhs = pd_eval( + value, local_dict=self.env, engine=self.engine, parser=self.parser + ) + v = lhs[result] + name = self.env.add_tmp(v) + return self.term_type(name, env=self.env) + + def visit_Slice(self, node, **kwargs): + """df.index[slice(4,6)]""" + lower = node.lower + if lower is not None: + lower = self.visit(lower).value + upper = node.upper + if upper is not None: + upper = self.visit(upper).value + step = node.step + if step is not None: + step = self.visit(step).value + + return slice(lower, upper, step) + + def visit_Assign(self, node, **kwargs): + """ + support a single assignment node, like + + c = a + b + + set the assigner at the top level, must be a Name node which + might or might not exist in the resolvers + + """ + if len(node.targets) != 1: + raise SyntaxError("can only assign a single expression") + if not isinstance(node.targets[0], ast.Name): + raise SyntaxError("left hand side of an assignment must be a single name") + if self.env.target is None: + raise ValueError("cannot assign without a target object") + + try: + assigner = self.visit(node.targets[0], **kwargs) + except UndefinedVariableError: + assigner = node.targets[0].id + + self.assigner = getattr(assigner, "name", assigner) + if self.assigner is None: + raise SyntaxError( + "left hand side of an assignment must be a single resolvable name" + ) + + return self.visit(node.value, **kwargs) + + def visit_Attribute(self, node, **kwargs): + attr = node.attr + value = node.value + + ctx = node.ctx + if isinstance(ctx, ast.Load): + # resolve the value + resolved = self.visit(value).value + try: + v = getattr(resolved, attr) + name = self.env.add_tmp(v) + return self.term_type(name, self.env) + except AttributeError: + # something like datetime.datetime where scope is overridden + if isinstance(value, ast.Name) and value.id == attr: + return resolved + raise + + raise ValueError(f"Invalid Attribute context {type(ctx).__name__}") + + def visit_Call(self, node, side=None, **kwargs): + if isinstance(node.func, ast.Attribute) and node.func.attr != "__call__": + res = self.visit_Attribute(node.func) + elif not isinstance(node.func, ast.Name): + raise TypeError("Only named functions are supported") + else: + try: + res = self.visit(node.func) + except UndefinedVariableError: + # Check if this is a supported function name + try: + res = FuncNode(node.func.id) + except ValueError: + # Raise original error + raise + + if res is None: + # error: "expr" has no attribute "id" + raise ValueError( + f"Invalid function call {node.func.id}" # type: ignore[attr-defined] + ) + if hasattr(res, "value"): + res = res.value + + if isinstance(res, FuncNode): + new_args = [self.visit(arg) for arg in node.args] + + if node.keywords: + raise TypeError( + f'Function "{res.name}" does not support keyword arguments' + ) + + return res(*new_args) + + else: + new_args = [self.visit(arg)(self.env) for arg in node.args] + + for key in node.keywords: + if not isinstance(key, ast.keyword): + # error: "expr" has no attribute "id" + raise ValueError( + "keyword error in function call " # type: ignore[attr-defined] + f"'{node.func.id}'" + ) + + if key.arg: + kwargs[key.arg] = self.visit(key.value)(self.env) + + name = self.env.add_tmp(res(*new_args, **kwargs)) + return self.term_type(name=name, env=self.env) + + def translate_In(self, op): + return op + + def visit_Compare(self, node, **kwargs): + ops = node.ops + comps = node.comparators + + # base case: we have something like a CMP b + if len(comps) == 1: + op = self.translate_In(ops[0]) + binop = ast.BinOp(op=op, left=node.left, right=comps[0]) + return self.visit(binop) + + # recursive case: we have a chained comparison, a CMP b CMP c, etc. + left = node.left + values = [] + for op, comp in zip(ops, comps): + new_node = self.visit( + ast.Compare(comparators=[comp], left=left, ops=[self.translate_In(op)]) + ) + left = comp + values.append(new_node) + return self.visit(ast.BoolOp(op=ast.And(), values=values)) + + def _try_visit_binop(self, bop): + if isinstance(bop, (Op, Term)): + return bop + return self.visit(bop) + + def visit_BoolOp(self, node, **kwargs): + def visitor(x, y): + lhs = self._try_visit_binop(x) + rhs = self._try_visit_binop(y) + + op, op_class, lhs, rhs = self._maybe_transform_eq_ne(node, lhs, rhs) + return self._maybe_evaluate_binop(op, node.op, lhs, rhs) + + operands = node.values + return reduce(visitor, operands) + + +_python_not_supported = frozenset(["Dict", "BoolOp", "In", "NotIn"]) + + +@disallow( + (_unsupported_nodes | _python_not_supported) + - (_boolop_nodes | frozenset(["BoolOp", "Attribute", "In", "NotIn", "Tuple"])) +) +class PandasExprVisitor(BaseExprVisitor): + def __init__( + self, + env, + engine, + parser, + preparser=partial( + _preparse, + f=_compose(_replace_locals, _replace_booleans, clean_backtick_quoted_toks), + ), + ) -> None: + super().__init__(env, engine, parser, preparser) + + +@disallow(_unsupported_nodes | _python_not_supported | frozenset(["Not"])) +class PythonExprVisitor(BaseExprVisitor): + def __init__( + self, env, engine, parser, preparser=lambda source, f=None: source + ) -> None: + super().__init__(env, engine, parser, preparser=preparser) + + +class Expr: + """ + Object encapsulating an expression. + + Parameters + ---------- + expr : str + engine : str, optional, default 'numexpr' + parser : str, optional, default 'pandas' + env : Scope, optional, default None + level : int, optional, default 2 + """ + + env: Scope + engine: str + parser: str + + def __init__( + self, + expr, + engine: str = "numexpr", + parser: str = "pandas", + env: Scope | None = None, + level: int = 0, + ) -> None: + self.expr = expr + self.env = env or Scope(level=level + 1) + self.engine = engine + self.parser = parser + self._visitor = PARSERS[parser](self.env, self.engine, self.parser) + self.terms = self.parse() + + @property + def assigner(self): + return getattr(self._visitor, "assigner", None) + + def __call__(self): + return self.terms(self.env) + + def __repr__(self) -> str: + return printing.pprint_thing(self.terms) + + def __len__(self) -> int: + return len(self.expr) + + def parse(self): + """ + Parse an expression. + """ + return self._visitor.visit(self.expr) + + @property + def names(self): + """ + Get the names in an expression. + """ + if is_term(self.terms): + return frozenset([self.terms.name]) + return frozenset(term.name for term in com.flatten(self.terms)) + + +PARSERS = {"python": PythonExprVisitor, "pandas": PandasExprVisitor} diff --git a/third_party/bigframes_vendored/pandas/core/computation/ops.py b/third_party/bigframes_vendored/pandas/core/computation/ops.py new file mode 100644 index 00000000000..75b914c876b --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/ops.py @@ -0,0 +1,605 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/ops.py +""" +Operator classes for eval. +""" + +from __future__ import annotations + +from datetime import datetime +from functools import partial +import operator +from typing import Callable, Literal, TYPE_CHECKING + +import bigframes_vendored.pandas.core.common as com +from bigframes_vendored.pandas.core.computation.common import ( + ensure_decoded, + result_type_many, +) +from bigframes_vendored.pandas.core.computation.scope import DEFAULT_GLOBALS +import numpy as np +from pandas._libs.tslibs import Timestamp +from pandas.core.dtypes.common import is_list_like, is_scalar +from pandas.io.formats.printing import pprint_thing, pprint_thing_encoded + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + +REDUCTIONS = ("sum", "prod", "min", "max") + +_unary_math_ops = ( + "sin", + "cos", + "exp", + "log", + "expm1", + "log1p", + "sqrt", + "sinh", + "cosh", + "tanh", + "arcsin", + "arccos", + "arctan", + "arccosh", + "arcsinh", + "arctanh", + "abs", + "log10", + "floor", + "ceil", +) +_binary_math_ops = ("arctan2",) + +MATHOPS = _unary_math_ops + _binary_math_ops + + +LOCAL_TAG = "__pd_eval_local_" + + +class Term: + def __new__(cls, name, env, side=None, encoding=None): + klass = Constant if not isinstance(name, str) else cls + # error: Argument 2 for "super" not an instance of argument 1 + supr_new = super(Term, klass).__new__ # type: ignore[misc] + return supr_new(klass) + + is_local: bool + + def __init__(self, name, env, side=None, encoding=None) -> None: + # name is a str for Term, but may be something else for subclasses + self._name = name + self.env = env + self.side = side + tname = str(name) + self.is_local = tname.startswith(LOCAL_TAG) or tname in DEFAULT_GLOBALS + self._value = self._resolve_name() + self.encoding = encoding + + @property + def local_name(self) -> str: + return self.name.replace(LOCAL_TAG, "") + + def __repr__(self) -> str: + return pprint_thing(self.name) + + def __call__(self, *args, **kwargs): + return self.value + + def evaluate(self, *args, **kwargs) -> Term: + return self + + def _resolve_name(self): + local_name = str(self.local_name) + is_local = self.is_local + if local_name in self.env.scope and isinstance( + self.env.scope[local_name], type + ): + is_local = False + + res = self.env.resolve(local_name, is_local=is_local) + self.update(res) + + if hasattr(res, "ndim") and res.ndim > 2: + raise NotImplementedError( + "N-dimensional objects, where N > 2, are not supported with eval" + ) + return res + + def update(self, value) -> None: + """ + search order for local (i.e., @variable) variables: + + scope, key_variable + [('locals', 'local_name'), + ('globals', 'local_name'), + ('locals', 'key'), + ('globals', 'key')] + """ + key = self.name + + # if it's a variable name (otherwise a constant) + if isinstance(key, str): + self.env.swapkey(self.local_name, key, new_value=value) + + self.value = value + + @property + def is_scalar(self) -> bool: + return is_scalar(self._value) + + @property + def type(self): + try: + # potentially very slow for large, mixed dtype frames + return self._value.values.dtype + except AttributeError: + try: + # ndarray + return self._value.dtype + except AttributeError: + # scalar + return type(self._value) + + return_type = type + + @property + def raw(self) -> str: + return f"{type(self).__name__}(name={repr(self.name)}, type={self.type})" + + @property + def is_datetime(self) -> bool: + try: + t = self.type.type + except AttributeError: + t = self.type + + return issubclass(t, (datetime, np.datetime64)) + + @property + def value(self): + return self._value + + @value.setter + def value(self, new_value) -> None: + self._value = new_value + + @property + def name(self): + return self._name + + @property + def ndim(self) -> int: + return self._value.ndim + + +class Constant(Term): + def _resolve_name(self): + return self._name + + @property + def name(self): + return self.value + + def __repr__(self) -> str: + # in python 2 str() of float + # can truncate shorter than repr() + return repr(self.name) + + +_bool_op_map = {"not": "~", "and": "&", "or": "|"} + + +class Op: + """ + Hold an operator of arbitrary arity. + """ + + op: str + + def __init__(self, op: str, operands: Iterable[Term | Op], encoding=None) -> None: + self.op = _bool_op_map.get(op, op) + self.operands = operands + self.encoding = encoding + + def __iter__(self) -> Iterator: + return iter(self.operands) + + def __repr__(self) -> str: + """ + Print a generic n-ary operator and its operands using infix notation. + """ + # recurse over the operands + parened = (f"({pprint_thing(opr)})" for opr in self.operands) + return pprint_thing(f" {self.op} ".join(parened)) + + @property + def return_type(self): + # clobber types to bool if the op is a boolean operator + if self.op in (CMP_OPS_SYMS + BOOL_OPS_SYMS): + return np.bool_ + return result_type_many(*(term.type for term in com.flatten(self))) + + @property + def has_invalid_return_type(self) -> bool: + types = self.operand_types + obj_dtype_set = frozenset([np.dtype("object")]) + return self.return_type == object and types - obj_dtype_set + + @property + def operand_types(self): + return frozenset(term.type for term in com.flatten(self)) + + @property + def is_scalar(self) -> bool: + return all(operand.is_scalar for operand in self.operands) + + @property + def is_datetime(self) -> bool: + try: + t = self.return_type.type + except AttributeError: + t = self.return_type + + return issubclass(t, (datetime, np.datetime64)) + + +def _in(x, y): + """ + Compute the vectorized membership of ``x in y`` if possible, otherwise + use Python. + """ + try: + return x.isin(y) + except AttributeError: + if is_list_like(x): + try: + return y.isin(x) + except AttributeError: + pass + return x in y + + +def _not_in(x, y): + """ + Compute the vectorized membership of ``x not in y`` if possible, + otherwise use Python. + """ + try: + return ~x.isin(y) + except AttributeError: + if is_list_like(x): + try: + return ~y.isin(x) + except AttributeError: + pass + return x not in y + + +CMP_OPS_SYMS = (">", "<", ">=", "<=", "==", "!=", "in", "not in") +_cmp_ops_funcs = ( + operator.gt, + operator.lt, + operator.ge, + operator.le, + operator.eq, + operator.ne, + _in, + _not_in, +) +_cmp_ops_dict = dict(zip(CMP_OPS_SYMS, _cmp_ops_funcs)) + +BOOL_OPS_SYMS = ("&", "|", "and", "or") +_bool_ops_funcs = (operator.and_, operator.or_, operator.and_, operator.or_) +_bool_ops_dict = dict(zip(BOOL_OPS_SYMS, _bool_ops_funcs)) + +ARITH_OPS_SYMS = ("+", "-", "*", "/", "**", "//", "%") +_arith_ops_funcs = ( + operator.add, + operator.sub, + operator.mul, + operator.truediv, + operator.pow, + operator.floordiv, + operator.mod, +) +_arith_ops_dict = dict(zip(ARITH_OPS_SYMS, _arith_ops_funcs)) + +SPECIAL_CASE_ARITH_OPS_SYMS = ("**", "//", "%") +_special_case_arith_ops_funcs = (operator.pow, operator.floordiv, operator.mod) +_special_case_arith_ops_dict = dict( + zip(SPECIAL_CASE_ARITH_OPS_SYMS, _special_case_arith_ops_funcs) +) + +_binary_ops_dict = {} + +for d in (_cmp_ops_dict, _bool_ops_dict, _arith_ops_dict): + _binary_ops_dict.update(d) + + +def _cast_inplace(terms, acceptable_dtypes, dtype) -> None: + """ + Cast an expression inplace. + + Parameters + ---------- + terms : Op + The expression that should cast. + acceptable_dtypes : list of acceptable numpy.dtype + Will not cast if term's dtype in this list. + dtype : str or numpy.dtype + The dtype to cast to. + """ + dt = np.dtype(dtype) + for term in terms: + if term.type in acceptable_dtypes: + continue + + try: + new_value = term.value.astype(dt) + except AttributeError: + new_value = dt.type(term.value) + term.update(new_value) + + +def is_term(obj) -> bool: + return isinstance(obj, Term) + + +class BinOp(Op): + """ + Hold a binary operator and its operands. + + Parameters + ---------- + op : str + lhs : Term or Op + rhs : Term or Op + """ + + def __init__(self, op: str, lhs, rhs) -> None: + super().__init__(op, (lhs, rhs)) + self.lhs = lhs + self.rhs = rhs + + self._disallow_scalar_only_bool_ops() + + self.convert_values() + + try: + self.func = _binary_ops_dict[op] + except KeyError as err: + # has to be made a list for python3 + keys = list(_binary_ops_dict.keys()) + raise ValueError( + f"Invalid binary operator {repr(op)}, valid operators are {keys}" + ) from err + + def __call__(self, env): + """ + Recursively evaluate an expression in Python space. + + Parameters + ---------- + env : Scope + + Returns + ------- + object + The result of an evaluated expression. + """ + # recurse over the left/right nodes + left = self.lhs(env) + right = self.rhs(env) + + return self.func(left, right) + + def evaluate(self, env, engine: str, parser, term_type, eval_in_python): + """ + Evaluate a binary operation *before* being passed to the engine. + + Parameters + ---------- + env : Scope + engine : str + parser : str + term_type : type + eval_in_python : list + + Returns + ------- + term_type + The "pre-evaluated" expression as an instance of ``term_type`` + """ + if engine == "python": + res = self(env) + else: + # recurse over the left/right nodes + + left = self.lhs.evaluate( + env, + engine=engine, + parser=parser, + term_type=term_type, + eval_in_python=eval_in_python, + ) + + right = self.rhs.evaluate( + env, + engine=engine, + parser=parser, + term_type=term_type, + eval_in_python=eval_in_python, + ) + + # base cases + if self.op in eval_in_python: + res = self.func(left.value, right.value) + else: + from pandas.core.computation.eval import eval + + res = eval(self, local_dict=env, engine=engine, parser=parser) + + name = env.add_tmp(res) + return term_type(name, env=env) + + def convert_values(self) -> None: + """ + Convert datetimes to a comparable value in an expression. + """ + + def stringify(value): + encoder: Callable + if self.encoding is not None: + encoder = partial(pprint_thing_encoded, encoding=self.encoding) + else: + encoder = pprint_thing + return encoder(value) + + lhs, rhs = self.lhs, self.rhs + + if is_term(lhs) and lhs.is_datetime and is_term(rhs) and rhs.is_scalar: + v = rhs.value + if isinstance(v, (int, float)): + v = stringify(v) + v = Timestamp(ensure_decoded(v)) + if v.tz is not None: + v = v.tz_convert("UTC") + self.rhs.update(v) + + if is_term(rhs) and rhs.is_datetime and is_term(lhs) and lhs.is_scalar: + v = lhs.value + if isinstance(v, (int, float)): + v = stringify(v) + v = Timestamp(ensure_decoded(v)) + if v.tz is not None: + v = v.tz_convert("UTC") + self.lhs.update(v) + + def _disallow_scalar_only_bool_ops(self): + rhs = self.rhs + lhs = self.lhs + + # GH#24883 unwrap dtype if necessary to ensure we have a type object + rhs_rt = rhs.return_type + rhs_rt = getattr(rhs_rt, "type", rhs_rt) + lhs_rt = lhs.return_type + lhs_rt = getattr(lhs_rt, "type", lhs_rt) + if ( + (lhs.is_scalar or rhs.is_scalar) + and self.op in _bool_ops_dict + and ( + not ( + issubclass(rhs_rt, (bool, np.bool_)) + and issubclass(lhs_rt, (bool, np.bool_)) + ) + ) + ): + raise NotImplementedError("cannot evaluate scalar only bool ops") + + +def isnumeric(dtype) -> bool: + return issubclass(np.dtype(dtype).type, np.number) + + +class Div(BinOp): + """ + Div operator to special case casting. + + Parameters + ---------- + lhs, rhs : Term or Op + The Terms or Ops in the ``/`` expression. + """ + + def __init__(self, lhs, rhs) -> None: + super().__init__("/", lhs, rhs) + + if not isnumeric(lhs.return_type) or not isnumeric(rhs.return_type): + raise TypeError( + f"unsupported operand type(s) for {self.op}: " + f"'{lhs.return_type}' and '{rhs.return_type}'" + ) + + # do not upcast float32s to float64 un-necessarily + acceptable_dtypes = [np.float32, np.float64] + _cast_inplace(com.flatten(self), acceptable_dtypes, np.float64) + + +UNARY_OPS_SYMS = ("+", "-", "~", "not") +_unary_ops_funcs = (operator.pos, operator.neg, operator.invert, operator.invert) +_unary_ops_dict = dict(zip(UNARY_OPS_SYMS, _unary_ops_funcs)) + + +class UnaryOp(Op): + """ + Hold a unary operator and its operands. + + Parameters + ---------- + op : str + The token used to represent the operator. + operand : Term or Op + The Term or Op operand to the operator. + + Raises + ------ + ValueError + * If no function associated with the passed operator token is found. + """ + + def __init__(self, op: Literal["+", "-", "~", "not"], operand) -> None: + super().__init__(op, (operand,)) + self.operand = operand + + try: + self.func = _unary_ops_dict[op] + except KeyError as err: + raise ValueError( + f"Invalid unary operator {repr(op)}, " + f"valid operators are {UNARY_OPS_SYMS}" + ) from err + + def __call__(self, env) -> MathCall: + operand = self.operand(env) + # error: Cannot call function of unknown type + return self.func(operand) # type: ignore[operator] + + def __repr__(self) -> str: + return pprint_thing(f"{self.op}({self.operand})") + + @property + def return_type(self) -> np.dtype: + operand = self.operand + if operand.return_type == np.dtype("bool"): + return np.dtype("bool") + if isinstance(operand, Op) and ( + operand.op in _cmp_ops_dict or operand.op in _bool_ops_dict + ): + return np.dtype("bool") + return np.dtype("int") + + +class MathCall(Op): + def __init__(self, func, args) -> None: + super().__init__(func.name, args) + self.func = func + + def __call__(self, env): + # error: "Op" not callable + operands = [op(env) for op in self.operands] # type: ignore[operator] + return self.func.func(*operands) + + def __repr__(self) -> str: + operands = map(str, self.operands) + return pprint_thing(f"{self.op}({','.join(operands)})") + + +class FuncNode: + def __init__(self, name: str) -> None: + if name not in MATHOPS: + raise ValueError(f'"{name}" is not a supported function') + self.name = name + self.func = getattr(np, name) + + def __call__(self, *args): + return MathCall(self, args) diff --git a/third_party/bigframes_vendored/pandas/core/computation/parsing.py b/third_party/bigframes_vendored/pandas/core/computation/parsing.py new file mode 100644 index 00000000000..e54f4597357 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/parsing.py @@ -0,0 +1,196 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/parsing.py +""" +:func:`~pandas.eval` source string parsing functions +""" +from __future__ import annotations + +from io import StringIO +from keyword import iskeyword +import token +import tokenize +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Hashable, Iterator + +# A token value Python's tokenizer probably will never use. +BACKTICK_QUOTED_STRING = 100 + + +def create_valid_python_identifier(name: str) -> str: + """ + Create valid Python identifiers from any string. + + Check if name contains any special characters. If it contains any + special characters, the special characters will be replaced by + a special string and a prefix is added. + + Raises + ------ + SyntaxError + If the returned name is not a Python valid identifier, raise an exception. + This can happen if there is a hashtag in the name, as the tokenizer will + than terminate and not find the backtick. + But also for characters that fall out of the range of (U+0001..U+007F). + """ + if name.isidentifier() and not iskeyword(name): + return name + + # Create a dict with the special characters and their replacement string. + # EXACT_TOKEN_TYPES contains these special characters + # token.tok_name contains a readable description of the replacement string. + special_characters_replacements = { + char: f"_{token.tok_name[tokval]}_" + for char, tokval in (tokenize.EXACT_TOKEN_TYPES.items()) + } + special_characters_replacements.update( + { + " ": "_", + "?": "_QUESTIONMARK_", + "!": "_EXCLAMATIONMARK_", + "$": "_DOLLARSIGN_", + "€": "_EUROSIGN_", + "°": "_DEGREESIGN_", + # Including quotes works, but there are exceptions. + "'": "_SINGLEQUOTE_", + '"': "_DOUBLEQUOTE_", + # Currently not possible. Terminates parser and won't find backtick. + # "#": "_HASH_", + } + ) + + name = "".join([special_characters_replacements.get(char, char) for char in name]) + name = f"BACKTICK_QUOTED_STRING_{name}" + + if not name.isidentifier(): + raise SyntaxError(f"Could not convert '{name}' to a valid Python identifier.") + + return name + + +def clean_backtick_quoted_toks(tok: tuple[int, str]) -> tuple[int, str]: + """ + Clean up a column name if surrounded by backticks. + + Backtick quoted string are indicated by a certain tokval value. If a string + is a backtick quoted token it will processed by + :func:`_create_valid_python_identifier` so that the parser can find this + string when the query is executed. + In this case the tok will get the NAME tokval. + + Parameters + ---------- + tok : tuple of int, str + ints correspond to the all caps constants in the tokenize module + + Returns + ------- + tok : Tuple[int, str] + Either the input or token or the replacement values + """ + toknum, tokval = tok + if toknum == BACKTICK_QUOTED_STRING: + return tokenize.NAME, create_valid_python_identifier(tokval) + return toknum, tokval + + +def clean_column_name(name: Hashable) -> Hashable: + """ + Function to emulate the cleaning of a backtick quoted name. + + The purpose for this function is to see what happens to the name of + identifier if it goes to the process of being parsed a Python code + inside a backtick quoted string and than being cleaned + (removed of any special characters). + + Parameters + ---------- + name : hashable + Name to be cleaned. + + Returns + ------- + name : hashable + Returns the name after tokenizing and cleaning. + + Notes + ----- + For some cases, a name cannot be converted to a valid Python identifier. + In that case :func:`tokenize_string` raises a SyntaxError. + In that case, we just return the name unmodified. + + If this name was used in the query string (this makes the query call impossible) + an error will be raised by :func:`tokenize_backtick_quoted_string` instead, + which is not caught and propagates to the user level. + """ + try: + tokenized = tokenize_string(f"`{name}`") + tokval = next(tokenized)[1] + return create_valid_python_identifier(tokval) + except SyntaxError: + return name + + +def tokenize_backtick_quoted_string( + token_generator: Iterator[tokenize.TokenInfo], source: str, string_start: int +) -> tuple[int, str]: + """ + Creates a token from a backtick quoted string. + + Moves the token_generator forwards till right after the next backtick. + + Parameters + ---------- + token_generator : Iterator[tokenize.TokenInfo] + The generator that yields the tokens of the source string (Tuple[int, str]). + The generator is at the first token after the backtick (`) + + source : str + The Python source code string. + + string_start : int + This is the start of backtick quoted string inside the source string. + + Returns + ------- + tok: Tuple[int, str] + The token that represents the backtick quoted string. + The integer is equal to BACKTICK_QUOTED_STRING (100). + """ + for _, tokval, start, _, _ in token_generator: + if tokval == "`": + string_end = start[1] + break + + return BACKTICK_QUOTED_STRING, source[string_start:string_end] + + +def tokenize_string(source: str) -> Iterator[tuple[int, str]]: + """ + Tokenize a Python source code string. + + Parameters + ---------- + source : str + The Python source code string. + + Returns + ------- + tok_generator : Iterator[Tuple[int, str]] + An iterator yielding all tokens with only toknum and tokval (Tuple[ing, str]). + """ + line_reader = StringIO(source).readline + token_generator = tokenize.generate_tokens(line_reader) + + # Loop over all tokens till a backtick (`) is found. + # Then, take all tokens till the next backtick to form a backtick quoted string + for toknum, tokval, start, _, _ in token_generator: + if tokval == "`": + try: + yield tokenize_backtick_quoted_string( + token_generator, source, string_start=start[1] + 1 + ) + except Exception as err: + raise SyntaxError(f"Failed to parse backticks in '{source}'.") from err + else: + yield toknum, tokval diff --git a/third_party/bigframes_vendored/pandas/core/computation/scope.py b/third_party/bigframes_vendored/pandas/core/computation/scope.py new file mode 100644 index 00000000000..bfd7eb1d12b --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/computation/scope.py @@ -0,0 +1,355 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/computation/scope.py +""" +Module for scope operations +""" +from __future__ import annotations + +from collections import ChainMap +import datetime +import inspect +from io import StringIO +import itertools +import pprint +import struct +import sys +from typing import TypeVar + +import numpy as np +from pandas._libs.tslibs import Timestamp +from pandas.errors import UndefinedVariableError + +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") + + +# https://docs.python.org/3/library/collections.html#chainmap-examples-and-recipes +class DeepChainMap(ChainMap[_KT, _VT]): + """ + Variant of ChainMap that allows direct updates to inner scopes. + + Only works when all passed mapping are mutable. + """ + + def __setitem__(self, key: _KT, value: _VT) -> None: + for mapping in self.maps: + if key in mapping: + mapping[key] = value + return + self.maps[0][key] = value + + def __delitem__(self, key: _KT) -> None: + """ + Raises + ------ + KeyError + If `key` doesn't exist. + """ + for mapping in self.maps: + if key in mapping: + del mapping[key] + return + raise KeyError(key) + + +def ensure_scope( + level: int, global_dict=None, local_dict=None, resolvers=(), target=None +) -> Scope: + """Ensure that we are grabbing the correct scope.""" + return Scope( + level + 1, + global_dict=global_dict, + local_dict=local_dict, + resolvers=resolvers, + target=target, + ) + + +def _replacer(x) -> str: + """ + Replace a number with its hexadecimal representation. Used to tag + temporary variables with their calling scope's id. + """ + # get the hex repr of the binary char and remove 0x and pad by pad_size + # zeros + try: + hexin = ord(x) + except TypeError: + # bytes literals masquerade as ints when iterating in py3 + hexin = x + + return hex(hexin) + + +def _raw_hex_id(obj) -> str: + """Return the padded hexadecimal id of ``obj``.""" + # interpret as a pointer since that's what really what id returns + packed = struct.pack("@P", id(obj)) + return "".join([_replacer(x) for x in packed]) + + +DEFAULT_GLOBALS = { + "Timestamp": Timestamp, + "datetime": datetime.datetime, + "True": True, + "False": False, + "list": list, + "tuple": tuple, + "inf": np.inf, + "Inf": np.inf, +} + + +def _get_pretty_string(obj) -> str: + """ + Return a prettier version of obj. + + Parameters + ---------- + obj : object + Object to pretty print + + Returns + ------- + str + Pretty print object repr + """ + sio = StringIO() + pprint.pprint(obj, stream=sio) + return sio.getvalue() + + +class Scope: + """ + Object to hold scope, with a few bells to deal with some custom syntax + and contexts added by pandas. + + Parameters + ---------- + level : int + global_dict : dict or None, optional, default None + local_dict : dict or Scope or None, optional, default None + resolvers : list-like or None, optional, default None + target : object + + Attributes + ---------- + level : int + scope : DeepChainMap + target : object + temps : dict + """ + + __slots__ = ["level", "scope", "target", "resolvers", "temps"] + level: int + scope: DeepChainMap + resolvers: DeepChainMap + temps: dict + + def __init__( + self, level: int, global_dict=None, local_dict=None, resolvers=(), target=None + ) -> None: + self.level = level + 1 + + # shallow copy because we don't want to keep filling this up with what + # was there before if there are multiple calls to Scope/_ensure_scope + self.scope = DeepChainMap(DEFAULT_GLOBALS.copy()) + self.target = target + + if isinstance(local_dict, Scope): + self.scope.update(local_dict.scope) + if local_dict.target is not None: + self.target = local_dict.target + self._update(local_dict.level) + + frame = sys._getframe(self.level) + + try: + # shallow copy here because we don't want to replace what's in + # scope when we align terms (alignment accesses the underlying + # numpy array of pandas objects) + scope_global = self.scope.new_child( + (global_dict if global_dict is not None else frame.f_globals).copy() + ) + self.scope = DeepChainMap(scope_global) + if not isinstance(local_dict, Scope): + scope_local = self.scope.new_child( + (local_dict if local_dict is not None else frame.f_locals).copy() + ) + self.scope = DeepChainMap(scope_local) + finally: + del frame + + # assumes that resolvers are going from outermost scope to inner + if isinstance(local_dict, Scope): + resolvers += tuple(local_dict.resolvers.maps) + self.resolvers = DeepChainMap(*resolvers) + self.temps = {} + + def __repr__(self) -> str: + scope_keys = _get_pretty_string(list(self.scope.keys())) + res_keys = _get_pretty_string(list(self.resolvers.keys())) + return f"{type(self).__name__}(scope={scope_keys}, resolvers={res_keys})" + + @property + def has_resolvers(self) -> bool: + """ + Return whether we have any extra scope. + + For example, DataFrames pass Their columns as resolvers during calls to + ``DataFrame.eval()`` and ``DataFrame.query()``. + + Returns + ------- + hr : bool + """ + return bool(len(self.resolvers)) + + def resolve(self, key: str, is_local: bool): + """ + Resolve a variable name in a possibly local context. + + Parameters + ---------- + key : str + A variable name + is_local : bool + Flag indicating whether the variable is local or not (prefixed with + the '@' symbol) + + Returns + ------- + value : object + The value of a particular variable + """ + try: + # only look for locals in outer scope + if is_local: + return self.scope[key] + + # not a local variable so check in resolvers if we have them + if self.has_resolvers: + return self.resolvers[key] + + # if we're here that means that we have no locals and we also have + # no resolvers + assert not is_local and not self.has_resolvers + return self.scope[key] + except KeyError: + try: + # last ditch effort we look in temporaries + # these are created when parsing indexing expressions + # e.g., df[df > 0] + return self.temps[key] + except KeyError as err: + raise UndefinedVariableError(key, is_local) from err + + def swapkey(self, old_key: str, new_key: str, new_value=None) -> None: + """ + Replace a variable name, with a potentially new value. + + Parameters + ---------- + old_key : str + Current variable name to replace + new_key : str + New variable name to replace `old_key` with + new_value : object + Value to be replaced along with the possible renaming + """ + if self.has_resolvers: + maps = self.resolvers.maps + self.scope.maps + else: + maps = self.scope.maps + + maps.append(self.temps) + + for mapping in maps: + if old_key in mapping: + mapping[new_key] = new_value + return + + def _get_vars(self, stack, scopes: list[str]) -> None: + """ + Get specifically scoped variables from a list of stack frames. + + Parameters + ---------- + stack : list + A list of stack frames as returned by ``inspect.stack()`` + scopes : sequence of strings + A sequence containing valid stack frame attribute names that + evaluate to a dictionary. For example, ('locals', 'globals') + """ + variables = itertools.product(scopes, stack) + for scope, (frame, _, _, _, _, _) in variables: + try: + d = getattr(frame, f"f_{scope}") + self.scope = DeepChainMap(self.scope.new_child(d)) + finally: + # won't remove it, but DECREF it + # in Py3 this probably isn't necessary since frame won't be + # scope after the loop + del frame + + def _update(self, level: int) -> None: + """ + Update the current scope by going back `level` levels. + + Parameters + ---------- + level : int + """ + sl = level + 1 + + # add sl frames to the scope starting with the + # most distant and overwriting with more current + # makes sure that we can capture variable scope + stack = inspect.stack() + + try: + self._get_vars(stack[:sl], scopes=["locals"]) + finally: + del stack[:], stack + + def add_tmp(self, value) -> str: + """ + Add a temporary variable to the scope. + + Parameters + ---------- + value : object + An arbitrary object to be assigned to a temporary variable. + + Returns + ------- + str + The name of the temporary variable created. + """ + name = f"{type(value).__name__}_{self.ntemps}_{_raw_hex_id(self)}" + + # add to inner most scope + assert name not in self.temps + self.temps[name] = value + assert name in self.temps + + # only increment if the variable gets put in the scope + return name + + @property + def ntemps(self) -> int: + """The number of temporary variables in this scope""" + return len(self.temps) + + @property + def full_scope(self) -> DeepChainMap: + """ + Return the full scope for use with passing to engines transparently + as a mapping. + + Returns + ------- + vars : DeepChainMap + All variables in this scope. + """ + maps = [self.temps] + self.resolvers.maps + self.scope.maps + return DeepChainMap(*maps) diff --git a/third_party/bigframes_vendored/pandas/core/dtypes/inference.py b/third_party/bigframes_vendored/pandas/core/dtypes/inference.py new file mode 100644 index 00000000000..fcbb4c242f4 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/dtypes/inference.py @@ -0,0 +1,31 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/dtypes/inference.py +""" basic inference routines """ + +from __future__ import annotations + +from collections import abc + + +def iterable_not_string(obj) -> bool: + """ + Check if the object is an iterable but not a string. + + Parameters + ---------- + obj : The object to check. + + Returns + ------- + is_iter_not_string : bool + Whether `obj` is a non-string iterable. + + Examples + -------- + >>> iterable_not_string([1, 2, 3]) + True + >>> iterable_not_string("foo") + False + >>> iterable_not_string(1) + False + """ + return isinstance(obj, abc.Iterable) and not isinstance(obj, str) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index d70d3827e7f..2640cce6dad 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2865,6 +2865,7 @@ def cov(self, *, numeric_only) -> DataFrame: Returns: DataFrame: The covariance matrix of the series of the DataFrame. """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) def update( self, other, join: str = "left", overwrite: bool = True, filter_func=None @@ -4931,6 +4932,158 @@ def value_counts( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def eval(self, expr: str) -> DataFrame: + """ + Evaluate a string describing operations on DataFrame columns. + + Operates on columns only, not specific rows or elements. This allows + `eval` to run arbitrary code, which can make you vulnerable to code + injection if you pass user input to this function. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': range(1, 6), 'B': range(10, 0, -2)}) + >>> df + A B + 0 1 10 + 1 2 8 + 2 3 6 + 3 4 4 + 4 5 2 + + [5 rows x 2 columns] + >>> df.eval('A + B') + 0 11 + 1 10 + 2 9 + 3 8 + 4 7 + dtype: Int64 + + Assignment is allowed though by default the original DataFrame is not + modified. + + >>> df.eval('C = A + B') + A B C + 0 1 10 11 + 1 2 8 10 + 2 3 6 9 + 3 4 4 8 + 4 5 2 7 + + [5 rows x 3 columns] + >>> df + A B + 0 1 10 + 1 2 8 + 2 3 6 + 3 4 4 + 4 5 2 + + [5 rows x 2 columns] + + Multiple columns can be assigned to using multi-line expressions: + + >>> df.eval( + ... ''' + ... C = A + B + ... D = A - B + ... ''' + ... ) + A B C D + 0 1 10 11 -9 + 1 2 8 10 -6 + 2 3 6 9 -3 + 3 4 4 8 0 + 4 5 2 7 3 + + [5 rows x 4 columns] + + + Args: + expr (str): + The expression string to evaluate. + + Returns: + DataFrame + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def query(self, expr: str) -> DataFrame | None: + """ + Query the columns of a DataFrame with a boolean expression. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': range(1, 6), + ... 'B': range(10, 0, -2), + ... 'C C': range(10, 5, -1)}) + >>> df + A B C C + 0 1 10 10 + 1 2 8 9 + 2 3 6 8 + 3 4 4 7 + 4 5 2 6 + + [5 rows x 3 columns] + >>> df.query('A > B') + A B C C + 4 5 2 6 + + [1 rows x 3 columns] + + The previous expression is equivalent to + + >>> df[df.A > df.B] + A B C C + 4 5 2 6 + + [1 rows x 3 columns] + + For columns with spaces in their name, you can use backtick quoting. + + >>> df.query('B == `C C`') + A B C C + 0 1 10 10 + + [1 rows x 3 columns] + + The previous expression is equivalent to + + >>> df[df.B == df['C C']] + A B C C + 0 1 10 10 + + [1 rows x 3 columns] + + Args: + expr (str): + The query string to evaluate. + + You can refer to variables + in the environment by prefixing them with an '@' character like + ``@a + b``. + + You can refer to column names that are not valid Python variable names + by surrounding them in backticks. Thus, column names containing spaces + or punctuations (besides underscores) or starting with digits must be + surrounded by backticks. (For example, a column named "Area (cm^2)" would + be referenced as ```Area (cm^2)```). Column names which are Python keywords + (like "list", "for", "import", etc) cannot be used. + + For example, if one of your columns is called ``a a`` and you want + to sum it with ``b``, your query should be ```a a` + b``. + + Returns: + DataFrame + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def interpolate(self, method: str = "linear"): """ Fill NaN values using an interpolation method. diff --git a/third_party/bigframes_vendored/pandas/util/_exceptions.py b/third_party/bigframes_vendored/pandas/util/_exceptions.py new file mode 100644 index 00000000000..4ca649153a0 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/util/_exceptions.py @@ -0,0 +1,29 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/util/_exceptions.py +from __future__ import annotations + +import inspect +import os + + +def find_stack_level() -> int: + """ + Find the first place in the stack that is not inside pandas + (tests notwithstanding). + """ + + import pandas as pd + + pkg_dir = os.path.dirname(pd.__file__) + test_dir = os.path.join(pkg_dir, "tests") + + # https://stackoverflow.com/questions/17407119/python-inspect-stack-is-slow + frame = inspect.currentframe() + n = 0 + while frame: + fname = inspect.getfile(frame) + if fname.startswith(pkg_dir) and not fname.startswith(test_dir): + frame = frame.f_back + n += 1 + else: + break + return n diff --git a/third_party/bigframes_vendored/pandas/util/_validators.py b/third_party/bigframes_vendored/pandas/util/_validators.py new file mode 100644 index 00000000000..1f36e0d528c --- /dev/null +++ b/third_party/bigframes_vendored/pandas/util/_validators.py @@ -0,0 +1,58 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/util/_validators.py +""" +Module that contains many useful utilities +for validating data or function arguments +""" +from __future__ import annotations + +from typing import TypeVar + +from pandas.core.dtypes.common import is_bool + +BoolishT = TypeVar("BoolishT", bool, int) +BoolishNoneT = TypeVar("BoolishNoneT", bool, int, None) + + +def validate_bool_kwarg( + value: BoolishNoneT, + arg_name: str, + none_allowed: bool = True, + int_allowed: bool = False, +) -> BoolishNoneT: + """ + Ensure that argument passed in arg_name can be interpreted as boolean. + + Parameters + ---------- + value : bool + Value to be validated. + arg_name : str + Name of the argument. To be reflected in the error message. + none_allowed : bool, default True + Whether to consider None to be a valid boolean. + int_allowed : bool, default False + Whether to consider integer value to be a valid boolean. + + Returns + ------- + value + The same value as input. + + Raises + ------ + ValueError + If the value is not a valid boolean. + """ + good_value = is_bool(value) + if none_allowed: + good_value = good_value or (value is None) + + if int_allowed: + good_value = good_value or isinstance(value, int) + + if not good_value: + raise ValueError( + f'For argument "{arg_name}" expected type bool, received ' + f"type {type(value).__name__}." + ) + return value # pyright: ignore[reportGeneralTypeIssues] From e7a8e461ea4ba8df74c0da978b23413e590368bc Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:54:48 -0700 Subject: [PATCH 428/921] chore: remove initialization workaround in to_datetime. (#514) * chore: Remove initialization workaround in to_datetime. * remove unused import * Update document. --- bigframes/core/tools/datetimes.py | 17 +---------------- .../pandas/core/arrays/datetimelike.py | 2 +- .../pandas/core/tools/datetimes.py | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/bigframes/core/tools/datetimes.py b/bigframes/core/tools/datetimes.py index 96bf556101d..a2851bc256b 100644 --- a/bigframes/core/tools/datetimes.py +++ b/bigframes/core/tools/datetimes.py @@ -20,7 +20,6 @@ import pandas as pd import bigframes.constants as constants -import bigframes.core.global_session as global_session import bigframes.dataframe import bigframes.operations as ops import bigframes.series @@ -52,21 +51,7 @@ def to_datetime( f"to datetime is not implemented. {constants.FEEDBACK_LINK}" ) - if not isinstance(arg, bigframes.series.Series): - # This block ensures compatibility with local data formats, including - # iterables and pandas.Series - # TODO: Currently, data upload is performed using pandas DataFrames - # combined with the `read_pandas` method due to the BigFrames DataFrame - # constructor's limitations in handling various data types. Plan to update - # the upload process to utilize the BigFrames DataFrame constructor directly - # once it is enhanced for more related datatypes. - arg = global_session.with_default_session( - bigframes.session.Session.read_pandas, pd.DataFrame(arg) - ) - if len(arg.columns) != 1: - raise ValueError("Input must be 1-dimensional.") - - arg = arg[arg.columns[0]] + arg = bigframes.series.Series(arg) if not utc and arg.dtype not in ("Int64", "Float64"): # type: ignore raise NotImplementedError( diff --git a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py index bd5f055ece0..ce5f8d55f37 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py @@ -26,7 +26,7 @@ def strftime(self, date_format: str): 0 August 15, 2014, 08:15:12 AM 1 February 29, 2012, 02:15:12 AM 2 August 15, 2015, 03:15:12 AM - Name: 0, dtype: string + dtype: string Args: date_format (str): diff --git a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py index 8a8a562baeb..442220f2376 100644 --- a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py +++ b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py @@ -48,7 +48,7 @@ def to_datetime( >>> bpd.to_datetime(list_str, format="%m-%d-%Y %H:%M", utc=True) 0 2021-01-31 14:30:00+00:00 1 2021-02-28 15:45:00+00:00 - Name: 0, dtype: timestamp[us, tz=UTC][pyarrow] + dtype: timestamp[us, tz=UTC][pyarrow] Converting a Series of Strings with Timezone Information: From 6873b30b691a11a368308825a72013d8ec1408ed Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 26 Mar 2024 13:42:16 -0700 Subject: [PATCH 429/921] fix: Product operation produces float result for all input types (#501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/compile/aggregate_compiler.py | 2 +- bigframes/operations/aggregations.py | 5 +---- tests/system/small/test_groupby.py | 3 +-- tests/system/small/test_series.py | 2 +- third_party/bigframes_vendored/pandas/core/frame.py | 8 ++++---- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 9c1db0f1622..ae212435069 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -190,7 +190,7 @@ def _( .else_(magnitude * pow(-1, negative_count_parity)) .end() ) - return float_result.cast(column.type()) # type: ignore + return float_result @compile_unary_agg.register diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 9a270f1ce71..76aa2a61127 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -139,10 +139,7 @@ class ProductOp(UnaryAggregateOp): name: ClassVar[str] = "product" def output_type(self, *input_types: dtypes.ExpressionType): - if pd.api.types.is_bool_dtype(input_types[0]): - return dtypes.INT_DTYPE - else: - return input_types[0] + return dtypes.FLOAT_DTYPE @dataclasses.dataclass(frozen=True) diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index e7ecbedfc2e..ba79ba1ab1a 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -228,8 +228,7 @@ def test_dataframe_groupby_multi_sum( (lambda x: x.cumsum(numeric_only=True)), (lambda x: x.cummax(numeric_only=True)), (lambda x: x.cummin(numeric_only=True)), - # pandas 2.2 uses floating point for cumulative product even for - # integer inputs. + # Pre-pandas 2.2 doesn't always proeduce float. (lambda x: x.cumprod().astype("Float64")), (lambda x: x.shift(periods=2)), ], diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 8847753e881..258fb1cfd8f 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1481,7 +1481,7 @@ def test_groupby_prod(scalars_dfs): bf_series = scalars_df[col_name].groupby(scalars_df["int64_col"]).prod() pd_series = ( scalars_pandas_df[col_name].groupby(scalars_pandas_df["int64_col"]).prod() - ) + ).astype(pd.Float64Dtype()) # TODO(swast): Update groupby to use index based on group by key(s). bf_result = bf_series.to_pandas() assert_series_equal( diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 2640cce6dad..50cce1eeabc 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4416,10 +4416,10 @@ def cumprod(self) -> DataFrame: [3 rows x 2 columns] >>> df.cumprod() - A B - 0 3 1 - 1 3 2 - 2 6 6 + A B + 0 3.0 1.0 + 1 3.0 2.0 + 2 6.0 6.0 [3 rows x 2 columns] From 3b80f956755c9d7043138aab6e5687cba50be8cb Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:09:44 -0700 Subject: [PATCH 430/921] docs: fix docs of ARIMAPlus.predict (#512) --- bigframes/ml/forecasting.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 292389dcbba..79933272004 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -234,7 +234,12 @@ def _fit( def predict( self, X=None, *, horizon: int = 3, confidence_level: float = 0.95 ) -> bpd.DataFrame: - """Predict the closest cluster for each sample in X. + """Forecast time series at future horizon. + + .. note:: + + Output matches that of the BigQuery ML.FORECAST function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-forecast Args: X (default None): From 5f37b0902fae2c099207acf3ce2e251c09ac889d Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:56:20 -0700 Subject: [PATCH 431/921] docs: add the code samples for metrics{auc, roc_auc_score, roc_curve} (#520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../sklearn/metrics/_ranking.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py index ac919edbe33..dee8b350c0e 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py @@ -27,6 +27,29 @@ def auc(x, y) -> float: way to summarize a precision-recall curve, see :func:`average_precision_score`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> x = bpd.DataFrame([1, 1, 2, 2]) + >>> y = bpd.DataFrame([2, 3, 4, 5]) + >>> auc = bigframes.ml.metrics.auc(x, y) + >>> auc + 3.5 + + The input can be Series: + + >>> df = bpd.DataFrame( + ... {"x": [1, 1, 2, 2], + ... "y": [2, 3, 4, 5],} + ... ) + >>> auc = bigframes.ml.metrics.auc(df["x"], df["y"]) + >>> auc + 3.5 + + Args: x (Series or DataFrame of shape (n_samples,)): X coordinates. These must be either monotonic increasing or monotonic @@ -44,6 +67,28 @@ def roc_auc_score(y_true, y_score) -> float: """Compute Area Under the Receiver Operating Characteristic Curve (ROC AUC) \ from prediction scores. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([0, 0, 1, 1, 0, 1, 0, 1, 1, 1]) + >>> y_score = bpd.DataFrame([0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45]) + >>> roc_auc_score = bigframes.ml.metrics.roc_auc_score(y_true, y_score) + >>> roc_auc_score + 0.625 + + The input can be Series: + + >>> df = bpd.DataFrame( + ... {"y_true": [0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + ... "y_score": [0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45],} + ... ) + >>> roc_auc_score = bigframes.ml.metrics.roc_auc_score(df["y_true"], df["y_score"]) + >>> roc_auc_score + 0.625 + Args: y_true (Series or DataFrame of shape (n_samples,)): True labels or binary label indicators. The binary and multiclass cases @@ -72,6 +117,39 @@ def roc_curve( ): """Compute Receiver operating characteristic (ROC). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([1, 1, 2, 2]) + >>> y_score = bpd.DataFrame([0.1, 0.4, 0.35, 0.8]) + >>> fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve(y_true, y_score, drop_intermediate=False) + >>> fpr + 0 0.0 + 1 0.0 + 2 0.0 + 3 0.0 + 4 0.0 + Name: fpr, dtype: Float64 + + >>> tpr + 0 0.0 + 1 0.333333 + 2 0.5 + 3 0.833333 + 4 1.0 + Name: tpr, dtype: Float64 + + >>> thresholds + 0 inf + 1 0.8 + 2 0.4 + 3 0.35 + 4 0.1 + Name: thresholds, dtype: Float64 + Args: y_true: Series or DataFrame of shape (n_samples,) True binary labels. If labels are not either {-1, 1} or {0, 1}, then From 36920951b1d3bd216b3abc18f7d770ada590a3d0 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 26 Mar 2024 18:18:21 -0700 Subject: [PATCH 432/921] chore: remove unused ColumnTransformer check (#527) The check is no-op and misleading. It is not a chained transformer. But transformer to the same column, which is supported both in BQML and sklearn. --- bigframes/ml/compose.py | 6 ------ tests/system/large/ml/test_compose.py | 26 ++++++++++---------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index d35941b338b..6d4fa5b76d0 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -23,7 +23,6 @@ import bigframes_vendored.sklearn.compose._column_transformer -from bigframes import constants from bigframes.core import log_adapter from bigframes.ml import base, core, globals, preprocessing, utils import bigframes.pandas as bpd @@ -77,7 +76,6 @@ def transformers_( ] ] = [] - column_set: set[str] = set() for entry in self.transformers: name, transformer, column_or_columns = entry columns = ( @@ -87,10 +85,6 @@ def transformers_( ) for column in columns: - if column in column_set: - raise NotImplementedError( - f"Chained transformers on the same column isn't supported. {constants.FEEDBACK_LINK}" - ) result.append((name, transformer, column)) return result diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 6ea4f724898..bb9a4d8f649 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -32,10 +32,15 @@ def test_columntransformer_standalone_fit_and_transform( "species", ), ( - "scale", + "starndard_scale", bigframes.ml.preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "min_max_scale", + bigframes.ml.preprocessing.MinMaxScaler(), + ["culmen_length_mm"], + ), ] ) @@ -51,6 +56,7 @@ def test_columntransformer_standalone_fit_and_transform( expected = pandas.DataFrame( { + "min_max_scaled_culmen_length_mm": [0.269, 0.232, 0.210], "onehotencoded_species": [ [{"index": 1, "value": 1.0}], [{"index": 1, "value": 1.0}], @@ -65,14 +71,8 @@ def test_columntransformer_standalone_fit_and_transform( }, index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), ) - expected.standard_scaled_culmen_length_mm = ( - expected.standard_scaled_culmen_length_mm.astype("Float64") - ) - expected.standard_scaled_flipper_length_mm = ( - expected.standard_scaled_flipper_length_mm.astype("Float64") - ) - pandas.testing.assert_frame_equal(result, expected, rtol=1e-3, check_dtype=False) + pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) def test_columntransformer_standalone_fit_transform(new_penguins_df): @@ -84,7 +84,7 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): "species", ), ( - "scale", + "standard_scale", bigframes.ml.preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), @@ -116,11 +116,5 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): }, index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), ) - expected.standard_scaled_culmen_length_mm = ( - expected.standard_scaled_culmen_length_mm.astype("Float64") - ) - expected.standard_scaled_flipper_length_mm = ( - expected.standard_scaled_flipper_length_mm.astype("Float64") - ) - pandas.testing.assert_frame_equal(result, expected, rtol=1e-3, check_dtype=False) + pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) From 0be891191ed89be77494e4dcda30fb37836842ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 27 Mar 2024 13:35:00 -0500 Subject: [PATCH 433/921] feat: expose `DataFrame.bqclient` to assist in integrations (#519) * feat: expose `DataFrame.session` to assist in integrations * add code sample for sharing when the service account is only known on the backend * expose bqclient instead * start a DF construction sample * finish read sample --- bigframes/dataframe.py | 5 + notebooks/dataframes/dataframe.ipynb | 22 + notebooks/dataframes/integrations.ipynb | 635 ++++++++++++++++++++++++ 3 files changed, 662 insertions(+) create mode 100644 notebooks/dataframes/integrations.ipynb diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 7e82ba125ca..599546284bd 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -306,6 +306,11 @@ def empty(self) -> bool: def values(self) -> numpy.ndarray: return self.to_numpy() + @property + def bqclient(self) -> bigframes.Session: + """BigQuery REST API Client the DataFrame uses for operations.""" + return self._session.bqclient + @property def _session(self) -> bigframes.Session: return self._get_block().expr.session diff --git a/notebooks/dataframes/dataframe.ipynb b/notebooks/dataframes/dataframe.ipynb index 15da0755520..de9bb1d04f4 100644 --- a/notebooks/dataframes/dataframe.ipynb +++ b/notebooks/dataframes/dataframe.ipynb @@ -1,5 +1,27 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "eeec3428", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, { "attachments": {}, "cell_type": "markdown", diff --git a/notebooks/dataframes/integrations.ipynb b/notebooks/dataframes/integrations.ipynb new file mode 100644 index 00000000000..735e18d94e9 --- /dev/null +++ b/notebooks/dataframes/integrations.ipynb @@ -0,0 +1,635 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Integrating with BigQuery DataFrames\n", + "\n", + "This notebook demonstrates operations for building applications that integrate with BigQuery DataFrames. Follow these samples to build an integration that accepts a BigQuery DataFrames object or returns one." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "\n", + "# Sample data\n", + "df = bpd.DataFrame({\n", + " \"index\": [0, 1, 2, 3, 4],\n", + " \"int_col\": [1, 2, 3, 4, 5],\n", + " \"float_col\": [1.0, -0.5, 0.25, -0.125, 0.0625],\n", + " \"string_col\": [\"a\", \"b\", \"c\", \"d\", \"e\"],\n", + "}).set_index(\"index\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Accepting a BigQuery DataFrames (bigframes) DataFrame\n", + "\n", + "The recommended serialization format for a BigQuery DataFrames (bigframes) DataFrame is a BigQuery table. To write a DataFrame to a BigQuery table, use the `DataFrame.to_gbq()` method. With no `destination_table`, BigQuery DataFrames creates a table in the anonymous dataset corresponding to the BigQuery user & location and returns the corresponding table ID." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 00b5c727-f2bf-4265-be22-d7d505619db7 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240327_43bbc4c64fb947f7b69db570a5641506'" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "table_id = df.to_gbq()\n", + "table_id" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sharing the table with your application's backend\n", + "\n", + "Tables created in the user's anonymous dataset are only queryable by the user who created them. Many applications authenticate with a [service account](https://cloud.google.com/iam/docs/service-account-overview), which may be different from the end-user running BigQuery DataFrames (bigframes).\n", + "\n", + "Grant your application access to this table by granting your application's service account associated with the customer the `roles/bigquery.dataViewer` role on the [BigQuery table with an IAM policy](https://cloud.google.com/bigquery/docs/control-access-to-resources-iam#grant_access_to_a_table_or_view)." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job f9c39ac2-a428-45c9-bb3a-643fc62a1c5b is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " index int_col float_col string_col\n", + "0 2 3 0.2500 c\n", + "1 4 5 0.0625 e\n", + "2 0 1 1.0000 a\n", + "3 1 2 -0.5000 b\n", + "4 3 4 -0.1250 d\n" + ] + } + ], + "source": [ + "# This sample assumes the client code knows which service account to share with.\n", + "your_service_account_email = \"your-service-account@bigframes-samples.iam.gserviceaccount.com\"\n", + "\n", + "\n", + "def df_to_gbq_plus_workoad(df):\n", + " table_id = df.to_gbq()\n", + "\n", + " bqclient = df.bqclient\n", + " policy = bqclient.get_iam_policy(table_id)\n", + " binding = {\n", + " \"role\": \"roles/bigquery.dataViewer\",\n", + " \"members\": {f\"serviceAccount:{your_service_account_email}\"},\n", + " }\n", + " policy.bindings.append(binding)\n", + " bqclient.set_iam_policy(table_id, policy)\n", + "\n", + " # TODO(developer): Pass table_id to your application and start your workload.\n", + " example_workload(table_id)\n", + "\n", + "\n", + "def example_workload(table_id):\n", + " # For example, for one node workloads, use the client library to read the table\n", + " # as a pandas DataFrame.\n", + " from google.cloud import bigquery\n", + "\n", + " # This sample assumes this client is authenticated as the user\n", + " # your_service_account_email.\n", + " client = bigquery.Client()\n", + " pandas_df = client.list_rows(table_id).to_dataframe()\n", + " print(pandas_df)\n", + "\n", + "\n", + "df_to_gbq_plus_workoad(df)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job ad53c7f2-e3bd-4667-b60b-b700c24b7a81 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " index int_col float_col string_col\n", + "0 4 5 0.0625 e\n", + "1 0 1 1.0000 a\n", + "2 2 3 0.2500 c\n", + "3 3 4 -0.1250 d\n", + "4 1 2 -0.5000 b\n" + ] + } + ], + "source": [ + "# This sample assumes the client code doesn't know which service account to share with.\n", + "\n", + "\n", + "def df_to_gbq_plus_workoad(df):\n", + " table_id = df.to_gbq()\n", + "\n", + " bqclient = df.bqclient\n", + " token = bqclient._credentials.token\n", + " project_id = bqclient.project\n", + "\n", + " share_table_and_start_workload(table_id, token, project_id)\n", + "\n", + "\n", + "def share_table_and_start_workload(table_id, token, project_id):\n", + " # This code runs in the backend for your application.\n", + " from google.cloud import bigquery\n", + " import google.oauth2.credentials\n", + "\n", + " # Note: these credentials don't have any way to be refreshed,\n", + " # so only use them long enough to share the table with the\n", + " # service account.\n", + " credentials = google.oauth2.credentials.Credentials(token)\n", + " bqclient = bigquery.Client(\n", + " project=project_id,\n", + " credentials=credentials,\n", + " )\n", + "\n", + " # This is assumed to only be available on the backend.\n", + " your_service_account_email = \"your-service-account@bigframes-samples.iam.gserviceaccount.com\"\n", + " policy = bqclient.get_iam_policy(table_id)\n", + " binding = {\n", + " \"role\": \"roles/bigquery.dataViewer\",\n", + " \"members\": {f\"serviceAccount:{your_service_account_email}\"},\n", + " }\n", + " policy.bindings.append(binding)\n", + " bqclient.set_iam_policy(table_id, policy)\n", + "\n", + " # Now that the table has been shared, bqclient with the temporary token\n", + " # is no longer needed.\n", + " example_workload(table_id)\n", + "\n", + "\n", + "def example_workload(table_id):\n", + " # For example, for one node workloads, use the client library to read the table\n", + " # as a pandas DataFrame.\n", + " from google.cloud import bigquery\n", + "\n", + " # This sample assumes this client is authenticated as the user\n", + " # your_service_account_email.\n", + " client = bigquery.Client()\n", + " pandas_df = client.list_rows(table_id).to_dataframe()\n", + " print(pandas_df)\n", + "\n", + "\n", + "df_to_gbq_plus_workoad(df)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Preserving order\n", + "\n", + "Depending on your use case, you may want to include the ordering so that it can be restored withing your application." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 2aa7033c-c547-4ae2-a9aa-33272be82b9c is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240327_b484a3967fba4a41850f4eb21b4b3bd8'" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ordering_column = \"ordering_id_maybe_with_some_random_text_to_avoid_collisions\"\n", + "table_id = df.to_gbq(ordering_id=ordering_column)\n", + "table_id" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating clustered tables\n", + "\n", + "Large tables can be optimized by passing in `clustering_columns` to create a [clustered table](https://cloud.google.com/bigquery/docs/clustered-tables)." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 1d489f94-2840-405e-9114-d439dcfcf7aa is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240327_d00699eeeed743b487c870dca5bcf23b'" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "table_id = df.to_gbq(clustering_columns=(\"index\", \"int_col\"))\n", + "table_id" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Returning a BigQuery DataFrames (bigframes) DataFrame\n", + "\n", + "The recommended way to construct a DataFrame is from a BigQuery table which has a unique primary key. By default a primary key is used as the index, which allows for more efficient queries than the default index generation.\n", + "\n", + "This sample assumes there is a shared dataset that\n", + "\n", + "1. The application can write to and\n", + "2. the bigframes user can read from.\n", + "\n", + "There are many ways an application can [write to a BigQuery table](https://cloud.google.com/bigquery/docs/loading-data), including BigQuery load jobs, DML, streaming REST API, and the BigQuery Write API. Each has different costs, performance, and limitations. Choose the one that best suits your application's needs." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Dataset(DatasetReference('swast-scratch', 'my_dataset'))" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The assumption is that there is a shared dataset to work with.\n", + "from google.cloud import bigquery\n", + "\n", + "bqclient = bigquery.Client()\n", + "bqclient.create_dataset(\"my_dataset\", exists_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 40977e60-97c3-4c93-89e2-d7334e5af71d is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 81e35bb8-2e27-4a18-b596-15a7805331f0 is DONE. 270 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
statepostal_codepop
unique_index
2MI48105669
3GA303092581
5TX787015373
7CO803012087
11MA021422592
13IL606072630
17MI482012
19NC27701801
23CA926121115
29WA980334952
\n", + "

10 rows × 3 columns

\n", + "
[10 rows x 3 columns in total]" + ], + "text/plain": [ + " state postal_code pop\n", + "unique_index \n", + "2 MI 48105 669\n", + "3 GA 30309 2581\n", + "5 TX 78701 5373\n", + "7 CO 80301 2087\n", + "11 MA 02142 2592\n", + "13 IL 60607 2630\n", + "17 MI 48201 2\n", + "19 NC 27701 801\n", + "23 CA 92612 1115\n", + "29 WA 98033 4952\n", + "\n", + "[10 rows x 3 columns]" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# For simplicity, this sample assumes your application uses\n", + "# a load job with the CSV file format.\n", + "# See: https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv#python\n", + "import datetime\n", + "import io\n", + "import random\n", + "\n", + "\n", + "def create_table_for_bigframes():\n", + " # This code is assumed to run on the application's backend.\n", + " from google.cloud import bigquery\n", + "\n", + " client = bigquery.Client()\n", + "\n", + " # The end-user is expected to have read access to this table.\n", + " table_suffix = f\"{datetime.datetime.now().strftime('%Y%m%d_%H%M%S_%f')}_{random.randrange(1_000_000)}\"\n", + " table_id = f\"{client.project}.my_dataset.integrations_ipynb_{table_suffix}\"\n", + "\n", + " # Best practice: set the primary key to a unique column to use as the\n", + " # index and default ordering in a BigQuery DataFrames (bigframes) DataFrame.\n", + " # Having a unique identity column allows the DataFrame to be constructed\n", + " # more efficiently.\n", + " #\n", + " # Note 1: Even a random UUID would be helpful for efficiency.\n", + " #\n", + " # Note 2: Don't do this if you can't guarantee uniqueness, as the BigQuery\n", + " # query engine uses this property to optimize queries. Non-unique primary\n", + " # keys result in undefined behavior.\n", + " #\n", + " # Note 3: client.create_table doesn't support primary key, so instead\n", + " # use DDL to create the table.\n", + " create_table_ddl = f\"\"\"\n", + " CREATE OR REPLACE TABLE `{table_id}`\n", + " (\n", + " unique_index INT64,\n", + " state STRING,\n", + " postal_code STRING,\n", + " pop INT64,\n", + " PRIMARY KEY (unique_index) NOT ENFORCED\n", + " )\n", + " -- Clustering by the index column can make joins and loc operations more efficient.\n", + " -- Also cluster by columns which are expected to be used as common filters.\n", + " CLUSTER BY unique_index, state\n", + " \"\"\"\n", + " client.query_and_wait(create_table_ddl)\n", + "\n", + " csv_file = io.BytesIO(\n", + "b\"\"\"unique_index,state,postal_code,pop\n", + "2,MI,48105,669\n", + "3,GA,30309,2581\n", + "5,TX,78701,5373\n", + "7,CO,80301,2087\n", + "11,MA,02142,2592\n", + "13,IL,60607,2630\n", + "17,MI,48201,2\n", + "19,NC,27701,801\n", + "23,CA,92612,1115\n", + "29,WA,98033,4952\n", + "\"\"\"\n", + " )\n", + " job_config = bigquery.LoadJobConfig(\n", + " skip_leading_rows=1,\n", + " source_format=bigquery.SourceFormat.CSV,\n", + " )\n", + " load_job = client.load_table_from_file(\n", + " csv_file, table_id, job_config=job_config\n", + " )\n", + " load_job.result() # Waits for the job to complete.\n", + "\n", + " return table_id\n", + "\n", + "\n", + "table_id = create_table_for_bigframes()\n", + "\n", + "\n", + "# This is assumed to run on the client.\n", + "import bigframes.pandas as bpd\n", + "df = bpd.read_gbq_table(table_id, index_col=[\"unique_index\"])\n", + "df" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "bigframes", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 893fe154ed0bf4156ff8424b788782eda1ac9d7a Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:24:16 -0700 Subject: [PATCH 434/921] chore: address comments from technical writers (#528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #325274012 🦕 --- bigframes/_config/bigquery_options.py | 46 ++++++++++++++++----------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index d035fe5df1e..9da953a5828 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -56,7 +56,8 @@ def __init__( def application_name(self) -> Optional[str]: """The application name to amend to the user-agent sent to Google APIs. - Recommended format is ``"appplication-name/major.minor.patch_version"`` + The application name to amend to the user agent sent to Google APIs. + The recommended format is ``"appplication-name/major.minor.patch_version"`` or ``"(gpn:PartnerName;)"`` for official Google partners. """ return self._application_name @@ -71,7 +72,7 @@ def application_name(self, value: Optional[str]): @property def credentials(self) -> Optional[google.auth.credentials.Credentials]: - """The OAuth2 Credentials to use for this client.""" + """The OAuth2 credentials to use for this client.""" return self._credentials @credentials.setter @@ -84,7 +85,7 @@ def credentials(self, value: Optional[google.auth.credentials.Credentials]): def location(self) -> Optional[str]: """Default location for job, datasets, and tables. - See: https://cloud.google.com/bigquery/docs/locations + For more information, see https://cloud.google.com/bigquery/docs/locations BigQuery locations. """ return self._location @@ -107,13 +108,15 @@ def project(self, value: Optional[str]): @property def bq_connection(self) -> Optional[str]: - """Name of the BigQuery connection to use. Should be of the form + """Name of the BigQuery connection to use in the form ... - You should either have the connection already created in the - location you have chosen, or you should have the Project IAM - Admin role to enable the service to create the connection for you if you - need it. + You either need to create the connection in a location of your choice, or + you need the Project Admin IAM role to enable the service to create the + connection for you. + + If this option isn't available, or the project or location isn't provided, + then the default connection project/location/connection_id is used in the session. If this option isn't provided, or project or location aren't provided, session will use its default project/location/connection_id as default connection. @@ -151,12 +154,12 @@ def use_regional_endpoints(self) -> bool: """Flag to connect to regional API endpoints. .. deprecated:: 0.13.0 - Use of regional endpoints is a feature in preview and + Use of regional endpoints is a feature in Preview and available only in selected regions and projects. - Requires ``location`` to also be set. For example, set - ``location='asia-northeast1'`` and ``use_regional_endpoints=True`` to - connect to asia-northeast1-bigquery.googleapis.com. + Requires that ``location`` is set. For example, to connect to + asia-northeast1-bigquery.googleapis.com, specify + ``location='asia-northeast1'`` and ``use_regional_endpoints=True``. """ return self._use_regional_endpoints @@ -177,17 +180,22 @@ def use_regional_endpoints(self, value: bool): @property def kms_key_name(self) -> Optional[str]: - """Customer managed encryption key used to control encryption of the + """ + Customer-managed encryption key + used to control encryption of the data at rest in BigQuery. This key + takes the format projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY + + Customer managed encryption key used to control encryption of the data-at-rest in BigQuery. This is of the format projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY - See https://cloud.google.com/bigquery/docs/customer-managed-encryption - for more details. + For more information, see https://cloud.google.com/bigquery/docs/customer-managed-encryption + Customer-managed Cloud KMS keys - Please make sure the project used for Bigquery DataFrames has "Cloud KMS - CryptoKey Encrypter/Decrypter" role in the key's project, See - https://cloud.google.com/bigquery/docs/customer-managed-encryption#assign_role - for steps on how to ensure that. + Make sure the project used for Bigquery DataFrames has the + Cloud KMS CryptoKey Encrypter/Decrypter IAM role in the key's project. + For more information, see https://cloud.google.com/bigquery/docs/customer-managed-encryption#assign_role + Assign the Encrypter/Decrypter. """ return self._kms_key_name From 9b1525a0c359455160bfbc0dc1366e37982ad01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 27 Mar 2024 16:30:36 -0500 Subject: [PATCH 435/921] deps: include `pyarrow` as a dependency (#529) * deps: include `pyarrow` as a dependency Test with a minimum version of pyarrow to confirm compatibility. * fix unit test * fix unit tests * try pyarrow 11 * try pyarrow 8 * skip failing astype test --- noxfile.py | 2 +- setup.py | 1 + testing/constraints-3.10.txt | 14 ++++++++------ testing/constraints-3.9.txt | 1 + tests/system/small/test_series.py | 6 ++++++ 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/noxfile.py b/noxfile.py index 6b369954807..a002c9def7e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -341,8 +341,8 @@ def run_system( pytest_cmd.extend(extra_pytest_options) session.run( *pytest_cmd, - test_folder, *session.posargs, + test_folder, ) diff --git a/setup.py b/setup.py index 768fac530cc..86fb9d496c3 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ "ibis-framework[bigquery] >=8.0.0,<9.0.0dev", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. "pandas >=1.5.0", + "pyarrow >=8.0.0", "pydata-google-auth >=1.8.2", "requests >=2.27.1", "scikit-learn >=1.2.2", diff --git a/testing/constraints-3.10.txt b/testing/constraints-3.10.txt index 9f0786f47e6..5782b03a2f3 100644 --- a/testing/constraints-3.10.txt +++ b/testing/constraints-3.10.txt @@ -1,15 +1,17 @@ # Keep in sync with colab/containers/requirements.core.in image -google-auth==2.17.3 +google-auth==2.27.0 ipykernel==5.5.6 ipython==7.34.0 -notebook==6.4.8 -pandas==1.5.3 -portpicker==1.3.9 -requests==2.27.1 -tornado==6.3.1 +notebook==6.5.5 +pandas==2.0.3 +pandas-stubs==2.0.3.230814 +portpicker==1.5.2 +requests==2.31.0 +tornado==6.3.3 absl-py==1.4.0 debugpy==1.6.6 ipywidgets==7.7.1 matplotlib==3.7.1 psutil==5.9.5 +seaborn==0.13.1 traitlets==5.7.1 diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 0aeb15eab86..1e1f3a3e665 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -12,6 +12,7 @@ google-cloud-resource-manager==1.10.3 google-cloud-storage==2.0.0 ibis-framework==8.0.0 pandas==1.5.0 +pyarrow==8.0.0 pydata-google-auth==1.8.2 requests==2.27.1 scikit-learn==1.2.2 diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 258fb1cfd8f..794ab6b7a29 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2783,6 +2783,12 @@ def test_string_astype_float(): def test_string_astype_date(): + if int(pa.__version__.split(".")[0]) < 15: + pytest.skip( + "Avoid pyarrow.lib.ArrowNotImplementedError: " + "Unsupported cast from string to date32 using function cast_date32." + ) + pd_series = pd.Series(["2014-08-15", "2215-08-15", "2016-02-29"]).astype( pd.ArrowDtype(pa.string()) ) From 56cefff894a1819aab85cc7bdd38469fbc8072bb Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 27 Mar 2024 14:44:16 -0700 Subject: [PATCH 436/921] chore: experimentally double load test timeout (#535) While I work in parallel from other angles, I'd like to try out doubling the timeouts for the kokoro runs, just as another data point. --- .kokoro/load/common.cfg | 2 +- noxfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.kokoro/load/common.cfg b/.kokoro/load/common.cfg index 7f6fa7e0d9b..d86932662d4 100644 --- a/.kokoro/load/common.cfg +++ b/.kokoro/load/common.cfg @@ -8,4 +8,4 @@ action { } build_file: "python-bigquery-dataframes/.kokoro/build.sh" -timeout_mins: 360 +timeout_mins: 720 diff --git a/noxfile.py b/noxfile.py index a002c9def7e..a5e77964f1e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -399,7 +399,7 @@ def load(session: nox.sessions.Session): prefix_name="load", test_folder=os.path.join("tests", "system", "load"), print_duration=True, - timeout_seconds=60 * 60, + timeout_seconds=60 * 60 * 12, ) From 082c58bbe76821b90337dc5af0ab5fa7515682c2 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 28 Mar 2024 07:08:18 -0700 Subject: [PATCH 437/921] fix: don't download 100gb onto local python machine in load test (#537) * fix: don't download 100gb onto local python machine in load test * Update test_large_tables.py --- tests/system/load/test_large_tables.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/system/load/test_large_tables.py b/tests/system/load/test_large_tables.py index 1d4a6b0a5b8..22baa2268fd 100644 --- a/tests/system/load/test_large_tables.py +++ b/tests/system/load/test_large_tables.py @@ -74,13 +74,9 @@ def test_index_repr_large_table(): assert actual is not None -# FAILED -# tests/system/load/test_large_tables.py::test_to_pandas_batches_large_table -# google.api_core.exceptions.Forbidden: 403 Response too large to return. -# Consider specifying a destination table in your job... -@pytest.mark.xfail def test_to_pandas_batches_large_table(): - df = bpd.read_gbq("load_testing.scalars_100gb") + df = bpd.read_gbq("load_testing.scalars_10gb") + # df will be downloaded locally expected_row_count, expected_column_count = df.shape row_count = 0 From ae528d76fa1585f7581e3c774f9554944d25431e Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 28 Mar 2024 18:58:55 +0000 Subject: [PATCH 438/921] test: add e2e tests for all BQ locations (#517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support BQ regional endpoints for europe-west9, europe-west3, us-east4, and us-west1 * add location tests with lep and rep behavior * add default location test * fix mypy failures * Update tests/system/large/test_location.py * make pytest paramxn order deterministic * only unit tests for LEP * remove unit tests on ClientsProvider as it is failing for credentials --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/session/clients.py | 12 +-- tests/config.py | 72 ++++++++++++++++ tests/system/large/test_location.py | 129 ++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 tests/config.py create mode 100644 tests/system/large/test_location.py diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index d97e53901d2..32f13fa00db 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -69,12 +69,12 @@ class ClientsProvider: def __init__( self, - project: Optional[str], - location: Optional[str], - use_regional_endpoints: Optional[bool], - credentials: Optional[google.auth.credentials.Credentials], - application_name: Optional[str], - bq_kms_key_name: Optional[str], + project: Optional[str] = None, + location: Optional[str] = None, + use_regional_endpoints: Optional[bool] = None, + credentials: Optional[google.auth.credentials.Credentials] = None, + application_name: Optional[str] = None, + bq_kms_key_name: Optional[str] = None, ): credentials_project = None if credentials is None: diff --git a/tests/config.py b/tests/config.py new file mode 100644 index 00000000000..a885d7e71df --- /dev/null +++ b/tests/config.py @@ -0,0 +1,72 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://cloud.google.com/bigquery/docs/locations +ALL_BIGQUERY_LOCATIONS = [ + "us-east5", + "us-south1", + "us-central1", + "us-west4", + "us-west2", + "northamerica-northeast1", + "us-east4", + "us-west1", + "us-west3", + "southamerica-east1", + "southamerica-west1", + "us-east1", + "northamerica-northeast2", + "asia-south2", + "asia-east2", + "asia-southeast2", + "australia-southeast2", + "asia-south1", + "asia-northeast2", + "asia-northeast3", + "asia-southeast1", + "australia-southeast1", + "asia-east1", + "asia-northeast1", + "europe-west1", + "europe-west10", + "europe-north1", + "europe-west3", + "europe-west2", + "europe-southwest1", + "europe-west8", + "europe-west4", + "europe-west9", + "europe-west12", + "europe-central2", + "europe-west6", + "me-central2", + "me-central1", + "me-west1", + "me-central2", + "me-central1", + "me-west1", + "africa-south1", +] + +REP_ENABLED_BIGQUERY_LOCATIONS = [ + "me-central2", + "europe-west9", + "europe-west3", + "us-east4", + "us-west1", +] + +LEP_ENABLED_BIGQUERY_LOCATIONS = sorted( + set(ALL_BIGQUERY_LOCATIONS) - set(REP_ENABLED_BIGQUERY_LOCATIONS) +) diff --git a/tests/system/large/test_location.py b/tests/system/large/test_location.py new file mode 100644 index 00000000000..a4cf8919a02 --- /dev/null +++ b/tests/system/large/test_location.py @@ -0,0 +1,129 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + +from google.cloud import bigquery +import pytest + +import bigframes +import bigframes.session.clients +from tests import config + + +def _assert_bq_execution_location(session: bigframes.Session): + df = session.read_gbq( + """ + SELECT "aaa" as name, 111 as number + UNION ALL + SELECT "bbb" as name, 222 as number + UNION ALL + SELECT "aaa" as name, 333 as number + """ + ) + + assert ( + typing.cast(bigquery.QueryJob, df.query_job).location + == session.bqclient.location + ) + + result = ( + df[["name", "number"]] + .groupby("name") + .sum(numeric_only=True) + .sort_values("number", ascending=False) + .head() + ) + + assert ( + typing.cast(bigquery.QueryJob, result.query_job).location + == session.bqclient.location + ) + + +def test_bq_location_default(): + session = bigframes.Session() + + assert session.bqclient.location == "US" + + # by default global endpoint is used + assert ( + session.bqclient._connection.API_BASE_URL == "https://bigquery.googleapis.com" + ) + + # assert that bigframes session honors the location + _assert_bq_execution_location(session) + + +@pytest.mark.parametrize("bigquery_location", config.ALL_BIGQUERY_LOCATIONS) +def test_bq_location(bigquery_location): + session = bigframes.Session( + context=bigframes.BigQueryOptions(location=bigquery_location) + ) + + assert session.bqclient.location == bigquery_location + + # by default global endpoint is used + assert ( + session.bqclient._connection.API_BASE_URL == "https://bigquery.googleapis.com" + ) + + # assert that bigframes session honors the location + _assert_bq_execution_location(session) + + +@pytest.mark.parametrize( + "bigquery_location", + config.REP_ENABLED_BIGQUERY_LOCATIONS, +) +def test_bq_rep_endpoints(bigquery_location): + session = bigframes.Session( + context=bigframes.BigQueryOptions( + location=bigquery_location, use_regional_endpoints=True + ) + ) + + assert session.bqclient.location == bigquery_location + assert ( + session.bqclient._connection.API_BASE_URL + == "https://bigquery.{location}.rep.googleapis.com".format( + location=bigquery_location + ) + ) + + # assert that bigframes session honors the location + _assert_bq_execution_location(session) + + +@pytest.mark.parametrize( + "bigquery_location", + config.LEP_ENABLED_BIGQUERY_LOCATIONS, +) +def test_bq_lep_endpoints(bigquery_location): + # We are not testing BigFrames Session for LEP endpoints because it involves + # query execution using the endpoint, which requires the project to be + # allowlisted for LEP access. We could hardcode one project which is + # allowlisted but then not every open source developer will have access to + # that. Let's rely on just creating the clients for LEP. + clients_provider = bigframes.session.clients.ClientsProvider( + location=bigquery_location, use_regional_endpoints=True + ) + + assert clients_provider.bqclient.location == bigquery_location + assert ( + clients_provider.bqclient._connection.API_BASE_URL + == "https://{location}-bigquery.googleapis.com".format( + location=bigquery_location + ) + ) From ae4ff8d1ef40e0634b842a257ea1043c6d00dccb Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 28 Mar 2024 12:50:57 -0700 Subject: [PATCH 439/921] refactor: Add type constraints to internal op definitions. (#532) --- bigframes/core/nodes.py | 6 + bigframes/dtypes.py | 47 +++- bigframes/operations/__init__.py | 367 +++++++++++++++++++-------- bigframes/operations/type.py | 197 +++++++++++--- tests/system/small/test_dataframe.py | 2 +- 5 files changed, 472 insertions(+), 147 deletions(-) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 5ebd2a5997c..c1ceeebffea 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -318,6 +318,12 @@ def __hash__(self): class ProjectionNode(UnaryNode): assignments: typing.Tuple[typing.Tuple[ex.Expression, str], ...] + def __post_init__(self): + input_types = self.child.schema._mapping + for expression, id in self.assignments: + # throws TypeError if invalid + _ = expression.output_type(input_types) + def __hash__(self): return self._node_hash diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 63adc059f3c..79e1456f317 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -47,13 +47,19 @@ # None represents the type of a None scalar. ExpressionType = typing.Optional[Dtype] -# Used when storing Null expressions -DEFAULT_DTYPE = pd.Float64Dtype() INT_DTYPE = pd.Int64Dtype() FLOAT_DTYPE = pd.Float64Dtype() BOOL_DTYPE = pd.BooleanDtype() STRING_DTYPE = pd.StringDtype(storage="pyarrow") +BYTES_DTYPE = pd.ArrowDtype(pa.binary()) +DATE_DTYPE = pd.ArrowDtype(pa.date32()) +TIME_DTYPE = pd.ArrowDtype(pa.time64("us")) +DATETIME_DTYPE = pd.ArrowDtype(pa.timestamp("us")) +TIMESTAMP_DTYPE = pd.ArrowDtype(pa.timestamp("us", tz="UTC")) + +# Used when storing Null expressions +DEFAULT_DTYPE = FLOAT_DTYPE # On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable UNORDERED_DTYPES = [gpd.array.GeometryDtype()] @@ -100,6 +106,43 @@ pd.ArrowDtype(pa.decimal256(76, 38)), ] + +## dtype predicates - use these to maintain consistency +def is_datetime_like(type: ExpressionType) -> bool: + return type in (DATETIME_DTYPE, TIMESTAMP_DTYPE) + + +def is_date_like(type: ExpressionType) -> bool: + return type in (DATETIME_DTYPE, TIMESTAMP_DTYPE, DATE_DTYPE) + + +def is_time_like(type: ExpressionType) -> bool: + return type in (DATETIME_DTYPE, TIMESTAMP_DTYPE, TIME_DTYPE) + + +def is_binary_like(type: ExpressionType) -> bool: + return type in (BOOL_DTYPE, BYTES_DTYPE, INT_DTYPE) + + +def is_string_like(type: ExpressionType) -> bool: + return type in (STRING_DTYPE, BYTES_DTYPE) + + +def is_array_like(type: ExpressionType) -> bool: + if isinstance(type, pd.ArrowDtype) and isinstance(type.pyarrow_dtype, pa.ListType): + return True + else: + return type in (STRING_DTYPE, BYTES_DTYPE) + + +def is_numeric(type: ExpressionType) -> bool: + return type in NUMERIC_BIGFRAMES_TYPES_PERMISSIVE + + +def is_comparable(type: ExpressionType) -> bool: + return (type is not None) and (type not in UNORDERED_DTYPES) + + # Type hints for Ibis data types that can be read to Python objects by BigQuery DataFrame ReadOnlyIbisDtype = Union[ ibis_dtypes.Binary, diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 2ef71fde7fa..dcd54946266 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -161,102 +161,165 @@ def _convert_expr_input( # Operation Factories -def create_unary_op( - name: str, type_rule: op_typing.OpTypeRule = op_typing.INPUT_TYPE -) -> UnaryOp: +def create_unary_op(name: str, type_signature: op_typing.UnaryTypeSignature) -> UnaryOp: return dataclasses.make_dataclass( name, - [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_rule.as_method)], # type: ignore + [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method)], # type: ignore bases=(UnaryOp,), frozen=True, )() def create_binary_op( - name: str, type_rule: op_typing.OpTypeRule = op_typing.Supertype() + name: str, type_signature: op_typing.BinaryTypeSignature ) -> BinaryOp: return dataclasses.make_dataclass( name, - [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_rule.as_method)], # type: ignore + [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method)], # type: ignore bases=(BinaryOp,), frozen=True, )() -def create_ternary_op( - name: str, type_rule: op_typing.OpTypeRule = op_typing.Supertype() -) -> TernaryOp: - return dataclasses.make_dataclass( - name, - [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_rule.as_method)], # type: ignore - bases=(TernaryOp,), - frozen=True, - )() - - # Unary Ops ## Generic Ops -invert_op = create_unary_op(name="invert", type_rule=op_typing.INPUT_TYPE) -isnull_op = create_unary_op(name="isnull", type_rule=op_typing.PREDICATE) -notnull_op = create_unary_op(name="notnull", type_rule=op_typing.PREDICATE) -hash_op = create_unary_op(name="hash", type_rule=op_typing.INTEGER) +invert_op = create_unary_op( + name="invert", + type_signature=op_typing.TypePreserving( + dtypes.is_binary_like, + description="binary-like", + ), +) # numeric +isnull_op = create_unary_op( + name="isnull", + type_signature=op_typing.FixedOutputType( + lambda x: True, dtypes.BOOL_DTYPE, description="nullable" + ), +) +notnull_op = create_unary_op( + name="notnull", + type_signature=op_typing.FixedOutputType( + lambda x: True, dtypes.BOOL_DTYPE, description="nullable" + ), +) +hash_op = create_unary_op( + name="hash", + type_signature=op_typing.FixedOutputType( + dtypes.is_string_like, dtypes.INT_DTYPE, description="string-like" + ), +) ## String Ops -len_op = create_unary_op(name="len", type_rule=op_typing.INTEGER) -reverse_op = create_unary_op(name="reverse", type_rule=op_typing.STRING) -lower_op = create_unary_op(name="lower", type_rule=op_typing.STRING) -upper_op = create_unary_op(name="upper", type_rule=op_typing.STRING) -strip_op = create_unary_op(name="strip", type_rule=op_typing.STRING) -isalnum_op = create_unary_op(name="isalnum", type_rule=op_typing.PREDICATE) -isalpha_op = create_unary_op(name="isalpha", type_rule=op_typing.PREDICATE) -isdecimal_op = create_unary_op(name="isdecimal", type_rule=op_typing.PREDICATE) -isdigit_op = create_unary_op(name="isdigit", type_rule=op_typing.PREDICATE) -isnumeric_op = create_unary_op(name="isnumeric", type_rule=op_typing.PREDICATE) -isspace_op = create_unary_op(name="isspace", type_rule=op_typing.PREDICATE) -islower_op = create_unary_op(name="islower", type_rule=op_typing.PREDICATE) -isupper_op = create_unary_op(name="isupper", type_rule=op_typing.PREDICATE) -rstrip_op = create_unary_op(name="rstrip", type_rule=op_typing.STRING) -lstrip_op = create_unary_op(name="lstrip", type_rule=op_typing.STRING) -capitalize_op = create_unary_op(name="capitalize", type_rule=op_typing.STRING) +len_op = create_unary_op( + name="len", + type_signature=op_typing.FixedOutputType( + dtypes.is_array_like, dtypes.INT_DTYPE, description="array-like" + ), +) +reverse_op = create_unary_op(name="reverse", type_signature=op_typing.STRING_TRANSFORM) +lower_op = create_unary_op(name="lower", type_signature=op_typing.STRING_TRANSFORM) +upper_op = create_unary_op(name="upper", type_signature=op_typing.STRING_TRANSFORM) +strip_op = create_unary_op(name="strip", type_signature=op_typing.STRING_TRANSFORM) +isalnum_op = create_unary_op(name="isalnum", type_signature=op_typing.STRING_PREDICATE) +isalpha_op = create_unary_op(name="isalpha", type_signature=op_typing.STRING_PREDICATE) +isdecimal_op = create_unary_op( + name="isdecimal", type_signature=op_typing.STRING_PREDICATE +) +isdigit_op = create_unary_op(name="isdigit", type_signature=op_typing.STRING_PREDICATE) +isnumeric_op = create_unary_op( + name="isnumeric", type_signature=op_typing.STRING_PREDICATE +) +isspace_op = create_unary_op(name="isspace", type_signature=op_typing.STRING_PREDICATE) +islower_op = create_unary_op(name="islower", type_signature=op_typing.STRING_PREDICATE) +isupper_op = create_unary_op(name="isupper", type_signature=op_typing.STRING_PREDICATE) +rstrip_op = create_unary_op(name="rstrip", type_signature=op_typing.STRING_TRANSFORM) +lstrip_op = create_unary_op(name="lstrip", type_signature=op_typing.STRING_TRANSFORM) +capitalize_op = create_unary_op( + name="capitalize", type_signature=op_typing.STRING_TRANSFORM +) ## DateTime Ops -day_op = create_unary_op(name="day", type_rule=op_typing.INTEGER) -dayofweek_op = create_unary_op(name="dayofweek", type_rule=op_typing.INTEGER) +### datelike accessors +day_op = create_unary_op( + name="day", + type_signature=op_typing.DATELIKE_ACCESSOR, +) +month_op = create_unary_op( + name="month", + type_signature=op_typing.DATELIKE_ACCESSOR, +) +year_op = create_unary_op( + name="year", + type_signature=op_typing.DATELIKE_ACCESSOR, +) +dayofweek_op = create_unary_op( + name="dayofweek", + type_signature=op_typing.DATELIKE_ACCESSOR, +) +quarter_op = create_unary_op( + name="quarter", + type_signature=op_typing.DATELIKE_ACCESSOR, +) +### timelike accessors +hour_op = create_unary_op( + name="hour", + type_signature=op_typing.TIMELIKE_ACCESSOR, +) +minute_op = create_unary_op( + name="minute", + type_signature=op_typing.TIMELIKE_ACCESSOR, +) +second_op = create_unary_op( + name="second", + type_signature=op_typing.TIMELIKE_ACCESSOR, +) +normalize_op = create_unary_op( + name="normalize", + type_signature=op_typing.TypePreserving( + dtypes.is_time_like, + description="time-like", + ), +) +### datetimelike accessors date_op = create_unary_op( - name="date", type_rule=op_typing.Fixed(pd.ArrowDtype(pa.date32())) + name="date", + type_signature=op_typing.FixedOutputType( + dtypes.is_date_like, dtypes.DATE_DTYPE, description="date-like" + ), ) -hour_op = create_unary_op(name="hour", type_rule=op_typing.INTEGER) -minute_op = create_unary_op(name="minute", type_rule=op_typing.INTEGER) -month_op = create_unary_op(name="month", type_rule=op_typing.INTEGER) -quarter_op = create_unary_op(name="quarter", type_rule=op_typing.INTEGER) -second_op = create_unary_op(name="second", type_rule=op_typing.INTEGER) time_op = create_unary_op( - name="time", type_rule=op_typing.Fixed(pd.ArrowDtype(pa.time64("us"))) + name="time", + type_signature=op_typing.FixedOutputType( + dtypes.is_time_like, dtypes.TIME_DTYPE, description="time-like" + ), ) -year_op = create_unary_op(name="year", type_rule=op_typing.INTEGER) -normalize_op = create_unary_op(name="normalize") ## Trigonometry Ops -sin_op = create_unary_op(name="sin", type_rule=op_typing.REAL_NUMERIC) -cos_op = create_unary_op(name="cos", type_rule=op_typing.REAL_NUMERIC) -tan_op = create_unary_op(name="tan", type_rule=op_typing.REAL_NUMERIC) -arcsin_op = create_unary_op(name="arcsin", type_rule=op_typing.REAL_NUMERIC) -arccos_op = create_unary_op(name="arccos", type_rule=op_typing.REAL_NUMERIC) -arctan_op = create_unary_op(name="arctan", type_rule=op_typing.REAL_NUMERIC) -sinh_op = create_unary_op(name="sinh", type_rule=op_typing.REAL_NUMERIC) -cosh_op = create_unary_op(name="cosh", type_rule=op_typing.REAL_NUMERIC) -tanh_op = create_unary_op(name="tanh", type_rule=op_typing.REAL_NUMERIC) -arcsinh_op = create_unary_op(name="arcsinh", type_rule=op_typing.REAL_NUMERIC) -arccosh_op = create_unary_op(name="arccosh", type_rule=op_typing.REAL_NUMERIC) -arctanh_op = create_unary_op(name="arctanh", type_rule=op_typing.REAL_NUMERIC) -arctan2_op = create_binary_op(name="arctan2", type_rule=op_typing.REAL_NUMERIC) +sin_op = create_unary_op(name="sin", type_signature=op_typing.UNARY_REAL_NUMERIC) +cos_op = create_unary_op(name="cos", type_signature=op_typing.UNARY_REAL_NUMERIC) +tan_op = create_unary_op(name="tan", type_signature=op_typing.UNARY_REAL_NUMERIC) +arcsin_op = create_unary_op(name="arcsin", type_signature=op_typing.UNARY_REAL_NUMERIC) +arccos_op = create_unary_op(name="arccos", type_signature=op_typing.UNARY_REAL_NUMERIC) +arctan_op = create_unary_op(name="arctan", type_signature=op_typing.UNARY_REAL_NUMERIC) +sinh_op = create_unary_op(name="sinh", type_signature=op_typing.UNARY_REAL_NUMERIC) +cosh_op = create_unary_op(name="cosh", type_signature=op_typing.UNARY_REAL_NUMERIC) +tanh_op = create_unary_op(name="tanh", type_signature=op_typing.UNARY_REAL_NUMERIC) +arcsinh_op = create_unary_op( + name="arcsinh", type_signature=op_typing.UNARY_REAL_NUMERIC +) +arccosh_op = create_unary_op( + name="arccosh", type_signature=op_typing.UNARY_REAL_NUMERIC +) +arctanh_op = create_unary_op( + name="arctanh", type_signature=op_typing.UNARY_REAL_NUMERIC +) ## Numeric Ops -floor_op = create_unary_op(name="floor", type_rule=op_typing.REAL_NUMERIC) -ceil_op = create_unary_op(name="ceil", type_rule=op_typing.REAL_NUMERIC) -abs_op = create_unary_op(name="abs", type_rule=op_typing.INPUT_TYPE) -exp_op = create_unary_op(name="exp", type_rule=op_typing.REAL_NUMERIC) -expm1_op = create_unary_op(name="expm1", type_rule=op_typing.REAL_NUMERIC) -ln_op = create_unary_op(name="log", type_rule=op_typing.REAL_NUMERIC) -log10_op = create_unary_op(name="log10", type_rule=op_typing.REAL_NUMERIC) -log1p_op = create_unary_op(name="log1p", type_rule=op_typing.REAL_NUMERIC) -sqrt_op = create_unary_op(name="sqrt", type_rule=op_typing.REAL_NUMERIC) +floor_op = create_unary_op(name="floor", type_signature=op_typing.UNARY_REAL_NUMERIC) +ceil_op = create_unary_op(name="ceil", type_signature=op_typing.UNARY_REAL_NUMERIC) +abs_op = create_unary_op(name="abs", type_signature=op_typing.UNARY_NUMERIC) +exp_op = create_unary_op(name="exp", type_signature=op_typing.UNARY_REAL_NUMERIC) +expm1_op = create_unary_op(name="expm1", type_signature=op_typing.UNARY_REAL_NUMERIC) +ln_op = create_unary_op(name="log", type_signature=op_typing.UNARY_REAL_NUMERIC) +log10_op = create_unary_op(name="log10", type_signature=op_typing.UNARY_REAL_NUMERIC) +log1p_op = create_unary_op(name="log1p", type_signature=op_typing.UNARY_REAL_NUMERIC) +sqrt_op = create_unary_op(name="sqrt", type_signature=op_typing.UNARY_REAL_NUMERIC) # Parameterized unary ops @@ -266,7 +329,7 @@ class StrContainsOp(UnaryOp): pat: str def output_type(self, *input_types): - return dtypes.BOOL_DTYPE + return op_typing.STRING_PREDICATE.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -275,7 +338,7 @@ class StrContainsRegexOp(UnaryOp): pat: str def output_type(self, *input_types): - return dtypes.BOOL_DTYPE + return op_typing.STRING_PREDICATE.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -284,7 +347,7 @@ class StrGetOp(UnaryOp): i: int def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -295,7 +358,7 @@ class StrPadOp(UnaryOp): side: typing.Literal["both", "left", "right"] def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -305,7 +368,7 @@ class ReplaceStrOp(UnaryOp): repl: str def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -315,7 +378,7 @@ class RegexReplaceStrOp(UnaryOp): repl: str def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -324,7 +387,7 @@ class StartsWithOp(UnaryOp): pat: typing.Sequence[str] def output_type(self, *input_types): - return dtypes.BOOL_DTYPE + return op_typing.STRING_PREDICATE.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -333,7 +396,7 @@ class EndsWithOp(UnaryOp): pat: typing.Sequence[str] def output_type(self, *input_types): - return dtypes.BOOL_DTYPE + return op_typing.STRING_PREDICATE.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -342,7 +405,7 @@ class ZfillOp(UnaryOp): width: int def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -353,7 +416,10 @@ class StrFindOp(UnaryOp): end: typing.Optional[int] def output_type(self, *input_types): - return dtypes.INT_DTYPE + signature = op_typing.FixedOutputType( + dtypes.is_string_like, dtypes.INT_DTYPE, "string-like" + ) + return signature.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -363,7 +429,7 @@ class StrExtractOp(UnaryOp): n: int = 1 def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -373,7 +439,7 @@ class StrSliceOp(UnaryOp): end: typing.Optional[int] def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -382,7 +448,7 @@ class StrRepeatOp(UnaryOp): repeats: int def output_type(self, *input_types): - return dtypes.STRING_DTYPE + return op_typing.STRING_TRANSFORM.output_type(input_types[0]) # Other parameterized unary operations @@ -392,9 +458,15 @@ class StructFieldOp(UnaryOp): name_or_index: str | int def output_type(self, *input_types): - pd_type = typing.cast(pd.ArrowDtype, input_types[0]) - pa_struct_t = typing.cast(pa.StructType, pd_type.pyarrow_dtype) - pa_result_type = pa_struct_t[self.name_or_index].type + input_type = input_types[0] + if not isinstance(input_type, pd.ArrowDtype): + raise TypeError("field accessor input must be a struct type") + + pa_type = input_type.pyarrow_dtype + if not isinstance(pa_type, pa.StructType): + raise TypeError("field accessor input must be a struct type") + + pa_result_type = pa_type[self.name_or_index].type # TODO: Directly convert from arrow to pandas type ibis_result_type = dtypes.arrow_dtype_to_ibis_dtype(pa_result_type) return dtypes.ibis_dtype_to_bigframes_dtype(ibis_result_type) @@ -476,37 +548,100 @@ def output_type(self, *input_types): # Binary Ops -fillna_op = create_binary_op(name="fillna") -cliplower_op = create_binary_op(name="clip_lower") -clipupper_op = create_binary_op(name="clip_upper") -coalesce_op = create_binary_op(name="coalesce") +fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COMMON_SUPERTYPE) +cliplower_op = create_binary_op( + name="clip_lower", type_signature=op_typing.COMMON_SUPERTYPE +) +clipupper_op = create_binary_op( + name="clip_upper", type_signature=op_typing.COMMON_SUPERTYPE +) +coalesce_op = create_binary_op( + name="coalesce", type_signature=op_typing.COMMON_SUPERTYPE +) + + ## Math Ops -add_op = create_binary_op(name="add", type_rule=op_typing.NUMERIC) -sub_op = create_binary_op(name="sub", type_rule=op_typing.NUMERIC) -mul_op = create_binary_op(name="mul", type_rule=op_typing.NUMERIC) -div_op = create_binary_op(name="div", type_rule=op_typing.REAL_NUMERIC) -floordiv_op = create_binary_op(name="floordiv", type_rule=op_typing.NUMERIC) -pow_op = create_binary_op(name="pow", type_rule=op_typing.NUMERIC) -mod_op = create_binary_op(name="mod", type_rule=op_typing.NUMERIC) -round_op = create_binary_op(name="round", type_rule=op_typing.REAL_NUMERIC) -unsafe_pow_op = create_binary_op(name="unsafe_pow_op", type_rule=op_typing.REAL_NUMERIC) +@dataclasses.dataclass(frozen=True) +class AddOp(BinaryOp): + name: typing.ClassVar[str] = "add" + + def output_type(self, *input_types): + left_type = input_types[0] + right_type = input_types[1] + if all(map(dtypes.is_string_like, input_types)) and len(set(input_types)) == 1: + # String addition + return input_types[0] + if (left_type is None or dtypes.is_numeric(left_type)) and ( + right_type is None or dtypes.is_numeric(right_type) + ): + # Numeric addition + return dtypes.lcd_etype(left_type, right_type) + # TODO: Add temporal addition once delta types supported + raise TypeError(f"Cannot add dtypes {left_type} and {right_type}") + + +@dataclasses.dataclass(frozen=True) +class SubOp(BinaryOp): + name: typing.ClassVar[str] = "sub" + + # Note: this is actualyl a vararg op, but we don't model that yet + def output_type(self, *input_types): + left_type = input_types[0] + right_type = input_types[1] + if (left_type is None or dtypes.is_numeric(left_type)) and ( + right_type is None or dtypes.is_numeric(right_type) + ): + # Numeric subtraction + return dtypes.lcd_etype(left_type, right_type) + # TODO: Add temporal addition once delta types supported + raise TypeError(f"Cannot subtract dtypes {left_type} and {right_type}") + + +add_op = AddOp() +sub_op = SubOp() +mul_op = create_binary_op(name="mul", type_signature=op_typing.BINARY_NUMERIC) +div_op = create_binary_op(name="div", type_signature=op_typing.BINARY_REAL_NUMERIC) +floordiv_op = create_binary_op(name="floordiv", type_signature=op_typing.BINARY_NUMERIC) +pow_op = create_binary_op(name="pow", type_signature=op_typing.BINARY_NUMERIC) +mod_op = create_binary_op(name="mod", type_signature=op_typing.BINARY_NUMERIC) +arctan2_op = create_binary_op( + name="arctan2", type_signature=op_typing.BINARY_REAL_NUMERIC +) +round_op = create_binary_op(name="round", type_signature=op_typing.BINARY_REAL_NUMERIC) +unsafe_pow_op = create_binary_op( + name="unsafe_pow_op", type_signature=op_typing.BINARY_REAL_NUMERIC +) # Logical Ops -and_op = create_binary_op(name="and") -or_op = create_binary_op(name="or") +and_op = create_binary_op(name="and", type_signature=op_typing.LOGICAL) +or_op = create_binary_op(name="or", type_signature=op_typing.LOGICAL) ## Comparison Ops -eq_op = create_binary_op(name="eq", type_rule=op_typing.PREDICATE) +eq_op = create_binary_op(name="eq", type_signature=op_typing.COMPARISON) eq_null_match_op = create_binary_op( - name="eq_nulls_match", type_rule=op_typing.PREDICATE + name="eq_nulls_match", type_signature=op_typing.COMPARISON ) -ne_op = create_binary_op(name="ne", type_rule=op_typing.PREDICATE) -lt_op = create_binary_op(name="lt", type_rule=op_typing.PREDICATE) -gt_op = create_binary_op(name="gt", type_rule=op_typing.PREDICATE) -le_op = create_binary_op(name="le", type_rule=op_typing.PREDICATE) -ge_op = create_binary_op(name="ge", type_rule=op_typing.PREDICATE) +ne_op = create_binary_op(name="ne", type_signature=op_typing.COMPARISON) +lt_op = create_binary_op(name="lt", type_signature=op_typing.COMPARISON) +gt_op = create_binary_op(name="gt", type_signature=op_typing.COMPARISON) +le_op = create_binary_op(name="le", type_signature=op_typing.COMPARISON) +ge_op = create_binary_op(name="ge", type_signature=op_typing.COMPARISON) + ## String Ops -strconcat_op = create_binary_op(name="strconcat", type_rule=op_typing.STRING) +@dataclasses.dataclass(frozen=True) +class StrConcatOp(BinaryOp): + name: typing.ClassVar[str] = "str_concat" + + # Note: this is actualyl a vararg op, but we don't model that yet + def output_type(self, *input_types): + if not all(map(dtypes.is_string_like, input_types)): + raise TypeError("string concat requires string-like arguments") + if len(set(input_types)) != 1: + raise TypeError("string concat requires like-typed arguments") + return input_types[0] + + +strconcat_op = StrConcatOp() # Ternary Ops @@ -515,15 +650,25 @@ class WhereOp(TernaryOp): name: typing.ClassVar[str] = "where" def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - # Second input is boolean and doesn't affect output type + if input_types[1] != dtypes.BOOL_DTYPE: + raise TypeError("where condition must be a boolean") return dtypes.lcd_etype(input_types[0], input_types[2]) where_op = WhereOp() -clip_op = create_ternary_op(name="clip", type_rule=op_typing.Supertype()) +@dataclasses.dataclass(frozen=True) +class ClipOp(TernaryOp): + name: typing.ClassVar[str] = "clip" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return dtypes.lcd_etype( + input_types[0], dtypes.lcd_etype(input_types[1], input_types[2]) + ) + +clip_op = ClipOp() # Just parameterless unary ops for now # TODO: Parameter mappings diff --git a/bigframes/operations/type.py b/bigframes/operations/type.py index 30e0c1e7458..a1dc8edffc7 100644 --- a/bigframes/operations/type.py +++ b/bigframes/operations/type.py @@ -12,64 +12,195 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc import dataclasses -import functools +from typing import Callable import bigframes.dtypes from bigframes.dtypes import ExpressionType -# TODO: Apply input type constraints to help pre-empt invalid expression construction - @dataclasses.dataclass -class OpTypeRule: - def output_type(self, *input_types: ExpressionType) -> ExpressionType: - raise NotImplementedError("Abstract typing rule has no output type") +class TypeSignature(abc.ABC): + """ + Type Signature represent a mapping from input types to output type. + + Type signatures should throw a TypeError if the input types cannot be handled by the operation. + """ + + @property + @abc.abstractmethod + def as_method(self): + """Convert the signature into an object method. Convenience function for constructing ops that use the signature.""" + ... + + +class UnaryTypeSignature(TypeSignature): + @abc.abstractmethod + def output_type(self, input_type: ExpressionType) -> ExpressionType: + ... + + @property + def as_method(self): + def meth(_, *input_types: ExpressionType) -> ExpressionType: + assert len(input_types) == 1 + return self.output_type(input_types[0]) + + return meth + + +class BinaryTypeSignature(TypeSignature): + @abc.abstractmethod + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + ... @property def as_method(self): def meth(_, *input_types: ExpressionType) -> ExpressionType: - return self.output_type(*input_types) + assert len(input_types) == 2 + return self.output_type(input_types[0], input_types[1]) return meth @dataclasses.dataclass -class InputType(OpTypeRule): - def output_type(self, *input_types: ExpressionType) -> ExpressionType: - assert len(input_types) == 1 - return input_types[0] +class TypePreserving(UnaryTypeSignature): + type_predicate: Callable[[ExpressionType], bool] + description: str + + def output_type(self, input_type: ExpressionType) -> ExpressionType: + if not self.type_predicate(input_type): + raise TypeError( + f"Type {input_type} is not supported. Type must be {self.description}" + ) + return input_type + + +@dataclasses.dataclass +class FixedOutputType(UnaryTypeSignature): + type_predicate: Callable[[ExpressionType], bool] + fixed_type: ExpressionType + description: str + + def output_type(self, input_type: ExpressionType) -> ExpressionType: + if (input_type is not None) and not self.type_predicate(input_type): + raise TypeError( + f"Type {input_type} is not supported. Type must be {self.description}" + ) + return self.fixed_type @dataclasses.dataclass -class RealNumeric(OpTypeRule): - def output_type(self, *input_types: ExpressionType) -> ExpressionType: - return functools.reduce( - lambda t1, t2: bigframes.dtypes.lcd_etype(t1, t2), - [*input_types, bigframes.dtypes.FLOAT_DTYPE], - ) +class UnaryRealNumeric(UnaryTypeSignature): + """Type signature for real-valued functions like exp, log, sin, tan.""" + + def output_type(self, type: ExpressionType) -> ExpressionType: + if type is None: + return bigframes.dtypes.FLOAT_DTYPE + if not bigframes.dtypes.is_numeric(type): + raise TypeError(f"Type {type} is not numeric") + if type in (bigframes.dtypes.INT_DTYPE, bigframes.dtypes.BOOL_DTYPE): + # Real numeric ops produce floats on int input + return bigframes.dtypes.FLOAT_DTYPE + return type @dataclasses.dataclass -class Supertype(OpTypeRule): - def output_type(self, *input_types: ExpressionType) -> ExpressionType: - return functools.reduce( - lambda t1, t2: bigframes.dtypes.lcd_etype(t1, t2), input_types - ) +class BinaryNumeric(BinaryTypeSignature): + """Type signature for numeric functions like multiply, modulo that can map ints to ints.""" + + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + if (left_type is not None) and not bigframes.dtypes.is_numeric(left_type): + raise TypeError(f"Type {left_type} is not numeric") + if (right_type is not None) and not bigframes.dtypes.is_numeric(right_type): + raise TypeError(f"Type {right_type} is not numeric") + return bigframes.dtypes.lcd_etype(left_type, right_type) @dataclasses.dataclass -class Fixed(OpTypeRule): - out_type: ExpressionType +class BinaryRealNumeric(BinaryTypeSignature): + """Type signature for real-valued functions like divide, arctan2, pow.""" + + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + if (left_type is not None) and not bigframes.dtypes.is_numeric(left_type): + raise TypeError(f"Type {left_type} is not numeric") + if (right_type is not None) and not bigframes.dtypes.is_numeric(right_type): + raise TypeError(f"Type {right_type} is not numeric") + lcd_type = bigframes.dtypes.lcd_etype(left_type, right_type) + if lcd_type == bigframes.dtypes.INT_DTYPE: + # Real numeric ops produce floats on int input + return bigframes.dtypes.FLOAT_DTYPE + return lcd_type - def output_type(self, *input_types: ExpressionType) -> ExpressionType: - return self.out_type +@dataclasses.dataclass +class Supertype(BinaryTypeSignature): + """Type signature for functions that return a the supertype of its inputs. Currently BigFrames just supports upcasting numerics.""" -# Common type rules -NUMERIC = Supertype() -REAL_NUMERIC = RealNumeric() -PREDICATE = Fixed(bigframes.dtypes.BOOL_DTYPE) -INTEGER = Fixed(bigframes.dtypes.INT_DTYPE) -STRING = Fixed(bigframes.dtypes.STRING_DTYPE) -INPUT_TYPE = InputType() + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + return bigframes.dtypes.lcd_etype(left_type, right_type) + + +@dataclasses.dataclass +class Comparison(BinaryTypeSignature): + """Type signature for comparison operators.""" + + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + common_type = bigframes.dtypes.lcd_etype(left_type, right_type) + if not bigframes.dtypes.is_comparable(common_type): + raise TypeError(f"Types {left_type} and {right_type} are not comparable") + return bigframes.dtypes.BOOL_DTYPE + + +@dataclasses.dataclass +class Logical(BinaryTypeSignature): + """Type signature for logical operators like AND, OR and NOT.""" + + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + if left_type is None or right_type is None: + return bigframes.dtypes.BOOL_DTYPE + if not bigframes.dtypes.is_binary_like(left_type): + raise TypeError(f"Type {left_type} is not binary") + if not bigframes.dtypes.is_binary_like(right_type): + raise TypeError(f"Type {right_type} is not binary") + if left_type != right_type: + raise TypeError( + "Bitwise operands {left_type} and {right_type} do not match" + ) + return left_type + + +# Common type signatures +UNARY_NUMERIC = TypePreserving(bigframes.dtypes.is_numeric, description="numeric") +UNARY_REAL_NUMERIC = UnaryRealNumeric() +BINARY_NUMERIC = BinaryNumeric() +BINARY_REAL_NUMERIC = BinaryRealNumeric() +COMPARISON = Comparison() +COMMON_SUPERTYPE = Supertype() +LOGICAL = Logical() +STRING_TRANSFORM = TypePreserving( + bigframes.dtypes.is_string_like, description="numeric" +) +STRING_PREDICATE = FixedOutputType( + bigframes.dtypes.is_string_like, + bigframes.dtypes.BOOL_DTYPE, + description="string-like", +) +DATELIKE_ACCESSOR = FixedOutputType( + bigframes.dtypes.is_date_like, bigframes.dtypes.INT_DTYPE, description="date-like" +) +TIMELIKE_ACCESSOR = FixedOutputType( + bigframes.dtypes.is_time_like, bigframes.dtypes.INT_DTYPE, description="time-like" +) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e58a6667096..355849538e4 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2003,7 +2003,7 @@ def test_mod(scalars_dfs, other_scalar): def test_scalar_binop_str_exception(scalars_dfs): scalars_df, _ = scalars_dfs columns = ["string_col"] - with pytest.raises(Exception): + with pytest.raises(TypeError, match="Cannot add dtypes"): (scalars_df[columns] + 1).to_pandas() From a0fb8bbfddd07f1e0ef03eeb4be653d1e9f06772 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:49:22 -0700 Subject: [PATCH 440/921] docs: Migrate the overview page to Bigframes official landing page (#536) --- README.rst | 386 +---------------------------------------------------- 1 file changed, 5 insertions(+), 381 deletions(-) diff --git a/README.rst b/README.rst index 73709641def..26bbbffa886 100644 --- a/README.rst +++ b/README.rst @@ -10,395 +10,19 @@ powered by the BigQuery engine. BigQuery DataFrames is an open-source package. You can run ``pip install --upgrade bigframes`` to install the latest version. + Documentation ------------- * `BigQuery DataFrames source code (GitHub) `_ * `BigQuery DataFrames sample notebooks `_ * `BigQuery DataFrames API reference `_ -* `BigQuery documentation `_ - - -Quickstart ----------- - -Prerequisites -^^^^^^^^^^^^^ - -* Install the ``bigframes`` package. -* Create a Google Cloud project and billing account. -* In an interactive environment (like Notebook, Python REPL or command line), - ``bigframes`` will do the authentication on-the-fly if needed. Otherwise, see - `how to set up application default credentials `_ - for various environments. For example, to pre-authenticate on your laptop you can - `install and initialize the gcloud CLI `_, - and then generate the application default credentials by doing - `gcloud auth application-default login `_. -* The user must have - `BigQuery Job User `_ and - `BigQuery Read Session User `_ - roles for the minimum usage. Additional IAM requirements apply for using remote - functions and ML. - -Code sample -^^^^^^^^^^^ - -Import ``bigframes.pandas`` for a pandas-like interface. The ``read_gbq`` -method accepts either a fully-qualified table ID or a SQL query. - -.. code-block:: python - - import bigframes.pandas as bpd - - bpd.options.bigquery.project = your_gcp_project_id - df1 = bpd.read_gbq("project.dataset.table") - df2 = bpd.read_gbq("SELECT a, b, c, FROM `project.dataset.table`") - -* `More code samples `_ - - -Locations ---------- -BigQuery DataFrames uses a -`BigQuery session `_ -internally to manage metadata on the service side. This session is tied to a -`location `_ . -BigQuery DataFrames uses the US multi-region as the default location, but you -can use ``session_options.location`` to set a different location. Every query -in a session is executed in the location where the session was created. -BigQuery DataFrames -auto-populates ``bf.options.bigquery.location`` if the user starts with -``read_gbq/read_gbq_table/read_gbq_query()`` and specifies a table, either -directly or in a SQL statement. - -If you want to reset the location of the created DataFrame or Series objects, -you can close the session by executing ``bigframes.pandas.close_session()``. -After that, you can reuse ``bigframes.pandas.options.bigquery.location`` to -specify another location. - - -``read_gbq()`` requires you to specify a location if the dataset you are -querying is not in the US multi-region. If you try to read a table from another -location, you get a NotFound exception. - -Project -------- -If ``bf.options.bigquery.project`` is not set, the ``$GOOGLE_CLOUD_PROJECT`` -environment variable is used, which is set in the notebook runtime serving the -BigQuery Studio/Vertex Notebooks. - -ML Capabilities ---------------- - -The ML capabilities in BigQuery DataFrames let you preprocess data, and -then train models on that data. You can also chain these actions together to -create data pipelines. - -Preprocess data -^^^^^^^^^^^^^^^^^^^^^^^^ - -Create transformers to prepare data for use in estimators (models) by -using the -`bigframes.ml.preprocessing module `_ -and the `bigframes.ml.compose module `_. -BigQuery DataFrames offers the following transformations: - -* Use the `KBinsDiscretizer class `_ - in the ``bigframes.ml.preprocessing`` module to bin continuous data into intervals. -* Use the `LabelEncoder class `_ - in the ``bigframes.ml.preprocessing`` module to normalize the target labels as integer values. -* Use the `MaxAbsScaler class `_ - in the ``bigframes.ml.preprocessing`` module to scale each feature to the range ``[-1, 1]`` by its maximum absolute value. -* Use the `MinMaxScaler class `_ - in the ``bigframes.ml.preprocessing`` module to standardize features by scaling each feature to the range ``[0, 1]``. -* Use the `StandardScaler class `_ - in the ``bigframes.ml.preprocessing`` module to standardize features by removing the mean and scaling to unit variance. -* Use the `OneHotEncoder class `_ - in the ``bigframes.ml.preprocessing`` module to transform categorical values into numeric format. -* Use the `ColumnTransformer class `_ - in the ``bigframes.ml.compose`` module to apply transformers to DataFrames columns. - - -Train models -^^^^^^^^^^^^ - -Create estimators to train models in BigQuery DataFrames. - -**Clustering models** - -Create estimators for clustering models by using the -`bigframes.ml.cluster module `_. - -* Use the `KMeans class `_ - to create K-means clustering models. Use these models for - data segmentation. For example, identifying customer segments. K-means is an - unsupervised learning technique, so model training doesn't require labels or split - data for training or evaluation. - -**Decomposition models** - -Create estimators for decomposition models by using the `bigframes.ml.decomposition module `_. - -* Use the `PCA class `_ - to create principal component analysis (PCA) models. Use these - models for computing principal components and using them to perform a change of - basis on the data. This provides dimensionality reduction by projecting each data - point onto only the first few principal components to obtain lower-dimensional - data while preserving as much of the data's variation as possible. - - -**Ensemble models** - -Create estimators for ensemble models by using the `bigframes.ml.ensemble module `_. - -* Use the `RandomForestClassifier class `_ - to create random forest classifier models. Use these models for constructing multiple - learning method decision trees for classification. -* Use the `RandomForestRegressor class `_ - to create random forest regression models. Use - these models for constructing multiple learning method decision trees for regression. -* Use the `XGBClassifier class `_ - to create gradient boosted tree classifier models. Use these models for additively - constructing multiple learning method decision trees for classification. -* Use the `XGBRegressor class `_ - to create gradient boosted tree regression models. Use these models for additively - constructing multiple learning method decision trees for regression. - - -**Forecasting models** - -Create estimators for forecasting models by using the `bigframes.ml.forecasting module `_. - -* Use the `ARIMAPlus class `_ - to create time series forecasting models. - -**Imported models** - -Create estimators for imported models by using the `bigframes.ml.imported module `_. - -* Use the `ONNXModel class `_ - to import Open Neural Network Exchange (ONNX) models. -* Use the `TensorFlowModel class `_ - to import TensorFlow models. -* Use the `XGBoostModel class `_ - to import XGBoostModel models. - -**Linear models** - -Create estimators for linear models by using the `bigframes.ml.linear_model module `_. - -* Use the `LinearRegression class `_ - to create linear regression models. Use these models for forecasting. For example, - forecasting the sales of an item on a given day. -* Use the `LogisticRegression class `_ - to create logistic regression models. Use these models for the classification of two - or more possible values such as whether an input is ``low-value``, ``medium-value``, - or ``high-value``. - -**Large language models** - -Create estimators for LLMs by using the `bigframes.ml.llm module `_. - -* Use the `GeminiTextGenerator class `_ to create Gemini text generator models. Use these models - for text generation tasks. -* Use the `PaLM2TextGenerator class `_ to create PaLM2 text generator models. Use these models - for text generation tasks. -* Use the `PaLM2TextEmbeddingGenerator class `_ to create PaLM2 text embedding generator models. - Use these models for text embedding generation tasks. - - -Create pipelines -^^^^^^^^^^^^^^^^ - -Create ML pipelines by using -`bigframes.ml.pipeline module `_. -Pipelines let you assemble several ML steps to be cross-validated together while setting -different parameters. This simplifies your code, and allows you to deploy data preprocessing -steps and an estimator together. - -* Use the `Pipeline class `_ - to create a pipeline of transforms with a final estimator. - - -ML remote models ----------------- - -**Requirements** - -To use BigQuery DataFrames ML remote models (`bigframes.ml.remote` or `bigframes.ml.llm`), -you must enable the following APIs: - -* The BigQuery API (bigquery.googleapis.com) -* The BigQuery Connection API (bigqueryconnection.googleapis.com) -* The Vertex AI API (aiplatform.googleapis.com) - -and you must be granted the following IAM roles in the project: - -* BigQuery Data Editor (roles/bigquery.dataEditor) -* BigQuery Connection Admin (roles/bigquery.connectionAdmin) -* Service Account User (roles/iam.serviceAccountUser) -* Vertex AI User (roles/aiplatform.user) -* Project IAM Admin (roles/resourcemanager.projectIamAdmin) if using default - BigQuery connection, or Browser (roles/browser) if using a pre-configured connection. - This requirement can be avoided by setting - ``bigframes.pandas.options.bigquery.skip_bq_connection_check`` option to ``True``, - in which case the connection (default or pre-configured) would be - used as-is without any existence or permission check. - - -ML locations ------------- - -``bigframes.ml`` supports the same locations as BigQuery ML. BigQuery ML model -prediction and other ML functions are supported in all BigQuery regions. Support -for model training varies by region. For more information, see -`BigQuery ML locations `_. - - -Data types ----------- - -BigQuery DataFrames supports the following numpy and pandas dtypes: - -* ``numpy.dtype("O")`` -* ``pandas.BooleanDtype()`` -* ``pandas.Float64Dtype()`` -* ``pandas.Int64Dtype()`` -* ``pandas.StringDtype(storage="pyarrow")`` -* ``pandas.ArrowDtype(pa.date32())`` -* ``pandas.ArrowDtype(pa.time64("us"))`` -* ``pandas.ArrowDtype(pa.timestamp("us"))`` -* ``pandas.ArrowDtype(pa.timestamp("us", tz="UTC"))`` - -BigQuery DataFrames doesn’t support the following BigQuery data types: - -* ``ARRAY`` -* ``NUMERIC`` -* ``BIGNUMERIC`` -* ``INTERVAL`` -* ``STRUCT`` -* ``JSON`` - -All other BigQuery data types display as the object type. - - -Remote functions ----------------- - -BigQuery DataFrames gives you the ability to turn your custom scalar functions -into `BigQuery remote functions -`_ . Creating a remote -function in BigQuery DataFrames (See `code samples -`_) -creates: - -1. A `Cloud Functions (2nd gen) function `_. -2. A `BigQuery connection `_. - If the BigQuery connection is created, the BigQuery service will - create a - `Google Cloud-managed IAM service account `_ - and attach it to the connection. You can use a pre-configured BigQuery - connection if you prefer, in which case the connection creation is skipped. -3. A BigQuery remote function that talks to the cloud function (1) using the BigQuery - connection (2). - -BigQuery connections are created in the same location as the BigQuery -DataFrames session, using the name you provide in the custom function -definition. To view and manage connections, do the following: - -1. Go to `BigQuery in the Google Cloud Console `__. -2. Select the project in which you created the remote function. -3. In the Explorer pane, expand that project and then expand External connections. - -BigQuery remote functions are created in the dataset you specify, or -in a special type of `hidden dataset `__ -referred to as an anonymous dataset. To view and manage remote functions created -in a user provided dataset, do the following: - -1. Go to `BigQuery in the Google Cloud Console `__. -2. Select the project in which you created the remote function. -3. In the Explorer pane, expand that project, expand the dataset in which you - created the remote function, and then expand Routines. - -To view and manage Cloud Functions functions, use the -`Functions `_ -page and use the project picker to select the project in which you -created the function. For easy identification, the names of the functions -created by BigQuery DataFrames are prefixed by ``bigframes``. - -**Requirements** - -To use BigQuery DataFrames remote functions, you must enable the following APIs: - -* The BigQuery API (bigquery.googleapis.com) -* The BigQuery Connection API (bigqueryconnection.googleapis.com) -* The Cloud Functions API (cloudfunctions.googleapis.com) -* The Cloud Run API (run.googleapis.com) -* The Artifact Registry API (artifactregistry.googleapis.com) -* The Cloud Build API (cloudbuild.googleapis.com ) -* The Cloud Resource Manager API (cloudresourcemanager.googleapis.com) - -To use BigQuery DataFrames remote functions, you must be granted the -following IAM roles in the project: - -* BigQuery Data Editor (roles/bigquery.dataEditor) -* BigQuery Connection Admin (roles/bigquery.connectionAdmin) -* Cloud Functions Developer (roles/cloudfunctions.developer) -* Service Account User (roles/iam.serviceAccountUser) -* Storage Object Viewer (roles/storage.objectViewer) -* Project IAM Admin (roles/resourcemanager.projectIamAdmin) if using default - BigQuery connection, or Browser (roles/browser) if using a pre-configured connection. - This requirement can be avoided by setting - ``bigframes.pandas.options.bigquery.skip_bq_connection_check`` option to ``True``, - in which case the connection (default or pre-configured) would be - used as-is without any existence or permission check. - -**Limitations** - -* Remote functions take about 90 seconds to become available when you first create them. -* Trivial changes in the notebook, such as inserting a new cell or renaming a variable, - might cause the remote function to be re-created, even if these changes are unrelated - to the remote function code. -* BigQuery DataFrames does not differentiate any personal data you include in the remote - function code. The remote function code is serialized as an opaque box to deploy it as a - Cloud Functions function. -* The Cloud Functions (2nd gen) functions, BigQuery connections, and BigQuery remote - functions created by BigQuery DataFrames persist in Google Cloud. If you don’t want to - keep these resources, you must delete them separately using an appropriate Cloud Functions - or BigQuery interface. -* A project can have up to 1000 Cloud Functions (2nd gen) functions at a time. See Cloud - Functions quotas for all the limits. - - -Quotas and limits ------------------- - -`BigQuery quotas `_ -including hardware, software, and network components. - - -Session termination -------------------- - -Each BigQuery DataFrames DataFrame or Series object is tied to a BigQuery -DataFrames session, which is in turn based on a BigQuery session. BigQuery -sessions -`auto-terminate `_ -; when this happens, you can’t use previously -created DataFrame or Series objects and must re-create them using a new -BigQuery DataFrames session. You can do this by running -``bigframes.pandas.close_session()`` and then re-running the BigQuery -DataFrames expressions. - -Data processing location ------------------------- -BigQuery DataFrames is designed for scale, which it achieves by keeping data -and processing on the BigQuery service. However, you can bring data into the -memory of your client machine by calling ``.to_pandas()`` on a DataFrame or Series -object. If you choose to do this, the memory limitation of your client machine -applies. +Getting started with BigQuery DataFrames +---------------------------------------- +Try the `BigQuery DataFrames quickstart `_ +to get up and running in just a few minutes. License From 769868b9fc7dfff2e7b1ed5cec52a5dd3dfd6ff2 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 28 Mar 2024 21:12:16 -0700 Subject: [PATCH 441/921] docs: Mark Gemini model as Pre-GA (#543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/llm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 6c4ae2ea434..e35f4d813db 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -421,6 +421,12 @@ def to_gbq( class GeminiTextGenerator(base.BaseEstimator): """Gemini text generator LLM model. + .. note:: + This product or feature 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 + (https://cloud.google.com/products#product-launch-stages). + Args: session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. From 54e49cff89bd329852a823cd5cf5c5b41b7f9e32 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Fri, 29 Mar 2024 10:03:15 -0700 Subject: [PATCH 442/921] feat: Support max_columns in repr and make repr more efficient (#515) --- bigframes/core/blocks.py | 42 ++++++++++++++------- bigframes/core/indexes/index.py | 10 ++--- bigframes/dataframe.py | 66 +++++++++++++-------------------- bigframes/series.py | 9 ++--- bigframes/session/__init__.py | 8 +++- 5 files changed, 70 insertions(+), 65 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index afa13375b18..6827e1afe8a 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -464,6 +464,23 @@ def to_pandas_batches(self): self._copy_index_to_pandas(df) yield df + def download_pandas_preview( + self, max_rows: int + ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: + """Download one page of results and return the query job.""" + dtypes = dict(zip(self.index_columns, self.index.dtypes)) + dtypes.update(zip(self.value_columns, self.dtypes)) + results_iterator, query_job = self.session._execute( + self.expr, sorted=True, max_results=max_rows + ) + arrow_results_iterator = results_iterator.to_arrow_iterable() + arrow_table = next(arrow_results_iterator) + downloaded_df = bigframes.session._io.pandas.arrow_to_pandas( + arrow_table, dtypes + ) + self._copy_index_to_pandas(downloaded_df) + return downloaded_df, query_job + def _copy_index_to_pandas(self, df: pd.DataFrame): """Set the index on pandas DataFrame to match this block. @@ -1294,26 +1311,25 @@ def _forward_slice(self, start: int = 0, stop=None, step: int = 1): # queries. @functools.cache def retrieve_repr_request_results( - self, max_results: int - ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: + self, max_results: int, max_columns: int + ) -> Tuple[pd.DataFrame, Tuple[int, int], bigquery.QueryJob]: """ Retrieves a pandas dataframe containing only max_results many rows for use with printing methods. - Returns a tuple of the dataframe and the overall number of rows of the query. + Returns a tuple of the dataframe preview for printing and the overall number + of rows and columns of the table, as well as the query job used. """ - # TODO(swast): Select a subset of columns if max_columns is less than the - # number of columns in the schema. - count = self.shape[0] - if count > max_results: - head_block = self.slice(0, max_results) - else: - head_block = self - computed_df, query_job = head_block.to_pandas() - formatted_df = computed_df.set_axis(self.column_labels, axis=1) + pandas_df, query_job = self.download_pandas_preview(max_results) + row_count = self.session._get_table_row_count(query_job.destination) + column_count = len(self.value_columns) + + formatted_df = pandas_df.set_axis(self.column_labels, axis=1) # we reset the axis and substitute the bf index name for the default formatted_df.index.name = self.index.name - return formatted_df, count, query_job + # limit column count + formatted_df = formatted_df.iloc[:, 0:max_columns] + return formatted_df, (row_count, column_count), query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: result_id = guid.generate_guid() diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index c818b68711c..48988aaffe8 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -205,17 +205,17 @@ def query_job(self) -> Optional[bigquery.QueryJob]: return self._query_job def __repr__(self) -> str: - # TODO(swast): Add a timeout here? If the query is taking a long time, - # maybe we just print the job metadata that we have so far? - # TODO(swast): Avoid downloading the whole series by using job - # metadata, like we do with DataFrame. opts = bigframes.options.display max_results = opts.max_rows + max_columns = opts.max_columns if opts.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) - pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) + pandas_df, _, query_job = self._block.retrieve_repr_request_results( + max_results, max_columns + ) self._query_job = query_job + return repr(pandas_df.index) def copy(self, name: Optional[Hashable] = None): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 599546284bd..b75cc5faacc 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -579,28 +579,16 @@ def __setattr__(self, key: str, value): object.__setattr__(self, key, value) def __repr__(self) -> str: - """Converts a DataFrame to a string. Calls to_pandas. + """Converts a DataFrame to a string using pandas dataframe __repr__. - Only represents the first `bigframes.options.display.max_rows`. + Only represents the first `bigframes.options.display.max_rows` + and `bigframes.options.display.max_columns`. """ - opts = bigframes.options.display - max_results = opts.max_rows - if opts.repr_mode == "deferred": + if bigframes.options.display.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) - self._cached() - # TODO(swast): pass max_columns and get the true column count back. Maybe - # get 1 more column than we have requested so that pandas can add the - # ... for us? - pandas_df, row_count, query_job = self._block.retrieve_repr_request_results( - max_results - ) - - self._set_internal_query_job(query_job) - - column_count = len(pandas_df.columns) - - with display_options.pandas_repr(opts): + pandas_df, shape = self._perform_repr_request() + with display_options.pandas_repr(bigframes.options.display): repr_string = repr(pandas_df) # Modify the end of the string to reflect count. @@ -608,42 +596,40 @@ def __repr__(self) -> str: pattern = re.compile("\\[[0-9]+ rows x [0-9]+ columns\\]") if pattern.match(lines[-1]): lines = lines[:-2] - - if row_count > len(lines) - 1: + if shape[0] > len(lines) - 1: lines.append("...") - lines.append("") - lines.append(f"[{row_count} rows x {column_count} columns]") + lines.append(f"[{shape[0]} rows x {shape[1]} columns]") return "\n".join(lines) + def _perform_repr_request(self) -> Tuple[pandas.DataFrame, Tuple[int, int]]: + max_results = bigframes.options.display.max_rows + max_columns = bigframes.options.display.max_columns + self._cached() + pandas_df, shape, query_job = self._block.retrieve_repr_request_results( + max_results, max_columns + ) + self._set_internal_query_job(query_job) + return pandas_df, shape + def _repr_html_(self) -> str: """ Returns an html string primarily for use by notebooks for displaying - a representation of the DataFrame. Displays 20 rows by default since - many notebooks are not configured for large tables. + a representation of the DataFrame. Displays at most the number of rows + and columns given by `bigframes.options.display.max_rows` and + `bigframes.options.display.max_columns`. """ - opts = bigframes.options.display - max_results = bigframes.options.display.max_rows - if opts.repr_mode == "deferred": - return formatter.repr_query_job_html(self.query_job) - self._cached() - # TODO(swast): pass max_columns and get the true column count back. Maybe - # get 1 more column than we have requested so that pandas can add the - # ... for us? - pandas_df, row_count, query_job = self._block.retrieve_repr_request_results( - max_results - ) - - self._set_internal_query_job(query_job) + if bigframes.options.display.repr_mode == "deferred": + return formatter.repr_query_job_html(self.query_job) - column_count = len(pandas_df.columns) + pandas_df, shape = self._perform_repr_request() - with display_options.pandas_repr(opts): + with display_options.pandas_repr(bigframes.options.display): # _repr_html_ stub is missing so mypy thinks it's a Series. Ignore mypy. html_string = pandas_df._repr_html_() # type:ignore - html_string += f"[{row_count} rows x {column_count} columns in total]" + html_string += f"[{shape[0]} rows x {shape[1]} columns in total]" return html_string def __setitem__(self, key: str, value: SingleItemValue): diff --git a/bigframes/series.py b/bigframes/series.py index e7b358c2feb..f1ac89f5143 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -272,17 +272,16 @@ def reset_index( return bigframes.dataframe.DataFrame(block) def __repr__(self) -> str: - # TODO(swast): Add a timeout here? If the query is taking a long time, - # maybe we just print the job metadata that we have so far? - # TODO(swast): Avoid downloading the whole series by using job - # metadata, like we do with DataFrame. opts = bigframes.options.display max_results = opts.max_rows + max_columns = opts.max_columns if opts.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) self._cached() - pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) + pandas_df, _, query_job = self._block.retrieve_repr_request_results( + max_results, max_columns + ) self._set_internal_query_job(query_job) return repr(pandas_df.iloc[:, 0]) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 479b3a7bacc..6a2c87bb056 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1831,6 +1831,7 @@ def _execute( sorted: bool = True, dry_run=False, col_id_overrides: Mapping[str, str] = {}, + max_results: Optional[int] = None, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: sql = self._to_sql( array_value, sorted=sorted, col_id_overrides=col_id_overrides @@ -1840,8 +1841,7 @@ def _execute( else: job_config.dry_run = dry_run return self._start_query( - sql=sql, - job_config=job_config, + sql=sql, job_config=job_config, max_results=max_results ) def _peek( @@ -1886,6 +1886,10 @@ def _get_table_size(self, destination_table): table = self.bqclient.get_table(destination_table) return table.num_bytes + def _get_table_row_count(self, destination_table) -> int: + table = self.bqclient.get_table(destination_table) + return table.num_rows + def _rows_to_dataframe( self, row_iterator: bigquery.table.RowIterator, dtypes: Dict ) -> pandas.DataFrame: From 1156c1e3ce8c1e62898dbe68ccd6c5ab3cd4068f Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:23:23 -0700 Subject: [PATCH 443/921] feat: support `ML.GENERATE_EMBEDDING` in `PaLM2TextEmbeddingGenerator` (#539) * feat: support ML.GENERATE_EMBEDDING in PaLM2TextEmbeddingGenerator --- bigframes/ml/core.py | 4 ++-- bigframes/ml/llm.py | 4 ++-- bigframes/ml/sql.py | 6 +++--- tests/system/small/ml/test_llm.py | 12 ++++++------ tests/unit/ml/test_sql.py | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 03d9b806b97..04aaeec1bcf 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -152,14 +152,14 @@ def generate_text( ), ) - def generate_text_embedding( + def generate_embedding( self, input_data: bpd.DataFrame, options: Mapping[str, int | float], ) -> bpd.DataFrame: return self._apply_sql( input_data, - lambda source_df: self._model_manipulation_sql_generator.ml_generate_text_embedding( + lambda source_df: self._model_manipulation_sql_generator.ml_generate_embedding( source_df=source_df, struct_options=options, ), diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index e35f4d813db..031656f1d80 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -44,7 +44,7 @@ _GEMINI_PRO_ENDPOINT = "gemini-pro" _ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" -_ML_EMBED_TEXT_STATUS = "ml_embed_text_status" +_ML_EMBED_TEXT_STATUS = "ml_generate_embedding_status" @log_adapter.class_logger @@ -389,7 +389,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: "flatten_json_output": True, } - df = self._bqml_model.generate_text_embedding(X, options) + df = self._bqml_model.generate_embedding(X, options) if (df[_ML_EMBED_TEXT_STATUS] != "").any(): warnings.warn( diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 807fadc06a9..fab358cce37 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -270,12 +270,12 @@ def ml_generate_text( return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL `{self._model_name}`, ({self._source_sql(source_df)}), {struct_options_sql})""" - def ml_generate_text_embedding( + def ml_generate_embedding( self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] ) -> str: - """Encode ML.GENERATE_TEXT_EMBEDDING for BQML""" + """Encode ML.GENERATE_EMBEDDING for BQML""" struct_options_sql = self.struct_options(**struct_options) - return f"""SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `{self._model_name}`, ({self._source_sql(source_df)}), {struct_options_sql})""" def ml_detect_anomalies( diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 4d2ddfe5133..2e135bef7b0 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -261,8 +261,8 @@ def test_embedding_generator_predict_success( ): df = palm2_embedding_generator_model.predict(llm_text_df).to_pandas() assert df.shape == (3, 4) - assert "text_embedding" in df.columns - series = df["text_embedding"] + assert "ml_generate_embedding_result" in df.columns + series = df["ml_generate_embedding_result"] value = series[0] assert len(value) == 768 @@ -273,8 +273,8 @@ def test_embedding_generator_multilingual_predict_success( ): df = palm2_embedding_generator_multilingual_model.predict(llm_text_df).to_pandas() assert df.shape == (3, 4) - assert "text_embedding" in df.columns - series = df["text_embedding"] + assert "ml_generate_embedding_result" in df.columns + series = df["ml_generate_embedding_result"] value = series[0] assert len(value) == 768 @@ -285,8 +285,8 @@ def test_embedding_generator_predict_series_success( ): df = palm2_embedding_generator_model.predict(llm_text_df["prompt"]).to_pandas() assert df.shape == (3, 4) - assert "text_embedding" in df.columns - series = df["text_embedding"] + assert "ml_generate_embedding_result" in df.columns + series = df["ml_generate_embedding_result"] value = series[0] assert len(value) == 768 diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 913bab0379d..5b1ff37775c 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -373,17 +373,17 @@ def test_ml_generate_text_correct( ) -def test_ml_generate_text_embedding_correct( +def test_ml_generate_embedding_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, ): - sql = model_manipulation_sql_generator.ml_generate_text_embedding( + sql = model_manipulation_sql_generator.ml_generate_embedding( source_df=mock_df, struct_options={"option_key1": 1, "option_key2": 2.2}, ) assert ( sql - == """SELECT * FROM ML.GENERATE_TEXT_EMBEDDING(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `my_project_id.my_dataset_id.my_model_id`, (input_X_sql), STRUCT( 1 AS option_key1, 2.2 AS option_key2))""" From f6c40cdc91968364c0a072092ebfecf6e0f5cf34 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:36:27 -0700 Subject: [PATCH 444/921] test: add the code snippets for gemini model (#546) --- samples/snippets/gemini_model_test.py | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 samples/snippets/gemini_model_test.py diff --git a/samples/snippets/gemini_model_test.py b/samples/snippets/gemini_model_test.py new file mode 100644 index 00000000000..89212875ae6 --- /dev/null +++ b/samples/snippets/gemini_model_test.py @@ -0,0 +1,44 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_gemini_text_generator_model(): + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") + REGION = "us" + CONN_NAME = "bigframes-default-connection" + + # [START bigquery_dataframes_gemini_model] + from bigframes.ml.llm import GeminiTextGenerator + import bigframes.pandas as bpd + + # Create the Gemini LLM model + session = bpd.get_global_session() + connection = f"{PROJECT_ID}.{REGION}.{CONN_NAME}" + model = GeminiTextGenerator(session=session, connection_name=connection) + + df_api = bpd.read_csv("gs://cloud-samples-data/vertex-ai/bigframe/df.csv") + + # Prepare the prompts and send them to the LLM model for prediction + df_prompt_prefix = "Generate Pandas sample code for DataFrame." + df_prompt = df_prompt_prefix + df_api["API"] + + # Predict using the model + df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024) + # [END bigquery_dataframes_gemini_model] + assert df_pred["ml_generate_text_llm_result"] is not None + assert df_pred["ml_generate_text_llm_result"].iloc[0] is not None From 0a4153cc71a44c09b8d691897f1e5afa58c69f25 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:07:06 -0700 Subject: [PATCH 445/921] fix: assign NaN scalar to column error. (#513) * fix: assign NaN scalar to column error. * Update test. * remove import. --- bigframes/core/__init__.py | 4 ++++ tests/system/small/test_dataframe.py | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 6fd6fc23c2f..ce9c22132ba 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -229,6 +229,10 @@ def assign_constant( value: typing.Any, dtype: typing.Optional[bigframes.dtypes.Dtype], ) -> ArrayValue: + if pandas.isna(value): + # Need to assign a data type when value is NaN. + dtype = dtype or bigframes.dtypes.DEFAULT_DTYPE + if destination_id in self.column_ids: # Mutate case exprs = [ ( diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 355849538e4..645914285ee 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -605,17 +605,24 @@ def test_assign_new_column_w_loc(scalars_dfs): pd.testing.assert_frame_equal(bf_result, pd_result) -def test_assign_new_column_w_setitem(scalars_dfs): +@pytest.mark.parametrize( + ("scalar",), + [ + (2.1,), + (None,), + ], +) +def test_assign_new_column_w_setitem(scalars_dfs, scalar): scalars_df, scalars_pandas_df = scalars_dfs bf_df = scalars_df.copy() pd_df = scalars_pandas_df.copy() - bf_df["new_col"] = 2 - pd_df["new_col"] = 2 + bf_df["new_col"] = scalar + pd_df["new_col"] = scalar bf_result = bf_df.to_pandas() pd_result = pd_df - # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. - pd_result["new_col"] = pd_result["new_col"].astype("Int64") + # Convert default pandas dtypes `float64` to match BigQuery DataFrames dtypes. + pd_result["new_col"] = pd_result["new_col"].astype("Float64") pd.testing.assert_frame_equal(bf_result, pd_result) From f79827757bbf022c718a4d239c622a7a7ec2feee Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 29 Mar 2024 21:08:20 +0000 Subject: [PATCH 446/921] chore: enhance documentation coverage script (#545) to include just the docstring coverage by default, provide `-c`/`--code-samples` option for the code sample coverage. --- ...erage.py => get_documentation_coverage.py} | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) rename scripts/{get_code_sample_coverage.py => get_documentation_coverage.py} (74%) diff --git a/scripts/get_code_sample_coverage.py b/scripts/get_documentation_coverage.py similarity index 74% rename from scripts/get_code_sample_coverage.py rename to scripts/get_documentation_coverage.py index d81023394ff..0b9417b2d30 100755 --- a/scripts/get_code_sample_coverage.py +++ b/scripts/get_documentation_coverage.py @@ -16,7 +16,7 @@ import importlib import inspect import sys -from typing import Dict, List +import typing import bigframes import bigframes.pandas as bpd @@ -50,6 +50,11 @@ "remote", ] +COVERAGE_GENERATORS = { + "documentation": lambda docstr: docstr, + "code samples": lambda docstr: docstr and "**Examples:**" in docstr, +} + for module_name in ML_MODULE_NAMES: module = importlib.import_module(f"bigframes.ml.{module_name}") classes_ = [ @@ -58,9 +63,15 @@ CLASSES.extend(classes_) -def get_code_samples_summary() -> Dict[str, Dict[str, List[str]]]: +def get_coverage_summary( + func: typing.Callable, +) -> typing.Dict[str, typing.Dict[str, typing.List[str]]]: """Get Summary of the code samples coverage in BigFrames APIs. + Args: + func (callable): + Function to accept documentation and return whether it satisfies + coverage. Returns: Summary: A dictionary of the format { @@ -73,7 +84,7 @@ def get_code_samples_summary() -> Dict[str, Dict[str, List[str]]]: } } """ - summary: Dict[str, Dict[str, List[str]]] = dict() + summary: typing.Dict[str, typing.Dict[str, typing.List[str]]] = dict() for class_ in CLASSES: class_key = f"{class_.__module__}.{class_.__name__}" @@ -104,8 +115,8 @@ def predicate(impl): impl = getattr(class_, name) docstr = inspect.getdoc(impl) - code_samples_present = docstr and "**Examples:**" in docstr - key = PRESENT if code_samples_present else NOT_PRESENT + coverage_present = func(docstr) + key = PRESENT if coverage_present else NOT_PRESENT summary[class_key][key].append(name) return summary @@ -113,7 +124,16 @@ def predicate(impl): if __name__ == "__main__": parser = argparse.ArgumentParser( - description="Get a summary of code samples coverage in BigFrames APIs." + description="Get a summary of documentation coverage in BigFrames APIs." + ) + parser.add_argument( + "-c", + "--code-samples", + type=bool, + action=argparse.BooleanOptionalAction, + default=False, + help="Whether to calculate code samples coverage. By default the tool" + " calculates the documentation (docstring) coverage.", ) parser.add_argument( "-d", @@ -121,12 +141,13 @@ def predicate(impl): type=bool, action=argparse.BooleanOptionalAction, default=False, - help="Whether to print APIs with and without code samples.", + help="Whether to print APIs with and without the coverage.", ) args = parser.parse_args(sys.argv[1:]) - summary = get_code_samples_summary() + scenario = "code samples" if args.code_samples else "documentation" + summary = get_coverage_summary(COVERAGE_GENERATORS[scenario]) total_with_code_samples = 0 total = 0 @@ -140,8 +161,8 @@ def predicate(impl): coverage = 100 * apis_with_code_samples / apis_total print(f"{class_}: {coverage:.1f}% ({apis_with_code_samples}/{apis_total})") if args.details: - print(f"===> APIs WITH code samples: {class_summary[PRESENT]}") - print(f"===> APIs WITHOUT code samples: {class_summary[NOT_PRESENT]}") + print(f"===> APIs WITH {scenario}: {class_summary[PRESENT]}") + print(f"===> APIs WITHOUT {scenario}: {class_summary[NOT_PRESENT]}") coverage = 100 * total_with_code_samples / total print(f"Total: {coverage:.1f}% ({total_with_code_samples}/{total})") From 8eca99a03bc4bdaccf15a979b5382f3659f2aac5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 29 Mar 2024 14:09:34 -0700 Subject: [PATCH 447/921] fix: Fix case where df.peek would fail to execute even with force=True (#511) * fix: Fix case where df.peek would fail to execute even with force=True * remove cache from peekable property * if force=True always peek after caching even if peeking inefficient --- bigframes/core/blocks.py | 7 +++- bigframes/core/nodes.py | 37 +------------------ .../core/{traversal.py => tree_properties.py} | 11 ++++++ bigframes/dataframe.py | 2 +- bigframes/session/__init__.py | 7 ++-- tests/system/small/test_dataframe.py | 11 ++++++ 6 files changed, 33 insertions(+), 42 deletions(-) rename bigframes/core/{traversal.py => tree_properties.py} (72%) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 6827e1afe8a..aab8b1ad4db 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -41,6 +41,7 @@ import bigframes.core.guid as guid import bigframes.core.join_def as join_defs import bigframes.core.ordering as ordering +import bigframes.core.tree_properties as tree_properties import bigframes.core.utils import bigframes.core.utils as utils import bigframes.dtypes @@ -443,8 +444,10 @@ def to_pandas( df.set_axis(self.column_labels, axis=1, copy=False) return df, query_job - def try_peek(self, n: int = 20) -> typing.Optional[pd.DataFrame]: - if self.expr.node.peekable: + def try_peek( + self, n: int = 20, force: bool = False + ) -> typing.Optional[pd.DataFrame]: + if force or tree_properties.peekable(self.expr.node): iterator, _ = self.session._peek(self.expr, n) df = self._to_dataframe(iterator) self._copy_index_to_pandas(df) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index c1ceeebffea..8f646ac4bb2 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -90,11 +90,6 @@ def session(self): def _node_hash(self): return hash(tuple(hash(getattr(self, field.name)) for field in fields(self))) - @property - def peekable(self) -> bool: - """Indicates whether the node can be sampled efficiently""" - return all(child.peekable for child in self.child_nodes) - @property def roots(self) -> typing.Set[BigFrameNode]: roots = itertools.chain.from_iterable( @@ -143,12 +138,6 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: def __hash__(self): return self._node_hash - @property - def peekable(self) -> bool: - children_peekable = all(child.peekable for child in self.child_nodes) - single_root = len(self.roots) == 1 - return children_peekable and single_root - @functools.cached_property def schema(self) -> schemata.ArraySchema: def join_mapping_to_schema_item(mapping: JoinColumnMapping): @@ -204,10 +193,6 @@ class ReadLocalNode(BigFrameNode): def __hash__(self): return self._node_hash - @property - def peekable(self) -> bool: - return True - @property def roots(self) -> typing.Set[BigFrameNode]: return {self} @@ -233,10 +218,6 @@ def session(self): def __hash__(self): return self._node_hash - @property - def peekable(self) -> bool: - return True - @property def roots(self) -> typing.Set[BigFrameNode]: return {self} @@ -261,13 +242,9 @@ class PromoteOffsetsNode(UnaryNode): def __hash__(self): return self._node_hash - @property - def peekable(self) -> bool: - return False - @property def non_local(self) -> bool: - return False + return True @property def schema(self) -> schemata.ArraySchema: @@ -371,10 +348,6 @@ def row_preserving(self) -> bool: def __hash__(self): return self._node_hash - @property - def peekable(self) -> bool: - return False - @property def non_local(self) -> bool: return True @@ -407,10 +380,6 @@ class WindowOpNode(UnaryNode): def __hash__(self): return self._node_hash - @property - def peekable(self) -> bool: - return False - @property def non_local(self) -> bool: return True @@ -459,10 +428,6 @@ def row_preserving(self) -> bool: def non_local(self) -> bool: return True - @property - def peekable(self) -> bool: - return False - @functools.cached_property def schema(self) -> schemata.ArraySchema: def infer_dtype( diff --git a/bigframes/core/traversal.py b/bigframes/core/tree_properties.py similarity index 72% rename from bigframes/core/traversal.py rename to bigframes/core/tree_properties.py index b038ee6599c..bc29f115f6d 100644 --- a/bigframes/core/traversal.py +++ b/bigframes/core/tree_properties.py @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. + import bigframes.core.nodes as nodes +# TODO: Convert these functions to iterative or enforce hard limit on tree depth. The below algorithms can cause stack to exceed limit. + def is_trivially_executable(node: nodes.BigFrameNode) -> bool: if local_only(node): @@ -25,3 +28,11 @@ def is_trivially_executable(node: nodes.BigFrameNode) -> bool: def local_only(node: nodes.BigFrameNode) -> bool: return all(isinstance(node, nodes.ReadLocalNode) for node in node.roots) + + +def peekable(node: nodes.BigFrameNode) -> bool: + if local_only(node): + return True + children_peekable = all(peekable(child) for child in node.child_nodes) + self_peekable = not node.non_local + return children_peekable and self_peekable diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index b75cc5faacc..1df78dd4cd2 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1111,7 +1111,7 @@ def peek(self, n: int = 5, *, force: bool = True) -> pandas.DataFrame: if maybe_result is None: if force: self._cached() - maybe_result = self._block.try_peek(n) + maybe_result = self._block.try_peek(n, force=True) assert maybe_result is not None else: raise ValueError( diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 6a2c87bb056..8294bc3aa1e 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -82,7 +82,8 @@ import bigframes.core.guid as guid from bigframes.core.ordering import IntegerEncoding import bigframes.core.ordering as order -import bigframes.core.traversal as traversals +import bigframes.core.tree_properties as traversals +import bigframes.core.tree_properties as tree_properties import bigframes.core.utils as utils import bigframes.dtypes import bigframes.formatting_helpers as formatting_helpers @@ -1848,8 +1849,8 @@ def _peek( self, array_value: core.ArrayValue, n_rows: int ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """A 'peek' efficiently accesses a small number of rows in the dataframe.""" - if not array_value.node.peekable: - raise NotImplementedError("cannot efficient peek this dataframe") + if not tree_properties.peekable(array_value.node): + warnings.warn("Peeking this value cannot be done efficiently.") sql = self._compile_unordered(array_value).peek_sql(n_rows) return self._start_query( sql=sql, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 645914285ee..cf907b02d68 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -494,6 +494,17 @@ def test_df_peek_force_default(scalars_dfs): assert len(peek_result) == 3 +def test_df_peek_reset_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + peek_result = ( + scalars_df[["int64_col", "int64_too"]].reset_index(drop=True).peek(n=3) + ) + pd.testing.assert_index_equal( + scalars_pandas_df[["int64_col", "int64_too"]].columns, peek_result.columns + ) + assert len(peek_result) == 3 + + def test_repr_w_all_rows(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs From 9d8cf6792a8dbe03e03b102c454d15fcde7986af Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:12:03 -0700 Subject: [PATCH 448/921] feat: add ColumnTransformer save/load (#541) --- bigframes/ml/compose.py | 133 ++++++++++++++++++++++++- bigframes/ml/loader.py | 30 +++++- bigframes/ml/pipeline.py | 113 +-------------------- bigframes/session/__init__.py | 2 +- tests/system/large/ml/test_compose.py | 57 ++++++++--- tests/system/large/ml/test_pipeline.py | 4 +- 6 files changed, 204 insertions(+), 135 deletions(-) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 6d4fa5b76d0..cd233589d6f 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -18,16 +18,21 @@ from __future__ import annotations +import re +import types import typing -from typing import List, Optional, Tuple, Union +from typing import cast, List, Optional, Tuple, Union import bigframes_vendored.sklearn.compose._column_transformer +from google.cloud import bigquery +import bigframes +from bigframes import constants from bigframes.core import log_adapter from bigframes.ml import base, core, globals, preprocessing, utils import bigframes.pandas as bpd -CompilablePreprocessorType = Union[ +_PREPROCESSING_TYPES = Union[ preprocessing.OneHotEncoder, preprocessing.StandardScaler, preprocessing.MaxAbsScaler, @@ -36,6 +41,17 @@ preprocessing.LabelEncoder, ] +_BQML_TRANSFROM_TYPE_MAPPING = types.MappingProxyType( + { + "ML.STANDARD_SCALER": preprocessing.StandardScaler, + "ML.ONE_HOT_ENCODER": preprocessing.OneHotEncoder, + "ML.MAX_ABS_SCALER": preprocessing.MaxAbsScaler, + "ML.MIN_MAX_SCALER": preprocessing.MinMaxScaler, + "ML.BUCKETIZE": preprocessing.KBinsDiscretizer, + "ML.LABEL_ENCODER": preprocessing.LabelEncoder, + } +) + @log_adapter.class_logger class ColumnTransformer( @@ -51,7 +67,7 @@ def __init__( transformers: List[ Tuple[ str, - CompilablePreprocessorType, + _PREPROCESSING_TYPES, Union[str, List[str]], ] ], @@ -66,12 +82,12 @@ def __init__( @property def transformers_( self, - ) -> List[Tuple[str, CompilablePreprocessorType, str,]]: + ) -> List[Tuple[str, _PREPROCESSING_TYPES, str,]]: """The collection of transformers as tuples of (name, transformer, column).""" result: List[ Tuple[ str, - CompilablePreprocessorType, + _PREPROCESSING_TYPES, str, ] ] = [] @@ -89,6 +105,96 @@ def transformers_( return result + @classmethod + def _from_bq( + cls, session: bigframes.Session, model: bigquery.Model + ) -> ColumnTransformer: + col_transformer = cls._extract_from_bq_model(model) + col_transformer._bqml_model = core.BqmlModel(session, model) + + return col_transformer + + @classmethod + def _extract_from_bq_model( + cls, + bq_model: bigquery.Model, + ) -> ColumnTransformer: + """Extract transformers as ColumnTransformer obj from a BQ Model. Keep the _bqml_model field as None.""" + assert "transformColumns" in bq_model._properties + + transformers: List[ + Tuple[ + str, + _PREPROCESSING_TYPES, + Union[str, List[str]], + ] + ] = [] + + def camel_to_snake(name): + name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + for transform_col in bq_model._properties["transformColumns"]: + # pass the columns that are not transformed + if "transformSql" not in transform_col: + continue + transform_sql: str = cast(dict, transform_col)["transformSql"] + if not transform_sql.startswith("ML."): + continue + + found_transformer = False + for prefix in _BQML_TRANSFROM_TYPE_MAPPING: + if transform_sql.startswith(prefix): + transformer_cls = _BQML_TRANSFROM_TYPE_MAPPING[prefix] + transformers.append( + ( + camel_to_snake(transformer_cls.__name__), + *transformer_cls._parse_from_sql(transform_sql), # type: ignore + ) + ) + + found_transformer = True + break + if not found_transformer: + raise NotImplementedError( + f"Unsupported transformer type. {constants.FEEDBACK_LINK}" + ) + + return cls(transformers=transformers) + + def _merge( + self, bq_model: bigquery.Model + ) -> Union[ + ColumnTransformer, + preprocessing.StandardScaler, + preprocessing.OneHotEncoder, + preprocessing.MaxAbsScaler, + preprocessing.MinMaxScaler, + preprocessing.KBinsDiscretizer, + preprocessing.LabelEncoder, + ]: + """Try to merge the column transformer to a simple transformer. Depends on all the columns in bq_model are transformed with the same transformer.""" + transformers = self.transformers_ + + assert len(transformers) > 0 + _, transformer_0, column_0 = transformers[0] + columns = [column_0] + for _, transformer, column in transformers[1:]: + # all transformers are the same + if transformer != transformer_0: + return self + columns.append(column) + # all feature columns are transformed + if sorted( + [ + cast(str, feature_column.name) + for feature_column in bq_model.feature_columns + ] + ) == sorted(columns): + return transformer_0 + + return self + def _compile_to_sql( self, columns: List[str], @@ -143,3 +249,20 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: bpd.DataFrame, df[self._output_names], ) + + def to_gbq(self, model_name: str, replace: bool = False) -> ColumnTransformer: + """Save the transformer as a BigQuery model. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + ColumnTransformer: saved model.""" + if not self._bqml_model: + raise RuntimeError("A transformer must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 31912a01294..508003a98d6 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -23,6 +23,7 @@ import bigframes.constants as constants from bigframes.ml import ( cluster, + compose, decomposition, ensemble, forecasting, @@ -79,6 +80,7 @@ def from_bq( llm.PaLM2TextGenerator, llm.PaLM2TextEmbeddingGenerator, pipeline.Pipeline, + compose.ColumnTransformer, ]: """Load a BQML model to BigQuery DataFrames ML. @@ -89,22 +91,32 @@ def from_bq( Returns: A BigQuery DataFrames ML model object. """ + # TODO(garrettwu): the entire condition only to TRANSFORM_ONLY when b/331679273 is fixed. + if ( + bq_model.model_type == "TRANSFORM_ONLY" + or bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + and "transformColumns" in bq_model._properties + and not _is_bq_model_remote(bq_model) + ): + return _transformer_from_bq(session, bq_model) + if _is_bq_model_pipeline(bq_model): return pipeline.Pipeline._from_bq(session, bq_model) return _model_from_bq(session, bq_model) +def _transformer_from_bq(session: bigframes.Session, bq_model: bigquery.Model): + # TODO(garrettwu): add other transformers + return compose.ColumnTransformer._from_bq(session, bq_model) + + def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): if bq_model.model_type in _BQML_MODEL_TYPE_MAPPING: return _BQML_MODEL_TYPE_MAPPING[bq_model.model_type]._from_bq( # type: ignore session=session, model=bq_model ) - if ( - bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" - and "remoteModelInfo" in bq_model._properties - and "endpoint" in bq_model._properties["remoteModelInfo"] - ): + if _is_bq_model_remote(bq_model): # Parse the remote model endpoint bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] model_endpoint = bqml_endpoint.split("/")[-1] @@ -121,3 +133,11 @@ def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): def _is_bq_model_pipeline(bq_model: bigquery.Model) -> bool: return "transformColumns" in bq_model._properties + + +def _is_bq_model_remote(bq_model: bigquery.Model) -> bool: + return ( + bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + and "remoteModelInfo" in bq_model._properties + and "endpoint" in bq_model._properties["remoteModelInfo"] + ) diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 9289b613b87..92a3bae77d3 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import cast, List, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union import bigframes_vendored.sklearn.pipeline from google.cloud import bigquery @@ -83,8 +83,8 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): @classmethod def _from_bq(cls, session: bigframes.Session, bq_model: bigquery.Model) -> Pipeline: - col_transformer = _extract_as_column_transformer(bq_model) - transform = _merge_column_transformer(bq_model, col_transformer) + col_transformer = compose.ColumnTransformer._extract_from_bq_model(bq_model) + transform = col_transformer._merge(bq_model) estimator = loader._model_from_bq(session, bq_model) return cls([("transform", transform), ("estimator", estimator)]) @@ -138,110 +138,3 @@ def to_gbq(self, model_name: str, replace: bool = False) -> Pipeline: new_model = self._estimator._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) - - -def _extract_as_column_transformer( - bq_model: bigquery.Model, -) -> compose.ColumnTransformer: - """Extract transformers as ColumnTransformer obj from a BQ Model.""" - assert "transformColumns" in bq_model._properties - - transformers: List[ - Tuple[ - str, - Union[ - preprocessing.OneHotEncoder, - preprocessing.StandardScaler, - preprocessing.MaxAbsScaler, - preprocessing.MinMaxScaler, - preprocessing.KBinsDiscretizer, - preprocessing.LabelEncoder, - ], - Union[str, List[str]], - ] - ] = [] - for transform_col in bq_model._properties["transformColumns"]: - # pass the columns that are not transformed - if "transformSql" not in transform_col: - continue - - transform_sql: str = cast(dict, transform_col)["transformSql"] - if transform_sql.startswith("ML.STANDARD_SCALER"): - transformers.append( - ( - "standard_scaler", - *preprocessing.StandardScaler._parse_from_sql(transform_sql), - ) - ) - elif transform_sql.startswith("ML.ONE_HOT_ENCODER"): - transformers.append( - ( - "ont_hot_encoder", - *preprocessing.OneHotEncoder._parse_from_sql(transform_sql), - ) - ) - elif transform_sql.startswith("ML.MAX_ABS_SCALER"): - transformers.append( - ( - "max_abs_scaler", - *preprocessing.MaxAbsScaler._parse_from_sql(transform_sql), - ) - ) - elif transform_sql.startswith("ML.MIN_MAX_SCALER"): - transformers.append( - ( - "min_max_scaler", - *preprocessing.MinMaxScaler._parse_from_sql(transform_sql), - ) - ) - elif transform_sql.startswith("ML.BUCKETIZE"): - transformers.append( - ( - "k_bins_discretizer", - *preprocessing.KBinsDiscretizer._parse_from_sql(transform_sql), - ) - ) - elif transform_sql.startswith("ML.LABEL_ENCODER"): - transformers.append( - ( - "label_encoder", - *preprocessing.LabelEncoder._parse_from_sql(transform_sql), - ) - ) - else: - raise NotImplementedError( - f"Unsupported transformer type. {constants.FEEDBACK_LINK}" - ) - - return compose.ColumnTransformer(transformers=transformers) - - -def _merge_column_transformer( - bq_model: bigquery.Model, column_transformer: compose.ColumnTransformer -) -> Union[ - compose.ColumnTransformer, - preprocessing.StandardScaler, - preprocessing.OneHotEncoder, - preprocessing.MaxAbsScaler, - preprocessing.MinMaxScaler, - preprocessing.KBinsDiscretizer, - preprocessing.LabelEncoder, -]: - """Try to merge the column transformer to a simple transformer.""" - transformers = column_transformer.transformers_ - - assert len(transformers) > 0 - _, transformer_0, column_0 = transformers[0] - columns = [column_0] - for _, transformer, column in transformers[1:]: - # all transformers are the same - if transformer != transformer_0: - return column_transformer - columns.append(column) - # all feature columns are transformed - if sorted( - [cast(str, feature_column.name) for feature_column in bq_model.feature_columns] - ) == sorted(columns): - return transformer_0 - - return column_transformer diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 8294bc3aa1e..6573934f94a 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -953,7 +953,7 @@ def read_gbq_model(self, model_name: str): to load from the default project. Returns: - A bigframes.ml Model wrapping the model. + A bigframes.ml Model, Transformer or Pipeline wrapping the model. """ import bigframes.ml.loader diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index bb9a4d8f649..d7c49ca95a0 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -14,31 +14,27 @@ import pandas -import bigframes.ml.cluster -import bigframes.ml.compose -import bigframes.ml.linear_model -import bigframes.ml.pipeline -import bigframes.ml.preprocessing +from bigframes.ml import compose, preprocessing def test_columntransformer_standalone_fit_and_transform( penguins_df_default_index, new_penguins_df ): - transformer = bigframes.ml.compose.ColumnTransformer( + transformer = compose.ColumnTransformer( [ ( "onehot", - bigframes.ml.preprocessing.OneHotEncoder(), + preprocessing.OneHotEncoder(), "species", ), ( "starndard_scale", - bigframes.ml.preprocessing.StandardScaler(), + preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), ( "min_max_scale", - bigframes.ml.preprocessing.MinMaxScaler(), + preprocessing.MinMaxScaler(), ["culmen_length_mm"], ), ] @@ -76,16 +72,16 @@ def test_columntransformer_standalone_fit_and_transform( def test_columntransformer_standalone_fit_transform(new_penguins_df): - transformer = bigframes.ml.compose.ColumnTransformer( + transformer = compose.ColumnTransformer( [ ( "onehot", - bigframes.ml.preprocessing.OneHotEncoder(), + preprocessing.OneHotEncoder(), "species", ), ( "standard_scale", - bigframes.ml.preprocessing.StandardScaler(), + preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), ] @@ -118,3 +114,40 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): ) pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) + + +def test_columntransformer_save_load(new_penguins_df, dataset_id): + transformer = compose.ColumnTransformer( + [ + ( + "onehot", + preprocessing.OneHotEncoder(), + "species", + ), + ( + "standard_scale", + preprocessing.StandardScaler(), + ["culmen_length_mm", "flipper_length_mm"], + ), + ] + ) + transformer.fit( + new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + + assert isinstance(reloaded_transformer, compose.ColumnTransformer) + + expected = [ + ( + "one_hot_encoder", + preprocessing.OneHotEncoder(max_categories=1000001, min_frequency=0), + "species", + ), + ("standard_scaler", preprocessing.StandardScaler(), "culmen_length_mm"), + ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), + ] + assert reloaded_transformer.transformers_ == expected diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index c128469bd20..c460efa75f7 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -646,7 +646,7 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id compose.ColumnTransformer( [ ( - "ont_hot_encoder", + "one_hot_encoder", preprocessing.OneHotEncoder( drop="most_frequent", min_frequency=5, @@ -699,7 +699,7 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id transformers = pl_loaded._transform.transformers_ expected = [ ( - "ont_hot_encoder", + "one_hot_encoder", preprocessing.OneHotEncoder( drop="most_frequent", max_categories=100, min_frequency=5 ), From 74c391586280b55c35d66c697167122d72c13386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 29 Mar 2024 18:28:16 -0500 Subject: [PATCH 449/921] docs: add "Supported pandas APIs" reference to the documentation (#542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) 🦕 --- .kokoro/release-nightly.sh | 1 + docs/index.rst | 1 + docs/supported_pandas_apis.rst | 62 +++++++ docs/supported_pandas_apis/.gitignore | 1 + docs/templates/toc.yml | 2 + noxfile.py | 12 ++ scripts/publish_api_coverage.py | 222 ++++++++++++++++++++++++-- scripts/test_publish_api_coverage.py | 2 + 8 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 docs/supported_pandas_apis.rst create mode 100644 docs/supported_pandas_apis/.gitignore diff --git a/.kokoro/release-nightly.sh b/.kokoro/release-nightly.sh index 5624df3b8d9..7da0881bbef 100755 --- a/.kokoro/release-nightly.sh +++ b/.kokoro/release-nightly.sh @@ -106,6 +106,7 @@ for gcs_path in gs://vertex_sdk_private_releases/bigframe/ \ # write access to COVERAGE_TABLE=bigframes-metrics.coverage_report.bigframes_coverage_nightly python3.10 scripts/publish_api_coverage.py \ + bigquery \ --bigframes_version=$BIGFRAMES_VERSION \ --release_version=$RELEASE_VERSION \ --bigquery_table=$COVERAGE_TABLE diff --git a/docs/index.rst b/docs/index.rst index d239ea3a785..b17ac7cbd9c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,7 @@ API reference :maxdepth: 3 reference/index + supported_pandas_apis Changelog --------- diff --git a/docs/supported_pandas_apis.rst b/docs/supported_pandas_apis.rst new file mode 100644 index 00000000000..f4b57f05d10 --- /dev/null +++ b/docs/supported_pandas_apis.rst @@ -0,0 +1,62 @@ +Supported pandas APIs +===================== + +The following tables show the pandas APIs that have been implemented (or not) +in BigQuery DataFrames. + +* 'Y' means it implements all parameters. +* 'P' means it implements only some parameters. + +DataFrame +--------- + +.. raw:: html + :file: supported_pandas_apis/bf_dataframe.html + +DataFrameGroupBy +---------------- + +.. raw:: html + :file: supported_pandas_apis/bf_dataframegroupby.html + +Index +----- + +.. raw:: html + :file: supported_pandas_apis/bf_index.html + +pandas module +------------- + +.. raw:: html + :file: supported_pandas_apis/bf_pandas.html + +Series +------ + +.. raw:: html + :file: supported_pandas_apis/bf_series.html + +Series.dt methods +----------------- + +.. raw:: html + :file: supported_pandas_apis/bf_datetimemethods.html + +Series.str methods +------------------ + +.. raw:: html + :file: supported_pandas_apis/bf_stringmethods.html + +SeriesGroupBy +------------- + +.. raw:: html + :file: supported_pandas_apis/bf_seriesgroupby.html + +Window +------ + +.. raw:: html + :file: supported_pandas_apis/bf_window.html diff --git a/docs/supported_pandas_apis/.gitignore b/docs/supported_pandas_apis/.gitignore new file mode 100644 index 00000000000..2d19fc766d9 --- /dev/null +++ b/docs/supported_pandas_apis/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index c07e6141f1b..57b0522d043 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -72,6 +72,8 @@ name: Series - name: Window uid: bigframes.core.window.Window + - href: supported_pandas_apis.html + name: Supported pandas APIs name: bigframes.pandas - items: - items: diff --git a/noxfile.py b/noxfile.py index a5e77964f1e..4ac3a817236 100644 --- a/noxfile.py +++ b/noxfile.py @@ -467,6 +467,12 @@ def docs(session): ) shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) + + session.run( + "python", + "scripts/publish_api_coverage.py", + "docs", + ) session.run( "sphinx-build", "-W", # warnings as errors @@ -503,6 +509,12 @@ def docfx(session): ) shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) + + session.run( + "python", + "scripts/publish_api_coverage.py", + "docs", + ) session.run( "sphinx-build", "-T", # show full traceback on exception diff --git a/scripts/publish_api_coverage.py b/scripts/publish_api_coverage.py index 856307e4407..4a35ade9efb 100644 --- a/scripts/publish_api_coverage.py +++ b/scripts/publish_api_coverage.py @@ -17,27 +17,110 @@ import argparse import inspect +import pathlib +import sys import pandas as pd +import pandas.core.groupby +import pandas.core.indexes.accessors +import pandas.core.strings.accessor +import pandas.core.window.rolling +import bigframes +import bigframes.core.groupby +import bigframes.core.window +import bigframes.operations.datetimes import bigframes.pandas as bpd +REPO_ROOT = pathlib.Path(__file__).parent.parent + +URL_PREFIX = { + "pandas": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.pandas#bigframes_pandas_" + ), + "dataframe": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.dataframe.DataFrame#bigframes_dataframe_DataFrame_" + ), + "dataframegroupby": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.core.groupby.DataFrameGroupBy#bigframes_core_groupby_DataFrameGroupBy_" + ), + "series": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.series.Series#bigframes_series_Series_" + ), + "seriesgroupby": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.core.groupby.SeriesGroupBy#bigframes_core_groupby_SeriesGroupBy_" + ), + "datetimemethods": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.operations.datetimes.DatetimeMethods#bigframes_operations_datetimes_DatetimeMethods_" + ), + "stringmethods": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.operations.strings.StringMethods#bigframes_operations_strings_StringMethods_" + ), + "window": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.core.window.Window#bigframes_core_window_Window_" + ), + # TODO: Index not documented. +} + + +PANDAS_TARGETS = [ + ("pandas", pd, bpd), + ("dataframe", pd.DataFrame, bpd.DataFrame), + ( + "dataframegroupby", + pandas.core.groupby.DataFrameGroupBy, + bigframes.core.groupby.DataFrameGroupBy, + ), + ("series", pd.Series, bpd.Series), + ( + "seriesgroupby", + pandas.core.groupby.DataFrameGroupBy, + bigframes.core.groupby.DataFrameGroupBy, + ), + ( + "datetimemethods", + pandas.core.indexes.accessors.CombinedDatetimelikeProperties, + bigframes.operations.datetimes.DatetimeMethods, + ), + ( + "stringmethods", + pandas.core.strings.accessor.StringMethods, + bigframes.operations.strings.StringMethods, + ), + ( + "window", + pandas.core.window.rolling.Rolling, + bigframes.core.window.Window, + ), + ("index", pd.Index, bpd.Index), +] + + +def names_from_signature(signature): + """Extract the names of parameters from signature + + See: https://docs.python.org/3/library/inspect.html#inspect.signature + """ + return frozenset({parameter for parameter in signature.parameters}) + + +def calculate_missing_parameters(bigframes_function, target_function): + bigframes_params = names_from_signature(inspect.signature(bigframes_function)) + target_params = names_from_signature(inspect.signature(target_function)) + return target_params - bigframes_params + def generate_pandas_api_coverage(): """Inspect all our pandas objects, and compare with the real pandas objects, to see which methods we implement. For each, generate a regex that can be used to check if its present in a notebook""" - header = ["api", "pattern", "kind", "is_in_bigframes"] + header = ["api", "pattern", "kind", "is_in_bigframes", "missing_parameters"] api_patterns = [] - targets = [ - ("pandas", pd, bpd), - ("dataframe", pd.DataFrame, bpd.DataFrame), - ("series", pd.Series, bpd.Series), - ("index", pd.Index, bpd.Index), - ] indexers = ["loc", "iloc", "iat", "ix", "at"] - for name, pandas_obj, bigframes_obj in targets: + for name, pandas_obj, bigframes_obj in PANDAS_TARGETS: for member in dir(pandas_obj): + missing_parameters = "" + # skip private functions and properties if member[0] == "_" and member[1] != "_": continue @@ -50,6 +133,17 @@ def generate_pandas_api_coverage(): # Function, match .member( token = f"\\.{member}\\(" token_type = "function" + + if hasattr(bigframes_obj, member): + bigframes_function = getattr(bigframes_obj, member) + pandas_function = getattr(pandas_obj, member) + missing_parameters = ", ".join( + sorted( + calculate_missing_parameters( + bigframes_function, pandas_function + ) + ) + ) elif member in indexers: # Indexer, match .indexer[ token = f"\\.{member}\\[" @@ -62,7 +156,13 @@ def generate_pandas_api_coverage(): is_in_bigframes = hasattr(bigframes_obj, member) api_patterns.append( - [f"{name}.{member}", token, token_type, is_in_bigframes] + [ + f"{name}.{member}", + token, + token_type, + is_in_bigframes, + missing_parameters, + ] ) return pd.DataFrame(api_patterns, columns=header) @@ -165,14 +265,112 @@ def build_api_coverage_table(bigframes_version: str, release_version: str): return combined_df.infer_objects().convert_dtypes() +def format_api(api_names, is_in_bigframes, api_prefix): + api_names = api_names.str.slice(start=len(f"{api_prefix}.")) + formatted = "" + api_names + "" + url_prefix = URL_PREFIX.get(api_prefix) + if url_prefix is None: + return formatted + + linked = '' + formatted + "" + return formatted.mask(is_in_bigframes, linked) + + +def generate_api_coverage(df, api_prefix): + dataframe_apis = df.loc[df["api"].str.startswith(f"{api_prefix}.")] + fully_implemented = ( + dataframe_apis["missing_parameters"].str.len() == 0 + ) & dataframe_apis["is_in_bigframes"] + partial_implemented = ( + dataframe_apis["missing_parameters"].str.len() != 0 + ) & dataframe_apis["is_in_bigframes"] + not_implemented = ~dataframe_apis["is_in_bigframes"] + dataframe_table = pd.DataFrame( + { + "API": format_api( + dataframe_apis["api"], + dataframe_apis["is_in_bigframes"], + api_prefix, + ), + "Implemented": "", + "Missing parameters": dataframe_apis["missing_parameters"], + } + ) + dataframe_table.loc[fully_implemented, "Implemented"] = "Y" + dataframe_table.loc[partial_implemented, "Implemented"] = "P" + dataframe_table.loc[not_implemented, "Implemented"] = "N" + return dataframe_table + + +def generate_api_coverage_doc(df, api_prefix): + dataframe_table = generate_api_coverage(df, api_prefix) + dataframe_table = dataframe_table.loc[~(dataframe_table["Implemented"] == "N")] + dataframe_table["Implemented"] = dataframe_table["Implemented"].map( + { + "Y": "Y", + "P": "P", + } + ) + + with open( + REPO_ROOT / "docs" / "supported_pandas_apis" / f"bf_{api_prefix}.html", + "w", + ) as html_file: + dataframe_table.to_html( + html_file, index=False, header=True, escape=False, border=0, col_space="8em" + ) + + +def generate_api_coverage_docs(df): + for target in PANDAS_TARGETS: + api_prefix = target[0] + generate_api_coverage_doc(df, api_prefix) + + +def print_api_coverage_summary(df, api_prefix): + dataframe_table = generate_api_coverage(df, api_prefix) + + print(api_prefix) + print(dataframe_table[["Implemented", "API"]].groupby(["Implemented"]).count()) + print(f"{api_prefix} APIs: {dataframe_table.shape[0]}\n") + + +def print_api_coverage_summaries(df): + for target in PANDAS_TARGETS: + api_prefix = target[0] + print_api_coverage_summary(df, api_prefix) + + print(f"\nAll APIs: {len(df.index)}") + fully_implemented = (df["missing_parameters"].str.len() == 0) & df[ + "is_in_bigframes" + ] + print(f"Y: {fully_implemented.sum()}") + partial_implemented = (df["missing_parameters"].str.len() != 0) & df[ + "is_in_bigframes" + ] + print(f"P: {partial_implemented.sum()}") + not_implemented = ~df["is_in_bigframes"] + print(f"N: {not_implemented.sum()}") + + def main(): parser = argparse.ArgumentParser() - parser.add_argument("--bigframes_version") - parser.add_argument("--release_version") + parser.add_argument("output_type") + parser.add_argument("--bigframes_version", default=bigframes.__version__) + parser.add_argument("--release_version", default="") parser.add_argument("--bigquery_table_name") args = parser.parse_args() df = build_api_coverage_table(args.bigframes_version, args.release_version) - df.to_gbq(args.bigquery_table_name, if_exists="append") + + if args.output_type == "bigquery": + df.to_gbq(args.bigquery_table_name, if_exists="append") + elif args.output_type == "docs": + generate_api_coverage_docs(df) + elif args.output_type == "summary": + print_api_coverage_summaries(df) + else: + print(f"Unexpected output_type {repr(args.output_type)}") + sys.exit(1) if __name__ == "__main__": diff --git a/scripts/test_publish_api_coverage.py b/scripts/test_publish_api_coverage.py index 96b2d1bb48d..061cc1c25ce 100644 --- a/scripts/test_publish_api_coverage.py +++ b/scripts/test_publish_api_coverage.py @@ -27,6 +27,7 @@ def test_api_coverage_produces_expected_schema(): "string", "boolean", "string", + "string", "datetime64[ns]", "string", "string", @@ -36,6 +37,7 @@ def test_api_coverage_produces_expected_schema(): "pattern", "kind", "is_in_bigframes", + "missing_parameters", "module", "timestamp", "bigframes_version", From 347f2dda2298e17cd44a298f04a723f2d20c080a Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:08:16 -0700 Subject: [PATCH 450/921] fix: sync the notebook with embedding changes (#550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .../bq_dataframes_llm_kmeans.ipynb | 1074 +++++++++-------- 1 file changed, 547 insertions(+), 527 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 61445d85c5b..2c6d109ba8d 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -289,7 +289,7 @@ { "data": { "text/html": [ - "Query job d5778724-6966-42ba-b8a6-2a1865a1184c is DONE. 2.3 GB processed. Open Job" + "Query job 030e5d08-f690-47e4-b7cc-342731245575 is DONE. 2.3 GB processed. Open Job" ], "text/plain": [ "" @@ -301,7 +301,7 @@ { "data": { "text/html": [ - "Query job 4d48bf69-571c-4773-8486-0232840597d5 is DONE. 55.1 MB processed. Open Job" + "Query job a9c5f416-c5d2-4209-b639-bccb81a25d7e is DONE. 58.8 MB processed. Open Job" ], "text/plain": [ "" @@ -336,36 +336,36 @@ " \n", " \n", " \n", - " 24\n", - " I sent disputed to Transunion, XXXX and XXXX f...\n", + " 1053364\n", + " My Macy 's American Express account was taken ...\n", " \n", " \n", - " 942\n", - " on XX/XX/2017 I sent XXXX, transunion, XXXX pr...\n", + " 1053757\n", + " I am a victim of identity theft. The informati...\n", " \n", " \n", - " 1193\n", - " On Wednesday, XXXX XXXX , I initiated a wir...\n", + " 1053784\n", + " In XXXX 2016, Amex took out $ XXXX.+ unauthori...\n", " \n", " \n", - " 1292\n", - " Dear Sir or Madam, I am a victim of identity t...\n", + " 1054237\n", + " I am not for sure the exact date of my loan it...\n", " \n", " \n", - " 1377\n", - " For the purpose of this complaint, I will refe...\n", + " 1054244\n", + " I entered a consumer credit transaction with t...\n", " \n", " \n", "\n", "" ], "text/plain": [ - " consumer_complaint_narrative\n", - "24 I sent disputed to Transunion, XXXX and XXXX f...\n", - "942 on XX/XX/2017 I sent XXXX, transunion, XXXX pr...\n", - "1193 On Wednesday, XXXX XXXX , I initiated a wir...\n", - "1292 Dear Sir or Madam, I am a victim of identity t...\n", - "1377 For the purpose of this complaint, I will refe..." + " consumer_complaint_narrative\n", + "1053364 My Macy 's American Express account was taken ...\n", + "1053757 I am a victim of identity theft. The informati...\n", + "1053784 In XXXX 2016, Amex took out $ XXXX.+ unauthori...\n", + "1054237 I am not for sure the exact date of my loan it...\n", + "1054244 I entered a consumer credit transaction with t..." ] }, "execution_count": 7, @@ -418,7 +418,7 @@ { "data": { "text/html": [ - "Query job 15b352c2-783c-42b1-bc03-e5772f00381a is DONE. 0 Bytes processed. Open Job" + "Query job 77eee871-31eb-4939-a015-f5505c94786e is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -444,7 +444,7 @@ { "data": { "text/html": [ - "Query job e2152e81-b736-4a68-a25a-c5eb2b03d734 is DONE. 1.3 GB processed. Open Job" + "Query job 63cdd004-21b6-41bf-8876-aa646f1f268e is DONE. 1.3 GB processed. Open Job" ], "text/plain": [ "" @@ -456,7 +456,7 @@ { "data": { "text/html": [ - "Query job b1a3d20b-aee3-424c-a0c5-5b36f1177709 is DONE. 80.0 kB processed. Open Job" + "Query job cda12546-9931-48f6-8b22-74a9ab85fa28 is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -468,7 +468,7 @@ { "data": { "text/html": [ - "Query job 6b2fad50-cbc8-42ea-83c1-b5d3eaac10b9 is DONE. 20.0 kB processed. Open Job" + "Query job 759a13c5-c02f-4ae8-9b22-d7ef423ffe8d is DONE. 20.0 kB processed. Open Job" ], "text/plain": [ "" @@ -480,19 +480,7 @@ { "data": { "text/html": [ - "Query job 31896ae6-fbb5-42fb-98c4-13bd19d1adfa is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 43f04543-f59b-4f1b-8598-c529324904be is DONE. 72.1 MB processed. Open Job" + "Query job 1bad8ef3-8103-4a98-bec4-699d97673b9a is DONE. 72.0 MB processed. Open Job" ], "text/plain": [ "" @@ -522,187 +510,188 @@ " \n", " \n", " \n", - " text_embedding\n", - " statistics\n", - " ml_embed_text_status\n", + " ml_generate_embedding_result\n", + " ml_generate_embedding_statistics\n", + " ml_generate_embedding_status\n", " content\n", " \n", " \n", " \n", " \n", - " 545\n", - " [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...\n", - " {\"token_count\":178,\"truncated\":false}\n", + " 357\n", + " [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-...\n", + " {\"token_count\":306,\"truncated\":false}\n", " \n", - " My payments have been approximately {$89.00} w...\n", + " I decided to try XXXX services for my wife and...\n", " \n", " \n", - " 614\n", - " [ 5.40032536e-02 -5.28502129e-02 -5.33268750e-...\n", - " {\"token_count\":399,\"truncated\":false}\n", + " 428\n", + " [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-...\n", + " {\"token_count\":134,\"truncated\":false}\n", " \n", - " Hi, I have contacted Trans Union XXXX XXXX abo...\n", + " XXXX I went to the bank in question ( XXXX XXX...\n", " \n", " \n", - " 1236\n", - " [-5.32836001e-03 -5.84292673e-02 -5.86670786e-...\n", - " {\"token_count\":129,\"truncated\":false}\n", + " 1319\n", + " [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-...\n", + " {\"token_count\":215,\"truncated\":false}\n", " \n", - " I have a XXXX XXXX XXXX credit card on my Exp...\n", + " I currently have a home loan with my ex husban...\n", " \n", " \n", - " 1477\n", - " [ 3.02605387e-02 -4.37121317e-02 -2.70802993e-...\n", - " {\"token_count\":16,\"truncated\":false}\n", + " 1993\n", + " [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-...\n", + " {\"token_count\":536,\"truncated\":false}\n", " \n", - " Wrongs information, selling my information to ...\n", + " NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800....\n", " \n", " \n", - " 2261\n", - " [ 2.35723313e-02 -3.73509154e-02 -6.44604117e-...\n", - " {\"token_count\":33,\"truncated\":false}\n", + " 1997\n", + " [ 0.03145148 -0.01011822 -0.02316323 -0.025078...\n", + " {\"token_count\":123,\"truncated\":false}\n", " \n", - " Please investigate and delete disputed item th...\n", + " After a while the payments became harder and h...\n", " \n", " \n", - " 2361\n", - " [ 1.04440488e-02 -9.37070698e-03 -7.36323372e-...\n", - " {\"token_count\":45,\"truncated\":false}\n", + " 2469\n", + " [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-...\n", + " {\"token_count\":60,\"truncated\":false}\n", " \n", - " By the provisions of the Fair Credit Reporting...\n", + " In the course of my student loan, I have been ...\n", " \n", " \n", - " 2378\n", - " [ 3.04989032e-02 -4.08191867e-02 -6.18648790e-...\n", - " {\"token_count\":892,\"truncated\":false}\n", + " 2624\n", + " [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-...\n", + " {\"token_count\":254,\"truncated\":false}\n", " \n", - " Since XX/XX/XXXX I have been trying to dispute...\n", + " In accordance with the Fair Credit Reporting A...\n", " \n", " \n", - " 3133\n", - " [ 0.00152804 -0.04189068 -0.04220504 -0.053740...\n", - " {\"token_count\":90,\"truncated\":false}\n", + " 2832\n", + " [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-...\n", + " {\"token_count\":79,\"truncated\":false}\n", " \n", - " Out of the blue I received a debt collection n...\n", + " LVNV FUNDING LLC is continually placing a coll...\n", " \n", " \n", - " 3140\n", - " [ 3.11435573e-02 -4.44000624e-02 -2.10917685e-...\n", - " {\"token_count\":372,\"truncated\":false}\n", + " 3328\n", + " [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-...\n", + " {\"token_count\":156,\"truncated\":false}\n", " \n", - " My wife and I have been sending money to XXXX ...\n", + " On XX/XX/2020 I sent a letter regarding inaccu...\n", " \n", " \n", - " 3322\n", - " [ 2.75927987e-02 -6.23729872e-03 -3.83295454e-...\n", - " {\"token_count\":36,\"truncated\":false}\n", + " 3650\n", + " [-6.10093866e-03 -5.93599863e-02 -8.04531425e-...\n", + " {\"token_count\":175,\"truncated\":false}\n", " \n", - " Phone calls from Convergent Outsourcing XXXX. ...\n", + " Over a year and a half ago we started the proc...\n", " \n", " \n", - " 3583\n", - " [ 9.20385588e-03 -3.83387171e-02 -6.46291822e-...\n", - " {\"token_count\":52,\"truncated\":false}\n", + " 3860\n", + " [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-...\n", + " {\"token_count\":1267,\"truncated\":false}\n", " \n", - " I recently received a copy of my credit report...\n", + " The issue is 26 late payments on me and my wif...\n", " \n", " \n", - " 4134\n", - " [-7.04960374e-04 -3.52595337e-02 -1.65264793e-...\n", - " {\"token_count\":412,\"truncated\":false}\n", + " 4464\n", + " [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-...\n", + " {\"token_count\":906,\"truncated\":false}\n", " \n", - " I have been sending the creditor what they hav...\n", + " I purchased as replacement for a lost XXXX XXX...\n", " \n", " \n", - " 4496\n", - " [ 3.67735326e-02 1.21120387e-03 -5.20942472e-...\n", - " {\"token_count\":182,\"truncated\":false}\n", + " 4470\n", + " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", + " {\"token_count\":200,\"truncated\":false}\n", " \n", - " This is my second complaint. Their response to...\n", + " in accordance with the Fair Credit Reporting a...\n", " \n", " \n", - " 5260\n", - " [ 2.07133405e-02 -1.69602726e-02 -5.07124476e-...\n", - " {\"token_count\":103,\"truncated\":false}\n", + " 4567\n", + " [-5.49167022e-03 -3.84587422e-02 -8.56091827e-...\n", + " {\"token_count\":110,\"truncated\":false}\n", " \n", - " XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte...\n", + " I have submitted multiple disputes through the...\n", " \n", " \n", - " 5400\n", - " [ 1.44114876e-02 -2.34710164e-02 -6.58538565e-...\n", - " {\"token_count\":60,\"truncated\":false}\n", + " 4713\n", + " [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-...\n", + " {\"token_count\":549,\"truncated\":false}\n", " \n", - " Upon checking my XXXX credit report I noticed ...\n", + " While shopping for furniture for my home I ope...\n", " \n", " \n", - " 5425\n", - " [ 3.10326386e-02 -2.19427086e-02 -6.56386837e-...\n", - " {\"token_count\":87,\"truncated\":false}\n", + " 5181\n", + " [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-...\n", + " {\"token_count\":77,\"truncated\":false}\n", " \n", - " Follow up to previous complaint XXXX XXXX XXXX...\n", + " I had opened a Wells Fargo checking account wi...\n", " \n", " \n", - " 6014\n", - " [ 1.90773793e-02 -2.27493346e-02 -3.27166244e-...\n", - " {\"token_count\":175,\"truncated\":false}\n", + " 5511\n", + " [-0.00217485 -0.04031368 -0.06604777 -0.052006...\n", + " {\"token_count\":262,\"truncated\":false}\n", " \n", - " My new XXXX lease was over always paid on time...\n", + " I recently disputed ( see attached letter ) wi...\n", " \n", " \n", - " 8192\n", - " [ 0.01937891 -0.05466933 -0.06070872 -0.059028...\n", - " {\"token_count\":131,\"truncated\":false}\n", + " 5888\n", + " [-8.15972779e-03 -3.46563384e-02 -5.91776446e-...\n", + " {\"token_count\":176,\"truncated\":false}\n", " \n", - " I have no idea where this account cane from. B...\n", + " XXXX XXXX XXXX XXXX \n", + "I have disputed this acco...\n", " \n", " \n", - " 8240\n", - " [ 4.34123818e-03 -3.40953320e-02 -4.06381376e-...\n", - " {\"token_count\":87,\"truncated\":false}\n", + " 6299\n", + " [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-...\n", + " {\"token_count\":151,\"truncated\":false}\n", " \n", - " I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F...\n", + " XXXX ; XXXX and Transunion are reporting ( 30 ...\n", " \n", " \n", - " 8720\n", - " [ 0.03133732 -0.03972461 -0.00178199 -0.035876...\n", - " {\"token_count\":645,\"truncated\":false}\n", + " 7143\n", + " [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-...\n", + " {\"token_count\":234,\"truncated\":false}\n", " \n", - " XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum...\n", + " My Macys account is due on the first of every ...\n", " \n", " \n", - " 8914\n", - " [ 1.75969116e-02 -2.25022305e-02 -5.70390299e-...\n", - " {\"token_count\":180,\"truncated\":false}\n", + " 7219\n", + " [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-...\n", + " {\"token_count\":26,\"truncated\":false}\n", " \n", - " On XX/XX/21 I sent a letter regarding inaccura...\n", + " Keep getting letters and calls from collection...\n", " \n", " \n", - " 10021\n", - " [ 5.02460636e-02 -5.25112189e-02 -4.12914790e-...\n", - " {\"token_count\":30,\"truncated\":false}\n", + " 7574\n", + " [-0.00149564 -0.06619431 -0.05084481 -0.048579...\n", + " {\"token_count\":129,\"truncated\":false}\n", " \n", - " XX/XX/XXXX and XX/XX/XXXX inaccurate informati...\n", + " On XXXX I was on the XXXX app and there was a ...\n", " \n", " \n", - " 10327\n", - " [-0.00979626 -0.04912931 -0.08654705 -0.021063...\n", - " {\"token_count\":194,\"truncated\":false}\n", + " 8759\n", + " [ 0.01501553 -0.03575936 -0.050562 -0.034884...\n", + " {\"token_count\":501,\"truncated\":false}\n", " \n", - " When I reviewed my credit report, I discovered...\n", + " Obviously I've been a victim of fraud, therefo...\n", " \n", " \n", - " 10345\n", - " [-0.04292191 -0.02636929 -0.06177032 -0.076520...\n", - " {\"token_count\":262,\"truncated\":false}\n", + " 9700\n", + " [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-...\n", + " {\"token_count\":48,\"truncated\":false}\n", " \n", - " U.S. Bank sent two letters containing Visa Deb...\n", + " The following item have not been properly inve...\n", " \n", " \n", - " 10369\n", - " [ 2.16020197e-02 -5.62509745e-02 -5.93873672e-...\n", - " {\"token_count\":77,\"truncated\":false}\n", + " 9822\n", + " [ 2.95880195e-02 1.65440738e-02 -3.33247967e-...\n", + " {\"token_count\":2373,\"truncated\":true}\n", " \n", - " I requested from XXXX that they reverse the la...\n", + " During the housing market crash I went through...\n", " \n", " \n", "\n", @@ -710,86 +699,87 @@ "[10000 rows x 4 columns in total]" ], "text/plain": [ - " text_embedding \\\n", - "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", - "614 [ 5.40032536e-02 -5.28502129e-02 -5.33268750e-... \n", - "1236 [-5.32836001e-03 -5.84292673e-02 -5.86670786e-... \n", - "1477 [ 3.02605387e-02 -4.37121317e-02 -2.70802993e-... \n", - "2261 [ 2.35723313e-02 -3.73509154e-02 -6.44604117e-... \n", - "2361 [ 1.04440488e-02 -9.37070698e-03 -7.36323372e-... \n", - "2378 [ 3.04989032e-02 -4.08191867e-02 -6.18648790e-... \n", - "3133 [ 0.00152804 -0.04189068 -0.04220504 -0.053740... \n", - "3140 [ 3.11435573e-02 -4.44000624e-02 -2.10917685e-... \n", - "3322 [ 2.75927987e-02 -6.23729872e-03 -3.83295454e-... \n", - "3583 [ 9.20385588e-03 -3.83387171e-02 -6.46291822e-... \n", - "4134 [-7.04960374e-04 -3.52595337e-02 -1.65264793e-... \n", - "4496 [ 3.67735326e-02 1.21120387e-03 -5.20942472e-... \n", - "5260 [ 2.07133405e-02 -1.69602726e-02 -5.07124476e-... \n", - "5400 [ 1.44114876e-02 -2.34710164e-02 -6.58538565e-... \n", - "5425 [ 3.10326386e-02 -2.19427086e-02 -6.56386837e-... \n", - "6014 [ 1.90773793e-02 -2.27493346e-02 -3.27166244e-... \n", - "8192 [ 0.01937891 -0.05466933 -0.06070872 -0.059028... \n", - "8240 [ 4.34123818e-03 -3.40953320e-02 -4.06381376e-... \n", - "8720 [ 0.03133732 -0.03972461 -0.00178199 -0.035876... \n", - "8914 [ 1.75969116e-02 -2.25022305e-02 -5.70390299e-... \n", - "10021 [ 5.02460636e-02 -5.25112189e-02 -4.12914790e-... \n", - "10327 [-0.00979626 -0.04912931 -0.08654705 -0.021063... \n", - "10345 [-0.04292191 -0.02636929 -0.06177032 -0.076520... \n", - "10369 [ 2.16020197e-02 -5.62509745e-02 -5.93873672e-... \n", + " ml_generate_embedding_result \\\n", + "357 [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-... \n", + "428 [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-... \n", + "1319 [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-... \n", + "1993 [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-... \n", + "1997 [ 0.03145148 -0.01011822 -0.02316323 -0.025078... \n", + "2469 [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-... \n", + "2624 [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-... \n", + "2832 [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-... \n", + "3328 [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-... \n", + "3650 [-6.10093866e-03 -5.93599863e-02 -8.04531425e-... \n", + "3860 [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-... \n", + "4464 [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-... \n", + "4470 [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-... \n", + "4567 [-5.49167022e-03 -3.84587422e-02 -8.56091827e-... \n", + "4713 [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-... \n", + "5181 [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-... \n", + "5511 [-0.00217485 -0.04031368 -0.06604777 -0.052006... \n", + "5888 [-8.15972779e-03 -3.46563384e-02 -5.91776446e-... \n", + "6299 [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-... \n", + "7143 [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-... \n", + "7219 [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-... \n", + "7574 [-0.00149564 -0.06619431 -0.05084481 -0.048579... \n", + "8759 [ 0.01501553 -0.03575936 -0.050562 -0.034884... \n", + "9700 [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-... \n", + "9822 [ 2.95880195e-02 1.65440738e-02 -3.33247967e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "545 {\"token_count\":178,\"truncated\":false} \n", - "614 {\"token_count\":399,\"truncated\":false} \n", - "1236 {\"token_count\":129,\"truncated\":false} \n", - "1477 {\"token_count\":16,\"truncated\":false} \n", - "2261 {\"token_count\":33,\"truncated\":false} \n", - "2361 {\"token_count\":45,\"truncated\":false} \n", - "2378 {\"token_count\":892,\"truncated\":false} \n", - "3133 {\"token_count\":90,\"truncated\":false} \n", - "3140 {\"token_count\":372,\"truncated\":false} \n", - "3322 {\"token_count\":36,\"truncated\":false} \n", - "3583 {\"token_count\":52,\"truncated\":false} \n", - "4134 {\"token_count\":412,\"truncated\":false} \n", - "4496 {\"token_count\":182,\"truncated\":false} \n", - "5260 {\"token_count\":103,\"truncated\":false} \n", - "5400 {\"token_count\":60,\"truncated\":false} \n", - "5425 {\"token_count\":87,\"truncated\":false} \n", - "6014 {\"token_count\":175,\"truncated\":false} \n", - "8192 {\"token_count\":131,\"truncated\":false} \n", - "8240 {\"token_count\":87,\"truncated\":false} \n", - "8720 {\"token_count\":645,\"truncated\":false} \n", - "8914 {\"token_count\":180,\"truncated\":false} \n", - "10021 {\"token_count\":30,\"truncated\":false} \n", - "10327 {\"token_count\":194,\"truncated\":false} \n", - "10345 {\"token_count\":262,\"truncated\":false} \n", - "10369 {\"token_count\":77,\"truncated\":false} \n", + " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", + "357 {\"token_count\":306,\"truncated\":false} \n", + "428 {\"token_count\":134,\"truncated\":false} \n", + "1319 {\"token_count\":215,\"truncated\":false} \n", + "1993 {\"token_count\":536,\"truncated\":false} \n", + "1997 {\"token_count\":123,\"truncated\":false} \n", + "2469 {\"token_count\":60,\"truncated\":false} \n", + "2624 {\"token_count\":254,\"truncated\":false} \n", + "2832 {\"token_count\":79,\"truncated\":false} \n", + "3328 {\"token_count\":156,\"truncated\":false} \n", + "3650 {\"token_count\":175,\"truncated\":false} \n", + "3860 {\"token_count\":1267,\"truncated\":false} \n", + "4464 {\"token_count\":906,\"truncated\":false} \n", + "4470 {\"token_count\":200,\"truncated\":false} \n", + "4567 {\"token_count\":110,\"truncated\":false} \n", + "4713 {\"token_count\":549,\"truncated\":false} \n", + "5181 {\"token_count\":77,\"truncated\":false} \n", + "5511 {\"token_count\":262,\"truncated\":false} \n", + "5888 {\"token_count\":176,\"truncated\":false} \n", + "6299 {\"token_count\":151,\"truncated\":false} \n", + "7143 {\"token_count\":234,\"truncated\":false} \n", + "7219 {\"token_count\":26,\"truncated\":false} \n", + "7574 {\"token_count\":129,\"truncated\":false} \n", + "8759 {\"token_count\":501,\"truncated\":false} \n", + "9700 {\"token_count\":48,\"truncated\":false} \n", + "9822 {\"token_count\":2373,\"truncated\":true} \n", "\n", - " content \n", - "545 My payments have been approximately {$89.00} w... \n", - "614 Hi, I have contacted Trans Union XXXX XXXX abo... \n", - "1236 I have a XXXX XXXX XXXX credit card on my Exp... \n", - "1477 Wrongs information, selling my information to ... \n", - "2261 Please investigate and delete disputed item th... \n", - "2361 By the provisions of the Fair Credit Reporting... \n", - "2378 Since XX/XX/XXXX I have been trying to dispute... \n", - "3133 Out of the blue I received a debt collection n... \n", - "3140 My wife and I have been sending money to XXXX ... \n", - "3322 Phone calls from Convergent Outsourcing XXXX. ... \n", - "3583 I recently received a copy of my credit report... \n", - "4134 I have been sending the creditor what they hav... \n", - "4496 This is my second complaint. Their response to... \n", - "5260 XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte... \n", - "5400 Upon checking my XXXX credit report I noticed ... \n", - "5425 Follow up to previous complaint XXXX XXXX XXXX... \n", - "6014 My new XXXX lease was over always paid on time... \n", - "8192 I have no idea where this account cane from. B... \n", - "8240 I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F... \n", - "8720 XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum... \n", - "8914 On XX/XX/21 I sent a letter regarding inaccura... \n", - "10021 XX/XX/XXXX and XX/XX/XXXX inaccurate informati... \n", - "10327 When I reviewed my credit report, I discovered... \n", - "10345 U.S. Bank sent two letters containing Visa Deb... \n", - "10369 I requested from XXXX that they reverse the la... \n", + " content \n", + "357 I decided to try XXXX services for my wife and... \n", + "428 XXXX I went to the bank in question ( XXXX XXX... \n", + "1319 I currently have a home loan with my ex husban... \n", + "1993 NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800.... \n", + "1997 After a while the payments became harder and h... \n", + "2469 In the course of my student loan, I have been ... \n", + "2624 In accordance with the Fair Credit Reporting A... \n", + "2832 LVNV FUNDING LLC is continually placing a coll... \n", + "3328 On XX/XX/2020 I sent a letter regarding inaccu... \n", + "3650 Over a year and a half ago we started the proc... \n", + "3860 The issue is 26 late payments on me and my wif... \n", + "4464 I purchased as replacement for a lost XXXX XXX... \n", + "4470 in accordance with the Fair Credit Reporting a... \n", + "4567 I have submitted multiple disputes through the... \n", + "4713 While shopping for furniture for my home I ope... \n", + "5181 I had opened a Wells Fargo checking account wi... \n", + "5511 I recently disputed ( see attached letter ) wi... \n", + "5888 XXXX XXXX XXXX XXXX \n", + "I have disputed this acco... \n", + "6299 XXXX ; XXXX and Transunion are reporting ( 30 ... \n", + "7143 My Macys account is due on the first of every ... \n", + "7219 Keep getting letters and calls from collection... \n", + "7574 On XXXX I was on the XXXX app and there was a ... \n", + "8759 Obviously I've been a victim of fraud, therefo... \n", + "9700 The following item have not been properly inve... \n", + "9822 During the housing market crash I went through... \n", "...\n", "\n", "[10000 rows x 4 columns]" @@ -822,19 +812,7 @@ { "data": { "text/html": [ - "Query job c78e1040-2a57-42f6-8fdb-5b9524846259 is DONE. 72.1 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 0986541b-3941-4387-b813-8888f53d149e is DONE. 0 Bytes processed. Open Job" + "Query job b4594edf-80e5-4476-ac06-b799001f4cb0 is DONE. 72.0 MB processed. Open Job" ], "text/plain": [ "" @@ -846,7 +824,7 @@ { "data": { "text/html": [ - "Query job 754aadd2-fee6-495c-acef-506f4e13c062 is DONE. 72.6 MB processed. Open Job" + "Query job 417e806a-2574-4b1b-8276-a95fa2df56e1 is DONE. 72.5 MB processed. Open Job" ], "text/plain": [ "" @@ -876,187 +854,188 @@ " \n", " \n", " \n", - " text_embedding\n", - " statistics\n", - " ml_embed_text_status\n", + " ml_generate_embedding_result\n", + " ml_generate_embedding_statistics\n", + " ml_generate_embedding_status\n", " content\n", " \n", " \n", " \n", " \n", - " 545\n", - " [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-...\n", - " {\"token_count\":178,\"truncated\":false}\n", + " 357\n", + " [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-...\n", + " {\"token_count\":306,\"truncated\":false}\n", " \n", - " My payments have been approximately {$89.00} w...\n", + " I decided to try XXXX services for my wife and...\n", " \n", " \n", - " 614\n", - " [ 5.40032536e-02 -5.28502129e-02 -5.33268750e-...\n", - " {\"token_count\":399,\"truncated\":false}\n", + " 428\n", + " [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-...\n", + " {\"token_count\":134,\"truncated\":false}\n", " \n", - " Hi, I have contacted Trans Union XXXX XXXX abo...\n", + " XXXX I went to the bank in question ( XXXX XXX...\n", " \n", " \n", - " 1236\n", - " [-5.32836001e-03 -5.84292673e-02 -5.86670786e-...\n", - " {\"token_count\":129,\"truncated\":false}\n", + " 1319\n", + " [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-...\n", + " {\"token_count\":215,\"truncated\":false}\n", " \n", - " I have a XXXX XXXX XXXX credit card on my Exp...\n", + " I currently have a home loan with my ex husban...\n", " \n", " \n", - " 1477\n", - " [ 3.02605387e-02 -4.37121317e-02 -2.70802993e-...\n", - " {\"token_count\":16,\"truncated\":false}\n", + " 1993\n", + " [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-...\n", + " {\"token_count\":536,\"truncated\":false}\n", " \n", - " Wrongs information, selling my information to ...\n", + " NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800....\n", " \n", " \n", - " 2261\n", - " [ 2.35723313e-02 -3.73509154e-02 -6.44604117e-...\n", - " {\"token_count\":33,\"truncated\":false}\n", + " 1997\n", + " [ 0.03145148 -0.01011822 -0.02316323 -0.025078...\n", + " {\"token_count\":123,\"truncated\":false}\n", " \n", - " Please investigate and delete disputed item th...\n", + " After a while the payments became harder and h...\n", " \n", " \n", - " 2361\n", - " [ 1.04440488e-02 -9.37070698e-03 -7.36323372e-...\n", - " {\"token_count\":45,\"truncated\":false}\n", + " 2469\n", + " [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-...\n", + " {\"token_count\":60,\"truncated\":false}\n", " \n", - " By the provisions of the Fair Credit Reporting...\n", + " In the course of my student loan, I have been ...\n", " \n", " \n", - " 2378\n", - " [ 3.04989032e-02 -4.08191867e-02 -6.18648790e-...\n", - " {\"token_count\":892,\"truncated\":false}\n", + " 2624\n", + " [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-...\n", + " {\"token_count\":254,\"truncated\":false}\n", " \n", - " Since XX/XX/XXXX I have been trying to dispute...\n", + " In accordance with the Fair Credit Reporting A...\n", " \n", " \n", - " 3133\n", - " [ 0.00152804 -0.04189068 -0.04220504 -0.053740...\n", - " {\"token_count\":90,\"truncated\":false}\n", + " 2832\n", + " [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-...\n", + " {\"token_count\":79,\"truncated\":false}\n", " \n", - " Out of the blue I received a debt collection n...\n", + " LVNV FUNDING LLC is continually placing a coll...\n", " \n", " \n", - " 3140\n", - " [ 3.11435573e-02 -4.44000624e-02 -2.10917685e-...\n", - " {\"token_count\":372,\"truncated\":false}\n", + " 3328\n", + " [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-...\n", + " {\"token_count\":156,\"truncated\":false}\n", " \n", - " My wife and I have been sending money to XXXX ...\n", + " On XX/XX/2020 I sent a letter regarding inaccu...\n", " \n", " \n", - " 3322\n", - " [ 2.75927987e-02 -6.23729872e-03 -3.83295454e-...\n", - " {\"token_count\":36,\"truncated\":false}\n", + " 3650\n", + " [-6.10093866e-03 -5.93599863e-02 -8.04531425e-...\n", + " {\"token_count\":175,\"truncated\":false}\n", " \n", - " Phone calls from Convergent Outsourcing XXXX. ...\n", + " Over a year and a half ago we started the proc...\n", " \n", " \n", - " 3583\n", - " [ 9.20385588e-03 -3.83387171e-02 -6.46291822e-...\n", - " {\"token_count\":52,\"truncated\":false}\n", + " 3860\n", + " [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-...\n", + " {\"token_count\":1267,\"truncated\":false}\n", " \n", - " I recently received a copy of my credit report...\n", + " The issue is 26 late payments on me and my wif...\n", " \n", " \n", - " 4134\n", - " [-7.04960374e-04 -3.52595337e-02 -1.65264793e-...\n", - " {\"token_count\":412,\"truncated\":false}\n", + " 4464\n", + " [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-...\n", + " {\"token_count\":906,\"truncated\":false}\n", " \n", - " I have been sending the creditor what they hav...\n", + " I purchased as replacement for a lost XXXX XXX...\n", " \n", " \n", - " 4496\n", - " [ 3.67735326e-02 1.21120387e-03 -5.20942472e-...\n", - " {\"token_count\":182,\"truncated\":false}\n", + " 4470\n", + " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", + " {\"token_count\":200,\"truncated\":false}\n", " \n", - " This is my second complaint. Their response to...\n", + " in accordance with the Fair Credit Reporting a...\n", " \n", " \n", - " 5260\n", - " [ 2.07133405e-02 -1.69602726e-02 -5.07124476e-...\n", - " {\"token_count\":103,\"truncated\":false}\n", + " 4567\n", + " [-5.49167022e-03 -3.84587422e-02 -8.56091827e-...\n", + " {\"token_count\":110,\"truncated\":false}\n", " \n", - " XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte...\n", + " I have submitted multiple disputes through the...\n", " \n", " \n", - " 5400\n", - " [ 1.44114876e-02 -2.34710164e-02 -6.58538565e-...\n", - " {\"token_count\":60,\"truncated\":false}\n", + " 4713\n", + " [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-...\n", + " {\"token_count\":549,\"truncated\":false}\n", " \n", - " Upon checking my XXXX credit report I noticed ...\n", + " While shopping for furniture for my home I ope...\n", " \n", " \n", - " 5425\n", - " [ 3.10326386e-02 -2.19427086e-02 -6.56386837e-...\n", - " {\"token_count\":87,\"truncated\":false}\n", + " 5181\n", + " [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-...\n", + " {\"token_count\":77,\"truncated\":false}\n", " \n", - " Follow up to previous complaint XXXX XXXX XXXX...\n", + " I had opened a Wells Fargo checking account wi...\n", " \n", " \n", - " 6014\n", - " [ 1.90773793e-02 -2.27493346e-02 -3.27166244e-...\n", - " {\"token_count\":175,\"truncated\":false}\n", + " 5511\n", + " [-0.00217485 -0.04031368 -0.06604777 -0.052006...\n", + " {\"token_count\":262,\"truncated\":false}\n", " \n", - " My new XXXX lease was over always paid on time...\n", + " I recently disputed ( see attached letter ) wi...\n", " \n", " \n", - " 8192\n", - " [ 0.01937891 -0.05466933 -0.06070872 -0.059028...\n", - " {\"token_count\":131,\"truncated\":false}\n", + " 5888\n", + " [-8.15972779e-03 -3.46563384e-02 -5.91776446e-...\n", + " {\"token_count\":176,\"truncated\":false}\n", " \n", - " I have no idea where this account cane from. B...\n", + " XXXX XXXX XXXX XXXX \n", + "I have disputed this acco...\n", " \n", " \n", - " 8240\n", - " [ 4.34123818e-03 -3.40953320e-02 -4.06381376e-...\n", - " {\"token_count\":87,\"truncated\":false}\n", + " 6299\n", + " [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-...\n", + " {\"token_count\":151,\"truncated\":false}\n", " \n", - " I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F...\n", + " XXXX ; XXXX and Transunion are reporting ( 30 ...\n", " \n", " \n", - " 8720\n", - " [ 0.03133732 -0.03972461 -0.00178199 -0.035876...\n", - " {\"token_count\":645,\"truncated\":false}\n", + " 7143\n", + " [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-...\n", + " {\"token_count\":234,\"truncated\":false}\n", " \n", - " XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum...\n", + " My Macys account is due on the first of every ...\n", " \n", " \n", - " 8914\n", - " [ 1.75969116e-02 -2.25022305e-02 -5.70390299e-...\n", - " {\"token_count\":180,\"truncated\":false}\n", + " 7219\n", + " [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-...\n", + " {\"token_count\":26,\"truncated\":false}\n", " \n", - " On XX/XX/21 I sent a letter regarding inaccura...\n", + " Keep getting letters and calls from collection...\n", " \n", " \n", - " 10021\n", - " [ 5.02460636e-02 -5.25112189e-02 -4.12914790e-...\n", - " {\"token_count\":30,\"truncated\":false}\n", + " 7574\n", + " [-0.00149564 -0.06619431 -0.05084481 -0.048579...\n", + " {\"token_count\":129,\"truncated\":false}\n", " \n", - " XX/XX/XXXX and XX/XX/XXXX inaccurate informati...\n", + " On XXXX I was on the XXXX app and there was a ...\n", " \n", " \n", - " 10327\n", - " [-0.00979626 -0.04912931 -0.08654705 -0.021063...\n", - " {\"token_count\":194,\"truncated\":false}\n", + " 8759\n", + " [ 0.01501553 -0.03575936 -0.050562 -0.034884...\n", + " {\"token_count\":501,\"truncated\":false}\n", " \n", - " When I reviewed my credit report, I discovered...\n", + " Obviously I've been a victim of fraud, therefo...\n", " \n", " \n", - " 10345\n", - " [-0.04292191 -0.02636929 -0.06177032 -0.076520...\n", - " {\"token_count\":262,\"truncated\":false}\n", + " 9700\n", + " [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-...\n", + " {\"token_count\":48,\"truncated\":false}\n", " \n", - " U.S. Bank sent two letters containing Visa Deb...\n", + " The following item have not been properly inve...\n", " \n", " \n", - " 10369\n", - " [ 2.16020197e-02 -5.62509745e-02 -5.93873672e-...\n", - " {\"token_count\":77,\"truncated\":false}\n", + " 9822\n", + " [ 2.95880195e-02 1.65440738e-02 -3.33247967e-...\n", + " {\"token_count\":2373,\"truncated\":true}\n", " \n", - " I requested from XXXX that they reverse the la...\n", + " During the housing market crash I went through...\n", " \n", " \n", "\n", @@ -1064,86 +1043,87 @@ "[10000 rows x 4 columns in total]" ], "text/plain": [ - " text_embedding \\\n", - "545 [ 1.82510037e-02 -1.27867460e-02 -1.57095697e-... \n", - "614 [ 5.40032536e-02 -5.28502129e-02 -5.33268750e-... \n", - "1236 [-5.32836001e-03 -5.84292673e-02 -5.86670786e-... \n", - "1477 [ 3.02605387e-02 -4.37121317e-02 -2.70802993e-... \n", - "2261 [ 2.35723313e-02 -3.73509154e-02 -6.44604117e-... \n", - "2361 [ 1.04440488e-02 -9.37070698e-03 -7.36323372e-... \n", - "2378 [ 3.04989032e-02 -4.08191867e-02 -6.18648790e-... \n", - "3133 [ 0.00152804 -0.04189068 -0.04220504 -0.053740... \n", - "3140 [ 3.11435573e-02 -4.44000624e-02 -2.10917685e-... \n", - "3322 [ 2.75927987e-02 -6.23729872e-03 -3.83295454e-... \n", - "3583 [ 9.20385588e-03 -3.83387171e-02 -6.46291822e-... \n", - "4134 [-7.04960374e-04 -3.52595337e-02 -1.65264793e-... \n", - "4496 [ 3.67735326e-02 1.21120387e-03 -5.20942472e-... \n", - "5260 [ 2.07133405e-02 -1.69602726e-02 -5.07124476e-... \n", - "5400 [ 1.44114876e-02 -2.34710164e-02 -6.58538565e-... \n", - "5425 [ 3.10326386e-02 -2.19427086e-02 -6.56386837e-... \n", - "6014 [ 1.90773793e-02 -2.27493346e-02 -3.27166244e-... \n", - "8192 [ 0.01937891 -0.05466933 -0.06070872 -0.059028... \n", - "8240 [ 4.34123818e-03 -3.40953320e-02 -4.06381376e-... \n", - "8720 [ 0.03133732 -0.03972461 -0.00178199 -0.035876... \n", - "8914 [ 1.75969116e-02 -2.25022305e-02 -5.70390299e-... \n", - "10021 [ 5.02460636e-02 -5.25112189e-02 -4.12914790e-... \n", - "10327 [-0.00979626 -0.04912931 -0.08654705 -0.021063... \n", - "10345 [-0.04292191 -0.02636929 -0.06177032 -0.076520... \n", - "10369 [ 2.16020197e-02 -5.62509745e-02 -5.93873672e-... \n", + " ml_generate_embedding_result \\\n", + "357 [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-... \n", + "428 [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-... \n", + "1319 [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-... \n", + "1993 [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-... \n", + "1997 [ 0.03145148 -0.01011822 -0.02316323 -0.025078... \n", + "2469 [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-... \n", + "2624 [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-... \n", + "2832 [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-... \n", + "3328 [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-... \n", + "3650 [-6.10093866e-03 -5.93599863e-02 -8.04531425e-... \n", + "3860 [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-... \n", + "4464 [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-... \n", + "4470 [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-... \n", + "4567 [-5.49167022e-03 -3.84587422e-02 -8.56091827e-... \n", + "4713 [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-... \n", + "5181 [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-... \n", + "5511 [-0.00217485 -0.04031368 -0.06604777 -0.052006... \n", + "5888 [-8.15972779e-03 -3.46563384e-02 -5.91776446e-... \n", + "6299 [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-... \n", + "7143 [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-... \n", + "7219 [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-... \n", + "7574 [-0.00149564 -0.06619431 -0.05084481 -0.048579... \n", + "8759 [ 0.01501553 -0.03575936 -0.050562 -0.034884... \n", + "9700 [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-... \n", + "9822 [ 2.95880195e-02 1.65440738e-02 -3.33247967e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "545 {\"token_count\":178,\"truncated\":false} \n", - "614 {\"token_count\":399,\"truncated\":false} \n", - "1236 {\"token_count\":129,\"truncated\":false} \n", - "1477 {\"token_count\":16,\"truncated\":false} \n", - "2261 {\"token_count\":33,\"truncated\":false} \n", - "2361 {\"token_count\":45,\"truncated\":false} \n", - "2378 {\"token_count\":892,\"truncated\":false} \n", - "3133 {\"token_count\":90,\"truncated\":false} \n", - "3140 {\"token_count\":372,\"truncated\":false} \n", - "3322 {\"token_count\":36,\"truncated\":false} \n", - "3583 {\"token_count\":52,\"truncated\":false} \n", - "4134 {\"token_count\":412,\"truncated\":false} \n", - "4496 {\"token_count\":182,\"truncated\":false} \n", - "5260 {\"token_count\":103,\"truncated\":false} \n", - "5400 {\"token_count\":60,\"truncated\":false} \n", - "5425 {\"token_count\":87,\"truncated\":false} \n", - "6014 {\"token_count\":175,\"truncated\":false} \n", - "8192 {\"token_count\":131,\"truncated\":false} \n", - "8240 {\"token_count\":87,\"truncated\":false} \n", - "8720 {\"token_count\":645,\"truncated\":false} \n", - "8914 {\"token_count\":180,\"truncated\":false} \n", - "10021 {\"token_count\":30,\"truncated\":false} \n", - "10327 {\"token_count\":194,\"truncated\":false} \n", - "10345 {\"token_count\":262,\"truncated\":false} \n", - "10369 {\"token_count\":77,\"truncated\":false} \n", + " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", + "357 {\"token_count\":306,\"truncated\":false} \n", + "428 {\"token_count\":134,\"truncated\":false} \n", + "1319 {\"token_count\":215,\"truncated\":false} \n", + "1993 {\"token_count\":536,\"truncated\":false} \n", + "1997 {\"token_count\":123,\"truncated\":false} \n", + "2469 {\"token_count\":60,\"truncated\":false} \n", + "2624 {\"token_count\":254,\"truncated\":false} \n", + "2832 {\"token_count\":79,\"truncated\":false} \n", + "3328 {\"token_count\":156,\"truncated\":false} \n", + "3650 {\"token_count\":175,\"truncated\":false} \n", + "3860 {\"token_count\":1267,\"truncated\":false} \n", + "4464 {\"token_count\":906,\"truncated\":false} \n", + "4470 {\"token_count\":200,\"truncated\":false} \n", + "4567 {\"token_count\":110,\"truncated\":false} \n", + "4713 {\"token_count\":549,\"truncated\":false} \n", + "5181 {\"token_count\":77,\"truncated\":false} \n", + "5511 {\"token_count\":262,\"truncated\":false} \n", + "5888 {\"token_count\":176,\"truncated\":false} \n", + "6299 {\"token_count\":151,\"truncated\":false} \n", + "7143 {\"token_count\":234,\"truncated\":false} \n", + "7219 {\"token_count\":26,\"truncated\":false} \n", + "7574 {\"token_count\":129,\"truncated\":false} \n", + "8759 {\"token_count\":501,\"truncated\":false} \n", + "9700 {\"token_count\":48,\"truncated\":false} \n", + "9822 {\"token_count\":2373,\"truncated\":true} \n", "\n", - " content \n", - "545 My payments have been approximately {$89.00} w... \n", - "614 Hi, I have contacted Trans Union XXXX XXXX abo... \n", - "1236 I have a XXXX XXXX XXXX credit card on my Exp... \n", - "1477 Wrongs information, selling my information to ... \n", - "2261 Please investigate and delete disputed item th... \n", - "2361 By the provisions of the Fair Credit Reporting... \n", - "2378 Since XX/XX/XXXX I have been trying to dispute... \n", - "3133 Out of the blue I received a debt collection n... \n", - "3140 My wife and I have been sending money to XXXX ... \n", - "3322 Phone calls from Convergent Outsourcing XXXX. ... \n", - "3583 I recently received a copy of my credit report... \n", - "4134 I have been sending the creditor what they hav... \n", - "4496 This is my second complaint. Their response to... \n", - "5260 XX/XX/XXXX and XX/XX/XXXX, {$3200.00} contacte... \n", - "5400 Upon checking my XXXX credit report I noticed ... \n", - "5425 Follow up to previous complaint XXXX XXXX XXXX... \n", - "6014 My new XXXX lease was over always paid on time... \n", - "8192 I have no idea where this account cane from. B... \n", - "8240 I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL F... \n", - "8720 XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consum... \n", - "8914 On XX/XX/21 I sent a letter regarding inaccura... \n", - "10021 XX/XX/XXXX and XX/XX/XXXX inaccurate informati... \n", - "10327 When I reviewed my credit report, I discovered... \n", - "10345 U.S. Bank sent two letters containing Visa Deb... \n", - "10369 I requested from XXXX that they reverse the la... \n", + " content \n", + "357 I decided to try XXXX services for my wife and... \n", + "428 XXXX I went to the bank in question ( XXXX XXX... \n", + "1319 I currently have a home loan with my ex husban... \n", + "1993 NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800.... \n", + "1997 After a while the payments became harder and h... \n", + "2469 In the course of my student loan, I have been ... \n", + "2624 In accordance with the Fair Credit Reporting A... \n", + "2832 LVNV FUNDING LLC is continually placing a coll... \n", + "3328 On XX/XX/2020 I sent a letter regarding inaccu... \n", + "3650 Over a year and a half ago we started the proc... \n", + "3860 The issue is 26 late payments on me and my wif... \n", + "4464 I purchased as replacement for a lost XXXX XXX... \n", + "4470 in accordance with the Fair Credit Reporting a... \n", + "4567 I have submitted multiple disputes through the... \n", + "4713 While shopping for furniture for my home I ope... \n", + "5181 I had opened a Wells Fargo checking account wi... \n", + "5511 I recently disputed ( see attached letter ) wi... \n", + "5888 XXXX XXXX XXXX XXXX \n", + "I have disputed this acco... \n", + "6299 XXXX ; XXXX and Transunion are reporting ( 30 ... \n", + "7143 My Macys account is due on the first of every ... \n", + "7219 Keep getting letters and calls from collection... \n", + "7574 On XXXX I was on the XXXX app and there was a ... \n", + "8759 Obviously I've been a victim of fraud, therefo... \n", + "9700 The following item have not been properly inve... \n", + "9822 During the housing market crash I went through... \n", "...\n", "\n", "[10000 rows x 4 columns]" @@ -1156,10 +1136,10 @@ ], "source": [ "successful_rows = (\n", - " (predicted_embeddings[\"ml_embed_text_status\"] == \"\")\n", + " (predicted_embeddings[\"ml_generate_embedding_status\"] == \"\")\n", " # Series.str.len() gives the length of an array.\n", " # See: https://stackoverflow.com/a/41340543/101923\n", - " & (predicted_embeddings[\"text_embedding\"].str.len() != 0)\n", + " & (predicted_embeddings[\"ml_generate_embedding_result\"].str.len() != 0)\n", ")\n", "predicted_embeddings = predicted_embeddings[successful_rows]\n", "predicted_embeddings\n" @@ -1214,7 +1194,7 @@ { "data": { "text/html": [ - "Query job fa4bbc13-3831-4c80-9b59-9939e605ed58 is DONE. 61.7 MB processed. Open Job" + "Query job 18aa46ee-0b10-4912-ae14-87b7e81ee447 is DONE. 61.7 MB processed. Open Job" ], "text/plain": [ "" @@ -1226,7 +1206,7 @@ { "data": { "text/html": [ - "Query job d2d681aa-e49a-4fda-89fd-60cf906d3aec is DONE. 0 Bytes processed. Open Job" + "Query job fd573f97-2424-472a-969d-463f184967d9 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1238,7 +1218,7 @@ { "data": { "text/html": [ - "Query job 234bb6be-625c-4c96-baea-c37c33410114 is DONE. 72.7 MB processed. Open Job" + "Query job 9f2e0a3f-d7d6-4fb8-b558-95f39235410d is DONE. 72.7 MB processed. Open Job" ], "text/plain": [ "" @@ -1250,7 +1230,7 @@ { "data": { "text/html": [ - "Query job 285817cb-99d3-426f-82c3-89d36119e8db is DONE. 80.0 kB processed. Open Job" + "Query job 786ababe-7c40-426f-bb39-154329e4c51a is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -1262,7 +1242,7 @@ { "data": { "text/html": [ - "Query job 3a39d2b0-55a1-4922-972a-8806b387f877 is DONE. 73.3 MB processed. Open Job" + "Query job a191fc97-baa6-4c7c-b78f-4365678caa60 is DONE. 73.2 MB processed. Open Job" ], "text/plain": [ "" @@ -1294,57 +1274,57 @@ " \n", " CENTROID_ID\n", " NEAREST_CENTROIDS_DISTANCE\n", - " text_embedding\n", - " statistics\n", - " ml_embed_text_status\n", + " ml_generate_embedding_result\n", + " ml_generate_embedding_statistics\n", + " ml_generate_embedding_status\n", " content\n", " \n", " \n", " \n", " \n", - " 182250\n", + " 1244571\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.570560301900...\n", - " [ 4.70298417e-02 -4.08669300e-02 -2.99868709e-...\n", - " {\"token_count\":10,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.414497263076...\n", + " [ 1.10590272e-02 -2.11433582e-02 -5.66212423e-...\n", + " {\"token_count\":100,\"truncated\":false}\n", " \n", - " These are not my accounts. Please remove them.\n", + " Ive disputed two Bankruptcies that still exist...\n", " \n", " \n", - " 3023485\n", + " 744390\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.523572693768...\n", - " [ 1.55437263e-02 -1.93240177e-02 -2.48466972e-...\n", - " {\"token_count\":10,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.416584344032...\n", + " [ 4.15011719e-02 -4.50705849e-02 -7.35541508e-...\n", + " {\"token_count\":100,\"truncated\":false}\n", " \n", - " This debt is not mine due to identity theft.\n", + " The XXXX account was settled as a class action...\n", " \n", " \n", - " 407254\n", + " 127514\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.515173566816...\n", - " [-0.01293471 -0.01959546 -0.02238463 -0.066214...\n", - " {\"token_count\":10,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.541137734253...\n", + " [ 3.54415141e-02 1.23769706e-02 -2.61783414e-...\n", + " {\"token_count\":100,\"truncated\":false}\n", " \n", - " I do not owe this company money!!!!!\n", + " I have late payments reported on my student lo...\n", " \n", " \n", - " 1509454\n", + " 630563\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.645342721754...\n", - " [ 3.21860723e-02 -2.67103072e-02 -4.78175096e-...\n", - " {\"token_count\":10,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.477175150810...\n", + " [ 2.34235693e-02 -4.21241224e-02 -3.90484147e-...\n", + " {\"token_count\":100,\"truncated\":false}\n", " \n", - " VIOLATES HIPPA AND CRA\n", + " A Military Star Credit card, aka Take it Home ...\n", " \n", " \n", - " 2357848\n", + " 2651231\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.519872186251...\n", - " [-1.88122243e-02 -2.68064123e-02 -4.69480827e-...\n", - " {\"token_count\":10,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.489760037964...\n", + " [ 2.64898203e-02 -5.62610961e-02 -5.82714193e-...\n", + " {\"token_count\":101,\"truncated\":false}\n", " \n", - " Receive numerous phone calls. I have no debt.\n", + " My mortgage is with Bank of America. I filed C...\n", " \n", " \n", "\n", @@ -1352,32 +1332,32 @@ ], "text/plain": [ " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", - "182250 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.570560301900... \n", - "3023485 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.523572693768... \n", - "407254 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.515173566816... \n", - "1509454 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.645342721754... \n", - "2357848 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.519872186251... \n", + "1244571 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.414497263076... \n", + "744390 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.416584344032... \n", + "127514 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.541137734253... \n", + "630563 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.477175150810... \n", + "2651231 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.489760037964... \n", "\n", - " text_embedding \\\n", - "182250 [ 4.70298417e-02 -4.08669300e-02 -2.99868709e-... \n", - "3023485 [ 1.55437263e-02 -1.93240177e-02 -2.48466972e-... \n", - "407254 [-0.01293471 -0.01959546 -0.02238463 -0.066214... \n", - "1509454 [ 3.21860723e-02 -2.67103072e-02 -4.78175096e-... \n", - "2357848 [-1.88122243e-02 -2.68064123e-02 -4.69480827e-... \n", + " ml_generate_embedding_result \\\n", + "1244571 [ 1.10590272e-02 -2.11433582e-02 -5.66212423e-... \n", + "744390 [ 4.15011719e-02 -4.50705849e-02 -7.35541508e-... \n", + "127514 [ 3.54415141e-02 1.23769706e-02 -2.61783414e-... \n", + "630563 [ 2.34235693e-02 -4.21241224e-02 -3.90484147e-... \n", + "2651231 [ 2.64898203e-02 -5.62610961e-02 -5.82714193e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "182250 {\"token_count\":10,\"truncated\":false} \n", - "3023485 {\"token_count\":10,\"truncated\":false} \n", - "407254 {\"token_count\":10,\"truncated\":false} \n", - "1509454 {\"token_count\":10,\"truncated\":false} \n", - "2357848 {\"token_count\":10,\"truncated\":false} \n", + " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", + "1244571 {\"token_count\":100,\"truncated\":false} \n", + "744390 {\"token_count\":100,\"truncated\":false} \n", + "127514 {\"token_count\":100,\"truncated\":false} \n", + "630563 {\"token_count\":100,\"truncated\":false} \n", + "2651231 {\"token_count\":101,\"truncated\":false} \n", "\n", - " content \n", - "182250 These are not my accounts. Please remove them. \n", - "3023485 This debt is not mine due to identity theft. \n", - "407254 I do not owe this company money!!!!! \n", - "1509454 VIOLATES HIPPA AND CRA \n", - "2357848 Receive numerous phone calls. I have no debt. " + " content \n", + "1244571 Ive disputed two Bankruptcies that still exist... \n", + "744390 The XXXX account was settled as a class action... \n", + "127514 I have late payments reported on my student lo... \n", + "630563 A Military Star Credit card, aka Take it Home ... \n", + "2651231 My mortgage is with Bank of America. I filed C... " ] }, "execution_count": 13, @@ -1387,7 +1367,7 @@ ], "source": [ "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", - "cluster_model.fit(predicted_embeddings[[\"text_embedding\"]])\n", + "cluster_model.fit(predicted_embeddings[[\"ml_generate_embedding_result\"]])\n", "clustered_result = cluster_model.predict(predicted_embeddings)\n", "# Notice the CENTROID_ID column, which is the ID number of the group that\n", "# each complaint belongs to.\n", @@ -1430,7 +1410,7 @@ { "data": { "text/html": [ - "Query job 85ead687-4ba9-44bf-88da-23a066f45960 is DONE. 10.7 MB processed. Open Job" + "Query job 8bfc647f-b9e5-40a2-816c-d12e8f81bea3 is DONE. 10.6 MB processed. Open Job" ], "text/plain": [ "" @@ -1442,7 +1422,7 @@ { "data": { "text/html": [ - "Query job 68ef20cd-220d-40a9-bb42-63ed3d6f5d3f is DONE. 10.7 MB processed. Open Job" + "Query job 6f834214-9cc3-4577-bb2d-980ba05df817 is DONE. 10.6 MB processed. Open Job" ], "text/plain": [ "" @@ -1478,42 +1458,62 @@ "output_type": "stream", "text": [ "comment list 1:\n", - "1. Wrongs information, selling my information to third party. Incorrect reporting\n", - "2. I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL FROM XXXX XXXX XXXX XXXX WHICH ENDED A A LIE. THEY ALSO, PULLED MANY OTHERS I DID NT AGREED TO. SOLF PULLS ONLY\n", - "3. XX/XX/XXXX and XX/XX/XXXX inaccurate information reported 30 days late.\n", - "4. Im working on myCredit and I see a couple of inquiries that I have no idea where they came from.\n", - "5. I request a copy of all dispute results and documentary evidence from XXXX, and XXXX mailed to me\n", + "1. I currently have a home loan with my ex husband with PHH Mortgages. We filed for divorce and in the divorce decree he became liable for the home and paying the payments. He ended up missing XXXX payments which effected my credit fairly substaintailly. when I became aware of the late payments, I ensured that the account was up to date and have since. I presented to them that I have the legal documents that he is obligated to make the payments each month and that I am not responisble for the payment. I asked them to remove the XXXX dings on my credit and they would not. I offered to present the paperwork and they still would not. The home is now being sold. I even filed with XXXX as a discrepency and they would not remove it. I would have never let these become a late payment. I was not even notified as they had all of his information in the file.\n", + "2. In the course of my student loan, I have been making payments and I feel that the payments haven't been added to the debt, the company stated that I am delinquent over 180 and my payments are auto pay. This has had a negative impact on my credit score.\n", + "3. The issue is 26 late payments on me and my wife 's credit reports due to a system error on a joint mortgage account that was always paid on time using autopay. ( will attach docs to support this ). \n", "\n", - "comment list 2:\n", - "1. My wife and I have been sending money to XXXX via my brother-in-law to finish a building project we have been working on since XXXX with target date of completion by XX/XX/XXXX. In XXXX XXXX my brother-in-law in was contacted by his bank to confirm he was not defrauding my wife. My brother-in-law confirmed he was helping to handle the building project by organizing and paying the workers. In XXXX XXXX Bank of America reach out to my wife to update her profile to avoid account restrictions. My wife 's account was eventually restricted until she called and confirmed her employment and other personal information. My wife 's full account activities were then restored and we continued sending wire transfers to XXXX via her checking account. \n", - "Then I received a letter dated XXXX XXXX XXXX from Bank of America stating the money market account I share with my wife which has been opened since XXXX will be will be restricted from use in 21 days and closed in 30 days with no reason. I strongly believe this is a result of the legal international wires because there was no reason to close the Savings account which had with hardly any activity. \n", - "I agree that Bank of America has a right to close accounts but I do not agree with Bank of America closing accounts because of international transactions unless they can prove fraud, criminal activity or support for terrorism, this is discriminatory towards foreign nationals. How are foreign nationals suppose to make investments or support their family/community if they are excluded from the banking system?\n", - "2. XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consumer Financial Protection Bureau XXXX XXXX XXXX XXXX, IA XXXX Dear Sir or Madam : In XX/XX/XXXX Out of the blue JP Morgan Chase arbitrarily closed my account. This was after my mother is a XXXX survivor who is over XXXX years old and for whom I have a general power of attorney and take care of her bill paying was questioned about a transaction. She is also XXXX XXXX. \n", + "This is an ongoing nightmare me and my wife are going through over the past 3 years. \n", + "Sent many dispute letters to the creditor and to the 3 bureaus, was promised multiple times that all late payments will be removed, we also has a letter from the bank stating we were never late on this account, also have a recording of a phone call with bank 's permission were the representative admits there was a system error and promised again that all late payments will be deleted from both of our credit reports. \n", + "As of today, for an unknown reason XXXX reports 6x30 days late payments, XXXX reports 24 lates, and Transunion 23 lates. \n", + "\n", + "We have always paid our mortgage on time for many years, enrolled in autopay and making 2 payments per month. Our mortgage is currently with XXXX XXXX, XXXX XXXX is the mortgage servicer who's collecting from us and disbursing payments to XXXX XXXX. \n", + "\n", + "I will attach here our mortgage transaction history confirming payments have been made on time, letter from the mortgage servicer XXXX XXXX XXXX XXXX confirming we were never late, copy of a page from my credit report showing all the late payments, and a few bank statements showing payment made on time while showing as late on our credit reports. ( XXXX & XXXX XXXX ) PLEASE HELP us to resolve this issue and have all late payments on this account removed from XXXX & XXXX XXXX. \n", + "\n", + "Her is a small portion of our previous communication with XXXX and their response : XX/XX/XXXX : we spoke with XXXX, XXXX stated that the funds were misappropriated and went to the wrong account. Said he'll contact their Tax Dep ' and get back to me and never did. \n", "\n", - "I have reason to belief that a mentally disturbed family member for whom I have an order of protection initiated this situation. This individual has ben harassing me and other members of my family for a considerable amount of time. \n", + "XX/XX/XXXX : I spoke in length with XXXX from XXXX XXXX & XXXX from XXXX at XXXX who opened an investigation. Their supervisor said he made all the necessary changes. The next month, the money was withdrawn from my account on time and i received again a late fee and 30 days late on my credit report. \n", "\n", - "The bank initially was satisfied with her response. However within 2 days they closed the account of a XXXX year old XXXX XXXX person. \n", + "XX/XX/XXXX Spoke to XXXX who sent me to XXXX who sent me to XXXX XXXX from Escalation department, she promised the issue was fixed and late payments will be removed in up to 30 days and she will email me a deletion letter. Nothing was sent! and i called and wrote 5 emails to her and never got a response. \n", "\n", - "Soon after for no reason my account was closed as well. I tried to reach out to the corporate offices of Chase and make great effort to find out what happened and to restore my account as well as my mothers but I was unsuccessful. In addition the people I spoke to were not only unhelpful bu exceedingly rude. \n", + "XX/XX/XXXX spoke with XXXX to follow up with XXXX XXXX, no response. \n", "\n", - "I should add that I have had an account with Chase since XX/XX/XXXX and took care of my ailing father before he passed away for over 25 years as well. I am now taking care of my mother for over 28 years. \n", + "XX/XX/XXXX spoke with XXXX at XXXX, she said that the transaction history is our proof that issue was corrected and she'll submit a request to delete the late payments as the system does not show that previous request was made. \n", "\n", - "I went so far as contacting a prominent Television reporter who was interested in doing a report on what happened. \n", + "XX/XX/XXXX XXXX sent a letter stating that we had a shortage of {$5300.00}. Again, upon checking my bank account all monthly payments were made on time. To avoid further issues i sent a check for {$5300.00} on XX/XX/XXXX. \n", "\n", - "I have since managed to open an account at another bank but this week I had reason to go to a branch of Chase regarding another issue and a manager using my That is a very serious unsubstantiated accusation and given this information I have no choice but to submit this complaint. \n", + "XX/XX/XXXX following many joint calls with XXXX XXXX i received a letter from XXXX stating that my credit might have been affected due to processing error and that they sent XXXX XXXX a letter requesting a removal of all late payments. \n", "\n", - "I have no interest in having an account again at a disreputable bank like Chase but I can not and will not accept or tolerate a derogatory accusation be associated with my name. \n", + "XX/XX/XXXX spoke with XXXX XXXX again.. \n", + "XX/XX/XXXX spoke with XXXX at XXXX I have many more... \n", "\n", - "I hope that my complaint will hAve the desired effect of removing this derogatory unsubstantiated accusation be removed from my name. However. I will not let this unfair matter stand and Chase ought to know that I have already retained an attorney and will if necessary hold Chase responsible and liable all damage i have incurred now And in the future Enclosed, please find the letter from Chase stating that they were closing my mothers account and a similar letter was received by me too. \n", + "*** XX/XX/XXXX : SPOKE WITH XXXX XXXX ( resolution team ) at XXXX. She said they will delete the late payments from both reports ( XXXX & XXXX XXXX and will call me to follow up on XX/XX/XXXX. She also emailed me the payment activity on the account. XXXX I received the payment history but NO CALL OR RESOLUTION. \n", "\n", - "Also please find a letter from her Doctor stating that she is XXXX XXXX. \n", + "XX/XX/XXXX : SPOKE WITH XXXX FROM XXXX AND XXXX FROM XXXX XXXX ON A XXXX WAY CALL at XXXX, XXXX PERMISSION TO RECORD THE CALL, SHE AGREED, we went over all the late payments, she said she sees the error and promised that this time it will be resolved and get deleted from our credit reports. Again, nothing was resolved and we never heard back from anyone.\n", + "4. XXXX ; XXXX and Transunion are reporting ( 30 ) plus days late on the XXXX XXXX partial account number XXXX. ( Please see page 3 of the attached credit report. ) This account was paid in XXXX, 2019 and the lates are reporting in XXXX, 2019. Please keep in mind that it is impossible to have late payments on an account that was paid off a month prior. This incorrect reporting is harming my credit score and this line item need to be removed from my credit report. I have contacted the ( 3 ) bureaus to fix this, however I have been unsuccessful.\n", + "5. My Macys account is due on the first of every month. Since I have had the card I have paid on the XXXX PRIOR to the due date. And have paid over the amount due. In XXXX my XXXX XXXX auto pay did not come out of my account and rather than calling me - on the XXXX of XXXX just 5 days late they cut my credit off and shut me out of my account so I can not even see my credit profile - I have made the payment and they still are locking me out - please look into this - you will see that is what happened and they are stating in a letter it is becasue my XXXX report shows a seriuos derogorty item which it does not and I have submitted a complaint with them as well. Macys has been the worst credit experience of my LIFE and I did read the reviews but thought it would be different for me I guess? \n", + "thank you for your help.\n", "\n", - "Thank you. \n", + "comment list 2:\n", + "1. I decided to try XXXX services for my wife and I so I purchased phones for both of us. After a day or two of trial we felt unsatisfied so we headed back to the store and we returned all items. We got charged with restocking fees and taxes. Later on I got a bill in the mail in which I was being charged {$1200.00} for the returned items. After several attempts of arguing for about XXXX months about whether I owed XXXX or not I was dismissed of such charges, but a month after I was charged by a collecting company called ERC for {$61.00}. I asked them to explain such charges weather if they were fees or taxes and they we unable to disclose information. Therefore, I asked them to send me a bill in the mail with details about the charges, as well as a dispute package and they told me they would send me a bill. About the dispute part, they said that I needed to call XXXX to discuss the charges with them but XXXX said that I had to discuss this with the collecting company. I never received a detailed statement neither a chanse to defend my self about such charges, I checked my credit score and found a red flag in it because of this. \n", + "\n", + "I am now hoping you may help me with this case. \n", + "\n", + "Thanks :\n", + "2. Over a year and a half ago we started the process of buying a home. Our mortgage guy sent us to a credit repair co. They got the collection account from Weltman , Weinberg & Reis taken off my credit, because it was unverifiable. Now it is back on my credit. I have credit reports showing the trade line on and then off and now today it is currently on my report. When I called to verify the account with WW & R they sent me a heavily redacted letter verifying absolutely nothing. I would like this unverifiable account taken off my credit and removed permanently. This should not be a loan I have to pay for if there is no verification that it is my debt. Attached are the credit reports and the letter of verification that was sent to me.\n", + "3. I recently disputed ( see attached letter ) with Receivable Management Services an account entry that they placed on my credit report without providing a dunning letter or any correspondence that would have allowed me 30 days to dispute the validity of the alleged debt. To date, I have not received any communication from them. They are blatantly violating my rights by reporting this inaccurate, erroneous, unverifiable entry.\n", + "\n", + "Additionally, this account entry does not reflect a payment history which should be included on any entry that is reflected on my credit report. In my previous communication to them, I specifically requested that they provide an agreement that states their authority to collect on the alleged debt, agreement with signature of the alleged debtor wherein he/she agreed to pay the creditor, alleged account number, date this alleged debt became payable, original delinquency date, and to date to no avail. \n", "\n", - "XXXX XXXX\n", - "3. U.S. Bank sent two letters containing Visa Debit Cards to our address on XX/XX/2021. One Visa Debit Card is in the name of XXXX XXXX and one Visa Debit Card is in the name of XXXX XXXX. These cards supposedly link to existing checking accounts at U.S. Bank. However : ( 1 ) Neither of us have existing checking accounts at U.S. Bank, ( 2 ) Neither of us solicited a bank account at U.S. Bank, and ( 3 ) Neither of us solicited a Visa Debit Card. We have attempted to call U.S. Bank at the phone numbers provided in the letters but are only able to access an automated system which will not proceed without us establishing accounts and activating these cards. We are concerned here that one of two things has happened : either ( 1 ) we are victims of identity theft and some third party is trying to establish accounts in our name, or ( 2 ) U.S. Bank is engaged in bank fraud. In either case, we request the assistance of the Consumer Financial Protection Bureau. Thank you.\n", - "4. I contacted my bank over 3 times about this amount, the first two times I spoke to gentleman that agreed with me that I didnt get back a certain amount of dollars back, I did the math and they refuse to see that I do not owe this amount because I never had it in the first place. I wrote out all my charges and connected it to the charges made back from the consumer and I was missing XXXX, I called XXXX they said they gave it all back which is not their fault because they showed me proof. Along the lines Capital One does not want to take responsibility for the missing money. I have wrote everything out and then its not adding up, they keep saying that they did a charge back twice which is incorrect. My balance was at XXXX before I made this purchase and it shouldve been returned back to XXXX because I return all the items and nothing is in my possession. I have proof that I returned everything.\n", - "5. CB INDIGO ( Bank ) XX/XX/2022 I just recently got off the phone with the company and they wont put in a request of removal of a fraudulent hard inquiry from Insigo Mastercard to XXXX. They dont even have my information on file, I called 3 times most of them are lazy and was giving me a hard time.\n", + "As such, since they have refused to respond to my request and not provide any documentation to substantiate their allegations, coupled with the fact that they did not provide me a dunning letter is grounds for this erroneous, inaccurate, unverifiable entry to be deleted from my credit report.\n", + "4. I accepted service from XXXX XXXX XXXX. The company did not inform me that internet was required. They also told me that the agreement was at will without penalty. They never addressed my needs as a customer. My bill is only {$230.00}. They placed false information regarding my bill with a collection agency who has placed information on my credit report without contacting me or giving me an opportunity to dispute the validity of the debt. The debt is not valid. The actions are unlawful and I am requesting that the actions of this collection agency be reported to the Federal Trade Commission.\n", + "5. I have continued to submit an investigation for a Bankruptcy place on my credit report. I have been trying to get this removed because it was place on my credit report in error and inaccurate. ALL THREE CREDIT BUREAUS have continue to ignore the information proving this was place in error and fail to properly investigate the dispute I have place in their office. \n", + "\n", + "1. They say they have verified this dispute with XXXX but I have a letter from XXXX stating this was removed because they were unable to verify the accuracy of the bankruptcy. I received this letter on XX/XX/XXXX. XXXX just finished an investigation on XX/XX/2019 stating the verified this with XXXX. \n", + "2.Experian Open the dispute on XX/XX/2019 and closed it on XX/XX/19 stated they verified with XXXX and the Bankruptcy court and I have a letter From XXXX stating they could not verify the accuracy of this dispute. I also, have a letter from the court house stating they do not verify information with the credit bureaus How could be this be on my XXXX file when XXXX has removed this item. \n", + "3. XXXX open and investigation XX/XX/2019 and closed it XX/XX/2019 No way they properly investigation I have submitted all information to dispute the inaccurate information. Please do a proper investigation. \n", + "\n", + "XXXX, Experian, and XXXX please do a proper investigation under 611 of the FCRA thank you very much I have attached the letter proving this this is not on my XXXX consumer report and a letter form the court house stating they do not report information to the credit bureaus from the XXXX XXXX XXXX, Clerk of Court United State Bankruptcy Court on dated XX/XX/2019 I have summited it to the credit bureaus to be ignored. I have as for a description of my investigation by section 611 of the FCRA and the information from the investigation is inaccurate.\n", "\n" ] } @@ -1547,41 +1547,61 @@ "text": [ "Please highlight the most obvious difference between the two lists of comments:\n", "comment list 1:\n", - "1. Wrongs information, selling my information to third party. Incorrect reporting\n", - "2. I TIED TO BUY CAR AT XXXX, THEY GOT APPROVAL FROM XXXX XXXX XXXX XXXX WHICH ENDED A A LIE. THEY ALSO, PULLED MANY OTHERS I DID NT AGREED TO. SOLF PULLS ONLY\n", - "3. XX/XX/XXXX and XX/XX/XXXX inaccurate information reported 30 days late.\n", - "4. Im working on myCredit and I see a couple of inquiries that I have no idea where they came from.\n", - "5. I request a copy of all dispute results and documentary evidence from XXXX, and XXXX mailed to me\n", - "comment list 2:\n", - "1. My wife and I have been sending money to XXXX via my brother-in-law to finish a building project we have been working on since XXXX with target date of completion by XX/XX/XXXX. In XXXX XXXX my brother-in-law in was contacted by his bank to confirm he was not defrauding my wife. My brother-in-law confirmed he was helping to handle the building project by organizing and paying the workers. In XXXX XXXX Bank of America reach out to my wife to update her profile to avoid account restrictions. My wife 's account was eventually restricted until she called and confirmed her employment and other personal information. My wife 's full account activities were then restored and we continued sending wire transfers to XXXX via her checking account. \n", - "Then I received a letter dated XXXX XXXX XXXX from Bank of America stating the money market account I share with my wife which has been opened since XXXX will be will be restricted from use in 21 days and closed in 30 days with no reason. I strongly believe this is a result of the legal international wires because there was no reason to close the Savings account which had with hardly any activity. \n", - "I agree that Bank of America has a right to close accounts but I do not agree with Bank of America closing accounts because of international transactions unless they can prove fraud, criminal activity or support for terrorism, this is discriminatory towards foreign nationals. How are foreign nationals suppose to make investments or support their family/community if they are excluded from the banking system?\n", - "2. XXXX XXXX XXXX XXXX, NY XXXX XX/XX/XXXX Consumer Financial Protection Bureau XXXX XXXX XXXX XXXX, IA XXXX Dear Sir or Madam : In XX/XX/XXXX Out of the blue JP Morgan Chase arbitrarily closed my account. This was after my mother is a XXXX survivor who is over XXXX years old and for whom I have a general power of attorney and take care of her bill paying was questioned about a transaction. She is also XXXX XXXX. \n", + "1. I currently have a home loan with my ex husband with PHH Mortgages. We filed for divorce and in the divorce decree he became liable for the home and paying the payments. He ended up missing XXXX payments which effected my credit fairly substaintailly. when I became aware of the late payments, I ensured that the account was up to date and have since. I presented to them that I have the legal documents that he is obligated to make the payments each month and that I am not responisble for the payment. I asked them to remove the XXXX dings on my credit and they would not. I offered to present the paperwork and they still would not. The home is now being sold. I even filed with XXXX as a discrepency and they would not remove it. I would have never let these become a late payment. I was not even notified as they had all of his information in the file.\n", + "2. In the course of my student loan, I have been making payments and I feel that the payments haven't been added to the debt, the company stated that I am delinquent over 180 and my payments are auto pay. This has had a negative impact on my credit score.\n", + "3. The issue is 26 late payments on me and my wife 's credit reports due to a system error on a joint mortgage account that was always paid on time using autopay. ( will attach docs to support this ). \n", + "\n", + "This is an ongoing nightmare me and my wife are going through over the past 3 years. \n", + "Sent many dispute letters to the creditor and to the 3 bureaus, was promised multiple times that all late payments will be removed, we also has a letter from the bank stating we were never late on this account, also have a recording of a phone call with bank 's permission were the representative admits there was a system error and promised again that all late payments will be deleted from both of our credit reports. \n", + "As of today, for an unknown reason XXXX reports 6x30 days late payments, XXXX reports 24 lates, and Transunion 23 lates. \n", "\n", - "I have reason to belief that a mentally disturbed family member for whom I have an order of protection initiated this situation. This individual has ben harassing me and other members of my family for a considerable amount of time. \n", + "We have always paid our mortgage on time for many years, enrolled in autopay and making 2 payments per month. Our mortgage is currently with XXXX XXXX, XXXX XXXX is the mortgage servicer who's collecting from us and disbursing payments to XXXX XXXX. \n", "\n", - "The bank initially was satisfied with her response. However within 2 days they closed the account of a XXXX year old XXXX XXXX person. \n", + "I will attach here our mortgage transaction history confirming payments have been made on time, letter from the mortgage servicer XXXX XXXX XXXX XXXX confirming we were never late, copy of a page from my credit report showing all the late payments, and a few bank statements showing payment made on time while showing as late on our credit reports. ( XXXX & XXXX XXXX ) PLEASE HELP us to resolve this issue and have all late payments on this account removed from XXXX & XXXX XXXX. \n", "\n", - "Soon after for no reason my account was closed as well. I tried to reach out to the corporate offices of Chase and make great effort to find out what happened and to restore my account as well as my mothers but I was unsuccessful. In addition the people I spoke to were not only unhelpful bu exceedingly rude. \n", + "Her is a small portion of our previous communication with XXXX and their response : XX/XX/XXXX : we spoke with XXXX, XXXX stated that the funds were misappropriated and went to the wrong account. Said he'll contact their Tax Dep ' and get back to me and never did. \n", "\n", - "I should add that I have had an account with Chase since XX/XX/XXXX and took care of my ailing father before he passed away for over 25 years as well. I am now taking care of my mother for over 28 years. \n", + "XX/XX/XXXX : I spoke in length with XXXX from XXXX XXXX & XXXX from XXXX at XXXX who opened an investigation. Their supervisor said he made all the necessary changes. The next month, the money was withdrawn from my account on time and i received again a late fee and 30 days late on my credit report. \n", "\n", - "I went so far as contacting a prominent Television reporter who was interested in doing a report on what happened. \n", + "XX/XX/XXXX Spoke to XXXX who sent me to XXXX who sent me to XXXX XXXX from Escalation department, she promised the issue was fixed and late payments will be removed in up to 30 days and she will email me a deletion letter. Nothing was sent! and i called and wrote 5 emails to her and never got a response. \n", + "\n", + "XX/XX/XXXX spoke with XXXX to follow up with XXXX XXXX, no response. \n", + "\n", + "XX/XX/XXXX spoke with XXXX at XXXX, she said that the transaction history is our proof that issue was corrected and she'll submit a request to delete the late payments as the system does not show that previous request was made. \n", + "\n", + "XX/XX/XXXX XXXX sent a letter stating that we had a shortage of {$5300.00}. Again, upon checking my bank account all monthly payments were made on time. To avoid further issues i sent a check for {$5300.00} on XX/XX/XXXX. \n", + "\n", + "XX/XX/XXXX following many joint calls with XXXX XXXX i received a letter from XXXX stating that my credit might have been affected due to processing error and that they sent XXXX XXXX a letter requesting a removal of all late payments. \n", + "\n", + "XX/XX/XXXX spoke with XXXX XXXX again.. \n", + "XX/XX/XXXX spoke with XXXX at XXXX I have many more... \n", + "\n", + "*** XX/XX/XXXX : SPOKE WITH XXXX XXXX ( resolution team ) at XXXX. She said they will delete the late payments from both reports ( XXXX & XXXX XXXX and will call me to follow up on XX/XX/XXXX. She also emailed me the payment activity on the account. XXXX I received the payment history but NO CALL OR RESOLUTION. \n", + "\n", + "XX/XX/XXXX : SPOKE WITH XXXX FROM XXXX AND XXXX FROM XXXX XXXX ON A XXXX WAY CALL at XXXX, XXXX PERMISSION TO RECORD THE CALL, SHE AGREED, we went over all the late payments, she said she sees the error and promised that this time it will be resolved and get deleted from our credit reports. Again, nothing was resolved and we never heard back from anyone.\n", + "4. XXXX ; XXXX and Transunion are reporting ( 30 ) plus days late on the XXXX XXXX partial account number XXXX. ( Please see page 3 of the attached credit report. ) This account was paid in XXXX, 2019 and the lates are reporting in XXXX, 2019. Please keep in mind that it is impossible to have late payments on an account that was paid off a month prior. This incorrect reporting is harming my credit score and this line item need to be removed from my credit report. I have contacted the ( 3 ) bureaus to fix this, however I have been unsuccessful.\n", + "5. My Macys account is due on the first of every month. Since I have had the card I have paid on the XXXX PRIOR to the due date. And have paid over the amount due. In XXXX my XXXX XXXX auto pay did not come out of my account and rather than calling me - on the XXXX of XXXX just 5 days late they cut my credit off and shut me out of my account so I can not even see my credit profile - I have made the payment and they still are locking me out - please look into this - you will see that is what happened and they are stating in a letter it is becasue my XXXX report shows a seriuos derogorty item which it does not and I have submitted a complaint with them as well. Macys has been the worst credit experience of my LIFE and I did read the reviews but thought it would be different for me I guess? \n", + "thank you for your help.\n", + "comment list 2:\n", + "1. I decided to try XXXX services for my wife and I so I purchased phones for both of us. After a day or two of trial we felt unsatisfied so we headed back to the store and we returned all items. We got charged with restocking fees and taxes. Later on I got a bill in the mail in which I was being charged {$1200.00} for the returned items. After several attempts of arguing for about XXXX months about whether I owed XXXX or not I was dismissed of such charges, but a month after I was charged by a collecting company called ERC for {$61.00}. I asked them to explain such charges weather if they were fees or taxes and they we unable to disclose information. Therefore, I asked them to send me a bill in the mail with details about the charges, as well as a dispute package and they told me they would send me a bill. About the dispute part, they said that I needed to call XXXX to discuss the charges with them but XXXX said that I had to discuss this with the collecting company. I never received a detailed statement neither a chanse to defend my self about such charges, I checked my credit score and found a red flag in it because of this. \n", "\n", - "I have since managed to open an account at another bank but this week I had reason to go to a branch of Chase regarding another issue and a manager using my That is a very serious unsubstantiated accusation and given this information I have no choice but to submit this complaint. \n", + "I am now hoping you may help me with this case. \n", "\n", - "I have no interest in having an account again at a disreputable bank like Chase but I can not and will not accept or tolerate a derogatory accusation be associated with my name. \n", + "Thanks :\n", + "2. Over a year and a half ago we started the process of buying a home. Our mortgage guy sent us to a credit repair co. They got the collection account from Weltman , Weinberg & Reis taken off my credit, because it was unverifiable. Now it is back on my credit. I have credit reports showing the trade line on and then off and now today it is currently on my report. When I called to verify the account with WW & R they sent me a heavily redacted letter verifying absolutely nothing. I would like this unverifiable account taken off my credit and removed permanently. This should not be a loan I have to pay for if there is no verification that it is my debt. Attached are the credit reports and the letter of verification that was sent to me.\n", + "3. I recently disputed ( see attached letter ) with Receivable Management Services an account entry that they placed on my credit report without providing a dunning letter or any correspondence that would have allowed me 30 days to dispute the validity of the alleged debt. To date, I have not received any communication from them. They are blatantly violating my rights by reporting this inaccurate, erroneous, unverifiable entry.\n", "\n", - "I hope that my complaint will hAve the desired effect of removing this derogatory unsubstantiated accusation be removed from my name. However. I will not let this unfair matter stand and Chase ought to know that I have already retained an attorney and will if necessary hold Chase responsible and liable all damage i have incurred now And in the future Enclosed, please find the letter from Chase stating that they were closing my mothers account and a similar letter was received by me too. \n", + "Additionally, this account entry does not reflect a payment history which should be included on any entry that is reflected on my credit report. In my previous communication to them, I specifically requested that they provide an agreement that states their authority to collect on the alleged debt, agreement with signature of the alleged debtor wherein he/she agreed to pay the creditor, alleged account number, date this alleged debt became payable, original delinquency date, and to date to no avail. \n", "\n", - "Also please find a letter from her Doctor stating that she is XXXX XXXX. \n", + "As such, since they have refused to respond to my request and not provide any documentation to substantiate their allegations, coupled with the fact that they did not provide me a dunning letter is grounds for this erroneous, inaccurate, unverifiable entry to be deleted from my credit report.\n", + "4. I accepted service from XXXX XXXX XXXX. The company did not inform me that internet was required. They also told me that the agreement was at will without penalty. They never addressed my needs as a customer. My bill is only {$230.00}. They placed false information regarding my bill with a collection agency who has placed information on my credit report without contacting me or giving me an opportunity to dispute the validity of the debt. The debt is not valid. The actions are unlawful and I am requesting that the actions of this collection agency be reported to the Federal Trade Commission.\n", + "5. I have continued to submit an investigation for a Bankruptcy place on my credit report. I have been trying to get this removed because it was place on my credit report in error and inaccurate. ALL THREE CREDIT BUREAUS have continue to ignore the information proving this was place in error and fail to properly investigate the dispute I have place in their office. \n", "\n", - "Thank you. \n", + "1. They say they have verified this dispute with XXXX but I have a letter from XXXX stating this was removed because they were unable to verify the accuracy of the bankruptcy. I received this letter on XX/XX/XXXX. XXXX just finished an investigation on XX/XX/2019 stating the verified this with XXXX. \n", + "2.Experian Open the dispute on XX/XX/2019 and closed it on XX/XX/19 stated they verified with XXXX and the Bankruptcy court and I have a letter From XXXX stating they could not verify the accuracy of this dispute. I also, have a letter from the court house stating they do not verify information with the credit bureaus How could be this be on my XXXX file when XXXX has removed this item. \n", + "3. XXXX open and investigation XX/XX/2019 and closed it XX/XX/2019 No way they properly investigation I have submitted all information to dispute the inaccurate information. Please do a proper investigation. \n", "\n", - "XXXX XXXX\n", - "3. U.S. Bank sent two letters containing Visa Debit Cards to our address on XX/XX/2021. One Visa Debit Card is in the name of XXXX XXXX and one Visa Debit Card is in the name of XXXX XXXX. These cards supposedly link to existing checking accounts at U.S. Bank. However : ( 1 ) Neither of us have existing checking accounts at U.S. Bank, ( 2 ) Neither of us solicited a bank account at U.S. Bank, and ( 3 ) Neither of us solicited a Visa Debit Card. We have attempted to call U.S. Bank at the phone numbers provided in the letters but are only able to access an automated system which will not proceed without us establishing accounts and activating these cards. We are concerned here that one of two things has happened : either ( 1 ) we are victims of identity theft and some third party is trying to establish accounts in our name, or ( 2 ) U.S. Bank is engaged in bank fraud. In either case, we request the assistance of the Consumer Financial Protection Bureau. Thank you.\n", - "4. I contacted my bank over 3 times about this amount, the first two times I spoke to gentleman that agreed with me that I didnt get back a certain amount of dollars back, I did the math and they refuse to see that I do not owe this amount because I never had it in the first place. I wrote out all my charges and connected it to the charges made back from the consumer and I was missing XXXX, I called XXXX they said they gave it all back which is not their fault because they showed me proof. Along the lines Capital One does not want to take responsibility for the missing money. I have wrote everything out and then its not adding up, they keep saying that they did a charge back twice which is incorrect. My balance was at XXXX before I made this purchase and it shouldve been returned back to XXXX because I return all the items and nothing is in my possession. I have proof that I returned everything.\n", - "5. CB INDIGO ( Bank ) XX/XX/2022 I just recently got off the phone with the company and they wont put in a request of removal of a fraudulent hard inquiry from Insigo Mastercard to XXXX. They dont even have my information on file, I called 3 times most of them are lazy and was giving me a hard time.\n", + "XXXX, Experian, and XXXX please do a proper investigation under 611 of the FCRA thank you very much I have attached the letter proving this this is not on my XXXX consumer report and a letter form the court house stating they do not report information to the credit bureaus from the XXXX XXXX XXXX, Clerk of Court United State Bankruptcy Court on dated XX/XX/2019 I have summited it to the credit bureaus to be ignored. I have as for a description of my investigation by section 611 of the FCRA and the information from the investigation is inaccurate.\n", "\n" ] } @@ -1613,7 +1633,7 @@ { "data": { "text/html": [ - "Query job a7ce86a7-3a18-47b9-a46f-98dbe6a5a339 is DONE. 0 Bytes processed. Open Job" + "Query job a069b4a5-5238-4ca8-a6c0-d48781d00f6c is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1651,7 +1671,7 @@ { "data": { "text/html": [ - "Query job d568c03d-6bbd-4c3e-b087-563b7f5135ed is DONE. 0 Bytes processed. Open Job" + "Query job 63f6e1d0-b0dc-4f5c-a001-5889c28162c5 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1663,7 +1683,7 @@ { "data": { "text/html": [ - "Query job 17eaa806-51a4-4ee9-b219-75455d0095a7 is DONE. 8 Bytes processed. Open Job" + "Query job c1c9e28b-ba6d-4485-b892-0bf2428f927c is DONE. 8 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1675,7 +1695,7 @@ { "data": { "text/html": [ - "Query job e6d40ded-691d-4523-94ea-dd8202bd0220 is DONE. 2 Bytes processed. Open Job" + "Query job 67402b3c-eee4-4fe4-aeaf-fb27606ecde7 is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1687,7 +1707,7 @@ { "data": { "text/html": [ - "Query job 200f0b88-7b6d-417b-a181-a98138e3bc95 is DONE. 193 Bytes processed. Open Job" + "Query job 83166900-0787-4a6d-b822-c3be87990e35 is DONE. 328 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1699,7 +1719,7 @@ { "data": { "text/plain": [ - "'The most obvious difference between the two lists of comments is that list 1 is related to credit reporting disputes and list 2 is a collection of general consumer banking complaints.'" + "'The most obvious difference between the two lists of comments is that the first list contains comments about credit report issues related to mortgages and loans, while the second list contains comments about credit report issues related to other types of debts, such as cell phone bills, collections, and bankruptcies.'" ] }, "execution_count": 19, @@ -1753,7 +1773,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.10.13" } }, "nbformat": 4, From 9e741543ee978a33101a73f7ff1a8ef8925abbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 1 Apr 2024 11:12:38 -0500 Subject: [PATCH 451/921] Revert "feat: Support max_columns in repr and make repr more efficient (#515)" (#554) This reverts commit 54e49cff89bd329852a823cd5cf5c5b41b7f9e32. --- bigframes/core/blocks.py | 42 +++++++-------------- bigframes/core/indexes/index.py | 10 ++--- bigframes/dataframe.py | 66 ++++++++++++++++++++------------- bigframes/series.py | 9 +++-- bigframes/session/__init__.py | 8 +--- 5 files changed, 65 insertions(+), 70 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index aab8b1ad4db..11899eef113 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -467,23 +467,6 @@ def to_pandas_batches(self): self._copy_index_to_pandas(df) yield df - def download_pandas_preview( - self, max_rows: int - ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: - """Download one page of results and return the query job.""" - dtypes = dict(zip(self.index_columns, self.index.dtypes)) - dtypes.update(zip(self.value_columns, self.dtypes)) - results_iterator, query_job = self.session._execute( - self.expr, sorted=True, max_results=max_rows - ) - arrow_results_iterator = results_iterator.to_arrow_iterable() - arrow_table = next(arrow_results_iterator) - downloaded_df = bigframes.session._io.pandas.arrow_to_pandas( - arrow_table, dtypes - ) - self._copy_index_to_pandas(downloaded_df) - return downloaded_df, query_job - def _copy_index_to_pandas(self, df: pd.DataFrame): """Set the index on pandas DataFrame to match this block. @@ -1314,25 +1297,26 @@ def _forward_slice(self, start: int = 0, stop=None, step: int = 1): # queries. @functools.cache def retrieve_repr_request_results( - self, max_results: int, max_columns: int - ) -> Tuple[pd.DataFrame, Tuple[int, int], bigquery.QueryJob]: + self, max_results: int + ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: """ Retrieves a pandas dataframe containing only max_results many rows for use with printing methods. - Returns a tuple of the dataframe preview for printing and the overall number - of rows and columns of the table, as well as the query job used. + Returns a tuple of the dataframe and the overall number of rows of the query. """ - pandas_df, query_job = self.download_pandas_preview(max_results) - row_count = self.session._get_table_row_count(query_job.destination) - column_count = len(self.value_columns) - - formatted_df = pandas_df.set_axis(self.column_labels, axis=1) + # TODO(swast): Select a subset of columns if max_columns is less than the + # number of columns in the schema. + count = self.shape[0] + if count > max_results: + head_block = self.slice(0, max_results) + else: + head_block = self + computed_df, query_job = head_block.to_pandas() + formatted_df = computed_df.set_axis(self.column_labels, axis=1) # we reset the axis and substitute the bf index name for the default formatted_df.index.name = self.index.name - # limit column count - formatted_df = formatted_df.iloc[:, 0:max_columns] - return formatted_df, (row_count, column_count), query_job + return formatted_df, count, query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: result_id = guid.generate_guid() diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/index.py index 48988aaffe8..c818b68711c 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/index.py @@ -205,17 +205,17 @@ def query_job(self) -> Optional[bigquery.QueryJob]: return self._query_job def __repr__(self) -> str: + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + # TODO(swast): Avoid downloading the whole series by using job + # metadata, like we do with DataFrame. opts = bigframes.options.display max_results = opts.max_rows - max_columns = opts.max_columns if opts.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) - pandas_df, _, query_job = self._block.retrieve_repr_request_results( - max_results, max_columns - ) + pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) self._query_job = query_job - return repr(pandas_df.index) def copy(self, name: Optional[Hashable] = None): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1df78dd4cd2..066b0824901 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -579,16 +579,28 @@ def __setattr__(self, key: str, value): object.__setattr__(self, key, value) def __repr__(self) -> str: - """Converts a DataFrame to a string using pandas dataframe __repr__. + """Converts a DataFrame to a string. Calls to_pandas. - Only represents the first `bigframes.options.display.max_rows` - and `bigframes.options.display.max_columns`. + Only represents the first `bigframes.options.display.max_rows`. """ - if bigframes.options.display.repr_mode == "deferred": + opts = bigframes.options.display + max_results = opts.max_rows + if opts.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) - pandas_df, shape = self._perform_repr_request() - with display_options.pandas_repr(bigframes.options.display): + self._cached() + # TODO(swast): pass max_columns and get the true column count back. Maybe + # get 1 more column than we have requested so that pandas can add the + # ... for us? + pandas_df, row_count, query_job = self._block.retrieve_repr_request_results( + max_results + ) + + self._set_internal_query_job(query_job) + + column_count = len(pandas_df.columns) + + with display_options.pandas_repr(opts): repr_string = repr(pandas_df) # Modify the end of the string to reflect count. @@ -596,40 +608,42 @@ def __repr__(self) -> str: pattern = re.compile("\\[[0-9]+ rows x [0-9]+ columns\\]") if pattern.match(lines[-1]): lines = lines[:-2] - if shape[0] > len(lines) - 1: + + if row_count > len(lines) - 1: lines.append("...") + lines.append("") - lines.append(f"[{shape[0]} rows x {shape[1]} columns]") + lines.append(f"[{row_count} rows x {column_count} columns]") return "\n".join(lines) - def _perform_repr_request(self) -> Tuple[pandas.DataFrame, Tuple[int, int]]: - max_results = bigframes.options.display.max_rows - max_columns = bigframes.options.display.max_columns - self._cached() - pandas_df, shape, query_job = self._block.retrieve_repr_request_results( - max_results, max_columns - ) - self._set_internal_query_job(query_job) - return pandas_df, shape - def _repr_html_(self) -> str: """ Returns an html string primarily for use by notebooks for displaying - a representation of the DataFrame. Displays at most the number of rows - and columns given by `bigframes.options.display.max_rows` and - `bigframes.options.display.max_columns`. + a representation of the DataFrame. Displays 20 rows by default since + many notebooks are not configured for large tables. """ - - if bigframes.options.display.repr_mode == "deferred": + opts = bigframes.options.display + max_results = bigframes.options.display.max_rows + if opts.repr_mode == "deferred": return formatter.repr_query_job_html(self.query_job) - pandas_df, shape = self._perform_repr_request() + self._cached() + # TODO(swast): pass max_columns and get the true column count back. Maybe + # get 1 more column than we have requested so that pandas can add the + # ... for us? + pandas_df, row_count, query_job = self._block.retrieve_repr_request_results( + max_results + ) + + self._set_internal_query_job(query_job) + + column_count = len(pandas_df.columns) - with display_options.pandas_repr(bigframes.options.display): + with display_options.pandas_repr(opts): # _repr_html_ stub is missing so mypy thinks it's a Series. Ignore mypy. html_string = pandas_df._repr_html_() # type:ignore - html_string += f"[{shape[0]} rows x {shape[1]} columns in total]" + html_string += f"[{row_count} rows x {column_count} columns in total]" return html_string def __setitem__(self, key: str, value: SingleItemValue): diff --git a/bigframes/series.py b/bigframes/series.py index f1ac89f5143..e7b358c2feb 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -272,16 +272,17 @@ def reset_index( return bigframes.dataframe.DataFrame(block) def __repr__(self) -> str: + # TODO(swast): Add a timeout here? If the query is taking a long time, + # maybe we just print the job metadata that we have so far? + # TODO(swast): Avoid downloading the whole series by using job + # metadata, like we do with DataFrame. opts = bigframes.options.display max_results = opts.max_rows - max_columns = opts.max_columns if opts.repr_mode == "deferred": return formatter.repr_query_job(self.query_job) self._cached() - pandas_df, _, query_job = self._block.retrieve_repr_request_results( - max_results, max_columns - ) + pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) self._set_internal_query_job(query_job) return repr(pandas_df.iloc[:, 0]) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 6573934f94a..ac266da3bda 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1832,7 +1832,6 @@ def _execute( sorted: bool = True, dry_run=False, col_id_overrides: Mapping[str, str] = {}, - max_results: Optional[int] = None, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: sql = self._to_sql( array_value, sorted=sorted, col_id_overrides=col_id_overrides @@ -1842,7 +1841,8 @@ def _execute( else: job_config.dry_run = dry_run return self._start_query( - sql=sql, job_config=job_config, max_results=max_results + sql=sql, + job_config=job_config, ) def _peek( @@ -1887,10 +1887,6 @@ def _get_table_size(self, destination_table): table = self.bqclient.get_table(destination_table) return table.num_bytes - def _get_table_row_count(self, destination_table) -> int: - table = self.bqclient.get_table(destination_table) - return table.num_rows - def _rows_to_dataframe( self, row_iterator: bigquery.table.RowIterator, dtypes: Dict ) -> pandas.DataFrame: From 9ac4ed8a374b757b5b19eaa64e8ec4739866c9bd Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 1 Apr 2024 16:24:29 +0000 Subject: [PATCH 452/921] chore: fix typo `mertics` to `metrics` (#549) * chore: fix typo `mertics` to `metrics` * revert unintended change --- bigframes/ml/metrics/_metrics.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index e8c7400f35f..ee86798b33f 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -19,8 +19,8 @@ import typing from typing import Tuple, Union -import bigframes_vendored.sklearn.metrics._classification as vendored_mertics_classification -import bigframes_vendored.sklearn.metrics._ranking as vendored_mertics_ranking +import bigframes_vendored.sklearn.metrics._classification as vendored_metrics_classification +import bigframes_vendored.sklearn.metrics._ranking as vendored_metrics_ranking import bigframes_vendored.sklearn.metrics._regression as vendored_metrics_regression import numpy as np import pandas as pd @@ -79,7 +79,7 @@ def accuracy_score( return score.sum() -accuracy_score.__doc__ = inspect.getdoc(vendored_mertics_classification.accuracy_score) +accuracy_score.__doc__ = inspect.getdoc(vendored_metrics_classification.accuracy_score) def roc_curve( @@ -149,7 +149,7 @@ def roc_curve( ) -roc_curve.__doc__ = inspect.getdoc(vendored_mertics_ranking.roc_curve) +roc_curve.__doc__ = inspect.getdoc(vendored_metrics_ranking.roc_curve) def roc_auc_score( @@ -171,7 +171,7 @@ def roc_auc_score( return (width_diff * height_avg).sum() -roc_auc_score.__doc__ = inspect.getdoc(vendored_mertics_ranking.roc_auc_score) +roc_auc_score.__doc__ = inspect.getdoc(vendored_metrics_ranking.roc_auc_score) def auc( @@ -185,7 +185,7 @@ def auc( return auc -auc.__doc__ = inspect.getdoc(vendored_mertics_ranking.auc) +auc.__doc__ = inspect.getdoc(vendored_metrics_ranking.auc) def confusion_matrix( @@ -223,7 +223,7 @@ def confusion_matrix( confusion_matrix.__doc__ = inspect.getdoc( - vendored_mertics_classification.confusion_matrix + vendored_metrics_classification.confusion_matrix ) @@ -261,7 +261,7 @@ def recall_score( return recall_score -recall_score.__doc__ = inspect.getdoc(vendored_mertics_classification.recall_score) +recall_score.__doc__ = inspect.getdoc(vendored_metrics_classification.recall_score) def precision_score( @@ -299,7 +299,7 @@ def precision_score( precision_score.__doc__ = inspect.getdoc( - vendored_mertics_classification.precision_score + vendored_metrics_classification.precision_score ) @@ -334,4 +334,4 @@ def f1_score( return f1_score -f1_score.__doc__ = inspect.getdoc(vendored_mertics_classification.f1_score) +f1_score.__doc__ = inspect.getdoc(vendored_metrics_classification.f1_score) From f207c8f16151523aa9bb37f5c6abe6c37ee5bbb3 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:19:51 -0700 Subject: [PATCH 453/921] chore: address comments from technical writers for legal review (#555) --- bigframes/_config/bigquery_options.py | 2 +- bigframes/ml/compose.py | 2 +- bigframes/ml/imported.py | 6 +++--- .../bigframes_vendored/pandas/core/groupby/__init__.py | 10 +++++----- .../sklearn/compose/_column_transformer.py | 4 ++-- .../bigframes_vendored/sklearn/decomposition/_pca.py | 4 ++-- .../sklearn/linear_model/_logistic.py | 2 +- .../sklearn/preprocessing/_discretization.py | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 9da953a5828..50e14eaf288 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -57,7 +57,7 @@ def application_name(self) -> Optional[str]: """The application name to amend to the user-agent sent to Google APIs. The application name to amend to the user agent sent to Google APIs. - The recommended format is ``"appplication-name/major.minor.patch_version"`` + The recommended format is ``"application-name/major.minor.patch_version"`` or ``"(gpn:PartnerName;)"`` for official Google partners. """ return self._application_name diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index cd233589d6f..21cfba8e016 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -13,7 +13,7 @@ # limitations under the License. """Build composite transformers on heterogeneous data. This module is styled -after Scikit-Learn's compose module: +after scikit-Learn's compose module: https://scikit-learn.org/stable/modules/classes.html#module-sklearn.compose.""" from __future__ import annotations diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index 7f758270830..b5511500500 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -34,7 +34,7 @@ class TensorFlowModel(base.Predictor): model_path (str): GCS path that holds the model files. session (BigQuery Session): - BQ session to create the model + BQ session to create the model. """ def __init__( @@ -113,7 +113,7 @@ class ONNXModel(base.Predictor): model_path (str): Cloud Storage path that holds the model files. session (BigQuery Session): - BQ session to create the model + BQ session to create the model. """ def __init__( @@ -207,7 +207,7 @@ class XGBoostModel(base.Predictor): and feature_types are both specified in the model file. Supported types are "bool", "string", "int64", "float64", "array", "array", "array", "array". session (BigQuery Session): - BQ session to create the model + BQ session to create the model. """ def __init__( diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 8730cf0007a..e1cc8c5a538 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -78,7 +78,7 @@ def median( Include only float, int, boolean columns. exact (bool, default False): Calculate the exact median instead of an approximation. Note: - ``exact=True`` not yet supported. + ``exact=True`` is not supported. Returns: pandas.Series or pandas.DataFrame: Median of groups. @@ -178,7 +178,7 @@ def sum( Include only float, int, boolean columns. min_count (int, default 0): The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. + than ``min_count`` and non-NA values are present, the result will be NA. Returns: Series or DataFrame: Computed sum of values within each group. @@ -194,7 +194,7 @@ def prod(self, numeric_only: bool = False, min_count: int = 0): Include only float, int, boolean columns. min_count (int, default 0): The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. + than ``min_count`` and non-NA values are present, the result will be NA. Returns: Series or DataFrame: Computed prod of values within each group. @@ -214,7 +214,7 @@ def min( Include only float, int, boolean columns. min_count (int, default 0): The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. + than ``min_count`` and non-NA values are present, the result will be NA. Returns: Series or DataFrame: Computed min of values within each group. @@ -234,7 +234,7 @@ def max( Include only float, int, boolean columns. min_count (int, default 0): The required number of valid values to perform the operation. If fewer - than ``min_count`` non-NA values are present the result will be NA. + than ``min_count`` and non-NA values are present, the result will be NA. Returns: Series or DataFrame: Computed max of values within each group. diff --git a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py index b08eb104924..4b0bd427061 100644 --- a/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py +++ b/third_party/bigframes_vendored/sklearn/compose/_column_transformer.py @@ -19,9 +19,9 @@ class ColumnTransformer(_BaseComposition): """Applies transformers to columns of BigQuery DataFrames. This estimator allows different columns or column subsets of the input - to be transformed separately and the features generated by each transformer + to be transformed separately, and the features generated by each transformer will be concatenated to form a single feature space. - This is useful for heterogeneous or columnar data, to combine several + This is useful for heterogeneous or columnar data to combine several feature extraction mechanisms or transformations into a single transformer. Args: diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index dcce75d1d90..f126e0439db 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -22,8 +22,8 @@ class PCA(BaseEstimator, metaclass=ABCMeta): Args: n_components (int, float or None, default None): - Number of components to keep. - If n_components is not set all components are kept. n_components = min(n_samples, n_features). + Number of components to keep. If n_components is not set all + components are kept, n_components = min(n_samples, n_features). If 0 < n_components < 1, select the number of components such that the amount of variance that needs to be explained is greater than the percentage specified by n_components. svd_solver ("full", "randomized" or "auto", default "auto"): The solver to use to calculate the principal components. Details: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-pca#pca_solver. diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 88ff32ea06d..494c730a6d6 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -38,7 +38,7 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): automatically adjust weights inversely proportional to class frequencies in the input data as ``n_samples / (n_classes * np.bincount(y))``. Dict isn't - supported now. + supported. l1_reg (float or None, default None): The amount of L1 regularization applied. Default to None. Can't be set in "normal_equation" mode. If unset, value 0 is used. l2_reg (float, default 0.0): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py index 5fcc4815730..98b9d0371fe 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py @@ -18,7 +18,7 @@ class KBinsDiscretizer(TransformerMixin, BaseEstimator): strategy ({'uniform', 'quantile'}, default='quantile'): Strategy used to define the widths of the bins. 'uniform': All bins in each feature have identical widths. 'quantile': All bins in each - feature have the same number of points. Only `uniform` is supported now. + feature have the same number of points. Only `uniform` is supported. """ def fit(self, X, y=None): From d805241b7ec99fcb7579dce778d4b04778a72002 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:29:44 -0700 Subject: [PATCH 454/921] feat: add transformers save/load (#552) * feat: add transformers save/load * fix mypy --- bigframes/ml/base.py | 30 ++++- bigframes/ml/compose.py | 54 +------- bigframes/ml/loader.py | 11 +- bigframes/ml/preprocessing.py | 10 ++ tests/system/large/ml/test_compose.py | 1 + tests/system/large/ml/test_pipeline.py | 6 +- tests/system/small/ml/test_core.py | 2 +- tests/system/small/ml/test_llm.py | 24 ++-- tests/system/small/ml/test_preprocessing.py | 130 ++++++++++++++++---- 9 files changed, 173 insertions(+), 95 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index e58ed4feef6..5e7aada8de0 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -178,7 +178,33 @@ def fit( return self._fit(X, y) -class Transformer(BaseEstimator): +class BaseTransformer(BaseEstimator): + """Transformer base class.""" + + def __init__(self): + self._bqml_model: Optional[core.BqmlModel] = None + + _T = TypeVar("_T", bound="BaseTransformer") + + def to_gbq(self: _T, model_name: str, replace: bool = False) -> _T: + """Save the transformer as a BigQuery model. + + Args: + model_name (str): + the name of the model. + replace (bool, default False): + whether to replace if the model already exists. Default to False. + + Returns: + Saved transformer.""" + if not self._bqml_model: + raise RuntimeError("A transformer must be fitted before it can be saved") + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) + + +class Transformer(BaseTransformer): """A BigQuery DataFrames Transformer base class that transforms data. Also the transformers can be attached to a pipeline with a predictor.""" @@ -199,7 +225,7 @@ def fit_transform( return self.fit(X, y).transform(X) -class LabelTransformer(BaseEstimator): +class LabelTransformer(BaseTransformer): """A BigQuery DataFrames Label Transformer base class that transforms data. Also the transformers can be attached to a pipeline with a predictor.""" diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 21cfba8e016..8638f4d182f 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -26,21 +26,11 @@ import bigframes_vendored.sklearn.compose._column_transformer from google.cloud import bigquery -import bigframes from bigframes import constants from bigframes.core import log_adapter from bigframes.ml import base, core, globals, preprocessing, utils import bigframes.pandas as bpd -_PREPROCESSING_TYPES = Union[ - preprocessing.OneHotEncoder, - preprocessing.StandardScaler, - preprocessing.MaxAbsScaler, - preprocessing.MinMaxScaler, - preprocessing.KBinsDiscretizer, - preprocessing.LabelEncoder, -] - _BQML_TRANSFROM_TYPE_MAPPING = types.MappingProxyType( { "ML.STANDARD_SCALER": preprocessing.StandardScaler, @@ -67,7 +57,7 @@ def __init__( transformers: List[ Tuple[ str, - _PREPROCESSING_TYPES, + preprocessing.PreprocessingType, Union[str, List[str]], ] ], @@ -82,12 +72,12 @@ def __init__( @property def transformers_( self, - ) -> List[Tuple[str, _PREPROCESSING_TYPES, str,]]: + ) -> List[Tuple[str, preprocessing.PreprocessingType, str,]]: """The collection of transformers as tuples of (name, transformer, column).""" result: List[ Tuple[ str, - _PREPROCESSING_TYPES, + preprocessing.PreprocessingType, str, ] ] = [] @@ -105,15 +95,6 @@ def transformers_( return result - @classmethod - def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model - ) -> ColumnTransformer: - col_transformer = cls._extract_from_bq_model(model) - col_transformer._bqml_model = core.BqmlModel(session, model) - - return col_transformer - @classmethod def _extract_from_bq_model( cls, @@ -125,7 +106,7 @@ def _extract_from_bq_model( transformers: List[ Tuple[ str, - _PREPROCESSING_TYPES, + preprocessing.PreprocessingType, Union[str, List[str]], ] ] = [] @@ -164,15 +145,7 @@ def camel_to_snake(name): def _merge( self, bq_model: bigquery.Model - ) -> Union[ - ColumnTransformer, - preprocessing.StandardScaler, - preprocessing.OneHotEncoder, - preprocessing.MaxAbsScaler, - preprocessing.MinMaxScaler, - preprocessing.KBinsDiscretizer, - preprocessing.LabelEncoder, - ]: + ) -> Union[ColumnTransformer, preprocessing.PreprocessingType,]: """Try to merge the column transformer to a simple transformer. Depends on all the columns in bq_model are transformed with the same transformer.""" transformers = self.transformers_ @@ -249,20 +222,3 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: bpd.DataFrame, df[self._output_names], ) - - def to_gbq(self, model_name: str, replace: bool = False) -> ColumnTransformer: - """Save the transformer as a BigQuery model. - - Args: - model_name (str): - the name of the model. - replace (bool, default False): - whether to replace if the model already exists. Default to False. - - Returns: - ColumnTransformer: saved model.""" - if not self._bqml_model: - raise RuntimeError("A transformer must be fitted before it can be saved") - - new_model = self._bqml_model.copy(model_name, replace) - return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 508003a98d6..c6e38e6534f 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -24,6 +24,7 @@ from bigframes.ml import ( cluster, compose, + core, decomposition, ensemble, forecasting, @@ -31,6 +32,7 @@ linear_model, llm, pipeline, + preprocessing, utils, ) @@ -81,6 +83,7 @@ def from_bq( llm.PaLM2TextEmbeddingGenerator, pipeline.Pipeline, compose.ColumnTransformer, + preprocessing.PreprocessingType, ]: """Load a BQML model to BigQuery DataFrames ML. @@ -107,8 +110,12 @@ def from_bq( def _transformer_from_bq(session: bigframes.Session, bq_model: bigquery.Model): - # TODO(garrettwu): add other transformers - return compose.ColumnTransformer._from_bq(session, bq_model) + transformer = compose.ColumnTransformer._extract_from_bq_model(bq_model)._merge( + bq_model + ) + transformer._bqml_model = core.BqmlModel(session, bq_model) + + return transformer def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 23eab429784..fd7d44f731b 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -639,3 +639,13 @@ def transform(self, y: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: bpd.DataFrame, df[self._output_names], ) + + +PreprocessingType = Union[ + OneHotEncoder, + StandardScaler, + MaxAbsScaler, + MinMaxScaler, + KBinsDiscretizer, + LabelEncoder, +] diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index d7c49ca95a0..72e016f4bb7 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -151,3 +151,4 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), ] assert reloaded_transformer.transformers_ == expected + assert reloaded_transformer._bqml_model is not None diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index c460efa75f7..c165b1e0301 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -222,7 +222,7 @@ def test_pipeline_logistic_regression_fit_score_predict( ) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_index): """Test a supervised model with a minimal preprocessing step""" pl = pipeline.Pipeline( @@ -297,7 +297,7 @@ def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_in ) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_pipeline_random_forest_classifier_fit_score_predict( session, penguins_df_default_index ): @@ -445,7 +445,7 @@ def test_pipeline_PCA_fit_score_predict(session, penguins_df_default_index): ) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_pipeline_standard_scaler_kmeans_fit_score_predict( session, penguins_pandas_df_default_index ): diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 02030cd31e7..c505057d7b5 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -333,7 +333,7 @@ def test_remote_model_predict( ) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_model_generate_text( bqml_palm2_text_generator_model: core.BqmlModel, llm_text_df ): diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 2e135bef7b0..e526d543622 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -49,7 +49,7 @@ def test_create_text_generator_32k_model( assert reloaded_model.connection_name == bq_connection -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_create_text_generator_model_default_session( bq_connection, llm_text_pandas_df, bigquery_client ): @@ -76,7 +76,7 @@ def test_create_text_generator_model_default_session( assert all(series.str.len() > 20) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_create_text_generator_32k_model_default_session( bq_connection, llm_text_pandas_df, bigquery_client ): @@ -103,7 +103,7 @@ def test_create_text_generator_32k_model_default_session( assert all(series.str.len() > 20) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_create_text_generator_model_default_connection( llm_text_pandas_df, bigquery_client ): @@ -131,7 +131,7 @@ def test_create_text_generator_model_default_connection( # Marked as flaky only because BQML LLM is in preview, the service only has limited capacity, not stable enough. -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_text_generator_predict_default_params_success( palm2_text_generator_model, llm_text_df ): @@ -142,7 +142,7 @@ def test_text_generator_predict_default_params_success( assert all(series.str.len() > 20) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_text_generator_predict_series_default_params_success( palm2_text_generator_model, llm_text_df ): @@ -153,7 +153,7 @@ def test_text_generator_predict_series_default_params_success( assert all(series.str.len() > 20) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_text_generator_predict_arbitrary_col_label_success( palm2_text_generator_model, llm_text_df ): @@ -165,7 +165,7 @@ def test_text_generator_predict_arbitrary_col_label_success( assert all(series.str.len() > 20) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_text_generator_predict_with_params_success( palm2_text_generator_model, llm_text_df ): @@ -255,7 +255,7 @@ def test_create_text_embedding_generator_multilingual_model_defaults(bq_connecti assert model._bqml_model is not None -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_embedding_generator_predict_success( palm2_embedding_generator_model, llm_text_df ): @@ -267,7 +267,7 @@ def test_embedding_generator_predict_success( assert len(value) == 768 -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_embedding_generator_multilingual_predict_success( palm2_embedding_generator_multilingual_model, llm_text_df ): @@ -279,7 +279,7 @@ def test_embedding_generator_multilingual_predict_success( assert len(value) == 768 -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_embedding_generator_predict_series_success( palm2_embedding_generator_model, llm_text_df ): @@ -306,7 +306,7 @@ def test_create_gemini_text_generator_model( assert reloaded_model.connection_name == bq_connection -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_gemini_text_generator_predict_default_params_success( gemini_text_generator_model, llm_text_df ): @@ -317,7 +317,7 @@ def test_gemini_text_generator_predict_default_params_success( assert all(series.str.len() > 20) -@pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2) def test_gemini_text_generator_predict_with_params_success( gemini_text_generator_model, llm_text_df ): diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 990795da3b0..040111f38ad 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -18,7 +18,7 @@ import pyarrow as pa import bigframes.features -import bigframes.ml.preprocessing +from bigframes.ml import preprocessing ONE_HOT_ENCODED_DTYPE = ( pd.ArrowDtype(pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())]))) @@ -29,7 +29,7 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. - scaler = bigframes.ml.preprocessing.StandardScaler() + scaler = preprocessing.StandardScaler() scaler.fit( penguins_df_default_index[ ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] @@ -68,7 +68,7 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): def test_standard_scaler_normalizeds_fit_transform(new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. - scaler = bigframes.ml.preprocessing.StandardScaler() + scaler = preprocessing.StandardScaler() result = scaler.fit_transform( new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] ).to_pandas() @@ -97,7 +97,7 @@ def test_standard_scaler_normalizeds_fit_transform(new_penguins_df): def test_standard_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.StandardScaler, when BQML's change is in prod. - scaler = bigframes.ml.preprocessing.StandardScaler() + scaler = preprocessing.StandardScaler() scaler.fit(penguins_df_default_index["culmen_length_mm"]) result = scaler.transform(penguins_df_default_index["culmen_length_mm"]).to_pandas() @@ -128,9 +128,22 @@ def test_standard_scaler_series_normalizes(penguins_df_default_index, new_pengui pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_standard_scaler_save_load(new_penguins_df, dataset_id): + transformer = preprocessing.StandardScaler() + transformer.fit( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.StandardScaler) + assert reloaded_transformer._bqml_model is not None + + def test_max_abs_scaler_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.MaxAbsScaler, when BQML's change is in prod. - scaler = bigframes.ml.preprocessing.MaxAbsScaler() + scaler = preprocessing.MaxAbsScaler() scaler.fit( penguins_df_default_index[ ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] @@ -168,7 +181,7 @@ def test_max_abs_scaler_normalizes(penguins_df_default_index, new_penguins_df): def test_max_abs_scaler_normalizeds_fit_transform(new_penguins_df): - scaler = bigframes.ml.preprocessing.MaxAbsScaler() + scaler = preprocessing.MaxAbsScaler() result = scaler.fit_transform( new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] ).to_pandas() @@ -192,7 +205,7 @@ def test_max_abs_scaler_normalizeds_fit_transform(new_penguins_df): def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): - scaler = bigframes.ml.preprocessing.MaxAbsScaler() + scaler = preprocessing.MaxAbsScaler() scaler.fit(penguins_df_default_index["culmen_length_mm"]) result = scaler.transform(penguins_df_default_index["culmen_length_mm"]).to_pandas() @@ -219,8 +232,21 @@ def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguin pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_max_abs_scaler_save_load(new_penguins_df, dataset_id): + transformer = preprocessing.MaxAbsScaler() + transformer.fit( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.MaxAbsScaler) + assert reloaded_transformer._bqml_model is not None + + def test_min_max_scaler_normalized_fit_transform(new_penguins_df): - scaler = bigframes.ml.preprocessing.MinMaxScaler() + scaler = preprocessing.MinMaxScaler() result = scaler.fit_transform( new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] ).to_pandas() @@ -244,7 +270,7 @@ def test_min_max_scaler_normalized_fit_transform(new_penguins_df): def test_min_max_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): - scaler = bigframes.ml.preprocessing.MinMaxScaler() + scaler = preprocessing.MinMaxScaler() scaler.fit(penguins_df_default_index["culmen_length_mm"]) result = scaler.transform(penguins_df_default_index["culmen_length_mm"]).to_pandas() @@ -274,7 +300,7 @@ def test_min_max_scaler_series_normalizes(penguins_df_default_index, new_penguin def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.MinMaxScaler, when BQML's change is in prod. - scaler = bigframes.ml.preprocessing.MinMaxScaler() + scaler = preprocessing.MinMaxScaler() scaler.fit( penguins_df_default_index[ ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] @@ -312,8 +338,21 @@ def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_min_max_scaler_save_load(new_penguins_df, dataset_id): + transformer = preprocessing.MinMaxScaler() + transformer.fit( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.MinMaxScaler) + assert reloaded_transformer._bqml_model is not None + + def test_k_bins_discretizer_normalized_fit_transform_default_params(new_penguins_df): - discretizer = bigframes.ml.preprocessing.KBinsDiscretizer(strategy="uniform") + discretizer = preprocessing.KBinsDiscretizer(strategy="uniform") result = discretizer.fit_transform( new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] ).to_pandas() @@ -339,7 +378,7 @@ def test_k_bins_discretizer_normalized_fit_transform_default_params(new_penguins def test_k_bins_discretizer_series_normalizes( penguins_df_default_index, new_penguins_df ): - discretizer = bigframes.ml.preprocessing.KBinsDiscretizer(strategy="uniform") + discretizer = preprocessing.KBinsDiscretizer(strategy="uniform") discretizer.fit(penguins_df_default_index["culmen_length_mm"]) result = discretizer.transform( @@ -365,7 +404,7 @@ def test_k_bins_discretizer_series_normalizes( def test_k_bins_discretizer_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.KBinsDiscretizer, when BQML's change is in prod. - discretizer = bigframes.ml.preprocessing.KBinsDiscretizer(strategy="uniform") + discretizer = preprocessing.KBinsDiscretizer(strategy="uniform") discretizer.fit( penguins_df_default_index[ ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] @@ -402,9 +441,7 @@ def test_k_bins_discretizer_normalizes_different_params( penguins_df_default_index, new_penguins_df ): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.KBinsDiscretizer, when BQML's change is in prod. - discretizer = bigframes.ml.preprocessing.KBinsDiscretizer( - n_bins=6, strategy="uniform" - ) + discretizer = preprocessing.KBinsDiscretizer(n_bins=6, strategy="uniform") discretizer.fit( penguins_df_default_index[ ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] @@ -437,8 +474,23 @@ def test_k_bins_discretizer_normalizes_different_params( pd.testing.assert_frame_equal(result, expected, rtol=1e-3) +def test_k_bins_discretizer_save_load(new_penguins_df, dataset_id): + transformer = preprocessing.KBinsDiscretizer(n_bins=6, strategy="uniform") + transformer.fit( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.KBinsDiscretizer) + assert reloaded_transformer.n_bins == transformer.n_bins + assert reloaded_transformer.strategy == transformer.strategy + assert reloaded_transformer._bqml_model is not None + + def test_one_hot_encoder_default_params(new_penguins_df): - encoder = bigframes.ml.preprocessing.OneHotEncoder() + encoder = preprocessing.OneHotEncoder() encoder.fit(new_penguins_df[["species", "sex"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -469,7 +521,7 @@ def test_one_hot_encoder_default_params(new_penguins_df): def test_one_hot_encoder_default_params_fit_transform(new_penguins_df): - encoder = bigframes.ml.preprocessing.OneHotEncoder() + encoder = preprocessing.OneHotEncoder() result = encoder.fit_transform(new_penguins_df[["species", "sex"]]).to_pandas() @@ -499,7 +551,7 @@ def test_one_hot_encoder_default_params_fit_transform(new_penguins_df): def test_one_hot_encoder_series_default_params(new_penguins_df): - encoder = bigframes.ml.preprocessing.OneHotEncoder() + encoder = preprocessing.OneHotEncoder() encoder.fit(new_penguins_df["species"]) result = encoder.transform(new_penguins_df).to_pandas() @@ -525,7 +577,7 @@ def test_one_hot_encoder_series_default_params(new_penguins_df): def test_one_hot_encoder_params(new_penguins_df): - encoder = bigframes.ml.preprocessing.OneHotEncoder("most_frequent", 100, 2) + encoder = preprocessing.OneHotEncoder("most_frequent", 100, 2) encoder.fit(new_penguins_df[["species", "sex"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -556,7 +608,7 @@ def test_one_hot_encoder_params(new_penguins_df): def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_df): - encoder = bigframes.ml.preprocessing.OneHotEncoder() + encoder = preprocessing.OneHotEncoder() encoder.fit(penguins_df_default_index[["species", "sex"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -586,8 +638,21 @@ def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_ pd.testing.assert_frame_equal(result, expected) +def test_one_hot_encoder_save_load(new_penguins_df, dataset_id): + transformer = preprocessing.OneHotEncoder(min_frequency=1, max_categories=10) + transformer.fit(new_penguins_df[["species", "sex"]]) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.OneHotEncoder) + assert reloaded_transformer.min_frequency == transformer.min_frequency + assert reloaded_transformer.max_categories == transformer.max_categories + assert reloaded_transformer._bqml_model is not None + + def test_label_encoder_default_params(new_penguins_df): - encoder = bigframes.ml.preprocessing.LabelEncoder() + encoder = preprocessing.LabelEncoder() encoder.fit(new_penguins_df["species"]) result = encoder.transform(new_penguins_df["species"]).to_pandas() @@ -613,7 +678,7 @@ def test_label_encoder_default_params(new_penguins_df): def test_label_encoder_default_params_fit_transform(new_penguins_df): - encoder = bigframes.ml.preprocessing.LabelEncoder() + encoder = preprocessing.LabelEncoder() result = encoder.fit_transform(new_penguins_df[["species"]]).to_pandas() @@ -638,7 +703,7 @@ def test_label_encoder_default_params_fit_transform(new_penguins_df): def test_label_encoder_series_default_params(new_penguins_df): - encoder = bigframes.ml.preprocessing.LabelEncoder() + encoder = preprocessing.LabelEncoder() encoder.fit(new_penguins_df["species"]) result = encoder.transform(new_penguins_df).to_pandas() @@ -664,7 +729,7 @@ def test_label_encoder_series_default_params(new_penguins_df): def test_label_encoder_params(new_penguins_df): - encoder = bigframes.ml.preprocessing.LabelEncoder(100, 2) + encoder = preprocessing.LabelEncoder(100, 2) encoder.fit(new_penguins_df[["species"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -690,7 +755,7 @@ def test_label_encoder_params(new_penguins_df): def test_label_encoder_different_data(penguins_df_default_index, new_penguins_df): - encoder = bigframes.ml.preprocessing.LabelEncoder() + encoder = preprocessing.LabelEncoder() encoder.fit(penguins_df_default_index[["species"]]) result = encoder.transform(new_penguins_df).to_pandas() @@ -715,4 +780,17 @@ def test_label_encoder_different_data(penguins_df_default_index, new_penguins_df pd.testing.assert_frame_equal(result, expected) +def test_label_encoder_save_load(new_penguins_df, dataset_id): + transformer = preprocessing.LabelEncoder(min_frequency=1, max_categories=10) + transformer.fit(new_penguins_df[["species"]]) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.LabelEncoder) + assert reloaded_transformer.min_frequency == transformer.min_frequency + assert reloaded_transformer.max_categories == transformer.max_categories + assert reloaded_transformer._bqml_model is not None + + # TODO(garrettwu): add OneHotEncoder tests to compare with sklearn. From 483390830ae0ee2fe0fb47dc7d2aea143b2dc7d8 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 1 Apr 2024 16:02:17 -0700 Subject: [PATCH 455/921] fix: Respect hard stack size limit and swallow limit change exception. (#558) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/pandas/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index b6476c5eb88..fc008f36e5c 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -714,9 +714,13 @@ def to_datetime( # which the applicable limit is now hard coded. See: # https://github.com/python/cpython/issues/112282 sys.setrecursionlimit(max(10000000, sys.getrecursionlimit())) -resource.setrlimit( - resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) -) + +soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_STACK) +if soft_limit < hard_limit or hard_limit == resource.RLIM_INFINITY: + try: + resource.setrlimit(resource.RLIMIT_STACK, (hard_limit, hard_limit)) + except Exception: + pass # Use __all__ to let type checkers know what is part of the public API. __all___ = [ From 4995c0046265463bc5c502cbeb34c7632d5a255e Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 1 Apr 2024 20:47:19 -0700 Subject: [PATCH 456/921] fix: rename PaLM2TextEmbeddingGenerator.predict output columns to be backward compatible (#561) --- bigframes/ml/llm.py | 9 +- .../bq_dataframes_llm_kmeans.ipynb | 1066 ++++++++--------- tests/system/small/ml/test_llm.py | 12 +- 3 files changed, 527 insertions(+), 560 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 031656f1d80..ffaeb399bbc 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -44,7 +44,7 @@ _GEMINI_PRO_ENDPOINT = "gemini-pro" _ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" -_ML_EMBED_TEXT_STATUS = "ml_generate_embedding_status" +_ML_EMBED_TEXT_STATUS = "ml_embed_text_status" @log_adapter.class_logger @@ -390,6 +390,13 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: } df = self._bqml_model.generate_embedding(X, options) + df = df.rename( + columns={ + "ml_generate_embedding_result": "text_embedding", + "ml_generate_embedding_statistics": "statistics", + "ml_generate_embedding_status": _ML_EMBED_TEXT_STATUS, + } + ) if (df[_ML_EMBED_TEXT_STATUS] != "").any(): warnings.warn( diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 2c6d109ba8d..ab6fd93f9a3 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -289,7 +289,7 @@ { "data": { "text/html": [ - "Query job 030e5d08-f690-47e4-b7cc-342731245575 is DONE. 2.3 GB processed. Open Job" + "Query job 952b852e-7cf0-493d-8258-fe60daf45ebf is DONE. 2.3 GB processed. Open Job" ], "text/plain": [ "" @@ -301,7 +301,7 @@ { "data": { "text/html": [ - "Query job a9c5f416-c5d2-4209-b639-bccb81a25d7e is DONE. 58.8 MB processed. Open Job" + "Query job f9939880-6c66-4da5-9e90-daf8d9a9d83c is DONE. 50.3 MB processed. Open Job" ], "text/plain": [ "" @@ -336,24 +336,24 @@ " \n", " \n", " \n", - " 1053364\n", - " My Macy 's American Express account was taken ...\n", + " 1799560\n", + " Thursday, XX/XX/XXXX, unauthorized charges wer...\n", " \n", " \n", - " 1053757\n", - " I am a victim of identity theft. The informati...\n", + " 1800272\n", + " The credit reporting company is reporting inac...\n", " \n", " \n", - " 1053784\n", - " In XXXX 2016, Amex took out $ XXXX.+ unauthori...\n", + " 1800409\n", + " In accordance with the Fair Credit Reporting a...\n", " \n", " \n", - " 1054237\n", - " I am not for sure the exact date of my loan it...\n", + " 1800550\n", + " I told the credit bureaus to \" investigate eve...\n", " \n", " \n", - " 1054244\n", - " I entered a consumer credit transaction with t...\n", + " 1800818\n", + " Im writing in reference regarding XXXXXXXX XXX...\n", " \n", " \n", "\n", @@ -361,11 +361,11 @@ ], "text/plain": [ " consumer_complaint_narrative\n", - "1053364 My Macy 's American Express account was taken ...\n", - "1053757 I am a victim of identity theft. The informati...\n", - "1053784 In XXXX 2016, Amex took out $ XXXX.+ unauthori...\n", - "1054237 I am not for sure the exact date of my loan it...\n", - "1054244 I entered a consumer credit transaction with t..." + "1799560 Thursday, XX/XX/XXXX, unauthorized charges wer...\n", + "1800272 The credit reporting company is reporting inac...\n", + "1800409 In accordance with the Fair Credit Reporting a...\n", + "1800550 I told the credit bureaus to \" investigate eve...\n", + "1800818 Im writing in reference regarding XXXXXXXX XXX..." ] }, "execution_count": 7, @@ -418,7 +418,7 @@ { "data": { "text/html": [ - "Query job 77eee871-31eb-4939-a015-f5505c94786e is DONE. 0 Bytes processed. Open Job" + "Query job e3ff0549-f0ee-4508-bb4f-beea14bf54f5 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -444,7 +444,7 @@ { "data": { "text/html": [ - "Query job 63cdd004-21b6-41bf-8876-aa646f1f268e is DONE. 1.3 GB processed. Open Job" + "Query job 5b3d8f8c-9e8d-4378-b4df-e3328300f17a is DONE. 1.3 GB processed. Open Job" ], "text/plain": [ "" @@ -456,7 +456,7 @@ { "data": { "text/html": [ - "Query job cda12546-9931-48f6-8b22-74a9ab85fa28 is DONE. 80.0 kB processed. Open Job" + "Query job f35c2982-4953-45fa-84bd-d0ce04e13c5e is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -468,7 +468,7 @@ { "data": { "text/html": [ - "Query job 759a13c5-c02f-4ae8-9b22-d7ef423ffe8d is DONE. 20.0 kB processed. Open Job" + "Query job b70c55a3-b18b-4313-86b0-31f5b3b570fb is DONE. 20.0 kB processed. Open Job" ], "text/plain": [ "" @@ -480,7 +480,19 @@ { "data": { "text/html": [ - "Query job 1bad8ef3-8103-4a98-bec4-699d97673b9a is DONE. 72.0 MB processed. Open Job" + "Query job 2b2cfd9f-c713-4411-a3ca-1916cec84ff0 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 09cadae1-1c66-43cf-a76f-7495b0123006 is DONE. 71.9 MB processed. Open Job" ], "text/plain": [ "" @@ -510,188 +522,187 @@ " \n", " \n", " \n", - " ml_generate_embedding_result\n", - " ml_generate_embedding_statistics\n", - " ml_generate_embedding_status\n", + " text_embedding\n", + " statistics\n", + " ml_embed_text_status\n", " content\n", " \n", " \n", " \n", " \n", - " 357\n", - " [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-...\n", - " {\"token_count\":306,\"truncated\":false}\n", + " 782\n", + " [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-...\n", + " {\"token_count\":121,\"truncated\":false}\n", " \n", - " I decided to try XXXX services for my wife and...\n", + " I 've sent multiple letters to this agency abo...\n", " \n", " \n", - " 428\n", - " [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-...\n", - " {\"token_count\":134,\"truncated\":false}\n", + " 795\n", + " [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-...\n", + " {\"token_count\":141,\"truncated\":false}\n", " \n", - " XXXX I went to the bank in question ( XXXX XXX...\n", + " I receive social security XXXX funds in my XXX...\n", " \n", " \n", - " 1319\n", - " [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-...\n", - " {\"token_count\":215,\"truncated\":false}\n", + " 861\n", + " [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-...\n", + " {\"token_count\":160,\"truncated\":false}\n", " \n", - " I currently have a home loan with my ex husban...\n", + " Hello, My name is XXXX XXXX XXXX. I have a pro...\n", " \n", " \n", - " 1993\n", - " [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-...\n", - " {\"token_count\":536,\"truncated\":false}\n", + " 1103\n", + " [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-...\n", + " {\"token_count\":31,\"truncated\":false}\n", " \n", - " NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800....\n", + " The debt occurred more than 7 years in the pas...\n", " \n", " \n", - " 1997\n", - " [ 0.03145148 -0.01011822 -0.02316323 -0.025078...\n", - " {\"token_count\":123,\"truncated\":false}\n", + " 1241\n", + " [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-...\n", + " {\"token_count\":23,\"truncated\":false}\n", " \n", - " After a while the payments became harder and h...\n", + " UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA...\n", " \n", " \n", - " 2469\n", - " [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-...\n", - " {\"token_count\":60,\"truncated\":false}\n", + " 1729\n", + " [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-...\n", + " {\"token_count\":382,\"truncated\":false}\n", " \n", - " In the course of my student loan, I have been ...\n", + " XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort...\n", " \n", " \n", - " 2624\n", - " [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-...\n", - " {\"token_count\":254,\"truncated\":false}\n", + " 2167\n", + " [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-...\n", + " {\"token_count\":556,\"truncated\":false}\n", " \n", - " In accordance with the Fair Credit Reporting A...\n", + " This is the third such complaint I have submit...\n", " \n", " \n", - " 2832\n", - " [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-...\n", - " {\"token_count\":79,\"truncated\":false}\n", + " 2219\n", + " [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-...\n", + " {\"token_count\":196,\"truncated\":false}\n", " \n", - " LVNV FUNDING LLC is continually placing a coll...\n", + " Found and add online for a Prepaid Credit card...\n", " \n", " \n", - " 3328\n", - " [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-...\n", - " {\"token_count\":156,\"truncated\":false}\n", + " 2392\n", + " [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-...\n", + " {\"token_count\":641,\"truncated\":false}\n", " \n", - " On XX/XX/2020 I sent a letter regarding inaccu...\n", + " I am furnishing this complaint against Fed Loa...\n", " \n", " \n", - " 3650\n", - " [-6.10093866e-03 -5.93599863e-02 -8.04531425e-...\n", - " {\"token_count\":175,\"truncated\":false}\n", + " 2528\n", + " [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-...\n", + " {\"token_count\":176,\"truncated\":false}\n", " \n", - " Over a year and a half ago we started the proc...\n", + " Despite multiple written requests, the unverif...\n", " \n", " \n", - " 3860\n", - " [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-...\n", - " {\"token_count\":1267,\"truncated\":false}\n", + " 2737\n", + " [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-...\n", + " {\"token_count\":230,\"truncated\":false}\n", " \n", - " The issue is 26 late payments on me and my wif...\n", + " After unsatisfying communication in the messag...\n", " \n", " \n", - " 4464\n", - " [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-...\n", - " {\"token_count\":906,\"truncated\":false}\n", + " 2859\n", + " [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-...\n", + " {\"token_count\":238,\"truncated\":false}\n", " \n", - " I purchased as replacement for a lost XXXX XXX...\n", + " Good Morning. My name is XXXX XXXX. My account...\n", " \n", " \n", - " 4470\n", - " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", - " {\"token_count\":200,\"truncated\":false}\n", + " 3439\n", + " [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-...\n", + " {\"token_count\":197,\"truncated\":false}\n", " \n", - " in accordance with the Fair Credit Reporting a...\n", + " I have ongoing disputes that are preventing me...\n", " \n", " \n", - " 4567\n", - " [-5.49167022e-03 -3.84587422e-02 -8.56091827e-...\n", - " {\"token_count\":110,\"truncated\":false}\n", + " 3738\n", + " [ 0.01422119 -0.01114973 -0.04438976 -0.024421...\n", + " {\"token_count\":160,\"truncated\":false}\n", " \n", - " I have submitted multiple disputes through the...\n", + " I had a loan with national Collegiate Trust. i...\n", " \n", " \n", - " 4713\n", - " [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-...\n", - " {\"token_count\":549,\"truncated\":false}\n", + " 3805\n", + " [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-...\n", + " {\"token_count\":477,\"truncated\":false}\n", " \n", - " While shopping for furniture for my home I ope...\n", + " Hi I am submitting this XXXX XXXX this isn't a...\n", " \n", " \n", - " 5181\n", - " [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-...\n", - " {\"token_count\":77,\"truncated\":false}\n", + " 3915\n", + " [-7.23852217e-03 -4.69538383e-02 -5.60489520e-...\n", + " {\"token_count\":116,\"truncated\":false}\n", " \n", - " I had opened a Wells Fargo checking account wi...\n", + " portfolio is showin on my credit report with a...\n", " \n", " \n", - " 5511\n", - " [-0.00217485 -0.04031368 -0.06604777 -0.052006...\n", - " {\"token_count\":262,\"truncated\":false}\n", + " 3917\n", + " [-8.92711710e-03 -4.49132621e-02 -4.29662578e-...\n", + " {\"token_count\":71,\"truncated\":false}\n", " \n", - " I recently disputed ( see attached letter ) wi...\n", + " the company shared my information with another...\n", " \n", " \n", - " 5888\n", - " [-8.15972779e-03 -3.46563384e-02 -5.91776446e-...\n", - " {\"token_count\":176,\"truncated\":false}\n", + " 4281\n", + " [-1.69487391e-02 -1.89835522e-02 -3.80971469e-...\n", + " {\"token_count\":130,\"truncated\":false}\n", " \n", - " XXXX XXXX XXXX XXXX \n", - "I have disputed this acco...\n", + " I tried to submit a teacher loan forgiveness a...\n", " \n", " \n", - " 6299\n", - " [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-...\n", - " {\"token_count\":151,\"truncated\":false}\n", + " 4470\n", + " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", + " {\"token_count\":200,\"truncated\":false}\n", " \n", - " XXXX ; XXXX and Transunion are reporting ( 30 ...\n", + " in accordance with the Fair Credit Reporting a...\n", " \n", " \n", - " 7143\n", - " [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-...\n", - " {\"token_count\":234,\"truncated\":false}\n", + " 4915\n", + " [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-...\n", + " {\"token_count\":23,\"truncated\":false}\n", " \n", - " My Macys account is due on the first of every ...\n", + " XXXX XXXX did not give me a receipt or a copy ...\n", " \n", " \n", - " 7219\n", - " [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-...\n", - " {\"token_count\":26,\"truncated\":false}\n", + " 4928\n", + " [-4.43694415e-03 -3.66776163e-04 -9.08496231e-...\n", + " {\"token_count\":83,\"truncated\":false}\n", " \n", - " Keep getting letters and calls from collection...\n", + " This company has filed a civil suit during a g...\n", " \n", " \n", - " 7574\n", - " [-0.00149564 -0.06619431 -0.05084481 -0.048579...\n", - " {\"token_count\":129,\"truncated\":false}\n", + " 5338\n", + " [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-...\n", + " {\"token_count\":1279,\"truncated\":false}\n", " \n", - " On XXXX I was on the XXXX app and there was a ...\n", + " My credit report contains errors that is keepi...\n", " \n", " \n", - " 8759\n", - " [ 0.01501553 -0.03575936 -0.050562 -0.034884...\n", - " {\"token_count\":501,\"truncated\":false}\n", + " 5582\n", + " [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-...\n", + " {\"token_count\":396,\"truncated\":false}\n", " \n", - " Obviously I've been a victim of fraud, therefo...\n", + " Coast Professional, XXXX, LA contacted me by m...\n", " \n", " \n", - " 9700\n", - " [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-...\n", - " {\"token_count\":48,\"truncated\":false}\n", + " 6386\n", + " [ 3.33276950e-02 1.53224478e-02 -1.89354066e-...\n", + " {\"token_count\":79,\"truncated\":false}\n", " \n", - " The following item have not been properly inve...\n", + " Cares act refund requested in XXXX, called mul...\n", " \n", " \n", - " 9822\n", - " [ 2.95880195e-02 1.65440738e-02 -3.33247967e-...\n", - " {\"token_count\":2373,\"truncated\":true}\n", + " 6956\n", + " [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-...\n", + " {\"token_count\":194,\"truncated\":false}\n", " \n", - " During the housing market crash I went through...\n", + " n accordance with the Fair Credit Reporting ac...\n", " \n", " \n", "\n", @@ -699,87 +710,86 @@ "[10000 rows x 4 columns in total]" ], "text/plain": [ - " ml_generate_embedding_result \\\n", - "357 [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-... \n", - "428 [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-... \n", - "1319 [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-... \n", - "1993 [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-... \n", - "1997 [ 0.03145148 -0.01011822 -0.02316323 -0.025078... \n", - "2469 [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-... \n", - "2624 [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-... \n", - "2832 [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-... \n", - "3328 [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-... \n", - "3650 [-6.10093866e-03 -5.93599863e-02 -8.04531425e-... \n", - "3860 [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-... \n", - "4464 [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-... \n", + " text_embedding \\\n", + "782 [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-... \n", + "795 [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-... \n", + "861 [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-... \n", + "1103 [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-... \n", + "1241 [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-... \n", + "1729 [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-... \n", + "2167 [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-... \n", + "2219 [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-... \n", + "2392 [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-... \n", + "2528 [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-... \n", + "2737 [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-... \n", + "2859 [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-... \n", + "3439 [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-... \n", + "3738 [ 0.01422119 -0.01114973 -0.04438976 -0.024421... \n", + "3805 [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-... \n", + "3915 [-7.23852217e-03 -4.69538383e-02 -5.60489520e-... \n", + "3917 [-8.92711710e-03 -4.49132621e-02 -4.29662578e-... \n", + "4281 [-1.69487391e-02 -1.89835522e-02 -3.80971469e-... \n", "4470 [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-... \n", - "4567 [-5.49167022e-03 -3.84587422e-02 -8.56091827e-... \n", - "4713 [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-... \n", - "5181 [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-... \n", - "5511 [-0.00217485 -0.04031368 -0.06604777 -0.052006... \n", - "5888 [-8.15972779e-03 -3.46563384e-02 -5.91776446e-... \n", - "6299 [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-... \n", - "7143 [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-... \n", - "7219 [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-... \n", - "7574 [-0.00149564 -0.06619431 -0.05084481 -0.048579... \n", - "8759 [ 0.01501553 -0.03575936 -0.050562 -0.034884... \n", - "9700 [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-... \n", - "9822 [ 2.95880195e-02 1.65440738e-02 -3.33247967e-... \n", + "4915 [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-... \n", + "4928 [-4.43694415e-03 -3.66776163e-04 -9.08496231e-... \n", + "5338 [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-... \n", + "5582 [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-... \n", + "6386 [ 3.33276950e-02 1.53224478e-02 -1.89354066e-... \n", + "6956 [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-... \n", "\n", - " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", - "357 {\"token_count\":306,\"truncated\":false} \n", - "428 {\"token_count\":134,\"truncated\":false} \n", - "1319 {\"token_count\":215,\"truncated\":false} \n", - "1993 {\"token_count\":536,\"truncated\":false} \n", - "1997 {\"token_count\":123,\"truncated\":false} \n", - "2469 {\"token_count\":60,\"truncated\":false} \n", - "2624 {\"token_count\":254,\"truncated\":false} \n", - "2832 {\"token_count\":79,\"truncated\":false} \n", - "3328 {\"token_count\":156,\"truncated\":false} \n", - "3650 {\"token_count\":175,\"truncated\":false} \n", - "3860 {\"token_count\":1267,\"truncated\":false} \n", - "4464 {\"token_count\":906,\"truncated\":false} \n", - "4470 {\"token_count\":200,\"truncated\":false} \n", - "4567 {\"token_count\":110,\"truncated\":false} \n", - "4713 {\"token_count\":549,\"truncated\":false} \n", - "5181 {\"token_count\":77,\"truncated\":false} \n", - "5511 {\"token_count\":262,\"truncated\":false} \n", - "5888 {\"token_count\":176,\"truncated\":false} \n", - "6299 {\"token_count\":151,\"truncated\":false} \n", - "7143 {\"token_count\":234,\"truncated\":false} \n", - "7219 {\"token_count\":26,\"truncated\":false} \n", - "7574 {\"token_count\":129,\"truncated\":false} \n", - "8759 {\"token_count\":501,\"truncated\":false} \n", - "9700 {\"token_count\":48,\"truncated\":false} \n", - "9822 {\"token_count\":2373,\"truncated\":true} \n", + " statistics ml_embed_text_status \\\n", + "782 {\"token_count\":121,\"truncated\":false} \n", + "795 {\"token_count\":141,\"truncated\":false} \n", + "861 {\"token_count\":160,\"truncated\":false} \n", + "1103 {\"token_count\":31,\"truncated\":false} \n", + "1241 {\"token_count\":23,\"truncated\":false} \n", + "1729 {\"token_count\":382,\"truncated\":false} \n", + "2167 {\"token_count\":556,\"truncated\":false} \n", + "2219 {\"token_count\":196,\"truncated\":false} \n", + "2392 {\"token_count\":641,\"truncated\":false} \n", + "2528 {\"token_count\":176,\"truncated\":false} \n", + "2737 {\"token_count\":230,\"truncated\":false} \n", + "2859 {\"token_count\":238,\"truncated\":false} \n", + "3439 {\"token_count\":197,\"truncated\":false} \n", + "3738 {\"token_count\":160,\"truncated\":false} \n", + "3805 {\"token_count\":477,\"truncated\":false} \n", + "3915 {\"token_count\":116,\"truncated\":false} \n", + "3917 {\"token_count\":71,\"truncated\":false} \n", + "4281 {\"token_count\":130,\"truncated\":false} \n", + "4470 {\"token_count\":200,\"truncated\":false} \n", + "4915 {\"token_count\":23,\"truncated\":false} \n", + "4928 {\"token_count\":83,\"truncated\":false} \n", + "5338 {\"token_count\":1279,\"truncated\":false} \n", + "5582 {\"token_count\":396,\"truncated\":false} \n", + "6386 {\"token_count\":79,\"truncated\":false} \n", + "6956 {\"token_count\":194,\"truncated\":false} \n", "\n", " content \n", - "357 I decided to try XXXX services for my wife and... \n", - "428 XXXX I went to the bank in question ( XXXX XXX... \n", - "1319 I currently have a home loan with my ex husban... \n", - "1993 NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800.... \n", - "1997 After a while the payments became harder and h... \n", - "2469 In the course of my student loan, I have been ... \n", - "2624 In accordance with the Fair Credit Reporting A... \n", - "2832 LVNV FUNDING LLC is continually placing a coll... \n", - "3328 On XX/XX/2020 I sent a letter regarding inaccu... \n", - "3650 Over a year and a half ago we started the proc... \n", - "3860 The issue is 26 late payments on me and my wif... \n", - "4464 I purchased as replacement for a lost XXXX XXX... \n", + "782 I 've sent multiple letters to this agency abo... \n", + "795 I receive social security XXXX funds in my XXX... \n", + "861 Hello, My name is XXXX XXXX XXXX. I have a pro... \n", + "1103 The debt occurred more than 7 years in the pas... \n", + "1241 UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA... \n", + "1729 XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort... \n", + "2167 This is the third such complaint I have submit... \n", + "2219 Found and add online for a Prepaid Credit card... \n", + "2392 I am furnishing this complaint against Fed Loa... \n", + "2528 Despite multiple written requests, the unverif... \n", + "2737 After unsatisfying communication in the messag... \n", + "2859 Good Morning. My name is XXXX XXXX. My account... \n", + "3439 I have ongoing disputes that are preventing me... \n", + "3738 I had a loan with national Collegiate Trust. i... \n", + "3805 Hi I am submitting this XXXX XXXX this isn't a... \n", + "3915 portfolio is showin on my credit report with a... \n", + "3917 the company shared my information with another... \n", + "4281 I tried to submit a teacher loan forgiveness a... \n", "4470 in accordance with the Fair Credit Reporting a... \n", - "4567 I have submitted multiple disputes through the... \n", - "4713 While shopping for furniture for my home I ope... \n", - "5181 I had opened a Wells Fargo checking account wi... \n", - "5511 I recently disputed ( see attached letter ) wi... \n", - "5888 XXXX XXXX XXXX XXXX \n", - "I have disputed this acco... \n", - "6299 XXXX ; XXXX and Transunion are reporting ( 30 ... \n", - "7143 My Macys account is due on the first of every ... \n", - "7219 Keep getting letters and calls from collection... \n", - "7574 On XXXX I was on the XXXX app and there was a ... \n", - "8759 Obviously I've been a victim of fraud, therefo... \n", - "9700 The following item have not been properly inve... \n", - "9822 During the housing market crash I went through... \n", + "4915 XXXX XXXX did not give me a receipt or a copy ... \n", + "4928 This company has filed a civil suit during a g... \n", + "5338 My credit report contains errors that is keepi... \n", + "5582 Coast Professional, XXXX, LA contacted me by m... \n", + "6386 Cares act refund requested in XXXX, called mul... \n", + "6956 n accordance with the Fair Credit Reporting ac... \n", "...\n", "\n", "[10000 rows x 4 columns]" @@ -812,7 +822,19 @@ { "data": { "text/html": [ - "Query job b4594edf-80e5-4476-ac06-b799001f4cb0 is DONE. 72.0 MB processed. Open Job" + "Query job 2c99b34a-1956-4de7-8330-898f1f25560b is DONE. 71.9 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 3ffed5f8-935a-4a3f-a560-6416445e4868 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -824,7 +846,7 @@ { "data": { "text/html": [ - "Query job 417e806a-2574-4b1b-8276-a95fa2df56e1 is DONE. 72.5 MB processed. Open Job" + "Query job 7b55783a-6d8f-41b9-b404-73253140029a is DONE. 72.3 MB processed. Open Job" ], "text/plain": [ "" @@ -854,188 +876,187 @@ " \n", " \n", " \n", - " ml_generate_embedding_result\n", - " ml_generate_embedding_statistics\n", - " ml_generate_embedding_status\n", + " text_embedding\n", + " statistics\n", + " ml_embed_text_status\n", " content\n", " \n", " \n", " \n", " \n", - " 357\n", - " [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-...\n", - " {\"token_count\":306,\"truncated\":false}\n", + " 782\n", + " [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-...\n", + " {\"token_count\":121,\"truncated\":false}\n", " \n", - " I decided to try XXXX services for my wife and...\n", + " I 've sent multiple letters to this agency abo...\n", " \n", " \n", - " 428\n", - " [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-...\n", - " {\"token_count\":134,\"truncated\":false}\n", + " 795\n", + " [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-...\n", + " {\"token_count\":141,\"truncated\":false}\n", " \n", - " XXXX I went to the bank in question ( XXXX XXX...\n", + " I receive social security XXXX funds in my XXX...\n", " \n", " \n", - " 1319\n", - " [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-...\n", - " {\"token_count\":215,\"truncated\":false}\n", + " 861\n", + " [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-...\n", + " {\"token_count\":160,\"truncated\":false}\n", " \n", - " I currently have a home loan with my ex husban...\n", + " Hello, My name is XXXX XXXX XXXX. I have a pro...\n", " \n", " \n", - " 1993\n", - " [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-...\n", - " {\"token_count\":536,\"truncated\":false}\n", + " 1103\n", + " [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-...\n", + " {\"token_count\":31,\"truncated\":false}\n", " \n", - " NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800....\n", + " The debt occurred more than 7 years in the pas...\n", " \n", " \n", - " 1997\n", - " [ 0.03145148 -0.01011822 -0.02316323 -0.025078...\n", - " {\"token_count\":123,\"truncated\":false}\n", + " 1241\n", + " [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-...\n", + " {\"token_count\":23,\"truncated\":false}\n", " \n", - " After a while the payments became harder and h...\n", + " UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA...\n", " \n", " \n", - " 2469\n", - " [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-...\n", - " {\"token_count\":60,\"truncated\":false}\n", + " 1729\n", + " [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-...\n", + " {\"token_count\":382,\"truncated\":false}\n", " \n", - " In the course of my student loan, I have been ...\n", + " XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort...\n", " \n", " \n", - " 2624\n", - " [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-...\n", - " {\"token_count\":254,\"truncated\":false}\n", + " 2167\n", + " [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-...\n", + " {\"token_count\":556,\"truncated\":false}\n", " \n", - " In accordance with the Fair Credit Reporting A...\n", + " This is the third such complaint I have submit...\n", " \n", " \n", - " 2832\n", - " [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-...\n", - " {\"token_count\":79,\"truncated\":false}\n", + " 2219\n", + " [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-...\n", + " {\"token_count\":196,\"truncated\":false}\n", " \n", - " LVNV FUNDING LLC is continually placing a coll...\n", + " Found and add online for a Prepaid Credit card...\n", " \n", " \n", - " 3328\n", - " [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-...\n", - " {\"token_count\":156,\"truncated\":false}\n", + " 2392\n", + " [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-...\n", + " {\"token_count\":641,\"truncated\":false}\n", " \n", - " On XX/XX/2020 I sent a letter regarding inaccu...\n", + " I am furnishing this complaint against Fed Loa...\n", " \n", " \n", - " 3650\n", - " [-6.10093866e-03 -5.93599863e-02 -8.04531425e-...\n", - " {\"token_count\":175,\"truncated\":false}\n", + " 2528\n", + " [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-...\n", + " {\"token_count\":176,\"truncated\":false}\n", " \n", - " Over a year and a half ago we started the proc...\n", + " Despite multiple written requests, the unverif...\n", " \n", " \n", - " 3860\n", - " [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-...\n", - " {\"token_count\":1267,\"truncated\":false}\n", + " 2737\n", + " [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-...\n", + " {\"token_count\":230,\"truncated\":false}\n", " \n", - " The issue is 26 late payments on me and my wif...\n", + " After unsatisfying communication in the messag...\n", " \n", " \n", - " 4464\n", - " [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-...\n", - " {\"token_count\":906,\"truncated\":false}\n", + " 2859\n", + " [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-...\n", + " {\"token_count\":238,\"truncated\":false}\n", " \n", - " I purchased as replacement for a lost XXXX XXX...\n", + " Good Morning. My name is XXXX XXXX. My account...\n", " \n", " \n", - " 4470\n", - " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", - " {\"token_count\":200,\"truncated\":false}\n", + " 3439\n", + " [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-...\n", + " {\"token_count\":197,\"truncated\":false}\n", " \n", - " in accordance with the Fair Credit Reporting a...\n", + " I have ongoing disputes that are preventing me...\n", " \n", " \n", - " 4567\n", - " [-5.49167022e-03 -3.84587422e-02 -8.56091827e-...\n", - " {\"token_count\":110,\"truncated\":false}\n", + " 3738\n", + " [ 0.01422119 -0.01114973 -0.04438976 -0.024421...\n", + " {\"token_count\":160,\"truncated\":false}\n", " \n", - " I have submitted multiple disputes through the...\n", + " I had a loan with national Collegiate Trust. i...\n", " \n", " \n", - " 4713\n", - " [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-...\n", - " {\"token_count\":549,\"truncated\":false}\n", + " 3805\n", + " [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-...\n", + " {\"token_count\":477,\"truncated\":false}\n", " \n", - " While shopping for furniture for my home I ope...\n", + " Hi I am submitting this XXXX XXXX this isn't a...\n", " \n", " \n", - " 5181\n", - " [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-...\n", - " {\"token_count\":77,\"truncated\":false}\n", + " 3915\n", + " [-7.23852217e-03 -4.69538383e-02 -5.60489520e-...\n", + " {\"token_count\":116,\"truncated\":false}\n", " \n", - " I had opened a Wells Fargo checking account wi...\n", + " portfolio is showin on my credit report with a...\n", " \n", " \n", - " 5511\n", - " [-0.00217485 -0.04031368 -0.06604777 -0.052006...\n", - " {\"token_count\":262,\"truncated\":false}\n", + " 3917\n", + " [-8.92711710e-03 -4.49132621e-02 -4.29662578e-...\n", + " {\"token_count\":71,\"truncated\":false}\n", " \n", - " I recently disputed ( see attached letter ) wi...\n", + " the company shared my information with another...\n", " \n", " \n", - " 5888\n", - " [-8.15972779e-03 -3.46563384e-02 -5.91776446e-...\n", - " {\"token_count\":176,\"truncated\":false}\n", + " 4281\n", + " [-1.69487391e-02 -1.89835522e-02 -3.80971469e-...\n", + " {\"token_count\":130,\"truncated\":false}\n", " \n", - " XXXX XXXX XXXX XXXX \n", - "I have disputed this acco...\n", + " I tried to submit a teacher loan forgiveness a...\n", " \n", " \n", - " 6299\n", - " [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-...\n", - " {\"token_count\":151,\"truncated\":false}\n", + " 4470\n", + " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", + " {\"token_count\":200,\"truncated\":false}\n", " \n", - " XXXX ; XXXX and Transunion are reporting ( 30 ...\n", + " in accordance with the Fair Credit Reporting a...\n", " \n", " \n", - " 7143\n", - " [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-...\n", - " {\"token_count\":234,\"truncated\":false}\n", + " 4915\n", + " [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-...\n", + " {\"token_count\":23,\"truncated\":false}\n", " \n", - " My Macys account is due on the first of every ...\n", + " XXXX XXXX did not give me a receipt or a copy ...\n", " \n", " \n", - " 7219\n", - " [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-...\n", - " {\"token_count\":26,\"truncated\":false}\n", + " 4928\n", + " [-4.43694415e-03 -3.66776163e-04 -9.08496231e-...\n", + " {\"token_count\":83,\"truncated\":false}\n", " \n", - " Keep getting letters and calls from collection...\n", + " This company has filed a civil suit during a g...\n", " \n", " \n", - " 7574\n", - " [-0.00149564 -0.06619431 -0.05084481 -0.048579...\n", - " {\"token_count\":129,\"truncated\":false}\n", + " 5338\n", + " [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-...\n", + " {\"token_count\":1279,\"truncated\":false}\n", " \n", - " On XXXX I was on the XXXX app and there was a ...\n", + " My credit report contains errors that is keepi...\n", " \n", " \n", - " 8759\n", - " [ 0.01501553 -0.03575936 -0.050562 -0.034884...\n", - " {\"token_count\":501,\"truncated\":false}\n", + " 5582\n", + " [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-...\n", + " {\"token_count\":396,\"truncated\":false}\n", " \n", - " Obviously I've been a victim of fraud, therefo...\n", + " Coast Professional, XXXX, LA contacted me by m...\n", " \n", " \n", - " 9700\n", - " [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-...\n", - " {\"token_count\":48,\"truncated\":false}\n", + " 6386\n", + " [ 3.33276950e-02 1.53224478e-02 -1.89354066e-...\n", + " {\"token_count\":79,\"truncated\":false}\n", " \n", - " The following item have not been properly inve...\n", + " Cares act refund requested in XXXX, called mul...\n", " \n", " \n", - " 9822\n", - " [ 2.95880195e-02 1.65440738e-02 -3.33247967e-...\n", - " {\"token_count\":2373,\"truncated\":true}\n", + " 6956\n", + " [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-...\n", + " {\"token_count\":194,\"truncated\":false}\n", " \n", - " During the housing market crash I went through...\n", + " n accordance with the Fair Credit Reporting ac...\n", " \n", " \n", "\n", @@ -1043,87 +1064,86 @@ "[10000 rows x 4 columns in total]" ], "text/plain": [ - " ml_generate_embedding_result \\\n", - "357 [ 1.33585772e-02 -3.76597494e-02 -6.14452176e-... \n", - "428 [ 3.10036819e-04 -3.82593311e-02 -3.41922641e-... \n", - "1319 [ 1.97481886e-02 -1.97448786e-02 -5.13443351e-... \n", - "1993 [ 9.83821880e-03 -6.55664057e-02 -5.46210706e-... \n", - "1997 [ 0.03145148 -0.01011822 -0.02316323 -0.025078... \n", - "2469 [ 4.74590808e-03 -4.56819348e-02 -2.49751769e-... \n", - "2624 [ 3.91883589e-03 -3.26644145e-02 -7.10378587e-... \n", - "2832 [ 8.35181400e-03 -2.91643552e-02 -4.30776961e-... \n", - "3328 [ 2.71253809e-02 -1.77491009e-02 -5.32273464e-... \n", - "3650 [-6.10093866e-03 -5.93599863e-02 -8.04531425e-... \n", - "3860 [ 5.84836192e-02 -2.43354496e-03 -5.57337068e-... \n", - "4464 [ 6.05084226e-02 -3.21578234e-02 -7.51668587e-... \n", + " text_embedding \\\n", + "782 [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-... \n", + "795 [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-... \n", + "861 [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-... \n", + "1103 [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-... \n", + "1241 [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-... \n", + "1729 [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-... \n", + "2167 [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-... \n", + "2219 [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-... \n", + "2392 [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-... \n", + "2528 [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-... \n", + "2737 [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-... \n", + "2859 [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-... \n", + "3439 [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-... \n", + "3738 [ 0.01422119 -0.01114973 -0.04438976 -0.024421... \n", + "3805 [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-... \n", + "3915 [-7.23852217e-03 -4.69538383e-02 -5.60489520e-... \n", + "3917 [-8.92711710e-03 -4.49132621e-02 -4.29662578e-... \n", + "4281 [-1.69487391e-02 -1.89835522e-02 -3.80971469e-... \n", "4470 [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-... \n", - "4567 [-5.49167022e-03 -3.84587422e-02 -8.56091827e-... \n", - "4713 [ 2.68485844e-02 -3.46762352e-02 -4.59849052e-... \n", - "5181 [ 2.05754172e-02 -3.83999050e-02 -9.29225236e-... \n", - "5511 [-0.00217485 -0.04031368 -0.06604777 -0.052006... \n", - "5888 [-8.15972779e-03 -3.46563384e-02 -5.91776446e-... \n", - "6299 [ 4.80043218e-02 -4.13420722e-02 -6.12363108e-... \n", - "7143 [ 4.39200476e-02 -3.04005221e-02 -3.47866341e-... \n", - "7219 [ 1.00224940e-02 -3.79302073e-03 -3.41785327e-... \n", - "7574 [-0.00149564 -0.06619431 -0.05084481 -0.048579... \n", - "8759 [ 0.01501553 -0.03575936 -0.050562 -0.034884... \n", - "9700 [ 1.01501048e-02 -2.80565154e-02 -4.05892394e-... \n", - "9822 [ 2.95880195e-02 1.65440738e-02 -3.33247967e-... \n", + "4915 [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-... \n", + "4928 [-4.43694415e-03 -3.66776163e-04 -9.08496231e-... \n", + "5338 [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-... \n", + "5582 [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-... \n", + "6386 [ 3.33276950e-02 1.53224478e-02 -1.89354066e-... \n", + "6956 [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-... \n", "\n", - " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", - "357 {\"token_count\":306,\"truncated\":false} \n", - "428 {\"token_count\":134,\"truncated\":false} \n", - "1319 {\"token_count\":215,\"truncated\":false} \n", - "1993 {\"token_count\":536,\"truncated\":false} \n", - "1997 {\"token_count\":123,\"truncated\":false} \n", - "2469 {\"token_count\":60,\"truncated\":false} \n", - "2624 {\"token_count\":254,\"truncated\":false} \n", - "2832 {\"token_count\":79,\"truncated\":false} \n", - "3328 {\"token_count\":156,\"truncated\":false} \n", - "3650 {\"token_count\":175,\"truncated\":false} \n", - "3860 {\"token_count\":1267,\"truncated\":false} \n", - "4464 {\"token_count\":906,\"truncated\":false} \n", - "4470 {\"token_count\":200,\"truncated\":false} \n", - "4567 {\"token_count\":110,\"truncated\":false} \n", - "4713 {\"token_count\":549,\"truncated\":false} \n", - "5181 {\"token_count\":77,\"truncated\":false} \n", - "5511 {\"token_count\":262,\"truncated\":false} \n", - "5888 {\"token_count\":176,\"truncated\":false} \n", - "6299 {\"token_count\":151,\"truncated\":false} \n", - "7143 {\"token_count\":234,\"truncated\":false} \n", - "7219 {\"token_count\":26,\"truncated\":false} \n", - "7574 {\"token_count\":129,\"truncated\":false} \n", - "8759 {\"token_count\":501,\"truncated\":false} \n", - "9700 {\"token_count\":48,\"truncated\":false} \n", - "9822 {\"token_count\":2373,\"truncated\":true} \n", + " statistics ml_embed_text_status \\\n", + "782 {\"token_count\":121,\"truncated\":false} \n", + "795 {\"token_count\":141,\"truncated\":false} \n", + "861 {\"token_count\":160,\"truncated\":false} \n", + "1103 {\"token_count\":31,\"truncated\":false} \n", + "1241 {\"token_count\":23,\"truncated\":false} \n", + "1729 {\"token_count\":382,\"truncated\":false} \n", + "2167 {\"token_count\":556,\"truncated\":false} \n", + "2219 {\"token_count\":196,\"truncated\":false} \n", + "2392 {\"token_count\":641,\"truncated\":false} \n", + "2528 {\"token_count\":176,\"truncated\":false} \n", + "2737 {\"token_count\":230,\"truncated\":false} \n", + "2859 {\"token_count\":238,\"truncated\":false} \n", + "3439 {\"token_count\":197,\"truncated\":false} \n", + "3738 {\"token_count\":160,\"truncated\":false} \n", + "3805 {\"token_count\":477,\"truncated\":false} \n", + "3915 {\"token_count\":116,\"truncated\":false} \n", + "3917 {\"token_count\":71,\"truncated\":false} \n", + "4281 {\"token_count\":130,\"truncated\":false} \n", + "4470 {\"token_count\":200,\"truncated\":false} \n", + "4915 {\"token_count\":23,\"truncated\":false} \n", + "4928 {\"token_count\":83,\"truncated\":false} \n", + "5338 {\"token_count\":1279,\"truncated\":false} \n", + "5582 {\"token_count\":396,\"truncated\":false} \n", + "6386 {\"token_count\":79,\"truncated\":false} \n", + "6956 {\"token_count\":194,\"truncated\":false} \n", "\n", " content \n", - "357 I decided to try XXXX services for my wife and... \n", - "428 XXXX I went to the bank in question ( XXXX XXX... \n", - "1319 I currently have a home loan with my ex husban... \n", - "1993 NOT MY ACCOUNT, NOT AN AUTHORIZED USER {$1800.... \n", - "1997 After a while the payments became harder and h... \n", - "2469 In the course of my student loan, I have been ... \n", - "2624 In accordance with the Fair Credit Reporting A... \n", - "2832 LVNV FUNDING LLC is continually placing a coll... \n", - "3328 On XX/XX/2020 I sent a letter regarding inaccu... \n", - "3650 Over a year and a half ago we started the proc... \n", - "3860 The issue is 26 late payments on me and my wif... \n", - "4464 I purchased as replacement for a lost XXXX XXX... \n", + "782 I 've sent multiple letters to this agency abo... \n", + "795 I receive social security XXXX funds in my XXX... \n", + "861 Hello, My name is XXXX XXXX XXXX. I have a pro... \n", + "1103 The debt occurred more than 7 years in the pas... \n", + "1241 UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA... \n", + "1729 XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort... \n", + "2167 This is the third such complaint I have submit... \n", + "2219 Found and add online for a Prepaid Credit card... \n", + "2392 I am furnishing this complaint against Fed Loa... \n", + "2528 Despite multiple written requests, the unverif... \n", + "2737 After unsatisfying communication in the messag... \n", + "2859 Good Morning. My name is XXXX XXXX. My account... \n", + "3439 I have ongoing disputes that are preventing me... \n", + "3738 I had a loan with national Collegiate Trust. i... \n", + "3805 Hi I am submitting this XXXX XXXX this isn't a... \n", + "3915 portfolio is showin on my credit report with a... \n", + "3917 the company shared my information with another... \n", + "4281 I tried to submit a teacher loan forgiveness a... \n", "4470 in accordance with the Fair Credit Reporting a... \n", - "4567 I have submitted multiple disputes through the... \n", - "4713 While shopping for furniture for my home I ope... \n", - "5181 I had opened a Wells Fargo checking account wi... \n", - "5511 I recently disputed ( see attached letter ) wi... \n", - "5888 XXXX XXXX XXXX XXXX \n", - "I have disputed this acco... \n", - "6299 XXXX ; XXXX and Transunion are reporting ( 30 ... \n", - "7143 My Macys account is due on the first of every ... \n", - "7219 Keep getting letters and calls from collection... \n", - "7574 On XXXX I was on the XXXX app and there was a ... \n", - "8759 Obviously I've been a victim of fraud, therefo... \n", - "9700 The following item have not been properly inve... \n", - "9822 During the housing market crash I went through... \n", + "4915 XXXX XXXX did not give me a receipt or a copy ... \n", + "4928 This company has filed a civil suit during a g... \n", + "5338 My credit report contains errors that is keepi... \n", + "5582 Coast Professional, XXXX, LA contacted me by m... \n", + "6386 Cares act refund requested in XXXX, called mul... \n", + "6956 n accordance with the Fair Credit Reporting ac... \n", "...\n", "\n", "[10000 rows x 4 columns]" @@ -1136,10 +1156,10 @@ ], "source": [ "successful_rows = (\n", - " (predicted_embeddings[\"ml_generate_embedding_status\"] == \"\")\n", + " (predicted_embeddings[\"ml_embed_text_status\"] == \"\")\n", " # Series.str.len() gives the length of an array.\n", " # See: https://stackoverflow.com/a/41340543/101923\n", - " & (predicted_embeddings[\"ml_generate_embedding_result\"].str.len() != 0)\n", + " & (predicted_embeddings[\"text_embedding\"].str.len() != 0)\n", ")\n", "predicted_embeddings = predicted_embeddings[successful_rows]\n", "predicted_embeddings\n" @@ -1194,7 +1214,7 @@ { "data": { "text/html": [ - "Query job 18aa46ee-0b10-4912-ae14-87b7e81ee447 is DONE. 61.7 MB processed. Open Job" + "Query job 46da96c8-c454-44d3-8b98-0e1bfeca69dd is DONE. 61.7 MB processed. Open Job" ], "text/plain": [ "" @@ -1206,7 +1226,7 @@ { "data": { "text/html": [ - "Query job fd573f97-2424-472a-969d-463f184967d9 is DONE. 0 Bytes processed. Open Job" + "Query job dc6fe7cf-329d-4274-aff9-0b8dc2e56230 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1218,7 +1238,7 @@ { "data": { "text/html": [ - "Query job 9f2e0a3f-d7d6-4fb8-b558-95f39235410d is DONE. 72.7 MB processed. Open Job" + "Query job 8c25a14a-af39-40a9-add5-de0f14bce9ce is DONE. 72.4 MB processed. Open Job" ], "text/plain": [ "" @@ -1230,7 +1250,7 @@ { "data": { "text/html": [ - "Query job 786ababe-7c40-426f-bb39-154329e4c51a is DONE. 80.0 kB processed. Open Job" + "Query job 0a6a45b2-7c35-4be8-91a3-391a5381553e is DONE. 80.0 kB processed. Open Job" ], "text/plain": [ "" @@ -1242,7 +1262,7 @@ { "data": { "text/html": [ - "Query job a191fc97-baa6-4c7c-b78f-4365678caa60 is DONE. 73.2 MB processed. Open Job" + "Query job b5e00edd-de21-40c1-bf61-9f1affdea318 is DONE. 73.1 MB processed. Open Job" ], "text/plain": [ "" @@ -1274,57 +1294,57 @@ " \n", " CENTROID_ID\n", " NEAREST_CENTROIDS_DISTANCE\n", - " ml_generate_embedding_result\n", - " ml_generate_embedding_statistics\n", - " ml_generate_embedding_status\n", + " text_embedding\n", + " statistics\n", + " ml_embed_text_status\n", " content\n", " \n", " \n", " \n", " \n", - " 1244571\n", + " 1094645\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.414497263076...\n", - " [ 1.10590272e-02 -2.11433582e-02 -5.66212423e-...\n", - " {\"token_count\":100,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.530282685572...\n", + " [ 7.32792774e-03 -7.59598315e-02 -4.49591577e-...\n", + " {\"token_count\":10,\"truncated\":false}\n", " \n", - " Ive disputed two Bankruptcies that still exist...\n", + " I do not have an account with this creditor\n", " \n", " \n", - " 744390\n", + " 3372485\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.416584344032...\n", - " [ 4.15011719e-02 -4.50705849e-02 -7.35541508e-...\n", - " {\"token_count\":100,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.643931578310...\n", + " [-0.00161087 -0.04956109 -0.07371692 -0.057822...\n", + " {\"token_count\":10,\"truncated\":false}\n", " \n", - " The XXXX account was settled as a class action...\n", + " Hard inquiries in my report that I do not reco...\n", " \n", " \n", - " 127514\n", + " 2669308\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.541137734253...\n", - " [ 3.54415141e-02 1.23769706e-02 -2.61783414e-...\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.599709344244...\n", + " [ 5.50241247e-02 -1.50039541e-02 -2.08624080e-...\n", " {\"token_count\":100,\"truncated\":false}\n", " \n", - " I have late payments reported on my student lo...\n", + " I purchase {$25.00} for stock on the cash app ...\n", " \n", " \n", - " 630563\n", + " 133816\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.477175150810...\n", - " [ 2.34235693e-02 -4.21241224e-02 -3.90484147e-...\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.618444281124...\n", + " [ 1.77251529e-02 -3.89547497e-02 -3.82236368e-...\n", " {\"token_count\":100,\"truncated\":false}\n", " \n", - " A Military Star Credit card, aka Take it Home ...\n", + " BBVA fees I am in The Texas snow storm where I...\n", " \n", " \n", - " 2651231\n", + " 2697156\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.489760037964...\n", - " [ 2.64898203e-02 -5.62610961e-02 -5.82714193e-...\n", - " {\"token_count\":101,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.500398902102...\n", + " [-1.28429877e-02 -1.85956229e-02 -3.93197313e-...\n", + " {\"token_count\":1011,\"truncated\":false}\n", " \n", - " My mortgage is with Bank of America. I filed C...\n", + " After paying on my student loan for years, I o...\n", " \n", " \n", "\n", @@ -1332,32 +1352,32 @@ ], "text/plain": [ " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", - "1244571 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.414497263076... \n", - "744390 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.416584344032... \n", - "127514 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.541137734253... \n", - "630563 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.477175150810... \n", - "2651231 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.489760037964... \n", + "1094645 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.530282685572... \n", + "3372485 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.643931578310... \n", + "2669308 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.599709344244... \n", + "133816 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.618444281124... \n", + "2697156 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.500398902102... \n", "\n", - " ml_generate_embedding_result \\\n", - "1244571 [ 1.10590272e-02 -2.11433582e-02 -5.66212423e-... \n", - "744390 [ 4.15011719e-02 -4.50705849e-02 -7.35541508e-... \n", - "127514 [ 3.54415141e-02 1.23769706e-02 -2.61783414e-... \n", - "630563 [ 2.34235693e-02 -4.21241224e-02 -3.90484147e-... \n", - "2651231 [ 2.64898203e-02 -5.62610961e-02 -5.82714193e-... \n", + " text_embedding \\\n", + "1094645 [ 7.32792774e-03 -7.59598315e-02 -4.49591577e-... \n", + "3372485 [-0.00161087 -0.04956109 -0.07371692 -0.057822... \n", + "2669308 [ 5.50241247e-02 -1.50039541e-02 -2.08624080e-... \n", + "133816 [ 1.77251529e-02 -3.89547497e-02 -3.82236368e-... \n", + "2697156 [-1.28429877e-02 -1.85956229e-02 -3.93197313e-... \n", "\n", - " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", - "1244571 {\"token_count\":100,\"truncated\":false} \n", - "744390 {\"token_count\":100,\"truncated\":false} \n", - "127514 {\"token_count\":100,\"truncated\":false} \n", - "630563 {\"token_count\":100,\"truncated\":false} \n", - "2651231 {\"token_count\":101,\"truncated\":false} \n", + " statistics ml_embed_text_status \\\n", + "1094645 {\"token_count\":10,\"truncated\":false} \n", + "3372485 {\"token_count\":10,\"truncated\":false} \n", + "2669308 {\"token_count\":100,\"truncated\":false} \n", + "133816 {\"token_count\":100,\"truncated\":false} \n", + "2697156 {\"token_count\":1011,\"truncated\":false} \n", "\n", " content \n", - "1244571 Ive disputed two Bankruptcies that still exist... \n", - "744390 The XXXX account was settled as a class action... \n", - "127514 I have late payments reported on my student lo... \n", - "630563 A Military Star Credit card, aka Take it Home ... \n", - "2651231 My mortgage is with Bank of America. I filed C... " + "1094645 I do not have an account with this creditor \n", + "3372485 Hard inquiries in my report that I do not reco... \n", + "2669308 I purchase {$25.00} for stock on the cash app ... \n", + "133816 BBVA fees I am in The Texas snow storm where I... \n", + "2697156 After paying on my student loan for years, I o... " ] }, "execution_count": 13, @@ -1367,7 +1387,7 @@ ], "source": [ "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", - "cluster_model.fit(predicted_embeddings[[\"ml_generate_embedding_result\"]])\n", + "cluster_model.fit(predicted_embeddings[[\"text_embedding\"]])\n", "clustered_result = cluster_model.predict(predicted_embeddings)\n", "# Notice the CENTROID_ID column, which is the ID number of the group that\n", "# each complaint belongs to.\n", @@ -1410,7 +1430,7 @@ { "data": { "text/html": [ - "Query job 8bfc647f-b9e5-40a2-816c-d12e8f81bea3 is DONE. 10.6 MB processed. Open Job" + "Query job 8d4f24d6-dc37-47d3-8b4d-4505a55c4ccc is DONE. 10.4 MB processed. Open Job" ], "text/plain": [ "" @@ -1422,7 +1442,7 @@ { "data": { "text/html": [ - "Query job 6f834214-9cc3-4577-bb2d-980ba05df817 is DONE. 10.6 MB processed. Open Job" + "Query job c1f979ee-1f5d-4f37-8595-ee2167c06e63 is DONE. 10.4 MB processed. Open Job" ], "text/plain": [ "" @@ -1458,62 +1478,32 @@ "output_type": "stream", "text": [ "comment list 1:\n", - "1. I currently have a home loan with my ex husband with PHH Mortgages. We filed for divorce and in the divorce decree he became liable for the home and paying the payments. He ended up missing XXXX payments which effected my credit fairly substaintailly. when I became aware of the late payments, I ensured that the account was up to date and have since. I presented to them that I have the legal documents that he is obligated to make the payments each month and that I am not responisble for the payment. I asked them to remove the XXXX dings on my credit and they would not. I offered to present the paperwork and they still would not. The home is now being sold. I even filed with XXXX as a discrepency and they would not remove it. I would have never let these become a late payment. I was not even notified as they had all of his information in the file.\n", - "2. In the course of my student loan, I have been making payments and I feel that the payments haven't been added to the debt, the company stated that I am delinquent over 180 and my payments are auto pay. This has had a negative impact on my credit score.\n", - "3. The issue is 26 late payments on me and my wife 's credit reports due to a system error on a joint mortgage account that was always paid on time using autopay. ( will attach docs to support this ). \n", - "\n", - "This is an ongoing nightmare me and my wife are going through over the past 3 years. \n", - "Sent many dispute letters to the creditor and to the 3 bureaus, was promised multiple times that all late payments will be removed, we also has a letter from the bank stating we were never late on this account, also have a recording of a phone call with bank 's permission were the representative admits there was a system error and promised again that all late payments will be deleted from both of our credit reports. \n", - "As of today, for an unknown reason XXXX reports 6x30 days late payments, XXXX reports 24 lates, and Transunion 23 lates. \n", - "\n", - "We have always paid our mortgage on time for many years, enrolled in autopay and making 2 payments per month. Our mortgage is currently with XXXX XXXX, XXXX XXXX is the mortgage servicer who's collecting from us and disbursing payments to XXXX XXXX. \n", - "\n", - "I will attach here our mortgage transaction history confirming payments have been made on time, letter from the mortgage servicer XXXX XXXX XXXX XXXX confirming we were never late, copy of a page from my credit report showing all the late payments, and a few bank statements showing payment made on time while showing as late on our credit reports. ( XXXX & XXXX XXXX ) PLEASE HELP us to resolve this issue and have all late payments on this account removed from XXXX & XXXX XXXX. \n", - "\n", - "Her is a small portion of our previous communication with XXXX and their response : XX/XX/XXXX : we spoke with XXXX, XXXX stated that the funds were misappropriated and went to the wrong account. Said he'll contact their Tax Dep ' and get back to me and never did. \n", + "1. This is the third such complaint I have submitted regarding the same type of issue over the past 12-18 months. \n", "\n", - "XX/XX/XXXX : I spoke in length with XXXX from XXXX XXXX & XXXX from XXXX at XXXX who opened an investigation. Their supervisor said he made all the necessary changes. The next month, the money was withdrawn from my account on time and i received again a late fee and 30 days late on my credit report. \n", - "\n", - "XX/XX/XXXX Spoke to XXXX who sent me to XXXX who sent me to XXXX XXXX from Escalation department, she promised the issue was fixed and late payments will be removed in up to 30 days and she will email me a deletion letter. Nothing was sent! and i called and wrote 5 emails to her and never got a response. \n", - "\n", - "XX/XX/XXXX spoke with XXXX to follow up with XXXX XXXX, no response. \n", - "\n", - "XX/XX/XXXX spoke with XXXX at XXXX, she said that the transaction history is our proof that issue was corrected and she'll submit a request to delete the late payments as the system does not show that previous request was made. \n", - "\n", - "XX/XX/XXXX XXXX sent a letter stating that we had a shortage of {$5300.00}. Again, upon checking my bank account all monthly payments were made on time. To avoid further issues i sent a check for {$5300.00} on XX/XX/XXXX. \n", - "\n", - "XX/XX/XXXX following many joint calls with XXXX XXXX i received a letter from XXXX stating that my credit might have been affected due to processing error and that they sent XXXX XXXX a letter requesting a removal of all late payments. \n", - "\n", - "XX/XX/XXXX spoke with XXXX XXXX again.. \n", - "XX/XX/XXXX spoke with XXXX at XXXX I have many more... \n", - "\n", - "*** XX/XX/XXXX : SPOKE WITH XXXX XXXX ( resolution team ) at XXXX. She said they will delete the late payments from both reports ( XXXX & XXXX XXXX and will call me to follow up on XX/XX/XXXX. She also emailed me the payment activity on the account. XXXX I received the payment history but NO CALL OR RESOLUTION. \n", - "\n", - "XX/XX/XXXX : SPOKE WITH XXXX FROM XXXX AND XXXX FROM XXXX XXXX ON A XXXX WAY CALL at XXXX, XXXX PERMISSION TO RECORD THE CALL, SHE AGREED, we went over all the late payments, she said she sees the error and promised that this time it will be resolved and get deleted from our credit reports. Again, nothing was resolved and we never heard back from anyone.\n", - "4. XXXX ; XXXX and Transunion are reporting ( 30 ) plus days late on the XXXX XXXX partial account number XXXX. ( Please see page 3 of the attached credit report. ) This account was paid in XXXX, 2019 and the lates are reporting in XXXX, 2019. Please keep in mind that it is impossible to have late payments on an account that was paid off a month prior. This incorrect reporting is harming my credit score and this line item need to be removed from my credit report. I have contacted the ( 3 ) bureaus to fix this, however I have been unsuccessful.\n", - "5. My Macys account is due on the first of every month. Since I have had the card I have paid on the XXXX PRIOR to the due date. And have paid over the amount due. In XXXX my XXXX XXXX auto pay did not come out of my account and rather than calling me - on the XXXX of XXXX just 5 days late they cut my credit off and shut me out of my account so I can not even see my credit profile - I have made the payment and they still are locking me out - please look into this - you will see that is what happened and they are stating in a letter it is becasue my XXXX report shows a seriuos derogorty item which it does not and I have submitted a complaint with them as well. Macys has been the worst credit experience of my LIFE and I did read the reviews but thought it would be different for me I guess? \n", - "thank you for your help.\n", + "On XX/XX/XXXX, my co-signed account was flagged by Navient as past due. The XXXX payment was mailed priority on XX/XX/XXXX and received by Navient on XX/XX/XXXX and delivered to \" an individual '' per the post office. \n", + "I called Navient on XX/XX/XXXX to talk to them about why my account was flagged since they received the payment long before the due date. The payment is sent via XXXX money orders under the same cover. The XXXX money order ( {$160.00} ) was cashed on XX/XX/XXXX per XXXX XXXX, the second money order ( {$250.00} ) which was sent in the same priority envelope and received the same time has not been cashed. \n", + "When I called the customer service agent at Navient she told me that my account was past due and wanted me to send another payment. When I explained that they had received the payment she argued with me that if they received it, the payment would have been cashed. I asked to speak with a supervisor. \n", + "I was connected with supervisor, XXXX XXXX, who asked that I send copies of the payments to him so he could submit for a missing payment request. I faxed the proof on XXXX @ XXXX with a receipt acknowledgment. \n", + "On XX/XX/XXXX, the payment was still not applied to the account. When I called XXXX XXXX, the money order was still not cashed. I called Navient again. Because of an argumentative customer service rep again, I requested to speak with a supervisor. I spoke with XXXX XXXX. She states that payment was not received. I explained the situation again. She said the missing payment request had not been submitted. She had me upload the documents so she could request a missing payment search. I have done everything I have been asked. \n", + "This issues continues to occur. For approximately 6 months at a time, Navient gets this right then there are issues again and again. I have submitted CFPB complaints about this in the past. \n", + "I was told it would take 5-7 business days to be resolved.\n", + "2. I tried to submit a teacher loan forgiveness application and they lost my application. I submitted the application again after talking to XXXX people at Nelnet. Then when I called back to check on the status they told me that I needed to submit another form for a different school that I worked at. I had already called previously and asked if I needed to submit any other papers and they told me \" no ''. Therefore, I have been paying my loan for 5 months and it should be forgiven. I am still paying my loan because I have to wait for them to approve the new forgiveness paperwork.\n", + "3. PayPal continues to overcharge their currency rate. It it always inflated in their favor so that they collect the difference.\n", + "4. My government feeds are not coming on to my card and I need the problem fix today\n", + "5. Paypal Credit 's website is confusing and does not accurately reflect all activity. When speaking with representatives like XXXX, it 's confusing to them and they can barely follow along with it. I am not receiving statements, which proves it difficult to determine the due dates on the accounts. The Reps are n't knowledgeable and the only thing they repeat to you is the amount due on the screen.\n", "\n", "comment list 2:\n", - "1. I decided to try XXXX services for my wife and I so I purchased phones for both of us. After a day or two of trial we felt unsatisfied so we headed back to the store and we returned all items. We got charged with restocking fees and taxes. Later on I got a bill in the mail in which I was being charged {$1200.00} for the returned items. After several attempts of arguing for about XXXX months about whether I owed XXXX or not I was dismissed of such charges, but a month after I was charged by a collecting company called ERC for {$61.00}. I asked them to explain such charges weather if they were fees or taxes and they we unable to disclose information. Therefore, I asked them to send me a bill in the mail with details about the charges, as well as a dispute package and they told me they would send me a bill. About the dispute part, they said that I needed to call XXXX to discuss the charges with them but XXXX said that I had to discuss this with the collecting company. I never received a detailed statement neither a chanse to defend my self about such charges, I checked my credit score and found a red flag in it because of this. \n", - "\n", - "I am now hoping you may help me with this case. \n", - "\n", - "Thanks :\n", - "2. Over a year and a half ago we started the process of buying a home. Our mortgage guy sent us to a credit repair co. They got the collection account from Weltman , Weinberg & Reis taken off my credit, because it was unverifiable. Now it is back on my credit. I have credit reports showing the trade line on and then off and now today it is currently on my report. When I called to verify the account with WW & R they sent me a heavily redacted letter verifying absolutely nothing. I would like this unverifiable account taken off my credit and removed permanently. This should not be a loan I have to pay for if there is no verification that it is my debt. Attached are the credit reports and the letter of verification that was sent to me.\n", - "3. I recently disputed ( see attached letter ) with Receivable Management Services an account entry that they placed on my credit report without providing a dunning letter or any correspondence that would have allowed me 30 days to dispute the validity of the alleged debt. To date, I have not received any communication from them. They are blatantly violating my rights by reporting this inaccurate, erroneous, unverifiable entry.\n", + "1. XXXX on XXXX XX/XX/2021 I have Mr. Cooper mortgage for years now. On XXXX XXXX XXXX I made an additional payment of $ XXXX towards my principal. More than 4 days - it's not reflected in the Amortization schedule ( Amortization schedule is not even visible ). Even after so many additional principal payments, Payoff calculator is way off and it still shows XXXX maturity date while it should start showing something like XX/XX/XXXX/XX/XX/XXXX as the initial date. There are lots of discrepancies on their website to reflect the balance and total. When called customer service on Friday and also chatted - i was assured of fixing this - but no fix till this point of time. Customer service there is a long wait. Auto bot doesn't let customers talk to the real person. \n", + "Finally after a lots of follow-up I got the amortization schedule via email but it is not reflecting another additional principal payment of {$4700.00} made on XX/XX/2021. \n", + "I did numerous chats and phone calls. Why i should depend on inefficinent humans to see my revised amortization schedule? \n", + "Why the online amortization schedule is not visible now? \n", "\n", - "Additionally, this account entry does not reflect a payment history which should be included on any entry that is reflected on my credit report. In my previous communication to them, I specifically requested that they provide an agreement that states their authority to collect on the alleged debt, agreement with signature of the alleged debtor wherein he/she agreed to pay the creditor, alleged account number, date this alleged debt became payable, original delinquency date, and to date to no avail. \n", - "\n", - "As such, since they have refused to respond to my request and not provide any documentation to substantiate their allegations, coupled with the fact that they did not provide me a dunning letter is grounds for this erroneous, inaccurate, unverifiable entry to be deleted from my credit report.\n", - "4. I accepted service from XXXX XXXX XXXX. The company did not inform me that internet was required. They also told me that the agreement was at will without penalty. They never addressed my needs as a customer. My bill is only {$230.00}. They placed false information regarding my bill with a collection agency who has placed information on my credit report without contacting me or giving me an opportunity to dispute the validity of the debt. The debt is not valid. The actions are unlawful and I am requesting that the actions of this collection agency be reported to the Federal Trade Commission.\n", - "5. I have continued to submit an investigation for a Bankruptcy place on my credit report. I have been trying to get this removed because it was place on my credit report in error and inaccurate. ALL THREE CREDIT BUREAUS have continue to ignore the information proving this was place in error and fail to properly investigate the dispute I have place in their office. \n", - "\n", - "1. They say they have verified this dispute with XXXX but I have a letter from XXXX stating this was removed because they were unable to verify the accuracy of the bankruptcy. I received this letter on XX/XX/XXXX. XXXX just finished an investigation on XX/XX/2019 stating the verified this with XXXX. \n", - "2.Experian Open the dispute on XX/XX/2019 and closed it on XX/XX/19 stated they verified with XXXX and the Bankruptcy court and I have a letter From XXXX stating they could not verify the accuracy of this dispute. I also, have a letter from the court house stating they do not verify information with the credit bureaus How could be this be on my XXXX file when XXXX has removed this item. \n", - "3. XXXX open and investigation XX/XX/2019 and closed it XX/XX/2019 No way they properly investigation I have submitted all information to dispute the inaccurate information. Please do a proper investigation. \n", - "\n", - "XXXX, Experian, and XXXX please do a proper investigation under 611 of the FCRA thank you very much I have attached the letter proving this this is not on my XXXX consumer report and a letter form the court house stating they do not report information to the credit bureaus from the XXXX XXXX XXXX, Clerk of Court United State Bankruptcy Court on dated XX/XX/2019 I have summited it to the credit bureaus to be ignored. I have as for a description of my investigation by section 611 of the FCRA and the information from the investigation is inaccurate.\n", + "Worst thing, after turning on escrow account, there is no transparency. Amount of escrow account is not in sync with all the sections of my online account. It seems that there are too many internal buggy systems in Mr. Cooper and everybody from different IT department is showing a different $ $ figure at various places. \n", + "Highly inefficient organization.\n", + "2. I had a loan with national Collegiate Trust. i did n't make any payments on the loan for 6 years and due to the \" statute to limitations '' the loan collector should not be contacting me any more, by they still are in both forms phone call 's and letters. I am also trying to buy a house for my family and found out that i can not get a USDA loan for the house because National Collegiate Trust has filled the loan as a government delinquent loan. The problem with that is the loan is a private loan not a federal/Government loan. due to the way they filled the lion on my credit report i am not able to buy my first home.\n", + "3. Cares act refund requested in XXXX, called multiple times given conflicting information every time. Its now XXXX and looking like its going to be even longer potentially months before the refund arrives. Blatant disregard for the cares act signed into law and terrible customer service. This company is ill suited to service the federal government contract it enjoys.\n", + "4. In an attempt to pay off my loan with Ocwen, I sent XXXX large payments to the institution on XXXX XXXX that would have decreased my loan amount to within a couple of hundred dollars ( you can not send XXXX payment that exceeds 80 % of the loan balance so I broke the payments up into XXXX XXXX. I scheduled the payments for the same day because I did not want any interest to accrue. After a few days, I noticed where the XXXX payments were withdrawn from my bank but that my Ocwen balance had not changed to reflect my XXXX payments. I called Ocwen on XXXX XXXX to ask about the payment. The Ocwen rep explained that because the XXXX payments were scheduled for the same day, that it exceeded the 80 % max limit. I asked Ocwen to return my XXXX payments. The rep said it would take XXXX business days. I called Ocwen back on XXXX XXXX when my funds had not been returned to my bank account. I gave them my account number and routing number to my bank, information that I am sure they already had since my monthly mortgage payments are debited from my account. They asked me to wait a couple more days. I called Ocwen back on XXXX XXXX. The rep asked me to be patient and assured me that the funds would be returned by XXXX XXXX. There were no funds returned to my account on XXXX XXXX. I called Ocwen again. I was asked to wait 40+ minutes on hold while the Ocwen rep put me in touch with an escalation specialist ( ES ). The ES told me that my funds would be reurned within XXXX hours and that he had put it in as a priority because I had called so often. There were no funds on XXXX XXXX. I called Ocwen again to see if there was a problem. There was. After speaking to a rep and another ES, I was told that my funds could not be returned to me! The ES said that he did not see my funds! He claimed to put me in touch with someone who XXXX be able to address my concerns. So that 's where I am now, on hold waiting to speak to yet another person! This is a significant amount of money and I fear that Ocwen is trying to get away with keeping my XXXX payments!\n", + "5. In XX/XX/XXXX we received our first bill from XXXX XXXX for XXXX. ( attached ) We promptly paid the bill. Again, in XX/XX/XXXX we paid our second bill for XXXX. Again, both on time. Then when XX/XX/XXXX statement came we were billed XXXX. My husband called XXXX XXXX to find out what the issue was. We were told there was a loan shortfall caused by us paying XXXX ( the amount we were billed ) and that our loan was re-amoritized. I argued I had a fixed rate loan, had never missed a payment, had never made a late payment, and paid exactly what was billed. Well, after double checking my promissory note ( attached ) and TILA ( attached ) I was to always be billed XXXX. XXXX changed the monthly payment and thus caused a shortfall. When I told them this information they refused to correct the shortfall. Not only did they not correct the mistake they refused to return my calls or emails to provide answers for these issues. Around 90 days later and this issue still exists and they still refuse to answer. Additionally I offered to make up the shortfall myself by offering a check for the missing amount and they just applied it to interest. Thus the shortfall still exists. The extra amount would have gone directly to principal. Additionally, in XX/XX/XXXX we made an extra payment amount on top of the monthly payment. This was made all at the same time. The letter we sent contained directions to only apply extra payments beyond the monthly requirement be applied to principal and not the next months payment. This was ignored. Then XXXX \" a higher up '' as she calls herself lied and told me it went to principal when clearly it did not. We requested this be fixed and it has not been done. No one has offered to fix anything either. A certified letter is attached that I mailed. I also made dozens of calls.\n", "\n" ] } @@ -1547,61 +1537,31 @@ "text": [ "Please highlight the most obvious difference between the two lists of comments:\n", "comment list 1:\n", - "1. I currently have a home loan with my ex husband with PHH Mortgages. We filed for divorce and in the divorce decree he became liable for the home and paying the payments. He ended up missing XXXX payments which effected my credit fairly substaintailly. when I became aware of the late payments, I ensured that the account was up to date and have since. I presented to them that I have the legal documents that he is obligated to make the payments each month and that I am not responisble for the payment. I asked them to remove the XXXX dings on my credit and they would not. I offered to present the paperwork and they still would not. The home is now being sold. I even filed with XXXX as a discrepency and they would not remove it. I would have never let these become a late payment. I was not even notified as they had all of his information in the file.\n", - "2. In the course of my student loan, I have been making payments and I feel that the payments haven't been added to the debt, the company stated that I am delinquent over 180 and my payments are auto pay. This has had a negative impact on my credit score.\n", - "3. The issue is 26 late payments on me and my wife 's credit reports due to a system error on a joint mortgage account that was always paid on time using autopay. ( will attach docs to support this ). \n", - "\n", - "This is an ongoing nightmare me and my wife are going through over the past 3 years. \n", - "Sent many dispute letters to the creditor and to the 3 bureaus, was promised multiple times that all late payments will be removed, we also has a letter from the bank stating we were never late on this account, also have a recording of a phone call with bank 's permission were the representative admits there was a system error and promised again that all late payments will be deleted from both of our credit reports. \n", - "As of today, for an unknown reason XXXX reports 6x30 days late payments, XXXX reports 24 lates, and Transunion 23 lates. \n", - "\n", - "We have always paid our mortgage on time for many years, enrolled in autopay and making 2 payments per month. Our mortgage is currently with XXXX XXXX, XXXX XXXX is the mortgage servicer who's collecting from us and disbursing payments to XXXX XXXX. \n", - "\n", - "I will attach here our mortgage transaction history confirming payments have been made on time, letter from the mortgage servicer XXXX XXXX XXXX XXXX confirming we were never late, copy of a page from my credit report showing all the late payments, and a few bank statements showing payment made on time while showing as late on our credit reports. ( XXXX & XXXX XXXX ) PLEASE HELP us to resolve this issue and have all late payments on this account removed from XXXX & XXXX XXXX. \n", - "\n", - "Her is a small portion of our previous communication with XXXX and their response : XX/XX/XXXX : we spoke with XXXX, XXXX stated that the funds were misappropriated and went to the wrong account. Said he'll contact their Tax Dep ' and get back to me and never did. \n", - "\n", - "XX/XX/XXXX : I spoke in length with XXXX from XXXX XXXX & XXXX from XXXX at XXXX who opened an investigation. Their supervisor said he made all the necessary changes. The next month, the money was withdrawn from my account on time and i received again a late fee and 30 days late on my credit report. \n", - "\n", - "XX/XX/XXXX Spoke to XXXX who sent me to XXXX who sent me to XXXX XXXX from Escalation department, she promised the issue was fixed and late payments will be removed in up to 30 days and she will email me a deletion letter. Nothing was sent! and i called and wrote 5 emails to her and never got a response. \n", - "\n", - "XX/XX/XXXX spoke with XXXX to follow up with XXXX XXXX, no response. \n", + "1. This is the third such complaint I have submitted regarding the same type of issue over the past 12-18 months. \n", "\n", - "XX/XX/XXXX spoke with XXXX at XXXX, she said that the transaction history is our proof that issue was corrected and she'll submit a request to delete the late payments as the system does not show that previous request was made. \n", - "\n", - "XX/XX/XXXX XXXX sent a letter stating that we had a shortage of {$5300.00}. Again, upon checking my bank account all monthly payments were made on time. To avoid further issues i sent a check for {$5300.00} on XX/XX/XXXX. \n", - "\n", - "XX/XX/XXXX following many joint calls with XXXX XXXX i received a letter from XXXX stating that my credit might have been affected due to processing error and that they sent XXXX XXXX a letter requesting a removal of all late payments. \n", - "\n", - "XX/XX/XXXX spoke with XXXX XXXX again.. \n", - "XX/XX/XXXX spoke with XXXX at XXXX I have many more... \n", - "\n", - "*** XX/XX/XXXX : SPOKE WITH XXXX XXXX ( resolution team ) at XXXX. She said they will delete the late payments from both reports ( XXXX & XXXX XXXX and will call me to follow up on XX/XX/XXXX. She also emailed me the payment activity on the account. XXXX I received the payment history but NO CALL OR RESOLUTION. \n", - "\n", - "XX/XX/XXXX : SPOKE WITH XXXX FROM XXXX AND XXXX FROM XXXX XXXX ON A XXXX WAY CALL at XXXX, XXXX PERMISSION TO RECORD THE CALL, SHE AGREED, we went over all the late payments, she said she sees the error and promised that this time it will be resolved and get deleted from our credit reports. Again, nothing was resolved and we never heard back from anyone.\n", - "4. XXXX ; XXXX and Transunion are reporting ( 30 ) plus days late on the XXXX XXXX partial account number XXXX. ( Please see page 3 of the attached credit report. ) This account was paid in XXXX, 2019 and the lates are reporting in XXXX, 2019. Please keep in mind that it is impossible to have late payments on an account that was paid off a month prior. This incorrect reporting is harming my credit score and this line item need to be removed from my credit report. I have contacted the ( 3 ) bureaus to fix this, however I have been unsuccessful.\n", - "5. My Macys account is due on the first of every month. Since I have had the card I have paid on the XXXX PRIOR to the due date. And have paid over the amount due. In XXXX my XXXX XXXX auto pay did not come out of my account and rather than calling me - on the XXXX of XXXX just 5 days late they cut my credit off and shut me out of my account so I can not even see my credit profile - I have made the payment and they still are locking me out - please look into this - you will see that is what happened and they are stating in a letter it is becasue my XXXX report shows a seriuos derogorty item which it does not and I have submitted a complaint with them as well. Macys has been the worst credit experience of my LIFE and I did read the reviews but thought it would be different for me I guess? \n", - "thank you for your help.\n", + "On XX/XX/XXXX, my co-signed account was flagged by Navient as past due. The XXXX payment was mailed priority on XX/XX/XXXX and received by Navient on XX/XX/XXXX and delivered to \" an individual '' per the post office. \n", + "I called Navient on XX/XX/XXXX to talk to them about why my account was flagged since they received the payment long before the due date. The payment is sent via XXXX money orders under the same cover. The XXXX money order ( {$160.00} ) was cashed on XX/XX/XXXX per XXXX XXXX, the second money order ( {$250.00} ) which was sent in the same priority envelope and received the same time has not been cashed. \n", + "When I called the customer service agent at Navient she told me that my account was past due and wanted me to send another payment. When I explained that they had received the payment she argued with me that if they received it, the payment would have been cashed. I asked to speak with a supervisor. \n", + "I was connected with supervisor, XXXX XXXX, who asked that I send copies of the payments to him so he could submit for a missing payment request. I faxed the proof on XXXX @ XXXX with a receipt acknowledgment. \n", + "On XX/XX/XXXX, the payment was still not applied to the account. When I called XXXX XXXX, the money order was still not cashed. I called Navient again. Because of an argumentative customer service rep again, I requested to speak with a supervisor. I spoke with XXXX XXXX. She states that payment was not received. I explained the situation again. She said the missing payment request had not been submitted. She had me upload the documents so she could request a missing payment search. I have done everything I have been asked. \n", + "This issues continues to occur. For approximately 6 months at a time, Navient gets this right then there are issues again and again. I have submitted CFPB complaints about this in the past. \n", + "I was told it would take 5-7 business days to be resolved.\n", + "2. I tried to submit a teacher loan forgiveness application and they lost my application. I submitted the application again after talking to XXXX people at Nelnet. Then when I called back to check on the status they told me that I needed to submit another form for a different school that I worked at. I had already called previously and asked if I needed to submit any other papers and they told me \" no ''. Therefore, I have been paying my loan for 5 months and it should be forgiven. I am still paying my loan because I have to wait for them to approve the new forgiveness paperwork.\n", + "3. PayPal continues to overcharge their currency rate. It it always inflated in their favor so that they collect the difference.\n", + "4. My government feeds are not coming on to my card and I need the problem fix today\n", + "5. Paypal Credit 's website is confusing and does not accurately reflect all activity. When speaking with representatives like XXXX, it 's confusing to them and they can barely follow along with it. I am not receiving statements, which proves it difficult to determine the due dates on the accounts. The Reps are n't knowledgeable and the only thing they repeat to you is the amount due on the screen.\n", "comment list 2:\n", - "1. I decided to try XXXX services for my wife and I so I purchased phones for both of us. After a day or two of trial we felt unsatisfied so we headed back to the store and we returned all items. We got charged with restocking fees and taxes. Later on I got a bill in the mail in which I was being charged {$1200.00} for the returned items. After several attempts of arguing for about XXXX months about whether I owed XXXX or not I was dismissed of such charges, but a month after I was charged by a collecting company called ERC for {$61.00}. I asked them to explain such charges weather if they were fees or taxes and they we unable to disclose information. Therefore, I asked them to send me a bill in the mail with details about the charges, as well as a dispute package and they told me they would send me a bill. About the dispute part, they said that I needed to call XXXX to discuss the charges with them but XXXX said that I had to discuss this with the collecting company. I never received a detailed statement neither a chanse to defend my self about such charges, I checked my credit score and found a red flag in it because of this. \n", - "\n", - "I am now hoping you may help me with this case. \n", - "\n", - "Thanks :\n", - "2. Over a year and a half ago we started the process of buying a home. Our mortgage guy sent us to a credit repair co. They got the collection account from Weltman , Weinberg & Reis taken off my credit, because it was unverifiable. Now it is back on my credit. I have credit reports showing the trade line on and then off and now today it is currently on my report. When I called to verify the account with WW & R they sent me a heavily redacted letter verifying absolutely nothing. I would like this unverifiable account taken off my credit and removed permanently. This should not be a loan I have to pay for if there is no verification that it is my debt. Attached are the credit reports and the letter of verification that was sent to me.\n", - "3. I recently disputed ( see attached letter ) with Receivable Management Services an account entry that they placed on my credit report without providing a dunning letter or any correspondence that would have allowed me 30 days to dispute the validity of the alleged debt. To date, I have not received any communication from them. They are blatantly violating my rights by reporting this inaccurate, erroneous, unverifiable entry.\n", - "\n", - "Additionally, this account entry does not reflect a payment history which should be included on any entry that is reflected on my credit report. In my previous communication to them, I specifically requested that they provide an agreement that states their authority to collect on the alleged debt, agreement with signature of the alleged debtor wherein he/she agreed to pay the creditor, alleged account number, date this alleged debt became payable, original delinquency date, and to date to no avail. \n", - "\n", - "As such, since they have refused to respond to my request and not provide any documentation to substantiate their allegations, coupled with the fact that they did not provide me a dunning letter is grounds for this erroneous, inaccurate, unverifiable entry to be deleted from my credit report.\n", - "4. I accepted service from XXXX XXXX XXXX. The company did not inform me that internet was required. They also told me that the agreement was at will without penalty. They never addressed my needs as a customer. My bill is only {$230.00}. They placed false information regarding my bill with a collection agency who has placed information on my credit report without contacting me or giving me an opportunity to dispute the validity of the debt. The debt is not valid. The actions are unlawful and I am requesting that the actions of this collection agency be reported to the Federal Trade Commission.\n", - "5. I have continued to submit an investigation for a Bankruptcy place on my credit report. I have been trying to get this removed because it was place on my credit report in error and inaccurate. ALL THREE CREDIT BUREAUS have continue to ignore the information proving this was place in error and fail to properly investigate the dispute I have place in their office. \n", - "\n", - "1. They say they have verified this dispute with XXXX but I have a letter from XXXX stating this was removed because they were unable to verify the accuracy of the bankruptcy. I received this letter on XX/XX/XXXX. XXXX just finished an investigation on XX/XX/2019 stating the verified this with XXXX. \n", - "2.Experian Open the dispute on XX/XX/2019 and closed it on XX/XX/19 stated they verified with XXXX and the Bankruptcy court and I have a letter From XXXX stating they could not verify the accuracy of this dispute. I also, have a letter from the court house stating they do not verify information with the credit bureaus How could be this be on my XXXX file when XXXX has removed this item. \n", - "3. XXXX open and investigation XX/XX/2019 and closed it XX/XX/2019 No way they properly investigation I have submitted all information to dispute the inaccurate information. Please do a proper investigation. \n", + "1. XXXX on XXXX XX/XX/2021 I have Mr. Cooper mortgage for years now. On XXXX XXXX XXXX I made an additional payment of $ XXXX towards my principal. More than 4 days - it's not reflected in the Amortization schedule ( Amortization schedule is not even visible ). Even after so many additional principal payments, Payoff calculator is way off and it still shows XXXX maturity date while it should start showing something like XX/XX/XXXX/XX/XX/XXXX as the initial date. There are lots of discrepancies on their website to reflect the balance and total. When called customer service on Friday and also chatted - i was assured of fixing this - but no fix till this point of time. Customer service there is a long wait. Auto bot doesn't let customers talk to the real person. \n", + "Finally after a lots of follow-up I got the amortization schedule via email but it is not reflecting another additional principal payment of {$4700.00} made on XX/XX/2021. \n", + "I did numerous chats and phone calls. Why i should depend on inefficinent humans to see my revised amortization schedule? \n", + "Why the online amortization schedule is not visible now? \n", "\n", - "XXXX, Experian, and XXXX please do a proper investigation under 611 of the FCRA thank you very much I have attached the letter proving this this is not on my XXXX consumer report and a letter form the court house stating they do not report information to the credit bureaus from the XXXX XXXX XXXX, Clerk of Court United State Bankruptcy Court on dated XX/XX/2019 I have summited it to the credit bureaus to be ignored. I have as for a description of my investigation by section 611 of the FCRA and the information from the investigation is inaccurate.\n", + "Worst thing, after turning on escrow account, there is no transparency. Amount of escrow account is not in sync with all the sections of my online account. It seems that there are too many internal buggy systems in Mr. Cooper and everybody from different IT department is showing a different $ $ figure at various places. \n", + "Highly inefficient organization.\n", + "2. I had a loan with national Collegiate Trust. i did n't make any payments on the loan for 6 years and due to the \" statute to limitations '' the loan collector should not be contacting me any more, by they still are in both forms phone call 's and letters. I am also trying to buy a house for my family and found out that i can not get a USDA loan for the house because National Collegiate Trust has filled the loan as a government delinquent loan. The problem with that is the loan is a private loan not a federal/Government loan. due to the way they filled the lion on my credit report i am not able to buy my first home.\n", + "3. Cares act refund requested in XXXX, called multiple times given conflicting information every time. Its now XXXX and looking like its going to be even longer potentially months before the refund arrives. Blatant disregard for the cares act signed into law and terrible customer service. This company is ill suited to service the federal government contract it enjoys.\n", + "4. In an attempt to pay off my loan with Ocwen, I sent XXXX large payments to the institution on XXXX XXXX that would have decreased my loan amount to within a couple of hundred dollars ( you can not send XXXX payment that exceeds 80 % of the loan balance so I broke the payments up into XXXX XXXX. I scheduled the payments for the same day because I did not want any interest to accrue. After a few days, I noticed where the XXXX payments were withdrawn from my bank but that my Ocwen balance had not changed to reflect my XXXX payments. I called Ocwen on XXXX XXXX to ask about the payment. The Ocwen rep explained that because the XXXX payments were scheduled for the same day, that it exceeded the 80 % max limit. I asked Ocwen to return my XXXX payments. The rep said it would take XXXX business days. I called Ocwen back on XXXX XXXX when my funds had not been returned to my bank account. I gave them my account number and routing number to my bank, information that I am sure they already had since my monthly mortgage payments are debited from my account. They asked me to wait a couple more days. I called Ocwen back on XXXX XXXX. The rep asked me to be patient and assured me that the funds would be returned by XXXX XXXX. There were no funds returned to my account on XXXX XXXX. I called Ocwen again. I was asked to wait 40+ minutes on hold while the Ocwen rep put me in touch with an escalation specialist ( ES ). The ES told me that my funds would be reurned within XXXX hours and that he had put it in as a priority because I had called so often. There were no funds on XXXX XXXX. I called Ocwen again to see if there was a problem. There was. After speaking to a rep and another ES, I was told that my funds could not be returned to me! The ES said that he did not see my funds! He claimed to put me in touch with someone who XXXX be able to address my concerns. So that 's where I am now, on hold waiting to speak to yet another person! This is a significant amount of money and I fear that Ocwen is trying to get away with keeping my XXXX payments!\n", + "5. In XX/XX/XXXX we received our first bill from XXXX XXXX for XXXX. ( attached ) We promptly paid the bill. Again, in XX/XX/XXXX we paid our second bill for XXXX. Again, both on time. Then when XX/XX/XXXX statement came we were billed XXXX. My husband called XXXX XXXX to find out what the issue was. We were told there was a loan shortfall caused by us paying XXXX ( the amount we were billed ) and that our loan was re-amoritized. I argued I had a fixed rate loan, had never missed a payment, had never made a late payment, and paid exactly what was billed. Well, after double checking my promissory note ( attached ) and TILA ( attached ) I was to always be billed XXXX. XXXX changed the monthly payment and thus caused a shortfall. When I told them this information they refused to correct the shortfall. Not only did they not correct the mistake they refused to return my calls or emails to provide answers for these issues. Around 90 days later and this issue still exists and they still refuse to answer. Additionally I offered to make up the shortfall myself by offering a check for the missing amount and they just applied it to interest. Thus the shortfall still exists. The extra amount would have gone directly to principal. Additionally, in XX/XX/XXXX we made an extra payment amount on top of the monthly payment. This was made all at the same time. The letter we sent contained directions to only apply extra payments beyond the monthly requirement be applied to principal and not the next months payment. This was ignored. Then XXXX \" a higher up '' as she calls herself lied and told me it went to principal when clearly it did not. We requested this be fixed and it has not been done. No one has offered to fix anything either. A certified letter is attached that I mailed. I also made dozens of calls.\n", "\n" ] } @@ -1633,7 +1593,7 @@ { "data": { "text/html": [ - "Query job a069b4a5-5238-4ca8-a6c0-d48781d00f6c is DONE. 0 Bytes processed. Open Job" + "Query job de5da6c9-96b5-42a1-b199-42687392fe37 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1671,7 +1631,7 @@ { "data": { "text/html": [ - "Query job 63f6e1d0-b0dc-4f5c-a001-5889c28162c5 is DONE. 0 Bytes processed. Open Job" + "Query job 1363c327-00b5-4835-a902-da84882bc996 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1683,7 +1643,7 @@ { "data": { "text/html": [ - "Query job c1c9e28b-ba6d-4485-b892-0bf2428f927c is DONE. 8 Bytes processed. Open Job" + "Query job c5996f1e-a140-4e7d-8775-091e1a73d882 is DONE. 8 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1695,7 +1655,7 @@ { "data": { "text/html": [ - "Query job 67402b3c-eee4-4fe4-aeaf-fb27606ecde7 is DONE. 2 Bytes processed. Open Job" + "Query job db1de3ab-2e6e-4b3f-8e6a-01bad33ac45f is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1707,7 +1667,7 @@ { "data": { "text/html": [ - "Query job 83166900-0787-4a6d-b822-c3be87990e35 is DONE. 328 Bytes processed. Open Job" + "Query job 38d9a9d0-7f03-4091-858b-f864da30987e is DONE. 375 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1719,7 +1679,7 @@ { "data": { "text/plain": [ - "'The most obvious difference between the two lists of comments is that the first list contains comments about credit report issues related to mortgages and loans, while the second list contains comments about credit report issues related to other types of debts, such as cell phone bills, collections, and bankruptcies.'" + "'The most obvious difference between the two lists of comments is the subject matter. The first list of comments is primarily focused on issues with financial institutions, such as Navient, Nelnet, PayPal, and Mr. Cooper. The second list of comments is primarily focused on issues with government agencies, such as the National Collegiate Trust, the USDA, and Ocwen.'" ] }, "execution_count": 19, @@ -1773,7 +1733,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.9" } }, "nbformat": 4, diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index e526d543622..b9e48898017 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -261,8 +261,8 @@ def test_embedding_generator_predict_success( ): df = palm2_embedding_generator_model.predict(llm_text_df).to_pandas() assert df.shape == (3, 4) - assert "ml_generate_embedding_result" in df.columns - series = df["ml_generate_embedding_result"] + assert "text_embedding" in df.columns + series = df["text_embedding"] value = series[0] assert len(value) == 768 @@ -273,8 +273,8 @@ def test_embedding_generator_multilingual_predict_success( ): df = palm2_embedding_generator_multilingual_model.predict(llm_text_df).to_pandas() assert df.shape == (3, 4) - assert "ml_generate_embedding_result" in df.columns - series = df["ml_generate_embedding_result"] + assert "text_embedding" in df.columns + series = df["text_embedding"] value = series[0] assert len(value) == 768 @@ -285,8 +285,8 @@ def test_embedding_generator_predict_series_success( ): df = palm2_embedding_generator_model.predict(llm_text_df["prompt"]).to_pandas() assert df.shape == (3, 4) - assert "ml_generate_embedding_result" in df.columns - series = df["ml_generate_embedding_result"] + assert "text_embedding" in df.columns + series = df["text_embedding"] value = series[0] assert len(value) == 768 From ea9576125d46f3912372f75ebe51196ba83e96db Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:06:17 -0700 Subject: [PATCH 457/921] docs: add opeartions in API docs (#557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - [x] Plotting module:https://screenshot.googleplex.com/8VQbFyz4U2vzVd2 Fixes # 🦕 --- docs/templates/toc.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 57b0522d043..1898655535a 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -60,6 +60,11 @@ name: Indexers - name: pandas uid: bigframes.pandas + - items: + - name: Plotting + uid: bigframes.operations.plotting + - name: PlotAccessor + uid: bigframes.operations.plotting.PlotAccessor - items: - name: Series uid: bigframes.series.Series From 1caac27fe95ef3eb36bad2ac351090891922858c Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:29:31 -0700 Subject: [PATCH 458/921] fix: exclude list-like s parameter in plot.scatter (#568) --- bigframes/operations/_matplotlib/core.py | 6 ++++++ tests/system/small/operations/test_plotting.py | 16 ++++++++++++++++ .../bigframes_vendored/pandas/plotting/_core.py | 3 --- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index ad5abb4bcad..2c1c2bc4ac7 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -98,6 +98,12 @@ def __init__(self, data, **kwargs) -> None: f"Only support a single color string or a column name/posision. {constants.FEEDBACK_LINK}" ) + s = self.kwargs.get("s", None) + if self._is_sequence_arg(s): + raise NotImplementedError( + f"Only support a single color string or a column name/posision. {constants.FEEDBACK_LINK}" + ) + def _compute_plot_data(self): sample = self._compute_sample_data(self.data) diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index 41ea7d4ebb8..824125adf29 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -240,6 +240,22 @@ def test_scatter_args_c(c): ) +@pytest.mark.parametrize( + ("arg_name"), + [ + pytest.param("c", marks=pytest.mark.xfail(raises=NotImplementedError)), + pytest.param("s", marks=pytest.mark.xfail(raises=NotImplementedError)), + ], +) +def test_scatter_sequence_arg(arg_name): + data = { + "a": [1, 2, 3], + "b": [1, 2, 3], + } + arg_value = [3, 3, 1] + bpd.DataFrame(data).plot.scatter(x="a", y="b", **{arg_name: arg_value}) + + def test_sampling_plot_args_n(): df = bpd.DataFrame(np.arange(bf_mpl.DEFAULT_SAMPLING_N * 10), columns=["one"]) ax = df.plot.line() diff --git a/third_party/bigframes_vendored/pandas/plotting/_core.py b/third_party/bigframes_vendored/pandas/plotting/_core.py index f8da9efdc00..19f56965df8 100644 --- a/third_party/bigframes_vendored/pandas/plotting/_core.py +++ b/third_party/bigframes_vendored/pandas/plotting/_core.py @@ -257,9 +257,6 @@ def scatter( - A string with the name of the column to be used for marker's size. - A single scalar so all points have the same size. - - A sequence of scalars, which will be used for each point's size - recursively. For instance, when passing [2,14] all points size - will be either 2 or 14, alternatively. c (str, int or array-like, optional): The color of each point. Possible values are: From 4ae0262a2b1dfc35c1e4c3392b9e21456d6e964e Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 2 Apr 2024 23:40:16 -0700 Subject: [PATCH 459/921] fix: Restore string to date/time type coercion (#565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/dtypes.py | 23 +++++-- bigframes/operations/__init__.py | 24 +++---- bigframes/operations/type.py | 22 +++++-- .../system/small/operations/test_datetimes.py | 64 +++++++++++++++++++ 4 files changed, 107 insertions(+), 26 deletions(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 79e1456f317..3d8c06d1887 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -648,6 +648,7 @@ def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: def lcd_type(dtype1: Dtype, dtype2: Dtype) -> Dtype: + """Get the supertype of the two types.""" if dtype1 == dtype2: return dtype1 # Implicit conversion currently only supported for numeric types @@ -664,12 +665,26 @@ def lcd_type(dtype1: Dtype, dtype2: Dtype) -> Dtype: return hierarchy[lcd_index] -def lcd_etype(etype1: ExpressionType, etype2: ExpressionType) -> ExpressionType: - if etype1 is None: +def coerce_to_common(etype1: ExpressionType, etype2: ExpressionType) -> ExpressionType: + """Coerce types to a common type or throw a TypeError""" + if etype1 is not None and etype2 is not None: + common_supertype = lcd_type(etype1, etype2) + if common_supertype is not None: + return common_supertype + if can_coerce(etype1, etype2): return etype2 - if etype2 is None: + if can_coerce(etype2, etype1): return etype1 - return lcd_type_or_throw(etype1, etype2) + raise TypeError(f"Cannot coerce {etype1} and {etype2} to a common type.") + + +def can_coerce(source_type: ExpressionType, target_type: ExpressionType) -> bool: + if source_type is None: + return True # None can be coerced to any supported type + else: + return (source_type == STRING_DTYPE) and ( + target_type in (DATETIME_DTYPE, TIMESTAMP_DTYPE, TIME_DTYPE, DATE_DTYPE) + ) def lcd_type_or_throw(dtype1: Dtype, dtype2: Dtype) -> Dtype: diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index dcd54946266..0dcc6432383 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -548,16 +548,10 @@ def output_type(self, *input_types): # Binary Ops -fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COMMON_SUPERTYPE) -cliplower_op = create_binary_op( - name="clip_lower", type_signature=op_typing.COMMON_SUPERTYPE -) -clipupper_op = create_binary_op( - name="clip_upper", type_signature=op_typing.COMMON_SUPERTYPE -) -coalesce_op = create_binary_op( - name="coalesce", type_signature=op_typing.COMMON_SUPERTYPE -) +fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COERCE) +cliplower_op = create_binary_op(name="clip_lower", type_signature=op_typing.COERCE) +clipupper_op = create_binary_op(name="clip_upper", type_signature=op_typing.COERCE) +coalesce_op = create_binary_op(name="coalesce", type_signature=op_typing.COERCE) ## Math Ops @@ -575,7 +569,7 @@ def output_type(self, *input_types): right_type is None or dtypes.is_numeric(right_type) ): # Numeric addition - return dtypes.lcd_etype(left_type, right_type) + return dtypes.coerce_to_common(left_type, right_type) # TODO: Add temporal addition once delta types supported raise TypeError(f"Cannot add dtypes {left_type} and {right_type}") @@ -592,7 +586,7 @@ def output_type(self, *input_types): right_type is None or dtypes.is_numeric(right_type) ): # Numeric subtraction - return dtypes.lcd_etype(left_type, right_type) + return dtypes.coerce_to_common(left_type, right_type) # TODO: Add temporal addition once delta types supported raise TypeError(f"Cannot subtract dtypes {left_type} and {right_type}") @@ -652,7 +646,7 @@ class WhereOp(TernaryOp): def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: if input_types[1] != dtypes.BOOL_DTYPE: raise TypeError("where condition must be a boolean") - return dtypes.lcd_etype(input_types[0], input_types[2]) + return dtypes.coerce_to_common(input_types[0], input_types[2]) where_op = WhereOp() @@ -663,8 +657,8 @@ class ClipOp(TernaryOp): name: typing.ClassVar[str] = "clip" def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - return dtypes.lcd_etype( - input_types[0], dtypes.lcd_etype(input_types[1], input_types[2]) + return dtypes.coerce_to_common( + input_types[0], dtypes.coerce_to_common(input_types[1], input_types[2]) ) diff --git a/bigframes/operations/type.py b/bigframes/operations/type.py index a1dc8edffc7..f4690708054 100644 --- a/bigframes/operations/type.py +++ b/bigframes/operations/type.py @@ -118,7 +118,7 @@ def output_type( raise TypeError(f"Type {left_type} is not numeric") if (right_type is not None) and not bigframes.dtypes.is_numeric(right_type): raise TypeError(f"Type {right_type} is not numeric") - return bigframes.dtypes.lcd_etype(left_type, right_type) + return bigframes.dtypes.coerce_to_common(left_type, right_type) @dataclasses.dataclass @@ -132,7 +132,7 @@ def output_type( raise TypeError(f"Type {left_type} is not numeric") if (right_type is not None) and not bigframes.dtypes.is_numeric(right_type): raise TypeError(f"Type {right_type} is not numeric") - lcd_type = bigframes.dtypes.lcd_etype(left_type, right_type) + lcd_type = bigframes.dtypes.coerce_to_common(left_type, right_type) if lcd_type == bigframes.dtypes.INT_DTYPE: # Real numeric ops produce floats on int input return bigframes.dtypes.FLOAT_DTYPE @@ -140,13 +140,21 @@ def output_type( @dataclasses.dataclass -class Supertype(BinaryTypeSignature): - """Type signature for functions that return a the supertype of its inputs. Currently BigFrames just supports upcasting numerics.""" +class CoerceCommon(BinaryTypeSignature): + """Attempt to coerce inputs to a compatible type.""" def output_type( self, left_type: ExpressionType, right_type: ExpressionType ) -> ExpressionType: - return bigframes.dtypes.lcd_etype(left_type, right_type) + try: + return bigframes.dtypes.coerce_to_common(left_type, right_type) + except TypeError: + pass + if bigframes.dtypes.can_coerce(left_type, right_type): + return right_type + if bigframes.dtypes.can_coerce(right_type, left_type): + return left_type + raise TypeError(f"Cannot coerce {left_type} and {right_type} to a common type.") @dataclasses.dataclass @@ -156,7 +164,7 @@ class Comparison(BinaryTypeSignature): def output_type( self, left_type: ExpressionType, right_type: ExpressionType ) -> ExpressionType: - common_type = bigframes.dtypes.lcd_etype(left_type, right_type) + common_type = CoerceCommon().output_type(left_type, right_type) if not bigframes.dtypes.is_comparable(common_type): raise TypeError(f"Types {left_type} and {right_type} are not comparable") return bigframes.dtypes.BOOL_DTYPE @@ -188,7 +196,7 @@ def output_type( BINARY_NUMERIC = BinaryNumeric() BINARY_REAL_NUMERIC = BinaryRealNumeric() COMPARISON = Comparison() -COMMON_SUPERTYPE = Supertype() +COERCE = CoerceCommon() LOGICAL = Logical() STRING_TRANSFORM = TypePreserving( bigframes.dtypes.is_string_like, description="numeric" diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index b952289a72d..2824e86979a 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime + import pandas as pd import pytest @@ -303,3 +305,65 @@ def test_dt_floor(scalars_dfs, col_name, freq): pd_result.astype(scalars_df[col_name].dtype), # floor preserves type bf_result, ) + + +def test_dt_compare_coerce_str_datetime(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df["datetime_col"] + bf_result = (bf_series >= "2024-01-01").to_pandas() + + pd_result = scalars_pandas_df["datetime_col"] >= pd.to_datetime("2024-01-01") + + # pandas produces pyarrow bool dtype + assert_series_equal(pd_result, bf_result, check_dtype=False) + + +def test_dt_clip_datetime_literals(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df["date_col"] + bf_result = bf_series.clip( + datetime.date(2020, 1, 1), datetime.date(2024, 1, 1) + ).to_pandas() + + pd_result = scalars_pandas_df["date_col"].clip( + datetime.date(2020, 1, 1), datetime.date(2024, 1, 1) + ) + + assert_series_equal( + pd_result, + bf_result, + ) + + +def test_dt_clip_coerce_str_date(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df["date_col"] + bf_result = bf_series.clip("2020-01-01", "2024-01-01").to_pandas() + + # Pandas can't coerce with pyarrow types so convert first + pd_result = scalars_pandas_df["date_col"].clip( + datetime.date(2020, 1, 1), datetime.date(2024, 1, 1) + ) + + assert_series_equal( + pd_result, + bf_result, + ) + + +def test_dt_clip_coerce_str_timestamp(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series: bigframes.series.Series = scalars_df["timestamp_col"] + bf_result = bf_series.clip( + "2020-01-01T20:03:50Z", "2024-01-01T20:03:50Z" + ).to_pandas() + + pd_result = scalars_pandas_df["timestamp_col"].clip( + pd.to_datetime("2020-01-01T20:03:50Z", utc=True), + pd.to_datetime("2024-01-01T20:03:50Z", utc=True), + ) + + assert_series_equal( + pd_result, + bf_result, + ) From b188146466780e6f7a041f51f5be51a7d60719c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 3 Apr 2024 08:43:00 -0500 Subject: [PATCH 460/921] fix: include all names in MultiIndex repr (#564) docs: include Index in table-of-contents (#564) --- bigframes/core/blocks.py | 4 +- bigframes/core/indexes/__init__.py | 2 +- bigframes/core/indexes/{index.py => base.py} | 15 +++---- docs/reference/bigframes.pandas/indexing.rst | 2 +- docs/templates/toc.yml | 2 + scripts/publish_api_coverage.py | 4 +- tests/system/small/test_index.py | 39 +++++++++++++++++++ tests/system/small/test_session.py | 2 +- .../bigframes_vendored/pandas/core/frame.py | 2 +- .../pandas/core/indexes/base.py | 12 ++++++ .../bigframes_vendored/pandas/core/series.py | 2 +- 11 files changed, 69 insertions(+), 17 deletions(-) rename bigframes/core/indexes/{index.py => base.py} (98%) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 11899eef113..04a98ac9a45 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1314,8 +1314,8 @@ def retrieve_repr_request_results( head_block = self computed_df, query_job = head_block.to_pandas() formatted_df = computed_df.set_axis(self.column_labels, axis=1) - # we reset the axis and substitute the bf index name for the default - formatted_df.index.name = self.index.name + # we reset the axis and substitute the bf index name(s) for the default + formatted_df.index.names = self.index.names # type: ignore return formatted_df, count, query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: diff --git a/bigframes/core/indexes/__init__.py b/bigframes/core/indexes/__init__.py index 6419d0985af..ae6011ffa57 100644 --- a/bigframes/core/indexes/__init__.py +++ b/bigframes/core/indexes/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from bigframes.core.indexes.index import Index +from bigframes.core.indexes.base import Index __all__ = [ "Index", diff --git a/bigframes/core/indexes/index.py b/bigframes/core/indexes/base.py similarity index 98% rename from bigframes/core/indexes/index.py rename to bigframes/core/indexes/base.py index c818b68711c..daa52a02b9d 100644 --- a/bigframes/core/indexes/index.py +++ b/bigframes/core/indexes/base.py @@ -88,7 +88,12 @@ def from_frame( @property def name(self) -> blocks.Label: - return self.names[0] + names = self.names + if len(names) == 1: + return self.names[0] + else: + # pandas returns None for MultiIndex.name. + return None @name.setter def name(self, value: blocks.Label): @@ -460,14 +465,6 @@ def __init__( super().__init__(series_or_dataframe._block) self._whole_frame = series_or_dataframe - @property - def name(self) -> blocks.Label: - return self.names[0] - - @name.setter - def name(self, value: blocks.Label): - self.names = [value] - @property def names(self) -> typing.Sequence[blocks.Label]: """Returns the names of the Index.""" diff --git a/docs/reference/bigframes.pandas/indexing.rst b/docs/reference/bigframes.pandas/indexing.rst index 8f7f1947401..2cc1acfabfa 100644 --- a/docs/reference/bigframes.pandas/indexing.rst +++ b/docs/reference/bigframes.pandas/indexing.rst @@ -3,7 +3,7 @@ Index objects ============= -.. autoclass:: bigframes.core.indexes.index.Index +.. autoclass:: bigframes.core.indexes.base.Index :members: :inherited-members: :undoc-members: diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 1898655535a..3c2c688d782 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -40,6 +40,8 @@ - name: SeriesGroupBy uid: bigframes.core.groupby.SeriesGroupBy name: Groupby + - name: Index + uid: bigframes.core.indexes.base.Index - items: - name: AtDataFrameIndexer uid: bigframes.core.indexers.AtDataFrameIndexer diff --git a/scripts/publish_api_coverage.py b/scripts/publish_api_coverage.py index 4a35ade9efb..25fbfbf9884 100644 --- a/scripts/publish_api_coverage.py +++ b/scripts/publish_api_coverage.py @@ -44,6 +44,9 @@ "dataframegroupby": ( "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.core.groupby.DataFrameGroupBy#bigframes_core_groupby_DataFrameGroupBy_" ), + "index": ( + "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.core.indexes.base.Index#bigframes_core_indexes_base_Index_" + ), "series": ( "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.series.Series#bigframes_series_Series_" ), @@ -59,7 +62,6 @@ "window": ( "https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.core.window.Window#bigframes_core_window_Window_" ), - # TODO: Index not documented. } diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index 1f39ba25fe1..c419dc49075 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -370,3 +370,42 @@ def test_index_isin(scalars_df_index, scalars_pandas_df_index): bf_series, check_names=False, ) + + +def test_multiindex_name_is_none(session): + df = pd.DataFrame( + { + "A": [0, 0, 0, 1, 1, 1], + "B": ["x", "y", "z", "x", "y", "z"], + "C": [123, 345, 789, -123, -345, -789], + "D": ["a", "b", "c", "d", "e", "f"], + }, + ) + index = session.read_pandas(df).set_index(["A", "B"]).index + assert index.name is None + + +def test_multiindex_names_not_none(session): + df = pd.DataFrame( + { + "A": [0, 0, 0, 1, 1, 1], + "B": ["x", "y", "z", "x", "y", "z"], + "C": [123, 345, 789, -123, -345, -789], + "D": ["a", "b", "c", "d", "e", "f"], + }, + ) + index = session.read_pandas(df).set_index(["A", "B"]).index + assert tuple(index.names) == ("A", "B") + + +def test_multiindex_repr_includes_all_names(session): + df = pd.DataFrame( + { + "A": [0, 0, 0, 1, 1, 1], + "B": ["x", "y", "z", "x", "y", "z"], + "C": [123, 345, 789, -123, -345, -789], + "D": ["a", "b", "c", "d", "e", "f"], + }, + ) + index = session.read_pandas(df).set_index(["A", "B"]).index + assert "names=['A', 'B']" in repr(index) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index d0c20f3839b..28a3f038605 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -27,7 +27,7 @@ import pytest import bigframes -import bigframes.core.indexes.index +import bigframes.core.indexes.base import bigframes.dataframe import bigframes.dtypes import bigframes.ml.linear_model diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 50cce1eeabc..3ae5b0db2aa 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4797,7 +4797,7 @@ def index(self): MultiIndex([( 'Alice', 'Seattle'), ( 'Bob', 'New York'), ('Aritra', 'Kona')], - name='Name') + names=['Name', 'Location']) >>> df1.index.values array([('Alice', 'Seattle'), ('Bob', 'New York'), ('Aritra', 'Kona')], dtype=object) diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index 3ad87292719..7f5761e45b9 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -8,6 +8,18 @@ class Index: """Immutable sequence used for indexing and alignment. The basic object storing axis labels for all objects. + + Args: + data (pandas.Series | pandas.Index | bigframes.series.Series | bigframes.core.indexes.base.Index): + Labels (1-dimensional). + dtype: + Data type for the output Index. If not specified, this will be + inferred from `data`. + name: + Name to be stored in the index. + session (Optional[bigframes.session.Session]): + BigQuery DataFrames session where queries are run. If not set, + a default session is used. """ @property diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 0aebd0660f7..89b39cf8a04 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -87,7 +87,7 @@ def index(self): MultiIndex([( 'Alice', 'Seattle'), ( 'Bob', 'New York'), ('Aritra', 'Kona')], - name='Name') + names=['Name', 'Location']) >>> s1.index.values array([('Alice', 'Seattle'), ('Bob', 'New York'), ('Aritra', 'Kona')], dtype=object) From 90bcec5c73f7eefeff14bbd8bdcad3a4c9d91d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 3 Apr 2024 09:46:18 -0500 Subject: [PATCH 461/921] docs: `bigframes.options.bigquery.project` and `location` are optional in some circumstances (#548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Follow-up to launch feedback and https://togithub.com/googleapis/python-bigquery-dataframes/pull/451 🦕 --- samples/snippets/quickstart.py | 3 ++- samples/snippets/set_options_test.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/samples/snippets/quickstart.py b/samples/snippets/quickstart.py index a15ea168534..ae3a9340049 100644 --- a/samples/snippets/quickstart.py +++ b/samples/snippets/quickstart.py @@ -29,8 +29,9 @@ def run_quickstart(project_id: str): import bigframes.pandas as bpd # Set BigQuery DataFrames options + # Note: The project option is not required in all environments. + # On BigQuery Studio, the project ID is automatically detected. bpd.options.bigquery.project = your_gcp_project_id - bpd.options.bigquery.location = "us" # Create a DataFrame from a BigQuery table query_or_table = "bigquery-public-data.ml_datasets.penguins" diff --git a/samples/snippets/set_options_test.py b/samples/snippets/set_options_test.py index ef6f41ce541..f981009e9a8 100644 --- a/samples/snippets/set_options_test.py +++ b/samples/snippets/set_options_test.py @@ -26,7 +26,14 @@ def test_bigquery_dataframes_set_options(): REGION = "US" # @param {type:"string"} # Set BigQuery DataFrames options + # Note: The project option is not required in all environments. + # On BigQuery Studio, the project ID is automatically detected. bpd.options.bigquery.project = PROJECT_ID + + # Note: The location option is not required. + # It defaults to the location of the first table or query + # passed to read_gbq(). For APIs where a location can't be + # auto-detected, the location defaults to the "US" location. bpd.options.bigquery.location = REGION # [END bigquery_dataframes_set_options] From 853c25e8023bf877f28cda4dade0694d0299a83e Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:33:45 -0700 Subject: [PATCH 462/921] feat: add ml.metrics.mean_squared_error (#559) * feat: add ml.metrics.mean_squared_error * fix docs * fix docs --- bigframes/ml/metrics/__init__.py | 2 + bigframes/ml/metrics/_metrics.py | 14 ++ tests/system/small/ml/test_metrics.py | 121 ++++++++---------- .../sklearn/metrics/_regression.py | 27 ++++ 4 files changed, 95 insertions(+), 69 deletions(-) diff --git a/bigframes/ml/metrics/__init__.py b/bigframes/ml/metrics/__init__.py index 6b0a243426e..e79b46877b0 100644 --- a/bigframes/ml/metrics/__init__.py +++ b/bigframes/ml/metrics/__init__.py @@ -18,6 +18,7 @@ auc, confusion_matrix, f1_score, + mean_squared_error, precision_score, r2_score, recall_score, @@ -35,5 +36,6 @@ "confusion_matrix", "precision_score", "f1_score", + "mean_squared_error", "pairwise", ] diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index ee86798b33f..542e6300a8f 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -335,3 +335,17 @@ def f1_score( f1_score.__doc__ = inspect.getdoc(vendored_metrics_classification.f1_score) + + +def mean_squared_error( + y_true: Union[bpd.DataFrame, bpd.Series], + y_pred: Union[bpd.DataFrame, bpd.Series], +) -> float: + y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) + + return (y_pred_series - y_true_series).pow(2).sum() / len(y_true_series) + + +mean_squared_error.__doc__ = inspect.getdoc( + vendored_metrics_regression.mean_squared_error +) diff --git a/tests/system/small/ml/test_metrics.py b/tests/system/small/ml/test_metrics.py index b40982e2829..c4c7eb4b881 100644 --- a/tests/system/small/ml/test_metrics.py +++ b/tests/system/small/ml/test_metrics.py @@ -19,7 +19,8 @@ import pytest import sklearn.metrics as sklearn_metrics # type: ignore -import bigframes.ml.metrics +import bigframes +from bigframes.ml import metrics def test_r2_score_perfect_fit(session): @@ -32,9 +33,7 @@ def test_r2_score_perfect_fit(session): df = session.read_pandas(pd_df) assert ( - bigframes.ml.metrics.r2_score( - df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]] - ) + metrics.r2_score(df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]]) == 1.0 ) @@ -43,7 +42,7 @@ def test_r2_score_bad_fit(session): pd_df = pd.DataFrame({"y_true": [1, 2, 3, 4, 5], "y_pred": [5, 4, 3, 2, 1]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred"]]) == -3.0 + assert metrics.r2_score(df[["y_true"]], df[["y_pred"]]) == -3.0 def test_r2_score_force_finite(session): @@ -56,23 +55,21 @@ def test_r2_score_force_finite(session): ) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.r2_score( + assert metrics.r2_score( df[["y_true"]], df[["y_pred_1"]], force_finite=False ) == float("-inf") - assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred_1"]]) == 0.0 + assert metrics.r2_score(df[["y_true"]], df[["y_pred_1"]]) == 0.0 assert math.isnan( - bigframes.ml.metrics.r2_score( - df[["y_true"]], df[["y_pred_2"]], force_finite=False - ) + metrics.r2_score(df[["y_true"]], df[["y_pred_2"]], force_finite=False) ) - assert bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred_2"]]) == 1.0 + assert metrics.r2_score(df[["y_true"]], df[["y_pred_2"]]) == 1.0 def test_r2_score_ok_fit_matches_sklearn(session): pd_df = pd.DataFrame({"y_true": [1, 2, 3, 4, 5], "y_pred": [2, 3, 4, 3, 6]}) df = session.read_pandas(pd_df) - bf_result = bigframes.ml.metrics.r2_score(df[["y_true"]], df[["y_pred"]]) + bf_result = metrics.r2_score(df[["y_true"]], df[["y_pred"]]) sklearn_result = sklearn_metrics.r2_score(pd_df[["y_true"]], pd_df[["y_pred"]]) assert math.isclose(bf_result, sklearn_result) @@ -81,7 +78,7 @@ def test_r2_score_series(session): pd_df = pd.DataFrame({"y_true": [1, 7, 3, 2, 5], "y_pred": [1, 7, 3, 2, 5]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.r2_score(df["y_true"], df["y_pred"]) == 1.0 + assert metrics.r2_score(df["y_true"], df["y_pred"]) == 1.0 def test_accuracy_score_perfect_fit(session): @@ -94,7 +91,7 @@ def test_accuracy_score_perfect_fit(session): df = session.read_pandas(pd_df) assert ( - bigframes.ml.metrics.accuracy_score( + metrics.accuracy_score( df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]] ) == 1.0 @@ -105,26 +102,21 @@ def test_accuracy_score_bad_fit(session): pd_df = pd.DataFrame({"y_true": [0, 2, 1, 3, 4], "y_pred": [0, 1, 2, 3, 4]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) == 0.6 + assert metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) == 0.6 def test_accuracy_score_not_normailze(session): pd_df = pd.DataFrame({"y_true": [0, 2, 1, 3, 4], "y_pred": [0, 1, 2, 3, 4]}) df = session.read_pandas(pd_df) - assert ( - bigframes.ml.metrics.accuracy_score( - df[["y_true"]], df[["y_pred"]], normalize=False - ) - == 3 - ) + assert metrics.accuracy_score(df[["y_true"]], df[["y_pred"]], normalize=False) == 3 def test_accuracy_score_fit_matches_sklearn(session): pd_df = pd.DataFrame({"y_true": [1, 2, 3, 4, 5], "y_pred": [2, 3, 4, 3, 6]}) df = session.read_pandas(pd_df) - bf_result = bigframes.ml.metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) + bf_result = metrics.accuracy_score(df[["y_true"]], df[["y_pred"]]) sklearn_result = sklearn_metrics.accuracy_score( pd_df[["y_true"]], pd_df[["y_pred"]] ) @@ -135,7 +127,7 @@ def test_accuracy_score_series(session): pd_df = pd.DataFrame({"y_true": [1, 7, 3, 2, 5], "y_pred": [1, 7, 3, 2, 5]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.accuracy_score(df["y_true"], df["y_pred"]) == 1.0 + assert metrics.accuracy_score(df["y_true"], df["y_pred"]) == 1.0 def test_roc_curve_binary_classification_prediction_returns_expected(session): @@ -158,7 +150,7 @@ def test_roc_curve_binary_classification_prediction_returns_expected(session): ) df = session.read_pandas(pd_df) - fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + fpr, tpr, thresholds = metrics.roc_curve( df[["y_true_arbitrary_name"]], df[["y_score_arbitrary_name"]], drop_intermediate=False, @@ -219,7 +211,7 @@ def test_roc_curve_binary_classification_prediction_matches_sklearn(session): ) df = session.read_pandas(pd_df) - fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + fpr, tpr, thresholds = metrics.roc_curve( df[["y_true"]], df[["y_score"]], drop_intermediate=False ) expected_fpr, expected_tpr, expected_thresholds = sklearn_metrics.roc_curve( @@ -259,7 +251,7 @@ def test_roc_curve_binary_classification_decision_returns_expected(session): ) df = session.read_pandas(pd_df) - fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + fpr, tpr, thresholds = metrics.roc_curve( df[["y_true"]], df[["y_score"]], drop_intermediate=False ) @@ -314,7 +306,7 @@ def test_roc_curve_binary_classification_decision_matches_sklearn(session): ) df = session.read_pandas(pd_df) - fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + fpr, tpr, thresholds = metrics.roc_curve( df[["y_true"]], df[["y_score"]], drop_intermediate=False ) expected_fpr, expected_tpr, expected_thresholds = sklearn_metrics.roc_curve( @@ -350,7 +342,7 @@ def test_roc_curve_binary_classification_prediction_series(session): ) df = session.read_pandas(pd_df) - fpr, tpr, thresholds = bigframes.ml.metrics.roc_curve( + fpr, tpr, thresholds = metrics.roc_curve( df["y_true"], df["y_score"], drop_intermediate=False ) @@ -420,7 +412,7 @@ def test_roc_auc_score_returns_expected(session): ) df = session.read_pandas(pd_df) - score = bigframes.ml.metrics.roc_auc_score( + score = metrics.roc_auc_score( df[["y_true_arbitrary_name"]], df[["y_score_arbitrary_name"]] ) @@ -436,7 +428,7 @@ def test_roc_auc_score_returns_matches_sklearn(session): ) df = session.read_pandas(pd_df) - score = bigframes.ml.metrics.roc_auc_score(df[["y_true"]], df[["y_score"]]) + score = metrics.roc_auc_score(df[["y_true"]], df[["y_score"]]) expected_score = sklearn_metrics.roc_auc_score( pd_df[["y_true"]], pd_df[["y_score"]] ) @@ -453,7 +445,7 @@ def test_roc_auc_score_series(session): ) df = session.read_pandas(pd_df) - score = bigframes.ml.metrics.roc_auc_score(df["y_true"], df["y_score"]) + score = metrics.roc_auc_score(df["y_true"], df["y_score"]) assert score == 0.625 @@ -462,33 +454,33 @@ def test_auc_invalid_x_size(session): pd_df = pd.DataFrame({"x_arbitrary_name": [0], "y_arbitrary_name": [0]}) df = session.read_pandas(pd_df) with pytest.raises(ValueError): - bigframes.ml.metrics.auc(df[["x_arbitrary_name"]], df[["y_arbitrary_name"]]) + metrics.auc(df[["x_arbitrary_name"]], df[["y_arbitrary_name"]]) def test_auc_nondecreasing_x(session): pd_df = pd.DataFrame({"x": [0, 0, 0.5, 0.5, 1], "y": [0, 0.5, 0.5, 1, 1]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) == 0.75 + assert metrics.auc(df[["x"]], df[["y"]]) == 0.75 def test_auc_nonincreasing_x(session): pd_df = pd.DataFrame({"x": [0, 0, -0.5, -0.5, -1], "y": [0, 0.5, 0.5, 1, 1]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) == 0.75 + assert metrics.auc(df[["x"]], df[["y"]]) == 0.75 def test_auc_nonincreasing_x_negative(session): pd_df = pd.DataFrame({"x": [0, 0, -0.5, -0.5, -1], "y": [0, -0.5, -0.5, -1, -1]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.auc(df[["x"]], df[["y"]]) == -0.75 + assert metrics.auc(df[["x"]], df[["y"]]) == -0.75 def test_auc_series(session): pd_df = pd.DataFrame({"x": [0, 0, 0.5, 0.5, 1], "y": [0, 0.5, 0.5, 1, 1]}) df = session.read_pandas(pd_df) - assert bigframes.ml.metrics.auc(df["x"], df["y"]) == 0.75 + assert metrics.auc(df["x"], df["y"]) == 0.75 def test_confusion_matrix(session): @@ -499,7 +491,7 @@ def test_confusion_matrix(session): } ).astype("Int64") df = session.read_pandas(pd_df) - confusion_matrix = bigframes.ml.metrics.confusion_matrix( + confusion_matrix = metrics.confusion_matrix( df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]] ) expected_pd_df = pd.DataFrame( @@ -522,9 +514,7 @@ def test_confusion_matrix_column_index(session): } ).astype("Int64") df = session.read_pandas(pd_df) - confusion_matrix = bigframes.ml.metrics.confusion_matrix( - df[["y_true"]], df[["y_pred"]] - ) + confusion_matrix = metrics.confusion_matrix(df[["y_true"]], df[["y_pred"]]) expected_pd_df = ( pd.DataFrame( {1: [1, 0, 1, 0], 2: [0, 0, 2, 0], 3: [0, 0, 0, 0], 4: [0, 1, 0, 1]} @@ -545,9 +535,7 @@ def test_confusion_matrix_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - confusion_matrix = bigframes.ml.metrics.confusion_matrix( - df[["y_true"]], df[["y_pred"]] - ) + confusion_matrix = metrics.confusion_matrix(df[["y_true"]], df[["y_pred"]]) expected_confusion_matrix = sklearn_metrics.confusion_matrix( pd_df[["y_true"]], pd_df[["y_pred"]] ) @@ -565,9 +553,7 @@ def test_confusion_matrix_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - confusion_matrix = bigframes.ml.metrics.confusion_matrix( - df[["y_true"]], df[["y_pred"]] - ) + confusion_matrix = metrics.confusion_matrix(df[["y_true"]], df[["y_pred"]]) expected_confusion_matrix = sklearn_metrics.confusion_matrix( pd_df[["y_true"]], pd_df[["y_pred"]] ) @@ -588,7 +574,7 @@ def test_confusion_matrix_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - confusion_matrix = bigframes.ml.metrics.confusion_matrix(df["y_true"], df["y_pred"]) + confusion_matrix = metrics.confusion_matrix(df["y_true"], df["y_pred"]) expected_pd_df = pd.DataFrame( { 0: [2, 0, 1], @@ -609,7 +595,7 @@ def test_recall_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) - recall = bigframes.ml.metrics.recall_score( + recall = metrics.recall_score( df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [1.000000, 0.000000, 0.666667] @@ -627,9 +613,7 @@ def test_recall_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - recall = bigframes.ml.metrics.recall_score( - df[["y_true"]], df[["y_pred"]], average=None - ) + recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.recall_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -646,9 +630,7 @@ def test_recall_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - recall = bigframes.ml.metrics.recall_score( - df[["y_true"]], df[["y_pred"]], average=None - ) + recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.recall_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -665,7 +647,7 @@ def test_recall_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - recall = bigframes.ml.metrics.recall_score(df["y_true"], df["y_pred"], average=None) + recall = metrics.recall_score(df["y_true"], df["y_pred"], average=None) expected_values = [1.000000, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_recall = pd.Series(expected_values, index=expected_index) @@ -681,7 +663,7 @@ def test_precision_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) - precision_score = bigframes.ml.metrics.precision_score( + precision_score = metrics.precision_score( df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [0.666667, 0.000000, 0.666667] @@ -701,7 +683,7 @@ def test_precision_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - precision_score = bigframes.ml.metrics.precision_score( + precision_score = metrics.precision_score( df[["y_true"]], df[["y_pred"]], average=None ) expected_values = sklearn_metrics.precision_score( @@ -722,7 +704,7 @@ def test_precision_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - precision_score = bigframes.ml.metrics.precision_score( + precision_score = metrics.precision_score( df[["y_true"]], df[["y_pred"]], average=None ) expected_values = sklearn_metrics.precision_score( @@ -743,9 +725,7 @@ def test_precision_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - precision_score = bigframes.ml.metrics.precision_score( - df["y_true"], df["y_pred"], average=None - ) + precision_score = metrics.precision_score(df["y_true"], df["y_pred"], average=None) expected_values = [0.666667, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_precision = pd.Series(expected_values, index=expected_index) @@ -763,7 +743,7 @@ def test_f1_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) - f1_score = bigframes.ml.metrics.f1_score( + f1_score = metrics.f1_score( df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [0.8, 0.000000, 0.666667] @@ -781,9 +761,7 @@ def test_f1_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - f1_score = bigframes.ml.metrics.f1_score( - df[["y_true"]], df[["y_pred"]], average=None - ) + f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.f1_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -800,9 +778,7 @@ def test_f1_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - f1_score = bigframes.ml.metrics.f1_score( - df[["y_true"]], df[["y_pred"]], average=None - ) + f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.f1_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -819,9 +795,16 @@ def test_f1_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - f1_score = bigframes.ml.metrics.f1_score(df["y_true"], df["y_pred"], average=None) + f1_score = metrics.f1_score(df["y_true"], df["y_pred"], average=None) expected_values = [0.8, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_f1 = pd.Series(expected_values, index=expected_index) pd.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + + +def test_mean_squared_error(session: bigframes.Session): + pd_df = pd.DataFrame({"y_true": [3, -0.5, 2, 7], "y_pred": [2.5, 0.0, 2, 8]}) + df = session.read_pandas(pd_df) + mse = metrics.mean_squared_error(df["y_true"], df["y_pred"]) + assert mse == 0.375 diff --git a/third_party/bigframes_vendored/sklearn/metrics/_regression.py b/third_party/bigframes_vendored/sklearn/metrics/_regression.py index be531a9b1cd..c3e579bd299 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_regression.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_regression.py @@ -64,3 +64,30 @@ def r2_score(y_true, y_pred, force_finite=True) -> float: float: The :math:`R^2` score. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +def mean_squared_error(y_true, y_pred) -> float: + """Mean squared error regression loss. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.ml.metrics + >>> bpd.options.display.progress_bar = None + + >>> y_true = bpd.DataFrame([3, -0.5, 2, 7]) + >>> y_pred = bpd.DataFrame([2.5, 0.0, 2, 8]) + >>> mse = bigframes.ml.metrics.mean_squared_error(y_true, y_pred) + >>> mse + 0.375 + + Args: + y_true (Series or DataFrame of shape (n_samples,)): + Ground truth (correct) target values. + y_pred (Series or DataFrame of shape (n_samples,)): + Estimated target values. + + Returns: + float: Mean squared error. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 512e932f360ea67b2912464e566e22c1e35fffe9 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:34:04 -0700 Subject: [PATCH 463/921] refactor: remove to_pandas in ml.metrics.roc_auc_score (#560) * refactor: remove to_pandas in ml.metrics.roc_auc_score * fix mypy --- bigframes/ml/metrics/_metrics.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index 542e6300a8f..b8c264e91b7 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -161,14 +161,10 @@ def roc_auc_score( fpr, tpr, _ = roc_curve(y_true_series, y_score_series, drop_intermediate=False) - # TODO(bmil): remove this once bigframes supports the necessary operations - pd_fpr = fpr.to_pandas() - pd_tpr = tpr.to_pandas() - # Use the trapezoid rule to compute the area under the ROC curve - width_diff = pd_fpr.diff().iloc[1:].reset_index(drop=True) - height_avg = (pd_tpr.iloc[:-1] + pd_tpr.iloc[1:].reset_index(drop=True)) / 2 - return (width_diff * height_avg).sum() + width_diff = fpr.diff().iloc[1:].reset_index(drop=True) + height_avg = (tpr.iloc[:-1] + tpr.iloc[1:].reset_index(drop=True)) / 2 + return typing.cast(float, (width_diff * height_avg).sum()) roc_auc_score.__doc__ = inspect.getdoc(vendored_metrics_ranking.roc_auc_score) From 2fce51f820a18f54c51d25894a0ed02bf53293d2 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:34:32 -0700 Subject: [PATCH 464/921] test: remove transformer test column reorders (#566) --- tests/system/large/ml/test_compose.py | 12 +- tests/system/small/ml/test_preprocessing.py | 163 +++----------------- 2 files changed, 25 insertions(+), 150 deletions(-) diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 72e016f4bb7..0107d371cbf 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -45,14 +45,8 @@ def test_columntransformer_standalone_fit_and_transform( ) result = transformer.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pandas.DataFrame( { - "min_max_scaled_culmen_length_mm": [0.269, 0.232, 0.210], "onehotencoded_species": [ [{"index": 1, "value": 1.0}], [{"index": 1, "value": 1.0}], @@ -63,6 +57,7 @@ def test_columntransformer_standalone_fit_and_transform( -0.9945520581113803, -1.104611490204711, ], + "min_max_scaled_culmen_length_mm": [0.269, 0.232, 0.210], "standard_scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], }, index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), @@ -91,11 +86,6 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] ).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pandas.DataFrame( { "onehotencoded_species": [ diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 040111f38ad..22c3c84959f 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -48,15 +48,10 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): result = scaler.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "standard_scaled_culmen_depth_mm": [0.836148, 0.024748, 0.48116], "standard_scaled_culmen_length_mm": [-0.81112, -0.994552, -1.104611], + "standard_scaled_culmen_depth_mm": [0.836148, 0.024748, 0.48116], "standard_scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], }, dtype="Float64", @@ -77,15 +72,10 @@ def test_standard_scaler_normalizeds_fit_transform(new_penguins_df): for column in result.columns: assert math.isclose(result[column].mean(), 0.0, abs_tol=1e-3) - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "standard_scaled_culmen_depth_mm": [1.17072, -1.272416, 0.101848], "standard_scaled_culmen_length_mm": [1.313249, -0.20198, -1.111118], + "standard_scaled_culmen_depth_mm": [1.17072, -1.272416, 0.101848], "standard_scaled_flipper_length_mm": [1.251089, -1.196588, -0.054338], }, dtype="Float64", @@ -108,11 +98,6 @@ def test_standard_scaler_series_normalizes(penguins_df_default_index, new_pengui result = scaler.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "standard_scaled_culmen_length_mm": [ @@ -162,15 +147,10 @@ def test_max_abs_scaler_normalizes(penguins_df_default_index, new_penguins_df): result = scaler.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "max_abs_scaled_culmen_depth_mm": [0.874419, 0.8, 0.84186], "max_abs_scaled_culmen_length_mm": [0.662752, 0.645973, 0.635906], + "max_abs_scaled_culmen_depth_mm": [0.874419, 0.8, 0.84186], "max_abs_scaled_flipper_length_mm": [0.848485, 0.78355, 0.813853], }, dtype="Float64", @@ -186,15 +166,10 @@ def test_max_abs_scaler_normalizeds_fit_transform(new_penguins_df): new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] ).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "max_abs_scaled_culmen_depth_mm": [1.0, 0.914894, 0.962766], "max_abs_scaled_culmen_length_mm": [1.0, 0.974684, 0.959494], + "max_abs_scaled_culmen_depth_mm": [1.0, 0.914894, 0.962766], "max_abs_scaled_flipper_length_mm": [1.0, 0.923469, 0.959184], }, dtype="Float64", @@ -216,11 +191,6 @@ def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguin result = scaler.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "max_abs_scaled_culmen_length_mm": [0.662752, 0.645973, 0.635906], @@ -251,15 +221,10 @@ def test_min_max_scaler_normalized_fit_transform(new_penguins_df): new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] ).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "min_max_scaled_culmen_depth_mm": [1.0, 0.0, 0.5625], "min_max_scaled_culmen_length_mm": [1.0, 0.375, 0.0], + "min_max_scaled_culmen_depth_mm": [1.0, 0.0, 0.5625], "min_max_scaled_flipper_length_mm": [1.0, 0.0, 0.466667], }, dtype="Float64", @@ -282,11 +247,6 @@ def test_min_max_scaler_series_normalizes(penguins_df_default_index, new_penguin result = scaler.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "min_max_scaled_culmen_length_mm": [0.269091, 0.232727, 0.210909], @@ -320,15 +280,10 @@ def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): result = scaler.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "min_max_scaled_culmen_depth_mm": [0.678571, 0.4880952, 0.595238], "min_max_scaled_culmen_length_mm": [0.269091, 0.232727, 0.210909], + "min_max_scaled_culmen_depth_mm": [0.678571, 0.4880952, 0.595238], "min_max_scaled_flipper_length_mm": [0.40678, 0.152542, 0.271186], }, dtype="Float64", @@ -357,15 +312,10 @@ def test_k_bins_discretizer_normalized_fit_transform_default_params(new_penguins new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] ).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "kbinsdiscretizer_culmen_depth_mm": ["bin_5", "bin_2", "bin_4"], "kbinsdiscretizer_culmen_length_mm": ["bin_5", "bin_3", "bin_2"], + "kbinsdiscretizer_culmen_depth_mm": ["bin_5", "bin_2", "bin_4"], "kbinsdiscretizer_flipper_length_mm": ["bin_5", "bin_2", "bin_4"], }, dtype="string[pyarrow]", @@ -386,11 +336,6 @@ def test_k_bins_discretizer_series_normalizes( ).to_pandas() result = discretizer.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "kbinsdiscretizer_culmen_length_mm": ["bin_3", "bin_3", "bin_3"], @@ -419,15 +364,10 @@ def test_k_bins_discretizer_normalizes(penguins_df_default_index, new_penguins_d result = discretizer.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "kbinsdiscretizer_culmen_depth_mm": ["bin_5", "bin_4", "bin_4"], "kbinsdiscretizer_culmen_length_mm": ["bin_3", "bin_3", "bin_3"], + "kbinsdiscretizer_culmen_depth_mm": ["bin_5", "bin_4", "bin_4"], "kbinsdiscretizer_flipper_length_mm": ["bin_4", "bin_2", "bin_3"], }, dtype="string[pyarrow]", @@ -456,15 +396,10 @@ def test_k_bins_discretizer_normalizes_different_params( result = discretizer.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "kbinsdiscretizer_culmen_depth_mm": ["bin_6", "bin_4", "bin_5"], "kbinsdiscretizer_culmen_length_mm": ["bin_3", "bin_3", "bin_3"], + "kbinsdiscretizer_culmen_depth_mm": ["bin_6", "bin_4", "bin_5"], "kbinsdiscretizer_flipper_length_mm": ["bin_4", "bin_2", "bin_3"], }, dtype="string[pyarrow]", @@ -495,22 +430,17 @@ def test_one_hot_encoder_default_params(new_penguins_df): result = encoder.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "onehotencoded_sex": [ - [{"index": 2, "value": 1.0}], + "onehotencoded_species": [ [{"index": 1, "value": 1.0}], [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], ], - "onehotencoded_species": [ + "onehotencoded_sex": [ + [{"index": 2, "value": 1.0}], [{"index": 1, "value": 1.0}], [{"index": 1, "value": 1.0}], - [{"index": 2, "value": 1.0}], ], }, dtype=ONE_HOT_ENCODED_DTYPE, @@ -525,22 +455,17 @@ def test_one_hot_encoder_default_params_fit_transform(new_penguins_df): result = encoder.fit_transform(new_penguins_df[["species", "sex"]]).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "onehotencoded_sex": [ - [{"index": 2, "value": 1.0}], + "onehotencoded_species": [ [{"index": 1, "value": 1.0}], [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], ], - "onehotencoded_species": [ + "onehotencoded_sex": [ + [{"index": 2, "value": 1.0}], [{"index": 1, "value": 1.0}], [{"index": 1, "value": 1.0}], - [{"index": 2, "value": 1.0}], ], }, dtype=ONE_HOT_ENCODED_DTYPE, @@ -556,11 +481,6 @@ def test_one_hot_encoder_series_default_params(new_penguins_df): result = encoder.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "onehotencoded_species": [ @@ -582,19 +502,14 @@ def test_one_hot_encoder_params(new_penguins_df): result = encoder.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "onehotencoded_sex": [ + "onehotencoded_species": [ [{"index": 0, "value": 1.0}], [{"index": 0, "value": 1.0}], [{"index": 0, "value": 1.0}], ], - "onehotencoded_species": [ + "onehotencoded_sex": [ [{"index": 0, "value": 1.0}], [{"index": 0, "value": 1.0}], [{"index": 0, "value": 1.0}], @@ -613,23 +528,18 @@ def test_one_hot_encoder_different_data(penguins_df_default_index, new_penguins_ result = encoder.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { - "onehotencoded_sex": [ - [{"index": 3, "value": 1.0}], - [{"index": 2, "value": 1.0}], - [{"index": 2, "value": 1.0}], - ], "onehotencoded_species": [ [{"index": 1, "value": 1.0}], [{"index": 1, "value": 1.0}], [{"index": 2, "value": 1.0}], ], + "onehotencoded_sex": [ + [{"index": 3, "value": 1.0}], + [{"index": 2, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], }, dtype=ONE_HOT_ENCODED_DTYPE, index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), @@ -657,11 +567,6 @@ def test_label_encoder_default_params(new_penguins_df): result = encoder.transform(new_penguins_df["species"]).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "labelencoded_species": [ @@ -682,11 +587,6 @@ def test_label_encoder_default_params_fit_transform(new_penguins_df): result = encoder.fit_transform(new_penguins_df[["species"]]).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "labelencoded_species": [ @@ -708,11 +608,6 @@ def test_label_encoder_series_default_params(new_penguins_df): result = encoder.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "labelencoded_species": [ @@ -734,11 +629,6 @@ def test_label_encoder_params(new_penguins_df): result = encoder.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "labelencoded_species": [ @@ -760,11 +650,6 @@ def test_label_encoder_different_data(penguins_df_default_index, new_penguins_df result = encoder.transform(new_penguins_df).to_pandas() - # TODO: bug? feature columns seem to be in nondeterministic random order - # workaround: sort columns by name. Can't repro it in pantheon, so could - # be a bigframes issue... - result = result.reindex(sorted(result.columns), axis=1) - expected = pd.DataFrame( { "labelencoded_species": [ From 8d3918761a17649180aa806d7b01aa103f69b4fe Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:10:17 -0700 Subject: [PATCH 465/921] fix: plot.scatter s parameter cannot accept float-like column (#563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes internal b/330574847 🦕 --- bigframes/operations/_matplotlib/core.py | 19 +++++++++----- .../system/small/operations/test_plotting.py | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index 2c1c2bc4ac7..04534e20a94 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -14,7 +14,6 @@ import abc import typing -import uuid import pandas as pd @@ -115,6 +114,18 @@ def _compute_plot_data(self): if self._is_column_name(c, sample) and sample[c].dtype == dtypes.STRING_DTYPE: sample[c] = sample[c].astype("object") + # To avoid Matplotlib's automatic conversion of `Float64` or `Int64` columns + # to `object` types (which breaks float-like behavior), this code proactively + # converts the column to a compatible format. + s = self.kwargs.get("s", None) + if pd.core.dtypes.common.is_integer(s): + s = self.data.columns[s] + if self._is_column_name(s, sample): + if sample[s].dtype == dtypes.INT_DTYPE: + sample[s] = sample[s].astype("int64") + elif sample[s].dtype == dtypes.FLOAT_DTYPE: + sample[s] = sample[s].astype("float64") + return sample def _is_sequence_arg(self, arg): @@ -130,9 +141,3 @@ def _is_column_name(self, arg, data): and pd.core.dtypes.common.is_hashable(arg) and arg in data.columns ) - - def _generate_new_column_name(self, data): - col_name = None - while col_name is None or col_name in data.columns: - col_name = f"plot_temp_{str(uuid.uuid4())[:8]}" - return col_name diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index 824125adf29..6542ce6de34 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -240,6 +240,32 @@ def test_scatter_args_c(c): ) +@pytest.mark.parametrize( + ("s"), + [ + pytest.param([10, 34, 50], id="int"), + pytest.param([1.0, 3.4, 5.0], id="float"), + pytest.param( + [True, True, False], id="bool", marks=pytest.mark.xfail(raises=ValueError) + ), + ], +) +def test_scatter_args_s(s): + data = { + "a": [1, 2, 3], + "b": [1, 2, 3], + } + data["s"] = s + df = bpd.DataFrame(data) + pd_df = pd.DataFrame(data) + + ax = df.plot.scatter(x="a", y="b", s="s") + pd_ax = pd_df.plot.scatter(x="a", y="b", s="s") + tm.assert_numpy_array_equal( + ax.collections[0].get_sizes(), pd_ax.collections[0].get_sizes() + ) + + @pytest.mark.parametrize( ("arg_name"), [ From f8821fe7ecf8a80532a6aab98044fad601ff939c Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 3 Apr 2024 18:20:18 -0700 Subject: [PATCH 466/921] feat: read_pandas accepts pandas Series and Index objects (#573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- .pre-commit-config.yaml | 2 +- bigframes/pandas/__init__.py | 15 ++++++++++ bigframes/series.py | 2 +- bigframes/session/__init__.py | 44 +++++++++++++++++++++++++++--- tests/system/small/test_session.py | 15 ++++++++++ 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 517176da892..af05f4423c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,4 +38,4 @@ repos: rev: v1.1.1 hooks: - id: mypy - additional_dependencies: [types-requests, types-tabulate] + additional_dependencies: [types-requests, types-tabulate, pandas-stubs] diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index fc008f36e5c..4b0ac4310c6 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -577,7 +577,22 @@ def read_gbq_table( read_gbq_table.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_table) +@typing.overload def read_pandas(pandas_dataframe: pandas.DataFrame) -> bigframes.dataframe.DataFrame: + ... + + +@typing.overload +def read_pandas(pandas_dataframe: pandas.Series) -> bigframes.series.Series: + ... + + +@typing.overload +def read_pandas(pandas_dataframe: pandas.Index) -> bigframes.core.indexes.Index: + ... + + +def read_pandas(pandas_dataframe: Union[pandas.DataFrame, pandas.Series, pandas.Index]): return global_session.with_default_session( bigframes.session.Session.read_pandas, pandas_dataframe, diff --git a/bigframes/series.py b/bigframes/series.py index e7b358c2feb..7e2b0408b78 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1514,7 +1514,7 @@ def map( map_df = map_df.rename(columns={arg.name: self.name}) elif isinstance(arg, Mapping): map_df = bigframes.dataframe.DataFrame( - {"keys": list(arg.keys()), self.name: list(arg.values())}, + {"keys": list(arg.keys()), self.name: list(arg.values())}, # type: ignore session=self._get_block().expr.session, ) map_df = map_df.set_index("keys") diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index ac266da3bda..c7605e89d79 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -95,7 +95,9 @@ # Avoid circular imports. if typing.TYPE_CHECKING: + import bigframes.core.indexes import bigframes.dataframe as dataframe + import bigframes.series _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" @@ -963,7 +965,23 @@ def read_gbq_model(self, model_name: str): model = self.bqclient.get_model(model_ref) return bigframes.ml.loader.from_bq(self, model) + @typing.overload + def read_pandas( + self, pandas_dataframe: pandas.Index + ) -> bigframes.core.indexes.Index: + ... + + @typing.overload + def read_pandas(self, pandas_dataframe: pandas.Series) -> bigframes.series.Series: + ... + + @typing.overload def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame: + ... + + def read_pandas( + self, pandas_dataframe: Union[pandas.DataFrame, pandas.Series, pandas.Index] + ): """Loads DataFrame from a pandas DataFrame. The pandas DataFrame will be persisted as a temporary BigQuery table, which can be @@ -986,13 +1004,31 @@ def read_pandas(self, pandas_dataframe: pandas.DataFrame) -> dataframe.DataFrame [2 rows x 2 columns] Args: - pandas_dataframe (pandas.DataFrame): - a pandas DataFrame object to be loaded. + pandas_dataframe (pandas.DataFrame, pandas.Series, or pandas.Index): + a pandas DataFrame/Series/Index object to be loaded. Returns: - bigframes.dataframe.DataFrame: The BigQuery DataFrame. + An equivalent bigframes.pandas.(DataFrame/Series/Index) object """ - return self._read_pandas(pandas_dataframe, "read_pandas") + import bigframes.series as series + + # Try to handle non-dataframe pandas objects as well + if isinstance(pandas_dataframe, pandas.Series): + bf_df = self._read_pandas(pandas.DataFrame(pandas_dataframe), "read_pandas") + bf_series = typing.cast(series.Series, bf_df[bf_df.columns[0]]) + # wrapping into df can set name to 0 so reset to original object name + bf_series.name = pandas_dataframe.name + return bf_series + if isinstance(pandas_dataframe, pandas.Index): + return self._read_pandas( + pandas.DataFrame(index=pandas_dataframe), "read_pandas" + ).index + if isinstance(pandas_dataframe, pandas.DataFrame): + return self._read_pandas(pandas_dataframe, "read_pandas") + else: + raise ValueError( + f"read_pandas() expects a pandas.DataFrame, but got a {type(pandas_dataframe)}" + ) def _read_pandas( self, pandas_dataframe: pandas.DataFrame, api_name: str diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 28a3f038605..eb6a0a8dd93 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -421,6 +421,21 @@ def test_read_pandas(session, scalars_dfs): pd.testing.assert_frame_equal(result, expected) +def test_read_pandas_series(session): + idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) + pd_series = pd.Series([3, 1, 4, 1, 5], dtype=pd.Int64Dtype(), index=idx) + bf_series = session.read_pandas(pd_series) + + pd.testing.assert_series_equal(bf_series.to_pandas(), pd_series) + + +def test_read_pandas_index(session): + pd_idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) + bf_idx = session.read_pandas(pd_idx) + + pd.testing.assert_index_equal(bf_idx.to_pandas(), pd_idx) + + def test_read_pandas_inline_respects_location(): options = bigframes.BigQueryOptions(location="europe-west1") session = bigframes.Session(options) From 6d8f3afe28d39eb15b969f50d37c58a2c3ff1967 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 4 Apr 2024 09:12:39 -0700 Subject: [PATCH 467/921] =?UTF-8?q?feat:=20Allow=20DataFrame=20binary=20op?= =?UTF-8?q?s=20to=20align=20on=20either=20axis=20and=20with=20loc=E2=80=A6?= =?UTF-8?q?=20(#544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bigframes/core/convert.py | 49 +++++++++++++++ bigframes/dataframe.py | 93 +++++++++++++++++++++------- bigframes/typing.py | 0 tests/system/small/test_dataframe.py | 32 ++++++++++ 4 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 bigframes/core/convert.py create mode 100644 bigframes/typing.py diff --git a/bigframes/core/convert.py b/bigframes/core/convert.py new file mode 100644 index 00000000000..98f854ad72e --- /dev/null +++ b/bigframes/core/convert.py @@ -0,0 +1,49 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import pandas as pd + +import bigframes.core.indexes as index +import bigframes.series as series + + +def to_bf_series(obj, default_index: index.Index) -> series.Series: + if isinstance(obj, series.Series): + return obj + if isinstance(obj, pd.Series): + return series.Series(obj) + if isinstance(obj, index.Index): + return series.Series(obj, default_index) + if isinstance(obj, pd.Index): + return series.Series(obj, default_index) + if pd.api.types.is_list_like(obj): + return series.Series(obj, default_index) + else: + raise TypeError(f"Cannot interpret {obj} as series.") + + +def to_pd_series(obj, default_index: pd.Index) -> pd.Series: + if isinstance(obj, series.Series): + return obj.to_pandas() + if isinstance(obj, pd.Series): + return obj + if isinstance(obj, index.Index): + return pd.Series(obj.to_pandas(), default_index) + if isinstance(obj, pd.Index): + return pd.Series(obj, default_index) + if pd.api.types.is_list_like(obj): + return pd.Series(obj, default_index) + else: + raise TypeError(f"Cannot interpret {obj} as series.") diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 066b0824901..97a100474a1 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -50,6 +50,7 @@ from bigframes.core import log_adapter import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks +import bigframes.core.convert import bigframes.core.expression as ex import bigframes.core.groupby as groupby import bigframes.core.guid @@ -663,22 +664,20 @@ def _apply_binop( how: str = "outer", reverse: bool = False, ): - if isinstance(other, (float, int)): + if isinstance(other, (float, int, bool)): return self._apply_scalar_binop(other, op, reverse=reverse) - elif isinstance(other, indexes.Index): - return self._apply_series_binop( - other.to_series(index=self.index), - op, - axis=axis, - how=how, - reverse=reverse, - ) - elif isinstance(other, bigframes.series.Series): - return self._apply_series_binop( - other, op, axis=axis, how=how, reverse=reverse - ) elif isinstance(other, DataFrame): return self._apply_dataframe_binop(other, op, how=how, reverse=reverse) + elif isinstance(other, pandas.DataFrame): + return self._apply_dataframe_binop( + DataFrame(other), op, how=how, reverse=reverse + ) + elif utils.get_axis_number(axis) == 0: + bf_series = bigframes.core.convert.to_bf_series(other, self.index) + return self._apply_series_binop_axis_0(bf_series, op, how, reverse) + elif utils.get_axis_number(axis) == 1: + pd_series = bigframes.core.convert.to_pd_series(other, self.columns) + return self._apply_series_binop_axis_1(pd_series, op, how, reverse) raise NotImplementedError( f"binary operation is not implemented on the second operand of type {type(other).__name__}." f"{constants.FEEDBACK_LINK}" @@ -700,22 +699,13 @@ def _apply_scalar_binop( block = block.drop_columns([column_id]) return DataFrame(block) - def _apply_series_binop( + def _apply_series_binop_axis_0( self, other: bigframes.series.Series, op: ops.BinaryOp, - axis: str | int = "columns", how: str = "outer", reverse: bool = False, ) -> DataFrame: - if axis not in ("columns", "index", 0, 1): - raise ValueError(f"Invalid input: axis {axis}.") - - if axis in ("columns", 1): - raise NotImplementedError( - f"Row Series operations haven't been supported. {constants.FEEDBACK_LINK}" - ) - block, (get_column_left, get_column_right) = self._block.join( other._block, how=how ) @@ -738,6 +728,63 @@ def _apply_series_binop( block = block.with_index_labels(self.index.names) return DataFrame(block) + def _apply_series_binop_axis_1( + self, + other: pandas.Series, + op: ops.BinaryOp, + how: str = "outer", + reverse: bool = False, + ) -> DataFrame: + # Somewhat different alignment than df-df so separate codepath for now. + if self.columns.equals(other.index): + columns, lcol_indexer, rcol_indexer = self.columns, None, None + else: + if not (self.columns.is_unique and other.index.is_unique): + raise ValueError("Cannot align non-unique indices") + columns, lcol_indexer, rcol_indexer = self.columns.join( + other.index, how=how, return_indexers=True + ) + + binop_result_ids = [] + + column_indices = zip( + lcol_indexer if (lcol_indexer is not None) else range(len(columns)), + rcol_indexer if (rcol_indexer is not None) else range(len(columns)), + ) + + block = self._block + for left_index, right_index in column_indices: + if left_index >= 0 and right_index >= 0: # -1 indices indicate missing + self_col_id = self._block.value_columns[left_index] + other_scalar = other.iloc[right_index] + expr = ( + op.as_expr(ex.const(other_scalar), self_col_id) + if reverse + else op.as_expr(self_col_id, ex.const(other_scalar)) + ) + elif left_index >= 0: + self_col_id = self._block.value_columns[left_index] + expr = ( + op.as_expr(ex.const(None), self_col_id) + if reverse + else op.as_expr(self_col_id, ex.const(None)) + ) + elif right_index >= 0: + other_scalar = other.iloc[right_index] + expr = ( + op.as_expr(ex.const(other_scalar), ex.const(None)) + if reverse + else op.as_expr(ex.const(None), ex.const(other_scalar)) + ) + else: + # Should not be possible + raise ValueError("No right or left index.") + block, result_col_id = block.project_expr(expr) + binop_result_ids.append(result_col_id) + + block = block.select_columns(binop_result_ids) + return DataFrame(block.with_column_labels(columns)) + def _apply_dataframe_binop( self, other: DataFrame, diff --git a/bigframes/typing.py b/bigframes/typing.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index cf907b02d68..ae80a088b5b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -27,6 +27,7 @@ import bigframes import bigframes._config.display_options as display_options +import bigframes.core.indexes as bf_indexes import bigframes.dataframe as dataframe import bigframes.series as series from tests.system.utils import ( @@ -2074,6 +2075,37 @@ def test_series_binop_axis_index( assert_pandas_df_equal(bf_result, pd_result) +@skip_legacy_pandas +@pytest.mark.parametrize( + ("input"), + [ + ((1000, 2000, 3000)), + (pd.Index([1000, 2000, 3000])), + (bf_indexes.Index([1000, 2000, 3000])), + (pd.Series((1000, 2000), index=["int64_too", "float64_col"])), + (series.Series((1000, 2000), index=["int64_too", "float64_col"])), + ], + ids=[ + "tuple", + "pd_index", + "bf_index", + "pd_series", + "bf_series", + ], +) +def test_listlike_binop_axis_1(scalars_dfs, input): + scalars_df, scalars_pandas_df = scalars_dfs + + df_columns = ["int64_col", "float64_col", "int64_too"] + + bf_result = scalars_df[df_columns].add(input, axis=1).to_pandas() + if hasattr(input, "to_pandas"): + input = input.to_pandas() + pd_result = scalars_pandas_df[df_columns].add(input, axis=1) + + assert_pandas_df_equal(bf_result, pd_result, check_dtype=False) + + @pytest.mark.parametrize( ("left_labels", "right_labels"), [ From 659a161a53e93f66334cd04d1c3dc1f1f47ecc16 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 4 Apr 2024 09:36:41 -0700 Subject: [PATCH 468/921] fix: Use bytes limit on frame inlining rather than element count (#576) --- bigframes/session/__init__.py | 8 ++++---- tests/system/small/test_dataframe.py | 7 +++++++ tests/system/small/test_progress_bar.py | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index c7605e89d79..671a3d65e73 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -116,9 +116,9 @@ "UTF-32LE", } -# BigQuery has 1 MB query size limit, 5000 items shouldn't take more than 10% of this depending on data type. -# TODO(tbergeron): Convert to bytes-based limit -MAX_INLINE_DF_SIZE = 5000 +# BigQuery has 1 MB query size limit. Don't want to take up more than a few % of that inlining a table. +# Also must assume that text encoding as literals is much less efficient than in-memory representation. +MAX_INLINE_DF_BYTES = 5000 logger = logging.getLogger(__name__) @@ -1051,7 +1051,7 @@ def _read_pandas_inline( ) -> Optional[dataframe.DataFrame]: import bigframes.dataframe as dataframe - if pandas_dataframe.size > MAX_INLINE_DF_SIZE: + if pandas_dataframe.memory_usage(deep=True).sum() > MAX_INLINE_DF_BYTES: return None try: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ae80a088b5b..f28de37d68c 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -66,6 +66,13 @@ def test_df_construct_pandas_default(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result) +def test_df_construct_large_strings(): + data = [["hello", "w" + "o" * 50000 + "rld"]] + bf_result = dataframe.DataFrame(data).to_pandas() + pd_result = pd.DataFrame(data, dtype=pd.StringDtype(storage="pyarrow")) + pandas.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + def test_df_construct_pandas_load_job(scalars_dfs): # This should trigger the inlined codepath columns = [ diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index 1c04b580fc1..ea139b9802a 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -20,7 +20,7 @@ import bigframes as bf import bigframes.formatting_helpers as formatting_helpers -from bigframes.session import MAX_INLINE_DF_SIZE +from bigframes.session import MAX_INLINE_DF_BYTES job_load_message_regex = r"\w+ job [\w-]+ is \w+\." @@ -70,7 +70,7 @@ def test_progress_bar_load_jobs( ): # repeat the DF to be big enough to trigger the load job. df = penguins_pandas_df_default_index - while len(df) < MAX_INLINE_DF_SIZE: + while len(df) < MAX_INLINE_DF_BYTES: df = pd.DataFrame(np.repeat(df.values, 2, axis=0)) bf.options.display.progress_bar = "terminal" From 9e32f570b42c8ddae0c9b281b25beff91f0c922c Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:42:36 -0700 Subject: [PATCH 469/921] feat: (Series|DataFrame).explode (#556) * feat: (Series|DataFrame).explode * fixing schema and adding tests * fixing multi-index tests * add docs and fix tests --- bigframes/core/__init__.py | 9 ++ bigframes/core/blocks.py | 30 +++++ bigframes/core/compile/compiled.py | 118 ++++++++++++++++++ bigframes/core/compile/compiler.py | 5 + bigframes/core/nodes.py | 27 ++++ bigframes/dataframe.py | 30 +++++ bigframes/dtypes.py | 15 ++- bigframes/operations/__init__.py | 2 +- bigframes/series.py | 7 ++ tests/system/small/test_dataframe.py | 71 +++++++++++ tests/system/small/test_multiindex.py | 17 +++ tests/system/small/test_series.py | 101 +++++++++++++++ .../ibis/backends/bigquery/registry.py | 6 + .../ibis/expr/operations/__init__.py | 1 + .../ibis/expr/operations/generic.py | 9 ++ .../bigframes_vendored/pandas/core/frame.py | 51 ++++++++ .../bigframes_vendored/pandas/core/series.py | 30 ++++- 17 files changed, 523 insertions(+), 6 deletions(-) create mode 100644 third_party/bigframes_vendored/ibis/expr/operations/generic.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index ce9c22132ba..9358dab1b1d 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -401,6 +401,15 @@ def join( return ArrayValue(bigframes.core.rewrite.maybe_rewrite_join(join_node)) return ArrayValue(join_node) + def explode(self, column_ids: typing.Sequence[str]) -> ArrayValue: + assert len(column_ids) > 0 + for column_id in column_ids: + assert bigframes.dtypes.is_array_like(self.get_column_type(column_id)) + + return ArrayValue( + nodes.ExplodeNode(child=self.node, column_ids=tuple(column_ids)) + ) + def _uniform_sampling(self, fraction: float) -> ArrayValue: """Sampling the table on given fraction. diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 04a98ac9a45..0b6e50cfa3e 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1162,6 +1162,36 @@ def calculate_pairwise_metric(self, op=agg_ops.CorrOp()): index_labels=self.column_labels.names, ) + def explode( + self, + column_ids: typing.Sequence[str], + ignore_index: Optional[bool], + ) -> Block: + column_ids = [ + column_id + for column_id in column_ids + if bigframes.dtypes.is_array_like(self.expr.get_column_type(column_id)) + ] + if len(column_ids) == 0: + expr = self.expr + else: + expr = self.expr.explode(column_ids) + + if ignore_index: + return Block( + expr.drop_columns(self.index_columns), + column_labels=self.column_labels, + # Initiates default index creation using the block constructor. + index_columns=[], + ) + else: + return Block( + expr, + column_labels=self.column_labels, + index_columns=self.index_columns, + index_labels=self.column_labels.names, + ) + def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.UnaryAggregateOp]: """ Gets a standard set of stats to preemptively fetch for a column if diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index af2d69275aa..f1c5d620106 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -20,6 +20,7 @@ import typing from typing import Collection, Iterable, Literal, Optional, Sequence +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis import ibis.backends.bigquery as ibis_bigquery import ibis.common.deferred # type: ignore @@ -502,6 +503,51 @@ def _uniform_sampling(self, fraction: float) -> UnorderedIR: columns=columns, ) + def explode(self, column_ids: typing.Sequence[str]) -> UnorderedIR: + table = self._to_ibis_expr() + + # The offset array ensures null represents empty arrays after unnesting. + offset_array_id = bigframes.core.guid.generate_guid("offset_array_") + offset_array = ( + vendored_ibis_ops.GenerateArray( + ibis.greatest( + 0, + ibis.least( + *[table[column_id].length() - 1 for column_id in column_ids] + ), + ) + ) + .to_expr() + .name(offset_array_id), + ) + table_w_offset_array = table.select( + offset_array, + *self._column_names, + ) + + unnest_offset_id = bigframes.core.guid.generate_guid("unnest_offset_") + unnest_offset = ( + table_w_offset_array[offset_array_id].unnest().name(unnest_offset_id) + ) + table_w_offset = table_w_offset_array.select( + unnest_offset, + *self._column_names, + ) + + unnested_columns = [ + table_w_offset[column_id][table_w_offset[unnest_offset_id]].name(column_id) + if column_id in column_ids + else table_w_offset[column_id] + for column_id in self._column_names + ] + table_w_unnest = table_w_offset.select(*unnested_columns) + + columns = [table_w_unnest[column_name] for column_name in self._column_names] + return UnorderedIR( + table_w_unnest, + columns=columns, + ) + ## Helpers def _set_or_replace_by_id( self, id: str, new_value: ibis_types.Value @@ -719,6 +765,78 @@ def _uniform_sampling(self, fraction: float) -> OrderedIR: ordering=self._ordering, ) + def explode(self, column_ids: typing.Sequence[str]) -> OrderedIR: + table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) + + offset_array_id = bigframes.core.guid.generate_guid("offset_array_") + offset_array = ( + vendored_ibis_ops.GenerateArray( + ibis.greatest( + 0, + ibis.least( + *[table[column_id].length() - 1 for column_id in column_ids] + ), + ) + ) + .to_expr() + .name(offset_array_id), + ) + table_w_offset_array = table.select( + offset_array, + *self._column_names, + *self._hidden_ordering_column_names, + ) + + unnest_offset_id = bigframes.core.guid.generate_guid("unnest_offset_") + unnest_offset = ( + table_w_offset_array[offset_array_id].unnest().name(unnest_offset_id) + ) + table_w_offset = table_w_offset_array.select( + unnest_offset, + *self._column_names, + *self._hidden_ordering_column_names, + ) + + unnested_columns = [ + table_w_offset[column_id][table_w_offset[unnest_offset_id]].name(column_id) + if column_id in column_ids + else table_w_offset[column_id] + for column_id in self._column_names + ] + + table_w_unnest = table_w_offset.select( + table_w_offset[unnest_offset_id], + *unnested_columns, + *self._hidden_ordering_column_names, + ) + + columns = [table_w_unnest[column_name] for column_name in self._column_names] + hidden_ordering_columns = [ + *[ + table_w_unnest[column_name] + for column_name in self._hidden_ordering_column_names + ], + table_w_unnest[unnest_offset_id], + ] + ordering = ExpressionOrdering( + ordering_value_columns=tuple( + [ + *self._ordering.ordering_value_columns, + ascending_over(unnest_offset_id), + ] + ), + total_ordering_columns=frozenset( + [*self._ordering.total_ordering_columns, unnest_offset_id] + ), + ) + + return OrderedIR( + table_w_unnest, + columns=columns, + hidden_ordering_columns=hidden_ordering_columns, + ordering=ordering, + ) + def promote_offsets(self, col_id: str) -> OrderedIR: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 6f10d85f31c..638e3eacdd0 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -191,6 +191,11 @@ def compile_unpivot(node: nodes.UnpivotNode, ordered: bool = True): ) +@_compile_node.register +def compiler_explode(node: nodes.ExplodeNode, ordered: bool = True): + return compile_node(node.child, ordered).explode(node.column_ids) + + @_compile_node.register def compiler_random_sample(node: nodes.RandomSampleNode, ordered: bool = True): return compile_node(node.child, ordered)._uniform_sampling(node.fraction) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 8f646ac4bb2..d740605a56f 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -484,3 +484,30 @@ def row_preserving(self) -> bool: def __hash__(self): return self._node_hash + + +@dataclass(frozen=True) +class ExplodeNode(UnaryNode): + column_ids: typing.Tuple[str, ...] + + @property + def row_preserving(self) -> bool: + return False + + def __hash__(self): + return self._node_hash + + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + items = tuple( + schemata.SchemaItem( + name, + bigframes.dtypes.arrow_dtype_to_bigframes_dtype( + self.child.schema.get_type(name).pyarrow_dtype.value_type + ), + ) + if name in self.column_ids + else schemata.SchemaItem(name, self.child.schema.get_type(name)) + for name in self.child.schema.names + ) + return schemata.ArraySchema(items) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 97a100474a1..0bb88beb2bc 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2579,6 +2579,36 @@ def sample( )[0] ) + def explode( + self, + column: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], + *, + ignore_index: Optional[bool] = False, + ) -> DataFrame: + if not utils.is_list_like(column): + column_labels = typing.cast(typing.Sequence[blocks.Label], (column,)) + else: + column_labels = typing.cast(typing.Sequence[blocks.Label], tuple(column)) + + if not column_labels: + raise ValueError("column must be nonempty") + if len(column_labels) > len(set(column_labels)): + raise ValueError("column must be unique") + + column_ids = [self._resolve_label_exact(label) for label in column_labels] + missing = [ + column_labels[i] for i in range(len(column_ids)) if column_ids[i] is None + ] + if len(missing) > 0: + raise KeyError(f"None of {missing} are in the columns") + + return DataFrame( + self._block.explode( + column_ids=typing.cast(typing.Sequence[str], tuple(column_ids)), + ignore_index=ignore_index, + ) + ) + def _split( self, ns: Iterable[int] = (), diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 3d8c06d1887..c5bf5db2fe0 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -129,16 +129,19 @@ def is_string_like(type: ExpressionType) -> bool: def is_array_like(type: ExpressionType) -> bool: - if isinstance(type, pd.ArrowDtype) and isinstance(type.pyarrow_dtype, pa.ListType): - return True - else: - return type in (STRING_DTYPE, BYTES_DTYPE) + return isinstance(type, pd.ArrowDtype) and isinstance( + type.pyarrow_dtype, pa.ListType + ) def is_numeric(type: ExpressionType) -> bool: return type in NUMERIC_BIGFRAMES_TYPES_PERMISSIVE +def is_iterable(type: ExpressionType) -> bool: + return type in (STRING_DTYPE, BYTES_DTYPE) or is_array_like(type) + + def is_comparable(type: ExpressionType) -> bool: return (type is not None) and (type not in UNORDERED_DTYPES) @@ -348,6 +351,10 @@ def arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType: ) +def arrow_dtype_to_bigframes_dtype(arrow_dtype: pa.DataType) -> Dtype: + return ibis_dtype_to_bigframes_dtype(arrow_dtype_to_ibis_dtype(arrow_dtype)) + + def bigframes_dtype_to_ibis_dtype( bigframes_dtype: Union[DtypeString, Dtype, np.dtype[Any]] ) -> ibis_dtypes.DataType: diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 0dcc6432383..d631ba85081 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -212,7 +212,7 @@ def create_binary_op( len_op = create_unary_op( name="len", type_signature=op_typing.FixedOutputType( - dtypes.is_array_like, dtypes.INT_DTYPE, description="array-like" + dtypes.is_iterable, dtypes.INT_DTYPE, description="iterable" ), ) reverse_op = create_unary_op(name="reverse", type_signature=op_typing.STRING_TRANSFORM) diff --git a/bigframes/series.py b/bigframes/series.py index 7e2b0408b78..42264c35b69 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1547,6 +1547,13 @@ def sample( )[0] ) + def explode(self, *, ignore_index: Optional[bool] = False) -> Series: + return Series( + self._block.explode( + column_ids=[self._value_column], ignore_index=ignore_index + ) + ) + def __array_ufunc__( self, ufunc: numpy.ufunc, method: str, *inputs, **kwargs ) -> Series: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index f28de37d68c..8bcdfe168b6 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -20,6 +20,7 @@ from typing import Tuple import geopandas as gpd # type: ignore +import numpy as np import pandas as pd import pandas.testing import pyarrow as pa # type: ignore @@ -29,6 +30,7 @@ import bigframes._config.display_options as display_options import bigframes.core.indexes as bf_indexes import bigframes.dataframe as dataframe +import bigframes.pandas as bpd import bigframes.series as series from tests.system.utils import ( assert_pandas_df_equal, @@ -4167,3 +4169,72 @@ def test_to_gbq_and_create_dataset(session, scalars_df_index, dataset_id_not_cre loaded_scalars_df_index = session.read_gbq(result_table) assert not loaded_scalars_df_index.empty + + +@pytest.mark.parametrize( + ("col_names", "ignore_index"), + [ + pytest.param(["A"], False, id="one_array_false"), + pytest.param(["A"], True, id="one_array_true"), + pytest.param(["B"], False, id="one_float_false"), + pytest.param(["B"], True, id="one_float_true"), + pytest.param(["A", "C"], False, id="two_arrays_false"), + pytest.param(["A", "C"], True, id="two_arrays_true"), + ], +) +def test_dataframe_explode(col_names, ignore_index): + data = { + "A": [[0, 1, 2], [], [3, 4]], + "B": 3, + "C": [["a", "b", "c"], np.nan, ["d", "e"]], + } + df = bpd.DataFrame(data) + pd_df = df.to_pandas() + pd.testing.assert_frame_equal( + df.explode(col_names, ignore_index=ignore_index).to_pandas(), + pd_df.explode(col_names, ignore_index=ignore_index), + check_index_type=False, + check_dtype=False, + ) + + +@pytest.mark.parametrize( + ("ignore_index", "ordered"), + [ + pytest.param(True, True, id="include_index_ordered"), + pytest.param(True, False, id="include_index_unordered"), + pytest.param(False, True, id="ignore_index_ordered"), + ], +) +def test_dataframe_explode_reserve_order(ignore_index, ordered): + data = { + "a": [np.random.randint(0, 10, 10) for _ in range(10)], + "b": [np.random.randint(0, 10, 10) for _ in range(10)], + } + df = bpd.DataFrame(data) + pd_df = pd.DataFrame(data) + + res = df.explode(["a", "b"], ignore_index=ignore_index).to_pandas(ordered=ordered) + pd_res = pd_df.explode(["a", "b"], ignore_index=ignore_index).astype( + pd.Int64Dtype() + ) + pd.testing.assert_frame_equal( + res if ordered else res.sort_index(), + pd_res, + check_index_type=False, + ) + + +@pytest.mark.parametrize( + ("col_names"), + [ + pytest.param([], id="empty", marks=pytest.mark.xfail(raises=ValueError)), + pytest.param( + ["A", "A"], id="duplicate", marks=pytest.mark.xfail(raises=ValueError) + ), + pytest.param("unknown", id="unknown", marks=pytest.mark.xfail(raises=KeyError)), + ], +) +def test_dataframe_explode_xfail(col_names): + df = bpd.DataFrame({"A": [[0, 1, 2], [], [3, 4]]}) + df.explode(col_names) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index d585d4f73e3..6aca7628cfd 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import numpy as np import pandas import pytest @@ -1168,3 +1169,19 @@ def test_column_multi_index_dot_not_supported(): NotImplementedError, match="Multi-level column input is not supported" ): bf1 @ bf2 + + +def test_explode_w_multi_index(): + data = [[[1, 1], np.nan, [3, 3]], [[2], [5], []]] + multi_level_columns = pandas.MultiIndex.from_arrays( + [["col0", "col0", "col1"], ["col00", "col01", "col11"]] + ) + + df = bpd.DataFrame(data, columns=multi_level_columns) + pd_df = df.to_pandas() + pandas.testing.assert_frame_equal( + df["col0"].explode("col00").to_pandas(), + pd_df["col0"].explode("col00"), + check_dtype=False, + check_index_type=False, + ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 794ab6b7a29..e15dbc6a3f9 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -3416,3 +3416,104 @@ def foo(x: int, y: int, df): ) assert_series_equal(bf_result, pd_result) + + +@pytest.mark.parametrize( + ("data"), + [ + pytest.param([1, 2, 3], id="int"), + pytest.param([[1, 2, 3], [], numpy.nan, [3, 4]], id="int_array"), + pytest.param( + [["A", "AA", "AAA"], ["BB", "B"], numpy.nan, [], ["C"]], id="string_array" + ), + pytest.param( + [ + {"A": {"x": 1.0}, "B": "b"}, + {"A": {"y": 2.0}, "B": "bb"}, + {"A": {"z": 4.0}}, + {}, + numpy.nan, + ], + id="struct_array", + ), + ], +) +def test_series_explode(data): + data = [[1, 2, 3], [], numpy.nan, [3, 4]] + s = bigframes.pandas.Series(data) + pd_s = pd.Series(data) + pd.testing.assert_series_equal( + s.explode().to_pandas(), + pd_s.explode(), + check_index_type=False, + check_dtype=False, + ) + + +@pytest.mark.parametrize( + ("index", "ignore_index"), + [ + pytest.param(None, True, id="default_index"), + pytest.param(None, False, id="ignore_default_index"), + pytest.param([5, 1, 3, 2], True, id="unordered_index"), + pytest.param([5, 1, 3, 2], False, id="ignore_unordered_index"), + pytest.param(["z", "x", "a", "b"], True, id="str_index"), + pytest.param(["z", "x", "a", "b"], False, id="ignore_str_index"), + ], +) +def test_series_explode_w_index(index, ignore_index): + data = [[], [200.0, 23.12], [4.5, -9.0], [1.0]] + s = bigframes.pandas.Series(data, index=index) + pd_s = pd.Series(data, index=index) + pd.testing.assert_series_equal( + s.explode(ignore_index=ignore_index).to_pandas(), + pd_s.explode(ignore_index=ignore_index).astype(pd.Float64Dtype()), + check_index_type=False, + ) + + +@pytest.mark.parametrize( + ("ignore_index", "ordered"), + [ + pytest.param(True, True, id="include_index_ordered"), + pytest.param(True, False, id="include_index_unordered"), + pytest.param(False, True, id="ignore_index_ordered"), + ], +) +def test_series_explode_reserve_order(ignore_index, ordered): + data = [numpy.random.randint(0, 10, 10) for _ in range(10)] + s = bigframes.pandas.Series(data) + pd_s = pd.Series(data) + + res = s.explode(ignore_index=ignore_index).to_pandas(ordered=ordered) + pd_res = pd_s.explode(ignore_index=ignore_index).astype(pd.Int64Dtype()) + pd.testing.assert_series_equal( + res if ordered else res.sort_index(), + pd_res, + check_index_type=False, + ) + + +def test_series_explode_w_aggregate(): + data = [[1, 2, 3], [], numpy.nan, [3, 4]] + s = bigframes.pandas.Series(data) + pd_s = pd.Series(data) + assert s.explode().sum() == pd_s.explode().sum() + + +@pytest.mark.parametrize( + ("data"), + [ + pytest.param(numpy.nan, id="null"), + pytest.param([numpy.nan], id="null_array"), + pytest.param([[]], id="empty_array"), + pytest.param([numpy.nan, []], id="null_and_empty_array"), + ], +) +def test_series_explode_null(data): + s = bigframes.pandas.Series(data) + pd.testing.assert_series_equal( + s.explode().to_pandas(), + s.to_pandas().explode(), + check_dtype=False, + ) diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py index 3f89feaa34e..88826b31cee 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -26,11 +26,17 @@ def _to_json_string(translator, op: vendored_ibis_ops.ToJsonString): return f"TO_JSON_STRING({arg})" +def _generate_array(translator, op: vendored_ibis_ops.GenerateArray): + arg = translator.translate(op.arg) + return f"GENERATE_ARRAY(0, {arg})" + + patched_ops = { vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, # type:ignore vendored_ibis_ops.FirstNonNullValue: _first_non_null_value, # type:ignore vendored_ibis_ops.LastNonNullValue: _last_non_null_value, # type:ignore vendored_ibis_ops.ToJsonString: _to_json_string, # type:ignore + vendored_ibis_ops.GenerateArray: _generate_array, # type:ignore } OPERATION_REGISTRY.update(patched_ops) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py index 2c2efe528db..3d5a5a7fa0a 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py @@ -2,5 +2,6 @@ from __future__ import annotations from bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F401 F403 +from bigframes_vendored.ibis.expr.operations.generic import * # noqa: F401 F403 from bigframes_vendored.ibis.expr.operations.json import * # noqa: F401 F403 from bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F401 F403 diff --git a/third_party/bigframes_vendored/ibis/expr/operations/generic.py b/third_party/bigframes_vendored/ibis/expr/operations/generic.py new file mode 100644 index 00000000000..82d0a13371c --- /dev/null +++ b/third_party/bigframes_vendored/ibis/expr/operations/generic.py @@ -0,0 +1,9 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/generic.py +from __future__ import annotations + +import ibis.expr.datatypes as dt +from ibis.expr.operations.core import Unary + + +class GenerateArray(Unary): + dtype = dt.Array(dt.int64) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 3ae5b0db2aa..e5aa47ad3e1 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2804,6 +2804,57 @@ def combine_first(self, other) -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def explode( + self, column: Union[str, Sequence[str]], *, ignore_index: Optional[bool] = False + ) -> DataFrame: + """ + Transform each element of an array to a row, replicating index values. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'A': [[0, 1, 2], [], [], [3, 4]], + ... 'B': 1, + ... 'C': [['a', 'b', 'c'], np.nan, [], ['d', 'e']]}) + >>> df.explode('A') + A B C + 0 0 1 ['a' 'b' 'c'] + 0 1 1 ['a' 'b' 'c'] + 0 2 1 ['a' 'b' 'c'] + 1 1 [] + 2 1 [] + 3 3 1 ['d' 'e'] + 3 4 1 ['d' 'e'] + + [7 rows x 3 columns] + >>> df.explode(list('AC')) + A B C + 0 0 1 a + 0 1 1 b + 0 2 1 c + 1 1 + 2 1 + 3 3 1 d + 3 4 1 e + + [7 rows x 3 columns] + + Args: + column (str, Sequence[str]): + Column(s) to explode. For multiple columns, specify a non-empty list + with each element be str or tuple, and all specified columns their + list-like data on same row of the frame must have matching length. + ignore_index (bool, default False): + If True, the resulting index will be labeled 0, 1, …, n - 1. + + Returns: + bigframes.series.DataFrame: Exploded lists to rows of the subset columns; + index will be duplicated for these rows. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def corr(self, method, min_periods, numeric_only) -> DataFrame: """ Compute pairwise correlation of columns, excluding NA/null values. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 89b39cf8a04..785755a5620 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3,7 +3,7 @@ """ from __future__ import annotations -from typing import Hashable, IO, Literal, Mapping, Sequence, TYPE_CHECKING +from typing import Hashable, IO, Literal, Mapping, Optional, Sequence, TYPE_CHECKING from bigframes_vendored.pandas.core.generic import NDFrame import numpy as np @@ -751,6 +751,34 @@ def round(self, decimals: int = 0) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def explode(self, *, ignore_index: Optional[bool] = False) -> Series: + """ + Transform each element of a list-like to a row. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([[1, 2, 3], [], [3, 4]]) + >>> s.explode() + 0 1 + 0 2 + 0 3 + 1 + 2 3 + 2 4 + dtype: Int64 + + Args: + ignore_index (bool, default False): + If True, the resulting index will be labeled 0, 1, …, n - 1. + + Returns: + bigframes.series.Series: Exploded lists to rows; index will be duplicated for these rows. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def corr(self, other, method="pearson", min_periods=None) -> float: """ Compute the correlation with the other Series. Non-number values are ignored in the From 098d4443807ec1229f69760bda1dd7bf6af488aa Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:05:32 -0700 Subject: [PATCH 470/921] chore: remove outdated notebook (#567) --- .../vertex_sdk/sdk2_bigframes_pytorch.ipynb | 723 ----------------- .../vertex_sdk/sdk2_bigframes_sklearn.ipynb | 727 ------------------ .../sdk2_bigframes_tensorflow.ipynb | 646 ---------------- 3 files changed, 2096 deletions(-) delete mode 100644 notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb delete mode 100644 notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb delete mode 100644 notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb diff --git a/notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb b/notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb deleted file mode 100644 index 598d958f0c3..00000000000 --- a/notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb +++ /dev/null @@ -1,723 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ur8xi4C7S06n" - }, - "outputs": [], - "source": [ - "# Copyright 2023 Google LLC\n", - "#\n", - "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", - "# you may not use this file except in compliance with the License.\n", - "# You may obtain a copy of the License at\n", - "#\n", - "# https://www.apache.org/licenses/LICENSE-2.0\n", - "#\n", - "# Unless required by applicable law or agreed to in writing, software\n", - "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", - "# See the License for the specific language governing permissions and\n", - "# limitations under the License." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "JAPoU8Sm5E6e" - }, - "source": [ - "# Train a pytorch model with Vertex AI SDK 2.0 and Bigframes\n", - "\n", - "\n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"VertexOpen in Vertex AI Workbench\n", - " \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tvgnzT1CKxrO" - }, - "source": [ - "## Overview\n", - "\n", - "This tutorial demonstrates how to train a pytorch model using Vertex AI local-to-remote training with Vertex AI SDK 2.0 and BigQuery Bigframes as the data source.\n", - "\n", - "Learn more about [bigframes](https://cloud.google.com/bigquery/docs/)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d975e698c9a4" - }, - "source": [ - "### Objective\n", - "\n", - "In this tutorial, you learn to use `Vertex AI SDK 2.0` with Bigframes as input data source.\n", - "\n", - "\n", - "This tutorial uses the following Google Cloud ML services:\n", - "\n", - "- `Vertex AI Training`\n", - "- `Vertex AI Remote Training`\n", - "\n", - "\n", - "The steps performed include:\n", - "\n", - "- Initialize a dataframe from a BigQuery table and split the dataset\n", - "- Perform transformations as a Vertex AI remote training.\n", - "- Train the model remotely and evaluate the model locally\n", - "\n", - "**Local-to-remote training**\n", - "\n", - "```\n", - "import vertexai\n", - "from my_module import MyModelClass\n", - "\n", - "vertexai.preview.init(remote=True, project=\"my-project\", location=\"my-location\", staging_bucket=\"gs://my-bucket\")\n", - "\n", - "# Wrap the model class with `vertex_ai.preview.remote`\n", - "MyModelClass = vertexai.preview.remote(MyModelClass)\n", - "\n", - "# Instantiate the class\n", - "model = MyModelClass(...)\n", - "\n", - "# Optional set remote config\n", - "model.fit.vertex.remote_config.display_name = \"MyModelClass-remote-training\"\n", - "model.fit.vertex.remote_config.staging_bucket = \"gs://my-bucket\"\n", - "\n", - "# This `fit` call will be executed remotely\n", - "model.fit(...)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "08d289fa873f" - }, - "source": [ - "### Dataset\n", - "\n", - "This tutorial uses the IRIS dataset, which predicts the iris species." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "aed92deeb4a0" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* Vertex AI\n", - "* BigQuery\n", - "* Cloud Storage\n", - "\n", - "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing),\n", - "[BigQuery pricing](https://cloud.google.com/bigquery/pricing),\n", - "and [Cloud Storage pricing](https://cloud.google.com/storage/pricing), \n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i7EUnXsZhAGF" - }, - "source": [ - "## Installation\n", - "\n", - "Install the following packages required to execute this notebook. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "2b4ef9b72d43" - }, - "outputs": [], - "source": [ - "# Install the packages\n", - "! pip3 install --upgrade --quiet google-cloud-aiplatform[preview]\n", - "! pip3 install --upgrade --quiet bigframes\n", - "! pip3 install --upgrade --quiet torch" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "58707a750154" - }, - "source": [ - "### Colab only: Uncomment the following cell to restart the kernel." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "f200f10a1da3" - }, - "outputs": [], - "source": [ - "# Automatically restart kernel after installs so that your environment can access the new packages\n", - "# import IPython\n", - "\n", - "# app = IPython.Application.instance()\n", - "# app.kernel.do_shutdown(True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BF1j6f9HApxa" - }, - "source": [ - "## Before you begin\n", - "\n", - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", - "\n", - "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WReHDGG5g0XY" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "**If you don't know your project ID**, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "oM1iC_MfAts1" - }, - "outputs": [], - "source": [ - "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", - "\n", - "# Set the project id\n", - "! gcloud config set project {PROJECT_ID}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "region" - }, - "source": [ - "#### Region\n", - "\n", - "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "region" - }, - "outputs": [], - "source": [ - "REGION = \"us-central1\" # @param {type: \"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sBCra4QMA2wR" - }, - "source": [ - "### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "74ccc9e52986" - }, - "source": [ - "**1. Vertex AI Workbench**\n", - "* Do nothing as you are already authenticated." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "de775a3773ba" - }, - "source": [ - "**2. Local JupyterLab instance, uncomment and run:**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "254614fa0c46" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ef21552ccea8" - }, - "source": [ - "**3. Colab, uncomment and run:**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "603adbbf0532" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "f6b2ccc891ed" - }, - "source": [ - "**4. Service account or other**\n", - "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "zgPO1eR3CYjk" - }, - "source": [ - "### Create a Cloud Storage bucket\n", - "\n", - "Create a storage bucket to store intermediate artifacts such as datasets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "MzGDU7TWdts_" - }, - "outputs": [], - "source": [ - "BUCKET_URI = f\"gs://your-bucket-name-{PROJECT_ID}-unique\" # @param {type:\"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "-EcIXiGsCePi" - }, - "source": [ - "**Only if your bucket doesn't already exist**: Run the following cell to create your Cloud Storage bucket." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "NIq7R4HZCfIc" - }, - "outputs": [], - "source": [ - "! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "960505627ddf" - }, - "source": [ - "### Import libraries and define constants" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "PyQmSRbKA8r-" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bf\n", - "import torch\n", - "import vertexai\n", - "from vertexai.preview import VertexModel\n", - "\n", - "bf.options.bigquery.location = \"us\" # Dataset is in 'us' not 'us-central1'\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "\n", - "from bigframes.ml.model_selection import \\\n", - " train_test_split as bf_train_test_split" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "source": [ - "## Initialize Vertex AI SDK for Python\n", - "\n", - "Initialize the Vertex AI SDK for Python for your project and corresponding bucket." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "outputs": [], - "source": [ - "vertexai.init(\n", - " project=PROJECT_ID,\n", - " location=REGION,\n", - " staging_bucket=BUCKET_URI,\n", - ")\n", - "\n", - "REMOTE_JOB_NAME = \"sdk2-bigframes-pytorch\"\n", - "REMOTE_JOB_BUCKET = f\"{BUCKET_URI}/{REMOTE_JOB_NAME}\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "105334524e96" - }, - "source": [ - "## Prepare the dataset\n", - "\n", - "Now load the Iris dataset and split the data into train and test sets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "b44cdc4e03f1" - }, - "outputs": [], - "source": [ - "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.iris\")\n", - "\n", - "species_categories = {\n", - " \"versicolor\": 0,\n", - " \"virginica\": 1,\n", - " \"setosa\": 2,\n", - "}\n", - "df[\"species\"] = df[\"species\"].map(species_categories)\n", - "\n", - "# Assign an index column name\n", - "index_col = \"index\"\n", - "df.index.name = index_col" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "9cb8616b1997" - }, - "outputs": [], - "source": [ - "feature_columns = df[[\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]]\n", - "label_columns = df[[\"species\"]]\n", - "train_X, test_X, train_y, test_y = bf_train_test_split(\n", - " feature_columns, label_columns, test_size=0.2\n", - ")\n", - "\n", - "print(\"X_train size: \", train_X.size)\n", - "print(\"X_test size: \", test_X.size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "23fe7b734b08" - }, - "outputs": [], - "source": [ - "# Switch to remote mode for training\n", - "vertexai.preview.init(remote=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "5904a0f1bb03" - }, - "source": [ - "## PyTorch remote training with CPU (Custom PyTorch model)\n", - "\n", - "First, train a PyTorch model as a remote training job:\n", - "\n", - "- Reinitialize Vertex AI for remote training.\n", - "- Set TorchLogisticRegression for the remote training job.\n", - "- Invoke TorchLogisticRegression locally which will launch the remote training job." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "2a1b85195a17" - }, - "outputs": [], - "source": [ - "# define the custom model\n", - "class TorchLogisticRegression(VertexModel, torch.nn.Module):\n", - " def __init__(self, input_size: int, output_size: int):\n", - " torch.nn.Module.__init__(self)\n", - " VertexModel.__init__(self)\n", - " self.linear = torch.nn.Linear(input_size, output_size)\n", - " self.softmax = torch.nn.Softmax(dim=1)\n", - "\n", - " def forward(self, x):\n", - " return self.softmax(self.linear(x))\n", - "\n", - " @vertexai.preview.developer.mark.train()\n", - " def train(self, X, y, num_epochs, lr):\n", - " X = X.to(torch.float32)\n", - " y = torch.flatten(y) # necessary to get 1D tensor\n", - " dataloader = torch.utils.data.DataLoader(\n", - " torch.utils.data.TensorDataset(X, y),\n", - " batch_size=10,\n", - " shuffle=True,\n", - " generator=torch.Generator(device=X.device),\n", - " )\n", - "\n", - " criterion = torch.nn.CrossEntropyLoss()\n", - " optimizer = torch.optim.SGD(self.parameters(), lr=lr)\n", - "\n", - " for t in range(num_epochs):\n", - " for batch, (X, y) in enumerate(dataloader):\n", - " optimizer.zero_grad()\n", - " pred = self(X)\n", - " loss = criterion(pred, y)\n", - " loss.backward()\n", - " optimizer.step()\n", - "\n", - " @vertexai.preview.developer.mark.predict()\n", - " def predict(self, X):\n", - " X = torch.tensor(X).to(torch.float32)\n", - " with torch.no_grad():\n", - " pred = torch.argmax(self(X), dim=1)\n", - " return pred" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "4e35593f520a" - }, - "outputs": [], - "source": [ - "# Switch to remote mode for training\n", - "vertexai.preview.init(remote=True)\n", - "\n", - "# Instantiate model\n", - "model = TorchLogisticRegression(4, 3)\n", - "\n", - "# Set training config\n", - "model.train.vertex.remote_config.custom_commands = [\n", - " \"pip install torchdata\",\n", - " \"pip install torcharrow\",\n", - "]\n", - "model.train.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-torch-model\"\n", - "model.train.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", - "\n", - "# Train model on Vertex\n", - "model.train(train_X, train_y, num_epochs=200, lr=0.05)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "edf4d0708f02" - }, - "source": [ - "## Remote prediction\n", - "\n", - "Obtain predictions from the trained model." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "42dfbff0ca15" - }, - "outputs": [], - "source": [ - "vertexai.preview.init(remote=True)\n", - "\n", - "# Set remote config\n", - "model.predict.vertex.remote_config.custom_commands = [\n", - " \"pip install torchdata\",\n", - " \"pip install torcharrow\",\n", - "]\n", - "model.predict.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-torch-predict\"\n", - "model.predict.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", - "\n", - "predictions = model.predict(test_X)\n", - "\n", - "print(f\"Remote predictions: {predictions}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "4340ed8316cd" - }, - "source": [ - "## Local evaluation\n", - "\n", - "Evaluate model results locally." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "eb27a31cec6f" - }, - "outputs": [], - "source": [ - "# User must convert bigframes to torch tensor for local evaluation\n", - "train_X_tensor = torch.from_numpy(\n", - " train_X.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", - ")\n", - "train_y_tensor = torch.from_numpy(\n", - " train_y.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", - ")\n", - "\n", - "test_X_tensor = torch.from_numpy(\n", - " test_X.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", - ")\n", - "test_y_tensor = torch.from_numpy(\n", - " test_y.to_pandas().reset_index().drop(columns=[\"index\"]).values.astype(float)\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "7db44ad81389" - }, - "outputs": [], - "source": [ - "from sklearn.metrics import accuracy_score\n", - "\n", - "# Switch to local mode for evaluation\n", - "vertexai.preview.init(remote=False)\n", - "\n", - "# Evaluate model's accuracy score\n", - "print(\n", - " f\"Train accuracy: {accuracy_score(train_y_tensor, model.predict(train_X_tensor))}\"\n", - ")\n", - "\n", - "print(f\"Test accuracy: {accuracy_score(test_y_tensor, model.predict(test_X_tensor))}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TpV-iwP9qw9c" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can delete the individual resources you created in this tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "sx_vKniMq9ZX" - }, - "outputs": [], - "source": [ - "import os\n", - "\n", - "# Delete Cloud Storage objects that were created\n", - "delete_bucket = False\n", - "if delete_bucket or os.getenv(\"IS_TESTING\"):\n", - " ! gsutil -m rm -r $BUCKET_URI" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "sdk2_bigframes_pytorch.ipynb", - "toc_visible": true - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb b/notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb deleted file mode 100644 index 021c0707535..00000000000 --- a/notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb +++ /dev/null @@ -1,727 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ur8xi4C7S06n" - }, - "outputs": [], - "source": [ - "# Copyright 2023 Google LLC\n", - "#\n", - "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", - "# you may not use this file except in compliance with the License.\n", - "# You may obtain a copy of the License at\n", - "#\n", - "# https://www.apache.org/licenses/LICENSE-2.0\n", - "#\n", - "# Unless required by applicable law or agreed to in writing, software\n", - "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", - "# See the License for the specific language governing permissions and\n", - "# limitations under the License." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "JAPoU8Sm5E6e" - }, - "source": [ - "# Train a scikit-learn model with Vertex AI SDK 2.0 and Bigframes\n", - "\n", - "\n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"VertexOpen in Vertex AI Workbench\n", - " \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tvgnzT1CKxrO" - }, - "source": [ - "## Overview\n", - "\n", - "This tutorial demonstrates how to train a scikit-learn model using Vertex AI local-to-remote training with Vertex AI SDK 2.0 and BigQuery Bigframes as the data source.\n", - "\n", - "Learn more about [bigframes](https://cloud.google.com/bigquery/docs/)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d975e698c9a4" - }, - "source": [ - "### Objective\n", - "\n", - "In this tutorial, you learn to use `Vertex AI SDK 2.0` with Bigframes as input data source.\n", - "\n", - "\n", - "This tutorial uses the following Google Cloud ML services:\n", - "\n", - "- `Vertex AI Training`\n", - "- `Vertex AI Remote Training`\n", - "\n", - "\n", - "The steps performed include:\n", - "\n", - "- Initialize a dataframe from a BigQuery table and split the dataset\n", - "- Perform transformations as a Vertex AI remote training.\n", - "- Train the model remotely and evaluate the model locally\n", - "\n", - "**Local-to-remote training**\n", - "\n", - "```\n", - "import vertexai\n", - "from my_module import MyModelClass\n", - "\n", - "vertexai.preview.init(remote=True, project=\"my-project\", location=\"my-location\", staging_bucket=\"gs://my-bucket\")\n", - "\n", - "# Wrap the model class with `vertex_ai.preview.remote`\n", - "MyModelClass = vertexai.preview.remote(MyModelClass)\n", - "\n", - "# Instantiate the class\n", - "model = MyModelClass(...)\n", - "\n", - "# Optional set remote config\n", - "model.fit.vertex.remote_config.display_name = \"MyModelClass-remote-training\"\n", - "model.fit.vertex.remote_config.staging_bucket = \"gs://my-bucket\"\n", - "\n", - "# This `fit` call will be executed remotely\n", - "model.fit(...)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "08d289fa873f" - }, - "source": [ - "### Dataset\n", - "\n", - "This tutorial uses the IRIS dataset, which predicts the iris species." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "aed92deeb4a0" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* Vertex AI\n", - "* BigQuery\n", - "* Cloud Storage\n", - "\n", - "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing),\n", - "[BigQuery pricing](https://cloud.google.com/bigquery/pricing),\n", - "and [Cloud Storage pricing](https://cloud.google.com/storage/pricing), \n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i7EUnXsZhAGF" - }, - "source": [ - "## Installation\n", - "\n", - "Install the following packages required to execute this notebook. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "2b4ef9b72d43" - }, - "outputs": [], - "source": [ - "# Install the packages\n", - "! pip3 install --upgrade --quiet google-cloud-aiplatform[preview]\n", - "! pip3 install --upgrade --quiet bigframes" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "58707a750154" - }, - "source": [ - "### Colab only: Uncomment the following cell to restart the kernel." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "f200f10a1da3" - }, - "outputs": [], - "source": [ - "# Automatically restart kernel after installs so that your environment can access the new packages\n", - "# import IPython\n", - "\n", - "# app = IPython.Application.instance()\n", - "# app.kernel.do_shutdown(True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BF1j6f9HApxa" - }, - "source": [ - "## Before you begin\n", - "\n", - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", - "\n", - "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WReHDGG5g0XY" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "**If you don't know your project ID**, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "oM1iC_MfAts1" - }, - "outputs": [], - "source": [ - "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", - "\n", - "# Set the project id\n", - "! gcloud config set project {PROJECT_ID}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "region" - }, - "source": [ - "#### Region\n", - "\n", - "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "region" - }, - "outputs": [], - "source": [ - "REGION = \"us-central1\" # @param {type: \"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sBCra4QMA2wR" - }, - "source": [ - "### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "74ccc9e52986" - }, - "source": [ - "**1. Vertex AI Workbench**\n", - "* Do nothing as you are already authenticated." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "de775a3773ba" - }, - "source": [ - "**2. Local JupyterLab instance, uncomment and run:**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "254614fa0c46" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ef21552ccea8" - }, - "source": [ - "**3. Colab, uncomment and run:**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "603adbbf0532" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "f6b2ccc891ed" - }, - "source": [ - "**4. Service account or other**\n", - "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "zgPO1eR3CYjk" - }, - "source": [ - "### Create a Cloud Storage bucket\n", - "\n", - "Create a storage bucket to store intermediate artifacts such as datasets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "MzGDU7TWdts_" - }, - "outputs": [], - "source": [ - "BUCKET_URI = f\"gs://your-bucket-name-{PROJECT_ID}-unique\" # @param {type:\"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "-EcIXiGsCePi" - }, - "source": [ - "**Only if your bucket doesn't already exist**: Run the following cell to create your Cloud Storage bucket." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "NIq7R4HZCfIc" - }, - "outputs": [], - "source": [ - "! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "960505627ddf" - }, - "source": [ - "### Import libraries and define constants" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "PyQmSRbKA8r-" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bf\n", - "import vertexai\n", - "\n", - "bf.options.bigquery.location = \"us\" # Dataset is in 'us' not 'us-central1'\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "\n", - "from bigframes.ml.model_selection import \\\n", - " train_test_split as bf_train_test_split\n", - "\n", - "REMOTE_JOB_NAME = \"sdk2-bigframes-sklearn\"\n", - "REMOTE_JOB_BUCKET = f\"{BUCKET_URI}/{REMOTE_JOB_NAME}\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "source": [ - "## Initialize Vertex AI SDK for Python\n", - "\n", - "Initialize the Vertex AI SDK for Python for your project and corresponding bucket." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "outputs": [], - "source": [ - "vertexai.init(\n", - " project=PROJECT_ID,\n", - " location=REGION,\n", - " staging_bucket=BUCKET_URI,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "105334524e96" - }, - "source": [ - "## Prepare the dataset\n", - "\n", - "Now load the Iris dataset and split the data into train and test sets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "b44cdc4e03f1" - }, - "outputs": [], - "source": [ - "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.iris\")\n", - "\n", - "species_categories = {\n", - " \"versicolor\": 0,\n", - " \"virginica\": 1,\n", - " \"setosa\": 2,\n", - "}\n", - "df[\"species\"] = df[\"species\"].map(species_categories)\n", - "\n", - "# Assign an index column name\n", - "index_col = \"index\"\n", - "df.index.name = index_col" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "9cb8616b1997" - }, - "outputs": [], - "source": [ - "feature_columns = df[[\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]]\n", - "label_columns = df[[\"species\"]]\n", - "train_X, test_X, train_y, test_y = bf_train_test_split(\n", - " feature_columns, label_columns, test_size=0.2\n", - ")\n", - "\n", - "print(\"X_train size: \", train_X.size)\n", - "print(\"X_test size: \", test_X.size)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "8306545fcc57" - }, - "source": [ - "## Feature transformation\n", - "\n", - "Next, you do feature transformations on the data using the Vertex AI remote training service.\n", - "\n", - "First, you re-initialize Vertex AI to enable remote training." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "55e701c31036" - }, - "outputs": [], - "source": [ - "# Switch to remote mode for training\n", - "vertexai.preview.init(remote=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "4a0e9d59b273" - }, - "source": [ - "### Execute remote job for fit_transform() on training data\n", - "\n", - "Next, indicate that the `StandardScalar` class is to be executed remotely. Then set up the data transform and call the `fit_transform()` method is executed remotely." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "90333089d362" - }, - "outputs": [], - "source": [ - "from sklearn.preprocessing import StandardScaler\n", - "\n", - "# Wrap classes to enable Vertex remote execution\n", - "StandardScaler = vertexai.preview.remote(StandardScaler)\n", - "\n", - "# Instantiate transformer\n", - "transformer = StandardScaler()\n", - "\n", - "# Set training config\n", - "transformer.fit_transform.vertex.remote_config.display_name = (\n", - " f\"{REMOTE_JOB_NAME}-fit-transformer-bigframes\"\n", - ")\n", - "transformer.fit_transform.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", - "\n", - "# Execute transformer on Vertex (train_X is bigframes.dataframe.DataFrame, X_train is np.array)\n", - "X_train = transformer.fit_transform(train_X)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6bf95574c907" - }, - "source": [ - "### Remote transform on test data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "da6eea22a89a" - }, - "outputs": [], - "source": [ - "# Transform test dataset before calculate test score\n", - "transformer.transform.vertex.remote_config.display_name = (\n", - " REMOTE_JOB_NAME + \"-transformer\"\n", - ")\n", - "transformer.transform.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", - "\n", - "# Execute transformer on Vertex (test_X is bigframes.dataframe.DataFrame, X_test is np.array)\n", - "X_test = transformer.transform(test_X)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ddf906c886e4" - }, - "source": [ - "## Remote training\n", - "\n", - "First, train the scikit-learn model as a remote training job:\n", - "\n", - "- Set LogisticRegression for the remote training job.\n", - "- Invoke LogisticRegression locally which will launch the remote training job." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "c7b0116fa60c" - }, - "outputs": [], - "source": [ - "from sklearn.linear_model import LogisticRegression\n", - "\n", - "# Wrap classes to enable Vertex remote execution\n", - "LogisticRegression = vertexai.preview.remote(LogisticRegression)\n", - "\n", - "# Instantiate model, warm_start=True for uptraining\n", - "model = LogisticRegression(warm_start=True)\n", - "\n", - "# Set training config\n", - "model.fit.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-sklearn-model\"\n", - "model.fit.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", - "\n", - "# Train model on Vertex\n", - "model.fit(train_X, train_y)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ffe1d5903bcb" - }, - "source": [ - "## Remote prediction\n", - "\n", - "Obtain predictions from the trained model." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "d00ce35920fa" - }, - "outputs": [], - "source": [ - "# Remote evaluation\n", - "vertexai.preview.init(remote=True)\n", - "\n", - "# Evaluate model's accuracy score\n", - "predictions = model.predict(test_X)\n", - "\n", - "print(f\"Remote predictions: {predictions}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "a8cd6cbd4403" - }, - "source": [ - "## Local evaluation\n", - "\n", - "Score model results locally." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "dc105dafdfb9" - }, - "outputs": [], - "source": [ - "# User must convert bigframes to pandas dataframe for local evaluation\n", - "train_X_pd = train_X.to_pandas().reset_index(drop=True)\n", - "train_y_pd = train_y.to_pandas().reset_index(drop=True)\n", - "\n", - "test_X_pd = test_X.to_pandas().reset_index(drop=True)\n", - "test_y_pd = test_y.to_pandas().reset_index(drop=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "25fec549de69" - }, - "outputs": [], - "source": [ - "# Switch to local mode for testing\n", - "vertexai.preview.init(remote=False)\n", - "\n", - "# Evaluate model's accuracy score\n", - "print(f\"Train accuracy: {model.score(train_X_pd, train_y_pd)}\")\n", - "\n", - "print(f\"Test accuracy: {model.score(test_X_pd, test_y_pd)}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TpV-iwP9qw9c" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can delete the individual resources you created in this tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "sx_vKniMq9ZX" - }, - "outputs": [], - "source": [ - "import os\n", - "\n", - "# Delete Cloud Storage objects that were created\n", - "delete_bucket = False\n", - "if delete_bucket or os.getenv(\"IS_TESTING\"):\n", - " ! gsutil -m rm -r $BUCKET_URI" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "sdk2_bigframes_sklearn.ipynb", - "toc_visible": true - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb b/notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb deleted file mode 100644 index e6843b66b57..00000000000 --- a/notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb +++ /dev/null @@ -1,646 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ur8xi4C7S06n" - }, - "outputs": [], - "source": [ - "# Copyright 2023 Google LLC\n", - "#\n", - "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", - "# you may not use this file except in compliance with the License.\n", - "# You may obtain a copy of the License at\n", - "#\n", - "# https://www.apache.org/licenses/LICENSE-2.0\n", - "#\n", - "# Unless required by applicable law or agreed to in writing, software\n", - "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", - "# See the License for the specific language governing permissions and\n", - "# limitations under the License." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "JAPoU8Sm5E6e" - }, - "source": [ - "# Train a Tensorflow Keras model with Vertex AI SDK 2.0 and Bigframes \n", - "\n", - "\n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"VertexOpen in Vertex AI Workbench\n", - " \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tvgnzT1CKxrO" - }, - "source": [ - "## Overview\n", - "\n", - "This tutorial demonstrates how to train a tensorflow keras model using Vertex AI local-to-remote training with Vertex AI SDK 2.0 and BigQuery Bigframes as the data source.\n", - "\n", - "Learn more about [bigframes](https://cloud.google.com/bigquery/docs/)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d975e698c9a4" - }, - "source": [ - "### Objective\n", - "\n", - "In this tutorial, you learn to use `Vertex AI SDK 2.0` with Bigframes as input data source.\n", - "\n", - "\n", - "This tutorial uses the following Google Cloud ML services:\n", - "\n", - "- `Vertex AI Training`\n", - "- `Vertex AI Remote Training`\n", - "\n", - "\n", - "The steps performed include:\n", - "\n", - "- Initialize a dataframe from a BigQuery table and split the dataset\n", - "- Perform transformations as a Vertex AI remote training.\n", - "- Train the model remotely and evaluate the model locally\n", - "\n", - "**Local-to-remote training**\n", - "\n", - "```\n", - "import vertexai\n", - "from my_module import MyModelClass\n", - "\n", - "vertexai.preview.init(remote=True, project=\"my-project\", location=\"my-location\", staging_bucket=\"gs://my-bucket\")\n", - "\n", - "# Wrap the model class with `vertex_ai.preview.remote`\n", - "MyModelClass = vertexai.preview.remote(MyModelClass)\n", - "\n", - "# Instantiate the class\n", - "model = MyModelClass(...)\n", - "\n", - "# Optional set remote config\n", - "model.fit.vertex.remote_config.display_name = \"MyModelClass-remote-training\"\n", - "model.fit.vertex.remote_config.staging_bucket = \"gs://my-bucket\"\n", - "\n", - "# This `fit` call will be executed remotely\n", - "model.fit(...)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "08d289fa873f" - }, - "source": [ - "### Dataset\n", - "\n", - "This tutorial uses the IRIS dataset, which predicts the iris species." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "aed92deeb4a0" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* Vertex AI\n", - "* BigQuery\n", - "* Cloud Storage\n", - "\n", - "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing),\n", - "[BigQuery pricing](https://cloud.google.com/bigquery/pricing),\n", - "and [Cloud Storage pricing](https://cloud.google.com/storage/pricing), \n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i7EUnXsZhAGF" - }, - "source": [ - "## Installation\n", - "\n", - "Install the following packages required to execute this notebook. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "2b4ef9b72d43" - }, - "outputs": [], - "source": [ - "# Install the packages\n", - "! pip3 install --upgrade --quiet google-cloud-aiplatform[preview]\n", - "! pip3 install --upgrade --quiet bigframes\n", - "! pip3 install --upgrade --quiet tensorflow==2.12.0" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "58707a750154" - }, - "source": [ - "### Colab only: Uncomment the following cell to restart the kernel." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "f200f10a1da3" - }, - "outputs": [], - "source": [ - "# Automatically restart kernel after installs so that your environment can access the new packages\n", - "# import IPython\n", - "\n", - "# app = IPython.Application.instance()\n", - "# app.kernel.do_shutdown(True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BF1j6f9HApxa" - }, - "source": [ - "## Before you begin\n", - "\n", - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", - "\n", - "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WReHDGG5g0XY" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "**If you don't know your project ID**, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "oM1iC_MfAts1" - }, - "outputs": [], - "source": [ - "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", - "\n", - "# Set the project id\n", - "! gcloud config set project {PROJECT_ID}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "region" - }, - "source": [ - "#### Region\n", - "\n", - "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "region" - }, - "outputs": [], - "source": [ - "REGION = \"us-central1\" # @param {type: \"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sBCra4QMA2wR" - }, - "source": [ - "### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "74ccc9e52986" - }, - "source": [ - "**1. Vertex AI Workbench**\n", - "* Do nothing as you are already authenticated." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "de775a3773ba" - }, - "source": [ - "**2. Local JupyterLab instance, uncomment and run:**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "254614fa0c46" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ef21552ccea8" - }, - "source": [ - "**3. Colab, uncomment and run:**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "603adbbf0532" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "f6b2ccc891ed" - }, - "source": [ - "**4. Service account or other**\n", - "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "zgPO1eR3CYjk" - }, - "source": [ - "### Create a Cloud Storage bucket\n", - "\n", - "Create a storage bucket to store intermediate artifacts such as datasets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "MzGDU7TWdts_" - }, - "outputs": [], - "source": [ - "BUCKET_URI = f\"gs://your-bucket-name-{PROJECT_ID}-unique\" # @param {type:\"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "-EcIXiGsCePi" - }, - "source": [ - "**Only if your bucket doesn't already exist**: Run the following cell to create your Cloud Storage bucket." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "NIq7R4HZCfIc" - }, - "outputs": [], - "source": [ - "! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "960505627ddf" - }, - "source": [ - "### Import libraries and define constants" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "PyQmSRbKA8r-" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bf\n", - "import tensorflow as tf\n", - "import vertexai\n", - "from tensorflow import keras\n", - "\n", - "bf.options.bigquery.location = \"us\" # Dataset is in 'us' not 'us-central1'\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "\n", - "from bigframes.ml.model_selection import \\\n", - " train_test_split as bf_train_test_split" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "source": [ - "## Initialize Vertex AI SDK for Python\n", - "\n", - "Initialize the Vertex AI SDK for Python for your project and corresponding bucket." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "outputs": [], - "source": [ - "vertexai.init(\n", - " project=PROJECT_ID,\n", - " location=REGION,\n", - " staging_bucket=BUCKET_URI,\n", - ")\n", - "\n", - "REMOTE_JOB_NAME = \"sdk2-bigframes-tensorflow\"\n", - "REMOTE_JOB_BUCKET = f\"{BUCKET_URI}/{REMOTE_JOB_NAME}\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "105334524e96" - }, - "source": [ - "## Prepare the dataset\n", - "\n", - "Now load the Iris dataset and split the data into train and test sets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "94576deccd8c" - }, - "outputs": [], - "source": [ - "df = bf.read_gbq(\"bigquery-public-data.ml_datasets.iris\")\n", - "\n", - "species_categories = {\n", - " \"versicolor\": 0,\n", - " \"virginica\": 1,\n", - " \"setosa\": 2,\n", - "}\n", - "df[\"target\"] = df[\"species\"].map(species_categories)\n", - "df = df.drop(columns=[\"species\"])\n", - "\n", - "train, test = bf_train_test_split(df, test_size=0.2)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "cfcbce726efa" - }, - "source": [ - "## Remote training with GPU\n", - "\n", - "First, train a TensorFlow model as a remote training job:\n", - "\n", - "- Reinitialize Vertex AI for remote training.\n", - "- Instantiate the tensorflow keras model for the remote training job.\n", - "- Invoke the tensorflow keras model.fit() locally which will launch the remote training job." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "fd865b0c4e8b" - }, - "outputs": [], - "source": [ - "# Switch to remote mode for training\n", - "vertexai.preview.init(remote=True)\n", - "\n", - "keras.Sequential = vertexai.preview.remote(keras.Sequential)\n", - "\n", - "# Instantiate model\n", - "model = keras.Sequential(\n", - " [keras.layers.Dense(5, input_shape=(4,)), keras.layers.Softmax()]\n", - ")\n", - "\n", - "# Specify optimizer and loss function\n", - "model.compile(optimizer=\"adam\", loss=\"mean_squared_error\")\n", - "\n", - "# Set training config\n", - "model.fit.vertex.remote_config.enable_cuda = True\n", - "model.fit.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-keras-model-gpu\"\n", - "model.fit.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", - "model.fit.vertex.remote_config.custom_commands = [\"pip install tensorflow-io==0.32.0\"]\n", - "\n", - "# Manually set compute resources this time\n", - "model.fit.vertex.remote_config.machine_type = \"n1-highmem-4\"\n", - "model.fit.vertex.remote_config.accelerator_type = \"NVIDIA_TESLA_K80\"\n", - "model.fit.vertex.remote_config.accelerator_count = 4\n", - "\n", - "# Train model on Vertex\n", - "model.fit(train, epochs=10)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "f1af94ac1477" - }, - "source": [ - "## Remote prediction\n", - "\n", - "Obtain predictions from the trained model." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "1d75879948b5" - }, - "outputs": [], - "source": [ - "vertexai.preview.init(remote=True)\n", - "\n", - "# Set remote config\n", - "model.predict.vertex.remote_config.enable_cuda = False\n", - "model.predict.vertex.remote_config.display_name = REMOTE_JOB_NAME + \"-keras-predict-cpu\"\n", - "model.predict.vertex.remote_config.staging_bucket = REMOTE_JOB_BUCKET\n", - "model.predict.vertex.remote_config.custom_commands = [\n", - " \"pip install tensorflow-io==0.32.0\"\n", - "]\n", - "\n", - "predictions = model.predict(train)\n", - "\n", - "print(f\"Remote predictions: {predictions}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "798b77c95067" - }, - "source": [ - "## Local evaluation\n", - "\n", - "Evaluate model results locally." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "88e734e30791" - }, - "outputs": [], - "source": [ - "# User must convert bigframes to pandas dataframe for local evaluation\n", - "feature_columns = [\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]\n", - "label_columns = [\"target\"]\n", - "\n", - "train_X_np = train[feature_columns].to_pandas().values.astype(float)\n", - "train_y_np = train[label_columns].to_pandas().values.astype(float)\n", - "train_ds = tf.data.Dataset.from_tensor_slices((train_X_np, train_y_np))\n", - "\n", - "test_X_np = test[feature_columns].to_pandas().values.astype(float)\n", - "test_y_np = test[label_columns].to_pandas().values.astype(float)\n", - "test_ds = tf.data.Dataset.from_tensor_slices((test_X_np, test_y_np))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "cb8637f783ad" - }, - "outputs": [], - "source": [ - "# Switch to local mode for evaluation\n", - "vertexai.preview.init(remote=False)\n", - "\n", - "# Evaluate model's mean square errors\n", - "print(f\"Train loss: {model.evaluate(train_ds.batch(32))}\")\n", - "\n", - "print(f\"Test loss: {model.evaluate(test_ds.batch(32))}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TpV-iwP9qw9c" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can delete the individual resources you created in this tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "sx_vKniMq9ZX" - }, - "outputs": [], - "source": [ - "import os\n", - "\n", - "# Delete Cloud Storage objects that were created\n", - "delete_bucket = False\n", - "if delete_bucket or os.getenv(\"IS_TESTING\"):\n", - " ! gsutil -m rm -r $BUCKET_URI" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "sdk2_bigframes_tensorflow.ipynb", - "toc_visible": true - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} From 39fe47451d24a8cf55d7dbb15c6d3b176d25ab18 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:19:03 -0700 Subject: [PATCH 471/921] fix: reloaded transformer .transform error (#569) * fix: reloaded transformer .transform error * fix mypy --- bigframes/ml/compose.py | 13 +- tests/system/large/ml/test_compose.py | 23 ++++ tests/system/small/ml/test_preprocessing.py | 129 ++++++++++++++++++-- 3 files changed, 149 insertions(+), 16 deletions(-) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 8638f4d182f..89969f23e71 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -115,14 +115,17 @@ def camel_to_snake(name): name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + output_names = [] for transform_col in bq_model._properties["transformColumns"]: + transform_col_dict = cast(dict, transform_col) # pass the columns that are not transformed - if "transformSql" not in transform_col: + if "transformSql" not in transform_col_dict: continue - transform_sql: str = cast(dict, transform_col)["transformSql"] + transform_sql: str = transform_col_dict["transformSql"] if not transform_sql.startswith("ML."): continue + output_names.append(transform_col_dict["name"]) found_transformer = False for prefix in _BQML_TRANSFROM_TYPE_MAPPING: if transform_sql.startswith(prefix): @@ -141,7 +144,10 @@ def camel_to_snake(name): f"Unsupported transformer type. {constants.FEEDBACK_LINK}" ) - return cls(transformers=transformers) + transformer = cls(transformers=transformers) + transformer._output_names = output_names + + return transformer def _merge( self, bq_model: bigquery.Model @@ -164,6 +170,7 @@ def _merge( for feature_column in bq_model.feature_columns ] ) == sorted(columns): + transformer_0._output_names = self._output_names return transformer_0 return self diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 0107d371cbf..7513b78b29e 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -142,3 +142,26 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): ] assert reloaded_transformer.transformers_ == expected assert reloaded_transformer._bqml_model is not None + + result = transformer.fit_transform( + new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] + ).to_pandas() + + expected = pandas.DataFrame( + { + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + "standard_scaled_culmen_length_mm": [ + 1.313249, + -0.20198, + -1.111118, + ], + "standard_scaled_flipper_length_mm": [1.251098, -1.196588, -0.054338], + }, + index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), + ) + + pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 22c3c84959f..faa0cd7bbd6 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -58,7 +58,7 @@ def test_standard_scaler_normalizes(penguins_df_default_index, new_penguins_df): index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_standard_scaler_normalizeds_fit_transform(new_penguins_df): @@ -82,7 +82,7 @@ def test_standard_scaler_normalizeds_fit_transform(new_penguins_df): index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_standard_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): @@ -110,7 +110,7 @@ def test_standard_scaler_series_normalizes(penguins_df_default_index, new_pengui index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_standard_scaler_save_load(new_penguins_df, dataset_id): @@ -125,6 +125,22 @@ def test_standard_scaler_save_load(new_penguins_df, dataset_id): assert isinstance(reloaded_transformer, preprocessing.StandardScaler) assert reloaded_transformer._bqml_model is not None + result = reloaded_transformer.transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + expected = pd.DataFrame( + { + "standard_scaled_culmen_length_mm": [1.313249, -0.20198, -1.111118], + "standard_scaled_culmen_depth_mm": [1.17072, -1.272416, 0.101848], + "standard_scaled_flipper_length_mm": [1.251089, -1.196588, -0.054338], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=0.1) + def test_max_abs_scaler_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.MaxAbsScaler, when BQML's change is in prod. @@ -157,7 +173,7 @@ def test_max_abs_scaler_normalizes(penguins_df_default_index, new_penguins_df): index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_max_abs_scaler_normalizeds_fit_transform(new_penguins_df): @@ -176,7 +192,7 @@ def test_max_abs_scaler_normalizeds_fit_transform(new_penguins_df): index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): @@ -199,7 +215,7 @@ def test_max_abs_scaler_series_normalizes(penguins_df_default_index, new_penguin index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_max_abs_scaler_save_load(new_penguins_df, dataset_id): @@ -214,6 +230,22 @@ def test_max_abs_scaler_save_load(new_penguins_df, dataset_id): assert isinstance(reloaded_transformer, preprocessing.MaxAbsScaler) assert reloaded_transformer._bqml_model is not None + result = reloaded_transformer.transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + expected = pd.DataFrame( + { + "max_abs_scaled_culmen_length_mm": [1.0, 0.974684, 0.959494], + "max_abs_scaled_culmen_depth_mm": [1.0, 0.914894, 0.962766], + "max_abs_scaled_flipper_length_mm": [1.0, 0.923469, 0.959184], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=0.1) + def test_min_max_scaler_normalized_fit_transform(new_penguins_df): scaler = preprocessing.MinMaxScaler() @@ -231,7 +263,7 @@ def test_min_max_scaler_normalized_fit_transform(new_penguins_df): index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_min_max_scaler_series_normalizes(penguins_df_default_index, new_penguins_df): @@ -255,7 +287,7 @@ def test_min_max_scaler_series_normalizes(penguins_df_default_index, new_penguin index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): @@ -290,7 +322,7 @@ def test_min_max_scaler_normalizes(penguins_df_default_index, new_penguins_df): index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_min_max_scaler_save_load(new_penguins_df, dataset_id): @@ -305,6 +337,22 @@ def test_min_max_scaler_save_load(new_penguins_df, dataset_id): assert isinstance(reloaded_transformer, preprocessing.MinMaxScaler) assert reloaded_transformer._bqml_model is not None + result = reloaded_transformer.fit_transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + expected = pd.DataFrame( + { + "min_max_scaled_culmen_length_mm": [1.0, 0.375, 0.0], + "min_max_scaled_culmen_depth_mm": [1.0, 0.0, 0.5625], + "min_max_scaled_flipper_length_mm": [1.0, 0.0, 0.466667], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=0.1) + def test_k_bins_discretizer_normalized_fit_transform_default_params(new_penguins_df): discretizer = preprocessing.KBinsDiscretizer(strategy="uniform") @@ -322,7 +370,7 @@ def test_k_bins_discretizer_normalized_fit_transform_default_params(new_penguins index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_k_bins_discretizer_series_normalizes( @@ -344,7 +392,7 @@ def test_k_bins_discretizer_series_normalizes( index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_k_bins_discretizer_normalizes(penguins_df_default_index, new_penguins_df): @@ -374,7 +422,7 @@ def test_k_bins_discretizer_normalizes(penguins_df_default_index, new_penguins_d index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_k_bins_discretizer_normalizes_different_params( @@ -406,7 +454,7 @@ def test_k_bins_discretizer_normalizes_different_params( index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), ) - pd.testing.assert_frame_equal(result, expected, rtol=1e-3) + pd.testing.assert_frame_equal(result, expected, rtol=0.1) def test_k_bins_discretizer_save_load(new_penguins_df, dataset_id): @@ -423,6 +471,22 @@ def test_k_bins_discretizer_save_load(new_penguins_df, dataset_id): assert reloaded_transformer.strategy == transformer.strategy assert reloaded_transformer._bqml_model is not None + result = reloaded_transformer.fit_transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + expected = pd.DataFrame( + { + "kbinsdiscretizer_culmen_length_mm": ["bin_6", "bin_4", "bin_2"], + "kbinsdiscretizer_culmen_depth_mm": ["bin_6", "bin_2", "bin_5"], + "kbinsdiscretizer_flipper_length_mm": ["bin_6", "bin_2", "bin_4"], + }, + dtype="string[pyarrow]", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=0.1) + def test_one_hot_encoder_default_params(new_penguins_df): encoder = preprocessing.OneHotEncoder() @@ -560,6 +624,29 @@ def test_one_hot_encoder_save_load(new_penguins_df, dataset_id): assert reloaded_transformer.max_categories == transformer.max_categories assert reloaded_transformer._bqml_model is not None + result = reloaded_transformer.fit_transform( + new_penguins_df[["species", "sex"]] + ).to_pandas() + + expected = pd.DataFrame( + { + "onehotencoded_species": [ + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 2, "value": 1.0}], + ], + "onehotencoded_sex": [ + [{"index": 2, "value": 1.0}], + [{"index": 1, "value": 1.0}], + [{"index": 1, "value": 1.0}], + ], + }, + dtype=ONE_HOT_ENCODED_DTYPE, + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + def test_label_encoder_default_params(new_penguins_df): encoder = preprocessing.LabelEncoder() @@ -677,5 +764,21 @@ def test_label_encoder_save_load(new_penguins_df, dataset_id): assert reloaded_transformer.max_categories == transformer.max_categories assert reloaded_transformer._bqml_model is not None + result = reloaded_transformer.transform(new_penguins_df).to_pandas() + + expected = pd.DataFrame( + { + "labelencoded_species": [ + 1, + 1, + 2, + ], + }, + dtype="Int64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + # TODO(garrettwu): add OneHotEncoder tests to compare with sklearn. From 9084df369bc6819edf5f57ceba85667a14371ac5 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:28:16 -0700 Subject: [PATCH 472/921] docs: address more comments from technical writers to meet legal purposes (#571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/ml/base.py | 10 +++++----- bigframes/ml/ensemble.py | 18 +++++++++--------- bigframes/ml/forecasting.py | 6 +++--- bigframes/ml/metrics/_metrics.py | 2 +- bigframes/ml/model_selection.py | 4 ++-- bigframes/ml/pipeline.py | 2 +- bigframes/ml/preprocessing.py | 2 +- .../ml_fundamentals_bq_dataframes.ipynb | 2 +- .../regression/sklearn_linear_regression.ipynb | 2 +- .../pandas/core/config_init.py | 10 +++++----- .../bigframes_vendored/xgboost/sklearn.py | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 5e7aada8de0..c57cb78791a 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -77,7 +77,7 @@ def fit_transform(self, x_train: Union[DataFrame, Series], y_train: Union[DataFr """ def __repr__(self): - """Print the estimator's constructor with all non-default parameter values""" + """Print the estimator's constructor with all non-default parameter values.""" # Estimator pretty printer adapted from Sklearn's, which is in turn an adaption of # the inbuilt pretty-printer in CPython @@ -106,13 +106,13 @@ def predict(self, X): def register(self: _T, vertex_ai_model_id: Optional[str] = None) -> _T: """Register the model to Vertex AI. - After register, go to Google Cloud Console (https://console.cloud.google.com/vertex-ai/models) + After register, go to the Google Cloud console (https://console.cloud.google.com/vertex-ai/models) to manage the model registries. Refer to https://cloud.google.com/vertex-ai/docs/model-registry/introduction for more options. Args: vertex_ai_model_id (Optional[str], default None): - optional string id as model id in Vertex. If not set, will by default to 'bigframes_{bq_model_id}'. + Optional string id as model id in Vertex. If not set, will default to 'bigframes_{bq_model_id}'. Vertex Ai model id will be truncated to 63 characters due to its limitation. Returns: @@ -191,9 +191,9 @@ def to_gbq(self: _T, model_name: str, replace: bool = False) -> _T: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Whether to replace if the model already exists. Default to False. Returns: Saved transformer.""" diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 72ea600c581..a8f0329145b 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Ensemble models. This module is styled after Scikit-Learn's ensemble module: +"""Ensemble models. This module is styled after scikit-learn's ensemble module: https://scikit-learn.org/stable/modules/ensemble.html""" from __future__ import annotations @@ -190,9 +190,9 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Whether to replace if the model already exists. Default to False. Returns: saved model.""" if not self._bqml_model: @@ -343,9 +343,9 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Whether to replace if the model already exists. Default to False. Returns: XGBClassifier: saved model.""" @@ -506,9 +506,9 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegresso Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Whether to replace if the model already exists. Default to False. Returns: RandomForestRegressor: saved model.""" @@ -669,9 +669,9 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestClassifi Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Whether to replace if the model already exists. Default to False. Returns: RandomForestClassifier: saved model.""" diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 79933272004..e50a8ed35b9 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -248,12 +248,12 @@ def predict( an int value that specifies the number of time points to forecast. The default value is 3, and the maximum value is 1000. confidence_level (float, default 0.95): - a float value that specifies percentage of the future values that fall in the prediction interval. + A float value that specifies percentage of the future values that fall in the prediction interval. The valid input range is [0.0, 1.0). Returns: bigframes.dataframe.DataFrame: The predicted DataFrames. Which - contains 2 columns "forecast_timestamp" and "forecast_value". + contains 2 columns: "forecast_timestamp" and "forecast_value". """ if horizon < 1 or horizon > 1000: raise ValueError(f"horizon must be [1, 1000], but is {horizon}.") @@ -284,7 +284,7 @@ def detect_anomalies( Identifies the custom threshold to use for anomaly detection. The value must be in the range [0, 1), with a default value of 0.95. Returns: - bigframes.dataframe.DataFrame: detected DataFrame.""" + bigframes.dataframe.DataFrame: Detected DataFrame.""" if anomaly_prob_threshold < 0.0 or anomaly_prob_threshold >= 1.0: raise ValueError( f"anomaly_prob_threshold must be [0.0, 1.0), but is {anomaly_prob_threshold}." diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index b8c264e91b7..2525ecd34fe 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -13,7 +13,7 @@ # limitations under the License. """Metrics functions for evaluating models. This module is styled after -Scikit-Learn's metrics module: https://scikit-learn.org/stable/modules/metrics.html.""" +scikit-learn's metrics module: https://scikit-learn.org/stable/modules/metrics.html.""" import inspect import typing diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index 443b9e7be6e..42c13fdb40b 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -13,7 +13,7 @@ # limitations under the License. """Functions for test/train split and model tuning. This module is styled after -Scikit-Learn's model_selection module: +scikit-learn's model_selection module: https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection.""" @@ -51,7 +51,7 @@ def train_test_split( List[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]]: A list of BigQuery DataFrames or Series. """ - # TODO(garrettwu): Scikit-Learn throws an error when the dataframes don't have the same + # TODO(garrettwu): scikit-learn throws an error when the dataframes don't have the same # number of rows. We probably want to do something similar. Now the implementation is based # on index. We'll move to based on ordering first. diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 92a3bae77d3..5df2378575e 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""For composing estimators together. This module is styled after Scikit-Learn's +"""For composing estimators together. This module is styled after scikit-learn's pipeline module: https://scikit-learn.org/stable/modules/pipeline.html.""" diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index fd7d44f731b..673ee27db04 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -13,7 +13,7 @@ # limitations under the License. """Transformers that prepare data for other estimators. This module is styled after -Scikit-Learn's preprocessing module: https://scikit-learn.org/stable/modules/preprocessing.html.""" +scikit-learn's preprocessing module: https://scikit-learn.org/stable/modules/preprocessing.html.""" from __future__ import annotations diff --git a/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb b/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb index b3c965aded5..e7b69f017b6 100644 --- a/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb +++ b/notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb @@ -1051,7 +1051,7 @@ "source": [ "## Estimators\n", "\n", - "Following Scikit-Learn, all learning components are \"estimators\"; objects that can learn from training data and then apply themselves to new data. Estimators share the following patterns:\n", + "Following scikit-learn, all learning components are \"estimators\"; objects that can learn from training data and then apply themselves to new data. Estimators share the following patterns:\n", "\n", "- a constructor that takes a list of parameters\n", "- a standard string representation that shows the class name and all non-default parameters, e.g. `LinearRegression(fit_intercept=False)`\n", diff --git a/notebooks/regression/sklearn_linear_regression.ipynb b/notebooks/regression/sklearn_linear_regression.ipynb index ec14d15cdf6..2873527449a 100644 --- a/notebooks/regression/sklearn_linear_regression.ipynb +++ b/notebooks/regression/sklearn_linear_regression.ipynb @@ -7,7 +7,7 @@ "source": [ "# Using ML - SKLearn linear regression\n", "\n", - "This demo shows how we can implement a linear regression in BigQuery DataFrames ML, with API that is exactly compatible with Scikit-Learn." + "This demo shows how we can implement a linear regression in BigQuery DataFrames ML, with API that is exactly compatible with scikit-learn." ] }, { diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index ecc103d7c86..a3178e27618 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -59,18 +59,18 @@ repr_mode (Literal[`head`, `deferred`]): `head`: Execute, download, and display results (limited to head) from - dataframe and series objects during repr. + Dataframe and Series objects during repr. `deferred`: - Prevent executions from repr statements in dataframe and series objects. - Instead estimated bytes processed will be shown. Dataframe and Series + Prevent executions from repr statements in DataFrame and Series objects. + Instead, estimated bytes processed will be shown. DataFrame and Series objects can still be computed with methods that explicitly execute and download results. max_info_columns (int): max_info_columns is used in DataFrame.info method to decide if - per column information will be printed. + information in each column will be printed. max_info_rows (int or None): df.info() will usually show null-counts for each column. - For large frames this can be quite slow. max_info_rows and max_info_cols + For large frames, this can be quite slow. max_info_rows and max_info_cols limit this null check only to frames with smaller dimensions than specified. memory_usage (bool): diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index 250e34dc2ca..424b17a371b 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -1,4 +1,4 @@ -"""Scikit-Learn Wrapper interface for XGBoost.""" +"""scikit-learn Wrapper interface for XGBoost.""" from typing import Any From 75dd7862e60502c97f7defe5dfefb044ea74bae8 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 4 Apr 2024 10:40:16 -0700 Subject: [PATCH 473/921] fix: fix error in `Series.drop(0)` (#575) Due to implicit 0 non-truthfulness, 0 was getting erroneously converted to None. --- bigframes/series.py | 8 +++++--- tests/system/small/test_series.py | 14 ++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/bigframes/series.py b/bigframes/series.py index 42264c35b69..e4d48904b0b 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -351,9 +351,11 @@ def drop( columns: Union[blocks.Label, typing.Iterable[blocks.Label]] = None, level: typing.Optional[LevelType] = None, ) -> Series: - if labels and index: - raise ValueError("Must specify exacly one of 'labels' or 'index'") - index = labels or index + if (labels is None) == (index is None): + raise ValueError("Must specify exactly one of 'labels' or 'index'") + + if labels is not None: + index = labels # ignore axis, columns params block = self._block diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index e15dbc6a3f9..e350286940b 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1529,10 +1529,16 @@ def test_groupby_window_ops(scalars_df_index, scalars_pandas_df_index, operator) ) -def test_drop_label(scalars_df_index, scalars_pandas_df_index): - col_name = "int64_col" - bf_series = scalars_df_index[col_name].drop(1).to_pandas() - pd_series = scalars_pandas_df_index[col_name].drop(1) +@pytest.mark.parametrize( + ("label", "col_name"), + [ + (0, "bool_col"), + (1, "int64_col"), + ], +) +def test_drop_label(scalars_df_index, scalars_pandas_df_index, label, col_name): + bf_series = scalars_df_index[col_name].drop(label).to_pandas() + pd_series = scalars_pandas_df_index[col_name].drop(label) pd.testing.assert_series_equal( pd_series, bf_series, From d2d7e33b1f8b4e184ef3e76eedbd673a8fcee60e Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 4 Apr 2024 13:03:01 -0700 Subject: [PATCH 474/921] perf: Add multi-query execution capability for complex dataframes (#427) --- bigframes/_config/compute_options.py | 6 +- bigframes/core/blocks.py | 4 + bigframes/core/expression.py | 9 ++ bigframes/core/nodes.py | 207 ++++++++++++++++++++++++++- bigframes/core/tree_properties.py | 51 ++++++- bigframes/dataframe.py | 12 +- bigframes/series.py | 10 ++ bigframes/session/__init__.py | 52 +++++++ tests/system/conftest.py | 8 ++ tests/system/small/test_dataframe.py | 50 +++++++ 10 files changed, 403 insertions(+), 6 deletions(-) diff --git a/bigframes/_config/compute_options.py b/bigframes/_config/compute_options.py index fb708b844cb..2b849c558ab 100644 --- a/bigframes/_config/compute_options.py +++ b/bigframes/_config/compute_options.py @@ -40,7 +40,11 @@ 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 `_. - + 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. """ maximum_bytes_billed: Optional[int] = None + enable_multi_query_execution: bool = False diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 0b6e50cfa3e..c7b41e93eb8 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1873,6 +1873,10 @@ def cached(self, *, optimize_offsets=False, force: bool = False) -> Block: expr = self.session._cache_with_cluster_cols( self.expr, cluster_cols=self.index_columns ) + return self.swap_array_expr(expr) + + def swap_array_expr(self, expr: core.ArrayValue) -> Block: + # TODO: Validate schema unchanged return Block( expr, index_columns=self.index_columns, diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 8c3f52d22bd..4980f5369de 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -108,6 +108,11 @@ def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: def is_bijective(self) -> bool: return False + @property + def is_identity(self) -> bool: + """True for identity operation that does not transform input.""" + return False + @dataclasses.dataclass(frozen=True) class ScalarConstantExpression(Expression): @@ -173,6 +178,10 @@ def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: def is_bijective(self) -> bool: return True + @property + def is_identity(self) -> bool: + return True + @dataclasses.dataclass(frozen=True) class OpExpression(Expression): diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index d740605a56f..a1072b0d687 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -15,11 +15,11 @@ from __future__ import annotations import abc -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, field, fields, replace import functools import itertools import typing -from typing import Tuple +from typing import Callable, Tuple import pandas @@ -39,6 +39,10 @@ import bigframes.session +# A fixed number of variable to assume for overhead on some operations +OVERHEAD_VARIABLES = 5 + + @dataclass(frozen=True) class BigFrameNode: """ @@ -102,6 +106,60 @@ def roots(self) -> typing.Set[BigFrameNode]: def schema(self) -> schemata.ArraySchema: ... + @property + @abc.abstractmethod + def variables_introduced(self) -> int: + """ + Defines number of values created by the current node. Helps represent the "width" of a query + """ + ... + + @property + def relation_ops_created(self) -> int: + """ + Defines the number of relational ops generated by the current node. Used to estimate query planning complexity. + """ + return 1 + + @property + def joins(self) -> bool: + """ + Defines whether the node joins data. + """ + return False + + @functools.cached_property + def total_variables(self) -> int: + return self.variables_introduced + sum( + map(lambda x: x.total_variables, self.child_nodes) + ) + + @functools.cached_property + def total_relational_ops(self) -> int: + return self.relation_ops_created + sum( + map(lambda x: x.total_relational_ops, self.child_nodes) + ) + + @functools.cached_property + def total_joins(self) -> int: + return int(self.joins) + sum(map(lambda x: x.total_joins, self.child_nodes)) + + @property + def planning_complexity(self) -> int: + """ + Empirical heuristic measure of planning complexity. + + Used to determine when to decompose overly complex computations. May require tuning. + """ + return self.total_variables * self.total_relational_ops * (1 + self.total_joins) + + @abc.abstractmethod + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + """Apply a function to each child node.""" + ... + @dataclass(frozen=True) class UnaryNode(BigFrameNode): @@ -115,6 +173,11 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: def schema(self) -> schemata.ArraySchema: return self.child.schema + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return replace(self, child=t(self.child)) + @dataclass(frozen=True) class JoinNode(BigFrameNode): @@ -154,6 +217,22 @@ def join_mapping_to_schema_item(mapping: JoinColumnMapping): ) return schemata.ArraySchema(items) + @functools.cached_property + def variables_introduced(self) -> int: + """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" + return OVERHEAD_VARIABLES + + @property + def joins(self) -> bool: + return True + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return replace( + self, left_child=t(self.left_child), right_child=t(self.right_child) + ) + @dataclass(frozen=True) class ConcatNode(BigFrameNode): @@ -182,6 +261,16 @@ def schema(self) -> schemata.ArraySchema: ) return schemata.ArraySchema(items) + @functools.cached_property + def variables_introduced(self) -> int: + """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" + return len(self.schema.items) + OVERHEAD_VARIABLES + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return replace(self, children=tuple(t(child) for child in self.children)) + # Input Nodex @dataclass(frozen=True) @@ -201,6 +290,16 @@ def roots(self) -> typing.Set[BigFrameNode]: def schema(self) -> schemata.ArraySchema: return self.data_schema + @functools.cached_property + def variables_introduced(self) -> int: + """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" + return len(self.schema.items) + 1 + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return self + # TODO: Refactor to take raw gbq object reference @dataclass(frozen=True) @@ -233,6 +332,20 @@ def schema(self) -> schemata.ArraySchema: ) return schemata.ArraySchema(items) + @functools.cached_property + def variables_introduced(self) -> int: + return len(self.columns) + len(self.hidden_ordering_columns) + + @property + def relation_ops_created(self) -> int: + # Assume worst case, where readgbq actually has baked in analytic operation to generate index + return 2 + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return self + # Unary nodes @dataclass(frozen=True) @@ -252,6 +365,14 @@ def schema(self) -> schemata.ArraySchema: schemata.SchemaItem(self.col_id, bigframes.dtypes.INT_DTYPE) ) + @property + def relation_ops_created(self) -> int: + return 2 + + @functools.cached_property + def variables_introduced(self) -> int: + return 1 + @dataclass(frozen=True) class FilterNode(UnaryNode): @@ -264,6 +385,10 @@ def row_preserving(self) -> bool: def __hash__(self): return self._node_hash + @property + def variables_introduced(self) -> int: + return 1 + @dataclass(frozen=True) class OrderByNode(UnaryNode): @@ -281,6 +406,15 @@ def __post_init__(self): def __hash__(self): return self._node_hash + @property + def variables_introduced(self) -> int: + return 0 + + @property + def relation_ops_created(self) -> int: + # Doesnt directly create any relational operations + return 0 + @dataclass(frozen=True) class ReversedNode(UnaryNode): @@ -290,6 +424,15 @@ class ReversedNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def variables_introduced(self) -> int: + return 0 + + @property + def relation_ops_created(self) -> int: + # Doesnt directly create any relational operations + return 0 + @dataclass(frozen=True) class ProjectionNode(UnaryNode): @@ -315,6 +458,12 @@ def schema(self) -> schemata.ArraySchema: ) return schemata.ArraySchema(items) + @property + def variables_introduced(self) -> int: + # ignore passthrough expressions + new_vars = sum(1 for i in self.assignments if not i[0].is_identity) + return new_vars + # TODO: Merge RowCount into Aggregate Node? # Row count can be compute from table metadata sometimes, so it is a bit special. @@ -334,6 +483,10 @@ def schema(self) -> schemata.ArraySchema: (schemata.SchemaItem("count", bigframes.dtypes.INT_DTYPE),) ) + @property + def variables_introduced(self) -> int: + return 1 + @dataclass(frozen=True) class AggregateNode(UnaryNode): @@ -367,6 +520,10 @@ def schema(self) -> schemata.ArraySchema: ) return schemata.ArraySchema(tuple([*by_items, *agg_items])) + @property + def variables_introduced(self) -> int: + return len(self.aggregations) + len(self.by_column_ids) + @dataclass(frozen=True) class WindowOpNode(UnaryNode): @@ -396,12 +553,31 @@ def schema(self) -> schemata.ArraySchema: schemata.SchemaItem(self.output_name, new_item_dtype) ) + @property + def variables_introduced(self) -> int: + return 1 + + @property + def relation_ops_created(self) -> int: + # Assume that if not reprojecting, that there is a sequence of window operations sharing the same window + return 0 if self.skip_reproject_unsafe else 4 + +# TODO: Remove this op @dataclass(frozen=True) class ReprojectOpNode(UnaryNode): def __hash__(self): return self._node_hash + @property + def variables_introduced(self) -> int: + return 0 + + @property + def relation_ops_created(self) -> int: + # This op is not a real transformation, just a hint to the sql generator + return 0 + @dataclass(frozen=True) class UnpivotNode(UnaryNode): @@ -428,6 +604,10 @@ def row_preserving(self) -> bool: def non_local(self) -> bool: return True + @property + def joins(self) -> bool: + return True + @functools.cached_property def schema(self) -> schemata.ArraySchema: def infer_dtype( @@ -469,6 +649,17 @@ def infer_dtype( ] return schemata.ArraySchema((*index_items, *value_items, *passthrough_items)) + @property + def variables_introduced(self) -> int: + return ( + len(self.schema.items) - len(self.passthrough_columns) + OVERHEAD_VARIABLES + ) + + @property + def relation_ops_created(self) -> int: + # Unpivot is essentially a cross join and a projection. + return 2 + @dataclass(frozen=True) class RandomSampleNode(UnaryNode): @@ -485,6 +676,10 @@ def row_preserving(self) -> bool: def __hash__(self): return self._node_hash + @property + def variables_introduced(self) -> int: + return 1 + @dataclass(frozen=True) class ExplodeNode(UnaryNode): @@ -511,3 +706,11 @@ def schema(self) -> schemata.ArraySchema: for name in self.child.schema.names ) return schemata.ArraySchema(items) + + @property + def relation_ops_created(self) -> int: + return 3 + + @functools.cached_property + def variables_introduced(self) -> int: + return len(self.column_ids) + 1 diff --git a/bigframes/core/tree_properties.py b/bigframes/core/tree_properties.py index bc29f115f6d..125a7e6bff7 100644 --- a/bigframes/core/tree_properties.py +++ b/bigframes/core/tree_properties.py @@ -11,12 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations +import functools +import itertools +from typing import Dict import bigframes.core.nodes as nodes -# TODO: Convert these functions to iterative or enforce hard limit on tree depth. The below algorithms can cause stack to exceed limit. - def is_trivially_executable(node: nodes.BigFrameNode) -> bool: if local_only(node): @@ -36,3 +38,48 @@ def peekable(node: nodes.BigFrameNode) -> bool: children_peekable = all(peekable(child) for child in node.child_nodes) self_peekable = not node.non_local return children_peekable and self_peekable + + +def count_complex_nodes( + root: nodes.BigFrameNode, min_complexity: float, max_complexity: float +) -> Dict[nodes.BigFrameNode, int]: + @functools.cache + def _node_counts_inner( + subtree: nodes.BigFrameNode, + ) -> Dict[nodes.BigFrameNode, int]: + """Helper function to count occurences of duplicate nodes in a subtree. Considers only nodes in a complexity range""" + empty_counts: Dict[nodes.BigFrameNode, int] = {} + if subtree.planning_complexity >= min_complexity: + child_counts = [_node_counts_inner(child) for child in subtree.child_nodes] + node_counts = functools.reduce(_combine_counts, child_counts, empty_counts) + if subtree.planning_complexity <= max_complexity: + return _combine_counts(node_counts, {subtree: 1}) + else: + return node_counts + return empty_counts + + return _node_counts_inner(root) + + +def replace_nodes( + root: nodes.BigFrameNode, + to_replace: nodes.BigFrameNode, + replacemenet: nodes.BigFrameNode, +): + @functools.cache + def apply_substition(n: nodes.BigFrameNode) -> nodes.BigFrameNode: + if n == to_replace: + return replacemenet + else: + return n.transform_children(apply_substition) + + return root.transform_children(apply_substition) + + +def _combine_counts( + left: Dict[nodes.BigFrameNode, int], right: Dict[nodes.BigFrameNode, int] +) -> Dict[nodes.BigFrameNode, int]: + return { + key: left.get(key, 0) + right.get(key, 0) + for key in itertools.chain(left.keys(), right.keys()) + } diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0bb88beb2bc..460d1056a32 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1123,7 +1123,7 @@ def to_pandas( downsampled rows and all columns of this DataFrame. """ # TODO(orrbradford): Optimize this in future. Potentially some cases where we can return the stored query job - + self._optimize_query_complexity() df, query_job = self._block.to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, @@ -1135,6 +1135,7 @@ def to_pandas( def to_pandas_batches(self) -> Iterable[pandas.DataFrame]: """Stream DataFrame results to an iterable of pandas DataFrame""" + self._optimize_query_complexity() return self._block.to_pandas_batches() def _compute_dry_run(self) -> bigquery.QueryJob: @@ -3079,6 +3080,7 @@ def _run_io_query( """Executes a query job presenting this dataframe and returns the destination table.""" session = self._block.expr.session + self._optimize_query_complexity() export_array, id_overrides = self._prepare_export( index=index, ordering_id=ordering_id ) @@ -3215,6 +3217,14 @@ def _cached(self, *, force: bool = False) -> DataFrame: self._set_block(self._block.cached(force=force)) return self + def _optimize_query_complexity(self): + """Reduce query complexity by caching repeated subtrees and recursively materializing maximum-complexity subtrees. + May generate many queries and take substantial time to execute. + """ + # TODO: Move all this to session + new_expr = self._session._simplify_with_caching(self._block.expr) + self._set_block(self._block.swap_array_expr(new_expr)) + _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") def dot(self, other: _DataFrameOrSeries) -> _DataFrameOrSeries: diff --git a/bigframes/series.py b/bigframes/series.py index e4d48904b0b..185891bc010 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -176,6 +176,7 @@ def __len__(self): return self.shape[0] def __iter__(self) -> typing.Iterator: + self._optimize_query_complexity() return itertools.chain.from_iterable( map(lambda x: x.squeeze(axis=1), self._block.to_pandas_batches()) ) @@ -328,6 +329,7 @@ def to_pandas( 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. """ + self._optimize_query_complexity() df, query_job = self._block.to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, @@ -1603,6 +1605,14 @@ def _cached(self, *, force: bool = True) -> Series: self._set_block(self._block.cached(force=force)) return self + def _optimize_query_complexity(self): + """Reduce query complexity by caching repeated subtrees and recursively materializing maximum-complexity subtrees. + May generate many queries and take substantial time to execute. + """ + # TODO: Move all this to session + new_expr = self._block.session._simplify_with_caching(self._block.expr) + self._set_block(self._block.swap_array_expr(new_expr)) + def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: return pandas.api.types.is_list_like(obj) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 671a3d65e73..354352f1c9f 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -80,6 +80,7 @@ import bigframes.core.blocks as blocks import bigframes.core.compile import bigframes.core.guid as guid +import bigframes.core.nodes as nodes from bigframes.core.ordering import IntegerEncoding import bigframes.core.ordering as order import bigframes.core.tree_properties as traversals @@ -120,6 +121,11 @@ # Also must assume that text encoding as literals is much less efficient than in-memory representation. MAX_INLINE_DF_BYTES = 5000 +# Max complexity that should be executed as a single query +QUERY_COMPLEXITY_LIMIT = 1e7 +# Number of times to factor out subqueries before giving up. +MAX_SUBTREE_FACTORINGS = 5 + logger = logging.getLogger(__name__) # Excludes geography, bytes, and nested (array, struct) datatypes @@ -1851,6 +1857,52 @@ def _cache_with_offsets(self, array_value: core.ArrayValue) -> core.ArrayValue: ordering=order.ExpressionOrdering.from_offset_col("bigframes_offsets"), ) + def _simplify_with_caching(self, array_value: core.ArrayValue) -> core.ArrayValue: + """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" + if not bigframes.options.compute.enable_multi_query_execution: + return array_value + node = array_value.node + if node.planning_complexity < QUERY_COMPLEXITY_LIMIT: + return array_value + + for _ in range(MAX_SUBTREE_FACTORINGS): + updated = self._cache_most_complex_subtree(node) + if updated is None: + return core.ArrayValue(node) + else: + node = updated + + return core.ArrayValue(node) + + def _cache_most_complex_subtree( + self, node: nodes.BigFrameNode + ) -> Optional[nodes.BigFrameNode]: + # TODO: If query fails, retry with lower complexity limit + valid_candidates = traversals.count_complex_nodes( + node, + min_complexity=(QUERY_COMPLEXITY_LIMIT / 500), + max_complexity=QUERY_COMPLEXITY_LIMIT, + ).items() + # Heuristic: subtree_compleixty * (copies of subtree)^2 + best_candidate = max( + valid_candidates, + key=lambda i: i[0].planning_complexity + (i[1] ** 2), + default=None, + ) + + if best_candidate is None: + # No good subtrees to cache, just return original tree + return None + + # TODO: Add clustering columns based on access patterns + materialized = self._cache_with_cluster_cols( + core.ArrayValue(best_candidate[0]), [] + ).node + + return traversals.replace_nodes( + node, to_replace=best_candidate[0], replacemenet=materialized + ) + def _is_trivially_executable(self, array_value: core.ArrayValue): """ Can the block be evaluated very cheaply? diff --git a/tests/system/conftest.py b/tests/system/conftest.py index a108ff4a8e9..70ff6eee39d 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -953,6 +953,14 @@ def restore_sampling_settings(): bigframes.options.sampling.max_download_size = max_download_size +@pytest.fixture() +def with_multiquery_execution(): + original_setting = bigframes.options.compute.enable_multi_query_execution + bigframes.options.compute.enable_multi_query_execution = True + yield + bigframes.options.compute.enable_multi_query_execution = original_setting + + @pytest.fixture() def weird_strings_pd(): df = pd.DataFrame( diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 8bcdfe168b6..5d6a859c112 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4145,6 +4145,56 @@ def test_recursion_limit(scalars_df_index): scalars_df_index.to_pandas() +def test_query_complexity_repeated_joins( + scalars_df_index, scalars_pandas_df_index, with_multiquery_execution +): + pd_df = scalars_pandas_df_index + bf_df = scalars_df_index + for _ in range(6): + # recursively join, resuling in 2^6 - 1 = 63 joins + pd_df = pd_df.merge(pd_df, on="int64_col").head(30) + pd_df = pd_df[pd_df.columns[:20]] + bf_df = bf_df.merge(bf_df, on="int64_col").head(30) + bf_df = bf_df[bf_df.columns[:20]] + + bf_result = bf_df.to_pandas() + pd_result = pd_df + assert_pandas_df_equal(bf_result, pd_result, check_index_type=False) + + +def test_query_complexity_repeated_subtrees( + scalars_df_index, scalars_pandas_df_index, with_multiquery_execution +): + # Recursively union the data, if fully inlined has 10^5 identical root tables. + pd_df = scalars_pandas_df_index + bf_df = scalars_df_index + for _ in range(5): + pd_df = pd.concat(10 * [pd_df]).head(5) + bf_df = bigframes.pandas.concat(10 * [bf_df]).head(5) + bf_result = bf_df.to_pandas() + pd_result = pd_df + assert_pandas_df_equal(bf_result, pd_result) + + +@pytest.mark.skipif( + sys.version_info >= (3, 12), + # See: https://github.com/python/cpython/issues/112282 + reason="setrecursionlimit has no effect on the Python C stack since Python 3.12.", +) +def test_query_complexity_repeated_analytic( + scalars_df_index, scalars_pandas_df_index, with_multiquery_execution +): + bf_df = scalars_df_index[["int64_col", "int64_too"]] + pd_df = scalars_pandas_df_index[["int64_col", "int64_too"]] + # Uses LAG analytic operator, each in a new SELECT + for _ in range(50): + bf_df = bf_df.diff() + pd_df = pd_df.diff() + bf_result = bf_df.to_pandas() + pd_result = pd_df + assert_pandas_df_equal(bf_result, pd_result) + + def test_to_pandas_downsampling_option_override(session): df = session.read_gbq("bigframes-dev.bigframes_tests_sys.batting") download_size = 1 From 262ff5922643039e037bd9b6c0a91b5bd20a4e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 4 Apr 2024 15:12:53 -0500 Subject: [PATCH 475/921] docs: add General Availability banner to README (#507) * docs: add General Availability banner to README As of 1.0.0 release, BigQuery DataFrames is available as a [Generally Available](https://github.com/googleapis/google-cloud-python/blob/main/README.rst#general-availability) Google Cloud product. * update release status --- README.rst | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 26bbbffa886..64d1e4e72cf 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ BigQuery DataFrames =================== +|GA| |pypi| |versions| + BigQuery DataFrames provides a Pythonic DataFrame and machine learning (ML) API powered by the BigQuery engine. @@ -10,6 +12,12 @@ powered by the BigQuery engine. BigQuery DataFrames is an open-source package. You can run ``pip install --upgrade bigframes`` to install the latest version. +.. |GA| image:: https://img.shields.io/badge/support-GA-gold.svg + :target: https://github.com/googleapis/google-cloud-python/blob/main/README.rst#general-availability +.. |pypi| image:: https://img.shields.io/pypi/v/bigframes.svg + :target: https://pypi.org/project/bigframes/ +.. |versions| image:: https://img.shields.io/pypi/pyversions/bigframes.svg + :target: https://pypi.org/project/bigframes/ Documentation ------------- diff --git a/setup.py b/setup.py index 86fb9d496c3..83049f97152 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ # 'Development Status :: 3 - Alpha' # 'Development Status :: 4 - Beta' # 'Development Status :: 5 - Production/Stable' -release_status = "Development Status :: 3 - Alpha" +release_status = "Development Status :: 5 - Production/Stable" dependencies = [ # please keep these in sync with the minimum versions in testing/constraints-3.9.txt "cloudpickle >= 2.0.0", From 8add6b1e569c385d25d13e45afdd8ad5bd8d3294 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:00:27 -0700 Subject: [PATCH 476/921] chore(main): release 1.1.0 (#509) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 60 ++++++++++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d0e833bb5..bcb062f08f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,66 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.1.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.0.0...v1.1.0) (2024-04-04) + + +### Features + +* (Series|DataFrame).explode ([#556](https://github.com/googleapis/python-bigquery-dataframes/issues/556)) ([9e32f57](https://github.com/googleapis/python-bigquery-dataframes/commit/9e32f570b42c8ddae0c9b281b25beff91f0c922c)) +* Add `DataFrame.eval` and `DataFrame.query` ([#361](https://github.com/googleapis/python-bigquery-dataframes/issues/361)) ([5e28ebd](https://github.com/googleapis/python-bigquery-dataframes/commit/5e28ebd1ba3a5559e093c2ea676c0714c1434ba9)) +* Add ColumnTransformer save/load ([#541](https://github.com/googleapis/python-bigquery-dataframes/issues/541)) ([9d8cf67](https://github.com/googleapis/python-bigquery-dataframes/commit/9d8cf6792a8dbe03e03b102c454d15fcde7986af)) +* Add ml.metrics.mean_squared_error ([#559](https://github.com/googleapis/python-bigquery-dataframes/issues/559)) ([853c25e](https://github.com/googleapis/python-bigquery-dataframes/commit/853c25e8023bf877f28cda4dade0694d0299a83e)) +* Add support for numpy expm1, log1p, floor, ceil, arctan2 ops ([#505](https://github.com/googleapis/python-bigquery-dataframes/issues/505)) ([e8e66cf](https://github.com/googleapis/python-bigquery-dataframes/commit/e8e66cf25887f64d2a7cb26081c2ef3cea10827d)) +* Add transformers save/load ([#552](https://github.com/googleapis/python-bigquery-dataframes/issues/552)) ([d805241](https://github.com/googleapis/python-bigquery-dataframes/commit/d805241b7ec99fcb7579dce778d4b04778a72002)) +* Allow DataFrame binary ops to align on either axis and with loc… ([#544](https://github.com/googleapis/python-bigquery-dataframes/issues/544)) ([6d8f3af](https://github.com/googleapis/python-bigquery-dataframes/commit/6d8f3afe28d39eb15b969f50d37c58a2c3ff1967)) +* Expose `DataFrame.bqclient` to assist in integrations ([#519](https://github.com/googleapis/python-bigquery-dataframes/issues/519)) ([0be8911](https://github.com/googleapis/python-bigquery-dataframes/commit/0be891191ed89be77494e4dcda30fb37836842ac)) +* Read_pandas accepts pandas Series and Index objects ([#573](https://github.com/googleapis/python-bigquery-dataframes/issues/573)) ([f8821fe](https://github.com/googleapis/python-bigquery-dataframes/commit/f8821fe7ecf8a80532a6aab98044fad601ff939c)) +* Support `ML.GENERATE_EMBEDDING` in `PaLM2TextEmbeddingGenerator` ([#539](https://github.com/googleapis/python-bigquery-dataframes/issues/539)) ([1156c1e](https://github.com/googleapis/python-bigquery-dataframes/commit/1156c1e3ce8c1e62898dbe68ccd6c5ab3cd4068f)) +* Support max_columns in repr and make repr more efficient ([#515](https://github.com/googleapis/python-bigquery-dataframes/issues/515)) ([54e49cf](https://github.com/googleapis/python-bigquery-dataframes/commit/54e49cff89bd329852a823cd5cf5c5b41b7f9e32)) + + +### Bug Fixes + +* Assign NaN scalar to column error. ([#513](https://github.com/googleapis/python-bigquery-dataframes/issues/513)) ([0a4153c](https://github.com/googleapis/python-bigquery-dataframes/commit/0a4153cc71a44c09b8d691897f1e5afa58c69f25)) +* Don't download 100gb onto local python machine in load test ([#537](https://github.com/googleapis/python-bigquery-dataframes/issues/537)) ([082c58b](https://github.com/googleapis/python-bigquery-dataframes/commit/082c58bbe76821b90337dc5af0ab5fa7515682c2)) +* Exclude list-like s parameter in plot.scatter ([#568](https://github.com/googleapis/python-bigquery-dataframes/issues/568)) ([1caac27](https://github.com/googleapis/python-bigquery-dataframes/commit/1caac27fe95ef3eb36bad2ac351090891922858c)) +* Fix case where df.peek would fail to execute even with force=True ([#511](https://github.com/googleapis/python-bigquery-dataframes/issues/511)) ([8eca99a](https://github.com/googleapis/python-bigquery-dataframes/commit/8eca99a03bc4bdaccf15a979b5382f3659f2aac5)) +* Fix error in `Series.drop(0)` ([#575](https://github.com/googleapis/python-bigquery-dataframes/issues/575)) ([75dd786](https://github.com/googleapis/python-bigquery-dataframes/commit/75dd7862e60502c97f7defe5dfefb044ea74bae8)) +* Include all names in MultiIndex repr ([#564](https://github.com/googleapis/python-bigquery-dataframes/issues/564)) ([b188146](https://github.com/googleapis/python-bigquery-dataframes/commit/b188146466780e6f7a041f51f5be51a7d60719c9)) +* Plot.scatter s parameter cannot accept float-like column ([#563](https://github.com/googleapis/python-bigquery-dataframes/issues/563)) ([8d39187](https://github.com/googleapis/python-bigquery-dataframes/commit/8d3918761a17649180aa806d7b01aa103f69b4fe)) +* Product operation produces float result for all input types ([#501](https://github.com/googleapis/python-bigquery-dataframes/issues/501)) ([6873b30](https://github.com/googleapis/python-bigquery-dataframes/commit/6873b30b691a11a368308825a72013d8ec1408ed)) +* Reloaded transformer .transform error ([#569](https://github.com/googleapis/python-bigquery-dataframes/issues/569)) ([39fe474](https://github.com/googleapis/python-bigquery-dataframes/commit/39fe47451d24a8cf55d7dbb15c6d3b176d25ab18)) +* Rename PaLM2TextEmbeddingGenerator.predict output columns to be backward compatible ([#561](https://github.com/googleapis/python-bigquery-dataframes/issues/561)) ([4995c00](https://github.com/googleapis/python-bigquery-dataframes/commit/4995c0046265463bc5c502cbeb34c7632d5a255e)) +* Respect hard stack size limit and swallow limit change exception. ([#558](https://github.com/googleapis/python-bigquery-dataframes/issues/558)) ([4833908](https://github.com/googleapis/python-bigquery-dataframes/commit/483390830ae0ee2fe0fb47dc7d2aea143b2dc7d8)) +* Restore string to date/time type coercion ([#565](https://github.com/googleapis/python-bigquery-dataframes/issues/565)) ([4ae0262](https://github.com/googleapis/python-bigquery-dataframes/commit/4ae0262a2b1dfc35c1e4c3392b9e21456d6e964e)) +* Sync the notebook with embedding changes ([#550](https://github.com/googleapis/python-bigquery-dataframes/issues/550)) ([347f2dd](https://github.com/googleapis/python-bigquery-dataframes/commit/347f2dda2298e17cd44a298f04a723f2d20c080a)) +* Use bytes limit on frame inlining rather than element count ([#576](https://github.com/googleapis/python-bigquery-dataframes/issues/576)) ([659a161](https://github.com/googleapis/python-bigquery-dataframes/commit/659a161a53e93f66334cd04d1c3dc1f1f47ecc16)) + + +### Performance Improvements + +* Add multi-query execution capability for complex dataframes ([#427](https://github.com/googleapis/python-bigquery-dataframes/issues/427)) ([d2d7e33](https://github.com/googleapis/python-bigquery-dataframes/commit/d2d7e33b1f8b4e184ef3e76eedbd673a8fcee60e)) + + +### Dependencies + +* Include `pyarrow` as a dependency ([#529](https://github.com/googleapis/python-bigquery-dataframes/issues/529)) ([9b1525a](https://github.com/googleapis/python-bigquery-dataframes/commit/9b1525a0c359455160bfbc0dc1366e37982ad01f)) + + +### Documentation + +* `bigframes.options.bigquery.project` and `location` are optional in some circumstances ([#548](https://github.com/googleapis/python-bigquery-dataframes/issues/548)) ([90bcec5](https://github.com/googleapis/python-bigquery-dataframes/commit/90bcec5c73f7eefeff14bbd8bdcad3a4c9d91d8f)) +* Add "Supported pandas APIs" reference to the documentation ([#542](https://github.com/googleapis/python-bigquery-dataframes/issues/542)) ([74c3915](https://github.com/googleapis/python-bigquery-dataframes/commit/74c391586280b55c35d66c697167122d72c13386)) +* Add General Availability banner to README ([#507](https://github.com/googleapis/python-bigquery-dataframes/issues/507)) ([262ff59](https://github.com/googleapis/python-bigquery-dataframes/commit/262ff5922643039e037bd9b6c0a91b5bd20a4e08)) +* Add opeartions in API docs ([#557](https://github.com/googleapis/python-bigquery-dataframes/issues/557)) ([ea95761](https://github.com/googleapis/python-bigquery-dataframes/commit/ea9576125d46f3912372f75ebe51196ba83e96db)) +* Add progress_bar code sample ([#508](https://github.com/googleapis/python-bigquery-dataframes/issues/508)) ([92a1af3](https://github.com/googleapis/python-bigquery-dataframes/commit/92a1af35b8de4afb6cdb5b5e89facdceb5c151d2)) +* Add the code samples for metrics{auc, roc_auc_score, roc_curve} ([#520](https://github.com/googleapis/python-bigquery-dataframes/issues/520)) ([5f37b09](https://github.com/googleapis/python-bigquery-dataframes/commit/5f37b0902fae2c099207acf3ce2e251c09ac889d)) +* Address more comments from technical writers to meet legal purposes ([#571](https://github.com/googleapis/python-bigquery-dataframes/issues/571)) ([9084df3](https://github.com/googleapis/python-bigquery-dataframes/commit/9084df369bc6819edf5f57ceba85667a14371ac5)) +* Fix docs of ARIMAPlus.predict ([#512](https://github.com/googleapis/python-bigquery-dataframes/issues/512)) ([3b80f95](https://github.com/googleapis/python-bigquery-dataframes/commit/3b80f956755c9d7043138aab6e5687cba50be8cb)) +* Include Index in table-of-contents ([#564](https://github.com/googleapis/python-bigquery-dataframes/issues/564)) ([b188146](https://github.com/googleapis/python-bigquery-dataframes/commit/b188146466780e6f7a041f51f5be51a7d60719c9)) +* Mark Gemini model as Pre-GA ([#543](https://github.com/googleapis/python-bigquery-dataframes/issues/543)) ([769868b](https://github.com/googleapis/python-bigquery-dataframes/commit/769868b9fc7dfff2e7b1ed5cec52a5dd3dfd6ff2)) +* Migrate the overview page to Bigframes official landing page ([#536](https://github.com/googleapis/python-bigquery-dataframes/issues/536)) ([a0fb8bb](https://github.com/googleapis/python-bigquery-dataframes/commit/a0fb8bbfddd07f1e0ef03eeb4be653d1e9f06772)) + ## [1.0.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v0.26.0...v1.0.0) (2024-03-25) diff --git a/bigframes/version.py b/bigframes/version.py index 8e31592250b..41a38955498 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" +__version__ = "1.1.0" From 2c1cd45707f539171a764a907f39ab692b25ed0c Mon Sep 17 00:00:00 2001 From: Stephanie A <129541811+DevStephanie@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:31:06 -0500 Subject: [PATCH 477/921] chore: add BigQuery locations to bigframes/constants.py (#578) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add BigQuery locations to bigframes/constants.py * Apply suggestions from code review --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/constants.py | 65 ++++++++++++++++++++++++++ tests/config.py | 72 ----------------------------- tests/system/large/test_location.py | 14 ++++-- 3 files changed, 75 insertions(+), 76 deletions(-) delete mode 100644 tests/config.py diff --git a/bigframes/constants.py b/bigframes/constants.py index a1ffd2b755c..07515010858 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -27,3 +27,68 @@ ABSTRACT_METHOD_ERROR_MESSAGE = f"Abstract method. You have likely encountered a bug. Please share this stacktrace and how you reached it with the BigQuery DataFrames team. {FEEDBACK_LINK}" DEFAULT_EXPIRATION = datetime.timedelta(days=7) + +# https://cloud.google.com/bigquery/docs/locations +ALL_BIGQUERY_LOCATIONS = frozenset( + { + "us-east5", + "us-south1", + "us-central1", + "us-west4", + "us-west2", + "northamerica-northeast1", + "us-east4", + "us-west1", + "us-west3", + "southamerica-east1", + "southamerica-west1", + "us-east1", + "northamerica-northeast2", + "asia-south2", + "asia-east2", + "asia-southeast2", + "australia-southeast2", + "asia-south1", + "asia-northeast2", + "asia-northeast3", + "asia-southeast1", + "australia-southeast1", + "asia-east1", + "asia-northeast1", + "europe-west1", + "europe-west10", + "europe-north1", + "europe-west3", + "europe-west2", + "europe-southwest1", + "europe-west8", + "europe-west4", + "europe-west9", + "europe-west12", + "europe-central2", + "europe-west6", + "me-central2", + "me-central1", + "me-west1", + "me-central2", + "me-central1", + "me-west1", + "africa-south1", + } +) + +# https://cloud.google.com/storage/docs/regional-endpoints +REP_ENABLED_BIGQUERY_LOCATIONS = frozenset( + { + "me-central2", + "europe-west9", + "europe-west3", + "us-east4", + "us-west1", + } +) + +# https://cloud.google.com/storage/docs/locational-endpoints +LEP_ENABLED_BIGQUERY_LOCATIONS = frozenset( + ALL_BIGQUERY_LOCATIONS - REP_ENABLED_BIGQUERY_LOCATIONS +) diff --git a/tests/config.py b/tests/config.py deleted file mode 100644 index a885d7e71df..00000000000 --- a/tests/config.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# https://cloud.google.com/bigquery/docs/locations -ALL_BIGQUERY_LOCATIONS = [ - "us-east5", - "us-south1", - "us-central1", - "us-west4", - "us-west2", - "northamerica-northeast1", - "us-east4", - "us-west1", - "us-west3", - "southamerica-east1", - "southamerica-west1", - "us-east1", - "northamerica-northeast2", - "asia-south2", - "asia-east2", - "asia-southeast2", - "australia-southeast2", - "asia-south1", - "asia-northeast2", - "asia-northeast3", - "asia-southeast1", - "australia-southeast1", - "asia-east1", - "asia-northeast1", - "europe-west1", - "europe-west10", - "europe-north1", - "europe-west3", - "europe-west2", - "europe-southwest1", - "europe-west8", - "europe-west4", - "europe-west9", - "europe-west12", - "europe-central2", - "europe-west6", - "me-central2", - "me-central1", - "me-west1", - "me-central2", - "me-central1", - "me-west1", - "africa-south1", -] - -REP_ENABLED_BIGQUERY_LOCATIONS = [ - "me-central2", - "europe-west9", - "europe-west3", - "us-east4", - "us-west1", -] - -LEP_ENABLED_BIGQUERY_LOCATIONS = sorted( - set(ALL_BIGQUERY_LOCATIONS) - set(REP_ENABLED_BIGQUERY_LOCATIONS) -) diff --git a/tests/system/large/test_location.py b/tests/system/large/test_location.py index a4cf8919a02..204c6b7463b 100644 --- a/tests/system/large/test_location.py +++ b/tests/system/large/test_location.py @@ -18,8 +18,8 @@ import pytest import bigframes +import bigframes.constants import bigframes.session.clients -from tests import config def _assert_bq_execution_location(session: bigframes.Session): @@ -66,7 +66,11 @@ def test_bq_location_default(): _assert_bq_execution_location(session) -@pytest.mark.parametrize("bigquery_location", config.ALL_BIGQUERY_LOCATIONS) +@pytest.mark.parametrize( + "bigquery_location", + # Sort the set to avoid nondeterminism. + sorted(bigframes.constants.ALL_BIGQUERY_LOCATIONS), +) def test_bq_location(bigquery_location): session = bigframes.Session( context=bigframes.BigQueryOptions(location=bigquery_location) @@ -85,7 +89,8 @@ def test_bq_location(bigquery_location): @pytest.mark.parametrize( "bigquery_location", - config.REP_ENABLED_BIGQUERY_LOCATIONS, + # Sort the set to avoid nondeterminism. + sorted(bigframes.constants.REP_ENABLED_BIGQUERY_LOCATIONS), ) def test_bq_rep_endpoints(bigquery_location): session = bigframes.Session( @@ -108,7 +113,8 @@ def test_bq_rep_endpoints(bigquery_location): @pytest.mark.parametrize( "bigquery_location", - config.LEP_ENABLED_BIGQUERY_LOCATIONS, + # Sort the set to avoid nondeterminism. + sorted(bigframes.constants.LEP_ENABLED_BIGQUERY_LOCATIONS), ) def test_bq_lep_endpoints(bigquery_location): # We are not testing BigFrames Session for LEP endpoints because it involves From c8da22ff951dd9ef65948bd56dbb7970d80d3018 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:14:16 -0700 Subject: [PATCH 478/921] chore: pin noxfile to the earlier plugin version (#583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …e docs generation Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 4ac3a817236..6fa7dbead1e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -505,7 +505,7 @@ def docfx(session): SPHINX_VERSION, "alabaster", "recommonmark", - "gcp-sphinx-docfx-yaml", + "gcp-sphinx-docfx-yaml==3.0.1", ) shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) From 57e1cca42190f603eb248916e241f6acf6b9b549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 5 Apr 2024 15:15:01 -0500 Subject: [PATCH 479/921] chore: add context and address some TODOs in noxfile (#572) * chore: add context and address some TODOs in noxfile * fix lint * remove dead code * mark test_query_job_dry_run as flaky * remove more dead code * escape [ * remove failing test * missing NO COVER --- noxfile.py | 11 ++-- .../system/small/operations/test_plotting.py | 11 ++-- tests/system/small/test_dataframe.py | 10 ---- tests/system/small/test_dataframe_io.py | 43 +++++--------- tests/system/small/test_encryption.py | 36 ++++++------ tests/system/small/test_multiindex.py | 19 ------- tests/system/small/test_pandas.py | 4 +- tests/system/small/test_progress_bar.py | 10 ---- tests/system/small/test_remote_function.py | 30 ++++++---- tests/system/small/test_series.py | 56 ------------------- tests/system/small/test_session.py | 10 +--- .../bigframes_vendored/cpython/_pprint.py | 1 + .../bigframes_vendored/pandas/core/generic.py | 21 ++++--- 13 files changed, 81 insertions(+), 181 deletions(-) diff --git a/noxfile.py b/noxfile.py index 6fa7dbead1e..fa9c0a57d89 100644 --- a/noxfile.py +++ b/noxfile.py @@ -112,8 +112,7 @@ def lint(session): "--check", *LINT_PATHS, ) - # TODO(tswast): lint all LINT_PATHS - session.run("flake8", "bigframes", "tests") + session.run("flake8", *LINT_PATHS) @nox.session(python=DEFAULT_PYTHON_VERSION) @@ -411,8 +410,8 @@ def samples(session): CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) - # TODO(swast): Use `requirements.txt` files from the samples directories to - # test samples. + # TODO(b/332735129): Remove this session and use python_samples templates + # where each samples directory has its own noxfile.py file, instead. install_test_extra = True install_systemtest_dependencies(session, install_test_extra, "-c", constraints_path) @@ -434,12 +433,12 @@ def cover(session): session.run("coverage", "report", "--show-missing", "--fail-under=90") # Make sure there is no dead code in our test directories. - # TODO(swast): Cleanup dead code in the system tests directory. session.run( "coverage", "report", "--show-missing", "--include=tests/unit/*", + "--include=tests/system/small/*", "--fail-under=100", ) @@ -714,7 +713,7 @@ def notebook(session: nox.Session): "notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb", # Needs DATASET. "notebooks/regression/bq_dataframes_ml_linear_regression.ipynb", # Needs DATASET_ID. "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", # Needs CONNECTION. - # TODO(swast): investigate why we get 404 errors, even though + # TODO(b/332737009): investigate why we get 404 errors, even though # bq_dataframes_llm_code_generation creates a bucket in the sample. "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", # Needs BUCKET_URI. "notebooks/generative_ai/sentiment_analysis.ipynb", # Too slow diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index 6542ce6de34..faf7cb7e6bf 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -27,13 +27,10 @@ def _check_legend_labels(ax, labels): """ assert ax.get_legend() is not None texts = ax.get_legend().get_texts() - if not isinstance(texts, list): - assert texts.get_text() == labels - else: - actual_labels = [t.get_text() for t in texts] - assert len(actual_labels) == len(labels) - for label, e in zip(actual_labels, labels): - assert label == e + actual_labels = [t.get_text() for t in texts] + assert len(actual_labels) == len(labels) + for label, e in zip(actual_labels, labels): + assert label == e def test_series_hist_bins(scalars_dfs): diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 5d6a859c112..0811defbc1b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -524,13 +524,6 @@ def test_repr_w_all_rows(scalars_dfs): scalars_df = scalars_df.drop(columns=["numeric_col"]) scalars_pandas_df = scalars_pandas_df.drop(columns=["numeric_col"]) - if scalars_pandas_df.index.name is None: - # Note: Not quite the same as no index / default index, but hopefully - # simulates it well enough while being consistent enough for string - # comparison to work. - scalars_df = scalars_df.set_index("rowindex", drop=False).sort_index() - scalars_df.index.name = None - # When there are 10 or fewer rows, the outputs should be identical. actual = repr(scalars_df.head(10)) @@ -3956,9 +3949,6 @@ def test_df_value_counts(scalars_dfs, subset, normalize, ascending, dropna): ("bottom", "dense", False, False), ], ) -@pytest.mark.skipif( - True, reason="Blocked by possible pandas rank() regression (b/283278923)" -) def test_df_rank_with_nulls( scalars_df_index, scalars_pandas_df_index, diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 10d74087903..f26902f0844 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -23,7 +23,8 @@ try: import pandas_gbq # type: ignore -except ImportError: +except ImportError: # pragma: NO COVER + # TODO(b/332758806): Run system tests without "extras" pandas_gbq = None import typing @@ -129,12 +130,9 @@ def test_to_csv_index( """Test the `to_csv` API with the `index` parameter.""" scalars_df, scalars_pandas_df = scalars_dfs index_col = None - if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_csv_index_{index}*.csv" - if index: - index_col = typing.cast(str, scalars_df.index.name) - else: - path = gcs_folder + f"test_default_index_df_to_csv_index_{index}*.csv" + path = gcs_folder + f"test_index_df_to_csv_index_{index}*.csv" + if index: + index_col = typing.cast(str, scalars_df.index.name) # TODO(swast): Support "date_format" parameter and make sure our # DATETIME/TIMESTAMP column export is the same format as pandas by default. @@ -386,11 +384,8 @@ def test_to_json_index_invalid_orient( gcs_folder: str, index: bool, ): - scalars_df, scalars_pandas_df = scalars_dfs - if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_json_index_{index}*.jsonl" - else: - path = gcs_folder + f"test_default_index_df_to_json_index_{index}*.jsonl" + scalars_df, _ = scalars_dfs + path = gcs_folder + f"test_index_df_to_json_index_{index}*.jsonl" with pytest.raises(ValueError): scalars_df.to_json(path, index=index, lines=True) @@ -404,11 +399,8 @@ def test_to_json_index_invalid_lines( gcs_folder: str, index: bool, ): - scalars_df, scalars_pandas_df = scalars_dfs - if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_json_index_{index}.jsonl" - else: - path = gcs_folder + f"test_default_index_df_to_json_index_{index}.jsonl" + scalars_df, _ = scalars_dfs + path = gcs_folder + f"test_index_df_to_json_index_{index}.jsonl" with pytest.raises(NotImplementedError): scalars_df.to_json(path, index=index) @@ -422,14 +414,13 @@ def test_to_json_index_records_orient( gcs_folder: str, index: bool, ): - """Test the `to_json` API with the `index` parameter.""" + """Test the `to_json` API with the `index` parameter. + + Uses the scalable options orient='records' and lines=True. + """ scalars_df, scalars_pandas_df = scalars_dfs - if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_json_index_{index}*.jsonl" - else: - path = gcs_folder + f"test_default_index_df_to_json_index_{index}*.jsonl" + path = gcs_folder + f"test_index_df_to_json_index_{index}*.jsonl" - """ Test the `to_json` API with `orient` is `records` and `lines` is True""" scalars_df.to_json(path, index=index, orient="records", lines=True) gcs_df = pd.read_json( @@ -460,11 +451,7 @@ def test_to_parquet_index(scalars_dfs, gcs_folder, index): """Test the `to_parquet` API with the `index` parameter.""" scalars_df, scalars_pandas_df = scalars_dfs scalars_pandas_df = scalars_pandas_df.copy() - - if scalars_df.index.name is not None: - path = gcs_folder + f"test_index_df_to_parquet_{index}*.parquet" - else: - path = gcs_folder + f"test_default_index_df_to_parquet_{index}*.parquet" + path = gcs_folder + f"test_index_df_to_parquet_{index}*.parquet" # TODO(b/268693993): Type GEOGRAPHY is not currently supported for parquet. scalars_df = scalars_df.drop(columns="geography_col") diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 3389e5cd68b..eae667dc9d7 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -64,8 +64,8 @@ def _assert_bq_table_is_encrypted( def test_session_query_job(bq_cmek, session_with_bq_cmek): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER _, query_job = session_with_bq_cmek._start_query( "SELECT 123", job_config=bigquery.QueryJobConfig(use_query_cache=False) @@ -82,8 +82,8 @@ def test_session_query_job(bq_cmek, session_with_bq_cmek): def test_session_load_job(bq_cmek, session_with_bq_cmek): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + 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 = bigframes.session._io.bigquery.random_table( @@ -114,8 +114,8 @@ def test_session_load_job(bq_cmek, session_with_bq_cmek): def test_read_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER # Read the BQ table df = session_with_bq_cmek.read_gbq(scalars_table_id) @@ -125,8 +125,8 @@ def test_read_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): def test_df_apis(bq_cmek, session_with_bq_cmek, scalars_table_id): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER # Read a BQ table and assert encryption df = session_with_bq_cmek.read_gbq(scalars_table_id) @@ -152,8 +152,8 @@ def test_df_apis(bq_cmek, session_with_bq_cmek, scalars_table_id): def test_read_csv_gcs( bq_cmek, session_with_bq_cmek, scalars_df_index, gcs_folder, engine ): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER # Create a csv in gcs write_path = gcs_folder + "test_read_csv_gcs_bigquery_engine*.csv" @@ -170,8 +170,8 @@ def test_read_csv_gcs( def test_to_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER # Read a BQ table and assert encryption df = session_with_bq_cmek.read_gbq(scalars_table_id) @@ -205,8 +205,8 @@ def test_to_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): def test_read_pandas(bq_cmek, session_with_bq_cmek): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER # Read a pandas dataframe df = session_with_bq_cmek.read_pandas(pandas.DataFrame([1])) @@ -216,8 +216,8 @@ def test_read_pandas(bq_cmek, session_with_bq_cmek): def test_read_pandas_large(bq_cmek, session_with_bq_cmek): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER # Read a pandas dataframe large enough to trigger a BQ load job df = session_with_bq_cmek.read_pandas(pandas.DataFrame(range(10_000))) @@ -227,8 +227,8 @@ def test_read_pandas_large(bq_cmek, session_with_bq_cmek): def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): - if not bq_cmek: - pytest.skip("no cmek set for testing") + if not bq_cmek: # pragma: NO COVER + pytest.skip("no cmek set for testing") # pragma: NO COVER model = bigframes.ml.linear_model.LinearRegression() df = session_with_bq_cmek.read_gbq(penguins_table_id).dropna() diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 6aca7628cfd..330fe44eb85 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -882,25 +882,6 @@ def test_column_multi_index_unstack(scalars_df_index, scalars_pandas_df_index): pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) -@pytest.mark.skip(reason="Pandas fails in newer versions.") -def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index): - columns = ["int64_too", "int64_col", "rowindex_2"] - level1 = pandas.Index(["b", pandas.NA, pandas.NA]) - # Need resulting column to be pyarrow string rather than object dtype - level2 = pandas.Index([pandas.NA, "b", "b"], dtype="string[pyarrow]") - multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) - bf_df = scalars_df_index[columns].copy() - bf_df.columns = multi_columns - pd_df = scalars_pandas_df_index[columns].copy() - pd_df.columns = multi_columns - - bf_result = bf_df.stack().to_pandas() - pd_result = pd_df.stack() - - # Pandas produces NaN, where bq dataframes produces pd.NA - pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) - - def test_corr_w_multi_index(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "float64_col", "int64_col"] multi_columns = pandas.MultiIndex.from_tuples(zip(["a", "b", "b"], [1, 2, 2])) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index a080a969c87..95b34a56c5b 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -136,7 +136,7 @@ def test_get_dummies_series(scalars_dfs): # adjust for expected dtype differences for (column_name, type_name) in zip(pd_result.columns, pd_result.dtypes): - if type_name == "bool": + if type_name == "bool": # pragma: NO COVER pd_result[column_name] = pd_result[column_name].astype("boolean") pd_result.columns = pd_result.columns.astype(object) @@ -157,7 +157,7 @@ def test_get_dummies_series_nameless(scalars_dfs): # adjust for expected dtype differences for (column_name, type_name) in zip(pd_result.columns, pd_result.dtypes): - if type_name == "bool": + if type_name == "bool": # pragma: NO COVER pd_result[column_name] = pd_result[column_name].astype("boolean") pd_result.columns = pd_result.columns.astype(object) diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index ea139b9802a..5ccc6db0ac9 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -126,13 +126,3 @@ def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): ] for string in string_checks: assert string in query_job_repr - - -def test_query_job_dry_run(penguins_df_default_index: bf.dataframe.DataFrame, capsys): - with bf.option_context("display.repr_mode", "deferred"): - repr(penguins_df_default_index) - repr(penguins_df_default_index["body_mass_g"]) - lines = capsys.readouterr().out.split("\n") - lines = filter(None, lines) - for line in lines: - assert "Computation deferred. Computation will process" in line diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index e7e434dbd05..106638cef3c 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -105,7 +105,8 @@ def test_remote_function_direct_no_session_param( reuse=True, ) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER assert square.bigframes_remote_function assert square.bigframes_cloud_function @@ -157,7 +158,8 @@ def test_remote_function_direct_no_session_param_location_specified( reuse=True, ) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER scalars_df, scalars_pandas_df = scalars_dfs @@ -207,7 +209,8 @@ def test_remote_function_direct_no_session_param_location_mismatched( reuse=True, ) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER @pytest.mark.flaky(retries=2, delay=120) @@ -233,7 +236,8 @@ def test_remote_function_direct_no_session_param_location_project_specified( reuse=True, ) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER scalars_df, scalars_pandas_df = scalars_dfs @@ -283,7 +287,8 @@ def test_remote_function_direct_no_session_param_project_mismatched( reuse=True, ) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER @pytest.mark.flaky(retries=2, delay=120) @@ -294,7 +299,8 @@ def test_remote_function_direct_session_param(session_with_bq_connection, scalar session=session_with_bq_connection, ) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER scalars_df, scalars_pandas_df = scalars_dfs @@ -331,7 +337,8 @@ def test_remote_function_via_session_default(session_with_bq_connection, scalars # cloud function would be common and quickly reused. @session_with_bq_connection.remote_function([int], int) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER scalars_df, scalars_pandas_df = scalars_dfs @@ -370,7 +377,8 @@ def test_remote_function_via_session_with_overrides( reuse=True, ) def square(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER scalars_df, scalars_pandas_df = scalars_dfs @@ -497,7 +505,8 @@ def test_skip_bq_connection_check(dataset_id_permanent): @session.remote_function([int], int, dataset=dataset_id_permanent) def add_one(x): - return x + 1 + # This executes on a remote function, where coverage isn't tracked. + return x + 1 # pragma: NO COVER @pytest.mark.flaky(retries=2, delay=120) @@ -534,7 +543,8 @@ def test_read_gbq_function_like_original( reuse=True, ) def square1(x): - return x * x + # This executes on a remote function, where coverage isn't tracked. + return x * x # pragma: NO COVER square2 = rf.read_gbq_function( function_name=square1.bigframes_remote_function, diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index e350286940b..539b45eb6a6 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1276,8 +1276,6 @@ def test_numeric_literal(scalars_dfs): def test_repr(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - if scalars_pandas_df.index.name != "rowindex": - pytest.skip("Require index & ordering for consistent repr.") col_name = "int64_col" bf_series = scalars_df[col_name] @@ -1405,8 +1403,6 @@ def test_groupby_level_sum(scalars_dfs): # TODO(tbergeron): Use a non-unique index once that becomes possible in tests scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - if scalars_pandas_df.index.name != "rowindex": - pytest.skip("Require index for groupby level.") bf_series = scalars_df[col_name].groupby(level=0).sum() pd_series = scalars_pandas_df[col_name].groupby(level=0).sum() @@ -1421,8 +1417,6 @@ def test_groupby_level_list_sum(scalars_dfs): # TODO(tbergeron): Use a non-unique index once that becomes possible in tests scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - if scalars_pandas_df.index.name != "rowindex": - pytest.skip("Require index for groupby level.") bf_series = scalars_df[col_name].groupby(level=["rowindex"]).sum() pd_series = scalars_pandas_df[col_name].groupby(level=["rowindex"]).sum() @@ -1710,9 +1704,6 @@ def test_dtypes(scalars_dfs): def test_head(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - if scalars_df.index.name is None: - pytest.skip("Require explicit index for offset ops.") - bf_result = scalars_df["string_col"].head(2).to_pandas() pd_result = scalars_pandas_df["string_col"].head(2) @@ -1725,9 +1716,6 @@ def test_head(scalars_dfs): def test_tail(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - if scalars_df.index.name is None: - pytest.skip("Require explicit index for offset ops.") - bf_result = scalars_df["string_col"].tail(2).to_pandas() pd_result = scalars_pandas_df["string_col"].tail(2) @@ -1740,9 +1728,6 @@ def test_tail(scalars_dfs): def test_head_then_scalar_operation(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - if scalars_df.index.name is None: - pytest.skip("Require explicit index for offset ops.") - bf_result = (scalars_df["float64_col"].head(1) + 4).to_pandas() pd_result = scalars_pandas_df["float64_col"].head(1) + 4 @@ -1755,9 +1740,6 @@ def test_head_then_scalar_operation(scalars_dfs): def test_head_then_series_operation(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - if scalars_df.index.name is None: - pytest.skip("Require explicit index for offset ops.") - bf_result = ( scalars_df["float64_col"].head(4) + scalars_df["float64_col"].head(2) ).to_pandas() @@ -1841,44 +1823,6 @@ def test_cumsum_int_ordered(scalars_df_index, scalars_pandas_df_index): ) -@pytest.mark.parametrize( - ("na_option",), - [ - ("keep",), - ("top",), - ("bottom",), - ], -) -@pytest.mark.parametrize( - ("method",), - [ - ("average",), - ("min",), - ("max",), - ("first",), - ("dense",), - ], -) -@pytest.mark.skipif( - True, reason="Blocked by possible pandas rank() regression (b/283278923)" -) -def test_rank_with_nulls(scalars_df_index, scalars_pandas_df_index, na_option, method): - col_name = "bool_col" - bf_result = ( - scalars_df_index[col_name].rank(na_option=na_option, method=method).to_pandas() - ) - pd_result = ( - scalars_pandas_df_index[col_name] - .rank(na_option=na_option, method=method) - .astype(pd.Float64Dtype()) - ) - - pd.testing.assert_series_equal( - bf_result, - pd_result, - ) - - @pytest.mark.parametrize( ("keep",), [ diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index eb6a0a8dd93..d84244e5cfb 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -493,10 +493,7 @@ def test_read_pandas_tokyo( @utils.skip_legacy_pandas def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs - if scalars_df.index.name is not None: - path = gcs_folder + "test_read_csv_gcs_default_engine_w_index*.csv" - else: - path = gcs_folder + "test_read_csv_gcs_default_engine_wo_index*.csv" + path = gcs_folder + "test_read_csv_gcs_default_engine_w_index*.csv" read_path = utils.get_first_file_from_wildcard(path) scalars_df.to_csv(path, index=False) dtype = scalars_df.dtypes.to_dict() @@ -520,10 +517,7 @@ def test_read_csv_gcs_default_engine(session, scalars_dfs, gcs_folder): def test_read_csv_gcs_bq_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs - if scalars_df.index.name is not None: - path = gcs_folder + "test_read_csv_gcs_bq_engine_w_index*.csv" - else: - path = gcs_folder + "test_read_csv_gcs_bq_engine_wo_index*.csv" + path = gcs_folder + "test_read_csv_gcs_bq_engine_w_index*.csv" scalars_df.to_csv(path, index=False) df = session.read_csv(path, engine="bigquery") diff --git a/third_party/bigframes_vendored/cpython/_pprint.py b/third_party/bigframes_vendored/cpython/_pprint.py index 617c14df0d9..9b586c939bd 100644 --- a/third_party/bigframes_vendored/cpython/_pprint.py +++ b/third_party/bigframes_vendored/cpython/_pprint.py @@ -110,6 +110,7 @@ def has_changed(k, v): # try to avoid calling repr on nested estimators if isinstance(v, BaseEstimator) and v.__class__ != init_params[k].__class__: return True + # Use repr as a last resort. It may be expensive. def is_scalar_nan(x): return isinstance(x, numbers.Real) and math.isnan(x) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 04cc3990a4e..d14cbfaa52f 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -165,13 +165,20 @@ def astype(self, dtype): Args: dtype (str or pandas.ExtensionDtype): - A dtype supported by BigQuery DataFrame include ``'boolean'``, ``'Float64'``, ``'Int64'``, - ``'int64[pyarrow]'``, ``'string'``, ``'string[pyarrow]'``, ``'timestamp[us, tz=UTC][pyarrow]'``, - ``'timestamp\[us\]\[pyarrow\]'``, ``'date32\[day\]\[pyarrow\]'``, ``'time64\[us\]\[pyarrow\]'``. - A pandas.ExtensionDtype include ``pandas.BooleanDtype()``, ``pandas.Float64Dtype()``, - ``pandas.Int64Dtype()``, ``pandas.StringDtype(storage="pyarrow")``, - ``pd.ArrowDtype(pa.date32())``, ``pd.ArrowDtype(pa.time64("us"))``, - ``pd.ArrowDtype(pa.timestamp("us"))``, ``pd.ArrowDtype(pa.timestamp("us", tz="UTC"))``. + A dtype supported by BigQuery DataFrame include ``'boolean'``, + ``'Float64'``, ``'Int64'``, ``'int64\\[pyarrow\\]'``, + ``'string'``, ``'string\\[pyarrow\\]'``, + ``'timestamp\\[us, tz=UTC\\]\\[pyarrow\\]'``, + ``'timestamp\\[us\\]\\[pyarrow\\]'``, + ``'date32\\[day\\]\\[pyarrow\\]'``, + ``'time64\\[us\\]\\[pyarrow\\]'``. + A pandas.ExtensionDtype include ``pandas.BooleanDtype()``, + ``pandas.Float64Dtype()``, ``pandas.Int64Dtype()``, + ``pandas.StringDtype(storage="pyarrow")``, + ``pd.ArrowDtype(pa.date32())``, + ``pd.ArrowDtype(pa.time64("us"))``, + ``pd.ArrowDtype(pa.timestamp("us"))``, + ``pd.ArrowDtype(pa.timestamp("us", tz="UTC"))``. Returns: same type as caller From 5f1db8b270b32ab366be3690761da137d9fe65f5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 5 Apr 2024 13:50:46 -0700 Subject: [PATCH 480/921] fix: Inverting int now does bitwise inversion rather than sign flip (#574) --- bigframes/core/compile/scalar_op_compiler.py | 2 +- tests/system/small/test_series.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 5c165fa1df4..53a25d63ed0 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -397,7 +397,7 @@ def expm1_op_impl(x: ibis_types.Value): @scalar_op_compiler.register_unary_op(ops.invert_op) def invert_op_impl(x: ibis_types.Value): - return typing.cast(ibis_types.NumericValue, x).negate() + return x.__invert__() ## String Operation diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 539b45eb6a6..c8826775081 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -284,6 +284,21 @@ def test_abs(scalars_dfs, col_name): assert_series_equal(pd_result, bf_result) +@pytest.mark.parametrize( + ("col_name",), + ( + ("bool_col",), + ("int64_col",), + ), +) +def test_series_invert(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = (~scalars_df[col_name]).to_pandas() + pd_result = ~scalars_pandas_df[col_name] + + assert_series_equal(pd_result, bf_result) + + def test_fillna(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" From c8b137b4340c53454df9da67fa772e21ed704ea8 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:59:33 -0700 Subject: [PATCH 481/921] chore: reorganize the supported pandas apis page (#584) --- docs/templates/toc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 3c2c688d782..72d135cc965 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -12,6 +12,8 @@ name: Methods - href: summary_property.html name: Properties and Attributes + - href: supported_pandas_apis.html + name: Supported pandas APIs name: BigQuery DataFrames API - items: - items: @@ -79,8 +81,6 @@ name: Series - name: Window uid: bigframes.core.window.Window - - href: supported_pandas_apis.html - name: Supported pandas APIs name: bigframes.pandas - items: - items: From 4b08d9243272229f71688152dbeb69d0ab7c68b4 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:19:44 -0700 Subject: [PATCH 482/921] fix: address more technical writers feedback (#581) --- bigframes/_config/compute_options.py | 4 ++-- bigframes/ml/base.py | 2 +- bigframes/ml/cluster.py | 2 +- bigframes/ml/decomposition.py | 2 +- bigframes/ml/ensemble.py | 16 ++++++------- bigframes/ml/forecasting.py | 4 ++-- bigframes/ml/imported.py | 24 +++++++++---------- bigframes/ml/linear_model.py | 4 ++-- bigframes/ml/llm.py | 6 ++--- .../pandas/core/config_init.py | 4 ++-- .../pandas/core/groupby/__init__.py | 6 ++--- .../pandas/core/indexes/base.py | 10 ++++---- .../bigframes_vendored/pandas/core/series.py | 2 +- .../bigframes_vendored/sklearn/base.py | 6 ++--- .../sklearn/decomposition/_pca.py | 6 ++--- 15 files changed, 49 insertions(+), 49 deletions(-) diff --git a/bigframes/_config/compute_options.py b/bigframes/_config/compute_options.py index 2b849c558ab..81ef044f4d8 100644 --- a/bigframes/_config/compute_options.py +++ b/bigframes/_config/compute_options.py @@ -21,7 +21,7 @@ @dataclasses.dataclass class ComputeOptions: """ - Encapsulates configuration for compute options. + Encapsulates the configuration for compute options. **Examples:** @@ -39,7 +39,7 @@ class ComputeOptions: Limits the bytes billed for query jobs. Queries that will have 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 `_. + 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 diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index c57cb78791a..6c81b66e55d 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -193,7 +193,7 @@ def to_gbq(self: _T, model_name: str, replace: bool = False) -> _T: model_name (str): The name of the model. replace (bool, default False): - Whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: Saved transformer.""" diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index 1035def54db..e63764e7bb3 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -177,7 +177,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> KMeans: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: KMeans: saved model.""" diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 475b4a046fa..0dfb46efaaa 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -171,7 +171,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> PCA: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: PCA: saved model.""" diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index a8f0329145b..b248c295f4c 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -192,9 +192,9 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBRegressor: model_name (str): The name of the model. replace (bool, default False): - Whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. - Returns: saved model.""" + Returns: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") @@ -345,10 +345,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBClassifier: model_name (str): The name of the model. replace (bool, default False): - Whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: - XGBClassifier: saved model.""" + XGBClassifier: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") @@ -508,10 +508,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestRegresso model_name (str): The name of the model. replace (bool, default False): - Whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: - RandomForestRegressor: saved model.""" + RandomForestRegressor: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") @@ -671,10 +671,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> RandomForestClassifi model_name (str): The name of the model. replace (bool, default False): - Whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: - RandomForestClassifier: saved model.""" + RandomForestClassifier: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index e50a8ed35b9..a7e0c3c0d90 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -363,10 +363,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> ARIMAPlus: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: - ARIMAPlus: saved model.""" + ARIMAPlus: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index b5511500500..9198b4eafb0 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -32,7 +32,7 @@ class TensorFlowModel(base.Predictor): Args: model_path (str): - GCS path that holds the model files. + Cloud Storage path that holds the model files. session (BigQuery Session): BQ session to create the model. """ @@ -69,10 +69,10 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: Args: X (bigframes.dataframe.DataFrame): - Input DataFrame, schema is defined by the model. + Input DataFrame. Schema is defined by the model. Returns: - bigframes.dataframe.DataFrame: Output DataFrame, schema is defined by the model.""" + bigframes.dataframe.DataFrame: Output DataFrame. Schema is defined by the model.""" if not self._bqml_model: if self.model_path is None: @@ -91,10 +91,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> TensorFlowModel: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Default to False. Returns: - TensorFlowModel: saved model.""" + TensorFlowModel: Saved model.""" if not self._bqml_model: if self.model_path is None: raise ValueError("Model GCS path must be provided.") @@ -146,7 +146,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame or Series, schema is defined by the model. + Input DataFrame or Series. Schema is defined by the model. Returns: bigframes.dataframe.DataFrame: Output DataFrame, schema is defined by the model.""" @@ -168,10 +168,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> ONNXModel: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: - ONNXModel: saved model.""" + ONNXModel: Saved model.""" if not self._bqml_model: if self.model_path is None: raise ValueError("Model GCS path must be provided.") @@ -262,10 +262,10 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame or Series, schema is defined by the model. + Input DataFrame or Series. Schema is defined by the model. Returns: - bigframes.dataframe.DataFrame: Output DataFrame, schema is defined by the model.""" + bigframes.dataframe.DataFrame: Output DataFrame. Schema is defined by the model.""" if not self._bqml_model: if self.model_path is None: @@ -284,10 +284,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBoostModel: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: - XGBoostModel: saved model.""" + XGBoostModel: Saved model.""" if not self._bqml_model: if self.model_path is None: raise ValueError("Model GCS path must be provided.") diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index c0abe77b9f7..63462be09f5 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -184,7 +184,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: LinearRegression: saved model.""" @@ -349,7 +349,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LogisticRegression: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: LogisticRegression: saved model.""" diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index ffaeb399bbc..31c691fd515 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -248,7 +248,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> PaLM2TextGenerator: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: PaLM2TextGenerator: saved model.""" @@ -415,7 +415,7 @@ def to_gbq( model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: PaLM2TextEmbeddingGenerator: saved model.""" @@ -595,7 +595,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> GeminiTextGenerator: model_name (str): the name of the model. replace (bool, default False): - whether to replace if the model already exists. Default to False. + Determine whether to replace if the model already exists. Default to False. Returns: GeminiTextGenerator: saved model.""" diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index a3178e27618..84ab90a322d 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -13,7 +13,7 @@ from __future__ import annotations display_options_doc = """ -Encapsulates configuration for displaying objects. +Encapsulates the configuration for displaying objects. **Examples:** @@ -79,7 +79,7 @@ """ sampling_options_doc = """ -Encapsulates configuration for data sampling. +Encapsulates the configuration for data sampling. Attributes: max_download_size (int, default 500): diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index e1cc8c5a538..ed4ca66f385 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -24,7 +24,7 @@ def any(self): Returns: Series or DataFrame: DataFrame or Series of boolean values, where a value is True if any element is True within its - respective group, False otherwise. + respective group; otherwise False. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -35,7 +35,7 @@ def all(self): Returns: Series or DataFrame: DataFrame or Series of boolean values, where a value is True if all elements are True within its - respective group, False otherwise. + respective group; otherwise False. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -339,7 +339,7 @@ def expanding(self, *args, **kwargs): Provides expanding functionality. Returns: - Series or DataFrame: A expanding grouper, providing expanding functionality per group. + Series or DataFrame: An expanding grouper, providing expanding functionality per group. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index 7f5761e45b9..eb6b9161fc2 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -83,7 +83,7 @@ def copy( name (Label, optional): Set name for new object. Returns: - Index: Index refer to new object which is a copy of this object. + Index: Index reference to new object, which is a copy of this object. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -148,7 +148,7 @@ def isin(self, values): """ Return a boolean array where the index values are in `values`. - Compute boolean array of whether each index value is found in the + Compute boolean array to check whether each index value is found in the passed set of values. The length of the returned boolean array matches the length of the index. @@ -195,7 +195,7 @@ def max(self): def argmin(self) -> int: """ - Return int position of the smallest value in the Series. + Return int position of the smallest value in the series. If the minimum is achieved in multiple locations, the first row position is returned. @@ -264,7 +264,7 @@ def value_counts( Args: normalize (bool, default False): - If True then the object returned will contain the relative + If True, then the object returned will contain the relative frequencies of the unique values. sort (bool, default True): Sort by frequencies. @@ -316,7 +316,7 @@ def drop(self, labels) -> Index: labels (array-like or scalar): Returns: - Index: Will be same type as self + Index: Will be same type as self. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 785755a5620..5426e434b38 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -2836,7 +2836,7 @@ def unstack(self, level): def argmax(self): """ - Return int position of the smallest value in the Series. + Return int position of the smallest value in the series. If the minimum is achieved in multiple locations, the first row position is returned. diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index 768328e5529..fd8db7a2270 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -81,9 +81,9 @@ class ClassifierMixin: def score(self, X, y): """Return the mean accuracy on the given test data and labels. - In multi-label classification, this is the subset accuracy - which is a harsh metric since you require for each sample that - each label set be correctly predicted. + In multi-label classification, this is the subset accuracy, + which is a harsh metric since you require that + each label set be correctly predicted for each sample. .. note:: diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index f126e0439db..71e53bf4a99 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -22,7 +22,7 @@ class PCA(BaseEstimator, metaclass=ABCMeta): Args: n_components (int, float or None, default None): - Number of components to keep. If n_components is not set all + Number of components to keep. If n_components is not set, all components are kept, n_components = min(n_samples, n_features). If 0 < n_components < 1, select the number of components such that the amount of variance that needs to be explained is greater than the percentage specified by n_components. svd_solver ("full", "randomized" or "auto", default "auto"): @@ -75,7 +75,7 @@ def predict(self, X): Series or a DataFrame to predict. Returns: - bigframes.dataframe.DataFrame: predicted DataFrames.""" + bigframes.dataframe.DataFrame: Predicted DataFrames.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property @@ -90,7 +90,7 @@ def components_(self): numerical_value: If feature is numeric, the value of feature for the principal component that principal_component_id identifies. If feature isn't numeric, the value is NULL. - categorical_value: An list of mappings containing information about categorical features. Each mapping contains the following fields: + categorical_value: A list of mappings containing information about categorical features. Each mapping contains the following fields: categorical_value.category: The name of each category. categorical_value.value: The value of categorical_value.category for the centroid that centroid_id identifies. From 3be4a2e784e046ca9a1fac8d386d072537b6c4de Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Mon, 8 Apr 2024 09:09:08 -0700 Subject: [PATCH 483/921] docs: add examples for at/iat (#582) * docs: add examples for at/iat * fix example * fix example * fix example * fix example --- .../bigframes_vendored/pandas/core/frame.py | 48 +++++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 33 +++++++++++++ 2 files changed, 81 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index e5aa47ad3e1..ed615000c12 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -5362,6 +5362,30 @@ def loc(self): def iat(self): """Access a single value for a row/column pair by integer position. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> df = bpd.DataFrame([[0, 2, 3], [0, 4, 1], [10, 20, 30]], + ... columns=['A', 'B', 'C']) + >>> bpd.options.display.progress_bar = None + >>> df + A B C + 0 0 2 3 + 1 0 4 1 + 2 10 20 30 + + [3 rows x 3 columns] + + Get value at specified row/column pair + + >>> df.iat[1, 2] + 1 + + Get value within a series + + >>> df.loc[0].iat[1] + 2 + Returns: bigframes.core.indexers.IatDataFrameIndexer: Indexers object. """ @@ -5371,6 +5395,30 @@ def iat(self): def at(self): """Access a single value for a row/column label pair. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> df = bpd.DataFrame([[0, 2, 3], [0, 4, 1], [10, 20, 30]], + ... index=[4, 5, 6], columns=['A', 'B', 'C']) + >>> bpd.options.display.progress_bar = None + >>> df + A B C + 4 0 2 3 + 5 0 4 1 + 6 10 20 30 + + [3 rows x 3 columns] + + Get value at specified row/column pair + + >>> df.at[4, 'B'] + 2 + + Get value within a series + + >>> df.loc[5].at['B'] + 4 + Returns: bigframes.core.indexers.AtDataFrameIndexer: Indexers object. """ diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 5426e434b38..2d306fb05df 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3308,6 +3308,22 @@ def loc(self): def iat(self): """Access a single value for a row/column pair by integer position. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> s = bpd.Series(bpd.Series([1, 2, 3])) + >>> bpd.options.display.progress_bar = None + >>> s + 0 1 + 1 2 + 2 3 + dtype: Int64 + + Get value at specified row number + + >>> s.iat[1] + 2 + Returns: bigframes.core.indexers.IatSeriesIndexer: Indexers object. """ @@ -3317,6 +3333,23 @@ def iat(self): def at(self): """Access a single value for a row/column label pair. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> s = bpd.Series([1, 2, 3], index=['A', 'B', 'C']) + >>> bpd.options.display.progress_bar = None + >>> s + A 1 + B 2 + C 3 + dtype: Int64 + + Get value at specified row label + + >>> s.at['B'] + 2 + + Returns: bigframes.core.indexers.AtSeriesIndexer: Indexers object. """ From eed12c181ff8724333b1c426a0eb442c627528b8 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:02:16 -0700 Subject: [PATCH 484/921] fix: toc menu missing plotting name (#591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #333374239 🦕 --- docs/templates/toc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 72d135cc965..4573296ec3b 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -69,6 +69,7 @@ uid: bigframes.operations.plotting - name: PlotAccessor uid: bigframes.operations.plotting.PlotAccessor + name: Plotting - items: - name: Series uid: bigframes.series.Series From d048aa8248e008e70e8427d4e56f7833da284698 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 9 Apr 2024 08:57:33 -0700 Subject: [PATCH 485/921] refactor: Switch to using internal schema rules rather than ibis schema (#587) --- bigframes/core/__init__.py | 3 +- bigframes/core/blocks.py | 32 +++++++- bigframes/dataframe.py | 6 -- bigframes/dtypes.py | 30 +++---- bigframes/operations/aggregations.py | 112 ++++++++++++++++++--------- 5 files changed, 122 insertions(+), 61 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 9358dab1b1d..3fa690ef374 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -106,8 +106,7 @@ def session(self) -> Session: @functools.cached_property def schema(self) -> schemata.ArraySchema: - # TODO: switch to use self.node.schema - return self._compiled_schema + return self.node.schema @functools.cached_property def _compiled_schema(self) -> schemata.ArraySchema: diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index c7b41e93eb8..5b411e5416d 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -24,6 +24,7 @@ import dataclasses import functools import itertools +import os import random import typing from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple @@ -41,10 +42,12 @@ import bigframes.core.guid as guid import bigframes.core.join_def as join_defs import bigframes.core.ordering as ordering +import bigframes.core.schema as bf_schema import bigframes.core.tree_properties as tree_properties import bigframes.core.utils import bigframes.core.utils as utils import bigframes.dtypes +import bigframes.features import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.session._io.pandas @@ -411,7 +414,32 @@ def _to_dataframe(self, result) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) - return self.session._rows_to_dataframe(result, dtypes) + result_dataframe = self.session._rows_to_dataframe(result, dtypes) + # Runs strict validations to ensure internal type predictions and ibis are completely in sync + # Do not execute these validations outside of testing suite. + if "PYTEST_CURRENT_TEST" in os.environ: + self._validate_result_schema(result_dataframe) + return result_dataframe + + def _validate_result_schema(self, result_df: pd.DataFrame): + ibis_schema = self.expr._compiled_schema + internal_schema = self.expr.node.schema + actual_schema = bf_schema.ArraySchema( + tuple( + bf_schema.SchemaItem(name, dtype) # type: ignore + for name, dtype in result_df.dtypes.items() + ) + ) + if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: + return + if internal_schema != actual_schema: + raise ValueError( + f"This error should only occur while testing. BigFrames internal schema: {internal_schema} does not match actual schema: {actual_schema}" + ) + if ibis_schema != actual_schema: + raise ValueError( + f"This error should only occur while testing. Ibis schema: {ibis_schema} does not match actual schema: {actual_schema}" + ) def to_pandas( self, @@ -1204,7 +1232,7 @@ def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.UnaryAggregateOp # TODO: annotate aggregations themself with this information dtype = self.expr.get_column_type(column_id) stats: list[agg_ops.UnaryAggregateOp] = [agg_ops.count_op] - if dtype not in bigframes.dtypes.UNORDERED_DTYPES: + if bigframes.dtypes.is_orderable(dtype): stats += [agg_ops.min_op, agg_ops.max_op] if dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: # Notable exclusions: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 460d1056a32..7b282783bdb 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -17,7 +17,6 @@ from __future__ import annotations import datetime -import os import re import sys import textwrap @@ -175,11 +174,6 @@ def __init__( self._block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() self._query_job: Optional[bigquery.QueryJob] = None - # Runs strict validations to ensure internal type predictions and ibis are completely in sync - # Do not execute these validations outside of testing suite. - if "PYTEST_CURRENT_TEST" in os.environ: - self._block.expr.validate_schema() - def __dir__(self): return dir(type(self)) + [ label diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index c5bf5db2fe0..3b2092bf852 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -57,13 +57,11 @@ TIME_DTYPE = pd.ArrowDtype(pa.time64("us")) DATETIME_DTYPE = pd.ArrowDtype(pa.timestamp("us")) TIMESTAMP_DTYPE = pd.ArrowDtype(pa.timestamp("us", tz="UTC")) +GEO_DTYPE = gpd.array.GeometryDtype() # Used when storing Null expressions DEFAULT_DTYPE = FLOAT_DTYPE -# On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable -UNORDERED_DTYPES = [gpd.array.GeometryDtype()] - # Type hints for dtype strings supported by BigQuery DataFrame DtypeString = Literal[ "boolean", @@ -134,6 +132,12 @@ def is_array_like(type: ExpressionType) -> bool: ) +def is_struct_like(type: ExpressionType) -> bool: + return isinstance(type, pd.ArrowDtype) and isinstance( + type.pyarrow_dtype, pa.StructType + ) + + def is_numeric(type: ExpressionType) -> bool: return type in NUMERIC_BIGFRAMES_TYPES_PERMISSIVE @@ -143,18 +147,18 @@ def is_iterable(type: ExpressionType) -> bool: def is_comparable(type: ExpressionType) -> bool: - return (type is not None) and (type not in UNORDERED_DTYPES) + return (type is not None) and is_orderable(type) -# Type hints for Ibis data types that can be read to Python objects by BigQuery DataFrame -ReadOnlyIbisDtype = Union[ - ibis_dtypes.Binary, - ibis_dtypes.JSON, - ibis_dtypes.Decimal, - ibis_dtypes.GeoSpatial, - ibis_dtypes.Array, - ibis_dtypes.Struct, -] +def is_orderable(type: ExpressionType) -> bool: + # On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable + return not is_array_like(type) and not is_struct_like(type) and (type != GEO_DTYPE) + + +def is_bool_coercable(type: ExpressionType) -> bool: + # TODO: Implement more bool coercions + return (type is None) or is_numeric(type) or is_string_like(type) + BIDIRECTIONAL_MAPPINGS: Iterable[Tuple[IbisDtype, Dtype]] = ( (ibis_dtypes.boolean, pd.BooleanDtype()), diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 76aa2a61127..36fa7876445 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -23,6 +23,7 @@ import pyarrow as pa import bigframes.dtypes as dtypes +import bigframes.operations.type as signatures @dataclasses.dataclass(frozen=True) @@ -38,7 +39,7 @@ def handles_ties(self): return False @abc.abstractmethod - def output_type(self, *input_types: dtypes.ExpressionType): + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: ... @@ -48,7 +49,7 @@ class UnaryWindowOp(WindowOp): def arguments(self) -> int: return 1 - def output_type(self, *input_types: dtypes.ExpressionType): + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return input_types[0] @@ -85,7 +86,9 @@ def arguments(self) -> int: class SumOp(UnaryAggregateOp): name: ClassVar[str] = "sum" - def output_type(self, *input_types: dtypes.ExpressionType): + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + if not dtypes.is_numeric(input_types[0]): + raise TypeError(f"Type {input_types[0]} is not numeric") if pd.api.types.is_bool_dtype(input_types[0]): return dtypes.INT_DTYPE else: @@ -96,8 +99,10 @@ def output_type(self, *input_types: dtypes.ExpressionType): class MedianOp(UnaryAggregateOp): name: ClassVar[str] = "median" - def output_type(self, *input_types: dtypes.ExpressionType): + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: # These will change if median is changed to exact implementation. + if not dtypes.is_orderable(input_types[0]): + raise TypeError(f"Type {input_types[0]} is not orderable") if pd.api.types.is_bool_dtype(input_types[0]): return dtypes.INT_DTYPE else: @@ -112,7 +117,9 @@ class ApproxQuartilesOp(UnaryAggregateOp): def name(self): return f"{self.quartile*25}%" - def output_type(self, *input_types: dtypes.ExpressionType): + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + if not dtypes.is_orderable(input_types[0]): + raise TypeError(f"Type {input_types[0]} is not orderable") if pd.api.types.is_bool_dtype(input_types[0]) or pd.api.types.is_integer_dtype( input_types[0] ): @@ -125,55 +132,68 @@ def output_type(self, *input_types: dtypes.ExpressionType): class MeanOp(UnaryAggregateOp): name: ClassVar[str] = "mean" - def output_type(self, *input_types: dtypes.ExpressionType): - if pd.api.types.is_bool_dtype(input_types[0]) or pd.api.types.is_integer_dtype( - input_types[0] - ): - return dtypes.FLOAT_DTYPE - else: - return input_types[0] + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.UNARY_REAL_NUMERIC.output_type(input_types[0]) @dataclasses.dataclass(frozen=True) class ProductOp(UnaryAggregateOp): name: ClassVar[str] = "product" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.FLOAT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_numeric, dtypes.FLOAT_DTYPE, "numeric" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) class MaxOp(UnaryAggregateOp): name: ClassVar[str] = "max" + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.TypePreserving(dtypes.is_orderable, "orderable").output_type( + input_types[0] + ) + @dataclasses.dataclass(frozen=True) class MinOp(UnaryAggregateOp): name: ClassVar[str] = "min" + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.TypePreserving(dtypes.is_orderable, "orderable").output_type( + input_types[0] + ) + @dataclasses.dataclass(frozen=True) class StdOp(UnaryAggregateOp): name: ClassVar[str] = "std" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.FLOAT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_numeric, dtypes.FLOAT_DTYPE, "numeric" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) class VarOp(UnaryAggregateOp): name: ClassVar[str] = "var" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.FLOAT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_numeric, dtypes.FLOAT_DTYPE, "numeric" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) class PopVarOp(UnaryAggregateOp): name: ClassVar[str] = "popvar" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.FLOAT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_numeric, dtypes.FLOAT_DTYPE, "numeric" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -184,8 +204,10 @@ class CountOp(UnaryAggregateOp): def skips_nulls(self): return False - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.INT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + lambda x: True, dtypes.INT_DTYPE, "" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -202,7 +224,7 @@ def skips_nulls(self): def handles_ties(self): return True - def output_type(self, *input_types: dtypes.ExpressionType): + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: if isinstance(self.bins, int) and (self.labels is False): return dtypes.INT_DTYPE else: @@ -237,8 +259,10 @@ def skips_nulls(self): def handles_ties(self): return True - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.INT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -249,7 +273,7 @@ class NuniqueOp(UnaryAggregateOp): def skips_nulls(self): return False - def output_type(self, *input_types: dtypes.ExpressionType): + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return dtypes.INT_DTYPE @@ -276,8 +300,10 @@ def skips_nulls(self): def handles_ties(self): return True - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.INT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -290,8 +316,10 @@ def skips_nulls(self): def handles_ties(self): return True - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.INT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) @@ -340,32 +368,40 @@ def skips_nulls(self): class AllOp(UnaryAggregateOp): name: ClassVar[str] = "all" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.BOOL_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_bool_coercable, dtypes.BOOL_DTYPE, "convertible to boolean" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) class AnyOp(UnaryAggregateOp): name: ClassVar[str] = "any" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.BOOL_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.FixedOutputType( + dtypes.is_bool_coercable, dtypes.BOOL_DTYPE, "convertible to boolean" + ).output_type(input_types[0]) @dataclasses.dataclass(frozen=True) class CorrOp(BinaryAggregateOp): name: ClassVar[str] = "corr" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.FLOAT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.BINARY_REAL_NUMERIC.output_type( + input_types[0], input_types[1] + ) @dataclasses.dataclass(frozen=True) class CovOp(BinaryAggregateOp): name: ClassVar[str] = "cov" - def output_type(self, *input_types: dtypes.ExpressionType): - return dtypes.FLOAT_DTYPE + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.BINARY_REAL_NUMERIC.output_type( + input_types[0], input_types[1] + ) sum_op = SumOp() From a43573d0457ddc6b8ec082df4c04109e34f12e89 Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Tue, 9 Apr 2024 12:37:22 -0700 Subject: [PATCH 486/921] doc: add examples for DatetimeMethods (#577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * doc: add examples for DatetimeMethods * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix presubmit failure * fix presubmit failure --------- Co-authored-by: Owl Bot --- .../pandas/core/arrays/datetimelike.py | 4 +- .../pandas/core/indexes/accessor.py | 238 ++++++++++++++++-- .../pandas/core/tools/datetimes.py | 2 +- 3 files changed, 223 insertions(+), 21 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py index ce5f8d55f37..0d910cec927 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/datetimelike.py @@ -9,8 +9,8 @@ def strftime(self, date_format: str): Convert to string Series using specified date_format. Return a Series of formatted strings specified by date_format. Details - of the string format can be found in `BigQuery format elements doc - <%(https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements)s>`__. + of the string format can be found in BigQuery format elements doc: + https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#format_elements_date_time. **Examples:** diff --git a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py index 9490f4608b2..3f0175359a7 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py @@ -8,7 +8,27 @@ class DatetimeProperties: @property def day(self): - """The day of the datetime.""" + """The day of the datetime. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... pd.date_range("2000-01-01", periods=3, freq="D") + ... ) + >>> s + 0 2000-01-01 00:00:00 + 1 2000-01-02 00:00:00 + 2 2000-01-03 00:00:00 + dtype: timestamp[us][pyarrow] + >>> s.dt.day + 0 1 + 1 2 + 2 3 + dtype: Int64 + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -18,62 +38,187 @@ def dayofweek(self): Return the day of the week. It is assumed the week starts on Monday, which is denoted by 0 and ends on Sunday which is denoted - by 6. This method is available on both Series with datetime - values (using the `dt` accessor) or DatetimeIndex. + by 6. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... pd.date_range('2016-12-31', '2017-01-08', freq='D').to_series() + ... ) + >>> s.dt.dayofweek + 2016-12-31 00:00:00 5 + 2017-01-01 00:00:00 6 + 2017-01-02 00:00:00 0 + 2017-01-03 00:00:00 1 + 2017-01-04 00:00:00 2 + 2017-01-05 00:00:00 3 + 2017-01-06 00:00:00 4 + 2017-01-07 00:00:00 5 + 2017-01-08 00:00:00 6 + dtype: Int64 Returns: - Series or Index: Containing integers indicating the day number. + Series: Containing integers indicating the day number. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def date(self): - """Returns numpy array of Python :class:`datetime.date` objects. - - Namely, the date part of Timestamps without time and + """Returns a Series with the date part of Timestamps without time and timezone information. .. warning:: This method returns a Series whereas pandas returns a numpy array. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]) + >>> s = bpd.to_datetime(s, utc=True, format="%d/%m/%Y %H:%M:%S%Ez") + >>> s + 0 2020-01-01 10:00:00+00:00 + 1 2020-01-02 11:00:00+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + >>> s.dt.date + 0 2020-01-01 + 1 2020-01-02 + dtype: date32[day][pyarrow] """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def hour(self): - """The hours of the datetime.""" + """The hours of the datetime. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... pd.date_range("2000-01-01", periods=3, freq="h") + ... ) + >>> s + 0 2000-01-01 00:00:00 + 1 2000-01-01 01:00:00 + 2 2000-01-01 02:00:00 + dtype: timestamp[us][pyarrow] + >>> s.dt.hour + 0 0 + 1 1 + 2 2 + dtype: Int64 + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def minute(self): - """The minutes of the datetime.""" + """The minutes of the datetime. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... pd.date_range("2000-01-01", periods=3, freq="min") + ... ) + >>> s + 0 2000-01-01 00:00:00 + 1 2000-01-01 00:01:00 + 2 2000-01-01 00:02:00 + dtype: timestamp[us][pyarrow] + >>> s.dt.minute + 0 0 + 1 1 + 2 2 + dtype: Int64 + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def month(self): - """The month as January=1, December=12.""" + """The month as January=1, December=12. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... pd.date_range("2000-01-01", periods=3, freq="M") + ... ) + >>> s + 0 2000-01-31 00:00:00 + 1 2000-02-29 00:00:00 + 2 2000-03-31 00:00:00 + dtype: timestamp[us][pyarrow] + >>> s.dt.month + 0 1 + 1 2 + 2 3 + dtype: Int64 + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def second(self): - """The seconds of the datetime.""" + """The seconds of the datetime. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... pd.date_range("2000-01-01", periods=3, freq="s") + ... ) + >>> s + 0 2000-01-01 00:00:00 + 1 2000-01-01 00:00:01 + 2 2000-01-01 00:00:02 + dtype: timestamp[us][pyarrow] + >>> s.dt.second + 0 0 + 1 1 + 2 2 + dtype: Int64 + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def time(self): - """Returns numpy array of :class:`datetime.time` objects. - - The time part of the Timestamps. + """Returns a Series with the time part of the Timestamps. .. warning:: This method returns a Series whereas pandas returns a numpy array. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]) + >>> s = bpd.to_datetime(s, utc=True, format="%m/%d/%Y %H:%M:%S%Ez") + >>> s + 0 2020-01-01 10:00:00+00:00 + 1 2020-02-01 11:00:00+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + >>> s.dt.time + 0 10:00:00 + 1 11:00:00 + dtype: time64[us][pyarrow] """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -82,16 +227,47 @@ def time(self): def quarter(self): """The quarter of the date. - .. warning:: - This method returns a Series whereas pandas returns - a numpy array. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series(["1/1/2020 10:00:00+00:00", "4/1/2020 11:00:00+00:00"]) + >>> s = bpd.to_datetime(s, utc=True, format="%m/%d/%Y %H:%M:%S%Ez") + >>> s + 0 2020-01-01 10:00:00+00:00 + 1 2020-04-01 11:00:00+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + >>> s.dt.quarter + 0 1 + 1 2 + dtype: Int64 """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @property def year(self): - """The year of the datetime.""" + """The year of the datetime. + + **Examples:** + + >>> import pandas as pd + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... pd.date_range("2000-01-01", periods=3, freq="Y") + ... ) + >>> s + 0 2000-12-31 00:00:00 + 1 2001-12-31 00:00:00 + 2 2002-12-31 00:00:00 + dtype: timestamp[us][pyarrow] + >>> s.dt.year + 0 2000 + 1 2001 + 2 2002 + dtype: Int64 + """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -99,6 +275,19 @@ def year(self): def tz(self): """Return the timezone. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]) + >>> s = bpd.to_datetime(s, utc=True, format="%m/%d/%Y %H:%M:%S%Ez") + >>> s + 0 2020-01-01 10:00:00+00:00 + 1 2020-02-01 11:00:00+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + >>> s.dt.tz + datetime.timezone.utc + Returns: datetime.tzinfo, pytz.tzinfo.BaseTZInfo, dateutil.tz.tz.tzfile, or None """ @@ -109,6 +298,19 @@ def tz(self): def unit(self) -> str: """Returns the unit of time precision. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]) + >>> s = bpd.to_datetime(s, utc=True, format="%m/%d/%Y %H:%M:%S%Ez") + >>> s + 0 2020-01-01 10:00:00+00:00 + 1 2020-02-01 11:00:00+00:00 + dtype: timestamp[us, tz=UTC][pyarrow] + >>> s.dt.unit + 'us' + Returns: Unit as string (eg. "us"). """ diff --git a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py index 442220f2376..3d460b2b16f 100644 --- a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py +++ b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py @@ -29,7 +29,7 @@ def to_datetime( .. note:: The format strings for specifying datetime representations in BigQuery and pandas are not completely identical. Ensure that the format string provided is compatible - with BigQuery. + with BigQuery (https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#format_elements_date_time). **Examples:** From edef48f7a93e19bc1f6d37fb041dfd6314d881d5 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:55:21 -0700 Subject: [PATCH 487/921] docs: (Series|Dataframe).dtypes (#598) --- .../bigframes_vendored/pandas/core/generic.py | 12 ++++++++++++ .../bigframes_vendored/pandas/core/series.py | 14 ++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index d14cbfaa52f..61bc39bb12a 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -589,6 +589,18 @@ def dtypes(self): The result's index is the original DataFrame's columns. Columns with mixed types aren't supported yet in BigQuery DataFrames. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'float': [1.0], 'int': [1], 'string': ['foo']}) + >>> df.dtypes + float Float64 + int Int64 + string string[pyarrow] + dtype: object + Returns: A *pandas* Series with the data type of each column. """ diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 2d306fb05df..a75d6c21676 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -119,13 +119,15 @@ def shape(self): def dtype(self): """ Return the dtype object of the underlying data. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - @property - def dtypes(self): - """ - Return the dtype object of the underlying data. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3]) + >>> s.dtype + Int64Dtype() """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 5f1d670e6b839a30acdb495a05011c2ce4e0c7a4 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:53:57 -0700 Subject: [PATCH 488/921] feat: Add pivot_table for DataFrame. (#473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add pivot_table for DataFrame. * Update logic * Update comments * Remove code unused after merge. * Code update. * Update code example. * Update for Tuple type. * Update code logic * Update format --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/dataframe.py | 60 ++++++++++++++ tests/system/small/test_dataframe.py | 28 +++++++ .../bigframes_vendored/pandas/core/frame.py | 82 +++++++++++++++++++ 3 files changed, 170 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 7b282783bdb..32f5a36f790 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2132,6 +2132,66 @@ def pivot( ) -> DataFrame: return self._pivot(columns=columns, index=index, values=values) + def pivot_table( + self, + values: typing.Optional[ + typing.Union[blocks.Label, Sequence[blocks.Label]] + ] = None, + index: typing.Optional[ + typing.Union[blocks.Label, Sequence[blocks.Label]] + ] = None, + columns: typing.Union[blocks.Label, Sequence[blocks.Label]] = None, + aggfunc: str = "mean", + ) -> DataFrame: + if isinstance(index, Iterable) and not ( + isinstance(index, blocks.Label) and index in self.columns + ): + index = list(index) + else: + index = [index] + + if isinstance(columns, Iterable) and not ( + isinstance(columns, blocks.Label) and columns in self.columns + ): + columns = list(columns) + else: + columns = [columns] + + if isinstance(values, Iterable) and not ( + isinstance(values, blocks.Label) and values in self.columns + ): + values = list(values) + else: + values = [values] + + # Unlike pivot, pivot_table has values always ordered. + values.sort() + + keys = index + columns + agged = self.groupby(keys, dropna=True)[values].agg(aggfunc) + + if isinstance(agged, bigframes.series.Series): + agged = agged.to_frame() + + agged = agged.dropna(how="all") + + if len(values) == 1: + agged = agged.rename(columns={agged.columns[0]: values[0]}) + + agged = agged.reset_index() + + pivoted = agged.pivot( + columns=columns, + index=index, + values=values if len(values) > 1 else None, + ).sort_index() + + # TODO: Remove the reordering step once the issue is resolved. + # The pivot_table method results in multi-index columns that are always ordered. + # However, the order of the pivoted result columns is not guaranteed to be sorted. + # Sort and reorder. + return pivoted[pivoted.columns.sort_values()] + def stack(self, level: LevelsType = -1): if not isinstance(self.columns, pandas.MultiIndex): if level not in [0, -1, self.columns.name]: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 0811defbc1b..ba205078ed3 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2606,6 +2606,34 @@ def test_df_pivot_hockey(hockey_df, hockey_pandas_df, values, index, columns): pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) +@pytest.mark.parametrize( + ("values", "index", "columns", "aggfunc"), + [ + (("culmen_length_mm", "body_mass_g"), "species", "sex", "std"), + (["body_mass_g", "culmen_length_mm"], ("species", "island"), "sex", "sum"), + ("body_mass_g", "sex", ["island", "species"], "mean"), + ("culmen_depth_mm", "island", "species", "max"), + ], +) +def test_df_pivot_table( + penguins_df_default_index, + penguins_pandas_df_default_index, + values, + index, + columns, + aggfunc, +): + bf_result = penguins_df_default_index.pivot_table( + values=values, index=index, columns=columns, aggfunc=aggfunc + ).to_pandas() + pd_result = penguins_pandas_df_default_index.pivot_table( + values=values, index=index, columns=columns, aggfunc=aggfunc + ) + pd.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_column_type=False + ) + + def test_ipython_key_completions_with_drop(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_names = "string_col" diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index ed615000c12..1fc80449d14 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4711,6 +4711,88 @@ def pivot(self, *, columns, index=None, values=None): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def pivot_table(self, values=None, index=None, columns=None, aggfunc="mean"): + """ + Create a spreadsheet-style pivot table as a DataFrame. + + The levels in the pivot table will be stored in MultiIndex objects (hierarchical indexes) + on the index and columns of the result DataFrame. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'Product': ['Product A', 'Product B', 'Product A', 'Product B', 'Product A', 'Product B'], + ... 'Region': ['East', 'West', 'East', 'West', 'West', 'East'], + ... 'Sales': [100, 200, 150, 100, 200, 150], + ... 'Rating': [3, 5, 4, 3, 3, 5] + ... }) + >>> df + Product Region Sales Rating + 0 Product A East 100 3 + 1 Product B West 200 5 + 2 Product A East 150 4 + 3 Product B West 100 3 + 4 Product A West 200 3 + 5 Product B East 150 5 + + [6 rows x 4 columns] + + Using `pivot_table` with default aggfunc "mean": + + >>> pivot_table = df.pivot_table( + ... values=['Sales', 'Rating'], + ... index='Product', + ... columns='Region' + ... ) + >>> pivot_table + Rating Sales + Region East West East West + Product + Product A 3.5 3.0 125.0 200.0 + Product B 5.0 4.0 150.0 150.0 + + [2 rows x 4 columns] + + Using `pivot_table` with specified aggfunc "max": + + >>> pivot_table = df.pivot_table( + ... values=['Sales', 'Rating'], + ... index='Product', + ... columns='Region', + ... aggfunc="max" + ... ) + >>> pivot_table + Rating Sales + Region East West East West + Product + Product A 4 3 150 200 + Product B 5 5 150 200 + + [2 rows x 4 columns] + + Args: + values (str, object or a list of the previous, optional): + Column(s) to use for populating new frame's values. If not + specified, all remaining columns will be used and the result will + have hierarchically indexed columns. + + index (str or object or a list of str, optional): + Column to use to make new frame's index. If not given, uses existing index. + + columns (str or object or a list of str): + Column to use to make new frame's columns. + + aggfunc (str, default "mean"): + Aggregation function name to compute summary statistics (e.g., 'sum', 'mean'). + + Returns: + DataFrame: An Excel style pivot table. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def stack(self, level=-1): """ Stack the prescribed level(s) from columns to index. From a557ea2b64633932f730b56688f76806da6195fb Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 10 Apr 2024 02:00:26 +0000 Subject: [PATCH 489/921] docs: add code samples for `str` accessor methdos (#594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal issue 333397327 🦕 --- bigframes/operations/strings.py | 20 +- scripts/get_documentation_coverage.py | 4 + .../pandas/core/strings/accessor.py | 663 +++++++++++++++++- 3 files changed, 677 insertions(+), 10 deletions(-) diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index abd45a14531..883d19a1e32 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -53,7 +53,25 @@ def lower(self) -> series.Series: return self._apply_unary_op(ops.lower_op) def reverse(self) -> series.Series: - """Reverse strings in the Series.""" + """Reverse strings in the Series. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["apple", "banana", "", bpd.NA]) + >>> s.str.reverse() + 0 elppa + 1 ananab + 2 + 3 + dtype: string + + Returns: + bigframes.series.Series: A Series of booleans indicating whether the given + pattern matches the start of each string element. + """ # reverse method is in ibis, not pandas. return self._apply_unary_op(ops.reverse_op) diff --git a/scripts/get_documentation_coverage.py b/scripts/get_documentation_coverage.py index 0b9417b2d30..a6566cafab0 100755 --- a/scripts/get_documentation_coverage.py +++ b/scripts/get_documentation_coverage.py @@ -97,6 +97,10 @@ def get_coverage_summary( if name.startswith("_") and not name.startswith("__"): continue + # ignore constructor + if name == "__init__": + continue + def predicate(impl): return ( # This includes class methods like `from_dict`, `from_records` diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py index ecdd9547d54..5bb69dc1f27 100644 --- a/third_party/bigframes_vendored/pandas/core/strings/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -20,19 +20,57 @@ def extract(self, pat: str, flags: int = 0): For each subject string in the Series, extract groups from the first match of regular expression `pat`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + A pattern with two groups will return a DataFrame with two columns. + Non-matches will be `NaN`. + + >>> s = bpd.Series(['a1', 'b2', 'c3']) + >>> s.str.extract(r'([ab])(\\d)') + 0 1 + 0 a 1 + 1 b 2 + 2 + + [3 rows x 2 columns] + + Named groups will become column names in the result. + + >>> s.str.extract(r'(?P[ab])(?P\\d)') + letter digit + 0 a 1 + 1 b 2 + 2 + + [3 rows x 2 columns] + + A pattern with one group will return a DataFrame with one column. + + >>> s.str.extract(r'[ab](\\d)') + 0 + 0 1 + 1 2 + 2 + + [3 rows x 1 columns] + Args: - pat: + pat (str): Regular expression pattern with capturing groups. - flags: + flags (int, default 0 (no flags)): Flags from the ``re`` module, e.g. ``re.IGNORECASE``, that modify regular expression matching for things like case, spaces, etc. For more details, see :mod:`re`. Returns: - A DataFrame with one row for each subject string, and one - column for each group. Any capture group names in regular - expression pat will be used for column names; otherwise - capture group numbers will be used. + bigframes.dataframe.DataFrame: + A DataFrame with one row for each subject string, and one + column for each group. Any capture group names in regular + expression pat will be used for column names; otherwise + capture group numbers will be used. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -43,12 +81,24 @@ def find(self, sub, start: int = 0, end=None): substring is fully contained between [start:end]. Return -1 on failure. Equivalent to standard :meth:`str.find`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series(["cow_", "duck_", "do_ve"]) + >>> ser.str.find("_") + 0 3 + 1 4 + 2 2 + dtype: Int64 + Args: - sub: + sub (str): Substring being searched. start (int, default 0): Left edge index. - end (None): + end (int, default None): Right edge index. Returns: @@ -62,6 +112,20 @@ def len(self): The element may be a sequence (such as a string, tuple or list) or a collection (such as a dictionary). + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Returns the length (number of characters) in a string. + + >>> s = bpd.Series(['dog', '', bpd.NA]) + >>> s.str.len() + 0 3 + 1 0 + 2 + dtype: Int64 + Returns: bigframes.series.Series: A Series or Index of integer values indicating the length of each element in the Series or Index. @@ -74,6 +138,22 @@ def lower(self): Equivalent to :meth:`str.lower`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['lower', + ... 'CAPITALS', + ... 'this is a sentence', + ... 'SwApCaSe']) + >>> s.str.lower() + 0 lower + 1 capitals + 2 this is a sentence + 3 swapcase + dtype: string + Returns: bigframes.series.Series: Series with lowercase. """ @@ -83,6 +163,36 @@ def lower(self): def slice(self, start=None, stop=None): """Slice substrings from each element in the Series or Index. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["koala", "dog", "chameleon"]) + >>> s + 0 koala + 1 dog + 2 chameleon + dtype: string + + >>> s.str.slice(start=1) + 0 oala + 1 og + 2 hameleon + dtype: string + + >>> s.str.slice(stop=2) + 0 ko + 1 do + 2 ch + dtype: string + + >>> s.str.slice(start=2, stop=5) + 0 ala + 1 g + 2 ame + dtype: string + Args: start (int, optional): Start position for slice operation. @@ -106,6 +216,27 @@ def strip(self): Replaces any non-strings in Series with NaNs. Equivalent to :meth:`str.strip`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['Ant', ' Bee ', '\\tCat\\n', bpd.NA]) + >>> s + 0 Ant + 1 Bee + 2 Cat + + 3 + dtype: string + + >>> s.str.strip() + 0 Ant + 1 Bee + 2 Cat + 3 + dtype: string + Returns: bigframes.series.Series: Series or Index without leading and trailing characters. @@ -118,6 +249,22 @@ def upper(self): Equivalent to :meth:`str.upper`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['lower', + ... 'CAPITALS', + ... 'this is a sentence', + ... 'SwApCaSe']) + >>> s.str.upper() + 0 LOWER + 1 CAPITALS + 2 THIS IS A SENTENCE + 3 SWAPCASE + dtype: string + Returns: bigframes.series.Series: Series with uppercase strings. """ @@ -131,6 +278,19 @@ def isnumeric(self): :meth:`str.isnumeric` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s1 = bpd.Series(['one', 'one1', '1', '']) + >>> s1.str.isnumeric() + 0 False + 1 False + 2 True + 3 False + dtype: boolean + Returns: bigframes.series.Series: Series or Index of boolean values with the same length as the original Series/Index. @@ -145,6 +305,19 @@ def isalpha(self): :meth:`str.isalpha` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s1 = bpd.Series(['one', 'one1', '1', '']) + >>> s1.str.isalpha() + 0 True + 1 False + 2 False + 3 False + dtype: boolean + Returns: bigframes.series.Series: Series with the same length as the originalSeries/Index. """ @@ -158,6 +331,19 @@ def isdigit(self): :meth:`str.isdigit` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['23', '1a', '1/5', '']) + >>> s.str.isdigit() + 0 True + 1 False + 2 False + 3 False + dtype: boolean + Returns: bigframes.series.Series: Series with the same length as the originalSeries/Index. """ @@ -171,6 +357,30 @@ def isalnum(self): :meth:`str.isalnum` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s1 = bpd.Series(['one', 'one1', '1', '']) + >>> s1.str.isalnum() + 0 True + 1 True + 2 True + 3 False + dtype: boolean + + Note that checks against characters mixed with any additional + punctuation or whitespace will evaluate to false for an alphanumeric + check. + + >>> s2 = bpd.Series(['A B', '1.5', '3,000']) + >>> s2.str.isalnum() + 0 False + 1 False + 2 False + dtype: boolean + Returns: bigframes.series.Series: Series or Index of boolean values with the same length as the original Series/Index. @@ -185,6 +395,18 @@ def isspace(self): :meth:`str.isspace` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([' ', '\\t\\r\\n ', '']) + >>> s.str.isspace() + 0 True + 1 True + 2 False + dtype: boolean + Returns: bigframes.series.Series: Series or Index of boolean values with the same length as the original Series/Index. @@ -199,6 +421,19 @@ def islower(self): :meth:`str.islower` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['leopard', 'Golden Eagle', 'SNAKE', '']) + >>> s.str.islower() + 0 True + 1 False + 2 False + 3 False + dtype: boolean + Returns: bigframes.series.Series: Series or Index of boolean values with the same length as the original Series/Index. @@ -213,6 +448,19 @@ def isupper(self): :meth:`str.isupper` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['leopard', 'Golden Eagle', 'SNAKE', '']) + >>> s.str.isupper() + 0 False + 1 False + 2 True + 3 False + dtype: boolean + Returns: bigframes.series.Series: Series or Index of boolean values with the same length as the original Series/Index. @@ -227,6 +475,22 @@ def isdecimal(self): :meth:`str.isdecimal` for each element of the Series/Index. If a string has zero characters, ``False`` is returned for that check. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + The `isdecimal` method checks for characters used to form numbers in + base 10. + + >>> s = bpd.Series(['23', '³', '⅕', '']) + >>> s.str.isdecimal() + 0 True + 1 False + 2 False + 3 False + dtype: boolean + Returns: bigframes.series.Series: Series or Index of boolean values with the same length as the original Series/Index. @@ -242,6 +506,27 @@ def rstrip(self): Replaces any non-strings in Series with NaNs. Equivalent to :meth:`str.rstrip`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['Ant', ' Bee ', '\\tCat\\n', bpd.NA]) + >>> s + 0 Ant + 1 Bee + 2 Cat + + 3 + dtype: string + + >>> s.str.rstrip() + 0 Ant + 1 Bee + 2 Cat + 3 + dtype: string + Returns: bigframes.series.Series: Series without trailing characters. """ @@ -256,6 +541,28 @@ def lstrip(self): Replaces any non-strings in Series with NaNs. Equivalent to :meth:`str.lstrip`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['Ant', ' Bee ', '\\tCat\\n', bpd.NA]) + >>> s + 0 Ant + 1 Bee + 2 Cat + + 3 + dtype: string + + >>> s.str.lstrip() + 0 Ant + 1 Bee + 2 Cat + + 3 + dtype: string + Returns: bigframes.series.Series: Series without leading characters. """ @@ -265,6 +572,24 @@ def lstrip(self): def repeat(self, repeats: int): """Duplicate each string in the Series or Index. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['a', 'b', 'c']) + >>> s + 0 a + 1 b + 2 c + dtype: string + + >>> s.str.repeat(repeats=2) + 0 aa + 1 bb + 2 cc + dtype: string + Args: repeats : int or sequence of int Same value for all (int) or different value per (sequence). @@ -281,6 +606,22 @@ def capitalize(self): Equivalent to :meth:`str.capitalize`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['lower', + ... 'CAPITALS', + ... 'this is a sentence', + ... 'SwApCaSe']) + >>> s.str.capitalize() + 0 Lower + 1 Capitals + 2 This is a sentence + 3 Swapcase + dtype: string + Returns: bigframes.series.Series: Series with captitalized strings. """ @@ -293,8 +634,43 @@ def cat(self, others, *, join): If `others` is specified, this function concatenates the Series/Index and elements of `others` element-wise. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can concatenate each string in a Series to another string. + + >>> s = bpd.Series(['Jane', 'John']) + >>> s.str.cat(" Doe") + 0 Jane Doe + 1 John Doe + dtype: string + + You can concatenate another Series. By default left join is performed to + align the corresponding elements. + + >>> s.str.cat(bpd.Series([" Doe", " Foe", " Roe"])) + 0 Jane Doe + 1 John Foe + dtype: string + + >>> s.str.cat(bpd.Series([" Doe", " Foe", " Roe"], index=[2, 0, 1])) + 0 Jane Foe + 1 John Roe + dtype: string + + You can enforce an outer join. + + >>> s.str.cat(bpd.Series([" Doe", " Foe", " Roe"]), join="outer") + 0 Jane Doe + 1 John Foe + 2 + dtype: string + Args: - others (Series): + others (str or Series): + A string or a Series of strings. join ({'left', 'outer'}, default 'left'): Determines the join-style between the calling Series and any @@ -315,6 +691,77 @@ def contains(self, pat, case: bool = True, flags: int = 0, *, regex: bool = True Return boolean Series or Index based on whether a given pattern or regex is contained within a string of a Series or Index. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + Returning a Series of booleans using only a literal pattern. + + >>> s1 = bpd.Series(['Mouse', 'dog', 'house and parrot', '23', None]) + >>> s1.str.contains('og') + 0 False + 1 True + 2 False + 3 False + 4 + dtype: boolean + + Specifying case sensitivity using `case`. + + >>> s1.str.contains('oG', case=True) + 0 False + 1 False + 2 False + 3 False + 4 + dtype: boolean + + Returning 'house' or 'dog' when either expression occurs in a string. + + >>> s1.str.contains('house|dog', regex=True) + 0 False + 1 True + 2 True + 3 False + 4 + dtype: boolean + + Ignoring case sensitivity using `flags` with regex. + + >>> import re + >>> s1.str.contains('PARROT', flags=re.IGNORECASE, regex=True) + 0 False + 1 False + 2 True + 3 False + 4 + dtype: boolean + + Returning any digit using regular expression. + + >>> s1.str.contains('\\d', regex=True) + 0 False + 1 False + 2 False + 3 True + 4 + dtype: boolean + + Ensure `pat` is a not a literal pattern when `regex` is set to True. + Note in the following example one might expect only *s2[1]* and *s2[3]* + to return `True`. However, '.0' as a regex matches any character + followed by a 0. + + >>> s2 = bpd.Series(['40', '40.0', '41', '41.0', '35']) + >>> s2.str.contains('.0', regex=True) + 0 True + 1 True + 2 False + 3 True + 4 False + dtype: boolean + Args: pat (str, re.Pattern): Character sequence or regular expression. @@ -348,6 +795,32 @@ def replace( Equivalent to :meth:`str.replace` or :func:`re.sub`, depending on the regex value. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + When *pat* is a string and *regex* is True, the given *pat* is compiled + as a regex. When *repl* is a string, it replaces matching regex patterns + as with `re.sub()`. NaN value(s) in the Series are left as is: + + >>> s = bpd.Series(['foo', 'fuz', bpd.NA]) + >>> s.str.replace('f.', 'ba', regex=True) + 0 bao + 1 baz + 2 + dtype: string + + When *pat* is a string and *regex* is False, every *pat* is replaced + with *repl* as with `str.replace()`: + + >>> s = bpd.Series(['f.o', 'fuz', bpd.NA]) + >>> s.str.replace('f.', 'ba', regex=False) + 0 bao + 1 fuz + 2 + dtype: string + Args: pat (str, re.Pattern): String can be a character sequence or regular expression. @@ -384,6 +857,33 @@ def startswith( """ Test if the start of each string element matches a pattern. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['bat', 'Bear', 'caT', bpd.NA]) + >>> s + 0 bat + 1 Bear + 2 caT + 3 + dtype: string + + >>> s.str.startswith('b') + 0 True + 1 False + 2 False + 3 + dtype: boolean + + >>> s.str.startswith(('b', 'B')) + 0 True + 1 True + 2 False + 3 + dtype: boolean + Args: pat (str, tuple[str, ...]): Character sequence or tuple of strings. Regular expressions are not @@ -402,6 +902,33 @@ def endswith( """ Test if the end of each string element matches a pattern. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['bat', 'bear', 'caT', bpd.NA]) + >>> s + 0 bat + 1 bear + 2 caT + 3 + dtype: string + + >>> s.str.endswith('t') + 0 True + 1 False + 2 False + 3 + dtype: boolean + + >>> s.str.endswith(('t', 'T')) + 0 True + 1 False + 2 True + 3 + dtype: boolean + Args: pat (str, tuple[str, ...]): Character sequence or tuple of strings. Regular expressions are not @@ -417,6 +944,18 @@ def match(self, pat: str, case: bool = True, flags: int = 0): """ Determine if each string starts with a match of a regular expression. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series(["horse", "eagle", "donkey"]) + >>> ser.str.match("e") + 0 False + 1 True + 2 False + dtype: boolean + Args: pat (str): Character sequence or regular expression. @@ -434,6 +973,18 @@ def fullmatch(self, pat: str, case: bool = True, flags: int = 0): """ Determine if each string entirely matches a regular expression. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series(["cat", "duck", "dove"]) + >>> ser.str.fullmatch(r'd.+') + 0 False + 1 True + 2 True + dtype: boolean + Args: pat (str): Character sequence or regular expression. @@ -454,6 +1005,18 @@ def get(self, i: int): Extract element from lists, tuples, dict, or strings in each element in the Series/Index. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["apple", "banana", "fig"]) + >>> s.str.get(3) + 0 l + 1 a + 2 + dtype: string + Args: i (int): Position or key of element to extract. @@ -472,6 +1035,32 @@ def pad( """ Pad strings in the Series/Index up to width. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(["caribou", "tiger"]) + >>> s + 0 caribou + 1 tiger + dtype: string + + >>> s.str.pad(width=10) + 0 caribou + 1 tiger + dtype: string + + >>> s.str.pad(width=10, side='right', fillchar='-') + 0 caribou--- + 1 tiger----- + dtype: string + + >>> s.str.pad(width=10, side='both', fillchar='-') + 0 -caribou-- + 1 --tiger--- + dtype: string + Args: width (int): Minimum width of resulting string; additional characters will be filled @@ -494,6 +1083,18 @@ def ljust( """ Pad right side of strings in the Series/Index up to width. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series(['dog', 'bird', 'mouse']) + >>> ser.str.ljust(8, fillchar='.') + 0 dog..... + 1 bird.... + 2 mouse... + dtype: string + Args: width (int): Minimum width of resulting string; additional characters will be filled @@ -514,6 +1115,18 @@ def rjust( """ Pad left side of strings in the Series/Index up to width. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series(['dog', 'bird', 'mouse']) + >>> ser.str.rjust(8, fillchar='.') + 0 .....dog + 1 ....bird + 2 ...mouse + dtype: string + Args: width (int): Minimum width of resulting string; additional characters will be filled @@ -538,6 +1151,26 @@ def zfill( in the Series/Index with length greater or equal to `width` are unchanged. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['-1', '1', '1000', bpd.NA]) + >>> s + 0 -1 + 1 1 + 2 1000 + 3 + dtype: string + + >>> s.str.zfill(3) + 0 -01 + 1 001 + 2 1000 + 3 + dtype: string + Args: width (int): Minimum length of resulting string; strings with length less @@ -558,6 +1191,18 @@ def center( Equivalent to :meth:`str.center`. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series(['dog', 'bird', 'mouse']) + >>> ser.str.center(8, fillchar='.') + 0 ..dog... + 1 ..bird.. + 2 .mouse.. + dtype: string + Args: width (int): Minimum width of resulting string; additional characters will be filled From 8702dcf54c0f2073e21df42eaef51927481da421 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:37:02 -0700 Subject: [PATCH 490/921] fix: error for object dtype on read_pandas (#570) --- bigframes/session/__init__.py | 11 ++++++++++- tests/system/small/test_session.py | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 354352f1c9f..b6d56006be0 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1050,7 +1050,12 @@ def _read_pandas( inline_df = self._read_pandas_inline(pandas_dataframe) if inline_df is not None: return inline_df - return self._read_pandas_load_job(pandas_dataframe, api_name) + try: + return self._read_pandas_load_job(pandas_dataframe, api_name) + except pa.ArrowInvalid as e: + raise pa.ArrowInvalid( + f"Could not convert with a BigQuery type: `{e}`. " + ) from e def _read_pandas_inline( self, pandas_dataframe: pandas.DataFrame @@ -1064,6 +1069,10 @@ def _read_pandas_inline( inline_df = dataframe.DataFrame( blocks.Block.from_local(pandas_dataframe, self) ) + except pa.ArrowInvalid as e: + raise pa.ArrowInvalid( + f"Could not convert with a BigQuery type: `{e}`. " + ) from e except ValueError: # Thrown by ibis for some unhandled types return None except pa.ArrowTypeError: # Thrown by arrow for types without mapping (geo). diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index d84244e5cfb..ce415f93242 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -24,6 +24,7 @@ import google.cloud.bigquery as bigquery import numpy as np import pandas as pd +import pyarrow as pa import pytest import bigframes @@ -436,6 +437,11 @@ def test_read_pandas_index(session): pd.testing.assert_index_equal(bf_idx.to_pandas(), pd_idx) +def test_read_pandas_w_unsupported_mixed_dtype(session): + with pytest.raises(pa.ArrowInvalid, match="Could not convert"): + session.read_pandas(pd.DataFrame({"a": [1, "hello"]})) + + def test_read_pandas_inline_respects_location(): options = bigframes.BigQueryOptions(location="europe-west1") session = bigframes.Session(options) From 5d0f149dce5425098fcd154d96a302c1661ce5d3 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 10 Apr 2024 14:40:27 -0700 Subject: [PATCH 491/921] feat: Add MultiIndex subclass. (#596) --- bigframes/core/indexes/__init__.py | 2 + bigframes/core/indexes/base.py | 57 ++++++------ bigframes/core/indexes/multi.py | 48 ++++++++++ bigframes/pandas/__init__.py | 2 + tests/system/small/test_multiindex.py | 25 ++++++ .../pandas/core/indexes/multi.py | 88 +++++++++++++++++++ 6 files changed, 193 insertions(+), 29 deletions(-) create mode 100644 bigframes/core/indexes/multi.py create mode 100644 third_party/bigframes_vendored/pandas/core/indexes/multi.py diff --git a/bigframes/core/indexes/__init__.py b/bigframes/core/indexes/__init__.py index ae6011ffa57..0a95adcd83f 100644 --- a/bigframes/core/indexes/__init__.py +++ b/bigframes/core/indexes/__init__.py @@ -13,7 +13,9 @@ # limitations under the License. from bigframes.core.indexes.base import Index +from bigframes.core.indexes.multi import MultiIndex __all__ = [ "Index", + "MultiIndex", ] diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index daa52a02b9d..46a9e30637e 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -42,9 +42,15 @@ class Index(vendored_pandas_index.Index): __doc__ = vendored_pandas_index.Index.__doc__ - - def __init__( - self, + _query_job = None + _block: blocks.Block + _linked_frame: Union[ + bigframes.dataframe.DataFrame, bigframes.series.Series, None + ] = None + + # Overrided on __new__ to create subclasses like pandas does + def __new__( + cls, data=None, dtype=None, *, @@ -73,18 +79,30 @@ def __init__( if dtype is not None: index = index.astype(dtype) block = index._block + elif isinstance(data, pandas.Index): + pd_df = pandas.DataFrame(index=data) + block = df.DataFrame(pd_df, session=session)._block else: pd_index = pandas.Index(data=data, dtype=dtype, name=name) pd_df = pandas.DataFrame(index=pd_index) block = df.DataFrame(pd_df, session=session)._block - self._query_job = None - self._block: blocks.Block = block + + # TODO: Support more index subtypes + from bigframes.core.indexes.multi import MultiIndex + + klass = MultiIndex if len(block._index_columns) > 1 else cls + result = typing.cast(Index, object.__new__(klass)) + result._query_job = None + result._block = block + return result @classmethod def from_frame( cls, frame: Union[bigframes.series.Series, bigframes.dataframe.DataFrame] ) -> Index: - return FrameIndex(frame) + index = Index(frame._block) + index._linked_frame = frame + return index @property def name(self) -> blocks.Label: @@ -107,6 +125,10 @@ def names(self) -> typing.Sequence[blocks.Label]: @names.setter def names(self, values: typing.Sequence[blocks.Label]): new_block = self._block.with_index_labels(values) + if self._linked_frame is not None: + self._linked_frame._set_block( + self._linked_frame._block.with_index_labels(values) + ) self._block = new_block @property @@ -452,26 +474,3 @@ def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: def __len__(self): return self.shape[0] - - -# Index that mutates the originating dataframe/series -class FrameIndex(Index): - def __init__( - self, - series_or_dataframe: typing.Union[ - bigframes.series.Series, bigframes.dataframe.DataFrame - ], - ): - super().__init__(series_or_dataframe._block) - self._whole_frame = series_or_dataframe - - @property - def names(self) -> typing.Sequence[blocks.Label]: - """Returns the names of the Index.""" - return self._block._index_labels - - @names.setter - def names(self, values: typing.Sequence[blocks.Label]): - new_block = self._whole_frame._get_block().with_index_labels(values) - self._whole_frame._set_block(new_block) - self._block = new_block diff --git a/bigframes/core/indexes/multi.py b/bigframes/core/indexes/multi.py new file mode 100644 index 00000000000..182d1f101cf --- /dev/null +++ b/bigframes/core/indexes/multi.py @@ -0,0 +1,48 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import cast, Hashable, Iterable, Sequence + +import bigframes_vendored.pandas.core.indexes.multi as vendored_pandas_multindex +import pandas + +from bigframes.core.indexes.base import Index + + +class MultiIndex(Index, vendored_pandas_multindex.MultiIndex): + __doc__ = vendored_pandas_multindex.MultiIndex.__doc__ + + @classmethod + def from_tuples( + cls, + tuples: Iterable[tuple[Hashable, ...]], + sortorder: int | None = None, + names: Sequence[Hashable] | Hashable | None = None, + ) -> MultiIndex: + pd_index = pandas.MultiIndex.from_tuples(tuples, sortorder, names) + # Index.__new__ should detect multiple levels and properly create a multiindex + return cast(MultiIndex, Index(pd_index)) + + @classmethod + def from_arrays( + cls, + arrays, + sortorder: int | None = None, + names=None, + ) -> MultiIndex: + pd_index = pandas.MultiIndex.from_arrays(arrays, sortorder, names) + # Index.__new__ should detect multiple levels and properly create a multiindex + return cast(MultiIndex, Index(pd_index)) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 4b0ac4310c6..f5be4421e4c 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -707,6 +707,7 @@ def to_datetime( # checking and docstrings. DataFrame = bigframes.dataframe.DataFrame Index = bigframes.core.indexes.Index +MultiIndex = bigframes.core.indexes.MultiIndex Series = bigframes.series.Series # Other public pandas attributes @@ -760,6 +761,7 @@ def to_datetime( # Class aliases "DataFrame", "Index", + "MultiIndex", "Series", # Other public pandas attributes "NamedAgg", diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 330fe44eb85..bb0af52976d 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -20,6 +20,31 @@ from tests.system.utils import assert_pandas_df_equal, skip_legacy_pandas +def test_multi_index_from_arrays(): + bf_idx = bpd.MultiIndex.from_arrays( + [ + pandas.Index([4, 99], dtype=pandas.Int64Dtype()), + pandas.Index( + [" Hello, World!", "_some_new_string"], + dtype=pandas.StringDtype(storage="pyarrow"), + ), + ], + names=[" 1index 1", "_1index 2"], + ) + pd_idx = pandas.MultiIndex.from_arrays( + [ + pandas.Index([4, 99], dtype=pandas.Int64Dtype()), + pandas.Index( + [" Hello, World!", "_some_new_string"], + dtype=pandas.StringDtype(storage="pyarrow"), + ), + ], + names=[" 1index 1", "_1index 2"], + ) + assert bf_idx.names == pd_idx.names + pandas.testing.assert_index_equal(bf_idx.to_pandas(), pd_idx) + + @skip_legacy_pandas def test_read_pandas_multi_index_axes(): index = pandas.MultiIndex.from_arrays( diff --git a/third_party/bigframes_vendored/pandas/core/indexes/multi.py b/third_party/bigframes_vendored/pandas/core/indexes/multi.py new file mode 100644 index 00000000000..a882aa40e37 --- /dev/null +++ b/third_party/bigframes_vendored/pandas/core/indexes/multi.py @@ -0,0 +1,88 @@ +# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexes/multi.py +from __future__ import annotations + +from typing import Hashable, Iterable, Sequence + +import bigframes_vendored.pandas.core.indexes.base + +from bigframes import constants + + +class MultiIndex(bigframes_vendored.pandas.core.indexes.base.Index): + """ + A multi-level, or hierarchical, index object for pandas objects. + """ + + @classmethod + def from_tuples( + cls, + tuples: Iterable[tuple[Hashable, ...]], + sortorder: int | None = None, + names: Sequence[Hashable] | Hashable | None = None, + ) -> MultiIndex: + """ + Convert list of tuples to MultiIndex. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> tuples = [(1, 'red'), (1, 'blue'), + ... (2, 'red'), (2, 'blue')] + >>> bpd.MultiIndex.from_tuples(tuples, names=('number', 'color')) + MultiIndex([(1, 'red'), + (1, 'blue'), + (2, 'red'), + (2, 'blue')], + names=['number', 'color']) + + Args: + tuples (list / sequence of tuple-likes): + Each tuple is the index of one row/column. + sortorder (int or None): + Level of sortedness (must be lexicographically sorted by that + level). + names (list / sequence of str, optional): + Names for the levels in the index. + + Returns: + MultiIndex + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + @classmethod + def from_arrays( + cls, + arrays, + sortorder: int | None = None, + names=None, + ) -> MultiIndex: + """ + Convert arrays to MultiIndex. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> arrays = [[1, 1, 2, 2], ['red', 'blue', 'red', 'blue']] + >>> bpd.MultiIndex.from_arrays(arrays, names=('number', 'color')) + MultiIndex([(1, 'red'), + (1, 'blue'), + (2, 'red'), + (2, 'blue')], + names=['number', 'color']) + + Args: + arrays (list / sequence of array-likes): + Each array-like gives one level's value for each data point. + len(arrays) is the number of levels. + sortorder (int or None): + Level of sortedness (must be lexicographically sorted by that + level). + names (list / sequence of str, optional): + Names for the levels in the index. + + Returns: + MultiIndex + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 86e0f38adc71d76e09dd832e5e33cb7c1aab02ac Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 10 Apr 2024 16:48:16 -0700 Subject: [PATCH 492/921] feat: Add hasnans, combine_first, update to Series (#600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/convert.py | 28 +++- bigframes/series.py | 21 ++- tests/system/small/test_series.py | 51 +++++++ .../bigframes_vendored/pandas/core/series.py | 138 ++++++++++++++++++ 4 files changed, 236 insertions(+), 2 deletions(-) diff --git a/bigframes/core/convert.py b/bigframes/core/convert.py index 98f854ad72e..1ef329b0c78 100644 --- a/bigframes/core/convert.py +++ b/bigframes/core/convert.py @@ -13,13 +13,27 @@ # limitations under the License. from __future__ import annotations +from typing import Optional + import pandas as pd import bigframes.core.indexes as index import bigframes.series as series -def to_bf_series(obj, default_index: index.Index) -> series.Series: +def to_bf_series(obj, default_index: Optional[index.Index]) -> series.Series: + """ + Convert a an object to a bigframes series + + Args: + obj (list-like or Series): + Object to convert to bigframes Series + default_index (list-like or Index or None): + Index to use if obj has no index + + Returns + bigframes.pandas.Series + """ if isinstance(obj, series.Series): return obj if isinstance(obj, pd.Series): @@ -35,6 +49,18 @@ def to_bf_series(obj, default_index: index.Index) -> series.Series: def to_pd_series(obj, default_index: pd.Index) -> pd.Series: + """ + Convert a an object to a pandas series + + Args: + obj (list-like or Series): + Object to convert to pandas Series + default_index (list-like or Index or None): + Index to use if obj has no index + + Returns + pandas.Series + """ if isinstance(obj, series.Series): return obj.to_pandas() if isinstance(obj, pd.Series): diff --git a/bigframes/series.py b/bigframes/series.py index 185891bc010..b975979eaf5 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -22,7 +22,7 @@ import os import textwrap import typing -from typing import Any, Literal, Mapping, Optional, Tuple, Union +from typing import Any, Literal, Mapping, Optional, Sequence, Tuple, Union import bigframes_vendored.pandas.core.series as vendored_pandas_series import google.cloud.bigquery as bigquery @@ -130,6 +130,11 @@ def ndim(self) -> int: def empty(self) -> bool: return self.shape[0] == 0 + @property + def hasnans(self) -> bool: + # Note, hasnans is actually a null check, and NaNs don't count for nullable float + return self.isnull().any() + @property def values(self) -> numpy.ndarray: return self.to_numpy() @@ -753,6 +758,20 @@ def __matmul__(self, other): dot = __matmul__ + def combine_first(self, other: Series) -> Series: + result = self._apply_binary_op(other, ops.coalesce_op) + result.name = self.name + return result + + def update(self, other: Union[Series, Sequence, Mapping]) -> None: + import bigframes.core.convert + + other = bigframes.core.convert.to_bf_series(other, default_index=None) + result = self._apply_binary_op( + other, ops.coalesce_op, reverse=True, alignment="left" + ) + self._set_block(result._get_block()) + def abs(self) -> Series: return self._apply_unary_op(ops.abs_op) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index c8826775081..c93af1bf2fd 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1261,6 +1261,39 @@ def test_binop_right_filtered(scalars_dfs): ) +@skip_legacy_pandas +def test_series_combine_first(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + int64_col = scalars_df["int64_col"].head(7) + float64_col = scalars_df["float64_col"].tail(7) + bf_result = int64_col.combine_first(float64_col).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_col"].head(7) + pd_float64_col = scalars_pandas_df["float64_col"].tail(7) + pd_result = pd_int64_col.combine_first(pd_float64_col) + + assert_series_equal( + bf_result, + pd_result, + ) + + +def test_series_update(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + int64_col = scalars_df["int64_col"].head(7) + float64_col = scalars_df["float64_col"].tail(7).copy() + float64_col.update(int64_col) + + pd_int64_col = scalars_pandas_df["int64_col"].head(7) + pd_float64_col = scalars_pandas_df["float64_col"].tail(7).copy() + pd_float64_col.update(pd_int64_col) + + assert_series_equal( + float64_col.to_pandas(), + pd_float64_col, + ) + + def test_mean(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" @@ -1649,6 +1682,24 @@ def test_size(scalars_dfs): assert pd_result == bf_result +def test_series_hasnans_true(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"].hasnans + pd_result = scalars_pandas_df["string_col"].hasnans + + assert pd_result == bf_result + + +def test_series_hasnans_false(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["string_col"].dropna().hasnans + pd_result = scalars_pandas_df["string_col"].dropna().hasnans + + assert pd_result == bf_result + + def test_empty_false(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index a75d6c21676..572f29ff179 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -175,6 +175,31 @@ def name(self) -> Hashable: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def hasnans(self) -> bool: + """ + Return True if there are any NaNs. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3, None]) + >>> s + 0 1.0 + 1 2.0 + 2 3.0 + 3 + dtype: Float64 + >>> s.hasnans + True + + Returns: + bool + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property def T(self) -> Series: """Return the transpose, which is by definition self. @@ -2343,6 +2368,119 @@ def rdivmod(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def combine_first(self, other) -> Series: + """ + Update null elements with value in the same location in 'other'. + + Combine two Series objects by filling null values in one Series with + non-null values from the other Series. Result index will be the union + of the two indexes. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s1 = bpd.Series([1, np.nan]) + >>> s2 = bpd.Series([3, 4, 5]) + >>> s1.combine_first(s2) + 0 1.0 + 1 4.0 + 2 5.0 + dtype: Float64 + + Null values still persist if the location of that null value + does not exist in `other` + + >>> s1 = bpd.Series({'falcon': np.nan, 'eagle': 160.0}) + >>> s2 = bpd.Series({'eagle': 200.0, 'duck': 30.0}) + >>> s1.combine_first(s2) + falcon + eagle 160.0 + duck 30.0 + dtype: Float64 + + Args: + other (Series): + The value(s) to be used for filling null values. + + Returns: + Series: The result of combining the provided Series with the other object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def update(self, other) -> None: + """ + Modify Series in place using values from passed Series. + + Uses non-NA values from passed Series to make updates. Aligns + on index. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> import pandas as pd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3]) + >>> s.update(bpd.Series([4, 5, 6])) + >>> s + 0 4 + 1 5 + 2 6 + dtype: Int64 + + >>> s = bpd.Series(['a', 'b', 'c']) + >>> s.update(bpd.Series(['d', 'e'], index=[0, 2])) + >>> s + 0 d + 1 b + 2 e + dtype: string + + >>> s = bpd.Series([1, 2, 3]) + >>> s.update(bpd.Series([4, 5, 6, 7, 8])) + >>> s + 0 4 + 1 5 + 2 6 + dtype: Int64 + + If ``other`` contains NaNs the corresponding values are not updated + in the original Series. + + >>> s = bpd.Series([1, 2, 3]) + >>> s.update(bpd.Series([4, np.nan, 6], dtype=pd.Int64Dtype())) + >>> s + 0 4 + 1 2 + 2 6 + dtype: Int64 + + ``other`` can also be a non-Series object type + that is coercible into a Series + + >>> s = bpd.Series([1, 2, 3]) + >>> s.update([4, np.nan, 6]) + >>> s + 0 4.0 + 1 2.0 + 2 6.0 + dtype: Float64 + + >>> s = bpd.Series([1, 2, 3]) + >>> s.update({1: 9}) + >>> s + 0 1 + 1 9 + 2 3 + dtype: Int64 + + Args: + other (Series, or object coercible into Series) + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def all( self, ): From b94bae9892e0fa79dc4bde0f4f1427d00accda6d Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:21:54 -0700 Subject: [PATCH 493/921] fix: loc setitem dtype issue. (#603) * fix: loc setitem dtype issue. * Update NaN selection * Update code example --- bigframes/core/indexers.py | 10 +++++++++- tests/system/small/test_dataframe.py | 14 +++++++++++--- .../bigframes_vendored/pandas/core/generic.py | 6 +++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index da6f3f3740b..bc03bd1df03 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -192,7 +192,15 @@ def __setitem__( and isinstance(key[0], bigframes.series.Series) and key[0].dtype == "boolean" ) and pd.api.types.is_scalar(value): - new_column = key[0].map({True: value, False: None}) + # For integer scalar, if set value to a new column, the dtype would be default to float. + # But if set value to an existing Int64 column, the dtype would still be integer. + # So we need to use different NaN type to match this behavior. + new_column = key[0].map( + { + True: value, + False: pd.NA if key[1] in self._dataframe.columns else None, + } + ) try: original_column = self._dataframe[key[1]] except KeyError: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ba205078ed3..e70764fcc0a 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2918,15 +2918,23 @@ def test_loc_setitem_bool_series_scalar_new_col(scalars_dfs): ) -def test_loc_setitem_bool_series_scalar_existing_col(scalars_dfs): +@pytest.mark.parametrize( + ("col", "value"), + [ + ("string_col", "hello"), + ("int64_col", 3), + ("float64_col", 3.5), + ], +) +def test_loc_setitem_bool_series_scalar_existing_col(scalars_dfs, col, value): if pd.__version__.startswith("1."): pytest.skip("this loc overload not supported in pandas 1.x.") scalars_df, scalars_pandas_df = scalars_dfs bf_df = scalars_df.copy() pd_df = scalars_pandas_df.copy() - bf_df.loc[bf_df["int64_too"] == 1, "string_col"] = "hello" - pd_df.loc[pd_df["int64_too"] == 1, "string_col"] = "hello" + bf_df.loc[bf_df["int64_too"] == 1, col] = value + pd_df.loc[pd_df["int64_too"] == 1, col] = value pd.testing.assert_frame_equal( bf_df.to_pandas(), diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 61bc39bb12a..baa9534a0ec 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -662,9 +662,9 @@ def copy(self): >>> df.loc[df["b"] == 2, "b"] = 22 >>> df - a b - 0 1 22.0 - 1 3 4.0 + a b + 0 1 22 + 1 3 4 [2 rows x 2 columns] >>> df_copy From 231cf298e1afee1a145aa8886f185a90cf64c93b Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 11 Apr 2024 10:32:48 -0700 Subject: [PATCH 494/921] refactor: Remove join name mapper code. (#597) * refactor: Remove join name mapper code. * remove dead import --- bigframes/core/compile/single_column.py | 12 +++++--- bigframes/core/joins/__init__.py | 5 +-- bigframes/core/joins/name_resolution.py | 41 ------------------------- bigframes/pandas/__init__.py | 1 + 4 files changed, 12 insertions(+), 47 deletions(-) delete mode 100644 bigframes/core/joins/name_resolution.py diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index f1a3d723ac4..dbf25891bf4 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -23,8 +23,8 @@ import ibis.expr.types as ibis_types import bigframes.core.compile.compiled as compiled +import bigframes.core.guid as guids import bigframes.core.join_def as join_defs -import bigframes.core.joins as joining import bigframes.core.ordering as orderings @@ -50,9 +50,13 @@ def join_by_column_ordered( finally, all the right columns. """ - l_hidden_mapping, r_hidden_mapping = joining.JoinNameRemapper(namespace="hidden")( - left._hidden_column_ids, right._hidden_column_ids - ) + l_hidden_mapping = { + id: guids.generate_guid("hidden_") for id in left._hidden_column_ids + } + r_hidden_mapping = { + id: guids.generate_guid("hidden_") for id in right._hidden_column_ids + } + l_mapping = {**join.get_left_mapping(), **l_hidden_mapping} r_mapping = {**join.get_right_mapping(), **r_hidden_mapping} diff --git a/bigframes/core/joins/__init__.py b/bigframes/core/joins/__init__.py index 415ee4e49d6..3c5b9605a31 100644 --- a/bigframes/core/joins/__init__.py +++ b/bigframes/core/joins/__init__.py @@ -15,6 +15,7 @@ """Helpers to join ArrayValue objects.""" from bigframes.core.joins.merge import merge -from bigframes.core.joins.name_resolution import JoinNameRemapper -__all__ = ("merge", "JoinNameRemapper") +__all__ = [ + "merge", +] diff --git a/bigframes/core/joins/name_resolution.py b/bigframes/core/joins/name_resolution.py deleted file mode 100644 index f648d28ad20..00000000000 --- a/bigframes/core/joins/name_resolution.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -from typing import Mapping, Sequence, Tuple - - -class JoinNameRemapper: - def __init__(self, namespace: str) -> None: - self._namespace = namespace - - def __call__( - self, left_column_ids: Sequence[str], right_column_ids: Sequence[str] - ) -> Tuple[Mapping[str, str], Mapping[str, str]]: - """ - When joining column ids from different namespaces, this function defines how names are remapped. - - Take care to map value column ids and hidden column ids in separate namespaces. This is important because value - column ids must be deterministic as they are referenced by dependent operators. The generation of hidden ids is - dependent on compilation context, and should be completely separated from value column id mappings. - """ - # This naming strategy depends on the number of value columns in source tables. - # This means column id mappings must be adjusted if pushing operations above or below join in transformation - new_left_ids = { - col: f"{self._namespace}_l_{i}" for i, col in enumerate(left_column_ids) - } - new_right_ids = { - col: f"{self._namespace}_r_{i}" for i, col in enumerate(right_column_ids) - } - return new_left_ids, new_right_ids diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index f5be4421e4c..91c3eb603ba 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -59,6 +59,7 @@ import bigframes.core.expression as ex import bigframes.core.global_session as global_session import bigframes.core.indexes +import bigframes.core.joins import bigframes.core.reshape import bigframes.core.tools import bigframes.dataframe From 4ec80340459e675b82b437f6c48b2872d362bafe Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 11 Apr 2024 11:24:38 -0700 Subject: [PATCH 495/921] feat: Add Series.autocorr (#605) --- bigframes/series.py | 3 ++ tests/system/small/test_series.py | 8 +++++ .../bigframes_vendored/pandas/core/series.py | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/bigframes/series.py b/bigframes/series.py index b975979eaf5..f11511f969a 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -791,6 +791,9 @@ def corr(self, other: Series, method="pearson", min_periods=None) -> float: ) return self._apply_binary_aggregation(other, agg_ops.CorrOp()) + def autocorr(self, lag: int = 1) -> float: + return self.corr(self.shift(lag)) + def cov(self, other: Series) -> float: return self._apply_binary_aggregation(other, agg_ops.CovOp()) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index c93af1bf2fd..6e4a87df4fb 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -718,6 +718,14 @@ def test_series_corr(scalars_dfs): assert math.isclose(pd_result, bf_result) +@skip_legacy_pandas +def test_series_autocorr(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df["float64_col"].autocorr(2) + pd_result = scalars_pandas_df["float64_col"].autocorr(2) + assert math.isclose(pd_result, bf_result) + + def test_series_cov(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df["int64_too"].cov(scalars_df["int64_too"]) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 572f29ff179..192e19fa5ad 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -844,6 +844,38 @@ def corr(self, other, method="pearson", min_periods=None) -> float: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def autocorr(self, lag: int = 1) -> float: + """ + Compute the lag-N autocorrelation. + + This method computes the Pearson correlation between + the Series and its shifted self. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([0.25, 0.5, 0.2, -0.05]) + >>> s.autocorr() # doctest: +ELLIPSIS + 0.10355... + >>> s.autocorr(lag=2) + -1.0 + + If the Pearson correlation is not well defined, then 'NaN' is returned. + + >>> s = bpd.Series([1, 0, 0, 0]) + >>> s.autocorr() + nan + + Args: + lag (int, default 1): + Number of lags to apply before performing autocorrelation. + + Returns: + float: The Pearson correlation between self and self.shift(lag). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def cov( self, other, From 8fc26c424b29a8b78542372e402fcc4e8fface7b Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 12 Apr 2024 05:27:17 +0000 Subject: [PATCH 496/921] docs: add docs for `DataFrame` and `Series` dunder methods (#562) * docs: add docs for `DataFrame.{radd,__add__,__radd__}` * fix rendering, revert ineffective changes, add __eq__ * newline * docs for more df dunders * fix mypy errors and couple of wordings * fix sub and rmod, add docs for __bool__, __nonzero__, __getattr__ * add documentation for Series dunders * fix doctest failure with python 3.12 * move docstrings to third_party for compliance safety * add DataFrame.__getitem__ docstring and code samples * add dunder doc overrides from third_party --- bigframes/dataframe.py | 160 ++- bigframes/series.py | 71 +- .../bigframes_vendored/pandas/core/frame.py | 1050 ++++++++++++++++- .../bigframes_vendored/pandas/core/generic.py | 30 + .../bigframes_vendored/pandas/core/series.py | 567 ++++++++- 5 files changed, 1803 insertions(+), 75 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 32f5a36f790..2deef952770 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -17,6 +17,7 @@ from __future__ import annotations import datetime +import inspect import re import sys import textwrap @@ -314,6 +315,8 @@ def __len__(self): rows, _ = self.shape return rows + __len__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__len__) + def __iter__(self): return iter(self.columns) @@ -466,7 +469,6 @@ def __getitem__( bigframes.series.Series, ], ): # No return type annotations (like pandas) as type cannot always be determined statically - """Gets the specified column(s) from the DataFrame.""" # NOTE: This implements the operations described in # https://pandas.pydata.org/docs/getting_started/intro_tutorials/03_subset_data.html @@ -498,6 +500,8 @@ def __getitem__( return DataFrame(self._block.select_columns(selected_ids)) + __getitem__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__getitem__) + def _getitem_label(self, key: blocks.Label): col_ids = self._block.cols_matching_label(key) if len(col_ids) == 0: @@ -642,14 +646,11 @@ def _repr_html_(self) -> str: return html_string def __setitem__(self, key: str, value: SingleItemValue): - """Modify or insert a column into the DataFrame. - - Note: This does **not** modify the original table the DataFrame was - derived from. - """ df = self._assign_single_item(key, value) self._set_block(df._get_block()) + __setitem__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__setitem__) + def _apply_binop( self, other: float | int | bigframes.series.Series | DataFrame, @@ -838,32 +839,50 @@ def _apply_dataframe_binop( def eq(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.eq_op, axis=axis) + def __eq__(self, other) -> DataFrame: # type: ignore + return self.eq(other) + + __eq__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__eq__) + def ne(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.ne_op, axis=axis) - __eq__ = eq # type: ignore + def __ne__(self, other) -> DataFrame: # type: ignore + return self.ne(other) - __ne__ = ne # type: ignore + __ne__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__ne__) def le(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.le_op, axis=axis) + def __le__(self, other) -> DataFrame: + return self.le(other) + + __le__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__le__) + def lt(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.lt_op, axis=axis) + def __lt__(self, other) -> DataFrame: + return self.lt(other) + + __lt__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__lt__) + def ge(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.ge_op, axis=axis) - def gt(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: - return self._apply_binop(other, ops.gt_op, axis=axis) + def __ge__(self, other) -> DataFrame: + return self.ge(other) - __lt__ = lt + __ge__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__ge__) - __le__ = le + def gt(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: + return self._apply_binop(other, ops.gt_op, axis=axis) - __gt__ = gt + def __gt__(self, other) -> DataFrame: + return self.gt(other) - __ge__ = ge + __gt__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__gt__) def add( self, @@ -874,7 +893,21 @@ def add( # TODO(swast): Support level parameter with MultiIndex. return self._apply_binop(other, ops.add_op, axis=axis) - __radd__ = __add__ = radd = add + def radd( + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", + ) -> DataFrame: + # TODO(swast): Support fill_value parameter. + # TODO(swast): Support level parameter with MultiIndex. + return self.add(other, axis=axis) + + def __add__(self, other) -> DataFrame: + return self.add(other) + + __add__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__add__) + + __radd__ = __add__ def sub( self, @@ -883,7 +916,13 @@ def sub( ) -> DataFrame: return self._apply_binop(other, ops.sub_op, axis=axis) - __sub__ = subtract = sub + subtract = sub + subtract.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.sub) + + def __sub__(self, other): + return self.sub(other) + + __sub__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__sub__) def rsub( self, @@ -892,7 +931,10 @@ def rsub( ) -> DataFrame: return self._apply_binop(other, ops.sub_op, axis=axis, reverse=True) - __rsub__ = rsub + def __rsub__(self, other): + return self.rsub(other) + + __rsub__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__rsub__) def mul( self, @@ -901,7 +943,25 @@ def mul( ) -> DataFrame: return self._apply_binop(other, ops.mul_op, axis=axis) - __rmul__ = __mul__ = rmul = multiply = mul + multiply = mul + multiply.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.mul) + + def __mul__(self, other): + return self.mul(other) + + __mul__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__mul__) + + def rmul( + self, + other: float | int | bigframes.series.Series | DataFrame, + axis: str | int = "columns", + ) -> DataFrame: + return self.mul(other, axis=axis) + + def __rmul__(self, other): + return self.rmul(other) + + __rmul__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__rmul__) def truediv( self, @@ -910,7 +970,13 @@ def truediv( ) -> DataFrame: return self._apply_binop(other, ops.div_op, axis=axis) - div = divide = __truediv__ = truediv + truediv.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.truediv) + div = divide = truediv + + def __truediv__(self, other): + return self.truediv(other) + + __truediv__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__truediv__) def rtruediv( self, @@ -919,7 +985,13 @@ def rtruediv( ) -> DataFrame: return self._apply_binop(other, ops.div_op, axis=axis, reverse=True) - __rtruediv__ = rdiv = rtruediv + rdiv = rtruediv + rdiv.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.rtruediv) + + def __rtruediv__(self, other): + return self.rtruediv(other) + + __rtruediv__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__rtruediv__) def floordiv( self, @@ -928,7 +1000,10 @@ def floordiv( ) -> DataFrame: return self._apply_binop(other, ops.floordiv_op, axis=axis) - __floordiv__ = floordiv + def __floordiv__(self, other): + return self.floordiv(other) + + __floordiv__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__floordiv__) def rfloordiv( self, @@ -937,31 +1012,48 @@ def rfloordiv( ) -> DataFrame: return self._apply_binop(other, ops.floordiv_op, axis=axis, reverse=True) - __rfloordiv__ = rfloordiv + def __rfloordiv__(self, other): + return self.rfloordiv(other) + + __rfloordiv__.__doc__ = inspect.getdoc( + vendored_pandas_frame.DataFrame.__rfloordiv__ + ) def mod(self, other: int | bigframes.series.Series | DataFrame, axis: str | int = "columns") -> DataFrame: # type: ignore return self._apply_binop(other, ops.mod_op, axis=axis) + def __mod__(self, other): + return self.mod(other) + + __mod__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__mod__) + def rmod(self, other: int | bigframes.series.Series | DataFrame, axis: str | int = "columns") -> DataFrame: # type: ignore return self._apply_binop(other, ops.mod_op, axis=axis, reverse=True) - __mod__ = mod + def __rmod__(self, other): + return self.rmod(other) - __rmod__ = rmod + __rmod__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__rmod__) def pow( self, other: int | bigframes.series.Series, axis: str | int = "columns" ) -> DataFrame: return self._apply_binop(other, ops.pow_op, axis=axis) + def __pow__(self, other): + return self.pow(other) + + __pow__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__pow__) + def rpow( self, other: int | bigframes.series.Series, axis: str | int = "columns" ) -> DataFrame: return self._apply_binop(other, ops.pow_op, axis=axis, reverse=True) - __pow__ = pow + def __rpow__(self, other): + return self.rpow(other) - __rpow__ = rpow + __rpow__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__rpow__) def align( self, @@ -1971,6 +2063,7 @@ def prod( return bigframes.series.Series(block.select_column("values")) product = prod + product.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.prod) def count(self, *, numeric_only: bool = False) -> bigframes.series.Series: if not numeric_only: @@ -2010,6 +2103,7 @@ def agg( ) aggregate = agg + aggregate.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.agg) def idxmin(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmin(self._block)) @@ -2083,6 +2177,7 @@ def kurt(self, *, numeric_only: bool = False): return bigframes.series.Series(result_block) kurtosis = kurt + kurtosis.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.kurt) def _pivot( self, @@ -2542,11 +2637,13 @@ def isna(self) -> DataFrame: return self._apply_unary_op(ops.isnull_op) isnull = isna + isnull.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.isna) def notna(self) -> DataFrame: return self._apply_unary_op(ops.notnull_op) notnull = notna + notnull.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.notna) def cumsum(self): is_numeric_types = [ @@ -2860,7 +2957,10 @@ def to_numpy( ) -> numpy.ndarray: return self.to_pandas().to_numpy(dtype, copy, na_value, **kwargs) - __array__ = to_numpy + def __array__(self, dtype=None) -> numpy.ndarray: + return self.to_numpy(dtype=dtype) + + __array__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__array__) def to_parquet( self, @@ -3227,6 +3327,7 @@ def first_valid_index(self): return applymap = map + applymap.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.map) def _slice( self, @@ -3367,4 +3468,7 @@ def get_right_id(id): def plot(self): return plotting.PlotAccessor(self) - __matmul__ = dot + def __matmul__(self, other) -> DataFrame: + return self.dot(other) + + __matmul__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__matmul__) diff --git a/bigframes/series.py b/bigframes/series.py index f11511f969a..2f9123f9a3b 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -17,6 +17,7 @@ from __future__ import annotations import functools +import inspect import itertools import numbers import os @@ -180,6 +181,8 @@ def _set_internal_query_job(self, query_job: bigquery.QueryJob): def __len__(self): return self.shape[0] + __len__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__len__) + def __iter__(self) -> typing.Iterator: self._optimize_query_complexity() return itertools.chain.from_iterable( @@ -423,6 +426,7 @@ def ffill(self, *, limit: typing.Optional[int] = None) -> Series: return self._apply_window_op(agg_ops.LastNonNullOp(), window) pad = ffill + pad.__doc__ = inspect.getdoc(vendored_pandas_series.Series.ffill) def bfill(self, *, limit: typing.Optional[int] = None) -> Series: window = bigframes.core.window_spec.WindowSpec(preceding=0, following=limit) @@ -609,28 +613,38 @@ def isna(self) -> "Series": return self._apply_unary_op(ops.isnull_op) isnull = isna + isnull.__doc__ = inspect.getdoc(vendored_pandas_series.Series.isna) def notna(self) -> "Series": return self._apply_unary_op(ops.notnull_op) notnull = notna + notnull.__doc__ = inspect.getdoc(vendored_pandas_series.Series.notna) def __and__(self, other: bool | int | Series) -> Series: return self._apply_binary_op(other, ops.and_op) + __and__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__and__) + __rand__ = __and__ def __or__(self, other: bool | int | Series) -> Series: return self._apply_binary_op(other, ops.or_op) + __or__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__or__) + __ror__ = __or__ def __add__(self, other: float | int | Series) -> Series: return self.add(other) + __add__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__add__) + def __radd__(self, other: float | int | Series) -> Series: return self.radd(other) + __radd__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__radd__) + def add(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.add_op) @@ -640,9 +654,13 @@ def radd(self, other: float | int | Series) -> Series: def __sub__(self, other: float | int | Series) -> Series: return self.sub(other) + __sub__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__sub__) + def __rsub__(self, other: float | int | Series) -> Series: return self.rsub(other) + __rsub__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__rsub__) + def sub(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.sub_op) @@ -650,13 +668,18 @@ def rsub(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.sub_op, reverse=True) subtract = sub + subtract.__doc__ = inspect.getdoc(vendored_pandas_series.Series.sub) def __mul__(self, other: float | int | Series) -> Series: return self.mul(other) + __mul__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__mul__) + def __rmul__(self, other: float | int | Series) -> Series: return self.rmul(other) + __rmul__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__rmul__) + def mul(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.mul_op) @@ -664,31 +687,40 @@ def rmul(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.mul_op, reverse=True) multiply = mul + multiply.__doc__ = inspect.getdoc(vendored_pandas_series.Series.mul) def __truediv__(self, other: float | int | Series) -> Series: return self.truediv(other) + __truediv__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__truediv__) + def __rtruediv__(self, other: float | int | Series) -> Series: return self.rtruediv(other) + __rtruediv__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__rtruediv__) + def truediv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.div_op) def rtruediv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.div_op, reverse=True) - div = truediv - - divide = truediv + truediv.__doc__ = inspect.getdoc(vendored_pandas_series.Series.truediv) + div = divide = truediv rdiv = rtruediv + rdiv.__doc__ = inspect.getdoc(vendored_pandas_series.Series.rtruediv) def __floordiv__(self, other: float | int | Series) -> Series: return self.floordiv(other) + __floordiv__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__floordiv__) + def __rfloordiv__(self, other: float | int | Series) -> Series: return self.rfloordiv(other) + __rfloordiv__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__rfloordiv__) + def floordiv(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.floordiv_op) @@ -698,9 +730,13 @@ def rfloordiv(self, other: float | int | Series) -> Series: def __pow__(self, other: float | int | Series) -> Series: return self.pow(other) + __pow__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__pow__) + def __rpow__(self, other: float | int | Series) -> Series: return self.rpow(other) + __rpow__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__rpow__) + def pow(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.pow_op) @@ -734,9 +770,13 @@ def ge(self, other) -> Series: def __mod__(self, other) -> Series: # type: ignore return self.mod(other) + __mod__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__mod__) + def __rmod__(self, other) -> Series: # type: ignore return self.rmod(other) + __rmod__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__rmod__) + def mod(self, other) -> Series: # type: ignore return self._apply_binary_op(other, ops.mod_op) @@ -753,10 +793,18 @@ def rdivmod(self, other) -> Tuple[Series, Series]: # type: ignore # the output should be dtype float, both floordiv and mod returns dtype int in this case. return (self.rfloordiv(other), self.rmod(other)) - def __matmul__(self, other): + def dot(self, other): return (self * other).sum() - dot = __matmul__ + def __matmul__(self, other): + return self.dot(other) + + __matmul__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__matmul__) + + def __rmatmul__(self, other): + return self.dot(other) + + __rmatmul__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__rmatmul__) def combine_first(self, other: Series) -> Series: result = self._apply_binary_op(other, ops.coalesce_op) @@ -849,6 +897,7 @@ def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: ) aggregate = agg + aggregate.__doc__ = inspect.getdoc(vendored_pandas_series.Series.agg) def skew(self): count = self.count() @@ -883,6 +932,7 @@ def kurt(self): return (numerator / denominator) - adjustment kurtosis = kurt + kurtosis.__doc__ = inspect.getdoc(vendored_pandas_series.Series.kurt) def mode(self) -> Series: block = self._block @@ -930,6 +980,7 @@ def prod(self) -> float: return typing.cast(float, self._apply_aggregation(agg_ops.product_op)) product = prod + product.__doc__ = inspect.getdoc(vendored_pandas_series.Series.prod) def __eq__(self, other: object) -> Series: # type: ignore return self.eq(other) @@ -940,6 +991,8 @@ def __ne__(self, other: object) -> Series: # type: ignore def __invert__(self) -> Series: return self._apply_unary_op(ops.invert_op) + __invert__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__invert__) + def eq(self, other: object) -> Series: # TODO: enforce stricter alignment return self._apply_binary_op(other, ops.eq_op) @@ -1074,6 +1127,8 @@ def __getitem__(self, indexer): return Series(block) return self.loc[indexer] + __getitem__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__getitem__) + def __getattr__(self, key: str): if hasattr(pandas.Series, key): raise AttributeError( @@ -1461,6 +1516,7 @@ def tolist(self) -> list: return self.to_pandas().to_list() to_list = tolist + to_list.__doc__ = inspect.getdoc(vendored_pandas_series.Series.tolist) def to_markdown( self, @@ -1476,7 +1532,10 @@ def to_numpy( ) -> numpy.ndarray: return self.to_pandas().to_numpy(dtype, copy, na_value, **kwargs) - __array__ = to_numpy + def __array__(self, dtype=None) -> numpy.ndarray: + return self.to_numpy(dtype=dtype) + + __array__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__array__) def to_pickle(self, path, **kwargs) -> None: return self.to_pandas().to_pickle(path, **kwargs) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 1fc80449d14..6707dc14036 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -339,6 +339,7 @@ def to_gbq( [2 rows x 2 columns] Write a DataFrame to a BigQuery table with clustering columns: + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4], 'col3': [5, 6]}) >>> clustering_cols = ['col1', 'col3'] >>> df.to_gbq( @@ -910,28 +911,6 @@ def to_orc(self, path=None, **kwargs) -> bytes | None: # ---------------------------------------------------------------------- # Unsorted - def equals(self, other) -> bool: - """ - Test whether two objects contain the same elements. - - This function allows two Series or DataFrames to be compared against - each other to see if they have the same shape and elements. NaNs in - the same location are considered equal. - - The row/column index do not need to have the same type, as long - as the values are considered equal. Corresponding columns must be of - the same dtype. - - Args: - other (Series or DataFrame): - The other Series or DataFrame to be compared with the first. - - Returns: - bool: True if all elements are the same in both objects, False - otherwise. - """ - raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def assign(self, **kwargs) -> DataFrame: r""" Assign new columns to a DataFrame. @@ -1208,7 +1187,6 @@ def rename_axis(self, mapper: Optional[str], **kwargs) -> DataFrame: Set the name of the axis for the index. .. note:: - Currently only accepts a single string parameter (the new name of the index). Args: @@ -1862,7 +1840,7 @@ def sort_index( raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) # ---------------------------------------------------------------------- - # Arithmetic Methods + # Arithmetic and Logical Methods def eq(self, other, axis: str | int = "columns") -> DataFrame: """ @@ -1890,7 +1868,8 @@ def eq(self, other, axis: str | int = "columns") -> DataFrame: rectangle True Name: degrees, dtype: boolean - You can also use arithmetic operator ``==``: + You can also use logical operator `==`: + >>> df["degrees"] == 360 circle True triangle False @@ -1909,6 +1888,39 @@ def eq(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __eq__(self, other): + """ + Check equality of DataFrame and other, element-wise, using logical + operator `==`. + + Equivalent to `DataFrame.eq(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'a': [0, 3, 4], + ... 'b': [360, 0, 180] + ... }) + >>> df == 0 + a b + 0 True False + 1 False True + 2 False False + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to be compared to the DataFrame for equality. + + Returns: + DataFrame: The result of comparing `other` to DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def ne(self, other, axis: str | int = "columns") -> DataFrame: """ Get not equal to of DataFrame and other, element-wise (binary operator `ne`). @@ -1954,6 +1966,39 @@ def ne(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __ne__(self, other): + """ + Check inequality of DataFrame and other, element-wise, using logical + operator `!=`. + + Equivalent to `DataFrame.ne(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'a': [0, 3, 4], + ... 'b': [360, 0, 180] + ... }) + >>> df != 0 + a b + 0 False True + 1 True False + 2 True True + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to be compared to the DataFrame for inequality. + + Returns: + DataFrame: The result of comparing `other` to DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def le(self, other, axis: str | int = "columns") -> DataFrame: """Get 'less than or equal to' of dataframe and other, element-wise (binary operator `<=`). @@ -2004,6 +2049,39 @@ def le(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __le__(self, other): + """ + Check whether DataFrame is less than or equal to other, element-wise, + using logical operator `<=`. + + Equivalent to `DataFrame.le(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'a': [0, -1, 1], + ... 'b': [1, 0, -1] + ... }) + >>> df <= 0 + a b + 0 True False + 1 True True + 2 False True + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to be compared to the DataFrame. + + Returns: + DataFrame: The result of comparing `other` to DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def lt(self, other, axis: str | int = "columns") -> DataFrame: """Get 'less than' of DataFrame and other, element-wise (binary operator `<`). @@ -2054,6 +2132,39 @@ def lt(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __lt__(self, other): + """ + Check whether DataFrame is less than other, element-wise, using logical + operator `<`. + + Equivalent to `DataFrame.lt(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'a': [0, -1, 1], + ... 'b': [1, 0, -1] + ... }) + >>> df < 0 + a b + 0 False False + 1 True False + 2 False True + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to be compared to the DataFrame. + + Returns: + DataFrame: The result of comparing `other` to DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def ge(self, other, axis: str | int = "columns") -> DataFrame: """Get 'greater than or equal to' of DataFrame and other, element-wise (binary operator `>=`). @@ -2104,6 +2215,39 @@ def ge(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __ge__(self, other): + """ + Check whether DataFrame is greater than or equal to other, element-wise, + using logical operator `>=`. + + Equivalent to `DataFrame.ge(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'a': [0, -1, 1], + ... 'b': [1, 0, -1] + ... }) + >>> df >= 0 + a b + 0 True True + 1 False True + 2 True False + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to be compared to the DataFrame. + + Returns: + DataFrame: The result of comparing `other` to DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def gt(self, other, axis: str | int = "columns") -> DataFrame: """Get 'greater than' of DataFrame and other, element-wise (binary operator `>`). @@ -2152,6 +2296,39 @@ def gt(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __gt__(self, other): + """ + Check whether DataFrame is greater than other, element-wise, using logical + operator `>`. + + Equivalent to `DataFrame.gt(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'a': [0, -1, 1], + ... 'b': [1, 0, -1] + ... }) + >>> df > 0 + a b + 0 False True + 1 False False + 2 True False + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to be compared to the DataFrame. + + Returns: + DataFrame: The result of comparing `other` to DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def add(self, other, axis: str | int = "columns") -> DataFrame: """Get addition of DataFrame and other, element-wise (binary operator `+`). @@ -2183,7 +2360,126 @@ def add(self, other, axis: str | int = "columns") -> DataFrame: You can also use arithmetic operator ``+``: - >>> df['A'] + (df['B']) + >>> df['A'] + df['B'] + 0 5 + 1 7 + 2 9 + dtype: Int64 + + Args: + other (float, int, or Series): + Any single or multiple element data structure, or list-like object. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. + + Returns: + DataFrame: DataFrame result of the arithmetic operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __add__(self, other) -> DataFrame: + """Get addition of DataFrame and other, column-wise, using arithmatic + operator `+`. + + Equivalent to ``DataFrame.add(other)``. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'height': [1.5, 2.6], + ... 'weight': [500, 800] + ... }, + ... index=['elk', 'moose']) + >>> df + height weight + elk 1.5 500 + moose 2.6 800 + + [2 rows x 2 columns] + + Adding a scalar affects all rows and columns. + + >>> df + 1.5 + height weight + elk 3.0 501.5 + moose 4.1 801.5 + + [2 rows x 2 columns] + + You can add another DataFrame with index and columns aligned. + + >>> delta = bpd.DataFrame({ + ... 'height': [0.5, 0.9], + ... 'weight': [50, 80] + ... }, + ... index=['elk', 'moose']) + >>> df + delta + height weight + elk 2.0 550 + moose 3.5 880 + + [2 rows x 2 columns] + + Adding any mis-aligned index and columns will result in invalid values. + + >>> delta = bpd.DataFrame({ + ... 'depth': [0.5, 0.9, 1.0], + ... 'weight': [50, 80, 100] + ... }, + ... index=['elk', 'moose', 'bison']) + >>> df + delta + depth height weight + elk 550 + moose 880 + bison + + [3 rows x 3 columns] + + Args: + other (scalar or DataFrame): + Object to be added to the DataFrame. + + Returns: + DataFrame: The result of adding `other` to DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def radd(self, other, axis: str | int = "columns") -> DataFrame: + """Get addition of DataFrame and other, element-wise (binary operator `+`). + + Equivalent to ``other + dataframe``. With reverse version, `add`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + .. note:: + Mismatched indices will be unioned together. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].radd(df['B']) + 0 5 + 1 7 + 2 9 + dtype: Int64 + + You can also use arithmetic operator ``+``: + + >>> df['A'] + df['B'] 0 5 1 7 2 9 @@ -2250,6 +2546,49 @@ def sub(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __sub__(self, other): + """ + Get subtraction of other from DataFrame, element-wise, using operator `-`. + + Equivalent to `DataFrame.sub(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can subtract a scalar: + + >>> df = bpd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> df - 2 + a b + 0 -1 2 + 1 0 3 + 2 1 4 + + [3 rows x 2 columns] + + You can also subtract another DataFrame with index and column labels + aligned: + + >>> df1 = bpd.DataFrame({"a": [2, 2, 2], "b": [3, 3, 3]}) + >>> df - df1 + a b + 0 -1 1 + 1 0 2 + 2 1 3 + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to subtract from the DataFrame. + + Returns: + DataFrame: The result of the subtraction. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rsub(self, other, axis: str | int = "columns") -> DataFrame: """Get subtraction of DataFrame and other, element-wise (binary operator `-`). @@ -2296,6 +2635,21 @@ def rsub(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rsub__(self, other): + """ + Get subtraction of DataFrame from other, element-wise, using operator `-`. + + Equivalent to `DataFrame.rsub(other)`. + + Args: + other (scalar or DataFrame): + Object to subtract the DataFrame from. + + Returns: + DataFrame: The result of the subtraction. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def mul(self, other, axis: str | int = "columns") -> DataFrame: """Get multiplication of DataFrame and other, element-wise (binary operator `*`). @@ -2345,6 +2699,141 @@ def mul(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __mul__(self, other): + """ + Get multiplication of DataFrame with other, element-wise, using operator `*`. + + Equivalent to `DataFrame.mul(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can multiply with a scalar: + + >>> df = bpd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> df * 3 + a b + 0 3 12 + 1 6 15 + 2 9 18 + + [3 rows x 2 columns] + + You can also multiply with another DataFrame with index and column labels + aligned: + + >>> df1 = bpd.DataFrame({"a": [2, 2, 2], "b": [3, 3, 3]}) + >>> df * df1 + a b + 0 2 12 + 1 4 15 + 2 6 18 + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to multiply with the DataFrame. + + Returns: + DataFrame: The result of the multiplication. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def rmul(self, other, axis: str | int = "columns") -> DataFrame: + """Get multiplication of DataFrame and other, element-wise (binary operator `*`). + + Equivalent to ``other * dataframe``. With reverse version, `mul`. + + Among flexible wrappers (`add`, `sub`, `mul`, `div`, `mod`, `pow`) to + arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**`. + + .. note:: + Mismatched indices will be unioned together. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'A': [1, 2, 3], + ... 'B': [4, 5, 6], + ... }) + + You can use method name: + + >>> df['A'].rmul(df['B']) + 0 4 + 1 10 + 2 18 + dtype: Int64 + + You can also use arithmetic operator ``*``: + + >>> df['A'] * (df['B']) + 0 4 + 1 10 + 2 18 + dtype: Int64 + + Args: + other (float, int, or Series): + Any single or multiple element data structure, or list-like object. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. + + Returns: + DataFrame: DataFrame result of the arithmetic operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __rmul__(self, other): + """ + Get multiplication of DataFrame with other, element-wise, using operator `*`. + + Equivalent to `DataFrame.rmul(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can multiply with a scalar: + + >>> df = bpd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> df * 3 + a b + 0 3 12 + 1 6 15 + 2 9 18 + + [3 rows x 2 columns] + + You can also multiply with another DataFrame with index and column labels + aligned: + + >>> df1 = bpd.DataFrame({"a": [2, 2, 2], "b": [3, 3, 3]}) + >>> df * df1 + a b + 0 2 12 + 1 4 15 + 2 6 18 + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to multiply the DataFrame with. + + Returns: + DataFrame: The result of the multiplication. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def truediv(self, other, axis: str | int = "columns") -> DataFrame: """Get floating division of DataFrame and other, element-wise (binary operator `/`). @@ -2383,14 +2872,57 @@ def truediv(self, other, axis: str | int = "columns") -> DataFrame: dtype: Float64 Args: - other (float, int, or Series): - Any single or multiple element data structure, or list-like object. - axis ({0 or 'index', 1 or 'columns'}): - Whether to compare by the index (0 or 'index') or columns. - (1 or 'columns'). For Series input, axis to match Series index on. + other (float, int, or Series): + Any single or multiple element data structure, or list-like object. + axis ({0 or 'index', 1 or 'columns'}): + Whether to compare by the index (0 or 'index') or columns. + (1 or 'columns'). For Series input, axis to match Series index on. + + Returns: + DataFrame: DataFrame result of the arithmetic operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __truediv__(self, other): + """ + Get division of DataFrame by other, element-wise, using operator `/`. + + Equivalent to `DataFrame.truediv(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can multiply with a scalar: + + >>> df = bpd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> df / 2 + a b + 0 0.5 2.0 + 1 1.0 2.5 + 2 1.5 3.0 + + [3 rows x 2 columns] + + You can also multiply with another DataFrame with index and column labels + aligned: + + >>> denominator = bpd.DataFrame({"a": [2, 2, 2], "b": [3, 3, 3]}) + >>> df / denominator + a b + 0 0.5 1.333333 + 1 1.0 1.666667 + 2 1.5 2.0 + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to divide the DataFrame by. Returns: - DataFrame: DataFrame result of the arithmetic operation. + DataFrame: The result of the division. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -2440,6 +2972,21 @@ def rtruediv(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rtruediv__(self, other): + """ + Get division of other by DataFrame, element-wise, using operator `/`. + + Equivalent to `DataFrame.rtruediv(other)`. + + Args: + other (scalar or DataFrame): + Object to divide by the DataFrame. + + Returns: + DataFrame: The result of the division. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def floordiv(self, other, axis: str | int = "columns") -> DataFrame: """Get integer division of DataFrame and other, element-wise (binary operator `//`). @@ -2489,6 +3036,49 @@ def floordiv(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __floordiv__(self, other): + """ + Get integer divison of DataFrame by other, using arithmatic operator `//`. + + Equivalent to `DataFrame.floordiv(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can divide by a scalar: + + >>> df = bpd.DataFrame({"a": [15, 15, 15], "b": [30, 30, 30]}) + >>> df // 2 + a b + 0 7 15 + 1 7 15 + 2 7 15 + + [3 rows x 2 columns] + + You can also divide by another DataFrame with index and column labels + aligned: + + >>> divisor = bpd.DataFrame({"a": [2, 3, 4], "b": [5, 6, 7]}) + >>> df // divisor + a b + 0 7 6 + 1 5 5 + 2 3 4 + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to divide the DataFrame by. + + Returns: + DataFrame: The result of the integer divison. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rfloordiv(self, other, axis: str | int = "columns") -> DataFrame: """Get integer division of DataFrame and other, element-wise (binary operator `//`). @@ -2535,6 +3125,21 @@ def rfloordiv(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rfloordiv__(self, other): + """ + Get integer divison of other by DataFrame. + + Equivalent to `DataFrame.rfloordiv(other)`. + + Args: + other (scalar or DataFrame): + Object to divide by the DataFrame. + + Returns: + DataFrame: The result of the integer divison. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def mod(self, other, axis: str | int = "columns") -> DataFrame: """Get modulo of DataFrame and other, element-wise (binary operator `%`). @@ -2584,6 +3189,49 @@ def mod(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __mod__(self, other): + """ + Get modulo of DataFrame with other, element-wise, using operator `%`. + + Equivalent to `DataFrame.mod(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can modulo with a scalar: + + >>> df = bpd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> df % 3 + a b + 0 1 1 + 1 2 2 + 2 0 0 + + [3 rows x 2 columns] + + You can also modulo with another DataFrame with index and column labels + aligned: + + >>> modulo = bpd.DataFrame({"a": [2, 2, 2], "b": [3, 3, 3]}) + >>> df % modulo + a b + 0 1 1 + 1 0 2 + 2 1 0 + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to modulo the DataFrame by. + + Returns: + DataFrame: The result of the modulo. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rmod(self, other, axis: str | int = "columns") -> DataFrame: """Get modulo of DataFrame and other, element-wise (binary operator `%`). @@ -2630,6 +3278,21 @@ def rmod(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rmod__(self, other): + """ + Get integer divison of other by DataFrame. + + Equivalent to `DataFrame.rmod(other)`. + + Args: + other (scalar or DataFrame): + Object to modulo by the DataFrame. + + Returns: + DataFrame: The result of the modulo. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def pow(self, other, axis: str | int = "columns") -> DataFrame: """Get Exponential power of dataframe and other, element-wise (binary operator `**`). @@ -2680,6 +3343,50 @@ def pow(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __pow__(self, other): + """ + Get exponentiation of DataFrame with other, element-wise, using operator + `**`. + + Equivalent to `DataFrame.pow(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can exponentiate with a scalar: + + >>> df = bpd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + >>> df ** 2 + a b + 0 1 16 + 1 4 25 + 2 9 36 + + [3 rows x 2 columns] + + You can also exponentiate with another DataFrame with index and column + labels aligned: + + >>> exponent = bpd.DataFrame({"a": [2, 2, 2], "b": [3, 3, 3]}) + >>> df ** exponent + a b + 0 1 64 + 1 4 125 + 2 9 216 + + [3 rows x 2 columns] + + Args: + other (scalar or DataFrame): + Object to exponentiate the DataFrame with. + + Returns: + DataFrame: The result of the exponentiation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rpow(self, other, axis: str | int = "columns") -> DataFrame: """Get Exponential power of dataframe and other, element-wise (binary operator `rpow`). @@ -2727,6 +3434,22 @@ def rpow(self, other, axis: str | int = "columns") -> DataFrame: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rpow__(self, other): + """ + Get exponentiation of other with DataFrame, element-wise, using operator + `**`. + + Equivalent to `DataFrame.rpow(other)`. + + Args: + other (scalar or DataFrame): + Object to exponentiate with the DataFrame. + + Returns: + DataFrame: The result of the exponentiation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def combine( self, other, func, fill_value=None, overwrite: bool = True ) -> DataFrame: @@ -4102,7 +4825,6 @@ def nsmallest(self, n: int, columns, keep: str = "first"): performant. .. note:: - This function cannot be used with all column types. For example, when specifying columns with `object` or `category` dtypes, ``TypeError`` is raised. @@ -5074,6 +5796,7 @@ def eval(self, expr: str) -> DataFrame: injection if you pass user input to this function. **Examples:** + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None @@ -5095,11 +5818,11 @@ def eval(self, expr: str) -> DataFrame: 4 7 dtype: Int64 - Assignment is allowed though by default the original DataFrame is not - modified. + Assignment is allowed though by default the original DataFrame is not + modified. >>> df.eval('C = A + B') - A B C + A B C 0 1 10 11 1 2 8 10 2 3 6 9 @@ -5108,7 +5831,7 @@ def eval(self, expr: str) -> DataFrame: [5 rows x 3 columns] >>> df - A B + A B 0 1 10 1 2 8 2 3 6 @@ -5117,7 +5840,7 @@ def eval(self, expr: str) -> DataFrame: [5 rows x 2 columns] - Multiple columns can be assigned to using multi-line expressions: + Multiple columns can be assigned to using multi-line expressions: >>> df.eval( ... ''' @@ -5125,7 +5848,7 @@ def eval(self, expr: str) -> DataFrame: ... D = A - B ... ''' ... ) - A B C D + A B C D 0 1 10 11 -9 1 2 8 10 -6 2 3 6 9 -3 @@ -5149,6 +5872,7 @@ def query(self, expr: str) -> DataFrame | None: Query the columns of a DataFrame with a boolean expression. **Examples:** + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None @@ -5521,6 +6245,7 @@ def dot(self, other): DataFrame and the index of other must contain the same values, as they will be aligned prior to the multiplication. + .. note:: The dot method for Series computes the inner product, instead of the matrix product here. @@ -5607,6 +6332,59 @@ def dot(self, other): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __matmul__(self, other): + """ + Compute the matrix multiplication between the DataFrame and other, using + operator `@`. + + Equivalent to `DataFrame.dot(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> left = bpd.DataFrame([[0, 1, -2, -1], [1, 1, 1, 1]]) + >>> left + 0 1 2 3 + 0 0 1 -2 -1 + 1 1 1 1 1 + + [2 rows x 4 columns] + >>> right = bpd.DataFrame([[0, 1], [1, 2], [-1, -1], [2, 0]]) + >>> right + 0 1 + 0 0 1 + 1 1 2 + 2 -1 -1 + 3 2 0 + + [4 rows x 2 columns] + >>> left @ right + 0 1 + 0 1 4 + 1 2 2 + + [2 rows x 2 columns] + + The operand can be a Series, in which case the result will also be a + Series: + + >>> right = bpd.Series([1, 2, -1,0]) + >>> left @ right + 0 4 + 1 2 + dtype: Int64 + + Args: + other (DataFrame or Series): + Object to be matrix multiplied with the DataFrame. + + Returns: + DataFrame or Series: The result of the matrix multiplication. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property def plot(self): """ @@ -5617,3 +6395,197 @@ def plot(self): An accessor making plots. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __len__(self): + """Returns number of rows in the DataFrame, serves `len` operator. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... 'a': [0, 1, 2], + ... 'b': [3, 4, 5] + ... }) + >>> len(df) + 3 + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __array__(self): + """ + Returns the rows as NumPy array. + + Equivalent to `DataFrame.to_numpy(dtype)`. + + Users should not call this directly. Rather, it is invoked by + `numpy.array` and `numpy.asarray`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> import numpy as np + + >>> df = bpd.DataFrame({"a": [1, 2, 3], "b": [11, 22, 33]}) + + >>> np.array(df) + array([[1, 11], + [2, 22], + [3, 33]], dtype=object) + + >>> np.asarray(df) + array([[1, 11], + [2, 22], + [3, 33]], dtype=object) + + Args: + dtype (str or numpy.dtype, optional): + The dtype to use for the resulting NumPy array. By default, + the dtype is inferred from the data. + + Returns: + numpy.ndarray: + The rows in the DataFrame converted to a `numpy.ndarray` with + the specified dtype. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __getitem__(self, key): + """Gets the specified column(s) from the DataFrame. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... "name" : ["alpha", "beta", "gamma"], + ... "age": [20, 30, 40], + ... "location": ["WA", "NY", "CA"] + ... }) + >>> df + name age location + 0 alpha 20 WA + 1 beta 30 NY + 2 gamma 40 CA + + [3 rows x 3 columns] + + You can specify a column label to retrieve the corresponding Series. + + >>> df["name"] + 0 alpha + 1 beta + 2 gamma + Name: name, dtype: string + + You can specify a list of column labels to retrieve a Dataframe. + + >>> df[["name", "age"]] + name age + 0 alpha 20 + 1 beta 30 + 2 gamma 40 + + [3 rows x 2 columns] + + You can specify a condition as a series of booleans to retrieve matching + rows. + + >>> df[df["age"] > 25] + name age location + 1 beta 30 NY + 2 gamma 40 CA + + [2 rows x 3 columns] + + You can specify a pandas Index with desired column labels. + + >>> import pandas as pd + >>> df[pd.Index(["age", "location"])] + age location + 0 20 WA + 1 30 NY + 2 40 CA + + [3 rows x 2 columns] + + Args: + key (index): + Index or list of indices. It can be a column label, a list of + column labels, a Series of booleans or a pandas Index of desired + column labels + + Returns: + Series or Value: Value(s) at the requested index(es). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __setitem__(self, key, value): + """Modify or insert a column into the DataFrame. + + .. note:: + This does **not** modify the original table the DataFrame was + derived from. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({ + ... "name" : ["alpha", "beta", "gamma"], + ... "age": [20, 30, 40], + ... "location": ["WA", "NY", "CA"] + ... }) + >>> df + name age location + 0 alpha 20 WA + 1 beta 30 NY + 2 gamma 40 CA + + [3 rows x 3 columns] + + You can add assign a constant to a new column. + + >>> df["country"] = "USA" + >>> df + name age location country + 0 alpha 20 WA USA + 1 beta 30 NY USA + 2 gamma 40 CA USA + + [3 rows x 4 columns] + + You can assign a Series to a new column. + + >>> df["new_age"] = df["age"] + 5 + >>> df + name age location country new_age + 0 alpha 20 WA USA 25 + 1 beta 30 NY USA 35 + 2 gamma 40 CA USA 45 + + [3 rows x 5 columns] + + You can assign a Series to an existing column. + + >>> df["new_age"] = bpd.Series([29, 39, 19], index=[1, 2, 0]) + >>> df + name age location country new_age + 0 alpha 20 WA USA 19 + 1 beta 30 NY USA 29 + 2 gamma 40 CA USA 39 + + [3 rows x 5 columns] + + Args: + key (column index): + It can be a new column to be inserted, or an existing column to + be modified. + value (scalar or Series): + Value to be assigned to the column + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index baa9534a0ec..9c6120fd6c6 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -1120,9 +1120,39 @@ def pipe( return common.pipe(self, func, *args, **kwargs) def __nonzero__(self): + """Returns the truth value of the object.""" raise ValueError( f"The truth value of a {type(self).__name__} is ambiguous. " "Use a.empty, a.bool(), a.item(), a.any() or a.all()." ) __bool__ = __nonzero__ + + def __getattr__(self, name: str): + """ + After regular attribute access, try looking up the name + This allows simpler access to columns for interactive use. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def equals(self, other) -> bool: + """ + Test whether two objects contain the same elements. + + This function allows two Series or DataFrames to be compared against + each other to see if they have the same shape and elements. NaNs in + the same location are considered equal. + + The row/column index do not need to have the same type, as long + as the values are considered equal. Corresponding columns must be of + the same dtype. + + Args: + other (Series or DataFrame): + The other Series or DataFrame to be compared with the first. + + Returns: + bool: True if all elements are the same in both objects, False + otherwise. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 192e19fa5ad..46bc9714f87 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -6,6 +6,7 @@ from typing import Hashable, IO, Literal, Mapping, Optional, Sequence, TYPE_CHECKING from bigframes_vendored.pandas.core.generic import NDFrame +import numpy import numpy as np from pandas._libs import lib from pandas._typing import Axis, FilePath, NaPosition, WriteBuffer @@ -961,13 +962,13 @@ def dot(self, other) -> Series | np.ndarray: def __matmul__(self, other): """ - Matrix multiplication using binary `@` operator in Python>=3.5. + Matrix multiplication using binary `@` operator. """ return NotImplemented def __rmatmul__(self, other): """ - Matrix multiplication using binary `@` operator in Python>=3.5. + Matrix multiplication using binary `@` operator. """ return NotImplemented @@ -2173,6 +2174,55 @@ def add(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __add__(self, other): + """Get addition of Series and other, element-wise, using operator `+`. + + Equivalent to `Series.add(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1.5, 2.6], index=['elk', 'moose']) + >>> s + elk 1.5 + moose 2.6 + dtype: Float64 + + You can add a scalar. + + >>> s + 1.5 + elk 3.0 + moose 4.1 + dtype: Float64 + + You can add another Series with index aligned. + + >>> delta = bpd.Series([1.5, 2.6], index=['elk', 'moose']) + >>> s + delta + elk 3.0 + moose 5.2 + dtype: Float64 + + Adding any mis-aligned index will result in invalid values. + + >>> delta = bpd.Series([1.5, 2.6], index=['moose', 'bison']) + >>> s + delta + elk + moose 4.1 + bison + dtype: Float64 + + Args: + other (scalar or Series): + Object to be added to the Series. + + Returns: + Series: The result of adding `other` to Series. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def radd(self, other) -> Series: """Return addition of Series and other, element-wise (binary operator radd). @@ -2188,6 +2238,20 @@ def radd(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __radd__(self, other): + """Get addition of Series and other, element-wise, using operator `+`. + + Equivalent to `Series.radd(other)`. + + Args: + other (scalar or Series): + Object to which Series should be added. + + Returns: + Series: The result of adding Series to `other`. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def sub( self, other, @@ -2206,6 +2270,55 @@ def sub( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __sub__(self, other): + """Get subtraction of other from Series, element-wise, using operator `-`. + + Equivalent to `Series.sub(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1.5, 2.6], index=['elk', 'moose']) + >>> s + elk 1.5 + moose 2.6 + dtype: Float64 + + You can subtract a scalar. + + >>> s - 1.5 + elk 0.0 + moose 1.1 + dtype: Float64 + + You can subtract another Series with index aligned. + + >>> delta = bpd.Series([0.5, 1.0], index=['elk', 'moose']) + >>> s - delta + elk 1.0 + moose 1.6 + dtype: Float64 + + Adding any mis-aligned index will result in invalid values. + + >>> delta = bpd.Series([0.5, 1.0], index=['moose', 'bison']) + >>> s - delta + elk + moose 2.1 + bison + dtype: Float64 + + Args: + other (scalar or Series): + Object to subtract from the Series. + + Returns: + Series: The result of subtraction. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rsub(self, other) -> Series: """Return subtraction of Series and other, element-wise (binary operator rsub). @@ -2221,6 +2334,20 @@ def rsub(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rsub__(self, other): + """Get subtraction of Series from other, element-wise, using operator `-`. + + Equivalent to `Series.rsub(other)`. + + Args: + other (scalar or Series): + Object to subtract the Series from. + + Returns: + Series: The result of subtraction. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def mul(self, other) -> Series: """Return multiplication of Series and other, element-wise (binary operator mul). @@ -2236,6 +2363,44 @@ def mul(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __mul__(self, other): + """ + Get multiplication of Series with other, element-wise, using operator `*`. + + Equivalent to `Series.mul(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can multiply with a scalar: + + >>> s = bpd.Series([1, 2, 3]) + >>> s * 3 + 0 3 + 1 6 + 2 9 + dtype: Int64 + + You can also multiply with another Series: + + >>> s1 = bpd.Series([2, 3, 4]) + >>> s * s1 + 0 2 + 1 6 + 2 12 + dtype: Int64 + + Args: + other (scalar or Series): + Object to multiply with the Series. + + Returns: + Series: The result of the multiplication. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rmul(self, other) -> Series: """Return multiplication of Series and other, element-wise (binary operator mul). @@ -2250,6 +2415,21 @@ def rmul(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rmul__(self, other): + """ + Get multiplication of other with Series, element-wise, using operator `*`. + + Equivalent to `Series.rmul(other)`. + + Args: + other (scalar or Series): + Object to multiply the Series with. + + Returns: + Series: The result of the multiplication. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def truediv(self, other) -> Series: """Return floating division of Series and other, element-wise (binary operator truediv). @@ -2265,6 +2445,44 @@ def truediv(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __truediv__(self, other): + """ + Get division of Series by other, element-wise, using operator `/`. + + Equivalent to `Series.truediv(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can multiply with a scalar: + + >>> s = bpd.Series([1, 2, 3]) + >>> s / 2 + 0 0.5 + 1 1.0 + 2 1.5 + dtype: Float64 + + You can also multiply with another Series: + + >>> denominator = bpd.Series([2, 3, 4]) + >>> s / denominator + 0 0.5 + 1 0.666667 + 2 0.75 + dtype: Float64 + + Args: + other (scalar or Series): + Object to divide the Series by. + + Returns: + Series: The result of the division. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rtruediv(self, other) -> Series: """Return floating division of Series and other, element-wise (binary operator rtruediv). @@ -2280,6 +2498,21 @@ def rtruediv(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rtruediv__(self, other): + """ + Get division of other by Series, element-wise, using operator `/`. + + Equivalent to `Series.rtruediv(other)`. + + Args: + other (scalar or Series): + Object to divide by the Series. + + Returns: + Series: The result of the division. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def floordiv(self, other) -> Series: """Return integer division of Series and other, element-wise (binary operator floordiv). @@ -2295,6 +2528,44 @@ def floordiv(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __floordiv__(self, other): + """ + Get integer divison of Series by other, using arithmatic operator `//`. + + Equivalent to `Series.floordiv(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can divide by a scalar: + + >>> s = bpd.Series([15, 30, 45]) + >>> s // 2 + 0 7 + 1 15 + 2 22 + dtype: Int64 + + You can also divide by another DataFrame: + + >>> divisor = bpd.Series([3, 4, 4]) + >>> s // divisor + 0 5 + 1 7 + 2 11 + dtype: Int64 + + Args: + other (scalar or Series): + Object to divide the Series by. + + Returns: + Series: The result of the integer divison. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rfloordiv(self, other) -> Series: """Return integer division of Series and other, element-wise (binary operator rfloordiv). @@ -2310,6 +2581,21 @@ def rfloordiv(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rfloordiv__(self, other): + """ + Get integer divison of other by Series, using arithmatic operator `//`. + + Equivalent to `Series.rfloordiv(other)`. + + Args: + other (scalar or Series): + Object to divide by the Series. + + Returns: + Series: The result of the integer divison. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def mod(self, other) -> Series: """Return modulo of Series and other, element-wise (binary operator mod). @@ -2325,6 +2611,44 @@ def mod(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __mod__(self, other): + """ + Get modulo of Series with other, element-wise, using operator `%`. + + Equivalent to `Series.mod(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can modulo with a scalar: + + >>> s = bpd.Series([1, 2, 3]) + >>> s % 3 + 0 1 + 1 2 + 2 0 + dtype: Int64 + + You can also modulo with another Series: + + >>> modulo = bpd.Series([3, 3, 3]) + >>> s % modulo + 0 1 + 1 2 + 2 0 + dtype: Int64 + + Args: + other (scalar or Series): + Object to modulo the Series by. + + Returns: + Series: The result of the modulo. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rmod(self, other) -> Series: """Return modulo of Series and other, element-wise (binary operator mod). @@ -2340,6 +2664,21 @@ def rmod(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rmod__(self, other): + """ + Get modulo of other with Series, element-wise, using operator `%`. + + Equivalent to `Series.rmod(other)`. + + Args: + other (scalar or Series): + Object to modulo by the Series. + + Returns: + Series: The result of the modulo. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def pow(self, other) -> Series: """Return Exponential power of series and other, element-wise (binary operator `pow`). @@ -2355,6 +2694,45 @@ def pow(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __pow__(self, other): + """ + Get exponentiation of Series with other, element-wise, using operator + `**`. + + Equivalent to `Series.pow(other)`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + You can exponentiate with a scalar: + + >>> s = bpd.Series([1, 2, 3]) + >>> s ** 2 + 0 1 + 1 4 + 2 9 + dtype: Int64 + + You can also exponentiate with another Series: + + >>> exponent = bpd.Series([3, 2, 1]) + >>> s ** exponent + 0 1 + 1 4 + 2 3 + dtype: Int64 + + Args: + other (scalar or Series): + Object to exponentiate the Series with. + + Returns: + Series: The result of the exponentiation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def rpow(self, other) -> Series: """Return Exponential power of series and other, element-wise (binary operator `rpow`). @@ -2370,6 +2748,22 @@ def rpow(self, other) -> Series: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __rpow__(self, other): + """ + Get exponentiation of other with Series, element-wise, using operator + `**`. + + Equivalent to `Series.rpow(other)`. + + Args: + other (scalar or Series): + Object to exponentiate with the Series. + + Returns: + Series: The result of the exponentiation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def divmod(self, other) -> Series: """Return integer division and modulo of Series and other, element-wise (binary operator divmod). @@ -3574,3 +3968,172 @@ def size(self) -> int: int: Return the number of elements in the underlying data. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __array__(self, dtype=None) -> numpy.ndarray: + """ + Returns the values as NumPy array. + + Equivalent to `Series.to_numpy(dtype)`. + + Users should not call this directly. Rather, it is invoked by + `numpy.array` and `numpy.asarray`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> import numpy as np + + >>> ser = bpd.Series([1, 2, 3]) + + >>> np.asarray(ser) + array([1, 2, 3]) + + Args: + dtype (str or numpy.dtype, optional): + The dtype to use for the resulting NumPy array. By default, + the dtype is inferred from the data. + + Returns: + numpy.ndarray: + The values in the series converted to a `numpy.ndarray` with the + specified dtype. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __len__(self): + """Returns number of values in the Series, serves `len` operator. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3]) + >>> len(s) + 3 + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __invert__(self): + """ + Returns the logical inversion (binary NOT) of the Series, element-wise + using operator `~`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series([True, False, True]) + >>> ~ser + 0 False + 1 True + 2 False + dtype: boolean + + Returns: + Series: The inverted values in the series. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __and__(self, other): + """Get bitwise AND of Series and other, element-wise, using operator `&`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([0, 1, 2, 3]) + + You can operate with a scalar. + + >>> s & 6 + 0 0 + 1 0 + 2 2 + 3 2 + dtype: Int64 + + You can operate with another Series. + + >>> s1 = bpd.Series([5, 6, 7, 8]) + >>> s & s1 + 0 0 + 1 0 + 2 2 + 3 0 + dtype: Int64 + + Args: + other (scalar or Series): + Object to bitwise AND with the Series. + + Returns: + Series: The result of the operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __or__(self, other): + """Get bitwise OR of Series and other, element-wise, using operator `|`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([0, 1, 2, 3]) + + You can operate with a scalar. + + >>> s | 6 + 0 6 + 1 7 + 2 6 + 3 7 + dtype: Int64 + + You can operate with another Series. + + >>> s1 = bpd.Series([5, 6, 7, 8]) + >>> s | s1 + 0 5 + 1 7 + 2 7 + 3 11 + dtype: Int64 + + Args: + other (scalar or Series): + Object to bitwise OR with the Series. + + Returns: + Series: The result of the operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __getitem__(self, indexer): + """Gets the specified index from the Series. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([15, 30, 45]) + >>> s[1] + 30 + >>> s[0:2] + 0 15 + 1 30 + dtype: Int64 + + Args: + indexer (int or slice): + Index or slice of indices. + + Returns: + Series or Value: Value(s) at the requested index(es). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 290f95dc5198f9ab7cd9d726d40af704250c0449 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Fri, 12 Apr 2024 16:58:17 -0700 Subject: [PATCH 497/921] feat: support list of numerics in pandas.cut (#580) An internal user encountered this missing overload --- bigframes/core/reshape/__init__.py | 26 ++++++++-- bigframes/operations/aggregations.py | 6 +-- tests/system/small/test_pandas.py | 52 +++++++++++++++++++ .../pandas/core/reshape/tile.py | 16 +++++- 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index e3ed8edd21c..6bcc25319b8 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -14,7 +14,7 @@ from __future__ import annotations import typing -from typing import Iterable, Literal, Optional, Tuple, Union +from typing import Iterable, Literal, Optional, Union import pandas as pd @@ -113,7 +113,7 @@ def cut( bins: Union[ int, pd.IntervalIndex, - Iterable[Tuple[Union[int, float], Union[int, float]]], + Iterable, ], *, labels: Optional[bool] = None, @@ -125,9 +125,29 @@ def cut( if isinstance(bins, pd.IntervalIndex): as_index: pd.IntervalIndex = bins bins = tuple((bin.left.item(), bin.right.item()) for bin in bins) - else: + elif len(list(bins)) == 0: + raise ValueError("`bins` iterable should have at least one item") + elif isinstance(list(bins)[0], tuple): as_index = pd.IntervalIndex.from_tuples(list(bins)) bins = tuple(bins) + elif pd.api.types.is_number(list(bins)[0]): + bins_list = list(bins) + if len(bins_list) < 2: + raise ValueError( + "`bins` iterable of numeric breaks should have" + " at least two items" + ) + as_index = pd.IntervalIndex.from_breaks(bins_list) + single_type = all([isinstance(n, type(bins_list[0])) for n in bins_list]) + numeric_type = type(bins_list[0]) if single_type else float + bins = tuple( + [ + (numeric_type(bins_list[i]), numeric_type(bins_list[i + 1])) + for i in range(len(bins_list) - 1) + ] + ) + else: + raise ValueError("`bins` iterable should contain tuples or numerics") if as_index.is_overlapping: raise ValueError("Overlapping IntervalIndex is not accepted.") diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 36fa7876445..f33dc16e304 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -17,7 +17,7 @@ import abc import dataclasses import typing -from typing import ClassVar, Hashable, Optional, Tuple +from typing import ClassVar, Iterable, Optional import pandas as pd import pyarrow as pa @@ -213,7 +213,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class CutOp(UnaryWindowOp): # TODO: Unintuitive, refactor into multiple ops? - bins: typing.Union[int, Tuple[Tuple[Hashable, Hashable], ...]] + bins: typing.Union[int, Iterable] labels: Optional[bool] @property @@ -232,7 +232,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT interval_dtype = ( pa.float64() if isinstance(self.bins, int) - else dtypes.infer_literal_arrow_type(self.bins[0][0]) + else dtypes.infer_literal_arrow_type(list(self.bins)[0][0]) ) pa_type = pa.struct( [ diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index 95b34a56c5b..d543f926553 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -424,6 +424,58 @@ def test_cut_default_labels(scalars_dfs): ) +@pytest.mark.parametrize( + ("breaks",), + [ + ([0, 5, 10, 15, 20, 100, 1000],), # ints + ([0.5, 10.5, 15.5, 20.5, 100.5, 1000.5],), # floats + ([0, 5, 10.5, 15.5, 20, 100, 1000.5],), # mixed + ], +) +def test_cut_numeric_breaks(scalars_dfs, breaks): + scalars_df, scalars_pandas_df = scalars_dfs + + pd_result = pd.cut(scalars_pandas_df["float64_col"], breaks) + bf_result = bpd.cut(scalars_df["float64_col"], breaks).to_pandas() + + # Convert to match data format + pd_result_converted = pd.Series( + [ + {"left_exclusive": interval.left, "right_inclusive": interval.right} + if pd.notna(val) + else pd.NA + for val, interval in zip( + pd_result, pd_result.cat.categories[pd_result.cat.codes] + ) + ], + name=pd_result.name, + ) + + pd.testing.assert_series_equal( + bf_result, pd_result_converted, check_index=False, check_dtype=False + ) + + +@pytest.mark.parametrize( + ("bins",), + [ + (-1,), # negative integer bins argument + ([],), # empty iterable of bins + (["notabreak"],), # iterable of wrong type + ([1],), # numeric breaks with only one numeric + # this is supported by pandas but not by + # the bigquery operation and a bigframes workaround + # is not yet available. Should return column + # of structs with all NaN values. + ], +) +def test_cut_errors(scalars_dfs, bins): + scalars_df, _ = scalars_dfs + + with pytest.raises(ValueError): + bpd.cut(scalars_df["float64_col"], bins) + + @pytest.mark.parametrize( ("bins",), [ diff --git a/third_party/bigframes_vendored/pandas/core/reshape/tile.py b/third_party/bigframes_vendored/pandas/core/reshape/tile.py index fbd1d2d0527..6ba3950a761 100644 --- a/third_party/bigframes_vendored/pandas/core/reshape/tile.py +++ b/third_party/bigframes_vendored/pandas/core/reshape/tile.py @@ -76,10 +76,20 @@ def cut( 3 {'left_exclusive': 5, 'right_inclusive': 20} dtype: struct[pyarrow] + Cut with an iterable of ints: + + >>> bins_ints = [0, 1, 5, 20] + >>> bpd.cut(s, bins=bins_ints) + 0 + 1 {'left_exclusive': 0, 'right_inclusive': 1} + 2 {'left_exclusive': 1, 'right_inclusive': 5} + 3 {'left_exclusive': 5, 'right_inclusive': 20} + dtype: struct[pyarrow] + Args: x (Series): The input Series to be binned. Must be 1-dimensional. - bins (int, pd.IntervalIndex, Iterable[Tuple[Union[int, float], Union[int, float]]]): + bins (int, pd.IntervalIndex, Iterable): The criteria to bin by. int: Defines the number of equal-width bins in the range of `x`. The @@ -88,6 +98,10 @@ def cut( pd.IntervalIndex or Iterable of tuples: Defines the exact bins to be used. It's important to ensure that these bins are non-overlapping. + + Iterable of numerics: Defines the exact bins by using the interval + between each item and its following item. The items must be monotonically + increasing. labels (None): Specifies the labels for the returned bins. Must be the same length as the resulting bins. If False, returns only integer indicators of the From 0e24036fe33c60cef08ba78e7ce69319338b6b03 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:53:53 -0700 Subject: [PATCH 498/921] test: series explode test (#606) --- tests/system/small/test_series.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 6e4a87df4fb..d27cd0a236a 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -3463,9 +3463,8 @@ def foo(x: int, y: int, df): ], ) def test_series_explode(data): - data = [[1, 2, 3], [], numpy.nan, [3, 4]] s = bigframes.pandas.Series(data) - pd_s = pd.Series(data) + pd_s = s.to_pandas() pd.testing.assert_series_equal( s.explode().to_pandas(), pd_s.explode(), From 458bfb2b42c6fc0406489f19210a7d5a406ee90d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:08:11 -0700 Subject: [PATCH 499/921] chore(main): release 1.2.0 (#588) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcb062f08f1..a3314c976e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.2.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.1.0...v1.2.0) (2024-04-15) + + +### Features + +* Add hasnans, combine_first, update to Series ([#600](https://github.com/googleapis/python-bigquery-dataframes/issues/600)) ([86e0f38](https://github.com/googleapis/python-bigquery-dataframes/commit/86e0f38adc71d76e09dd832e5e33cb7c1aab02ac)) +* Add MultiIndex subclass. ([#596](https://github.com/googleapis/python-bigquery-dataframes/issues/596)) ([5d0f149](https://github.com/googleapis/python-bigquery-dataframes/commit/5d0f149dce5425098fcd154d96a302c1661ce5d3)) +* Add pivot_table for DataFrame. ([#473](https://github.com/googleapis/python-bigquery-dataframes/issues/473)) ([5f1d670](https://github.com/googleapis/python-bigquery-dataframes/commit/5f1d670e6b839a30acdb495a05011c2ce4e0c7a4)) +* Add Series.autocorr ([#605](https://github.com/googleapis/python-bigquery-dataframes/issues/605)) ([4ec8034](https://github.com/googleapis/python-bigquery-dataframes/commit/4ec80340459e675b82b437f6c48b2872d362bafe)) +* Support list of numerics in pandas.cut ([#580](https://github.com/googleapis/python-bigquery-dataframes/issues/580)) ([290f95d](https://github.com/googleapis/python-bigquery-dataframes/commit/290f95dc5198f9ab7cd9d726d40af704250c0449)) + + +### Bug Fixes + +* Address more technical writers feedback ([#581](https://github.com/googleapis/python-bigquery-dataframes/issues/581)) ([4b08d92](https://github.com/googleapis/python-bigquery-dataframes/commit/4b08d9243272229f71688152dbeb69d0ab7c68b4)) +* Error for object dtype on read_pandas ([#570](https://github.com/googleapis/python-bigquery-dataframes/issues/570)) ([8702dcf](https://github.com/googleapis/python-bigquery-dataframes/commit/8702dcf54c0f2073e21df42eaef51927481da421)) +* Inverting int now does bitwise inversion rather than sign flip ([#574](https://github.com/googleapis/python-bigquery-dataframes/issues/574)) ([5f1db8b](https://github.com/googleapis/python-bigquery-dataframes/commit/5f1db8b270b32ab366be3690761da137d9fe65f5)) +* Loc setitem dtype issue. ([#603](https://github.com/googleapis/python-bigquery-dataframes/issues/603)) ([b94bae9](https://github.com/googleapis/python-bigquery-dataframes/commit/b94bae9892e0fa79dc4bde0f4f1427d00accda6d)) +* Toc menu missing plotting name ([#591](https://github.com/googleapis/python-bigquery-dataframes/issues/591)) ([eed12c1](https://github.com/googleapis/python-bigquery-dataframes/commit/eed12c181ff8724333b1c426a0eb442c627528b8)) + + +### Documentation + +* (Series|Dataframe).dtypes ([#598](https://github.com/googleapis/python-bigquery-dataframes/issues/598)) ([edef48f](https://github.com/googleapis/python-bigquery-dataframes/commit/edef48f7a93e19bc1f6d37fb041dfd6314d881d5)) +* Add code samples for `str` accessor methdos ([#594](https://github.com/googleapis/python-bigquery-dataframes/issues/594)) ([a557ea2](https://github.com/googleapis/python-bigquery-dataframes/commit/a557ea2b64633932f730b56688f76806da6195fb)) +* Add docs for `DataFrame` and `Series` dunder methods ([#562](https://github.com/googleapis/python-bigquery-dataframes/issues/562)) ([8fc26c4](https://github.com/googleapis/python-bigquery-dataframes/commit/8fc26c424b29a8b78542372e402fcc4e8fface7b)) +* Add examples for at/iat ([#582](https://github.com/googleapis/python-bigquery-dataframes/issues/582)) ([3be4a2e](https://github.com/googleapis/python-bigquery-dataframes/commit/3be4a2e784e046ca9a1fac8d386d072537b6c4de)) + ## [1.1.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.0.0...v1.1.0) (2024-04-04) diff --git a/bigframes/version.py b/bigframes/version.py index 41a38955498..ec2105b6485 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.1.0" +__version__ = "1.2.0" From bc82804da43c03c2311cd56f47a2316d3aae93d2 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 16 Apr 2024 16:32:17 -0700 Subject: [PATCH 500/921] feat: Add quantile statistic (#613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/constants.py | 3 + bigframes/core/block_transforms.py | 34 +++++++++++ bigframes/core/blocks.py | 13 +++-- bigframes/core/compile/aggregate_compiler.py | 8 +++ bigframes/core/groupby/__init__.py | 57 +++++++++++++++++-- bigframes/dataframe.py | 30 +++++++++- bigframes/operations/aggregations.py | 12 ++++ bigframes/series.py | 19 +++++-- tests/system/small/test_dataframe.py | 30 +++++++++- tests/system/small/test_groupby.py | 35 ++++++++++++ tests/system/small/test_series.py | 21 +++++++ .../ibis/backends/bigquery/registry.py | 8 +++ .../bigframes_vendored/pandas/core/frame.py | 40 ++++++++++++- .../pandas/core/groupby/__init__.py | 30 ++++++++++ .../bigframes_vendored/pandas/core/series.py | 42 +++++++++++++- .../pandas/plotting/_core.py | 4 ++ 16 files changed, 366 insertions(+), 20 deletions(-) diff --git a/bigframes/constants.py b/bigframes/constants.py index 07515010858..c6d8f3acc2e 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -92,3 +92,6 @@ LEP_ENABLED_BIGQUERY_LOCATIONS = frozenset( ALL_BIGQUERY_LOCATIONS - REP_ENABLED_BIGQUERY_LOCATIONS ) + +# BigQuery default is 10000, leave 100 for overhead +MAX_COLUMNS = 9900 diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index c789b2a69cf..1eae73014c7 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -15,6 +15,7 @@ import functools import typing +from typing import Sequence import pandas as pd @@ -105,6 +106,39 @@ def indicate_duplicates( ) +def quantile( + block: blocks.Block, + columns: Sequence[str], + qs: Sequence[float], + grouping_column_ids: Sequence[str] = (), +) -> blocks.Block: + # TODO: handle windowing and more interpolation methods + window = core.WindowSpec( + grouping_keys=tuple(grouping_column_ids), + ) + quantile_cols = [] + labels = [] + if len(columns) * len(qs) > constants.MAX_COLUMNS: + raise NotImplementedError("Too many aggregates requested.") + for col in columns: + for q in qs: + label = block.col_id_to_label[col] + new_label = (*label, q) if isinstance(label, tuple) else (label, q) + labels.append(new_label) + block, quantile_col = block.apply_window_op( + col, + agg_ops.QuantileOp(q), + window_spec=window, + ) + quantile_cols.append(quantile_col) + block, results = block.aggregate( + grouping_column_ids, + tuple((col, agg_ops.AnyValueOp()) for col in quantile_cols), + dropna=True, + ) + return block.select_columns(results).with_column_labels(labels) + + def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: supported_methods = [ "linear", diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 5b411e5416d..f6850020df9 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1498,12 +1498,17 @@ def stack(self, how="left", levels: int = 1): row_label_tuples = utils.index_as_tuples(row_labels) - if col_labels is not None: + if col_labels is None: + result_index: pd.Index = pd.Index([None]) + result_col_labels: Sequence[Tuple] = list([()]) + elif (col_labels.nlevels == 1) and all( + col_labels.isna() + ): # isna not implemented for MultiIndex for newer pandas versions + result_index = pd.Index([None]) + result_col_labels = utils.index_as_tuples(col_labels.drop_duplicates()) + else: result_index = col_labels.drop_duplicates().dropna(how="all") result_col_labels = utils.index_as_tuples(result_index) - else: - result_index = pd.Index([None]) - result_col_labels = list([()]) # Get matching columns unpivot_columns: List[Tuple[str, List[str]]] = [] diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index ae212435069..98d296c779f 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -148,6 +148,14 @@ def _( return cast(ibis_types.NumericValue, value) +@compile_unary_agg.register +@numeric_op +def _( + op: agg_ops.QuantileOp, column: ibis_types.NumericColumn, window=None +) -> ibis_types.NumericValue: + return _apply_window_if_present(column.quantile(op.q), window) + + @compile_unary_agg.register @numeric_op def _( diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index e2b28553c6b..0f533423528 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -15,6 +15,7 @@ from __future__ import annotations import typing +from typing import Sequence, Union import bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby import pandas as pd @@ -115,14 +116,35 @@ def mean(self, numeric_only: bool = False, *args) -> df.DataFrame: def median( self, numeric_only: bool = False, *, exact: bool = False ) -> df.DataFrame: - if exact: - raise NotImplementedError( - f"Only approximate median is supported. {constants.FEEDBACK_LINK}" - ) if not numeric_only: self._raise_on_non_numeric("median") + if exact: + return self.quantile(0.5) return self._aggregate_all(agg_ops.median_op, numeric_only=True) + def quantile( + self, q: Union[float, Sequence[float]] = 0.5, *, numeric_only: bool = False + ) -> df.DataFrame: + if not numeric_only: + self._raise_on_non_numeric("quantile") + q_cols = tuple( + col + for col in self._selected_cols + if self._column_type(col) in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE + ) + multi_q = utils.is_list_like(q) + result = block_ops.quantile( + self._block, + q_cols, + qs=tuple(q) if multi_q else (q,), # type: ignore + grouping_column_ids=self._by_col_ids, + ) + result_df = df.DataFrame(result) + if multi_q: + return result_df.stack() + else: + return result_df.droplevel(-1, 1) + def min(self, numeric_only: bool = False, *args) -> df.DataFrame: return self._aggregate_all(agg_ops.min_op, numeric_only=numeric_only) @@ -466,8 +488,31 @@ def sum(self, *args) -> series.Series: def mean(self, *args) -> series.Series: return self._aggregate(agg_ops.mean_op) - def median(self, *args, **kwargs) -> series.Series: - return self._aggregate(agg_ops.mean_op) + def median( + self, + *args, + exact: bool = False, + **kwargs, + ) -> series.Series: + if exact: + return self.quantile(0.5) + else: + return self._aggregate(agg_ops.median_op) + + def quantile( + self, q: Union[float, Sequence[float]] = 0.5, *, numeric_only: bool = False + ) -> series.Series: + multi_q = utils.is_list_like(q) + result = block_ops.quantile( + self._block, + (self._value_column,), + qs=tuple(q) if multi_q else (q,), # type: ignore + grouping_column_ids=self._by_col_ids, + ) + if multi_q: + return series.Series(result.stack()) + else: + return series.Series(result.stack()).droplevel(-1) def std(self, *args, **kwargs) -> series.Series: return self._aggregate(agg_ops.std_op) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 2deef952770..953a89c34fd 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2009,8 +2009,34 @@ def median( frame = self._raise_on_non_numeric("median") else: frame = self._drop_non_numeric() - block = frame._block.aggregate_all_and_stack(agg_ops.median_op) - return bigframes.series.Series(block.select_column("values")) + if exact: + return self.quantile() + else: + block = frame._block.aggregate_all_and_stack(agg_ops.median_op) + return bigframes.series.Series(block.select_column("values")) + + def quantile( + self, q: Union[float, Sequence[float]] = 0.5, *, numeric_only: bool = False + ): + if not numeric_only: + frame = self._raise_on_non_numeric("median") + else: + frame = self._drop_non_numeric() + multi_q = utils.is_list_like(q) + result = block_ops.quantile( + frame._block, frame._block.value_columns, qs=tuple(q) if multi_q else (q,) # type: ignore + ) + if multi_q: + return DataFrame(result.stack()).droplevel(0) + else: + result_df = ( + DataFrame(result) + .stack(list(range(0, frame.columns.nlevels))) + .droplevel(0) + ) + result_series = bigframes.series.Series(result_df._block) + result_series.name = q + return result_series def std( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index f33dc16e304..0d27d1d75d3 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -109,6 +109,18 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT return input_types[0] +@dataclasses.dataclass(frozen=True) +class QuantileOp(UnaryAggregateOp): + q: float + + @property + def name(self): + return f"{int(self.q*100)}%" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return signatures.UNARY_REAL_NUMERIC.output_type(input_types[0]) + + @dataclasses.dataclass(frozen=True) class ApproxQuartilesOp(UnaryAggregateOp): quartile: int diff --git a/bigframes/series.py b/bigframes/series.py index 2f9123f9a3b..b834411bce5 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -23,7 +23,7 @@ import os import textwrap import typing -from typing import Any, Literal, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, cast, Literal, Mapping, Optional, Sequence, Tuple, Union import bigframes_vendored.pandas.core.series as vendored_pandas_series import google.cloud.bigquery as bigquery @@ -968,10 +968,19 @@ def mean(self) -> float: def median(self, *, exact: bool = False) -> float: if exact: - raise NotImplementedError( - f"Only approximate median is supported. {constants.FEEDBACK_LINK}" - ) - return typing.cast(float, self._apply_aggregation(agg_ops.median_op)) + return typing.cast(float, self.quantile(0.5)) + else: + return typing.cast(float, self._apply_aggregation(agg_ops.median_op)) + + def quantile(self, q: Union[float, Sequence[float]] = 0.5) -> Union[Series, float]: + qs = tuple(q) if utils.is_list_like(q) else (q,) + result = block_ops.quantile(self._block, (self._value_column,), qs=qs) + if utils.is_list_like(q): + result = result.stack() + result = result.drop_levels([result.index_columns[0]]) + return Series(result) + else: + return cast(float, Series(result).to_pandas().squeeze()) def sum(self) -> float: return typing.cast(float, self._apply_aggregation(agg_ops.sum_op)) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e70764fcc0a..7fef7a9dc73 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2504,7 +2504,10 @@ def test_df_melt_default(scalars_dfs): # Pandas produces int64 index, Bigframes produces Int64 (nullable) pd.testing.assert_frame_equal( - bf_result, pd_result, check_index_type=False, check_dtype=False + bf_result, + pd_result, + check_index_type=False, + check_dtype=False, ) @@ -3029,6 +3032,31 @@ def test_dataframe_aggregates_median(scalars_df_index, scalars_pandas_df_index): ) +def test_dataframe_aggregates_quantile_mono(scalars_df_index, scalars_pandas_df_index): + q = 0.45 + col_names = ["int64_too", "int64_col", "float64_col"] + bf_result = scalars_df_index[col_names].quantile(q=q).to_pandas() + pd_result = scalars_pandas_df_index[col_names].quantile(q=q) + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + + pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + + +def test_dataframe_aggregates_quantile_multi(scalars_df_index, scalars_pandas_df_index): + q = [0, 0.33, 0.67, 1.0] + col_names = ["int64_too", "int64_col", "float64_col"] + bf_result = scalars_df_index[col_names].quantile(q=q).to_pandas() + pd_result = scalars_pandas_df_index[col_names].quantile(q=q) + + # Pandas may produce narrower numeric types, but bigframes always produces Float64 + pd_result = pd_result.astype("Float64") + pd_result.index = pd_result.index.astype("Float64") + + pd.testing.assert_frame_equal(bf_result, pd_result) + + @pytest.mark.parametrize( ("op"), [ diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index ba79ba1ab1a..7b36a06f492 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -65,6 +65,24 @@ def test_dataframe_groupby_median(scalars_df_index, scalars_pandas_df_index): assert ((pd_min <= bf_result_computed) & (bf_result_computed <= pd_max)).all().all() +@pytest.mark.parametrize( + ("q"), + [ + ([0.2, 0.4, 0.6, 0.8]), + (0.11), + ], +) +def test_dataframe_groupby_quantile(scalars_df_index, scalars_pandas_df_index, q): + col_names = ["int64_too", "float64_col", "int64_col", "string_col"] + bf_result = ( + scalars_df_index[col_names].groupby("string_col").quantile(q) + ).to_pandas() + pd_result = scalars_pandas_df_index[col_names].groupby("string_col").quantile(q) + pd.testing.assert_frame_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + @pytest.mark.parametrize( ("operator"), [ @@ -389,3 +407,20 @@ def test_dataframe_groupby_nonnumeric_with_mean(): pd.testing.assert_frame_equal( pd_result, bf_result, check_index_type=False, check_dtype=False ) + + +@pytest.mark.parametrize( + ("q"), + [ + ([0.2, 0.4, 0.6, 0.8]), + (0.11), + ], +) +def test_series_groupby_quantile(scalars_df_index, scalars_pandas_df_index, q): + bf_result = ( + scalars_df_index.groupby("string_col")["int64_col"].quantile(q) + ).to_pandas() + pd_result = scalars_pandas_df_index.groupby("string_col")["int64_col"].quantile(q) + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index d27cd0a236a..87267696bab 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1320,6 +1320,27 @@ def test_median(scalars_dfs): assert pd_min < bf_result < pd_max +def test_median_exact(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_result = scalars_df[col_name].median(exact=True) + pd_result = scalars_pandas_df[col_name].median() + assert math.isclose(pd_result, bf_result) + + +def test_series_quantile(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_col" + bf_series = scalars_df[col_name] + pd_series = scalars_pandas_df[col_name] + + pd_result = pd_series.quantile([0.0, 0.4, 0.6, 1.0]) + bf_result = bf_series.quantile([0.0, 0.4, 0.6, 1.0]) + pd.testing.assert_series_equal( + pd_result, bf_result.to_pandas(), check_dtype=False, check_index_type=False + ) + + def test_numeric_literal(scalars_dfs): scalars_df, _ = scalars_dfs col_name = "numeric_col" diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py index 88826b31cee..fddeab19a2a 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -3,6 +3,7 @@ import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops from ibis.backends.bigquery.registry import OPERATION_REGISTRY +import ibis.expr.operations.reductions as ibis_reductions def _approx_quantiles(translator, op: vendored_ibis_ops.ApproximateMultiQuantile): @@ -31,12 +32,19 @@ def _generate_array(translator, op: vendored_ibis_ops.GenerateArray): return f"GENERATE_ARRAY(0, {arg})" +def _quantile(translator, op: ibis_reductions.Quantile): + arg = translator.translate(op.arg) + quantile = translator.translate(op.quantile) + return f"PERCENTILE_CONT({arg}, {quantile})" + + patched_ops = { vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, # type:ignore vendored_ibis_ops.FirstNonNullValue: _first_non_null_value, # type:ignore vendored_ibis_ops.LastNonNullValue: _last_non_null_value, # type:ignore vendored_ibis_ops.ToJsonString: _to_json_string, # type:ignore vendored_ibis_ops.GenerateArray: _generate_array, # type:ignore + ibis_reductions.Quantile: _quantile, # type:ignore } OPERATION_REGISTRY.update(patched_ops) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 6707dc14036..e8949006466 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4509,13 +4509,51 @@ def median(self, *, numeric_only: bool = False, exact: bool = False): Default False. Include only float, int, boolean columns. exact (bool. default False): Default False. Get the exact median instead of an approximate - one. Note: ``exact=True`` not yet supported. + one. Returns: bigframes.series.Series: Series with the median of values. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def quantile( + self, q: Union[float, Sequence[float]] = 0.5, *, numeric_only: bool = False + ): + """ + Return values at the given quantile over requested axis. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame(np.array([[1, 1], [2, 10], [3, 100], [4, 100]]), + ... columns=['a', 'b']) + >>> df.quantile(.1) + a 1.3 + b 3.7 + Name: 0.1, dtype: Float64 + >>> df.quantile([.1, .5]) + a b + 0.1 1.3 3.7 + 0.5 2.5 55.0 + + [2 rows x 2 columns] + + Args: + q (float or array-like, default 0.5 (50% quantile)): + Value between 0 <= q <= 1, the quantile(s) to compute. + numeric_only (bool, default False): + Include only `float`, `int` or `boolean` data. + + Returns: + Series or DataFrame: + If ``q`` is an array, a DataFrame will be returned where the + index is ``q``, the columns are the columns of self, and the + values are the quantiles. + If ``q`` is a float, a Series will be returned where the + index is the columns of self and the values are the quantiles. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def var(self, axis=0, *, numeric_only: bool = False): """Return unbiased variance over requested axis. diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index ed4ca66f385..6310d7e271b 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -85,6 +85,36 @@ def median( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def quantile(self, q=0.5, *, numeric_only: bool = False): + """ + Return group values at the given quantile, a la numpy.percentile. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame([ + ... ['a', 1], ['a', 2], ['a', 3], + ... ['b', 1], ['b', 3], ['b', 5] + ... ], columns=['key', 'val']) + >>> df.groupby('key').quantile() + val + key + a 2.0 + b 3.0 + + [2 rows x 1 columns] + + Args: + q (float or array-like, default 0.5 (50% quantile)): + Value(s) between 0 and 1 providing the quantile(s) to compute. + numeric_only (bool, default False): + Include only `float`, `int` or `boolean` data. + + Returns: + Series or DataFrame: Return type determined by caller of GroupBy object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def std( self, *, diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 46bc9714f87..5e3b4c46ef4 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3,7 +3,16 @@ """ from __future__ import annotations -from typing import Hashable, IO, Literal, Mapping, Optional, Sequence, TYPE_CHECKING +from typing import ( + Hashable, + IO, + Literal, + Mapping, + Optional, + Sequence, + TYPE_CHECKING, + Union, +) from bigframes_vendored.pandas.core.generic import NDFrame import numpy @@ -3151,6 +3160,37 @@ def median(self, *, exact: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def quantile( + self, + q: Union[float, Sequence[float]] = 0.5, + ) -> Union[Series, float]: + """ + Return value at the given quantile. + + **Examples:** + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series([1, 2, 3, 4]) + >>> s.quantile(.5) + 2.5 + >>> s.quantile([.25, .5, .75]) + 0.25 1.75 + 0.5 2.5 + 0.75 3.25 + dtype: Float64 + + Args: + q (float or array-like, default 0.5 (50% quantile)): + The quantile(s) to compute, which can lie in range: 0 <= q <= 1. + + Returns: + float or Series: + If ``q`` is an array, a Series will be returned where the + index is ``q`` and the values are the quantiles, otherwise + a float will be returned. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def prod(self): """Return the product of the values over the requested axis. diff --git a/third_party/bigframes_vendored/pandas/plotting/_core.py b/third_party/bigframes_vendored/pandas/plotting/_core.py index 19f56965df8..bf016357a6f 100644 --- a/third_party/bigframes_vendored/pandas/plotting/_core.py +++ b/third_party/bigframes_vendored/pandas/plotting/_core.py @@ -11,6 +11,7 @@ class PlotAccessor: For Series: >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None >>> ser = bpd.Series([1, 2, 3, 3]) >>> plot = ser.plot(kind='hist', title="My plot") @@ -57,6 +58,7 @@ def hist( >>> import bigframes.pandas as bpd >>> import numpy as np + >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame(np.random.randint(1, 7, 6000), columns=['one']) >>> df['two'] = np.random.randint(1, 7, 6000) + np.random.randint(1, 7, 6000) >>> ax = df.plot.hist(bins=12, alpha=0.5) @@ -93,6 +95,7 @@ def line( **Examples:** >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame( ... { ... 'one': [1, 2, 3, 4], @@ -160,6 +163,7 @@ def area( Draw an area plot based on basic business metrics: >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame( ... { ... 'sales': [3, 2, 3, 9, 10, 6], From 250548c248fe3a4fdfa92494aa0d550de8608612 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 16 Apr 2024 17:42:09 -0700 Subject: [PATCH 501/921] test: add a bigquery usage report to notebook test session (#604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add a bigquery usage report to notebook test session * filter out mocks * remove pointless type hint * fix replace statement * account for dry runs * ipynb only * use env var via nox * don't import bigframes from noxfile * address comments * 🦉 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 --- bigframes/session/_io/bigquery.py | 31 +++++++++++++++++ noxfile.py | 58 ++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index 75283a060aa..ac6ba4bae41 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -18,6 +18,7 @@ import datetime import itertools +import os import textwrap import types from typing import Dict, Iterable, Optional, Sequence, Tuple, Union @@ -34,6 +35,8 @@ MAX_LABELS_COUNT = 64 TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}" +LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" + def create_job_configs_labels( job_configs_labels: Optional[Dict[str, str]], @@ -243,4 +246,32 @@ def start_query_with_client( ) else: results_iterator = query_job.result(max_results=max_results) + + if LOGGING_NAME_ENV_VAR in os.environ: + # when running notebooks via pytest nbmake + pytest_log_job(query_job) + return results_iterator, query_job + + +def pytest_log_job(query_job: bigquery.QueryJob): + """For pytest runs only, log information about the query job + to a file in order to create a performance report. + """ + if LOGGING_NAME_ENV_VAR not in os.environ: + raise EnvironmentError( + "Environment variable {env_var} is not set".format( + env_var=LOGGING_NAME_ENV_VAR + ) + ) + test_name = os.environ[LOGGING_NAME_ENV_VAR] + current_directory = os.getcwd() + bytes_processed = query_job.total_bytes_processed + if not isinstance(bytes_processed, int): + return # filter out mocks + if query_job.configuration.dry_run: + # dry runs don't process their total_bytes_processed + bytes_processed = 0 + bytes_file = os.path.join(current_directory, test_name + ".bytesprocessed") + with open(bytes_file, "a") as f: + f.write(str(bytes_processed) + "\n") diff --git a/noxfile.py b/noxfile.py index fa9c0a57d89..9479a7a318c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -764,6 +764,8 @@ def notebook(session: nox.Session): "--nbmake-timeout=900", # 15 minutes ] + logging_name_env_var = "BIGFRAMES_PERFORMANCE_LOG_NAME" + try: # Populate notebook parameters and make a backup so that the notebooks # are runnable. @@ -773,13 +775,21 @@ def notebook(session: nox.Session): *notebooks, ) - # Run self-contained notebooks in single session.run - # achieve parallelization via -n - session.run( - *pytest_command, - "-nauto", - *notebooks, - ) + # Run notebooks in parallel session.run's, since each notebook + # takes an environment variable for performance logging + processes = [] + for notebook in notebooks: + session.env[logging_name_env_var] = os.path.basename(notebook) + process = Process( + target=session.run, + args=(*pytest_command, notebook), + ) + process.start() + processes.append(process) + + for process in processes: + process.join() + finally: # Prevent our notebook changes from getting checked in to git # accidentally. @@ -789,11 +799,12 @@ def notebook(session: nox.Session): *notebooks, ) - # Run regionalized notebooks in parallel session.run's, since each notebook - # takes a different region via env param. + # Additionally run regionalized notebooks in parallel session.run's. + # Each notebook takes a different region via env param. processes = [] for notebook, regions in notebooks_reg.items(): for region in regions: + session.env[logging_name_env_var] = os.path.basename(notebook) process = Process( target=session.run, args=(*pytest_command, notebook), @@ -805,6 +816,35 @@ def notebook(session: nox.Session): for process in processes: process.join() + # when run via pytest, notebooks output a .bytesprocessed report + # collect those reports and print a summary + _print_bytes_processed_report() + + +def _print_bytes_processed_report(): + """Add an informational report about http queries and bytes + processed to the testlog output for purposes of measuring + bigquery-related performance changes. + """ + print("---BIGQUERY USAGE REPORT---") + cumulative_queries = 0 + cumulative_bytes = 0 + for report in Path("notebooks/").glob("*/*.bytesprocessed"): + with open(report, "r") as f: + filename = report.stem + lines = f.read().splitlines() + query_count = len(lines) + total_bytes = sum([int(line) for line in lines]) + format_string = f"{filename} - query count: {query_count}, bytes processed sum: {total_bytes}" + print(format_string) + cumulative_bytes += total_bytes + cumulative_queries += query_count + print( + "---total queries: {total_queries}, total bytes: {total_bytes}---".format( + total_queries=cumulative_queries, total_bytes=cumulative_bytes + ) + ) + @nox.session(python="3.10") def release_dry_run(session): From 34f9f61eee6878c74f50197f657682e37474becc Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:50:46 -0400 Subject: [PATCH 502/921] chore(python): bump idna from 3.4 to 3.7 in .kokoro (#608) Source-Link: https://github.com/googleapis/synthtool/commit/d50980e704793a2d3310bfb3664f3a82f24b5796 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:5a4c19d17e597b92d786e569be101e636c9c2817731f80a5adec56b2aa8fe070 Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 ++-- .github/auto-label.yaml | 5 +++++ .github/blunderbuss.yml | 17 +++++++++++++++++ .kokoro/requirements.txt | 6 +++--- 4 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 .github/blunderbuss.yml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 4bdeef3904e..81f87c56917 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:a8a80fc6456e433df53fc2a0d72ca0345db0ddefb409f1b75b118dfd1babd952 -# created: 2024-03-15T16:25:47.905264637Z + digest: sha256:5a4c19d17e597b92d786e569be101e636c9c2817731f80a5adec56b2aa8fe070 +# created: 2024-04-12T11:35:58.922854369Z diff --git a/.github/auto-label.yaml b/.github/auto-label.yaml index b2016d119b4..8b37ee89711 100644 --- a/.github/auto-label.yaml +++ b/.github/auto-label.yaml @@ -13,3 +13,8 @@ # limitations under the License. requestsize: enabled: true + +path: + pullrequest: true + paths: + samples: "samples" diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml new file mode 100644 index 00000000000..8d9cb1008e7 --- /dev/null +++ b/.github/blunderbuss.yml @@ -0,0 +1,17 @@ +# Blunderbuss config +# +# This file controls who is assigned for pull requests and issues. +# Note: This file is autogenerated. To make changes to the assignee +# team, please update `codeowner_team` in `.repo-metadata.json`. +assign_issues: + - googleapis/api-bigquery-dataframe + +assign_issues_by: + - labels: + - "samples" + to: + - googleapis/python-samples-reviewers + - googleapis/api-bigquery-dataframe + +assign_prs: + - googleapis/api-bigquery-dataframe diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index dd61f5f3201..51f92b8e12f 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -252,9 +252,9 @@ googleapis-common-protos==1.61.0 \ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b # via google-api-core -idna==3.4 \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests importlib-metadata==6.8.0 \ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ From 9f8f181279133abdb7da3aa045df6fa278587013 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:40:51 -0700 Subject: [PATCH 503/921] fix: address technical writers fb (#611) * fix: address technical writers fb --- bigframes/ml/model_selection.py | 2 +- .../pandas/core/indexes/accessor.py | 2 +- .../bigframes_vendored/sklearn/base.py | 2 +- .../sklearn/metrics/_classification.py | 8 +++--- .../bigframes_vendored/sklearn/pipeline.py | 7 ++--- .../sklearn/preprocessing/_encoder.py | 26 ++++++++++++------- .../sklearn/preprocessing/_label.py | 2 +- 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index 42c13fdb40b..48eb5a93a71 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -35,7 +35,7 @@ def train_test_split( Args: *arrays (bigframes.dataframe.DataFrame or bigframes.series.Series): A sequence of BigQuery DataFrames or Series that can be joined on - their indexes + their indexes. test_size (default None): The proportion of the dataset to include in the test split. If None, this will default to the complement of train_size. If both diff --git a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py index 3f0175359a7..f34612cb117 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/accessor.py @@ -37,7 +37,7 @@ def dayofweek(self): """The day of the week with Monday=0, Sunday=6. Return the day of the week. It is assumed the week starts on - Monday, which is denoted by 0 and ends on Sunday which is denoted + Monday, which is denoted by 0 and ends on Sunday, which is denoted by 6. **Examples:** diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index fd8db7a2270..1a151a1119c 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -153,7 +153,7 @@ def fit_transform(self, X, y=None): Target values (None for unsupervised transformations). Returns: - bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_features_new) + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_features_new). Transformed DataFrame. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/metrics/_classification.py b/third_party/bigframes_vendored/sklearn/metrics/_classification.py index 00bbf8cd605..8e8b2c1952e 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_classification.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_classification.py @@ -122,7 +122,7 @@ def recall_score( ): """Compute the recall. - The recall is the ratio ``tp / (tp + fn)`` where ``tp`` is the number of + The recall is the ratio ``tp / (tp + fn)``, where ``tp`` is the number of true positives and ``fn`` the number of false negatives. The recall is intuitively the ability of the classifier to find all the positive samples. @@ -170,7 +170,7 @@ def precision_score( ): """Compute the precision. - The precision is the ratio ``tp / (tp + fp)`` where ``tp`` is the number of + The precision is the ratio ``tp / (tp + fp)``, where ``tp`` is the number of true positives and ``fp`` the number of false positives. The precision is intuitively the ability of the classifier not to label as positive a sample that is negative. @@ -244,9 +244,9 @@ def f1_score( dtype: float64 Args: - y_true: Series or DataFrame of shape (n_samples,) + y_true: Series or DataFrame of shape (n_samples,). Ground truth (correct) target values. - y_pred: Series or DataFrame of shape (n_samples,) + y_pred: Series or DataFrame of shape (n_samples,). Estimated targets as returned by a classifier. average: {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \ default='binary' diff --git a/third_party/bigframes_vendored/sklearn/pipeline.py b/third_party/bigframes_vendored/sklearn/pipeline.py index aed15659601..8a98ee41417 100644 --- a/third_party/bigframes_vendored/sklearn/pipeline.py +++ b/third_party/bigframes_vendored/sklearn/pipeline.py @@ -20,13 +20,14 @@ class Pipeline(BaseEstimator, metaclass=ABCMeta): """Pipeline of transforms with a final estimator. Sequentially apply a list of transforms and a final estimator. - Intermediate steps of the pipeline must be `transforms`, that is, they + Intermediate steps of the pipeline must be `transforms`. That is, they must implement `fit` and `transform` methods. The final estimator only needs to implement `fit`. The purpose of the pipeline is to assemble several steps that can be - cross-validated together while setting different parameters. This simplifies code, and allows deploying an estimator - and peprocessing together, e.g. with `Pipeline.to_gbq(...).` + cross-validated together while setting different parameters. This + simplifies code and allows for deploying an estimator and peprocessing + together, e.g. with `Pipeline.to_gbq(...).` """ def fit( diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index 5e5e8ac0429..b883e822499 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -23,15 +23,21 @@ class OneHotEncoder(BaseEstimator): Given a dataset with two features, we let the encoder find the unique values per feature and transform the data to a binary one-hot encoding. - .. code-block:: - - from bigframes.ml.preprocessing import OneHotEncoder - import bigframes.pandas as bpd - - enc = OneHotEncoder() - X = bpd.DataFrame({"a": ["Male", "Female", "Female"], "b": ["1", "3", "2"]}) - enc.fit(X) - print(enc.transform(bpd.DataFrame({"a": ["Female", "Male"], "b": ["1", "4"]}))) + >>> from bigframes.ml.preprocessing import OneHotEncoder + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> enc = OneHotEncoder() + >>> X = bpd.DataFrame({"a": ["Male", "Female", "Female"], "b": ["1", "3", "2"]}) + >>> enc.fit(X) + OneHotEncoder() + + >>> print(enc.transform(bpd.DataFrame({"a": ["Female", "Male"], "b": ["1", "4"]}))) + onehotencoded_a onehotencoded_b + 0 [{'index': 1, 'value': 1.0}] [{'index': 1, 'value': 1.0}] + 1 [{'index': 2, 'value': 1.0}] [{'index': 0, 'value': 1.0}] + + [2 rows x 2 columns] Args: drop (Optional[Literal["most_frequent"]], default None): @@ -52,7 +58,7 @@ class OneHotEncoder(BaseEstimator): Specifies an upper limit to the number of output features for each input feature when considering infrequent categories. If there are infrequent categories, max_categories includes the category representing the infrequent categories along with the frequent categories. - Default None, set limit to 1,000,000. + Default None. Set limit to 1,000,000. """ def fit(self, X, y=None): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_label.py b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py index cc6b995c8cf..61a44db92f8 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_label.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_label.py @@ -26,7 +26,7 @@ class LabelEncoder(BaseEstimator): Specifies an upper limit to the number of output features for each input feature when considering infrequent categories. If there are infrequent categories, max_categories includes the category representing the infrequent categories along with the frequent categories. - Default None, set limit to 1,000,000. + Default None. Set limit to 1,000,000. """ def fit(self, y): From 8f9ece6d13f57f02d677bf0e3fea97dea94ae240 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 17 Apr 2024 15:48:38 -0700 Subject: [PATCH 504/921] fix: infer narrowest numeric type when combining numeric columns (#602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Sweña (Swast) --- bigframes/core/__init__.py | 88 +++++- bigframes/core/block_transforms.py | 2 +- bigframes/core/blocks.py | 16 +- bigframes/core/compile/compiled.py | 284 ------------------ bigframes/core/compile/compiler.py | 12 - bigframes/core/compile/scalar_op_compiler.py | 38 +++ bigframes/core/expression.py | 3 - bigframes/core/join_def.py | 5 + bigframes/core/nodes.py | 84 ------ bigframes/dataframe.py | 8 +- bigframes/operations/__init__.py | 100 +++--- tests/system/small/test_dataframe.py | 10 +- .../bigframes_vendored/pandas/core/frame.py | 84 +++--- .../bigframes_vendored/pandas/core/series.py | 6 +- 14 files changed, 233 insertions(+), 507 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 3fa690ef374..9e6b86fc303 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -354,10 +354,7 @@ def unpivot( *, passthrough_columns: typing.Sequence[str] = (), index_col_ids: typing.Sequence[str] = ["index"], - dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Tuple[bigframes.dtypes.Dtype, ...] - ] = pandas.Float64Dtype(), - how: typing.Literal["left", "right"] = "left", + join_side: typing.Literal["left", "right"] = "left", ) -> ArrayValue: """ Unpivot ArrayValue columns. @@ -367,23 +364,88 @@ def unpivot( unpivot_columns: Mapping of column id to list of input column ids. Lists of input columns may use None. passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. index_col_id (str): The column id to be used for the row labels. - dtype (dtype or list of dtype): Dtype to use for the unpivot columns. If list, must be equal in number to unpivot_columns. Returns: ArrayValue: The unpivoted ArrayValue """ + # There will be N labels, used to disambiguate which of N source columns produced each output row + explode_offsets_id = bigframes.core.guid.generate_guid("unpivot_offsets_") + labels_array = self._create_unpivot_labels_array(row_labels, index_col_ids) + labels_array = labels_array.promote_offsets(explode_offsets_id) + + # Unpivot creates N output rows for each input row, labels disambiguate these N rows + joined_array = self._cross_join_w_labels(labels_array, join_side) + + # Build the output rows as a case statment that selects between the N input columns + unpivot_exprs = [] + # Supports producing multiple stacked ouput columns for stacking only part of hierarchical index + for col_id, input_ids in unpivot_columns: + # row explode offset used to choose the input column + # we use offset instead of label as labels are not necessarily unique + cases = tuple( + ( + ops.eq_op.as_expr(explode_offsets_id, ex.const(i)), + ex.free_var(id_or_null) + if (id_or_null is not None) + else ex.const(None), + ) + for i, id_or_null in enumerate(input_ids) + ) + col_expr = ops.case_when_op.as_expr(*cases) + unpivot_exprs.append((col_expr, col_id)) + + label_exprs = ((ex.free_var(id), id) for id in index_col_ids) + # passthrough columns are unchanged, just repeated N times each + passthrough_exprs = ((ex.free_var(id), id) for id in passthrough_columns) return ArrayValue( - nodes.UnpivotNode( - child=self.node, - row_labels=tuple(row_labels), - unpivot_columns=tuple(unpivot_columns), - passthrough_columns=tuple(passthrough_columns), - index_col_ids=tuple(index_col_ids), - dtype=dtype, - how=how, + nodes.ProjectionNode( + child=joined_array.node, + assignments=(*label_exprs, *unpivot_exprs, *passthrough_exprs), ) ) + def _cross_join_w_labels( + self, labels_array: ArrayValue, join_side: typing.Literal["left", "right"] + ) -> ArrayValue: + """ + Convert each row in self to N rows, one for each label in labels array. + """ + table_join_side = ( + join_def.JoinSide.LEFT if join_side == "left" else join_def.JoinSide.RIGHT + ) + labels_join_side = table_join_side.inverse() + labels_mappings = tuple( + join_def.JoinColumnMapping(labels_join_side, id, id) + for id in labels_array.schema.names + ) + table_mappings = tuple( + join_def.JoinColumnMapping(table_join_side, id, id) + for id in self.schema.names + ) + join = join_def.JoinDefinition( + conditions=(), mappings=(*labels_mappings, *table_mappings), type="cross" + ) + if join_side == "left": + joined_array = self.join(labels_array, join_def=join) + else: + joined_array = labels_array.join(self, join_def=join) + return joined_array + + def _create_unpivot_labels_array( + self, + former_column_labels: typing.Sequence[typing.Hashable], + col_ids: typing.Sequence[str], + ) -> ArrayValue: + """Create an ArrayValue from a list of label tuples.""" + rows = [] + for row_offset in range(len(former_column_labels)): + row_label = former_column_labels[row_offset] + row_label = (row_label,) if not isinstance(row_label, tuple) else row_label + row = {col_ids[i]: row_label[i] for i in range(len(col_ids))} + rows.append(row) + + return ArrayValue.from_pyarrow(pa.Table.from_pylist(rows), session=self.session) + def join( self, other: ArrayValue, diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 1eae73014c7..562689a7361 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -857,5 +857,5 @@ def _idx_extrema( # Stack the entire column axis to produce single-column result # Assumption: uniform dtype for stackability return block.aggregate_all_and_stack( - agg_ops.AnyValueOp(), dtype=block.dtypes[0] + agg_ops.AnyValueOp(), ).with_column_labels([original_block.index.name]) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index f6850020df9..0f9cacd83dc 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -914,9 +914,6 @@ def aggregate_all_and_stack( axis: int | str = 0, value_col_id: str = "values", dropna: bool = True, - dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Tuple[bigframes.dtypes.Dtype, ...] - ] = pd.Float64Dtype(), ) -> Block: axis_n = utils.get_axis_number(axis) if axis_n == 0: @@ -931,7 +928,6 @@ def aggregate_all_and_stack( row_labels=self.column_labels.to_list(), index_col_ids=index_col_ids, unpivot_columns=tuple([(value_col_id, tuple(self.value_columns))]), - dtype=dtype, ) return Block( result_expr, @@ -949,7 +945,6 @@ def aggregate_all_and_stack( index_col_ids=[guid.generate_guid()], unpivot_columns=[(value_col_id, tuple(self.value_columns))], passthrough_columns=[*self.index_columns, offset_col], - dtype=dtype, ) index_aggregations = [ (ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.free_var(col_id)), col_id) @@ -1512,13 +1507,10 @@ def stack(self, how="left", levels: int = 1): # Get matching columns unpivot_columns: List[Tuple[str, List[str]]] = [] - dtypes = [] for val in result_col_labels: col_id = guid.generate_guid("unpivot_") input_columns, dtype = self._create_stack_column(val, row_label_tuples) unpivot_columns.append((col_id, input_columns)) - if dtype: - dtypes.append(dtype or pd.Float64Dtype()) added_index_columns = [guid.generate_guid() for _ in range(row_labels.nlevels)] unpivot_expr = self._expr.unpivot( @@ -1526,8 +1518,7 @@ def stack(self, how="left", levels: int = 1): passthrough_columns=self.index_columns, unpivot_columns=unpivot_columns, index_col_ids=added_index_columns, - dtype=tuple(dtypes), - how=how, + join_side=how, ) new_index_level_names = self.column_labels.names[-levels:] if how == "left": @@ -1559,15 +1550,12 @@ def melt( value_labels = [self.col_id_to_label[col_id] for col_id in value_vars] id_labels = [self.col_id_to_label[col_id] for col_id in id_vars] - dtype = self._expr.get_column_type(value_vars[0]) - unpivot_expr = self._expr.unpivot( row_labels=value_labels, passthrough_columns=id_vars, unpivot_columns=(unpivot_col,), index_col_ids=var_col_ids, - dtype=dtype, - how="right", + join_side="right", ) index_id = guid.generate_guid() unpivot_expr = unpivot_expr.promote_offsets(index_id) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index f1c5d620106..a59d5996797 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -40,10 +40,8 @@ OrderingExpression, ) import bigframes.core.schema as schemata -import bigframes.core.utils as utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes -import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops ORDER_ID_COLUMN = "bigframes_ordering_id" @@ -109,36 +107,6 @@ def filter(self: T, predicate: ex.Expression) -> T: """Filter the table on a given expression, the predicate must be a boolean expression.""" ... - @abc.abstractmethod - def unpivot( - self: T, - row_labels: typing.Sequence[typing.Hashable], - unpivot_columns: typing.Sequence[ - typing.Tuple[str, typing.Sequence[typing.Optional[str]]] - ], - *, - passthrough_columns: typing.Sequence[str] = (), - index_col_ids: typing.Sequence[str] = ["index"], - dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] - ] = pandas.Float64Dtype(), - how="left", - ) -> T: - """ - Unpivot ArrayValue columns. - - Args: - row_labels: Identifies the source of the row. Must be equal to length to source column list in unpivot_columns argument. - unpivot_columns: Mapping of column id to list of input column ids. Lists of input columns may use None. - passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. - index_col_id (str): The column id to be used for the row labels. - dtype (dtype or list of dtype): Dtype to use for the unpivot columns. If list, must be equal in number to unpivot_columns. - - Returns: - ArrayValue: The unpivoted ArrayValue - """ - ... - @abc.abstractmethod def _reproject_to_table(self: T) -> T: """ @@ -332,115 +300,6 @@ def _filter(self, predicate_value: ibis_types.BooleanValue) -> UnorderedIR: expr.predicates = [*self._predicates, predicate_value] return expr.build() - def unpivot( - self, - row_labels: typing.Sequence[typing.Hashable], - unpivot_columns: typing.Sequence[ - typing.Tuple[str, typing.Sequence[typing.Optional[str]]] - ], - *, - passthrough_columns: typing.Sequence[str] = (), - index_col_ids: typing.Sequence[str] = ["index"], - dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] - ] = pandas.Float64Dtype(), - how="left", - ) -> UnorderedIR: - if how not in ("left", "right"): - raise ValueError("'how' must be 'left' or 'right'") - table = self._to_ibis_expr() - row_n = len(row_labels) - if not all( - len(source_columns) == row_n for _, source_columns in unpivot_columns - ): - raise ValueError("Columns and row labels must all be same length.") - - unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") - unpivot_table = table.cross_join( - ibis.memtable({unpivot_offset_id: range(row_n)}) - ) - # Use ibis memtable to infer type of rowlabels (if possible) - # TODO: Allow caller to specify dtype - if isinstance(row_labels[0], tuple): - labels_table = ibis.memtable(row_labels) - labels_ibis_types = [ - labels_table[col].type() for col in labels_table.columns - ] - else: - labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] - labels_dtypes = [ - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) - for ibis_type in labels_ibis_types - ] - - label_columns = [] - for label_part, (col_id, label_dtype) in enumerate( - zip(index_col_ids, labels_dtypes) - ): - # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels - labels_as_tuples = [ - label if isinstance(label, tuple) else (label,) for label in row_labels - ] - cases = [ - ( - i, - bigframes.dtypes.literal_to_ibis_scalar( - label_tuple[label_part], # type:ignore - force_dtype=label_dtype, # type:ignore - ), - ) - for i, label_tuple in enumerate(labels_as_tuples) - ] - labels_value = ( - typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) - .cases(cases, default=None) # type:ignore - .name(col_id) - ) - label_columns.append(labels_value) - - unpivot_values = [] - for j in range(len(unpivot_columns)): - col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype - result_col, source_cols = unpivot_columns[j] - null_value = bigframes.dtypes.literal_to_ibis_scalar( - None, force_dtype=col_dtype - ) - ibis_values = [ - op_compiler.compile_row_op( - ops.AsTypeOp(col_dtype), (unpivot_table[col],) - ) - if col is not None - else null_value - for col in source_cols - ] - cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] - unpivot_value = typing.cast( - ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] - ).cases( - cases, default=null_value # type:ignore - ) - unpivot_values.append(unpivot_value.name(result_col)) - - unpivot_table = unpivot_table.select( - passthrough_columns, - *label_columns, - *unpivot_values, - unpivot_offset_id, - ) - - value_columns = [ - unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns - ] - passthrough_values = [unpivot_table[col] for col in passthrough_columns] - return UnorderedIR( - table=unpivot_table, - columns=[ - *[unpivot_table[col_id] for col_id in index_col_ids], - *value_columns, - *passthrough_values, - ], - ) - def aggregate( self, aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]], @@ -920,149 +779,6 @@ def project_window_op( # TODO(tbergeron): Automatically track analytic expression usage and defer reprojection until required for valid query generation. return result._reproject_to_table() if not skip_reproject_unsafe else result - def unpivot( - self, - row_labels: typing.Sequence[typing.Hashable], - unpivot_columns: typing.Sequence[ - typing.Tuple[str, typing.Sequence[typing.Optional[str]]] - ], - *, - passthrough_columns: typing.Sequence[str] = (), - index_col_ids: typing.Sequence[str] = ["index"], - dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Sequence[bigframes.dtypes.Dtype] - ] = pandas.Float64Dtype(), - how="left", - ) -> OrderedIR: - if how not in ("left", "right"): - raise ValueError("'how' must be 'left' or 'right'") - table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) - row_n = len(row_labels) - hidden_col_ids = self._hidden_ordering_column_names.keys() - if not all( - len(source_columns) == row_n for _, source_columns in unpivot_columns - ): - raise ValueError("Columns and row labels must all be same length.") - - unpivot_offset_id = bigframes.core.guid.generate_guid("unpivot_offsets_") - unpivot_table = table.cross_join( - ibis.memtable({unpivot_offset_id: range(row_n)}) - ) - # Use ibis memtable to infer type of rowlabels (if possible) - # TODO: Allow caller to specify dtype - if isinstance(row_labels[0], tuple): - labels_table = ibis.memtable(row_labels) - labels_ibis_types = [ - labels_table[col].type() for col in labels_table.columns - ] - else: - labels_ibis_types = [ibis.memtable({"col": row_labels})["col"].type()] - labels_dtypes = [ - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type) - for ibis_type in labels_ibis_types - ] - - label_columns = [] - for label_part, (col_id, label_dtype) in enumerate( - zip(index_col_ids, labels_dtypes) - ): - # interpret as tuples even if it wasn't originally so can apply same logic for multi-column labels - labels_as_tuples = [ - label if isinstance(label, tuple) else (label,) for label in row_labels - ] - cases = [ - ( - i, - bigframes.dtypes.literal_to_ibis_scalar( - label_tuple[label_part], # type:ignore - force_dtype=label_dtype, # type:ignore - ), - ) - for i, label_tuple in enumerate(labels_as_tuples) - ] - labels_value = ( - typing.cast(ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id]) - .cases(cases, default=None) # type:ignore - .name(col_id) - ) - label_columns.append(labels_value) - - unpivot_values = [] - for j in range(len(unpivot_columns)): - col_dtype = dtype[j] if utils.is_list_like(dtype) else dtype - result_col, source_cols = unpivot_columns[j] - null_value = bigframes.dtypes.literal_to_ibis_scalar( - None, force_dtype=col_dtype - ) - ibis_values = [ - op_compiler.compile_row_op( - ops.AsTypeOp(col_dtype), (unpivot_table[col],) - ) - if col is not None - else null_value - for col in source_cols - ] - cases = [(i, ibis_values[i]) for i in range(len(ibis_values))] - unpivot_value = typing.cast( - ibis_types.IntegerColumn, unpivot_table[unpivot_offset_id] - ).cases( - cases, default=null_value # type:ignore - ) - unpivot_values.append(unpivot_value.name(result_col)) - - unpivot_table = unpivot_table.select( - passthrough_columns, - *label_columns, - *unpivot_values, - *hidden_col_ids, - unpivot_offset_id, - ) - - # Extend the original ordering using unpivot_offset_id - old_ordering = self._ordering - if how == "left": - new_ordering = ExpressionOrdering( - ordering_value_columns=tuple( - [ - *old_ordering.ordering_value_columns, - ascending_over(unpivot_offset_id), - ] - ), - total_ordering_columns=frozenset( - [*old_ordering.total_ordering_columns, unpivot_offset_id] - ), - ) - else: # how=="right" - new_ordering = ExpressionOrdering( - ordering_value_columns=tuple( - [ - ascending_over(unpivot_offset_id), - *old_ordering.ordering_value_columns, - ] - ), - total_ordering_columns=frozenset( - [*old_ordering.total_ordering_columns, unpivot_offset_id] - ), - ) - value_columns = [ - unpivot_table[value_col_id] for value_col_id, _ in unpivot_columns - ] - passthrough_values = [unpivot_table[col] for col in passthrough_columns] - hidden_ordering_columns = [ - unpivot_table[unpivot_offset_id], - *[unpivot_table[hidden_col] for hidden_col in hidden_col_ids], - ] - return OrderedIR( - table=unpivot_table, - columns=[ - *[unpivot_table[col_id] for col_id in index_col_ids], - *value_columns, - *passthrough_values, - ], - hidden_ordering_columns=hidden_ordering_columns, - ordering=new_ordering, - ) - def _reproject_to_table(self) -> OrderedIR: table = self._to_ibis_expr( ordering_mode="unordered", diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 638e3eacdd0..a68023d13d0 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -179,18 +179,6 @@ def compile_reproject(node: nodes.ReprojectOpNode, ordered: bool = True): return compile_node(node.child, ordered)._reproject_to_table() -@_compile_node.register -def compile_unpivot(node: nodes.UnpivotNode, ordered: bool = True): - return compile_node(node.child, ordered).unpivot( - node.row_labels, - node.unpivot_columns, - passthrough_columns=node.passthrough_columns, - index_col_ids=node.index_col_ids, - dtype=node.dtype, - how=node.how, - ) - - @_compile_node.register def compiler_explode(node: nodes.ExplodeNode, ordered: bool = True): return compile_node(node.child, ordered).explode(node.column_ids) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 53a25d63ed0..072d974b390 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -189,6 +189,25 @@ def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): return decorator + def register_nary_op(self, op_ref: typing.Union[ops.NaryOp, type[ops.NaryOp]]): + """ + Decorator to register a nary op implementation. + + Args: + op_ref (NaryOp or NaryOp type): + Class or instance of operator that is implemented by the decorated function. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + return impl(*args) + + self._register(key, normalized_impl) + return impl + + return decorator + def _register( self, op_name: str, @@ -1346,6 +1365,25 @@ def clip_op( ) +@scalar_op_compiler.register_nary_op(ops.case_when_op) +def switch_op(*cases_and_outputs: ibis_types.Value) -> ibis_types.Value: + # ibis can handle most type coercions, but we need to force bool -> int + # TODO: dispatch coercion depending on bigframes dtype schema + result_values = cases_and_outputs[1::2] + do_upcast_bool = any(t.type().is_numeric() for t in result_values) + if do_upcast_bool: + # Just need to upcast to int, ibis can handle further coercion + result_values = tuple( + val.cast(ibis_dtypes.int64) if val.type().is_boolean() else val + for val in result_values + ) + + case_val = ibis.case() + for predicate, output in zip(cases_and_outputs[::2], result_values): + case_val = case_val.when(predicate, output) + return case_val.end() + + # Helpers def is_null(value) -> bool: # float NaN/inf should be treated as distinct from 'true' null values diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 4980f5369de..70eb519a1bd 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -190,9 +190,6 @@ class OpExpression(Expression): op: bigframes.operations.RowOp inputs: typing.Tuple[Expression, ...] - def __post_init__(self): - assert self.op.arguments == len(self.inputs) - @property def unbound_variables(self) -> typing.Tuple[str, ...]: return tuple( diff --git a/bigframes/core/join_def.py b/bigframes/core/join_def.py index 4646a0d6ae8..632a1864daa 100644 --- a/bigframes/core/join_def.py +++ b/bigframes/core/join_def.py @@ -22,6 +22,11 @@ class JoinSide(enum.Enum): LEFT = 0 RIGHT = 1 + def inverse(self) -> JoinSide: + if self == JoinSide.LEFT: + return JoinSide.RIGHT + return JoinSide.LEFT + JoinType = Literal["inner", "outer", "left", "right", "cross"] diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index a1072b0d687..688e1657328 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -21,8 +21,6 @@ import typing from typing import Callable, Tuple -import pandas - import bigframes.core.expression as ex import bigframes.core.guid from bigframes.core.join_def import JoinColumnMapping, JoinDefinition, JoinSide @@ -579,88 +577,6 @@ def relation_ops_created(self) -> int: return 0 -@dataclass(frozen=True) -class UnpivotNode(UnaryNode): - # TODO: Refactor unpivot - row_labels: typing.Tuple[typing.Hashable, ...] - unpivot_columns: typing.Tuple[ - typing.Tuple[str, typing.Tuple[typing.Optional[str], ...]], ... - ] - passthrough_columns: typing.Tuple[str, ...] = () - index_col_ids: typing.Tuple[str, ...] = ("index",) - dtype: typing.Union[ - bigframes.dtypes.Dtype, typing.Tuple[bigframes.dtypes.Dtype, ...] - ] = (pandas.Float64Dtype(),) - how: typing.Literal["left", "right"] = "left" - - def __hash__(self): - return self._node_hash - - @property - def row_preserving(self) -> bool: - return False - - @property - def non_local(self) -> bool: - return True - - @property - def joins(self) -> bool: - return True - - @functools.cached_property - def schema(self) -> schemata.ArraySchema: - def infer_dtype( - values: typing.Iterable[typing.Hashable], - ) -> bigframes.dtypes.Dtype: - item_types = map(lambda x: bigframes.dtypes.infer_literal_type(x), values) - etype = functools.reduce( - lambda t1, t2: bigframes.dtypes.lcd_type(t1, t2) - if (t1 and t2) - else None, - item_types, - ) - return bigframes.dtypes.dtype_for_etype(etype) - - label_tuples = [ - label if isinstance(label, tuple) else (label,) for label in self.row_labels - ] - idx_dtypes = [ - infer_dtype(map(lambda x: typing.cast(tuple, x)[i], label_tuples)) - for i in range(len(self.index_col_ids)) - ] - - index_items = [ - schemata.SchemaItem(id, dtype) - for id, dtype in zip(self.index_col_ids, idx_dtypes) - ] - value_dtypes = ( - self.dtype - if isinstance(self.dtype, tuple) - else (self.dtype,) * len(self.unpivot_columns) - ) - value_items = [ - schemata.SchemaItem(col[0], dtype) - for col, dtype in zip(self.unpivot_columns, value_dtypes) - ] - passthrough_items = [ - schemata.SchemaItem(id, self.child.schema.get_type(id)) - for id in self.passthrough_columns - ] - return schemata.ArraySchema((*index_items, *value_items, *passthrough_items)) - - @property - def variables_introduced(self) -> int: - return ( - len(self.schema.items) - len(self.passthrough_columns) + OVERHEAD_VARIABLES - ) - - @property - def relation_ops_created(self) -> int: - # Unpivot is essentially a cross join and a projection. - return 2 - - @dataclass(frozen=True) class RandomSampleNode(UnaryNode): fraction: float diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 953a89c34fd..11e592542c1 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1961,9 +1961,7 @@ def any( frame = self._raise_on_non_boolean("any") else: frame = self._drop_non_bool() - block = frame._block.aggregate_all_and_stack( - agg_ops.any_op, dtype=pandas.BooleanDtype(), axis=axis - ) + block = frame._block.aggregate_all_and_stack(agg_ops.any_op, axis=axis) return bigframes.series.Series(block.select_column("values")) def all( @@ -1973,9 +1971,7 @@ def all( frame = self._raise_on_non_boolean("all") else: frame = self._drop_non_bool() - block = frame._block.aggregate_all_and_stack( - agg_ops.all_op, dtype=pandas.BooleanDtype(), axis=axis - ) + block = frame._block.aggregate_all_and_stack(agg_ops.all_op, axis=axis) return bigframes.series.Series(block.select_column("values")) def sum( diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index d631ba85081..a7c385a2b8a 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -15,7 +15,9 @@ from __future__ import annotations import dataclasses +import functools import typing +from typing import Tuple, Union import numpy as np import pandas as pd @@ -34,11 +36,6 @@ class RowOp(typing.Protocol): def name(self) -> str: ... - @property - def arguments(self) -> int: - """The number of column argument the operation takes""" - ... - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: ... @@ -48,21 +45,29 @@ def order_preserving(self) -> bool: ... -# These classes can be used to create simple ops that don't take local parameters -# All is needed is a unique name, and to register an implementation in ibis_mappings.py @dataclasses.dataclass(frozen=True) -class UnaryOp: +class NaryOp: @property def name(self) -> str: raise NotImplementedError("RowOp abstract base class has no implementation") + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + raise NotImplementedError("Abstract operation has no output type") + + @property + def order_preserving(self) -> bool: + """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" + return False + + +# These classes can be used to create simple ops that don't take local parameters +# All is needed is a unique name, and to register an implementation in ibis_mappings.py +@dataclasses.dataclass(frozen=True) +class UnaryOp(NaryOp): @property def arguments(self) -> int: return 1 - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - raise NotImplementedError("Abstract operation has no output type") - def as_expr( self, input_id: typing.Union[str, bigframes.core.expression.Expression] = "arg" ) -> bigframes.core.expression.Expression: @@ -72,25 +77,13 @@ def as_expr( self, (_convert_expr_input(input_id),) ) - @property - def order_preserving(self) -> bool: - """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" - return False - @dataclasses.dataclass(frozen=True) -class BinaryOp: - @property - def name(self) -> str: - raise NotImplementedError("RowOp abstract base class has no implementation") - +class BinaryOp(NaryOp): @property def arguments(self) -> int: return 2 - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - raise NotImplementedError("Abstract operation has no output type") - def as_expr( self, left_input: typing.Union[str, bigframes.core.expression.Expression] = "arg1", @@ -106,25 +99,13 @@ def as_expr( ), ) - @property - def order_preserving(self) -> bool: - """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" - return False - @dataclasses.dataclass(frozen=True) -class TernaryOp: - @property - def name(self) -> str: - raise NotImplementedError("RowOp abstract base class has no implementation") - +class TernaryOp(NaryOp): @property def arguments(self) -> int: return 3 - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: - raise NotImplementedError("Abstract operation has no output type") - def as_expr( self, input1: typing.Union[str, bigframes.core.expression.Expression] = "arg1", @@ -142,11 +123,6 @@ def as_expr( ), ) - @property - def order_preserving(self) -> bool: - """Whether the row operation preserves total ordering. Can be pruned from ordering expressions.""" - return False - def _convert_expr_input( input: typing.Union[str, bigframes.core.expression.Expression] @@ -664,6 +640,46 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT clip_op = ClipOp() + +class CaseWhenOp(NaryOp): + name: typing.ClassVar[str] = "switch" + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + assert len(input_types) % 2 == 0 + # predicate1, output1, predicate2, output2... + if not all(map(lambda x: x == dtypes.BOOL_DTYPE, input_types[::2])): + raise TypeError(f"Case inputs {input_types[::2]} must be boolean-valued") + output_expr_types = input_types[1::2] + return functools.reduce( + lambda t1, t2: dtypes.coerce_to_common(t1, t2), + output_expr_types, + ) + + def as_expr( + self, + *case_output_pairs: Tuple[ + Union[str | bigframes.core.expression.Expression], + Union[str | bigframes.core.expression.Expression], + ], + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + # Keep this in sync with output_type and compilers + inputs: list[bigframes.core.expression.Expression] = [] + + for case, output in case_output_pairs: + inputs.append(_convert_expr_input(case)) + inputs.append(_convert_expr_input(output)) + + return bigframes.core.expression.OpExpression( + self, + tuple(inputs), + ) + + +case_when_op = CaseWhenOp() + + # Just parameterless unary ops for now # TODO: Parameter mappings NUMPY_TO_OP: typing.Final = { diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 7fef7a9dc73..4c598a682d8 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2987,10 +2987,14 @@ def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op, ord bf_result = bf_series.to_pandas(ordered=ordered) # Pandas may produce narrower numeric types, but bigframes always produces Float64 - pd_series = pd_series.astype("Float64") # Pandas has object index type + pd_series.index = pd_series.index.astype(pd.StringDtype(storage="pyarrow")) assert_series_equal( - pd_series, bf_result, check_index_type=False, ignore_order=not ordered + pd_series, + bf_result, + check_index_type=False, + ignore_order=not ordered, + check_dtype=False, ) @@ -3079,7 +3083,7 @@ def test_dataframe_bool_aggregates(scalars_df_index, scalars_pandas_df_index, op pd_series = op(scalars_pandas_df_index).astype("boolean") bf_result = bf_series.to_pandas() - # Pandas has object index type + pd_series.index = pd_series.index.astype(bf_result.index.dtype) pd.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index e8949006466..c692bdbfec6 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4327,16 +4327,16 @@ def min(self, axis=0, *, numeric_only: bool = False): Finding the minimum value in each column (the default behavior without an explicit axis parameter). >>> df.min() - A 1.0 - B 2.0 - dtype: Float64 + A 1 + B 2 + dtype: Int64 Finding the minimum value in each row. >>> df.min(axis=1) - 0 1.0 - 1 3.0 - dtype: Float64 + 0 1 + 1 3 + dtype: Int64 Args: axis ({index (0), columns (1)}): @@ -4372,16 +4372,16 @@ def max(self, axis=0, *, numeric_only: bool = False): Finding the maximum value in each column (the default behavior without an explicit axis parameter). >>> df.max() - A 3.0 - B 4.0 - dtype: Float64 + A 3 + B 4 + dtype: Int64 Finding the maximum value in each row. >>> df.max(axis=1) - 0 2.0 - 1 4.0 - dtype: Float64 + 0 2 + 1 4 + dtype: Int64 Args: axis ({index (0), columns (1)}): @@ -4416,16 +4416,16 @@ def sum(self, axis=0, *, numeric_only: bool = False): Calculating the sum of each column (the default behavior without an explicit axis parameter). >>> df.sum() - A 4.0 - B 6.0 - dtype: Float64 + A 4 + B 6 + dtype: Int64 Calculating the sum of each row. >>> df.sum(axis=1) - 0 3.0 - 1 7.0 - dtype: Float64 + 0 3 + 1 7 + dtype: Int64 Args: axis ({index (0), columns (1)}): @@ -4500,9 +4500,9 @@ def median(self, *, numeric_only: bool = False, exact: bool = False): Finding the median value of each column. >>> df.median() - A 1.0 - B 2.0 - dtype: Float64 + A 1 + B 2 + dtype: Int64 Args: numeric_only (bool. default False): @@ -4748,10 +4748,10 @@ def count(self, *, numeric_only: bool = False): Counting non-NA values for each column: >>> df.count() - A 4.0 - B 5.0 - C 3.0 - dtype: Float64 + A 4 + B 5 + C 3 + dtype: Int64 Args: numeric_only (bool, default False): @@ -5051,17 +5051,17 @@ def melt(self, id_vars, value_vars, var_name, value_name): Using `melt` with `id_vars` and `value_vars`: >>> df.melt(id_vars='A', value_vars=['B', 'C']) - A variable value - 0 1.0 B 1 - 1 B 2 - 2 3.0 B 3 - 3 4.0 B 4 - 4 5.0 B 5 - 5 1.0 C - 6 C 3 - 7 3.0 C - 8 4.0 C 4 - 9 5.0 C 5 + A variable value + 0 1.0 B 1.0 + 1 B 2.0 + 2 3.0 B 3.0 + 3 4.0 B 4.0 + 4 5.0 B 5.0 + 5 1.0 C + 6 C 3.5 + 7 3.0 C + 8 4.0 C 4.5 + 9 5.0 C 5.0 [10 rows x 3 columns] @@ -5102,9 +5102,9 @@ def nunique(self): [3 rows x 2 columns] >>> df.nunique() - A 3.0 - B 2.0 - dtype: Float64 + A 3 + B 2 + dtype: Int64 Returns: bigframes.series.Series: Series with number of distinct elements. @@ -5313,9 +5313,9 @@ def agg(self, func): Using a single function: >>> df.agg('sum') - A 6.0 - B 6.0 - dtype: Float64 + A 6 + B 6 + dtype: Int64 Using a list of functions: diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 5e3b4c46ef4..edefb334b30 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -593,9 +593,9 @@ def agg(self, func): 1 >>> s.agg(['min', 'max']) - min 1.0 - max 4.0 - dtype: Float64 + min 1 + max 4 + dtype: Int64 Args: func (function): From 9c106bd24482620ef5ff3c85f94be9da76c49716 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:56:38 -0700 Subject: [PATCH 505/921] feat: Add fine tuning `fit()` for Palm2TextGenerator (#616) * feat: support list of numerics in pandas.cut (#580) An internal user encountered this missing overload * move the tests to load-testing * add predict tests * address comments * address comments --------- Co-authored-by: Henry Solberg --- bigframes/ml/core.py | 40 +++++++++++++++++ bigframes/ml/llm.py | 71 ++++++++++++++++++++++++++++++- bigframes/ml/sql.py | 17 ++++++++ tests/system/load/test_llm.py | 68 +++++++++++++++++++++++++++++ tests/system/small/ml/test_llm.py | 2 +- tests/unit/ml/test_sql.py | 23 ++++++++++ 6 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 tests/system/load/test_llm.py diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 04aaeec1bcf..b94ae39687a 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -321,6 +321,46 @@ def create_model( return self._create_model_with_sql(session=session, sql=sql) + def create_llm_remote_model( + self, + X_train: bpd.DataFrame, + y_train: bpd.DataFrame, + connection_name: str, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> BqmlModel: + """Create a session-temporary BQML model with the CREATE OR REPLACE MODEL statement + + Args: + X_train: features columns for training + y_train: labels columns for training + options: a dict of options to configure the model. Generates a BQML OPTIONS + clause + connection_name: + a BQ connection to talk with Vertex AI, of the format ... https://cloud.google.com/bigquery/docs/create-cloud-resource-connection + + Returns: a BqmlModel, wrapping a trained model in BigQuery + """ + options = dict(options) + # Cache dataframes to make sure base table is not a snapshot + # cached dataframe creates a full copy, never uses snapshot + input_data = X_train._cached(force=True).join( + y_train._cached(force=True), how="outer" + ) + options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) + + session = X_train._session + + model_ref = self._create_model_ref(session._anonymous_dataset) + + sql = self._model_creation_sql_generator.create_llm_remote_model( + source_df=input_data, + model_ref=model_ref, + options=options, + connection_name=connection_name, + ) + + return self._create_model_with_sql(session=session, sql=sql) + def create_time_series_model( self, X_train: bpd.DataFrame, diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 31c691fd515..37a38cdd5c9 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -27,6 +27,10 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +_BQML_PARAMS_MAPPING = { + "max_iterations": "maxIterations", +} + _TEXT_GENERATOR_BISON_ENDPOINT = "text-bison" _TEXT_GENERATOR_BISON_32K_ENDPOINT = "text-bison-32k" _TEXT_GENERATOR_ENDPOINTS = ( @@ -62,6 +66,8 @@ class PaLM2TextGenerator(base.BaseEstimator): Connection to connect with remote service. str of the format ... if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach permission if the connection isn't fully setup. + max_iterations (Optional[int], Default to 300): + The number of steps to run when performing supervised tuning. """ def __init__( @@ -70,9 +76,11 @@ def __init__( model_name: Literal["text-bison", "text-bison-32k"] = "text-bison", session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, + max_iterations: int = 300, ): self.model_name = model_name self.session = session or bpd.get_global_session() + self.max_iterations = max_iterations self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection @@ -132,12 +140,73 @@ def _from_bq( model_connection = model._properties["remoteModelInfo"]["connection"] model_endpoint = bqml_endpoint.split("/")[-1] + # Get the optional params + kwargs: dict = {} + last_fitting = model.training_runs[-1]["trainingOptions"] + + dummy_text_generator = cls() + for bf_param, _ in dummy_text_generator.__dict__.items(): + bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) + if bqml_param in last_fitting: + # Convert types + if bf_param in ["max_iterations"]: + kwargs[bf_param] = int(last_fitting[bqml_param]) + text_generator_model = cls( - session=session, model_name=model_endpoint, connection_name=model_connection + **kwargs, + session=session, + model_name=model_endpoint, + connection_name=model_connection, ) text_generator_model._bqml_model = core.BqmlModel(session, model) return text_generator_model + @property + def _bqml_options(self) -> dict: + """The model options as they will be set for BQML""" + options = { + "max_iterations": self.max_iterations, + "data_split_method": "NO_SPLIT", + } + return options + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + ) -> PaLM2TextGenerator: + """Fine tune PaLM2TextGenerator model. + + .. note:: + + This product or feature 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 + (https://cloud.google.com/products#product-launch-stages). + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + DataFrame of shape (n_samples, n_features). Training data. + y (bigframes.dataframe.DataFrame or bigframes.series.Series: + Training labels. + + Returns: + PaLM2TextGenerator: Fitted Estimator. + """ + X, y = utils.convert_to_dataframe(X, y) + + options = self._bqml_options + options["endpoint"] = self.model_name + "@001" + options["prompt_col"] = X.columns.tolist()[0] + + self._bqml_model = self._bqml_model_factory.create_llm_remote_model( + X, + y, + options=options, + connection_name=self.connection_name, + ) + return self + def predict( self, X: Union[bpd.DataFrame, bpd.Series], diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index fab358cce37..59c768ce819 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -177,6 +177,23 @@ def create_model( parts.append(f"AS {source_sql}") return "\n".join(parts) + def create_llm_remote_model( + self, + source_df: bpd.DataFrame, + connection_name: str, + model_ref: google.cloud.bigquery.ModelReference, + options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, + ) -> str: + """Encode the CREATE OR REPLACE MODEL statement for BQML""" + source_sql = source_df.sql + + parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] + parts.append(self.connection(connection_name)) + if options: + parts.append(self.options(**options)) + parts.append(f"AS {source_sql}") + return "\n".join(parts) + def create_remote_model( self, connection_name: str, diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py new file mode 100644 index 00000000000..62ef7d5c725 --- /dev/null +++ b/tests/system/load/test_llm.py @@ -0,0 +1,68 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +import bigframes.ml.llm + + +@pytest.fixture(scope="session") +def llm_fine_tune_df_default_index( + session: bigframes.Session, +) -> bigframes.dataframe.DataFrame: + sql = """ +SELECT + CONCAT("Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: ", text) as prompt, + CAST(label AS STRING) as label +FROM `llm_tuning.emotion_classification_train` +""" + return session.read_gbq(sql) + + +@pytest.fixture(scope="session") +def llm_remote_text_pandas_df(): + """Additional data matching the penguins dataset, with a new index""" + return pd.DataFrame( + { + "prompt": [ + "Please do sentiment analysis on the following text and only output a number from 0 to 5where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: i feel beautifully emotional knowing that these women of whom i knew just a handful were holding me and my baba on our journey", + "Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: i was feeling a little vain when i did this one", + "Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: a father of children killed in an accident", + ], + } + ) + + +def test_llm_palm_configure_fit( + llm_fine_tune_df_default_index, llm_remote_text_pandas_df +): + model = bigframes.ml.llm.PaLM2TextGenerator( + model_name="text-bison", max_iterations=1 + ) + + df = llm_fine_tune_df_default_index.dropna() + X_train = df[["prompt"]] + y_train = df[["label"]] + model.fit(X_train, y_train) + + assert model is not None + + df = model.predict(llm_remote_text_pandas_df).to_pandas() + assert df.shape == (3, 4) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() == 1) + + # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index b9e48898017..6f6b67597ac 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 5b1ff37775c..3560f05cb68 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -181,6 +181,29 @@ def test_create_model_transform_correct( ) +def test_create_llm_remote_model_correct( + model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, + mock_df: bpd.DataFrame, +): + sql = model_creation_sql_generator.create_llm_remote_model( + source_df=mock_df, + connection_name="my_project.us.my_connection", + model_ref=bigquery.ModelReference.from_string( + "test-proj._anonXYZ.create_remote_model" + ), + options={"option_key1": "option_value1", "option_key2": 2}, + ) + assert ( + sql + == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_remote_model` +REMOTE WITH CONNECTION `my_project.us.my_connection` +OPTIONS( + option_key1="option_value1", + option_key2=2) +AS input_X_y_sql""" + ) + + def test_create_remote_model_correct( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, ): From 9665e39ef288841f03a9d823bd2210ef58394ad3 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 18 Apr 2024 10:10:30 -0700 Subject: [PATCH 506/921] docs: Fix rendering of examples for multiple apis (#620) --- third_party/bigframes_vendored/pandas/core/frame.py | 1 + third_party/bigframes_vendored/pandas/core/generic.py | 1 + .../bigframes_vendored/pandas/core/groupby/__init__.py | 1 + third_party/bigframes_vendored/pandas/core/series.py | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index c692bdbfec6..f06128f150e 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4523,6 +4523,7 @@ def quantile( Return values at the given quantile over requested axis. **Examples:** + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame(np.array([[1, 1], [2, 10], [3, 100], [4, 100]]), diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 9c6120fd6c6..54c876ef3c8 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -61,6 +61,7 @@ def __iter__(self) -> Iterator: iterator **Examples:** + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 6310d7e271b..7347963d179 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -90,6 +90,7 @@ def quantile(self, q=0.5, *, numeric_only: bool = False): Return group values at the given quantile, a la numpy.percentile. **Examples:** + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame([ diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index edefb334b30..a5e14c5b1c1 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -862,6 +862,7 @@ def autocorr(self, lag: int = 1) -> float: the Series and its shifted self. **Examples:** + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None @@ -2812,6 +2813,7 @@ def combine_first(self, other) -> Series: of the two indexes. **Examples:** + >>> import bigframes.pandas as bpd >>> import numpy as np >>> bpd.options.display.progress_bar = None @@ -2852,6 +2854,7 @@ def update(self, other) -> None: on index. **Examples:** + >>> import bigframes.pandas as bpd >>> import pandas as pd >>> import numpy as np @@ -3168,6 +3171,7 @@ def quantile( Return value at the given quantile. **Examples:** + >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None >>> s = bpd.Series([1, 2, 3, 4]) From 3706b4f9dde65788b5e6343a6428fb1866499461 Mon Sep 17 00:00:00 2001 From: Stephanie A <129541811+DevStephanie@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:29:22 -0500 Subject: [PATCH 507/921] feat: warn if location is set to unknown location (#609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: warn if location is set to unknown location * tests error message --------- Co-authored-by: Shobhit Singh Co-authored-by: Tim Sweña (Swast) --- bigframes/_config/bigquery_options.py | 23 ++++++++++ bigframes/exceptions.py | 17 +++++++ tests/unit/_config/test_bigquery_options.py | 51 +++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 bigframes/exceptions.py diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 50e14eaf288..74561e6f248 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -22,11 +22,33 @@ import google.api_core.exceptions import google.auth.credentials +import bigframes.constants +import bigframes.exceptions + SESSION_STARTED_MESSAGE = ( "Cannot change '{attribute}' once a session has started. " "Call bigframes.pandas.close_session() first, if you are using the bigframes.pandas API." ) +UNKNOWN_LOCATION_MESSAGE = "The location '{location}' is set to an unknown value." + + +def _validate_location(value: Optional[str]): + + if value is None: + return + + if value not in bigframes.constants.ALL_BIGQUERY_LOCATIONS: + warnings.warn( + UNKNOWN_LOCATION_MESSAGE.format(location=value), + # There are many layers before we get to (possibly) the user's code: + # -> bpd.options.bigquery.location = "us-central-1" + # -> location.setter + # -> _validate_location + stacklevel=3, + category=bigframes.exceptions.UnknownLocationWarning, + ) + class BigQueryOptions: """Encapsulates configuration for working with a session.""" @@ -93,6 +115,7 @@ def location(self) -> Optional[str]: def location(self, value: Optional[str]): if self._session_started and self._location != value: raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="location")) + _validate_location(value) self._location = value @property diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py new file mode 100644 index 00000000000..62122e79d2d --- /dev/null +++ b/bigframes/exceptions.py @@ -0,0 +1,17 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class UnknownLocationWarning(Warning): + """The location is set to an unknown value.""" diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index cf130846101..7d9a452f424 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -13,10 +13,13 @@ # limitations under the License. import re +import warnings import pytest +import bigframes import bigframes._config.bigquery_options as bigquery_options +import bigframes.exceptions @pytest.mark.parametrize( @@ -78,3 +81,51 @@ def test_setter_if_session_started_but_setting_the_same_value(attribute): setattr(options, attribute, original_object) assert getattr(options, attribute) is original_object + + +@pytest.mark.parametrize( + [ + "valid_location", + ], + [ + (None,), + ("us-central1",), + ], +) +def test_location_set_to_valid_no_warning(valid_location): + options = bigquery_options.BigQueryOptions() + # Ensure that no warnings are emitted. + # https://docs.pytest.org/en/7.0.x/how-to/capture-warnings.html#additional-use-cases-of-warnings-in-tests + with warnings.catch_warnings(): + # Turn matching UnknownLocationWarning into exceptions. + # https://docs.python.org/3/library/warnings.html#warning-filter + warnings.simplefilter( + "error", category=bigframes.exceptions.UnknownLocationWarning + ) + options.location = valid_location + + +@pytest.mark.parametrize( + [ + "invalid_location", + ], + [ + # Test with common mistakes, see article. + # https://en.wikipedia.org/wiki/Edit_distance#Formal_definition_and_properties + # Substitution + ("us-wist-3",), + # Insertion + ("us-central-1",), + # Deletion + ("asia-suth2",), + ], +) +def test_location_set_to_invalid_warning(invalid_location): + options = bigquery_options.BigQueryOptions() + with pytest.warns( + bigframes.exceptions.UnknownLocationWarning, + match=re.escape( + f"The location '{invalid_location}' is set to an unknown value." + ), + ): + options.location = invalid_location From 9d205aecb77f35baeec82a8f6e1b72c2d852ca46 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 18 Apr 2024 15:31:03 -0700 Subject: [PATCH 508/921] fix: Use exact median implementation by default (#619) --- bigframes/core/block_transforms.py | 3 +- bigframes/core/groupby/__init__.py | 8 ++--- bigframes/dataframe.py | 10 +++---- bigframes/series.py | 2 +- tests/system/small/test_series.py | 29 +++++++++++++++---- .../bigframes_vendored/pandas/core/frame.py | 12 ++++---- .../pandas/core/groupby/__init__.py | 7 ++--- .../bigframes_vendored/pandas/core/series.py | 8 ++--- 8 files changed, 48 insertions(+), 31 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 562689a7361..a221b343a5b 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -111,6 +111,7 @@ def quantile( columns: Sequence[str], qs: Sequence[float], grouping_column_ids: Sequence[str] = (), + dropna: bool = False, ) -> blocks.Block: # TODO: handle windowing and more interpolation methods window = core.WindowSpec( @@ -134,7 +135,7 @@ def quantile( block, results = block.aggregate( grouping_column_ids, tuple((col, agg_ops.AnyValueOp()) for col in quantile_cols), - dropna=True, + dropna=dropna, ) return block.select_columns(results).with_column_labels(labels) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 0f533423528..05b1cc7f419 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -113,9 +113,7 @@ def mean(self, numeric_only: bool = False, *args) -> df.DataFrame: self._raise_on_non_numeric("mean") return self._aggregate_all(agg_ops.mean_op, numeric_only=True) - def median( - self, numeric_only: bool = False, *, exact: bool = False - ) -> df.DataFrame: + def median(self, numeric_only: bool = False, *, exact: bool = True) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("median") if exact: @@ -138,6 +136,7 @@ def quantile( q_cols, qs=tuple(q) if multi_q else (q,), # type: ignore grouping_column_ids=self._by_col_ids, + dropna=self._dropna, ) result_df = df.DataFrame(result) if multi_q: @@ -491,7 +490,7 @@ def mean(self, *args) -> series.Series: def median( self, *args, - exact: bool = False, + exact: bool = True, **kwargs, ) -> series.Series: if exact: @@ -508,6 +507,7 @@ def quantile( (self._value_column,), qs=tuple(q) if multi_q else (q,), # type: ignore grouping_column_ids=self._by_col_ids, + dropna=self._dropna, ) if multi_q: return series.Series(result.stack()) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 11e592542c1..ff8404761ce 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1995,18 +1995,16 @@ def mean( return bigframes.series.Series(block.select_column("values")) def median( - self, *, numeric_only: bool = False, exact: bool = False + self, *, numeric_only: bool = False, exact: bool = True ) -> bigframes.series.Series: - if exact: - raise NotImplementedError( - f"Only approximate median is supported. {constants.FEEDBACK_LINK}" - ) if not numeric_only: frame = self._raise_on_non_numeric("median") else: frame = self._drop_non_numeric() if exact: - return self.quantile() + result = frame.quantile() + result.name = None + return result else: block = frame._block.aggregate_all_and_stack(agg_ops.median_op) return bigframes.series.Series(block.select_column("values")) diff --git a/bigframes/series.py b/bigframes/series.py index b834411bce5..47acfd0afb6 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -966,7 +966,7 @@ def mode(self) -> Series: def mean(self) -> float: return typing.cast(float, self._apply_aggregation(agg_ops.mean_op)) - def median(self, *, exact: bool = False) -> float: + def median(self, *, exact: bool = True) -> float: if exact: return typing.cast(float, self.quantile(0.5)) else: diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 87267696bab..9cb615fdcbf 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1345,10 +1345,9 @@ def test_numeric_literal(scalars_dfs): scalars_df, _ = scalars_dfs col_name = "numeric_col" assert scalars_df[col_name].dtype == pd.ArrowDtype(pa.decimal128(38, 9)) - bf_result = scalars_df[col_name] - scalars_df[col_name].median() + bf_result = scalars_df[col_name] + 42 assert bf_result.size == scalars_df[col_name].size - # TODO(b/323387826): The precision increased by 1 unexpectedly. - # assert bf_result.dtype == pd.ArrowDtype(pa.decimal128(38, 9)) + assert bf_result.dtype == pd.ArrowDtype(pa.decimal128(38, 9)) def test_repr(scalars_dfs): @@ -1523,12 +1522,32 @@ def test_groupby_mean(scalars_dfs): ) -def test_groupby_median(scalars_dfs): +def test_groupby_median_exact(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_series = ( + bf_result = ( scalars_df[col_name].groupby(scalars_df["string_col"], dropna=False).median() ) + pd_result = ( + scalars_pandas_df[col_name] + .groupby(scalars_pandas_df["string_col"], dropna=False) + .median() + ) + + assert_series_equal( + pd_result, + bf_result.to_pandas(), + ) + + +def test_groupby_median_inexact(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "int64_too" + bf_series = ( + scalars_df[col_name] + .groupby(scalars_df["string_col"], dropna=False) + .median(exact=False) + ) pd_max = ( scalars_pandas_df[col_name] .groupby(scalars_pandas_df["string_col"], dropna=False) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index f06128f150e..0515f690e35 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4481,7 +4481,7 @@ def mean(self, axis=0, *, numeric_only: bool = False): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def median(self, *, numeric_only: bool = False, exact: bool = False): + def median(self, *, numeric_only: bool = False, exact: bool = True): """Return the median of the values over colunms. **Examples:** @@ -4500,15 +4500,15 @@ def median(self, *, numeric_only: bool = False, exact: bool = False): Finding the median value of each column. >>> df.median() - A 1 - B 2 - dtype: Int64 + A 2.0 + B 3.0 + dtype: Float64 Args: numeric_only (bool. default False): Default False. Include only float, int, boolean columns. - exact (bool. default False): - Default False. Get the exact median instead of an approximate + exact (bool. default True): + Default True. Get the exact median instead of an approximate one. Returns: diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index 7347963d179..f3f7748e34a 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -68,7 +68,7 @@ def median( self, numeric_only: bool = False, *, - exact: bool = False, + exact: bool = True, ): """ Compute median of groups, excluding missing values. @@ -76,9 +76,8 @@ def median( Args: numeric_only (bool, default False): Include only float, int, boolean columns. - exact (bool, default False): - Calculate the exact median instead of an approximation. Note: - ``exact=True`` is not supported. + exact (bool, default True): + Calculate the exact median instead of an approximation. Returns: pandas.Series or pandas.DataFrame: Median of groups. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index a5e14c5b1c1..0c5b8d45219 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3150,13 +3150,13 @@ def mean(self): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def median(self, *, exact: bool = False): + def median(self, *, exact: bool = True): """Return the median of the values over the requested axis. Args: - exact (bool. default False): - Default False. Get the exact median instead of an approximate - one. Note: ``exact=True`` not yet supported. + exact (bool. default True): + Default True. Get the exact median instead of an approximate + one. Returns: scalar: Scalar. From 240a1ac6fa914550bb6216cd5d179a36009f2657 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 19 Apr 2024 21:54:50 +0000 Subject: [PATCH 509/921] feat: expose `max_batching_rows` in `remote_function` (#622) * feat: expose `max_batching_rows` in `remote_function` * fix option formation, add tests * fix type annotation * assert max_batching_rows after routing creation * add forgotten assert --- bigframes/functions/remote_function.py | 46 +++++++++++++++++++--- bigframes/pandas/__init__.py | 2 + bigframes/session/__init__.py | 11 ++++++ tests/system/large/test_remote_function.py | 36 +++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 178c911591d..f866575a268 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -145,7 +145,13 @@ def __init__( self._cloud_function_docker_repository = cloud_function_docker_repository def create_bq_remote_function( - self, input_args, input_types, output_type, endpoint, bq_function_name + self, + input_args, + input_types, + output_type, + endpoint, + bq_function_name, + max_batching_rows, ): """Create a BigQuery remote function given the artifacts of a user defined function and the http endpoint of a corresponding cloud function.""" @@ -169,14 +175,25 @@ def create_bq_remote_function( bq_function_args.append( f"{name} {third_party_ibis_bqtypes.BigQueryType.from_ibis(input_types[idx])}" ) + + remote_function_options = { + "endpoint": endpoint, + "max_batching_rows": max_batching_rows, + } + + remote_function_options_str = ", ".join( + [ + f'{key}="{val}"' if isinstance(val, str) else f"{key}={val}" + for key, val in remote_function_options.items() + if val is not None + ] + ) + create_function_ddl = f""" CREATE OR REPLACE FUNCTION `{self._gcp_project_id}.{self._bq_dataset}`.{bq_function_name}({','.join(bq_function_args)}) RETURNS {bq_function_return_type} REMOTE WITH CONNECTION `{self._gcp_project_id}.{self._bq_location}.{self._bq_connection_id}` - OPTIONS ( - endpoint = "{endpoint}", - max_batching_rows = 1000 - )""" + OPTIONS ({remote_function_options_str})""" logger.info(f"Creating BQ remote function: {create_function_ddl}") @@ -438,6 +455,7 @@ def provision_bq_remote_function( reuse, name, package_requirements, + max_batching_rows, ): """Provision a BigQuery remote function.""" # If reuse of any existing function with the same name (indicated by the @@ -485,7 +503,12 @@ def provision_bq_remote_function( "Exactly one type should be provided for every input arg." ) self.create_bq_remote_function( - input_args, input_types, output_type, cf_endpoint, remote_function_name + input_args, + input_types, + output_type, + cf_endpoint, + remote_function_name, + max_batching_rows, ) else: logger.info(f"Remote function {remote_function_name} already exists.") @@ -607,6 +630,7 @@ def remote_function( cloud_function_service_account: Optional[str] = None, cloud_function_kms_key_name: Optional[str] = None, cloud_function_docker_repository: Optional[str] = None, + max_batching_rows: Optional[int] = 1000, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -723,6 +747,15 @@ def remote_function( projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. + max_batching_rows (int, Optional): + The maximum number of rows to be batched for processing in the + BQ remote function. Default value is 1000. A lower number can be + passed to avoid timeouts in case the user code is too complex to + process large number of rows fast enough. A higher number can be + used to increase throughput in case the user code is fast enough. + `None` can be passed to let BQ remote functions service apply + default batching. See for more details + https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. """ import bigframes.pandas as bpd @@ -846,6 +879,7 @@ def wrapper(f): reuse, name, packages, + max_batching_rows, ) # TODO: Move ibis logic to compiler step diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 91c3eb603ba..96af6ab1b3f 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -643,6 +643,7 @@ def remote_function( cloud_function_service_account: Optional[str] = None, cloud_function_kms_key_name: Optional[str] = None, cloud_function_docker_repository: Optional[str] = None, + max_batching_rows: Optional[int] = 1000, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -656,6 +657,7 @@ def remote_function( cloud_function_service_account=cloud_function_service_account, cloud_function_kms_key_name=cloud_function_kms_key_name, cloud_function_docker_repository=cloud_function_docker_repository, + max_batching_rows=max_batching_rows, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index b6d56006be0..64bcebb6cc3 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1541,6 +1541,7 @@ def remote_function( cloud_function_service_account: Optional[str] = None, cloud_function_kms_key_name: Optional[str] = None, cloud_function_docker_repository: Optional[str] = None, + max_batching_rows: Optional[int] = 1000, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1635,6 +1636,15 @@ def remote_function( projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. + max_batching_rows (int, Optional): + The maximum number of rows to be batched for processing in the + BQ remote function. Default value is 1000. A lower number can be + passed to avoid timeouts in case the user code is too complex to + process large number of rows fast enough. A higher number can be + used to increase throughput in case the user code is fast enough. + `None` can be passed to let BQ remote functions service apply + default batching. See for more details + https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1656,6 +1666,7 @@ def remote_function( cloud_function_service_account=cloud_function_service_account, cloud_function_kms_key_name=cloud_function_kms_key_name, cloud_function_docker_repository=cloud_function_docker_repository, + max_batching_rows=max_batching_rows, ) def read_gbq_function( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index cf6b2a01f80..ec9acc292ea 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1300,3 +1300,39 @@ def square_num(x): cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, square_num ) + + +@pytest.mark.parametrize( + ("max_batching_rows"), + [ + 10_000, + None, + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_max_batching_rows(session, scalars_dfs, max_batching_rows): + try: + + def square(x): + return x * x + + square_remote = session.remote_function( + [int], int, reuse=False, max_batching_rows=max_batching_rows + )(square) + + bq_routine = session.bqclient.get_routine( + square_remote.bigframes_remote_function + ) + assert bq_routine.remote_function_options.max_batching_rows == max_batching_rows + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["int64_too"].apply(square_remote).to_pandas() + pd_result = scalars_pandas_df["int64_too"].apply(square) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square_remote + ) From b66e3e6b221ea18d944ac478330bb009fe1a2c93 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 22 Apr 2024 18:03:25 +0000 Subject: [PATCH 510/921] chore: add synthetic data generation notebook (#615) * chore: add synthetic data generation notebook * markdown improvements * add copyright header * add a title --- .../apps/synthetic_data_generation.ipynb | 1133 +++++++++++++++++ noxfile.py | 4 + 2 files changed, 1137 insertions(+) create mode 100644 notebooks/apps/synthetic_data_generation.ipynb diff --git a/notebooks/apps/synthetic_data_generation.ipynb b/notebooks/apps/synthetic_data_generation.ipynb new file mode 100644 index 00000000000..a6e8444aaca --- /dev/null +++ b/notebooks/apps/synthetic_data_generation.ipynb @@ -0,0 +1,1133 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BigQuery DataFrames: Synthetic Data Generation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In addition to BigQuery DataFrames (installing which also installs `pandas` as a dependency) we will use\n", + "`faker` library as a building block for synthetic data generation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "suoG7eWDZARj", + "outputId": "b5c620a9-8f5b-413f-dd38-93448f941846" + }, + "outputs": [ + { + "name": "stdout", + "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" + ] + } + ], + "source": [ + "!pip install faker" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "m3q1oeJALhsG" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "bpd.options.bigquery.project = PROJECT_ID" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's use `GeminiTextGenerator` for our purpose, which is BigQuery DataFrame's state-of-the-art LLM integration at the time of writing this notebook (Apr 16 2024)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 69 + }, + "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" + } + ], + "source": [ + "from bigframes.ml.llm import GeminiTextGenerator\n", + "\n", + "model = GeminiTextGenerator()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Craft a prompt for the LLM to indicate the schema of the desired data and hints for the code that could generate such data. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 162 + }, + "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" + } + ], + "source": [ + "prompt = \"\"\"\\\n", + "Write python code to generate a pandas dataframe based on the requirements:\n", + " Column name: Name, type: string, Description: Latin American Names\n", + " Column name: Age, type: int\n", + " Column name: Gender, type: string, Description: Inclusive\n", + "\n", + "Note:\n", + " - Return the code only, no additional texts or comments\n", + " - Use faker library\n", + " - Generate 100 rows\n", + " - The final dataframe should be named 'result_df'.\n", + "\"\"\"\n", + "\n", + "df_prompt = bpd.DataFrame({\"prompt\" : [prompt]})\n", + "df_prompt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Be accommodating that LLM may not produce a runnable code in the first go and may need some nudging. We will retry by adding the failing code and the exception it throws as additional context in the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 277 + }, + "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" + ] + } + ], + "source": [ + "max_tries = 5\n", + "for i in range(max_tries):\n", + " # Get LLM generated code\n", + " df_result = model.predict(df_prompt)\n", + " llm_result = df_result['ml_generate_text_llm_result'].iloc[0]\n", + "\n", + " # Python code comes back as a markdown code block,\n", + " # remove the prefix \"```python\" and suffix \"```\"\n", + " code = llm_result[9:-3]\n", + " print(code)\n", + "\n", + " # Check if the generated code is runnable\n", + " try:\n", + " exec(code)\n", + " break\n", + " except Exception as ex:\n", + " print(ex)\n", + " error_context = f\"\"\"\n", + "Previous code:\n", + "{code}\n", + "\n", + "Had this exception:\n", + "{ex}\"\"\"\n", + "\n", + " # Update the prompt to help LLM correct error\n", + " df_prompt[\"prompt\"] += error_context\n", + "\n", + " # If we have exhausted max tries then stop trying\n", + " if i+1 == max_tries:\n", + " raise Exception(\"Failed to generate runnable code\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the generated code and verify that it produced the desired data." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 424 + }, + "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" + } + ], + "source": [ + "execution_context = {}\n", + "exec(code, execution_context)\n", + "execution_context.get(\"result_df\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to run this code at scale to generate since we want to generate large amount of data. Let's deploy a `remote_function` for this purpose." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 36 + }, + "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" + } + ], + "source": [ + "@bpd.remote_function([int], str, packages=['faker', 'pandas'])\n", + "def data_generator(id):\n", + " context = {}\n", + " exec(code, context)\n", + " result_df = context.get(\"result_df\")\n", + " return result_df.to_json(orient=\"records\")\n", + "\n", + "data_generator.bigframes_cloud_function" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let’s say we want to generate 1 million rows of synthetic data. Since our generated code produces 100 rows in one run, we can initialize an indicator dataframe with 1M/100 = 10K indicator rows. Then we can apply the remote function to produce 100 synthetic data rows for each indicator row." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "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" + } + ], + "source": [ + "desired_num_rows = 1_000_000 # 1 million rows\n", + "batch_size = 100 # used in the prompt\n", + "num_batches = int(desired_num_rows/batch_size)\n", + "\n", + "df = bpd.DataFrame({\"row_id\": range(num_batches)})" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "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" + } + ], + "source": [ + "df[\"json_data\"] = df[\"row_id\"].apply(data_generator)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this point each item in `df[\"json_data\"]` is a json serialized array of 100 records. Let’s flatten that into 1 record per row using a direct SQL." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 932 + }, + "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" + } + ], + "source": [ + "sql = f\"\"\"\n", + "WITH T0 AS ({df.sql}),\n", + "T1 AS (\n", + " SELECT PARSE_JSON(json_row) AS json_row\n", + " FROM T0, UNNEST(JSON_EXTRACT_ARRAY(json_data)) AS json_row\n", + ")\n", + "SELECT STRING(json_row.Name) AS Name,\n", + " INT64(json_row.Age) AS Age,\n", + " STRING(json_row.Gender) AS Gender\n", + "FROM T1\n", + "\"\"\"\n", + "df_result = bpd.read_gbq(sql)\n", + "df_result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There you have it, 1 million synthetic data rows ready to use, or save them in a BigQuery table for future use." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/noxfile.py b/noxfile.py index 9479a7a318c..91ad6bc0e6e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -723,6 +723,10 @@ def notebook(session: nox.Session): # The experimental notebooks imagine features that don't yet # exist or only exist as temporary prototypes. "notebooks/experimental/longer_ml_demo.ipynb", + # The notebooks that are added for more use cases, such as backing a + # blog post, which may take longer to execute and need not be + # continuously tested. + "notebooks/apps/synthetic_data_generation.ipynb", ] # Convert each Path notebook object to a string using a list comprehension. From d924ec2937c158644b5d1bbae4f82476de2c1655 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:20:14 -0700 Subject: [PATCH 511/921] feat: add `Series.struct.dtypes` property (#599) --- bigframes/operations/structs.py | 12 ++++++++ .../pandas/core/arrays/arrow/accessors.py | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/bigframes/operations/structs.py b/bigframes/operations/structs.py index e8a1af96026..d222f0993b6 100644 --- a/bigframes/operations/structs.py +++ b/bigframes/operations/structs.py @@ -15,9 +15,11 @@ from __future__ import annotations import bigframes_vendored.pandas.core.arrays.arrow.accessors as vendoracessors +import pandas as pd from bigframes.core import log_adapter import bigframes.dataframe +import bigframes.dtypes import bigframes.operations import bigframes.operations.base import bigframes.series @@ -45,3 +47,13 @@ def explode(self) -> bigframes.dataframe.DataFrame: return bigframes.pandas.concat( [self.field(i) for i in range(pa_type.num_fields)], axis="columns" ) + + def dtypes(self) -> pd.Series: + pa_type = self._dtype.pyarrow_dtype + return pd.Series( + data=[ + bigframes.dtypes.arrow_dtype_to_bigframes_dtype(pa_type.field(i).type) + for i in range(pa_type.num_fields) + ], + index=[pa_type.field(i).name for i in range(pa_type.num_fields)], + ) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py index 8e3ea06a3d4..bd6e50d0961 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py @@ -92,3 +92,32 @@ def explode(self): The data corresponding to all child fields. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def dtypes(self): + """ + Return the dtype object of each child field of the struct. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pyarrow as pa + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... [ + ... {"version": 1, "project": "pandas"}, + ... {"version": 2, "project": "pandas"}, + ... {"version": 1, "project": "numpy"}, + ... ], + ... dtype=bpd.ArrowDtype(pa.struct( + ... [("version", pa.int64()), ("project", pa.string())] + ... )) + ... ) + >>> s.struct.dtypes() + version Int64 + project string[pyarrow] + dtype: object + + Returns: + A *pandas* Series with the data type of all child fields. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 70015b79e8cff16ff1b36c5e3f019fe099750a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 22 Apr 2024 15:05:33 -0500 Subject: [PATCH 512/921] docs: set `index_cols` in `read_gbq` as a best practice (#624) --- .../bigframes_vendored/pandas/io/gbq.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index b5feeb13c5c..c60a2763380 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -27,13 +27,17 @@ def read_gbq( ): """Loads a DataFrame from BigQuery. - BigQuery tables are an unordered, unindexed data source. By default, - the DataFrame will have an arbitrary index and ordering. - - Set the `index_col` argument to one or more columns to choose an - index. The resulting DataFrame is sorted by the index columns. For the - best performance, ensure the index columns don't contain duplicate - values. + BigQuery tables are an unordered, unindexed data source. To add support + pandas-compatibility, the following indexing options are supported: + + * (Default behavior) Add an arbitrary sequential index and ordering + using an an analytic windowed operation that prevents filtering + push down. + * (Recommended) Set the ``index_col`` argument to one or more columns. + Unique values for the row labels are recommended. Duplicate labels + are possible, but note that joins on a non-unique index can duplicate + rows and operations like ``cumsum()`` that window across a non-unique + index can have some non-deternimism. .. note:: By default, even SQL query inputs with an ORDER BY clause create a From 75bb2409532e80de742030d05ffcbacacf5ffba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 22 Apr 2024 17:00:36 -0500 Subject: [PATCH 513/921] feat: support primary key(s) in `read_gbq` by using as the `index_col` by default (#625) * feat: support primary key(s) in `read_gbq` by using as the `index_col` by default * revert WIP commit * address type error in tests --- bigframes/session/__init__.py | 25 ++++++------ setup.py | 2 +- testing/constraints-3.9.txt | 2 +- tests/system/small/test_session.py | 13 +++---- tests/unit/resources.py | 7 ++-- tests/unit/session/test_session.py | 39 +++++++++++++++++++ .../bigframes_vendored/pandas/io/gbq.py | 3 ++ 7 files changed, 68 insertions(+), 23 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 64bcebb6cc3..f3f1ffce16f 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -708,13 +708,15 @@ def _get_snapshot_sql_and_primary_key( f"Current session is in {self._location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" ) - # TODO(b/305264153): Use public properties to fetch primary keys once - # added to google-cloud-bigquery. - primary_keys = ( - table._properties.get("tableConstraints", {}) - .get("primaryKey", {}) - .get("columns") - ) + primary_keys = None + if ( + (table_constraints := getattr(table, "table_constraints", None)) is not None + and (primary_key := table_constraints.primary_key) is not None + # This will be False for either None or empty list. + # We want primary_keys = None if no primary keys are set. + and (columns := primary_key.columns) + ): + primary_keys = columns job_config = bigquery.QueryJobConfig() job_config.labels["bigframes-api"] = api_name @@ -777,12 +779,13 @@ def _read_gbq_table( query, default_project=self.bqclient.project ) - ( - table_expression, - total_ordering_cols, - ) = self._get_snapshot_sql_and_primary_key( + (table_expression, primary_keys,) = self._get_snapshot_sql_and_primary_key( table_ref, api_name=api_name, use_cache=use_cache ) + total_ordering_cols = primary_keys + + if not index_col and primary_keys is not None: + index_col = primary_keys for key in columns: if key not in table_expression.columns: diff --git a/setup.py b/setup.py index 83049f97152..2ccf63259cd 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ "gcsfs >=2023.3.0", "geopandas >=0.12.2", "google-auth >=2.15.0,<3.0dev", - "google-cloud-bigquery[bqstorage,pandas] >=3.10.0", + "google-cloud-bigquery[bqstorage,pandas] >=3.16.0", "google-cloud-functions >=1.12.0", "google-cloud-bigquery-connection >=1.12.0", "google-cloud-iam >=2.12.1", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 1e1f3a3e665..f5007ed564f 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -4,7 +4,7 @@ fsspec==2023.3.0 gcsfs==2023.3.0 geopandas==0.12.2 google-auth==2.15.0 -google-cloud-bigquery==3.10.0 +google-cloud-bigquery==3.16.0 google-cloud-functions==1.12.0 google-cloud-bigquery-connection==1.12.0 google-cloud-iam==2.12.1 diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index ce415f93242..1e76a8bd8b9 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -236,14 +236,13 @@ def test_read_gbq_w_anonymous_query_results_table(session: bigframes.Session): def test_read_gbq_w_primary_keys_table( session: bigframes.Session, usa_names_grouped_table: bigquery.Table ): + # Validate that the table we're querying has a primary key. table = usa_names_grouped_table - # TODO(b/305264153): Use public properties to fetch primary keys once - # added to google-cloud-bigquery. - primary_keys = ( - table._properties.get("tableConstraints", {}) - .get("primaryKey", {}) - .get("columns") - ) + table_constraints = table.table_constraints + assert table_constraints is not None + primary_key = table_constraints.primary_key + assert primary_key is not None + primary_keys = primary_key.columns assert len(primary_keys) != 0 df = session.read_gbq(f"{table.project}.{table.dataset_id}.{table.table_id}") diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 6846659930a..28b08e49dc2 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -13,7 +13,7 @@ # limitations under the License. import datetime -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Sequence import unittest.mock as mock import google.auth.credentials @@ -37,6 +37,7 @@ def create_bigquery_session( bqclient: Optional[mock.Mock] = None, session_id: str = "abcxyz", + table_schema: Sequence[google.cloud.bigquery.SchemaField] = TEST_SCHEMA, anonymous_dataset: Optional[google.cloud.bigquery.DatasetReference] = None, ) -> bigframes.Session: credentials = mock.create_autospec( @@ -51,7 +52,7 @@ def create_bigquery_session( table = mock.create_autospec(google.cloud.bigquery.Table, instance=True) table._properties = {} type(table).location = mock.PropertyMock(return_value="test-region") - type(table).schema = mock.PropertyMock(return_value=TEST_SCHEMA) + type(table).schema = mock.PropertyMock(return_value=table_schema) bqclient.get_table.return_value = table if anonymous_dataset is None: @@ -72,7 +73,7 @@ def query_mock(query, *args, **kwargs): if query.startswith("SELECT CURRENT_TIMESTAMP()"): query_job.result = mock.MagicMock(return_value=[[datetime.datetime.now()]]) else: - type(query_job).schema = mock.PropertyMock(return_value=TEST_SCHEMA) + type(query_job).schema = mock.PropertyMock(return_value=table_schema) return query_job diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 3e2b28c2004..543196066a3 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -19,9 +19,11 @@ import google.api_core.exceptions import google.cloud.bigquery +import google.cloud.bigquery.table import pytest import bigframes +import bigframes.exceptions from .. import resources @@ -50,6 +52,43 @@ def test_read_gbq_cached_table(): assert "1999-01-02T03:04:05.678901" in df.sql +def test_read_gbq_clustered_table_ok_default_index_with_primary_key(): + """If a primary key is set on the table, we use that as the index column + by default, no error should be raised in this case. + + See internal issue 335727141. + """ + table = google.cloud.bigquery.Table("my-project.my_dataset.my_table") + table.clustering_fields = ["col1", "col2"] + table.schema = ( + google.cloud.bigquery.SchemaField("pk_1", "INT64"), + google.cloud.bigquery.SchemaField("pk_2", "INT64"), + google.cloud.bigquery.SchemaField("col_1", "INT64"), + google.cloud.bigquery.SchemaField("col_2", "INT64"), + ) + + # TODO(b/305264153): use setter for table_constraints in client library + # when available. + table._properties["tableConstraints"] = { + "primaryKey": { + "columns": ["pk_1", "pk_2"], + }, + } + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + bqclient.get_table.return_value = table + session = resources.create_bigquery_session( + bqclient=bqclient, table_schema=table.schema + ) + table._properties["location"] = session._location + + df = session.read_gbq("my-project.my_dataset.my_table") + + # There should be no analytic operators to prevent row filtering pushdown. + assert "OVER" not in df.sql + assert tuple(df.index.names) == ("pk_1", "pk_2") + + @pytest.mark.parametrize( "not_found_table_id", [("unknown.dataset.table"), ("project.unknown.table"), ("project.dataset.unknown")], diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index c60a2763380..93cee71289c 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -109,6 +109,9 @@ def read_gbq( In tha case, will read all the matched table as one DataFrame. index_col (Iterable[str] or str): Name of result column(s) to use for index in results DataFrame. + + **New in bigframes version 1.3.0**: If ``index_cols`` is not + set, the primary key(s) of the table are used as the index. columns (Iterable[str]): List of BigQuery column names in the desired order for results DataFrame. From 7227a6af37a3c0553db2d9a5a6d86c1e37d33b21 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:16:17 +0000 Subject: [PATCH 514/921] chore(main): release 1.3.0 (#617) :robot: I have created a release *beep* *boop* --- ## [1.3.0](https://togithub.com/googleapis/python-bigquery-dataframes/compare/v1.2.0...v1.3.0) (2024-04-22) ### Features * Add `Series.struct.dtypes` property ([#599](https://togithub.com/googleapis/python-bigquery-dataframes/issues/599)) ([d924ec2](https://togithub.com/googleapis/python-bigquery-dataframes/commit/d924ec2937c158644b5d1bbae4f82476de2c1655)) * Add fine tuning `fit()` for Palm2TextGenerator ([#616](https://togithub.com/googleapis/python-bigquery-dataframes/issues/616)) ([9c106bd](https://togithub.com/googleapis/python-bigquery-dataframes/commit/9c106bd24482620ef5ff3c85f94be9da76c49716)) * Add quantile statistic ([#613](https://togithub.com/googleapis/python-bigquery-dataframes/issues/613)) ([bc82804](https://togithub.com/googleapis/python-bigquery-dataframes/commit/bc82804da43c03c2311cd56f47a2316d3aae93d2)) * Expose `max_batching_rows` in `remote_function` ([#622](https://togithub.com/googleapis/python-bigquery-dataframes/issues/622)) ([240a1ac](https://togithub.com/googleapis/python-bigquery-dataframes/commit/240a1ac6fa914550bb6216cd5d179a36009f2657)) * Support primary key(s) in `read_gbq` by using as the `index_col` by default ([#625](https://togithub.com/googleapis/python-bigquery-dataframes/issues/625)) ([75bb240](https://togithub.com/googleapis/python-bigquery-dataframes/commit/75bb2409532e80de742030d05ffcbacacf5ffba2)) * Warn if location is set to unknown location ([#609](https://togithub.com/googleapis/python-bigquery-dataframes/issues/609)) ([3706b4f](https://togithub.com/googleapis/python-bigquery-dataframes/commit/3706b4f9dde65788b5e6343a6428fb1866499461)) ### Bug Fixes * Address technical writers fb ([#611](https://togithub.com/googleapis/python-bigquery-dataframes/issues/611)) ([9f8f181](https://togithub.com/googleapis/python-bigquery-dataframes/commit/9f8f181279133abdb7da3aa045df6fa278587013)) * Infer narrowest numeric type when combining numeric columns ([#602](https://togithub.com/googleapis/python-bigquery-dataframes/issues/602)) ([8f9ece6](https://togithub.com/googleapis/python-bigquery-dataframes/commit/8f9ece6d13f57f02d677bf0e3fea97dea94ae240)) * Use exact median implementation by default ([#619](https://togithub.com/googleapis/python-bigquery-dataframes/issues/619)) ([9d205ae](https://togithub.com/googleapis/python-bigquery-dataframes/commit/9d205aecb77f35baeec82a8f6e1b72c2d852ca46)) ### Documentation * Fix rendering of examples for multiple apis ([#620](https://togithub.com/googleapis/python-bigquery-dataframes/issues/620)) ([9665e39](https://togithub.com/googleapis/python-bigquery-dataframes/commit/9665e39ef288841f03a9d823bd2210ef58394ad3)) * Set `index_cols` in `read_gbq` as a best practice ([#624](https://togithub.com/googleapis/python-bigquery-dataframes/issues/624)) ([70015b7](https://togithub.com/googleapis/python-bigquery-dataframes/commit/70015b79e8cff16ff1b36c5e3f019fe099750a9d)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 25 +++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3314c976e4..a96c9028350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.3.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.2.0...v1.3.0) (2024-04-22) + + +### Features + +* Add `Series.struct.dtypes` property ([#599](https://github.com/googleapis/python-bigquery-dataframes/issues/599)) ([d924ec2](https://github.com/googleapis/python-bigquery-dataframes/commit/d924ec2937c158644b5d1bbae4f82476de2c1655)) +* Add fine tuning `fit()` for Palm2TextGenerator ([#616](https://github.com/googleapis/python-bigquery-dataframes/issues/616)) ([9c106bd](https://github.com/googleapis/python-bigquery-dataframes/commit/9c106bd24482620ef5ff3c85f94be9da76c49716)) +* Add quantile statistic ([#613](https://github.com/googleapis/python-bigquery-dataframes/issues/613)) ([bc82804](https://github.com/googleapis/python-bigquery-dataframes/commit/bc82804da43c03c2311cd56f47a2316d3aae93d2)) +* Expose `max_batching_rows` in `remote_function` ([#622](https://github.com/googleapis/python-bigquery-dataframes/issues/622)) ([240a1ac](https://github.com/googleapis/python-bigquery-dataframes/commit/240a1ac6fa914550bb6216cd5d179a36009f2657)) +* Support primary key(s) in `read_gbq` by using as the `index_col` by default ([#625](https://github.com/googleapis/python-bigquery-dataframes/issues/625)) ([75bb240](https://github.com/googleapis/python-bigquery-dataframes/commit/75bb2409532e80de742030d05ffcbacacf5ffba2)) +* Warn if location is set to unknown location ([#609](https://github.com/googleapis/python-bigquery-dataframes/issues/609)) ([3706b4f](https://github.com/googleapis/python-bigquery-dataframes/commit/3706b4f9dde65788b5e6343a6428fb1866499461)) + + +### Bug Fixes + +* Address technical writers fb ([#611](https://github.com/googleapis/python-bigquery-dataframes/issues/611)) ([9f8f181](https://github.com/googleapis/python-bigquery-dataframes/commit/9f8f181279133abdb7da3aa045df6fa278587013)) +* Infer narrowest numeric type when combining numeric columns ([#602](https://github.com/googleapis/python-bigquery-dataframes/issues/602)) ([8f9ece6](https://github.com/googleapis/python-bigquery-dataframes/commit/8f9ece6d13f57f02d677bf0e3fea97dea94ae240)) +* Use exact median implementation by default ([#619](https://github.com/googleapis/python-bigquery-dataframes/issues/619)) ([9d205ae](https://github.com/googleapis/python-bigquery-dataframes/commit/9d205aecb77f35baeec82a8f6e1b72c2d852ca46)) + + +### Documentation + +* Fix rendering of examples for multiple apis ([#620](https://github.com/googleapis/python-bigquery-dataframes/issues/620)) ([9665e39](https://github.com/googleapis/python-bigquery-dataframes/commit/9665e39ef288841f03a9d823bd2210ef58394ad3)) +* Set `index_cols` in `read_gbq` as a best practice ([#624](https://github.com/googleapis/python-bigquery-dataframes/issues/624)) ([70015b7](https://github.com/googleapis/python-bigquery-dataframes/commit/70015b79e8cff16ff1b36c5e3f019fe099750a9d)) + ## [1.2.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.1.0...v1.2.0) (2024-04-15) diff --git a/bigframes/version.py b/bigframes/version.py index ec2105b6485..1f103401e4d 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.2.0" +__version__ = "1.3.0" From 518d315487f351c227070c0127382d11381c5e88 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 22 Apr 2024 17:15:33 -0700 Subject: [PATCH 515/921] feat: Series binary ops compatible with more types (#618) --- bigframes/core/__init__.py | 12 -- bigframes/core/compile/scalar_op_compiler.py | 2 +- bigframes/core/convert.py | 29 ++++- bigframes/dataframe.py | 7 +- bigframes/operations/base.py | 128 ++++++++++++------- bigframes/series.py | 9 -- tests/system/small/test_series.py | 112 ++++++++++++---- 7 files changed, 200 insertions(+), 99 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 9e6b86fc303..04291edbb17 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -117,18 +117,6 @@ def _compiled_schema(self) -> schemata.ArraySchema: ) return schemata.ArraySchema(items) - def validate_schema(self): - tree_derived = self.node.schema - ibis_derived = self._compiled_schema - if tree_derived.names != ibis_derived.names: - raise ValueError( - f"Unexpected names internal {tree_derived.names} vs compiled {ibis_derived.names}" - ) - if tree_derived.dtypes != ibis_derived.dtypes: - raise ValueError( - f"Unexpected types internal {tree_derived.dtypes} vs compiled {ibis_derived.dtypes}" - ) - def _try_evaluate_local(self): """Use only for unit testing paths - not fully featured. Will throw exception if fails.""" import ibis diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 072d974b390..a65ff6fe0c2 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -1366,7 +1366,7 @@ def clip_op( @scalar_op_compiler.register_nary_op(ops.case_when_op) -def switch_op(*cases_and_outputs: ibis_types.Value) -> ibis_types.Value: +def case_when_op(*cases_and_outputs: ibis_types.Value) -> ibis_types.Value: # ibis can handle most type coercions, but we need to force bool -> int # TODO: dispatch coercion depending on bigframes dtype schema result_values = cases_and_outputs[1::2] diff --git a/bigframes/core/convert.py b/bigframes/core/convert.py index 1ef329b0c78..7bfca827798 100644 --- a/bigframes/core/convert.py +++ b/bigframes/core/convert.py @@ -21,7 +21,22 @@ import bigframes.series as series -def to_bf_series(obj, default_index: Optional[index.Index]) -> series.Series: +def is_series_convertible(obj) -> bool: + if isinstance(obj, series.Series): + return True + if isinstance(obj, pd.Series): + return True + if isinstance(obj, index.Index): + return True + if isinstance(obj, pd.Index): + return True + if pd.api.types.is_list_like(obj): + return True + else: + return False + + +def to_bf_series(obj, default_index: Optional[index.Index], session) -> series.Series: """ Convert a an object to a bigframes series @@ -37,13 +52,15 @@ def to_bf_series(obj, default_index: Optional[index.Index]) -> series.Series: if isinstance(obj, series.Series): return obj if isinstance(obj, pd.Series): - return series.Series(obj) + return series.Series(obj, session=session) if isinstance(obj, index.Index): - return series.Series(obj, default_index) + return series.Series(obj, default_index, session=session) if isinstance(obj, pd.Index): - return series.Series(obj, default_index) + return series.Series(obj, default_index, session=session) + if pd.api.types.is_dict_like(obj): + return series.Series(obj, session=session) if pd.api.types.is_list_like(obj): - return series.Series(obj, default_index) + return series.Series(obj, default_index, session=session) else: raise TypeError(f"Cannot interpret {obj} as series.") @@ -69,6 +86,8 @@ def to_pd_series(obj, default_index: pd.Index) -> pd.Series: return pd.Series(obj.to_pandas(), default_index) if isinstance(obj, pd.Index): return pd.Series(obj, default_index) + if pd.api.types.is_dict_like(obj): + return pd.Series(obj) if pd.api.types.is_list_like(obj): return pd.Series(obj, default_index) else: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ff8404761ce..4f9ee44f099 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -105,6 +105,9 @@ def __init__( raise ValueError( f"DataFrame constructor only supports copy=True. {constants.FEEDBACK_LINK}" ) + # just ignore object dtype if provided + if dtype in {numpy.dtypes.ObjectDType, "object"}: + dtype = None # Check to see if constructing from BigQuery-backed objects before # falling back to pandas constructor @@ -668,7 +671,9 @@ def _apply_binop( DataFrame(other), op, how=how, reverse=reverse ) elif utils.get_axis_number(axis) == 0: - bf_series = bigframes.core.convert.to_bf_series(other, self.index) + bf_series = bigframes.core.convert.to_bf_series( + other, self.index, self._session + ) return self._apply_series_binop_axis_0(bf_series, op, how, reverse) elif utils.get_axis_number(axis) == 1: pd_series = bigframes.core.convert.to_pd_series(other, self.columns) diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 9bfa0500b5f..b003ce59cc2 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -17,10 +17,12 @@ import typing import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing +import numpy import pandas as pd import bigframes.constants as constants import bigframes.core.blocks as blocks +import bigframes.core.convert import bigframes.core.expression as ex import bigframes.core.indexes as indexes import bigframes.core.scalar as scalars @@ -44,7 +46,19 @@ def __init__( *, session: typing.Optional[bigframes.session.Session] = None, ): - block = None + import bigframes.pandas + + # just ignore object dtype if provided + if dtype in {numpy.dtypes.ObjectDType, "object"}: + dtype = None + + read_pandas_func = ( + session.read_pandas + if (session is not None) + else (lambda x: bigframes.pandas.read_pandas(x)) + ) + + block: typing.Optional[blocks.Block] = None if copy is not None and not copy: raise ValueError( f"Series constructor only supports copy=True. {constants.FEEDBACK_LINK}" @@ -55,58 +69,75 @@ def __init__( assert index is None block = data - elif isinstance(data, SeriesMethods): - block = data._block + # interpret these cases as both index and data + elif ( + isinstance(data, SeriesMethods) + or isinstance(data, pd.Series) + or pd.api.types.is_dict_like(data) + ): + if isinstance(data, pd.Series): + data = read_pandas_func(data) + elif pd.api.types.is_dict_like(data): + data = read_pandas_func(pd.Series(data, dtype=dtype)) # type: ignore + dtype = None + data_block = data._block if index is not None: # reindex - bf_index = indexes.Index(index) + bf_index = indexes.Index(index, session=session) idx_block = bf_index._block idx_cols = idx_block.value_columns - block_idx, _ = idx_block.join(block, how="left") - block = block_idx.with_index_labels(bf_index.names) - - elif isinstance(data, indexes.Index): + block_idx, _ = idx_block.join(data_block, how="left") + data_block = block_idx.with_index_labels(bf_index.names) + block = data_block + + # list-like data that will get default index + elif isinstance(data, indexes.Index) or pd.api.types.is_list_like(data): + data = indexes.Index(data, dtype=dtype, session=session) + dtype = ( + None # set to none as it has already been applied, avoid re-cast later + ) if data.nlevels != 1: raise NotImplementedError("Cannot interpret multi-index as Series.") # Reset index to promote index columns to value columns, set default index - block = data._block.reset_index(drop=False) + data_block = data._block.reset_index(drop=False).with_column_labels( + data.names + ) if index is not None: # Align by offset - bf_index = indexes.Index(index) - idx_block = bf_index._block.reset_index(drop=False) + bf_index = indexes.Index(index, session=session) + idx_block = bf_index._block.reset_index( + drop=False + ) # reset to align by offsets, and then reset back idx_cols = idx_block.value_columns - block, (l_mapping, _) = idx_block.join(block, how="left") - block = block.set_index([l_mapping[col] for col in idx_cols]) - block = block.with_index_labels(bf_index.names) - - if block: - if name: - if not isinstance(name, typing.Hashable): - raise ValueError( - f"BigQuery DataFrames only supports hashable series names. {constants.FEEDBACK_LINK}" - ) - block = block.with_column_labels([name]) - if dtype: - block = block.multi_apply_unary_op( - block.value_columns, ops.AsTypeOp(to_type=dtype) - ) - else: - import bigframes.pandas + data_block, (l_mapping, _) = idx_block.join(data_block, how="left") + data_block = data_block.set_index([l_mapping[col] for col in idx_cols]) + data_block = data_block.with_index_labels(bf_index.names) + block = data_block - pd_series = pd.Series( - data=data, index=index, dtype=dtype, name=name # type:ignore - ) - pd_dataframe = pd_series.to_frame() - if pd_series.name is None: - # to_frame will set default numeric column label if unnamed, but we do not support int column label, so must rename - pd_dataframe = pd_dataframe.set_axis(["unnamed_col"], axis=1) - if session: - block = session.read_pandas(pd_dataframe)._get_block() + else: # Scalar case + if index is not None: + bf_index = indexes.Index(index, session=session) else: - # Uses default global session - block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() - if pd_series.name is None: - block = block.with_column_labels([None]) + bf_index = indexes.Index( + [] if (data is None) else [0], + session=session, + dtype=bigframes.dtypes.INT_DTYPE, + ) + block, _ = bf_index._block.create_constant(data, dtype) + dtype = None + block = block.with_column_labels([name]) + + assert block is not None + if name: + if not isinstance(name, typing.Hashable): + raise ValueError( + f"BigQuery DataFrames only supports hashable series names. {constants.FEEDBACK_LINK}" + ) + block = block.with_column_labels([name]) + if dtype: + block = block.multi_apply_unary_op( + block.value_columns, ops.AsTypeOp(to_type=dtype) + ) self._block: blocks.Block = block @property @@ -145,17 +176,16 @@ def _apply_binary_op( reverse: bool = False, ) -> series.Series: """Applies a binary operator to the series and other.""" - if isinstance(other, pd.Series): - # TODO: Convert to BigQuery DataFrames series - raise NotImplementedError( - f"Pandas series not supported as operand. {constants.FEEDBACK_LINK}" + if bigframes.core.convert.is_series_convertible(other): + self_index = indexes.Index(self._block) + other_series = bigframes.core.convert.to_bf_series( + other, self_index, self._block.session ) - if isinstance(other, series.Series): - (self_col, other_col, block) = self._align(other, how=alignment) + (self_col, other_col, block) = self._align(other_series, how=alignment) name = self._name if ( - isinstance(other, series.Series) + hasattr(other, "name") and other.name != self._name and alignment == "outer" ): @@ -166,7 +196,7 @@ def _apply_binary_op( block, result_id = block.project_expr(expr, name) return series.Series(block.select_column(result_id)) - else: + else: # Scalar binop name = self._name expr = op.as_expr( ex.const(other) if reverse else self._value_column, diff --git a/bigframes/series.py b/bigframes/series.py index 47acfd0afb6..5184d4bf1dd 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -20,7 +20,6 @@ import inspect import itertools import numbers -import os import textwrap import typing from typing import Any, cast, Literal, Mapping, Optional, Sequence, Tuple, Union @@ -73,11 +72,6 @@ def __init__(self, *args, **kwargs): self._query_job: Optional[bigquery.QueryJob] = None super().__init__(*args, **kwargs) - # Runs strict validations to ensure internal type predictions and ibis are completely in sync - # Do not execute these validations outside of testing suite. - if "PYTEST_CURRENT_TEST" in os.environ: - self._block.expr.validate_schema() - @property def dt(self) -> dt.DatetimeMethods: return dt.DatetimeMethods(self._block) @@ -812,9 +806,6 @@ def combine_first(self, other: Series) -> Series: return result def update(self, other: Union[Series, Sequence, Mapping]) -> None: - import bigframes.core.convert - - other = bigframes.core.convert.to_bf_series(other, default_index=None) result = self._apply_binary_op( other, ops.coalesce_op, reverse=True, alignment="left" ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 9cb615fdcbf..38aed19f05f 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -126,6 +126,75 @@ def test_series_construct_from_list(): pd.testing.assert_series_equal(bf_result, pd_result) +def test_series_construct_reindex(): + bf_result = series.Series( + series.Series({1: 10, 2: 30, 3: 30}), index=[3, 2], dtype="Int64" + ).to_pandas() + pd_result = pd.Series(pd.Series({1: 10, 2: 30, 3: 30}), index=[3, 2], dtype="Int64") + + # BigQuery DataFrame default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal(bf_result, pd_result) + + +def test_series_construct_from_list_w_index(): + bf_result = series.Series( + [1, 1, 2, 3, 5, 8, 13], index=[10, 20, 30, 40, 50, 60, 70], dtype="Int64" + ).to_pandas() + pd_result = pd.Series( + [1, 1, 2, 3, 5, 8, 13], index=[10, 20, 30, 40, 50, 60, 70], dtype="Int64" + ) + + # BigQuery DataFrame default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal(bf_result, pd_result) + + +def test_series_construct_empty(session: bigframes.Session): + bf_series: series.Series = series.Series(session=session) + pd_series: pd.Series = pd.Series() + + bf_result = bf_series.empty + pd_result = pd_series.empty + + assert pd_result + assert bf_result == pd_result + + +def test_series_construct_scalar_no_index(): + bf_result = series.Series("hello world", dtype="string[pyarrow]").to_pandas() + pd_result = pd.Series("hello world", dtype="string[pyarrow]") + + # BigQuery DataFrame default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal(bf_result, pd_result) + + +def test_series_construct_scalar_w_index(): + bf_result = series.Series( + "hello world", dtype="string[pyarrow]", index=[0, 2, 1] + ).to_pandas() + pd_result = pd.Series("hello world", dtype="string[pyarrow]", index=[0, 2, 1]) + + # BigQuery DataFrame default indices use nullable Int64 always + pd_result.index = pd_result.index.astype("Int64") + + pd.testing.assert_series_equal(bf_result, pd_result) + + +def test_series_construct_nan(): + bf_result = series.Series(numpy.nan).to_pandas() + pd_result = pd.Series(numpy.nan) + + pd_result.index = pd_result.index.astype("Int64") + pd_result = pd_result.astype("Float64") + + pd.testing.assert_series_equal(bf_result, pd_result) + + def test_series_construct_from_list_escaped_strings(): """Check that special characters are supported.""" strings = [ @@ -949,17 +1018,6 @@ def test_reset_index_no_drop(scalars_df_index, scalars_pandas_df_index, name): pd.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) -def test_series_add_pandas_series_not_implemented(scalars_dfs): - scalars_df, _ = scalars_dfs - with pytest.raises(NotImplementedError): - ( - scalars_df["float64_col"] - + pd.Series( - [1, 1, 1, 1], - ) - ).to_pandas() - - def test_copy(scalars_df_index, scalars_pandas_df_index): col_name = "float64_col" # Expect mutation on original not to effect_copy @@ -1269,6 +1327,27 @@ def test_binop_right_filtered(scalars_dfs): ) +@pytest.mark.parametrize( + ("other",), + [ + ([-1.4, 2.3, None],), + (pd.Index([-1.4, 2.3, None]),), + (pd.Series([-1.4, 2.3, None], index=[44, 2, 1]),), + ], +) +@skip_legacy_pandas +def test_series_binop_w_other_types(scalars_dfs, other): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = (scalars_df["int64_col"].head(3) + other).to_pandas() + pd_result = scalars_pandas_df["int64_col"].head(3) + other + + assert_series_equal( + bf_result, + pd_result, + ) + + @skip_legacy_pandas def test_series_combine_first(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs @@ -1771,17 +1850,6 @@ def test_empty_true_row_filter(scalars_dfs): assert pd_result == bf_result -def test_empty_true_memtable(session: bigframes.Session): - bf_series: series.Series = series.Series(session=session) - pd_series: pd.Series = pd.Series() - - bf_result = bf_series.empty - pd_result = pd_series.empty - - assert pd_result - assert bf_result == pd_result - - def test_series_names(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs From 054075d448f7de1b3bc1a4631b4e2340643de4ef Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 22 Apr 2024 18:20:09 -0700 Subject: [PATCH 516/921] feat: Add transpose support for small homogeneously typed DataFrames. (#621) --- bigframes/core/blocks.py | 51 ++++++++++++ bigframes/dataframe.py | 7 ++ tests/system/small/test_dataframe.py | 23 ++++++ .../bigframes_vendored/pandas/core/frame.py | 82 +++++++++++++++++++ 4 files changed, 163 insertions(+) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 0f9cacd83dc..2a888125f86 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -35,6 +35,7 @@ import pyarrow as pa import bigframes._config.sampling_options as sampling_options +import bigframes.constants import bigframes.constants as constants import bigframes.core as core import bigframes.core.expression as ex @@ -1542,6 +1543,10 @@ def melt( var_names=typing.Sequence[typing.Hashable], value_name: typing.Hashable = "value", ): + """ + Unpivot columns to produce longer, narrower dataframe. + Arguments correspond to pandas.melt arguments. + """ # TODO: Implement col_level and ignore_index unpivot_col_id = guid.generate_guid() var_col_ids = tuple([guid.generate_guid() for _ in var_names]) @@ -1570,6 +1575,52 @@ def melt( index_columns=[index_id], ) + def transpose(self) -> Block: + """Transpose the block. Will fail if dtypes aren't coercible to a common type or too many rows""" + original_col_index = self.column_labels + original_row_index = self.index.to_pandas() + original_row_count = len(original_row_index) + if original_row_count > bigframes.constants.MAX_COLUMNS: + raise NotImplementedError( + f"Object has {original_row_count} rows and is too large to transpose." + ) + + # Add row numbers to both axes to disambiguate, clean them up later + block = self + numbered_block = block.with_column_labels( + utils.combine_indices( + block.column_labels, pd.Index(range(len(block.column_labels))) + ) + ) + numbered_block, offsets = numbered_block.promote_offsets() + + stacked_block = numbered_block.melt( + id_vars=(offsets,), + var_names=( + *[name for name in original_col_index.names], + "col_offset", + ), + value_vars=block.value_columns, + ) + col_labels = stacked_block.value_columns[-2 - original_col_index.nlevels : -2] + col_offset = stacked_block.value_columns[-2] # disambiguator we created earlier + cell_values = stacked_block.value_columns[-1] + # Groupby source column + stacked_block = stacked_block.set_index( + [*col_labels, col_offset] + ) # col index is now row index + result = stacked_block.pivot( + columns=[offsets], + values=[cell_values], + columns_unique_values=tuple(range(original_row_count)), + ) + # Drop the offsets from both axes before returning + return ( + result.with_column_labels(original_row_index) + .order_by([ordering.ascending_over(result.index_columns[-1])]) + .drop_levels([result.index_columns[-1]]) + ) + def _create_stack_column( self, col_label: typing.Tuple, stack_labels: typing.Sequence[typing.Tuple] ): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 4f9ee44f099..a55b7f569b3 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -314,6 +314,13 @@ def bqclient(self) -> bigframes.Session: def _session(self) -> bigframes.Session: return self._get_block().expr.session + @property + def T(self) -> DataFrame: + return DataFrame(self._get_block().transpose()) + + def transpose(self) -> DataFrame: + return self.T + def __len__(self): rows, _ = self.shape return rows diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 4c598a682d8..f41a21add07 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2465,6 +2465,29 @@ def test_df_describe(scalars_dfs): ).all() +def test_df_transpose(): + # Include some floats to ensure type coercion + values = [[0, 3.5, True], [1, 4.5, False], [2, 6.5, None]] + # Test complex case of both axes being multi-indices with non-unique elements + columns = pd.Index(["A", "B", "A"], dtype=pd.StringDtype(storage="pyarrow")) + columns_multi = pd.MultiIndex.from_arrays([columns, columns], names=["c1", "c2"]) + index = pd.Index(["b", "a", "a"], dtype=pd.StringDtype(storage="pyarrow")) + rows_multi = pd.MultiIndex.from_arrays([index, index], names=["r1", "r2"]) + + pd_df = pandas.DataFrame(values, index=rows_multi, columns=columns_multi) + bf_df = dataframe.DataFrame(values, index=rows_multi, columns=columns_multi) + + pd_result = pd_df.T + bf_result = bf_df.T.to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + + +def test_df_transpose_error(): + with pytest.raises(TypeError, match="Cannot coerce.*to a common type."): + dataframe.DataFrame([[1, "hello"], [2, "world"]]).transpose() + + @pytest.mark.parametrize( ("ordered"), [ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 0515f690e35..1669a291c91 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -93,6 +93,88 @@ def values(self) -> np.ndarray: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + @property + def T(self) -> DataFrame: + """ + The transpose of the DataFrame. + + All columns must be the same dtype (numerics can be coerced to a common supertype). + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + >>> df + col1 col2 + 0 1 3 + 1 2 4 + + [2 rows x 2 columns] + + >>> df.T + 0 1 + col1 1 2 + col2 3 4 + + [2 rows x 2 columns] + + Returns: + DataFrame: The transposed DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transpose(self) -> DataFrame: + """ + Transpose index and columns. + + Reflect the DataFrame over its main diagonal by writing rows as columns + and vice-versa. The property :attr:`.T` is an accessor to the method + :meth:`transpose`. + + All columns must be the same dtype (numerics can be coerced to a common supertype). + + **Examples:** + + **Square DataFrame with homogeneous dtype** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> d1 = {'col1': [1, 2], 'col2': [3, 4]} + >>> df1 = bpd.DataFrame(data=d1) + >>> df1 + col1 col2 + 0 1 3 + 1 2 4 + + [2 rows x 2 columns] + + >>> df1_transposed = df1.T # or df1.transpose() + >>> df1_transposed + 0 1 + col1 1 2 + col2 3 4 + + [2 rows x 2 columns] + + When the dtype is homogeneous in the original DataFrame, we get a + transposed DataFrame with the same dtype: + + >>> df1.dtypes + col1 Int64 + col2 Int64 + dtype: object + >>> df1_transposed.dtypes + 0 Int64 + 1 Int64 + dtype: object + + Returns: + DataFrame: The transposed DataFrame. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def info( self, verbose: bool | None = None, From 4724a1a456076d003613d2e964a8dd2d80a09ad9 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:45:12 -0700 Subject: [PATCH 517/921] fix: llm fine tuning tests (#627) --- tests/system/load/test_llm.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index 62ef7d5c725..d56f6100c12 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -45,9 +45,12 @@ def llm_remote_text_pandas_df(): ) -def test_llm_palm_configure_fit( - llm_fine_tune_df_default_index, llm_remote_text_pandas_df -): +@pytest.fixture(scope="session") +def llm_remote_text_df(session, llm_remote_text_pandas_df): + return session.read_pandas(llm_remote_text_pandas_df) + + +def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): model = bigframes.ml.llm.PaLM2TextGenerator( model_name="text-bison", max_iterations=1 ) @@ -59,7 +62,7 @@ def test_llm_palm_configure_fit( assert model is not None - df = model.predict(llm_remote_text_pandas_df).to_pandas() + df = model.predict(llm_remote_text_df["prompt"]).to_pandas() assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] From 2b84c4f173e956ba2c7fcc0ad92785ae95161d8e Mon Sep 17 00:00:00 2001 From: Salem Jorden <115185670+SalemJorden@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:07:55 -0500 Subject: [PATCH 518/921] docs: add the first sample for the Single time-series forecasting from Google Analytics data tutorial (#623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BigQuery DataFrames sample for [Step two (optional): Visualize the time series you want to forecast](https://cloud.google.com/bigquery/docs/arima-single-time-series-forecasting-tutorial#step_two_optional_visualize_the_time_series_you_want_to_forecast). Co-authored-by: Salem Boyland Co-authored-by: Tim Sweña (Swast) --- ...ingle_timeseries_forecasting_model_test.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 samples/snippets/create_single_timeseries_forecasting_model_test.py diff --git a/samples/snippets/create_single_timeseries_forecasting_model_test.py b/samples/snippets/create_single_timeseries_forecasting_model_test.py new file mode 100644 index 00000000000..57509337136 --- /dev/null +++ b/samples/snippets/create_single_timeseries_forecasting_model_test.py @@ -0,0 +1,40 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (t +# you may not use this file except in compliance wi +# 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 +# distributed under the License is distributed on a +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, eit +# See the License for the specific language governi +# limitations under the License. + + +def test_create_single_timeseries(): + + # [START bigquery_dataframes_single_timeseries_forecasting_model_tutorial] + import bigframes.pandas as bpd + + # Start by loading the historical data from BigQuerythat you want to analyze and forecast. + # This clause indicates that you are querying the ga_sessions_* tables in the google_analytics_sample dataset. + # Read and visualize the time series you want to forecast. + df = bpd.read_gbq("bigquery-public-data.google_analytics_sample.ga_sessions_*") + parsed_date = bpd.to_datetime(df.date, format="%Y%m%d", utc=True) + visits = df["totals"].struct.field("visits") + total_visits = visits.groupby(parsed_date).sum() + + # Expected output: total_visits.head() + # date + # 2016-08-01 00:00:00+00:00 1711 + # 2016-08-02 00:00:00+00:00 2140 + # 2016-08-03 00:00:00+00:00 2890 + # 2016-08-04 00:00:00+00:00 3161 + # 2016-08-05 00:00:00+00:00 2702 + # Name: visits, dtype: Int64 + + total_visits.plot.line() + + # [END bigquery_dataframes_single_timeseries_forecasting_model_tutorial] From 8d2a51c4079844daba20f414b6c0c0ca030ba1f9 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 24 Apr 2024 07:45:46 -0700 Subject: [PATCH 519/921] docs: add supported pandas apis on the main page (#628) --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 64d1e4e72cf..70041c7c8e6 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,7 @@ Documentation * `BigQuery DataFrames source code (GitHub) `_ * `BigQuery DataFrames sample notebooks `_ * `BigQuery DataFrames API reference `_ +* `BigQuery DataFrames supported pandas APIs `_ Getting started with BigQuery DataFrames From 96c150a9de9a854872d47a2c4c545c77b79096e0 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 25 Apr 2024 02:12:43 +0000 Subject: [PATCH 520/921] test: test most relevant dtype for aggregates (#595) * fix: keep most relevant dtype for aggregates * add aggregate tests for bool result * refactor and reuse dtypes.lcd_dtype * check_dtype=False --- bigframes/dtypes.py | 16 ++-- tests/system/small/test_dataframe.py | 126 +++++++++++++++++++++++---- 2 files changed, 119 insertions(+), 23 deletions(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 3b2092bf852..d2dc210e0df 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -658,10 +658,14 @@ def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: return None -def lcd_type(dtype1: Dtype, dtype2: Dtype) -> Dtype: - """Get the supertype of the two types.""" - if dtype1 == dtype2: - return dtype1 +def lcd_type(*dtypes: Dtype) -> Dtype: + if len(dtypes) < 1: + raise ValueError("at least one dypes should be provided") + if len(dtypes) == 1: + return dtypes[0] + unique_dtypes = set(dtypes) + if len(unique_dtypes) == 1: + return unique_dtypes.pop() # Implicit conversion currently only supported for numeric types hierarchy: list[Dtype] = [ pd.BooleanDtype(), @@ -670,9 +674,9 @@ def lcd_type(dtype1: Dtype, dtype2: Dtype) -> Dtype: pd.ArrowDtype(pa.decimal256(76, 38)), pd.Float64Dtype(), ] - if (dtype1 not in hierarchy) or (dtype2 not in hierarchy): + if any([dtype not in hierarchy for dtype in dtypes]): return None - lcd_index = max(hierarchy.index(dtype1), hierarchy.index(dtype2)) + lcd_index = max([hierarchy.index(dtype) for dtype in dtypes]) return hierarchy[lcd_index] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index f41a21add07..3230ad2a89a 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2390,12 +2390,27 @@ def test_dataframe_pct_change(scalars_df_index, scalars_pandas_df_index, periods def test_dataframe_agg_single_string(scalars_dfs): numeric_cols = ["int64_col", "int64_too", "float64_col"] scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[numeric_cols].agg("sum").to_pandas() pd_result = scalars_pandas_df[numeric_cols].agg("sum") - # Pandas may produce narrower numeric types, but bigframes always produces Float64 - pd_result = pd_result.astype("Float64") - pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + assert bf_result.dtype == "Float64" + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + +def test_dataframe_agg_int_single_string(scalars_dfs): + numeric_cols = ["int64_col", "int64_too", "bool_col"] + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df[numeric_cols].agg("sum").to_pandas() + pd_result = scalars_pandas_df[numeric_cols].agg("sum") + + assert bf_result.dtype == "Int64" + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) def test_dataframe_agg_multi_string(scalars_dfs): @@ -2431,6 +2446,27 @@ def test_dataframe_agg_multi_string(scalars_dfs): ).all() +def test_dataframe_agg_int_multi_string(scalars_dfs): + numeric_cols = ["int64_col", "int64_too", "bool_col"] + aggregations = [ + "sum", + "nunique", + "count", + ] + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[numeric_cols].agg(aggregations).to_pandas() + pd_result = scalars_pandas_df[numeric_cols].agg(aggregations) + + for dtype in bf_result.dtypes: + assert dtype == "Int64" + + # Pandas may produce narrower numeric types + # Pandas has object index type + pd.testing.assert_frame_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + @skip_legacy_pandas def test_df_describe(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs @@ -2982,6 +3018,58 @@ def test_loc_setitem_bool_series_scalar_error(scalars_dfs): pd_df.loc[pd_df["int64_too"] == 1, "string_col"] = 99 +@pytest.mark.parametrize( + ("col", "op"), + [ + # Int aggregates + pytest.param("int64_col", lambda x: x.sum(), id="int-sum"), + pytest.param("int64_col", lambda x: x.min(), id="int-min"), + pytest.param("int64_col", lambda x: x.max(), id="int-max"), + pytest.param("int64_col", lambda x: x.count(), id="int-count"), + pytest.param("int64_col", lambda x: x.nunique(), id="int-nunique"), + # Float aggregates + pytest.param("float64_col", lambda x: x.count(), id="float-count"), + pytest.param("float64_col", lambda x: x.nunique(), id="float-nunique"), + # Bool aggregates + pytest.param("bool_col", lambda x: x.sum(), id="bool-sum"), + pytest.param("bool_col", lambda x: x.count(), id="bool-count"), + pytest.param("bool_col", lambda x: x.nunique(), id="bool-nunique"), + # String aggregates + pytest.param("string_col", lambda x: x.count(), id="string-count"), + pytest.param("string_col", lambda x: x.nunique(), id="string-nunique"), + ], +) +def test_dataframe_aggregate_int(scalars_df_index, scalars_pandas_df_index, col, op): + bf_result = op(scalars_df_index[[col]]).to_pandas() + pd_result = op(scalars_pandas_df_index[[col]]) + + # Check dtype separately + assert bf_result.dtype == "Int64" + + # Pandas may produce narrower numeric types + # Pandas has object index type + assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) + + +@pytest.mark.parametrize( + ("col", "op"), + [ + pytest.param("bool_col", lambda x: x.min(), id="bool-min"), + pytest.param("bool_col", lambda x: x.max(), id="bool-max"), + ], +) +def test_dataframe_aggregate_bool(scalars_df_index, scalars_pandas_df_index, col, op): + bf_result = op(scalars_df_index[[col]]).to_pandas() + pd_result = op(scalars_pandas_df_index[[col]]) + + # Check dtype separately + assert bf_result.dtype == "boolean" + + # Pandas may produce narrower numeric types + # Pandas has object index type + assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) + + @pytest.mark.parametrize( ("ordered"), [ @@ -2990,34 +3078,38 @@ def test_loc_setitem_bool_series_scalar_error(scalars_dfs): ], ) @pytest.mark.parametrize( - ("op"), + ("op", "bf_dtype"), [ - (lambda x: x.sum(numeric_only=True)), - (lambda x: x.mean(numeric_only=True)), - (lambda x: x.min(numeric_only=True)), - (lambda x: x.max(numeric_only=True)), - (lambda x: x.std(numeric_only=True)), - (lambda x: x.var(numeric_only=True)), - (lambda x: x.count(numeric_only=False)), - (lambda x: x.nunique()), + (lambda x: x.sum(numeric_only=True), "Float64"), + (lambda x: x.mean(numeric_only=True), "Float64"), + (lambda x: x.min(numeric_only=True), "Float64"), + (lambda x: x.max(numeric_only=True), "Float64"), + (lambda x: x.std(numeric_only=True), "Float64"), + (lambda x: x.var(numeric_only=True), "Float64"), + (lambda x: x.count(numeric_only=False), "Int64"), + (lambda x: x.nunique(), "Int64"), ], ids=["sum", "mean", "min", "max", "std", "var", "count", "nunique"], ) -def test_dataframe_aggregates(scalars_df_index, scalars_pandas_df_index, op, ordered): +def test_dataframe_aggregates( + scalars_df_index, scalars_pandas_df_index, op, bf_dtype, ordered +): col_names = ["int64_too", "float64_col", "string_col", "int64_col", "bool_col"] bf_series = op(scalars_df_index[col_names]) - pd_series = op(scalars_pandas_df_index[col_names]) bf_result = bf_series.to_pandas(ordered=ordered) + pd_result = op(scalars_pandas_df_index[col_names]) + + # Check dtype separately + assert bf_result.dtype == bf_dtype # Pandas may produce narrower numeric types, but bigframes always produces Float64 # Pandas has object index type - pd_series.index = pd_series.index.astype(pd.StringDtype(storage="pyarrow")) assert_series_equal( - pd_series, + pd_result, bf_result, + check_dtype=False, check_index_type=False, ignore_order=not ordered, - check_dtype=False, ) From 3acc4943e30446a8013a86b76823de8dcc0ab5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 25 Apr 2024 09:14:10 -0500 Subject: [PATCH 521/921] refactor: cache table metadata alongside snapshot time (#636) This ensures the cached `primary_keys` is more likely to be correct, in case the user called ALTER TABLE after we originally cached the snapshot time. --- bigframes/session/__init__.py | 52 +++++++++------------------- bigframes/session/_io/bigquery.py | 54 ++++++++++++++++++++++++++++++ tests/unit/session/test_session.py | 7 ++-- 3 files changed, 75 insertions(+), 38 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index f3f1ffce16f..1a0ea20e55f 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -231,7 +231,9 @@ def __init__( # Now that we're starting the session, don't allow the options to be # changed. context._session_started = True - self._df_snapshot: Dict[bigquery.TableReference, datetime.datetime] = {} + self._df_snapshot: Dict[ + bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table] + ] = {} @property def bqclient(self): @@ -698,16 +700,25 @@ def _get_snapshot_sql_and_primary_key( column(s), then return those too so that ordering generation can be avoided. """ - # If there are primary keys defined, the query engine assumes these - # columns are unique, even if the constraint is not enforced. We make - # the same assumption and use these columns as the total ordering keys. - table = self.bqclient.get_table(table_ref) + ( + snapshot_timestamp, + table, + ) = bigframes_io.get_snapshot_datetime_and_table_metadata( + self.bqclient, + table_ref=table_ref, + api_name=api_name, + cache=self._df_snapshot, + use_cache=use_cache, + ) if table.location.casefold() != self._location.casefold(): raise ValueError( f"Current session is in {self._location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" ) + # If there are primary keys defined, the query engine assumes these + # columns are unique, even if the constraint is not enforced. We make + # the same assumption and use these columns as the total ordering keys. primary_keys = None if ( (table_constraints := getattr(table, "table_constraints", None)) is not None @@ -718,37 +729,6 @@ def _get_snapshot_sql_and_primary_key( ): primary_keys = columns - job_config = bigquery.QueryJobConfig() - job_config.labels["bigframes-api"] = api_name - if use_cache and table_ref in self._df_snapshot.keys(): - snapshot_timestamp = self._df_snapshot[table_ref] - - # Cache hit could be unexpected. See internal issue 329545805. - # Raise a warning with more information about how to avoid the - # problems with the cache. - warnings.warn( - f"Reading cached table from {snapshot_timestamp} to avoid " - "incompatibilies with previous reads of this table. To read " - "the latest version, set `use_cache=False` or close the " - "current session with Session.close() or " - "bigframes.pandas.close_session().", - # There are many layers before we get to (possibly) the user's code: - # pandas.read_gbq_table - # -> with_default_session - # -> Session.read_gbq_table - # -> _read_gbq_table - # -> _get_snapshot_sql_and_primary_key - stacklevel=6, - ) - else: - snapshot_timestamp = list( - self.bqclient.query( - "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", - job_config=job_config, - ).result() - )[0][0] - self._df_snapshot[table_ref] = snapshot_timestamp - try: table_expression = self.ibis_client.sql( bigframes_io.create_snapshot_sql(table_ref, snapshot_timestamp) diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery.py index ac6ba4bae41..94576cfa124 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery.py @@ -23,6 +23,7 @@ import types from typing import Dict, Iterable, Optional, Sequence, Tuple, Union import uuid +import warnings import google.api_core.exceptions import google.cloud.bigquery as bigquery @@ -121,6 +122,59 @@ def table_ref_to_sql(table: bigquery.TableReference) -> str: return f"`{table.project}`.`{table.dataset_id}`.`{table.table_id}`" +def get_snapshot_datetime_and_table_metadata( + bqclient: bigquery.Client, + table_ref: bigquery.TableReference, + *, + api_name: str, + cache: Dict[bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table]], + use_cache: bool = True, +) -> Tuple[datetime.datetime, bigquery.Table]: + cached_table = cache.get(table_ref) + if use_cache and cached_table is not None: + snapshot_timestamp, _ = cached_table + + # Cache hit could be unexpected. See internal issue 329545805. + # Raise a warning with more information about how to avoid the + # problems with the cache. + warnings.warn( + f"Reading cached table from {snapshot_timestamp} to avoid " + "incompatibilies with previous reads of this table. To read " + "the latest version, set `use_cache=False` or close the " + "current session with Session.close() or " + "bigframes.pandas.close_session().", + # There are many layers before we get to (possibly) the user's code: + # pandas.read_gbq_table + # -> with_default_session + # -> Session.read_gbq_table + # -> _read_gbq_table + # -> _get_snapshot_sql_and_primary_key + # -> get_snapshot_datetime_and_table_metadata + stacklevel=7, + ) + return cached_table + + # TODO(swast): It's possible that the table metadata is changed between now + # and when we run the CURRENT_TIMESTAMP() query to see when we can time + # travel to. Find a way to fetch the table metadata and BQ's current time + # atomically. + table = bqclient.get_table(table_ref) + + # TODO(b/336521938): Refactor to make sure we set the "bigframes-api" + # whereever we execute a query. + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + snapshot_timestamp = list( + bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + cached_table = (snapshot_timestamp, table) + cache[table_ref] = cached_table + return cached_table + + def create_snapshot_sql( table_ref: bigquery.TableReference, current_timestamp: datetime.datetime ) -> str: diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 543196066a3..4ba47190bda 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -42,8 +42,11 @@ def test_read_gbq_cached_table(): google.cloud.bigquery.DatasetReference("my-project", "my_dataset"), "my_table", ) - session._df_snapshot[table_ref] = datetime.datetime( - 1999, 1, 2, 3, 4, 5, 678901, tzinfo=datetime.timezone.utc + table = google.cloud.bigquery.Table(table_ref) + table._properties["location"] = session._location + session._df_snapshot[table_ref] = ( + datetime.datetime(1999, 1, 2, 3, 4, 5, 678901, tzinfo=datetime.timezone.utc), + table, ) with pytest.warns(UserWarning, match=re.escape("use_cache=False")): From 3ffc1d275ae110bffea2f08e63ef75b053764a0c Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:39:18 -0700 Subject: [PATCH 522/921] feat: support the `score` method for `PaLM2TextGenerator` (#634) * feat: support the score method for PaLM2TextGenerator * address comments * address additional comments * address minor comments --- bigframes/ml/core.py | 11 ++++ bigframes/ml/llm.py | 59 ++++++++++++++++++- bigframes/ml/sql.py | 10 ++++ tests/system/load/test_llm.py | 56 +++++++++++++++--- tests/unit/ml/test_sql.py | 14 +++++ .../sklearn/ensemble/_forest.py | 4 +- .../bigframes_vendored/xgboost/sklearn.py | 4 +- 7 files changed, 146 insertions(+), 12 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index b94ae39687a..12c881c19ae 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -187,6 +187,17 @@ def evaluate(self, input_data: Optional[bpd.DataFrame] = None): return self._session.read_gbq(sql) + def llm_evaluate( + self, + input_data: bpd.DataFrame, + task_type: Optional[str] = None, + ): + sql = self._model_manipulation_sql_generator.ml_llm_evaluate( + input_data, task_type + ) + + return self._session.read_gbq(sql) + def arima_evaluate(self, show_all_candidate_models: bool = False): sql = self._model_manipulation_sql_generator.ml_arima_evaluate( show_all_candidate_models diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 37a38cdd5c9..4a58152d14d 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -220,7 +220,7 @@ def predict( Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame or Series, which needs to contain a column with name "prompt". Only the column will be used as input. + Input DataFrame or Series, which contains only one column of prompts. Prompts can include preamble, questions, suggestions, instructions, or examples. temperature (float, default 0.0): @@ -310,6 +310,63 @@ def predict( return df + def score( + self, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + task_type: Literal[ + "text_generation", "classification", "summarization", "question_answering" + ] = "text_generation", + ) -> bpd.DataFrame: + """Calculate evaluation metrics of the model. + + .. note:: + + This product or feature 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 + (https://cloud.google.com/products#product-launch-stages). + + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#remote-model-llm + for the outputs relevant to this model type. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation data, which contains only one column of input_text + that contains the prompt text to use when evaluating the model. + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation labels, which contains only one column of output_text + that you would expect to be returned by the model. + task_type (str): + The type of the task for LLM model. Default to "text_generation". + Possible values: "text_generation", "classification", "summarization", and "question_answering". + + Returns: + bigframes.dataframe.DataFrame: The DataFrame as evaluation result. + """ + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + X, y = utils.convert_to_dataframe(X, y) + + if len(X.columns) != 1 or len(y.columns) != 1: + raise ValueError( + f"Only support one column as input for X and y. {constants.FEEDBACK_LINK}" + ) + + # BQML identified the column by name + X_col_label = cast(blocks.Label, X.columns[0]) + y_col_label = cast(blocks.Label, y.columns[0]) + X = X.rename(columns={X_col_label: "input_text"}) + y = y.rename(columns={y_col_label: "output_text"}) + + input_data = X.join(y, how="outer") + + return self._bqml_model.llm_evaluate(input_data, task_type) + def to_gbq(self, model_name: str, replace: bool = False) -> PaLM2TextGenerator: """Save the model to BigQuery. diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 59c768ce819..3679be16c6b 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -318,6 +318,16 @@ def ml_evaluate(self, source_df: Optional[bpd.DataFrame] = None) -> str: return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, ({source_sql}))""" + # ML evaluation TVFs + def ml_llm_evaluate( + self, source_df: bpd.DataFrame, task_type: Optional[str] = None + ) -> str: + """Encode ML.EVALUATE for BQML""" + # Note: don't need index as evaluate returns a new table + source_sql, _, _ = source_df._to_sql_query(include_index=False) + return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, + ({source_sql}), STRUCT("{task_type}" AS task_type))""" + # ML evaluation TVFs def ml_arima_evaluate(self, show_all_candidate_models: bool = False) -> str: """Encode ML.ARMIA_EVALUATE for BQML""" diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index d56f6100c12..835b31955ed 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -22,13 +22,12 @@ def llm_fine_tune_df_default_index( session: bigframes.Session, ) -> bigframes.dataframe.DataFrame: - sql = """ -SELECT - CONCAT("Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: ", text) as prompt, - CAST(label AS STRING) as label -FROM `llm_tuning.emotion_classification_train` -""" - return session.read_gbq(sql) + training_table_name = "llm_tuning.emotion_classification_train" + df = session.read_gbq(training_table_name) + prefix = "Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: " + df["prompt"] = prefix + df["text"] + df["label"] = df["label"].astype("string") + return df @pytest.fixture(scope="session") @@ -69,3 +68,46 @@ def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_ assert all(series.str.len() == 1) # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept + + +def test_llm_palm_score(llm_fine_tune_df_default_index): + model = bigframes.ml.llm.PaLM2TextGenerator(model_name="text-bison") + + # Check score to ensure the model was fitted + score_result = model.score( + X=llm_fine_tune_df_default_index[["prompt"]], + y=llm_fine_tune_df_default_index[["label"]], + ).to_pandas() + score_result_col = score_result.columns.to_list() + expected_col = [ + "bleu4_score", + "rouge-l_precision", + "rouge-l_recall", + "rouge-l_f1_score", + "evaluation_status", + ] + assert all(col in score_result_col for col in expected_col) + + +def test_llm_palm_score_params(llm_fine_tune_df_default_index): + model = bigframes.ml.llm.PaLM2TextGenerator( + model_name="text-bison", max_iterations=1 + ) + + # Check score to ensure the model was fitted + score_result = model.score( + X=llm_fine_tune_df_default_index["prompt"], + y=llm_fine_tune_df_default_index["label"], + task_type="classification", + ).to_pandas() + score_result_col = score_result.columns.to_list() + expected_col = [ + "trial_id", + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", + ] + assert all(col in score_result_col for col in expected_col) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 3560f05cb68..1a5e8fe962d 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -319,6 +319,20 @@ def test_ml_predict_correct( ) +def test_ml_llm_evaluate_correct( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, + mock_df: bpd.DataFrame, +): + sql = model_manipulation_sql_generator.ml_llm_evaluate( + source_df=mock_df, task_type="CLASSIFICATION" + ) + assert ( + sql + == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, + (input_X_sql), STRUCT("CLASSIFICATION" AS task_type))""" + ) + + def test_ml_evaluate_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py index 53a211dd7f2..a55b7b80d32 100644 --- a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -95,7 +95,7 @@ class RandomForestRegressor(ForestRegressor): Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to - default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + default, XGBoost will choose the most conservative option available. Possible values: "exact", "approx", "hist". min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. @@ -160,7 +160,7 @@ class RandomForestClassifier(ForestClassifier): Number of parallel trees constructed during each iteration. Default to 100. Minimum value is 2. tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to - default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + default, XGBoost will choose the most conservative option available. Possible values: "exact", "approx", "hist". min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index 424b17a371b..5a2a69dff4f 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -63,7 +63,7 @@ class XGBRegressor(XGBModel, XGBRegressorBase): Type of normalization algorithm for DART booster. Possible values: "TREE", "FOREST". Default to "TREE". tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to - default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + default, XGBoost will choose the most conservative option available. Possible values: "exact", "approx", "hist". min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. @@ -110,7 +110,7 @@ class XGBClassifier(XGBModel, XGBClassifierMixIn, XGBClassifierBase): Type of normalization algorithm for DART booster. Possible values: "TREE", "FOREST". Default to "TREE". tree_method (Optional[str]): Specify which tree method to use. Default to "auto". If this parameter is set to - default, XGBoost will choose the most conservative option available. Possible values: ""exact", "approx", + default, XGBoost will choose the most conservative option available. Possible values: "exact", "approx", "hist". min_child_weight (Optional[float]): Minimum sum of instance weight(hessian) needed in a child. Default to 1. From 44b738df07d0ee9d9ae2ced339a123f31139f887 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 25 Apr 2024 11:24:30 -0700 Subject: [PATCH 523/921] perf: Cache transpose to allow performant retranspose (#635) --- bigframes/core/blocks.py | 80 ++++++++++++++++++++-------- bigframes/core/compile/compiled.py | 17 +++--- bigframes/core/ordering.py | 2 + bigframes/dataframe.py | 47 ++++++++-------- tests/system/small/test_dataframe.py | 14 +++++ 5 files changed, 105 insertions(+), 55 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 2a888125f86..5965c963748 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -27,7 +27,7 @@ import os import random import typing -from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple +from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple, Union import warnings import google.cloud.bigquery as bigquery @@ -105,6 +105,8 @@ def __init__( index_columns: Iterable[str], column_labels: typing.Union[pd.Index, typing.Iterable[Label]], index_labels: typing.Union[pd.Index, typing.Iterable[Label], None] = None, + *, + transpose_cache: Optional[Block] = None, ): """Construct a block object, will create default index if no index columns specified.""" index_columns = list(index_columns) @@ -144,6 +146,7 @@ def __init__( # TODO(kemppeterson) Add a cache for corr to parallel the single-column stats. self._stats_cache[" ".join(self.index_columns)] = {} + self._transpose_cache: Optional[Block] = transpose_cache @classmethod def from_local(cls, data: pd.DataFrame, session: bigframes.Session) -> Block: @@ -716,6 +719,15 @@ def with_column_labels( index_labels=self.index.names, ) + def with_transpose_cache(self, transposed: Block): + return Block( + self._expr, + index_columns=self.index_columns, + column_labels=self._column_labels, + index_labels=self.index.names, + transpose_cache=transposed, + ) + def with_index_labels(self, value: typing.Sequence[Label]) -> Block: if len(value) != len(self.index_columns): raise ValueError( @@ -804,18 +816,35 @@ def multi_apply_window_op( def multi_apply_unary_op( self, columns: typing.Sequence[str], - op: ops.UnaryOp, + op: Union[ops.UnaryOp, ex.Expression], ) -> Block: + if isinstance(op, ops.UnaryOp): + input_varname = guid.generate_guid() + expr = op.as_expr(input_varname) + else: + input_varnames = op.unbound_variables + assert len(input_varnames) == 1 + expr = op + input_varname = input_varnames[0] + block = self - for i, col_id in enumerate(columns): + for col_id in columns: label = self.col_id_to_label[col_id] - block, result_id = block.apply_unary_op( - col_id, - op, - result_label=label, + block, result_id = block.project_expr( + expr.bind_all_variables({input_varname: ex.free_var(col_id)}), + label=label, ) block = block.copy_values(result_id, col_id) block = block.drop_columns([result_id]) + # Special case, we can preserve transpose cache for full-frame unary ops + if (self._transpose_cache is not None) and set(self.value_columns) == set( + columns + ): + transpose_columns = self._transpose_cache.value_columns + new_transpose_cache = self._transpose_cache.multi_apply_unary_op( + transpose_columns, op + ) + block = block.with_transpose_cache(new_transpose_cache) return block def apply_window_op( @@ -922,20 +951,17 @@ def aggregate_all_and_stack( (ex.UnaryAggregation(operation, ex.free_var(col_id)), col_id) for col_id in self.value_columns ] - index_col_ids = [ - guid.generate_guid() for i in range(self.column_labels.nlevels) - ] - result_expr = self.expr.aggregate(aggregations, dropna=dropna).unpivot( - row_labels=self.column_labels.to_list(), - index_col_ids=index_col_ids, - unpivot_columns=tuple([(value_col_id, tuple(self.value_columns))]), - ) + index_id = guid.generate_guid() + result_expr = self.expr.aggregate( + aggregations, dropna=dropna + ).assign_constant(index_id, None, None) + # Transpose as last operation so that final block has valid transpose cache return Block( result_expr, - index_columns=index_col_ids, - column_labels=[None], - index_labels=self.column_labels.names, - ) + index_columns=[index_id], + column_labels=self.column_labels, + index_labels=[None], + ).transpose(original_row_index=pd.Index([None])) else: # axis_n == 1 # using offsets as identity to group on. # TODO: Allow to promote identity/total_order columns instead for better perf @@ -1575,10 +1601,19 @@ def melt( index_columns=[index_id], ) - def transpose(self) -> Block: - """Transpose the block. Will fail if dtypes aren't coercible to a common type or too many rows""" + def transpose(self, *, original_row_index: Optional[pd.Index] = None) -> Block: + """Transpose the block. Will fail if dtypes aren't coercible to a common type or too many rows. + Can provide the original_row_index directly if it is already known, otherwise a query is needed. + """ + if self._transpose_cache is not None: + return self._transpose_cache.with_transpose_cache(self) + original_col_index = self.column_labels - original_row_index = self.index.to_pandas() + original_row_index = ( + original_row_index + if original_row_index is not None + else self.index.to_pandas() + ) original_row_count = len(original_row_index) if original_row_count > bigframes.constants.MAX_COLUMNS: raise NotImplementedError( @@ -1619,6 +1654,7 @@ def transpose(self) -> Block: result.with_column_labels(original_row_index) .order_by([ordering.ascending_over(result.index_columns[-1])]) .drop_levels([result.index_columns[-1]]) + .with_transpose_cache(self) ) def _create_stack_column( diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index a59d5996797..88c1006c79a 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -823,17 +823,14 @@ def to_sql( for col in baked_ir.column_ids ] selection = ", ".join(map(lambda col_id: f"`{col_id}`", output_columns)) - order_by_clause = baked_ir._ordering_clause( - baked_ir._ordering.all_ordering_columns - ) - sql = textwrap.dedent( - f"SELECT {selection}\n" - "FROM (\n" - f"{sql}\n" - ")\n" - f"{order_by_clause}\n" - ) + sql = textwrap.dedent(f"SELECT {selection}\n" "FROM (\n" f"{sql}\n" ")\n") + # Single row frames may not have any ordering columns + if len(baked_ir._ordering.all_ordering_columns) > 0: + order_by_clause = baked_ir._ordering_clause( + baked_ir._ordering.all_ordering_columns + ) + sql += f"{order_by_clause}\n" else: sql = ibis_bigquery.Backend().compile( self._to_ibis_expr( diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 2543a3b722b..9009e31be39 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -167,6 +167,8 @@ def _truncate_ordering( truncated_refs.append(order_part) if columns_seen.issuperset(must_see): return tuple(truncated_refs) + if len(must_see) == 0: + return () raise ValueError("Ordering did not contain all total_order_cols") def with_reverse(self): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a55b7f569b3..48c4af7a37e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -693,18 +693,19 @@ def _apply_binop( def _apply_scalar_binop( self, other: float | int, op: ops.BinaryOp, reverse: bool = False ) -> DataFrame: - block = self._block - for column_id, label in zip( - self._block.value_columns, self._block.column_labels - ): - expr = ( - op.as_expr(ex.const(other), column_id) - if reverse - else op.as_expr(column_id, ex.const(other)) + if reverse: + expr = op.as_expr( + left_input=ex.const(other), + right_input=bigframes.core.guid.generate_guid(), ) - block, _ = block.project_expr(expr, label) - block = block.drop_columns([column_id]) - return DataFrame(block) + else: + expr = op.as_expr( + left_input=bigframes.core.guid.generate_guid(), + right_input=ex.const(other), + ) + return DataFrame( + self._block.multi_apply_unary_op(self._block.value_columns, expr) + ) def _apply_series_binop_axis_0( self, @@ -1974,7 +1975,7 @@ def any( else: frame = self._drop_non_bool() block = frame._block.aggregate_all_and_stack(agg_ops.any_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def all( self, axis: typing.Union[str, int] = 0, *, bool_only: bool = False @@ -1984,7 +1985,7 @@ def all( else: frame = self._drop_non_bool() block = frame._block.aggregate_all_and_stack(agg_ops.all_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def sum( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False @@ -1994,7 +1995,7 @@ def sum( else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.sum_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def mean( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False @@ -2004,7 +2005,7 @@ def mean( else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.mean_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def median( self, *, numeric_only: bool = False, exact: bool = True @@ -2019,7 +2020,7 @@ def median( return result else: block = frame._block.aggregate_all_and_stack(agg_ops.median_op) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def quantile( self, q: Union[float, Sequence[float]] = 0.5, *, numeric_only: bool = False @@ -2052,7 +2053,7 @@ def std( else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.std_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def var( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False @@ -2062,7 +2063,7 @@ def var( else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.var_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def min( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False @@ -2072,7 +2073,7 @@ def min( else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.min_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def max( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False @@ -2082,7 +2083,7 @@ def max( else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.max_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def prod( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False @@ -2092,7 +2093,7 @@ def prod( else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.product_op, axis=axis) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) product = prod product.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.prod) @@ -2103,11 +2104,11 @@ def count(self, *, numeric_only: bool = False) -> bigframes.series.Series: else: frame = self._drop_non_numeric() block = frame._block.aggregate_all_and_stack(agg_ops.count_op) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def nunique(self) -> bigframes.series.Series: block = self._block.aggregate_all_and_stack(agg_ops.nunique_op) - return bigframes.series.Series(block.select_column("values")) + return bigframes.series.Series(block) def agg( self, func: str | typing.Sequence[str] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 3230ad2a89a..2a4b53403d5 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2524,6 +2524,20 @@ def test_df_transpose_error(): dataframe.DataFrame([[1, "hello"], [2, "world"]]).transpose() +def test_df_transpose_repeated_uses_cache(): + bf_df = dataframe.DataFrame([[1, 2.5], [2, 3.5]]) + pd_df = pandas.DataFrame([[1, 2.5], [2, 3.5]]) + # Transposing many times so that operation will fail from complexity if not using cache + for i in range(10): + # Cache still works even with simple scalar binop + bf_df = bf_df.transpose() + i + pd_df = pd_df.transpose() + i + + pd.testing.assert_frame_equal( + pd_df, bf_df.to_pandas(), check_dtype=False, check_index_type=False + ) + + @pytest.mark.parametrize( ("ordered"), [ From ce5649513b66c5191a56fc1fd29240b5dbe02394 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 25 Apr 2024 11:36:36 -0700 Subject: [PATCH 524/921] fix: Allow to_pandas to download more than 10GB (#637) * fix: Allow to_pandas to download more than 10GB * remove unecessary line --- bigframes/core/blocks.py | 9 +++++++-- bigframes/session/__init__.py | 16 ++++++++++------ tests/system/load/test_large_tables.py | 11 +++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 5965c963748..4ff8a1836b5 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -517,9 +517,14 @@ def _materialize_local( ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. - results_iterator, query_job = self.session._execute( - self.expr, sorted=materialize_options.ordered + _, query_job = self.session._query_to_destination( + self.session._to_sql(self.expr, sorted=True), + list(self.index_columns), + api_name="cached", + do_clustering=False, ) + results_iterator = query_job.result() + table_size = ( self.session._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 1a0ea20e55f..ecb672f0909 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -430,7 +430,8 @@ def _query_to_destination( index_cols: List[str], api_name: str, configuration: dict = {"query": {"useQueryCache": True}}, - ) -> Tuple[Optional[bigquery.TableReference], Optional[bigquery.QueryJob]]: + do_clustering=True, + ) -> Tuple[Optional[bigquery.TableReference], bigquery.QueryJob]: # If a dry_run indicates this is not a query type job, then don't # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. dry_run_config = bigquery.QueryJobConfig() @@ -444,11 +445,14 @@ def _query_to_destination( # internal issue 303057336. # Since we have a `statement_type == 'SELECT'`, schema should be populated. schema = typing.cast(Iterable[bigquery.SchemaField], dry_run_job.schema) - cluster_cols = [ - item.name - for item in schema - if (item.name in index_cols) and _can_cluster_bq(item) - ][:_MAX_CLUSTER_COLUMNS] + if do_clustering: + cluster_cols = [ + item.name + for item in schema + if (item.name in index_cols) and _can_cluster_bq(item) + ][:_MAX_CLUSTER_COLUMNS] + else: + cluster_cols = [] temp_table = self._create_empty_temp_table(schema, cluster_cols) timeout_ms = configuration.get("jobTimeoutMs") or configuration["query"].get( diff --git a/tests/system/load/test_large_tables.py b/tests/system/load/test_large_tables.py index 22baa2268fd..871c846c799 100644 --- a/tests/system/load/test_large_tables.py +++ b/tests/system/load/test_large_tables.py @@ -90,3 +90,14 @@ def test_to_pandas_batches_large_table(): del df assert row_count == expected_row_count + + +def test_to_pandas_large_table(): + df = bpd.read_gbq("load_testing.scalars_10gb") + # df will be downloaded locally + expected_row_count, expected_column_count = df.shape + + df = df.to_pandas() + row_count, column_count = df.shape + assert column_count == expected_column_count + assert row_count == expected_row_count From dfeaad0ae3b3557a9e8ccb21ddbdc55cfd611e0f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 25 Apr 2024 22:10:54 +0000 Subject: [PATCH 525/921] feat: expose gcf max timeout in `remote_function` (#639) * feat: expose gcf max timeout in `remote_function` * remove duplicate test case from parametrize --- bigframes/functions/remote_function.py | 28 +++++++++++-- bigframes/pandas/__init__.py | 2 + bigframes/session/__init__.py | 12 ++++++ tests/system/large/test_remote_function.py | 49 ++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index f866575a268..4bb667ccc71 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -341,7 +341,9 @@ def generate_cloud_function_code(self, def_, dir, package_requirements=None): entry_point = self.generate_cloud_function_main_code(def_, dir) return entry_point - def create_cloud_function(self, def_, cf_name, package_requirements=None): + def create_cloud_function( + self, def_, cf_name, package_requirements=None, cloud_function_timeout=600 + ): """Create a cloud function from the given user defined function.""" # Build and deploy folder structure containing cloud function @@ -409,7 +411,14 @@ def create_cloud_function(self, def_, cf_name, package_requirements=None): ) function.service_config = functions_v2.ServiceConfig() function.service_config.available_memory = "1024M" - function.service_config.timeout_seconds = 600 + if cloud_function_timeout is not None: + if cloud_function_timeout > 1200: + raise ValueError( + "BigQuery remote function can wait only up to 20 minutes" + ", see for more details " + "https://cloud.google.com/bigquery/quotas#remote_function_limits." + ) + function.service_config.timeout_seconds = cloud_function_timeout function.service_config.service_account_email = ( self._cloud_function_service_account ) @@ -456,6 +465,7 @@ def provision_bq_remote_function( name, package_requirements, max_batching_rows, + cloud_function_timeout, ): """Provision a BigQuery remote function.""" # If reuse of any existing function with the same name (indicated by the @@ -477,7 +487,7 @@ def provision_bq_remote_function( # Create the cloud function if it does not exist if not cf_endpoint: cf_endpoint = self.create_cloud_function( - def_, cloud_function_name, package_requirements + def_, cloud_function_name, package_requirements, cloud_function_timeout ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") @@ -631,6 +641,7 @@ def remote_function( cloud_function_kms_key_name: Optional[str] = None, cloud_function_docker_repository: Optional[str] = None, max_batching_rows: Optional[int] = 1000, + cloud_function_timeout: Optional[int] = 600, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -756,6 +767,16 @@ def remote_function( `None` can be passed to let BQ remote functions service apply default batching. See for more details https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. + cloud_function_timeout (int, Optional): + The maximum amount of time (in seconds) BigQuery should wait for + the cloud function to return a response. See for more details + https://cloud.google.com/functions/docs/configuring/timeout. + Please note that even though the cloud function (2nd gen) itself + allows seeting up to 60 minutes of timeout, BigQuery remote + function can wait only up to 20 minutes, see for more details + https://cloud.google.com/bigquery/quotas#remote_function_limits. + By default BigQuery DataFrames uses a 10 minute timeout. `None` + can be passed to let the cloud functions default timeout take effect. """ import bigframes.pandas as bpd @@ -880,6 +901,7 @@ def wrapper(f): name, packages, max_batching_rows, + cloud_function_timeout, ) # TODO: Move ibis logic to compiler step diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 96af6ab1b3f..71ef4e609e5 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -644,6 +644,7 @@ def remote_function( cloud_function_kms_key_name: Optional[str] = None, cloud_function_docker_repository: Optional[str] = None, max_batching_rows: Optional[int] = 1000, + cloud_function_timeout: Optional[int] = 600, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -658,6 +659,7 @@ def remote_function( cloud_function_kms_key_name=cloud_function_kms_key_name, cloud_function_docker_repository=cloud_function_docker_repository, max_batching_rows=max_batching_rows, + cloud_function_timeout=cloud_function_timeout, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index ecb672f0909..973c87b59b6 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1529,6 +1529,7 @@ def remote_function( cloud_function_kms_key_name: Optional[str] = None, cloud_function_docker_repository: Optional[str] = None, max_batching_rows: Optional[int] = 1000, + cloud_function_timeout: Optional[int] = 600, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1632,6 +1633,16 @@ def remote_function( `None` can be passed to let BQ remote functions service apply default batching. See for more details https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. + cloud_function_timeout (int, Optional): + The maximum amount of time (in seconds) BigQuery should wait for + the cloud function to return a response. See for more details + https://cloud.google.com/functions/docs/configuring/timeout. + Please note that even though the cloud function (2nd gen) itself + allows seeting up to 60 minutes of timeout, BigQuery remote + function can wait only up to 20 minutes, see for more details + https://cloud.google.com/bigquery/quotas#remote_function_limits. + By default BigQuery DataFrames uses a 10 minute timeout. `None` + can be passed to let the cloud functions default timeout take effect. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1654,6 +1665,7 @@ def remote_function( cloud_function_kms_key_name=cloud_function_kms_key_name, cloud_function_docker_repository=cloud_function_docker_repository, max_batching_rows=max_batching_rows, + cloud_function_timeout=cloud_function_timeout, ) def read_gbq_function( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index ec9acc292ea..6cae893f9cd 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1336,3 +1336,52 @@ def square(x): cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, square_remote ) + + +@pytest.mark.parametrize( + ("timeout_args", "effective_gcf_timeout"), + [ + pytest.param({}, 600, id="no-set"), + pytest.param({"cloud_function_timeout": None}, 60, id="set-None"), + pytest.param({"cloud_function_timeout": 1200}, 1200, id="set-max-allowed"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_gcf_timeout( + session, scalars_dfs, timeout_args, effective_gcf_timeout +): + try: + + def square(x): + return x * x + + square_remote = session.remote_function( + [int], int, reuse=False, **timeout_args + )(square) + + # Assert that the GCF is created with the intended maximum timeout + gcf = session.cloudfunctionsclient.get_function( + name=square_remote.bigframes_cloud_function + ) + assert gcf.service_config.timeout_seconds == effective_gcf_timeout + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["int64_too"].apply(square_remote).to_pandas() + pd_result = scalars_pandas_df["int64_too"].apply(square) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square_remote + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_gcf_timeout_max_supported_exceeded(session): + with pytest.raises(ValueError): + + @session.remote_function([int], int, reuse=False, cloud_function_timeout=1201) + def square(x): + return x * x From 9005c6e79297d7130e93a0e632eb3936aa145efe Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 26 Apr 2024 09:12:09 -0700 Subject: [PATCH 526/921] fix: Extend row hash to 128 bits to guarantee unique row id (#632) * fix: Extend row hash to 128 bits to guarantee unique row id * decide hash size based on row count * fix read_gbq tests * handle unknown row_num --- bigframes/session/__init__.py | 50 +++++++++++++++++++++--------- tests/unit/resources.py | 10 ++++++ tests/unit/session/test_session.py | 21 ++++++++++--- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 973c87b59b6..34047ff1551 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -54,6 +54,7 @@ import google.api_core.gapic_v1.client_info import google.auth.credentials import google.cloud.bigquery as bigquery +import google.cloud.bigquery.table import google.cloud.bigquery_connection_v1 import google.cloud.bigquery_storage_v1 import google.cloud.functions_v2 @@ -693,7 +694,7 @@ def read_gbq_table( def _get_snapshot_sql_and_primary_key( self, - table_ref: bigquery.table.TableReference, + table: google.cloud.bigquery.table.Table, *, api_name: str, use_cache: bool = True, @@ -709,7 +710,7 @@ def _get_snapshot_sql_and_primary_key( table, ) = bigframes_io.get_snapshot_datetime_and_table_metadata( self.bqclient, - table_ref=table_ref, + table_ref=table.reference, api_name=api_name, cache=self._df_snapshot, use_cache=use_cache, @@ -735,7 +736,7 @@ def _get_snapshot_sql_and_primary_key( try: table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table_ref, snapshot_timestamp) + bigframes_io.create_snapshot_sql(table.reference, snapshot_timestamp) ) except google.api_core.exceptions.Forbidden as ex: if "Drive credentials" in ex.message: @@ -763,8 +764,9 @@ def _read_gbq_table( query, default_project=self.bqclient.project ) + table = self.bqclient.get_table(table_ref) (table_expression, primary_keys,) = self._get_snapshot_sql_and_primary_key( - table_ref, api_name=api_name, use_cache=use_cache + table, api_name=api_name, use_cache=use_cache ) total_ordering_cols = primary_keys @@ -836,9 +838,13 @@ def _read_gbq_table( ordering=ordering, ) else: - array_value = self._create_total_ordering(table_expression) + array_value = self._create_total_ordering( + table_expression, table_rows=table.num_rows + ) else: - array_value = self._create_total_ordering(table_expression) + array_value = self._create_total_ordering( + table_expression, table_rows=table.num_rows + ) value_columns = [col for col in array_value.column_ids if col not in index_cols] block = blocks.Block( @@ -1459,10 +1465,19 @@ def _create_empty_temp_table( def _create_total_ordering( self, table: ibis_types.Table, + table_rows: Optional[int], ) -> core.ArrayValue: # Since this might also be used as the index, don't use the default # "ordering ID" name. + + # For small tables, 64 bits is enough to avoid collisions, 128 bits will never ever collide no matter what + # Assume table is large if table row count is unknown + use_double_hash = ( + (table_rows is None) or (table_rows == 0) or (table_rows > 100000) + ) + ordering_hash_part = guid.generate_guid("bigframes_ordering_") + ordering_hash_part2 = guid.generate_guid("bigframes_ordering_") ordering_rand_part = guid.generate_guid("bigframes_ordering_") # All inputs into hash must be non-null or resulting hash will be null @@ -1475,25 +1490,30 @@ def _create_total_ordering( else str_values[0] ) full_row_hash = full_row_str.hash().name(ordering_hash_part) + # By modifying value slightly, we get another hash uncorrelated with the first + full_row_hash_p2 = (full_row_str + "_").hash().name(ordering_hash_part2) # Used to disambiguate between identical rows (which will have identical hash) random_value = ibis.random().name(ordering_rand_part) + order_values = ( + [full_row_hash, full_row_hash_p2, random_value] + if use_double_hash + else [full_row_hash, random_value] + ) + original_column_ids = table.columns table_with_ordering = table.select( - itertools.chain(original_column_ids, [full_row_hash, random_value]) + itertools.chain(original_column_ids, order_values) ) - ordering_ref1 = order.ascending_over(ordering_hash_part) - ordering_ref2 = order.ascending_over(ordering_rand_part) ordering = order.ExpressionOrdering( - ordering_value_columns=(ordering_ref1, ordering_ref2), - total_ordering_columns=frozenset([ordering_hash_part, ordering_rand_part]), + ordering_value_columns=tuple( + order.ascending_over(col.get_name()) for col in order_values + ), + total_ordering_columns=frozenset(col.get_name() for col in order_values), ) columns = [table_with_ordering[col] for col in original_column_ids] - hidden_columns = [ - table_with_ordering[ordering_hash_part], - table_with_ordering[ordering_rand_part], - ] + hidden_columns = [table_with_ordering[col.get_name()] for col in order_values] return core.ArrayValue.from_ibis( self, table_with_ordering, diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 28b08e49dc2..623af93f65a 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -44,6 +44,12 @@ def create_bigquery_session( google.auth.credentials.Credentials, instance=True ) + if anonymous_dataset is None: + anonymous_dataset = google.cloud.bigquery.DatasetReference( + "test-project", + "test_dataset", + ) + if bqclient is None: bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" @@ -53,6 +59,10 @@ def create_bigquery_session( table._properties = {} type(table).location = mock.PropertyMock(return_value="test-region") type(table).schema = mock.PropertyMock(return_value=table_schema) + type(table).reference = mock.PropertyMock( + return_value=anonymous_dataset.table("test_table") + ) + type(table).num_rows = mock.PropertyMock(return_value=1000000000) bqclient.get_table.return_value = table if anonymous_dataset is None: diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 4ba47190bda..34f185cafd0 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -49,6 +49,16 @@ def test_read_gbq_cached_table(): table, ) + def get_table_mock(table_ref): + table = google.cloud.bigquery.Table( + table_ref, (google.cloud.bigquery.SchemaField("col", "INTEGER"),) + ) + table._properties["numRows"] = "1000000000" + table._properties["location"] = session._location + return table + + session.bqclient.get_table = get_table_mock + with pytest.warns(UserWarning, match=re.escape("use_cache=False")): df = session.read_gbq("my-project.my_dataset.my_table") @@ -137,10 +147,13 @@ def query_mock(query, *args, **kwargs): session.bqclient.query = query_mock - def get_table_mock(dataset_ref): - dataset = google.cloud.bigquery.Dataset(dataset_ref) - dataset.location = session._location - return dataset + def get_table_mock(table_ref): + table = google.cloud.bigquery.Table( + table_ref, (google.cloud.bigquery.SchemaField("col", "INTEGER"),) + ) + table._properties["numRows"] = 1000000000 + table._properties["location"] = session._location + return table session.bqclient.get_table = get_table_mock From 1e7793cdcb56b8c0bcccc1c1ab356bac44454592 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 26 Apr 2024 09:24:27 -0700 Subject: [PATCH 527/921] docs: address more technical writers' feedback (#640) --- bigframes/ml/cluster.py | 4 +-- bigframes/ml/decomposition.py | 4 +-- bigframes/ml/forecasting.py | 2 +- bigframes/ml/imported.py | 6 ++-- bigframes/ml/linear_model.py | 8 +++--- bigframes/ml/llm.py | 28 +++++++++---------- bigframes/ml/pipeline.py | 6 ++-- bigframes/ml/remote.py | 10 +++---- .../sklearn/cluster/_kmeans.py | 2 +- .../sklearn/ensemble/_forest.py | 2 +- .../sklearn/linear_model/_base.py | 2 +- .../sklearn/linear_model/_logistic.py | 2 +- .../sklearn/metrics/pairwise.py | 6 ++-- .../bigframes_vendored/sklearn/pipeline.py | 5 ++-- .../sklearn/preprocessing/_encoder.py | 4 +-- .../bigframes_vendored/xgboost/sklearn.py | 2 +- 16 files changed, 46 insertions(+), 47 deletions(-) diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index e63764e7bb3..e572bb3bfb1 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -175,12 +175,12 @@ def to_gbq(self, model_name: str, replace: bool = False) -> KMeans: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. Returns: - KMeans: saved model.""" + KMeans: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 0dfb46efaaa..01b1fda6283 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -169,12 +169,12 @@ def to_gbq(self, model_name: str, replace: bool = False) -> PCA: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. Returns: - PCA: saved model.""" + PCA: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index a7e0c3c0d90..5bd01c8826d 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -361,7 +361,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> ARIMAPlus: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index 9198b4eafb0..a642fae74d3 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -89,7 +89,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> TensorFlowModel: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Default to False. @@ -166,7 +166,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> ONNXModel: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. @@ -282,7 +282,7 @@ def to_gbq(self, model_name: str, replace: bool = False) -> XGBoostModel: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 63462be09f5..0c76a39a1c1 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -182,12 +182,12 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LinearRegression: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. Returns: - LinearRegression: saved model.""" + LinearRegression: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") @@ -347,12 +347,12 @@ def to_gbq(self, model_name: str, replace: bool = False) -> LogisticRegression: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. Returns: - LogisticRegression: saved model.""" + LogisticRegression: Saved model.""" if not self._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 4a58152d14d..b455e35b675 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -64,8 +64,8 @@ class PaLM2TextGenerator(base.BaseEstimator): BQ session to create the model. If None, use the global default session. connection_name (str or None): Connection to connect with remote service. str of the format ... - if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach - permission if the connection isn't fully setup. + If None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach + permission if the connection isn't fully set up. max_iterations (Optional[int], Default to 300): The number of steps to run when performing supervised tuning. """ @@ -191,7 +191,7 @@ def fit( Training labels. Returns: - PaLM2TextGenerator: Fitted Estimator. + PaLM2TextGenerator: Fitted estimator. """ X, y = utils.convert_to_dataframe(X, y) @@ -372,12 +372,12 @@ def to_gbq(self, model_name: str, replace: bool = False) -> PaLM2TextGenerator: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. Returns: - PaLM2TextGenerator: saved model.""" + PaLM2TextGenerator: Saved model.""" new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) @@ -390,7 +390,7 @@ class PaLM2TextEmbeddingGenerator(base.BaseEstimator): Args: model_name (str, Default to "textembedding-gecko"): The model for text embedding. “textembedding-gecko” returns model embeddings for text inputs. - "textembedding-gecko-multilingual" returns model embeddings for text inputs which support over 100 languages + "textembedding-gecko-multilingual" returns model embeddings for text inputs which support over 100 languages. Default to "textembedding-gecko". version (str or None): Model version. Accepted values are "001", "002", "003", "latest" etc. Will use the default version if unset. @@ -398,8 +398,8 @@ class PaLM2TextEmbeddingGenerator(base.BaseEstimator): session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. connection_name (str or None): - connection to connect with remote service. str of the format ... - if None, use default connection in session context. + Connection to connect with remote service. str of the format ... + If None, use default connection in session context. """ def __init__( @@ -539,12 +539,12 @@ def to_gbq( Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. Returns: - PaLM2TextEmbeddingGenerator: saved model.""" + PaLM2TextEmbeddingGenerator: Saved model.""" new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) @@ -565,8 +565,8 @@ class GeminiTextGenerator(base.BaseEstimator): BQ session to create the model. If None, use the global default session. connection_name (str or None): Connection to connect with remote service. str of the format ... - if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach - permission if the connection isn't fully setup. + If None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach + permission if the connection isn't fully set up. """ def __init__( @@ -719,12 +719,12 @@ def to_gbq(self, model_name: str, replace: bool = False) -> GeminiTextGenerator: Args: model_name (str): - the name of the model. + The name of the model. replace (bool, default False): Determine whether to replace if the model already exists. Default to False. Returns: - GeminiTextGenerator: saved model.""" + GeminiTextGenerator: Saved model.""" new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 5df2378575e..61b5a22da9a 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -126,12 +126,12 @@ def to_gbq(self, model_name: str, replace: bool = False) -> Pipeline: Args: model_name (str): - the name of the model(pipeline). + The name of the model(pipeline). replace (bool, default False): - whether to replace if the model(pipeline) already exists. Default to False. + Whether to replace if the model(pipeline) already exists. Default to False. Returns: - Pipeline: saved model(pipeline).""" + Pipeline: Saved model(pipeline).""" if not self._estimator._bqml_model: raise RuntimeError("A model must be fitted before it can be saved") diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index 8cf892f5362..8fb6d9db4c6 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -30,12 +30,12 @@ @log_adapter.class_logger class VertexAIModel(base.BaseEstimator): - """Remote model from a Vertex AI https endpoint. User must specify https endpoint, input schema and output schema. - How to deploy a model in Vertex AI https://cloud.google.com/bigquery/docs/bigquery-ml-remote-model-tutorial#Deploy-Model-on-Vertex-AI. + """Remote model from a Vertex AI HTTPS endpoint. User must specify HTTPS endpoint, input schema and output schema. + For more information, see Deploy model on Vertex AI: https://cloud.google.com/bigquery/docs/bigquery-ml-remote-model-tutorial#Deploy-Model-on-Vertex-AI. Args: endpoint (str): - Vertex AI https endpoint. + Vertex AI HTTPS endpoint. input (Mapping): Input schema: `{column_name: column_type}`. Supported types are "bool", "string", "int64", "float64", "array", "array", "array", "array". output (Mapping): @@ -44,8 +44,8 @@ class VertexAIModel(base.BaseEstimator): BQ session to create the model. If None, use the global default session. connection_name (str or None): Connection to connect with remote service. str of the format ... - if None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach - permission if the connection isn't fully setup. + If None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach + permission if the connection isn't fully set up. """ def __init__( diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index 2a0acc8cfe1..386b620f4af 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -75,7 +75,7 @@ def fit( Not used, present here for API consistency by convention. Returns: - KMeans: Fitted Estimator. + KMeans: Fitted estimator. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py index a55b7b80d32..92794bb68ee 100644 --- a/third_party/bigframes_vendored/sklearn/ensemble/_forest.py +++ b/third_party/bigframes_vendored/sklearn/ensemble/_forest.py @@ -56,7 +56,7 @@ def fit(self, X, y): Returns: - ForestModel: Fitted Estimator. + ForestModel: Fitted estimator. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_base.py b/third_party/bigframes_vendored/sklearn/linear_model/_base.py index a845b782c05..69f98697af4 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_base.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_base.py @@ -109,6 +109,6 @@ def fit( Target values. Will be cast to X's dtype if necessary. Returns: - LinearRegression: Fitted Estimator. + LinearRegression: Fitted estimator. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 494c730a6d6..49198eb9bdc 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -79,6 +79,6 @@ def fit( Returns: - LogisticRegression: Fitted Estimator. + LogisticRegression: Fitted estimator. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py index be3d6753a70..7584230be6b 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/pairwise.py +++ b/third_party/bigframes_vendored/sklearn/metrics/pairwise.py @@ -21,7 +21,7 @@ def paired_cosine_distances(X, Y) -> bpd.DataFrame: Input data. X and Y are mapped by indexes, must have the same index. Returns: - bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and cosine_distance + bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and cosine_distance. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -36,7 +36,7 @@ def paired_manhattan_distance(X, Y) -> bpd.DataFrame: Input data. X and Y are mapped by indexes, must have the same index. Returns: - bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and manhattan_distance + bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and manhattan_distance. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -51,6 +51,6 @@ def paired_euclidean_distances(X, Y) -> bpd.DataFrame: Input data. X and Y are mapped by indexes, must have the same index. Returns: - bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and euclidean_distance + bigframes.dataframe.DataFrame: DataFrame with columns of X, Y and euclidean_distance. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/sklearn/pipeline.py b/third_party/bigframes_vendored/sklearn/pipeline.py index 8a98ee41417..96eaa903be7 100644 --- a/third_party/bigframes_vendored/sklearn/pipeline.py +++ b/third_party/bigframes_vendored/sklearn/pipeline.py @@ -25,9 +25,8 @@ class Pipeline(BaseEstimator, metaclass=ABCMeta): The final estimator only needs to implement `fit`. The purpose of the pipeline is to assemble several steps that can be - cross-validated together while setting different parameters. This - simplifies code and allows for deploying an estimator and peprocessing - together, e.g. with `Pipeline.to_gbq(...).` + cross-validated together while setting different parameters. This simplifies code and allows for + deploying an estimator and preprocessing together, e.g. with `Pipeline.to_gbq(...).` """ def fit( diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py index b883e822499..7cdca9229a2 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_encoder.py @@ -84,6 +84,6 @@ def transform(self, X): The DataFrame or Series to be transformed. Returns: - bigframes.dataframe.DataFrame: The result is categorized as index: number, value: number. - Where index is the position of the dict that seeing the category, and value is 0 or 1.""" + bigframes.dataframe.DataFrame: The result is categorized as index: number, value: number, + where index is the position of the dict seeing the category, and value is 0 or 1.""" raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/xgboost/sklearn.py b/third_party/bigframes_vendored/xgboost/sklearn.py index 5a2a69dff4f..da1396af02f 100644 --- a/third_party/bigframes_vendored/xgboost/sklearn.py +++ b/third_party/bigframes_vendored/xgboost/sklearn.py @@ -38,7 +38,7 @@ def fit(self, X, y): Target values. Will be cast to X's dtype if necessary. Returns: - XGBModel: Fitted Estimator. + XGBModel: Fitted estimator. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From a5c94ec90dcf2c541d7d4b9558a629f935649dd2 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 26 Apr 2024 10:53:34 -0700 Subject: [PATCH 528/921] feat: Add .cache() method to persist intermediate dataframe (#626) --- bigframes/dataframe.py | 11 +++++++++++ bigframes/ml/core.py | 16 +++++----------- bigframes/series.py | 11 +++++++++++ tests/system/small/test_dataframe.py | 2 +- tests/unit/ml/test_golden_sql.py | 4 ++-- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 48c4af7a37e..092c8ab82fe 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3397,6 +3397,17 @@ def _set_block(self, block: blocks.Block): def _get_block(self) -> blocks.Block: return self._block + def cache(self): + """ + Materializes the DataFrame to a temporary table. + + Useful if the dataframe will be used multiple times, as this will avoid recomputating the shared intermediate value. + + Returns: + DataFrame: Self + """ + return self._cached(force=True) + def _cached(self, *, force: bool = False) -> DataFrame: """Materialize dataframe to a temporary table. No-op if the dataframe represents a trivial transformation of an existing materialization. diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 12c881c19ae..7b4638157ea 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -83,7 +83,7 @@ def distance( """ assert len(x.columns) == 1 and len(y.columns) == 1 - input_data = x._cached().join(y._cached(), how="outer") + input_data = x.cache().join(y.cache(), how="outer") x_column_id, y_column_id = x._block.value_columns[0], y._block.value_columns[0] return self._apply_sql( @@ -310,11 +310,9 @@ def create_model( # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot if y_train is None: - input_data = X_train._cached(force=True) + input_data = X_train.cache() else: - input_data = X_train._cached(force=True).join( - y_train._cached(force=True), how="outer" - ) + input_data = X_train.cache().join(y_train.cache(), how="outer") options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session @@ -354,9 +352,7 @@ def create_llm_remote_model( options = dict(options) # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot - input_data = X_train._cached(force=True).join( - y_train._cached(force=True), how="outer" - ) + input_data = X_train.cache().join(y_train.cache(), how="outer") options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session @@ -389,9 +385,7 @@ def create_time_series_model( options = dict(options) # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot - input_data = X_train._cached(force=True).join( - y_train._cached(force=True), how="outer" - ) + input_data = X_train.cache().join(y_train.cache(), how="outer") options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) diff --git a/bigframes/series.py b/bigframes/series.py index 5184d4bf1dd..3986d38445d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1682,6 +1682,17 @@ def _slice( ), ) + def cache(self): + """ + Materializes the Series to a temporary table. + + Useful if the series will be used multiple times, as this will avoid recomputating the shared intermediate value. + + Returns: + Series: Self + """ + return self._cached(force=True) + def _cached(self, *, force: bool = True) -> Series: self._set_block(self._block.cached(force=force)) return self diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 2a4b53403d5..b428207314d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4204,7 +4204,7 @@ def test_df_cached(scalars_df_index): ) df = df[df["rowindex_2"] % 2 == 0] - df_cached_copy = df._cached() + df_cached_copy = df.cache() pandas.testing.assert_frame_equal(df.to_pandas(), df_cached_copy.to_pandas()) diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index bcb220b1071..48fb7011ea1 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -63,7 +63,7 @@ def bqml_model_factory(mocker: pytest_mock.MockerFixture): def mock_y(): mock_y = mock.create_autospec(spec=bpd.DataFrame) mock_y.columns = pd.Index(["input_column_label"]) - mock_y._cached.return_value = mock_y + mock_y.cache.return_value = mock_y return mock_y @@ -83,7 +83,7 @@ def mock_X(mock_y, mock_session): ["index_column_id"], ["index_column_label"], ) - mock_X._cached.return_value = mock_X + mock_X.cache.return_value = mock_X return mock_X From 3aa643f7ab6dd0ff826ca2aafbeef29035d7c912 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 29 Apr 2024 17:53:04 +0000 Subject: [PATCH 529/921] feat: allow single input type in `remote_function` (#641) * feat: allow single input type in `remote_function` * say sequence instead of list in the remote_function docstring * fix more doc --- bigframes/functions/remote_function.py | 14 +++++---- bigframes/pandas/__init__.py | 2 +- bigframes/session/__init__.py | 7 +++-- samples/snippets/remote_function.py | 4 +-- tests/system/large/test_remote_function.py | 29 +++++++++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 2 +- .../bigframes_vendored/pandas/core/series.py | 8 ++--- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 4bb667ccc71..f7237c564c6 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -24,7 +24,7 @@ import sys import tempfile import textwrap -from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING +from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING, Union import ibis import requests @@ -623,7 +623,7 @@ def get_routine_reference( # which has moved as @js to the ibis package # https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py def remote_function( - input_types: Sequence[type], + input_types: Union[type, Sequence[type]], output_type: type, session: Optional[Session] = None, bigquery_client: Optional[bigquery.Client] = None, @@ -686,9 +686,10 @@ def remote_function( `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. Args: - input_types list(type): - List of input data types in the user defined function. - output_type type: + input_types (type or sequence(type)): + Input data type, or sequence of input data types in the user + defined function. + output_type (type): Data type of the output in the user defined function. session (bigframes.Session, Optional): BigQuery DataFrames session to use for getting default project, @@ -778,6 +779,9 @@ def remote_function( By default BigQuery DataFrames uses a 10 minute timeout. `None` can be passed to let the cloud functions default timeout take effect. """ + if isinstance(input_types, type): + input_types = [input_types] + import bigframes.pandas as bpd session = session or bpd.get_global_session() diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 71ef4e609e5..48a4b0f68df 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -633,7 +633,7 @@ def read_parquet( def remote_function( - input_types: List[type], + input_types: Union[type, Sequence[type]], output_type: type, dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 34047ff1551..79febcc5d90 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1538,7 +1538,7 @@ def _ibis_to_temp_table( def remote_function( self, - input_types: List[type], + input_types: Union[type, Sequence[type]], output_type: type, dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, @@ -1592,8 +1592,9 @@ def remote_function( `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. Args: - input_types (list(type)): - List of input data types in the user defined function. + input_types (type or sequence(type)): + Input data type, or sequence of input data types in the user + defined function. output_type (type): Data type of the output in the user defined function. dataset (str, Optional): diff --git a/samples/snippets/remote_function.py b/samples/snippets/remote_function.py index 61b7dc092ad..4db4e67619e 100644 --- a/samples/snippets/remote_function.py +++ b/samples/snippets/remote_function.py @@ -47,7 +47,7 @@ def run_remote_function_and_read_gbq_function(project_id: str): # of the penguins, which is a real number, into a category, which is a # string. @bpd.remote_function( - [float], + float, str, reuse=False, ) @@ -91,7 +91,7 @@ def get_bucket(num): # as a remote function. The custom function in this example has external # package dependency, which can be specified via `packages` parameter. @bpd.remote_function( - [str], + str, str, reuse=False, packages=["cryptography"], diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 6cae893f9cd..eb7cb8308ba 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -310,6 +310,35 @@ def add_one(x): ) +@pytest.mark.parametrize( + ("input_types"), + [ + pytest.param([int], id="list-of-int"), + pytest.param(int, id="int"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_input_types(session, scalars_dfs, input_types): + try: + + def add_one(x): + return x + 1 + + remote_add_one = session.remote_function(input_types, int)(add_one) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.int64_too.map(remote_add_one).to_pandas() + pd_result = scalars_pandas_df.int64_too.map(add_one) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, remote_add_one + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_explicit_dataset_not_created( session, diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 1669a291c91..c5168cd160d 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3892,7 +3892,7 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: to potentially reuse a previously deployed ``remote_function`` from the same user defined function. - >>> @bpd.remote_function([int], float, reuse=False) + >>> @bpd.remote_function(int, float, reuse=False) ... def minutes_to_hours(x): ... return x/60 diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 0c5b8d45219..4833c41ff70 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1181,7 +1181,7 @@ def apply( to potentially reuse a previously deployed `remote_function` from the same user defined function. - >>> @bpd.remote_function([int], float, reuse=False) + >>> @bpd.remote_function(int, float, reuse=False) ... def minutes_to_hours(x): ... return x/60 @@ -1208,7 +1208,7 @@ def apply( `packages` param. >>> @bpd.remote_function( - ... [str], + ... str, ... str, ... reuse=False, ... packages=["cryptography"], @@ -3341,7 +3341,7 @@ def mask(self, cond, other): condition is evaluated based on a complicated business logic which cannot be expressed in form of a Series. - >>> @bpd.remote_function([str], bool, reuse=False) + >>> @bpd.remote_function(str, bool, reuse=False) ... def should_mask(name): ... hash = 0 ... for char_ in name: @@ -3860,7 +3860,7 @@ def map( It also accepts a remote function: - >>> @bpd.remote_function([str], str) + >>> @bpd.remote_function(str, str) ... def my_mapper(val): ... vowels = ["a", "e", "i", "o", "u"] ... if val: From 03c1b0d8122afe9e56b480100d6207d1228ca576 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 29 Apr 2024 11:04:26 -0700 Subject: [PATCH 530/921] perf: Automatically condense internal expression representation (#516) --- bigframes/core/__init__.py | 14 +++++---- bigframes/core/compile/compiled.py | 26 +++++++++++----- bigframes/core/rewrite.py | 48 +++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 04291edbb17..185ce7cd4fd 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -183,7 +183,7 @@ def project_to_id(self, expression: ex.Expression, output_id: str): child=self.node, assignments=tuple(exprs), ) - ) + ).merge_projections() def assign(self, source_id: str, destination_id: str) -> ArrayValue: if destination_id in self.column_ids: # Mutate case @@ -208,7 +208,7 @@ def assign(self, source_id: str, destination_id: str) -> ArrayValue: child=self.node, assignments=tuple(exprs), ) - ) + ).merge_projections() def assign_constant( self, @@ -242,7 +242,7 @@ def assign_constant( child=self.node, assignments=tuple(exprs), ) - ) + ).merge_projections() def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: selections = ((ex.free_var(col_id), col_id) for col_id in column_ids) @@ -251,7 +251,7 @@ def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: child=self.node, assignments=tuple(selections), ) - ) + ).merge_projections() def drop_columns(self, columns: Iterable[str]) -> ArrayValue: new_projection = ( @@ -264,7 +264,7 @@ def drop_columns(self, columns: Iterable[str]) -> ArrayValue: child=self.node, assignments=tuple(new_projection), ) - ) + ).merge_projections() def aggregate( self, @@ -466,3 +466,7 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: The row numbers of result is non-deterministic, avoid to use. """ return ArrayValue(nodes.RandomSampleNode(self.node, fraction)) + + def merge_projections(self) -> ArrayValue: + new_node = bigframes.core.rewrite.maybe_squash_projection(self.node) + return ArrayValue(new_node) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 88c1006c79a..d14a5d3241c 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -1050,8 +1050,8 @@ def _hide_column(self, column_id) -> OrderedIR: def _bake_ordering(self) -> OrderedIR: """Bakes ordering expression into the selection, maybe creating hidden columns.""" ordering_expressions = self._ordering.all_ordering_columns - new_exprs = [] - new_baked_cols = [] + new_exprs: list[OrderingExpression] = [] + new_baked_cols: list[ibis_types.Value] = [] for expr in ordering_expressions: if isinstance(expr.scalar_expression, ex.OpExpression): baked_column = self._compile_expression(expr.scalar_expression).name( @@ -1059,18 +1059,28 @@ def _bake_ordering(self) -> OrderedIR: ) new_baked_cols.append(baked_column) new_expr = OrderingExpression( - ex.free_var(baked_column.name), expr.direction, expr.na_last + ex.free_var(baked_column.get_name()), expr.direction, expr.na_last ) new_exprs.append(new_expr) - else: + elif isinstance(expr.scalar_expression, ex.UnboundVariableExpression): + order_col = expr.scalar_expression.id new_exprs.append(expr) - - ordering = self._ordering.with_ordering_columns(new_exprs) + if order_col not in self.column_ids: + new_baked_cols.append( + self._ibis_bindings[expr.scalar_expression.id] + ) + + new_ordering = ExpressionOrdering( + tuple(new_exprs), + self._ordering.integer_encoding, + self._ordering.string_encoding, + self._ordering.total_ordering_columns, + ) return OrderedIR( self._table, columns=self.columns, - hidden_ordering_columns=[*self._hidden_ordering_columns, *new_baked_cols], - ordering=ordering, + hidden_ordering_columns=tuple(new_baked_cols), + ordering=new_ordering, predicates=self._predicates, ) diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index 61fe28b7b53..e3a07c04b42 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -35,16 +35,21 @@ class SquashedSelect: columns: Tuple[Tuple[scalar_exprs.Expression, str], ...] predicate: Optional[scalar_exprs.Expression] ordering: Tuple[order.OrderingExpression, ...] + reverse_root: bool = False @classmethod - def from_node(cls, node: nodes.BigFrameNode) -> SquashedSelect: + def from_node( + cls, node: nodes.BigFrameNode, projections_only: bool = False + ) -> SquashedSelect: if isinstance(node, nodes.ProjectionNode): - return cls.from_node(node.child).project(node.assignments) - elif isinstance(node, nodes.FilterNode): + return cls.from_node(node.child, projections_only=projections_only).project( + node.assignments + ) + elif not projections_only and isinstance(node, nodes.FilterNode): return cls.from_node(node.child).filter(node.predicate) - elif isinstance(node, nodes.ReversedNode): + elif not projections_only and isinstance(node, nodes.ReversedNode): return cls.from_node(node.child).reverse() - elif isinstance(node, nodes.OrderByNode): + elif not projections_only and isinstance(node, nodes.OrderByNode): return cls.from_node(node.child).order_with(node.by) else: selection = tuple( @@ -63,7 +68,9 @@ def project( new_columns = tuple( (expr.bind_all_variables(self.column_lookup), id) for expr, id in projection ) - return SquashedSelect(self.root, new_columns, self.predicate, self.ordering) + return SquashedSelect( + self.root, new_columns, self.predicate, self.ordering, self.reverse_root + ) def filter(self, predicate: scalar_exprs.Expression) -> SquashedSelect: if self.predicate is None: @@ -72,18 +79,24 @@ def filter(self, predicate: scalar_exprs.Expression) -> SquashedSelect: new_predicate = ops.and_op.as_expr( self.predicate, predicate.bind_all_variables(self.column_lookup) ) - return SquashedSelect(self.root, self.columns, new_predicate, self.ordering) + return SquashedSelect( + self.root, self.columns, new_predicate, self.ordering, self.reverse_root + ) def reverse(self) -> SquashedSelect: new_ordering = tuple(expr.with_reverse() for expr in self.ordering) - return SquashedSelect(self.root, self.columns, self.predicate, new_ordering) + return SquashedSelect( + self.root, self.columns, self.predicate, new_ordering, not self.reverse_root + ) def order_with(self, by: Tuple[order.OrderingExpression, ...]): adjusted_orderings = [ order_part.bind_variables(self.column_lookup) for order_part in by ] new_ordering = (*adjusted_orderings, *self.ordering) - return SquashedSelect(self.root, self.columns, self.predicate, new_ordering) + return SquashedSelect( + self.root, self.columns, self.predicate, new_ordering, self.reverse_root + ) def maybe_join( self, right: SquashedSelect, join_def: join_defs.JoinDefinition @@ -126,8 +139,10 @@ def maybe_join( new_columns = remap_names(join_def, lselection, rselection) # Reconstruct ordering + reverse_root = self.reverse_root if join_type == "right": new_ordering = right.ordering + reverse_root = right.reverse_root elif join_type == "outer": if lmask is not None: prefix = order.OrderingExpression(lmask, order.OrderingDirection.DESC) @@ -158,11 +173,15 @@ def maybe_join( new_ordering = self.ordering else: raise ValueError(f"Unexpected join type {join_type}") - return SquashedSelect(self.root, new_columns, new_predicate, new_ordering) + return SquashedSelect( + self.root, new_columns, new_predicate, new_ordering, reverse_root + ) def expand(self) -> nodes.BigFrameNode: # Safest to apply predicates first, as it may filter out inputs that cannot be handled by other expressions root = self.root + if self.reverse_root: + root = nodes.ReversedNode(child=root) if self.predicate: root = nodes.FilterNode(child=root, predicate=self.predicate) if self.ordering: @@ -170,6 +189,15 @@ def expand(self) -> nodes.BigFrameNode: return nodes.ProjectionNode(child=root, assignments=self.columns) +def maybe_squash_projection(node: nodes.BigFrameNode) -> nodes.BigFrameNode: + if isinstance(node, nodes.ProjectionNode) and isinstance( + node.child, nodes.ProjectionNode + ): + # Conservative approach, only squash consecutive projections, even though could also squash filters, reorderings + return SquashedSelect.from_node(node, projections_only=True).expand() + return node + + def maybe_rewrite_join(join_node: nodes.JoinNode) -> nodes.BigFrameNode: left_side = SquashedSelect.from_node(join_node.left_child) right_side = SquashedSelect.from_node(join_node.right_child) From cf4ec3af96c28d42e76868c6230a38511052c44e Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:16:16 -0700 Subject: [PATCH 531/921] fix: llm palm score tests (#643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal # 336527025🦕 --- tests/system/load/test_llm.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index 835b31955ed..fd136622754 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -49,12 +49,13 @@ def llm_remote_text_df(session, llm_remote_text_pandas_df): return session.read_pandas(llm_remote_text_pandas_df) +@pytest.mark.flaky(retries=2) def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): model = bigframes.ml.llm.PaLM2TextGenerator( model_name="text-bison", max_iterations=1 ) - df = llm_fine_tune_df_default_index.dropna() + df = llm_fine_tune_df_default_index.dropna().sample(n=100) X_train = df[["prompt"]] y_train = df[["label"]] model.fit(X_train, y_train) @@ -70,6 +71,7 @@ def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_ # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept +@pytest.mark.flaky(retries=2) def test_llm_palm_score(llm_fine_tune_df_default_index): model = bigframes.ml.llm.PaLM2TextGenerator(model_name="text-bison") @@ -89,6 +91,7 @@ def test_llm_palm_score(llm_fine_tune_df_default_index): assert all(col in score_result_col for col in expected_col) +@pytest.mark.flaky(retries=2) def test_llm_palm_score_params(llm_fine_tune_df_default_index): model = bigframes.ml.llm.PaLM2TextGenerator( model_name="text-bison", max_iterations=1 @@ -102,12 +105,10 @@ def test_llm_palm_score_params(llm_fine_tune_df_default_index): ).to_pandas() score_result_col = score_result.columns.to_list() expected_col = [ - "trial_id", "precision", "recall", - "accuracy", "f1_score", - "log_loss", - "roc_auc", + "label", + "evaluation_status", ] assert all(col in score_result_col for col in expected_col) From ac8f40c6df80c906079986042875cd6b57ab576e Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:11:25 -0700 Subject: [PATCH 532/921] chore(main): release 1.4.0 (#633) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a96c9028350..b01e78ec423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.4.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.3.0...v1.4.0) (2024-04-29) + + +### Features + +* Add .cache() method to persist intermediate dataframe ([#626](https://github.com/googleapis/python-bigquery-dataframes/issues/626)) ([a5c94ec](https://github.com/googleapis/python-bigquery-dataframes/commit/a5c94ec90dcf2c541d7d4b9558a629f935649dd2)) +* Add transpose support for small homogeneously typed DataFrames. ([#621](https://github.com/googleapis/python-bigquery-dataframes/issues/621)) ([054075d](https://github.com/googleapis/python-bigquery-dataframes/commit/054075d448f7de1b3bc1a4631b4e2340643de4ef)) +* Allow single input type in `remote_function` ([#641](https://github.com/googleapis/python-bigquery-dataframes/issues/641)) ([3aa643f](https://github.com/googleapis/python-bigquery-dataframes/commit/3aa643f7ab6dd0ff826ca2aafbeef29035d7c912)) +* Expose gcf max timeout in `remote_function` ([#639](https://github.com/googleapis/python-bigquery-dataframes/issues/639)) ([dfeaad0](https://github.com/googleapis/python-bigquery-dataframes/commit/dfeaad0ae3b3557a9e8ccb21ddbdc55cfd611e0f)) +* Series binary ops compatible with more types ([#618](https://github.com/googleapis/python-bigquery-dataframes/issues/618)) ([518d315](https://github.com/googleapis/python-bigquery-dataframes/commit/518d315487f351c227070c0127382d11381c5e88)) +* Support the `score` method for `PaLM2TextGenerator` ([#634](https://github.com/googleapis/python-bigquery-dataframes/issues/634)) ([3ffc1d2](https://github.com/googleapis/python-bigquery-dataframes/commit/3ffc1d275ae110bffea2f08e63ef75b053764a0c)) + + +### Bug Fixes + +* Allow to_pandas to download more than 10GB ([#637](https://github.com/googleapis/python-bigquery-dataframes/issues/637)) ([ce56495](https://github.com/googleapis/python-bigquery-dataframes/commit/ce5649513b66c5191a56fc1fd29240b5dbe02394)) +* Extend row hash to 128 bits to guarantee unique row id ([#632](https://github.com/googleapis/python-bigquery-dataframes/issues/632)) ([9005c6e](https://github.com/googleapis/python-bigquery-dataframes/commit/9005c6e79297d7130e93a0e632eb3936aa145efe)) +* Llm fine tuning tests ([#627](https://github.com/googleapis/python-bigquery-dataframes/issues/627)) ([4724a1a](https://github.com/googleapis/python-bigquery-dataframes/commit/4724a1a456076d003613d2e964a8dd2d80a09ad9)) +* Llm palm score tests ([#643](https://github.com/googleapis/python-bigquery-dataframes/issues/643)) ([cf4ec3a](https://github.com/googleapis/python-bigquery-dataframes/commit/cf4ec3af96c28d42e76868c6230a38511052c44e)) + + +### Performance Improvements + +* Automatically condense internal expression representation ([#516](https://github.com/googleapis/python-bigquery-dataframes/issues/516)) ([03c1b0d](https://github.com/googleapis/python-bigquery-dataframes/commit/03c1b0d8122afe9e56b480100d6207d1228ca576)) +* Cache transpose to allow performant retranspose ([#635](https://github.com/googleapis/python-bigquery-dataframes/issues/635)) ([44b738d](https://github.com/googleapis/python-bigquery-dataframes/commit/44b738df07d0ee9d9ae2ced339a123f31139f887)) + + +### Documentation + +* Add supported pandas apis on the main page ([#628](https://github.com/googleapis/python-bigquery-dataframes/issues/628)) ([8d2a51c](https://github.com/googleapis/python-bigquery-dataframes/commit/8d2a51c4079844daba20f414b6c0c0ca030ba1f9)) +* Add the first sample for the Single time-series forecasting from Google Analytics data tutorial ([#623](https://github.com/googleapis/python-bigquery-dataframes/issues/623)) ([2b84c4f](https://github.com/googleapis/python-bigquery-dataframes/commit/2b84c4f173e956ba2c7fcc0ad92785ae95161d8e)) +* Address more technical writers' feedback ([#640](https://github.com/googleapis/python-bigquery-dataframes/issues/640)) ([1e7793c](https://github.com/googleapis/python-bigquery-dataframes/commit/1e7793cdcb56b8c0bcccc1c1ab356bac44454592)) + ## [1.3.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.2.0...v1.3.0) (2024-04-22) diff --git a/bigframes/version.py b/bigframes/version.py index 1f103401e4d..e892a8893fc 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.3.0" +__version__ = "1.4.0" From 9a9f2bc4867e99e59fc1548497006d99883b1dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 30 Apr 2024 12:26:12 -0500 Subject: [PATCH 533/921] refactor: split `read_gbq_table` implementation into functions and move to separate module (#642) * refactor: split `read_gbq_table` implementation into functions and move to separate module add todos * refactor progress * add index_cols function * maybe ready for review * Update bigframes/session/__init__.py --- bigframes/session/__init__.py | 288 ++++--------- .../_io/{bigquery.py => bigquery/__init__.py} | 74 +--- .../session/_io/bigquery/read_gbq_table.py | 386 ++++++++++++++++++ tests/unit/session/test_io_bigquery.py | 16 - tests/unit/session/test_read_gbq_table.py | 37 ++ 5 files changed, 493 insertions(+), 308 deletions(-) rename bigframes/session/_io/{bigquery.py => bigquery/__init__.py} (74%) create mode 100644 bigframes/session/_io/bigquery/read_gbq_table.py create mode 100644 tests/unit/session/test_read_gbq_table.py diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 79febcc5d90..0f5aa19592a 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -18,7 +18,6 @@ import copy import datetime -import itertools import logging import os import re @@ -43,7 +42,6 @@ # Even though the ibis.backends.bigquery import is unused, it's needed # to register new and replacement ops with the Ibis BigQuery backend. import bigframes_vendored.ibis.backends.bigquery # noqa -import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet import bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers @@ -62,7 +60,6 @@ import google.cloud.storage as storage # type: ignore import ibis import ibis.backends.bigquery as ibis_bigquery -import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types import numpy as np import pandas @@ -80,7 +77,6 @@ import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.compile -import bigframes.core.guid as guid import bigframes.core.nodes as nodes from bigframes.core.ordering import IntegerEncoding import bigframes.core.ordering as order @@ -92,6 +88,7 @@ from bigframes.functions.remote_function import read_gbq_function as bigframes_rgf from bigframes.functions.remote_function import remote_function as bigframes_rf import bigframes.session._io.bigquery as bigframes_io +import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table import bigframes.session.clients import bigframes.version @@ -692,59 +689,6 @@ def read_gbq_table( use_cache=use_cache, ) - def _get_snapshot_sql_and_primary_key( - self, - table: google.cloud.bigquery.table.Table, - *, - api_name: str, - use_cache: bool = True, - ) -> Tuple[ibis_types.Table, Optional[Sequence[str]]]: - """Create a read-only Ibis table expression representing a table. - - If we can get a total ordering from the table, such as via primary key - column(s), then return those too so that ordering generation can be - avoided. - """ - ( - snapshot_timestamp, - table, - ) = bigframes_io.get_snapshot_datetime_and_table_metadata( - self.bqclient, - table_ref=table.reference, - api_name=api_name, - cache=self._df_snapshot, - use_cache=use_cache, - ) - - if table.location.casefold() != self._location.casefold(): - raise ValueError( - f"Current session is in {self._location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" - ) - - # If there are primary keys defined, the query engine assumes these - # columns are unique, even if the constraint is not enforced. We make - # the same assumption and use these columns as the total ordering keys. - primary_keys = None - if ( - (table_constraints := getattr(table, "table_constraints", None)) is not None - and (primary_key := table_constraints.primary_key) is not None - # This will be False for either None or empty list. - # We want primary_keys = None if no primary keys are set. - and (columns := primary_key.columns) - ): - primary_keys = columns - - try: - table_expression = self.ibis_client.sql( - bigframes_io.create_snapshot_sql(table.reference, snapshot_timestamp) - ) - except google.api_core.exceptions.Forbidden as ex: - if "Drive credentials" in ex.message: - ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." - raise - - return table_expression, primary_keys - def _read_gbq_table( self, query: str, @@ -757,21 +701,47 @@ def _read_gbq_table( ) -> dataframe.DataFrame: import bigframes.dataframe as dataframe + # --------------------------------- + # Validate and transform parameters + # --------------------------------- + if max_results and max_results <= 0: - raise ValueError("`max_results` should be a positive number.") + raise ValueError( + f"`max_results` should be a positive number, got {max_results}." + ) table_ref = bigquery.table.TableReference.from_string( query, default_project=self.bqclient.project ) - table = self.bqclient.get_table(table_ref) - (table_expression, primary_keys,) = self._get_snapshot_sql_and_primary_key( - table, api_name=api_name, use_cache=use_cache + # --------------------------------- + # Fetch table metadata and validate + # --------------------------------- + + (time_travel_timestamp, table,) = bf_read_gbq_table.get_table_metadata( + self.bqclient, + table_ref=table_ref, + api_name=api_name, + cache=self._df_snapshot, + use_cache=use_cache, ) - total_ordering_cols = primary_keys - if not index_col and primary_keys is not None: - index_col = primary_keys + if table.location.casefold() != self._location.casefold(): + raise ValueError( + f"Current session is in {self._location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" + ) + + # ----------------------------------------- + # Create Ibis table expression and validate + # ----------------------------------------- + + # Use a time travel to make sure the DataFrame is deterministic, even + # if the underlying table changes. + table_expression = bf_read_gbq_table.get_ibis_time_travel_table( + self.ibis_client, + table_ref, + time_travel_timestamp, + ) for key in columns: if key not in table_expression.columns: @@ -779,10 +749,22 @@ def _read_gbq_table( f"Column '{key}' of `columns` not found in this table." ) - if isinstance(index_col, str): - index_cols: List[str] = [index_col] - else: - index_cols = list(index_col) + # --------------------------------------- + # Create a non-default index and validate + # --------------------------------------- + + # TODO(b/337925142): Move index_cols creation to before we create the + # Ibis table expression so we don't have a "SELECT *" subquery in the + # query that checks for index uniqueness. + + index_cols, is_index_unique = bf_read_gbq_table.get_index_cols_and_uniqueness( + bqclient=self.bqclient, + ibis_client=self.ibis_client, + table=table, + table_expression=table_expression, + index_col=index_col, + api_name=api_name, + ) for key in index_cols: if key not in table_expression.columns: @@ -790,62 +772,33 @@ def _read_gbq_table( f"Column `{key}` of `index_col` not found in this table." ) + # TODO(b/337925142): We should push down column filters when we get the time + # travel table to avoid "SELECT *" subqueries. if columns: table_expression = table_expression.select([*index_cols, *columns]) - # If the index is unique and sortable, then we don't need to generate - # an ordering column. - ordering = None - if total_ordering_cols is not None: - # Note: currently, a table has a total ordering only when the - # primary key(s) are set on a table. The query engine assumes such - # columns are unique, even if not enforced. - ordering = order.ExpressionOrdering( - ordering_value_columns=tuple( - order.ascending_over(column_id) for column_id in total_ordering_cols - ), - total_ordering_columns=frozenset(total_ordering_cols), - ) - column_values = [table_expression[col] for col in table_expression.columns] - array_value = core.ArrayValue.from_ibis( - self, - table_expression, - columns=column_values, - hidden_ordering_columns=[], - ordering=ordering, - ) + # ---------------------------- + # Create ordering and validate + # ---------------------------- - elif len(index_cols) != 0: - # We have index columns, lets see if those are actually total_order_columns - ordering = order.ExpressionOrdering( - ordering_value_columns=tuple( - [order.ascending_over(column_id) for column_id in index_cols] - ), - total_ordering_columns=frozenset(index_cols), - ) - is_total_ordering = self._check_index_uniqueness( - table_expression, index_cols + if is_index_unique: + array_value = bf_read_gbq_table.to_array_value_with_total_ordering( + session=self, + table_expression=table_expression, + total_ordering_cols=index_cols, ) - if is_total_ordering: - column_values = [ - table_expression[col] for col in table_expression.columns - ] - array_value = core.ArrayValue.from_ibis( - self, - table_expression, - columns=column_values, - hidden_ordering_columns=[], - ordering=ordering, - ) - else: - array_value = self._create_total_ordering( - table_expression, table_rows=table.num_rows - ) else: - array_value = self._create_total_ordering( - table_expression, table_rows=table.num_rows + # Note: Even though we're adding a default ordering here, that's + # just so we have a deterministic total ordering. If the user + # specified a non-unique index, we still sort by that later. + array_value = bf_read_gbq_table.to_array_value_with_default_ordering( + session=self, table=table_expression, table_rows=table.num_rows ) + # ---------------------------------------------------- + # Create Block & default index if len(index_cols) == 0 + # ---------------------------------------------------- + value_columns = [col for col in array_value.column_ids if col not in index_cols] block = blocks.Block( array_value, @@ -862,27 +815,6 @@ def _read_gbq_table( df.sort_index() return df - def _check_index_uniqueness( - self, table: ibis_types.Table, index_cols: List[str] - ) -> bool: - distinct_table = table.select(*index_cols).distinct() - is_unique_sql = f"""WITH full_table AS ( - {self.ibis_client.compile(table)} - ), - distinct_table AS ( - {self.ibis_client.compile(distinct_table)} - ) - - SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, - (SELECT COUNT(*) FROM distinct_table) AS `distinct_count` - """ - results, _ = self._start_query(is_unique_sql) - row = next(iter(results)) - - total_count = row["total_count"] - distinct_count = row["distinct_count"] - return total_count == distinct_count - def _read_bigquery_load_job( self, filepath_or_buffer: str | IO["bytes"], @@ -1462,66 +1394,6 @@ def _create_empty_temp_table( ) return bigquery.TableReference.from_string(table) - def _create_total_ordering( - self, - table: ibis_types.Table, - table_rows: Optional[int], - ) -> core.ArrayValue: - # Since this might also be used as the index, don't use the default - # "ordering ID" name. - - # For small tables, 64 bits is enough to avoid collisions, 128 bits will never ever collide no matter what - # Assume table is large if table row count is unknown - use_double_hash = ( - (table_rows is None) or (table_rows == 0) or (table_rows > 100000) - ) - - ordering_hash_part = guid.generate_guid("bigframes_ordering_") - ordering_hash_part2 = guid.generate_guid("bigframes_ordering_") - ordering_rand_part = guid.generate_guid("bigframes_ordering_") - - # All inputs into hash must be non-null or resulting hash will be null - str_values = list( - map(lambda col: _convert_to_nonnull_string(table[col]), table.columns) - ) - full_row_str = ( - str_values[0].concat(*str_values[1:]) - if len(str_values) > 1 - else str_values[0] - ) - full_row_hash = full_row_str.hash().name(ordering_hash_part) - # By modifying value slightly, we get another hash uncorrelated with the first - full_row_hash_p2 = (full_row_str + "_").hash().name(ordering_hash_part2) - # Used to disambiguate between identical rows (which will have identical hash) - random_value = ibis.random().name(ordering_rand_part) - - order_values = ( - [full_row_hash, full_row_hash_p2, random_value] - if use_double_hash - else [full_row_hash, random_value] - ) - - original_column_ids = table.columns - table_with_ordering = table.select( - itertools.chain(original_column_ids, order_values) - ) - - ordering = order.ExpressionOrdering( - ordering_value_columns=tuple( - order.ascending_over(col.get_name()) for col in order_values - ), - total_ordering_columns=frozenset(col.get_name() for col in order_values), - ) - columns = [table_with_ordering[col] for col in original_column_ids] - hidden_columns = [table_with_ordering[col.get_name()] for col in order_values] - return core.ArrayValue.from_ibis( - self, - table_with_ordering, - columns, - hidden_ordering_columns=hidden_columns, - ordering=ordering, - ) - def _ibis_to_temp_table( self, table: ibis_types.Table, @@ -2056,28 +1928,6 @@ def _can_cluster_bq(field: bigquery.SchemaField): ) -def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringValue: - col_type = column.type() - if ( - col_type.is_numeric() - or col_type.is_boolean() - or col_type.is_binary() - or col_type.is_temporal() - ): - result = column.cast(ibis_dtypes.String(nullable=True)) - elif col_type.is_geospatial(): - result = typing.cast(ibis_types.GeoSpatialColumn, column).as_text() - elif col_type.is_string(): - result = column - else: - # TO_JSON_STRING works with all data types, but isn't the most efficient - # Needed for JSON, STRUCT and ARRAY datatypes - result = vendored_ibis_ops.ToJsonString(column).to_expr() # type: ignore - # Escape backslashes and use backslash as delineator - escaped = typing.cast(ibis_types.StringColumn, result.fillna("")).replace("\\", "\\\\") # type: ignore - return typing.cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) - - def _transform_read_gbq_configuration(configuration: Optional[dict]) -> dict: """ For backwards-compatibility, convert any previously client-side only diff --git a/bigframes/session/_io/bigquery.py b/bigframes/session/_io/bigquery/__init__.py similarity index 74% rename from bigframes/session/_io/bigquery.py rename to bigframes/session/_io/bigquery/__init__.py index 94576cfa124..2cd2d8ff9a8 100644 --- a/bigframes/session/_io/bigquery.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Private module: Helpers for I/O operations.""" +"""Private module: Helpers for BigQuery I/O operations.""" from __future__ import annotations @@ -23,7 +23,6 @@ import types from typing import Dict, Iterable, Optional, Sequence, Tuple, Union import uuid -import warnings import google.api_core.exceptions import google.cloud.bigquery as bigquery @@ -122,77 +121,6 @@ def table_ref_to_sql(table: bigquery.TableReference) -> str: return f"`{table.project}`.`{table.dataset_id}`.`{table.table_id}`" -def get_snapshot_datetime_and_table_metadata( - bqclient: bigquery.Client, - table_ref: bigquery.TableReference, - *, - api_name: str, - cache: Dict[bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table]], - use_cache: bool = True, -) -> Tuple[datetime.datetime, bigquery.Table]: - cached_table = cache.get(table_ref) - if use_cache and cached_table is not None: - snapshot_timestamp, _ = cached_table - - # Cache hit could be unexpected. See internal issue 329545805. - # Raise a warning with more information about how to avoid the - # problems with the cache. - warnings.warn( - f"Reading cached table from {snapshot_timestamp} to avoid " - "incompatibilies with previous reads of this table. To read " - "the latest version, set `use_cache=False` or close the " - "current session with Session.close() or " - "bigframes.pandas.close_session().", - # There are many layers before we get to (possibly) the user's code: - # pandas.read_gbq_table - # -> with_default_session - # -> Session.read_gbq_table - # -> _read_gbq_table - # -> _get_snapshot_sql_and_primary_key - # -> get_snapshot_datetime_and_table_metadata - stacklevel=7, - ) - return cached_table - - # TODO(swast): It's possible that the table metadata is changed between now - # and when we run the CURRENT_TIMESTAMP() query to see when we can time - # travel to. Find a way to fetch the table metadata and BQ's current time - # atomically. - table = bqclient.get_table(table_ref) - - # TODO(b/336521938): Refactor to make sure we set the "bigframes-api" - # whereever we execute a query. - job_config = bigquery.QueryJobConfig() - job_config.labels["bigframes-api"] = api_name - snapshot_timestamp = list( - bqclient.query( - "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", - job_config=job_config, - ).result() - )[0][0] - cached_table = (snapshot_timestamp, table) - cache[table_ref] = cached_table - return cached_table - - -def create_snapshot_sql( - table_ref: bigquery.TableReference, current_timestamp: datetime.datetime -) -> str: - """Query a table via 'time travel' for consistent reads.""" - # If we have an anonymous query results table, it can't be modified and - # there isn't any BigQuery time travel. - if table_ref.dataset_id.startswith("_"): - return f"SELECT * FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}`" - - return textwrap.dedent( - f""" - SELECT * - FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}` - FOR SYSTEM_TIME AS OF TIMESTAMP({repr(current_timestamp.isoformat())}) - """ - ) - - def create_temp_table( bqclient: bigquery.Client, dataset: bigquery.DatasetReference, diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py new file mode 100644 index 00000000000..3235ca92e5d --- /dev/null +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -0,0 +1,386 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Private helpers for loading a BigQuery table as a BigQuery DataFrames DataFrame. +""" + +from __future__ import annotations + +import datetime +import itertools +import textwrap +import typing +from typing import Dict, Iterable, List, Optional, Tuple +import warnings + +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +import google.api_core.exceptions +import google.cloud.bigquery as bigquery +import ibis +import ibis.backends +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types + +import bigframes +import bigframes.clients +import bigframes.core as core +import bigframes.core.compile +import bigframes.core.guid as guid +import bigframes.core.ordering as order +import bigframes.dtypes +import bigframes.session._io.bigquery.read_gbq_table +import bigframes.session.clients +import bigframes.version + +# Avoid circular imports. +if typing.TYPE_CHECKING: + import bigframes.session + + +def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringValue: + col_type = column.type() + if ( + col_type.is_numeric() + or col_type.is_boolean() + or col_type.is_binary() + or col_type.is_temporal() + ): + result = column.cast(ibis_dtypes.String(nullable=True)) + elif col_type.is_geospatial(): + result = typing.cast(ibis_types.GeoSpatialColumn, column).as_text() + elif col_type.is_string(): + result = column + else: + # TO_JSON_STRING works with all data types, but isn't the most efficient + # Needed for JSON, STRUCT and ARRAY datatypes + result = vendored_ibis_ops.ToJsonString(column).to_expr() # type: ignore + # Escape backslashes and use backslash as delineator + escaped = typing.cast(ibis_types.StringColumn, result.fillna("")).replace("\\", "\\\\") # type: ignore + return typing.cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) + + +def get_table_metadata( + bqclient: bigquery.Client, + table_ref: google.cloud.bigquery.table.TableReference, + *, + api_name: str, + cache: Dict[bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table]], + use_cache: bool = True, +) -> Tuple[datetime.datetime, google.cloud.bigquery.table.Table]: + """Get the table metadata, either from cache or via REST API.""" + + cached_table = cache.get(table_ref) + if use_cache and cached_table is not None: + snapshot_timestamp, _ = cached_table + + # Cache hit could be unexpected. See internal issue 329545805. + # Raise a warning with more information about how to avoid the + # problems with the cache. + warnings.warn( + f"Reading cached table from {snapshot_timestamp} to avoid " + "incompatibilies with previous reads of this table. To read " + "the latest version, set `use_cache=False` or close the " + "current session with Session.close() or " + "bigframes.pandas.close_session().", + # There are many layers before we get to (possibly) the user's code: + # pandas.read_gbq_table + # -> with_default_session + # -> Session.read_gbq_table + # -> _read_gbq_table + # -> _get_snapshot_sql_and_primary_key + # -> get_snapshot_datetime_and_table_metadata + stacklevel=7, + ) + return cached_table + + # TODO(swast): It's possible that the table metadata is changed between now + # and when we run the CURRENT_TIMESTAMP() query to see when we can time + # travel to. Find a way to fetch the table metadata and BQ's current time + # atomically. + table = bqclient.get_table(table_ref) + + # TODO(b/336521938): Refactor to make sure we set the "bigframes-api" + # whereever we execute a query. + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + snapshot_timestamp = list( + bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + cached_table = (snapshot_timestamp, table) + cache[table_ref] = cached_table + return cached_table + + +def _create_time_travel_sql( + table_ref: bigquery.TableReference, time_travel_timestamp: datetime.datetime +) -> str: + """Query a table via 'time travel' for consistent reads.""" + # If we have an anonymous query results table, it can't be modified and + # there isn't any BigQuery time travel. + if table_ref.dataset_id.startswith("_"): + return f"SELECT * FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}`" + + return textwrap.dedent( + f""" + SELECT * + FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}` + FOR SYSTEM_TIME AS OF TIMESTAMP({repr(time_travel_timestamp.isoformat())}) + """ + ) + + +def get_ibis_time_travel_table( + ibis_client: ibis.BaseBackend, + table_ref: bigquery.TableReference, + time_travel_timestamp: datetime.datetime, +) -> ibis_types.Table: + try: + return ibis_client.sql( + _create_time_travel_sql(table_ref, time_travel_timestamp) + ) + except google.api_core.exceptions.Forbidden as ex: + # Ibis does a dry run to get the types of the columns from the SQL. + if "Drive credentials" in ex.message: + ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." + raise + + +def _check_index_uniqueness( + bqclient: bigquery.Client, + ibis_client: ibis.BaseBackend, + table: ibis_types.Table, + index_cols: List[str], + api_name: str, +) -> bool: + distinct_table = table.select(*index_cols).distinct() + is_unique_sql = f"""WITH full_table AS ( + {ibis_client.compile(table)} + ), + distinct_table AS ( + {ibis_client.compile(distinct_table)} + ) + + SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, + (SELECT COUNT(*) FROM distinct_table) AS `distinct_count` + """ + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + results = bqclient.query_and_wait(is_unique_sql, job_config=job_config) + row = next(iter(results)) + + total_count = row["total_count"] + distinct_count = row["distinct_count"] + return total_count == distinct_count + + +def _get_primary_keys( + table: bigquery.table.Table, +) -> List[str]: + """Get primary keys from table if they are set.""" + + primary_keys: List[str] = [] + if ( + (table_constraints := getattr(table, "table_constraints", None)) is not None + and (primary_key := table_constraints.primary_key) is not None + # This will be False for either None or empty list. + # We want primary_keys = None if no primary keys are set. + and (columns := primary_key.columns) + ): + primary_keys = columns if columns is not None else [] + + return primary_keys + + +def get_index_cols_and_uniqueness( + bqclient: bigquery.Client, + ibis_client: ibis.BaseBackend, + table: bigquery.table.Table, + table_expression: ibis_types.Table, + index_col: Iterable[str] | str, + api_name: str, +) -> Tuple[List[str], bool]: + """ + If we can get a total ordering from the table, such as via primary key + column(s), then return those too so that ordering generation can be + avoided. + """ + + # Transform index_col -> index_cols so we have a variable that is + # always a list of column names (possibly empty). + if isinstance(index_col, str): + index_cols: List[str] = [index_col] + else: + index_cols = list(index_col) + + # If the isn't an index selected, use the primary keys of the table as the + # index. If there are no primary keys, we'll return an empty list. + if len(index_cols) == 0: + index_cols = _get_primary_keys(table) + + # TODO(b/335727141): If table has clustering/partitioning, fail if + # index_cols is empty. + + # If there are primary keys defined, the query engine assumes these + # columns are unique, even if the constraint is not enforced. We make + # the same assumption and use these columns as the total ordering keys. + is_index_unique = len(index_cols) != 0 + else: + is_index_unique = _check_index_uniqueness( + bqclient=bqclient, + ibis_client=ibis_client, + # TODO(b/337925142): Avoid a "SELECT *" subquery here by using + # _create_time_travel_sql with just index_cols. + table=table_expression, + index_cols=index_cols, + api_name=api_name, + ) + + return index_cols, is_index_unique + + +def get_time_travel_datetime_and_table_metadata( + bqclient: bigquery.Client, + table_ref: bigquery.TableReference, + *, + api_name: str, + cache: Dict[bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table]], + use_cache: bool = True, +) -> Tuple[datetime.datetime, bigquery.Table]: + cached_table = cache.get(table_ref) + if use_cache and cached_table is not None: + snapshot_timestamp, _ = cached_table + + # Cache hit could be unexpected. See internal issue 329545805. + # Raise a warning with more information about how to avoid the + # problems with the cache. + warnings.warn( + f"Reading cached table from {snapshot_timestamp} to avoid " + "incompatibilies with previous reads of this table. To read " + "the latest version, set `use_cache=False` or close the " + "current session with Session.close() or " + "bigframes.pandas.close_session().", + # There are many layers before we get to (possibly) the user's code: + # pandas.read_gbq_table + # -> with_default_session + # -> Session.read_gbq_table + # -> _read_gbq_table + # -> _get_snapshot_sql_and_primary_key + # -> get_snapshot_datetime_and_table_metadata + stacklevel=7, + ) + return cached_table + + # TODO(swast): It's possible that the table metadata is changed between now + # and when we run the CURRENT_TIMESTAMP() query to see when we can time + # travel to. Find a way to fetch the table metadata and BQ's current time + # atomically. + table = bqclient.get_table(table_ref) + + # TODO(b/336521938): Refactor to make sure we set the "bigframes-api" + # whereever we execute a query. + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + snapshot_timestamp = list( + bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + cached_table = (snapshot_timestamp, table) + cache[table_ref] = cached_table + return cached_table + + +def to_array_value_with_total_ordering( + session: bigframes.session.Session, + table_expression: ibis_types.Table, + total_ordering_cols: List[str], +) -> core.ArrayValue: + """Create an ArrayValue, assuming we already have a total ordering.""" + ordering = order.ExpressionOrdering( + ordering_value_columns=tuple( + order.ascending_over(column_id) for column_id in total_ordering_cols + ), + total_ordering_columns=frozenset(total_ordering_cols), + ) + column_values = [table_expression[col] for col in table_expression.columns] + return core.ArrayValue.from_ibis( + session, + table_expression, + columns=column_values, + hidden_ordering_columns=[], + ordering=ordering, + ) + + +def to_array_value_with_default_ordering( + session: bigframes.session.Session, + table: ibis_types.Table, + table_rows: Optional[int], +) -> core.ArrayValue: + """Create an ArrayValue with a deterministic default ordering.""" + # Since this might also be used as the index, don't use the default + # "ordering ID" name. + + # For small tables, 64 bits is enough to avoid collisions, 128 bits will never ever collide no matter what + # Assume table is large if table row count is unknown + use_double_hash = (table_rows is None) or (table_rows == 0) or (table_rows > 100000) + + ordering_hash_part = guid.generate_guid("bigframes_ordering_") + ordering_hash_part2 = guid.generate_guid("bigframes_ordering_") + ordering_rand_part = guid.generate_guid("bigframes_ordering_") + + # All inputs into hash must be non-null or resulting hash will be null + str_values = list( + map(lambda col: _convert_to_nonnull_string(table[col]), table.columns) + ) + full_row_str = ( + str_values[0].concat(*str_values[1:]) if len(str_values) > 1 else str_values[0] + ) + full_row_hash = full_row_str.hash().name(ordering_hash_part) + # By modifying value slightly, we get another hash uncorrelated with the first + full_row_hash_p2 = (full_row_str + "_").hash().name(ordering_hash_part2) + # Used to disambiguate between identical rows (which will have identical hash) + random_value = ibis.random().name(ordering_rand_part) + + order_values = ( + [full_row_hash, full_row_hash_p2, random_value] + if use_double_hash + else [full_row_hash, random_value] + ) + + original_column_ids = table.columns + table_with_ordering = table.select( + itertools.chain(original_column_ids, order_values) + ) + + ordering = order.ExpressionOrdering( + ordering_value_columns=tuple( + order.ascending_over(col.get_name()) for col in order_values + ), + total_ordering_columns=frozenset(col.get_name() for col in order_values), + ) + columns = [table_with_ordering[col] for col in original_column_ids] + hidden_columns = [table_with_ordering[col.get_name()] for col in order_values] + return core.ArrayValue.from_ibis( + session, + table_with_ordering, + columns, + hidden_ordering_columns=hidden_columns, + ordering=ordering, + ) diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 406de2b88e6..eed1acb5a38 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -137,22 +137,6 @@ def test_create_job_configs_labels_length_limit_met(): assert "source" in labels.keys() -def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): - table_ref = bigquery.TableReference.from_string( - "my-test-project._e8166e0cdb.anonbb92cd" - ) - - sql = bigframes.session._io.bigquery.create_snapshot_sql( - table_ref, datetime.datetime.now(datetime.timezone.utc) - ) - - # Anonymous query results tables don't support time travel. - assert "SYSTEM_TIME" not in sql - - # Need fully-qualified table name. - assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql - - def test_create_temp_table_default_expiration(): """Make sure the created table has an expiration.""" bqclient = mock.create_autospec(bigquery.Client) diff --git a/tests/unit/session/test_read_gbq_table.py b/tests/unit/session/test_read_gbq_table.py new file mode 100644 index 00000000000..1d09769aecc --- /dev/null +++ b/tests/unit/session/test_read_gbq_table.py @@ -0,0 +1,37 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for read_gbq_table helper functions.""" + +import datetime + +import google.cloud.bigquery as bigquery + +import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table + + +def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): + table_ref = bigquery.TableReference.from_string( + "my-test-project._e8166e0cdb.anonbb92cd" + ) + + sql = bf_read_gbq_table._create_time_travel_sql( + table_ref, datetime.datetime.now(datetime.timezone.utc) + ) + + # Anonymous query results tables don't support time travel. + assert "SYSTEM_TIME" not in sql + + # Need fully-qualified table name. + assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql From 9963f85b84c3b3c681447ab79e22ac93ac48349c Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:50:22 -0700 Subject: [PATCH 534/921] feat: add the `bigframes.bigquery` sub-package with a `bigframes.bigquery.array_length` function (#630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: creats bigquery namespace and adds bigquery.array_length function * add docs * minor fix * fixing docs * add more doc tests * sentence-case * TODO for null arrays --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/bigquery/__init__.py | 60 +++++++++++++++++++++ docs/reference/bigframes.bigquery/index.rst | 9 ++++ docs/reference/bigframes/index.rst | 1 + docs/reference/index.rst | 1 + docs/templates/toc.yml | 4 ++ tests/system/small/bigquery/__init__.py | 13 +++++ tests/system/small/bigquery/test_array.py | 32 +++++++++++ 7 files changed, 120 insertions(+) create mode 100644 bigframes/bigquery/__init__.py create mode 100644 docs/reference/bigframes.bigquery/index.rst create mode 100644 tests/system/small/bigquery/__init__.py create mode 100644 tests/system/small/bigquery/test_array.py diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py new file mode 100644 index 00000000000..197e0a83b54 --- /dev/null +++ b/bigframes/bigquery/__init__.py @@ -0,0 +1,60 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""This module integrates BigQuery built-in functions for use with DataFrame objects, +such as array functions: +https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions. """ + + +from __future__ import annotations + +import typing + +import bigframes.operations as ops + +if typing.TYPE_CHECKING: + import bigframes.series as series + + +def array_length(series: series.Series) -> series.Series: + """Compute the length of each array element in the Series. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([[1, 2, 8, 3], [], [3, 4]]) + >>> bbq.array_length(s) + 0 4 + 1 0 + 2 2 + dtype: Int64 + + You can also apply this function directly to Series. + + >>> s.apply(bbq.array_length, by_row=False) + 0 4 + 1 0 + 2 2 + dtype: Int64 + + Returns: + bigframes.series.Series: A Series of integer values indicating + the length of each element in the Series. + + """ + return series._apply_unary_op(ops.len_op) diff --git a/docs/reference/bigframes.bigquery/index.rst b/docs/reference/bigframes.bigquery/index.rst new file mode 100644 index 00000000000..03e9bb48a42 --- /dev/null +++ b/docs/reference/bigframes.bigquery/index.rst @@ -0,0 +1,9 @@ + +=========================== +BigQuery Built-in Functions +=========================== + +.. automodule:: bigframes.bigquery + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes/index.rst b/docs/reference/bigframes/index.rst index 76d64444faf..d26db18c969 100644 --- a/docs/reference/bigframes/index.rst +++ b/docs/reference/bigframes/index.rst @@ -1,4 +1,5 @@ +============ Core objects ============ diff --git a/docs/reference/index.rst b/docs/reference/index.rst index c790831db18..387e9b5cedf 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -10,3 +10,4 @@ packages. bigframes/index bigframes.pandas/index bigframes.ml/index + bigframes.bigquery/index diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 4573296ec3b..80ccc01fac6 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -189,5 +189,9 @@ uid: bigframes.ml.remote.VertexAIModel name: remote name: bigframes.ml + - items: + - name: BigQuery built-in functions + uid: bigframes.bigquery + name: bigframes.bigquery name: BigQuery DataFrames status: beta diff --git a/tests/system/small/bigquery/__init__.py b/tests/system/small/bigquery/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/tests/system/small/bigquery/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/system/small/bigquery/test_array.py b/tests/system/small/bigquery/test_array.py new file mode 100644 index 00000000000..a91669cd887 --- /dev/null +++ b/tests/system/small/bigquery/test_array.py @@ -0,0 +1,32 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd + +import bigframes.bigquery as bbq +import bigframes.pandas as bpd + + +def test_array_length(): + series = bpd.Series([["A", "AA", "AAA"], ["BB", "B"], np.nan, [], ["C"]]) + # TODO(b/336880368): Allow for NULL values to be input for ARRAY columns. + # Once we actually store NULL values, this will be NULL where the input is NULL. + expected = pd.Series([3, 2, 0, 0, 1]) + pd.testing.assert_series_equal( + bbq.array_length(series).to_pandas(), + expected, + check_dtype=False, + check_index_type=False, + ) From c67e501a4958ac097216cc1c0a9d5c1530c87ae5 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Wed, 1 May 2024 12:20:16 -0700 Subject: [PATCH 535/921] docs: fix the Palm2TextGenerator output token size (#649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #333480290 🦕 --- bigframes/ml/llm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index b455e35b675..9fa9a9acd0f 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -233,7 +233,7 @@ def predict( max_output_tokens (int, default 128): Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. A token may be smaller than a word. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. - Default 128. For the 'text-bison' model, possible values are in the range [1, 1024]. For the 'text-bison-32k' model, possible values are in the range [1, 8196]. + Default 128. For the 'text-bison' model, possible values are in the range [1, 1024]. For the 'text-bison-32k' model, possible values are in the range [1, 8192]. Please ensure that the specified value for max_output_tokens is within the appropriate range for the model being used. top_k (int, default 40): @@ -269,10 +269,10 @@ def predict( if ( self.model_name == _TEXT_GENERATOR_BISON_32K_ENDPOINT - and max_output_tokens not in range(1, 8197) + and max_output_tokens not in range(1, 8193) ): raise ValueError( - f"max_output_token must be [1, 8196] for TextBison 32k model, but is {max_output_tokens}." + f"max_output_token must be [1, 8192] for TextBison 32k model, but is {max_output_tokens}." ) if top_k not in range(1, 41): From 3867390229e47cae631f43d91e866632bd2b31aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 1 May 2024 16:26:32 -0500 Subject: [PATCH 536/921] test: explicitly use US location for session in tests (#650) This avoids some warnings we see and ignore in our tests. It might also address some flakiness in `tests/system/small/ml/test_llm.py::test_create_text_generator_model` and `tests/system/small/ml/test_llm.py::test_create_text_generator_32k_model`, but the root cause of that flakiness is still TBD. --- tests/system/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 70ff6eee39d..a9fb8cb3f58 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -128,7 +128,10 @@ def resourcemanager_client( @pytest.fixture(scope="session") def session() -> bigframes.Session: - return bigframes.Session() + context = bigframes.BigQueryOptions( + location="US", + ) + return bigframes.Session(context=context) @pytest.fixture(scope="session") From e4f13c3633b90e32d3171976d8b27ed10049882f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 1 May 2024 21:35:21 +0000 Subject: [PATCH 537/921] fix: use explicit session in `PaLM2TextGenerator` (#651) This is to avoid running into conflict with the global session which may have different options (e.g. location, connection etc.) set. --- bigframes/ml/llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 9fa9a9acd0f..4b075241949 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -144,7 +144,7 @@ def _from_bq( kwargs: dict = {} last_fitting = model.training_runs[-1]["trainingOptions"] - dummy_text_generator = cls() + dummy_text_generator = cls(session=session) for bf_param, _ in dummy_text_generator.__dict__.items(): bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) if bqml_param in last_fitting: From 8e4616b896f4e0d13d8bb0424c89335d3a1fe697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 2 May 2024 15:39:12 -0500 Subject: [PATCH 538/921] fix: don't raise UnknownLocationWarning for US or EU multi-regions (#653) --- bigframes/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bigframes/constants.py b/bigframes/constants.py index c6d8f3acc2e..4778eb9c9e1 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -31,6 +31,7 @@ # https://cloud.google.com/bigquery/docs/locations ALL_BIGQUERY_LOCATIONS = frozenset( { + # regions "us-east5", "us-south1", "us-central1", @@ -74,6 +75,9 @@ "me-central1", "me-west1", "africa-south1", + # multi-regions + "US", + "EU", } ) From 81d1262a40c133017c6debe89506d66aab7bb0c5 Mon Sep 17 00:00:00 2001 From: Salem Jorden <115185670+SalemJorden@users.noreply.github.com> Date: Thu, 2 May 2024 16:47:30 -0500 Subject: [PATCH 539/921] feat: add `ARIMAPlus.coef_` property exposing `ML.ARIMA_COEFFICIENTS` functionality (#585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create_single_timeseries_forecasting_model_test.py code sample * fix: forecast method to forecast time series * pair programming PR draft creation * feature: insoect coefficients * update tests for new feature * add arima_model.coef_ to fetch coefficients * updated tests for coefficients feature * feature update for arima_coefficients * updates to output cols * docstring updates --------- Co-authored-by: Salem Boyland Co-authored-by: Tim Sweña (Swast) --- bigframes/ml/core.py | 5 +++ bigframes/ml/forecasting.py | 21 ++++++++++++ bigframes/ml/sql.py | 4 +++ tests/system/large/ml/test_forecasting.py | 42 ++++++++++++++++------- tests/unit/ml/test_sql.py | 10 ++++++ 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 7b4638157ea..168bc584f7e 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -205,6 +205,11 @@ def arima_evaluate(self, show_all_candidate_models: bool = False): return self._session.read_gbq(sql) + def arima_coefficients(self) -> bpd.DataFrame: + sql = self._model_manipulation_sql_generator.ml_arima_coefficients() + + return self._session.read_gbq(sql) + def centroids(self) -> bpd.DataFrame: assert self._model.model_type == "KMEANS" diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 5bd01c8826d..783e7741b81 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -269,6 +269,27 @@ def predict( options={"horizon": horizon, "confidence_level": confidence_level} ) + @property + def coef_( + self, + ) -> bpd.DataFrame: + """Inspect the coefficients of the model. + + ..note:: + + Output matches that of the ML.ARIMA_COEFFICIENTS function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-arima-coefficients + for the outputs relevant to this model type. + + Returns: + bigframes.dataframe.DataFrame: + A DataFrame with the coefficients for the model. + """ + + if not self._bqml_model: + raise RuntimeError("A model must be fitted before inspect coefficients") + return self._bqml_model.arima_coefficients() + def detect_anomalies( self, X: Union[bpd.DataFrame, bpd.Series], diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 3679be16c6b..ea693e34379 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -318,6 +318,10 @@ def ml_evaluate(self, source_df: Optional[bpd.DataFrame] = None) -> str: return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, ({source_sql}))""" + def ml_arima_coefficients(self) -> str: + """Encode ML.ARIMA_COEFFICIENTS for BQML""" + return f"""SELECT * FROM ML.ARIMA_COEFFICIENTS(MODEL `{self._model_name}`)""" + # ML evaluation TVFs def ml_llm_evaluate( self, source_df: bpd.DataFrame, task_type: Optional[str] = None diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index b333839e2ea..ef74398c2e2 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -13,6 +13,7 @@ # limitations under the License. import pandas as pd +import pytest from bigframes.ml import forecasting @@ -31,15 +32,22 @@ ] -def test_arima_plus_model_fit_score( - time_series_df_default_index, dataset_id, new_time_series_df -): +@pytest.fixture(scope="module") +def arima_model(time_series_df_default_index): model = forecasting.ARIMAPlus() X_train = time_series_df_default_index[["parsed_date"]] y_train = time_series_df_default_index[["total_visits"]] model.fit(X_train, y_train) + return model + + +def test_arima_plus_model_fit_score( + dataset_id, + new_time_series_df, + arima_model, +): - result = model.score( + result = arima_model.score( new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] ).to_pandas() expected = pd.DataFrame( @@ -56,29 +64,39 @@ def test_arima_plus_model_fit_score( pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load to ensure configuration was kept - reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) + reloaded_model = arima_model.to_gbq( + f"{dataset_id}.temp_arima_plus_model", replace=True + ) assert ( f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name ) -def test_arima_plus_model_fit_summary(time_series_df_default_index, dataset_id): - model = forecasting.ARIMAPlus() - X_train = time_series_df_default_index[["parsed_date"]] - y_train = time_series_df_default_index[["total_visits"]] - model.fit(X_train, y_train) +def test_arima_plus_model_fit_summary(dataset_id, arima_model): - result = model.summary() + result = arima_model.summary() assert result.shape == (1, 12) assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) # save, load to ensure configuration was kept - reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) + reloaded_model = arima_model.to_gbq( + f"{dataset_id}.temp_arima_plus_model", replace=True + ) assert ( f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name ) +def test_arima_coefficients(arima_model): + got = arima_model.coef_ + expected_columns = { + "ar_coefficients", + "ma_coefficients", + "intercept_or_drift", + } + assert set(got.columns) == expected_columns + + def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): model = forecasting.ARIMAPlus( horizon=100, diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 1a5e8fe962d..4dd90b2c4a4 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -47,6 +47,16 @@ def mock_df(): return mock_df +def test_ml_arima_coefficients( + model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, +): + sql = model_manipulation_sql_generator.ml_arima_coefficients() + assert ( + sql + == """SELECT * FROM ML.ARIMA_COEFFICIENTS(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + ) + + def test_options_correct(base_sql_generator: ml_sql.BaseSqlGenerator): sql = base_sql_generator.options( model_type="lin_reg", input_label_cols=["col_a"], l1_reg=0.6 From 73064dd2aa1ece5de8f5849a0fd337d0ba677404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 2 May 2024 17:51:14 -0500 Subject: [PATCH 540/921] feat: raise `NoDefaultIndexError` from `read_gbq` on clustered/partitioned tables with no `index_col` or `filters` set (#631) This should help customers better discover the best practices for working with large tables. feat: support `index_col=False` in `read_csv` and `engine="bigquery"` --- bigframes/__init__.py | 4 + bigframes/core/blocks.py | 10 + bigframes/enums.py | 29 +++ bigframes/exceptions.py | 8 + bigframes/pandas/__init__.py | 15 +- bigframes/session/__init__.py | 74 ++++-- .../session/_io/bigquery/read_gbq_table.py | 64 ++++- docs/reference/bigframes/enums.rst | 8 + docs/reference/bigframes/exceptions.rst | 8 + docs/reference/bigframes/index.rst | 2 + docs/templates/toc.yml | 4 + tests/system/small/test_dataframe_io.py | 4 +- tests/system/small/test_session.py | 62 ++--- tests/unit/session/test_session.py | 222 +++++++++++++++++- .../bigframes_vendored/pandas/io/gbq.py | 38 ++- .../pandas/io/parsers/readers.py | 9 +- 16 files changed, 478 insertions(+), 83 deletions(-) create mode 100644 bigframes/enums.py create mode 100644 docs/reference/bigframes/enums.rst create mode 100644 docs/reference/bigframes/exceptions.rst diff --git a/bigframes/__init__.py b/bigframes/__init__.py index bd1476957bf..240608ebc2d 100644 --- a/bigframes/__init__.py +++ b/bigframes/__init__.py @@ -17,6 +17,8 @@ from bigframes._config import option_context, options from bigframes._config.bigquery_options import BigQueryOptions from bigframes.core.global_session import close_session, get_global_session +import bigframes.enums as enums +import bigframes.exceptions as exceptions from bigframes.session import connect, Session from bigframes.version import __version__ @@ -25,6 +27,8 @@ "BigQueryOptions", "get_global_session", "close_session", + "enums", + "exceptions", "connect", "Session", "__version__", diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 4ff8a1836b5..402581eb6fd 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -116,10 +116,20 @@ def __init__( raise ValueError( f"'index_columns' (size {len(index_columns)}) and 'index_labels' (size {len(index_labels)}) must have equal length" ) + + # If no index columns are set, create one. + # + # Note: get_index_cols_and_uniqueness in + # bigframes/session/_io/bigquery/read_gbq_table.py depends on this + # being as sequential integer index column. If this default behavior + # ever changes, please also update get_index_cols_and_uniqueness so + # that users who explicitly request a sequential integer index can + # still get one. if len(index_columns) == 0: new_index_col_id = guid.generate_guid() expr = expr.promote_offsets(new_index_col_id) index_columns = [new_index_col_id] + self._index_columns = tuple(index_columns) # Index labels don't need complicated hierarchical access so can store as tuple self._index_labels = ( diff --git a/bigframes/enums.py b/bigframes/enums.py new file mode 100644 index 00000000000..4bec75f5dff --- /dev/null +++ b/bigframes/enums.py @@ -0,0 +1,29 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Public enums used across BigQuery DataFrames.""" + +# NOTE: This module should not depend on any others in the package. + + +import enum + + +class DefaultIndexKind(enum.Enum): + """Sentinel values used to override default indexing behavior.""" + + #: Use consecutive integers as the index. This is ``0``, ``1``, ``2``, ..., + #: ``n - 3``, ``n - 2``, ``n - 1``, where ``n`` is the number of items in + #: the index. + SEQUENTIAL_INT64 = enum.auto() diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 62122e79d2d..d179914983b 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -12,6 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Public exceptions and warnings used across BigQuery DataFrames.""" + +# NOTE: This module should not depend on any others in the package. + class UnknownLocationWarning(Warning): """The location is set to an unknown value.""" + + +class NoDefaultIndexError(ValueError): + """Unable to create a default index.""" diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 48a4b0f68df..ce69f49c892 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -63,6 +63,7 @@ import bigframes.core.reshape import bigframes.core.tools import bigframes.dataframe +import bigframes.enums import bigframes.operations as ops import bigframes.series import bigframes.session @@ -423,7 +424,13 @@ def read_csv( Union[MutableSequence[Any], numpy.ndarray[Any, Any], Tuple[Any, ...], range] ] = None, index_col: Optional[ - Union[int, str, Sequence[Union[str, int]], Literal[False]] + Union[ + int, + str, + Sequence[Union[str, int]], + bigframes.enums.DefaultIndexKind, + Literal[False], + ] ] = None, usecols: Optional[ Union[ @@ -491,7 +498,7 @@ def read_json( def read_gbq( query_or_table: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), configuration: Optional[Dict] = None, max_results: Optional[int] = None, @@ -529,7 +536,7 @@ def read_gbq_model(model_name: str): def read_gbq_query( query: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), configuration: Optional[Dict] = None, max_results: Optional[int] = None, @@ -555,7 +562,7 @@ def read_gbq_query( def read_gbq_table( query: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), max_results: Optional[int] = None, filters: vendored_pandas_gbq.FiltersType = (), diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 0f5aa19592a..6b84d838cf2 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -294,7 +294,7 @@ def read_gbq( self, query_or_table: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), configuration: Optional[Dict] = None, max_results: Optional[int] = None, @@ -313,6 +313,9 @@ def read_gbq( filters = list(filters) if len(filters) != 0 or _is_table_with_wildcard_suffix(query_or_table): + # TODO(b/338111344): This appears to be missing index_cols, which + # are necessary to be selected. + # TODO(b/338039517): Also, need to account for primary keys. query_or_table = self._to_query(query_or_table, columns, filters) if _is_query(query_or_table): @@ -326,9 +329,6 @@ def read_gbq( use_cache=use_cache, ) else: - # TODO(swast): Query the snapshot table but mark it as a - # deterministic query so we can avoid serializing if we have a - # unique index. if configuration is not None: raise ValueError( "The 'configuration' argument is not allowed when " @@ -359,6 +359,8 @@ def _to_query( else f"`{query_or_table}`" ) + # TODO(b/338111344): Generate an index based on DefaultIndexKind if we + # don't have index columns specified. select_clause = "SELECT " + ( ", ".join(f"`{column}`" for column in columns) if columns else "*" ) @@ -488,7 +490,7 @@ def read_gbq_query( self, query: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), configuration: Optional[Dict] = None, max_results: Optional[int] = None, @@ -566,7 +568,7 @@ def _read_gbq_query( self, query: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), configuration: Optional[Dict] = None, max_results: Optional[int] = None, @@ -598,7 +600,9 @@ def _read_gbq_query( True if use_cache is None else use_cache ) - if isinstance(index_col, str): + if isinstance(index_col, bigframes.enums.DefaultIndexKind): + index_cols = [] + elif isinstance(index_col, str): index_cols = [index_col] else: index_cols = list(index_col) @@ -628,7 +632,7 @@ def _read_gbq_query( return self.read_gbq_table( f"{destination.project}.{destination.dataset_id}.{destination.table_id}", - index_col=index_cols, + index_col=index_col, columns=columns, max_results=max_results, use_cache=configuration["query"]["useQueryCache"], @@ -638,7 +642,7 @@ def read_gbq_table( self, query: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), max_results: Optional[int] = None, filters: third_party_pandas_gbq.FiltersType = (), @@ -693,7 +697,7 @@ def _read_gbq_table( self, query: str, *, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), max_results: Optional[int] = None, api_name: str, @@ -821,10 +825,12 @@ def _read_bigquery_load_job( table: Union[bigquery.Table, bigquery.TableReference], *, job_config: bigquery.LoadJobConfig, - index_col: Iterable[str] | str = (), + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), ) -> dataframe.DataFrame: - if isinstance(index_col, str): + if isinstance(index_col, bigframes.enums.DefaultIndexKind): + index_cols = [] + elif isinstance(index_col, str): index_cols = [index_col] else: index_cols = list(index_col) @@ -1113,7 +1119,13 @@ def read_csv( Union[MutableSequence[Any], np.ndarray[Any, Any], Tuple[Any, ...], range] ] = None, index_col: Optional[ - Union[int, str, Sequence[Union[str, int]], Literal[False]] + Union[ + int, + str, + Sequence[Union[str, int]], + bigframes.enums.DefaultIndexKind, + Literal[False], + ] ] = None, usecols: Optional[ Union[ @@ -1143,18 +1155,37 @@ def read_csv( f"{constants.FEEDBACK_LINK}" ) - if index_col is not None and ( - not index_col or not isinstance(index_col, str) + # TODO(b/338089659): Looks like we can relax this 1 column + # restriction if we check the contents of an iterable are strings + # not integers. + if ( + # Empty tuples, None, and False are allowed and falsey. + index_col + and not isinstance(index_col, bigframes.enums.DefaultIndexKind) + and not isinstance(index_col, str) ): raise NotImplementedError( - "BigQuery engine only supports a single column name for `index_col`. " - f"{constants.FEEDBACK_LINK}" + "BigQuery engine only supports a single column name for `index_col`, " + f"got: {repr(index_col)}. {constants.FEEDBACK_LINK}" ) - # None value for index_col cannot be passed to read_gbq - if index_col is None: + # None and False cannot be passed to read_gbq. + # TODO(b/338400133): When index_col is None, we should be using the + # first column of the CSV as the index to be compatible with the + # pandas engine. According to the pandas docs, only "False" + # indicates a default sequential index. + if not index_col: index_col = () + index_col = typing.cast( + Union[ + Sequence[str], # Falsey values + bigframes.enums.DefaultIndexKind, + str, + ], + index_col, + ) + # usecols should only be an iterable of strings (column names) for use as columns in read_gbq. columns: Tuple[Any, ...] = tuple() if usecols is not None: @@ -1199,6 +1230,11 @@ def read_csv( columns=columns, ) else: + if isinstance(index_col, bigframes.enums.DefaultIndexKind): + raise NotImplementedError( + f"With index_col={repr(index_col)}, only engine='bigquery' is supported. " + f"{constants.FEEDBACK_LINK}" + ) if any(arg in kwargs for arg in ("chunksize", "iterator")): raise NotImplementedError( "'chunksize' and 'iterator' arguments are not supported. " diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 3235ca92e5d..29d5a5567f3 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -35,6 +35,7 @@ import bigframes import bigframes.clients +import bigframes.constants import bigframes.core as core import bigframes.core.compile import bigframes.core.guid as guid @@ -206,12 +207,35 @@ def _get_primary_keys( return primary_keys +def _is_table_clustered_or_partitioned( + table: bigquery.table.Table, +) -> bool: + """Returns True if the table is clustered or partitioned.""" + + # Could be None or an empty tuple if it's not clustered, both of which are + # falsey. + if table.clustering_fields: + return True + + if ( + time_partitioning := table.time_partitioning + ) is not None and time_partitioning.type_ is not None: + return True + + if ( + range_partitioning := table.range_partitioning + ) is not None and range_partitioning.field is not None: + return True + + return False + + def get_index_cols_and_uniqueness( bqclient: bigquery.Client, ibis_client: ibis.BaseBackend, table: bigquery.table.Table, table_expression: ibis_types.Table, - index_col: Iterable[str] | str, + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind, api_name: str, ) -> Tuple[List[str], bool]: """ @@ -222,7 +246,23 @@ def get_index_cols_and_uniqueness( # Transform index_col -> index_cols so we have a variable that is # always a list of column names (possibly empty). - if isinstance(index_col, str): + if isinstance(index_col, bigframes.enums.DefaultIndexKind): + if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: + # User has explicity asked for a default, sequential index. + # Use that, even if there are primary keys on the table. + # + # Note: This relies on the default behavior of the Block + # constructor to create a default sequential index. If that ever + # changes, this logic will need to be revisited. + return [], False + else: + # Note: It's actually quite difficult to mock this out to unit + # test, as it's not possible to subclass enums in Python. See: + # https://stackoverflow.com/a/33680021/101923 + raise NotImplementedError( + f"Got unexpected index_col {repr(index_col)}. {bigframes.constants.FEEDBACK_LINK}" + ) + elif isinstance(index_col, str): index_cols: List[str] = [index_col] else: index_cols = list(index_col) @@ -230,14 +270,26 @@ def get_index_cols_and_uniqueness( # If the isn't an index selected, use the primary keys of the table as the # index. If there are no primary keys, we'll return an empty list. if len(index_cols) == 0: - index_cols = _get_primary_keys(table) - - # TODO(b/335727141): If table has clustering/partitioning, fail if - # index_cols is empty. + primary_keys = _get_primary_keys(table) + + # If table has clustering/partitioning, fail if we haven't been able to + # find index_cols to use. This is to avoid unexpected performance and + # resource utilization because of the default sequential index. See + # internal issue 335727141. + if _is_table_clustered_or_partitioned(table) and not primary_keys: + raise bigframes.exceptions.NoDefaultIndexError( + f"Table '{str(table.reference)}' is clustered and/or " + "partitioned, but BigQuery DataFrames was not able to find a " + "suitable index. To avoid this error, set at least one of: " + # TODO(b/338037499): Allow max_results to override this too, + # once we make it more efficient. + "`index_col` or `filters`." + ) # If there are primary keys defined, the query engine assumes these # columns are unique, even if the constraint is not enforced. We make # the same assumption and use these columns as the total ordering keys. + index_cols = primary_keys is_index_unique = len(index_cols) != 0 else: is_index_unique = _check_index_uniqueness( diff --git a/docs/reference/bigframes/enums.rst b/docs/reference/bigframes/enums.rst new file mode 100644 index 00000000000..b0a198e1842 --- /dev/null +++ b/docs/reference/bigframes/enums.rst @@ -0,0 +1,8 @@ + +===== +Enums +===== + +.. automodule:: bigframes.enums + :members: + :undoc-members: diff --git a/docs/reference/bigframes/exceptions.rst b/docs/reference/bigframes/exceptions.rst new file mode 100644 index 00000000000..c471aecdf75 --- /dev/null +++ b/docs/reference/bigframes/exceptions.rst @@ -0,0 +1,8 @@ + +======================= +Exceptions and Warnings +======================= + +.. automodule:: bigframes.exceptions + :members: + :undoc-members: diff --git a/docs/reference/bigframes/index.rst b/docs/reference/bigframes/index.rst index d26db18c969..f56883dc8e3 100644 --- a/docs/reference/bigframes/index.rst +++ b/docs/reference/bigframes/index.rst @@ -6,6 +6,8 @@ Core objects .. toctree:: :maxdepth: 2 + enums + exceptions options diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 80ccc01fac6..67e628eb7dc 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -32,6 +32,10 @@ - name: Session uid: bigframes.session.Session name: Session + - name: Enumerations + uid: bigframes.enums + - name: Exceptions and warnings + uid: bigframes.exceptions name: Core Objects - items: - name: DataFrame diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index f26902f0844..f36dd64cbe4 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -117,8 +117,8 @@ def test_to_pandas_batches_w_correct_dtypes(scalars_df_default_index): @pytest.mark.parametrize( - ("index"), - [True, False], + ("index",), + [(True,), (False,)], ) def test_to_csv_index( scalars_dfs: Tuple[bigframes.dataframe.DataFrame, pd.DataFrame], diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 1e76a8bd8b9..2779874d6c8 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -524,7 +524,11 @@ def test_read_csv_gcs_bq_engine(session, scalars_dfs, gcs_folder): scalars_df, _ = scalars_dfs path = gcs_folder + "test_read_csv_gcs_bq_engine_w_index*.csv" scalars_df.to_csv(path, index=False) - df = session.read_csv(path, engine="bigquery") + df = session.read_csv( + path, + engine="bigquery", + index_col=bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64, + ) # TODO(chelsealin): If we serialize the index, can more easily compare values. pd.testing.assert_index_equal(df.columns, scalars_df.columns) @@ -629,44 +633,24 @@ def test_read_csv_localbuffer_bq_engine(session, scalars_dfs): pd.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) -@pytest.mark.parametrize( - ("kwargs", "match"), - [ - pytest.param( - {"engine": "bigquery", "names": []}, - "BigQuery engine does not support these arguments", - id="with_names", - ), - pytest.param( - {"engine": "bigquery", "dtype": {}}, - "BigQuery engine does not support these arguments", - id="with_dtype", - ), - pytest.param( - {"engine": "bigquery", "index_col": False}, - "BigQuery engine only supports a single column name for `index_col`.", - id="with_index_col_false", - ), - pytest.param( - {"engine": "bigquery", "index_col": 5}, - "BigQuery engine only supports a single column name for `index_col`.", - id="with_index_col_not_str", - ), - pytest.param( - {"engine": "bigquery", "usecols": [1, 2]}, - "BigQuery engine only supports an iterable of strings for `usecols`.", - id="with_usecols_invalid", - ), - pytest.param( - {"engine": "bigquery", "encoding": "ASCII"}, - "BigQuery engine only supports the following encodings", - id="with_encoding_invalid", - ), - ], -) -def test_read_csv_bq_engine_throws_not_implemented_error(session, kwargs, match): - with pytest.raises(NotImplementedError, match=match): - session.read_csv("", **kwargs) +def test_read_csv_bq_engine_supports_index_col_false( + session, scalars_df_index, gcs_folder +): + path = gcs_folder + "test_read_csv_bq_engine_supports_index_col_false*.csv" + read_path = utils.get_first_file_from_wildcard(path) + scalars_df_index.to_csv(path) + + df = session.read_csv( + read_path, + # Normally, pandas uses the first column as the index. index_col=False + # turns off that behavior. + index_col=False, + ) + assert df.shape[0] == scalars_df_index.shape[0] + + # We use a default index because of index_col=False, so the previous index + # column is just loaded as a column. + assert len(df.columns) == len(scalars_df_index.columns) + 1 @pytest.mark.parametrize( diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 34f185cafd0..70a121435c1 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import datetime import os import re @@ -23,10 +24,129 @@ import pytest import bigframes +import bigframes.enums import bigframes.exceptions from .. import resources +TABLE_REFERENCE = { + "projectId": "my-project", + "datasetId": "my_dataset", + "tableId": "my_table", +} +CLUSTERED_OR_PARTITIONED_TABLES = [ + pytest.param( + google.cloud.bigquery.Table.from_api_repr( + { + "tableReference": TABLE_REFERENCE, + "clustering": { + "fields": ["col1", "col2"], + }, + }, + ), + id="clustered", + ), + pytest.param( + google.cloud.bigquery.Table.from_api_repr( + { + "tableReference": TABLE_REFERENCE, + "rangePartitioning": { + "field": "col1", + "range": { + "start": 1, + "end": 100, + "interval": 1, + }, + }, + }, + ), + id="range-partitioned", + ), + pytest.param( + google.cloud.bigquery.Table.from_api_repr( + { + "tableReference": TABLE_REFERENCE, + "timePartitioning": { + "type": "MONTH", + "field": "col1", + }, + }, + ), + id="time-partitioned", + ), + pytest.param( + google.cloud.bigquery.Table.from_api_repr( + { + "tableReference": TABLE_REFERENCE, + "clustering": { + "fields": ["col1", "col2"], + }, + "timePartitioning": { + "type": "MONTH", + "field": "col1", + }, + }, + ), + id="time-partitioned-and-clustered", + ), +] + + +@pytest.mark.parametrize( + ("kwargs", "match"), + [ + pytest.param( + {"engine": "bigquery", "names": []}, + "BigQuery engine does not support these arguments", + id="with_names", + ), + pytest.param( + {"engine": "bigquery", "dtype": {}}, + "BigQuery engine does not support these arguments", + id="with_dtype", + ), + pytest.param( + {"engine": "bigquery", "index_col": 5}, + "BigQuery engine only supports a single column name for `index_col`.", + id="with_index_col_not_str", + ), + pytest.param( + {"engine": "bigquery", "usecols": [1, 2]}, + "BigQuery engine only supports an iterable of strings for `usecols`.", + id="with_usecols_invalid", + ), + pytest.param( + {"engine": "bigquery", "encoding": "ASCII"}, + "BigQuery engine only supports the following encodings", + id="with_encoding_invalid", + ), + ], +) +def test_read_csv_bq_engine_throws_not_implemented_error(kwargs, match): + session = resources.create_bigquery_session() + + with pytest.raises(NotImplementedError, match=match): + session.read_csv("", **kwargs) + + +@pytest.mark.parametrize( + ("engine",), + ( + ("c",), + ("python",), + ("pyarrow",), + ), +) +def test_read_csv_pandas_engines_index_col_sequential_int64_not_supported(engine): + session = resources.create_bigquery_session() + + with pytest.raises(NotImplementedError, match="index_col"): + session.read_csv( + "path/to/csv.csv", + engine=engine, + index_col=bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64, + ) + @pytest.mark.parametrize("missing_parts_table_id", [(""), ("table")]) def test_read_gbq_missing_parts(missing_parts_table_id): @@ -65,14 +185,109 @@ def get_table_mock(table_ref): assert "1999-01-02T03:04:05.678901" in df.sql -def test_read_gbq_clustered_table_ok_default_index_with_primary_key(): +@pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) +def test_no_default_index_error_raised_by_read_gbq(table): + """Because of the windowing operation to create a default index, row + filters can't push down to the clustering column. + + Raise an exception in this case so that the user is directed to supply a + unique index column or filter if possible. + + See internal issue 335727141. + """ + table = copy.deepcopy(table) + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + bqclient.get_table.return_value = table + session = resources.create_bigquery_session(bqclient=bqclient) + table._properties["location"] = session._location + + with pytest.raises(bigframes.exceptions.NoDefaultIndexError): + session.read_gbq("my-project.my_dataset.my_table") + + +@pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) +def test_no_default_index_error_not_raised_by_read_gbq_index_col_sequential_int64( + table, +): + """Because of the windowing operation to create a default index, row + filters can't push down to the clustering column. + + Allow people to use the default index only if they explicitly request it. + + See internal issue 335727141. + """ + table = copy.deepcopy(table) + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + bqclient.get_table.return_value = table + session = resources.create_bigquery_session(bqclient=bqclient) + table._properties["location"] = session._location + + # No exception raised because we set the option allowing the default indexes. + df = session.read_gbq( + "my-project.my_dataset.my_table", + index_col=bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64, + ) + + # We expect a window operation because we specificaly requested a sequential index. + generated_sql = df.sql.casefold() + assert "OVER".casefold() in generated_sql + assert "ROW_NUMBER()".casefold() in generated_sql + + +@pytest.mark.parametrize( + ("total_count", "distinct_count"), + ( + (0, 0), + (123, 123), + # Should still have a positive effect, even if the index is not unique. + (123, 111), + ), +) +@pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) +def test_no_default_index_error_not_raised_by_read_gbq_index_col_columns( + total_count, + distinct_count, + table, +): + table = copy.deepcopy(table) + table.schema = ( + google.cloud.bigquery.SchemaField("idx_1", "INT64"), + google.cloud.bigquery.SchemaField("idx_2", "INT64"), + google.cloud.bigquery.SchemaField("col_1", "INT64"), + google.cloud.bigquery.SchemaField("col_2", "INT64"), + ) + + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + bqclient.get_table.return_value = table + bqclient.query_and_wait.return_value = ( + {"total_count": total_count, "distinct_count": distinct_count}, + ) + session = resources.create_bigquery_session( + bqclient=bqclient, table_schema=table.schema + ) + table._properties["location"] = session._location + + # No exception raised because there are columns to use as the index. + df = session.read_gbq( + "my-project.my_dataset.my_table", index_col=("idx_1", "idx_2") + ) + + # There should be no analytic operators to prevent row filtering pushdown. + assert "OVER" not in df.sql + assert tuple(df.index.names) == ("idx_1", "idx_2") + + +@pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) +def test_no_default_index_error_not_raised_by_read_gbq_primary_key(table): """If a primary key is set on the table, we use that as the index column by default, no error should be raised in this case. See internal issue 335727141. """ - table = google.cloud.bigquery.Table("my-project.my_dataset.my_table") - table.clustering_fields = ["col1", "col2"] + table = copy.deepcopy(table) table.schema = ( google.cloud.bigquery.SchemaField("pk_1", "INT64"), google.cloud.bigquery.SchemaField("pk_2", "INT64"), @@ -95,6 +310,7 @@ def test_read_gbq_clustered_table_ok_default_index_with_primary_key(): ) table._properties["location"] = session._location + # No exception raised because there is a primary key to use as the index. df = session.read_gbq("my-project.my_dataset.my_table") # There should be no analytic operators to prevent row filtering pushdown. diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 93cee71289c..c25dd8776f1 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Iterable, Literal, Optional, Tuple, Union from bigframes import constants +import bigframes.enums FilterOps = Literal["in", "not in", "<", "<=", "==", "!=", ">=", ">", "LIKE"] FilterType = Tuple[str, FilterOps, Any] @@ -17,7 +18,7 @@ def read_gbq( self, query_or_table: str, *, - index_col: Iterable[str] | str = (), + index_col: Union[Iterable[str], str, bigframes.enums.DefaultIndexKind] = (), columns: Iterable[str] = (), configuration: Optional[Dict] = None, max_results: Optional[int] = None, @@ -28,16 +29,23 @@ def read_gbq( """Loads a DataFrame from BigQuery. BigQuery tables are an unordered, unindexed data source. To add support - pandas-compatibility, the following indexing options are supported: - - * (Default behavior) Add an arbitrary sequential index and ordering - using an an analytic windowed operation that prevents filtering - push down. + pandas-compatibility, the following indexing options are supported via + the ``index_col`` parameter: + + * (Empty iterable, default) A default index. **Behavior may change.** + Explicitly set ``index_col`` if your application makes use of + specific index values. + + If a table has primary key(s), those are used as the index, + otherwise a sequential index is generated. + * (:attr:`bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64`) Add an + arbitrary sequential index and ordering. **Warning** This uses an + analytic windowed operation that prevents filtering push down. Avoid + using on large clustered or partitioned tables. * (Recommended) Set the ``index_col`` argument to one or more columns. Unique values for the row labels are recommended. Duplicate labels are possible, but note that joins on a non-unique index can duplicate - rows and operations like ``cumsum()`` that window across a non-unique - index can have some non-deternimism. + rows via pandas-like outer join behavior. .. note:: By default, even SQL query inputs with an ORDER BY clause create a @@ -107,11 +115,18 @@ def read_gbq( `project.dataset.tablename` or `dataset.tablename`. Can also take wildcard table name, such as `project.dataset.table_prefix*`. In tha case, will read all the matched table as one DataFrame. - index_col (Iterable[str] or str): + index_col (Iterable[str], str, bigframes.enums.DefaultIndexKind): Name of result column(s) to use for index in results DataFrame. + If an empty iterable, such as ``()``, a default index is + generated. Do not depend on specific index values in this case. + **New in bigframes version 1.3.0**: If ``index_cols`` is not set, the primary key(s) of the table are used as the index. + + **New in bigframes version 1.4.0**: Support + :class:`bigframes.enums.DefaultIndexKind` to override default index + behavior. columns (Iterable[str]): List of BigQuery column names in the desired order for results DataFrame. @@ -141,6 +156,11 @@ def read_gbq( col_order (Iterable[str]): Alias for columns, retained for backwards compatibility. + Raises: + bigframes.exceptions.NoDefaultIndexError: + Using the default index is discouraged, such as with clustered + or partitioned tables without primary keys. + Returns: bigframes.dataframe.DataFrame: A DataFrame representing results of the query or table. """ diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py index e8ed6182a6f..d147abfd228 100644 --- a/third_party/bigframes_vendored/pandas/io/parsers/readers.py +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -21,6 +21,7 @@ import numpy as np from bigframes import constants +import bigframes.enums class ReaderIOMixin: @@ -34,7 +35,13 @@ def read_csv( Union[MutableSequence[Any], np.ndarray[Any, Any], Tuple[Any, ...], range] ] = None, index_col: Optional[ - Union[int, str, Sequence[Union[str, int]], Literal[False]] + Union[ + int, + str, + Sequence[Union[str, int]], + bigframes.enums.DefaultIndexKind, + Literal[False], + ] ] = None, usecols=None, dtype: Optional[Dict] = None, From c8d4e231fe8263f5b10fae9b879ff82df58da534 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 2 May 2024 18:10:19 -0700 Subject: [PATCH 541/921] feat: Add a unique session_id to Session and allow cleaning up sessions (#553) - temporary tables will have the session id in their names - session.close() will delete temporary tables that were created using the session. - add pandas.get_default_session_id() - add manual_cleanup_by_session_id(session_id: str). this is slow but allows users to clean up if they lost the session object --- bigframes/core/global_session.py | 20 +++- bigframes/dataframe.py | 6 +- bigframes/exceptions.py | 4 + bigframes/pandas/__init__.py | 62 +++++++++++ bigframes/session/__init__.py | 84 ++++++++++----- bigframes/session/_io/bigquery/__init__.py | 102 +++++++++++++----- tests/system/conftest.py | 14 ++- tests/system/large/test_session.py | 81 +++++++++++++- tests/system/small/test_encryption.py | 8 +- tests/system/small/test_pandas_options.py | 11 +- tests/unit/session/test_io_bigquery.py | 10 +- .../bigframes_vendored/pandas/core/frame.py | 2 +- .../pandas/plotting/_core.py | 2 + 13 files changed, 330 insertions(+), 76 deletions(-) diff --git a/bigframes/core/global_session.py b/bigframes/core/global_session.py index 1f960839a0a..31dfc9bd172 100644 --- a/bigframes/core/global_session.py +++ b/bigframes/core/global_session.py @@ -15,7 +15,11 @@ """Utilities for managing a default, globally available Session object.""" import threading +import traceback from typing import Callable, Optional, TypeVar +import warnings + +import google.auth.exceptions import bigframes._config import bigframes.session @@ -27,7 +31,8 @@ def close_session() -> None: """Start a fresh session the next time a function requires a session. - Closes the current session if it was already started. + Closes the current session if it was already started, deleting any + temporary tables that were created. Returns: None @@ -36,7 +41,18 @@ def close_session() -> None: with _global_session_lock: if _global_session is not None: - _global_session.close() + try: + _global_session.close() + except google.auth.exceptions.RefreshError as e: + session_id = _global_session.session_id + location = _global_session._location + project_id = _global_session._project + warnings.warn( + f"Session cleanup failed for session with id: {session_id}, " + f"location: {location}, project: {project_id}", + category=bigframes.exceptions.CleanupFailedWarning, + ) + traceback.print_tb(e.__traceback__) _global_session = None bigframes._config.options.bigquery._session_started = False diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 092c8ab82fe..d694216ebe7 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2928,8 +2928,10 @@ def to_gbq( ) if_exists = "replace" - temp_table_ref = bigframes.session._io.bigquery.random_table( - self._session._anonymous_dataset + temp_table_ref = self._session._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, ) destination_table = f"{temp_table_ref.project}.{temp_table_ref.dataset_id}.{temp_table_ref.table_id}" diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index d179914983b..222df069f65 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -21,5 +21,9 @@ class UnknownLocationWarning(Warning): """The location is set to an unknown value.""" +class CleanupFailedWarning(Warning): + """Bigframes failed to clean up a table resource.""" + + class NoDefaultIndexError(ValueError): """Unable to create a default index.""" diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index ce69f49c892..fc87bec3dfc 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -706,6 +706,68 @@ def to_datetime( to_datetime.__doc__ = vendored_pandas_datetimes.to_datetime.__doc__ +def get_default_session_id() -> str: + """Gets the session id that is used whenever a custom session + has not been provided. + + It is the session id of the default global session. It is prefixed to + the table id of all temporary tables created in the global session. + + Returns: + str, the default global session id, ex. 'sessiona1b2c' + """ + return get_global_session().session_id + + +def clean_up_by_session_id( + session_id: str, + location: Optional[str] = None, + project: Optional[str] = None, +) -> None: + """Searches through table names in BigQuery and deletes tables + found matching the expected format. + + This could be useful if the session object has been lost. + Calling `session.close()` or `bigframes.pandas.close_session()` + is preferred in most cases. + + Args: + session_id (str): + The session id to clean up. Can be found using + session.session_id or get_default_session_id(). + + location (str, default None): + The location of the session to clean up. If given, used + together with project kwarg to determine the dataset + to search through for tables to clean up. + + project (str, default None): + The project id associated with the session to clean up. + If given, used together with location kwarg to determine + the dataset to search through for tables to clean up. + + Returns: + None + """ + session = get_global_session() + client = session.bqclient + + if (location is None) != (project is None): + raise ValueError( + "Only one of project or location was given. Must specify both or neither." + ) + elif location is None and project is None: + dataset = session._anonymous_dataset + else: + dataset = bigframes.session._io.bigquery.create_bq_dataset_reference( + client, location=location, project=project + ) + + bigframes.session._io.bigquery.delete_tables_matching_session_id( + client, dataset, session_id + ) + + # pandas dtype attributes NA = pandas.NA BooleanDtype = pandas.BooleanDtype diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 6b84d838cf2..2425369edd9 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -21,6 +21,7 @@ import logging import os import re +import secrets import typing from typing import ( Any, @@ -37,6 +38,7 @@ Tuple, Union, ) +import uuid import warnings # Even though the ibis.backends.bigquery import is unused, it's needed @@ -100,6 +102,8 @@ _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" +_TEMP_TABLE_ID_FORMAT = "bqdf{date}_{session_id}_{random_id}" + _MAX_CLUSTER_COLUMNS = 4 # TODO(swast): Need to connect to regional endpoints when performing remote @@ -203,7 +207,11 @@ def __init__( bq_kms_key_name=self._bq_kms_key_name, ) - self._create_bq_datasets() + self._anonymous_dataset = ( + bigframes.session._io.bigquery.create_bq_dataset_reference( + self.bqclient, location=self._location + ) + ) # TODO(shobs): Remove this logic after https://github.com/ibis-project/ibis/issues/8494 # has been fixed. The ibis client changes the default query job config @@ -233,6 +241,13 @@ def __init__( bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table] ] = {} + # unique session identifier, short enough to be human readable + # only needs to be unique among sessions created by the same user + # at the same time in the same region + self._session_id: str = "session" + secrets.token_hex(3) + self._table_ids: List[str] = [] + # store table ids and delete them when the session is closed + @property def bqclient(self): return self._clients_provider.bqclient @@ -263,6 +278,10 @@ def bqconnectionmanager(self): ) return self._bq_connection_manager + @property + def session_id(self): + return self._session_id + @property def _project(self): return self.bqclient.project @@ -271,24 +290,15 @@ def __hash__(self): # Stable hash needed to use in expression tree return hash(str(self._anonymous_dataset)) - def _create_bq_datasets(self): - """Create and identify dataset(s) for temporary BQ resources.""" - query_job = self.bqclient.query("SELECT 1", location=self._location) - query_job.result() # blocks until finished - - # The anonymous dataset is used by BigQuery to write query results and - # session tables. BigQuery DataFrames also writes temp tables directly - # to the dataset, no BigQuery Session required. Note: there is a - # different anonymous dataset per location. See: - # https://cloud.google.com/bigquery/docs/cached-results#how_cached_results_are_stored - query_destination = query_job.destination - self._anonymous_dataset = bigquery.DatasetReference( - query_destination.project, - query_destination.dataset_id, - ) - def close(self): - """No-op. Temporary resources are deleted after 7 days.""" + """Delete tables that were created with this session's session_id.""" + client = self.bqclient + project_id = self._anonymous_dataset.project + dataset_id = self._anonymous_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) def read_gbq( self, @@ -1063,7 +1073,7 @@ def _read_pandas_load_job( job_config.labels = {"bigframes-api": api_name} - load_table_destination = bigframes_io.random_table(self._anonymous_dataset) + load_table_destination = self._random_table() load_job = self.bqclient.load_table_from_dataframe( pandas_dataframe_copy, load_table_destination, @@ -1145,7 +1155,7 @@ def read_csv( encoding: Optional[str] = None, **kwargs, ) -> dataframe.DataFrame: - table = bigframes_io.random_table(self._anonymous_dataset) + table = self._random_table() if engine is not None and engine == "bigquery": if any(param is not None for param in (dtype, names)): @@ -1282,7 +1292,7 @@ def read_parquet( *, engine: str = "auto", ) -> dataframe.DataFrame: - table = bigframes_io.random_table(self._anonymous_dataset) + table = self._random_table() if engine == "bigquery": job_config = self._prepare_load_job_config() @@ -1319,7 +1329,7 @@ def read_json( engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", **kwargs, ) -> dataframe.DataFrame: - table = bigframes_io.random_table(self._anonymous_dataset) + table = self._random_table() if engine == "bigquery": @@ -1416,14 +1426,12 @@ def _create_empty_temp_table( ) -> bigquery.TableReference: # Can't set a table in _SESSION as destination via query job API, so we # run DDL, instead. - dataset = self._anonymous_dataset expiration = ( datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION ) table = bigframes_io.create_temp_table( - self.bqclient, - dataset, + self, expiration, schema=schema, cluster_columns=cluster_cols, @@ -1939,6 +1947,32 @@ def _start_generic_job(self, job: formatting_helpers.GenericJob): else: job.result() + def _random_table(self, skip_cleanup: bool = False) -> 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. + + Args: + skip_cleanup (bool, default False): + If True, do not add the generated ID to the list of tables + to clean up when the session is closed. + + Returns: + google.cloud.bigquery.TableReference: + Fully qualified table ID of a table that doesn't exist. + """ + dataset = self._anonymous_dataset + session_id = self.session_id + now = datetime.datetime.now(datetime.timezone.utc) + random_id = uuid.uuid4().hex + table_id = _TEMP_TABLE_ID_FORMAT.format( + date=now.strftime("%Y%m%d"), session_id=session_id, random_id=random_id + ) + if not skip_cleanup: + self._table_ids.append(table_id) + return dataset.table(table_id) + def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: return Session(context) diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 2cd2d8ff9a8..79108c71a29 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -22,7 +22,6 @@ import textwrap import types from typing import Dict, Iterable, Optional, Sequence, Tuple, Union -import uuid import google.api_core.exceptions import google.cloud.bigquery as bigquery @@ -33,7 +32,8 @@ IO_ORDERING_ID = "bqdf_row_nums" MAX_LABELS_COUNT = 64 -TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}" +_LIST_TABLES_LIMIT = 10000 # calls to bqclient.list_tables +# will be limited to this many tables LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" @@ -98,39 +98,25 @@ def create_export_data_statement( ) -def random_table(dataset: bigquery.DatasetReference) -> bigquery.TableReference: - """Generate a random table ID with BigQuery DataFrames prefix. - Args: - dataset (google.cloud.bigquery.DatasetReference): - The dataset to make the table reference in. Usually the anonymous - dataset for the session. - Returns: - google.cloud.bigquery.TableReference: - Fully qualified table ID of a table that doesn't exist. - """ - now = datetime.datetime.now(datetime.timezone.utc) - random_id = uuid.uuid4().hex - table_id = TEMP_TABLE_PREFIX.format( - date=now.strftime("%Y%m%d"), random_id=random_id - ) - return dataset.table(table_id) - - def table_ref_to_sql(table: bigquery.TableReference) -> str: """Format a table reference as escaped SQL.""" return f"`{table.project}`.`{table.dataset_id}`.`{table.table_id}`" def create_temp_table( - bqclient: bigquery.Client, - dataset: bigquery.DatasetReference, + session: bigframes.session.Session, expiration: datetime.datetime, *, schema: Optional[Iterable[bigquery.SchemaField]] = None, cluster_columns: Optional[list[str]] = None, ) -> str: - """Create an empty table with an expiration in the desired dataset.""" - table_ref = random_table(dataset) + """Create an empty table with an expiration in the desired session. + + The table will be deleted when the session is closed or the expiration + is reached. + """ + bqclient: bigquery.Client = session.bqclient + table_ref = session._random_table() destination = bigquery.Table(table_ref) destination.expires = expiration destination.schema = schema @@ -257,3 +243,71 @@ def pytest_log_job(query_job: bigquery.QueryJob): bytes_file = os.path.join(current_directory, test_name + ".bytesprocessed") with open(bytes_file, "a") as f: f.write(str(bytes_processed) + "\n") + + +def delete_tables_matching_session_id( + client: bigquery.Client, dataset: bigquery.DatasetReference, session_id: str +) -> None: + """Searches within the dataset for tables conforming to the + expected session_id form, and instructs bigquery to delete them. + + Args: + client (bigquery.Client): + The client to use to list tables + dataset (bigquery.DatasetReference): + The dataset to search in + session_id (str): + The session id to match on in the table name + + Returns: + None + """ + + tables = client.list_tables( + dataset, max_results=_LIST_TABLES_LIMIT, page_size=_LIST_TABLES_LIMIT + ) + for table in tables: + split_id = table.table_id.split("_") + if not split_id[0].startswith("bqdf") or len(split_id) < 2: + continue + found_session_id = split_id[1] + if found_session_id == session_id: + client.delete_table(table, not_found_ok=True) + print("Deleting temporary table '{}'.".format(table.table_id)) + + +def create_bq_dataset_reference( + bq_client: bigquery.Client, location=None, project=None +) -> bigquery.DatasetReference: + """Create and identify dataset(s) for temporary BQ resources. + + bq_client project and location will be used unless kwargs "project" + and/or "location" are given. If given, location and project + will be passed through to + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + + Args: + bq_client (bigquery.Client): + The bigquery.Client to use for the http request to + create the dataset reference. + location (str, default None): + The location of the project to create the dataset in. + project (str, default None): + The project id of the project to create the dataset in. + + Returns: + bigquery.DatasetReference: The constructed reference to the anonymous dataset. + """ + query_job = bq_client.query("SELECT 1", location=location, project=project) + query_job.result() # blocks until finished + + # The anonymous dataset is used by BigQuery to write query results and + # session tables. BigQuery DataFrames also writes temp tables directly + # to the dataset, no BigQuery Session required. Note: there is a + # different anonymous dataset per location. See: + # https://cloud.google.com/bigquery/docs/cached-results#how_cached_results_are_stored + query_destination = query_job.destination + return bigquery.DatasetReference( + query_destination.project, + query_destination.dataset_id, + ) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index a9fb8cb3f58..4ebb3cb93b4 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -19,7 +19,7 @@ import pathlib import textwrap import typing -from typing import Dict, Optional +from typing import Dict, Generator, Optional import google.api_core.exceptions import google.cloud.bigquery as bigquery @@ -127,19 +127,23 @@ def resourcemanager_client( @pytest.fixture(scope="session") -def session() -> bigframes.Session: +def session() -> Generator[bigframes.Session, None, None]: context = bigframes.BigQueryOptions( location="US", ) - return bigframes.Session(context=context) + session = bigframes.Session(context=context) + yield session + session.close() # close generated session at cleanup time @pytest.fixture(scope="session") -def session_tokyo(tokyo_location: str) -> bigframes.Session: +def session_tokyo(tokyo_location: str) -> Generator[bigframes.Session, None, None]: context = bigframes.BigQueryOptions( location=tokyo_location, ) - return bigframes.Session(context=context) + session = bigframes.Session(context=context) + yield session + session.close() # close generated session at cleanup type @pytest.fixture(scope="session", autouse=True) diff --git a/tests/system/large/test_session.py b/tests/system/large/test_session.py index 62fa5a83d33..c7a19dc26eb 100644 --- a/tests/system/large/test_session.py +++ b/tests/system/large/test_session.py @@ -12,9 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime + +import google.cloud.exceptions import pytest -from bigframes import Session +import bigframes +import bigframes.pandas as bpd @pytest.mark.parametrize( @@ -46,7 +50,80 @@ # ), ], ) -def test_read_gbq_for_large_tables(session: Session, query_or_table, index_col): +def test_read_gbq_for_large_tables( + session: bigframes.Session, query_or_table, index_col +): """Verify read_gbq() is able to read large tables.""" df = session.read_gbq(query_or_table, index_col=index_col) assert len(df.columns) != 0 + + +def test_close(session): + # we will create two tables and confirm that they are deleted + # when the session is closed + + bqclient = session.bqclient + + expiration = ( + datetime.datetime.now(datetime.timezone.utc) + + bigframes.constants.DEFAULT_EXPIRATION + ) + full_id_1 = bigframes.session._io.bigquery.create_temp_table(session, expiration) + full_id_2 = bigframes.session._io.bigquery.create_temp_table(session, expiration) + + # check that the tables were actually created + assert bqclient.get_table(full_id_1).created is not None + assert bqclient.get_table(full_id_2).created is not None + + session.close() + + # check that the tables are already deleted + with pytest.raises(google.cloud.exceptions.NotFound): + bqclient.delete_table(full_id_1) + with pytest.raises(google.cloud.exceptions.NotFound): + bqclient.delete_table(full_id_2) + + +def test_clean_up_by_session_id(): + # we do this test in a different region in order to avoid + # overly large amounts of temp tables slowing the test down + option_context = bigframes.BigQueryOptions() + option_context.location = "europe-west10" + session = bigframes.Session(context=option_context) + session_id = session.session_id + + # we will create two tables and confirm that they are deleted + # when the session is closed by id + + bqclient = session.bqclient + dataset = session._anonymous_dataset + expiration = ( + datetime.datetime.now(datetime.timezone.utc) + + bigframes.constants.DEFAULT_EXPIRATION + ) + bigframes.session._io.bigquery.create_temp_table(session, expiration) + bigframes.session._io.bigquery.create_temp_table(session, expiration) + + # check that some table exists with the expected session_id + tables_before = bqclient.list_tables( + dataset, + max_results=bigframes.session._io.bigquery._LIST_TABLES_LIMIT, + page_size=bigframes.session._io.bigquery._LIST_TABLES_LIMIT, + ) + assert any( + [(session.session_id in table.full_table_id) for table in list(tables_before)] + ) + + bpd.clean_up_by_session_id( + session_id, location=session._location, project=session._project + ) + + # check that no tables with the session_id are left after cleanup + tables_after = bqclient.list_tables( + dataset, + max_results=bigframes.session._io.bigquery._LIST_TABLES_LIMIT, + page_size=bigframes.session._io.bigquery._LIST_TABLES_LIMIT, + ) + assert not any( + [(session.session_id in table.full_table_id) for table in list(tables_after)] + ) diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index eae667dc9d7..fcaca7a493b 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -86,9 +86,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 = bigframes.session._io.bigquery.random_table( - session_with_bq_cmek._anonymous_dataset - ) + load_table = session_with_bq_cmek._random_table() df = pandas.DataFrame({"col0": [1, 2, 3]}) load_job_config = session_with_bq_cmek._prepare_load_job_config() @@ -188,9 +186,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 = bigframes.session._io.bigquery.random_table( - session_with_bq_cmek._anonymous_dataset - ) + output_table_ref = session_with_bq_cmek._random_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) diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index c410d70fe73..dd13196981c 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -14,6 +14,7 @@ import datetime from unittest import mock +import warnings import google.api_core.exceptions import google.auth @@ -254,7 +255,7 @@ def test_read_gbq_must_comply_with_set_location_non_US( assert df is not None -def test_close_session_after_credentials_need_reauthentication(monkeypatch): +def test_credentials_need_reauthentication(monkeypatch): # Use a simple test query to verify that default session works to interact # with BQ test_query = "SELECT 1" @@ -288,8 +289,12 @@ def test_close_session_after_credentials_need_reauthentication(monkeypatch): with pytest.raises(google.auth.exceptions.RefreshError): bpd.read_gbq(test_query) - # Now verify that closing the session works - bpd.close_session() + # Now verify that closing the session works and we throw + # the expected warning + with warnings.catch_warnings(record=True) as warned: + bpd.close_session() # CleanupFailedWarning: can't clean up + assert len(warned) == 1 + assert warned[0].category == bigframes.exceptions.CleanupFailedWarning assert bigframes.core.global_session._global_session is None # Now verify that use is able to start over diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index eed1acb5a38..43865fc2c85 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -14,7 +14,6 @@ import datetime from typing import Iterable -import unittest.mock as mock import google.cloud.bigquery as bigquery import pytest @@ -139,16 +138,15 @@ def test_create_job_configs_labels_length_limit_met(): def test_create_temp_table_default_expiration(): """Make sure the created table has an expiration.""" - bqclient = mock.create_autospec(bigquery.Client) - dataset = bigquery.DatasetReference("test-project", "test_dataset") expiration = datetime.datetime( 2023, 11, 2, 13, 44, 55, 678901, datetime.timezone.utc ) - bigframes.session._io.bigquery.create_temp_table(bqclient, dataset, expiration) + session = resources.create_bigquery_session() + bigframes.session._io.bigquery.create_temp_table(session, expiration) - bqclient.create_table.assert_called_once() - call_args = bqclient.create_table.call_args + session.bqclient.create_table.assert_called_once() + call_args = session.bqclient.create_table.call_args table = call_args.args[0] assert table.project == "test-project" assert table.dataset_id == "test_dataset" diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index c5168cd160d..4e17bca54da 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -411,7 +411,7 @@ def to_gbq( >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) >>> destination = df.to_gbq(ordering_id="ordering_id") >>> # The table created can be read outside of the current session. - >>> bpd.close_session() # For demonstration, only. + >>> bpd.close_session() # Optional, to demonstrate a new session. >>> bpd.read_gbq(destination, index_col="ordering_id") col1 col2 ordering_id diff --git a/third_party/bigframes_vendored/pandas/plotting/_core.py b/third_party/bigframes_vendored/pandas/plotting/_core.py index bf016357a6f..2409068fa81 100644 --- a/third_party/bigframes_vendored/pandas/plotting/_core.py +++ b/third_party/bigframes_vendored/pandas/plotting/_core.py @@ -57,6 +57,7 @@ def hist( **Examples:** >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None >>> import numpy as np >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame(np.random.randint(1, 7, 6000), columns=['one']) @@ -235,6 +236,7 @@ def scatter( in a DataFrame's columns. >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame([[5.1, 3.5, 0], [4.9, 3.0, 0], [7.0, 3.2, 1], ... [6.4, 3.2, 1], [5.9, 3.0, 2]], ... columns=['length', 'width', 'species']) From 36578ab431119f71dda746de415d0c6417bb4de2 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 3 May 2024 02:33:46 +0000 Subject: [PATCH 542/921] feat: support gcf max instance count in `remote_function` (#657) * feat: support gcf max instance count in `remote_function` * fix comment in test * enable back the retry annotation --- bigframes/functions/remote_function.py | 32 ++++++++++++++--- bigframes/pandas/__init__.py | 2 ++ bigframes/session/__init__.py | 10 ++++++ tests/system/large/test_remote_function.py | 40 ++++++++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index f7237c564c6..9d826d0fa17 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -342,7 +342,12 @@ def generate_cloud_function_code(self, def_, dir, package_requirements=None): return entry_point def create_cloud_function( - self, def_, cf_name, package_requirements=None, cloud_function_timeout=600 + self, + def_, + cf_name, + package_requirements=None, + timeout_seconds=600, + max_instance_count=None, ): """Create a cloud function from the given user defined function.""" @@ -411,14 +416,16 @@ def create_cloud_function( ) function.service_config = functions_v2.ServiceConfig() function.service_config.available_memory = "1024M" - if cloud_function_timeout is not None: - if cloud_function_timeout > 1200: + if timeout_seconds is not None: + if timeout_seconds > 1200: raise ValueError( "BigQuery remote function can wait only up to 20 minutes" ", see for more details " "https://cloud.google.com/bigquery/quotas#remote_function_limits." ) - function.service_config.timeout_seconds = cloud_function_timeout + function.service_config.timeout_seconds = timeout_seconds + if max_instance_count is not None: + function.service_config.max_instance_count = max_instance_count function.service_config.service_account_email = ( self._cloud_function_service_account ) @@ -466,6 +473,7 @@ def provision_bq_remote_function( package_requirements, max_batching_rows, cloud_function_timeout, + cloud_function_max_instance_count, ): """Provision a BigQuery remote function.""" # If reuse of any existing function with the same name (indicated by the @@ -487,7 +495,11 @@ def provision_bq_remote_function( # Create the cloud function if it does not exist if not cf_endpoint: cf_endpoint = self.create_cloud_function( - def_, cloud_function_name, package_requirements, cloud_function_timeout + def_, + cloud_function_name, + package_requirements, + cloud_function_timeout, + cloud_function_max_instance_count, ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") @@ -642,6 +654,7 @@ def remote_function( cloud_function_docker_repository: Optional[str] = None, max_batching_rows: Optional[int] = 1000, cloud_function_timeout: Optional[int] = 600, + cloud_function_max_instances: Optional[int] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -778,6 +791,14 @@ def remote_function( https://cloud.google.com/bigquery/quotas#remote_function_limits. By default BigQuery DataFrames uses a 10 minute timeout. `None` can be passed to let the cloud functions default timeout take effect. + cloud_function_max_instances (int, Optional): + The maximumm instance count for the cloud function created. This + can be used to control how many cloud function instances can be + active at max at any given point of time. Lower setting can help + control the spike in the billing. Higher setting can help + support processing larger scale data. When not specified, cloud + function's default setting applies. For more details see + https://cloud.google.com/functions/docs/configuring/max-instances """ if isinstance(input_types, type): input_types = [input_types] @@ -906,6 +927,7 @@ def wrapper(f): packages, max_batching_rows, cloud_function_timeout, + cloud_function_max_instances, ) # TODO: Move ibis logic to compiler step diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index fc87bec3dfc..2200fd6aa41 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -652,6 +652,7 @@ def remote_function( cloud_function_docker_repository: Optional[str] = None, max_batching_rows: Optional[int] = 1000, cloud_function_timeout: Optional[int] = 600, + cloud_function_max_instances: Optional[int] = None, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -667,6 +668,7 @@ def remote_function( cloud_function_docker_repository=cloud_function_docker_repository, max_batching_rows=max_batching_rows, cloud_function_timeout=cloud_function_timeout, + cloud_function_max_instances=cloud_function_max_instances, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 2425369edd9..ac03b56f945 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1466,6 +1466,7 @@ def remote_function( cloud_function_docker_repository: Optional[str] = None, max_batching_rows: Optional[int] = 1000, cloud_function_timeout: Optional[int] = 600, + cloud_function_max_instances: Optional[int] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1580,6 +1581,14 @@ def remote_function( https://cloud.google.com/bigquery/quotas#remote_function_limits. By default BigQuery DataFrames uses a 10 minute timeout. `None` can be passed to let the cloud functions default timeout take effect. + cloud_function_max_instances (int, Optional): + The maximumm instance count for the cloud function created. This + can be used to control how many cloud function instances can be + active at max at any given point of time. Lower setting can help + control the spike in the billing. Higher setting can help + support processing larger scale data. When not specified, cloud + function's default setting applies. For more details see + https://cloud.google.com/functions/docs/configuring/max-instances Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1603,6 +1612,7 @@ def remote_function( cloud_function_docker_repository=cloud_function_docker_repository, max_batching_rows=max_batching_rows, cloud_function_timeout=cloud_function_timeout, + cloud_function_max_instances=cloud_function_max_instances, ) def read_gbq_function( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index eb7cb8308ba..eb2a0884fef 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1414,3 +1414,43 @@ def test_remote_function_gcf_timeout_max_supported_exceeded(session): @session.remote_function([int], int, reuse=False, cloud_function_timeout=1201) def square(x): return x * x + + +@pytest.mark.parametrize( + ("max_instances_args", "expected_max_instances"), + [ + pytest.param({}, 100, id="no-set"), + pytest.param({"cloud_function_max_instances": None}, 100, id="set-None"), + pytest.param({"cloud_function_max_instances": 1000}, 1000, id="set-explicit"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_max_instances( + session, scalars_dfs, max_instances_args, expected_max_instances +): + try: + + def square(x): + return x * x + + square_remote = session.remote_function( + [int], int, reuse=False, **max_instances_args + )(square) + + # Assert that the GCF is created with the intended max instance count + gcf = session.cloudfunctionsclient.get_function( + name=square_remote.bigframes_cloud_function + ) + assert gcf.service_config.max_instance_count == expected_max_instances + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["int64_too"].apply(square_remote).to_pandas() + pd_result = scalars_pandas_df["int64_too"].apply(square) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square_remote + ) From f5617994bc136de5caa72719b8c3c297c512cb36 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 3 May 2024 10:25:15 -0700 Subject: [PATCH 543/921] feat: custom query labels for compute options (#638) * feat: Custom query labels for compute options * Update docstring * Update test * Code example update. * ignore type * update format --- bigframes/_config/compute_options.py | 53 +++++++++++++++++++++++++++- bigframes/session/__init__.py | 6 ++++ tests/system/small/test_session.py | 18 ++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/bigframes/_config/compute_options.py b/bigframes/_config/compute_options.py index 81ef044f4d8..c8a54fe0b37 100644 --- a/bigframes/_config/compute_options.py +++ b/bigframes/_config/compute_options.py @@ -15,7 +15,7 @@ """Options for displaying objects.""" import dataclasses -from typing import Optional +from typing import Any, Dict, Optional @dataclasses.dataclass @@ -34,6 +34,26 @@ class ComputeOptions: >>> bpd.options.compute.maximum_bytes_billed = None # reset option + To add multiple extra labels to a query configuration, use the `assign_extra_query_labels` + method with keyword arguments: + + >>> bpd.options.compute.assign_extra_query_labels(test1=1, test2="abc") + >>> bpd.options.compute.extra_query_labels + {'test1': 1, 'test2': 'abc'} + + Alternatively, you can add labels individually by directly accessing the `extra_query_labels` + dictionary: + + >>> bpd.options.compute.extra_query_labels["test3"] = False + >>> bpd.options.compute.extra_query_labels + {'test1': 1, 'test2': 'abc', 'test3': False} + + To remove a label from the configuration, use the `del` keyword on the desired label key: + + >>> del bpd.options.compute.extra_query_labels["test1"] + >>> bpd.options.compute.extra_query_labels + {'test2': 'abc', 'test3': False} + Attributes: maximum_bytes_billed (int, Options): Limits the bytes billed for query jobs. Queries that will have @@ -44,7 +64,38 @@ class ComputeOptions: 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. """ maximum_bytes_billed: Optional[int] = None enable_multi_query_execution: bool = False + extra_query_labels: Dict[str, Any] = dataclasses.field( + default_factory=dict, init=False + ) + + def assign_extra_query_labels(self, **kwargs: Any) -> None: + """ + Assigns additional custom labels for query configuration. The method updates the + `extra_query_labels` dictionary with new labels provided through keyword arguments. + + Args: + kwargs (Any): + Custom labels provided as keyword arguments. Each key-value pair + in `kwargs` represents a label name and its value. + + Raises: + ValueError: If a key matches one of the reserved attribute names, + specifically 'maximum_bytes_billed' or 'enable_multi_query_execution', + to prevent conflicts with built-in settings. + """ + reserved_keys = ["maximum_bytes_billed", "enable_multi_query_execution"] + for key in kwargs: + if key in reserved_keys: + raise ValueError( + f"'{key}' is a reserved attribute name. Please use " + "a different key for your custom labels to avoid " + "conflicts with built-in settings." + ) + + self.extra_query_labels.update(kwargs) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index ac03b56f945..7c7d93541c5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1699,6 +1699,12 @@ def _prepare_query_job_config( bigframes.options.compute.maximum_bytes_billed ) + current_labels = job_config.labels if job_config.labels else {} + for key, value in bigframes.options.compute.extra_query_labels.items(): + if key not in current_labels: + current_labels[key] = value + job_config.labels = current_labels + if self._bq_kms_key_name: job_config.destination_encryption_configuration = ( bigquery.EncryptionConfiguration(kms_key_name=self._bq_kms_key_name) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 2779874d6c8..6b2d7df50da 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -405,6 +405,24 @@ def test_read_gbq_with_configuration( assert df.shape == (9, 3) +def test_read_gbq_with_custom_global_labels( + session: bigframes.Session, scalars_table_id: str +): + bigframes.options.compute.assign_extra_query_labels(test1=1, test2="abc") + bigframes.options.compute.extra_query_labels["test3"] = False + + job_labels = session.read_gbq(scalars_table_id).query_job.labels # type:ignore + expected_labels = {"test1": "1", "test2": "abc", "test3": "false"} + + assert all(job_labels.get(key) == value for key, value in expected_labels.items()) + + del bigframes.options.compute.extra_query_labels["test1"] + del bigframes.options.compute.extra_query_labels["test2"] + del bigframes.options.compute.extra_query_labels["test3"] + + assert len(bigframes.options.compute.extra_query_labels) == 0 + + 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) From 2715d2b4a353710175a66a4f6149356f583f2c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 3 May 2024 19:54:33 -0500 Subject: [PATCH 544/921] fix: downgrade NoDefaultIndexError to DefaultIndexWarning (#658) --- bigframes/exceptions.py | 4 +- .../session/_io/bigquery/read_gbq_table.py | 7 ++-- tests/unit/session/test_session.py | 39 +++++++++++-------- .../bigframes_vendored/pandas/io/gbq.py | 2 +- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 222df069f65..5caf2aa1df1 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -25,5 +25,5 @@ class CleanupFailedWarning(Warning): """Bigframes failed to clean up a table resource.""" -class NoDefaultIndexError(ValueError): - """Unable to create a default index.""" +class DefaultIndexWarning(Warning): + """Default index may cause unexpected costs.""" diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 29d5a5567f3..f6c1463e6c9 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -277,13 +277,14 @@ def get_index_cols_and_uniqueness( # resource utilization because of the default sequential index. See # internal issue 335727141. if _is_table_clustered_or_partitioned(table) and not primary_keys: - raise bigframes.exceptions.NoDefaultIndexError( + warnings.warn( f"Table '{str(table.reference)}' is clustered and/or " "partitioned, but BigQuery DataFrames was not able to find a " - "suitable index. To avoid this error, set at least one of: " + "suitable index. To avoid this warning, set at least one of: " # TODO(b/338037499): Allow max_results to override this too, # once we make it more efficient. - "`index_col` or `filters`." + "`index_col` or `filters`.", + category=bigframes.exceptions.DefaultIndexWarning, ) # If there are primary keys defined, the query engine assumes these diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 70a121435c1..a161c2df765 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -17,6 +17,7 @@ import os import re from unittest import mock +import warnings import google.api_core.exceptions import google.cloud.bigquery @@ -186,7 +187,7 @@ def get_table_mock(table_ref): @pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) -def test_no_default_index_error_raised_by_read_gbq(table): +def test_default_index_warning_raised_by_read_gbq(table): """Because of the windowing operation to create a default index, row filters can't push down to the clustering column. @@ -202,12 +203,12 @@ def test_no_default_index_error_raised_by_read_gbq(table): session = resources.create_bigquery_session(bqclient=bqclient) table._properties["location"] = session._location - with pytest.raises(bigframes.exceptions.NoDefaultIndexError): + with pytest.warns(bigframes.exceptions.DefaultIndexWarning): session.read_gbq("my-project.my_dataset.my_table") @pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) -def test_no_default_index_error_not_raised_by_read_gbq_index_col_sequential_int64( +def test_default_index_warning_not_raised_by_read_gbq_index_col_sequential_int64( table, ): """Because of the windowing operation to create a default index, row @@ -224,11 +225,13 @@ def test_no_default_index_error_not_raised_by_read_gbq_index_col_sequential_int6 session = resources.create_bigquery_session(bqclient=bqclient) table._properties["location"] = session._location - # No exception raised because we set the option allowing the default indexes. - df = session.read_gbq( - "my-project.my_dataset.my_table", - index_col=bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64, - ) + # No warnings raised because we set the option allowing the default indexes. + with warnings.catch_warnings(): + warnings.simplefilter("error", bigframes.exceptions.DefaultIndexWarning) + df = session.read_gbq( + "my-project.my_dataset.my_table", + index_col=bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64, + ) # We expect a window operation because we specificaly requested a sequential index. generated_sql = df.sql.casefold() @@ -246,7 +249,7 @@ def test_no_default_index_error_not_raised_by_read_gbq_index_col_sequential_int6 ), ) @pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) -def test_no_default_index_error_not_raised_by_read_gbq_index_col_columns( +def test_default_index_warning_not_raised_by_read_gbq_index_col_columns( total_count, distinct_count, table, @@ -270,10 +273,12 @@ def test_no_default_index_error_not_raised_by_read_gbq_index_col_columns( ) table._properties["location"] = session._location - # No exception raised because there are columns to use as the index. - df = session.read_gbq( - "my-project.my_dataset.my_table", index_col=("idx_1", "idx_2") - ) + # No warning raised because there are columns to use as the index. + with warnings.catch_warnings(): + warnings.simplefilter("error", bigframes.exceptions.DefaultIndexWarning) + df = session.read_gbq( + "my-project.my_dataset.my_table", index_col=("idx_1", "idx_2") + ) # There should be no analytic operators to prevent row filtering pushdown. assert "OVER" not in df.sql @@ -281,7 +286,7 @@ def test_no_default_index_error_not_raised_by_read_gbq_index_col_columns( @pytest.mark.parametrize("table", CLUSTERED_OR_PARTITIONED_TABLES) -def test_no_default_index_error_not_raised_by_read_gbq_primary_key(table): +def test_default_index_warning_not_raised_by_read_gbq_primary_key(table): """If a primary key is set on the table, we use that as the index column by default, no error should be raised in this case. @@ -310,8 +315,10 @@ def test_no_default_index_error_not_raised_by_read_gbq_primary_key(table): ) table._properties["location"] = session._location - # No exception raised because there is a primary key to use as the index. - df = session.read_gbq("my-project.my_dataset.my_table") + # No warning raised because there is a primary key to use as the index. + with warnings.catch_warnings(): + warnings.simplefilter("error", bigframes.exceptions.DefaultIndexWarning) + df = session.read_gbq("my-project.my_dataset.my_table") # There should be no analytic operators to prevent row filtering pushdown. assert "OVER" not in df.sql diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index c25dd8776f1..38ea208eafa 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -157,7 +157,7 @@ def read_gbq( Alias for columns, retained for backwards compatibility. Raises: - bigframes.exceptions.NoDefaultIndexError: + bigframes.exceptions.DefaultIndexWarning: Using the default index is discouraged, such as with clustered or partitioned tables without primary keys. From 651fd7daf14273f172c6c55e5d6c374eb590a22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Sun, 5 May 2024 20:02:48 -0500 Subject: [PATCH 545/921] feat: `bigframes.options` and `bigframes.option_context` now uses thread-local variables to prevent context managers in separate threads from affecting each other (#652) * feat: `bigframes.options` and `bigframes.option_context` now uses thread-local variables to prevent context managers in separate threads from affecting each other In our tests, this allows us to actually test things like `bf.option_context("display.repr_mode", "deferred"):` without always having some other test change the display mode and break the test. Fixes internal issue 308657813 * catch close errors on thread-local session too * use presence of _local.bigquery_options to indicate thread locality feat: always do a query dry run when `option.repr_mode == "deferred"` (#652) --- bigframes/_config/__init__.py | 74 +++++++++-- bigframes/core/global_session.py | 60 +++++++-- bigframes/core/indexes/base.py | 2 +- bigframes/dataframe.py | 6 +- bigframes/series.py | 2 +- tests/system/small/ml/test_llm.py | 120 ++++++++++-------- tests/system/small/test_dataframe.py | 17 +-- tests/system/small/test_pandas_options.py | 29 ++++- tests/system/small/test_progress_bar.py | 55 +++++--- .../pandas/_config/config.py | 29 ++++- 10 files changed, 275 insertions(+), 119 deletions(-) diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index bdd7a8f2d63..bf33420e60e 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -17,6 +17,9 @@ DataFrames from this package. """ +import copy +import threading + import bigframes_vendored.pandas._config.config as pandas_config import bigframes._config.bigquery_options as bigquery_options @@ -29,44 +32,91 @@ class Options: """Global options affecting BigQuery DataFrames behavior.""" def __init__(self): + self._local = threading.local() + + # Initialize these in the property getters to make sure we do have a + # separate instance per thread. + self._local.bigquery_options = None + self._local.display_options = None + self._local.sampling_options = None + self._local.compute_options = None + + # BigQuery options are special because they can only be set once per + # session, so we need an indicator as to whether we are using the + # thread-local session or the global session. self._bigquery_options = bigquery_options.BigQueryOptions() - self._display_options = display_options.DisplayOptions() - self._sampling_options = sampling_options.SamplingOptions() - self._compute_options = compute_options.ComputeOptions() + + def _init_bigquery_thread_local(self): + """Initialize thread-local options, based on current global options.""" + + # Already thread-local, so don't reset any options that have been set + # already. No locks needed since this only modifies thread-local + # variables. + if self._local.bigquery_options is not None: + return + + self._local.bigquery_options = copy.deepcopy(self._bigquery_options) + self._local.bigquery_options._session_started = False @property def bigquery(self) -> bigquery_options.BigQueryOptions: """Options to use with the BigQuery engine.""" + if self._local.bigquery_options is not None: + # The only way we can get here is if someone called + # _init_bigquery_thread_local. + return self._local.bigquery_options + return self._bigquery_options @property def display(self) -> display_options.DisplayOptions: """Options controlling object representation.""" - return self._display_options + if self._local.display_options is None: + self._local.display_options = display_options.DisplayOptions() + + return self._local.display_options @property def sampling(self) -> sampling_options.SamplingOptions: """Options controlling downsampling when downloading data - to memory. The data will be downloaded into memory explicitly + to memory. + + The data can be downloaded into memory explicitly (e.g., to_pandas, to_numpy, values) or implicitly (e.g., matplotlib plotting). This option can be overriden by - parameters in specific functions.""" - return self._sampling_options + parameters in specific functions. + """ + if self._local.sampling_options is None: + self._local.sampling_options = sampling_options.SamplingOptions() + + return self._local.sampling_options @property def compute(self) -> compute_options.ComputeOptions: - """Options controlling object computation.""" - return self._compute_options + """Thread-local options controlling object computation.""" + if self._local.compute_options is None: + self._local.compute_options = compute_options.ComputeOptions() + + return self._local.compute_options + + @property + def is_bigquery_thread_local(self) -> bool: + """Indicator that we're using a thread-local session. + + A thread-local session can be started by using + `with bigframes.option_context("bigquery.some_option", "some-value"):`. + """ + return self._local.bigquery_options is not None options = Options() """Global options for default session.""" +option_context = pandas_config.option_context + __all__ = ( "Options", "options", + "option_context", ) - - -option_context = pandas_config.option_context diff --git a/bigframes/core/global_session.py b/bigframes/core/global_session.py index 31dfc9bd172..3187c5c11bf 100644 --- a/bigframes/core/global_session.py +++ b/bigframes/core/global_session.py @@ -26,6 +26,24 @@ _global_session: Optional[bigframes.session.Session] = None _global_session_lock = threading.Lock() +_global_session_state = threading.local() +_global_session_state.thread_local_session = None + + +def _try_close_session(session): + """Try to close the session and warn if couldn't.""" + try: + session.close() + except google.auth.exceptions.RefreshError as e: + session_id = session.session_id + location = session._location + project_id = session._project + warnings.warn( + f"Session cleanup failed for session with id: {session_id}, " + f"location: {location}, project: {project_id}", + category=bigframes.exceptions.CleanupFailedWarning, + ) + traceback.print_tb(e.__traceback__) def close_session() -> None: @@ -37,24 +55,30 @@ def close_session() -> None: Returns: None """ - global _global_session + global _global_session, _global_session_lock, _global_session_state + + if bigframes._config.options.is_bigquery_thread_local: + if _global_session_state.thread_local_session is not None: + _try_close_session(_global_session_state.thread_local_session) + _global_session_state.thread_local_session = None + + # Currently using thread-local options, so no global lock needed. + # Don't reset options.bigquery, as that's the responsibility + # of the context manager that started it in the first place. The user + # might have explicitly closed the session in the context manager and + # the thread-locality property needs to be retained. + bigframes._config.options.bigquery._session_started = False + + # Don't close the non-thread-local session. + return with _global_session_lock: if _global_session is not None: - try: - _global_session.close() - except google.auth.exceptions.RefreshError as e: - session_id = _global_session.session_id - location = _global_session._location - project_id = _global_session._project - warnings.warn( - f"Session cleanup failed for session with id: {session_id}, " - f"location: {location}, project: {project_id}", - category=bigframes.exceptions.CleanupFailedWarning, - ) - traceback.print_tb(e.__traceback__) + _try_close_session(_global_session) _global_session = None + # This should be global, not thread-local because of the if clause + # above. bigframes._config.options.bigquery._session_started = False @@ -63,7 +87,15 @@ def get_global_session(): Creates the global session if it does not exist. """ - global _global_session, _global_session_lock + global _global_session, _global_session_lock, _global_session_state + + if bigframes._config.options.is_bigquery_thread_local: + if _global_session_state.thread_local_session is None: + _global_session_state.thread_local_session = bigframes.session.connect( + bigframes._config.options.bigquery + ) + + return _global_session_state.thread_local_session with _global_session_lock: if _global_session is None: diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 46a9e30637e..569dae4ffc3 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -239,7 +239,7 @@ def __repr__(self) -> str: opts = bigframes.options.display max_results = opts.max_rows if opts.repr_mode == "deferred": - return formatter.repr_query_job(self.query_job) + return formatter.repr_query_job(self._block._compute_dry_run()) pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) self._query_job = query_job diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d694216ebe7..1f1fb5467f0 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -595,7 +595,7 @@ def __repr__(self) -> str: opts = bigframes.options.display max_results = opts.max_rows if opts.repr_mode == "deferred": - return formatter.repr_query_job(self.query_job) + return formatter.repr_query_job(self._compute_dry_run()) self._cached() # TODO(swast): pass max_columns and get the true column count back. Maybe @@ -632,9 +632,9 @@ def _repr_html_(self) -> str: many notebooks are not configured for large tables. """ opts = bigframes.options.display - max_results = bigframes.options.display.max_rows + max_results = opts.max_rows if opts.repr_mode == "deferred": - return formatter.repr_query_job_html(self.query_job) + return formatter.repr_query_job(self._compute_dry_run()) self._cached() # TODO(swast): pass max_columns and get the true column count back. Maybe diff --git a/bigframes/series.py b/bigframes/series.py index 3986d38445d..aea3d60ff51 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -282,7 +282,7 @@ def __repr__(self) -> str: opts = bigframes.options.display max_results = opts.max_rows if opts.repr_mode == "deferred": - return formatter.repr_query_job(self.query_job) + return formatter.repr_query_job(self._compute_dry_run()) self._cached() pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 6f6b67597ac..8a6874b1780 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -55,25 +55,28 @@ def test_create_text_generator_model_default_session( ): import bigframes.pandas as bpd - bpd.close_session() - bpd.options.bigquery.bq_connection = bq_connection - bpd.options.bigquery.location = "us" - - model = llm.PaLM2TextGenerator() - assert model is not None - assert model._bqml_model is not None - assert ( - model.connection_name.casefold() - == f"{bigquery_client.project}.us.bigframes-rf-conn" - ) - - llm_text_df = bpd.read_pandas(llm_text_pandas_df) - - df = model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + # Note: This starts a thread-local session. + with bpd.option_context( + "bigquery.bq_connection", + bq_connection, + "bigquery.location", + "US", + ): + model = llm.PaLM2TextGenerator() + assert model is not None + assert model._bqml_model is not None + assert ( + model.connection_name.casefold() + == f"{bigquery_client.project}.us.bigframes-rf-conn" + ) + + llm_text_df = bpd.read_pandas(llm_text_pandas_df) + + df = model.predict(llm_text_df).to_pandas() + assert df.shape == (3, 4) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) @pytest.mark.flaky(retries=2) @@ -82,25 +85,28 @@ def test_create_text_generator_32k_model_default_session( ): import bigframes.pandas as bpd - bpd.close_session() - bpd.options.bigquery.bq_connection = bq_connection - bpd.options.bigquery.location = "us" - - model = llm.PaLM2TextGenerator(model_name="text-bison-32k") - assert model is not None - assert model._bqml_model is not None - assert ( - model.connection_name.casefold() - == f"{bigquery_client.project}.us.bigframes-rf-conn" - ) - - llm_text_df = bpd.read_pandas(llm_text_pandas_df) - - df = model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + # Note: This starts a thread-local session. + with bpd.option_context( + "bigquery.bq_connection", + bq_connection, + "bigquery.location", + "US", + ): + model = llm.PaLM2TextGenerator(model_name="text-bison-32k") + assert model is not None + assert model._bqml_model is not None + assert ( + model.connection_name.casefold() + == f"{bigquery_client.project}.us.bigframes-rf-conn" + ) + + llm_text_df = bpd.read_pandas(llm_text_pandas_df) + + df = model.predict(llm_text_df).to_pandas() + assert df.shape == (3, 4) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) @pytest.mark.flaky(retries=2) @@ -232,27 +238,33 @@ def test_create_embedding_generator_multilingual_model( def test_create_text_embedding_generator_model_defaults(bq_connection): import bigframes.pandas as bpd - bpd.close_session() - bpd.options.bigquery.bq_connection = bq_connection - bpd.options.bigquery.location = "us" - - model = llm.PaLM2TextEmbeddingGenerator() - assert model is not None - assert model._bqml_model is not None + # Note: This starts a thread-local session. + with bpd.option_context( + "bigquery.bq_connection", + bq_connection, + "bigquery.location", + "US", + ): + model = llm.PaLM2TextEmbeddingGenerator() + assert model is not None + assert model._bqml_model is not None def test_create_text_embedding_generator_multilingual_model_defaults(bq_connection): import bigframes.pandas as bpd - bpd.close_session() - bpd.options.bigquery.bq_connection = bq_connection - bpd.options.bigquery.location = "us" - - model = llm.PaLM2TextEmbeddingGenerator( - model_name="textembedding-gecko-multilingual" - ) - assert model is not None - assert model._bqml_model is not None + # Note: This starts a thread-local session. + with bpd.option_context( + "bigquery.bq_connection", + bq_connection, + "bigquery.location", + "US", + ): + model = llm.PaLM2TextEmbeddingGenerator( + model_name="textembedding-gecko-multilingual" + ) + assert model is not None + assert model._bqml_model is not None @pytest.mark.flaky(retries=2) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index b428207314d..5ed6908640d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -142,18 +142,13 @@ def test_df_construct_from_dict(): def test_df_construct_inline_respects_location(): import bigframes.pandas as bpd - bpd.close_session() - bpd.options.bigquery.location = "europe-west1" + # Note: This starts a thread-local session. + with bpd.option_context("bigquery.location", "europe-west1"): + df = bpd.DataFrame([[1, 2, 3], [4, 5, 6]]) + repr(df) - df = bpd.DataFrame([[1, 2, 3], [4, 5, 6]]) - repr(df) - - table = bpd.get_global_session().bqclient.get_table(df.query_job.destination) - assert table.location == "europe-west1" - - # Reset global session - bpd.close_session() - bpd.options.bigquery.location = "us" + table = bpd.get_global_session().bqclient.get_table(df.query_job.destination) + assert table.location == "europe-west1" def test_get_column(scalars_dfs): diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index dd13196981c..afb75c65e30 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -27,8 +27,11 @@ @pytest.fixture(autouse=True) def reset_default_session_and_location(): - bpd.close_session() - bpd.options.bigquery.location = None + # Note: This starts a thread-local session and closes it once the test + # finishes. + with bpd.option_context("bigquery.location", None): + bpd.options.bigquery.location = None + yield @pytest.mark.parametrize( @@ -80,7 +83,9 @@ def test_read_gbq_start_sets_session_location( ): read_method(query) - # Close global session to start over + # Close the global session to start over. + # Note: This is a thread-local operation because of the + # reset_default_session_and_location fixture above. bpd.close_session() # There should still be the previous location set in the bigquery options @@ -289,13 +294,25 @@ def test_credentials_need_reauthentication(monkeypatch): with pytest.raises(google.auth.exceptions.RefreshError): bpd.read_gbq(test_query) - # Now verify that closing the session works and we throw - # the expected warning + # Now verify that closing the session works We look at the + # thread-local session because of the + # reset_default_session_and_location fixture and that this test mutates + # state that might otherwise be used by tests running in parallel. + assert ( + bigframes.core.global_session._global_session_state.thread_local_session + is not None + ) + with warnings.catch_warnings(record=True) as warned: bpd.close_session() # CleanupFailedWarning: can't clean up + assert len(warned) == 1 assert warned[0].category == bigframes.exceptions.CleanupFailedWarning - assert bigframes.core.global_session._global_session is None + + assert ( + bigframes.core.global_session._global_session_state.thread_local_session + is None + ) # Now verify that use is able to start over df = bpd.read_gbq(test_query) diff --git a/tests/system/small/test_progress_bar.py b/tests/system/small/test_progress_bar.py index 5ccc6db0ac9..73a9743e2f2 100644 --- a/tests/system/small/test_progress_bar.py +++ b/tests/system/small/test_progress_bar.py @@ -23,33 +23,37 @@ from bigframes.session import MAX_INLINE_DF_BYTES job_load_message_regex = r"\w+ job [\w-]+ is \w+\." +EXPECTED_DRY_RUN_MESSAGE = "Computation deferred. Computation will process" def test_progress_bar_dataframe( penguins_df_default_index: bf.dataframe.DataFrame, capsys ): - bf.options.display.progress_bar = "terminal" capsys.readouterr() # clear output - penguins_df_default_index.to_pandas() + + with bf.option_context("display.progress_bar", "terminal"): + penguins_df_default_index.to_pandas() assert_loading_msg_exist(capsys.readouterr().out) assert penguins_df_default_index.query_job is not None def test_progress_bar_series(penguins_df_default_index: bf.dataframe.DataFrame, capsys): - bf.options.display.progress_bar = "terminal" series = penguins_df_default_index["body_mass_g"].head(10) capsys.readouterr() # clear output - series.to_pandas() + + with bf.option_context("display.progress_bar", "terminal"): + series.to_pandas() assert_loading_msg_exist(capsys.readouterr().out) assert series.query_job is not None def test_progress_bar_scalar(penguins_df_default_index: bf.dataframe.DataFrame, capsys): - bf.options.display.progress_bar = "terminal" capsys.readouterr() # clear output - penguins_df_default_index["body_mass_g"].head(10).mean() + + with bf.option_context("display.progress_bar", "terminal"): + penguins_df_default_index["body_mass_g"].head(10).mean() assert_loading_msg_exist(capsys.readouterr().out) @@ -57,10 +61,11 @@ def test_progress_bar_scalar(penguins_df_default_index: bf.dataframe.DataFrame, def test_progress_bar_extract_jobs( penguins_df_default_index: bf.dataframe.DataFrame, gcs_folder, capsys ): - bf.options.display.progress_bar = "terminal" path = gcs_folder + "test_read_csv_progress_bar*.csv" capsys.readouterr() # clear output - penguins_df_default_index.to_csv(path) + + with bf.option_context("display.progress_bar", "terminal"): + penguins_df_default_index.to_csv(path) assert_loading_msg_exist(capsys.readouterr().out) @@ -73,8 +78,9 @@ def test_progress_bar_load_jobs( while len(df) < MAX_INLINE_DF_BYTES: df = pd.DataFrame(np.repeat(df.values, 2, axis=0)) - bf.options.display.progress_bar = "terminal" - with tempfile.TemporaryDirectory() as dir: + with bf.option_context( + "display.progress_bar", "terminal" + ), tempfile.TemporaryDirectory() as dir: path = dir + "/test_read_csv_progress_bar*.csv" df.to_csv(path, index=False) capsys.readouterr() # clear output @@ -96,11 +102,12 @@ def assert_loading_msg_exist(capystOut: str, pattern=job_load_message_regex): def test_query_job_repr_html(penguins_df_default_index: bf.dataframe.DataFrame): - bf.options.display.progress_bar = "terminal" - penguins_df_default_index.to_pandas() - query_job_repr = formatting_helpers.repr_query_job_html( - penguins_df_default_index.query_job - ).value + with bf.option_context("display.progress_bar", "terminal"): + penguins_df_default_index.to_pandas() + query_job_repr = formatting_helpers.repr_query_job_html( + penguins_df_default_index.query_job + ).value + string_checks = [ "Job Id", "Destination Table", @@ -126,3 +133,21 @@ def test_query_job_repr(penguins_df_default_index: bf.dataframe.DataFrame): ] for string in string_checks: assert string in query_job_repr + + +def test_query_job_dry_run_dataframe(penguins_df_default_index: bf.dataframe.DataFrame): + with bf.option_context("display.repr_mode", "deferred"): + df_result = repr(penguins_df_default_index) + assert EXPECTED_DRY_RUN_MESSAGE in df_result + + +def test_query_job_dry_run_index(penguins_df_default_index: bf.dataframe.DataFrame): + with bf.option_context("display.repr_mode", "deferred"): + index_result = repr(penguins_df_default_index.index) + assert EXPECTED_DRY_RUN_MESSAGE in index_result + + +def test_query_job_dry_run_series(penguins_df_default_index: bf.dataframe.DataFrame): + with bf.option_context("display.repr_mode", "deferred"): + series_result = repr(penguins_df_default_index["body_mass_g"]) + assert EXPECTED_DRY_RUN_MESSAGE in series_result diff --git a/third_party/bigframes_vendored/pandas/_config/config.py b/third_party/bigframes_vendored/pandas/_config/config.py index 1b73e649c85..13ccfdac894 100644 --- a/third_party/bigframes_vendored/pandas/_config/config.py +++ b/third_party/bigframes_vendored/pandas/_config/config.py @@ -7,10 +7,17 @@ class option_context(contextlib.ContextDecorator): """ - Context manager to temporarily set options in the `with` statement context. + Context manager to temporarily set thread-local options in the `with` + statement context. You need to invoke as ``option_context(pat, val, [(pat, val), ...])``. + .. note:: + + `"bigquery"` options can't be changed on a running session. Setting any + of these options creates a new thread-local session that only lives for + the lifetime of the context manager. + **Examples:** >>> import bigframes @@ -29,7 +36,11 @@ def __init__(self, *args) -> None: def __enter__(self) -> None: self.undo = [ - (pat, operator.attrgetter(pat)(bigframes.options)) for pat, val in self.ops + (pat, operator.attrgetter(pat)(bigframes.options)) + for pat, _ in self.ops + # Don't try to undo changes to bigquery options. We're starting and + # closing a new thread-local session if those are set. + if not pat.startswith("bigquery.") ] for pat, val in self.ops: @@ -40,7 +51,21 @@ def __exit__(self, *args) -> None: for pat, val in self.undo: self._set_option(pat, val) + # TODO(tswast): What to do if someone nests several context managers + # with separate "bigquery" options? We might need a "stack" of + # sessions if we allow that. + if bigframes.options.is_bigquery_thread_local: + bigframes.close_session() + + # Reset bigquery_options so that we're no longer thread-local. + bigframes.options._local.bigquery_options = None + def _set_option(self, pat, val): root, attr = pat.rsplit(".", 1) + + # We are now using a thread-specific session. + if root == "bigquery": + bigframes.options._init_bigquery_thread_local() + parent = operator.attrgetter(root)(bigframes.options) setattr(parent, attr, val) From 4a342933559fba417fe42e2bd386838defdb2778 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Mon, 6 May 2024 12:50:17 -0700 Subject: [PATCH 546/921] fix: fix bug with na in the column labels in stack (#659) --- bigframes/core/__init__.py | 5 ++++- tests/system/small/test_multiindex.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 185ce7cd4fd..eef0efcf83b 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -429,7 +429,10 @@ def _create_unpivot_labels_array( for row_offset in range(len(former_column_labels)): row_label = former_column_labels[row_offset] row_label = (row_label,) if not isinstance(row_label, tuple) else row_label - row = {col_ids[i]: row_label[i] for i in range(len(col_ids))} + row = { + col_ids[i]: (row_label[i] if pandas.notnull(row_label[i]) else None) + for i in range(len(col_ids)) + } rows.append(row) return ArrayValue.from_pyarrow(pa.Table.from_pylist(rows), session=self.session) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index bb0af52976d..613ad945c11 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -1191,3 +1191,22 @@ def test_explode_w_multi_index(): check_dtype=False, check_index_type=False, ) + + +def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index): + columns = ["int64_too", "int64_col", "rowindex_2"] + level1 = pandas.Index(["b", "c", "d"]) + # Need resulting column to be pyarrow string rather than object dtype + level2 = pandas.Index([None, "b", "b"], dtype="string[pyarrow]") + multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + pd_result = pd_df.stack() + bf_result = bf_df.stack().to_pandas() + + # Pandas produces pd.NA, where bq dataframes produces NaN + pd_result["c"] = pd_result["c"].replace(pandas.NA, np.nan) + pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) From 16866d2bbd4901b1bf57f7e8cfbdb444d63fee6c Mon Sep 17 00:00:00 2001 From: Stephanie A <129541811+DevStephanie@users.noreply.github.com> Date: Mon, 6 May 2024 17:41:35 -0500 Subject: [PATCH 547/921] docs: add python code sample fore multiple forecasting time series (#531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add python code sample to multiple timeseries forecasting --------- Co-authored-by: Tim Sweña (Swast) --- ...e_multiple_timeseries_forecasting_model.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 samples/snippets/create_multiple_timeseries_forecasting_model.py diff --git a/samples/snippets/create_multiple_timeseries_forecasting_model.py b/samples/snippets/create_multiple_timeseries_forecasting_model.py new file mode 100644 index 00000000000..26fc15595fc --- /dev/null +++ b/samples/snippets/create_multiple_timeseries_forecasting_model.py @@ -0,0 +1,98 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_multiple_timeseries_forecasting_model(random_model_id): + your_model_id = random_model_id + + # [START bigquery_dataframes_bqml_arima_multiple_step_2_visualize] + + import bigframes.pandas as bpd + + df = bpd.read_gbq("bigquery-public-data.new_york.citibike_trips") + + features = bpd.DataFrame( + { + "num_trips": df.starttime, + "date": df["starttime"].dt.date, + } + ) + date = df["starttime"].dt.date + df.groupby([date]) + num_trips = features.groupby(["date"]).count() + + # Results from running "print(num_trips)" + + # num_trips + # date + # 2013-07-01 16650 + # 2013-07-02 22745 + # 2013-07-03 21864 + # 2013-07-04 22326 + # 2013-07-05 21842 + # 2013-07-06 20467 + # 2013-07-07 20477 + # 2013-07-08 21615 + # 2013-07-09 26641 + # 2013-07-10 25732 + # 2013-07-11 24417 + # 2013-07-12 19006 + # 2013-07-13 26119 + # 2013-07-14 29287 + # 2013-07-15 28069 + # 2013-07-16 29842 + # 2013-07-17 30550 + # 2013-07-18 28869 + # 2013-07-19 26591 + # 2013-07-20 25278 + # 2013-07-21 30297 + # 2013-07-22 25979 + # 2013-07-23 32376 + # 2013-07-24 35271 + # 2013-07-25 31084 + + num_trips.plot.line( + # Rotate the x labels so they are more visible. + rot=45, + ) + + # [END bigquery_dataframes_bqml_arima_multiple_step_2_visualize] + + # [START bigquery_dataframes_bqml_arima_multiple_step_3_fit] + from bigframes.ml import forecasting + import bigframes.pandas as bpd + + df = bpd.read_gbq("bigquery-public-data.new_york.citibike_trips") + + features = bpd.DataFrame( + { + "num_trips": df.starttime, + "date": df["starttime"].dt.date, + } + ) + num_trips = features.groupby(["date"], as_index=False).count() + model = forecasting.ARIMAPlus() + + X = num_trips["date"].to_frame() + y = num_trips["num_trips"].to_frame() + + model.fit(X, y) + # The model.fit() call above created a temporary model. + # Use the to_gbq() method to write to a permanent location. + + model.to_gbq( + your_model_id, # For example: "bqml_tutorial.nyc_citibike_arima_model", + replace=True, + ) + # [END bigquery_dataframes_bqml_arima_multiple_step_3_fit] From e26ec206e27767f88a33847e135ee52935657aa2 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 6 May 2024 17:52:25 -0700 Subject: [PATCH 548/921] chore: disable BQML regression affected tests (#661) --- tests/system/large/ml/test_core.py | 3 +++ tests/system/large/ml/test_ensemble.py | 2 ++ tests/system/large/ml/test_pipeline.py | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py index df387e6ee17..aec1065e41e 100644 --- a/tests/system/large/ml/test_core.py +++ b/tests/system/large/ml/test_core.py @@ -13,10 +13,13 @@ # limitations under the License. import pandas +import pytest from bigframes.ml import globals +# TODO(garrettwu): Re-enable or not check exact numbers. +@pytest.mark.skip(reason="bqml regression") def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_df): df = penguins_df_default_index.dropna() X_train = df[ diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index 2403644a423..2260e7bbcef 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -20,6 +20,8 @@ import bigframes.ml.ensemble +# TODO(garrettwu): Re-enable or not check exact numbers. +@pytest.mark.skip(reason="bqml regression") @pytest.mark.flaky(retries=2) def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBRegressor() diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index c165b1e0301..1a92d0f7d42 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -222,6 +222,8 @@ def test_pipeline_logistic_regression_fit_score_predict( ) +# TODO(garrettwu): Re-enable or not check exact numbers. +@pytest.mark.skip(reason="bqml regression") @pytest.mark.flaky(retries=2) def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_index): """Test a supervised model with a minimal preprocessing step""" @@ -297,6 +299,8 @@ def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_in ) +# TODO(garrettwu): Re-enable or not check exact numbers. +@pytest.mark.skip(reason="bqml regression") @pytest.mark.flaky(retries=2) def test_pipeline_random_forest_classifier_fit_score_predict( session, penguins_df_default_index From ff23b1891817c10d1d70e030cf9aa5e870a6421c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 23:45:56 -0700 Subject: [PATCH 549/921] chore(main): release 1.5.0 (#645) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01e78ec423..f73d4b57502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.5.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.4.0...v1.5.0) (2024-05-07) + + +### Features + +* `bigframes.options` and `bigframes.option_context` now uses thread-local variables to prevent context managers in separate threads from affecting each other ([#652](https://github.com/googleapis/python-bigquery-dataframes/issues/652)) ([651fd7d](https://github.com/googleapis/python-bigquery-dataframes/commit/651fd7daf14273f172c6c55e5d6c374eb590a22d)) +* Add `ARIMAPlus.coef_` property exposing `ML.ARIMA_COEFFICIENTS` functionality ([#585](https://github.com/googleapis/python-bigquery-dataframes/issues/585)) ([81d1262](https://github.com/googleapis/python-bigquery-dataframes/commit/81d1262a40c133017c6debe89506d66aab7bb0c5)) +* Add a unique session_id to Session and allow cleaning up sessions ([#553](https://github.com/googleapis/python-bigquery-dataframes/issues/553)) ([c8d4e23](https://github.com/googleapis/python-bigquery-dataframes/commit/c8d4e231fe8263f5b10fae9b879ff82df58da534)) +* Add the `bigframes.bigquery` sub-package with a `bigframes.bigquery.array_length` function ([#630](https://github.com/googleapis/python-bigquery-dataframes/issues/630)) ([9963f85](https://github.com/googleapis/python-bigquery-dataframes/commit/9963f85b84c3b3c681447ab79e22ac93ac48349c)) +* Always do a query dry run when `option.repr_mode == "deferred"` ([#652](https://github.com/googleapis/python-bigquery-dataframes/issues/652)) ([651fd7d](https://github.com/googleapis/python-bigquery-dataframes/commit/651fd7daf14273f172c6c55e5d6c374eb590a22d)) +* Custom query labels for compute options ([#638](https://github.com/googleapis/python-bigquery-dataframes/issues/638)) ([f561799](https://github.com/googleapis/python-bigquery-dataframes/commit/f5617994bc136de5caa72719b8c3c297c512cb36)) +* Raise `NoDefaultIndexError` from `read_gbq` on clustered/partitioned tables with no `index_col` or `filters` set ([#631](https://github.com/googleapis/python-bigquery-dataframes/issues/631)) ([73064dd](https://github.com/googleapis/python-bigquery-dataframes/commit/73064dd2aa1ece5de8f5849a0fd337d0ba677404)) +* Support `index_col=False` in `read_csv` and `engine="bigquery"` ([73064dd](https://github.com/googleapis/python-bigquery-dataframes/commit/73064dd2aa1ece5de8f5849a0fd337d0ba677404)) +* Support gcf max instance count in `remote_function` ([#657](https://github.com/googleapis/python-bigquery-dataframes/issues/657)) ([36578ab](https://github.com/googleapis/python-bigquery-dataframes/commit/36578ab431119f71dda746de415d0c6417bb4de2)) + + +### Bug Fixes + +* Don't raise UnknownLocationWarning for US or EU multi-regions ([#653](https://github.com/googleapis/python-bigquery-dataframes/issues/653)) ([8e4616b](https://github.com/googleapis/python-bigquery-dataframes/commit/8e4616b896f4e0d13d8bb0424c89335d3a1fe697)) +* Downgrade NoDefaultIndexError to DefaultIndexWarning ([#658](https://github.com/googleapis/python-bigquery-dataframes/issues/658)) ([2715d2b](https://github.com/googleapis/python-bigquery-dataframes/commit/2715d2b4a353710175a66a4f6149356f583f2c45)) +* Fix bug with na in the column labels in stack ([#659](https://github.com/googleapis/python-bigquery-dataframes/issues/659)) ([4a34293](https://github.com/googleapis/python-bigquery-dataframes/commit/4a342933559fba417fe42e2bd386838defdb2778)) +* Use explicit session in `PaLM2TextGenerator` ([#651](https://github.com/googleapis/python-bigquery-dataframes/issues/651)) ([e4f13c3](https://github.com/googleapis/python-bigquery-dataframes/commit/e4f13c3633b90e32d3171976d8b27ed10049882f)) + + +### Documentation + +* Add python code sample for multiple forecasting time series ([#531](https://github.com/googleapis/python-bigquery-dataframes/issues/531)) ([16866d2](https://github.com/googleapis/python-bigquery-dataframes/commit/16866d2bbd4901b1bf57f7e8cfbdb444d63fee6c)) +* Fix the Palm2TextGenerator output token size ([#649](https://github.com/googleapis/python-bigquery-dataframes/issues/649)) ([c67e501](https://github.com/googleapis/python-bigquery-dataframes/commit/c67e501a4958ac097216cc1c0a9d5c1530c87ae5)) + ## [1.4.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.3.0...v1.4.0) (2024-04-29) diff --git a/bigframes/version.py b/bigframes/version.py index e892a8893fc..5f56ef9c61a 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.0" +__version__ = "1.5.0" From 5a7b1c9a0d1607a46823863a6a7bc861f9c9b5af Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 7 May 2024 11:44:07 -0700 Subject: [PATCH 550/921] refactor: ml model load read from class type hints (#656) * refactor: ml model load read from class type hints * exclude unrelated files * fix NoneType * fix tests * fix tests * fix param mappings * fix tests --- bigframes/ml/cluster.py | 28 +++--- bigframes/ml/decomposition.py | 23 ++--- bigframes/ml/ensemble.py | 119 ++++++++++--------------- bigframes/ml/forecasting.py | 42 +++------ bigframes/ml/imported.py | 32 +++---- bigframes/ml/linear_model.py | 61 ++++--------- bigframes/ml/llm.py | 71 +++++++-------- bigframes/ml/loader.py | 4 +- bigframes/ml/utils.py | 39 +++++++- tests/system/large/ml/test_ensemble.py | 4 +- 10 files changed, 192 insertions(+), 231 deletions(-) diff --git a/bigframes/ml/cluster.py b/bigframes/ml/cluster.py index e572bb3bfb1..43cfbdd4246 100644 --- a/bigframes/ml/cluster.py +++ b/bigframes/ml/cluster.py @@ -34,6 +34,7 @@ "distance_type": "distanceType", "max_iter": "maxIterations", "tol": "minRelativeProgress", + "warm_start": "warmStart", } @@ -67,27 +68,18 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() @classmethod - def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> KMeans: - assert model.model_type == "KMEANS" + def _from_bq(cls, session: bigframes.Session, bq_model: bigquery.Model) -> KMeans: + assert bq_model.model_type == "KMEANS" kwargs: dict = {} - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] - dummy_kmeans = cls() - for bf_param, bf_value in dummy_kmeans.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param in last_fitting: - # Convert types - kwargs[bf_param] = ( - str(last_fitting[bqml_param]) - if bf_param in ["init"] - else type(bf_value)(last_fitting[bqml_param]) - ) - - new_kmeans = cls(**kwargs) - new_kmeans._bqml_model = core.BqmlModel(session, model) - return new_kmeans + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) + + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> dict: diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index 01b1fda6283..ad0bce481fc 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -27,6 +27,8 @@ from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd +_BQML_PARAMS_MAPPING = {"svd_solver": "pcaSolver"} + @log_adapter.class_logger class PCA( @@ -47,23 +49,22 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() @classmethod - def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> PCA: - assert model.model_type == "PCA" + def _from_bq(cls, session: bigframes.Session, bq_model: bigquery.Model) -> PCA: + assert bq_model.model_type == "PCA" - kwargs: dict = {} + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] + last_fitting = bq_model.training_runs[-1]["trainingOptions"] if "numPrincipalComponents" in last_fitting: kwargs["n_components"] = int(last_fitting["numPrincipalComponents"]) - if "pcaExplainedVarianceRatio" in last_fitting: + elif "pcaExplainedVarianceRatio" in last_fitting: kwargs["n_components"] = float(last_fitting["pcaExplainedVarianceRatio"]) - if "pcaSolver" in last_fitting: - kwargs["svd_solver"] = str(last_fitting["pcaSolver"]) - new_pca = cls(**kwargs) - new_pca._bqml_model = core.BqmlModel(session, model) - return new_pca + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> dict: diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index b248c295f4c..8fc1e221461 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -30,9 +30,10 @@ _BQML_PARAMS_MAPPING = { "booster": "boosterType", + "dart_normalized_type": "dartNormalizeType", "tree_method": "treeMethod", - "colsample_bytree": "colsampleBylevel", - "colsample_bylevel": "colsampleBytree", + "colsample_bytree": "colsampleBytree", + "colsample_bylevel": "colsampleBylevel", "colsample_bynode": "colsampleBynode", "gamma": "minSplitLoss", "subsample": "subsample", @@ -44,6 +45,8 @@ "min_tree_child_weight": "minTreeChildWeight", "max_depth": "maxTreeDepth", "max_iterations": "maxIterations", + "enable_global_explain": "enableGlobalExplain", + "xgboost_version": "xgboostVersion", } @@ -99,24 +102,17 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> XGBRegressor: - assert model.model_type == "BOOSTED_TREE_REGRESSOR" + assert bq_model.model_type == "BOOSTED_TREE_REGRESSOR" - kwargs = {} - - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] - - dummy_regressor = cls() - for bf_param, bf_value in dummy_regressor.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param in last_fitting: - kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) - new_xgb_regressor = cls(**kwargs) - new_xgb_regressor._bqml_model = core.BqmlModel(session, model) - return new_xgb_regressor + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: @@ -255,24 +251,17 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> XGBClassifier: - assert model.model_type == "BOOSTED_TREE_CLASSIFIER" + assert bq_model.model_type == "BOOSTED_TREE_CLASSIFIER" - kwargs = {} - - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] - - dummy_classifier = XGBClassifier() - for bf_param, bf_value in dummy_classifier.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param is not None: - kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) - new_xgb_classifier = cls(**kwargs) - new_xgb_classifier._bqml_model = core.BqmlModel(session, model) - return new_xgb_classifier + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: @@ -370,16 +359,16 @@ def __init__( *, tree_method: Literal["auto", "exact", "approx", "hist"] = "auto", min_tree_child_weight: int = 1, - colsample_bytree=1.0, - colsample_bylevel=1.0, - colsample_bynode=0.8, - gamma=0.00, + colsample_bytree: float = 1.0, + colsample_bylevel: float = 1.0, + colsample_bynode: float = 0.8, + gamma: float = 0.0, max_depth: int = 15, - subsample=0.8, - reg_alpha=0.0, - reg_lambda=1.0, - tol=0.01, - enable_global_explain=False, + subsample: float = 0.8, + reg_alpha: float = 0.0, + reg_lambda: float = 1.0, + tol: float = 0.01, + enable_global_explain: bool = False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): self.n_estimators = n_estimators @@ -401,24 +390,17 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> RandomForestRegressor: - assert model.model_type == "RANDOM_FOREST_REGRESSOR" - - kwargs = {} - - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] + assert bq_model.model_type == "RANDOM_FOREST_REGRESSOR" - dummy_model = cls() - for bf_param, bf_value in dummy_model.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param in last_fitting: - kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) - new_random_forest_regressor = cls(**kwargs) - new_random_forest_regressor._bqml_model = core.BqmlModel(session, model) - return new_random_forest_regressor + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: @@ -542,7 +524,7 @@ def __init__( reg_alpha: float = 0.0, reg_lambda: float = 1.0, tol: float = 0.01, - enable_global_explain=False, + enable_global_explain: bool = False, xgboost_version: Literal["0.9", "1.1"] = "0.9", ): self.n_estimators = n_estimators @@ -564,24 +546,17 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> RandomForestClassifier: - assert model.model_type == "RANDOM_FOREST_CLASSIFIER" - - kwargs = {} + assert bq_model.model_type == "RANDOM_FOREST_CLASSIFIER" - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] - - dummy_model = RandomForestClassifier() - for bf_param, bf_value in dummy_model.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param is not None: - kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) - new_random_forest_classifier = cls(**kwargs) - new_random_forest_classifier._bqml_model = core.BqmlModel(session, model) - return new_random_forest_classifier + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> Dict[str, str | int | bool | float | List[str]]: diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 783e7741b81..a87df61801a 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -32,6 +32,7 @@ "auto_arima_min_order": "autoArimaMinOrder", "order": "nonSeasonalOrder", "data_frequency": "dataFrequency", + "include_drift": "includeDrift", "holiday_region": "holidayRegion", "clean_spikes_and_dips": "cleanSpikesAndDips", "adjust_step_changes": "adjustStepChanges", @@ -131,35 +132,18 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() @classmethod - def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ARIMAPlus: - assert model.model_type == "ARIMA_PLUS" - - kwargs: dict = {} - last_fitting = model.training_runs[-1]["trainingOptions"] - - dummy_arima = cls() - for bf_param, bf_value in dummy_arima.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param in last_fitting: - # Convert types - if bf_param in ["time_series_length_fraction"]: - kwargs[bf_param] = float(last_fitting[bqml_param]) - elif bf_param in [ - "auto_arima_max_order", - "auto_arima_min_order", - "min_time_series_length", - "max_time_series_length", - "trend_smoothing_window_size", - ]: - kwargs[bf_param] = int(last_fitting[bqml_param]) - elif bf_param in ["holiday_region"]: - kwargs[bf_param] = str(last_fitting[bqml_param]) - else: - kwargs[bf_param] = type(bf_value)(last_fitting[bqml_param]) - - new_arima_plus = cls(**kwargs) - new_arima_plus._bqml_model = core.BqmlModel(session, model) - return new_arima_plus + def _from_bq( + cls, session: bigframes.Session, bq_model: bigquery.Model + ) -> ARIMAPlus: + assert bq_model.model_type == "ARIMA_PLUS" + + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) + + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> dict: diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index a642fae74d3..cb8fe7a96e5 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -56,13 +56,13 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> TensorFlowModel: - assert model.model_type == "TENSORFLOW" + assert bq_model.model_type == "TENSORFLOW" - tf_model = cls(session=session, model_path="") - tf_model._bqml_model = core.BqmlModel(session, model) - return tf_model + model = cls(session=session, model_path="") + model._bqml_model = core.BqmlModel(session, bq_model) + return model def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. @@ -134,12 +134,14 @@ def _create_bqml_model(self): ) @classmethod - def _from_bq(cls, session: bigframes.Session, model: bigquery.Model) -> ONNXModel: - assert model.model_type == "ONNX" + def _from_bq( + cls, session: bigframes.Session, bq_model: bigquery.Model + ) -> ONNXModel: + assert bq_model.model_type == "ONNX" - onnx_model = cls(session=session, model_path="") - onnx_model._bqml_model = core.BqmlModel(session, model) - return onnx_model + model = cls(session=session, model_path="") + model._bqml_model = core.BqmlModel(session, bq_model) + return model def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. @@ -249,13 +251,13 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> XGBoostModel: - assert model.model_type == "XGBOOST" + assert bq_model.model_type == "XGBOOST" - xgboost_model = cls(session=session, model_path="") - xgboost_model._bqml_model = core.BqmlModel(session, model) - return xgboost_model + model = cls(session=session, model_path="") + model._bqml_model = core.BqmlModel(session, bq_model) + return model def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 0c76a39a1c1..32168e9a342 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -42,7 +42,6 @@ "warm_start": "warmStart", "calculate_p_values": "calculatePValues", "enable_global_explain": "enableGlobalExplain", - "category_encoding_method": "categoryEncodingMethod", } @@ -88,30 +87,17 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> LinearRegression: - assert model.model_type == "LINEAR_REGRESSION" + assert bq_model.model_type == "LINEAR_REGRESSION" - # TODO(bmil): construct a standard way to extract these properties - kwargs = {} - - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] - - dummy_linear = cls() - for bf_param, bf_value in dummy_linear.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param in last_fitting: - # Convert types - kwargs[bf_param] = ( - float(last_fitting[bqml_param]) - if bf_param in ["l1_reg", "learning_rate", "ls_init_learning_rate"] - else type(bf_value)(last_fitting[bqml_param]) - ) + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) - new_linear_regression = cls(**kwargs) - new_linear_regression._bqml_model = core.BqmlModel(session, model) - return new_linear_regression + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> dict: @@ -243,33 +229,24 @@ def __init__( @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> LogisticRegression: - assert model.model_type == "LOGISTIC_REGRESSION" - - kwargs = {} - - # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun - last_fitting = model.training_runs[-1]["trainingOptions"] - dummy_logistic = cls() - for bf_param, bf_value in dummy_logistic.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param in last_fitting: - # Convert types - kwargs[bf_param] = ( - float(last_fitting[bqml_param]) - if bf_param in ["l1_reg", "learning_rate", "ls_init_learning_rate"] - else type(bf_value)(last_fitting[bqml_param]) - ) + assert bq_model.model_type == "LOGISTIC_REGRESSION" + + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) + + last_fitting = bq_model.training_runs[-1]["trainingOptions"] if last_fitting["autoClassWeights"]: kwargs["class_weight"] = "balanced" # TODO(ashleyxu) support class_weight in the constructor. # if "labelClassWeights" in last_fitting: # kwargs["class_weight"] = last_fitting["labelClassWeights"] - new_logistic_regression = cls(**kwargs) - new_logistic_regression._bqml_model = core.BqmlModel(session, model) - return new_logistic_regression + model = cls(**kwargs) + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> dict: diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 4b075241949..77dc1d2b0fd 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -128,38 +128,30 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> PaLM2TextGenerator: - assert model.model_type == "MODEL_TYPE_UNSPECIFIED" - assert "remoteModelInfo" in model._properties - assert "endpoint" in model._properties["remoteModelInfo"] - assert "connection" in model._properties["remoteModelInfo"] + assert bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in bq_model._properties + assert "endpoint" in bq_model._properties["remoteModelInfo"] + assert "connection" in bq_model._properties["remoteModelInfo"] # Parse the remote model endpoint - bqml_endpoint = model._properties["remoteModelInfo"]["endpoint"] - model_connection = model._properties["remoteModelInfo"]["connection"] + bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] + model_connection = bq_model._properties["remoteModelInfo"]["connection"] model_endpoint = bqml_endpoint.split("/")[-1] - # Get the optional params - kwargs: dict = {} - last_fitting = model.training_runs[-1]["trainingOptions"] - - dummy_text_generator = cls(session=session) - for bf_param, _ in dummy_text_generator.__dict__.items(): - bqml_param = _BQML_PARAMS_MAPPING.get(bf_param) - if bqml_param in last_fitting: - # Convert types - if bf_param in ["max_iterations"]: - kwargs[bf_param] = int(last_fitting[bqml_param]) + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) - text_generator_model = cls( + model = cls( **kwargs, session=session, model_name=model_endpoint, connection_name=model_connection, ) - text_generator_model._bqml_model = core.BqmlModel(session, model) - return text_generator_model + model._bqml_model = core.BqmlModel(session, bq_model) + return model @property def _bqml_options(self) -> dict: @@ -464,29 +456,30 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> PaLM2TextEmbeddingGenerator: - assert model.model_type == "MODEL_TYPE_UNSPECIFIED" - assert "remoteModelInfo" in model._properties - assert "endpoint" in model._properties["remoteModelInfo"] - assert "connection" in model._properties["remoteModelInfo"] + assert bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in bq_model._properties + assert "endpoint" in bq_model._properties["remoteModelInfo"] + assert "connection" in bq_model._properties["remoteModelInfo"] # Parse the remote model endpoint - bqml_endpoint = model._properties["remoteModelInfo"]["endpoint"] - model_connection = model._properties["remoteModelInfo"]["connection"] + bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] + model_connection = bq_model._properties["remoteModelInfo"]["connection"] model_endpoint = bqml_endpoint.split("/")[-1] model_name, version = utils.parse_model_endpoint(model_endpoint) - embedding_generator_model = cls( + model = cls( session=session, # str to literals model_name=model_name, # type: ignore version=version, connection_name=model_connection, ) - embedding_generator_model._bqml_model = core.BqmlModel(session, model) - return embedding_generator_model + + model._bqml_model = core.BqmlModel(session, bq_model) + return model def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: """Predict the result from input DataFrame. @@ -616,18 +609,18 @@ def _create_bqml_model(self): @classmethod def _from_bq( - cls, session: bigframes.Session, model: bigquery.Model + cls, session: bigframes.Session, bq_model: bigquery.Model ) -> GeminiTextGenerator: - assert model.model_type == "MODEL_TYPE_UNSPECIFIED" - assert "remoteModelInfo" in model._properties - assert "connection" in model._properties["remoteModelInfo"] + assert bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in bq_model._properties + assert "connection" in bq_model._properties["remoteModelInfo"] # Parse the remote model endpoint - model_connection = model._properties["remoteModelInfo"]["connection"] + model_connection = bq_model._properties["remoteModelInfo"]["connection"] - text_generator_model = cls(session=session, connection_name=model_connection) - text_generator_model._bqml_model = core.BqmlModel(session, model) - return text_generator_model + model = cls(session=session, connection_name=model_connection) + model._bqml_model = core.BqmlModel(session, bq_model) + return model def predict( self, diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index c6e38e6534f..916949077fa 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -121,7 +121,7 @@ def _transformer_from_bq(session: bigframes.Session, bq_model: bigquery.Model): def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): if bq_model.model_type in _BQML_MODEL_TYPE_MAPPING: return _BQML_MODEL_TYPE_MAPPING[bq_model.model_type]._from_bq( # type: ignore - session=session, model=bq_model + session=session, bq_model=bq_model ) if _is_bq_model_remote(bq_model): # Parse the remote model endpoint @@ -130,7 +130,7 @@ def _model_from_bq(session: bigframes.Session, bq_model: bigquery.Model): model_name, _ = utils.parse_model_endpoint(model_endpoint) return _BQML_ENDPOINT_TYPE_MAPPING[model_name]._from_bq( # type: ignore - session=session, model=bq_model + session=session, bq_model=bq_model ) raise NotImplementedError( diff --git a/bigframes/ml/utils.py b/bigframes/ml/utils.py index 364fb5e88d6..75dfb916f6e 100644 --- a/bigframes/ml/utils.py +++ b/bigframes/ml/utils.py @@ -13,7 +13,9 @@ # limitations under the License. import typing -from typing import Iterable, Optional, Union +from typing import Any, Iterable, Literal, Mapping, Optional, Union + +from google.cloud import bigquery import bigframes.constants as constants from bigframes.core import blocks @@ -69,3 +71,38 @@ def parse_model_endpoint(model_endpoint: str) -> tuple[str, Optional[str]]: model_name = model_endpoint[:at_idx] return model_name, version + + +def _resolve_param_type(t: type) -> type: + def is_optional(t): + return typing.get_origin(t) is Union and type(None) in typing.get_args(t) + + # Optional[type] to type + if is_optional(t): + union_set = set(typing.get_args(t)) + union_set.remove(type(None)) + t = Union[tuple(union_set)] # type: ignore + + # Literal[value0, value1...] to type(value0) + if typing.get_origin(t) is Literal: + return type(typing.get_args(t)[0]) + + return t + + +def retrieve_params_from_bq_model( + cls, bq_model: bigquery.Model, params_mapping: Mapping[str, str] +) -> dict[str, Any]: + """Retrieve parameters of class constructor from BQ model. params_mapping specifies the names mapping param_name -> bqml_name. Params couldn't be found will be ignored.""" + kwargs = {} + + # See https://cloud.google.com/bigquery/docs/reference/rest/v2/models#trainingrun + last_fitting = bq_model.training_runs[-1]["trainingOptions"] + + for bf_param, bf_param_type in typing.get_type_hints(cls.__init__).items(): + bqml_param = params_mapping.get(bf_param) + if bqml_param in last_fitting: + bf_param_type = _resolve_param_type(bf_param_type) + kwargs[bf_param] = bf_param_type(last_fitting[bqml_param]) + + return kwargs diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index 2260e7bbcef..3d1fcaf41c7 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -123,7 +123,7 @@ def test_xgbregressor_dart_booster_multiple_params( in reloaded_model._bqml_model.model_name ) assert reloaded_model.booster == "DART" - assert reloaded_model.dart_normalized_type == "tree" + assert reloaded_model.dart_normalized_type == "TREE" assert reloaded_model.tree_method == "AUTO" assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 @@ -236,7 +236,7 @@ def test_xgbclassifier_dart_booster_multiple_params( in reloaded_model._bqml_model.model_name ) assert reloaded_model.booster == "DART" - assert reloaded_model.dart_normalized_type == "tree" + assert reloaded_model.dart_normalized_type == "TREE" assert reloaded_model.tree_method == "AUTO" assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 From c6c487fb3e39a980a05ff2dab5fb2b528d44016a Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 7 May 2024 13:24:17 -0700 Subject: [PATCH 551/921] feat: add `strategy="quantile"` in KBinsDiscretizer (#654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #310685445 🦕 --- bigframes/ml/compose.py | 1 + bigframes/ml/preprocessing.py | 51 +++++++++++----- bigframes/ml/sql.py | 11 +++- tests/system/small/ml/test_preprocessing.py | 58 +++++++++++++++++++ tests/unit/ml/test_sql.py | 7 +++ .../sklearn/preprocessing/_discretization.py | 2 +- 6 files changed, 112 insertions(+), 18 deletions(-) diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 89969f23e71..77bfd76bdeb 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -38,6 +38,7 @@ "ML.MAX_ABS_SCALER": preprocessing.MaxAbsScaler, "ML.MIN_MAX_SCALER": preprocessing.MinMaxScaler, "ML.BUCKETIZE": preprocessing.KBinsDiscretizer, + "ML.QUANTILE_BUCKETIZE": preprocessing.KBinsDiscretizer, "ML.LABEL_ENCODER": preprocessing.LabelEncoder, } ) diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 673ee27db04..954d5adff0e 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -290,10 +290,6 @@ def __init__( n_bins: int = 5, strategy: Literal["uniform", "quantile"] = "quantile", ): - if strategy != "uniform": - raise NotImplementedError( - f"Only strategy = 'uniform' is supported now, input is {strategy}." - ) if n_bins < 2: raise ValueError( f"n_bins has to be larger than or equal to 2, input is {n_bins}." @@ -337,30 +333,53 @@ def _compile_to_sql( min_value + i * bin_size for i in range(self.n_bins - 1) ] - return [ - ( - self._base_sql_generator.ml_bucketize( - column, array_split_points[column], f"kbinsdiscretizer_{column}" - ), - f"kbinsdiscretizer_{column}", + return [ + ( + self._base_sql_generator.ml_bucketize( + column, array_split_points[column], f"kbinsdiscretizer_{column}" + ), + f"kbinsdiscretizer_{column}", + ) + for column in columns + ] + + elif self.strategy == "quantile": + + return [ + ( + self._base_sql_generator.ml_quantile_bucketize( + column, self.n_bins, f"kbinsdiscretizer_{column}" + ), + f"kbinsdiscretizer_{column}", + ) + for column in columns + ] + + else: + raise ValueError( + f"strategy should be set 'quantile' or 'uniform', but your input is {self.strategy}." ) - for column in columns - ] @classmethod def _parse_from_sql(cls, sql: str) -> tuple[KBinsDiscretizer, str]: """Parse SQL to tuple(KBinsDiscretizer, column_label). Args: - sql: SQL string of format "ML.BUCKETIZE({col_label}, array_split_points, FALSE) OVER()" + sql: SQL string of format "ML.BUCKETIZE({col_label}, array_split_points, FALSE)" + or ML.QUANTILE_BUCKETIZE({col_label}, num_bucket) OVER()" Returns: tuple(KBinsDiscretizer, column_label)""" s = sql[sql.find("(") + 1 : sql.find(")")] - array_split_points = s[s.find("[") + 1 : s.find("]")] col_label = s[: s.find(",")] - n_bins = array_split_points.count(",") + 2 - return cls(n_bins, "uniform"), col_label + + if sql.startswith("ML.QUANTILE_BUCKETIZE"): + num_bins = s.split(",")[1] + return cls(int(num_bins), "quantile"), col_label + else: + array_split_points = s[s.find("[") + 1 : s.find("]")] + n_bins = array_split_points.count(",") + 2 + return cls(n_bins, "uniform"), col_label def fit( self, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index ea693e34379..b701ab301c3 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -109,9 +109,18 @@ def ml_bucketize( array_split_points: Iterable[Union[int, float]], name: str, ) -> str: - """Encode ML.MIN_MAX_SCALER for BQML""" + """Encode ML.BUCKETIZE for BQML""" return f"""ML.BUCKETIZE({numeric_expr_sql}, {array_split_points}, FALSE) AS {name}""" + def ml_quantile_bucketize( + self, + numeric_expr_sql: str, + num_bucket: int, + name: str, + ) -> str: + """Encode ML.QUANTILE_BUCKETIZE for BQML""" + return f"""ML.QUANTILE_BUCKETIZE({numeric_expr_sql}, {num_bucket}) OVER() AS {name}""" + def ml_one_hot_encoder( self, numeric_expr_sql: str, diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index faa0cd7bbd6..5b457cc9c08 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -373,6 +373,27 @@ def test_k_bins_discretizer_normalized_fit_transform_default_params(new_penguins pd.testing.assert_frame_equal(result, expected, rtol=0.1) +def test_k_bins_discretizer_normalized_fit_transform_default_params_quantile( + new_penguins_df, +): + discretizer = preprocessing.KBinsDiscretizer(strategy="quantile") + result = discretizer.fit_transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ).to_pandas() + + expected = pd.DataFrame( + { + "kbinsdiscretizer_culmen_length_mm": ["bin_2", "bin_2", "bin_1"], + "kbinsdiscretizer_culmen_depth_mm": ["bin_2", "bin_1", "bin_2"], + "kbinsdiscretizer_flipper_length_mm": ["bin_2", "bin_1", "bin_2"], + }, + dtype="string[pyarrow]", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=0.1) + + def test_k_bins_discretizer_series_normalizes( penguins_df_default_index, new_penguins_df ): @@ -395,6 +416,28 @@ def test_k_bins_discretizer_series_normalizes( pd.testing.assert_frame_equal(result, expected, rtol=0.1) +def test_k_bins_discretizer_series_normalizes_quantile( + penguins_df_default_index, new_penguins_df +): + discretizer = preprocessing.KBinsDiscretizer(strategy="quantile") + discretizer.fit(penguins_df_default_index["culmen_length_mm"]) + + result = discretizer.transform( + penguins_df_default_index["culmen_length_mm"] + ).to_pandas() + result = discretizer.transform(new_penguins_df).to_pandas() + + expected = pd.DataFrame( + { + "kbinsdiscretizer_culmen_length_mm": ["bin_2", "bin_2", "bin_1"], + }, + dtype="string[pyarrow]", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=0.1) + + def test_k_bins_discretizer_normalizes(penguins_df_default_index, new_penguins_df): # TODO(http://b/292431644): add a second test that compares output to sklearn.preprocessing.KBinsDiscretizer, when BQML's change is in prod. discretizer = preprocessing.KBinsDiscretizer(strategy="uniform") @@ -488,6 +531,21 @@ def test_k_bins_discretizer_save_load(new_penguins_df, dataset_id): pd.testing.assert_frame_equal(result, expected, rtol=0.1) +def test_k_bins_discretizer_save_load_quantile(new_penguins_df, dataset_id): + transformer = preprocessing.KBinsDiscretizer(n_bins=6, strategy="quantile") + transformer.fit( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"]] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.KBinsDiscretizer) + assert reloaded_transformer.n_bins == transformer.n_bins + assert reloaded_transformer.strategy == transformer.strategy + assert reloaded_transformer._bqml_model is not None + + def test_one_hot_encoder_default_params(new_penguins_df): encoder = preprocessing.OneHotEncoder() encoder.fit(new_penguins_df[["species", "sex"]]) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 4dd90b2c4a4..07b247fb41c 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -113,6 +113,13 @@ def test_k_bins_discretizer_correct( assert sql == "ML.BUCKETIZE(col_a, [1, 2, 3, 4], FALSE) AS scaled_col_a" +def test_k_bins_discretizer_quantile_correct( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_quantile_bucketize("col_a", 5, "scaled_col_a") + assert sql == "ML.QUANTILE_BUCKETIZE(col_a, 5) OVER() AS scaled_col_a" + + def test_one_hot_encoder_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py index 98b9d0371fe..54c81af71df 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_discretization.py @@ -18,7 +18,7 @@ class KBinsDiscretizer(TransformerMixin, BaseEstimator): strategy ({'uniform', 'quantile'}, default='quantile'): Strategy used to define the widths of the bins. 'uniform': All bins in each feature have identical widths. 'quantile': All bins in each - feature have the same number of points. Only `uniform` is supported. + feature have the same number of points. """ def fit(self, X, y=None): From 4fc89644e47a6da9367b54826b25c6abbe97327b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 8 May 2024 12:03:54 -0500 Subject: [PATCH 552/921] docs: address lint errors in code samples (#665) Towards internal issue 332735129 test: move samples tests to their own kokoro jobs --- .kokoro/continuous/e2e.cfg | 2 +- .kokoro/presubmit/e2e-gerrit.cfg | 2 +- .kokoro/presubmit/e2e.cfg | 2 +- noxfile.py | 20 -- owlbot.py | 6 +- samples/snippets/bqml_getting_started_test.py | 2 +- samples/snippets/clustering_model_test.py | 2 +- samples/snippets/conftest.py | 2 +- samples/snippets/create_kmeans_model_test.py | 8 +- ...e_multiple_timeseries_forecasting_model.py | 2 +- ...ingle_timeseries_forecasting_model_test.py | 3 +- samples/snippets/explore_query_result_test.py | 2 +- samples/snippets/gemini_model_test.py | 2 +- samples/snippets/gen_ai_model_test.py | 2 +- .../snippets/load_data_from_bigquery_test.py | 2 +- .../load_data_from_biquery_job_test.py | 2 +- samples/snippets/load_data_from_csv_test.py | 2 +- samples/snippets/noxfile.py | 292 ++++++++++++++++++ samples/snippets/noxfile_config.py | 42 +++ samples/snippets/pandas_methods_test.py | 2 +- samples/snippets/quickstart.py | 2 +- samples/snippets/regression_model_test.py | 2 +- samples/snippets/remote_function.py | 6 +- samples/snippets/remote_function_test.py | 31 ++ samples/snippets/requirements-test.txt | 3 + samples/snippets/requirements.txt | 2 + samples/snippets/set_options_test.py | 2 +- 27 files changed, 401 insertions(+), 46 deletions(-) create mode 100644 samples/snippets/noxfile.py create mode 100644 samples/snippets/noxfile_config.py create mode 100644 samples/snippets/requirements-test.txt create mode 100644 samples/snippets/requirements.txt diff --git a/.kokoro/continuous/e2e.cfg b/.kokoro/continuous/e2e.cfg index 74793465907..774b63313e1 100644 --- a/.kokoro/continuous/e2e.cfg +++ b/.kokoro/continuous/e2e.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "unit_prerelease system_prerelease system_noextras e2e notebook samples" + value: "unit_prerelease system_prerelease system_noextras e2e notebook" } env_vars: { diff --git a/.kokoro/presubmit/e2e-gerrit.cfg b/.kokoro/presubmit/e2e-gerrit.cfg index d875f360603..19913344b6c 100644 --- a/.kokoro/presubmit/e2e-gerrit.cfg +++ b/.kokoro/presubmit/e2e-gerrit.cfg @@ -3,5 +3,5 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "system_noextras e2e notebook samples" + value: "system_noextras e2e notebook" } diff --git a/.kokoro/presubmit/e2e.cfg b/.kokoro/presubmit/e2e.cfg index 74793465907..774b63313e1 100644 --- a/.kokoro/presubmit/e2e.cfg +++ b/.kokoro/presubmit/e2e.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "unit_prerelease system_prerelease system_noextras e2e notebook samples" + value: "unit_prerelease system_prerelease system_noextras e2e notebook" } env_vars: { diff --git a/noxfile.py b/noxfile.py index 91ad6bc0e6e..af73495a7f3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -402,26 +402,6 @@ def load(session: nox.sessions.Session): ) -@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) -def samples(session): - """Run the samples test suite.""" - - constraints_path = str( - CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" - ) - - # TODO(b/332735129): Remove this session and use python_samples templates - # where each samples directory has its own noxfile.py file, instead. - install_test_extra = True - install_systemtest_dependencies(session, install_test_extra, "-c", constraints_path) - - session.run( - "py.test", - "samples", - *session.posargs, - ) - - @nox.session(python=DEFAULT_PYTHON_VERSION) def cover(session): """Run the final coverage report. diff --git a/owlbot.py b/owlbot.py index f804859689a..ddc578c3a25 100644 --- a/owlbot.py +++ b/owlbot.py @@ -74,7 +74,7 @@ import pandas import pyarrow import sqlglot - + print(f"Python: {sys.version}") print(f"bigframes=={bigframes.__version__}") print(f"google-cloud-bigquery=={google.cloud.bigquery.__version__}") @@ -83,7 +83,7 @@ print(f"pyarrow=={pyarrow.__version__}") print(f"sqlglot=={sqlglot.__version__}") ``` - + #### Steps to reproduce """, ), @@ -148,3 +148,5 @@ # ---------------------------------------------------------------------------- s.shell.run(["nox", "-s", "format"], hide_output=False) +for noxfile in REPO_ROOT.glob("samples/**/noxfile.py"): + s.shell.run(["nox", "-s", "format"], cwd=noxfile.parent, hide_output=False) diff --git a/samples/snippets/bqml_getting_started_test.py b/samples/snippets/bqml_getting_started_test.py index d9f9135faa1..d249ca4ff34 100644 --- a/samples/snippets/bqml_getting_started_test.py +++ b/samples/snippets/bqml_getting_started_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_bqml_getting_started(random_model_id): +def test_bqml_getting_started(random_model_id: str) -> None: your_model_id = random_model_id # for example: bqml_tutorial.sample_model # [START bigquery_dataframes_bqml_getting_started_tutorial] diff --git a/samples/snippets/clustering_model_test.py b/samples/snippets/clustering_model_test.py index a407fc78058..fec4bbcefd8 100644 --- a/samples/snippets/clustering_model_test.py +++ b/samples/snippets/clustering_model_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_clustering_model(): +def test_clustering_model() -> None: # [START bigquery_dataframes_clustering_model] from bigframes.ml.cluster import KMeans import bigframes.pandas as bpd diff --git a/samples/snippets/conftest.py b/samples/snippets/conftest.py index d34837b3e28..9171ac78a45 100644 --- a/samples/snippets/conftest.py +++ b/samples/snippets/conftest.py @@ -46,7 +46,7 @@ def project_id(bigquery_client: bigquery.Client) -> str: @pytest.fixture(autouse=True) -def reset_session(): +def reset_session() -> None: """An autouse fixture ensuring each sample runs in a fresh session. This allows us to have samples that query data in different locations. diff --git a/samples/snippets/create_kmeans_model_test.py b/samples/snippets/create_kmeans_model_test.py index 2429060d09f..32ebc60a69f 100644 --- a/samples/snippets/create_kmeans_model_test.py +++ b/samples/snippets/create_kmeans_model_test.py @@ -13,12 +13,14 @@ # limitations under the License. -def test_kmeans_sample(project_id: str, random_model_id_eu: str): +def test_kmeans_sample(project_id: str, random_model_id_eu: str) -> None: your_gcp_project_id = project_id your_model_id = random_model_id_eu # [START bigquery_dataframes_bqml_kmeans] import datetime + import pandas as pd + import bigframes import bigframes.pandas as bpd @@ -92,7 +94,9 @@ def test_kmeans_sample(project_id: str, random_model_id_eu: str): stationstats = merged_df.groupby(["station_name", "isweekday"]).agg( {"duration": ["mean", "count"], "distance_from_city_center": "max"} ) - stationstats.columns = ["duration", "num_trips", "distance_from_city_center"] + stationstats.columns = pd.Index( + ["duration", "num_trips", "distance_from_city_center"] + ) stationstats = stationstats.sort_values( by="distance_from_city_center", ascending=True ).reset_index() diff --git a/samples/snippets/create_multiple_timeseries_forecasting_model.py b/samples/snippets/create_multiple_timeseries_forecasting_model.py index 26fc15595fc..b749c37d50c 100644 --- a/samples/snippets/create_multiple_timeseries_forecasting_model.py +++ b/samples/snippets/create_multiple_timeseries_forecasting_model.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_multiple_timeseries_forecasting_model(random_model_id): +def test_multiple_timeseries_forecasting_model(random_model_id: str) -> None: your_model_id = random_model_id # [START bigquery_dataframes_bqml_arima_multiple_step_2_visualize] diff --git a/samples/snippets/create_single_timeseries_forecasting_model_test.py b/samples/snippets/create_single_timeseries_forecasting_model_test.py index 57509337136..0c694de2dc1 100644 --- a/samples/snippets/create_single_timeseries_forecasting_model_test.py +++ b/samples/snippets/create_single_timeseries_forecasting_model_test.py @@ -13,8 +13,7 @@ # limitations under the License. -def test_create_single_timeseries(): - +def test_create_single_timeseries() -> None: # [START bigquery_dataframes_single_timeseries_forecasting_model_tutorial] import bigframes.pandas as bpd diff --git a/samples/snippets/explore_query_result_test.py b/samples/snippets/explore_query_result_test.py index 5f0ec7d9b61..42f48fd94e6 100644 --- a/samples/snippets/explore_query_result_test.py +++ b/samples/snippets/explore_query_result_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_bigquery_dataframes_explore_query_result(): +def test_bigquery_dataframes_explore_query_result() -> None: import bigframes.pandas as bpd # [START bigquery_dataframes_explore_query_result] diff --git a/samples/snippets/gemini_model_test.py b/samples/snippets/gemini_model_test.py index 89212875ae6..24b4e7d26d2 100644 --- a/samples/snippets/gemini_model_test.py +++ b/samples/snippets/gemini_model_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_gemini_text_generator_model(): +def test_gemini_text_generator_model() -> None: # Determine project id, in this case prefer the one set in the environment # variable GOOGLE_CLOUD_PROJECT (if any) import os diff --git a/samples/snippets/gen_ai_model_test.py b/samples/snippets/gen_ai_model_test.py index e4bead0e463..5cdcd6d3a7e 100644 --- a/samples/snippets/gen_ai_model_test.py +++ b/samples/snippets/gen_ai_model_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_llm_model(): +def test_llm_model() -> None: # Determine project id, in this case prefer the one set in the environment # variable GOOGLE_CLOUD_PROJECT (if any) import os diff --git a/samples/snippets/load_data_from_bigquery_test.py b/samples/snippets/load_data_from_bigquery_test.py index e4c65688bdd..4523eece97f 100644 --- a/samples/snippets/load_data_from_bigquery_test.py +++ b/samples/snippets/load_data_from_bigquery_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_bigquery_dataframes_load_data_from_bigquery(): +def test_bigquery_dataframes_load_data_from_bigquery() -> None: # [START bigquery_dataframes_load_data_from_bigquery] # Create a DataFrame from a BigQuery table: import bigframes.pandas as bpd diff --git a/samples/snippets/load_data_from_biquery_job_test.py b/samples/snippets/load_data_from_biquery_job_test.py index 9a7793a7e53..4f1ddc062fd 100644 --- a/samples/snippets/load_data_from_biquery_job_test.py +++ b/samples/snippets/load_data_from_biquery_job_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_bigquery_dataframes_load_data_from_bigquery_job(): +def test_bigquery_dataframes_load_data_from_bigquery_job() -> None: # Determine project id, in this case prefer the one set in the environment # variable GOOGLE_CLOUD_PROJECT (if any) import os diff --git a/samples/snippets/load_data_from_csv_test.py b/samples/snippets/load_data_from_csv_test.py index 31ab9255bf4..cc96b92fb8c 100644 --- a/samples/snippets/load_data_from_csv_test.py +++ b/samples/snippets/load_data_from_csv_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_bigquery_dataframes_load_data_from_csv(): +def test_bigquery_dataframes_load_data_from_csv() -> None: # [START bigquery_dataframes_load_data_from_csv] import bigframes.pandas as bpd diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py new file mode 100644 index 00000000000..c36d5f2d81f --- /dev/null +++ b/samples/snippets/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) + elif "pytest-xdist" in packages: + concurrent_args.extend(["-n", "auto"]) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """Returns the root folder of the project.""" + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/samples/snippets/noxfile_config.py b/samples/snippets/noxfile_config.py new file mode 100644 index 00000000000..211d6974b9e --- /dev/null +++ b/samples/snippets/noxfile_config.py @@ -0,0 +1,42 @@ +# Copyright 2020 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be inported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7", "3.7", "3.8"], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": True, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # "gcloud_project_env": "BUILD_SPECIFIC_GCLOUD_PROJECT", + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} diff --git a/samples/snippets/pandas_methods_test.py b/samples/snippets/pandas_methods_test.py index bd8e29c0030..0f128f9e6ac 100644 --- a/samples/snippets/pandas_methods_test.py +++ b/samples/snippets/pandas_methods_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_bigquery_dataframes_pandas_methods(): +def test_bigquery_dataframes_pandas_methods() -> None: # [START bigquery_dataframes_pandas_methods] import bigframes.pandas as bpd diff --git a/samples/snippets/quickstart.py b/samples/snippets/quickstart.py index ae3a9340049..c26c6f44428 100644 --- a/samples/snippets/quickstart.py +++ b/samples/snippets/quickstart.py @@ -13,7 +13,7 @@ # limitations under the License. -def run_quickstart(project_id: str): +def run_quickstart(project_id: str) -> None: import bigframes session_options = bigframes.BigQueryOptions() diff --git a/samples/snippets/regression_model_test.py b/samples/snippets/regression_model_test.py index 7d1bde689cb..43cdabacb46 100644 --- a/samples/snippets/regression_model_test.py +++ b/samples/snippets/regression_model_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_regression_model(): +def test_regression_model() -> None: # [START bigquery_dataframes_regression_model] from bigframes.ml.linear_model import LinearRegression import bigframes.pandas as bpd diff --git a/samples/snippets/remote_function.py b/samples/snippets/remote_function.py index 4db4e67619e..c35daf35fce 100644 --- a/samples/snippets/remote_function.py +++ b/samples/snippets/remote_function.py @@ -13,7 +13,7 @@ # limitations under the License. -def run_remote_function_and_read_gbq_function(project_id: str): +def run_remote_function_and_read_gbq_function(project_id: str) -> None: your_gcp_project_id = project_id # [START bigquery_dataframes_remote_function] @@ -51,7 +51,7 @@ def run_remote_function_and_read_gbq_function(project_id: str): str, reuse=False, ) - def get_bucket(num): + def get_bucket(num: float) -> str: if not num: return "NA" boundary = 4000 @@ -96,7 +96,7 @@ def get_bucket(num): reuse=False, packages=["cryptography"], ) - def get_hash(input): + def get_hash(input: str) -> str: from cryptography.fernet import Fernet # handle missing value diff --git a/samples/snippets/remote_function_test.py b/samples/snippets/remote_function_test.py index 8f891274dec..24bc7e854e2 100644 --- a/samples/snippets/remote_function_test.py +++ b/samples/snippets/remote_function_test.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import google.api_core.exceptions +import google.cloud.bigquery_connection_v1 import pytest import bigframes.pandas @@ -19,6 +21,35 @@ from . import remote_function +# TODO(tswast): Once the connections are cleaned up in the sample test project +# and https://github.com/GoogleCloudPlatform/python-docs-samples/issues/11720 +# is closed, we shouldn't need this because AFAIK we only use one BQ connection +# in this sample. +@pytest.fixture(autouse=True) +def cleanup_connections() -> None: + client = google.cloud.bigquery_connection_v1.ConnectionServiceClient() + + for conn in client.list_connections( + parent="projects/python-docs-samples-tests/locations/us" + ): + try: + int(conn.name.split("/")[-1].split("-")[0], base=16) + except ValueError: + print(f"Couldn't parse {conn.name}") + continue + + try: + print(f"removing {conn.name}") + client.delete_connection( + google.cloud.bigquery_connection_v1.DeleteConnectionRequest( + {"name": conn.name}, + ) + ) + except google.api_core.exceptions.GoogleAPIError: + # We did as much clean up as we can. + break + + def test_remote_function_and_read_gbq_function( capsys: pytest.CaptureFixture[str], ) -> None: diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt new file mode 100644 index 00000000000..62b0c02e79c --- /dev/null +++ b/samples/snippets/requirements-test.txt @@ -0,0 +1,3 @@ +# samples/snippets should be runnable with no "extras" +google-cloud-testutils==1.4.0 +pytest==8.1.1 diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt new file mode 100644 index 00000000000..1da77c1715a --- /dev/null +++ b/samples/snippets/requirements.txt @@ -0,0 +1,2 @@ +# samples/snippets should be runnable with no "extras" +bigframes==1.4.0 diff --git a/samples/snippets/set_options_test.py b/samples/snippets/set_options_test.py index f981009e9a8..3dea524a171 100644 --- a/samples/snippets/set_options_test.py +++ b/samples/snippets/set_options_test.py @@ -13,7 +13,7 @@ # limitations under the License. -def test_bigquery_dataframes_set_options(): +def test_bigquery_dataframes_set_options() -> None: # Close the session before resetting the options import bigframes.pandas as bpd From bcc054b90b7f84f79e127b27fd41ab5125f6c496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 8 May 2024 15:29:01 -0500 Subject: [PATCH 553/921] chore: remove outdated `NoDefaultIndexError` info from CHANGELOG.md (#668) * chore: remove outdated `NoDefaultIndexError` info from CHANGELOG.md We don't actually raise an error, just a `DefaultIndexWarning ` warning. * remove redundant line --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f73d4b57502..4457c2e4432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ * Add the `bigframes.bigquery` sub-package with a `bigframes.bigquery.array_length` function ([#630](https://github.com/googleapis/python-bigquery-dataframes/issues/630)) ([9963f85](https://github.com/googleapis/python-bigquery-dataframes/commit/9963f85b84c3b3c681447ab79e22ac93ac48349c)) * Always do a query dry run when `option.repr_mode == "deferred"` ([#652](https://github.com/googleapis/python-bigquery-dataframes/issues/652)) ([651fd7d](https://github.com/googleapis/python-bigquery-dataframes/commit/651fd7daf14273f172c6c55e5d6c374eb590a22d)) * Custom query labels for compute options ([#638](https://github.com/googleapis/python-bigquery-dataframes/issues/638)) ([f561799](https://github.com/googleapis/python-bigquery-dataframes/commit/f5617994bc136de5caa72719b8c3c297c512cb36)) -* Raise `NoDefaultIndexError` from `read_gbq` on clustered/partitioned tables with no `index_col` or `filters` set ([#631](https://github.com/googleapis/python-bigquery-dataframes/issues/631)) ([73064dd](https://github.com/googleapis/python-bigquery-dataframes/commit/73064dd2aa1ece5de8f5849a0fd337d0ba677404)) +* Warn with `DefaultIndexWarning` from `read_gbq` on clustered/partitioned tables with no `index_col` or `filters` set ([#631](https://github.com/googleapis/python-bigquery-dataframes/issues/631), [#658](https://github.com/googleapis/python-bigquery-dataframes/issues/658)) ([2715d2b](https://github.com/googleapis/python-bigquery-dataframes/commit/2715d2b4a353710175a66a4f6149356f583f2c45), [73064dd](https://github.com/googleapis/python-bigquery-dataframes/commit/73064dd2aa1ece5de8f5849a0fd337d0ba677404)) * Support `index_col=False` in `read_csv` and `engine="bigquery"` ([73064dd](https://github.com/googleapis/python-bigquery-dataframes/commit/73064dd2aa1ece5de8f5849a0fd337d0ba677404)) * Support gcf max instance count in `remote_function` ([#657](https://github.com/googleapis/python-bigquery-dataframes/issues/657)) ([36578ab](https://github.com/googleapis/python-bigquery-dataframes/commit/36578ab431119f71dda746de415d0c6417bb4de2)) @@ -23,7 +23,6 @@ ### Bug Fixes * Don't raise UnknownLocationWarning for US or EU multi-regions ([#653](https://github.com/googleapis/python-bigquery-dataframes/issues/653)) ([8e4616b](https://github.com/googleapis/python-bigquery-dataframes/commit/8e4616b896f4e0d13d8bb0424c89335d3a1fe697)) -* Downgrade NoDefaultIndexError to DefaultIndexWarning ([#658](https://github.com/googleapis/python-bigquery-dataframes/issues/658)) ([2715d2b](https://github.com/googleapis/python-bigquery-dataframes/commit/2715d2b4a353710175a66a4f6149356f583f2c45)) * Fix bug with na in the column labels in stack ([#659](https://github.com/googleapis/python-bigquery-dataframes/issues/659)) ([4a34293](https://github.com/googleapis/python-bigquery-dataframes/commit/4a342933559fba417fe42e2bd386838defdb2778)) * Use explicit session in `PaLM2TextGenerator` ([#651](https://github.com/googleapis/python-bigquery-dataframes/issues/651)) ([e4f13c3](https://github.com/googleapis/python-bigquery-dataframes/commit/e4f13c3633b90e32d3171976d8b27ed10049882f)) From e084e54557addff78522bbd710637ecb4b46d23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 8 May 2024 17:57:41 -0500 Subject: [PATCH 554/921] fix: include `index_col` when selecting `columns` and `filters` in `read_gbq_table` (#648) * fix: include `index_col` when selecting `columns` and `filters` in `read_gbq_table` Fixes internal issue 339430305 * feat: warn with a more specific `DefaultLocationWarning` category when no location can be detected (#648) test: refactor `read_gbq` / `read_gbq_table` tests to test with all parameters combined (#648) refactor: move query generation code to BigQuery I/O module (#648) --- bigframes/exceptions.py | 6 + bigframes/pandas/__init__.py | 3 +- bigframes/session/__init__.py | 158 +++++++-------------- bigframes/session/_io/bigquery/__init__.py | 97 ++++++++++++- tests/system/small/test_pandas_options.py | 26 ++-- tests/system/small/test_session.py | 94 +++++++++--- tests/unit/session/test_io_bigquery.py | 106 ++++++++++++++ tests/unit/session/test_session.py | 82 ----------- 8 files changed, 349 insertions(+), 223 deletions(-) diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 5caf2aa1df1..3ca6d8e1af3 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -17,6 +17,12 @@ # NOTE: This module should not depend on any others in the package. +# Uses UserWarning for backwards compatibility with warning without a category +# set. +class DefaultLocationWarning(UserWarning): + """No location was specified, so using a default one.""" + + class UnknownLocationWarning(Warning): """The location is set to an unknown value.""" diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 2200fd6aa41..1d6da46fae1 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -67,6 +67,7 @@ import bigframes.operations as ops import bigframes.series import bigframes.session +import bigframes.session._io.bigquery import bigframes.session.clients @@ -391,7 +392,7 @@ def _set_default_session_location_if_possible(query): bqclient = clients_provider.bqclient - if bigframes.session._is_query(query): + if bigframes.session._io.bigquery.is_query(query): job = bqclient.query(query, bigquery.QueryJobConfig(dry_run=True)) options.bigquery.location = job.location else: diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 7c7d93541c5..89845bb8429 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -20,7 +20,6 @@ import datetime import logging import os -import re import secrets import typing from typing import ( @@ -86,10 +85,11 @@ import bigframes.core.tree_properties as tree_properties import bigframes.core.utils as utils import bigframes.dtypes +import bigframes.exceptions import bigframes.formatting_helpers as formatting_helpers from bigframes.functions.remote_function import read_gbq_function as bigframes_rgf from bigframes.functions.remote_function import remote_function as bigframes_rf -import bigframes.session._io.bigquery as bigframes_io +import bigframes.session._io.bigquery as bf_io_bigquery import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table import bigframes.session.clients import bigframes.version @@ -145,14 +145,18 @@ ) -def _is_query(query_or_table: str) -> bool: - """Determine if `query_or_table` is a table ID or a SQL string""" - return re.search(r"\s", query_or_table.strip(), re.MULTILINE) is not None +def _to_index_cols( + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), +) -> List[str]: + """Convert index_col into a list of column names.""" + if isinstance(index_col, bigframes.enums.DefaultIndexKind): + index_cols: List[str] = [] + elif isinstance(index_col, str): + index_cols = [index_col] + else: + index_cols = list(index_col) - -def _is_table_with_wildcard_suffix(query_or_table: str) -> bool: - """Determine if `query_or_table` is a table and contains a wildcard suffix.""" - return not _is_query(query_or_table) and query_or_table.endswith("*") + return index_cols class Session( @@ -181,12 +185,26 @@ def __init__( if context is None: context = bigquery_options.BigQueryOptions() - # TODO(swast): Get location from the environment. if context.location is None: self._location = "US" warnings.warn( f"No explicit location is set, so using location {self._location} for the session.", - stacklevel=2, + # User's code + # -> get_global_session() + # -> connect() + # -> Session() + # + # Note: We could also have: + # User's code + # -> read_gbq() + # -> with_default_session() + # -> get_global_session() + # -> connect() + # -> Session() + # but we currently have no way to disambiguate these + # situations. + stacklevel=4, + category=bigframes.exceptions.DefaultLocationWarning, ) else: self._location = context.location @@ -322,13 +340,19 @@ def read_gbq( columns = col_order filters = list(filters) - if len(filters) != 0 or _is_table_with_wildcard_suffix(query_or_table): + if len(filters) != 0 or bf_io_bigquery.is_table_with_wildcard_suffix( + query_or_table + ): # TODO(b/338111344): This appears to be missing index_cols, which # are necessary to be selected. - # TODO(b/338039517): Also, need to account for primary keys. - query_or_table = self._to_query(query_or_table, columns, filters) + # TODO(b/338039517): Refactor this to be called inside both + # _read_gbq_query and _read_gbq_table (after detecting primary keys) + # so we can make sure index_col/index_cols reflects primary keys. + query_or_table = bf_io_bigquery.to_query( + query_or_table, _to_index_cols(index_col), columns, filters + ) - if _is_query(query_or_table): + if bf_io_bigquery.is_query(query_or_table): return self._read_gbq_query( query_or_table, index_col=index_col, @@ -355,85 +379,6 @@ def read_gbq( use_cache=use_cache if use_cache is not None else True, ) - def _to_query( - self, - query_or_table: str, - columns: Iterable[str], - filters: third_party_pandas_gbq.FiltersType, - ) -> str: - """Compile query_or_table with conditions(filters, wildcards) to query.""" - filters = list(filters) - sub_query = ( - f"({query_or_table})" - if _is_query(query_or_table) - else f"`{query_or_table}`" - ) - - # TODO(b/338111344): Generate an index based on DefaultIndexKind if we - # don't have index columns specified. - select_clause = "SELECT " + ( - ", ".join(f"`{column}`" for column in columns) if columns else "*" - ) - - where_clause = "" - if filters: - valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { - "in": "IN", - "not in": "NOT IN", - "LIKE": "LIKE", - "==": "=", - ">": ">", - "<": "<", - ">=": ">=", - "<=": "<=", - "!=": "!=", - } - - # If single layer filter, add another pseudo layer. So the single layer represents "and" logic. - if isinstance(filters[0], tuple) and ( - len(filters[0]) == 0 or not isinstance(list(filters[0])[0], tuple) - ): - filters = typing.cast(third_party_pandas_gbq.FiltersType, [filters]) - - or_expressions = [] - for group in filters: - if not isinstance(group, Iterable): - group = [group] - - and_expressions = [] - for filter_item in group: - if not isinstance(filter_item, tuple) or (len(filter_item) != 3): - raise ValueError( - f"Filter condition should be a tuple of length 3, {filter_item} is not valid." - ) - - column, operator, value = filter_item - - if not isinstance(column, str): - raise ValueError( - f"Column name should be a string, but received '{column}' of type {type(column).__name__}." - ) - - if operator not in valid_operators: - raise ValueError(f"Operator {operator} is not valid.") - - operator_str = valid_operators[operator] - - if operator_str in ["IN", "NOT IN"]: - value_list = ", ".join([repr(v) for v in value]) - expression = f"`{column}` {operator_str} ({value_list})" - else: - expression = f"`{column}` {operator_str} {repr(value)}" - and_expressions.append(expression) - - or_expressions.append(" AND ".join(and_expressions)) - - if or_expressions: - where_clause = " WHERE " + " OR ".join(or_expressions) - - full_query = f"{select_clause} FROM {sub_query} AS sub{where_clause}" - return full_query - def _query_to_destination( self, query: str, @@ -610,12 +555,7 @@ def _read_gbq_query( True if use_cache is None else use_cache ) - if isinstance(index_col, bigframes.enums.DefaultIndexKind): - index_cols = [] - elif isinstance(index_col, str): - index_cols = [index_col] - else: - index_cols = list(index_col) + index_cols = _to_index_cols(index_col) destination, query_job = self._query_to_destination( query, @@ -682,8 +622,13 @@ def read_gbq_table( columns = col_order filters = list(filters) - if len(filters) != 0 or _is_table_with_wildcard_suffix(query): - query = self._to_query(query, columns, filters) + if len(filters) != 0 or bf_io_bigquery.is_table_with_wildcard_suffix(query): + # TODO(b/338039517): Refactor this to be called inside both + # _read_gbq_query and _read_gbq_table (after detecting primary keys) + # so we can make sure index_col/index_cols reflects primary keys. + query = bf_io_bigquery.to_query( + query, _to_index_cols(index_col), columns, filters + ) return self._read_gbq_query( query, @@ -838,12 +783,7 @@ def _read_bigquery_load_job( index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), ) -> dataframe.DataFrame: - if isinstance(index_col, bigframes.enums.DefaultIndexKind): - index_cols = [] - elif isinstance(index_col, str): - index_cols = [index_col] - else: - index_cols = list(index_col) + index_cols = _to_index_cols(index_col) if not job_config.clustering_fields and index_cols: job_config.clustering_fields = index_cols[:_MAX_CLUSTER_COLUMNS] @@ -1430,7 +1370,7 @@ def _create_empty_temp_table( datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION ) - table = bigframes_io.create_temp_table( + table = bf_io_bigquery.create_temp_table( self, expiration, schema=schema, diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 79108c71a29..98e0dac1e83 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -19,10 +19,13 @@ import datetime import itertools import os +import re import textwrap import types -from typing import Dict, Iterable, Optional, Sequence, Tuple, Union +import typing +from typing import Dict, Iterable, Mapping, Optional, Sequence, Tuple, Union +import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions import google.cloud.bigquery as bigquery @@ -311,3 +314,95 @@ def create_bq_dataset_reference( query_destination.project, query_destination.dataset_id, ) + + +def is_query(query_or_table: str) -> bool: + """Determine if `query_or_table` is a table ID or a SQL string""" + return re.search(r"\s", query_or_table.strip(), re.MULTILINE) is not None + + +def is_table_with_wildcard_suffix(query_or_table: str) -> bool: + """Determine if `query_or_table` is a table and contains a wildcard suffix.""" + return not is_query(query_or_table) and query_or_table.endswith("*") + + +def to_query( + query_or_table: str, + index_cols: Iterable[str], + columns: Iterable[str], + filters: third_party_pandas_gbq.FiltersType, +) -> str: + """Compile query_or_table with conditions(filters, wildcards) to query.""" + filters = list(filters) + sub_query = ( + f"({query_or_table})" if is_query(query_or_table) else f"`{query_or_table}`" + ) + + # TODO(b/338111344): Generate an index based on DefaultIndexKind if we + # don't have index columns specified. + if columns: + # We only reduce the selection if columns is set, but we always + # want to make sure index_cols is also included. + all_columns = itertools.chain(index_cols, columns) + select_clause = "SELECT " + ", ".join(f"`{column}`" for column in all_columns) + else: + select_clause = "SELECT *" + + where_clause = "" + if filters: + valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { + "in": "IN", + "not in": "NOT IN", + "LIKE": "LIKE", + "==": "=", + ">": ">", + "<": "<", + ">=": ">=", + "<=": "<=", + "!=": "!=", + } + + # If single layer filter, add another pseudo layer. So the single layer represents "and" logic. + if isinstance(filters[0], tuple) and ( + len(filters[0]) == 0 or not isinstance(list(filters[0])[0], tuple) + ): + filters = typing.cast(third_party_pandas_gbq.FiltersType, [filters]) + + or_expressions = [] + for group in filters: + if not isinstance(group, Iterable): + group = [group] + + and_expressions = [] + for filter_item in group: + if not isinstance(filter_item, tuple) or (len(filter_item) != 3): + raise ValueError( + f"Filter condition should be a tuple of length 3, {filter_item} is not valid." + ) + + column, operator, value = filter_item + + if not isinstance(column, str): + raise ValueError( + f"Column name should be a string, but received '{column}' of type {type(column).__name__}." + ) + + if operator not in valid_operators: + raise ValueError(f"Operator {operator} is not valid.") + + operator_str = valid_operators[operator] + + if operator_str in ["IN", "NOT IN"]: + value_list = ", ".join([repr(v) for v in value]) + expression = f"`{column}` {operator_str} ({value_list})" + else: + expression = f"`{column}` {operator_str} {repr(value)}" + and_expressions.append(expression) + + or_expressions.append(" AND ".join(and_expressions)) + + if or_expressions: + where_clause = " WHERE " + " OR ".join(or_expressions) + + full_query = f"{select_clause} FROM {sub_query} AS sub{where_clause}" + return full_query diff --git a/tests/system/small/test_pandas_options.py b/tests/system/small/test_pandas_options.py index afb75c65e30..c580f926c92 100644 --- a/tests/system/small/test_pandas_options.py +++ b/tests/system/small/test_pandas_options.py @@ -13,6 +13,7 @@ # limitations under the License. import datetime +import re from unittest import mock import warnings @@ -69,8 +70,12 @@ def test_read_gbq_start_sets_session_location( assert not bpd.options.bigquery.location # Starting user journey with read_gbq* should work for a table in any - # location, in this case tokyo - df = read_method(query_tokyo) + # location, in this case tokyo. + with warnings.catch_warnings(): + # Since the query refers to a specific location, no warning should be + # raised. + warnings.simplefilter("error", bigframes.exceptions.DefaultLocationWarning) + df = read_method(query_tokyo) assert df is not None # Now bigquery options location should be set to tokyo @@ -146,7 +151,11 @@ def test_read_gbq_after_session_start_must_comply_with_default_location( # Starting user journey with anything other than read_gbq*, such as # read_pandas would bind the session to default location US - df = bpd.read_pandas(scalars_pandas_df_index) + with pytest.warns( + bigframes.exceptions.DefaultLocationWarning, + match=re.escape("using location US for the session"), + ): + df = bpd.read_pandas(scalars_pandas_df_index) assert df is not None # Doing read_gbq* from a table in another location should fail @@ -262,17 +271,18 @@ def test_read_gbq_must_comply_with_set_location_non_US( def test_credentials_need_reauthentication(monkeypatch): # Use a simple test query to verify that default session works to interact - # with BQ + # with BQ. test_query = "SELECT 1" - # Confirm that default session has BQ client with valid credentials - session = bpd.get_global_session() - assert session.bqclient._credentials.valid - # Confirm that default session works as usual df = bpd.read_gbq(test_query) assert df is not None + # Call get_global_session() *after* read_gbq so that our location detection + # has a chance to work. + session = bpd.get_global_session() + assert session.bqclient._credentials.valid + with monkeypatch.context() as m: # Simulate expired credentials to trigger the credential refresh flow m.setattr(session.bqclient._credentials, "expiry", datetime.datetime.utcnow()) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 6b2d7df50da..5daa01ad386 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -18,7 +18,7 @@ import textwrap import time import typing -from typing import List +from typing import List, Sequence import google import google.cloud.bigquery as bigquery @@ -338,30 +338,80 @@ def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): assert "OLI_TIRS" in sensors.index -def test_read_gbq_wildcard(session: bigframes.Session): - df = session.read_gbq("bigquery-public-data.noaa_gsod.gsod193*") - assert df.shape == (348485, 32) +_GSOD_ALL_TABLES = "bigquery-public-data.noaa_gsod.gsod*" +_GSOD_1930S = "bigquery-public-data.noaa_gsod.gsod193*" -def test_read_gbq_wildcard_with_filter(session: bigframes.Session): - df = session.read_gbq( - "bigquery-public-data.noaa_gsod.gsod19*", - filters=[("_table_suffix", ">=", "30"), ("_table_suffix", "<=", "39")], # type: ignore - ) - assert df.shape == (348485, 32) - - -def test_read_gbq_table_wildcard(session: bigframes.Session): - df = session.read_gbq_table("bigquery-public-data.noaa_gsod.gsod193*") - assert df.shape == (348485, 32) - - -def test_read_gbq_table_wildcard_with_filter(session: bigframes.Session): - df = session.read_gbq_table( - "bigquery-public-data.noaa_gsod.gsod19*", - filters=[("_table_suffix", ">=", "30"), ("_table_suffix", "<=", "39")], # type: ignore +@pytest.mark.parametrize( + "api_method", + # Test that both methods work as there's a risk that read_gbq / + # read_gbq_table makes for an infinite loop. Table reads can convert to + # queries and read_gbq reads from tables. + ["read_gbq", "read_gbq_table"], +) +@pytest.mark.parametrize( + ("filters", "table_id", "index_col", "columns"), + [ + pytest.param( + [("_table_suffix", ">=", "1930"), ("_table_suffix", "<=", "1939")], + _GSOD_ALL_TABLES, + ["stn", "wban", "year", "mo", "da"], + ["temp", "max", "min"], + id="all", + ), + pytest.param( + (), # filters + _GSOD_1930S, + (), # index_col + ["temp", "max", "min"], + id="columns", + ), + pytest.param( + [("_table_suffix", ">=", "1930"), ("_table_suffix", "<=", "1939")], + _GSOD_ALL_TABLES, + (), # index_col, + (), # columns + id="filters", + ), + pytest.param( + (), # filters + _GSOD_1930S, + ["stn", "wban", "year", "mo", "da"], + (), # columns + id="index_col", + ), + ], +) +def test_read_gbq_wildcard( + session: bigframes.Session, + api_method: str, + filters, + table_id: str, + index_col: Sequence[str], + columns: Sequence[str], +): + table_metadata = session.bqclient.get_table(table_id) + method = getattr(session, api_method) + df = method(table_id, filters=filters, index_col=index_col, columns=columns) + num_rows, num_columns = df.shape + + if index_col: + assert list(df.index.names) == list(index_col) + else: + assert df.index.name is None + + expected_columns = ( + columns + if columns + else [ + field.name + for field in table_metadata.schema + if field.name not in index_col and field.name not in columns + ] ) - assert df.shape == (348485, 32) + assert list(df.columns) == expected_columns + assert num_rows > 0 + assert num_columns == len(expected_columns) @pytest.mark.parametrize( diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 43865fc2c85..9da085e8240 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -210,3 +210,109 @@ def test_create_temp_table_default_expiration(): def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str): sql = io_bq.bq_schema_to_sql(schema) assert sql == expected + + +@pytest.mark.parametrize( + ("query_or_table", "index_cols", "columns", "filters", "expected_output"), + [ + pytest.param( + "test_table", + [], + [], + ["date_col", ">", "2022-10-20"], + None, + marks=pytest.mark.xfail( + raises=ValueError, + ), + id="raise_error", + ), + pytest.param( + "test_table", + ["row_index"], + ["string_col"], + [ + (("rowindex", "not in", [0, 6]),), + (("string_col", "in", ["Hello, World!", "こんにちは"]),), + ], + ( + "SELECT `row_index`, `string_col` FROM `test_table` AS sub WHERE " + "`rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " + "'こんにちは')" + ), + id="table-all_params-filter_or_operation", + ), + pytest.param( + """SELECT + rowindex, + string_col, + FROM `test_table` AS t + """, + ["rowindex"], + ["string_col"], + [ + ("rowindex", "<", 4), + ("string_col", "==", "Hello, World!"), + ], + """SELECT `rowindex`, `string_col` FROM (SELECT + rowindex, + string_col, + FROM `test_table` AS t + ) AS sub WHERE `rowindex` < 4 AND `string_col` = 'Hello, World!'""", + id="subquery-all_params-filter_and_operation", + ), + pytest.param( + "test_table", + [], + ["col_a", "col_b"], + [], + "SELECT `col_a`, `col_b` FROM `test_table` AS sub", + id="table-columns", + ), + pytest.param( + "test_table", + [], + [], + [("date_col", ">", "2022-10-20")], + "SELECT * FROM `test_table` AS sub WHERE `date_col` > '2022-10-20'", + id="table-filter", + ), + pytest.param( + "test_table*", + [], + [], + [], + "SELECT * FROM `test_table*` AS sub", + id="wildcard-no_params", + ), + pytest.param( + "test_table*", + [], + [], + [("_TABLE_SUFFIX", ">", "2022-10-20")], + "SELECT * FROM `test_table*` AS sub WHERE `_TABLE_SUFFIX` > '2022-10-20'", + id="wildcard-filter", + ), + ], +) +def test_to_query(query_or_table, index_cols, columns, filters, expected_output): + query = io_bq.to_query( + query_or_table, + index_cols, + columns, + filters, + ) + assert query == expected_output + + +@pytest.mark.parametrize( + ("query_or_table", "filters", "expected_output"), + [], +) +def test_to_query_with_wildcard_table(query_or_table, filters, expected_output): + query = io_bq.to_query( + query_or_table, + (), # index_cols + (), # columns + filters, + ) + assert query == expected_output diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index a161c2df765..bea858e0377 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -398,85 +398,3 @@ def test_session_init_fails_with_no_project(): credentials=mock.Mock(spec=google.auth.credentials.Credentials) ) ) - - -@pytest.mark.parametrize( - ("query_or_table", "columns", "filters", "expected_output"), - [ - pytest.param( - """SELECT - rowindex, - string_col, - FROM `test_table` AS t - """, - [], - [("rowindex", "<", 4), ("string_col", "==", "Hello, World!")], - """SELECT * FROM (SELECT - rowindex, - string_col, - FROM `test_table` AS t - ) AS sub WHERE `rowindex` < 4 AND `string_col` = 'Hello, World!'""", - id="query_input", - ), - pytest.param( - "test_table", - [], - [("date_col", ">", "2022-10-20")], - "SELECT * FROM `test_table` AS sub WHERE `date_col` > '2022-10-20'", - id="table_input", - ), - pytest.param( - "test_table", - ["row_index", "string_col"], - [ - (("rowindex", "not in", [0, 6]),), - (("string_col", "in", ["Hello, World!", "こんにちは"]),), - ], - ( - "SELECT `row_index`, `string_col` FROM `test_table` AS sub WHERE " - "`rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " - "'こんにちは')" - ), - id="or_operation", - ), - pytest.param( - "test_table", - [], - ["date_col", ">", "2022-10-20"], - None, - marks=pytest.mark.xfail( - raises=ValueError, - ), - id="raise_error", - ), - ], -) -def test_read_gbq_with_filters(query_or_table, columns, filters, expected_output): - session = resources.create_bigquery_session() - query = session._to_query(query_or_table, columns, filters) - assert query == expected_output - - -@pytest.mark.parametrize( - ("query_or_table", "columns", "filters", "expected_output"), - [ - pytest.param( - "test_table*", - [], - [], - "SELECT * FROM `test_table*` AS sub", - id="wildcard_table_input", - ), - pytest.param( - "test_table*", - [], - [("_TABLE_SUFFIX", ">", "2022-10-20")], - "SELECT * FROM `test_table*` AS sub WHERE `_TABLE_SUFFIX` > '2022-10-20'", - id="wildcard_table_input_with_filter", - ), - ], -) -def test_read_gbq_wildcard(query_or_table, columns, filters, expected_output): - session = resources.create_bigquery_session() - query = session._to_query(query_or_table, columns, filters) - assert query == expected_output From 306953aaae69e57c7c2f5eefb88d55a35bdcca9d Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 8 May 2024 22:58:29 +0000 Subject: [PATCH 555/921] docs: document inlining of small data in `read_*` APIs (#670) * docs: document inlining of small data in `read_*` APIs * mention that threshold is in memory size * non-bigquery instead of non-"bigquery" --- bigframes/session/__init__.py | 5 +++++ third_party/bigframes_vendored/pandas/io/parquet.py | 5 +++++ .../bigframes_vendored/pandas/io/parsers/readers.py | 10 ++++++++++ third_party/bigframes_vendored/pandas/io/pickle.py | 5 +++++ 4 files changed, 25 insertions(+) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 89845bb8429..5f70fd77f9c 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -874,6 +874,11 @@ def read_pandas( The pandas DataFrame will be persisted as a temporary BigQuery table, which can be automatically recycled after the Session is closed. + .. note:: + Data is inlined in the query SQL if it is small enough (roughly 5MB + or less in memory). Larger size data is loaded to a BigQuery table + instead. + **Examples:** >>> import bigframes.pandas as bpd diff --git a/third_party/bigframes_vendored/pandas/io/parquet.py b/third_party/bigframes_vendored/pandas/io/parquet.py index 877a384b6d1..1f5563c9626 100644 --- a/third_party/bigframes_vendored/pandas/io/parquet.py +++ b/third_party/bigframes_vendored/pandas/io/parquet.py @@ -19,6 +19,11 @@ def read_parquet( Instead, set a serialized index column as the index and sort by that in the resulting DataFrame. + .. note:: + For non-"bigquery" engine, data is inlined in the query SQL if it is + small enough (roughly 5MB or less in memory). Larger size data is + loaded to a BigQuery table instead. + **Examples:** >>> import bigframes.pandas as bpd diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py index d147abfd228..248cf8e0fe9 100644 --- a/third_party/bigframes_vendored/pandas/io/parsers/readers.py +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -62,6 +62,11 @@ def read_csv( file. Instead, set a serialized index column as the index and sort by that in the resulting DataFrame. + .. note:: + For non-bigquery engine, data is inlined in the query SQL if it is + small enough (roughly 5MB or less in memory). Larger size data is + loaded to a BigQuery table instead. + **Examples:** >>> import bigframes.pandas as bpd @@ -167,6 +172,11 @@ def read_json( file. Instead, set a serialized index column as the index and sort by that in the resulting DataFrame. + .. note:: + For non-bigquery engine, data is inlined in the query SQL if it is + small enough (roughly 5MB or less in memory). Larger size data is + loaded to a BigQuery table instead. + **Examples:** >>> import bigframes.pandas as bpd diff --git a/third_party/bigframes_vendored/pandas/io/pickle.py b/third_party/bigframes_vendored/pandas/io/pickle.py index 096d9b13d68..88684309f91 100644 --- a/third_party/bigframes_vendored/pandas/io/pickle.py +++ b/third_party/bigframes_vendored/pandas/io/pickle.py @@ -25,6 +25,11 @@ def read_pickle( If the content of the pickle file is a Series and its name attribute is None, the name will be set to '0' by default. + .. note:: + Data is inlined in the query SQL if it is small enough (roughly 5MB + or less in memory). Larger size data is loaded to a BigQuery table + instead. + **Examples:** >>> import bigframes.pandas as bpd From 57ccabcd1402b7938e2c7068e5b4880ef018f39c Mon Sep 17 00:00:00 2001 From: Stephanie A <129541811+DevStephanie@users.noreply.github.com> Date: Thu, 9 May 2024 09:38:28 -0500 Subject: [PATCH 556/921] feat: suggest correct options in bpd.options.bigquery.location (#666) * feat: suggest correct options in bpd.options.bigquery.location deps: add jellyfish as a dependency for spelling correction --- bigframes/_config/bigquery_options.py | 11 +++++++++-- setup.py | 2 ++ testing/constraints-3.9.txt | 1 + tests/unit/_config/test_bigquery_options.py | 11 ++++++----- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 74561e6f248..6f841a36b31 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -21,6 +21,7 @@ import google.api_core.exceptions import google.auth.credentials +import jellyfish import bigframes.constants import bigframes.exceptions @@ -30,7 +31,8 @@ "Call bigframes.pandas.close_session() first, if you are using the bigframes.pandas API." ) -UNKNOWN_LOCATION_MESSAGE = "The location '{location}' is set to an unknown value." + +UNKNOWN_LOCATION_MESSAGE = "The location '{location}' is set to an unknown value. Did you mean '{possibility}'?" def _validate_location(value: Optional[str]): @@ -39,8 +41,13 @@ def _validate_location(value: Optional[str]): return if value not in bigframes.constants.ALL_BIGQUERY_LOCATIONS: + location = str(value) + possibility = min( + bigframes.constants.ALL_BIGQUERY_LOCATIONS, + key=lambda item: jellyfish.levenshtein_distance(location, item), + ) warnings.warn( - UNKNOWN_LOCATION_MESSAGE.format(location=value), + UNKNOWN_LOCATION_MESSAGE.format(location=location, possibility=possibility), # There are many layers before we get to (possibly) the user's code: # -> bpd.options.bigquery.location = "us-central-1" # -> location.setter diff --git a/setup.py b/setup.py index 2ccf63259cd..d5d282d11a2 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + import io import itertools import os @@ -45,6 +46,7 @@ "google-cloud-resource-manager >=1.10.3", "google-cloud-storage >=2.0.0", "ibis-framework[bigquery] >=8.0.0,<9.0.0dev", + "jellyfish >=0.8.9", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. "pandas >=1.5.0", "pyarrow >=8.0.0", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index f5007ed564f..3c51668655c 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -11,6 +11,7 @@ google-cloud-iam==2.12.1 google-cloud-resource-manager==1.10.3 google-cloud-storage==2.0.0 ibis-framework==8.0.0 +jellyfish==0.8.9 pandas==1.5.0 pyarrow==8.0.0 pydata-google-auth==1.8.2 diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index 7d9a452f424..b827b0723d9 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -108,24 +108,25 @@ def test_location_set_to_valid_no_warning(valid_location): @pytest.mark.parametrize( [ "invalid_location", + "possibility", ], [ # Test with common mistakes, see article. # https://en.wikipedia.org/wiki/Edit_distance#Formal_definition_and_properties # Substitution - ("us-wist-3",), + ("us-wist3", "us-west3"), # Insertion - ("us-central-1",), + ("us-central-1", "us-central1"), # Deletion - ("asia-suth2",), + ("asia-suth2", "asia-south2"), ], ) -def test_location_set_to_invalid_warning(invalid_location): +def test_location_set_to_invalid_warning(invalid_location, possibility): options = bigquery_options.BigQueryOptions() with pytest.warns( bigframes.exceptions.UnknownLocationWarning, match=re.escape( - f"The location '{invalid_location}' is set to an unknown value." + f"The location '{invalid_location}' is set to an unknown value. Did you mean '{possibility}'?" ), ): options.location = invalid_location From 93416ed2f8353c12eb162e21e9bf155312b0ed8c Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 9 May 2024 13:20:20 -0700 Subject: [PATCH 557/921] docs: add code snippets for llm text generatiion (#669) * docs: add code snippets for llm text generatiion --- samples/snippets/text_generation_test.py | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 samples/snippets/text_generation_test.py diff --git a/samples/snippets/text_generation_test.py b/samples/snippets/text_generation_test.py new file mode 100644 index 00000000000..c4df1dde3b7 --- /dev/null +++ b/samples/snippets/text_generation_test.py @@ -0,0 +1,68 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_llm_text_generation() -> None: + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") + LOCATION = "US" + + # [START bigquery_dataframes_generate_text_tutorial_create_remote_model] + import bigframes + from bigframes.ml.llm import PaLM2TextGenerator + + bigframes.options.bigquery.project = PROJECT_ID + bigframes.options.bigquery.location = LOCATION + + model = PaLM2TextGenerator() + # [END bigquery_dataframes_generate_text_tutorial_create_remote_model] + assert model is not None + + # [START bigquery_dataframes_generate_text_tutorial_perform_keyword_extraction] + import bigframes.pandas as bpd + + df = bpd.read_gbq("bigquery-public-data.imdb.reviews", max_results=5) + df_prompt_prefix = "Extract the key words from the text below: " + df_prompt = df_prompt_prefix + df["review"] + + # Predict using the model + df_pred = model.predict(df_prompt, temperature=0.2, max_output_tokens=100) + df_pred.peek(5) + # [END bigquery_dataframes_generate_text_tutorial_perform_keyword_extraction] + # peek() is used to show a preview of the results. If the output + # of this sample changes, also update the screenshot for the associated + # tutorial on cloud.google.com. + assert df_pred["ml_generate_text_llm_result"] is not None + assert df_pred["ml_generate_text_llm_result"].iloc[0] is not None + + # [START bigquery_dataframes_generate_text_tutorial_perform_sentiment_analysis] + import bigframes.pandas as bpd + + df = bpd.read_gbq("bigquery-public-data.imdb.reviews", max_results=5) + df_prompt_prefix = "perform sentiment analysis on the following text, return one the following categories: positive, negative: " + df_prompt = df_prompt_prefix + df["review"] + + # Predict using the model + df_pred = model.predict(df_prompt, temperature=0.2, max_output_tokens=100) + df_pred.peek(5) + # [END bigquery_dataframes_generate_text_tutorial_perform_sentiment_analysis] + # peek() is used to show a preview of the results. If the output + # of this sample changes, also update the screenshot for the associated + # tutorial on cloud.google.com. + + assert df_pred["ml_generate_text_llm_result"] is not None + assert df_pred["ml_generate_text_llm_result"].iloc[0] is not None From 2218c21b5bb0f9e54a365ba1ada0203cbc4c9efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 9 May 2024 21:17:57 -0500 Subject: [PATCH 558/921] feat: add `Series.case_when()` (#673) * feat: add `Series.case_when()` * rename to ScalarOp * rename to exprs * add type annotations feat: add `DataFrame.__delitem__` (#673) docs: add logistic regression samples (#673) --- bigframes/core/__init__.py | 17 ++- bigframes/core/blocks.py | 9 ++ bigframes/dataframe.py | 4 + bigframes/operations/__init__.py | 51 ++++--- bigframes/operations/base.py | 23 ++- bigframes/series.py | 19 +++ .../logistic_regression_prediction_test.py | 137 ++++++++++++++++++ tests/system/small/test_series.py | 30 ++++ .../bigframes_vendored/pandas/core/series.py | 55 +++++++ 9 files changed, 311 insertions(+), 34 deletions(-) create mode 100644 samples/snippets/logistic_regression_prediction_test.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index eef0efcf83b..79c6bb6495f 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -16,6 +16,7 @@ from dataclasses import dataclass import functools import io +import itertools import typing from typing import Iterable, Sequence @@ -370,14 +371,16 @@ def unpivot( for col_id, input_ids in unpivot_columns: # row explode offset used to choose the input column # we use offset instead of label as labels are not necessarily unique - cases = tuple( - ( - ops.eq_op.as_expr(explode_offsets_id, ex.const(i)), - ex.free_var(id_or_null) - if (id_or_null is not None) - else ex.const(None), + cases = itertools.chain( + *( + ( + ops.eq_op.as_expr(explode_offsets_id, ex.const(i)), + ex.free_var(id_or_null) + if (id_or_null is not None) + else ex.const(None), + ) + for i, id_or_null in enumerate(input_ids) ) - for i, id_or_null in enumerate(input_ids) ) col_expr = ops.case_when_op.as_expr(*cases) unpivot_exprs.append((col_expr, col_id)) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 402581eb6fd..277409f3a32 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -803,6 +803,15 @@ def apply_ternary_op( expr = op.as_expr(col_id_1, col_id_2, col_id_3) return self.project_expr(expr, result_label) + def apply_nary_op( + self, + columns: Iterable[str], + op: ops.NaryOp, + result_label: Label = None, + ) -> typing.Tuple[Block, str]: + expr = op.as_expr(*columns) + return self.project_expr(expr, result_label) + def multi_apply_window_op( self, columns: typing.Sequence[str], diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1f1fb5467f0..47730630e32 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -655,6 +655,10 @@ def _repr_html_(self) -> str: html_string += f"[{row_count} rows x {column_count} columns in total]" return html_string + def __delitem__(self, key: str): + df = self.drop(columns=[key]) + self._set_block(df._get_block()) + def __setitem__(self, key: str, value: SingleItemValue): df = self._assign_single_item(key, value) self._set_block(df._get_block()) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index a7c385a2b8a..e52f488d389 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -17,7 +17,7 @@ import dataclasses import functools import typing -from typing import Tuple, Union +from typing import Union import numpy as np import pandas as pd @@ -46,7 +46,7 @@ def order_preserving(self) -> bool: @dataclasses.dataclass(frozen=True) -class NaryOp: +class ScalarOp: @property def name(self) -> str: raise NotImplementedError("RowOp abstract base class has no implementation") @@ -60,10 +60,30 @@ def order_preserving(self) -> bool: return False +@dataclasses.dataclass(frozen=True) +class NaryOp(ScalarOp): + def as_expr( + self, + *exprs: Union[str | bigframes.core.expression.Expression], + ) -> bigframes.core.expression.Expression: + import bigframes.core.expression + + # Keep this in sync with output_type and compilers + inputs: list[bigframes.core.expression.Expression] = [] + + for expr in exprs: + inputs.append(_convert_expr_input(expr)) + + return bigframes.core.expression.OpExpression( + self, + tuple(inputs), + ) + + # These classes can be used to create simple ops that don't take local parameters # All is needed is a unique name, and to register an implementation in ibis_mappings.py @dataclasses.dataclass(frozen=True) -class UnaryOp(NaryOp): +class UnaryOp(ScalarOp): @property def arguments(self) -> int: return 1 @@ -79,7 +99,7 @@ def as_expr( @dataclasses.dataclass(frozen=True) -class BinaryOp(NaryOp): +class BinaryOp(ScalarOp): @property def arguments(self) -> int: return 2 @@ -101,7 +121,7 @@ def as_expr( @dataclasses.dataclass(frozen=True) -class TernaryOp(NaryOp): +class TernaryOp(ScalarOp): @property def arguments(self) -> int: return 3 @@ -655,27 +675,6 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT output_expr_types, ) - def as_expr( - self, - *case_output_pairs: Tuple[ - Union[str | bigframes.core.expression.Expression], - Union[str | bigframes.core.expression.Expression], - ], - ) -> bigframes.core.expression.Expression: - import bigframes.core.expression - - # Keep this in sync with output_type and compilers - inputs: list[bigframes.core.expression.Expression] = [] - - for case, output in case_output_pairs: - inputs.append(_convert_expr_input(case)) - inputs.append(_convert_expr_input(output)) - - return bigframes.core.expression.OpExpression( - self, - tuple(inputs), - ) - case_when_op = CaseWhenOp() diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index b003ce59cc2..75d14f3fbc1 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -15,6 +15,7 @@ from __future__ import annotations import typing +from typing import List, Sequence import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing import numpy @@ -205,6 +206,21 @@ def _apply_binary_op( block, result_id = self._block.project_expr(expr, name) return series.Series(block.select_column(result_id)) + def _apply_nary_op( + self, + op: ops.NaryOp, + others: Sequence[typing.Union[series.Series, scalars.Scalar]], + ignore_self=False, + ): + """Applies an n-ary operator to the series and others.""" + values, block = self._align_n(others, ignore_self=ignore_self) + block, result_id = block.apply_nary_op( + values, + op, + self._name, + ) + return series.Series(block.select_column(result_id)) + def _apply_binary_aggregation( self, other: series.Series, stat: agg_ops.BinaryAggregateOp ) -> float: @@ -226,8 +242,13 @@ def _align_n( self, others: typing.Sequence[typing.Union[series.Series, scalars.Scalar]], how="outer", + ignore_self=False, ) -> tuple[typing.Sequence[str], blocks.Block]: - value_ids = [self._value_column] + if ignore_self: + value_ids: List[str] = [] + else: + value_ids = [self._value_column] + block = self._block for other in others: if isinstance(other, series.Series): diff --git a/bigframes/series.py b/bigframes/series.py index aea3d60ff51..ce13d205bd0 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -410,6 +410,25 @@ def between(self, left, right, inclusive="both"): self._apply_binary_op(right, right_op) ) + def case_when(self, caselist) -> Series: + return self._apply_nary_op( + ops.case_when_op, + tuple( + itertools.chain( + itertools.chain(*caselist), + # Fallback to current value if no other matches. + ( + # We make a Series with a constant value to avoid casts to + # types other than boolean. + Series(True, index=self.index, dtype=pandas.BooleanDtype()), + self, + ), + ), + ), + # Self is already included in "others". + ignore_self=True, + ) + def cumsum(self) -> Series: return self._apply_window_op( agg_ops.sum_op, bigframes.core.window_spec.WindowSpec(following=0) diff --git a/samples/snippets/logistic_regression_prediction_test.py b/samples/snippets/logistic_regression_prediction_test.py new file mode 100644 index 00000000000..6a40369ba89 --- /dev/null +++ b/samples/snippets/logistic_regression_prediction_test.py @@ -0,0 +1,137 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BigQuery DataFrames code samples for +https://cloud.google.com/bigquery/docs/logistic-regression-prediction. +""" + + +def test_logistic_regression_prediction(random_model_id: str) -> None: + your_model_id = random_model_id + + # [START bigquery_dataframes_logistic_regression_prediction_examine] + import bigframes.pandas as bpd + + df = bpd.read_gbq( + "bigquery-public-data.ml_datasets.census_adult_income", + columns=( + "age", + "workclass", + "marital_status", + "education_num", + "occupation", + "hours_per_week", + "income_bracket", + "functional_weight", + ), + max_results=100, + ) + df.peek() + # Output: + # age workclass marital_status education_num occupation hours_per_week income_bracket functional_weight + # 47 Local-gov Married-civ-spouse 13 Prof-specialty 40 >50K 198660 + # 56 Private Never-married 9 Adm-clerical 40 <=50K 85018 + # 40 Private Married-civ-spouse 12 Tech-support 40 >50K 285787 + # 34 Self-emp-inc Married-civ-spouse 9 Craft-repair 54 >50K 207668 + # 23 Private Married-civ-spouse 10 Handlers-cleaners 40 <=50K 40060 + # [END bigquery_dataframes_logistic_regression_prediction_examine] + + # [START bigquery_dataframes_logistic_regression_prediction_prepare] + import bigframes.pandas as bpd + + input_data = bpd.read_gbq( + "bigquery-public-data.ml_datasets.census_adult_income", + columns=( + "age", + "workclass", + "marital_status", + "education_num", + "occupation", + "hours_per_week", + "income_bracket", + "functional_weight", + ), + ) + input_data["dataframe"] = bpd.Series("training", index=input_data.index,).case_when( + [ + (((input_data["functional_weight"] % 10) == 8), "evaluation"), + (((input_data["functional_weight"] % 10) == 9), "prediction"), + ] + ) + del input_data["functional_weight"] + # [END bigquery_dataframes_logistic_regression_prediction_prepare] + + # [START bigquery_dataframes_logistic_regression_prediction_create_model] + import bigframes.ml.linear_model + + # input_data is defined in an earlier step. + training_data = input_data[input_data["dataframe"] == "training"] + X = training_data.drop(columns=["income_bracket", "dataframe"]) + y = training_data["income_bracket"] + + census_model = bigframes.ml.linear_model.LogisticRegression() + census_model.fit(X, y) + + census_model.to_gbq( + your_model_id, # For example: "your-project.census.census_model" + replace=True, + ) + # [END bigquery_dataframes_logistic_regression_prediction_create_model] + + # [START bigquery_dataframes_logistic_regression_prediction_evaluate_model] + # Select model you'll use for predictions. `read_gbq_model` loads model + # data from BigQuery, but you could also use the `census_model` object + # from previous steps. + census_model = bpd.read_gbq_model( + your_model_id, # For example: "your-project.census.census_model" + ) + + # input_data is defined in an earlier step. + evaluation_data = input_data[input_data["dataframe"] == "evaluation"] + X = evaluation_data.drop(columns=["income_bracket", "dataframe"]) + y = evaluation_data["income_bracket"] + + # The score() method evaluates how the model performs compared to the + # actual data. Output DataFrame matches that of ML.EVALUATE(). + score = census_model.score(X, y) + score.peek() + # Output: + # precision recall accuracy f1_score log_loss roc_auc + # 0 0.685764 0.536685 0.83819 0.602134 0.350417 0.882953 + # [END bigquery_dataframes_logistic_regression_prediction_evaluate_model] + + # [START bigquery_dataframes_logistic_regression_prediction_predict_income_bracket] + # Select model you'll use for predictions. `read_gbq_model` loads model + # data from BigQuery, but you could also use the `census_model` object + # from previous steps. + census_model = bpd.read_gbq_model( + your_model_id, # For example: "your-project.census.census_model" + ) + + # input_data is defined in an earlier step. + prediction_data = input_data[input_data["dataframe"] == "prediction"] + + predictions = census_model.predict(prediction_data) + predictions.peek() + # Output: + # predicted_income_bracket predicted_income_bracket_probs age workclass ... occupation hours_per_week income_bracket dataframe + # 18004 <=50K [{'label': ' >50K', 'prob': 0.0763305999358786... 75 ? ... ? 6 <=50K prediction + # 18886 <=50K [{'label': ' >50K', 'prob': 0.0448866871906495... 73 ? ... ? 22 >50K prediction + # 31024 <=50K [{'label': ' >50K', 'prob': 0.0362982319421936... 69 ? ... ? 1 <=50K prediction + # 31022 <=50K [{'label': ' >50K', 'prob': 0.0787836112058324... 75 ? ... ? 5 <=50K prediction + # 23295 <=50K [{'label': ' >50K', 'prob': 0.3385373037905673... 78 ? ... ? 32 <=50K prediction + # [END bigquery_dataframes_logistic_regression_prediction_predict_income_bracket] + + # TODO(tswast): Implement ML.EXPLAIN_PREDICT() and corresponding sample. + # TODO(tswast): Implement ML.GLOBAL_EXPLAIN() and corresponding sample. diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 38aed19f05f..beb99b1adab 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2565,6 +2565,36 @@ def test_between(scalars_df_index, scalars_pandas_df_index, left, right, inclusi ) +def test_case_when(scalars_df_index, scalars_pandas_df_index): + pytest.importorskip( + "pandas", + minversion="2.2.0", + reason="case_when added in pandas 2.2.0", + ) + + bf_series = scalars_df_index["int64_col"] + pd_series = scalars_pandas_df_index["int64_col"] + + # TODO(tswast): pandas case_when appears to assume True when a value is + # null. I suspect this should be considered a bug in pandas. + bf_result = bf_series.case_when( + [ + ((bf_series > 100).fillna(True), 1000), + ((bf_series < -100).fillna(True), -1000), + ] + ).to_pandas() + pd_result = pd_series.case_when( + [ + (pd_series > 100, 1000), + (pd_series < -100, -1000), + ] + ) + pd.testing.assert_series_equal( + bf_result, + pd_result.astype(pd.Int64Dtype()), + ) + + def test_to_frame(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 4833c41ff70..e155fb073ae 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -6,10 +6,12 @@ from typing import ( Hashable, IO, + List, Literal, Mapping, Optional, Sequence, + Tuple, TYPE_CHECKING, Union, ) @@ -1937,6 +1939,59 @@ def between( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def case_when( + self, + caselist: List[Tuple[Series, Series]], + ) -> Series: + """Replace values where the conditions are True. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> c = bpd.Series([6, 7, 8, 9], name="c") + >>> a = bpd.Series([0, 0, 1, 2]) + >>> b = bpd.Series([0, 3, 4, 5]) + + >>> c.case_when( + ... caselist=[ + ... (a.gt(0), a), # condition, replacement + ... (b.gt(0), b), + ... ] + ... ) + 0 6 + 1 3 + 2 1 + 3 2 + Name: c, dtype: Int64 + + **See also:** + + - :func:`bigframes.series.Series.mask` : Replace values where the condition is True. + + Args: + caselist: + A list of tuples of conditions and expected replacements + Takes the form: ``(condition0, replacement0)``, + ``(condition1, replacement1)``, ... . + ``condition`` should be a 1-D boolean array-like object + or a callable. If ``condition`` is a callable, + it is computed on the Series + and should return a boolean Series or array. + The callable must not change the input Series + (though pandas doesn`t check it). ``replacement`` should be a + 1-D array-like object, a scalar or a callable. + If ``replacement`` is a callable, it is computed on the Series + and should return a scalar or Series. The callable + must not change the input Series + (though pandas doesn`t check it). + + Returns: + bigframes.series.Series + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def cumprod(self): """ Return cumulative product over a DataFrame or Series axis. From f2ed29cba8866508d1c68e45818c275b99732333 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 10 May 2024 09:21:52 -0700 Subject: [PATCH 559/921] refactor: Distinguish between range and row windows (#672) --- bigframes/core/block_transforms.py | 49 +++++----- bigframes/core/blocks.py | 7 +- bigframes/core/compile/compiled.py | 42 ++++++--- bigframes/core/groupby/__init__.py | 27 +++--- bigframes/core/reshape/__init__.py | 6 +- bigframes/core/window_spec.py | 129 +++++++++++++++++++++++++-- bigframes/dataframe.py | 27 +++--- bigframes/operations/aggregations.py | 44 +++++---- bigframes/series.py | 24 ++--- 9 files changed, 249 insertions(+), 106 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index a221b343a5b..e12e6bf054a 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -71,21 +71,19 @@ def indicate_duplicates( if keep == "first": # Count how many copies occur up to current copy of value # Discard this value if there are copies BEFORE - window_spec = windows.WindowSpec( + window_spec = windows.cumulative_rows( grouping_keys=tuple(columns), - following=0, ) elif keep == "last": # Count how many copies occur up to current copy of values # Discard this value if there are copies AFTER - window_spec = windows.WindowSpec( + window_spec = windows.inverse_cumulative_rows( grouping_keys=tuple(columns), - preceding=0, ) else: # keep == False # Count how many copies of the value occur in entire series. # Discard this value if there are copies ANYWHERE - window_spec = windows.WindowSpec(grouping_keys=tuple(columns)) + window_spec = windows.unbound(grouping_keys=tuple(columns)) block, dummy = block.create_constant(1) block, val_count_col_id = block.apply_window_op( dummy, @@ -114,7 +112,7 @@ def quantile( dropna: bool = False, ) -> blocks.Block: # TODO: handle windowing and more interpolation methods - window = core.WindowSpec( + window = windows.unbound( grouping_keys=tuple(grouping_column_ids), ) quantile_cols = [] @@ -212,8 +210,8 @@ def _interpolate_column( if interpolate_method not in ["linear", "nearest", "ffill"]: raise ValueError("interpolate method not supported") window_ordering = (ordering.OrderingExpression(ex.free_var(x_values)),) - backwards_window = windows.WindowSpec(following=0, ordering=window_ordering) - forwards_window = windows.WindowSpec(preceding=0, ordering=window_ordering) + backwards_window = windows.rows(following=0, ordering=window_ordering) + forwards_window = windows.rows(preceding=0, ordering=window_ordering) # Note, this method may block, notnull = block.apply_unary_op(column, ops.notnull_op) @@ -364,7 +362,7 @@ def value_counts( ) count_id = agg_ids[0] if normalize: - unbound_window = windows.WindowSpec() + unbound_window = windows.unbound() block, total_count_id = block.apply_window_op( count_id, agg_ops.sum_op, unbound_window ) @@ -388,7 +386,7 @@ def value_counts( def pct_change(block: blocks.Block, periods: int = 1) -> blocks.Block: column_labels = block.column_labels - window_spec = windows.WindowSpec( + window_spec = windows.rows( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -430,23 +428,22 @@ def rank( ops.isnull_op, ) nullity_col_ids.append(nullity_col_id) - window = windows.WindowSpec( - # BigQuery has syntax to reorder nulls with "NULLS FIRST/LAST", but that is unavailable through ibis presently, so must order on a separate nullity expression first. - ordering=( - ordering.OrderingExpression( - ex.free_var(col), - ordering.OrderingDirection.ASC - if ascending - else ordering.OrderingDirection.DESC, - na_last=(na_option in ["bottom", "keep"]), - ), + window_ordering = ( + ordering.OrderingExpression( + ex.free_var(col), + ordering.OrderingDirection.ASC + if ascending + else ordering.OrderingDirection.DESC, + na_last=(na_option in ["bottom", "keep"]), ), ) # Count_op ignores nulls, so if na_option is "top" or "bottom", we instead count the nullity columns, where nulls have been mapped to bools block, rownum_id = block.apply_window_op( col if na_option == "keep" else nullity_col_id, agg_ops.dense_rank_op if method == "dense" else agg_ops.count_op, - window_spec=window, + window_spec=windows.unbound(ordering=window_ordering) + if method == "dense" + else windows.rows(following=0, ordering=window_ordering), skip_reproject_unsafe=(col != columns[-1]), ) rownum_col_ids.append(rownum_id) @@ -464,7 +461,7 @@ def rank( block, result_id = block.apply_window_op( rownum_col_ids[i], agg_op, - window_spec=windows.WindowSpec(grouping_keys=(columns[i],)), + window_spec=windows.unbound(grouping_keys=(columns[i],)), skip_reproject_unsafe=(i < (len(columns) - 1)), ) post_agg_rownum_col_ids.append(result_id) @@ -528,7 +525,7 @@ def nsmallest( block, counter = block.apply_window_op( column_ids[0], agg_ops.rank_op, - window_spec=windows.WindowSpec(ordering=tuple(order_refs)), + window_spec=windows.unbound(ordering=tuple(order_refs)), ) block, condition = block.project_expr(ops.le_op.as_expr(counter, ex.const(n))) block = block.filter_by_id(condition) @@ -558,7 +555,7 @@ def nlargest( block, counter = block.apply_window_op( column_ids[0], agg_ops.rank_op, - window_spec=windows.WindowSpec(ordering=tuple(order_refs)), + window_spec=windows.unbound(ordering=tuple(order_refs)), ) block, condition = block.project_expr(ops.le_op.as_expr(counter, ex.const(n))) block = block.filter_by_id(condition) @@ -653,7 +650,7 @@ def _mean_delta_to_power( grouping_column_ids: typing.Sequence[str], ) -> typing.Tuple[blocks.Block, typing.Sequence[str]]: """Calculate (x-mean(x))^n. Useful for calculating moment statistics such as skew and kurtosis.""" - window = windows.WindowSpec(grouping_keys=tuple(grouping_column_ids)) + window = windows.unbound(grouping_keys=tuple(grouping_column_ids)) block, mean_ids = block.multi_apply_window_op(column_ids, agg_ops.mean_op, window) delta_ids = [] for val_id, mean_val_id in zip(column_ids, mean_ids): @@ -845,7 +842,7 @@ def _idx_extrema( for idx_col in original_block.index_columns ], ] - window_spec = windows.WindowSpec(ordering=tuple(order_refs)) + window_spec = windows.unbound(ordering=tuple(order_refs)) idx_col = original_block.index_columns[0] block, result_col = block.apply_window_op( idx_col, agg_ops.first_op, window_spec diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 277409f3a32..2b2803b6496 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -47,6 +47,7 @@ import bigframes.core.tree_properties as tree_properties import bigframes.core.utils import bigframes.core.utils as utils +import bigframes.core.window_spec as window_specs import bigframes.dtypes import bigframes.features import bigframes.operations as ops @@ -816,7 +817,7 @@ def multi_apply_window_op( self, columns: typing.Sequence[str], op: agg_ops.WindowOp, - window_spec: core.WindowSpec, + window_spec: window_specs.WindowSpec, *, skip_null_groups: bool = False, never_skip_nulls: bool = False, @@ -875,7 +876,7 @@ def apply_window_op( self, column: str, op: agg_ops.WindowOp, - window_spec: core.WindowSpec, + window_spec: window_specs.WindowSpec, *, result_label: Label = None, skip_null_groups: bool = False, @@ -2029,7 +2030,7 @@ def _is_monotonic( return self._stats_cache[column_name][op_name] period = 1 - window = bigframes.core.WindowSpec( + window = window_specs.rows( preceding=period, following=None, ) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index d14a5d3241c..cc1d6baaa1c 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -40,7 +40,7 @@ OrderingExpression, ) import bigframes.core.schema as schemata -from bigframes.core.window_spec import WindowSpec +from bigframes.core.window_spec import RangeWindowBounds, RowsWindowBounds, WindowSpec import bigframes.dtypes import bigframes.operations.aggregations as agg_ops @@ -735,7 +735,9 @@ def project_window_op( skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection """ column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) - window = self._ibis_window_from_spec(window_spec, allow_ties=op.handles_ties) + window = self._ibis_window_from_spec( + window_spec, require_total_order=op.uses_total_row_ordering + ) bindings = {col: self._get_ibis_column(col) for col in self.column_ids} window_op = agg_compiler.compile_analytic( @@ -1162,7 +1164,9 @@ def _create_string_ordering_column(self) -> ibis_types.StringColumn: def _compile_expression(self, expr: ex.Expression): return op_compiler.compile_expression(expr, self._ibis_bindings) - def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = False): + def _ibis_window_from_spec( + self, window_spec: WindowSpec, require_total_order: bool + ): group_by: typing.List[ibis_types.Value] = ( [ typing.cast( @@ -1175,26 +1179,40 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec, allow_ties: bool = Fal ) if self._reduced_predicate is not None: group_by.append(self._reduced_predicate) + + # Construct ordering. There are basically 3 main cases + # 1. Order-independent op (aggregation, cut, rank) with unbound window - no ordering clause needed + # 2. Order-independent op (aggregation, cut, rank) with range window - use ordering clause, ties allowed + # 3. Order-depedenpent op (navigation functions, array_agg) or rows bounds - use total row order to break ties. if window_spec.ordering: order_by = _convert_ordering_to_table_values( {**self._column_names, **self._hidden_ordering_column_names}, window_spec.ordering, ) - if not allow_ties: - # Most operator need an unambiguous ordering, so the table's total ordering is appended + if require_total_order or isinstance(window_spec.bounds, RowsWindowBounds): + # Some operators need an unambiguous ordering, so the table's total ordering is appended order_by = tuple([*order_by, *self._ibis_order]) - elif (window_spec.following is not None) or (window_spec.preceding is not None): + elif isinstance(window_spec.bounds, RowsWindowBounds): # If window spec has following or preceding bounds, we need to apply an unambiguous ordering. order_by = tuple(self._ibis_order) else: # Unbound grouping window. Suitable for aggregations but not for analytic function application. order_by = None - return ibis.window( - preceding=window_spec.preceding, - following=window_spec.following, - order_by=order_by, - group_by=group_by, - ) + + bounds = window_spec.bounds + window = 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): + window = window.preceding_following( + bounds.preceding, bounds.following, how="rows" + ) + else: + raise ValueError(f"unrecognized window bounds {bounds}") + return window class Builder: def __init__( diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 05b1cc7f419..41d0750030f 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -28,6 +28,7 @@ import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.core.window as windows +import bigframes.core.window_spec as window_specs import bigframes.dataframe as df import bigframes.dtypes as dtypes import bigframes.operations.aggregations as agg_ops @@ -217,7 +218,7 @@ def cumprod(self, *args, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.product_op, numeric_only=True) def shift(self, periods=1) -> series.Series: - window = core.WindowSpec( + window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, @@ -225,7 +226,7 @@ def shift(self, periods=1) -> series.Series: return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) def diff(self, periods=1) -> series.Series: - window = core.WindowSpec( + window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, @@ -234,7 +235,7 @@ def diff(self, periods=1) -> series.Series: 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 = core.WindowSpec( + window_spec = window_specs.rows( grouping_keys=tuple(self._by_col_ids), preceding=window - 1, following=0, @@ -248,9 +249,8 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: ) def expanding(self, min_periods: int = 1) -> windows.Window: - window_spec = core.WindowSpec( + window_spec = window_specs.cumulative_rows( grouping_keys=tuple(self._by_col_ids), - following=0, min_periods=min_periods, ) block = self._block.order_by( @@ -424,8 +424,8 @@ def _apply_window_op( numeric_only: bool = False, ): """Apply window op to groupby. Defaults to grouped cumulative window.""" - window_spec = window or core.WindowSpec( - grouping_keys=tuple(self._by_col_ids), following=0 + window_spec = window or window_specs.cumulative_rows( + grouping_keys=tuple(self._by_col_ids) ) columns = self._aggregated_columns(numeric_only=numeric_only) block, result_ids = self._block.multi_apply_window_op( @@ -594,7 +594,7 @@ def cumcount(self, *args, **kwargs) -> series.Series: def shift(self, periods=1) -> series.Series: """Shift index by desired number of periods.""" - window = core.WindowSpec( + window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, @@ -602,7 +602,7 @@ def shift(self, periods=1) -> series.Series: return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) def diff(self, periods=1) -> series.Series: - window = core.WindowSpec( + window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, @@ -611,7 +611,7 @@ def diff(self, periods=1) -> series.Series: 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 = core.WindowSpec( + window_spec = window_specs.rows( grouping_keys=tuple(self._by_col_ids), preceding=window - 1, following=0, @@ -629,9 +629,8 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: ) def expanding(self, min_periods: int = 1) -> windows.Window: - window_spec = core.WindowSpec( + window_spec = window_specs.cumulative_rows( grouping_keys=tuple(self._by_col_ids), - following=0, min_periods=min_periods, ) block = self._block.order_by( @@ -661,8 +660,8 @@ def _apply_window_op( window: typing.Optional[core.WindowSpec] = None, ): """Apply window op to groupby. Defaults to grouped cumulative window.""" - window_spec = window or core.WindowSpec( - grouping_keys=tuple(self._by_col_ids), following=0 + window_spec = window or window_specs.cumulative_rows( + grouping_keys=tuple(self._by_col_ids) ) label = self._value_name if not discard_name else None diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index 6bcc25319b8..05cb5c7e94b 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -19,10 +19,10 @@ import pandas as pd import bigframes.constants as constants -import bigframes.core as core import bigframes.core.expression as ex import bigframes.core.ordering as order import bigframes.core.utils as utils +import bigframes.core.window_spec as window_specs import bigframes.dataframe import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -159,7 +159,7 @@ def cut( ) return x._apply_window_op( - agg_ops.CutOp(bins, labels=labels), window_spec=core.WindowSpec() + agg_ops.CutOp(bins, labels=labels), window_spec=window_specs.unbound() ) @@ -189,7 +189,7 @@ def qcut( block, result = block.apply_window_op( x._value_column, agg_ops.QcutOp(q), # type: ignore - window_spec=core.WindowSpec( + window_spec=window_specs.unbound( grouping_keys=(nullity_id,), ordering=(order.ascending_over(x._value_column),), ), diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py index b02f13d333e..71e88a4c3d6 100644 --- a/bigframes/core/window_spec.py +++ b/bigframes/core/window_spec.py @@ -11,13 +11,133 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from dataclasses import dataclass -import typing +from typing import Optional, Tuple, Union import bigframes.core.ordering as orderings +# Unbound Windows +def unbound( + grouping_keys: Tuple[str, ...] = (), + min_periods: int = 0, + ordering: Tuple[orderings.OrderingExpression, ...] = (), +) -> WindowSpec: + """ + Create an unbound window. + + Args: + grouping_keys: + Columns ids of grouping keys + min_periods (int, default 0): + Minimum number of input rows to generate output. + ordering: + Orders the rows within the window. + + Returns: + WindowSpec + """ + return WindowSpec( + grouping_keys=grouping_keys, min_periods=min_periods, ordering=ordering + ) + + +### Rows-based Windows +def rows( + grouping_keys: Tuple[str, ...] = (), + preceding: Optional[int] = None, + following: Optional[int] = None, + min_periods: int = 0, + ordering: Tuple[orderings.OrderingExpression, ...] = (), +) -> WindowSpec: + """ + Create a row-bounded window. + + Args: + grouping_keys: + Columns ids of grouping keys + preceding: + number of preceding rows to include. If None, include all preceding rows + following: + number of following rows to include. If None, include all following rows + min_periods (int, default 0): + Minimum number of input rows to generate output. + ordering: + Ordering to apply on top of based dataframe ordering + Returns: + WindowSpec + """ + assert (preceding is not None) or (following is not None) + bounds = RowsWindowBounds(preceding=preceding, following=following) + return WindowSpec( + grouping_keys=grouping_keys, + bounds=bounds, + min_periods=min_periods, + ordering=ordering, + ) + + +def cumulative_rows( + grouping_keys: Tuple[str, ...] = (), min_periods: int = 0 +) -> WindowSpec: + """ + Create a expanding window that includes all preceding rows + + Args: + grouping_keys: + Columns ids of grouping keys + min_periods (int, default 0): + Minimum number of input rows to generate output. + Returns: + WindowSpec + """ + bounds = RowsWindowBounds(following=0) + return WindowSpec( + grouping_keys=grouping_keys, bounds=bounds, min_periods=min_periods + ) + + +def inverse_cumulative_rows( + grouping_keys: Tuple[str, ...] = (), min_periods: int = 0 +) -> WindowSpec: + """ + Create a shrinking window that includes all following rows + + Args: + grouping_keys: + Columns ids of grouping keys + min_periods (int, default 0): + Minimum number of input rows to generate output. + Returns: + WindowSpec + """ + bounds = RowsWindowBounds(preceding=0) + return WindowSpec( + grouping_keys=grouping_keys, bounds=bounds, min_periods=min_periods + ) + + +### Struct Classes + + +@dataclass(frozen=True) +class RowsWindowBounds: + preceding: Optional[int] = None + following: Optional[int] = None + + +# TODO: Expand to datetime offsets +OffsetType = Union[float, int] + + +@dataclass(frozen=True) +class RangeWindowBounds: + preceding: Optional[OffsetType] = None + following: Optional[OffsetType] = None + + @dataclass(frozen=True) class WindowSpec: """ @@ -28,8 +148,7 @@ class WindowSpec: ordering: List of columns ids and ordering direction to override base ordering """ - grouping_keys: typing.Tuple[str, ...] = tuple() - ordering: typing.Tuple[orderings.OrderingExpression, ...] = tuple() - preceding: typing.Optional[int] = None - following: typing.Optional[int] = None + grouping_keys: Tuple[str, ...] = tuple() + ordering: Tuple[orderings.OrderingExpression, ...] = tuple() + bounds: Union[RowsWindowBounds, RangeWindowBounds, None] = None min_periods: int = 0 diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 47730630e32..5be28acf535 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -59,6 +59,7 @@ import bigframes.core.ordering as order import bigframes.core.utils as utils import bigframes.core.window +import bigframes.core.window_spec as window_spec import bigframes.dtypes import bigframes.formatting_helpers as formatter import bigframes.operations as ops @@ -1874,11 +1875,11 @@ def replace( ) def ffill(self, *, limit: typing.Optional[int] = None) -> DataFrame: - window = bigframes.core.WindowSpec(preceding=limit, following=0) + window = window_spec.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) def bfill(self, *, limit: typing.Optional[int] = None) -> DataFrame: - window = bigframes.core.WindowSpec(preceding=0, following=limit) + window = window_spec.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) def isin(self, values) -> DataFrame: @@ -2574,17 +2575,17 @@ 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_spec = bigframes.core.WindowSpec( + window_def = window_spec.rows( preceding=window - 1, following=0, min_periods=min_periods or window ) return bigframes.core.window.Window( - self._block, window_spec, self._block.value_columns + self._block, window_def, self._block.value_columns ) def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: - window_spec = bigframes.core.WindowSpec(following=0, min_periods=min_periods) + window = window_spec.cumulative_rows(min_periods=min_periods) return bigframes.core.window.Window( - self._block, window_spec, self._block.value_columns + self._block, window, self._block.value_columns ) def groupby( @@ -2691,7 +2692,7 @@ def cumsum(self): raise ValueError("All values must be numeric to apply cumsum.") return self._apply_window_op( agg_ops.sum_op, - bigframes.core.WindowSpec(following=0), + window_spec.cumulative_rows(), ) def cumprod(self) -> DataFrame: @@ -2703,30 +2704,30 @@ def cumprod(self) -> DataFrame: raise ValueError("All values must be numeric to apply cumsum.") return self._apply_window_op( agg_ops.product_op, - bigframes.core.WindowSpec(following=0), + window_spec.cumulative_rows(), ) def cummin(self) -> DataFrame: return self._apply_window_op( agg_ops.min_op, - bigframes.core.WindowSpec(following=0), + window_spec.cumulative_rows(), ) def cummax(self) -> DataFrame: return self._apply_window_op( agg_ops.max_op, - bigframes.core.WindowSpec(following=0), + window_spec.cumulative_rows(), ) def shift(self, periods: int = 1) -> DataFrame: - window = bigframes.core.WindowSpec( + window = window_spec.rows( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) def diff(self, periods: int = 1) -> DataFrame: - window = bigframes.core.WindowSpec( + window = window_spec.rows( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -2740,7 +2741,7 @@ def pct_change(self, periods: int = 1) -> DataFrame: def _apply_window_op( self, op: agg_ops.WindowOp, - window_spec: bigframes.core.WindowSpec, + window_spec: window_spec.WindowSpec, ): block, result_ids = self._block.multi_apply_window_op( self._block.value_columns, diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 0d27d1d75d3..c57fac41129 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -34,8 +34,8 @@ def skips_nulls(self): return True @property - def handles_ties(self): - """Whether the operator can handle ties without nondeterministic output. (eg. rank operator can handle ties but not the count operator)""" + def uses_total_row_ordering(self): + """Whether the operator needs total row ordering. (eg. lead, lag, array_agg)""" return False @abc.abstractmethod @@ -232,10 +232,6 @@ class CutOp(UnaryWindowOp): def skips_nulls(self): return False - @property - def handles_ties(self): - return True - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: if isinstance(self.bins, int) and (self.labels is False): return dtypes.INT_DTYPE @@ -267,10 +263,6 @@ def name(self): def skips_nulls(self): return False - @property - def handles_ties(self): - return True - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return signatures.FixedOutputType( dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" @@ -308,10 +300,6 @@ class RankOp(UnaryWindowOp): def skips_nulls(self): return False - @property - def handles_ties(self): - return True - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return signatures.FixedOutputType( dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" @@ -324,10 +312,6 @@ class DenseRankOp(UnaryWindowOp): def skips_nulls(self): return False - @property - def handles_ties(self): - return True - def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return signatures.FixedOutputType( dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" @@ -338,9 +322,17 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT class FirstOp(UnaryWindowOp): name: ClassVar[str] = "first" + @property + def uses_total_row_ordering(self): + return True + @dataclasses.dataclass(frozen=True) class FirstNonNullOp(UnaryWindowOp): + @property + def uses_total_row_ordering(self): + return True + @property def skips_nulls(self): return False @@ -350,9 +342,17 @@ def skips_nulls(self): class LastOp(UnaryWindowOp): name: ClassVar[str] = "last" + @property + def uses_total_row_ordering(self): + return True + @dataclasses.dataclass(frozen=True) class LastNonNullOp(UnaryWindowOp): + @property + def uses_total_row_ordering(self): + return True + @property def skips_nulls(self): return False @@ -362,6 +362,10 @@ def skips_nulls(self): class ShiftOp(UnaryWindowOp): periods: int + @property + def uses_total_row_ordering(self): + return True + @property def skips_nulls(self): return False @@ -371,6 +375,10 @@ def skips_nulls(self): class DiffOp(UnaryWindowOp): periods: int + @property + def uses_total_row_ordering(self): + return True + @property def skips_nulls(self): return False diff --git a/bigframes/series.py b/bigframes/series.py index ce13d205bd0..313380e4a4f 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -431,44 +431,44 @@ def case_when(self, caselist) -> Series: def cumsum(self) -> Series: return self._apply_window_op( - agg_ops.sum_op, bigframes.core.window_spec.WindowSpec(following=0) + agg_ops.sum_op, bigframes.core.window_spec.cumulative_rows() ) def ffill(self, *, limit: typing.Optional[int] = None) -> Series: - window = bigframes.core.window_spec.WindowSpec(preceding=limit, following=0) + window = bigframes.core.window_spec.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) pad = ffill pad.__doc__ = inspect.getdoc(vendored_pandas_series.Series.ffill) def bfill(self, *, limit: typing.Optional[int] = None) -> Series: - window = bigframes.core.window_spec.WindowSpec(preceding=0, following=limit) + window = bigframes.core.window_spec.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) def cummax(self) -> Series: return self._apply_window_op( - agg_ops.max_op, bigframes.core.window_spec.WindowSpec(following=0) + agg_ops.max_op, bigframes.core.window_spec.cumulative_rows() ) def cummin(self) -> Series: return self._apply_window_op( - agg_ops.min_op, bigframes.core.window_spec.WindowSpec(following=0) + agg_ops.min_op, bigframes.core.window_spec.cumulative_rows() ) def cumprod(self) -> Series: return self._apply_window_op( - agg_ops.product_op, bigframes.core.window_spec.WindowSpec(following=0) + agg_ops.product_op, bigframes.core.window_spec.cumulative_rows() ) def shift(self, periods: int = 1) -> Series: - window = bigframes.core.window_spec.WindowSpec( + window = bigframes.core.window_spec.rows( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) def diff(self, periods: int = 1) -> Series: - window = bigframes.core.window_spec.WindowSpec( + window = bigframes.core.window_spec.rows( preceding=periods if periods > 0 else None, following=-periods if periods < 0 else None, ) @@ -955,7 +955,7 @@ def mode(self) -> Series: block, max_value_count_col_id = block.apply_window_op( value_count_col_id, agg_ops.max_op, - window_spec=bigframes.core.window_spec.WindowSpec(), + window_spec=bigframes.core.window_spec.unbound(), ) block, is_mode_col_id = block.apply_binary_op( value_count_col_id, @@ -1226,7 +1226,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 = bigframes.core.window_spec.WindowSpec( + window_spec = bigframes.core.window_spec.rows( preceding=window - 1, following=0, min_periods=min_periods or window ) return bigframes.core.window.Window( @@ -1234,8 +1234,8 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window ) def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: - window_spec = bigframes.core.window_spec.WindowSpec( - following=0, min_periods=min_periods + window_spec = bigframes.core.window_spec.cumulative_rows( + min_periods=min_periods ) return bigframes.core.window.Window( self._block, window_spec, self._block.value_columns, is_series=True From f6bdc4aeb3f81a1e0b955521c04ac0dd22981c76 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 10 May 2024 19:50:35 +0000 Subject: [PATCH 560/921] feat: Support `axis=1` in `df.apply` for scalar outputs (#629) * feat: Support `axis=1` in `df.apply` for scalar outputs * avoid mixing other changes in the input_types param * use guid instead of hard coded column name * check_exact=False to avoid failing system_prerelease * handle index in remote function, add large system tests * make the test case more robust * handle non-string column names, add unsupported dtype tests * fix import * use `_cached` in df.apply to catch any rf execution errors early * add test for row aggregates * add row dtype information, also test * preserve the order of input in the output * absorb to_numpy() disparity in prerelease tests * add tests for column multiindex and non remote function * add preview note for row processing * add warning for input_types="row" and axis=1 * introduce early check on the supported dtypes * asjust test after early dtype handling * address review comments * user NameError for column name parsing issue, address test coverage failure * address nan return handling in the gcf code * handle (nan, inf, -inf) * replace "row" by bpd.Series for input types * make the bq parity assert more readable * fix the series name before assert * fix docstring for args * move more low level string logic in sql module * raise explicit error when a column name cannot be supported * keep literal_eval check on the serialization side to match deserialization --- bigframes/core/blocks.py | 101 ++++++- bigframes/core/sql.py | 59 +++++ bigframes/dataframe.py | 56 +++- bigframes/exceptions.py | 4 + bigframes/functions/remote_function.py | 191 ++++++++++--- bigframes/session/__init__.py | 8 +- tests/system/large/test_remote_function.py | 250 ++++++++++++++++++ tests/system/small/test_remote_function.py | 146 ++++++++++ .../bigframes_vendored/pandas/core/frame.py | 29 +- 9 files changed, 792 insertions(+), 52 deletions(-) create mode 100644 bigframes/core/sql.py diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 2b2803b6496..58b85154184 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -21,11 +21,13 @@ from __future__ import annotations +import ast import dataclasses import functools import itertools import os import random +import textwrap import typing from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple, Union import warnings @@ -44,8 +46,8 @@ import bigframes.core.join_def as join_defs import bigframes.core.ordering as ordering import bigframes.core.schema as bf_schema +import bigframes.core.sql as sql import bigframes.core.tree_properties as tree_properties -import bigframes.core.utils import bigframes.core.utils as utils import bigframes.core.window_spec as window_specs import bigframes.dtypes @@ -1437,9 +1439,7 @@ def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: ) def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: - axis_number = bigframes.core.utils.get_axis_number( - "rows" if (axis is None) else axis - ) + axis_number = utils.get_axis_number("rows" if (axis is None) else axis) if axis_number == 0: expr = self._expr for index_col in self._index_columns: @@ -1460,9 +1460,7 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: return self.rename(columns=lambda label: f"{prefix}{label}") def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: - axis_number = bigframes.core.utils.get_axis_number( - "rows" if (axis is None) else axis - ) + axis_number = utils.get_axis_number("rows" if (axis is None) else axis) if axis_number == 0: expr = self._expr for index_col in self._index_columns: @@ -2072,6 +2070,95 @@ def _is_monotonic( self._stats_cache[column_name].update({op_name: result}) return result + def _get_rows_as_json_values(self) -> Block: + # We want to preserve any ordering currently present before turning to + # direct SQL manipulation. We will restore the ordering when we rebuild + # expression. + # TODO(shobs): Replace direct SQL manipulation by structured expression + # manipulation + ordering_column_name = guid.generate_guid() + expr = self.session._cache_with_offsets(self.expr) + expr = expr.promote_offsets(ordering_column_name) + expr_sql = self.session._to_sql(expr) + + # Names of the columns to serialize for the row. + # We will use the repr-eval pattern to serialize a value here and + # deserialize in the cloud function. Let's make sure that would work. + column_names = [] + for col in list(self.index_columns) + [col for col in self.column_labels]: + serialized_column_name = repr(col) + try: + ast.literal_eval(serialized_column_name) + except Exception: + raise NameError( + f"Column name type '{type(col).__name__}' is not supported for row serialization." + " Please consider using a name for which literal_eval(repr(name)) works." + ) + + column_names.append(serialized_column_name) + column_names_csv = sql.csv(column_names, quoted=True) + + # index columns count + index_columns_count = len(self.index_columns) + + # column references to form the array of values for the row + column_references_csv = sql.csv( + [sql.cast_as_string(col) for col in self.expr.column_ids] + ) + + # types of the columns to serialize for the row + column_types = list(self.index.dtypes) + list(self.dtypes) + column_types_csv = sql.csv([str(typ) for typ in column_types], quoted=True) + + # row dtype to use for deserializing the row as pandas series + pandas_row_dtype = bigframes.dtypes.lcd_type(*column_types) + if pandas_row_dtype is None: + pandas_row_dtype = "object" + pandas_row_dtype = sql.quote(str(pandas_row_dtype)) + + # create a json column representing row through SQL manipulation + row_json_column_name = guid.generate_guid() + select_columns = ( + [ordering_column_name] + list(self.index_columns) + [row_json_column_name] + ) + select_columns_csv = sql.csv( + [sql.column_reference(col) for col in select_columns] + ) + json_sql = f"""\ +With T0 AS ( +{textwrap.indent(expr_sql, " ")} +), +T1 AS ( + SELECT *, + JSON_OBJECT( + "names", [{column_names_csv}], + "types", [{column_types_csv}], + "values", [{column_references_csv}], + "indexlength", {index_columns_count}, + "dtype", {pandas_row_dtype} + ) AS {row_json_column_name} FROM T0 +) +SELECT {select_columns_csv} FROM T1 +""" + ibis_table = self.session.ibis_client.sql(json_sql) + order_for_ibis_table = ordering.ExpressionOrdering.from_offset_col( + ordering_column_name + ) + expr = core.ArrayValue.from_ibis( + self.session, + ibis_table, + [ibis_table[col] for col in select_columns if col != ordering_column_name], + hidden_ordering_columns=[ibis_table[ordering_column_name]], + ordering=order_for_ibis_table, + ) + block = Block( + expr, + index_columns=self.index_columns, + column_labels=[row_json_column_name], + index_labels=self._index_labels, + ) + return block + class BlockIndexProperties: """Accessor for the index-related block properties.""" diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py new file mode 100644 index 00000000000..31ee5f90642 --- /dev/null +++ b/bigframes/core/sql.py @@ -0,0 +1,59 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utility functions for SQL construction. +""" + +from typing import Iterable + + +def quote(value: str): + """Return quoted input string.""" + + # Let's use repr which also escapes any special characters + # + # >>> for val in [ + # ... "123", + # ... "str with no special chars", + # ... "str with special chars.,'\"/\\" + # ... ]: + # ... print(f"{val} -> {repr(val)}") + # ... + # 123 -> '123' + # str with no special chars -> 'str with no special chars' + # str with special chars.,'"/\ -> 'str with special chars.,\'"/\\' + + return repr(value) + + +def column_reference(column_name: str): + """Return a string representing column reference in a SQL.""" + + return f"`{column_name}`" + + +def cast_as_string(column_name: str): + """Return a string representing string casting of a column.""" + + return f"CAST({column_reference(column_name)} AS STRING)" + + +def csv(values: Iterable[str], quoted=False): + """Return a string of comma separated values.""" + + if quoted: + values = [quote(val) for val in values] + + return ", ".join(values) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 5be28acf535..d3fd39afa76 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -34,6 +34,7 @@ Tuple, Union, ) +import warnings import bigframes_vendored.pandas.core.frame as vendored_pandas_frame import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing @@ -61,6 +62,7 @@ import bigframes.core.window import bigframes.core.window_spec as window_spec import bigframes.dtypes +import bigframes.exceptions import bigframes.formatting_helpers as formatter import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -3308,7 +3310,59 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: ops.RemoteFunctionOp(func=func, apply_on_null=(na_action is None)) ) - def apply(self, func, *, args: typing.Tuple = (), **kwargs): + def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs): + if utils.get_axis_number(axis) == 1: + warnings.warn( + "axis=1 scenario is in preview.", + category=bigframes.exceptions.PreviewWarning, + ) + + # Early check whether the dataframe dtypes are currently supported + # in the remote function + # NOTE: Keep in sync with the value converters used in the gcf code + # generated in generate_cloud_function_main_code in remote_function.py + remote_function_supported_dtypes = ( + bigframes.dtypes.INT_DTYPE, + bigframes.dtypes.FLOAT_DTYPE, + bigframes.dtypes.BOOL_DTYPE, + bigframes.dtypes.STRING_DTYPE, + ) + supported_dtypes_types = tuple( + type(dtype) for dtype in remote_function_supported_dtypes + ) + supported_dtypes_hints = tuple( + str(dtype) for dtype in remote_function_supported_dtypes + ) + + for dtype in self.dtypes: + if not isinstance(dtype, supported_dtypes_types): + raise NotImplementedError( + f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1." + f" Supported dtypes are {supported_dtypes_hints}." + ) + + # Check if the function is a remote function + if not hasattr(func, "bigframes_remote_function"): + raise ValueError("For axis=1 a remote function must be used.") + + # Serialize the rows as json values + block = self._get_block() + rows_as_json_series = bigframes.series.Series( + block._get_rows_as_json_values() + ) + + # Apply the function + result_series = rows_as_json_series._apply_unary_op( + ops.RemoteFunctionOp(func=func, apply_on_null=True) + ) + result_series.name = None + + # Return Series with materialized result so that any error in the remote + # function is caught early + materialized_series = result_series.cache() + return materialized_series + + # Per-column apply results = {name: func(col, *args, **kwargs) for name, col in self.items()} if all( [ diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 3ca6d8e1af3..eae021b4cdd 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -33,3 +33,7 @@ class CleanupFailedWarning(Warning): class DefaultIndexWarning(Warning): """Default index may cause unexpected costs.""" + + +class PreviewWarning(Warning): + """The feature is in preview.""" diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 9d826d0fa17..6e42ca9f48c 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -25,8 +25,10 @@ import tempfile import textwrap from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING, Union +import warnings import ibis +import pandas import requests if TYPE_CHECKING: @@ -262,7 +264,7 @@ def generate_udf_code(self, def_, dir): return udf_code_file_name, udf_bytecode_file_name - def generate_cloud_function_main_code(self, def_, dir): + def generate_cloud_function_main_code(self, def_, dir, is_row_processor=False): """Get main.py code for the cloud function for the given user defined function.""" # Pickle the udf with all its dependencies @@ -285,38 +287,120 @@ def generate_cloud_function_main_code(self, def_, dir): # ... # } # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#input_format - code_template = textwrap.dedent( - """\ - import cloudpickle - import functions_framework - from flask import jsonify - import json - - # original udf code is in {udf_code_file} - # serialized udf code is in {udf_bytecode_file} - with open("{udf_bytecode_file}", "rb") as f: - udf = cloudpickle.load(f) - - def {handler_func_name}(request): - try: - request_json = request.get_json(silent=True) - calls = request_json["calls"] - replies = [] - for call in calls: - reply = udf(*call) - replies.append(reply) - return_json = json.dumps({{"replies" : replies}}) - return return_json - except Exception as e: - return jsonify( {{ "errorMessage": str(e) }} ), 400 - """ - ) - - code = code_template.format( - udf_code_file=udf_code_file, - udf_bytecode_file=udf_bytecode_file, - handler_func_name=handler_func_name, - ) + code = """\ +import cloudpickle +import functions_framework +from flask import jsonify +import json +""" + if is_row_processor: + code += """\ +import ast +import math +import pandas as pd + +def get_pd_series(row): + row_json = json.loads(row) + col_names = row_json["names"] + col_types = row_json["types"] + col_values = row_json["values"] + index_length = row_json["indexlength"] + dtype = row_json["dtype"] + + # At this point we are assuming that col_names, col_types and col_values are + # arrays of the same length, representing column names, types and values for + # one row of data + + # column names are not necessarily strings + # they are serialized as repr(name) at source + evaluated_col_names = [] + for col_name in col_names: + try: + col_name = ast.literal_eval(col_name) + except Exception as ex: + raise NameError(f"Failed to evaluate column name from '{col_name}': {ex}") + evaluated_col_names.append(col_name) + col_names = evaluated_col_names + + # Supported converters for pandas to python types + value_converters = { + "boolean": lambda val: val == "true", + "Int64": int, + "Float64": float, + "string": str, + } + + def convert_value(value, value_type): + value_converter = value_converters.get(value_type) + if value_converter is None: + raise ValueError(f"Don't know how to handle type '{value_type}'") + if value is None: + return None + return value_converter(value) + + index_values = [ + pd.Series([convert_value(col_values[i], col_types[i])], dtype=col_types[i])[0] + for i in range(index_length) + ] + + data_col_names = col_names[index_length:] + data_col_types = col_types[index_length:] + data_col_values = col_values[index_length:] + data_col_values = [ + pd.Series([convert_value(a, data_col_types[i])], dtype=data_col_types[i])[0] + for i, a in enumerate(data_col_values) + ] + + row_index = index_values[0] if len(index_values) == 1 else tuple(index_values) + row_series = pd.Series(data_col_values, index=data_col_names, name=row_index, dtype=dtype) + return row_series +""" + code += f"""\ + +# original udf code is in {udf_code_file} +# serialized udf code is in {udf_bytecode_file} +with open("{udf_bytecode_file}", "rb") as f: + udf = cloudpickle.load(f) + +def {handler_func_name}(request): + try: + request_json = request.get_json(silent=True) + calls = request_json["calls"] + replies = [] + for call in calls: +""" + + if is_row_processor: + code += """\ + reply = udf(get_pd_series(call[0])) + if isinstance(reply, float) and (math.isnan(reply) or math.isinf(reply)): + # json serialization of the special float values (nan, inf, -inf) + # is not in strict compliance of the JSON specification + # https://docs.python.org/3/library/json.html#basic-usage. + # Let's convert them to a quoted string representation ("NaN", + # "Infinity", "-Infinity" respectively) which is handled by + # BigQuery + reply = json.dumps(reply) + elif pd.isna(reply): + # Pandas N/A values are not json serializable, so use a python + # equivalent instead + reply = None + elif hasattr(reply, "item"): + # Numpy types are not json serializable, so use its Python + # value instead + reply = reply.item() +""" + else: + code += """\ + reply = udf(*call) +""" + code += """\ + replies.append(reply) + return_json = json.dumps({"replies" : replies}) + return return_json + except Exception as e: + return jsonify( { "errorMessage": str(e) } ), 400 +""" main_py = os.path.join(dir, "main.py") with open(main_py, "w") as f: @@ -325,11 +409,17 @@ def {handler_func_name}(request): return handler_func_name - def generate_cloud_function_code(self, def_, dir, package_requirements=None): + def generate_cloud_function_code( + self, def_, dir, package_requirements=None, is_row_processor=False + ): """Generate the cloud function code for a given user defined function.""" # requirements.txt requirements = ["cloudpickle >= 2.1.0"] + if is_row_processor: + # bigframes remote function will send an entire row of data as json, + # which would be converted to a pandas series and processed + requirements.append(f"pandas=={pandas.__version__}") if package_requirements: requirements.extend(package_requirements) requirements = sorted(requirements) @@ -338,7 +428,9 @@ def generate_cloud_function_code(self, def_, dir, package_requirements=None): f.write("\n".join(requirements)) # main.py - entry_point = self.generate_cloud_function_main_code(def_, dir) + entry_point = self.generate_cloud_function_main_code( + def_, dir, is_row_processor + ) return entry_point def create_cloud_function( @@ -348,13 +440,14 @@ def create_cloud_function( package_requirements=None, timeout_seconds=600, max_instance_count=None, + is_row_processor=False, ): """Create a cloud function from the given user defined function.""" # Build and deploy folder structure containing cloud function with tempfile.TemporaryDirectory() as dir: entry_point = self.generate_cloud_function_code( - def_, dir, package_requirements + def_, dir, package_requirements, is_row_processor ) archive_path = shutil.make_archive(dir, "zip", dir) @@ -474,6 +567,7 @@ def provision_bq_remote_function( max_batching_rows, cloud_function_timeout, cloud_function_max_instance_count, + is_row_processor, ): """Provision a BigQuery remote function.""" # If reuse of any existing function with the same name (indicated by the @@ -500,6 +594,7 @@ def provision_bq_remote_function( package_requirements, cloud_function_timeout, cloud_function_max_instance_count, + is_row_processor, ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") @@ -700,8 +795,9 @@ def remote_function( Args: input_types (type or sequence(type)): - Input data type, or sequence of input data types in the user - defined function. + For scalar user defined function it should be the input type or + sequence of input types. For row processing user defined function, + type `Series` should be specified. output_type (type): Data type of the output in the user defined function. session (bigframes.Session, Optional): @@ -800,9 +896,25 @@ def remote_function( function's default setting applies. For more details see https://cloud.google.com/functions/docs/configuring/max-instances """ - if isinstance(input_types, type): + is_row_processor = False + + import bigframes.series + + if input_types == bigframes.series.Series: + warnings.warn( + "input_types=Series scenario is in preview.", + stacklevel=1, + category=bigframes.exceptions.PreviewWarning, + ) + + # we will model the row as a json serialized string containing the data + # and the metadata representing the row + input_types = [str] + is_row_processor = True + elif isinstance(input_types, type): input_types = [input_types] + # Some defaults may be used from the session if not provided otherwise import bigframes.pandas as bpd session = session or bpd.get_global_session() @@ -928,6 +1040,7 @@ def wrapper(f): max_batching_rows, cloud_function_timeout, cloud_function_max_instances, + is_row_processor, ) # TODO: Move ibis logic to compiler step diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 5f70fd77f9c..473fc4f0986 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1416,6 +1416,9 @@ def remote_function( """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. + .. note:: + ``input_types=Series`` scenario is in preview. + .. note:: Please make sure following is setup before using this API: @@ -1455,8 +1458,9 @@ def remote_function( Args: input_types (type or sequence(type)): - Input data type, or sequence of input data types in the user - defined function. + For scalar user defined function it should be the input type or + sequence of input types. For row processing user defined function, + type `Series` should be specified. output_type (type): Data type of the output in the user defined function. dataset (str, Optional): diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index eb2a0884fef..e086903d03f 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime import importlib.util import inspect import math # must keep this at top level to test udf referring global import @@ -28,6 +29,7 @@ import bigframes from bigframes.functions.remote_function import get_cloud_function_name +import bigframes.series from tests.system.utils import ( assert_pandas_df_equal, delete_cloud_function, @@ -1454,3 +1456,251 @@ def square(x): cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, square_remote ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1(session, scalars_dfs): + columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] + scalars_df, scalars_pandas_df = scalars_dfs + try: + + def serialize_row(row): + custom = { + "name": row.name, + "index": [idx for idx in row.index], + "values": [ + val.item() if hasattr(val, "item") else val for val in row.values + ], + } + + return str( + { + "default": row.to_json(), + "split": row.to_json(orient="split"), + "records": row.to_json(orient="records"), + "index": row.to_json(orient="index"), + "table": row.to_json(orient="table"), + "custom": custom, + } + ) + + serialize_row_remote = session.remote_function( + bigframes.series.Series, str, reuse=False + )(serialize_row) + + bf_result = scalars_df[columns].apply(serialize_row_remote, axis=1).to_pandas() + pd_result = scalars_pandas_df[columns].apply(serialize_row, axis=1) + + # bf_result.dtype is 'string[pyarrow]' while pd_result.dtype is 'object' + # , ignore this mismatch by using check_dtype=False. + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, serialize_row_remote + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_aggregates(session, scalars_dfs): + columns = ["int64_col", "int64_too", "float64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + + try: + + def analyze(row): + return str( + { + "dtype": row.dtype, + "count": row.count(), + "min": row.max(), + "max": row.max(), + "mean": row.mean(), + "std": row.std(), + "var": row.var(), + } + ) + + analyze_remote = session.remote_function(bigframes.series.Series, str)(analyze) + + bf_result = ( + scalars_df[columns].dropna().apply(analyze_remote, axis=1).to_pandas() + ) + pd_result = scalars_pandas_df[columns].dropna().apply(analyze, axis=1) + + # bf_result.dtype is 'string[pyarrow]' while pd_result.dtype is 'object' + # , ignore this mismatch by using check_dtype=False. + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, analyze_remote + ) + + +@pytest.mark.parametrize( + ("pd_df"), + [ + pytest.param( + pandas.DataFrame( + { + "2": [1, 2, 3], + 2: [1.5, 3.75, 5], + "name, [with. special'- chars\")/\\": [10, 20, 30], + (3, 4): ["pq", "rs", "tu"], + (5.0, "six", 7): [8, 9, 10], + 'raise Exception("hacked!")': [11, 12, 13], + } + ), + id="all-kinds-of-column-names", + ), + pytest.param( + pandas.DataFrame( + { + "x": [1, 2, 3], + "y": [1.5, 3.75, 5], + "z": ["pq", "rs", "tu"], + }, + index=pandas.MultiIndex.from_tuples( + [ + ("a", 100), + ("a", 200), + ("b", 300), + ] + ), + ), + id="multiindex", + ), + pytest.param( + pandas.DataFrame( + [ + [10, 1.5, "pq"], + [20, 3.75, "rs"], + [30, 8.0, "tu"], + ], + columns=pandas.MultiIndex.from_arrays( + [ + ["first", "last_two", "last_two"], + [1, 2, 3], + ] + ), + ), + id="column-multiindex", + ), + pytest.param( + pandas.DataFrame( + { + datetime.now(): [1, 2, 3], + } + ), + id="column-name-not-supported", + marks=pytest.mark.xfail(raises=NameError), + ), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_complex(session, pd_df): + bf_df = session.read_pandas(pd_df) + + try: + + def serialize_row(row): + custom = { + "name": row.name, + "index": [idx for idx in row.index], + "values": [ + val.item() if hasattr(val, "item") else val for val in row.values + ], + } + return str( + { + "default": row.to_json(), + "split": row.to_json(orient="split"), + "records": row.to_json(orient="records"), + "index": row.to_json(orient="index"), + "custom": custom, + } + ) + + serialize_row_remote = session.remote_function( + bigframes.series.Series, str, reuse=False + )(serialize_row) + + bf_result = bf_df.apply(serialize_row_remote, axis=1).to_pandas() + pd_result = pd_df.apply(serialize_row, axis=1) + + # bf_result.dtype is 'string[pyarrow]' while pd_result.dtype is 'object' + # , ignore this mismatch by using check_dtype=False. + # + # bf_result.index[0].dtype is 'string[pyarrow]' while + # pd_result.index[0].dtype is 'object', ignore this mismatch by using + # check_index_type=False. + pandas.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, serialize_row_remote + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_na_nan_inf(session): + """This test is for special cases of float values, to make sure any (nan, + inf, -inf) produced by user code is honored. + """ + bf_df = session.read_gbq( + """\ +SELECT "1" AS text, 1 AS num +UNION ALL +SELECT "2.5" AS text, 2.5 AS num +UNION ALL +SELECT "nan" AS text, IEEE_DIVIDE(0, 0) AS num +UNION ALL +SELECT "inf" AS text, IEEE_DIVIDE(1, 0) AS num +UNION ALL +SELECT "-inf" AS text, IEEE_DIVIDE(-1, 0) AS num +UNION ALL +SELECT "numpy nan" AS text, IEEE_DIVIDE(0, 0) AS num +UNION ALL +SELECT "pandas na" AS text, NULL AS num + """ + ) + + pd_df = bf_df.to_pandas() + + try: + + def float_parser(row): + import numpy as mynp + import pandas as mypd + + if row["text"] == "pandas na": + return mypd.NA + if row["text"] == "numpy nan": + return mynp.nan + return float(row["text"]) + + float_parser_remote = session.remote_function( + bigframes.series.Series, float, reuse=False + )(float_parser) + + pd_result = pd_df.apply(float_parser, axis=1) + bf_result = bf_df.apply(float_parser_remote, axis=1).to_pandas() + + # bf_result.dtype is 'Float64' while pd_result.dtype is 'object' + # , ignore this mismatch by using check_dtype=False. + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + # Let's also assert that the data is consistent in this round trip + # (BQ -> BigFrames -> BQ -> GCF -> BQ -> BigFrames) w.r.t. their + # expected values in BQ + bq_result = bf_df["num"].to_pandas() + bq_result.name = None + pandas.testing.assert_series_equal(bq_result, bf_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, float_parser_remote + ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 106638cef3c..9c60c821a7d 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + import google.api_core.exceptions from google.cloud import bigquery import pandas as pd import pytest import bigframes +import bigframes.exceptions from bigframes.functions import remote_function as rf from tests.system.utils import assert_pandas_df_equal @@ -685,3 +688,146 @@ def test_read_gbq_function_enforces_explicit_types(bigquery_client, dataset_id): rf.read_gbq_function( str(neither_type_specified.reference), bigquery_client=bigquery_client ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1(session, scalars_dfs): + columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] + scalars_df, scalars_pandas_df = scalars_dfs + + def add_ints(row): + return row["int64_col"] + row["int64_too"] + + with pytest.warns( + bigframes.exceptions.PreviewWarning, + match="input_types=Series scenario is in preview.", + ): + add_ints_remote = session.remote_function(bigframes.series.Series, int)( + add_ints + ) + + with pytest.warns( + bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." + ): + bf_result = scalars_df[columns].apply(add_ints_remote, axis=1).to_pandas() + + pd_result = scalars_pandas_df[columns].apply(add_ints, axis=1) + + # bf_result.dtype is 'Int64' while pd_result.dtype is 'object', ignore this + # mismatch by using check_dtype=False. + # + # bf_result.to_numpy() produces an array of numpy.float64's + # (in system_prerelease tests), while pd_result.to_numpy() produces an + # array of ints, ignore this mismatch by using check_exact=False. + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_exact=False + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_ordering(session, scalars_dfs): + columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] + ordering_columns = ["bool_col", "int64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + + def add_ints(row): + return row["int64_col"] + row["int64_too"] + + add_ints_remote = session.remote_function(bigframes.series.Series, int)(add_ints) + + bf_result = ( + scalars_df[columns] + .sort_values(ordering_columns) + .apply(add_ints_remote, axis=1) + .to_pandas() + ) + pd_result = ( + scalars_pandas_df[columns].sort_values(ordering_columns).apply(add_ints, axis=1) + ) + + # bf_result.dtype is 'Int64' while pd_result.dtype is 'object', ignore this + # mismatch by using check_dtype=False. + # + # bf_result.to_numpy() produces an array of numpy.float64's + # (in system_prerelease tests), while pd_result.to_numpy() produces an + # array of ints, ignore this mismatch by using check_exact=False. + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_exact=False + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_multiindex(session): + pd_df = pd.DataFrame( + {"x": [1, 2, 3], "y": [1.5, 3.75, 5], "z": ["pq", "rs", "tu"]}, + index=pd.MultiIndex.from_tuples([("a", 100), ("a", 200), ("b", 300)]), + ) + bf_df = session.read_pandas(pd_df) + + def add_numbers(row): + return row["x"] + row["y"] + + add_numbers_remote = session.remote_function(bigframes.series.Series, float)( + add_numbers + ) + + bf_result = bf_df.apply(add_numbers_remote, axis=1).to_pandas() + pd_result = pd_df.apply(add_numbers, axis=1) + + # bf_result.dtype is 'Float64' while pd_result.dtype is 'float64', ignore this + # mismatch by using check_dtype=False. + # + # bf_result.index[0].dtype is 'string[pyarrow]' while + # pd_result.index[0].dtype is 'object', ignore this mismatch by using + # check_index_type=False. + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + +def test_df_apply_axis_1_unsupported_callable(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] + + def add_ints(row): + return row["int64_col"] + row["int64_too"] + + # pandas works + scalars_pandas_df.apply(add_ints, axis=1) + + with pytest.raises(ValueError, match="For axis=1 a remote function must be used."): + scalars_df[columns].apply(add_ints, axis=1) + + +@pytest.mark.parametrize( + ("column"), + [ + pytest.param("bytes_col"), + pytest.param("date_col"), + pytest.param("datetime_col"), + pytest.param("geography_col"), + pytest.param("numeric_col"), + pytest.param("time_col"), + pytest.param("timestamp_col"), + ], +) +def test_df_apply_axis_1_unsupported_dtype(scalars_dfs, column): + scalars_df, scalars_pandas_df = scalars_dfs + + # It doesn't matter if it is a remote function or not, the dtype check + # is done even before the function type check with axis=1 + def echo(row): + return row[column] + + # pandas works + scalars_pandas_df[[column]].apply(echo, axis=1) + + dtype = scalars_df[column].dtype + + with pytest.raises( + NotImplementedError, + match=re.escape( + f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1. Supported dtypes are ('Int64', 'Float64', 'boolean', 'string')." + ), + ): + scalars_df[[column]].apply(echo, axis=1) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 4e17bca54da..31d5e88c7e8 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4200,12 +4200,16 @@ def merge( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def apply(self, func, *, args=(), **kwargs): + def apply(self, func, *, axis=0, args=(), **kwargs): """Apply a function along an axis of the DataFrame. Objects passed to the function are Series objects whose index is - the DataFrame's index (``axis=0``) the final return type - is inferred from the return type of the applied function. + the DataFrame's index (``axis=0``) or the DataFrame's columns (``axis=1``). + The final return type is inferred from the return type of the applied + function. + + .. note:: + ``axis=1`` scenario is in preview. **Examples:** @@ -4230,9 +4234,28 @@ def apply(self, func, *, args=(), **kwargs): [2 rows x 2 columns] + You could apply a user defined function to every row of the DataFrame by + creating a remote function out of it, and using it with `axis=1`. + + >>> @bpd.remote_function(bpd.Series, int, reuse=False) + ... def foo(row): + ... result = 1 + ... result += row["col1"] + ... result += row["col2"]*row["col2"] + ... return result + + >>> df.apply(foo, axis=1) + 0 11 + 1 19 + dtype: Int64 + Args: func (function): Function to apply to each column or row. + axis ({index (0), columns (1)}): + Axis along which the function is applied. Specify 0 or 'index' + to apply function to each column. Specify 1 or 'columns' to + apply function to each row. args (tuple): Positional arguments to pass to `func` in addition to the array/series. From 21bd3e4f0be64c20ed8774f1a6e0b79fe40bba5a Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 10 May 2024 14:40:40 -0700 Subject: [PATCH 561/921] chore: log and labels update (#674) * chore: log and labels update * remove unused logic * Update unit test. * fixes for mypy * lint update --- bigframes/core/log_adapter.py | 16 +++++++++-- bigframes/session/_io/bigquery/__init__.py | 17 +++++++---- .../session/_io/bigquery/read_gbq_table.py | 4 --- tests/unit/session/test_io_bigquery.py | 28 ++++++------------- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/bigframes/core/log_adapter.py b/bigframes/core/log_adapter.py index 860d394cd20..b5afafbe7c5 100644 --- a/bigframes/core/log_adapter.py +++ b/bigframes/core/log_adapter.py @@ -21,6 +21,9 @@ _api_methods: List = [] _excluded_methods = ["__setattr__", "__getattr__"] +# Stack to track method calls +_call_stack: List = [] + def class_logger(decorated_cls): """Decorator that adds logging functionality to each method of the class.""" @@ -38,10 +41,17 @@ def wrapper(*args, **kwargs): class_name = decorated_cls.__name__ # Access decorated class name api_method_name = str(method.__name__) full_method_name = f"{class_name.lower()}-{api_method_name}" - # Track regular and "dunder" methods - if api_method_name.startswith("__") or not api_method_name.startswith("_"): + + # Track directly called methods + if len(_call_stack) == 0: add_api_method(full_method_name) - return method(*args, **kwargs) + + _call_stack.append(full_method_name) + + try: + return method(*args, **kwargs) + finally: + _call_stack.pop() return wrapper diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 98e0dac1e83..ed1bd39adae 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -23,7 +23,7 @@ import textwrap import types import typing -from typing import Dict, Iterable, Mapping, Optional, Sequence, Tuple, Union +from typing import Dict, Iterable, Mapping, Optional, Tuple, Union import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions @@ -43,11 +43,15 @@ def create_job_configs_labels( job_configs_labels: Optional[Dict[str, str]], - api_methods: Sequence[str], + api_methods: typing.List[str], ) -> Dict[str, str]: if job_configs_labels is None: job_configs_labels = {} + if api_methods: + job_configs_labels["bigframes-api"] = api_methods[0] + del api_methods[0] + labels = list( itertools.chain( job_configs_labels.keys(), @@ -198,10 +202,11 @@ def start_query_with_client( """ Starts query job and waits for results. """ - api_methods = log_adapter.get_and_reset_api_methods() - job_config.labels = create_job_configs_labels( - job_configs_labels=job_config.labels, api_methods=api_methods - ) + if not job_config.dry_run: + api_methods = log_adapter.get_and_reset_api_methods() + job_config.labels = create_job_configs_labels( + job_configs_labels=job_config.labels, api_methods=api_methods + ) try: query_job = bq_client.query(sql, job_config=job_config, timeout=timeout) diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index f6c1463e6c9..0f6a3dadd2b 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -112,8 +112,6 @@ def get_table_metadata( # atomically. table = bqclient.get_table(table_ref) - # TODO(b/336521938): Refactor to make sure we set the "bigframes-api" - # whereever we execute a query. job_config = bigquery.QueryJobConfig() job_config.labels["bigframes-api"] = api_name snapshot_timestamp = list( @@ -344,8 +342,6 @@ def get_time_travel_datetime_and_table_metadata( # atomically. table = bqclient.get_table(table_ref) - # TODO(b/336521938): Refactor to make sure we set the "bigframes-api" - # whereever we execute a query. job_config = bigquery.QueryJobConfig() job_config.labels["bigframes-api"] = api_name snapshot_timestamp = list( diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 9da085e8240..5f4072e9c20 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -30,17 +30,13 @@ def test_create_job_configs_labels_is_none(): labels = io_bq.create_job_configs_labels( job_configs_labels=None, api_methods=api_methods ) - expected_dict = { - "recent-bigframes-api-0": "agg", - "recent-bigframes-api-1": "series-mode", - } + expected_dict = {"bigframes-api": "agg", "recent-bigframes-api-0": "series-mode"} assert labels is not None assert labels == expected_dict def test_create_job_configs_labels_length_limit_not_met(): cur_labels = { - "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", } api_methods = ["agg", "series-mode"] @@ -48,20 +44,18 @@ def test_create_job_configs_labels_length_limit_not_met(): job_configs_labels=cur_labels, api_methods=api_methods ) expected_dict = { - "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", - "recent-bigframes-api-0": "agg", - "recent-bigframes-api-1": "series-mode", + "bigframes-api": "agg", + "recent-bigframes-api-0": "series-mode", } assert labels is not None - assert len(labels) == 4 + assert len(labels) == 3 assert labels == expected_dict def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): log_adapter.get_and_reset_api_methods() cur_labels = { - "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", } df = bpd.DataFrame( @@ -76,14 +70,10 @@ def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): job_configs_labels=cur_labels, api_methods=api_methods ) expected_dict = { - "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", - "recent-bigframes-api-0": "series-__init__", - "recent-bigframes-api-1": "dataframe-max", - "recent-bigframes-api-2": "dataframe-__init__", - "recent-bigframes-api-3": "dataframe-head", - "recent-bigframes-api-4": "dataframe-__init__", - "recent-bigframes-api-5": "dataframe-__init__", + "bigframes-api": "dataframe-max", + "recent-bigframes-api-0": "dataframe-head", + "recent-bigframes-api-1": "dataframe-__init__", } assert labels == expected_dict @@ -94,7 +84,7 @@ def test_create_job_configs_labels_length_limit_met_and_labels_is_none(): {"col1": [1, 2], "col2": [3, 4]}, session=resources.create_bigquery_session() ) # Test running methods more than the labels' length limit - for i in range(66): + for i in range(100): df.head() api_methods = log_adapter._api_methods @@ -112,7 +102,7 @@ def test_create_job_configs_labels_length_limit_met(): "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", } - for i in range(60): + for i in range(100): key = f"bigframes-api-test-{i}" value = f"test{i}" cur_labels[key] = value From 9ca92d09e9c56db408350b35ec698152c13954ed Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 10 May 2024 23:54:16 +0000 Subject: [PATCH 562/921] feat: support gcf vpc connector in `remote_function` (#677) --- bigframes/functions/remote_function.py | 14 +++- bigframes/pandas/__init__.py | 2 + bigframes/session/__init__.py | 9 ++- tests/system/large/test_remote_function.py | 75 +++++++++++++++++++++- 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 6e42ca9f48c..2a7a900779a 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -441,6 +441,7 @@ def create_cloud_function( timeout_seconds=600, max_instance_count=None, is_row_processor=False, + vpc_connector=None, ): """Create a cloud function from the given user defined function.""" @@ -519,6 +520,8 @@ def create_cloud_function( function.service_config.timeout_seconds = timeout_seconds if max_instance_count is not None: function.service_config.max_instance_count = max_instance_count + if vpc_connector is not None: + function.service_config.vpc_connector = vpc_connector function.service_config.service_account_email = ( self._cloud_function_service_account ) @@ -568,6 +571,7 @@ def provision_bq_remote_function( cloud_function_timeout, cloud_function_max_instance_count, is_row_processor, + cloud_function_vpc_connector, ): """Provision a BigQuery remote function.""" # If reuse of any existing function with the same name (indicated by the @@ -595,6 +599,7 @@ def provision_bq_remote_function( cloud_function_timeout, cloud_function_max_instance_count, is_row_processor, + cloud_function_vpc_connector, ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") @@ -750,6 +755,7 @@ def remote_function( max_batching_rows: Optional[int] = 1000, cloud_function_timeout: Optional[int] = 600, cloud_function_max_instances: Optional[int] = None, + cloud_function_vpc_connector: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -894,7 +900,12 @@ def remote_function( control the spike in the billing. Higher setting can help support processing larger scale data. When not specified, cloud function's default setting applies. For more details see - https://cloud.google.com/functions/docs/configuring/max-instances + https://cloud.google.com/functions/docs/configuring/max-instances. + cloud_function_vpc_connector (str, Optional): + The VPC connector you would like to configure for your cloud + function. This is useful if your code needs access to data or + service(s) that are on a VPC network. See for more details + https://cloud.google.com/functions/docs/networking/connecting-vpc. """ is_row_processor = False @@ -1041,6 +1052,7 @@ def wrapper(f): cloud_function_timeout, cloud_function_max_instances, is_row_processor, + cloud_function_vpc_connector, ) # TODO: Move ibis logic to compiler step diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 1d6da46fae1..8d2c0b148c5 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -654,6 +654,7 @@ def remote_function( max_batching_rows: Optional[int] = 1000, cloud_function_timeout: Optional[int] = 600, cloud_function_max_instances: Optional[int] = None, + cloud_function_vpc_connector: Optional[str] = None, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -670,6 +671,7 @@ def remote_function( max_batching_rows=max_batching_rows, cloud_function_timeout=cloud_function_timeout, cloud_function_max_instances=cloud_function_max_instances, + cloud_function_vpc_connector=cloud_function_vpc_connector, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 473fc4f0986..727269e7ee5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1412,6 +1412,7 @@ def remote_function( max_batching_rows: Optional[int] = 1000, cloud_function_timeout: Optional[int] = 600, cloud_function_max_instances: Optional[int] = None, + cloud_function_vpc_connector: Optional[str] = None, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1537,7 +1538,12 @@ def remote_function( control the spike in the billing. Higher setting can help support processing larger scale data. When not specified, cloud function's default setting applies. For more details see - https://cloud.google.com/functions/docs/configuring/max-instances + https://cloud.google.com/functions/docs/configuring/max-instances. + cloud_function_vpc_connector (str, Optional): + The VPC connector you would like to configure for your cloud + function. This is useful if your code needs access to data or + service(s) that are on a VPC network. See for more details + https://cloud.google.com/functions/docs/networking/connecting-vpc. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1562,6 +1568,7 @@ def remote_function( max_batching_rows=max_batching_rows, cloud_function_timeout=cloud_function_timeout, cloud_function_max_instances=cloud_function_max_instances, + cloud_function_vpc_connector=cloud_function_vpc_connector, ) def read_gbq_function( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index e086903d03f..b7d99ea36ca 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -21,7 +21,7 @@ import tempfile import textwrap -from google.api_core.exceptions import BadRequest, NotFound +from google.api_core.exceptions import BadRequest, InvalidArgument, NotFound from google.cloud import bigquery, storage import pandas import pytest @@ -1333,6 +1333,79 @@ def square_num(x): ) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_via_session_vpc(scalars_dfs): + # TODO(shobs): Automate the following set-up during testing in the test project. + # + # For upfront convenience, the following set up has been statically created + # in the project bigfrmames-dev-perf via cloud console: + # + # 1. Create a vpc connector as per + # https://cloud.google.com/vpc/docs/configure-serverless-vpc-access#gcloud + # + # $ gcloud compute networks vpc-access connectors create bigframes-vpc --project=bigframes-dev-perf --region=us-central1 --range 10.8.0.0/28 + # Create request issued for: [bigframes-vpc] + # Waiting for operation [projects/bigframes-dev-perf/locations/us-central1/operations/f9f90df6-7cf4-4420-8c2f-b3952775dcfb] to complete...done. + # Created connector [bigframes-vpc]. + # + # $ gcloud compute networks vpc-access connectors list --project=bigframes-dev-perf --region=us-central1 + # CONNECTOR_ID REGION NETWORK IP_CIDR_RANGE SUBNET SUBNET_PROJECT MACHINE_TYPE MIN_INSTANCES MAX_INSTANCES MIN_THROUGHPUT MAX_THROUGHPUT STATE + # bigframes-vpc us-central1 default 10.8.0.0/28 e2-micro 2 10 200 1000 READY + + project = "bigframes-dev-perf" + gcf_vpc_connector = "bigframes-vpc" + + rf_session = bigframes.Session(context=bigframes.BigQueryOptions(project=project)) + + try: + + def square_num(x): + if x is None: + return x + return x * x + + square_num_remote = rf_session.remote_function( + [int], int, reuse=False, cloud_function_vpc_connector=gcf_vpc_connector + )(square_num) + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_int64_col = scalars_df["int64_col"] + bf_result_col = bf_int64_col.apply(square_num_remote) + bf_result = bf_int64_col.to_frame().assign(result=bf_result_col).to_pandas() + + pd_int64_col = scalars_pandas_df["int64_col"] + pd_result_col = pd_int64_col.apply(square_num) + pd_result = pd_int64_col.to_frame().assign(result=pd_result_col) + + assert_pandas_df_equal(bf_result, pd_result, check_dtype=False) + + # Assert that the GCF is created with the intended vpc connector + gcf = rf_session.cloudfunctionsclient.get_function( + name=square_num_remote.bigframes_cloud_function + ) + assert gcf.service_config.vpc_connector == gcf_vpc_connector + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + rf_session.bqclient, rf_session.cloudfunctionsclient, square_num_remote + ) + + +def test_remote_function_via_session_vpc_invalid(session): + with pytest.raises( + InvalidArgument, match="400.*Serverless VPC Access connector is not found" + ): + + @session.remote_function( + [int], int, reuse=False, cloud_function_vpc_connector="does-not-exist" + ) + def square_num(x): + if x is None: + return x + return x * x + + @pytest.mark.parametrize( ("max_batching_rows"), [ From 2fd1b8117bda0dee5d8fc0924c80ce257fa9e3f1 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 10 May 2024 18:22:01 -0700 Subject: [PATCH 563/921] feat: Add Series.combine (#680) --- bigframes/core/compile/scalar_op_compiler.py | 22 ++++++-- bigframes/operations/__init__.py | 16 +++++- bigframes/series.py | 36 +++++++++++- tests/system/large/test_remote_function.py | 35 ++++++++++++ tests/system/small/test_numpy.py | 53 ++++++++---------- tests/system/small/test_series.py | 35 ++++++++++++ .../bigframes_vendored/pandas/core/series.py | 56 +++++++++++++++++++ 7 files changed, 214 insertions(+), 39 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index a65ff6fe0c2..90025b3994f 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -1298,22 +1298,36 @@ def coalesce_impl( return ibis.coalesce(x, y) -@scalar_op_compiler.register_binary_op(ops.cliplower_op) -def clip_lower( +@scalar_op_compiler.register_binary_op(ops.maximum_op) +def maximum_impl( value: ibis_types.Value, lower: ibis_types.Value, ): + # Note: propagates nulls return ibis.case().when(lower.isnull() | (value < lower), lower).else_(value).end() -@scalar_op_compiler.register_binary_op(ops.clipupper_op) -def clip_upper( +@scalar_op_compiler.register_binary_op(ops.minimum_op) +def minimum_impl( value: ibis_types.Value, upper: ibis_types.Value, ): + # Note: propagates nulls return ibis.case().when(upper.isnull() | (value > upper), upper).else_(value).end() +@scalar_op_compiler.register_binary_op(ops.BinaryRemoteFunctionOp, pass_op=True) +def binary_remote_function_op_impl( + x: ibis_types.Value, y: ibis_types.Value, op: ops.BinaryRemoteFunctionOp +): + if not hasattr(op.func, "bigframes_remote_function"): + raise TypeError( + f"only a bigframes remote function is supported as a callable. {constants.FEEDBACK_LINK}" + ) + x_transformed = op.func(x, y) + return x_transformed + + # Ternary Operations @scalar_op_compiler.register_ternary_op(ops.where_op) def where_op( diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index e52f488d389..6f99f710136 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -545,8 +545,8 @@ def output_type(self, *input_types): # Binary Ops fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COERCE) -cliplower_op = create_binary_op(name="clip_lower", type_signature=op_typing.COERCE) -clipupper_op = create_binary_op(name="clip_upper", type_signature=op_typing.COERCE) +maximum_op = create_binary_op(name="maximum", type_signature=op_typing.COERCE) +minimum_op = create_binary_op(name="minimum", type_signature=op_typing.COERCE) coalesce_op = create_binary_op(name="coalesce", type_signature=op_typing.COERCE) @@ -587,6 +587,16 @@ def output_type(self, *input_types): raise TypeError(f"Cannot subtract dtypes {left_type} and {right_type}") +@dataclasses.dataclass(frozen=True) +class BinaryRemoteFunctionOp(BinaryOp): + name: typing.ClassVar[str] = "binary_remote_function" + func: typing.Callable + + def output_type(self, *input_types): + # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method + return self.func.output_dtype + + add_op = AddOp() sub_op = SubOp() mul_op = create_binary_op(name="mul", type_signature=op_typing.BINARY_NUMERIC) @@ -713,4 +723,6 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT np.divide: div_op, np.power: pow_op, np.arctan2: arctan2_op, + np.maximum: maximum_op, + np.minimum: minimum_op, } diff --git a/bigframes/series.py b/bigframes/series.py index 313380e4a4f..d1fb0d679bd 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1031,9 +1031,9 @@ def clip(self, lower, upper): if lower is None and upper is None: return self if lower is None: - return self._apply_binary_op(upper, ops.clipupper_op, alignment="left") + return self._apply_binary_op(upper, ops.minimum_op, alignment="left") if upper is None: - return self._apply_binary_op(lower, ops.cliplower_op, alignment="left") + return self._apply_binary_op(lower, ops.maximum_op, alignment="left") value_id, lower_id, upper_id, block = self._align3(lower, upper) block, result_id = block.apply_ternary_op( value_id, lower_id, upper_id, ops.clip_op @@ -1374,6 +1374,38 @@ def apply( materialized_series = result_series._cached() return materialized_series + def combine( + self, + other, + func, + ) -> Series: + if not callable(func): + raise ValueError( + "Only a ufunc (a function that applies to the entire Series) or a remote function that only works on single values are supported." + ) + + if not hasattr(func, "bigframes_remote_function"): + # Keep this in sync with .apply + try: + return func(self, other) + except Exception as ex: + # This could happen if any of the operators in func is not + # supported on a Series. Let's guide the customer to use a + # remote function instead + if hasattr(ex, "message"): + ex.message += f"\n{_remote_function_recommendation_message}" + raise + + reprojected_series = Series(self._block._force_reproject()) + result_series = reprojected_series._apply_binary_op( + other, ops.BinaryRemoteFunctionOp(func=func) + ) + + # return Series with materialized result so that any error in the remote + # function is caught early + materialized_series = result_series._cached() + return materialized_series + def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_prefix(prefix)) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index b7d99ea36ca..0fa1d90e8b0 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -221,6 +221,41 @@ def stringify(x): ) +# @pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_binop(session, scalars_dfs, dataset_id, bq_cf_connection): + try: + + def func(x, y): + return x * abs(y % 4) + + remote_func = session.remote_function( + [str, int], + str, + dataset_id, + bq_cf_connection, + reuse=False, + )(func) + + scalars_df, scalars_pandas_df = scalars_dfs + + scalars_df = scalars_df.dropna() + scalars_pandas_df = scalars_pandas_df.dropna() + bf_result = ( + scalars_df["string_col"] + .combine(scalars_df["int64_col"], remote_func) + .to_pandas() + ) + pd_result = scalars_pandas_df["string_col"].combine( + scalars_pandas_df["int64_col"], func + ) + pandas.testing.assert_series_equal(bf_result, pd_result) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, remote_func + ) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_decorator_with_bigframes_series( session, scalars_dfs, dataset_id, bq_cf_connection diff --git a/tests/system/small/test_numpy.py b/tests/system/small/test_numpy.py index 8e349e472a1..8f62d9628c0 100644 --- a/tests/system/small/test_numpy.py +++ b/tests/system/small/test_numpy.py @@ -73,27 +73,6 @@ def test_df_ufuncs(scalars_dfs, opname): pd.testing.assert_frame_equal(bf_result, pd_result) -@pytest.mark.parametrize( - ("opname",), - [ - ("add",), - ("subtract",), - ("multiply",), - ("divide",), - ("power",), - ("arctan2",), - ], -) -def test_series_binary_ufuncs(floats_product_pd, floats_product_bf, opname): - bf_result = getattr(np, opname)( - floats_product_bf.float64_col_x, floats_product_bf.float64_col_y - ).to_pandas() - pd_result = getattr(np, opname)( - floats_product_pd.float64_col_x, floats_product_pd.float64_col_y - ) - pd.testing.assert_series_equal(bf_result, pd_result) - - @pytest.mark.parametrize( ("opname",), [ @@ -106,17 +85,16 @@ def test_series_binary_ufuncs(floats_product_pd, floats_product_bf, opname): ) def test_df_binary_ufuncs(scalars_dfs, opname): scalars_df, scalars_pandas_df = scalars_dfs + op = getattr(np, opname) - bf_result = getattr(np, opname)( - scalars_df[["float64_col", "int64_col"]], 5.1 - ).to_pandas() - pd_result = getattr(np, opname)( - scalars_pandas_df[["float64_col", "int64_col"]], 5.1 - ) + bf_result = op(scalars_df[["float64_col", "int64_col"]], 5.1).to_pandas() + pd_result = op(scalars_pandas_df[["float64_col", "int64_col"]], 5.1) pd.testing.assert_frame_equal(bf_result, pd_result) +# Operations tested here don't work on full dataframe in numpy+pandas +# Maybe because of nullable dtypes? @pytest.mark.parametrize( ("x", "y"), [ @@ -124,12 +102,25 @@ def test_df_binary_ufuncs(scalars_dfs, opname): ("float64_col", "int64_col"), ], ) -def test_series_atan2(scalars_dfs, x, y): - # Test atan2 separately as pandas errors when passing entire df as input, so pass only series +@pytest.mark.parametrize( + ("opname",), + [ + ("add",), + ("subtract",), + ("multiply",), + ("divide",), + ("arctan2",), + ("minimum",), + ("maximum",), + ], +) +def test_series_binary_ufuncs(scalars_dfs, x, y, opname): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = np.arctan2(scalars_df[x], scalars_df[y]).to_pandas() - pd_result = np.arctan2(scalars_pandas_df[x], scalars_pandas_df[y]) + op = getattr(np, opname) + + bf_result = op(scalars_df[x], scalars_df[y]).to_pandas() + pd_result = op(scalars_pandas_df[x], scalars_pandas_df[y]) pd.testing.assert_series_equal(bf_result, pd_result) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index beb99b1adab..fa514784c01 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -3509,6 +3509,41 @@ def test_apply_numpy_ufunc(scalars_dfs, ufunc): assert_series_equal(bf_result, pd_result) +@pytest.mark.parametrize( + ("ufunc",), + [ + pytest.param(numpy.add), + pytest.param(numpy.divide), + ], + ids=[ + "add", + "divide", + ], +) +def test_combine_series_ufunc(scalars_dfs, ufunc): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_col = scalars_df["int64_col"].dropna() + bf_result = bf_col.combine(bf_col, ufunc).to_pandas() + + pd_col = scalars_pandas_df["int64_col"].dropna() + pd_result = pd_col.combine(pd_col, ufunc) + + assert_series_equal(bf_result, pd_result, check_dtype=False) + + +def test_combine_scalar_ufunc(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_col = scalars_df["int64_col"].dropna() + bf_result = bf_col.combine(2.5, numpy.add).to_pandas() + + pd_col = scalars_pandas_df["int64_col"].dropna() + pd_result = pd_col.combine(2.5, numpy.add) + + assert_series_equal(bf_result, pd_result, check_dtype=False) + + def test_apply_simple_udf(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index e155fb073ae..585e20275c7 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1279,6 +1279,62 @@ def apply( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def combine( + self, + other: Series | Hashable, + func, + ) -> Series: + """ + Combine the Series with a Series or scalar according to `func`. + + Combine the Series and `other` using `func` to perform elementwise + selection for combined Series. + `fill_value` is assumed when value is missing at some index + from one of the two objects being combined. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + Consider 2 Datasets ``s1`` and ``s2`` containing + highest clocked speeds of different birds. + + >>> s1 = bpd.Series({'falcon': 330.0, 'eagle': 160.0}) + >>> s1 + falcon 330.0 + eagle 160.0 + dtype: Float64 + >>> s2 = bpd.Series({'falcon': 345.0, 'eagle': 200.0, 'duck': 30.0}) + >>> s2 + falcon 345.0 + eagle 200.0 + duck 30.0 + dtype: Float64 + + Now, to combine the two datasets and view the highest speeds + of the birds across the two datasets + + >>> s1.combine(s2, np.maximum) + falcon 345.0 + eagle 200.0 + duck + dtype: Float64 + + Args: + other (Series or scalar): + The value(s) to be combined with the `Series`. + func (function): + BigFrames DataFrames ``remote_function`` to apply. + Takes two scalars as inputs and returns an element. + Also accepts some numpy binary functions. + + Returns: + Series: The result of combining the Series with the other object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def groupby( self, by=None, From 6eb19a7288155b093aa7cc9bcbc710b31e7dc87a Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 13 May 2024 10:13:08 -0700 Subject: [PATCH 564/921] feat: Series.str.split (#675) * feat: Series.str.split * add more tests * format fix --- bigframes/core/compile/scalar_op_compiler.py | 5 ++ bigframes/dtypes.py | 6 +++ bigframes/operations/__init__.py | 17 +++++-- bigframes/operations/strings.py | 12 +++++ tests/system/small/operations/test_strings.py | 31 ++++++++++++ .../pandas/core/strings/accessor.py | 48 +++++++++++++++++++ 6 files changed, 116 insertions(+), 3 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 90025b3994f..8a44844fbaf 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -588,6 +588,11 @@ def endswith_op_impl(x: ibis_types.Value, op: ops.EndsWithOp): return any_match if any_match is not None else ibis_types.literal(False) +@scalar_op_compiler.register_unary_op(ops.StringSplitOp, pass_op=True) +def stringsplit_op_impl(x: ibis_types.Value, op: ops.StringSplitOp): + return typing.cast(ibis_types.StringValue, x).split(op.pat) + + @scalar_op_compiler.register_unary_op(ops.ZfillOp, pass_op=True) def zfill_op_impl(x: ibis_types.Value, op: ops.ZfillOp): str_value = typing.cast(ibis_types.StringValue, x) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index d2dc210e0df..2a344aff2df 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -405,6 +405,12 @@ def bigframes_dtype_to_ibis_dtype( return BIGFRAMES_TO_IBIS[bigframes_dtype] +def bigframes_dtype_to_arrow_dtype( + bigframes_dtype: Union[DtypeString, Dtype, np.dtype[Any]] +) -> pa.DataType: + return ibis_dtype_to_arrow_dtype(bigframes_dtype_to_ibis_dtype(bigframes_dtype)) + + def literal_to_ibis_scalar( literal, force_dtype: typing.Optional[Dtype] = None, validate: bool = True ): diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 6f99f710136..929ccaecc59 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -386,6 +386,19 @@ def output_type(self, *input_types): return op_typing.STRING_PREDICATE.output_type(input_types[0]) +@dataclasses.dataclass(frozen=True) +class StringSplitOp(UnaryOp): + name: typing.ClassVar[str] = "str_split" + pat: typing.Sequence[str] + + def output_type(self, *input_types): + input_type = input_types[0] + if not isinstance(input_type, pd.StringDtype): + raise TypeError("field accessor input must be a string type") + arrow_type = dtypes.bigframes_dtype_to_arrow_dtype(input_type) + return pd.ArrowDtype(pa.list_(arrow_type)) + + @dataclasses.dataclass(frozen=True) class EndsWithOp(UnaryOp): name: typing.ClassVar[str] = "str_endswith" @@ -463,9 +476,7 @@ def output_type(self, *input_types): raise TypeError("field accessor input must be a struct type") pa_result_type = pa_type[self.name_or_index].type - # TODO: Directly convert from arrow to pandas type - ibis_result_type = dtypes.arrow_dtype_to_ibis_dtype(pa_result_type) - return dtypes.ibis_dtype_to_bigframes_dtype(ibis_result_type) + return dtypes.arrow_dtype_to_bigframes_dtype(pa_result_type) @dataclasses.dataclass(frozen=True) diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 883d19a1e32..22c325d7e06 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -247,6 +247,18 @@ def endswith( pat = (pat,) return self._apply_unary_op(ops.EndsWithOp(pat=pat)) + def split( + self, + pat: str = " ", + regex: Union[bool, None] = None, + ) -> series.Series: + if regex is True or (regex is None and len(pat) > 1): + raise NotImplementedError( + "Regular expressions aren't currently supported. Please set " + + f"`regex=False` and try again. {constants.FEEDBACK_LINK}" + ) + return self._apply_unary_op(ops.StringSplitOp(pat=pat)) + def zfill(self, width: int) -> series.Series: return self._apply_unary_op(ops.ZfillOp(width=width)) diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 9654c77ec44..b8a8ad2d1ed 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -531,3 +531,34 @@ def test_str_rjust(scalars_dfs): pd_result, bf_result, ) + + +@pytest.mark.parametrize( + ("pat", "regex"), + [ + pytest.param(" ", None, id="one_char"), + pytest.param("ll", False, id="two_chars"), + pytest.param( + " ", + True, + id="one_char_reg", + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + pytest.param( + "ll", + None, + id="two_chars_reg", + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + ], +) +def test_str_split_raise_errors(scalars_dfs, pat, regex): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = scalars_df[col_name].str.split(pat=pat, regex=regex).to_pandas() + pd_result = scalars_pandas_df[col_name].str.split(pat=pat, regex=regex) + + # TODO(b/336880368): Allow for NULL values for ARRAY columns in BigQuery. + pd_result = pd_result.apply(lambda x: [] if pd.isnull(x) is True else x) + + assert_series_equal(pd_result, bf_result, check_dtype=False) diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py index 5bb69dc1f27..b02c23f945a 100644 --- a/third_party/bigframes_vendored/pandas/core/strings/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -940,6 +940,54 @@ def endswith( """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def split( + self, + pat: str = " ", + regex: typing.Union[bool, None] = None, + ): + """ + Split strings around given separator/delimiter. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series( + ... [ + ... "a regular sentence", + ... "https://docs.python.org/index.html", + ... np.nan + ... ] + ... ) + >>> s.str.split() + 0 ['a' 'regular' 'sentence'] + 1 ['https://docs.python.org/index.html'] + 2 [] + dtype: list[pyarrow] + + The pat parameter can be used to split by other characters. + + >>> s.str.split("//", regex=False) + 0 ['a regular sentence'] + 1 ['https:' 'docs.python.org/index.html'] + 2 [] + dtype: list[pyarrow] + + Args: + pat (str, default " "): + String to split on. If not specified, split on whitespace. + regex (bool, default None): + Determines if the passed-in pattern is a regular expression. Regular + expressions aren't currently supported. Please set `regex=False` when + `pat` length is not 1. + + Returns: + bigframes.series.Series: Type matches caller. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def match(self, pat: str, case: bool = True, flags: int = 0): """ Determine if each string starts with a match of a regular expression. From c7e0eadfaa33ae2fde6e6c666b6c670258b5d643 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 13 May 2024 10:53:20 -0700 Subject: [PATCH 565/921] chore: add logger support for properties. (#683) * chore: add logger support for properties. * update function * update label format --- bigframes/core/log_adapter.py | 33 ++++++++++++++++++++++++++ tests/unit/session/test_io_bigquery.py | 8 ++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/bigframes/core/log_adapter.py b/bigframes/core/log_adapter.py index b5afafbe7c5..877e4a9fa1b 100644 --- a/bigframes/core/log_adapter.py +++ b/bigframes/core/log_adapter.py @@ -30,6 +30,10 @@ def class_logger(decorated_cls): for attr_name, attr_value in decorated_cls.__dict__.items(): if callable(attr_value) and (attr_name not in _excluded_methods): setattr(decorated_cls, attr_name, method_logger(attr_value, decorated_cls)) + elif isinstance(attr_value, property): + setattr( + decorated_cls, attr_name, property_logger(attr_value, decorated_cls) + ) return decorated_cls @@ -56,6 +60,35 @@ def wrapper(*args, **kwargs): return wrapper +def property_logger(prop, decorated_cls): + """Decorator that adds logging functionality to a property.""" + + def shared_wrapper(f): + @functools.wraps(f) + def wrapped(*args, **kwargs): + class_name = decorated_cls.__name__ + property_name = f.__name__ + full_property_name = f"{class_name.lower()}-{property_name.lower()}" + + if len(_call_stack) == 0: + add_api_method(full_property_name) + + _call_stack.append(full_property_name) + try: + return f(*args, **kwargs) + finally: + _call_stack.pop() + + return wrapped + + # Apply the wrapper to the getter, setter, and deleter + return property( + shared_wrapper(prop.fget), + shared_wrapper(prop.fset) if prop.fset else None, + shared_wrapper(prop.fdel) if prop.fdel else None, + ) + + def add_api_method(api_method_name): global _lock global _api_methods diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 5f4072e9c20..5a3470e8830 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -64,6 +64,7 @@ def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): # Test running two methods df.head() df.max() + df.columns api_methods = log_adapter._api_methods labels = io_bq.create_job_configs_labels( @@ -71,9 +72,10 @@ def test_create_job_configs_labels_log_adaptor_call_method_under_length_limit(): ) expected_dict = { "source": "bigquery-dataframes-temp", - "bigframes-api": "dataframe-max", - "recent-bigframes-api-0": "dataframe-head", - "recent-bigframes-api-1": "dataframe-__init__", + "bigframes-api": "dataframe-columns", + "recent-bigframes-api-0": "dataframe-max", + "recent-bigframes-api-1": "dataframe-head", + "recent-bigframes-api-2": "dataframe-__init__", } assert labels == expected_dict From 0b8b82761f94b240ea671b20732144a5481899aa Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 14:27:20 -0700 Subject: [PATCH 566/921] chore(main): release 1.6.0 (#667) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Huan Chen <142538604+Genesis929@users.noreply.github.com> --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4457c2e4432..568efa68b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.6.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.5.0...v1.6.0) (2024-05-13) + + +### Features + +* Add `DataFrame.__delitem__` ([#673](https://github.com/googleapis/python-bigquery-dataframes/issues/673)) ([2218c21](https://github.com/googleapis/python-bigquery-dataframes/commit/2218c21b5bb0f9e54a365ba1ada0203cbc4c9efc)) +* Add `Series.case_when()` ([#673](https://github.com/googleapis/python-bigquery-dataframes/issues/673)) ([2218c21](https://github.com/googleapis/python-bigquery-dataframes/commit/2218c21b5bb0f9e54a365ba1ada0203cbc4c9efc)) +* Add `strategy="quantile"` in KBinsDiscretizer ([#654](https://github.com/googleapis/python-bigquery-dataframes/issues/654)) ([c6c487f](https://github.com/googleapis/python-bigquery-dataframes/commit/c6c487fb3e39a980a05ff2dab5fb2b528d44016a)) +* Add Series.combine ([#680](https://github.com/googleapis/python-bigquery-dataframes/issues/680)) ([2fd1b81](https://github.com/googleapis/python-bigquery-dataframes/commit/2fd1b8117bda0dee5d8fc0924c80ce257fa9e3f1)) +* Series.str.split ([#675](https://github.com/googleapis/python-bigquery-dataframes/issues/675)) ([6eb19a7](https://github.com/googleapis/python-bigquery-dataframes/commit/6eb19a7288155b093aa7cc9bcbc710b31e7dc87a)) +* Suggest correct options in bpd.options.bigquery.location ([#666](https://github.com/googleapis/python-bigquery-dataframes/issues/666)) ([57ccabc](https://github.com/googleapis/python-bigquery-dataframes/commit/57ccabcd1402b7938e2c7068e5b4880ef018f39c)) +* Support `axis=1` in `df.apply` for scalar outputs ([#629](https://github.com/googleapis/python-bigquery-dataframes/issues/629)) ([f6bdc4a](https://github.com/googleapis/python-bigquery-dataframes/commit/f6bdc4aeb3f81a1e0b955521c04ac0dd22981c76)) +* Support gcf vpc connector in `remote_function` ([#677](https://github.com/googleapis/python-bigquery-dataframes/issues/677)) ([9ca92d0](https://github.com/googleapis/python-bigquery-dataframes/commit/9ca92d09e9c56db408350b35ec698152c13954ed)) +* Warn with a more specific `DefaultLocationWarning` category when no location can be detected ([#648](https://github.com/googleapis/python-bigquery-dataframes/issues/648)) ([e084e54](https://github.com/googleapis/python-bigquery-dataframes/commit/e084e54557addff78522bbd710637ecb4b46d23e)) + + +### Bug Fixes + +* Include `index_col` when selecting `columns` and `filters` in `read_gbq_table` ([#648](https://github.com/googleapis/python-bigquery-dataframes/issues/648)) ([e084e54](https://github.com/googleapis/python-bigquery-dataframes/commit/e084e54557addff78522bbd710637ecb4b46d23e)) + + +### Dependencies + +* Add jellyfish as a dependency for spelling correction ([57ccabc](https://github.com/googleapis/python-bigquery-dataframes/commit/57ccabcd1402b7938e2c7068e5b4880ef018f39c)) + + +### Documentation + +* Add code snippets for llm text generatiion ([#669](https://github.com/googleapis/python-bigquery-dataframes/issues/669)) ([93416ed](https://github.com/googleapis/python-bigquery-dataframes/commit/93416ed2f8353c12eb162e21e9bf155312b0ed8c)) +* Add logistic regression samples ([#673](https://github.com/googleapis/python-bigquery-dataframes/issues/673)) ([2218c21](https://github.com/googleapis/python-bigquery-dataframes/commit/2218c21b5bb0f9e54a365ba1ada0203cbc4c9efc)) +* Address lint errors in code samples ([#665](https://github.com/googleapis/python-bigquery-dataframes/issues/665)) ([4fc8964](https://github.com/googleapis/python-bigquery-dataframes/commit/4fc89644e47a6da9367b54826b25c6abbe97327b)) +* Document inlining of small data in `read_*` APIs ([#670](https://github.com/googleapis/python-bigquery-dataframes/issues/670)) ([306953a](https://github.com/googleapis/python-bigquery-dataframes/commit/306953aaae69e57c7c2f5eefb88d55a35bdcca9d)) + ## [1.5.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.4.0...v1.5.0) (2024-05-07) diff --git a/bigframes/version.py b/bigframes/version.py index 5f56ef9c61a..e139eaa89ef 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0" +__version__ = "1.6.0" From b02c401614eeab9cbf2e9a7c648b3d0a4e741b97 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 13 May 2024 16:06:07 -0700 Subject: [PATCH 567/921] docs: add code snippets for imported tensorflow model (#679) --- .../imported_tensorflow_model_test.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 samples/snippets/imported_tensorflow_model_test.py diff --git a/samples/snippets/imported_tensorflow_model_test.py b/samples/snippets/imported_tensorflow_model_test.py new file mode 100644 index 00000000000..4913c635c21 --- /dev/null +++ b/samples/snippets/imported_tensorflow_model_test.py @@ -0,0 +1,43 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (t +# you may not use this file except in compliance wi +# 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 +# distributed under the License is distributed on a +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, eit +# See the License for the specific language governi +# limitations under the License. + + +def test_imported_tensorflow_model() -> None: + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") + + # [START bigquery_dataframes_imported_tensorflow_tutorial_import_tensorflow_models] + import bigframes + from bigframes.ml.imported import TensorFlowModel + + bigframes.options.bigquery.project = PROJECT_ID + # You can change the location to one of the valid locations: https://cloud.google.com/bigquery/docs/locations#supported_locations + bigframes.options.bigquery.location = "US" + + imported_tensorflow_model = TensorFlowModel( + model_path="gs://cloud-training-demos/txtclass/export/exporter/1549825580/*" + ) + # [END bigquery_dataframes_imported_tensorflow_tutorial_import_tensorflow_models] + assert imported_tensorflow_model is not None + + # [START bigquery_dataframes_imported_tensorflow_tutorial_make_predictions] + import bigframes.pandas as bpd + + df = bpd.read_gbq("bigquery-public-data.hacker_news.full") + predictions = imported_tensorflow_model.predict(df) + predictions.head(5) + # [END bigquery_dataframes_imported_tensorflow_tutorial_make_predictions] From cb36e468d1c2a34c2231638124f3c8d9052f032b Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 14 May 2024 07:46:21 -0700 Subject: [PATCH 568/921] docs: add code snippets for imported onnx tutorials (#684) --- samples/snippets/imported_onnx_model_test.py | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 samples/snippets/imported_onnx_model_test.py diff --git a/samples/snippets/imported_onnx_model_test.py b/samples/snippets/imported_onnx_model_test.py new file mode 100644 index 00000000000..a74d673087a --- /dev/null +++ b/samples/snippets/imported_onnx_model_test.py @@ -0,0 +1,43 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (t +# you may not use this file except in compliance wi +# 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 +# distributed under the License is distributed on a +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, eit +# See the License for the specific language governi +# limitations under the License. + + +def test_imported_sklearn_onnx_model() -> None: + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") + + # [START bigquery_dataframes_imported_sklearn_onnx_tutorial_import_tensorflow_models] + import bigframes + from bigframes.ml.imported import ONNXModel + + bigframes.options.bigquery.project = PROJECT_ID + # You can change the location to one of the valid locations: https://cloud.google.com/bigquery/docs/locations#supported_locations + bigframes.options.bigquery.location = "US" + + imported_onnx_model = ONNXModel( + model_path="gs://cloud-samples-data/bigquery/ml/onnx/pipeline_rf.onnx" + ) + # [END bigquery_dataframes_imported_sklearn_onnx_tutorial_import_tensorflow_models] + assert imported_onnx_model is not None + + # [START bigquery_dataframes_imported_sklearn_onnx_tutorial_make_predictions] + import bigframes.pandas as bpd + + df = bpd.read_gbq("bigquery-public-data.ml_datasets.iris") + predictions = imported_onnx_model.predict(df) + predictions.head(5) + # [END bigquery_dataframes_imported_sklearn_onnx_tutorial_make_predictions] From adf98892e499f4a9c85162c38f56ca5634a1ba6d Mon Sep 17 00:00:00 2001 From: Lily Zhang <32233490+junyazhang@users.noreply.github.com> Date: Tue, 14 May 2024 13:28:00 -0700 Subject: [PATCH 569/921] feat: to_datetime supports utc=False for string inputs (#579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: to_datetime supports utc=False for string inputs * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * add unit tests for timestamp inputs * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * simplify conversion ops * address comments * fix failed presubmit test --------- Co-authored-by: Owl Bot Co-authored-by: Trevor Bergeron --- bigframes/core/compile/scalar_op_compiler.py | 39 +++++--- bigframes/core/tools/datetimes.py | 72 +++++++++++--- bigframes/operations/__init__.py | 28 +++++- tests/system/small/test_pandas.py | 96 +++++++++++++++++++ .../ibis/backends/bigquery/registry.py | 6 ++ .../ibis/expr/operations/generic.py | 4 + 6 files changed, 217 insertions(+), 28 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 8a44844fbaf..e8e5a1f3acf 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -17,6 +17,7 @@ import functools import typing +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis import ibis.common.exceptions import ibis.expr.datatypes as ibis_dtypes @@ -737,7 +738,7 @@ def struct_field_op_impl(x: ibis_types.Value, op: ops.StructFieldOp): return struct_value[name].name(name) -def numeric_to_datatime(x: ibis_types.Value, unit: str) -> ibis_types.TimestampValue: +def numeric_to_datetime(x: ibis_types.Value, unit: str) -> ibis_types.TimestampValue: if not isinstance(x, ibis_types.IntegerValue) and not isinstance( x, ibis_types.FloatingValue ): @@ -779,7 +780,7 @@ def astype_op_impl(x: ibis_types.Value, op: ops.AsTypeOp): # with pandas converting int64[pyarrow] to timestamp[us][pyarrow], # timestamp[us, tz=UTC][pyarrow], and time64[us][pyarrow]. unit = "us" - x_converted = numeric_to_datatime(x, unit) + x_converted = numeric_to_datetime(x, unit) if to_type == ibis_dtypes.timestamp: return x_converted.cast(ibis_dtypes.Timestamp()) elif to_type == ibis_dtypes.Timestamp(timezone="UTC"): @@ -818,23 +819,39 @@ def isin_op_impl(x: ibis_types.Value, op: ops.IsInOp): @scalar_op_compiler.register_unary_op(ops.ToDatetimeOp, pass_op=True) def to_datetime_op_impl(x: ibis_types.Value, op: ops.ToDatetimeOp): if x.type() == ibis_dtypes.str: - x = x.to_timestamp(op.format) if op.format else timestamp(x) - elif x.type() == ibis_dtypes.Timestamp(timezone="UTC"): + return vendored_ibis_ops.SafeCastToDatetime(x).to_expr() + else: + # Numerical inputs. if op.format: - raise NotImplementedError( - f"Format parameter is not supported for Timestamp input types. {constants.FEEDBACK_LINK}" - ) - return x - elif x.type() != ibis_dtypes.timestamp: + x = x.cast(ibis_dtypes.str).to_timestamp(op.format) + else: + # The default unit is set to "ns" (nanoseconds) for consistency + # with pandas, where "ns" is the default unit for datetime operations. + unit = op.unit or "ns" + x = numeric_to_datetime(x, unit) + + return x.cast(ibis_dtypes.Timestamp(None)) + + +@scalar_op_compiler.register_unary_op(ops.ToTimestampOp, pass_op=True) +def to_timestamp_op_impl(x: ibis_types.Value, op: ops.ToTimestampOp): + if x.type() == ibis_dtypes.str: + x = ( + typing.cast(ibis_types.StringValue, x).to_timestamp(op.format) + if op.format + else timestamp(x) + ) + else: + # Numerical inputs. if op.format: x = x.cast(ibis_dtypes.str).to_timestamp(op.format) else: # The default unit is set to "ns" (nanoseconds) for consistency # with pandas, where "ns" is the default unit for datetime operations. unit = op.unit or "ns" - x = numeric_to_datatime(x, unit) + x = numeric_to_datetime(x, unit) - return x.cast(ibis_dtypes.Timestamp(timezone="UTC" if op.utc else None)) + return x.cast(ibis_dtypes.Timestamp(timezone="UTC")) @scalar_op_compiler.register_unary_op(ops.RemoteFunctionOp, pass_op=True) diff --git a/bigframes/core/tools/datetimes.py b/bigframes/core/tools/datetimes.py index a2851bc256b..5eac4cceb94 100644 --- a/bigframes/core/tools/datetimes.py +++ b/bigframes/core/tools/datetimes.py @@ -21,6 +21,7 @@ import bigframes.constants as constants import bigframes.dataframe +import bigframes.dtypes import bigframes.operations as ops import bigframes.series @@ -51,25 +52,68 @@ def to_datetime( f"to datetime is not implemented. {constants.FEEDBACK_LINK}" ) - arg = bigframes.series.Series(arg) + arg = bigframes.series.Series(arg)._cached() - if not utc and arg.dtype not in ("Int64", "Float64"): # type: ignore - raise NotImplementedError( - f"String and Timestamp requires utc=True. {constants.FEEDBACK_LINK}" - ) - - if format and unit and arg.dtype in ("Int64", "Float64"): # type: ignore + if format and unit and arg.dtype in (bigframes.dtypes.INT_DTYPE, bigframes.dtypes.FLOAT_DTYPE): # type: ignore raise ValueError("cannot specify both format and unit") - if unit and arg.dtype not in ("Int64", "Float64"): # type: ignore + if unit and arg.dtype not in (bigframes.dtypes.INT_DTYPE, bigframes.dtypes.FLOAT_DTYPE): # type: ignore raise NotImplementedError( f"Unit parameter is not supported for non-numerical input types. {constants.FEEDBACK_LINK}" ) - return arg._apply_unary_op( # type: ignore - ops.ToDatetimeOp( - utc=utc, - format=format, - unit=unit, + if arg.dtype in (bigframes.dtypes.TIMESTAMP_DTYPE, bigframes.dtypes.DATETIME_DTYPE): + to_type = ( + bigframes.dtypes.TIMESTAMP_DTYPE if utc else bigframes.dtypes.DATETIME_DTYPE + ) + return arg._apply_unary_op(ops.AsTypeOp(to_type=to_type)) # type: ignore + if (not utc) and arg.dtype == bigframes.dtypes.STRING_DTYPE: + if format: + raise NotImplementedError( + f"Customized formats are not supported for string inputs when utc=False. Please set utc=True if possible. {constants.FEEDBACK_LINK}" + ) + + assert unit is None + as_datetime = arg._apply_unary_op( # type: ignore + ops.ToDatetimeOp( + format=format, + unit=unit, + ) + ) + failed_datetime_cast = arg.notnull() & as_datetime.isnull() + is_utc = arg._apply_unary_op( + ops.EndsWithOp( + pat=("Z", "-00:00", "+00:00", "-0000", "+0000", "-00", "+00") + ) + ) + + # Cast to DATETIME shall succeed if all inputs are tz-naive. + if not failed_datetime_cast.any(): + return as_datetime + + if is_utc.all(): + return arg._apply_unary_op( # type: ignore + ops.ToTimestampOp( + format=format, + unit=unit, + ) + ) + + raise NotImplementedError( + f"Non-UTC string inputs are not supported when utc=False. Please set utc=True if possible. {constants.FEEDBACK_LINK}" + ) + # If utc: + elif utc: + return arg._apply_unary_op( # type: ignore + ops.ToTimestampOp( + format=format, + unit=unit, + ) + ) + else: + return arg._apply_unary_op( # type: ignore + ops.ToDatetimeOp( + format=format, + unit=unit, + ) ) - ) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 929ccaecc59..2f39b096ce8 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -23,6 +23,7 @@ import pandas as pd import pyarrow as pa +import bigframes.dtypes import bigframes.dtypes as dtypes import bigframes.operations.type as op_typing @@ -527,13 +528,34 @@ def output_type(self, *input_types): @dataclasses.dataclass(frozen=True) class ToDatetimeOp(UnaryOp): name: typing.ClassVar[str] = "to_datetime" - utc: bool = False format: typing.Optional[str] = None unit: typing.Optional[str] = None def output_type(self, *input_types): - timezone = "UTC" if self.utc else None - return pd.ArrowDtype(pa.timestamp("us", tz=timezone)) + if input_types[0] not in ( + bigframes.dtypes.FLOAT_DTYPE, + bigframes.dtypes.INT_DTYPE, + bigframes.dtypes.STRING_DTYPE, + ): + raise TypeError("expected string or numeric input") + return pd.ArrowDtype(pa.timestamp("us", tz=None)) + + +@dataclasses.dataclass(frozen=True) +class ToTimestampOp(UnaryOp): + name: typing.ClassVar[str] = "to_timestamp" + format: typing.Optional[str] = None + unit: typing.Optional[str] = None + + def output_type(self, *input_types): + # Must be numeric or string + if input_types[0] not in ( + bigframes.dtypes.FLOAT_DTYPE, + bigframes.dtypes.INT_DTYPE, + bigframes.dtypes.STRING_DTYPE, + ): + raise TypeError("expected string or numeric input") + return pd.ArrowDtype(pa.timestamp("us", tz="UTC")) @dataclasses.dataclass(frozen=True) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index d543f926553..6eee01dd31d 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -634,3 +634,99 @@ def test_to_datetime_format_param(arg, utc, format): pd.testing.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) + + +@pytest.mark.parametrize( + ("arg", "utc", "output_in_utc", "format"), + [ + ( + ["2014-08-15 08:15:12", "2011-08-15 08:15:12", "2015-08-15 08:15:12"], + False, + False, + None, + ), + ( + [ + "2008-12-25 05:30:00Z", + "2008-12-25 05:30:00-00:00", + "2008-12-25 05:30:00+00:00", + "2008-12-25 05:30:00-0000", + "2008-12-25 05:30:00+0000", + "2008-12-25 05:30:00-00", + "2008-12-25 05:30:00+00", + ], + False, + True, + None, + ), + ( + ["2014-08-15 08:15:12", "2011-08-15 08:15:12", "2015-08-15 08:15:12"], + True, + True, + "%Y-%m-%d %H:%M:%S", + ), + ( + [ + "2014-08-15 08:15:12+05:00", + "2011-08-15 08:15:12+05:00", + "2015-08-15 08:15:12+05:00", + ], + True, + True, + None, + ), + ], +) +def test_to_datetime_string_inputs(arg, utc, output_in_utc, format): + bf_result = ( + bpd.to_datetime(arg, utc=utc, format=format) + .to_pandas() + .astype("datetime64[ns, UTC]" if output_in_utc else "datetime64[ns]") + ) + pd_result = pd.Series(pd.to_datetime(arg, utc=utc, format=format)).dt.floor("us") + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_names=False + ) + + +@pytest.mark.parametrize( + ("arg", "utc", "output_in_utc"), + [ + ( + [datetime(2023, 1, 1, 12, 0), datetime(2023, 2, 1, 12, 0)], + False, + False, + ), + ( + [datetime(2023, 1, 1, 12, 0), datetime(2023, 2, 1, 12, 0)], + True, + True, + ), + ( + [ + datetime(2023, 1, 1, 12, 0, tzinfo=pytz.timezone("UTC")), + datetime(2023, 1, 1, 12, 0, tzinfo=pytz.timezone("UTC")), + ], + True, + True, + ), + ( + [ + datetime(2023, 1, 1, 12, 0, tzinfo=pytz.timezone("America/New_York")), + datetime(2023, 1, 1, 12, 0, tzinfo=pytz.timezone("UTC")), + ], + True, + True, + ), + ], +) +def test_to_datetime_timestamp_inputs(arg, utc, output_in_utc): + bf_result = ( + bpd.to_datetime(arg, utc=utc) + .to_pandas() + .astype("datetime64[ns, UTC]" if output_in_utc else "datetime64[ns]") + ) + pd_result = pd.Series(pd.to_datetime(arg, utc=utc)).dt.floor("us") + pd.testing.assert_series_equal( + bf_result, pd_result, check_index_type=False, check_names=False + ) diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py index fddeab19a2a..64ef05366dc 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -32,6 +32,11 @@ def _generate_array(translator, op: vendored_ibis_ops.GenerateArray): return f"GENERATE_ARRAY(0, {arg})" +def _safe_cast_to_datetime(translator, op: vendored_ibis_ops.SafeCastToDatetime): + arg = translator.translate(op.arg) + return f"SAFE_CAST({arg} AS DATETIME)" + + def _quantile(translator, op: ibis_reductions.Quantile): arg = translator.translate(op.arg) quantile = translator.translate(op.quantile) @@ -44,6 +49,7 @@ def _quantile(translator, op: ibis_reductions.Quantile): vendored_ibis_ops.LastNonNullValue: _last_non_null_value, # type:ignore vendored_ibis_ops.ToJsonString: _to_json_string, # type:ignore vendored_ibis_ops.GenerateArray: _generate_array, # type:ignore + vendored_ibis_ops.SafeCastToDatetime: _safe_cast_to_datetime, # type:ignore ibis_reductions.Quantile: _quantile, # type:ignore } diff --git a/third_party/bigframes_vendored/ibis/expr/operations/generic.py b/third_party/bigframes_vendored/ibis/expr/operations/generic.py index 82d0a13371c..98acaacfbda 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/generic.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/generic.py @@ -7,3 +7,7 @@ class GenerateArray(Unary): dtype = dt.Array(dt.int64) + + +class SafeCastToDatetime(Unary): + dtype = dt.Timestamp(timezone=None) From 1a0b744c5aacdd8ba4eececf7b0a374808e8672c Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 14 May 2024 15:06:16 -0700 Subject: [PATCH 570/921] fix: the imported samples error and use peek() (#688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- samples/snippets/imported_onnx_model_test.py | 6 +++--- samples/snippets/imported_tensorflow_model_test.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/snippets/imported_onnx_model_test.py b/samples/snippets/imported_onnx_model_test.py index a74d673087a..87157ee60d3 100644 --- a/samples/snippets/imported_onnx_model_test.py +++ b/samples/snippets/imported_onnx_model_test.py @@ -20,7 +20,7 @@ def test_imported_sklearn_onnx_model() -> None: PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") - # [START bigquery_dataframes_imported_sklearn_onnx_tutorial_import_tensorflow_models] + # [START bigquery_dataframes_imported_sklearn_onnx_tutorial_import_onnx_models] import bigframes from bigframes.ml.imported import ONNXModel @@ -31,7 +31,7 @@ def test_imported_sklearn_onnx_model() -> None: imported_onnx_model = ONNXModel( model_path="gs://cloud-samples-data/bigquery/ml/onnx/pipeline_rf.onnx" ) - # [END bigquery_dataframes_imported_sklearn_onnx_tutorial_import_tensorflow_models] + # [END bigquery_dataframes_imported_sklearn_onnx_tutorial_import_onnx_models] assert imported_onnx_model is not None # [START bigquery_dataframes_imported_sklearn_onnx_tutorial_make_predictions] @@ -39,5 +39,5 @@ def test_imported_sklearn_onnx_model() -> None: df = bpd.read_gbq("bigquery-public-data.ml_datasets.iris") predictions = imported_onnx_model.predict(df) - predictions.head(5) + predictions.peek(5) # [END bigquery_dataframes_imported_sklearn_onnx_tutorial_make_predictions] diff --git a/samples/snippets/imported_tensorflow_model_test.py b/samples/snippets/imported_tensorflow_model_test.py index 4913c635c21..7dcf0ffe6d2 100644 --- a/samples/snippets/imported_tensorflow_model_test.py +++ b/samples/snippets/imported_tensorflow_model_test.py @@ -38,6 +38,7 @@ def test_imported_tensorflow_model() -> None: import bigframes.pandas as bpd df = bpd.read_gbq("bigquery-public-data.hacker_news.full") - predictions = imported_tensorflow_model.predict(df) + df_pred = df.rename(columns={"title": "input"}) + predictions = imported_tensorflow_model.predict(df_pred) predictions.head(5) # [END bigquery_dataframes_imported_tensorflow_tutorial_make_predictions] From 96243f23a1571001509d0d01c16c1e72e47e0d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 15 May 2024 14:34:22 -0500 Subject: [PATCH 571/921] fix: remove a usage of the `resource` package when not available, such as on Windows (#681) --- bigframes/pandas/__init__.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 8d2c0b148c5..c18d14b3dd0 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -19,7 +19,6 @@ from collections import namedtuple from datetime import datetime import inspect -import resource import sys import typing from typing import ( @@ -70,6 +69,13 @@ import bigframes.session._io.bigquery import bigframes.session.clients +try: + import resource +except ImportError: + # resource is only available on Unix-like systems. + # https://docs.python.org/3/library/resource.html + resource = None # type: ignore + # Include method definition so that the method appears in our docs for # bigframes.pandas general functions. @@ -810,12 +816,13 @@ def clean_up_by_session_id( # https://github.com/python/cpython/issues/112282 sys.setrecursionlimit(max(10000000, sys.getrecursionlimit())) -soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_STACK) -if soft_limit < hard_limit or hard_limit == resource.RLIM_INFINITY: - try: - resource.setrlimit(resource.RLIMIT_STACK, (hard_limit, hard_limit)) - except Exception: - pass +if resource is not None: + soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_STACK) + if soft_limit < hard_limit or hard_limit == resource.RLIM_INFINITY: + try: + resource.setrlimit(resource.RLIMIT_STACK, (hard_limit, hard_limit)) + except Exception: + pass # Use __all__ to let type checkers know what is part of the public API. __all___ = [ From da9b136df08b243c8515946f7c0d7b591b8fcbdc Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 15 May 2024 15:38:05 -0700 Subject: [PATCH 572/921] fix: Improve escaping of literals and identifiers (#682) --- bigframes/core/blocks.py | 14 +- bigframes/core/compile/compiled.py | 28 +--- bigframes/core/sql.py | 155 +++++++++++++++--- bigframes/session/_io/bigquery/__init__.py | 38 +++-- .../session/_io/bigquery/read_gbq_table.py | 31 ++-- tests/system/large/test_remote_function.py | 2 +- tests/unit/session/test_io_bigquery.py | 2 +- 7 files changed, 178 insertions(+), 92 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 58b85154184..830fa74f0be 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2096,7 +2096,7 @@ def _get_rows_as_json_values(self) -> Block: ) column_names.append(serialized_column_name) - column_names_csv = sql.csv(column_names, quoted=True) + column_names_csv = sql.csv(map(sql.simple_literal, column_names)) # index columns count index_columns_count = len(self.index_columns) @@ -2108,22 +2108,22 @@ def _get_rows_as_json_values(self) -> Block: # types of the columns to serialize for the row column_types = list(self.index.dtypes) + list(self.dtypes) - column_types_csv = sql.csv([str(typ) for typ in column_types], quoted=True) + column_types_csv = sql.csv( + [sql.simple_literal(str(typ)) for typ in column_types] + ) # row dtype to use for deserializing the row as pandas series pandas_row_dtype = bigframes.dtypes.lcd_type(*column_types) if pandas_row_dtype is None: pandas_row_dtype = "object" - pandas_row_dtype = sql.quote(str(pandas_row_dtype)) + pandas_row_dtype = sql.simple_literal(str(pandas_row_dtype)) # create a json column representing row through SQL manipulation row_json_column_name = guid.generate_guid() select_columns = ( [ordering_column_name] + list(self.index_columns) + [row_json_column_name] ) - select_columns_csv = sql.csv( - [sql.column_reference(col) for col in select_columns] - ) + select_columns_csv = sql.csv([sql.identifier(col) for col in select_columns]) json_sql = f"""\ With T0 AS ( {textwrap.indent(expr_sql, " ")} @@ -2136,7 +2136,7 @@ def _get_rows_as_json_values(self) -> Block: "values", [{column_references_csv}], "indexlength", {index_columns_count}, "dtype", {pandas_row_dtype} - ) AS {row_json_column_name} FROM T0 + ) AS {sql.identifier(row_json_column_name)} FROM T0 ) SELECT {select_columns_csv} FROM T1 """ diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index cc1d6baaa1c..b57e0c4d351 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -16,9 +16,8 @@ import abc import functools import itertools -import textwrap import typing -from typing import Collection, Iterable, Literal, Optional, Sequence +from typing import Collection, Literal, Optional, Sequence import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis @@ -40,6 +39,7 @@ OrderingExpression, ) import bigframes.core.schema as schemata +import bigframes.core.sql from bigframes.core.window_spec import RangeWindowBounds, RowsWindowBounds, WindowSpec import bigframes.dtypes import bigframes.operations.aggregations as agg_ops @@ -821,15 +821,13 @@ def to_sql( ) ) output_columns = [ - col_id_overrides.get(col) if (col in col_id_overrides) else col - for col in baked_ir.column_ids + col_id_overrides.get(col, col) for col in baked_ir.column_ids ] - selection = ", ".join(map(lambda col_id: f"`{col_id}`", output_columns)) + sql = bigframes.core.sql.select_from(output_columns, sql) - sql = textwrap.dedent(f"SELECT {selection}\n" "FROM (\n" f"{sql}\n" ")\n") # Single row frames may not have any ordering columns if len(baked_ir._ordering.all_ordering_columns) > 0: - order_by_clause = baked_ir._ordering_clause( + order_by_clause = bigframes.core.sql.ordering_clause( baked_ir._ordering.all_ordering_columns ) sql += f"{order_by_clause}\n" @@ -843,22 +841,6 @@ def to_sql( ) return typing.cast(str, sql) - def _ordering_clause(self, ordering: Iterable[OrderingExpression]) -> str: - parts = [] - for col_ref in ordering: - asc_desc = "ASC" if col_ref.direction.is_ascending else "DESC" - null_clause = "NULLS LAST" if col_ref.na_last else "NULLS FIRST" - ordering_expr = col_ref.scalar_expression - # We don't know how to compile scalar expressions in isolation - if ordering_expr.is_const: - # Probably shouldn't have constants in ordering definition, but best to ignore if somehow they end up here. - continue - if not isinstance(ordering_expr, ex.UnboundVariableExpression): - raise ValueError("Expected direct column reference.") - part = f"`{ordering_expr.id}` {asc_desc} {null_clause}" - parts.append(part) - return f"ORDER BY {' ,'.join(parts)}" - def _to_ibis_expr( self, *, diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index 31ee5f90642..3ad06610b68 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -11,49 +11,152 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """ Utility functions for SQL construction. """ -from typing import Iterable +import datetime +import math +import textwrap +from typing import Iterable, TYPE_CHECKING +# Literals and identifiers matching this pattern can be unquoted +unquoted = r"^[A-Za-z_][A-Za-z_0-9]*$" -def quote(value: str): - """Return quoted input string.""" - # Let's use repr which also escapes any special characters - # - # >>> for val in [ - # ... "123", - # ... "str with no special chars", - # ... "str with special chars.,'\"/\\" - # ... ]: - # ... print(f"{val} -> {repr(val)}") - # ... - # 123 -> '123' - # str with no special chars -> 'str with no special chars' - # str with special chars.,'"/\ -> 'str with special chars.,\'"/\\' +if TYPE_CHECKING: + import google.cloud.bigquery as bigquery - return repr(value) + import bigframes.core.ordering -def column_reference(column_name: str): +### Writing SQL Values (literals, column references, table references, etc.) +def simple_literal(value: str | int | bool | float): + """Return quoted input string.""" + # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#literals + if isinstance(value, str): + # Single quoting seems to work nicer with ibis than double quoting + return f"'{escape_special_characters(value)}'" + elif isinstance(value, (bool, int)): + return str(value) + elif isinstance(value, float): + # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#floating_point_literals + if math.isnan(value): + return 'CAST("nan" as FLOAT)' + if value == math.inf: + return 'CAST("+inf" as FLOAT)' + if value == -math.inf: + return 'CAST("-inf" as FLOAT)' + return str(value) + else: + raise ValueError(f"Cannot produce literal for {value}") + + +def multi_literal(*values: str): + literal_strings = [simple_literal(i) for i in values] + return "(" + ", ".join(literal_strings) + ")" + + +def identifier(id: str) -> str: """Return a string representing column reference in a SQL.""" + # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers + # Just always escape, otherwise need to check against every reserved sql keyword + return f"`{escape_special_characters(id)}`" + + +def escape_special_characters(value: str): + """Escapes all special charactesrs""" + # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#string_and_bytes_literals + trans_table = str.maketrans( + { + "\a": r"\a", + "\b": r"\b", + "\f": r"\f", + "\n": r"\n", + "\r": r"\r", + "\t": r"\t", + "\v": r"\v", + "\\": r"\\", + "?": r"\?", + '"': r"\"", + "'": r"\'", + "`": r"\`", + } + ) + return value.translate(trans_table) + + +def cast_as_string(column_name: str) -> str: + """Return a string representing string casting of a column.""" - return f"`{column_name}`" + return f"CAST({identifier(column_name)} AS STRING)" -def cast_as_string(column_name: str): - """Return a string representing string casting of a column.""" +def csv(values: Iterable[str]) -> str: + """Return a string of comma separated values.""" + return ", ".join(values) - return f"CAST({column_reference(column_name)} AS STRING)" +def table_reference(table_ref: bigquery.TableReference) -> str: + return f"`{escape_special_characters(table_ref.project)}`.`{escape_special_characters(table_ref.dataset_id)}`.`{escape_special_characters(table_ref.table_id)}`" -def csv(values: Iterable[str], quoted=False): - """Return a string of comma separated values.""" - if quoted: - values = [quote(val) for val in values] +def infix_op(opname: str, left_arg: str, right_arg: str): + # Maybe should add parentheses?? + return f"{left_arg} {opname} {right_arg}" - return ", ".join(values) + +### Writing SELECT expressions +def select_from(columns: Iterable[str], subquery: str, distinct: bool = False): + selection = ", ".join(map(identifier, columns)) + distinct_clause = "DISTINCT " if distinct else "" + + return textwrap.dedent( + f"SELECT {distinct_clause}{selection}\nFROM (\n" f"{subquery}\n" ")\n" + ) + + +def select_table(table_ref: bigquery.TableReference): + return textwrap.dedent(f"SELECT * FROM {table_reference(table_ref)}") + + +def is_distinct_sql(columns: Iterable[str], table_sql: str) -> str: + is_unique_sql = f"""WITH full_table AS ( + {select_from(columns, table_sql)} + ), + distinct_table AS ( + {select_from(columns, table_sql, distinct=True)} + ) + + SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, + (SELECT COUNT(*) FROM distinct_table) AS `distinct_count` + """ + return is_unique_sql + + +def ordering_clause( + ordering: Iterable[bigframes.core.ordering.OrderingExpression], +) -> str: + import bigframes.core.expression + + parts = [] + for col_ref in ordering: + asc_desc = "ASC" if col_ref.direction.is_ascending else "DESC" + null_clause = "NULLS LAST" if col_ref.na_last else "NULLS FIRST" + ordering_expr = col_ref.scalar_expression + # We don't know how to compile scalar expressions in isolation + if ordering_expr.is_const: + # Probably shouldn't have constants in ordering definition, but best to ignore if somehow they end up here. + continue + assert isinstance( + ordering_expr, bigframes.core.expression.UnboundVariableExpression + ) + part = f"`{ordering_expr.id}` {asc_desc} {null_clause}" + parts.append(part) + return f"ORDER BY {' ,'.join(parts)}" + + +def snapshot_clause(time_travel_timestamp: datetime.datetime): + return f"FOR SYSTEM_TIME AS OF TIMESTAMP({repr(time_travel_timestamp.isoformat())})" diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index ed1bd39adae..95ab16fecfd 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -31,6 +31,7 @@ import bigframes from bigframes.core import log_adapter +import bigframes.core.sql import bigframes.formatting_helpers as formatting_helpers IO_ORDERING_ID = "bqdf_row_nums" @@ -353,7 +354,7 @@ def to_query( else: select_clause = "SELECT *" - where_clause = "" + filter_string = "" if filters: valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { "in": "IN", @@ -373,12 +374,11 @@ def to_query( ): filters = typing.cast(third_party_pandas_gbq.FiltersType, [filters]) - or_expressions = [] for group in filters: if not isinstance(group, Iterable): group = [group] - and_expressions = [] + and_expression = "" for filter_item in group: if not isinstance(filter_item, tuple) or (len(filter_item) != 3): raise ValueError( @@ -397,17 +397,29 @@ def to_query( operator_str = valid_operators[operator] + column_ref = bigframes.core.sql.identifier(column) if operator_str in ["IN", "NOT IN"]: - value_list = ", ".join([repr(v) for v in value]) - expression = f"`{column}` {operator_str} ({value_list})" + value_literal = bigframes.core.sql.multi_literal(*value) else: - expression = f"`{column}` {operator_str} {repr(value)}" - and_expressions.append(expression) - - or_expressions.append(" AND ".join(and_expressions)) + value_literal = bigframes.core.sql.simple_literal(value) + expression = bigframes.core.sql.infix_op( + operator_str, column_ref, value_literal + ) + if and_expression: + and_expression = bigframes.core.sql.infix_op( + "AND", and_expression, expression + ) + else: + and_expression = expression - if or_expressions: - where_clause = " WHERE " + " OR ".join(or_expressions) + if filter_string: + filter_string = bigframes.core.sql.infix_op( + "OR", filter_string, and_expression + ) + else: + filter_string = and_expression - full_query = f"{select_clause} FROM {sub_query} AS sub{where_clause}" - return full_query + if filter_string: + return f"{select_clause} FROM {sub_query} AS sub WHERE {filter_string}" + else: + return f"{select_clause} FROM {sub_query} AS sub" diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 0f6a3dadd2b..370ee546d78 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -40,6 +40,7 @@ import bigframes.core.compile import bigframes.core.guid as guid import bigframes.core.ordering as order +import bigframes.core.sql import bigframes.dtypes import bigframes.session._io.bigquery.read_gbq_table import bigframes.session.clients @@ -131,14 +132,14 @@ def _create_time_travel_sql( """Query a table via 'time travel' for consistent reads.""" # If we have an anonymous query results table, it can't be modified and # there isn't any BigQuery time travel. + selection = bigframes.core.sql.select_table(table_ref) if table_ref.dataset_id.startswith("_"): - return f"SELECT * FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}`" + return selection return textwrap.dedent( f""" - SELECT * - FROM `{table_ref.project}`.`{table_ref.dataset_id}`.`{table_ref.table_id}` - FOR SYSTEM_TIME AS OF TIMESTAMP({repr(time_travel_timestamp.isoformat())}) + {selection} + {bigframes.core.sql.snapshot_clause(time_travel_timestamp)} """ ) @@ -149,9 +150,8 @@ def get_ibis_time_travel_table( time_travel_timestamp: datetime.datetime, ) -> ibis_types.Table: try: - return ibis_client.sql( - _create_time_travel_sql(table_ref, time_travel_timestamp) - ) + sql = _create_time_travel_sql(table_ref, time_travel_timestamp) + return ibis_client.sql(sql) except google.api_core.exceptions.Forbidden as ex: # Ibis does a dry run to get the types of the columns from the SQL. if "Drive credentials" in ex.message: @@ -166,25 +166,14 @@ def _check_index_uniqueness( index_cols: List[str], api_name: str, ) -> bool: - distinct_table = table.select(*index_cols).distinct() - is_unique_sql = f"""WITH full_table AS ( - {ibis_client.compile(table)} - ), - distinct_table AS ( - {ibis_client.compile(distinct_table)} - ) - - SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, - (SELECT COUNT(*) FROM distinct_table) AS `distinct_count` - """ + table_sql = ibis_client.compile(table) + is_unique_sql = bigframes.core.sql.is_distinct_sql(index_cols, table_sql) job_config = bigquery.QueryJobConfig() job_config.labels["bigframes-api"] = api_name results = bqclient.query_and_wait(is_unique_sql, job_config=job_config) row = next(iter(results)) - total_count = row["total_count"] - distinct_count = row["distinct_count"] - return total_count == distinct_count + return row["total_count"] == row["distinct_count"] def _get_primary_keys( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 0fa1d90e8b0..4aa62c0f6d8 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -221,7 +221,7 @@ def stringify(x): ) -# @pytest.mark.flaky(retries=2, delay=120) +@pytest.mark.flaky(retries=2, delay=120) def test_remote_function_binop(session, scalars_dfs, dataset_id, bq_cf_connection): try: diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 5a3470e8830..57f9e003636 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -249,7 +249,7 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) rowindex, string_col, FROM `test_table` AS t - ) AS sub WHERE `rowindex` < 4 AND `string_col` = 'Hello, World!'""", + ) AS sub WHERE `rowindex` < 4 AND `string_col` = \'Hello, World!\'""", id="subquery-all_params-filter_and_operation", ), pytest.param( From 9fee857a3a8c67f79c2a1d583f8fded4e212f74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 15 May 2024 17:47:28 -0500 Subject: [PATCH 573/921] chore: type check with mypy `--check-untyped-defs` option (#693) * chore: type check with mypy `--check-untyped-defs` option * add TODOs for tests/system/small/ml/test_metrics.py * add TODOs for tests/system/large/ml/test_cluster.py * add TODOs for tests/system/large/ml/test_decomposition.py * more TODOs * test_dataframe TODOs * tests/system/small/test_encryption.py TODOs * test_index TODOs * TODO test_index, test_multiindex * tests/system/small/test_pandas.py TODOs * test_series TODOs * more TODOs * tests/system/large/ml/test_ensemble.py TODOs * more TODOs * more TODOs * add final TODOs * add bug number --- bigframes/core/blocks.py | 3 +- bigframes/core/indexers.py | 3 +- bigframes/core/indexes/base.py | 3 +- bigframes/dataframe.py | 3 +- bigframes/functions/remote_function.py | 3 +- bigframes/operations/__init__.py | 9 +- bigframes/operations/_matplotlib/__init__.py | 3 +- bigframes/operations/_matplotlib/core.py | 3 +- bigframes/session/clients.py | 26 +++--- noxfile.py | 1 + tests/system/conftest.py | 4 +- tests/system/large/ml/test_cluster.py | 6 +- tests/system/large/ml/test_compose.py | 6 +- tests/system/large/ml/test_decomposition.py | 12 ++- tests/system/large/ml/test_ensemble.py | 27 ++++-- tests/system/large/ml/test_forecasting.py | 3 +- tests/system/large/ml/test_linear_model.py | 12 ++- tests/system/large/test_remote_function.py | 3 +- tests/system/load/test_large_tables.py | 6 +- tests/system/small/ml/test_metrics.py | 54 +++++++---- tests/system/small/ml/test_model_selection.py | 32 ++++--- .../system/small/operations/test_datetimes.py | 3 +- .../system/small/operations/test_plotting.py | 3 +- tests/system/small/test_dataframe.py | 91 +++++++++++++------ tests/system/small/test_encryption.py | 21 +++-- tests/system/small/test_index.py | 21 +++-- tests/system/small/test_multiindex.py | 18 ++-- tests/system/small/test_pandas.py | 30 ++++-- tests/system/small/test_remote_function.py | 3 +- tests/system/small/test_series.py | 17 ++-- tests/system/small/test_session.py | 9 +- tests/unit/ml/test_api_primitives.py | 6 +- tests/unit/test_pandas.py | 3 +- 33 files changed, 295 insertions(+), 152 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 830fa74f0be..6cd93eec124 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1226,8 +1226,9 @@ def calculate_pairwise_metric(self, op=agg_ops.CorrOp()): ) labels = self._get_labels_for_columns(self.value_columns) + # TODO(b/340896143): fix type error expr = expr.unpivot( - row_labels=labels, + row_labels=labels, # type: ignore index_col_ids=index_col_ids, unpivot_columns=unpivot_columns, ) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index bc03bd1df03..582141d5390 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -160,7 +160,8 @@ def __getitem__(self, key): columns = key[1] if isinstance(columns, pd.Series) and columns.dtype == "bool": - columns = df.columns[columns] + # TODO(b/340892590): fix type error + columns = df.columns[columns] # type: ignore return df[columns] diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 569dae4ffc3..2db3e0791fa 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -91,7 +91,8 @@ def __new__( from bigframes.core.indexes.multi import MultiIndex klass = MultiIndex if len(block._index_columns) > 1 else cls - result = typing.cast(Index, object.__new__(klass)) + # TODO(b/340893286): fix type error + result = typing.cast(Index, object.__new__(klass)) # type: ignore result._query_job = None result._block = block return result diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d3fd39afa76..2ac423e3945 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1762,7 +1762,8 @@ def label_filter(label): if like: return like in label_str else: # regex - return re.match(regex, label_str) is not None + # TODO(b/340891296): fix type error + return re.match(regex, label_str) is not None # type: ignore cols = [ col_id diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 2a7a900779a..9eff802cc76 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -1023,8 +1023,9 @@ def wrapper(f): raise TypeError("f must be callable, got {}".format(f)) signature = inspect.signature(f) + # TODO(b/340898611): fix type error ibis_signature = ibis_signature_from_python_signature( - signature, input_types, output_type + signature, input_types, output_type # type: ignore ) remote_function_client = RemoteFunctionClient( diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 2f39b096ce8..fe9fe6df206 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -491,7 +491,8 @@ def output_type(self, *input_types): if self.to_type == pa.string(): return dtypes.STRING_DTYPE if isinstance(self.to_type, str): - return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[self.to_type] + # TODO(b/340895446): fix type error + return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[self.to_type] # type: ignore return self.to_type @@ -513,7 +514,8 @@ class RemoteFunctionOp(UnaryOp): def output_type(self, *input_types): # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method - return self.func.output_dtype + # TODO(b/340895446): fix type error + return self.func.output_dtype # type: ignore @dataclasses.dataclass(frozen=True) @@ -627,7 +629,8 @@ class BinaryRemoteFunctionOp(BinaryOp): def output_type(self, *input_types): # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method - return self.func.output_dtype + # TODO(b/340895446): fix type error + return self.func.output_dtype # type: ignore add_op = AddOp() diff --git a/bigframes/operations/_matplotlib/__init__.py b/bigframes/operations/_matplotlib/__init__.py index 02aca8cf5d4..f869c1e01df 100644 --- a/bigframes/operations/_matplotlib/__init__.py +++ b/bigframes/operations/_matplotlib/__init__.py @@ -24,7 +24,8 @@ def plot(data, kind, **kwargs): - plot_obj = PLOT_CLASSES[kind](data, **kwargs) + # TODO(b/340896123): fix type error + plot_obj = PLOT_CLASSES[kind](data, **kwargs) # type: ignore plot_obj.generate() plot_obj.draw() return plot_obj.result diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index 04534e20a94..78b3df1f19e 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -39,7 +39,8 @@ def draw(self) -> None: @property def result(self): - return self.axes + # TODO(b/340896123): fix type error + return self.axes # type: ignore class SamplingPlot(MPLPlot): diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index 32f13fa00db..e7680d1d35c 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -117,10 +117,11 @@ def __init__( def _create_bigquery_client(self): bq_options = None if self._use_regional_endpoints: + # TODO(b/340896138): fix type error bq_options = google.api_core.client_options.ClientOptions( api_endpoint=( _BIGQUERY_REGIONAL_ENDPOINT - if self._location.lower() in _REP_SUPPORTED_REGIONS + if self._location.lower() in _REP_SUPPORTED_REGIONS # type: ignore else _BIGQUERY_LOCATIONAL_ENDPOINT ).format(location=self._location), ) @@ -158,12 +159,11 @@ def bqconnectionclient(self): bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) - self._bqconnectionclient = ( - google.cloud.bigquery_connection_v1.ConnectionServiceClient( - client_info=bqconnection_info, - client_options=bqconnection_options, - credentials=self._credentials, - ) + # TODO(b/340896138): fix type error + self._bqconnectionclient = google.cloud.bigquery_connection_v1.ConnectionServiceClient( # type: ignore + client_info=bqconnection_info, + client_options=bqconnection_options, + credentials=self._credentials, ) return self._bqconnectionclient @@ -173,18 +173,20 @@ def bqstoragereadclient(self): if not self._bqstoragereadclient: bqstorage_options = None if self._use_regional_endpoints: + # TODO(b/340896138): fix type error bqstorage_options = google.api_core.client_options.ClientOptions( api_endpoint=( _BIGQUERYSTORAGE_REGIONAL_ENDPOINT - if self._location.lower() in _REP_SUPPORTED_REGIONS + if self._location.lower() in _REP_SUPPORTED_REGIONS # type: ignore else _BIGQUERYSTORAGE_LOCATIONAL_ENDPOINT ).format(location=self._location), ) bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) + # TODO(b/340896138): fix type error self._bqstoragereadclient = ( - google.cloud.bigquery_storage_v1.BigQueryReadClient( + google.cloud.bigquery_storage_v1.BigQueryReadClient( # type: ignore client_info=bqstorage_info, client_options=bqstorage_options, credentials=self._credentials, @@ -199,8 +201,9 @@ def cloudfunctionsclient(self): functions_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) + # TODO(b/340896138): fix type error self._cloudfunctionsclient = ( - google.cloud.functions_v2.FunctionServiceClient( + google.cloud.functions_v2.FunctionServiceClient( # type: ignore client_info=functions_info, credentials=self._credentials, ) @@ -214,8 +217,9 @@ def resourcemanagerclient(self): resourcemanager_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) + # TODO(b/340896138): fix type error self._resourcemanagerclient = ( - google.cloud.resourcemanager_v3.ProjectsClient( + google.cloud.resourcemanager_v3.ProjectsClient( # type: ignore credentials=self._credentials, client_info=resourcemanager_info ) ) diff --git a/noxfile.py b/noxfile.py index af73495a7f3..c816ec5f511 100644 --- a/noxfile.py +++ b/noxfile.py @@ -248,6 +248,7 @@ def mypy(session): "bigframes", os.path.join("tests", "system"), os.path.join("tests", "unit"), + "--check-untyped-defs", "--explicit-package-bases", '--exclude="^third_party"', ) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 4ebb3cb93b4..a040f2b19ba 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -1040,7 +1040,7 @@ def floats_pd(): dtype=pd.Float64Dtype(), ) # Index helps debug failed cases - df.index = df.float64_col + df.index = df.float64_col # type: ignore # Upload fails if index name same as column name df.index.name = None return df.float64_col @@ -1050,7 +1050,7 @@ def floats_pd(): def floats_product_pd(floats_pd): df = pd.merge(floats_pd, floats_pd, how="cross") # Index helps debug failed cases - df = df.set_index([df.float64_col_x, df.float64_col_y]) + df = df.set_index([df.float64_col_x, df.float64_col_y]) # type: ignore df.index.names = ["left", "right"] return df diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py index b633ca4ea2b..b65b6e5083f 100644 --- a/tests/system/large/ml/test_cluster.py +++ b/tests/system/large/ml/test_cluster.py @@ -113,9 +113,10 @@ def test_cluster_configure_fit_score_predict( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_cluster_model", replace=True ) + # TODO(b/340875247): fix type error assert ( f"{dataset_id}.temp_configured_cluster_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.n_clusters == 3 assert reloaded_model.init == "RANDOM" @@ -153,9 +154,10 @@ def test_cluster_configure_fit_load_params(penguins_df_default_index, dataset_id reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_cluster_model", replace=True ) + # TODO(b/340875247): fix type error assert ( f"{dataset_id}.temp_configured_cluster_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.n_clusters == 4 assert reloaded_model.init == "RANDOM" diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 7513b78b29e..38c5014c19b 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -147,7 +147,8 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] ).to_pandas() - expected = pandas.DataFrame( + # TODO(b/340888429): fix type error + expected = pandas.DataFrame( # type: ignore { "onehotencoded_species": [ [{"index": 1, "value": 1.0}], @@ -164,4 +165,5 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), ) - pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) + # TODO(b/340888429): fix type error + pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) # type: ignore diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index 264b95a92e6..9dab85023a6 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -79,9 +79,10 @@ def test_decomposition_configure_fit_score_predict( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) + # TODO(b/340876028): fix type error assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.n_components == 3 @@ -149,9 +150,10 @@ def test_decomposition_configure_fit_score_predict_params( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) + # TODO(b/340876028): fix type error assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.n_components == 5 assert reloaded_model.svd_solver == "RANDOMIZED" @@ -167,9 +169,10 @@ def test_decomposition_configure_fit_load_float_component( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) + # TODO(b/340876028): fix type error assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.n_components == 0.2 @@ -184,8 +187,9 @@ def test_decomposition_configure_fit_load_none_component( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) + # TODO(b/340876028): fix type error assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.n_components == 7 diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index 3d1fcaf41c7..b9aae219563 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -60,9 +60,10 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbregressor_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_xgbregressor_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) @@ -118,9 +119,10 @@ def test_xgbregressor_dart_booster_multiple_params( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbregressor_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_xgbregressor_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.booster == "DART" assert reloaded_model.dart_normalized_type == "TREE" @@ -174,9 +176,10 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbclassifier_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_xgbclassifier_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) @@ -231,9 +234,10 @@ def test_xgbclassifier_dart_booster_multiple_params( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbclassifier_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_xgbclassifier_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.booster == "DART" assert reloaded_model.dart_normalized_type == "TREE" @@ -288,9 +292,10 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestregressor_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_randomforestregressor_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) @@ -341,9 +346,10 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestregressor_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_randomforestregressor_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.tree_method == "AUTO" assert reloaded_model.colsample_bytree == 0.95 @@ -394,16 +400,18 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestclassifier_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_randomforestclassifier_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) @pytest.mark.flaky(retries=2) def test_randomforestclassifier_multiple_params(penguins_df_default_index, dataset_id): + # TODO(b/340888645): fix type error model = bigframes.ml.ensemble.RandomForestClassifier( - tree_method="AUTO", + tree_method="AUTO", # type: ignore min_tree_child_weight=2, colsample_bytree=0.95, colsample_bylevel=0.95, @@ -446,9 +454,10 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestclassifier_model", replace=True ) + # TODO(b/340888645): fix type error assert ( f"{dataset_id}.temp_configured_randomforestclassifier_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.tree_method == "AUTO" assert reloaded_model.colsample_bytree == 0.95 diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index ef74398c2e2..1c0924245ed 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -119,8 +119,9 @@ def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): # save, load to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) + # TODO(b/340891711): fix type error assert ( - f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name + f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.horizon == 100 diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 99121e4a31d..50e3de3fc45 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -52,8 +52,9 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) + # TODO(b/340890167): fix type error assert ( - f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.optimize_strategy == "NORMAL_EQUATION" assert reloaded_model.fit_intercept is True @@ -114,8 +115,9 @@ def test_linear_regression_customized_params_fit_score( # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) + # TODO(b/340890167): fix type error assert ( - f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" assert reloaded_model.fit_intercept is False @@ -170,9 +172,10 @@ def test_logistic_regression_configure_fit_score(penguins_df_default_index, data reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_logistic_reg_model", replace=True ) + # TODO(b/340890167): fix type error assert ( f"{dataset_id}.temp_configured_logistic_reg_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) assert reloaded_model.fit_intercept is True assert reloaded_model.class_weight is None @@ -225,9 +228,10 @@ def test_logistic_regression_customized_params_fit_score( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_logistic_reg_model", replace=True ) + # TODO(b/340890167): fix type error assert ( f"{dataset_id}.temp_configured_logistic_reg_model" - in reloaded_model._bqml_model.model_name + in reloaded_model._bqml_model.model_name # type: ignore ) # TODO(garrettwu) optimize_strategy isn't logged in BQML # assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 4aa62c0f6d8..cac8483b5ba 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -92,7 +92,8 @@ def make_uniq_udf(udf): target_code = source_code.replace(source_key, target_key, 1) f.write(target_code) spec = importlib.util.spec_from_file_location(udf_file_name, udf_file_path) - udf_uniq = getattr(spec.loader.load_module(), udf_uniq_name) + # TODO(b/340875260): fix type error + udf_uniq = getattr(spec.loader.load_module(), udf_uniq_name) # type: ignore # This is a bit of a hack but we need to remove the reference to a foreign # module, otherwise the serialization would keep the foreign module diff --git a/tests/system/load/test_large_tables.py b/tests/system/load/test_large_tables.py index 871c846c799..2621e62dde6 100644 --- a/tests/system/load/test_large_tables.py +++ b/tests/system/load/test_large_tables.py @@ -80,7 +80,8 @@ def test_to_pandas_batches_large_table(): expected_row_count, expected_column_count = df.shape row_count = 0 - for df in df.to_pandas_batches(): + # TODO(b/340890167): fix type error + for df in df.to_pandas_batches(): # type: ignore batch_row_count, batch_column_count = df.shape assert batch_column_count == expected_column_count row_count += batch_row_count @@ -97,7 +98,8 @@ def test_to_pandas_large_table(): # df will be downloaded locally expected_row_count, expected_column_count = df.shape - df = df.to_pandas() + # TODO(b/340893653): fix type error + df = df.to_pandas() # type: ignore row_count, column_count = df.shape assert column_count == expected_column_count assert row_count == expected_row_count diff --git a/tests/system/small/ml/test_metrics.py b/tests/system/small/ml/test_metrics.py index c4c7eb4b881..9aff2fe773d 100644 --- a/tests/system/small/ml/test_metrics.py +++ b/tests/system/small/ml/test_metrics.py @@ -228,12 +228,14 @@ def test_roc_curve_binary_classification_prediction_matches_sklearn(session): np_thresholds[1:], expected_thresholds[1:], ) + # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_fpr, + np_fpr, # type: ignore expected_fpr, ) + # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_tpr, + np_tpr, # type: ignore expected_tpr, ) @@ -323,12 +325,14 @@ def test_roc_curve_binary_classification_decision_matches_sklearn(session): np_thresholds[1:], expected_thresholds[1:], ) + # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_fpr, + np_fpr, # type: ignore expected_fpr, ) + # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_tpr, + np_tpr, # type: ignore expected_tpr, ) @@ -515,8 +519,9 @@ def test_confusion_matrix_column_index(session): ).astype("Int64") df = session.read_pandas(pd_df) confusion_matrix = metrics.confusion_matrix(df[["y_true"]], df[["y_pred"]]) + # TODO(b/340872435): fix type error expected_pd_df = ( - pd.DataFrame( + pd.DataFrame( # type: ignore {1: [1, 0, 1, 0], 2: [0, 0, 2, 0], 3: [0, 0, 0, 0], 4: [0, 1, 0, 1]} ) .astype("int64") @@ -557,7 +562,8 @@ def test_confusion_matrix_str_matches_sklearn(session): expected_confusion_matrix = sklearn_metrics.confusion_matrix( pd_df[["y_true"]], pd_df[["y_pred"]] ) - expected_pd_df = pd.DataFrame(expected_confusion_matrix).set_index( + # TODO(b/340872435): fix type error + expected_pd_df = pd.DataFrame(expected_confusion_matrix).set_index( # type: ignore [pd.Index(["ant", "bird", "cat"])] ) expected_pd_df.columns = pd.Index(["ant", "bird", "cat"]) @@ -595,8 +601,9 @@ def test_recall_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) + # TODO(b/340872435): fix type error recall = metrics.recall_score( - df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None # type: ignore ) expected_values = [1.000000, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -613,7 +620,8 @@ def test_recall_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) + # TODO(b/340872435): fix type error + recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore expected_values = sklearn_metrics.recall_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -630,7 +638,8 @@ def test_recall_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) + # TODO(b/340872435): fix type error + recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore expected_values = sklearn_metrics.recall_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -647,7 +656,8 @@ def test_recall_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - recall = metrics.recall_score(df["y_true"], df["y_pred"], average=None) + # TODO(b/340872435): fix type error + recall = metrics.recall_score(df["y_true"], df["y_pred"], average=None) # type: ignore expected_values = [1.000000, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_recall = pd.Series(expected_values, index=expected_index) @@ -663,8 +673,9 @@ def test_precision_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) + # TODO(b/340872435): fix type error precision_score = metrics.precision_score( - df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None # type: ignore ) expected_values = [0.666667, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -683,8 +694,9 @@ def test_precision_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) + # TODO(b/340872435): fix type error precision_score = metrics.precision_score( - df[["y_true"]], df[["y_pred"]], average=None + df[["y_true"]], df[["y_pred"]], average=None # type: ignore ) expected_values = sklearn_metrics.precision_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None @@ -704,8 +716,9 @@ def test_precision_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) + # TODO(b/340872435): fix type error precision_score = metrics.precision_score( - df[["y_true"]], df[["y_pred"]], average=None + df[["y_true"]], df[["y_pred"]], average=None # type: ignore ) expected_values = sklearn_metrics.precision_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None @@ -725,7 +738,8 @@ def test_precision_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - precision_score = metrics.precision_score(df["y_true"], df["y_pred"], average=None) + # TODO(b/340872435): fix type error + precision_score = metrics.precision_score(df["y_true"], df["y_pred"], average=None) # type: ignore expected_values = [0.666667, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_precision = pd.Series(expected_values, index=expected_index) @@ -743,8 +757,9 @@ def test_f1_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) + # TODO(b/340872435): fix type error f1_score = metrics.f1_score( - df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None # type: ignore ) expected_values = [0.8, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -761,7 +776,8 @@ def test_f1_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) + # TODO(b/340872435): fix type error + f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore expected_values = sklearn_metrics.f1_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -778,7 +794,8 @@ def test_f1_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) + # TODO(b/340872435): fix type error + f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore expected_values = sklearn_metrics.f1_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -795,7 +812,8 @@ def test_f1_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - f1_score = metrics.f1_score(df["y_true"], df["y_pred"], average=None) + # TODO(b/340872435): fix type error + f1_score = metrics.f1_score(df["y_true"], df["y_pred"], average=None) # type: ignore expected_values = [0.8, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_f1 = pd.Series(expected_values, index=expected_index) diff --git a/tests/system/small/ml/test_model_selection.py b/tests/system/small/ml/test_model_selection.py index 9eb36455913..ca14186a4d1 100644 --- a/tests/system/small/ml/test_model_selection.py +++ b/tests/system/small/ml/test_model_selection.py @@ -130,12 +130,17 @@ def test_train_test_split_seeded_correct_rows( X, y, random_state=42 ) - X_train = X_train.to_pandas().sort_index() - X_test = X_test.to_pandas().sort_index() - y_train = y_train.to_pandas().sort_index() - y_test = y_test.to_pandas().sort_index() - - train_index = pd.Index( + # TODO(b/340876926): fix type error + X_train = X_train.to_pandas().sort_index() # type: ignore + # TODO(b/340876926): fix type error + X_test = X_test.to_pandas().sort_index() # type: ignore + # TODO(b/340876926): fix type error + y_train = y_train.to_pandas().sort_index() # type: ignore + # TODO(b/340876926): fix type error + y_test = y_test.to_pandas().sort_index() # type: ignore + + # TODO(b/340876926): fix type error + train_index = pd.Index( # type: ignore [ 144, 146, @@ -162,13 +167,15 @@ def test_train_test_split_seeded_correct_rows( dtype="Int64", name="rowindex", ) - test_index = pd.Index( + # TODO(b/340876926): fix type error + test_index = pd.Index( # type: ignore [148, 161, 226, 269, 278, 289, 291], dtype="Int64", name="rowindex" ) all_data.index.name = "_" + # TODO(b/340876926): fix type error pd.testing.assert_frame_equal( - X_train, + X_train, # type: ignore all_data[ [ "species", @@ -177,8 +184,9 @@ def test_train_test_split_seeded_correct_rows( ] ].loc[train_index], ) + # TODO(b/340876926): fix type error pd.testing.assert_frame_equal( - X_test, + X_test, # type: ignore all_data[ [ "species", @@ -187,16 +195,18 @@ def test_train_test_split_seeded_correct_rows( ] ].loc[test_index], ) + # TODO(b/340876926): fix type error pd.testing.assert_frame_equal( - y_train, + y_train, # type: ignore all_data[ [ "body_mass_g", ] ].loc[train_index], ) + # TODO(b/340876926): fix type error pd.testing.assert_frame_equal( - y_test, + y_test, # type: ignore all_data[ [ "body_mass_g", diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 2824e86979a..838bc111085 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -310,7 +310,8 @@ def test_dt_floor(scalars_dfs, col_name, freq): def test_dt_compare_coerce_str_datetime(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df["datetime_col"] - bf_result = (bf_series >= "2024-01-01").to_pandas() + # TODO(b/340878286): fix type error + bf_result = (bf_series >= "2024-01-01").to_pandas() # type: ignore pd_result = scalars_pandas_df["datetime_col"] >= pd.to_datetime("2024-01-01") diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index faf7cb7e6bf..e0ef84641ce 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -258,8 +258,9 @@ def test_scatter_args_s(s): ax = df.plot.scatter(x="a", y="b", s="s") pd_ax = pd_df.plot.scatter(x="a", y="b", s="s") + # TODO(b/340891723): fix type error tm.assert_numpy_array_equal( - ax.collections[0].get_sizes(), pd_ax.collections[0].get_sizes() + ax.collections[0].get_sizes(), pd_ax.collections[0].get_sizes() # type: ignore ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 5ed6908640d..4b50922c0d1 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -147,7 +147,8 @@ def test_df_construct_inline_respects_location(): df = bpd.DataFrame([[1, 2, 3], [4, 5, 6]]) repr(df) - table = bpd.get_global_session().bqclient.get_table(df.query_job.destination) + # TODO(b/340876936): fix type error + table = bpd.get_global_session().bqclient.get_table(df.query_job.destination) # type: ignore assert table.location == "europe-west1" @@ -752,8 +753,10 @@ def test_assign_listlike_to_empty_df(session): def test_assign_to_empty_df_multiindex_error(session): empty_df = dataframe.DataFrame(session=session) empty_pandas_df = pd.DataFrame() - empty_df["empty_col_1"] = [] - empty_df["empty_col_2"] = [] + # TODO(b/340876936): fix type error + empty_df["empty_col_1"] = [] # type: ignore + # TODO(b/340876936): fix type error + empty_df["empty_col_2"] = [] # type: ignore empty_pandas_df["empty_col_1"] = [] empty_pandas_df["empty_col_2"] = [] empty_df = empty_df.set_index(["empty_col_1", "empty_col_2"]) @@ -1341,20 +1344,34 @@ def test_get_dtypes(scalars_df_default_index): dtypes, pd.Series( { - "bool_col": pd.BooleanDtype(), - "bytes_col": pd.ArrowDtype(pa.binary()), - "date_col": pd.ArrowDtype(pa.date32()), - "datetime_col": pd.ArrowDtype(pa.timestamp("us")), - "geography_col": gpd.array.GeometryDtype(), - "int64_col": pd.Int64Dtype(), - "int64_too": pd.Int64Dtype(), - "numeric_col": pd.ArrowDtype(pa.decimal128(38, 9)), - "float64_col": pd.Float64Dtype(), - "rowindex": pd.Int64Dtype(), - "rowindex_2": pd.Int64Dtype(), - "string_col": pd.StringDtype(storage="pyarrow"), - "time_col": pd.ArrowDtype(pa.time64("us")), - "timestamp_col": pd.ArrowDtype(pa.timestamp("us", tz="UTC")), + # TODO(b/340876936): fix type error + "bool_col": pd.BooleanDtype(), # type: ignore + # TODO(b/340876936): fix type error + "bytes_col": pd.ArrowDtype(pa.binary()), # type: ignore + # TODO(b/340876936): fix type error + "date_col": pd.ArrowDtype(pa.date32()), # type: ignore + # TODO(b/340876936): fix type error + "datetime_col": pd.ArrowDtype(pa.timestamp("us")), # type: ignore + # TODO(b/340876936): fix type error + "geography_col": gpd.array.GeometryDtype(), # type: ignore + # TODO(b/340876936): fix type error + "int64_col": pd.Int64Dtype(), # type: ignore + # TODO(b/340876936): fix type error + "int64_too": pd.Int64Dtype(), # type: ignore + # TODO(b/340876936): fix type error + "numeric_col": pd.ArrowDtype(pa.decimal128(38, 9)), # type: ignore + # TODO(b/340876936): fix type error + "float64_col": pd.Float64Dtype(), # type: ignore + # TODO(b/340876936): fix type error + "rowindex": pd.Int64Dtype(), # type: ignore + # TODO(b/340876936): fix type error + "rowindex_2": pd.Int64Dtype(), # type: ignore + # TODO(b/340876936): fix type error + "string_col": pd.StringDtype(storage="pyarrow"), # type: ignore + # TODO(b/340876936): fix type error + "time_col": pd.ArrowDtype(pa.time64("us")), # type: ignore + # TODO(b/340876936): fix type error + "timestamp_col": pd.ArrowDtype(pa.timestamp("us", tz="UTC")), # type: ignore } ), ) @@ -1784,8 +1801,11 @@ def test_combine( def test_df_update(overwrite, filter_func): if pd.__version__.startswith("1."): pytest.skip("dtype handled differently in pandas 1.x.") - index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") - index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") + + # TODO(b/340876936): fix type error + index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") # type: ignore + # TODO(b/340876936): fix type error + index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") # type: ignore pd_df1 = pandas.DataFrame( {"a": [1, None, 3, 4], "b": [5, 6, None, 8]}, dtype="Int64", index=index1 ) @@ -1845,8 +1865,10 @@ def test_df_idxmax(): ], ) def test_df_align(join, axis): - index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") - index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") + # TODO(b/340876936): fix type error + index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") # type: ignore + # TODO(b/340876936): fix type error + index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") # type: ignore pd_df1 = pandas.DataFrame( {"a": [1, None, 3, 4], "b": [5, 6, None, 8]}, dtype="Int64", index=index1 ) @@ -1863,8 +1885,10 @@ def test_df_align(join, axis): pd_result1, pd_result2 = pd_df1.align(pd_df2, join=join, axis=axis) # Don't check dtype as pandas does unnecessary float conversion - pd.testing.assert_frame_equal(bf_result1.to_pandas(), pd_result1, check_dtype=False) - pd.testing.assert_frame_equal(bf_result2.to_pandas(), pd_result2, check_dtype=False) + # TODO(b/340876936): fix type error + pd.testing.assert_frame_equal(bf_result1.to_pandas(), pd_result1, check_dtype=False) # type: ignore + # TODO(b/340876936): fix type error + pd.testing.assert_frame_equal(bf_result2.to_pandas(), pd_result2, check_dtype=False) # type: ignore def test_combine_first( @@ -2500,9 +2524,11 @@ def test_df_transpose(): # Include some floats to ensure type coercion values = [[0, 3.5, True], [1, 4.5, False], [2, 6.5, None]] # Test complex case of both axes being multi-indices with non-unique elements - columns = pd.Index(["A", "B", "A"], dtype=pd.StringDtype(storage="pyarrow")) + # TODO(b/340876936): fix type error + columns = pd.Index(["A", "B", "A"], dtype=pd.StringDtype(storage="pyarrow")) # type: ignore columns_multi = pd.MultiIndex.from_arrays([columns, columns], names=["c1", "c2"]) - index = pd.Index(["b", "a", "a"], dtype=pd.StringDtype(storage="pyarrow")) + # TODO(b/340876936): fix type error + index = pd.Index(["b", "a", "a"], dtype=pd.StringDtype(storage="pyarrow")) # type: ignore rows_multi = pd.MultiIndex.from_arrays([index, index], names=["r1", "r2"]) pd_df = pandas.DataFrame(values, index=rows_multi, columns=columns_multi) @@ -3670,8 +3696,10 @@ def test_df_setattr_index(): [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] ) bf_df = dataframe.DataFrame(pd_df) - pd_df.index = [4, 5] - bf_df.index = [4, 5] + # TODO(b/340876936): fix type error + pd_df.index = [4, 5] # type: ignore + # TODO(b/340876936): fix type error + bf_df.index = [4, 5] # type: ignore assert_pandas_df_equal( pd_df, bf_df.to_pandas(), check_index_type=False, check_dtype=False @@ -3683,8 +3711,10 @@ def test_df_setattr_columns(): [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] ) bf_df = dataframe.DataFrame(pd_df) - pd_df.columns = [4, 5, 6] - bf_df.columns = [4, 5, 6] + # TODO(b/340876936): fix type error + pd_df.columns = [4, 5, 6] # type: ignore + # TODO(b/340876936): fix type error + bf_df.columns = [4, 5, 6] # type: ignore assert_pandas_df_equal( pd_df, bf_df.to_pandas(), check_index_type=False, check_dtype=False @@ -3778,7 +3808,8 @@ def test_iloc_list_multiindex(scalars_dfs): def test_iloc_empty_list(scalars_df_index, scalars_pandas_df_index): - index_list = [] + # TODO(b/340876936): fix type error + index_list = [] # type: ignore bf_result = scalars_df_index.iloc[index_list] pd_result = scalars_pandas_df_index.iloc[index_list] diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index fcaca7a493b..088211d7fcb 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -242,11 +242,14 @@ def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): model.fit(X_train, y_train) assert model is not None - assert model._bqml_model.model.encryption_configuration is not None - assert model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek + # TODO(b/340879287): fix type error + assert model._bqml_model.model.encryption_configuration is not None # type: ignore + # TODO(b/340879287): fix type error + assert model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek # type: ignore # Assert that model exists in BQ with intended encryption - model_bq = session_with_bq_cmek.bqclient.get_model(model._bqml_model.model_name) + # TODO(b/340879287): fix type error + model_bq = session_with_bq_cmek.bqclient.get_model(model._bqml_model.model_name) # type: ignore assert model_bq.encryption_configuration.kms_key_name == bq_cmek # Explicitly save the model to a destination and assert that encryption holds @@ -257,10 +260,12 @@ def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): f"{model_ref.project}.{model_ref.dataset_id}.{model_ref.model_id}" ) new_model = model.to_gbq(model_ref_full_name) - assert new_model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek + # TODO(b/340879287): fix type error + assert new_model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek # type: ignore # Assert that model exists in BQ with intended encryption - model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) + # TODO(b/340879287): fix type error + model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) # type: ignore assert model_bq.encryption_configuration.kms_key_name == bq_cmek # Assert that model registration keeps the encryption @@ -274,9 +279,11 @@ def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): # https://cloud.google.com/vertex-ai/docs/general/cmek#create_resources_with_the_kms_key. # bigframes.ml does not provide any API for the model deployment. model_registered = new_model.register() + # TODO(b/340879287): fix type error assert ( - model_registered._bqml_model.model.encryption_configuration.kms_key_name + model_registered._bqml_model.model.encryption_configuration.kms_key_name # type: ignore == bq_cmek ) - model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) + # TODO(b/340879287): fix type error + model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) # type: ignore assert model_bq.encryption_configuration.kms_key_name == bq_cmek diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index c419dc49075..58fd346bc13 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -24,17 +24,20 @@ 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([3, 14, 159], dtype=pd.Int64Dtype(), name="my_index") + # TODO(b/340878489): fix type error + pd_result = pd.Index([3, 14, 159], dtype=pd.Int64Dtype(), name="my_index") # type: ignore pd.testing.assert_index_equal(bf_result, pd_result) def test_index_construct_from_series(): + # TODO(b/340878489): fix type error bf_result = bpd.Index( bpd.Series([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name"), name="index_name", dtype=pd.Int64Dtype(), - ).to_pandas() - pd_result = pd.Index( + ).to_pandas() # type: ignore + # TODO(b/340878489): fix type error + pd_result = pd.Index( # type: ignore pd.Series([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name"), name="index_name", dtype=pd.Int64Dtype(), @@ -46,11 +49,14 @@ def test_index_construct_from_index(): bf_index_input = bpd.Index( [3, 14, 159], dtype=pd.Float64Dtype(), name="series_name" ) + # TODO(b/340878489): fix type error bf_result = bpd.Index( - bf_index_input, dtype=pd.Int64Dtype(), name="index_name" + bf_index_input, dtype=pd.Int64Dtype(), name="index_name" # type: ignore ).to_pandas() - pd_index_input = pd.Index([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name") - pd_result = pd.Index(pd_index_input, dtype=pd.Int64Dtype(), name="index_name") + # TODO(b/340878489): fix type error + pd_index_input = pd.Index([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name") # type: ignore + # TODO(b/340878489): fix type error + pd_result = pd.Index(pd_index_input, dtype=pd.Int64Dtype(), name="index_name") # type: ignore pd.testing.assert_index_equal(bf_result, pd_result) @@ -365,7 +371,8 @@ def test_index_isin(scalars_df_index, scalars_pandas_df_index): pd_result_array = scalars_pandas_df_index.set_index("int64_col").index.isin( [2, 55555, 4] ) - pd.testing.assert_index_equal( + # TODO(b/340878489): fix type error + pd.testing.assert_index_equal( # type: ignore pd.Index(pd_result_array), bf_series, check_names=False, diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index 613ad945c11..de631ee20e1 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -850,12 +850,14 @@ def test_column_multi_index_stack(level): bf_result = bf_df.stack(level=level).to_pandas() # BigFrames emulates future_stack impl - pd_result = pd_df.stack(level=level, future_stack=True) + # TODO(b/340884387): fix type error + pd_result = pd_df.stack(level=level, future_stack=True) # type: ignore # Pandas produces NaN, where bq dataframes produces pd.NA # Column ordering seems to depend on pandas version + # TODO(b/340884387): fix type error pandas.testing.assert_frame_equal( - bf_result, pd_result, check_dtype=False, check_index_type=False + bf_result, pd_result, check_dtype=False, check_index_type=False # type: ignore ) @@ -889,9 +891,11 @@ def test_column_multi_index_melt(): def test_column_multi_index_unstack(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "int64_col", "rowindex_2"] - level1 = pandas.Index(["b", "a", "b"], dtype="string[pyarrow]") + # TODO(b/340884387): fix type error + level1 = pandas.Index(["b", "a", "b"], dtype="string[pyarrow]") # type: ignore # Need resulting column to be pyarrow string rather than object dtype - level2 = pandas.Index(["a", "b", "b"], dtype="string[pyarrow]") + # TODO(b/340884387): fix type error + level2 = pandas.Index(["a", "b", "b"], dtype="string[pyarrow]") # type: ignore multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) bf_df = scalars_df_index[columns].copy() bf_df.columns = multi_columns @@ -1185,9 +1189,10 @@ def test_explode_w_multi_index(): df = bpd.DataFrame(data, columns=multi_level_columns) pd_df = df.to_pandas() + # TODO(b/340884387): fix type error pandas.testing.assert_frame_equal( df["col0"].explode("col00").to_pandas(), - pd_df["col0"].explode("col00"), + pd_df["col0"].explode("col00"), # type: ignore check_dtype=False, check_index_type=False, ) @@ -1197,7 +1202,8 @@ def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index columns = ["int64_too", "int64_col", "rowindex_2"] level1 = pandas.Index(["b", "c", "d"]) # Need resulting column to be pyarrow string rather than object dtype - level2 = pandas.Index([None, "b", "b"], dtype="string[pyarrow]") + # TODO(b/340884387): fix type error + level2 = pandas.Index([None, "b", "b"], dtype="string[pyarrow]") # type: ignore multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) bf_df = scalars_df_index[columns].copy() bf_df.columns = multi_columns diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index 6eee01dd31d..256046f8b10 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -395,9 +395,11 @@ def test_cut(scalars_dfs): # make sure the result is a supported dtype assert bf_result.dtype == bpd.Int64Dtype() - bf_result = bf_result.to_pandas() + # TODO(b/340884971): fix type error + bf_result = bf_result.to_pandas() # type: ignore pd_result = pd_result.astype("Int64") - pd.testing.assert_series_equal(bf_result, pd_result) + # TODO(b/340884971): fix type error + pd.testing.assert_series_equal(bf_result, pd_result) # type: ignore def test_cut_default_labels(scalars_dfs): @@ -528,10 +530,12 @@ def test_qcut(scalars_dfs, q): ) bf_result = bpd.qcut(scalars_df["float64_col"], q, labels=False, duplicates="drop") - bf_result = bf_result.to_pandas() + # TODO(b/340884971): fix type error + bf_result = bf_result.to_pandas() # type: ignore pd_result = pd_result.astype("Int64") - pd.testing.assert_series_equal(bf_result, pd_result) + # TODO(b/340884971): fix type error + pd.testing.assert_series_equal(bf_result, pd_result) # type: ignore @pytest.mark.parametrize( @@ -568,8 +572,9 @@ def test_to_datetime_scalar(arg, utc, unit, format): ], ) def test_to_datetime_iterable(arg, utc, unit, format): + # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc, unit=unit, format=format) + bpd.to_datetime(arg, utc=utc, unit=unit, format=format) # type: ignore .to_pandas() .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") ) @@ -584,8 +589,9 @@ def test_to_datetime_iterable(arg, utc, unit, format): def test_to_datetime_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col = "int64_too" + # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(scalars_df[col], unit="s").to_pandas().astype("datetime64[s]") + bpd.to_datetime(scalars_df[col], unit="s").to_pandas().astype("datetime64[s]") # type: ignore ) pd_result = pd.Series(pd.to_datetime(scalars_pandas_df[col], unit="s")) pd.testing.assert_series_equal( @@ -608,7 +614,8 @@ def test_to_datetime_series(scalars_dfs): ], ) def test_to_datetime_unit_param(arg, unit): - bf_result = bpd.to_datetime(arg, unit=unit).to_pandas().astype("datetime64[ns]") + # TODO(b/340884971): fix type error + bf_result = bpd.to_datetime(arg, unit=unit).to_pandas().astype("datetime64[ns]") # type: ignore pd_result = pd.Series(pd.to_datetime(arg, unit=unit)).dt.floor("us") pd.testing.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False @@ -625,8 +632,9 @@ def test_to_datetime_unit_param(arg, unit): ], ) def test_to_datetime_format_param(arg, utc, format): + # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc, format=format) + bpd.to_datetime(arg, utc=utc, format=format) # type: ignore .to_pandas() .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") ) @@ -678,8 +686,9 @@ def test_to_datetime_format_param(arg, utc, format): ], ) def test_to_datetime_string_inputs(arg, utc, output_in_utc, format): + # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc, format=format) + bpd.to_datetime(arg, utc=utc, format=format) # type: ignore .to_pandas() .astype("datetime64[ns, UTC]" if output_in_utc else "datetime64[ns]") ) @@ -721,8 +730,9 @@ def test_to_datetime_string_inputs(arg, utc, output_in_utc, format): ], ) def test_to_datetime_timestamp_inputs(arg, utc, output_in_utc): + # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc) + bpd.to_datetime(arg, utc=utc) # type: ignore .to_pandas() .astype("datetime64[ns, UTC]" if output_in_utc else "datetime64[ns]") ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 9c60c821a7d..4a39e75ff95 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -623,7 +623,8 @@ def test_read_gbq_function_reads_udfs(bigquery_client, dataset_id): indirect_df = bigframes.dataframe.DataFrame(src) indirect_df = indirect_df.assign(y=indirect_df.x.apply(square)) - indirect_df = indirect_df.to_pandas() + # TODO(b/340875260): fix type error + indirect_df = indirect_df.to_pandas() # type: ignore assert_pandas_df_equal( direct_df, indirect_df, ignore_order=True, check_index_type=False diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index fa514784c01..2df7ab86b69 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -49,7 +49,8 @@ def test_series_construct_nullable_ints(): [1, 3, bigframes.pandas.NA], index=[0, 4, bigframes.pandas.NA] ).to_pandas() - expected_index = pd.Index( + # TODO(b/340885567): fix type error + expected_index = pd.Index( # type: ignore [0, 4, None], dtype=pd.Int64Dtype(), ) @@ -2957,7 +2958,8 @@ def test_string_astype_date(): bf_series = series.Series(pd_series) - pd_result = pd_series.astype("date32[day][pyarrow]") + # TODO(b/340885567): fix type error + pd_result = pd_series.astype("date32[day][pyarrow]") # type: ignore bf_result = bf_series.astype("date32[day][pyarrow]").to_pandas() pd.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) @@ -3661,9 +3663,10 @@ def test_series_explode_w_index(index, ignore_index): data = [[], [200.0, 23.12], [4.5, -9.0], [1.0]] s = bigframes.pandas.Series(data, index=index) pd_s = pd.Series(data, index=index) + # TODO(b/340885567): fix type error pd.testing.assert_series_equal( - s.explode(ignore_index=ignore_index).to_pandas(), - pd_s.explode(ignore_index=ignore_index).astype(pd.Float64Dtype()), + s.explode(ignore_index=ignore_index).to_pandas(), # type: ignore + pd_s.explode(ignore_index=ignore_index).astype(pd.Float64Dtype()), # type: ignore check_index_type=False, ) @@ -3681,8 +3684,10 @@ def test_series_explode_reserve_order(ignore_index, ordered): s = bigframes.pandas.Series(data) pd_s = pd.Series(data) - res = s.explode(ignore_index=ignore_index).to_pandas(ordered=ordered) - pd_res = pd_s.explode(ignore_index=ignore_index).astype(pd.Int64Dtype()) + # TODO(b/340885567): fix type error + res = s.explode(ignore_index=ignore_index).to_pandas(ordered=ordered) # type: ignore + # TODO(b/340885567): fix type error + pd_res = pd_s.explode(ignore_index=ignore_index).astype(pd.Int64Dtype()) # type: ignore pd.testing.assert_series_equal( res if ordered else res.sort_index(), pd_res, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 5daa01ad386..052ef273841 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -490,7 +490,8 @@ def test_read_pandas(session, scalars_dfs): def test_read_pandas_series(session): - idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) + # TODO(b/340887657): fix type error + idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) # type: ignore pd_series = pd.Series([3, 1, 4, 1, 5], dtype=pd.Int64Dtype(), index=idx) bf_series = session.read_pandas(pd_series) @@ -498,7 +499,8 @@ def test_read_pandas_series(session): def test_read_pandas_index(session): - pd_idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) + # TODO(b/340887657): fix type error + pd_idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) # type: ignore bf_idx = session.read_pandas(pd_idx) pd.testing.assert_index_equal(bf_idx.to_pandas(), pd_idx) @@ -516,7 +518,8 @@ def test_read_pandas_inline_respects_location(): df = session.read_pandas(pd.DataFrame([[1, 2, 3], [4, 5, 6]])) repr(df) - table = session.bqclient.get_table(df.query_job.destination) + # TODO(b/340887657): fix type error + table = session.bqclient.get_table(df.query_job.destination) # type: ignore assert table.location == "europe-west1" diff --git a/tests/unit/ml/test_api_primitives.py b/tests/unit/ml/test_api_primitives.py index da77a180a8d..a5b466cf951 100644 --- a/tests/unit/ml/test_api_primitives.py +++ b/tests/unit/ml/test_api_primitives.py @@ -30,7 +30,8 @@ def test_base_estimator_repr(): estimator = bigframes.ml.linear_model.LinearRegression(fit_intercept=True) assert estimator.__repr__() == "LinearRegression()" - estimator = bigframes.ml.decomposition.PCA(n_components=7) + # TODO(b/340891292): fix type error + estimator = bigframes.ml.decomposition.PCA(n_components=7) # type: ignore assert estimator.__repr__() == "PCA(n_components=7)" @@ -48,6 +49,7 @@ def test_base_estimator_repr_matches_sklearn(): sklearn_estimator = sklearn_linear_model.LinearRegression(fit_intercept=True) assert estimator.__repr__() == sklearn_estimator.__repr__() - estimator = bigframes.ml.decomposition.PCA(n_components=7) + # TODO(b/340891292): fix type error + estimator = bigframes.ml.decomposition.PCA(n_components=7) # type: ignore sklearn_estimator = sklearn_decomposition.PCA(n_components=7) assert estimator.__repr__() == sklearn_estimator.__repr__() diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 535b748345e..70639315bed 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -95,7 +95,8 @@ def test_cut_raises_with_labels(): match="The 'labels' parameter must be either False or None.", ): mock_series = mock.create_autospec(bigframes.pandas.Series, instance=True) - bigframes.pandas.cut(mock_series, 4, labels=["a", "b", "c", "d"]) + # TODO(b/340893280): fix type error + bigframes.pandas.cut(mock_series, 4, labels=["a", "b", "c", "d"]) # type: ignore @pytest.mark.parametrize( From a58dcd21b7edc2b46b8892d220129634e2011c86 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 15 May 2024 17:56:52 -0700 Subject: [PATCH 574/921] test: stop checking ml large tests exact numbers (#690) * test: stop checking ml large tests exact numbers * clean up --- tests/system/large/ml/test_cluster.py | 26 +-- tests/system/large/ml/test_compose.py | 85 +++---- tests/system/large/ml/test_core.py | 117 ++++------ tests/system/large/ml/test_decomposition.py | 72 ++---- tests/system/large/ml/test_ensemble.py | 113 ++-------- tests/system/large/ml/test_forecasting.py | 40 ++-- tests/system/large/ml/test_linear_model.py | 59 +---- tests/system/large/ml/test_pipeline.py | 238 +++++--------------- tests/system/utils.py | 46 +++- 9 files changed, 256 insertions(+), 540 deletions(-) diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py index b65b6e5083f..454c94599d6 100644 --- a/tests/system/large/ml/test_cluster.py +++ b/tests/system/large/ml/test_cluster.py @@ -13,13 +13,11 @@ # limitations under the License. import pandas as pd -import pytest from bigframes.ml import cluster -from tests.system.utils import assert_pandas_df_equal +from tests.system import utils -@pytest.mark.flaky(retries=2) def test_cluster_configure_fit_score_predict( session, penguins_df_default_index, dataset_id ): @@ -88,26 +86,18 @@ def test_cluster_configure_fit_score_predict( # Check score to ensure the model was fitted score_result = model.score(new_penguins).to_pandas() - score_expected = pd.DataFrame( - {"davies_bouldin_index": [1.502182], "mean_squared_distance": [1.953408]}, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 - ) + eval_metrics = ["davies_bouldin_index", "mean_squared_distance"] + utils.check_pandas_df_schema_and_index(score_result, columns=eval_metrics, index=1) predictions = model.predict(new_penguins).to_pandas() assert predictions.shape == (4, 9) - result = predictions[["CENTROID_ID"]] - expected = pd.DataFrame( - {"CENTROID_ID": [2, 3, 1, 2]}, - dtype="Int64", - index=pd.Index(["test1", "test2", "test3", "test4"], dtype="string[pyarrow]"), + utils.check_pandas_df_schema_and_index( + predictions, + columns=["CENTROID_ID"], + index=["test1", "test2", "test3", "test4"], + col_exact=False, ) - expected.index.name = "observation" - assert_pandas_df_equal(result, expected, ignore_order=True) # save, load, check n_clusters to ensure configuration was kept reloaded_model = model.to_gbq( diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 38c5014c19b..45322e78dd2 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas - from bigframes.ml import compose, preprocessing +from tests.system import utils def test_columntransformer_standalone_fit_and_transform( @@ -45,26 +44,18 @@ def test_columntransformer_standalone_fit_and_transform( ) result = transformer.transform(new_penguins_df).to_pandas() - expected = pandas.DataFrame( - { - "onehotencoded_species": [ - [{"index": 1, "value": 1.0}], - [{"index": 1, "value": 1.0}], - [{"index": 2, "value": 1.0}], - ], - "standard_scaled_culmen_length_mm": [ - -0.811119671289163, - -0.9945520581113803, - -1.104611490204711, - ], - "min_max_scaled_culmen_length_mm": [0.269, 0.232, 0.210], - "standard_scaled_flipper_length_mm": [-0.350044, -1.418336, -0.9198], - }, - index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), + utils.check_pandas_df_schema_and_index( + result, + columns=[ + "onehotencoded_species", + "standard_scaled_culmen_length_mm", + "min_max_scaled_culmen_length_mm", + "standard_scaled_flipper_length_mm", + ], + index=[1633, 1672, 1690], + col_exact=False, ) - pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) - def test_columntransformer_standalone_fit_transform(new_penguins_df): transformer = compose.ColumnTransformer( @@ -86,25 +77,17 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] ).to_pandas() - expected = pandas.DataFrame( - { - "onehotencoded_species": [ - [{"index": 1, "value": 1.0}], - [{"index": 1, "value": 1.0}], - [{"index": 2, "value": 1.0}], - ], - "standard_scaled_culmen_length_mm": [ - 1.313249, - -0.20198, - -1.111118, - ], - "standard_scaled_flipper_length_mm": [1.251098, -1.196588, -0.054338], - }, - index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), + utils.check_pandas_df_schema_and_index( + result, + columns=[ + "onehotencoded_species", + "standard_scaled_culmen_length_mm", + "standard_scaled_flipper_length_mm", + ], + index=[1633, 1672, 1690], + col_exact=False, ) - pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) - def test_columntransformer_save_load(new_penguins_df, dataset_id): transformer = compose.ColumnTransformer( @@ -147,23 +130,13 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] ).to_pandas() - # TODO(b/340888429): fix type error - expected = pandas.DataFrame( # type: ignore - { - "onehotencoded_species": [ - [{"index": 1, "value": 1.0}], - [{"index": 1, "value": 1.0}], - [{"index": 2, "value": 1.0}], - ], - "standard_scaled_culmen_length_mm": [ - 1.313249, - -0.20198, - -1.111118, - ], - "standard_scaled_flipper_length_mm": [1.251098, -1.196588, -0.054338], - }, - index=pandas.Index([1633, 1672, 1690], dtype="Int64", name="tag_number"), + utils.check_pandas_df_schema_and_index( + result, + columns=[ + "onehotencoded_species", + "standard_scaled_culmen_length_mm", + "standard_scaled_flipper_length_mm", + ], + index=[1633, 1672, 1690], + col_exact=False, ) - - # TODO(b/340888429): fix type error - pandas.testing.assert_frame_equal(result, expected, rtol=0.1, check_dtype=False) # type: ignore diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py index aec1065e41e..be5eea925fd 100644 --- a/tests/system/large/ml/test_core.py +++ b/tests/system/large/ml/test_core.py @@ -12,14 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas -import pytest - from bigframes.ml import globals +from tests.system import utils -# TODO(garrettwu): Re-enable or not check exact numbers. -@pytest.mark.skip(reason="bqml regression") def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_df): df = penguins_df_default_index.dropna() X_train = df[ @@ -38,41 +34,33 @@ def test_bqml_e2e(session, dataset_id, penguins_df_default_index, new_penguins_d X_train, y_train, options={"model_type": "linear_reg"} ) + eval_metrics = [ + "mean_absolute_error", + "mean_squared_error", + "mean_squared_log_error", + "median_absolute_error", + "r2_score", + "explained_variance", + ] # no data - report evaluation from the automatic data split evaluate_result = model.evaluate().to_pandas() - evaluate_expected = pandas.DataFrame( - { - "mean_absolute_error": [225.817334], - "mean_squared_error": [80540.705944], - "mean_squared_log_error": [0.004972], - "median_absolute_error": [173.080816], - "r2_score": [0.87529], - "explained_variance": [0.87529], - }, - dtype="Float64", - ) - evaluate_expected = evaluate_expected.reindex( - index=evaluate_expected.index.astype("Int64") - ) - pandas.testing.assert_frame_equal( - evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + evaluate_result, columns=eval_metrics, index=1 ) # evaluate on all training data evaluate_result = model.evaluate(df).to_pandas() - pandas.testing.assert_frame_equal( - evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + evaluate_result, columns=eval_metrics, index=1 ) # predict new labels predictions = model.predict(new_penguins_df).to_pandas() - expected = pandas.DataFrame( - {"predicted_body_mass_g": [4030.1, 3280.8, 3177.9]}, - dtype="Float64", - index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pandas.testing.assert_frame_equal( - predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_body_mass_g"], + index=[1633, 1672, 1690], + col_exact=False, ) new_name = f"{dataset_id}.my_model" @@ -108,42 +96,34 @@ def test_bqml_manual_preprocessing_e2e( X_train, y_train, transforms=transforms, options=options ) + eval_metrics = [ + "mean_absolute_error", + "mean_squared_error", + "mean_squared_log_error", + "median_absolute_error", + "r2_score", + "explained_variance", + ] + # no data - report evaluation from the automatic data split evaluate_result = model.evaluate().to_pandas() - evaluate_expected = pandas.DataFrame( - { - "mean_absolute_error": [309.477334], - "mean_squared_error": [152184.227218], - "mean_squared_log_error": [0.009524], - "median_absolute_error": [257.727777], - "r2_score": [0.764356], - "explained_variance": [0.764356], - }, - dtype="Float64", - ) - evaluate_expected = evaluate_expected.reindex( - index=evaluate_expected.index.astype("Int64") - ) - - pandas.testing.assert_frame_equal( - evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + evaluate_result, columns=eval_metrics, index=1 ) # evaluate on all training data evaluate_result = model.evaluate(df).to_pandas() - pandas.testing.assert_frame_equal( - evaluate_result, evaluate_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + evaluate_result, columns=eval_metrics, index=1 ) # predict new labels predictions = model.predict(new_penguins_df).to_pandas() - expected = pandas.DataFrame( - {"predicted_body_mass_g": [3968.8, 3176.3, 3545.2]}, - dtype="Float64", - index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pandas.testing.assert_frame_equal( - predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_body_mass_g"], + index=[1633, 1672, 1690], + col_exact=False, ) new_name = f"{dataset_id}.my_model" @@ -168,24 +148,9 @@ def test_bqml_standalone_transform(penguins_df_default_index, new_penguins_df): ) transformed = model.transform(new_penguins_df).to_pandas() - expected = pandas.DataFrame( - { - "scaled_culmen_length_mm": [-0.8099, -0.9931, -1.103], - "onehotencoded_species": [ - [{"index": 1, "value": 1.0}], - [{"index": 1, "value": 1.0}], - [{"index": 2, "value": 1.0}], - ], - }, - index=pandas.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - expected["scaled_culmen_length_mm"] = expected["scaled_culmen_length_mm"].astype( - "Float64" - ) - pandas.testing.assert_frame_equal( - transformed[["scaled_culmen_length_mm", "onehotencoded_species"]], - expected, - check_exact=False, - rtol=0.1, - check_dtype=False, + utils.check_pandas_df_schema_and_index( + transformed, + columns=["scaled_culmen_length_mm", "onehotencoded_species"], + index=[1633, 1672, 1690], + col_exact=False, ) diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index 9dab85023a6..4312c241257 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -15,7 +15,7 @@ import pandas as pd from bigframes.ml import decomposition -import tests.system.utils +from tests.system import utils def test_decomposition_configure_fit_score_predict( @@ -45,34 +45,19 @@ def test_decomposition_configure_fit_score_predict( # Check score to ensure the model was fitted score_result = model.score(new_penguins).to_pandas() - score_expected = pd.DataFrame( - { - "total_explained_variance_ratio": [0.812383], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=["total_explained_variance_ratio"], index=1 ) result = model.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - { - "principal_component_1": [-1.459, 2.258, -1.685], - "principal_component_2": [-1.120, -1.351, -0.874], - "principal_component_3": [-0.646, 0.443, -0.704], - }, - dtype="Float64", - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - - tests.system.utils.assert_pandas_df_equal_pca( + utils.check_pandas_df_schema_and_index( result, - expected, - check_exact=False, - rtol=0.1, + columns=[ + "principal_component_1", + "principal_component_2", + "principal_component_3", + ], + index=[1633, 1672, 1690], ) # save, load, check n_components to ensure configuration was kept @@ -114,36 +99,21 @@ def test_decomposition_configure_fit_score_predict_params( # Check score to ensure the model was fitted score_result = model.score(new_penguins).to_pandas() - score_expected = pd.DataFrame( - { - "total_explained_variance_ratio": [0.932897], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=["total_explained_variance_ratio"], index=1 ) result = model.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - { - "principal_component_1": [-1.459, 2.258, -1.685], - "principal_component_2": [-1.120, -1.351, -0.874], - "principal_component_3": [-0.646, 0.443, -0.704], - "principal_component_4": [-0.539, 0.234, -0.571], - "principal_component_5": [-0.876, 0.122, 0.609], - }, - dtype="Float64", - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - - tests.system.utils.assert_pandas_df_equal_pca( + utils.check_pandas_df_schema_and_index( result, - expected, - check_exact=False, - rtol=0.1, + columns=[ + "principal_component_1", + "principal_component_2", + "principal_component_3", + "principal_component_4", + "principal_component_5", + ], + index=[1633, 1672, 1690], ) # save, load, check n_components to ensure configuration was kept diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index b9aae219563..b71d0233027 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -12,16 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest import TestCase - -import pandas import pytest import bigframes.ml.ensemble +from tests.system import utils -# TODO(garrettwu): Re-enable or not check exact numbers. -@pytest.mark.skip(reason="bqml regression") @pytest.mark.flaky(retries=2) def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): model = bigframes.ml.ensemble.XGBRegressor() @@ -42,19 +38,9 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - expected = pandas.DataFrame( - { - "mean_absolute_error": [97.368139], - "mean_squared_error": [16284.877027], - "mean_squared_log_error": [0.0010189], - "median_absolute_error": [72.158691], - "r2_score": [0.974784], - "explained_variance": [0.974845], - }, - dtype="Float64", + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_REGRESSION_METRICS, index=1 ) - expected = expected.reindex(index=expected.index.astype("Int64")) - pandas.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -104,16 +90,9 @@ def test_xgbregressor_dart_booster_multiple_params( # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - TestCase().assertSequenceEqual(result.shape, (1, 6)) - for col_name in [ - "mean_absolute_error", - "mean_squared_error", - "mean_squared_log_error", - "median_absolute_error", - "r2_score", - "explained_variance", - ]: - assert col_name in result.columns + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_REGRESSION_METRICS, index=1 + ) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -161,16 +140,9 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - TestCase().assertSequenceEqual(result.shape, (1, 6)) - for col_name in [ - "precision", - "recall", - "accuracy", - "f1_score", - "log_loss", - "roc_auc", - ]: - assert col_name in result.columns + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_CLASSFICATION_METRICS, index=1 + ) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -183,7 +155,7 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): ) -@pytest.mark.flaky(retries=2) +# @pytest.mark.flaky(retries=2) def test_xgbclassifier_dart_booster_multiple_params( penguins_df_default_index, dataset_id ): @@ -219,16 +191,9 @@ def test_xgbclassifier_dart_booster_multiple_params( # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - TestCase().assertSequenceEqual(result.shape, (1, 6)) - for col_name in [ - "precision", - "recall", - "accuracy", - "f1_score", - "log_loss", - "roc_auc", - ]: - assert col_name in result.columns + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_CLASSFICATION_METRICS, index=1 + ) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -277,16 +242,9 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - TestCase().assertSequenceEqual(result.shape, (1, 6)) - for col_name in [ - "mean_absolute_error", - "mean_squared_error", - "mean_squared_log_error", - "median_absolute_error", - "r2_score", - "explained_variance", - ]: - assert col_name in result.columns + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_REGRESSION_METRICS, index=1 + ) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -331,16 +289,9 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - TestCase().assertSequenceEqual(result.shape, (1, 6)) - for col_name in [ - "mean_absolute_error", - "mean_squared_error", - "mean_squared_log_error", - "median_absolute_error", - "r2_score", - "explained_variance", - ]: - assert col_name in result.columns + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_REGRESSION_METRICS, index=1 + ) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -385,16 +336,9 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - TestCase().assertSequenceEqual(result.shape, (1, 6)) - for col_name in [ - "precision", - "recall", - "accuracy", - "f1_score", - "log_loss", - "roc_auc", - ]: - assert col_name in result.columns + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_CLASSFICATION_METRICS, index=1 + ) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -439,16 +383,9 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - TestCase().assertSequenceEqual(result.shape, (1, 6)) - for col_name in [ - "precision", - "recall", - "accuracy", - "f1_score", - "log_loss", - "roc_auc", - ]: - assert col_name in result.columns + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_CLASSFICATION_METRICS, index=1 + ) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index 1c0924245ed..be7cf8c93db 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -12,15 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas as pd import pytest from bigframes.ml import forecasting +from tests.system import utils ARIMA_EVALUATE_OUTPUT_COL = [ "non_seasonal_p", "non_seasonal_d", "non_seasonal_q", + "has_drift", "log_likelihood", "AIC", "variance", @@ -50,18 +51,17 @@ def test_arima_plus_model_fit_score( result = arima_model.score( new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] ).to_pandas() - expected = pd.DataFrame( - { - "mean_absolute_error": [154.742547], - "mean_squared_error": [26844.868855], - "root_mean_squared_error": [163.844038], - "mean_absolute_percentage_error": [6.189702], - "symmetric_mean_absolute_percentage_error": [6.097155], - }, - dtype="Float64", + utils.check_pandas_df_schema_and_index( + result, + columns=[ + "mean_absolute_error", + "mean_squared_error", + "root_mean_squared_error", + "mean_absolute_percentage_error", + "symmetric_mean_absolute_percentage_error", + ], + index=1, ) - expected = expected.reindex(index=expected.index.astype("Int64")) - pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load to ensure configuration was kept reloaded_model = arima_model.to_gbq( @@ -73,10 +73,10 @@ def test_arima_plus_model_fit_score( def test_arima_plus_model_fit_summary(dataset_id, arima_model): - - result = arima_model.summary() - assert result.shape == (1, 12) - assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) + result = arima_model.summary().to_pandas() + utils.check_pandas_df_schema_and_index( + result, columns=ARIMA_EVALUATE_OUTPUT_COL, index=1 + ) # save, load to ensure configuration was kept reloaded_model = arima_model.to_gbq( @@ -88,13 +88,13 @@ def test_arima_plus_model_fit_summary(dataset_id, arima_model): def test_arima_coefficients(arima_model): - got = arima_model.coef_ - expected_columns = { + result = arima_model.coef_.to_pandas() + expected_columns = [ "ar_coefficients", "ma_coefficients", "intercept_or_drift", - } - assert set(got.columns) == expected_columns + ] + utils.check_pandas_df_schema_and_index(result, columns=expected_columns, index=1) def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 50e3de3fc45..f1f79852785 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas as pd - import bigframes.ml.linear_model +from tests.system import utils def test_linear_regression_configure_fit_score(penguins_df_default_index, dataset_id): @@ -36,19 +35,9 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - expected = pd.DataFrame( - { - "mean_absolute_error": [225.735767], - "mean_squared_error": [80417.461828], - "mean_squared_log_error": [0.004967], - "median_absolute_error": [172.543702], - "r2_score": [0.87548], - "explained_variance": [0.87548], - }, - dtype="Float64", + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_REGRESSION_METRICS, index=1 ) - expected = expected.reindex(index=expected.index.astype("Int64")) - pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) @@ -99,19 +88,9 @@ def test_linear_regression_customized_params_fit_score( # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - expected = pd.DataFrame( - { - "mean_absolute_error": [240], - "mean_squared_error": [91197], - "mean_squared_log_error": [0.00573], - "median_absolute_error": [197], - "r2_score": [0.858], - "explained_variance": [0.8588], - }, - dtype="Float64", + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_REGRESSION_METRICS, index=1 ) - expected = expected.reindex(index=expected.index.astype("Int64")) - pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) @@ -154,19 +133,9 @@ def test_logistic_regression_configure_fit_score(penguins_df_default_index, data # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - expected = pd.DataFrame( - { - "precision": [0.616753], - "recall": [0.618615], - "accuracy": [0.92515], - "f1_score": [0.617681], - "log_loss": [1.498832], - "roc_auc": [0.975807], - }, - dtype="Float64", + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_CLASSFICATION_METRICS, index=1 ) - expected = expected.reindex(index=expected.index.astype("Int64")) - pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( @@ -210,19 +179,9 @@ def test_logistic_regression_customized_params_fit_score( # Check score to ensure the model was fitted result = model.score(X_train, y_train).to_pandas() - expected = pd.DataFrame( - { - "precision": [0.487], - "recall": [0.602], - "accuracy": [0.464], - "f1_score": [0.379], - "log_loss": [0.972], - "roc_auc": [0.700], - }, - dtype="Float64", + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_CLASSFICATION_METRICS, index=1 ) - expected = expected.reindex(index=expected.index.astype("Int64")) - pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq( diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 1a92d0f7d42..3d7eb2e426b 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -24,7 +24,7 @@ pipeline, preprocessing, ) -from tests.system.utils import assert_pandas_df_equal, assert_pandas_df_equal_pca +from tests.system import utils def test_pipeline_linear_regression_fit_score_predict( @@ -51,21 +51,8 @@ def test_pipeline_linear_regression_fit_score_predict( # Check score to ensure the model was fitted score_result = pl.score(X_train, y_train).to_pandas() - score_expected = pd.DataFrame( - { - "mean_absolute_error": [309.477331], - "mean_squared_error": [152184.227219], - "mean_squared_log_error": [0.009524], - "median_absolute_error": [257.728263], - "r2_score": [0.764356], - "explained_variance": [0.764356], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=utils.ML_REGRESSION_METRICS, index=1 ) # predict new labels @@ -87,13 +74,11 @@ def test_pipeline_linear_regression_fit_score_predict( ).set_index("tag_number") ) predictions = pl.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - {"predicted_body_mass_g": [3968.8, 3176.3, 3545.2]}, - dtype="Float64", - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pd.testing.assert_frame_equal( - predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_body_mass_g"], + index=[1633, 1672, 1690], + col_exact=False, ) @@ -115,21 +100,8 @@ def test_pipeline_linear_regression_series_fit_score_predict( # Check score to ensure the model was fitted score_result = pl.score(X_train, y_train).to_pandas() - score_expected = pd.DataFrame( - { - "mean_absolute_error": [528.495599], - "mean_squared_error": [421722.261808], - "mean_squared_log_error": [0.022963], - "median_absolute_error": [468.895249], - "r2_score": [0.346999], - "explained_variance": [0.346999], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=utils.ML_REGRESSION_METRICS, index=1 ) # predict new labels @@ -142,13 +114,11 @@ def test_pipeline_linear_regression_series_fit_score_predict( ).set_index("tag_number") ) predictions = pl.predict(new_penguins["culmen_length_mm"]).to_pandas() - expected = pd.DataFrame( - {"predicted_body_mass_g": [3818.845703, 3732.022253, 3679.928123]}, - dtype="Float64", - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pd.testing.assert_frame_equal( - predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_body_mass_g"], + index=[1633, 1672, 1690], + col_exact=False, ) @@ -176,21 +146,8 @@ def test_pipeline_logistic_regression_fit_score_predict( # Check score to ensure the model was fitted score_result = pl.score(X_train, y_train).to_pandas() - score_expected = pd.DataFrame( - { - "precision": [0.537091], - "recall": [0.538636], - "accuracy": [0.805389], - "f1_score": [0.537716], - "log_loss": [1.445433], - "roc_auc": [0.917818], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=utils.ML_CLASSFICATION_METRICS, index=1 ) # predict new labels @@ -211,19 +168,14 @@ def test_pipeline_logistic_regression_fit_score_predict( ).set_index("tag_number") ) predictions = pl.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - {"predicted_sex": ["MALE", "FEMALE", "FEMALE"]}, - dtype=pd.StringDtype(storage="pyarrow"), - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pd.testing.assert_frame_equal( - predictions[["predicted_sex"]], - expected, + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_sex"], + index=[1633, 1672, 1690], + col_exact=False, ) -# TODO(garrettwu): Re-enable or not check exact numbers. -@pytest.mark.skip(reason="bqml regression") @pytest.mark.flaky(retries=2) def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_index): """Test a supervised model with a minimal preprocessing step""" @@ -247,21 +199,8 @@ def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_in # Check score to ensure the model was fitted score_result = pl.score(X_train, y_train).to_pandas() - score_expected = pd.DataFrame( - { - "mean_absolute_error": [202.298434], - "mean_squared_error": [74515.108971], - "mean_squared_log_error": [0.004365], - "median_absolute_error": [142.949219], - "r2_score": [0.88462], - "explained_variance": [0.886454], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=utils.ML_REGRESSION_METRICS, index=1 ) # predict new labels @@ -283,24 +222,14 @@ def test_pipeline_xgbregressor_fit_score_predict(session, penguins_df_default_in ).set_index("tag_number") ) predictions = pl.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - { - "predicted_body_mass_g": [ - 4287.34521484375, - 3198.351806640625, - 3385.34130859375, - ] - }, - dtype="Float64", - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pd.testing.assert_frame_equal( - predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_body_mass_g"], + index=[1633, 1672, 1690], + col_exact=False, ) -# TODO(garrettwu): Re-enable or not check exact numbers. -@pytest.mark.skip(reason="bqml regression") @pytest.mark.flaky(retries=2) def test_pipeline_random_forest_classifier_fit_score_predict( session, penguins_df_default_index @@ -326,21 +255,8 @@ def test_pipeline_random_forest_classifier_fit_score_predict( # Check score to ensure the model was fitted score_result = pl.score(X_train, y_train).to_pandas() - score_expected = pd.DataFrame( - { - "precision": [0.585505], - "recall": [0.58676], - "accuracy": [0.877246], - "f1_score": [0.585657], - "log_loss": [0.880643], - "roc_auc": [0.970697], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=utils.ML_CLASSFICATION_METRICS, index=1 ) # predict new labels @@ -361,14 +277,11 @@ def test_pipeline_random_forest_classifier_fit_score_predict( ).set_index("tag_number") ) predictions = pl.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - {"predicted_sex": ["MALE", "FEMALE", "FEMALE"]}, - dtype=pd.StringDtype(storage="pyarrow"), - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pd.testing.assert_frame_equal( - predictions[["predicted_sex"]], - expected, + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_sex"], + index=[1633, 1672, 1690], + col_exact=False, ) @@ -412,40 +325,20 @@ def test_pipeline_PCA_fit_score_predict(session, penguins_df_default_index): # Check score to ensure the model was fitted score_result = pl.score(new_penguins).to_pandas() - score_expected = pd.DataFrame( - { - "total_explained_variance_ratio": [1.0], - }, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + score_result, columns=["total_explained_variance_ratio"], index=1 ) predictions = pl.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - { - "principal_component_1": [-1.115259, -1.506141, -1.471173], - "principal_component_2": [-0.074825, 0.69664, 0.406103], - "principal_component_3": [0.500013, -0.544479, 0.075849], - }, - dtype="Float64", - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - - assert_pandas_df_equal_pca( - predictions[ - [ - "principal_component_1", - "principal_component_2", - "principal_component_3", - ] + utils.check_pandas_df_schema_and_index( + predictions, + columns=[ + "principal_component_1", + "principal_component_2", + "principal_component_3", ], - expected, - check_exact=False, - rtol=0.1, + index=[1633, 1672, 1690], + col_exact=False, ) @@ -538,29 +431,16 @@ def test_pipeline_standard_scaler_kmeans_fit_score_predict( # Check score to ensure the model was fitted score_result = pl.score(new_penguins).to_pandas() - score_expected = pd.DataFrame( - {"davies_bouldin_index": [7.542981], "mean_squared_distance": [94.692409]}, - dtype="Float64", - ) - score_expected = score_expected.reindex(index=score_expected.index.astype("Int64")) - - pd.testing.assert_frame_equal( - score_result, score_expected, check_exact=False, rtol=0.1 - ) + eval_metrics = ["davies_bouldin_index", "mean_squared_distance"] + utils.check_pandas_df_schema_and_index(score_result, columns=eval_metrics, index=1) predictions = pl.predict(new_penguins).to_pandas().sort_index() - assert predictions.shape == (6, 9) - result = predictions[["CENTROID_ID"]] - expected = pd.DataFrame( - {"CENTROID_ID": [1, 2, 1, 2, 1, 2]}, - dtype="Int64", - index=pd.Index( - ["test1", "test2", "test3", "test4", "test5", "test6"], - dtype="string[pyarrow]", - ), + utils.check_pandas_df_schema_and_index( + predictions, + columns=["CENTROID_ID"], + index=["test1", "test2", "test3", "test4", "test5", "test6"], + col_exact=False, ) - expected.index.name = "observation" - assert_pandas_df_equal(result, expected, ignore_order=True) def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_index): @@ -632,13 +512,11 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind ).set_index("tag_number") ) predictions = pl.predict(new_penguins).to_pandas() - expected = pd.DataFrame( - {"predicted_body_mass_g": [3909.2, 3436.0, 2860.0]}, - dtype="Float64", - index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), - ) - pd.testing.assert_frame_equal( - predictions[["predicted_body_mass_g"]], expected, check_exact=False, rtol=0.1 + utils.check_pandas_df_schema_and_index( + predictions, + columns=["predicted_body_mass_g"], + index=[1633, 1672, 1690], + col_exact=False, ) diff --git a/tests/system/utils.py b/tests/system/utils.py index e40502e6f2a..ab4c2c119fa 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -15,7 +15,7 @@ import base64 import decimal import functools -from typing import Iterable, Optional, Set +from typing import Iterable, Optional, Set, Union import geopandas as gpd # type: ignore import google.api_core.operation @@ -28,6 +28,23 @@ from bigframes.functions import remote_function +ML_REGRESSION_METRICS = [ + "mean_absolute_error", + "mean_squared_error", + "mean_squared_log_error", + "median_absolute_error", + "r2_score", + "explained_variance", +] +ML_CLASSFICATION_METRICS = [ + "precision", + "recall", + "accuracy", + "f1_score", + "log_loss", + "roc_auc", +] + def skip_legacy_pandas(test): @functools.wraps(test) @@ -249,6 +266,33 @@ def assert_pandas_df_equal_pca(actual, expected, **kwargs): pd.testing.assert_series_equal(-actual[column], expected[column], **kwargs) +def check_pandas_df_schema_and_index( + pd_df: pd.DataFrame, + columns: Iterable, + index: Union[int, Iterable], + col_exact: bool = True, +): + """Check pandas df schema and index. But not the values. + + Args: + pd_df: the input pandas df + columns: target columns to check with + index: int or Iterable. If int, only check the length (index size) of the df. If Iterable, check index values match + col_exact: If True, check the columns param are exact match. Otherwise only check the df contains all of those columns + """ + if col_exact: + assert list(pd_df.columns) == list(columns) + else: + assert set(columns) <= set(pd_df.columns) + + if isinstance(index, int): + assert len(pd_df) == index + elif isinstance(index, Iterable): + assert list(pd_df.index) == list(index) + else: + raise ValueError("Unsupported index type.") + + def get_remote_function_endpoints( bigquery_client: bigquery.Client, dataset_id: str ) -> Set[str]: From b95154908fd7838e499a2af0fc3760c5ab33358f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 16 May 2024 10:34:00 -0500 Subject: [PATCH 575/921] docs: use `class_weight="balanced"` in the logistic regression prediction tutorial (#678) This aligns the Python code with the SQL at https://cloud.google.com/bigquery/docs/logistic-regression-prediction#create_a_logistic_regression_model ```sql CREATE OR REPLACE MODEL `census.census_model` OPTIONS ( model_type='LOGISTIC_REG', auto_class_weights=TRUE, data_split_method='NO_SPLIT', input_label_cols=['income_bracket'], max_iterations=15) AS SELECT * EXCEPT(dataframe) FROM `census.input_data` WHERE dataframe = 'training' ``` --- .../logistic_regression_prediction_test.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/samples/snippets/logistic_regression_prediction_test.py b/samples/snippets/logistic_regression_prediction_test.py index 6a40369ba89..dd92f8f3e3e 100644 --- a/samples/snippets/logistic_regression_prediction_test.py +++ b/samples/snippets/logistic_regression_prediction_test.py @@ -80,7 +80,21 @@ def test_logistic_regression_prediction(random_model_id: str) -> None: X = training_data.drop(columns=["income_bracket", "dataframe"]) y = training_data["income_bracket"] - census_model = bigframes.ml.linear_model.LogisticRegression() + census_model = bigframes.ml.linear_model.LogisticRegression( + # Balance the class labels in the training data by setting + # class_weight="balanced". + # + # By default, the training data is unweighted. If the labels + # in the training data are imbalanced, the model may learn to + # predict the most popular class of labels more heavily. In + # this case, most of the respondents in the dataset are in the + # lower income bracket. This may lead to a model that predicts + # the lower income bracket too heavily. Class weights balance + # the class labels by calculating the weights for each class in + # inverse proportion to the frequency of that class. + class_weight="balanced", + max_iterations=15, + ) census_model.fit(X, y) census_model.to_gbq( From c9cc1ff7b85bd6d39b491a3c981ef3520dd0124a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 16 May 2024 12:12:10 -0500 Subject: [PATCH 576/921] chore: update continuous samples tests to latest bigframes release (#691) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 1da77c1715a..8fcd19bb2c3 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ # samples/snippets should be runnable with no "extras" -bigframes==1.4.0 +bigframes==1.6.0 From ab1bc04da63cf0391f373890f68bc39160196ae3 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 16 May 2024 13:26:14 -0700 Subject: [PATCH 577/921] test: disable test_to_pandas_large_table (#695) --- tests/system/load/test_large_tables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/system/load/test_large_tables.py b/tests/system/load/test_large_tables.py index 2621e62dde6..cf1c787a588 100644 --- a/tests/system/load/test_large_tables.py +++ b/tests/system/load/test_large_tables.py @@ -93,6 +93,7 @@ def test_to_pandas_batches_large_table(): assert row_count == expected_row_count +@pytest.mark.skip(reason="See if it caused kokoro build aborted.") def test_to_pandas_large_table(): df = bpd.read_gbq("load_testing.scalars_10gb") # df will be downloaded locally From 412f28bf7551430473690160a2a1c4c2f133539e Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 16 May 2024 13:33:54 -0700 Subject: [PATCH 578/921] feat: bigframes.bigquery.array_agg(SeriesGroupBy|DataFrameGroupby) (#663) --- bigframes/bigquery/__init__.py | 61 ++++++ bigframes/core/compile/aggregate_compiler.py | 173 ++++++++++++++---- bigframes/core/compile/compiled.py | 151 +++++++++++---- bigframes/core/compile/compiler.py | 18 +- bigframes/operations/aggregations.py | 26 ++- tests/system/small/bigquery/test_array.py | 117 +++++++++++- .../ibis/backends/bigquery/registry.py | 16 ++ .../ibis/expr/operations/__init__.py | 2 +- .../expr/operations/{generic.py => arrays.py} | 7 +- .../ibis/expr/operations/reductions.py | 20 +- 10 files changed, 506 insertions(+), 85 deletions(-) rename third_party/bigframes_vendored/ibis/expr/operations/{generic.py => arrays.py} (64%) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 197e0a83b54..6c9c04dca72 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -22,9 +22,13 @@ import typing +import bigframes.constants as constants +import bigframes.core.groupby as groupby import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops if typing.TYPE_CHECKING: + import bigframes.dataframe as dataframe import bigframes.series as series @@ -52,9 +56,66 @@ def array_length(series: series.Series) -> series.Series: 2 2 dtype: Int64 + Args: + series (bigframes.series.Series): + A Series with array columns. + Returns: bigframes.series.Series: A Series of integer values indicating the length of each element in the Series. """ return series._apply_unary_op(ops.len_op) + + +def array_agg( + obj: groupby.SeriesGroupBy | groupby.DataFrameGroupBy, +) -> series.Series | dataframe.DataFrame: + """Group data and create arrays from selected columns, omitting NULLs to avoid + BigQuery errors (NULLs not allowed in arrays). + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + For a SeriesGroupBy object: + + >>> lst = ['a', 'a', 'b', 'b', 'a'] + >>> s = bpd.Series([1, 2, 3, 4, np.nan], index=lst) + >>> bbq.array_agg(s.groupby(level=0)) + a [1. 2.] + b [3. 4.] + dtype: list[pyarrow] + + For a DataFrameGroupBy object: + + >>> l = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]] + >>> df = bpd.DataFrame(l, columns=["a", "b", "c"]) + >>> bbq.array_agg(df.groupby(by=["b"])) + a c + b + 1.0 [2] [3] + 2.0 [1 1] [3 2] + + [2 rows x 2 columns] + + Args: + obj (groupby.SeriesGroupBy | groupby.DataFrameGroupBy): + A GroupBy object to be applied the function. + + Returns: + bigframes.series.Series | bigframes.dataframe.DataFrame: A Series or + DataFrame containing aggregated array columns, and indexed by the + original group columns. + """ + if isinstance(obj, groupby.SeriesGroupBy): + return obj._aggregate(agg_ops.ArrayAggOp()) + elif isinstance(obj, groupby.DataFrameGroupBy): + return obj._aggregate_all(agg_ops.ArrayAggOp(), numeric_only=False) + else: + raise ValueError( + f"Unsupported type {type(obj)} to apply `array_agg` function. {constants.FEEDBACK_LINK}" + ) diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 98d296c779f..c0b0562a547 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -34,13 +34,14 @@ def compile_aggregate( aggregate: ex.Aggregation, bindings: typing.Dict[str, ibis_types.Value], + order_by: typing.Sequence[ibis_types.Value] = [], ) -> ibis_types.Value: if isinstance(aggregate, ex.UnaryAggregation): input = scalar_compiler.compile_expression(aggregate.arg, bindings=bindings) - return compile_unary_agg( - aggregate.op, - input, - ) + if aggregate.op.can_order_by: + return compile_ordered_unary_agg(aggregate.op, input, order_by=order_by) + else: + return compile_unary_agg(aggregate.op, input) elif isinstance(aggregate, ex.BinaryAggregation): left = scalar_compiler.compile_expression(aggregate.left, bindings=bindings) right = scalar_compiler.compile_expression(aggregate.right, bindings=bindings) @@ -66,7 +67,8 @@ def compile_analytic( @functools.singledispatch def compile_binary_agg( op: agg_ops.WindowOp, - input: ibis_types.Column, + left: ibis_types.Column, + right: ibis_types.Column, window: Optional[window_spec.WindowSpec] = None, ) -> ibis_types.Value: raise ValueError(f"Can't compile unrecognized operation: {op}") @@ -81,9 +83,24 @@ def compile_unary_agg( raise ValueError(f"Can't compile unrecognized operation: {op}") +@functools.singledispatch +def compile_ordered_unary_agg( + op: agg_ops.WindowOp, + input: ibis_types.Column, + window: Optional[window_spec.WindowSpec] = None, + order_by: typing.Sequence[ibis_types.Value] = [], +) -> ibis_types.Value: + raise ValueError(f"Can't compile unrecognized operation: {op}") + + def numeric_op(operation): @functools.wraps(operation) - def constrained_op(op, column: ibis_types.Column, window=None): + def constrained_op( + op, + column: ibis_types.Column, + window=None, + order_by: typing.Sequence[ibis_types.Value] = [], + ): if column.type().is_boolean(): column = typing.cast( ibis_types.NumericColumn, column.cast(ibis_dtypes.int64) @@ -104,7 +121,9 @@ def constrained_op(op, column: ibis_types.Column, window=None): @compile_unary_agg.register @numeric_op def _( - op: agg_ops.SumOp, column: ibis_types.NumericColumn, window=None + op: agg_ops.SumOp, + column: ibis_types.NumericColumn, + window=None, ) -> ibis_types.NumericValue: # Will be null if all inputs are null. Pandas defaults to zero sum though. bq_sum = _apply_window_if_present(column.sum(), window) @@ -116,7 +135,9 @@ def _( @compile_unary_agg.register @numeric_op def _( - op: agg_ops.MedianOp, column: ibis_types.NumericColumn, window=None + op: agg_ops.MedianOp, + column: ibis_types.NumericColumn, + window=None, ) -> ibis_types.NumericValue: # PERCENTILE_CONT has very few allowed windows. For example, "window # framing clause is not allowed for analytic function percentile_cont". @@ -134,7 +155,9 @@ def _( @compile_unary_agg.register @numeric_op def _( - op: agg_ops.ApproxQuartilesOp, column: ibis_types.NumericColumn, window=None + op: agg_ops.ApproxQuartilesOp, + column: ibis_types.NumericColumn, + window=None, ) -> ibis_types.NumericValue: # PERCENTILE_CONT has very few allowed windows. For example, "window # framing clause is not allowed for analytic function percentile_cont". @@ -151,7 +174,9 @@ def _( @compile_unary_agg.register @numeric_op def _( - op: agg_ops.QuantileOp, column: ibis_types.NumericColumn, window=None + op: agg_ops.QuantileOp, + column: ibis_types.NumericColumn, + window=None, ) -> ibis_types.NumericValue: return _apply_window_if_present(column.quantile(op.q), window) @@ -159,7 +184,10 @@ def _( @compile_unary_agg.register @numeric_op def _( - op: agg_ops.MeanOp, column: ibis_types.NumericColumn, window=None + op: agg_ops.MeanOp, + column: ibis_types.NumericColumn, + window=None, + # order_by: typing.Sequence[ibis_types.Value] = [], ) -> ibis_types.NumericValue: return _apply_window_if_present(column.mean(), window) @@ -167,7 +195,9 @@ def _( @compile_unary_agg.register @numeric_op def _( - op: agg_ops.ProductOp, column: ibis_types.NumericColumn, window=None + op: agg_ops.ProductOp, + column: ibis_types.NumericColumn, + window=None, ) -> ibis_types.NumericValue: # Need to short-circuit as log with zeroes is illegal sql is_zero = cast(ibis_types.BooleanColumn, (column == 0)) @@ -202,30 +232,50 @@ def _( @compile_unary_agg.register -def _(op: agg_ops.MaxOp, column: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.MaxOp, + column: ibis_types.Column, + window=None, +) -> ibis_types.Value: return _apply_window_if_present(column.max(), window) @compile_unary_agg.register -def _(op: agg_ops.MinOp, column: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.MinOp, + column: ibis_types.Column, + window=None, +) -> ibis_types.Value: return _apply_window_if_present(column.min(), window) @compile_unary_agg.register @numeric_op -def _(op: agg_ops.StdOp, x: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.StdOp, + x: ibis_types.Column, + window=None, +) -> ibis_types.Value: return _apply_window_if_present(cast(ibis_types.NumericColumn, x).std(), window) @compile_unary_agg.register @numeric_op -def _(op: agg_ops.VarOp, x: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.VarOp, + x: ibis_types.Column, + window=None, +) -> ibis_types.Value: return _apply_window_if_present(cast(ibis_types.NumericColumn, x).var(), window) @compile_unary_agg.register @numeric_op -def _(op: agg_ops.PopVarOp, x: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.PopVarOp, + x: ibis_types.Column, + window=None, +) -> ibis_types.Value: return _apply_window_if_present( cast(ibis_types.NumericColumn, x).var(how="pop"), window ) @@ -233,13 +283,19 @@ def _(op: agg_ops.PopVarOp, x: ibis_types.Column, window=None) -> ibis_types.Val @compile_unary_agg.register def _( - op: agg_ops.CountOp, column: ibis_types.Column, window=None + op: agg_ops.CountOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.IntegerValue: return _apply_window_if_present(column.count(), window) @compile_unary_agg.register -def _(op: agg_ops.CutOp, x: ibis_types.Column, window=None): +def _( + op: agg_ops.CutOp, + x: ibis_types.Column, + window=None, +): out = ibis.case() if isinstance(op.bins, int): col_min = _apply_window_if_present(x.min(), window) @@ -292,7 +348,9 @@ def _(op: agg_ops.CutOp, x: ibis_types.Column, window=None): @compile_unary_agg.register @numeric_op def _( - self: agg_ops.QcutOp, column: ibis_types.Column, window=None + self: agg_ops.QcutOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.IntegerValue: if isinstance(self.quantiles, int): quantiles_ibis = dtypes.literal_to_ibis_scalar(self.quantiles) @@ -322,21 +380,27 @@ def _( @compile_unary_agg.register def _( - op: agg_ops.NuniqueOp, column: ibis_types.Column, window=None + op: agg_ops.NuniqueOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.IntegerValue: return _apply_window_if_present(column.nunique(), window) @compile_unary_agg.register def _( - op: agg_ops.AnyValueOp, column: ibis_types.Column, window=None + op: agg_ops.AnyValueOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.IntegerValue: return _apply_window_if_present(column.arbitrary(), window) @compile_unary_agg.register def _( - op: agg_ops.RankOp, column: ibis_types.Column, window=None + op: agg_ops.RankOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.IntegerValue: # Ibis produces 0-based ranks, while pandas creates 1-based ranks return _apply_window_if_present(ibis.rank(), window) + 1 @@ -344,7 +408,9 @@ def _( @compile_unary_agg.register def _( - op: agg_ops.DenseRankOp, column: ibis_types.Column, window=None + op: agg_ops.DenseRankOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.IntegerValue: # Ibis produces 0-based ranks, while pandas creates 1-based ranks return _apply_window_if_present(column.dense_rank(), window) + 1 @@ -357,7 +423,9 @@ def _(op: agg_ops.FirstOp, column: ibis_types.Column, window=None) -> ibis_types @compile_unary_agg.register def _( - op: agg_ops.FirstNonNullOp, column: ibis_types.Column, window=None + op: agg_ops.FirstNonNullOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.Value: return _apply_window_if_present( vendored_ibis_ops.FirstNonNullValue(column).to_expr(), window # type: ignore @@ -365,13 +433,19 @@ def _( @compile_unary_agg.register -def _(op: agg_ops.LastOp, column: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.LastOp, + column: ibis_types.Column, + window=None, +) -> ibis_types.Value: return _apply_window_if_present(column.last(), window) @compile_unary_agg.register def _( - op: agg_ops.LastNonNullOp, column: ibis_types.Column, window=None + op: agg_ops.LastNonNullOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.Value: return _apply_window_if_present( vendored_ibis_ops.LastNonNullValue(column).to_expr(), window # type: ignore @@ -379,7 +453,11 @@ def _( @compile_unary_agg.register -def _(op: agg_ops.ShiftOp, column: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.ShiftOp, + column: ibis_types.Column, + window=None, +) -> ibis_types.Value: if op.periods == 0: # No-op return column if op.periods > 0: @@ -388,7 +466,11 @@ def _(op: agg_ops.ShiftOp, column: ibis_types.Column, window=None) -> ibis_types @compile_unary_agg.register -def _(op: agg_ops.DiffOp, column: ibis_types.Column, window=None) -> ibis_types.Value: +def _( + op: agg_ops.DiffOp, + column: ibis_types.Column, + window=None, +) -> ibis_types.Value: shifted = compile_unary_agg(agg_ops.ShiftOp(op.periods), column, window) if column.type().is_boolean(): return cast(ibis_types.BooleanColumn, column) != cast( @@ -404,7 +486,9 @@ def _(op: agg_ops.DiffOp, column: ibis_types.Column, window=None) -> ibis_types. @compile_unary_agg.register def _( - op: agg_ops.AllOp, column: ibis_types.Column, window=None + op: agg_ops.AllOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.BooleanValue: # BQ will return null for empty column, result would be true in pandas. result = _is_true(column).all() @@ -416,7 +500,9 @@ def _( @compile_unary_agg.register def _( - op: agg_ops.AnyOp, column: ibis_types.Column, window=None + op: agg_ops.AnyOp, + column: ibis_types.Column, + window=None, ) -> ibis_types.BooleanValue: # BQ will return null for empty column, result would be false in pandas. result = _is_true(column).any() @@ -426,6 +512,31 @@ def _( ) +@compile_ordered_unary_agg.register +def _( + op: agg_ops.ArrayAggOp, + column: ibis_types.Column, + window=None, + order_by: typing.Sequence[ibis_types.Value] = [], +) -> ibis_types.ArrayValue: + # BigQuery doesn't currently support using ARRAY_AGG with both window and aggregate + # functions simultaneously. Some aggregate functions (or its equivalent syntax) + # are more important, such as: + # - `IGNORE NULLS` are required to avoid an raised error if the final result + # contains a NULL element. + # - `ORDER BY` are required for the default ordering mode. + # To keep things simpler, windowing support is skipped for now. + if window is not None: + raise NotImplementedError( + f"ArrayAgg with windowing is not supported. {constants.FEEDBACK_LINK}" + ) + + return vendored_ibis_ops.ArrayAggregate( + column, + order_by=order_by, + ).to_expr() + + @compile_binary_agg.register def _( op: agg_ops.CorrOp, left: ibis_types.Column, right: ibis_types.Column, window=None diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index b57e0c4d351..1c2217c25aa 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -102,6 +102,12 @@ def _reduced_predicate(self) -> typing.Optional[ibis_types.BooleanValue]: def _ibis_bindings(self) -> dict[str, ibis_types.Value]: return {col: self._get_ibis_column(col) for col in self.column_ids} + @property + @abc.abstractmethod + def is_ordered_ir(self: T) -> bool: + """Whether it is a OrderedIR or UnorderedIR.""" + ... + @abc.abstractmethod def filter(self: T, predicate: ex.Expression) -> T: """Filter the table on a given expression, the predicate must be a boolean expression.""" @@ -163,6 +169,53 @@ def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), ) + def _aggregate_base( + self, + table: ibis_types.Table, + order_by: typing.Sequence[ibis_types.Value] = [], + aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]] = [], + by_column_ids: typing.Sequence[str] = (), + dropna: bool = True, + ) -> OrderedIR: + assert not self.is_ordered_ir or len(order_by) > 0 + + bindings = {col: table[col] for col in self.column_ids} + stats = { + col_out: agg_compiler.compile_aggregate( + aggregate, bindings, order_by=order_by + ) + for aggregate, col_out in aggregations + } + if by_column_ids: + result = table.group_by(by_column_ids).aggregate(**stats) + # Must have deterministic ordering, so order by the unique "by" column + ordering = ExpressionOrdering( + tuple([ascending_over(column_id) for column_id in by_column_ids]), + total_ordering_columns=frozenset(by_column_ids), + ) + columns = tuple(result[key] for key in result.columns) + expr = OrderedIR(result, columns=columns, ordering=ordering) + if dropna: + for column_id in by_column_ids: + expr = expr._filter(expr._get_ibis_column(column_id).notnull()) + return expr + else: + aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} + result = table.aggregate(**aggregates) + # Ordering is irrelevant for single-row output, but set ordering id regardless + # as other ops(join etc.) expect it. + # TODO: Maybe can make completely empty + ordering = ExpressionOrdering( + ordering_value_columns=tuple([]), + total_ordering_columns=frozenset([]), + ) + return OrderedIR( + result, + columns=[result[col_id] for col_id in [*stats.keys()]], + hidden_ordering_columns=[result[ORDER_ID_COLUMN]], + ordering=ordering, + ) + # Ibis Implementations class UnorderedIR(BaseIbisIR): @@ -174,6 +227,10 @@ def __init__( ): super().__init__(table, columns, predicates) + @property + def is_ordered_ir(self) -> bool: + return False + def builder(self): """Creates a mutable builder for expressions.""" # Since ArrayValue is intended to be immutable (immutability offers @@ -310,44 +367,17 @@ def aggregate( Apply aggregations to the expression. Arguments: aggregations: input_column_id, operation, output_column_id tuples - by_column_id: column id of the aggregation key, this is preserved through the transform + by_column_ids: column ids of the aggregation key, this is preserved through + the transform dropna: whether null keys should be dropped + Returns: + OrderedIR: the grouping key is a unique-valued column and has ordering + information. """ table = self._to_ibis_expr() - bindings = {col: table[col] for col in self.column_ids} - stats = { - col_out: agg_compiler.compile_aggregate(aggregate, bindings) - for aggregate, col_out in aggregations - } - if by_column_ids: - result = table.group_by(by_column_ids).aggregate(**stats) - # Must have deterministic ordering, so order by the unique "by" column - ordering = ExpressionOrdering( - tuple([ascending_over(column_id) for column_id in by_column_ids]), - total_ordering_columns=frozenset(by_column_ids), - ) - columns = tuple(result[key] for key in result.columns) - expr = OrderedIR(result, columns=columns, ordering=ordering) - if dropna: - for column_id in by_column_ids: - expr = expr._filter(expr._get_ibis_column(column_id).notnull()) - # Can maybe remove this as Ordering id is redundant as by_column is unique after aggregation - return expr._project_offsets() - else: - aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} - result = table.aggregate(**aggregates) - # Ordering is irrelevant for single-row output, but set ordering id regardless as other ops(join etc.) expect it. - # TODO: Maybe can make completely empty - ordering = ExpressionOrdering( - ordering_value_columns=tuple([]), - total_ordering_columns=frozenset([]), - ) - return OrderedIR( - result, - columns=[result[col_id] for col_id in [*stats.keys()]], - hidden_ordering_columns=[result[ORDER_ID_COLUMN]], - ordering=ordering, - ) + return self._aggregate_base( + table, aggregations=aggregations, by_column_ids=by_column_ids, dropna=dropna + ) def _uniform_sampling(self, fraction: float) -> UnorderedIR: """Sampling the table on given fraction. @@ -526,6 +556,10 @@ def __init__( if not ordering_valid: raise ValueError(f"Illegal ordering keys: {ordering.all_ordering_columns}") + @property + def is_ordered_ir(self) -> bool: + return True + @classmethod def from_pandas( cls, @@ -535,7 +569,8 @@ def from_pandas( """ Builds an in-memory only (SQL only) expr from a pandas dataframe. - Assumed that the dataframe has unique string column names and bigframes-suppported dtypes. + Assumed that the dataframe has unique string column names and bigframes-suppported + dtypes. """ # ibis memtable cannot handle NA, must convert to None @@ -572,7 +607,8 @@ def _hidden_column_ids(self) -> typing.Sequence[str]: @property def _ibis_order(self) -> Sequence[ibis_types.Value]: - """Returns a sequence of ibis values which can be directly used to order a table expression. Has direction modifiers applied.""" + """Returns a sequence of ibis values which can be directly used to order a + table expression. Has direction modifiers applied.""" return _convert_ordering_to_table_values( {**self._column_names, **self._hidden_ordering_column_names}, self._ordering.all_ordering_columns, @@ -604,6 +640,44 @@ def reversed(self) -> OrderedIR: expr_builder.ordering = self._ordering.with_reverse() return expr_builder.build() + def aggregate( + self, + aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]], + by_column_ids: typing.Sequence[str] = (), + dropna: bool = True, + ) -> OrderedIR: + """ + Apply aggregations to the expression. + Arguments: + aggregations: input_column_id, operation, output_column_id tuples + by_column_ids: column ids of the aggregation key, this is preserved through + the transform + dropna: whether null keys should be dropped + Returns: + OrderedIR + """ + table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) + + all_columns = { + column_name: table[column_name] + for column_name in { + **self._column_names, + **self._hidden_ordering_column_names, + } + } + order_by = _convert_ordering_to_table_values( + all_columns, + self._ordering.all_ordering_columns, + ) + + return self._aggregate_base( + table, + order_by=order_by, + aggregations=aggregations, + by_column_ids=by_column_ids, + dropna=dropna, + ) + def _uniform_sampling(self, fraction: float) -> OrderedIR: """Sampling the table on given fraction. @@ -1069,7 +1143,8 @@ def _bake_ordering(self) -> OrderedIR: ) def _project_offsets(self) -> OrderedIR: - """Create a new expression that contains offsets. Should only be executed when offsets are needed for an operations. Has no effect on expression semantics.""" + """Create a new expression that contains offsets. Should only be executed when + offsets are needed for an operations. Has no effect on expression semantics.""" if self._ordering.is_sequential: return self table = self._to_ibis_expr( diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index a68023d13d0..a9908192f32 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -155,10 +155,18 @@ def compile_rowcount(node: nodes.RowCountNode, ordered: bool = True): @_compile_node.register def compile_aggregate(node: nodes.AggregateNode, ordered: bool = True): - result = compile_unordered_ir(node.child).aggregate( - node.aggregations, node.by_column_ids, node.dropna + has_ordered_aggregation_ops = any( + aggregate.op.can_order_by for aggregate, _ in node.aggregations ) - return result if ordered else result.to_unordered() + if ordered and has_ordered_aggregation_ops: + return compile_ordered_ir(node.child).aggregate( + node.aggregations, node.by_column_ids, node.dropna + ) + else: + result = compile_unordered_ir(node.child).aggregate( + node.aggregations, node.by_column_ids, node.dropna + ) + return result if ordered else result.to_unordered() @_compile_node.register @@ -180,10 +188,10 @@ def compile_reproject(node: nodes.ReprojectOpNode, ordered: bool = True): @_compile_node.register -def compiler_explode(node: nodes.ExplodeNode, ordered: bool = True): +def compile_explode(node: nodes.ExplodeNode, ordered: bool = True): return compile_node(node.child, ordered).explode(node.column_ids) @_compile_node.register -def compiler_random_sample(node: nodes.RandomSampleNode, ordered: bool = True): +def compile_random_sample(node: nodes.RandomSampleNode, ordered: bool = True): return compile_node(node.child, ordered)._uniform_sampling(node.fraction) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index c57fac41129..3b5310554b4 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -38,6 +38,10 @@ def uses_total_row_ordering(self): """Whether the operator needs total row ordering. (eg. lead, lag, array_agg)""" return False + @property + def can_order_by(self): + return False + @abc.abstractmethod def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: ... @@ -115,7 +119,7 @@ class QuantileOp(UnaryAggregateOp): @property def name(self): - return f"{int(self.q*100)}%" + return f"{int(self.q * 100)}%" def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return signatures.UNARY_REAL_NUMERIC.output_type(input_types[0]) @@ -127,7 +131,7 @@ class ApproxQuartilesOp(UnaryAggregateOp): @property def name(self): - return f"{self.quartile*25}%" + return f"{self.quartile * 25}%" def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: if not dtypes.is_orderable(input_types[0]): @@ -222,6 +226,24 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT ).output_type(input_types[0]) +@dataclasses.dataclass(frozen=True) +class ArrayAggOp(UnaryAggregateOp): + name: ClassVar[str] = "arrayagg" + + @property + def can_order_by(self): + return True + + @property + def skips_nulls(self): + return True + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return pd.ArrowDtype( + pa.list_(dtypes.bigframes_dtype_to_arrow_dtype(input_types[0])) + ) + + @dataclasses.dataclass(frozen=True) class CutOp(UnaryWindowOp): # TODO: Unintuitive, refactor into multiple ops? diff --git a/tests/system/small/bigquery/test_array.py b/tests/system/small/bigquery/test_array.py index a91669cd887..0664c31a3c4 100644 --- a/tests/system/small/bigquery/test_array.py +++ b/tests/system/small/bigquery/test_array.py @@ -14,6 +14,7 @@ import numpy as np import pandas as pd +import pytest import bigframes.bigquery as bbq import bigframes.pandas as bpd @@ -23,10 +24,118 @@ def test_array_length(): series = bpd.Series([["A", "AA", "AAA"], ["BB", "B"], np.nan, [], ["C"]]) # TODO(b/336880368): Allow for NULL values to be input for ARRAY columns. # Once we actually store NULL values, this will be NULL where the input is NULL. - expected = pd.Series([3, 2, 0, 0, 1]) + expected = bpd.Series([3, 2, 0, 0, 1]) pd.testing.assert_series_equal( bbq.array_length(series).to_pandas(), - expected, - check_dtype=False, - check_index_type=False, + expected.to_pandas(), + ) + + +@pytest.mark.parametrize( + ("input_data", "output_data"), + [ + pytest.param([1, 2, 3, 4, 5], [[1, 2], [3, 4], [5]], id="ints"), + pytest.param( + ["e", "d", "c", "b", "a"], + [["e", "d"], ["c", "b"], ["a"]], + id="reverse_strings", + ), + pytest.param( + [1.0, 2.0, np.nan, np.nan, np.nan], [[1.0, 2.0], [], []], id="nans" + ), + pytest.param( + [{"A": {"x": 1.0}}, {"A": {"z": 4.0}}, {}, {"B": "b"}, np.nan], + [[{"A": {"x": 1.0}}, {"A": {"z": 4.0}}], [{}, {"B": "b"}], []], + id="structs", + ), + ], +) +def test_array_agg_w_series_groupby(input_data, output_data): + input_index = ["a", "a", "b", "b", "c"] + series = bpd.Series(input_data, index=input_index) + result = bbq.array_agg(series.groupby(level=0)) + + expected = bpd.Series(output_data, index=["a", "b", "c"]) + pd.testing.assert_series_equal( + result.to_pandas(), # type: ignore + expected.to_pandas(), + ) + + +def test_array_agg_w_dataframe_groupby(): + data = { + "a": [1, 1, 2, 1], + "b": [2, None, 1, 2], + "c": [3, 4, 3, 2], + } + df = bpd.DataFrame(data) + result = bbq.array_agg(df.groupby(by=["b"])) + + expected_data = { + "b": [1.0, 2.0], + "a": [[2], [1, 1]], + "c": [[3], [3, 2]], + } + expected = bpd.DataFrame(expected_data).set_index("b") + + pd.testing.assert_frame_equal( + result.to_pandas(), # type: ignore + expected.to_pandas(), + ) + + +def test_array_agg_w_series(): + series = bpd.Series([1, 2, 3, 4, 5], index=["a", "a", "b", "b", "c"]) + # Mypy error expected: array_agg currently incompatible with Series. + # Test for coverage. + with pytest.raises(ValueError): + bbq.array_agg(series) # type: ignore + + +@pytest.mark.parametrize( + ("ascending", "expected_b", "expected_c"), + [ + pytest.param( + True, [["a", "b"], ["e", "d", "c"]], [[4, 5], [1, 2, 3]], id="asc" + ), + pytest.param( + False, [["b", "a"], ["c", "d", "e"]], [[5, 4], [3, 2, 1]], id="des" + ), + ], +) +def test_array_agg_reserve_order(ascending, expected_b, expected_c): + data = { + "a": [1, 1, 2, 2, 2], + "b": ["a", "b", "c", "d", "e"], + "c": [4, 5, 3, 2, 1], + } + df = bpd.DataFrame(data) + + result = bbq.array_agg(df.sort_values("c", ascending=ascending).groupby(by=["a"])) + expected_data = { + "a": [1, 2], + "b": expected_b, + "c": expected_c, + } + expected = bpd.DataFrame(expected_data).set_index("a") + + pd.testing.assert_frame_equal( + result.to_pandas(), # type: ignore + expected.to_pandas(), + ) + + +def test_array_agg_matches_after_explode(): + data = { + "index": np.arange(10), + "a": [np.random.randint(0, 10, 10) for _ in range(10)], + "b": [np.random.randint(0, 10, 10) for _ in range(10)], + } + df = bpd.DataFrame(data).set_index("index") + result = bbq.array_agg(df.explode(["a", "b"]).groupby(level=0)) + result.index.name = "index" + + pd.testing.assert_frame_equal( + result.to_pandas(), # type: ignore + df.to_pandas(), ) diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py index 64ef05366dc..ecef2115e5b 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py @@ -43,6 +43,21 @@ def _quantile(translator, op: ibis_reductions.Quantile): return f"PERCENTILE_CONT({arg}, {quantile})" +def _array_aggregate(translator, op: vendored_ibis_ops.ArrayAggregate): + """This method provides the same functionality as the collect() method in Ibis, with + the added capability of ordering the results using order_by. + https://github.com/ibis-project/ibis/issues/9170 + """ + arg = translator.translate(op.arg) + + order_by_sql = "" + if len(op.order_by) > 0: + order_by = ", ".join([translator.translate(column) for column in op.order_by]) + order_by_sql = f"ORDER BY {order_by}" + + return f"ARRAY_AGG({arg} IGNORE NULLS {order_by_sql})" + + patched_ops = { vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, # type:ignore vendored_ibis_ops.FirstNonNullValue: _first_non_null_value, # type:ignore @@ -51,6 +66,7 @@ def _quantile(translator, op: ibis_reductions.Quantile): vendored_ibis_ops.GenerateArray: _generate_array, # type:ignore vendored_ibis_ops.SafeCastToDatetime: _safe_cast_to_datetime, # type:ignore ibis_reductions.Quantile: _quantile, # type:ignore + vendored_ibis_ops.ArrayAggregate: _array_aggregate, # type:ignore } OPERATION_REGISTRY.update(patched_ops) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py index 3d5a5a7fa0a..3ae5fc10e42 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py @@ -2,6 +2,6 @@ from __future__ import annotations from bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F401 F403 -from bigframes_vendored.ibis.expr.operations.generic import * # noqa: F401 F403 +from bigframes_vendored.ibis.expr.operations.arrays import * # noqa: F401 F403 from bigframes_vendored.ibis.expr.operations.json import * # noqa: F401 F403 from bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F401 F403 diff --git a/third_party/bigframes_vendored/ibis/expr/operations/generic.py b/third_party/bigframes_vendored/ibis/expr/operations/arrays.py similarity index 64% rename from third_party/bigframes_vendored/ibis/expr/operations/generic.py rename to third_party/bigframes_vendored/ibis/expr/operations/arrays.py index 98acaacfbda..a0ad915a9be 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/generic.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/arrays.py @@ -1,4 +1,4 @@ -# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/generic.py +# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/arrays.py from __future__ import annotations import ibis.expr.datatypes as dt @@ -6,6 +6,11 @@ class GenerateArray(Unary): + """ + Generates an array of values, similar to ibis.range(), but with simpler and + more efficient SQL generation. + """ + dtype = dt.Array(dt.int64) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/reductions.py b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py index e6644f477a5..bd971e408a0 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/reductions.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py @@ -2,6 +2,8 @@ from __future__ import annotations +import ibis.common.annotations as ibis_annotations +from ibis.common.typing import VarTuple import ibis.expr.datatypes as dt import ibis.expr.operations.core as ibis_ops_core from ibis.expr.operations.reductions import Filterable, Reduction @@ -18,6 +20,18 @@ class ApproximateMultiQuantile(Filterable, Reduction): dtype = dt.Array(dt.float64) -__all__ = [ - "ApproximateMultiQuantile", -] +class ArrayAggregate(Filterable, Reduction): + """ + Collects the elements of this expression into an ordered array. Similar to + the ibis `ArrayCollect`, but adds `order_by_*` and `distinct_only` parameters. + """ + + arg: ibis_ops_core.Column + order_by: VarTuple[ibis_ops_core.Value] = () + + @ibis_annotations.attribute + def dtype(self): + return dt.Array(self.arg.dtype) + + +__all__ = ["ApproximateMultiQuantile", "ArrayAggregate"] From 9386373538c1e7827e2210c4fd9946312821b54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 16 May 2024 16:07:58 -0500 Subject: [PATCH 579/921] fix: `read_gbq_table` respects primary keys even when `filters` are set (#689) * fix: `read_gbq_table` respects primary keys even when `filters` are set Closes internal issues 338039517 (primary key inconsistency), 338037499 (LIMIT for max_results), 340540991 (avoid running query immediately if time travel is supported), 337925142 (push down column filters to when we create the time travel subquery). feat: `read_gbq` suggests a correct column name when one is not found feat: `read_gbq_query` supports `filters` perf: use a `LIMIT` clause when `max_results` is set perf: don't run query immediately from `read_gbq_table` if `filters` is set --- .pre-commit-config.yaml | 3 +- bigframes/core/blocks.py | 4 +- bigframes/core/sql.py | 8 +- bigframes/pandas/__init__.py | 2 + bigframes/session/__init__.py | 177 +++++++++++------- bigframes/session/_io/bigquery/__init__.py | 24 ++- .../session/_io/bigquery/read_gbq_table.py | 82 ++++---- tests/system/small/test_session.py | 103 ++++++++-- tests/unit/session/test_io_bigquery.py | 121 +++++++----- tests/unit/session/test_read_gbq_table.py | 18 +- .../bigframes_vendored/pandas/io/gbq.py | 6 +- 11 files changed, 364 insertions(+), 184 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af05f4423c6..4fd6488c9c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,8 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.1.1 + rev: v1.10.0 hooks: - id: mypy additional_dependencies: [types-requests, types-tabulate, pandas-stubs] + args: ["--check-untyped-defs", "--explicit-package-bases", '--exclude="^third_party"', "--ignore-missing-imports"] diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 6cd93eec124..6cdb0021f53 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -122,10 +122,10 @@ def __init__( # If no index columns are set, create one. # - # Note: get_index_cols_and_uniqueness in + # Note: get_index_cols in # bigframes/session/_io/bigquery/read_gbq_table.py depends on this # being as sequential integer index column. If this default behavior - # ever changes, please also update get_index_cols_and_uniqueness so + # ever changes, please also update get_index_cols so # that users who explicitly request a sequential integer index can # still get one. if len(index_columns) == 0: diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index 3ad06610b68..cacf86d2345 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -33,7 +33,7 @@ ### Writing SQL Values (literals, column references, table references, etc.) -def simple_literal(value: str | int | bool | float): +def simple_literal(value: str | int | bool | float | datetime.datetime): """Return quoted input string.""" # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#literals if isinstance(value, str): @@ -50,6 +50,8 @@ def simple_literal(value: str | int | bool | float): if value == -math.inf: return 'CAST("-inf" as FLOAT)' return str(value) + if isinstance(value, datetime.datetime): + return f"TIMESTAMP('{value.isoformat()}')" else: raise ValueError(f"Cannot produce literal for {value}") @@ -156,7 +158,3 @@ def ordering_clause( part = f"`{ordering_expr.id}` {asc_desc} {null_clause}" parts.append(part) return f"ORDER BY {' ,'.join(parts)}" - - -def snapshot_clause(time_travel_timestamp: datetime.datetime): - return f"FOR SYSTEM_TIME AS OF TIMESTAMP({repr(time_travel_timestamp.isoformat())})" diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index c18d14b3dd0..3628ecf67b6 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -549,6 +549,7 @@ def read_gbq_query( max_results: Optional[int] = None, use_cache: Optional[bool] = None, col_order: Iterable[str] = (), + filters: vendored_pandas_gbq.FiltersType = (), ) -> bigframes.dataframe.DataFrame: _set_default_session_location_if_possible(query) return global_session.with_default_session( @@ -560,6 +561,7 @@ def read_gbq_query( max_results=max_results, use_cache=use_cache, col_order=col_order, + filters=filters, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 727269e7ee5..89637644cf5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -62,6 +62,7 @@ import ibis import ibis.backends.bigquery as ibis_bigquery import ibis.expr.types as ibis_types +import jellyfish import numpy as np import pandas from pandas._typing import ( @@ -339,19 +340,6 @@ def read_gbq( elif col_order: columns = col_order - filters = list(filters) - if len(filters) != 0 or bf_io_bigquery.is_table_with_wildcard_suffix( - query_or_table - ): - # TODO(b/338111344): This appears to be missing index_cols, which - # are necessary to be selected. - # TODO(b/338039517): Refactor this to be called inside both - # _read_gbq_query and _read_gbq_table (after detecting primary keys) - # so we can make sure index_col/index_cols reflects primary keys. - query_or_table = bf_io_bigquery.to_query( - query_or_table, _to_index_cols(index_col), columns, filters - ) - if bf_io_bigquery.is_query(query_or_table): return self._read_gbq_query( query_or_table, @@ -361,6 +349,7 @@ def read_gbq( max_results=max_results, api_name="read_gbq", use_cache=use_cache, + filters=filters, ) else: if configuration is not None: @@ -377,6 +366,7 @@ def read_gbq( max_results=max_results, api_name="read_gbq", use_cache=use_cache if use_cache is not None else True, + filters=filters, ) def _query_to_destination( @@ -451,6 +441,7 @@ def read_gbq_query( max_results: Optional[int] = None, use_cache: Optional[bool] = None, col_order: Iterable[str] = (), + filters: third_party_pandas_gbq.FiltersType = (), ) -> dataframe.DataFrame: """Turn a SQL query into a DataFrame. @@ -517,6 +508,7 @@ def read_gbq_query( max_results=max_results, api_name="read_gbq_query", use_cache=use_cache, + filters=filters, ) def _read_gbq_query( @@ -529,6 +521,7 @@ def _read_gbq_query( max_results: Optional[int] = None, api_name: str = "read_gbq_query", use_cache: Optional[bool] = None, + filters: third_party_pandas_gbq.FiltersType = (), ) -> dataframe.DataFrame: import bigframes.dataframe as dataframe @@ -557,6 +550,21 @@ def _read_gbq_query( index_cols = _to_index_cols(index_col) + filters = list(filters) + if len(filters) != 0 or max_results is not None: + # TODO(b/338111344): If we are running a query anyway, we might as + # well generate ROW_NUMBER() at the same time. + query = bf_io_bigquery.to_query( + query, + index_cols, + columns, + filters, + max_results=max_results, + # We're executing the query, so we don't need time travel for + # determinism. + time_travel_timestamp=None, + ) + destination, query_job = self._query_to_destination( query, index_cols, @@ -580,12 +588,14 @@ def _read_gbq_query( session=self, ) - return self.read_gbq_table( + return self._read_gbq_table( f"{destination.project}.{destination.dataset_id}.{destination.table_id}", index_col=index_col, columns=columns, - max_results=max_results, use_cache=configuration["query"]["useQueryCache"], + api_name=api_name, + # max_results and filters are omitted because they are already + # handled by to_query(), above. ) def read_gbq_table( @@ -621,24 +631,6 @@ def read_gbq_table( elif col_order: columns = col_order - filters = list(filters) - if len(filters) != 0 or bf_io_bigquery.is_table_with_wildcard_suffix(query): - # TODO(b/338039517): Refactor this to be called inside both - # _read_gbq_query and _read_gbq_table (after detecting primary keys) - # so we can make sure index_col/index_cols reflects primary keys. - query = bf_io_bigquery.to_query( - query, _to_index_cols(index_col), columns, filters - ) - - return self._read_gbq_query( - query, - index_col=index_col, - columns=columns, - max_results=max_results, - api_name="read_gbq_table", - use_cache=use_cache, - ) - return self._read_gbq_table( query=query, index_col=index_col, @@ -646,6 +638,7 @@ def read_gbq_table( max_results=max_results, api_name="read_gbq_table", use_cache=use_cache, + filters=filters, ) def _read_gbq_table( @@ -657,6 +650,7 @@ def _read_gbq_table( max_results: Optional[int] = None, api_name: str, use_cache: bool = True, + filters: third_party_pandas_gbq.FiltersType = (), ) -> dataframe.DataFrame: import bigframes.dataframe as dataframe @@ -673,6 +667,9 @@ def _read_gbq_table( query, default_project=self.bqclient.project ) + columns = list(columns) + filters = list(filters) + # --------------------------------- # Fetch table metadata and validate # --------------------------------- @@ -684,62 +681,110 @@ def _read_gbq_table( cache=self._df_snapshot, use_cache=use_cache, ) + table_column_names = {field.name for field in table.schema} if table.location.casefold() != self._location.casefold(): raise ValueError( f"Current session is in {self._location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" ) - # ----------------------------------------- - # Create Ibis table expression and validate - # ----------------------------------------- - - # Use a time travel to make sure the DataFrame is deterministic, even - # if the underlying table changes. - table_expression = bf_read_gbq_table.get_ibis_time_travel_table( - self.ibis_client, - table_ref, - time_travel_timestamp, - ) - for key in columns: - if key not in table_expression.columns: + if key not in table_column_names: + possibility = min( + table_column_names, + key=lambda item: jellyfish.levenshtein_distance(key, item), + ) raise ValueError( - f"Column '{key}' of `columns` not found in this table." + f"Column '{key}' of `columns` not found in this table. Did you mean '{possibility}'?" ) - # --------------------------------------- - # Create a non-default index and validate - # --------------------------------------- - - # TODO(b/337925142): Move index_cols creation to before we create the - # Ibis table expression so we don't have a "SELECT *" subquery in the - # query that checks for index uniqueness. - - index_cols, is_index_unique = bf_read_gbq_table.get_index_cols_and_uniqueness( - bqclient=self.bqclient, - ibis_client=self.ibis_client, + # Converting index_col into a list of column names requires + # the table metadata because we might use the primary keys + # when constructing the index. + index_cols = bf_read_gbq_table.get_index_cols( table=table, - table_expression=table_expression, index_col=index_col, - api_name=api_name, ) for key in index_cols: - if key not in table_expression.columns: + if key not in table_column_names: + possibility = min( + table_column_names, + key=lambda item: jellyfish.levenshtein_distance(key, item), + ) raise ValueError( - f"Column `{key}` of `index_col` not found in this table." + f"Column '{key}' of `index_col` not found in this table. Did you mean '{possibility}'?" ) - # TODO(b/337925142): We should push down column filters when we get the time - # travel table to avoid "SELECT *" subqueries. - if columns: - table_expression = table_expression.select([*index_cols, *columns]) + # ----------------------------- + # Optionally, execute the query + # ----------------------------- + + # max_results introduces non-determinism and limits the cost on + # clustered tables, so fallback to a query. We do this here so that + # the index is consistent with tables that have primary keys, even + # when max_results is set. + # TODO(b/338419730): We don't need to fallback to a query for wildcard + # tables if we allow some non-determinism when time travel isn't supported. + if max_results is not None or bf_io_bigquery.is_table_with_wildcard_suffix( + query + ): + # TODO(b/338111344): If we are running a query anyway, we might as + # well generate ROW_NUMBER() at the same time. + query = bf_io_bigquery.to_query( + query, + index_cols=index_cols, + columns=columns, + filters=filters, + max_results=max_results, + # We're executing the query, so we don't need time travel for + # determinism. + time_travel_timestamp=None, + ) + + return self._read_gbq_query( + query, + index_col=index_cols, + columns=columns, + api_name="read_gbq_table", + use_cache=use_cache, + ) + + # ----------------------------------------- + # Create Ibis table expression and validate + # ----------------------------------------- + + # Use a time travel to make sure the DataFrame is deterministic, even + # if the underlying table changes. + # TODO(b/340540991): If a dry run query fails with time travel but + # succeeds without it, omit the time travel clause and raise a warning + # about potential non-determinism if the underlying tables are modified. + table_expression = bf_read_gbq_table.get_ibis_time_travel_table( + ibis_client=self.ibis_client, + table_ref=table_ref, + index_cols=index_cols, + columns=columns, + filters=filters, + time_travel_timestamp=time_travel_timestamp, + ) # ---------------------------- # Create ordering and validate # ---------------------------- + # TODO(b/337925142): Generate a new subquery with just the index_cols + # in the Ibis table expression so we don't have a "SELECT *" subquery + # in the query that checks for index uniqueness. + # TODO(b/338065601): Provide a way to assume uniqueness and avoid this + # check. + is_index_unique = bf_read_gbq_table.are_index_cols_unique( + bqclient=self.bqclient, + ibis_client=self.ibis_client, + table=table, + index_cols=index_cols, + api_name=api_name, + ) + if is_index_unique: array_value = bf_read_gbq_table.to_array_value_with_total_ordering( session=self, diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 95ab16fecfd..28eed479657 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -337,6 +337,8 @@ def to_query( index_cols: Iterable[str], columns: Iterable[str], filters: third_party_pandas_gbq.FiltersType, + max_results: Optional[int], + time_travel_timestamp: Optional[datetime.datetime], ) -> str: """Compile query_or_table with conditions(filters, wildcards) to query.""" filters = list(filters) @@ -354,6 +356,15 @@ def to_query( else: select_clause = "SELECT *" + time_travel_clause = "" + if time_travel_timestamp is not None: + time_travel_literal = bigframes.core.sql.simple_literal(time_travel_timestamp) + time_travel_clause = f" FOR SYSTEM_TIME AS OF {time_travel_literal}" + + limit_clause = "" + if max_results is not None: + limit_clause = f" LIMIT {bigframes.core.sql.simple_literal(max_results)}" + filter_string = "" if filters: valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { @@ -382,7 +393,7 @@ def to_query( for filter_item in group: if not isinstance(filter_item, tuple) or (len(filter_item) != 3): raise ValueError( - f"Filter condition should be a tuple of length 3, {filter_item} is not valid." + f"Elements of filters must be tuples of length 3, but got {repr(filter_item)}.", ) column, operator, value = filter_item @@ -419,7 +430,12 @@ def to_query( else: filter_string = and_expression + where_clause = "" if filter_string: - return f"{select_clause} FROM {sub_query} AS sub WHERE {filter_string}" - else: - return f"{select_clause} FROM {sub_query} AS sub" + where_clause = f" WHERE {filter_string}" + + return ( + f"{select_clause} " + f"FROM {sub_query}" + f"{time_travel_clause}{where_clause}{limit_clause}" + ) diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 370ee546d78..87083529cea 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -20,12 +20,12 @@ import datetime import itertools -import textwrap import typing from typing import Dict, Iterable, List, Optional, Tuple import warnings import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions import google.cloud.bigquery as bigquery import ibis @@ -42,7 +42,7 @@ import bigframes.core.ordering as order import bigframes.core.sql import bigframes.dtypes -import bigframes.session._io.bigquery.read_gbq_table +import bigframes.session._io.bigquery import bigframes.session.clients import bigframes.version @@ -126,32 +126,33 @@ def get_table_metadata( return cached_table -def _create_time_travel_sql( - table_ref: bigquery.TableReference, time_travel_timestamp: datetime.datetime -) -> str: - """Query a table via 'time travel' for consistent reads.""" - # If we have an anonymous query results table, it can't be modified and - # there isn't any BigQuery time travel. - selection = bigframes.core.sql.select_table(table_ref) - if table_ref.dataset_id.startswith("_"): - return selection - - return textwrap.dedent( - f""" - {selection} - {bigframes.core.sql.snapshot_clause(time_travel_timestamp)} - """ - ) - - def get_ibis_time_travel_table( ibis_client: ibis.BaseBackend, table_ref: bigquery.TableReference, - time_travel_timestamp: datetime.datetime, + index_cols: Iterable[str], + columns: Iterable[str], + filters: third_party_pandas_gbq.FiltersType, + time_travel_timestamp: Optional[datetime.datetime], ) -> ibis_types.Table: + # If we have an anonymous query results table, it can't be modified and + # there isn't any BigQuery time travel. + if table_ref.dataset_id.startswith("_"): + time_travel_timestamp = None + try: - sql = _create_time_travel_sql(table_ref, time_travel_timestamp) - return ibis_client.sql(sql) + return ibis_client.sql( + bigframes.session._io.bigquery.to_query( + f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}", + index_cols=index_cols, + columns=columns, + filters=filters, + time_travel_timestamp=time_travel_timestamp, + # If we've made it this far, we know we don't have any + # max_results to worry about, because in that case we will + # have executed a query with a LIMI clause. + max_results=None, + ) + ) except google.api_core.exceptions.Forbidden as ex: # Ibis does a dry run to get the types of the columns from the SQL. if "Drive credentials" in ex.message: @@ -159,13 +160,21 @@ def get_ibis_time_travel_table( raise -def _check_index_uniqueness( +def are_index_cols_unique( bqclient: bigquery.Client, ibis_client: ibis.BaseBackend, - table: ibis_types.Table, + table: bigquery.table.Table, index_cols: List[str], api_name: str, ) -> bool: + # If index_cols contain the primary_keys, the query engine assumes they are + # provide a unique index. + primary_keys = frozenset(_get_primary_keys(table)) + if primary_keys <= frozenset(index_cols): + return True + + # TODO(b/337925142): Avoid a "SELECT *" subquery here by ensuring + # table_expression only selects just index_cols. table_sql = ibis_client.compile(table) is_unique_sql = bigframes.core.sql.is_distinct_sql(index_cols, table_sql) job_config = bigquery.QueryJobConfig() @@ -217,14 +226,10 @@ def _is_table_clustered_or_partitioned( return False -def get_index_cols_and_uniqueness( - bqclient: bigquery.Client, - ibis_client: ibis.BaseBackend, +def get_index_cols( table: bigquery.table.Table, - table_expression: ibis_types.Table, index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind, - api_name: str, -) -> Tuple[List[str], bool]: +) -> List[str]: """ If we can get a total ordering from the table, such as via primary key column(s), then return those too so that ordering generation can be @@ -241,7 +246,7 @@ def get_index_cols_and_uniqueness( # Note: This relies on the default behavior of the Block # constructor to create a default sequential index. If that ever # changes, this logic will need to be revisited. - return [], False + return [] else: # Note: It's actually quite difficult to mock this out to unit # test, as it's not possible to subclass enums in Python. See: @@ -278,19 +283,8 @@ def get_index_cols_and_uniqueness( # columns are unique, even if the constraint is not enforced. We make # the same assumption and use these columns as the total ordering keys. index_cols = primary_keys - is_index_unique = len(index_cols) != 0 - else: - is_index_unique = _check_index_uniqueness( - bqclient=bqclient, - ibis_client=ibis_client, - # TODO(b/337925142): Avoid a "SELECT *" subquery here by using - # _create_time_travel_sql with just index_cols. - table=table_expression, - index_cols=index_cols, - api_name=api_name, - ) - return index_cols, is_index_unique + return index_cols def get_time_travel_datetime_and_table_metadata( diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 052ef273841..2b7c6178ff2 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -14,11 +14,12 @@ import io import random +import re import tempfile import textwrap import time import typing -from typing import List, Sequence +from typing import List, Optional, Sequence import google import google.cloud.bigquery as bigquery @@ -68,15 +69,6 @@ def test_read_gbq_tokyo( ["my_strings"], id="one_cols_in_query", ), - pytest.param( - "{scalars_table_id}", - ["unknown"], - marks=pytest.mark.xfail( - raises=ValueError, - reason="Column `unknown` not found in this table.", - ), - id="unknown_col", - ), ], ) def test_read_gbq_w_columns( @@ -91,6 +83,38 @@ def test_read_gbq_w_columns( assert df.columns.tolist() == columns +def test_read_gbq_w_unknown_column( + session: bigframes.Session, + scalars_table_id: str, +): + with pytest.raises( + ValueError, + match=re.escape( + "Column 'int63_col' of `columns` not found in this table. Did you mean 'int64_col'?" + ), + ): + session.read_gbq( + scalars_table_id, + columns=["string_col", "int63_col", "bool_col"], + ) + + +def test_read_gbq_w_unknown_index_col( + session: bigframes.Session, + scalars_table_id: str, +): + with pytest.raises( + ValueError, + match=re.escape( + "Column 'int64_two' of `index_col` not found in this table. Did you mean 'int64_too'?" + ), + ): + session.read_gbq( + scalars_table_id, + index_col=["int64_col", "int64_two"], + ) + + @pytest.mark.parametrize( ("query_or_table", "index_col"), [ @@ -248,6 +272,9 @@ def test_read_gbq_w_primary_keys_table( df = session.read_gbq(f"{table.project}.{table.dataset_id}.{table.table_id}") result = df.head(100).to_pandas() + # Verify that primary keys are used as the index. + assert list(result.index.names) == list(primary_keys) + # Verify that the DataFrame is already sorted by primary keys. sorted_result = result.sort_values(primary_keys) pd.testing.assert_frame_equal(result, sorted_result) @@ -256,6 +283,39 @@ def test_read_gbq_w_primary_keys_table( assert "FOR SYSTEM_TIME AS OF TIMESTAMP" in df.sql +def test_read_gbq_w_primary_keys_table_and_filters( + session: bigframes.Session, usa_names_grouped_table: bigquery.Table +): + """ + Verify fix for internal issue 338039517, where using filters didn't use the + primary keys for indexing / ordering. + """ + # Validate that the table we're querying has a primary key. + table = usa_names_grouped_table + table_constraints = table.table_constraints + assert table_constraints is not None + primary_key = table_constraints.primary_key + assert primary_key is not None + primary_keys = primary_key.columns + assert len(primary_keys) != 0 + + df = session.read_gbq( + f"{table.project}.{table.dataset_id}.{table.table_id}", + filters=[ + ("name", "LIKE", "W%"), + ("total_people", ">", 100), + ], # type: ignore + ) + result = df.to_pandas() + + # Verify that primary keys are used as the index. + assert list(result.index.names) == list(primary_keys) + + # Verify that the DataFrame is already sorted by primary keys. + sorted_result = result.sort_values(primary_keys) + pd.testing.assert_frame_equal(result, sorted_result) + + @pytest.mark.parametrize( ("query_or_table", "max_results"), [ @@ -350,13 +410,14 @@ def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): ["read_gbq", "read_gbq_table"], ) @pytest.mark.parametrize( - ("filters", "table_id", "index_col", "columns"), + ("filters", "table_id", "index_col", "columns", "max_results"), [ pytest.param( [("_table_suffix", ">=", "1930"), ("_table_suffix", "<=", "1939")], _GSOD_ALL_TABLES, ["stn", "wban", "year", "mo", "da"], ["temp", "max", "min"], + 100, id="all", ), pytest.param( @@ -364,6 +425,7 @@ def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): _GSOD_1930S, (), # index_col ["temp", "max", "min"], + None, # max_results id="columns", ), pytest.param( @@ -371,6 +433,7 @@ def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): _GSOD_ALL_TABLES, (), # index_col, (), # columns + None, # max_results id="filters", ), pytest.param( @@ -378,8 +441,17 @@ def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): _GSOD_1930S, ["stn", "wban", "year", "mo", "da"], (), # columns + None, # max_results id="index_col", ), + pytest.param( + (), # filters + _GSOD_1930S, + (), # index_col + (), # columns + 100, # max_results + id="max_results", + ), ], ) def test_read_gbq_wildcard( @@ -389,10 +461,17 @@ def test_read_gbq_wildcard( table_id: str, index_col: Sequence[str], columns: Sequence[str], + max_results: Optional[int], ): table_metadata = session.bqclient.get_table(table_id) method = getattr(session, api_method) - df = method(table_id, filters=filters, index_col=index_col, columns=columns) + df = method( + table_id, + filters=filters, + index_col=index_col, + columns=columns, + max_results=max_results, + ) num_rows, num_columns = df.shape if index_col: diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 57f9e003636..8ba13a72762 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -13,6 +13,7 @@ # limitations under the License. import datetime +import re from typing import Iterable import google.cloud.bigquery as bigquery @@ -205,19 +206,16 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) @pytest.mark.parametrize( - ("query_or_table", "index_cols", "columns", "filters", "expected_output"), + ( + "query_or_table", + "index_cols", + "columns", + "filters", + "max_results", + "time_travel_timestamp", + "expected_output", + ), [ - pytest.param( - "test_table", - [], - [], - ["date_col", ">", "2022-10-20"], - None, - marks=pytest.mark.xfail( - raises=ValueError, - ), - id="raise_error", - ), pytest.param( "test_table", ["row_index"], @@ -226,30 +224,42 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) (("rowindex", "not in", [0, 6]),), (("string_col", "in", ["Hello, World!", "こんにちは"]),), ], + 123, # max_results, + datetime.datetime(2024, 5, 14, 12, 42, 36, 125125), ( - "SELECT `row_index`, `string_col` FROM `test_table` AS sub WHERE " - "`rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " - "'こんにちは')" + "SELECT `row_index`, `string_col` FROM `test_table` " + "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125') " + "WHERE `rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " + "'こんにちは') LIMIT 123" ), id="table-all_params-filter_or_operation", ), pytest.param( - """SELECT - rowindex, - string_col, - FROM `test_table` AS t - """, + ( + """SELECT + rowindex, + string_col, + FROM `test_table` AS t + """ + ), ["rowindex"], ["string_col"], [ ("rowindex", "<", 4), ("string_col", "==", "Hello, World!"), ], - """SELECT `rowindex`, `string_col` FROM (SELECT - rowindex, - string_col, - FROM `test_table` AS t - ) AS sub WHERE `rowindex` < 4 AND `string_col` = \'Hello, World!\'""", + 123, # max_results, + datetime.datetime(2024, 5, 14, 12, 42, 36, 125125), + ( + """SELECT `rowindex`, `string_col` FROM (SELECT + rowindex, + string_col, + FROM `test_table` AS t + ) """ + "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125') " + "WHERE `rowindex` < 4 AND `string_col` = 'Hello, World!' " + "LIMIT 123" + ), id="subquery-all_params-filter_and_operation", ), pytest.param( @@ -257,7 +267,9 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) [], ["col_a", "col_b"], [], - "SELECT `col_a`, `col_b` FROM `test_table` AS sub", + None, # max_results + None, # time_travel_timestampe + "SELECT `col_a`, `col_b` FROM `test_table`", id="table-columns", ), pytest.param( @@ -265,7 +277,9 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) [], [], [("date_col", ">", "2022-10-20")], - "SELECT * FROM `test_table` AS sub WHERE `date_col` > '2022-10-20'", + None, # max_results + None, # time_travel_timestampe + "SELECT * FROM `test_table` WHERE `date_col` > '2022-10-20'", id="table-filter", ), pytest.param( @@ -273,7 +287,9 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) [], [], [], - "SELECT * FROM `test_table*` AS sub", + None, # max_results + None, # time_travel_timestampe + "SELECT * FROM `test_table*`", id="wildcard-no_params", ), pytest.param( @@ -281,30 +297,49 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) [], [], [("_TABLE_SUFFIX", ">", "2022-10-20")], - "SELECT * FROM `test_table*` AS sub WHERE `_TABLE_SUFFIX` > '2022-10-20'", + None, # max_results + None, # time_travel_timestampe + "SELECT * FROM `test_table*` WHERE `_TABLE_SUFFIX` > '2022-10-20'", id="wildcard-filter", ), ], ) -def test_to_query(query_or_table, index_cols, columns, filters, expected_output): +def test_to_query( + query_or_table, + index_cols, + columns, + filters, + max_results, + time_travel_timestamp, + expected_output, +): query = io_bq.to_query( query_or_table, - index_cols, - columns, - filters, + index_cols=index_cols, + columns=columns, + filters=filters, + max_results=max_results, + time_travel_timestamp=time_travel_timestamp, ) assert query == expected_output @pytest.mark.parametrize( - ("query_or_table", "filters", "expected_output"), - [], + ("filters", "expected_message"), + ( + pytest.param( + ["date_col", ">", "2022-10-20"], + "Elements of filters must be tuples of length 3, but got 'd'", + ), + ), ) -def test_to_query_with_wildcard_table(query_or_table, filters, expected_output): - query = io_bq.to_query( - query_or_table, - (), # index_cols - (), # columns - filters, - ) - assert query == expected_output +def test_to_query_fails_with_bad_filters(filters, expected_message): + with pytest.raises(ValueError, match=re.escape(expected_message)): + io_bq.to_query( + "test_table", + index_cols=(), + columns=(), + filters=filters, + max_results=None, + time_travel_timestamp=None, + ) diff --git a/tests/unit/session/test_read_gbq_table.py b/tests/unit/session/test_read_gbq_table.py index 1d09769aecc..5afd6bb1355 100644 --- a/tests/unit/session/test_read_gbq_table.py +++ b/tests/unit/session/test_read_gbq_table.py @@ -20,18 +20,28 @@ import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table +from .. import resources + + +def test_get_ibis_time_travel_table_doesnt_timetravel_anonymous_datasets(): + bqsession = resources.create_bigquery_session() -def test_create_snapshot_sql_doesnt_timetravel_anonymous_datasets(): table_ref = bigquery.TableReference.from_string( "my-test-project._e8166e0cdb.anonbb92cd" ) - sql = bf_read_gbq_table._create_time_travel_sql( - table_ref, datetime.datetime.now(datetime.timezone.utc) + table_expression = bf_read_gbq_table.get_ibis_time_travel_table( + bqsession.ibis_client, + table_ref, + index_cols=(), + columns=(), + filters=(), + time_travel_timestamp=datetime.datetime.now(datetime.timezone.utc), ) + sql = table_expression.compile() # Anonymous query results tables don't support time travel. assert "SYSTEM_TIME" not in sql # Need fully-qualified table name. - assert "`my-test-project`.`_e8166e0cdb`.`anonbb92cd`" in sql + assert "my-test-project" in sql diff --git a/third_party/bigframes_vendored/pandas/io/gbq.py b/third_party/bigframes_vendored/pandas/io/gbq.py index 38ea208eafa..47a6013c4c2 100644 --- a/third_party/bigframes_vendored/pandas/io/gbq.py +++ b/third_party/bigframes_vendored/pandas/io/gbq.py @@ -96,15 +96,15 @@ def read_gbq( Reading data with `columns` and `filters` parameters: >>> columns = ['pitcherFirstName', 'pitcherLastName', 'year', 'pitchSpeed'] - >>> filters = [('year', '==', 2016), ('pitcherFirstName', 'in', ['John', 'Doe']), ('pitcherLastName', 'in', ['Gant'])] + >>> filters = [('year', '==', 2016), ('pitcherFirstName', 'in', ['John', 'Doe']), ('pitcherLastName', 'in', ['Gant']), ('pitchSpeed', '>', 94)] >>> df = bpd.read_gbq( ... "bigquery-public-data.baseball.games_wide", ... columns=columns, ... filters=filters, ... ) >>> df.head(1) - pitcherFirstName pitcherLastName year pitchSpeed - 0 John Gant 2016 82 + pitcherFirstName pitcherLastName year pitchSpeed + 0 John Gant 2016 95 [1 rows x 4 columns] From ca284cc21d5386b4817ecab85b68235456361815 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 17 May 2024 10:10:53 -0700 Subject: [PATCH 580/921] chore: clean up type errors in test_api_primitives (#696) --- tests/unit/ml/test_api_primitives.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/ml/test_api_primitives.py b/tests/unit/ml/test_api_primitives.py index a5b466cf951..00a51ccfe93 100644 --- a/tests/unit/ml/test_api_primitives.py +++ b/tests/unit/ml/test_api_primitives.py @@ -31,8 +31,8 @@ def test_base_estimator_repr(): assert estimator.__repr__() == "LinearRegression()" # TODO(b/340891292): fix type error - estimator = bigframes.ml.decomposition.PCA(n_components=7) # type: ignore - assert estimator.__repr__() == "PCA(n_components=7)" + pca_estimator = bigframes.ml.decomposition.PCA(n_components=7) + assert pca_estimator.__repr__() == "PCA(n_components=7)" @pytest.mark.skipif(sklearn_linear_model is None, reason="requires sklearn") @@ -50,6 +50,6 @@ def test_base_estimator_repr_matches_sklearn(): assert estimator.__repr__() == sklearn_estimator.__repr__() # TODO(b/340891292): fix type error - estimator = bigframes.ml.decomposition.PCA(n_components=7) # type: ignore + pca_estimator = bigframes.ml.decomposition.PCA(n_components=7) sklearn_estimator = sklearn_decomposition.PCA(n_components=7) - assert estimator.__repr__() == sklearn_estimator.__repr__() + assert pca_estimator.__repr__() == sklearn_estimator.__repr__() From 6e0f4d8c76f78dc26f4aa1880dd67ebdb638bb5e Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 17 May 2024 11:01:06 -0700 Subject: [PATCH 581/921] fix: properly identify non-unique index in tables without primary keys (#699) * fix: Properly identify non-unique index in non-pk tables * fix previously unused code * fix unit tests * fix one more unit test * fix index uniqueness under no index * add unit test --- bigframes/core/compile/compiled.py | 2 +- bigframes/core/sql.py | 19 +++-- bigframes/session/__init__.py | 1 - .../session/_io/bigquery/read_gbq_table.py | 8 +-- tests/unit/session/test_read_gbq_table.py | 71 +++++++++++++++++++ tests/unit/session/test_session.py | 5 ++ 6 files changed, 96 insertions(+), 10 deletions(-) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 1c2217c25aa..552061f6127 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -897,7 +897,7 @@ def to_sql( output_columns = [ col_id_overrides.get(col, col) for col in baked_ir.column_ids ] - sql = bigframes.core.sql.select_from(output_columns, sql) + sql = bigframes.core.sql.select_from_subquery(output_columns, sql) # Single row frames may not have any ordering columns if len(baked_ir._ordering.all_ordering_columns) > 0: diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index cacf86d2345..c1e319b8607 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -111,7 +111,7 @@ def infix_op(opname: str, left_arg: str, right_arg: str): ### Writing SELECT expressions -def select_from(columns: Iterable[str], subquery: str, distinct: bool = False): +def select_from_subquery(columns: Iterable[str], subquery: str, distinct: bool = False): selection = ", ".join(map(identifier, columns)) distinct_clause = "DISTINCT " if distinct else "" @@ -120,16 +120,27 @@ def select_from(columns: Iterable[str], subquery: str, distinct: bool = False): ) +def select_from_table_ref( + columns: Iterable[str], table_ref: bigquery.TableReference, distinct: bool = False +): + selection = ", ".join(map(identifier, columns)) + distinct_clause = "DISTINCT " if distinct else "" + + return textwrap.dedent( + f"SELECT {distinct_clause}{selection}\nFROM {table_reference(table_ref)}" + ) + + def select_table(table_ref: bigquery.TableReference): return textwrap.dedent(f"SELECT * FROM {table_reference(table_ref)}") -def is_distinct_sql(columns: Iterable[str], table_sql: str) -> str: +def is_distinct_sql(columns: Iterable[str], table_ref: bigquery.TableReference) -> str: is_unique_sql = f"""WITH full_table AS ( - {select_from(columns, table_sql)} + {select_from_table_ref(columns, table_ref)} ), distinct_table AS ( - {select_from(columns, table_sql, distinct=True)} + {select_from_table_ref(columns, table_ref, distinct=True)} ) SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 89637644cf5..ab53de80419 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -779,7 +779,6 @@ def _read_gbq_table( # check. is_index_unique = bf_read_gbq_table.are_index_cols_unique( bqclient=self.bqclient, - ibis_client=self.ibis_client, table=table, index_cols=index_cols, api_name=api_name, diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 87083529cea..92e007c804b 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -162,21 +162,21 @@ def get_ibis_time_travel_table( def are_index_cols_unique( bqclient: bigquery.Client, - ibis_client: ibis.BaseBackend, table: bigquery.table.Table, index_cols: List[str], api_name: str, ) -> bool: + if len(index_cols) == 0: + return False # If index_cols contain the primary_keys, the query engine assumes they are # provide a unique index. primary_keys = frozenset(_get_primary_keys(table)) - if primary_keys <= frozenset(index_cols): + if (len(primary_keys) > 0) and primary_keys <= frozenset(index_cols): return True # TODO(b/337925142): Avoid a "SELECT *" subquery here by ensuring # table_expression only selects just index_cols. - table_sql = ibis_client.compile(table) - is_unique_sql = bigframes.core.sql.is_distinct_sql(index_cols, table_sql) + is_unique_sql = bigframes.core.sql.is_distinct_sql(index_cols, table.reference) job_config = bigquery.QueryJobConfig() job_config.labels["bigframes-api"] = api_name results = bqclient.query_and_wait(is_unique_sql, job_config=job_config) diff --git a/tests/unit/session/test_read_gbq_table.py b/tests/unit/session/test_read_gbq_table.py index 5afd6bb1355..52c86cd1e43 100644 --- a/tests/unit/session/test_read_gbq_table.py +++ b/tests/unit/session/test_read_gbq_table.py @@ -15,8 +15,11 @@ """Unit tests for read_gbq_table helper functions.""" import datetime +import unittest.mock as mock +import google.cloud.bigquery import google.cloud.bigquery as bigquery +import pytest import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table @@ -45,3 +48,71 @@ def test_get_ibis_time_travel_table_doesnt_timetravel_anonymous_datasets(): # Need fully-qualified table name. assert "my-test-project" in sql + + +@pytest.mark.parametrize( + ("index_cols", "primary_keys", "values_distinct", "expected"), + ( + (["col1", "col2"], ["col1", "col2", "col3"], False, False), + (["col1", "col2", "col3"], ["col1", "col2", "col3"], True, True), + ( + ["col2", "col3", "col1"], + [ + "col3", + "col2", + ], + True, + True, + ), + (["col1", "col2"], [], False, False), + ([], ["col1", "col2", "col3"], False, False), + ([], [], False, False), + ), +) +def test_are_index_cols_unique(index_cols, primary_keys, values_distinct, expected): + """If a primary key is set on the table, we use that as the index column + by default, no error should be raised in this case. + + See internal issue 335727141. + """ + table = google.cloud.bigquery.Table.from_api_repr( + { + "tableReference": { + "projectId": "my-project", + "datasetId": "my_dataset", + "tableId": "my_table", + }, + "clustering": { + "fields": ["col1", "col2"], + }, + }, + ) + table.schema = ( + google.cloud.bigquery.SchemaField("col1", "INT64"), + google.cloud.bigquery.SchemaField("col2", "INT64"), + google.cloud.bigquery.SchemaField("col3", "INT64"), + google.cloud.bigquery.SchemaField("col4", "INT64"), + ) + + # TODO(b/305264153): use setter for table_constraints in client library + # when available. + table._properties["tableConstraints"] = { + "primaryKey": { + "columns": primary_keys, + }, + } + bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) + bqclient.project = "test-project" + bqclient.get_table.return_value = table + + bqclient.query_and_wait.return_value = ( + {"total_count": 3, "distinct_count": 3 if values_distinct else 2}, + ) + session = resources.create_bigquery_session( + bqclient=bqclient, table_schema=table.schema + ) + table._properties["location"] = session._location + + result = bf_read_gbq_table.are_index_cols_unique(bqclient, table, index_cols, "") + + assert result == expected diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index bea858e0377..4f5daebc879 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -179,6 +179,9 @@ def get_table_mock(table_ref): return table session.bqclient.get_table = get_table_mock + session.bqclient.query_and_wait.return_value = ( + {"total_count": 3, "distinct_count": 2}, + ) with pytest.warns(UserWarning, match=re.escape("use_cache=False")): df = session.read_gbq("my-project.my_dataset.my_table") @@ -200,6 +203,7 @@ def test_default_index_warning_raised_by_read_gbq(table): bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" bqclient.get_table.return_value = table + bqclient.query_and_wait.return_value = ({"total_count": 3, "distinct_count": 2},) session = resources.create_bigquery_session(bqclient=bqclient) table._properties["location"] = session._location @@ -222,6 +226,7 @@ def test_default_index_warning_not_raised_by_read_gbq_index_col_sequential_int64 bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" bqclient.get_table.return_value = table + bqclient.query_and_wait.return_value = ({"total_count": 4, "distinct_count": 3},) session = resources.create_bigquery_session(bqclient=bqclient) table._properties["location"] = session._location From 14d81c17505f9a09439a874ff855aec6f95fc0d1 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 17 May 2024 11:14:52 -0700 Subject: [PATCH 582/921] fix: fix type error in test_cluster (#698) --- tests/system/large/ml/test_cluster.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/system/large/ml/test_cluster.py b/tests/system/large/ml/test_cluster.py index 454c94599d6..152fd168be5 100644 --- a/tests/system/large/ml/test_cluster.py +++ b/tests/system/large/ml/test_cluster.py @@ -103,10 +103,10 @@ def test_cluster_configure_fit_score_predict( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_cluster_model", replace=True ) - # TODO(b/340875247): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_cluster_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_clusters == 3 assert reloaded_model.init == "RANDOM" @@ -144,10 +144,10 @@ def test_cluster_configure_fit_load_params(penguins_df_default_index, dataset_id reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_cluster_model", replace=True ) - # TODO(b/340875247): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_cluster_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_clusters == 4 assert reloaded_model.init == "RANDOM" From ac1a188ff52c603151ce760cbe78a552962c6231 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Fri, 17 May 2024 15:28:55 -0700 Subject: [PATCH 583/921] chore: type error in test_ensemble (#700) * fix: ignore type error in plotting --- tests/system/large/ml/test_ensemble.py | 37 +++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index b71d0233027..e00f7fa665e 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -46,10 +46,10 @@ def test_xgbregressor_default_params(penguins_df_default_index, dataset_id): reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbregressor_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_xgbregressor_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) @@ -98,10 +98,10 @@ def test_xgbregressor_dart_booster_multiple_params( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbregressor_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_xgbregressor_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.booster == "DART" assert reloaded_model.dart_normalized_type == "TREE" @@ -148,10 +148,10 @@ def test_xgbclassifier_default_params(penguins_df_default_index, dataset_id): reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbclassifier_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_xgbclassifier_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) @@ -199,10 +199,10 @@ def test_xgbclassifier_dart_booster_multiple_params( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_xgbclassifier_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_xgbclassifier_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.booster == "DART" assert reloaded_model.dart_normalized_type == "TREE" @@ -250,10 +250,10 @@ def test_randomforestregressor_default_params(penguins_df_default_index, dataset reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestregressor_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_randomforestregressor_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) @@ -297,10 +297,10 @@ def test_randomforestregressor_multiple_params(penguins_df_default_index, datase reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestregressor_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_randomforestregressor_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.tree_method == "AUTO" assert reloaded_model.colsample_bytree == 0.95 @@ -344,18 +344,17 @@ def test_randomforestclassifier_default_params(penguins_df_default_index, datase reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestclassifier_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_randomforestclassifier_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) @pytest.mark.flaky(retries=2) def test_randomforestclassifier_multiple_params(penguins_df_default_index, dataset_id): - # TODO(b/340888645): fix type error model = bigframes.ml.ensemble.RandomForestClassifier( - tree_method="AUTO", # type: ignore + tree_method="auto", min_tree_child_weight=2, colsample_bytree=0.95, colsample_bylevel=0.95, @@ -391,12 +390,12 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_randomforestclassifier_model", replace=True ) - # TODO(b/340888645): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_randomforestclassifier_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) - assert reloaded_model.tree_method == "AUTO" + assert reloaded_model.tree_method == "auto" assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 assert reloaded_model.colsample_bynode == 0.95 From 66d69794dd37823fd51dcd5d5d0d4608eb22eab6 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Mon, 20 May 2024 08:36:16 -0700 Subject: [PATCH 584/921] chore: type errors in bigframes.ml tests (#702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #340890167, 340891711, 340876028🦕 --- tests/system/large/ml/test_decomposition.py | 16 ++++++++-------- tests/system/large/ml/test_forecasting.py | 4 ++-- tests/system/large/ml/test_linear_model.py | 16 ++++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/system/large/ml/test_decomposition.py b/tests/system/large/ml/test_decomposition.py index 4312c241257..49aa9851898 100644 --- a/tests/system/large/ml/test_decomposition.py +++ b/tests/system/large/ml/test_decomposition.py @@ -64,10 +64,10 @@ def test_decomposition_configure_fit_score_predict( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) - # TODO(b/340876028): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_components == 3 @@ -120,10 +120,10 @@ def test_decomposition_configure_fit_score_predict_params( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) - # TODO(b/340876028): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_components == 5 assert reloaded_model.svd_solver == "RANDOMIZED" @@ -139,10 +139,10 @@ def test_decomposition_configure_fit_load_float_component( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) - # TODO(b/340876028): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_components == 0.2 @@ -157,9 +157,9 @@ def test_decomposition_configure_fit_load_none_component( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_pca_model", replace=True ) - # TODO(b/340876028): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_pca_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.n_components == 7 diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index be7cf8c93db..79deb615b1b 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -119,9 +119,9 @@ def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): # save, load to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) - # TODO(b/340891711): fix type error + assert reloaded_model._bqml_model is not None assert ( - f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name # type: ignore + f"{dataset_id}.temp_arima_plus_model" in reloaded_model._bqml_model.model_name ) assert reloaded_model.horizon == 100 diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index f1f79852785..eaf666fd50a 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -41,9 +41,9 @@ def test_linear_regression_configure_fit_score(penguins_df_default_index, datase # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) - # TODO(b/340890167): fix type error + assert reloaded_model._bqml_model is not None assert ( - f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name # type: ignore + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name ) assert reloaded_model.optimize_strategy == "NORMAL_EQUATION" assert reloaded_model.fit_intercept is True @@ -94,9 +94,9 @@ def test_linear_regression_customized_params_fit_score( # save, load, check parameters to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) - # TODO(b/340890167): fix type error + assert reloaded_model._bqml_model is not None assert ( - f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name # type: ignore + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name ) assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" assert reloaded_model.fit_intercept is False @@ -141,10 +141,10 @@ def test_logistic_regression_configure_fit_score(penguins_df_default_index, data reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_logistic_reg_model", replace=True ) - # TODO(b/340890167): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_logistic_reg_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) assert reloaded_model.fit_intercept is True assert reloaded_model.class_weight is None @@ -187,10 +187,10 @@ def test_logistic_regression_customized_params_fit_score( reloaded_model = model.to_gbq( f"{dataset_id}.temp_configured_logistic_reg_model", replace=True ) - # TODO(b/340890167): fix type error + assert reloaded_model._bqml_model is not None assert ( f"{dataset_id}.temp_configured_logistic_reg_model" - in reloaded_model._bqml_model.model_name # type: ignore + in reloaded_model._bqml_model.model_name ) # TODO(garrettwu) optimize_strategy isn't logged in BQML # assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" From 29e4886d41e3d615bc493cf3a104ef1b0698ece8 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 20 May 2024 13:51:49 -0700 Subject: [PATCH 585/921] feat: add `DefaultIndexKind.NULL` to use as `index_col` in `read_gbq*`, creating an indexless DataFrame/Series (#662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Support indexless dataframe/series * fixes for kurt, skew, median * fix unit tests * fix more issues * fix defaulting to primary key logic * fix tests * many small changes * fix accidental null indexes and raising warning * fix df quantile index * disable legacy pandas for some tests, add concat test * fix series repr * Update bigframes/session/__init__.py Co-authored-by: Tim Sweña (Swast) * Update bigframes/core/rewrite.py Co-authored-by: Tim Sweña (Swast) * Update bigframes/core/rewrite.py Co-authored-by: Tim Sweña (Swast) * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * pr comments addressed --------- Co-authored-by: Tim Sweña (Swast) Co-authored-by: Owl Bot --- bigframes/core/__init__.py | 13 ++ bigframes/core/block_transforms.py | 19 +- bigframes/core/blocks.py | 135 +++++++++-- bigframes/core/indexes/base.py | 5 + bigframes/core/rewrite.py | 38 +++- bigframes/dataframe.py | 70 +++++- bigframes/enums.py | 2 + bigframes/exceptions.py | 4 + bigframes/operations/base.py | 16 ++ bigframes/series.py | 40 +++- bigframes/session/__init__.py | 19 +- .../session/_io/bigquery/read_gbq_table.py | 6 +- tests/system/conftest.py | 10 + tests/system/small/test_empty_index.py | 212 ++++++++++++++++++ tests/system/small/test_series.py | 6 +- 15 files changed, 528 insertions(+), 67 deletions(-) create mode 100644 tests/system/small/test_empty_index.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 79c6bb6495f..0a2936419f8 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -456,6 +456,19 @@ def join( return ArrayValue(bigframes.core.rewrite.maybe_rewrite_join(join_node)) return ArrayValue(join_node) + def try_align_as_projection( + self, + other: ArrayValue, + join_type: join_def.JoinType, + mappings: typing.Tuple[join_def.JoinColumnMapping, ...], + ) -> typing.Optional[ArrayValue]: + left_side = bigframes.core.rewrite.SquashedSelect.from_node(self.node) + right_side = bigframes.core.rewrite.SquashedSelect.from_node(other.node) + result = left_side.maybe_merge(right_side, join_type, mappings) + if result is not None: + return ArrayValue(result.expand()) + return None + def explode(self, column_ids: typing.Sequence[str]) -> ArrayValue: assert len(column_ids) > 0 for column_id in column_ids: diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index e12e6bf054a..eaee2e2cc06 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -597,9 +597,11 @@ def skew( block = block.select_columns(skew_ids).with_column_labels(column_labels) if not grouping_column_ids: - # When ungrouped, stack everything into single column so can be returned as series - block = block.stack() - block = block.drop_levels([block.index_columns[0]]) + # When ungrouped, transpose result row into a series + # perform transpose last, so as to not invalidate cache + block, index_col = block.create_constant(None, None) + block = block.set_index([index_col]) + return block.transpose(original_row_index=pd.Index([None])) return block @@ -637,9 +639,11 @@ def kurt( block = block.select_columns(kurt_ids).with_column_labels(column_labels) if not grouping_column_ids: - # When ungrouped, stack everything into single column so can be returned as series - block = block.stack() - block = block.drop_levels([block.index_columns[0]]) + # When ungrouped, transpose result row into a series + # perform transpose last, so as to not invalidate cache + block, index_col = block.create_constant(None, None) + block = block.set_index([index_col]) + return block.transpose(original_row_index=pd.Index([None])) return block @@ -820,7 +824,8 @@ def idxmax(block: blocks.Block) -> blocks.Block: def _idx_extrema( block: blocks.Block, min_or_max: typing.Literal["min", "max"] ) -> blocks.Block: - if len(block.index_columns) != 1: + block._throw_if_null_index("idx") + if len(block.index_columns) > 1: # TODO: Need support for tuple dtype raise NotImplementedError( f"idxmin not support for multi-index. {constants.FEEDBACK_LINK}" diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 6cdb0021f53..0bbb8a0b61c 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -51,6 +51,7 @@ import bigframes.core.utils as utils import bigframes.core.window_spec as window_specs import bigframes.dtypes +import bigframes.exceptions import bigframes.features import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops @@ -120,19 +121,11 @@ def __init__( f"'index_columns' (size {len(index_columns)}) and 'index_labels' (size {len(index_labels)}) must have equal length" ) - # If no index columns are set, create one. - # - # Note: get_index_cols in - # bigframes/session/_io/bigquery/read_gbq_table.py depends on this - # being as sequential integer index column. If this default behavior - # ever changes, please also update get_index_cols so - # that users who explicitly request a sequential integer index can - # still get one. if len(index_columns) == 0: - new_index_col_id = guid.generate_guid() - expr = expr.promote_offsets(new_index_col_id) - index_columns = [new_index_col_id] - + warnings.warn( + "Creating object with Null Index. Null Index is a preview feature.", + category=bigframes.exceptions.PreviewWarning, + ) self._index_columns = tuple(index_columns) # Index labels don't need complicated hierarchical access so can store as tuple self._index_labels = ( @@ -517,7 +510,8 @@ def _copy_index_to_pandas(self, df: pd.DataFrame): Warning: This method modifies ``df`` inplace. """ - if self.index_columns: + # Note: If BigQuery DataFrame has null index, a default one will be created for the local materialization. + if len(self.index_columns) > 0: df.set_index(list(self.index_columns), inplace=True) # Pandas names is annotated as list[str] rather than the more # general Sequence[Label] that BigQuery DataFrames has. @@ -1093,16 +1087,25 @@ def aggregate( aggregate_labels = self._get_labels_for_columns( [agg[0] for agg in aggregations] ) + names: typing.List[Label] = [] - for by_col_id in by_column_ids: - if by_col_id in self.value_columns: - names.append(self.col_id_to_label[by_col_id]) - else: - names.append(self.col_id_to_index_name[by_col_id]) + if len(by_column_ids) == 0: + label_id = guid.generate_guid() + result_expr = result_expr.assign_constant(label_id, 0, pd.Int64Dtype()) + index_columns = (label_id,) + names = [None] + else: + index_columns = tuple(by_column_ids) # type: ignore + for by_col_id in by_column_ids: + if by_col_id in self.value_columns: + names.append(self.col_id_to_label[by_col_id]) + else: + names.append(self.col_id_to_index_name[by_col_id]) + return ( Block( result_expr, - index_columns=by_column_ids, + index_columns=index_columns, column_labels=aggregate_labels, index_labels=names, ), @@ -1256,11 +1259,12 @@ def explode( expr = self.expr.explode(column_ids) if ignore_index: + new_index_ids = guid.generate_guid() return Block( - expr.drop_columns(self.index_columns), + expr.drop_columns(self.index_columns).promote_offsets(new_index_ids), column_labels=self.column_labels, # Initiates default index creation using the block constructor. - index_columns=[], + index_columns=[new_index_ids], ) else: return Block( @@ -1423,7 +1427,8 @@ def retrieve_repr_request_results( computed_df, query_job = head_block.to_pandas() formatted_df = computed_df.set_axis(self.column_labels, axis=1) # we reset the axis and substitute the bf index name(s) for the default - formatted_df.index.names = self.index.names # type: ignore + if len(self.index.names) > 0: + formatted_df.index.names = self.index.names # type: ignore return formatted_df, count, query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: @@ -1907,9 +1912,26 @@ def join( other: Block, *, how="left", - sort=False, + sort: bool = False, block_identity_join: bool = False, ) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: + """ + Join two blocks objects together, and provide mappings between source columns and output columns. + + Args: + other (Block): + The right operand of the join operation + how (str): + Describes the join type. 'inner', 'outer', 'left', or 'right' + sort (bool): + if true will sort result by index + block_identity_join (bool): + If true, will not convert join to a projection (implicitly assuming unique indices) + + Returns: + Block, (left_mapping, right_mapping): Result block and mappers from input column ids to result column ids. + """ + if not isinstance(other, Block): # TODO(swast): We need to improve this error message to be more # actionable for the user. For example, it's possible they @@ -1923,6 +1945,16 @@ def join( raise NotImplementedError( f"Only how='outer','left','right','inner' currently supported. {constants.FEEDBACK_LINK}" ) + # Special case for null index, + if ( + (self.index.nlevels == other.index.nlevels == 0) + and not sort + and not block_identity_join + ): + return join_indexless(self, other, how=how) + + self._throw_if_null_index("join") + other._throw_if_null_index("join") if self.index.nlevels == other.index.nlevels == 1: return join_mono_indexed( self, other, how=how, sort=sort, block_identity_join=block_identity_join @@ -2071,6 +2103,12 @@ def _is_monotonic( self._stats_cache[column_name].update({op_name: result}) return result + def _throw_if_null_index(self, opname: str): + if len(self.index_columns) == 0: + raise bigframes.exceptions.NullIndexError( + f"Cannot do {opname} without an index. Set an index using set_index." + ) + def _get_rows_as_json_values(self) -> Block: # We want to preserve any ordering currently present before turning to # direct SQL manipulation. We will restore the ordering when we rebuild @@ -2211,6 +2249,10 @@ def __repr__(self) -> str: def to_pandas(self) -> pd.Index: """Executes deferred operations and downloads the results.""" + if len(self.column_ids) == 0: + raise bigframes.exceptions.NullIndexError( + "Cannot materialize index, as this object does not have an index. Set index column(s) using set_index." + ) # Project down to only the index column. So the query can be cached to visualize other data. index_columns = list(self._block.index_columns) dtypes = dict(zip(index_columns, self.dtypes)) @@ -2252,6 +2294,53 @@ def is_uniquely_named(self: BlockIndexProperties): return len(set(self.names)) == len(self.names) +def join_indexless( + left: Block, + right: Block, + *, + how="left", +) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: + """Joins two blocks""" + left_expr = left.expr + right_expr = right.expr + left_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.LEFT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in left_expr.column_ids + ] + right_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.RIGHT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in right_expr.column_ids + ] + combined_expr = left_expr.try_align_as_projection( + right_expr, + join_type=how, + mappings=(*left_mappings, *right_mappings), + ) + if combined_expr is None: + raise bigframes.exceptions.NullIndexError( + "Cannot implicitly align objects. Set an explicit index using set_index." + ) + get_column_left = {m.source_id: m.destination_id for m in left_mappings} + get_column_right = {m.source_id: m.destination_id for m in right_mappings} + block = Block( + combined_expr, + column_labels=[*left.column_labels, *right.column_labels], + index_columns=(), + ) + return ( + block, + (get_column_left, get_column_right), + ) + + def join_mono_indexed( left: Block, right: Block, diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 2db3e0791fa..7f2c56c20a6 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -101,6 +101,11 @@ def __new__( def from_frame( cls, frame: Union[bigframes.series.Series, bigframes.dataframe.DataFrame] ) -> Index: + if len(frame._block.index_columns) == 0: + raise bigframes.exceptions.NullIndexError( + "Cannot access index properties with Null Index. Set an index using set_index." + ) + frame._block._throw_if_null_index("from_frame") index = Index(frame._block) index._linked_frame = frame return index diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index e3a07c04b42..15999c0558d 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -98,12 +98,12 @@ def order_with(self, by: Tuple[order.OrderingExpression, ...]): self.root, self.columns, self.predicate, new_ordering, self.reverse_root ) - def maybe_join( + def can_join( self, right: SquashedSelect, join_def: join_defs.JoinDefinition - ) -> Optional[SquashedSelect]: + ) -> bool: if join_def.type == "cross": # Cannot convert cross join to projection - return None + return False r_exprs_by_id = {id: expr for expr, id in right.columns} l_exprs_by_id = {id: expr for expr, id in self.columns} @@ -113,10 +113,17 @@ def maybe_join( if (self.root != right.root) or any( l_expr != r_expr for l_expr, r_expr in zip(l_join_exprs, r_join_exprs) ): + return False + return True + + def maybe_merge( + self, + right: SquashedSelect, + join_type: join_defs.JoinType, + mappings: Tuple[join_defs.JoinColumnMapping, ...], + ) -> Optional[SquashedSelect]: + if self.root != right.root: return None - - join_type = join_def.type - # Mask columns and remap names to expected schema lselection = self.columns rselection = right.columns @@ -136,7 +143,7 @@ def maybe_join( lselection = tuple((apply_mask(expr, lmask), id) for expr, id in lselection) if rmask is not None: rselection = tuple((apply_mask(expr, rmask), id) for expr, id in rselection) - new_columns = remap_names(join_def, lselection, rselection) + new_columns = remap_names(mappings, lselection, rselection) # Reconstruct ordering reverse_root = self.reverse_root @@ -201,20 +208,27 @@ def maybe_squash_projection(node: nodes.BigFrameNode) -> nodes.BigFrameNode: def maybe_rewrite_join(join_node: nodes.JoinNode) -> nodes.BigFrameNode: left_side = SquashedSelect.from_node(join_node.left_child) right_side = SquashedSelect.from_node(join_node.right_child) - joined = left_side.maybe_join(right_side, join_node.join) - if joined is not None: - return joined.expand() + if left_side.can_join(right_side, join_node.join): + merged = left_side.maybe_merge( + right_side, join_node.join.type, join_node.join.mappings + ) + assert ( + merged is not None + ), "Couldn't merge nodes. This shouldn't happen. Please share full stacktrace with the BigQuery DataFrames team at bigframes-feedback@google.com." + return merged.expand() else: return join_node def remap_names( - join: join_defs.JoinDefinition, lselection: Selection, rselection: Selection + mappings: Tuple[join_defs.JoinColumnMapping, ...], + lselection: Selection, + rselection: Selection, ) -> Selection: new_selection: Selection = tuple() l_exprs_by_id = {id: expr for expr, id in lselection} r_exprs_by_id = {id: expr for expr, id in rselection} - for mapping in join.mappings: + for mapping in mappings: if mapping.source_table == join_defs.JoinSide.LEFT: expr = l_exprs_by_id[mapping.source_id] else: # Right diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 2ac423e3945..105588de2fe 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -17,6 +17,7 @@ from __future__ import annotations import datetime +import functools import inspect import re import sys @@ -42,6 +43,7 @@ import google.cloud.bigquery as bigquery import numpy import pandas +import pandas.io.formats.format import tabulate import bigframes @@ -87,6 +89,15 @@ ) +def requires_index(meth): + @functools.wraps(meth) + def guarded_meth(df: DataFrame, *args, **kwargs): + df._throw_if_null_index(meth.__name__) + return meth(df, *args, **kwargs) + + return guarded_meth + + # Inherits from pandas DataFrame so that we can use the same docstrings. @log_adapter.class_logger class DataFrame(vendored_pandas_frame.DataFrame): @@ -244,6 +255,7 @@ def _sql_names( return results @property + @requires_index def index( self, ) -> indexes.Index: @@ -259,6 +271,7 @@ def index(self, value): self.index.name = value.name if hasattr(value, "name") else None @property + @requires_index def loc(self) -> indexers.LocDataFrameIndexer: return indexers.LocDataFrameIndexer(self) @@ -271,6 +284,7 @@ def iat(self) -> indexers.IatDataFrameIndexer: return indexers.IatDataFrameIndexer(self) @property + @requires_index def at(self) -> indexers.AtDataFrameIndexer: return indexers.AtDataFrameIndexer(self) @@ -317,10 +331,15 @@ def bqclient(self) -> bigframes.Session: def _session(self) -> bigframes.Session: return self._get_block().expr.session + @property + def _has_index(self) -> bool: + return len(self._block.index_columns) > 0 + @property def T(self) -> DataFrame: return DataFrame(self._get_block().transpose()) + @requires_index def transpose(self) -> DataFrame: return self.T @@ -613,7 +632,15 @@ def __repr__(self) -> str: column_count = len(pandas_df.columns) with display_options.pandas_repr(opts): - repr_string = repr(pandas_df) + import pandas.io.formats + + # safe to mutate this, this dict is owned by this code, and does not affect global config + to_string_kwargs = ( + pandas.io.formats.format.get_dataframe_repr_params() # type: ignore + ) + if not self._has_index: + to_string_kwargs.update({"index": False}) + repr_string = pandas_df.to_string(**to_string_kwargs) # Modify the end of the string to reflect count. lines = repr_string.split("\n") @@ -813,15 +840,18 @@ def _apply_dataframe_binop( ) # join columns schema # indexers will be none for exact match - columns, lcol_indexer, rcol_indexer = self.columns.join( - other.columns, how=how, return_indexers=True - ) + if self.columns.equals(other.columns): + columns, lcol_indexer, rcol_indexer = self.columns, None, None + else: + columns, lcol_indexer, rcol_indexer = self.columns.join( + other.columns, how=how, return_indexers=True + ) binop_result_ids = [] column_indices = zip( lcol_indexer if (lcol_indexer is not None) else range(len(columns)), - rcol_indexer if (lcol_indexer is not None) else range(len(columns)), + rcol_indexer if (rcol_indexer is not None) else range(len(columns)), ) for left_index, right_index in column_indices: @@ -1329,6 +1359,7 @@ def drop( block = self._block if index is not None: + self._throw_if_null_index("drop(axis=0)") level_id = self._resolve_levels(level or 0)[0] if utils.is_list_like(index): @@ -1603,6 +1634,7 @@ def set_index( col_ids_strs: List[str] = [col_id for col_id in col_ids if col_id is not None] return DataFrame(self._block.set_index(col_ids_strs, append=append, drop=drop)) + @requires_index def sort_index( self, ascending: bool = True, na_position: Literal["first", "last"] = "last" ) -> DataFrame: @@ -1804,6 +1836,7 @@ def reindex( if columns is not None: return self._reindex_columns(columns) + @requires_index def _reindex_rows( self, index, @@ -1850,9 +1883,11 @@ def _reindex_columns(self, columns): result_df.columns = new_column_index return result_df + @requires_index def reindex_like(self, other: DataFrame, *, validate: typing.Optional[bool] = None): return self.reindex(index=other.index, columns=other.columns, validate=validate) + @requires_index def interpolate(self, method: str = "linear") -> DataFrame: if method == "pad": return self.ffill() @@ -2044,14 +2079,13 @@ def quantile( if multi_q: return DataFrame(result.stack()).droplevel(0) else: - result_df = ( - DataFrame(result) - .stack(list(range(0, frame.columns.nlevels))) - .droplevel(0) + # Drop the last level, which contains q, unnecessary since only one q + result = result.with_column_labels(result.column_labels.droplevel(-1)) + result, index_col = result.create_constant(q, None) + result = result.set_index([index_col]) + return bigframes.series.Series( + result.transpose(original_row_index=pandas.Index([q])) ) - result_series = bigframes.series.Series(result_df._block) - result_series.name = q - return result_series def std( self, axis: typing.Union[str, int] = 0, *, numeric_only: bool = False @@ -2146,9 +2180,11 @@ def agg( aggregate = agg aggregate.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.agg) + @requires_index def idxmin(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmin(self._block)) + @requires_index def idxmax(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmax(self._block)) @@ -2255,6 +2291,7 @@ def _pivot( ) return DataFrame(pivot_block) + @requires_index def pivot( self, *, @@ -2268,6 +2305,7 @@ def pivot( ) -> DataFrame: return self._pivot(columns=columns, index=index, values=values) + @requires_index def pivot_table( self, values: typing.Optional[ @@ -2366,6 +2404,7 @@ def _stack_multi(self, level: LevelsType = -1): block = block.stack(levels=len(level)) return DataFrame(block) + @requires_index def unstack(self, level: LevelsType = -1): if not utils.is_list_like(level): level = [level] @@ -2613,6 +2652,7 @@ def groupby( else: raise TypeError("You have to supply one of 'by' and 'level'") + @requires_index def _groupby_level( self, level: LevelsType, @@ -3578,3 +3618,9 @@ def __matmul__(self, other) -> DataFrame: return self.dot(other) __matmul__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__matmul__) + + def _throw_if_null_index(self, opname: str): + if not self._has_index: + raise bigframes.exceptions.NullIndexError( + f"DataFrame cannot perform {opname} as it has no index. Set an index using set_index." + ) diff --git a/bigframes/enums.py b/bigframes/enums.py index 4bec75f5dff..9501d3f13ee 100644 --- a/bigframes/enums.py +++ b/bigframes/enums.py @@ -27,3 +27,5 @@ class DefaultIndexKind(enum.Enum): #: ``n - 3``, ``n - 2``, ``n - 1``, where ``n`` is the number of items in #: the index. SEQUENTIAL_INT64 = enum.auto() + # A completely null index incapable of indexing or alignment. + NULL = enum.auto() diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index eae021b4cdd..027b3a42363 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -37,3 +37,7 @@ class DefaultIndexWarning(Warning): class PreviewWarning(Warning): """The feature is in preview.""" + + +class NullIndexError(ValueError): + """Object has no index.""" diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 75d14f3fbc1..49ef7f76ee3 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -14,6 +14,7 @@ from __future__ import annotations +import functools import typing from typing import List, Sequence @@ -34,6 +35,15 @@ import bigframes.session +def requires_index(meth): + @functools.wraps(meth) + def guarded_meth(df: SeriesMethods, *args, **kwargs): + df._throw_if_null_index(meth.__name__) + return meth(df, *args, **kwargs) + + return guarded_meth + + class SeriesMethods: def __init__( self, @@ -266,3 +276,9 @@ def _align_n( block, constant_col_id = block.create_constant(other, dtype=dtype) value_ids = [*value_ids, constant_col_id] return (value_ids, block) + + def _throw_if_null_index(self, opname: str): + if len(self._block.index_columns) == 0: + raise bigframes.exceptions.NullIndexError( + f"Series cannot perform {opname} as it has no index. Set an index using set_index." + ) diff --git a/bigframes/series.py b/bigframes/series.py index d1fb0d679bd..4595164e805 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -51,6 +51,7 @@ import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.operations.base +from bigframes.operations.base import requires_index import bigframes.operations.datetimes as dt import bigframes.operations.plotting as plotting import bigframes.operations.strings as strings @@ -85,6 +86,7 @@ def dtypes(self): return self._dtype @property + @requires_index def loc(self) -> bigframes.core.indexers.LocSeriesIndexer: return bigframes.core.indexers.LocSeriesIndexer(self) @@ -97,6 +99,7 @@ def iat(self) -> bigframes.core.indexers.IatSeriesIndexer: return bigframes.core.indexers.IatSeriesIndexer(self) @property + @requires_index def at(self) -> bigframes.core.indexers.AtSeriesIndexer: return bigframes.core.indexers.AtSeriesIndexer(self) @@ -135,6 +138,7 @@ def values(self) -> numpy.ndarray: return self.to_numpy() @property + @requires_index def index(self) -> indexes.Index: return indexes.Index.from_frame(self) @@ -236,6 +240,7 @@ def rename( raise ValueError(f"Unsupported type of parameter index: {type(index)}") + @requires_index def rename_axis( self, mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], @@ -288,7 +293,17 @@ def __repr__(self) -> str: pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) self._set_internal_query_job(query_job) - return repr(pandas_df.iloc[:, 0]) + pd_series = pandas_df.iloc[:, 0] + + import pandas.io.formats + + # safe to mutate this, this dict is owned by this code, and does not affect global config + to_string_kwargs = pandas.io.formats.format.get_series_repr_params() # type: ignore + if len(self._block.index_columns) == 0: + to_string_kwargs.update({"index": False}) + repr_string = pd_series.to_string(**to_string_kwargs) + + return repr_string def astype( self, @@ -379,10 +394,12 @@ def drop( block = block.drop_columns([condition_id]) return Series(block.select_column(self._value_column)) + @requires_index def droplevel(self, level: LevelsType, axis: int | str = 0): resolved_level_ids = self._resolve_levels(level) return Series(self._block.drop_levels(resolved_level_ids)) + @requires_index def swaplevel(self, i: int = -2, j: int = -1): level_i = self._block.index_columns[i] level_j = self._block.index_columns[j] @@ -392,6 +409,7 @@ def swaplevel(self, i: int = -2, j: int = -1): ] return Series(self._block.reorder_levels(reordering)) + @requires_index def reorder_levels(self, order: LevelsType, axis: int | str = 0): resolved_level_ids = self._resolve_levels(order) return Series(self._block.reorder_levels(resolved_level_ids)) @@ -570,6 +588,7 @@ def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): ) return Series(block.select_column(result)) + @requires_index def interpolate(self, method: str = "linear") -> Series: if method == "pad": return self.ffill() @@ -986,9 +1005,13 @@ def quantile(self, q: Union[float, Sequence[float]] = 0.5) -> Union[Series, floa qs = tuple(q) if utils.is_list_like(q) else (q,) result = block_ops.quantile(self._block, (self._value_column,), qs=qs) if utils.is_list_like(q): - result = result.stack() - result = result.drop_levels([result.index_columns[0]]) - return Series(result) + # Drop the first level, since only one column + result = result.with_column_labels(result.column_labels.droplevel(0)) + result, index_col = result.create_constant(self.name, None) + result = result.set_index([index_col]) + return Series( + result.transpose(original_row_index=pandas.Index([self.name])) + ) else: return cast(float, Series(result).to_pandas().squeeze()) @@ -1064,6 +1087,7 @@ def argmin(self) -> int: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) + @requires_index def unstack(self, level: LevelsType = -1): if isinstance(level, int) or isinstance(level, str): level = [level] @@ -1087,6 +1111,7 @@ def unstack(self, level: LevelsType = -1): ) return bigframes.dataframe.DataFrame(pivot_block) + @requires_index def idxmax(self) -> blocks.Label: block = self._block.order_by( [ @@ -1100,6 +1125,7 @@ def idxmax(self) -> blocks.Label: block = block.slice(0, 1) return indexes.Index(block).to_pandas()[0] + @requires_index def idxmin(self) -> blocks.Label: block = self._block.order_by( [ @@ -1209,6 +1235,7 @@ def sort_values( ) return Series(block) + @requires_index def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: # TODO(tbergeron): Support level parameter once multi-index introduced. if na_position not in ["first", "last"]: @@ -1269,6 +1296,7 @@ def groupby( else: raise TypeError("You have to supply one of 'by' and 'level'") + @requires_index def _groupby_level( self, level: int | str | typing.Sequence[int] | typing.Sequence[str], @@ -1406,9 +1434,11 @@ def combine( materialized_series = result_series._cached() return materialized_series + @requires_index def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_prefix(prefix)) + @requires_index def add_suffix(self, suffix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_suffix(suffix)) @@ -1460,6 +1490,7 @@ def filter( else: raise ValueError("Need to provide 'items', 'like', or 'regex'") + @requires_index def reindex(self, index=None, *, validate: typing.Optional[bool] = None): if validate and not self.index.is_unique: raise ValueError("Original index must be unique to reindex") @@ -1488,6 +1519,7 @@ def reindex(self, index=None, *, validate: typing.Optional[bool] = None): )._block return Series(result_block) + @requires_index def reindex_like(self, other: Series, *, validate: typing.Optional[bool] = None): return self.reindex(other.index, validate=validate) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index ab53de80419..ccdc3c5eebc 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -26,6 +26,7 @@ Any, Callable, Dict, + Hashable, IO, Iterable, List, @@ -79,6 +80,7 @@ import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.compile +import bigframes.core.guid import bigframes.core.nodes as nodes from bigframes.core.ordering import IntegerEncoding import bigframes.core.ordering as order @@ -799,15 +801,28 @@ def _read_gbq_table( ) # ---------------------------------------------------- - # Create Block & default index if len(index_cols) == 0 + # Create Default Sequential Index if still have no index # ---------------------------------------------------- + # If no index columns provided or found, fall back to sequential index + if (index_col != bigframes.enums.DefaultIndexKind.NULL) and len( + index_cols + ) == 0: + index_col = bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 + + index_names: Sequence[Hashable] = index_cols + if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: + sequential_index_col = bigframes.core.guid.generate_guid("index_") + array_value = array_value.promote_offsets(sequential_index_col) + index_cols = [sequential_index_col] + index_names = [None] + value_columns = [col for col in array_value.column_ids if col not in index_cols] block = blocks.Block( array_value, index_columns=index_cols, column_labels=value_columns, - index_labels=index_cols, + index_labels=index_names, ) if max_results: block = block.slice(stop=max_results) diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 92e007c804b..063dde2a246 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -242,10 +242,8 @@ def get_index_cols( if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: # User has explicity asked for a default, sequential index. # Use that, even if there are primary keys on the table. - # - # Note: This relies on the default behavior of the Block - # constructor to create a default sequential index. If that ever - # changes, this logic will need to be revisited. + return [] + if index_col == bigframes.enums.DefaultIndexKind.NULL: return [] else: # Note: It's actually quite difficult to mock this out to unit diff --git a/tests/system/conftest.py b/tests/system/conftest.py index a040f2b19ba..ecf633b27fb 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -391,6 +391,16 @@ def scalars_df_index( return session.read_gbq(scalars_table_id, index_col="rowindex") +@pytest.fixture(scope="session") +def scalars_df_empty_index( + scalars_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq( + scalars_table_id, index_col=bigframes.enums.DefaultIndexKind.NULL + ).sort_values("rowindex") + + @pytest.fixture(scope="session") def scalars_df_2_default_index( scalars_df_2_index: bigframes.dataframe.DataFrame, diff --git a/tests/system/small/test_empty_index.py b/tests/system/small/test_empty_index.py new file mode 100644 index 00000000000..7a1715e3d13 --- /dev/null +++ b/tests/system/small/test_empty_index.py @@ -0,0 +1,212 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pandas as pd +import pytest + +import bigframes.exceptions +import bigframes.pandas as bpd +from tests.system.utils import skip_legacy_pandas + + +def test_empty_index_materialize( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_empty_index.to_pandas() + pd.testing.assert_frame_equal( + bf_result, scalars_pandas_df_default_index, check_index_type=False + ) + + +def test_empty_index_series_repr( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_empty_index["int64_too"].head(5).__repr__() + pd_result = ( + scalars_pandas_df_default_index["int64_too"] + .head(5) + .to_string(dtype=True, index=False, length=False, name=True) + ) + assert bf_result == pd_result + + +def test_empty_index_dataframe_repr( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_empty_index[["int64_too", "int64_col"]].head(5).__repr__() + pd_result = ( + scalars_pandas_df_default_index[["int64_too", "int64_col"]] + .head(5) + .to_string(index=False) + ) + assert bf_result == pd_result + "\n\n[5 rows x 2 columns]" + + +def test_empty_index_reset_index( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_empty_index.reset_index().to_pandas() + pd_result = scalars_pandas_df_default_index.reset_index(drop=True) + pd.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + +def test_empty_index_set_index(scalars_df_empty_index, scalars_pandas_df_default_index): + bf_result = scalars_df_empty_index.set_index("int64_col").to_pandas() + pd_result = scalars_pandas_df_default_index.set_index("int64_col") + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_empty_index_concat(scalars_df_empty_index, scalars_pandas_df_default_index): + bf_result = bpd.concat( + [scalars_df_empty_index, scalars_df_empty_index], axis=0 + ).to_pandas() + pd_result = pd.concat( + [scalars_pandas_df_default_index, scalars_pandas_df_default_index], axis=0 + ) + pd.testing.assert_frame_equal(bf_result, pd_result.reset_index(drop=True)) + + +def test_empty_index_aggregate(scalars_df_empty_index, scalars_pandas_df_default_index): + bf_result = scalars_df_empty_index.count().to_pandas() + pd_result = scalars_pandas_df_default_index.count() + + pd_result.index = pd_result.index.astype("string[pyarrow]") + + pd.testing.assert_series_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + +def test_empty_index_groupby_aggregate( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_empty_index.groupby("int64_col").count().to_pandas() + pd_result = scalars_pandas_df_default_index.groupby("int64_col").count() + + pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +@skip_legacy_pandas +def test_empty_index_analytic(scalars_df_empty_index, scalars_pandas_df_default_index): + bf_result = scalars_df_empty_index["int64_col"].cumsum().to_pandas() + pd_result = scalars_pandas_df_default_index["int64_col"].cumsum() + pd.testing.assert_series_equal( + bf_result, pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_empty_index_groupby_analytic( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = ( + scalars_df_empty_index.groupby("bool_col")["int64_col"].cummax().to_pandas() + ) + pd_result = scalars_pandas_df_default_index.groupby("bool_col")[ + "int64_col" + ].cummax() + pd.testing.assert_series_equal( + bf_result, pd_result.reset_index(drop=True), check_dtype=False + ) + + +@skip_legacy_pandas +def test_empty_index_stack(scalars_df_empty_index, scalars_pandas_df_default_index): + stacking_cols = ["int64_col", "int64_too"] + bf_result = scalars_df_empty_index[stacking_cols].stack().to_pandas() + pd_result = ( + scalars_pandas_df_default_index[stacking_cols] + .stack(future_stack=True) + .droplevel(level=0, axis=0) + ) + pd_result.index = pd_result.index.astype(bf_result.index.dtype) + pd.testing.assert_series_equal( + bf_result, + pd_result, + check_dtype=False, + ) + + +def test_empty_index_series_self_aligns( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = ( + scalars_df_empty_index["int64_col"] + scalars_df_empty_index["int64_too"] + ) + pd_result = ( + scalars_pandas_df_default_index["int64_col"] + + scalars_pandas_df_default_index["int64_too"] + ) + pd.testing.assert_series_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_empty_index_df_self_aligns( + scalars_df_empty_index, scalars_pandas_df_default_index +): + bf_result = ( + scalars_df_empty_index[["int64_col", "float64_col"]] + + scalars_df_empty_index[["int64_col", "float64_col"]] + ) + pd_result = ( + scalars_pandas_df_default_index[["int64_col", "float64_col"]] + + scalars_pandas_df_default_index[["int64_col", "float64_col"]] + ) + pd.testing.assert_frame_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_empty_index_df_concat(scalars_df_empty_index, scalars_pandas_df_default_index): + bf_result = bpd.concat([scalars_df_empty_index, scalars_df_empty_index]) + pd_result = pd.concat( + [scalars_pandas_df_default_index, scalars_pandas_df_default_index] + ) + pd.testing.assert_frame_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_empty_index_align_error(scalars_df_empty_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + _ = ( + scalars_df_empty_index["int64_col"] + + scalars_df_empty_index["int64_col"].cumsum() + ) + + +def test_empty_index_loc_error(scalars_df_empty_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + scalars_df_empty_index["int64_col"].loc[1] + + +def test_empty_index_at_error(scalars_df_empty_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + scalars_df_empty_index["int64_col"].at[1] + + +def test_empty_index_idxmin_error(scalars_df_empty_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + scalars_df_empty_index[["int64_col", "int64_too"]].idxmin() + + +def test_empty_index_index_property(scalars_df_empty_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + _ = scalars_df_empty_index.index + + +def test_empty_index_transpose(scalars_df_empty_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + _ = scalars_df_empty_index.T diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 2df7ab86b69..dbc8ddec6f9 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1430,13 +1430,13 @@ def test_numeric_literal(scalars_dfs): assert bf_result.dtype == pd.ArrowDtype(pa.decimal128(38, 9)) -def test_repr(scalars_dfs): +def test_series_small_repr(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" bf_series = scalars_df[col_name] pd_series = scalars_pandas_df[col_name] - assert repr(bf_series) == repr(pd_series) + assert repr(bf_series) == pd_series.to_string(length=False, dtype=True, name=True) def test_sum(scalars_dfs): @@ -3688,10 +3688,10 @@ def test_series_explode_reserve_order(ignore_index, ordered): res = s.explode(ignore_index=ignore_index).to_pandas(ordered=ordered) # type: ignore # TODO(b/340885567): fix type error pd_res = pd_s.explode(ignore_index=ignore_index).astype(pd.Int64Dtype()) # type: ignore + pd_res.index = pd_res.index.astype(pd.Int64Dtype()) pd.testing.assert_series_equal( res if ordered else res.sort_index(), pd_res, - check_index_type=False, ) From f89b6be55c360c65f804c920ce9c1b9dd034557a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 09:00:06 -0500 Subject: [PATCH 586/921] chore(main): release 1.7.0 (#685) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568efa68b4a..34ab012fd73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.7.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.6.0...v1.7.0) (2024-05-20) + + +### Features + +* `read_gbq_query` supports `filters` ([9386373](https://github.com/googleapis/python-bigquery-dataframes/commit/9386373538c1e7827e2210c4fd9946312821b54d)) +* `read_gbq` suggests a correct column name when one is not found ([9386373](https://github.com/googleapis/python-bigquery-dataframes/commit/9386373538c1e7827e2210c4fd9946312821b54d)) +* Add `DefaultIndexKind.NULL` to use as `index_col` in `read_gbq*`, creating an indexless DataFrame/Series ([#662](https://github.com/googleapis/python-bigquery-dataframes/issues/662)) ([29e4886](https://github.com/googleapis/python-bigquery-dataframes/commit/29e4886d41e3d615bc493cf3a104ef1b0698ece8)) +* Bigframes.bigquery.array_agg(SeriesGroupBy|DataFrameGroupby) ([#663](https://github.com/googleapis/python-bigquery-dataframes/issues/663)) ([412f28b](https://github.com/googleapis/python-bigquery-dataframes/commit/412f28bf7551430473690160a2a1c4c2f133539e)) +* To_datetime supports utc=False for string inputs ([#579](https://github.com/googleapis/python-bigquery-dataframes/issues/579)) ([adf9889](https://github.com/googleapis/python-bigquery-dataframes/commit/adf98892e499f4a9c85162c38f56ca5634a1ba6d)) + + +### Bug Fixes + +* `read_gbq_table` respects primary keys even when `filters` are set ([#689](https://github.com/googleapis/python-bigquery-dataframes/issues/689)) ([9386373](https://github.com/googleapis/python-bigquery-dataframes/commit/9386373538c1e7827e2210c4fd9946312821b54d)) +* Fix type error in test_cluster ([#698](https://github.com/googleapis/python-bigquery-dataframes/issues/698)) ([14d81c1](https://github.com/googleapis/python-bigquery-dataframes/commit/14d81c17505f9a09439a874ff855aec6f95fc0d1)) +* Improve escaping of literals and identifiers ([#682](https://github.com/googleapis/python-bigquery-dataframes/issues/682)) ([da9b136](https://github.com/googleapis/python-bigquery-dataframes/commit/da9b136df08b243c8515946f7c0d7b591b8fcbdc)) +* Properly identify non-unique index in tables without primary keys ([#699](https://github.com/googleapis/python-bigquery-dataframes/issues/699)) ([6e0f4d8](https://github.com/googleapis/python-bigquery-dataframes/commit/6e0f4d8c76f78dc26f4aa1880dd67ebdb638bb5e)) +* Remove a usage of the `resource` package when not available, such as on Windows ([#681](https://github.com/googleapis/python-bigquery-dataframes/issues/681)) ([96243f2](https://github.com/googleapis/python-bigquery-dataframes/commit/96243f23a1571001509d0d01c16c1e72e47e0d23)) +* The imported samples error and use peek() ([#688](https://github.com/googleapis/python-bigquery-dataframes/issues/688)) ([1a0b744](https://github.com/googleapis/python-bigquery-dataframes/commit/1a0b744c5aacdd8ba4eececf7b0a374808e8672c)) + + +### Performance Improvements + +* Don't run query immediately from `read_gbq_table` if `filters` is set ([9386373](https://github.com/googleapis/python-bigquery-dataframes/commit/9386373538c1e7827e2210c4fd9946312821b54d)) +* Use a `LIMIT` clause when `max_results` is set ([9386373](https://github.com/googleapis/python-bigquery-dataframes/commit/9386373538c1e7827e2210c4fd9946312821b54d)) + + +### Documentation + +* Add code snippets for imported onnx tutorials ([#684](https://github.com/googleapis/python-bigquery-dataframes/issues/684)) ([cb36e46](https://github.com/googleapis/python-bigquery-dataframes/commit/cb36e468d1c2a34c2231638124f3c8d9052f032b)) +* Add code snippets for imported tensorflow model ([#679](https://github.com/googleapis/python-bigquery-dataframes/issues/679)) ([b02c401](https://github.com/googleapis/python-bigquery-dataframes/commit/b02c401614eeab9cbf2e9a7c648b3d0a4e741b97)) +* Use `class_weight="balanced"` in the logistic regression prediction tutorial ([#678](https://github.com/googleapis/python-bigquery-dataframes/issues/678)) ([b951549](https://github.com/googleapis/python-bigquery-dataframes/commit/b95154908fd7838e499a2af0fc3760c5ab33358f)) + ## [1.6.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.5.0...v1.6.0) (2024-05-13) diff --git a/bigframes/version.py b/bigframes/version.py index e139eaa89ef..da33be63fce 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.6.0" +__version__ = "1.7.0" From de0881bcd404cbb246bec85ae6247ea13bf2e85e Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 21 May 2024 12:32:26 -0700 Subject: [PATCH 587/921] refactor: caching is now a session property (#697) --- bigframes/core/__init__.py | 10 +-- bigframes/core/blocks.py | 24 ++---- bigframes/core/indexes/base.py | 1 + bigframes/core/tree_properties.py | 66 ++++++++++------ bigframes/dataframe.py | 6 +- bigframes/series.py | 6 +- bigframes/session/__init__.py | 123 +++++++++++++++++++----------- 7 files changed, 140 insertions(+), 96 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 0a2936419f8..5024b01323d 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -184,7 +184,7 @@ def project_to_id(self, expression: ex.Expression, output_id: str): child=self.node, assignments=tuple(exprs), ) - ).merge_projections() + ) def assign(self, source_id: str, destination_id: str) -> ArrayValue: if destination_id in self.column_ids: # Mutate case @@ -209,7 +209,7 @@ def assign(self, source_id: str, destination_id: str) -> ArrayValue: child=self.node, assignments=tuple(exprs), ) - ).merge_projections() + ) def assign_constant( self, @@ -243,7 +243,7 @@ def assign_constant( child=self.node, assignments=tuple(exprs), ) - ).merge_projections() + ) def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: selections = ((ex.free_var(col_id), col_id) for col_id in column_ids) @@ -252,7 +252,7 @@ def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: child=self.node, assignments=tuple(selections), ) - ).merge_projections() + ) def drop_columns(self, columns: Iterable[str]) -> ArrayValue: new_projection = ( @@ -265,7 +265,7 @@ def drop_columns(self, columns: Iterable[str]) -> ArrayValue: child=self.node, assignments=tuple(new_projection), ) - ).merge_projections() + ) def aggregate( self, diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 0bbb8a0b61c..f189bb704e0 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2026,27 +2026,17 @@ def to_sql_query( idx_labels, ) - def cached(self, *, optimize_offsets=False, force: bool = False) -> Block: - """Write the block to a session table and create a new block object that references it.""" + def cached(self, *, optimize_offsets=False, force: bool = False) -> None: + """Write the block to a session table.""" # use a heuristic for whether something needs to be cached if (not force) and self.session._is_trivially_executable(self.expr): - return self + return if optimize_offsets: - expr = self.session._cache_with_offsets(self.expr) + self.session._cache_with_offsets(self.expr) else: - expr = self.session._cache_with_cluster_cols( + self.session._cache_with_cluster_cols( self.expr, cluster_cols=self.index_columns ) - return self.swap_array_expr(expr) - - def swap_array_expr(self, expr: core.ArrayValue) -> Block: - # TODO: Validate schema unchanged - return Block( - expr, - index_columns=self.index_columns, - column_labels=self.column_labels, - index_labels=self.index.names, - ) def _is_monotonic( self, column_ids: typing.Union[str, Sequence[str]], increasing: bool @@ -2116,8 +2106,8 @@ def _get_rows_as_json_values(self) -> Block: # TODO(shobs): Replace direct SQL manipulation by structured expression # manipulation ordering_column_name = guid.generate_guid() - expr = self.session._cache_with_offsets(self.expr) - expr = expr.promote_offsets(ordering_column_name) + self.session._cache_with_offsets(self.expr) + expr = self.expr.promote_offsets(ordering_column_name) expr_sql = self.session._to_sql(expr) # Names of the columns to serialize for the row. diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 7f2c56c20a6..e40e20b0cb2 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -95,6 +95,7 @@ def __new__( result = typing.cast(Index, object.__new__(klass)) # type: ignore result._query_job = None result._block = block + block.session._register_object(result) return result @classmethod diff --git a/bigframes/core/tree_properties.py b/bigframes/core/tree_properties.py index 125a7e6bff7..2847a8f7f15 100644 --- a/bigframes/core/tree_properties.py +++ b/bigframes/core/tree_properties.py @@ -15,7 +15,7 @@ import functools import itertools -from typing import Dict +from typing import Callable, Dict, Optional import bigframes.core.nodes as nodes @@ -40,46 +40,66 @@ def peekable(node: nodes.BigFrameNode) -> bool: return children_peekable and self_peekable -def count_complex_nodes( - root: nodes.BigFrameNode, min_complexity: float, max_complexity: float -) -> Dict[nodes.BigFrameNode, int]: +# Replace modified_cost(node) = cost(apply_cache(node)) +def select_cache_target( + root: nodes.BigFrameNode, + min_complexity: float, + max_complexity: float, + cache: dict[nodes.BigFrameNode, nodes.BigFrameNode], + heuristic: Callable[[int, int], float], +) -> Optional[nodes.BigFrameNode]: + """Take tree, and return candidate nodes with (# of occurences, post-caching planning complexity). + + heurstic takes two args, node complexity, and node occurence count, in that order + """ + + @functools.cache + def _with_caching(subtree: nodes.BigFrameNode) -> nodes.BigFrameNode: + return replace_nodes(subtree, cache) + + def _combine_counts( + left: Dict[nodes.BigFrameNode, int], right: Dict[nodes.BigFrameNode, int] + ) -> Dict[nodes.BigFrameNode, int]: + return { + key: left.get(key, 0) + right.get(key, 0) + for key in itertools.chain(left.keys(), right.keys()) + } + @functools.cache def _node_counts_inner( subtree: nodes.BigFrameNode, ) -> Dict[nodes.BigFrameNode, int]: """Helper function to count occurences of duplicate nodes in a subtree. Considers only nodes in a complexity range""" empty_counts: Dict[nodes.BigFrameNode, int] = {} - if subtree.planning_complexity >= min_complexity: + subtree_complexity = _with_caching(subtree).planning_complexity + if subtree_complexity >= min_complexity: child_counts = [_node_counts_inner(child) for child in subtree.child_nodes] node_counts = functools.reduce(_combine_counts, child_counts, empty_counts) - if subtree.planning_complexity <= max_complexity: + if subtree_complexity <= max_complexity: return _combine_counts(node_counts, {subtree: 1}) else: return node_counts return empty_counts - return _node_counts_inner(root) + node_counts = _node_counts_inner(root) + + return max( + node_counts.keys(), + key=lambda node: heuristic( + _with_caching(node).planning_complexity, node_counts[node] + ), + ) def replace_nodes( root: nodes.BigFrameNode, - to_replace: nodes.BigFrameNode, - replacemenet: nodes.BigFrameNode, + replacements: dict[nodes.BigFrameNode, nodes.BigFrameNode], ): @functools.cache - def apply_substition(n: nodes.BigFrameNode) -> nodes.BigFrameNode: - if n == to_replace: - return replacemenet + def apply_substition(node: nodes.BigFrameNode) -> nodes.BigFrameNode: + if node in replacements.keys(): + return replacements[node] else: - return n.transform_children(apply_substition) - - return root.transform_children(apply_substition) - + return node.transform_children(apply_substition) -def _combine_counts( - left: Dict[nodes.BigFrameNode, int], right: Dict[nodes.BigFrameNode, int] -) -> Dict[nodes.BigFrameNode, int]: - return { - key: left.get(key, 0) + right.get(key, 0) - for key in itertools.chain(left.keys(), right.keys()) - } + return apply_substition(root) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 105588de2fe..e8f219d301b 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -191,6 +191,7 @@ def __init__( else: self._block = bigframes.pandas.read_pandas(pd_dataframe)._get_block() self._query_job: Optional[bigquery.QueryJob] = None + self._block.session._register_object(self) def __dir__(self): return dir(type(self)) + [ @@ -3515,7 +3516,7 @@ def _cached(self, *, force: bool = False) -> DataFrame: No-op if the dataframe represents a trivial transformation of an existing materialization. Force=True is used for BQML integration where need to copy data rather than use snapshot. """ - self._set_block(self._block.cached(force=force)) + self._block.cached(force=force) return self def _optimize_query_complexity(self): @@ -3523,8 +3524,7 @@ def _optimize_query_complexity(self): May generate many queries and take substantial time to execute. """ # TODO: Move all this to session - new_expr = self._session._simplify_with_caching(self._block.expr) - self._set_block(self._block.swap_array_expr(new_expr)) + self._session._simplify_with_caching(self._block.expr) _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") diff --git a/bigframes/series.py b/bigframes/series.py index 4595164e805..3f1fa4c3a5e 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -72,6 +72,7 @@ class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Ser def __init__(self, *args, **kwargs): self._query_job: Optional[bigquery.QueryJob] = None super().__init__(*args, **kwargs) + self._block.session._register_object(self) @property def dt(self) -> dt.DatetimeMethods: @@ -1777,7 +1778,7 @@ def cache(self): return self._cached(force=True) def _cached(self, *, force: bool = True) -> Series: - self._set_block(self._block.cached(force=force)) + self._block.cached(force=force) return self def _optimize_query_complexity(self): @@ -1785,8 +1786,7 @@ def _optimize_query_complexity(self): May generate many queries and take substantial time to execute. """ # TODO: Move all this to session - new_expr = self._block.session._simplify_with_caching(self._block.expr) - self._set_block(self._block.swap_array_expr(new_expr)) + self._block.session._simplify_with_caching(self._block.expr) def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index ccdc3c5eebc..226af9ec5cc 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,9 +16,11 @@ from __future__ import annotations +import collections.abc import copy import datetime import logging +import math import os import secrets import typing @@ -40,9 +42,8 @@ ) import uuid import warnings +import weakref -# Even though the ibis.backends.bigquery import is unused, it's needed -# to register new and replacement ops with the Ibis BigQuery backend. import bigframes_vendored.ibis.backends.bigquery # noqa import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet @@ -87,6 +88,10 @@ import bigframes.core.tree_properties as traversals import bigframes.core.tree_properties as tree_properties import bigframes.core.utils as utils + +# Even though the ibis.backends.bigquery import is unused, it's needed +# to register new and replacement ops with the Ibis BigQuery backend. +import bigframes.dataframe import bigframes.dtypes import bigframes.exceptions import bigframes.formatting_helpers as formatting_helpers @@ -269,6 +274,19 @@ def __init__( self._table_ids: List[str] = [] # store table ids and delete them when the session is closed + self._objects: list[ + weakref.ReferenceType[ + Union[ + bigframes.core.indexes.Index, + bigframes.series.Series, + dataframe.DataFrame, + ] + ] + ] = [] + self._cached_executions: weakref.WeakKeyDictionary[ + nodes.BigFrameNode, nodes.BigFrameNode + ] = weakref.WeakKeyDictionary() + @property def bqclient(self): return self._clients_provider.bqclient @@ -303,6 +321,17 @@ def bqconnectionmanager(self): def session_id(self): return self._session_id + @property + def objects( + self, + ) -> collections.abc.Set[ + Union[ + bigframes.core.indexes.Index, bigframes.series.Series, dataframe.DataFrame + ] + ]: + # Create a set with strong references, be careful not to hold onto this needlessly, as will prevent garbage collection. + return set(i() for i in self._objects if i() is not None) # type: ignore + @property def _project(self): return self.bqclient.project @@ -371,6 +400,14 @@ def read_gbq( filters=filters, ) + def _register_object( + self, + object: Union[ + bigframes.core.indexes.Index, bigframes.series.Series, dataframe.DataFrame + ], + ): + self._objects.append(weakref.ref(object)) + def _query_to_destination( self, query: str, @@ -1785,7 +1822,7 @@ def _start_query_ml_ddl( def _cache_with_cluster_cols( self, array_value: core.ArrayValue, cluster_cols: typing.Sequence[str] - ) -> core.ArrayValue: + ): """Executes the query and uses the resulting table to rewrite future executions.""" # TODO: Use this for all executions? Problem is that caching materializes extra # ordering columns @@ -1807,16 +1844,16 @@ def _cache_with_cluster_cols( table_expression[column] for column in compiled_value._hidden_ordering_column_names ] - # TODO: Instead, keep session-wide map of cached results and automatically reuse - return core.ArrayValue.from_ibis( + cached_replacement = core.ArrayValue.from_ibis( self, table_expression, columns=new_columns, hidden_ordering_columns=new_hidden_columns, ordering=compiled_value._ordering, - ) + ).node + self._cached_executions[array_value.node] = cached_replacement - def _cache_with_offsets(self, array_value: core.ArrayValue) -> core.ArrayValue: + def _cache_with_offsets(self, array_value: core.ArrayValue): """Executes the query and uses the resulting table to rewrite future executions.""" # TODO: Use this for all executions? Problem is that caching materializes extra # ordering columns @@ -1835,60 +1872,50 @@ def _cache_with_offsets(self, array_value: core.ArrayValue) -> core.ArrayValue: ) new_columns = [table_expression[column] for column in compiled_value.column_ids] new_hidden_columns = [table_expression["bigframes_offsets"]] - # TODO: Instead, keep session-wide map of cached results and automatically reuse - return core.ArrayValue.from_ibis( + cached_replacement = core.ArrayValue.from_ibis( self, table_expression, columns=new_columns, hidden_ordering_columns=new_hidden_columns, ordering=order.ExpressionOrdering.from_offset_col("bigframes_offsets"), - ) + ).node + self._cached_executions[array_value.node] = cached_replacement - def _simplify_with_caching(self, array_value: core.ArrayValue) -> core.ArrayValue: + def _simplify_with_caching(self, array_value: core.ArrayValue): """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" + # Apply existing caching first if not bigframes.options.compute.enable_multi_query_execution: - return array_value - node = array_value.node - if node.planning_complexity < QUERY_COMPLEXITY_LIMIT: - return array_value + return for _ in range(MAX_SUBTREE_FACTORINGS): - updated = self._cache_most_complex_subtree(node) - if updated is None: - return core.ArrayValue(node) - else: - node = updated + node_with_cache = self._with_cached_executions(array_value.node) + if node_with_cache.planning_complexity < QUERY_COMPLEXITY_LIMIT: + return - return core.ArrayValue(node) + did_cache = self._cache_most_complex_subtree(array_value.node) + if not did_cache: + return - def _cache_most_complex_subtree( - self, node: nodes.BigFrameNode - ) -> Optional[nodes.BigFrameNode]: + def _cache_most_complex_subtree(self, node: nodes.BigFrameNode) -> bool: # TODO: If query fails, retry with lower complexity limit - valid_candidates = traversals.count_complex_nodes( + selection = traversals.select_cache_target( node, min_complexity=(QUERY_COMPLEXITY_LIMIT / 500), max_complexity=QUERY_COMPLEXITY_LIMIT, - ).items() - # Heuristic: subtree_compleixty * (copies of subtree)^2 - best_candidate = max( - valid_candidates, - key=lambda i: i[0].planning_complexity + (i[1] ** 2), - default=None, + cache=dict(self._cached_executions), + # Heuristic: subtree_compleixty * (copies of subtree)^2 + heuristic=lambda complexity, count: math.log(complexity) + + 2 * math.log(count), ) - - if best_candidate is None: + if selection is None: # No good subtrees to cache, just return original tree - return None + return False - # TODO: Add clustering columns based on access patterns - materialized = self._cache_with_cluster_cols( - core.ArrayValue(best_candidate[0]), [] - ).node + self._cache_with_cluster_cols(core.ArrayValue(selection), []) + return True - return traversals.replace_nodes( - node, to_replace=best_candidate[0], replacemenet=materialized - ) + def _with_cached_executions(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: + return traversals.replace_nodes(node, (dict(self._cached_executions))) def _is_trivially_executable(self, array_value: core.ArrayValue): """ @@ -1897,7 +1924,9 @@ def _is_trivially_executable(self, array_value: core.ArrayValue): """ # Once rewriting is available, will want to rewrite before # evaluating execution cost. - return traversals.is_trivially_executable(array_value.node) + return traversals.is_trivially_executable( + self._with_cached_executions(array_value.node) + ) def _execute( self, @@ -1924,7 +1953,7 @@ def _peek( self, array_value: core.ArrayValue, n_rows: int ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """A 'peek' efficiently accesses a small number of rows in the dataframe.""" - if not tree_properties.peekable(array_value.node): + if not tree_properties.peekable(self._with_cached_executions(array_value.node)): warnings.warn("Peeking this value cannot be done efficiently.") sql = self._compile_unordered(array_value).peek_sql(n_rows) return self._start_query( @@ -1951,12 +1980,16 @@ def _to_sql( def _compile_ordered( self, array_value: core.ArrayValue ) -> bigframes.core.compile.OrderedIR: - return bigframes.core.compile.compile_ordered_ir(array_value.node) + return bigframes.core.compile.compile_ordered_ir( + self._with_cached_executions(array_value.node) + ) def _compile_unordered( self, array_value: core.ArrayValue ) -> bigframes.core.compile.UnorderedIR: - return bigframes.core.compile.compile_unordered_ir(array_value.node) + return bigframes.core.compile.compile_unordered_ir( + self._with_cached_executions(array_value.node) + ) def _get_table_size(self, destination_table): table = self.bqclient.get_table(destination_table) From 4c4415fb137e3baedc4b2d77ec146827b003557e Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 21 May 2024 18:31:23 -0700 Subject: [PATCH 588/921] feat: support `ml.SimpleImputer` in bigframes (#708) * feat: support ml.Imputer in bigframes * address comments * address more comments * address more comments --- bigframes/ml/compose.py | 17 ++- bigframes/ml/impute.py | 123 ++++++++++++++++++ bigframes/ml/loader.py | 2 + bigframes/ml/pipeline.py | 11 +- bigframes/ml/preprocessing.py | 1 + bigframes/ml/sql.py | 9 ++ docs/reference/bigframes.ml/impute.rst | 7 + docs/reference/bigframes.ml/index.rst | 2 + docs/templates/toc.yml | 6 + tests/system/conftest.py | 14 ++ tests/system/large/ml/test_pipeline.py | 54 ++++++++ tests/system/small/ml/test_impute.py | 89 +++++++++++++ tests/unit/ml/test_sql.py | 7 + .../sklearn/impute/_base.py | 53 ++++++++ 14 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 bigframes/ml/impute.py create mode 100644 docs/reference/bigframes.ml/impute.rst create mode 100644 tests/system/small/ml/test_impute.py create mode 100644 third_party/bigframes_vendored/sklearn/impute/_base.py diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 77bfd76bdeb..abf1a662b9d 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -28,7 +28,7 @@ from bigframes import constants from bigframes.core import log_adapter -from bigframes.ml import base, core, globals, preprocessing, utils +from bigframes.ml import base, core, globals, impute, preprocessing, utils import bigframes.pandas as bpd _BQML_TRANSFROM_TYPE_MAPPING = types.MappingProxyType( @@ -40,6 +40,7 @@ "ML.BUCKETIZE": preprocessing.KBinsDiscretizer, "ML.QUANTILE_BUCKETIZE": preprocessing.KBinsDiscretizer, "ML.LABEL_ENCODER": preprocessing.LabelEncoder, + "ML.IMPUTER": impute.SimpleImputer, } ) @@ -58,7 +59,7 @@ def __init__( transformers: List[ Tuple[ str, - preprocessing.PreprocessingType, + Union[preprocessing.PreprocessingType, impute.SimpleImputer], Union[str, List[str]], ] ], @@ -73,12 +74,14 @@ def __init__( @property def transformers_( self, - ) -> List[Tuple[str, preprocessing.PreprocessingType, str,]]: + ) -> List[ + Tuple[str, Union[preprocessing.PreprocessingType, impute.SimpleImputer], str] + ]: """The collection of transformers as tuples of (name, transformer, column).""" result: List[ Tuple[ str, - preprocessing.PreprocessingType, + Union[preprocessing.PreprocessingType, impute.SimpleImputer], str, ] ] = [] @@ -107,7 +110,7 @@ def _extract_from_bq_model( transformers: List[ Tuple[ str, - preprocessing.PreprocessingType, + Union[preprocessing.PreprocessingType, impute.SimpleImputer], Union[str, List[str]], ] ] = [] @@ -152,7 +155,9 @@ def camel_to_snake(name): def _merge( self, bq_model: bigquery.Model - ) -> Union[ColumnTransformer, preprocessing.PreprocessingType,]: + ) -> Union[ + ColumnTransformer, Union[preprocessing.PreprocessingType, impute.SimpleImputer] + ]: """Try to merge the column transformer to a simple transformer. Depends on all the columns in bq_model are transformed with the same transformer.""" transformers = self.transformers_ diff --git a/bigframes/ml/impute.py b/bigframes/ml/impute.py new file mode 100644 index 00000000000..d21fcbb1ad1 --- /dev/null +++ b/bigframes/ml/impute.py @@ -0,0 +1,123 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Transformers for missing value imputation. This module is styled after +scikit-learn's preprocessing module: https://scikit-learn.org/stable/modules/impute.html.""" + +from __future__ import annotations + +import typing +from typing import Any, List, Literal, Optional, Tuple, Union + +import bigframes_vendored.sklearn.impute._base + +from bigframes.core import log_adapter +from bigframes.ml import base, core, globals, utils +import bigframes.pandas as bpd + + +@log_adapter.class_logger +class SimpleImputer( + base.Transformer, + bigframes_vendored.sklearn.impute._base.SimpleImputer, +): + + __doc__ = bigframes_vendored.sklearn.impute._base.SimpleImputer.__doc__ + + def __init__( + self, + strategy: Literal["mean", "median", "most_frequent"] = "mean", + ): + self.strategy = strategy + self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return ( + type(other) is SimpleImputer + and self.strategy == other.strategy + and self._bqml_model == other._bqml_model + ) + + def _compile_to_sql( + self, + columns: List[str], + X=None, + ) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: + A list of column names to transform. + X: + The Dataframe with training data. + + Returns: a list of tuples of (sql_expression, output_name)""" + return [ + ( + self._base_sql_generator.ml_imputer( + column, self.strategy, f"imputer_{column}" + ), + f"imputer_{column}", + ) + for column in columns + ] + + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[SimpleImputer, str]: + """Parse SQL to tuple(SimpleImputer, column_label). + + Args: + sql: SQL string of format "ML.IMPUTER({col_label}, {strategy}) OVER()" + + Returns: + tuple(SimpleImputer, column_label)""" + s = sql[sql.find("(") + 1 : sql.find(")")] + col_label, strategy = s.split(", ") + return cls(strategy[1:-1]), col_label # type: ignore[arg-type] + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> SimpleImputer: + (X,) = utils.convert_to_dataframe(X) + + compiled_transforms = self._compile_to_sql(X.columns.tolist(), X) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = self._bqml_model_factory.create_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # The schema of TRANSFORM output is not available in the model API, so save it during fitting + self._output_names = [name for _, name in compiled_transforms] + return self + + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + (X,) = utils.convert_to_dataframe(X) + + df = self._bqml_model.transform(X) + return typing.cast( + bpd.DataFrame, + df[self._output_names], + ) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 916949077fa..8ae8d643013 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -29,6 +29,7 @@ ensemble, forecasting, imported, + impute, linear_model, llm, pipeline, @@ -84,6 +85,7 @@ def from_bq( pipeline.Pipeline, compose.ColumnTransformer, preprocessing.PreprocessingType, + impute.SimpleImputer, ]: """Load a BQML model to BigQuery DataFrames ML. diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 61b5a22da9a..03e5688453f 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -26,7 +26,15 @@ import bigframes import bigframes.constants as constants from bigframes.core import log_adapter -from bigframes.ml import base, compose, forecasting, loader, preprocessing, utils +from bigframes.ml import ( + base, + compose, + forecasting, + impute, + loader, + preprocessing, + utils, +) import bigframes.pandas as bpd @@ -56,6 +64,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): preprocessing.MinMaxScaler, preprocessing.KBinsDiscretizer, preprocessing.LabelEncoder, + impute.SimpleImputer, ), ): self._transform = transform diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 954d5adff0e..4b1a3fb7b73 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -305,6 +305,7 @@ def __eq__(self, other: Any) -> bool: return ( type(other) is KBinsDiscretizer and self.n_bins == other.n_bins + and self.strategy == other.strategy and self._bqml_model == other._bqml_model ) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index b701ab301c3..f060584a115 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -103,6 +103,15 @@ def ml_min_max_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.MIN_MAX_SCALER for BQML""" return f"""ML.MIN_MAX_SCALER({numeric_expr_sql}) OVER() AS {name}""" + def ml_imputer( + self, + expr_sql: str, + strategy: str, + name: str, + ) -> str: + """Encode ML.IMPUTER for BQML""" + return f"""ML.IMPUTER({expr_sql}, '{strategy}') OVER() AS {name}""" + def ml_bucketize( self, numeric_expr_sql: str, diff --git a/docs/reference/bigframes.ml/impute.rst b/docs/reference/bigframes.ml/impute.rst new file mode 100644 index 00000000000..3796e287ef9 --- /dev/null +++ b/docs/reference/bigframes.ml/impute.rst @@ -0,0 +1,7 @@ +bigframes.ml.impute +========================== + +.. automodule:: bigframes.ml.impute + :members: + :inherited-members: + :undoc-members: diff --git a/docs/reference/bigframes.ml/index.rst b/docs/reference/bigframes.ml/index.rst index 37504b0830d..c14efaede67 100644 --- a/docs/reference/bigframes.ml/index.rst +++ b/docs/reference/bigframes.ml/index.rst @@ -19,6 +19,8 @@ API Reference imported + impute + linear_model llm diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 67e628eb7dc..aee3058ecca 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -134,6 +134,12 @@ - name: XGBoostModel uid: bigframes.ml.imported.XGBoostModel name: imported + - items: + - name: Overview + uid: bigframes.ml.impute + - name: SimpleImputer + uid: bigframes.ml.impute.SimpleImputer + name: impute - items: - name: Overview uid: bigframes.ml.linear_model diff --git a/tests/system/conftest.py b/tests/system/conftest.py index ecf633b27fb..12a83a53eff 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -29,6 +29,7 @@ import google.cloud.resourcemanager_v3 as resourcemanager_v3 import google.cloud.storage as storage # type: ignore import ibis.backends.base +import numpy as np import pandas as pd import pytest import pytz @@ -36,6 +37,7 @@ import bigframes import bigframes.dataframe +import bigframes.pandas as bpd import tests.system.utils # Use this to control the number of cloud functions being deleted in a single @@ -624,6 +626,18 @@ def new_penguins_pandas_df(): ).set_index("tag_number") +@pytest.fixture(scope="session") +def missing_values_penguins_df(): + """Additional data matching the missing values penguins dataset""" + return bpd.DataFrame( + { + "culmen_length_mm": [39.5, 38.5, 37.9], + "culmen_depth_mm": [np.nan, 17.2, 18.1], + "flipper_length_mm": [np.nan, 181.0, 188.0], + } + ) + + @pytest.fixture(scope="session") def new_penguins_df(session, new_penguins_pandas_df): return session.read_pandas(new_penguins_pandas_df) diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 3d7eb2e426b..6e18248e0f6 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -20,6 +20,7 @@ compose, decomposition, ensemble, + impute, linear_model, pipeline, preprocessing, @@ -476,6 +477,11 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind preprocessing.KBinsDiscretizer(strategy="uniform"), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "simple_imputer", + impute.SimpleImputer(strategy="mean"), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -556,6 +562,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id preprocessing.KBinsDiscretizer(strategy="uniform"), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "simple_imputer", + impute.SimpleImputer(), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -600,6 +611,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id preprocessing.KBinsDiscretizer(strategy="uniform"), "culmen_length_mm", ), + ( + "simple_imputer", + impute.SimpleImputer(), + "culmen_length_mm", + ), ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), ("max_abs_scaler", preprocessing.MaxAbsScaler(), "flipper_length_mm"), ("min_max_scaler", preprocessing.MinMaxScaler(), "flipper_length_mm"), @@ -608,6 +624,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id preprocessing.KBinsDiscretizer(strategy="uniform"), "flipper_length_mm", ), + ( + "simple_imputer", + impute.SimpleImputer(), + "flipper_length_mm", + ), ] assert transformers == expected @@ -795,3 +816,36 @@ def test_pipeline_label_encoder_to_gbq(penguins_df_default_index, dataset_id): assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) assert pl_loaded._estimator.fit_intercept is False + + +def test_pipeline_simple_imputer_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ( + "transform", + impute.SimpleImputer(strategy="most_frequent"), + ), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "sex", + "species", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_simple_imputer", replace=True + ) + assert isinstance(pl_loaded._transform, impute.SimpleImputer) + + simple_imputer = pl_loaded._transform + assert simple_imputer.strategy == "most_frequent" + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False diff --git a/tests/system/small/ml/test_impute.py b/tests/system/small/ml/test_impute.py new file mode 100644 index 00000000000..46a614d7033 --- /dev/null +++ b/tests/system/small/ml/test_impute.py @@ -0,0 +1,89 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +from bigframes.ml import impute + + +def test_simple_imputer_fit_transform_default_params(missing_values_penguins_df): + imputer = impute.SimpleImputer(strategy="mean") + result = imputer.fit_transform( + missing_values_penguins_df[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ).to_pandas() + + expected = pd.DataFrame( + { + "imputer_culmen_length_mm": [39.5, 38.5, 37.9], + "imputer_culmen_depth_mm": [17.65, 17.2, 18.1], + "imputer_flipper_length_mm": [184.5, 181.0, 188.0], + }, + dtype="Float64", + index=pd.Index([0, 1, 2], dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected) + + +def test_simple_imputer_series(missing_values_penguins_df): + imputer = impute.SimpleImputer(strategy="mean") + imputer.fit(missing_values_penguins_df["culmen_depth_mm"]) + + result = imputer.transform( + missing_values_penguins_df["culmen_depth_mm"] + ).to_pandas() + + expected = pd.DataFrame( + { + "imputer_culmen_depth_mm": [17.65, 17.2, 18.1], + }, + dtype="Float64", + index=pd.Index([0, 1, 2], dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, rtol=0.1) + + +def test_simple_imputer_save_load_mean(missing_values_penguins_df, dataset_id): + transformer = impute.SimpleImputer(strategy="mean") + transformer.fit( + missing_values_penguins_df[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, impute.SimpleImputer) + assert reloaded_transformer.strategy == transformer.strategy + assert reloaded_transformer._bqml_model is not None + + +def test_simple_imputer_save_load_most_frequent(missing_values_penguins_df, dataset_id): + transformer = impute.SimpleImputer(strategy="most_frequent") + transformer.fit( + missing_values_penguins_df[ + ["culmen_length_mm", "culmen_depth_mm", "flipper_length_mm"] + ] + ) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, impute.SimpleImputer) + assert reloaded_transformer.strategy == transformer.strategy + assert reloaded_transformer._bqml_model is not None diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 07b247fb41c..01f173812c1 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -106,6 +106,13 @@ def test_min_max_scaler_correct( assert sql == "ML.MIN_MAX_SCALER(col_a) OVER() AS scaled_col_a" +def test_imputer_correct( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_imputer("col_a", "mean", "scaled_col_a") + assert sql == "ML.IMPUTER(col_a, 'mean') OVER() AS scaled_col_a" + + def test_k_bins_discretizer_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): diff --git a/third_party/bigframes_vendored/sklearn/impute/_base.py b/third_party/bigframes_vendored/sklearn/impute/_base.py new file mode 100644 index 00000000000..3064e8a1187 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/impute/_base.py @@ -0,0 +1,53 @@ +# Authors: Nicolas Tresegnie +# Sergey Feldman +# License: BSD 3 clause + +from bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin + +from bigframes import constants + + +class _BaseImputer(TransformerMixin, BaseEstimator): + """Base class for all imputers.""" + + +class SimpleImputer(_BaseImputer): + """ + Univariate imputer for completing missing values with simple strategies. + + Replace missing values using a descriptive statistic (e.g. mean, median, or + most frequent) along each column. + + Args: + strategy ({'mean', 'median', 'most_frequent'}, default='mean'): + The imputation strategy. 'mean': replace missing values using the mean along + the axis. 'median':replace missing values using the median along + the axis. 'most_frequent', replace missing using the most frequent + value along the axis. + """ + + def fit(self, X, y=None): + """Fit the imputer on X. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Dataframe or Series with training data. + + y (default None): + Ignored. + + Returns: + SimpleImputer: Fitted scaler. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transform(self, X): + """Impute all missing values in X. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. + + Returns: + bigframes.dataframe.DataFrame: Transformed result.""" + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 9035c3198c8ddb147c8db2eb3d4640bd6c842eb3 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 22 May 2024 18:59:08 +0200 Subject: [PATCH 589/921] chore(deps): update all dependencies (#692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update all dependencies * 🦉 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 --- samples/snippets/requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index 62b0c02e79c..beca2e44d9d 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -1,3 +1,3 @@ # samples/snippets should be runnable with no "extras" google-cloud-testutils==1.4.0 -pytest==8.1.1 +pytest==8.2.0 From e7da0f085eb9b9cec06e5de972f07d9c1d545ac7 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 22 May 2024 11:34:54 -0700 Subject: [PATCH 590/921] docs: add examples to DataFrameGroupBy and SeriesGroupBy (#701) * docs: add examples to DataFrameGroupBy and SeriesGroupBy * fixing * add alias methods docs --- .../pandas/core/groupby/__init__.py | 751 ++++++++++++++++++ 1 file changed, 751 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index f3f7748e34a..f9bedc2a7bb 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -21,6 +21,33 @@ def any(self): """ Return True if any value in the group is true, else False. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b'] + >>> ser = bpd.Series([1, 2, 0], index=lst) + >>> ser.groupby(level=0).any() + a True + b False + dtype: boolean + + For DataFrameGroupBy: + + >>> data = [[1, 0, 3], [1, 0, 6], [7, 1, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["ostrich", "penguin", "parrot"]) + >>> df.groupby(by=["a"]).any() + b c + a + 1 False True + 7 True True + + [2 rows x 2 columns] + Returns: Series or DataFrame: DataFrame or Series of boolean values, where a value is True if any element is True within its @@ -32,6 +59,33 @@ def all(self): """ Return True if all values in the group are true, else False. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b'] + >>> ser = bpd.Series([1, 2, 0], index=lst) + >>> ser.groupby(level=0).all() + a True + b False + dtype: boolean + + For DataFrameGroupBy: + + >>> data = [[1, 0, 3], [1, 5, 6], [7, 8, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["ostrich", "penguin", "parrot"]) + >>> df.groupby(by=["a"]).all() + b c + a + 1 False True + 7 True True + + [2 rows x 2 columns] + Returns: Series or DataFrame: DataFrame or Series of boolean values, where a value is True if all elements are True within its @@ -43,6 +97,34 @@ def count(self): """ Compute count of group, excluding missing values. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b'] + >>> ser = bpd.Series([1, 2, np.nan], index=lst) + >>> ser.groupby(level=0).count() + a 2 + b 0 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, np.nan, 3], [1, np.nan, 6], [7, 8, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["cow", "horse", "bull"]) + >>> df.groupby(by=["a"]).count() + b c + a + 1 0 2 + 7 1 1 + + [2 rows x 2 columns] + Returns: Series or DataFrame: Count of values within each group. """ @@ -55,6 +137,45 @@ def mean( """ Compute mean of groups, excluding missing values. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + >>> df = bpd.DataFrame({'A': [1, 1, 2, 1, 2], + ... 'B': [np.nan, 2, 3, 4, 5], + ... 'C': [1, 2, 1, 1, 2]}, columns=['A', 'B', 'C']) + + Groupby one column and return the mean of the remaining columns in each group. + + >>> df.groupby('A').mean() + B C + A + 1 3.0 1.333333 + 2 4.0 1.5 + + [2 rows x 2 columns] + + Groupby two columns and return the mean of the remaining column. + + >>> df.groupby(['A', 'B']).mean() + C + A B + 1 2.0 2.0 + 4.0 1.0 + 2 3.0 1.0 + 5.0 2.0 + + [4 rows x 1 columns] + + Groupby one column and return the mean of only particular column in the group. + + >>> df.groupby('A')['B'].mean() + A + 1 3.0 + 2 4.0 + Name: B, dtype: Float64 + Args: numeric_only (bool, default False): Include only float, int, boolean columns. @@ -73,6 +194,33 @@ def median( """ Compute median of groups, excluding missing values. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'a', 'b', 'b', 'b'] + >>> ser = bpd.Series([7, 2, 8, 4, 3, 3], index=lst) + >>> ser.groupby(level=0).median() + a 7.0 + b 3.0 + dtype: Float64 + + For DataFrameGroupBy: + + >>> data = {'a': [1, 3, 5, 7, 7, 8, 3], 'b': [1, 4, 8, 4, 4, 2, 1]} + >>> df = bpd.DataFrame(data, index=['dog', 'dog', 'dog', + ... 'mouse', 'mouse', 'mouse', 'mouse']) + >>> df.groupby(level=0).median() + a b + dog 3.0 4.0 + mouse 7.0 3.0 + + [2 rows x 2 columns] + Args: numeric_only (bool, default False): Include only float, int, boolean columns. @@ -125,6 +273,33 @@ def std( For multiple groupings, the result index will be a MultiIndex. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'a', 'b', 'b', 'b'] + >>> ser = bpd.Series([7, 2, 8, 4, 3, 3], index=lst) + >>> ser.groupby(level=0).std() + a 3.21455 + b 0.57735 + dtype: Float64 + + For DataFrameGroupBy: + + >>> data = {'a': [1, 3, 5, 7, 7, 8, 3], 'b': [1, 4, 8, 4, 4, 2, 1]} + >>> df = bpd.DataFrame(data, index=['dog', 'dog', 'dog', + ... 'mouse', 'mouse', 'mouse', 'mouse']) + >>> df.groupby(level=0).std() + a b + dog 2.0 3.511885 + mouse 2.217356 1.5 + + [2 rows x 2 columns] + Args: numeric_only (bool, default False): Include only `float`, `int` or `boolean` data. @@ -144,6 +319,33 @@ def var( For multiple groupings, the result index will be a MultiIndex. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'a', 'b', 'b', 'b'] + >>> ser = bpd.Series([7, 2, 8, 4, 3, 3], index=lst) + >>> ser.groupby(level=0).var() + a 10.333333 + b 0.333333 + dtype: Float64 + + For DataFrameGroupBy: + + >>> data = {'a': [1, 3, 5, 7, 7, 8, 3], 'b': [1, 4, 8, 4, 4, 2, 1]} + >>> df = bpd.DataFrame(data, index=['dog', 'dog', 'dog', + ... 'mouse', 'mouse', 'mouse', 'mouse']) + >>> df.groupby(level=0).var() + a b + dog 4.0 12.333333 + mouse 4.916667 2.25 + + [2 rows x 2 columns] + Args: numeric_only (bool, default False): Include only `float`, `int` or `boolean` data. @@ -164,6 +366,23 @@ def skew( Normalized by N-1. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> ser = bpd.Series([390., 350., 357., np.nan, 22., 20., 30.], + ... index=['Falcon', 'Falcon', 'Falcon', 'Falcon', + ... 'Parrot', 'Parrot', 'Parrot'], + ... name="Max Speed") + >>> ser.groupby(level=0).skew() + Falcon 1.525174 + Parrot 1.457863 + Name: Max Speed, dtype: Float64 + Args: numeric_only (bool, default False): Include only `float`, `int` or `boolean` data. @@ -185,6 +404,51 @@ def kurt( Kurtosis obtained using Fisher's definition of kurtosis (kurtosis of normal == 0.0). Normalized by N-1. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b', 'b'] + >>> ser = bpd.Series([0, 1, 1, 0, 0, 1, 2, 4, 5], index=lst) + >>> ser.groupby(level=0).kurt() + a -6.0 + b -1.963223 + dtype: Float64 + + Args: + numeric_only (bool, default False): + Include only `float`, `int` or `boolean` data. + + Returns: + Series or DataFrame + Variance of values within each group. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def kurtosis( + self, + *, + numeric_only: bool = False, + ): + """ + Return unbiased kurtosis over requested axis. + + Kurtosis obtained using Fisher's definition of + kurtosis (kurtosis of normal == 0.0). Normalized by N-1. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b', 'b'] + >>> ser = bpd.Series([0, 1, 1, 0, 0, 1, 2, 4, 5], index=lst) + >>> ser.groupby(level=0).kurtosis() + a -6.0 + b -1.963223 + dtype: Float64 + Args: numeric_only (bool, default False): Include only `float`, `int` or `boolean` data. @@ -203,6 +467,33 @@ def sum( """ Compute sum of group values. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b', 'b'] + >>> ser = bpd.Series([1, 2, 3, 4], index=lst) + >>> ser.groupby(level=0).sum() + a 3 + b 7 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, 8, 2], [1, 2, 5], [2, 5, 8], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["tiger", "leopard", "cheetah", "lion"]) + >>> df.groupby("a").sum() + b c + a + 1 10 7 + 2 11 17 + + [2 rows x 2 columns] + Args: numeric_only (bool, default False): Include only float, int, boolean columns. @@ -218,6 +509,22 @@ def sum( def prod(self, numeric_only: bool = False, min_count: int = 0): """ Compute prod of group values. + (DataFrameGroupBy functionality is not yet available.) + + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b', 'b'] + >>> ser = bpd.Series([1, 2, 3, 4], index=lst) + >>> ser.groupby(level=0).prod() + a 2.0 + b 12.0 + dtype: Float64 Args: numeric_only (bool, default False): @@ -239,6 +546,34 @@ def min( """ Compute min of group values. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b', 'b'] + >>> ser = bpd.Series([1, 2, 3, 4], index=lst) + >>> ser.groupby(level=0).min() + a 1 + b 3 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, 8, 2], [1, 2, 5], [2, 5, 8], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["tiger", "leopard", "cheetah", "lion"]) + >>> df.groupby(by=["a"]).min() + b c + a + 1 2 2 + 2 5 8 + + [2 rows x 2 columns] + Args: numeric_only (bool, default False): Include only float, int, boolean columns. @@ -259,6 +594,33 @@ def max( """ Compute max of group values. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b', 'b'] + >>> ser = bpd.Series([1, 2, 3, 4], index=lst) + >>> ser.groupby(level=0).max() + a 2 + b 4 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, 8, 2], [1, 2, 5], [2, 5, 8], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["tiger", "leopard", "cheetah", "lion"]) + >>> df.groupby(by=["a"]).max() + b c + a + 1 8 5 + 2 6 9 + + [2 rows x 2 columns] + Args: numeric_only (bool, default False): Include only float, int, boolean columns. @@ -274,6 +636,31 @@ def max( def cumcount(self, ascending: bool = True): """ Number each item in each group from 0 to the length of that group - 1. + (DataFrameGroupBy functionality is not yet available.) + + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b', 'b', 'c'] + >>> ser = bpd.Series([5, 1, 2, 3, 4], index=lst) + >>> ser.groupby(level=0).cumcount() + a 0 + a 1 + b 0 + b 1 + c 0 + dtype: Int64 + >>> ser.groupby(level=0).cumcount(ascending=False) + a 0 + a 1 + b 0 + b 1 + c 0 + dtype: Int64 Args: ascending (bool, default True): @@ -288,6 +675,35 @@ def cumprod(self, *args, **kwargs): """ Cumulative product for each group. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b'] + >>> ser = bpd.Series([6, 2, 0], index=lst) + >>> ser.groupby(level=0).cumprod() + a 6.0 + a 12.0 + b 0.0 + dtype: Float64 + + For DataFrameGroupBy: + + >>> data = [[1, 8, 2], [1, 2, 5], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["cow", "horse", "bull"]) + >>> df.groupby("a").cumprod() + b c + cow 8.0 2.0 + horse 16.0 10.0 + bull 6.0 9.0 + + [3 rows x 2 columns] + Returns: Series or DataFrame: Cumulative product for each group. """ @@ -297,6 +713,35 @@ def cumsum(self, *args, **kwargs): """ Cumulative sum for each group. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b'] + >>> ser = bpd.Series([6, 2, 0], index=lst) + >>> ser.groupby(level=0).cumsum() + a 6 + a 8 + b 0 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, 8, 2], [1, 2, 5], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["fox", "gorilla", "lion"]) + >>> df.groupby("a").cumsum() + b c + fox 8 2 + gorilla 10 7 + lion 6 9 + + [3 rows x 2 columns] + Returns: Series or DataFrame: Cumulative sum for each group. """ @@ -306,6 +751,35 @@ def cummin(self, *args, numeric_only: bool = False, **kwargs): """ Cumulative min for each group. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b'] + >>> ser = bpd.Series([6, 2, 0], index=lst) + >>> ser.groupby(level=0).cummin() + a 6 + a 2 + b 0 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, 8, 2], [1, 2, 5], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["fox", "gorilla", "lion"]) + >>> df.groupby("a").cummin() + b c + fox 8 2 + gorilla 2 2 + lion 6 9 + + [3 rows x 2 columns] + Returns: Series or DataFrame: Cumulative min for each group. """ @@ -315,6 +789,35 @@ def cummax(self, *args, numeric_only: bool = False, **kwargs): """ Cumulative max for each group. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b'] + >>> ser = bpd.Series([6, 2, 0], index=lst) + >>> ser.groupby(level=0).cummax() + a 6 + a 6 + b 0 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, 8, 2], [1, 2, 5], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["fox", "gorilla", "lion"]) + >>> df.groupby("a").cummax() + b c + fox 8 2 + gorilla 8 5 + lion 6 9 + + [3 rows x 2 columns] + Returns: Series or DataFrame: Cumulative max for each group. """ @@ -326,6 +829,42 @@ def diff(self): Calculates the difference of each element compared with another element in the group (default is element in previous row). + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'a', 'b', 'b', 'b'] + >>> ser = bpd.Series([7, 2, 8, 4, 3, 3], index=lst) + >>> ser.groupby(level=0).diff() + a + a -5 + a 6 + b + b -1 + b 0 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = {'a': [1, 3, 5, 7, 7, 8, 3], 'b': [1, 4, 8, 4, 4, 2, 1]} + >>> df = bpd.DataFrame(data, index=['dog', 'dog', 'dog', + ... 'mouse', 'mouse', 'mouse', 'mouse']) + >>> df.groupby(level=0).diff() + a b + dog + dog 2 3 + dog 2 4 + mouse + mouse 0 0 + mouse 1 -2 + mouse -5 -1 + + [7 rows x 2 columns] + Returns: Series or DataFrame: First differences. """ @@ -335,6 +874,37 @@ def shift(self, periods: int = 1): """ Shift each group by periods observations. + **Examples:** + + For SeriesGroupBy: + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b', 'b'] + >>> ser = bpd.Series([1, 2, 3, 4], index=lst) + >>> ser.groupby(level=0).shift(1) + a + a 1 + b + b 3 + dtype: Int64 + + For DataFrameGroupBy: + + >>> data = [[1, 2, 3], [1, 5, 6], [2, 5, 8], [2, 6, 9]] + >>> df = bpd.DataFrame(data, columns=["a", "b", "c"], + ... index=["tuna", "salmon", "catfish", "goldfish"]) + >>> df.groupby("a").shift(1) + b c + tuna + salmon 2 3 + catfish + goldfish 5 8 + + [4 rows x 2 columns] + Args: periods (int, default 1): Number of periods to shift. @@ -348,6 +918,23 @@ def rolling(self, *args, **kwargs): """ Returns a rolling grouper, providing rolling functionality per group. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'a', 'a', 'e'] + >>> ser = bpd.Series([1, 0, -2, -1, 2], index=lst) + >>> ser.groupby(level=0).rolling(2).min() + index index + a a + a 0 + a -2 + a -2 + e e + dtype: Int64 + Args: min_periods (int, default None): Minimum number of observations in window required to have a value; @@ -368,6 +955,23 @@ def expanding(self, *args, **kwargs): """ Provides expanding functionality. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'c', 'c', 'e'] + >>> ser = bpd.Series([1, 0, -2, -1, 2], index=lst) + >>> ser.groupby(level=0).expanding().min() + index index + a a 1 + a 0 + c c -2 + c -2 + e e 2 + dtype: Int64 + Returns: Series or DataFrame: An expanding grouper, providing expanding functionality per group. """ @@ -379,6 +983,52 @@ def agg(self, func): """ Aggregate using one or more operations. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3, 4], index=[1, 1, 2, 2]) + >>> s.groupby(level=0).agg(['min', 'max']) + min max + 1 1 2 + 2 3 4 + + [2 rows x 2 columns] + + Args: + func : function, str, list, dict or None + Function to use for aggregating the data. + + Accepted combinations are: + + - string function name + - list of function names, e.g. ``['sum', 'mean']`` + + Returns: + Series or DataFrame + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def aggregate(self, func): + """ + Aggregate using one or more operations. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([1, 2, 3, 4], index=[1, 1, 2, 2]) + >>> s.groupby(level=0).aggregate(['min', 'max']) + min max + 1 1 2 + 2 3 4 + + [2 rows x 2 columns] + Args: func : function, str, list, dict or None Function to use for aggregating the data. @@ -397,6 +1047,19 @@ def nunique(self): """ Return number of unique elements in the group. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> lst = ['a', 'a', 'b', 'b'] + >>> ser = bpd.Series([1, 2, 3, 3], index=lst) + >>> ser.groupby(level=0).nunique() + a 2 + b 1 + dtype: Int64 + Returns: Series: Number of unique values within each group. """ @@ -408,6 +1071,75 @@ def agg(self, func, **kwargs): """ Aggregate using one or more operations. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> data = {"A": [1, 1, 2, 2], + ... "B": [1, 2, 3, 4], + ... "C": [0.362838, 0.227877, 1.267767, -0.562860]} + >>> df = bpd.DataFrame(data) + + The aggregation is for each column. + + >>> df.groupby('A').agg('min') + B C + A + 1 1 0.227877 + 2 3 -0.56286 + + [2 rows x 2 columns] + + Args: + func (function, str, list, dict or None): + Function to use for aggregating the data. + + Accepted combinations are: + + - string function name + - list of function names, e.g. ``['sum', 'mean']`` + - dict of axis labels -> function names or list of such. + - None, in which case ``**kwargs`` are used with Named Aggregation. Here the + output has one column for each element in ``**kwargs``. The name of the + column is keyword, whereas the value determines the aggregation used to compute + the values in the column. + + kwargs + If ``func`` is None, ``**kwargs`` are used to define the output names and + aggregations via Named Aggregation. See ``func`` entry. + + Returns: + DataFrame + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def aggregate(self, func, **kwargs): + """ + Aggregate using one or more operations. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> data = {"A": [1, 1, 2, 2], + ... "B": [1, 2, 3, 4], + ... "C": [0.362838, 0.227877, 1.267767, -0.562860]} + >>> df = bpd.DataFrame(data) + + The aggregation is for each column. + + >>> df.groupby('A').aggregate('min') + B C + A + 1 1 0.227877 + 2 3 -0.56286 + + [2 rows x 2 columns] + Args: func (function, str, list, dict or None): Function to use for aggregating the data. @@ -435,6 +1167,25 @@ def nunique(self): """ Return DataFrame with counts of unique elements in each position. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'id': ['spam', 'egg', 'egg', 'spam', + ... 'ham', 'ham'], + ... 'value1': [1, 5, 5, 2, 5, 5], + ... 'value2': list('abbaxy')}) + >>> df.groupby('id').nunique() + value1 value2 + id + egg 1 1 + ham 1 2 + spam 2 1 + + [3 rows x 2 columns] + Returns: DataFrame """ From ffb4b5712a1a07c703ea88f66ba3f43dd2f98197 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 23 May 2024 11:46:50 -0700 Subject: [PATCH 591/921] fix: Fix Null index assign series to column (#711) --- bigframes/dataframe.py | 11 ++++++----- tests/system/small/test_empty_index.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index e8f219d301b..bb17bb9cb26 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -400,11 +400,12 @@ def memory_usage(self, index: bool = True): column_sizes = self.dtypes.map( lambda dtype: bigframes.dtypes.DTYPE_BYTE_SIZES.get(dtype, 8) * n_rows ) - if index: + if index and self._has_index: index_size = pandas.Series([self.index._memory_usage()], index=["Index"]) column_sizes = pandas.concat([index_size, column_sizes]) return column_sizes + @requires_index def info( self, verbose: Optional[bool] = None, @@ -768,7 +769,7 @@ def _apply_series_binop_axis_0( block = block.drop_columns([get_column_left[column_id]]) block = block.drop_columns([series_col]) - block = block.with_index_labels(self.index.names) + block = block.with_index_labels(self._block.index.names) return DataFrame(block) def _apply_series_binop_axis_1( @@ -1611,7 +1612,7 @@ def _assign_series_join_on_index( # Update case, remove after copying into columns block = block.drop_columns([source_column]) - return DataFrame(block.with_index_labels(self.index.names)) + return DataFrame(block.with_index_labels(self._block.index.names)) def reset_index(self, *, drop: bool = False) -> DataFrame: block = self._block.reset_index(drop) @@ -3283,7 +3284,7 @@ def _prepare_export( array_value = self._block.expr new_col_labels, new_idx_labels = utils.get_standardized_ids( - self._block.column_labels, self.index.names + self._block.column_labels, self._block.index.names ) columns = list(self._block.value_columns) @@ -3320,7 +3321,7 @@ def _run_io_query( session = self._block.expr.session self._optimize_query_complexity() export_array, id_overrides = self._prepare_export( - index=index, ordering_id=ordering_id + index=index and self._has_index, ordering_id=ordering_id ) _, query_job = session._execute( diff --git a/tests/system/small/test_empty_index.py b/tests/system/small/test_empty_index.py index 7a1715e3d13..3216264a8be 100644 --- a/tests/system/small/test_empty_index.py +++ b/tests/system/small/test_empty_index.py @@ -169,6 +169,21 @@ def test_empty_index_df_self_aligns( ) +def test_empty_index_setitem(scalars_df_empty_index, scalars_pandas_df_default_index): + bf_result = scalars_df_empty_index.copy() + bf_result["new_col"] = ( + scalars_df_empty_index["int64_col"] + scalars_df_empty_index["float64_col"] + ) + pd_result = scalars_pandas_df_default_index.copy() + pd_result["new_col"] = ( + scalars_pandas_df_default_index["int64_col"] + + scalars_pandas_df_default_index["float64_col"] + ) + pd.testing.assert_frame_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + def test_empty_index_df_concat(scalars_df_empty_index, scalars_pandas_df_default_index): bf_result = bpd.concat([scalars_df_empty_index, scalars_df_empty_index]) pd_result = pd.concat( From 085fa9d8fe1ea4cd02a3d25d443beaa697e10784 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Thu, 23 May 2024 14:47:34 -0700 Subject: [PATCH 592/921] fix: warn and disable time travel for linked datasets (#712) * fix: warn and disable time travel for linked datasets * add warning type --- bigframes/exceptions.py | 4 ++++ bigframes/session/__init__.py | 30 ++++++++++++++++++++++++++++-- tests/system/small/test_session.py | 8 ++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 027b3a42363..1162217fc1c 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -41,3 +41,7 @@ class PreviewWarning(Warning): class NullIndexError(ValueError): """Object has no index.""" + + +class TimeTravelDisabledWarning(Warning): + """A query was reattempted without time travel.""" diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 226af9ec5cc..45c1d15c5a5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -713,7 +713,8 @@ def _read_gbq_table( # Fetch table metadata and validate # --------------------------------- - (time_travel_timestamp, table,) = bf_read_gbq_table.get_table_metadata( + time_travel_timestamp: Optional[datetime.datetime] = None + time_travel_timestamp, table = bf_read_gbq_table.get_table_metadata( self.bqclient, table_ref=table_ref, api_name=api_name, @@ -795,9 +796,34 @@ def _read_gbq_table( # Use a time travel to make sure the DataFrame is deterministic, even # if the underlying table changes. - # TODO(b/340540991): If a dry run query fails with time travel but + + # If a dry run query fails with time travel but # succeeds without it, omit the time travel clause and raise a warning # about potential non-determinism if the underlying tables are modified. + sql = bigframes.session._io.bigquery.to_query( + f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}", + index_cols=index_cols, + columns=columns, + filters=filters, + time_travel_timestamp=time_travel_timestamp, + max_results=None, + ) + dry_run_config = bigquery.QueryJobConfig() + dry_run_config.dry_run = True + try: + self._start_query(sql, job_config=dry_run_config) + except google.api_core.exceptions.NotFound: + # note that a notfound caused by a simple typo will be + # caught above when the metadata is fetched, not here + time_travel_timestamp = None + warnings.warn( + "NotFound error when reading table with time travel." + " Attempting query without time travel. Warning: Without" + " time travel, modifications to the underlying table may" + " result in errors or unexpected behavior.", + category=bigframes.exceptions.TimeTravelDisabledWarning, + ) + table_expression = bf_read_gbq_table.get_ibis_time_travel_table( ibis_client=self.ibis_client, table_ref=table_ref, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 2b7c6178ff2..c617eab7f59 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -20,6 +20,7 @@ import time import typing from typing import List, Optional, Sequence +import warnings import google import google.cloud.bigquery as bigquery @@ -386,6 +387,13 @@ def test_read_gbq_twice_with_same_timestamp(session, penguins_table_id): assert df3 is not None +def test_read_gbq_on_linked_dataset_warns(session): + with warnings.catch_warnings(record=True) as warned: + session.read_gbq("bigframes-dev.thelook_ecommerce.orders") + assert len(warned) == 1 + assert warned[0].category == bigframes.exceptions.TimeTravelDisabledWarning + + def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): df = session.read_gbq_table( "bigquery-public-data.cloud_storage_geo_index.landsat_index", From d31cebd1387ed22618e063835e2c8a6996f08ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 23 May 2024 18:07:40 -0500 Subject: [PATCH 593/921] chore: make `cleanup_cloud_functions` test fixture more robust to flakiness (#720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: make `cleanup_cloud_functions` test fixture more robust to flakiness In response to a recent test failure in load test session just because this optional cleanup code failed. ``` ____________ ERROR at setup of test_read_gbq_sql_large_results[1gb] ____________ [gw1] linux -- Python 3.12.0 /tmpfs/src/github/python-bigquery-dataframes/.nox/load/bin/python ... tests/system/conftest.py:1119: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ tests/system/utils.py:349: in delete_cloud_function operation = functions_client.delete_function(request=request) .nox/load/lib/python3.12/site-packages/google/cloud/functions_v2/services/function_service/client.py:1499: in delete_function response = rpc( .nox/load/lib/python3.12/site-packages/google/api_core/gapic_v1/method.py:131: in __call__ return wrapped_func(*args, **kwargs) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (name: "projects/bigframes-load-testing/locations/us-central1/functions/bigframes-ab103e98fa51cfc67a56d754b7e5068e-u8owkeqc" ,) kwargs = {'metadata': [('x-goog-request-params', 'name=projects/bigframes-load-testing/locations/us-central1/functions/bigframe...6d754b7e5068e-u8owkeqc'), ('x-goog-api-client', 'bigframes/1.7.0 ibis/8.0.0 gl-python/3.12.0 grpc/1.64.0 gax/2.19.0')]} @functools.wraps(callable_) def error_remapped_callable(*args, **kwargs): try: return callable_(*args, **kwargs) except grpc.RpcError as exc: > raise exceptions.from_grpc_error(exc) from exc E google.api_core.exceptions.ServiceUnavailable: 503 502:Bad Gateway .nox/load/lib/python3.12/site-packages/google/api_core/grpc_helpers.py:78: ServiceUnavailable ``` * include warning about failed cleanup * 🦉 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 --- tests/system/conftest.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 12a83a53eff..1de1f54700b 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -20,6 +20,7 @@ import textwrap import typing from typing import Dict, Generator, Optional +import warnings import google.api_core.exceptions import google.cloud.bigquery as bigquery @@ -1129,7 +1130,9 @@ def cleanup_cloud_functions(session, cloudfunctions_client, dataset_id_permanent # successfully, while the other instance will run into this # exception. Ignore this exception. pass - except google.api_core.exceptions.ResourceExhausted: + except Exception as exc: + # Don't fail the tests for unknown exceptions. + # # This can happen if we are hitting GCP limits, e.g. # google.api_core.exceptions.ResourceExhausted: 429 Quota exceeded # for quota metric 'Per project mutation requests' and limit @@ -1137,5 +1140,11 @@ def cleanup_cloud_functions(session, cloudfunctions_client, dataset_id_permanent # 'cloudfunctions.googleapis.com' for consumer # 'project_number:1084210331973'. # [reason: "RATE_LIMIT_EXCEEDED" domain: "googleapis.com" ... + # + # It can also happen occasionally with + # google.api_core.exceptions.ServiceUnavailable when there is some + # backend flakiness. + # # Let's stop further clean up and leave it to later. + warnings.warn(f"Cloud functions cleanup failed: {str(exc)}") break From 354abc17b5bd55d70d47f893cfccd7cd0ac9794a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 23 May 2024 16:47:13 -0700 Subject: [PATCH 594/921] feat: Add DataFrame `~` operator (#721) --- bigframes/dataframe.py | 5 ++++ tests/system/small/test_dataframe.py | 10 ++++++++ .../bigframes_vendored/pandas/core/frame.py | 24 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index bb17bb9cb26..602865abd97 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -904,6 +904,11 @@ def __ne__(self, other) -> DataFrame: # type: ignore __ne__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__ne__) + def __invert__(self) -> DataFrame: + return self._apply_unary_op(ops.invert_op) + + __invert__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__invert__) + def le(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.le_op, axis=axis) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 4b50922c0d1..dbdcf7dafc1 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1699,6 +1699,16 @@ def test_df_abs(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) +def test_df_invert(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + columns = ["int64_col", "bool_col"] + + bf_result = (~scalars_df[columns]).to_pandas() + pd_result = ~scalars_pandas_df[columns] + + assert_pandas_df_equal(bf_result, pd_result) + + def test_df_isnull(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 31d5e88c7e8..01c7ab70ee3 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -2003,6 +2003,30 @@ def __eq__(self, other): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __invert__(self) -> DataFrame: + """ + Returns the bitwise inversion of the DataFrame, element-wise + using operator `~`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'a':[True, False, True], 'b':[-1, 0, 1]}) + >>> ~df + a b + 0 False 0 + 1 True -1 + 2 False -2 + + [3 rows x 2 columns] + + Returns: + DataFrame: The result of inverting elements in the input. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def ne(self, other, axis: str | int = "columns") -> DataFrame: """ Get not equal to of DataFrame and other, element-wise (binary operator `ne`). From 1832778cfc4f29fdab1b22380f03b192eb8aebb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 24 May 2024 17:48:22 -0500 Subject: [PATCH 595/921] feat: ensure `"bigframes-api"` label is always set on jobs, even if the API is unknown (#722) * feat: ensure `"bigframes-api"` label is always set on jobs, even if the API is unknown * remove some dead code. plumb through api_name * avoid . in label value * add tests --- .kokoro/continuous/e2e.cfg | 2 +- .kokoro/presubmit/e2e.cfg | 2 +- bigframes/core/log_adapter.py | 7 ++- bigframes/dataframe.py | 12 +++- bigframes/functions/remote_function.py | 15 +++-- bigframes/pandas/__init__.py | 8 ++- bigframes/session/__init__.py | 40 +++++++++----- bigframes/session/_io/bigquery/__init__.py | 47 +++++++++++++--- .../session/_io/bigquery/read_gbq_table.py | 55 +------------------ noxfile.py | 1 - tests/system/small/test_session.py | 20 ++++--- tests/unit/session/test_io_bigquery.py | 21 ++++++- 12 files changed, 131 insertions(+), 99 deletions(-) diff --git a/.kokoro/continuous/e2e.cfg b/.kokoro/continuous/e2e.cfg index 774b63313e1..3dbd0b47f06 100644 --- a/.kokoro/continuous/e2e.cfg +++ b/.kokoro/continuous/e2e.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "unit_prerelease system_prerelease system_noextras e2e notebook" + value: "e2e doctest notebook unit_prerelease system_prerelease system_noextras" } env_vars: { diff --git a/.kokoro/presubmit/e2e.cfg b/.kokoro/presubmit/e2e.cfg index 774b63313e1..3dbd0b47f06 100644 --- a/.kokoro/presubmit/e2e.cfg +++ b/.kokoro/presubmit/e2e.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "unit_prerelease system_prerelease system_noextras e2e notebook" + value: "e2e doctest notebook unit_prerelease system_prerelease system_noextras" } env_vars: { diff --git a/bigframes/core/log_adapter.py b/bigframes/core/log_adapter.py index 877e4a9fa1b..4afa6037de4 100644 --- a/bigframes/core/log_adapter.py +++ b/bigframes/core/log_adapter.py @@ -99,9 +99,12 @@ def add_api_method(api_method_name): _api_methods = _api_methods[:MAX_LABELS_COUNT] -def get_and_reset_api_methods(): +def get_and_reset_api_methods(dry_run: bool = False): global _lock with _lock: previous_api_methods = list(_api_methods) - _api_methods.clear() + + # dry_run might not make a job resource, so only reset the log on real queries. + if not dry_run: + _api_methods.clear() return previous_api_methods diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 602865abd97..12c96b90f06 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2912,7 +2912,9 @@ def to_csv( field_delimiter=sep, header=header, ) - _, query_job = self._block.expr.session._start_query(export_data_statement) + _, query_job = self._block.expr.session._start_query( + export_data_statement, api_name="dataframe-to_csv" + ) self._set_internal_query_job(query_job) def to_json( @@ -2954,7 +2956,9 @@ def to_json( format="JSON", export_options={}, ) - _, query_job = self._block.expr.session._start_query(export_data_statement) + _, query_job = self._block.expr.session._start_query( + export_data_statement, api_name="dataframe-to_json" + ) self._set_internal_query_job(query_job) def to_gbq( @@ -3086,7 +3090,9 @@ def to_parquet( format="PARQUET", export_options=export_options, ) - _, query_job = self._block.expr.session._start_query(export_data_statement) + _, query_job = self._block.expr.session._start_query( + export_data_statement, api_name="dataframe-to_parquet" + ) self._set_internal_query_job(query_job) def to_dict( diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 9eff802cc76..0ae90a28d3c 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -24,7 +24,7 @@ import sys import tempfile import textwrap -from typing import List, NamedTuple, Optional, Sequence, TYPE_CHECKING, Union +from typing import cast, List, NamedTuple, Optional, Sequence, TYPE_CHECKING, Union import warnings import ibis @@ -133,6 +133,8 @@ def __init__( cloud_function_service_account, cloud_function_kms_key_name, cloud_function_docker_repository, + *, + session: Session, ): self._gcp_project_id = gcp_project_id self._cloud_function_region = cloud_function_region @@ -145,6 +147,7 @@ def __init__( self._cloud_function_service_account = cloud_function_service_account self._cloud_function_kms_key_name = cloud_function_kms_key_name self._cloud_function_docker_repository = cloud_function_docker_repository + self._session = session def create_bq_remote_function( self, @@ -216,10 +219,8 @@ def create_bq_remote_function( # This requires bigquery.datasets.create IAM permission self._bq_client.create_dataset(dataset, exists_ok=True) - # TODO: Use session._start_query() so we get progress bar - query_job = self._bq_client.query(create_function_ddl) # Make an API request. - query_job.result() # Wait for the job to complete. - + # TODO(swast): plumb through the original, user-facing api_name. + _, query_job = self._session._start_query(create_function_ddl) logger.info(f"Created remote function {query_job.ddl_target_routine}") def get_cloud_function_fully_qualified_parent(self): @@ -910,6 +911,7 @@ def remote_function( is_row_processor = False import bigframes.series + import bigframes.session if input_types == bigframes.series.Series: warnings.warn( @@ -928,7 +930,7 @@ def remote_function( # Some defaults may be used from the session if not provided otherwise import bigframes.pandas as bpd - session = session or bpd.get_global_session() + session = cast(bigframes.session.Session, session or bpd.get_global_session()) # A BigQuery client is required to perform BQ operations if not bigquery_client: @@ -1040,6 +1042,7 @@ def wrapper(f): cloud_function_service_account, cloud_function_kms_key_name, cloud_function_docker_repository, + session=session, # type: ignore ) rf_name, cf_name = remote_function_client.provision_bq_remote_function( diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 3628ecf67b6..dc9b847a9b1 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -399,6 +399,9 @@ def _set_default_session_location_if_possible(query): bqclient = clients_provider.bqclient if bigframes.session._io.bigquery.is_query(query): + # Intentionally run outside of the session so that we can detect the + # location before creating the session. Since it's a dry_run, labels + # aren't necessary. job = bqclient.query(query, bigquery.QueryJobConfig(dry_run=True)) options.bigquery.location = job.location else: @@ -773,7 +776,10 @@ def clean_up_by_session_id( dataset = session._anonymous_dataset else: dataset = bigframes.session._io.bigquery.create_bq_dataset_reference( - client, location=location, project=project + client, + location=location, + project=project, + api_name="clean_up_by_session_id", ) bigframes.session._io.bigquery.delete_tables_matching_session_id( diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 45c1d15c5a5..83481a3ae9f 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -235,7 +235,9 @@ def __init__( self._anonymous_dataset = ( bigframes.session._io.bigquery.create_bq_dataset_reference( - self.bqclient, location=self._location + self.bqclient, + location=self._location, + api_name="session-__init__", ) ) @@ -420,9 +422,11 @@ def _query_to_destination( # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. dry_run_config = bigquery.QueryJobConfig() dry_run_config.dry_run = True - _, dry_run_job = self._start_query(query, job_config=dry_run_config) + _, dry_run_job = self._start_query( + query, job_config=dry_run_config, api_name=api_name + ) if dry_run_job.statement_type != "SELECT": - _, query_job = self._start_query(query) + _, query_job = self._start_query(query, api_name=api_name) return query_job.destination, query_job # Create a table to workaround BigQuery 10 GB query results limit. See: @@ -451,7 +455,6 @@ def _query_to_destination( bigquery.QueryJobConfig, bigquery.QueryJobConfig.from_api_repr(configuration), ) - job_config.labels["bigframes-api"] = api_name job_config.destination = temp_table try: @@ -459,7 +462,10 @@ def _query_to_destination( # limit. See: internal issue 303057336. job_config.labels["error_caught"] = "true" _, query_job = self._start_query( - query, job_config=job_config, timeout=timeout + query, + job_config=job_config, + timeout=timeout, + api_name=api_name, ) return query_job.destination, query_job except google.api_core.exceptions.BadRequest: @@ -467,7 +473,7 @@ def _query_to_destination( # tables as the destination. For example, if the query has a # top-level ORDER BY, this conflicts with our ability to cluster # the table by the index column(s). - _, query_job = self._start_query(query, timeout=timeout) + _, query_job = self._start_query(query, timeout=timeout, api_name=api_name) return query_job.destination, query_job def read_gbq_query( @@ -811,7 +817,7 @@ def _read_gbq_table( dry_run_config = bigquery.QueryJobConfig() dry_run_config.dry_run = True try: - self._start_query(sql, job_config=dry_run_config) + self._start_query(sql, job_config=dry_run_config, api_name=api_name) except google.api_core.exceptions.NotFound: # note that a notfound caused by a simple typo will be # caught above when the metadata is fetched, not here @@ -1777,12 +1783,6 @@ def _prepare_query_job_config( bigframes.options.compute.maximum_bytes_billed ) - current_labels = job_config.labels if job_config.labels else {} - for key, value in bigframes.options.compute.extra_query_labels.items(): - if key not in current_labels: - current_labels[key] = value - job_config.labels = current_labels - if self._bq_kms_key_name: job_config.destination_encryption_configuration = ( bigquery.EncryptionConfiguration(kms_key_name=self._bq_kms_key_name) @@ -1818,13 +1818,19 @@ def _start_query( job_config: Optional[bigquery.job.QueryJobConfig] = None, max_results: Optional[int] = None, timeout: Optional[float] = None, + api_name: Optional[str] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ Starts BigQuery query job and waits for results. """ job_config = self._prepare_query_job_config(job_config) return bigframes.session._io.bigquery.start_query_with_client( - self.bqclient, sql, job_config, max_results, timeout + self.bqclient, + sql, + job_config, + max_results, + timeout, + api_name=api_name, ) def _start_query_ml_ddl( @@ -1970,6 +1976,9 @@ def _execute( job_config = bigquery.QueryJobConfig(dry_run=dry_run) else: job_config.dry_run = dry_run + + # TODO(swast): plumb through the api_name of the user-facing api that + # caused this query. return self._start_query( sql=sql, job_config=job_config, @@ -1982,6 +1991,9 @@ def _peek( if not tree_properties.peekable(self._with_cached_executions(array_value.node)): warnings.warn("Peeking this value cannot be done efficiently.") sql = self._compile_unordered(array_value).peek_sql(n_rows) + + # TODO(swast): plumb through the api_name of the user-facing api that + # caused this query. return self._start_query( sql=sql, ) diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 28eed479657..cd6847c3126 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -28,6 +28,7 @@ import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions import google.cloud.bigquery as bigquery +import google.cloud.bigquery.table import bigframes from bigframes.core import log_adapter @@ -40,19 +41,34 @@ # will be limited to this many tables LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" +CHECK_DRIVE_PERMISSIONS = "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." def create_job_configs_labels( job_configs_labels: Optional[Dict[str, str]], api_methods: typing.List[str], + api_name: Optional[str] = None, ) -> Dict[str, str]: if job_configs_labels is None: job_configs_labels = {} - if api_methods: + # If the user has labels they wish to set, make sure we set those first so + # they are preserved. + for key, value in bigframes.options.compute.extra_query_labels.items(): + job_configs_labels[key] = value + + if api_name is not None: + job_configs_labels["bigframes-api"] = api_name + + if api_methods and "bigframes-api" not in job_configs_labels: job_configs_labels["bigframes-api"] = api_methods[0] del api_methods[0] + # Make sure we always populate bigframes-api with _something_, even if we + # have a code path which doesn't populate the list of api_methods. See + # internal issue 336521938. + job_configs_labels.setdefault("bigframes-api", "unknown") + labels = list( itertools.chain( job_configs_labels.keys(), @@ -193,27 +209,33 @@ def format_option(key: str, value: Union[bool, str]) -> str: return f"{key}={repr(value)}" +def add_labels(job_config, api_name: Optional[str] = None): + api_methods = log_adapter.get_and_reset_api_methods(dry_run=job_config.dry_run) + job_config.labels = create_job_configs_labels( + job_configs_labels=job_config.labels, + api_methods=api_methods, + api_name=api_name, + ) + + def start_query_with_client( bq_client: bigquery.Client, sql: str, job_config: bigquery.job.QueryJobConfig, max_results: Optional[int] = None, timeout: Optional[float] = None, + api_name: Optional[str] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ Starts query job and waits for results. """ - if not job_config.dry_run: - api_methods = log_adapter.get_and_reset_api_methods() - job_config.labels = create_job_configs_labels( - job_configs_labels=job_config.labels, api_methods=api_methods - ) + add_labels(job_config, api_name=api_name) try: query_job = bq_client.query(sql, job_config=job_config, timeout=timeout) except google.api_core.exceptions.Forbidden as ex: if "Drive credentials" in ex.message: - ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." + ex.message += CHECK_DRIVE_PERMISSIONS raise opts = bigframes.options.display @@ -286,7 +308,10 @@ def delete_tables_matching_session_id( def create_bq_dataset_reference( - bq_client: bigquery.Client, location=None, project=None + bq_client: bigquery.Client, + location=None, + project=None, + api_name: str = "unknown", ) -> bigquery.DatasetReference: """Create and identify dataset(s) for temporary BQ resources. @@ -307,7 +332,11 @@ def create_bq_dataset_reference( Returns: bigquery.DatasetReference: The constructed reference to the anonymous dataset. """ - query_job = bq_client.query("SELECT 1", location=location, project=project) + job_config = google.cloud.bigquery.QueryJobConfig() + add_labels(job_config, api_name=api_name) + query_job = bq_client.query( + "SELECT 1", location=location, project=project, job_config=job_config + ) query_job.result() # blocks until finished # The anonymous dataset is used by BigQuery to write query results and diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 063dde2a246..e00892fce99 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -113,8 +113,10 @@ def get_table_metadata( # atomically. table = bqclient.get_table(table_ref) + # TODO(swast): Use session._start_query instead? + # TODO(swast): Use query_and_wait since we know these are small results. job_config = bigquery.QueryJobConfig() - job_config.labels["bigframes-api"] = api_name + bigframes.session._io.bigquery.add_labels(job_config, api_name=api_name) snapshot_timestamp = list( bqclient.query( "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", @@ -285,57 +287,6 @@ def get_index_cols( return index_cols -def get_time_travel_datetime_and_table_metadata( - bqclient: bigquery.Client, - table_ref: bigquery.TableReference, - *, - api_name: str, - cache: Dict[bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table]], - use_cache: bool = True, -) -> Tuple[datetime.datetime, bigquery.Table]: - cached_table = cache.get(table_ref) - if use_cache and cached_table is not None: - snapshot_timestamp, _ = cached_table - - # Cache hit could be unexpected. See internal issue 329545805. - # Raise a warning with more information about how to avoid the - # problems with the cache. - warnings.warn( - f"Reading cached table from {snapshot_timestamp} to avoid " - "incompatibilies with previous reads of this table. To read " - "the latest version, set `use_cache=False` or close the " - "current session with Session.close() or " - "bigframes.pandas.close_session().", - # There are many layers before we get to (possibly) the user's code: - # pandas.read_gbq_table - # -> with_default_session - # -> Session.read_gbq_table - # -> _read_gbq_table - # -> _get_snapshot_sql_and_primary_key - # -> get_snapshot_datetime_and_table_metadata - stacklevel=7, - ) - return cached_table - - # TODO(swast): It's possible that the table metadata is changed between now - # and when we run the CURRENT_TIMESTAMP() query to see when we can time - # travel to. Find a way to fetch the table metadata and BQ's current time - # atomically. - table = bqclient.get_table(table_ref) - - job_config = bigquery.QueryJobConfig() - job_config.labels["bigframes-api"] = api_name - snapshot_timestamp = list( - bqclient.query( - "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", - job_config=job_config, - ).result() - )[0][0] - cached_table = (snapshot_timestamp, table) - cache[table_ref] = cached_table - return cached_table - - def to_array_value_with_total_ordering( session: bigframes.session.Session, table_expression: ibis_types.Table, diff --git a/noxfile.py b/noxfile.py index c816ec5f511..c6e8da8c816 100644 --- a/noxfile.py +++ b/noxfile.py @@ -91,7 +91,6 @@ "unit", "unit_noextras", "system", - "doctest", "cover", ] diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index c617eab7f59..9631e0c7ab5 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -545,18 +545,22 @@ def test_read_gbq_with_configuration( def test_read_gbq_with_custom_global_labels( session: bigframes.Session, scalars_table_id: str ): - bigframes.options.compute.assign_extra_query_labels(test1=1, test2="abc") - bigframes.options.compute.extra_query_labels["test3"] = False + # Ensure we use thread-local variables to avoid conflicts with parallel tests. + with bigframes.option_context("compute.extra_query_labels", {}): + bigframes.options.compute.assign_extra_query_labels(test1=1, test2="abc") + bigframes.options.compute.extra_query_labels["test3"] = False - job_labels = session.read_gbq(scalars_table_id).query_job.labels # type:ignore - expected_labels = {"test1": "1", "test2": "abc", "test3": "false"} + job_labels = session.read_gbq(scalars_table_id).query_job.labels # type:ignore + expected_labels = {"test1": "1", "test2": "abc", "test3": "false"} - assert all(job_labels.get(key) == value for key, value in expected_labels.items()) + # All jobs should include a bigframes-api key. See internal issue 336521938. + assert "bigframes-api" in job_labels - del bigframes.options.compute.extra_query_labels["test1"] - del bigframes.options.compute.extra_query_labels["test2"] - del bigframes.options.compute.extra_query_labels["test3"] + assert all( + job_labels.get(key) == value for key, value in expected_labels.items() + ) + # No labels outside of the option_context. assert len(bigframes.options.compute.extra_query_labels) == 0 diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 8ba13a72762..d687643c8a3 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -36,6 +36,25 @@ def test_create_job_configs_labels_is_none(): assert labels == expected_dict +def test_create_job_configs_labels_always_includes_bigframes_api(): + labels = io_bq.create_job_configs_labels(None, []) + assert labels == { + "bigframes-api": "unknown", + } + + +def test_create_job_configs_labels_includes_extra_query_labels(): + user_labels = {"my-label-1": "my-value-1", "my-label-2": "my-value-2"} + + with bigframes.option_context("compute.extra_query_labels", user_labels): + labels = io_bq.create_job_configs_labels(None, []) + assert labels == { + "my-label-1": "my-value-1", + "my-label-2": "my-value-2", + "bigframes-api": "unknown", + } + + def test_create_job_configs_labels_length_limit_not_met(): cur_labels = { "source": "bigquery-dataframes-temp", @@ -105,7 +124,7 @@ def test_create_job_configs_labels_length_limit_met(): "bigframes-api": "read_pandas", "source": "bigquery-dataframes-temp", } - for i in range(100): + for i in range(61): key = f"bigframes-api-test-{i}" value = f"test{i}" cur_labels[key] = value From 1fca588e4398baa0dae61bdea0d3bff17e3971b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 24 May 2024 18:57:36 -0500 Subject: [PATCH 596/921] feat: add `GroupBy.size()` to get number of rows in each group (#479) --- bigframes/core/blocks.py | 28 +++++ bigframes/core/compile/aggregate_compiler.py | 19 +++- bigframes/core/expression.py | 10 ++ bigframes/core/groupby/__init__.py | 21 ++++ bigframes/operations/aggregations.py | 23 ++++ tests/system/small/test_groupby.py | 107 ++++++++++++++---- .../ibis/expr/operations/analytic.py | 10 ++ 7 files changed, 194 insertions(+), 24 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index f189bb704e0..010eb96f751 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1014,6 +1014,34 @@ def aggregate_all_and_stack( index_labels=self.index.names, ) + def aggregate_size( + self, + by_column_ids: typing.Sequence[str] = (), + *, + dropna: bool = True, + ): + """Returns a block object to compute the size(s) of groups.""" + agg_specs = [ + (ex.NullaryAggregation(agg_ops.SizeOp()), guid.generate_guid()), + ] + output_col_ids = [agg_spec[1] for agg_spec in agg_specs] + result_expr = self.expr.aggregate(agg_specs, by_column_ids, dropna=dropna) + names: typing.List[Label] = [] + for by_col_id in by_column_ids: + if by_col_id in self.value_columns: + names.append(self.col_id_to_label[by_col_id]) + else: + names.append(self.col_id_to_index_name[by_col_id]) + return ( + Block( + result_expr, + index_columns=by_column_ids, + column_labels=["size"], + index_labels=names, + ), + output_col_ids, + ) + def select_column(self, id: str) -> Block: return self.select_columns([id]) diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index c0b0562a547..fada4ebbd84 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -36,6 +36,8 @@ def compile_aggregate( bindings: typing.Dict[str, ibis_types.Value], order_by: typing.Sequence[ibis_types.Value] = [], ) -> ibis_types.Value: + if isinstance(aggregate, ex.NullaryAggregation): + return compile_nullary_agg(aggregate.op) if isinstance(aggregate, ex.UnaryAggregation): input = scalar_compiler.compile_expression(aggregate.arg, bindings=bindings) if aggregate.op.can_order_by: @@ -55,7 +57,9 @@ def compile_analytic( window: window_spec.WindowSpec, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: - if isinstance(aggregate, ex.UnaryAggregation): + if isinstance(aggregate, ex.NullaryAggregation): + return compile_nullary_agg(aggregate.op, window) + elif isinstance(aggregate, ex.UnaryAggregation): input = scalar_compiler.compile_expression(aggregate.arg, bindings=bindings) return compile_unary_agg(aggregate.op, input, window) elif isinstance(aggregate, ex.BinaryAggregation): @@ -93,6 +97,14 @@ def compile_ordered_unary_agg( raise ValueError(f"Can't compile unrecognized operation: {op}") +@functools.singledispatch +def compile_nullary_agg( + op: agg_ops.WindowOp, + window: Optional[window_spec.WindowSpec] = None, +) -> ibis_types.Value: + raise ValueError(f"Can't compile unrecognized operation: {op}") + + def numeric_op(operation): @functools.wraps(operation) def constrained_op( @@ -118,6 +130,11 @@ def constrained_op( ### Specific Op implementations Below +@compile_nullary_agg.register +def _(op: agg_ops.SizeOp, window=None) -> ibis_types.NumericValue: + return _apply_window_if_present(vendored_ibis_ops.count(1), window) + + @compile_unary_agg.register @numeric_op def _( diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 70eb519a1bd..c216c297172 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -46,6 +46,16 @@ def output_type( ... +@dataclasses.dataclass(frozen=True) +class NullaryAggregation(Aggregation): + op: agg_ops.NullaryWindowOp = dataclasses.field() + + def output_type( + self, input_types: dict[str, bigframes.dtypes.Dtype] + ) -> dtypes.ExpressionType: + return self.op.output_type() + + @dataclasses.dataclass(frozen=True) class UnaryAggregation(Aggregation): op: agg_ops.UnaryWindowOp = dataclasses.field() diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 41d0750030f..91c5e54d895 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -104,6 +104,20 @@ def __getitem__( dropna=self._dropna, ) + def size(self) -> typing.Union[df.DataFrame, series.Series]: + agg_block, _ = self._block.aggregate_size( + by_column_ids=self._by_col_ids, + dropna=self._dropna, + ) + agg_block = agg_block.with_column_labels(pd.Index(["size"])) + dataframe = df.DataFrame(agg_block) + + if self._as_index: + series = dataframe["size"] + return series.rename(None) + else: + return self._convert_index(dataframe) + def sum(self, numeric_only: bool = False, *args) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("sum") @@ -520,6 +534,13 @@ def std(self, *args, **kwargs) -> series.Series: def var(self, *args, **kwargs) -> series.Series: return self._aggregate(agg_ops.var_op) + def size(self) -> series.Series: + agg_block, _ = self._block.aggregate_size( + by_column_ids=self._by_col_ids, + dropna=self._dropna, + ) + return series.Series(agg_block, name=self._value_name) + def skew(self, *args, **kwargs) -> series.Series: block = block_ops.skew(self._block, [self._value_column], self._by_col_ids) return series.Series(block) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 3b5310554b4..783abfd788b 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -47,6 +47,13 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT ... +@dataclasses.dataclass(frozen=True) +class NullaryWindowOp(WindowOp): + @property + def arguments(self) -> int: + return 0 + + @dataclasses.dataclass(frozen=True) class UnaryWindowOp(WindowOp): @property @@ -72,6 +79,13 @@ def arguments(self) -> int: ... +@dataclasses.dataclass(frozen=True) +class NullaryAggregateOp(AggregateOp, NullaryWindowOp): + @property + def arguments(self) -> int: + return 0 + + @dataclasses.dataclass(frozen=True) class UnaryAggregateOp(AggregateOp, UnaryWindowOp): @property @@ -86,6 +100,14 @@ def arguments(self) -> int: return 2 +@dataclasses.dataclass(frozen=True) +class SizeOp(NullaryAggregateOp): + name: ClassVar[str] = "size" + + def output_type(self, *input_types: dtypes.ExpressionType): + return dtypes.INT_DTYPE + + @dataclasses.dataclass(frozen=True) class SumOp(UnaryAggregateOp): name: ClassVar[str] = "sum" @@ -446,6 +468,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT ) +size_op = SizeOp() sum_op = SumOp() mean_op = MeanOp() median_op = MedianOp() diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 7b36a06f492..02d9bf97253 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -18,6 +18,10 @@ import bigframes.pandas as bpd from tests.system.utils import assert_pandas_df_equal +# ================= +# DataFrame.groupby +# ================= + @pytest.mark.parametrize( ("operator"), @@ -269,21 +273,26 @@ def test_dataframe_groupby_analytic( pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) -def test_series_groupby_skew(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.groupby("bool_col")["int64_too"].skew().to_pandas() - pd_result = scalars_pandas_df_index.groupby("bool_col")["int64_too"].skew() +def test_dataframe_groupby_size_as_index_false( + scalars_df_index, scalars_pandas_df_index +): + bf_result = scalars_df_index.groupby("string_col", as_index=False).size() + bf_result_computed = bf_result.to_pandas() + pd_result = scalars_pandas_df_index.groupby("string_col", as_index=False).size() - pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + pd.testing.assert_frame_equal( + pd_result, bf_result_computed, check_dtype=False, check_index_type=False + ) -def test_series_groupby_kurt(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.groupby("bool_col")["int64_too"].kurt().to_pandas() - # Pandas doesn't have groupby.kurt yet: https://github.com/pandas-dev/pandas/issues/40139 - pd_result = scalars_pandas_df_index.groupby("bool_col")["int64_too"].apply( - pd.Series.kurt - ) +def test_dataframe_groupby_size_as_index_true( + scalars_df_index, scalars_pandas_df_index +): + bf_result = scalars_df_index.groupby("string_col", as_index=True).size() + pd_result = scalars_pandas_df_index.groupby("string_col", as_index=True).size() + bf_result_computed = bf_result.to_pandas() - pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + pd.testing.assert_series_equal(pd_result, bf_result_computed, check_dtype=False) def test_dataframe_groupby_skew(scalars_df_index, scalars_pandas_df_index): @@ -356,6 +365,30 @@ def test_dataframe_groupby_getitem_list( pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) +def test_dataframe_groupby_nonnumeric_with_mean(): + df = pd.DataFrame( + { + "key1": ["a", "a", "a", "b"], + "key2": ["a", "a", "c", "c"], + "key3": [1, 2, 3, 4], + "key4": [1.6, 2, 3, 4], + } + ) + pd_result = df.groupby(["key1", "key2"]).mean() + + with bpd.option_context("bigquery.location", "US"): + bf_result = bpd.DataFrame(df).groupby(["key1", "key2"]).mean().to_pandas() + + pd.testing.assert_frame_equal( + pd_result, bf_result, check_index_type=False, check_dtype=False + ) + + +# ============== +# Series.groupby +# ============== + + def test_series_groupby_agg_string(scalars_df_index, scalars_pandas_df_index): bf_result = ( scalars_df_index["int64_col"] @@ -392,21 +425,49 @@ def test_series_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): ) -def test_dataframe_groupby_nonnumeric_with_mean(): - df = pd.DataFrame( - { - "key1": ["a", "a", "a", "b"], - "key2": ["a", "a", "c", "c"], - "key3": [1, 2, 3, 4], - "key4": [1.6, 2, 3, 4], - } +def test_series_groupby_kurt(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_too"] + .groupby(scalars_df_index["bool_col"]) + .kurt() + .to_pandas() + ) + # Pandas doesn't have groupby.kurt yet: https://github.com/pandas-dev/pandas/issues/40139 + pd_result = scalars_pandas_df_index.groupby("bool_col")["int64_too"].apply( + pd.Series.kurt ) - pd_result = df.groupby(["key1", "key2"]).mean() - bf_result = bpd.DataFrame(df).groupby(["key1", "key2"]).mean().to_pandas() - pd.testing.assert_frame_equal( - pd_result, bf_result, check_index_type=False, check_dtype=False + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + +def test_series_groupby_size(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_too"].groupby(scalars_df_index["bool_col"]).size() ) + pd_result = ( + scalars_pandas_df_index["int64_too"] + .groupby(scalars_pandas_df_index["bool_col"]) + .size() + ) + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_series_equal(pd_result, bf_result_computed, check_dtype=False) + + +def test_series_groupby_skew(scalars_df_index, scalars_pandas_df_index): + bf_result = ( + scalars_df_index["int64_too"] + .groupby(scalars_df_index["bool_col"]) + .skew() + .to_pandas() + ) + pd_result = ( + scalars_pandas_df_index["int64_too"] + .groupby(scalars_pandas_df_index["bool_col"]) + .skew() + ) + + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.parametrize( diff --git a/third_party/bigframes_vendored/ibis/expr/operations/analytic.py b/third_party/bigframes_vendored/ibis/expr/operations/analytic.py index 3d6a3b37b1b..2e1373533e2 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/analytic.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/analytic.py @@ -2,10 +2,19 @@ from __future__ import annotations +import ibis import ibis.expr.operations as ops import ibis.expr.rules as rlz +# TODO(swast): We can remove this if ibis adds aggregates over scalar values. +# See: https://github.com/ibis-project/ibis/issues/8698 +@ibis.udf.agg.builtin +def count(value: int) -> int: + """Count of a scalar.""" + return 0 # pragma: NO COVER + + class FirstNonNullValue(ops.Analytic): """Retrieve the first element.""" @@ -21,6 +30,7 @@ class LastNonNullValue(ops.Analytic): __all__ = [ + "count", "FirstNonNullValue", "LastNonNullValue", ] From 4a12e3c6d49d78fc2b51d783cc8de5d09e7c9995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 24 May 2024 19:05:12 -0500 Subject: [PATCH 597/921] feat: support type annotations to supply input and output types to `@remote_function` decorator (#717) * feat: support type annotations to supply input and output types to `@remote_function` decorator * make tests robust to cloud function listing failures too --- bigframes/functions/remote_function.py | 94 +++++++++++----- tests/system/conftest.py | 102 +++++++++--------- tests/unit/resources.py | 8 +- tests/unit/test_pandas.py | 2 +- tests/unit/test_remote_function.py | 39 +++++++ .../bigframes_vendored/pandas/core/frame.py | 16 +-- .../bigframes_vendored/pandas/core/series.py | 16 ++- 7 files changed, 181 insertions(+), 96 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 0ae90a28d3c..af4dd5982f2 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -24,7 +24,17 @@ import sys import tempfile import textwrap -from typing import cast, List, NamedTuple, Optional, Sequence, TYPE_CHECKING, Union +from typing import ( + Any, + cast, + List, + Mapping, + NamedTuple, + Optional, + Sequence, + TYPE_CHECKING, + Union, +) import warnings import ibis @@ -736,8 +746,8 @@ def get_routine_reference( # which has moved as @js to the ibis package # https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py def remote_function( - input_types: Union[type, Sequence[type]], - output_type: type, + input_types: Union[None, type, Sequence[type]] = None, + output_type: Optional[type] = None, session: Optional[Session] = None, bigquery_client: Optional[bigquery.Client] = None, bigquery_connection_client: Optional[ @@ -801,11 +811,11 @@ def remote_function( `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. Args: - input_types (type or sequence(type)): + input_types (None, type, or sequence(type)): For scalar user defined function it should be the input type or sequence of input types. For row processing user defined function, type `Series` should be specified. - output_type (type): + output_type (Optional[type]): Data type of the output in the user defined function. session (bigframes.Session, Optional): BigQuery DataFrames session to use for getting default project, @@ -908,27 +918,10 @@ def remote_function( service(s) that are on a VPC network. See for more details https://cloud.google.com/functions/docs/networking/connecting-vpc. """ - is_row_processor = False - - import bigframes.series - import bigframes.session - - if input_types == bigframes.series.Series: - warnings.warn( - "input_types=Series scenario is in preview.", - stacklevel=1, - category=bigframes.exceptions.PreviewWarning, - ) - - # we will model the row as a json serialized string containing the data - # and the metadata representing the row - input_types = [str] - is_row_processor = True - elif isinstance(input_types, type): - input_types = [input_types] - # Some defaults may be used from the session if not provided otherwise import bigframes.pandas as bpd + import bigframes.series + import bigframes.session session = cast(bigframes.session.Session, session or bpd.get_global_session()) @@ -1021,10 +1014,61 @@ def remote_function( bq_connection_manager = None if session is None else session.bqconnectionmanager def wrapper(f): + nonlocal input_types, output_type + if not callable(f): raise TypeError("f must be callable, got {}".format(f)) - signature = inspect.signature(f) + if sys.version_info >= (3, 10): + # Add `eval_str = True` so that deferred annotations are turned into their + # corresponding type objects. Need Python 3.10 for eval_str parameter. + # https://docs.python.org/3/library/inspect.html#inspect.signature + signature_kwargs: Mapping[str, Any] = {"eval_str": True} + else: + signature_kwargs = {} + + signature = inspect.signature( + f, + **signature_kwargs, + ) + + # Try to get input types via type annotations. + if input_types is None: + input_types = [] + for parameter in signature.parameters.values(): + if (param_type := parameter.annotation) is inspect.Signature.empty: + raise ValueError( + "'input_types' was not set and parameter " + f"'{parameter.name}' is missing a type annotation. " + "Types are required to use @remote_function." + ) + input_types.append(param_type) + + if output_type is None: + if (output_type := signature.return_annotation) is inspect.Signature.empty: + raise ValueError( + "'output_type' was not set and function is missing a " + "return type annotation. Types are required to use " + "@remote_function." + ) + + # The function will actually be receiving a pandas Series, but allow both + # BigQuery DataFrames and pandas object types for compatibility. + is_row_processor = False + if input_types == bigframes.series.Series or input_types == pandas.Series: + warnings.warn( + "input_types=Series scenario is in preview.", + stacklevel=1, + category=bigframes.exceptions.PreviewWarning, + ) + + # we will model the row as a json serialized string containing the data + # and the metadata representing the row + input_types = [str] + is_row_processor = True + elif isinstance(input_types, type): + input_types = [input_types] + # TODO(b/340898611): fix type error ibis_signature = ibis_signature_from_python_signature( signature, input_types, output_type # type: ignore diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 1de1f54700b..f7fbd5f4b6b 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -18,9 +18,9 @@ import math import pathlib import textwrap +import traceback import typing from typing import Dict, Generator, Optional -import warnings import google.api_core.exceptions import google.cloud.bigquery as bigquery @@ -1097,54 +1097,54 @@ def cleanup_cloud_functions(session, cloudfunctions_client, dataset_id_permanent session.bqclient, dataset_id_permanent ) delete_count = 0 - for cloud_function in tests.system.utils.get_cloud_functions( - cloudfunctions_client, - session.bqclient.project, - session.bqclient.location, - name_prefix="bigframes-", - ): - # Ignore bigframes cloud functions referred by the remote functions in - # the permanent dataset - if cloud_function.service_config.uri in permanent_endpoints: - continue - - # Ignore the functions less than one day old - age = datetime.now() - datetime.fromtimestamp( - cloud_function.update_time.timestamp() - ) - if age.days <= 0: - continue - - # Go ahead and delete - try: - tests.system.utils.delete_cloud_function( - cloudfunctions_client, cloud_function.name + try: + for cloud_function in tests.system.utils.get_cloud_functions( + cloudfunctions_client, + session.bqclient.project, + session.bqclient.location, + name_prefix="bigframes-", + ): + # Ignore bigframes cloud functions referred by the remote functions in + # the permanent dataset + if cloud_function.service_config.uri in permanent_endpoints: + continue + + # Ignore the functions less than one day old + age = datetime.now() - datetime.fromtimestamp( + cloud_function.update_time.timestamp() ) - delete_count += 1 - if delete_count >= MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION: - break - except google.api_core.exceptions.NotFound: - # This can happen when multiple pytest sessions are running in - # parallel. Two or more sessions may discover the same cloud - # function, but only one of them would be able to delete it - # successfully, while the other instance will run into this - # exception. Ignore this exception. - pass - except Exception as exc: - # Don't fail the tests for unknown exceptions. - # - # This can happen if we are hitting GCP limits, e.g. - # google.api_core.exceptions.ResourceExhausted: 429 Quota exceeded - # for quota metric 'Per project mutation requests' and limit - # 'Per project mutation requests per minute per region' of service - # 'cloudfunctions.googleapis.com' for consumer - # 'project_number:1084210331973'. - # [reason: "RATE_LIMIT_EXCEEDED" domain: "googleapis.com" ... - # - # It can also happen occasionally with - # google.api_core.exceptions.ServiceUnavailable when there is some - # backend flakiness. - # - # Let's stop further clean up and leave it to later. - warnings.warn(f"Cloud functions cleanup failed: {str(exc)}") - break + if age.days <= 0: + continue + + # Go ahead and delete + try: + tests.system.utils.delete_cloud_function( + cloudfunctions_client, cloud_function.name + ) + delete_count += 1 + if delete_count >= MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION: + break + except google.api_core.exceptions.NotFound: + # This can happen when multiple pytest sessions are running in + # parallel. Two or more sessions may discover the same cloud + # function, but only one of them would be able to delete it + # successfully, while the other instance will run into this + # exception. Ignore this exception. + pass + except Exception as exc: + # Don't fail the tests for unknown exceptions. + # + # This can happen if we are hitting GCP limits, e.g. + # google.api_core.exceptions.ResourceExhausted: 429 Quota exceeded + # for quota metric 'Per project mutation requests' and limit + # 'Per project mutation requests per minute per region' of service + # 'cloudfunctions.googleapis.com' for consumer + # 'project_number:1084210331973'. + # [reason: "RATE_LIMIT_EXCEEDED" domain: "googleapis.com" ... + # + # It can also happen occasionally with + # google.api_core.exceptions.ServiceUnavailable when there is some + # backend flakiness. + # + # Let's stop further clean up and leave it to later. + traceback.print_exception(exc) diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 623af93f65a..4d7998903c7 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -39,6 +39,7 @@ def create_bigquery_session( session_id: str = "abcxyz", table_schema: Sequence[google.cloud.bigquery.SchemaField] = TEST_SCHEMA, anonymous_dataset: Optional[google.cloud.bigquery.DatasetReference] = None, + location: str = "test-region", ) -> bigframes.Session: credentials = mock.create_autospec( google.auth.credentials.Credentials, instance=True @@ -53,11 +54,12 @@ def create_bigquery_session( if bqclient is None: bqclient = mock.create_autospec(google.cloud.bigquery.Client, instance=True) bqclient.project = "test-project" + bqclient.location = location # Mock the location. table = mock.create_autospec(google.cloud.bigquery.Table, instance=True) table._properties = {} - type(table).location = mock.PropertyMock(return_value="test-region") + type(table).location = mock.PropertyMock(return_value=location) type(table).schema = mock.PropertyMock(return_value=table_schema) type(table).reference = mock.PropertyMock( return_value=anonymous_dataset.table("test_table") @@ -93,9 +95,7 @@ def query_mock(query, *args, **kwargs): type(clients_provider).bqclient = mock.PropertyMock(return_value=bqclient) clients_provider._credentials = credentials - bqoptions = bigframes.BigQueryOptions( - credentials=credentials, location="test-region" - ) + bqoptions = bigframes.BigQueryOptions(credentials=credentials, location=location) session = bigframes.Session(context=bqoptions, clients_provider=clients_provider) return session diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 70639315bed..54a7a79d3c3 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -50,7 +50,7 @@ def all_session_methods(): [(method_name,) for method_name in all_session_methods()], ) def test_method_matches_session(method_name: str): - if sys.version_info <= (3, 10): + if sys.version_info < (3, 10): pytest.skip( "Need Python 3.10 to reconcile deferred annotations." ) # pragma: no cover diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py index 1acff27c7f0..ae9ab296c5f 100644 --- a/tests/unit/test_remote_function.py +++ b/tests/unit/test_remote_function.py @@ -14,8 +14,11 @@ import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes from ibis.expr import datatypes as ibis_types +import pytest import bigframes.dtypes +import bigframes.functions.remote_function +from tests.unit import resources def test_supported_types_correspond(): @@ -29,3 +32,39 @@ def test_supported_types_correspond(): } assert ibis_types_from_python == ibis_types_from_bigquery + + +def test_missing_input_types(): + session = resources.create_bigquery_session() + remote_function_decorator = bigframes.functions.remote_function.remote_function( + session=session + ) + + def function_without_parameter_annotations(myparam) -> str: + return str(myparam) + + assert function_without_parameter_annotations(42) == "42" + + with pytest.raises( + ValueError, + match="'input_types' was not set .* 'myparam' is missing a type annotation", + ): + remote_function_decorator(function_without_parameter_annotations) + + +def test_missing_output_type(): + session = resources.create_bigquery_session() + remote_function_decorator = bigframes.functions.remote_function.remote_function( + session=session + ) + + def function_without_return_annotation(myparam: int): + return str(myparam) + + assert function_without_return_annotation(42) == "42" + + with pytest.raises( + ValueError, + match="'output_type' was not set .* missing a return type annotation", + ): + remote_function_decorator(function_without_return_annotation) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 01c7ab70ee3..7d2e3b2cecf 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3916,8 +3916,8 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: to potentially reuse a previously deployed ``remote_function`` from the same user defined function. - >>> @bpd.remote_function(int, float, reuse=False) - ... def minutes_to_hours(x): + >>> @bpd.remote_function(reuse=False) + ... def minutes_to_hours(x: int) -> float: ... return x/60 >>> df_minutes = bpd.DataFrame( @@ -4238,6 +4238,7 @@ def apply(self, func, *, axis=0, args=(), **kwargs): **Examples:** >>> import bigframes.pandas as bpd + >>> import pandas as pd >>> bpd.options.display.progress_bar = None >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) @@ -4259,16 +4260,19 @@ def apply(self, func, *, axis=0, args=(), **kwargs): [2 rows x 2 columns] You could apply a user defined function to every row of the DataFrame by - creating a remote function out of it, and using it with `axis=1`. + creating a remote function out of it, and using it with `axis=1`. Within + the function, each row is passed as a ``pandas.Series``. It is recommended + to select only the necessary columns before calling `apply()`. Note: This + feature is currently in **preview**. - >>> @bpd.remote_function(bpd.Series, int, reuse=False) - ... def foo(row): + >>> @bpd.remote_function(reuse=False) + ... def foo(row: pd.Series) -> int: ... result = 1 ... result += row["col1"] ... result += row["col2"]*row["col2"] ... return result - >>> df.apply(foo, axis=1) + >>> df[["col1", "col2"]].apply(foo, axis=1) 0 11 1 19 dtype: Int64 diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 585e20275c7..56f1c8b3e05 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -1183,8 +1183,8 @@ def apply( to potentially reuse a previously deployed `remote_function` from the same user defined function. - >>> @bpd.remote_function(int, float, reuse=False) - ... def minutes_to_hours(x): + >>> @bpd.remote_function(reuse=False) + ... def minutes_to_hours(x: int) -> float: ... return x/60 >>> minutes = bpd.Series([0, 30, 60, 90, 120]) @@ -1210,12 +1210,10 @@ def apply( `packages` param. >>> @bpd.remote_function( - ... str, - ... str, ... reuse=False, ... packages=["cryptography"], ... ) - ... def get_hash(input): + ... def get_hash(input: str) -> str: ... from cryptography.fernet import Fernet ... ... # handle missing value @@ -3452,8 +3450,8 @@ def mask(self, cond, other): condition is evaluated based on a complicated business logic which cannot be expressed in form of a Series. - >>> @bpd.remote_function(str, bool, reuse=False) - ... def should_mask(name): + >>> @bpd.remote_function(reuse=False) + ... def should_mask(name: str) -> bool: ... hash = 0 ... for char_ in name: ... hash += ord(char_) @@ -3971,8 +3969,8 @@ def map( It also accepts a remote function: - >>> @bpd.remote_function(str, str) - ... def my_mapper(val): + >>> @bpd.remote_function + ... def my_mapper(val: str) -> str: ... vowels = ["a", "e", "i", "o", "u"] ... if val: ... return "".join([ From d850da6364b98c4e01120725e1e609ad8f6c1263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Sun, 26 May 2024 15:10:36 -0500 Subject: [PATCH 598/921] feat: allow functions decorated with `@bpd.remote_function` to execute locally (#704) * feat: allow functions decorated with `@bpd.remote_function` to execute locally * fix read_gbq_function * fix for rare case where re-deploy exact same function object --- bigframes/core/compile/scalar_op_compiler.py | 10 +-- bigframes/functions/remote_function.py | 52 ++++++++++----- tests/system/large/test_remote_function.py | 5 +- tests/system/small/test_remote_function.py | 70 ++++++++++++++------ 4 files changed, 94 insertions(+), 43 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index e8e5a1f3acf..a79a4eceabc 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -856,11 +856,12 @@ def to_timestamp_op_impl(x: ibis_types.Value, op: ops.ToTimestampOp): @scalar_op_compiler.register_unary_op(ops.RemoteFunctionOp, pass_op=True) def remote_function_op_impl(x: ibis_types.Value, op: ops.RemoteFunctionOp): - if not hasattr(op.func, "bigframes_remote_function"): + ibis_node = getattr(op.func, "ibis_node", None) + if ibis_node is None: raise TypeError( f"only a bigframes remote function is supported as a callable. {constants.FEEDBACK_LINK}" ) - x_transformed = op.func(x) + x_transformed = ibis_node(x) if not op.apply_on_null: x_transformed = ibis.case().when(x.isnull(), x).else_(x_transformed).end() return x_transformed @@ -1342,11 +1343,12 @@ def minimum_impl( def binary_remote_function_op_impl( x: ibis_types.Value, y: ibis_types.Value, op: ops.BinaryRemoteFunctionOp ): - if not hasattr(op.func, "bigframes_remote_function"): + ibis_node = getattr(op.func, "ibis_node", None) + if ibis_node is None: raise TypeError( f"only a bigframes remote function is supported as a callable. {constants.FEEDBACK_LINK}" ) - x_transformed = op.func(x, y) + x_transformed = ibis_node(x, y) return x_transformed diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index af4dd5982f2..fb4e3f2f365 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -1013,11 +1013,11 @@ def remote_function( bq_connection_manager = None if session is None else session.bqconnectionmanager - def wrapper(f): + def wrapper(func): nonlocal input_types, output_type - if not callable(f): - raise TypeError("f must be callable, got {}".format(f)) + if not callable(func): + raise TypeError("f must be callable, got {}".format(func)) if sys.version_info >= (3, 10): # Add `eval_str = True` so that deferred annotations are turned into their @@ -1028,7 +1028,7 @@ def wrapper(f): signature_kwargs = {} signature = inspect.signature( - f, + func, **signature_kwargs, ) @@ -1089,8 +1089,23 @@ def wrapper(f): session=session, # type: ignore ) + # In the unlikely case where the user is trying to re-deploy the same + # function, cleanup the attributes we add below, first. This prevents + # the pickle from having dependencies that might not otherwise be + # present such as ibis or pandas. + def try_delattr(attr): + try: + delattr(func, attr) + except AttributeError: + pass + + try_delattr("bigframes_cloud_function") + try_delattr("bigframes_remote_function") + try_delattr("output_dtype") + try_delattr("ibis_node") + rf_name, cf_name = remote_function_client.provision_bq_remote_function( - f, + func, ibis_signature.input_types, ibis_signature.output_type, reuse, @@ -1105,19 +1120,20 @@ def wrapper(f): # TODO: Move ibis logic to compiler step node = ibis.udf.scalar.builtin( - f, + func, name=rf_name, schema=f"{dataset_ref.project}.{dataset_ref.dataset_id}", signature=(ibis_signature.input_types, ibis_signature.output_type), ) - node.bigframes_cloud_function = ( + func.bigframes_cloud_function = ( remote_function_client.get_cloud_function_fully_qualified_name(cf_name) ) - node.bigframes_remote_function = str(dataset_ref.routine(rf_name)) # type: ignore - node.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( + func.bigframes_remote_function = str(dataset_ref.routine(rf_name)) # type: ignore + func.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( ibis_signature.output_type ) - return node + func.ibis_node = node + return func return wrapper @@ -1168,19 +1184,23 @@ def read_gbq_function( # The name "args" conflicts with the Ibis operator, so we use # non-standard names for the arguments here. - def node(*ignored_args, **ignored_kwargs): + def func(*ignored_args, **ignored_kwargs): f"""Remote function {str(routine_ref)}.""" + # TODO(swast): Construct an ibis client from bigquery_client and + # execute node via a query. # TODO: Move ibis logic to compiler step - node.__name__ = routine_ref.routine_id + func.__name__ = routine_ref.routine_id + node = ibis.udf.scalar.builtin( - node, + func, name=routine_ref.routine_id, schema=f"{routine_ref.project}.{routine_ref.dataset_id}", signature=(ibis_signature.input_types, ibis_signature.output_type), ) - node.bigframes_remote_function = str(routine_ref) # type: ignore - node.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( # type: ignore + func.bigframes_remote_function = str(routine_ref) # type: ignore + func.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( # type: ignore ibis_signature.output_type ) - return node + func.ibis_node = node # type: ignore + return func diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index cac8483b5ba..4114eaae03b 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -194,6 +194,9 @@ def test_remote_function_stringify_with_ibis( def stringify(x): return f"I got {x}" + # Function should work locally. + assert stringify(42) == "I got 42" + _, dataset_name, table_name = scalars_table_id.split(".") if not ibis_client.dataset: ibis_client.dataset = dataset_name @@ -205,7 +208,7 @@ def stringify(x): pandas_df_orig = bigquery_client.query(sql).to_dataframe() col = table[col_name] - col_2x = stringify(col).name("int64_str_col") + col_2x = stringify.ibis_node(col).name("int64_str_col") table = table.mutate([col_2x]) sql = table.compile() pandas_df_new = bigquery_client.query(sql).to_dataframe() diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 4a39e75ff95..096a268441c 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -67,7 +67,7 @@ def bq_cf_connection_location_project(bigquery_client) -> str: @pytest.fixture(scope="module") def bq_cf_connection_location_project_mismatched() -> str: - """Pre-created BQ connection in the migframes-metrics project in US location, + """Pre-created BQ connection in the bigframes-metrics project in US location, in format PROJECT_ID.LOCATION.CONNECTION_NAME, used to invoke cloud function. $ bq show --connection --location=us --project_id=PROJECT_ID bigframes-rf-conn @@ -108,11 +108,15 @@ def test_remote_function_direct_no_session_param( reuse=True, ) def square(x): - # This executes on a remote function, where coverage isn't tracked. - return x * x # pragma: NO COVER + return x * x - assert square.bigframes_remote_function - assert square.bigframes_cloud_function + # Function should still work normally. + assert square(2) == 4 + + # Function should have extra metadata attached for remote execution. + assert hasattr(square, "bigframes_remote_function") + assert hasattr(square, "bigframes_cloud_function") + assert hasattr(square, "ibis_node") scalars_df, scalars_pandas_df = scalars_dfs @@ -161,8 +165,10 @@ def test_remote_function_direct_no_session_param_location_specified( reuse=True, ) def square(x): - # This executes on a remote function, where coverage isn't tracked. - return x * x # pragma: NO COVER + return x * x + + # Function should still work normally. + assert square(2) == 4 scalars_df, scalars_pandas_df = scalars_dfs @@ -197,7 +203,10 @@ def test_remote_function_direct_no_session_param_location_mismatched( dataset_id_permanent, bq_cf_connection_location_mismatched, ): - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match=re.escape("The location does not match BigQuery connection location:"), + ): @rf.remote_function( [int], @@ -212,7 +221,8 @@ def test_remote_function_direct_no_session_param_location_mismatched( reuse=True, ) def square(x): - # This executes on a remote function, where coverage isn't tracked. + # Not expected to reach this code, as the location of the + # connection doesn't match the location of the dataset. return x * x # pragma: NO COVER @@ -239,8 +249,10 @@ def test_remote_function_direct_no_session_param_location_project_specified( reuse=True, ) def square(x): - # This executes on a remote function, where coverage isn't tracked. - return x * x # pragma: NO COVER + return x * x + + # Function should still work normally. + assert square(2) == 4 scalars_df, scalars_pandas_df = scalars_dfs @@ -275,7 +287,12 @@ def test_remote_function_direct_no_session_param_project_mismatched( dataset_id_permanent, bq_cf_connection_location_project_mismatched, ): - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match=re.escape( + "The project_id does not match BigQuery connection gcp_project_id:" + ), + ): @rf.remote_function( [int], @@ -290,7 +307,8 @@ def test_remote_function_direct_no_session_param_project_mismatched( reuse=True, ) def square(x): - # This executes on a remote function, where coverage isn't tracked. + # Not expected to reach this code, as the project of the + # connection doesn't match the project of the dataset. return x * x # pragma: NO COVER @@ -302,8 +320,10 @@ def test_remote_function_direct_session_param(session_with_bq_connection, scalar session=session_with_bq_connection, ) def square(x): - # This executes on a remote function, where coverage isn't tracked. - return x * x # pragma: NO COVER + return x * x + + # Function should still work normally. + assert square(2) == 4 scalars_df, scalars_pandas_df = scalars_dfs @@ -340,8 +360,10 @@ def test_remote_function_via_session_default(session_with_bq_connection, scalars # cloud function would be common and quickly reused. @session_with_bq_connection.remote_function([int], int) def square(x): - # This executes on a remote function, where coverage isn't tracked. - return x * x # pragma: NO COVER + return x * x + + # Function should still work normally. + assert square(2) == 4 scalars_df, scalars_pandas_df = scalars_dfs @@ -380,8 +402,10 @@ def test_remote_function_via_session_with_overrides( reuse=True, ) def square(x): - # This executes on a remote function, where coverage isn't tracked. - return x * x # pragma: NO COVER + return x * x + + # Function should still work normally. + assert square(2) == 4 scalars_df, scalars_pandas_df = scalars_dfs @@ -508,7 +532,7 @@ def test_skip_bq_connection_check(dataset_id_permanent): @session.remote_function([int], int, dataset=dataset_id_permanent) def add_one(x): - # This executes on a remote function, where coverage isn't tracked. + # Not expected to reach this code, as the connection doesn't exist. return x + 1 # pragma: NO COVER @@ -546,8 +570,10 @@ def test_read_gbq_function_like_original( reuse=True, ) def square1(x): - # This executes on a remote function, where coverage isn't tracked. - return x * x # pragma: NO COVER + return x * x + + # Function should still work normally. + assert square1(2) == 4 square2 = rf.read_gbq_function( function_name=square1.bigframes_remote_function, From 72e95834f8755760f3529d38f340703f3b971f0a Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 28 May 2024 16:03:29 -0700 Subject: [PATCH 599/921] feat: add slot_millis and add stats to session object (#725) * feat: add slot_millis and add stats to session object * fix none handling * fix indent * make incrementor internal * update noxfile * update comment * add comment --- bigframes/session/__init__.py | 26 ++++++++- bigframes/session/_io/bigquery/__init__.py | 51 ++++++++++++++---- noxfile.py | 62 +++++++++++++++------- 3 files changed, 107 insertions(+), 32 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 83481a3ae9f..07bb6ddce0f 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -289,6 +289,10 @@ def __init__( nodes.BigFrameNode, nodes.BigFrameNode ] = weakref.WeakKeyDictionary() + # performance logging + self._bytes_processed_sum = 0 + self._slot_millis_sum = 0 + @property def bqclient(self): return self._clients_provider.bqclient @@ -338,6 +342,24 @@ def objects( def _project(self): return self.bqclient.project + @property + def bytes_processed_sum(self): + """The sum of all bytes processed by bigquery jobs using this session.""" + return self._bytes_processed_sum + + @property + def slot_millis_sum(self): + """The sum of all slot time used by bigquery jobs in this session.""" + return self._slot_millis_sum + + def _add_bytes_processed(self, amount: int): + """Increment bytes_processed_sum by amount.""" + self._bytes_processed_sum += amount + + def _add_slot_millis(self, amount: int): + """Increment slot_millis_sum by amount.""" + self._slot_millis_sum += amount + def __hash__(self): # Stable hash needed to use in expression tree return hash(str(self._anonymous_dataset)) @@ -1825,7 +1847,7 @@ def _start_query( """ job_config = self._prepare_query_job_config(job_config) return bigframes.session._io.bigquery.start_query_with_client( - self.bqclient, + self, sql, job_config, max_results, @@ -1849,7 +1871,7 @@ def _start_query_ml_ddl( job_config.destination_encryption_configuration = None return bigframes.session._io.bigquery.start_query_with_client( - self.bqclient, sql, job_config + self, sql, job_config ) def _cache_with_cluster_cols( diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index cd6847c3126..6afa86aa2d2 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -219,7 +219,7 @@ def add_labels(job_config, api_name: Optional[str] = None): def start_query_with_client( - bq_client: bigquery.Client, + session: bigframes.session.Session, sql: str, job_config: bigquery.job.QueryJobConfig, max_results: Optional[int] = None, @@ -229,6 +229,7 @@ def start_query_with_client( """ Starts query job and waits for results. """ + bq_client: bigquery.Client = session.bqclient add_labels(job_config, api_name=api_name) try: @@ -246,14 +247,41 @@ def start_query_with_client( else: results_iterator = query_job.result(max_results=max_results) - if LOGGING_NAME_ENV_VAR in os.environ: - # when running notebooks via pytest nbmake - pytest_log_job(query_job) + stats = get_performance_stats(query_job) + if stats is not None: + bytes_processed, slot_millis = stats + session._add_bytes_processed(bytes_processed) + session._add_slot_millis(slot_millis) + if LOGGING_NAME_ENV_VAR in os.environ: + # when running notebooks via pytest nbmake + write_stats_to_disk(bytes_processed, slot_millis) return results_iterator, query_job -def pytest_log_job(query_job: bigquery.QueryJob): +def get_performance_stats(query_job: bigquery.QueryJob) -> Optional[Tuple[int, int]]: + """Parse the query job for performance stats. + + Return None if the stats do not reflect real work done in bigquery. + """ + bytes_processed = query_job.total_bytes_processed + if not isinstance(bytes_processed, int): + return None # filter out mocks + if query_job.configuration.dry_run: + # dry run stats are just predictions of the real run + bytes_processed = 0 + + slot_millis = query_job.slot_millis + if not isinstance(slot_millis, int): + return None # filter out mocks + if query_job.configuration.dry_run: + # dry run stats are just predictions of the real run + slot_millis = 0 + + return bytes_processed, slot_millis + + +def write_stats_to_disk(bytes_processed: int, slot_millis: int): """For pytest runs only, log information about the query job to a file in order to create a performance report. """ @@ -265,16 +293,17 @@ def pytest_log_job(query_job: bigquery.QueryJob): ) test_name = os.environ[LOGGING_NAME_ENV_VAR] current_directory = os.getcwd() - bytes_processed = query_job.total_bytes_processed - if not isinstance(bytes_processed, int): - return # filter out mocks - if query_job.configuration.dry_run: - # dry runs don't process their total_bytes_processed - bytes_processed = 0 + + # store bytes processed bytes_file = os.path.join(current_directory, test_name + ".bytesprocessed") with open(bytes_file, "a") as f: f.write(str(bytes_processed) + "\n") + # store slot milliseconds + bytes_file = os.path.join(current_directory, test_name + ".slotmillis") + with open(bytes_file, "a") as f: + f.write(str(slot_millis) + "\n") + def delete_tables_matching_session_id( client: bigquery.Client, dataset: bigquery.DatasetReference, session_id: str diff --git a/noxfile.py b/noxfile.py index c6e8da8c816..52583bbf1a7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -800,33 +800,57 @@ def notebook(session: nox.Session): for process in processes: process.join() - # when run via pytest, notebooks output a .bytesprocessed report + # when the environment variable is set as it is above, + # notebooks output a .bytesprocessed and .slotmillis report # collect those reports and print a summary - _print_bytes_processed_report() + _print_performance_report() -def _print_bytes_processed_report(): - """Add an informational report about http queries and bytes - processed to the testlog output for purposes of measuring - bigquery-related performance changes. +def _print_performance_report(): + """Add an informational report about http queries, bytes + processed, and slot time to the testlog output for purposes + of measuring bigquery-related performance changes. """ print("---BIGQUERY USAGE REPORT---") - cumulative_queries = 0 - cumulative_bytes = 0 - for report in Path("notebooks/").glob("*/*.bytesprocessed"): - with open(report, "r") as f: - filename = report.stem - lines = f.read().splitlines() + results_dict = {} + for bytes_report in Path("notebooks/").glob("*/*.bytesprocessed"): + with open(bytes_report, "r") as bytes_file: + filename = bytes_report.stem + lines = bytes_file.read().splitlines() query_count = len(lines) total_bytes = sum([int(line) for line in lines]) - format_string = f"{filename} - query count: {query_count}, bytes processed sum: {total_bytes}" - print(format_string) - cumulative_bytes += total_bytes - cumulative_queries += query_count - print( - "---total queries: {total_queries}, total bytes: {total_bytes}---".format( - total_queries=cumulative_queries, total_bytes=cumulative_bytes + results_dict[filename] = [query_count, total_bytes] + for millis_report in Path("notebooks/").glob("*/*.slotmillis"): + with open(millis_report, "r") as millis_file: + filename = millis_report.stem + lines = millis_file.read().splitlines() + total_slot_millis = sum([int(line) for line in lines]) + results_dict[filename] += [total_slot_millis] + + cumulative_queries = 0 + cumulative_bytes = 0 + cumulative_slot_millis = 0 + for results in results_dict.values(): + if len(results) != 3: + raise IOError( + "Mismatch in performance logging output. " + "Expected one .bytesprocessed and one .slotmillis " + "file for each notebook." + ) + query_count, total_bytes, total_slot_millis = results + cumulative_queries += query_count + cumulative_bytes += total_bytes + cumulative_slot_millis += total_slot_millis + print( + f"{filename} - query count: {query_count}," + f" bytes processed sum: {total_bytes}," + f" slot millis sum: {total_slot_millis}" ) + + print( + f"---total queries: {cumulative_queries}, " + f"total bytes: {cumulative_bytes}, " + f"total slot millis: {cumulative_slot_millis}---" ) From 804e9e3ef2d91a67d42fde63c37953d980e72f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 29 May 2024 10:46:54 -0500 Subject: [PATCH 600/921] tests: fix failing e2e tests (#726) * tests: fix failing e2e tests * remove unnecessary test We already check the VPC is added to the Cloud Function metadata here: https://github.com/googleapis/python-bigquery-dataframes/blob/72e95834f8755760f3529d38f340703f3b971f0a/tests/system/large/test_remote_function.py#L1426 --- tests/system/large/ml/test_ensemble.py | 2 +- tests/system/large/test_remote_function.py | 23 +++++----------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/tests/system/large/ml/test_ensemble.py b/tests/system/large/ml/test_ensemble.py index e00f7fa665e..706cbfdfafd 100644 --- a/tests/system/large/ml/test_ensemble.py +++ b/tests/system/large/ml/test_ensemble.py @@ -395,7 +395,7 @@ def test_randomforestclassifier_multiple_params(penguins_df_default_index, datas f"{dataset_id}.temp_configured_randomforestclassifier_model" in reloaded_model._bqml_model.model_name ) - assert reloaded_model.tree_method == "auto" + assert reloaded_model.tree_method.casefold() == "auto" assert reloaded_model.colsample_bytree == 0.95 assert reloaded_model.colsample_bylevel == 0.95 assert reloaded_model.colsample_bynode == 0.95 diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 4114eaae03b..cce49ea1871 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -21,7 +21,7 @@ import tempfile import textwrap -from google.api_core.exceptions import BadRequest, InvalidArgument, NotFound +import google.api_core.exceptions from google.cloud import bigquery, storage import pandas import pytest @@ -849,7 +849,7 @@ def square(x): expected_remote_function = f"{dataset_id}.{rf_name}" # Initially the expected BQ remote function should not exist - with pytest.raises(NotFound): + with pytest.raises(google.api_core.exceptions.NotFound): session.bqclient.get_routine(expected_remote_function) # Create the remote function with the name provided explicitly @@ -981,7 +981,7 @@ def test_internal(rf, udf): expected_remote_function = f"{dataset_id}.{rf_name}" # Initially the expected BQ remote function should not exist - with pytest.raises(NotFound): + with pytest.raises(google.api_core.exceptions.NotFound): session.bqclient.get_routine(expected_remote_function) # Create a new remote function with the name provided explicitly @@ -1198,7 +1198,8 @@ def square(x): scalars_df, _ = scalars_dfs with pytest.raises( - BadRequest, match="400.*errorMessage.*unsupported operand type" + google.api_core.exceptions.BadRequest, + match="400.*errorMessage.*unsupported operand type", ): # int64_col has nulls which should cause error in square scalars_df["int64_col"].apply(square).to_pandas() @@ -1431,20 +1432,6 @@ def square_num(x): ) -def test_remote_function_via_session_vpc_invalid(session): - with pytest.raises( - InvalidArgument, match="400.*Serverless VPC Access connector is not found" - ): - - @session.remote_function( - [int], int, reuse=False, cloud_function_vpc_connector="does-not-exist" - ) - def square_num(x): - if x is None: - return x - return x * x - - @pytest.mark.parametrize( ("max_batching_rows"), [ From 0e25a3b3ae704bf75b752c57f613e778af58bac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 29 May 2024 11:52:34 -0500 Subject: [PATCH 601/921] fix: set `@bpd.remote_function`s `input_types` and `output_types` default to `None` to allow omitting them when type annotations are present (#729) --- bigframes/constants.py | 14 ++++----- bigframes/pandas/__init__.py | 4 +-- bigframes/session/__init__.py | 4 +-- third_party/bigframes_vendored/constants.py | 29 +++++++++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 3 +- .../bigframes_vendored/pandas/core/generic.py | 3 +- .../pandas/core/indexing.py | 2 +- 7 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 third_party/bigframes_vendored/constants.py diff --git a/bigframes/constants.py b/bigframes/constants.py index 4778eb9c9e1..9591297956c 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -12,19 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime - """Constants used across BigQuery DataFrames. This module should not depend on any others in the package. """ -FEEDBACK_LINK = ( - "Share your usecase with the BigQuery DataFrames team at the " - "https://bit.ly/bigframes-feedback survey." -) +import datetime + +import bigframes_vendored.constants -ABSTRACT_METHOD_ERROR_MESSAGE = f"Abstract method. You have likely encountered a bug. Please share this stacktrace and how you reached it with the BigQuery DataFrames team. {FEEDBACK_LINK}" +FEEDBACK_LINK = bigframes_vendored.constants.FEEDBACK_LINK +ABSTRACT_METHOD_ERROR_MESSAGE = ( + bigframes_vendored.constants.ABSTRACT_METHOD_ERROR_MESSAGE +) DEFAULT_EXPIRATION = datetime.timedelta(days=7) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index dc9b847a9b1..603f6678a56 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -652,8 +652,8 @@ def read_parquet( def remote_function( - input_types: Union[type, Sequence[type]], - output_type: type, + input_types: Union[None, type, Sequence[type]] = None, + output_type: Optional[type] = None, dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, reuse: bool = True, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 07bb6ddce0f..c953087b3e0 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1549,8 +1549,8 @@ def _ibis_to_temp_table( def remote_function( self, - input_types: Union[type, Sequence[type]], - output_type: type, + input_types: Union[None, type, Sequence[type]] = None, + output_type: Optional[type] = None, dataset: Optional[str] = None, bigquery_connection: Optional[str] = None, reuse: bool = True, diff --git a/third_party/bigframes_vendored/constants.py b/third_party/bigframes_vendored/constants.py new file mode 100644 index 00000000000..0d4a7d1df69 --- /dev/null +++ b/third_party/bigframes_vendored/constants.py @@ -0,0 +1,29 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Constants used across BigQuery DataFrames and bigframes_vendored. + +This module should not depend on any others in the package. +""" + +FEEDBACK_LINK = ( + "Share your usecase with the BigQuery DataFrames team at the " + "https://bit.ly/bigframes-feedback survey." +) + +ABSTRACT_METHOD_ERROR_MESSAGE = ( + "Abstract method. You have likely encountered a bug. " + "Please share this stacktrace and how you reached it with the BigQuery DataFrames team. " + f"{FEEDBACK_LINK}" +) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 7d2e3b2cecf..ba222fc8c33 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -13,12 +13,11 @@ from typing import Hashable, Iterable, Literal, Mapping, Optional, Sequence, Union +from bigframes_vendored import constants import bigframes_vendored.pandas.core.generic as generic import numpy as np import pandas as pd -from bigframes import constants - # ----------------------------------------------------------------------- # DataFrame class diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 54c876ef3c8..95302e51b2d 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -3,11 +3,10 @@ from typing import Callable, Iterator, Literal, Optional, TYPE_CHECKING +import bigframes_vendored.constants as constants from bigframes_vendored.pandas.core import indexing import bigframes_vendored.pandas.core.common as common -import bigframes.constants as constants - if TYPE_CHECKING: from bigframes_vendored.pandas.pandas._typing import T diff --git a/third_party/bigframes_vendored/pandas/core/indexing.py b/third_party/bigframes_vendored/pandas/core/indexing.py index 3c7f8a6c9fb..a188c7197e2 100644 --- a/third_party/bigframes_vendored/pandas/core/indexing.py +++ b/third_party/bigframes_vendored/pandas/core/indexing.py @@ -1,6 +1,6 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexing.py -import bigframes.constants as constants +import bigframes_vendored.constants as constants class IndexingMixin: From 6a78c89a3a766b747b03c8a739760db1c79f533f Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 29 May 2024 12:18:17 -0700 Subject: [PATCH 602/921] fix: correct index labels in multiple aggregations for DataFrameGroupBy (#723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [X] Make sure to open an issue as a bug - [X] Ensure the tests and linter pass - [X] Code coverage does not decrease (if any source code was changed) - [X] Appropriate docs were updated (if necessary) Fixes internal issue 341157901 🦕 --- bigframes/core/groupby/__init__.py | 27 ++++++++++++++++--- tests/system/small/test_groupby.py | 17 ++++++++++++ .../pandas/core/groupby/__init__.py | 22 +++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 91c5e54d895..6e3a91cc1c6 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -339,9 +339,30 @@ def _agg_list(self, func: typing.Sequence) -> df.DataFrame: for col_id in self._aggregated_columns() for f in func ] - column_labels = [ - (col_id, f) for col_id in self._aggregated_columns() for f in func - ] + + if self._block.column_labels.nlevels > 1: + # Restructure MultiIndex for proper format: (idx1, idx2, func) + # rather than ((idx1, idx2), func). + aggregated_columns = pd.MultiIndex.from_tuples( + [ + self._block.col_id_to_label[col_id] + for col_id in self._aggregated_columns() + ], + names=[*self._block.column_labels.names], + ).to_frame(index=False) + + column_labels = [ + tuple(col_id) + (f,) + for col_id in aggregated_columns.to_numpy() + for f in func + ] + else: + column_labels = [ + (self._block.col_id_to_label[col_id], f) + for col_id in self._aggregated_columns() + for f in func + ] + agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, aggregations=aggregations, diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 02d9bf97253..b332d48574c 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -144,6 +144,23 @@ def test_dataframe_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +def test_dataframe_groupby_agg_list_w_column_multi_index( + scalars_df_index, scalars_pandas_df_index +): + columns = ["int64_too", "string_col", "bool_col"] + multi_columns = pd.MultiIndex.from_tuples(zip(["a", "b", "a"], columns)) + bf_df = scalars_df_index[columns].copy() + bf_df.columns = multi_columns + pd_df = scalars_pandas_df_index[columns].copy() + pd_df.columns = multi_columns + + bf_result = bf_df.groupby(level=0).agg(["count", "min"]) + pd_result = pd_df.groupby(level=0).agg(["count", "min"]) + + bf_result_computed = bf_result.to_pandas() + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + @pytest.mark.parametrize( ("as_index"), [ diff --git a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py index f9bedc2a7bb..6011dbfe5b3 100644 --- a/third_party/bigframes_vendored/pandas/core/groupby/__init__.py +++ b/third_party/bigframes_vendored/pandas/core/groupby/__init__.py @@ -1092,6 +1092,17 @@ def agg(self, func, **kwargs): [2 rows x 2 columns] + Multiple aggregations + + >>> df.groupby('A').agg(['min', 'max']) + B C + min max min max + A + 1 1 2 0.227877 0.362838 + 2 3 4 -0.56286 1.267767 + + [2 rows x 4 columns] + Args: func (function, str, list, dict or None): Function to use for aggregating the data. @@ -1140,6 +1151,17 @@ def aggregate(self, func, **kwargs): [2 rows x 2 columns] + Multiple aggregations + + >>> df.groupby('A').agg(['min', 'max']) + B C + min max min max + A + 1 1 2 0.227877 0.362838 + 2 3 4 -0.56286 1.267767 + + [2 rows x 4 columns] + Args: func (function, str, list, dict or None): Function to use for aggregating the data. From 968d8257edbfcb6d437c6203c7c0078ba782cfed Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 29 May 2024 16:15:43 -0700 Subject: [PATCH 603/921] feat: Add `+`, `-` as unary ops, `^` binary op (#724) --- bigframes/core/compile/scalar_op_compiler.py | 20 +++++++++ bigframes/dataframe.py | 27 ++++++++++++ bigframes/operations/__init__.py | 5 ++- bigframes/series.py | 13 ++++++ tests/system/small/test_dataframe.py | 16 +++++++ tests/system/small/test_series.py | 34 +++++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 36 ++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 42 ++++++++++++++++++- 8 files changed, 190 insertions(+), 3 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index a79a4eceabc..e1b497d0dd8 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -375,6 +375,16 @@ def abs_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.NumericValue, x).abs() +@scalar_op_compiler.register_unary_op(ops.pos_op) +def pos_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x) + + +@scalar_op_compiler.register_unary_op(ops.neg_op) +def neg_op_impl(x: ibis_types.Value): + return typing.cast(ibis_types.NumericValue, x).negate() + + @scalar_op_compiler.register_unary_op(ops.sqrt_op) def sqrt_op_impl(x: ibis_types.Value): numeric_value = typing.cast(ibis_types.NumericValue, x) @@ -979,6 +989,16 @@ def or_op( ) +@scalar_op_compiler.register_binary_op(ops.xor_op) +def xor_op( + x: ibis_types.Value, + y: ibis_types.Value, +): + return typing.cast(ibis_types.BooleanValue, x) ^ typing.cast( + ibis_types.BooleanValue, y + ) + + @scalar_op_compiler.register_binary_op(ops.add_op) @short_circuit_nulls() def add_op( diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 12c96b90f06..a349ea8f6b7 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1112,6 +1112,33 @@ def __rpow__(self, other): __rpow__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__rpow__) + def __and__(self, other: bool | int | bigframes.series.Series) -> DataFrame: + return self._apply_binop(other, ops.and_op) + + __and__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__and__) + + __rand__ = __and__ + + def __or__(self, other: bool | int | bigframes.series.Series) -> DataFrame: + return self._apply_binop(other, ops.or_op) + + __or__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__or__) + + __ror__ = __or__ + + def __xor__(self, other: bool | int | bigframes.series.Series) -> DataFrame: + return self._apply_binop(other, ops.xor_op) + + __xor__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__xor__) + + __rxor__ = __xor__ + + def __pos__(self) -> DataFrame: + return self._apply_unary_op(ops.pos_op) + + def __neg__(self) -> DataFrame: + return self._apply_unary_op(ops.neg_op) + def align( self, other: typing.Union[DataFrame, bigframes.series.Series], diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index fe9fe6df206..c1854b1b61b 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -186,7 +186,7 @@ def create_binary_op( dtypes.is_binary_like, description="binary-like", ), -) # numeric +) isnull_op = create_unary_op( name="isnull", type_signature=op_typing.FixedOutputType( @@ -311,6 +311,8 @@ def create_binary_op( floor_op = create_unary_op(name="floor", type_signature=op_typing.UNARY_REAL_NUMERIC) ceil_op = create_unary_op(name="ceil", type_signature=op_typing.UNARY_REAL_NUMERIC) abs_op = create_unary_op(name="abs", type_signature=op_typing.UNARY_NUMERIC) +pos_op = create_unary_op(name="pos", type_signature=op_typing.UNARY_NUMERIC) +neg_op = create_unary_op(name="neg", type_signature=op_typing.UNARY_NUMERIC) exp_op = create_unary_op(name="exp", type_signature=op_typing.UNARY_REAL_NUMERIC) expm1_op = create_unary_op(name="expm1", type_signature=op_typing.UNARY_REAL_NUMERIC) ln_op = create_unary_op(name="log", type_signature=op_typing.UNARY_REAL_NUMERIC) @@ -650,6 +652,7 @@ def output_type(self, *input_types): # Logical Ops and_op = create_binary_op(name="and", type_signature=op_typing.LOGICAL) or_op = create_binary_op(name="or", type_signature=op_typing.LOGICAL) +xor_op = create_binary_op(name="xor", type_signature=op_typing.LOGICAL) ## Comparison Ops eq_op = create_binary_op(name="eq", type_signature=op_typing.COMPARISON) diff --git a/bigframes/series.py b/bigframes/series.py index 3f1fa4c3a5e..367301f08e0 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -668,6 +668,13 @@ def __or__(self, other: bool | int | Series) -> Series: __ror__ = __or__ + def __xor__(self, other: bool | int | Series) -> Series: + return self._apply_binary_op(other, ops.xor_op) + + __or__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__xor__) + + __rxor__ = __xor__ + def __add__(self, other: float | int | Series) -> Series: return self.add(other) @@ -1036,6 +1043,12 @@ def __invert__(self) -> Series: __invert__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__invert__) + def __pos__(self) -> Series: + return self._apply_unary_op(ops.pos_op) + + def __neg__(self) -> Series: + return self._apply_unary_op(ops.neg_op) + def eq(self, other: object) -> Series: # TODO: enforce stricter alignment return self._apply_binary_op(other, ops.eq_op) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index dbdcf7dafc1..e0f4793943c 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1699,6 +1699,22 @@ def test_df_abs(scalars_dfs): assert_pandas_df_equal(bf_result, pd_result) +def test_df_pos(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = (+scalars_df[["int64_col", "numeric_col"]]).to_pandas() + pd_result = +scalars_pandas_df[["int64_col", "numeric_col"]] + + assert_pandas_df_equal(pd_result, bf_result) + + +def test_df_neg(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = (-scalars_df[["int64_col", "numeric_col"]]).to_pandas() + pd_result = -scalars_pandas_df[["int64_col", "numeric_col"]] + + assert_pandas_df_equal(pd_result, bf_result) + + def test_df_invert(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs columns = ["int64_col", "bool_col"] diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index dbc8ddec6f9..3e21418f2f6 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -354,6 +354,36 @@ def test_abs(scalars_dfs, col_name): assert_series_equal(pd_result, bf_result) +@pytest.mark.parametrize( + ("col_name",), + ( + ("float64_col",), + ("int64_too",), + ), +) +def test_series_pos(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = (+scalars_df[col_name]).to_pandas() + pd_result = +scalars_pandas_df[col_name] + + assert_series_equal(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("col_name",), + ( + ("float64_col",), + ("int64_too",), + ), +) +def test_series_neg(scalars_dfs, col_name): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = (-scalars_df[col_name]).to_pandas() + pd_result = -scalars_pandas_df[col_name] + + assert_series_equal(pd_result, bf_result) + + @pytest.mark.parametrize( ("col_name",), ( @@ -678,10 +708,12 @@ def test_series_pow_scalar_reverse(scalars_dfs): [ (lambda x, y: x & y), (lambda x, y: x | y), + (lambda x, y: x ^ y), ], ids=[ "and", "or", + "xor", ], ) @pytest.mark.parametrize(("other_scalar"), [True, False, pd.NA]) @@ -714,6 +746,7 @@ def test_series_bool_bool_operators_scalar( (lambda x, y: x // y), (lambda x, y: x & y), (lambda x, y: x | y), + (lambda x, y: x ^ y), ], ids=[ "add", @@ -728,6 +761,7 @@ def test_series_bool_bool_operators_scalar( "floordivide", "bitwise_and", "bitwise_or", + "bitwise_xor", ], ) def test_series_int_int_operators_series(scalars_dfs, operator): diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index ba222fc8c33..d46fa4cfc7a 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -3555,6 +3555,42 @@ def __rpow__(self, other): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def __and__(self, other): + """Get bitwise AND of DataFrame and other, element-wise, using operator `&`. + + Args: + other (scalar, Series or DataFrame): + Object to bitwise AND with the DataFrame. + + Returns: + bigframes.dataframe.DataFrame: The result of the operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __or__(self, other): + """Get bitwise OR of DataFrame and other, element-wise, using operator `|`. + + Args: + other (scalar, Series or DataFrame): + Object to bitwise OR with the DataFrame. + + Returns: + bigframes.dataframe.DataFrame: The result of the operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __xor__(self, other): + """Get bitwise XOR of DataFrame and other, element-wise, using operator `^`. + + Args: + other (scalar, Series or DataFrame): + Object to bitwise XOR with the DataFrame. + + Returns: + bigframes.dataframe.DataFrame: The result of the operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def combine( self, other, func, fill_value=None, overwrite: bool = True ) -> DataFrame: diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 56f1c8b3e05..6a7a815ed98 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -4224,7 +4224,7 @@ def __and__(self, other): Object to bitwise AND with the Series. Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) @@ -4262,7 +4262,45 @@ def __or__(self, other): Object to bitwise OR with the Series. Returns: - Series: The result of the operation. + bigframes.series.Series: The result of the operation. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __xor__(self, other): + """Get bitwise XOR of Series and other, element-wise, using operator `^`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([0, 1, 2, 3]) + + You can operate with a scalar. + + >>> s ^ 6 + 0 6 + 1 7 + 2 4 + 3 5 + dtype: Int64 + + You can operate with another Series. + + >>> s1 = bpd.Series([5, 6, 7, 8]) + >>> s ^ s1 + 0 5 + 1 7 + 2 5 + 3 11 + dtype: Int64 + + Args: + other (scalar or Series): + Object to bitwise XOR with the Series. + + Returns: + bigframes.series.Series: The result of the operation. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 9f0406ea1f346f446282d547d78de7dfabb7c1af Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 29 May 2024 17:30:32 -0700 Subject: [PATCH 604/921] refactor: New read node that defers ibis table instantiation (#709) --- bigframes/core/__init__.py | 34 ++- bigframes/core/compile/compiler.py | 89 +++++++ bigframes/core/compile/default_ordering.py | 91 +++++++ bigframes/core/compile/schema_translator.py | 34 +++ bigframes/core/nodes.py | 64 +++++ bigframes/core/schema.py | 17 ++ bigframes/dtypes.py | 17 +- bigframes/session/__init__.py | 80 +++---- bigframes/session/_io/bigquery/__init__.py | 154 ++++++------ .../session/_io/bigquery/read_gbq_table.py | 226 +++++++----------- tests/unit/session/test_io_bigquery.py | 18 +- tests/unit/session/test_read_gbq_table.py | 26 -- tests/unit/session/test_session.py | 30 ++- 13 files changed, 564 insertions(+), 316 deletions(-) create mode 100644 bigframes/core/compile/default_ordering.py create mode 100644 bigframes/core/compile/schema_translator.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 5024b01323d..133d271fedc 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -14,12 +14,15 @@ from __future__ import annotations from dataclasses import dataclass +import datetime import functools import io import itertools import typing -from typing import Iterable, Sequence +from typing import Iterable, Optional, Sequence +import warnings +import google.cloud.bigquery import ibis.expr.types as ibis_types import pandas import pyarrow as pa @@ -92,6 +95,35 @@ def from_pyarrow(cls, arrow_table: pa.Table, session: Session): ) return cls(node) + @classmethod + def from_table( + cls, + table: google.cloud.bigquery.Table, + schema: schemata.ArraySchema, + session: Session, + *, + predicate: Optional[str] = None, + at_time: Optional[datetime.datetime] = None, + primary_key: Sequence[str] = (), + ): + if any(i.field_type == "JSON" for i in table.schema if i.name in schema.names): + warnings.warn( + "Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.", + bigframes.exceptions.PreviewWarning, + ) + node = nodes.ReadTableNode( + project_id=table.reference.project, + dataset_id=table.reference.dataset_id, + table_id=table.reference.table_id, + physical_schema=tuple(table.schema), + total_order_cols=tuple(primary_key), + columns=schema, + at_time=at_time, + table_session=session, + sql_predicate=predicate, + ) + return cls(node) + @property def column_ids(self) -> typing.Sequence[str]: return self.schema.names diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index a9908192f32..f948d10a5bd 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -17,12 +17,20 @@ import io import typing +import ibis +import ibis.backends +import ibis.backends.bigquery +import ibis.expr.types import pandas as pd import bigframes.core.compile.compiled as compiled import bigframes.core.compile.concat as concat_impl +import bigframes.core.compile.default_ordering as default_ordering +import bigframes.core.compile.schema_translator import bigframes.core.compile.single_column import bigframes.core.nodes as nodes +import bigframes.core.ordering as bf_ordering +import bigframes.dtypes as bigframes_dtypes if typing.TYPE_CHECKING: import bigframes.core @@ -88,6 +96,87 @@ def compile_readlocal(node: nodes.ReadLocalNode, ordered: bool = True): return ordered_ir.to_unordered() +@_compile_node.register +def compile_readtable(node: nodes.ReadTableNode, ordered: bool = True): + if ordered: + return compile_read_table_ordered(node) + else: + return compile_read_table_unordered(node) + + +def read_table_as_unordered_ibis(node: nodes.ReadTableNode) -> ibis.expr.types.Table: + full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" + used_columns = ( + *node.schema.names, + *[i for i in node.total_order_cols if i not in node.schema.names], + ) + # Physical schema might include unused columns, unsupported datatypes like JSON + physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( + list(i for i in node.physical_schema if i.name in used_columns) + ) + if node.at_time is not None or node.sql_predicate is not None: + import bigframes.session._io.bigquery + + sql = bigframes.session._io.bigquery.to_query( + full_table_name, + columns=used_columns, + sql_predicate=node.sql_predicate, + time_travel_timestamp=node.at_time, + ) + return ibis.backends.bigquery.Backend().sql(schema=physical_schema, query=sql) + else: + return ibis.table(physical_schema, full_table_name) + + +def compile_read_table_unordered(node: nodes.ReadTableNode): + ibis_table = read_table_as_unordered_ibis(node) + return compiled.UnorderedIR( + ibis_table, + tuple( + bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + for col in node.schema.names + ), + ) + + +def compile_read_table_ordered(node: nodes.ReadTableNode): + ibis_table = read_table_as_unordered_ibis(node) + if node.total_order_cols: + ordering_value_columns = tuple( + bf_ordering.ascending_over(col) for col in node.total_order_cols + ) + if node.order_col_is_sequential: + integer_encoding = bf_ordering.IntegerEncoding( + is_encoded=True, is_sequential=True + ) + else: + integer_encoding = bf_ordering.IntegerEncoding() + ordering = bf_ordering.ExpressionOrdering( + ordering_value_columns, + integer_encoding=integer_encoding, + total_ordering_columns=frozenset(node.total_order_cols), + ) + hidden_columns = () + else: + ibis_table, ordering = default_ordering.gen_default_ordering( + ibis_table, use_double_hash=True + ) + hidden_columns = tuple( + ibis_table[col] + for col in ibis_table.columns + if col not in node.schema.names + ) + return compiled.OrderedIR( + ibis_table, + columns=tuple( + bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + for col in node.schema.names + ), + ordering=ordering, + hidden_ordering_columns=hidden_columns, + ) + + @_compile_node.register def compile_readgbq(node: nodes.ReadGbqNode, ordered: bool = True): if ordered: diff --git a/bigframes/core/compile/default_ordering.py b/bigframes/core/compile/default_ordering.py new file mode 100644 index 00000000000..d8bdc0546bb --- /dev/null +++ b/bigframes/core/compile/default_ordering.py @@ -0,0 +1,91 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Private helpers for loading a BigQuery table as a BigQuery DataFrames DataFrame. +""" + +from __future__ import annotations + +import itertools +from typing import cast + +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +import ibis +import ibis.backends +import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.types as ibis_types + +import bigframes.core.guid as guid +import bigframes.core.ordering as order + + +def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringValue: + col_type = column.type() + if ( + col_type.is_numeric() + or col_type.is_boolean() + or col_type.is_binary() + or col_type.is_temporal() + ): + result = column.cast(ibis_dtypes.String(nullable=True)) + elif col_type.is_geospatial(): + result = cast(ibis_types.GeoSpatialColumn, column).as_text() + elif col_type.is_string(): + result = column + else: + # TO_JSON_STRING works with all data types, but isn't the most efficient + # Needed for JSON, STRUCT and ARRAY datatypes + result = vendored_ibis_ops.ToJsonString(column).to_expr() # type: ignore + # Escape backslashes and use backslash as delineator + escaped = cast(ibis_types.StringColumn, result.fillna("")).replace("\\", "\\\\") # type: ignore + return cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) + + +def gen_default_ordering(table: ibis.table, use_double_hash: bool = True): + ordering_hash_part = guid.generate_guid("bigframes_ordering_") + ordering_hash_part2 = guid.generate_guid("bigframes_ordering_") + ordering_rand_part = guid.generate_guid("bigframes_ordering_") + + # All inputs into hash must be non-null or resulting hash will be null + str_values = list( + map(lambda col: _convert_to_nonnull_string(table[col]), table.columns) + ) + full_row_str = ( + str_values[0].concat(*str_values[1:]) if len(str_values) > 1 else str_values[0] + ) + full_row_hash = full_row_str.hash().name(ordering_hash_part) + # By modifying value slightly, we get another hash uncorrelated with the first + full_row_hash_p2 = (full_row_str + "_").hash().name(ordering_hash_part2) + # Used to disambiguate between identical rows (which will have identical hash) + random_value = ibis.random().name(ordering_rand_part) + + order_values = ( + [full_row_hash, full_row_hash_p2, random_value] + if use_double_hash + else [full_row_hash, random_value] + ) + + original_column_ids = table.columns + table_with_ordering = table.select( + itertools.chain(original_column_ids, order_values) + ) + + ordering = order.ExpressionOrdering( + ordering_value_columns=tuple( + order.ascending_over(col.get_name()) for col in order_values + ), + total_ordering_columns=frozenset(col.get_name() for col in order_values), + ) + return table_with_ordering, ordering diff --git a/bigframes/core/compile/schema_translator.py b/bigframes/core/compile/schema_translator.py new file mode 100644 index 00000000000..03e9691af64 --- /dev/null +++ b/bigframes/core/compile/schema_translator.py @@ -0,0 +1,34 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import ibis +import ibis.expr.schema + +import bigframes.core.schema as bf_schema +import bigframes.dtypes + + +def convert_bf_schema(schema: bf_schema.ArraySchema) -> ibis.expr.schema.Schema: + """ + Convert bigframes schema to ibis schema. This is unambigous as every bigframes type is backed by a specific SQL/ibis dtype. + """ + names = schema.names + types = [ + bigframes.dtypes.bigframes_dtype_to_ibis_dtype(bf_type) + for bf_type in schema.dtypes + ] + return ibis.schema(names=names, types=types) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 688e1657328..1af7c5bd17d 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -16,11 +16,14 @@ import abc from dataclasses import dataclass, field, fields, replace +import datetime import functools import itertools import typing from typing import Callable, Tuple +import google.cloud.bigquery as bq + import bigframes.core.expression as ex import bigframes.core.guid from bigframes.core.join_def import JoinColumnMapping, JoinDefinition, JoinSide @@ -345,6 +348,67 @@ def transform_children( return self +## Put ordering in here or just add order_by node above? +@dataclass(frozen=True) +class ReadTableNode(BigFrameNode): + project_id: str = field() + dataset_id: str = field() + table_id: str = field() + + physical_schema: Tuple[bq.SchemaField, ...] = field() + # Subset of physical schema columns, with chosen BQ types + columns: schemata.ArraySchema = field() + + table_session: bigframes.session.Session = field() + # Empty tuple if no primary key (primary key can be any set of columns that together form a unique key) + # Empty if no known unique key + total_order_cols: Tuple[str, ...] = field() + # indicates a primary key that is exactly offsets 0, 1, 2, ..., N-2, N-1 + order_col_is_sequential: bool = False + at_time: typing.Optional[datetime.datetime] = None + # Added for backwards compatibility, not validated + sql_predicate: typing.Optional[str] = None + + def __post_init__(self): + # enforce invariants + physical_names = set(map(lambda i: i.name, self.physical_schema)) + if not set(self.columns.names).issubset(physical_names): + raise ValueError( + f"Requested schema {self.columns} cannot be derived from table schemal {self.physical_schema}" + ) + if self.order_col_is_sequential and len(self.total_order_cols) == 1: + raise ValueError("Sequential primary key must have only one component") + + @property + def session(self): + return self.table_session + + def __hash__(self): + return self._node_hash + + @property + def roots(self) -> typing.Set[BigFrameNode]: + return {self} + + @property + def schema(self) -> schemata.ArraySchema: + return self.columns + + @property + def relation_ops_created(self) -> int: + # Assume worst case, where readgbq actually has baked in analytic operation to generate index + return 3 + + @functools.cached_property + def variables_introduced(self) -> int: + return len(self.schema.items) + 1 + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return self + + # Unary nodes @dataclass(frozen=True) class PromoteOffsetsNode(UnaryNode): diff --git a/bigframes/core/schema.py b/bigframes/core/schema.py index 3629778aaf4..30a2a6593a1 100644 --- a/bigframes/core/schema.py +++ b/bigframes/core/schema.py @@ -18,6 +18,8 @@ import functools import typing +import google.cloud.bigquery + import bigframes.core.guid import bigframes.dtypes @@ -34,6 +36,16 @@ class SchemaItem: class ArraySchema: items: typing.Tuple[SchemaItem, ...] + @classmethod + def from_bq_table(cls, table: google.cloud.bigquery.Table): + items = tuple( + SchemaItem(name, dtype) + for name, dtype in bigframes.dtypes.bf_type_from_type_kind( + table.schema + ).items() + ) + return ArraySchema(items) + @property def names(self) -> typing.Tuple[str, ...]: return tuple(item.column for item in self.items) @@ -51,6 +63,11 @@ def drop(self, columns: typing.Iterable[str]) -> ArraySchema: tuple(item for item in self.items if item.column not in columns) ) + def select(self, columns: typing.Iterable[str]) -> ArraySchema: + return ArraySchema( + tuple(SchemaItem(name, self.get_type(name)) for name in columns) + ) + def append(self, item: SchemaItem): return ArraySchema(tuple([*self.items, item])) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 2a344aff2df..0c32a81404d 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -19,6 +19,7 @@ import textwrap import typing from typing import Any, Dict, Iterable, Literal, Tuple, Union +import warnings import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops @@ -33,6 +34,7 @@ import pyarrow as pa import bigframes.constants as constants +import bigframes.exceptions # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ @@ -181,6 +183,10 @@ def is_bool_coercable(type: ExpressionType) -> bool: ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), pd.ArrowDtype(pa.decimal256(76, 38)), ), + ( + ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True), + gpd.array.GeometryDtype(), + ), ) BIGFRAMES_TO_IBIS: Dict[Dtype, ibis_dtypes.DataType] = { @@ -212,9 +218,6 @@ def is_bool_coercable(type: ExpressionType) -> bool: ) IBIS_TO_BIGFRAMES.update( { - ibis_dtypes.GeoSpatial( - geotype="geography", srid=4326, nullable=True - ): gpd.array.GeometryDtype(), # TODO: Interval } ) @@ -280,6 +283,14 @@ def ibis_dtype_to_bigframes_dtype( if isinstance(ibis_dtype, ibis_dtypes.Integer): return pd.Int64Dtype() + # Temporary: Will eventually support an explicit json type instead of casting to string. + if isinstance(ibis_dtype, ibis_dtypes.JSON): + warnings.warn( + "Interpreting JSON as string. This behavior may change in future versions.", + bigframes.exceptions.PreviewWarning, + ) + return STRING_DTYPE + if ibis_dtype in IBIS_TO_BIGFRAMES: return IBIS_TO_BIGFRAMES[ibis_dtype] elif isinstance(ibis_dtype, ibis_dtypes.Decimal): diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index c953087b3e0..c8601c101e5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -19,6 +19,7 @@ import collections.abc import copy import datetime +import itertools import logging import math import os @@ -85,6 +86,7 @@ import bigframes.core.nodes as nodes from bigframes.core.ordering import IntegerEncoding import bigframes.core.ordering as order +import bigframes.core.schema as schemata import bigframes.core.tree_properties as traversals import bigframes.core.tree_properties as tree_properties import bigframes.core.utils as utils @@ -621,11 +623,11 @@ def _read_gbq_query( if len(filters) != 0 or max_results is not None: # TODO(b/338111344): If we are running a query anyway, we might as # well generate ROW_NUMBER() at the same time. + all_columns = itertools.chain(index_cols, columns) if columns else () query = bf_io_bigquery.to_query( query, - index_cols, - columns, - filters, + all_columns, + bf_io_bigquery.compile_filters(filters) if filters else None, max_results=max_results, # We're executing the query, so we don't need time travel for # determinism. @@ -741,7 +743,6 @@ def _read_gbq_table( # Fetch table metadata and validate # --------------------------------- - time_travel_timestamp: Optional[datetime.datetime] = None time_travel_timestamp, table = bf_read_gbq_table.get_table_metadata( self.bqclient, table_ref=table_ref, @@ -799,11 +800,13 @@ def _read_gbq_table( ): # TODO(b/338111344): If we are running a query anyway, we might as # well generate ROW_NUMBER() at the same time. + all_columns = itertools.chain(index_cols, columns) if columns else () query = bf_io_bigquery.to_query( query, - index_cols=index_cols, - columns=columns, - filters=filters, + columns=all_columns, + sql_predicate=bf_io_bigquery.compile_filters(filters) + if filters + else None, max_results=max_results, # We're executing the query, so we don't need time travel for # determinism. @@ -819,7 +822,7 @@ def _read_gbq_table( ) # ----------------------------------------- - # Create Ibis table expression and validate + # Validate table access and features # ----------------------------------------- # Use a time travel to make sure the DataFrame is deterministic, even @@ -828,37 +831,15 @@ def _read_gbq_table( # If a dry run query fails with time travel but # succeeds without it, omit the time travel clause and raise a warning # about potential non-determinism if the underlying tables are modified. - sql = bigframes.session._io.bigquery.to_query( - f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}", - index_cols=index_cols, - columns=columns, - filters=filters, - time_travel_timestamp=time_travel_timestamp, - max_results=None, + filter_str = bf_io_bigquery.compile_filters(filters) if filters else None + all_columns = ( + () + if len(columns) == 0 + else (*columns, *[col for col in index_cols if col not in columns]) ) - dry_run_config = bigquery.QueryJobConfig() - dry_run_config.dry_run = True - try: - self._start_query(sql, job_config=dry_run_config, api_name=api_name) - except google.api_core.exceptions.NotFound: - # note that a notfound caused by a simple typo will be - # caught above when the metadata is fetched, not here - time_travel_timestamp = None - warnings.warn( - "NotFound error when reading table with time travel." - " Attempting query without time travel. Warning: Without" - " time travel, modifications to the underlying table may" - " result in errors or unexpected behavior.", - category=bigframes.exceptions.TimeTravelDisabledWarning, - ) - table_expression = bf_read_gbq_table.get_ibis_time_travel_table( - ibis_client=self.ibis_client, - table_ref=table_ref, - index_cols=index_cols, - columns=columns, - filters=filters, - time_travel_timestamp=time_travel_timestamp, + supports_snapshot = bf_read_gbq_table.validate_table( + self.bqclient, table_ref, all_columns, time_travel_timestamp, filter_str ) # ---------------------------- @@ -876,20 +857,17 @@ def _read_gbq_table( index_cols=index_cols, api_name=api_name, ) - - if is_index_unique: - array_value = bf_read_gbq_table.to_array_value_with_total_ordering( - session=self, - table_expression=table_expression, - total_ordering_cols=index_cols, - ) - else: - # Note: Even though we're adding a default ordering here, that's - # just so we have a deterministic total ordering. If the user - # specified a non-unique index, we still sort by that later. - array_value = bf_read_gbq_table.to_array_value_with_default_ordering( - session=self, table=table_expression, table_rows=table.num_rows - ) + schema = schemata.ArraySchema.from_bq_table(table) + if columns: + schema = schema.select(index_cols + columns) + array_value = core.ArrayValue.from_table( + table, + schema=schema, + predicate=filter_str, + at_time=time_travel_timestamp if supports_snapshot else None, + primary_key=index_cols if is_index_unique else (), + session=self, + ) # ---------------------------------------------------- # Create Default Sequential Index if still have no index diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 6afa86aa2d2..f26ca26c2a3 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -392,14 +392,12 @@ def is_table_with_wildcard_suffix(query_or_table: str) -> bool: def to_query( query_or_table: str, - index_cols: Iterable[str], columns: Iterable[str], - filters: third_party_pandas_gbq.FiltersType, - max_results: Optional[int], - time_travel_timestamp: Optional[datetime.datetime], + sql_predicate: Optional[str], + max_results: Optional[int] = None, + time_travel_timestamp: Optional[datetime.datetime] = None, ) -> str: """Compile query_or_table with conditions(filters, wildcards) to query.""" - filters = list(filters) sub_query = ( f"({query_or_table})" if is_query(query_or_table) else f"`{query_or_table}`" ) @@ -409,8 +407,7 @@ def to_query( if columns: # We only reduce the selection if columns is set, but we always # want to make sure index_cols is also included. - all_columns = itertools.chain(index_cols, columns) - select_clause = "SELECT " + ", ".join(f"`{column}`" for column in all_columns) + select_clause = "SELECT " + ", ".join(f"`{column}`" for column in columns) else: select_clause = "SELECT *" @@ -423,77 +420,84 @@ def to_query( if max_results is not None: limit_clause = f" LIMIT {bigframes.core.sql.simple_literal(max_results)}" - filter_string = "" - if filters: - valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { - "in": "IN", - "not in": "NOT IN", - "LIKE": "LIKE", - "==": "=", - ">": ">", - "<": "<", - ">=": ">=", - "<=": "<=", - "!=": "!=", - } - - # If single layer filter, add another pseudo layer. So the single layer represents "and" logic. - if isinstance(filters[0], tuple) and ( - len(filters[0]) == 0 or not isinstance(list(filters[0])[0], tuple) - ): - filters = typing.cast(third_party_pandas_gbq.FiltersType, [filters]) - - for group in filters: - if not isinstance(group, Iterable): - group = [group] - - and_expression = "" - for filter_item in group: - if not isinstance(filter_item, tuple) or (len(filter_item) != 3): - raise ValueError( - f"Elements of filters must be tuples of length 3, but got {repr(filter_item)}.", - ) - - column, operator, value = filter_item - - if not isinstance(column, str): - raise ValueError( - f"Column name should be a string, but received '{column}' of type {type(column).__name__}." - ) - - if operator not in valid_operators: - raise ValueError(f"Operator {operator} is not valid.") - - operator_str = valid_operators[operator] - - column_ref = bigframes.core.sql.identifier(column) - if operator_str in ["IN", "NOT IN"]: - value_literal = bigframes.core.sql.multi_literal(*value) - else: - value_literal = bigframes.core.sql.simple_literal(value) - expression = bigframes.core.sql.infix_op( - operator_str, column_ref, value_literal - ) - if and_expression: - and_expression = bigframes.core.sql.infix_op( - "AND", and_expression, expression - ) - else: - and_expression = expression - - if filter_string: - filter_string = bigframes.core.sql.infix_op( - "OR", filter_string, and_expression - ) - else: - filter_string = and_expression - - where_clause = "" - if filter_string: - where_clause = f" WHERE {filter_string}" + where_clause = f" WHERE {sql_predicate}" if sql_predicate else "" return ( f"{select_clause} " f"FROM {sub_query}" f"{time_travel_clause}{where_clause}{limit_clause}" ) + + +def compile_filters(filters: third_party_pandas_gbq.FiltersType) -> str: + """Compiles a set of filters into a boolean sql expression""" + if not filters: + return "" + filter_string = "" + valid_operators: Mapping[third_party_pandas_gbq.FilterOps, str] = { + "in": "IN", + "not in": "NOT IN", + "LIKE": "LIKE", + "==": "=", + ">": ">", + "<": "<", + ">=": ">=", + "<=": "<=", + "!=": "!=", + } + + # If single layer filter, add another pseudo layer. So the single layer represents "and" logic. + filters_list: list = list(filters) + if isinstance(filters_list[0], tuple) and ( + len(filters_list[0]) == 0 or not isinstance(list(filters_list[0])[0], tuple) + ): + filter_items = [filters_list] + else: + filter_items = filters_list + + for group in filter_items: + if not isinstance(group, Iterable): + group = [group] + + and_expression = "" + for filter_item in group: + if not isinstance(filter_item, tuple) or (len(filter_item) != 3): + raise ValueError( + f"Elements of filters must be tuples of length 3, but got {repr(filter_item)}.", + ) + + column, operator, value = filter_item + + if not isinstance(column, str): + raise ValueError( + f"Column name should be a string, but received '{column}' of type {type(column).__name__}." + ) + + if operator not in valid_operators: + raise ValueError(f"Operator {operator} is not valid.") + + operator_str = valid_operators[operator] + + column_ref = bigframes.core.sql.identifier(column) + if operator_str in ["IN", "NOT IN"]: + value_literal = bigframes.core.sql.multi_literal(*value) + else: + value_literal = bigframes.core.sql.simple_literal(value) + expression = bigframes.core.sql.infix_op( + operator_str, column_ref, value_literal + ) + if and_expression: + and_expression = bigframes.core.sql.infix_op( + "AND", and_expression, expression + ) + else: + and_expression = expression + + if filter_string: + filter_string = bigframes.core.sql.infix_op( + "OR", filter_string, and_expression + ) + else: + filter_string = and_expression + + return filter_string diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index e00892fce99..879a8ba44ce 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -19,27 +19,18 @@ from __future__ import annotations import datetime -import itertools import typing -from typing import Dict, Iterable, List, Optional, Tuple +from typing import Dict, Iterable, List, Optional, Sequence, Tuple import warnings -import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops -import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions import google.cloud.bigquery as bigquery -import ibis -import ibis.backends -import ibis.expr.datatypes as ibis_dtypes -import ibis.expr.types as ibis_types import bigframes import bigframes.clients import bigframes.constants -import bigframes.core as core import bigframes.core.compile -import bigframes.core.guid as guid -import bigframes.core.ordering as order +import bigframes.core.compile.default_ordering import bigframes.core.sql import bigframes.dtypes import bigframes.session._io.bigquery @@ -51,28 +42,6 @@ import bigframes.session -def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringValue: - col_type = column.type() - if ( - col_type.is_numeric() - or col_type.is_boolean() - or col_type.is_binary() - or col_type.is_temporal() - ): - result = column.cast(ibis_dtypes.String(nullable=True)) - elif col_type.is_geospatial(): - result = typing.cast(ibis_types.GeoSpatialColumn, column).as_text() - elif col_type.is_string(): - result = column - else: - # TO_JSON_STRING works with all data types, but isn't the most efficient - # Needed for JSON, STRUCT and ARRAY datatypes - result = vendored_ibis_ops.ToJsonString(column).to_expr() # type: ignore - # Escape backslashes and use backslash as delineator - escaped = typing.cast(ibis_types.StringColumn, result.fillna("")).replace("\\", "\\\\") # type: ignore - return typing.cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) - - def get_table_metadata( bqclient: bigquery.Client, table_ref: google.cloud.bigquery.table.TableReference, @@ -128,39 +97,55 @@ def get_table_metadata( return cached_table -def get_ibis_time_travel_table( - ibis_client: ibis.BaseBackend, - table_ref: bigquery.TableReference, - index_cols: Iterable[str], - columns: Iterable[str], - filters: third_party_pandas_gbq.FiltersType, - time_travel_timestamp: Optional[datetime.datetime], -) -> ibis_types.Table: - # If we have an anonymous query results table, it can't be modified and - # there isn't any BigQuery time travel. - if table_ref.dataset_id.startswith("_"): - time_travel_timestamp = None - +def validate_table( + bqclient: bigquery.Client, + table_ref: bigquery.table.TableReference, + columns: Optional[Sequence[str]], + snapshot_time: datetime.datetime, + filter_str: Optional[str] = None, +) -> bool: + """Validates that the table can be read, returns True iff snapshot is supported.""" + # First run without snapshot to verify table can be read + sql = bigframes.session._io.bigquery.to_query( + query_or_table=f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}", + columns=columns or (), + sql_predicate=filter_str, + ) + dry_run_config = bigquery.QueryJobConfig() + dry_run_config.dry_run = True try: - return ibis_client.sql( - bigframes.session._io.bigquery.to_query( - f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}", - index_cols=index_cols, - columns=columns, - filters=filters, - time_travel_timestamp=time_travel_timestamp, - # If we've made it this far, we know we don't have any - # max_results to worry about, because in that case we will - # have executed a query with a LIMI clause. - max_results=None, - ) - ) + bqclient.query_and_wait(sql, job_config=dry_run_config) except google.api_core.exceptions.Forbidden as ex: - # Ibis does a dry run to get the types of the columns from the SQL. if "Drive credentials" in ex.message: ex.message += "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." raise + # Anonymous dataset, does not support snapshot ever + if table_ref.dataset_id.startswith("_"): + return False + + # Second, try with snapshot to verify table supports this feature + snapshot_sql = bigframes.session._io.bigquery.to_query( + query_or_table=f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}", + columns=columns or (), + sql_predicate=filter_str, + time_travel_timestamp=snapshot_time, + ) + try: + bqclient.query_and_wait(snapshot_sql, job_config=dry_run_config) + return True + except google.api_core.exceptions.NotFound: + # note that a notfound caused by a simple typo will be + # caught above when the metadata is fetched, not here + warnings.warn( + "NotFound error when reading table with time travel." + " Attempting query without time travel. Warning: Without" + " time travel, modifications to the underlying table may" + " result in errors or unexpected behavior.", + category=bigframes.exceptions.TimeTravelDisabledWarning, + ) + return False + def are_index_cols_unique( bqclient: bigquery.Client, @@ -287,81 +272,52 @@ def get_index_cols( return index_cols -def to_array_value_with_total_ordering( - session: bigframes.session.Session, - table_expression: ibis_types.Table, - total_ordering_cols: List[str], -) -> core.ArrayValue: - """Create an ArrayValue, assuming we already have a total ordering.""" - ordering = order.ExpressionOrdering( - ordering_value_columns=tuple( - order.ascending_over(column_id) for column_id in total_ordering_cols - ), - total_ordering_columns=frozenset(total_ordering_cols), - ) - column_values = [table_expression[col] for col in table_expression.columns] - return core.ArrayValue.from_ibis( - session, - table_expression, - columns=column_values, - hidden_ordering_columns=[], - ordering=ordering, - ) - - -def to_array_value_with_default_ordering( - session: bigframes.session.Session, - table: ibis_types.Table, - table_rows: Optional[int], -) -> core.ArrayValue: - """Create an ArrayValue with a deterministic default ordering.""" - # Since this might also be used as the index, don't use the default - # "ordering ID" name. - - # For small tables, 64 bits is enough to avoid collisions, 128 bits will never ever collide no matter what - # Assume table is large if table row count is unknown - use_double_hash = (table_rows is None) or (table_rows == 0) or (table_rows > 100000) - - ordering_hash_part = guid.generate_guid("bigframes_ordering_") - ordering_hash_part2 = guid.generate_guid("bigframes_ordering_") - ordering_rand_part = guid.generate_guid("bigframes_ordering_") +def get_time_travel_datetime_and_table_metadata( + bqclient: bigquery.Client, + table_ref: bigquery.TableReference, + *, + api_name: str, + cache: Dict[bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table]], + use_cache: bool = True, +) -> Tuple[datetime.datetime, bigquery.Table]: + cached_table = cache.get(table_ref) + if use_cache and cached_table is not None: + snapshot_timestamp, _ = cached_table - # All inputs into hash must be non-null or resulting hash will be null - str_values = list( - map(lambda col: _convert_to_nonnull_string(table[col]), table.columns) - ) - full_row_str = ( - str_values[0].concat(*str_values[1:]) if len(str_values) > 1 else str_values[0] - ) - full_row_hash = full_row_str.hash().name(ordering_hash_part) - # By modifying value slightly, we get another hash uncorrelated with the first - full_row_hash_p2 = (full_row_str + "_").hash().name(ordering_hash_part2) - # Used to disambiguate between identical rows (which will have identical hash) - random_value = ibis.random().name(ordering_rand_part) - - order_values = ( - [full_row_hash, full_row_hash_p2, random_value] - if use_double_hash - else [full_row_hash, random_value] - ) + # Cache hit could be unexpected. See internal issue 329545805. + # Raise a warning with more information about how to avoid the + # problems with the cache. + warnings.warn( + f"Reading cached table from {snapshot_timestamp} to avoid " + "incompatibilies with previous reads of this table. To read " + "the latest version, set `use_cache=False` or close the " + "current session with Session.close() or " + "bigframes.pandas.close_session().", + # There are many layers before we get to (possibly) the user's code: + # pandas.read_gbq_table + # -> with_default_session + # -> Session.read_gbq_table + # -> _read_gbq_table + # -> _get_snapshot_sql_and_primary_key + # -> get_snapshot_datetime_and_table_metadata + stacklevel=7, + ) + return cached_table - original_column_ids = table.columns - table_with_ordering = table.select( - itertools.chain(original_column_ids, order_values) - ) + # TODO(swast): It's possible that the table metadata is changed between now + # and when we run the CURRENT_TIMESTAMP() query to see when we can time + # travel to. Find a way to fetch the table metadata and BQ's current time + # atomically. + table = bqclient.get_table(table_ref) - ordering = order.ExpressionOrdering( - ordering_value_columns=tuple( - order.ascending_over(col.get_name()) for col in order_values - ), - total_ordering_columns=frozenset(col.get_name() for col in order_values), - ) - columns = [table_with_ordering[col] for col in original_column_ids] - hidden_columns = [table_with_ordering[col.get_name()] for col in order_values] - return core.ArrayValue.from_ibis( - session, - table_with_ordering, - columns, - hidden_ordering_columns=hidden_columns, - ordering=ordering, - ) + job_config = bigquery.QueryJobConfig() + job_config.labels["bigframes-api"] = api_name + snapshot_timestamp = list( + bqclient.query( + "SELECT CURRENT_TIMESTAMP() AS `current_timestamp`", + job_config=job_config, + ).result() + )[0][0] + cached_table = (snapshot_timestamp, table) + cache[table_ref] = cached_table + return cached_table diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index d687643c8a3..1d6be3dff8a 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -227,7 +227,6 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) @pytest.mark.parametrize( ( "query_or_table", - "index_cols", "columns", "filters", "max_results", @@ -237,8 +236,7 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) [ pytest.param( "test_table", - ["row_index"], - ["string_col"], + ["row_index", "string_col"], [ (("rowindex", "not in", [0, 6]),), (("string_col", "in", ["Hello, World!", "こんにちは"]),), @@ -261,8 +259,7 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) FROM `test_table` AS t """ ), - ["rowindex"], - ["string_col"], + ["rowindex", "string_col"], [ ("rowindex", "<", 4), ("string_col", "==", "Hello, World!"), @@ -283,7 +280,6 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) ), pytest.param( "test_table", - [], ["col_a", "col_b"], [], None, # max_results @@ -294,7 +290,6 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) pytest.param( "test_table", [], - [], [("date_col", ">", "2022-10-20")], None, # max_results None, # time_travel_timestampe @@ -305,7 +300,6 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) "test_table*", [], [], - [], None, # max_results None, # time_travel_timestampe "SELECT * FROM `test_table*`", @@ -314,7 +308,6 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) pytest.param( "test_table*", [], - [], [("_TABLE_SUFFIX", ">", "2022-10-20")], None, # max_results None, # time_travel_timestampe @@ -325,7 +318,6 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) ) def test_to_query( query_or_table, - index_cols, columns, filters, max_results, @@ -334,9 +326,8 @@ def test_to_query( ): query = io_bq.to_query( query_or_table, - index_cols=index_cols, columns=columns, - filters=filters, + sql_predicate=io_bq.compile_filters(filters), max_results=max_results, time_travel_timestamp=time_travel_timestamp, ) @@ -356,9 +347,8 @@ def test_to_query_fails_with_bad_filters(filters, expected_message): with pytest.raises(ValueError, match=re.escape(expected_message)): io_bq.to_query( "test_table", - index_cols=(), columns=(), - filters=filters, + sql_predicate=io_bq.compile_filters(filters), max_results=None, time_travel_timestamp=None, ) diff --git a/tests/unit/session/test_read_gbq_table.py b/tests/unit/session/test_read_gbq_table.py index 52c86cd1e43..6933957e53d 100644 --- a/tests/unit/session/test_read_gbq_table.py +++ b/tests/unit/session/test_read_gbq_table.py @@ -14,11 +14,9 @@ """Unit tests for read_gbq_table helper functions.""" -import datetime import unittest.mock as mock import google.cloud.bigquery -import google.cloud.bigquery as bigquery import pytest import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table @@ -26,30 +24,6 @@ from .. import resources -def test_get_ibis_time_travel_table_doesnt_timetravel_anonymous_datasets(): - bqsession = resources.create_bigquery_session() - - table_ref = bigquery.TableReference.from_string( - "my-test-project._e8166e0cdb.anonbb92cd" - ) - - table_expression = bf_read_gbq_table.get_ibis_time_travel_table( - bqsession.ibis_client, - table_ref, - index_cols=(), - columns=(), - filters=(), - time_travel_timestamp=datetime.datetime.now(datetime.timezone.utc), - ) - sql = table_expression.compile() - - # Anonymous query results tables don't support time travel. - assert "SYSTEM_TIME" not in sql - - # Need fully-qualified table name. - assert "my-test-project" in sql - - @pytest.mark.parametrize( ("index_cols", "primary_keys", "values_distinct", "expected"), ( diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 4f5daebc879..31029abd67f 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -35,6 +35,14 @@ "datasetId": "my_dataset", "tableId": "my_table", } +SCHEMA = { + "fields": [ + {"name": "col1", "type": "INTEGER"}, + {"name": "col2", "type": "INTEGER"}, + {"name": "col3", "type": "INTEGER"}, + {"name": "col4", "type": "INTEGER"}, + ] +} CLUSTERED_OR_PARTITIONED_TABLES = [ pytest.param( google.cloud.bigquery.Table.from_api_repr( @@ -43,6 +51,7 @@ "clustering": { "fields": ["col1", "col2"], }, + "schema": SCHEMA, }, ), id="clustered", @@ -59,6 +68,7 @@ "interval": 1, }, }, + "schema": SCHEMA, }, ), id="range-partitioned", @@ -71,6 +81,7 @@ "type": "MONTH", "field": "col1", }, + "schema": SCHEMA, }, ), id="time-partitioned", @@ -86,6 +97,7 @@ "type": "MONTH", "field": "col1", }, + "schema": SCHEMA, }, ), id="time-partitioned-and-clustered", @@ -163,22 +175,18 @@ def test_read_gbq_cached_table(): google.cloud.bigquery.DatasetReference("my-project", "my_dataset"), "my_table", ) - table = google.cloud.bigquery.Table(table_ref) + table = google.cloud.bigquery.Table( + table_ref, (google.cloud.bigquery.SchemaField("col", "INTEGER"),) + ) + table._properties["location"] = session._location + table._properties["numRows"] = "1000000000" table._properties["location"] = session._location session._df_snapshot[table_ref] = ( datetime.datetime(1999, 1, 2, 3, 4, 5, 678901, tzinfo=datetime.timezone.utc), table, ) - def get_table_mock(table_ref): - table = google.cloud.bigquery.Table( - table_ref, (google.cloud.bigquery.SchemaField("col", "INTEGER"),) - ) - table._properties["numRows"] = "1000000000" - table._properties["location"] = session._location - return table - - session.bqclient.get_table = get_table_mock + session.bqclient.get_table.return_value = table session.bqclient.query_and_wait.return_value = ( {"total_count": 3, "distinct_count": 2}, ) @@ -373,7 +381,7 @@ def query_mock(query, *args, **kwargs): return session_query_mock(query, *args, **kwargs) - session.bqclient.query = query_mock + session.bqclient.query_and_wait = query_mock def get_table_mock(table_ref): table = google.cloud.bigquery.Table( From f12c90611adb4741069ec32840ebbf2aea83a9f3 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 30 May 2024 10:31:02 -0700 Subject: [PATCH 605/921] feat: adds bigframes.bigquery.array_to_string to convert array elements to delimited strings (#731) --- bigframes/bigquery/__init__.py | 35 ++++++++++++++++++-- bigframes/core/compile/scalar_op_compiler.py | 6 ++++ bigframes/dtypes.py | 8 +++++ bigframes/operations/__init__.py | 13 ++++++++ tests/system/small/bigquery/test_array.py | 13 ++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 6c9c04dca72..5808aa28bfb 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -57,8 +57,7 @@ def array_length(series: series.Series) -> series.Series: dtype: Int64 Args: - series (bigframes.series.Series): - A Series with array columns. + series (bigframes.series.Series): A Series with array columns. Returns: bigframes.series.Series: A Series of integer values indicating @@ -104,7 +103,7 @@ def array_agg( Args: obj (groupby.SeriesGroupBy | groupby.DataFrameGroupBy): - A GroupBy object to be applied the function. + A GroupBy object to be applied the function. Returns: bigframes.series.Series | bigframes.dataframe.DataFrame: A Series or @@ -119,3 +118,33 @@ def array_agg( raise ValueError( f"Unsupported type {type(obj)} to apply `array_agg` function. {constants.FEEDBACK_LINK}" ) + + +def array_to_string(series: series.Series, delimiter: str) -> series.Series: + """Converts array elements within a Series into delimited strings. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([["H", "i", "!"], ["Hello", "World"], np.nan, [], ["Hi"]]) + >>> bbq.array_to_string(s, delimiter=", ") + 0 H, i, ! + 1 Hello, World + 2 + 3 + 4 Hi + dtype: string + + Args: + series (bigframes.series.Series): A Series containing arrays. + delimiter (str): The string used to separate array elements. + + Returns: + bigframes.series.Series: A Series containing delimited strings. + + """ + return series._apply_unary_op(ops.ArrayToStringOp(delimiter=delimiter)) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index e1b497d0dd8..000c4a4c091 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -885,6 +885,12 @@ def map_op_impl(x: ibis_types.Value, op: ops.MapOp): return case.else_(x).end() +# Array Ops +@scalar_op_compiler.register_unary_op(ops.ArrayToStringOp, pass_op=True) +def array_to_string_op_impl(x: ibis_types.Value, op: ops.ArrayToStringOp): + return typing.cast(ibis_types.ArrayValue, x).join(op.delimiter) + + ### Binary Ops def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): """Wraps a binary operator to generate nulls of the expected type if either input is a null scalar.""" diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 0c32a81404d..3df67ed9e40 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -134,6 +134,14 @@ def is_array_like(type: ExpressionType) -> bool: ) +def is_array_string_like(type: ExpressionType) -> bool: + return ( + isinstance(type, pd.ArrowDtype) + and isinstance(type.pyarrow_dtype, pa.ListType) + and pa.types.is_string(type.pyarrow_dtype.value_type) + ) + + def is_struct_like(type: ExpressionType) -> bool: return isinstance(type, pd.ArrowDtype) and isinstance( type.pyarrow_dtype, pa.StructType diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index c1854b1b61b..42f83913ee5 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -580,6 +580,19 @@ def output_type(self, *input_types): return input_types[0] +## Array Ops +@dataclasses.dataclass(frozen=True) +class ArrayToStringOp(UnaryOp): + name: typing.ClassVar[str] = "array_to_string" + delimiter: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_array_string_like(input_type): + raise TypeError("Input type must be an array of string type.") + return dtypes.STRING_DTYPE + + # Binary Ops fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COERCE) maximum_op = create_binary_op(name="maximum", type_signature=op_typing.COERCE) diff --git a/tests/system/small/bigquery/test_array.py b/tests/system/small/bigquery/test_array.py index 0664c31a3c4..d6823a3a54e 100644 --- a/tests/system/small/bigquery/test_array.py +++ b/tests/system/small/bigquery/test_array.py @@ -139,3 +139,16 @@ def test_array_agg_matches_after_explode(): result.to_pandas(), # type: ignore df.to_pandas(), ) + + +@pytest.mark.parametrize( + ("data"), + [ + pytest.param([[1, 2], [3, 4], [5]], id="int_array"), + pytest.param(["hello", "world"], id="string"), + ], +) +def test_array_to_string_w_type_checks(data): + series = bpd.Series(data) + with pytest.raises(TypeError): + bbq.array_to_string(series, delimiter=", ") From e5a299271e3bcf94c66fb6ef70393071c1b7dc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 30 May 2024 15:20:10 -0500 Subject: [PATCH 606/921] feat: support type annotations with `bpd.remote_function()` and `axis=1` (a preview feature) (#730) * feat: support type annotations with `bpd.remote_function()` and `axis=1` (a preview feature) * add missing parentheses --- bigframes/functions/remote_function.py | 10 ++++- tests/system/small/test_remote_function.py | 2 +- tests/unit/resources.py | 4 ++ tests/unit/test_remote_function.py | 38 +++++++++++++++++++ .../bigframes_vendored/pandas/core/series.py | 2 +- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index fb4e3f2f365..7be252406c3 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -14,6 +14,7 @@ from __future__ import annotations +import collections.abc import hashlib import inspect import logging @@ -1043,6 +1044,8 @@ def wrapper(func): "Types are required to use @remote_function." ) input_types.append(param_type) + elif not isinstance(input_types, collections.abc.Sequence): + input_types = [input_types] if output_type is None: if (output_type := signature.return_annotation) is inspect.Signature.empty: @@ -1055,9 +1058,12 @@ def wrapper(func): # The function will actually be receiving a pandas Series, but allow both # BigQuery DataFrames and pandas object types for compatibility. is_row_processor = False - if input_types == bigframes.series.Series or input_types == pandas.Series: + if len(input_types) == 1 and ( + (input_type := input_types[0]) == bigframes.series.Series + or input_type == pandas.Series + ): warnings.warn( - "input_types=Series scenario is in preview.", + "input_types=Series is in preview.", stacklevel=1, category=bigframes.exceptions.PreviewWarning, ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 096a268441c..d2ee4411f42 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -727,7 +727,7 @@ def add_ints(row): with pytest.warns( bigframes.exceptions.PreviewWarning, - match="input_types=Series scenario is in preview.", + match="input_types=Series is in preview.", ): add_ints_remote = session.remote_function(bigframes.series.Series, int)( add_ints diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 4d7998903c7..84699459e64 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -23,6 +23,7 @@ import pytest import bigframes +import bigframes.clients import bigframes.core as core import bigframes.core.ordering import bigframes.dataframe @@ -97,6 +98,9 @@ def query_mock(query, *args, **kwargs): bqoptions = bigframes.BigQueryOptions(credentials=credentials, location=location) session = bigframes.Session(context=bqoptions, clients_provider=clients_provider) + session._bq_connection_manager = mock.create_autospec( + bigframes.clients.BqConnectionManager, instance=True + ) return session diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py index ae9ab296c5f..1bd3f3b14f1 100644 --- a/tests/unit/test_remote_function.py +++ b/tests/unit/test_remote_function.py @@ -12,15 +12,53 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes from ibis.expr import datatypes as ibis_types +import pandas import pytest import bigframes.dtypes import bigframes.functions.remote_function +import bigframes.series from tests.unit import resources +@pytest.mark.parametrize( + "series_type", + ( + pytest.param( + pandas.Series, + id="pandas.Series", + ), + pytest.param( + bigframes.series.Series, + id="bigframes.series.Series", + ), + ), +) +def test_series_input_types_to_str(series_type): + """Check that is_row_processor=True uses str as the input type to serialize a row.""" + session = resources.create_bigquery_session() + remote_function_decorator = bigframes.functions.remote_function.remote_function( + session=session + ) + + with pytest.warns( + bigframes.exceptions.PreviewWarning, + match=re.escape("input_types=Series is in preview."), + ): + + @remote_function_decorator + def axis_1_function(myparam: series_type) -> str: # type: ignore + return "Hello, " + myparam["str_col"] + "!" # type: ignore + + # Still works as a normal function. + assert axis_1_function(pandas.Series({"str_col": "World"})) == "Hello, World!" + assert axis_1_function.ibis_node is not None + + def test_supported_types_correspond(): # The same types should be representable by the supported Python and BigQuery types. ibis_types_from_python = { diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index 6a7a815ed98..a430c3375fc 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3969,7 +3969,7 @@ def map( It also accepts a remote function: - >>> @bpd.remote_function + >>> @bpd.remote_function() ... def my_mapper(val: str) -> str: ... vowels = ["a", "e", "i", "o", "u"] ... if val: From 56cbd3b6f17c5ac22572e872b270ac7e3636675a Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 30 May 2024 15:55:58 -0700 Subject: [PATCH 607/921] feat: add GeminiText 1.5 Preview models (#737) * feat: add gemini 1.5 preview models * tests --- bigframes/ml/llm.py | 41 +++++++++++++++++++++++++------ bigframes/ml/loader.py | 2 ++ tests/system/small/ml/conftest.py | 5 ---- tests/system/small/ml/test_llm.py | 40 ++++++++++++++++++++++++------ 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 77dc1d2b0fd..7fa0e236eb7 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -46,6 +46,14 @@ ) _GEMINI_PRO_ENDPOINT = "gemini-pro" +_GEMINI_1P5_PRO_PREVIEW_ENDPOINT = "gemini-1.5-pro-preview-0514" +_GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT = "gemini-1.5-flash-preview-0514" +_GEMINI_ENDPOINTS = ( + _GEMINI_PRO_ENDPOINT, + _GEMINI_1P5_PRO_PREVIEW_ENDPOINT, + _GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT, +) + _ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" _ML_EMBED_TEXT_STATUS = "ml_embed_text_status" @@ -547,13 +555,16 @@ def to_gbq( class GeminiTextGenerator(base.BaseEstimator): """Gemini text generator LLM model. - .. note:: - This product or feature 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 - (https://cloud.google.com/products#product-launch-stages). - 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" and "gemini-1.5-flash-preview-0514". Default to "gemini-pro". + + .. note:: + "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 + (https://cloud.google.com/products#product-launch-stages). + session (bigframes.Session or None): BQ session to create the model. If None, use the global default session. connection_name (str or None): @@ -565,9 +576,13 @@ class GeminiTextGenerator(base.BaseEstimator): def __init__( self, *, + model_name: Literal[ + "gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514" + ] = "gemini-pro", session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, ): + self.model_name = model_name self.session = session or bpd.get_global_session() self._bq_connection_manager = self.session.bqconnectionmanager @@ -601,7 +616,12 @@ def _create_bqml_model(self): iam_role="aiplatform.user", ) - options = {"endpoint": _GEMINI_PRO_ENDPOINT} + if self.model_name not in _GEMINI_ENDPOINTS: + raise ValueError( + f"Model name {self.model_name} is not supported. We only support {', '.join(_GEMINI_ENDPOINTS)}." + ) + + options = {"endpoint": self.model_name} return self._bqml_model_factory.create_remote_model( session=self.session, connection_name=self.connection_name, options=options @@ -613,12 +633,17 @@ def _from_bq( ) -> GeminiTextGenerator: assert bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" assert "remoteModelInfo" in bq_model._properties + assert "endpoint" in bq_model._properties["remoteModelInfo"] assert "connection" in bq_model._properties["remoteModelInfo"] # Parse the remote model endpoint + bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] model_connection = bq_model._properties["remoteModelInfo"]["connection"] + model_endpoint = bqml_endpoint.split("/")[-1] - model = cls(session=session, connection_name=model_connection) + model = cls( + model_name=model_endpoint, session=session, connection_name=model_connection + ) model._bqml_model = core.BqmlModel(session, bq_model) return model diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 8ae8d643013..66f207929ad 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -61,6 +61,8 @@ llm._EMBEDDING_GENERATOR_GECKO_ENDPOINT: llm.PaLM2TextEmbeddingGenerator, llm._EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT: llm.PaLM2TextEmbeddingGenerator, llm._GEMINI_PRO_ENDPOINT: llm.GeminiTextGenerator, + llm._GEMINI_1P5_PRO_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, + llm._GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, } ) diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 33351afe451..ee966466870 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -275,11 +275,6 @@ def palm2_embedding_generator_multilingual_model( ) -@pytest.fixture(scope="session") -def gemini_text_generator_model(session, bq_connection) -> llm.GeminiTextGenerator: - return llm.GeminiTextGenerator(session=session, connection_name=bq_connection) - - @pytest.fixture(scope="session") def linear_remote_model_params() -> dict: # Pre-deployed endpoint of linear reg model in Vertex. diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 8a6874b1780..20e8dd0c19e 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -303,10 +303,16 @@ def test_embedding_generator_predict_series_success( assert len(value) == 768 -def test_create_gemini_text_generator_model( - gemini_text_generator_model, dataset_id, bq_connection +@pytest.mark.parametrize( + "model_name", + ("gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514"), +) +def test_create_load_gemini_text_generator_model( + dataset_id, model_name, session, bq_connection ): - # Model creation doesn't return error + gemini_text_generator_model = llm.GeminiTextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) assert gemini_text_generator_model is not None assert gemini_text_generator_model._bqml_model is not None @@ -316,12 +322,25 @@ def test_create_gemini_text_generator_model( ) assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name assert reloaded_model.connection_name == bq_connection - - + assert reloaded_model.model_name == model_name + + +@pytest.mark.parametrize( + "model_name", + ( + "gemini-pro", + "gemini-1.5-pro-preview-0514", + # TODO(garrrettwu): enable when cl/637028077 is in prod. + # "gemini-1.5-flash-preview-0514" + ), +) @pytest.mark.flaky(retries=2) def test_gemini_text_generator_predict_default_params_success( - gemini_text_generator_model, llm_text_df + llm_text_df, model_name, session, bq_connection ): + gemini_text_generator_model = llm.GeminiTextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) df = gemini_text_generator_model.predict(llm_text_df).to_pandas() assert df.shape == (3, 4) assert "ml_generate_text_llm_result" in df.columns @@ -329,10 +348,17 @@ def test_gemini_text_generator_predict_default_params_success( assert all(series.str.len() > 20) +@pytest.mark.parametrize( + "model_name", + ("gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514"), +) @pytest.mark.flaky(retries=2) def test_gemini_text_generator_predict_with_params_success( - gemini_text_generator_model, llm_text_df + llm_text_df, model_name, session, bq_connection ): + gemini_text_generator_model = llm.GeminiTextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) df = gemini_text_generator_model.predict( llm_text_df, temperature=0.5, max_output_tokens=100, top_k=20, top_p=0.5 ).to_pandas() From 25d049c078693466905a19cc0954fafcac6c414c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 31 May 2024 13:35:19 -0500 Subject: [PATCH 608/921] feat: `merge` only generates a default index if both inputs already have an index (#733) * feat: `merge` only generates a default index if both inputs already have an index * add tests for merge with null index --- bigframes/_config/__init__.py | 6 +- bigframes/core/blocks.py | 22 +- bigframes/exceptions.py | 4 + notebooks/dataframes/index_col_null.ipynb | 1409 +++++++++++++++++++++ tests/system/conftest.py | 2 +- tests/system/small/test_empty_index.py | 227 ---- tests/system/small/test_null_index.py | 288 +++++ 7 files changed, 1723 insertions(+), 235 deletions(-) create mode 100644 notebooks/dataframes/index_col_null.ipynb delete mode 100644 tests/system/small/test_empty_index.py create mode 100644 tests/system/small/test_null_index.py diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index bf33420e60e..4729532e98f 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -61,10 +61,12 @@ def _init_bigquery_thread_local(self): @property def bigquery(self) -> bigquery_options.BigQueryOptions: """Options to use with the BigQuery engine.""" - if self._local.bigquery_options is not None: + if ( + bigquery_options := getattr(self._local, "bigquery_options", None) + ) is not None: # The only way we can get here is if someone called # _init_bigquery_thread_local. - return self._local.bigquery_options + return bigquery_options return self._bigquery_options diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 010eb96f751..9c567555f07 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -124,7 +124,7 @@ def __init__( if len(index_columns) == 0: warnings.warn( "Creating object with Null Index. Null Index is a preview feature.", - category=bigframes.exceptions.PreviewWarning, + category=bigframes.exceptions.NullIndexPreviewWarning, ) self._index_columns = tuple(index_columns) # Index labels don't need complicated hierarchical access so can store as tuple @@ -1930,10 +1930,22 @@ def merge( coalesce_labels=matching_join_labels, suffixes=suffixes, ) - # Constructs default index - offset_index_id = guid.generate_guid() - expr = joined_expr.promote_offsets(offset_index_id) - return Block(expr, index_columns=[offset_index_id], column_labels=labels) + + # Construct a default index only if this object and the other both have + # indexes. In other words, joining anything to a NULL index object + # keeps everything as a NULL index. + # + # This keeps us from generating an index if the user joins a large + # BigQuery table against small local data, for example. + if len(self._index_columns) > 0 and len(other._index_columns) > 0: + offset_index_id = guid.generate_guid() + expr = joined_expr.promote_offsets(offset_index_id) + index_columns = [offset_index_id] + else: + expr = joined_expr + index_columns = [] + + return Block(expr, index_columns=index_columns, column_labels=labels) def join( self, diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 1162217fc1c..bae239b6dab 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -39,6 +39,10 @@ class PreviewWarning(Warning): """The feature is in preview.""" +class NullIndexPreviewWarning(PreviewWarning): + """Null index feature is in preview.""" + + class NullIndexError(ValueError): """Object has no index.""" diff --git a/notebooks/dataframes/index_col_null.ipynb b/notebooks/dataframes/index_col_null.ipynb new file mode 100644 index 00000000000..de373050fe2 --- /dev/null +++ b/notebooks/dataframes/index_col_null.ipynb @@ -0,0 +1,1409 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "eeec3428", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "47439dbd-4e54-4954-8b16-edc4bcd4f855", + "metadata": {}, + "source": [ + "# Operations with an \"NULL index\" DataFrame\n", + "\n", + "**Note**: This notebook describes a feature that is currently in [preview](https://cloud.google.com/blog/products/gcp/google-cloud-gets-simplified-product-launch-stages). There may be breaking changes to the functionality when using \"NULL index\" objects.\n", + "\n", + "Use the \"NULL\" index for more efficient query generation, but\n", + "some pandas-compatible methods may not be possible without an index." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "96757c59-fc22-420e-a42f-c6cb956110ec", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "import bigframes.enums\n", + "import bigframes.exceptions\n", + "import bigframes.pandas as bpd\n", + "\n", + "# Explicitly opt-in to the NULL index preview feature.\n", + "warnings.simplefilter(\n", + " \"ignore\",\n", + " bigframes.exceptions.NullIndexPreviewWarning,\n", + ")\n", + "\n", + "df = bpd.read_gbq(\n", + " \"bigquery-public-data.baseball.schedules\",\n", + " index_col=bigframes.enums.DefaultIndexKind.NULL,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d15688e1", + "metadata": {}, + "source": [ + "Use `peek()` to view an arbitrary selection of rows from the DataFrame. This is much more efficient than `head()`, which requires a total ordering for determinism." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c93949fb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 1b8726ce-c4ea-47fe-a47c-d6fae50d8fb0 is DONE. 582.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdgameNumberseasonIdyeartypedayNightdurationduration_minuteshomeTeamIdhomeTeamNameawayTeamIdawayTeamNamestartTimeattendancestatuscreated
0e14b6493-9e7f-404f-840a-8a680cc364bf1565de4be-dc80-4849-a7e1-54bc79156cc82016REGD3:0718703556285-bdbb-4576-a06d-42f71f46ddc5Marlins55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-26 17:10:00+00:0027318closed2016-10-06 06:25:15+00:00
11f32b347-cbcb-4c31-a145-0e685306d1681565de4be-dc80-4849-a7e1-54bc79156cc82016REGD3:0918903556285-bdbb-4576-a06d-42f71f46ddc5Marlins55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-25 20:10:00+00:0029457closed2016-10-06 06:25:15+00:00
20c2292d1-7398-48be-bf8e-b41dad5e1a431565de4be-dc80-4849-a7e1-54bc79156cc82016REGD2:4516512079497-e414-450a-8bf2-29f91de646bfBraves55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-11 20:10:00+00:0043114closed2016-10-06 06:25:15+00:00
38fbec734-a15a-42ab-8d51-60790de7750b1565de4be-dc80-4849-a7e1-54bc79156cc82016REGD3:4222212079497-e414-450a-8bf2-29f91de646bfBraves55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-12 17:35:00+00:0031625closed2016-10-06 06:25:15+00:00
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd1565de4be-dc80-4849-a7e1-54bc79156cc82016REGD2:441642142e1ba-3b40-445c-b8bb-f1f8b1054220Phillies55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-08 17:05:00+00:0028650closed2016-10-06 06:25:15+00:00
\n", + "
" + ], + "text/plain": [ + " gameId gameNumber \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 1 \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 1 \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 1 \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 1 \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 1 \n", + "\n", + " seasonId year type dayNight duration \\\n", + "0 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG D 3:07 \n", + "1 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG D 3:09 \n", + "2 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG D 2:45 \n", + "3 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG D 3:42 \n", + "4 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG D 2:44 \n", + "\n", + " duration_minutes homeTeamId homeTeamName \\\n", + "0 187 03556285-bdbb-4576-a06d-42f71f46ddc5 Marlins \n", + "1 189 03556285-bdbb-4576-a06d-42f71f46ddc5 Marlins \n", + "2 165 12079497-e414-450a-8bf2-29f91de646bf Braves \n", + "3 222 12079497-e414-450a-8bf2-29f91de646bf Braves \n", + "4 164 2142e1ba-3b40-445c-b8bb-f1f8b1054220 Phillies \n", + "\n", + " awayTeamId awayTeamName \\\n", + "0 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "1 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "2 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "3 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "4 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "\n", + " startTime attendance status created \n", + "0 2016-06-26 17:10:00+00:00 27318 closed 2016-10-06 06:25:15+00:00 \n", + "1 2016-06-25 20:10:00+00:00 29457 closed 2016-10-06 06:25:15+00:00 \n", + "2 2016-06-11 20:10:00+00:00 43114 closed 2016-10-06 06:25:15+00:00 \n", + "3 2016-06-12 17:35:00+00:00 31625 closed 2016-10-06 06:25:15+00:00 \n", + "4 2016-06-08 17:05:00+00:00 28650 closed 2016-10-06 06:25:15+00:00 " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.peek()" + ] + }, + { + "cell_type": "markdown", + "id": "78e3d27d", + "metadata": {}, + "source": [ + "# Inspect the properties of the DataFrame\n", + "\n", + "Some properties, such as `dtypes`, can be retrieved without executing a query job." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "38f566c5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "gameId string[pyarrow]\n", + "gameNumber Int64\n", + "seasonId string[pyarrow]\n", + "year Int64\n", + "type string[pyarrow]\n", + "dayNight string[pyarrow]\n", + "duration string[pyarrow]\n", + "duration_minutes Int64\n", + "homeTeamId string[pyarrow]\n", + "homeTeamName string[pyarrow]\n", + "awayTeamId string[pyarrow]\n", + "awayTeamName string[pyarrow]\n", + "startTime timestamp[us, tz=UTC][pyarrow]\n", + "attendance Int64\n", + "status string[pyarrow]\n", + "created timestamp[us, tz=UTC][pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "markdown", + "id": "38a59ecc", + "metadata": {}, + "source": [ + "Other properties, such as `shape` require a query. In this case, `shape` runs a `COUNT(1)` query." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e3b43d37", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 0f85f12c-227c-4001-b851-6e9b9087ab7e is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "(2431, 16)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.shape" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "13861abc-120c-4db6-ad0c-e414b85d3443", + "metadata": {}, + "source": [ + "### Select a subset of the DataFrame\n", + "\n", + "Filter columns by selecting a list of columns from the DataFrame.\n", + "\n", + "**Note**: Even with `index_col=bigframes.enums.DefaultIndexKind.NULL`, it is more efficient to do this selection in `read_gbq` / `read_gbq_table` except in cases where the total ordering ID columns can be pruned." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "05cb36e9-bb75-4f6f-8eb6-e4219df6e1d2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job efa6b4be-cf60-4951-9125-7d77fb6b6b44 is DONE. 174.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdyearhomeTeamNameawayTeamNameduration_minutes
0e14b6493-9e7f-404f-840a-8a680cc364bf2016MarlinsCubs187
11f32b347-cbcb-4c31-a145-0e685306d1682016MarlinsCubs189
20c2292d1-7398-48be-bf8e-b41dad5e1a432016BravesCubs165
38fbec734-a15a-42ab-8d51-60790de7750b2016BravesCubs222
489e514d5-fbf5-4b9d-bdac-6ca45bfd18dd2016PhilliesCubs164
\n", + "
" + ], + "text/plain": [ + " gameId year homeTeamName awayTeamName \\\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf 2016 Marlins Cubs \n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 2016 Marlins Cubs \n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 2016 Braves Cubs \n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b 2016 Braves Cubs \n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd 2016 Phillies Cubs \n", + "\n", + " duration_minutes \n", + "0 187 \n", + "1 189 \n", + "2 165 \n", + "3 222 \n", + "4 164 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "column_filtered = df[[\"gameId\", \"year\", \"homeTeamName\", \"awayTeamName\", \"duration_minutes\"]]\n", + "column_filtered.peek()" + ] + }, + { + "cell_type": "markdown", + "id": "d4d52c41", + "metadata": {}, + "source": [ + "Filter by rows using a boolean Series. This Series must be derived from the DataFrame being filtered so that the NULL index can still align correctly." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a6b8b3ac-1df8-46ff-ac4f-d6e7657fc80c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 0be8e44d-854a-45ca-950b-269280e3de41 is DONE. 582.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdgameNumberseasonIdyeartypedayNightdurationduration_minuteshomeTeamIdhomeTeamNameawayTeamIdawayTeamNamestartTimeattendancestatuscreated
063f14670-c28e-432b-84ee-1a2c6ac295271565de4be-dc80-4849-a7e1-54bc79156cc82016REGN2:4316303556285-bdbb-4576-a06d-42f71f46ddc5Marlins55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-23 23:10:00+00:0025291closed2016-10-06 06:25:15+00:00
1bf4e80d1-3125-44fa-8a89-de93d039d4651565de4be-dc80-4849-a7e1-54bc79156cc82016REGN3:2420403556285-bdbb-4576-a06d-42f71f46ddc5Marlins55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-24 23:10:00+00:0024385closed2016-10-06 06:25:15+00:00
2e8af534c-36ed-4ff9-8511-780825fdd0411565de4be-dc80-4849-a7e1-54bc79156cc82016REGN2:5117112079497-e414-450a-8bf2-29f91de646bfBraves55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-10 23:35:00+00:0030547closed2016-10-06 06:25:15+00:00
3e599c525-ac42-4b54-928d-7ee5fbe67dd91565de4be-dc80-4849-a7e1-54bc79156cc82016REGN2:451652142e1ba-3b40-445c-b8bb-f1f8b1054220Phillies55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-07 23:05:00+00:0027381closed2016-10-06 06:25:15+00:00
4d80ffb65-57a4-42c9-ae1c-2c51d06503361565de4be-dc80-4849-a7e1-54bc79156cc82016REGN3:051852142e1ba-3b40-445c-b8bb-f1f8b1054220Phillies55714da8-fcaf-4574-8443-59bfb511a524Cubs2016-06-06 23:05:00+00:0022162closed2016-10-06 06:25:15+00:00
\n", + "
" + ], + "text/plain": [ + " gameId gameNumber \\\n", + "0 63f14670-c28e-432b-84ee-1a2c6ac29527 1 \n", + "1 bf4e80d1-3125-44fa-8a89-de93d039d465 1 \n", + "2 e8af534c-36ed-4ff9-8511-780825fdd041 1 \n", + "3 e599c525-ac42-4b54-928d-7ee5fbe67dd9 1 \n", + "4 d80ffb65-57a4-42c9-ae1c-2c51d0650336 1 \n", + "\n", + " seasonId year type dayNight duration \\\n", + "0 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG N 2:43 \n", + "1 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG N 3:24 \n", + "2 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG N 2:51 \n", + "3 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG N 2:45 \n", + "4 565de4be-dc80-4849-a7e1-54bc79156cc8 2016 REG N 3:05 \n", + "\n", + " duration_minutes homeTeamId homeTeamName \\\n", + "0 163 03556285-bdbb-4576-a06d-42f71f46ddc5 Marlins \n", + "1 204 03556285-bdbb-4576-a06d-42f71f46ddc5 Marlins \n", + "2 171 12079497-e414-450a-8bf2-29f91de646bf Braves \n", + "3 165 2142e1ba-3b40-445c-b8bb-f1f8b1054220 Phillies \n", + "4 185 2142e1ba-3b40-445c-b8bb-f1f8b1054220 Phillies \n", + "\n", + " awayTeamId awayTeamName \\\n", + "0 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "1 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "2 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "3 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "4 55714da8-fcaf-4574-8443-59bfb511a524 Cubs \n", + "\n", + " startTime attendance status created \n", + "0 2016-06-23 23:10:00+00:00 25291 closed 2016-10-06 06:25:15+00:00 \n", + "1 2016-06-24 23:10:00+00:00 24385 closed 2016-10-06 06:25:15+00:00 \n", + "2 2016-06-10 23:35:00+00:00 30547 closed 2016-10-06 06:25:15+00:00 \n", + "3 2016-06-07 23:05:00+00:00 27381 closed 2016-10-06 06:25:15+00:00 \n", + "4 2016-06-06 23:05:00+00:00 22162 closed 2016-10-06 06:25:15+00:00 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "night_games = df[df['dayNight'] == 'N']\n", + "night_games.peek()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "80e9a2e2-c4c9-4c17-bbd0-06882d7657fe", + "metadata": {}, + "source": [ + "### Join two DataFrames\n", + "\n", + "Even though pandas usually joins by the index, NULL index objects can still be manually joined by a column using the `on` parameter in `merge`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3f09ff32-ef43-4fab-a86b-8868afc34363", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 5d2c69d2-33fe-4513-923b-fd64f4da098b is DONE. 113.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamName
0e14b6493-9e7f-404f-840a-8a680cc364bfMarlins
11f32b347-cbcb-4c31-a145-0e685306d168Marlins
20c2292d1-7398-48be-bf8e-b41dad5e1a43Braves
38fbec734-a15a-42ab-8d51-60790de7750bBraves
489e514d5-fbf5-4b9d-bdac-6ca45bfd18ddPhillies
\n", + "
" + ], + "text/plain": [ + " gameId homeTeamName\n", + "0 e14b6493-9e7f-404f-840a-8a680cc364bf Marlins\n", + "1 1f32b347-cbcb-4c31-a145-0e685306d168 Marlins\n", + "2 0c2292d1-7398-48be-bf8e-b41dad5e1a43 Braves\n", + "3 8fbec734-a15a-42ab-8d51-60790de7750b Braves\n", + "4 89e514d5-fbf5-4b9d-bdac-6ca45bfd18dd Phillies" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df[[\"gameId\", \"homeTeamName\"]]\n", + "df1.peek()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5331d2c8-7912-4d96-8da1-f64b57374df3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job b6b70d6d-a490-44d6-ba74-0ee32b4f0a1a is DONE. 582.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 68acd168-8b42-44f8-8702-99618935991e is DONE. 94 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdawayTeamName
0af72a0b9-65f7-49fb-9b30-d505068bdf6dBrewers
1d60c6036-0ce1-4c90-8dd9-de3b403c92a8Brewers
\n", + "
" + ], + "text/plain": [ + " gameId awayTeamName\n", + "0 af72a0b9-65f7-49fb-9b30-d505068bdf6d Brewers\n", + "1 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Brewers" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df2 = df[[\"gameId\", \"awayTeamName\"]].head(2)\n", + "df2.peek()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a574ad3e-a219-454c-8bb5-c5ed6627f2c6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 0ac171dd-3859-4589-b7ff-59fd81ec3c3a is DONE. 582.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 034f8807-c128-444a-8033-0c95f34b0e32 is DONE. 111 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0af72a0b9-65f7-49fb-9b30-d505068bdf6dRedsBrewers
1d60c6036-0ce1-4c90-8dd9-de3b403c92a8NationalsBrewers
\n", + "
" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds Brewers\n", + "1 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals Brewers" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "merged = df1.merge(df2, on=\"gameId\", how=\"inner\")\n", + "merged.peek()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "288e7a95-a077-46c4-8fe6-802474c01f8b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 30fd5a60-772c-4ef0-a151-5ab390ff4322 is DONE. 582.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 701fa9a8-1ec6-49b9-ac41-228cb34d4c8c is DONE. 114.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0039bb40e-7613-4674-a653-584b93e9b21bAmerican League<NA>
178000e12-2ef3-4246-adc1-c8a4d157631cAngels<NA>
2de5555dc-9228-4f7c-88ae-4451e3ffb980Angels<NA>
3f29a2754-004b-436c-91fe-3d86c0bb17a8Angels<NA>
48e5af008-8a07-4f9a-90cb-336ca4c84c71Angels<NA>
\n", + "
" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 039bb40e-7613-4674-a653-584b93e9b21b American League \n", + "1 78000e12-2ef3-4246-adc1-c8a4d157631c Angels \n", + "2 de5555dc-9228-4f7c-88ae-4451e3ffb980 Angels \n", + "3 f29a2754-004b-436c-91fe-3d86c0bb17a8 Angels \n", + "4 8e5af008-8a07-4f9a-90cb-336ca4c84c71 Angels " + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "merged = df1.merge(df2, on=\"gameId\", how=\"outer\")\n", + "merged.peek()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7ee87a01-2ff5-4021-855d-44b71cf2a225", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job e3d8168c-48e9-4ba9-a916-10259ad9c0ea is DONE. 582.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 82d2a5e4-66a8-4478-92de-57d3f806aa76 is DONE. 114.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0039bb40e-7613-4674-a653-584b93e9b21bAmerican League<NA>
1f6fcd83c-e130-487c-a0cc-d00b2712d08bAngels<NA>
2fe401dd2-089c-4822-8657-4d510d460f38Angels<NA>
3c894bdee-5dda-49f4-87c8-53b9b9bfcd3bAngels<NA>
4bbda59d9-fd52-4bed-bcfb-2ceed4be997cAngels<NA>
\n", + "
" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 039bb40e-7613-4674-a653-584b93e9b21b American League \n", + "1 f6fcd83c-e130-487c-a0cc-d00b2712d08b Angels \n", + "2 fe401dd2-089c-4822-8657-4d510d460f38 Angels \n", + "3 c894bdee-5dda-49f4-87c8-53b9b9bfcd3b Angels \n", + "4 bbda59d9-fd52-4bed-bcfb-2ceed4be997c Angels " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "merged = df1.merge(df2, on=\"gameId\", how=\"left\")\n", + "merged.peek()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "330ed69c-f122-4af9-bf5e-96e309d3fa0c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 518ed511-606a-42b2-a28d-61a601eccfa7 is DONE. 582.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job bc381640-74e0-4885-9c32-87805a49f357 is DONE. 111 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0af72a0b9-65f7-49fb-9b30-d505068bdf6dRedsBrewers
1d60c6036-0ce1-4c90-8dd9-de3b403c92a8NationalsBrewers
\n", + "
" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds Brewers\n", + "1 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals Brewers" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "merged = df1.merge(df2, on=\"gameId\", how=\"right\")\n", + "merged.peek()" + ] + }, + { + "cell_type": "markdown", + "id": "162eede7", + "metadata": {}, + "source": [ + "### Download the result as (in-memory) pandas DataFrame\n", + "\n", + "Use the `ordered=False` argument for more efficient query execution." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ab429fa5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 2d4fbd55-ba6a-46d2-87ae-5da416ad3642 is DONE. 159 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gameIdhomeTeamNameawayTeamName
0d60c6036-0ce1-4c90-8dd9-de3b403c92a8NationalsBrewers
1af72a0b9-65f7-49fb-9b30-d505068bdf6dRedsBrewers
\n", + "
" + ], + "text/plain": [ + " gameId homeTeamName awayTeamName\n", + "0 d60c6036-0ce1-4c90-8dd9-de3b403c92a8 Nationals Brewers\n", + "1 af72a0b9-65f7-49fb-9b30-d505068bdf6d Reds Brewers" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dfp = merged.to_pandas(ordered=False)\n", + "dfp" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "896212ab", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/system/conftest.py b/tests/system/conftest.py index f7fbd5f4b6b..25016930840 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -395,7 +395,7 @@ def scalars_df_index( @pytest.fixture(scope="session") -def scalars_df_empty_index( +def scalars_df_null_index( scalars_table_id: str, session: bigframes.Session ) -> bigframes.dataframe.DataFrame: """DataFrame pointing at test data.""" diff --git a/tests/system/small/test_empty_index.py b/tests/system/small/test_empty_index.py deleted file mode 100644 index 3216264a8be..00000000000 --- a/tests/system/small/test_empty_index.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import pandas as pd -import pytest - -import bigframes.exceptions -import bigframes.pandas as bpd -from tests.system.utils import skip_legacy_pandas - - -def test_empty_index_materialize( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = scalars_df_empty_index.to_pandas() - pd.testing.assert_frame_equal( - bf_result, scalars_pandas_df_default_index, check_index_type=False - ) - - -def test_empty_index_series_repr( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = scalars_df_empty_index["int64_too"].head(5).__repr__() - pd_result = ( - scalars_pandas_df_default_index["int64_too"] - .head(5) - .to_string(dtype=True, index=False, length=False, name=True) - ) - assert bf_result == pd_result - - -def test_empty_index_dataframe_repr( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = scalars_df_empty_index[["int64_too", "int64_col"]].head(5).__repr__() - pd_result = ( - scalars_pandas_df_default_index[["int64_too", "int64_col"]] - .head(5) - .to_string(index=False) - ) - assert bf_result == pd_result + "\n\n[5 rows x 2 columns]" - - -def test_empty_index_reset_index( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = scalars_df_empty_index.reset_index().to_pandas() - pd_result = scalars_pandas_df_default_index.reset_index(drop=True) - pd.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) - - -def test_empty_index_set_index(scalars_df_empty_index, scalars_pandas_df_default_index): - bf_result = scalars_df_empty_index.set_index("int64_col").to_pandas() - pd_result = scalars_pandas_df_default_index.set_index("int64_col") - pd.testing.assert_frame_equal(bf_result, pd_result) - - -def test_empty_index_concat(scalars_df_empty_index, scalars_pandas_df_default_index): - bf_result = bpd.concat( - [scalars_df_empty_index, scalars_df_empty_index], axis=0 - ).to_pandas() - pd_result = pd.concat( - [scalars_pandas_df_default_index, scalars_pandas_df_default_index], axis=0 - ) - pd.testing.assert_frame_equal(bf_result, pd_result.reset_index(drop=True)) - - -def test_empty_index_aggregate(scalars_df_empty_index, scalars_pandas_df_default_index): - bf_result = scalars_df_empty_index.count().to_pandas() - pd_result = scalars_pandas_df_default_index.count() - - pd_result.index = pd_result.index.astype("string[pyarrow]") - - pd.testing.assert_series_equal( - bf_result, pd_result, check_dtype=False, check_index_type=False - ) - - -def test_empty_index_groupby_aggregate( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = scalars_df_empty_index.groupby("int64_col").count().to_pandas() - pd_result = scalars_pandas_df_default_index.groupby("int64_col").count() - - pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) - - -@skip_legacy_pandas -def test_empty_index_analytic(scalars_df_empty_index, scalars_pandas_df_default_index): - bf_result = scalars_df_empty_index["int64_col"].cumsum().to_pandas() - pd_result = scalars_pandas_df_default_index["int64_col"].cumsum() - pd.testing.assert_series_equal( - bf_result, pd_result.reset_index(drop=True), check_dtype=False - ) - - -def test_empty_index_groupby_analytic( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = ( - scalars_df_empty_index.groupby("bool_col")["int64_col"].cummax().to_pandas() - ) - pd_result = scalars_pandas_df_default_index.groupby("bool_col")[ - "int64_col" - ].cummax() - pd.testing.assert_series_equal( - bf_result, pd_result.reset_index(drop=True), check_dtype=False - ) - - -@skip_legacy_pandas -def test_empty_index_stack(scalars_df_empty_index, scalars_pandas_df_default_index): - stacking_cols = ["int64_col", "int64_too"] - bf_result = scalars_df_empty_index[stacking_cols].stack().to_pandas() - pd_result = ( - scalars_pandas_df_default_index[stacking_cols] - .stack(future_stack=True) - .droplevel(level=0, axis=0) - ) - pd_result.index = pd_result.index.astype(bf_result.index.dtype) - pd.testing.assert_series_equal( - bf_result, - pd_result, - check_dtype=False, - ) - - -def test_empty_index_series_self_aligns( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = ( - scalars_df_empty_index["int64_col"] + scalars_df_empty_index["int64_too"] - ) - pd_result = ( - scalars_pandas_df_default_index["int64_col"] - + scalars_pandas_df_default_index["int64_too"] - ) - pd.testing.assert_series_equal( - bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False - ) - - -def test_empty_index_df_self_aligns( - scalars_df_empty_index, scalars_pandas_df_default_index -): - bf_result = ( - scalars_df_empty_index[["int64_col", "float64_col"]] - + scalars_df_empty_index[["int64_col", "float64_col"]] - ) - pd_result = ( - scalars_pandas_df_default_index[["int64_col", "float64_col"]] - + scalars_pandas_df_default_index[["int64_col", "float64_col"]] - ) - pd.testing.assert_frame_equal( - bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False - ) - - -def test_empty_index_setitem(scalars_df_empty_index, scalars_pandas_df_default_index): - bf_result = scalars_df_empty_index.copy() - bf_result["new_col"] = ( - scalars_df_empty_index["int64_col"] + scalars_df_empty_index["float64_col"] - ) - pd_result = scalars_pandas_df_default_index.copy() - pd_result["new_col"] = ( - scalars_pandas_df_default_index["int64_col"] - + scalars_pandas_df_default_index["float64_col"] - ) - pd.testing.assert_frame_equal( - bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False - ) - - -def test_empty_index_df_concat(scalars_df_empty_index, scalars_pandas_df_default_index): - bf_result = bpd.concat([scalars_df_empty_index, scalars_df_empty_index]) - pd_result = pd.concat( - [scalars_pandas_df_default_index, scalars_pandas_df_default_index] - ) - pd.testing.assert_frame_equal( - bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False - ) - - -def test_empty_index_align_error(scalars_df_empty_index): - with pytest.raises(bigframes.exceptions.NullIndexError): - _ = ( - scalars_df_empty_index["int64_col"] - + scalars_df_empty_index["int64_col"].cumsum() - ) - - -def test_empty_index_loc_error(scalars_df_empty_index): - with pytest.raises(bigframes.exceptions.NullIndexError): - scalars_df_empty_index["int64_col"].loc[1] - - -def test_empty_index_at_error(scalars_df_empty_index): - with pytest.raises(bigframes.exceptions.NullIndexError): - scalars_df_empty_index["int64_col"].at[1] - - -def test_empty_index_idxmin_error(scalars_df_empty_index): - with pytest.raises(bigframes.exceptions.NullIndexError): - scalars_df_empty_index[["int64_col", "int64_too"]].idxmin() - - -def test_empty_index_index_property(scalars_df_empty_index): - with pytest.raises(bigframes.exceptions.NullIndexError): - _ = scalars_df_empty_index.index - - -def test_empty_index_transpose(scalars_df_empty_index): - with pytest.raises(bigframes.exceptions.NullIndexError): - _ = scalars_df_empty_index.T diff --git a/tests/system/small/test_null_index.py b/tests/system/small/test_null_index.py new file mode 100644 index 00000000000..27a3d8dffea --- /dev/null +++ b/tests/system/small/test_null_index.py @@ -0,0 +1,288 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pandas as pd +import pytest + +import bigframes.exceptions +import bigframes.pandas as bpd +from tests.system.utils import skip_legacy_pandas + + +def test_null_index_materialize(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = scalars_df_null_index.to_pandas() + pd.testing.assert_frame_equal( + bf_result, scalars_pandas_df_default_index, check_index_type=False + ) + + +def test_null_index_series_repr(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = scalars_df_null_index["int64_too"].head(5).__repr__() + pd_result = ( + scalars_pandas_df_default_index["int64_too"] + .head(5) + .to_string(dtype=True, index=False, length=False, name=True) + ) + assert bf_result == pd_result + + +def test_null_index_dataframe_repr( + scalars_df_null_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_null_index[["int64_too", "int64_col"]].head(5).__repr__() + pd_result = ( + scalars_pandas_df_default_index[["int64_too", "int64_col"]] + .head(5) + .to_string(index=False) + ) + assert bf_result == pd_result + "\n\n[5 rows x 2 columns]" + + +def test_null_index_reset_index(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = scalars_df_null_index.reset_index().to_pandas() + pd_result = scalars_pandas_df_default_index.reset_index(drop=True) + pd.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + +def test_null_index_set_index(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = scalars_df_null_index.set_index("int64_col").to_pandas() + pd_result = scalars_pandas_df_default_index.set_index("int64_col") + pd.testing.assert_frame_equal(bf_result, pd_result) + + +def test_null_index_concat(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = bpd.concat( + [scalars_df_null_index, scalars_df_null_index], axis=0 + ).to_pandas() + pd_result = pd.concat( + [scalars_pandas_df_default_index, scalars_pandas_df_default_index], axis=0 + ) + pd.testing.assert_frame_equal(bf_result, pd_result.reset_index(drop=True)) + + +def test_null_index_aggregate(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = scalars_df_null_index.count().to_pandas() + pd_result = scalars_pandas_df_default_index.count() + + pd_result.index = pd_result.index.astype("string[pyarrow]") + + pd.testing.assert_series_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + +def test_null_index_groupby_aggregate( + scalars_df_null_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_null_index.groupby("int64_col").count().to_pandas() + pd_result = scalars_pandas_df_default_index.groupby("int64_col").count() + + pd.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + + +@skip_legacy_pandas +def test_null_index_analytic(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = scalars_df_null_index["int64_col"].cumsum().to_pandas() + pd_result = scalars_pandas_df_default_index["int64_col"].cumsum() + pd.testing.assert_series_equal( + bf_result, pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_null_index_groupby_analytic( + scalars_df_null_index, scalars_pandas_df_default_index +): + bf_result = ( + scalars_df_null_index.groupby("bool_col")["int64_col"].cummax().to_pandas() + ) + pd_result = scalars_pandas_df_default_index.groupby("bool_col")[ + "int64_col" + ].cummax() + pd.testing.assert_series_equal( + bf_result, pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_null_index_merge_left_null_index_object( + scalars_df_null_index, scalars_df_default_index, scalars_pandas_df_default_index +): + df1 = scalars_df_null_index[scalars_df_null_index["int64_col"] > 0] + df1_pd = scalars_pandas_df_default_index[ + scalars_pandas_df_default_index["int64_col"] > 0 + ] + assert not df1._has_index + df2 = scalars_df_default_index[scalars_df_default_index["int64_col"] <= 55555] + df2_pd = scalars_pandas_df_default_index[ + scalars_pandas_df_default_index["int64_col"] <= 55555 + ] + assert df2._has_index + + got = df1.merge(df2, how="inner", on="bool_col") + expected = df1_pd.merge(df2_pd, how="inner", on="bool_col") + + # Combining any NULL index object should result in a NULL index. + # This keeps us from generating an index if the user joins a large + # BigQuery table against small local data, for example. + assert not got._has_index + assert got.shape == expected.shape + + +def test_null_index_merge_right_null_index_object( + scalars_df_null_index, scalars_df_default_index, scalars_pandas_df_default_index +): + df1 = scalars_df_default_index[scalars_df_default_index["int64_col"] > 0] + df1_pd = scalars_pandas_df_default_index[ + scalars_pandas_df_default_index["int64_col"] > 0 + ] + assert df1._has_index + df2 = scalars_df_null_index[scalars_df_null_index["int64_col"] <= 55555] + df2_pd = scalars_pandas_df_default_index[ + scalars_pandas_df_default_index["int64_col"] <= 55555 + ] + assert not df2._has_index + + got = df1.merge(df2, how="left", on="bool_col") + expected = df1_pd.merge(df2_pd, how="left", on="bool_col") + + # Combining any NULL index object should result in a NULL index. + # This keeps us from generating an index if the user joins a large + # BigQuery table against small local data, for example. + assert not got._has_index + assert got.shape == expected.shape + + +def test_null_index_merge_two_null_index_objects( + scalars_df_null_index, scalars_pandas_df_default_index +): + df1 = scalars_df_null_index[scalars_df_null_index["int64_col"] > 0] + df1_pd = scalars_pandas_df_default_index[ + scalars_pandas_df_default_index["int64_col"] > 0 + ] + assert not df1._has_index + df2 = scalars_df_null_index[scalars_df_null_index["int64_col"] <= 55555] + df2_pd = scalars_pandas_df_default_index[ + scalars_pandas_df_default_index["int64_col"] <= 55555 + ] + assert not df2._has_index + + got = df1.merge(df2, how="outer", on="bool_col") + expected = df1_pd.merge(df2_pd, how="outer", on="bool_col") + + assert not got._has_index + assert got.shape == expected.shape + + +@skip_legacy_pandas +def test_null_index_stack(scalars_df_null_index, scalars_pandas_df_default_index): + stacking_cols = ["int64_col", "int64_too"] + bf_result = scalars_df_null_index[stacking_cols].stack().to_pandas() + pd_result = ( + scalars_pandas_df_default_index[stacking_cols] + .stack(future_stack=True) + .droplevel(level=0, axis=0) + ) + pd_result.index = pd_result.index.astype(bf_result.index.dtype) + pd.testing.assert_series_equal( + bf_result, + pd_result, + check_dtype=False, + ) + + +def test_null_index_series_self_aligns( + scalars_df_null_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_null_index["int64_col"] + scalars_df_null_index["int64_too"] + pd_result = ( + scalars_pandas_df_default_index["int64_col"] + + scalars_pandas_df_default_index["int64_too"] + ) + pd.testing.assert_series_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_null_index_df_self_aligns( + scalars_df_null_index, scalars_pandas_df_default_index +): + bf_result = ( + scalars_df_null_index[["int64_col", "float64_col"]] + + scalars_df_null_index[["int64_col", "float64_col"]] + ) + pd_result = ( + scalars_pandas_df_default_index[["int64_col", "float64_col"]] + + scalars_pandas_df_default_index[["int64_col", "float64_col"]] + ) + pd.testing.assert_frame_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_null_index_setitem(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = scalars_df_null_index.copy() + bf_result["new_col"] = ( + scalars_df_null_index["int64_col"] + scalars_df_null_index["float64_col"] + ) + pd_result = scalars_pandas_df_default_index.copy() + pd_result["new_col"] = ( + scalars_pandas_df_default_index["int64_col"] + + scalars_pandas_df_default_index["float64_col"] + ) + pd.testing.assert_frame_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_null_index_df_concat(scalars_df_null_index, scalars_pandas_df_default_index): + bf_result = bpd.concat([scalars_df_null_index, scalars_df_null_index]) + pd_result = pd.concat( + [scalars_pandas_df_default_index, scalars_pandas_df_default_index] + ) + pd.testing.assert_frame_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + +def test_null_index_align_error(scalars_df_null_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + _ = ( + scalars_df_null_index["int64_col"] + + scalars_df_null_index["int64_col"].cumsum() + ) + + +def test_null_index_loc_error(scalars_df_null_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + scalars_df_null_index["int64_col"].loc[1] + + +def test_null_index_at_error(scalars_df_null_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + scalars_df_null_index["int64_col"].at[1] + + +def test_null_index_idxmin_error(scalars_df_null_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + scalars_df_null_index[["int64_col", "int64_too"]].idxmin() + + +def test_null_index_index_property(scalars_df_null_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + _ = scalars_df_null_index.index + + +def test_null_index_transpose(scalars_df_null_index): + with pytest.raises(bigframes.exceptions.NullIndexError): + _ = scalars_df_null_index.T From 3d39221526df82617a8560fd2ab7ea13bc3c03d9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 31 May 2024 13:28:41 -0700 Subject: [PATCH 609/921] perf: Optimize dataframe-series alignment on axis=1 (#732) * perf: Use transpose cache to align series on axis=1 * refactor df binary op alignment * cleanup assertion, comments --- bigframes/core/blocks.py | 240 +++++++++++++++++++++++++-- bigframes/core/groupby/__init__.py | 2 +- bigframes/dataframe.py | 156 ++++------------- bigframes/operations/base.py | 45 +++-- bigframes/session/__init__.py | 15 +- tests/system/small/test_dataframe.py | 22 ++- 6 files changed, 313 insertions(+), 167 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 9c567555f07..ea063669d51 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -155,7 +155,13 @@ def __init__( self._transpose_cache: Optional[Block] = transpose_cache @classmethod - def from_local(cls, data: pd.DataFrame, session: bigframes.Session) -> Block: + def from_local( + cls, + data: pd.DataFrame, + session: bigframes.Session, + *, + cache_transpose: bool = True, + ) -> Block: # Assumes caller has already converted datatypes to bigframes ones. pd_data = data column_labels = pd_data.columns @@ -169,12 +175,21 @@ def from_local(cls, data: pd.DataFrame, session: bigframes.Session) -> Block: pd_data = pd_data.reset_index(names=index_ids) as_pyarrow = pa.Table.from_pandas(pd_data, preserve_index=False) array_value = core.ArrayValue.from_pyarrow(as_pyarrow, session=session) - return cls( + block = cls( array_value, column_labels=column_labels, index_columns=index_ids, index_labels=index_labels, ) + if cache_transpose: + try: + # this cache will help when aligning on axis=1 + block = block.with_transpose_cache( + cls.from_local(data.T, session, cache_transpose=False) + ) + except Exception: + pass + return block @property def index(self) -> BlockIndexProperties: @@ -724,12 +739,18 @@ def with_column_labels( f"The column labels size `{len(label_list)} ` should equal to the value" + f"columns size: {len(self.value_columns)}." ) - return Block( + block = Block( self._expr, index_columns=self.index_columns, column_labels=label_list, index_labels=self.index.names, ) + singleton_label = len(list(value)) == 1 and list(value)[0] + if singleton_label is not None and self._transpose_cache is not None: + new_cache, label_id = self._transpose_cache.create_constant(singleton_label) + new_cache = new_cache.set_index([label_id]) + block = block.with_transpose_cache(new_cache) + return block def with_transpose_cache(self, transposed: Block): return Block( @@ -1947,6 +1968,153 @@ def merge( return Block(expr, index_columns=index_columns, column_labels=labels) + def _align_both_axes( + self, other: Block, how: str + ) -> Tuple[Block, pd.Index, Sequence[Tuple[ex.Expression, ex.Expression]]]: + # Join rows + aligned_block, (get_column_left, get_column_right) = self.join(other, how=how) + # join columns schema + # indexers will be none for exact match + if self.column_labels.equals(other.column_labels): + columns, lcol_indexer, rcol_indexer = self.column_labels, None, None + else: + columns, lcol_indexer, rcol_indexer = self.column_labels.join( + other.column_labels, how="outer", return_indexers=True + ) + lcol_indexer = ( + lcol_indexer if (lcol_indexer is not None) else range(len(columns)) + ) + rcol_indexer = ( + rcol_indexer if (rcol_indexer is not None) else range(len(columns)) + ) + + left_input_lookup = ( + lambda index: ex.free_var(get_column_left[self.value_columns[index]]) + if index != -1 + else ex.const(None) + ) + righ_input_lookup = ( + lambda index: ex.free_var(get_column_right[other.value_columns[index]]) + if index != -1 + else ex.const(None) + ) + + left_inputs = [left_input_lookup(i) for i in lcol_indexer] + right_inputs = [righ_input_lookup(i) for i in rcol_indexer] + return aligned_block, columns, tuple(zip(left_inputs, right_inputs)) + + def _align_axis_0( + self, other: Block, how: str + ) -> Tuple[Block, pd.Index, Sequence[Tuple[ex.Expression, ex.Expression]]]: + assert len(other.value_columns) == 1 + aligned_block, (get_column_left, get_column_right) = self.join(other, how=how) + + series_column_id = other.value_columns[0] + inputs = tuple( + ( + ex.free_var(get_column_left[col]), + ex.free_var(get_column_right[series_column_id]), + ) + for col in self.value_columns + ) + return aligned_block, self.column_labels, inputs + + def _align_series_block_axis_1( + self, other: Block, how: str + ) -> Tuple[Block, pd.Index, Sequence[Tuple[ex.Expression, ex.Expression]]]: + assert len(other.value_columns) == 1 + if other._transpose_cache is None: + raise ValueError( + "Wrong align method, this approach requires transpose cache" + ) + + # Join rows + aligned_block, (get_column_left, get_column_right) = join_with_single_row( + self, other.transpose() + ) + # join columns schema + # indexers will be none for exact match + if self.column_labels.equals(other.transpose().column_labels): + columns, lcol_indexer, rcol_indexer = self.column_labels, None, None + else: + columns, lcol_indexer, rcol_indexer = self.column_labels.join( + other.transpose().column_labels, how=how, return_indexers=True + ) + lcol_indexer = ( + lcol_indexer if (lcol_indexer is not None) else range(len(columns)) + ) + rcol_indexer = ( + rcol_indexer if (rcol_indexer is not None) else range(len(columns)) + ) + + left_input_lookup = ( + lambda index: ex.free_var(get_column_left[self.value_columns[index]]) + if index != -1 + else ex.const(None) + ) + righ_input_lookup = ( + lambda index: ex.free_var( + get_column_right[other.transpose().value_columns[index]] + ) + if index != -1 + else ex.const(None) + ) + + left_inputs = [left_input_lookup(i) for i in lcol_indexer] + right_inputs = [righ_input_lookup(i) for i in rcol_indexer] + return aligned_block, columns, tuple(zip(left_inputs, right_inputs)) + + def _align_pd_series_axis_1( + self, other: pd.Series, how: str + ) -> Tuple[Block, pd.Index, Sequence[Tuple[ex.Expression, ex.Expression]]]: + if self.column_labels.equals(other.index): + columns, lcol_indexer, rcol_indexer = self.column_labels, None, None + else: + if not (self.column_labels.is_unique and other.index.is_unique): + raise ValueError("Cannot align non-unique indices") + columns, lcol_indexer, rcol_indexer = self.column_labels.join( + other.index, how=how, return_indexers=True + ) + lcol_indexer = ( + lcol_indexer if (lcol_indexer is not None) else range(len(columns)) + ) + rcol_indexer = ( + rcol_indexer if (rcol_indexer is not None) else range(len(columns)) + ) + + left_input_lookup = ( + lambda index: ex.free_var(self.value_columns[index]) + if index != -1 + else ex.const(None) + ) + righ_input_lookup = ( + lambda index: ex.const(other.iloc[index]) if index != -1 else ex.const(None) + ) + + left_inputs = [left_input_lookup(i) for i in lcol_indexer] + right_inputs = [righ_input_lookup(i) for i in rcol_indexer] + return self, columns, tuple(zip(left_inputs, right_inputs)) + + def _apply_binop( + self, + op: ops.BinaryOp, + inputs: Sequence[Tuple[ex.Expression, ex.Expression]], + labels: pd.Index, + reverse: bool = False, + ) -> Block: + block = self + binop_result_ids = [] + for left_input, right_input in inputs: + expr = ( + op.as_expr(right_input, left_input) + if reverse + else op.as_expr(left_input, right_input) + ) + block, result_col_id = block.project_expr(expr) + binop_result_ids.append(result_col_id) + + return block.select_columns(binop_result_ids).with_column_labels(labels) + def join( self, other: Block, @@ -2268,15 +2436,6 @@ def column_ids(self) -> Sequence[str]: """Column(s) to use as row labels.""" return self._block._index_columns - def __repr__(self) -> str: - """Converts an Index to a string.""" - # TODO(swast): Add a timeout here? If the query is taking a long time, - # maybe we just print the job metadata that we have so far? - # TODO(swast): Avoid downloading the whole index by using job - # metadata, like we do with DataFrame. - preview = self.to_pandas() - return repr(preview) - def to_pandas(self) -> pd.Index: """Executes deferred operations and downloads the results.""" if len(self.column_ids) == 0: @@ -2371,6 +2530,61 @@ def join_indexless( ) +def join_with_single_row( + left: Block, + single_row_block: Block, +) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: + """ + Special join case where other is a single row block. + This property is not validated, caller responsible for not passing multi-row block. + Preserves index of the left block, ignoring label of other. + """ + left_expr = left.expr + # ignore index columns by dropping them + right_expr = single_row_block.expr.select_columns(single_row_block.value_columns) + left_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.LEFT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in left_expr.column_ids + ] + right_mappings = [ + join_defs.JoinColumnMapping( + source_table=join_defs.JoinSide.RIGHT, + source_id=id, + destination_id=guid.generate_guid(), + ) + for id in right_expr.column_ids # skip index column + ] + + join_def = join_defs.JoinDefinition( + conditions=(), + mappings=(*left_mappings, *right_mappings), + type="cross", + ) + combined_expr = left_expr.join( + right_expr, + join_def=join_def, + ) + get_column_left = join_def.get_left_mapping() + get_column_right = join_def.get_right_mapping() + # Drop original indices from each side. and used the coalesced combination generated by the join. + index_cols_post_join = [get_column_left[id] for id in left.index_columns] + + block = Block( + combined_expr, + index_columns=index_cols_post_join, + column_labels=left.column_labels.append(single_row_block.column_labels), + index_labels=[left.index.name], + ) + return ( + block, + (get_column_left, get_column_right), + ) + + def join_mono_indexed( left: Block, right: Block, @@ -2558,7 +2772,7 @@ def coalesce_columns( ) -> Tuple[core.ArrayValue, Sequence[str]]: result_ids = [] for left_id, right_id in zip(left_ids, right_ids): - if how == "left" or how == "inner": + if how == "left" or how == "inner" or how == "cross": result_ids.append(left_id) expr = expr.drop_columns([right_id]) elif how == "right": diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 6e3a91cc1c6..ee120635d32 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -560,7 +560,7 @@ def size(self) -> series.Series: by_column_ids=self._by_col_ids, dropna=self._dropna, ) - return series.Series(agg_block, name=self._value_name) + return series.Series(agg_block.with_column_labels([self._value_name])) def skew(self, *args, **kwargs) -> series.Series: block = block_ops.skew(self._block, [self._value_column], self._by_col_ids) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a349ea8f6b7..e404e439ab0 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -714,13 +714,9 @@ def _apply_binop( DataFrame(other), op, how=how, reverse=reverse ) elif utils.get_axis_number(axis) == 0: - bf_series = bigframes.core.convert.to_bf_series( - other, self.index, self._session - ) - return self._apply_series_binop_axis_0(bf_series, op, how, reverse) + return self._apply_series_binop_axis_0(other, op, how, reverse) elif utils.get_axis_number(axis) == 1: - pd_series = bigframes.core.convert.to_pd_series(other, self.columns) - return self._apply_series_binop_axis_1(pd_series, op, how, reverse) + return self._apply_series_binop_axis_1(other, op, how, reverse) raise NotImplementedError( f"binary operation is not implemented on the second operand of type {type(other).__name__}." f"{constants.FEEDBACK_LINK}" @@ -745,89 +741,49 @@ def _apply_scalar_binop( def _apply_series_binop_axis_0( self, - other: bigframes.series.Series, + other, op: ops.BinaryOp, how: str = "outer", reverse: bool = False, ) -> DataFrame: - block, (get_column_left, get_column_right) = self._block.join( - other._block, how=how + bf_series = bigframes.core.convert.to_bf_series( + other, self.index, self._session ) - - series_column_id = other._value_column - series_col = get_column_right[series_column_id] - for column_id, label in zip( - self._block.value_columns, self._block.column_labels - ): - self_col = get_column_left[column_id] - expr = ( - op.as_expr(series_col, self_col) - if reverse - else op.as_expr(self_col, series_col) - ) - block, _ = block.project_expr(expr, label) - block = block.drop_columns([get_column_left[column_id]]) - - block = block.drop_columns([series_col]) - block = block.with_index_labels(self._block.index.names) - return DataFrame(block) + aligned_block, columns, expr_pairs = self._block._align_axis_0( + bf_series._block, how=how + ) + result = aligned_block._apply_binop( + op, inputs=expr_pairs, labels=columns, reverse=reverse + ) + return DataFrame(result) def _apply_series_binop_axis_1( self, - other: pandas.Series, + other, op: ops.BinaryOp, how: str = "outer", reverse: bool = False, ) -> DataFrame: - # Somewhat different alignment than df-df so separate codepath for now. - if self.columns.equals(other.index): - columns, lcol_indexer, rcol_indexer = self.columns, None, None + """Align dataframe with pandas series by inlining series values as literals.""" + # If we already know the transposed schema (from the transpose cache), we don't need to materialize rows from other + # Instead, can fully defer execution (as a cross-join) + if ( + isinstance(other, bigframes.series.Series) + and other._block._transpose_cache is not None + ): + aligned_block, columns, expr_pairs = self._block._align_series_block_axis_1( + other._block, how=how + ) else: - if not (self.columns.is_unique and other.index.is_unique): - raise ValueError("Cannot align non-unique indices") - columns, lcol_indexer, rcol_indexer = self.columns.join( - other.index, how=how, return_indexers=True + # Fallback path, materialize `other` locally + pd_series = bigframes.core.convert.to_pd_series(other, self.columns) + aligned_block, columns, expr_pairs = self._block._align_pd_series_axis_1( + pd_series, how=how ) - - binop_result_ids = [] - - column_indices = zip( - lcol_indexer if (lcol_indexer is not None) else range(len(columns)), - rcol_indexer if (rcol_indexer is not None) else range(len(columns)), + result = aligned_block._apply_binop( + op, inputs=expr_pairs, labels=columns, reverse=reverse ) - - block = self._block - for left_index, right_index in column_indices: - if left_index >= 0 and right_index >= 0: # -1 indices indicate missing - self_col_id = self._block.value_columns[left_index] - other_scalar = other.iloc[right_index] - expr = ( - op.as_expr(ex.const(other_scalar), self_col_id) - if reverse - else op.as_expr(self_col_id, ex.const(other_scalar)) - ) - elif left_index >= 0: - self_col_id = self._block.value_columns[left_index] - expr = ( - op.as_expr(ex.const(None), self_col_id) - if reverse - else op.as_expr(self_col_id, ex.const(None)) - ) - elif right_index >= 0: - other_scalar = other.iloc[right_index] - expr = ( - op.as_expr(ex.const(other_scalar), ex.const(None)) - if reverse - else op.as_expr(ex.const(None), ex.const(other_scalar)) - ) - else: - # Should not be possible - raise ValueError("No right or left index.") - block, result_col_id = block.project_expr(expr) - binop_result_ids.append(result_col_id) - - block = block.select_columns(binop_result_ids) - return DataFrame(block.with_column_labels(columns)) + return DataFrame(result) def _apply_dataframe_binop( self, @@ -836,57 +792,13 @@ def _apply_dataframe_binop( how: str = "outer", reverse: bool = False, ) -> DataFrame: - # Join rows - block, (get_column_left, get_column_right) = self._block.join( + aligned_block, columns, expr_pairs = self._block._align_both_axes( other._block, how=how ) - # join columns schema - # indexers will be none for exact match - if self.columns.equals(other.columns): - columns, lcol_indexer, rcol_indexer = self.columns, None, None - else: - columns, lcol_indexer, rcol_indexer = self.columns.join( - other.columns, how=how, return_indexers=True - ) - - binop_result_ids = [] - - column_indices = zip( - lcol_indexer if (lcol_indexer is not None) else range(len(columns)), - rcol_indexer if (rcol_indexer is not None) else range(len(columns)), + result = aligned_block._apply_binop( + op, inputs=expr_pairs, labels=columns, reverse=reverse ) - - for left_index, right_index in column_indices: - if left_index >= 0 and right_index >= 0: # -1 indices indicate missing - self_col_id = get_column_left[self._block.value_columns[left_index]] - other_col_id = get_column_right[other._block.value_columns[right_index]] - expr = ( - op.as_expr(other_col_id, self_col_id) - if reverse - else op.as_expr(self_col_id, other_col_id) - ) - elif left_index >= 0: - self_col_id = get_column_left[self._block.value_columns[left_index]] - expr = ( - op.as_expr(ex.const(None), self_col_id) - if reverse - else op.as_expr(self_col_id, ex.const(None)) - ) - elif right_index >= 0: - other_col_id = get_column_right[other._block.value_columns[right_index]] - expr = ( - op.as_expr(other_col_id, ex.const(None)) - if reverse - else op.as_expr(ex.const(None), other_col_id) - ) - else: - # Should not be possible - raise ValueError("No right or left index.") - block, result_col_id = block.project_expr(expr) - binop_result_ids.append(result_col_id) - - block = block.select_columns(binop_result_ids).with_column_labels(columns) - return DataFrame(block) + return DataFrame(result) def eq(self, other: typing.Any, axis: str | int = "columns") -> DataFrame: return self._apply_binop(other, ops.eq_op, axis=axis) diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 49ef7f76ee3..f339345971d 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -70,27 +70,35 @@ def __init__( ) block: typing.Optional[blocks.Block] = None + if (name is not None) and not isinstance(name, typing.Hashable): + raise ValueError( + f"BigQuery DataFrames only supports hashable series names. {constants.FEEDBACK_LINK}" + ) if copy is not None and not copy: raise ValueError( f"Series constructor only supports copy=True. {constants.FEEDBACK_LINK}" ) if isinstance(data, blocks.Block): + # Constructing from block is for internal use only - shouldn't use parameters, block encompasses all state assert len(data.value_columns) == 1 assert len(data.column_labels) == 1 assert index is None + assert name is None + assert dtype is None block = data # interpret these cases as both index and data - elif ( - isinstance(data, SeriesMethods) - or isinstance(data, pd.Series) - or pd.api.types.is_dict_like(data) - ): - if isinstance(data, pd.Series): - data = read_pandas_func(data) - elif pd.api.types.is_dict_like(data): - data = read_pandas_func(pd.Series(data, dtype=dtype)) # type: ignore - dtype = None + elif isinstance(data, bigframes.pandas.Series) or pd.api.types.is_dict_like( + data + ): # includes pd.Series + if isinstance(data, bigframes.pandas.Series): + data = data.copy() + if name is not None: + data.name = name + if dtype is not None: + data = data.astype(dtype) + else: # local dict-like data + data = read_pandas_func(pd.Series(data, name=name, dtype=dtype)) # type: ignore data_block = data._block if index is not None: # reindex @@ -103,10 +111,8 @@ def __init__( # list-like data that will get default index elif isinstance(data, indexes.Index) or pd.api.types.is_list_like(data): - data = indexes.Index(data, dtype=dtype, session=session) - dtype = ( - None # set to none as it has already been applied, avoid re-cast later - ) + data = indexes.Index(data, dtype=dtype, name=name, session=session) + # set to none as it has already been applied, avoid re-cast later if data.nlevels != 1: raise NotImplementedError("Cannot interpret multi-index as Series.") # Reset index to promote index columns to value columns, set default index @@ -135,20 +141,9 @@ def __init__( dtype=bigframes.dtypes.INT_DTYPE, ) block, _ = bf_index._block.create_constant(data, dtype) - dtype = None block = block.with_column_labels([name]) assert block is not None - if name: - if not isinstance(name, typing.Hashable): - raise ValueError( - f"BigQuery DataFrames only supports hashable series names. {constants.FEEDBACK_LINK}" - ) - block = block.with_column_labels([name]) - if dtype: - block = block.multi_apply_unary_op( - block.value_columns, ops.AsTypeOp(to_type=dtype) - ) self._block: blocks.Block = block @property diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index c8601c101e5..2919b2d77fb 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -294,6 +294,7 @@ def __init__( # performance logging self._bytes_processed_sum = 0 self._slot_millis_sum = 0 + self._execution_count = 0 @property def bqclient(self): @@ -362,6 +363,10 @@ def _add_slot_millis(self, amount: int): """Increment slot_millis_sum by amount.""" self._slot_millis_sum += amount + def _add_execution(self, amount: int = 1): + """Increment slot_millis_sum by amount.""" + self._execution_count += amount + def __hash__(self): # Stable hash needed to use in expression tree return hash(str(self._anonymous_dataset)) @@ -442,6 +447,7 @@ def _query_to_destination( configuration: dict = {"query": {"useQueryCache": True}}, do_clustering=True, ) -> Tuple[Optional[bigquery.TableReference], bigquery.QueryJob]: + self._add_execution(1) # If a dry_run indicates this is not a query type job, then don't # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. dry_run_config = bigquery.QueryJobConfig() @@ -1035,7 +1041,7 @@ def read_pandas( # Try to handle non-dataframe pandas objects as well if isinstance(pandas_dataframe, pandas.Series): bf_df = self._read_pandas(pandas.DataFrame(pandas_dataframe), "read_pandas") - bf_series = typing.cast(series.Series, bf_df[bf_df.columns[0]]) + bf_series = series.Series(bf_df._block) # wrapping into df can set name to 0 so reset to original object name bf_series.name = pandas_dataframe.name return bf_series @@ -1080,9 +1086,8 @@ def _read_pandas_inline( return None try: - inline_df = dataframe.DataFrame( - blocks.Block.from_local(pandas_dataframe, self) - ) + local_block = blocks.Block.from_local(pandas_dataframe, self) + inline_df = dataframe.DataFrame(local_block) except pa.ArrowInvalid as e: raise pa.ArrowInvalid( f"Could not convert with a BigQuery type: `{e}`. " @@ -1969,6 +1974,8 @@ def _execute( dry_run=False, col_id_overrides: Mapping[str, str] = {}, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + if not dry_run: + self._add_execution(1) sql = self._to_sql( array_value, sorted=sorted, col_id_overrides=col_id_overrides ) # type:ignore diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e0f4793943c..eae25bb027b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2130,14 +2130,12 @@ def test_series_binop_axis_index( (pd.Index([1000, 2000, 3000])), (bf_indexes.Index([1000, 2000, 3000])), (pd.Series((1000, 2000), index=["int64_too", "float64_col"])), - (series.Series((1000, 2000), index=["int64_too", "float64_col"])), ], ids=[ "tuple", "pd_index", "bf_index", "pd_series", - "bf_series", ], ) def test_listlike_binop_axis_1(scalars_dfs, input): @@ -2153,6 +2151,26 @@ def test_listlike_binop_axis_1(scalars_dfs, input): assert_pandas_df_equal(bf_result, pd_result, check_dtype=False) +def test_binop_with_self_aggregate(session, scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + df_columns = ["int64_col", "float64_col", "int64_too"] + + # Ensure that this takes the optimized single-query path by counting executions + execution_count_before = session._execution_count + bf_df = scalars_df[df_columns] + bf_result = (bf_df - bf_df.mean()).to_pandas() + execution_count_after = session._execution_count + + pd_df = scalars_pandas_df[df_columns] + pd_result = pd_df - pd_df.mean() + + executions = execution_count_after - execution_count_before + + assert executions == 1 + assert_pandas_df_equal(bf_result, pd_result, check_dtype=False) + + @pytest.mark.parametrize( ("left_labels", "right_labels"), [ From b5a39283cf3916689a784839eb9a0be04a084395 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:11:09 -0700 Subject: [PATCH 610/921] chore(main): release 1.8.0 (#713) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34ab012fd73..cad061ce05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.8.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.7.0...v1.8.0) (2024-05-31) + + +### Features + +* `merge` only generates a default index if both inputs already have an index ([#733](https://github.com/googleapis/python-bigquery-dataframes/issues/733)) ([25d049c](https://github.com/googleapis/python-bigquery-dataframes/commit/25d049c078693466905a19cc0954fafcac6c414c)) +* Add `+`, `-` as unary ops, `^` binary op ([#724](https://github.com/googleapis/python-bigquery-dataframes/issues/724)) ([968d825](https://github.com/googleapis/python-bigquery-dataframes/commit/968d8257edbfcb6d437c6203c7c0078ba782cfed)) +* Add `GroupBy.size()` to get number of rows in each group ([#479](https://github.com/googleapis/python-bigquery-dataframes/issues/479)) ([1fca588](https://github.com/googleapis/python-bigquery-dataframes/commit/1fca588e4398baa0dae61bdea0d3bff17e3971b5)) +* Add DataFrame `~` operator ([#721](https://github.com/googleapis/python-bigquery-dataframes/issues/721)) ([354abc1](https://github.com/googleapis/python-bigquery-dataframes/commit/354abc17b5bd55d70d47f893cfccd7cd0ac9794a)) +* Add GeminiText 1.5 Preview models ([#737](https://github.com/googleapis/python-bigquery-dataframes/issues/737)) ([56cbd3b](https://github.com/googleapis/python-bigquery-dataframes/commit/56cbd3b6f17c5ac22572e872b270ac7e3636675a)) +* Add slot_millis and add stats to session object ([#725](https://github.com/googleapis/python-bigquery-dataframes/issues/725)) ([72e9583](https://github.com/googleapis/python-bigquery-dataframes/commit/72e95834f8755760f3529d38f340703f3b971f0a)) +* Adds bigframes.bigquery.array_to_string to convert array elements to delimited strings ([#731](https://github.com/googleapis/python-bigquery-dataframes/issues/731)) ([f12c906](https://github.com/googleapis/python-bigquery-dataframes/commit/f12c90611adb4741069ec32840ebbf2aea83a9f3)) +* Allow functions decorated with `bpd.remote_function()` to execute locally ([#704](https://github.com/googleapis/python-bigquery-dataframes/issues/704)) ([d850da6](https://github.com/googleapis/python-bigquery-dataframes/commit/d850da6364b98c4e01120725e1e609ad8f6c1263)) +* Ensure `"bigframes-api"` label is always set on jobs, even if the API is unknown ([#722](https://github.com/googleapis/python-bigquery-dataframes/issues/722)) ([1832778](https://github.com/googleapis/python-bigquery-dataframes/commit/1832778cfc4f29fdab1b22380f03b192eb8aebb9)) +* Support `ml.SimpleImputer` in bigframes ([#708](https://github.com/googleapis/python-bigquery-dataframes/issues/708)) ([4c4415f](https://github.com/googleapis/python-bigquery-dataframes/commit/4c4415fb137e3baedc4b2d77ec146827b003557e)) +* Support type annotations to supply input and output types to `bpd.remote_function()` decorator ([#717](https://github.com/googleapis/python-bigquery-dataframes/issues/717)) ([4a12e3c](https://github.com/googleapis/python-bigquery-dataframes/commit/4a12e3c6d49d78fc2b51d783cc8de5d09e7c9995)) +* Support type annotations with `bpd.remote_function()` and `axis=1` (a preview feature) ([#730](https://github.com/googleapis/python-bigquery-dataframes/issues/730)) ([e5a2992](https://github.com/googleapis/python-bigquery-dataframes/commit/e5a299271e3bcf94c66fb6ef70393071c1b7dc69)) + + +### Bug Fixes + +* Correct index labels in multiple aggregations for DataFrameGroupBy ([#723](https://github.com/googleapis/python-bigquery-dataframes/issues/723)) ([6a78c89](https://github.com/googleapis/python-bigquery-dataframes/commit/6a78c89a3a766b747b03c8a739760db1c79f533f)) +* Fix Null index assign series to column ([#711](https://github.com/googleapis/python-bigquery-dataframes/issues/711)) ([ffb4b57](https://github.com/googleapis/python-bigquery-dataframes/commit/ffb4b5712a1a07c703ea88f66ba3f43dd2f98197)) +* Set `bpd.remote_function()`s `input_types` and `output_types` default to `None` to allow omitting them when type annotations are present ([#729](https://github.com/googleapis/python-bigquery-dataframes/issues/729)) ([0e25a3b](https://github.com/googleapis/python-bigquery-dataframes/commit/0e25a3b3ae704bf75b752c57f613e778af58bac3)) +* Warn and disable time travel for linked datasets ([#712](https://github.com/googleapis/python-bigquery-dataframes/issues/712)) ([085fa9d](https://github.com/googleapis/python-bigquery-dataframes/commit/085fa9d8fe1ea4cd02a3d25d443beaa697e10784)) + + +### Performance Improvements + +* Optimize dataframe-series alignment on axis=1 ([#732](https://github.com/googleapis/python-bigquery-dataframes/issues/732)) ([3d39221](https://github.com/googleapis/python-bigquery-dataframes/commit/3d39221526df82617a8560fd2ab7ea13bc3c03d9)) + + +### Documentation + +* Add examples to DataFrameGroupBy and SeriesGroupBy ([#701](https://github.com/googleapis/python-bigquery-dataframes/issues/701)) ([e7da0f0](https://github.com/googleapis/python-bigquery-dataframes/commit/e7da0f085eb9b9cec06e5de972f07d9c1d545ac7)) + ## [1.7.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.6.0...v1.7.0) (2024-05-20) diff --git a/bigframes/version.py b/bigframes/version.py index da33be63fce..74a30e35b78 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.7.0" +__version__ = "1.8.0" From d93dbafe2bb405c60f7141d9ae4135db4ffdb702 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 3 Jun 2024 14:36:15 -0700 Subject: [PATCH 611/921] fix: Resolve issue with unset thread-local options (#741) --- bigframes/_config/__init__.py | 44 +++++++++++---------- tests/unit/_config/test_threaded_options.py | 41 +++++++++++++++++++ 2 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 tests/unit/_config/test_threaded_options.py diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index 4729532e98f..c9b2a3f95a9 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -17,8 +17,12 @@ DataFrames from this package. """ +from __future__ import annotations + import copy +from dataclasses import dataclass, field import threading +from typing import Optional import bigframes_vendored.pandas._config.config as pandas_config @@ -28,18 +32,27 @@ import bigframes._config.sampling_options as sampling_options +@dataclass +class ThreadLocalConfig(threading.local): + # If unset, global settings will be used + bigquery_options: Optional[bigquery_options.BigQueryOptions] = None + # Note: use default factory instead of default instance so each thread initializes to default values + display_options: display_options.DisplayOptions = field( + default_factory=display_options.DisplayOptions + ) + sampling_options: sampling_options.SamplingOptions = field( + default_factory=sampling_options.SamplingOptions + ) + compute_options: compute_options.ComputeOptions = field( + default_factory=compute_options.ComputeOptions + ) + + class Options: """Global options affecting BigQuery DataFrames behavior.""" def __init__(self): - self._local = threading.local() - - # Initialize these in the property getters to make sure we do have a - # separate instance per thread. - self._local.bigquery_options = None - self._local.display_options = None - self._local.sampling_options = None - self._local.compute_options = None + self._local = ThreadLocalConfig() # BigQuery options are special because they can only be set once per # session, so we need an indicator as to whether we are using the @@ -61,21 +74,16 @@ def _init_bigquery_thread_local(self): @property def bigquery(self) -> bigquery_options.BigQueryOptions: """Options to use with the BigQuery engine.""" - if ( - bigquery_options := getattr(self._local, "bigquery_options", None) - ) is not None: + if self._local.bigquery_options is not None: # The only way we can get here is if someone called # _init_bigquery_thread_local. - return bigquery_options + return self._local.bigquery_options return self._bigquery_options @property def display(self) -> display_options.DisplayOptions: """Options controlling object representation.""" - if self._local.display_options is None: - self._local.display_options = display_options.DisplayOptions() - return self._local.display_options @property @@ -88,17 +96,11 @@ def sampling(self) -> sampling_options.SamplingOptions: matplotlib plotting). This option can be overriden by parameters in specific functions. """ - if self._local.sampling_options is None: - self._local.sampling_options = sampling_options.SamplingOptions() - return self._local.sampling_options @property def compute(self) -> compute_options.ComputeOptions: """Thread-local options controlling object computation.""" - if self._local.compute_options is None: - self._local.compute_options = compute_options.ComputeOptions() - return self._local.compute_options @property diff --git a/tests/unit/_config/test_threaded_options.py b/tests/unit/_config/test_threaded_options.py new file mode 100644 index 00000000000..7fc97a9f72d --- /dev/null +++ b/tests/unit/_config/test_threaded_options.py @@ -0,0 +1,41 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading + +import bigframes._config + + +def test_mutate_options_threaded(): + options = bigframes._config.Options() + options.display.max_rows = 50 + result_dict = {"this_before": options.display.max_rows} + + def mutate_options_threaded(options, result_dict): + result_dict["other_before"] = options.display.max_rows + + options.display.max_rows = 100 + result_dict["other_after"] = options.display.max_rows + + thread = threading.Thread( + target=(lambda: mutate_options_threaded(options, result_dict)) + ) + thread.start() + thread.join(1) + result_dict["this_after"] = options.display.max_rows + + assert result_dict["this_before"] == 50 + assert result_dict["this_after"] == 50 + assert result_dict["other_before"] == 25 + assert result_dict["other_after"] == 100 From 74d447956925f1be4f7eaa7d6a663817d7576268 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Tue, 4 Jun 2024 07:28:31 -0700 Subject: [PATCH 612/921] tests: add benchmark and split nox sessions into multiple kokoro jobs (#734) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tests: add benchmark nox session and split nox sessions into multiple kokoro jobs * remove accidentally added file * Update dataframe.ipynb * update noxfile * revert noxfile * remove benchmark notebook * use regular python files for benchmark scripts * add benchmark script * remove accidentally added files * remove stray line * add to comment * correct filepath * fix filenames --------- Co-authored-by: Tim Sweña (Swast) --- .kokoro/continuous/doctest.cfg | 17 ++++++++ .kokoro/continuous/e2e.cfg | 2 +- .kokoro/continuous/notebook.cfg | 17 ++++++++ .kokoro/load/benchmark.cfg | 17 ++++++++ .kokoro/presubmit/doctest.cfg | 17 ++++++++ .kokoro/presubmit/e2e.cfg | 2 +- .kokoro/presubmit/notebook.cfg | 17 ++++++++ noxfile.py | 61 ++++++++++++++++++++++----- scripts/benchmark/simple_benchmark.py | 27 ++++++++++++ 9 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 .kokoro/continuous/doctest.cfg create mode 100644 .kokoro/continuous/notebook.cfg create mode 100644 .kokoro/load/benchmark.cfg create mode 100644 .kokoro/presubmit/doctest.cfg create mode 100644 .kokoro/presubmit/notebook.cfg create mode 100644 scripts/benchmark/simple_benchmark.py diff --git a/.kokoro/continuous/doctest.cfg b/.kokoro/continuous/doctest.cfg new file mode 100644 index 00000000000..dfdc78782fe --- /dev/null +++ b/.kokoro/continuous/doctest.cfg @@ -0,0 +1,17 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "doctest" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/.kokoro/continuous/e2e.cfg b/.kokoro/continuous/e2e.cfg index 3dbd0b47f06..e049dd30b3a 100644 --- a/.kokoro/continuous/e2e.cfg +++ b/.kokoro/continuous/e2e.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "e2e doctest notebook unit_prerelease system_prerelease system_noextras" + value: "e2e unit_prerelease system_prerelease system_noextras" } env_vars: { diff --git a/.kokoro/continuous/notebook.cfg b/.kokoro/continuous/notebook.cfg new file mode 100644 index 00000000000..94e2a3c686e --- /dev/null +++ b/.kokoro/continuous/notebook.cfg @@ -0,0 +1,17 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "notebook" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/.kokoro/load/benchmark.cfg b/.kokoro/load/benchmark.cfg new file mode 100644 index 00000000000..a489e05bbc7 --- /dev/null +++ b/.kokoro/load/benchmark.cfg @@ -0,0 +1,17 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "benchmark" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/.kokoro/presubmit/doctest.cfg b/.kokoro/presubmit/doctest.cfg new file mode 100644 index 00000000000..dfdc78782fe --- /dev/null +++ b/.kokoro/presubmit/doctest.cfg @@ -0,0 +1,17 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "doctest" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/.kokoro/presubmit/e2e.cfg b/.kokoro/presubmit/e2e.cfg index 3dbd0b47f06..e049dd30b3a 100644 --- a/.kokoro/presubmit/e2e.cfg +++ b/.kokoro/presubmit/e2e.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "e2e doctest notebook unit_prerelease system_prerelease system_noextras" + value: "e2e unit_prerelease system_prerelease system_noextras" } env_vars: { diff --git a/.kokoro/presubmit/notebook.cfg b/.kokoro/presubmit/notebook.cfg new file mode 100644 index 00000000000..94e2a3c686e --- /dev/null +++ b/.kokoro/presubmit/notebook.cfg @@ -0,0 +1,17 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "notebook" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-load-testing" +} + +env_vars: { + key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" + value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" +} diff --git a/noxfile.py b/noxfile.py index 52583bbf1a7..24ad88d650f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -76,6 +76,8 @@ SYSTEM_TEST_EXTRAS: List[str] = ["tests"] SYSTEM_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} +LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" + CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() # Sessions are executed in the order so putting the smaller sessions @@ -748,8 +750,6 @@ def notebook(session: nox.Session): "--nbmake-timeout=900", # 15 minutes ] - logging_name_env_var = "BIGFRAMES_PERFORMANCE_LOG_NAME" - try: # Populate notebook parameters and make a backup so that the notebooks # are runnable. @@ -763,10 +763,10 @@ def notebook(session: nox.Session): # takes an environment variable for performance logging processes = [] for notebook in notebooks: - session.env[logging_name_env_var] = os.path.basename(notebook) process = Process( target=session.run, args=(*pytest_command, notebook), + kwargs={"env": {LOGGING_NAME_ENV_VAR: os.path.basename(notebook)}}, ) process.start() processes.append(process) @@ -788,11 +788,15 @@ def notebook(session: nox.Session): processes = [] for notebook, regions in notebooks_reg.items(): for region in regions: - session.env[logging_name_env_var] = os.path.basename(notebook) process = Process( target=session.run, args=(*pytest_command, notebook), - kwargs={"env": {"BIGQUERY_LOCATION": region}}, + kwargs={ + "env": { + "BIGQUERY_LOCATION": region, + LOGGING_NAME_ENV_VAR: os.path.basename(notebook), + } + }, ) process.start() processes.append(process) @@ -803,24 +807,59 @@ def notebook(session: nox.Session): # when the environment variable is set as it is above, # notebooks output a .bytesprocessed and .slotmillis report # collect those reports and print a summary - _print_performance_report() + _print_performance_report("notebooks/") + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) +def benchmark(session: nox.Session): + session.install("-e", ".[all]") + + benchmark_script_list = list(Path("scripts/benchmark/").glob("*.py")) + + # Run benchmarks in parallel session.run's, since each benchmark + # takes an environment variable for performance logging + processes = [] + for benchmark in benchmark_script_list: + process = Process( + target=session.run, + args=("python", benchmark), + kwargs={ + "env": { + LOGGING_NAME_ENV_VAR: "scripts/benchmark/" + + os.path.basename(benchmark) + } + }, + ) + process.start() + processes.append(process) + + for process in processes: + process.join() + + # when the environment variable is set as it is above, + # notebooks output a .bytesprocessed and .slotmillis report + # collect those reports and print a summary + _print_performance_report("scripts/") -def _print_performance_report(): +def _print_performance_report(path: str): """Add an informational report about http queries, bytes processed, and slot time to the testlog output for purposes of measuring bigquery-related performance changes. + + Looks specifically for output files in subfolders of the + passed path. (*/*.bytesprocessed and */*.slotmillis) """ print("---BIGQUERY USAGE REPORT---") results_dict = {} - for bytes_report in Path("notebooks/").glob("*/*.bytesprocessed"): + for bytes_report in Path(path).glob("*/*.bytesprocessed"): with open(bytes_report, "r") as bytes_file: filename = bytes_report.stem lines = bytes_file.read().splitlines() query_count = len(lines) total_bytes = sum([int(line) for line in lines]) results_dict[filename] = [query_count, total_bytes] - for millis_report in Path("notebooks/").glob("*/*.slotmillis"): + for millis_report in Path(path).glob("*/*.slotmillis"): with open(millis_report, "r") as millis_file: filename = millis_report.stem lines = millis_file.read().splitlines() @@ -830,7 +869,7 @@ def _print_performance_report(): cumulative_queries = 0 cumulative_bytes = 0 cumulative_slot_millis = 0 - for results in results_dict.values(): + for name, results in results_dict.items(): if len(results) != 3: raise IOError( "Mismatch in performance logging output. " @@ -842,7 +881,7 @@ def _print_performance_report(): cumulative_bytes += total_bytes cumulative_slot_millis += total_slot_millis print( - f"{filename} - query count: {query_count}," + f"{name} - query count: {query_count}," f" bytes processed sum: {total_bytes}," f" slot millis sum: {total_slot_millis}" ) diff --git a/scripts/benchmark/simple_benchmark.py b/scripts/benchmark/simple_benchmark.py new file mode 100644 index 00000000000..53b35c52adc --- /dev/null +++ b/scripts/benchmark/simple_benchmark.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.pandas as bpd + +# This is a placeholder benchmark. +# TODO(340278185): Add more data analysis tasks and benchmark files +# like this one. + +print("Performing simple benchmark.") +df = bpd.DataFrame() +df["column_1"] = bpd.Series([i for i in range(100000)]) +df["column_2"] = bpd.Series([i * 2 for i in range(100000)]) +df["column_3"] = df["column_1"] + df["column_2"] +df.__repr__() +bpd.reset_session() From 9aebeccac4528cccb5de68ff70d3909021861733 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 4 Jun 2024 07:30:09 -0700 Subject: [PATCH 613/921] chore: cleanup type errors in bigframes/session/clients.py (#743) --- bigframes/session/clients.py | 44 ++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index e7680d1d35c..85664d8dc8b 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -109,19 +109,27 @@ def __init__( # cloud clients initialized for lazy load self._bqclient = None - self._bqconnectionclient = None - self._bqstoragereadclient = None - self._cloudfunctionsclient = None - self._resourcemanagerclient = None + self._bqconnectionclient: Optional[ + google.cloud.bigquery_connection_v1.ConnectionServiceClient + ] = None + self._bqstoragereadclient: Optional[ + google.cloud.bigquery_storage_v1.BigQueryReadClient + ] = None + self._cloudfunctionsclient: Optional[ + google.cloud.functions_v2.FunctionServiceClient + ] = None + self._resourcemanagerclient: Optional[ + google.cloud.resourcemanager_v3.ProjectsClient + ] = None def _create_bigquery_client(self): bq_options = None if self._use_regional_endpoints: - # TODO(b/340896138): fix type error bq_options = google.api_core.client_options.ClientOptions( api_endpoint=( _BIGQUERY_REGIONAL_ENDPOINT - if self._location.lower() in _REP_SUPPORTED_REGIONS # type: ignore + if self._location is not None + and self._location.lower() in _REP_SUPPORTED_REGIONS else _BIGQUERY_LOCATIONAL_ENDPOINT ).format(location=self._location), ) @@ -159,11 +167,12 @@ def bqconnectionclient(self): bqconnection_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) - # TODO(b/340896138): fix type error - self._bqconnectionclient = google.cloud.bigquery_connection_v1.ConnectionServiceClient( # type: ignore - client_info=bqconnection_info, - client_options=bqconnection_options, - credentials=self._credentials, + self._bqconnectionclient = ( + google.cloud.bigquery_connection_v1.ConnectionServiceClient( + client_info=bqconnection_info, + client_options=bqconnection_options, + credentials=self._credentials, + ) ) return self._bqconnectionclient @@ -173,20 +182,19 @@ def bqstoragereadclient(self): if not self._bqstoragereadclient: bqstorage_options = None if self._use_regional_endpoints: - # TODO(b/340896138): fix type error bqstorage_options = google.api_core.client_options.ClientOptions( api_endpoint=( _BIGQUERYSTORAGE_REGIONAL_ENDPOINT - if self._location.lower() in _REP_SUPPORTED_REGIONS # type: ignore + if self._location is not None + and self._location.lower() in _REP_SUPPORTED_REGIONS else _BIGQUERYSTORAGE_LOCATIONAL_ENDPOINT ).format(location=self._location), ) bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) - # TODO(b/340896138): fix type error self._bqstoragereadclient = ( - google.cloud.bigquery_storage_v1.BigQueryReadClient( # type: ignore + google.cloud.bigquery_storage_v1.BigQueryReadClient( client_info=bqstorage_info, client_options=bqstorage_options, credentials=self._credentials, @@ -201,9 +209,8 @@ def cloudfunctionsclient(self): functions_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) - # TODO(b/340896138): fix type error self._cloudfunctionsclient = ( - google.cloud.functions_v2.FunctionServiceClient( # type: ignore + google.cloud.functions_v2.FunctionServiceClient( client_info=functions_info, credentials=self._credentials, ) @@ -217,9 +224,8 @@ def resourcemanagerclient(self): resourcemanager_info = google.api_core.gapic_v1.client_info.ClientInfo( user_agent=self._application_name ) - # TODO(b/340896138): fix type error self._resourcemanagerclient = ( - google.cloud.resourcemanager_v3.ProjectsClient( # type: ignore + google.cloud.resourcemanager_v3.ProjectsClient( credentials=self._credentials, client_info=resourcemanager_info ) ) From d859c5f685a2bd792b85225827d5a4a59640e656 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:37:21 -0700 Subject: [PATCH 614/921] chore: type issue fix for _matplotlib/core.py (#716) * chore: type issue update for core.py * update format --- bigframes/operations/_matplotlib/core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index 78b3df1f19e..ff8dd86cffe 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -39,8 +39,10 @@ def draw(self) -> None: @property def result(self): - # TODO(b/340896123): fix type error - return self.axes # type: ignore + if hasattr(self, "axes"): + return self.axes + else: + raise AttributeError("Axes not defined") class SamplingPlot(MPLPlot): From efb3f215568acf1a042c24521a6cf3cac5daa2d8 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:42:58 -0700 Subject: [PATCH 615/921] chore: update type for _matplotlib/__init__.py (#718) --- bigframes/operations/_matplotlib/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bigframes/operations/_matplotlib/__init__.py b/bigframes/operations/_matplotlib/__init__.py index f869c1e01df..6ffe71139d8 100644 --- a/bigframes/operations/_matplotlib/__init__.py +++ b/bigframes/operations/_matplotlib/__init__.py @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import typing + import bigframes.operations._matplotlib.core as core import bigframes.operations._matplotlib.hist as hist -PLOT_CLASSES: dict[str, type[core.MPLPlot]] = { +PLOT_TYPES = typing.Union[type[core.SamplingPlot], type[hist.HistPlot]] + +PLOT_CLASSES: dict[str, PLOT_TYPES] = { "hist": hist.HistPlot, "line": core.LinePlot, "area": core.AreaPlot, @@ -24,8 +28,7 @@ def plot(data, kind, **kwargs): - # TODO(b/340896123): fix type error - plot_obj = PLOT_CLASSES[kind](data, **kwargs) # type: ignore + plot_obj = PLOT_CLASSES[kind](data, **kwargs) plot_obj.generate() plot_obj.draw() return plot_obj.result From 0d69ac4ef87e5eb7908e96474521f6d916afb1b1 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:44:21 -0700 Subject: [PATCH 616/921] chore: cleanup type errors in bigframes/operations/__init__.py (#742) --- bigframes/operations/__init__.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 42f83913ee5..c10b7436315 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -161,7 +161,10 @@ def _convert_expr_input( def create_unary_op(name: str, type_signature: op_typing.UnaryTypeSignature) -> UnaryOp: return dataclasses.make_dataclass( name, - [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method)], # type: ignore + [ + ("name", typing.ClassVar[str], name), + ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method), + ], bases=(UnaryOp,), frozen=True, )() @@ -172,7 +175,10 @@ def create_binary_op( ) -> BinaryOp: return dataclasses.make_dataclass( name, - [("name", typing.ClassVar[str], name), ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method)], # type: ignore + [ + ("name", typing.ClassVar[str], name), + ("output_type", typing.ClassVar[typing.Callable], type_signature.as_method), + ], bases=(BinaryOp,), frozen=True, )() @@ -493,8 +499,9 @@ def output_type(self, *input_types): if self.to_type == pa.string(): return dtypes.STRING_DTYPE if isinstance(self.to_type, str): - # TODO(b/340895446): fix type error - return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[self.to_type] # type: ignore + return dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[ + typing.cast(dtypes.DtypeString, self.to_type) + ] return self.to_type @@ -516,8 +523,10 @@ class RemoteFunctionOp(UnaryOp): def output_type(self, *input_types): # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method - # TODO(b/340895446): fix type error - return self.func.output_dtype # type: ignore + if hasattr(self.func, "output_dtype"): + return self.func.output_dtype + else: + raise AttributeError("output_dtype not defined") @dataclasses.dataclass(frozen=True) @@ -644,8 +653,10 @@ class BinaryRemoteFunctionOp(BinaryOp): def output_type(self, *input_types): # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method - # TODO(b/340895446): fix type error - return self.func.output_dtype # type: ignore + if hasattr(self.func, "output_dtype"): + return self.func.output_dtype + else: + raise AttributeError("output_dtype not defined") add_op = AddOp() From ad7d8ac1247ec3b9532dd5375265c36907f50da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 4 Jun 2024 14:00:43 -0500 Subject: [PATCH 617/921] feat: allow functions returned from `bpd.read_gbq_function` to execute outside of `apply` (#706) * feat: allow functions decorated with `@bpd.remote_function` to execute locally * fix read_gbq_function * feat: allow functions returned from `bpd.read_gbq_function` to execute outside of `apply` * fix for rare case where re-deploy exact same function object * feat: support type annotations to supply input and output types to `@remote_function` decorator * make tests robust to cloud function listing failures too * remove unused bigquery_client argument * add test that function can be called directly --- bigframes/functions/remote_function.py | 21 +++++-------- tests/system/small/test_remote_function.py | 35 +++++++++++++++------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 7be252406c3..4223998c53d 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -1146,21 +1146,14 @@ def try_delattr(attr): def read_gbq_function( function_name: str, - session: Optional[Session] = None, - bigquery_client: Optional[bigquery.Client] = None, + *, + session: Session, ): """ Read an existing BigQuery function and prepare it for use in future queries. """ - - # A BigQuery client is required to perform BQ operations - if not bigquery_client and session: - bigquery_client = session.bqclient - if not bigquery_client: - raise ValueError( - "A bigquery client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) + bigquery_client = session.bqclient + ibis_client = session.ibis_client try: routine_ref = get_routine_reference(function_name, bigquery_client, session) @@ -1192,8 +1185,10 @@ def read_gbq_function( # non-standard names for the arguments here. def func(*ignored_args, **ignored_kwargs): f"""Remote function {str(routine_ref)}.""" - # TODO(swast): Construct an ibis client from bigquery_client and - # execute node via a query. + nonlocal node # type: ignore + + expr = node(*ignored_args, **ignored_kwargs) # type: ignore + return ibis_client.execute(expr) # TODO: Move ibis logic to compiler step func.__name__ = routine_ref.routine_id diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index d2ee4411f42..bf3424d0f0c 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -537,12 +537,12 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_read_gbq_function_detects_invalid_function(bigquery_client, dataset_id): +def test_read_gbq_function_detects_invalid_function(session, dataset_id): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) with pytest.raises(ValueError) as e: rf.read_gbq_function( str(dataset_ref.routine("not_a_function")), - bigquery_client=bigquery_client, + session=session, ) assert "Unknown function" in str(e.value) @@ -550,6 +550,7 @@ def test_read_gbq_function_detects_invalid_function(bigquery_client, dataset_id) @pytest.mark.flaky(retries=2, delay=120) def test_read_gbq_function_like_original( + session, bigquery_client, bigqueryconnection_client, cloudfunctions_client, @@ -577,7 +578,7 @@ def square1(x): square2 = rf.read_gbq_function( function_name=square1.bigframes_remote_function, - bigquery_client=bigquery_client, + session=session, ) # The newly-created function (square1) should have a remote function AND a @@ -607,7 +608,14 @@ def square1(x): @pytest.mark.flaky(retries=2, delay=120) -def test_read_gbq_function_reads_udfs(bigquery_client, dataset_id): +def test_read_gbq_function_runs_existing_udf(session, bigquery_client, dataset_id): + func = session.read_gbq_function("bqutil.fn.cw_lower_case_ascii_only") + got = func("AURÉLIE") + assert got == "aurÉlie" + + +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_reads_udfs(session, bigquery_client, dataset_id): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) arg = bigquery.RoutineArgument( name="x", @@ -633,7 +641,8 @@ def test_read_gbq_function_reads_udfs(bigquery_client, dataset_id): # Create the routine in BigQuery and read it back using read_gbq_function. bigquery_client.create_routine(routine, exists_ok=True) square = rf.read_gbq_function( - str(routine.reference), bigquery_client=bigquery_client + str(routine.reference), + session=session, ) # It should point to the named routine and yield the expected results. @@ -658,7 +667,9 @@ def test_read_gbq_function_reads_udfs(bigquery_client, dataset_id): @pytest.mark.flaky(retries=2, delay=120) -def test_read_gbq_function_enforces_explicit_types(bigquery_client, dataset_id): +def test_read_gbq_function_enforces_explicit_types( + session, bigquery_client, dataset_id +): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) typed_arg = bigquery.RoutineArgument( name="x", @@ -702,18 +713,22 @@ def test_read_gbq_function_enforces_explicit_types(bigquery_client, dataset_id): bigquery_client.create_routine(neither_type_specified, exists_ok=True) rf.read_gbq_function( - str(both_types_specified.reference), bigquery_client=bigquery_client + str(both_types_specified.reference), + session=session, ) rf.read_gbq_function( - str(only_return_type_specified.reference), bigquery_client=bigquery_client + str(only_return_type_specified.reference), + session=session, ) with pytest.raises(ValueError): rf.read_gbq_function( - str(only_arg_type_specified.reference), bigquery_client=bigquery_client + str(only_arg_type_specified.reference), + session=session, ) with pytest.raises(ValueError): rf.read_gbq_function( - str(neither_type_specified.reference), bigquery_client=bigquery_client + str(neither_type_specified.reference), + session=session, ) From b2c7d8b28e235c839370818137fba71796c9f02a Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:56:41 -0700 Subject: [PATCH 618/921] feat: support `score()` in GeminiTextGenerator (#740) * feat: support score in GeminiTextGenerator * address comments * reorganize the tests --- bigframes/ml/llm.py | 61 +++++++++++++++++++++++++++++++ tests/system/small/ml/conftest.py | 12 ++++++ tests/system/small/ml/test_llm.py | 46 +++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 7fa0e236eb7..c94a1184e7b 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -732,6 +732,67 @@ def predict( return df + def score( + self, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + task_type: Literal[ + "text_generation", "classification", "summarization", "question_answering" + ] = "text_generation", + ) -> bpd.DataFrame: + """Calculate evaluation metrics of the model. Only "gemini-pro" model is supported for now. + + .. note:: + + This product or feature 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 + (https://cloud.google.com/products#product-launch-stages). + + .. note:: + + Output matches that of the BigQuery ML.EVALUTE function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#remote-model-llm + for the outputs relevant to this model type. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation data, which contains only one column of input_text + that contains the prompt text to use when evaluating the model. + y (bigframes.dataframe.DataFrame or bigframes.series.Series): + A BigQuery DataFrame as evaluation labels, which contains only one column of output_text + that you would expect to be returned by the model. + task_type (str): + The type of the task for LLM model. Default to "text_generation". + Possible values: "text_generation", "classification", "summarization", and "question_answering". + + Returns: + bigframes.dataframe.DataFrame: The DataFrame as evaluation result. + """ + if not self._bqml_model: + raise RuntimeError("A model must be fitted before score") + + # TODO(ashleyxu): Support gemini-1.5 when the rollout is ready. b/344891364. + if self._bqml_model.model_name.startswith("gemini-1.5"): + raise NotImplementedError("Score is not supported for gemini-1.5 model.") + + X, y = utils.convert_to_dataframe(X, y) + + if len(X.columns) != 1 or len(y.columns) != 1: + raise ValueError( + f"Only support one column as input for X and y. {constants.FEEDBACK_LINK}" + ) + + # BQML identified the column by name + X_col_label = cast(blocks.Label, X.columns[0]) + y_col_label = cast(blocks.Label, y.columns[0]) + X = X.rename(columns={X_col_label: "input_text"}) + y = y.rename(columns={y_col_label: "output_text"}) + + input_data = X.join(y, how="outer") + + return self._bqml_model.llm_evaluate(input_data, task_type) + def to_gbq(self, model_name: str, replace: bool = False) -> GeminiTextGenerator: """Save the model to BigQuery. diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index ee966466870..ab75053b0e7 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -171,6 +171,18 @@ def llm_text_pandas_df(): ) +@pytest.fixture(scope="session") +def llm_fine_tune_df_default_index( + session: bigframes.Session, +) -> bigframes.dataframe.DataFrame: + training_table_name = "llm_tuning.emotion_classification_train" + df = session.read_gbq(training_table_name) + prefix = "Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: " + df["prompt"] = prefix + df["text"] + df["label"] = df["label"].astype("string") + return df + + @pytest.fixture(scope="session") def onnx_iris_pandas_df(): """Data matching the iris dataset.""" diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 20e8dd0c19e..36d01e126f9 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -15,6 +15,7 @@ import pytest from bigframes.ml import llm +from tests.system import utils def test_create_text_generator_model( @@ -366,3 +367,48 @@ def test_gemini_text_generator_predict_with_params_success( assert "ml_generate_text_llm_result" in df.columns series = df["ml_generate_text_llm_result"] assert all(series.str.len() > 20) + + +@pytest.mark.flaky(retries=2) +def test_llm_gemini_pro_score(llm_fine_tune_df_default_index): + model = llm.GeminiTextGenerator(model_name="gemini-pro") + + # Check score to ensure the model was fitted + score_result = model.score( + X=llm_fine_tune_df_default_index[["prompt"]], + y=llm_fine_tune_df_default_index[["label"]], + ).to_pandas() + utils.check_pandas_df_schema_and_index( + score_result, + columns=[ + "bleu4_score", + "rouge-l_precision", + "rouge-l_recall", + "rouge-l_f1_score", + "evaluation_status", + ], + index=1, + ) + + +@pytest.mark.flaky(retries=2) +def test_llm_gemini_pro_score_params(llm_fine_tune_df_default_index): + model = llm.GeminiTextGenerator(model_name="gemini-pro") + + # Check score to ensure the model was fitted + score_result = model.score( + X=llm_fine_tune_df_default_index["prompt"], + y=llm_fine_tune_df_default_index["label"], + task_type="classification", + ).to_pandas() + utils.check_pandas_df_schema_and_index( + score_result, + columns=[ + "precision", + "recall", + "f1_score", + "label", + "evaluation_status", + ], + index=6, + ) From 7899749505a75ed89c68e9df64124a153644de96 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:44:08 -0700 Subject: [PATCH 619/921] docs: fix ML.EVALUATE spelling (#749) --- bigframes/ml/decomposition.py | 2 +- bigframes/ml/ensemble.py | 4 ++-- bigframes/ml/forecasting.py | 2 +- bigframes/ml/llm.py | 4 ++-- third_party/bigframes_vendored/sklearn/base.py | 4 ++-- third_party/bigframes_vendored/sklearn/cluster/_kmeans.py | 2 +- third_party/bigframes_vendored/sklearn/decomposition/_pca.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bigframes/ml/decomposition.py b/bigframes/ml/decomposition.py index ad0bce481fc..41dea7617f2 100644 --- a/bigframes/ml/decomposition.py +++ b/bigframes/ml/decomposition.py @@ -190,5 +190,5 @@ def score( if not self._bqml_model: raise RuntimeError("A model must be fitted before score") - # TODO(b/291973741): X param is ignored. Update BQML supports input in ML.EVALUTE. + # TODO(b/291973741): X param is ignored. Update BQML supports input in ML.EVALUATE. return self._bqml_model.evaluate() diff --git a/bigframes/ml/ensemble.py b/bigframes/ml/ensemble.py index 8fc1e221461..0194d768b8f 100644 --- a/bigframes/ml/ensemble.py +++ b/bigframes/ml/ensemble.py @@ -460,7 +460,7 @@ def score( .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#regression_models for the outputs relevant to this model type. @@ -616,7 +616,7 @@ def score( .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#classification_models for the outputs relevant to this model type. diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index a87df61801a..a1ae8435d58 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -313,7 +313,7 @@ def score( .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#time_series_models for the outputs relevant to this model type. diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index c94a1184e7b..f62867cdd57 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -329,7 +329,7 @@ def score( .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#remote-model-llm for the outputs relevant to this model type. @@ -751,7 +751,7 @@ def score( .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#remote-model-llm for the outputs relevant to this model type. diff --git a/third_party/bigframes_vendored/sklearn/base.py b/third_party/bigframes_vendored/sklearn/base.py index 1a151a1119c..57c9e79f8de 100644 --- a/third_party/bigframes_vendored/sklearn/base.py +++ b/third_party/bigframes_vendored/sklearn/base.py @@ -87,7 +87,7 @@ def score(self, X, y): .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#classification_models for the outputs relevant to this model type. @@ -115,7 +115,7 @@ def score(self, X, y): .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#regression_models for the outputs relevant to this model type. diff --git a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py index 386b620f4af..aaf43dbcfe6 100644 --- a/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py +++ b/third_party/bigframes_vendored/sklearn/cluster/_kmeans.py @@ -103,7 +103,7 @@ def score( .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#k-means_models for the outputs relevant to this model type. diff --git a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py index 71e53bf4a99..ae6f0b0561f 100644 --- a/third_party/bigframes_vendored/sklearn/decomposition/_pca.py +++ b/third_party/bigframes_vendored/sklearn/decomposition/_pca.py @@ -52,7 +52,7 @@ def score(self, X=None, y=None): .. note:: - Output matches that of the BigQuery ML.EVALUTE function. + Output matches that of the BigQuery ML.EVALUATE function. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#pca_models for the outputs relevant to this model type. From 39d7013a8a8d2908f20bfe54a7dc8de166323b90 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:08:30 -0700 Subject: [PATCH 620/921] fix: ARIMAPlus loads auto_arima_min_order param (#752) --- tests/system/large/ml/test_forecasting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index 79deb615b1b..438177b1a06 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -127,8 +127,7 @@ def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): assert reloaded_model.horizon == 100 assert reloaded_model.auto_arima is True assert reloaded_model.auto_arima_max_order == 4 - # TODO(garrettwu): now BQML doesn't populate auto_arima_min_order - # assert reloaded_model.auto_arima_min_order == 1 + assert reloaded_model.auto_arima_min_order == 1 assert reloaded_model.data_frequency == "DAILY" assert reloaded_model.holiday_region == "US" assert reloaded_model.clean_spikes_and_dips is False From ea5d367d5ecc6826d30082e75c957af8362c9e61 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:25:51 -0700 Subject: [PATCH 621/921] docs: remove LogisticRegression normal_equation strategy (#753) --- bigframes/ml/linear_model.py | 2 +- tests/system/large/ml/test_linear_model.py | 2 -- .../bigframes_vendored/sklearn/linear_model/_logistic.py | 6 ++++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 32168e9a342..0816ef9b24e 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -195,7 +195,7 @@ def __init__( self, *, optimize_strategy: Literal[ - "auto_strategy", "batch_gradient_descent", "normal_equation" + "auto_strategy", "batch_gradient_descent" ] = "auto_strategy", fit_intercept: bool = True, l1_reg: Optional[float] = None, diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index eaf666fd50a..0cc9fc53538 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -192,8 +192,6 @@ def test_logistic_regression_customized_params_fit_score( f"{dataset_id}.temp_configured_logistic_reg_model" in reloaded_model._bqml_model.model_name ) - # TODO(garrettwu) optimize_strategy isn't logged in BQML - # assert reloaded_model.optimize_strategy == "BATCH_GRADIENT_DESCENT" assert reloaded_model.fit_intercept is False assert reloaded_model.class_weight == "balanced" assert reloaded_model.calculate_p_values is False diff --git a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py index 49198eb9bdc..c52a37018c2 100644 --- a/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py +++ b/third_party/bigframes_vendored/sklearn/linear_model/_logistic.py @@ -26,8 +26,10 @@ class LogisticRegression(LinearClassifierMixin, BaseEstimator): Args: optimize_strategy (str, default "auto_strategy"): The strategy to train logistic regression models. Possible values are - "auto_strategy", "batch_gradient_descent", "normal_equation". Default - to "auto_strategy". + "auto_strategy" and "batch_gradient_descent". The two are equilevant since + "auto_strategy" will fall back to "batch_gradient_descent". The API is kept + for consistency. + Default to "auto_strategy". fit_intercept (default True): Default True. Specifies if a constant (a.k.a. bias or intercept) should be added to the decision function. From 5bab676c20fc62697d6234f069b983e4a5fbe61d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 5 Jun 2024 13:07:38 -0700 Subject: [PATCH 622/921] refactor: New node type for cached subtrees (#738) --- bigframes/core/__init__.py | 41 +++++++++++++++++- bigframes/core/compile/compiler.py | 33 ++++++++++++++ bigframes/core/nodes.py | 49 ++++++++++++++++++++- bigframes/core/ordering.py | 8 ++++ bigframes/session/__init__.py | 69 ++++++------------------------ 5 files changed, 141 insertions(+), 59 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 133d271fedc..f8a04712a40 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -95,6 +95,23 @@ def from_pyarrow(cls, arrow_table: pa.Table, session: Session): ) return cls(node) + @classmethod + def from_cached( + cls, + original: ArrayValue, + table: google.cloud.bigquery.Table, + ordering: orderings.ExpressionOrdering, + ): + node = nodes.CachedTableNode( + original_node=original.node, + project_id=table.reference.project, + dataset_id=table.reference.dataset_id, + table_id=table.reference.table_id, + physical_schema=tuple(table.schema), + ordering=ordering, + ) + return cls(node) + @classmethod def from_table( cls, @@ -105,7 +122,10 @@ def from_table( predicate: Optional[str] = None, at_time: Optional[datetime.datetime] = None, primary_key: Sequence[str] = (), + offsets_col: Optional[str] = None, ): + if offsets_col and primary_key: + raise ValueError("must set at most one of 'offests', 'primary_key'") if any(i.field_type == "JSON" for i in table.schema if i.name in schema.names): warnings.warn( "Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.", @@ -116,7 +136,8 @@ def from_table( dataset_id=table.reference.dataset_id, table_id=table.reference.table_id, physical_schema=tuple(table.schema), - total_order_cols=tuple(primary_key), + total_order_cols=(offsets_col,) if offsets_col else tuple(primary_key), + order_col_is_sequential=(offsets_col is not None), columns=schema, at_time=at_time, table_session=session, @@ -150,6 +171,24 @@ def _compiled_schema(self) -> schemata.ArraySchema: ) return schemata.ArraySchema(items) + def as_cached( + self: ArrayValue, + cache_table: google.cloud.bigquery.Table, + ordering: orderings.ExpressionOrdering, + ) -> ArrayValue: + """ + Replace the node with an equivalent one that references a tabel where the value has been materialized to. + """ + node = nodes.CachedTableNode( + original_node=self.node, + project_id=cache_table.reference.project, + dataset_id=cache_table.reference.dataset_id, + table_id=cache_table.reference.table_id, + physical_schema=tuple(cache_table.schema), + ordering=ordering, + ) + return ArrayValue(node) + def _try_evaluate_local(self): """Use only for unit testing paths - not fully featured. Will throw exception if fails.""" import ibis diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index f948d10a5bd..b8c768dac8e 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -96,6 +96,39 @@ def compile_readlocal(node: nodes.ReadLocalNode, ordered: bool = True): return ordered_ir.to_unordered() +@_compile_node.register +def compile_cached_table(node: nodes.CachedTableNode, ordered: bool = True): + full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" + used_columns = ( + *node.schema.names, + *node.hidden_columns, + ) + # Physical schema might include unused columns, unsupported datatypes like JSON + physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( + list(i for i in node.physical_schema if i.name in used_columns) + ) + ibis_table = ibis.table(physical_schema, full_table_name) + if ordered: + return compiled.OrderedIR( + ibis_table, + columns=tuple( + bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + for col in node.schema.names + ), + ordering=node.ordering, + hidden_ordering_columns=[ibis_table[c] for c in node.hidden_columns], + ) + + else: + return compiled.UnorderedIR( + ibis_table, + columns=tuple( + bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + for col in node.schema.names + ), + ) + + @_compile_node.register def compile_readtable(node: nodes.ReadTableNode, ordered: bool = True): if ordered: diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 1af7c5bd17d..3b82401e230 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -376,7 +376,7 @@ def __post_init__(self): raise ValueError( f"Requested schema {self.columns} cannot be derived from table schemal {self.physical_schema}" ) - if self.order_col_is_sequential and len(self.total_order_cols) == 1: + if self.order_col_is_sequential and len(self.total_order_cols) != 1: raise ValueError("Sequential primary key must have only one component") @property @@ -409,6 +409,53 @@ def transform_children( return self +@dataclass(frozen=True) +class CachedTableNode(BigFrameNode): + # The original BFET subtree that was cached + # note: this isn't a "child" node. + original_node: BigFrameNode = field() + # reference to cached materialization of original_node + project_id: str = field() + dataset_id: str = field() + table_id: str = field() + physical_schema: Tuple[bq.SchemaField, ...] = field() + + ordering: orderings.ExpressionOrdering = field() + + @property + def session(self): + return self.original_node.session + + def __hash__(self): + return self._node_hash + + @property + def roots(self) -> typing.Set[BigFrameNode]: + return {self} + + @property + def schema(self) -> schemata.ArraySchema: + return self.original_node.schema + + @functools.cached_property + def variables_introduced(self) -> int: + return len(self.schema.items) + OVERHEAD_VARIABLES + + @property + def hidden_columns(self) -> typing.Tuple[str, ...]: + """Physical columns used to define ordering but not directly exposed as value columns.""" + return tuple( + col + for col in sorted(self.ordering.referenced_columns) + if col not in self.schema.names + ) + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return self + + # Unary nodes @dataclass(frozen=True) class PromoteOffsetsNode(UnaryNode): diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 9009e31be39..15625927205 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -215,6 +215,14 @@ def is_sequential(self) -> bool: def all_ordering_columns(self) -> Sequence[OrderingExpression]: return list(self.ordering_value_columns) + @property + def referenced_columns(self) -> Set[str]: + return set( + col + for part in self.ordering_value_columns + for col in part.scalar_expression.unbound_variables + ) + def encode_order_string( order_id: ibis_types.IntegerColumn, length: int = DEFAULT_ORDERING_ID_LENGTH diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 2919b2d77fb..80bf659a3d9 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -84,7 +84,6 @@ import bigframes.core.compile import bigframes.core.guid import bigframes.core.nodes as nodes -from bigframes.core.ordering import IntegerEncoding import bigframes.core.ordering as order import bigframes.core.schema as schemata import bigframes.core.tree_properties as traversals @@ -1159,35 +1158,14 @@ def _read_pandas_load_job( ) self._start_generic_job(load_job) - ordering = order.ExpressionOrdering( - ordering_value_columns=tuple([order.ascending_over(ordering_col)]), - total_ordering_columns=frozenset([ordering_col]), - integer_encoding=IntegerEncoding(True, is_sequential=True), - ) - table_expression = self.ibis_client.table( # type: ignore - load_table_destination.table_id, - schema=load_table_destination.dataset_id, - database=load_table_destination.project, - ) - - # b/297590178 Potentially a bug in bqclient.load_table_from_dataframe(), that only when the DF is empty, the index columns disappear in table_expression. - if any( - [new_idx_id not in table_expression.columns for new_idx_id in new_idx_ids] - ): - new_idx_ids, idx_labels = [], [] - - column_values = [ - table_expression[col] - for col in table_expression.columns - if col != ordering_col - ] - array_value = core.ArrayValue.from_ibis( - self, - table_expression, - columns=column_values, - hidden_ordering_columns=[table_expression[ordering_col]], - ordering=ordering, - ) + destination_table = self.bqclient.get_table(load_table_destination) + array_value = core.ArrayValue.from_table( + table=destination_table, + # TODO: Generate this directly from original pandas df. + schema=schemata.ArraySchema.from_bq_table(destination_table), + session=self, + offsets_col=ordering_col, + ).drop_columns([ordering_col]) block = blocks.Block( array_value, @@ -1871,21 +1849,8 @@ def _cache_with_cluster_cols( tmp_table = self._ibis_to_temp_table( ibis_expr, cluster_cols=cluster_cols, api_name="cached" ) - table_expression = self.ibis_client.table( - tmp_table.table_id, - schema=tmp_table.dataset_id, - database=tmp_table.project, - ) - new_columns = [table_expression[column] for column in compiled_value.column_ids] - new_hidden_columns = [ - table_expression[column] - for column in compiled_value._hidden_ordering_column_names - ] - cached_replacement = core.ArrayValue.from_ibis( - self, - table_expression, - columns=new_columns, - hidden_ordering_columns=new_hidden_columns, + cached_replacement = array_value.as_cached( + cache_table=self.bqclient.get_table(tmp_table), ordering=compiled_value._ordering, ).node self._cached_executions[array_value.node] = cached_replacement @@ -1902,18 +1867,8 @@ def _cache_with_offsets(self, array_value: core.ArrayValue): tmp_table = self._ibis_to_temp_table( ibis_expr, cluster_cols=["bigframes_offsets"], api_name="cached" ) - table_expression = self.ibis_client.table( - tmp_table.table_id, - schema=tmp_table.dataset_id, - database=tmp_table.project, - ) - new_columns = [table_expression[column] for column in compiled_value.column_ids] - new_hidden_columns = [table_expression["bigframes_offsets"]] - cached_replacement = core.ArrayValue.from_ibis( - self, - table_expression, - columns=new_columns, - hidden_ordering_columns=new_hidden_columns, + cached_replacement = array_value.as_cached( + cache_table=self.bqclient.get_table(tmp_table), ordering=order.ExpressionOrdering.from_offset_col("bigframes_offsets"), ).node self._cached_executions[array_value.node] = cached_replacement From 181db809e039a5a9a92157b8205136b2783a919d Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:16:08 -0700 Subject: [PATCH 623/921] refactor: read transformers from TRANSFORM_ONLY type (#750) --- bigframes/ml/loader.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 66f207929ad..515fb50c6fb 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -98,13 +98,7 @@ def from_bq( Returns: A BigQuery DataFrames ML model object. """ - # TODO(garrettwu): the entire condition only to TRANSFORM_ONLY when b/331679273 is fixed. - if ( - bq_model.model_type == "TRANSFORM_ONLY" - or bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" - and "transformColumns" in bq_model._properties - and not _is_bq_model_remote(bq_model) - ): + if bq_model.model_type == "TRANSFORM_ONLY": return _transformer_from_bq(session, bq_model) if _is_bq_model_pipeline(bq_model): From fdb29f6726d2549d732169c6228b6064ce1c8884 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:18:40 -0700 Subject: [PATCH 624/921] cleanup type errors in tests/system/large/test_remote_function.py and tests/system/small/test_remote_function.py (#748) --- tests/system/large/test_remote_function.py | 19 ++++++++----------- tests/system/small/test_remote_function.py | 5 ++--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index cce49ea1871..6bfc9f0da3d 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -92,17 +92,14 @@ def make_uniq_udf(udf): target_code = source_code.replace(source_key, target_key, 1) f.write(target_code) spec = importlib.util.spec_from_file_location(udf_file_name, udf_file_path) - # TODO(b/340875260): fix type error - udf_uniq = getattr(spec.loader.load_module(), udf_uniq_name) # type: ignore - - # This is a bit of a hack but we need to remove the reference to a foreign - # module, otherwise the serialization would keep the foreign module - # reference and deserialization would fail with error like following: - # ModuleNotFoundError: No module named 'add_one_2nxcmd9j' - # TODO(shobs): Figure out if there is a better way of generating the unique - # function object, but for now let's just set it to same module as the - # original udf. - udf_uniq.__module__ = udf.__module__ + + assert (spec is not None) and (spec.loader is not None) + module = importlib.util.module_from_spec(spec) + + # exec_module fills the module object with all the functions, classes, and + # variables defined in the module file. + spec.loader.exec_module(module) + udf_uniq = getattr(module, udf_uniq_name) return udf_uniq, tmpdir diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index bf3424d0f0c..85e23fae16a 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -658,11 +658,10 @@ def test_read_gbq_function_reads_udfs(session, bigquery_client, dataset_id): indirect_df = bigframes.dataframe.DataFrame(src) indirect_df = indirect_df.assign(y=indirect_df.x.apply(square)) - # TODO(b/340875260): fix type error - indirect_df = indirect_df.to_pandas() # type: ignore + converted_indirect_df = indirect_df.to_pandas() assert_pandas_df_equal( - direct_df, indirect_df, ignore_order=True, check_index_type=False + direct_df, converted_indirect_df, ignore_order=True, check_index_type=False ) From 448c4a634abde18bfa444b87144f5a5b71ae5eb9 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:41:18 -0700 Subject: [PATCH 625/921] chore: cleanup type errors in tests/system/large/test_remote_function.py and tests/system/small/test_remote_function.py (#747) --- tests/system/small/ml/test_model_selection.py | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/tests/system/small/ml/test_model_selection.py b/tests/system/small/ml/test_model_selection.py index ca14186a4d1..63d0840d291 100644 --- a/tests/system/small/ml/test_model_selection.py +++ b/tests/system/small/ml/test_model_selection.py @@ -130,17 +130,12 @@ def test_train_test_split_seeded_correct_rows( X, y, random_state=42 ) - # TODO(b/340876926): fix type error - X_train = X_train.to_pandas().sort_index() # type: ignore - # TODO(b/340876926): fix type error - X_test = X_test.to_pandas().sort_index() # type: ignore - # TODO(b/340876926): fix type error - y_train = y_train.to_pandas().sort_index() # type: ignore - # TODO(b/340876926): fix type error - y_test = y_test.to_pandas().sort_index() # type: ignore - - # TODO(b/340876926): fix type error - train_index = pd.Index( # type: ignore + X_train_sorted = X_train.to_pandas().sort_index() + X_test_sorted = X_test.to_pandas().sort_index() + y_train_sorted = y_train.to_pandas().sort_index() + y_test_sorted = y_test.to_pandas().sort_index() + + train_index: pd.Index = pd.Index( [ 144, 146, @@ -167,15 +162,20 @@ def test_train_test_split_seeded_correct_rows( dtype="Int64", name="rowindex", ) - # TODO(b/340876926): fix type error - test_index = pd.Index( # type: ignore + test_index: pd.Index = pd.Index( [148, 161, 226, 269, 278, 289, 291], dtype="Int64", name="rowindex" ) all_data.index.name = "_" - # TODO(b/340876926): fix type error + + assert ( + isinstance(X_train_sorted, pd.DataFrame) + and isinstance(X_test_sorted, pd.DataFrame) + and isinstance(y_train_sorted, pd.DataFrame) + and isinstance(y_test_sorted, pd.DataFrame) + ) pd.testing.assert_frame_equal( - X_train, # type: ignore + X_train_sorted, all_data[ [ "species", @@ -184,9 +184,8 @@ def test_train_test_split_seeded_correct_rows( ] ].loc[train_index], ) - # TODO(b/340876926): fix type error pd.testing.assert_frame_equal( - X_test, # type: ignore + X_test_sorted, all_data[ [ "species", @@ -195,18 +194,16 @@ def test_train_test_split_seeded_correct_rows( ] ].loc[test_index], ) - # TODO(b/340876926): fix type error pd.testing.assert_frame_equal( - y_train, # type: ignore + y_train_sorted, all_data[ [ "body_mass_g", ] ].loc[train_index], ) - # TODO(b/340876926): fix type error pd.testing.assert_frame_equal( - y_test, # type: ignore + y_test_sorted, all_data[ [ "body_mass_g", From fb8cf8f8dd2f1bbfb57e9af3632d2ab18184a95b Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 6 Jun 2024 05:57:28 -0700 Subject: [PATCH 626/921] test: fix gemini-pro score test too large (#760) --- tests/system/conftest.py | 12 ++++++++++++ tests/system/load/test_llm.py | 17 ++--------------- tests/system/small/ml/conftest.py | 12 ------------ 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 25016930840..54a3deffe21 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -946,6 +946,18 @@ def penguins_randomforest_classifier_model_name( return model_name +@pytest.fixture(scope="session") +def llm_fine_tune_df_default_index( + session: bigframes.Session, +) -> bigframes.dataframe.DataFrame: + training_table_name = "llm_tuning.emotion_classification_train" + df = session.read_gbq(training_table_name).dropna().head(30) + prefix = "Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: " + df["prompt"] = prefix + df["text"] + df["label"] = df["label"].astype("string") + return df + + @pytest.fixture(scope="session") def usa_names_grouped_table( session: bigframes.Session, dataset_id_permanent diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index fd136622754..beed884686c 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -18,18 +18,6 @@ import bigframes.ml.llm -@pytest.fixture(scope="session") -def llm_fine_tune_df_default_index( - session: bigframes.Session, -) -> bigframes.dataframe.DataFrame: - training_table_name = "llm_tuning.emotion_classification_train" - df = session.read_gbq(training_table_name) - prefix = "Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: " - df["prompt"] = prefix + df["text"] - df["label"] = df["label"].astype("string") - return df - - @pytest.fixture(scope="session") def llm_remote_text_pandas_df(): """Additional data matching the penguins dataset, with a new index""" @@ -55,9 +43,8 @@ def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_ model_name="text-bison", max_iterations=1 ) - df = llm_fine_tune_df_default_index.dropna().sample(n=100) - X_train = df[["prompt"]] - y_train = df[["label"]] + X_train = llm_fine_tune_df_default_index[["prompt"]] + y_train = llm_fine_tune_df_default_index[["label"]] model.fit(X_train, y_train) assert model is not None diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index ab75053b0e7..ee966466870 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -171,18 +171,6 @@ def llm_text_pandas_df(): ) -@pytest.fixture(scope="session") -def llm_fine_tune_df_default_index( - session: bigframes.Session, -) -> bigframes.dataframe.DataFrame: - training_table_name = "llm_tuning.emotion_classification_train" - df = session.read_gbq(training_table_name) - prefix = "Please do sentiment analysis on the following text and only output a number from 0 to 5 where 0 means sadness, 1 means joy, 2 means love, 3 means anger, 4 means fear, and 5 means surprise. Text: " - df["prompt"] = prefix + df["text"] - df["label"] = df["label"].astype("string") - return df - - @pytest.fixture(scope="session") def onnx_iris_pandas_df(): """Data matching the iris dataset.""" From 93353c368f6af5ae186493137616bf562d890118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 6 Jun 2024 09:07:03 -0500 Subject: [PATCH 627/921] refactor: use unit testable real python functions for most templated remote_functions code (#751) * refactor: use unit testable real python functions for most templated remote_functions code * revert changes and add unit tests * Update tests/unit/functions/test_remote_function_template.py * mypy failure * Update bigframes/functions/remote_function_template.py --- bigframes/functions/remote_function.py | 179 +------------- .../functions/remote_function_template.py | 223 ++++++++++++++++++ mypy.ini | 3 + .../test_remote_function_template.py | 50 ++++ 4 files changed, 284 insertions(+), 171 deletions(-) create mode 100644 bigframes/functions/remote_function_template.py create mode 100644 tests/unit/functions/test_remote_function_template.py diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 4223998c53d..5b4d7ad6ddd 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -24,7 +24,6 @@ import string import sys import tempfile -import textwrap from typing import ( Any, cast, @@ -61,6 +60,7 @@ from bigframes import clients import bigframes.constants as constants import bigframes.dtypes +import bigframes.functions.remote_function_template logger = logging.getLogger(__name__) @@ -258,171 +258,8 @@ def get_cloud_function_endpoint(self, name): pass return None - def generate_udf_code(self, def_, dir): - """Generate serialized bytecode using cloudpickle given a udf.""" - udf_code_file_name = "udf.py" - udf_bytecode_file_name = "udf.cloudpickle" - - # original code, only for debugging purpose - udf_code = textwrap.dedent(inspect.getsource(def_)) - udf_code_file_path = os.path.join(dir, udf_code_file_name) - with open(udf_code_file_path, "w") as f: - f.write(udf_code) - - # serialized bytecode - udf_bytecode_file_path = os.path.join(dir, udf_bytecode_file_name) - with open(udf_bytecode_file_path, "wb") as f: - cloudpickle.dump(def_, f, protocol=_pickle_protocol_version) - - return udf_code_file_name, udf_bytecode_file_name - - def generate_cloud_function_main_code(self, def_, dir, is_row_processor=False): - """Get main.py code for the cloud function for the given user defined function.""" - - # Pickle the udf with all its dependencies - udf_code_file, udf_bytecode_file = self.generate_udf_code(def_, dir) - handler_func_name = "udf_http" - - # We want to build a cloud function that works for BQ remote functions, - # where we receive `calls` in json which is a batch of rows from BQ SQL. - # The number and the order of values in each row is expected to exactly - # match to the number and order of arguments in the udf , e.g. if the udf is - # def foo(x: int, y: str): - # ... - # then the http request body could look like - # { - # ... - # "calls" : [ - # [123, "hello"], - # [456, "world"] - # ] - # ... - # } - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#input_format - code = """\ -import cloudpickle -import functions_framework -from flask import jsonify -import json -""" - if is_row_processor: - code += """\ -import ast -import math -import pandas as pd - -def get_pd_series(row): - row_json = json.loads(row) - col_names = row_json["names"] - col_types = row_json["types"] - col_values = row_json["values"] - index_length = row_json["indexlength"] - dtype = row_json["dtype"] - - # At this point we are assuming that col_names, col_types and col_values are - # arrays of the same length, representing column names, types and values for - # one row of data - - # column names are not necessarily strings - # they are serialized as repr(name) at source - evaluated_col_names = [] - for col_name in col_names: - try: - col_name = ast.literal_eval(col_name) - except Exception as ex: - raise NameError(f"Failed to evaluate column name from '{col_name}': {ex}") - evaluated_col_names.append(col_name) - col_names = evaluated_col_names - - # Supported converters for pandas to python types - value_converters = { - "boolean": lambda val: val == "true", - "Int64": int, - "Float64": float, - "string": str, - } - - def convert_value(value, value_type): - value_converter = value_converters.get(value_type) - if value_converter is None: - raise ValueError(f"Don't know how to handle type '{value_type}'") - if value is None: - return None - return value_converter(value) - - index_values = [ - pd.Series([convert_value(col_values[i], col_types[i])], dtype=col_types[i])[0] - for i in range(index_length) - ] - - data_col_names = col_names[index_length:] - data_col_types = col_types[index_length:] - data_col_values = col_values[index_length:] - data_col_values = [ - pd.Series([convert_value(a, data_col_types[i])], dtype=data_col_types[i])[0] - for i, a in enumerate(data_col_values) - ] - - row_index = index_values[0] if len(index_values) == 1 else tuple(index_values) - row_series = pd.Series(data_col_values, index=data_col_names, name=row_index, dtype=dtype) - return row_series -""" - code += f"""\ - -# original udf code is in {udf_code_file} -# serialized udf code is in {udf_bytecode_file} -with open("{udf_bytecode_file}", "rb") as f: - udf = cloudpickle.load(f) - -def {handler_func_name}(request): - try: - request_json = request.get_json(silent=True) - calls = request_json["calls"] - replies = [] - for call in calls: -""" - - if is_row_processor: - code += """\ - reply = udf(get_pd_series(call[0])) - if isinstance(reply, float) and (math.isnan(reply) or math.isinf(reply)): - # json serialization of the special float values (nan, inf, -inf) - # is not in strict compliance of the JSON specification - # https://docs.python.org/3/library/json.html#basic-usage. - # Let's convert them to a quoted string representation ("NaN", - # "Infinity", "-Infinity" respectively) which is handled by - # BigQuery - reply = json.dumps(reply) - elif pd.isna(reply): - # Pandas N/A values are not json serializable, so use a python - # equivalent instead - reply = None - elif hasattr(reply, "item"): - # Numpy types are not json serializable, so use its Python - # value instead - reply = reply.item() -""" - else: - code += """\ - reply = udf(*call) -""" - code += """\ - replies.append(reply) - return_json = json.dumps({"replies" : replies}) - return return_json - except Exception as e: - return jsonify( { "errorMessage": str(e) } ), 400 -""" - - main_py = os.path.join(dir, "main.py") - with open(main_py, "w") as f: - f.write(code) - logger.debug(f"Wrote {os.path.abspath(main_py)}:\n{open(main_py).read()}") - - return handler_func_name - def generate_cloud_function_code( - self, def_, dir, package_requirements=None, is_row_processor=False + self, def_, directory, package_requirements=None, is_row_processor=False ): """Generate the cloud function code for a given user defined function.""" @@ -435,13 +272,13 @@ def generate_cloud_function_code( if package_requirements: requirements.extend(package_requirements) requirements = sorted(requirements) - requirements_txt = os.path.join(dir, "requirements.txt") + requirements_txt = os.path.join(directory, "requirements.txt") with open(requirements_txt, "w") as f: f.write("\n".join(requirements)) # main.py - entry_point = self.generate_cloud_function_main_code( - def_, dir, is_row_processor + entry_point = bigframes.functions.remote_function_template.generate_cloud_function_main_code( + def_, directory, is_row_processor ) return entry_point @@ -458,11 +295,11 @@ def create_cloud_function( """Create a cloud function from the given user defined function.""" # Build and deploy folder structure containing cloud function - with tempfile.TemporaryDirectory() as dir: + with tempfile.TemporaryDirectory() as directory: entry_point = self.generate_cloud_function_code( - def_, dir, package_requirements, is_row_processor + def_, directory, package_requirements, is_row_processor ) - archive_path = shutil.make_archive(dir, "zip", dir) + archive_path = shutil.make_archive(directory, "zip", directory) # We are creating cloud function source code from the currently running # python version. Use the same version to deploy. This is necessary diff --git a/bigframes/functions/remote_function_template.py b/bigframes/functions/remote_function_template.py new file mode 100644 index 00000000000..5a70482469a --- /dev/null +++ b/bigframes/functions/remote_function_template.py @@ -0,0 +1,223 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import inspect +import logging +import os +import textwrap + +import cloudpickle + +logger = logging.getLogger(__name__) + + +# Protocol version 4 is available in python version 3.4 and above +# https://docs.python.org/3/library/pickle.html#data-stream-format +_pickle_protocol_version = 4 + + +# get_pd_series is the inverse of Block._get_rows_as_json_values +def get_pd_series(row): + import ast + import json + from typing import Callable, cast + + import pandas as pd + + row_json = json.loads(row) + col_names = row_json["names"] + col_types = row_json["types"] + col_values = row_json["values"] + index_length = row_json["indexlength"] + dtype = row_json["dtype"] + + # At this point we are assuming that col_names, col_types and col_values are + # arrays of the same length, representing column names, types and values for + # one row of data + + # column names are not necessarily strings + # they are serialized as repr(name) at source + evaluated_col_names = [] + for col_name in col_names: + try: + col_name = ast.literal_eval(col_name) + except Exception as ex: + raise NameError(f"Failed to evaluate column name from '{col_name}': {ex}") + evaluated_col_names.append(col_name) + col_names = evaluated_col_names + + # Supported converters for pandas to python types + value_converters = { + "boolean": lambda val: val == "true", + "Int64": int, + "Float64": float, + "string": str, + } + + def convert_value(value, value_type): + value_converter = cast(Callable, value_converters.get(value_type)) + if value_converter is None: + raise ValueError(f"Don't know how to handle type '{value_type}'") + if value is None: + return None + return value_converter(value) + + index_values = [ + pd.Series([convert_value(col_values[i], col_types[i])], dtype=col_types[i])[0] + for i in range(index_length) + ] + + data_col_names = col_names[index_length:] + data_col_types = col_types[index_length:] + data_col_values = col_values[index_length:] + data_col_values = [ + pd.Series([convert_value(a, data_col_types[i])], dtype=data_col_types[i])[0] + for i, a in enumerate(data_col_values) + ] + + row_index = index_values[0] if len(index_values) == 1 else tuple(index_values) + row_series = pd.Series( + data_col_values, index=data_col_names, name=row_index, dtype=dtype + ) + return row_series + + +def udf(*args): + """Dummy function to use as a placeholder for function code in templates.""" + pass + + +# We want to build a cloud function that works for BQ remote functions, +# where we receive `calls` in json which is a batch of rows from BQ SQL. +# The number and the order of values in each row is expected to exactly +# match to the number and order of arguments in the udf , e.g. if the udf is +# def foo(x: int, y: str): +# ... +# then the http request body could look like +# { +# ... +# "calls" : [ +# [123, "hello"], +# [456, "world"] +# ] +# ... +# } +# https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#input_format +def udf_http(request): + import json + + from flask import jsonify + + try: + request_json = request.get_json(silent=True) + calls = request_json["calls"] + replies = [] + for call in calls: + reply = udf(*call) + replies.append(reply) + return_json = json.dumps({"replies": replies}) + return return_json + except Exception as e: + return jsonify({"errorMessage": str(e)}), 400 + + +def udf_http_row_processor(request): + import json + import math + + from flask import jsonify + import pandas as pd + + try: + request_json = request.get_json(silent=True) + calls = request_json["calls"] + replies = [] + for call in calls: + reply = udf(get_pd_series(call[0])) + if isinstance(reply, float) and (math.isnan(reply) or math.isinf(reply)): + # json serialization of the special float values (nan, inf, -inf) + # is not in strict compliance of the JSON specification + # https://docs.python.org/3/library/json.html#basic-usage. + # Let's convert them to a quoted string representation ("NaN", + # "Infinity", "-Infinity" respectively) which is handled by + # BigQuery + reply = json.dumps(reply) + elif pd.isna(reply): + # Pandas N/A values are not json serializable, so use a python + # equivalent instead + reply = None + elif hasattr(reply, "item"): + # Numpy types are not json serializable, so use its Python + # value instead + reply = reply.item() + replies.append(reply) + return_json = json.dumps({"replies": replies}) + return return_json + except Exception as e: + return jsonify({"errorMessage": str(e)}), 400 + + +def generate_udf_code(def_, directory): + """Generate serialized bytecode using cloudpickle given a udf.""" + udf_code_file_name = "udf.py" + udf_bytecode_file_name = "udf.cloudpickle" + + # original code, only for debugging purpose + udf_code = textwrap.dedent(inspect.getsource(def_)) + udf_code_file_path = os.path.join(directory, udf_code_file_name) + with open(udf_code_file_path, "w") as f: + f.write(udf_code) + + # serialized bytecode + udf_bytecode_file_path = os.path.join(directory, udf_bytecode_file_name) + # TODO(b/345433300): try io.BytesIO to avoid writing to the file system + with open(udf_bytecode_file_path, "wb") as f: + cloudpickle.dump(def_, f, protocol=_pickle_protocol_version) + + return udf_code_file_name, udf_bytecode_file_name + + +def generate_cloud_function_main_code(def_, directory, is_row_processor=False): + """Get main.py code for the cloud function for the given user defined function.""" + + # Pickle the udf with all its dependencies + udf_code_file, udf_bytecode_file = generate_udf_code(def_, directory) + + code_blocks = [ + f"""\ +import cloudpickle + +# original udf code is in {udf_code_file} +# serialized udf code is in {udf_bytecode_file} +with open("{udf_bytecode_file}", "rb") as f: + udf = cloudpickle.load(f) +""" + ] + + if is_row_processor: + code_blocks.append(inspect.getsource(get_pd_series)) + handler_func_name = "udf_http_row_processor" + code_blocks.append(inspect.getsource(udf_http_row_processor)) + else: + handler_func_name = "udf_http" + code_blocks.append(inspect.getsource(udf_http)) + + main_py = os.path.join(directory, "main.py") + with open(main_py, "w") as f: + f.writelines(code_blocks) + logger.debug(f"Wrote {os.path.abspath(main_py)}:\n{open(main_py).read()}") + + return handler_func_name diff --git a/mypy.ini b/mypy.ini index 5707f141540..f0a005d2e5a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -9,6 +9,9 @@ ignore_missing_imports = True [mypy-cloudpickle.*] ignore_missing_imports = True +[mypy-flask] +ignore_missing_imports = True + [mypy-pydata_google_auth] ignore_missing_imports = True diff --git a/tests/unit/functions/test_remote_function_template.py b/tests/unit/functions/test_remote_function_template.py new file mode 100644 index 00000000000..4b92b13149a --- /dev/null +++ b/tests/unit/functions/test_remote_function_template.py @@ -0,0 +1,50 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import pandas as pd +import pandas.testing +import pytest + +import bigframes.functions.remote_function_template as remote_function_template + + +@pytest.mark.parametrize( + ["row_json", "expected"], + ( + pytest.param( + json.dumps( + { + "names": ["'my-index'", "'col1'", "'col2'", "'col3'"], + "types": ["string", "Int64", "Int64", "Int64"], + "values": ["my-index-value", "1", None, "-1"], + "indexlength": 1, + "dtype": "Int64", + } + ), + pd.Series( + [1, pd.NA, -1], + dtype="Int64", + index=["col1", "col2", "col3"], + name="my-index-value", + ), + id="int64-string-index", + ), + # TODO: test more types and indexes + ), +) +def test_get_pd_series(row_json, expected): + got = remote_function_template.get_pd_series(row_json) + pandas.testing.assert_series_equal(got, expected) From e452203fa9127f797ff5b106faaa684265cabb88 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 6 Jun 2024 10:45:47 -0700 Subject: [PATCH 628/921] refactor: Move ibis type mappings to compile module (#745) --- bigframes/core/__init__.py | 5 +- bigframes/core/compile/aggregate_compiler.py | 22 +- bigframes/core/compile/compiled.py | 22 +- bigframes/core/compile/compiler.py | 18 +- bigframes/core/compile/ibis_types.py | 476 +++++++++++++++++ bigframes/core/compile/scalar_op_compiler.py | 21 +- bigframes/core/compile/schema_translator.py | 3 +- bigframes/core/local_data.py | 4 +- bigframes/core/nodes.py | 4 +- bigframes/dtypes.py | 516 +++---------------- bigframes/functions/remote_function.py | 36 +- tests/unit/{ => core}/test_dtypes.py | 35 +- tests/unit/test_remote_function.py | 5 +- 13 files changed, 645 insertions(+), 522 deletions(-) create mode 100644 bigframes/core/compile/ibis_types.py rename tests/unit/{ => core}/test_dtypes.py (86%) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index f8a04712a40..f8ee984d5fb 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -60,6 +60,7 @@ class ArrayValue: node: nodes.BigFrameNode + # DO NOT use, on deprecation path @classmethod def from_ibis( cls, @@ -69,11 +70,13 @@ def from_ibis( hidden_ordering_columns: Sequence[ibis_types.Value], ordering: orderings.ExpressionOrdering, ): + import bigframes.core.compile.ibis_types + node = nodes.ReadGbqNode( table=table, table_session=session, columns=tuple( - bigframes.dtypes.ibis_value_to_canonical_type(column) + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type(column) for column in columns ), hidden_ordering_columns=tuple(hidden_ordering_columns), diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index fada4ebbd84..58973b10eb6 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -22,10 +22,10 @@ import pandas as pd import bigframes.constants as constants +import bigframes.core.compile.ibis_types as compile_ibis_types import bigframes.core.compile.scalar_op_compiler as scalar_compilers import bigframes.core.expression as ex import bigframes.core.window_spec as window_spec -import bigframes.dtypes as dtypes import bigframes.operations.aggregations as agg_ops scalar_compiler = scalar_compilers.scalar_op_compiler @@ -323,7 +323,7 @@ def _( for this_bin in range(op.bins - 1): out = out.when( x <= (col_min + (this_bin + 1) * bin_width), - dtypes.literal_to_ibis_scalar( + compile_ibis_types.literal_to_ibis_scalar( this_bin, force_dtype=pd.Int64Dtype() ), ) @@ -352,8 +352,8 @@ def _( out = out.when(x.notnull(), interval_struct) else: # Interpret as intervals for interval in op.bins: - left = dtypes.literal_to_ibis_scalar(interval[0]) - right = dtypes.literal_to_ibis_scalar(interval[1]) + left = compile_ibis_types.literal_to_ibis_scalar(interval[0]) + right = compile_ibis_types.literal_to_ibis_scalar(interval[1]) condition = (x > left) & (x <= right) interval_struct = ibis.struct( {"left_exclusive": left, "right_inclusive": right} @@ -370,7 +370,7 @@ def _( window=None, ) -> ibis_types.IntegerValue: if isinstance(self.quantiles, int): - quantiles_ibis = dtypes.literal_to_ibis_scalar(self.quantiles) + quantiles_ibis = compile_ibis_types.literal_to_ibis_scalar(self.quantiles) percent_ranks = cast( ibis_types.FloatingColumn, _apply_window_if_present(column.percent_rank(), window), @@ -383,13 +383,19 @@ def _( _apply_window_if_present(column.percent_rank(), window), ) out = ibis.case() - first_ibis_quantile = dtypes.literal_to_ibis_scalar(self.quantiles[0]) + first_ibis_quantile = compile_ibis_types.literal_to_ibis_scalar( + self.quantiles[0] + ) out = out.when(percent_ranks < first_ibis_quantile, None) for bucket_n in range(len(self.quantiles) - 1): - ibis_quantile = dtypes.literal_to_ibis_scalar(self.quantiles[bucket_n + 1]) + ibis_quantile = compile_ibis_types.literal_to_ibis_scalar( + self.quantiles[bucket_n + 1] + ) out = out.when( percent_ranks <= ibis_quantile, - dtypes.literal_to_ibis_scalar(bucket_n, force_dtype=pd.Int64Dtype()), + compile_ibis_types.literal_to_ibis_scalar( + bucket_n, force_dtype=pd.Int64Dtype() + ), ) out = out.else_(None) return out.end() # type: ignore diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 552061f6127..dac814a08c9 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -28,6 +28,7 @@ import pandas import bigframes.core.compile.aggregate_compiler as agg_compiler +import bigframes.core.compile.ibis_types import bigframes.core.compile.scalar_op_compiler as op_compilers import bigframes.core.expression as ex import bigframes.core.guid @@ -157,16 +158,19 @@ def _get_ibis_column(self, key: str) -> ibis_types.Value: ) return typing.cast( ibis_types.Value, - bigframes.dtypes.ibis_value_to_canonical_type(self._column_names[key]), + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + self._column_names[key] + ), ) def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: ibis_type = typing.cast( - bigframes.dtypes.IbisDtype, self._get_ibis_column(key).type() + bigframes.core.compile.ibis_types.IbisDtype, + self._get_ibis_column(key).type(), ) return typing.cast( bigframes.dtypes.Dtype, - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_type), + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype(ibis_type), ) def _aggregate_base( @@ -332,7 +336,8 @@ def _to_ibis_expr( # Make sure all dtypes are the "canonical" ones for BigFrames. This is # important for operations like UNION where the schema must match. table = self._table.select( - bigframes.dtypes.ibis_value_to_canonical_type(column) for column in columns + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type(column) + for column in columns ) base_table = table if self._reduced_predicate is not None: @@ -579,7 +584,10 @@ def from_pandas( ibis_values = ibis_values.assign(**{ORDER_ID_COLUMN: range(len(pd_df))}) # derive the ibis schema from the original pandas schema ibis_schema = [ - (name, bigframes.dtypes.bigframes_dtype_to_ibis_dtype(dtype)) + ( + name, + bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype(dtype), + ) for name, dtype in zip(schema.names, schema.dtypes) ] ibis_schema.append((ORDER_ID_COLUMN, ibis_dtypes.int64)) @@ -993,7 +1001,9 @@ def _to_ibis_expr( # Make sure all dtypes are the "canonical" ones for BigFrames. This is # important for operations like UNION where the schema must match. table = table.select( - bigframes.dtypes.ibis_value_to_canonical_type(table[column]) + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + table[column] + ) for column in table.columns ) base_table = table diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index b8c768dac8e..4ecba062365 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -26,11 +26,11 @@ import bigframes.core.compile.compiled as compiled import bigframes.core.compile.concat as concat_impl import bigframes.core.compile.default_ordering as default_ordering +import bigframes.core.compile.ibis_types import bigframes.core.compile.schema_translator import bigframes.core.compile.single_column import bigframes.core.nodes as nodes import bigframes.core.ordering as bf_ordering -import bigframes.dtypes as bigframes_dtypes if typing.TYPE_CHECKING: import bigframes.core @@ -112,7 +112,9 @@ def compile_cached_table(node: nodes.CachedTableNode, ordered: bool = True): return compiled.OrderedIR( ibis_table, columns=tuple( - bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + ibis_table[col] + ) for col in node.schema.names ), ordering=node.ordering, @@ -123,7 +125,9 @@ def compile_cached_table(node: nodes.CachedTableNode, ordered: bool = True): return compiled.UnorderedIR( ibis_table, columns=tuple( - bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + ibis_table[col] + ) for col in node.schema.names ), ) @@ -166,7 +170,9 @@ def compile_read_table_unordered(node: nodes.ReadTableNode): return compiled.UnorderedIR( ibis_table, tuple( - bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + ibis_table[col] + ) for col in node.schema.names ), ) @@ -202,7 +208,9 @@ def compile_read_table_ordered(node: nodes.ReadTableNode): return compiled.OrderedIR( ibis_table, columns=tuple( - bigframes_dtypes.ibis_value_to_canonical_type(ibis_table[col]) + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + ibis_table[col] + ) for col in node.schema.names ), ordering=ordering, diff --git a/bigframes/core/compile/ibis_types.py b/bigframes/core/compile/ibis_types.py new file mode 100644 index 00000000000..f73fce3e4db --- /dev/null +++ b/bigframes/core/compile/ibis_types.py @@ -0,0 +1,476 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import textwrap +from typing import Any, cast, Dict, Iterable, Optional, Tuple, Union +import warnings + +import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes +import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +import geopandas as gpd # type: ignore +import google.cloud.bigquery as bigquery +import ibis +import ibis.expr.datatypes as ibis_dtypes +from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type +import ibis.expr.types as ibis_types +import numpy as np +import pandas as pd +import pyarrow as pa + +import bigframes.constants as constants +import bigframes.dtypes + +# Type hints for Ibis data types supported by BigQuery DataFrame +IbisDtype = Union[ + ibis_dtypes.Boolean, + ibis_dtypes.Float64, + ibis_dtypes.Int64, + ibis_dtypes.String, + ibis_dtypes.Date, + ibis_dtypes.Time, + ibis_dtypes.Timestamp, +] + + +BIDIRECTIONAL_MAPPINGS: Iterable[Tuple[IbisDtype, bigframes.dtypes.Dtype]] = ( + (ibis_dtypes.boolean, pd.BooleanDtype()), + (ibis_dtypes.date, pd.ArrowDtype(pa.date32())), + (ibis_dtypes.float64, pd.Float64Dtype()), + (ibis_dtypes.int64, pd.Int64Dtype()), + (ibis_dtypes.string, pd.StringDtype(storage="pyarrow")), + (ibis_dtypes.time, pd.ArrowDtype(pa.time64("us"))), + (ibis_dtypes.Timestamp(timezone=None), pd.ArrowDtype(pa.timestamp("us"))), + ( + ibis_dtypes.Timestamp(timezone="UTC"), + pd.ArrowDtype(pa.timestamp("us", tz="UTC")), + ), + (ibis_dtypes.binary, pd.ArrowDtype(pa.binary())), + ( + ibis_dtypes.Decimal(precision=38, scale=9, nullable=True), + pd.ArrowDtype(pa.decimal128(38, 9)), + ), + ( + ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), + pd.ArrowDtype(pa.decimal256(76, 38)), + ), + ( + ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True), + gpd.array.GeometryDtype(), + ), +) + +BIGFRAMES_TO_IBIS: Dict[bigframes.dtypes.Dtype, ibis_dtypes.DataType] = { + pandas: ibis for ibis, pandas in BIDIRECTIONAL_MAPPINGS +} + +IBIS_TO_ARROW: Dict[ibis_dtypes.DataType, pa.DataType] = { + ibis_dtypes.boolean: pa.bool_(), + ibis_dtypes.date: pa.date32(), + ibis_dtypes.float64: pa.float64(), + ibis_dtypes.int64: pa.int64(), + ibis_dtypes.string: pa.string(), + ibis_dtypes.time: pa.time64("us"), + ibis_dtypes.Timestamp(timezone=None): pa.timestamp("us"), + ibis_dtypes.Timestamp(timezone="UTC"): pa.timestamp("us", tz="UTC"), + ibis_dtypes.binary: pa.binary(), + ibis_dtypes.Decimal(precision=38, scale=9, nullable=True): pa.decimal128(38, 9), + ibis_dtypes.Decimal(precision=76, scale=38, nullable=True): pa.decimal256(76, 38), +} + +ARROW_TO_IBIS = {arrow: ibis for ibis, arrow in IBIS_TO_ARROW.items()} + +IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, bigframes.dtypes.Dtype] = { + ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS +} +# Allow REQUIRED fields to map correctly. +IBIS_TO_BIGFRAMES.update( + {ibis.copy(nullable=False): pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS} +) +IBIS_TO_BIGFRAMES.update( + { + # TODO: Interval + } +) + + +def cast_ibis_value( + value: ibis_types.Value, to_type: ibis_dtypes.DataType +) -> ibis_types.Value: + """Perform compatible type casts of ibis values + + Args: + value: + Ibis value, which could be a literal, scalar, or column + + to_type: + The Ibis type to cast to + + Returns: + A new Ibis value of type to_type + + Raises: + TypeError: if the type cast cannot be executed""" + if value.type() == to_type: + return value + # casts that just work + # TODO(bmil): add to this as more casts are verified + good_casts = { + ibis_dtypes.bool: (ibis_dtypes.int64,), + ibis_dtypes.int64: ( + ibis_dtypes.bool, + ibis_dtypes.float64, + ibis_dtypes.string, + ibis_dtypes.Decimal(precision=38, scale=9), + ibis_dtypes.Decimal(precision=76, scale=38), + ibis_dtypes.time, + ibis_dtypes.timestamp, + ibis_dtypes.Timestamp(timezone="UTC"), + ), + ibis_dtypes.float64: ( + ibis_dtypes.string, + ibis_dtypes.int64, + ibis_dtypes.Decimal(precision=38, scale=9), + ibis_dtypes.Decimal(precision=76, scale=38), + ), + ibis_dtypes.string: ( + ibis_dtypes.int64, + ibis_dtypes.float64, + ibis_dtypes.Decimal(precision=38, scale=9), + ibis_dtypes.Decimal(precision=76, scale=38), + ibis_dtypes.binary, + ibis_dtypes.date, + ibis_dtypes.timestamp, + ibis_dtypes.Timestamp(timezone="UTC"), + ), + ibis_dtypes.date: ( + ibis_dtypes.string, + ibis_dtypes.timestamp, + ibis_dtypes.Timestamp(timezone="UTC"), + ), + ibis_dtypes.Decimal(precision=38, scale=9): ( + ibis_dtypes.float64, + ibis_dtypes.Decimal(precision=76, scale=38), + ), + ibis_dtypes.Decimal(precision=76, scale=38): ( + ibis_dtypes.float64, + ibis_dtypes.Decimal(precision=38, scale=9), + ), + ibis_dtypes.time: ( + ibis_dtypes.int64, + ibis_dtypes.string, + ), + ibis_dtypes.timestamp: ( + ibis_dtypes.date, + ibis_dtypes.int64, + ibis_dtypes.string, + ibis_dtypes.time, + ibis_dtypes.Timestamp(timezone="UTC"), + ), + ibis_dtypes.Timestamp(timezone="UTC"): ( + ibis_dtypes.date, + ibis_dtypes.int64, + ibis_dtypes.string, + ibis_dtypes.time, + ibis_dtypes.timestamp, + ), + ibis_dtypes.binary: (ibis_dtypes.string,), + } + + value = ibis_value_to_canonical_type(value) + if value.type() in good_casts: + if to_type in good_casts[value.type()]: + return value.cast(to_type) + else: + # this should never happen + raise TypeError( + f"Unexpected value type {value.type()}. {constants.FEEDBACK_LINK}" + ) + + # casts that need some encouragement + + # BigQuery casts bools to lower case strings. Capitalize the result to match Pandas + # TODO(bmil): remove this workaround after fixing Ibis + if value.type() == ibis_dtypes.bool and to_type == ibis_dtypes.string: + return cast(ibis_types.StringValue, value.cast(to_type)).capitalize() + + if value.type() == ibis_dtypes.bool and to_type == ibis_dtypes.float64: + return value.cast(ibis_dtypes.int64).cast(ibis_dtypes.float64) + + if value.type() == ibis_dtypes.float64 and to_type == ibis_dtypes.bool: + return value != ibis_types.literal(0) + + raise TypeError( + f"Unsupported cast {value.type()} to {to_type}. {constants.FEEDBACK_LINK}" + ) + + +def ibis_value_to_canonical_type(value: ibis_types.Value) -> ibis_types.Value: + """Converts an Ibis expression to canonical type. + + This is useful in cases where multiple types correspond to the same BigFrames dtype. + """ + ibis_type = value.type() + name = value.get_name() + if ibis_type.is_json(): + value = vendored_ibis_ops.ToJsonString(value).to_expr() + return value.name(name) + # Allow REQUIRED fields to be joined with NULLABLE fields. + nullable_type = ibis_type.copy(nullable=True) + return value.cast(nullable_type).name(name) + + +def bigframes_dtype_to_ibis_dtype( + bigframes_dtype: Union[ + bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype, np.dtype[Any] + ] +) -> ibis_dtypes.DataType: + """Converts a BigQuery DataFrames supported dtype to an Ibis dtype. + + Args: + bigframes_dtype: + A dtype supported by BigQuery DataFrame + + Returns: + IbisDtype: The corresponding Ibis type + + Raises: + ValueError: If passed a dtype not supported by BigQuery DataFrames. + """ + if isinstance(bigframes_dtype, pd.ArrowDtype): + return _arrow_dtype_to_ibis_dtype(bigframes_dtype.pyarrow_dtype) + + type_string = str(bigframes_dtype) + if type_string in bigframes.dtypes.BIGFRAMES_STRING_TO_BIGFRAMES: + bigframes_dtype = bigframes.dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[ + cast(bigframes.dtypes.DtypeString, type_string) + ] + else: + raise ValueError( + textwrap.dedent( + f""" + Unexpected data type {bigframes_dtype}. The following + str dtypes are supppted: 'boolean','Float64','Int64', + 'int64[pyarrow]','string','string[pyarrow]', + 'timestamp[us, tz=UTC][pyarrow]','timestamp[us][pyarrow]', + 'date32[day][pyarrow]','time64[us][pyarrow]'. + The following pandas.ExtensionDtype are supported: + pandas.BooleanDtype(), pandas.Float64Dtype(), + pandas.Int64Dtype(), pandas.StringDtype(storage="pyarrow"), + pd.ArrowDtype(pa.date32()), pd.ArrowDtype(pa.time64("us")), + pd.ArrowDtype(pa.timestamp("us")), + pd.ArrowDtype(pa.timestamp("us", tz="UTC")). + {constants.FEEDBACK_LINK} + """ + ) + ) + + return BIGFRAMES_TO_IBIS[bigframes_dtype] + + +def ibis_dtype_to_bigframes_dtype( + ibis_dtype: ibis_dtypes.DataType, +) -> bigframes.dtypes.Dtype: + """Converts an Ibis dtype to a BigQuery DataFrames dtype + + Args: + ibis_dtype: The ibis dtype used to represent this type, which + should in turn correspond to an underlying BigQuery type + + Returns: + The supported BigQuery DataFrames dtype, which may be provided by + pandas, numpy, or db_types + + Raises: + ValueError: if passed an unexpected type + """ + # Special cases: Ibis supports variations on these types, but currently + # our IO returns them as objects. Eventually, we should support them as + # ArrowDType (and update the IO accordingly) + if isinstance(ibis_dtype, ibis_dtypes.Array): + return pd.ArrowDtype(_ibis_dtype_to_arrow_dtype(ibis_dtype)) + + if isinstance(ibis_dtype, ibis_dtypes.Struct): + return pd.ArrowDtype(_ibis_dtype_to_arrow_dtype(ibis_dtype)) + + # BigQuery only supports integers of size 64 bits. + if isinstance(ibis_dtype, ibis_dtypes.Integer): + return pd.Int64Dtype() + + # Temporary: Will eventually support an explicit json type instead of casting to string. + if isinstance(ibis_dtype, ibis_dtypes.JSON): + warnings.warn( + "Interpreting JSON as string. This behavior may change in future versions.", + bigframes.exceptions.PreviewWarning, + ) + return bigframes.dtypes.STRING_DTYPE + + if ibis_dtype in IBIS_TO_BIGFRAMES: + return IBIS_TO_BIGFRAMES[ibis_dtype] + elif isinstance(ibis_dtype, ibis_dtypes.Decimal): + # Temporary workaround for ibis decimal issue (b/323387826) + if ibis_dtype.precision >= 76: + return pd.ArrowDtype(pa.decimal256(76, 38)) + else: + return pd.ArrowDtype(pa.decimal128(38, 9)) + elif isinstance(ibis_dtype, ibis_dtypes.Null): + # Fallback to STRING for NULL values for most flexibility in SQL. + return IBIS_TO_BIGFRAMES[ibis_dtypes.string] + else: + raise ValueError( + f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" + ) + + +def _ibis_dtype_to_arrow_dtype(ibis_dtype: ibis_dtypes.DataType) -> pa.DataType: + """Private utility to convert ibis dtype to equivalent arrow type.""" + if isinstance(ibis_dtype, ibis_dtypes.Array): + return pa.list_( + _ibis_dtype_to_arrow_dtype(ibis_dtype.value_type.copy(nullable=True)) + ) + + if isinstance(ibis_dtype, ibis_dtypes.Struct): + return pa.struct( + [ + (name, _ibis_dtype_to_arrow_dtype(dtype)) + for name, dtype in ibis_dtype.fields.items() + ] + ) + + if ibis_dtype in IBIS_TO_ARROW: + return IBIS_TO_ARROW[ibis_dtype] + else: + raise ValueError( + f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" + ) + + +def _arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType: + if pa.types.is_struct(arrow_dtype): + struct_dtype = cast(pa.StructType, arrow_dtype) + return ibis_dtypes.Struct.from_tuples( + [ + (field.name, _arrow_dtype_to_ibis_dtype(field.type)) + for field in struct_dtype + ] + ) + + if arrow_dtype in ARROW_TO_IBIS: + return ARROW_TO_IBIS[arrow_dtype] + if arrow_dtype == pa.null(): + # Used for empty local dataframes where pyarrow has null type + return ibis_dtypes.float64 + else: + raise ValueError( + f"Unexpected Arrow data type {arrow_dtype}. {constants.FEEDBACK_LINK}" + ) + + +def literal_to_ibis_scalar( + literal, force_dtype: Optional[bigframes.dtypes.Dtype] = None, validate: bool = True +): + """Accept any literal and, if possible, return an Ibis Scalar + expression with a BigQuery DataFrames compatible data type + + Args: + literal: + any value accepted by Ibis + force_dtype: + force the value to a specific dtype + validate: + If true, will raise ValueError if type cannot be stored in a + BigQuery DataFrames object. If used as a subexpression, this should + be disabled. + + Returns: + An ibis Scalar supported by BigQuery DataFrame + + Raises: + ValueError: if passed literal cannot be coerced to a + BigQuery DataFrames compatible scalar + """ + # Special case: Can create nulls for non-bidirectional types + if (force_dtype == gpd.array.GeometryDtype()) and pd.isna(literal): + # Ibis has bug for casting nulltype to geospatial, so we perform intermediate cast first + geotype = ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True) + return ibis.literal(None, geotype) + ibis_dtype = BIGFRAMES_TO_IBIS[force_dtype] if force_dtype else None + + if pd.api.types.is_list_like(literal): + if validate: + raise ValueError( + f"List types can't be stored in BigQuery DataFrames. {constants.FEEDBACK_LINK}" + ) + # "correct" way would be to use ibis.array, but this produces invalid BQ SQL syntax + return tuple(literal) + if not pd.api.types.is_list_like(literal) and pd.isna(literal): + if ibis_dtype: + return ibis.null().cast(ibis_dtype) + else: + return ibis.null() + + scalar_expr = ibis.literal(literal) + if ibis_dtype: + scalar_expr = ibis.literal(literal, ibis_dtype) + elif scalar_expr.type().is_floating(): + scalar_expr = ibis.literal(literal, ibis_dtypes.float64) + elif scalar_expr.type().is_integer(): + scalar_expr = ibis.literal(literal, ibis_dtypes.int64) + elif scalar_expr.type().is_decimal(): + precision = scalar_expr.type().precision + scale = scalar_expr.type().scale + if (not precision and not scale) or ( + precision and scale and scale <= 9 and precision + (9 - scale) <= 38 + ): + scalar_expr = ibis.literal( + literal, ibis_dtypes.decimal(precision=38, scale=9) + ) + elif precision and scale and scale <= 38 and precision + (38 - scale) <= 76: + scalar_expr = ibis.literal( + literal, ibis_dtypes.decimal(precision=76, scale=38) + ) + else: + raise TypeError( + "BigQuery's decimal data type supports a maximum precision of 76 and a maximum scale of 38." + f"Current precision: {precision}. Current scale: {scale}" + ) + + # TODO(bmil): support other literals that can be coerced to compatible types + if validate and (scalar_expr.type() not in BIGFRAMES_TO_IBIS.values()): + raise ValueError( + f"Literal did not coerce to a supported data type: {scalar_expr.type()}. {constants.FEEDBACK_LINK}" + ) + + return scalar_expr + + +class UnsupportedTypeError(ValueError): + def __init__(self, type_, supported_types): + self.type = type_ + self.supported_types = supported_types + + +def ibis_type_from_python_type(t: type) -> ibis_dtypes.DataType: + if t not in bigframes.dtypes.RF_SUPPORTED_IO_PYTHON_TYPES: + raise UnsupportedTypeError(t, bigframes.dtypes.RF_SUPPORTED_IO_PYTHON_TYPES) + return python_type_to_bigquery_type(t) + + +def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> ibis_dtypes.DataType: + """Convert bq type to ibis. Only to be used for remote functions, does not handle all types.""" + if tk not in bigframes.dtypes.RF_SUPPORTED_IO_BIGQUERY_TYPEKINDS: + raise UnsupportedTypeError( + tk, bigframes.dtypes.RF_SUPPORTED_IO_BIGQUERY_TYPEKINDS + ) + return third_party_ibis_bqtypes.BigQueryType.to_ibis(tk) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 000c4a4c091..6b8e60434ee 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -27,6 +27,7 @@ import pandas as pd import bigframes.constants as constants +import bigframes.core.compile.ibis_types import bigframes.core.expression as ex import bigframes.dtypes import bigframes.operations as ops @@ -78,7 +79,7 @@ def _( expression: ex.ScalarConstantExpression, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: - return bigframes.dtypes.literal_to_ibis_scalar( + return bigframes.core.compile.ibis_types.literal_to_ibis_scalar( expression.value, expression.dtype ) @@ -771,14 +772,16 @@ def numeric_to_datetime(x: ibis_types.Value, unit: str) -> ibis_types.TimestampV @scalar_op_compiler.register_unary_op(ops.AsTypeOp, pass_op=True) def astype_op_impl(x: ibis_types.Value, op: ops.AsTypeOp): - to_type = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(op.to_type) + to_type = bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype( + op.to_type + ) if isinstance(x, ibis_types.NullScalar): return ibis_types.null().cast(to_type) # When casting DATETIME column into INT column, we need to convert the column into TIMESTAMP first. if to_type == ibis_dtypes.int64 and x.type() == ibis_dtypes.timestamp: x_converted = x.cast(ibis_dtypes.Timestamp(timezone="UTC")) - return bigframes.dtypes.cast_ibis_value(x_converted, to_type) + return bigframes.core.compile.ibis_types.cast_ibis_value(x_converted, to_type) if to_type == ibis_dtypes.int64 and x.type() == ibis_dtypes.time: # The conversion unit is set to "us" (microseconds) for consistency @@ -798,7 +801,7 @@ def astype_op_impl(x: ibis_types.Value, op: ops.AsTypeOp): elif to_type == ibis_dtypes.time: return x_converted.time() - return bigframes.dtypes.cast_ibis_value(x, to_type) + return bigframes.core.compile.ibis_types.cast_ibis_value(x, to_type) @scalar_op_compiler.register_unary_op(ops.IsInOp, pass_op=True) @@ -1013,15 +1016,7 @@ def add_op( ): if isinstance(x, ibis_types.NullScalar) or isinstance(x, ibis_types.NullScalar): return ibis.null() - try: - # Could be string concatenation or numeric addition. - return x + y # type: ignore - except ibis.common.annotations.SignatureValidationError as exc: - left_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(x.type()) - right_type = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(y.type()) - raise TypeError( - f"Cannot add {repr(left_type)} and {repr(right_type)}. {constants.FEEDBACK_LINK}" - ) from exc + return x + y # type: ignore @scalar_op_compiler.register_binary_op(ops.sub_op) diff --git a/bigframes/core/compile/schema_translator.py b/bigframes/core/compile/schema_translator.py index 03e9691af64..d19c1bfb86e 100644 --- a/bigframes/core/compile/schema_translator.py +++ b/bigframes/core/compile/schema_translator.py @@ -18,6 +18,7 @@ import ibis import ibis.expr.schema +import bigframes.core.compile.ibis_types import bigframes.core.schema as bf_schema import bigframes.dtypes @@ -28,7 +29,7 @@ def convert_bf_schema(schema: bf_schema.ArraySchema) -> ibis.expr.schema.Schema: """ names = schema.names types = [ - bigframes.dtypes.bigframes_dtype_to_ibis_dtype(bf_type) + bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype(bf_type) for bf_type in schema.dtypes ] return ibis.schema(names=names, types=types) diff --git a/bigframes/core/local_data.py b/bigframes/core/local_data.py index 8b256be6d2b..ac658d1bb8f 100644 --- a/bigframes/core/local_data.py +++ b/bigframes/core/local_data.py @@ -46,8 +46,8 @@ def adapt_pa_table(arrow_table: pa.Table) -> pa.Table: def bigframes_type_for_arrow_type(pa_type: pa.DataType) -> bigframes.dtypes.Dtype: - return bigframes.dtypes.ibis_dtype_to_bigframes_dtype( - bigframes.dtypes.arrow_dtype_to_ibis_dtype(arrow_type_replacements(pa_type)) + return bigframes.dtypes.arrow_dtype_to_bigframes_dtype( + arrow_type_replacements(pa_type) ) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 3b82401e230..f9bbe8c5155 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -324,10 +324,12 @@ def roots(self) -> typing.Set[BigFrameNode]: @functools.cached_property def schema(self) -> schemata.ArraySchema: + from bigframes.core.compile.ibis_types import ibis_dtype_to_bigframes_dtype + items = tuple( schemata.SchemaItem( value.get_name(), - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(value.type()), + ibis_dtype_to_bigframes_dtype(value.type()), ) for value in self.columns ) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 3df67ed9e40..d70f6f930e6 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -16,25 +16,17 @@ import datetime import decimal -import textwrap import typing -from typing import Any, Dict, Iterable, Literal, Tuple, Union -import warnings +from typing import Any, Dict, Literal, Union import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes -import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import geopandas as gpd # type: ignore -import google.cloud.bigquery as bigquery import ibis -import ibis.expr.datatypes as ibis_dtypes -from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type -import ibis.expr.types as ibis_types import numpy as np import pandas as pd import pyarrow as pa import bigframes.constants as constants -import bigframes.exceptions # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ @@ -81,17 +73,6 @@ "binary[pyarrow]", ] -# Type hints for Ibis data types supported by BigQuery DataFrame -IbisDtype = Union[ - ibis_dtypes.Boolean, - ibis_dtypes.Float64, - ibis_dtypes.Int64, - ibis_dtypes.String, - ibis_dtypes.Date, - ibis_dtypes.Time, - ibis_dtypes.Timestamp, -] - BOOL_BIGFRAMES_TYPES = [pd.BooleanDtype()] # Corresponds to the pandas concept of numeric type (such as when 'numeric_only' is specified in an operation) @@ -170,68 +151,23 @@ def is_bool_coercable(type: ExpressionType) -> bool: return (type is None) or is_numeric(type) or is_string_like(type) -BIDIRECTIONAL_MAPPINGS: Iterable[Tuple[IbisDtype, Dtype]] = ( - (ibis_dtypes.boolean, pd.BooleanDtype()), - (ibis_dtypes.date, pd.ArrowDtype(pa.date32())), - (ibis_dtypes.float64, pd.Float64Dtype()), - (ibis_dtypes.int64, pd.Int64Dtype()), - (ibis_dtypes.string, pd.StringDtype(storage="pyarrow")), - (ibis_dtypes.time, pd.ArrowDtype(pa.time64("us"))), - (ibis_dtypes.Timestamp(timezone=None), pd.ArrowDtype(pa.timestamp("us"))), - ( - ibis_dtypes.Timestamp(timezone="UTC"), - pd.ArrowDtype(pa.timestamp("us", tz="UTC")), - ), - (ibis_dtypes.binary, pd.ArrowDtype(pa.binary())), - ( - ibis_dtypes.Decimal(precision=38, scale=9, nullable=True), - pd.ArrowDtype(pa.decimal128(38, 9)), - ), - ( - ibis_dtypes.Decimal(precision=76, scale=38, nullable=True), - pd.ArrowDtype(pa.decimal256(76, 38)), - ), - ( - ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True), - gpd.array.GeometryDtype(), - ), -) - -BIGFRAMES_TO_IBIS: Dict[Dtype, ibis_dtypes.DataType] = { - pandas: ibis for ibis, pandas in BIDIRECTIONAL_MAPPINGS -} - -IBIS_TO_ARROW: Dict[ibis_dtypes.DataType, pa.DataType] = { - ibis_dtypes.boolean: pa.bool_(), - ibis_dtypes.date: pa.date32(), - ibis_dtypes.float64: pa.float64(), - ibis_dtypes.int64: pa.int64(), - ibis_dtypes.string: pa.string(), - ibis_dtypes.time: pa.time64("us"), - ibis_dtypes.Timestamp(timezone=None): pa.timestamp("us"), - ibis_dtypes.Timestamp(timezone="UTC"): pa.timestamp("us", tz="UTC"), - ibis_dtypes.binary: pa.binary(), - ibis_dtypes.Decimal(precision=38, scale=9, nullable=True): pa.decimal128(38, 9), - ibis_dtypes.Decimal(precision=76, scale=38, nullable=True): pa.decimal256(76, 38), -} - -ARROW_TO_IBIS = {arrow: ibis for ibis, arrow in IBIS_TO_ARROW.items()} - -IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, Dtype] = { - ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS -} -# Allow REQUIRED fields to map correctly. -IBIS_TO_BIGFRAMES.update( - {ibis.copy(nullable=False): pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS} -) -IBIS_TO_BIGFRAMES.update( - { - # TODO: Interval - } +_ALL_DTYPES = ( + pd.BooleanDtype(), + pd.ArrowDtype(pa.date32()), + pd.Float64Dtype(), + pd.Int64Dtype(), + pd.StringDtype(storage="pyarrow"), + pd.ArrowDtype(pa.time64("us")), + pd.ArrowDtype(pa.timestamp("us")), + pd.ArrowDtype(pa.timestamp("us", tz="UTC")), + pd.ArrowDtype(pa.binary()), + pd.ArrowDtype(pa.decimal128(38, 9)), + pd.ArrowDtype(pa.decimal256(76, 38)), + gpd.array.GeometryDtype(), ) BIGFRAMES_STRING_TO_BIGFRAMES: Dict[DtypeString, Dtype] = { - typing.cast(DtypeString, dtype.name): dtype for dtype in BIGFRAMES_TO_IBIS.keys() + typing.cast(DtypeString, dtype.name): dtype for dtype in _ALL_DTYPES } # special case - string[pyarrow] doesn't include the storage in its name, and both @@ -262,362 +198,27 @@ def dtype_for_etype(etype: ExpressionType) -> Dtype: return etype -def ibis_dtype_to_bigframes_dtype( - ibis_dtype: ibis_dtypes.DataType, -) -> Dtype: - """Converts an Ibis dtype to a BigQuery DataFrames dtype - - Args: - ibis_dtype: The ibis dtype used to represent this type, which - should in turn correspond to an underlying BigQuery type - - Returns: - The supported BigQuery DataFrames dtype, which may be provided by - pandas, numpy, or db_types - - Raises: - ValueError: if passed an unexpected type - """ - # Special cases: Ibis supports variations on these types, but currently - # our IO returns them as objects. Eventually, we should support them as - # ArrowDType (and update the IO accordingly) - if isinstance(ibis_dtype, ibis_dtypes.Array): - return pd.ArrowDtype(ibis_dtype_to_arrow_dtype(ibis_dtype)) - - if isinstance(ibis_dtype, ibis_dtypes.Struct): - return pd.ArrowDtype(ibis_dtype_to_arrow_dtype(ibis_dtype)) - - # BigQuery only supports integers of size 64 bits. - if isinstance(ibis_dtype, ibis_dtypes.Integer): - return pd.Int64Dtype() - - # Temporary: Will eventually support an explicit json type instead of casting to string. - if isinstance(ibis_dtype, ibis_dtypes.JSON): - warnings.warn( - "Interpreting JSON as string. This behavior may change in future versions.", - bigframes.exceptions.PreviewWarning, - ) - return STRING_DTYPE - - if ibis_dtype in IBIS_TO_BIGFRAMES: - return IBIS_TO_BIGFRAMES[ibis_dtype] - elif isinstance(ibis_dtype, ibis_dtypes.Decimal): - # Temporary workaround for ibis decimal issue (b/323387826) - if ibis_dtype.precision >= 76: - return pd.ArrowDtype(pa.decimal256(76, 38)) - else: - return pd.ArrowDtype(pa.decimal128(38, 9)) - elif isinstance(ibis_dtype, ibis_dtypes.Null): - # Fallback to STRING for NULL values for most flexibility in SQL. - return IBIS_TO_BIGFRAMES[ibis_dtypes.string] - else: - raise ValueError( - f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" - ) - - -def ibis_dtype_to_arrow_dtype(ibis_dtype: ibis_dtypes.DataType) -> pa.DataType: - if isinstance(ibis_dtype, ibis_dtypes.Array): - return pa.list_( - ibis_dtype_to_arrow_dtype(ibis_dtype.value_type.copy(nullable=True)) - ) - - if isinstance(ibis_dtype, ibis_dtypes.Struct): - return pa.struct( - [ - (name, ibis_dtype_to_arrow_dtype(dtype)) - for name, dtype in ibis_dtype.fields.items() - ] - ) - - if ibis_dtype in IBIS_TO_ARROW: - return IBIS_TO_ARROW[ibis_dtype] - else: - raise ValueError( - f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" - ) - - -def ibis_value_to_canonical_type(value: ibis_types.Value) -> ibis_types.Value: - """Converts an Ibis expression to canonical type. - - This is useful in cases where multiple types correspond to the same BigFrames dtype. - """ - ibis_type = value.type() - name = value.get_name() - if ibis_type.is_json(): - value = vendored_ibis_ops.ToJsonString(value).to_expr() - return value.name(name) - # Allow REQUIRED fields to be joined with NULLABLE fields. - nullable_type = ibis_type.copy(nullable=True) - return value.cast(nullable_type).name(name) - - -def arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType: - if pa.types.is_struct(arrow_dtype): - struct_dtype = typing.cast(pa.StructType, arrow_dtype) - return ibis_dtypes.Struct.from_tuples( - [ - (field.name, arrow_dtype_to_ibis_dtype(field.type)) - for field in struct_dtype - ] - ) - - if arrow_dtype in ARROW_TO_IBIS: - return ARROW_TO_IBIS[arrow_dtype] - if arrow_dtype == pa.null(): - # Used for empty local dataframes where pyarrow has null type - return ibis_dtypes.float64 - else: - raise ValueError( - f"Unexpected Arrow data type {arrow_dtype}. {constants.FEEDBACK_LINK}" - ) - - def arrow_dtype_to_bigframes_dtype(arrow_dtype: pa.DataType) -> Dtype: - return ibis_dtype_to_bigframes_dtype(arrow_dtype_to_ibis_dtype(arrow_dtype)) - - -def bigframes_dtype_to_ibis_dtype( - bigframes_dtype: Union[DtypeString, Dtype, np.dtype[Any]] -) -> ibis_dtypes.DataType: - """Converts a BigQuery DataFrames supported dtype to an Ibis dtype. - - Args: - bigframes_dtype: - A dtype supported by BigQuery DataFrame - - Returns: - IbisDtype: The corresponding Ibis type - - Raises: - ValueError: If passed a dtype not supported by BigQuery DataFrames. - """ - if isinstance(bigframes_dtype, pd.ArrowDtype): - return arrow_dtype_to_ibis_dtype(bigframes_dtype.pyarrow_dtype) - - type_string = str(bigframes_dtype) - if type_string in BIGFRAMES_STRING_TO_BIGFRAMES: - bigframes_dtype = BIGFRAMES_STRING_TO_BIGFRAMES[ - typing.cast(DtypeString, type_string) - ] - else: - raise ValueError( - textwrap.dedent( - f""" - Unexpected data type {bigframes_dtype}. The following - str dtypes are supppted: 'boolean','Float64','Int64', - 'int64[pyarrow]','string','string[pyarrow]', - 'timestamp[us, tz=UTC][pyarrow]','timestamp[us][pyarrow]', - 'date32[day][pyarrow]','time64[us][pyarrow]'. - The following pandas.ExtensionDtype are supported: - pandas.BooleanDtype(), pandas.Float64Dtype(), - pandas.Int64Dtype(), pandas.StringDtype(storage="pyarrow"), - pd.ArrowDtype(pa.date32()), pd.ArrowDtype(pa.time64("us")), - pd.ArrowDtype(pa.timestamp("us")), - pd.ArrowDtype(pa.timestamp("us", tz="UTC")). - {constants.FEEDBACK_LINK} - """ - ) - ) + # TODO: Directly convert instead of using ibis dtype as intermediate step + from bigframes.core.compile.ibis_types import ( + _arrow_dtype_to_ibis_dtype, + ibis_dtype_to_bigframes_dtype, + ) - return BIGFRAMES_TO_IBIS[bigframes_dtype] + return ibis_dtype_to_bigframes_dtype(_arrow_dtype_to_ibis_dtype(arrow_dtype)) def bigframes_dtype_to_arrow_dtype( bigframes_dtype: Union[DtypeString, Dtype, np.dtype[Any]] ) -> pa.DataType: - return ibis_dtype_to_arrow_dtype(bigframes_dtype_to_ibis_dtype(bigframes_dtype)) - - -def literal_to_ibis_scalar( - literal, force_dtype: typing.Optional[Dtype] = None, validate: bool = True -): - """Accept any literal and, if possible, return an Ibis Scalar - expression with a BigQuery DataFrames compatible data type - - Args: - literal: - any value accepted by Ibis - force_dtype: - force the value to a specific dtype - validate: - If true, will raise ValueError if type cannot be stored in a - BigQuery DataFrames object. If used as a subexpression, this should - be disabled. - - Returns: - An ibis Scalar supported by BigQuery DataFrame - - Raises: - ValueError: if passed literal cannot be coerced to a - BigQuery DataFrames compatible scalar - """ - # Special case: Can create nulls for non-bidirectional types - if (force_dtype == gpd.array.GeometryDtype()) and pd.isna(literal): - # Ibis has bug for casting nulltype to geospatial, so we perform intermediate cast first - geotype = ibis_dtypes.GeoSpatial(geotype="geography", srid=4326, nullable=True) - return ibis.literal(None, geotype) - ibis_dtype = BIGFRAMES_TO_IBIS[force_dtype] if force_dtype else None - - if pd.api.types.is_list_like(literal): - if validate: - raise ValueError( - f"List types can't be stored in BigQuery DataFrames. {constants.FEEDBACK_LINK}" - ) - # "correct" way would be to use ibis.array, but this produces invalid BQ SQL syntax - return tuple(literal) - if not pd.api.types.is_list_like(literal) and pd.isna(literal): - if ibis_dtype: - return ibis.null().cast(ibis_dtype) - else: - return ibis.null() - - scalar_expr = ibis.literal(literal) - if ibis_dtype: - scalar_expr = ibis.literal(literal, ibis_dtype) - elif scalar_expr.type().is_floating(): - scalar_expr = ibis.literal(literal, ibis_dtypes.float64) - elif scalar_expr.type().is_integer(): - scalar_expr = ibis.literal(literal, ibis_dtypes.int64) - elif scalar_expr.type().is_decimal(): - precision = scalar_expr.type().precision - scale = scalar_expr.type().scale - if (not precision and not scale) or ( - precision and scale and scale <= 9 and precision + (9 - scale) <= 38 - ): - scalar_expr = ibis.literal( - literal, ibis_dtypes.decimal(precision=38, scale=9) - ) - elif precision and scale and scale <= 38 and precision + (38 - scale) <= 76: - scalar_expr = ibis.literal( - literal, ibis_dtypes.decimal(precision=76, scale=38) - ) - else: - raise TypeError( - "BigQuery's decimal data type supports a maximum precision of 76 and a maximum scale of 38." - f"Current precision: {precision}. Current scale: {scale}" - ) - - # TODO(bmil): support other literals that can be coerced to compatible types - if validate and (scalar_expr.type() not in BIGFRAMES_TO_IBIS.values()): - raise ValueError( - f"Literal did not coerce to a supported data type: {scalar_expr.type()}. {constants.FEEDBACK_LINK}" - ) - - return scalar_expr - - -def cast_ibis_value( - value: ibis_types.Value, to_type: ibis_dtypes.DataType -) -> ibis_types.Value: - """Perform compatible type casts of ibis values - - Args: - value: - Ibis value, which could be a literal, scalar, or column - - to_type: - The Ibis type to cast to - - Returns: - A new Ibis value of type to_type - - Raises: - TypeError: if the type cast cannot be executed""" - if value.type() == to_type: - return value - # casts that just work - # TODO(bmil): add to this as more casts are verified - good_casts = { - ibis_dtypes.bool: (ibis_dtypes.int64,), - ibis_dtypes.int64: ( - ibis_dtypes.bool, - ibis_dtypes.float64, - ibis_dtypes.string, - ibis_dtypes.Decimal(precision=38, scale=9), - ibis_dtypes.Decimal(precision=76, scale=38), - ibis_dtypes.time, - ibis_dtypes.timestamp, - ibis_dtypes.Timestamp(timezone="UTC"), - ), - ibis_dtypes.float64: ( - ibis_dtypes.string, - ibis_dtypes.int64, - ibis_dtypes.Decimal(precision=38, scale=9), - ibis_dtypes.Decimal(precision=76, scale=38), - ), - ibis_dtypes.string: ( - ibis_dtypes.int64, - ibis_dtypes.float64, - ibis_dtypes.Decimal(precision=38, scale=9), - ibis_dtypes.Decimal(precision=76, scale=38), - ibis_dtypes.binary, - ibis_dtypes.date, - ibis_dtypes.timestamp, - ibis_dtypes.Timestamp(timezone="UTC"), - ), - ibis_dtypes.date: ( - ibis_dtypes.string, - ibis_dtypes.timestamp, - ibis_dtypes.Timestamp(timezone="UTC"), - ), - ibis_dtypes.Decimal(precision=38, scale=9): ( - ibis_dtypes.float64, - ibis_dtypes.Decimal(precision=76, scale=38), - ), - ibis_dtypes.Decimal(precision=76, scale=38): ( - ibis_dtypes.float64, - ibis_dtypes.Decimal(precision=38, scale=9), - ), - ibis_dtypes.time: ( - ibis_dtypes.int64, - ibis_dtypes.string, - ), - ibis_dtypes.timestamp: ( - ibis_dtypes.date, - ibis_dtypes.int64, - ibis_dtypes.string, - ibis_dtypes.time, - ibis_dtypes.Timestamp(timezone="UTC"), - ), - ibis_dtypes.Timestamp(timezone="UTC"): ( - ibis_dtypes.date, - ibis_dtypes.int64, - ibis_dtypes.string, - ibis_dtypes.time, - ibis_dtypes.timestamp, - ), - ibis_dtypes.binary: (ibis_dtypes.string,), - } - - value = ibis_value_to_canonical_type(value) - if value.type() in good_casts: - if to_type in good_casts[value.type()]: - return value.cast(to_type) - else: - # this should never happen - raise TypeError( - f"Unexpected value type {value.type()}. {constants.FEEDBACK_LINK}" - ) - - # casts that need some encouragement - - # BigQuery casts bools to lower case strings. Capitalize the result to match Pandas - # TODO(bmil): remove this workaround after fixing Ibis - if value.type() == ibis_dtypes.bool and to_type == ibis_dtypes.string: - return typing.cast(ibis_types.StringValue, value.cast(to_type)).capitalize() - - if value.type() == ibis_dtypes.bool and to_type == ibis_dtypes.float64: - return value.cast(ibis_dtypes.int64).cast(ibis_dtypes.float64) - - if value.type() == ibis_dtypes.float64 and to_type == ibis_dtypes.bool: - return value != ibis_types.literal(0) - - raise TypeError( - f"Unsupported cast {value.type()} to {to_type}. {constants.FEEDBACK_LINK}" + # TODO: Directly convert instead of using ibis dtype as intermediate step + from bigframes.core.compile.ibis_types import ( + _ibis_dtype_to_arrow_dtype, + bigframes_dtype_to_ibis_dtype, ) + return _ibis_dtype_to_arrow_dtype(bigframes_dtype_to_ibis_dtype(bigframes_dtype)) + def is_dtype(scalar: typing.Any, dtype: Dtype) -> bool: """Captures whether a scalar can be losslessly represented by a dtype.""" @@ -740,6 +341,11 @@ def infer_literal_type(literal) -> typing.Optional[Dtype]: if pd.isna(literal): return None # Null value without a definite type # Temporary logic, use ibis inferred type + from bigframes.core.compile.ibis_types import ( + ibis_dtype_to_bigframes_dtype, + literal_to_ibis_scalar, + ) + ibis_literal = literal_to_ibis_scalar(literal) return ibis_dtype_to_bigframes_dtype(ibis_literal.type()) @@ -748,15 +354,38 @@ def infer_literal_arrow_type(literal) -> typing.Optional[pa.DataType]: if pd.isna(literal): return None # Null value without a definite type # Temporary logic, use ibis inferred type + # TODO: Directly convert instead of using ibis dtype as intermediate step + from bigframes.core.compile.ibis_types import ( + _ibis_dtype_to_arrow_dtype, + literal_to_ibis_scalar, + ) + ibis_literal = literal_to_ibis_scalar(literal) - return ibis_dtype_to_arrow_dtype(ibis_literal.type()) + return _ibis_dtype_to_arrow_dtype(ibis_literal.type()) + + +def bf_type_from_type_kind(bf_schema) -> Dict[str, Dtype]: + """Converts bigquery sql type to the default bigframes dtype.""" + ibis_schema: ibis.Schema = third_party_ibis_bqtypes.BigQuerySchema.to_ibis( + bf_schema + ) + # TODO: Directly convert instead of using ibis dtype as intermediate step + from bigframes.core.compile.ibis_types import ibis_dtype_to_bigframes_dtype + return { + name: ibis_dtype_to_bigframes_dtype(type) for name, type in ibis_schema.items() + } + + +# Remote functions use only +# TODO: Refactor into remote function module # Input and output types supported by BigQuery DataFrames remote functions. # TODO(shobs): Extend the support to all types supported by BQ remote functions # https://cloud.google.com/bigquery/docs/remote-functions#limitations -SUPPORTED_IO_PYTHON_TYPES = {bool, float, int, str} -SUPPORTED_IO_BIGQUERY_TYPEKINDS = { +RF_SUPPORTED_IO_PYTHON_TYPES = {bool, float, int, str} + +RF_SUPPORTED_IO_BIGQUERY_TYPEKINDS = { "BOOLEAN", "BOOL", "FLOAT", @@ -765,32 +394,3 @@ def infer_literal_arrow_type(literal) -> typing.Optional[pa.DataType]: "INTEGER", "STRING", } - - -class UnsupportedTypeError(ValueError): - def __init__(self, type_, supported_types): - self.type = type_ - self.supported_types = supported_types - - -def ibis_type_from_python_type(t: type) -> ibis_dtypes.DataType: - if t not in SUPPORTED_IO_PYTHON_TYPES: - raise UnsupportedTypeError(t, SUPPORTED_IO_PYTHON_TYPES) - return python_type_to_bigquery_type(t) - - -def ibis_type_from_type_kind(tk: bigquery.StandardSqlTypeNames) -> ibis_dtypes.DataType: - """Convert bq type to ibis. Only to be used for remote functions, does not handle all types.""" - if tk not in SUPPORTED_IO_BIGQUERY_TYPEKINDS: - raise UnsupportedTypeError(tk, SUPPORTED_IO_BIGQUERY_TYPEKINDS) - return third_party_ibis_bqtypes.BigQueryType.to_ibis(tk) - - -def bf_type_from_type_kind(bf_schema) -> Dict[str, Dtype]: - """Converts bigquery sql type to the default bigframes dtype.""" - ibis_schema: ibis.Schema = third_party_ibis_bqtypes.BigQuerySchema.to_ibis( - bf_schema - ) - return { - name: ibis_dtype_to_bigframes_dtype(type) for name, type in ibis_schema.items() - } diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 5b4d7ad6ddd..7f3a023c560 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -59,7 +59,7 @@ from bigframes import clients import bigframes.constants as constants -import bigframes.dtypes +import bigframes.core.compile.ibis_types import bigframes.functions.remote_function_template logger = logging.getLogger(__name__) @@ -523,12 +523,16 @@ def ibis_signature_from_python_signature( input_types: Sequence[type], output_type: type, ) -> IbisSignature: + return IbisSignature( parameter_names=list(signature.parameters.keys()), input_types=[ - bigframes.dtypes.ibis_type_from_python_type(t) for t in input_types + bigframes.core.compile.ibis_types.ibis_type_from_python_type(t) + for t in input_types ], - output_type=bigframes.dtypes.ibis_type_from_python_type(output_type), + output_type=bigframes.core.compile.ibis_types.ibis_type_from_python_type( + output_type + ), ) @@ -536,6 +540,7 @@ class ReturnTypeMissingError(ValueError): pass +# TODO: Move this to compile folder def ibis_signature_from_routine(routine: bigquery.Routine) -> IbisSignature: if not routine.return_type: raise ReturnTypeMissingError @@ -543,12 +548,14 @@ def ibis_signature_from_routine(routine: bigquery.Routine) -> IbisSignature: return IbisSignature( parameter_names=[arg.name for arg in routine.arguments], input_types=[ - bigframes.dtypes.ibis_type_from_type_kind(arg.data_type.type_kind) + bigframes.core.compile.ibis_types.ibis_type_from_type_kind( + arg.data_type.type_kind + ) if arg.data_type else None for arg in routine.arguments ], - output_type=bigframes.dtypes.ibis_type_from_type_kind( + output_type=bigframes.core.compile.ibis_types.ibis_type_from_type_kind( routine.return_type.type_kind ), ) @@ -757,8 +764,9 @@ def remote_function( https://cloud.google.com/functions/docs/networking/connecting-vpc. """ # Some defaults may be used from the session if not provided otherwise + import bigframes.exceptions as bf_exceptions import bigframes.pandas as bpd - import bigframes.series + import bigframes.series as bf_series import bigframes.session session = cast(bigframes.session.Session, session or bpd.get_global_session()) @@ -896,13 +904,13 @@ def wrapper(func): # BigQuery DataFrames and pandas object types for compatibility. is_row_processor = False if len(input_types) == 1 and ( - (input_type := input_types[0]) == bigframes.series.Series + (input_type := input_types[0]) == bf_series.Series or input_type == pandas.Series ): warnings.warn( "input_types=Series is in preview.", stacklevel=1, - category=bigframes.exceptions.PreviewWarning, + category=bf_exceptions.PreviewWarning, ) # we will model the row as a json serialized string containing the data @@ -972,8 +980,11 @@ def try_delattr(attr): remote_function_client.get_cloud_function_fully_qualified_name(cf_name) ) func.bigframes_remote_function = str(dataset_ref.routine(rf_name)) # type: ignore - func.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( - ibis_signature.output_type + + func.output_dtype = ( + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( + ibis_signature.output_type + ) ) func.ibis_node = node return func @@ -1012,7 +1023,7 @@ def read_gbq_function( raise ValueError( f"Function return type must be specified. {constants.FEEDBACK_LINK}" ) - except bigframes.dtypes.UnsupportedTypeError as e: + except bigframes.core.compile.ibis_types.UnsupportedTypeError as e: raise ValueError( f"Type {e.type} not supported, supported types are {e.supported_types}. " f"{constants.FEEDBACK_LINK}" @@ -1028,6 +1039,7 @@ def func(*ignored_args, **ignored_kwargs): return ibis_client.execute(expr) # TODO: Move ibis logic to compiler step + func.__name__ = routine_ref.routine_id node = ibis.udf.scalar.builtin( @@ -1037,7 +1049,7 @@ def func(*ignored_args, **ignored_kwargs): signature=(ibis_signature.input_types, ibis_signature.output_type), ) func.bigframes_remote_function = str(routine_ref) # type: ignore - func.output_dtype = bigframes.dtypes.ibis_dtype_to_bigframes_dtype( # type: ignore + func.output_dtype = bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( # type: ignore ibis_signature.output_type ) func.ibis_node = node # type: ignore diff --git a/tests/unit/test_dtypes.py b/tests/unit/core/test_dtypes.py similarity index 86% rename from tests/unit/test_dtypes.py rename to tests/unit/core/test_dtypes.py index dabbf11c6c7..ae194be83fd 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/core/test_dtypes.py @@ -20,6 +20,7 @@ import pyarrow as pa # type: ignore import pytest +import bigframes.core.compile.ibis_types import bigframes.dtypes @@ -67,14 +68,14 @@ ) def test_ibis_dtype_converts(ibis_dtype, bigframes_dtype): """Test all the Ibis data types needed to read BigQuery tables""" - result = bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_dtype) + result = bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype(ibis_dtype) assert result == bigframes_dtype def test_ibis_timestamp_pst_raises_unexpected_datatype(): """BigQuery timestamp only supports UTC time""" with pytest.raises(ValueError, match="Unexpected Ibis data type"): - bigframes.dtypes.ibis_dtype_to_bigframes_dtype( + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( ibis_dtypes.Timestamp(timezone="PST") ) @@ -82,7 +83,9 @@ def test_ibis_timestamp_pst_raises_unexpected_datatype(): def test_ibis_float32_raises_unexpected_datatype(): """Other Ibis types not read from BigQuery are not expected""" with pytest.raises(ValueError, match="Unexpected Ibis data type"): - bigframes.dtypes.ibis_dtype_to_bigframes_dtype(ibis_dtypes.float32) + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( + ibis_dtypes.float32 + ) IBIS_ARROW_DTYPES = ( @@ -139,13 +142,13 @@ def test_ibis_float32_raises_unexpected_datatype(): @pytest.mark.parametrize(("ibis_dtype", "arrow_dtype"), IBIS_ARROW_DTYPES) def test_arrow_dtype_to_ibis_dtype(ibis_dtype, arrow_dtype): - result = bigframes.dtypes.arrow_dtype_to_ibis_dtype(arrow_dtype) + result = bigframes.core.compile.ibis_types._arrow_dtype_to_ibis_dtype(arrow_dtype) assert result == ibis_dtype @pytest.mark.parametrize(("ibis_dtype", "arrow_dtype"), IBIS_ARROW_DTYPES) def test_ibis_dtype_to_arrow_dtype(ibis_dtype, arrow_dtype): - result = bigframes.dtypes.ibis_dtype_to_arrow_dtype(ibis_dtype) + result = bigframes.core.compile.ibis_types._ibis_dtype_to_arrow_dtype(ibis_dtype) assert result == arrow_dtype @@ -178,7 +181,9 @@ def test_ibis_dtype_to_arrow_dtype(ibis_dtype, arrow_dtype): ) def test_bigframes_dtype_converts(ibis_dtype, bigframes_dtype): """Test all the Ibis data types needed to read BigQuery tables""" - result = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(bigframes_dtype) + result = bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype( + bigframes_dtype + ) assert result == ibis_dtype @@ -203,20 +208,22 @@ def test_bigframes_dtype_converts(ibis_dtype, bigframes_dtype): ) def test_bigframes_string_dtype_converts(ibis_dtype, bigframes_dtype_str): """Test all the Ibis data types needed to read BigQuery tables""" - result = bigframes.dtypes.bigframes_dtype_to_ibis_dtype(bigframes_dtype_str) + result = bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype( + bigframes_dtype_str + ) assert result == ibis_dtype def test_unsupported_dtype_raises_unexpected_datatype(): """Incompatible dtypes should fail when passed into BigQuery DataFrames""" with pytest.raises(ValueError, match="Unexpected data type"): - bigframes.dtypes.bigframes_dtype_to_ibis_dtype(np.float32) + bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype(np.float32) def test_unsupported_dtype_str_raises_unexpected_datatype(): """Incompatible dtypes should fail when passed into BigQuery DataFrames""" with pytest.raises(ValueError, match="Unexpected data type"): - bigframes.dtypes.bigframes_dtype_to_ibis_dtype("int64") + bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype("int64") @pytest.mark.parametrize( @@ -228,21 +235,23 @@ def test_unsupported_dtype_str_raises_unexpected_datatype(): ], ) def test_literal_to_ibis_scalar_converts(literal, ibis_scalar): - assert bigframes.dtypes.literal_to_ibis_scalar(literal).equals(ibis_scalar) + assert bigframes.core.compile.ibis_types.literal_to_ibis_scalar(literal).equals( + ibis_scalar + ) def test_literal_to_ibis_scalar_throws_on_incompatible_literal(): with pytest.raises( ValueError, ): - bigframes.dtypes.literal_to_ibis_scalar({"mykey": "myval"}) + bigframes.core.compile.ibis_types.literal_to_ibis_scalar({"mykey": "myval"}) def test_remote_function_io_types_are_supported_bigframes_types(): from ibis.expr.datatypes.core import dtype as python_type_to_bigquery_type - from bigframes.dtypes import SUPPORTED_IO_PYTHON_TYPES as rf_supported_io_types + from bigframes.dtypes import RF_SUPPORTED_IO_PYTHON_TYPES as rf_supported_io_types for python_type in rf_supported_io_types: ibis_type = python_type_to_bigquery_type(python_type) - assert ibis_type in bigframes.dtypes.IBIS_TO_BIGFRAMES + assert ibis_type in bigframes.core.compile.ibis_types.IBIS_TO_BIGFRAMES diff --git a/tests/unit/test_remote_function.py b/tests/unit/test_remote_function.py index 1bd3f3b14f1..6868e85b9c3 100644 --- a/tests/unit/test_remote_function.py +++ b/tests/unit/test_remote_function.py @@ -19,6 +19,7 @@ import pandas import pytest +import bigframes.core.compile.ibis_types import bigframes.dtypes import bigframes.functions.remote_function import bigframes.series @@ -62,11 +63,11 @@ def axis_1_function(myparam: series_type) -> str: # type: ignore def test_supported_types_correspond(): # The same types should be representable by the supported Python and BigQuery types. ibis_types_from_python = { - ibis_types.dtype(t) for t in bigframes.dtypes.SUPPORTED_IO_PYTHON_TYPES + ibis_types.dtype(t) for t in bigframes.dtypes.RF_SUPPORTED_IO_PYTHON_TYPES } ibis_types_from_bigquery = { third_party_ibis_bqtypes.BigQueryType.to_ibis(tk) - for tk in bigframes.dtypes.SUPPORTED_IO_BIGQUERY_TYPEKINDS + for tk in bigframes.dtypes.RF_SUPPORTED_IO_BIGQUERY_TYPEKINDS } assert ibis_types_from_python == ibis_types_from_bigquery From d751f5cd1cf578618eabbb992cfb6b0a3c36608c Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:20:17 -0700 Subject: [PATCH 629/921] feat: support fit() in GeminiTextGenerator (#758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes internal #343765747🦕 --- bigframes/ml/llm.py | 53 +++++++++++++++++++++++++++++++++++ tests/system/load/test_llm.py | 27 ++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index f62867cdd57..2517178d890 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -571,6 +571,8 @@ class GeminiTextGenerator(base.BaseEstimator): Connection to connect with remote service. str of the format ... If None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach permission if the connection isn't fully set up. + max_iterations (Optional[int], Default to 300): + The number of steps to run when performing supervised tuning. """ def __init__( @@ -581,9 +583,11 @@ def __init__( ] = "gemini-pro", session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, + max_iterations: int = 300, ): self.model_name = model_name self.session = session or bpd.get_global_session() + self.max_iterations = max_iterations self._bq_connection_manager = self.session.bqconnectionmanager connection_name = connection_name or self.session._bq_connection @@ -647,6 +651,55 @@ def _from_bq( model._bqml_model = core.BqmlModel(session, bq_model) return model + @property + def _bqml_options(self) -> dict: + """The model options as they will be set for BQML""" + options = { + "max_iterations": self.max_iterations, + "data_split_method": "NO_SPLIT", + } + return options + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series], + ) -> GeminiTextGenerator: + """Fine tune GeminiTextGenerator model. Only support "gemini-pro" model for now. + + .. note:: + + This product or feature 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 + (https://cloud.google.com/products#product-launch-stages). + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + DataFrame of shape (n_samples, n_features). Training data. + y (bigframes.dataframe.DataFrame or bigframes.series.Series: + Training labels. + + Returns: + GeminiTextGenerator: Fitted estimator. + """ + if self._bqml_model.model_name.startswith("gemini-1.5"): + raise NotImplementedError("Fit is not supported for gemini-1.5 model.") + + X, y = utils.convert_to_dataframe(X, y) + + options = self._bqml_options + options["endpoint"] = "gemini-1.0-pro-002" + options["prompt_col"] = X.columns.tolist()[0] + + self._bqml_model = self._bqml_model_factory.create_llm_remote_model( + X, + y, + options=options, + connection_name=self.connection_name, + ) + return self + def predict( self, X: Union[bpd.DataFrame, bpd.Series], diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index beed884686c..fd047b3ba6e 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -99,3 +99,30 @@ def test_llm_palm_score_params(llm_fine_tune_df_default_index): "evaluation_status", ] assert all(col in score_result_col for col in expected_col) + + +@pytest.mark.flaky(retries=2) +def test_llm_gemini_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): + model = bigframes.ml.llm.GeminiTextGenerator( + model_name="gemini-pro", max_iterations=1 + ) + + X_train = llm_fine_tune_df_default_index[["prompt"]] + y_train = llm_fine_tune_df_default_index[["label"]] + model.fit(X_train, y_train) + + assert model is not None + + df = model.predict( + llm_remote_text_df["prompt"], + temperature=0.5, + max_output_tokens=100, + top_k=20, + top_p=0.5, + ).to_pandas() + assert df.shape == (3, 4) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() == 1) + + # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept From dff4d6e1c65798e6e3fe0e7cd4c8acada936c5ca Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:47:29 -0700 Subject: [PATCH 630/921] chore: cleanup type errors in tests/system/small/test_index.py (#756) --- tests/system/small/test_index.py | 33 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index 58fd346bc13..d68cf6c3f33 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -24,20 +24,17 @@ def test_index_construct_from_list(): bf_result = bpd.Index( [3, 14, 159], dtype=pd.Int64Dtype(), name="my_index" ).to_pandas() - # TODO(b/340878489): fix type error - pd_result = pd.Index([3, 14, 159], dtype=pd.Int64Dtype(), name="my_index") # type: ignore + pd_result: pd.Index = pd.Index([3, 14, 159], dtype=pd.Int64Dtype(), name="my_index") pd.testing.assert_index_equal(bf_result, pd_result) def test_index_construct_from_series(): - # TODO(b/340878489): fix type error bf_result = bpd.Index( bpd.Series([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name"), name="index_name", dtype=pd.Int64Dtype(), - ).to_pandas() # type: ignore - # TODO(b/340878489): fix type error - pd_result = pd.Index( # type: ignore + ).to_pandas() + pd_result: pd.Index = pd.Index( pd.Series([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name"), name="index_name", dtype=pd.Int64Dtype(), @@ -49,14 +46,15 @@ def test_index_construct_from_index(): bf_index_input = bpd.Index( [3, 14, 159], dtype=pd.Float64Dtype(), name="series_name" ) - # TODO(b/340878489): fix type error bf_result = bpd.Index( - bf_index_input, dtype=pd.Int64Dtype(), name="index_name" # type: ignore + bf_index_input, dtype=pd.Int64Dtype(), name="index_name" ).to_pandas() - # TODO(b/340878489): fix type error - pd_index_input = pd.Index([3, 14, 159], dtype=pd.Float64Dtype(), name="series_name") # type: ignore - # TODO(b/340878489): fix type error - pd_result = pd.Index(pd_index_input, dtype=pd.Int64Dtype(), name="index_name") # type: ignore + pd_index_input: pd.Index = pd.Index( + [3, 14, 159], dtype=pd.Float64Dtype(), name="series_name" + ) + pd_result: pd.Index = pd.Index( + pd_index_input, dtype=pd.Int64Dtype(), name="index_name" + ) pd.testing.assert_index_equal(bf_result, pd_result) @@ -365,17 +363,16 @@ def test_index_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep): def test_index_isin(scalars_df_index, scalars_pandas_df_index): + col_name = "int64_col" bf_series = ( - scalars_df_index.set_index("int64_col").index.isin([2, 55555, 4]).to_pandas() + scalars_df_index.set_index(col_name).index.isin([2, 55555, 4]).to_pandas() ) - pd_result_array = scalars_pandas_df_index.set_index("int64_col").index.isin( + pd_result_array = scalars_pandas_df_index.set_index(col_name).index.isin( [2, 55555, 4] ) - # TODO(b/340878489): fix type error - pd.testing.assert_index_equal( # type: ignore - pd.Index(pd_result_array), + pd.testing.assert_index_equal( + pd.Index(pd_result_array).set_names(col_name), bf_series, - check_names=False, ) From 60f13e7085660305e35e3abd07de2a4cc02ac5a5 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:12:22 -0700 Subject: [PATCH 631/921] chore: make benchmark to read subfolders and three groupby benchmarks. (#754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: make benchmark to read subfolders. * group_by test for 1E9 rows * update q1 * update noxfile for lint path. * update performance report logic * format update * 🦉 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 --- noxfile.py | 36 +++++++++++-------- .../db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py | 14 ++++++++ .../db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py | 14 ++++++++ .../db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py | 14 ++++++++ 4 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py diff --git a/noxfile.py b/noxfile.py index 24ad88d650f..533dba3ba84 100644 --- a/noxfile.py +++ b/noxfile.py @@ -35,7 +35,15 @@ # https://github.com/str0zzapreti/pytest-retry/issues/32 PYTEST_VERSION = "pytest<8.0.0dev" SPHINX_VERSION = "sphinx==4.5.0" -LINT_PATHS = ["docs", "bigframes", "tests", "third_party", "noxfile.py", "setup.py"] +LINT_PATHS = [ + "docs", + "bigframes", + "tests", + "third_party", + "noxfile.py", + "setup.py", + os.path.join("scripts", "benchmark"), +] DEFAULT_PYTHON_VERSION = "3.10" @@ -813,9 +821,9 @@ def notebook(session: nox.Session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def benchmark(session: nox.Session): session.install("-e", ".[all]") + base_path = os.path.join("scripts", "benchmark") - benchmark_script_list = list(Path("scripts/benchmark/").glob("*.py")) - + benchmark_script_list = list(Path(base_path).rglob("*.py")) # Run benchmarks in parallel session.run's, since each benchmark # takes an environment variable for performance logging processes = [] @@ -823,12 +831,7 @@ def benchmark(session: nox.Session): process = Process( target=session.run, args=("python", benchmark), - kwargs={ - "env": { - LOGGING_NAME_ENV_VAR: "scripts/benchmark/" - + os.path.basename(benchmark) - } - }, + kwargs={"env": {LOGGING_NAME_ENV_VAR: benchmark.as_posix()}}, ) process.start() processes.append(process) @@ -839,7 +842,7 @@ def benchmark(session: nox.Session): # when the environment variable is set as it is above, # notebooks output a .bytesprocessed and .slotmillis report # collect those reports and print a summary - _print_performance_report("scripts/") + _print_performance_report(base_path) def _print_performance_report(path: str): @@ -852,19 +855,24 @@ def _print_performance_report(path: str): """ print("---BIGQUERY USAGE REPORT---") results_dict = {} - for bytes_report in Path(path).glob("*/*.bytesprocessed"): + bytes_reports = sorted(Path(path).rglob("*.bytesprocessed"), key=lambda x: x.name) + for bytes_report in bytes_reports: with open(bytes_report, "r") as bytes_file: - filename = bytes_report.stem + filename = bytes_report.relative_to(path).with_suffix("") lines = bytes_file.read().splitlines() query_count = len(lines) total_bytes = sum([int(line) for line in lines]) results_dict[filename] = [query_count, total_bytes] - for millis_report in Path(path).glob("*/*.slotmillis"): + os.remove(bytes_report) + + millis_reports = sorted(Path(path).rglob("*.slotmillis"), key=lambda x: x.name) + for millis_report in millis_reports: with open(millis_report, "r") as millis_file: - filename = millis_report.stem + filename = millis_report.relative_to(path).with_suffix("") lines = millis_file.read().splitlines() total_slot_millis = sum([int(line) for line in lines]) results_dict[filename] += [total_slot_millis] + os.remove(millis_report) cumulative_queries = 0 cumulative_bytes = 0 diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py new file mode 100644 index 00000000000..cc5f77b49b4 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py @@ -0,0 +1,14 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 1: sum v1 by id1") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = x.groupby("id1", as_index=False, dropna=False).agg({"v1": "sum"}) +print(ans.shape) +chk = [ans["v1"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py new file mode 100644 index 00000000000..734a17242b8 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py @@ -0,0 +1,14 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 2: sum v1 by id1:id2") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = x.groupby(["id1", "id2"], as_index=False, dropna=False).agg({"v1": "sum"}) +print(ans.shape) +chk = [ans["v1"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py new file mode 100644 index 00000000000..242902de64d --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py @@ -0,0 +1,14 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 3: sum v1 mean v3 by id3") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = x.groupby("id3", as_index=False, dropna=False).agg({"v1": "sum", "v3": "mean"}) +print(ans.shape) +chk = [ans["v1"].sum(), ans["v3"].sum()] +print(chk) + +bpd.reset_session() From dad66fdd22bb2d507e7f366c970d971554598cf3 Mon Sep 17 00:00:00 2001 From: Ashley Xu <139821907+ashleyxuu@users.noreply.github.com> Date: Thu, 6 Jun 2024 22:24:38 -0700 Subject: [PATCH 632/921] feat: support `bigquery.vector_search()` (#736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: supoort bigquery.vector_search() * minor fix * address comments * docstring fix * address comments * small fix * add docstring clarification * Update bigframes/bigquery/__init__.py --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/bigquery/__init__.py | 154 ++++++++++++++++++ bigframes/core/sql.py | 46 +++++- .../small/bigquery/test_vector_search.py | 136 ++++++++++++++++ tests/unit/core/test_sql.py | 78 +++++++++ 4 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 tests/system/small/bigquery/test_vector_search.py create mode 100644 tests/unit/core/test_sql.py diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 5808aa28bfb..85a9010a7d9 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -21,11 +21,15 @@ from __future__ import annotations import typing +from typing import Literal, Optional, Union import bigframes.constants as constants import bigframes.core.groupby as groupby +import bigframes.core.sql +import bigframes.ml.utils as utils import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops +import bigframes.series if typing.TYPE_CHECKING: import bigframes.dataframe as dataframe @@ -148,3 +152,153 @@ def array_to_string(series: series.Series, delimiter: str) -> series.Series: """ return series._apply_unary_op(ops.ArrayToStringOp(delimiter=delimiter)) + + +def vector_search( + base_table: str, + column_to_search: str, + query: Union[dataframe.DataFrame, series.Series], + *, + query_column_to_search: Optional[str] = None, + top_k: Optional[int] = 10, + distance_type: Literal["euclidean", "cosine"] = "euclidean", + fraction_lists_to_search: Optional[float] = None, + use_brute_force: bool = False, +) -> dataframe.DataFrame: + """ + Conduct vector search which searches embeddings to find semantically similar entities. + + **Examples:** + + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + DataFrame embeddings for which to find nearest neighbors. The ``ARRAY`` column + is used as the search query: + + >>> search_query = bpd.DataFrame({"query_id": ["dog", "cat"], + ... "embedding": [[1.0, 2.0], [3.0, 5.2]]}) + >>> bbq.vector_search( + ... base_table="bigframes-dev.bigframes_tests_sys.base_table", + ... column_to_search="my_embedding", + ... query=search_query, + ... top_k=2) + query_id embedding id my_embedding distance + 1 cat [3. 5.2] 5 [5. 5.4] 2.009975 + 0 dog [1. 2.] 1 [1. 2.] 0.0 + 0 dog [1. 2.] 4 [1. 3.2] 1.2 + 1 cat [3. 5.2] 2 [2. 4.] 1.56205 + + [4 rows x 5 columns] + + Series embeddings for which to find nearest neighbors: + + >>> search_query = bpd.Series([[1.0, 2.0], [3.0, 5.2]], + ... index=["dog", "cat"], + ... name="embedding") + >>> bbq.vector_search( + ... base_table="bigframes-dev.bigframes_tests_sys.base_table", + ... column_to_search="my_embedding", + ... query=search_query, + ... top_k=2) + embedding id my_embedding distance + dog [1. 2.] 1 [1. 2.] 0.0 + cat [3. 5.2] 5 [5. 5.4] 2.009975 + dog [1. 2.] 4 [1. 3.2] 1.2 + cat [3. 5.2] 2 [2. 4.] 1.56205 + + [4 rows x 4 columns] + + You can specify the name of the column in the query DataFrame embeddings and distance type. + If you specify query_column_to_search_value, it will use the provided column which contains + the embeddings for which to find nearest neighbors. Otherwiese, it uses the column_to_search value. + + >>> search_query = bpd.DataFrame({"query_id": ["dog", "cat"], + ... "embedding": [[1.0, 2.0], [3.0, 5.2]], + ... "another_embedding": [[0.7, 2.2], [3.3, 5.2]]}) + >>> bbq.vector_search( + ... base_table="bigframes-dev.bigframes_tests_sys.base_table", + ... column_to_search="my_embedding", + ... query=search_query, + ... distance_type="cosine", + ... query_column_to_search="another_embedding", + ... top_k=2) + query_id embedding another_embedding id my_embedding distance + 1 cat [3. 5.2] [3.3 5.2] 2 [2. 4.] 0.005181 + 0 dog [1. 2.] [0.7 2.2] 4 [1. 3.2] 0.000013 + 1 cat [3. 5.2] [3.3 5.2] 1 [1. 2.] 0.005181 + 0 dog [1. 2.] [0.7 2.2] 3 [1.5 7. ] 0.004697 + + [4 rows x 6 columns] + + Args: + base_table (str): + The table to search for nearest neighbor embeddings. + column_to_search (str): + The name of the base table column to search for nearest neighbor embeddings. + The column must have a type of ``ARRAY``. All elements in the array must be non-NULL. + query (bigframes.dataframe.DataFrame | bigframes.dataframe.Series): + A Series or DataFrame that provides the embeddings for which to find nearest neighbors. + query_column_to_search (str): + Specifies the name of the column in the query that contains the embeddings for which to + find nearest neighbors. The column must have a type of ``ARRAY``. All elements in + the array must be non-NULL and all values in the column must have the same array dimensions + as the values in the ``column_to_search`` column. Can only be set when query is a DataFrame. + top_k (int, default 10): + Sepecifies the number of nearest neighbors to return. Default to 10. + distance_type (str, defalt "euclidean"): + Specifies the type of metric to use to compute the distance between two vectors. + Possible values are "euclidean" and "cosine". Default to "euclidean". + fraction_lists_to_search (float, range in [0.0, 1.0]): + Specifies the percentage of lists to search. Specifying a higher percentage leads to + higher recall and slower performance, and the converse is true when specifying a lower + percentage. It is only used when a vector index is also used. You can only specify + ``fraction_lists_to_search`` when ``use_brute_force`` is set to False. + use_brute_force (bool, default False): + Determines whether to use brute force search by skipping the vector index if one is available. + Default to False. + + Returns: + bigframes.dataframe.DataFrame: A DataFrame containing vector search result. + """ + if not fraction_lists_to_search and use_brute_force is True: + raise ValueError( + "You can't specify fraction_lists_to_search when use_brute_force is set to True." + ) + if ( + isinstance(query, bigframes.series.Series) + and query_column_to_search is not None + ): + raise ValueError( + "You can't specify query_column_to_search when query is a Series." + ) + # TODO(ashleyxu): Support options in vector search. b/344019989 + if fraction_lists_to_search is not None or use_brute_force is True: + raise NotImplementedError( + f"fraction_lists_to_search and use_brute_force is not supported. {constants.FEEDBACK_LINK}" + ) + options = { + "base_table": base_table, + "column_to_search": column_to_search, + "query_column_to_search": query_column_to_search, + "distance_type": distance_type, + "top_k": top_k, + "fraction_lists_to_search": fraction_lists_to_search, + "use_brute_force": use_brute_force, + } + + (query,) = utils.convert_to_dataframe(query) + sql_string, index_col_ids, index_labels = query._to_sql_query(include_index=True) + + sql = bigframes.core.sql.create_vector_search_sql( + sql_string=sql_string, options=options # type: ignore + ) + if index_col_ids is not None: + df = query._session.read_gbq(sql, index_col=index_col_ids) + else: + df = query._session.read_gbq(sql) + df.index.names = index_labels + + return df diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index c1e319b8607..a011bc99653 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -20,7 +20,7 @@ import datetime import math import textwrap -from typing import Iterable, TYPE_CHECKING +from typing import Iterable, Mapping, TYPE_CHECKING, Union # Literals and identifiers matching this pattern can be unquoted unquoted = r"^[A-Za-z_][A-Za-z_0-9]*$" @@ -169,3 +169,47 @@ def ordering_clause( part = f"`{ordering_expr.id}` {asc_desc} {null_clause}" parts.append(part) return f"ORDER BY {' ,'.join(parts)}" + + +def create_vector_search_sql( + sql_string: str, + options: Mapping[str, Union[str | int | bool | float]] = {}, +) -> str: + """Encode the VECTOR SEARCH statement for BigQuery Vector Search.""" + + base_table = options["base_table"] + column_to_search = options["column_to_search"] + distance_type = options["distance_type"] + top_k = options["top_k"] + query_column_to_search = options.get("query_column_to_search", None) + + if query_column_to_search is not None: + query_str = f""" + SELECT + query.*, + base.*, + distance, + FROM VECTOR_SEARCH( + TABLE `{base_table}`, + {simple_literal(column_to_search)}, + ({sql_string}), + {simple_literal(query_column_to_search)}, + distance_type => {simple_literal(distance_type)}, + top_k => {simple_literal(top_k)} + ) + """ + else: + query_str = f""" + SELECT + query.*, + base.*, + distance, + FROM VECTOR_SEARCH( + TABLE `{base_table}`, + {simple_literal(column_to_search)}, + ({sql_string}), + distance_type => {simple_literal(distance_type)}, + top_k => {simple_literal(top_k)} + ) + """ + return query_str diff --git a/tests/system/small/bigquery/test_vector_search.py b/tests/system/small/bigquery/test_vector_search.py new file mode 100644 index 00000000000..4280c0a888f --- /dev/null +++ b/tests/system/small/bigquery/test_vector_search.py @@ -0,0 +1,136 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd + +import bigframes.bigquery as bbq +import bigframes.pandas as bpd + + +def test_vector_search_basic_params_with_df(): + search_query = bpd.DataFrame( + { + "query_id": ["dog", "cat"], + "embedding": [[1.0, 2.0], [3.0, 5.2]], + } + ) + vector_search_result = bbq.vector_search( + base_table="bigframes-dev.bigframes_tests_sys.base_table", + column_to_search="my_embedding", + query=search_query, + top_k=2, + ).to_pandas() # type:ignore + expected = pd.DataFrame( + { + "query_id": ["cat", "dog", "dog", "cat"], + "embedding": [ + np.array([3.0, 5.2]), + np.array([1.0, 2.0]), + np.array([1.0, 2.0]), + np.array([3.0, 5.2]), + ], + "id": [5, 1, 4, 2], + "my_embedding": [ + np.array([5.0, 5.4]), + np.array([1.0, 2.0]), + np.array([1.0, 3.2]), + np.array([2.0, 4.0]), + ], + "distance": [2.009975, 0.0, 1.2, 1.56205], + }, + index=pd.Index([1, 0, 0, 1], dtype="Int64"), + ) + pd.testing.assert_frame_equal( + vector_search_result, expected, check_dtype=False, rtol=0.1 + ) + + +def test_vector_search_different_params_with_query(): + search_query = bpd.Series([[1.0, 2.0], [3.0, 5.2]]) + vector_search_result = bbq.vector_search( + base_table="bigframes-dev.bigframes_tests_sys.base_table", + column_to_search="my_embedding", + query=search_query, + distance_type="cosine", + top_k=2, + ).to_pandas() # type:ignore + expected = pd.DataFrame( + { + "0": [ + np.array([1.0, 2.0]), + np.array([1.0, 2.0]), + np.array([3.0, 5.2]), + np.array([3.0, 5.2]), + ], + "id": [2, 1, 1, 2], + "my_embedding": [ + np.array([2.0, 4.0]), + np.array([1.0, 2.0]), + np.array([1.0, 2.0]), + np.array([2.0, 4.0]), + ], + "distance": [0.0, 0.0, 0.001777, 0.001777], + }, + index=pd.Index([0, 0, 1, 1], dtype="Int64"), + ) + pd.testing.assert_frame_equal( + vector_search_result, expected, check_dtype=False, rtol=0.1 + ) + + +def test_vector_search_df_with_query_column_to_search(): + search_query = bpd.DataFrame( + { + "query_id": ["dog", "cat"], + "embedding": [[1.0, 2.0], [3.0, 5.2]], + "another_embedding": [[1.0, 2.5], [3.3, 5.2]], + } + ) + vector_search_result = bbq.vector_search( + base_table="bigframes-dev.bigframes_tests_sys.base_table", + column_to_search="my_embedding", + query=search_query, + query_column_to_search="another_embedding", + top_k=2, + ).to_pandas() # type:ignore + expected = pd.DataFrame( + { + "query_id": ["dog", "dog", "cat", "cat"], + "embedding": [ + np.array([1.0, 2.0]), + np.array([1.0, 2.0]), + np.array([3.0, 5.2]), + np.array([3.0, 5.2]), + ], + "another_embedding": [ + np.array([1.0, 2.5]), + np.array([1.0, 2.5]), + np.array([3.3, 5.2]), + np.array([3.3, 5.2]), + ], + "id": [1, 4, 2, 5], + "my_embedding": [ + np.array([1.0, 2.0]), + np.array([1.0, 3.2]), + np.array([2.0, 4.0]), + np.array([5.0, 5.4]), + ], + "distance": [0.5, 0.7, 1.769181, 1.711724], + }, + index=pd.Index([0, 0, 1, 1], dtype="Int64"), + ) + pd.testing.assert_frame_equal( + vector_search_result, expected, check_dtype=False, rtol=0.1 + ) diff --git a/tests/unit/core/test_sql.py b/tests/unit/core/test_sql.py new file mode 100644 index 00000000000..29f1e48a705 --- /dev/null +++ b/tests/unit/core/test_sql.py @@ -0,0 +1,78 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from bigframes.core import sql + + +def test_create_vector_search_sql_simple(): + sql_string = "SELECT embedding FROM my_embeddings_table WHERE id = 1" + options = { + "base_table": "my_base_table", + "column_to_search": "my_embedding_column", + "distance_type": "COSINE", + "top_k": 10, + "use_brute_force": False, + } + + expected_query = f""" + SELECT + query.*, + base.*, + distance, + FROM VECTOR_SEARCH( + TABLE `my_base_table`, + 'my_embedding_column', + ({sql_string}), + distance_type => 'COSINE', + top_k => 10 + ) + """ + + result_query = sql.create_vector_search_sql( + sql_string, options # type:ignore + ) + assert result_query == expected_query + + +def test_create_vector_search_sql_query_column_to_search(): + sql_string = "SELECT embedding FROM my_embeddings_table WHERE id = 1" + options = { + "base_table": "my_base_table", + "column_to_search": "my_embedding_column", + "distance_type": "COSINE", + "top_k": 10, + "query_column_to_search": "new_embedding_column", + "use_brute_force": False, + } + + expected_query = f""" + SELECT + query.*, + base.*, + distance, + FROM VECTOR_SEARCH( + TABLE `my_base_table`, + 'my_embedding_column', + ({sql_string}), + 'new_embedding_column', + distance_type => 'COSINE', + top_k => 10 + ) + """ + + result_query = sql.create_vector_search_sql( + sql_string, options # type:ignore + ) + assert result_query == expected_query From 4915424a68f36542e901a0ac27946f1ecb2d05ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 7 Jun 2024 09:01:46 -0500 Subject: [PATCH 633/921] feat: support bytes type in `remote_function` (#761) * feat: support bytes type in `remote_function` * Update bigframes/functions/remote_function_template.py * Update dataframe.py * print more information about the current exception * fix axis=1 * restore test retries --- bigframes/core/blocks.py | 15 +- bigframes/core/sql.py | 6 + bigframes/dataframe.py | 27 +++- bigframes/dtypes.py | 3 +- bigframes/functions/remote_function.py | 93 +++++++---- .../functions/remote_function_template.py | 82 +++++++++- tests/system/small/test_remote_function.py | 50 ++++-- .../test_remote_function_template.py | 145 +++++++++++++++++- 8 files changed, 362 insertions(+), 59 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index ea063669d51..40efc4f3971 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2339,12 +2339,19 @@ def _get_rows_as_json_values(self) -> Block: index_columns_count = len(self.index_columns) # column references to form the array of values for the row - column_references_csv = sql.csv( - [sql.cast_as_string(col) for col in self.expr.column_ids] - ) + column_types = list(self.index.dtypes) + list(self.dtypes) + column_references = [] + for type_, col in zip(column_types, self.expr.column_ids): + if isinstance(type_, pd.ArrowDtype) and pa.types.is_binary( + type_.pyarrow_dtype + ): + column_references.append(sql.to_json_string(col)) + else: + column_references.append(sql.cast_as_string(col)) + + column_references_csv = sql.csv(column_references) # types of the columns to serialize for the row - column_types = list(self.index.dtypes) + list(self.dtypes) column_types_csv = sql.csv( [sql.simple_literal(str(typ)) for typ in column_types] ) diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index a011bc99653..01dcebad6ed 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -96,6 +96,12 @@ def cast_as_string(column_name: str) -> str: return f"CAST({identifier(column_name)} AS STRING)" +def to_json_string(column_name: str) -> str: + """Return a string representing JSON version of a column.""" + + return f"TO_JSON_STRING({identifier(column_name)})" + + def csv(values: Iterable[str]) -> str: """Return a string of comma separated values.""" return ", ".join(values) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index e404e439ab0..e7bce520e69 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3313,22 +3313,43 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs): # Early check whether the dataframe dtypes are currently supported # in the remote function # NOTE: Keep in sync with the value converters used in the gcf code - # generated in generate_cloud_function_main_code in remote_function.py + # generated in remote_function_template.py remote_function_supported_dtypes = ( bigframes.dtypes.INT_DTYPE, bigframes.dtypes.FLOAT_DTYPE, bigframes.dtypes.BOOL_DTYPE, + bigframes.dtypes.BYTES_DTYPE, bigframes.dtypes.STRING_DTYPE, ) supported_dtypes_types = tuple( - type(dtype) for dtype in remote_function_supported_dtypes + type(dtype) + for dtype in remote_function_supported_dtypes + if not isinstance(dtype, pandas.ArrowDtype) + ) + # Check ArrowDtype separately since multiple BigQuery types map to + # ArrowDtype, including BYTES and TIMESTAMP. + supported_arrow_types = tuple( + dtype.pyarrow_dtype + for dtype in remote_function_supported_dtypes + if isinstance(dtype, pandas.ArrowDtype) ) supported_dtypes_hints = tuple( str(dtype) for dtype in remote_function_supported_dtypes ) for dtype in self.dtypes: - if not isinstance(dtype, supported_dtypes_types): + if ( + # Not one of the pandas/numpy types. + not isinstance(dtype, supported_dtypes_types) + # And not one of the arrow types. + and not ( + isinstance(dtype, pandas.ArrowDtype) + and any( + dtype.pyarrow_dtype.equals(arrow_type) + for arrow_type in supported_arrow_types + ) + ) + ): raise NotImplementedError( f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1." f" Supported dtypes are {supported_dtypes_hints}." diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index d70f6f930e6..3b9d5bf141a 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -383,11 +383,12 @@ def bf_type_from_type_kind(bf_schema) -> Dict[str, Dtype]: # Input and output types supported by BigQuery DataFrames remote functions. # TODO(shobs): Extend the support to all types supported by BQ remote functions # https://cloud.google.com/bigquery/docs/remote-functions#limitations -RF_SUPPORTED_IO_PYTHON_TYPES = {bool, float, int, str} +RF_SUPPORTED_IO_PYTHON_TYPES = {bool, bytes, float, int, str} RF_SUPPORTED_IO_BIGQUERY_TYPEKINDS = { "BOOLEAN", "BOOL", + "BYTES", "FLOAT", "FLOAT64", "INT64", diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 7f3a023c560..472ac075474 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -32,6 +32,7 @@ NamedTuple, Optional, Sequence, + Tuple, TYPE_CHECKING, Union, ) @@ -39,6 +40,7 @@ import ibis import pandas +import pyarrow import requests if TYPE_CHECKING: @@ -182,15 +184,11 @@ def create_bq_remote_function( # Create BQ function # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 bq_function_args = [] - bq_function_return_type = third_party_ibis_bqtypes.BigQueryType.from_ibis( - output_type - ) + bq_function_return_type = output_type # We are expecting the input type annotations to be 1:1 with the input args - for idx, name in enumerate(input_args): - bq_function_args.append( - f"{name} {third_party_ibis_bqtypes.BigQueryType.from_ibis(input_types[idx])}" - ) + for name, type_ in zip(input_args, input_types): + bq_function_args.append(f"{name} {type_}") remote_function_options = { "endpoint": endpoint, @@ -259,9 +257,23 @@ def get_cloud_function_endpoint(self, name): return None def generate_cloud_function_code( - self, def_, directory, package_requirements=None, is_row_processor=False + self, + def_, + directory, + *, + input_types: Tuple[str], + output_type: str, + package_requirements=None, + is_row_processor=False, ): - """Generate the cloud function code for a given user defined function.""" + """Generate the cloud function code for a given user defined function. + + Args: + input_types (tuple[str]): + Types of the input arguments in BigQuery SQL data type names. + output_type (str): + Types of the output scalar as a BigQuery SQL data type name. + """ # requirements.txt requirements = ["cloudpickle >= 2.1.0"] @@ -269,6 +281,7 @@ def generate_cloud_function_code( # bigframes remote function will send an entire row of data as json, # which would be converted to a pandas series and processed requirements.append(f"pandas=={pandas.__version__}") + requirements.append(f"pyarrow=={pyarrow.__version__}") if package_requirements: requirements.extend(package_requirements) requirements = sorted(requirements) @@ -278,7 +291,11 @@ def generate_cloud_function_code( # main.py entry_point = bigframes.functions.remote_function_template.generate_cloud_function_main_code( - def_, directory, is_row_processor + def_, + directory, + input_types=input_types, + output_type=output_type, + is_row_processor=is_row_processor, ) return entry_point @@ -286,18 +303,33 @@ def create_cloud_function( self, def_, cf_name, + *, + input_types: Tuple[str], + output_type: str, package_requirements=None, timeout_seconds=600, max_instance_count=None, is_row_processor=False, vpc_connector=None, ): - """Create a cloud function from the given user defined function.""" + """Create a cloud function from the given user defined function. + + Args: + input_types (tuple[str]): + Types of the input arguments in BigQuery SQL data type names. + output_type (str): + Types of the output scalar as a BigQuery SQL data type name. + """ # Build and deploy folder structure containing cloud function with tempfile.TemporaryDirectory() as directory: entry_point = self.generate_cloud_function_code( - def_, directory, package_requirements, is_row_processor + def_, + directory, + package_requirements=package_requirements, + input_types=input_types, + output_type=output_type, + is_row_processor=is_row_processor, ) archive_path = shutil.make_archive(directory, "zip", directory) @@ -444,11 +476,13 @@ def provision_bq_remote_function( cf_endpoint = self.create_cloud_function( def_, cloud_function_name, - package_requirements, - cloud_function_timeout, - cloud_function_max_instance_count, - is_row_processor, - cloud_function_vpc_connector, + input_types=input_types, + output_type=output_type, + package_requirements=package_requirements, + timeout_seconds=cloud_function_timeout, + max_instance_count=cloud_function_max_instance_count, + is_row_processor=is_row_processor, + vpc_connector=cloud_function_vpc_connector, ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") @@ -957,16 +991,21 @@ def try_delattr(attr): rf_name, cf_name = remote_function_client.provision_bq_remote_function( func, - ibis_signature.input_types, - ibis_signature.output_type, - reuse, - name, - packages, - max_batching_rows, - cloud_function_timeout, - cloud_function_max_instances, - is_row_processor, - cloud_function_vpc_connector, + input_types=tuple( + third_party_ibis_bqtypes.BigQueryType.from_ibis(type_) + for type_ in ibis_signature.input_types + ), + output_type=third_party_ibis_bqtypes.BigQueryType.from_ibis( + ibis_signature.output_type + ), + reuse=reuse, + name=name, + package_requirements=packages, + max_batching_rows=max_batching_rows, + cloud_function_timeout=cloud_function_timeout, + cloud_function_max_instance_count=cloud_function_max_instances, + is_row_processor=is_row_processor, + cloud_function_vpc_connector=cloud_function_vpc_connector, ) # TODO: Move ibis logic to compiler step diff --git a/bigframes/functions/remote_function_template.py b/bigframes/functions/remote_function_template.py index 5a70482469a..68fe1b917dc 100644 --- a/bigframes/functions/remote_function_template.py +++ b/bigframes/functions/remote_function_template.py @@ -18,6 +18,7 @@ import logging import os import textwrap +from typing import Tuple import cloudpickle @@ -29,9 +30,45 @@ _pickle_protocol_version = 4 +# Placeholder variables for testing. +input_types = ("STRING",) +output_type = "STRING" + + +# Convert inputs to BigQuery JSON. See: +# https://cloud.google.com/bigquery/docs/remote-functions#json_encoding_of_sql_data_type +# and +# https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#to_json_string +def convert_call(input_types, call): + for type_, arg in zip(input_types, call): + yield convert_from_bq_json(type_, arg) + + +def convert_from_bq_json(type_, arg): + import base64 + import collections + + converters = collections.defaultdict(lambda: (lambda value: value)) # type: ignore + converters["BYTES"] = base64.b64decode + converter = converters[type_] + return converter(arg) if arg is not None else None + + +def convert_to_bq_json(type_, arg): + import base64 + import collections + + converters = collections.defaultdict(lambda: (lambda value: value)) # type: ignore + converters["BYTES"] = lambda value: base64.b64encode(value).decode("utf-8") + converter = converters[type_] + return converter(arg) if arg is not None else None + + # get_pd_series is the inverse of Block._get_rows_as_json_values +# NOTE: Keep in sync with the list of supported types in DataFrame.apply. def get_pd_series(row): import ast + import base64 import json from typing import Callable, cast @@ -65,6 +102,7 @@ def get_pd_series(row): "Int64": int, "Float64": float, "string": str, + "binary[pyarrow]": base64.b64decode, } def convert_value(value, value_type): @@ -117,7 +155,9 @@ def udf(*args): # } # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#input_format def udf_http(request): + global input_types, output_type import json + import traceback from flask import jsonify @@ -126,17 +166,21 @@ def udf_http(request): calls = request_json["calls"] replies = [] for call in calls: - reply = udf(*call) + reply = convert_to_bq_json( + output_type, udf(*convert_call(input_types, call)) + ) replies.append(reply) return_json = json.dumps({"replies": replies}) return return_json - except Exception as e: - return jsonify({"errorMessage": str(e)}), 400 + except Exception: + return jsonify({"errorMessage": traceback.format_exc()}), 400 def udf_http_row_processor(request): + global output_type import json import math + import traceback from flask import jsonify import pandas as pd @@ -146,7 +190,7 @@ def udf_http_row_processor(request): calls = request_json["calls"] replies = [] for call in calls: - reply = udf(get_pd_series(call[0])) + reply = convert_to_bq_json(output_type, udf(get_pd_series(call[0]))) if isinstance(reply, float) and (math.isnan(reply) or math.isinf(reply)): # json serialization of the special float values (nan, inf, -inf) # is not in strict compliance of the JSON specification @@ -166,8 +210,8 @@ def udf_http_row_processor(request): replies.append(reply) return_json = json.dumps({"replies": replies}) return return_json - except Exception as e: - return jsonify({"errorMessage": str(e)}), 400 + except Exception: + return jsonify({"errorMessage": traceback.format_exc()}), 400 def generate_udf_code(def_, directory): @@ -190,8 +234,22 @@ def generate_udf_code(def_, directory): return udf_code_file_name, udf_bytecode_file_name -def generate_cloud_function_main_code(def_, directory, is_row_processor=False): - """Get main.py code for the cloud function for the given user defined function.""" +def generate_cloud_function_main_code( + def_, + directory, + *, + input_types: Tuple[str], + output_type: str, + is_row_processor=False, +): + """Get main.py code for the cloud function for the given user defined function. + + Args: + input_types (tuple[str]): + Types of the input arguments in BigQuery SQL data type names. + output_type (str): + Types of the output scalar as a BigQuery SQL data type name. + """ # Pickle the udf with all its dependencies udf_code_file, udf_bytecode_file = generate_udf_code(def_, directory) @@ -204,14 +262,22 @@ def generate_cloud_function_main_code(def_, directory, is_row_processor=False): # serialized udf code is in {udf_bytecode_file} with open("{udf_bytecode_file}", "rb") as f: udf = cloudpickle.load(f) + +input_types = {repr(input_types)} +output_type = {repr(output_type)} """ ] + # For converting scalar outputs to the correct type. + code_blocks.append(inspect.getsource(convert_to_bq_json)) + if is_row_processor: code_blocks.append(inspect.getsource(get_pd_series)) handler_func_name = "udf_http_row_processor" code_blocks.append(inspect.getsource(udf_http_row_processor)) else: + code_blocks.append(inspect.getsource(convert_call)) + code_blocks.append(inspect.getsource(convert_from_bq_json)) handler_func_name = "udf_http" code_blocks.append(inspect.getsource(udf_http)) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 85e23fae16a..d84d520988a 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -17,6 +17,7 @@ import google.api_core.exceptions from google.cloud import bigquery import pandas as pd +import pyarrow import pytest import bigframes @@ -80,7 +81,7 @@ def session_with_bq_connection( bq_cf_connection, dataset_id_permanent ) -> bigframes.Session: session = bigframes.Session( - bigframes.BigQueryOptions(bq_connection=bq_cf_connection) + bigframes.BigQueryOptions(bq_connection=bq_cf_connection, location="US") ) return session @@ -484,17 +485,27 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_series_map(session_with_bq_connection, scalars_dfs): - def add_one(x): - return x + 1 +def test_series_map_bytes(session_with_bq_connection, scalars_dfs): + """Check that bytes is support as input and output.""" + scalars_df, scalars_pandas_df = scalars_dfs - remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + def bytes_to_hex(mybytes: bytes) -> bytes: + import pandas - scalars_df, scalars_pandas_df = scalars_dfs + return mybytes.hex().encode("utf-8") if pandas.notna(mybytes) else None # type: ignore - bf_result = scalars_df.int64_too.map(remote_add_one).to_pandas() - pd_result = scalars_pandas_df.int64_too.map(add_one) - pd_result = pd_result.astype("Int64") # pandas type differences + # TODO(b/345516010): the type: ignore is because "Optional" not yet + # supported as a type annotation in @remote_function(). + assert bytes_to_hex(None) is None # type: ignore + assert bytes_to_hex(b"\x00\xdd\xba\x11") == b"00ddba11" + pd_result = scalars_pandas_df.bytes_col.map(bytes_to_hex).astype( + pd.ArrowDtype(pyarrow.binary()) + ) + + remote_bytes_to_hex = session_with_bq_connection.remote_function( + packages=["pandas"] + )(bytes_to_hex) + bf_result = scalars_df.bytes_col.map(remote_bytes_to_hex).to_pandas() pd.testing.assert_series_equal( bf_result, @@ -733,7 +744,14 @@ def test_read_gbq_function_enforces_explicit_types( @pytest.mark.flaky(retries=2, delay=120) def test_df_apply_axis_1(session, scalars_dfs): - columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] + columns = [ + "bool_col", + "int64_col", + "int64_too", + "float64_col", + "string_col", + "bytes_col", + ] scalars_df, scalars_pandas_df = scalars_dfs def add_ints(row): @@ -743,9 +761,10 @@ def add_ints(row): bigframes.exceptions.PreviewWarning, match="input_types=Series is in preview.", ): - add_ints_remote = session.remote_function(bigframes.series.Series, int)( - add_ints - ) + add_ints_remote = session.remote_function( + bigframes.series.Series, + int, + )(add_ints) with pytest.warns( bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." @@ -843,7 +862,6 @@ def add_ints(row): @pytest.mark.parametrize( ("column"), [ - pytest.param("bytes_col"), pytest.param("date_col"), pytest.param("datetime_col"), pytest.param("geography_col"), @@ -868,7 +886,9 @@ def echo(row): with pytest.raises( NotImplementedError, match=re.escape( - f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1. Supported dtypes are ('Int64', 'Float64', 'boolean', 'string')." + f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1. Supported dtypes are (" ), + ), pytest.warns( + bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." ): scalars_df[[column]].apply(echo, axis=1) diff --git a/tests/unit/functions/test_remote_function_template.py b/tests/unit/functions/test_remote_function_template.py index 4b92b13149a..70b033d9386 100644 --- a/tests/unit/functions/test_remote_function_template.py +++ b/tests/unit/functions/test_remote_function_template.py @@ -16,10 +16,123 @@ import pandas as pd import pandas.testing +import pyarrow import pytest +import bigframes.dtypes import bigframes.functions.remote_function_template as remote_function_template +HELLO_WORLD_BASE64_BYTES = b"SGVsbG8sIFdvcmxkIQ==" +HELLO_WORLD_BASE64_STR = "SGVsbG8sIFdvcmxkIQ==" + + +@pytest.mark.parametrize( + ["type_", "json_value", "expected"], + ( + pytest.param( + # Type names should match those in BigQueryType.from_ibis in + # third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py + "BOOLEAN", + True, + True, + ), + pytest.param( + "BYTES", + HELLO_WORLD_BASE64_STR, + b"Hello, World!", + ), + pytest.param( + "FLOAT64", + 1.25, + 1.25, + ), + pytest.param( + "INT64", + 123, + 123, + ), + pytest.param( + "STRING", + "Hello, World!", + "Hello, World!", + ), + ), +) +def test_convert_from_bq_json(type_, json_value, expected): + got = remote_function_template.convert_from_bq_json(type_, json_value) + assert got == expected + + +@pytest.mark.parametrize( + "type_", + [ + # Type names should match those in BigQueryType.from_ibis in + # third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py + "BOOLEAN", + "BYTES", + "FLOAT64", + "INT64", + "STRING", + ], +) +def test_convert_from_bq_json_none(type_): + got = remote_function_template.convert_from_bq_json(type_, None) + assert got is None + + +@pytest.mark.parametrize( + ["type_", "value", "expected"], + ( + pytest.param( + # Type names should match those in BigQueryType.from_ibis in + # third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py + "BOOLEAN", + True, + True, + ), + pytest.param( + "BYTES", + b"Hello, World!", + HELLO_WORLD_BASE64_STR, + ), + pytest.param( + "FLOAT64", + 1.25, + 1.25, + ), + pytest.param( + "INT64", + 123, + 123, + ), + pytest.param( + "STRING", + "Hello, World!", + "Hello, World!", + ), + ), +) +def test_convert_to_bq_json(type_, value, expected): + got = remote_function_template.convert_to_bq_json(type_, value) + assert got == expected + + +@pytest.mark.parametrize( + "type_", + [ + # Type names should match those in BigQueryType.from_ibis in + # third_party/bigframes_vendored/ibis/backends/bigquery/datatypes.py + "BOOLEAN", + "BYTES", + "FLOAT64", + "INT64", + "STRING", + ], +) +def test_convert_to_bq_json_none(type_): + got = remote_function_template.convert_to_bq_json(type_, None) + assert got is None + @pytest.mark.parametrize( ["row_json", "expected"], @@ -42,9 +155,39 @@ ), id="int64-string-index", ), - # TODO: test more types and indexes + pytest.param( + json.dumps( + { + "names": ["'col1'", "'col2'", "'col3'"], + "types": ["binary[pyarrow]", "binary[pyarrow]", "binary[pyarrow]"], + "values": [HELLO_WORLD_BASE64_STR, "dGVzdDI=", "dGVzdDM="], + "indexlength": 0, + "dtype": "binary[pyarrow]", + } + ), + pd.Series( + [b"Hello, World!", b"test2", b"test3"], + dtype=pd.ArrowDtype(pyarrow.binary()), + index=["col1", "col2", "col3"], + name=(), + ), + id="binary-no-index", + ), ), ) def test_get_pd_series(row_json, expected): got = remote_function_template.get_pd_series(row_json) pandas.testing.assert_series_equal(got, expected) + + +def test_get_pd_series_converter_dtypes(): + """Ensures the string format of the dtype doesn't change from that expected by get_pd_series.""" + + # Keep in sync with value_converters in get_pd_series. + # NOTE: Any change here is a red flag that there has been a breaking change + # that will affect deployed axis=1 remote functions. + assert str(bigframes.dtypes.BOOL_DTYPE) == "boolean" + assert str(bigframes.dtypes.BYTES_DTYPE) == "binary[pyarrow]" + assert str(bigframes.dtypes.FLOAT_DTYPE) == "Float64" + assert str(bigframes.dtypes.INT_DTYPE) == "Int64" + assert str(bigframes.dtypes.STRING_DTYPE) == "string" From 61f18cb63f2785c03dc612a34c030079fc8f4172 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Fri, 7 Jun 2024 08:17:01 -0700 Subject: [PATCH 634/921] fix: Improve to_pandas_batches for large results (#746) * fix: Improve to_pandas_batches for large results * remove page_size and use 1tb * don't pass bqstorage client * use page_size * still pass storage client * use max_results instead of iterator trick --- bigframes/core/blocks.py | 19 ++++++++++++++++--- bigframes/dataframe.py | 26 +++++++++++++++++++++++--- tests/system/load/test_large_tables.py | 25 ++++++++++--------------- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 40efc4f3971..1b27d9dedf4 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -508,11 +508,24 @@ def try_peek( else: return None - def to_pandas_batches(self): - """Download results one message at a time.""" + def to_pandas_batches( + self, page_size: Optional[int] = None, max_results: Optional[int] = None + ): + """Download results one message at a time. + + page_size and max_results determine the size and number of batches, + see https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob#google_cloud_bigquery_job_QueryJob_result""" dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) - results_iterator, _ = self.session._execute(self.expr, sorted=True) + _, query_job = self.session._query_to_destination( + self.session._to_sql(self.expr, sorted=True), + list(self.index_columns), + api_name="cached", + do_clustering=False, + ) + results_iterator = query_job.result( + page_size=page_size, max_results=max_results + ) for arrow_table in results_iterator.to_arrow_iterable( bqstorage_client=self.session.bqstoragereadclient ): diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index e7bce520e69..f12c3467768 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1215,10 +1215,30 @@ def to_pandas( self._set_internal_query_job(query_job) return df.set_axis(self._block.column_labels, axis=1, copy=False) - def to_pandas_batches(self) -> Iterable[pandas.DataFrame]: - """Stream DataFrame results to an iterable of pandas DataFrame""" + def to_pandas_batches( + self, page_size: Optional[int] = None, max_results: Optional[int] = None + ) -> Iterable[pandas.DataFrame]: + """Stream DataFrame results to an iterable of pandas DataFrame. + + page_size and max_results determine the size and number of batches, + see https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob#google_cloud_bigquery_job_QueryJob_result + + Args: + page_size (int, default None): + The size of each batch. + max_results (int, default None): + If given, only download this many rows at maximum. + + Returns: + Iterable[pandas.DataFrame]: + An iterable of smaller dataframes which combine to + form the original dataframe. Results stream from bigquery, + see https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.table.RowIterator#google_cloud_bigquery_table_RowIterator_to_arrow_iterable + """ self._optimize_query_complexity() - return self._block.to_pandas_batches() + return self._block.to_pandas_batches( + page_size=page_size, max_results=max_results + ) def _compute_dry_run(self) -> bigquery.QueryJob: return self._block._compute_dry_run() diff --git a/tests/system/load/test_large_tables.py b/tests/system/load/test_large_tables.py index cf1c787a588..f92207b1915 100644 --- a/tests/system/load/test_large_tables.py +++ b/tests/system/load/test_large_tables.py @@ -75,22 +75,17 @@ def test_index_repr_large_table(): def test_to_pandas_batches_large_table(): - df = bpd.read_gbq("load_testing.scalars_10gb") - # df will be downloaded locally - expected_row_count, expected_column_count = df.shape - - row_count = 0 - # TODO(b/340890167): fix type error - for df in df.to_pandas_batches(): # type: ignore - batch_row_count, batch_column_count = df.shape + df = bpd.read_gbq("load_testing.scalars_1tb") + _, expected_column_count = df.shape + + # download only a few batches, since 1tb would be too much + iterable = df.to_pandas_batches(page_size=500, max_results=1500) + # use page size since client library doesn't support + # streaming only part of the dataframe via bqstorage + for pdf in iterable: + batch_row_count, batch_column_count = pdf.shape assert batch_column_count == expected_column_count - row_count += batch_row_count - - # Attempt to save on memory by manually removing the batch df - # from local memory after finishing with processing. - del df - - assert row_count == expected_row_count + assert batch_row_count > 0 @pytest.mark.skip(reason="See if it caused kokoro build aborted.") From c254e9ce1007fda88f8e9f9f0f71e82b3315cd73 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:45:54 -0700 Subject: [PATCH 635/921] chore: cleanup type errors in tests/system/small/ml/test_metrics.py (#755) --- bigframes/ml/metrics/_metrics.py | 6 +-- tests/system/small/ml/test_metrics.py | 72 ++++++++++----------------- 2 files changed, 29 insertions(+), 49 deletions(-) diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index 2525ecd34fe..a40c175000a 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -227,7 +227,7 @@ def recall_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], *, - average: str = "binary", + average: typing.Optional[str] = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" if average is not None: @@ -264,7 +264,7 @@ def precision_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], *, - average: str = "binary", + average: typing.Optional[str] = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" if average is not None: @@ -303,7 +303,7 @@ def f1_score( y_true: Union[bpd.DataFrame, bpd.Series], y_pred: Union[bpd.DataFrame, bpd.Series], *, - average: str = "binary", + average: typing.Optional[str] = "binary", ) -> pd.Series: # TODO(ashleyxu): support more average type, default to "binary" y_true_series, y_pred_series = utils.convert_to_series(y_true, y_pred) diff --git a/tests/system/small/ml/test_metrics.py b/tests/system/small/ml/test_metrics.py index 9aff2fe773d..81e1b2f77fd 100644 --- a/tests/system/small/ml/test_metrics.py +++ b/tests/system/small/ml/test_metrics.py @@ -219,8 +219,8 @@ def test_roc_curve_binary_classification_prediction_matches_sklearn(session): ) # sklearn returns float64 np arrays - np_fpr = fpr.to_pandas().astype("float64").array - np_tpr = tpr.to_pandas().astype("float64").array + np_fpr = fpr.to_pandas().astype("float64").array.to_numpy() + np_tpr = tpr.to_pandas().astype("float64").array.to_numpy() np_thresholds = thresholds.to_pandas().astype("float64").array np.testing.assert_array_equal( @@ -228,14 +228,12 @@ def test_roc_curve_binary_classification_prediction_matches_sklearn(session): np_thresholds[1:], expected_thresholds[1:], ) - # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_fpr, # type: ignore + np_fpr, expected_fpr, ) - # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_tpr, # type: ignore + np_tpr, expected_tpr, ) @@ -316,8 +314,8 @@ def test_roc_curve_binary_classification_decision_matches_sklearn(session): ) # sklearn returns float64 np arrays - np_fpr = fpr.to_pandas().astype("float64").array - np_tpr = tpr.to_pandas().astype("float64").array + np_fpr = fpr.to_pandas().astype("float64").array.to_numpy() + np_tpr = tpr.to_pandas().astype("float64").array.to_numpy() np_thresholds = thresholds.to_pandas().astype("float64").array np.testing.assert_array_equal( @@ -325,14 +323,12 @@ def test_roc_curve_binary_classification_decision_matches_sklearn(session): np_thresholds[1:], expected_thresholds[1:], ) - # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_fpr, # type: ignore + np_fpr, expected_fpr, ) - # TODO(b/340872435): fix type error np.testing.assert_array_equal( - np_tpr, # type: ignore + np_tpr, expected_tpr, ) @@ -519,14 +515,10 @@ def test_confusion_matrix_column_index(session): ).astype("Int64") df = session.read_pandas(pd_df) confusion_matrix = metrics.confusion_matrix(df[["y_true"]], df[["y_pred"]]) - # TODO(b/340872435): fix type error - expected_pd_df = ( - pd.DataFrame( # type: ignore - {1: [1, 0, 1, 0], 2: [0, 0, 2, 0], 3: [0, 0, 0, 0], 4: [0, 1, 0, 1]} - ) - .astype("int64") - .set_index([pd.Index([1, 2, 3, 4])]) - ) + expected_pd_df = pd.DataFrame( + {1: [1, 0, 1, 0], 2: [0, 0, 2, 0], 3: [0, 0, 0, 0], 4: [0, 1, 0, 1]}, + index=[1, 2, 3, 4], + ).astype("int64") pd.testing.assert_frame_equal( confusion_matrix, expected_pd_df, check_index_type=False ) @@ -562,9 +554,8 @@ def test_confusion_matrix_str_matches_sklearn(session): expected_confusion_matrix = sklearn_metrics.confusion_matrix( pd_df[["y_true"]], pd_df[["y_pred"]] ) - # TODO(b/340872435): fix type error - expected_pd_df = pd.DataFrame(expected_confusion_matrix).set_index( # type: ignore - [pd.Index(["ant", "bird", "cat"])] + expected_pd_df = pd.DataFrame( + expected_confusion_matrix, index=["ant", "bird", "cat"] ) expected_pd_df.columns = pd.Index(["ant", "bird", "cat"]) pd.testing.assert_frame_equal( @@ -601,9 +592,8 @@ def test_recall_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error recall = metrics.recall_score( - df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None # type: ignore + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [1.000000, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -620,8 +610,7 @@ def test_recall_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error - recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore + recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.recall_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -638,8 +627,7 @@ def test_recall_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error - recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore + recall = metrics.recall_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.recall_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -656,8 +644,7 @@ def test_recall_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error - recall = metrics.recall_score(df["y_true"], df["y_pred"], average=None) # type: ignore + recall = metrics.recall_score(df["y_true"], df["y_pred"], average=None) expected_values = [1.000000, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_recall = pd.Series(expected_values, index=expected_index) @@ -673,9 +660,8 @@ def test_precision_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error precision_score = metrics.precision_score( - df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None # type: ignore + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [0.666667, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -696,7 +682,7 @@ def test_precision_score_matches_sklearn(session): df = session.read_pandas(pd_df) # TODO(b/340872435): fix type error precision_score = metrics.precision_score( - df[["y_true"]], df[["y_pred"]], average=None # type: ignore + df[["y_true"]], df[["y_pred"]], average=None ) expected_values = sklearn_metrics.precision_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None @@ -716,9 +702,8 @@ def test_precision_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error precision_score = metrics.precision_score( - df[["y_true"]], df[["y_pred"]], average=None # type: ignore + df[["y_true"]], df[["y_pred"]], average=None ) expected_values = sklearn_metrics.precision_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None @@ -738,8 +723,7 @@ def test_precision_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error - precision_score = metrics.precision_score(df["y_true"], df["y_pred"], average=None) # type: ignore + precision_score = metrics.precision_score(df["y_true"], df["y_pred"], average=None) expected_values = [0.666667, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_precision = pd.Series(expected_values, index=expected_index) @@ -757,9 +741,8 @@ def test_f1_score(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error f1_score = metrics.f1_score( - df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None # type: ignore + df[["y_true_arbitrary_name"]], df[["y_pred_arbitrary_name"]], average=None ) expected_values = [0.8, 0.000000, 0.666667] expected_index = [0, 1, 2] @@ -776,8 +759,7 @@ def test_f1_score_matches_sklearn(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error - f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore + f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.f1_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -794,8 +776,7 @@ def test_f1_score_str_matches_sklearn(session): } ).astype("str") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error - f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) # type: ignore + f1_score = metrics.f1_score(df[["y_true"]], df[["y_pred"]], average=None) expected_values = sklearn_metrics.f1_score( pd_df[["y_true"]], pd_df[["y_pred"]], average=None ) @@ -812,8 +793,7 @@ def test_f1_score_series(session): } ).astype("Int64") df = session.read_pandas(pd_df) - # TODO(b/340872435): fix type error - f1_score = metrics.f1_score(df["y_true"], df["y_pred"], average=None) # type: ignore + f1_score = metrics.f1_score(df["y_true"], df["y_pred"], average=None) expected_values = [0.8, 0.000000, 0.666667] expected_index = [0, 1, 2] expected_f1 = pd.Series(expected_values, index=expected_index) From 8fa5f49564d2d5681b23fa816daf285478485af7 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:39:42 -0700 Subject: [PATCH 636/921] chore: cleanup type errors in bigframes/core/indexes/base.py (#762) --- bigframes/core/indexes/base.py | 16 ++++++++++------ .../pandas/core/indexes/base.py | 4 +++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index e40e20b0cb2..0e5082447a8 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -90,9 +90,12 @@ def __new__( # TODO: Support more index subtypes from bigframes.core.indexes.multi import MultiIndex - klass = MultiIndex if len(block._index_columns) > 1 else cls - # TODO(b/340893286): fix type error - result = typing.cast(Index, object.__new__(klass)) # type: ignore + if len(block._index_columns) <= 1: + klass = cls + else: + klass = MultiIndex + + result = typing.cast(Index, object.__new__(klass)) result._query_job = None result._block = block block.session._register_object(result) @@ -161,7 +164,8 @@ def dtype(self): @property def dtypes(self) -> pandas.Series: return pandas.Series( - data=self._block.index.dtypes, index=self._block.index.names # type:ignore + data=self._block.index.dtypes, + index=typing.cast(typing.Tuple, self._block.index.names), ) @property @@ -408,10 +412,10 @@ def drop( block = block.drop_columns([condition_id]) return Index(block) - def dropna(self, how: str = "any") -> Index: + def dropna(self, how: typing.Literal["all", "any"] = "any") -> Index: if how not in ("any", "all"): raise ValueError("'how' must be one of 'any', 'all'") - result = block_ops.dropna(self._block, self._block.index_columns, how=how) # type: ignore + result = block_ops.dropna(self._block, self._block.index_columns, how=how) return Index(result) def drop_duplicates(self, *, keep: str = "first") -> Index: diff --git a/third_party/bigframes_vendored/pandas/core/indexes/base.py b/third_party/bigframes_vendored/pandas/core/indexes/base.py index eb6b9161fc2..b0e1a093921 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/base.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/base.py @@ -1,6 +1,8 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexes/base.py from __future__ import annotations +import typing + from bigframes import constants @@ -320,7 +322,7 @@ def drop(self, labels) -> Index: """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def dropna(self, how: str = "any"): + def dropna(self, how: typing.Literal["all", "any"] = "any"): """Return Index without NA/NaN values. Args: From 35fc7e49612094eec113da5c2e6b90c42054a1cc Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:39:58 -0700 Subject: [PATCH 637/921] chore: cleanup type errors in tests/system/small/test_encryption.py (#763) --- tests/system/small/test_encryption.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 088211d7fcb..428a6a28bf0 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -242,14 +242,12 @@ def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): model.fit(X_train, y_train) assert model is not None - # TODO(b/340879287): fix type error - assert model._bqml_model.model.encryption_configuration is not None # type: ignore - # TODO(b/340879287): fix type error - assert model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek # type: ignore + assert model._bqml_model is not None + assert model._bqml_model.model.encryption_configuration is not None + assert model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek # Assert that model exists in BQ with intended encryption - # TODO(b/340879287): fix type error - model_bq = session_with_bq_cmek.bqclient.get_model(model._bqml_model.model_name) # type: ignore + model_bq = session_with_bq_cmek.bqclient.get_model(model._bqml_model.model_name) assert model_bq.encryption_configuration.kms_key_name == bq_cmek # Explicitly save the model to a destination and assert that encryption holds @@ -260,12 +258,12 @@ def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): f"{model_ref.project}.{model_ref.dataset_id}.{model_ref.model_id}" ) new_model = model.to_gbq(model_ref_full_name) - # TODO(b/340879287): fix type error - assert new_model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek # type: ignore + assert new_model._bqml_model is not None + assert new_model._bqml_model.model.encryption_configuration is not None + assert new_model._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek # Assert that model exists in BQ with intended encryption - # TODO(b/340879287): fix type error - model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) # type: ignore + model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) assert model_bq.encryption_configuration.kms_key_name == bq_cmek # Assert that model registration keeps the encryption @@ -279,11 +277,11 @@ def test_bqml(bq_cmek, session_with_bq_cmek, penguins_table_id): # https://cloud.google.com/vertex-ai/docs/general/cmek#create_resources_with_the_kms_key. # bigframes.ml does not provide any API for the model deployment. model_registered = new_model.register() - # TODO(b/340879287): fix type error + assert model_registered._bqml_model is not None + assert model_registered._bqml_model.model.encryption_configuration is not None assert ( - model_registered._bqml_model.model.encryption_configuration.kms_key_name # type: ignore + model_registered._bqml_model.model.encryption_configuration.kms_key_name == bq_cmek ) - # TODO(b/340879287): fix type error - model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) # type: ignore + model_bq = session_with_bq_cmek.bqclient.get_model(new_model._bqml_model.model_name) assert model_bq.encryption_configuration.kms_key_name == bq_cmek From 7e8296d7e31e683927cc53eb2477b2707bea6fb3 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 7 Jun 2024 14:50:17 -0700 Subject: [PATCH 638/921] refactor: caching and aggregation don't require ordering (#759) --- bigframes/core/__init__.py | 15 +++++++++--- bigframes/core/blocks.py | 34 +++++++++++++++++++++------- bigframes/core/compile/compiler.py | 5 ++++ bigframes/core/nodes.py | 5 +++- bigframes/session/__init__.py | 14 ++++++++++-- tests/system/conftest.py | 11 +++++++++ tests/system/small/test_dataframe.py | 6 +++-- tests/system/small/test_unordered.py | 28 +++++++++++++++++++++++ 8 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 tests/system/small/test_unordered.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index f8ee984d5fb..e0b63b4a8c2 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -177,7 +177,7 @@ def _compiled_schema(self) -> schemata.ArraySchema: def as_cached( self: ArrayValue, cache_table: google.cloud.bigquery.Table, - ordering: orderings.ExpressionOrdering, + ordering: Optional[orderings.ExpressionOrdering], ) -> ArrayValue: """ Replace the node with an equivalent one that references a tabel where the value has been materialized to. @@ -234,6 +234,8 @@ def promote_offsets(self, col_id: str) -> ArrayValue: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. """ + if not self.session._strictly_ordered: + raise ValueError("Generating offsets not supported in unordered mode") return ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)) def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: @@ -382,6 +384,10 @@ def project_window_op( never_skip_nulls: will disable null skipping for operators that would otherwise do so skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection """ + if not self.session._strictly_ordered: + # TODO: Support unbounded windows with aggregate ops and some row-order-independent analytic ops + # TODO: Support non-deterministic windowing + raise ValueError("Windowed ops not supported in unordered mode") return ArrayValue( nodes.WindowOpNode( child=self.node, @@ -433,8 +439,9 @@ def unpivot( """ # There will be N labels, used to disambiguate which of N source columns produced each output row explode_offsets_id = bigframes.core.guid.generate_guid("unpivot_offsets_") - labels_array = self._create_unpivot_labels_array(row_labels, index_col_ids) - labels_array = labels_array.promote_offsets(explode_offsets_id) + labels_array = self._create_unpivot_labels_array( + row_labels, index_col_ids, explode_offsets_id + ) # Unpivot creates N output rows for each input row, labels disambiguate these N rows joined_array = self._cross_join_w_labels(labels_array, join_side) @@ -500,6 +507,7 @@ def _create_unpivot_labels_array( self, former_column_labels: typing.Sequence[typing.Hashable], col_ids: typing.Sequence[str], + offsets_id: str, ) -> ArrayValue: """Create an ArrayValue from a list of label tuples.""" rows = [] @@ -510,6 +518,7 @@ def _create_unpivot_labels_array( col_ids[i]: (row_label[i] if pandas.notnull(row_label[i]) else None) for i in range(len(col_ids)) } + row[offsets_id] = row_offset rows.append(row) return ArrayValue.from_pyarrow(pa.Table.from_pylist(rows), session=self.session) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 1b27d9dedf4..301bcc20e94 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -553,7 +553,7 @@ def _materialize_local( """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. _, query_job = self.session._query_to_destination( - self.session._to_sql(self.expr, sorted=True), + self.session._to_sql(self.expr, sorted=materialize_options.ordered), list(self.index_columns), api_name="cached", do_clustering=False, @@ -1016,7 +1016,7 @@ def aggregate_all_and_stack( index_columns=[index_id], column_labels=self.column_labels, index_labels=[None], - ).transpose(original_row_index=pd.Index([None])) + ).transpose(original_row_index=pd.Index([None]), single_row_mode=True) else: # axis_n == 1 # using offsets as identity to group on. # TODO: Allow to promote identity/total_order columns instead for better perf @@ -1659,6 +1659,8 @@ def melt( value_vars=typing.Sequence[str], var_names=typing.Sequence[typing.Hashable], value_name: typing.Hashable = "value", + *, + create_offsets_index: bool = True, ): """ Unpivot columns to produce longer, narrower dataframe. @@ -1679,20 +1681,31 @@ def melt( index_col_ids=var_col_ids, join_side="right", ) - index_id = guid.generate_guid() - unpivot_expr = unpivot_expr.promote_offsets(index_id) + + if create_offsets_index: + index_id = guid.generate_guid() + unpivot_expr = unpivot_expr.promote_offsets(index_id) + index_cols = [index_id] + else: + index_cols = [] + # Need to reorder to get id_vars before var_col and unpivot_col unpivot_expr = unpivot_expr.select_columns( - [index_id, *id_vars, *var_col_ids, unpivot_col_id] + [*index_cols, *id_vars, *var_col_ids, unpivot_col_id] ) return Block( unpivot_expr, column_labels=[*id_labels, *var_names, value_name], - index_columns=[index_id], + index_columns=index_cols, ) - def transpose(self, *, original_row_index: Optional[pd.Index] = None) -> Block: + def transpose( + self, + *, + original_row_index: Optional[pd.Index] = None, + single_row_mode: bool = False, + ) -> Block: """Transpose the block. Will fail if dtypes aren't coercible to a common type or too many rows. Can provide the original_row_index directly if it is already known, otherwise a query is needed. """ @@ -1718,7 +1731,11 @@ def transpose(self, *, original_row_index: Optional[pd.Index] = None) -> Block: block.column_labels, pd.Index(range(len(block.column_labels))) ) ) - numbered_block, offsets = numbered_block.promote_offsets() + # TODO: Determine if single row from expression tree (after aggregation without groupby) + if single_row_mode: + numbered_block, offsets = numbered_block.create_constant(0) + else: + numbered_block, offsets = numbered_block.promote_offsets() stacked_block = numbered_block.melt( id_vars=(offsets,), @@ -1727,6 +1744,7 @@ def transpose(self, *, original_row_index: Optional[pd.Index] = None) -> Block: "col_offset", ), value_vars=block.value_columns, + create_offsets_index=False, ) col_labels = stacked_block.value_columns[-2 - original_col_index.nlevels : -2] col_offset = stacked_block.value_columns[-2] # disambiguator we created earlier diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 4ecba062365..021ec8b176b 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -109,6 +109,11 @@ def compile_cached_table(node: nodes.CachedTableNode, ordered: bool = True): ) ibis_table = ibis.table(physical_schema, full_table_name) if ordered: + if node.ordering is None: + # If this happens, session malfunctioned while applying cached results. + raise ValueError( + "Cannot use unordered cached value. Result requires ordering information." + ) return compiled.OrderedIR( ibis_table, columns=tuple( diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index f9bbe8c5155..077a362ba03 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -411,6 +411,7 @@ def transform_children( return self +# This node shouldn't be used in the "original" expression tree, only used as replacement for original during planning @dataclass(frozen=True) class CachedTableNode(BigFrameNode): # The original BFET subtree that was cached @@ -422,7 +423,7 @@ class CachedTableNode(BigFrameNode): table_id: str = field() physical_schema: Tuple[bq.SchemaField, ...] = field() - ordering: orderings.ExpressionOrdering = field() + ordering: typing.Optional[orderings.ExpressionOrdering] = field() @property def session(self): @@ -446,6 +447,8 @@ def variables_introduced(self) -> int: @property def hidden_columns(self) -> typing.Tuple[str, ...]: """Physical columns used to define ordering but not directly exposed as value columns.""" + if self.ordering is None: + return () return tuple( col for col in sorted(self.ordering.referenced_columns) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 80bf659a3d9..4c5ce211530 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -294,6 +294,9 @@ def __init__( self._bytes_processed_sum = 0 self._slot_millis_sum = 0 self._execution_count = 0 + # Whether this session treats objects as totally ordered. + # Will expose as feature later, only False for internal testing + self._strictly_ordered = True @property def bqclient(self): @@ -1841,17 +1844,20 @@ def _cache_with_cluster_cols( """Executes the query and uses the resulting table to rewrite future executions.""" # TODO: Use this for all executions? Problem is that caching materializes extra # ordering columns + # TODO: May want to support some partial ordering info even for non-strict ordering mode + keep_order_info = self._strictly_ordered + compiled_value = self._compile_ordered(array_value) ibis_expr = compiled_value._to_ibis_expr( - ordering_mode="unordered", expose_hidden_cols=True + ordering_mode="unordered", expose_hidden_cols=keep_order_info ) tmp_table = self._ibis_to_temp_table( ibis_expr, cluster_cols=cluster_cols, api_name="cached" ) cached_replacement = array_value.as_cached( cache_table=self.bqclient.get_table(tmp_table), - ordering=compiled_value._ordering, + ordering=compiled_value._ordering if keep_order_info else None, ).node self._cached_executions[array_value.node] = cached_replacement @@ -1859,6 +1865,10 @@ def _cache_with_offsets(self, array_value: core.ArrayValue): """Executes the query and uses the resulting table to rewrite future executions.""" # TODO: Use this for all executions? Problem is that caching materializes extra # ordering columns + if not self._strictly_ordered: + raise ValueError( + "Caching with offsets only supported in strictly ordered mode." + ) compiled_value = self._compile_ordered(array_value) ibis_expr = compiled_value._to_ibis_expr( diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 54a3deffe21..06ad73a7020 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -139,6 +139,17 @@ def session() -> Generator[bigframes.Session, None, None]: session.close() # close generated session at cleanup time +@pytest.fixture(scope="session") +def unordered_session() -> Generator[bigframes.Session, None, None]: + context = bigframes.BigQueryOptions( + location="US", + ) + session = bigframes.Session(context=context) + session._strictly_ordered = False + yield session + session.close() # close generated session at cleanup type + + @pytest.fixture(scope="session") def session_tokyo(tokyo_location: str) -> Generator[bigframes.Session, None, None]: context = bigframes.BigQueryOptions( diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index eae25bb027b..ffc09a1a1fd 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3124,9 +3124,9 @@ def test_dataframe_aggregate_int(scalars_df_index, scalars_pandas_df_index, col, # Check dtype separately assert bf_result.dtype == "Int64" - + # Is otherwise "object" dtype + pd_result.index = pd_result.index.astype("string[pyarrow]") # Pandas may produce narrower numeric types - # Pandas has object index type assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) @@ -3146,6 +3146,7 @@ def test_dataframe_aggregate_bool(scalars_df_index, scalars_pandas_df_index, col # Pandas may produce narrower numeric types # Pandas has object index type + pd_result.index = pd_result.index.astype("string[pyarrow]") assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) @@ -3183,6 +3184,7 @@ def test_dataframe_aggregates( # Pandas may produce narrower numeric types, but bigframes always produces Float64 # Pandas has object index type + pd_result.index = pd_result.index.astype("string[pyarrow]") assert_series_equal( pd_result, bf_result, diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py new file mode 100644 index 00000000000..12c0d6e2597 --- /dev/null +++ b/tests/system/small/test_unordered.py @@ -0,0 +1,28 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pandas as pd + +import bigframes.pandas as bpd +from tests.system.utils import assert_pandas_df_equal + + +def test_unordered_mode_cache_aggregate(unordered_session): + pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session) + mean_diff = df - df.mean() + mean_diff.cache() + bf_result = mean_diff.to_pandas(ordered=False) + pd_result = pd_df - pd_df.mean() + + assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) From 3076a1387bb305bff97f2ea1d8b876fbd13eb1ef Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:54:28 -0700 Subject: [PATCH 639/921] chore: add overload to to_datetime and cleanup type errors in tests/system/small/test_pandas.py (#766) * chore: add overload to to_datetime and cleanup type errors in tests/unit/test_pandas.py * update import --- bigframes/pandas/__init__.py | 26 ++++++++++++++++ tests/system/small/test_pandas.py | 30 +++++-------------- .../pandas/core/tools/datetimes.py | 4 +-- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 603f6678a56..4166512219a 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -699,6 +699,32 @@ def read_gbq_function(function_name: str): read_gbq_function.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_function) +@typing.overload +def to_datetime( + arg: vendored_pandas_datetimes.local_scalars, + *, + utc: bool = False, + format: Optional[str] = None, + unit: Optional[str] = None, +) -> Union[pandas.Timestamp, datetime]: + ... + + +@typing.overload +def to_datetime( + arg: Union[ + vendored_pandas_datetimes.local_iterables, + bigframes.series.Series, + bigframes.dataframe.DataFrame, + ], + *, + utc: bool = False, + format: Optional[str] = None, + unit: Optional[str] = None, +) -> bigframes.series.Series: + ... + + def to_datetime( arg: Union[ vendored_pandas_datetimes.local_scalars, diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index 256046f8b10..30ffaa8a7d8 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -394,12 +394,8 @@ def test_cut(scalars_dfs): # make sure the result is a supported dtype assert bf_result.dtype == bpd.Int64Dtype() - - # TODO(b/340884971): fix type error - bf_result = bf_result.to_pandas() # type: ignore pd_result = pd_result.astype("Int64") - # TODO(b/340884971): fix type error - pd.testing.assert_series_equal(bf_result, pd_result) # type: ignore + pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) def test_cut_default_labels(scalars_dfs): @@ -529,13 +525,9 @@ def test_qcut(scalars_dfs, q): scalars_pandas_df["float64_col"], q, labels=False, duplicates="drop" ) bf_result = bpd.qcut(scalars_df["float64_col"], q, labels=False, duplicates="drop") - - # TODO(b/340884971): fix type error - bf_result = bf_result.to_pandas() # type: ignore pd_result = pd_result.astype("Int64") - # TODO(b/340884971): fix type error - pd.testing.assert_series_equal(bf_result, pd_result) # type: ignore + pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -572,9 +564,8 @@ def test_to_datetime_scalar(arg, utc, unit, format): ], ) def test_to_datetime_iterable(arg, utc, unit, format): - # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc, unit=unit, format=format) # type: ignore + bpd.to_datetime(arg, utc=utc, unit=unit, format=format) .to_pandas() .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") ) @@ -589,9 +580,8 @@ def test_to_datetime_iterable(arg, utc, unit, format): def test_to_datetime_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col = "int64_too" - # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(scalars_df[col], unit="s").to_pandas().astype("datetime64[s]") # type: ignore + bpd.to_datetime(scalars_df[col], unit="s").to_pandas().astype("datetime64[s]") ) pd_result = pd.Series(pd.to_datetime(scalars_pandas_df[col], unit="s")) pd.testing.assert_series_equal( @@ -614,8 +604,7 @@ def test_to_datetime_series(scalars_dfs): ], ) def test_to_datetime_unit_param(arg, unit): - # TODO(b/340884971): fix type error - bf_result = bpd.to_datetime(arg, unit=unit).to_pandas().astype("datetime64[ns]") # type: ignore + bf_result = bpd.to_datetime(arg, unit=unit).to_pandas().astype("datetime64[ns]") pd_result = pd.Series(pd.to_datetime(arg, unit=unit)).dt.floor("us") pd.testing.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False @@ -632,9 +621,8 @@ def test_to_datetime_unit_param(arg, unit): ], ) def test_to_datetime_format_param(arg, utc, format): - # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc, format=format) # type: ignore + bpd.to_datetime(arg, utc=utc, format=format) .to_pandas() .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") ) @@ -686,9 +674,8 @@ def test_to_datetime_format_param(arg, utc, format): ], ) def test_to_datetime_string_inputs(arg, utc, output_in_utc, format): - # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc, format=format) # type: ignore + bpd.to_datetime(arg, utc=utc, format=format) .to_pandas() .astype("datetime64[ns, UTC]" if output_in_utc else "datetime64[ns]") ) @@ -730,9 +717,8 @@ def test_to_datetime_string_inputs(arg, utc, output_in_utc, format): ], ) def test_to_datetime_timestamp_inputs(arg, utc, output_in_utc): - # TODO(b/340884971): fix type error bf_result = ( - bpd.to_datetime(arg, utc=utc) # type: ignore + bpd.to_datetime(arg, utc=utc) .to_pandas() .astype("datetime64[ns, UTC]" if output_in_utc else "datetime64[ns]") ) diff --git a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py index 3d460b2b16f..9cc0f26b199 100644 --- a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py +++ b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py @@ -1,14 +1,14 @@ # Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/tools/datetimes.py from datetime import datetime -from typing import Iterable, Mapping, Union +from typing import List, Mapping, Tuple, Union import pandas as pd from bigframes import constants, series local_scalars = Union[int, float, str, datetime] -local_iterables = Union[Iterable, pd.Series, pd.DataFrame, Mapping] +local_iterables = Union[List, Tuple, pd.Series, pd.DataFrame, Mapping] def to_datetime( From 11ed16be3aeebbf7f0ea2a4357c1f64eb2e3b53f Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:29:26 -0700 Subject: [PATCH 640/921] cleanup type errors in tests/unit/test_pandas.py (#767) --- bigframes/core/reshape/__init__.py | 2 +- bigframes/pandas/__init__.py | 2 +- tests/unit/test_pandas.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index 05cb5c7e94b..a23461bdb94 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -116,7 +116,7 @@ def cut( Iterable, ], *, - labels: Optional[bool] = None, + labels: Union[Iterable[str], bool, None] = None, ) -> bigframes.series.Series: if isinstance(bins, int) and bins <= 0: raise ValueError("`bins` should be a positive integer.") diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 4166512219a..0fb03b897d8 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -142,7 +142,7 @@ def cut( x: bigframes.series.Series, bins: int, *, - labels: Optional[bool] = None, + labels: Union[Iterable[str], bool, None] = None, ) -> bigframes.series.Series: return bigframes.core.reshape.cut( x, diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 54a7a79d3c3..408590d4bb1 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -95,8 +95,7 @@ def test_cut_raises_with_labels(): match="The 'labels' parameter must be either False or None.", ): mock_series = mock.create_autospec(bigframes.pandas.Series, instance=True) - # TODO(b/340893280): fix type error - bigframes.pandas.cut(mock_series, 4, labels=["a", "b", "c", "d"]) # type: ignore + bigframes.pandas.cut(mock_series, 4, labels=["a", "b", "c", "d"]) @pytest.mark.parametrize( From c921c8a262f1fc9e7cd9c93d8d5a2169c2d377f0 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:54:40 -0700 Subject: [PATCH 641/921] chore: update benchmark sorting logic, run environment, and add join benchmarks. (#768) --- noxfile.py | 6 +++--- .../db-benchmark/join/J1_1e9_NA_0_0/q1.py | 16 ++++++++++++++++ .../db-benchmark/join/J1_1e9_NA_0_0/q2.py | 16 ++++++++++++++++ .../db-benchmark/join/J1_1e9_NA_0_0/q3.py | 16 ++++++++++++++++ .../db-benchmark/join/J1_1e9_NA_0_0/q4.py | 16 ++++++++++++++++ .../db-benchmark/join/J1_1e9_NA_0_0/q5.py | 16 ++++++++++++++++ 6 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py create mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py create mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py create mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py create mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py diff --git a/noxfile.py b/noxfile.py index 533dba3ba84..177e0e2ab88 100644 --- a/noxfile.py +++ b/noxfile.py @@ -818,7 +818,7 @@ def notebook(session: nox.Session): _print_performance_report("notebooks/") -@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) +@nox.session(python=DEFAULT_PYTHON_VERSION) def benchmark(session: nox.Session): session.install("-e", ".[all]") base_path = os.path.join("scripts", "benchmark") @@ -855,7 +855,7 @@ def _print_performance_report(path: str): """ print("---BIGQUERY USAGE REPORT---") results_dict = {} - bytes_reports = sorted(Path(path).rglob("*.bytesprocessed"), key=lambda x: x.name) + bytes_reports = sorted(Path(path).rglob("*.bytesprocessed")) for bytes_report in bytes_reports: with open(bytes_report, "r") as bytes_file: filename = bytes_report.relative_to(path).with_suffix("") @@ -865,7 +865,7 @@ def _print_performance_report(path: str): results_dict[filename] = [query_count, total_bytes] os.remove(bytes_report) - millis_reports = sorted(Path(path).rglob("*.slotmillis"), key=lambda x: x.name) + millis_reports = sorted(Path(path).rglob("*.slotmillis")) for millis_report in millis_reports: with open(millis_report, "r") as millis_file: filename = millis_report.relative_to(path).with_suffix("") diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py new file mode 100644 index 00000000000..429dc72ad0a --- /dev/null +++ b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py + +import bigframes.pandas as bpd + +print("Join benchmark 1: small inner on int") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") +small = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e3_0_0") + +ans = x.merge(small, on="id1") +print(ans.shape) + +chk = [ans["v1"].sum(), ans["v2"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py new file mode 100644 index 00000000000..210c29f844f --- /dev/null +++ b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py + +import bigframes.pandas as bpd + +print("Join benchmark 2: medium inner on int") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") +medium = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e6_0_0") + +ans = x.merge(medium, on="id2") +print(ans.shape) + +chk = [ans["v1"].sum(), ans["v2"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py new file mode 100644 index 00000000000..d88d9436044 --- /dev/null +++ b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py + +import bigframes.pandas as bpd + +print("Join benchmark 3: medium outer on int") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") +medium = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e6_0_0") + +ans = x.merge(medium, how="left", on="id2") +print(ans.shape) + +chk = [ans["v1"].sum(), ans["v2"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py new file mode 100644 index 00000000000..9167043d9aa --- /dev/null +++ b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py + +import bigframes.pandas as bpd + +print("Join benchmark 4: medium inner on factor") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") +medium = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e6_0_0") + +ans = x.merge(medium, on="id5") +print(ans.shape) + +chk = [ans["v1"].sum(), ans["v2"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py new file mode 100644 index 00000000000..39eb23ac457 --- /dev/null +++ b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py + +import bigframes.pandas as bpd + +print("Join benchmark 5: big inner on int") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") +big = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e9_0_0") + +ans = x.merge(big, on="id3") +print(ans.shape) + +chk = [ans["v1"].sum(), ans["v2"].sum()] +print(chk) + +bpd.reset_session() From e627896b617fbf08009b12e0dd1b4d3ac5b27332 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:58:34 -0700 Subject: [PATCH 642/921] chore: add q4-7 for groupby benchmark (#765) * chore: add q4-7 for groupby benchmark * update benchmark print message. --- .../db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py | 16 ++++++++++++++++ .../db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py | 16 ++++++++++++++++ .../db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py | 16 ++++++++++++++++ .../db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py | 18 ++++++++++++++++++ scripts/benchmark/db-benchmark/sort | 0 5 files changed, 66 insertions(+) create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py create mode 100644 scripts/benchmark/db-benchmark/sort diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py new file mode 100644 index 00000000000..e4f769545e5 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 4: mean v1:v3 by id4") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = x.groupby("id4", as_index=False, dropna=False).agg( + {"v1": "mean", "v2": "mean", "v3": "mean"} +) +print(ans.shape) +chk = [ans["v1"].sum(), ans["v2"].sum(), ans["v3"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py new file mode 100644 index 00000000000..d34a6c055f1 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 5: sum v1:v3 by id6") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = x.groupby("id6", as_index=False, dropna=False).agg( + {"v1": "sum", "v2": "sum", "v3": "sum"} +) +print(ans.shape) +chk = [ans["v1"].sum(), ans["v2"].sum(), ans["v3"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py new file mode 100644 index 00000000000..0f3240a1298 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 6: median v3 sd v3 by id4 id5") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = x.groupby(["id4", "id5"], as_index=False, dropna=False).agg( + {"v3": ["median", "std"]} +) +print(ans.shape) +chk = [ans["v3"]["median"].sum(), ans["v3"]["std"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py new file mode 100644 index 00000000000..78e1e94b853 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py @@ -0,0 +1,18 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 7: max v1 - min v2 by id3") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = ( + x.groupby("id3", as_index=False, dropna=False) + .agg({"v1": "max", "v2": "min"}) + .assign(range_v1_v2=lambda x: x["v1"] - x["v2"])[["id3", "range_v1_v2"]] +) +print(ans.shape) +chk = [ans["range_v1_v2"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/sort b/scripts/benchmark/db-benchmark/sort new file mode 100644 index 00000000000..e69de29bb2d From e7c6fc147f68f5713e0befca080da9e0413886e4 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:13:21 -0700 Subject: [PATCH 643/921] chore: cleanup type errors in tests/system/small/operations/test_datetimes.py (#769) --- bigframes/series.py | 8 ++++---- tests/system/small/operations/test_datetimes.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/bigframes/series.py b/bigframes/series.py index 367301f08e0..d858060aec0 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -783,10 +783,10 @@ def pow(self, other: float | int | Series) -> Series: def rpow(self, other: float | int | Series) -> Series: return self._apply_binary_op(other, ops.pow_op, reverse=True) - def __lt__(self, other: float | int | Series) -> Series: # type: ignore + def __lt__(self, other: float | int | str | Series) -> Series: return self.lt(other) - def __le__(self, other: float | int | Series) -> Series: # type: ignore + def __le__(self, other: float | int | str | Series) -> Series: return self.le(other) def lt(self, other) -> Series: @@ -795,10 +795,10 @@ def lt(self, other) -> Series: def le(self, other) -> Series: return self._apply_binary_op(other, ops.le_op) - def __gt__(self, other: float | int | Series) -> Series: # type: ignore + def __gt__(self, other: float | int | str | Series) -> Series: return self.gt(other) - def __ge__(self, other: float | int | Series) -> Series: # type: ignore + def __ge__(self, other: float | int | str | Series) -> Series: return self.ge(other) def gt(self, other) -> Series: diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 838bc111085..c5c649c638d 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -310,9 +310,8 @@ def test_dt_floor(scalars_dfs, col_name, freq): def test_dt_compare_coerce_str_datetime(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_series: bigframes.series.Series = scalars_df["datetime_col"] - # TODO(b/340878286): fix type error - bf_result = (bf_series >= "2024-01-01").to_pandas() # type: ignore + bf_result = (bf_series >= "2024-01-01").to_pandas() pd_result = scalars_pandas_df["datetime_col"] >= pd.to_datetime("2024-01-01") # pandas produces pyarrow bool dtype From 91efe8ea3883f52f527535155bb9bfac52535a02 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:37:20 -0700 Subject: [PATCH 644/921] refactor: create a basic set of SQL AST nodes (#757) * feat: define googlesql AST nodes * updates docstring * address comments: quoting, adding as_alias, select_all to respect googlesql restrictly --- bigframes/core/compile/googlesql/__init__.py | 52 ++++++ bigframes/core/compile/googlesql/abc.py | 25 +++ .../core/compile/googlesql/expression.py | 95 ++++++++++ bigframes/core/compile/googlesql/query.py | 165 ++++++++++++++++++ tests/unit/core/compiler/__init__.py | 13 ++ tests/unit/core/compiler/test_googlesql.py | 155 ++++++++++++++++ 6 files changed, 505 insertions(+) create mode 100644 bigframes/core/compile/googlesql/__init__.py create mode 100644 bigframes/core/compile/googlesql/abc.py create mode 100644 bigframes/core/compile/googlesql/expression.py create mode 100644 bigframes/core/compile/googlesql/query.py create mode 100644 tests/unit/core/compiler/__init__.py create mode 100644 tests/unit/core/compiler/test_googlesql.py diff --git a/bigframes/core/compile/googlesql/__init__.py b/bigframes/core/compile/googlesql/__init__.py new file mode 100644 index 00000000000..32265c0d518 --- /dev/null +++ b/bigframes/core/compile/googlesql/__init__.py @@ -0,0 +1,52 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Python classes representing GoogleSQL syntax nodes, adhering to the official syntax: +https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax""" + +from __future__ import annotations + +from bigframes.core.compile.googlesql.expression import ( + AliasExpression, + ColumnExpression, + CTEExpression, + StarExpression, + TableExpression, +) +from bigframes.core.compile.googlesql.query import ( + AsAlias, + FromClause, + FromItem, + NonRecursiveCTE, + QueryExpr, + Select, + SelectAll, + SelectExpression, +) + +__all__ = [ + "AliasExpression", + "AsAlias", + "ColumnExpression", + "CTEExpression", + "FromClause", + "FromItem", + "NonRecursiveCTE", + "QueryExpr", + "Select", + "SelectAll", + "SelectExpression", + "StarExpression", + "TableExpression", +] diff --git a/bigframes/core/compile/googlesql/abc.py b/bigframes/core/compile/googlesql/abc.py new file mode 100644 index 00000000000..081836467c2 --- /dev/null +++ b/bigframes/core/compile/googlesql/abc.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import abc + + +class SQLSyntax(abc.ABC): + """Abstract base class provides GoogleSQL syntax.""" + + @abc.abstractmethod + def sql(self): + ... diff --git a/bigframes/core/compile/googlesql/expression.py b/bigframes/core/compile/googlesql/expression.py new file mode 100644 index 00000000000..702aa2c5e55 --- /dev/null +++ b/bigframes/core/compile/googlesql/expression.py @@ -0,0 +1,95 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import dataclasses +import typing + +import bigframes.core.compile.googlesql.abc as abc + +"""This module represents GoogleSQL `expression` and its extensions. +Core class: + +* `expression`: Models basic SQL expressions. + +Extended classes (not part of standard GoogleSQL syntax, but added for convenience): + +* `ColumnExpression`: Represents column references. +* `TableExpression`: Represents table references. +* `AliasExpression`: Represents aliased expressions. +* ... +""" + + +@dataclasses.dataclass +class Expression(abc.SQLSyntax): + pass + + +@dataclasses.dataclass +class ColumnExpression(Expression): + name: str + parent: typing.Optional[TableExpression | AliasExpression | CTEExpression] = None + + def sql(self) -> str: + if self.parent is not None: + return f"{self.parent.sql()}.`{self.name}`" + return f"`{self.name}`" + + +@dataclasses.dataclass +class StarExpression(Expression): + parent: typing.Optional[TableExpression | AliasExpression | CTEExpression] = None + + def sql(self) -> str: + if self.parent is not None: + return f"{self.parent.sql()}.*" + return "*" + + +@dataclasses.dataclass +class TableExpression(Expression): + table_id: str + dataset_id: typing.Optional[str] = None + project_id: typing.Optional[str] = None + + def __post_init__(self): + if self.project_id is not None and self.dataset_id is None: + raise ValueError("The `dataset_id` is missing.") + + def sql(self) -> str: + text = [] + if self.project_id is not None: + text.append(f"`{self.project_id}`") + if self.dataset_id is not None: + text.append(f"`{self.dataset_id}`") + text.append(f"`{self.table_id}`") + return ".".join(text) + + +@dataclasses.dataclass +class AliasExpression(Expression): + alias: str + + def sql(self) -> str: + return f"`{self.alias}`" + + +@dataclasses.dataclass +class CTEExpression(Expression): + name: str + + def sql(self) -> str: + return f"`{self.name}`" diff --git a/bigframes/core/compile/googlesql/query.py b/bigframes/core/compile/googlesql/query.py new file mode 100644 index 00000000000..6210aa67f4b --- /dev/null +++ b/bigframes/core/compile/googlesql/query.py @@ -0,0 +1,165 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import dataclasses +import typing + +import bigframes.core.compile.googlesql.abc as abc +import bigframes.core.compile.googlesql.expression as expr + +"""This module provides a structured representation of GoogleSQL syntax using nodes. +Each node's name and child nodes are designed to strictly follow the official GoogleSQL +syntax rules outlined in the documentation: +https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax""" + + +@dataclasses.dataclass +class QueryExpr(abc.SQLSyntax): + """This class represents GoogleSQL `query_expr` syntax.""" + + select: Select + with_cte_list: typing.Sequence[NonRecursiveCTE] = () + + def sql(self) -> str: + text = [] + if len(self.with_cte_list) > 0: + with_cte_text = ",\n".join( + [with_cte.sql() for with_cte in self.with_cte_list] + ) + text.append(f"WITH {with_cte_text}") + + text.append(self.select.sql()) + return "\n".join(text) + + +@dataclasses.dataclass +class Select(abc.SQLSyntax): + """This class represents GoogleSQL `select` syntax.""" + + select_list: typing.Sequence[typing.Union[SelectExpression, SelectAll]] + from_clause_list: typing.Sequence[FromClause] = () + + def sql(self) -> str: + text = ["SELECT"] + + select_list_sql = ",\n".join([select.sql() for select in self.select_list]) + text.append(select_list_sql) + + if self.from_clause_list is not None: + from_clauses_sql = ",\n".join( + [clause.sql() for clause in self.from_clause_list] + ) + text.append(f"FROM\n{from_clauses_sql}") + return "\n".join(text) + + +@dataclasses.dataclass +class SelectExpression(abc.SQLSyntax): + """This class represents `select_expression`.""" + + expression: expr.ColumnExpression + alias: typing.Optional[expr.AliasExpression] = None + + def sql(self) -> str: + if self.alias is None: + return self.expression.sql() + else: + return f"{self.expression.sql()} AS {self.alias.sql()}" + + +@dataclasses.dataclass +class SelectAll(abc.SQLSyntax): + """This class represents `select_all` (aka. `SELECT *`).""" + + expression: expr.StarExpression + + def sql(self) -> str: + return self.expression.sql() + + +@dataclasses.dataclass +class FromClause(abc.SQLSyntax): + """This class represents GoogleSQL `from_clause` syntax.""" + + from_item: FromItem + + def sql(self) -> str: + return self.from_item.sql() + + +@dataclasses.dataclass +class FromItem(abc.SQLSyntax): + """This class represents GoogleSQL `from_item` syntax.""" + + table_name: typing.Optional[expr.TableExpression] = None + # Note: Temporarily introduces the `str` type to interact with pre-existing, + # compiled SQL strings. + query_expr: typing.Optional[QueryExpr | str] = None + cte_name: typing.Optional[expr.CTEExpression] = None + as_alias: typing.Optional[AsAlias] = None + + def __post_init__(self): + non_none = sum( + expr is not None + for expr in [ + self.table_name, + self.query_expr, + self.cte_name, + ] + ) + if non_none != 1: + raise ValueError("Exactly one of expressions must be provided.") + + def sql(self) -> str: + if self.table_name is not None: + text = self.table_name.sql() + elif self.query_expr is not None: + text = ( + self.query_expr + if isinstance(self.query_expr, str) + else self.query_expr.sql() + ) + text = f"({text})" + elif self.cte_name is not None: + text = self.cte_name.sql() + else: + raise ValueError("One of from items must be provided.") + + if self.as_alias is None: + return text + else: + return f"{text} {self.as_alias.sql()}" + + +@dataclasses.dataclass +class NonRecursiveCTE(abc.SQLSyntax): + """This class represents GoogleSQL `non_recursive_cte` syntax.""" + + cte_name: expr.CTEExpression + query_expr: QueryExpr + + def sql(self) -> str: + return f"{self.cte_name.sql()} AS (\n{self.query_expr.sql()}\n)" + + +@dataclasses.dataclass +class AsAlias(abc.SQLSyntax): + """This class represents GoogleSQL `as_alias` syntax.""" + + alias: expr.AliasExpression + + def sql(self) -> str: + return f"AS {self.alias.sql()}" diff --git a/tests/unit/core/compiler/__init__.py b/tests/unit/core/compiler/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/tests/unit/core/compiler/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/core/compiler/test_googlesql.py b/tests/unit/core/compiler/test_googlesql.py new file mode 100644 index 00000000000..70ca5cfa12b --- /dev/null +++ b/tests/unit/core/compiler/test_googlesql.py @@ -0,0 +1,155 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.core.compile.googlesql as sql + + +@pytest.mark.parametrize( + ("table_id", "dataset_id", "project_id", "expected"), + [ + pytest.param("a", None, None, "`a`"), + pytest.param("a", "b", None, "`b`.`a`"), + pytest.param("a", "b", "c", "`c`.`b`.`a`"), + pytest.param("a", None, "c", None, marks=pytest.mark.xfail(raises=ValueError)), + ], +) +def test_table_expression(table_id, dataset_id, project_id, expected): + expr = sql.TableExpression( + table_id=table_id, dataset_id=dataset_id, project_id=project_id + ) + assert expr.sql() == expected + + +@pytest.mark.parametrize( + ("table_name", "alias", "expected"), + [ + pytest.param(None, None, None, marks=pytest.mark.xfail(raises=ValueError)), + pytest.param("a", None, "`a`"), + pytest.param("a", "aa", "`a` AS `aa`"), + ], +) +def test_from_item_w_table_name(table_name, alias, expected): + expr = sql.FromItem( + table_name=None + if table_name is None + else sql.TableExpression(table_id=table_name), + as_alias=None + if alias is None + else sql.AsAlias(sql.AliasExpression(alias=alias)), + ) + assert expr.sql() == expected + + +def test_from_item_w_query_expr(): + from_clause = sql.FromClause( + sql.FromItem(table_name=sql.TableExpression(table_id="table_a")) + ) + select = sql.Select( + select_list=[sql.SelectAll(sql.StarExpression())], + from_clause_list=[from_clause], + ) + query_expr = sql.QueryExpr(select=select) + expected = "SELECT\n*\nFROM\n`table_a`" + + # A QueryExpr object + expr = sql.FromItem(query_expr=query_expr) + assert expr.sql() == f"({expected})" + + # A str object + expr = sql.FromItem(query_expr=expected) + assert expr.sql() == f"({expected})" + + +def test_from_item_w_cte(): + expr = sql.FromItem(cte_name=sql.CTEExpression("test")) + assert expr.sql() == "`test`" + + +@pytest.mark.parametrize( + ("col_name", "alias", "expected"), + [ + pytest.param("a", None, "`a`"), + pytest.param("a", "aa", "`a` AS `aa`"), + ], +) +def test_select_expression(col_name, alias, expected): + expr = sql.SelectExpression( + expression=sql.ColumnExpression(col_name), + alias=None if alias is None else sql.AliasExpression(alias=alias), + ) + assert expr.sql() == expected + + +def test_select(): + select_1 = sql.SelectExpression(expression=sql.ColumnExpression("a")) + select_2 = sql.SelectExpression( + expression=sql.ColumnExpression("b"), alias=sql.AliasExpression(alias="bb") + ) + from_1 = sql.FromItem(table_name=sql.TableExpression(table_id="table_a")) + from_2 = sql.FromItem( + query_expr="SELECT * FROM project.table_b", + as_alias=sql.AsAlias(sql.AliasExpression(alias="table_b")), + ) + expr = sql.Select( + select_list=[select_1, select_2], + from_clause_list=[sql.FromClause(from_1), sql.FromClause(from_2)], + ) + expected = "SELECT\n`a`,\n`b` AS `bb`\nFROM\n`table_a`,\n(SELECT * FROM project.table_b) AS `table_b`" + + assert expr.sql() == expected + + +def test_query_expr_w_cte(): + # Test a simple SELECT query. + from_clause1 = sql.FromClause( + sql.FromItem(table_name=sql.TableExpression(table_id="table_a")) + ) + select1 = sql.Select( + select_list=[sql.SelectAll(sql.StarExpression())], + from_clause_list=[from_clause1], + ) + query1 = sql.QueryExpr(select=select1) + query1_sql = "SELECT\n*\nFROM\n`table_a`" + assert query1.sql() == query1_sql + + # Test a query with CTE statements. + cte1 = sql.NonRecursiveCTE(cte_name=sql.CTEExpression("a"), query_expr=query1) + cte2 = sql.NonRecursiveCTE(cte_name=sql.CTEExpression("b"), query_expr=query1) + + cte1_sql = f"`a` AS (\n{query1_sql}\n)" + cte2_sql = f"`b` AS (\n{query1_sql}\n)" + assert cte1.sql() == cte1_sql + assert cte2.sql() == cte2_sql + + with_cte_list = [cte1, cte2] + select2 = sql.Select( + select_list=[ + sql.SelectExpression( + sql.ColumnExpression(parent=cte1.cte_name, name="column_x") + ), + sql.SelectAll(sql.StarExpression(parent=cte2.cte_name)), + ], + from_clause_list=[ + sql.FromClause(sql.FromItem(cte_name=cte1.cte_name)), + sql.FromClause(sql.FromItem(cte_name=cte2.cte_name)), + ], + ) + select2_sql = "SELECT\n`a`.`column_x`,\n`b`.*\nFROM\n`a`,\n`b`" + assert select2.sql() == select2_sql + + query2 = sql.QueryExpr(select=select2, with_cte_list=with_cte_list) + query2_sql = f"WITH {cte1_sql},\n{cte2_sql}\n{select2_sql}" + assert query2.sql() == query2_sql From 11af385ed621011ddc5824d9941a087f63b576cf Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:07:57 -0700 Subject: [PATCH 645/921] chore: overload update for pre-commit mypy. (#773) --- bigframes/core/tools/datetimes.py | 2 +- bigframes/pandas/__init__.py | 18 +++++++++--------- .../pandas/core/tools/datetimes.py | 1 - 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/bigframes/core/tools/datetimes.py b/bigframes/core/tools/datetimes.py index 5eac4cceb94..5d8d8c96854 100644 --- a/bigframes/core/tools/datetimes.py +++ b/bigframes/core/tools/datetimes.py @@ -28,7 +28,7 @@ def to_datetime( arg: Union[ - vendored_pandas_datetimes.local_scalars, + Union[int, float, str, datetime], vendored_pandas_datetimes.local_iterables, bigframes.series.Series, bigframes.dataframe.DataFrame, diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 0fb03b897d8..f6f9aec8007 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -701,33 +701,33 @@ def read_gbq_function(function_name: str): @typing.overload def to_datetime( - arg: vendored_pandas_datetimes.local_scalars, + arg: Union[ + vendored_pandas_datetimes.local_iterables, + bigframes.series.Series, + bigframes.dataframe.DataFrame, + ], *, utc: bool = False, format: Optional[str] = None, unit: Optional[str] = None, -) -> Union[pandas.Timestamp, datetime]: +) -> bigframes.series.Series: ... @typing.overload def to_datetime( - arg: Union[ - vendored_pandas_datetimes.local_iterables, - bigframes.series.Series, - bigframes.dataframe.DataFrame, - ], + arg: Union[int, float, str, datetime], *, utc: bool = False, format: Optional[str] = None, unit: Optional[str] = None, -) -> bigframes.series.Series: +) -> Union[pandas.Timestamp, datetime]: ... def to_datetime( arg: Union[ - vendored_pandas_datetimes.local_scalars, + Union[int, float, str, datetime], vendored_pandas_datetimes.local_iterables, bigframes.series.Series, bigframes.dataframe.DataFrame, diff --git a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py index 9cc0f26b199..52b287b9490 100644 --- a/third_party/bigframes_vendored/pandas/core/tools/datetimes.py +++ b/third_party/bigframes_vendored/pandas/core/tools/datetimes.py @@ -7,7 +7,6 @@ from bigframes import constants, series -local_scalars = Union[int, float, str, datetime] local_iterables = Union[List, Tuple, pd.Series, pd.DataFrame, Mapping] From d11274c30b27340d0c01e11c86daf05257ea6630 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:17:29 -0700 Subject: [PATCH 646/921] chore: cleanup type errors in tests/system/small/test_dataframe.py (#771) * chore: cleanup type errors in tests/system/small/test_dataframe.py * Some updates --- tests/system/small/test_dataframe.py | 117 ++++++++++++--------------- 1 file changed, 52 insertions(+), 65 deletions(-) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ffc09a1a1fd..d5854bd8d0b 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -17,7 +17,7 @@ import sys import tempfile import typing -from typing import Tuple +from typing import Dict, List, Tuple import geopandas as gpd # type: ignore import numpy as np @@ -146,9 +146,9 @@ def test_df_construct_inline_respects_location(): with bpd.option_context("bigquery.location", "europe-west1"): df = bpd.DataFrame([[1, 2, 3], [4, 5, 6]]) repr(df) + assert df.query_job is not None + table = bpd.get_global_session().bqclient.get_table(df.query_job.destination) - # TODO(b/340876936): fix type error - table = bpd.get_global_session().bqclient.get_table(df.query_job.destination) # type: ignore assert table.location == "europe-west1" @@ -753,10 +753,9 @@ def test_assign_listlike_to_empty_df(session): def test_assign_to_empty_df_multiindex_error(session): empty_df = dataframe.DataFrame(session=session) empty_pandas_df = pd.DataFrame() - # TODO(b/340876936): fix type error - empty_df["empty_col_1"] = [] # type: ignore - # TODO(b/340876936): fix type error - empty_df["empty_col_2"] = [] # type: ignore + + empty_df["empty_col_1"] = typing.cast(series.Series, []) + empty_df["empty_col_2"] = typing.cast(series.Series, []) empty_pandas_df["empty_col_1"] = [] empty_pandas_df["empty_col_2"] = [] empty_df = empty_df.set_index(["empty_col_1", "empty_col_2"]) @@ -1340,40 +1339,25 @@ def test_merge_left_on_right_on(scalars_dfs, merge_how): def test_get_dtypes(scalars_df_default_index): dtypes = scalars_df_default_index.dtypes + dtypes_dict: Dict[str, bigframes.dtypes.Dtype] = { + "bool_col": pd.BooleanDtype(), + "bytes_col": pd.ArrowDtype(pa.binary()), + "date_col": pd.ArrowDtype(pa.date32()), + "datetime_col": pd.ArrowDtype(pa.timestamp("us")), + "geography_col": gpd.array.GeometryDtype(), + "int64_col": pd.Int64Dtype(), + "int64_too": pd.Int64Dtype(), + "numeric_col": pd.ArrowDtype(pa.decimal128(38, 9)), + "float64_col": pd.Float64Dtype(), + "rowindex": pd.Int64Dtype(), + "rowindex_2": pd.Int64Dtype(), + "string_col": pd.StringDtype(storage="pyarrow"), + "time_col": pd.ArrowDtype(pa.time64("us")), + "timestamp_col": pd.ArrowDtype(pa.timestamp("us", tz="UTC")), + } pd.testing.assert_series_equal( dtypes, - pd.Series( - { - # TODO(b/340876936): fix type error - "bool_col": pd.BooleanDtype(), # type: ignore - # TODO(b/340876936): fix type error - "bytes_col": pd.ArrowDtype(pa.binary()), # type: ignore - # TODO(b/340876936): fix type error - "date_col": pd.ArrowDtype(pa.date32()), # type: ignore - # TODO(b/340876936): fix type error - "datetime_col": pd.ArrowDtype(pa.timestamp("us")), # type: ignore - # TODO(b/340876936): fix type error - "geography_col": gpd.array.GeometryDtype(), # type: ignore - # TODO(b/340876936): fix type error - "int64_col": pd.Int64Dtype(), # type: ignore - # TODO(b/340876936): fix type error - "int64_too": pd.Int64Dtype(), # type: ignore - # TODO(b/340876936): fix type error - "numeric_col": pd.ArrowDtype(pa.decimal128(38, 9)), # type: ignore - # TODO(b/340876936): fix type error - "float64_col": pd.Float64Dtype(), # type: ignore - # TODO(b/340876936): fix type error - "rowindex": pd.Int64Dtype(), # type: ignore - # TODO(b/340876936): fix type error - "rowindex_2": pd.Int64Dtype(), # type: ignore - # TODO(b/340876936): fix type error - "string_col": pd.StringDtype(storage="pyarrow"), # type: ignore - # TODO(b/340876936): fix type error - "time_col": pd.ArrowDtype(pa.time64("us")), # type: ignore - # TODO(b/340876936): fix type error - "timestamp_col": pd.ArrowDtype(pa.timestamp("us", tz="UTC")), # type: ignore - } - ), + pd.Series(dtypes_dict), ) @@ -1828,10 +1812,9 @@ def test_df_update(overwrite, filter_func): if pd.__version__.startswith("1."): pytest.skip("dtype handled differently in pandas 1.x.") - # TODO(b/340876936): fix type error - index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") # type: ignore - # TODO(b/340876936): fix type error - index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") # type: ignore + index1: pandas.Index = pandas.Index([1, 2, 3, 4], dtype="Int64") + + index2: pandas.Index = pandas.Index([1, 2, 4, 5], dtype="Int64") pd_df1 = pandas.DataFrame( {"a": [1, None, 3, 4], "b": [5, 6, None, 8]}, dtype="Int64", index=index1 ) @@ -1891,10 +1874,10 @@ def test_df_idxmax(): ], ) def test_df_align(join, axis): - # TODO(b/340876936): fix type error - index1 = pandas.Index([1, 2, 3, 4], dtype="Int64") # type: ignore - # TODO(b/340876936): fix type error - index2 = pandas.Index([1, 2, 4, 5], dtype="Int64") # type: ignore + + index1: pandas.Index = pandas.Index([1, 2, 3, 4], dtype="Int64") + + index2: pandas.Index = pandas.Index([1, 2, 4, 5], dtype="Int64") pd_df1 = pandas.DataFrame( {"a": [1, None, 3, 4], "b": [5, 6, None, 8]}, dtype="Int64", index=index1 ) @@ -1911,10 +1894,11 @@ def test_df_align(join, axis): pd_result1, pd_result2 = pd_df1.align(pd_df2, join=join, axis=axis) # Don't check dtype as pandas does unnecessary float conversion - # TODO(b/340876936): fix type error - pd.testing.assert_frame_equal(bf_result1.to_pandas(), pd_result1, check_dtype=False) # type: ignore - # TODO(b/340876936): fix type error - pd.testing.assert_frame_equal(bf_result2.to_pandas(), pd_result2, check_dtype=False) # type: ignore + assert isinstance(bf_result1, dataframe.DataFrame) and isinstance( + bf_result2, dataframe.DataFrame + ) + pd.testing.assert_frame_equal(bf_result1.to_pandas(), pd_result1, check_dtype=False) + pd.testing.assert_frame_equal(bf_result2.to_pandas(), pd_result2, check_dtype=False) def test_combine_first( @@ -2568,11 +2552,15 @@ def test_df_transpose(): # Include some floats to ensure type coercion values = [[0, 3.5, True], [1, 4.5, False], [2, 6.5, None]] # Test complex case of both axes being multi-indices with non-unique elements - # TODO(b/340876936): fix type error - columns = pd.Index(["A", "B", "A"], dtype=pd.StringDtype(storage="pyarrow")) # type: ignore + + columns: pandas.Index = pd.Index( + ["A", "B", "A"], dtype=pd.StringDtype(storage="pyarrow") + ) columns_multi = pd.MultiIndex.from_arrays([columns, columns], names=["c1", "c2"]) - # TODO(b/340876936): fix type error - index = pd.Index(["b", "a", "a"], dtype=pd.StringDtype(storage="pyarrow")) # type: ignore + + index: pandas.Index = pd.Index( + ["b", "a", "a"], dtype=pd.StringDtype(storage="pyarrow") + ) rows_multi = pd.MultiIndex.from_arrays([index, index], names=["r1", "r2"]) pd_df = pandas.DataFrame(values, index=rows_multi, columns=columns_multi) @@ -3742,10 +3730,9 @@ def test_df_setattr_index(): [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] ) bf_df = dataframe.DataFrame(pd_df) - # TODO(b/340876936): fix type error - pd_df.index = [4, 5] # type: ignore - # TODO(b/340876936): fix type error - bf_df.index = [4, 5] # type: ignore + + pd_df.index = pandas.Index([4, 5]) + bf_df.index = [4, 5] assert_pandas_df_equal( pd_df, bf_df.to_pandas(), check_index_type=False, check_dtype=False @@ -3757,10 +3744,10 @@ def test_df_setattr_columns(): [[1, 1, 1], [1, 1, 1]], columns=["index", "columns", "my_column"] ) bf_df = dataframe.DataFrame(pd_df) - # TODO(b/340876936): fix type error - pd_df.columns = [4, 5, 6] # type: ignore - # TODO(b/340876936): fix type error - bf_df.columns = [4, 5, 6] # type: ignore + + pd_df.columns = typing.cast(pandas.Index, pandas.Index([4, 5, 6])) + + bf_df.columns = pandas.Index([4, 5, 6]) assert_pandas_df_equal( pd_df, bf_df.to_pandas(), check_index_type=False, check_dtype=False @@ -3854,8 +3841,8 @@ def test_iloc_list_multiindex(scalars_dfs): def test_iloc_empty_list(scalars_df_index, scalars_pandas_df_index): - # TODO(b/340876936): fix type error - index_list = [] # type: ignore + + index_list: List[int] = [] bf_result = scalars_df_index.iloc[index_list] pd_result = scalars_pandas_df_index.iloc[index_list] From b7b134e7c7efb13d4ddf661ac8adc1cc2646312b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:39:18 -0700 Subject: [PATCH 647/921] chore(main): release 1.9.0 (#744) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 24 ++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cad061ce05b..d585b5b1c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.9.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.8.0...v1.9.0) (2024-06-10) + + +### Features + +* Allow functions returned from `bpd.read_gbq_function` to execute outside of `apply` ([#706](https://github.com/googleapis/python-bigquery-dataframes/issues/706)) ([ad7d8ac](https://github.com/googleapis/python-bigquery-dataframes/commit/ad7d8ac1247ec3b9532dd5375265c36907f50da2)) +* Support `bigquery.vector_search()` ([#736](https://github.com/googleapis/python-bigquery-dataframes/issues/736)) ([dad66fd](https://github.com/googleapis/python-bigquery-dataframes/commit/dad66fdd22bb2d507e7f366c970d971554598cf3)) +* Support `score()` in GeminiTextGenerator ([#740](https://github.com/googleapis/python-bigquery-dataframes/issues/740)) ([b2c7d8b](https://github.com/googleapis/python-bigquery-dataframes/commit/b2c7d8b28e235c839370818137fba71796c9f02a)) +* Support bytes type in `remote_function` ([#761](https://github.com/googleapis/python-bigquery-dataframes/issues/761)) ([4915424](https://github.com/googleapis/python-bigquery-dataframes/commit/4915424a68f36542e901a0ac27946f1ecb2d05ab)) +* Support fit() in GeminiTextGenerator ([#758](https://github.com/googleapis/python-bigquery-dataframes/issues/758)) ([d751f5c](https://github.com/googleapis/python-bigquery-dataframes/commit/d751f5cd1cf578618eabbb992cfb6b0a3c36608c)) + + +### Bug Fixes + +* ARIMAPlus loads auto_arima_min_order param ([#752](https://github.com/googleapis/python-bigquery-dataframes/issues/752)) ([39d7013](https://github.com/googleapis/python-bigquery-dataframes/commit/39d7013a8a8d2908f20bfe54a7dc8de166323b90)) +* Improve to_pandas_batches for large results ([#746](https://github.com/googleapis/python-bigquery-dataframes/issues/746)) ([61f18cb](https://github.com/googleapis/python-bigquery-dataframes/commit/61f18cb63f2785c03dc612a34c030079fc8f4172)) +* Resolve issue with unset thread-local options ([#741](https://github.com/googleapis/python-bigquery-dataframes/issues/741)) ([d93dbaf](https://github.com/googleapis/python-bigquery-dataframes/commit/d93dbafe2bb405c60f7141d9ae4135db4ffdb702)) + + +### Documentation + +* Fix ML.EVALUATE spelling ([#749](https://github.com/googleapis/python-bigquery-dataframes/issues/749)) ([7899749](https://github.com/googleapis/python-bigquery-dataframes/commit/7899749505a75ed89c68e9df64124a153644de96)) +* Remove LogisticRegression normal_equation strategy ([#753](https://github.com/googleapis/python-bigquery-dataframes/issues/753)) ([ea5d367](https://github.com/googleapis/python-bigquery-dataframes/commit/ea5d367d5ecc6826d30082e75c957af8362c9e61)) + ## [1.8.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.7.0...v1.8.0) (2024-05-31) diff --git a/bigframes/version.py b/bigframes/version.py index 74a30e35b78..56a12008579 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.0" +__version__ = "1.9.0" From e8bab681a2d07636e5809e804f4fd81b0d582685 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:36:58 -0700 Subject: [PATCH 648/921] feat: add dataframe.insert (#770) * feat: add dataframe.insert * update logic. * fix --- bigframes/dataframe.py | 28 ++++++++++++ tests/system/small/test_dataframe.py | 38 ++++++++++++++++ .../bigframes_vendored/pandas/core/frame.py | 45 +++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index f12c3467768..f78dee16424 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1305,6 +1305,34 @@ def nsmallest( column_ids = self._sql_names(columns) return DataFrame(block_ops.nsmallest(self._block, n, column_ids, keep=keep)) + def insert( + self, + loc: int, + column: blocks.Label, + value: SingleItemValue, + allow_duplicates: bool = False, + ): + column_count = len(self.columns) + if loc > column_count: + raise IndexError( + f"Column index {loc} is out of bounds with {column_count} total columns." + ) + if (column in self.columns) and not allow_duplicates: + raise ValueError(f"cannot insert {column}, already exists") + + temp_column = bigframes.core.guid.generate_guid(prefix=str(column)) + df = self._assign_single_item(temp_column, value) + + block = df._get_block() + value_columns = typing.cast(List, block.value_columns) + value_columns, new_column = value_columns[:-1], value_columns[-1] + value_columns.insert(loc, new_column) + + block = block.select_columns(value_columns) + block = block.rename(columns={temp_column: column}) + + self._set_block(block) + def drop( self, labels: typing.Any = None, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index d5854bd8d0b..8de7f2c2050 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -270,6 +270,44 @@ def test_get_columns_default(scalars_dfs): assert result == "default_val" +@pytest.mark.parametrize( + ("loc", "column", "value", "allow_duplicates"), + [ + (0, 666, 2, False), + (5, "float64_col", 2.2, True), + (13, "rowindex_2", [8, 7, 6, 5, 4, 3, 2, 1, 0], True), + pytest.param( + 14, + "test", + 2, + False, + marks=pytest.mark.xfail( + raises=IndexError, + ), + ), + pytest.param( + 12, + "int64_col", + 2, + False, + marks=pytest.mark.xfail( + raises=ValueError, + ), + ), + ], +) +def test_insert(scalars_dfs, loc, column, value, allow_duplicates): + scalars_df, scalars_pandas_df = scalars_dfs + # insert works inplace, so will influence other tests. + # make a copy to avoid inplace changes. + bf_df = scalars_df.copy() + pd_df = scalars_pandas_df.copy() + bf_df.insert(loc, column, value, allow_duplicates) + pd_df.insert(loc, column, value, allow_duplicates) + + pd.testing.assert_frame_equal(bf_df.to_pandas(), pd_df, check_dtype=False) + + def test_drop_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index d46fa4cfc7a..f8088f8060a 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1067,6 +1067,51 @@ def reindex_like(self, other): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def insert(self, loc, column, value, allow_duplicates=False): + """Insert column into DataFrame at specified location. + + Raises a ValueError if `column` is already contained in the DataFrame, + unless `allow_duplicates` is set to True. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + + Insert a new column named 'col3' between 'col1' and 'col2' with all entries set to 5. + + >>> df.insert(1, 'col3', 5) + >>> df + col1 col3 col2 + 0 1 5 3 + 1 2 5 4 + + [2 rows x 3 columns] + + Insert another column named 'col2' at the beginning of the DataFrame with values [5, 6] + + >>> df.insert(0, 'col2', [5, 6], allow_duplicates=True) + >>> df + col2 col1 col3 col2 + 0 5 1 5 3 + 1 6 2 5 4 + + [2 rows x 4 columns] + + Args: + loc (int): + Insertion index. Must verify 0 <= loc <= len(columns). + column (str, number, or hashable object): + Label of the inserted column. + value (Scalar, Series, or array-like): + Content of the inserted column. + allow_duplicates (bool, default False): + Allow duplicate column labels to be created. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def drop( self, labels=None, *, axis=0, index=None, columns=None, level=None ) -> DataFrame | None: From b248986298619965f6c6079e5e0d227fdfd2fd0e Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:13:26 -0700 Subject: [PATCH 649/921] chore: cleanup type errors in tests/system/small/test_multiindex.py (#774) --- tests/system/small/test_multiindex.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index de631ee20e1..ab2a9c19b8d 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -850,14 +850,13 @@ def test_column_multi_index_stack(level): bf_result = bf_df.stack(level=level).to_pandas() # BigFrames emulates future_stack impl - # TODO(b/340884387): fix type error - pd_result = pd_df.stack(level=level, future_stack=True) # type: ignore + pd_result = pd_df.stack(level=level, future_stack=True) # Pandas produces NaN, where bq dataframes produces pd.NA # Column ordering seems to depend on pandas version - # TODO(b/340884387): fix type error + assert isinstance(pd_result, pandas.DataFrame) pandas.testing.assert_frame_equal( - bf_result, pd_result, check_dtype=False, check_index_type=False # type: ignore + bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -891,11 +890,9 @@ def test_column_multi_index_melt(): def test_column_multi_index_unstack(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "int64_col", "rowindex_2"] - # TODO(b/340884387): fix type error - level1 = pandas.Index(["b", "a", "b"], dtype="string[pyarrow]") # type: ignore + level1: pandas.Index = pandas.Index(["b", "a", "b"], dtype="string[pyarrow]") # Need resulting column to be pyarrow string rather than object dtype - # TODO(b/340884387): fix type error - level2 = pandas.Index(["a", "b", "b"], dtype="string[pyarrow]") # type: ignore + level2: pandas.Index = pandas.Index(["a", "b", "b"], dtype="string[pyarrow]") multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) bf_df = scalars_df_index[columns].copy() bf_df.columns = multi_columns @@ -1189,10 +1186,12 @@ def test_explode_w_multi_index(): df = bpd.DataFrame(data, columns=multi_level_columns) pd_df = df.to_pandas() - # TODO(b/340884387): fix type error + + assert isinstance(pd_df, pandas.DataFrame) + assert isinstance(pd_df["col0"], pandas.DataFrame) pandas.testing.assert_frame_equal( df["col0"].explode("col00").to_pandas(), - pd_df["col0"].explode("col00"), # type: ignore + pd_df["col0"].explode("col00"), check_dtype=False, check_index_type=False, ) @@ -1202,8 +1201,7 @@ def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index columns = ["int64_too", "int64_col", "rowindex_2"] level1 = pandas.Index(["b", "c", "d"]) # Need resulting column to be pyarrow string rather than object dtype - # TODO(b/340884387): fix type error - level2 = pandas.Index([None, "b", "b"], dtype="string[pyarrow]") # type: ignore + level2: pandas.Index = pandas.Index([None, "b", "b"], dtype="string[pyarrow]") multi_columns = pandas.MultiIndex.from_arrays([level1, level2]) bf_df = scalars_df_index[columns].copy() bf_df.columns = multi_columns From bfd8bee67738017417f9f455e26579808cd517f0 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 11 Jun 2024 11:14:36 -0700 Subject: [PATCH 650/921] refactor: Refactor dtype conversions (#764) --- bigframes/core/compile/ibis_types.py | 67 +++---- bigframes/dtypes.py | 290 +++++++++++++++++++-------- tests/system/small/test_dataframe.py | 2 +- 3 files changed, 239 insertions(+), 120 deletions(-) diff --git a/bigframes/core/compile/ibis_types.py b/bigframes/core/compile/ibis_types.py index f73fce3e4db..f3221f605fd 100644 --- a/bigframes/core/compile/ibis_types.py +++ b/bigframes/core/compile/ibis_types.py @@ -74,23 +74,6 @@ BIGFRAMES_TO_IBIS: Dict[bigframes.dtypes.Dtype, ibis_dtypes.DataType] = { pandas: ibis for ibis, pandas in BIDIRECTIONAL_MAPPINGS } - -IBIS_TO_ARROW: Dict[ibis_dtypes.DataType, pa.DataType] = { - ibis_dtypes.boolean: pa.bool_(), - ibis_dtypes.date: pa.date32(), - ibis_dtypes.float64: pa.float64(), - ibis_dtypes.int64: pa.int64(), - ibis_dtypes.string: pa.string(), - ibis_dtypes.time: pa.time64("us"), - ibis_dtypes.Timestamp(timezone=None): pa.timestamp("us"), - ibis_dtypes.Timestamp(timezone="UTC"): pa.timestamp("us", tz="UTC"), - ibis_dtypes.binary: pa.binary(), - ibis_dtypes.Decimal(precision=38, scale=9, nullable=True): pa.decimal128(38, 9), - ibis_dtypes.Decimal(precision=76, scale=38, nullable=True): pa.decimal256(76, 38), -} - -ARROW_TO_IBIS = {arrow: ibis for ibis, arrow in IBIS_TO_ARROW.items()} - IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, bigframes.dtypes.Dtype] = { ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS } @@ -248,14 +231,17 @@ def bigframes_dtype_to_ibis_dtype( Raises: ValueError: If passed a dtype not supported by BigQuery DataFrames. """ - if isinstance(bigframes_dtype, pd.ArrowDtype): - return _arrow_dtype_to_ibis_dtype(bigframes_dtype.pyarrow_dtype) - - type_string = str(bigframes_dtype) - if type_string in bigframes.dtypes.BIGFRAMES_STRING_TO_BIGFRAMES: + if str(bigframes_dtype) in bigframes.dtypes.BIGFRAMES_STRING_TO_BIGFRAMES: bigframes_dtype = bigframes.dtypes.BIGFRAMES_STRING_TO_BIGFRAMES[ - cast(bigframes.dtypes.DtypeString, type_string) + cast(bigframes.dtypes.DtypeString, str(bigframes_dtype)) ] + + if bigframes_dtype in BIGFRAMES_TO_IBIS.keys(): + return BIGFRAMES_TO_IBIS[bigframes_dtype] + + elif isinstance(bigframes_dtype, pd.ArrowDtype) and bigframes_dtype.pyarrow_dtype: + return _arrow_dtype_to_ibis_dtype(bigframes_dtype.pyarrow_dtype) + else: raise ValueError( textwrap.dedent( @@ -276,8 +262,6 @@ def bigframes_dtype_to_ibis_dtype( ) ) - return BIGFRAMES_TO_IBIS[bigframes_dtype] - def ibis_dtype_to_bigframes_dtype( ibis_dtype: ibis_dtypes.DataType, @@ -348,15 +332,27 @@ def _ibis_dtype_to_arrow_dtype(ibis_dtype: ibis_dtypes.DataType) -> pa.DataType: ] ) - if ibis_dtype in IBIS_TO_ARROW: - return IBIS_TO_ARROW[ibis_dtype] + if ibis_dtype in IBIS_TO_BIGFRAMES: + dtype = IBIS_TO_BIGFRAMES[ibis_dtype] + # Note: arrow mappings are incomplete, no geography type + return bigframes.dtypes.bigframes_dtype_to_arrow_dtype(dtype) else: raise ValueError( f"Unexpected Ibis data type {ibis_dtype}. {constants.FEEDBACK_LINK}" ) +_ARROW_TO_IBIS = { + mapping.arrow_dtype: bigframes_dtype_to_ibis_dtype(mapping.dtype) + for mapping in bigframes.dtypes.SIMPLE_TYPES + if mapping.arrow_dtype is not None +} + + def _arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType: + if arrow_dtype == pa.null(): + # Used for empty local dataframes where pyarrow has null type + return ibis_dtypes.float64 if pa.types.is_struct(arrow_dtype): struct_dtype = cast(pa.StructType, arrow_dtype) return ibis_dtypes.Struct.from_tuples( @@ -365,16 +361,15 @@ def _arrow_dtype_to_ibis_dtype(arrow_dtype: pa.DataType) -> ibis_dtypes.DataType for field in struct_dtype ] ) - - if arrow_dtype in ARROW_TO_IBIS: - return ARROW_TO_IBIS[arrow_dtype] - if arrow_dtype == pa.null(): - # Used for empty local dataframes where pyarrow has null type - return ibis_dtypes.float64 + if pa.types.is_list(arrow_dtype): + list_dtype = cast(pa.ListType, arrow_dtype) + value_dtype = list_dtype.value_type + value_ibis_type = _arrow_dtype_to_ibis_dtype(value_dtype) + return ibis_dtypes.Array(value_type=value_ibis_type) + elif arrow_dtype in _ARROW_TO_IBIS: + return _ARROW_TO_IBIS[arrow_dtype] else: - raise ValueError( - f"Unexpected Arrow data type {arrow_dtype}. {constants.FEEDBACK_LINK}" - ) + raise ValueError(f"Unexpected arrow type: {arrow_dtype}") def literal_to_ibis_scalar( diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 3b9d5bf141a..ced1c215e5b 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -14,14 +14,14 @@ """Mappings for Pandas dtypes supported by BigQuery DataFrames package""" +from dataclasses import dataclass import datetime import decimal import typing -from typing import Any, Dict, Literal, Union +from typing import Dict, Literal, Union -import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes import geopandas as gpd # type: ignore -import ibis +import google.cloud.bigquery import numpy as np import pandas as pd import pyarrow as pa @@ -41,21 +41,89 @@ # None represents the type of a None scalar. ExpressionType = typing.Optional[Dtype] - +# Convert to arrow when in array or struct INT_DTYPE = pd.Int64Dtype() FLOAT_DTYPE = pd.Float64Dtype() BOOL_DTYPE = pd.BooleanDtype() +# Wrapped arrow dtypes STRING_DTYPE = pd.StringDtype(storage="pyarrow") BYTES_DTYPE = pd.ArrowDtype(pa.binary()) DATE_DTYPE = pd.ArrowDtype(pa.date32()) TIME_DTYPE = pd.ArrowDtype(pa.time64("us")) DATETIME_DTYPE = pd.ArrowDtype(pa.timestamp("us")) TIMESTAMP_DTYPE = pd.ArrowDtype(pa.timestamp("us", tz="UTC")) +NUMERIC_DTYPE = pd.ArrowDtype(pa.decimal128(38, 9)) +BIGNUMERIC_DTYPE = pd.ArrowDtype(pa.decimal256(76, 38)) +# No arrow equivalent GEO_DTYPE = gpd.array.GeometryDtype() # Used when storing Null expressions DEFAULT_DTYPE = FLOAT_DTYPE + +# Will have a few dtype variants: simple(eg. int, string, bool), complex (eg. list, struct), and virtual (eg. micro intervals, categorical) +@dataclass(frozen=True) +class SimpleDtypeInfo: + """ + A simple dtype maps 1:1 with a database type and is not parameterized. + """ + + dtype: Dtype + arrow_dtype: typing.Optional[pa.DataType] + type_kind: typing.Tuple[str, ...] # Should all correspond to the same db type + logical_bytes: int = ( + 8 # this is approximate only, some types are variably sized, also, compression + ) + + +# TODO: Missing BQ types: INTERVAL, JSON, RANGE +# TODO: Add mappings to python types +SIMPLE_TYPES = ( + SimpleDtypeInfo( + dtype=INT_DTYPE, arrow_dtype=pa.int64(), type_kind=("INT64", "INTEGER") + ), + SimpleDtypeInfo( + dtype=FLOAT_DTYPE, arrow_dtype=pa.float64(), type_kind=("FLOAT64", "FLOAT") + ), + SimpleDtypeInfo( + dtype=BOOL_DTYPE, + arrow_dtype=pa.bool_(), + type_kind=("BOOL", "BOOLEAN"), + logical_bytes=1, + ), + SimpleDtypeInfo(dtype=STRING_DTYPE, arrow_dtype=pa.string(), type_kind=("STRING",)), + SimpleDtypeInfo( + dtype=DATE_DTYPE, arrow_dtype=pa.date32(), type_kind=("DATE",), logical_bytes=4 + ), + SimpleDtypeInfo(dtype=TIME_DTYPE, arrow_dtype=pa.time64("us"), type_kind=("TIME",)), + SimpleDtypeInfo( + dtype=DATETIME_DTYPE, arrow_dtype=pa.timestamp("us"), type_kind=("DATETIME",) + ), + SimpleDtypeInfo( + dtype=TIMESTAMP_DTYPE, + arrow_dtype=pa.timestamp("us", tz="UTC"), + type_kind=("TIMESTAMP",), + ), + SimpleDtypeInfo(dtype=BYTES_DTYPE, arrow_dtype=pa.binary(), type_kind=("BYTES",)), + SimpleDtypeInfo( + dtype=NUMERIC_DTYPE, + arrow_dtype=pa.decimal128(38, 9), + type_kind=("NUMERIC",), + logical_bytes=16, + ), + SimpleDtypeInfo( + dtype=BIGNUMERIC_DTYPE, + arrow_dtype=pa.decimal256(76, 38), + type_kind=("BIGNUMERIC",), + logical_bytes=32, + ), + # Geo has no corresponding arrow dtype + SimpleDtypeInfo( + dtype=GEO_DTYPE, arrow_dtype=None, type_kind=("GEOGRAPHY",), logical_bytes=40 + ), +) + + # Type hints for dtype strings supported by BigQuery DataFrame DtypeString = Literal[ "boolean", @@ -151,23 +219,9 @@ def is_bool_coercable(type: ExpressionType) -> bool: return (type is None) or is_numeric(type) or is_string_like(type) -_ALL_DTYPES = ( - pd.BooleanDtype(), - pd.ArrowDtype(pa.date32()), - pd.Float64Dtype(), - pd.Int64Dtype(), - pd.StringDtype(storage="pyarrow"), - pd.ArrowDtype(pa.time64("us")), - pd.ArrowDtype(pa.timestamp("us")), - pd.ArrowDtype(pa.timestamp("us", tz="UTC")), - pd.ArrowDtype(pa.binary()), - pd.ArrowDtype(pa.decimal128(38, 9)), - pd.ArrowDtype(pa.decimal256(76, 38)), - gpd.array.GeometryDtype(), -) - BIGFRAMES_STRING_TO_BIGFRAMES: Dict[DtypeString, Dtype] = { - typing.cast(DtypeString, dtype.name): dtype for dtype in _ALL_DTYPES + typing.cast(DtypeString, mapping.dtype.name): mapping.dtype + for mapping in SIMPLE_TYPES } # special case - string[pyarrow] doesn't include the storage in its name, and both @@ -178,18 +232,12 @@ def is_bool_coercable(type: ExpressionType) -> bool: BIGFRAMES_STRING_TO_BIGFRAMES["int64[pyarrow]"] = pd.Int64Dtype() # For the purposes of dataframe.memory_usage -# https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#data_type_sizes DTYPE_BYTE_SIZES = { - pd.BooleanDtype(): 1, - pd.Int64Dtype(): 8, - pd.Float32Dtype(): 8, - pd.StringDtype(): 8, - pd.ArrowDtype(pa.time64("us")): 8, - pd.ArrowDtype(pa.timestamp("us")): 8, - pd.ArrowDtype(pa.timestamp("us", tz="UTC")): 8, - pd.ArrowDtype(pa.date32()): 8, + type_info.dtype: type_info.logical_bytes for type_info in SIMPLE_TYPES } +### Conversion Functions + def dtype_for_etype(etype: ExpressionType) -> Dtype: if etype is None: @@ -198,26 +246,141 @@ def dtype_for_etype(etype: ExpressionType) -> Dtype: return etype +# Mapping between arrow and bigframes types are necessary because arrow types are used for structured types, but not all primitive types, +# so conversion are needed when data is nested or unnested. Also, sometimes local data is stored as arrow. +_ARROW_TO_BIGFRAMES = { + mapping.arrow_dtype: mapping.dtype + for mapping in SIMPLE_TYPES + if mapping.arrow_dtype is not None +} + + def arrow_dtype_to_bigframes_dtype(arrow_dtype: pa.DataType) -> Dtype: - # TODO: Directly convert instead of using ibis dtype as intermediate step - from bigframes.core.compile.ibis_types import ( - _arrow_dtype_to_ibis_dtype, - ibis_dtype_to_bigframes_dtype, - ) + if arrow_dtype in _ARROW_TO_BIGFRAMES: + return _ARROW_TO_BIGFRAMES[arrow_dtype] + if pa.types.is_list(arrow_dtype): + return pd.ArrowDtype(arrow_dtype) + if pa.types.is_struct(arrow_dtype): + return pd.ArrowDtype(arrow_dtype) + if arrow_dtype == pa.null(): + return DEFAULT_DTYPE + else: + raise ValueError( + f"Unexpected Arrow data type {arrow_dtype}. {constants.FEEDBACK_LINK}" + ) - return ibis_dtype_to_bigframes_dtype(_arrow_dtype_to_ibis_dtype(arrow_dtype)) + +_BIGFRAMES_TO_ARROW = { + mapping.dtype: mapping.arrow_dtype + for mapping in SIMPLE_TYPES + if mapping.arrow_dtype is not None +} def bigframes_dtype_to_arrow_dtype( - bigframes_dtype: Union[DtypeString, Dtype, np.dtype[Any]] + bigframes_dtype: Dtype, ) -> pa.DataType: - # TODO: Directly convert instead of using ibis dtype as intermediate step - from bigframes.core.compile.ibis_types import ( - _ibis_dtype_to_arrow_dtype, - bigframes_dtype_to_ibis_dtype, - ) + if bigframes_dtype in _BIGFRAMES_TO_ARROW: + return _BIGFRAMES_TO_ARROW[bigframes_dtype] + if isinstance(bigframes_dtype, pd.ArrowDtype): + if pa.types.is_list(bigframes_dtype.pyarrow_dtype): + return bigframes_dtype.pyarrow_dtype + if pa.types.is_struct(bigframes_dtype.pyarrow_dtype): + return bigframes_dtype.pyarrow_dtype + else: + raise ValueError( + f"No arrow conversion for {bigframes_dtype}. {constants.FEEDBACK_LINK}" + ) + + +def infer_literal_type(literal) -> typing.Optional[Dtype]: + # Maybe also normalize literal to canonical python representation to remove this burden from compilers? + if pd.api.types.is_list_like(literal): + element_types = [infer_literal_type(i) for i in literal] + common_type = lcd_type(*element_types) + as_arrow = bigframes_dtype_to_arrow_dtype(common_type) + return pd.ArrowDtype(as_arrow) + if pd.api.types.is_dict_like(literal): + fields = [ + (key, bigframes_dtype_to_arrow_dtype(infer_literal_type(literal[key]))) + for key in literal.keys() + ] + return pd.ArrowDtype(pa.struct(fields)) + if pd.isna(literal): + return None # Null value without a definite type + if isinstance(literal, (bool, np.bool_)): + return BOOL_DTYPE + if isinstance(literal, (int, np.integer)): + return INT_DTYPE + if isinstance(literal, (float, np.floating)): + return FLOAT_DTYPE + if isinstance(literal, decimal.Decimal): + return NUMERIC_DTYPE + if isinstance(literal, (str, np.str_)): + return STRING_DTYPE + if isinstance(literal, (bytes, np.bytes_)): + return BYTES_DTYPE + # Make sure to check datetime before date as datetimes are also dates + if isinstance(literal, (datetime.datetime, pd.Timestamp)): + if literal.tzinfo is not None: + return TIMESTAMP_DTYPE + else: + return DATETIME_DTYPE + if isinstance(literal, datetime.date): + return DATE_DTYPE + if isinstance(literal, datetime.time): + return TIME_DTYPE + else: + raise ValueError(f"Unable to infer type for value: {literal}") + - return _ibis_dtype_to_arrow_dtype(bigframes_dtype_to_ibis_dtype(bigframes_dtype)) +def infer_literal_arrow_type(literal) -> typing.Optional[pa.DataType]: + if pd.isna(literal): + return None # Null value without a definite type + return bigframes_dtype_to_arrow_dtype(infer_literal_type(literal)) + + +# Don't have dtype for json, so just end up interpreting as STRING +_REMAPPED_TYPEKINDS = {"JSON": "STRING"} +_TK_TO_BIGFRAMES = { + type_kind: mapping.dtype + for mapping in SIMPLE_TYPES + for type_kind in mapping.type_kind +} + + +def convert_schema_field( + field: google.cloud.bigquery.SchemaField, +) -> typing.Tuple[str, Dtype]: + is_repeated = field.mode == "REPEATED" + if field.field_type == "RECORD": + mapped_fields = map(convert_schema_field, field.fields) + pa_struct = pa.struct( + (name, bigframes_dtype_to_arrow_dtype(dtype)) + for name, dtype in mapped_fields + ) + pa_type = pa.list_(pa_struct) if is_repeated else pa_struct + return field.name, pd.ArrowDtype(pa_type) + elif ( + field.field_type in _TK_TO_BIGFRAMES or field.field_type in _REMAPPED_TYPEKINDS + ): + singular_type = _TK_TO_BIGFRAMES[ + _REMAPPED_TYPEKINDS.get(field.field_type, field.field_type) + ] + if is_repeated: + pa_type = pa.list_(bigframes_dtype_to_arrow_dtype(singular_type)) + return field.name, pd.ArrowDtype(pa_type) + else: + return field.name, singular_type + else: + raise ValueError(f"Cannot handle type: {field.field_type}") + + +def bf_type_from_type_kind( + bq_schema: list[google.cloud.bigquery.SchemaField], +) -> typing.Dict[str, Dtype]: + """Converts bigquery sql type to the default bigframes dtype.""" + return {name: dtype for name, dtype in map(convert_schema_field, bq_schema)} def is_dtype(scalar: typing.Any, dtype: Dtype) -> bool: @@ -266,6 +429,7 @@ def is_patype(scalar: typing.Any, pa_type: pa.DataType) -> bool: return False +# Utilities for type coercion, and compatibility def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: """Whether scalar can be compare to items of dtype (though maybe requiring coercion). Returns the datatype that must be used for the comparison""" if is_dtype(scalar, dtype): @@ -337,47 +501,7 @@ def lcd_type_or_throw(dtype1: Dtype, dtype2: Dtype) -> Dtype: return result -def infer_literal_type(literal) -> typing.Optional[Dtype]: - if pd.isna(literal): - return None # Null value without a definite type - # Temporary logic, use ibis inferred type - from bigframes.core.compile.ibis_types import ( - ibis_dtype_to_bigframes_dtype, - literal_to_ibis_scalar, - ) - - ibis_literal = literal_to_ibis_scalar(literal) - return ibis_dtype_to_bigframes_dtype(ibis_literal.type()) - - -def infer_literal_arrow_type(literal) -> typing.Optional[pa.DataType]: - if pd.isna(literal): - return None # Null value without a definite type - # Temporary logic, use ibis inferred type - # TODO: Directly convert instead of using ibis dtype as intermediate step - from bigframes.core.compile.ibis_types import ( - _ibis_dtype_to_arrow_dtype, - literal_to_ibis_scalar, - ) - - ibis_literal = literal_to_ibis_scalar(literal) - return _ibis_dtype_to_arrow_dtype(ibis_literal.type()) - - -def bf_type_from_type_kind(bf_schema) -> Dict[str, Dtype]: - """Converts bigquery sql type to the default bigframes dtype.""" - ibis_schema: ibis.Schema = third_party_ibis_bqtypes.BigQuerySchema.to_ibis( - bf_schema - ) - # TODO: Directly convert instead of using ibis dtype as intermediate step - from bigframes.core.compile.ibis_types import ibis_dtype_to_bigframes_dtype - - return { - name: ibis_dtype_to_bigframes_dtype(type) for name, type in ibis_schema.items() - } - - -# Remote functions use only +### Remote functions use only # TODO: Refactor into remote function module # Input and output types supported by BigQuery DataFrames remote functions. diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 8de7f2c2050..93db0c41964 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -384,7 +384,7 @@ def test_df_info(scalars_dfs): " 11 time_col 6 non-null time64[us][pyarrow]\n" " 12 timestamp_col 6 non-null timestamp[us, tz=UTC][pyarrow]\n" "dtypes: Float64(1), Int64(3), binary[pyarrow](1), boolean(1), date32[day][pyarrow](1), decimal128(38, 9)[pyarrow](1), geometry(1), string(1), time64[us][pyarrow](1), timestamp[us, tz=UTC][pyarrow](1), timestamp[us][pyarrow](1)\n" - "memory usage: 945 bytes\n" + "memory usage: 1269 bytes\n" ) scalars_df, _ = scalars_dfs From 638ba2578473ffa96c93675d50f549a2b4023d28 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 11 Jun 2024 14:53:08 -0700 Subject: [PATCH 651/921] refactor: update core/sql.py selects to use SQL AST nodes and nodes updates. (#775) * refactor: update core/sql.py to use SQL AST nodes and nodes updates. * add note back. * update unit test. * update escape. * new utils * update format * move function * move function * unittest update, add escape to alias and cte * update code --- bigframes/core/compile/googlesql/__init__.py | 2 + .../core/compile/googlesql/expression.py | 32 ++++++-- bigframes/core/compile/googlesql/query.py | 57 ++++++++------- bigframes/core/sql.py | 73 +++++++++---------- tests/unit/core/compiler/test_googlesql.py | 43 +++++++---- 5 files changed, 123 insertions(+), 84 deletions(-) diff --git a/bigframes/core/compile/googlesql/__init__.py b/bigframes/core/compile/googlesql/__init__.py index 32265c0d518..ce94fbb24b1 100644 --- a/bigframes/core/compile/googlesql/__init__.py +++ b/bigframes/core/compile/googlesql/__init__.py @@ -18,6 +18,7 @@ from __future__ import annotations from bigframes.core.compile.googlesql.expression import ( + _escape_chars, AliasExpression, ColumnExpression, CTEExpression, @@ -36,6 +37,7 @@ ) __all__ = [ + "_escape_chars", "AliasExpression", "AsAlias", "ColumnExpression", diff --git a/bigframes/core/compile/googlesql/expression.py b/bigframes/core/compile/googlesql/expression.py index 702aa2c5e55..d55e4eda85b 100644 --- a/bigframes/core/compile/googlesql/expression.py +++ b/bigframes/core/compile/googlesql/expression.py @@ -72,10 +72,10 @@ def __post_init__(self): def sql(self) -> str: text = [] if self.project_id is not None: - text.append(f"`{self.project_id}`") + text.append(f"`{_escape_chars(self.project_id)}`") if self.dataset_id is not None: - text.append(f"`{self.dataset_id}`") - text.append(f"`{self.table_id}`") + text.append(f"`{_escape_chars(self.dataset_id)}`") + text.append(f"`{_escape_chars(self.table_id)}`") return ".".join(text) @@ -84,7 +84,7 @@ class AliasExpression(Expression): alias: str def sql(self) -> str: - return f"`{self.alias}`" + return f"`{_escape_chars(self.alias)}`" @dataclasses.dataclass @@ -92,4 +92,26 @@ class CTEExpression(Expression): name: str def sql(self) -> str: - return f"`{self.name}`" + return f"`{_escape_chars(self.name)}`" + + +def _escape_chars(value: str): + """Escapes all special charactesrs""" + # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#string_and_bytes_literals + trans_table = str.maketrans( + { + "\a": r"\a", + "\b": r"\b", + "\f": r"\f", + "\n": r"\n", + "\r": r"\r", + "\t": r"\t", + "\v": r"\v", + "\\": r"\\", + "?": r"\?", + '"': r"\"", + "'": r"\'", + "`": r"\`", + } + ) + return value.translate(trans_table) diff --git a/bigframes/core/compile/googlesql/query.py b/bigframes/core/compile/googlesql/query.py index 6210aa67f4b..adfd4b711d0 100644 --- a/bigframes/core/compile/googlesql/query.py +++ b/bigframes/core/compile/googlesql/query.py @@ -16,10 +16,14 @@ import dataclasses import typing +from typing import TYPE_CHECKING import bigframes.core.compile.googlesql.abc as abc import bigframes.core.compile.googlesql.expression as expr +if TYPE_CHECKING: + import google.cloud.bigquery as bigquery + """This module provides a structured representation of GoogleSQL syntax using nodes. Each node's name and child nodes are designed to strictly follow the official GoogleSQL syntax rules outlined in the documentation: @@ -51,10 +55,14 @@ class Select(abc.SQLSyntax): select_list: typing.Sequence[typing.Union[SelectExpression, SelectAll]] from_clause_list: typing.Sequence[FromClause] = () + distinct: bool = False def sql(self) -> str: text = ["SELECT"] + if self.distinct: + text.append("DISTINCT") + select_list_sql = ",\n".join([select.sql() for select in self.select_list]) text.append(select_list_sql) @@ -104,39 +112,38 @@ def sql(self) -> str: class FromItem(abc.SQLSyntax): """This class represents GoogleSQL `from_item` syntax.""" - table_name: typing.Optional[expr.TableExpression] = None # Note: Temporarily introduces the `str` type to interact with pre-existing, # compiled SQL strings. - query_expr: typing.Optional[QueryExpr | str] = None - cte_name: typing.Optional[expr.CTEExpression] = None + expression: typing.Union[expr.TableExpression, QueryExpr, str, expr.CTEExpression] as_alias: typing.Optional[AsAlias] = None - def __post_init__(self): - non_none = sum( - expr is not None - for expr in [ - self.table_name, - self.query_expr, - self.cte_name, - ] + @classmethod + def from_table_ref( + cls, + table_ref: bigquery.TableReference, + as_alias: typing.Optional[AsAlias] = None, + ): + return cls( + expression=expr.TableExpression( + table_id=table_ref.table_id, + dataset_id=table_ref.dataset_id, + project_id=table_ref.project, + ), + as_alias=as_alias, ) - if non_none != 1: - raise ValueError("Exactly one of expressions must be provided.") def sql(self) -> str: - if self.table_name is not None: - text = self.table_name.sql() - elif self.query_expr is not None: - text = ( - self.query_expr - if isinstance(self.query_expr, str) - else self.query_expr.sql() - ) - text = f"({text})" - elif self.cte_name is not None: - text = self.cte_name.sql() + if isinstance(self.expression, (expr.TableExpression, expr.CTEExpression)): + text = self.expression.sql() + elif isinstance(self.expression, str): + text = f"({self.expression})" + elif isinstance(self.expression, QueryExpr): + text = f"({self.expression.sql()})" else: - raise ValueError("One of from items must be provided.") + raise ValueError( + f"Unsupported expression type {type(self.expression).__name__};" + "expected one of TableExpression, QueryExpr, str, or CTEExpression." + ) if self.as_alias is None: return text diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index 01dcebad6ed..6926ed910fd 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -19,9 +19,10 @@ import datetime import math -import textwrap from typing import Iterable, Mapping, TYPE_CHECKING, Union +import bigframes.core.compile.googlesql as googlesql + # Literals and identifiers matching this pattern can be unquoted unquoted = r"^[A-Za-z_][A-Za-z_0-9]*$" @@ -38,7 +39,7 @@ def simple_literal(value: str | int | bool | float | datetime.datetime): # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#literals if isinstance(value, str): # Single quoting seems to work nicer with ibis than double quoting - return f"'{escape_special_characters(value)}'" + return f"'{googlesql._escape_chars(value)}'" elif isinstance(value, (bool, int)): return str(value) elif isinstance(value, float): @@ -65,29 +66,7 @@ def identifier(id: str) -> str: """Return a string representing column reference in a SQL.""" # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers # Just always escape, otherwise need to check against every reserved sql keyword - return f"`{escape_special_characters(id)}`" - - -def escape_special_characters(value: str): - """Escapes all special charactesrs""" - # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#string_and_bytes_literals - trans_table = str.maketrans( - { - "\a": r"\a", - "\b": r"\b", - "\f": r"\f", - "\n": r"\n", - "\r": r"\r", - "\t": r"\t", - "\v": r"\v", - "\\": r"\\", - "?": r"\?", - '"': r"\"", - "'": r"\'", - "`": r"\`", - } - ) - return value.translate(trans_table) + return f"`{googlesql._escape_chars(id)}`" def cast_as_string(column_name: str) -> str: @@ -107,10 +86,6 @@ def csv(values: Iterable[str]) -> str: return ", ".join(values) -def table_reference(table_ref: bigquery.TableReference) -> str: - return f"`{escape_special_characters(table_ref.project)}`.`{escape_special_characters(table_ref.dataset_id)}`.`{escape_special_characters(table_ref.table_id)}`" - - def infix_op(opname: str, left_arg: str, right_arg: str): # Maybe should add parentheses?? return f"{left_arg} {opname} {right_arg}" @@ -118,27 +93,45 @@ def infix_op(opname: str, left_arg: str, right_arg: str): ### Writing SELECT expressions def select_from_subquery(columns: Iterable[str], subquery: str, distinct: bool = False): - selection = ", ".join(map(identifier, columns)) - distinct_clause = "DISTINCT " if distinct else "" - - return textwrap.dedent( - f"SELECT {distinct_clause}{selection}\nFROM (\n" f"{subquery}\n" ")\n" + select_list = [ + googlesql.SelectExpression(expression=googlesql.ColumnExpression(name=column)) + for column in columns + ] + from_clause_list = [googlesql.FromClause(googlesql.FromItem(expression=subquery))] + + select_expr = googlesql.Select( + select_list=select_list, from_clause_list=from_clause_list, distinct=distinct ) + return select_expr.sql() def select_from_table_ref( columns: Iterable[str], table_ref: bigquery.TableReference, distinct: bool = False ): - selection = ", ".join(map(identifier, columns)) - distinct_clause = "DISTINCT " if distinct else "" - - return textwrap.dedent( - f"SELECT {distinct_clause}{selection}\nFROM {table_reference(table_ref)}" + select_list = [ + googlesql.SelectExpression(expression=googlesql.ColumnExpression(name=column)) + for column in columns + ] + from_clause_list = [ + googlesql.FromClause(googlesql.FromItem.from_table_ref(table_ref)) + ] + + select_expr = googlesql.Select( + select_list=select_list, from_clause_list=from_clause_list, distinct=distinct ) + return select_expr.sql() def select_table(table_ref: bigquery.TableReference): - return textwrap.dedent(f"SELECT * FROM {table_reference(table_ref)}") + select_list = [googlesql.SelectAll(expression=googlesql.StarExpression())] + from_clause_list = [ + googlesql.FromClause(googlesql.FromItem.from_table_ref(table_ref)) + ] + + select_expr = googlesql.Select( + select_list=select_list, from_clause_list=from_clause_list + ) + return select_expr.sql() def is_distinct_sql(columns: Iterable[str], table_ref: bigquery.TableReference) -> str: diff --git a/tests/unit/core/compiler/test_googlesql.py b/tests/unit/core/compiler/test_googlesql.py index 70ca5cfa12b..0430bd348fd 100644 --- a/tests/unit/core/compiler/test_googlesql.py +++ b/tests/unit/core/compiler/test_googlesql.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest.mock import Mock + import pytest import bigframes.core.compile.googlesql as sql @@ -36,16 +38,13 @@ def test_table_expression(table_id, dataset_id, project_id, expected): @pytest.mark.parametrize( ("table_name", "alias", "expected"), [ - pytest.param(None, None, None, marks=pytest.mark.xfail(raises=ValueError)), pytest.param("a", None, "`a`"), pytest.param("a", "aa", "`a` AS `aa`"), ], ) def test_from_item_w_table_name(table_name, alias, expected): expr = sql.FromItem( - table_name=None - if table_name is None - else sql.TableExpression(table_id=table_name), + sql.TableExpression(table_id=table_name), as_alias=None if alias is None else sql.AsAlias(sql.AliasExpression(alias=alias)), @@ -55,7 +54,7 @@ def test_from_item_w_table_name(table_name, alias, expected): def test_from_item_w_query_expr(): from_clause = sql.FromClause( - sql.FromItem(table_name=sql.TableExpression(table_id="table_a")) + sql.FromItem(expression=sql.TableExpression(table_id="table_a")) ) select = sql.Select( select_list=[sql.SelectAll(sql.StarExpression())], @@ -65,19 +64,30 @@ def test_from_item_w_query_expr(): expected = "SELECT\n*\nFROM\n`table_a`" # A QueryExpr object - expr = sql.FromItem(query_expr=query_expr) + expr = sql.FromItem(expression=query_expr) assert expr.sql() == f"({expected})" # A str object - expr = sql.FromItem(query_expr=expected) + expr = sql.FromItem(expression=expected) assert expr.sql() == f"({expected})" def test_from_item_w_cte(): - expr = sql.FromItem(cte_name=sql.CTEExpression("test")) + expr = sql.FromItem(expression=sql.CTEExpression("test")) assert expr.sql() == "`test`" +def test_from_item_w_table_ref(): + mock_table_ref = Mock() + mock_table_ref.table_id = "mock_table" + mock_table_ref.dataset_id = "mock_dataset" + mock_table_ref.project = "mock_project" + + from_item = sql.FromItem.from_table_ref(mock_table_ref) + + assert from_item.sql() == "`mock_project`.`mock_dataset`.`mock_table`" + + @pytest.mark.parametrize( ("col_name", "alias", "expected"), [ @@ -98,9 +108,9 @@ def test_select(): select_2 = sql.SelectExpression( expression=sql.ColumnExpression("b"), alias=sql.AliasExpression(alias="bb") ) - from_1 = sql.FromItem(table_name=sql.TableExpression(table_id="table_a")) + from_1 = sql.FromItem(expression=sql.TableExpression(table_id="table_a")) from_2 = sql.FromItem( - query_expr="SELECT * FROM project.table_b", + expression="SELECT * FROM project.table_b", as_alias=sql.AsAlias(sql.AliasExpression(alias="table_b")), ) expr = sql.Select( @@ -115,7 +125,7 @@ def test_select(): def test_query_expr_w_cte(): # Test a simple SELECT query. from_clause1 = sql.FromClause( - sql.FromItem(table_name=sql.TableExpression(table_id="table_a")) + sql.FromItem(expression=sql.TableExpression(table_id="table_a")) ) select1 = sql.Select( select_list=[sql.SelectAll(sql.StarExpression())], @@ -143,13 +153,18 @@ def test_query_expr_w_cte(): sql.SelectAll(sql.StarExpression(parent=cte2.cte_name)), ], from_clause_list=[ - sql.FromClause(sql.FromItem(cte_name=cte1.cte_name)), - sql.FromClause(sql.FromItem(cte_name=cte2.cte_name)), + sql.FromClause(sql.FromItem(expression=cte1.cte_name)), + sql.FromClause(sql.FromItem(expression=cte2.cte_name)), ], + distinct=True, ) - select2_sql = "SELECT\n`a`.`column_x`,\n`b`.*\nFROM\n`a`,\n`b`" + select2_sql = "SELECT\nDISTINCT\n`a`.`column_x`,\n`b`.*\nFROM\n`a`,\n`b`" assert select2.sql() == select2_sql query2 = sql.QueryExpr(select=select2, with_cte_list=with_cte_list) query2_sql = f"WITH {cte1_sql},\n{cte2_sql}\n{select2_sql}" assert query2.sql() == query2_sql + + +def test_escape_chars(): + assert sql._escape_chars("\a\b\f\n\r\t\v\\?'\"`") == r"\a\b\f\n\r\t\v\\\?\'\"\`" From 6a57a263728c45e0b45330c7358e156e7d4c3f98 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:10:12 -0700 Subject: [PATCH 652/921] chore: cleanup type errors in tests/system/small/test_session.py (#777) --- tests/system/small/test_session.py | 31 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 9631e0c7ab5..5d53a5af172 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -11,7 +11,6 @@ # 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 io import random import re @@ -22,6 +21,7 @@ from typing import List, Optional, Sequence import warnings +import bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq import google import google.cloud.bigquery as bigquery import numpy as np @@ -302,10 +302,13 @@ def test_read_gbq_w_primary_keys_table_and_filters( df = session.read_gbq( f"{table.project}.{table.dataset_id}.{table.table_id}", - filters=[ - ("name", "LIKE", "W%"), - ("total_people", ">", 100), - ], # type: ignore + filters=typing.cast( + vendored_pandas_gbq.FiltersType, + [ + ("name", "LIKE", "W%"), + ("total_people", ">", 100), + ], + ), ) result = df.to_pandas() @@ -397,7 +400,10 @@ def test_read_gbq_on_linked_dataset_warns(session): def test_read_gbq_table_clustered_with_filter(session: bigframes.Session): df = session.read_gbq_table( "bigquery-public-data.cloud_storage_geo_index.landsat_index", - filters=[[("sensor_id", "LIKE", "OLI%")], [("sensor_id", "LIKE", "%TIRS")]], # type: ignore + filters=typing.cast( + vendored_pandas_gbq.FiltersType, + [[("sensor_id", "LIKE", "OLI%")], [("sensor_id", "LIKE", "%TIRS")]], + ), columns=["sensor_id"], ) sensors = df.groupby(["sensor_id"]).agg("count").to_pandas(ordered=False) @@ -581,8 +587,8 @@ def test_read_pandas(session, scalars_dfs): def test_read_pandas_series(session): - # TODO(b/340887657): fix type error - idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) # type: ignore + + idx: pd.Index = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) pd_series = pd.Series([3, 1, 4, 1, 5], dtype=pd.Int64Dtype(), index=idx) bf_series = session.read_pandas(pd_series) @@ -590,8 +596,8 @@ def test_read_pandas_series(session): def test_read_pandas_index(session): - # TODO(b/340887657): fix type error - pd_idx = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) # type: ignore + + pd_idx: pd.Index = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) bf_idx = session.read_pandas(pd_idx) pd.testing.assert_index_equal(bf_idx.to_pandas(), pd_idx) @@ -609,8 +615,9 @@ def test_read_pandas_inline_respects_location(): df = session.read_pandas(pd.DataFrame([[1, 2, 3], [4, 5, 6]])) repr(df) - # TODO(b/340887657): fix type error - table = session.bqclient.get_table(df.query_job.destination) # type: ignore + assert df.query_job is not None + + table = session.bqclient.get_table(df.query_job.destination) assert table.location == "europe-west1" From 49c1a92c50bdd74470418bcf87ca7f1a239c507f Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:26:02 -0700 Subject: [PATCH 653/921] chore: cleanup type errors in bigframes/core/indexers.py (#779) --- bigframes/core/indexers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 582141d5390..8ce2a5903c4 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -159,9 +159,8 @@ def __getitem__(self, key): ) columns = key[1] - if isinstance(columns, pd.Series) and columns.dtype == "bool": - # TODO(b/340892590): fix type error - columns = df.columns[columns] # type: ignore + if isinstance(columns, pd.Series) and columns.dtype == bool: + columns = df.columns[typing.cast(pd.Series, columns)] return df[columns] @@ -252,7 +251,7 @@ def __getitem__(self, key: tuple) -> bigframes.core.scalar.Scalar: raise ValueError(error_message) if len(key) != 2: raise TypeError(error_message) - block: bigframes.core.blocks.Block = self._dataframe._block # type: ignore + block: bigframes.core.blocks.Block = self._dataframe._block column_block = block.select_columns([block.value_columns[key[1]]]) column = bigframes.series.Series(column_block) return column.iloc[key[0]] @@ -376,14 +375,14 @@ def _perform_loc_list_join( ) result = result.rename(original_name) else: - result = series_or_dataframe._perform_join_by_index(keys_index, how="right") # type: ignore + result = series_or_dataframe._perform_join_by_index(keys_index, how="right") if drop_levels and series_or_dataframe.index.nlevels > keys_index.nlevels: # drop common levels levels_to_drop = [ name for name in series_or_dataframe.index.names if name in keys_index.names ] - result = result.droplevel(levels_to_drop) # type: ignore + result = result.droplevel(levels_to_drop) return result From 36b43f2622cb4082ec1b0ec6bc5ba8226d2f2134 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:20:57 -0700 Subject: [PATCH 654/921] chore: tests/system/load/test_large_tables.py (#780) --- tests/system/load/test_large_tables.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/system/load/test_large_tables.py b/tests/system/load/test_large_tables.py index f92207b1915..472be3d2ada 100644 --- a/tests/system/load/test_large_tables.py +++ b/tests/system/load/test_large_tables.py @@ -94,8 +94,7 @@ def test_to_pandas_large_table(): # df will be downloaded locally expected_row_count, expected_column_count = df.shape - # TODO(b/340893653): fix type error - df = df.to_pandas() # type: ignore - row_count, column_count = df.shape + df_converted = df.to_pandas() + row_count, column_count = df_converted.shape assert column_count == expected_column_count assert row_count == expected_row_count From e14c7a9e7a9cb8847e0382b135fc06c7b82b872a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 12 Jun 2024 13:02:15 -0500 Subject: [PATCH 655/921] fix: allow `__repr__` to work with uninitialed DataFrame/Series/Index (#778) --- bigframes/core/indexes/base.py | 5 +++++ bigframes/dataframe.py | 19 +++++++++++++--- bigframes/series.py | 5 +++++ tests/unit/core/test_indexes.py | 39 +++++++++++++++++++++++++++++++++ tests/unit/test_dataframe.py | 14 ++++++++++++ tests/unit/test_series.py | 27 +++++++++++++++++++++++ 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 tests/unit/core/test_indexes.py create mode 100644 tests/unit/test_series.py diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 0e5082447a8..8df61555919 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -243,6 +243,11 @@ def query_job(self) -> Optional[bigquery.QueryJob]: return self._query_job def __repr__(self) -> str: + # Protect against errors with uninitialized Series. See: + # https://github.com/googleapis/python-bigquery-dataframes/issues/728 + if not hasattr(self, "_block"): + return object.__repr__(self) + # TODO(swast): Add a timeout here? If the query is taking a long time, # maybe we just print the job metadata that we have so far? # TODO(swast): Avoid downloading the whole series by using job diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index f78dee16424..9763b68fef0 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -574,9 +574,18 @@ def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame: return DataFrame(block) def __getattr__(self, key: str): + # Protect against recursion errors with uninitialized DataFrame + # objects. See: + # https://github.com/googleapis/python-bigquery-dataframes/issues/728 + # and + # https://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html + if key == "_block": + raise AttributeError("_block") + if key in self._block.column_labels: return self.__getitem__(key) - elif hasattr(pandas.DataFrame, key): + + if hasattr(pandas.DataFrame, key): raise AttributeError( textwrap.dedent( f""" @@ -585,8 +594,7 @@ def __getattr__(self, key: str): """ ) ) - else: - raise AttributeError(key) + raise AttributeError(key) def __setattr__(self, key: str, value): if key in ["_block", "_query_job"]: @@ -616,6 +624,11 @@ def __repr__(self) -> str: Only represents the first `bigframes.options.display.max_rows`. """ + # Protect against errors with uninitialized DataFrame. See: + # https://github.com/googleapis/python-bigquery-dataframes/issues/728 + if not hasattr(self, "_block"): + return object.__repr__(self) + opts = bigframes.options.display max_results = opts.max_rows if opts.repr_mode == "deferred": diff --git a/bigframes/series.py b/bigframes/series.py index d858060aec0..cb563194719 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -281,6 +281,11 @@ def reset_index( return bigframes.dataframe.DataFrame(block) def __repr__(self) -> str: + # Protect against errors with uninitialized Series. See: + # https://github.com/googleapis/python-bigquery-dataframes/issues/728 + if not hasattr(self, "_block"): + return object.__repr__(self) + # TODO(swast): Add a timeout here? If the query is taking a long time, # maybe we just print the job metadata that we have so far? # TODO(swast): Avoid downloading the whole series by using job diff --git a/tests/unit/core/test_indexes.py b/tests/unit/core/test_indexes.py new file mode 100644 index 00000000000..6e739c9dc98 --- /dev/null +++ b/tests/unit/core/test_indexes.py @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.core.indexes + + +def test_index_repr_with_uninitialized_object(): + """Ensures Index.__init__ can be paused in a visual debugger without crashing. + + Regression test for https://github.com/googleapis/python-bigquery-dataframes/issues/728 + """ + # Avoid calling __init__ to simulate pausing __init__ in a debugger. + # https://stackoverflow.com/a/6384982/101923 + index = object.__new__(bigframes.core.indexes.Index) + got = repr(index) + assert "Index" in got + + +def test_multiindex_repr_with_uninitialized_object(): + """Ensures MultiIndex.__init__ can be paused in a visual debugger without crashing. + + Regression test for https://github.com/googleapis/python-bigquery-dataframes/issues/728 + """ + # Avoid calling __init__ to simulate pausing __init__ in a debugger. + # https://stackoverflow.com/a/6384982/101923 + index = object.__new__(bigframes.core.indexes.MultiIndex) + got = repr(index) + assert "MultiIndex" in got diff --git a/tests/unit/test_dataframe.py b/tests/unit/test_dataframe.py index 17a82908893..6370d1b9876 100644 --- a/tests/unit/test_dataframe.py +++ b/tests/unit/test_dataframe.py @@ -15,9 +15,23 @@ import google.cloud.bigquery import pytest +import bigframes.dataframe + from . import resources +def test_dataframe_repr_with_uninitialized_object(): + """Ensures DataFrame.__init__ can be paused in a visual debugger without crashing. + + Regression test for https://github.com/googleapis/python-bigquery-dataframes/issues/728 + """ + # Avoid calling __init__ to simulate pausing __init__ in a debugger. + # https://stackoverflow.com/a/6384982/101923 + dataframe = bigframes.dataframe.DataFrame.__new__(bigframes.dataframe.DataFrame) + got = repr(dataframe) + assert "DataFrame" in got + + def test_dataframe_to_gbq_invalid_destination(monkeypatch: pytest.MonkeyPatch): dataframe = resources.create_dataframe(monkeypatch) diff --git a/tests/unit/test_series.py b/tests/unit/test_series.py new file mode 100644 index 00000000000..1409209c6ca --- /dev/null +++ b/tests/unit/test_series.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.series + + +def test_series_repr_with_uninitialized_object(): + """Ensures Series.__init__ can be paused in a visual debugger without crashing. + + Regression test for https://github.com/googleapis/python-bigquery-dataframes/issues/728 + """ + # Avoid calling __init__ to simulate pausing __init__ in a debugger. + # https://stackoverflow.com/a/6384982/101923 + series = bigframes.series.Series.__new__(bigframes.series.Series) + got = repr(series) + assert "Series" in got From d5ae6807022aa556e80c76a254c2a72aef82f54b Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:29:56 -0700 Subject: [PATCH 656/921] chore: cleanup type errors in tests/system/small/operations/test_plotting.py (#785) --- tests/system/small/operations/test_plotting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/system/small/operations/test_plotting.py b/tests/system/small/operations/test_plotting.py index e0ef84641ce..7be44e0a0f1 100644 --- a/tests/system/small/operations/test_plotting.py +++ b/tests/system/small/operations/test_plotting.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from matplotlib.collections import PathCollection import numpy as np import pandas as pd import pandas._testing as tm @@ -258,9 +259,10 @@ def test_scatter_args_s(s): ax = df.plot.scatter(x="a", y="b", s="s") pd_ax = pd_df.plot.scatter(x="a", y="b", s="s") - # TODO(b/340891723): fix type error + + assert isinstance(pd_ax.collections[0], PathCollection) tm.assert_numpy_array_equal( - ax.collections[0].get_sizes(), pd_ax.collections[0].get_sizes() # type: ignore + ax.collections[0].get_sizes(), pd_ax.collections[0].get_sizes() ) From 56dd591c7ceb6587741b48c2ce8c4f990efd0970 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 13 Jun 2024 14:25:31 -0700 Subject: [PATCH 657/921] refactor: Remove ibis usage to define new dataframe objects (#783) --- bigframes/core/__init__.py | 25 --------------- bigframes/core/blocks.py | 35 ++++++++++++++------- bigframes/core/compile/compiler.py | 16 ---------- bigframes/core/nodes.py | 50 ------------------------------ tests/unit/resources.py | 18 +++-------- tests/unit/test_core.py | 32 ------------------- 6 files changed, 29 insertions(+), 147 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index e0b63b4a8c2..2dc6184afce 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -23,7 +23,6 @@ import warnings import google.cloud.bigquery -import ibis.expr.types as ibis_types import pandas import pyarrow as pa import pyarrow.feather as pa_feather @@ -60,30 +59,6 @@ class ArrayValue: node: nodes.BigFrameNode - # DO NOT use, on deprecation path - @classmethod - def from_ibis( - cls, - session: Session, - table: ibis_types.Table, - columns: Sequence[ibis_types.Value], - hidden_ordering_columns: Sequence[ibis_types.Value], - ordering: orderings.ExpressionOrdering, - ): - import bigframes.core.compile.ibis_types - - node = nodes.ReadGbqNode( - table=table, - table_session=session, - columns=tuple( - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type(column) - for column in columns - ), - hidden_ordering_columns=tuple(hidden_ordering_columns), - ordering=ordering, - ) - return cls(node) - @classmethod def from_pyarrow(cls, arrow_table: pa.Table, session: Session): adapted_table = local_data.adapt_pa_table(arrow_table) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 301bcc20e94..2e7d6eb913b 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2345,7 +2345,6 @@ def _get_rows_as_json_values(self) -> Block: # TODO(shobs): Replace direct SQL manipulation by structured expression # manipulation ordering_column_name = guid.generate_guid() - self.session._cache_with_offsets(self.expr) expr = self.expr.promote_offsets(ordering_column_name) expr_sql = self.session._to_sql(expr) @@ -2415,17 +2414,31 @@ def _get_rows_as_json_values(self) -> Block: ) SELECT {select_columns_csv} FROM T1 """ - ibis_table = self.session.ibis_client.sql(json_sql) - order_for_ibis_table = ordering.ExpressionOrdering.from_offset_col( - ordering_column_name - ) - expr = core.ArrayValue.from_ibis( - self.session, - ibis_table, - [ibis_table[col] for col in select_columns if col != ordering_column_name], - hidden_ordering_columns=[ibis_table[ordering_column_name]], - ordering=order_for_ibis_table, + # The only ways this code is used is through df.apply(axis=1) cope path + destination, query_job = self.session._query_to_destination( + json_sql, index_cols=[ordering_column_name], api_name="apply" + ) + if not destination: + raise ValueError(f"Query job {query_job} did not produce result table") + + new_schema = ( + self.expr.schema.select([*self.index_columns]) + .append( + bf_schema.SchemaItem( + row_json_column_name, bigframes.dtypes.STRING_DTYPE + ) + ) + .append( + bf_schema.SchemaItem(ordering_column_name, bigframes.dtypes.INT_DTYPE) + ) ) + + expr = core.ArrayValue.from_table( + self.session.bqclient.get_table(destination), + schema=new_schema, + session=self.session, + offsets_col=ordering_column_name, + ).drop_columns([ordering_column_name]) block = Block( expr, index_columns=self.index_columns, diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 021ec8b176b..9272b8ad1b0 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -223,22 +223,6 @@ def compile_read_table_ordered(node: nodes.ReadTableNode): ) -@_compile_node.register -def compile_readgbq(node: nodes.ReadGbqNode, ordered: bool = True): - if ordered: - return compiled.OrderedIR( - node.table, - node.columns, - node.hidden_ordering_columns, - node.ordering, - ) - else: - return compiled.UnorderedIR( - node.table, - node.columns, - ) - - @_compile_node.register def compile_promote_offsets(node: nodes.PromoteOffsetsNode, ordered: bool = True): result = compile_ordered_ir(node.child).promote_offsets(node.col_id) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 077a362ba03..359f494fa1d 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -34,8 +34,6 @@ import bigframes.operations.aggregations as agg_ops if typing.TYPE_CHECKING: - import ibis.expr.types as ibis_types - import bigframes.core.ordering as orderings import bigframes.session @@ -302,54 +300,6 @@ def transform_children( return self -# TODO: Refactor to take raw gbq object reference -@dataclass(frozen=True) -class ReadGbqNode(BigFrameNode): - table: ibis_types.Table = field() - table_session: bigframes.session.Session = field() - columns: Tuple[ibis_types.Value, ...] = field() - hidden_ordering_columns: Tuple[ibis_types.Value, ...] = field() - ordering: orderings.ExpressionOrdering = field() - - @property - def session(self): - return self.table_session - - def __hash__(self): - return self._node_hash - - @property - def roots(self) -> typing.Set[BigFrameNode]: - return {self} - - @functools.cached_property - def schema(self) -> schemata.ArraySchema: - from bigframes.core.compile.ibis_types import ibis_dtype_to_bigframes_dtype - - items = tuple( - schemata.SchemaItem( - value.get_name(), - ibis_dtype_to_bigframes_dtype(value.type()), - ) - for value in self.columns - ) - return schemata.ArraySchema(items) - - @functools.cached_property - def variables_introduced(self) -> int: - return len(self.columns) + len(self.hidden_ordering_columns) - - @property - def relation_ops_created(self) -> int: - # Assume worst case, where readgbq actually has baked in analytic operation to generate index - return 2 - - def transform_children( - self, t: Callable[[BigFrameNode], BigFrameNode] - ) -> BigFrameNode: - return self - - ## Put ordering in here or just add order_by node above? @dataclass(frozen=True) class ReadTableNode(BigFrameNode): diff --git a/tests/unit/resources.py b/tests/unit/resources.py index 84699459e64..d45da82ab96 100644 --- a/tests/unit/resources.py +++ b/tests/unit/resources.py @@ -20,6 +20,7 @@ import google.cloud.bigquery import ibis import pandas +import pyarrow as pa import pytest import bigframes @@ -130,18 +131,9 @@ def create_arrayvalue( df: pandas.DataFrame, total_ordering_columns: List[str] ) -> core.ArrayValue: session = create_pandas_session({"test_table": df}) - ibis_table = session.ibis_client.table("test_table") - columns = tuple(ibis_table[key] for key in ibis_table.columns) - ordering = bigframes.core.ordering.ExpressionOrdering( - tuple( - [core.orderings.ascending_over(column) for column in total_ordering_columns] - ), - total_ordering_columns=frozenset(total_ordering_columns), - ) - return core.ArrayValue.from_ibis( + return core.ArrayValue.from_pyarrow( + arrow_table=pa.Table.from_pandas(df, preserve_index=False), session=session, - table=ibis_table, - columns=columns, - hidden_ordering_columns=(), - ordering=ordering, + ).order_by( + [bigframes.core.ordering.ascending_over(col) for col in total_ordering_columns] ) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 0a2fc61418f..583e22d06e2 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -15,45 +15,13 @@ import ibis.expr.types as ibis_types import pandas -import bigframes.core as core import bigframes.core.expression as ex -import bigframes.core.ordering as order import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops from . import resources -def test_arrayvalue_constructor_from_ibis_table_adds_all_columns(): - session = resources.create_pandas_session( - { - "test_table": pandas.DataFrame( - { - "col1": [1, 2, 3], - "not_included": [True, False, True], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ) - } - ) - ibis_table = session.ibis_client.table("test_table") - columns = (ibis_table["col1"], ibis_table["col2"], ibis_table["col3"]) - ordering = order.ExpressionOrdering( - tuple([order.ascending_over("col1")]), - total_ordering_columns=frozenset(["col1"]), - ) - actual = core.ArrayValue.from_ibis( - session=session, - table=ibis_table, - columns=columns, - ordering=ordering, - hidden_ordering_columns=(), - ) - assert actual._compile_ordered()._table is ibis_table - assert len(actual.column_ids) == 3 - - def test_arrayvalue_with_get_column_type(): value = resources.create_arrayvalue( pandas.DataFrame( From 0e57d1f1f8a150ba6faac5f667bb5b4c78f4c0a3 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 13 Jun 2024 14:53:24 -0700 Subject: [PATCH 658/921] fix: Fix temp table creation retries by now throwing if table already exists. (#787) --- bigframes/session/_io/bigquery/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index f26ca26c2a3..c4d007be220 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -146,7 +146,9 @@ def create_temp_table( destination.schema = schema if cluster_columns: destination.clustering_fields = cluster_columns - bqclient.create_table(destination) + # Ok if already exists, since this will only happen from retries internal to this method + # as the requested table id has a random UUID4 component. + bqclient.create_table(destination, exists_ok=True) return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}" From f6c89b5e4242350f402f716309cf9f6c4ab4c8f9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 13 Jun 2024 17:09:14 -0700 Subject: [PATCH 659/921] chore: Remove owlbot from required status checks (#790) --- .github/sync-repo-settings.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index b98d68799ad..e098468da69 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -7,15 +7,16 @@ branchProtectionRules: requiresCodeOwnerReviews: true requiresStrictStatusChecks: false requiredStatusCheckContexts: +# TODO(b/347075426): Restore owlbot as required check +# - 'OwlBot Post Processor' - 'conventionalcommits.org' - 'cla/google' - - 'OwlBot Post Processor' - 'docs' - 'lint' - 'unit (3.9)' - 'unit (3.10)' - 'unit (3.11)' - - 'unit (3.12)' + - 'unit (3.12)' - 'cover' - 'Kokoro presubmit' permissionRules: From a4ac82e06221581ddfcfc1246a3e3cd65a8bb00e Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:31:23 -0700 Subject: [PATCH 660/921] fix: df.loc with the 2nd input as bigframes boolean Series (#789) * fix: df.loc with the 2nd input as bigframes boolean Series * merge main * modify test case * fix comment --- bigframes/core/indexers.py | 7 ++++++- tests/system/small/test_dataframe.py | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index 8ce2a5903c4..dae5eada700 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -159,7 +159,12 @@ def __getitem__(self, key): ) columns = key[1] - if isinstance(columns, pd.Series) and columns.dtype == bool: + if isinstance(columns, bigframes.series.Series): + columns = columns.to_pandas() + if isinstance(columns, pd.Series) and columns.dtype in ( + bool, + pd.BooleanDtype(), + ): columns = df.columns[typing.cast(pd.Series, columns)] return df[columns] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 93db0c41964..782ef2d5eaf 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3020,6 +3020,29 @@ def test_loc_select_with_column_condition(scalars_df_index, scalars_pandas_df_in ) +def test_loc_select_with_column_condition_bf_series( + scalars_df_index, scalars_pandas_df_index +): + # (b/347072677) GEOGRAPH type doesn't support DISTINCT op + columns = [ + item for item in scalars_pandas_df_index.columns if item != "geography_col" + ] + scalars_df_index = scalars_df_index[columns] + scalars_pandas_df_index = scalars_pandas_df_index[columns] + + size_half = len(scalars_pandas_df_index) / 2 + bf_result = scalars_df_index.loc[ + :, scalars_df_index.nunique() > size_half + ].to_pandas() + pd_result = scalars_pandas_df_index.loc[ + :, scalars_pandas_df_index.nunique() > size_half + ] + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + def test_loc_single_index_with_duplicate(scalars_df_index, scalars_pandas_df_index): scalars_df_index = scalars_df_index.set_index("string_col", drop=False) scalars_pandas_df_index = scalars_pandas_df_index.set_index( From c8d16c0f72a25bce854b80be517114e1603c947e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 14 Jun 2024 16:22:41 -0500 Subject: [PATCH 661/921] feat: include index columns in DataFrame.sql if they are named (#788) --- bigframes/dataframe.py | 3 +- tests/system/small/test_dataframe_io.py | 97 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9763b68fef0..a27e4abe9aa 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -379,7 +379,8 @@ def _to_sql_query( @property def sql(self) -> str: """Compiles this DataFrame's expression tree to SQL.""" - sql, _, _ = self._to_sql_query(include_index=False) + include_index = self.index.name is not None or len(self.index.names) > 1 + sql, _, _ = self._to_sql_query(include_index=include_index) return sql @property diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index f36dd64cbe4..8adbea88e4f 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -16,6 +16,7 @@ import google.api_core.exceptions import pandas as pd +import pandas.testing import pyarrow as pa import pytest @@ -35,6 +36,102 @@ import bigframes.pandas as bpd +def test_sql_executes(scalars_df_default_index, bigquery_client): + """Test that DataFrame.sql returns executable SQL. + + DF.sql is used in public documentation such as + https://cloud.google.com/blog/products/data-analytics/using-bigquery-dataframes-with-carto-geospatial-tools + as a way to pass a DataFrame on to carto without executing the SQL + immediately. + + Make sure that this SQL can be run outside of BigQuery DataFrames (assuming + similar credentials / access to the referenced tables). + """ + # Do some operations to make for more complex SQL. + df = ( + scalars_df_default_index.drop(columns=["geography_col"]) + .groupby("string_col") + .max() + ) + df.index.name = None # Don't include unnamed indexes. + query = df.sql + + bf_result = df.to_pandas().sort_values("rowindex").reset_index(drop=True) + bq_result = ( + bigquery_client.query_and_wait(query) + .to_dataframe() + .sort_values("rowindex") + .reset_index(drop=True) + ) + pandas.testing.assert_frame_equal(bf_result, bq_result, check_dtype=False) + + +def test_sql_executes_and_includes_named_index( + scalars_df_default_index, bigquery_client +): + """Test that DataFrame.sql returns executable SQL. + + DF.sql is used in public documentation such as + https://cloud.google.com/blog/products/data-analytics/using-bigquery-dataframes-with-carto-geospatial-tools + as a way to pass a DataFrame on to carto without executing the SQL + immediately. + + Make sure that this SQL can be run outside of BigQuery DataFrames (assuming + similar credentials / access to the referenced tables). + """ + # Do some operations to make for more complex SQL. + df = ( + scalars_df_default_index.drop(columns=["geography_col"]) + .groupby("string_col") + .max() + ) + query = df.sql + + bf_result = df.to_pandas().sort_values("rowindex") + bq_result = ( + bigquery_client.query_and_wait(query) + .to_dataframe() + .set_index("string_col") + .sort_values("rowindex") + ) + pandas.testing.assert_frame_equal( + bf_result, bq_result, check_dtype=False, check_index_type=False + ) + + +def test_sql_executes_and_includes_named_multiindex( + scalars_df_default_index, bigquery_client +): + """Test that DataFrame.sql returns executable SQL. + + DF.sql is used in public documentation such as + https://cloud.google.com/blog/products/data-analytics/using-bigquery-dataframes-with-carto-geospatial-tools + as a way to pass a DataFrame on to carto without executing the SQL + immediately. + + Make sure that this SQL can be run outside of BigQuery DataFrames (assuming + similar credentials / access to the referenced tables). + """ + # Do some operations to make for more complex SQL. + df = ( + scalars_df_default_index.drop(columns=["geography_col"]) + .groupby(["string_col", "bool_col"]) + .max() + ) + query = df.sql + + bf_result = df.to_pandas().sort_values("rowindex") + bq_result = ( + bigquery_client.query_and_wait(query) + .to_dataframe() + .set_index(["string_col", "bool_col"]) + .sort_values("rowindex") + ) + pandas.testing.assert_frame_equal( + bf_result, bq_result, check_dtype=False, check_index_type=False + ) + + def test_to_pandas_w_correct_dtypes(scalars_df_default_index): """Verify to_pandas() APIs returns the expected dtypes.""" actual = scalars_df_default_index.to_pandas().dtypes From 44202bc3541df03154ea0b2cca8eac18094a91a9 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:57:27 -0700 Subject: [PATCH 662/921] feat: add groupby head API (#791) * feat: add groupby head API * update annotations * update order --- bigframes/core/blocks.py | 20 ++++++++++++++++++++ bigframes/core/groupby/__init__.py | 22 ++++++++++++++++++++++ tests/system/small/test_groupby.py | 20 ++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 2e7d6eb913b..56e822eb4e7 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1384,6 +1384,26 @@ def _normalize_expression( raise ValueError("Unexpected number of value columns.") return expr.select_columns([*index_columns, *value_columns]) + def grouped_head( + self, + by_column_ids: typing.Sequence[str], + value_columns: typing.Sequence[str], + n: int, + ): + window_spec = window_specs.cumulative_rows(grouping_keys=tuple(by_column_ids)) + + block, result_id = self.apply_window_op( + value_columns[0], + agg_ops.rank_op, + window_spec=window_spec, + ) + + cond = ops.lt_op.as_expr(result_id, ex.const(n + 1)) + block, cond_id = block.project_expr(cond) + block = block.filter_by_id(cond_id) + if value_columns: + return block.select_columns(value_columns) + def slice( self, start: typing.Optional[int] = None, diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index ee120635d32..9c2bf18caaa 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -104,6 +104,18 @@ def __getitem__( dropna=self._dropna, ) + def head(self, n: int = 5) -> df.DataFrame: + block = self._block + if self._dropna: + block = block_ops.dropna(self._block, self._by_col_ids, how="any") + return df.DataFrame( + block.grouped_head( + by_column_ids=self._by_col_ids, + value_columns=self._block.value_columns, + n=n, + ) + ) + def size(self) -> typing.Union[df.DataFrame, series.Series]: agg_block, _ = self._block.aggregate_size( by_column_ids=self._by_col_ids, @@ -498,6 +510,16 @@ def __init__( self._value_name = value_name self._dropna = dropna # Applies to aggregations but not windowing + def head(self, n: int = 5) -> series.Series: + block = self._block + if self._dropna: + block = block_ops.dropna(self._block, self._by_col_ids, how="any") + return series.Series( + block.grouped_head( + by_column_ids=self._by_col_ids, value_columns=[self._value_column], n=n + ) + ) + def all(self) -> series.Series: return self._aggregate(agg_ops.all_op) diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index b332d48574c..960dc10948c 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -53,6 +53,13 @@ def test_dataframe_groupby_numeric_aggregate( pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +def test_dataframe_groupby_head(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = scalars_df_index[col_names].groupby("bool_col").head(2).to_pandas() + pd_result = scalars_pandas_df_index[col_names].groupby("bool_col").head(2) + pd.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + + def test_dataframe_groupby_median(scalars_df_index, scalars_pandas_df_index): col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = ( @@ -442,6 +449,19 @@ def test_series_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): ) +@pytest.mark.parametrize("dropna", [True, False]) +def test_series_groupby_head(scalars_df_index, scalars_pandas_df_index, dropna): + bf_result = ( + scalars_df_index.groupby("bool_col", dropna=dropna)["int64_too"] + .head(1) + .to_pandas() + ) + pd_result = scalars_pandas_df_index.groupby("bool_col", dropna=dropna)[ + "int64_too" + ].head(1) + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + def test_series_groupby_kurt(scalars_df_index, scalars_pandas_df_index): bf_result = ( scalars_df_index["int64_too"] From fbd6c4ca0f87a46938a131ad47ec638cb532057d Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:06:19 -0700 Subject: [PATCH 663/921] chore: add sort benchmark (#781) * chore: add sort benchmark * add file. --- scripts/benchmark/db-benchmark/sort | 0 .../db-benchmark/sort/J1_1e9_NA_0_0/q1.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+) delete mode 100644 scripts/benchmark/db-benchmark/sort create mode 100644 scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py diff --git a/scripts/benchmark/db-benchmark/sort b/scripts/benchmark/db-benchmark/sort deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py b/scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py new file mode 100644 index 00000000000..45cac7b5439 --- /dev/null +++ b/scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py @@ -0,0 +1,15 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/sort-pandas.py + +import bigframes.pandas as bpd + +print("Sort benchmark 1: sort by int id2") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") + +ans = x.sort_values("id2") +print(ans.shape) + +chk = [ans["v1"].sum()] +print(chk) + +bpd.reset_session() From 324d93cb31191520b790bbbc501468b8d1d8467d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 18 Jun 2024 18:13:05 -0500 Subject: [PATCH 664/921] fix: ensure numpy version matches in `remote_function` deployment (#798) * fix: ensure numpy version matches in `remote_function` deploymnet * add comment linking to the bug * restore noxfile * restore flaky decoration * skip tests that reuse functions with stale packages --- bigframes/functions/remote_function.py | 4 + tests/system/small/test_remote_function.py | 103 --------------------- 2 files changed, 4 insertions(+), 103 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 472ac075474..920dc7c039a 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -39,6 +39,7 @@ import warnings import ibis +import numpy import pandas import pyarrow import requests @@ -280,6 +281,9 @@ def generate_cloud_function_code( if is_row_processor: # bigframes remote function will send an entire row of data as json, # which would be converted to a pandas series and processed + # Ensure numpy versions match to avoid unpickling problems. See + # internal issue b/347934471. + requirements.append(f"numpy=={numpy.__version__}") requirements.append(f"pandas=={pandas.__version__}") requirements.append(f"pyarrow=={pyarrow.__version__}") if package_requirements: diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index d84d520988a..5838ad75b00 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -742,109 +742,6 @@ def test_read_gbq_function_enforces_explicit_types( ) -@pytest.mark.flaky(retries=2, delay=120) -def test_df_apply_axis_1(session, scalars_dfs): - columns = [ - "bool_col", - "int64_col", - "int64_too", - "float64_col", - "string_col", - "bytes_col", - ] - scalars_df, scalars_pandas_df = scalars_dfs - - def add_ints(row): - return row["int64_col"] + row["int64_too"] - - with pytest.warns( - bigframes.exceptions.PreviewWarning, - match="input_types=Series is in preview.", - ): - add_ints_remote = session.remote_function( - bigframes.series.Series, - int, - )(add_ints) - - with pytest.warns( - bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." - ): - bf_result = scalars_df[columns].apply(add_ints_remote, axis=1).to_pandas() - - pd_result = scalars_pandas_df[columns].apply(add_ints, axis=1) - - # bf_result.dtype is 'Int64' while pd_result.dtype is 'object', ignore this - # mismatch by using check_dtype=False. - # - # bf_result.to_numpy() produces an array of numpy.float64's - # (in system_prerelease tests), while pd_result.to_numpy() produces an - # array of ints, ignore this mismatch by using check_exact=False. - pd.testing.assert_series_equal( - pd_result, bf_result, check_dtype=False, check_exact=False - ) - - -@pytest.mark.flaky(retries=2, delay=120) -def test_df_apply_axis_1_ordering(session, scalars_dfs): - columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] - ordering_columns = ["bool_col", "int64_col"] - scalars_df, scalars_pandas_df = scalars_dfs - - def add_ints(row): - return row["int64_col"] + row["int64_too"] - - add_ints_remote = session.remote_function(bigframes.series.Series, int)(add_ints) - - bf_result = ( - scalars_df[columns] - .sort_values(ordering_columns) - .apply(add_ints_remote, axis=1) - .to_pandas() - ) - pd_result = ( - scalars_pandas_df[columns].sort_values(ordering_columns).apply(add_ints, axis=1) - ) - - # bf_result.dtype is 'Int64' while pd_result.dtype is 'object', ignore this - # mismatch by using check_dtype=False. - # - # bf_result.to_numpy() produces an array of numpy.float64's - # (in system_prerelease tests), while pd_result.to_numpy() produces an - # array of ints, ignore this mismatch by using check_exact=False. - pd.testing.assert_series_equal( - pd_result, bf_result, check_dtype=False, check_exact=False - ) - - -@pytest.mark.flaky(retries=2, delay=120) -def test_df_apply_axis_1_multiindex(session): - pd_df = pd.DataFrame( - {"x": [1, 2, 3], "y": [1.5, 3.75, 5], "z": ["pq", "rs", "tu"]}, - index=pd.MultiIndex.from_tuples([("a", 100), ("a", 200), ("b", 300)]), - ) - bf_df = session.read_pandas(pd_df) - - def add_numbers(row): - return row["x"] + row["y"] - - add_numbers_remote = session.remote_function(bigframes.series.Series, float)( - add_numbers - ) - - bf_result = bf_df.apply(add_numbers_remote, axis=1).to_pandas() - pd_result = pd_df.apply(add_numbers, axis=1) - - # bf_result.dtype is 'Float64' while pd_result.dtype is 'float64', ignore this - # mismatch by using check_dtype=False. - # - # bf_result.index[0].dtype is 'string[pyarrow]' while - # pd_result.index[0].dtype is 'object', ignore this mismatch by using - # check_index_type=False. - pd.testing.assert_series_equal( - pd_result, bf_result, check_dtype=False, check_index_type=False - ) - - def test_df_apply_axis_1_unsupported_callable(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] From 2de4129142a66f96d5a50c45c24bace5043a2266 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 18 Jun 2024 17:35:27 -0700 Subject: [PATCH 665/921] refactor: Reduce compile interface to returning just SQL text (#786) --- bigframes/core/__init__.py | 21 +--- bigframes/core/compile/__init__.py | 21 +++- bigframes/core/compile/api.py | 72 ++++++++++++ bigframes/core/compile/compiled.py | 9 ++ bigframes/core/nodes.py | 13 +++ bigframes/session/__init__.py | 61 ++++------ tests/unit/test_core.py | 178 ----------------------------- 7 files changed, 137 insertions(+), 238 deletions(-) create mode 100644 bigframes/core/compile/api.py delete mode 100644 tests/unit/test_core.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 2dc6184afce..25088148948 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -27,7 +27,7 @@ import pyarrow as pa import pyarrow.feather as pa_feather -import bigframes.core.compile as compiling +import bigframes.core.compile import bigframes.core.expression as ex import bigframes.core.guid import bigframes.core.join_def as join_def @@ -142,12 +142,7 @@ def schema(self) -> schemata.ArraySchema: @functools.cached_property def _compiled_schema(self) -> schemata.ArraySchema: - compiled = self._compile_unordered() - items = tuple( - schemata.SchemaItem(id, compiled.get_column_type(id)) - for id in compiled.column_ids - ) - return schemata.ArraySchema(items) + return bigframes.core.compile.test_only_ibis_inferred_schema(self.node) def as_cached( self: ArrayValue, @@ -169,21 +164,11 @@ def as_cached( def _try_evaluate_local(self): """Use only for unit testing paths - not fully featured. Will throw exception if fails.""" - import ibis - - return ibis.pandas.connect({}).execute( - self._compile_ordered()._to_ibis_expr(ordering_mode="unordered") - ) + return bigframes.core.compile.test_only_try_evaluate(self.node) def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: return self.schema.get_type(key) - def _compile_ordered(self) -> compiling.OrderedIR: - return compiling.compile_ordered_ir(self.node) - - def _compile_unordered(self) -> compiling.UnorderedIR: - return compiling.compile_unordered_ir(self.node) - def row_count(self) -> ArrayValue: """Get number of rows in ArrayValue as a single-entry ArrayValue.""" return ArrayValue(nodes.RowCountNode(child=self.node)) diff --git a/bigframes/core/compile/__init__.py b/bigframes/core/compile/__init__.py index c3e2bd832a0..4c105ed03bf 100644 --- a/bigframes/core/compile/__init__.py +++ b/bigframes/core/compile/__init__.py @@ -11,13 +11,22 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations -from bigframes.core.compile.compiled import OrderedIR, UnorderedIR -from bigframes.core.compile.compiler import compile_ordered_ir, compile_unordered_ir +from bigframes.core.compile.api import ( + compile_ordered, + compile_peek, + compile_raw, + compile_unordered, + test_only_ibis_inferred_schema, + test_only_try_evaluate, +) __all__ = [ - "compile_ordered_ir", - "compile_unordered_ir", - "OrderedIR", - "UnorderedIR", + "compile_peek", + "compile_unordered", + "compile_ordered", + "compile_raw", + "test_only_try_evaluate", + "test_only_ibis_inferred_schema", ] diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py new file mode 100644 index 00000000000..f00af6412e5 --- /dev/null +++ b/bigframes/core/compile/api.py @@ -0,0 +1,72 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from typing import Mapping, Tuple, TYPE_CHECKING + +import bigframes.core.compile.compiler as compiler + +if TYPE_CHECKING: + import bigframes.core.nodes + import bigframes.core.ordering + import bigframes.core.schema + + +def compile_peek(node: bigframes.core.nodes.BigFrameNode, n_rows: int) -> str: + """Compile node into sql that selects N arbitrary rows, may not execute deterministically.""" + return compiler.compile_unordered_ir(node).peek_sql(n_rows) + + +def compile_unordered( + node: bigframes.core.nodes.BigFrameNode, *, col_id_overrides: Mapping[str, str] = {} +) -> str: + """Compile node into sql where rows are unsorted, and no ordering information is preserved.""" + return compiler.compile_unordered_ir(node).to_sql(col_id_overrides=col_id_overrides) + + +def compile_ordered( + node: bigframes.core.nodes.BigFrameNode, *, col_id_overrides: Mapping[str, str] = {} +) -> str: + """Compile node into sql where rows are sorted with ORDER BY.""" + return compiler.compile_ordered_ir(node).to_sql( + col_id_overrides=col_id_overrides, sorted=True + ) + + +def compile_raw( + node: bigframes.core.nodes.BigFrameNode, +) -> Tuple[str, bigframes.core.ordering.ExpressionOrdering]: + """Compile node into sql that exposes all columns, including hidden ordering-only columns.""" + ir = compiler.compile_ordered_ir(node) + sql = ir.raw_sql() + ordering_info = ir._ordering + return sql, ordering_info + + +def test_only_try_evaluate(node: bigframes.core.nodes.BigFrameNode): + """Use only for unit testing paths - not fully featured. Will throw exception if fails.""" + ibis = compiler.compile_ordered_ir(node)._to_ibis_expr(ordering_mode="unordered") + return ibis.pandas.connect({}).execute(ibis) + + +def test_only_ibis_inferred_schema(node: bigframes.core.nodes.BigFrameNode): + """Use only for testing paths to ensure ibis inferred schema does not diverge from bigframes inferred schema.""" + import bigframes.core.schema + + compiled = compiler.compile_unordered_ir(node) + items = tuple( + bigframes.core.schema.SchemaItem(id, compiled.get_column_type(id)) + for id in compiled.column_ids + ) + return bigframes.core.schema.ArraySchema(items) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index dac814a08c9..7b258149e0a 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -923,6 +923,15 @@ def to_sql( ) return typing.cast(str, sql) + def raw_sql(self) -> str: + """Return sql with all hidden columns. Used to cache with ordering information.""" + return ibis_bigquery.Backend().compile( + self._to_ibis_expr( + ordering_mode="unordered", + expose_hidden_cols=True, + ) + ) + def _to_ibis_expr( self, *, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 359f494fa1d..65a1bd80846 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -375,6 +375,19 @@ class CachedTableNode(BigFrameNode): ordering: typing.Optional[orderings.ExpressionOrdering] = field() + def __post_init__(self): + # enforce invariants + physical_names = set(map(lambda i: i.name, self.physical_schema)) + logical_names = self.original_node.schema.names + if not set(logical_names).issubset(physical_names): + raise ValueError( + f"Requested schema {logical_names} cannot be derived from table schema {self.physical_schema}" + ) + if not set(self.hidden_columns).issubset(physical_names): + raise ValueError( + f"Requested hidden columns {self.hidden_columns} cannot be derived from table schema {self.physical_schema}" + ) + @property def session(self): return self.original_node.session diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 4c5ce211530..78004af0df4 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -64,7 +64,6 @@ import google.cloud.storage as storage # type: ignore import ibis import ibis.backends.bigquery as ibis_bigquery -import ibis.expr.types as ibis_types import jellyfish import numpy as np import pandas @@ -248,6 +247,8 @@ def __init__( # the ibis client has been created original_default_query_job_config = self.bqclient.default_query_job_config + # Only used to fetch remote function metadata. + # TODO: Remove in favor of raw bq client self.ibis_client = typing.cast( ibis_bigquery.Backend, ibis.bigquery.connect( @@ -1497,14 +1498,14 @@ def _create_empty_temp_table( ) return bigquery.TableReference.from_string(table) - def _ibis_to_temp_table( + def _sql_to_temp_table( self, - table: ibis_types.Table, + sql: str, cluster_cols: Iterable[str], api_name: str, ) -> bigquery.TableReference: destination, _ = self._query_to_destination( - self.ibis_client.compile(table), + sql, index_cols=list(cluster_cols), api_name=api_name, ) @@ -1847,17 +1848,15 @@ def _cache_with_cluster_cols( # TODO: May want to support some partial ordering info even for non-strict ordering mode keep_order_info = self._strictly_ordered - compiled_value = self._compile_ordered(array_value) - - ibis_expr = compiled_value._to_ibis_expr( - ordering_mode="unordered", expose_hidden_cols=keep_order_info + sql, ordering_info = bigframes.core.compile.compile_raw( + self._with_cached_executions(array_value.node) ) - tmp_table = self._ibis_to_temp_table( - ibis_expr, cluster_cols=cluster_cols, api_name="cached" + tmp_table = self._sql_to_temp_table( + sql, cluster_cols=cluster_cols, api_name="cached" ) cached_replacement = array_value.as_cached( cache_table=self.bqclient.get_table(tmp_table), - ordering=compiled_value._ordering if keep_order_info else None, + ordering=ordering_info if keep_order_info else None, ).node self._cached_executions[array_value.node] = cached_replacement @@ -1869,13 +1868,14 @@ def _cache_with_offsets(self, array_value: core.ArrayValue): raise ValueError( "Caching with offsets only supported in strictly ordered mode." ) - compiled_value = self._compile_ordered(array_value) - - ibis_expr = compiled_value._to_ibis_expr( - ordering_mode="offset_col", order_col_name="bigframes_offsets" + sql = bigframes.core.compile.compile_unordered( + self._with_cached_executions( + array_value.promote_offsets("bigframes_offsets").node + ) ) - tmp_table = self._ibis_to_temp_table( - ibis_expr, cluster_cols=["bigframes_offsets"], api_name="cached" + + tmp_table = self._sql_to_temp_table( + sql, cluster_cols=["bigframes_offsets"], api_name="cached" ) cached_replacement = array_value.as_cached( cache_table=self.bqclient.get_table(tmp_table), @@ -1962,7 +1962,9 @@ def _peek( """A 'peek' efficiently accesses a small number of rows in the dataframe.""" if not tree_properties.peekable(self._with_cached_executions(array_value.node)): warnings.warn("Peeking this value cannot be done efficiently.") - sql = self._compile_unordered(array_value).peek_sql(n_rows) + sql = bigframes.core.compile.compile_peek( + self._with_cached_executions(array_value.node), n_rows + ) # TODO(swast): plumb through the api_name of the user-facing api that # caused this query. @@ -1979,26 +1981,13 @@ def _to_sql( ) -> str: if offset_column: array_value = array_value.promote_offsets(offset_column) + node_w_cached = self._with_cached_executions(array_value.node) if sorted: - return self._compile_ordered(array_value).to_sql( - col_id_overrides=col_id_overrides, sorted=True + return bigframes.core.compile.compile_ordered( + node_w_cached, col_id_overrides=col_id_overrides ) - return self._compile_unordered(array_value).to_sql( - col_id_overrides=col_id_overrides - ) - - def _compile_ordered( - self, array_value: core.ArrayValue - ) -> bigframes.core.compile.OrderedIR: - return bigframes.core.compile.compile_ordered_ir( - self._with_cached_executions(array_value.node) - ) - - def _compile_unordered( - self, array_value: core.ArrayValue - ) -> bigframes.core.compile.UnorderedIR: - return bigframes.core.compile.compile_unordered_ir( - self._with_cached_executions(array_value.node) + return bigframes.core.compile.compile_unordered( + node_w_cached, col_id_overrides=col_id_overrides ) def _get_table_size(self, destination_table): diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py deleted file mode 100644 index 583e22d06e2..00000000000 --- a/tests/unit/test_core.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import ibis.expr.types as ibis_types -import pandas - -import bigframes.core.expression as ex -import bigframes.operations as ops -import bigframes.operations.aggregations as agg_ops - -from . import resources - - -def test_arrayvalue_with_get_column_type(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - col1_type = value.get_column_type("col1") - col2_type = value.get_column_type("col2") - col3_type = value.get_column_type("col3") - assert isinstance(col1_type, pandas.Int64Dtype) - assert isinstance(col2_type, pandas.StringDtype) - assert isinstance(col3_type, pandas.Float64Dtype) - - -def test_arrayvalue_with_get_column(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - col1 = value._compile_ordered()._get_ibis_column("col1") - assert isinstance(col1, ibis_types.Value) - assert col1.get_name() == "col1" - assert col1.type().is_int64() - - -def test_arrayvalues_to_ibis_expr_with_get_column(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value._compile_ordered()._get_ibis_column("col1") - assert expr.get_name() == "col1" - assert expr.type().is_int64() - - -def test_arrayvalues_to_ibis_expr_with_concat(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value.concat([value]) - actual = expr._compile_unordered()._to_ibis_expr() - assert len(actual.columns) == 3 - # TODO(ashleyxu, b/299631930): test out the union expression - assert actual.columns[0] == "column_0" - assert actual.columns[1] == "column_1" - assert actual.columns[2] == "column_2" - - -def test_arrayvalues_to_ibis_expr_with_project_unary_op(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value.project_to_id( - ops.AsTypeOp("string").as_expr("col1"), output_id="col1" - )._compile_ordered() - assert value._compile_ordered().columns[0].type().is_int64() - assert expr.columns[0].type().is_string() - - -def test_arrayvalues_to_ibis_expr_with_project_binary_op(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": [0.2, 0.3, 0.4], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value.project_to_id( - ops.add_op.as_expr("col2", "col3"), "col4" - )._compile_ordered() - assert expr.columns[3].type().is_float64() - actual = expr._to_ibis_expr(ordering_mode="unordered") - assert len(expr.columns) == 4 - assert actual.columns[3] == "col4" - - -def test_arrayvalues_to_ibis_expr_with_project_ternary_op(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": [0.2, 0.3, 0.4], - "col3": [True, False, False], - "col4": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value.project_to_id( - ops.where_op.as_expr("col2", "col3", "col4"), "col5" - )._compile_ordered() - assert expr.columns[4].type().is_float64() - actual = expr._to_ibis_expr(ordering_mode="unordered") - assert len(expr.columns) == 5 - assert actual.columns[4] == "col5" - - -def test_arrayvalue_to_ibis_expr_with_aggregate(): - value = resources.create_arrayvalue( - pandas.DataFrame( - { - "col1": [1, 2, 3], - "col2": ["a", "b", "c"], - "col3": [0.1, 0.2, 0.3], - } - ), - total_ordering_columns=["col1"], - ) - expr = value.aggregate( - aggregations=( - (ex.UnaryAggregation(agg_ops.sum_op, ex.free_var("col1")), "col4"), - ), - by_column_ids=["col1"], - dropna=False, - )._compile_ordered() - actual = expr._to_ibis_expr(ordering_mode="unordered") - assert len(expr.columns) == 2 - assert actual.columns[0] == "col1" - assert actual.columns[1] == "col4" - assert expr.columns[1].type().is_int64() From 0afbceca19f4f542efc9f5faab8dcb395bcd3269 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 18 Jun 2024 19:02:42 -0700 Subject: [PATCH 666/921] refactor: create internal session ordering mode flag (#772) --- bigframes/_config/bigquery_options.py | 9 +++++++ bigframes/core/blocks.py | 17 ++++++++----- bigframes/core/compile/api.py | 2 +- bigframes/core/compile/compiled.py | 8 +++--- bigframes/core/indexes/base.py | 4 ++- bigframes/dataframe.py | 11 +++++---- bigframes/series.py | 9 ++++--- bigframes/session/__init__.py | 20 +++++++++------ tests/system/conftest.py | 5 +--- tests/system/small/test_unordered.py | 35 ++++++++++++++++++++++++++- 10 files changed, 87 insertions(+), 33 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 6f841a36b31..ad79543cb8e 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -70,6 +70,8 @@ def __init__( application_name: Optional[str] = None, kms_key_name: Optional[str] = None, skip_bq_connection_check: bool = False, + *, + _strictly_ordered: bool = True, ): self._credentials = credentials self._project = project @@ -80,6 +82,8 @@ def __init__( self._kms_key_name = kms_key_name self._skip_bq_connection_check = skip_bq_connection_check self._session_started = False + # Determines the ordering strictness for the session. For internal use only. + self._strictly_ordered_internal = _strictly_ordered @property def application_name(self) -> Optional[str]: @@ -235,3 +239,8 @@ def kms_key_name(self, value: str): raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="kms_key_name")) self._kms_key_name = value + + @property + def _strictly_ordered(self) -> bool: + """Internal use only. Controls whether total row order is always maintained for DataFrame/Series.""" + return self._strictly_ordered_internal diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 56e822eb4e7..640c070af12 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -209,7 +209,7 @@ def shape(self) -> typing.Tuple[int, int]: except Exception: pass - iter, _ = self.session._execute(row_count_expr, sorted=False) + iter, _ = self.session._execute(row_count_expr, ordered=False) row_count = next(iter)[0] return (row_count, len(self.value_columns)) @@ -518,7 +518,7 @@ def to_pandas_batches( dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) _, query_job = self.session._query_to_destination( - self.session._to_sql(self.expr, sorted=True), + self.session._to_sql(self.expr, ordered=self.session._strictly_ordered), list(self.index_columns), api_name="cached", do_clustering=False, @@ -553,7 +553,7 @@ def _materialize_local( """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. _, query_job = self.session._query_to_destination( - self.session._to_sql(self.expr, sorted=materialize_options.ordered), + self.session._to_sql(self.expr, ordered=materialize_options.ordered), list(self.index_columns), api_name="cached", do_clustering=False, @@ -1736,7 +1736,7 @@ def transpose( original_row_index = ( original_row_index if original_row_index is not None - else self.index.to_pandas() + else self.index.to_pandas(ordered=True) ) original_row_count = len(original_row_index) if original_row_count > bigframes.constants.MAX_COLUMNS: @@ -2507,7 +2507,7 @@ def column_ids(self) -> Sequence[str]: """Column(s) to use as row labels.""" return self._block._index_columns - def to_pandas(self) -> pd.Index: + def to_pandas(self, *, ordered: Optional[bool] = None) -> pd.Index: """Executes deferred operations and downloads the results.""" if len(self.column_ids) == 0: raise bigframes.exceptions.NullIndexError( @@ -2517,7 +2517,12 @@ def to_pandas(self) -> pd.Index: index_columns = list(self._block.index_columns) dtypes = dict(zip(index_columns, self.dtypes)) expr = self._expr.select_columns(index_columns) - results, _ = self.session._execute(expr) + results, _ = self.session._execute( + expr, + ordered=ordered + if (ordered is not None) + else self.session._strictly_ordered, + ) df = expr.session._rows_to_dataframe(results, dtypes) df = df.set_index(index_columns) index = df.index diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index f00af6412e5..9fba3081ca5 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -40,7 +40,7 @@ def compile_ordered( ) -> str: """Compile node into sql where rows are sorted with ORDER BY.""" return compiler.compile_ordered_ir(node).to_sql( - col_id_overrides=col_id_overrides, sorted=True + col_id_overrides=col_id_overrides, ordered=True ) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 7b258149e0a..6aeb944343a 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -257,9 +257,9 @@ def to_sql( self, offset_column: typing.Optional[str] = None, col_id_overrides: typing.Mapping[str, str] = {}, - sorted: bool = False, + ordered: bool = False, ) -> str: - if offset_column or sorted: + if offset_column or ordered: raise ValueError("Cannot produce sorted sql in unordered mode") sql = ibis_bigquery.Backend().compile( self._to_ibis_expr( @@ -890,9 +890,9 @@ def _reproject_to_table(self) -> OrderedIR: def to_sql( self, col_id_overrides: typing.Mapping[str, str] = {}, - sorted: bool = False, + ordered: bool = False, ) -> str: - if sorted: + if ordered: # Need to bake ordering expressions into the selected column in order for our ordering clause builder to work. baked_ir = self._bake_ordering() sql = ibis_bigquery.Backend().compile( diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 8df61555919..cfb22929c88 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -481,7 +481,9 @@ def to_pandas(self) -> pandas.Index: pandas.Index: A pandas Index with all of the labels from this Index. """ - return self._block.index.to_pandas() + return self._block.index.to_pandas( + ordered=self._block.session._strictly_ordered + ) def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: return self.to_pandas().to_numpy(dtype, **kwargs) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a27e4abe9aa..75420ca9575 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1189,7 +1189,7 @@ def to_pandas( sampling_method: Optional[str] = None, random_state: Optional[int] = None, *, - ordered: bool = True, + ordered: Optional[bool] = None, ) -> pandas.DataFrame: """Write DataFrame to pandas DataFrame. @@ -1209,9 +1209,10 @@ def to_pandas( The seed for the uniform downsampling algorithm. If provided, the uniform method may take longer to execute and require more computation. If set to a value other than None, this will supersede the global config. - ordered (bool, default True): + ordered (bool, default None): Determines whether the resulting pandas dataframe will be deterministically ordered. - In some cases, unordered may result in a faster-executing query. + In some cases, unordered may result in a faster-executing query. If set to a value + other than None, will override Session default. Returns: pandas.DataFrame: A pandas DataFrame with all rows and columns of this DataFrame if the @@ -1224,7 +1225,7 @@ def to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, - ordered=ordered, + ordered=ordered if ordered is not None else self._session._strictly_ordered, ) self._set_internal_query_job(query_job) return df.set_axis(self._block.column_labels, axis=1, copy=False) @@ -3339,7 +3340,7 @@ def _run_io_query( _, query_job = session._execute( export_array, job_config=job_config, - sorted=False, + ordered=False, col_id_overrides=id_overrides, ) self._set_internal_query_job(query_job) diff --git a/bigframes/series.py b/bigframes/series.py index cb563194719..eda95fa1e8a 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -323,7 +323,7 @@ def to_pandas( sampling_method: Optional[str] = None, random_state: Optional[int] = None, *, - ordered: bool = True, + ordered: Optional[bool] = None, ) -> pandas.Series: """Writes Series to pandas Series. @@ -343,9 +343,10 @@ def to_pandas( The seed for the uniform downsampling algorithm. If provided, the uniform method may take longer to execute and require more computation. If set to a value other than None, this will supersede the global config. - ordered (bool, default True): + ordered (bool, default None): Determines whether the resulting pandas series will be deterministically ordered. - In some cases, unordered may result in a faster-executing query. + In some cases, unordered may result in a faster-executing query. If set to a value + other than None, will override Session default. Returns: @@ -357,7 +358,7 @@ def to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, - ordered=ordered, + ordered=ordered if ordered is not None else self._session._strictly_ordered, ) self._set_internal_query_job(query_job) series = df.squeeze(axis=1) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 78004af0df4..b0b2a3c4187 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -297,7 +297,13 @@ def __init__( self._execution_count = 0 # Whether this session treats objects as totally ordered. # Will expose as feature later, only False for internal testing - self._strictly_ordered = True + self._strictly_ordered: bool = context._strictly_ordered + # Sequential index needs total ordering to generate, so use null index with unstrict ordering. + self._default_index_type: bigframes.enums.DefaultIndexKind = ( + bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 + if context._strictly_ordered + else bigframes.enums.DefaultIndexKind.NULL + ) @property def bqclient(self): @@ -882,11 +888,11 @@ def _read_gbq_table( # Create Default Sequential Index if still have no index # ---------------------------------------------------- - # If no index columns provided or found, fall back to sequential index + # If no index columns provided or found, fall back to session default if (index_col != bigframes.enums.DefaultIndexKind.NULL) and len( index_cols ) == 0: - index_col = bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 + index_col = self._default_index_type index_names: Sequence[Hashable] = index_cols if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: @@ -1935,14 +1941,14 @@ def _execute( array_value: core.ArrayValue, job_config: Optional[bigquery.job.QueryJobConfig] = None, *, - sorted: bool = True, + ordered: bool = True, dry_run=False, col_id_overrides: Mapping[str, str] = {}, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: if not dry_run: self._add_execution(1) sql = self._to_sql( - array_value, sorted=sorted, col_id_overrides=col_id_overrides + array_value, ordered=ordered, col_id_overrides=col_id_overrides ) # type:ignore if job_config is None: job_config = bigquery.QueryJobConfig(dry_run=dry_run) @@ -1977,12 +1983,12 @@ def _to_sql( array_value: core.ArrayValue, offset_column: typing.Optional[str] = None, col_id_overrides: typing.Mapping[str, str] = {}, - sorted: bool = False, + ordered: bool = False, ) -> str: if offset_column: array_value = array_value.promote_offsets(offset_column) node_w_cached = self._with_cached_executions(array_value.node) - if sorted: + if ordered: return bigframes.core.compile.compile_ordered( node_w_cached, col_id_overrides=col_id_overrides ) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 06ad73a7020..a41e6dc6b7a 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -141,11 +141,8 @@ def session() -> Generator[bigframes.Session, None, None]: @pytest.fixture(scope="session") def unordered_session() -> Generator[bigframes.Session, None, None]: - context = bigframes.BigQueryOptions( - location="US", - ) + context = bigframes.BigQueryOptions(location="US", _strictly_ordered=False) session = bigframes.Session(context=context) - session._strictly_ordered = False yield session session.close() # close generated session at cleanup type diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 12c0d6e2597..d555cedcc03 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import pandas as pd +import pyarrow as pa import bigframes.pandas as bpd -from tests.system.utils import assert_pandas_df_equal +from tests.system.utils import assert_pandas_df_equal, skip_legacy_pandas def test_unordered_mode_cache_aggregate(unordered_session): @@ -26,3 +27,35 @@ def test_unordered_mode_cache_aggregate(unordered_session): pd_result = pd_df - pd_df.mean() assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) + + +@skip_legacy_pandas +def test_unordered_mode_read_gbq(unordered_session): + df = unordered_session.read_gbq( + """SELECT + [1, 3, 2] AS array_column, + STRUCT( + "a" AS string_field, + 1.2 AS float_field) AS struct_column""" + ) + expected = pd.DataFrame( + { + "array_column": pd.Series( + [[1, 3, 2]], + dtype=(pd.ArrowDtype(pa.list_(pa.int64()))), + ), + "struct_column": pd.Series( + [{"string_field": "a", "float_field": 1.2}], + dtype=pd.ArrowDtype( + pa.struct( + [ + ("string_field", pa.string()), + ("float_field", pa.float64()), + ] + ) + ), + ), + } + ) + # Don't need ignore_order as there is only 1 row + assert_pandas_df_equal(df.to_pandas(), expected) From 0433a1cff57fddda26b2c57adc0ea71f3fdd3201 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 19 Jun 2024 07:51:49 -0700 Subject: [PATCH 667/921] feat: bigframes.streaming module for continuous queries (#703) * feat: bigframes.streaming module for continuous queries * mypy fix * mypy fix 2 * mypy fix 3 * ignore mypy, error is in bq library * address comments from meeting * fix mypy * don't use app profile * address comments * check job_id * add bigtable setup script * further simplify string * fix bugs * add str() for consistent clarification --- bigframes/streaming/__init__.py | 139 +++++++++++++++++++++++++++ scripts/create_bigtable.py | 76 +++++++++++++++ setup.py | 1 + testing/constraints-3.9.txt | 1 + tests/system/large/test_streaming.py | 48 +++++++++ 5 files changed, 265 insertions(+) create mode 100644 bigframes/streaming/__init__.py create mode 100644 scripts/create_bigtable.py create mode 100644 tests/system/large/test_streaming.py diff --git a/bigframes/streaming/__init__.py b/bigframes/streaming/__init__.py new file mode 100644 index 00000000000..16da677ef5d --- /dev/null +++ b/bigframes/streaming/__init__.py @@ -0,0 +1,139 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for bigquery continuous queries""" + +import json +from typing import Optional + +from google.cloud import bigquery + +import bigframes + + +def to_bigtable( + query: str, + instance: str, + table: str, + bq_client: Optional[bigquery.Client] = None, + app_profile: Optional[str] = None, + truncate: bool = False, + overwrite: bool = False, + auto_create_column_families: bool = False, + bigtable_options: Optional[dict] = None, + job_id: Optional[str] = None, + job_id_prefix: Optional[str] = None, +) -> bigquery.QueryJob: + """Launches a BigQuery continuous query and returns a + QueryJob object for some management functionality. + + This method requires an existing bigtable preconfigured to + accept the continuous query export statement. For instructions + on export to bigtable, see + https://cloud.google.com/bigquery/docs/export-to-bigtable. + + Args: + query (str): + The sql statement to execute as a continuous function. + For example: "SELECT * FROM dataset.table" + This will be wrapped in an EXPORT DATA statement to + launch a continuous query writing to bigtable. + instance (str): + The name of the bigtable instance to export to. + table (str): + The name of the bigtable table to export to. + bq_client (str, default None): + The Client object to use for the query. This determines + the project id and location of the query. If None, will + default to the bigframes global session default client. + app_profile (str, default None): + The bigtable app profile to export to. If None, no app + profile will be used. + truncate (bool, default False): + The export truncate option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + overwrite (bool, default False): + The export overwrite option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + auto_create_column_families (bool, default False): + The auto_create_column_families option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + bigtable_options (dict, default None): + The bigtable options dict, which will be converted to JSON + using json.dumps, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + If None, no bigtable_options parameter will be passed. + job_id (str, default None): + If specified, replace the default job id for the query, + see job_id parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + job_id_prefix (str, default None): + If specified, a job id prefix for the query, see + job_id_prefix parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + + Returns: + google.cloud.bigquery.QueryJob: + See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob + The ongoing query job can be managed using this object. + For example, the job can be cancelled or its error status + can be examined. + """ + # get default client if not passed + if bq_client is None: + bq_client = bigframes.get_global_session().bqclient + + # build export string from parameters + project = bq_client.project + + app_profile_url_string = "" + if app_profile is not None: + app_profile_url_string = f"appProfiles/{app_profile}/" + + bigtable_options_parameter_string = "" + if bigtable_options is not None: + bigtable_options_parameter_string = ( + 'bigtable_options = """' + json.dumps(bigtable_options) + '""",\n' + ) + + sql = ( + "EXPORT DATA\n" + "OPTIONS (\n" + "format = 'CLOUD_BIGTABLE',\n" + f"{bigtable_options_parameter_string}" + f"truncate = {str(truncate)},\n" + f"overwrite = {str(overwrite)},\n" + f"auto_create_column_families = {str(auto_create_column_families)},\n" + f'uri = "https://bigtable.googleapis.com/projects/{project}/instances/{instance}/{app_profile_url_string}tables/{table}"\n' + ")\n" + "AS (\n" + f"{query});" + ) + + # override continuous http parameter + job_config = bigquery.job.QueryJobConfig() + job_config_filled = job_config.from_api_repr({"query": {"continuous": True}}) + + # begin the query job + query_job = bq_client.query( + sql, + job_config=job_config_filled, # type:ignore + # typing error above is in bq client library + # (should accept abstract job_config, only takes concrete) + job_id=job_id, + job_id_prefix=job_id_prefix, + ) + + # return the query job to the user for lifetime management + return query_job diff --git a/scripts/create_bigtable.py b/scripts/create_bigtable.py new file mode 100644 index 00000000000..655e4b31ab2 --- /dev/null +++ b/scripts/create_bigtable.py @@ -0,0 +1,76 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# This script create the bigtable resources required for +# bigframes.streaming testing if they don't already exist + +import os +import pathlib +import sys + +import google.cloud.bigtable as bigtable + +REPO_ROOT = pathlib.Path(__file__).parent.parent + +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") + +if not PROJECT_ID: + print( + "Please set GOOGLE_CLOUD_PROJECT environment variable before running.", + file=sys.stderr, + ) + sys.exit(1) + + +def create_instance(client): + instance_name = "streaming-testing-instance" + instance = bigtable.instance.Instance( + instance_name, + client, + ) + cluster_id = "streaming-testing-instance-c1" + cluster = instance.cluster( + cluster_id, + location_id="us-west1-a", + serve_nodes=1, + ) + if not instance.exists(): + operation = instance.create( + clusters=[cluster], + ) + operation.result(timeout=480) + print(f"Created instance {instance_name}") + return instance + + +def create_table(instance): + table_id = "table-testing" + table = bigtable.table.Table( + table_id, + instance, + ) + if not table.exists(): + table.create() + print(f"Created table {table_id}") + + +def main(): + client = bigtable.Client(project=PROJECT_ID, admin=True) + + instance = create_instance(client) + create_table(instance) + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index d5d282d11a2..dbd9ce5fc27 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ "gcsfs >=2023.3.0", "geopandas >=0.12.2", "google-auth >=2.15.0,<3.0dev", + "google-cloud-bigtable >=2.24.0", "google-cloud-bigquery[bqstorage,pandas] >=3.16.0", "google-cloud-functions >=1.12.0", "google-cloud-bigquery-connection >=1.12.0", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 3c51668655c..bbd7bf0069b 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -4,6 +4,7 @@ fsspec==2023.3.0 gcsfs==2023.3.0 geopandas==0.12.2 google-auth==2.15.0 +google-cloud-bigtable==2.24.0 google-cloud-bigquery==3.16.0 google-cloud-functions==1.12.0 google-cloud-bigquery-connection==1.12.0 diff --git a/tests/system/large/test_streaming.py b/tests/system/large/test_streaming.py new file mode 100644 index 00000000000..48db61e5bf1 --- /dev/null +++ b/tests/system/large/test_streaming.py @@ -0,0 +1,48 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import bigframes.streaming + + +def test_streaming_to_bigtable(): + # launch a continuous query + job_id_prefix = "test_streaming_" + sql = """SELECT + body_mass_g, island as rowkey + FROM birds.penguins""" + query_job = bigframes.streaming.to_bigtable( + sql, + "streaming-testing-instance", + "table-testing", + app_profile=None, + truncate=True, + overwrite=True, + auto_create_column_families=True, + bigtable_options={}, + job_id=None, + job_id_prefix=job_id_prefix, + ) + + try: + # wait 100 seconds in order to ensure the query doesn't stop + # (i.e. it is continuous) + time.sleep(100) + assert query_job.error_result is None + assert query_job.errors is None + assert query_job.running() + assert str(query_job.job_id).startswith(job_id_prefix) + finally: + query_job.cancel() From 1f55da383e3648426cb0820434b47e26d0d9333f Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:41:57 -0700 Subject: [PATCH 668/921] refactor: replace a few functions in sql.py with AST. (#784) * refactor: update Select, update sql.py * update test * update test * update test --- bigframes/core/blocks.py | 7 +- bigframes/core/compile/compiled.py | 8 +- bigframes/core/compile/googlesql/__init__.py | 2 + .../core/compile/googlesql/expression.py | 21 +++-- bigframes/core/compile/googlesql/query.py | 78 +++++++++++++++---- bigframes/core/sql.py | 62 +-------------- bigframes/session/_io/bigquery/__init__.py | 3 +- tests/unit/core/compiler/test_googlesql.py | 59 +++++++++++++- 8 files changed, 151 insertions(+), 89 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 640c070af12..598c32670e4 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -40,6 +40,7 @@ import bigframes.constants import bigframes.constants as constants import bigframes.core as core +import bigframes.core.compile.googlesql as googlesql import bigframes.core.expression as ex import bigframes.core.expression as scalars import bigframes.core.guid as guid @@ -2417,7 +2418,9 @@ def _get_rows_as_json_values(self) -> Block: select_columns = ( [ordering_column_name] + list(self.index_columns) + [row_json_column_name] ) - select_columns_csv = sql.csv([sql.identifier(col) for col in select_columns]) + select_columns_csv = sql.csv( + [googlesql.identifier(col) for col in select_columns] + ) json_sql = f"""\ With T0 AS ( {textwrap.indent(expr_sql, " ")} @@ -2430,7 +2433,7 @@ def _get_rows_as_json_values(self) -> Block: "values", [{column_references_csv}], "indexlength", {index_columns_count}, "dtype", {pandas_row_dtype} - ) AS {sql.identifier(row_json_column_name)} FROM T0 + ) AS {googlesql.identifier(row_json_column_name)} FROM T0 ) SELECT {select_columns_csv} FROM T1 """ diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 6aeb944343a..907c918efd2 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -28,6 +28,7 @@ import pandas import bigframes.core.compile.aggregate_compiler as agg_compiler +import bigframes.core.compile.googlesql import bigframes.core.compile.ibis_types import bigframes.core.compile.scalar_op_compiler as op_compilers import bigframes.core.expression as ex @@ -905,7 +906,12 @@ def to_sql( output_columns = [ col_id_overrides.get(col, col) for col in baked_ir.column_ids ] - sql = bigframes.core.sql.select_from_subquery(output_columns, sql) + sql = ( + bigframes.core.compile.googlesql.Select() + .from_(sql) + .select(output_columns) + .sql() + ) # Single row frames may not have any ordering columns if len(baked_ir._ordering.all_ordering_columns) > 0: diff --git a/bigframes/core/compile/googlesql/__init__.py b/bigframes/core/compile/googlesql/__init__.py index ce94fbb24b1..82b6d3e0910 100644 --- a/bigframes/core/compile/googlesql/__init__.py +++ b/bigframes/core/compile/googlesql/__init__.py @@ -22,6 +22,7 @@ AliasExpression, ColumnExpression, CTEExpression, + identifier, StarExpression, TableExpression, ) @@ -38,6 +39,7 @@ __all__ = [ "_escape_chars", + "identifier", "AliasExpression", "AsAlias", "ColumnExpression", diff --git a/bigframes/core/compile/googlesql/expression.py b/bigframes/core/compile/googlesql/expression.py index d55e4eda85b..581ab67718a 100644 --- a/bigframes/core/compile/googlesql/expression.py +++ b/bigframes/core/compile/googlesql/expression.py @@ -45,8 +45,8 @@ class ColumnExpression(Expression): def sql(self) -> str: if self.parent is not None: - return f"{self.parent.sql()}.`{self.name}`" - return f"`{self.name}`" + return f"{self.parent.sql()}.{identifier(self.name)}" + return identifier(self.name) @dataclasses.dataclass @@ -72,10 +72,10 @@ def __post_init__(self): def sql(self) -> str: text = [] if self.project_id is not None: - text.append(f"`{_escape_chars(self.project_id)}`") + text.append(identifier(self.project_id)) if self.dataset_id is not None: - text.append(f"`{_escape_chars(self.dataset_id)}`") - text.append(f"`{_escape_chars(self.table_id)}`") + text.append(identifier(self.dataset_id)) + text.append(identifier(self.table_id)) return ".".join(text) @@ -84,7 +84,7 @@ class AliasExpression(Expression): alias: str def sql(self) -> str: - return f"`{_escape_chars(self.alias)}`" + return identifier(self.alias) @dataclasses.dataclass @@ -92,7 +92,14 @@ class CTEExpression(Expression): name: str def sql(self) -> str: - return f"`{_escape_chars(self.name)}`" + return identifier(self.name) + + +def identifier(id: str) -> str: + """Return a string representing column reference in a SQL.""" + # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers + # Just always escape, otherwise need to check against every reserved sql keyword + return f"`{_escape_chars(id)}`" def _escape_chars(value: str): diff --git a/bigframes/core/compile/googlesql/query.py b/bigframes/core/compile/googlesql/query.py index adfd4b711d0..dfe21ef7b23 100644 --- a/bigframes/core/compile/googlesql/query.py +++ b/bigframes/core/compile/googlesql/query.py @@ -16,19 +16,19 @@ import dataclasses import typing -from typing import TYPE_CHECKING + +import google.cloud.bigquery as bigquery import bigframes.core.compile.googlesql.abc as abc import bigframes.core.compile.googlesql.expression as expr -if TYPE_CHECKING: - import google.cloud.bigquery as bigquery - """This module provides a structured representation of GoogleSQL syntax using nodes. Each node's name and child nodes are designed to strictly follow the official GoogleSQL syntax rules outlined in the documentation: https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax""" +TABLE_SOURCE_TYPE = typing.Union[str, bigquery.TableReference] + @dataclasses.dataclass class QueryExpr(abc.SQLSyntax): @@ -53,11 +53,47 @@ def sql(self) -> str: class Select(abc.SQLSyntax): """This class represents GoogleSQL `select` syntax.""" - select_list: typing.Sequence[typing.Union[SelectExpression, SelectAll]] - from_clause_list: typing.Sequence[FromClause] = () + select_list: typing.Sequence[ + typing.Union[SelectExpression, SelectAll] + ] = dataclasses.field(default_factory=list) + from_clause_list: typing.Sequence[FromClause] = dataclasses.field( + default_factory=list + ) distinct: bool = False + def select( + self, + columns: typing.Union[typing.Iterable[str], str, None] = None, + distinct: bool = False, + ) -> Select: + if isinstance(columns, str): + columns = [columns] + self.select_list: typing.List[typing.Union[SelectExpression, SelectAll]] = ( + [ + SelectExpression(expression=expr.ColumnExpression(name=column)) + for column in columns + ] + if columns + else [SelectAll(expression=expr.StarExpression())] + ) + self.distinct = distinct + return self + + def from_( + self, + sources: typing.Union[TABLE_SOURCE_TYPE, typing.Iterable[TABLE_SOURCE_TYPE]], + ) -> Select: + if (not isinstance(sources, typing.Iterable)) or isinstance(sources, str): + sources = [sources] + self.from_clause_list = [ + FromClause(FromItem.from_source(source)) for source in sources + ] + return self + def sql(self) -> str: + if (self.select_list is not None) and (not self.select_list): + raise ValueError("Select clause has not been properly initialized.") + text = ["SELECT"] if self.distinct: @@ -66,7 +102,7 @@ def sql(self) -> str: select_list_sql = ",\n".join([select.sql() for select in self.select_list]) text.append(select_list_sql) - if self.from_clause_list is not None: + if self.from_clause_list: from_clauses_sql = ",\n".join( [clause.sql() for clause in self.from_clause_list] ) @@ -118,19 +154,27 @@ class FromItem(abc.SQLSyntax): as_alias: typing.Optional[AsAlias] = None @classmethod - def from_table_ref( + def from_source( cls, - table_ref: bigquery.TableReference, + subquery_or_tableref: typing.Union[bigquery.TableReference, str], as_alias: typing.Optional[AsAlias] = None, ): - return cls( - expression=expr.TableExpression( - table_id=table_ref.table_id, - dataset_id=table_ref.dataset_id, - project_id=table_ref.project, - ), - as_alias=as_alias, - ) + if isinstance(subquery_or_tableref, bigquery.TableReference): + return cls( + expression=expr.TableExpression( + table_id=subquery_or_tableref.table_id, + dataset_id=subquery_or_tableref.dataset_id, + project_id=subquery_or_tableref.project, + ), + as_alias=as_alias, + ) + elif isinstance(subquery_or_tableref, str): + return cls( + expression=subquery_or_tableref, + as_alias=as_alias, + ) + else: + raise ValueError("The source must be bigquery.TableReference or str.") def sql(self) -> str: if isinstance(self.expression, (expr.TableExpression, expr.CTEExpression)): diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index 6926ed910fd..48d3eba2cb7 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -23,10 +23,6 @@ import bigframes.core.compile.googlesql as googlesql -# Literals and identifiers matching this pattern can be unquoted -unquoted = r"^[A-Za-z_][A-Za-z_0-9]*$" - - if TYPE_CHECKING: import google.cloud.bigquery as bigquery @@ -62,23 +58,16 @@ def multi_literal(*values: str): return "(" + ", ".join(literal_strings) + ")" -def identifier(id: str) -> str: - """Return a string representing column reference in a SQL.""" - # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers - # Just always escape, otherwise need to check against every reserved sql keyword - return f"`{googlesql._escape_chars(id)}`" - - def cast_as_string(column_name: str) -> str: """Return a string representing string casting of a column.""" - return f"CAST({identifier(column_name)} AS STRING)" + return f"CAST({googlesql.identifier(column_name)} AS STRING)" def to_json_string(column_name: str) -> str: """Return a string representing JSON version of a column.""" - return f"TO_JSON_STRING({identifier(column_name)})" + return f"TO_JSON_STRING({googlesql.identifier(column_name)})" def csv(values: Iterable[str]) -> str: @@ -91,55 +80,12 @@ def infix_op(opname: str, left_arg: str, right_arg: str): return f"{left_arg} {opname} {right_arg}" -### Writing SELECT expressions -def select_from_subquery(columns: Iterable[str], subquery: str, distinct: bool = False): - select_list = [ - googlesql.SelectExpression(expression=googlesql.ColumnExpression(name=column)) - for column in columns - ] - from_clause_list = [googlesql.FromClause(googlesql.FromItem(expression=subquery))] - - select_expr = googlesql.Select( - select_list=select_list, from_clause_list=from_clause_list, distinct=distinct - ) - return select_expr.sql() - - -def select_from_table_ref( - columns: Iterable[str], table_ref: bigquery.TableReference, distinct: bool = False -): - select_list = [ - googlesql.SelectExpression(expression=googlesql.ColumnExpression(name=column)) - for column in columns - ] - from_clause_list = [ - googlesql.FromClause(googlesql.FromItem.from_table_ref(table_ref)) - ] - - select_expr = googlesql.Select( - select_list=select_list, from_clause_list=from_clause_list, distinct=distinct - ) - return select_expr.sql() - - -def select_table(table_ref: bigquery.TableReference): - select_list = [googlesql.SelectAll(expression=googlesql.StarExpression())] - from_clause_list = [ - googlesql.FromClause(googlesql.FromItem.from_table_ref(table_ref)) - ] - - select_expr = googlesql.Select( - select_list=select_list, from_clause_list=from_clause_list - ) - return select_expr.sql() - - def is_distinct_sql(columns: Iterable[str], table_ref: bigquery.TableReference) -> str: is_unique_sql = f"""WITH full_table AS ( - {select_from_table_ref(columns, table_ref)} + {googlesql.Select().from_(table_ref).select(columns).sql()} ), distinct_table AS ( - {select_from_table_ref(columns, table_ref, distinct=True)} + {googlesql.Select().from_(table_ref).select(columns, distinct=True).sql()} ) SELECT (SELECT COUNT(*) FROM full_table) AS `total_count`, diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index c4d007be220..3a33352a672 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -32,6 +32,7 @@ import bigframes from bigframes.core import log_adapter +import bigframes.core.compile.googlesql as googlesql import bigframes.core.sql import bigframes.formatting_helpers as formatting_helpers @@ -480,7 +481,7 @@ def compile_filters(filters: third_party_pandas_gbq.FiltersType) -> str: operator_str = valid_operators[operator] - column_ref = bigframes.core.sql.identifier(column) + column_ref = googlesql.identifier(column) if operator_str in ["IN", "NOT IN"]: value_literal = bigframes.core.sql.multi_literal(*value) else: diff --git a/tests/unit/core/compiler/test_googlesql.py b/tests/unit/core/compiler/test_googlesql.py index 0430bd348fd..b8d1d024e2b 100644 --- a/tests/unit/core/compiler/test_googlesql.py +++ b/tests/unit/core/compiler/test_googlesql.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import Mock +from unittest.mock import MagicMock +import google.cloud.bigquery as bigquery import pytest import bigframes.core.compile.googlesql as sql @@ -78,12 +79,12 @@ def test_from_item_w_cte(): def test_from_item_w_table_ref(): - mock_table_ref = Mock() + mock_table_ref = MagicMock(spec=bigquery.TableReference) mock_table_ref.table_id = "mock_table" mock_table_ref.dataset_id = "mock_dataset" mock_table_ref.project = "mock_project" - from_item = sql.FromItem.from_table_ref(mock_table_ref) + from_item = sql.FromItem.from_source(mock_table_ref) assert from_item.sql() == "`mock_project`.`mock_dataset`.`mock_table`" @@ -122,6 +123,54 @@ def test_select(): assert expr.sql() == expected +@pytest.mark.parametrize( + "columns, source, expected", + [ + ( + ["a", "b", "c"], + "select * from test", + "SELECT\nDISTINCT\n`a`,\n`b`,\n`c`\nFROM\n(select * from test)", + ), + ( + "a", + "select * from test", + "SELECT\nDISTINCT\n`a`\nFROM\n(select * from test)", + ), + ], +) +def test_select_from_str(columns, source, expected): + expr = sql.Select().from_(source).select(columns, distinct=True) + assert expr.sql() == expected + + +@pytest.mark.parametrize( + ("columns", "distinct", "expected"), + [ + pytest.param( + ["a", "b", "c"], + True, + "SELECT\nDISTINCT\n`a`,\n`b`,\n`c`\nFROM\n`mock_project`.`mock_dataset`.`mock_table`", + ), + pytest.param( + None, + True, + "SELECT\nDISTINCT\n*\nFROM\n`mock_project`.`mock_dataset`.`mock_table`", + ), + pytest.param( + None, False, "SELECT\n*\nFROM\n`mock_project`.`mock_dataset`.`mock_table`" + ), + ], +) +def test_select_from_table_ref(columns, distinct, expected): + mock_table_ref = MagicMock(spec=bigquery.TableReference) + mock_table_ref.table_id = "mock_table" + mock_table_ref.dataset_id = "mock_dataset" + mock_table_ref.project = "mock_project" + + expr = sql.Select().from_(mock_table_ref).select(columns, distinct=distinct) + assert expr.sql() == expected + + def test_query_expr_w_cte(): # Test a simple SELECT query. from_clause1 = sql.FromClause( @@ -166,5 +215,9 @@ def test_query_expr_w_cte(): assert query2.sql() == query2_sql +def test_identifier(): + assert sql.identifier("\aa") == r"`\aa`" + + def test_escape_chars(): assert sql._escape_chars("\a\b\f\n\r\t\v\\?'\"`") == r"\a\b\f\n\r\t\v\\\?\'\"\`" From 2b38236759a3b505e7d7d7400223b60f18167bbb Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:47:38 -0700 Subject: [PATCH 669/921] chore: add dbbenchmark grouby benchmark q8 (#796) --- .../db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py new file mode 100644 index 00000000000..4bbad0048f2 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py @@ -0,0 +1,19 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 8: largest two v3 by id6") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = ( + x[~x["v3"].isna()][["id6", "v3"]] + .sort_values("v3", ascending=False) + .groupby("id6", as_index=False, dropna=False) + .head(2) +) +print(ans.shape) +chk = [ans["v3"].sum()] +print(chk) + +bpd.reset_session() From 2ebb6188aa3b43abaec1683935e1db9ab5e00514 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 20 Jun 2024 15:13:23 -0700 Subject: [PATCH 670/921] refactor: add googlesql cast function (#794) --- bigframes/core/compile/googlesql/__init__.py | 5 +++ bigframes/core/compile/googlesql/datatype.py | 23 ++++++++++++ bigframes/core/compile/googlesql/function.py | 32 ++++++++++++++++ bigframes/core/sql.py | 4 +- .../core/{compiler => compile}/__init__.py | 0 tests/unit/core/compile/googlesql/__init__.py | 13 +++++++ .../core/compile/googlesql/test_expression.py | 37 +++++++++++++++++++ .../core/compile/googlesql/test_function.py | 21 +++++++++++ .../googlesql/test_query.py} | 0 9 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 bigframes/core/compile/googlesql/datatype.py create mode 100644 bigframes/core/compile/googlesql/function.py rename tests/unit/core/{compiler => compile}/__init__.py (100%) create mode 100644 tests/unit/core/compile/googlesql/__init__.py create mode 100644 tests/unit/core/compile/googlesql/test_expression.py create mode 100644 tests/unit/core/compile/googlesql/test_function.py rename tests/unit/core/{compiler/test_googlesql.py => compile/googlesql/test_query.py} (100%) diff --git a/bigframes/core/compile/googlesql/__init__.py b/bigframes/core/compile/googlesql/__init__.py index 82b6d3e0910..add0c5ec445 100644 --- a/bigframes/core/compile/googlesql/__init__.py +++ b/bigframes/core/compile/googlesql/__init__.py @@ -17,6 +17,7 @@ from __future__ import annotations +from bigframes.core.compile.googlesql.datatype import DataType from bigframes.core.compile.googlesql.expression import ( _escape_chars, AliasExpression, @@ -26,6 +27,7 @@ StarExpression, TableExpression, ) +from bigframes.core.compile.googlesql.function import Cast from bigframes.core.compile.googlesql.query import ( AsAlias, FromClause, @@ -42,8 +44,10 @@ "identifier", "AliasExpression", "AsAlias", + "Cast", "ColumnExpression", "CTEExpression", + "DataType", "FromClause", "FromItem", "NonRecursiveCTE", @@ -52,5 +56,6 @@ "SelectAll", "SelectExpression", "StarExpression", + "StringType", "TableExpression", ] diff --git a/bigframes/core/compile/googlesql/datatype.py b/bigframes/core/compile/googlesql/datatype.py new file mode 100644 index 00000000000..ccf3ff4d41b --- /dev/null +++ b/bigframes/core/compile/googlesql/datatype.py @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum + +"""This module represents all GoogleSQL for BigQuery data types: +https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types""" + + +class DataType(enum.Enum): + STRING = 1 + FLOAT64 = 2 diff --git a/bigframes/core/compile/googlesql/function.py b/bigframes/core/compile/googlesql/function.py new file mode 100644 index 00000000000..19b61f2fc99 --- /dev/null +++ b/bigframes/core/compile/googlesql/function.py @@ -0,0 +1,32 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses + +import bigframes.core.compile.googlesql.datatype as datatype +import bigframes.core.compile.googlesql.expression as expr + +# Conversion functions: +# https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions + + +@dataclasses.dataclass +class Cast(expr.Expression): + """This class represents the `cast` function.""" + + expression: expr.ColumnExpression + type: datatype.DataType + + def sql(self) -> str: + return f"CAST ({self.expression.sql()} AS {self.type.name})" diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index 48d3eba2cb7..528c9bcc748 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -61,7 +61,9 @@ def multi_literal(*values: str): def cast_as_string(column_name: str) -> str: """Return a string representing string casting of a column.""" - return f"CAST({googlesql.identifier(column_name)} AS STRING)" + return googlesql.Cast( + googlesql.ColumnExpression(column_name), googlesql.DataType.STRING + ).sql() def to_json_string(column_name: str) -> str: diff --git a/tests/unit/core/compiler/__init__.py b/tests/unit/core/compile/__init__.py similarity index 100% rename from tests/unit/core/compiler/__init__.py rename to tests/unit/core/compile/__init__.py diff --git a/tests/unit/core/compile/googlesql/__init__.py b/tests/unit/core/compile/googlesql/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/tests/unit/core/compile/googlesql/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/core/compile/googlesql/test_expression.py b/tests/unit/core/compile/googlesql/test_expression.py new file mode 100644 index 00000000000..e72598b1760 --- /dev/null +++ b/tests/unit/core/compile/googlesql/test_expression.py @@ -0,0 +1,37 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.core.compile.googlesql as sql + + +@pytest.mark.parametrize( + ("table_id", "dataset_id", "project_id", "expected"), + [ + pytest.param("a", None, None, "`a`"), + pytest.param("a", "b", None, "`b`.`a`"), + pytest.param("a", "b", "c", "`c`.`b`.`a`"), + pytest.param("a", None, "c", None, marks=pytest.mark.xfail(raises=ValueError)), + ], +) +def test_table_expression(table_id, dataset_id, project_id, expected): + expr = sql.TableExpression( + table_id=table_id, dataset_id=dataset_id, project_id=project_id + ) + assert expr.sql() == expected + + +def test_escape_chars(): + assert sql._escape_chars("\a\b\f\n\r\t\v\\?'\"`") == r"\a\b\f\n\r\t\v\\\?\'\"\`" diff --git a/tests/unit/core/compile/googlesql/test_function.py b/tests/unit/core/compile/googlesql/test_function.py new file mode 100644 index 00000000000..4edfda6f345 --- /dev/null +++ b/tests/unit/core/compile/googlesql/test_function.py @@ -0,0 +1,21 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.core.compile.googlesql as sql + + +def test_cast(): + col = sql.ColumnExpression("col") + assert sql.Cast(col, sql.DataType.STRING).sql() == "CAST (`col` AS STRING)" + assert sql.Cast(col, sql.DataType.FLOAT64).sql() == "CAST (`col` AS FLOAT64)" diff --git a/tests/unit/core/compiler/test_googlesql.py b/tests/unit/core/compile/googlesql/test_query.py similarity index 100% rename from tests/unit/core/compiler/test_googlesql.py rename to tests/unit/core/compile/googlesql/test_query.py From 1b96b8027a550e1601a5360f2af35d24a8806da9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 20 Jun 2024 17:00:48 -0700 Subject: [PATCH 671/921] fix: Self-join optimization doesn't needlessly invalidate caching (#797) --- bigframes/core/__init__.py | 12 +-- bigframes/core/rewrite.py | 108 +++++++++++++++++++-------- tests/system/small/test_dataframe.py | 12 +++ 3 files changed, 91 insertions(+), 41 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 25088148948..89ef5f525ed 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -505,11 +505,11 @@ def try_align_as_projection( join_type: join_def.JoinType, mappings: typing.Tuple[join_def.JoinColumnMapping, ...], ) -> typing.Optional[ArrayValue]: - left_side = bigframes.core.rewrite.SquashedSelect.from_node(self.node) - right_side = bigframes.core.rewrite.SquashedSelect.from_node(other.node) - result = left_side.maybe_merge(right_side, join_type, mappings) + result = bigframes.core.rewrite.join_as_projection( + self.node, other.node, mappings, join_type + ) if result is not None: - return ArrayValue(result.expand()) + return ArrayValue(result) return None def explode(self, column_ids: typing.Sequence[str]) -> ArrayValue: @@ -528,7 +528,3 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: The row numbers of result is non-deterministic, avoid to use. """ return ArrayValue(nodes.RandomSampleNode(self.node, fraction)) - - def merge_projections(self) -> ArrayValue: - new_node = bigframes.core.rewrite.maybe_squash_projection(self.node) - return ArrayValue(new_node) diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index 15999c0558d..101d5cc8820 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -26,10 +26,17 @@ Selection = Tuple[Tuple[scalar_exprs.Expression, str], ...] +REWRITABLE_NODE_TYPES = ( + nodes.ProjectionNode, + nodes.FilterNode, + nodes.ReversedNode, + nodes.OrderByNode, +) + @dataclasses.dataclass(frozen=True) class SquashedSelect: - """Squash together as many nodes as possible, separating out the projection, filter and reordering expressions.""" + """Squash nodes together until target node, separating out the projection, filter and reordering expressions.""" root: nodes.BigFrameNode columns: Tuple[Tuple[scalar_exprs.Expression, str], ...] @@ -38,25 +45,25 @@ class SquashedSelect: reverse_root: bool = False @classmethod - def from_node( - cls, node: nodes.BigFrameNode, projections_only: bool = False + def from_node_span( + cls, node: nodes.BigFrameNode, target: nodes.BigFrameNode ) -> SquashedSelect: - if isinstance(node, nodes.ProjectionNode): - return cls.from_node(node.child, projections_only=projections_only).project( - node.assignments - ) - elif not projections_only and isinstance(node, nodes.FilterNode): - return cls.from_node(node.child).filter(node.predicate) - elif not projections_only and isinstance(node, nodes.ReversedNode): - return cls.from_node(node.child).reverse() - elif not projections_only and isinstance(node, nodes.OrderByNode): - return cls.from_node(node.child).order_with(node.by) - else: + if node == target: selection = tuple( (scalar_exprs.UnboundVariableExpression(id), id) for id in get_node_column_ids(node) ) return cls(node, selection, None, ()) + if isinstance(node, nodes.ProjectionNode): + return cls.from_node_span(node.child, target).project(node.assignments) + elif isinstance(node, nodes.FilterNode): + return cls.from_node_span(node.child, target).filter(node.predicate) + elif isinstance(node, nodes.ReversedNode): + return cls.from_node_span(node.child, target).reverse() + elif isinstance(node, nodes.OrderByNode): + return cls.from_node_span(node.child, target).order_with(node.by) + else: + raise ValueError(f"Cannot rewrite node {node}") @property def column_lookup(self) -> Mapping[str, scalar_exprs.Expression]: @@ -98,9 +105,10 @@ def order_with(self, by: Tuple[order.OrderingExpression, ...]): self.root, self.columns, self.predicate, new_ordering, self.reverse_root ) - def can_join( + def can_merge( self, right: SquashedSelect, join_def: join_defs.JoinDefinition ) -> bool: + """Determines whether the two selections can be merged into a single selection.""" if join_def.type == "cross": # Cannot convert cross join to projection return False @@ -116,14 +124,14 @@ def can_join( return False return True - def maybe_merge( + def merge( self, right: SquashedSelect, join_type: join_defs.JoinType, mappings: Tuple[join_defs.JoinColumnMapping, ...], - ) -> Optional[SquashedSelect]: + ) -> SquashedSelect: if self.root != right.root: - return None + raise ValueError("Cannot merge expressions with different roots") # Mask columns and remap names to expected schema lselection = self.columns rselection = right.columns @@ -196,28 +204,40 @@ def expand(self) -> nodes.BigFrameNode: return nodes.ProjectionNode(child=root, assignments=self.columns) -def maybe_squash_projection(node: nodes.BigFrameNode) -> nodes.BigFrameNode: - if isinstance(node, nodes.ProjectionNode) and isinstance( - node.child, nodes.ProjectionNode - ): - # Conservative approach, only squash consecutive projections, even though could also squash filters, reorderings - return SquashedSelect.from_node(node, projections_only=True).expand() - return node - - def maybe_rewrite_join(join_node: nodes.JoinNode) -> nodes.BigFrameNode: - left_side = SquashedSelect.from_node(join_node.left_child) - right_side = SquashedSelect.from_node(join_node.right_child) - if left_side.can_join(right_side, join_node.join): - merged = left_side.maybe_merge( + rewrite_common_node = common_selection_root( + join_node.left_child, join_node.right_child + ) + if rewrite_common_node is None: + return join_node + left_side = SquashedSelect.from_node_span(join_node.left_child, rewrite_common_node) + right_side = SquashedSelect.from_node_span( + join_node.right_child, rewrite_common_node + ) + if left_side.can_merge(right_side, join_node.join): + return left_side.merge( right_side, join_node.join.type, join_node.join.mappings - ) + ).expand() + return join_node + + +def join_as_projection( + l_node: nodes.BigFrameNode, + r_node: nodes.BigFrameNode, + mappings: Tuple[join_defs.JoinColumnMapping, ...], + how: join_defs.JoinType, +) -> Optional[nodes.BigFrameNode]: + rewrite_common_node = common_selection_root(l_node, r_node) + if rewrite_common_node is not None: + left_side = SquashedSelect.from_node_span(l_node, rewrite_common_node) + right_side = SquashedSelect.from_node_span(r_node, rewrite_common_node) + merged = left_side.merge(right_side, how, mappings) assert ( merged is not None ), "Couldn't merge nodes. This shouldn't happen. Please share full stacktrace with the BigQuery DataFrames team at bigframes-feedback@google.com." return merged.expand() else: - return join_node + return None def remap_names( @@ -311,3 +331,25 @@ def get_node_column_ids(node: nodes.BigFrameNode) -> Tuple[str, ...]: import bigframes.core return tuple(bigframes.core.ArrayValue(node).column_ids) + + +def common_selection_root( + l_tree: nodes.BigFrameNode, r_tree: nodes.BigFrameNode +) -> Optional[nodes.BigFrameNode]: + """Find common subtree between join subtrees""" + l_node = l_tree + l_nodes: set[nodes.BigFrameNode] = set() + while isinstance(l_node, REWRITABLE_NODE_TYPES): + l_nodes.add(l_node) + l_node = l_node.child + l_nodes.add(l_node) + + r_node = r_tree + while isinstance(r_node, REWRITABLE_NODE_TYPES): + if r_node in l_nodes: + return r_node + r_node = r_node.child + + if r_node in l_nodes: + return r_node + return None diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 782ef2d5eaf..0aac9e2578c 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4328,6 +4328,18 @@ def test_df_cached(scalars_df_index): pandas.testing.assert_frame_equal(df.to_pandas(), df_cached_copy.to_pandas()) +def test_df_cache_with_implicit_join(scalars_df_index): + """expectation is that cache will be used, but no explicit join will be performed""" + df = scalars_df_index[["int64_col", "int64_too"]].sort_index().reset_index() + 3 + df.cache() + bf_result = df + (df * 2) + sql = bf_result.sql + + # Very crude asserts, want sql to not use join and not use base table, only reference cached table + assert "JOIN" not in sql + assert "bigframes_testing" not in sql + + def test_df_dot_inline(session): df1 = pd.DataFrame([[1, 2, 3], [2, 5, 7]]) df2 = pd.DataFrame([[2, 4, 8], [1, 5, 10], [3, 6, 9]]) From b4fbb518711922c09ac6f55f3b8f6ab57c89114b Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:50:55 -0700 Subject: [PATCH 672/921] feat: add ml.preprocessing.PolynomialFeatures class (#793) --- bigframes/ml/preprocessing.py | 104 ++++++++++++++++++ bigframes/ml/sql.py | 12 ++ tests/system/small/ml/test_preprocessing.py | 67 +++++++++++ tests/unit/ml/test_sql.py | 7 ++ .../sklearn/preprocessing/_polynomial.py | 38 +++++++ 5 files changed, 228 insertions(+) create mode 100644 third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 4b1a3fb7b73..f3621d3a33b 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -24,6 +24,7 @@ import bigframes_vendored.sklearn.preprocessing._discretization import bigframes_vendored.sklearn.preprocessing._encoder import bigframes_vendored.sklearn.preprocessing._label +import bigframes_vendored.sklearn.preprocessing._polynomial from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils @@ -661,6 +662,109 @@ def transform(self, y: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +@log_adapter.class_logger +class PolynomialFeatures( + base.Transformer, + bigframes_vendored.sklearn.preprocessing._polynomial.PolynomialFeatures, +): + __doc__ = ( + bigframes_vendored.sklearn.preprocessing._polynomial.PolynomialFeatures.__doc__ + ) + + def __init__(self, degree: int = 2): + self.degree = degree + self._bqml_model: Optional[core.BqmlModel] = None + self._bqml_model_factory = globals.bqml_model_factory() + self._base_sql_generator = globals.base_sql_generator() + + # TODO(garrettwu): implement __hash__ + def __eq__(self, other: Any) -> bool: + return ( + type(other) is PolynomialFeatures and self._bqml_model == other._bqml_model + ) + + def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: + """Compile this transformer to a list of SQL expressions that can be included in + a BQML TRANSFORM clause + + Args: + columns: + a list of column names to transform. + X (default None): + Ignored. + + Returns: a list of tuples of (sql_expression, output_name)""" + output_name = "poly_feat" + return [ + ( + self._base_sql_generator.ml_polynomial_expand( + columns, self.degree, output_name + ), + output_name, + ) + ] + + @classmethod + def _parse_from_sql(cls, sql: str) -> tuple[PolynomialFeatures, str]: + """Parse SQL to tuple(PolynomialFeatures, column_label). + + Args: + sql: SQL string of format "ML.POLYNOMIAL_EXPAND(STRUCT(col_label0, col_label1, ...), degree)" + + Returns: + tuple(MaxAbsScaler, column_label)""" + col_label = sql[sql.find("STRUCT(") + 7 : sql.find(")")] + degree = int(sql[sql.rfind(",") + 1 : sql.rfind(")")]) + return cls(degree), col_label + + def fit( + self, + X: Union[bpd.DataFrame, bpd.Series], + y=None, # ignored + ) -> PolynomialFeatures: + (X,) = utils.convert_to_dataframe(X) + + compiled_transforms = self._compile_to_sql(X.columns.tolist()) + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] + + self._bqml_model = self._bqml_model_factory.create_model( + X, + options={"model_type": "transform_only"}, + transforms=transform_sqls, + ) + + # TODO(garrettwu): generalize the approach to other transformers + output_names = [] + for transform_col in self._bqml_model._model._properties["transformColumns"]: + transform_col_dict = cast(dict, transform_col) + # pass the columns that are not transformed + if "transformSql" not in transform_col_dict: + continue + transform_sql: str = transform_col_dict["transformSql"] + if not transform_sql.startswith("ML."): + continue + + output_names.append(transform_col_dict["name"]) + + self._output_names = output_names + + return self + + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + if not self._bqml_model: + raise RuntimeError("Must be fitted before transform") + + (X,) = utils.convert_to_dataframe(X) + + df = self._bqml_model.transform(X) + return typing.cast( + bpd.DataFrame, + df[self._output_names], + ) + + # TODO(garrettwu): to_gbq() + + PreprocessingType = Union[ OneHotEncoder, StandardScaler, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index f060584a115..0399db3a10f 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -73,6 +73,11 @@ def struct_options(self, **kwargs: Union[int, float]) -> str: """Encode a BQ STRUCT as options.""" return f"STRUCT({self.build_structs(**kwargs)})" + def struct_columns(self, columns: Iterable[str]) -> str: + """Encode a BQ Table columns to a STRUCT.""" + columns_str = ", ".join(columns) + return f"STRUCT({columns_str})" + def input(self, **kwargs: str) -> str: """Encode a BQML INPUT clause.""" return f"INPUT({self.build_schema(**kwargs)})" @@ -153,6 +158,13 @@ def ml_label_encoder( https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-label-encoder for params.""" return f"""ML.LABEL_ENCODER({numeric_expr_sql}, {top_k}, {frequency_threshold}) OVER() AS {name}""" + def ml_polynomial_expand( + self, columns: Iterable[str], degree: int, name: str + ) -> str: + """Encode ML.POLYNOMIAL_EXPAND. + https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-polynomial-expand""" + return f"""ML.POLYNOMIAL_EXPAND({self.struct_columns(columns)}, {degree}) AS {name}""" + def ml_distance( self, col_x: str, diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 5b457cc9c08..73b1855e092 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -19,6 +19,7 @@ import bigframes.features from bigframes.ml import preprocessing +from tests.system import utils ONE_HOT_ENCODED_DTYPE = ( pd.ArrowDtype(pa.list_(pa.struct([("index", pa.int64()), ("value", pa.float64())]))) @@ -840,3 +841,69 @@ def test_label_encoder_save_load(new_penguins_df, dataset_id): # TODO(garrettwu): add OneHotEncoder tests to compare with sklearn. + + +def test_poly_features_default_params(new_penguins_df): + transformer = preprocessing.PolynomialFeatures() + df = new_penguins_df[["culmen_length_mm", "culmen_depth_mm"]] + transformer.fit(df) + + result = transformer.transform(df).to_pandas() + + expected = pd.DataFrame( + { + "poly_feat_culmen_length_mm": [ + 39.5, + 38.5, + 37.9, + ], + "poly_feat_culmen_length_mm_culmen_length_mm": [ + 1560.25, + 1482.25, + 1436.41, + ], + "poly_feat_culmen_length_mm_culmen_depth_mm": [ + 742.6, + 662.2, + 685.99, + ], + "poly_feat_culmen_depth_mm": [ + 18.8, + 17.2, + 18.1, + ], + "poly_feat_culmen_depth_mm_culmen_depth_mm": [ + 353.44, + 295.84, + 327.61, + ], + }, + dtype="Float64", + index=pd.Index([1633, 1672, 1690], name="tag_number", dtype="Int64"), + ) + + pd.testing.assert_frame_equal(result, expected, check_exact=False, rtol=0.1) + + +def test_poly_features_params(new_penguins_df): + transformer = preprocessing.PolynomialFeatures(degree=3) + df = new_penguins_df[["culmen_length_mm", "culmen_depth_mm"]] + transformer.fit(df) + + result = transformer.transform(df).to_pandas() + + utils.check_pandas_df_schema_and_index( + result, + [ + "poly_feat_culmen_length_mm", + "poly_feat_culmen_length_mm_culmen_length_mm", + "poly_feat_culmen_length_mm_culmen_length_mm_culmen_length_mm", + "poly_feat_culmen_length_mm_culmen_length_mm_culmen_depth_mm", + "poly_feat_culmen_length_mm_culmen_depth_mm", + "poly_feat_culmen_length_mm_culmen_depth_mm_culmen_depth_mm", + "poly_feat_culmen_depth_mm", + "poly_feat_culmen_depth_mm_culmen_depth_mm", + "poly_feat_culmen_depth_mm_culmen_depth_mm_culmen_depth_mm", + ], + [1633, 1672, 1690], + ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index 01f173812c1..e90146565dc 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -145,6 +145,13 @@ def test_label_encoder_correct( assert sql == "ML.LABEL_ENCODER(col_a, 1000000, 0) OVER() AS encoded_col_a" +def test_polynomial_expand( + base_sql_generator: ml_sql.BaseSqlGenerator, +): + sql = base_sql_generator.ml_polynomial_expand(["col_a", "col_b"], 2, "poly_exp") + assert sql == "ML.POLYNOMIAL_EXPAND(STRUCT(col_a, col_b), 2) AS poly_exp" + + def test_distance_correct( base_sql_generator: ml_sql.BaseSqlGenerator, mock_df: bpd.DataFrame, diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py b/third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py new file mode 100644 index 00000000000..4e4624ba847 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py @@ -0,0 +1,38 @@ +""" +This file contains preprocessing tools based on polynomials. +""" + +from bigframes_vendored.sklearn.base import BaseEstimator, TransformerMixin + +from bigframes import constants + + +class PolynomialFeatures(TransformerMixin, BaseEstimator): + """Generate polynomial and interaction features.""" + + def fit(self, X, y=None): + """Compute number of output features. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The Dataframe or Series with training data. + + y (default None): + Ignored. + + Returns: + PolynomialFeatures: Fitted transformer. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def transform(self, X): + """Transform data to polynomial features. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The DataFrame or Series to be transformed. + + Returns: + bigframes.dataframe.DataFrame: Transformed result. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 2e692e92641152a4cf12b2af48dde08186e216b5 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:08:13 -0700 Subject: [PATCH 673/921] chore(main): release 1.10.0 (#776) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d585b5b1c27..633f9930eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.10.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.9.0...v1.10.0) (2024-06-21) + + +### Features + +* Add dataframe.insert ([#770](https://github.com/googleapis/python-bigquery-dataframes/issues/770)) ([e8bab68](https://github.com/googleapis/python-bigquery-dataframes/commit/e8bab681a2d07636e5809e804f4fd81b0d582685)) +* Add groupby head API ([#791](https://github.com/googleapis/python-bigquery-dataframes/issues/791)) ([44202bc](https://github.com/googleapis/python-bigquery-dataframes/commit/44202bc3541df03154ea0b2cca8eac18094a91a9)) +* Add ml.preprocessing.PolynomialFeatures class ([#793](https://github.com/googleapis/python-bigquery-dataframes/issues/793)) ([b4fbb51](https://github.com/googleapis/python-bigquery-dataframes/commit/b4fbb518711922c09ac6f55f3b8f6ab57c89114b)) +* Bigframes.streaming module for continuous queries ([#703](https://github.com/googleapis/python-bigquery-dataframes/issues/703)) ([0433a1c](https://github.com/googleapis/python-bigquery-dataframes/commit/0433a1cff57fddda26b2c57adc0ea71f3fdd3201)) +* Include index columns in DataFrame.sql if they are named ([#788](https://github.com/googleapis/python-bigquery-dataframes/issues/788)) ([c8d16c0](https://github.com/googleapis/python-bigquery-dataframes/commit/c8d16c0f72a25bce854b80be517114e1603c947e)) + + +### Bug Fixes + +* Allow `__repr__` to work with uninitialed DataFrame/Series/Index ([#778](https://github.com/googleapis/python-bigquery-dataframes/issues/778)) ([e14c7a9](https://github.com/googleapis/python-bigquery-dataframes/commit/e14c7a9e7a9cb8847e0382b135fc06c7b82b872a)) +* Df.loc with the 2nd input as bigframes boolean Series ([#789](https://github.com/googleapis/python-bigquery-dataframes/issues/789)) ([a4ac82e](https://github.com/googleapis/python-bigquery-dataframes/commit/a4ac82e06221581ddfcfc1246a3e3cd65a8bb00e)) +* Ensure numpy version matches in `remote_function` deployment ([#798](https://github.com/googleapis/python-bigquery-dataframes/issues/798)) ([324d93c](https://github.com/googleapis/python-bigquery-dataframes/commit/324d93cb31191520b790bbbc501468b8d1d8467d)) +* Fix temp table creation retries by now throwing if table already exists. ([#787](https://github.com/googleapis/python-bigquery-dataframes/issues/787)) ([0e57d1f](https://github.com/googleapis/python-bigquery-dataframes/commit/0e57d1f1f8a150ba6faac5f667bb5b4c78f4c0a3)) +* Self-join optimization doesn't needlessly invalidate caching ([#797](https://github.com/googleapis/python-bigquery-dataframes/issues/797)) ([1b96b80](https://github.com/googleapis/python-bigquery-dataframes/commit/1b96b8027a550e1601a5360f2af35d24a8806da9)) + ## [1.9.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.8.0...v1.9.0) (2024-06-10) diff --git a/bigframes/version.py b/bigframes/version.py index 56a12008579..014b0640714 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.9.0" +__version__ = "1.10.0" From 4b8fc15ec2c126566269f84d75289198fee2c655 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 25 Jun 2024 23:50:42 +0000 Subject: [PATCH 674/921] fix: include internally required packages in `remote_function` hash (#799) * fix: include internally required packages in `remote_function` id computation * refactor to keep the tests supported --- bigframes/functions/remote_function.py | 55 +++++++---- tests/system/large/test_remote_function.py | 2 +- tests/system/small/test_remote_function.py | 103 +++++++++++++++++++++ 3 files changed, 140 insertions(+), 20 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 920dc7c039a..bc352322efa 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -102,6 +102,24 @@ def _get_hash(def_, package_requirements=None): return hashlib.md5(def_repr).hexdigest() +def _get_updated_package_requirements(package_requirements, is_row_processor): + requirements = [f"cloudpickle=={cloudpickle.__version__}"] + if is_row_processor: + # bigframes remote function will send an entire row of data as json, + # which would be converted to a pandas series and processed + # Ensure numpy versions match to avoid unpickling problems. See + # internal issue b/347934471. + requirements.append(f"numpy=={numpy.__version__}") + requirements.append(f"pandas=={pandas.__version__}") + requirements.append(f"pyarrow=={pyarrow.__version__}") + + if package_requirements: + requirements.extend(package_requirements) + + requirements = sorted(requirements) + return requirements + + def routine_ref_to_string_for_query(routine_ref: bigquery.RoutineReference) -> str: return f"`{routine_ref.project}.{routine_ref.dataset_id}`.{routine_ref.routine_id}" @@ -112,13 +130,22 @@ class IbisSignature(NamedTuple): output_type: IbisDataType -def get_cloud_function_name(def_, uniq_suffix=None, package_requirements=None): +def get_cloud_function_name( + def_, uniq_suffix=None, package_requirements=None, is_row_processor=False +): "Get a name for the cloud function for the given user defined function." + + # Augment user package requirements with any internal package + # requirements + package_requirements = _get_updated_package_requirements( + package_requirements, is_row_processor + ) + cf_name = _get_hash(def_, package_requirements) cf_name = f"bigframes-{cf_name}" # for identification if uniq_suffix: cf_name = f"{cf_name}-{uniq_suffix}" - return cf_name + return cf_name, package_requirements def get_remote_function_name(def_, uniq_suffix=None, package_requirements=None): @@ -277,21 +304,10 @@ def generate_cloud_function_code( """ # requirements.txt - requirements = ["cloudpickle >= 2.1.0"] - if is_row_processor: - # bigframes remote function will send an entire row of data as json, - # which would be converted to a pandas series and processed - # Ensure numpy versions match to avoid unpickling problems. See - # internal issue b/347934471. - requirements.append(f"numpy=={numpy.__version__}") - requirements.append(f"pandas=={pandas.__version__}") - requirements.append(f"pyarrow=={pyarrow.__version__}") if package_requirements: - requirements.extend(package_requirements) - requirements = sorted(requirements) - requirements_txt = os.path.join(directory, "requirements.txt") - with open(requirements_txt, "w") as f: - f.write("\n".join(requirements)) + requirements_txt = os.path.join(directory, "requirements.txt") + with open(requirements_txt, "w") as f: + f.write("\n".join(package_requirements)) # main.py entry_point = bigframes.functions.remote_function_template.generate_cloud_function_main_code( @@ -469,9 +485,10 @@ def provision_bq_remote_function( ) # Derive the name of the cloud function underlying the intended BQ - # remote function - cloud_function_name = get_cloud_function_name( - def_, uniq_suffix, package_requirements + # remote function, also collect updated package requirements as + # determined in the name resolution + cloud_function_name, package_requirements = get_cloud_function_name( + def_, uniq_suffix, package_requirements, is_row_processor ) cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 6bfc9f0da3d..3f4bfea97e5 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -590,7 +590,7 @@ def add_one(x): add_one_uniq, add_one_uniq_dir = make_uniq_udf(add_one) # Expected cloud function name for the unique udf - add_one_uniq_cf_name = get_cloud_function_name(add_one_uniq) + add_one_uniq_cf_name, _ = get_cloud_function_name(add_one_uniq) # There should be no cloud function yet for the unique udf cloud_functions = list( diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 5838ad75b00..d84d520988a 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -742,6 +742,109 @@ def test_read_gbq_function_enforces_explicit_types( ) +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1(session, scalars_dfs): + columns = [ + "bool_col", + "int64_col", + "int64_too", + "float64_col", + "string_col", + "bytes_col", + ] + scalars_df, scalars_pandas_df = scalars_dfs + + def add_ints(row): + return row["int64_col"] + row["int64_too"] + + with pytest.warns( + bigframes.exceptions.PreviewWarning, + match="input_types=Series is in preview.", + ): + add_ints_remote = session.remote_function( + bigframes.series.Series, + int, + )(add_ints) + + with pytest.warns( + bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." + ): + bf_result = scalars_df[columns].apply(add_ints_remote, axis=1).to_pandas() + + pd_result = scalars_pandas_df[columns].apply(add_ints, axis=1) + + # bf_result.dtype is 'Int64' while pd_result.dtype is 'object', ignore this + # mismatch by using check_dtype=False. + # + # bf_result.to_numpy() produces an array of numpy.float64's + # (in system_prerelease tests), while pd_result.to_numpy() produces an + # array of ints, ignore this mismatch by using check_exact=False. + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_exact=False + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_ordering(session, scalars_dfs): + columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] + ordering_columns = ["bool_col", "int64_col"] + scalars_df, scalars_pandas_df = scalars_dfs + + def add_ints(row): + return row["int64_col"] + row["int64_too"] + + add_ints_remote = session.remote_function(bigframes.series.Series, int)(add_ints) + + bf_result = ( + scalars_df[columns] + .sort_values(ordering_columns) + .apply(add_ints_remote, axis=1) + .to_pandas() + ) + pd_result = ( + scalars_pandas_df[columns].sort_values(ordering_columns).apply(add_ints, axis=1) + ) + + # bf_result.dtype is 'Int64' while pd_result.dtype is 'object', ignore this + # mismatch by using check_dtype=False. + # + # bf_result.to_numpy() produces an array of numpy.float64's + # (in system_prerelease tests), while pd_result.to_numpy() produces an + # array of ints, ignore this mismatch by using check_exact=False. + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_exact=False + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_multiindex(session): + pd_df = pd.DataFrame( + {"x": [1, 2, 3], "y": [1.5, 3.75, 5], "z": ["pq", "rs", "tu"]}, + index=pd.MultiIndex.from_tuples([("a", 100), ("a", 200), ("b", 300)]), + ) + bf_df = session.read_pandas(pd_df) + + def add_numbers(row): + return row["x"] + row["y"] + + add_numbers_remote = session.remote_function(bigframes.series.Series, float)( + add_numbers + ) + + bf_result = bf_df.apply(add_numbers_remote, axis=1).to_pandas() + pd_result = pd_df.apply(add_numbers, axis=1) + + # bf_result.dtype is 'Float64' while pd_result.dtype is 'float64', ignore this + # mismatch by using check_dtype=False. + # + # bf_result.index[0].dtype is 'string[pyarrow]' while + # pd_result.index[0].dtype is 'object', ignore this mismatch by using + # check_index_type=False. + pd.testing.assert_series_equal( + pd_result, bf_result, check_dtype=False, check_index_type=False + ) + + def test_df_apply_axis_1_unsupported_callable(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] From 487dff6ac147683aef529e1ff8c197dce3fb437c Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 26 Jun 2024 15:09:41 +0000 Subject: [PATCH 675/921] docs: document dtype limitation on row processing `remote_function` (#800) --- bigframes/session/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index b0b2a3c4187..3e8133df48b 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1539,7 +1539,9 @@ def remote_function( the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. .. note:: - ``input_types=Series`` scenario is in preview. + ``input_types=Series`` scenario is in preview. It currently only + supports dataframe with column types ``Int64``/``Float64``/``boolean``/ + ``string``/``binary[pyarrow]``. .. note:: Please make sure following is setup before using this API: From 580e1b9e965d883a67f91a6db8311c2416ca8fe5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 26 Jun 2024 10:38:44 -0700 Subject: [PATCH 676/921] feat: Add Series.peek to preview data efficiently (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Sweña (Swast) --- bigframes/core/blocks.py | 6 +- bigframes/core/pruning.py | 77 +++++++++++++++++++ bigframes/core/tree_properties.py | 39 +++++++++- bigframes/dtypes.py | 77 ++++++++++++++++--- bigframes/series.py | 43 ++++++++++- bigframes/session/__init__.py | 28 +++++-- bigframes/session/planner.py | 74 ++++++++++++++++++ tests/system/small/test_series.py | 64 ++++++++++++++++ tests/unit/test_planner.py | 121 ++++++++++++++++++++++++++++++ 9 files changed, 506 insertions(+), 23 deletions(-) create mode 100644 bigframes/core/pruning.py create mode 100644 bigframes/session/planner.py create mode 100644 tests/unit/test_planner.py diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 598c32670e4..8c90828091c 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2286,13 +2286,13 @@ def to_sql_query( idx_labels, ) - def cached(self, *, optimize_offsets=False, force: bool = False) -> None: + def cached(self, *, force: bool = False, session_aware: bool = False) -> None: """Write the block to a session table.""" # use a heuristic for whether something needs to be cached if (not force) and self.session._is_trivially_executable(self.expr): return - if optimize_offsets: - self.session._cache_with_offsets(self.expr) + elif session_aware: + self.session._cache_with_session_awareness(self.expr) else: self.session._cache_with_cluster_cols( self.expr, cluster_cols=self.index_columns diff --git a/bigframes/core/pruning.py b/bigframes/core/pruning.py new file mode 100644 index 00000000000..55165a616ca --- /dev/null +++ b/bigframes/core/pruning.py @@ -0,0 +1,77 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.core.expression as ex +import bigframes.core.schema as schemata +import bigframes.dtypes +import bigframes.operations as ops + +LOW_CARDINALITY_TYPES = [bigframes.dtypes.BOOL_DTYPE] + +COMPARISON_OP_TYPES = tuple( + type(i) + for i in ( + ops.eq_op, + ops.eq_null_match_op, + ops.ne_op, + ops.gt_op, + ops.ge_op, + ops.lt_op, + ops.le_op, + ) +) + + +def cluster_cols_for_predicate( + predicate: ex.Expression, schema: schemata.ArraySchema +) -> list[str]: + """Try to determine cluster col candidates that work with given predicates.""" + # TODO: Prioritize based on predicted selectivity (eg. equality conditions are probably very selective) + if isinstance(predicate, ex.UnboundVariableExpression): + cols = [predicate.id] + elif isinstance(predicate, ex.OpExpression): + op = predicate.op + # TODO: Support geo predicates, which support pruning if clustered (other than st_disjoint) + # https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions + if isinstance(op, COMPARISON_OP_TYPES): + cols = cluster_cols_for_comparison(predicate.inputs[0], predicate.inputs[1]) + elif isinstance(op, (type(ops.invert_op))): + cols = cluster_cols_for_predicate(predicate.inputs[0], schema) + elif isinstance(op, (type(ops.and_op), type(ops.or_op))): + left_cols = cluster_cols_for_predicate(predicate.inputs[0], schema) + right_cols = cluster_cols_for_predicate(predicate.inputs[1], schema) + cols = [*left_cols, *[col for col in right_cols if col not in left_cols]] + else: + cols = [] + else: + # Constant + cols = [] + return [ + col for col in cols if bigframes.dtypes.is_clusterable(schema.get_type(col)) + ] + + +def cluster_cols_for_comparison( + left_ex: ex.Expression, right_ex: ex.Expression +) -> list[str]: + # TODO: Try to normalize expressions such that one side is a single variable. + # eg. Convert -cola>=3 to cola<-3 and colb+3 < 4 to colb < 1 + if left_ex.is_const: + # There are some invertible ops that would also be ok + if isinstance(right_ex, ex.UnboundVariableExpression): + return [right_ex.id] + elif right_ex.is_const: + if isinstance(left_ex, ex.UnboundVariableExpression): + return [left_ex.id] + return [] diff --git a/bigframes/core/tree_properties.py b/bigframes/core/tree_properties.py index 2847a8f7f15..846cf50d779 100644 --- a/bigframes/core/tree_properties.py +++ b/bigframes/core/tree_properties.py @@ -15,7 +15,7 @@ import functools import itertools -from typing import Callable, Dict, Optional +from typing import Callable, Dict, Optional, Sequence import bigframes.core.nodes as nodes @@ -91,6 +91,43 @@ def _node_counts_inner( ) +def count_nodes(forest: Sequence[nodes.BigFrameNode]) -> dict[nodes.BigFrameNode, int]: + """ + Counts the number of instances of each subtree present within a forest. + + Memoizes internally to accelerate execution, but cache not persisted (not reused between invocations). + + Args: + forest (Sequence of BigFrameNode): + The roots of each tree in the forest + + Returns: + dict[BigFramesNode, int]: The number of occurences of each subtree. + """ + + def _combine_counts( + left: Dict[nodes.BigFrameNode, int], right: Dict[nodes.BigFrameNode, int] + ) -> Dict[nodes.BigFrameNode, int]: + return { + key: left.get(key, 0) + right.get(key, 0) + for key in itertools.chain(left.keys(), right.keys()) + } + + empty_counts: Dict[nodes.BigFrameNode, int] = {} + + @functools.cache + def _node_counts_inner( + subtree: nodes.BigFrameNode, + ) -> Dict[nodes.BigFrameNode, int]: + """Helper function to count occurences of duplicate nodes in a subtree. Considers only nodes in a complexity range""" + child_counts = [_node_counts_inner(child) for child in subtree.child_nodes] + node_counts = functools.reduce(_combine_counts, child_counts, empty_counts) + return _combine_counts(node_counts, {subtree: 1}) + + counts = [_node_counts_inner(root) for root in forest] + return functools.reduce(_combine_counts, counts, empty_counts) + + def replace_nodes( root: nodes.BigFrameNode, replacements: dict[nodes.BigFrameNode, nodes.BigFrameNode], diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index ced1c215e5b..5de8f896a92 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -74,52 +74,95 @@ class SimpleDtypeInfo: logical_bytes: int = ( 8 # this is approximate only, some types are variably sized, also, compression ) + orderable: bool = False + clusterable: bool = False # TODO: Missing BQ types: INTERVAL, JSON, RANGE # TODO: Add mappings to python types SIMPLE_TYPES = ( SimpleDtypeInfo( - dtype=INT_DTYPE, arrow_dtype=pa.int64(), type_kind=("INT64", "INTEGER") + dtype=INT_DTYPE, + arrow_dtype=pa.int64(), + type_kind=("INT64", "INTEGER"), + orderable=True, + clusterable=True, ), SimpleDtypeInfo( - dtype=FLOAT_DTYPE, arrow_dtype=pa.float64(), type_kind=("FLOAT64", "FLOAT") + dtype=FLOAT_DTYPE, + arrow_dtype=pa.float64(), + type_kind=("FLOAT64", "FLOAT"), + orderable=True, ), SimpleDtypeInfo( dtype=BOOL_DTYPE, arrow_dtype=pa.bool_(), type_kind=("BOOL", "BOOLEAN"), logical_bytes=1, + orderable=True, + clusterable=True, ), - SimpleDtypeInfo(dtype=STRING_DTYPE, arrow_dtype=pa.string(), type_kind=("STRING",)), SimpleDtypeInfo( - dtype=DATE_DTYPE, arrow_dtype=pa.date32(), type_kind=("DATE",), logical_bytes=4 + dtype=STRING_DTYPE, + arrow_dtype=pa.string(), + type_kind=("STRING",), + orderable=True, + clusterable=True, ), - SimpleDtypeInfo(dtype=TIME_DTYPE, arrow_dtype=pa.time64("us"), type_kind=("TIME",)), SimpleDtypeInfo( - dtype=DATETIME_DTYPE, arrow_dtype=pa.timestamp("us"), type_kind=("DATETIME",) + dtype=DATE_DTYPE, + arrow_dtype=pa.date32(), + type_kind=("DATE",), + logical_bytes=4, + orderable=True, + clusterable=True, + ), + SimpleDtypeInfo( + dtype=TIME_DTYPE, + arrow_dtype=pa.time64("us"), + type_kind=("TIME",), + orderable=True, + ), + SimpleDtypeInfo( + dtype=DATETIME_DTYPE, + arrow_dtype=pa.timestamp("us"), + type_kind=("DATETIME",), + orderable=True, + clusterable=True, ), SimpleDtypeInfo( dtype=TIMESTAMP_DTYPE, arrow_dtype=pa.timestamp("us", tz="UTC"), type_kind=("TIMESTAMP",), + orderable=True, + clusterable=True, + ), + SimpleDtypeInfo( + dtype=BYTES_DTYPE, arrow_dtype=pa.binary(), type_kind=("BYTES",), orderable=True ), - SimpleDtypeInfo(dtype=BYTES_DTYPE, arrow_dtype=pa.binary(), type_kind=("BYTES",)), SimpleDtypeInfo( dtype=NUMERIC_DTYPE, arrow_dtype=pa.decimal128(38, 9), type_kind=("NUMERIC",), logical_bytes=16, + orderable=True, + clusterable=True, ), SimpleDtypeInfo( dtype=BIGNUMERIC_DTYPE, arrow_dtype=pa.decimal256(76, 38), type_kind=("BIGNUMERIC",), logical_bytes=32, + orderable=True, + clusterable=True, ), # Geo has no corresponding arrow dtype SimpleDtypeInfo( - dtype=GEO_DTYPE, arrow_dtype=None, type_kind=("GEOGRAPHY",), logical_bytes=40 + dtype=GEO_DTYPE, + arrow_dtype=None, + type_kind=("GEOGRAPHY",), + logical_bytes=40, + clusterable=True, ), ) @@ -209,9 +252,25 @@ def is_comparable(type: ExpressionType) -> bool: return (type is not None) and is_orderable(type) +_ORDERABLE_SIMPLE_TYPES = set( + mapping.dtype for mapping in SIMPLE_TYPES if mapping.orderable +) + + def is_orderable(type: ExpressionType) -> bool: # On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable - return not is_array_like(type) and not is_struct_like(type) and (type != GEO_DTYPE) + return type in _ORDERABLE_SIMPLE_TYPES + + +_CLUSTERABLE_SIMPLE_TYPES = set( + mapping.dtype for mapping in SIMPLE_TYPES if mapping.clusterable +) + + +def is_clusterable(type: ExpressionType) -> bool: + # https://cloud.google.com/bigquery/docs/clustered-tables#cluster_column_types + # This is based on default database type mapping, could in theory represent in non-default bq type to cluster. + return type in _CLUSTERABLE_SIMPLE_TYPES def is_bool_coercable(type: ExpressionType) -> bool: diff --git a/bigframes/series.py b/bigframes/series.py index eda95fa1e8a..57543abef3c 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -623,6 +623,40 @@ def head(self, n: int = 5) -> Series: def tail(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[-n:]) + def peek(self, n: int = 5, *, force: bool = True) -> pandas.DataFrame: + """ + Preview n arbitrary elements from the series without guarantees about row selection or ordering. + + ``Series.peek(force=False)`` will always be very fast, but will not succeed if data requires + full data scanning. Using ``force=True`` will always succeed, but may be perform queries. + Query results will be cached so that future steps will benefit from these queries. + + Args: + n (int, default 5): + The number of rows to select from the series. Which N rows are returned is non-deterministic. + force (bool, default True): + If the data cannot be peeked efficiently, the series will instead be fully materialized as part + of the operation if ``force=True``. If ``force=False``, the operation will throw a ValueError. + Returns: + pandas.Series: A pandas Series with n rows. + + Raises: + ValueError: If force=False and data cannot be efficiently peeked. + """ + maybe_result = self._block.try_peek(n) + if maybe_result is None: + if force: + self._cached() + maybe_result = self._block.try_peek(n, force=True) + assert maybe_result is not None + else: + raise ValueError( + "Cannot peek efficiently when data has aggregates, joins or window functions applied. Use force=True to fully compute dataframe." + ) + as_series = maybe_result.squeeze(axis=1) + as_series.name = self.name + return as_series + def nlargest(self, n: int = 5, keep: str = "first") -> Series: if keep not in ("first", "last", "all"): raise ValueError("'keep must be one of 'first', 'last', or 'all'") @@ -1419,7 +1453,7 @@ def apply( # return Series with materialized result so that any error in the remote # function is caught early - materialized_series = result_series._cached() + materialized_series = result_series._cached(session_aware=False) return materialized_series def combine( @@ -1794,10 +1828,11 @@ def cache(self): Returns: Series: Self """ - return self._cached(force=True) + # Do not use session-aware cashing if user-requested + return self._cached(force=True, session_aware=False) - def _cached(self, *, force: bool = True) -> Series: - self._block.cached(force=force) + def _cached(self, *, force: bool = True, session_aware: bool = True) -> Series: + self._block.cached(force=force, session_aware=session_aware) return self def _optimize_query_complexity(self): diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 3e8133df48b..a4c926de723 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,7 +16,6 @@ from __future__ import annotations -import collections.abc import copy import datetime import itertools @@ -84,6 +83,7 @@ import bigframes.core.guid import bigframes.core.nodes as nodes import bigframes.core.ordering as order +import bigframes.core.pruning import bigframes.core.schema as schemata import bigframes.core.tree_properties as traversals import bigframes.core.tree_properties as tree_properties @@ -100,6 +100,7 @@ import bigframes.session._io.bigquery as bf_io_bigquery import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table import bigframes.session.clients +import bigframes.session.planner import bigframes.version # Avoid circular imports. @@ -342,13 +343,15 @@ def session_id(self): @property def objects( self, - ) -> collections.abc.Set[ + ) -> Iterable[ Union[ bigframes.core.indexes.Index, bigframes.series.Series, dataframe.DataFrame ] ]: + still_alive = [i for i in self._objects if i() is not None] + self._objects = still_alive # Create a set with strong references, be careful not to hold onto this needlessly, as will prevent garbage collection. - return set(i() for i in self._objects if i() is not None) # type: ignore + return tuple(i() for i in self._objects if i() is not None) # type: ignore @property def _project(self): @@ -1876,21 +1879,34 @@ def _cache_with_offsets(self, array_value: core.ArrayValue): raise ValueError( "Caching with offsets only supported in strictly ordered mode." ) + offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") sql = bigframes.core.compile.compile_unordered( self._with_cached_executions( - array_value.promote_offsets("bigframes_offsets").node + array_value.promote_offsets(offset_column).node ) ) tmp_table = self._sql_to_temp_table( - sql, cluster_cols=["bigframes_offsets"], api_name="cached" + sql, cluster_cols=[offset_column], api_name="cached" ) cached_replacement = array_value.as_cached( cache_table=self.bqclient.get_table(tmp_table), - ordering=order.ExpressionOrdering.from_offset_col("bigframes_offsets"), + ordering=order.ExpressionOrdering.from_offset_col(offset_column), ).node self._cached_executions[array_value.node] = cached_replacement + def _cache_with_session_awareness(self, array_value: core.ArrayValue) -> None: + # this is the occurence count across the whole session + forest = [obj._block.expr.node for obj in self.objects] + # These node types are cheap to re-compute + target, cluster_cols = bigframes.session.planner.session_aware_cache_plan( + array_value.node, forest + ) + if len(cluster_cols) > 0: + self._cache_with_cluster_cols(core.ArrayValue(target), cluster_cols) + else: + self._cache_with_offsets(core.ArrayValue(target)) + def _simplify_with_caching(self, array_value: core.ArrayValue): """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" # Apply existing caching first diff --git a/bigframes/session/planner.py b/bigframes/session/planner.py new file mode 100644 index 00000000000..2a74521b43c --- /dev/null +++ b/bigframes/session/planner.py @@ -0,0 +1,74 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import itertools +from typing import Sequence, Tuple + +import bigframes.core.expression as ex +import bigframes.core.nodes as nodes +import bigframes.core.pruning as predicate_pruning +import bigframes.core.tree_properties as traversals + + +def session_aware_cache_plan( + root: nodes.BigFrameNode, session_forest: Sequence[nodes.BigFrameNode] +) -> Tuple[nodes.BigFrameNode, list[str]]: + """ + Determines the best node to cache given a target and a list of object roots for objects in a session. + + Returns the node to cache, and optionally a clustering column. + """ + node_counts = traversals.count_nodes(session_forest) + # These node types are cheap to re-compute, so it makes more sense to cache their children. + de_cachable_types = (nodes.FilterNode, nodes.ProjectionNode) + caching_target = cur_node = root + caching_target_refs = node_counts.get(caching_target, 0) + + filters: list[ + ex.Expression + ] = [] # accumulate filters into this as traverse downwards + clusterable_cols: set[str] = set() + while isinstance(cur_node, de_cachable_types): + if isinstance(cur_node, nodes.FilterNode): + # Filter node doesn't define any variables, so no need to chain expressions + filters.append(cur_node.predicate) + elif isinstance(cur_node, nodes.ProjectionNode): + # Projection defines the variables that are used in the filter expressions, need to substitute variables with their scalar expressions + # that instead reference variables in the child node. + bindings = {name: expr for expr, name in cur_node.assignments} + filters = [i.bind_all_variables(bindings) for i in filters] + else: + raise ValueError(f"Unexpected de-cached node: {cur_node}") + + cur_node = cur_node.child + cur_node_refs = node_counts.get(cur_node, 0) + if cur_node_refs > caching_target_refs: + caching_target, caching_target_refs = cur_node, cur_node_refs + schema = cur_node.schema + # Cluster cols only consider the target object and not other sesssion objects + clusterable_cols = set( + itertools.chain.from_iterable( + map( + lambda f: predicate_pruning.cluster_cols_for_predicate( + f, schema + ), + filters, + ) + ) + ) + # BQ supports up to 4 cluster columns, just prioritize by alphabetical ordering + # TODO: Prioritize caching columns by estimated filter selectivity + return caching_target, sorted(list(clusterable_cols))[:4] diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 3e21418f2f6..cb28686d59e 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1970,6 +1970,70 @@ def test_head_then_series_operation(scalars_dfs): ) +def test_series_peek(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + peek_result = scalars_df["float64_col"].peek(n=3, force=False) + pd.testing.assert_series_equal( + peek_result, + scalars_pandas_df["float64_col"].reindex_like(peek_result), + ) + + +def test_series_peek_multi_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + bf_series = scalars_df.set_index(["string_col", "bool_col"])["float64_col"] + bf_series.name = ("2-part", "name") + pd_series = scalars_pandas_df.set_index(["string_col", "bool_col"])["float64_col"] + pd_series.name = ("2-part", "name") + peek_result = bf_series.peek(n=3, force=False) + pd.testing.assert_series_equal( + peek_result, + pd_series.reindex_like(peek_result), + ) + + +def test_series_peek_filtered(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + peek_result = scalars_df[scalars_df.int64_col > 0]["float64_col"].peek( + n=3, force=False + ) + pd_result = scalars_pandas_df[scalars_pandas_df.int64_col > 0]["float64_col"] + pd.testing.assert_series_equal( + peek_result, + pd_result.reindex_like(peek_result), + ) + + +@skip_legacy_pandas +def test_series_peek_force(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + cumsum_df = scalars_df[["int64_col", "int64_too"]].cumsum() + df_filtered = cumsum_df[cumsum_df.int64_col > 0]["int64_too"] + peek_result = df_filtered.peek(n=3, force=True) + pd_cumsum_df = scalars_pandas_df[["int64_col", "int64_too"]].cumsum() + pd_result = pd_cumsum_df[pd_cumsum_df.int64_col > 0]["int64_too"] + pd.testing.assert_series_equal( + peek_result, + pd_result.reindex_like(peek_result), + ) + + +@skip_legacy_pandas +def test_series_peek_force_float(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + cumsum_df = scalars_df[["int64_col", "float64_col"]].cumsum() + df_filtered = cumsum_df[cumsum_df.float64_col > 0]["float64_col"] + peek_result = df_filtered.peek(n=3, force=True) + pd_cumsum_df = scalars_pandas_df[["int64_col", "float64_col"]].cumsum() + pd_result = pd_cumsum_df[pd_cumsum_df.float64_col > 0]["float64_col"] + pd.testing.assert_series_equal( + peek_result, + pd_result.reindex_like(peek_result), + ) + + def test_shift(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" bf_result = scalars_df_index[col_name].shift().to_pandas() diff --git a/tests/unit/test_planner.py b/tests/unit/test_planner.py new file mode 100644 index 00000000000..2e276d0f1ae --- /dev/null +++ b/tests/unit/test_planner.py @@ -0,0 +1,121 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import unittest.mock as mock + +import google.cloud.bigquery +import pandas as pd + +import bigframes.core as core +import bigframes.core.expression as ex +import bigframes.core.schema +import bigframes.operations as ops +import bigframes.session.planner as planner + +TABLE_REF = google.cloud.bigquery.TableReference.from_string("project.dataset.table") +SCHEMA = ( + google.cloud.bigquery.SchemaField("col_a", "INTEGER"), + google.cloud.bigquery.SchemaField("col_b", "INTEGER"), +) +TABLE = google.cloud.bigquery.Table( + table_ref=TABLE_REF, + schema=SCHEMA, +) +FAKE_SESSION = mock.create_autospec(bigframes.Session, instance=True) +type(FAKE_SESSION)._strictly_ordered = mock.PropertyMock(return_value=True) +LEAF: core.ArrayValue = core.ArrayValue.from_table( + session=FAKE_SESSION, + table=TABLE, + schema=bigframes.core.schema.ArraySchema.from_bq_table(TABLE), +) + + +def test_session_aware_caching_project_filter(): + """ + Test that if a node is filtered by a column, the node is cached pre-filter and clustered by the filter column. + """ + session_objects = [LEAF, LEAF.assign_constant("col_c", 4, pd.Int64Dtype())] + target = LEAF.assign_constant("col_c", 4, pd.Int64Dtype()).filter( + ops.gt_op.as_expr("col_a", ex.const(3)) + ) + result, cluster_cols = planner.session_aware_cache_plan( + target.node, [obj.node for obj in session_objects] + ) + assert result == LEAF.node + assert cluster_cols == ["col_a"] + + +def test_session_aware_caching_project_multi_filter(): + """ + Test that if a node is filtered by multiple columns, all of them are in the cluster cols + """ + session_objects = [LEAF, LEAF.assign_constant("col_c", 4, pd.Int64Dtype())] + predicate_1a = ops.gt_op.as_expr("col_a", ex.const(3)) + predicate_1b = ops.lt_op.as_expr("col_a", ex.const(55)) + predicate_1 = ops.and_op.as_expr(predicate_1a, predicate_1b) + predicate_3 = ops.eq_op.as_expr("col_b", ex.const(1)) + target = ( + LEAF.filter(predicate_1) + .assign_constant("col_c", 4, pd.Int64Dtype()) + .filter(predicate_3) + ) + result, cluster_cols = planner.session_aware_cache_plan( + target.node, [obj.node for obj in session_objects] + ) + assert result == LEAF.node + assert cluster_cols == ["col_a", "col_b"] + + +def test_session_aware_caching_unusable_filter(): + """ + Test that if a node is filtered by multiple columns in the same comparison, the node is cached pre-filter and not clustered by either column. + + Most filters with multiple column references cannot be used for scan pruning, as they cannot be converted to fixed value ranges. + """ + session_objects = [LEAF, LEAF.assign_constant("col_c", 4, pd.Int64Dtype())] + target = LEAF.assign_constant("col_c", 4, pd.Int64Dtype()).filter( + ops.gt_op.as_expr("col_a", "col_b") + ) + result, cluster_cols = planner.session_aware_cache_plan( + target.node, [obj.node for obj in session_objects] + ) + assert result == LEAF.node + assert cluster_cols == [] + + +def test_session_aware_caching_fork_after_window_op(): + """ + Test that caching happens only after an windowed operation, but before filtering, projecting. + + Windowing is expensive, so caching should always compute the window function, in order to avoid later recomputation. + """ + other = LEAF.promote_offsets("offsets_col").assign_constant( + "col_d", 5, pd.Int64Dtype() + ) + target = ( + LEAF.promote_offsets("offsets_col") + .assign_constant("col_c", 4, pd.Int64Dtype()) + .filter( + ops.eq_op.as_expr("col_a", ops.add_op.as_expr(ex.const(4), ex.const(3))) + ) + ) + result, cluster_cols = planner.session_aware_cache_plan( + target.node, + [ + other.node, + ], + ) + assert result == LEAF.promote_offsets("offsets_col").node + assert cluster_cols == ["col_a"] From 87e60182c964c369079165e87ce73dd0c0481a5a Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:06:25 -0700 Subject: [PATCH 677/921] feat: add .agg support for size (#792) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add .agg support for size * undo test change. * logic fix. * type update * Apply suggestions from code review * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Tim Sweña (Swast) Co-authored-by: Owl Bot --- bigframes/core/blocks.py | 55 +++++++++++++++++++++++----- bigframes/core/groupby/__init__.py | 10 +++-- bigframes/operations/aggregations.py | 10 ++++- bigframes/series.py | 5 ++- tests/system/small/test_dataframe.py | 14 +++++-- tests/system/small/test_groupby.py | 43 +++++++++++++++------- tests/system/small/test_series.py | 25 +++++++++++-- 7 files changed, 126 insertions(+), 36 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 8c90828091c..f40dfc0071c 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -995,7 +995,7 @@ def filter(self, predicate: scalars.Expression): def aggregate_all_and_stack( self, - operation: agg_ops.UnaryAggregateOp, + operation: typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp], *, axis: int | str = 0, value_col_id: str = "values", @@ -1004,7 +1004,12 @@ def aggregate_all_and_stack( axis_n = utils.get_axis_number(axis) if axis_n == 0: aggregations = [ - (ex.UnaryAggregation(operation, ex.free_var(col_id)), col_id) + ( + ex.UnaryAggregation(operation, ex.free_var(col_id)) + if isinstance(operation, agg_ops.UnaryAggregateOp) + else ex.NullaryAggregation(operation), + col_id, + ) for col_id in self.value_columns ] index_id = guid.generate_guid() @@ -1033,6 +1038,11 @@ def aggregate_all_and_stack( (ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.free_var(col_id)), col_id) for col_id in [*self.index_columns] ] + # TODO: may need add NullaryAggregation in main_aggregation + # when agg add support for axis=1, needed for agg("size", axis=1) + assert isinstance( + operation, agg_ops.UnaryAggregateOp + ), f"Expected a unary operation, but got {operation}. Please report this error and how you got here to the BigQuery DataFrames team (bit.ly/bigframes-feedback)." main_aggregation = ( ex.UnaryAggregation(operation, ex.free_var(value_col_id)), value_col_id, @@ -1125,7 +1135,11 @@ def remap_f(x): def aggregate( self, by_column_ids: typing.Sequence[str] = (), - aggregations: typing.Sequence[typing.Tuple[str, agg_ops.UnaryAggregateOp]] = (), + aggregations: typing.Sequence[ + typing.Tuple[ + str, typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp] + ] + ] = (), *, dropna: bool = True, ) -> typing.Tuple[Block, typing.Sequence[str]]: @@ -1139,7 +1153,9 @@ def aggregate( """ agg_specs = [ ( - ex.UnaryAggregation(operation, ex.free_var(input_id)), + ex.UnaryAggregation(operation, ex.free_var(input_id)) + if isinstance(operation, agg_ops.UnaryAggregateOp) + else ex.NullaryAggregation(operation), guid.generate_guid(), ) for input_id, operation in aggregations @@ -1175,18 +1191,32 @@ def aggregate( output_col_ids, ) - def get_stat(self, column_id: str, stat: agg_ops.UnaryAggregateOp): + def get_stat( + self, + column_id: str, + stat: typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp], + ): """Gets aggregates immediately, and caches it""" if stat.name in self._stats_cache[column_id]: return self._stats_cache[column_id][stat.name] # TODO: Convert nonstandard stats into standard stats where possible (popvar, etc.) # if getting a standard stat, just go get the rest of them - standard_stats = self._standard_stats(column_id) + standard_stats = typing.cast( + typing.Sequence[ + typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp] + ], + self._standard_stats(column_id), + ) stats_to_fetch = standard_stats if stat in standard_stats else [stat] aggregations = [ - (ex.UnaryAggregation(stat, ex.free_var(column_id)), stat.name) + ( + ex.UnaryAggregation(stat, ex.free_var(column_id)) + if isinstance(stat, agg_ops.UnaryAggregateOp) + else ex.NullaryAggregation(stat), + stat.name, + ) for stat in stats_to_fetch ] expr = self.expr.aggregate(aggregations) @@ -1231,13 +1261,20 @@ def get_binary_stat( def summarize( self, column_ids: typing.Sequence[str], - stats: typing.Sequence[agg_ops.UnaryAggregateOp], + stats: typing.Sequence[ + typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp] + ], ): """Get a list of stats as a deferred block object.""" label_col_id = guid.generate_guid() labels = [stat.name for stat in stats] aggregations = [ - (ex.UnaryAggregation(stat, ex.free_var(col_id)), f"{col_id}-{stat.name}") + ( + ex.UnaryAggregation(stat, ex.free_var(col_id)) + if isinstance(stat, agg_ops.UnaryAggregateOp) + else ex.NullaryAggregation(stat), + f"{col_id}-{stat.name}", + ) for stat in stats for col_id in column_ids ] diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 9c2bf18caaa..2b23ccf0e4c 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -286,10 +286,10 @@ def expanding(self, min_periods: int = 1) -> windows.Window: block, window_spec, self._selected_cols, drop_null_groups=self._dropna ) - def agg(self, func=None, **kwargs) -> df.DataFrame: + def agg(self, func=None, **kwargs) -> typing.Union[df.DataFrame, series.Series]: if func: if isinstance(func, str): - return self._agg_string(func) + return self.size() if func == "size" else self._agg_string(func) elif utils.is_dict_like(func): return self._agg_dict(func) elif utils.is_list_like(func): @@ -315,7 +315,11 @@ def _agg_string(self, func: str) -> df.DataFrame: return dataframe if self._as_index else self._convert_index(dataframe) def _agg_dict(self, func: typing.Mapping) -> df.DataFrame: - aggregations: typing.List[typing.Tuple[str, agg_ops.UnaryAggregateOp]] = [] + aggregations: typing.List[ + typing.Tuple[ + str, typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp] + ] + ] = [] column_labels = [] want_aggfunc_level = any(utils.is_list_like(aggs) for aggs in func.values()) diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 783abfd788b..619183287fe 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -487,7 +487,9 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT # TODO: Alternative names and lookup from numpy function objects -_AGGREGATIONS_LOOKUP: dict[str, UnaryAggregateOp] = { +_AGGREGATIONS_LOOKUP: typing.Dict[ + str, typing.Union[UnaryAggregateOp, NullaryAggregateOp] +] = { op.name: op for op in [ sum_op, @@ -506,10 +508,14 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT ApproxQuartilesOp(2), ApproxQuartilesOp(3), ] + + [ + # Add size_op separately to avoid Mypy type inference errors. + size_op, + ] } -def lookup_agg_func(key: str) -> UnaryAggregateOp: +def lookup_agg_func(key: str) -> typing.Union[UnaryAggregateOp, NullaryAggregateOp]: if callable(key): raise NotImplementedError( "Aggregating with callable object not supported, pass method name as string instead (eg. 'sum' instead of np.sum)." diff --git a/bigframes/series.py b/bigframes/series.py index 57543abef3c..33343211582 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -968,7 +968,6 @@ def agg(self, func: str | typing.Sequence[str]) -> scalars.Scalar | Series: ) ) else: - return self._apply_aggregation( agg_ops.lookup_agg_func(typing.cast(str, func)) ) @@ -1246,7 +1245,9 @@ def _align3(self, other1: Series | scalars.Scalar, other2: Series | scalars.Scal values, index = self._align_n([other1, other2], how) return (values[0], values[1], values[2], index) - def _apply_aggregation(self, op: agg_ops.UnaryAggregateOp) -> Any: + def _apply_aggregation( + self, op: agg_ops.UnaryAggregateOp | agg_ops.NullaryAggregateOp + ) -> Any: return self._block.get_stat(self._value_column, op) def _apply_window_op( diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 0aac9e2578c..a5c810b91b6 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2485,12 +2485,19 @@ def test_dataframe_agg_single_string(scalars_dfs): ) -def test_dataframe_agg_int_single_string(scalars_dfs): +@pytest.mark.parametrize( + ("agg",), + ( + ("sum",), + ("size",), + ), +) +def test_dataframe_agg_int_single_string(scalars_dfs, agg): numeric_cols = ["int64_col", "int64_too", "bool_col"] scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df[numeric_cols].agg("sum").to_pandas() - pd_result = scalars_pandas_df[numeric_cols].agg("sum") + bf_result = scalars_df[numeric_cols].agg(agg).to_pandas() + pd_result = scalars_pandas_df[numeric_cols].agg(agg) assert bf_result.dtype == "Int64" pd.testing.assert_series_equal( @@ -2537,6 +2544,7 @@ def test_dataframe_agg_int_multi_string(scalars_dfs): "sum", "nunique", "count", + "size", ] scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df[numeric_cols].agg(aggregations).to_pandas() diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 960dc10948c..8e3baff4c27 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -140,11 +140,23 @@ def test_dataframe_groupby_agg_string( ) +def test_dataframe_groupby_agg_size_string(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = scalars_df_index[col_names].groupby("string_col").agg("size") + pd_result = scalars_pandas_df_index[col_names].groupby("string_col").agg("size") + + pd.testing.assert_series_equal(pd_result, bf_result.to_pandas(), check_dtype=False) + + def test_dataframe_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] - bf_result = scalars_df_index[col_names].groupby("string_col").agg(["count", "min"]) + bf_result = ( + scalars_df_index[col_names].groupby("string_col").agg(["count", "min", "size"]) + ) pd_result = ( - scalars_pandas_df_index[col_names].groupby("string_col").agg(["count", "min"]) + scalars_pandas_df_index[col_names] + .groupby("string_col") + .agg(["count", "min", "size"]) ) bf_result_computed = bf_result.to_pandas() @@ -161,8 +173,8 @@ def test_dataframe_groupby_agg_list_w_column_multi_index( pd_df = scalars_pandas_df_index[columns].copy() pd_df.columns = multi_columns - bf_result = bf_df.groupby(level=0).agg(["count", "min"]) - pd_result = pd_df.groupby(level=0).agg(["count", "min"]) + bf_result = bf_df.groupby(level=0).agg(["count", "min", "size"]) + pd_result = pd_df.groupby(level=0).agg(["count", "min", "size"]) bf_result_computed = bf_result.to_pandas() pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) @@ -182,12 +194,12 @@ def test_dataframe_groupby_agg_dict_with_list( bf_result = ( scalars_df_index[col_names] .groupby("string_col", as_index=as_index) - .agg({"int64_too": ["mean", "max"], "string_col": "count"}) + .agg({"int64_too": ["mean", "max"], "string_col": "count", "bool_col": "size"}) ) pd_result = ( scalars_pandas_df_index[col_names] .groupby("string_col", as_index=as_index) - .agg({"int64_too": ["mean", "max"], "string_col": "count"}) + .agg({"int64_too": ["mean", "max"], "string_col": "count", "bool_col": "size"}) ) bf_result_computed = bf_result.to_pandas() @@ -413,16 +425,21 @@ def test_dataframe_groupby_nonnumeric_with_mean(): # ============== -def test_series_groupby_agg_string(scalars_df_index, scalars_pandas_df_index): +@pytest.mark.parametrize( + ("agg"), + [ + ("count"), + ("size"), + ], +) +def test_series_groupby_agg_string(scalars_df_index, scalars_pandas_df_index, agg): bf_result = ( - scalars_df_index["int64_col"] - .groupby(scalars_df_index["string_col"]) - .agg("count") + scalars_df_index["int64_col"].groupby(scalars_df_index["string_col"]).agg(agg) ) pd_result = ( scalars_pandas_df_index["int64_col"] .groupby(scalars_pandas_df_index["string_col"]) - .agg("count") + .agg(agg) ) bf_result_computed = bf_result.to_pandas() @@ -435,12 +452,12 @@ def test_series_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): bf_result = ( scalars_df_index["int64_col"] .groupby(scalars_df_index["string_col"]) - .agg(["sum", "mean"]) + .agg(["sum", "mean", "size"]) ) pd_result = ( scalars_pandas_df_index["int64_col"] .groupby(scalars_pandas_df_index["string_col"]) - .agg(["sum", "mean"]) + .agg(["sum", "mean", "size"]) ) bf_result_computed = bf_result.to_pandas() diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index cb28686d59e..10fcec63ce8 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -506,15 +506,32 @@ def test_series_dropna(scalars_dfs, ignore_index): pd.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) -def test_series_agg_single_string(scalars_dfs): +@pytest.mark.parametrize( + ("agg",), + ( + ("sum",), + ("size",), + ), +) +def test_series_agg_single_string(scalars_dfs, agg): scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df["int64_col"].agg("sum") - pd_result = scalars_pandas_df["int64_col"].agg("sum") + bf_result = scalars_df["int64_col"].agg(agg) + pd_result = scalars_pandas_df["int64_col"].agg(agg) assert math.isclose(pd_result, bf_result) def test_series_agg_multi_string(scalars_dfs): - aggregations = ["sum", "mean", "std", "var", "min", "max", "nunique", "count"] + aggregations = [ + "sum", + "mean", + "std", + "var", + "min", + "max", + "nunique", + "count", + "size", + ] scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df["int64_col"].agg(aggregations).to_pandas() pd_result = scalars_pandas_df["int64_col"].agg(aggregations) From b47f32d74a0c9eb908be690b2dd56b0f5579b133 Mon Sep 17 00:00:00 2001 From: Henry Solberg Date: Wed, 26 Jun 2024 13:48:22 -0700 Subject: [PATCH 678/921] feat: Add `bigframes.streaming.to_pubsub` method to create continuous query that writes to Pub/Sub (#801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add to_pubsub streaming method * fix comment * fix comment further * remove accidental files * remove more accidental files * remove another accidental file * use service account * fix return value * pass session * have the user provide the service account * fix mypy error * address comments * update service account name * fix invalid character in label * rename service_account to service_account_email * move streaming tests to own tables --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/streaming/__init__.py | 139 +++++++++++++++++++++++++-- scripts/create_bigtable.py | 3 - scripts/create_pubsub.py | 49 ++++++++++ setup.py | 1 + testing/constraints-3.9.txt | 1 + tests/system/large/test_streaming.py | 33 ++++++- 6 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 scripts/create_pubsub.py diff --git a/bigframes/streaming/__init__.py b/bigframes/streaming/__init__.py index 16da677ef5d..0b6fd18561e 100644 --- a/bigframes/streaming/__init__.py +++ b/bigframes/streaming/__init__.py @@ -16,6 +16,7 @@ import json from typing import Optional +import warnings from google.cloud import bigquery @@ -24,9 +25,11 @@ def to_bigtable( query: str, + *, instance: str, table: str, - bq_client: Optional[bigquery.Client] = None, + service_account_email: Optional[str] = None, + session: Optional[bigframes.Session] = None, app_profile: Optional[str] = None, truncate: bool = False, overwrite: bool = False, @@ -53,10 +56,15 @@ def to_bigtable( The name of the bigtable instance to export to. table (str): The name of the bigtable table to export to. - bq_client (str, default None): - The Client object to use for the query. This determines + service_account_email (str): + Full name of the service account to run the continuous query. + Example: accountname@projectname.gserviceaccounts.com + If not provided, the user account will be used, but this + limits the lifetime of the continuous query. + session (bigframes.Session, default None): + The session object to use for the query. This determines the project id and location of the query. If None, will - default to the bigframes global session default client. + default to the bigframes global session. app_profile (str, default None): The bigtable app profile to export to. If None, no app profile will be used. @@ -90,9 +98,16 @@ def to_bigtable( For example, the job can be cancelled or its error status can be examined. """ + warnings.warn( + "The bigframes.streaming module is a preview feature, and subject to change.", + stacklevel=1, + category=bigframes.exceptions.PreviewWarning, + ) + # get default client if not passed - if bq_client is None: - bq_client = bigframes.get_global_session().bqclient + if session is None: + session = bigframes.get_global_session() + bq_client = session.bqclient # build export string from parameters project = bq_client.project @@ -123,7 +138,117 @@ def to_bigtable( # override continuous http parameter job_config = bigquery.job.QueryJobConfig() - job_config_filled = job_config.from_api_repr({"query": {"continuous": True}}) + + job_config_dict: dict = {"query": {"continuous": True}} + if service_account_email is not None: + job_config_dict["query"]["connectionProperties"] = { + "key": "service_account", + "value": service_account_email, + } + job_config_filled = job_config.from_api_repr(job_config_dict) + job_config_filled.labels = {"bigframes-api": "streaming_to_bigtable"} + + # begin the query job + query_job = bq_client.query( + sql, + job_config=job_config_filled, # type:ignore + # typing error above is in bq client library + # (should accept abstract job_config, only takes concrete) + job_id=job_id, + job_id_prefix=job_id_prefix, + ) + + # return the query job to the user for lifetime management + return query_job + + +def to_pubsub( + query: str, + *, + topic: str, + service_account_email: str, + session: Optional[bigframes.Session] = None, + job_id: Optional[str] = None, + job_id_prefix: Optional[str] = None, +) -> bigquery.QueryJob: + """Launches a BigQuery continuous query and returns a + QueryJob object for some management functionality. + + This method requires an existing pubsub topic. For instructions + on creating a pubsub topic, see + https://cloud.google.com/pubsub/docs/samples/pubsub-quickstart-create-topic?hl=en + + Note that a service account is a requirement for continuous queries + exporting to pubsub. + + Args: + query (str): + The sql statement to execute as a continuous function. + For example: "SELECT * FROM dataset.table" + This will be wrapped in an EXPORT DATA statement to + launch a continuous query writing to pubsub. + topic (str): + The name of the pubsub topic to export to. + For example: "taxi-rides" + service_account_email (str): + Full name of the service account to run the continuous query. + Example: accountname@projectname.gserviceaccounts.com + session (bigframes.Session, default None): + The session object to use for the query. This determines + the project id and location of the query. If None, will + default to the bigframes global session. + job_id (str, default None): + If specified, replace the default job id for the query, + see job_id parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + job_id_prefix (str, default None): + If specified, a job id prefix for the query, see + job_id_prefix parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + + Returns: + google.cloud.bigquery.QueryJob: + See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob + The ongoing query job can be managed using this object. + For example, the job can be cancelled or its error status + can be examined. + """ + warnings.warn( + "The bigframes.streaming module is a preview feature, and subject to change.", + stacklevel=1, + category=bigframes.exceptions.PreviewWarning, + ) + + # get default client if not passed + if session is None: + session = bigframes.get_global_session() + bq_client = session.bqclient + + # build export string from parameters + sql = ( + "EXPORT DATA\n" + "OPTIONS (\n" + "format = 'CLOUD_PUBSUB',\n" + f'uri = "https://pubsub.googleapis.com/projects/{bq_client.project}/topics/{topic}"\n' + ")\n" + "AS (\n" + f"{query});" + ) + + # override continuous http parameter + job_config = bigquery.job.QueryJobConfig() + job_config_filled = job_config.from_api_repr( + { + "query": { + "continuous": True, + "connectionProperties": { + "key": "service_account", + "value": service_account_email, + }, + } + } + ) + job_config_filled.labels = {"bigframes-api": "streaming_to_pubsub"} # begin the query job query_job = bq_client.query( diff --git a/scripts/create_bigtable.py b/scripts/create_bigtable.py index 655e4b31ab2..f81bb8a0138 100644 --- a/scripts/create_bigtable.py +++ b/scripts/create_bigtable.py @@ -16,13 +16,10 @@ # bigframes.streaming testing if they don't already exist import os -import pathlib import sys import google.cloud.bigtable as bigtable -REPO_ROOT = pathlib.Path(__file__).parent.parent - PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") if not PROJECT_ID: diff --git a/scripts/create_pubsub.py b/scripts/create_pubsub.py new file mode 100644 index 00000000000..5d253989833 --- /dev/null +++ b/scripts/create_pubsub.py @@ -0,0 +1,49 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# This script create the bigtable resources required for +# bigframes.streaming testing if they don't already exist + +import os +import sys + +from google.cloud import pubsub_v1 + +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") + +if not PROJECT_ID: + print( + "Please set GOOGLE_CLOUD_PROJECT environment variable before running.", + file=sys.stderr, + ) + sys.exit(1) + + +def create_topic(topic_id): + # based on + # https://cloud.google.com/pubsub/docs/samples/pubsub-quickstart-create-topic?hl=en + + publisher = pubsub_v1.PublisherClient() + topic_path = publisher.topic_path(PROJECT_ID, topic_id) + + topic = publisher.create_topic(request={"name": topic_path}) + print(f"Created topic: {topic.name}") + + +def main(): + create_topic("penguins") + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index dbd9ce5fc27..79baf1fb23a 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ "geopandas >=0.12.2", "google-auth >=2.15.0,<3.0dev", "google-cloud-bigtable >=2.24.0", + "google-cloud-pubsub >=2.21.4", "google-cloud-bigquery[bqstorage,pandas] >=3.16.0", "google-cloud-functions >=1.12.0", "google-cloud-bigquery-connection >=1.12.0", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index bbd7bf0069b..5a766985766 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -5,6 +5,7 @@ gcsfs==2023.3.0 geopandas==0.12.2 google-auth==2.15.0 google-cloud-bigtable==2.24.0 +google-cloud-pubsub==2.21.4 google-cloud-bigquery==3.16.0 google-cloud-functions==1.12.0 google-cloud-bigquery-connection==1.12.0 diff --git a/tests/system/large/test_streaming.py b/tests/system/large/test_streaming.py index 48db61e5bf1..c125fde15ab 100644 --- a/tests/system/large/test_streaming.py +++ b/tests/system/large/test_streaming.py @@ -22,11 +22,12 @@ def test_streaming_to_bigtable(): job_id_prefix = "test_streaming_" sql = """SELECT body_mass_g, island as rowkey - FROM birds.penguins""" + FROM birds.penguins_bigtable_streaming""" query_job = bigframes.streaming.to_bigtable( sql, - "streaming-testing-instance", - "table-testing", + instance="streaming-testing-instance", + table="table-testing", + service_account_email="streaming-testing@bigframes-load-testing.iam.gserviceaccount.com", app_profile=None, truncate=True, overwrite=True, @@ -46,3 +47,29 @@ def test_streaming_to_bigtable(): assert str(query_job.job_id).startswith(job_id_prefix) finally: query_job.cancel() + + +def test_streaming_to_pubsub(): + # launch a continuous query + job_id_prefix = "test_streaming_pubsub_" + sql = """SELECT + island + FROM birds.penguins_pubsub_streaming""" + query_job = bigframes.streaming.to_pubsub( + sql, + topic="penguins", + service_account_email="streaming-testing@bigframes-load-testing.iam.gserviceaccount.com", + job_id=None, + job_id_prefix=job_id_prefix, + ) + + try: + # wait 100 seconds in order to ensure the query doesn't stop + # (i.e. it is continuous) + time.sleep(100) + assert query_job.error_result is None + assert query_job.errors is None + assert query_job.running() + assert str(query_job.job_id).startswith(job_id_prefix) + finally: + query_job.cancel() From 014765c22410a0b4559896d163c440f46f7ce98f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 27 Jun 2024 02:28:27 +0000 Subject: [PATCH 679/921] feat: expose gcf memory param in `remote_function` (#803) * feat: expose gcf memory param in `remote_function` * add the reported broken usecase as a test * fix mypy failure * revert test that is prone to timing out during deployment --- bigframes/functions/remote_function.py | 17 +++++- bigframes/pandas/__init__.py | 2 + bigframes/session/__init__.py | 11 ++++ tests/system/large/test_remote_function.py | 60 ++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index bc352322efa..c1878b6c31b 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -331,6 +331,7 @@ def create_cloud_function( max_instance_count=None, is_row_processor=False, vpc_connector=None, + memory_mib=1024, ): """Create a cloud function from the given user defined function. @@ -410,7 +411,8 @@ def create_cloud_function( self._cloud_function_docker_repository ) function.service_config = functions_v2.ServiceConfig() - function.service_config.available_memory = "1024M" + if memory_mib is not None: + function.service_config.available_memory = f"{memory_mib}Mi" if timeout_seconds is not None: if timeout_seconds > 1200: raise ValueError( @@ -473,6 +475,7 @@ def provision_bq_remote_function( cloud_function_max_instance_count, is_row_processor, cloud_function_vpc_connector, + cloud_function_memory_mib, ): """Provision a BigQuery remote function.""" # If reuse of any existing function with the same name (indicated by the @@ -504,6 +507,7 @@ def provision_bq_remote_function( max_instance_count=cloud_function_max_instance_count, is_row_processor=is_row_processor, vpc_connector=cloud_function_vpc_connector, + memory_mib=cloud_function_memory_mib, ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") @@ -667,6 +671,7 @@ def remote_function( cloud_function_timeout: Optional[int] = 600, cloud_function_max_instances: Optional[int] = None, cloud_function_vpc_connector: Optional[str] = None, + cloud_function_memory_mib: Optional[int] = 1024, ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -817,6 +822,15 @@ def remote_function( function. This is useful if your code needs access to data or service(s) that are on a VPC network. See for more details https://cloud.google.com/functions/docs/networking/connecting-vpc. + cloud_function_memory_mib (int, Optional): + The amounts of memory (in mebibytes) to allocate for the cloud + function (2nd gen) created. This also dictates a corresponding + amount of allocated CPU for the function. By default a memory of + 1024 MiB is set for the cloud functions created to support + BigQuery DataFrames remote function. If you want to let the + default memory of cloud functions be allocated, pass `None`. See + for more details + https://cloud.google.com/functions/docs/configuring/memory. """ # Some defaults may be used from the session if not provided otherwise import bigframes.exceptions as bf_exceptions @@ -1027,6 +1041,7 @@ def try_delattr(attr): cloud_function_max_instance_count=cloud_function_max_instances, is_row_processor=is_row_processor, cloud_function_vpc_connector=cloud_function_vpc_connector, + cloud_function_memory_mib=cloud_function_memory_mib, ) # TODO: Move ibis logic to compiler step diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index f6f9aec8007..faba0f3aa38 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -666,6 +666,7 @@ def remote_function( cloud_function_timeout: Optional[int] = 600, cloud_function_max_instances: Optional[int] = None, cloud_function_vpc_connector: Optional[str] = None, + cloud_function_memory_mib: Optional[int] = 1024, ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -683,6 +684,7 @@ def remote_function( cloud_function_timeout=cloud_function_timeout, cloud_function_max_instances=cloud_function_max_instances, cloud_function_vpc_connector=cloud_function_vpc_connector, + cloud_function_memory_mib=cloud_function_memory_mib, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index a4c926de723..3aba3581aac 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1537,6 +1537,7 @@ def remote_function( cloud_function_timeout: Optional[int] = 600, cloud_function_max_instances: Optional[int] = None, cloud_function_vpc_connector: Optional[str] = None, + cloud_function_memory_mib: Optional[int] = 1024, ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1670,6 +1671,15 @@ def remote_function( function. This is useful if your code needs access to data or service(s) that are on a VPC network. See for more details https://cloud.google.com/functions/docs/networking/connecting-vpc. + cloud_function_memory_mib (int, Optional): + The amounts of memory (in mebibytes) to allocate for the cloud + function (2nd gen) created. This also dictates a corresponding + amount of allocated CPU for the function. By default a memory of + 1024 MiB is set for the cloud functions created to support + BigQuery DataFrames remote function. If you want to let the + default memory of cloud functions be allocated, pass `None`. See + for more details + https://cloud.google.com/functions/docs/configuring/memory. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1695,6 +1705,7 @@ def remote_function( cloud_function_timeout=cloud_function_timeout, cloud_function_max_instances=cloud_function_max_instances, cloud_function_vpc_connector=cloud_function_vpc_connector, + cloud_function_memory_mib=cloud_function_memory_mib, ) def read_gbq_function( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 3f4bfea97e5..ef8b9811dfa 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1800,3 +1800,63 @@ def float_parser(row): cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, float_parser_remote ) + + +@pytest.mark.parametrize( + ("memory_mib_args", "expected_memory"), + [ + pytest.param({}, "1024Mi", id="no-set"), + pytest.param({"cloud_function_memory_mib": None}, "256M", id="set-None"), + pytest.param({"cloud_function_memory_mib": 128}, "128Mi", id="set-128"), + pytest.param({"cloud_function_memory_mib": 1024}, "1024Mi", id="set-1024"), + pytest.param({"cloud_function_memory_mib": 4096}, "4096Mi", id="set-4096"), + pytest.param({"cloud_function_memory_mib": 32768}, "32768Mi", id="set-32768"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_gcf_memory( + session, scalars_dfs, memory_mib_args, expected_memory +): + try: + + def square(x: int) -> int: + return x * x + + square_remote = session.remote_function(reuse=False, **memory_mib_args)(square) + + # Assert that the GCF is created with the intended memory + gcf = session.cloudfunctionsclient.get_function( + name=square_remote.bigframes_cloud_function + ) + assert gcf.service_config.available_memory == expected_memory + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["int64_too"].apply(square_remote).to_pandas() + pd_result = scalars_pandas_df["int64_too"].apply(square) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square_remote + ) + + +@pytest.mark.parametrize( + ("memory_mib",), + [ + pytest.param(127, id="127-too-low"), + pytest.param(32769, id="set-32769-too-high"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_gcf_memory_unsupported(session, memory_mib): + with pytest.raises( + google.api_core.exceptions.InvalidArgument, + match="Invalid value specified for container memory", + ): + + @session.remote_function(reuse=False, cloud_function_memory_mib=memory_mib) + def square(x: int) -> int: + return x * x From 3da559e2975a222326a00dfeb53ec5e4051ebe03 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:36:04 -0700 Subject: [PATCH 680/921] chore: add groupby benchmark q10, update q8 (#809) * chore: add groupby benchmark q10, update q8 * update code --- .../db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py | 16 ++++++++++++++++ .../db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py | 1 + 2 files changed, 17 insertions(+) create mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py new file mode 100644 index 00000000000..83d5d4ee147 --- /dev/null +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes.pandas as bpd + +print("Groupby benchmark 10: sum v3 count by id1:id6") + +x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") + +ans = x.groupby( + ["id1", "id2", "id3", "id4", "id5", "id6"], as_index=False, dropna=False +).agg({"v3": "sum", "v1": "size"}) +print(ans.shape) +chk = [ans["v3"].sum(), ans["v1"].sum()] +print(chk) + +bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py index 4bbad0048f2..7a57d03efed 100644 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py +++ b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py @@ -12,6 +12,7 @@ .groupby("id6", as_index=False, dropna=False) .head(2) ) +ans = ans.reset_index(drop=True) print(ans.shape) chk = [ans["v3"].sum()] print(chk) From 1e3feda9e8fe9d08a0e3838066f6414f8015197d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 27 Jun 2024 18:06:59 -0500 Subject: [PATCH 681/921] feat: add `DataFrame.to_arrow` to create Arrow Table from DataFrame (#807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add `DataFrame.to_arrow` to create Arrow Table from DataFrame * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * type annotation for sample * align index names in to_arrow * better assertions --------- Co-authored-by: Owl Bot --- bigframes/core/blocks.py | 30 ++ bigframes/dataframe.py | 29 ++ .../create_polars_df_with_to_arrow_test.py | 40 +++ samples/polars/noxfile.py | 292 ++++++++++++++++++ samples/polars/noxfile_config.py | 42 +++ samples/polars/requirements-test.txt | 3 + samples/polars/requirements.txt | 3 + tests/system/small/test_dataframe_io.py | 61 ++++ 8 files changed, 500 insertions(+) create mode 100644 samples/polars/create_polars_df_with_to_arrow_test.py create mode 100644 samples/polars/noxfile.py create mode 100644 samples/polars/noxfile_config.py create mode 100644 samples/polars/requirements-test.txt create mode 100644 samples/polars/requirements.txt diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index f40dfc0071c..379c6611791 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -467,6 +467,36 @@ def _validate_result_schema(self, result_df: pd.DataFrame): f"This error should only occur while testing. Ibis schema: {ibis_schema} does not match actual schema: {actual_schema}" ) + def to_arrow( + self, + *, + ordered: bool = True, + ) -> Tuple[pa.Table, bigquery.QueryJob]: + """Run query and download results as a pyarrow Table.""" + # pa.Table.from_pandas puts index columns last, so update the expression to match. + expr = self.expr.select_columns( + list(self.value_columns) + list(self.index_columns) + ) + + _, query_job = self.session._query_to_destination( + self.session._to_sql(expr, ordered=ordered), + list(self.index_columns), + api_name="cached", + do_clustering=False, + ) + results_iterator = query_job.result() + pa_table = results_iterator.to_arrow() + + pa_index_labels = [] + for index_level, index_label in enumerate(self._index_labels): + if isinstance(index_label, str): + pa_index_labels.append(index_label) + else: + pa_index_labels.append(f"__index_level_{index_level}__") + + pa_table = pa_table.rename_columns(list(self.column_labels) + pa_index_labels) + return pa_table, query_job + def to_pandas( self, max_download_size: Optional[int] = None, diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 75420ca9575..274e176dd53 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -44,6 +44,7 @@ import numpy import pandas import pandas.io.formats.format +import pyarrow import tabulate import bigframes @@ -1183,6 +1184,34 @@ def cov(self, *, numeric_only: bool = False) -> DataFrame: return DataFrame(frame._block.calculate_pairwise_metric(agg_ops.CovOp())) + def to_arrow( + self, + *, + ordered: Optional[bool] = None, + ) -> pyarrow.Table: + """Write DataFrame to an Arrow table / record batch. + + Args: + ordered (bool, default None): + Determines whether the resulting Arrow table will be deterministically ordered. + In some cases, unordered may result in a faster-executing query. If set to a value + other than None, will override Session default. + + Returns: + pyarrow.Table: A pyarrow Table with all rows and columns of this DataFrame. + """ + warnings.warn( + "to_arrow is in preview. Types and unnamed / duplicate name columns may change in future.", + category=bigframes.exceptions.PreviewWarning, + ) + + self._optimize_query_complexity() + pa_table, query_job = self._block.to_arrow( + ordered=ordered if ordered is not None else self._session._strictly_ordered, + ) + self._set_internal_query_job(query_job) + return pa_table + def to_pandas( self, max_download_size: Optional[int] = None, diff --git a/samples/polars/create_polars_df_with_to_arrow_test.py b/samples/polars/create_polars_df_with_to_arrow_test.py new file mode 100644 index 00000000000..acb79f23c82 --- /dev/null +++ b/samples/polars/create_polars_df_with_to_arrow_test.py @@ -0,0 +1,40 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_create_polars_df() -> None: + # [START bigquery_dataframes_to_polars] + import polars + + import bigframes.enums + import bigframes.pandas as bpd + + bf_df = bpd.read_gbq_table( + "bigquery-public-data.usa_names.usa_1910_current", + # Setting index_col to either a unique column or NULL will give the + # best performance. + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + # TODO(developer): Do some analysis using BigQuery DataFrames. + # ... + + # Run the query and download the results as an Arrow table to convert into + # a Polars DataFrame. Use ordered=False if your polars analysis is OK with + # non-deterministic ordering. + arrow_table = bf_df.to_arrow(ordered=False) + polars_df = polars.from_arrow(arrow_table) + # [END bigquery_dataframes_to_polars] + + assert polars_df.shape == bf_df.shape + assert polars_df["number"].sum() == bf_df["number"].sum() diff --git a/samples/polars/noxfile.py b/samples/polars/noxfile.py new file mode 100644 index 00000000000..c36d5f2d81f --- /dev/null +++ b/samples/polars/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) + elif "pytest-xdist" in packages: + concurrent_args.extend(["-n", "auto"]) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """Returns the root folder of the project.""" + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/samples/polars/noxfile_config.py b/samples/polars/noxfile_config.py new file mode 100644 index 00000000000..91238e9e2ff --- /dev/null +++ b/samples/polars/noxfile_config.py @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be inported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7", "3.7", "3.8"], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": True, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # "gcloud_project_env": "BUILD_SPECIFIC_GCLOUD_PROJECT", + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} diff --git a/samples/polars/requirements-test.txt b/samples/polars/requirements-test.txt new file mode 100644 index 00000000000..beca2e44d9d --- /dev/null +++ b/samples/polars/requirements-test.txt @@ -0,0 +1,3 @@ +# samples/snippets should be runnable with no "extras" +google-cloud-testutils==1.4.0 +pytest==8.2.0 diff --git a/samples/polars/requirements.txt b/samples/polars/requirements.txt new file mode 100644 index 00000000000..e3f886e7e32 --- /dev/null +++ b/samples/polars/requirements.txt @@ -0,0 +1,3 @@ +bigframes==1.6.0 +polars==0.20.31 +pyarrow==15.0.0 diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 8adbea88e4f..ab1fdceae57 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -132,6 +132,67 @@ def test_sql_executes_and_includes_named_multiindex( ) +def test_to_arrow(scalars_df_default_index, scalars_pandas_df_default_index): + """Verify to_arrow() APIs returns the expected data.""" + expected = pa.Table.from_pandas( + scalars_pandas_df_default_index.drop(columns=["geography_col"]) + ) + + with pytest.warns( + bigframes.exceptions.PreviewWarning, + match="to_arrow", + ): + actual = scalars_df_default_index.drop(columns=["geography_col"]).to_arrow() + + # Make string_col match type. Otherwise, pa.Table.from_pandas uses + # LargeStringArray. LargeStringArray is unnecessary because our strings are + # less than 2 GB. + expected = expected.set_column( + expected.column_names.index("string_col"), + pa.field("string_col", pa.string()), + expected["string_col"].cast(pa.string()), + ) + + # Note: the final .equals assertion covers all these checks, but these + # finer-grained assertions are easier to debug. + assert actual.column_names == expected.column_names + for column in actual.column_names: + assert actual[column].equals(expected[column]) + assert actual.equals(expected) + + +def test_to_arrow_multiindex(scalars_df_index, scalars_pandas_df_index): + scalars_df_multiindex = scalars_df_index.set_index(["string_col", "int64_col"]) + scalars_pandas_df_multiindex = scalars_pandas_df_index.set_index( + ["string_col", "int64_col"] + ) + expected = pa.Table.from_pandas( + scalars_pandas_df_multiindex.drop(columns=["geography_col"]) + ) + + with pytest.warns( + bigframes.exceptions.PreviewWarning, + match="to_arrow", + ): + actual = scalars_df_multiindex.drop(columns=["geography_col"]).to_arrow() + + # Make string_col match type. Otherwise, pa.Table.from_pandas uses + # LargeStringArray. LargeStringArray is unnecessary because our strings are + # less than 2 GB. + expected = expected.set_column( + expected.column_names.index("string_col"), + pa.field("string_col", pa.string()), + expected["string_col"].cast(pa.string()), + ) + + # Note: the final .equals assertion covers all these checks, but these + # finer-grained assertions are easier to debug. + assert actual.column_names == expected.column_names + for column in actual.column_names: + assert actual[column].equals(expected[column]) + assert actual.equals(expected) + + def test_to_pandas_w_correct_dtypes(scalars_df_default_index): """Verify to_pandas() APIs returns the expected dtypes.""" actual = scalars_df_default_index.to_pandas().dtypes From ec5b06856762e67060642cc106f43f0d202a08bf Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 28 Jun 2024 08:57:38 -0700 Subject: [PATCH 682/921] refactor: Decorate api methods that require total ordering (#802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: label all apis that use total ordering * reorganize flags * fix config reference and add another test * fix broken attribute reference * add _session property to groupby objects * restore session_tokyo fixture * add docstring for OrderRequiredError * add _session property to index object * handled methods where only some parameterizations need total order * fix inverted validation * import validations module and not functions * mark some analytic ops as order independent * rename validate.py to validations.py * docstring for WindowSpec.row_bounded * Apply suggestions from code review --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/core/__init__.py | 10 +++--- bigframes/core/groupby/__init__.py | 26 ++++++++++++++ bigframes/core/indexes/base.py | 11 ++++++ bigframes/core/validations.py | 51 ++++++++++++++++++++++++++++ bigframes/core/window_spec.py | 10 ++++++ bigframes/dataframe.py | 35 +++++++++++++++++++ bigframes/exceptions.py | 4 +++ bigframes/operations/aggregations.py | 34 +++++++++++++++++++ bigframes/series.py | 35 +++++++++++++++++++ tests/system/conftest.py | 4 +-- tests/system/small/test_unordered.py | 51 ++++++++++++++++++++++++++++ 11 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 bigframes/core/validations.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 89ef5f525ed..00a36b9c051 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -344,10 +344,12 @@ def project_window_op( never_skip_nulls: will disable null skipping for operators that would otherwise do so skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection """ - if not self.session._strictly_ordered: - # TODO: Support unbounded windows with aggregate ops and some row-order-independent analytic ops - # TODO: Support non-deterministic windowing - raise ValueError("Windowed ops not supported in unordered mode") + # TODO: Support non-deterministic windowing + if window_spec.row_bounded or not op.order_independent: + if not self.session._strictly_ordered: + raise ValueError( + "Order-dependent windowed ops not supported in unordered mode" + ) return ArrayValue( nodes.WindowOpNode( child=self.node, diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 2b23ccf0e4c..11a5d43ba0e 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -27,6 +27,7 @@ import bigframes.core.blocks as blocks import bigframes.core.ordering as order import bigframes.core.utils as utils +import bigframes.core.validations as validations import bigframes.core.window as windows import bigframes.core.window_spec as window_specs import bigframes.dataframe as df @@ -72,6 +73,10 @@ def __init__( if col_id not in self._by_col_ids ] + @property + def _session(self) -> core.Session: + return self._block.session + def __getitem__( self, key: typing.Union[ @@ -229,20 +234,25 @@ def count(self) -> df.DataFrame: def nunique(self) -> df.DataFrame: return self._aggregate_all(agg_ops.nunique_op) + @validations.requires_strict_ordering() def cumsum(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("cumsum") return self._apply_window_op(agg_ops.sum_op, numeric_only=True) + @validations.requires_strict_ordering() def cummin(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.min_op, numeric_only=numeric_only) + @validations.requires_strict_ordering() def cummax(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.max_op, numeric_only=numeric_only) + @validations.requires_strict_ordering() def cumprod(self, *args, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.product_op, numeric_only=True) + @validations.requires_strict_ordering() def shift(self, periods=1) -> series.Series: window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), @@ -251,6 +261,7 @@ def shift(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) + @validations.requires_strict_ordering() def diff(self, periods=1) -> series.Series: window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), @@ -259,6 +270,7 @@ def diff(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.DiffOp(periods), window=window) + @validations.requires_strict_ordering() 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( @@ -274,6 +286,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: block, window_spec, self._selected_cols, drop_null_groups=self._dropna ) + @validations.requires_strict_ordering() def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = window_specs.cumulative_rows( grouping_keys=tuple(self._by_col_ids), @@ -514,6 +527,10 @@ def __init__( self._value_name = value_name self._dropna = dropna # Applies to aggregations but not windowing + @property + def _session(self) -> core.Session: + return self._block.session + def head(self, n: int = 5) -> series.Series: block = self._block if self._dropna: @@ -631,26 +648,31 @@ def agg(self, func=None) -> typing.Union[df.DataFrame, series.Series]: aggregate = agg + @validations.requires_strict_ordering() def cumsum(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.sum_op, ) + @validations.requires_strict_ordering() def cumprod(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.product_op, ) + @validations.requires_strict_ordering() def cummax(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.max_op, ) + @validations.requires_strict_ordering() def cummin(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.min_op, ) + @validations.requires_strict_ordering() def cumcount(self, *args, **kwargs) -> series.Series: return ( self._apply_window_op( @@ -660,6 +682,7 @@ def cumcount(self, *args, **kwargs) -> series.Series: - 1 ) + @validations.requires_strict_ordering() def shift(self, periods=1) -> series.Series: """Shift index by desired number of periods.""" window = window_specs.rows( @@ -669,6 +692,7 @@ def shift(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) + @validations.requires_strict_ordering() def diff(self, periods=1) -> series.Series: window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), @@ -677,6 +701,7 @@ def diff(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.DiffOp(periods), window=window) + @validations.requires_strict_ordering() 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( @@ -696,6 +721,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: is_series=True, ) + @validations.requires_strict_ordering() def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = window_specs.cumulative_rows( grouping_keys=tuple(self._by_col_ids), diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index cfb22929c88..696742180bd 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -30,6 +30,7 @@ import bigframes.core.expression as ex import bigframes.core.ordering as order import bigframes.core.utils as utils +import bigframes.core.validations as validations import bigframes.dtypes import bigframes.formatting_helpers as formatter import bigframes.operations as ops @@ -114,6 +115,10 @@ def from_frame( index._linked_frame = frame return index + @property + def _session(self): + return self._block.session + @property def name(self) -> blocks.Label: names = self.names @@ -179,6 +184,7 @@ def empty(self) -> bool: return self.shape[0] == 0 @property + @validations.requires_strict_ordering() def is_monotonic_increasing(self) -> bool: """ Return a boolean if the values are equal or increasing. @@ -192,6 +198,7 @@ def is_monotonic_increasing(self) -> bool: ) @property + @validations.requires_strict_ordering() def is_monotonic_decreasing(self) -> bool: """ Return a boolean if the values are equal or decreasing. @@ -341,6 +348,7 @@ def max(self) -> typing.Any: def min(self) -> typing.Any: return self._apply_aggregation(agg_ops.min_op) + @validations.requires_strict_ordering() def argmax(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( @@ -353,6 +361,7 @@ def argmax(self) -> int: return typing.cast(int, series.Series(block.select_column(row_nums)).iloc[0]) + @validations.requires_strict_ordering() def argmin(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( @@ -424,6 +433,8 @@ def dropna(self, how: typing.Literal["all", "any"] = "any") -> Index: return Index(result) def drop_duplicates(self, *, keep: str = "first") -> Index: + if keep is not False: + validations.enforce_ordered(self, "drop_duplicates") block = block_ops.drop_duplicates(self._block, self._block.index_columns, keep) return Index(block) diff --git a/bigframes/core/validations.py b/bigframes/core/validations.py new file mode 100644 index 00000000000..dc22047e3bb --- /dev/null +++ b/bigframes/core/validations.py @@ -0,0 +1,51 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DataFrame is a two dimensional data structure.""" + +from __future__ import annotations + +import functools +from typing import Protocol, TYPE_CHECKING + +import bigframes.constants +import bigframes.exceptions + +if TYPE_CHECKING: + from bigframes import Session + + +class HasSession(Protocol): + @property + def _session(self) -> Session: + ... + + +def requires_strict_ordering(): + def decorator(meth): + @functools.wraps(meth) + def guarded_meth(object: HasSession, *args, **kwargs): + enforce_ordered(object, meth.__name__) + return meth(object, *args, **kwargs) + + return guarded_meth + + return decorator + + +def enforce_ordered(object: HasSession, opname: str) -> None: + if not object._session._strictly_ordered: + raise bigframes.exceptions.OrderRequiredError( + f"Op {opname} not supported when strict ordering is disabled. {bigframes.constants.FEEDBACK_LINK}" + ) diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py index 71e88a4c3d6..57c57b451a7 100644 --- a/bigframes/core/window_spec.py +++ b/bigframes/core/window_spec.py @@ -152,3 +152,13 @@ class WindowSpec: ordering: Tuple[orderings.OrderingExpression, ...] = tuple() bounds: Union[RowsWindowBounds, RangeWindowBounds, None] = None min_periods: int = 0 + + @property + def row_bounded(self): + """ + Whether the window is bounded by row offsets. + + This is relevant for determining whether the window requires a total order + to calculate deterministically. + """ + return isinstance(self.bounds, RowsWindowBounds) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 274e176dd53..4dcc4414eda 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -62,6 +62,7 @@ import bigframes.core.indexes as indexes import bigframes.core.ordering as order import bigframes.core.utils as utils +import bigframes.core.validations as validations import bigframes.core.window import bigframes.core.window_spec as window_spec import bigframes.dtypes @@ -278,10 +279,12 @@ def loc(self) -> indexers.LocDataFrameIndexer: return indexers.LocDataFrameIndexer(self) @property + @validations.requires_strict_ordering() def iloc(self) -> indexers.ILocDataFrameIndexer: return indexers.ILocDataFrameIndexer(self) @property + @validations.requires_strict_ordering() def iat(self) -> indexers.IatDataFrameIndexer: return indexers.IatDataFrameIndexer(self) @@ -338,10 +341,12 @@ def _has_index(self) -> bool: return len(self._block.index_columns) > 0 @property + @validations.requires_strict_ordering() def T(self) -> DataFrame: return DataFrame(self._get_block().transpose()) @requires_index + @validations.requires_strict_ordering() def transpose(self) -> DataFrame: return self.T @@ -1293,6 +1298,7 @@ def copy(self) -> DataFrame: def head(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[:n]) + @validations.requires_strict_ordering() def tail(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[-n:]) @@ -1335,6 +1341,8 @@ def nlargest( ) -> DataFrame: if keep not in ("first", "last", "all"): raise ValueError("'keep must be one of 'first', 'last', or 'all'") + if keep != "all": + validations.enforce_ordered(self, "nlargest") column_ids = self._sql_names(columns) return DataFrame(block_ops.nlargest(self._block, n, column_ids, keep=keep)) @@ -1346,6 +1354,8 @@ def nsmallest( ) -> DataFrame: if keep not in ("first", "last", "all"): raise ValueError("'keep must be one of 'first', 'last', or 'all'") + if keep != "all": + validations.enforce_ordered(self, "nlargest") column_ids = self._sql_names(columns) return DataFrame(block_ops.nsmallest(self._block, n, column_ids, keep=keep)) @@ -1528,6 +1538,7 @@ def rename_axis( labels = [mapper] return DataFrame(self._block.with_index_labels(labels)) + @validations.requires_strict_ordering() def equals(self, other: typing.Union[bigframes.series.Series, DataFrame]) -> bool: # Must be same object type, same column dtypes, and same label values if not isinstance(other, DataFrame): @@ -1925,6 +1936,7 @@ def _reindex_columns(self, columns): def reindex_like(self, other: DataFrame, *, validate: typing.Optional[bool] = None): return self.reindex(index=other.index, columns=other.columns, validate=validate) + @validations.requires_strict_ordering() @requires_index def interpolate(self, method: str = "linear") -> DataFrame: if method == "pad": @@ -1950,10 +1962,12 @@ def replace( lambda x: x.replace(to_replace=to_replace, value=value, regex=regex) ) + @validations.requires_strict_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> DataFrame: window = window_spec.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) + @validations.requires_strict_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> DataFrame: window = window_spec.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) @@ -2219,13 +2233,16 @@ def agg( aggregate.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.agg) @requires_index + @validations.requires_strict_ordering() def idxmin(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmin(self._block)) @requires_index + @validations.requires_strict_ordering() def idxmax(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmax(self._block)) + @validations.requires_strict_ordering() def melt( self, id_vars: typing.Optional[typing.Iterable[typing.Hashable]] = None, @@ -2330,6 +2347,7 @@ def _pivot( return DataFrame(pivot_block) @requires_index + @validations.requires_strict_ordering() def pivot( self, *, @@ -2344,6 +2362,7 @@ def pivot( return self._pivot(columns=columns, index=index, values=values) @requires_index + @validations.requires_strict_ordering() def pivot_table( self, values: typing.Optional[ @@ -2443,6 +2462,7 @@ def _stack_multi(self, level: LevelsType = -1): return DataFrame(block) @requires_index + @validations.requires_strict_ordering() def unstack(self, level: LevelsType = -1): if not utils.is_list_like(level): level = [level] @@ -2653,6 +2673,7 @@ def _perform_join_by_index( block, _ = self._block.join(other._block, how=how, block_identity_join=True) return DataFrame(block) + @validations.requires_strict_ordering() 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 = window_spec.rows( @@ -2662,6 +2683,7 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window self._block, window_def, self._block.value_columns ) + @validations.requires_strict_ordering() def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: window = window_spec.cumulative_rows(min_periods=min_periods) return bigframes.core.window.Window( @@ -2764,6 +2786,7 @@ def notna(self) -> DataFrame: notnull = notna notnull.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.notna) + @validations.requires_strict_ordering() def cumsum(self): is_numeric_types = [ (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) @@ -2776,6 +2799,7 @@ def cumsum(self): window_spec.cumulative_rows(), ) + @validations.requires_strict_ordering() def cumprod(self) -> DataFrame: is_numeric_types = [ (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) @@ -2788,18 +2812,21 @@ def cumprod(self) -> DataFrame: window_spec.cumulative_rows(), ) + @validations.requires_strict_ordering() def cummin(self) -> DataFrame: return self._apply_window_op( agg_ops.min_op, window_spec.cumulative_rows(), ) + @validations.requires_strict_ordering() def cummax(self) -> DataFrame: return self._apply_window_op( agg_ops.max_op, window_spec.cumulative_rows(), ) + @validations.requires_strict_ordering() def shift(self, periods: int = 1) -> DataFrame: window = window_spec.rows( preceding=periods if periods > 0 else None, @@ -2807,6 +2834,7 @@ def shift(self, periods: int = 1) -> DataFrame: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) + @validations.requires_strict_ordering() def diff(self, periods: int = 1) -> DataFrame: window = window_spec.rows( preceding=periods if periods > 0 else None, @@ -2814,6 +2842,7 @@ def diff(self, periods: int = 1) -> DataFrame: ) return self._apply_window_op(agg_ops.DiffOp(periods), window) + @validations.requires_strict_ordering() def pct_change(self, periods: int = 1) -> DataFrame: # Future versions of pandas will not perfrom ffill automatically df = self.ffill() @@ -2831,6 +2860,7 @@ def _apply_window_op( ) return DataFrame(block.select_columns(result_ids)) + @validations.requires_strict_ordering() def sample( self, n: Optional[int] = None, @@ -3486,6 +3516,8 @@ def drop_duplicates( *, keep: str = "first", ) -> DataFrame: + if keep is not False: + validations.enforce_ordered(self, "drop_duplicates(keep != False)") if subset is None: column_ids = self._block.value_columns elif utils.is_list_like(subset): @@ -3499,6 +3531,8 @@ def drop_duplicates( return DataFrame(block) def duplicated(self, subset=None, keep: str = "first") -> bigframes.series.Series: + if keep is not False: + validations.enforce_ordered(self, "duplicated(keep != False)") if subset is None: column_ids = self._block.value_columns else: @@ -3592,6 +3626,7 @@ def _optimize_query_complexity(self): _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") + @validations.requires_strict_ordering() def dot(self, other: _DataFrameOrSeries) -> _DataFrameOrSeries: if not isinstance(other, (DataFrame, bf_series.Series)): raise NotImplementedError( diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index bae239b6dab..bc0d83b4f67 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -47,5 +47,9 @@ class NullIndexError(ValueError): """Object has no index.""" +class OrderRequiredError(ValueError): + """Operation requires total row ordering to be enabled.""" + + class TimeTravelDisabledWarning(Warning): """A query was reattempted without time travel.""" diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 619183287fe..675ead11889 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -42,6 +42,15 @@ def uses_total_row_ordering(self): def can_order_by(self): return False + @property + def order_independent(self): + """ + True if the output of the operator does not depend on the ordering of input rows. + + Navigation functions are a notable case that are not order independent. + """ + return False + @abc.abstractmethod def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: ... @@ -78,6 +87,15 @@ def name(self) -> str: def arguments(self) -> int: ... + @property + def order_independent(self): + """ + True if results don't depend on the order of the input. + + Almost all aggregation functions are order independent, excepting ``array_agg`` and ``string_agg``. + """ + return not self.can_order_by + @dataclasses.dataclass(frozen=True) class NullaryAggregateOp(AggregateOp, NullaryWindowOp): @@ -294,6 +312,10 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT ) return pd.ArrowDtype(pa_type) + @property + def order_independent(self): + return True + @dataclasses.dataclass(frozen=True) class QcutOp(UnaryWindowOp): @@ -312,6 +334,10 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" ).output_type(input_types[0]) + @property + def order_independent(self): + return True + @dataclasses.dataclass(frozen=True) class NuniqueOp(UnaryAggregateOp): @@ -349,6 +375,10 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" ).output_type(input_types[0]) + @property + def order_independent(self): + return True + @dataclasses.dataclass(frozen=True) class DenseRankOp(UnaryWindowOp): @@ -361,6 +391,10 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT dtypes.is_orderable, dtypes.INT_DTYPE, "orderable" ).output_type(input_types[0]) + @property + def order_independent(self): + return True + @dataclasses.dataclass(frozen=True) class FirstOp(UnaryWindowOp): diff --git a/bigframes/series.py b/bigframes/series.py index 33343211582..c325783e965 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -43,6 +43,7 @@ import bigframes.core.ordering as order import bigframes.core.scalar as scalars import bigframes.core.utils as utils +import bigframes.core.validations as validations import bigframes.core.window import bigframes.core.window_spec import bigframes.dataframe @@ -92,10 +93,12 @@ def loc(self) -> bigframes.core.indexers.LocSeriesIndexer: return bigframes.core.indexers.LocSeriesIndexer(self) @property + @validations.requires_strict_ordering() def iloc(self) -> bigframes.core.indexers.IlocSeriesIndexer: return bigframes.core.indexers.IlocSeriesIndexer(self) @property + @validations.requires_strict_ordering() def iat(self) -> bigframes.core.indexers.IatSeriesIndexer: return bigframes.core.indexers.IatSeriesIndexer(self) @@ -160,6 +163,7 @@ def struct(self) -> structs.StructAccessor: return structs.StructAccessor(self._block) @property + @validations.requires_strict_ordering() def T(self) -> Series: return self.transpose() @@ -171,6 +175,7 @@ def _info_axis(self) -> indexes.Index: def _session(self) -> bigframes.Session: return self._get_block().expr.session + @validations.requires_strict_ordering() def transpose(self) -> Series: return self @@ -266,6 +271,7 @@ def equals( return False return block_ops.equals(self._block, other._block) + @validations.requires_strict_ordering() def reset_index( self, *, @@ -454,11 +460,13 @@ def case_when(self, caselist) -> Series: ignore_self=True, ) + @validations.requires_strict_ordering() def cumsum(self) -> Series: return self._apply_window_op( agg_ops.sum_op, bigframes.core.window_spec.cumulative_rows() ) + @validations.requires_strict_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> Series: window = bigframes.core.window_spec.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) @@ -466,25 +474,30 @@ def ffill(self, *, limit: typing.Optional[int] = None) -> Series: pad = ffill pad.__doc__ = inspect.getdoc(vendored_pandas_series.Series.ffill) + @validations.requires_strict_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> Series: window = bigframes.core.window_spec.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) + @validations.requires_strict_ordering() def cummax(self) -> Series: return self._apply_window_op( agg_ops.max_op, bigframes.core.window_spec.cumulative_rows() ) + @validations.requires_strict_ordering() def cummin(self) -> Series: return self._apply_window_op( agg_ops.min_op, bigframes.core.window_spec.cumulative_rows() ) + @validations.requires_strict_ordering() def cumprod(self) -> Series: return self._apply_window_op( agg_ops.product_op, bigframes.core.window_spec.cumulative_rows() ) + @validations.requires_strict_ordering() def shift(self, periods: int = 1) -> Series: window = bigframes.core.window_spec.rows( preceding=periods if periods > 0 else None, @@ -492,6 +505,7 @@ def shift(self, periods: int = 1) -> Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) + @validations.requires_strict_ordering() def diff(self, periods: int = 1) -> Series: window = bigframes.core.window_spec.rows( preceding=periods if periods > 0 else None, @@ -499,11 +513,13 @@ def diff(self, periods: int = 1) -> Series: ) return self._apply_window_op(agg_ops.DiffOp(periods), window) + @validations.requires_strict_ordering() def pct_change(self, periods: int = 1) -> Series: # Future versions of pandas will not perfrom ffill automatically series = self.ffill() return Series(block_ops.pct_change(series._block, periods=periods)) + @validations.requires_strict_ordering() def rank( self, axis=0, @@ -595,6 +611,7 @@ def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): ) return Series(block.select_column(result)) + @validations.requires_strict_ordering() @requires_index def interpolate(self, method: str = "linear") -> Series: if method == "pad": @@ -617,9 +634,11 @@ def dropna( result = result.reset_index() return Series(result) + @validations.requires_strict_ordering() def head(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[0:n]) + @validations.requires_strict_ordering() def tail(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[-n:]) @@ -660,6 +679,8 @@ def peek(self, n: int = 5, *, force: bool = True) -> pandas.DataFrame: def nlargest(self, n: int = 5, keep: str = "first") -> Series: if keep not in ("first", "last", "all"): raise ValueError("'keep must be one of 'first', 'last', or 'all'") + if keep != "all": + validations.enforce_ordered(self, "nlargest(keep != 'all')") return Series( block_ops.nlargest(self._block, n, [self._value_column], keep=keep) ) @@ -667,6 +688,8 @@ def nlargest(self, n: int = 5, keep: str = "first") -> Series: def nsmallest(self, n: int = 5, keep: str = "first") -> Series: if keep not in ("first", "last", "all"): raise ValueError("'keep must be one of 'first', 'last', or 'all'") + if keep != "all": + validations.enforce_ordered(self, "nsmallest(keep != 'all')") return Series( block_ops.nsmallest(self._block, n, [self._value_column], keep=keep) ) @@ -1116,6 +1139,7 @@ def clip(self, lower, upper): ) return Series(block.select_column(result_id).with_column_labels([self.name])) + @validations.requires_strict_ordering() def argmax(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( @@ -1128,6 +1152,7 @@ def argmax(self) -> int: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) + @validations.requires_strict_ordering() def argmin(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( @@ -1193,12 +1218,14 @@ def idxmin(self) -> blocks.Label: return indexes.Index(block).to_pandas()[0] @property + @validations.requires_strict_ordering() def is_monotonic_increasing(self) -> bool: return typing.cast( bool, self._block.is_monotonic_increasing(self._value_column) ) @property + @validations.requires_strict_ordering() def is_monotonic_decreasing(self) -> bool: return typing.cast( bool, self._block.is_monotonic_decreasing(self._value_column) @@ -1306,6 +1333,7 @@ def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: block = block.order_by(ordering) return Series(block) + @validations.requires_strict_ordering() 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 = bigframes.core.window_spec.rows( @@ -1315,6 +1343,7 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window self._block, window_spec, self._block.value_columns, is_series=True ) + @validations.requires_strict_ordering() def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: window_spec = bigframes.core.window_spec.cumulative_rows( min_periods=min_periods @@ -1579,13 +1608,18 @@ def reindex_like(self, other: Series, *, validate: typing.Optional[bool] = None) return self.reindex(other.index, validate=validate) def drop_duplicates(self, *, keep: str = "first") -> Series: + if keep is not False: + validations.enforce_ordered(self, "drop_duplicates(keep != False)") block = block_ops.drop_duplicates(self._block, (self._value_column,), keep) return Series(block) + @validations.requires_strict_ordering() def unique(self) -> Series: return self.drop_duplicates() def duplicated(self, keep: str = "first") -> Series: + if keep is not False: + validations.enforce_ordered(self, "duplicated(keep != False)") block, indicator = block_ops.indicate_duplicates( self._block, (self._value_column,), keep ) @@ -1751,6 +1785,7 @@ def map( result_df = self_df.join(map_df, on="series") return result_df[self.name] + @validations.requires_strict_ordering() def sample( self, n: Optional[int] = None, diff --git a/tests/system/conftest.py b/tests/system/conftest.py index a41e6dc6b7a..df4ff9aff0e 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -149,9 +149,7 @@ def unordered_session() -> Generator[bigframes.Session, None, None]: @pytest.fixture(scope="session") def session_tokyo(tokyo_location: str) -> Generator[bigframes.Session, None, None]: - context = bigframes.BigQueryOptions( - location=tokyo_location, - ) + context = bigframes.BigQueryOptions(location=tokyo_location) session = bigframes.Session(context=context) yield session session.close() # close generated session at cleanup type diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index d555cedcc03..36bf2a25857 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -13,7 +13,9 @@ # limitations under the License. import pandas as pd import pyarrow as pa +import pytest +import bigframes.exceptions import bigframes.pandas as bpd from tests.system.utils import assert_pandas_df_equal, skip_legacy_pandas @@ -59,3 +61,52 @@ def test_unordered_mode_read_gbq(unordered_session): ) # Don't need ignore_order as there is only 1 row assert_pandas_df_equal(df.to_pandas(), expected) + + +@pytest.mark.parametrize( + ("keep"), + [ + pytest.param( + "first", + marks=pytest.mark.xfail(raises=bigframes.exceptions.OrderRequiredError), + ), + pytest.param( + False, + ), + ], +) +def test_unordered_drop_duplicates(unordered_session, keep): + pd_df = pd.DataFrame({"a": [1, 1, 3], "b": [4, 4, 6]}, dtype=pd.Int64Dtype()) + bf_df = bpd.DataFrame(pd_df, session=unordered_session) + + bf_result = bf_df.drop_duplicates(keep=keep) + pd_result = pd_df.drop_duplicates(keep=keep) + + assert_pandas_df_equal(bf_result.to_pandas(), pd_result, ignore_order=True) + + +@pytest.mark.parametrize( + ("function"), + [ + pytest.param( + lambda x: x.cumsum(), + id="cumsum", + ), + pytest.param( + lambda x: x.idxmin(), + id="idxmin", + ), + pytest.param( + lambda x: x.a.iloc[1::2], + id="series_iloc", + ), + ], +) +def test_unordered_mode_blocks_windowing(unordered_session, function): + pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session) + with pytest.raises( + bigframes.exceptions.OrderRequiredError, + match=r"Op.*not supported when strict ordering is disabled", + ): + function(df) From 57d98b9e3298583ec40c04665ab84e6ad2b948fb Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 28 Jun 2024 16:10:29 -0700 Subject: [PATCH 683/921] feat: add PolynomailFeatures to_gbq and pipeline support (#805) * feat: add ml.preprocessing.PolynomialFeatures class * feat: add PolynomailFeatures to_gbq and pipeline support * fix tests * fix tests --- bigframes/ml/base.py | 10 +++ bigframes/ml/compose.py | 60 +++++++++----- bigframes/ml/core.py | 9 ++ bigframes/ml/impute.py | 13 +-- bigframes/ml/pipeline.py | 1 + bigframes/ml/preprocessing.py | 83 +++++++------------ tests/system/large/ml/test_compose.py | 2 +- tests/system/large/ml/test_pipeline.py | 52 +++++++++++- tests/system/small/ml/test_preprocessing.py | 32 +++++++ .../sklearn/preprocessing/_polynomial.py | 7 +- 10 files changed, 182 insertions(+), 87 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 6c81b66e55d..70854a36e91 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -184,6 +184,16 @@ class BaseTransformer(BaseEstimator): def __init__(self): self._bqml_model: Optional[core.BqmlModel] = None + @abc.abstractmethod + def _keys(self): + pass + + def __eq__(self, other) -> bool: + return type(self) is type(other) and self._keys() == other._keys() + + def __hash__(self) -> int: + return hash(self._keys()) + _T = TypeVar("_T", bound="BaseTransformer") def to_gbq(self: _T, model_name: str, replace: bool = False) -> _T: diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index abf1a662b9d..7f1bfe8d550 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -21,7 +21,7 @@ import re import types import typing -from typing import cast, List, Optional, Tuple, Union +from typing import cast, Iterable, List, Optional, Set, Tuple, Union import bigframes_vendored.sklearn.compose._column_transformer from google.cloud import bigquery @@ -40,6 +40,7 @@ "ML.BUCKETIZE": preprocessing.KBinsDiscretizer, "ML.QUANTILE_BUCKETIZE": preprocessing.KBinsDiscretizer, "ML.LABEL_ENCODER": preprocessing.LabelEncoder, + "ML.POLYNOMIAL_EXPAND": preprocessing.PolynomialFeatures, "ML.IMPUTER": impute.SimpleImputer, } ) @@ -56,21 +57,24 @@ class ColumnTransformer( def __init__( self, - transformers: List[ + transformers: Iterable[ Tuple[ str, Union[preprocessing.PreprocessingType, impute.SimpleImputer], - Union[str, List[str]], + Union[str, Iterable[str]], ] ], ): # TODO: if any(transformers) has fitted raise warning - self.transformers = transformers + self.transformers = list(transformers) self._bqml_model: Optional[core.BqmlModel] = None self._bqml_model_factory = globals.bqml_model_factory() # call self.transformers_ to check chained transformers self.transformers_ + def _keys(self): + return (self.transformers, self._bqml_model) + @property def transformers_( self, @@ -107,13 +111,13 @@ def _extract_from_bq_model( """Extract transformers as ColumnTransformer obj from a BQ Model. Keep the _bqml_model field as None.""" assert "transformColumns" in bq_model._properties - transformers: List[ + transformers_set: Set[ Tuple[ str, Union[preprocessing.PreprocessingType, impute.SimpleImputer], Union[str, List[str]], ] - ] = [] + ] = set() def camel_to_snake(name): name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) @@ -134,7 +138,7 @@ def camel_to_snake(name): for prefix in _BQML_TRANSFROM_TYPE_MAPPING: if transform_sql.startswith(prefix): transformer_cls = _BQML_TRANSFROM_TYPE_MAPPING[prefix] - transformers.append( + transformers_set.add( ( camel_to_snake(transformer_cls.__name__), *transformer_cls._parse_from_sql(transform_sql), # type: ignore @@ -148,7 +152,7 @@ def camel_to_snake(name): f"Unsupported transformer type. {constants.FEEDBACK_LINK}" ) - transformer = cls(transformers=transformers) + transformer = cls(transformers=list(transformers_set)) transformer._output_names = output_names return transformer @@ -159,23 +163,37 @@ def _merge( ColumnTransformer, Union[preprocessing.PreprocessingType, impute.SimpleImputer] ]: """Try to merge the column transformer to a simple transformer. Depends on all the columns in bq_model are transformed with the same transformer.""" - transformers = self.transformers_ + transformers = self.transformers assert len(transformers) > 0 _, transformer_0, column_0 = transformers[0] + feature_columns_sorted = sorted( + [ + cast(str, feature_column.name) + for feature_column in bq_model.feature_columns + ] + ) + + if ( + len(transformers) == 1 + and isinstance(transformer_0, preprocessing.PolynomialFeatures) + and sorted(column_0) == feature_columns_sorted + ): + transformer_0._output_names = self._output_names + return transformer_0 + + if not isinstance(column_0, str): + return self columns = [column_0] for _, transformer, column in transformers[1:]: + if not isinstance(column, str): + return self # all transformers are the same if transformer != transformer_0: return self columns.append(column) # all feature columns are transformed - if sorted( - [ - cast(str, feature_column.name) - for feature_column in bq_model.feature_columns - ] - ) == sorted(columns): + if sorted(columns) == feature_columns_sorted: transformer_0._output_names = self._output_names return transformer_0 @@ -197,12 +215,12 @@ def _compile_to_sql( Returns: a list of tuples of (sql_expression, output_name)""" - return [ - transformer._compile_to_sql([column], X=X)[0] - for column in columns - for _, transformer, target_column in self.transformers_ - if column == target_column - ] + result = [] + for _, transformer, target_columns in self.transformers: + if isinstance(target_columns, str): + target_columns = [target_columns] + result += transformer._compile_to_sql(target_columns, X=X) + return result def fit( self, diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 168bc584f7e..ee4d8a8c27c 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -112,6 +112,15 @@ def __init__(self, session: bigframes.Session, model: bigquery.Model): self.model_name ) + def _keys(self): + return (self._session, self._model) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self._keys() == other._keys() + + def __hash__(self): + return hash(self._keys()) + @property def session(self) -> bigframes.Session: """Get the BigQuery DataFrames session that this BQML model wrapper is tied to""" diff --git a/bigframes/ml/impute.py b/bigframes/ml/impute.py index d21fcbb1ad1..ae71637aa56 100644 --- a/bigframes/ml/impute.py +++ b/bigframes/ml/impute.py @@ -18,7 +18,7 @@ from __future__ import annotations import typing -from typing import Any, List, Literal, Optional, Tuple, Union +from typing import Iterable, List, Literal, Optional, Tuple, Union import bigframes_vendored.sklearn.impute._base @@ -44,17 +44,12 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return ( - type(other) is SimpleImputer - and self.strategy == other.strategy - and self._bqml_model == other._bqml_model - ) + def _keys(self): + return (self._bqml_model, self.strategy) def _compile_to_sql( self, - columns: List[str], + columns: Iterable[str], X=None, ) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 03e5688453f..04b8d73cf53 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -64,6 +64,7 @@ def __init__(self, steps: List[Tuple[str, base.BaseEstimator]]): preprocessing.MinMaxScaler, preprocessing.KBinsDiscretizer, preprocessing.LabelEncoder, + preprocessing.PolynomialFeatures, impute.SimpleImputer, ), ): diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index f3621d3a33b..07fdc171cff 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -18,7 +18,7 @@ from __future__ import annotations import typing -from typing import Any, cast, List, Literal, Optional, Tuple, Union +from typing import cast, Iterable, List, Literal, Optional, Tuple, Union import bigframes_vendored.sklearn.preprocessing._data import bigframes_vendored.sklearn.preprocessing._discretization @@ -43,11 +43,10 @@ def __init__(self): self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return type(other) is StandardScaler and self._bqml_model == other._bqml_model + def _keys(self): + return (self._bqml_model,) - def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause @@ -125,11 +124,10 @@ def __init__(self): self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return type(other) is MaxAbsScaler and self._bqml_model == other._bqml_model + def _keys(self): + return (self._bqml_model,) - def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause @@ -207,11 +205,10 @@ def __init__(self): self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return type(other) is MinMaxScaler and self._bqml_model == other._bqml_model + def _keys(self): + return (self._bqml_model,) - def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause @@ -301,18 +298,12 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return ( - type(other) is KBinsDiscretizer - and self.n_bins == other.n_bins - and self.strategy == other.strategy - and self._bqml_model == other._bqml_model - ) + def _keys(self): + return (self._bqml_model, self.n_bins, self.strategy) def _compile_to_sql( self, - columns: List[str], + columns: Iterable[str], X: bpd.DataFrame, ) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in @@ -446,17 +437,10 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return ( - type(other) is OneHotEncoder - and self._bqml_model == other._bqml_model - and self.drop == other.drop - and self.min_frequency == other.min_frequency - and self.max_categories == other.max_categories - ) + def _keys(self): + return (self._bqml_model, self.drop, self.min_frequency, self.max_categories) - def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause @@ -572,16 +556,10 @@ def __init__( self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return ( - type(other) is LabelEncoder - and self._bqml_model == other._bqml_model - and self.min_frequency == other.min_frequency - and self.max_categories == other.max_categories - ) + def _keys(self): + return (self._bqml_model, self.min_frequency, self.max_categories) - def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause @@ -672,18 +650,17 @@ class PolynomialFeatures( ) def __init__(self, degree: int = 2): + if degree not in range(1, 5): + raise ValueError(f"degree has to be [1, 4], input is {degree}.") self.degree = degree self._bqml_model: Optional[core.BqmlModel] = None self._bqml_model_factory = globals.bqml_model_factory() self._base_sql_generator = globals.base_sql_generator() - # TODO(garrettwu): implement __hash__ - def __eq__(self, other: Any) -> bool: - return ( - type(other) is PolynomialFeatures and self._bqml_model == other._bqml_model - ) + def _keys(self): + return (self._bqml_model, self.degree) - def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause @@ -705,17 +682,18 @@ def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: ] @classmethod - def _parse_from_sql(cls, sql: str) -> tuple[PolynomialFeatures, str]: - """Parse SQL to tuple(PolynomialFeatures, column_label). + def _parse_from_sql(cls, sql: str) -> tuple[PolynomialFeatures, tuple[str, ...]]: + """Parse SQL to tuple(PolynomialFeatures, column_labels). Args: sql: SQL string of format "ML.POLYNOMIAL_EXPAND(STRUCT(col_label0, col_label1, ...), degree)" Returns: tuple(MaxAbsScaler, column_label)""" - col_label = sql[sql.find("STRUCT(") + 7 : sql.find(")")] + col_labels = sql[sql.find("STRUCT(") + 7 : sql.find(")")].split(",") + col_labels = [label.strip() for label in col_labels] degree = int(sql[sql.rfind(",") + 1 : sql.rfind(")")]) - return cls(degree), col_label + return cls(degree), tuple(col_labels) def fit( self, @@ -762,8 +740,6 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: df[self._output_names], ) - # TODO(garrettwu): to_gbq() - PreprocessingType = Union[ OneHotEncoder, @@ -772,4 +748,5 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: MinMaxScaler, KBinsDiscretizer, LabelEncoder, + PolynomialFeatures, ] diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 45322e78dd2..59c5a1538f3 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -123,7 +123,7 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): ("standard_scaler", preprocessing.StandardScaler(), "culmen_length_mm"), ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), ] - assert reloaded_transformer.transformers_ == expected + assert set(reloaded_transformer.transformers) == set(expected) assert reloaded_transformer._bqml_model is not None result = transformer.fit_transform( diff --git a/tests/system/large/ml/test_pipeline.py b/tests/system/large/ml/test_pipeline.py index 6e18248e0f6..84a6b11ff26 100644 --- a/tests/system/large/ml/test_pipeline.py +++ b/tests/system/large/ml/test_pipeline.py @@ -487,6 +487,11 @@ def test_pipeline_columntransformer_fit_predict(session, penguins_df_default_ind preprocessing.LabelEncoder(), "species", ), + ( + "poly_feats", + preprocessing.PolynomialFeatures(), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ), ), @@ -567,6 +572,11 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id impute.SimpleImputer(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "polynomial_features", + preprocessing.PolynomialFeatures(), + ["culmen_length_mm", "flipper_length_mm"], + ), ( "label", preprocessing.LabelEncoder(), @@ -589,7 +599,7 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id ) assert isinstance(pl_loaded._transform, compose.ColumnTransformer) - transformers = pl_loaded._transform.transformers_ + transformers = pl_loaded._transform.transformers expected = [ ( "one_hot_encoder", @@ -629,9 +639,14 @@ def test_pipeline_columntransformer_to_gbq(penguins_df_default_index, dataset_id impute.SimpleImputer(), "flipper_length_mm", ), + ( + "polynomial_features", + preprocessing.PolynomialFeatures(), + ("culmen_length_mm", "flipper_length_mm"), + ), ] - assert transformers == expected + assert set(transformers) == set(expected) assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) assert pl_loaded._estimator.fit_intercept is False @@ -849,3 +864,36 @@ def test_pipeline_simple_imputer_to_gbq(penguins_df_default_index, dataset_id): assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) assert pl_loaded._estimator.fit_intercept is False + + +def test_pipeline_poly_features_to_gbq(penguins_df_default_index, dataset_id): + pl = pipeline.Pipeline( + [ + ( + "transform", + preprocessing.PolynomialFeatures(degree=3), + ), + ("estimator", linear_model.LinearRegression(fit_intercept=False)), + ] + ) + + df = penguins_df_default_index.dropna() + X_train = df[ + [ + "culmen_length_mm", + "flipper_length_mm", + ] + ] + y_train = df[["body_mass_g"]] + pl.fit(X_train, y_train) + + pl_loaded = pl.to_gbq( + f"{dataset_id}.test_penguins_pipeline_poly_features", replace=True + ) + assert isinstance(pl_loaded._transform, preprocessing.PolynomialFeatures) + + poly_features = pl_loaded._transform + assert poly_features.degree == 3 + + assert isinstance(pl_loaded._estimator, linear_model.LinearRegression) + assert pl_loaded._estimator.fit_intercept is False diff --git a/tests/system/small/ml/test_preprocessing.py b/tests/system/small/ml/test_preprocessing.py index 73b1855e092..16b153ab45a 100644 --- a/tests/system/small/ml/test_preprocessing.py +++ b/tests/system/small/ml/test_preprocessing.py @@ -907,3 +907,35 @@ def test_poly_features_params(new_penguins_df): ], [1633, 1672, 1690], ) + + +def test_poly_features_save_load(new_penguins_df, dataset_id): + transformer = preprocessing.PolynomialFeatures(degree=3) + transformer.fit(new_penguins_df[["culmen_length_mm", "culmen_depth_mm"]]) + + reloaded_transformer = transformer.to_gbq( + f"{dataset_id}.temp_configured_model", replace=True + ) + assert isinstance(reloaded_transformer, preprocessing.PolynomialFeatures) + assert reloaded_transformer.degree == 3 + assert reloaded_transformer._bqml_model is not None + + result = reloaded_transformer.transform( + new_penguins_df[["culmen_length_mm", "culmen_depth_mm"]] + ).to_pandas() + + utils.check_pandas_df_schema_and_index( + result, + [ + "poly_feat_culmen_length_mm", + "poly_feat_culmen_length_mm_culmen_length_mm", + "poly_feat_culmen_length_mm_culmen_length_mm_culmen_length_mm", + "poly_feat_culmen_length_mm_culmen_length_mm_culmen_depth_mm", + "poly_feat_culmen_length_mm_culmen_depth_mm", + "poly_feat_culmen_length_mm_culmen_depth_mm_culmen_depth_mm", + "poly_feat_culmen_depth_mm", + "poly_feat_culmen_depth_mm_culmen_depth_mm", + "poly_feat_culmen_depth_mm_culmen_depth_mm_culmen_depth_mm", + ], + [1633, 1672, 1690], + ) diff --git a/third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py b/third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py index 4e4624ba847..9ad43b79566 100644 --- a/third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py +++ b/third_party/bigframes_vendored/sklearn/preprocessing/_polynomial.py @@ -8,7 +8,12 @@ class PolynomialFeatures(TransformerMixin, BaseEstimator): - """Generate polynomial and interaction features.""" + """Generate polynomial and interaction features. + + Args: + degree (int): + Specifies the maximal degree of the polynomial features. Valid values [1, 4]. Default to 2. + """ def fit(self, X, y=None): """Compute number of output features. From 1b613e00eddf18fa40ed1d08ff19c4ebeeac2197 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Fri, 28 Jun 2024 21:49:54 -0700 Subject: [PATCH 684/921] feat: bigframes.bigquery.json_set (#782) * feat: bigframes.bigquery.json_set * add tests * fix * fixes * update to binary operator for series supports * supports more than two pairs, defines as ibis udf * add more tests --- bigframes/bigquery/__init__.py | 54 ++++++++ bigframes/core/compile/scalar_op_compiler.py | 32 +++++ bigframes/dtypes.py | 11 ++ bigframes/operations/__init__.py | 24 ++++ tests/system/small/bigquery/test_json.py | 119 ++++++++++++++++++ .../ibis/expr/operations/json.py | 4 +- 6 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 tests/system/small/bigquery/test_json.py diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 85a9010a7d9..ec26d14f33e 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -36,6 +36,10 @@ import bigframes.series as series +# Array functions defined from +# https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions + + def array_length(series: series.Series) -> series.Series: """Compute the length of each array element in the Series. @@ -154,6 +158,56 @@ def array_to_string(series: series.Series, delimiter: str) -> series.Series: return series._apply_unary_op(ops.ArrayToStringOp(delimiter=delimiter)) +# JSON functions defined from +# https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions + + +def json_set( + series: series.Series, + json_path_value_pairs: typing.Sequence[typing.Tuple[str, typing.Any]], +) -> series.Series: + """Produces a new JSON value within a Series by inserting or replacing values at + specified paths. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.read_gbq("SELECT JSON '{\\\"a\\\": 1}' AS data")["data"] + >>> bbq.json_set(s, json_path_value_pairs=[("$.a", 100), ("$.b", "hi")]) + 0 {"a":100,"b":"hi"} + Name: data, dtype: string + + Args: + series (bigframes.series.Series): + The Series containing JSON data (as native JSON objects or JSON-formatted strings). + json_path_value_pairs (Sequence[Tuple[str, typing.Any]]): + Pairs of JSON path and the new value to insert/replace. + + Returns: + bigframes.series.Series: A new Series with the transformed JSON data. + + """ + # SQLGlot parser does not support the "create_if_missing => true" syntax, so + # create_if_missing is not currently implemented. + + for json_path_value_pair in json_path_value_pairs: + if len(json_path_value_pair) != 2: + raise ValueError( + "Incorrect format: Expected (, ), but found: " + + f"{json_path_value_pair}" + ) + + json_path, json_value = json_path_value_pair + series = series._apply_binary_op( + json_value, ops.JSONSet(json_path=json_path), alignment="left" + ) + return series + + def vector_search( base_table: str, column_to_search: str, diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 6b8e60434ee..0bc9f2e370d 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -894,6 +894,26 @@ def array_to_string_op_impl(x: ibis_types.Value, op: ops.ArrayToStringOp): return typing.cast(ibis_types.ArrayValue, x).join(op.delimiter) +# JSON Ops +@scalar_op_compiler.register_binary_op(ops.JSONSet, pass_op=True) +def json_set_op_impl(x: ibis_types.Value, y: ibis_types.Value, op: ops.JSONSet): + if x.type().is_json(): + return json_set( + json_obj=x, + json_path=op.json_path, + json_value=y, + ).to_expr() + else: + # Enabling JSON type eliminates the need for less efficient string conversions. + return vendored_ibis_ops.ToJsonString( + json_set( + json_obj=parse_json(x), + json_path=op.json_path, + json_value=y, + ) + ).to_expr() + + ### Binary Ops def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): """Wraps a binary operator to generate nulls of the expected type if either input is a null scalar.""" @@ -1469,3 +1489,15 @@ def float_floor(a: float) -> float: def float_ceil(a: float) -> float: """Convert string to timestamp.""" return 0 # pragma: NO COVER + + +@ibis.udf.scalar.builtin(name="parse_json") +def parse_json(a: str) -> ibis_dtypes.JSON: + """Converts a JSON-formatted STRING value to a JSON value.""" + + +@ibis.udf.scalar.builtin(name="json_set") +def json_set( + json_obj: ibis_dtypes.JSON, json_path: ibis_dtypes.str, json_value +) -> ibis_dtypes.JSON: + """Produces a new SQL JSON value with the specified JSON data inserted or replaced.""" diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 5de8f896a92..160802ded92 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -240,6 +240,17 @@ def is_struct_like(type: ExpressionType) -> bool: ) +def is_json_like(type: ExpressionType) -> bool: + # TODO: Add JSON type support + return type == STRING_DTYPE + + +def is_json_encoding_type(type: ExpressionType) -> bool: + # Types can be converted into JSON. + # https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#json_encodings + return type != GEO_DTYPE + + def is_numeric(type: ExpressionType) -> bool: return type in NUMERIC_BIGFRAMES_TYPES_PERMISSIVE diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index c10b7436315..145c415ca07 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -707,6 +707,30 @@ def output_type(self, *input_types): strconcat_op = StrConcatOp() +## JSON Ops +@dataclasses.dataclass(frozen=True) +class JSONSet(BinaryOp): + name: typing.ClassVar[str] = "json_set" + json_path: str + + def output_type(self, *input_types): + left_type = input_types[0] + right_type = input_types[1] + if not dtypes.is_json_like(left_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {left_type}" + ) + if not dtypes.is_json_encoding_type(right_type): + raise TypeError( + "The value to be assigned must be a type that can be encoded as JSON." + + f"Received type: {right_type}" + ) + + # After JSON type implementation, ONLY return JSON data. + return left_type + + # Ternary Ops @dataclasses.dataclass(frozen=True) class WhereOp(TernaryOp): diff --git a/tests/system/small/bigquery/test_json.py b/tests/system/small/bigquery/test_json.py new file mode 100644 index 00000000000..ff759b8fda8 --- /dev/null +++ b/tests/system/small/bigquery/test_json.py @@ -0,0 +1,119 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import geopandas as gpd # type: ignore +import pandas as pd +import pytest + +import bigframes.bigquery as bbq +import bigframes.pandas as bpd + + +def _get_series_from_json(json_data): + sql = " UNION ALL ".join( + [ + f"SELECT {id} AS id, JSON '{json.dumps(data)}' AS data" + for id, data in enumerate(json_data) + ] + ) + df = bpd.read_gbq(sql).set_index("id").sort_index() + return df["data"] + + +@pytest.mark.parametrize( + ("json_path", "expected_json"), + [ + pytest.param("$.a", [{"a": 10}], id="simple"), + pytest.param("$.a.b.c", [{"a": {"b": {"c": 10, "d": []}}}], id="nested"), + ], +) +def test_json_set_at_json_path(json_path, expected_json): + s = _get_series_from_json([{"a": {"b": {"c": "tester", "d": []}}}]) + actual = bbq.json_set(s, json_path_value_pairs=[(json_path, 10)]) + + expected = _get_series_from_json(expected_json) + pd.testing.assert_series_equal( + actual.to_pandas(), + expected.to_pandas(), + ) + + +@pytest.mark.parametrize( + ("json_value", "expected_json"), + [ + pytest.param(10, [{"a": {"b": 10}}, {"a": {"b": 10}}], id="int"), + pytest.param(0.333, [{"a": {"b": 0.333}}, {"a": {"b": 0.333}}], id="float"), + pytest.param("eng", [{"a": {"b": "eng"}}, {"a": {"b": "eng"}}], id="string"), + pytest.param([1, 2], [{"a": {"b": 1}}, {"a": {"b": 2}}], id="series"), + ], +) +def test_json_set_at_json_value_type(json_value, expected_json): + s = _get_series_from_json([{"a": {"b": "dev"}}, {"a": {"b": [1, 2]}}]) + actual = bbq.json_set(s, json_path_value_pairs=[("$.a.b", json_value)]) + + expected = _get_series_from_json(expected_json) + pd.testing.assert_series_equal( + actual.to_pandas(), + expected.to_pandas(), + ) + + +def test_json_set_w_more_pairs(): + s = _get_series_from_json([{"a": 2}, {"b": 5}, {"c": 1}]) + actual = bbq.json_set( + s, json_path_value_pairs=[("$.a", 1), ("$.b", 2), ("$.a", [3, 4, 5])] + ) + expected = _get_series_from_json( + [{"a": 3, "b": 2}, {"a": 4, "b": 2}, {"a": 5, "b": 2, "c": 1}] + ) + pd.testing.assert_series_equal( + actual.to_pandas(), + expected.to_pandas(), + ) + + +@pytest.mark.parametrize( + ("series", "json_path_value_pairs"), + [ + pytest.param( + _get_series_from_json([{"a": 10}]), + [("$.a", 1, 100)], + id="invalid_json_path_value_pairs", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + _get_series_from_json([{"a": 10}]), + [ + ( + "$.a", + bpd.read_pandas( + gpd.GeoSeries.from_wkt(["POINT (1 2)", "POINT (2 1)"]) + ), + ) + ], + id="invalid_json_value_type", + marks=pytest.mark.xfail(raises=TypeError), + ), + pytest.param( + bpd.Series([1, 2]), + [("$.a", 1)], + id="invalid_series_type", + marks=pytest.mark.xfail(raises=TypeError), + ), + ], +) +def test_json_set_w_invalid(series, json_path_value_pairs): + bbq.json_set(series, json_path_value_pairs=json_path_value_pairs) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/json.py b/third_party/bigframes_vendored/ibis/expr/operations/json.py index 772c2e8ff44..1eb05541379 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/json.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/json.py @@ -2,8 +2,8 @@ from __future__ import annotations import ibis.expr.datatypes as dt -from ibis.expr.operations.core import Unary +import ibis.expr.operations.core as ibis_ops_core -class ToJsonString(Unary): +class ToJsonString(ibis_ops_core.Unary): dtype = dt.string From 0b1f07f59173d14f6d1091a9c974a5429b7bc541 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:45:51 -0700 Subject: [PATCH 685/921] chore: add a test for external table reading. (#812) --- tests/system/small/test_session.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 5d53a5af172..2f779f337e4 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -570,6 +570,18 @@ def test_read_gbq_with_custom_global_labels( assert len(bigframes.options.compute.extra_query_labels) == 0 +def test_read_gbq_external_table(session: bigframes.Session): + # Verify the table is external to ensure it hasn't been altered + external_table_id = "bigframes-dev.bigframes_tests_sys.parquet_external_table" + external_table = session.bqclient.get_table(external_table_id) + assert external_table.table_type == "EXTERNAL" + + df = session.read_gbq(external_table_id) + + assert list(df.columns) == ["idx", "s1", "s2", "s3", "s4", "i1", "f1", "i2", "f2"] + assert df["i1"].max() == 99 + + 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) From 136dc24e160339d27f6335e7b28f08cd95d2c67d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 1 Jul 2024 11:55:24 -0700 Subject: [PATCH 686/921] feat: More informative error when query plan too complex (#811) --- bigframes/exceptions.py | 4 ++++ bigframes/session/__init__.py | 24 ++++++++++++++++-------- tests/system/small/test_dataframe.py | 17 +++++++++++++++-- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index bc0d83b4f67..1d317497605 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -51,5 +51,9 @@ class OrderRequiredError(ValueError): """Operation requires total row ordering to be enabled.""" +class QueryComplexityError(RuntimeError): + """Query plan is too complex to execute.""" + + class TimeTravelDisabledWarning(Warning): """A query was reattempted without time travel.""" diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 3aba3581aac..867bdedf1ca 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1833,14 +1833,22 @@ def _start_query( Starts BigQuery query job and waits for results. """ job_config = self._prepare_query_job_config(job_config) - return bigframes.session._io.bigquery.start_query_with_client( - self, - sql, - job_config, - max_results, - timeout, - api_name=api_name, - ) + try: + return bigframes.session._io.bigquery.start_query_with_client( + self, + sql, + job_config, + max_results, + timeout, + api_name=api_name, + ) + except google.api_core.exceptions.BadRequest as e: + # Unfortunately, this error type does not have a separate error code or exception type + if "Resources exceeded during query execution" in e.message: + new_message = "Computation is too complex to execute as a single query. Try using DataFrame.cache() on intermediate results, or setting bigframes.options.compute.enable_multi_query_execution." + raise bigframes.exceptions.QueryComplexityError(new_message) from e + else: + raise def _start_query_ml_ddl( self, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index a5c810b91b6..625b920763e 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4472,13 +4472,26 @@ def test_recursion_limit(scalars_df_index): scalars_df_index.to_pandas() +def test_query_complexity_error(scalars_df_index): + # This test requires automatic caching/query decomposition to be turned off + bf_df = scalars_df_index + for _ in range(8): + bf_df = bf_df.merge(bf_df, on="int64_col").head(30) + bf_df = bf_df[bf_df.columns[:20]] + + with pytest.raises( + bigframes.exceptions.QueryComplexityError, match=r"Try using DataFrame\.cache" + ): + bf_df.to_pandas() + + def test_query_complexity_repeated_joins( scalars_df_index, scalars_pandas_df_index, with_multiquery_execution ): pd_df = scalars_pandas_df_index bf_df = scalars_df_index - for _ in range(6): - # recursively join, resuling in 2^6 - 1 = 63 joins + for _ in range(8): + # recursively join, resuling in 2^8 - 1 = 255 joins pd_df = pd_df.merge(pd_df, on="int64_col").head(30) pd_df = pd_df[pd_df.columns[:20]] bf_df = bf_df.merge(bf_df, on="int64_col").head(30) From 6d947a2b2930cd34faf39e920711d0330b8a5651 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:56:52 -0500 Subject: [PATCH 687/921] chore(main): release 1.11.0 (#806) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 24 ++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 633f9930eaa..b27da8d3014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.11.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.10.0...v1.11.0) (2024-07-01) + + +### Features + +* Add .agg support for size ([#792](https://github.com/googleapis/python-bigquery-dataframes/issues/792)) ([87e6018](https://github.com/googleapis/python-bigquery-dataframes/commit/87e60182c964c369079165e87ce73dd0c0481a5a)) +* Add `bigframes.bigquery.json_set` ([#782](https://github.com/googleapis/python-bigquery-dataframes/issues/782)) ([1b613e0](https://github.com/googleapis/python-bigquery-dataframes/commit/1b613e00eddf18fa40ed1d08ff19c4ebeeac2197)) +* Add `bigframes.streaming.to_pubsub` method to create continuous query that writes to Pub/Sub ([#801](https://github.com/googleapis/python-bigquery-dataframes/issues/801)) ([b47f32d](https://github.com/googleapis/python-bigquery-dataframes/commit/b47f32d74a0c9eb908be690b2dd56b0f5579b133)) +* Add `DataFrame.to_arrow` to create Arrow Table from DataFrame ([#807](https://github.com/googleapis/python-bigquery-dataframes/issues/807)) ([1e3feda](https://github.com/googleapis/python-bigquery-dataframes/commit/1e3feda9e8fe9d08a0e3838066f6414f8015197d)) +* Add `PolynomialFeatures` support to `to_gbq` and pipelines ([#805](https://github.com/googleapis/python-bigquery-dataframes/issues/805)) ([57d98b9](https://github.com/googleapis/python-bigquery-dataframes/commit/57d98b9e3298583ec40c04665ab84e6ad2b948fb)) +* Add Series.peek to preview data efficiently ([#727](https://github.com/googleapis/python-bigquery-dataframes/issues/727)) ([580e1b9](https://github.com/googleapis/python-bigquery-dataframes/commit/580e1b9e965d883a67f91a6db8311c2416ca8fe5)) +* Expose gcf memory param in `remote_function` ([#803](https://github.com/googleapis/python-bigquery-dataframes/issues/803)) ([014765c](https://github.com/googleapis/python-bigquery-dataframes/commit/014765c22410a0b4559896d163c440f46f7ce98f)) +* More informative error when query plan too complex ([#811](https://github.com/googleapis/python-bigquery-dataframes/issues/811)) ([136dc24](https://github.com/googleapis/python-bigquery-dataframes/commit/136dc24e160339d27f6335e7b28f08cd95d2c67d)) + + +### Bug Fixes + +* Include internally required packages in `remote_function` hash ([#799](https://github.com/googleapis/python-bigquery-dataframes/issues/799)) ([4b8fc15](https://github.com/googleapis/python-bigquery-dataframes/commit/4b8fc15ec2c126566269f84d75289198fee2c655)) + + +### Documentation + +* Document dtype limitation on row processing `remote_function` ([#800](https://github.com/googleapis/python-bigquery-dataframes/issues/800)) ([487dff6](https://github.com/googleapis/python-bigquery-dataframes/commit/487dff6ac147683aef529e1ff8c197dce3fb437c)) + ## [1.10.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.9.0...v1.10.0) (2024-06-21) diff --git a/bigframes/version.py b/bigframes/version.py index 014b0640714..75f3ffb3610 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.10.0" +__version__ = "1.11.0" From bb1b1e3669738b551e1a345e0f37dfdede656a17 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 2 Jul 2024 13:36:04 -0700 Subject: [PATCH 688/921] refactor: Add mappings from internal dtypes to bq types (#810) --- bigframes/core/blocks.py | 21 +++++++--------- bigframes/core/schema.py | 6 +++++ bigframes/dtypes.py | 54 ++++++++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 379c6611791..25acfe2f429 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -444,27 +444,24 @@ def _to_dataframe(self, result) -> pd.DataFrame: # Runs strict validations to ensure internal type predictions and ibis are completely in sync # Do not execute these validations outside of testing suite. if "PYTEST_CURRENT_TEST" in os.environ: - self._validate_result_schema(result_dataframe) + self._validate_result_schema(result.schema) return result_dataframe - def _validate_result_schema(self, result_df: pd.DataFrame): + def _validate_result_schema( + self, bq_result_schema: list[bigquery.schema.SchemaField] + ): + actual_schema = tuple(bq_result_schema) ibis_schema = self.expr._compiled_schema internal_schema = self.expr.node.schema - actual_schema = bf_schema.ArraySchema( - tuple( - bf_schema.SchemaItem(name, dtype) # type: ignore - for name, dtype in result_df.dtypes.items() - ) - ) if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: return - if internal_schema != actual_schema: + if internal_schema.to_bigquery() != actual_schema: raise ValueError( - f"This error should only occur while testing. BigFrames internal schema: {internal_schema} does not match actual schema: {actual_schema}" + f"This error should only occur while testing. BigFrames internal schema: {internal_schema.to_bigquery()} does not match actual schema: {actual_schema}" ) - if ibis_schema != actual_schema: + if ibis_schema.to_bigquery() != actual_schema: raise ValueError( - f"This error should only occur while testing. Ibis schema: {ibis_schema} does not match actual schema: {actual_schema}" + f"This error should only occur while testing. Ibis schema: {ibis_schema.to_bigquery()} does not match actual schema: {actual_schema}" ) def to_arrow( diff --git a/bigframes/core/schema.py b/bigframes/core/schema.py index 30a2a6593a1..ee27c6ff301 100644 --- a/bigframes/core/schema.py +++ b/bigframes/core/schema.py @@ -58,6 +58,12 @@ def dtypes(self) -> typing.Tuple[bigframes.dtypes.Dtype, ...]: def _mapping(self) -> typing.Dict[ColumnIdentifierType, bigframes.dtypes.Dtype]: return {item.column: item.dtype for item in self.items} + def to_bigquery(self) -> typing.Tuple[google.cloud.bigquery.SchemaField, ...]: + return tuple( + bigframes.dtypes.convert_to_schema_field(item.column, item.dtype) + for item in self.items + ) + def drop(self, columns: typing.Iterable[str]) -> ArraySchema: return ArraySchema( tuple(item for item in self.items if item.column not in columns) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 160802ded92..563904fbb6d 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -70,7 +70,9 @@ class SimpleDtypeInfo: dtype: Dtype arrow_dtype: typing.Optional[pa.DataType] - type_kind: typing.Tuple[str, ...] # Should all correspond to the same db type + type_kind: typing.Tuple[ + str, ... + ] # Should all correspond to the same db type. Put preferred canonical sql type name first logical_bytes: int = ( 8 # this is approximate only, some types are variably sized, also, compression ) @@ -84,20 +86,23 @@ class SimpleDtypeInfo: SimpleDtypeInfo( dtype=INT_DTYPE, arrow_dtype=pa.int64(), - type_kind=("INT64", "INTEGER"), + type_kind=("INTEGER", "INT64"), orderable=True, clusterable=True, ), SimpleDtypeInfo( dtype=FLOAT_DTYPE, arrow_dtype=pa.float64(), - type_kind=("FLOAT64", "FLOAT"), + type_kind=("FLOAT", "FLOAT64"), orderable=True, ), SimpleDtypeInfo( dtype=BOOL_DTYPE, arrow_dtype=pa.bool_(), - type_kind=("BOOL", "BOOLEAN"), + type_kind=( + "BOOLEAN", + "BOOL", + ), logical_bytes=1, orderable=True, clusterable=True, @@ -143,7 +148,7 @@ class SimpleDtypeInfo: SimpleDtypeInfo( dtype=NUMERIC_DTYPE, arrow_dtype=pa.decimal128(38, 9), - type_kind=("NUMERIC",), + type_kind=("NUMERIC", "DECIMAL"), logical_bytes=16, orderable=True, clusterable=True, @@ -151,7 +156,7 @@ class SimpleDtypeInfo: SimpleDtypeInfo( dtype=BIGNUMERIC_DTYPE, arrow_dtype=pa.decimal256(76, 38), - type_kind=("BIGNUMERIC",), + type_kind=("BIGNUMERIC", "BIGDECIMAL"), logical_bytes=32, orderable=True, clusterable=True, @@ -417,6 +422,7 @@ def infer_literal_arrow_type(literal) -> typing.Optional[pa.DataType]: for mapping in SIMPLE_TYPES for type_kind in mapping.type_kind } +_BIGFRAMES_TO_TK = {mapping.dtype: mapping.type_kind[0] for mapping in SIMPLE_TYPES} def convert_schema_field( @@ -440,12 +446,44 @@ def convert_schema_field( if is_repeated: pa_type = pa.list_(bigframes_dtype_to_arrow_dtype(singular_type)) return field.name, pd.ArrowDtype(pa_type) - else: - return field.name, singular_type + return field.name, singular_type else: raise ValueError(f"Cannot handle type: {field.field_type}") +def convert_to_schema_field( + name: str, + bigframes_dtype: Dtype, +) -> google.cloud.bigquery.SchemaField: + if bigframes_dtype in _BIGFRAMES_TO_TK: + return google.cloud.bigquery.SchemaField( + name, _BIGFRAMES_TO_TK[bigframes_dtype] + ) + if isinstance(bigframes_dtype, pd.ArrowDtype): + if pa.types.is_list(bigframes_dtype.pyarrow_dtype): + inner_type = arrow_dtype_to_bigframes_dtype( + bigframes_dtype.pyarrow_dtype.value_type + ) + inner_field = convert_to_schema_field(name, inner_type) + return google.cloud.bigquery.SchemaField( + name, inner_field.field_type, mode="REPEATED", fields=inner_field.fields + ) + if pa.types.is_struct(bigframes_dtype.pyarrow_dtype): + inner_fields: list[pa.Field] = [] + struct_type = typing.cast(pa.StructType, bigframes_dtype.pyarrow_dtype) + for i in range(struct_type.num_fields): + field = struct_type.field(i) + inner_bf_type = arrow_dtype_to_bigframes_dtype(field.type) + inner_fields.append(convert_to_schema_field(field.name, inner_bf_type)) + + return google.cloud.bigquery.SchemaField( + name, "RECORD", fields=inner_fields + ) + raise ValueError( + f"No arrow conversion for {bigframes_dtype}. {constants.FEEDBACK_LINK}" + ) + + def bf_type_from_type_kind( bq_schema: list[google.cloud.bigquery.SchemaField], ) -> typing.Dict[str, Dtype]: From 68b02d5467373b944de9942eb4e26a15e5f2f402 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:44:09 -0400 Subject: [PATCH 689/921] chore: update templated files (#819) Source-Link: https://github.com/googleapis/synthtool/commit/a37f74cd300d1f56d6f28c368d2931f72adee948 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:d3de8a02819f65001effcbd3ea76ce97e9bcff035c7a89457f40f892c87c5b32 Co-authored-by: Owl Bot --- .coveragerc | 2 +- .flake8 | 2 +- .github/.OwlBot.lock.yaml | 4 +- .github/auto-label.yaml | 2 +- .kokoro/docker/docs/Dockerfile | 2 +- .kokoro/populate-secrets.sh | 2 +- .kokoro/publish-docs.sh | 2 +- .kokoro/release.sh | 2 +- .kokoro/requirements.txt | 509 ++++++++++++++------------- .kokoro/test-samples-against-head.sh | 2 +- .kokoro/test-samples-impl.sh | 2 +- .kokoro/test-samples.sh | 2 +- .kokoro/trampoline.sh | 2 +- .kokoro/trampoline_v2.sh | 2 +- .trampolinerc | 2 +- MANIFEST.in | 2 +- docs/conf.py | 2 +- scripts/decrypt-secrets.sh | 2 +- scripts/readme-gen/readme_gen.py | 2 +- 19 files changed, 285 insertions(+), 262 deletions(-) diff --git a/.coveragerc b/.coveragerc index dffe22a6f46..9d801989cba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.flake8 b/.flake8 index 87f6e408c47..32986c79287 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 81f87c56917..91d742b5b9f 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:5a4c19d17e597b92d786e569be101e636c9c2817731f80a5adec56b2aa8fe070 -# created: 2024-04-12T11:35:58.922854369Z + digest: sha256:d3de8a02819f65001effcbd3ea76ce97e9bcff035c7a89457f40f892c87c5b32 +# created: 2024-07-03T17:43:00.77142528Z diff --git a/.github/auto-label.yaml b/.github/auto-label.yaml index 8b37ee89711..21786a4eb08 100644 --- a/.github/auto-label.yaml +++ b/.github/auto-label.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index bdaf39fe22d..a26ce61930f 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh index 6f3972140e8..c435402f473 100755 --- a/.kokoro/populate-secrets.sh +++ b/.kokoro/populate-secrets.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC. +# Copyright 2024 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index 7700c90ee92..da9ce803dd0 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/release.sh b/.kokoro/release.sh index 320ac51271e..21a9b558c5e 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 51f92b8e12f..35ece0e4d2e 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -4,21 +4,25 @@ # # pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.1.4 \ - --hash=sha256:72558ba729e4c468572609817226fb0a6e7e9a0a7d477b882be168c0b4a62b94 \ - --hash=sha256:fbe56f8cda08aa9a04b307d8482ea703e96a6a801611acb4be9bf3942017989f +argcomplete==3.4.0 \ + --hash=sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5 \ + --hash=sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f # via nox -attrs==23.1.0 \ - --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ - --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 +attrs==23.2.0 \ + --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ + --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via gcp-releasetool -cachetools==5.3.2 \ - --hash=sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2 \ - --hash=sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1 +backports-tarfile==1.2.0 \ + --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ + --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 + # via jaraco-context +cachetools==5.3.3 \ + --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ + --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 # via google-auth -certifi==2023.7.22 \ - --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ - --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 +certifi==2024.6.2 \ + --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ + --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 # via requests cffi==1.16.0 \ --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ @@ -87,90 +91,90 @@ click==8.0.4 \ # -r requirements.in # gcp-docuploader # gcp-releasetool -colorlog==6.7.0 \ - --hash=sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662 \ - --hash=sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5 +colorlog==6.8.2 \ + --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ + --hash=sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33 # via # gcp-docuploader # nox -cryptography==42.0.5 \ - --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ - --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ - --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ - --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ - --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ - --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ - --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ - --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ - --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ - --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ - --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ - --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ - --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ - --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ - --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ - --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ - --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ - --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ - --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ - --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ - --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ - --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ - --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ - --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ - --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ - --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ - --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ - --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ - --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ - --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ - --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ - --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 +cryptography==42.0.8 \ + --hash=sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad \ + --hash=sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583 \ + --hash=sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b \ + --hash=sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c \ + --hash=sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1 \ + --hash=sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648 \ + --hash=sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949 \ + --hash=sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba \ + --hash=sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c \ + --hash=sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9 \ + --hash=sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d \ + --hash=sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c \ + --hash=sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e \ + --hash=sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2 \ + --hash=sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d \ + --hash=sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7 \ + --hash=sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70 \ + --hash=sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2 \ + --hash=sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7 \ + --hash=sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14 \ + --hash=sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe \ + --hash=sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e \ + --hash=sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71 \ + --hash=sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961 \ + --hash=sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7 \ + --hash=sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c \ + --hash=sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28 \ + --hash=sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842 \ + --hash=sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902 \ + --hash=sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801 \ + --hash=sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a \ + --hash=sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e # via # -r requirements.in # gcp-releasetool # secretstorage -distlib==0.3.7 \ - --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \ - --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8 +distlib==0.3.8 \ + --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ + --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 # via virtualenv -docutils==0.20.1 \ - --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \ - --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b +docutils==0.21.2 \ + --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ + --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # via readme-renderer -filelock==3.13.1 \ - --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ - --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c +filelock==3.15.4 \ + --hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \ + --hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7 # via virtualenv gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==2.0.0 \ - --hash=sha256:3d73480b50ba243f22d7c7ec08b115a30e1c7817c4899781840c26f9c55b8277 \ - --hash=sha256:7aa9fd935ec61e581eb8458ad00823786d91756c25e492f372b2b30962f3c28f +gcp-releasetool==2.0.1 \ + --hash=sha256:34314a910c08e8911d9c965bd44f8f2185c4f556e737d719c33a41f6a610de96 \ + --hash=sha256:b0d5863c6a070702b10883d37c4bdfd74bf930fe417f36c0c965d3b7c779ae62 # via -r requirements.in -google-api-core==2.12.0 \ - --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \ - --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160 +google-api-core==2.19.1 \ + --hash=sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125 \ + --hash=sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd # via # google-cloud-core # google-cloud-storage -google-auth==2.23.4 \ - --hash=sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3 \ - --hash=sha256:d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2 +google-auth==2.31.0 \ + --hash=sha256:042c4702efa9f7d3c48d3a69341c209381b125faa6dbf3ebe56bc7e40ae05c23 \ + --hash=sha256:87805c36970047247c8afe614d4e3af8eceafc1ebba0c679fe75ddd1d575e871 # via # gcp-releasetool # google-api-core # google-cloud-core # google-cloud-storage -google-cloud-core==2.3.3 \ - --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \ - --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863 +google-cloud-core==2.4.1 \ + --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ + --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 # via google-cloud-storage -google-cloud-storage==2.13.0 \ - --hash=sha256:ab0bf2e1780a1b74cf17fccb13788070b729f50c252f0c94ada2aae0ca95437d \ - --hash=sha256:f62dc4c7b6cd4360d072e3deb28035fbdad491ac3d9b0b1815a12daea10f37c7 +google-cloud-storage==2.17.0 \ + --hash=sha256:49378abff54ef656b52dca5ef0f2eba9aa83dc2b2c72c78714b03a1a95fe9388 \ + --hash=sha256:5b393bc766b7a3bc6f5407b9e665b2450d36282614b7945e570b3480a456d1e1 # via gcp-docuploader google-crc32c==1.5.0 \ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ @@ -244,28 +248,36 @@ google-crc32c==1.5.0 \ # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.6.0 \ - --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \ - --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b +google-resumable-media==2.7.1 \ + --hash=sha256:103ebc4ba331ab1bfdac0250f8033627a2cd7cde09e7ccff9181e31ba4315b2c \ + --hash=sha256:eae451a7b2e2cdbaaa0fd2eb00cc8a1ee5e95e16b55597359cbc3d27d7d90e33 # via google-cloud-storage -googleapis-common-protos==1.61.0 \ - --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \ - --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b +googleapis-common-protos==1.63.2 \ + --hash=sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945 \ + --hash=sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87 # via google-api-core idna==3.7 \ --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests -importlib-metadata==6.8.0 \ - --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ - --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 +importlib-metadata==8.0.0 \ + --hash=sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f \ + --hash=sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812 # via # -r requirements.in # keyring # twine -jaraco-classes==3.3.0 \ - --hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \ - --hash=sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621 +jaraco-classes==3.4.0 \ + --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ + --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 + # via keyring +jaraco-context==5.3.0 \ + --hash=sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266 \ + --hash=sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2 + # via keyring +jaraco-functools==4.0.1 \ + --hash=sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664 \ + --hash=sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8 # via keyring jeepney==0.8.0 \ --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ @@ -273,13 +285,13 @@ jeepney==0.8.0 \ # via # keyring # secretstorage -jinja2==3.1.3 \ - --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ - --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d # via gcp-releasetool -keyring==24.2.0 \ - --hash=sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6 \ - --hash=sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509 +keyring==25.2.1 \ + --hash=sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50 \ + --hash=sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b # via # gcp-releasetool # twine @@ -287,146 +299,153 @@ markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb # via rich -markupsafe==2.1.3 \ - --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ - --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ - --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ - --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ - --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ - --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ - --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ - --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ - --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ - --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ - --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ - --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ - --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ - --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ - --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ - --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ - --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ - --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ - --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ - --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ - --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ - --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ - --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ - --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ - --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ - --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ - --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ - --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ - --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ - --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ - --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ - --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ - --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ - --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ - --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ - --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ - --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ - --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ - --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ - --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ - --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ - --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ - --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ - --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ - --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ - --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ - --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ - --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ - --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ - --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ - --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ - --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ - --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ - --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ - --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ - --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ - --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ - --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ - --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ - --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 +markupsafe==2.1.5 \ + --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ + --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ + --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ + --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ + --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ + --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ + --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ + --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ + --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ + --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ + --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ + --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ + --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ + --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ + --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ + --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ + --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ + --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ + --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ + --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ + --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ + --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ + --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ + --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ + --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ + --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ + --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ + --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ + --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ + --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ + --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ + --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ + --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ + --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ + --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ + --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ + --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ + --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ + --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ + --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ + --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ + --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ + --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ + --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ + --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ + --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ + --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ + --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ + --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ + --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ + --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ + --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ + --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ + --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ + --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ + --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ + --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ + --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ + --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ + --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 # via jinja2 mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.1.0 \ - --hash=sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a \ - --hash=sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6 - # via jaraco-classes -nh3==0.2.14 \ - --hash=sha256:116c9515937f94f0057ef50ebcbcc10600860065953ba56f14473ff706371873 \ - --hash=sha256:18415df36db9b001f71a42a3a5395db79cf23d556996090d293764436e98e8ad \ - --hash=sha256:203cac86e313cf6486704d0ec620a992c8bc164c86d3a4fd3d761dd552d839b5 \ - --hash=sha256:2b0be5c792bd43d0abef8ca39dd8acb3c0611052ce466d0401d51ea0d9aa7525 \ - --hash=sha256:377aaf6a9e7c63962f367158d808c6a1344e2b4f83d071c43fbd631b75c4f0b2 \ - --hash=sha256:525846c56c2bcd376f5eaee76063ebf33cf1e620c1498b2a40107f60cfc6054e \ - --hash=sha256:5529a3bf99402c34056576d80ae5547123f1078da76aa99e8ed79e44fa67282d \ - --hash=sha256:7771d43222b639a4cd9e341f870cee336b9d886de1ad9bec8dddab22fe1de450 \ - --hash=sha256:88c753efbcdfc2644a5012938c6b9753f1c64a5723a67f0301ca43e7b85dcf0e \ - --hash=sha256:93a943cfd3e33bd03f77b97baa11990148687877b74193bf777956b67054dcc6 \ - --hash=sha256:9be2f68fb9a40d8440cbf34cbf40758aa7f6093160bfc7fb018cce8e424f0c3a \ - --hash=sha256:a0c509894fd4dccdff557068e5074999ae3b75f4c5a2d6fb5415e782e25679c4 \ - --hash=sha256:ac8056e937f264995a82bf0053ca898a1cb1c9efc7cd68fa07fe0060734df7e4 \ - --hash=sha256:aed56a86daa43966dd790ba86d4b810b219f75b4bb737461b6886ce2bde38fd6 \ - --hash=sha256:e8986f1dd3221d1e741fda0a12eaa4a273f1d80a35e31a1ffe579e7c621d069e \ - --hash=sha256:f99212a81c62b5f22f9e7c3e347aa00491114a5647e1f13bbebd79c3e5f08d75 +more-itertools==10.3.0 \ + --hash=sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463 \ + --hash=sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320 + # via + # jaraco-classes + # jaraco-functools +nh3==0.2.17 \ + --hash=sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a \ + --hash=sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911 \ + --hash=sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb \ + --hash=sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a \ + --hash=sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc \ + --hash=sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028 \ + --hash=sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9 \ + --hash=sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3 \ + --hash=sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351 \ + --hash=sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10 \ + --hash=sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71 \ + --hash=sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f \ + --hash=sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b \ + --hash=sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a \ + --hash=sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062 \ + --hash=sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a # via readme-renderer -nox==2023.4.22 \ - --hash=sha256:0b1adc619c58ab4fa57d6ab2e7823fe47a32e70202f287d78474adcc7bda1891 \ - --hash=sha256:46c0560b0dc609d7d967dc99e22cb463d3c4caf54a5fda735d6c11b5177e3a9f +nox==2024.4.15 \ + --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ + --hash=sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f # via -r requirements.in -packaging==23.2 \ - --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ - --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 # via # gcp-releasetool # nox -pkginfo==1.9.6 \ - --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \ - --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046 +pkginfo==1.10.0 \ + --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ + --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 # via twine -platformdirs==3.11.0 \ - --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ - --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 # via virtualenv -protobuf==4.25.3 \ - --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ - --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ - --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ - --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ - --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ - --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ - --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ - --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ - --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ - --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ - --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 +proto-plus==1.24.0 \ + --hash=sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445 \ + --hash=sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12 + # via google-api-core +protobuf==5.27.2 \ + --hash=sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505 \ + --hash=sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b \ + --hash=sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38 \ + --hash=sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863 \ + --hash=sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470 \ + --hash=sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6 \ + --hash=sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce \ + --hash=sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca \ + --hash=sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5 \ + --hash=sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e \ + --hash=sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714 # via # gcp-docuploader # gcp-releasetool # google-api-core # googleapis-common-protos -pyasn1==0.5.0 \ - --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \ - --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde + # proto-plus +pyasn1==0.6.0 \ + --hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \ + --hash=sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473 # via # pyasn1-modules # rsa -pyasn1-modules==0.3.0 \ - --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ - --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d +pyasn1-modules==0.4.0 \ + --hash=sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6 \ + --hash=sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b # via google-auth -pycparser==2.21 \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +pycparser==2.22 \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi -pygments==2.16.1 \ - --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \ - --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29 +pygments==2.18.0 \ + --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ + --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a # via # readme-renderer # rich @@ -434,20 +453,20 @@ pyjwt==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via gcp-releasetool -pyperclip==1.8.2 \ - --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 +pyperclip==1.9.0 \ + --hash=sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310 # via gcp-releasetool -python-dateutil==2.8.2 \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via gcp-releasetool -readme-renderer==42.0 \ - --hash=sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d \ - --hash=sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1 +readme-renderer==43.0 \ + --hash=sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311 \ + --hash=sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9 # via twine -requests==2.31.0 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via # gcp-releasetool # google-api-core @@ -462,9 +481,9 @@ rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==13.6.0 \ - --hash=sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245 \ - --hash=sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef +rich==13.7.1 \ + --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ + --hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432 # via twine rsa==4.9 \ --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ @@ -480,35 +499,39 @@ six==1.16.0 \ # via # gcp-docuploader # python-dateutil -twine==4.0.2 \ - --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \ - --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8 +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via nox +twine==5.1.1 \ + --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ + --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r requirements.in -typing-extensions==4.8.0 \ - --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ - --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via -r requirements.in -urllib3==2.0.7 \ - --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \ - --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e +urllib3==2.2.2 \ + --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ + --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 # via # requests # twine -virtualenv==20.24.6 \ - --hash=sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af \ - --hash=sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381 +virtualenv==20.26.3 \ + --hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \ + --hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589 # via nox -wheel==0.41.3 \ - --hash=sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942 \ - --hash=sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841 +wheel==0.43.0 \ + --hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \ + --hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81 # via -r requirements.in -zipp==3.17.0 \ - --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ - --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 +zipp==3.19.2 \ + --hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \ + --hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==69.2.0 \ - --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ - --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c +setuptools==70.2.0 \ + --hash=sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05 \ + --hash=sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1 # via -r requirements.in diff --git a/.kokoro/test-samples-against-head.sh b/.kokoro/test-samples-against-head.sh index 63ac41dfae1..e9d8bd79a64 100755 --- a/.kokoro/test-samples-against-head.sh +++ b/.kokoro/test-samples-against-head.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh index 5a0f5fab6a8..55910c8ba17 100755 --- a/.kokoro/test-samples-impl.sh +++ b/.kokoro/test-samples-impl.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh index 50b35a48c19..7933d820149 100755 --- a/.kokoro/test-samples.sh +++ b/.kokoro/test-samples.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh index d85b1f26769..48f79699706 100755 --- a/.kokoro/trampoline.sh +++ b/.kokoro/trampoline.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 59a7cf3a937..35fa529231d 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.trampolinerc b/.trampolinerc index a7dfeb42c6d..0080152373d 100644 --- a/.trampolinerc +++ b/.trampolinerc @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/MANIFEST.in b/MANIFEST.in index 02b1f4ba4bd..16a933a6297 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/conf.py b/docs/conf.py index af8c5efda89..23ec7a6b36a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/decrypt-secrets.sh b/scripts/decrypt-secrets.sh index 0018b421ddf..120b0ddc436 100755 --- a/scripts/decrypt-secrets.sh +++ b/scripts/decrypt-secrets.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2023 Google LLC All rights reserved. +# Copyright 2024 Google LLC All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/readme-gen/readme_gen.py b/scripts/readme-gen/readme_gen.py index 1acc119835b..8f5e248a0da 100644 --- a/scripts/readme-gen/readme_gen.py +++ b/scripts/readme-gen/readme_gen.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From bca4ee0c3e7f516588b104655901033aa20b19f1 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 3 Jul 2024 15:17:26 -0700 Subject: [PATCH 690/921] test: add notebook with remote function use cases (#808) * test: add notebook with remote function usecases * use reuse=False to avoid concurrency conflicts during testing * decorate the notebook with comments * update comment * use aliased import for demo purpose * alias the outside import for demo --- .../remote_function_usecases.ipynb | 1408 +++++++++++++++++ 1 file changed, 1408 insertions(+) create mode 100644 notebooks/remote_functions/remote_function_usecases.ipynb diff --git a/notebooks/remote_functions/remote_function_usecases.ipynb b/notebooks/remote_functions/remote_function_usecases.ipynb new file mode 100644 index 00000000000..3d7ae3e8c72 --- /dev/null +++ b/notebooks/remote_functions/remote_function_usecases.ipynb @@ -0,0 +1,1408 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "id": "Y6QAttCqqMM0" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 296 + }, + "id": "xraJ9RRzsvel", + "outputId": "6e3308cf-8de0-4b89-9128-4c6ddf3598c0" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shobs/code/bigframes/venv/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3550: UserWarning: Reading cached table from 2024-06-28 02:49:31.716256+00:00 to avoid incompatibilies with previous reads of this table. To read the latest version, set `use_cache=False` or close the current session with Session.close() or bigframes.pandas.close_session().\n", + " exec(code_obj, self.user_global_ns, self.user_ns)\n" + ] + }, + { + "data": { + "text/html": [ + "Query job f72cda67-2a96-4cd2-a624-591c0d540fc9 is DONE. 582.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 65cf6ca3-73f0-49e6-84a8-1ff79af6ec75 is DONE. 82.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
homeTeamNameawayTeamNameduration_minutes
50RaysRangers181
72PhilliesPirates192
89MarinersBlue Jays183
351AstrosAngels212
382RoyalsYankees259
\n", + "
" + ], + "text/plain": [ + " homeTeamName awayTeamName duration_minutes\n", + "50 Rays Rangers 181\n", + "72 Phillies Pirates 192\n", + "89 Mariners Blue Jays 183\n", + "351 Astros Angels 212\n", + "382 Royals Yankees 259" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.read_gbq(\"bigquery-public-data.baseball.schedules\")[[\"homeTeamName\", \"awayTeamName\", \"duration_minutes\"]]\n", + "df.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Notes\n", + "\n", + "* The API reference documentation for the `remote_function` can be found at\n", + " https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.session.Session#bigframes_session_Session_remote_function\n", + "\n", + "* More code samples for `remote_function` can be found in the BigQuery\n", + " DataFrames API reference documentation, e.g.\n", + " * https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.series.Series#bigframes_series_Series_apply\n", + " * https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.dataframe.DataFrame#bigframes_dataframe_DataFrame_map\n", + " * https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.dataframe.DataFrame#bigframes_dataframe_DataFrame_apply\n", + "\n", + "* The following examples are only for the purpose of demonstrating\n", + "`remote_function` usage. They are not necessarily the best way to achieve the\n", + "end result.\n", + "\n", + "* In the examples in this notebook we are using `reuse=False` just as a caution\n", + " to avoid concurrent runs of this notebook in the same google cloud project\n", + " stepping over each other's remote function deployment. It may not be neccesary\n", + " in a simple use case." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Pt4mWYE1p5o8" + }, + "source": [ + "# Self-contained function\n", + "\n", + "Let's consider a scenario where we want to categorize the matches as short,\n", + "medium or long duration based on the `duration_minutes` column." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 52 + }, + "id": "VoCPBJ-ZpyeG", + "outputId": "19351206-116e-4da2-8ff0-f288b7745b27" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job f039d478-8dc4-4b60-8eda-179955e06586 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-862150459da5240a6df1ce01c59b32d8-em4ibov0' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_862150459da5240a6df1ce01c59b32d8_em4ibov0'.\n" + ] + } + ], + "source": [ + "@bpd.remote_function(reuse=False)\n", + "def duration_category(duration_minutes: int) -> str:\n", + " if duration_minutes < 90:\n", + " return \"short\"\n", + " elif duration_minutes < 180:\n", + " return \"medium\"\n", + " else:\n", + " return \"long\"\n", + "\n", + "print(f\"Created cloud function '{duration_category.bigframes_cloud_function}' and BQ remote function '{duration_category.bigframes_remote_function}'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 258 + }, + "id": "oXgDB70Lp5cG", + "outputId": "c08aade0-8b03-425b-fc26-deafd89275a4" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 23e95831-d913-4d2b-97f6-588fc7967455 is DONE. 58.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job bb8b3d13-a521-4d45-b4c8-5686c944a9f2 is DONE. 157.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2a4653f5-cc6b-4279-a45e-40f0f97090a7 is DONE. 98.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
homeTeamNameawayTeamNameduration_minutesduration_cat
1911DodgersAngels132medium
2365AthleticsAngels134medium
1977AthleticsAngels139medium
554CubsAngels142medium
654AstrosAngels143medium
\n", + "
" + ], + "text/plain": [ + " homeTeamName awayTeamName duration_minutes duration_cat\n", + "1911 Dodgers Angels 132 medium\n", + "2365 Athletics Angels 134 medium\n", + "1977 Athletics Angels 139 medium\n", + "554 Cubs Angels 142 medium\n", + "654 Astros Angels 143 medium" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df.assign(duration_cat=df[\"duration_minutes\"].apply(duration_category))\n", + "df1.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zTaNSVmuzEkc" + }, + "source": [ + "# Function referring to variables outside the function body\n", + "\n", + "Let's consider a slight variation of the earlier example where the labels for\n", + "the short, medium and long duration matches are defined outside the function\n", + "body. They would be captured at the time of `remote_function` deployment and\n", + "any change in their values in the notebook after the deployment will not\n", + "automatically propagate to the `remote_function`." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "id": "2UEmTbu4znyS" + }, + "outputs": [], + "source": [ + "DURATION_CATEGORY_SHORT = \"S\"\n", + "DURATION_CATEGORY_MEDIUM = \"M\"\n", + "DURATION_CATEGORY_LONG = \"L\"" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 52 + }, + "id": "G-73kpmrznHn", + "outputId": "b5923b7c-d412-43bf-9a20-3946154df81a" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 5d914fde-81ec-46eb-9219-9822f77dd9a2 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-f3231b74ec807496f4894218d5d40ed5-688mx7hi' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_f3231b74ec807496f4894218d5d40ed5_688mx7hi'.\n" + ] + } + ], + "source": [ + "@bpd.remote_function(reuse=False)\n", + "def duration_category(duration_minutes: int) -> str:\n", + " if duration_minutes < 90:\n", + " return DURATION_CATEGORY_SHORT\n", + " elif duration_minutes < 180:\n", + " return DURATION_CATEGORY_MEDIUM\n", + " else:\n", + " return DURATION_CATEGORY_LONG\n", + "\n", + "print(f\"Created cloud function '{duration_category.bigframes_cloud_function}' and BQ remote function '{duration_category.bigframes_remote_function}'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 258 + }, + "id": "DWHKsfF-z7rL", + "outputId": "c736b57f-1fcb-464a-f725-eb203265ddc2" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job b0b39944-1e69-4185-97ba-985178ee241f is DONE. 58.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 90d99515-eb5e-4bcd-bce5-292eea09770e is DONE. 147.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job eb31d033-c871-49c5-a75e-4427e376516f is DONE. 89.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
homeTeamNameawayTeamNameduration_minutesduration_cat
1911DodgersAngels132M
2365AthleticsAngels134M
1977AthleticsAngels139M
554CubsAngels142M
654AstrosAngels143M
\n", + "
" + ], + "text/plain": [ + " homeTeamName awayTeamName duration_minutes duration_cat\n", + "1911 Dodgers Angels 132 M\n", + "2365 Athletics Angels 134 M\n", + "1977 Athletics Angels 139 M\n", + "554 Cubs Angels 142 M\n", + "654 Astros Angels 143 M" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df.assign(duration_cat=df[\"duration_minutes\"].apply(duration_category))\n", + "df1.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J-1BIasNzKil" + }, + "source": [ + "# Function referring to imports (built-in) outside the function body\n", + "\n", + "Let's consider a scenario in which we want to categorize the matches in terms of\n", + "hour buckets. E.g. a match finishing in 0-60 minutes would be in 1h category,\n", + "61-120 minutes in 2h category and so on. The function itself makes use of the\n", + "`math` module (a built-in module in a standard python installation) which\n", + "happens to be imported outside the function body, let's say in one of the\n", + "previous cells. For the demo purpose we have aliased the import to `mymath`, but\n", + "it is not necessary.\n", + "\n", + "Later in the notebook we will see another example with a third-party module." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "id": "zlQfhcW41uzM" + }, + "outputs": [], + "source": [ + "import math as mymath" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 52 + }, + "id": "ktADchck2mh4", + "outputId": "9aed6aea-b361-4414-a0f6-8873e8291090" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 2895676f-d15c-40fd-8cf2-3a0436291e6b is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-9b20b0257558a42da610d8998022c25e-7k62x9l6' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_9b20b0257558a42da610d8998022c25e_7k62x9l6'.\n" + ] + } + ], + "source": [ + "@bpd.remote_function(reuse=False)\n", + "def duration_category(duration_minutes: int) -> str:\n", + " duration_hours = mymath.ceil(duration_minutes / 60)\n", + " return f\"{duration_hours}h\"\n", + "\n", + "print(f\"Created cloud function '{duration_category.bigframes_cloud_function}' and BQ remote function '{duration_category.bigframes_remote_function}'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 258 + }, + "id": "ywAtZlJU3GoB", + "outputId": "d3c93a31-3367-4ccf-bdf7-62d5bbff4461" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 4efda755-2f54-4477-b48a-4a424c888559 is DONE. 58.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a8992776-c2e8-4c3e-ab75-dfc01c5de89f is DONE. 150.1 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 3ea299b0-27ad-432b-8dbf-81da3aae884f is DONE. 91.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
homeTeamNameawayTeamNameduration_minutesduration_cat
1911DodgersAngels1323h
2365AthleticsAngels1343h
1977AthleticsAngels1393h
554CubsAngels1423h
654AstrosAngels1433h
\n", + "
" + ], + "text/plain": [ + " homeTeamName awayTeamName duration_minutes duration_cat\n", + "1911 Dodgers Angels 132 3h\n", + "2365 Athletics Angels 134 3h\n", + "1977 Athletics Angels 139 3h\n", + "554 Cubs Angels 142 3h\n", + "654 Astros Angels 143 3h" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df.assign(duration_cat=df[\"duration_minutes\"].apply(duration_category))\n", + "df1.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WO0FH7Bm3OxR" + }, + "source": [ + "# Function referring to another function outside the function body\n", + "\n", + "In this example let's create a `remote_function` from a function\n", + "`duration_category` which depends upon another function `get_hour_ceiling`,\n", + "which further depends on another function `get_minutes_in_hour`. This dependency\n", + "chain could be even longer in a real world example. The behaviors of the\n", + "dependencies would be captured at the time of the remote function\n", + "deployment.\n", + "\n", + "Please ntoe that any changes in those functions in the notebook after the\n", + "deployment would not automatically propagate to the remote function." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "id": "0G91fWiF3pKg" + }, + "outputs": [], + "source": [ + "import math\n", + "\n", + "def get_minutes_in_hour():\n", + " return 60\n", + "\n", + "def get_hour_ceiling(minutes):\n", + " return math.ceil(minutes / get_minutes_in_hour())" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 52 + }, + "id": "lQrC8T2031EJ", + "outputId": "420e7c3d-54cb-4814-f973-c7678be61caa" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 411853db-bf83-4df8-af78-55b1ceb39cb1 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-b54aa0aa752af6a3bd6d9d529dac373b-h4lgpy4y' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_b54aa0aa752af6a3bd6d9d529dac373b_h4lgpy4y'.\n" + ] + } + ], + "source": [ + "@bpd.remote_function(reuse=False)\n", + "def duration_category(duration_minutes: int) -> str:\n", + " duration_hours = get_hour_ceiling(duration_minutes)\n", + " return f\"{duration_hours} hrs\"\n", + "\n", + "print(f\"Created cloud function '{duration_category.bigframes_cloud_function}' and BQ remote function '{duration_category.bigframes_remote_function}'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 258 + }, + "id": "GVyrihii4EFG", + "outputId": "e979b649-4ed4-4b82-e814-54180420e3fc" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job d04abfa5-e2f2-4936-a708-ed97ef429df3 is DONE. 58.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2fc4edf0-7a86-4532-b8fb-bd3f5d153dcb is DONE. 157.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f7e6e18c-70d7-4b4e-926a-03b3a1abd1fe is DONE. 99.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
homeTeamNameawayTeamNameduration_minutesduration_cat
1911DodgersAngels1323 hrs
2365AthleticsAngels1343 hrs
1977AthleticsAngels1393 hrs
554CubsAngels1423 hrs
654AstrosAngels1433 hrs
\n", + "
" + ], + "text/plain": [ + " homeTeamName awayTeamName duration_minutes duration_cat\n", + "1911 Dodgers Angels 132 3 hrs\n", + "2365 Athletics Angels 134 3 hrs\n", + "1977 Athletics Angels 139 3 hrs\n", + "554 Cubs Angels 142 3 hrs\n", + "654 Astros Angels 143 3 hrs" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df.assign(duration_cat=df[\"duration_minutes\"].apply(duration_category))\n", + "df1.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Uu7SOoT94vSP" + }, + "source": [ + "# Function requiring external packages\n", + "\n", + "In this example let's say we want to redact the `homeTeamName` values, and we\n", + "choose to use a third party library `cryptography`. Any third party dependencies\n", + "can be specified in [pip format](https://pip.pypa.io/en/stable/reference/requirements-file-format/)\n", + "(with or without version number) as a list via the `packages` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "id": "3EUEyNcW41_l", + "outputId": "2d09d60f-da1a-4eab-86d3-0e62390a360c" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job c674e7b7-2349-4317-8f08-8bfd9aa99785 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "@bpd.remote_function(reuse=False, packages=[\"cryptography\"])\n", + "def get_hash(input: str) -> str:\n", + " from cryptography.fernet import Fernet\n", + "\n", + " # handle missing value\n", + " if input is None:\n", + " input = \"\"\n", + "\n", + " key = Fernet.generate_key()\n", + " f = Fernet(key)\n", + " return f.encrypt(input.encode()).decode()" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 258 + }, + "id": "OX1Hl7bR5uyd", + "outputId": "8ac3bf28-d16d-438b-b636-74ef2371715f" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job eb9384c9-de7d-4232-bdca-94b61b50ff89 is DONE. 60.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 11a736a5-96d1-4e62-90e2-576156131a94 is DONE. 388.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job c66a9ad1-60f7-4af1-ad7c-65e4eecbb035 is DONE. 330.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
homeTeamNameawayTeamNameduration_minuteshomeTeamNameRedacted
719AstrosAngels180gAAAAABmflbKCFygsmoTzFkUCObFSBJG29Ksk8HEtk82ib...
2295AstrosAngels204gAAAAABmflbKv-XzIxcNS92RO4fXYIAwA0kGWsAy-tI5fm...
1126AstrosAngels176gAAAAABmflbJdjgpqnfvmklU7Zg3NJUqlTMYMs44dLEkwg...
294AstrosAngels189gAAAAABmflbKmfBh4P3FnwyiIpVFek9TzF4GzwP_5rQmkv...
351AstrosAngels212gAAAAABmflbJ_mzqao9i7BtoYlMpb6y3bV3x7-cYuWGxsT...
\n", + "
" + ], + "text/plain": [ + " homeTeamName awayTeamName duration_minutes \\\n", + "719 Astros Angels 180 \n", + "2295 Astros Angels 204 \n", + "1126 Astros Angels 176 \n", + "294 Astros Angels 189 \n", + "351 Astros Angels 212 \n", + "\n", + " homeTeamNameRedacted \n", + "719 gAAAAABmflbKCFygsmoTzFkUCObFSBJG29Ksk8HEtk82ib... \n", + "2295 gAAAAABmflbKv-XzIxcNS92RO4fXYIAwA0kGWsAy-tI5fm... \n", + "1126 gAAAAABmflbJdjgpqnfvmklU7Zg3NJUqlTMYMs44dLEkwg... \n", + "294 gAAAAABmflbKmfBh4P3FnwyiIpVFek9TzF4GzwP_5rQmkv... \n", + "351 gAAAAABmflbJ_mzqao9i7BtoYlMpb6y3bV3x7-cYuWGxsT... " + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df.assign(homeTeamNameRedacted=df[\"homeTeamName\"].apply(get_hash))\n", + "df1.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Function referring to imports (third-party) outside the function body\n", + "\n", + "In this scenario the function depends on a third party library and the module\n", + "from the third party library used in the function is imported outside the\n", + "function body in a previous cell. Below is such an example where the third-party\n", + "dependency is `humanize` and its module of the same name is imported outside the\n", + "function body." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "import datetime as dt\n", + "import humanize" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 21b054a9-8fb2-418f-a17b-effdf5aba9b5 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-0879f72acd9b8ede460b69c5a8cc0dcb-edxlst27' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_0879f72acd9b8ede460b69c5a8cc0dcb_edxlst27'.\n" + ] + } + ], + "source": [ + "@bpd.remote_function(reuse=False, packages=[\"humanize\"])\n", + "def duration_category(duration_minutes: int) -> str:\n", + " timedelta = dt.timedelta(minutes=duration_minutes)\n", + " return humanize.naturaldelta(timedelta)\n", + "\n", + "print(f\"Created cloud function '{duration_category.bigframes_cloud_function}' and BQ remote function '{duration_category.bigframes_remote_function}'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job d67b7cb9-9813-4863-99d1-01cf45ab4949 is DONE. 58.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 579ba853-a7b8-49df-9539-bf22f08d2370 is DONE. 162.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 72f9eb5d-1c1a-4ce8-8f2f-1f5a8f7cec99 is DONE. 103.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
homeTeamNameawayTeamNameduration_minutesduration_cat
1911DodgersAngels1322 hours
2365AthleticsAngels1342 hours
1977AthleticsAngels1392 hours
554CubsAngels1422 hours
654AstrosAngels1432 hours
\n", + "
" + ], + "text/plain": [ + " homeTeamName awayTeamName duration_minutes duration_cat\n", + "1911 Dodgers Angels 132 2 hours\n", + "2365 Athletics Angels 134 2 hours\n", + "1977 Athletics Angels 139 2 hours\n", + "554 Cubs Angels 142 2 hours\n", + "654 Astros Angels 143 2 hours" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = df.assign(duration_cat=df[\"duration_minutes\"].apply(duration_category))\n", + "df1.peek()" + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From e22801047440eefef9c4acc1731ec7c5c1a62b88 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 3 Jul 2024 16:08:17 -0700 Subject: [PATCH 691/921] refactor: Create ordering base class to represent partial ordering (#814) --- bigframes/core/__init__.py | 4 +- bigframes/core/compile/api.py | 2 +- bigframes/core/compile/compiled.py | 23 ++-- bigframes/core/compile/compiler.py | 2 +- bigframes/core/compile/concat.py | 4 +- bigframes/core/compile/default_ordering.py | 2 +- bigframes/core/compile/single_column.py | 34 +----- bigframes/core/nodes.py | 2 +- bigframes/core/ordering.py | 117 +++++++++++++++++---- bigframes/session/__init__.py | 2 +- 10 files changed, 116 insertions(+), 76 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 00a36b9c051..9b858046bc4 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -78,7 +78,7 @@ def from_cached( cls, original: ArrayValue, table: google.cloud.bigquery.Table, - ordering: orderings.ExpressionOrdering, + ordering: orderings.TotalOrdering, ): node = nodes.CachedTableNode( original_node=original.node, @@ -147,7 +147,7 @@ def _compiled_schema(self) -> schemata.ArraySchema: def as_cached( self: ArrayValue, cache_table: google.cloud.bigquery.Table, - ordering: Optional[orderings.ExpressionOrdering], + ordering: Optional[orderings.TotalOrdering], ) -> ArrayValue: """ Replace the node with an equivalent one that references a tabel where the value has been materialized to. diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index 9fba3081ca5..1f7d0a45072 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -46,7 +46,7 @@ def compile_ordered( def compile_raw( node: bigframes.core.nodes.BigFrameNode, -) -> Tuple[str, bigframes.core.ordering.ExpressionOrdering]: +) -> Tuple[str, bigframes.core.ordering.TotalOrdering]: """Compile node into sql that exposes all columns, including hidden ordering-only columns.""" ir = compiler.compile_ordered_ir(node) sql = ir.raw_sql() diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 907c918efd2..cc601744c1b 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -36,9 +36,9 @@ from bigframes.core.ordering import ( ascending_over, encode_order_string, - ExpressionOrdering, IntegerEncoding, OrderingExpression, + TotalOrdering, ) import bigframes.core.schema as schemata import bigframes.core.sql @@ -194,7 +194,7 @@ def _aggregate_base( if by_column_ids: result = table.group_by(by_column_ids).aggregate(**stats) # Must have deterministic ordering, so order by the unique "by" column - ordering = ExpressionOrdering( + ordering = TotalOrdering( tuple([ascending_over(column_id) for column_id in by_column_ids]), total_ordering_columns=frozenset(by_column_ids), ) @@ -210,7 +210,7 @@ def _aggregate_base( # Ordering is irrelevant for single-row output, but set ordering id regardless # as other ops(join etc.) expect it. # TODO: Maybe can make completely empty - ordering = ExpressionOrdering( + ordering = TotalOrdering( ordering_value_columns=tuple([]), total_ordering_columns=frozenset([]), ) @@ -279,7 +279,7 @@ def row_count(self) -> OrderedIR: return OrderedIR( ibis_table, (ibis_table["count"],), - ordering=ExpressionOrdering( + ordering=TotalOrdering( ordering_value_columns=(ascending_over("count"),), total_ordering_columns=frozenset(["count"]), ), @@ -519,7 +519,7 @@ def __init__( table: ibis_types.Table, columns: Sequence[ibis_types.Value], hidden_ordering_columns: Optional[Sequence[ibis_types.Value]] = None, - ordering: ExpressionOrdering = ExpressionOrdering(), + ordering: TotalOrdering = TotalOrdering(), predicates: Optional[Collection[ibis_types.BooleanValue]] = None, ): super().__init__(table, columns, predicates) @@ -598,10 +598,7 @@ def from_pandas( return cls( keys_memtable, columns=[keys_memtable[column].name(column) for column in pd_df.columns], - ordering=ExpressionOrdering( - ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - ), + ordering=TotalOrdering.from_offset_col(ORDER_ID_COLUMN), hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), ) @@ -760,7 +757,7 @@ def explode(self, column_ids: typing.Sequence[str]) -> OrderedIR: ], table_w_unnest[unnest_offset_id], ] - ordering = ExpressionOrdering( + ordering = TotalOrdering( ordering_value_columns=tuple( [ *self._ordering.ordering_value_columns, @@ -1153,7 +1150,7 @@ def _bake_ordering(self) -> OrderedIR: self._ibis_bindings[expr.scalar_expression.id] ) - new_ordering = ExpressionOrdering( + new_ordering = TotalOrdering( tuple(new_exprs), self._ordering.integer_encoding, self._ordering.string_encoding, @@ -1176,7 +1173,7 @@ def _project_offsets(self) -> OrderedIR: ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN ) columns = [table[column_name] for column_name in self._column_names] - ordering = ExpressionOrdering( + ordering = TotalOrdering( ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), total_ordering_columns=frozenset([ORDER_ID_COLUMN]), integer_encoding=IntegerEncoding(True, is_sequential=True), @@ -1300,7 +1297,7 @@ class Builder: def __init__( self, table: ibis_types.Table, - ordering: ExpressionOrdering, + ordering: TotalOrdering, columns: Collection[ibis_types.Value] = (), hidden_ordering_columns: Collection[ibis_types.Value] = (), predicates: Optional[Collection[ibis_types.BooleanValue]] = None, diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 9272b8ad1b0..ca9c479fffb 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -195,7 +195,7 @@ def compile_read_table_ordered(node: nodes.ReadTableNode): ) else: integer_encoding = bf_ordering.IntegerEncoding() - ordering = bf_ordering.ExpressionOrdering( + ordering = bf_ordering.TotalOrdering( ordering_value_columns, integer_encoding=integer_encoding, total_ordering_columns=frozenset(node.total_order_cols), diff --git a/bigframes/core/compile/concat.py b/bigframes/core/compile/concat.py index 41a8f97821b..35cf9ec5b46 100644 --- a/bigframes/core/compile/concat.py +++ b/bigframes/core/compile/concat.py @@ -21,9 +21,9 @@ import bigframes.core.compile.compiled as compiled from bigframes.core.ordering import ( ascending_over, - ExpressionOrdering, reencode_order_string, StringEncoding, + TotalOrdering, ) ORDER_ID_COLUMN = "bigframes_ordering_id" @@ -83,7 +83,7 @@ def concat_ordered( ) tables.append(table) combined_table = ibis.union(*tables) - ordering = ExpressionOrdering( + ordering = TotalOrdering( ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), total_ordering_columns=frozenset([ORDER_ID_COLUMN]), string_encoding=StringEncoding(True, prefix_size + max_encoding_size), diff --git a/bigframes/core/compile/default_ordering.py b/bigframes/core/compile/default_ordering.py index d8bdc0546bb..7d7a41f742a 100644 --- a/bigframes/core/compile/default_ordering.py +++ b/bigframes/core/compile/default_ordering.py @@ -82,7 +82,7 @@ def gen_default_ordering(table: ibis.table, use_double_hash: bool = True): itertools.chain(original_column_ids, order_values) ) - ordering = order.ExpressionOrdering( + ordering = order.TotalOrdering( ordering_value_columns=tuple( order.ascending_over(col.get_name()) for col in order_values ), diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index dbf25891bf4..9b621c9c79d 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -16,8 +16,6 @@ from __future__ import annotations -from typing import Mapping - import ibis import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types @@ -84,7 +82,7 @@ def join_by_column_ordered( ) # Preserve ordering accross joins. - ordering = join_orderings( + ordering = orderings.join_orderings( left._ordering, right._ordering, l_mapping, @@ -173,33 +171,3 @@ def value_to_join_key(value: ibis_types.Value): if not value.type().is_string(): value = value.cast(ibis_dtypes.str) return value.fillna(ibis_types.literal("$NULL_SENTINEL$")) - - -def join_orderings( - left: orderings.ExpressionOrdering, - right: orderings.ExpressionOrdering, - left_id_mapping: Mapping[str, str], - right_id_mapping: Mapping[str, str], - left_order_dominates: bool = True, -) -> orderings.ExpressionOrdering: - left_ordering_refs = [ - ref.remap_names(left_id_mapping) for ref in left.all_ordering_columns - ] - right_ordering_refs = [ - ref.remap_names(right_id_mapping) for ref in right.all_ordering_columns - ] - if left_order_dominates: - joined_refs = [*left_ordering_refs, *right_ordering_refs] - else: - joined_refs = [*right_ordering_refs, *left_ordering_refs] - - left_total_order_cols = frozenset( - [left_id_mapping[id] for id in left.total_ordering_columns] - ) - right_total_order_cols = frozenset( - [right_id_mapping[id] for id in right.total_ordering_columns] - ) - return orderings.ExpressionOrdering( - ordering_value_columns=tuple(joined_refs), - total_ordering_columns=left_total_order_cols | right_total_order_cols, - ) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 65a1bd80846..a703cf1969d 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -373,7 +373,7 @@ class CachedTableNode(BigFrameNode): table_id: str = field() physical_schema: Tuple[bq.SchemaField, ...] = field() - ordering: typing.Optional[orderings.ExpressionOrdering] = field() + ordering: typing.Optional[orderings.TotalOrdering] = field() def __post_init__(self): # enforce invariants diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 15625927205..406ca52731b 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -94,10 +94,42 @@ class IntegerEncoding: @dataclass(frozen=True) -class ExpressionOrdering: - """Immutable object that holds information about the ordering of rows in a ArrayValue object.""" +class RowOrdering: + """Immutable object that holds information about the ordering of rows in a ArrayValue object. May not be unambiguous.""" ordering_value_columns: typing.Tuple[OrderingExpression, ...] = () + + @property + def all_ordering_columns(self) -> Sequence[OrderingExpression]: + return list(self.ordering_value_columns) + + @property + def referenced_columns(self) -> Set[str]: + return set( + col + for part in self.ordering_value_columns + for col in part.scalar_expression.unbound_variables + ) + + def with_reverse(self) -> RowOrdering: + """Reverses the ordering.""" + return RowOrdering( + tuple([col.with_reverse() for col in self.ordering_value_columns]), + ) + + def with_column_remap(self, mapping: typing.Mapping[str, str]) -> RowOrdering: + new_value_columns = [ + col.remap_names(mapping) for col in self.all_ordering_columns + ] + return TotalOrdering( + tuple(new_value_columns), + ) + + +@dataclass(frozen=True) +class TotalOrdering(RowOrdering): + """Immutable object that holds information about the ordering of rows in a ArrayValue object. Guaranteed to be unambiguous.""" + integer_encoding: IntegerEncoding = IntegerEncoding(False) string_encoding: StringEncoding = StringEncoding(False) # A table has a total ordering defined by the identities of a set of 1 or more columns. @@ -106,8 +138,8 @@ class ExpressionOrdering: total_ordering_columns: frozenset[str] = field(default_factory=frozenset) @classmethod - def from_offset_col(cls, col: str) -> ExpressionOrdering: - return ExpressionOrdering( + def from_offset_col(cls, col: str) -> TotalOrdering: + return TotalOrdering( (ascending_over(col),), integer_encoding=IntegerEncoding(True, is_sequential=True), total_ordering_columns=frozenset({col}), @@ -119,7 +151,7 @@ def with_non_sequential(self): This is useful when filtering, but not sorting, an expression. """ if self.integer_encoding.is_sequential: - return ExpressionOrdering( + return TotalOrdering( self.ordering_value_columns, integer_encoding=IntegerEncoding( self.integer_encoding.is_encoded, is_sequential=False @@ -132,7 +164,7 @@ def with_non_sequential(self): def with_ordering_columns( self, ordering_value_columns: Sequence[OrderingExpression] = (), - ) -> ExpressionOrdering: + ) -> TotalOrdering: """Creates a new ordering that reorders by the given columns. Args: @@ -147,7 +179,7 @@ def with_ordering_columns( new_ordering = self._truncate_ordering( (*ordering_value_columns, *self.ordering_value_columns) ) - return ExpressionOrdering( + return TotalOrdering( new_ordering, total_ordering_columns=self.total_ordering_columns, ) @@ -173,7 +205,7 @@ def _truncate_ordering( def with_reverse(self): """Reverses the ordering.""" - return ExpressionOrdering( + return TotalOrdering( tuple([col.with_reverse() for col in self.ordering_value_columns]), total_ordering_columns=self.total_ordering_columns, ) @@ -185,7 +217,7 @@ def with_column_remap(self, mapping: typing.Mapping[str, str]): new_total_order = frozenset( mapping.get(col_id, col_id) for col_id in self.total_ordering_columns ) - return ExpressionOrdering( + return TotalOrdering( tuple(new_value_columns), integer_encoding=self.integer_encoding, string_encoding=self.string_encoding, @@ -211,18 +243,6 @@ def is_string_encoded(self) -> bool: def is_sequential(self) -> bool: return self.integer_encoding.is_encoded and self.integer_encoding.is_sequential - @property - def all_ordering_columns(self) -> Sequence[OrderingExpression]: - return list(self.ordering_value_columns) - - @property - def referenced_columns(self) -> Set[str]: - return set( - col - for part in self.ordering_value_columns - for col in part.scalar_expression.unbound_variables - ) - def encode_order_string( order_id: ibis_types.IntegerColumn, length: int = DEFAULT_ORDERING_ID_LENGTH @@ -257,3 +277,58 @@ def descending_over(id: str, nulls_last: bool = True) -> OrderingExpression: return OrderingExpression( expression.free_var(id), direction=OrderingDirection.DESC, na_last=nulls_last ) + + +@typing.overload +def join_orderings( + left: TotalOrdering, + right: TotalOrdering, + left_id_mapping: Mapping[str, str], + right_id_mapping: Mapping[str, str], + left_order_dominates: bool = True, +) -> TotalOrdering: + ... + + +@typing.overload +def join_orderings( + left: RowOrdering, + right: RowOrdering, + left_id_mapping: Mapping[str, str], + right_id_mapping: Mapping[str, str], + left_order_dominates: bool = True, +) -> RowOrdering: + ... + + +def join_orderings( + left: RowOrdering, + right: RowOrdering, + left_id_mapping: Mapping[str, str], + right_id_mapping: Mapping[str, str], + left_order_dominates: bool = True, +) -> RowOrdering: + left_ordering_refs = [ + ref.remap_names(left_id_mapping) for ref in left.all_ordering_columns + ] + right_ordering_refs = [ + ref.remap_names(right_id_mapping) for ref in right.all_ordering_columns + ] + if left_order_dominates: + joined_refs = [*left_ordering_refs, *right_ordering_refs] + else: + joined_refs = [*right_ordering_refs, *left_ordering_refs] + + if isinstance(left, TotalOrdering) and isinstance(right, TotalOrdering): + left_total_order_cols = frozenset( + [left_id_mapping[id] for id in left.total_ordering_columns] + ) + right_total_order_cols = frozenset( + [right_id_mapping[id] for id in right.total_ordering_columns] + ) + return TotalOrdering( + ordering_value_columns=tuple(joined_refs), + total_ordering_columns=left_total_order_cols | right_total_order_cols, + ) + else: + return RowOrdering(tuple(joined_refs)) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 867bdedf1ca..02d30cdbd28 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1910,7 +1910,7 @@ def _cache_with_offsets(self, array_value: core.ArrayValue): ) cached_replacement = array_value.as_cached( cache_table=self.bqclient.get_table(tmp_table), - ordering=order.ExpressionOrdering.from_offset_col(offset_column), + ordering=order.TotalOrdering.from_offset_col(offset_column), ).node self._cached_executions[array_value.node] = cached_replacement From 74170dabd323f1b08ad76241e37ff9f2a5b67ab5 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:05:52 -0700 Subject: [PATCH 692/921] docs: remove session and connection in llm notebook (#821) --- .../generative_ai/large_language_models.ipynb | 106 +++++++----------- 1 file changed, 41 insertions(+), 65 deletions(-) diff --git a/notebooks/generative_ai/large_language_models.ipynb b/notebooks/generative_ai/large_language_models.ipynb index 08ef52b544f..744706cab8a 100644 --- a/notebooks/generative_ai/large_language_models.ipynb +++ b/notebooks/generative_ai/large_language_models.ipynb @@ -16,8 +16,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Prerequisites\n", - "Create session and define a BQ connection which we already created and allowlisted. " + "## Define the model" ] }, { @@ -29,33 +28,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/garrettwu/src/bigframes/bigframes/session/__init__.py:1762: UserWarning: No explicit location is set, so using location US for the session.\n", - " return Session(context)\n" + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/ml/llm.py:589: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", + " self.session = session or bpd.get_global_session()\n" ] - } - ], - "source": [ - "session = bigframes.pandas.get_global_session()\n", - "connection = f\"{session.bqclient.project}.us.bigframes-default-connection\"" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define the model" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ + }, { "data": { "text/html": [ - "Query job 12bcd690-ca99-4001-bf26-032f50e77d62 is DONE. 0 Bytes processed. Open Job" + "Query job 675a6c8a-213b-496c-9f77-b87bf7cfa5e0 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -66,7 +46,7 @@ } ], "source": [ - "model = GeminiTextGenerator(session=session, connection_name=connection)" + "model = GeminiTextGenerator()" ] }, { @@ -81,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -102,13 +82,13 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job f8fe31c6-7d8a-4919-9492-8304a0083cca is DONE. 0 Bytes processed. Open Job" + "Query job 7967df2b-9f0f-45c8-a363-15f65891c3bf is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -118,21 +98,17 @@ "output_type": "display_data" }, { - "data": { - "text/html": [ - "Query job 28bab71f-e218-4d92-9a50-dab41bb0c71f is DONE. 24 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/__init__.py:108: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] }, { "data": { "text/html": [ - "Query job 01d66b61-459f-474e-9f66-d519f9c2f23d is DONE. 6 Bytes processed. Open Job" + "Query job 9a1f57cd-98e1-4eac-a1b3-8f88d61971cd is DONE. 6 Bytes processed. Open Job" ], "text/plain": [ "" @@ -144,7 +120,7 @@ { "data": { "text/html": [ - "Query job af606ca7-4bcf-4bd1-95fd-c516542b5a4f is DONE. 5.3 kB processed. Open Job" + "Query job 2a94a2cf-7d4c-4009-a798-d7a5d6d4049d is DONE. 8.5 kB processed. Open Job" ], "text/plain": [ "" @@ -183,28 +159,28 @@ " \n", " \n", " 0\n", - " **BigQuery**\n", + " ## BigQuery: A Serverless Data Warehouse\n", "\n", - "**Definition:**\n", - "\n", - "BigQuery is a s...\n", - " null\n", + "BigQ...\n", + " [{\"category\":1,\"probability\":1,\"probability_sc...\n", " \n", " What is BigQuery?\n", " \n", " \n", " 1\n", - " **BigQuery Machine Learning (BQML)**\n", + " ## BigQuery Machine Learning (BQML)\n", "\n", - "BQML is ...\n", - " null\n", + "BQML is a...\n", + " [{\"category\":1,\"probability\":1,\"probability_sc...\n", " \n", " What is BQML?\n", " \n", " \n", " 2\n", - " BigQuery DataFrame is a Python DataFrame imple...\n", - " null\n", + " ## What is BigQuery DataFrame?\n", + "\n", + "**BigQuery Dat...\n", + " [{\"category\":1,\"probability\":1,\"probability_sc...\n", " \n", " What is BigQuery DataFrame?\n", " \n", @@ -214,20 +190,20 @@ ], "text/plain": [ " ml_generate_text_llm_result \\\n", - "0 **BigQuery**\n", + "0 ## BigQuery: A Serverless Data Warehouse\n", "\n", - "**Definition:**\n", + "BigQ... \n", + "1 ## BigQuery Machine Learning (BQML)\n", "\n", - "BigQuery is a s... \n", - "1 **BigQuery Machine Learning (BQML)**\n", + "BQML is a... \n", + "2 ## What is BigQuery DataFrame?\n", "\n", - "BQML is ... \n", - "2 BigQuery DataFrame is a Python DataFrame imple... \n", + "**BigQuery Dat... \n", "\n", - " ml_generate_text_rai_result ml_generate_text_status \\\n", - "0 null \n", - "1 null \n", - "2 null \n", + " ml_generate_text_rai_result ml_generate_text_status \\\n", + "0 [{\"category\":1,\"probability\":1,\"probability_sc... \n", + "1 [{\"category\":1,\"probability\":1,\"probability_sc... \n", + "2 [{\"category\":1,\"probability\":1,\"probability_sc... \n", "\n", " prompt \n", "0 What is BigQuery? \n", @@ -235,7 +211,7 @@ "2 What is BigQuery DataFrame? " ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -255,16 +231,16 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'**BigQuery**\\n\\n**Definition:**\\n\\nBigQuery is a serverless, highly scalable, cloud-based data warehouse and analytics platform offered by Google Cloud.\\n\\n**Key Features:**\\n\\n* **Massive Scalability:** Can handle large datasets (petabytes or more) with fast query execution.\\n* **Elastic:** Automatically scales compute resources based on workload requirements.\\n* **Serverless:** Users do not need to manage infrastructure or provision resources.\\n* **Flexible Data Loading:** Supports a wide range of data sources, including files, databases, and streaming data.\\n* **SQL-Based Querying:** Uses standard SQL syntax for querying and analyzing data.\\n* **Machine Learning Integration:** Provides built-in machine learning capabilities for predictive analytics and data exploration.\\n* **Real-Time Analysis:** Supports streaming data analysis and interactive dashboards.\\n* **Collaboration and Sharing:** Allows multiple users to access and analyze data in a collaborative environment.\\n* **Cost-Effective:** Pay-as-you-go pricing based on data scanned and compute resources used.\\n\\n**Applications:**\\n\\n* Data warehousing and analytics\\n* Business intelligence and reporting\\n* Data science and machine learning\\n* Data exploration and visualization\\n* Marketing analytics\\n* Fraud detection and risk management\\n\\n**Benefits:**\\n\\n* Rapid data analysis on large datasets\\n* Reduced infrastructure management overhead\\n* Increased agility and flexibility\\n* Enhanced collaboration and data sharing\\n* Cost-effective data storage and analytics'" + "\"## BigQuery: A Serverless Data Warehouse\\n\\nBigQuery is a serverless, cloud-based data warehouse that enables scalable analysis of large datasets. It's a popular choice for businesses of all sizes due to its ability to handle petabytes of data and run complex queries quickly and efficiently. Let's delve into its key features:\\n\\n**Serverless Architecture:** BigQuery eliminates the need for server management, allowing you to focus on analyzing data. Google manages the infrastructure, scaling resources up or down automatically based on your needs.\\n\\n**Scalability:** BigQuery can handle massive datasets, scaling seamlessly as your data volume grows. It automatically distributes queries across its infrastructure, ensuring fast and efficient processing.\\n\\n**SQL-like Querying:** BigQuery uses a familiar SQL-like syntax, making it easy for data analysts and developers to learn and use. This allows them to leverage their existing SQL knowledge for data exploration and analysis.\\n\\n**Cost-Effectiveness:** BigQuery offers a pay-as-you-go pricing model, meaning you only pay for the resources you use. This makes it a cost-effective solution for businesses with varying data processing needs.\\n\\n**Integration with Google Cloud:** BigQuery integrates seamlessly with other Google Cloud services like Cloud Storage, Dataflow, and Machine Learning, enabling a comprehensive data processing and analysis workflow within the Google Cloud ecosystem.\\n\\n**Security and Reliability:** BigQuery offers robust security features and high availability, ensuring data protection and reliable access.\\n\\n**Use Cases:** BigQuery finds applications in various scenarios, including:\\n\\n* **Data Warehousing:** Store and analyze large amounts of structured and semi-structured data.\\n* **Business Intelligence:** Generate insights from data for informed decision-making.\\n* **Data Analytics:** Perform complex data analysis and extract valuable patterns.\\n* **Machine Learning:** Train and deploy machine learning models on large datasets.\\n\\n**Getting Started:** To get started with BigQuery, you can create a free trial account on Google Cloud Platform and explore its features. Numerous tutorials and documentation are available to help you learn and use BigQuery effectively.\\n\\n## Additional Resources:\\n\\n* **BigQuery Documentation:** https://cloud.google.com/bigquery/docs/\\n* **BigQuery Quickstart:** https://cloud.google.com/bigquery/docs/quickstarts/quickstart-console\\n* **BigQuery Pricing:** https://cloud.google.com/bigquery/pricing\\n\\nFeel free to ask if you have any further questions about BigQuery!\"" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } From 067ff173f0abfcf5bf06d3fbdb6d12e0fa5283c3 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 8 Jul 2024 10:47:50 -0700 Subject: [PATCH 693/921] docs: remove the experimental flask icon from the public docs (#820) --- docs/templates/toc.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index aee3058ecca..95bded9a600 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -204,4 +204,3 @@ uid: bigframes.bigquery name: bigframes.bigquery name: BigQuery DataFrames - status: beta From 7cae3f41cee2600bc901e084974203931b065ee2 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:44:18 -0700 Subject: [PATCH 694/921] chore: remove an unused parameter in _rows_to_dataframe (#813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [X] Ensure the tests and linter pass - [X] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/blocks.py | 7 ++----- bigframes/session/__init__.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 25acfe2f429..fef91f88dcc 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -438,9 +438,7 @@ def reorder_levels(self, ids: typing.Sequence[str]): def _to_dataframe(self, result) -> pd.DataFrame: """Convert BigQuery data to pandas DataFrame with specific dtypes.""" - dtypes = dict(zip(self.index_columns, self.index.dtypes)) - dtypes.update(zip(self.value_columns, self.dtypes)) - result_dataframe = self.session._rows_to_dataframe(result, dtypes) + result_dataframe = self.session._rows_to_dataframe(result) # Runs strict validations to ensure internal type predictions and ibis are completely in sync # Do not execute these validations outside of testing suite. if "PYTEST_CURRENT_TEST" in os.environ: @@ -2582,7 +2580,6 @@ def to_pandas(self, *, ordered: Optional[bool] = None) -> pd.Index: ) # Project down to only the index column. So the query can be cached to visualize other data. index_columns = list(self._block.index_columns) - dtypes = dict(zip(index_columns, self.dtypes)) expr = self._expr.select_columns(index_columns) results, _ = self.session._execute( expr, @@ -2590,7 +2587,7 @@ def to_pandas(self, *, ordered: Optional[bool] = None) -> pd.Index: if (ordered is not None) else self.session._strictly_ordered, ) - df = expr.session._rows_to_dataframe(results, dtypes) + df = expr.session._rows_to_dataframe(results) df = df.set_index(index_columns) index = df.index index.names = list(self._block._index_labels) # type:ignore diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 02d30cdbd28..0f7953d3d4b 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -2038,7 +2038,7 @@ def _get_table_size(self, destination_table): return table.num_bytes def _rows_to_dataframe( - self, row_iterator: bigquery.table.RowIterator, dtypes: Dict + self, row_iterator: bigquery.table.RowIterator ) -> pandas.DataFrame: # Can ignore inferred datatype until dtype emulation breaks 1:1 mapping between BQ types and bigframes types dtypes_from_bq = bigframes.dtypes.bf_type_from_type_kind(row_iterator.schema) From ee2b660f15e1ab29de6e8abfba093c7839b14918 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:28:02 -0700 Subject: [PATCH 695/921] chore(main): release 1.11.1 (#822) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ bigframes/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b27da8d3014..82495157192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.11.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.11.0...v1.11.1) (2024-07-08) + + +### Documentation + +* Remove session and connection in llm notebook ([#821](https://github.com/googleapis/python-bigquery-dataframes/issues/821)) ([74170da](https://github.com/googleapis/python-bigquery-dataframes/commit/74170dabd323f1b08ad76241e37ff9f2a5b67ab5)) +* Remove the experimental flask icon from the public docs ([#820](https://github.com/googleapis/python-bigquery-dataframes/issues/820)) ([067ff17](https://github.com/googleapis/python-bigquery-dataframes/commit/067ff173f0abfcf5bf06d3fbdb6d12e0fa5283c3)) + ## [1.11.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.10.0...v1.11.0) (2024-07-01) diff --git a/bigframes/version.py b/bigframes/version.py index 75f3ffb3610..1186811c97f 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.0" +__version__ = "1.11.1" From f3b0f542ab516c768de6dfc230b9f9e9eed567ba Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:14:46 -0500 Subject: [PATCH 696/921] chore(python): use python 3.10 for docs build (#828) Source-Link: https://github.com/googleapis/synthtool/commit/9ae07858520bf035a3d5be569b5a65d960ee4392 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:52210e0e0559f5ea8c52be148b33504022e1faef4e95fbe4b32d68022af2fa7e Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 +-- .kokoro/docker/docs/Dockerfile | 21 +++++++------ .kokoro/docker/docs/requirements.txt | 40 +++++++++++++----------- .kokoro/requirements.txt | 46 ++++++++++++++-------------- 4 files changed, 59 insertions(+), 52 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 91d742b5b9f..f30cb3775af 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:d3de8a02819f65001effcbd3ea76ce97e9bcff035c7a89457f40f892c87c5b32 -# created: 2024-07-03T17:43:00.77142528Z + digest: sha256:52210e0e0559f5ea8c52be148b33504022e1faef4e95fbe4b32d68022af2fa7e +# created: 2024-07-08T19:25:35.862283192Z diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index a26ce61930f..5205308b334 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ubuntu:22.04 +from ubuntu:24.04 ENV DEBIAN_FRONTEND noninteractive @@ -40,7 +40,6 @@ RUN apt-get update \ libssl-dev \ libsqlite3-dev \ portaudio19-dev \ - python3-distutils \ redis-server \ software-properties-common \ ssh \ @@ -60,18 +59,22 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* \ && rm -f /var/cache/apt/archives/*.deb -###################### Install python 3.9.13 -# Download python 3.9.13 -RUN wget https://www.python.org/ftp/python/3.9.13/Python-3.9.13.tgz +###################### Install python 3.10.14 for docs/docfx session + +# Download python 3.10.14 +RUN wget https://www.python.org/ftp/python/3.10.14/Python-3.10.14.tgz # Extract files -RUN tar -xvf Python-3.9.13.tgz +RUN tar -xvf Python-3.10.14.tgz -# Install python 3.9.13 -RUN ./Python-3.9.13/configure --enable-optimizations +# Install python 3.10.14 +RUN ./Python-3.10.14/configure --enable-optimizations RUN make altinstall +RUN python3.10 -m venv /venv +ENV PATH /venv/bin:$PATH + ###################### Install pip RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ && python3 /tmp/get-pip.py \ @@ -84,4 +87,4 @@ RUN python3 -m pip COPY requirements.txt /requirements.txt RUN python3 -m pip install --require-hashes -r requirements.txt -CMD ["python3.8"] +CMD ["python3.10"] diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index 0e5d70f20f8..7129c771559 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -4,9 +4,9 @@ # # pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.2.3 \ - --hash=sha256:bf7900329262e481be5a15f56f19736b376df6f82ed27576fa893652c5de6c23 \ - --hash=sha256:c12355e0494c76a2a7b73e3a59b09024ca0ba1e279fb9ed6c1b82d5b74b6a70c +argcomplete==3.4.0 \ + --hash=sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5 \ + --hash=sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f # via nox colorlog==6.8.2 \ --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ @@ -16,23 +16,27 @@ distlib==0.3.8 \ --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 # via virtualenv -filelock==3.13.1 \ - --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ - --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c +filelock==3.15.4 \ + --hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \ + --hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7 # via virtualenv -nox==2024.3.2 \ - --hash=sha256:e53514173ac0b98dd47585096a55572fe504fecede58ced708979184d05440be \ - --hash=sha256:f521ae08a15adbf5e11f16cb34e8d0e6ea521e0b92868f684e91677deb974553 +nox==2024.4.15 \ + --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ + --hash=sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f # via -r requirements.in -packaging==24.0 \ - --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ - --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 # via nox -platformdirs==4.2.0 \ - --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ - --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 # via virtualenv -virtualenv==20.25.1 \ - --hash=sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a \ - --hash=sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197 +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via nox +virtualenv==20.26.3 \ + --hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \ + --hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589 # via nox diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 35ece0e4d2e..9622baf0ba3 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -20,9 +20,9 @@ cachetools==5.3.3 \ --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 # via google-auth -certifi==2024.6.2 \ - --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ - --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 +certifi==2024.7.4 \ + --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ + --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 # via requests cffi==1.16.0 \ --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ @@ -371,23 +371,23 @@ more-itertools==10.3.0 \ # via # jaraco-classes # jaraco-functools -nh3==0.2.17 \ - --hash=sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a \ - --hash=sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911 \ - --hash=sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb \ - --hash=sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a \ - --hash=sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc \ - --hash=sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028 \ - --hash=sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9 \ - --hash=sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3 \ - --hash=sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351 \ - --hash=sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10 \ - --hash=sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71 \ - --hash=sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f \ - --hash=sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b \ - --hash=sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a \ - --hash=sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062 \ - --hash=sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a +nh3==0.2.18 \ + --hash=sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164 \ + --hash=sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86 \ + --hash=sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b \ + --hash=sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad \ + --hash=sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204 \ + --hash=sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a \ + --hash=sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200 \ + --hash=sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189 \ + --hash=sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f \ + --hash=sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811 \ + --hash=sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844 \ + --hash=sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4 \ + --hash=sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be \ + --hash=sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50 \ + --hash=sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307 \ + --hash=sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe # via readme-renderer nox==2024.4.15 \ --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ @@ -460,9 +460,9 @@ python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via gcp-releasetool -readme-renderer==43.0 \ - --hash=sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311 \ - --hash=sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9 +readme-renderer==44.0 \ + --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ + --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 # via twine requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ From cdfd979596f8dcb5f6266f56d37689203a4d8e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 9 Jul 2024 16:57:03 -0500 Subject: [PATCH 697/921] chore: remove references to conda (#830) --- noxfile.py | 2 -- third_party/bigframes_vendored/ibis/README.md | 15 --------------- third_party/bigframes_vendored/pandas/README.md | 10 ++-------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/noxfile.py b/noxfile.py index 177e0e2ab88..b38bcacfdbe 100644 --- a/noxfile.py +++ b/noxfile.py @@ -552,8 +552,6 @@ def prerelease(session: nox.sessions.Session, tests_path): already_installed.add("pyarrow") session.install( - "--extra-index-url", - "https://pypi.anaconda.org/scipy-wheels-nightly/simple", "--prefer-binary", "--pre", "--upgrade", diff --git a/third_party/bigframes_vendored/ibis/README.md b/third_party/bigframes_vendored/ibis/README.md index 8a00750e920..fa8224214f4 100644 --- a/third_party/bigframes_vendored/ibis/README.md +++ b/third_party/bigframes_vendored/ibis/README.md @@ -1,7 +1,6 @@ # Ibis [![Documentation Status](https://img.shields.io/badge/docs-docs.ibis--project.org-blue.svg)](http://ibis-project.org) -[![Anaconda-Server Badge](https://anaconda.org/conda-forge/ibis-framework/badges/version.svg)](https://anaconda.org/conda-forge/ibis-framework) [![PyPI](https://img.shields.io/pypi/v/ibis-framework.svg)](https://pypi.org/project/ibis-framework) [![Build status](https://github.com/ibis-project/ibis/actions/workflows/ibis-main.yml/badge.svg)](https://github.com/ibis-project/ibis/actions/workflows/ibis-main.yml?query=branch%3Amaster) [![Build status](https://github.com/ibis-project/ibis/actions/workflows/ibis-backends.yml/badge.svg)](https://github.com/ibis-project/ibis/actions/workflows/ibis-backends.yml?query=branch%3Amaster) @@ -83,28 +82,14 @@ Install Ibis from PyPI with: pip install 'ibis-framework[duckdb]' ``` -Or from conda-forge with: - -```bash -conda install ibis-framework -c conda-forge -``` - (It’s a common mistake to `pip install ibis`. If you try to use Ibis and get errors early on try uninstalling `ibis` and installing `ibis-framework`) -To discover ibis, we suggest starting with the DuckDB backend (which is included by default in the conda-forge package). The DuckDB backend is performant and fully featured. - To use ibis with other backends, include the backend name in brackets for PyPI: ```bash pip install 'ibis-framework[postgres]' ``` -Or use `ibis-$BACKEND` where `$BACKEND` is the specific backend you want to use when installing from conda-forge: - -```bash -conda install ibis-postgres -c conda-forge -``` - ## Getting Started with Ibis We provide a number of tutorial and example notebooks in the diff --git a/third_party/bigframes_vendored/pandas/README.md b/third_party/bigframes_vendored/pandas/README.md index 9f2bc800e84..1aa5068d5e5 100644 --- a/third_party/bigframes_vendored/pandas/README.md +++ b/third_party/bigframes_vendored/pandas/README.md @@ -6,7 +6,6 @@ # pandas: powerful Python data analysis toolkit [![PyPI Latest Release](https://img.shields.io/pypi/v/pandas.svg)](https://pypi.org/project/pandas/) -[![Conda Latest Release](https://anaconda.org/conda-forge/pandas/badges/version.svg)](https://anaconda.org/anaconda/pandas/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3509134.svg)](https://doi.org/10.5281/zenodo.3509134) [![Package Status](https://img.shields.io/pypi/status/pandas.svg)](https://pypi.org/project/pandas/) [![License](https://img.shields.io/pypi/l/pandas.svg)](https://github.com/pandas-dev/pandas/blob/main/LICENSE) @@ -86,15 +85,10 @@ The source code is currently hosted on GitHub at: https://github.com/pandas-dev/pandas Binary installers for the latest released version are available at the [Python -Package Index (PyPI)](https://pypi.org/project/pandas) and on [Conda](https://docs.conda.io/en/latest/). +Package Index (PyPI)](https://pypi.org/project/pandas). ```sh -# conda -conda install -c conda-forge pandas -``` - -```sh -# or PyPI +# PyPI pip install pandas ``` From eaa1db0e5527606a84b49e58aa58b8a36e70e10c Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:41:35 -0700 Subject: [PATCH 698/921] refactor: read transformer output columns from model entity (#817) * refactor: read transformer output columns from model entity * fix tests and docs * remove dup code * fix comment --- bigframes/ml/base.py | 18 +++ bigframes/ml/compose.py | 20 +--- bigframes/ml/impute.py | 32 ++--- bigframes/ml/pipeline.py | 4 +- bigframes/ml/preprocessing.py | 220 +++++++++++++--------------------- 5 files changed, 124 insertions(+), 170 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 70854a36e91..6ae06c9d9f4 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -188,6 +188,24 @@ def __init__(self): def _keys(self): pass + def _extract_output_names(self): + """Extract transform output column names. Save the results to self._output_names.""" + assert self._bqml_model is not None + + output_names = [] + for transform_col in self._bqml_model._model._properties["transformColumns"]: + transform_col_dict = cast(dict, transform_col) + # pass the columns that are not transformed + if "transformSql" not in transform_col_dict: + continue + transform_sql: str = transform_col_dict["transformSql"] + if not transform_sql.startswith("ML."): + continue + + output_names.append(transform_col_dict["name"]) + + self._output_names = output_names + def __eq__(self, other) -> bool: return type(self) is type(other) and self._keys() == other._keys() diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 7f1bfe8d550..4ea63d2e817 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -201,25 +201,20 @@ def _merge( def _compile_to_sql( self, - columns: List[str], X: bpd.DataFrame, - ) -> List[Tuple[str, str]]: + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns (List[str]): - a list of column names to transform - X (bpd.DataFrame): - The Dataframe with training data. + X: DataFrame to transform. - Returns: - a list of tuples of (sql_expression, output_name)""" + Returns: a list of sql_expr.""" result = [] for _, transformer, target_columns in self.transformers: if isinstance(target_columns, str): target_columns = [target_columns] - result += transformer._compile_to_sql(target_columns, X=X) + result += transformer._compile_to_sql(X, target_columns) return result def fit( @@ -229,17 +224,14 @@ def fit( ) -> ColumnTransformer: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist(), X) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: diff --git a/bigframes/ml/impute.py b/bigframes/ml/impute.py index ae71637aa56..4955eb5de52 100644 --- a/bigframes/ml/impute.py +++ b/bigframes/ml/impute.py @@ -18,7 +18,7 @@ from __future__ import annotations import typing -from typing import Iterable, List, Literal, Optional, Tuple, Union +from typing import Iterable, List, Literal, Optional, Union import bigframes_vendored.sklearn.impute._base @@ -49,25 +49,22 @@ def _keys(self): def _compile_to_sql( self, - columns: Iterable[str], - X=None, - ) -> List[Tuple[str, str]]: + X: bpd.DataFrame, + columns: Optional[Iterable[str]] = None, + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - A list of column names to transform. - X: - The Dataframe with training data. + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns return [ - ( - self._base_sql_generator.ml_imputer( - column, self.strategy, f"imputer_{column}" - ), - f"imputer_{column}", + self._base_sql_generator.ml_imputer( + column, self.strategy, f"imputer_{column}" ) for column in columns ] @@ -92,17 +89,14 @@ def fit( ) -> SimpleImputer: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist(), X) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 04b8d73cf53..4cd60c5836a 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -106,9 +106,7 @@ def fit( ) -> Pipeline: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._transform._compile_to_sql(X.columns.tolist(), X=X) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._transform._compile_to_sql(X) if y is not None: # If labels columns are present, they should pass through un-transformed (y,) = utils.convert_to_dataframe(y) diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 07fdc171cff..13d2041ef3e 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -18,7 +18,7 @@ from __future__ import annotations import typing -from typing import cast, Iterable, List, Literal, Optional, Tuple, Union +from typing import cast, Iterable, List, Literal, Optional, Union import bigframes_vendored.sklearn.preprocessing._data import bigframes_vendored.sklearn.preprocessing._discretization @@ -46,23 +46,22 @@ def __init__(self): def _keys(self): return (self._bqml_model,) - def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql( + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - a list of column names to transform. - X (default None): - Ignored. + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns return [ - ( - self._base_sql_generator.ml_standard_scaler( - column, f"standard_scaled_{column}" - ), - f"standard_scaled_{column}", + self._base_sql_generator.ml_standard_scaler( + column, f"standard_scaled_{column}" ) for column in columns ] @@ -86,17 +85,14 @@ def fit( ) -> StandardScaler: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist()) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @@ -127,23 +123,22 @@ def __init__(self): def _keys(self): return (self._bqml_model,) - def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql( + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - a list of column names to transform. - X (default None): - Ignored. + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns return [ - ( - self._base_sql_generator.ml_max_abs_scaler( - column, f"max_abs_scaled_{column}" - ), - f"max_abs_scaled_{column}", + self._base_sql_generator.ml_max_abs_scaler( + column, f"max_abs_scaled_{column}" ) for column in columns ] @@ -167,17 +162,14 @@ def fit( ) -> MaxAbsScaler: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist()) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @@ -208,23 +200,22 @@ def __init__(self): def _keys(self): return (self._bqml_model,) - def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql( + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - a list of column names to transform. - X (default None): - Ignored. + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns return [ - ( - self._base_sql_generator.ml_min_max_scaler( - column, f"min_max_scaled_{column}" - ), - f"min_max_scaled_{column}", + self._base_sql_generator.ml_min_max_scaler( + column, f"min_max_scaled_{column}" ) for column in columns ] @@ -248,17 +239,14 @@ def fit( ) -> MinMaxScaler: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist()) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @@ -302,20 +290,18 @@ def _keys(self): return (self._bqml_model, self.n_bins, self.strategy) def _compile_to_sql( - self, - columns: Iterable[str], - X: bpd.DataFrame, - ) -> List[Tuple[str, str]]: + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - a list of column names to transform - X: - The Dataframe with training data. + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns array_split_points = {} if self.strategy == "uniform": for column in columns: @@ -327,11 +313,8 @@ def _compile_to_sql( ] return [ - ( - self._base_sql_generator.ml_bucketize( - column, array_split_points[column], f"kbinsdiscretizer_{column}" - ), - f"kbinsdiscretizer_{column}", + self._base_sql_generator.ml_bucketize( + column, array_split_points[column], f"kbinsdiscretizer_{column}" ) for column in columns ] @@ -339,11 +322,8 @@ def _compile_to_sql( elif self.strategy == "quantile": return [ - ( - self._base_sql_generator.ml_quantile_bucketize( - column, self.n_bins, f"kbinsdiscretizer_{column}" - ), - f"kbinsdiscretizer_{column}", + self._base_sql_generator.ml_quantile_bucketize( + column, self.n_bins, f"kbinsdiscretizer_{column}" ) for column in columns ] @@ -381,17 +361,14 @@ def fit( ) -> KBinsDiscretizer: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist(), X) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @@ -440,18 +417,19 @@ def __init__( def _keys(self): return (self._bqml_model, self.drop, self.min_frequency, self.max_categories) - def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql( + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - a list of column names to transform. - X (default None): - Ignored. - - Returns: a list of tuples of (sql_expression, output_name)""" + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns drop = self.drop if self.drop is not None else "none" # minus one here since BQML's inplimentation always includes index 0, and top_k is on top of that. top_k = ( @@ -465,11 +443,8 @@ def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str else OneHotEncoder.FREQUENCY_THRESHOLD_DEFAULT ) return [ - ( - self._base_sql_generator.ml_one_hot_encoder( - column, drop, top_k, frequency_threshold, f"onehotencoded_{column}" - ), - f"onehotencoded_{column}", + self._base_sql_generator.ml_one_hot_encoder( + column, drop, top_k, frequency_threshold, f"onehotencoded_{column}" ) for column in columns ] @@ -502,17 +477,14 @@ def fit( ) -> OneHotEncoder: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist()) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @@ -559,17 +531,19 @@ def __init__( def _keys(self): return (self._bqml_model, self.min_frequency, self.max_categories) - def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql( + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - a list of column names to transform. - X (default None): - Ignored. + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns # minus one here since BQML's inplimentation always includes index 0, and top_k is on top of that. top_k = ( @@ -583,11 +557,8 @@ def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str else LabelEncoder.FREQUENCY_THRESHOLD_DEFAULT ) return [ - ( - self._base_sql_generator.ml_label_encoder( - column, top_k, frequency_threshold, f"labelencoded_{column}" - ), - f"labelencoded_{column}", + self._base_sql_generator.ml_label_encoder( + column, top_k, frequency_threshold, f"labelencoded_{column}" ) for column in columns ] @@ -614,17 +585,14 @@ def fit( ) -> LabelEncoder: (y,) = utils.convert_to_dataframe(y) - compiled_transforms = self._compile_to_sql(y.columns.tolist()) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(y) self._bqml_model = self._bqml_model_factory.create_model( y, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # The schema of TRANSFORM output is not available in the model API, so save it during fitting - self._output_names = [name for _, name in compiled_transforms] + self._extract_output_names() return self def transform(self, y: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: @@ -660,24 +628,23 @@ def __init__(self, degree: int = 2): def _keys(self): return (self._bqml_model, self.degree) - def _compile_to_sql(self, columns: Iterable[str], X=None) -> List[Tuple[str, str]]: + def _compile_to_sql( + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: """Compile this transformer to a list of SQL expressions that can be included in a BQML TRANSFORM clause Args: - columns: - a list of column names to transform. - X (default None): - Ignored. + X: DataFrame to transform. + columns: transform columns. If None, transform all columns in X. - Returns: a list of tuples of (sql_expression, output_name)""" + Returns: a list of tuples sql_expr.""" + if columns is None: + columns = X.columns output_name = "poly_feat" return [ - ( - self._base_sql_generator.ml_polynomial_expand( - columns, self.degree, output_name - ), - output_name, + self._base_sql_generator.ml_polynomial_expand( + columns, self.degree, output_name ) ] @@ -702,29 +669,14 @@ def fit( ) -> PolynomialFeatures: (X,) = utils.convert_to_dataframe(X) - compiled_transforms = self._compile_to_sql(X.columns.tolist()) - transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] - + transform_sqls = self._compile_to_sql(X) self._bqml_model = self._bqml_model_factory.create_model( X, options={"model_type": "transform_only"}, transforms=transform_sqls, ) - # TODO(garrettwu): generalize the approach to other transformers - output_names = [] - for transform_col in self._bqml_model._model._properties["transformColumns"]: - transform_col_dict = cast(dict, transform_col) - # pass the columns that are not transformed - if "transformSql" not in transform_col_dict: - continue - transform_sql: str = transform_col_dict["transformSql"] - if not transform_sql.startswith("ML."): - continue - - output_names.append(transform_col_dict["name"]) - - self._output_names = output_names + self._extract_output_names() return self From 27f8631be81a3e136cfeb8904558bb4f3f5caa05 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:53:59 -0700 Subject: [PATCH 699/921] feat: add stratify param support to ml.model_selection.train_test_split method (#815) * feat: add stratify param to ml.model_selection.train_test_split * fix mypy * add notes for limit --- bigframes/ml/model_selection.py | 40 +++++++++++- tests/system/small/ml/test_model_selection.py | 62 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index 48eb5a93a71..6220e899aee 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -18,7 +18,7 @@ import typing -from typing import List, Union +from typing import cast, List, Union from bigframes.ml import utils import bigframes.pandas as bpd @@ -29,6 +29,7 @@ def train_test_split( test_size: Union[float, None] = None, train_size: Union[float, None] = None, random_state: Union[int, None] = None, + stratify: Union[bpd.Series, None] = None, ) -> List[Union[bpd.DataFrame, bpd.Series]]: """Splits dataframes or series into random train and test subsets. @@ -46,6 +47,10 @@ def train_test_split( random_state (default None): A seed to use for randomly choosing the rows of the split. If not set, a random split will be generated each time. + stratify: (bigframes.series.Series or None, default None): + If not None, data is split in a stratified fashion, using this as the class labels. Each split has the same distribution of the class labels with the original dataset. + Default to None. + Note: By setting the stratify parameter, the memory consumption and generated SQL will be linear to the unique values in the Series. May return errors if the unique values size is too large. Returns: List[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]]: A list of BigQuery DataFrames or Series. @@ -76,7 +81,38 @@ def train_test_split( dfs = list(utils.convert_to_dataframe(*arrays)) - split_dfs = dfs[0]._split(fracs=(train_size, test_size), random_state=random_state) + def _stratify_split(df: bpd.DataFrame, stratify: bpd.Series) -> List[bpd.DataFrame]: + """Split a single DF accoding to the stratify Series.""" + stratify = stratify.rename("bigframes_stratify_col") # avoid name conflicts + merged_df = df.join(stratify.to_frame(), how="outer") + + train_dfs, test_dfs = [], [] + uniq = stratify.unique() + for value in uniq: + cur = merged_df[merged_df["bigframes_stratify_col"] == value] + train, test = train_test_split( + cur, + test_size=test_size, + train_size=train_size, + random_state=random_state, + ) + train_dfs.append(train) + test_dfs.append(test) + + train_df = cast( + bpd.DataFrame, bpd.concat(train_dfs).drop(columns="bigframes_stratify_col") + ) + test_df = cast( + bpd.DataFrame, bpd.concat(test_dfs).drop(columns="bigframes_stratify_col") + ) + return [train_df, test_df] + + if stratify is None: + split_dfs = dfs[0]._split( + fracs=(train_size, test_size), random_state=random_state + ) + else: + split_dfs = _stratify_split(dfs[0], stratify) train_index = split_dfs[0].index test_index = split_dfs[1].index diff --git a/tests/system/small/ml/test_model_selection.py b/tests/system/small/ml/test_model_selection.py index 63d0840d291..ea9220feb45 100644 --- a/tests/system/small/ml/test_model_selection.py +++ b/tests/system/small/ml/test_model_selection.py @@ -234,3 +234,65 @@ def test_train_test_split_value_error(penguins_df_default_index, train_size, tes model_selection.train_test_split( X, y, train_size=train_size, test_size=test_size ) + + +def test_train_test_split_stratify(penguins_df_default_index): + X = penguins_df_default_index[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = penguins_df_default_index[["species"]] + X_train, X_test, y_train, y_test = model_selection.train_test_split( + X, y, stratify=penguins_df_default_index["species"] + ) + + # Original distribution is [152, 124, 68]. All the categories follow 75/25 split + train_counts = pd.Series( + [114, 93, 51], + index=pd.Index( + [ + "Adelie Penguin (Pygoscelis adeliae)", + "Gentoo penguin (Pygoscelis papua)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + name="species", + ), + dtype="Int64", + name="count", + ) + test_counts = pd.Series( + [38, 31, 17], + index=pd.Index( + [ + "Adelie Penguin (Pygoscelis adeliae)", + "Gentoo penguin (Pygoscelis papua)", + "Chinstrap penguin (Pygoscelis antarctica)", + ], + name="species", + ), + dtype="Int64", + name="count", + ) + pd.testing.assert_series_equal( + X_train["species"].value_counts().to_pandas(), + train_counts, + check_index_type=False, + ) + pd.testing.assert_series_equal( + X_test["species"].value_counts().to_pandas(), + test_counts, + check_index_type=False, + ) + pd.testing.assert_series_equal( + y_train["species"].value_counts().to_pandas(), + train_counts, + check_index_type=False, + ) + pd.testing.assert_series_equal( + y_test["species"].value_counts().to_pandas(), + test_counts, + check_index_type=False, + ) From 0d24f737041c7dd70253ebb4baa8d8ef67bd4f1d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 10 Jul 2024 18:14:49 -0700 Subject: [PATCH 700/921] fix: Fewer relation joins from df self-operations (#823) --- bigframes/core/__init__.py | 15 +++--- bigframes/core/blocks.py | 79 ++++++++++++++++------------ bigframes/core/join_def.py | 9 ++++ bigframes/core/nodes.py | 1 - bigframes/core/rewrite.py | 74 +++++++++++++------------- tests/system/small/test_dataframe.py | 23 ++++++-- 6 files changed, 118 insertions(+), 83 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 9b858046bc4..cfe8f29327f 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -460,9 +460,9 @@ def _cross_join_w_labels( conditions=(), mappings=(*labels_mappings, *table_mappings), type="cross" ) if join_side == "left": - joined_array = self.join(labels_array, join_def=join) + joined_array = self.relational_join(labels_array, join_def=join) else: - joined_array = labels_array.join(self, join_def=join) + joined_array = labels_array.relational_join(self, join_def=join) return joined_array def _create_unpivot_labels_array( @@ -485,30 +485,27 @@ def _create_unpivot_labels_array( return ArrayValue.from_pyarrow(pa.Table.from_pylist(rows), session=self.session) - def join( + def relational_join( self, other: ArrayValue, join_def: join_def.JoinDefinition, - allow_row_identity_join: bool = False, - ): + ) -> ArrayValue: join_node = nodes.JoinNode( left_child=self.node, right_child=other.node, join=join_def, - allow_row_identity_join=allow_row_identity_join, ) - if allow_row_identity_join: - return ArrayValue(bigframes.core.rewrite.maybe_rewrite_join(join_node)) return ArrayValue(join_node) def try_align_as_projection( self, other: ArrayValue, join_type: join_def.JoinType, + join_keys: typing.Tuple[join_def.CoalescedColumnMapping, ...], mappings: typing.Tuple[join_def.JoinColumnMapping, ...], ) -> typing.Optional[ArrayValue]: result = bigframes.core.rewrite.join_as_projection( - self.node, other.node, mappings, join_type + self.node, other.node, join_keys, mappings, join_type ) if result is not None: return ArrayValue(result) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index fef91f88dcc..fab0035e1ae 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2008,7 +2008,7 @@ def merge( mappings=(*left_mappings, *right_mappings), type=how, ) - joined_expr = self.expr.join(other.expr, join_def=join_def) + joined_expr = self.expr.relational_join(other.expr, join_def=join_def) result_columns = [] matching_join_labels = [] @@ -2267,25 +2267,33 @@ def join( raise NotImplementedError( f"Only how='outer','left','right','inner' currently supported. {constants.FEEDBACK_LINK}" ) - # Special case for null index, + # Handle null index, which only supports row join + if (self.index.nlevels == other.index.nlevels == 0) and not block_identity_join: + if not block_identity_join: + result = try_row_join(self, other, how=how) + if result is not None: + return result + raise bigframes.exceptions.NullIndexError( + "Cannot implicitly align objects. Set an explicit index using set_index." + ) + + # Oddly, pandas row-wise join ignores right index names if ( - (self.index.nlevels == other.index.nlevels == 0) - and not sort - and not block_identity_join + not block_identity_join + and (self.index.nlevels == other.index.nlevels) + and (self.index.dtypes == other.index.dtypes) ): - return join_indexless(self, other, how=how) + result = try_row_join(self, other, how=how) + if result is not None: + return result self._throw_if_null_index("join") other._throw_if_null_index("join") if self.index.nlevels == other.index.nlevels == 1: - return join_mono_indexed( - self, other, how=how, sort=sort, block_identity_join=block_identity_join - ) - else: + return join_mono_indexed(self, other, how=how, sort=sort) + else: # Handles cases where one or both sides are multi-indexed # Always sort mult-index join - return join_multi_indexed( - self, other, how=how, sort=sort, block_identity_join=block_identity_join - ) + return join_multi_indexed(self, other, how=how, sort=sort) def _force_reproject(self) -> Block: """Forces a reprojection of the underlying tables expression. Used to force predicate/order application before subsequent operations.""" @@ -2623,22 +2631,31 @@ def is_uniquely_named(self: BlockIndexProperties): return len(set(self.names)) == len(self.names) -def join_indexless( +def try_row_join( left: Block, right: Block, *, how="left", -) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: - """Joins two blocks""" +) -> Optional[Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]]: + """Joins two blocks that have a common root expression by merging the projections.""" left_expr = left.expr right_expr = right.expr + # Create a new array value, mapping from both, then left, and then right + join_keys = tuple( + join_defs.CoalescedColumnMapping( + left_source_id=left_id, + right_source_id=right_id, + destination_id=guid.generate_guid(), + ) + for left_id, right_id in zip(left.index_columns, right.index_columns) + ) left_mappings = [ join_defs.JoinColumnMapping( source_table=join_defs.JoinSide.LEFT, source_id=id, destination_id=guid.generate_guid(), ) - for id in left_expr.column_ids + for id in left.value_columns ] right_mappings = [ join_defs.JoinColumnMapping( @@ -2646,23 +2663,23 @@ def join_indexless( source_id=id, destination_id=guid.generate_guid(), ) - for id in right_expr.column_ids + for id in right.value_columns ] combined_expr = left_expr.try_align_as_projection( right_expr, join_type=how, + join_keys=join_keys, mappings=(*left_mappings, *right_mappings), ) if combined_expr is None: - raise bigframes.exceptions.NullIndexError( - "Cannot implicitly align objects. Set an explicit index using set_index." - ) + return None get_column_left = {m.source_id: m.destination_id for m in left_mappings} get_column_right = {m.source_id: m.destination_id for m in right_mappings} block = Block( combined_expr, column_labels=[*left.column_labels, *right.column_labels], - index_columns=(), + index_columns=(key.destination_id for key in join_keys), + index_labels=left.index.names, ) return ( block, @@ -2704,7 +2721,7 @@ def join_with_single_row( mappings=(*left_mappings, *right_mappings), type="cross", ) - combined_expr = left_expr.join( + combined_expr = left_expr.relational_join( right_expr, join_def=join_def, ) @@ -2731,7 +2748,6 @@ def join_mono_indexed( *, how="left", sort=False, - block_identity_join: bool = False, ) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: left_expr = left.expr right_expr = right.expr @@ -2759,14 +2775,14 @@ def join_mono_indexed( mappings=(*left_mappings, *right_mappings), type=how, ) - combined_expr = left_expr.join( + + combined_expr = left_expr.relational_join( right_expr, join_def=join_def, - allow_row_identity_join=(not block_identity_join), ) + get_column_left = join_def.get_left_mapping() get_column_right = join_def.get_right_mapping() - # Drop original indices from each side. and used the coalesced combination generated by the join. left_index = get_column_left[left.index_columns[0]] right_index = get_column_right[right.index_columns[0]] # Drop original indices from each side. and used the coalesced combination generated by the join. @@ -2800,7 +2816,6 @@ def join_multi_indexed( *, how="left", sort=False, - block_identity_join: bool = False, ) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: if not (left.index.is_uniquely_named() and right.index.is_uniquely_named()): raise ValueError("Joins not supported on indices with non-unique level names") @@ -2819,8 +2834,6 @@ def join_multi_indexed( left_join_ids = [left.index.resolve_level_exact(name) for name in common_names] right_join_ids = [right.index.resolve_level_exact(name) for name in common_names] - names_fully_match = len(left_only_names) == 0 and len(right_only_names) == 0 - left_expr = left.expr right_expr = right.expr @@ -2850,13 +2863,11 @@ def join_multi_indexed( type=how, ) - combined_expr = left_expr.join( + combined_expr = left_expr.relational_join( right_expr, join_def=join_def, - # If we're only joining on a subset of the index columns, we need to - # perform a true join. - allow_row_identity_join=(names_fully_match and not block_identity_join), ) + get_column_left = join_def.get_left_mapping() get_column_right = join_def.get_right_mapping() left_ids_post_join = [get_column_left[id] for id in left_join_ids] diff --git a/bigframes/core/join_def.py b/bigframes/core/join_def.py index 632a1864daa..4079abc8fa8 100644 --- a/bigframes/core/join_def.py +++ b/bigframes/core/join_def.py @@ -43,6 +43,15 @@ class JoinColumnMapping: destination_id: str +@dataclasses.dataclass(frozen=True) +class CoalescedColumnMapping: + """Special column mapping used only by implicit joiner only""" + + left_source_id: str + right_source_id: str + destination_id: str + + @dataclasses.dataclass(frozen=True) class JoinDefinition: conditions: Tuple[JoinCondition, ...] diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index a703cf1969d..dbcfc282e4f 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -183,7 +183,6 @@ class JoinNode(BigFrameNode): left_child: BigFrameNode right_child: BigFrameNode join: JoinDefinition - allow_row_identity_join: bool = False @property def row_preserving(self) -> bool: diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index 101d5cc8820..60ed4069a9c 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -106,21 +106,25 @@ def order_with(self, by: Tuple[order.OrderingExpression, ...]): ) def can_merge( - self, right: SquashedSelect, join_def: join_defs.JoinDefinition + self, + right: SquashedSelect, + join_keys: Tuple[join_defs.CoalescedColumnMapping, ...], ) -> bool: """Determines whether the two selections can be merged into a single selection.""" - if join_def.type == "cross": - # Cannot convert cross join to projection - return False - r_exprs_by_id = {id: expr for expr, id in right.columns} l_exprs_by_id = {id: expr for expr, id in self.columns} - l_join_exprs = [l_exprs_by_id[cond.left_id] for cond in join_def.conditions] - r_join_exprs = [r_exprs_by_id[cond.right_id] for cond in join_def.conditions] + l_join_exprs = [ + l_exprs_by_id[join_key.left_source_id] for join_key in join_keys + ] + r_join_exprs = [ + r_exprs_by_id[join_key.right_source_id] for join_key in join_keys + ] - if (self.root != right.root) or any( - l_expr != r_expr for l_expr, r_expr in zip(l_join_exprs, r_join_exprs) - ): + if self.root != right.root: + return False + if len(l_join_exprs) != len(r_join_exprs): + return False + if any(l_expr != r_expr for l_expr, r_expr in zip(l_join_exprs, r_join_exprs)): return False return True @@ -128,6 +132,7 @@ def merge( self, right: SquashedSelect, join_type: join_defs.JoinType, + join_keys: Tuple[join_defs.CoalescedColumnMapping, ...], mappings: Tuple[join_defs.JoinColumnMapping, ...], ) -> SquashedSelect: if self.root != right.root: @@ -147,11 +152,9 @@ def merge( l_relative, r_relative = relative_predicates(self.predicate, right.predicate) lmask = l_relative if join_type in {"right", "outer"} else None rmask = r_relative if join_type in {"left", "outer"} else None - if lmask is not None: - lselection = tuple((apply_mask(expr, lmask), id) for expr, id in lselection) - if rmask is not None: - rselection = tuple((apply_mask(expr, rmask), id) for expr, id in rselection) - new_columns = remap_names(mappings, lselection, rselection) + new_columns = merge_expressions( + join_keys, mappings, lselection, rselection, lmask, rmask + ) # Reconstruct ordering reverse_root = self.reverse_root @@ -204,26 +207,10 @@ def expand(self) -> nodes.BigFrameNode: return nodes.ProjectionNode(child=root, assignments=self.columns) -def maybe_rewrite_join(join_node: nodes.JoinNode) -> nodes.BigFrameNode: - rewrite_common_node = common_selection_root( - join_node.left_child, join_node.right_child - ) - if rewrite_common_node is None: - return join_node - left_side = SquashedSelect.from_node_span(join_node.left_child, rewrite_common_node) - right_side = SquashedSelect.from_node_span( - join_node.right_child, rewrite_common_node - ) - if left_side.can_merge(right_side, join_node.join): - return left_side.merge( - right_side, join_node.join.type, join_node.join.mappings - ).expand() - return join_node - - def join_as_projection( l_node: nodes.BigFrameNode, r_node: nodes.BigFrameNode, + join_keys: Tuple[join_defs.CoalescedColumnMapping, ...], mappings: Tuple[join_defs.JoinColumnMapping, ...], how: join_defs.JoinType, ) -> Optional[nodes.BigFrameNode]: @@ -231,7 +218,10 @@ def join_as_projection( if rewrite_common_node is not None: left_side = SquashedSelect.from_node_span(l_node, rewrite_common_node) right_side = SquashedSelect.from_node_span(r_node, rewrite_common_node) - merged = left_side.merge(right_side, how, mappings) + if not left_side.can_merge(right_side, join_keys): + # Most likely because join keys didn't match + return None + merged = left_side.merge(right_side, how, join_keys, mappings) assert ( merged is not None ), "Couldn't merge nodes. This shouldn't happen. Please share full stacktrace with the BigQuery DataFrames team at bigframes-feedback@google.com." @@ -240,21 +230,33 @@ def join_as_projection( return None -def remap_names( +def merge_expressions( + join_keys: Tuple[join_defs.CoalescedColumnMapping, ...], mappings: Tuple[join_defs.JoinColumnMapping, ...], lselection: Selection, rselection: Selection, + lmask: Optional[scalar_exprs.Expression], + rmask: Optional[scalar_exprs.Expression], ) -> Selection: new_selection: Selection = tuple() l_exprs_by_id = {id: expr for expr, id in lselection} r_exprs_by_id = {id: expr for expr, id in rselection} + for key in join_keys: + # Join keys expressions are equivalent on both sides, so can choose either left or right key + assert l_exprs_by_id[key.left_source_id] == r_exprs_by_id[key.right_source_id] + expr = l_exprs_by_id[key.left_source_id] + id = key.destination_id + new_selection = (*new_selection, (expr, id)) for mapping in mappings: if mapping.source_table == join_defs.JoinSide.LEFT: expr = l_exprs_by_id[mapping.source_id] + if lmask is not None: + expr = apply_mask(expr, lmask) else: # Right expr = r_exprs_by_id[mapping.source_id] - id = mapping.destination_id - new_selection = (*new_selection, (expr, id)) + if rmask is not None: + expr = apply_mask(expr, rmask) + new_selection = (*new_selection, (expr, mapping.destination_id)) return new_selection diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 625b920763e..7273e2079ff 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -140,8 +140,6 @@ def test_df_construct_from_dict(): def test_df_construct_inline_respects_location(): - import bigframes.pandas as bpd - # Note: This starts a thread-local session. with bpd.option_context("bigquery.location", "europe-west1"): df = bpd.DataFrame([[1, 2, 3], [4, 5, 6]]) @@ -4336,6 +4334,25 @@ def test_df_cached(scalars_df_index): pandas.testing.assert_frame_equal(df.to_pandas(), df_cached_copy.to_pandas()) +def test_assign_after_binop_row_joins(): + pd_df = pd.DataFrame( + { + "idx1": [1, 1, 1, 1, 2, 2, 2, 2], + "idx2": [10, 10, 20, 20, 10, 10, 20, 20], + "metric1": [10, 14, 2, 13, 6, 2, 9, 5], + "metric2": [25, -3, 8, 2, -1, 0, 0, -4], + }, + dtype=pd.Int64Dtype(), + ).set_index(["idx1", "idx2"]) + bf_df = dataframe.DataFrame(pd_df) + + # Expect implicit joiner to be used, preserving input cardinality rather than getting relational join + bf_df["metric_diff"] = bf_df.metric1 - bf_df.metric2 + pd_df["metric_diff"] = pd_df.metric1 - pd_df.metric2 + + assert_pandas_df_equal(bf_df.to_pandas(), pd_df) + + def test_df_cache_with_implicit_join(scalars_df_index): """expectation is that cache will be used, but no explicit join will be performed""" df = scalars_df_index[["int64_col", "int64_too"]].sort_index().reset_index() + 3 @@ -4510,7 +4527,7 @@ def test_query_complexity_repeated_subtrees( bf_df = scalars_df_index for _ in range(5): pd_df = pd.concat(10 * [pd_df]).head(5) - bf_df = bigframes.pandas.concat(10 * [bf_df]).head(5) + bf_df = bpd.concat(10 * [bf_df]).head(5) bf_result = bf_df.to_pandas() pd_result = pd_df assert_pandas_df_equal(bf_result, pd_result) From d7b333fa26acddaeb5ccca4f81b1d624dff03ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 11 Jul 2024 10:59:02 -0500 Subject: [PATCH 701/921] docs: add partner attribution steps to integrations sample notebook (#835) --- notebooks/dataframes/integrations.ipynb | 138 ++++++++++++++++++------ 1 file changed, 104 insertions(+), 34 deletions(-) diff --git a/notebooks/dataframes/integrations.ipynb b/notebooks/dataframes/integrations.ipynb index 735e18d94e9..9edb174f18c 100644 --- a/notebooks/dataframes/integrations.ipynb +++ b/notebooks/dataframes/integrations.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 35, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -30,11 +30,47 @@ "This notebook demonstrates operations for building applications that integrate with BigQuery DataFrames. Follow these samples to build an integration that accepts a BigQuery DataFrames object or returns one." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Attributing requests initiated by BigQuery DataFrames\n", + "\n", + "Partners are required to attribute API calls to BigQuery and other Google APIs. Where possible, this should be done via the User-Agent string, but can also be done via job labels if your integration doesn't initialize the BigQuery DataFrames session.\n", + "\n", + "### Setting the User-Agent\n", + "\n", + "Set [`bpd.options.bigquery.application_name`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes._config.bigquery_options.BigQueryOptions#bigframes__config_bigquery_options_BigQueryOptions_application_name) to a compliant string. Reach out to your Google Partner Engineering team contact for further instructions." + ] + }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 2, "metadata": {}, "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "\n", + "# Set this to the string informed by your Google Partner Engineering team contact.\n", + "# Note: This can only be set once per session, so is most appropriate for partners\n", + "# who provide a Python + BigQuery DataFrames environment to their customers.\n", + "bpd.options.bigquery.application_name = \"notebook-samples/1.0.0 (GPN:notebook-samples)\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/swast/src/bigframes-2/bigframes/core/global_session.py:113: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", + " return func(get_global_session(), *args, **kwargs)\n" + ] + } + ], "source": [ "import bigframes.pandas as bpd\n", "\n", @@ -47,6 +83,40 @@ "}).set_index(\"index\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setting the job label\n", + "\n", + "If your application works with customer-created BigQuery DataFrames objects, you might not be able to set the user-agent header because the session has already started (watch https://github.com/googleapis/python-bigquery-dataframes/issues/833 for updates on this limitation). Instead, attach a label to the jobs your application initiates, such as if you are performing `to_gbq()`on an existing DataFrame, as described below.\n", + "\n", + "Use `bpd.option_context()` so that the labels are only set during the operations your application performs." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job eb7f3bbe-dda9-4d2f-b195-21de862d7055 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with bpd.option_context(\"compute.extra_query_labels\", {\"application-name\": \"notebook-samples\"}):\n", + " table_id = df.to_gbq()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -58,13 +128,13 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 00b5c727-f2bf-4265-be22-d7d505619db7 is DONE. 0 Bytes processed. Open Job" + "Query job 4ad50c3c-91d0-4fef-91f6-0a2c5a30c38f is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -76,10 +146,10 @@ { "data": { "text/plain": [ - "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240327_43bbc4c64fb947f7b69db570a5641506'" + "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240710_sessionf75568_9a045ff143db4f8ab2018994287020f3'" ] }, - "execution_count": 37, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -102,13 +172,13 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job f9c39ac2-a428-45c9-bb3a-643fc62a1c5b is DONE. 0 Bytes processed. Open Job" + "Query job 9e7d4b1a-d7fc-4599-bab4-40062c83288e is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -122,11 +192,11 @@ "output_type": "stream", "text": [ " index int_col float_col string_col\n", - "0 2 3 0.2500 c\n", - "1 4 5 0.0625 e\n", + "0 3 4 -0.1250 d\n", + "1 1 2 -0.5000 b\n", "2 0 1 1.0000 a\n", - "3 1 2 -0.5000 b\n", - "4 3 4 -0.1250 d\n" + "3 4 5 0.0625 e\n", + "4 2 3 0.2500 c\n" ] } ], @@ -168,13 +238,13 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job ad53c7f2-e3bd-4667-b60b-b700c24b7a81 is DONE. 0 Bytes processed. Open Job" + "Query job 62db313e-7632-4dbb-8eff-5035d0e6c27e is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -188,11 +258,11 @@ "output_type": "stream", "text": [ " index int_col float_col string_col\n", - "0 4 5 0.0625 e\n", - "1 0 1 1.0000 a\n", - "2 2 3 0.2500 c\n", - "3 3 4 -0.1250 d\n", - "4 1 2 -0.5000 b\n" + "0 1 2 -0.5000 b\n", + "1 3 4 -0.1250 d\n", + "2 0 1 1.0000 a\n", + "3 4 5 0.0625 e\n", + "4 2 3 0.2500 c\n" ] } ], @@ -265,13 +335,13 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 2aa7033c-c547-4ae2-a9aa-33272be82b9c is DONE. 0 Bytes processed. Open Job" + "Query job 1cbd8898-97c7-419e-87af-b72a9432afb6 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -283,10 +353,10 @@ { "data": { "text/plain": [ - "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240327_b484a3967fba4a41850f4eb21b4b3bd8'" + "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240710_sessionf75568_58b9b6fc0c3349bf8d3dd6fb29ab5322'" ] }, - "execution_count": 40, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -308,13 +378,13 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 1d489f94-2840-405e-9114-d439dcfcf7aa is DONE. 0 Bytes processed. Open Job" + "Query job 40e54aa9-fad7-47c3-9bec-144f6c7106d8 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -326,10 +396,10 @@ { "data": { "text/plain": [ - "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240327_d00699eeeed743b487c870dca5bcf23b'" + "'swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bqdf20240710_sessionf75568_cdb4f54063b0417a8309c462b70239fa'" ] }, - "execution_count": 41, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -357,7 +427,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -366,7 +436,7 @@ "Dataset(DatasetReference('swast-scratch', 'my_dataset'))" ] }, - "execution_count": 42, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -381,13 +451,13 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 40977e60-97c3-4c93-89e2-d7334e5af71d is DONE. 0 Bytes processed. Open Job" + "Query job 73cf9e04-d5fa-4765-827c-665f0e6b9e00 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -399,7 +469,7 @@ { "data": { "text/html": [ - "Query job 81e35bb8-2e27-4a18-b596-15a7805331f0 is DONE. 270 Bytes processed. Open Job" + "Query job b177eb37-197f-4732-8978-c74cccb36e01 is DONE. 270 Bytes processed. Open Job" ], "text/plain": [ "" @@ -523,7 +593,7 @@ "[10 rows x 3 columns]" ] }, - "execution_count": 43, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -627,7 +697,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.10.9" } }, "nbformat": 4, From 8d1a03ab8916873fe977cf8ba02cd00de3ffed78 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:33:01 -0700 Subject: [PATCH 702/921] test: fix llm load tests (#836) --- tests/system/load/test_llm.py | 85 +++++++++---------------------- tests/system/small/ml/test_llm.py | 45 ++++++++++++++++ 2 files changed, 70 insertions(+), 60 deletions(-) diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index fd047b3ba6e..9b8868bb27e 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -15,7 +15,8 @@ import pandas as pd import pytest -import bigframes.ml.llm +from bigframes.ml import llm +from tests.system import utils @pytest.fixture(scope="session") @@ -39,9 +40,7 @@ def llm_remote_text_df(session, llm_remote_text_pandas_df): @pytest.mark.flaky(retries=2) def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): - model = bigframes.ml.llm.PaLM2TextGenerator( - model_name="text-bison", max_iterations=1 - ) + model = llm.PaLM2TextGenerator(model_name="text-bison", max_iterations=1) X_train = llm_fine_tune_df_default_index[["prompt"]] y_train = llm_fine_tune_df_default_index[["label"]] @@ -50,62 +49,23 @@ def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_ assert model is not None df = model.predict(llm_remote_text_df["prompt"]).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() == 1) - - # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept - - -@pytest.mark.flaky(retries=2) -def test_llm_palm_score(llm_fine_tune_df_default_index): - model = bigframes.ml.llm.PaLM2TextGenerator(model_name="text-bison") - - # Check score to ensure the model was fitted - score_result = model.score( - X=llm_fine_tune_df_default_index[["prompt"]], - y=llm_fine_tune_df_default_index[["label"]], - ).to_pandas() - score_result_col = score_result.columns.to_list() - expected_col = [ - "bleu4_score", - "rouge-l_precision", - "rouge-l_recall", - "rouge-l_f1_score", - "evaluation_status", - ] - assert all(col in score_result_col for col in expected_col) - - -@pytest.mark.flaky(retries=2) -def test_llm_palm_score_params(llm_fine_tune_df_default_index): - model = bigframes.ml.llm.PaLM2TextGenerator( - model_name="text-bison", max_iterations=1 + utils.check_pandas_df_schema_and_index( + df, + columns=[ + "ml_generate_text_llm_result", + "ml_generate_text_rai_result", + "ml_generate_text_status", + "prompt", + ], + index=3, ) - - # Check score to ensure the model was fitted - score_result = model.score( - X=llm_fine_tune_df_default_index["prompt"], - y=llm_fine_tune_df_default_index["label"], - task_type="classification", - ).to_pandas() - score_result_col = score_result.columns.to_list() - expected_col = [ - "precision", - "recall", - "f1_score", - "label", - "evaluation_status", - ] - assert all(col in score_result_col for col in expected_col) + # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept +@pytest.mark.skip(reason="b/351905648. Credential error to be fixed.") @pytest.mark.flaky(retries=2) def test_llm_gemini_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): - model = bigframes.ml.llm.GeminiTextGenerator( - model_name="gemini-pro", max_iterations=1 - ) + model = llm.GeminiTextGenerator(model_name="gemini-pro", max_iterations=1) X_train = llm_fine_tune_df_default_index[["prompt"]] y_train = llm_fine_tune_df_default_index[["label"]] @@ -120,9 +80,14 @@ def test_llm_gemini_configure_fit(llm_fine_tune_df_default_index, llm_remote_tex top_k=20, top_p=0.5, ).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() == 1) - + utils.check_pandas_df_schema_and_index( + df, + columns=[ + "ml_generate_text_llm_result", + "ml_generate_text_rai_result", + "ml_generate_text_status", + "prompt", + ], + index=3, + ) # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 36d01e126f9..ee9d654d938 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -369,6 +369,51 @@ def test_gemini_text_generator_predict_with_params_success( assert all(series.str.len() > 20) +@pytest.mark.flaky(retries=2) +def test_llm_palm_score(llm_fine_tune_df_default_index): + model = llm.PaLM2TextGenerator(model_name="text-bison") + + # Check score to ensure the model was fitted + score_result = model.score( + X=llm_fine_tune_df_default_index[["prompt"]], + y=llm_fine_tune_df_default_index[["label"]], + ).to_pandas() + utils.check_pandas_df_schema_and_index( + score_result, + columns=[ + "bleu4_score", + "rouge-l_precision", + "rouge-l_recall", + "rouge-l_f1_score", + "evaluation_status", + ], + index=1, + ) + + +@pytest.mark.flaky(retries=2) +def test_llm_palm_score_params(llm_fine_tune_df_default_index): + model = llm.PaLM2TextGenerator(model_name="text-bison", max_iterations=1) + + # Check score to ensure the model was fitted + score_result = model.score( + X=llm_fine_tune_df_default_index["prompt"], + y=llm_fine_tune_df_default_index["label"], + task_type="classification", + ).to_pandas() + utils.check_pandas_df_schema_and_index( + score_result, + columns=[ + "precision", + "recall", + "f1_score", + "label", + "evaluation_status", + ], + index=6, + ) + + @pytest.mark.flaky(retries=2) def test_llm_gemini_pro_score(llm_fine_tune_df_default_index): model = llm.GeminiTextGenerator(model_name="gemini-pro") From 4e7e67bfe07844b1faa9763c6ab1ad7ac89ed043 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 11 Jul 2024 14:24:48 -0700 Subject: [PATCH 703/921] refactor: SQL builder supports partial ordering (#838) --- bigframes/core/compile/api.py | 3 +- bigframes/core/compile/compiled.py | 46 ++++++++++------- bigframes/core/compile/compiler.py | 4 +- bigframes/core/ordering.py | 80 +++++++++++++++++++++++++----- 4 files changed, 101 insertions(+), 32 deletions(-) diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index 1f7d0a45072..a918cbd324a 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -51,7 +51,8 @@ def compile_raw( ir = compiler.compile_ordered_ir(node) sql = ir.raw_sql() ordering_info = ir._ordering - return sql, ordering_info + assert ir.has_total_order + return sql, ordering_info # type: ignore def test_only_try_evaluate(node: bigframes.core.nodes.BigFrameNode): diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index cc601744c1b..c822dd331cf 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -37,7 +37,9 @@ ascending_over, encode_order_string, IntegerEncoding, + join_orderings, OrderingExpression, + RowOrdering, TotalOrdering, ) import bigframes.core.schema as schemata @@ -519,7 +521,7 @@ def __init__( table: ibis_types.Table, columns: Sequence[ibis_types.Value], hidden_ordering_columns: Optional[Sequence[ibis_types.Value]] = None, - ordering: TotalOrdering = TotalOrdering(), + ordering: RowOrdering = RowOrdering(), predicates: Optional[Collection[ibis_types.BooleanValue]] = None, ): super().__init__(table, columns, predicates) @@ -566,6 +568,10 @@ def __init__( def is_ordered_ir(self) -> bool: return True + @property + def has_total_order(self) -> bool: + return isinstance(self._ordering, TotalOrdering) + @classmethod def from_pandas( cls, @@ -757,16 +763,13 @@ def explode(self, column_ids: typing.Sequence[str]) -> OrderedIR: ], table_w_unnest[unnest_offset_id], ] - ordering = TotalOrdering( - ordering_value_columns=tuple( - [ - *self._ordering.ordering_value_columns, - ascending_over(unnest_offset_id), - ] - ), - total_ordering_columns=frozenset( - [*self._ordering.total_ordering_columns, unnest_offset_id] - ), + l_mappings = {id: id for id in self._ordering.referenced_columns} + r_mappings = {unnest_offset_id: unnest_offset_id} + ordering = join_orderings( + self._ordering, + TotalOrdering.from_offset_col(unnest_offset_id), + l_mappings, + r_mappings, ) return OrderedIR( @@ -1150,12 +1153,19 @@ def _bake_ordering(self) -> OrderedIR: self._ibis_bindings[expr.scalar_expression.id] ) - new_ordering = TotalOrdering( - tuple(new_exprs), - self._ordering.integer_encoding, - self._ordering.string_encoding, - self._ordering.total_ordering_columns, - ) + if isinstance(self._ordering, TotalOrdering): + new_ordering: RowOrdering = TotalOrdering( + tuple(new_exprs), + self._ordering.integer_encoding, + self._ordering.string_encoding, + self._ordering.total_ordering_columns, + ) + else: + new_ordering = RowOrdering( + tuple(new_exprs), + self._ordering.integer_encoding, + self._ordering.string_encoding, + ) return OrderedIR( self._table, columns=self.columns, @@ -1297,7 +1307,7 @@ class Builder: def __init__( self, table: ibis_types.Table, - ordering: TotalOrdering, + ordering: RowOrdering, columns: Collection[ibis_types.Value] = (), hidden_ordering_columns: Collection[ibis_types.Value] = (), predicates: Optional[Collection[ibis_types.BooleanValue]] = None, diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index ca9c479fffb..5bea88739d2 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -38,7 +38,9 @@ def compile_ordered_ir(node: nodes.BigFrameNode) -> compiled.OrderedIR: - return typing.cast(compiled.OrderedIR, compile_node(node, True)) + ir = typing.cast(compiled.OrderedIR, compile_node(node, True)) + assert ir.has_total_order + return ir def compile_unordered_ir(node: nodes.BigFrameNode) -> compiled.UnorderedIR: diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index 406ca52731b..bff7e2ce44b 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -98,6 +98,8 @@ class RowOrdering: """Immutable object that holds information about the ordering of rows in a ArrayValue object. May not be unambiguous.""" ordering_value_columns: typing.Tuple[OrderingExpression, ...] = () + integer_encoding: IntegerEncoding = IntegerEncoding(False) + string_encoding: StringEncoding = StringEncoding(False) @property def all_ordering_columns(self) -> Sequence[OrderingExpression]: @@ -111,6 +113,20 @@ def referenced_columns(self) -> Set[str]: for col in part.scalar_expression.unbound_variables ) + @property + def is_string_encoded(self) -> bool: + """True if ordering is fully defined by a fixed length string column.""" + return self.string_encoding.is_encoded + + @property + def is_sequential(self) -> bool: + return self.integer_encoding.is_encoded and self.integer_encoding.is_sequential + + @property + def total_order_col(self) -> Optional[OrderingExpression]: + """Returns column id of columns that defines total ordering, if such as column exists""" + return None + def with_reverse(self) -> RowOrdering: """Reverses the ordering.""" return RowOrdering( @@ -121,17 +137,66 @@ def with_column_remap(self, mapping: typing.Mapping[str, str]) -> RowOrdering: new_value_columns = [ col.remap_names(mapping) for col in self.all_ordering_columns ] - return TotalOrdering( + return RowOrdering( tuple(new_value_columns), ) + def with_non_sequential(self): + """Create a copy that is marked as non-sequential. + + This is useful when filtering, but not sorting, an expression. + """ + if self.integer_encoding.is_sequential: + return RowOrdering( + self.ordering_value_columns, + integer_encoding=IntegerEncoding( + self.integer_encoding.is_encoded, is_sequential=False + ), + ) + + return self + + def with_ordering_columns( + self, + ordering_value_columns: Sequence[OrderingExpression] = (), + ) -> RowOrdering: + """Creates a new ordering that reorders by the given columns. + + Args: + ordering_value_columns: + In decreasing precedence order, the values used to sort the ordering + + Returns: + Modified ExpressionOrdering + """ + + # Truncate to remove any unneded col references after all total order cols included + new_ordering = self._truncate_ordering( + (*ordering_value_columns, *self.ordering_value_columns) + ) + return RowOrdering( + new_ordering, + ) + + def _truncate_ordering( + self, order_refs: tuple[OrderingExpression, ...] + ) -> tuple[OrderingExpression, ...]: + # Truncate once we refer to a full key in bijective operations + columns_seen: Set[str] = set() + truncated_refs = [] + for order_part in order_refs: + expr = order_part.scalar_expression + if not set(expr.unbound_variables).issubset(columns_seen): + if expr.is_bijective: + columns_seen.update(expr.unbound_variables) + truncated_refs.append(order_part) + return tuple(truncated_refs) + @dataclass(frozen=True) class TotalOrdering(RowOrdering): """Immutable object that holds information about the ordering of rows in a ArrayValue object. Guaranteed to be unambiguous.""" - integer_encoding: IntegerEncoding = IntegerEncoding(False) - string_encoding: StringEncoding = StringEncoding(False) # A table has a total ordering defined by the identities of a set of 1 or more columns. # These columns must always be part of the ordering, in order to guarantee that the ordering is total. # Therefore, any modifications(or drops) done to these columns must result in hidden copies being made. @@ -234,15 +299,6 @@ def total_order_col(self) -> Optional[OrderingExpression]: return None return order_ref - @property - def is_string_encoded(self) -> bool: - """True if ordering is fully defined by a fixed length string column.""" - return self.string_encoding.is_encoded - - @property - def is_sequential(self) -> bool: - return self.integer_encoding.is_encoded and self.integer_encoding.is_sequential - def encode_order_string( order_id: ibis_types.IntegerColumn, length: int = DEFAULT_ORDERING_ID_LENGTH From 93785cb48be4a2eb8770129148bd0b897fed4ee7 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 11 Jul 2024 17:34:27 -0700 Subject: [PATCH 704/921] fix: Fix unordered mode using ordered path to print frame (#839) --- bigframes/core/blocks.py | 7 +++++-- tests/system/small/test_unordered.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index fab0035e1ae..c2bf20076ac 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -498,7 +498,7 @@ def to_pandas( sampling_method: Optional[str] = None, random_state: Optional[int] = None, *, - ordered: bool = True, + ordered: Optional[bool] = None, ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame.""" if (sampling_method is not None) and (sampling_method not in _SAMPLING_METHODS): @@ -517,7 +517,10 @@ def to_pandas( df, query_job = self._materialize_local( materialize_options=MaterializationOptions( - downsampling=sampling, ordered=ordered + downsampling=sampling, + ordered=ordered + if ordered is not None + else self.session._strictly_ordered, ) ) df.set_axis(self.column_labels, axis=1, copy=False) diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 36bf2a25857..4448ddc8387 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -31,6 +31,12 @@ def test_unordered_mode_cache_aggregate(unordered_session): assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) +def test_unordered_mode_print(unordered_session): + pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session).cache() + print(df) + + @skip_legacy_pandas def test_unordered_mode_read_gbq(unordered_session): df = unordered_session.read_gbq( From c6d1c7c9e0608e55bc7121ea8f97146a2581b9b7 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:26:14 -0700 Subject: [PATCH 705/921] chore: fix notebook test. (#841) --- notebooks/regression/sklearn_linear_regression.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/regression/sklearn_linear_regression.ipynb b/notebooks/regression/sklearn_linear_regression.ipynb index 2873527449a..95aa314bb09 100644 --- a/notebooks/regression/sklearn_linear_regression.ipynb +++ b/notebooks/regression/sklearn_linear_regression.ipynb @@ -857,7 +857,7 @@ "from bigframes.ml.preprocessing import StandardScaler, OneHotEncoder\n", "\n", "preprocessing = ColumnTransformer([\n", - " (\"onehot\", OneHotEncoder(), [\"island\", \"species\", \"sex\"]),\n", + " (\"onehot\", OneHotEncoder(), [\"island\", \"sex\"]),\n", " (\"scaler\", StandardScaler(), [\"culmen_depth_mm\", \"culmen_length_mm\", \"flipper_length_mm\"]),\n", "])\n", "\n", From 877bcc052b27a59485bb155cafed29c640642c96 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 12 Jul 2024 16:01:34 -0700 Subject: [PATCH 706/921] refactor: Turn compiler into object with toggleable order strictness (#840) --- bigframes/core/compile/api.py | 18 +- bigframes/core/compile/compiler.py | 488 +++++++++++++++-------------- bigframes/core/nodes.py | 2 +- 3 files changed, 258 insertions(+), 250 deletions(-) diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index a918cbd324a..4e76d42bef4 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -22,24 +22,28 @@ import bigframes.core.ordering import bigframes.core.schema +_STRICT_COMPILER = compiler.Compiler(strict=True) + def compile_peek(node: bigframes.core.nodes.BigFrameNode, n_rows: int) -> str: """Compile node into sql that selects N arbitrary rows, may not execute deterministically.""" - return compiler.compile_unordered_ir(node).peek_sql(n_rows) + return _STRICT_COMPILER.compile_unordered_ir(node).peek_sql(n_rows) def compile_unordered( node: bigframes.core.nodes.BigFrameNode, *, col_id_overrides: Mapping[str, str] = {} ) -> str: """Compile node into sql where rows are unsorted, and no ordering information is preserved.""" - return compiler.compile_unordered_ir(node).to_sql(col_id_overrides=col_id_overrides) + return _STRICT_COMPILER.compile_unordered_ir(node).to_sql( + col_id_overrides=col_id_overrides + ) def compile_ordered( node: bigframes.core.nodes.BigFrameNode, *, col_id_overrides: Mapping[str, str] = {} ) -> str: """Compile node into sql where rows are sorted with ORDER BY.""" - return compiler.compile_ordered_ir(node).to_sql( + return _STRICT_COMPILER.compile_ordered_ir(node).to_sql( col_id_overrides=col_id_overrides, ordered=True ) @@ -48,7 +52,7 @@ def compile_raw( node: bigframes.core.nodes.BigFrameNode, ) -> Tuple[str, bigframes.core.ordering.TotalOrdering]: """Compile node into sql that exposes all columns, including hidden ordering-only columns.""" - ir = compiler.compile_ordered_ir(node) + ir = _STRICT_COMPILER.compile_ordered_ir(node) sql = ir.raw_sql() ordering_info = ir._ordering assert ir.has_total_order @@ -57,7 +61,9 @@ def compile_raw( def test_only_try_evaluate(node: bigframes.core.nodes.BigFrameNode): """Use only for unit testing paths - not fully featured. Will throw exception if fails.""" - ibis = compiler.compile_ordered_ir(node)._to_ibis_expr(ordering_mode="unordered") + ibis = _STRICT_COMPILER.compile_ordered_ir(node)._to_ibis_expr( + ordering_mode="unordered" + ) return ibis.pandas.connect({}).execute(ibis) @@ -65,7 +71,7 @@ def test_only_ibis_inferred_schema(node: bigframes.core.nodes.BigFrameNode): """Use only for testing paths to ensure ibis inferred schema does not diverge from bigframes inferred schema.""" import bigframes.core.schema - compiled = compiler.compile_unordered_ir(node) + compiled = _STRICT_COMPILER.compile_unordered_ir(node) items = tuple( bigframes.core.schema.SchemaItem(id, compiled.get_column_type(id)) for id in compiled.column_ids diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 5bea88739d2..da74ffeb8f9 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -13,6 +13,7 @@ # limitations under the License. from __future__ import annotations +import dataclasses import functools import io import typing @@ -37,99 +38,191 @@ import bigframes.session -def compile_ordered_ir(node: nodes.BigFrameNode) -> compiled.OrderedIR: - ir = typing.cast(compiled.OrderedIR, compile_node(node, True)) - assert ir.has_total_order - return ir - - -def compile_unordered_ir(node: nodes.BigFrameNode) -> compiled.UnorderedIR: - return typing.cast(compiled.UnorderedIR, compile_node(node, False)) - - -def compile_peak_sql(node: nodes.BigFrameNode, n_rows: int) -> typing.Optional[str]: - return compile_unordered_ir(node).peek_sql(n_rows) - - -# TODO: Remove cache when schema no longer requires compilation to derive schema (and therefor only compiles for execution) -@functools.lru_cache(maxsize=5000) -def compile_node( - node: nodes.BigFrameNode, ordered: bool = True -) -> compiled.UnorderedIR | compiled.OrderedIR: - """Compile node into CompileArrayValue. Caches result.""" - return _compile_node(node, ordered) - - -@functools.singledispatch -def _compile_node( - node: nodes.BigFrameNode, ordered: bool = True -) -> compiled.UnorderedIR: - """Defines transformation but isn't cached, always use compile_node instead""" - raise ValueError(f"Can't compile unrecognized node: {node}") - +@dataclasses.dataclass(frozen=True) +class Compiler: + # In strict mode, ordering will always be deterministic + # In unstrict mode, ordering from ReadTable or after joins may be ambiguous to improve query performance. + strict: bool = True + + def compile_ordered_ir(self, node: nodes.BigFrameNode) -> compiled.OrderedIR: + ir = typing.cast(compiled.OrderedIR, self.compile_node(node, True)) + assert ir.has_total_order + return ir + + def compile_unordered_ir(self, node: nodes.BigFrameNode) -> compiled.UnorderedIR: + return typing.cast(compiled.UnorderedIR, self.compile_node(node, False)) + + def compile_peak_sql( + self, node: nodes.BigFrameNode, n_rows: int + ) -> typing.Optional[str]: + return self.compile_unordered_ir(node).peek_sql(n_rows) + + # TODO: Remove cache when schema no longer requires compilation to derive schema (and therefor only compiles for execution) + @functools.lru_cache(maxsize=5000) + def compile_node( + self, node: nodes.BigFrameNode, ordered: bool = True + ) -> compiled.UnorderedIR | compiled.OrderedIR: + """Compile node into CompileArrayValue. Caches result.""" + return self._compile_node(node, ordered) + + @functools.singledispatchmethod + def _compile_node( + self, node: nodes.BigFrameNode, ordered: bool = True + ) -> compiled.UnorderedIR: + """Defines transformation but isn't cached, always use compile_node instead""" + raise ValueError(f"Can't compile unrecognized node: {node}") + + @_compile_node.register + def compile_join(self, node: nodes.JoinNode, ordered: bool = True): + if ordered: + left_ordered = self.compile_ordered_ir(node.left_child) + right_ordered = self.compile_ordered_ir(node.right_child) + return bigframes.core.compile.single_column.join_by_column_ordered( + left=left_ordered, + right=right_ordered, + join=node.join, + ) + else: + left_unordered = self.compile_unordered_ir(node.left_child) + right_unordered = self.compile_unordered_ir(node.right_child) + return bigframes.core.compile.single_column.join_by_column_unordered( + left=left_unordered, + right=right_unordered, + join=node.join, + ) -@_compile_node.register -def compile_join(node: nodes.JoinNode, ordered: bool = True): - if ordered: - left_ordered = compile_ordered_ir(node.left_child) - right_ordered = compile_ordered_ir(node.right_child) - return bigframes.core.compile.single_column.join_by_column_ordered( - left=left_ordered, - right=right_ordered, - join=node.join, + @_compile_node.register + def compile_readlocal(self, node: nodes.ReadLocalNode, ordered: bool = True): + array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) + ordered_ir = compiled.OrderedIR.from_pandas(array_as_pd, node.schema) + if ordered: + return ordered_ir + else: + return ordered_ir.to_unordered() + + @_compile_node.register + def compile_cached_table(self, node: nodes.CachedTableNode, ordered: bool = True): + full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" + used_columns = ( + *node.schema.names, + *node.hidden_columns, ) - else: - left_unordered = compile_unordered_ir(node.left_child) - right_unordered = compile_unordered_ir(node.right_child) - return bigframes.core.compile.single_column.join_by_column_unordered( - left=left_unordered, - right=right_unordered, - join=node.join, + # Physical schema might include unused columns, unsupported datatypes like JSON + physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( + list(i for i in node.physical_schema if i.name in used_columns) ) + ibis_table = ibis.table(physical_schema, full_table_name) + if ordered: + if node.ordering is None: + # If this happens, session malfunctioned while applying cached results. + raise ValueError( + "Cannot use unordered cached value. Result requires ordering information." + ) + if self.strict and not isinstance(node.ordering, bf_ordering.TotalOrdering): + raise ValueError( + "Cannot use partially ordered cached value. Result requires total ordering information." + ) + return compiled.OrderedIR( + ibis_table, + columns=tuple( + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + ibis_table[col] + ) + for col in node.schema.names + ), + ordering=node.ordering, + hidden_ordering_columns=[ibis_table[c] for c in node.hidden_columns], + ) + else: + return compiled.UnorderedIR( + ibis_table, + columns=tuple( + bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( + ibis_table[col] + ) + for col in node.schema.names + ), + ) -@_compile_node.register -def compile_readlocal(node: nodes.ReadLocalNode, ordered: bool = True): - array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) - ordered_ir = compiled.OrderedIR.from_pandas(array_as_pd, node.schema) - if ordered: - return ordered_ir - else: - return ordered_ir.to_unordered() - - -@_compile_node.register -def compile_cached_table(node: nodes.CachedTableNode, ordered: bool = True): - full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" - used_columns = ( - *node.schema.names, - *node.hidden_columns, - ) - # Physical schema might include unused columns, unsupported datatypes like JSON - physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( - list(i for i in node.physical_schema if i.name in used_columns) - ) - ibis_table = ibis.table(physical_schema, full_table_name) - if ordered: - if node.ordering is None: - # If this happens, session malfunctioned while applying cached results. - raise ValueError( - "Cannot use unordered cached value. Result requires ordering information." + @_compile_node.register + def compile_readtable(self, node: nodes.ReadTableNode, ordered: bool = True): + if ordered: + return self.compile_read_table_ordered(node) + else: + return self.compile_read_table_unordered(node) + + def read_table_as_unordered_ibis( + self, node: nodes.ReadTableNode + ) -> ibis.expr.types.Table: + full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" + used_columns = ( + *node.schema.names, + *[i for i in node.total_order_cols if i not in node.schema.names], + ) + # Physical schema might include unused columns, unsupported datatypes like JSON + physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( + list(i for i in node.physical_schema if i.name in used_columns) + ) + if node.at_time is not None or node.sql_predicate is not None: + import bigframes.session._io.bigquery + + sql = bigframes.session._io.bigquery.to_query( + full_table_name, + columns=used_columns, + sql_predicate=node.sql_predicate, + time_travel_timestamp=node.at_time, ) - return compiled.OrderedIR( + return ibis.backends.bigquery.Backend().sql( + schema=physical_schema, query=sql + ) + else: + return ibis.table(physical_schema, full_table_name) + + def compile_read_table_unordered(self, node: nodes.ReadTableNode): + ibis_table = self.read_table_as_unordered_ibis(node) + return compiled.UnorderedIR( ibis_table, - columns=tuple( + tuple( bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( ibis_table[col] ) for col in node.schema.names ), - ordering=node.ordering, - hidden_ordering_columns=[ibis_table[c] for c in node.hidden_columns], ) - else: - return compiled.UnorderedIR( + def compile_read_table_ordered(self, node: nodes.ReadTableNode): + ibis_table = self.read_table_as_unordered_ibis(node) + if node.total_order_cols: + ordering_value_columns = tuple( + bf_ordering.ascending_over(col) for col in node.total_order_cols + ) + if node.order_col_is_sequential: + integer_encoding = bf_ordering.IntegerEncoding( + is_encoded=True, is_sequential=True + ) + else: + integer_encoding = bf_ordering.IntegerEncoding() + ordering: bf_ordering.RowOrdering = bf_ordering.TotalOrdering( + ordering_value_columns, + integer_encoding=integer_encoding, + total_ordering_columns=frozenset(node.total_order_cols), + ) + hidden_columns = () + elif self.strict: + ibis_table, ordering = default_ordering.gen_default_ordering( + ibis_table, use_double_hash=True + ) + hidden_columns = tuple( + ibis_table[col] + for col in ibis_table.columns + if col not in node.schema.names + ) + else: + # In unstrict mode, don't generate total ordering from hashing as this is + # expensive (prevent removing any columns from table scan) + ordering, hidden_columns = bf_ordering.RowOrdering(), () + return compiled.OrderedIR( ibis_table, columns=tuple( bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( @@ -137,182 +230,91 @@ def compile_cached_table(node: nodes.CachedTableNode, ordered: bool = True): ) for col in node.schema.names ), + ordering=ordering, + hidden_ordering_columns=hidden_columns, ) + @_compile_node.register + def compile_promote_offsets( + self, node: nodes.PromoteOffsetsNode, ordered: bool = True + ): + result = self.compile_ordered_ir(node.child).promote_offsets(node.col_id) + return result if ordered else result.to_unordered() -@_compile_node.register -def compile_readtable(node: nodes.ReadTableNode, ordered: bool = True): - if ordered: - return compile_read_table_ordered(node) - else: - return compile_read_table_unordered(node) - - -def read_table_as_unordered_ibis(node: nodes.ReadTableNode) -> ibis.expr.types.Table: - full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" - used_columns = ( - *node.schema.names, - *[i for i in node.total_order_cols if i not in node.schema.names], - ) - # Physical schema might include unused columns, unsupported datatypes like JSON - physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( - list(i for i in node.physical_schema if i.name in used_columns) - ) - if node.at_time is not None or node.sql_predicate is not None: - import bigframes.session._io.bigquery - - sql = bigframes.session._io.bigquery.to_query( - full_table_name, - columns=used_columns, - sql_predicate=node.sql_predicate, - time_travel_timestamp=node.at_time, - ) - return ibis.backends.bigquery.Backend().sql(schema=physical_schema, query=sql) - else: - return ibis.table(physical_schema, full_table_name) - + @_compile_node.register + def compile_filter(self, node: nodes.FilterNode, ordered: bool = True): + return self.compile_node(node.child, ordered).filter(node.predicate) -def compile_read_table_unordered(node: nodes.ReadTableNode): - ibis_table = read_table_as_unordered_ibis(node) - return compiled.UnorderedIR( - ibis_table, - tuple( - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - ibis_table[col] - ) - for col in node.schema.names - ), - ) + @_compile_node.register + def compile_orderby(self, node: nodes.OrderByNode, ordered: bool = True): + if ordered: + return self.compile_ordered_ir(node.child).order_by(node.by) + else: + return self.compile_unordered_ir(node.child) + @_compile_node.register + def compile_reversed(self, node: nodes.ReversedNode, ordered: bool = True): + if ordered: + return self.compile_ordered_ir(node.child).reversed() + else: + return self.compile_unordered_ir(node.child) + + @_compile_node.register + def compile_projection(self, node: nodes.ProjectionNode, ordered: bool = True): + result = self.compile_node(node.child, ordered) + return result.projection(node.assignments) + + @_compile_node.register + def compile_concat(self, node: nodes.ConcatNode, ordered: bool = True): + if ordered: + compiled_ordered = [self.compile_ordered_ir(node) for node in node.children] + return concat_impl.concat_ordered(compiled_ordered) + else: + compiled_unordered = [ + self.compile_unordered_ir(node) for node in node.children + ] + return concat_impl.concat_unordered(compiled_unordered) + + @_compile_node.register + def compile_rowcount(self, node: nodes.RowCountNode, ordered: bool = True): + result = self.compile_unordered_ir(node.child).row_count() + return result if ordered else result.to_unordered() -def compile_read_table_ordered(node: nodes.ReadTableNode): - ibis_table = read_table_as_unordered_ibis(node) - if node.total_order_cols: - ordering_value_columns = tuple( - bf_ordering.ascending_over(col) for col in node.total_order_cols + @_compile_node.register + def compile_aggregate(self, node: nodes.AggregateNode, ordered: bool = True): + has_ordered_aggregation_ops = any( + aggregate.op.can_order_by for aggregate, _ in node.aggregations ) - if node.order_col_is_sequential: - integer_encoding = bf_ordering.IntegerEncoding( - is_encoded=True, is_sequential=True + if ordered and has_ordered_aggregation_ops: + return self.compile_ordered_ir(node.child).aggregate( + node.aggregations, node.by_column_ids, node.dropna ) else: - integer_encoding = bf_ordering.IntegerEncoding() - ordering = bf_ordering.TotalOrdering( - ordering_value_columns, - integer_encoding=integer_encoding, - total_ordering_columns=frozenset(node.total_order_cols), - ) - hidden_columns = () - else: - ibis_table, ordering = default_ordering.gen_default_ordering( - ibis_table, use_double_hash=True - ) - hidden_columns = tuple( - ibis_table[col] - for col in ibis_table.columns - if col not in node.schema.names - ) - return compiled.OrderedIR( - ibis_table, - columns=tuple( - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - ibis_table[col] + result = self.compile_unordered_ir(node.child).aggregate( + node.aggregations, node.by_column_ids, node.dropna ) - for col in node.schema.names - ), - ordering=ordering, - hidden_ordering_columns=hidden_columns, - ) - - -@_compile_node.register -def compile_promote_offsets(node: nodes.PromoteOffsetsNode, ordered: bool = True): - result = compile_ordered_ir(node.child).promote_offsets(node.col_id) - return result if ordered else result.to_unordered() - - -@_compile_node.register -def compile_filter(node: nodes.FilterNode, ordered: bool = True): - return compile_node(node.child, ordered).filter(node.predicate) - - -@_compile_node.register -def compile_orderby(node: nodes.OrderByNode, ordered: bool = True): - if ordered: - return compile_ordered_ir(node.child).order_by(node.by) - else: - return compile_unordered_ir(node.child) - - -@_compile_node.register -def compile_reversed(node: nodes.ReversedNode, ordered: bool = True): - if ordered: - return compile_ordered_ir(node.child).reversed() - else: - return compile_unordered_ir(node.child) - - -@_compile_node.register -def compile_projection(node: nodes.ProjectionNode, ordered: bool = True): - result = compile_node(node.child, ordered) - return result.projection(node.assignments) - - -@_compile_node.register -def compile_concat(node: nodes.ConcatNode, ordered: bool = True): - if ordered: - compiled_ordered = [compile_ordered_ir(node) for node in node.children] - return concat_impl.concat_ordered(compiled_ordered) - else: - compiled_unordered = [compile_unordered_ir(node) for node in node.children] - return concat_impl.concat_unordered(compiled_unordered) - - -@_compile_node.register -def compile_rowcount(node: nodes.RowCountNode, ordered: bool = True): - result = compile_unordered_ir(node.child).row_count() - return result if ordered else result.to_unordered() - - -@_compile_node.register -def compile_aggregate(node: nodes.AggregateNode, ordered: bool = True): - has_ordered_aggregation_ops = any( - aggregate.op.can_order_by for aggregate, _ in node.aggregations - ) - if ordered and has_ordered_aggregation_ops: - return compile_ordered_ir(node.child).aggregate( - node.aggregations, node.by_column_ids, node.dropna - ) - else: - result = compile_unordered_ir(node.child).aggregate( - node.aggregations, node.by_column_ids, node.dropna + return result if ordered else result.to_unordered() + + @_compile_node.register + def compile_window(self, node: nodes.WindowOpNode, ordered: bool = True): + result = self.compile_ordered_ir(node.child).project_window_op( + node.column_name, + node.op, + node.window_spec, + node.output_name, + never_skip_nulls=node.never_skip_nulls, + skip_reproject_unsafe=node.skip_reproject_unsafe, ) return result if ordered else result.to_unordered() + @_compile_node.register + def compile_reproject(self, node: nodes.ReprojectOpNode, ordered: bool = True): + return self.compile_node(node.child, ordered)._reproject_to_table() -@_compile_node.register -def compile_window(node: nodes.WindowOpNode, ordered: bool = True): - result = compile_ordered_ir(node.child).project_window_op( - node.column_name, - node.op, - node.window_spec, - node.output_name, - never_skip_nulls=node.never_skip_nulls, - skip_reproject_unsafe=node.skip_reproject_unsafe, - ) - return result if ordered else result.to_unordered() - - -@_compile_node.register -def compile_reproject(node: nodes.ReprojectOpNode, ordered: bool = True): - return compile_node(node.child, ordered)._reproject_to_table() - - -@_compile_node.register -def compile_explode(node: nodes.ExplodeNode, ordered: bool = True): - return compile_node(node.child, ordered).explode(node.column_ids) - + @_compile_node.register + def compile_explode(self, node: nodes.ExplodeNode, ordered: bool = True): + return self.compile_node(node.child, ordered).explode(node.column_ids) -@_compile_node.register -def compile_random_sample(node: nodes.RandomSampleNode, ordered: bool = True): - return compile_node(node.child, ordered)._uniform_sampling(node.fraction) + @_compile_node.register + def compile_random_sample(self, node: nodes.RandomSampleNode, ordered: bool = True): + return self.compile_node(node.child, ordered)._uniform_sampling(node.fraction) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index dbcfc282e4f..aadab9f5cc7 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -372,7 +372,7 @@ class CachedTableNode(BigFrameNode): table_id: str = field() physical_schema: Tuple[bq.SchemaField, ...] = field() - ordering: typing.Optional[orderings.TotalOrdering] = field() + ordering: typing.Optional[orderings.RowOrdering] = field() def __post_init__(self): # enforce invariants From ed06436612c0d46f190f79721416d473bde7e2f4 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 15 Jul 2024 16:30:30 -0700 Subject: [PATCH 707/921] feat: support remote function cleanup with `session.close` (#818) * feat: support remote function cleanup with `session.close` * accept the possibility that the artifact may have already been deleted * add cleanup by previous session id * add more documentation * hold session artifacts in a remote function session class * fix the missing return keyword --- bigframes/functions/remote_function.py | 987 ++++++++++++--------- bigframes/pandas/__init__.py | 17 +- bigframes/session/__init__.py | 28 +- tests/system/large/test_remote_function.py | 150 +++- tests/system/large/test_session.py | 10 +- 5 files changed, 756 insertions(+), 436 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index c1878b6c31b..f24ba1b5fb6 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -24,14 +24,17 @@ import string import sys import tempfile +import threading from typing import ( Any, cast, + Dict, List, Mapping, NamedTuple, Optional, Sequence, + Set, Tuple, TYPE_CHECKING, Union, @@ -67,11 +70,80 @@ logger = logging.getLogger(__name__) +# Naming convention for the remote function artifacts +_BIGFRAMES_REMOTE_FUNCTION_PREFIX = "bigframes" +_BQ_FUNCTION_NAME_SEPERATOR = "_" +_GCF_FUNCTION_NAME_SEPERATOR = "-" + # Protocol version 4 is available in python version 3.4 and above # https://docs.python.org/3/library/pickle.html#data-stream-format _pickle_protocol_version = 4 +def _clean_up_by_session_id( + bqclient: bigquery.Client, + gcfclient: functions_v2.FunctionServiceClient, + dataset: bigquery.DatasetReference, + session_id: str, +): + """Delete remote function artifacts for a session id, where the session id + was not necessarily created in the current runtime. This is useful if the + user worked with a BigQuery DataFrames session previously and remembered the + session id, and now wants to clean up its temporary resources at a later + point in time. + """ + + # First clean up the BQ remote functions and then the underlying + # cloud functions, so that at no point we are left with a remote function + # that is pointing to a cloud function that does not exist + + endpoints_to_be_deleted: Set[str] = set() + match_prefix = "".join( + [ + _BIGFRAMES_REMOTE_FUNCTION_PREFIX, + _BQ_FUNCTION_NAME_SEPERATOR, + session_id, + _BQ_FUNCTION_NAME_SEPERATOR, + ] + ) + for routine in bqclient.list_routines(dataset): + routine = cast(bigquery.Routine, routine) + + # skip past the routines not belonging to the given session id, or + # non-remote-function routines + if ( + routine.type_ != bigquery.RoutineType.SCALAR_FUNCTION + or not cast(str, routine.routine_id).startswith(match_prefix) + or not routine.remote_function_options + or not routine.remote_function_options.endpoint + ): + continue + + # Let's forgive the edge case possibility that the BQ remote function + # may have been deleted at the same time directly by the user + bqclient.delete_routine(routine, not_found_ok=True) + endpoints_to_be_deleted.add(routine.remote_function_options.endpoint) + + # Now clean up the cloud functions + bq_location = bqclient.get_dataset(dataset).location + bq_location, gcf_location = get_remote_function_locations(bq_location) + parent_path = gcfclient.common_location_path( + project=dataset.project, location=gcf_location + ) + for gcf in gcfclient.list_functions(parent=parent_path): + # skip past the cloud functions not attached to any BQ remote function + # belonging to the given session id + if gcf.service_config.uri not in endpoints_to_be_deleted: + continue + + # Let's forgive the edge case possibility that the cloud function + # may have been deleted at the same time directly by the user + try: + gcfclient.delete_function(name=gcf.name) + except google.api_core.exceptions.NotFound: + pass + + def get_remote_function_locations(bq_location): """Get BQ location and cloud functions region given a BQ client.""" # TODO(shobs, b/274647164): Find the best way to determine default location. @@ -102,7 +174,9 @@ def _get_hash(def_, package_requirements=None): return hashlib.md5(def_repr).hexdigest() -def _get_updated_package_requirements(package_requirements, is_row_processor): +def _get_updated_package_requirements( + package_requirements=None, is_row_processor=False +): requirements = [f"cloudpickle=={cloudpickle.__version__}"] if is_row_processor: # bigframes remote function will send an entire row of data as json, @@ -130,31 +204,20 @@ class IbisSignature(NamedTuple): output_type: IbisDataType -def get_cloud_function_name( - def_, uniq_suffix=None, package_requirements=None, is_row_processor=False -): +def get_cloud_function_name(function_hash, session_id, uniq_suffix=None): "Get a name for the cloud function for the given user defined function." - - # Augment user package requirements with any internal package - # requirements - package_requirements = _get_updated_package_requirements( - package_requirements, is_row_processor - ) - - cf_name = _get_hash(def_, package_requirements) - cf_name = f"bigframes-{cf_name}" # for identification + parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX, session_id, function_hash] if uniq_suffix: - cf_name = f"{cf_name}-{uniq_suffix}" - return cf_name, package_requirements + parts.append(uniq_suffix) + return _GCF_FUNCTION_NAME_SEPERATOR.join(parts) -def get_remote_function_name(def_, uniq_suffix=None, package_requirements=None): +def get_remote_function_name(function_hash, session_id, uniq_suffix=None): "Get a name for the BQ remote function for the given user defined function." - bq_rf_name = _get_hash(def_, package_requirements) - bq_rf_name = f"bigframes_{bq_rf_name}" # for identification + parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX, session_id, function_hash] if uniq_suffix: - bq_rf_name = f"{bq_rf_name}_{uniq_suffix}" - return bq_rf_name + parts.append(uniq_suffix) + return _BQ_FUNCTION_NAME_SEPERATOR.join(parts) class RemoteFunctionClient: @@ -272,6 +335,10 @@ def get_cloud_function_fully_qualified_name(self, name): self._gcp_project_id, self._cloud_function_region, name ) + def get_remote_function_fully_qualilfied_name(self, name): + "Get the fully qualilfied name for a BQ remote function." + return f"{self._gcp_project_id}.{self._bq_dataset}.{name}" + def get_cloud_function_endpoint(self, name): """Get the http endpoint of a cloud function if it exists.""" fully_qualified_name = self.get_cloud_function_fully_qualified_name(name) @@ -478,20 +545,31 @@ def provision_bq_remote_function( cloud_function_memory_mib, ): """Provision a BigQuery remote function.""" + # Augment user package requirements with any internal package + # requirements + package_requirements = _get_updated_package_requirements( + package_requirements, is_row_processor + ) + + # Compute a unique hash representing the user code + function_hash = _get_hash(def_, package_requirements) + # If reuse of any existing function with the same name (indicated by the # same hash of its source code) is not intended, then attach a unique # suffix to the intended function name to make it unique. uniq_suffix = None if not reuse: + # use 4 digits as a unique suffix which should suffice for + # uniqueness per session uniq_suffix = "".join( - random.choices(string.ascii_lowercase + string.digits, k=8) + random.choices(string.ascii_lowercase + string.digits, k=4) ) # Derive the name of the cloud function underlying the intended BQ # remote function, also collect updated package requirements as # determined in the name resolution - cloud_function_name, package_requirements = get_cloud_function_name( - def_, uniq_suffix, package_requirements, is_row_processor + cloud_function_name = get_cloud_function_name( + function_hash, self._session.session_id, uniq_suffix ) cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) @@ -516,7 +594,7 @@ def provision_bq_remote_function( remote_function_name = name if not remote_function_name: remote_function_name = get_remote_function_name( - def_, uniq_suffix, package_requirements + function_hash, self._session.session_id, uniq_suffix ) rf_endpoint, rf_conn = self.get_remote_function_specs(remote_function_name) @@ -524,6 +602,7 @@ def provision_bq_remote_function( # 1. It does not exist # 2. It exists but the existing remote function has different # configuration than intended + created_new = False if not rf_endpoint or ( rf_endpoint != cf_endpoint or rf_conn != self._bq_connection_id ): @@ -540,10 +619,12 @@ def provision_bq_remote_function( remote_function_name, max_batching_rows, ) + + created_new = True else: logger.info(f"Remote function {remote_function_name} already exists.") - return remote_function_name, cloud_function_name + return remote_function_name, cloud_function_name, created_new def get_remote_function_specs(self, remote_function_name): """Check whether a remote function already exists for the udf.""" @@ -645,426 +726,500 @@ def get_routine_reference( return dataset_ref.routine(routine_ref_str) -# Inspired by @udf decorator implemented in ibis-bigquery package -# https://github.com/ibis-project/ibis-bigquery/blob/main/ibis_bigquery/udf/__init__.py -# which has moved as @js to the ibis package -# https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py -def remote_function( - input_types: Union[None, type, Sequence[type]] = None, - output_type: Optional[type] = None, - session: Optional[Session] = None, - bigquery_client: Optional[bigquery.Client] = None, - bigquery_connection_client: Optional[ - bigquery_connection_v1.ConnectionServiceClient - ] = None, - cloud_functions_client: Optional[functions_v2.FunctionServiceClient] = None, - resource_manager_client: Optional[resourcemanager_v3.ProjectsClient] = None, - dataset: Optional[str] = None, - bigquery_connection: Optional[str] = None, - reuse: bool = True, - name: Optional[str] = None, - packages: Optional[Sequence[str]] = None, - cloud_function_service_account: Optional[str] = None, - cloud_function_kms_key_name: Optional[str] = None, - cloud_function_docker_repository: Optional[str] = None, - max_batching_rows: Optional[int] = 1000, - cloud_function_timeout: Optional[int] = 600, - cloud_function_max_instances: Optional[int] = None, - cloud_function_vpc_connector: Optional[str] = None, - cloud_function_memory_mib: Optional[int] = 1024, -): - """Decorator to turn a user defined function into a BigQuery remote function. - - .. deprecated:: 0.0.1 - This is an internal method. Please use :func:`bigframes.pandas.remote_function` instead. - - .. note:: - Please make sure following is setup before using this API: - - 1. Have the below APIs enabled for your project: - - * BigQuery Connection API - * Cloud Functions API - * Cloud Run API - * Cloud Build API - * Artifact Registry API - * Cloud Resource Manager API - - This can be done from the cloud console (change `PROJECT_ID` to yours): - https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID - - Or from the gcloud CLI: - - `$ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com` - - 2. Have following IAM roles enabled for you: - - * BigQuery Data Editor (roles/bigquery.dataEditor) - * BigQuery Connection Admin (roles/bigquery.connectionAdmin) - * Cloud Functions Developer (roles/cloudfunctions.developer) - * Service Account User (roles/iam.serviceAccountUser) on the service account `PROJECT_NUMBER-compute@developer.gserviceaccount.com` - * Storage Object Viewer (roles/storage.objectViewer) - * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) - - 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: - - 1. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection - 2. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function - - Alternatively, the IAM could also be setup via the gcloud CLI: - - `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. - - Args: - input_types (None, type, or sequence(type)): - For scalar user defined function it should be the input type or - sequence of input types. For row processing user defined function, - type `Series` should be specified. - output_type (Optional[type]): - Data type of the output in the user defined function. - session (bigframes.Session, Optional): - BigQuery DataFrames session to use for getting default project, - dataset and BigQuery connection. - bigquery_client (google.cloud.bigquery.Client, Optional): - Client to use for BigQuery operations. If this param is not provided - then bigquery client from the session would be used. - bigquery_connection_client (google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional): - Client to use for BigQuery connection operations. If this param is - not provided then bigquery connection client from the session would - be used. - cloud_functions_client (google.cloud.functions_v2.FunctionServiceClient, Optional): - Client to use for cloud functions operations. If this param is not - provided then the functions client from the session would be used. - resource_manager_client (google.cloud.resourcemanager_v3.ProjectsClient, Optional): - Client to use for cloud resource management operations, e.g. for - getting and setting IAM roles on cloud resources. If this param is - not provided then resource manager client from the session would be - used. - dataset (str, Optional.): - Dataset in which to create a BigQuery remote function. It should be in - `.` or `` format. If this - parameter is not provided then session dataset id is used. - bigquery_connection (str, Optional): - Name of the BigQuery connection in the form of `CONNECTION_ID` or - `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID`. - If this param is not provided then the bigquery connection from the session - would be used. If it is pre created in the same location as the - `bigquery_client.location` then it would be used, otherwise it is created - dynamically using the `bigquery_connection_client` assuming the user has necessary - priviliges. The PROJECT_ID should be the same as the BigQuery connection project. - reuse (bool, Optional): - Reuse the remote function if is already exists. - `True` by default, which results in reusing an existing remote - function and corresponding cloud function (if any) that was - previously created for the same udf. - Setting it to `False` forces the creation of a unique remote function. - If the required remote function does not exist then it would be - created irrespective of this param. - name (str, Optional): - Explicit name of the persisted BigQuery remote function. Use it with - caution, because two users working in the same project and dataset - could overwrite each other's remote functions if they use the same - persistent name. - packages (str[], Optional): - Explicit name of the external package dependencies. Each dependency - is added to the `requirements.txt` as is, and can be of the form - supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. - cloud_function_service_account (str, Optional): - Service account to use for the cloud functions. If not provided then - the default service account would be used. See - https://cloud.google.com/functions/docs/securing/function-identity - for more details. Please make sure the service account has the - necessary IAM permissions configured as described in - https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. - cloud_function_kms_key_name (str, Optional): - Customer managed encryption key to protect cloud functions and - related data at rest. This is of the format - projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY. - Read https://cloud.google.com/functions/docs/securing/cmek for - more details including granting necessary service accounts - access to the key. - cloud_function_docker_repository (str, Optional): - Docker repository created with the same encryption key as - `cloud_function_kms_key_name` to store encrypted artifacts - created to support the cloud function. This is of the format - projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. - For more details see - https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. - max_batching_rows (int, Optional): - The maximum number of rows to be batched for processing in the - BQ remote function. Default value is 1000. A lower number can be - passed to avoid timeouts in case the user code is too complex to - process large number of rows fast enough. A higher number can be - used to increase throughput in case the user code is fast enough. - `None` can be passed to let BQ remote functions service apply - default batching. See for more details - https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. - cloud_function_timeout (int, Optional): - The maximum amount of time (in seconds) BigQuery should wait for - the cloud function to return a response. See for more details - https://cloud.google.com/functions/docs/configuring/timeout. - Please note that even though the cloud function (2nd gen) itself - allows seeting up to 60 minutes of timeout, BigQuery remote - function can wait only up to 20 minutes, see for more details - https://cloud.google.com/bigquery/quotas#remote_function_limits. - By default BigQuery DataFrames uses a 10 minute timeout. `None` - can be passed to let the cloud functions default timeout take effect. - cloud_function_max_instances (int, Optional): - The maximumm instance count for the cloud function created. This - can be used to control how many cloud function instances can be - active at max at any given point of time. Lower setting can help - control the spike in the billing. Higher setting can help - support processing larger scale data. When not specified, cloud - function's default setting applies. For more details see - https://cloud.google.com/functions/docs/configuring/max-instances. - cloud_function_vpc_connector (str, Optional): - The VPC connector you would like to configure for your cloud - function. This is useful if your code needs access to data or - service(s) that are on a VPC network. See for more details - https://cloud.google.com/functions/docs/networking/connecting-vpc. - cloud_function_memory_mib (int, Optional): - The amounts of memory (in mebibytes) to allocate for the cloud - function (2nd gen) created. This also dictates a corresponding - amount of allocated CPU for the function. By default a memory of - 1024 MiB is set for the cloud functions created to support - BigQuery DataFrames remote function. If you want to let the - default memory of cloud functions be allocated, pass `None`. See - for more details - https://cloud.google.com/functions/docs/configuring/memory. - """ - # Some defaults may be used from the session if not provided otherwise - import bigframes.exceptions as bf_exceptions - import bigframes.pandas as bpd - import bigframes.series as bf_series - import bigframes.session - - session = cast(bigframes.session.Session, session or bpd.get_global_session()) - - # A BigQuery client is required to perform BQ operations - if not bigquery_client: - bigquery_client = session.bqclient - if not bigquery_client: - raise ValueError( - "A bigquery client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) +class _RemoteFunctionSession: + """Session to manage remote functions.""" - # A BigQuery connection client is required to perform BQ connection operations - if not bigquery_connection_client: - bigquery_connection_client = session.bqconnectionclient - if not bigquery_connection_client: - raise ValueError( - "A bigquery connection client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) + def __init__(self): + # Session level mapping of remote function artifacts + self._temp_session_artifacts: Dict[str, str] = dict() - # A cloud functions client is required to perform cloud functions operations - if not cloud_functions_client: - cloud_functions_client = session.cloudfunctionsclient - if not cloud_functions_client: - raise ValueError( - "A cloud functions client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) + # Lock to synchronize the update of the session level mapping + self._session_artifacts_lock = threading.Lock() - # A resource manager client is required to get/set IAM operations - if not resource_manager_client: - resource_manager_client = session.resourcemanagerclient - if not resource_manager_client: - raise ValueError( - "A resource manager client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) + def _update_artifacts(self, bqrf_routine: str, gcf_path: str): + """Update remote function artifacts in the current session.""" + with self._session_artifacts_lock: + self._temp_session_artifacts[bqrf_routine] = gcf_path - # BQ remote function must be persisted, for which we need a dataset - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#:~:text=You%20cannot%20create%20temporary%20remote%20functions. - if dataset: - dataset_ref = bigquery.DatasetReference.from_string( - dataset, default_project=bigquery_client.project - ) - else: - dataset_ref = session._anonymous_dataset + def clean_up( + self, + bqclient: bigquery.Client, + gcfclient: functions_v2.FunctionServiceClient, + session_id: str, + ): + """Delete remote function artifacts in the current session.""" + with self._session_artifacts_lock: + for bqrf_routine, gcf_path in self._temp_session_artifacts.items(): + # Let's accept the possibility that the remote function may have + # been deleted directly by the user + bqclient.delete_routine(bqrf_routine, not_found_ok=True) + + # Let's accept the possibility that the cloud function may have + # been deleted directly by the user + try: + gcfclient.delete_function(name=gcf_path) + except google.api_core.exceptions.NotFound: + pass + + self._temp_session_artifacts.clear() + + # Inspired by @udf decorator implemented in ibis-bigquery package + # https://github.com/ibis-project/ibis-bigquery/blob/main/ibis_bigquery/udf/__init__.py + # which has moved as @js to the ibis package + # https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py + def remote_function( + self, + input_types: Union[None, type, Sequence[type]] = None, + output_type: Optional[type] = None, + session: Optional[Session] = None, + bigquery_client: Optional[bigquery.Client] = None, + bigquery_connection_client: Optional[ + bigquery_connection_v1.ConnectionServiceClient + ] = None, + cloud_functions_client: Optional[functions_v2.FunctionServiceClient] = None, + resource_manager_client: Optional[resourcemanager_v3.ProjectsClient] = None, + dataset: Optional[str] = None, + bigquery_connection: Optional[str] = None, + reuse: bool = True, + name: Optional[str] = None, + packages: Optional[Sequence[str]] = None, + cloud_function_service_account: Optional[str] = None, + cloud_function_kms_key_name: Optional[str] = None, + cloud_function_docker_repository: Optional[str] = None, + max_batching_rows: Optional[int] = 1000, + cloud_function_timeout: Optional[int] = 600, + cloud_function_max_instances: Optional[int] = None, + cloud_function_vpc_connector: Optional[str] = None, + cloud_function_memory_mib: Optional[int] = 1024, + ): + """Decorator to turn a user defined function into a BigQuery remote function. - bq_location, cloud_function_region = get_remote_function_locations( - bigquery_client.location - ) + .. deprecated:: 0.0.1 + This is an internal method. Please use :func:`bigframes.pandas.remote_function` instead. - # A connection is required for BQ remote function - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function - if not bigquery_connection: - bigquery_connection = session._bq_connection # type: ignore + .. note:: + Please make sure following is setup before using this API: - bigquery_connection = clients.resolve_full_bq_connection_name( - bigquery_connection, - default_project=dataset_ref.project, - default_location=bq_location, - ) - # Guaranteed to be the form of .. - ( - gcp_project_id, - bq_connection_location, - bq_connection_id, - ) = bigquery_connection.split(".") - if gcp_project_id.casefold() != dataset_ref.project.casefold(): - raise ValueError( - "The project_id does not match BigQuery connection gcp_project_id: " - f"{dataset_ref.project}." - ) - if bq_connection_location.casefold() != bq_location.casefold(): - raise ValueError( - "The location does not match BigQuery connection location: " - f"{bq_location}." - ) + 1. Have the below APIs enabled for your project: - # If any CMEK is intended then check that a docker repository is also specified - if ( - cloud_function_kms_key_name is not None - and cloud_function_docker_repository is None - ): - raise ValueError( - "cloud_function_docker_repository must be specified with cloud_function_kms_key_name." - " For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin" - ) + * BigQuery Connection API + * Cloud Functions API + * Cloud Run API + * Cloud Build API + * Artifact Registry API + * Cloud Resource Manager API - bq_connection_manager = None if session is None else session.bqconnectionmanager + This can be done from the cloud console (change `PROJECT_ID` to yours): + https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID - def wrapper(func): - nonlocal input_types, output_type + Or from the gcloud CLI: - if not callable(func): - raise TypeError("f must be callable, got {}".format(func)) + `$ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com` - if sys.version_info >= (3, 10): - # Add `eval_str = True` so that deferred annotations are turned into their - # corresponding type objects. Need Python 3.10 for eval_str parameter. - # https://docs.python.org/3/library/inspect.html#inspect.signature - signature_kwargs: Mapping[str, Any] = {"eval_str": True} + 2. Have following IAM roles enabled for you: + + * BigQuery Data Editor (roles/bigquery.dataEditor) + * BigQuery Connection Admin (roles/bigquery.connectionAdmin) + * Cloud Functions Developer (roles/cloudfunctions.developer) + * Service Account User (roles/iam.serviceAccountUser) on the service account `PROJECT_NUMBER-compute@developer.gserviceaccount.com` + * Storage Object Viewer (roles/storage.objectViewer) + * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) + + 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: + + 1. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection + 2. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + + Alternatively, the IAM could also be setup via the gcloud CLI: + + `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. + + Args: + input_types (None, type, or sequence(type)): + For scalar user defined function it should be the input type or + sequence of input types. For row processing user defined function, + type `Series` should be specified. + output_type (Optional[type]): + Data type of the output in the user defined function. + session (bigframes.Session, Optional): + BigQuery DataFrames session to use for getting default project, + dataset and BigQuery connection. + bigquery_client (google.cloud.bigquery.Client, Optional): + Client to use for BigQuery operations. If this param is not provided + then bigquery client from the session would be used. + bigquery_connection_client (google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional): + Client to use for BigQuery connection operations. If this param is + not provided then bigquery connection client from the session would + be used. + cloud_functions_client (google.cloud.functions_v2.FunctionServiceClient, Optional): + Client to use for cloud functions operations. If this param is not + provided then the functions client from the session would be used. + resource_manager_client (google.cloud.resourcemanager_v3.ProjectsClient, Optional): + Client to use for cloud resource management operations, e.g. for + getting and setting IAM roles on cloud resources. If this param is + not provided then resource manager client from the session would be + used. + dataset (str, Optional.): + Dataset in which to create a BigQuery remote function. It should be in + `.` or `` format. If this + parameter is not provided then session dataset id is used. + bigquery_connection (str, Optional): + Name of the BigQuery connection in the form of `CONNECTION_ID` or + `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID`. + If this param is not provided then the bigquery connection from the session + would be used. If it is pre created in the same location as the + `bigquery_client.location` then it would be used, otherwise it is created + dynamically using the `bigquery_connection_client` assuming the user has necessary + priviliges. The PROJECT_ID should be the same as the BigQuery connection project. + reuse (bool, Optional): + Reuse the remote function if is already exists. + `True` by default, which results in reusing an existing remote + function and corresponding cloud function (if any) that was + previously created for the same udf. + Setting it to `False` forces the creation of a unique remote function. + If the required remote function does not exist then it would be + created irrespective of this param. + name (str, Optional): + Explicit name of the persisted BigQuery remote function. Use it with + caution, because two users working in the same project and dataset + could overwrite each other's remote functions if they use the same + persistent name. When an explicit name is provided, any session + specific clean up (``bigframes.session.Session.close``/ + ``bigframes.pandas.close_session``/ + ``bigframes.pandas.reset_session``/ + ``bigframes.pandas.clean_up_by_session_id``) does not clean up + the function, and leaves it for the user to manage the function + and the associated cloud function directly. + packages (str[], Optional): + Explicit name of the external package dependencies. Each dependency + is added to the `requirements.txt` as is, and can be of the form + supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. + cloud_function_service_account (str, Optional): + Service account to use for the cloud functions. If not provided then + the default service account would be used. See + https://cloud.google.com/functions/docs/securing/function-identity + for more details. Please make sure the service account has the + necessary IAM permissions configured as described in + https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. + cloud_function_kms_key_name (str, Optional): + Customer managed encryption key to protect cloud functions and + related data at rest. This is of the format + projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY. + Read https://cloud.google.com/functions/docs/securing/cmek for + more details including granting necessary service accounts + access to the key. + cloud_function_docker_repository (str, Optional): + Docker repository created with the same encryption key as + `cloud_function_kms_key_name` to store encrypted artifacts + created to support the cloud function. This is of the format + projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. + For more details see + https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. + max_batching_rows (int, Optional): + The maximum number of rows to be batched for processing in the + BQ remote function. Default value is 1000. A lower number can be + passed to avoid timeouts in case the user code is too complex to + process large number of rows fast enough. A higher number can be + used to increase throughput in case the user code is fast enough. + `None` can be passed to let BQ remote functions service apply + default batching. See for more details + https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. + cloud_function_timeout (int, Optional): + The maximum amount of time (in seconds) BigQuery should wait for + the cloud function to return a response. See for more details + https://cloud.google.com/functions/docs/configuring/timeout. + Please note that even though the cloud function (2nd gen) itself + allows seeting up to 60 minutes of timeout, BigQuery remote + function can wait only up to 20 minutes, see for more details + https://cloud.google.com/bigquery/quotas#remote_function_limits. + By default BigQuery DataFrames uses a 10 minute timeout. `None` + can be passed to let the cloud functions default timeout take effect. + cloud_function_max_instances (int, Optional): + The maximumm instance count for the cloud function created. This + can be used to control how many cloud function instances can be + active at max at any given point of time. Lower setting can help + control the spike in the billing. Higher setting can help + support processing larger scale data. When not specified, cloud + function's default setting applies. For more details see + https://cloud.google.com/functions/docs/configuring/max-instances. + cloud_function_vpc_connector (str, Optional): + The VPC connector you would like to configure for your cloud + function. This is useful if your code needs access to data or + service(s) that are on a VPC network. See for more details + https://cloud.google.com/functions/docs/networking/connecting-vpc. + cloud_function_memory_mib (int, Optional): + The amounts of memory (in mebibytes) to allocate for the cloud + function (2nd gen) created. This also dictates a corresponding + amount of allocated CPU for the function. By default a memory of + 1024 MiB is set for the cloud functions created to support + BigQuery DataFrames remote function. If you want to let the + default memory of cloud functions be allocated, pass `None`. See + for more details + https://cloud.google.com/functions/docs/configuring/memory. + """ + # Some defaults may be used from the session if not provided otherwise + import bigframes.exceptions as bf_exceptions + import bigframes.pandas as bpd + import bigframes.series as bf_series + import bigframes.session + + session = cast(bigframes.session.Session, session or bpd.get_global_session()) + + # A BigQuery client is required to perform BQ operations + if not bigquery_client: + bigquery_client = session.bqclient + if not bigquery_client: + raise ValueError( + "A bigquery client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A BigQuery connection client is required to perform BQ connection operations + if not bigquery_connection_client: + bigquery_connection_client = session.bqconnectionclient + if not bigquery_connection_client: + raise ValueError( + "A bigquery connection client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A cloud functions client is required to perform cloud functions operations + if not cloud_functions_client: + cloud_functions_client = session.cloudfunctionsclient + if not cloud_functions_client: + raise ValueError( + "A cloud functions client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A resource manager client is required to get/set IAM operations + if not resource_manager_client: + resource_manager_client = session.resourcemanagerclient + if not resource_manager_client: + raise ValueError( + "A resource manager client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # BQ remote function must be persisted, for which we need a dataset + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#:~:text=You%20cannot%20create%20temporary%20remote%20functions. + if dataset: + dataset_ref = bigquery.DatasetReference.from_string( + dataset, default_project=bigquery_client.project + ) else: - signature_kwargs = {} + dataset_ref = session._anonymous_dataset - signature = inspect.signature( - func, - **signature_kwargs, + bq_location, cloud_function_region = get_remote_function_locations( + bigquery_client.location ) - # Try to get input types via type annotations. - if input_types is None: - input_types = [] - for parameter in signature.parameters.values(): - if (param_type := parameter.annotation) is inspect.Signature.empty: + # A connection is required for BQ remote function + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function + if not bigquery_connection: + bigquery_connection = session._bq_connection # type: ignore + + bigquery_connection = clients.resolve_full_bq_connection_name( + bigquery_connection, + default_project=dataset_ref.project, + default_location=bq_location, + ) + # Guaranteed to be the form of .. + ( + gcp_project_id, + bq_connection_location, + bq_connection_id, + ) = bigquery_connection.split(".") + if gcp_project_id.casefold() != dataset_ref.project.casefold(): + raise ValueError( + "The project_id does not match BigQuery connection gcp_project_id: " + f"{dataset_ref.project}." + ) + if bq_connection_location.casefold() != bq_location.casefold(): + raise ValueError( + "The location does not match BigQuery connection location: " + f"{bq_location}." + ) + + # If any CMEK is intended then check that a docker repository is also specified + if ( + cloud_function_kms_key_name is not None + and cloud_function_docker_repository is None + ): + raise ValueError( + "cloud_function_docker_repository must be specified with cloud_function_kms_key_name." + " For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin" + ) + + bq_connection_manager = session.bqconnectionmanager + + def wrapper(func): + nonlocal input_types, output_type + + if not callable(func): + raise TypeError("f must be callable, got {}".format(func)) + + if sys.version_info >= (3, 10): + # Add `eval_str = True` so that deferred annotations are turned into their + # corresponding type objects. Need Python 3.10 for eval_str parameter. + # https://docs.python.org/3/library/inspect.html#inspect.signature + signature_kwargs: Mapping[str, Any] = {"eval_str": True} + else: + signature_kwargs = {} + + signature = inspect.signature( + func, + **signature_kwargs, + ) + + # Try to get input types via type annotations. + if input_types is None: + input_types = [] + for parameter in signature.parameters.values(): + if (param_type := parameter.annotation) is inspect.Signature.empty: + raise ValueError( + "'input_types' was not set and parameter " + f"'{parameter.name}' is missing a type annotation. " + "Types are required to use @remote_function." + ) + input_types.append(param_type) + elif not isinstance(input_types, collections.abc.Sequence): + input_types = [input_types] + + if output_type is None: + if ( + output_type := signature.return_annotation + ) is inspect.Signature.empty: raise ValueError( - "'input_types' was not set and parameter " - f"'{parameter.name}' is missing a type annotation. " - "Types are required to use @remote_function." + "'output_type' was not set and function is missing a " + "return type annotation. Types are required to use " + "@remote_function." ) - input_types.append(param_type) - elif not isinstance(input_types, collections.abc.Sequence): - input_types = [input_types] - if output_type is None: - if (output_type := signature.return_annotation) is inspect.Signature.empty: - raise ValueError( - "'output_type' was not set and function is missing a " - "return type annotation. Types are required to use " - "@remote_function." + # The function will actually be receiving a pandas Series, but allow both + # BigQuery DataFrames and pandas object types for compatibility. + is_row_processor = False + if len(input_types) == 1 and ( + (input_type := input_types[0]) == bf_series.Series + or input_type == pandas.Series + ): + warnings.warn( + "input_types=Series is in preview.", + stacklevel=1, + category=bf_exceptions.PreviewWarning, ) - # The function will actually be receiving a pandas Series, but allow both - # BigQuery DataFrames and pandas object types for compatibility. - is_row_processor = False - if len(input_types) == 1 and ( - (input_type := input_types[0]) == bf_series.Series - or input_type == pandas.Series - ): - warnings.warn( - "input_types=Series is in preview.", - stacklevel=1, - category=bf_exceptions.PreviewWarning, + # we will model the row as a json serialized string containing the data + # and the metadata representing the row + input_types = [str] + is_row_processor = True + elif isinstance(input_types, type): + input_types = [input_types] + + # TODO(b/340898611): fix type error + ibis_signature = ibis_signature_from_python_signature( + signature, input_types, output_type # type: ignore ) - # we will model the row as a json serialized string containing the data - # and the metadata representing the row - input_types = [str] - is_row_processor = True - elif isinstance(input_types, type): - input_types = [input_types] + remote_function_client = RemoteFunctionClient( + dataset_ref.project, + cloud_function_region, + cloud_functions_client, + bq_location, + dataset_ref.dataset_id, + bigquery_client, + bq_connection_id, + bq_connection_manager, + cloud_function_service_account, + cloud_function_kms_key_name, + cloud_function_docker_repository, + session=session, # type: ignore + ) - # TODO(b/340898611): fix type error - ibis_signature = ibis_signature_from_python_signature( - signature, input_types, output_type # type: ignore - ) + # In the unlikely case where the user is trying to re-deploy the same + # function, cleanup the attributes we add below, first. This prevents + # the pickle from having dependencies that might not otherwise be + # present such as ibis or pandas. + def try_delattr(attr): + try: + delattr(func, attr) + except AttributeError: + pass + + try_delattr("bigframes_cloud_function") + try_delattr("bigframes_remote_function") + try_delattr("output_dtype") + try_delattr("ibis_node") + + ( + rf_name, + cf_name, + created_new, + ) = remote_function_client.provision_bq_remote_function( + func, + input_types=tuple( + third_party_ibis_bqtypes.BigQueryType.from_ibis(type_) + for type_ in ibis_signature.input_types + ), + output_type=third_party_ibis_bqtypes.BigQueryType.from_ibis( + ibis_signature.output_type + ), + reuse=reuse, + name=name, + package_requirements=packages, + max_batching_rows=max_batching_rows, + cloud_function_timeout=cloud_function_timeout, + cloud_function_max_instance_count=cloud_function_max_instances, + is_row_processor=is_row_processor, + cloud_function_vpc_connector=cloud_function_vpc_connector, + cloud_function_memory_mib=cloud_function_memory_mib, + ) - remote_function_client = RemoteFunctionClient( - dataset_ref.project, - cloud_function_region, - cloud_functions_client, - bq_location, - dataset_ref.dataset_id, - bigquery_client, - bq_connection_id, - bq_connection_manager, - cloud_function_service_account, - cloud_function_kms_key_name, - cloud_function_docker_repository, - session=session, # type: ignore - ) + # TODO: Move ibis logic to compiler step + node = ibis.udf.scalar.builtin( + func, + name=rf_name, + schema=f"{dataset_ref.project}.{dataset_ref.dataset_id}", + signature=(ibis_signature.input_types, ibis_signature.output_type), + ) + func.bigframes_cloud_function = ( + remote_function_client.get_cloud_function_fully_qualified_name(cf_name) + ) + func.bigframes_remote_function = ( + remote_function_client.get_remote_function_fully_qualilfied_name( + rf_name + ) + ) - # In the unlikely case where the user is trying to re-deploy the same - # function, cleanup the attributes we add below, first. This prevents - # the pickle from having dependencies that might not otherwise be - # present such as ibis or pandas. - def try_delattr(attr): - try: - delattr(func, attr) - except AttributeError: - pass - - try_delattr("bigframes_cloud_function") - try_delattr("bigframes_remote_function") - try_delattr("output_dtype") - try_delattr("ibis_node") - - rf_name, cf_name = remote_function_client.provision_bq_remote_function( - func, - input_types=tuple( - third_party_ibis_bqtypes.BigQueryType.from_ibis(type_) - for type_ in ibis_signature.input_types - ), - output_type=third_party_ibis_bqtypes.BigQueryType.from_ibis( - ibis_signature.output_type - ), - reuse=reuse, - name=name, - package_requirements=packages, - max_batching_rows=max_batching_rows, - cloud_function_timeout=cloud_function_timeout, - cloud_function_max_instance_count=cloud_function_max_instances, - is_row_processor=is_row_processor, - cloud_function_vpc_connector=cloud_function_vpc_connector, - cloud_function_memory_mib=cloud_function_memory_mib, - ) + func.output_dtype = ( + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( + ibis_signature.output_type + ) + ) + func.ibis_node = node + + # If a new remote function was created, update the cloud artifacts + # created in the session. This would be used to clean up any + # resources in the session. Note that we need to do this only for + # the case where an explicit name was not provided by the user and + # we used an internal name. For the cases where the user provided an + # explicit name, we are assuming that the user wants to persist them + # with that name and would directly manage their lifecycle. + if created_new and (not name): + self._update_artifacts( + func.bigframes_remote_function, func.bigframes_cloud_function + ) + return func - # TODO: Move ibis logic to compiler step - node = ibis.udf.scalar.builtin( - func, - name=rf_name, - schema=f"{dataset_ref.project}.{dataset_ref.dataset_id}", - signature=(ibis_signature.input_types, ibis_signature.output_type), - ) - func.bigframes_cloud_function = ( - remote_function_client.get_cloud_function_fully_qualified_name(cf_name) - ) - func.bigframes_remote_function = str(dataset_ref.routine(rf_name)) # type: ignore + return wrapper + + +def remote_function(*args, **kwargs): + remote_function_session = _RemoteFunctionSession() + return remote_function_session.remote_function(*args, **kwargs) - func.output_dtype = ( - bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( - ibis_signature.output_type - ) - ) - func.ibis_node = node - return func - return wrapper +remote_function.__doc__ = _RemoteFunctionSession.remote_function.__doc__ def read_gbq_function( diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index faba0f3aa38..eb990d23937 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -63,6 +63,7 @@ import bigframes.core.tools import bigframes.dataframe import bigframes.enums +import bigframes.functions.remote_function as bigframes_rf import bigframes.operations as ops import bigframes.series import bigframes.session @@ -768,8 +769,11 @@ def clean_up_by_session_id( location: Optional[str] = None, project: Optional[str] = None, ) -> None: - """Searches through table names in BigQuery and deletes tables - found matching the expected format. + """Searches through BigQuery tables and routines and deletes the ones + created during the session with the given session id. The match is + determined by having the session id present in the resource name or + metadata. The cloud functions serving the cleaned up routines are also + cleaned up. This could be useful if the session object has been lost. Calling `session.close()` or `bigframes.pandas.close_session()` @@ -794,7 +798,6 @@ def clean_up_by_session_id( None """ session = get_global_session() - client = session.bqclient if (location is None) != (project is None): raise ValueError( @@ -804,14 +807,18 @@ def clean_up_by_session_id( dataset = session._anonymous_dataset else: dataset = bigframes.session._io.bigquery.create_bq_dataset_reference( - client, + session.bqclient, location=location, project=project, api_name="clean_up_by_session_id", ) bigframes.session._io.bigquery.delete_tables_matching_session_id( - client, dataset, session_id + session.bqclient, dataset, session_id + ) + + bigframes_rf._clean_up_by_session_id( + session.bqclient, session.cloudfunctionsclient, dataset, session_id ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 0f7953d3d4b..10c0797873f 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -95,8 +95,7 @@ import bigframes.dtypes import bigframes.exceptions import bigframes.formatting_helpers as formatting_helpers -from bigframes.functions.remote_function import read_gbq_function as bigframes_rgf -from bigframes.functions.remote_function import remote_function as bigframes_rf +import bigframes.functions.remote_function as bigframes_rf import bigframes.session._io.bigquery as bf_io_bigquery import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table import bigframes.session.clients @@ -306,6 +305,8 @@ def __init__( else bigframes.enums.DefaultIndexKind.NULL ) + self._remote_function_session = bigframes_rf._RemoteFunctionSession() + @property def bqclient(self): return self._clients_provider.bqclient @@ -383,7 +384,7 @@ def __hash__(self): # Stable hash needed to use in expression tree return hash(str(self._anonymous_dataset)) - def close(self): + def _clean_up_tables(self): """Delete tables that were created with this session's session_id.""" client = self.bqclient project_id = self._anonymous_dataset.project @@ -393,6 +394,15 @@ def close(self): full_id = ".".join([project_id, dataset_id, table_id]) client.delete_table(full_id, not_found_ok=True) + def close(self): + """Delete resources that were created with this session's session_id. + This includes BigQuery tables, remote functions and cloud functions + serving the remote functions""" + self._clean_up_tables() + self._remote_function_session.clean_up( + self.bqclient, self.cloudfunctionsclient, self.session_id + ) + def read_gbq( self, query_or_table: str, @@ -1613,7 +1623,13 @@ def remote_function( Explicit name of the persisted BigQuery remote function. Use it with caution, because two users working in the same project and dataset could overwrite each other's remote functions if they use the same - persistent name. + persistent name. When an explicit name is provided, any session + specific clean up (``bigframes.session.Session.close``/ + ``bigframes.pandas.close_session``/ + ``bigframes.pandas.reset_session``/ + ``bigframes.pandas.clean_up_by_session_id``) does not clean up + the function, and leaves it for the user to manage the function + and the associated cloud function directly. packages (str[], Optional): Explicit name of the external package dependencies. Each dependency is added to the `requirements.txt` as is, and can be of the form @@ -1689,7 +1705,7 @@ def remote_function( `bigframes_remote_function` - The bigquery remote function capable of calling into `bigframes_cloud_function`. """ - return bigframes_rf( + return self._remote_function_session.remote_function( input_types, output_type, session=self, @@ -1769,7 +1785,7 @@ def read_gbq_function( not including the `bigframes_cloud_function` property. """ - return bigframes_rgf( + return bigframes_rf.read_gbq_function( function_name=function_name, session=self, ) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index ef8b9811dfa..303c74f1fd7 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -22,13 +22,14 @@ import textwrap import google.api_core.exceptions -from google.cloud import bigquery, storage +from google.cloud import bigquery, functions_v2, storage import pandas import pytest import test_utils.prefixer import bigframes -from bigframes.functions.remote_function import get_cloud_function_name +import bigframes.functions.remote_function as bigframes_rf +import bigframes.pandas as bpd import bigframes.series from tests.system.utils import ( assert_pandas_df_equal, @@ -590,7 +591,11 @@ def add_one(x): add_one_uniq, add_one_uniq_dir = make_uniq_udf(add_one) # Expected cloud function name for the unique udf - add_one_uniq_cf_name, _ = get_cloud_function_name(add_one_uniq) + package_requirements = bigframes_rf._get_updated_package_requirements() + add_one_uniq_hash = bigframes_rf._get_hash(add_one_uniq, package_requirements) + add_one_uniq_cf_name = bigframes_rf.get_cloud_function_name( + add_one_uniq_hash, session.session_id + ) # There should be no cloud function yet for the unique udf cloud_functions = list( @@ -1860,3 +1865,142 @@ def test_remote_function_gcf_memory_unsupported(session, memory_mib): @session.remote_function(reuse=False, cloud_function_memory_mib=memory_mib) def square(x: int) -> int: return x * x + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_unnamed_removed_w_session_cleanup(): + # create a clean session + session = bigframes.connect() + + # create an unnamed remote function in the session + @session.remote_function(reuse=False) + def foo(x: int) -> int: + return x + 1 + + # ensure that remote function artifacts are created + assert foo.bigframes_remote_function is not None + session.bqclient.get_routine(foo.bigframes_remote_function) is not None + assert foo.bigframes_cloud_function is not None + session.cloudfunctionsclient.get_function( + name=foo.bigframes_cloud_function + ) is not None + + # explicitly close the session + session.close() + + # ensure that the bq remote function is deleted + with pytest.raises(google.cloud.exceptions.NotFound): + session.bqclient.get_routine(foo.bigframes_remote_function) + + # the deletion of cloud function happens in a non-blocking way, ensure that + # it either exists in a being-deleted state, or is already deleted + try: + gcf = session.cloudfunctionsclient.get_function( + name=foo.bigframes_cloud_function + ) + assert gcf.state is functions_v2.Function.State.DELETING + except google.cloud.exceptions.NotFound: + pass + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_named_perists_w_session_cleanup(): + try: + # create a clean session + session = bigframes.connect() + + # create a name for the remote function + name = test_utils.prefixer.Prefixer("bigframes", "").create_prefix() + + # create an unnamed remote function in the session + @session.remote_function(name=name) + def foo(x: int) -> int: + return x + 1 + + # ensure that remote function artifacts are created + assert foo.bigframes_remote_function is not None + session.bqclient.get_routine(foo.bigframes_remote_function) is not None + assert foo.bigframes_cloud_function is not None + session.cloudfunctionsclient.get_function( + name=foo.bigframes_cloud_function + ) is not None + + # explicitly close the session + session.close() + + # ensure that the bq remote function still exists + session.bqclient.get_routine(foo.bigframes_remote_function) is not None + + # the deletion of cloud function happens in a non-blocking way, ensure + # that it was not deleted and still exists in active state + gcf = session.cloudfunctionsclient.get_function( + name=foo.bigframes_cloud_function + ) + assert gcf.state is functions_v2.Function.State.ACTIVE + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, foo + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_clean_up_by_session_id(): + # Use a brand new session to avoid conflict with other tests + session = bigframes.Session() + session_id = session.session_id + try: + # we will create remote functions, one with explicit name and another + # without it, and later confirm that the former is deleted when the session + # is cleaned up by session id, but the latter remains + ## unnamed + @session.remote_function(reuse=False) + def foo_unnamed(x: int) -> int: + return x + 1 + + ## named + rf_name = test_utils.prefixer.Prefixer("bigframes", "").create_prefix() + + @session.remote_function(reuse=False, name=rf_name) + def foo_named(x: int) -> int: + return x + 2 + + # check that BQ remote functiosn were created with corresponding cloud + # functions + for foo in [foo_unnamed, foo_named]: + assert foo.bigframes_remote_function is not None + session.bqclient.get_routine(foo.bigframes_remote_function) is not None + assert foo.bigframes_cloud_function is not None + session.cloudfunctionsclient.get_function( + name=foo.bigframes_cloud_function + ) is not None + + # clean up using explicit session id + bpd.clean_up_by_session_id( + session_id, location=session._location, project=session._project + ) + + # ensure that the unnamed bq remote function is deleted along with its + # corresponding cloud function + with pytest.raises(google.cloud.exceptions.NotFound): + session.bqclient.get_routine(foo_unnamed.bigframes_remote_function) + try: + gcf = session.cloudfunctionsclient.get_function( + name=foo_unnamed.bigframes_cloud_function + ) + assert gcf.state is functions_v2.Function.State.DELETING + except google.cloud.exceptions.NotFound: + pass + + # ensure that the named bq remote function still exists along with its + # corresponding cloud function + session.bqclient.get_routine(foo_named.bigframes_remote_function) is not None + gcf = session.cloudfunctionsclient.get_function( + name=foo_named.bigframes_cloud_function + ) + assert gcf.state is functions_v2.Function.State.ACTIVE + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, foo_named + ) diff --git a/tests/system/large/test_session.py b/tests/system/large/test_session.py index c7a19dc26eb..2b82d0133ba 100644 --- a/tests/system/large/test_session.py +++ b/tests/system/large/test_session.py @@ -19,6 +19,7 @@ import bigframes import bigframes.pandas as bpd +import bigframes.session._io.bigquery @pytest.mark.parametrize( @@ -93,8 +94,7 @@ def test_clean_up_by_session_id(): session_id = session.session_id # we will create two tables and confirm that they are deleted - # when the session is closed by id - + # when the session is cleaned up by id bqclient = session.bqclient dataset = session._anonymous_dataset expiration = ( @@ -110,9 +110,7 @@ def test_clean_up_by_session_id(): max_results=bigframes.session._io.bigquery._LIST_TABLES_LIMIT, page_size=bigframes.session._io.bigquery._LIST_TABLES_LIMIT, ) - assert any( - [(session.session_id in table.full_table_id) for table in list(tables_before)] - ) + assert any([(session.session_id in table.full_table_id) for table in tables_before]) bpd.clean_up_by_session_id( session_id, location=session._location, project=session._project @@ -125,5 +123,5 @@ def test_clean_up_by_session_id(): page_size=bigframes.session._io.bigquery._LIST_TABLES_LIMIT, ) assert not any( - [(session.session_id in table.full_table_id) for table in list(tables_after)] + [(session.session_id in table.full_table_id) for table in tables_after] ) From ab0dabcf83a1b25de3afc0e950d12d7c55ca0af4 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 16 Jul 2024 10:33:48 -0700 Subject: [PATCH 708/921] test: Fix json_set error tests to create resources in test body (#845) --- tests/system/small/bigquery/test_json.py | 41 ++++++++++-------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/tests/system/small/bigquery/test_json.py b/tests/system/small/bigquery/test_json.py index ff759b8fda8..9e0c06e0bdb 100644 --- a/tests/system/small/bigquery/test_json.py +++ b/tests/system/small/bigquery/test_json.py @@ -85,18 +85,18 @@ def test_json_set_w_more_pairs(): ) -@pytest.mark.parametrize( - ("series", "json_path_value_pairs"), - [ - pytest.param( - _get_series_from_json([{"a": 10}]), - [("$.a", 1, 100)], - id="invalid_json_path_value_pairs", - marks=pytest.mark.xfail(raises=ValueError), - ), - pytest.param( +def test_json_set_w_invalid_json_path_value_pairs(): + with pytest.raises(ValueError): + bbq.json_set( + _get_series_from_json([{"a": 10}]), json_path_value_pairs=[("$.a", 1, 100)] # type: ignore + ) + + +def test_json_set_w_invalid_value_type(): + with pytest.raises(TypeError): + bbq.json_set( _get_series_from_json([{"a": 10}]), - [ + json_path_value_pairs=[ ( "$.a", bpd.read_pandas( @@ -104,16 +104,9 @@ def test_json_set_w_more_pairs(): ), ) ], - id="invalid_json_value_type", - marks=pytest.mark.xfail(raises=TypeError), - ), - pytest.param( - bpd.Series([1, 2]), - [("$.a", 1)], - id="invalid_series_type", - marks=pytest.mark.xfail(raises=TypeError), - ), - ], -) -def test_json_set_w_invalid(series, json_path_value_pairs): - bbq.json_set(series, json_path_value_pairs=json_path_value_pairs) + ) + + +def test_json_set_w_invalid_series_type(): + with pytest.raises(TypeError): + bbq.json_set(bpd.Series([1, 2]), json_path_value_pairs=[("$.a", 1)]) From 6278e0b8fbdd3e6ff9bd008128dccf24c37d65c6 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 16 Jul 2024 12:25:40 -0700 Subject: [PATCH 709/921] test: Run several tests in unordered mode as well as ordered (#831) --- tests/system/conftest.py | 21 ++++++++ tests/system/small/test_dataframe.py | 77 +++++++++++++--------------- tests/system/utils.py | 18 +++++++ 3 files changed, 76 insertions(+), 40 deletions(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index df4ff9aff0e..59439c306f7 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -139,6 +139,16 @@ def session() -> Generator[bigframes.Session, None, None]: session.close() # close generated session at cleanup time +@pytest.fixture(scope="session", params=["ordered", "unordered"]) +def maybe_ordered_session(request) -> Generator[bigframes.Session, None, None]: + context = bigframes.BigQueryOptions( + location="US", _strictly_ordered=request.param == "ordered" + ) + session = bigframes.Session(context=context) + yield session + session.close() # close generated session at cleanup type + + @pytest.fixture(scope="session") def unordered_session() -> Generator[bigframes.Session, None, None]: context = bigframes.BigQueryOptions(location="US", _strictly_ordered=False) @@ -467,6 +477,17 @@ def scalars_dfs( return scalars_df_index, scalars_pandas_df_index +@pytest.fixture(scope="session") +def scalars_dfs_maybe_ordered( + maybe_ordered_session, + scalars_pandas_df_index, +): + return ( + maybe_ordered_session.read_pandas(scalars_pandas_df_index), + scalars_pandas_df_index, + ) + + @pytest.fixture(scope="session") def hockey_df( hockey_table_id: str, session: bigframes.Session diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 7273e2079ff..67792b3a1d5 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -33,8 +33,10 @@ import bigframes.pandas as bpd import bigframes.series as series from tests.system.utils import ( + assert_dfs_equivalent, assert_pandas_df_equal, assert_series_equal, + assert_series_equivalent, skip_legacy_pandas, ) @@ -75,7 +77,7 @@ def test_df_construct_large_strings(): pandas.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) -def test_df_construct_pandas_load_job(scalars_dfs): +def test_df_construct_pandas_load_job(scalars_dfs_maybe_ordered): # This should trigger the inlined codepath columns = [ "int64_too", @@ -91,10 +93,10 @@ def test_df_construct_pandas_load_job(scalars_dfs): "timestamp_col", "geography_col", ] - _, scalars_pandas_df = scalars_dfs - bf_result = dataframe.DataFrame(scalars_pandas_df, columns=columns).to_pandas() + _, scalars_pandas_df = scalars_dfs_maybe_ordered + bf_result = dataframe.DataFrame(scalars_pandas_df, columns=columns) pd_result = pd.DataFrame(scalars_pandas_df, columns=columns) - pandas.testing.assert_frame_equal(bf_result, pd_result) + assert_dfs_equivalent(pd_result, bf_result) def test_df_construct_pandas_set_dtype(scalars_dfs): @@ -112,17 +114,17 @@ def test_df_construct_pandas_set_dtype(scalars_dfs): pandas.testing.assert_frame_equal(bf_result, pd_result) -def test_df_construct_from_series(scalars_dfs): - scalars_df, scalars_pandas_df = scalars_dfs +def test_df_construct_from_series(scalars_dfs_maybe_ordered): + scalars_df, scalars_pandas_df = scalars_dfs_maybe_ordered bf_result = dataframe.DataFrame( {"a": scalars_df["int64_col"], "b": scalars_df["string_col"]}, dtype="string[pyarrow]", - ).to_pandas() + ) pd_result = pd.DataFrame( {"a": scalars_pandas_df["int64_col"], "b": scalars_pandas_df["string_col"]}, dtype="string[pyarrow]", ) - pandas.testing.assert_frame_equal(bf_result, pd_result) + assert_dfs_equivalent(pd_result, bf_result) def test_df_construct_from_dict(): @@ -505,8 +507,8 @@ def test_rename(scalars_dfs): ) -def test_df_peek(scalars_dfs): - scalars_df, scalars_pandas_df = scalars_dfs +def test_df_peek(scalars_dfs_maybe_ordered): + scalars_df, scalars_pandas_df = scalars_dfs_maybe_ordered peek_result = scalars_df.peek(n=3, force=False) pd.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) assert len(peek_result) == 3 @@ -1709,14 +1711,14 @@ def test_sort_index(scalars_dfs, ascending, na_position): pandas.testing.assert_frame_equal(bf_result, pd_result) -def test_df_abs(scalars_dfs): - scalars_df, scalars_pandas_df = scalars_dfs +def test_df_abs(scalars_dfs_maybe_ordered): + scalars_df, scalars_pandas_df = scalars_dfs_maybe_ordered columns = ["int64_col", "int64_too", "float64_col"] - bf_result = scalars_df[columns].abs().to_pandas() + bf_result = scalars_df[columns].abs() pd_result = scalars_pandas_df[columns].abs() - assert_pandas_df_equal(bf_result, pd_result) + assert_dfs_equivalent(pd_result, bf_result) def test_df_pos(scalars_dfs): @@ -2268,8 +2270,10 @@ def test_series_binop_add_different_table( @all_joins -def test_join_same_table(scalars_dfs, how): - bf_df, pd_df = scalars_dfs +def test_join_same_table(scalars_dfs_maybe_ordered, how): + bf_df, pd_df = scalars_dfs_maybe_ordered + if not bf_df._session._strictly_ordered and how == "cross": + pytest.skip("Cross join not supported in unordered mode.") bf_df_a = bf_df.set_index("int64_too")[["string_col", "int64_col"]] bf_df_a = bf_df_a.sort_index() @@ -2503,7 +2507,7 @@ def test_dataframe_agg_int_single_string(scalars_dfs, agg): ) -def test_dataframe_agg_multi_string(scalars_dfs): +def test_dataframe_agg_multi_string(scalars_dfs_maybe_ordered): numeric_cols = ["int64_col", "int64_too", "float64_col"] aggregations = [ "sum", @@ -2516,8 +2520,8 @@ def test_dataframe_agg_multi_string(scalars_dfs): "nunique", "count", ] - scalars_df, scalars_pandas_df = scalars_dfs - bf_result = scalars_df[numeric_cols].agg(aggregations).to_pandas() + scalars_df, scalars_pandas_df = scalars_dfs_maybe_ordered + bf_result = scalars_df[numeric_cols].agg(aggregations) pd_result = scalars_pandas_df[numeric_cols].agg(aggregations) # Pandas may produce narrower numeric types, but bigframes always produces Float64 @@ -2528,7 +2532,7 @@ def test_dataframe_agg_multi_string(scalars_dfs): bf_result = bf_result.drop(labels=["median"]) pd_result = pd_result.drop(labels=["median"]) - pd.testing.assert_frame_equal(pd_result, bf_result, check_index_type=False) + assert_dfs_equivalent(pd_result, bf_result, check_index_type=False) # Double-check that median is at least plausible. assert ( @@ -3205,13 +3209,6 @@ def test_dataframe_aggregate_bool(scalars_df_index, scalars_pandas_df_index, col assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) -@pytest.mark.parametrize( - ("ordered"), - [ - (True), - (False), - ], -) @pytest.mark.parametrize( ("op", "bf_dtype"), [ @@ -3226,12 +3223,11 @@ def test_dataframe_aggregate_bool(scalars_df_index, scalars_pandas_df_index, col ], ids=["sum", "mean", "min", "max", "std", "var", "count", "nunique"], ) -def test_dataframe_aggregates( - scalars_df_index, scalars_pandas_df_index, op, bf_dtype, ordered -): +def test_dataframe_aggregates(scalars_dfs_maybe_ordered, op, bf_dtype): + scalars_df_index, scalars_pandas_df_index = scalars_dfs_maybe_ordered col_names = ["int64_too", "float64_col", "string_col", "int64_col", "bool_col"] bf_series = op(scalars_df_index[col_names]) - bf_result = bf_series.to_pandas(ordered=ordered) + bf_result = bf_series pd_result = op(scalars_pandas_df_index[col_names]) # Check dtype separately @@ -3240,12 +3236,11 @@ def test_dataframe_aggregates( # Pandas may produce narrower numeric types, but bigframes always produces Float64 # Pandas has object index type pd_result.index = pd_result.index.astype("string[pyarrow]") - assert_series_equal( + assert_series_equivalent( pd_result, bf_result, check_dtype=False, check_index_type=False, - ignore_order=not ordered, ) @@ -3597,16 +3592,17 @@ def test_df_rows_filter_regex(scalars_df_index, scalars_pandas_df_index): ) -def test_df_reindex_rows_list(scalars_df_index, scalars_pandas_df_index): - bf_result = scalars_df_index.reindex(index=[5, 1, 3, 99, 1]).to_pandas() +def test_df_reindex_rows_list(scalars_dfs_maybe_ordered): + scalars_df_index, scalars_pandas_df_index = scalars_dfs_maybe_ordered + bf_result = scalars_df_index.reindex(index=[5, 1, 3, 99, 1]) pd_result = scalars_pandas_df_index.reindex(index=[5, 1, 3, 99, 1]) # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - pd.testing.assert_frame_equal( - bf_result, + assert_dfs_equivalent( pd_result, + bf_result, ) @@ -3861,7 +3857,8 @@ def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): ) -def test_loc_list_multiindex(scalars_df_index, scalars_pandas_df_index): +def test_loc_list_multiindex(scalars_dfs_maybe_ordered): + scalars_df_index, scalars_pandas_df_index = scalars_dfs_maybe_ordered scalars_df_multiindex = scalars_df_index.set_index(["string_col", "int64_col"]) scalars_pandas_df_multiindex = scalars_pandas_df_index.set_index( ["string_col", "int64_col"] @@ -3871,9 +3868,9 @@ def test_loc_list_multiindex(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_multiindex.loc[index_list] pd_result = scalars_pandas_df_multiindex.loc[index_list] - pd.testing.assert_frame_equal( - bf_result.to_pandas(), + assert_dfs_equivalent( pd_result, + bf_result, ) diff --git a/tests/system/utils.py b/tests/system/utils.py index ab4c2c119fa..9fbf191a3a2 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -27,6 +27,7 @@ import pytest from bigframes.functions import remote_function +import bigframes.pandas ML_REGRESSION_METRICS = [ "mean_absolute_error", @@ -56,6 +57,23 @@ def wrapper(*args, **kwds): return wrapper +# Prefer this function for tests that run in both ordered and unordered mode +def assert_dfs_equivalent( + pd_df: pd.DataFrame, bf_df: bigframes.pandas.DataFrame, **kwargs +): + bf_df_local = bf_df.to_pandas() + ignore_order = not bf_df._session._strictly_ordered + assert_pandas_df_equal(bf_df_local, pd_df, ignore_order=ignore_order, **kwargs) + + +def assert_series_equivalent( + pd_series: pd.Series, bf_series: bigframes.pandas.Series, **kwargs +): + bf_df_local = bf_series.to_pandas() + ignore_order = not bf_series._session._strictly_ordered + assert_series_equal(bf_df_local, pd_series, ignore_order=ignore_order, **kwargs) + + def assert_pandas_df_equal(df0, df1, ignore_order: bool = False, **kwargs): if ignore_order: # Sort by a column to get consistent results. From c9eaff0a1a0731b28f4c67bca5606db12a47c8c0 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 16 Jul 2024 12:26:41 -0700 Subject: [PATCH 710/921] feat: Add bigframes-mode label to query jobs (#832) --- bigframes/session/__init__.py | 2 ++ tests/system/small/test_unordered.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 10c0797873f..7cc862b93d7 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1849,6 +1849,8 @@ def _start_query( Starts BigQuery query job and waits for results. """ job_config = self._prepare_query_job_config(job_config) + if not self._strictly_ordered: + job_config.labels = {"bigframes-mode": "unordered"} try: return bigframes.session._io.bigquery.start_query_with_client( self, diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 4448ddc8387..7220d34d554 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -20,6 +20,15 @@ from tests.system.utils import assert_pandas_df_equal, skip_legacy_pandas +def test_unordered_mode_job_label(unordered_session): + pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session) + df.to_pandas() + job_labels = df.query_job.labels # type:ignore + assert "bigframes-mode" in job_labels + assert job_labels["bigframes-mode"] == "unordered" + + def test_unordered_mode_cache_aggregate(unordered_session): pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) df = bpd.DataFrame(pd_df, session=unordered_session) From ff2faedb133ce5f1059accbfd3351089b19895d1 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:46:55 -0700 Subject: [PATCH 711/921] test: disable unavailable BQML remote vertex model tests (#848) --- noxfile.py | 3 ++- tests/system/small/ml/test_core.py | 1 + tests/system/small/ml/test_remote.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index b38bcacfdbe..d69c16e69cc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -429,7 +429,8 @@ def cover(session): "--show-missing", "--include=tests/unit/*", "--include=tests/system/small/*", - "--fail-under=100", + # TODO(b/353775058) resume coverage to 100 when the issue is fixed. + "--fail-under=99", ) session.run("coverage", "erase") diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index c505057d7b5..95719ea0db3 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -312,6 +312,7 @@ def test_model_detect_anomalies( ) +@pytest.mark.skip("b/353775058 BQML internal error") def test_remote_model_predict( bqml_linear_remote_model: core.BqmlModel, new_penguins_df ): diff --git a/tests/system/small/ml/test_remote.py b/tests/system/small/ml/test_remote.py index 5036cdadfca..c52c4522448 100644 --- a/tests/system/small/ml/test_remote.py +++ b/tests/system/small/ml/test_remote.py @@ -13,10 +13,12 @@ # limitations under the License. import pandas as pd +import pytest from bigframes.ml import remote +@pytest.mark.skip("b/353775058 BQML internal error") def test_remote_linear_vertex_model_predict( linear_remote_vertex_model: remote.VertexAIModel, new_penguins_df ): From 83f254ab4a7f089e0a71e4a05a457319ab8b1df1 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 18 Jul 2024 11:04:31 -0700 Subject: [PATCH 712/921] refactor: Allow unambiguous windows even in unstrictly ordered sessions (#849) Co-authored-by: Huan Chen <142538604+Genesis929@users.noreply.github.com> --- bigframes/core/__init__.py | 4 ++-- bigframes/core/nodes.py | 36 ++++++++++++++++++++++++++++ tests/system/small/test_unordered.py | 7 ++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index cfe8f29327f..bce0f059eb3 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -194,7 +194,7 @@ def promote_offsets(self, col_id: str) -> ArrayValue: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. """ - if not self.session._strictly_ordered: + if self.node.order_ambiguous and not self.session._strictly_ordered: raise ValueError("Generating offsets not supported in unordered mode") return ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)) @@ -346,7 +346,7 @@ def project_window_op( """ # TODO: Support non-deterministic windowing if window_spec.row_bounded or not op.order_independent: - if not self.session._strictly_ordered: + if self.node.order_ambiguous and not self.session._strictly_ordered: raise ValueError( "Order-dependent windowed ops not supported in unordered mode" ) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index aadab9f5cc7..a979e079728 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -127,6 +127,14 @@ def joins(self) -> bool: """ return False + @property + @abc.abstractmethod + def order_ambiguous(self) -> bool: + """ + Whether row ordering is potentially ambiguous. For example, ReadTable (without a primary key) could be ordered in different ways. + """ + ... + @functools.cached_property def total_variables(self) -> int: return self.variables_introduced + sum( @@ -177,6 +185,10 @@ def transform_children( ) -> BigFrameNode: return replace(self, child=t(self.child)) + @property + def order_ambiguous(self) -> bool: + return self.child.order_ambiguous + @dataclass(frozen=True) class JoinNode(BigFrameNode): @@ -196,6 +208,10 @@ def non_local(self) -> bool: def child_nodes(self) -> typing.Sequence[BigFrameNode]: return (self.left_child, self.right_child) + @property + def order_ambiguous(self) -> bool: + return True + def __hash__(self): return self._node_hash @@ -247,6 +263,10 @@ def __post_init__(self): def child_nodes(self) -> typing.Sequence[BigFrameNode]: return self.children + @property + def order_ambiguous(self) -> bool: + return any(child.order_ambiguous for child in self.children) + def __hash__(self): return self._node_hash @@ -293,6 +313,10 @@ def variables_introduced(self) -> int: """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" return len(self.schema.items) + 1 + @property + def order_ambiguous(self) -> bool: + return False + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -350,6 +374,10 @@ def relation_ops_created(self) -> int: # Assume worst case, where readgbq actually has baked in analytic operation to generate index return 3 + @property + def order_ambiguous(self) -> bool: + return len(self.total_order_cols) == 0 + @functools.cached_property def variables_introduced(self) -> int: return len(self.schema.items) + 1 @@ -417,6 +445,10 @@ def hidden_columns(self) -> typing.Tuple[str, ...]: if col not in self.schema.names ) + @property + def order_ambiguous(self) -> bool: + return not isinstance(self.ordering, orderings.TotalOrdering) + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -600,6 +632,10 @@ def schema(self) -> schemata.ArraySchema: def variables_introduced(self) -> int: return len(self.aggregations) + len(self.by_column_ids) + @property + def order_ambiguous(self) -> bool: + return False + @dataclass(frozen=True) class WindowOpNode(UnaryNode): diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 7220d34d554..b1c836e1c09 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -40,6 +40,13 @@ def test_unordered_mode_cache_aggregate(unordered_session): assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) +def test_unordered_mode_single_aggregate(unordered_session): + pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) + bf_df = bpd.DataFrame(pd_df, session=unordered_session) + + assert bf_df.a.mean() == pd_df.a.mean() + + def test_unordered_mode_print(unordered_session): pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) df = bpd.DataFrame(pd_df, session=unordered_session).cache() From 827007c4590158fb83cf0bf04696cf6f4fde8f65 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:40:15 -0700 Subject: [PATCH 713/921] test: re-enable gemini tuning load test (#846) * test: re-enable gemini tuning load test * remove commented out --- tests/system/load/test_llm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index 9b8868bb27e..6d22963a97a 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -62,7 +62,6 @@ def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_ # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept -@pytest.mark.skip(reason="b/351905648. Credential error to be fixed.") @pytest.mark.flaky(retries=2) def test_llm_gemini_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): model = llm.GeminiTextGenerator(model_name="gemini-pro", max_iterations=1) From 33464947aca81d830183bc0652b15b1e73382e69 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 18 Jul 2024 13:35:10 -0700 Subject: [PATCH 714/921] refactor: Unordered mode supports user partial orders (#842) --- bigframes/core/__init__.py | 2 +- bigframes/core/blocks.py | 40 +++++++++++----- bigframes/core/compile/__init__.py | 10 +--- bigframes/core/compile/api.py | 70 +++++++++++++++------------- bigframes/core/compile/compiler.py | 3 +- bigframes/core/indexes/base.py | 4 +- bigframes/dataframe.py | 24 ++++------ bigframes/series.py | 11 ++--- bigframes/session/__init__.py | 17 +++---- tests/system/small/test_unordered.py | 14 ++++++ 10 files changed, 111 insertions(+), 84 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index bce0f059eb3..aa66129572c 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -147,7 +147,7 @@ def _compiled_schema(self) -> schemata.ArraySchema: def as_cached( self: ArrayValue, cache_table: google.cloud.bigquery.Table, - ordering: Optional[orderings.TotalOrdering], + ordering: Optional[orderings.RowOrdering], ) -> ArrayValue: """ Replace the node with an equivalent one that references a tabel where the value has been materialized to. diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index c2bf20076ac..2d7c5436782 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -498,9 +498,33 @@ def to_pandas( sampling_method: Optional[str] = None, random_state: Optional[int] = None, *, - ordered: Optional[bool] = None, + ordered: bool = True, ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: - """Run query and download results as a pandas DataFrame.""" + """Run query and download results as a pandas DataFrame. + + Args: + max_download_size (int, default None): + Download size threshold in MB. If max_download_size is exceeded when downloading data + (e.g., to_pandas()), the data will be downsampled if + bigframes.options.sampling.enable_downsampling is True, otherwise, an error will be + raised. If set to a value other than None, this will supersede the global config. + sampling_method (str, default None): + Downsampling algorithms to be chosen from, the choices are: "head": This algorithm + returns a portion of the data from the beginning. It is fast and requires minimal + computations to perform the downsampling; "uniform": This algorithm returns uniform + random samples of the data. If set to a value other than None, this will supersede + the global config. + random_state (int, default None): + The seed for the uniform downsampling algorithm. If provided, the uniform method may + take longer to execute and require more computation. If set to a value other than + None, this will supersede the global config. + ordered (bool, default True): + Determines whether the resulting pandas dataframe will be ordered. + Whether the row ordering is deterministics depends on whether session ordering is strict. + + 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, " @@ -517,10 +541,7 @@ def to_pandas( df, query_job = self._materialize_local( materialize_options=MaterializationOptions( - downsampling=sampling, - ordered=ordered - if ordered is not None - else self.session._strictly_ordered, + downsampling=sampling, ordered=ordered ) ) df.set_axis(self.column_labels, axis=1, copy=False) @@ -547,7 +568,7 @@ def to_pandas_batches( dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) _, query_job = self.session._query_to_destination( - self.session._to_sql(self.expr, ordered=self.session._strictly_ordered), + self.session._to_sql(self.expr, ordered=True), list(self.index_columns), api_name="cached", do_clustering=False, @@ -2593,10 +2614,7 @@ def to_pandas(self, *, ordered: Optional[bool] = None) -> pd.Index: index_columns = list(self._block.index_columns) expr = self._expr.select_columns(index_columns) results, _ = self.session._execute( - expr, - ordered=ordered - if (ordered is not None) - else self.session._strictly_ordered, + expr, ordered=ordered if ordered is not None else True ) df = expr.session._rows_to_dataframe(results) df = df.set_index(index_columns) diff --git a/bigframes/core/compile/__init__.py b/bigframes/core/compile/__init__.py index 4c105ed03bf..964113bd7b8 100644 --- a/bigframes/core/compile/__init__.py +++ b/bigframes/core/compile/__init__.py @@ -14,19 +14,13 @@ from __future__ import annotations from bigframes.core.compile.api import ( - compile_ordered, - compile_peek, - compile_raw, - compile_unordered, + SQLCompiler, test_only_ibis_inferred_schema, test_only_try_evaluate, ) __all__ = [ - "compile_peek", - "compile_unordered", - "compile_ordered", - "compile_raw", + "SQLCompiler", "test_only_try_evaluate", "test_only_ibis_inferred_schema", ] diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index 4e76d42bef4..468c5522d96 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -25,38 +25,44 @@ _STRICT_COMPILER = compiler.Compiler(strict=True) -def compile_peek(node: bigframes.core.nodes.BigFrameNode, n_rows: int) -> str: - """Compile node into sql that selects N arbitrary rows, may not execute deterministically.""" - return _STRICT_COMPILER.compile_unordered_ir(node).peek_sql(n_rows) - - -def compile_unordered( - node: bigframes.core.nodes.BigFrameNode, *, col_id_overrides: Mapping[str, str] = {} -) -> str: - """Compile node into sql where rows are unsorted, and no ordering information is preserved.""" - return _STRICT_COMPILER.compile_unordered_ir(node).to_sql( - col_id_overrides=col_id_overrides - ) - - -def compile_ordered( - node: bigframes.core.nodes.BigFrameNode, *, col_id_overrides: Mapping[str, str] = {} -) -> str: - """Compile node into sql where rows are sorted with ORDER BY.""" - return _STRICT_COMPILER.compile_ordered_ir(node).to_sql( - col_id_overrides=col_id_overrides, ordered=True - ) - - -def compile_raw( - node: bigframes.core.nodes.BigFrameNode, -) -> Tuple[str, bigframes.core.ordering.TotalOrdering]: - """Compile node into sql that exposes all columns, including hidden ordering-only columns.""" - ir = _STRICT_COMPILER.compile_ordered_ir(node) - sql = ir.raw_sql() - ordering_info = ir._ordering - assert ir.has_total_order - return sql, ordering_info # type: ignore +class SQLCompiler: + def __init__(self, strict: bool = True): + self._compiler = compiler.Compiler(strict=strict) + + def compile_peek(self, node: bigframes.core.nodes.BigFrameNode, n_rows: int) -> str: + """Compile node into sql that selects N arbitrary rows, may not execute deterministically.""" + return self._compiler.compile_unordered_ir(node).peek_sql(n_rows) + + def compile_unordered( + self, + node: bigframes.core.nodes.BigFrameNode, + *, + col_id_overrides: Mapping[str, str] = {}, + ) -> str: + """Compile node into sql where rows are unsorted, and no ordering information is preserved.""" + return self._compiler.compile_unordered_ir(node).to_sql( + col_id_overrides=col_id_overrides + ) + + def compile_ordered( + self, + node: bigframes.core.nodes.BigFrameNode, + *, + col_id_overrides: Mapping[str, str] = {}, + ) -> str: + """Compile node into sql where rows are sorted with ORDER BY.""" + return self._compiler.compile_ordered_ir(node).to_sql( + col_id_overrides=col_id_overrides, ordered=True + ) + + def compile_raw( + self, + node: bigframes.core.nodes.BigFrameNode, + ) -> Tuple[str, bigframes.core.ordering.RowOrdering]: + """Compile node into sql that exposes all columns, including hidden ordering-only columns.""" + ir = self._compiler.compile_ordered_ir(node) + sql = ir.raw_sql() + return sql, ir._ordering def test_only_try_evaluate(node: bigframes.core.nodes.BigFrameNode): diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index da74ffeb8f9..c7f8c5ab596 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -46,7 +46,8 @@ class Compiler: def compile_ordered_ir(self, node: nodes.BigFrameNode) -> compiled.OrderedIR: ir = typing.cast(compiled.OrderedIR, self.compile_node(node, True)) - assert ir.has_total_order + if self.strict: + assert ir.has_total_order return ir def compile_unordered_ir(self, node: nodes.BigFrameNode) -> compiled.UnorderedIR: diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 696742180bd..8b039707c21 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -492,9 +492,7 @@ def to_pandas(self) -> pandas.Index: pandas.Index: A pandas Index with all of the labels from this Index. """ - return self._block.index.to_pandas( - ordered=self._block.session._strictly_ordered - ) + return self._block.index.to_pandas(ordered=True) def to_numpy(self, dtype=None, **kwargs) -> np.ndarray: return self.to_pandas().to_numpy(dtype, **kwargs) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 4dcc4414eda..dcb2fd09cb3 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1192,15 +1192,14 @@ def cov(self, *, numeric_only: bool = False) -> DataFrame: def to_arrow( self, *, - ordered: Optional[bool] = None, + ordered: bool = True, ) -> pyarrow.Table: """Write DataFrame to an Arrow table / record batch. Args: - ordered (bool, default None): - Determines whether the resulting Arrow table will be deterministically ordered. - In some cases, unordered may result in a faster-executing query. If set to a value - other than None, will override Session default. + ordered (bool, default True): + Determines whether the resulting Arrow table will be ordered. + In some cases, unordered may result in a faster-executing query. Returns: pyarrow.Table: A pyarrow Table with all rows and columns of this DataFrame. @@ -1211,9 +1210,7 @@ def to_arrow( ) self._optimize_query_complexity() - pa_table, query_job = self._block.to_arrow( - ordered=ordered if ordered is not None else self._session._strictly_ordered, - ) + pa_table, query_job = self._block.to_arrow(ordered=ordered) self._set_internal_query_job(query_job) return pa_table @@ -1223,7 +1220,7 @@ def to_pandas( sampling_method: Optional[str] = None, random_state: Optional[int] = None, *, - ordered: Optional[bool] = None, + ordered: bool = True, ) -> pandas.DataFrame: """Write DataFrame to pandas DataFrame. @@ -1243,10 +1240,9 @@ def to_pandas( The seed for the uniform downsampling algorithm. If provided, the uniform method may take longer to execute and require more computation. If set to a value other than None, this will supersede the global config. - ordered (bool, default None): - Determines whether the resulting pandas dataframe will be deterministically ordered. - In some cases, unordered may result in a faster-executing query. If set to a value - other than None, will override Session default. + ordered (bool, default True): + Determines whether the resulting pandas dataframe will be ordered. + In some cases, unordered may result in a faster-executing query. Returns: pandas.DataFrame: A pandas DataFrame with all rows and columns of this DataFrame if the @@ -1259,7 +1255,7 @@ def to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, - ordered=ordered if ordered is not None else self._session._strictly_ordered, + ordered=ordered, ) self._set_internal_query_job(query_job) return df.set_axis(self._block.column_labels, axis=1, copy=False) diff --git a/bigframes/series.py b/bigframes/series.py index c325783e965..8fdafe25e7e 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -329,7 +329,7 @@ def to_pandas( sampling_method: Optional[str] = None, random_state: Optional[int] = None, *, - ordered: Optional[bool] = None, + ordered: bool = True, ) -> pandas.Series: """Writes Series to pandas Series. @@ -349,10 +349,9 @@ def to_pandas( The seed for the uniform downsampling algorithm. If provided, the uniform method may take longer to execute and require more computation. If set to a value other than None, this will supersede the global config. - ordered (bool, default None): - Determines whether the resulting pandas series will be deterministically ordered. - In some cases, unordered may result in a faster-executing query. If set to a value - other than None, will override Session default. + ordered (bool, default True): + Determines whether the resulting pandas series will be ordered. + In some cases, unordered may result in a faster-executing query. Returns: @@ -364,7 +363,7 @@ def to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, random_state=random_state, - ordered=ordered if ordered is not None else self._session._strictly_ordered, + ordered=ordered, ) self._set_internal_query_job(query_job) series = df.squeeze(axis=1) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 7cc862b93d7..ca242d269c4 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -304,6 +304,9 @@ def __init__( if context._strictly_ordered else bigframes.enums.DefaultIndexKind.NULL ) + self._compiler = bigframes.core.compile.SQLCompiler( + strict=context._strictly_ordered + ) self._remote_function_session = bigframes_rf._RemoteFunctionSession() @@ -1893,10 +1896,8 @@ def _cache_with_cluster_cols( """Executes the query and uses the resulting table to rewrite future executions.""" # TODO: Use this for all executions? Problem is that caching materializes extra # ordering columns - # TODO: May want to support some partial ordering info even for non-strict ordering mode - keep_order_info = self._strictly_ordered - sql, ordering_info = bigframes.core.compile.compile_raw( + sql, ordering_info = self._compiler.compile_raw( self._with_cached_executions(array_value.node) ) tmp_table = self._sql_to_temp_table( @@ -1904,7 +1905,7 @@ def _cache_with_cluster_cols( ) cached_replacement = array_value.as_cached( cache_table=self.bqclient.get_table(tmp_table), - ordering=ordering_info if keep_order_info else None, + ordering=ordering_info, ).node self._cached_executions[array_value.node] = cached_replacement @@ -1917,7 +1918,7 @@ def _cache_with_offsets(self, array_value: core.ArrayValue): "Caching with offsets only supported in strictly ordered mode." ) offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") - sql = bigframes.core.compile.compile_unordered( + sql = self._compiler.compile_unordered( self._with_cached_executions( array_value.promote_offsets(offset_column).node ) @@ -2023,7 +2024,7 @@ def _peek( """A 'peek' efficiently accesses a small number of rows in the dataframe.""" if not tree_properties.peekable(self._with_cached_executions(array_value.node)): warnings.warn("Peeking this value cannot be done efficiently.") - sql = bigframes.core.compile.compile_peek( + sql = self._compiler.compile_peek( self._with_cached_executions(array_value.node), n_rows ) @@ -2044,10 +2045,10 @@ def _to_sql( array_value = array_value.promote_offsets(offset_column) node_w_cached = self._with_cached_executions(array_value.node) if ordered: - return bigframes.core.compile.compile_ordered( + return self._compiler.compile_ordered( node_w_cached, col_id_overrides=col_id_overrides ) - return bigframes.core.compile.compile_unordered( + return self._compiler.compile_unordered( node_w_cached, col_id_overrides=col_id_overrides ) diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index b1c836e1c09..8dfc54c21d7 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -132,3 +132,17 @@ def test_unordered_mode_blocks_windowing(unordered_session, function): match=r"Op.*not supported when strict ordering is disabled", ): function(df) + + +def test_unordered_mode_cache_preserves_order(unordered_session): + pd_df = pd.DataFrame( + {"a": [1, 2, 3, 4, 5, 6], "b": [4, 5, 9, 3, 1, 6]}, dtype=pd.Int64Dtype() + ) + pd_df.index = pd_df.index.astype(pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session) + sorted_df = df.sort_values("b").cache() + bf_result = sorted_df.to_pandas() + pd_result = pd_df.sort_values("b") + + # B is unique so unstrict order mode result here should be equivalent to strictly ordered + assert_pandas_df_equal(bf_result, pd_result, ignore_order=False) From 676a41022c76684795acf35349770608b9e3be47 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 18 Jul 2024 15:24:57 -0700 Subject: [PATCH 715/921] test: temporarily disable streaming tests (#850) * test: temporarily disable streaming tests * fix import --- tests/system/large/test_streaming.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/system/large/test_streaming.py b/tests/system/large/test_streaming.py index c125fde15ab..2debc09994f 100644 --- a/tests/system/large/test_streaming.py +++ b/tests/system/large/test_streaming.py @@ -14,9 +14,12 @@ import time +import pytest + import bigframes.streaming +@pytest.mark.skip(reason="b/354024943. Concurrency error need to be fixed.") def test_streaming_to_bigtable(): # launch a continuous query job_id_prefix = "test_streaming_" @@ -49,6 +52,7 @@ def test_streaming_to_bigtable(): query_job.cancel() +@pytest.mark.skip(reason="b/354024943. Concurrency error need to be fixed.") def test_streaming_to_pubsub(): # launch a continuous query job_id_prefix = "test_streaming_pubsub_" From 01d6bbb7479da706dc62bb5e7d51dc28a4042812 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 19 Jul 2024 16:12:33 -0700 Subject: [PATCH 716/921] test: restore remote function stickiness in small tests (#847) * feat: support remote function cleanup with `session.close` * accept the possibility that the artifact may have already been deleted * add cleanup by previous session id * add more documentation * hold session artifacts in a remote function session class * fix the missing return keyword * test: restore stickiness in small `remote_function` tests docs: make `close_session`/`reset_session` appears in the docs --- bigframes/functions/remote_function.py | 45 +++-- bigframes/pandas/__init__.py | 24 ++- bigframes/session/__init__.py | 2 +- tests/system/small/test_remote_function.py | 202 +++++++++++++-------- 4 files changed, 178 insertions(+), 95 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index f24ba1b5fb6..b95067983fe 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -204,9 +204,12 @@ class IbisSignature(NamedTuple): output_type: IbisDataType -def get_cloud_function_name(function_hash, session_id, uniq_suffix=None): +def get_cloud_function_name(function_hash, session_id=None, uniq_suffix=None): "Get a name for the cloud function for the given user defined function." - parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX, session_id, function_hash] + parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX] + if session_id: + parts.append(session_id) + parts.append(function_hash) if uniq_suffix: parts.append(uniq_suffix) return _GCF_FUNCTION_NAME_SEPERATOR.join(parts) @@ -566,10 +569,13 @@ def provision_bq_remote_function( ) # Derive the name of the cloud function underlying the intended BQ - # remote function, also collect updated package requirements as - # determined in the name resolution + # remote function. Use the session id to identify the GCF for unnamed + # functions. The named remote functions are treated as a persistant + # artifacts, so let's keep them independent of session id, which also + # makes their naming more stable for the same udf code + session_id = None if name else self._session.session_id cloud_function_name = get_cloud_function_name( - function_hash, self._session.session_id, uniq_suffix + function_hash, session_id, uniq_suffix ) cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) @@ -635,13 +641,12 @@ def get_remote_function_specs(self, remote_function_name): ) try: for routine in routines: + routine = cast(bigquery.Routine, routine) if routine.reference.routine_id == remote_function_name: - # TODO(shobs): Use first class properties when they are available - # https://github.com/googleapis/python-bigquery/issues/1552 - rf_options = routine._properties.get("remoteFunctionOptions") + rf_options = routine.remote_function_options if rf_options: - http_endpoint = rf_options.get("endpoint") - bq_connection = rf_options.get("connection") + http_endpoint = rf_options.endpoint + bq_connection = rf_options.connection if bq_connection: bq_connection = os.path.basename(bq_connection) break @@ -731,15 +736,15 @@ class _RemoteFunctionSession: def __init__(self): # Session level mapping of remote function artifacts - self._temp_session_artifacts: Dict[str, str] = dict() + self._temp_artifacts: Dict[str, str] = dict() - # Lock to synchronize the update of the session level mapping - self._session_artifacts_lock = threading.Lock() + # Lock to synchronize the update of the session artifacts + self._artifacts_lock = threading.Lock() - def _update_artifacts(self, bqrf_routine: str, gcf_path: str): + def _update_temp_artifacts(self, bqrf_routine: str, gcf_path: str): """Update remote function artifacts in the current session.""" - with self._session_artifacts_lock: - self._temp_session_artifacts[bqrf_routine] = gcf_path + with self._artifacts_lock: + self._temp_artifacts[bqrf_routine] = gcf_path def clean_up( self, @@ -748,8 +753,8 @@ def clean_up( session_id: str, ): """Delete remote function artifacts in the current session.""" - with self._session_artifacts_lock: - for bqrf_routine, gcf_path in self._temp_session_artifacts.items(): + with self._artifacts_lock: + for bqrf_routine, gcf_path in self._temp_artifacts.items(): # Let's accept the possibility that the remote function may have # been deleted directly by the user bqclient.delete_routine(bqrf_routine, not_found_ok=True) @@ -761,7 +766,7 @@ def clean_up( except google.api_core.exceptions.NotFound: pass - self._temp_session_artifacts.clear() + self._temp_artifacts.clear() # Inspired by @udf decorator implemented in ibis-bigquery package # https://github.com/ibis-project/ibis-bigquery/blob/main/ibis_bigquery/udf/__init__.py @@ -1206,7 +1211,7 @@ def try_delattr(attr): # explicit name, we are assuming that the user wants to persist them # with that name and would directly manage their lifecycle. if created_new and (not name): - self._update_artifacts( + self._update_temp_artifacts( func.bigframes_remote_function, func.bigframes_cloud_function ) return func diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index eb990d23937..21f75eb82c9 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -847,10 +847,28 @@ def clean_up_by_session_id( option_context = config.option_context """Global :class:`~bigframes._config.option_context` to configure BigQuery DataFrames.""" + # Session management APIs -get_global_session = global_session.get_global_session -close_session = global_session.close_session -reset_session = global_session.close_session +def get_global_session(): + return global_session.get_global_session() + + +get_global_session.__doc__ = global_session.get_global_session.__doc__ + + +def close_session(): + return global_session.close_session() + + +close_session.__doc__ = global_session.close_session.__doc__ + + +def reset_session(): + return global_session.close_session() + + +reset_session.__doc__ = global_session.close_session.__doc__ + # SQL Compilation uses recursive algorithms on deep trees # 10M tree depth should be sufficient to generate any sql that is under bigquery limit diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index ca242d269c4..77a20026dd7 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -400,7 +400,7 @@ def _clean_up_tables(self): def close(self): """Delete resources that were created with this session's session_id. This includes BigQuery tables, remote functions and cloud functions - serving the remote functions""" + serving the remote functions.""" self._clean_up_tables() self._remote_function_session.clean_up( self.bqclient, self.cloudfunctionsclient, self.session_id diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index d84d520988a..c07a0afb44e 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -77,15 +77,27 @@ def bq_cf_connection_location_project_mismatched() -> str: @pytest.fixture(scope="module") -def session_with_bq_connection( - bq_cf_connection, dataset_id_permanent -) -> bigframes.Session: +def session_with_bq_connection(bq_cf_connection) -> bigframes.Session: session = bigframes.Session( bigframes.BigQueryOptions(bq_connection=bq_cf_connection, location="US") ) return session +def get_rf_name(func, package_requirements=None, is_row_processor=False): + """Get a remote function name for testing given a udf.""" + # Augment user package requirements with any internal package + # requirements + package_requirements = rf._get_updated_package_requirements( + package_requirements, is_row_processor + ) + + # Compute a unique hash representing the user code + function_hash = rf._get_hash(func, package_requirements) + + return f"bigframes_{function_hash}" + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_direct_no_session_param( bigquery_client, @@ -96,8 +108,11 @@ def test_remote_function_direct_no_session_param( dataset_id_permanent, bq_cf_connection, ): - @rf.remote_function( - [int], + def square(x): + return x * x + + square = rf.remote_function( + int, int, bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, @@ -107,9 +122,8 @@ def test_remote_function_direct_no_session_param( bigquery_connection=bq_cf_connection, # See e2e tests for tests that actually deploy the Cloud Function. reuse=True, - ) - def square(x): - return x * x + name=get_rf_name(square), + )(square) # Function should still work normally. assert square(2) == 4 @@ -153,8 +167,11 @@ def test_remote_function_direct_no_session_param_location_specified( dataset_id_permanent, bq_cf_connection_location, ): - @rf.remote_function( - [int], + def square(x): + return x * x + + square = rf.remote_function( + int, int, bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, @@ -164,9 +181,8 @@ def test_remote_function_direct_no_session_param_location_specified( bigquery_connection=bq_cf_connection_location, # See e2e tests for tests that actually deploy the Cloud Function. reuse=True, - ) - def square(x): - return x * x + name=get_rf_name(square), + )(square) # Function should still work normally. assert square(2) == 4 @@ -204,13 +220,17 @@ def test_remote_function_direct_no_session_param_location_mismatched( dataset_id_permanent, bq_cf_connection_location_mismatched, ): + def square(x): + # Not expected to reach this code, as the location of the + # connection doesn't match the location of the dataset. + return x * x # pragma: NO COVER + with pytest.raises( ValueError, match=re.escape("The location does not match BigQuery connection location:"), ): - - @rf.remote_function( - [int], + rf.remote_function( + int, int, bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, @@ -220,11 +240,8 @@ def test_remote_function_direct_no_session_param_location_mismatched( bigquery_connection=bq_cf_connection_location_mismatched, # See e2e tests for tests that actually deploy the Cloud Function. reuse=True, - ) - def square(x): - # Not expected to reach this code, as the location of the - # connection doesn't match the location of the dataset. - return x * x # pragma: NO COVER + name=get_rf_name(square), + )(square) @pytest.mark.flaky(retries=2, delay=120) @@ -237,8 +254,11 @@ def test_remote_function_direct_no_session_param_location_project_specified( dataset_id_permanent, bq_cf_connection_location_project, ): - @rf.remote_function( - [int], + def square(x): + return x * x + + square = rf.remote_function( + int, int, bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, @@ -248,9 +268,8 @@ def test_remote_function_direct_no_session_param_location_project_specified( bigquery_connection=bq_cf_connection_location_project, # See e2e tests for tests that actually deploy the Cloud Function. reuse=True, - ) - def square(x): - return x * x + name=get_rf_name(square), + )(square) # Function should still work normally. assert square(2) == 4 @@ -288,15 +307,19 @@ def test_remote_function_direct_no_session_param_project_mismatched( dataset_id_permanent, bq_cf_connection_location_project_mismatched, ): + def square(x): + # Not expected to reach this code, as the project of the + # connection doesn't match the project of the dataset. + return x * x # pragma: NO COVER + with pytest.raises( ValueError, match=re.escape( "The project_id does not match BigQuery connection gcp_project_id:" ), ): - - @rf.remote_function( - [int], + rf.remote_function( + int, int, bigquery_client=bigquery_client, bigquery_connection_client=bigqueryconnection_client, @@ -306,23 +329,25 @@ def test_remote_function_direct_no_session_param_project_mismatched( bigquery_connection=bq_cf_connection_location_project_mismatched, # See e2e tests for tests that actually deploy the Cloud Function. reuse=True, - ) - def square(x): - # Not expected to reach this code, as the project of the - # connection doesn't match the project of the dataset. - return x * x # pragma: NO COVER + name=get_rf_name(square), + )(square) @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_direct_session_param(session_with_bq_connection, scalars_dfs): - @rf.remote_function( - [int], - int, - session=session_with_bq_connection, - ) +def test_remote_function_direct_session_param( + session_with_bq_connection, scalars_dfs, dataset_id_permanent +): def square(x): return x * x + square = rf.remote_function( + int, + int, + session=session_with_bq_connection, + dataset=dataset_id_permanent, + name=get_rf_name(square), + )(square) + # Function should still work normally. assert square(2) == 4 @@ -351,7 +376,12 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_via_session_default(session_with_bq_connection, scalars_dfs): +def test_remote_function_via_session_default( + session_with_bq_connection, scalars_dfs, dataset_id_permanent +): + def square(x): + return x * x + # Session has bigquery connection initialized via context. Without an # explicit dataset the default dataset from the session would be used. # Without an explicit bigquery connection, the one present in Session set @@ -359,9 +389,9 @@ def test_remote_function_via_session_default(session_with_bq_connection, scalars # the default behavior of reuse=True will take effect. Please note that the # udf is same as the one used in other tests in this file so the underlying # cloud function would be common and quickly reused. - @session_with_bq_connection.remote_function([int], int) - def square(x): - return x * x + square = session_with_bq_connection.remote_function( + int, int, dataset_id_permanent, name=get_rf_name(square) + )(square) # Function should still work normally. assert square(2) == 4 @@ -394,16 +424,18 @@ def square(x): def test_remote_function_via_session_with_overrides( session, scalars_dfs, dataset_id_permanent, bq_cf_connection ): - @session.remote_function( - [int], + def square(x): + return x * x + + square = session.remote_function( + int, int, dataset_id_permanent, bq_cf_connection, # See e2e tests for tests that actually deploy the Cloud Function. reuse=True, - ) - def square(x): - return x * x + name=get_rf_name(square), + )(square) # Function should still work normally. assert square(2) == 4 @@ -433,11 +465,15 @@ def square(x): @pytest.mark.flaky(retries=2, delay=120) -def test_dataframe_applymap(session_with_bq_connection, scalars_dfs): +def test_dataframe_applymap( + session_with_bq_connection, scalars_dfs, dataset_id_permanent +): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + remote_add_one = session_with_bq_connection.remote_function( + [int], int, dataset_id_permanent, name=get_rf_name(add_one) + )(add_one) scalars_df, scalars_pandas_df = scalars_dfs int64_cols = ["int64_col", "int64_too"] @@ -460,11 +496,15 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_dataframe_applymap_na_ignore(session_with_bq_connection, scalars_dfs): +def test_dataframe_applymap_na_ignore( + session_with_bq_connection, scalars_dfs, dataset_id_permanent +): def add_one(x): return x + 1 - remote_add_one = session_with_bq_connection.remote_function([int], int)(add_one) + remote_add_one = session_with_bq_connection.remote_function( + [int], int, dataset_id_permanent, name=get_rf_name(add_one) + )(add_one) scalars_df, scalars_pandas_df = scalars_dfs int64_cols = ["int64_col", "int64_too"] @@ -485,7 +525,9 @@ def add_one(x): @pytest.mark.flaky(retries=2, delay=120) -def test_series_map_bytes(session_with_bq_connection, scalars_dfs): +def test_series_map_bytes( + session_with_bq_connection, scalars_dfs, dataset_id_permanent +): """Check that bytes is support as input and output.""" scalars_df, scalars_pandas_df = scalars_dfs @@ -502,8 +544,11 @@ def bytes_to_hex(mybytes: bytes) -> bytes: pd.ArrowDtype(pyarrow.binary()) ) + packages = ["pandas"] remote_bytes_to_hex = session_with_bq_connection.remote_function( - packages=["pandas"] + dataset=dataset_id_permanent, + name=get_rf_name(bytes_to_hex, package_requirements=packages), + packages=packages, )(bytes_to_hex) bf_result = scalars_df.bytes_col.map(remote_bytes_to_hex).to_pandas() @@ -541,11 +586,14 @@ def test_skip_bq_connection_check(dataset_id_permanent): match=f"Not found: Connection {connection_name}", ): - @session.remote_function([int], int, dataset=dataset_id_permanent) def add_one(x): # Not expected to reach this code, as the connection doesn't exist. return x + 1 # pragma: NO COVER + session.remote_function( + [int], int, dataset=dataset_id_permanent, name=get_rf_name(add_one) + )(add_one) + @pytest.mark.flaky(retries=2, delay=120) def test_read_gbq_function_detects_invalid_function(session, dataset_id): @@ -570,7 +618,10 @@ def test_read_gbq_function_like_original( dataset_id_permanent, bq_cf_connection, ): - @rf.remote_function( + def square1(x): + return x * x + + square1 = rf.remote_function( [int], int, bigquery_client=bigquery_client, @@ -580,29 +631,28 @@ def test_read_gbq_function_like_original( resource_manager_client=resourcemanager_client, bigquery_connection=bq_cf_connection, reuse=True, - ) - def square1(x): - return x * x + name=get_rf_name(square1), + )(square1) # Function should still work normally. assert square1(2) == 4 square2 = rf.read_gbq_function( - function_name=square1.bigframes_remote_function, + function_name=square1.bigframes_remote_function, # type: ignore session=session, ) # The newly-created function (square1) should have a remote function AND a # cloud function associated with it, while the read-back version (square2) # should only have a remote function. - assert square1.bigframes_remote_function - assert square1.bigframes_cloud_function + assert square1.bigframes_remote_function # type: ignore + assert square1.bigframes_cloud_function # type: ignore assert square2.bigframes_remote_function assert not hasattr(square2, "bigframes_cloud_function") # They should point to the same function. - assert square1.bigframes_remote_function == square2.bigframes_remote_function + assert square1.bigframes_remote_function == square2.bigframes_remote_function # type: ignore # The result of applying them should be the same. int64_col = scalars_df_index["int64_col"] @@ -743,7 +793,7 @@ def test_read_gbq_function_enforces_explicit_types( @pytest.mark.flaky(retries=2, delay=120) -def test_df_apply_axis_1(session, scalars_dfs): +def test_df_apply_axis_1(session, scalars_dfs, dataset_id_permanent): columns = [ "bool_col", "int64_col", @@ -764,6 +814,8 @@ def add_ints(row): add_ints_remote = session.remote_function( bigframes.series.Series, int, + dataset_id_permanent, + name=get_rf_name(add_ints, is_row_processor=True), )(add_ints) with pytest.warns( @@ -785,7 +837,7 @@ def add_ints(row): @pytest.mark.flaky(retries=2, delay=120) -def test_df_apply_axis_1_ordering(session, scalars_dfs): +def test_df_apply_axis_1_ordering(session, scalars_dfs, dataset_id_permanent): columns = ["bool_col", "int64_col", "int64_too", "float64_col", "string_col"] ordering_columns = ["bool_col", "int64_col"] scalars_df, scalars_pandas_df = scalars_dfs @@ -793,7 +845,12 @@ def test_df_apply_axis_1_ordering(session, scalars_dfs): def add_ints(row): return row["int64_col"] + row["int64_too"] - add_ints_remote = session.remote_function(bigframes.series.Series, int)(add_ints) + add_ints_remote = session.remote_function( + bigframes.series.Series, + int, + dataset_id_permanent, + name=get_rf_name(add_ints, is_row_processor=True), + )(add_ints) bf_result = ( scalars_df[columns] @@ -817,7 +874,7 @@ def add_ints(row): @pytest.mark.flaky(retries=2, delay=120) -def test_df_apply_axis_1_multiindex(session): +def test_df_apply_axis_1_multiindex(session, dataset_id_permanent): pd_df = pd.DataFrame( {"x": [1, 2, 3], "y": [1.5, 3.75, 5], "z": ["pq", "rs", "tu"]}, index=pd.MultiIndex.from_tuples([("a", 100), ("a", 200), ("b", 300)]), @@ -827,9 +884,12 @@ def test_df_apply_axis_1_multiindex(session): def add_numbers(row): return row["x"] + row["y"] - add_numbers_remote = session.remote_function(bigframes.series.Series, float)( - add_numbers - ) + add_numbers_remote = session.remote_function( + bigframes.series.Series, + float, + dataset_id_permanent, + name=get_rf_name(add_numbers, is_row_processor=True), + )(add_numbers) bf_result = bf_df.apply(add_numbers_remote, axis=1).to_pandas() pd_result = pd_df.apply(add_numbers, axis=1) From f9e4435f7ca47c9a53f295b54592396d8f2c7c2f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 22 Jul 2024 18:06:18 -0700 Subject: [PATCH 717/921] test: fix mypy failures to unblock presubmit tests (#852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The presubmit and continuous tests have started failing due to mypy. This change finds the failure points and puts in workarounds. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/session/__init__.py | 10 ++++++---- third_party/bigframes_vendored/pandas/core/series.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 77a20026dd7..f5482ff3898 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -650,15 +650,17 @@ def _read_gbq_query( index_cols = _to_index_cols(index_col) - filters = list(filters) - if len(filters) != 0 or max_results is not None: + filters_copy1, filters_copy2 = itertools.tee(filters) + has_filters = len(list(filters_copy1)) != 0 + filters = typing.cast(third_party_pandas_gbq.FiltersType, filters_copy2) + if has_filters or max_results is not None: # TODO(b/338111344): If we are running a query anyway, we might as # well generate ROW_NUMBER() at the same time. all_columns = itertools.chain(index_cols, columns) if columns else () query = bf_io_bigquery.to_query( query, all_columns, - bf_io_bigquery.compile_filters(filters) if filters else None, + bf_io_bigquery.compile_filters(filters) if has_filters else None, max_results=max_results, # We're executing the query, so we don't need time travel for # determinism. @@ -768,7 +770,7 @@ def _read_gbq_table( ) columns = list(columns) - filters = list(filters) + filters = typing.cast(list, list(filters)) # --------------------------------- # Fetch table metadata and validate diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index a430c3375fc..a30ed9cd922 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -3521,7 +3521,7 @@ def mask(self, cond, other): """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def clip(self): + def clip(self, lower, upper): """Trim values at input threshold(s). Assigns values outside boundary to boundary values. Thresholds can be From 1b6a556206a7a66283339d827ab12db2753521e2 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 23 Jul 2024 12:20:16 -0700 Subject: [PATCH 718/921] fix: Fix 'sql' property for null index (#844) --- bigframes/dataframe.py | 4 +++- bigframes/session/__init__.py | 2 ++ bigframes/session/_io/bigquery/read_gbq_table.py | 4 ++++ tests/system/small/test_unordered.py | 9 +++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index dcb2fd09cb3..2a3aead80a7 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -385,7 +385,9 @@ def _to_sql_query( @property def sql(self) -> str: """Compiles this DataFrame's expression tree to SQL.""" - include_index = self.index.name is not None or len(self.index.names) > 1 + include_index = self._has_index and ( + self.index.name is not None or len(self.index.names) > 1 + ) sql, _, _ = self._to_sql_query(include_index=include_index) return sql diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index f5482ff3898..9c953ee594d 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -889,6 +889,8 @@ def _read_gbq_table( table=table, index_cols=index_cols, api_name=api_name, + # If non in strict ordering mode, don't go through overhead of scanning index column(s) to determine if unique + metadata_only=not self._strictly_ordered, ) schema = schemata.ArraySchema.from_bq_table(table) if columns: diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 879a8ba44ce..03b26f94606 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -152,6 +152,7 @@ def are_index_cols_unique( table: bigquery.table.Table, index_cols: List[str], api_name: str, + metadata_only: bool = False, ) -> bool: if len(index_cols) == 0: return False @@ -161,6 +162,9 @@ def are_index_cols_unique( if (len(primary_keys) > 0) and primary_keys <= frozenset(index_cols): return True + if metadata_only: + # Sometimes not worth scanning data to check uniqueness + return False # TODO(b/337925142): Avoid a "SELECT *" subquery here by ensuring # table_expression only selects just index_cols. is_unique_sql = bigframes.core.sql.is_distinct_sql(index_cols, table.reference) diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 8dfc54c21d7..6d9171aeed1 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -20,6 +20,15 @@ from tests.system.utils import assert_pandas_df_equal, skip_legacy_pandas +def test_unordered_mode_sql_no_hash(unordered_session): + bf_df = unordered_session.read_gbq( + "bigquery-public-data.ethereum_blockchain.blocks" + ) + sql = bf_df.sql + assert "ORDER BY".casefold() not in sql.casefold() + assert "farm_fingerprint".casefold() not in sql.casefold() + + def test_unordered_mode_job_label(unordered_session): pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) df = bpd.DataFrame(pd_df, session=unordered_session) From 10da997a9276d8bd0ba8c13861c8a5246c905ba8 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:31:13 -0700 Subject: [PATCH 719/921] chore: fix create_bigtable script (#853) --- scripts/create_bigtable.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/create_bigtable.py b/scripts/create_bigtable.py index f81bb8a0138..da40e9063d2 100644 --- a/scripts/create_bigtable.py +++ b/scripts/create_bigtable.py @@ -18,6 +18,7 @@ import os import sys +from google.cloud.bigtable import column_family import google.cloud.bigtable as bigtable PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -57,8 +58,11 @@ def create_table(instance): table_id, instance, ) + max_versions_rule = column_family.MaxVersionsGCRule(1) + column_family_id = "body_mass_g" + column_families = {column_family_id: max_versions_rule} if not table.exists(): - table.create() + table.create(column_families=column_families) print(f"Created table {table_id}") From eb0ef75d7ce2e0a1e1735429d61df53828240a69 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:37:45 -0700 Subject: [PATCH 720/921] test: enable Gemini 1.5 flash default test (#859) --- tests/system/small/ml/test_llm.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index ee9d654d938..b926004fd80 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -328,12 +328,7 @@ def test_create_load_gemini_text_generator_model( @pytest.mark.parametrize( "model_name", - ( - "gemini-pro", - "gemini-1.5-pro-preview-0514", - # TODO(garrrettwu): enable when cl/637028077 is in prod. - # "gemini-1.5-flash-preview-0514" - ), + ("gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514"), ) @pytest.mark.flaky(retries=2) def test_gemini_text_generator_predict_default_params_success( From 823c0ce57611c0918a9e9999638d7393337fe9af Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 25 Jul 2024 14:50:10 -0700 Subject: [PATCH 721/921] feat: Add config option to set partial ordering mode (#855) --- bigframes/_config/bigquery_options.py | 33 +++++++++++++++++++++------ bigframes/session/__init__.py | 12 +++++++--- tests/system/conftest.py | 6 ++--- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index ad79543cb8e..0506f1841eb 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -16,7 +16,8 @@ from __future__ import annotations -from typing import Optional +from enum import Enum +from typing import Literal, Optional import warnings import google.api_core.exceptions @@ -26,6 +27,12 @@ import bigframes.constants import bigframes.exceptions + +class OrderingMode(Enum): + STRICT = "strict" + PARTIAL = "partial" + + SESSION_STARTED_MESSAGE = ( "Cannot change '{attribute}' once a session has started. " "Call bigframes.pandas.close_session() first, if you are using the bigframes.pandas API." @@ -57,6 +64,14 @@ def _validate_location(value: Optional[str]): ) +def _validate_ordering_mode(value: str) -> OrderingMode: + if value.casefold() == OrderingMode.STRICT.value.casefold(): + return OrderingMode.STRICT + if value.casefold() == OrderingMode.PARTIAL.value.casefold(): + return OrderingMode.PARTIAL + raise ValueError("Ordering mode must be one of 'strict' or 'partial'.") + + class BigQueryOptions: """Encapsulates configuration for working with a session.""" @@ -71,7 +86,7 @@ def __init__( kms_key_name: Optional[str] = None, skip_bq_connection_check: bool = False, *, - _strictly_ordered: bool = True, + ordering_mode: Literal["strict", "partial"] = "strict", ): self._credentials = credentials self._project = project @@ -82,8 +97,8 @@ def __init__( self._kms_key_name = kms_key_name self._skip_bq_connection_check = skip_bq_connection_check self._session_started = False - # Determines the ordering strictness for the session. For internal use only. - self._strictly_ordered_internal = _strictly_ordered + # Determines the ordering strictness for the session. + self._ordering_mode = _validate_ordering_mode(ordering_mode) @property def application_name(self) -> Optional[str]: @@ -241,6 +256,10 @@ def kms_key_name(self, value: str): self._kms_key_name = value @property - def _strictly_ordered(self) -> bool: - """Internal use only. Controls whether total row order is always maintained for DataFrame/Series.""" - return self._strictly_ordered_internal + def ordering_mode(self) -> Literal["strict", "partial"]: + """Controls whether total row order is always maintained for DataFrame/Series.""" + return self._ordering_mode.value + + @ordering_mode.setter + def ordering_mode(self, ordering_mode: Literal["strict", "partial"]) -> None: + self._ordering_mode = _validate_ordering_mode(ordering_mode) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 9c953ee594d..22ca63d25b0 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -297,15 +297,21 @@ def __init__( self._execution_count = 0 # Whether this session treats objects as totally ordered. # Will expose as feature later, only False for internal testing - self._strictly_ordered: bool = context._strictly_ordered + self._strictly_ordered: bool = context.ordering_mode != "partial" + if not self._strictly_ordered: + warnings.warn( + "Partial ordering mode is a preview feature and is subject to change.", + bigframes.exceptions.PreviewWarning, + ) + # Sequential index needs total ordering to generate, so use null index with unstrict ordering. self._default_index_type: bigframes.enums.DefaultIndexKind = ( bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 - if context._strictly_ordered + if self._strictly_ordered else bigframes.enums.DefaultIndexKind.NULL ) self._compiler = bigframes.core.compile.SQLCompiler( - strict=context._strictly_ordered + strict=self._strictly_ordered ) self._remote_function_session = bigframes_rf._RemoteFunctionSession() diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 59439c306f7..55079380f40 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -141,9 +141,7 @@ def session() -> Generator[bigframes.Session, None, None]: @pytest.fixture(scope="session", params=["ordered", "unordered"]) def maybe_ordered_session(request) -> Generator[bigframes.Session, None, None]: - context = bigframes.BigQueryOptions( - location="US", _strictly_ordered=request.param == "ordered" - ) + context = bigframes.BigQueryOptions(location="US", ordering_mode="partial") session = bigframes.Session(context=context) yield session session.close() # close generated session at cleanup type @@ -151,7 +149,7 @@ def maybe_ordered_session(request) -> Generator[bigframes.Session, None, None]: @pytest.fixture(scope="session") def unordered_session() -> Generator[bigframes.Session, None, None]: - context = bigframes.BigQueryOptions(location="US", _strictly_ordered=False) + context = bigframes.BigQueryOptions(location="US", ordering_mode="partial") session = bigframes.Session(context=context) yield session session.close() # close generated session at cleanup type From 2c810865035e96b577132893789608ee65d3e186 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 26 Jul 2024 15:12:51 -0700 Subject: [PATCH 722/921] chore: increase stale cloud functions cleanup rate (#863) * chore: increase stale cloud functions cleanup rate * reword the comment --- tests/system/conftest.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 55079380f40..3acae0e75bd 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -43,11 +43,15 @@ # Use this to control the number of cloud functions being deleted in a single # test session. This should help soften the spike of the number of mutations per -# minute tracked against a quota limit (default 60, increased to 120 for -# bigframes-dev project) by the Cloud Functions API -# We are running pytest with "-n 20". Let's say each session lasts about a -# minute, so we are setting a limit of 120/20 = 6 deletions per session. -MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION = 6 +# minute tracked against the quota limit: +# Cloud Functions API -> Per project mutation requests per minute per region +# (default 60, increased to 1000 for the test projects) +# We are running pytest with "-n 20". For a rough estimation, let's say all +# parallel sessions run in parallel. So that allows 1000/20 = 50 mutations per +# minute. One session takes about 1 minute to create a remote function. This +# would allow 50-1 = 49 deletions per session. As a heuristic let's use half of +# that potential for the clean up. +MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION = 25 CURRENT_DIR = pathlib.Path(__file__).parent DATA_DIR = CURRENT_DIR.parent / "data" From 0676f73c22411f9d19399fd34790298c32f8316b Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 29 Jul 2024 19:32:21 +0200 Subject: [PATCH 723/921] chore(deps): update all dependencies (#715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update all dependencies * 🦉 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 Co-authored-by: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> --- samples/snippets/requirements-test.txt | 2 +- samples/snippets/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index beca2e44d9d..d66afc7edb6 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -1,3 +1,3 @@ # samples/snippets should be runnable with no "extras" google-cloud-testutils==1.4.0 -pytest==8.2.0 +pytest==8.2.1 diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 8fcd19bb2c3..fc00b9362e3 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ # samples/snippets should be runnable with no "extras" -bigframes==1.6.0 +bigframes==1.7.0 From b9e6150c1c35959845e4cf129ccde75a8c2abecb Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 29 Jul 2024 11:05:32 -0700 Subject: [PATCH 724/921] refactor: Block .head consistently in unordered mode (#854) --- bigframes/constants.py | 2 ++ bigframes/core/groupby/__init__.py | 2 ++ bigframes/core/validations.py | 13 ++++++++----- bigframes/dataframe.py | 2 ++ bigframes/series.py | 2 +- tests/system/small/test_unordered.py | 4 ++++ 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/bigframes/constants.py b/bigframes/constants.py index 9591297956c..3c18fd20bdf 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -99,3 +99,5 @@ # BigQuery default is 10000, leave 100 for overhead MAX_COLUMNS = 9900 + +SUGGEST_PEEK_PREVIEW = "Use .peek(n) to preview n arbitrary rows." diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 11a5d43ba0e..02bf201ca0c 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -109,6 +109,7 @@ def __getitem__( dropna=self._dropna, ) + @validations.requires_strict_ordering() def head(self, n: int = 5) -> df.DataFrame: block = self._block if self._dropna: @@ -531,6 +532,7 @@ def __init__( def _session(self) -> core.Session: return self._block.session + @validations.requires_strict_ordering() def head(self, n: int = 5) -> series.Series: block = self._block if self._dropna: diff --git a/bigframes/core/validations.py b/bigframes/core/validations.py index dc22047e3bb..c5761f4e09d 100644 --- a/bigframes/core/validations.py +++ b/bigframes/core/validations.py @@ -17,7 +17,7 @@ from __future__ import annotations import functools -from typing import Protocol, TYPE_CHECKING +from typing import Optional, Protocol, TYPE_CHECKING import bigframes.constants import bigframes.exceptions @@ -32,11 +32,11 @@ def _session(self) -> Session: ... -def requires_strict_ordering(): +def requires_strict_ordering(suggestion: Optional[str] = None): def decorator(meth): @functools.wraps(meth) def guarded_meth(object: HasSession, *args, **kwargs): - enforce_ordered(object, meth.__name__) + enforce_ordered(object, meth.__name__, suggestion) return meth(object, *args, **kwargs) return guarded_meth @@ -44,8 +44,11 @@ def guarded_meth(object: HasSession, *args, **kwargs): return decorator -def enforce_ordered(object: HasSession, opname: str) -> None: +def enforce_ordered( + object: HasSession, opname: str, suggestion: Optional[str] = None +) -> None: if not object._session._strictly_ordered: + suggestion_substr = suggestion + " " if suggestion else "" raise bigframes.exceptions.OrderRequiredError( - f"Op {opname} not supported when strict ordering is disabled. {bigframes.constants.FEEDBACK_LINK}" + f"Op {opname} not supported when strict ordering is disabled. {suggestion_substr}{bigframes.constants.FEEDBACK_LINK}" ) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 2a3aead80a7..717549316ad 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -49,6 +49,7 @@ import bigframes import bigframes._config.display_options as display_options +import bigframes.constants import bigframes.constants as constants import bigframes.core from bigframes.core import log_adapter @@ -1293,6 +1294,7 @@ def _compute_dry_run(self) -> bigquery.QueryJob: def copy(self) -> DataFrame: return DataFrame(self._block) + @validations.requires_strict_ordering(bigframes.constants.SUGGEST_PEEK_PREVIEW) def head(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[:n]) diff --git a/bigframes/series.py b/bigframes/series.py index 8fdafe25e7e..7c530b9612d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -633,7 +633,7 @@ def dropna( result = result.reset_index() return Series(result) - @validations.requires_strict_ordering() + @validations.requires_strict_ordering(bigframes.constants.SUGGEST_PEEK_PREVIEW) def head(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[0:n]) diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 6d9171aeed1..2e97078ef5c 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -131,6 +131,10 @@ def test_unordered_drop_duplicates(unordered_session, keep): lambda x: x.a.iloc[1::2], id="series_iloc", ), + pytest.param( + lambda x: x.head(3), + id="head", + ), ], ) def test_unordered_mode_blocks_windowing(unordered_session, function): From f23de1a9711e149549c6df81990e043b042311d5 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 30 Jul 2024 19:29:19 +0200 Subject: [PATCH 725/921] chore(deps): update all dependencies (#866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update all dependencies * revert pyarrow change Can't update pyarrow until we update ibis. --------- Co-authored-by: Tim Sweña (Swast) --- samples/polars/requirements-test.txt | 2 +- samples/polars/requirements.txt | 4 ++-- samples/snippets/requirements-test.txt | 2 +- samples/snippets/requirements.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/polars/requirements-test.txt b/samples/polars/requirements-test.txt index beca2e44d9d..cbac5e3f126 100644 --- a/samples/polars/requirements-test.txt +++ b/samples/polars/requirements-test.txt @@ -1,3 +1,3 @@ # samples/snippets should be runnable with no "extras" google-cloud-testutils==1.4.0 -pytest==8.2.0 +pytest==8.3.2 diff --git a/samples/polars/requirements.txt b/samples/polars/requirements.txt index e3f886e7e32..a1d8fbcdac4 100644 --- a/samples/polars/requirements.txt +++ b/samples/polars/requirements.txt @@ -1,3 +1,3 @@ -bigframes==1.6.0 -polars==0.20.31 +bigframes==1.11.1 +polars==1.3.0 pyarrow==15.0.0 diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index d66afc7edb6..cbac5e3f126 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -1,3 +1,3 @@ # samples/snippets should be runnable with no "extras" google-cloud-testutils==1.4.0 -pytest==8.2.1 +pytest==8.3.2 diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index fc00b9362e3..9b5da5182e2 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ # samples/snippets should be runnable with no "extras" -bigframes==1.7.0 +bigframes==1.11.1 From 8e04c3827063874c27c2f0e9559b7387a8a206bc Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 30 Jul 2024 10:30:17 -0700 Subject: [PATCH 726/921] refactor: Respect session default index in merge and reset_index methods (#862) --- bigframes/core/blocks.py | 39 +++++++++++++++++++++------- tests/system/small/test_unordered.py | 22 ++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 2d7c5436782..05865a66990 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -327,8 +327,21 @@ def reset_index(self, drop: bool = True) -> Block: A new Block because dropping index columns can break references from Index classes that point to this block. """ - new_index_col_id = guid.generate_guid() - expr = self._expr.promote_offsets(new_index_col_id) + expr = self._expr + if ( + self.session._default_index_type + == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 + ): + new_index_col_id = guid.generate_guid() + expr = expr.promote_offsets(new_index_col_id) + new_index_cols = [new_index_col_id] + elif self.session._default_index_type == bigframes.enums.DefaultIndexKind.NULL: + new_index_cols = [] + else: + raise ValueError( + f"Unrecognized default index kind: {self.session._default_index_type}" + ) + if drop: # Even though the index might be part of the ordering, keep that # ordering expression as reset_index shouldn't change the row @@ -336,9 +349,8 @@ def reset_index(self, drop: bool = True) -> Block: expr = expr.drop_columns(self.index_columns) return Block( expr, - index_columns=[new_index_col_id], + index_columns=new_index_cols, column_labels=self.column_labels, - index_labels=[None], ) else: # Add index names to column index @@ -362,9 +374,8 @@ def reset_index(self, drop: bool = True) -> Block: return Block( expr, - index_columns=[new_index_col_id], + index_columns=new_index_cols, column_labels=column_labels_modified, - index_labels=[None], ) def set_index( @@ -2096,13 +2107,17 @@ def merge( # # This keeps us from generating an index if the user joins a large # BigQuery table against small local data, for example. - if len(self._index_columns) > 0 and len(other._index_columns) > 0: + if ( + self.index.is_null + or other.index.is_null + or self.session._default_index_type == bigframes.enums.DefaultIndexKind.NULL + ): + expr = joined_expr + index_columns = [] + else: offset_index_id = guid.generate_guid() expr = joined_expr.promote_offsets(offset_index_id) index_columns = [offset_index_id] - else: - expr = joined_expr - index_columns = [] return Block(expr, index_columns=index_columns, column_labels=labels) @@ -2604,6 +2619,10 @@ def column_ids(self) -> Sequence[str]: """Column(s) to use as row labels.""" return self._block._index_columns + @property + def is_null(self) -> bool: + return len(self._block._index_columns) == 0 + def to_pandas(self, *, ordered: Optional[bool] = None) -> pd.Index: """Executes deferred operations and downloads the results.""" if len(self.column_ids) == 0: diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 2e97078ef5c..7d7097ceb37 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -116,6 +116,28 @@ def test_unordered_drop_duplicates(unordered_session, keep): assert_pandas_df_equal(bf_result.to_pandas(), pd_result, ignore_order=True) +def test_unordered_reset_index(unordered_session): + pd_df = pd.DataFrame({"a": [1, 1, 3], "b": [4, 4, 6]}, dtype=pd.Int64Dtype()) + bf_df = bpd.DataFrame(pd_df, session=unordered_session) + + bf_result = bf_df.set_index("b").reset_index(drop=False) + pd_result = pd_df.set_index("b").reset_index(drop=False) + + assert_pandas_df_equal(bf_result.to_pandas(), pd_result) + + +def test_unordered_merge(unordered_session): + pd_df = pd.DataFrame( + {"a": [1, 1, 3], "b": [4, 4, 6], "c": [1, 2, 3]}, dtype=pd.Int64Dtype() + ) + bf_df = bpd.DataFrame(pd_df, session=unordered_session) + + bf_result = bf_df.merge(bf_df, left_on="a", right_on="c") + pd_result = pd_df.merge(pd_df, left_on="a", right_on="c") + + assert_pandas_df_equal(bf_result.to_pandas(), pd_result, ignore_order=True) + + @pytest.mark.parametrize( ("function"), [ From e95053372c36ea5a91a2d7295c1a3a3671181670 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 30 Jul 2024 10:33:14 -0700 Subject: [PATCH 727/921] feat: Allow DataFrame.join for self-join on Null index (#860) * feat: Allow DataFrame.join for self-join on Null index * fix ml caching to apply post-join, add test * fix ml golden sql test * change unordered test to use linear regression --- bigframes/core/blocks.py | 10 ++--- bigframes/ml/core.py | 8 ++-- tests/system/large/ml/test_linear_model.py | 44 ++++++++++++++++++++++ tests/system/small/test_null_index.py | 14 +++++++ tests/unit/ml/test_golden_sql.py | 1 + 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 05865a66990..fd0c9c9539c 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2307,11 +2307,11 @@ def join( f"Only how='outer','left','right','inner' currently supported. {constants.FEEDBACK_LINK}" ) # Handle null index, which only supports row join - if (self.index.nlevels == other.index.nlevels == 0) and not block_identity_join: - if not block_identity_join: - result = try_row_join(self, other, how=how) - if result is not None: - return result + # This is the canonical way of aligning on null index, so always allow (ignore block_identity_join) + if self.index.nlevels == other.index.nlevels == 0: + result = try_row_join(self, other, how=how) + if result is not None: + return result raise bigframes.exceptions.NullIndexError( "Cannot implicitly align objects. Set an explicit index using set_index." ) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index ee4d8a8c27c..f1b36651f4b 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -83,7 +83,7 @@ def distance( """ assert len(x.columns) == 1 and len(y.columns) == 1 - input_data = x.cache().join(y.cache(), how="outer") + input_data = x.join(y, how="outer").cache() x_column_id, y_column_id = x._block.value_columns[0], y._block.value_columns[0] return self._apply_sql( @@ -326,7 +326,7 @@ def create_model( if y_train is None: input_data = X_train.cache() else: - input_data = X_train.cache().join(y_train.cache(), how="outer") + input_data = X_train.join(y_train, how="outer").cache() options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session @@ -366,7 +366,7 @@ def create_llm_remote_model( options = dict(options) # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot - input_data = X_train.cache().join(y_train.cache(), how="outer") + input_data = X_train.join(y_train, how="outer").cache() options.update({"INPUT_LABEL_COLS": y_train.columns.tolist()}) session = X_train._session @@ -399,7 +399,7 @@ def create_time_series_model( options = dict(options) # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot - input_data = X_train.cache().join(y_train.cache(), how="outer") + input_data = X_train.join(y_train, how="outer").cache() options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 0cc9fc53538..2f4c07fa28f 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -111,6 +111,50 @@ def test_linear_regression_customized_params_fit_score( assert reloaded_model.learning_rate == 0.2 +def test_unordered_mode_regression_configure_fit_score( + unordered_session, penguins_table_id, dataset_id +): + model = bigframes.ml.linear_model.LinearRegression() + + df = unordered_session.read_gbq(penguins_table_id).dropna() + X_train = df[ + [ + "species", + "island", + "culmen_length_mm", + "culmen_depth_mm", + "flipper_length_mm", + "sex", + ] + ] + y_train = df[["body_mass_g"]] + model.fit(X_train, y_train) + + # Check score to ensure the model was fitted + result = model.score(X_train, y_train).to_pandas() + utils.check_pandas_df_schema_and_index( + result, columns=utils.ML_REGRESSION_METRICS, index=1 + ) + + # save, load, check parameters to ensure configuration was kept + reloaded_model = model.to_gbq(f"{dataset_id}.temp_configured_model", replace=True) + assert reloaded_model._bqml_model is not None + assert ( + f"{dataset_id}.temp_configured_model" in reloaded_model._bqml_model.model_name + ) + assert reloaded_model.optimize_strategy == "NORMAL_EQUATION" + assert reloaded_model.fit_intercept is True + assert reloaded_model.calculate_p_values is False + assert reloaded_model.enable_global_explain is False + assert reloaded_model.l1_reg is None + assert reloaded_model.l2_reg == 0.0 + assert reloaded_model.learning_rate is None + assert reloaded_model.learning_rate_strategy == "line_search" + assert reloaded_model.ls_init_learning_rate is None + assert reloaded_model.max_iterations == 20 + assert reloaded_model.tol == 0.01 + + # TODO(garrettwu): add tests for param warm_start. Requires a trained model. diff --git a/tests/system/small/test_null_index.py b/tests/system/small/test_null_index.py index 27a3d8dffea..a1e360f73d5 100644 --- a/tests/system/small/test_null_index.py +++ b/tests/system/small/test_null_index.py @@ -201,6 +201,20 @@ def test_null_index_stack(scalars_df_null_index, scalars_pandas_df_default_index ) +def test_null_index_series_self_join( + scalars_df_null_index, scalars_pandas_df_default_index +): + bf_result = scalars_df_null_index[["int64_col"]].join( + scalars_df_null_index[["int64_too"]] + ) + pd_result = scalars_pandas_df_default_index[["int64_col"]].join( + scalars_pandas_df_default_index[["int64_too"]] + ) + pd.testing.assert_frame_equal( + bf_result.to_pandas(), pd_result.reset_index(drop=True), check_dtype=False + ) + + def test_null_index_series_self_aligns( scalars_df_null_index, scalars_pandas_df_default_index ): diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 48fb7011ea1..aa7e919b24c 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -78,6 +78,7 @@ def mock_X(mock_y, mock_session): ["index_column_label"], ) mock_X.join(mock_y).sql = "input_X_y_sql" + mock_X.join(mock_y).cache.return_value = mock_X.join(mock_y) mock_X.join(mock_y)._to_sql_query.return_value = ( "input_X_y_sql", ["index_column_id"], From d0ab9cc47298bdde638299baecac9dffd7841ede Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 30 Jul 2024 11:28:48 -0700 Subject: [PATCH 728/921] feat: Support to_csv/parquet/json to local files/objects (#858) --- bigframes/core/utils.py | 4 ++ bigframes/dataframe.py | 50 +++++++++----- bigframes/series.py | 41 ++++++++--- tests/system/small/test_dataframe.py | 68 ++++++++++++++++++- tests/system/small/test_series.py | 38 +++++++++++ .../bigframes_vendored/pandas/core/frame.py | 14 ++-- .../bigframes_vendored/pandas/core/generic.py | 41 +++++++---- 7 files changed, 206 insertions(+), 50 deletions(-) diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 97c5ef03e58..43c05c6c839 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -23,6 +23,10 @@ UNNAMED_INDEX_ID = "bigframes_unnamed_index" +def is_gcs_path(value) -> typing_extensions.TypeGuard[str]: + return isinstance(value, str) and value.startswith("gs://") + + def get_axis_number(axis: typing.Union[str, int]) -> typing.Literal[0, 1]: if axis in {0, "index", "rows"}: return 0 diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 717549316ad..5d4918c3ceb 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2952,15 +2952,21 @@ def from_records( ) def to_csv( - self, path_or_buf: str, sep=",", *, header: bool = True, index: bool = True - ) -> None: + self, + path_or_buf=None, + sep=",", + *, + header: bool = True, + index: bool = True, + ) -> Optional[str]: # TODO(swast): Can we support partition columns argument? # TODO(chelsealin): Support local file paths. # TODO(swast): Some warning that wildcard is recommended for large # query results? See: # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size - if not path_or_buf.startswith("gs://"): - raise NotImplementedError(ERROR_IO_ONLY_GS_PATHS) + if not utils.is_gcs_path(path_or_buf): + pd_df = self.to_pandas() + return pd_df.to_csv(path_or_buf, sep=sep, header=header, index=index) if "*" not in path_or_buf: raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) @@ -2977,22 +2983,28 @@ def to_csv( export_data_statement, api_name="dataframe-to_csv" ) self._set_internal_query_job(query_job) + return None def to_json( self, - path_or_buf: str, - orient: Literal[ - "split", "records", "index", "columns", "values", "table" - ] = "columns", + path_or_buf=None, + orient: Optional[ + Literal["split", "records", "index", "columns", "values", "table"] + ] = None, *, lines: bool = False, index: bool = True, - ) -> None: + ) -> Optional[str]: # TODO(swast): Can we support partition columns argument? - # TODO(chelsealin): Support local file paths. - if not path_or_buf.startswith("gs://"): - raise NotImplementedError(ERROR_IO_ONLY_GS_PATHS) - + if not utils.is_gcs_path(path_or_buf): + pd_df = self.to_pandas() + return pd_df.to_json( + path_or_buf, + orient=orient, + lines=lines, + index=index, + default_handler=str, + ) if "*" not in path_or_buf: raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) @@ -3021,6 +3033,7 @@ def to_json( export_data_statement, api_name="dataframe-to_json" ) self._set_internal_query_job(query_job) + return None def to_gbq( self, @@ -3119,19 +3132,19 @@ def __array__(self, dtype=None) -> numpy.ndarray: def to_parquet( self, - path: str, + path=None, *, compression: Optional[Literal["snappy", "gzip"]] = "snappy", index: bool = True, - ) -> None: + ) -> Optional[bytes]: # TODO(swast): Can we support partition columns argument? # TODO(chelsealin): Support local file paths. # TODO(swast): Some warning that wildcard is recommended for large # query results? See: # https://cloud.google.com/bigquery/docs/exporting-data#limit_the_exported_file_size - if not path.startswith("gs://"): - raise NotImplementedError(ERROR_IO_ONLY_GS_PATHS) - + if not utils.is_gcs_path(path): + pd_df = self.to_pandas() + return pd_df.to_parquet(path, compression=compression, index=index) if "*" not in path: raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) @@ -3155,6 +3168,7 @@ def to_parquet( export_data_statement, api_name="dataframe-to_parquet" ) self._set_internal_query_job(query_job) + return None def to_dict( self, diff --git a/bigframes/series.py b/bigframes/series.py index 7c530b9612d..1a5661529c9 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1652,9 +1652,22 @@ def to_frame(self, name: blocks.Label = None) -> bigframes.dataframe.DataFrame: return bigframes.dataframe.DataFrame(block) def to_csv( - self, path_or_buf: str, sep=",", *, header: bool = True, index: bool = True - ) -> None: - return self.to_frame().to_csv(path_or_buf, sep=sep, header=header, index=index) + self, + path_or_buf=None, + sep=",", + *, + header: bool = True, + index: bool = True, + ) -> Optional[str]: + if utils.is_gcs_path(path_or_buf): + return self.to_frame().to_csv( + path_or_buf, sep=sep, header=header, index=index + ) + else: + pd_series = self.to_pandas() + return pd_series.to_csv( + path_or_buf=path_or_buf, sep=sep, header=header, index=index + ) def to_dict(self, into: type[dict] = dict) -> typing.Mapping: return typing.cast(dict, self.to_pandas().to_dict(into)) # type: ignore @@ -1664,17 +1677,23 @@ def to_excel(self, excel_writer, sheet_name="Sheet1", **kwargs) -> None: def to_json( self, - path_or_buf: str, - orient: typing.Literal[ - "split", "records", "index", "columns", "values", "table" - ] = "columns", + path_or_buf=None, + orient: Optional[ + typing.Literal["split", "records", "index", "columns", "values", "table"] + ] = None, *, lines: bool = False, index: bool = True, - ) -> None: - return self.to_frame().to_json( - path_or_buf=path_or_buf, orient=orient, lines=lines, index=index - ) + ) -> Optional[str]: + if utils.is_gcs_path(path_or_buf): + return self.to_frame().to_json( + path_or_buf=path_or_buf, orient=orient, lines=lines, index=index + ) + else: + pd_series = self.to_pandas() + return pd_series.to_json( + path_or_buf=path_or_buf, orient=orient, lines=lines, index=index # type: ignore + ) def to_latex( self, buf=None, columns=None, header=True, index=True, **kwargs diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 67792b3a1d5..3a7eff621f1 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4125,6 +4125,72 @@ def test_df_to_latex(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result +def test_df_to_json_local_str(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.to_json() + # default_handler for arrow types that have no default conversion + pd_result = scalars_pandas_df_index.to_json(default_handler=str) + + assert bf_result == pd_result + + +@skip_legacy_pandas +def test_df_to_json_local_file(scalars_df_index, scalars_pandas_df_index): + with tempfile.TemporaryFile() as bf_result_file, tempfile.TemporaryFile() as pd_result_file: + scalars_df_index.to_json(bf_result_file, orient="table") + # default_handler for arrow types that have no default conversion + scalars_pandas_df_index.to_json( + pd_result_file, orient="table", default_handler=str + ) + + bf_result = bf_result_file.read() + pd_result = pd_result_file.read() + + assert bf_result == pd_result + + +def test_df_to_csv_local_str(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.to_csv() + # default_handler for arrow types that have no default conversion + pd_result = scalars_pandas_df_index.to_csv() + + assert bf_result == pd_result + + +def test_df_to_csv_local_file(scalars_df_index, scalars_pandas_df_index): + with tempfile.TemporaryFile() as bf_result_file, tempfile.TemporaryFile() as pd_result_file: + scalars_df_index.to_csv(bf_result_file) + scalars_pandas_df_index.to_csv(pd_result_file) + + bf_result = bf_result_file.read() + pd_result = pd_result_file.read() + + assert bf_result == pd_result + + +def test_df_to_parquet_local_bytes(scalars_df_index, scalars_pandas_df_index): + # GEOGRAPHY not supported in parquet export. + unsupported = ["geography_col"] + + bf_result = scalars_df_index.drop(columns=unsupported).to_parquet() + # default_handler for arrow types that have no default conversion + pd_result = scalars_pandas_df_index.drop(columns=unsupported).to_parquet() + + assert bf_result == pd_result + + +def test_df_to_parquet_local_file(scalars_df_index, scalars_pandas_df_index): + # GEOGRAPHY not supported in parquet export. + unsupported = ["geography_col"] + with tempfile.TemporaryFile() as bf_result_file, tempfile.TemporaryFile() as pd_result_file: + scalars_df_index.drop(columns=unsupported).to_parquet(bf_result_file) + scalars_pandas_df_index.drop(columns=unsupported).to_parquet(pd_result_file) + + bf_result = bf_result_file.read() + pd_result = pd_result_file.read() + + assert bf_result == pd_result + + def test_df_to_records(scalars_df_index, scalars_pandas_df_index): unsupported = ["numeric_col"] bf_result = scalars_df_index.drop(columns=unsupported).to_records() @@ -4166,7 +4232,7 @@ def test_df_to_pickle(scalars_df_index, scalars_pandas_df_index): scalars_df_index.to_pickle(bf_result_file) scalars_pandas_df_index.to_pickle(pd_result_file) bf_result = bf_result_file.read() - pd_result = bf_result_file.read() + pd_result = pd_result_file.read() assert bf_result == pd_result diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 10fcec63ce8..fe6e0017973 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2753,6 +2753,44 @@ def test_to_latex(scalars_df_index, scalars_pandas_df_index): assert bf_result == pd_result +def test_series_to_json_local_str(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.int64_col.to_json() + pd_result = scalars_pandas_df_index.int64_col.to_json() + + assert bf_result == pd_result + + +@skip_legacy_pandas +def test_series_to_json_local_file(scalars_df_index, scalars_pandas_df_index): + with tempfile.TemporaryFile() as bf_result_file, tempfile.TemporaryFile() as pd_result_file: + scalars_df_index.int64_col.to_json(bf_result_file) + scalars_pandas_df_index.int64_col.to_json(pd_result_file) + + bf_result = bf_result_file.read() + pd_result = pd_result_file.read() + + assert bf_result == pd_result + + +def test_series_to_csv_local_str(scalars_df_index, scalars_pandas_df_index): + bf_result = scalars_df_index.int64_col.to_csv() + # default_handler for arrow types that have no default conversion + pd_result = scalars_pandas_df_index.int64_col.to_csv() + + assert bf_result == pd_result + + +def test_series_to_csv_local_file(scalars_df_index, scalars_pandas_df_index): + with tempfile.TemporaryFile() as bf_result_file, tempfile.TemporaryFile() as pd_result_file: + scalars_df_index.int64_col.to_csv(bf_result_file) + scalars_pandas_df_index.int64_col.to_csv(pd_result_file) + + bf_result = bf_result_file.read() + pd_result = pd_result_file.read() + + assert bf_result == pd_result + + def test_to_dict(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index["int64_too"].to_dict() diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index f8088f8060a..7048d9c6dd4 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -476,11 +476,11 @@ def to_gbq( def to_parquet( self, - path: str, + path: Optional[str], *, compression: Optional[Literal["snappy", "gzip"]] = "snappy", index: bool = True, - ) -> None: + ) -> Optional[bytes]: """Write a DataFrame to the binary Parquet format. This function writes the dataframe as a `parquet file @@ -496,9 +496,13 @@ def to_parquet( >>> df.to_parquet(path=gcs_bucket) Args: - path (str): + path (str, path object, file-like object, or None, default None): + String, path object (implementing ``os.PathLike[str]``), or file-like + object implementing a binary ``write()`` function. If None, the result is + returned as bytes. If a string or path, it will be used as Root Directory + path when writing a partitioned dataset. Destination URI(s) of Cloud Storage files(s) to store the extracted dataframe - in format of ``gs:///``. + should be formatted ``gs:///``. If the data size is more than 1GB, you must use a wildcard to export the data into multiple files and the size of the files varies. @@ -511,7 +515,7 @@ def to_parquet( If ``False``, they will not be written to the file. Returns: - None. + bytes if no path argument is provided else None """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index 95302e51b2d..6734fb6aa94 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -210,14 +210,14 @@ def empty(self) -> bool: def to_json( self, - path_or_buf: str, - orient: Literal[ - "split", "records", "index", "columns", "values", "table" - ] = "columns", + path_or_buf, + orient: Optional[ + Literal["split", "records", "index", "columns", "values", "table"] + ] = None, *, index: bool = True, lines: bool = False, - ) -> None: + ) -> Optional[str]: """Convert the object to a JSON string, written to Cloud Storage. Note NaN's and None will be converted to null and datetime objects @@ -227,16 +227,18 @@ def to_json( Only ``orient='records'`` and ``lines=True`` is supported so far. Args: - path_or_buf (str): - A destination URI of Cloud Storage files(s) to store the extracted + path_or_buf (str, path object, file-like object, or None, default None): + String, path object (implementing os.PathLike[str]), or file-like + object implementing a write() function. If None, the result is + returned as a string. + + Can be a destination URI of Cloud Storage files(s) to store the extracted dataframe in format of ``gs:///``. Must contain a wildcard `*` character. If the data size is more than 1GB, you must use a wildcard to export the data into multiple files and the size of the files varies. - - None, file-like objects or local file paths not yet supported. orient ({`split`, `records`, `index`, `columns`, `values`, `table`}, default 'columns): Indication of expected JSON string format. @@ -271,17 +273,25 @@ def to_json( list-like. Returns: - None: String output not yet supported. + None or str: If path_or_buf is None, returns the resulting json format as a + string. Otherwise returns None. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) - def to_csv(self, path_or_buf: str, *, index: bool = True) -> None: + def to_csv(self, path_or_buf, *, index: bool = True) -> Optional[str]: """Write object to a comma-separated values (csv) file on Cloud Storage. Args: - path_or_buf (str): - A destination URI of Cloud Storage files(s) to store the extracted dataframe - in format of ``gs:///``. + path_or_buf (str, path object, file-like object, or None, default None): + String, path object (implementing os.PathLike[str]), or file-like + object implementing a write() function. If None, the result is + returned as a string. If a non-binary file object is passed, it should + be opened with `newline=''`, disabling universal newlines. If a binary + file object is passed, `mode` might need to contain a `'b'`. + + Alternatively, a destination URI of Cloud Storage files(s) to store the + extracted dataframe in format of + ``gs:///``. If the data size is more than 1GB, you must use a wildcard to export the data into multiple files and the size of the files @@ -293,7 +303,8 @@ def to_csv(self, path_or_buf: str, *, index: bool = True) -> None: If True, write row names (index). Returns: - None: String output not yet supported. + None or str: If path_or_buf is None, returns the resulting json format as a + string. Otherwise returns None. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From cbf2d42e4d961a7537381a9c3b28a8b463ad8f74 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 30 Jul 2024 13:00:49 -0700 Subject: [PATCH 729/921] fix: reduce redundant `remote_function` deployments (#856) * fix: reduce redundant `remote_function` deployments * do filename override in the naming rather than pickling * update documentation * update documentation --- bigframes/functions/remote_function.py | 29 ++- .../functions/remote_function_template.py | 18 +- bigframes/session/__init__.py | 16 +- .../remote_function_usecases.ipynb | 232 +++++++++--------- 4 files changed, 161 insertions(+), 134 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index b95067983fe..d84fbcdbabc 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -167,7 +167,23 @@ def get_remote_function_locations(bq_location): def _get_hash(def_, package_requirements=None): "Get hash (32 digits alphanumeric) of a function." - def_repr = cloudpickle.dumps(def_, protocol=_pickle_protocol_version) + # There is a known cell-id sensitivity of the cloudpickle serialization in + # notebooks https://github.com/cloudpipe/cloudpickle/issues/538. Because of + # this, if a cell contains a udf decorated with @remote_function, a unique + # cloudpickle code is generated every time the cell is run, creating new + # cloud artifacts every time. This is slow and wasteful. + # A workaround of the same can be achieved by replacing the filename in the + # code object to a static value + # https://github.com/cloudpipe/cloudpickle/issues/120#issuecomment-338510661. + # + # To respect the user code/environment let's make this modification on a + # copy of the udf, not on the original udf itself. + def_copy = cloudpickle.loads(cloudpickle.dumps(def_)) + def_copy.__code__ = def_copy.__code__.replace( + co_filename="bigframes_place_holder_filename" + ) + + def_repr = cloudpickle.dumps(def_copy, protocol=_pickle_protocol_version) if package_requirements: for p in sorted(package_requirements): def_repr += p.encode() @@ -877,11 +893,16 @@ def remote_function( dynamically using the `bigquery_connection_client` assuming the user has necessary priviliges. The PROJECT_ID should be the same as the BigQuery connection project. reuse (bool, Optional): - Reuse the remote function if is already exists. - `True` by default, which results in reusing an existing remote + Reuse the remote function if already exists. + `True` by default, which will result in reusing an existing remote function and corresponding cloud function (if any) that was previously created for the same udf. - Setting it to `False` forces the creation of a unique remote function. + Please note that for an unnamed (i.e. created without an explicit + `name` argument) remote function, the BigQuery DataFrames + session id is attached in the cloud artifacts names. So for the + effective reuse across the sessions it is recommended to create + the remote function with an explicit `name`. + Setting it to `False` would force creating a unique remote function. If the required remote function does not exist then it would be created irrespective of this param. name (str, Optional): diff --git a/bigframes/functions/remote_function_template.py b/bigframes/functions/remote_function_template.py index 68fe1b917dc..c666f41daa4 100644 --- a/bigframes/functions/remote_function_template.py +++ b/bigframes/functions/remote_function_template.py @@ -215,9 +215,9 @@ def udf_http_row_processor(request): def generate_udf_code(def_, directory): - """Generate serialized bytecode using cloudpickle given a udf.""" + """Generate serialized code using cloudpickle given a udf.""" udf_code_file_name = "udf.py" - udf_bytecode_file_name = "udf.cloudpickle" + udf_pickle_file_name = "udf.cloudpickle" # original code, only for debugging purpose udf_code = textwrap.dedent(inspect.getsource(def_)) @@ -225,13 +225,13 @@ def generate_udf_code(def_, directory): with open(udf_code_file_path, "w") as f: f.write(udf_code) - # serialized bytecode - udf_bytecode_file_path = os.path.join(directory, udf_bytecode_file_name) + # serialized udf + udf_pickle_file_path = os.path.join(directory, udf_pickle_file_name) # TODO(b/345433300): try io.BytesIO to avoid writing to the file system - with open(udf_bytecode_file_path, "wb") as f: + with open(udf_pickle_file_path, "wb") as f: cloudpickle.dump(def_, f, protocol=_pickle_protocol_version) - return udf_code_file_name, udf_bytecode_file_name + return udf_code_file_name, udf_pickle_file_name def generate_cloud_function_main_code( @@ -252,15 +252,15 @@ def generate_cloud_function_main_code( """ # Pickle the udf with all its dependencies - udf_code_file, udf_bytecode_file = generate_udf_code(def_, directory) + udf_code_file, udf_pickle_file = generate_udf_code(def_, directory) code_blocks = [ f"""\ import cloudpickle # original udf code is in {udf_code_file} -# serialized udf code is in {udf_bytecode_file} -with open("{udf_bytecode_file}", "rb") as f: +# serialized udf code is in {udf_pickle_file} +with open("{udf_pickle_file}", "rb") as f: udf = cloudpickle.load(f) input_types = {repr(input_types)} diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 22ca63d25b0..dfec83a56a4 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1629,15 +1629,21 @@ def remote_function( `True` by default, which will result in reusing an existing remote function and corresponding cloud function (if any) that was previously created for the same udf. + Please note that for an unnamed (i.e. created without an explicit + `name` argument) remote function, the BigQuery DataFrames + session id is attached in the cloud artifacts names. So for the + effective reuse across the sessions it is recommended to create + the remote function with an explicit `name`. Setting it to `False` would force creating a unique remote function. If the required remote function does not exist then it would be created irrespective of this param. name (str, Optional): - Explicit name of the persisted BigQuery remote function. Use it with - caution, because two users working in the same project and dataset - could overwrite each other's remote functions if they use the same - persistent name. When an explicit name is provided, any session - specific clean up (``bigframes.session.Session.close``/ + Explicit name of the persisted BigQuery remote function. Use it + with caution, because more than one users working in the same + project and dataset could overwrite each other's remote + functions if they use the same persistent name. When an explicit + name is provided, any session specific clean up ( + ``bigframes.session.Session.close``/ ``bigframes.pandas.close_session``/ ``bigframes.pandas.reset_session``/ ``bigframes.pandas.clean_up_by_session_id``) does not clean up diff --git a/notebooks/remote_functions/remote_function_usecases.ipynb b/notebooks/remote_functions/remote_function_usecases.ipynb index 3d7ae3e8c72..9317e4b8fe1 100644 --- a/notebooks/remote_functions/remote_function_usecases.ipynb +++ b/notebooks/remote_functions/remote_function_usecases.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 28, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -30,7 +30,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 21, "metadata": { "id": "Y6QAttCqqMM0" }, @@ -41,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 22, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -55,14 +55,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/shobs/code/bigframes/venv/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3550: UserWarning: Reading cached table from 2024-06-28 02:49:31.716256+00:00 to avoid incompatibilies with previous reads of this table. To read the latest version, set `use_cache=False` or close the current session with Session.close() or bigframes.pandas.close_session().\n", + "/usr/local/google/home/shobs/code/bigframes1/venv/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3550: UserWarning: Reading cached table from 2024-07-24 08:01:12.491984+00:00 to avoid incompatibilies with previous reads of this table. To read the latest version, set `use_cache=False` or close the current session with Session.close() or bigframes.pandas.close_session().\n", " exec(code_obj, self.user_global_ns, self.user_ns)\n" ] }, { "data": { "text/html": [ - "Query job f72cda67-2a96-4cd2-a624-591c0d540fc9 is DONE. 582.8 kB processed. Open Job" + "Query job 9d155f10-e37a-4d20-b2ff-02868ecb58f4 is DONE. 582.8 kB processed. Open Job" ], "text/plain": [ "" @@ -74,7 +74,7 @@ { "data": { "text/html": [ - "Query job 65cf6ca3-73f0-49e6-84a8-1ff79af6ec75 is DONE. 82.0 kB processed. Open Job" + "Query job 5a524e70-12dc-4116-b416-04570bbf754e is DONE. 82.0 kB processed. Open Job" ], "text/plain": [ "" @@ -111,49 +111,49 @@ " \n", " \n", " \n", - " 50\n", - " Rays\n", - " Rangers\n", - " 181\n", + " 36\n", + " Reds\n", + " Cubs\n", + " 159\n", " \n", " \n", - " 72\n", - " Phillies\n", - " Pirates\n", - " 192\n", + " 358\n", + " Dodgers\n", + " Diamondbacks\n", + " 223\n", " \n", " \n", - " 89\n", - " Mariners\n", - " Blue Jays\n", - " 183\n", + " 416\n", + " Yankees\n", + " White Sox\n", + " 216\n", " \n", " \n", - " 351\n", - " Astros\n", - " Angels\n", - " 212\n", + " 523\n", + " Rays\n", + " Athletics\n", + " 187\n", " \n", " \n", - " 382\n", - " Royals\n", - " Yankees\n", - " 259\n", + " 594\n", + " Pirates\n", + " Brewers\n", + " 169\n", " \n", " \n", "\n", "" ], "text/plain": [ - " homeTeamName awayTeamName duration_minutes\n", - "50 Rays Rangers 181\n", - "72 Phillies Pirates 192\n", - "89 Mariners Blue Jays 183\n", - "351 Astros Angels 212\n", - "382 Royals Yankees 259" + " homeTeamName awayTeamName duration_minutes\n", + "36 Reds Cubs 159\n", + "358 Dodgers Diamondbacks 223\n", + "416 Yankees White Sox 216\n", + "523 Rays Athletics 187\n", + "594 Pirates Brewers 169" ] }, - "execution_count": 30, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -202,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 23, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -215,7 +215,7 @@ { "data": { "text/html": [ - "Query job f039d478-8dc4-4b60-8eda-179955e06586 is DONE. 0 Bytes processed. Open Job" + "Query job ec8d958d-93ef-45ae-8150-6ccfa8feb89a is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -228,7 +228,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-862150459da5240a6df1ce01c59b32d8-em4ibov0' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_862150459da5240a6df1ce01c59b32d8_em4ibov0'.\n" + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-session54c8b0-e22dbecc9ec0374bda36bc23df3775b0-g8zp' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_session54c8b0_e22dbecc9ec0374bda36bc23df3775b0_g8zp'.\n" ] } ], @@ -247,7 +247,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 24, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -260,7 +260,7 @@ { "data": { "text/html": [ - "Query job 23e95831-d913-4d2b-97f6-588fc7967455 is DONE. 58.3 kB processed. Open Job" + "Query job 4b116e3e-d4d3-4eb6-9764-0a29a7c5d036 is DONE. 58.3 kB processed. Open Job" ], "text/plain": [ "" @@ -272,7 +272,7 @@ { "data": { "text/html": [ - "Query job bb8b3d13-a521-4d45-b4c8-5686c944a9f2 is DONE. 157.2 kB processed. Open Job" + "Query job d62ac4f0-47c9-47ae-8611-c9ecf78f20c9 is DONE. 157.2 kB processed. Open Job" ], "text/plain": [ "" @@ -284,7 +284,7 @@ { "data": { "text/html": [ - "Query job 2a4653f5-cc6b-4279-a45e-40f0f97090a7 is DONE. 98.8 kB processed. Open Job" + "Query job 5f876ebb-2d95-4c68-9d84-947e02b37bad is DONE. 98.8 kB processed. Open Job" ], "text/plain": [ "" @@ -369,7 +369,7 @@ "654 Astros Angels 143 medium" ] }, - "execution_count": 32, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 25, "metadata": { "id": "2UEmTbu4znyS" }, @@ -409,7 +409,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 26, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -422,7 +422,7 @@ { "data": { "text/html": [ - "Query job 5d914fde-81ec-46eb-9219-9822f77dd9a2 is DONE. 0 Bytes processed. Open Job" + "Query job 1909a652-5735-401b-8a77-674d8539ded0 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -435,7 +435,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-f3231b74ec807496f4894218d5d40ed5-688mx7hi' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_f3231b74ec807496f4894218d5d40ed5_688mx7hi'.\n" + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-session54c8b0-4191f0fce98d46cc09359de47e203236-e009' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_session54c8b0_4191f0fce98d46cc09359de47e203236_e009'.\n" ] } ], @@ -454,7 +454,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 27, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -467,7 +467,7 @@ { "data": { "text/html": [ - "Query job b0b39944-1e69-4185-97ba-985178ee241f is DONE. 58.3 kB processed. Open Job" + "Query job a942bdc5-6a6d-4db8-b2aa-a556197377b3 is DONE. 58.3 kB processed. Open Job" ], "text/plain": [ "" @@ -479,7 +479,7 @@ { "data": { "text/html": [ - "Query job 90d99515-eb5e-4bcd-bce5-292eea09770e is DONE. 147.7 kB processed. Open Job" + "Query job 175ae9d3-604f-495b-a167-8b06c0283bd2 is DONE. 147.7 kB processed. Open Job" ], "text/plain": [ "" @@ -491,7 +491,7 @@ { "data": { "text/html": [ - "Query job eb31d033-c871-49c5-a75e-4427e376516f is DONE. 89.3 kB processed. Open Job" + "Query job d331a785-e574-45c9-86c8-d29ddd79a4d1 is DONE. 89.3 kB processed. Open Job" ], "text/plain": [ "" @@ -576,7 +576,7 @@ "654 Astros Angels 143 M" ] }, - "execution_count": 35, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -607,7 +607,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 28, "metadata": { "id": "zlQfhcW41uzM" }, @@ -618,7 +618,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 29, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -631,7 +631,7 @@ { "data": { "text/html": [ - "Query job 2895676f-d15c-40fd-8cf2-3a0436291e6b is DONE. 0 Bytes processed. Open Job" + "Query job bbc0b78f-bc04-4bd5-b711-399786a51519 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -644,7 +644,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-9b20b0257558a42da610d8998022c25e-7k62x9l6' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_9b20b0257558a42da610d8998022c25e_7k62x9l6'.\n" + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-session54c8b0-cf31fc2d2c7fe111afa5526f5a9cdf06-gmmo' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_session54c8b0_cf31fc2d2c7fe111afa5526f5a9cdf06_gmmo'.\n" ] } ], @@ -659,7 +659,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 30, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -672,7 +672,7 @@ { "data": { "text/html": [ - "Query job 4efda755-2f54-4477-b48a-4a424c888559 is DONE. 58.3 kB processed. Open Job" + "Query job 991b54ed-9eaa-450f-9208-3e73404bb112 is DONE. 58.3 kB processed. Open Job" ], "text/plain": [ "" @@ -684,7 +684,7 @@ { "data": { "text/html": [ - "Query job a8992776-c2e8-4c3e-ab75-dfc01c5de89f is DONE. 150.1 kB processed. Open Job" + "Query job 4e464a58-ac5b-42fd-91e3-92c115bdd273 is DONE. 150.1 kB processed. Open Job" ], "text/plain": [ "" @@ -696,7 +696,7 @@ { "data": { "text/html": [ - "Query job 3ea299b0-27ad-432b-8dbf-81da3aae884f is DONE. 91.7 kB processed. Open Job" + "Query job d340f55d-1511-431a-970d-a70ed4356935 is DONE. 91.7 kB processed. Open Job" ], "text/plain": [ "" @@ -781,7 +781,7 @@ "654 Astros Angels 143 3h" ] }, - "execution_count": 38, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -812,7 +812,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 31, "metadata": { "id": "0G91fWiF3pKg" }, @@ -829,7 +829,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 32, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -842,7 +842,7 @@ { "data": { "text/html": [ - "Query job 411853db-bf83-4df8-af78-55b1ceb39cb1 is DONE. 0 Bytes processed. Open Job" + "Query job 10d1afa3-349b-49a8-adbd-79a8309ce77c is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -855,7 +855,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-b54aa0aa752af6a3bd6d9d529dac373b-h4lgpy4y' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_b54aa0aa752af6a3bd6d9d529dac373b_h4lgpy4y'.\n" + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-session54c8b0-3c03836c2044bf625d02e25ccdbfe101-k1m4' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_session54c8b0_3c03836c2044bf625d02e25ccdbfe101_k1m4'.\n" ] } ], @@ -870,7 +870,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 33, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -883,7 +883,7 @@ { "data": { "text/html": [ - "Query job d04abfa5-e2f2-4936-a708-ed97ef429df3 is DONE. 58.3 kB processed. Open Job" + "Query job 33aff336-48d6-4caa-8cae-f459d21b180e is DONE. 58.3 kB processed. Open Job" ], "text/plain": [ "" @@ -895,7 +895,7 @@ { "data": { "text/html": [ - "Query job 2fc4edf0-7a86-4532-b8fb-bd3f5d153dcb is DONE. 157.4 kB processed. Open Job" + "Query job 561e0aa7-3962-4ef3-b308-a117a0ac3a7d is DONE. 157.4 kB processed. Open Job" ], "text/plain": [ "" @@ -907,7 +907,7 @@ { "data": { "text/html": [ - "Query job f7e6e18c-70d7-4b4e-926a-03b3a1abd1fe is DONE. 99.0 kB processed. Open Job" + "Query job 759dccf8-3d88-40e1-a38a-2a2064e1d269 is DONE. 99.0 kB processed. Open Job" ], "text/plain": [ "" @@ -992,7 +992,7 @@ "654 Astros Angels 143 3 hrs" ] }, - "execution_count": 41, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -1018,7 +1018,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 34, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -1031,7 +1031,7 @@ { "data": { "text/html": [ - "Query job c674e7b7-2349-4317-8f08-8bfd9aa99785 is DONE. 0 Bytes processed. Open Job" + "Query job e2a44878-2564-44a5-8dec-b7ea2f42afd4 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1057,7 +1057,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 35, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -1070,7 +1070,7 @@ { "data": { "text/html": [ - "Query job eb9384c9-de7d-4232-bdca-94b61b50ff89 is DONE. 60.5 kB processed. Open Job" + "Query job bcfab000-ca19-4633-bf0e-45e7d053f3eb is DONE. 60.5 kB processed. Open Job" ], "text/plain": [ "" @@ -1082,7 +1082,7 @@ { "data": { "text/html": [ - "Query job 11a736a5-96d1-4e62-90e2-576156131a94 is DONE. 388.3 kB processed. Open Job" + "Query job 139a6449-c07e-41ff-9aed-c6fdd633740a is DONE. 388.3 kB processed. Open Job" ], "text/plain": [ "" @@ -1094,7 +1094,7 @@ { "data": { "text/html": [ - "Query job c66a9ad1-60f7-4af1-ad7c-65e4eecbb035 is DONE. 330.0 kB processed. Open Job" + "Query job 035fa2fb-0a55-4358-bb50-3ef915f5bf54 is DONE. 330.0 kB processed. Open Job" ], "text/plain": [ "" @@ -1132,61 +1132,61 @@ " \n", " \n", " \n", - " 719\n", - " Astros\n", - " Angels\n", - " 180\n", - " gAAAAABmflbKCFygsmoTzFkUCObFSBJG29Ksk8HEtk82ib...\n", + " 641\n", + " American League\n", + " National League\n", + " 185\n", + " gAAAAABmo0n2I391cbYwIYeg8lyJq1MSFZatrtpvuUD5v-...\n", " \n", " \n", - " 2295\n", - " Astros\n", + " 349\n", " Angels\n", - " 204\n", - " gAAAAABmflbKv-XzIxcNS92RO4fXYIAwA0kGWsAy-tI5fm...\n", + " Astros\n", + " 187\n", + " gAAAAABmo0n2pX-siRwl2tIZA4m--swndC_b7vgGXrqSNM...\n", " \n", " \n", - " 1126\n", - " Astros\n", + " 2349\n", " Angels\n", - " 176\n", - " gAAAAABmflbJdjgpqnfvmklU7Zg3NJUqlTMYMs44dLEkwg...\n", + " Astros\n", + " 160\n", + " gAAAAABmo0n28Q9RwH62HvYRhTDpQ9lo8c6G8F5bnn7wgF...\n", " \n", " \n", - " 294\n", - " Astros\n", + " 557\n", " Angels\n", - " 189\n", - " gAAAAABmflbKmfBh4P3FnwyiIpVFek9TzF4GzwP_5rQmkv...\n", + " Astros\n", + " 166\n", + " gAAAAABmo0n2YlwHlSGQ0_XvXd-QVBtB_Lq2zUifu7vKhg...\n", " \n", " \n", - " 351\n", - " Astros\n", + " 220\n", " Angels\n", - " 212\n", - " gAAAAABmflbJ_mzqao9i7BtoYlMpb6y3bV3x7-cYuWGxsT...\n", + " Astros\n", + " 162\n", + " gAAAAABmo0n2l8HMSGKYizxfEmRvGQy96mrjwx734-Rl_Z...\n", " \n", " \n", "\n", "" ], "text/plain": [ - " homeTeamName awayTeamName duration_minutes \\\n", - "719 Astros Angels 180 \n", - "2295 Astros Angels 204 \n", - "1126 Astros Angels 176 \n", - "294 Astros Angels 189 \n", - "351 Astros Angels 212 \n", + " homeTeamName awayTeamName duration_minutes \\\n", + "641 American League National League 185 \n", + "349 Angels Astros 187 \n", + "2349 Angels Astros 160 \n", + "557 Angels Astros 166 \n", + "220 Angels Astros 162 \n", "\n", " homeTeamNameRedacted \n", - "719 gAAAAABmflbKCFygsmoTzFkUCObFSBJG29Ksk8HEtk82ib... \n", - "2295 gAAAAABmflbKv-XzIxcNS92RO4fXYIAwA0kGWsAy-tI5fm... \n", - "1126 gAAAAABmflbJdjgpqnfvmklU7Zg3NJUqlTMYMs44dLEkwg... \n", - "294 gAAAAABmflbKmfBh4P3FnwyiIpVFek9TzF4GzwP_5rQmkv... \n", - "351 gAAAAABmflbJ_mzqao9i7BtoYlMpb6y3bV3x7-cYuWGxsT... " + "641 gAAAAABmo0n2I391cbYwIYeg8lyJq1MSFZatrtpvuUD5v-... \n", + "349 gAAAAABmo0n2pX-siRwl2tIZA4m--swndC_b7vgGXrqSNM... \n", + "2349 gAAAAABmo0n28Q9RwH62HvYRhTDpQ9lo8c6G8F5bnn7wgF... \n", + "557 gAAAAABmo0n2YlwHlSGQ0_XvXd-QVBtB_Lq2zUifu7vKhg... \n", + "220 gAAAAABmo0n2l8HMSGKYizxfEmRvGQy96mrjwx734-Rl_Z... " ] }, - "execution_count": 43, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -1211,7 +1211,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 36, "metadata": {}, "outputs": [], "source": [ @@ -1221,13 +1221,13 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 21b054a9-8fb2-418f-a17b-effdf5aba9b5 is DONE. 0 Bytes processed. Open Job" + "Query job af73ab2d-8d88-4cbe-863f-d35e48af84e1 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1240,7 +1240,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-0879f72acd9b8ede460b69c5a8cc0dcb-edxlst27' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_0879f72acd9b8ede460b69c5a8cc0dcb_edxlst27'.\n" + "Created cloud function 'projects/bigframes-dev/locations/us-central1/functions/bigframes-session54c8b0-a5e21a4ad488ce8b90de19c3c8cd33b6-0ab2' and BQ remote function 'bigframes-dev._1b6c31ff1bcd5d2f6d86833cf8268317f1b12d57.bigframes_session54c8b0_a5e21a4ad488ce8b90de19c3c8cd33b6_0ab2'.\n" ] } ], @@ -1255,13 +1255,13 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job d67b7cb9-9813-4863-99d1-01cf45ab4949 is DONE. 58.3 kB processed. Open Job" + "Query job 0a9ac329-619d-4303-8dbd-176a576d4ce8 is DONE. 58.3 kB processed. Open Job" ], "text/plain": [ "" @@ -1273,7 +1273,7 @@ { "data": { "text/html": [ - "Query job 579ba853-a7b8-49df-9539-bf22f08d2370 is DONE. 162.2 kB processed. Open Job" + "Query job 456bb9b4-0576-4c04-b707-4a04496aa538 is DONE. 162.2 kB processed. Open Job" ], "text/plain": [ "" @@ -1285,7 +1285,7 @@ { "data": { "text/html": [ - "Query job 72f9eb5d-1c1a-4ce8-8f2f-1f5a8f7cec99 is DONE. 103.9 kB processed. Open Job" + "Query job 37f59939-5d2c-4fb1-839b-282ae3702d3d is DONE. 103.9 kB processed. Open Job" ], "text/plain": [ "" @@ -1370,7 +1370,7 @@ "654 Astros Angels 143 2 hours" ] }, - "execution_count": 46, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } From a7d7197a32c55b989ae4ea8f6cf6e1c0f7184cd4 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:53:53 -0700 Subject: [PATCH 730/921] feat: add streaming.StreamingDataFrame class (#864) * feat: add StreamingDataFrame support * use setattr for properties * fix bug * read session from DF * fix docs and tests * fix test * add preview warning * resolve comments * move to streaming.read_gbq_table, add logger * fix unit test * fix doc test * update notebook * add back preview warning --- bigframes/core/blocks.py | 6 +- bigframes/dataframe.py | 8 +- bigframes/session/__init__.py | 51 +- bigframes/streaming/__init__.py | 256 +-------- bigframes/streaming/dataframe.py | 504 +++++++++++++++++ notebooks/streaming/streaming_dataframe.ipynb | 535 ++++++++++++++++++ tests/system/conftest.py | 8 + tests/system/large/test_streaming.py | 38 +- tests/unit/test_pandas.py | 2 + 9 files changed, 1134 insertions(+), 274 deletions(-) create mode 100644 bigframes/streaming/dataframe.py create mode 100644 notebooks/streaming/streaming_dataframe.ipynb diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index fd0c9c9539c..1b7b2314032 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2354,7 +2354,7 @@ def is_monotonic_decreasing( return self._is_monotonic(column_id, increasing=False) def to_sql_query( - self, include_index: bool + self, include_index: bool, enable_cache: bool = True ) -> typing.Tuple[str, list[str], list[Label]]: """ Compiles this DataFrame's expression tree to SQL, optionally @@ -2388,7 +2388,9 @@ def to_sql_query( # the BigQuery unicode column name feature? substitutions[old_id] = new_id - sql = self.session._to_sql(array_value, col_id_overrides=substitutions) + sql = self.session._to_sql( + array_value, col_id_overrides=substitutions, enable_cache=enable_cache + ) return ( sql, new_ids[: len(idx_labels)], diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 5d4918c3ceb..9789c7cf9f1 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -105,6 +105,8 @@ def guarded_meth(df: DataFrame, *args, **kwargs): @log_adapter.class_logger class DataFrame(vendored_pandas_frame.DataFrame): __doc__ = vendored_pandas_frame.DataFrame.__doc__ + # internal flag to disable cache at all + _disable_cache_override: bool = False def __init__( self, @@ -367,7 +369,7 @@ def astype( return self._apply_unary_op(ops.AsTypeOp(to_type=dtype)) def _to_sql_query( - self, include_index: bool + self, include_index: bool, enable_cache: bool = True ) -> Tuple[str, list[str], list[blocks.Label]]: """Compiles this DataFrame's expression tree to SQL, optionally including index columns. @@ -381,7 +383,7 @@ def _to_sql_query( If include_index is set to False, index_column_id_list and index_column_label_list return empty lists. """ - return self._block.to_sql_query(include_index) + return self._block.to_sql_query(include_index, enable_cache=enable_cache) @property def sql(self) -> str: @@ -3628,6 +3630,8 @@ def _cached(self, *, force: bool = False) -> DataFrame: No-op if the dataframe represents a trivial transformation of an existing materialization. Force=True is used for BQML integration where need to copy data rather than use snapshot. """ + if self._disable_cache_override: + return self self._block.cached(force=force) return self diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index dfec83a56a4..98cba867f23 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -107,6 +107,7 @@ import bigframes.core.indexes import bigframes.dataframe as dataframe import bigframes.series + import bigframes.streaming.dataframe as streaming_dataframe _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" @@ -749,6 +750,38 @@ def read_gbq_table( filters=filters, ) + def read_gbq_table_streaming( + self, table: str + ) -> streaming_dataframe.StreamingDataFrame: + """Turn a BigQuery table into a StreamingDataFrame. + + Note: The bigframes.streaming module is a preview feature, and subject to change. + + **Examples:** + + >>> import bigframes.streaming as bst + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> sdf = bst.read_gbq_table("bigquery-public-data.ml_datasets.penguins") + """ + warnings.warn( + "The bigframes.streaming module is a preview feature, and subject to change.", + stacklevel=1, + category=bigframes.exceptions.PreviewWarning, + ) + + import bigframes.streaming.dataframe as streaming_dataframe + + df = self._read_gbq_table( + table, + api_name="read_gbq_table_steaming", + enable_snapshot=False, + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + return streaming_dataframe.StreamingDataFrame._from_table_df(df) + def _read_gbq_table( self, query: str, @@ -759,6 +792,7 @@ def _read_gbq_table( api_name: str, use_cache: bool = True, filters: third_party_pandas_gbq.FiltersType = (), + enable_snapshot: bool = True, ) -> dataframe.DataFrame: import bigframes.dataframe as dataframe @@ -877,7 +911,7 @@ def _read_gbq_table( else (*columns, *[col for col in index_cols if col not in columns]) ) - supports_snapshot = bf_read_gbq_table.validate_table( + enable_snapshot = enable_snapshot and bf_read_gbq_table.validate_table( self.bqclient, table_ref, all_columns, time_travel_timestamp, filter_str ) @@ -905,7 +939,7 @@ def _read_gbq_table( table, schema=schema, predicate=filter_str, - at_time=time_travel_timestamp if supports_snapshot else None, + at_time=time_travel_timestamp if enable_snapshot else None, primary_key=index_cols if is_index_unique else (), session=self, ) @@ -2056,17 +2090,20 @@ def _to_sql( offset_column: typing.Optional[str] = None, col_id_overrides: typing.Mapping[str, str] = {}, ordered: bool = False, + enable_cache: bool = True, ) -> str: if offset_column: array_value = array_value.promote_offsets(offset_column) - node_w_cached = self._with_cached_executions(array_value.node) + node = ( + self._with_cached_executions(array_value.node) + if enable_cache + else array_value.node + ) if ordered: return self._compiler.compile_ordered( - node_w_cached, col_id_overrides=col_id_overrides + node, col_id_overrides=col_id_overrides ) - return self._compiler.compile_unordered( - node_w_cached, col_id_overrides=col_id_overrides - ) + return self._compiler.compile_unordered(node, col_id_overrides=col_id_overrides) def _get_table_size(self, destination_table): table = self.bqclient.get_table(destination_table) diff --git a/bigframes/streaming/__init__.py b/bigframes/streaming/__init__.py index 0b6fd18561e..66f345f0ab9 100644 --- a/bigframes/streaming/__init__.py +++ b/bigframes/streaming/__init__.py @@ -12,253 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for bigquery continuous queries""" +import inspect -import json -from typing import Optional -import warnings +import bigframes.core.global_session as global_session +import bigframes.pandas as bpd +import bigframes.session +import bigframes.streaming.dataframe as streaming_dataframe -from google.cloud import bigquery -import bigframes - - -def to_bigtable( - query: str, - *, - instance: str, - table: str, - service_account_email: Optional[str] = None, - session: Optional[bigframes.Session] = None, - app_profile: Optional[str] = None, - truncate: bool = False, - overwrite: bool = False, - auto_create_column_families: bool = False, - bigtable_options: Optional[dict] = None, - job_id: Optional[str] = None, - job_id_prefix: Optional[str] = None, -) -> bigquery.QueryJob: - """Launches a BigQuery continuous query and returns a - QueryJob object for some management functionality. - - This method requires an existing bigtable preconfigured to - accept the continuous query export statement. For instructions - on export to bigtable, see - https://cloud.google.com/bigquery/docs/export-to-bigtable. - - Args: - query (str): - The sql statement to execute as a continuous function. - For example: "SELECT * FROM dataset.table" - This will be wrapped in an EXPORT DATA statement to - launch a continuous query writing to bigtable. - instance (str): - The name of the bigtable instance to export to. - table (str): - The name of the bigtable table to export to. - service_account_email (str): - Full name of the service account to run the continuous query. - Example: accountname@projectname.gserviceaccounts.com - If not provided, the user account will be used, but this - limits the lifetime of the continuous query. - session (bigframes.Session, default None): - The session object to use for the query. This determines - the project id and location of the query. If None, will - default to the bigframes global session. - app_profile (str, default None): - The bigtable app profile to export to. If None, no app - profile will be used. - truncate (bool, default False): - The export truncate option, see - https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option - overwrite (bool, default False): - The export overwrite option, see - https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option - auto_create_column_families (bool, default False): - The auto_create_column_families option, see - https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option - bigtable_options (dict, default None): - The bigtable options dict, which will be converted to JSON - using json.dumps, see - https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option - If None, no bigtable_options parameter will be passed. - job_id (str, default None): - If specified, replace the default job id for the query, - see job_id parameter of - https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query - job_id_prefix (str, default None): - If specified, a job id prefix for the query, see - job_id_prefix parameter of - https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query - - Returns: - google.cloud.bigquery.QueryJob: - See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob - The ongoing query job can be managed using this object. - For example, the job can be cancelled or its error status - can be examined. - """ - warnings.warn( - "The bigframes.streaming module is a preview feature, and subject to change.", - stacklevel=1, - category=bigframes.exceptions.PreviewWarning, - ) - - # get default client if not passed - if session is None: - session = bigframes.get_global_session() - bq_client = session.bqclient - - # build export string from parameters - project = bq_client.project - - app_profile_url_string = "" - if app_profile is not None: - app_profile_url_string = f"appProfiles/{app_profile}/" - - bigtable_options_parameter_string = "" - if bigtable_options is not None: - bigtable_options_parameter_string = ( - 'bigtable_options = """' + json.dumps(bigtable_options) + '""",\n' - ) - - sql = ( - "EXPORT DATA\n" - "OPTIONS (\n" - "format = 'CLOUD_BIGTABLE',\n" - f"{bigtable_options_parameter_string}" - f"truncate = {str(truncate)},\n" - f"overwrite = {str(overwrite)},\n" - f"auto_create_column_families = {str(auto_create_column_families)},\n" - f'uri = "https://bigtable.googleapis.com/projects/{project}/instances/{instance}/{app_profile_url_string}tables/{table}"\n' - ")\n" - "AS (\n" - f"{query});" - ) - - # override continuous http parameter - job_config = bigquery.job.QueryJobConfig() - - job_config_dict: dict = {"query": {"continuous": True}} - if service_account_email is not None: - job_config_dict["query"]["connectionProperties"] = { - "key": "service_account", - "value": service_account_email, - } - job_config_filled = job_config.from_api_repr(job_config_dict) - job_config_filled.labels = {"bigframes-api": "streaming_to_bigtable"} - - # begin the query job - query_job = bq_client.query( - sql, - job_config=job_config_filled, # type:ignore - # typing error above is in bq client library - # (should accept abstract job_config, only takes concrete) - job_id=job_id, - job_id_prefix=job_id_prefix, - ) - - # return the query job to the user for lifetime management - return query_job - - -def to_pubsub( - query: str, - *, - topic: str, - service_account_email: str, - session: Optional[bigframes.Session] = None, - job_id: Optional[str] = None, - job_id_prefix: Optional[str] = None, -) -> bigquery.QueryJob: - """Launches a BigQuery continuous query and returns a - QueryJob object for some management functionality. - - This method requires an existing pubsub topic. For instructions - on creating a pubsub topic, see - https://cloud.google.com/pubsub/docs/samples/pubsub-quickstart-create-topic?hl=en - - Note that a service account is a requirement for continuous queries - exporting to pubsub. - - Args: - query (str): - The sql statement to execute as a continuous function. - For example: "SELECT * FROM dataset.table" - This will be wrapped in an EXPORT DATA statement to - launch a continuous query writing to pubsub. - topic (str): - The name of the pubsub topic to export to. - For example: "taxi-rides" - service_account_email (str): - Full name of the service account to run the continuous query. - Example: accountname@projectname.gserviceaccounts.com - session (bigframes.Session, default None): - The session object to use for the query. This determines - the project id and location of the query. If None, will - default to the bigframes global session. - job_id (str, default None): - If specified, replace the default job id for the query, - see job_id parameter of - https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query - job_id_prefix (str, default None): - If specified, a job id prefix for the query, see - job_id_prefix parameter of - https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query - - Returns: - google.cloud.bigquery.QueryJob: - See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob - The ongoing query job can be managed using this object. - For example, the job can be cancelled or its error status - can be examined. - """ - warnings.warn( - "The bigframes.streaming module is a preview feature, and subject to change.", - stacklevel=1, - category=bigframes.exceptions.PreviewWarning, +def read_gbq_table(table: str) -> streaming_dataframe.StreamingDataFrame: + bpd._set_default_session_location_if_possible(table) + return global_session.with_default_session( + bigframes.session.Session.read_gbq_table_streaming, table ) - # get default client if not passed - if session is None: - session = bigframes.get_global_session() - bq_client = session.bqclient - - # build export string from parameters - sql = ( - "EXPORT DATA\n" - "OPTIONS (\n" - "format = 'CLOUD_PUBSUB',\n" - f'uri = "https://pubsub.googleapis.com/projects/{bq_client.project}/topics/{topic}"\n' - ")\n" - "AS (\n" - f"{query});" - ) - # override continuous http parameter - job_config = bigquery.job.QueryJobConfig() - job_config_filled = job_config.from_api_repr( - { - "query": { - "continuous": True, - "connectionProperties": { - "key": "service_account", - "value": service_account_email, - }, - } - } - ) - job_config_filled.labels = {"bigframes-api": "streaming_to_pubsub"} - - # begin the query job - query_job = bq_client.query( - sql, - job_config=job_config_filled, # type:ignore - # typing error above is in bq client library - # (should accept abstract job_config, only takes concrete) - job_id=job_id, - job_id_prefix=job_id_prefix, - ) +read_gbq_table.__doc__ = inspect.getdoc( + bigframes.session.Session.read_gbq_table_streaming +) - # return the query job to the user for lifetime management - return query_job +StreamingDataFrame = streaming_dataframe.StreamingDataFrame diff --git a/bigframes/streaming/dataframe.py b/bigframes/streaming/dataframe.py new file mode 100644 index 00000000000..64a4898c57c --- /dev/null +++ b/bigframes/streaming/dataframe.py @@ -0,0 +1,504 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for bigquery continuous queries""" +from __future__ import annotations + +import functools +import inspect +import json +from typing import Optional +import warnings + +from google.cloud import bigquery + +import bigframes +from bigframes import dataframe +from bigframes.core import log_adapter + + +def _return_type_wrapper(method, cls): + @functools.wraps(method) + def wrapper(*args, **kwargs): + return_value = method(*args, **kwargs) + if isinstance(return_value, dataframe.DataFrame): + return cls._from_table_df(return_value) + return return_value + + return wrapper + + +def _curate_df_doc(doc: Optional[str]): + if not doc: + return doc + + # Remove examples, some are not applicable to StreamingDataFrame + doc = doc[: doc.find("**Examples:**")] + doc[doc.find("Args:") :] + + doc = doc.replace("dataframe.DataFrame", "streaming.StreamingDataFrame") + doc = doc.replace(" DataFrame", " StreamingDataFrame") + + return doc + + +class StreamingBase: + sql: str + _session: bigframes.Session + + def to_bigtable( + self, + *, + instance: str, + table: str, + service_account_email: Optional[str] = None, + app_profile: Optional[str] = None, + truncate: bool = False, + overwrite: bool = False, + auto_create_column_families: bool = False, + bigtable_options: Optional[dict] = None, + job_id: Optional[str] = None, + job_id_prefix: Optional[str] = None, + ) -> bigquery.QueryJob: + """ + Export the StreamingDataFrame as a continue job and returns a + QueryJob object for some management functionality. + + This method requires an existing bigtable preconfigured to + accept the continuous query export statement. For instructions + on export to bigtable, see + https://cloud.google.com/bigquery/docs/export-to-bigtable. + + Args: + instance (str): + The name of the bigtable instance to export to. + table (str): + The name of the bigtable table to export to. + service_account_email (str): + Full name of the service account to run the continuous query. + Example: accountname@projectname.gserviceaccounts.com + If not provided, the user account will be used, but this + limits the lifetime of the continuous query. + app_profile (str, default None): + The bigtable app profile to export to. If None, no app + profile will be used. + truncate (bool, default False): + The export truncate option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + overwrite (bool, default False): + The export overwrite option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + auto_create_column_families (bool, default False): + The auto_create_column_families option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + bigtable_options (dict, default None): + The bigtable options dict, which will be converted to JSON + using json.dumps, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + If None, no bigtable_options parameter will be passed. + job_id (str, default None): + If specified, replace the default job id for the query, + see job_id parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + job_id_prefix (str, default None): + If specified, a job id prefix for the query, see + job_id_prefix parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + + Returns: + google.cloud.bigquery.QueryJob: + See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob + The ongoing query job can be managed using this object. + For example, the job can be cancelled or its error status + can be examined. + """ + return _to_bigtable( + self.sql, + instance=instance, + table=table, + service_account_email=service_account_email, + session=self._session, + app_profile=app_profile, + truncate=truncate, + overwrite=overwrite, + auto_create_column_families=auto_create_column_families, + bigtable_options=bigtable_options, + job_id=job_id, + job_id_prefix=job_id_prefix, + ) + + def to_pubsub( + self, + *, + topic: str, + service_account_email: str, + job_id: Optional[str] = None, + job_id_prefix: Optional[str] = None, + ) -> bigquery.QueryJob: + """ + Export the StreamingDataFrame as a continue job and returns a + QueryJob object for some management functionality. + + This method requires an existing pubsub topic. For instructions + on creating a pubsub topic, see + https://cloud.google.com/pubsub/docs/samples/pubsub-quickstart-create-topic?hl=en + + Note that a service account is a requirement for continuous queries + exporting to pubsub. + + Args: + topic (str): + The name of the pubsub topic to export to. + For example: "taxi-rides" + service_account_email (str): + Full name of the service account to run the continuous query. + Example: accountname@projectname.gserviceaccounts.com + job_id (str, default None): + If specified, replace the default job id for the query, + see job_id parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + job_id_prefix (str, default None): + If specified, a job id prefix for the query, see + job_id_prefix parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + + Returns: + google.cloud.bigquery.QueryJob: + See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob + The ongoing query job can be managed using this object. + For example, the job can be cancelled or its error status + can be examined. + """ + return _to_pubsub( + self.sql, + topic=topic, + service_account_email=service_account_email, + session=self._session, + job_id=job_id, + job_id_prefix=job_id_prefix, + ) + + +@log_adapter.class_logger +class StreamingDataFrame(StreamingBase): + __doc__ = _curate_df_doc(dataframe.DataFrame.__doc__) + + # Private constructor + _create_key = object() + + def __init__(self, df: dataframe.DataFrame, *, create_key=0): + if create_key is not StreamingDataFrame._create_key: + raise ValueError( + "StreamingDataFrame class shouldn't be created through constructor. Call bigframes.Session.read_gbq_table_streaming method to create." + ) + self._df = df + self._df._disable_cache_override = True + + @classmethod + def _from_table_df(cls, df: dataframe.DataFrame) -> StreamingDataFrame: + return cls(df, create_key=cls._create_key) + + def __getitem__(self, *args, **kwargs): + return _return_type_wrapper(self._df.__getitem__, StreamingDataFrame)( + *args, **kwargs + ) + + __getitem__.__doc__ = _curate_df_doc( + inspect.getdoc(dataframe.DataFrame.__getitem__) + ) + + def __setitem__(self, *args, **kwargs): + return _return_type_wrapper(self._df.__setitem__, StreamingDataFrame)( + *args, **kwargs + ) + + __setitem__.__doc__ = _curate_df_doc( + inspect.getdoc(dataframe.DataFrame.__setitem__) + ) + + def rename(self, *args, **kwargs): + return _return_type_wrapper(self._df.rename, StreamingDataFrame)( + *args, **kwargs + ) + + rename.__doc__ = _curate_df_doc(inspect.getdoc(dataframe.DataFrame.rename)) + + def __repr__(self, *args, **kwargs): + return _return_type_wrapper(self._df.__repr__, StreamingDataFrame)( + *args, **kwargs + ) + + __repr__.__doc__ = _curate_df_doc(inspect.getdoc(dataframe.DataFrame.__repr__)) + + def _repr_html_(self, *args, **kwargs): + return _return_type_wrapper(self._df._repr_html_, StreamingDataFrame)( + *args, **kwargs + ) + + _repr_html_.__doc__ = _curate_df_doc( + inspect.getdoc(dataframe.DataFrame._repr_html_) + ) + + @property + def sql(self): + sql_str, _, _ = self._df._to_sql_query(include_index=False, enable_cache=False) + return sql_str + + sql.__doc__ = _curate_df_doc(inspect.getdoc(dataframe.DataFrame.sql)) + + @property + def _session(self): + return self._df._session + + _session.__doc__ = _curate_df_doc(inspect.getdoc(dataframe.DataFrame._session)) + + +def _to_bigtable( + query: str, + *, + instance: str, + table: str, + service_account_email: Optional[str] = None, + session: Optional[bigframes.Session] = None, + app_profile: Optional[str] = None, + truncate: bool = False, + overwrite: bool = False, + auto_create_column_families: bool = False, + bigtable_options: Optional[dict] = None, + job_id: Optional[str] = None, + job_id_prefix: Optional[str] = None, +) -> bigquery.QueryJob: + """Launches a BigQuery continuous query and returns a + QueryJob object for some management functionality. + + This method requires an existing bigtable preconfigured to + accept the continuous query export statement. For instructions + on export to bigtable, see + https://cloud.google.com/bigquery/docs/export-to-bigtable. + + Args: + query (str): + The sql statement to execute as a continuous function. + For example: "SELECT * FROM dataset.table" + This will be wrapped in an EXPORT DATA statement to + launch a continuous query writing to bigtable. + instance (str): + The name of the bigtable instance to export to. + table (str): + The name of the bigtable table to export to. + service_account_email (str): + Full name of the service account to run the continuous query. + Example: accountname@projectname.gserviceaccounts.com + If not provided, the user account will be used, but this + limits the lifetime of the continuous query. + session (bigframes.Session, default None): + The session object to use for the query. This determines + the project id and location of the query. If None, will + default to the bigframes global session. + app_profile (str, default None): + The bigtable app profile to export to. If None, no app + profile will be used. + truncate (bool, default False): + The export truncate option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + overwrite (bool, default False): + The export overwrite option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + auto_create_column_families (bool, default False): + The auto_create_column_families option, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + bigtable_options (dict, default None): + The bigtable options dict, which will be converted to JSON + using json.dumps, see + https://cloud.google.com/bigquery/docs/reference/standard-sql/other-statements#bigtable_export_option + If None, no bigtable_options parameter will be passed. + job_id (str, default None): + If specified, replace the default job id for the query, + see job_id parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + job_id_prefix (str, default None): + If specified, a job id prefix for the query, see + job_id_prefix parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + + Returns: + google.cloud.bigquery.QueryJob: + See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob + The ongoing query job can be managed using this object. + For example, the job can be cancelled or its error status + can be examined. + """ + warnings.warn( + "The bigframes.streaming module is a preview feature, and subject to change.", + stacklevel=1, + category=bigframes.exceptions.PreviewWarning, + ) + + # get default client if not passed + if session is None: + session = bigframes.get_global_session() + bq_client = session.bqclient + + # build export string from parameters + project = bq_client.project + + app_profile_url_string = "" + if app_profile is not None: + app_profile_url_string = f"appProfiles/{app_profile}/" + + bigtable_options_parameter_string = "" + if bigtable_options is not None: + bigtable_options_parameter_string = ( + 'bigtable_options = """' + json.dumps(bigtable_options) + '""",\n' + ) + + sql = ( + "EXPORT DATA\n" + "OPTIONS (\n" + "format = 'CLOUD_BIGTABLE',\n" + f"{bigtable_options_parameter_string}" + f"truncate = {str(truncate)},\n" + f"overwrite = {str(overwrite)},\n" + f"auto_create_column_families = {str(auto_create_column_families)},\n" + f'uri = "https://bigtable.googleapis.com/projects/{project}/instances/{instance}/{app_profile_url_string}tables/{table}"\n' + ")\n" + "AS (\n" + f"{query});" + ) + + # override continuous http parameter + job_config = bigquery.job.QueryJobConfig() + + job_config_dict: dict = {"query": {"continuous": True}} + if service_account_email is not None: + job_config_dict["query"]["connectionProperties"] = { + "key": "service_account", + "value": service_account_email, + } + job_config_filled = job_config.from_api_repr(job_config_dict) + job_config_filled.labels = {"bigframes-api": "streaming_to_bigtable"} + + # begin the query job + query_job = bq_client.query( + sql, + job_config=job_config_filled, # type:ignore + # typing error above is in bq client library + # (should accept abstract job_config, only takes concrete) + job_id=job_id, + job_id_prefix=job_id_prefix, + ) + + # return the query job to the user for lifetime management + return query_job + + +def _to_pubsub( + query: str, + *, + topic: str, + service_account_email: str, + session: Optional[bigframes.Session] = None, + job_id: Optional[str] = None, + job_id_prefix: Optional[str] = None, +) -> bigquery.QueryJob: + """Launches a BigQuery continuous query and returns a + QueryJob object for some management functionality. + + This method requires an existing pubsub topic. For instructions + on creating a pubsub topic, see + https://cloud.google.com/pubsub/docs/samples/pubsub-quickstart-create-topic?hl=en + + Note that a service account is a requirement for continuous queries + exporting to pubsub. + + Args: + query (str): + The sql statement to execute as a continuous function. + For example: "SELECT * FROM dataset.table" + This will be wrapped in an EXPORT DATA statement to + launch a continuous query writing to pubsub. + topic (str): + The name of the pubsub topic to export to. + For example: "taxi-rides" + service_account_email (str): + Full name of the service account to run the continuous query. + Example: accountname@projectname.gserviceaccounts.com + session (bigframes.Session, default None): + The session object to use for the query. This determines + the project id and location of the query. If None, will + default to the bigframes global session. + job_id (str, default None): + If specified, replace the default job id for the query, + see job_id parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + job_id_prefix (str, default None): + If specified, a job id prefix for the query, see + job_id_prefix parameter of + https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_query + + Returns: + google.cloud.bigquery.QueryJob: + See https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob + The ongoing query job can be managed using this object. + For example, the job can be cancelled or its error status + can be examined. + """ + warnings.warn( + "The bigframes.streaming module is a preview feature, and subject to change.", + stacklevel=1, + category=bigframes.exceptions.PreviewWarning, + ) + + # get default client if not passed + if session is None: + session = bigframes.get_global_session() + bq_client = session.bqclient + + # build export string from parameters + sql = ( + "EXPORT DATA\n" + "OPTIONS (\n" + "format = 'CLOUD_PUBSUB',\n" + f'uri = "https://pubsub.googleapis.com/projects/{bq_client.project}/topics/{topic}"\n' + ")\n" + "AS (\n" + f"{query});" + ) + + # override continuous http parameter + job_config = bigquery.job.QueryJobConfig() + job_config_filled = job_config.from_api_repr( + { + "query": { + "continuous": True, + "connectionProperties": { + "key": "service_account", + "value": service_account_email, + }, + } + } + ) + job_config_filled.labels = {"bigframes-api": "streaming_to_pubsub"} + + # begin the query job + query_job = bq_client.query( + sql, + job_config=job_config_filled, # type:ignore + # typing error above is in bq client library + # (should accept abstract job_config, only takes concrete) + job_id=job_id, + job_id_prefix=job_id_prefix, + ) + + # return the query job to the user for lifetime management + return query_job diff --git a/notebooks/streaming/streaming_dataframe.ipynb b/notebooks/streaming/streaming_dataframe.ipynb new file mode 100644 index 00000000000..a2da30720d0 --- /dev/null +++ b/notebooks/streaming/streaming_dataframe.ipynb @@ -0,0 +1,535 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### BigFrames StreamingDataFrame\n", + "bigframes.streaming.StreamingDataFrame is a special DataFrame type that allows simple operations and can create steaming jobs to BigTable and PubSub.\n", + "\n", + "In this notebook, we will:\n", + "* Create a StreamingDataFrame from a BigQuery table\n", + "* Do some opeartions like select, filter and preview the content\n", + "* Create and manage streaming jobs to both BigTable and Pubsub" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes\n", + "import bigframes.streaming as bst" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "bigframes.options._bigquery_options.project = \"bigframes-load-testing\"\n", + "job_id_prefix = \"test_streaming_\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create, select, filter and preview\n", + "Create the StreamingDataFrame from a BigQuery table, select certain columns, filter rows and preview the output" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/blocks.py:126: NullIndexPreviewWarning: Creating object with Null Index. Null Index is a preview feature.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "sdf = bst.read_gbq_table(\"birds.penguins_bigtable_streaming\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/blocks.py:126: NullIndexPreviewWarning: Creating object with Null Index. Null Index is a preview feature.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "Query job d57200dd-e6f1-42c7-876b-7f4a54994ae6 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/blocks.py:126: NullIndexPreviewWarning: Creating object with Null Index. Null Index is a preview feature.\n", + " warnings.warn(\n", + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/blocks.py:126: NullIndexPreviewWarning: Creating object with Null Index. Null Index is a preview feature.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 1decce4a-eb32-49f4-8e47-7bda0220037a is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesrowkeybody_mass_g
0Adelie Penguin (Pygoscelis adeliae)Torgersen3875
1Adelie Penguin (Pygoscelis adeliae)Torgersen2900
2Adelie Penguin (Pygoscelis adeliae)Biscoe3725
3Adelie Penguin (Pygoscelis adeliae)Dream2975
4Adelie Penguin (Pygoscelis adeliae)Torgersen3050
5Chinstrap penguin (Pygoscelis antarctica)Dream2700
6Adelie Penguin (Pygoscelis adeliae)Dream3900
7Adelie Penguin (Pygoscelis adeliae)Biscoe3825
8Chinstrap penguin (Pygoscelis antarctica)Dream3775
9Adelie Penguin (Pygoscelis adeliae)Dream3350
10Adelie Penguin (Pygoscelis adeliae)Biscoe3900
11Adelie Penguin (Pygoscelis adeliae)Torgersen3650
12Adelie Penguin (Pygoscelis adeliae)Biscoe3200
13Chinstrap penguin (Pygoscelis antarctica)Dream3650
14Adelie Penguin (Pygoscelis adeliae)Dream3700
15Chinstrap penguin (Pygoscelis antarctica)Dream3800
16Chinstrap penguin (Pygoscelis antarctica)Dream3950
17Chinstrap penguin (Pygoscelis antarctica)Dream3350
18Adelie Penguin (Pygoscelis adeliae)Dream3100
19Chinstrap penguin (Pygoscelis antarctica)Dream3750
20Adelie Penguin (Pygoscelis adeliae)Biscoe3550
21Chinstrap penguin (Pygoscelis antarctica)Dream3400
22Adelie Penguin (Pygoscelis adeliae)Torgersen3450
23Adelie Penguin (Pygoscelis adeliae)Torgersen3600
24Chinstrap penguin (Pygoscelis antarctica)Dream3650
\n", + "

25 rows × 3 columns

\n", + "
[165 rows x 3 columns in total]" + ], + "text/plain": [ + " species rowkey body_mass_g\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3875\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 2900\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3725\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 2975\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3050\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 2700\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3900\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3825\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3775\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3350\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3900\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3650\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3200\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3650\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3700\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3800\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3950\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3350\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3100\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3750\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3550\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3400\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3450\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3600\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3650\n", + "...\n", + "\n", + "[165 rows x 3 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sdf = sdf[[\"species\", \"island\", \"body_mass_g\"]]\n", + "sdf = sdf[sdf[\"body_mass_g\"] < 4000]\n", + "# BigTable needs a rowkey column\n", + "sdf = sdf.rename(columns={\"island\": \"rowkey\"})\n", + "print(type(sdf))\n", + "sdf" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### BigTable\n", + "Create BigTable streaming job" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/streaming/dataframe.py:338: PreviewWarning: The bigframes.streaming module is a preview feature, and subject to change.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "job = sdf.to_bigtable(instance=\"streaming-testing-instance\",\n", + " table=\"garrettwu-no-col-family\",\n", + " service_account_email=\"streaming-testing-admin@bigframes-load-testing.iam.gserviceaccount.com\",\n", + " app_profile=None,\n", + " truncate=True,\n", + " overwrite=True,\n", + " auto_create_column_families=True,\n", + " bigtable_options={},\n", + " job_id=None,\n", + " job_id_prefix=job_id_prefix,)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "None\n" + ] + } + ], + "source": [ + "print(job.running())\n", + "print(job.error_result)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.cancel()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PubSub\n", + "Create Pubsub streaming job" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/blocks.py:126: NullIndexPreviewWarning: Creating object with Null Index. Null Index is a preview feature.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "sdf = sdf[[\"rowkey\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/streaming/dataframe.py:453: PreviewWarning: The bigframes.streaming module is a preview feature, and subject to change.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "job = sdf.to_pubsub(\n", + " topic=\"penguins\",\n", + " service_account_email=\"streaming-testing@bigframes-load-testing.iam.gserviceaccount.com\",\n", + " job_id=None,\n", + " job_id_prefix=job_id_prefix,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "None\n" + ] + } + ], + "source": [ + "print(job.running())\n", + "print(job.error_result)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.cancel()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 3acae0e75bd..6bd7bf9348d 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -143,6 +143,14 @@ def session() -> Generator[bigframes.Session, None, None]: session.close() # close generated session at cleanup time +@pytest.fixture(scope="session") +def session_load() -> Generator[bigframes.Session, None, None]: + context = bigframes.BigQueryOptions(location="US", project="bigframes-load-testing") + session = bigframes.Session(context=context) + yield session + session.close() # close generated session at cleanup time + + @pytest.fixture(scope="session", params=["ordered", "unordered"]) def maybe_ordered_session(request) -> Generator[bigframes.Session, None, None]: context = bigframes.BigQueryOptions(location="US", ordering_mode="partial") diff --git a/tests/system/large/test_streaming.py b/tests/system/large/test_streaming.py index 2debc09994f..391aec85333 100644 --- a/tests/system/large/test_streaming.py +++ b/tests/system/large/test_streaming.py @@ -14,20 +14,20 @@ import time -import pytest - +import bigframes import bigframes.streaming -@pytest.mark.skip(reason="b/354024943. Concurrency error need to be fixed.") -def test_streaming_to_bigtable(): +def test_streaming_df_to_bigtable(session_load: bigframes.Session): # launch a continuous query job_id_prefix = "test_streaming_" - sql = """SELECT - body_mass_g, island as rowkey - FROM birds.penguins_bigtable_streaming""" - query_job = bigframes.streaming.to_bigtable( - sql, + sdf = session_load.read_gbq_table_streaming("birds.penguins_bigtable_streaming") + + sdf = sdf[["species", "island", "body_mass_g"]] + sdf = sdf[sdf["body_mass_g"] < 4000] + sdf = sdf.rename(columns={"island": "rowkey"}) + + query_job = sdf.to_bigtable( instance="streaming-testing-instance", table="table-testing", service_account_email="streaming-testing@bigframes-load-testing.iam.gserviceaccount.com", @@ -44,23 +44,22 @@ def test_streaming_to_bigtable(): # wait 100 seconds in order to ensure the query doesn't stop # (i.e. it is continuous) time.sleep(100) - assert query_job.error_result is None - assert query_job.errors is None assert query_job.running() + assert query_job.error_result is None assert str(query_job.job_id).startswith(job_id_prefix) finally: query_job.cancel() -@pytest.mark.skip(reason="b/354024943. Concurrency error need to be fixed.") -def test_streaming_to_pubsub(): +def test_streaming_df_to_pubsub(session_load: bigframes.Session): # launch a continuous query job_id_prefix = "test_streaming_pubsub_" - sql = """SELECT - island - FROM birds.penguins_pubsub_streaming""" - query_job = bigframes.streaming.to_pubsub( - sql, + sdf = session_load.read_gbq_table_streaming("birds.penguins_bigtable_streaming") + + sdf = sdf[sdf["body_mass_g"] < 4000] + sdf = sdf[["island"]] + + query_job = sdf.to_pubsub( topic="penguins", service_account_email="streaming-testing@bigframes-load-testing.iam.gserviceaccount.com", job_id=None, @@ -71,9 +70,8 @@ def test_streaming_to_pubsub(): # wait 100 seconds in order to ensure the query doesn't stop # (i.e. it is continuous) time.sleep(100) - assert query_job.error_result is None - assert query_job.errors is None assert query_job.running() + assert query_job.error_result is None assert str(query_job.job_id).startswith(job_id_prefix) finally: query_job.cancel() diff --git a/tests/unit/test_pandas.py b/tests/unit/test_pandas.py index 408590d4bb1..1ee52c08a10 100644 --- a/tests/unit/test_pandas.py +++ b/tests/unit/test_pandas.py @@ -35,6 +35,8 @@ def all_session_methods(): if not attribute.startswith("_") ) session_attributes.remove("close") + # streaming isn't in pandas + session_attributes.remove("read_gbq_table_streaming") for attribute in sorted(session_attributes): session_method = getattr(bigframes.session.Session, attribute) From 8e00fe2878aceeb222b4d67bf6520dd4544807c6 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:11:09 -0700 Subject: [PATCH 731/921] chore(main): release 1.12.0 (#834) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82495157192..354c356c7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.12.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.11.1...v1.12.0) (2024-07-31) + + +### Features + +* Add bigframes-mode label to query jobs ([#832](https://github.com/googleapis/python-bigquery-dataframes/issues/832)) ([c9eaff0](https://github.com/googleapis/python-bigquery-dataframes/commit/c9eaff0a1a0731b28f4c67bca5606db12a47c8c0)) +* Add config option to set partial ordering mode ([#855](https://github.com/googleapis/python-bigquery-dataframes/issues/855)) ([823c0ce](https://github.com/googleapis/python-bigquery-dataframes/commit/823c0ce57611c0918a9e9999638d7393337fe9af)) +* Add stratify param support to ml.model_selection.train_test_split method ([#815](https://github.com/googleapis/python-bigquery-dataframes/issues/815)) ([27f8631](https://github.com/googleapis/python-bigquery-dataframes/commit/27f8631be81a3e136cfeb8904558bb4f3f5caa05)) +* Add streaming.StreamingDataFrame class ([#864](https://github.com/googleapis/python-bigquery-dataframes/issues/864)) ([a7d7197](https://github.com/googleapis/python-bigquery-dataframes/commit/a7d7197a32c55b989ae4ea8f6cf6e1c0f7184cd4)) +* Allow DataFrame.join for self-join on Null index ([#860](https://github.com/googleapis/python-bigquery-dataframes/issues/860)) ([e950533](https://github.com/googleapis/python-bigquery-dataframes/commit/e95053372c36ea5a91a2d7295c1a3a3671181670)) +* Support remote function cleanup with `session.close` ([#818](https://github.com/googleapis/python-bigquery-dataframes/issues/818)) ([ed06436](https://github.com/googleapis/python-bigquery-dataframes/commit/ed06436612c0d46f190f79721416d473bde7e2f4)) +* Support to_csv/parquet/json to local files/objects ([#858](https://github.com/googleapis/python-bigquery-dataframes/issues/858)) ([d0ab9cc](https://github.com/googleapis/python-bigquery-dataframes/commit/d0ab9cc47298bdde638299baecac9dffd7841ede)) + + +### Bug Fixes + +* Fewer relation joins from df self-operations ([#823](https://github.com/googleapis/python-bigquery-dataframes/issues/823)) ([0d24f73](https://github.com/googleapis/python-bigquery-dataframes/commit/0d24f737041c7dd70253ebb4baa8d8ef67bd4f1d)) +* Fix 'sql' property for null index ([#844](https://github.com/googleapis/python-bigquery-dataframes/issues/844)) ([1b6a556](https://github.com/googleapis/python-bigquery-dataframes/commit/1b6a556206a7a66283339d827ab12db2753521e2)) +* Fix unordered mode using ordered path to print frame ([#839](https://github.com/googleapis/python-bigquery-dataframes/issues/839)) ([93785cb](https://github.com/googleapis/python-bigquery-dataframes/commit/93785cb48be4a2eb8770129148bd0b897fed4ee7)) +* Reduce redundant `remote_function` deployments ([#856](https://github.com/googleapis/python-bigquery-dataframes/issues/856)) ([cbf2d42](https://github.com/googleapis/python-bigquery-dataframes/commit/cbf2d42e4d961a7537381a9c3b28a8b463ad8f74)) + + +### Documentation + +* Add partner attribution steps to integrations sample notebook ([#835](https://github.com/googleapis/python-bigquery-dataframes/issues/835)) ([d7b333f](https://github.com/googleapis/python-bigquery-dataframes/commit/d7b333fa26acddaeb5ccca4f81b1d624dff03ba2)) +* Make `get_global_session`/`close_session`/`reset_session` appears in the docs ([#847](https://github.com/googleapis/python-bigquery-dataframes/issues/847)) ([01d6bbb](https://github.com/googleapis/python-bigquery-dataframes/commit/01d6bbb7479da706dc62bb5e7d51dc28a4042812)) + ## [1.11.1](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.11.0...v1.11.1) (2024-07-08) diff --git a/bigframes/version.py b/bigframes/version.py index 1186811c97f..29cf036f426 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.11.1" +__version__ = "1.12.0" From 30aaae554b5a0272b4d2900e7bd2f9530aa546b6 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 31 Jul 2024 16:48:07 -0700 Subject: [PATCH 732/921] chore: reduce the `remote_function` cleanup rate (#873) * chore: reduce the `remote_function` cleanup rate * minor comment change --- tests/system/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 6bd7bf9348d..83c8baac394 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -49,9 +49,11 @@ # We are running pytest with "-n 20". For a rough estimation, let's say all # parallel sessions run in parallel. So that allows 1000/20 = 50 mutations per # minute. One session takes about 1 minute to create a remote function. This -# would allow 50-1 = 49 deletions per session. As a heuristic let's use half of -# that potential for the clean up. -MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION = 25 +# would allow 50-1 = 49 deletions per session. +# However, because of b/356217175 the service may throw ResourceExhausted("Too +# many operations are currently being executed, try again later."), so we peg +# the cleanup to a more controlled rate. +MAX_NUM_FUNCTIONS_TO_DELETE_PER_SESSION = 15 CURRENT_DIR = pathlib.Path(__file__).parent DATA_DIR = CURRENT_DIR.parent / "data" From e9b05571123cf13079772856317ca3cd3d564c5a Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:35:36 -0700 Subject: [PATCH 733/921] docs: update streaming notebook (#875) --- notebooks/streaming/streaming_dataframe.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notebooks/streaming/streaming_dataframe.ipynb b/notebooks/streaming/streaming_dataframe.ipynb index a2da30720d0..d4cc255fa5e 100644 --- a/notebooks/streaming/streaming_dataframe.ipynb +++ b/notebooks/streaming/streaming_dataframe.ipynb @@ -5,12 +5,12 @@ "metadata": {}, "source": [ "### BigFrames StreamingDataFrame\n", - "bigframes.streaming.StreamingDataFrame is a special DataFrame type that allows simple operations and can create steaming jobs to BigTable and PubSub.\n", + "bigframes.streaming.StreamingDataFrame is a special DataFrame type that allows simple operations and can create streaming jobs to process real-time data and reverse ETL output to Bigtable and Pub/Sub using [BigQuery continuous queries](https://cloud.google.com/bigquery/docs/continuous-queries-introduction).\n", "\n", "In this notebook, we will:\n", "* Create a StreamingDataFrame from a BigQuery table\n", - "* Do some opeartions like select, filter and preview the content\n", - "* Create and manage streaming jobs to both BigTable and Pubsub" + "* Do some operations like select, filter and preview the content\n", + "* Create and manage streaming jobs to both Bigtable and Pub/Sub" ] }, { From 042db4b3d4e4142dabca305e706c78d7766697ef Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:12:15 -0700 Subject: [PATCH 734/921] chore(python): fix docs build (#871) Source-Link: https://github.com/googleapis/synthtool/commit/bef813d194de29ddf3576eda60148b6b3dcc93d9 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:94bb690db96e6242b2567a4860a94d48fa48696d092e51b0884a1a2c0a79a407 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/docker/docs/Dockerfile | 9 ++++----- .kokoro/publish-docs.sh | 23 ++++++++++------------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index f30cb3775af..6d064ddb9b0 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:52210e0e0559f5ea8c52be148b33504022e1faef4e95fbe4b32d68022af2fa7e -# created: 2024-07-08T19:25:35.862283192Z + digest: sha256:94bb690db96e6242b2567a4860a94d48fa48696d092e51b0884a1a2c0a79a407 +# created: 2024-07-31T14:52:44.926548819Z diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index 5205308b334..e5410e296bd 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -72,19 +72,18 @@ RUN tar -xvf Python-3.10.14.tgz RUN ./Python-3.10.14/configure --enable-optimizations RUN make altinstall -RUN python3.10 -m venv /venv -ENV PATH /venv/bin:$PATH +ENV PATH /usr/local/bin/python3.10:$PATH ###################### Install pip RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ - && python3 /tmp/get-pip.py \ + && python3.10 /tmp/get-pip.py \ && rm /tmp/get-pip.py # Test pip -RUN python3 -m pip +RUN python3.10 -m pip # Install build requirements COPY requirements.txt /requirements.txt -RUN python3 -m pip install --require-hashes -r requirements.txt +RUN python3.10 -m pip install --require-hashes -r requirements.txt CMD ["python3.10"] diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index da9ce803dd0..233205d580e 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -21,18 +21,18 @@ export PYTHONUNBUFFERED=1 export PATH="${HOME}/.local/bin:${PATH}" # Install nox -python3 -m pip install --require-hashes -r .kokoro/requirements.txt -python3 -m nox --version +python3.10 -m pip install --require-hashes -r .kokoro/requirements.txt +python3.10 -m nox --version # build docs nox -s docs # create metadata -python3 -m docuploader create-metadata \ +python3.10 -m docuploader create-metadata \ --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ - --version=$(python3 setup.py --version) \ + --version=$(python3.10 setup.py --version) \ --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ - --distribution-name=$(python3 setup.py --name) \ + --distribution-name=$(python3.10 setup.py --name) \ --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) @@ -40,26 +40,23 @@ python3 -m docuploader create-metadata \ cat docs.metadata # upload docs -python3 -m docuploader upload docs/_build/html --metadata-file docs.metadata --staging-bucket "${STAGING_BUCKET}" +python3.10 -m docuploader upload docs/_build/html --metadata-file docs.metadata --staging-bucket "${STAGING_BUCKET}" # docfx yaml files nox -s docfx # create metadata. -python3 -m docuploader create-metadata \ +python3.10 -m docuploader create-metadata \ --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ - --version=$(python3 setup.py --version) \ + --version=$(python3.10 setup.py --version) \ --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ - --distribution-name=$(python3 setup.py --name) \ + --distribution-name=$(python3.10 setup.py --name) \ --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) cat docs.metadata -# Replace toc.yml template file -mv docs/templates/toc.yml docs/_build/html/docfx_yaml/toc.yml - # upload docs -python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}" +python3.10 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}" From 9959fc8fcba93441fdd3d9c17e8fdbe6e6a7b504 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 1 Aug 2024 16:55:24 -0700 Subject: [PATCH 735/921] fix: Fix issue with invalid sql generated by ml distance functions (#865) --- bigframes/core/compile/scalar_op_compiler.py | 29 ++++ bigframes/ml/core.py | 131 ++++++++---------- bigframes/ml/metrics/pairwise.py | 47 ++++--- bigframes/ml/sql.py | 51 +++---- bigframes/operations/__init__.py | 11 ++ bigframes/operations/type.py | 19 +++ .../system/small/ml/test_metrics_pairwise.py | 41 ++++++ tests/unit/ml/test_sql.py | 41 ++---- 8 files changed, 216 insertions(+), 154 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 0bc9f2e370d..06e9481d178 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -1380,6 +1380,30 @@ def minimum_impl( return ibis.case().when(upper.isnull() | (value > upper), upper).else_(value).end() +@scalar_op_compiler.register_binary_op(ops.cosine_distance_op) +def cosine_distance_impl( + vector1: ibis_types.Value, + vector2: ibis_types.Value, +): + return vector_distance(vector1, vector2, "COSINE") + + +@scalar_op_compiler.register_binary_op(ops.euclidean_distance_op) +def euclidean_distance_impl( + vector1: ibis_types.Value, + vector2: ibis_types.Value, +): + return vector_distance(vector1, vector2, "EUCLIDEAN") + + +@scalar_op_compiler.register_binary_op(ops.manhattan_distance_op) +def manhattan_distance_impl( + vector1: ibis_types.Value, + vector2: ibis_types.Value, +): + return vector_distance(vector1, vector2, "MANHATTAN") + + @scalar_op_compiler.register_binary_op(ops.BinaryRemoteFunctionOp, pass_op=True) def binary_remote_function_op_impl( x: ibis_types.Value, y: ibis_types.Value, op: ops.BinaryRemoteFunctionOp @@ -1501,3 +1525,8 @@ def json_set( json_obj: ibis_dtypes.JSON, json_path: ibis_dtypes.str, json_value ) -> ibis_dtypes.JSON: """Produces a new SQL JSON value with the specified JSON data inserted or replaced.""" + + +@ibis.udf.scalar.builtin(name="ML.DISTANCE") +def vector_distance(vector1, vector2, type: str) -> ibis_dtypes.Float64: + """Computes the distance between two vectors using specified type ("EUCLIDEAN", "MANHATTAN", or "COSINE")""" diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index f1b36651f4b..d570945f16a 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -17,7 +17,7 @@ from __future__ import annotations import datetime -from typing import Callable, cast, Iterable, Literal, Mapping, Optional, Union +from typing import Callable, cast, Iterable, Mapping, Optional, Union import uuid from google.cloud import bigquery @@ -35,11 +35,27 @@ def __init__(self, session: bigframes.Session): self._session = session self._base_sql_generator = ml_sql.BaseSqlGenerator() - def _apply_sql( + +class BqmlModel(BaseBqml): + """Represents an existing BQML model in BigQuery. + + Wraps the BQML API and SQL interface to expose the functionality needed for + BigQuery DataFrames ML. + """ + + def __init__(self, session: bigframes.Session, model: bigquery.Model): + self._session = session + self._model = model + self._model_manipulation_sql_generator = ml_sql.ModelManipulationSqlGenerator( + self.model_name + ) + + def _apply_ml_tvf( self, input_data: bpd.DataFrame, - func: Callable[[bpd.DataFrame], str], + apply_sql_tvf: Callable[[str], str], ) -> bpd.DataFrame: + # Used for predict, transform, distance """Helper to wrap a dataframe in a SQL query, keeping the index intact. Args: @@ -50,67 +66,28 @@ def _apply_sql( the dataframe to be wrapped func (function): - a function that will accept a SQL string and produce a new SQL - string from which to construct the output dataframe. It must - include the index columns of the input SQL. + Takes an input sql table value and applies a prediction tvf. The + resulting table value must include all input columns, with new + columns appended to the end. """ - _, index_col_ids, index_labels = input_data._to_sql_query(include_index=True) - - sql = func(input_data) - df = self._session.read_gbq(sql, index_col=index_col_ids) - df.index.names = index_labels - - return df - - def distance( - self, - x: bpd.DataFrame, - y: bpd.DataFrame, - type: Literal["EUCLIDEAN", "MANHATTAN", "COSINE"], - name: str, - ) -> bpd.DataFrame: - """Calculate ML.DISTANCE from DataFrame inputs. - - Args: - x: - input DataFrame - y: - input DataFrame - type: - Distance types, accept values are "EUCLIDEAN", "MANHATTAN", "COSINE". - name: - name of the output result column - """ - assert len(x.columns) == 1 and len(y.columns) == 1 - - input_data = x.join(y, how="outer").cache() - x_column_id, y_column_id = x._block.value_columns[0], y._block.value_columns[0] - - return self._apply_sql( - input_data, - lambda source_df: self._base_sql_generator.ml_distance( - x_column_id, - y_column_id, - type=type, - source_df=source_df, - name=name, - ), + # TODO: Preserve ordering information? + input_sql, index_col_ids, index_labels = input_data._to_sql_query( + include_index=True ) - -class BqmlModel(BaseBqml): - """Represents an existing BQML model in BigQuery. - - Wraps the BQML API and SQL interface to expose the functionality needed for - BigQuery DataFrames ML. - """ - - def __init__(self, session: bigframes.Session, model: bigquery.Model): - self._session = session - self._model = model - self._model_manipulation_sql_generator = ml_sql.ModelManipulationSqlGenerator( - self.model_name + result_sql = apply_sql_tvf(input_sql) + df = self._session.read_gbq(result_sql, index_col=index_col_ids) + df.index.names = index_labels + # Restore column labels + df.rename( + columns={ + label: original_label + for label, original_label in zip( + df.columns.values, input_data.columns.values + ) + } ) + return df def _keys(self): return (self._session, self._model) @@ -137,13 +114,13 @@ def model(self) -> bigquery.Model: return self._model def predict(self, input_data: bpd.DataFrame) -> bpd.DataFrame: - return self._apply_sql( + return self._apply_ml_tvf( input_data, self._model_manipulation_sql_generator.ml_predict, ) def transform(self, input_data: bpd.DataFrame) -> bpd.DataFrame: - return self._apply_sql( + return self._apply_ml_tvf( input_data, self._model_manipulation_sql_generator.ml_transform, ) @@ -153,10 +130,10 @@ def generate_text( input_data: bpd.DataFrame, options: Mapping[str, int | float], ) -> bpd.DataFrame: - return self._apply_sql( + return self._apply_ml_tvf( input_data, - lambda source_df: self._model_manipulation_sql_generator.ml_generate_text( - source_df=source_df, + lambda source_sql: self._model_manipulation_sql_generator.ml_generate_text( + source_sql=source_sql, struct_options=options, ), ) @@ -166,10 +143,10 @@ def generate_embedding( input_data: bpd.DataFrame, options: Mapping[str, int | float], ) -> bpd.DataFrame: - return self._apply_sql( + return self._apply_ml_tvf( input_data, - lambda source_df: self._model_manipulation_sql_generator.ml_generate_embedding( - source_df=source_df, + lambda source_sql: self._model_manipulation_sql_generator.ml_generate_embedding( + source_sql=source_sql, struct_options=options, ), ) @@ -179,10 +156,10 @@ def detect_anomalies( ) -> bpd.DataFrame: assert self._model.model_type in ("PCA", "KMEANS", "ARIMA_PLUS") - return self._apply_sql( + return self._apply_ml_tvf( input_data, - lambda source_df: self._model_manipulation_sql_generator.ml_detect_anomalies( - source_df=source_df, + lambda source_sql: self._model_manipulation_sql_generator.ml_detect_anomalies( + source_sql=source_sql, struct_options=options, ), ) @@ -192,7 +169,9 @@ def forecast(self, options: Mapping[str, int | float]) -> bpd.DataFrame: return self._session.read_gbq(sql, index_col="forecast_timestamp").reset_index() def evaluate(self, input_data: Optional[bpd.DataFrame] = None): - sql = self._model_manipulation_sql_generator.ml_evaluate(input_data) + sql = self._model_manipulation_sql_generator.ml_evaluate( + input_data.sql if (input_data is not None) else None + ) return self._session.read_gbq(sql) @@ -202,7 +181,7 @@ def llm_evaluate( task_type: Optional[str] = None, ): sql = self._model_manipulation_sql_generator.ml_llm_evaluate( - input_data, task_type + input_data.sql, task_type ) return self._session.read_gbq(sql) @@ -336,7 +315,7 @@ def create_model( model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_model( - source_df=input_data, + source_sql=input_data.sql, model_ref=model_ref, transforms=transforms, options=options, @@ -374,7 +353,7 @@ def create_llm_remote_model( model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_llm_remote_model( - source_df=input_data, + source_sql=input_data.sql, model_ref=model_ref, options=options, connection_name=connection_name, @@ -407,7 +386,7 @@ def create_time_series_model( model_ref = self._create_model_ref(session._anonymous_dataset) sql = self._model_creation_sql_generator.create_model( - source_df=input_data, + source_sql=input_data.sql, model_ref=model_ref, transforms=transforms, options=options, diff --git a/bigframes/ml/metrics/pairwise.py b/bigframes/ml/metrics/pairwise.py index bdbe4a682d0..0e43412b210 100644 --- a/bigframes/ml/metrics/pairwise.py +++ b/bigframes/ml/metrics/pairwise.py @@ -17,19 +17,24 @@ import bigframes_vendored.sklearn.metrics.pairwise as vendored_metrics_pairwise -from bigframes.ml import core, utils +from bigframes.ml import utils +import bigframes.operations as ops import bigframes.pandas as bpd def paired_cosine_distances( X: Union[bpd.DataFrame, bpd.Series], Y: Union[bpd.DataFrame, bpd.Series] ) -> bpd.DataFrame: - X, Y = utils.convert_to_dataframe(X, Y) - if len(X.columns) != 1 or len(Y.columns) != 1: - raise ValueError("Inputs X and Y can only contain 1 column.") + X, Y = utils.convert_to_series(X, Y) + joined_block, _ = X._block.join(Y._block, how="outer") - base_bqml = core.BaseBqml(session=X._session) - return base_bqml.distance(X, Y, type="COSINE", name="cosine_distance") + result_block, _ = joined_block.project_expr( + ops.cosine_distance_op.as_expr( + joined_block.value_columns[0], joined_block.value_columns[1] + ), + label="cosine_distance", + ) + return bpd.DataFrame(result_block) paired_cosine_distances.__doc__ = inspect.getdoc( @@ -40,12 +45,16 @@ def paired_cosine_distances( def paired_manhattan_distance( X: Union[bpd.DataFrame, bpd.Series], Y: Union[bpd.DataFrame, bpd.Series] ) -> bpd.DataFrame: - X, Y = utils.convert_to_dataframe(X, Y) - if len(X.columns) != 1 or len(Y.columns) != 1: - raise ValueError("Inputs X and Y can only contain 1 column.") + X, Y = utils.convert_to_series(X, Y) + joined_block, _ = X._block.join(Y._block, how="outer") - base_bqml = core.BaseBqml(session=X._session) - return base_bqml.distance(X, Y, type="MANHATTAN", name="manhattan_distance") + result_block, _ = joined_block.project_expr( + ops.manhattan_distance_op.as_expr( + joined_block.value_columns[0], joined_block.value_columns[1] + ), + label="manhattan_distance", + ) + return bpd.DataFrame(result_block) paired_manhattan_distance.__doc__ = inspect.getdoc( @@ -56,12 +65,16 @@ def paired_manhattan_distance( def paired_euclidean_distances( X: Union[bpd.DataFrame, bpd.Series], Y: Union[bpd.DataFrame, bpd.Series] ) -> bpd.DataFrame: - X, Y = utils.convert_to_dataframe(X, Y) - if len(X.columns) != 1 or len(Y.columns) != 1: - raise ValueError("Inputs X and Y can only contain 1 column.") - - base_bqml = core.BaseBqml(session=X._session) - return base_bqml.distance(X, Y, type="EUCLIDEAN", name="euclidean_distance") + X, Y = utils.convert_to_series(X, Y) + joined_block, _ = X._block.join(Y._block, how="outer") + + result_block, _ = joined_block.project_expr( + ops.euclidean_distance_op.as_expr( + joined_block.value_columns[0], joined_block.value_columns[1] + ), + label="euclidean_distance", + ) + return bpd.DataFrame(result_block) paired_euclidean_distances.__doc__ = inspect.getdoc( diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 0399db3a10f..d14627f590f 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -21,9 +21,9 @@ import google.cloud.bigquery import bigframes.constants as constants -import bigframes.pandas as bpd +# TODO: Add proper escaping logic from core/compile module class BaseSqlGenerator: """Generate base SQL strings for ML. Model name isn't needed in this class.""" @@ -170,12 +170,11 @@ def ml_distance( col_x: str, col_y: str, type: Literal["EUCLIDEAN", "MANHATTAN", "COSINE"], - source_df: bpd.DataFrame, + source_sql: str, name: str, ) -> str: """Encode ML.DISTANCE for BQML. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-distance""" - source_sql, _, _ = source_df._to_sql_query(include_index=True) return f"""SELECT *, ML.DISTANCE({col_x}, {col_y}, '{type}') AS {name} FROM ({source_sql})""" @@ -191,14 +190,12 @@ def _model_id_sql( # Model create and alter def create_model( self, - source_df: bpd.DataFrame, + source_sql: str, model_ref: google.cloud.bigquery.ModelReference, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, transforms: Optional[Iterable[str]] = None, ) -> str: """Encode the CREATE OR REPLACE MODEL statement for BQML""" - source_sql = source_df.sql - parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] if transforms: parts.append(self.transform(*transforms)) @@ -209,14 +206,12 @@ def create_model( def create_llm_remote_model( self, - source_df: bpd.DataFrame, + source_sql: str, connection_name: str, model_ref: google.cloud.bigquery.ModelReference, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> str: """Encode the CREATE OR REPLACE MODEL statement for BQML""" - source_sql = source_df.sql - parts = [f"CREATE OR REPLACE MODEL {self._model_id_sql(model_ref)}"] parts.append(self.connection(connection_name)) if options: @@ -280,11 +275,6 @@ class ModelManipulationSqlGenerator(BaseSqlGenerator): def __init__(self, model_name: str): self._model_name = model_name - def _source_sql(self, source_df: bpd.DataFrame) -> str: - """Return DataFrame sql with index columns.""" - _source_sql, _, _ = source_df._to_sql_query(include_index=True) - return _source_sql - # Alter model def alter_model( self, @@ -298,10 +288,10 @@ def alter_model( return "\n".join(parts) # ML prediction TVFs - def ml_predict(self, source_df: bpd.DataFrame) -> str: + def ml_predict(self, source_sql: str) -> str: """Encode ML.PREDICT for BQML""" return f"""SELECT * FROM ML.PREDICT(MODEL `{self._model_name}`, - ({self._source_sql(source_df)}))""" + ({source_sql}))""" def ml_forecast(self, struct_options: Mapping[str, Union[int, float]]) -> str: """Encode ML.FORECAST for BQML""" @@ -310,38 +300,32 @@ def ml_forecast(self, struct_options: Mapping[str, Union[int, float]]) -> str: {struct_options_sql})""" def ml_generate_text( - self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] + self, source_sql: str, struct_options: Mapping[str, Union[int, float]] ) -> str: """Encode ML.GENERATE_TEXT for BQML""" struct_options_sql = self.struct_options(**struct_options) return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL `{self._model_name}`, - ({self._source_sql(source_df)}), {struct_options_sql})""" + ({source_sql}), {struct_options_sql})""" def ml_generate_embedding( - self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] + self, source_sql: str, struct_options: Mapping[str, Union[int, float]] ) -> str: """Encode ML.GENERATE_EMBEDDING for BQML""" struct_options_sql = self.struct_options(**struct_options) return f"""SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `{self._model_name}`, - ({self._source_sql(source_df)}), {struct_options_sql})""" + ({source_sql}), {struct_options_sql})""" def ml_detect_anomalies( - self, source_df: bpd.DataFrame, struct_options: Mapping[str, Union[int, float]] + self, source_sql: str, struct_options: Mapping[str, Union[int, float]] ) -> str: """Encode ML.DETECT_ANOMALIES for BQML""" struct_options_sql = self.struct_options(**struct_options) return f"""SELECT * FROM ML.DETECT_ANOMALIES(MODEL `{self._model_name}`, - {struct_options_sql}, ({self._source_sql(source_df)}))""" + {struct_options_sql}, ({source_sql}))""" # ML evaluation TVFs - def ml_evaluate(self, source_df: Optional[bpd.DataFrame] = None) -> str: + def ml_evaluate(self, source_sql: Optional[str] = None) -> str: """Encode ML.EVALUATE for BQML""" - if source_df is None: - source_sql = None - else: - # Note: don't need index as evaluate returns a new table - source_sql, _, _ = source_df._to_sql_query(include_index=False) - if source_sql is None: return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`)""" else: @@ -353,12 +337,9 @@ def ml_arima_coefficients(self) -> str: return f"""SELECT * FROM ML.ARIMA_COEFFICIENTS(MODEL `{self._model_name}`)""" # ML evaluation TVFs - def ml_llm_evaluate( - self, source_df: bpd.DataFrame, task_type: Optional[str] = None - ) -> str: + def ml_llm_evaluate(self, source_sql: str, task_type: Optional[str] = None) -> str: """Encode ML.EVALUATE for BQML""" # Note: don't need index as evaluate returns a new table - source_sql, _, _ = source_df._to_sql_query(include_index=False) return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, ({source_sql}), STRUCT("{task_type}" AS task_type))""" @@ -383,7 +364,7 @@ def ml_principal_component_info(self) -> str: ) # ML transform TVF, that require a transform_only type model - def ml_transform(self, source_df: bpd.DataFrame) -> str: + def ml_transform(self, source_sql: str) -> str: """Encode ML.TRANSFORM for BQML""" return f"""SELECT * FROM ML.TRANSFORM(MODEL `{self._model_name}`, - ({self._source_sql(source_df)}))""" + ({source_sql}))""" diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 145c415ca07..23f2a50a954 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -690,6 +690,17 @@ def output_type(self, *input_types): ge_op = create_binary_op(name="ge", type_signature=op_typing.COMPARISON) +cosine_distance_op = create_binary_op( + name="ml_cosine_distance", type_signature=op_typing.VECTOR_METRIC +) +manhattan_distance_op = create_binary_op( + name="ml_manhattan_distance", type_signature=op_typing.VECTOR_METRIC +) +euclidean_distance_op = create_binary_op( + name="ml_euclidean_distance", type_signature=op_typing.VECTOR_METRIC +) + + ## String Ops @dataclasses.dataclass(frozen=True) class StrConcatOp(BinaryOp): diff --git a/bigframes/operations/type.py b/bigframes/operations/type.py index f4690708054..ce37b8da55f 100644 --- a/bigframes/operations/type.py +++ b/bigframes/operations/type.py @@ -190,6 +190,24 @@ def output_type( return left_type +@dataclasses.dataclass +class VectorMetric(BinaryTypeSignature): + """Type signature for logical operators like AND, OR and NOT.""" + + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + if not bigframes.dtypes.is_array_like(left_type): + raise TypeError(f"Type {left_type} is not array-like") + if not bigframes.dtypes.is_array_like(right_type): + raise TypeError(f"Type {right_type} is not array-like") + if left_type != right_type: + raise TypeError( + "Vector op operands {left_type} and {right_type} do not match" + ) + return bigframes.dtypes.FLOAT_DTYPE + + # Common type signatures UNARY_NUMERIC = TypePreserving(bigframes.dtypes.is_numeric, description="numeric") UNARY_REAL_NUMERIC = UnaryRealNumeric() @@ -212,3 +230,4 @@ def output_type( TIMELIKE_ACCESSOR = FixedOutputType( bigframes.dtypes.is_time_like, bigframes.dtypes.INT_DTYPE, description="time-like" ) +VECTOR_METRIC = VectorMetric() diff --git a/tests/system/small/ml/test_metrics_pairwise.py b/tests/system/small/ml/test_metrics_pairwise.py index 717f32667f9..d3798f7cae1 100644 --- a/tests/system/small/ml/test_metrics_pairwise.py +++ b/tests/system/small/ml/test_metrics_pairwise.py @@ -35,6 +35,47 @@ def test_paired_cosine_distances(): ) +def test_paired_cosine_distances_multiindex(): + x_col = [np.array([4.1, 0.5, 1.0])] + y_col = [np.array([3.0, 0.0, 2.5])] + data = bpd.read_pandas( + pd.DataFrame( + {("DATA", "X"): x_col, ("DATA", "Y"): y_col}, + ) + ) + + result = metrics.pairwise.paired_cosine_distances( + data[("DATA", "X")], data[("DATA", "Y")] + ) + expected_pd_df = pd.DataFrame( + { + ("DATA", "X"): x_col, + ("DATA", "Y"): y_col, + ("cosine_distance", ""): [0.108199], + } + ) + + pd.testing.assert_frame_equal( + result.to_pandas(), expected_pd_df, check_dtype=False, check_index_type=False + ) + + +def test_paired_cosine_distances_single_frame(): + x_col = [np.array([4.1, 0.5, 1.0])] + y_col = [np.array([3.0, 0.0, 2.5])] + input = bpd.read_pandas(pd.DataFrame({"X": x_col})) + input["Y"] = y_col # type: ignore + + result = metrics.pairwise.paired_cosine_distances(input.X, input.Y) + expected_pd_df = pd.DataFrame( + {"X": x_col, "Y": y_col, "cosine_distance": [0.108199]} + ) + + pd.testing.assert_frame_equal( + result.to_pandas(), expected_pd_df, check_dtype=False, check_index_type=False + ) + + def test_paired_manhattan_distance(): x_col = [np.array([4.1, 0.5, 1.0])] y_col = [np.array([3.0, 0.0, 2.5])] diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index e90146565dc..cdf2d0b2e4a 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -152,23 +152,12 @@ def test_polynomial_expand( assert sql == "ML.POLYNOMIAL_EXPAND(STRUCT(col_a, col_b), 2) AS poly_exp" -def test_distance_correct( - base_sql_generator: ml_sql.BaseSqlGenerator, - mock_df: bpd.DataFrame, -): - sql = base_sql_generator.ml_distance("col_a", "col_b", "COSINE", mock_df, "cosine") - assert ( - sql - == "SELECT *, ML.DISTANCE(col_a, col_b, 'COSINE') AS cosine FROM (input_X_sql)" - ) - - def test_create_model_correct( model_creation_sql_generator: ml_sql.ModelCreationSqlGenerator, mock_df: bpd.DataFrame, ): sql = model_creation_sql_generator.create_model( - source_df=mock_df, + source_sql=mock_df.sql, model_ref=bigquery.ModelReference.from_string( "test-proj._anonXYZ.create_model_correct_sql" ), @@ -189,7 +178,7 @@ def test_create_model_transform_correct( mock_df: bpd.DataFrame, ): sql = model_creation_sql_generator.create_model( - source_df=mock_df, + source_sql=mock_df.sql, model_ref=bigquery.ModelReference.from_string( "test-proj._anonXYZ.create_model_transform" ), @@ -217,7 +206,7 @@ def test_create_llm_remote_model_correct( mock_df: bpd.DataFrame, ): sql = model_creation_sql_generator.create_llm_remote_model( - source_df=mock_df, + source_sql=mock_df.sql, connection_name="my_project.us.my_connection", model_ref=bigquery.ModelReference.from_string( "test-proj._anonXYZ.create_remote_model" @@ -342,11 +331,11 @@ def test_ml_predict_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, ): - sql = model_manipulation_sql_generator.ml_predict(source_df=mock_df) + sql = model_manipulation_sql_generator.ml_predict(source_sql=mock_df.sql) assert ( sql == """SELECT * FROM ML.PREDICT(MODEL `my_project_id.my_dataset_id.my_model_id`, - (input_X_sql))""" + (input_X_y_sql))""" ) @@ -355,12 +344,12 @@ def test_ml_llm_evaluate_correct( mock_df: bpd.DataFrame, ): sql = model_manipulation_sql_generator.ml_llm_evaluate( - source_df=mock_df, task_type="CLASSIFICATION" + source_sql=mock_df.sql, task_type="CLASSIFICATION" ) assert ( sql == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, - (input_X_sql), STRUCT("CLASSIFICATION" AS task_type))""" + (input_X_y_sql), STRUCT("CLASSIFICATION" AS task_type))""" ) @@ -368,11 +357,11 @@ def test_ml_evaluate_correct( model_manipulation_sql_generator: ml_sql.ModelManipulationSqlGenerator, mock_df: bpd.DataFrame, ): - sql = model_manipulation_sql_generator.ml_evaluate(source_df=mock_df) + sql = model_manipulation_sql_generator.ml_evaluate(source_sql=mock_df.sql) assert ( sql == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, - (input_X_sql))""" + (input_X_y_sql))""" ) @@ -429,13 +418,13 @@ def test_ml_generate_text_correct( mock_df: bpd.DataFrame, ): sql = model_manipulation_sql_generator.ml_generate_text( - source_df=mock_df, + source_sql=mock_df.sql, struct_options={"option_key1": 1, "option_key2": 2.2}, ) assert ( sql == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_project_id.my_dataset_id.my_model_id`, - (input_X_sql), STRUCT( + (input_X_y_sql), STRUCT( 1 AS option_key1, 2.2 AS option_key2))""" ) @@ -446,13 +435,13 @@ def test_ml_generate_embedding_correct( mock_df: bpd.DataFrame, ): sql = model_manipulation_sql_generator.ml_generate_embedding( - source_df=mock_df, + source_sql=mock_df.sql, struct_options={"option_key1": 1, "option_key2": 2.2}, ) assert ( sql == """SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `my_project_id.my_dataset_id.my_model_id`, - (input_X_sql), STRUCT( + (input_X_y_sql), STRUCT( 1 AS option_key1, 2.2 AS option_key2))""" ) @@ -463,7 +452,7 @@ def test_ml_detect_anomalies_correct_sql( mock_df: bpd.DataFrame, ): sql = model_manipulation_sql_generator.ml_detect_anomalies( - source_df=mock_df, + source_sql=mock_df.sql, struct_options={"option_key1": 1, "option_key2": 2.2}, ) assert ( @@ -471,7 +460,7 @@ def test_ml_detect_anomalies_correct_sql( == """SELECT * FROM ML.DETECT_ANOMALIES(MODEL `my_project_id.my_dataset_id.my_model_id`, STRUCT( 1 AS option_key1, - 2.2 AS option_key2), (input_X_sql))""" + 2.2 AS option_key2), (input_X_y_sql))""" ) From 2158818e53e09e55c87ffd574e3ebc2e201285fb Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 2 Aug 2024 10:02:13 -0700 Subject: [PATCH 736/921] feat: `df.apply(axis=1)` to support remote function with mutiple params (#851) * feat: extend `df.apply(axis=1)` to support remote function with mutiple params * add doctest, make small test remote function sticky * handle single param non-row-processing functions * reword the documentation a bit * handle missing input dtype in read_gbq_function * restore input types as tuple in read_gbq_function * clear previous remote function attributes * reword documentation for clarity * add/update comments to explain force reproject * make doctest example remote function with 3 params --- bigframes/core/compile/scalar_op_compiler.py | 26 ++- bigframes/dataframe.py | 148 ++++++++++------- bigframes/exceptions.py | 4 + bigframes/functions/remote_function.py | 40 ++++- bigframes/operations/__init__.py | 13 ++ bigframes/series.py | 11 +- bigframes/session/__init__.py | 4 +- tests/system/large/test_remote_function.py | 154 +++++++++++++++++- tests/system/small/test_remote_function.py | 80 +++++---- .../bigframes_vendored/pandas/core/frame.py | 43 ++++- 10 files changed, 417 insertions(+), 106 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 06e9481d178..67d0dac436f 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -191,19 +191,27 @@ def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): return decorator - def register_nary_op(self, op_ref: typing.Union[ops.NaryOp, type[ops.NaryOp]]): + def register_nary_op( + self, op_ref: typing.Union[ops.NaryOp, type[ops.NaryOp]], pass_op: bool = False + ): """ Decorator to register a nary op implementation. Args: op_ref (NaryOp or NaryOp type): Class or instance of operator that is implemented by the decorated function. + pass_op (bool): + Set to true if implementation takes the operator object as the last argument. + This is needed for parameterized ops where parameters are part of op object. """ key = typing.cast(str, op_ref.name) def decorator(impl: typing.Callable[..., ibis_types.Value]): def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): - return impl(*args) + if pass_op: + return impl(*args, op=op) + else: + return impl(*args) self._register(key, normalized_impl) return impl @@ -1468,6 +1476,7 @@ def clip_op( ) +# N-ary Operations @scalar_op_compiler.register_nary_op(ops.case_when_op) def case_when_op(*cases_and_outputs: ibis_types.Value) -> ibis_types.Value: # ibis can handle most type coercions, but we need to force bool -> int @@ -1487,6 +1496,19 @@ def case_when_op(*cases_and_outputs: ibis_types.Value) -> ibis_types.Value: return case_val.end() +@scalar_op_compiler.register_nary_op(ops.NaryRemoteFunctionOp, pass_op=True) +def nary_remote_function_op_impl( + *operands: ibis_types.Value, op: ops.NaryRemoteFunctionOp +): + ibis_node = getattr(op.func, "ibis_node", None) + if ibis_node is None: + raise TypeError( + f"only a bigframes remote function is supported as a callable. {constants.FEEDBACK_LINK}" + ) + result = ibis_node(*operands) + return result + + # Helpers def is_null(value) -> bool: # float NaN/inf should be treated as distinct from 'true' null values diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9789c7cf9f1..9d3b153d3a2 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3433,9 +3433,9 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: raise ValueError(f"na_action={na_action} not supported") # TODO(shobs): Support **kwargs - # Reproject as workaround to applying filter too late. This forces the filter - # to be applied before passing data to remote function, protecting from bad - # inputs causing errors. + # Reproject as workaround to applying filter too late. This forces the + # filter to be applied before passing data to remote function, + # protecting from bad inputs causing errors. reprojected_df = DataFrame(self._block._force_reproject()) return reprojected_df._apply_unary_op( ops.RemoteFunctionOp(func=func, apply_on_null=(na_action is None)) @@ -3448,65 +3448,99 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs): category=bigframes.exceptions.PreviewWarning, ) - # Early check whether the dataframe dtypes are currently supported - # in the remote function - # NOTE: Keep in sync with the value converters used in the gcf code - # generated in remote_function_template.py - remote_function_supported_dtypes = ( - bigframes.dtypes.INT_DTYPE, - bigframes.dtypes.FLOAT_DTYPE, - bigframes.dtypes.BOOL_DTYPE, - bigframes.dtypes.BYTES_DTYPE, - bigframes.dtypes.STRING_DTYPE, - ) - supported_dtypes_types = tuple( - type(dtype) - for dtype in remote_function_supported_dtypes - if not isinstance(dtype, pandas.ArrowDtype) - ) - # Check ArrowDtype separately since multiple BigQuery types map to - # ArrowDtype, including BYTES and TIMESTAMP. - supported_arrow_types = tuple( - dtype.pyarrow_dtype - for dtype in remote_function_supported_dtypes - if isinstance(dtype, pandas.ArrowDtype) - ) - supported_dtypes_hints = tuple( - str(dtype) for dtype in remote_function_supported_dtypes - ) - - for dtype in self.dtypes: - if ( - # Not one of the pandas/numpy types. - not isinstance(dtype, supported_dtypes_types) - # And not one of the arrow types. - and not ( - isinstance(dtype, pandas.ArrowDtype) - and any( - dtype.pyarrow_dtype.equals(arrow_type) - for arrow_type in supported_arrow_types - ) - ) - ): - raise NotImplementedError( - f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1." - f" Supported dtypes are {supported_dtypes_hints}." - ) - # Check if the function is a remote function if not hasattr(func, "bigframes_remote_function"): raise ValueError("For axis=1 a remote function must be used.") - # Serialize the rows as json values - block = self._get_block() - rows_as_json_series = bigframes.series.Series( - block._get_rows_as_json_values() - ) + is_row_processor = getattr(func, "is_row_processor") + if is_row_processor: + # Early check whether the dataframe dtypes are currently supported + # in the remote function + # NOTE: Keep in sync with the value converters used in the gcf code + # generated in remote_function_template.py + remote_function_supported_dtypes = ( + bigframes.dtypes.INT_DTYPE, + bigframes.dtypes.FLOAT_DTYPE, + bigframes.dtypes.BOOL_DTYPE, + bigframes.dtypes.BYTES_DTYPE, + bigframes.dtypes.STRING_DTYPE, + ) + supported_dtypes_types = tuple( + type(dtype) + for dtype in remote_function_supported_dtypes + if not isinstance(dtype, pandas.ArrowDtype) + ) + # Check ArrowDtype separately since multiple BigQuery types map to + # ArrowDtype, including BYTES and TIMESTAMP. + supported_arrow_types = tuple( + dtype.pyarrow_dtype + for dtype in remote_function_supported_dtypes + if isinstance(dtype, pandas.ArrowDtype) + ) + supported_dtypes_hints = tuple( + str(dtype) for dtype in remote_function_supported_dtypes + ) - # Apply the function - result_series = rows_as_json_series._apply_unary_op( - ops.RemoteFunctionOp(func=func, apply_on_null=True) - ) + for dtype in self.dtypes: + if ( + # Not one of the pandas/numpy types. + not isinstance(dtype, supported_dtypes_types) + # And not one of the arrow types. + and not ( + isinstance(dtype, pandas.ArrowDtype) + and any( + dtype.pyarrow_dtype.equals(arrow_type) + for arrow_type in supported_arrow_types + ) + ) + ): + raise NotImplementedError( + f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1." + f" Supported dtypes are {supported_dtypes_hints}." + ) + + # Serialize the rows as json values + block = self._get_block() + rows_as_json_series = bigframes.series.Series( + block._get_rows_as_json_values() + ) + + # Apply the function + result_series = rows_as_json_series._apply_unary_op( + ops.RemoteFunctionOp(func=func, apply_on_null=True) + ) + else: + # This is a special case where we are providing not-pandas-like + # extension. If the remote function can take one or more params + # then we assume that here the user intention is to use the + # column values of the dataframe as arguments to the function. + # For this to work the following condition must be true: + # 1. The number or input params in the function must be same + # as the number of columns in the dataframe + # 2. The dtypes of the columns in the dataframe must be + # compatible with the data types of the input params + # 3. The order of the columns in the dataframe must correspond + # to the order of the input params in the function + udf_input_dtypes = getattr(func, "input_dtypes") + if len(udf_input_dtypes) != len(self.columns): + raise ValueError( + f"Remote function takes {len(udf_input_dtypes)} arguments but DataFrame has {len(self.columns)} columns." + ) + if udf_input_dtypes != tuple(self.dtypes.to_list()): + raise ValueError( + f"Remote function takes arguments of types {udf_input_dtypes} but DataFrame dtypes are {tuple(self.dtypes)}." + ) + + series_list = [self[col] for col in self.columns] + # Reproject as workaround to applying filter too late. This forces the + # filter to be applied before passing data to remote function, + # protecting from bad inputs causing errors. + reprojected_series = bigframes.series.Series( + series_list[0]._block._force_reproject() + ) + result_series = reprojected_series._apply_nary_op( + ops.NaryRemoteFunctionOp(func=func), series_list[1:] + ) result_series.name = None # Return Series with materialized result so that any error in the remote diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 1d317497605..6c5b66bc470 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -57,3 +57,7 @@ class QueryComplexityError(RuntimeError): class TimeTravelDisabledWarning(Warning): """A query was reattempted without time travel.""" + + +class UnknownDataTypeWarning(Warning): + """Data type is unknown.""" diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index d84fbcdbabc..b3c6aee1b32 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -66,6 +66,7 @@ from bigframes import clients import bigframes.constants as constants import bigframes.core.compile.ibis_types +import bigframes.dtypes import bigframes.functions.remote_function_template logger = logging.getLogger(__name__) @@ -895,8 +896,8 @@ def remote_function( reuse (bool, Optional): Reuse the remote function if already exists. `True` by default, which will result in reusing an existing remote - function and corresponding cloud function (if any) that was - previously created for the same udf. + function and corresponding cloud function that was previously + created (if any) for the same udf. Please note that for an unnamed (i.e. created without an explicit `name` argument) remote function, the BigQuery DataFrames session id is attached in the cloud artifacts names. So for the @@ -1174,7 +1175,9 @@ def try_delattr(attr): try_delattr("bigframes_cloud_function") try_delattr("bigframes_remote_function") + try_delattr("input_dtypes") try_delattr("output_dtype") + try_delattr("is_row_processor") try_delattr("ibis_node") ( @@ -1216,12 +1219,20 @@ def try_delattr(attr): rf_name ) ) - + func.input_dtypes = tuple( + [ + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( + input_type + ) + for input_type in ibis_signature.input_types + ] + ) func.output_dtype = ( bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( ibis_signature.output_type ) ) + func.is_row_processor = is_row_processor func.ibis_node = node # If a new remote function was created, update the cloud artifacts @@ -1305,6 +1316,29 @@ def func(*ignored_args, **ignored_kwargs): signature=(ibis_signature.input_types, ibis_signature.output_type), ) func.bigframes_remote_function = str(routine_ref) # type: ignore + + # set input bigframes data types + has_unknown_dtypes = False + function_input_dtypes = [] + for ibis_type in ibis_signature.input_types: + input_dtype = cast(bigframes.dtypes.Dtype, bigframes.dtypes.DEFAULT_DTYPE) + if ibis_type is None: + has_unknown_dtypes = True + else: + input_dtype = ( + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( + ibis_type + ) + ) + function_input_dtypes.append(input_dtype) + if has_unknown_dtypes: + warnings.warn( + "The function has one or more missing input data types." + f" BigQuery DataFrames will assume default data type {bigframes.dtypes.DEFAULT_DTYPE} for them.", + category=bigframes.exceptions.UnknownDataTypeWarning, + ) + func.input_dtypes = tuple(function_input_dtypes) # type: ignore + func.output_dtype = bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( # type: ignore ibis_signature.output_type ) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 23f2a50a954..523882c14e6 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -659,6 +659,19 @@ def output_type(self, *input_types): raise AttributeError("output_dtype not defined") +@dataclasses.dataclass(frozen=True) +class NaryRemoteFunctionOp(NaryOp): + name: typing.ClassVar[str] = "nary_remote_function" + func: typing.Callable + + def output_type(self, *input_types): + # This property should be set to a valid Dtype by the @remote_function decorator or read_gbq_function method + if hasattr(self.func, "output_dtype"): + return self.func.output_dtype + else: + raise AttributeError("output_dtype not defined") + + add_op = AddOp() sub_op = SubOp() mul_op = create_binary_op(name="mul", type_signature=op_typing.BINARY_NUMERIC) diff --git a/bigframes/series.py b/bigframes/series.py index 1a5661529c9..9e338018347 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1442,9 +1442,6 @@ def apply( ) -> Series: # TODO(shobs, b/274645634): Support convert_dtype, args, **kwargs # is actually a ternary op - # Reproject as workaround to applying filter too late. This forces the filter - # to be applied before passing data to remote function, protecting from bad - # inputs causing errors. if by_row not in ["compat", False]: raise ValueError("Param by_row must be one of 'compat' or False") @@ -1474,7 +1471,10 @@ def apply( ex.message += f"\n{_remote_function_recommendation_message}" raise - # We are working with remote function at this point + # We are working with remote function at this point. + # Reproject as workaround to applying filter too late. This forces the + # filter to be applied before passing data to remote function, + # protecting from bad inputs causing errors. reprojected_series = Series(self._block._force_reproject()) result_series = reprojected_series._apply_unary_op( ops.RemoteFunctionOp(func=func, apply_on_null=True) @@ -1507,6 +1507,9 @@ def combine( ex.message += f"\n{_remote_function_recommendation_message}" raise + # Reproject as workaround to applying filter too late. This forces the + # filter to be applied before passing data to remote function, + # protecting from bad inputs causing errors. reprojected_series = Series(self._block._force_reproject()) result_series = reprojected_series._apply_binary_op( other, ops.BinaryRemoteFunctionOp(func=func) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 98cba867f23..233e6ef930d 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1661,8 +1661,8 @@ def remote_function( reuse (bool, Optional): Reuse the remote function if already exists. `True` by default, which will result in reusing an existing remote - function and corresponding cloud function (if any) that was - previously created for the same udf. + function and corresponding cloud function that was previously + created (if any) for the same udf. Please note that for an unnamed (i.e. created without an explicit `name` argument) remote function, the BigQuery DataFrames session id is attached in the cloud artifacts names. So for the diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 303c74f1fd7..095f7059cd8 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -28,6 +28,9 @@ import test_utils.prefixer import bigframes +import bigframes.dataframe +import bigframes.dtypes +import bigframes.exceptions import bigframes.functions.remote_function as bigframes_rf import bigframes.pandas as bpd import bigframes.series @@ -363,7 +366,8 @@ def test_remote_function_input_types(session, scalars_dfs, input_types): def add_one(x): return x + 1 - remote_add_one = session.remote_function(input_types, int)(add_one) + remote_add_one = session.remote_function(input_types, int, reuse=False)(add_one) + assert remote_add_one.input_dtypes == (bigframes.dtypes.INT_DTYPE,) scalars_df, scalars_pandas_df = scalars_dfs @@ -1589,6 +1593,8 @@ def serialize_row(row): bigframes.series.Series, str, reuse=False )(serialize_row) + assert getattr(serialize_row_remote, "is_row_processor") + bf_result = scalars_df[columns].apply(serialize_row_remote, axis=1).to_pandas() pd_result = scalars_pandas_df[columns].apply(serialize_row, axis=1) @@ -1622,7 +1628,11 @@ def analyze(row): } ) - analyze_remote = session.remote_function(bigframes.series.Series, str)(analyze) + analyze_remote = session.remote_function( + bigframes.series.Series, str, reuse=False + )(analyze) + + assert getattr(analyze_remote, "is_row_processor") bf_result = ( scalars_df[columns].dropna().apply(analyze_remote, axis=1).to_pandas() @@ -1727,6 +1737,8 @@ def serialize_row(row): bigframes.series.Series, str, reuse=False )(serialize_row) + assert getattr(serialize_row_remote, "is_row_processor") + bf_result = bf_df.apply(serialize_row_remote, axis=1).to_pandas() pd_result = pd_df.apply(serialize_row, axis=1) @@ -1787,6 +1799,8 @@ def float_parser(row): bigframes.series.Series, float, reuse=False )(float_parser) + assert getattr(float_parser_remote, "is_row_processor") + pd_result = pd_df.apply(float_parser, axis=1) bf_result = bf_df.apply(float_parser_remote, axis=1).to_pandas() @@ -1913,7 +1927,7 @@ def test_remote_function_named_perists_w_session_cleanup(): name = test_utils.prefixer.Prefixer("bigframes", "").create_prefix() # create an unnamed remote function in the session - @session.remote_function(name=name) + @session.remote_function(reuse=False, name=name) def foo(x: int) -> int: return x + 1 @@ -2004,3 +2018,137 @@ def foo_named(x: int) -> int: cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, foo_named ) + + +def test_df_apply_axis_1_multiple_params(session): + bf_df = bigframes.dataframe.DataFrame( + { + "Id": [1, 2, 3], + "Age": [22.5, 23, 23.5], + "Name": ["alpha", "beta", "gamma"], + } + ) + + expected_dtypes = ( + bigframes.dtypes.INT_DTYPE, + bigframes.dtypes.FLOAT_DTYPE, + bigframes.dtypes.STRING_DTYPE, + ) + + # Assert the dataframe dtypes + assert tuple(bf_df.dtypes) == expected_dtypes + + try: + + @session.remote_function([int, float, str], str, reuse=False) + def foo(x, y, z): + return f"I got {x}, {y} and {z}" + + assert getattr(foo, "is_row_processor") is False + assert getattr(foo, "input_dtypes") == expected_dtypes + + # Fails to apply on dataframe with incompatible number of columns + with pytest.raises( + ValueError, + match="^Remote function takes 3 arguments but DataFrame has 2 columns\\.$", + ): + bf_df[["Id", "Age"]].apply(foo, axis=1) + with pytest.raises( + ValueError, + match="^Remote function takes 3 arguments but DataFrame has 4 columns\\.$", + ): + bf_df.assign(Country="lalaland").apply(foo, axis=1) + + # Fails to apply on dataframe with incompatible column datatypes + with pytest.raises( + ValueError, + match="^Remote function takes arguments of types .* but DataFrame dtypes are .*", + ): + bf_df.assign(Age=bf_df["Age"].astype("Int64")).apply(foo, axis=1) + + # Successfully applies to dataframe with matching number of columns + # and their datatypes + bf_result = bf_df.apply(foo, axis=1).to_pandas() + + # Since this scenario is not pandas-like, let's handcraft the + # expected result + expected_result = pandas.Series( + [ + "I got 1, 22.5 and alpha", + "I got 2, 23 and beta", + "I got 3, 23.5 and gamma", + ] + ) + + pandas.testing.assert_series_equal( + expected_result, bf_result, check_dtype=False, check_index_type=False + ) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, foo + ) + + +def test_df_apply_axis_1_single_param_non_series(session): + bf_df = bigframes.dataframe.DataFrame( + { + "Id": [1, 2, 3], + } + ) + + expected_dtypes = (bigframes.dtypes.INT_DTYPE,) + + # Assert the dataframe dtypes + assert tuple(bf_df.dtypes) == expected_dtypes + + try: + + @session.remote_function([int], str, reuse=False) + def foo(x): + return f"I got {x}" + + assert getattr(foo, "is_row_processor") is False + assert getattr(foo, "input_dtypes") == expected_dtypes + + # Fails to apply on dataframe with incompatible number of columns + with pytest.raises( + ValueError, + match="^Remote function takes 1 arguments but DataFrame has 0 columns\\.$", + ): + bf_df[[]].apply(foo, axis=1) + with pytest.raises( + ValueError, + match="^Remote function takes 1 arguments but DataFrame has 2 columns\\.$", + ): + bf_df.assign(Country="lalaland").apply(foo, axis=1) + + # Fails to apply on dataframe with incompatible column datatypes + with pytest.raises( + ValueError, + match="^Remote function takes arguments of types .* but DataFrame dtypes are .*", + ): + bf_df.assign(Id=bf_df["Id"].astype("Float64")).apply(foo, axis=1) + + # Successfully applies to dataframe with matching number of columns + # and their datatypes + bf_result = bf_df.apply(foo, axis=1).to_pandas() + + # Since this scenario is not pandas-like, let's handcraft the + # expected result + expected_result = pandas.Series( + [ + "I got 1", + "I got 2", + "I got 3", + ] + ) + + pandas.testing.assert_series_equal( + expected_result, bf_result, check_dtype=False, check_index_type=False + ) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, foo + ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index c07a0afb44e..8ecf9eb3682 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -21,6 +21,7 @@ import pytest import bigframes +import bigframes.dtypes import bigframes.exceptions from bigframes.functions import remote_function as rf from tests.system.utils import assert_pandas_df_equal @@ -708,6 +709,8 @@ def test_read_gbq_function_reads_udfs(session, bigquery_client, dataset_id): # It should point to the named routine and yield the expected results. assert square.bigframes_remote_function == str(routine.reference) + assert square.input_dtypes == (bigframes.dtypes.INT_DTYPE,) + assert square.output_dtype == bigframes.dtypes.INT_DTYPE src = {"x": [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]} @@ -776,10 +779,14 @@ def test_read_gbq_function_enforces_explicit_types( str(both_types_specified.reference), session=session, ) - rf.read_gbq_function( - str(only_return_type_specified.reference), - session=session, - ) + with pytest.warns( + bigframes.exceptions.UnknownDataTypeWarning, + match="missing input data types.*assume default data type", + ): + rf.read_gbq_function( + str(only_return_type_specified.reference), + session=session, + ) with pytest.raises(ValueError): rf.read_gbq_function( str(only_arg_type_specified.reference), @@ -919,36 +926,41 @@ def add_ints(row): scalars_df[columns].apply(add_ints, axis=1) -@pytest.mark.parametrize( - ("column"), - [ - pytest.param("date_col"), - pytest.param("datetime_col"), - pytest.param("geography_col"), - pytest.param("numeric_col"), - pytest.param("time_col"), - pytest.param("timestamp_col"), - ], -) -def test_df_apply_axis_1_unsupported_dtype(scalars_dfs, column): - scalars_df, scalars_pandas_df = scalars_dfs - - # It doesn't matter if it is a remote function or not, the dtype check - # is done even before the function type check with axis=1 - def echo(row): - return row[column] +@pytest.mark.flaky(retries=2, delay=120) +def test_df_apply_axis_1_unsupported_dtype(session, scalars_dfs, dataset_id_permanent): + columns_with_not_supported_dtypes = [ + "date_col", + "datetime_col", + "geography_col", + "numeric_col", + "time_col", + "timestamp_col", + ] - # pandas works - scalars_pandas_df[[column]].apply(echo, axis=1) + scalars_df, scalars_pandas_df = scalars_dfs - dtype = scalars_df[column].dtype + def echo_len(row): + return len(row) - with pytest.raises( - NotImplementedError, - match=re.escape( - f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1. Supported dtypes are (" - ), - ), pytest.warns( - bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." - ): - scalars_df[[column]].apply(echo, axis=1) + echo_len_remote = session.remote_function( + bigframes.series.Series, + float, + dataset_id_permanent, + name=get_rf_name(echo_len, is_row_processor=True), + )(echo_len) + + for column in columns_with_not_supported_dtypes: + # pandas works + scalars_pandas_df[[column]].apply(echo_len, axis=1) + + dtype = scalars_df[column].dtype + + with pytest.raises( + NotImplementedError, + match=re.escape( + f"DataFrame has a column of dtype '{dtype}' which is not supported with axis=1. Supported dtypes are (" + ), + ), pytest.warns( + bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." + ): + scalars_df[[column]].apply(echo_len_remote, axis=1) diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 7048d9c6dd4..10565a2552f 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -4361,9 +4361,50 @@ def apply(self, func, *, axis=0, args=(), **kwargs): 1 19 dtype: Int64 + You could also apply a remote function which accepts multiple parameters + to every row of a DataFrame by using it with `axis=1` if the DataFrame + has matching number of columns and data types. Note: This feature is + currently in **preview**. + + >>> df = bpd.DataFrame({ + ... 'col1': [1, 2], + ... 'col2': [3, 4], + ... 'col3': [5, 5] + ... }) + >>> df + col1 col2 col3 + 0 1 3 5 + 1 2 4 5 + + [2 rows x 3 columns] + + >>> @bpd.remote_function(reuse=False) + ... def foo(x: int, y: int, z: int) -> float: + ... result = 1 + ... result += x + ... result += y/z + ... return result + + >>> df.apply(foo, axis=1) + 0 2.6 + 1 3.8 + dtype: Float64 + Args: func (function): - Function to apply to each column or row. + Function to apply to each column or row. To apply to each row + (i.e. when `axis=1` is specified) the function can be of one of + the two types: + + (1). It accepts a single input parameter of type `Series`, in + which case each row is delivered to the function as a pandas + Series. + + (2). It accept one or more parameters, in which case column values + are delivered to the function as separate arguments (mapping + to those parameters) for each row. For this to work the + `DataFrame` must have same number of columns and matching + data types. axis ({index (0), columns (1)}): Axis along which the function is applied. Specify 0 or 'index' to apply function to each column. Specify 1 or 'columns' to From 8753bdd1e44701e56eae914ebc0e91d9b1a6adf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 2 Aug 2024 12:37:25 -0500 Subject: [PATCH 737/921] feat: create a separate OrderingModePartialPreviewWarning for more fine-grained warning filters (#879) --- bigframes/exceptions.py | 4 ++++ bigframes/session/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 6c5b66bc470..b1af96c9c4e 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -47,6 +47,10 @@ class NullIndexError(ValueError): """Object has no index.""" +class OrderingModePartialPreviewWarning(PreviewWarning): + """Ordering mode 'partial' is in preview.""" + + class OrderRequiredError(ValueError): """Operation requires total row ordering to be enabled.""" diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 233e6ef930d..f449b52fbfc 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -302,7 +302,7 @@ def __init__( if not self._strictly_ordered: warnings.warn( "Partial ordering mode is a preview feature and is subject to change.", - bigframes.exceptions.PreviewWarning, + bigframes.exceptions.OrderingModePartialPreviewWarning, ) # Sequential index needs total ordering to generate, so use null index with unstrict ordering. From 9606dac3303e4cb97dc679295db6576f644f438a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 2 Aug 2024 12:41:30 -0500 Subject: [PATCH 738/921] chore: move OrderingMode to enums module (#870) --- bigframes/_config/bigquery_options.py | 18 ++++++------------ bigframes/enums.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 0506f1841eb..34b9a3128fd 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -16,7 +16,6 @@ from __future__ import annotations -from enum import Enum from typing import Literal, Optional import warnings @@ -25,14 +24,9 @@ import jellyfish import bigframes.constants +import bigframes.enums import bigframes.exceptions - -class OrderingMode(Enum): - STRICT = "strict" - PARTIAL = "partial" - - SESSION_STARTED_MESSAGE = ( "Cannot change '{attribute}' once a session has started. " "Call bigframes.pandas.close_session() first, if you are using the bigframes.pandas API." @@ -64,11 +58,11 @@ def _validate_location(value: Optional[str]): ) -def _validate_ordering_mode(value: str) -> OrderingMode: - if value.casefold() == OrderingMode.STRICT.value.casefold(): - return OrderingMode.STRICT - if value.casefold() == OrderingMode.PARTIAL.value.casefold(): - return OrderingMode.PARTIAL +def _validate_ordering_mode(value: str) -> bigframes.enums.OrderingMode: + if value.casefold() == bigframes.enums.OrderingMode.STRICT.value.casefold(): + return bigframes.enums.OrderingMode.STRICT + if value.casefold() == bigframes.enums.OrderingMode.PARTIAL.value.casefold(): + return bigframes.enums.OrderingMode.PARTIAL raise ValueError("Ordering mode must be one of 'strict' or 'partial'.") diff --git a/bigframes/enums.py b/bigframes/enums.py index 9501d3f13ee..fd7b5545bb2 100644 --- a/bigframes/enums.py +++ b/bigframes/enums.py @@ -20,6 +20,16 @@ import enum +class OrderingMode(enum.Enum): + """[Preview] Values used to determine the ordering mode. + + Default is 'strict'. + """ + + STRICT = "strict" + PARTIAL = "partial" + + class DefaultIndexKind(enum.Enum): """Sentinel values used to override default indexing behavior.""" From c415eb91eb71dea53d245ba2bce416062e3f02f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 5 Aug 2024 12:29:38 -0500 Subject: [PATCH 739/921] docs: create sample notebook using `ordering_mode="partial"` (#880) --- notebooks/dataframes/pypi.ipynb | 335 ++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 notebooks/dataframes/pypi.ipynb diff --git a/notebooks/dataframes/pypi.ipynb b/notebooks/dataframes/pypi.ipynb new file mode 100644 index 00000000000..3022dc71737 --- /dev/null +++ b/notebooks/dataframes/pypi.ipynb @@ -0,0 +1,335 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Analyzing Python dependencies with BigQuery DataFrames\n", + "\n", + "In this notebook, you'll use the [PyPI public dataset](https://console.cloud.google.com/marketplace/product/gcp-public-data-pypi/pypi) and the [deps.dev public dataset](https://deps.dev/) to visualize Python package downloads for a package and its dependencies.\n", + "\n", + "> **⚠ Important**\n", + ">\n", + "> You'll use features that are currently in [preview](https://cloud.google.com/blog/products/gcp/google-cloud-gets-simplified-product-launch-stages): `ordering_mode=\"partial\"` and \"NULL\" indexes. There may be breaking changes to this functionality in future versions of the BigQuery DataFrames package.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "\n", + "# Preview feature warning:\n", + "# Use `ordering_mode=\"partial\"` for more efficient query generation, but\n", + "# some pandas-compatible methods may not be possible without a total ordering.\n", + "bpd.options.bigquery.ordering_mode = \"partial\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Filter out the relevant warnings for preview features used." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "import bigframes.exceptions\n", + "\n", + "warnings.simplefilter(\"ignore\", category=bigframes.exceptions.NullIndexPreviewWarning)\n", + "warnings.simplefilter(\"ignore\", category=bigframes.exceptions.OrderingModePartialPreviewWarning)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Counting downloads and tracking dependencies\n", + "\n", + "The [PyPI `file_downloads`](https://console.cloud.google.com/bigquery?ws=!1m5!1m4!4m3!1sbigquery-public-data!2spypi!3sfile_downloads) table contains a row for each time there is a download request for a package. The [deps.dev Dependencies](https://console.cloud.google.com/bigquery?ws=!1m5!1m4!4m3!1sbigquery-public-data!2sdeps_dev_v1!3sDependencies) table contains a row for each dependency of each package.\n", + "\n", + "When `ordering_mode = \"partial\"`, `read_gbq_table` creates a DataFrame representing the table, but the DataFrame has no native ordering or index." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.enums\n", + "\n", + "# Without ordering_mode = \"partial\" it is recommended that you set\n", + "# the \"filters\" parameter to limit the number of rows subsequent queries\n", + "# have to read.\n", + "pypi = bpd.read_gbq_table(\n", + " \"bigquery-public-data.pypi.file_downloads\",\n", + "\n", + " # Using ordering_mode = \"partial\" changes the default index to a \"NULL\"\n", + " # index, meaning no index is available for implicit joins.\n", + " #\n", + " # Setting this explicitly avoids a DefaultIndexWarning.\n", + " index_col=bigframes.enums.DefaultIndexKind.NULL,\n", + ")\n", + "deps = bpd.read_gbq_table(\n", + " \"bigquery-public-data.deps_dev_v1.Dependencies\",\n", + " index_col=bigframes.enums.DefaultIndexKind.NULL,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Limit to the most recent 30 days of data\n", + "\n", + "The PyPI and deps.dev tables are partitioned by date. Query only the most recent 30 days of data to reduce the number of bytes scanned.\n", + "\n", + "Just as with the default ordering mode, filters can be describe in a pandas-compatible way by passing a Boolean Series to the DataFrame's `__getitem__` accessor." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "\n", + "last_30_days = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=30)\n", + "pypi = pypi[pypi[\"timestamp\"] > last_30_days]\n", + "deps = deps[(deps[\"SnapshotAt\"] > last_30_days) & (deps[\"System\"] == \"PYPI\")]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "**⚠ Warning**\n", + "\n", + "Without `ordering_mode = \"partial\"`, these filters do not change the number of bytes scanned. Instead, add column and row filters at \"read\" time. For example,\n", + "\n", + "```\n", + "import datetime\n", + "\n", + "last_30_days = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=30)\n", + "\n", + "# Without ordering_mode = \"partial\", one must limit the data at \"read\" time to reduce bytes scanned.\n", + "pypi = bpd.read_gbq_table(\n", + " \"bigquery-public-data.pypi.file_downloads\",\n", + " columns=[\"timestamp\", \"project\"],\n", + " filters=[(\"timestamp\", \">\", last_30_days)],\n", + ")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Find dependencies for pandas\n", + "\n", + "Use assign to add columns to the DataFrame after a scalar operations, such as extracting a sub-field from a `STRUCT` column.\n", + "\n", + "Because the DataFrame has no index, this does not work if the new column belongs to a different table expression." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "deps = deps.assign(DependencyName=deps[\"Dependency\"].struct.field(\"Name\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use an aggregation to identify the unique `DependencyName`s for the `pandas` package. Note: `drop_duplicates()` is not supported, as the order-based behavior such as `keep=\"first\"` is not applicable when using `ordering_mode = \"partial\"`.\n", + "\n", + "A DataFrame with no index still supports aggregation operations. Set `as_index=False` to keep the GROUP BY keys as regular columns, instead of turning them into an index." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pandas_deps = deps[deps[\"Name\"] == \"pandas\"].groupby([\"Name\", \"DependencyName\"], as_index=False).size()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Count downloads for pandas and its dependencies\n", + "\n", + "The previous step created `pandas_deps` with all the dependencies of `pandas` but not pandas itself.\n", + "\n", + "Combine two DataFrames with the same column names with the `bigframes.pandas.concat` function." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "pandas_and_deps = bpd.concat(\n", + " [\n", + " pandas_deps.drop(columns=[\"Name\", \"size\"]).rename(columns={\"DependencyName\": \"Name\"}),\n", + " bpd.DataFrame({\"Name\": [\"pandas\"]}),\n", + " ],\n", + "\n", + " # To join DataFrames that have a NULL index, set ignore_index = True.\n", + " ignore_index=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since there is no index to implicitly join on, use the `merge` method to join two DataFrames by column name." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "pandas_pypi = pandas_and_deps.merge(pypi, how=\"inner\", left_on=\"Name\", right_on=\"project\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a time series to visualize by grouping by the date, extracted from the `timestamp` column." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 5aa35b9c-459a-4b46-b70c-36e6418b61eb is DONE. 920.8 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# When BigQuery DataFrames aggregates over columns, those columns provide a\n", + "# unique key post-aggregation that is used for ordering. By aggregating over\n", + "# a time series, the line plots will render in the expexted order.\n", + "pandas_pypi = pandas_pypi.assign(date=pandas_pypi[\"timestamp\"].dt.date)\n", + "downloads_per_day = pandas_pypi.groupby([\"date\", \"project\"]).size()\n", + "\n", + "# Convert to a pandas DataFrame for further transformation and visualization.\n", + "pd_df = downloads_per_day.to_pandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once you've downloaded the time series with the `to_pandas()` method, you can use typical pandas methods to visualize the data." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAH0CAYAAADVKZLIAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/TGe4hAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd3hUZdqH7zN9Jr03UiihNwGpKsGG6KLo2gsiiBXLIuuuhf2wrKyFJu7iuq7Auir2unakCNJ7LyEhAdLbJNPb98fJDBkIkIQkM0ne+7oGZs6c8p7MzDm/96mSx+PxIBAIBAKBQNCBUAR6AAKBQCAQCAStjRBAAoFAIBAIOhxCAAkEAoFAIOhwCAEkEAgEAoGgwyEEkEAgEAgEgg6HEEACgUAgEAg6HEIACQQCgUAg6HAIASQQCAQCgaDDIQSQQCAQCASCDocQQAKBQCAQCDocQgCdg9WrVzN+/HiSk5ORJIkvvviiUdvPmjULSZJOe4SEhLTMgAUCgUAgEJwTIYDOgclkYsCAAfz9739v0vYzZsygoKDA79G7d29uuummZh6pQCAQCASChiIE0DkYN24cL774Itdff32979tsNmbMmEFKSgohISEMGzaMlStX+t4PDQ0lMTHR9ygqKmLv3r1MmTKllc5AIBAIBALBqQgBdJ5MmzaNdevWsWzZMnbu3MlNN93EVVddxaFDh+pd/+2336Z79+5cfPHFrTxSgUAgEAgEXoQAOg/y8vJYvHgxH3/8MRdffDFdu3ZlxowZXHTRRSxevPi09a1WK++9956w/ggEAoFAEGBUgR5AW2bXrl24XC66d+/ut9xmsxETE3Pa+p9//jnV1dXcfffdrTVEgUAgEAgE9SAE0HlQU1ODUqlky5YtKJVKv/dCQ0NPW//tt9/md7/7HQkJCa01RIFAIBAIBPUgBNB5cMEFF+ByuSguLj5nTE9OTg4rVqzgq6++aqXRCQQCgUAgOBNCAJ2DmpoaDh8+7Hudk5PD9u3biY6Opnv37txxxx1MnDiROXPmcMEFF1BSUsLy5cvp378/11xzjW+7d955h6SkJMaNGxeI0xAIBAKBQFAHyePxeAI9iGBm5cqVjBkz5rTld999N0uWLMHhcPDiiy/yn//8h+PHjxMbG8vw4cN57rnn6NevHwBut5v09HQmTpzIX//619Y+BYFAIBAIBKcQUAE0e/ZsPvvsM/bv349er2fkyJG8/PLL9OjR46zbffzxx8ycOZPc3FwyMzN5+eWXufrqq33vezwe/u///o9//etfVFZWMmrUKBYtWkRmZmZLn5JAIBAIBII2QEDT4FetWsXDDz/M+vXr+emnn3A4HFx55ZWYTKYzbvPbb79x2223MWXKFLZt28aECROYMGECu3fv9q3zyiuv8Prrr/Pmm2+yYcMGQkJCGDt2LFartTVOSyAQCAQCQZATVC6wkpIS4uPjWbVqFZdcckm969xyyy2YTCa++eYb37Lhw4czcOBA3nzzTTweD8nJyTzxxBPMmDEDgKqqKhISEliyZAm33nprq5yLQCAQCASC4CWogqCrqqoAiI6OPuM669atY/r06X7Lxo4d62tSmpOTQ2FhIZdffrnv/YiICIYNG8a6devqFUA2mw2bzeZ77Xa7KS8vJyYmBkmSzueUBAKBQCAQtBIej4fq6mqSk5NRKM7u5AoaAeR2u3n88ccZNWoUffv2PeN6hYWFp9XRSUhIoLCw0Pe+d9mZ1jmV2bNn89xzz53P8AUCgUAgEAQJ+fn5dOrU6azrBI0Aevjhh9m9ezdr1qxp9WM/9dRTflalqqoq0tLSyM/PJzw8vNXHIxAIBAKBoPEYjUZSU1MJCws757pBIYCmTZvGN998w+rVq8+p2Lwd1etSVFREYmKi733vsqSkJL91Bg4cWO8+tVotWq32tOXh4eFCAAkEAoFA0MZoSPhKQLPAPB4P06ZN4/PPP+eXX36hc+fO59xmxIgRLF++3G/ZTz/9xIgRIwDo3LkziYmJfusYjUY2bNjgW0cgEAgEAkHHJqAWoIcffpj333+fL7/8krCwMF+MTkREBHq9HoCJEyeSkpLC7NmzAXjssccYPXo0c+bM4ZprrmHZsmVs3ryZt956C5BV3+OPP86LL75IZmYmnTt3ZubMmSQnJzNhwoSAnKdAIBAIBILgIqACaNGiRQBkZWX5LV+8eDGTJk0CIC8vzy+Se+TIkbz//vs8++yzPP3002RmZvLFF1/4BU4/+eSTmEwm7rvvPiorK7nooov4/vvv0el0LX5OAoFAIBAIgp+gqgMULBiNRiIiIqiqqjpjDJDH48HpdOJyuVp5dIKOhFKpRKVSiXIMAoFA0AAacv/2EhRB0G0Nu91OQUEBZrM50EMRdAAMBgNJSUloNJpAD0UgEAjaDUIANRK3201OTg5KpZLk5GQ0Go2YnQtaBI/Hg91up6SkhJycHDIzM89Z2EsgEAgEDUMIoEZit9txu92kpqZiMBgCPRxBO0ev16NWqzl69Ch2u13EsQkEAkEzIaaTTUTMxAWthfiuCQQCQfMjrqwCgUAgEAg6HEIACQQCgUAg6HAIASRoFbKysnj88ccDPQyBQCAQCAARBC1oJT777DPUanWz7S8rK4uBAwcyf/78ZtunQCAQCDoOQgAJzgu73d6g+jTR0dGtMBqBQCAQCBqGcIEJ/MjKymLatGlMmzaNiIgIYmNjmTlzJt6C4RkZGbzwwgtMnDiR8PBw7rvvPgA+/fRT+vTpg1arJSMjgzlz5py237ouMJvNxowZM0hJSSEkJIRhw4axcuVKv23Wrl1LVlYWBoOBqKgoxo4dS0VFBZMmTWLVqlUsWLAASZKQJInc3NyW/LMIBAKBoJ0hBJDgNJYuXYpKpWLjxo0sWLCAuXPn8vbbb/vef+211xgwYADbtm1j5syZbNmyhZtvvplbb72VXbt2MWvWLGbOnMmSJUvOeIxp06axbt06li1bxs6dO7npppu46qqrOHToEADbt2/nsssuo3fv3qxbt441a9Ywfvx4XC4XCxYsYMSIEUydOpWCggIKCgpITU1t6T+LQCAQCNoRwgUmOI3U1FTmzZuHJEn06NGDXbt2MW/ePKZOnQrApZdeyhNPPOFb/4477uCyyy5j5syZAHTv3p29e/fy6quv+pra1iUvL4/FixeTl5dHcnIyADNmzOD7779n8eLFvPTSS7zyyisMGTKEf/zjH77t+vTp43uu0WgwGAwkJia2xJ9AIBAIBO0cYQESnMbw4cP92nuMGDGCQ4cO+Rq/DhkyxG/9ffv2MWrUKL9lo0aN8tumLrt27cLlctG9e3dCQ0N9j1WrVpGdnQ2ctAAJBAKBQNASCAuQoNGEhISc1/Y1NTUolUq2bNmCUqn0ey80NBSQW0AIBAKBQNBSCAuQ4DQ2bNjg93r9+vVkZmaeJla89OrVi7Vr1/otW7t2Ld27d693mwsuuACXy0VxcTHdunXze3hdWv3792f58uVnHKNGo6nXuiQQCAQCQUMQAkhwGnl5eUyfPp0DBw7wwQcfsHDhQh577LEzrv/EE0+wfPlyXnjhBQ4ePMjSpUt54403mDFjRr3rd+/enTvuuIOJEyfy2WefkZOTw8aNG5k9ezb/+9//AHjqqafYtGkTDz30EDt37mT//v0sWrSI0tJSQM5G27BhA7m5uZSWluJ2u5v/DyEQCASCdosQQILTmDhxIhaLhaFDh/Lwww/z2GOP+dLd62PQoEF89NFHLFu2jL59+/KXv/yF559/vt4AaC+LFy9m4sSJPPHEE/To0YMJEyawadMm0tLSAFkk/fjjj+zYsYOhQ4cyYsQIvvzyS1Qq2Ws7Y8YMlEolvXv3Ji4ujry8vGb9GwgEAoGgfSN5vAVeBD6MRiMRERFUVVURHh7u957VaiUnJ4fOnTuj0+kCNMKWo6UqLI8YMYLLLruMF198sVn32xFo7985gUAgaC7Odv8+FWEBErQoNpuNzZs3s2fPHr80doFAIBAIAokQQIIW5bvvvuPSSy/l2muv5cYbbwz0cAQCgUAgAEQavOAUTm1Hcb5MmDABo9HYrPsUCAQCgeB8ERYggUAgEAgEHQ4hgAQCgUAgEHQ4hAASCAQCgUDQ4RACSCAQCAQCQYdDCCCBQCAQCAQdDiGABAKBQCAQdDiEABIIBAKBQNDhEAJIIBAIBAJBh0MIIIFAIBAIBB0OIYCaAY/Hg9nubPVHY/vYZmVl8eijj/Lkk08SHR1NYmIis2bNAiA3NxdJkti+fbtv/crKSiRJ8lWHXrlyJZIk8cMPP3DBBReg1+u59NJLKS4u5rvvvqNXr16Eh4dz++23Yzab/Y47bdo0pk2bRkREBLGxscycOdM3/ueff56+ffueNt6BAwcyc+bMxn0YAoFAIBA0ANEKoxmwOFz0/ssPrX7cvc+PxaBp3Ee4dOlSpk+fzoYNG1i3bh2TJk1i1KhRZGZmNngfs2bN4o033sBgMHDzzTdz8803o9Vqef/996mpqeH6669n4cKF/OlPf/I77pQpU9i4cSObN2/mvvvuIy0tjalTpzJ58mSee+45Nm3axIUXXgjAtm3b2LlzJ5999lmjzk8gEAgEgoYgBFAHo3///vzf//0fAJmZmbzxxhssX768UQLoxRdfZNSoUQBMmTKFp556iuzsbLp06QLAjTfeyIoVK/wEUGpqKvPmzUOSJHr06MGuXbuYN28eU6dOpVOnTowdO5bFixf7BNDixYsZPXq0b58CgUAgEDQnQgA1A3q1kr3Pjw3IcRtL//79/V4nJSVRXFzc5H0kJCRgMBj8hEpCQgIbN27022b48OFIkuR7PWLECObMmYPL5UKpVPosQXPnzkWhUPD+++8zb968Ro1LIBAIBIKGIgRQMyBJUqNdUYFCrVb7vZYkCbfbjUIhh4PVjStyOBzn3IckSWfcZ2MYP348Wq2Wzz//HI1Gg8Ph4MYbb2zUPgQCgUAgaCgBDYJevXo148ePJzk5GUmS+OKLL866/qRJk5Ak6bRHnz59fOvMmjXrtPd79uzZwmfS9omLiwOgoKDAt6xuQPT5smHDBr/X69evJzMzE6VStmKpVCruvvtuFi9ezOLFi7n11lvR6/XNdnyBQCAQCOoSULOFyWRiwIABTJ48mRtuuOGc6y9YsIC//e1vvtdOp5MBAwZw0003+a3Xp08ffv75Z99rlaptWGcCiV6vZ/jw4fztb3+jc+fOFBcX8+yzzzbb/vPy8pg+fTr3338/W7duZeHChcyZM8dvnXvvvZdevXoBsHbt2mY7tkAgEAgEpxJQZTBu3DjGjRvX4PUjIiKIiIjwvf7iiy+oqKjgnnvu8VtPpVKRmJjYbOPsKLzzzjtMmTKFwYMH06NHD1555RWuvPLKZtn3xIkTsVgsDB06FKVSyWOPPcZ9993nt05mZiYjR46kvLycYcOGNctxBQKBQCCoD8nT2GIyLYQkSXz++edMmDChwduMHz8em83Gjz/+6Fs2a9YsXn31VSIiItDpdIwYMYLZs2eTlpZ2xv3YbDZsNpvvtdFoJDU1laqqKsLDw/3WtVqt5OTk0LlzZ3Q6XcNPsAOTlZXFwIEDmT9//lnX83g8ZGZm8tBDDzF9+vTWGVwbQHznBAKBoGEYjUYiIiLqvX+fSpsthHjixAm+++477r33Xr/lw4YNY8mSJXz//fcsWrSInJwcLr74Yqqrq8+4r9mzZ/usSxEREaSmprb08AWnUFJSwhtvvEFhYeFpFj2BQCAQCJqbNhscs3TpUiIjI0+zGNV1qfXv359hw4aRnp7ORx99xJQpU+rd11NPPeVncfBagAStR3x8PLGxsbz11ltERUUFejgCgUAgaOe0SQHk8Xh45513uOuuu9BoNGddNzIyku7du3P48OEzrqPVatFqtc09TEEt3lYaZyNIPLECgUAg6CC0SRfYqlWrOHz48BktOnWpqakhOzubpKSkVhiZQCAQCASCtkBABVBNTQ3bt2/31ZvJyclh+/bt5OXlAbJrauLEiadt9+9//5thw4bV20BzxowZrFq1itzcXH777Teuv/56lEolt912W4uei0AgEAgEgrZDQF1gmzdvZsyYMb7X3jicu+++myVLllBQUOATQ16qqqr49NNPWbBgQb37PHbsGLfddhtlZWXExcVx0UUXsX79el+hP4FAIBAIBIKACqCsrKyzxn4sWbLktGURERGYzeYzbrNs2bLmGJpAIBAIBIJ2TJuMARIIBAKBQCA4H4QAEggEAoFA0OEQAkjQ4syaNYuBAwcGehgCgUAgEPgQAkggEAgEAkGHQwgggUAgEAgEHQ4hgJoDjwfsptZ/NLJ6clZWFtOmTWPatGlEREQQGxvLzJkzfZl47777LkOGDCEsLIzExERuv/12iouLfduvXLkSSZJYvnw5Q4YMwWAwMHLkSA4cOOB3nL/97W8kJCQQFhbGlClTsFqtfu9v2rSJK664gtjYWCIiIhg9ejRbt26t8+f0MGvWLNLS0tBqtSQnJ/Poo4829lMRCAQCgeCMtMlWGEGHwwwvJbf+cZ8+AZqQRm2ydOlSpkyZwsaNG9m8eTP33XcfaWlpTJ06FYfDwQsvvECPHj0oLi5m+vTpTJo0iW+//dZvH8888wxz5swhLi6OBx54gMmTJ7N27VoAPvroI2bNmsXf//53LrroIt59911ef/11unTp4tu+urqau+++m4ULF+LxeJgzZw5XX301hw4dIiwsjE8//ZR58+axbNky+vTpQ2FhITt27Dj/v5dAIBAIBLVIHtGE6TSMRiMRERFUVVURHh7u957VaiUnJ4fOnTuj0+nkhXZTmxBAWVlZFBcXs2fPHiRJAuDPf/4zX331FXv37j1t/c2bN3PhhRdSXV1NaGgoK1euZMyYMfz8889cdtllAHz77bdcc801WCwWdDodI0eO5IILLuDvf/+7bz/Dhw/HarX6Kn6fitvtJjIykvfff5/f/e53zJ07l3/+85/s3r0btVrdiD9I+6Te75xAIBAITuNs9+9TERag5kBtkMVIII7bSIYPH+4TPwAjRoxgzpw5uFwutm/fzqxZs9ixYwcVFRW43W4A8vLy6N27t2+b/v37+557e6wVFxeTlpbGvn37eOCBB/yOOWLECFasWOF7XVRUxLPPPsvKlSspLi7G5XJhNpt9Vb9vuukm5s+fT5cuXbjqqqu4+uqrGT9+PCqV+LoKBAKBoHkQd5TmQJIa7YoKNqxWK2PHjmXs2LG89957xMXFkZeXx9ixY7Hb7X7r1rXKeMWUVyw1hLvvvpuysjIWLFhAeno6Wq2WESNG+I6TmprKgQMH+Pnnn/npp5946KGHePXVV1m1apWwCAkEAoGgWRBB0B2MDRs2+L1ev349mZmZ7N+/n7KyMv72t79x8cUX07NnT78A6IbSq1eveo9Rl7Vr1/Loo49y9dVX06dPH7RaLaWlpX7r6PV6xo8fz+uvv87KlStZt24du3btavR4BAKBQCCoD2EB6mDk5eUxffp07r//frZu3crChQuZM2cOaWlpaDQaFi5cyAMPPMDu3bt54YUXGr3/xx57jEmTJjFkyBBGjRrFe++9x549e/yCoDMzM30ZZ0ajkT/+8Y/o9Xrf+0uWLMHlcjFs2DAMBgP//e9/0ev1pKenN8vfQCAQCAQCYQHqYEycOBGLxcLQoUN5+OGHeeyxx7jvvvuIi4tjyZIlfPzxx/Tu3Zu//e1vvPbaa43e/y233MLMmTN58sknGTx4MEePHuXBBx/0W+ff//43FRUVDBo0iLvuuotHH32U+Ph43/uRkZH861//YtSoUfTv35+ff/6Zr7/+mpiYmPM+f4FAIBAIQGSB1Uujs8DaCFlZWQwcOJD58+cHeiiCRtCWv3MCgUDQmjQmC0xYgAQCgUBQL3aXnWJzMWKeLGiPiBgggUAgEJxGjb2GyT9MZl/5PqK0UfSO6U3vmN70ielD75jeJIYk+pXUEAjaGkIAdSBWrlwZ6CEIBII2gNPtZMbqGewr3wdAha2CtSfWsvbEWt86Udooesf2pne0EEWCtokQQAKBQCDw4fF4+NvGv7H2+Fp0Sh3/vOKfaJQa9pTuYW/5XvaW7eVwxWFZFB1fy9rjJ0VRtC6aXjG9ZFEU24c+MX1IDEkM4NkIBGdGCCCBQCAQ+Hhv33t8eOBDJCRmXzybQQmDAOgb29e3js1l42D5QfaW7fUTReXW8tNEUYIhgQFxA+RH/AB6RfdCo9S0+nkJBKciBJBAIBAIAFiZv5JXNr0CwB8G/4HL0y+vdz2tUku/uH70i+vnW3aqKNpTuofDlYcpMhfx49Ef+fHojwBoFBp6x/T2CaIBcQOIN8TXexyBoCURAkggEAgE7Cvbx5Orn8SDh99n/p5JfSY1avv6RJHZYWZP2R52lOxgR/EOdpTsoMJWwfaS7Wwv2Q61PZiTQ5L9BFGP6B6oFaLtjaBlEQJIIBAIOjhFpiKmLZ+GxWlheNJwnhn+TLMEMxvUBi5MvJALEy8E5PiivOo8nyDaXrKdw5WHOWE6wQnTCb7L/Q4AnVLHyOSRPD/qeSK0Eec9DoGgPkQdIMF5kZubiyRJbN++PdBDAeRMN0mSqKysDPRQzsisWbMYOHCg7/WkSZOYMGFCwMYj6NiYHWYe+eURii3FdI3oypysOS1mfZEkifTwdK7tei0zR8zk02s/5bfbfuNfV/6Lhwc+zEUpFxGuCcfqsvJL/i+8uePNFhmHQABCAAkaQXu9UWdlZfH444+3yL4lSeKLL77wWzZjxgyWL1/eIscTCBqDy+3iT6v/xL7yfUTronnjsjcI15y9em5zE6IOYXjScB4Y8ACLLl/Er7f+yvys+QAs27+Mo8ajrToeQf14PB6sTitlljLyjfnsK9vHoYpDbbpIpnCBCQStTGhoKKGhoYEehkDAa5tfY+WxlWgUGl6/9HU6hXUK9JBQSAouS7+MUSmjWHt8LQu2LmBu1txAD6vdYHaYKTYXU2wupshcRKmllBpHDSaH6awPs8OM0+M8bX8vjnqR67pdF4AzOX+EBagDkZWVxbRp05g2bRoRERHExsYyc+ZMPB4Pzz//PH379j1tm4EDBzJz5kxmzZrF0qVL+fLLL5EkCUmS/AorHjlyhDFjxmAwGBgwYADr1q3z28+nn35Knz590Gq1ZGRkMGfOHL/3MzIyeOmll5g8eTJhYWGkpaXx1ltvnfOcvv32W7p3745er2fMmDHk5ub6vV9WVsZtt91GSkoKBoOBfv368cEHH/jenzRpEqtWrWLBggW+8/LuY/fu3YwbN47Q0FASEhK46667KC0t9RvzqX3VBg4cyKxZs3zvA1x//fVIkuR7faoLTCAIBMv2L+O/+/4LwF8v/isD4gYEeET+PDH4CRSSgp+O/sT24u2BHk7Q4/F4qLJVcaD8AL8e+5VPDn7CP7b/g//77f944OcHuP7L6xn1wSiGvT+M8V+MZ8qPU3h6zdPM3TKXt3a+xXv73uOLw1/w09Gf+O3Eb+wo2cHhysMUmAow2o1+4segMmBQGQDYVrwtUKd83ggLUDPg8XiwOC2tfly9St/oQMWlS5cyZcoUNm7cyObNm7nvvvtIS0tj8uTJPPfcc2zatIkLL5QDFrdt28bOnTv57LPPiI+PZ9++fRiNRhYvXgxAdHQ0J06cAOCZZ57htddeIzMzk2eeeYbbbruNw4cPo1Kp2LJlCzfffDOzZs3illtu4bfffuOhhx4iJiaGSZMm+cY2Z84cXnjhBZ5++mk++eQTHnzwQUaPHk2PHj3qPZf8/HxuuOEGHn74Ye677z42b97ME0884beO1Wpl8ODB/OlPfyI8PJz//e9/3HXXXXTt2pWhQ4eyYMECDh48SN++fXn++ecBiIuLo7KykksvvZR7772XefPmYbFY+NOf/sTNN9/ML7/80qC/9aZNm4iPj2fx4sVcddVVKJXKRn1WAkFL8euxX5m9cTYAj17wKFdlXBXgEZ1OZlQm13e7nk8Pfcqrm1/lv+P+K6pM18Pyo8t5fdvrnKg5gdVlbdA2epWeBEMCCSEJxOnjCNOEEaIO8X+oQgjRnPK/OgSD2oBCUvBV9lc8s+YZjlUfa+EzbDmEAGoGLE4Lw94f1urH3XD7BgxqQ6O2SU1NZd68eUiSRI8ePdi1axfz5s1j6tSpjB07lsWLF/sE0OLFixk9ejRdunQBQK/XY7PZSEw8vbLrjBkzuOaaawB47rnn6NOnD4cPH6Znz57MnTuXyy67jJkzZwLQvXt39u7dy6uvvuongK6++moeeughAP70pz8xb948VqxYcUYBtGjRIrp27eqzJnnP5+WXX/atk5KSwowZM3yvH3nkEX744Qc++ugjhg4dSkREBBqNBoPB4Hdeb7zxBhdccAEvvfSSb9k777xDamoqBw8epHv37uf8W8fFxQEQGRlZ799MIAgEB8oPMGPVDNweN9d1vY57+90b6CGdkYcHPsy3Od+ys2QnPx79kbEZYwM9pKBiV8ku/rj6jzjcDt+ySG0kCYYE4g3xJIQkyEKn9uFdFqoOPW8xmRaWBkBedd557SeQCAHUwRg+fLjfF3/EiBHMmTMHl8vF1KlTmTx5MnPnzkWhUPD+++8zb968Bu23f//+vudJSUkAFBcX07NnT/bt28d11/n7iEeNGsX8+fNxuVw+y0jdfUiSRGJiIsXFxQCMGzeOX3/9FYD09HT27NnDvn37GDbMX3iOGDHC77XL5eKll17io48+4vjx49jtdmw2GwbD2YXjjh07WLFiRb2xOtnZ2Q0SQAJBsFFiLmHaL9MwO81cmHgh/zfi/4LaqhJniGNSn0ks2rGI+Vvmc2nqpaiVoj4QQKmllMdXPo7D7SArNYsnhzxJnCEOnUrXKsf3xosVmgqxu+xtsrq3EEDNgF6lZ8PtGwJy3OZk/PjxaLVaPv/8czQaDQ6HgxtvvLFB26rVJy9K3guq2+1u1PHr7sO7H+8+3n77bSwWS73rnY1XX32VBQsWMH/+fPr160dISAiPP/44drv9rNvV1NQwfvx4P2uSF6/AUygUp2VAOByO09YXCIIBb7p7oamQjPAM5mXNaxNiYlKfSXx88GOO1Rxj2YFl3NX7rkAPKeA4XA6mr5xOsbmYzhGdmX3RbEI1rZtYEaOLwaAyYHaaOVZzjC4RXVr1+M2BEEDNgCRJjXZFBYoNG/yF2vr168nMzPRZYe6++24WL16MRqPh1ltvRa8/KbI0Gg0ul6vRx+zVqxdr1671W7Z27Vq6d+/e4LiYlJSUevf71Vdf+S1bv379ace57rrruPPOOwFZlB08eJDevXv71qnvvAYNGsSnn35KRkYGKlX9P5O4uDgKCgp8r41GIzk5OX7rqNXqJv3NBILmxO1x8/Sap9lTtodIbST/uOwfbabAoEFt4OGBD/Pcuuf4585/cm3Xa9vM2FuKlze9zLbibYSqQ3l9zOutLn5Avu+lhaexv3w/+cb8NimARBZYByMvL4/p06dz4MABPvjgAxYuXMhjjz3me//ee+/ll19+4fvvv2fy5Ml+22ZkZLBz504OHDhAaWlpg60dTzzxBMuXL+eFF17g4MGDLF26lDfeeMMvNqcpPPDAAxw6dIg//vGPHDhwgPfff58lS5b4rZOZmclPP/3Eb7/9xr59+7j//vspKio67bw2bNhAbm4upaWluN1uHn74YcrLy7ntttvYtGkT2dnZ/PDDD9xzzz0+QXPppZfy7rvv8uuvv7Jr1y7uvvvu0wRdRkYGy5cvp7CwkIqKivM6X4GgqczfMp/lectRK9QsGLOA1PDUQA+pUUzoNoFukd2oslXx9q63Az2cgPLpwU99zWpfvuRlMiIyAjaW1DD5e9RW44CEAOpgTJw4EYvFwtChQ3n44Yd57LHHuO+++3zvZ2ZmMnLkSHr27HlafM3UqVPp0aMHQ4YMIS4u7jSrzpkYNGgQH330EcuWLaNv37785S9/4fnnn/cLgG4KaWlpfPrpp3zxxRcMGDCAN9980y9oGeDZZ59l0KBBjB07lqysLBITE08r5jhjxgyUSiW9e/cmLi6OvLw8kpOTWbt2LS6XiyuvvJJ+/frx+OOPExkZiUIh/2yeeuopRo8eze9+9zuuueYaJkyYQNeuXf32PWfOHH766SdSU1O54IILzut8Oxpuj5sKawXZldlsKtzE9znf896+91i4bSHPrXuO+Vvms7VoKy63sLCdjc8OfcbiPXLm5gujXvB1d29LqBQq/jD4D4Dcrb4tZx6dDztKdvDXDX8F5ADxSzpdEtDxeAVQfnV+QMfRVCRPWy7j2EIYjUYiIiKoqqoiPNy/KqrVaiUnJ4fOnTuj07VOsFlzkZWVxcCBA0+rXVMXj8dDZmYmDz30ENOnT2+9wQnOSFv+zp2NfGM+m4s2U24tp9xaTpm1jHLLyecV1gpcnnOLmyhtFBd3upis1CxGJo8kRB3SCqNvO1z28WUUm4t5cMCDPDTwoUAPp8l4PB6m/jSVDQUbGNd5HK9c8kqgh9SqlJhLuOWbWyixlHB52uXMyZqDQgqsDePTg58ya90sRqWM4s3Lg6Ntydnu36ciYoAEPkpKSli2bBmFhYXcc889gR6OoB2zu3Q3d393N3b32YPRAcI14UTroonRxxCti5af62LINeby6/FfqbBV8FX2V3yV/RVqhZqhiUMZnTqarE5ZJIUmtcLZBC9Wp5Vis5xJeXvP2wM8mvNDkiRmDJnBzV/fzHc533FXr7v8Os+3Z+wuO39Y+QdKLCV0jejKixe9GHDxA3UsQMa2aQEKqABavXo1r776Klu2bKGgoIDPP//8rL2mVq5cyZgxY05bXlBQ4Fdn5e9//zuvvvoqhYWFDBgwgIULFzJ06NCWOIV2RXx8PLGxsbz11ltERUUFejiCdkqVrYonVj6B3W0nMyqTXtG9iNHViht99MnntY+zZSo53A62F29nRf4KVuWvIq86j7Un1rL2xFpe2vASPaJ6MDp1NGNSx9A7pndQ3DRak0JTISBnjLaHwOGe0T0Z33U8X2V/xWubX2PJVUuCOo2/uZi9cTY7SnYQpg5jwaULgsbKmRYu1wI6UXMCp9uJStG2bCoBHa3JZGLAgAFMnjyZG264ocHbHThwwM+0FR8f73v+4YcfMn36dN58802GDRvG/PnzGTt2LAcOHPBbryNSt3VFfQhvqKClcXvcPLPmGU6YTpAalsrSq5YSpglr8v7UCjUXJl7IhYkX8schfyTHmMPK/JWsyl/F9pLtHKg4wIGKA7y18y1i9bGM7jSarNQshicNb7V6KYHkhEmu1J4cktxuhMIjFzzCD7k/sLV4KyvyV3Bp2qWBHlKL8vHBj/nk4Ce+oOf08PRAD8lHvCEejUKD3W2nwFTgswi1FQI6HRo3bhwvvvgi119/faO2i4+PJzEx0ffwBqUCzJ07l6lTp3LPPffQu3dv3nzzTQwGA++8805zD18gEDSSxbsXs+rYKjQKDXOz5p6X+DkVSZLoEtGFyX0ns3TcUlbevJK/XvRXrki/ghB1CKWWUj499CmP/PIIv//q91TZqprt2MFKQY1cpqE9uQITQxJ9tYDmbZnnVwW5vbG9eDsvbZATOx4d9CgXd7o4wCPyRyEp2rQbrE3agwcOHEhSUhJXXHGFXyaS3W5ny5YtXH755b5lCoWCyy+//LTmnHWx2WwYjUa/h0AgaF42F25m4baFADw97Gl6Rvds0eNF6aK4tuu1zM2ay+pbVvPPy//JbT1vI1oXTV51HvO3zm/R4wcDBaZaARTSfgQQwJS+U4jWRZNrzOWTg58EejgtQrG5mD+s/ANOt5Mr0q9gSt8pgR5SvbTlTLA2JYCSkpJ48803+fTTT/n0009JTU0lKyuLrVu3AlBaWorL5SIhIcFvu4SEBAoLC8+439mzZxMREeF7pKa2LTOeIPC4PW6sTitGmxG769yBvR2NUkspf1z9R1weF9d2vZYbMhvu8m4ONEoNI1NG8vSwp5kzWu4d98nBT9p9l3GvAEoOTQ7wSJqXUE0oDw54EIBF2xdRba8O8IiaF2/Qc6mllG6R3Xhx1ItB68L01pRqi7WA2pQA6tGjB/fffz+DBw9m5MiRvPPOO4wcObLB/arOxFNPPUVVVZXvkZ/f9pSsoHXweDzYXDaMNiMl5hLyq/M5XHmY/eX7ya7MJr86nyNVR3C42q9ZvrG43C7+tPpPvov5M8OeCejFfEjiECZ0mwDAc+uea9culBM1cgxQe7MAAfy+++/JCM+gwlbBO7vbT4iDx+PhpQ0vsbNkJ+GacF4f83pQdxpoy01R21bIdj0MHTqUNWvWABAbG4tSqTyt0m9RUdFZu3FrtVq0Wm2LjlPQ8njFicVpweq04sGDUlKikBQoJIXvuVJSolAo/JZLSH43ZY/Hg9PtxOqyYnPZsDltvudnChZXSAokScLldpFfnU9GREaHyzqqj79v/zsbCzdiUBmYkzUnKC7m0wdPZ2X+Sg5XHubdve8yue/kc27TFmmvFiCQA+D/MPgPPLbiMd7d+y639LiFxJAzX+fbCh8f/JhPD32KQlLwyiWvBH3V7rYcA9TmBdD27dt9zSk1Gg2DBw9m+fLlvnR6t9vN8uXLmTZtWgBHKWhuPB4PDrcDi9Pie1idVtyexjVg9SIh+Ykih8txxn1JkoRWqUWn1KFVaX3PVQoVDreD7MpsLE4LhabCdnnjaQy/HvuVf+36FwCzRs4Kmn5BUboonhjyBDPXzmTR9kWMzRhLSujp/ebaMi63iyKTPBlsjxYggDGpYxgUP4itxVtZuG0hf73or4Ee0nmxtWgrszfMBuCxQY8xKmVUgEd0brwWoGM1x3B73G1q0hdQAVRTU8Phw4d9r3Nycti+fTvR0dGkpaXx1FNPcfz4cf7zn/8AMH/+fDp37kyfPn2wWq28/fbb/PLLL/z444++fUyfPp27776bIUOGMHToUObPn4/JZBKF/do4TrcTq9OK2WnG6rRicVpwup2nraeQFOhVenQqHQpJgcvjwu1x+x4ujwu32+23HMCDB5fbhQv/ysNapdZP5GhVWjQKzRldOBqlhk5hncgz5lFhrUCv0hOl65g1lQpqCnhqzVMA3NLjFsZ1HhfgEflzXdfr+PLwl2wu2sxLG17ijUvfCNo4i6ZQYinB6XGiklTE6eMCPZwWwVsc8fZvb+fr7K+5s9ed9IrpFehhNYlCUyHTV07H6XFyVcZV3NOnbdyzkkKTUEkqbC4bxebiNmWFC6gA2rx5s19hQ2/rhbvvvpslS5ZQUFBAXt5Jv6LdbueJJ57g+PHjGAwG+vfvz88//+y3j1tuuYWSkhL+8pe/UFhYyMCBA/n+++9PC4wWNJ5JkyZRWVnJF1980eLH8ng8VNoqMTlMWJyWegOLJSS0Ki16ld730Cq1jbqJeTye0wWSx41KoUKj1DRpNhOmCSPeEE+xuZgCUwE6pQ69Wt/o/bRlHC4HM1bNoMpWRZ+YPjx54ZOBHtJpSJLEzBEz+f1Xv2f1sdUsz1vO5emXn3vDNoLX/ZUQkoBSoTzH2qfjcXtY+cEBDqwvBA8ggSQBkkTtf/JzqfY9+R+/dbQGFZ16RZPeJ4aU7pGoNI0fx7noF9ePcZ3H8V3Od8zZPId/XfmvNidkbS4b01dOp8xaRveo7jw38rk2cw4qhYqk0CTyq/PJr84XAqihZGVlnbX43qmdvZ988kmefPLcF9Jp06YJl1cbp8ZR4wvg9KJRavzEjtfKcz5IkoRSUqKkeS/MsfpYLE4L1fZq8qvz6RLZpc1VST0f5m6Zy87SnYRpwnht9GtolJpAD6levHWD3tr5FrM3zmZE8oigqbJ7vpxvAPSm/+Ww99cT517xLJiNdioKzexacQylWkFK90jS+8aQ1ieGyPjmiwV7bNBj/Hz0ZzYUbuDX478GvEloY/nnjn+yq3QXEdoI5o+ZHxRxco0hLSzNJ4AuTLww0MNpMB3niiwgKyuLvn37AvDuu++iVqt58MEHef7553nhhRf46KOP2L17t982AwcOZPz48SiVSpYuXQrgm5msWLGClStX8txzz512rMWLF59Xt3ebywbIJfzjDfHoVLo2JSAkSSIlNIUjVUewu+wcqz5Genh6m5nVnQ8/5v7If/f9F4CXLnqJTmGdAjyiszO131S+y/mO/Op83tj2Bn8a+qdAD6lZOJ8aQEe2l7Dpf7kAjL69B+l9Y+TJqgfkOasHb4icdxLrccuu5LrrGEutHN1TRt7uMmoqbOTtKSdvTzlwiIg4vSyG+saQknl+1qGU0BTu6HUHS/YsYe7muYxMHtmmrhebizYDcnB+W6umDCcDofOMbSsTrO18Q4IYj8eDx2Jp9eNKen2jb6hLly5lypQpbNy4kc2bN3PfffeRlpbG5MmTee6559i0aRMXXigr+G3btrFz504+++wz4uPj2bdvH0ajkcWLFwMQHR3NkCFDeOCBB3z7f++99/jLX/7CkCFDzuvcvKnJBrWBUE3oee0rUCgVSlLDUsmpysHkMFFkLmpT5uGmkFuVy19++wsAk/tOJis1K7ADagA6lY5nhz/L/T/dz/v732d81/H0jukd6GGdN02tAl1RaOLnJXsB6DemE30vaXpweGynMLoMjMPj8VB+wuQTQwWHq6gqsbBzxTF2rjiGSq0guXsU6X1jSO8bTURc4y0g9/a7l88OfUZ2VTZfHP6CG7vf2ORxtzbHq48D0D2qe4BH0jR8AqiNpcILAdQMeCwWDgwa3OrH7bF1C5KhcReK1NRU5s2bhyRJ9OjRg127djFv3jymTp3K2LFjWbx4sU8ALV68mNGjR9Oli5y5o9frsdlsfiUFNBoNoaGyQFm/fj3PPvssS5cu9Vmamoq3jo5GEZyuk4aiU+lIDk3mWPUxyixl7aYpZX1YnVaeWPUEJoeJwQmDeeSCRwI9pAYzMnmkL47k+XXP897V7zUpbiaYqNsHrKHYLE6+XbQLh9VFcmYko27s1ixjkSSJmJRQYlJCGXRlOnaLk2P7Kzi6p4yju8swVdrI21NG3p4yfv0QIuL1ZA5JYMjVGShVDXNzR2gjeGDAA7yy6RXe2PYGV3e+uk24kmwuG8WWYoA2m4nobYp6rPpYgEfSONpOvpqgWRg+fLif1WjEiBEcOnQIl8vF1KlT+eCDD7Bardjtdt5//30mT25YfZS8vDwmTJjAjBkzuPnmm897nF4L0Nk6gbcVIrQRxOhjADkuw+q0BnhELcNLG17iYMVBonXRvHrJq23KBQHw5IVPEqYOY0/ZHpYdWBbo4Zw3jbUAedwefl68l8oiM6FRWsZO7YtS2TK3CI1eRZcL4hhzZ0/unj2SW2cOZcT1XUnpHolCIVFVbGHzt7ls+7FxFoVbe9xKSmgKZdYy1hxf0yJjb26O18jWH4PKQKQ2MrCDaSJ1iyG2pababesKFaRIej09tm4JyHGbk/Hjx6PVavn888/RaDQ4HA5uvPHcZmSTycS1117LiBEjeP7555tlLF4LkFrR9gUQQIIhAavTislhkoOiI7q0eQtDXb44/AWfH/7cV7wtztD20q5j9bE8NugxXtzwIgu3LeTytMtJCGmb2aMej8dnAWpoDNCm/+WQu7MUpUrBuAf6YQhvHeurn3VorGwd2r36OOs+z2bLd7n0GJ5IWLSuQftSK9UMThjM8Zrj5BpzW3bgzYTX/dUprFObjRFMCUtBQsLkMFFuLfdN+IIdIYCaAUmSGu2KChQbNmzwe71+/XoyMzNRKuWb8d13383ixYvRaDTceuut6OuILI1Gg8vlXyfH4/Fw55134na7effdd5vlB+xyu3B55OO0FwEkSRKdwjpxpFIOij5ec5zUsNQ2e8Gry8GKg/x1vVyA7qEBDzEsaViAR9R0bupxE19lf8XO0p28vOll5mbNDfSQmoTRbsTilOMSGyKAcnacDHrOuqMH8enhLTm8s6LRq7jgyjRyd5VScLiKtZ8c5qr7Gu5S91ojjhqPttQQm5VjNbLbqCXcXx63B5vFidXkkB81p//vcnno1COKjP6xaPVNkwRapZaEkAQKTYXkV+cLASQITvLy8pg+fTr3338/W7duZeHChcyZM8f3/r333kuvXnIhsbVr1/ptm5GRwQ8//MCBAweIiYkhIiKCF198kZ9//pkff/yRmpoaampqAIiIiPATT43B6/5SKpTtykqiUqjkoGhjDtX2akosJcQb4gM9rPOixl7DEyufwOqyMiplFFP7Tw30kM4LhaTgLyP+wi3f3MJPR39i9bHVbS6lGk6mwEfrotGpzm49qSg08dPik0HPPUcEvmq0JElccmt3PvrrJrK3FpO/v5zUntEN2jY9Ih1oOxlJdS1ATcFYZmHvmhOYq+ynCx2TE4/73C6p/b8VoFBJpPaKpusF8XQeEIsupHGTz7SwNJ8AGhg/sEnn0toIAdTBmDhxIhaLhaFDh6JUKnnssce47777fO9nZmYycuRIysvLGTbMfyY/depUVq5cyZAhQ6ipqWHFihWsWrWKmpoaRo4c6bfu+aTB++J/2on1py56tZ6kkCRO1JygxFyCXqUnTBMW6GE1mefXPU+uMZcEQwKzL5rdpsrgn4ke0T24s9edLN27lJc2vMSFiReiV7WtQpYNDYBuqaDn5iC2Uxh9R3di18pj/LrsILfMHNqgmKT0MFkAtRULkDcGqCkWoOpyK5+9uhVTpe2s66m0SnQhKvShGnQhKnQhanS1z11ODzk7SqgoNHN0VxlHd5WhUEh06hlF10HxdB4Yiz703O7Q1LBUNhZubFOZYEIAdTDUajXz589n0aJF9b7v8Xg4ceIEDz300GnvxcXF+bUdAVi5cmWzj9Fb9bk9CiCQ+1BZnBYqrBUcqz5Gl8guaJVtrxlvQU0B3+V+h0JS8Nro19pVy4+HBj7ED0d/4HjNcf654588PvjxQA+pUTQkALo1g56bytDxnTm0uchXTHHg5Wnn3MabkVRhq6DKVhX0WZdeF1in0MZZgCw1dr5asB1TpY3IBAM9hiWiC1XXihv5f32oGm2ICpX67Jb0Edd3pfyEiextxWRvLaHseA15e8vJ21vOyvclkjMj6TYojs4D4wiJqP9a5WuKWt12mqIKASTwUVJSwrJlyygsLAxo7zSvBShYqwc3B4khiVhdViwOC/nV+XQO79zm3H05xhwAMsIz2ozJu6EY1AaeHvo0j654lKV7lnJNl2vIjMoM9LAaTEOKIG76NtcX9HzV/a0X9NwYdCFqRkzoyor/7mfjNzlkXphwxhuwlxB1CHH6OEosJeQZ8+gX16+VRts0muICs1udfLNwh0+8XvvYwAYHip+J6OQQopM7c+E1naksMvvEUEleNccPVHD8QAWrlh0kuVskXQfF0WVgPKFRJz8Lr/BsS13hg0vuCwJKfHw8zz//PG+99RZRUYGbzbdnF5gXhaQgNTQVpUKJzWmjwFTQptJH4eSFzht02t4YkzaGS1Mvxelx8sL6F3yNc9sCXgGUHFq/CyxnRwmbvpEF7Ojbe5CQEbig53PRa2QS8elhOKwu1n+e3aBtvDfjo9XB7QarslVR7agGzvxZnYrL6eb7t3ZTfLQabYiK8Y+ev/g5lcgEA4OvyuDmpy/kzhdGMOKGrsRnhIMHThyq5NcPD7H0qbV89uoWKovNgH8qfFtBCKAOxMqVK5k/f/4Z3/d4PJSUlHD77be33qDqob2lwJ8JtVItZ4IhUWWrotxaHughNQrvhS41vO2V7m8oTw17CoPKwLbibXx+6PNAD6fBnK0PmF/Qc1Yneo0MfNDz2ZAUEpfc2gOA/esLKciuOuc2GeEZQPDHAXndX7H62AbFmXncHpYv2Uv+3nJUGgW/mzaA6KSW7V0XEadn0JXp3PTnIUx8aSQX3ZRJUlfZrViQXcW+tfJ3zWvBqrRVYrQbW3RMzYUQQIKgw+6ujQFqB0UQz0WIOsRXa6bQVIjJYQrwiBqOVwB5g07bI4khiTw88GFAbvBaZikL8IgaxpksQHaLk+/erBP0fFPwBD2fjYTO4T6htnrZAdznyGzyWYCCXAB53V8NCYD2eDz8+vEhDm0uRqGQGHd/PxI7t258U1i0jgGXpXLDHwdz0U2yS7iiULYAhahDiNHJ6e9tJQ5ICCBBUOH2uHG521cNoHMRrYv2BWp6g1fbAt404/ZsAQK4vdft9IzuidFuZM7mOefeIMBYnVafNbGuBcjj9vDT4r1UFAZv0PPZGD6hKxq9itL8GvauOXuX+vTwtpEJ1pgaQFu+O8quFfL6l03qRVqfwNbaiU6WLU9eAQRtLw6o7Xz7BR0Cr/tLISlQSm0rKLipSJLkswLZXDafAAxmXG6Xb5bXXmOAvKgUKv4y/C9ISHx95Gs2FGw490YBxGv9MagMhGtOxvZs/i74g57PhiFcw9DxnQFY/2U21hrHGdf1WiXzjMHdmqGhAdB7fj3Ohq+OAHDRzZl0Hxr4psqRCXLxX2OJBZdLjo9ra5lgQgAJgoq67q/2UCW5oagVal8WmPdvEMwUm4txuB2oFKp23+EeoF9cP27pcQsAL65/MahFqteKmBya7PsN5ewsZePXbSPo+Wz0G51CdHIINpOT9bWCoD5Sw+XYuhpHTVDH1nlrAJ0tBT57WzGr3j8AwOBx6Qy4NDgsrqGRWlRaJW63B2OJXHW8rXWFFwJIEFT4UuDbeBf4puCtBWRznr2oWTDgvcB1Cu3U5pqeNpVHBz2KQWUg15jLkaoz33wDzak9wCoKTfz8zh6gbQQ9nw2FUsElt3YHZKtISV51vetplVrf+QezG+xcLrBjByr48d978Hig90XJDLu2S2sO76xIComoWiuQ1w3mywRrI1W4hQASBBUdJQOsPnwCyBX8Ash7U/H6/DsCYZowXy2g7MqGpWMHglNrAK14dz92q4ukbhFtJuj5bKR0jyJzSDx45IDoM7V6CPZAaLfH7cvWq88FVpJXzbeLduJ2eugyMI7Rt/cIOqu41w1WWSQLIK8F6Fj1sYCNqTEIASRg0qRJTJgwIdDDAOrUAOoAGWCn0pYEUEeJ/zmVbpGygDhceTjAIzkzdatAe9weio7KKclj7uzZpoKez8bI33dDpVVSeMTIgY2F9a4T7IHQPjeypCLBkOD3XmWxma8Xbvdl610xpTcKRXCJH4CoxFoLUK0A8orOYksxZof5jNsFCx3Ddi04KwsWLAiaQEFfDJCwAAU1XhN3R7IAAXSN7AoEtwCq2wfMXG3H7fQgSRAe1/h+Zt/tKuCHPYVoVAp0aiV6tRKtWolOrUCvVqKr81yrVqJTKdFr5GU6lZKYUA1huub/LYdG6bjw6gzWfZ7Nb59l02VAHJpTOpl7BVCwxqN4rSRJoUl+VeBNVTa+fn07lmoHsamhXP1Q/3O2sggUPgtQrQssQhtBuCYco93IsZpjdI/qHsjhnRMhgARERARPrxyvC6wjxwDZXXbcHndQNxb13lQ6mgXIK4CC2gVWJwi6uswKQEikttHWnyMlNTy2bDt2V9MrYKuVEjcNSeWhrK50ijI0eT/1MeDSVPauPUFVsYWN/8vhohv9W5V4BVCuMbdZj9tc1NcE1WZ28PXCHRhLrYTH6vjdtAFo9cF7m/ZZgApNeDweJEkiNSyVPWV7yDfmB70ACt4rrKDZ+eSTT+jXrx96vZ6YmBguv/xyTCaTnwuspKSExMREXnrpJd92v/32GxqNhuXLl7fo+NweN063E+iYLjCVQuUTPd6GsMGI2+PusC6wzEj5JptXnReUljqX20WRuQiQizhWl8sCqLGtEjweD3/5cg92l5tBaZH8cWwPHrm0G1Mv7sydw9O4cXAnftc/ict7xXNRt1iGpEfRNyWcrnEhpETqiQ3VEKJR4nB5eH9DHmNeW8lTn+0kv7z53CJKtYKLb5FvsLt+OUb5Cf8iol4BlG/MD8o2JqcGQDvtLr5dtIuyYzXowzVc+9jAc/Y9CzQR8QaQwGZ2+soSeK8JbSEVPnilZRvC4/HgtLf+D0ylUTQ4KK6goIDbbruNV155heuvv57q6mp+/fXX01xfcXFxvPPOO0yYMIErr7ySHj16cNdddzFt2jQuu+yyljgNH974H0mSOkwNoLpIkoRWpcXisGBz2dCpmre/T3NRbC7G5rKhklRn7TZ+NsxGO1/O34apykZCejjxncNJyAgnoXM4+tDgtf7F6mN9Jv7cqlx6RPcI9JD8KLGU4PK4UEkq4vRxFJTLVobQRgqgr3cWsOZwKRqVgrk3DyQjtmntFjbmlLNg+UHWHi7jg435fLz5GDcMSuHhMd1Ijzn/Fg7pfWLI6B9L7s5Sfv3oINc+NtB3TUwOTUYlqbC6rBSbi4OuXEPdGkBul5sf/72HE4cq0eiUjH9kABFxzWsxawnUGiVhUTqqy61UFJnRh2l8hVGD1fVYFyGAmgGn3c1bj61q9ePet2A0am3DhEJBQQFOp5MbbriB9HR5ZtSvX/1dkq+++mqmTp3KHXfcwZAhQwgJCWH27NnNNu4zUdf91dRsB68YtVmcZ8wOkXz/nPlNSZLTPBW1/0sKCUmSkBTe5RJINHtWhlZ5UgAFK96ZXUpYSpNS4F0ON9//c5dvxp63t5y8vSdrtYTH6WUxVCuI4lLDUKqDw1gtSRLdIruxtXgrhyoPBZ0A8mYVJYQkoFQom2QBMlodvPCN3Cvs4axuTRY/AEM7R/PevcPZnFvOguWH+PVQKR9tPsanW48zYWAK0y7tRufz2D/ARTdlkr+3nGP7K8jeWkK3wfGAHEeYEpbCUeNRjhqPBp8AqlMDaMv3R8nZIRepvPqh/sSlhgV4dA0nKtFAdbmVykIzyd0i21RTVCGAOggDBgzgsssuo1+/fowdO5Yrr7ySG2+88Yxd31977TX69u3Lxx9/zJYtW9BqW94Uez4ZYE67C6vZic3kwOVsPWucVxgpFCeFki5Ujc7QNBdeWwiE9mbVeFNeG4PH42Hle/spyK5Co1dxxT29qS63UpRjpCjXSGWRGWOJBWOJhUObZFeOQikR2ymUhM4RJNRaiiLi9QFLCe4a2ZWtxVuDMg7IFwBd2wPspABq+O937o8HKam20Tk2hAeymqfuzJCMaN6dMowtRyt4ffkhVh0s4dOtx/h82zEmDEzh4Uu70TUutEn7jojTc8HYNDb/L5e1nxwivV8Mao08MUwPT/cJoGFJw5rlXJoLbxB0SmgKh/bJE4CRv+9GSvf6r8nBSmSCgby95b5MsLaUCi8EUDOg0ii4b8HogBy3oSiVSn766Sd+++03fvzxRxYuXMgzzzzDhg31l/XPzs7mxIkTuN1ucnNzz2gtak58AqiBGWAupxuryYHN5MTpqFOZV5LQ6pUoVWf5+3jgbHlvHrcHj8eDxw3uOs+9y/3Ww0PdwsB2ixNFvHRaVkpDaAsC6HwCoLf9lMf+9YVIComxU/uQ1lvuZ9QvS37fanJQfNToE0RFOUasNQ6Kj1ZTfLSaXSvl9bQGFXFpYUQlhRCdFEJ0koGopJBWcZ8Fcyq8LwW+tgaQVwA11AW2+3gV/1mXC8AL1/VFq2peV/Tg9CiWTh7K9vxKXl9+iF/2F/PZtuN8sf044wck88il3egW33jrx6Cx6exfV0BNuY2t3x/1FQz0fkeDLRXe5rJRbCkGZBfY1jLZ4haf0XYsP168gdCVhbJF15sZWmAqwO6yo1EGr0tbCKBmQJKkBruiAokkSYwaNYpRo0bxl7/8hfT0dD7//PPT1rPb7dx5553ccsst9OjRg3vvvZddu3YRHx/fouPzBv6eTQC5XG5sJic2swOHzb8dgUavQmdQozGoWrRmhsfjweOpFT+nCCWbWR6bsdRCVFLI2UVYPdTNBPNmVQQb3kaHjU2Bz9lZyrrPZavJRTd184mfuuhC1KT1jvG95/F4qC6rtRDlGCnKraIkrwab2cmx/RUc21/ht70+TE1UYkitMDL4BJIhvOlu1VPxCqBgtACdWgSxphEuMJfbwzNf7MbtgfEDkrkoM7bFxjkwNZJ3Jl3IzmOyEPp5XzFfbj/BVztOcE2/JB69LJPuCQ0XA2qNkotuyuT7f+5m649H6TkikYg4w8lU+CCrTOx1VRpUBsKU4dRUyhOe8JjGlyo4H46U1PDFtuOE6lQkRehJjtSTEqknLkyLsoHX0MhE/6aoMboY9Co9FqeF4zXH6RzRucXGf74IAdRB2LBhA8uXL+fKK68kPj6eDRs2UFJSQq9evdi5c6ffus888wxVVVW8/vrrhIaG8u233zJ58mS++eabFh2jrw3GKTMGt0uO6bGZnNitTr/31DolOoMarUGFopWKvEmSHCNEPRcIrV5FhdON0+6iqsRCVKKhUTdetUKNQlLg9rixu+xoVcGXBdIUC1DZ8Rp++vce8ECfS1Lol3X25o9eJEkiPFZPeKyezAvlYnEup5uy4zWUHa+hvMBMRYGJikITxlIrlmoHlupKThyq9NuP1qCqFUYGopNCSO8bQ1Ri02JPvKnwx6qPYXFa0Kta96Z1Nuq6wOxWJzaz/HtpiAD6YGMeO/IrCdOqmHlNrxYdp5f+nSJ5++4L2X28iteXH+LHvUV8s7OA/+0q4Hf9k/nbDf0I0TbsNtVlYBypvaLI31fBmo8Pc81D/YM2Fd7rHuoU1omaCht4ZIu+Pqz1sl+3HC1n8pLNVFlObyqrUkgkhOtIidSTHKkjKdIrjnQ+oRSuUyFJJ9thGMusuBxulGoFqWGpHKw4SH51vhBAgsATHh7O6tWrmT9/PkajkfT0dObMmcO4ceP48MMPfeutXLmS+fPns2LFCsLD5YaJ7777LgMGDGDRokU8+OCDLTbGum0wPB4PdosTS40Du8Vf9Kg0SnQhsuhprIWlpZEU8g27otCE0+6ipsLWqABUSZLQKDVYnVZsLlvQCSCPx3MyBb6BFiCz0c7//r4Th81FSo8oLr4l87ysMUqVgvj0cOLT/Rt6OmwuKovMlBeYKC8wUVH7v7HEgs3spPBIFYVHqgBY90U2Nz45hLi0xrscYvQxRGmjqLBVcKTqCH1i+jT5XJqbui4wr/tLa1Cd0x1bUm3jle/3A/DEld2JD2/dDMS+KRG8NXEIe05UsXD5Yb7fU8jXO07gdLn5xx2DGvR9kSSJi2/pzrLnN5K7s5TCI1Wkx8sC6FjNMZxuZ9D0ratbA6i6tNZKF9N6cW0rDhTz4H+3YHW4a8sXhHKi0sKJSiuFRitOt4fjlRaOV1rOuI9QrYrOsSG8/Pt+qHVKHFZ50hedHEJaWJpPAAUzwfFtELQ4vXr14vvvv6/3vSVLlvieZ2Vl4XD4zwgyMjKoqqpqyeHh8Xj8YoC8PyYvSrXCJ3qCtSqqF5VaQXiMjqoSC5ZqO2qNEl1ow2d2WqXWJ4CCjRJLCRanBaWk9AXang1vxld1uZWIOD1X3de3xdoxqLVK4tLCThM1ToeLyiKLLIgKTRzdVUZJXjU//nsPNz99YZPc192iurGpcBPZldlBI4A8Ho+fC6wmX/7+NCT+Z/a3+zBanfRNCeeuERktOcyz0ic5gjfvGsz6I2Xc9e8NfLe7kH+szObhMQ3rYRaVGEJa3xhyd5ZSkldNn87JaBQa7G47BTUFvhTtQFM3ANpYJl/nwmNaR3R+uf04T3y0A6fbQ1aPOP5xxyAMmpNSwOX2UFxt9Qki+X8LJ6pOPq8wO6ixOdl1vIpPthynV4KB4qPVVBaZiU4OOZkKH2Sux1MRAkgQFNStAaRSqLDUijCVRkl4jA6VJrhFz6loDWoMEW7MVTaqy62oNIoGn0MwB0J7L2jJocnnDFb3eDysqJPxdc3D/dGFtH6BS5VaSWynUGI7yVlGA8aksuyFDVQWmVnz8SHG3Nmz0fvsGtGVTYWbgioQuspWhcUp30yTQpM4VF4KnNv9tS67jM+2HUeS4K8T+jU49qMlGd4lhueu7cvTn+/itR8P0Ds5nDE9GhaD6GvPUGxGISlIC0/jcOVhjlYfDRoB5EuBD+uE8YRsAWoNAbRkbQ6zvpYDrq8bmMxrNw1AfcqERKmQSIrQkxShZ3B6/fux2F0sXZfL377bz6HiakYkhlJ8tJqKIhMQ58sEC3YLUKOnYlu3bmXXrl2+119++SUTJkzg6aefxm4P3uq1guCmrvVHkiRfKrtaq2xz4sdLSIQGjU6Fx+OhqsSC+wx1iU4lqAVQI+J/tv2Yx4E6GV9NjblpbnShai6/pzdIsHfNCbK3FTd6H8EYCO2N/4nRxaBVak+mwEed2Y1qd7qZ+eVuAO4YlsaA1MgWH2dDuX1YGrcNTcPjgcc+2EZuqencGyGnxQM+C3IwNkWtWwOoulQeZ1hsy8WSeTwe5v54wCd+Jo3MYN7NA08TPw1Fr1FyYYacrn+oqIaoBP9A6LZSDbrRZ3///fdz8OBBAI4cOcKtt96KwWDg448/5sknn2z2AQo6BnXjf0AOfAaCLsanMcgBvDoUSgUup5vqUkuDms7WFUDB0qTWi9cCdK4aQDk7Slj3hTfjK7PejK9A0qlnNIOulC/SK97dT02FtVHb+5qiVgSPBahuDzDA1wcs9CyWhX/9eoTDxTXEhmr449jGW8JamlnX9mZQWiRGq5P73t2MyeY85zaR8bUCqFgWFt5YtWASQP4usJa1ALncHmZ+uZvXf5G/q9Ov6M7/jT//7vLecgWFRiuaKDlxpbLIXwAdqzmGy+2qfwdBQKPvLgcPHmTgwIEAfPzxx1xyySW8//77LFmyhE8//bS5xyfoIPi6wNcWQXQ55Ru/UhV4c/z5oFAq5BmpBDaLE7Px3FZSjVJO2a4bFxUs+CxAZwmALj1Ww4/v7K2T8ZVyxnUDydDxXYhPD8NmdvLz4r0NttDBSQvQCdMJzI7m6291PngtQN6Kx15RdyYXWH65mYW/HALgmWt6EaEPvv57WpWSRXcOJi5My8GiGv74yY5zTgoi4muzkkosuF1uMsIzgOCJR6myVVHtqAbkauo+AdQCFiC7082jy7bx3/V5SBK8MKEvj152fkkIXiL0ahJrg+Wrar86FYVmPB4P8YZ41Ao1TreTQnPheR+rpWi0APJ4PLjd8uz8559/5uqrrwYgNTWV0tLS5h2doMNwahFErwtM0YYtQF7UWrlfDoCp0nZaKv+peDPBIPjcYF6TttetcCpmo53//WMHzmbK+GpJlCoFV0zug0qr5PjBSrb92HALQaQukli9XCcnWNxg3gDo5BB/C1B9Asjj8TDrqz1YHW5GdIlhwsDgFKkACeE63rxzEGqlxLe7Clm06ux/79BILUq1ArfbQ3W51WeNCJZUeG8T1BhdDCq3BkvtpCismS1AJpuTKUs38b+dBaiVEgtvu4C7hp8hqKeJZCbIcXXH7HYkSS4CazbaUSqUdAqTS10Ei/Csj0bfXYYMGcKLL77Iu+++y6pVq7jmmmsAyMnJISEhoVH7Wr16NePHjyc5ORlJkvjiiy/Ouv5nn33GFVdcQVxcHOHh4YwYMYIffvjBb51Zs2bV1mk5+ejZM/hMuwJ/6vYBc9cWGARQKoPz5tlYdKFqXwCwscRyznYdwRgH5PF4ztoGw+Vw892bu6gpt7V4xldzEZlg4JJb5A7vG7/KoSjX2OBtfW6wIAmE9qXAhybhdrkx1RbXq08A/bi3iOX7i1ErJV6Y0DdoRaqXwenRzLpWzrZ79YcDrDxw5rgtSSGdjAMqtvjEurcycaCp2wTVK1I1OiVaQ/PlJJWb7Nz+9gZ+PVSKQaPknUkX8rv+587abCyZtW6wQ2Vmn4A71Q0WzHFAjb46zZ8/n61btzJt2jSeeeYZunWTTcGffPIJI0eObNS+TCYTAwYM4O9//3uD1l+9ejVXXHEF3377LVu2bGHMmDGMHz+ebdu2+a3Xp08fCgoKfI81a9Y0alyC1qduHzB3rTiQFFKrFTdsaSRJIjRah0qtxO32YDxHPFAwCqAyaxkWpwWFpCAl1N9i4M34KjwS2IyvptBzRBLdBsfjdnv46d97zmmh8xJsgdC+IoghyZiq7Hg8ch81Q7h/YVGTzclzX+0B4L5LutAtvmk9uFqbO4alc9vQVDweePSDbRwtO3NQtFcAVRZbiNXHYlAZcHvcQdGfqm4NIGOdAOjmEqEnKi3c9OZv7MivJNKg5r17h3FxZlyz7PtUutdagA4VV/uSHLyB0G0hE6zRkrN///5+WWBeXn31VZTKxmXrjBs3jnHjxjV4/fnz5/u9fumll/jyyy/5+uuvueCCC3zLVSoViYnB1fn3fHG5XVTYKtCr9BhUjasuHOycWgPIZasNgG4n4seLQiERHqejotCMw3b2Iok+AeS0BU2xCq8pOykk6bRq3XUzvq6a2jdoMr4agiRJjL69B4VHqqgqsfDrhwe57O7e59wuWC1AyaHJJwOgo7RIpwS7vr78ECeqrHSK0jNtTGarj/N8mHVtH/YXVrMtr5L7/rOFzx4aWW+laG8cUFWxGUmSSA9PZ1/5Po4aj9IlsnkavDaVugHQ1c0cAH24uIaJ/97AiSorSRE63p0ytEm91RpKZm27kkNFNURmRnN0d5nPAuQVQO3KBXYmdDodanXrzvjcbjfV1dVER0f7LT906BDJycl06dKFO+64g7y8s38ANpsNo9Ho9wg2Km2VFJmKyK3K5UjVESqtlbg9rdf1vCVxup0+a4haocbtkp8r2ngAdH2o1Erfxc5Sbcdqqj/IORgzwc6UAn9qxldq7+jTtg12dCFqrpjcB0mC/esKObS56JzbZEbK4iEYBJDFaaHCJvdFSwxJrNMF3v/GeqCwmn+vyQHg+ev6oG9jJSa0KiVv1gZFHyiq5slPdtb7+/BlggVhKrzXApQallonA+z8A6B35Fdy05u/caLKSpe4ED55cGSLih/AZz0sNFrRRcvXLF8qfG2ihPe6EYw0SABFRUURHR3doEdr8tprr1FTU8PNN9/sWzZs2DCWLFnC999/z6JFi8jJyeHiiy+murr6jPuZPXs2ERERvkdqanAUy6pL3Wwgq9PK8ZrjHKo4RLG5GKe7YSb75iAjI+M0S9z5cqYaQG05Bf5saA1qn1uiusyK0356mqjXwuL2uFv18z0b3plc3QywuhlffYM446shJGdGMnhcBgAr3zvgq9B7JryWhCJzEdX2M19fWgNvAHSIOoRwTXi9Asjt9vDsF7twuj2M7ZPApT0bF7MZLCSE61h0hxwU/b9dBby56shp63gtQJXF/jfjo9XBI4DkNhheF9j5WYDWHCrltn+tp8LsoH+nCD6+fwQpkS3fo65uJlh1rVG4sqi2K7w3Fb76WNBM4k6lQXeY+fPnM2/ePObNm8ezzz4LwNixY5k1axazZs1i7NixAMycObPlRnoK77//Ps899xwfffSRX5fycePGcdNNN9G/f3/Gjh3Lt99+S2VlJR999NEZ9/XUU09RVVXle+TnB5/P0nsTjNXHEm+IR6VQ4XQ7KTGXcLDiIMdrjmN1nr2WSVZWFo8//ngrjPYkS5YsITIy8qzr1I3/gfaTAn82QiK1Zy2SqJAUPhEUDIGbcHIm5zVtu5xuvntzJ06bi049o7goiDO+GsqQazJI6ByO3eLk53f2+upR1Ue4Jpx4g3ztCXQcUN0eYJIk+brA122D8cnWY2zKrcCgUfJ/44OjfUdTGZIR7TuHV37Yz6qDJX7ve2OAqkutfqnwgbYAuT3ukwKoGVLgLXYXb63OZvKSTZjtLkZ1i+H9qcOJCW29HoLeTLCC2nuUscyK0+EiKTQJpaTE6rJSYik52y4CRoME0N133+17rF27lueff54PPviARx99lEcffZQPPviA559/nlWrVrX0eAFYtmwZ9957Lx999BGXX375WdeNjIyke/fuHD58ZjO1VqslPDzc7xFseAWQTqUjzhBHZlQmncI6oVfp8Xg8VForya7MJrcqF6PNGLSKuz68N3iNQr7ht6cU+DMhSRJhdYsklp0eFO11g3lrJAUanwWodmZXUWjGWGpFrVMydmrwZ3w1BKVSTo1X65QUZFex5fuz3zC9gdCBdoPV7QIPnLQA1bpbK0x2Zn+7D4DHL88kuRWsAy3NHcPSuGVI/UHRp6XCB0kxxGJzMQ63A5WkIsGQ0OQ+YGa7k7dWZ3PxK7/w0rf7sbvcjOubyDuTLiS0npiolqR7bRzQ4SqLnMnmkbPv1Ao1SSFJQPDGATX6ivXDDz9w1VVXnbb8qquu4ueff26WQZ2NDz74gHvuuYcPPvjAl4J/NmpqasjOziYpKanFx9aSeK0k3m7GCklBhDaCzhGd6RzRmXCtLNpMDhP51fkcqjxEmaXMV4Vz0qRJrFq1igULFvjKA0yaNOm0kgGSJLFy5UoAiouLGT9+PHq9ns6dO/Pee++dNq65c+fSr18/QkJCSE1N5aGHHqKmpgaQO8vfc889VFVV+fY9a9YsQO4wP2TIEMLCwujTuQ9P3v8klWWVQJ0q0O3ghno2lEoFEXHyhc9mdmKp9hc6XgHkLREQSDwej88C5I2nMNbGV0QlGNpMxldDiIjTM/q2HgBs+l8uBdlnbgTsDYQOJgsQQHV5bQp8bf2pl7/fT4XZQY+EMO4Z1Tkwg2xmJEni+Ql9GJgaSZXFwf3vbsFslyeKdVPhK4stPgtQsbk4oIUrvQHQiSGJuG1gM8njbWgNILPdyT9XZXPxyyt46dv9lNbY6RSl52839OON2wehVbV+TFdmbRzQoZIaXx+2U+OAgjUTrNF3mJiYGL788svTln/55ZfExDSu3H1NTQ3bt29n+/btgFxLaPv27b6g5aeeeoqJEyf61n///feZOHEic+bMYdiwYRQWFlJYWOjXqXzGjBmsWrWK3NxcfvvtN66//nqUSiW33XZbY0+1wXg8HhxWa4s+rBYzTpsdj83pW+bxeJAkCYPaQGpYKt2juhOrj0UpKXG4HBSaCjlYcZACUwGvzn2VESNGMHXqVF95gAULFviVC3jssceIj4/31U2aNGkS+fn5rFixgk8++YR//OMfFBf7199QKBS8/vrr7Nmzh6VLl/LLL7/4WqKMHDmS+fPnEx4e7jvGjBkzAHA4HLzwwgvs2LGDf77/T47nH+fxBx7H4/b4gqDbswvMi1qr8rkpair8iyRqVcFjASq3lmNymJCQSAmT43y8AabeG017osewRLoPTcDj9vDTO3uwWeqPwwqWQOi6XeA9Ho/PBRYWo2PL0XKWbZJvQH+9vm+T+z8FI96g6NhQLfsLq/ljnaDoyDqZYBHaCCK0EUBgb8Z+TVBrrT+6UDUa3dmtNiabk0Urs7no5RXM/m4/ZSY7adEGXvl9f1bMyOLWoWkBa2JbNxMsytuItjYOKNhT4RttK3vuuee49957WblyJcOGDQNgw4YNfP/99/zrX/9q1L42b97MmDFjfK+nT58OyC63JUuWUFBQ4JfB9dZbb+F0Onn44Yd5+OGHfcu96wMcO3aM2267jbKyMuLi4rjoootYv349cXEtUwcBwGmz8frdN7bY/utS18b26NJPUOtOzhzUSjUJIQnE6mOpsldRZinD7rJTbikHZJeSwWDwKxEQESFfFD777DP++c9/8vPPP5OYmMjBgwf57rvv2LhxIxdeeCEA//73v+nVq5ffeOrGFGVkZPDiiy/ywAMP8I9//AONRkNERASSJJ1WlmDy5Mm+564oF0+99BS3XnErVVVGoNYaFQRdqVsDfagap82F1eSgutxKTLI8o/K5wFx2JAL7t/BewBJDEn3jqiqRZ3nh7VAAAVxym5wabyy1sur9A1w55fS4mWCxAJ2oOekCs5mdOGyy5Tc0SstL/94OwM1DOjEko+1l6J2LxAgdi+4cxG1vred/OwvonxLB/aO7+hVDBEgPS2enbSdHjUfpEd0jIGP1rwF07hT4GpuTpb/l8vavR6gwy5bg9BgD08Z0Y8IFKUEhZr0xQIVGK7p+tS7XU2oBBWsmWKMF0KRJk+jVqxevv/46n332GQC9evVizZo1PkHUULKyss4aq+IVNV68rpmzsWzZskaNoT2iVCiJ1kUTpY2ixlFDqaUUs8N8xmyibdu2cdddd/HGG28watQoAPbt24dKpWLw4MG+9Xr27HlaQPPPP//M7Nmz2b9/P0ajEafTidVqxWw2YzAYzjjGLVu2MGvWLHbs2EFZeZnve3A05yhJMRkoVFKbD6htKJIkERKpxWpy4HK48bg9SArJJzRcHheSJ7B/i/p6gJ20AJ35c27LaPUqrpjch89e28qhTUWk942hxzB/Ie8VQCWWEqpsVT4rQ2tT1wLkjf/Rh6nxKCV25FcC8MilbavmT2O4MCOa/7u2DzO/2M3L3++nd3I40fEnXWAgu253lu4MaByQ1wXWKawT1cVeK93pE4hqq0MWPmtyqKwVPp1jQ5g2phvXDUxGFQTCx0u4Ts4EKzRaMdfGXp9aDTpYY4CaFC01bNiweuNBOioqrZZHl37SYvs3OUwcNeahUap9QZfe454NSZII04ShUWg4XHkYt8d9muAsLCzk2muv5d5772XKlCmNGldubi6/+93vePDBB/nrX/9KdHQ0a9asYcqUKdjt9jMKIJPJxNixYxk7diz/efc/VGuqKThWwP0334/FIscutNcU+DOhUEq+BqgupxuVRolCUqBWqrE5bAFPhffeNOrWAPJWsW2PLjAviV0iuPCaDDZ+ncOqDw6Q2CXcT/CFqENICkmiwFTA4crDDE4YfJa9tQxOt5Nis+yaTgpJovrEyRT4o2VmnG4PIRolnaLa7+cEcOewNHYdq+SjzceY9v423rtuACC7wCA4usL7XGChnTDuPT0A2mh1sGRtLv9ek0OVRRY+XWJDeOSybozvH1zCpy6ZCaEUGq0UIcdvepuiei1A3lT4YJvUnle4uNVqxW73j08IxgyqlkaSJD9XVPPv34ZKq0GnNjTpOBqlBoWkQKVRYXee/LysVivXXXcdPXv2ZO7cuX7b9OzZE6fTyZYtW3wusAMHDlBZWelbZ8uWLbjdbubMmYNCIf8wTy03oNFocLn869zs37+fsrIy/va3vxGbGMuRqiPs37EfwNdot70HQJ+KJEko1QqcdhdOhyyAQHaD2Qi8AMo3yi4wrwCSM9fkG217FkAAg8dlkL+vnILDVfz0zl6unzHI7/vZLbIbBaYCsiuzAyKASswluDwuVAoVcYY4iirkm2xotI7sEjkhoWt8aNDdfJobSZJ4/rq+bM+v5GBRDbsq5TgUY5kVV5CkwnsboaaEppDnS4HXYbI5efvXHP695gjG2jjALnEhPHppJuMHJAcsvqehdE8I49dDpRyx2IhVSDhsLkyVdl9D1GpHNZW2SqJ0UQEeqT+NvsuYzWamTZtGfHw8ISEhREVF+T0Ezc+pndIbiyRJ6FQ6UlJT2LhhI7m5uZSWljJ16lTy8/N5/fXXKSkp8QWV2+12evTowVVXXcX999/Phg0b2LJlC/feey96/cmbXbdu3XA4HCxcuJAjR47w7rvv8uabb/odOyMjg5qaGpYvX05paSlms5m0tDQ0Gg0LFy7k4OGDrPh+BYvmLALA7Wy/VaDPhUot/xzrNkr1usECLYB8NYDC5RlddZkVj0cesyFCc7ZN2zwKhcQVk/ug0asoyjGy+X+5fu8HOhXemwKfaEhEISn8usD7BFBc2+j3db7o1EpGdYsFILvagkqtwOP2UF1mDXhlYpvL5rPUyY1QT/YBe/7rvcz7+SBGq5Nu8aEsuHUgP/1hNBMuSAl68QN1eoKV1hAe622KakKn0pFgkAtuBmMcUKMF0B//+Ed++eUXFi1ahFar5e233+a5554jOTmZ//znPy0xxg6P9+bnTYFvCjqVjkkPT0JSSvTu3Zu4uDh+/fVXCgoK6N27N0lJSb7Hb7/9BsDixYtJTk5m9OjR3HDDDdx3331+RScHDBjA3Llzefnll+nbty/vvfces2fP9jvuyJEjeeCBB7jllluIi4vjlVdeIS4ujiVLlvDxxx8z7IJhvP3628z8q1xE82QGWMeyAAEovQLIEVwCyOPx+Hz46WFyCnxVrfsrPK75mjgGM2HROrLukANnt3x/FGvNydIEgQ6ErhsADXVS4KN1ZBfLVpCucW2nN9v50qM2K+lAcQ0R8ad3hS+3lmO0t367I+/nZFAZiNBE+AVB7zhWCcCfrurJD49fwnUD24bw8eJtuXGwqG01RW30HfXrr7/mP//5D1lZWdxzzz1cfPHFdOvWjfT0dN577z3uuOOOlhhnh6Y5BJBeqSejawYf//gxnSMaVgckMTGRb775xm/ZXXfd5ff6D3/4A3/4wx/Ous6iRYtYtGiR37LbbruN2267jYKaAsqt5cTqY/F4PJQeq8HtcneIFPhT8Yo+Z5AJoEpbJdUOudWD16RtbMcp8Gcic0gC67/Ixlhqpex4DSk9ZIt3oC1A3gDoxBA5QLum4qQF6PD+jmUBAuieePJmfHtcAmXHTVQWm0nvG0OsPpZSSyl5xjz6xvZt1XH5mqCGpWA3u3yZevpIDUdKZaF6Tb+kNiV8vHgzwYqMNvRd5WuWLxA6PI3NRZt9bvRgotHT7PLycrp0kXvghIeHU14up1hfdNFFrF69unlHJwCazwIEch+xYKoSXbcNhsfj8RVBVHSwGCDwd4F5P6O6mWCB6jflNV0nGBJ83yNvBlh7TYE/E9FJ8uy2vOBk1WHvhKLcWk65tbzVx+QVQD4LUK0LLCRKy5HikzFAHQVvYb4iow1dlOye9X5fvTFsucbcVh+XXwB0rfvLEKGhoMaO3elGp1aQ0kYD1cN1apIiaou66uXrWMWpXeGD0AXW6Dtqly5dyMnJIS0tjZ49e/LRRx8xdOhQvv7663P2fBI0jfONAQL5RipJEm6PG7vL7iuyF2i8Rf40Co2vBxiShELZ9mZB54vXAuRxe3C7PSiVEkqFEpUk/0yPVR8jLrzl6lmdifqaoPpS4JvYw6gpWOwutuZVsP5IGRtyyrE6XHSK0pMaZaBTtIHUKD2p0QZSIvXo1C1TETcqKYTcXWU+8z6AQW2gU2gnjtUcI7sym+jE1q21460CnRySjMvhxmyUf1N2jUS1zYlCkmvHdBTCdGpSIvUcr7Rg1sq/KW8mWEZEBluLtwYkLftMNYAO14rULrGhbdL64yUzIYyCKislkjcTzL8partwgd1zzz3s2LGD0aNH8+c//5nx48fzxhtv4HA4TsskEjQPPguQ1HQLkDcQ2uKwYHVZg0IAeTweX5sHtUKN29sFXtlxagDVRVJIKFQK3E43Lofbl2mkUcmz2HxjPhekXNDq4/LVAAqrRwDFt5wAqit41h8pY3t+JQ6Xv/Vy57H621QkhGtJjTKQWiuMOkUbfGIpKULX5HTi+ixAILvBjtUc43DlYS5MvLBJ+24q3iDopNAkqmvdXyq1gjyz/Dw9JiQgLRICSfeEUI5XWijxyG4mby0g73c4EJlgdWsAGfNrA6Bj9D4B1K2NW+ky40NZfbCEXLudGKCm3IbD7mpfMUB14z0uv/xy9u3bx9atW+nWrRv9+/dv1sEJwOV24fbIwuB8XGAAOqUsgCxOS8AKttXF5Tl5bmqlGltt+mdHDID2olIpsNcKIGorHngtf3k1gTEhn2oB8rg9LVIDqCGCJzFcx4iuMQzvEk2UQcOxCgv5FWbyyy0cqzCTX27GZHdRZLRRZLSx+WjFacdRKSQuzozljdsHEdLIxpFRtQKo4hQB1DWyKyuPreRwRevGAXk8Hr8+YDUnTnaBP1IqWz06UgC0l+6JYaw4UEKu3UYcslsw0KnwdV1g1aUnU+APFcuu7cw2LoB8mWCVZlJC1FhNDiqLzKQmyAKo3FpOjb2GUE3wnOd5t43NyMggIyOjGYYiqA+nRxYFCkmBUnF+szi9Sk8FFVid1uYY2nnjde0pFXLRv46cAu9FqVaA1T8TTKOstQAFaAblPa539myqsuFyuJEUkq+PWVOwOlxsOdpwwTO8Swxp0YazWgc9Hg8VZgf55WafMMqvFUbHKiwcr7Bgd7lZcaCEGR/v4B93DGqUtTEqUXYlmY12rCaHrwmsNxOstQOhK22VWF3y7zkxJJEj5WWA3ANsa3HHC4D24s0E21dhIkmtwOlwU11aJxXemNfqhfnq1gDaVyZ/NuExerK3ywK2zVuAEk4Gn09IiKLwSBWVRWbiUhOI1kVTbi0nvzqfXjG9zrGn1qNJAmjVqlW89tpr7Nu3D4DevXvzxz/+kYsvvrhZBxfMeAv2tTTNEQDtxRvAanFagqIqp9f9pVHIN3hv/RtfLExtIHCgx9maeFPhnXVqAaklNR48ASsnf2obDK/7KyxG1+SClVuOlvPAf7dSUm3zW54UoWNElxiGNVDwnIokSUSHaIgO0TAgNfK0991uD+uPlHH34o18t7uQhb8c5tHLGt4iQqNTERqlpabCRkWhmaSusiU1M0reR3ZVdqv+trzur1h9LFqltk4XeC3ZJXKqd0cUQN29N+PiGibER1BemwmW2isVCYlqRzXl1nJi9I1r4N1UqmxVviSGlLAUNpbtACAsRttuXGDd6gSfh/TQwZEqv1T4cms5edV5bVsA/fe//+Wee+7hhhtu4NFHHwVgzZo1XHbZZSxZsoTbb7+92QcZTGg0GhQKBSdOnCAuLg6NRtOiFzuz3Yzb4UbySFit52e58Xg8eBwenDipNlf7LAuBwmQ1yecmyedmsVhwOt04nRLGGidHy80okDBolBi0SgwaVVA0/2tJnC4nDqcdl8WB1iJht9spKy6jyl7FPuM+zA4zBnXrBbRW2aqosslxNp1C5RT48+0C/+mWYzz12S7sLjdxYVou7hbL8C4xDO8SQ2p0y9YVUigkRnaL5cUJffnTp7uY+9NBeiaGcWWfxHNvXEt0UogsgApMPgHUOaIzCklBla2KMmsZsfrYljoFP+oGQAN+XeCP7JTf6xrf8Vxg3eJDUUhQYXag76SF4yaqSixk9IslMSSRAlMBedV5rSaAvO6vGF0MOqUOY22mnlWjwGR3oVJIpMe07c/JmwlWUGXFbpC9FZV1AqF3lOwIujigRgugv/71r7zyyit+sUCPPvooc+fO5YUXXmj3AkihUNC5c2cKCgo4ceJEix+vxlGD0WZEr9Lj0DnOvcE5KLeU43A5cOgc6FWBTbmsslVhcpgwq81YtVZqKm14XB4MJg0Wt9vXBLAuaqWETq1Eq1KgUSlQtDPrkNvtwVQhz+IrTHLmnsFgYGnBUlweFzlVOfSJPb0reUvhtTrF6+N9wsvYxAwwl9vDKz/s55+rjgAwtk8Cc28e2Og4nObglgvT2HvCyNJ1R/nDh9v5/OFRPqvBuYhKDCFvb7lfILRWqSU1LJWjxqMcrjzcagLIW1wvKTQJwNcIVROu4Xil/Dl1iW3bloWmoFMryYgJ4UipCbtevhlX1WmKWmAqILcqlwviWyepoG4NILPRLruQJThhP9nhXdMOYh+9mWAVStl670uFDw/OQOhGX3mOHDnC+PHjT1t+7bXX8vTTTzfLoIIdjUZDWloaTqfztD5Xzc3i3Yv5/PDnXNftOqZ0blyz0vr437b/8WPuj/y++++5O/PuZhhh03lpw0usP7Ge+wbcx9Xp1/DhfzbidnuYMP0C/rXpKF/vKObizDiSInVsPVrhMxV7USsU9E4OY1B6FIPTo+meENam00hBttJ99NImnHY3v5s2gOjEUFQqFbH7Y8kx5ZBdld26AuiUFhjQtBpANTYnj32wjeX75VYAj1zajT9c3h1FAD+vZ3/Xm4NFNaw7UsbU/2zmy4dHEWk4t1U0KkkWgt40Xy9dI7rKAqjiMMOThrfImE+lbhd4OCmAjJJ8A4oJ0RAV0r5blZyJ7glhHCk1UaGQ/xbeVPj08HTWF6xv1bo0fgHQdeo0HS6Tv0OZ8Q0T38FO99pMsDyHnWjkYoge98mmqMHWFb7RAig1NZXly5fTrVs3v+U///wzqampZ9iq/SFJEmq1GrW66bV5GkKuOZcCewGhhlB0zdBwNS06jYKDBWwt38r9uvubYYRNZ79xPwX2AuLC4nBZJcwVLhQKiai4cLYeN3G82sWwzASuv0B2vZTV2Pgtu4w1h0pZc7iU3EoLuVXlfLuvHMgmXKdiZNdYRmXGcnG3WNJjGhc/Eizo9XpKSqoxlTlJSJW/X10iurCpcFOrt1vwtcCobSMAjXeB5ZebuXfpZg4UVaNVKXjlxv5cNzCl+QfbSNRKBX+/YxDXvrGGo2Vmpr2/jSX3XHjOFPkzpcJ3jezKL/m/tGogdF0B5HF7qKmNASqqja/riPE/XronhPL9HjjudJAAVJYELhXevwaQtwu8no3tJP7Hi7ci9MFqCyMVEk67m5pKm+9vHmzFEBstgJ544gkeffRRtm/fzsiRIwFYu3YtS5YsYcGCBc0+wI5OqaUUoNlM6r1jegOwr2xfwAOhvQGcyaHJvrTQ0GgtCoXks/bUnRnFhGoZPyCZ8QOS8Xg85JaZWXO4lDWHSvgtuwyj1cn3ewr5fk8hAEqFRKReTaRBTaRBU/tcI7/Wq4kMkZdF1S6L0KuJCtEQolEG9O8SmWCgJK/aV0oeTmYZHak80qpj8VmAwk5ObhqTAr/hSBkP/HcLFWYH8WFa3po4hIH1BCcHiugQDf+aOIQb/vEbaw6XMvu7/cz8Xe+zbuNNha8pt2G3OtHo5MuoLxC6FUVq3T5glhqHnEggwVGLXAyxI8b/ePG2xDhgspBAnVT4iAygdQWQXw2gQ3WLIFYC7UkA1Qafl9YwLj6UikIzlYVm0rrIAqjYXIzVafUl5ASaRgugBx98kMTERObMmcNHH30EQK9evfjwww+57rrrmn2AHR1v9+B4Q/w51mwYmVGZKCUlFbYKCk2FvtiB1sbkMPmCa5NDkzl2UM6QCIvRUVpjo9xkR5LOPIOVJInOsSF0jg3hruHpOF1udh2v8lmHtuZV4HB5KDPZKTPZAVO9+6kPlUIiRKtCr1ai1yj9/tfVPjfU/q9Te99XoNfI2wzrHE1qdNMDlSMT5G3rCqAuEXL7mSNVgRFA3hmc1eTAZpYzE8/lAlu2MY9nv9iN0+2hX0oE/5o4hMSI4Ljw1aVXUjhzbx7Ag+9t5d9rcuiVFM6NgzudcX1diBp9uAaL0U5FoZmEjHDAvylqa00u6lqAfK6VCC17S71NUNvHjbUpeFPhd5fVcKlGh9NemwpfpzKx2+NGIbV87I2/C+xkF/jDW2Vh1G4EUJ1MsNDUOCoKzVQUmejUqxNh6jCqHdUcqz5Gt6hu59hT69Ck6MPrr7+e66+/vrnHIqiH5rYAaZVaukZ25WDFQfaW7w2YAPLOXCO0EYSoQ6guk4VeWIyeQ0Wy9Sc1yoBe07DaRyqlggvSorggLYpHLsvE5nRRYXJQabFTaXZQaa793+Kgwmynyuyg0lz7vHZZhdmB3enG6fZQZXFQZWla0LlGqeChMV15MKtrkyrwRtUjgLw312M1x1p1BuVtYOhLgS8+2cNIfYbPxuly89K3+3lnbQ4Av+ufxKs3DmjwZxkIxvVL4tHLMnl9+SGe/mwXXeNCuCAt6ozrRycZOG60U1Fo8gmgjPAMlJKSakc1ReYiX3PSlsLsMFNpqwTkIOjiHG8TVC3ZJR23BpCXjNgQ1EoJk8OFIVqHsdBMZbGZlD4pKCUlFqeFYnNxi39Obo/7pAssLIXNtdc6RaiKCrPjrBO9tkZYnUwwZ4g3E8yMJEmkhqeyt2wvedV5bVsACVoHs8NMjUO+kDWXBQhkN9jBioPsK9vHZWmXNdt+G4PPdB/i38AxLFrH/trKqN7Kok1Bq1KSGKFstMXB6nBRYbZjsrmwOlxYHC4sdhdm+8nXvuf201+fqLKw81gV838+xFfbT/DihL6M7NY48eq1AFXUEUAxuhjCNeEY7UaOGo/SI7pHo/bZFIx2IxU2uZKyd9ZcVVtd+EzuryqLg0c+2MbqgyUATL+iO49c2q1NxGI9flkm+wqM/LS3iPvf3cLXj1xEQnj935/oxBCOH6j0qwitUWpIC08jpyqH7MrsFr+xFppkV2+oOpRwTThHKirl19E6juTKf//2cmNtCmqlgq5xoewvrMYTKt/qqorlVPiU0BTyqvPIM+a1+OdUbC7G4XagklQkGBIwlslW1Yranlkpkfqgnhw0Fm8mWFWtuvBex9LC0thbtjeoMsEaJICioqIafAHzdocXnD8lFvkiplfpCVE3ny+/V3QvvuAL9pXva7Z9Npa68T+Ary5GeIyOQ0VyNdtuAciM0KmVJEU0vTyAx+Phm50FPP/NXo6Umrj97Q1cf0EKz1zTi9jQhvVf8woga40Da40DXagaSZLoGtmVbcXbyK7MbhUB5LX+xOpjT0+Br0cA5ZSamLJ0E0dKTOjUCubePJCr+wXGwtgUFAqJebcM5IZ/rOVgUQ33vbuFD+8bXm9j1ShfILTZb3m3yG7kVOVwuPIwo1JGteh46/YAg5OTCI9eid3pRqNqu93Fm4vuCWHsL6ymujZXpW4mWF51HrnGXIYmDW3RMXitP4khiShQ+mo1FThlV3Jbb4FxKt5MsBNuB5HgVwwRgisVvkECaP78+S08DEF9lJhlAdSc1h84GQi9t2xvs+63MdTtXwQn03fDonUc3Hn+FqBAIUkS4wckc0n3OF774QD/3XCUz7cd55f9xfx5XE9uGZJ6ztRvtVZ5stpwkZmkULnYXpeILmwr3tZqcUBnbYJ6igBae7iUh97bSpXFQVKEjn9NHELflMD3m2ssoVoV/5o4hGvfWMuO/Eqe+Xw3r93U/7QJYNRZmqL+dPSnVgmEPs2KWvsbMtVe1bvEhrT5shDnS4/EMNgBRW6XXyZYeng6vx7/tVXSsuvWADJV2nC7PCiUEkdq5LG0l/gfL74q3GYbQwFTpZwsEIyp8A0SQHffHdh6MR0VrwWouYuqdY/qjkJSUGoppcRcQpwhrln33xC8s6Lk0GS/9N2wGF29GWBtjQi9mhcm9OWGQSk88/lu9hYYeeqzXXy65Rh/vb6ffGE+C5EJBmoqbFQWnaw23NqB0N4LVd0MsPpqAL27LpdZX+/F5fYwMDWStyYOJj4s+IKdG0p6TAh/v30QE9/ZwKdbj9E7OZwpF3X2W8ebCl9dasFpd6GqdWHUDYRuabwB0F4XjlcAlbrl2mRd29mNtSl4b8ZHrFYS8LcAQetkgtUXAB0arWNtSfuqAeSlW+3EdV9ZDaPDtFiq5aao3jjCYLIANSn83e12c/DgQdasWcPq1av9HoLmw2cB0sfjtLvY9lMeR7aX+HpmNRWD2kDncPmCHig3mPfinRyajLnajsspV0a1qaGsNgOsPcyMLkiL4qtpo3j2ml4YNEo2H63gmtd/5W/f7cdiP3MRzfoywVrz5gonLUD11wCSx/fiN3uZ+eUeXG4P11+QwrL7hrdp8ePlosxYnrlGtpT+9X97WXOo1O99fZgabYgKjwcqi09+Rt0i5eDOw5WHfb3sWoq6vyHAN4k4bq9Nge/A8T9evFbk3dXyZ1RdZsXldPtuxkerW1EAhXXCWFo3Bb42UL0dXOfq4nXpFVfbCKudKFUWmX2W5BOmE74+kIGm0UHQ69ev5/bbb+fo0aOn/cAlSWrxysgdCa8FKM4Qx77fCvjtU7nAmtagouugeLoPTSC5WyRSE8zcvWJ6kV2Vzd6yvVzS6ZJmHXdD8FmAQpL9KqNml8kXqk5R7ScwUKVUcO/FXRjXL4nnvtrDj3uLeHNVNt/sPMEL1/VlTM/TXZy+QOjC0wVQnjEPh9uBWtGyRTh9FqDaKtAOuwtzlXxzjYjTk1tq4u01OUgSPDm2Jw+M7tImgp0byuRRGewrMPLJlmM8/P5Wvnx4FBmxsuVHkiSiE0MoyK6ivMBEbCd5Fp8WnoZKocLsNFNgKvCJk5agbh8wh82F1STfVA7VyL+nrnEdtwaQl9QoAzq1gkq7G6VGgcvuxlhq8Yn6/Op8nG5nszSbPhM+F1hoCsZceQKhi9JSWCh/Tu1holeXMJ2a5AgdJ6qseMLka1RFoZnMCzujV+mxOC2cMJ3wm1gFikZbgB544AGGDBnC7t27KS8vp6KiwvcQAdDNi08A6eN8lgBJApvZyd41J/hi7jb+88xv/PbpYUryqxs14+wVLXfkDUQckNVppdwqf1eSQ5P9MsAOtQP315lIidTz1sQh/GviEJIjdByrsHDPkk089N4WCqv8G93WlwqfYEjAoDLg9Dh9AcotyakxQN4AaK1BhS5EzY5jlQAM6BTJg1ld25X4AVnkvDihLwNTI6myOJj6n83U2Jy+971xQBV1AqHVCjUZ4RkALV4Rum4QtK8HmE7JwXJRA8iLQiHJbjAJlOHyzbiqxEKiIRGNQoPT7fRZ0lqKYzW1RRDrtMGwa+TfSnyYlgh9y05kAkG3WtdjTW0XloraVPhOYXJ9rWCJA2q0ADp06BAvvfQSvXr1IjIykoiICL+HoPnwusDiDHG+C9xFN3fnuscH0mtUEhq9ipoKG9t+yuOjv27ig+c3svnbXF+l3rPhqwgdABeY98Idog4hXBPuO7fwGD2HiuQA6Mw2GADdUK7oncBP00cz9eLOKBUS3+4q5PK5q1i8NgeXWxaxkYmyAKoqseB2yS5PSZJ8cUDZVS3rBqux1/hEqi8F/pQA6N3H5UKW/dpgsHND0amV/POuwcSHaTlUXMMfPtyOu/YzivYJoNMDoaFlXZUOt8NXJDUpJMmXWWSI0tYW/oQuwgIEnIwDsmrl211VsQWlQtkqQbk2l833OaWEpfiyXStr+5O1N+uPl+6151XokT1ClXVS4SF44oAaLYCGDRvG4cOt1+umI+P94cTpTwqg8FgdnXpGc+ldvbjnlVGMu78fXS+IQ6lSUFFgYsNXR3j32XV8+spmdq44htlor3ffPaN7AnItEe+NrrXwme5Dk5EkyXdRCIvR+YogtkcLUF1CtCqeuaY3X0+7iIGpkdTYnDz39V6e+Gg7AGFROlRqBW6Xx/f3AegSWSuAWjgOyGv9idZFE6qRL2anBkDvPm4EoG9KeIuOJdAkhOv4512D0agU/LS3iPk/HwRONkWtrycYtKwFqMRcgtvjRq1QE6uP9V0fMMiunJRIPQaNKPMGJytCl9fW3ak8JRA615jbYsf2ZuoZVAaitFG+yWmBQ3ZXtrcUeC9e0ZltlePSKov9m6K2WQH0yCOP8MQTT7BkyRK2bNnCzp07/R6C5sNbBTrOEHcySyr6ZICpSq2kywVxXHV/P+559SIundiTTj2jQILCI0Z+/fAgS/68lq8XbufAhkK/4OlQTajvArC/bH8rnlWdGkCnFkGM0XGoGYogtiV6J4fz6YMjee5aucP71zsLMNudSAqJiPgzB0K3dCZYfSnwvhpAsXo8Hg+7T8gWoLaY7t5YLkiLYvb1/QBYuOIwRUYrUYmyhaWq2ILLdfK3VTcQuqXw3lgTQxJRSAqfALKqZdeKsP6cxNsTLM8hTwar6qTCQ8tagOpWgHa7PJgq5et4trl9xv948Vrwd1eaUKgkXA431eXWk1a3IGmK2ugpwu9//3sAJk+e7FsmSZKv940Igm4e6laBjlREYzXJbQVCo+vPsNHqVfQamUyvkcmYKm0c2lzEoU1FFB+tJm9POXl7yik8UsXo204W0OsV3YujxqPsLd/LyJSRLX9StXgv3r4aQLWpoRhUlNZ0vAwWpULi7pEZvLkqm4IqKzvyqxjRNYbIBANlx2tkASTfe0+mwrdwU9RTW2AAVJXIQiw8Ts/RMjPVVicalcI322vv/H5wJ5b8lsuu41WsP1LGtQOSUWuVOGwuqootPpeYVwDlVOW0WK8pXwbYKTWAvNWFO9Lv51x4LUCHzFYGoW3VVPi6AdA1FVY8HlCpFeyvrI3TaqcCyCvsimpshMXGUFVopqLITFp0G3eB5eTknPY4cuSI739B8+C1/uhVetzVcjaURq9Cqz+3Zg2J1DLw8jRueupCbp81jL6XpABw/ECF33qBKojoFUApoSl4PB7fxbvEJQeYdorSE6LteOb7Qely76mtefLnFJV4ekuMrhGyBSinKgeXu+UmG96bQn1FECPj9eyqjf/plRiGWtnyzSSDhWGdowFYf6QcSZLqBEKfdIOlhqWiUWiwOC0+C0BzU9cCBCdT4AtrXSvt9cbaFBLCtYTrVJTVisPTUuFbUADVrQHkTYEPjdGRVyH/ltqrq9+bCQagiJCDvCsLzT4L0LHqYy16/Woojb5ypaenn/UhaB7qdoE3VXjdXw1rpVCXqMQQhlyTAcg3Urv1ZBZLrxg5E2xfWesGQvssQKFJWGscOO3yhSnPJlt/2qtf/FwMrm2+ueWoLIB8tYDqpMInhyajVWqxu+0tdnOFkzM0703C5XJTXXuTDY81+AKgO4L7qy5DawXQxhy5XUt04ulxQEqFks4Rcp2tlorVOrUGkNeNnGMRKfCnIkkSPRLDMEkgqSU8HjCWWnzZei1Zl6ZuDSDvZ6QIletHRejVxIZqWuS4wUBmreXNUht8XlFkJtGQiEqhwuF2UGQuCuTwgCYIoLS0NCZOnMi///1vsrNbpyBbR6RuF3ivheRM7q9zERKhJSRCAx4oPVbjW+5NhT9Wc4wqW9V5jrjheGOAUkJTfOdmiNBwuFQeW0dxqZzK4DoWILfbU28xxLo315aMAzo1Bqim3IrH7UGpVhASofFZgNpzBlh9DO0cjSRBdomJkmpbvRYgaPlAaK8ASgpJwu32UFMbW3LI216huVxgVcdh75dw6Gc4ug4Kd0H5EagpBrsZWrjYY3ORWZsK7zTI1vSqYovc405lwO1xk1/TMi4ZrwtMtgDJn42tVhB0iw9td6Uj6uKN4yyWajPBCk0oFUo6hcqp8MHgBmu0n+Gll15i9erVvPzyy0ydOpWUlBRGjx7N6NGjycrKIjMzsyXG2eHwWYD08dRUnB4A3Vji0sMx7Syl5Gg1yd0iAYjQRpASmsLxmuPsL9/PsKRh5z3uc+FwOXzp/UkhSVQcrdMEtbYGUHsNDDwXvZPD5aJtZgdHSmtIqxVAZqMdm8Xpc392jujM/vL9ZFdmk5Wa1ezjMDvMPgHuLYJYVVybARarB4kOawGKNGjoUdtgc2NOOX28PcHqWOmg5QOhfX3AQpMxV9nwuD1IColqIEynIi6s8dbi0zCVwtuXQ/WJs6wkgSYUNCGgrf1fE3pyWUgsJPaDpIEQ3wuUgal5440DMqogCjkrKUOKJS08jf3l+zladdQXX9eceGsApYSmcKTWAlRV64pr75Zur3sv12ajP/5NUXONueRV57XKPedsNFoA3Xnnndx5550AFBQUsGrVKr755hseeugh3G63CIJuJnx9wAyxVB85WSiwqcSlhZG7s5SSvGq/5b1jenO85jj7yva1ypex0FSIBw86pY5oXTR55fIsICxax8HCQqDjWoDUSgX9O0WyMaecLUcr6HZhGIZwDWajncpCMwmd5XRzbxxQS1mAvNafKG0U4Rr5mHVrAOWXWzBanWiUHScAui7Du8Swv7CaDTllXDRS/iwqC8243R5fo9uWrAXk8XgoNMm/laSQJKqL5euDMkSFR5IDoM/bsuB2w+f3YztehMUYjyo2GrXehlprRuE2gd1rSfaAvVp+1Jx1j6DUQkIfSB4oC6LkgRDXC1Qt7wbyfk8LXE6iOCno08PT2V++v0WykqpsVVTb5ettcmgyO2pjLb1d4Nv7RM+XCVZtoT8K30QuLTwNjtMqxVzPRZMiTc1mM2vWrGHlypWsWLGCbdu20bdvX7Kyspp5eB0XrwCK18fXcYE1fVYXlyZfAIpPEUC9onvx09Gf2FveOoHQdavXSpLkOzd1uIbSw7Klq71fGM7G4PQonwC65cI0IhMMsgAqMp0UQC3cE+zUFhgAVaUnBZDX/dUjMQyNquMEQHsZ2jmaJb/lsuFIOWHj+6BUK3A55BYLkbWlC7wC6EjlEVxuF0pF87V1qbBVYHXJv5vEkERyK+Q6Xg6dAhzNlAH22wLcB34mb0UiTosCMNa+oUQZ2wV1cjLqxHg0iXGo46NQx4ShjglFHaVDIdnBbpIfVflQsAMKdoKtCk5slR9elBpZFHkFUdJAiO/d7KLI6445arPTG40vo9Hr4m2JQGhv/E+MLgaD2uALgj4cTCnw+76Grf+BtOHQ61qIbT4PjjcG6LjJhj48AkvtRC6YagE1WgCNHDmSbdu20atXL7Kysvjzn//MJZdcQlRUVKMPvnr1al599VW2bNlCQUEBn3/+ORMmTDjrNitXrmT69Ons2bOH1NRUnn32WSZNmuS3zt///ndeffVVCgsLGTBgAAsXLmTo0KGNHl8g8bqJYg2xVFTUWoCimm4Biq8VQJWFJhw2F2qtfEFu7UBon+n+lBpAptpvYkpkx8wA83JaIHSigROHKv0ywbzFEI9UHfGVn2hOzloDKE7Pmg7q/vLiDYQ+UFRNlcVBVKKB0vwaKgpMPgGUEpaCTqnD6rKSX51PRkRGsx3fW0g0Th+HRqnx/Yaqa6sLd40/zwDovPWw/AUqjxhwWhQowsNRJyTgOH4ct9mMq7QUV2kp1jOUfVNGRaFOSUGdkoImLRVdn0fQX9YHld6KVLgDTmyHgu2yMLJWwYlt8mOLdwcaWQRlXARZfwbt+VsZY0K1xIZqqXDKwc6VtRYg7+fSkgIoJSwFp93lK0q7v7Yxa0AFkNsFy5+HtfPl14d+lF/H9oBe46HX72Qxeh7XllCtytcTTBWpgdqJXGqn4KkF1Og7zf79+wkJCaFnz5707NmTXr16NUn8AJhMJgYMGMDkyZO54YYbzrl+Tk4O11xzDQ888ADvvfcey5cv59577yUpKYmxY8cC8OGHHzJ9+nTefPNNhg0bxvz58xk7diwHDhwgPv70ppPBiq8PmDaO/HL5BxMW03QBFBKp9blTSo/VkNRVvnl5A6FzjbnU2Gt8VX9bCl8RxFOyV0prS6a35xYYDcGbCp9dYqLCZK+3J1hqWCoqSYXFaaHQVEhSaFKzjsFrAfKvAXSyCvTudfKFvaMFQHuJDdXSLT6Uw8U1bMwtJyoxRBZAhWY6D5DXUUgKOkd0Zl/5PrIrs5tVANW1osLJFPgSt+xaOS8LkLkcPpmCx+mi7HA8YCfusUeJvuMOPB4PrspKHCdO4Dh+HMfxEyefnziB49gx3DU1uCoqcFVUYN2922/XyuhodP36ou/bD12/J9Bf0xeVwnhSEJ3YXiuKKmsF0nbIWwd3fAKG6KafUy09EkPZbpSz92rKrbgc7ha1ANWtAeS1dCu1CmrcHvQaJckR+mY/ZoMwl8Mnk+HICvn1wDugpgiOrILSA/DrAfj1NYhIhZ6/kwVR2nBoghUzMyGME1VWbPraTLBCM2m9TtYCaokJXGNotAAqKytj165drFy5kh9++IFnnnkGjUbD6NGjGTNmDFOnTm3wvsaNG8e4ceMavP6bb75J586dmTNnDgC9evVizZo1zJs3zyeA5s6dy9SpU7nnnnt82/zvf//jnXfe4c9//nMjzjSweC1A4a5o3G4TkkLCEHF+gY1x6WEc3VVGSZ7RJ4Bi9DEkGBIoMhdxoOIAgxMGn/fYz0bd4E04WQQxvzYFviPGlNQlOkRDl9gQjpSa2JZfQZd6BJBaoSY9PJ3sqmyyq7KbXwCdYgHyeDx1qkDrOmwGWF2GdY7mcHENG46Uc7U3EPqUTLDMqEz2le/jcOVhLku/rNmOfZoVtfbmmme1g/I8BJDHA188BMZjVJak46yyo4qLI/LGGwE5nVwVFYUqKgp9nz717sJlNNYRSMexZR/BumsX1oMHcZWXY1q1GtOq1b71VclJtYKoL/p+V6O7vjdKZxkc2wTf/QmOb4HFV8Ndn0P4+X3PuyeEsfZQGR6lBC4PxjILGZEZABSZi7A4LehVzSdK6qsBpAxVg0u2/njjxVqVgh3w4Z1QmQdqA1z3BvSVixtjqYRDP8G+r+Dwz7L7csMi+WGIhR7jZDdZl9Ggati9qHtCKKsOllCu8BCKnAo/OLQnCkmBxWmhzFpGrD62xU73XDRaAEmSRP/+/enfvz+PPPIIW7Zs4Y033uC9997jww8/bJQAaizr1q3j8ssv91s2duxYHn/8cQDsdjtbtmzhqaee8r2vUCi4/PLLWbdu3Rn3a7PZsNlsvtdGo/GM67YGdatAay3yxSw0Usux2uJZ8eFadOrGq/G4tFoBdPSUOKCYXhSZi9hXtq/1BFBIMjazA7tVtvwcDAazcJAwOD2KI6UmthytYPAgWYRUFlt8mT4gu8Gyq7LJrszmopSLmvX4virQtQLIXGXH6XAjKSSqFB6qLA7USonuiR33sxrWJYb3NuSxIaeMOy6WL+BnSoVv7lituinwcFIAlbpdqNQS6TGGpu143d/h4Hd4JC1lB6OBIqKnTEahbfjESxkejjI8HF3Pnn7L3TYbtv37sezcxf+zd95hbtTX1//MqEvbey/uvXcwNmAw1bTQCSVAEgIBAmmkkF8SEt4ESEhC6J2Q0LuNKcY2NtjGFbd1t7f3plVv8/7x1Wi1u1p7i9brovM8eqTVjmZGq9XMmXvPPce1fRvObdvxHDyIr6qatqpq2j79NLSsvrgY4/hxxI3/BQm1jyLVl8AL58B334OU4r69N4KTYBI4DBIWh0JLnZPirDQS9AlYPVbKrGWMTBl55BX1EKEU+Pg8rJXi2O0xymAfpOPct6/Dh3eCzwXJxXDVq0J/pcKUBBMuFzevE/Z/ASUfwe4l4GiAza+Imz4eRpwtqkPDzzpsi1LVAZX5PIxBXMjpNDqyLdlU2iops5YdXwRo06ZNrFixghUrVrB69Wra2toYP348P/7xj5k3b95A7GMINTU1ZGZmdnguMzMTq9WK0+mkubkZv98fcZldu7rPu3rwwQf5/e9/PyD73BeEu0D7rOKE5zPKnPbQ8tAy8UYtGfEG0uMNZMQbyYg3kJHQ8XF6vJEEozZUYszoRgg9JmUMK8pXHBVH6HADNzXk0xSvY3eDOHmc6KOhPcHUwmTe3FjBxtJm4s8a2SFLJyFNXKEOTRrKZ6WfcbD1YFS37fA6qHMKCwa1Baa2v+JTDOysUbPa4jFooyfsPd6gOkLvrLaiDw4nNNU4OpDU0Ch8a3RH4VUNUHsLTHyPrLJCQaq5b87cFRvg898B0JpwLd6qpWhSUki+4oqo7LNsMGCaOBHTxImh5/w2G67tO0KEyLVtG96qKjwHD+I5eBDrB+D94U2kJf8Pmg/C8+eISlDmmD7tg3oyrsePBSkUiVGUUMTWhq2UWkujSoAq29orQGqrvxkxAn9UCZDfC5/+BtY9KX4efjZc+jSYDiNd0Zlg1Pni5vdC6VeCDO36CNqqYfvb4qY1wTWvwZD5EVejHs932pyMQaalTkxL5sfnCwLUVsaUzClRfsM9R68J0IwZM5g8eTLz5s3j1ltv5bTTTiMx8fguhd93333cc889oZ+tViv5+fmHecXAIjwFXu3vt8odDcfaXD7aXD7219u7vD4cBq1MeryB7EQjt88SV0/N1Xa8Hj86fSchdNPACqF9AV9ofDcnLoe2qqAJYpKBujZRdRt+krfAoN0Q8dvyVvyKQmK6meZqOy21jhABUj1Lol1dUCczEg2JJBrE9zqUAZZmirW/gshMMFKUauZQo4M9NieyRsLn9mNrcYfsKtQK0MHWg3gDXnRydDxwwnPAwquobbLC1L60v5zN8OZNEPChjLqIxhfFxWLKjTcim/tYTeoBNHFxWGbNxDKr3X7D19SEa9s2bKtW0/yf/1D/zCuYn/o75q2/g7od8MK5cN3bkDet19tTJ8Gq/T6K0IVG4QsSCtjasDWqotyAEugggt7W2BTcthBhHzUCZKuDN28UBAbgtJ/D/PtAFiTZ+e23tLz3HnFz5xJ32mlI2giUQKMTBGfIfDj3r2KKr+QD2P4utJbB1je6J0DB4/lBhxuN1oLfF6Ct0UlBfAFrq9cO+iRYrwlQU1MTCQkJA7EvR0RWVha1tR3ts2tra0lISMBkMqHRaNBoNBGXycrK6na9BoMBQy/KvAONjinwHc2zfnP+aC6flk99m4s6q5t6m5s6q5u6Nhd1bR0ft7l8uH0BKpqdVDQ7eVyBcxL0OK0eGitsZA0RJzE1E+xA64Go98HDUe+ox6/40ck60kxpVDeKA4Ri1kAb5CQaiTuJJ8BUDE2PI8Goxerysau6jeRMQYCaax0UjE0FwghQ6/6oCglDERgRMsASM8xsq2wBTt4JsHDMLE7lUKODb0qbKcwQn1FTtT1EgLIt2Zi0Jpw+J+XW8tD0Xn8RLoJW40kCOgmv1Af9j6LA+3eIE1lyEVbjIjyHfoucmEjyNddEZX97A21KCnHz5mE57TT8ra1YP/yQyt8+yJD//hfN4luENuilRXD1f7s96XaHeKOO3CQTzUG9YUuwAqRWOg+1Hora+6hz1OENeNFIGjLNmXzVID6zAw43yEeJAFVsgNe/K4ws9fFwyZNiuiuI1g8/pPpXv0bxemn532to09NJvPRSki67FH1BQeR1yrIgn3nToPBU+O/lYmqwG8QZtOQmmahscaJP1uOsd9Fc42BSxiSaXE0DYj7ZG/T6bKOSn40bN1JSIioGY8aMYcqUgS9jzZ49myVLlnR47rPPPmP27NkA6PV6pk6dyrJly0Lj9IFAgGXLlnHHHXcM+P5FC+EVoLYDwQmPYFBofoqZRJOORJOOYUcI0nN5/dRZ3ZTUWPnBKxvZUtHKtXnZVOxsoq60LUSA0k3ppBpTaXQ1sqd5DxPTJx52vX2FekWUZclCluSQdsGuFSfvWPVHQJYlphQms2J3PRtLmxgZQQhdlFgk/oaeNhqcDaSb06OybfUqWPXqgPYR+IQ0I9vXxipAKmYOSeH1DeWsPdjE5OwEQVKr7RQGSaosyQxLGsa2hm3sa9kXFQLk8DpCsTXZlmwaysV3yBEsLvU6A+ybp0VbQ9ahXPocjT8SUoCU67+LJm7w8sQkSSLrd7/DtXUrntJSqv74EHl/exfpjevgwAp49XL4zgsdTug9wYjMOPY0iEkwtQKkZoJFswKkHuuyLdloZW2HaVedRqIwZeAqawBsfBGW/Az8HkgbAVe+CukjAFACARoee4yGx58AwDRpEp6yMnz19TQ+9RSNTz2FedYskr7zHeLPWtC9Bix/urhv2g+2eoiLfAwalhFHZYsTn0UD9WIS7MKzLuTCoRdG+133Gr1uFtfV1XH66aczffp07rzzTu68806mTZvGmWeeSX19fa/WZbPZ2LJlC1u2bAHEmPuWLVsoKxP/iPfddx/XX399aPkf/vCHHDhwgJ///Ofs2rWLxx9/nDfeeIOf/OQnoWXuuecennnmGV566SVKSkq47bbbsNvtoamw4wHhFSD1i6NOSeUn9/yLY9RpKEg1c/aYTNLjDXj8AfzBZN76snahtyRJoTbYQOqAugtwbCQ4Ah/T/4Sg+gFtKG2OmAlm0BhCmTrRdIQ+3Ai8z6yh2eFFK4twyZMdqh/Q9spW4tJF1XSghdDqdyheF0+8Pj5UIW5SRIW4VynwVZuFNgTg7Ado29WMe+8+5Lg4Ur773ajsb3+gibOQ+/e/Iel02L74guY33oNr3hBj2X4PvHE9bPlfr9Y5IiueZjmYCt/swuf1D0gqfHj7y+Py4bIH/YdkheI0C9q+6LR6Ap8bPrgTPrxL/I1GXwi3fhEiPwGXi8p77w2Rn9Rbbqbwv68yfMVycv/xDyxz54Ik4Vi7lqqf/pS9p82j5k9/xrV7T9dtmZKFkzdA+bpud0ltPbYEyy3hx7HBRq8/hR//+MfYbDZ27NhBU1MTTU1NbN++HavVyp133tmrdW3YsIHJkyczefJkQJCXyZMnc//99wMiakMlQwDFxcUsXryYzz77jIkTJ/LII4/w7LPPhkbgAa688koefvhh7r//fiZNmsSWLVtYunRpF2H0sQxVhCpywMQBrtIjvkB5Kb1vT0mSFBJtVgSD6SJFYsDAGiKqB4XO47sVsRH4LggFo5Y2k5zVlQBBuyFiNHVAkUwQVQJUHTSRG5EZ36cpxBMNeclmcpNM+AMKTVqh0WuqjpwJtrdlb1S2qU5RqgJo9SKiLhhB1OMWmKtVaEP8Hhh1AcqM79PwhDgpJl93LZpBkjl0hnHMGDJ+8QsA6h56COfuffCdF2HSdaD44b0fwtone7y+kZnxOCTwyYAC1gYXhfGFADS5mkLRFf1FxxBU8RlhkPFKA9j+aq0UlgGbXgIkOPN+uOKV0JSWt66O0utvoO3jpaDTkf2nP5Hx058iyTKSXk/CwrMpeOZphi37nLTbb0ebnU2gtZXmV17h4EUXcfCKK2l+4w38tjCSnx80GC7vvg0WcoQO6p+aaw6vWz2a6DUBWrp0KY8//jijR48OPTdmzBj+/e9/8/HHH/dqXfPnz0dRlC63F198EYAXX3yRFStWdHnN5s2bcbvd7N+/v4sLNMAdd9xBaWkpbrebdevWMXPm4Aau9RYNDlEBStGk4XaI1pdVVkg06Ugw9k1IqRKgzTYxXt9U7cDnac9tG5MSJEADKITuXAGyBj2A9qgJ1ie5CWI4JuYnIUtQ1erCZRQtQluzG4/LF1pmIDLBOleAXHZv6H9wr10cyMflHhsnx2MBM4eI79UeZ3DKp8aOEpaQPlAVoNBFRHO7C3R6vIFEUw+OD4oiKgTNhyCxAC56DNuXX+LeWYJkNpNyww1R2ddoIfnaa4g/awGK10vlPffgd7pg0b9g1u1igaW/gBX/r0fJ9COCo/AtGrFsa52DOH0cqUbRtlT///uLkAdQfF7I68ytV1PgB+BC79BX8PQ8qNwAxiS47i2Ye2/Iydm1axeHrrwK19ataBITKXjuWZIui2w+rMvJIf3HdzDs88/If+Zp4s8+G7RaXFu3UnP/79h72mlU/frXODZvRskPnlvLDlcBEu93dzAC5LiuAAUCAXS6rl8ynU5HIBCIyk6d7FArQImeoJbAqMErQV5y38XJM4eIda2pasUYp0MJKDRUtqcXqi2wfc37cPvdEdfRX4SbIHpcPtx2cWI96BTbi7XA2mExaBmdLYjG9gYbxjjxnVN1C9B+co0WAXL5XNQ6xACBWgGyBjPAzAl6ttWKq+OY/qcds4rF9+qbeiuSBG6HLxR5AO0VoDJrGd7gFXB/oH6HsixiqKN9BD7Qc/3Phudhx7sga+HyF1CMSe3Vn6uuQttHZ/+BgiRJZD/wANqcbLylZdT87v9QJAkW/glO/7VYaMWDsPQ+EeJ6GAzLiEOWoCHYdm8JC0UF4YgfDYS7QKsVoFY5yiPwigKVm+Cje+DlRWCvh8zx8P0VMKzdL6/ti+UcuuZafNXV6IuLKXrjdSw9iIaSNBri5s4l75//YPjKFWT87Gfoi4tRHA5a336H0quv4eCvX8HdqhWu3V5XxPWo73e/SxznnW3eUEtwsNFrAnTGGWdw1113UVVVFXqusrKSn/zkJ5x5ZvTcTk9mqC7QBlfwSsEk2g290f90xvCMOFIsely+AIYMMaUSboiYbckm0ZCIT/Gxrzm6viUqwk0QQ9bwRg0eCbITjcT3sbp1okJtg20sbQ5FYjTXtpePoz0Kr06AxeviSTIkAe2EKyHdxPaTPAMsElQd0OaqVuLTuuqAMs2ZxOni8Cm+qJxcu9PRWWWlZ+2v6q2CKAAs+D/Im4b9669xfbsVyWAg9aYb+72PAwFNYiK5Dz8CGg3WxYtpffttUd2Y93Mxmg3CsfiDO8Dv63Y9Rp2GwlQLzUFbEbW9qxKgaFWAQiaIcXmhSndVsIU8rL9htW218PW/4PHZ8MzpsOE5CPhg/OVw86chs0hFUWh84UUqbr8dxeHAPHsWRa/9D31hYa83qU1NJfXm7zFkyWIK//MKiRddhGQ04j5QSnNpqmilVm+J+Fp1EswrgT5eHOOPlSpQrwnQY489htVqpaioiKFDhzJ06FCKi4uxWq3861//Goh9PKkQ7gKts4uTnksvypj9qQBJksSMInGwbg22VMJ1QJIkhdpgA5EMH1ACHQ7e6oEbsyB3sQmwrgjXASWpOqCa9gNHcaI40DW5mmh2Nfd7eyH9T0JBaKxePUHoE/U02j1oZClUmYoBClPNZCYY8PoVpOCAQbgOSJKkqLbBQi7Qcdn4fQHswWpTjwiQuy2o+3HD8IWhFpJa/Um64gq06dGZJhwImKdMJv2uuwCoeeBPuPcGdVUzfwAXPwmSBra8Cm/e0G01AoQotzmsBQZho/BRIKluvzt0EZsbnxs61tUH/MgSDOntpB6AzwM7P4D/XgV/Gy3E6/UloDUK4vPd9+DSZ0AvjhOK10vN/b+j7i9/AUUh6YorKHj6aTT99OyTJAnztGnk/OX/kf2nBwBwNgf/7w4zDq9mPAbihBK6ueY4JUD5+fls2rSJxYsXc/fdd3P33XezZMkSNm3aRF5e3kDs40mFcBdoT6somVqDpdP8fo5Oqlerez3ioNnZEXogk+EbnA0hX4wMc0booODUBUfgY+2vLlAJ0I4qK3HB6kL4lZNZZw5pQaLRBuscgQHQGmyB2YLFueEZcTEBdBjEgIFogzUFT6qdJ8GiKYQOr6Lamt2ggF8Ch3SECTBFgY9+IkaWE3KFJ4ws41i/HueGjUg6Ham33Nzv/RtopN5yM5ZTTkFxuai85x4CzmBLeNLVcOUrIkl+10fw3ysEaYiAkZnxtAQrQC1hbtAQnQpQla0KBQWT1kSyITmsBaaQn2Lu3feneit8/Ev42yh447uw52Mh/s6bDhf8He7dDZc9C0NPD+l9/C0tlN36fVrefBMkicz7fknW7/8PKYJ0pT8wB61vXLUuAl7psJNg6vG9TS9+bqk9NoTQfXKdkySJs846i7POOiva+3PSI5QCb0oPmZzVByc8+lMBgnbB5ppmK8PR01xlx+f1o9V1dIQeiFF49cCdac7s4IuhjsCPiAmguyA3yURmgoFaqzs0utscYRKsyl7FgdYD/c5xC3kAJXT1AKoJJo3H9D9dMXNICh98W8U+l4uRdA1FVQlQfytA3oA3dHzItmRjC3oAWaUASEfwANr8Cmx7U1RJvvN8KF1drf4kXnYpuuNgUlaSZXL++hcOXHwx7r37qP3zn8n+4x/FL0edL5LjX7sGDq4UbsXjv9NlHeGj8LZmd5dR+P4ai4YLoKF92KNVVpjSkws9eyNse0NUs2q2tT8flwUTr4JJ10B65MgOz6FDlP/wNjyHDiGbzeQ88jDxp5/e5/dyOOiys9FmZ+OrrsbZpMNSvk4Q7Qh/O7XCXx3wMYRjpwLUIwL0z3/+s8cr7O0ofAwdoZZO083p2A6EjYlL/a8AjcpKIMGopdbpQ2vW4nP4aKywk1ksWhpqC2xP856oWvdDx9I9hI/Aq9bwsRZYZ0iSxNTCZJZsq2FfMKy3pc7Z4QA9JHEIqytXc6Cl/xUg9epX1UNAe4tgv0OdAIsRoM5QK0CbWu2MRN9lzDdaLbA6Rx0BJYBO1pFqSmVPkxCst8oKRp1MTmI3F0i1O2HJz8XjM34DBbMAcG7Zgv3rNaDVknrLwIVYRxva1FRy//pXyr53My1vvoV51iwSzz9f/HLIPJhyA6z9NxxaHZEAqaPwbknBoEhY610UZAgC1OZto9ndTIoxpc/7p2aA5cbl4nb48AajSqyy0n2VLuCHvZ8K0rN7KQSCImGNHkaeB5OvgyGng6b7U7Z93TdU3HkngdZWtDnZ5D/xBMaR0cs2iwTz5ElYq6txNJqxZDZC4z5IG95lOXUSbK/LzRDkY0YD1CMC9Pe//73Dz/X19TgcDpKSkgBoaWnBbDaTkZERI0D9RMcKkDjp1Pp9oBUVgf5AI0vMKE7h85I6/IlacPioL7OGCFB+fD7xunjavG0caDkQ3WBAW/tBAQgFoVZ6PKCPpcB3hykFggBtbmxjqizypuwtbuKSO+ZNRUNf0tkDyOfxY28VbYTNTeKkfkwQIK8LDiwHjx2yJ0LK0FC20WBgaLqFtDg9tW3ib+Vs8+K0eTDFiXp/aBKsrQy3341B07fYnZAHkCW7g5O6VVYYkhaHLHdTtfjwTvA5YeiZcMrdoafr1erPokXo83L7tE+DBcvs2aT+8Ac0PvEkNff/DtO4ce3i3sLZggCVrYn42qI0CzqtRLOskOWXaKlzkJKTTpYlixp7DWXWsn4RoA4C6GAL2aMFn3QYAfRn98Oax9p/zp4kSM+4y0LVusOh5a23qP6/34PPh3HiBPIfe+yo6LlMk6dgXfIxzrYUoEXogCIQIPX4ftDtAYy01jnx+wNoBsoQsofo0dYPHjwYuv3pT39i0qRJlJSUhIwQS0pKmDJlCn9US5Ex9BmhCpAxHXtLu8Ax1aLHEoWcLFUHVB0sAdd1EkKPSh0FRL8NFkqwtnSsALXKClkJxp75l5yECE2ClbeQkCZIT3gbLDwTrD9w+92hoFo1BkPV/+iMGiodbmQJxgyWANrnhl1L4O1b4aGh8L+r4O2b4bFp8GAePHe2sP7f9IrQTXSj/xgISJK4sPBKwVw7oDlMCJ1mSiNBn0BACXCw9WCft9O5ihqeAt9tZcHZLPKzAC56rD0Ec/sO7Cu/BFkm7Qff7/M+DSbSb78d07SpBOx2Ku+5l0BQ20iBiEaifhc4mrq8TqeRGZoeF2qDdZ4E668QOrwFpup/VM1Rt8Mee5aK+0nXwm1fww9WwoxbD0t+lEAA2+qvKL/tR1T/5rfg85Fw3nkUvvTSUROzm4Imxs4av7Bh6kYHpE6CtUkKsk4mEFBoUw0iBxG9pl+//e1v+de//sXIsNLayJEj+fvf/85vfvObqO7cyQjVAyg1kIUSUEAGez89gMKhluu32sUBurMj9OiUgdEBVdrbK0A+jx9n2PTK8Jj+p1uMzUlEr5VpdnjRJ4vKQfgkmOoGXeeow+axRVxHT1DZVomCgkVnCV39qvofOV4HEgzPiMekP4oCaJ8H9nwC7/wAHhoGr10ttBEeGyTkQe400JrAaxcH3m+eFmPQT82FP+fAk3NF0Oc3zwijNs/ACS/V71VLkMeH64AkSQpVgfa19N1iIrwCBO0XEVZJ6V7/U7lR3KcMgYSc0NMNT4rqT8L55/dpLPpYgKTVkvvww2iSknDt2EH9I4+IX1jSRP4VdFsFGpEZ3z4KH2zzqo7Q/RVCh3sAhWeAQTc6LUeTaB0BnP0AZI497Pr9VitNL73EgXPPo/yWW7AtXw5A2o9+RM4jDyMbjf3a/97AOGokkslEwOkVfkCHE0JnxoEEUrw6CTb4QuhelxSqq6vx+br6LPj9/i4p7DH0HqoLdKI3DReAWQsS5EUpPG9sTgIWvYaDLh+gpakyshA62o7QoQpQXHbowK1oJVzBE2sMkaHXykzMS2T9oeawCYp2ApSgTyDdlE69s54DrQeYkD6hT9tRc5AK4ruOwDv0EniPUvvL7xVhlzveFdM8rtb238XnwNiLYewlgvzIstBONOyFmq1Q/a241WwVr6vZKm6bXwmuQBLl+awJMPJc0V7oh9g1HOqAwSGPh2Q0XQ7uw5KGsaluU79alWqFrj1KRujC2g43Al+xQdznTQ895dq9B9vny0CSSPvhD/q8P8cCdFlZZD/4Zypu+xFNL72MeeZM4s84Q1SBGvZA6ddCHN0JI7PiWaIR56tomyGGt8AqwwTQ3XqdqSQ1ddhhKz6ukhKa//tfWj/8CMUljqGyxULixReTfM3VGIYO7dd+9wWSVotpwgQc69bhbNBjTNojCF2E9zEiM54Vu+uxG2SMCCF08cDkbvcYvSZAZ555Jj/4wQ949tlnQwnwGzdu5LbbbmPBggVHeHUMR4JaATK7EgE3Hr0EgehVgLQamalFKXy5ux70MgFPgMZKO5lFQSF0MBNsd9Nu/AE/Grn/V/yKonQ0QSwPG4GXiFWAjoAphcmsP9RMpc9HCpEzweqd9exv2d9nAhTuAaRCJUB1wavX8QMVgeH3wsEvBekp+RBcLe2/i8uEMRcL0pM/s6vWR9ZAxihxm3CFeE5RoKVUtMJUQlS9FWw14qTYsAe2vyXIwcI/R0U/NCIjniSzjlq3H9B0G4oalQpQXDaKonRsgXVLgILtrzAC1PiUyM6KX7hwUE6a0Ub86aeTcsMNNL30EtX3/Qrje++iK5wjMrG6qQANz4gLM0MMVoCiYIbY6m4N5YnlxOVQ0iCsD6yy0r3OMcJnpCLg8dD2ySc0v/pfnMHQcADD8OEkX3sNiRdeiGzpg69QFGGaMlkQoLY0kikTVaCR53ZZTh2Fr8NPAceGGWKvCdDzzz/PDTfcwLRp00KRGD6fj4ULF/Lss89GfQdPNqgVIJ3DBLhp0ygQ6J8LdGfMLE7hyz31tJll4j0B6svaQgSoML4Qk9aE0+fkkPVQ6MDdHzS7m3H5XUhIZFmy2NskdE5NsRH4HkFNht/R5mAuXUfhhyYOZV31un55Aaku0OEeQGoL7IBzgCbAytbBt/8VBm/OMK2GJR3GXARjLxUTS70l4ZIEyUXiNmZR+/NttYIMHVghBKfrnoC2arjkKdD1r20gyxLTi1LYYQ3+b3cTitofl/XwHDCXzYvPGxzl1iiRzfUCgS4nV/eBA1g/FnqT4736E46Me+/BsXEjru3bqbz3pxT+449IIAiwxw76jn+fkYcZhS9rK+vzKLyq/0kxpmDWmUM5YK2ywpQjktRpoae8VVU0v/4GLW++ib8p+N3Qakk4+yySr74a07Rp/RrVjybMkyfTCDgagtWtsrWRCVBQ/3TA7aYA+ZgYhe81AUpPT2fJkiXs3buXkhLRJhk1ahQjRoyI+s6dbHD6nLR5g5qcNvHP1BgQJKG/I/DhmBUs1x/weZiI3EEHpJE1jEoZxea6zexs3BkVAqS2v9JN6eg1+tAEmJpgHRuBPzymBIXQW6125mKircmFz+NHG9TjRCMTTG2BqQJoaK8AlXu8yHoYkxOlCpCtHj79NWx9vf05c5ogK2MvgcJTek96eoL4TIg/C4afBTmT4d0fws73wN4AV70KpqR+rX5mcQpfbhdtFXuLG4/Th94kDrHqZ1Rpq+TLii+Zmzu3VycwRVE6iKDVNrJNUshOMUU212vcJ1qBWlNIV9L41FOgKMSdcQbGUaP6/F6PNUh6Pbl/e4SDl16Gc9Mm6v/zARkJuWCtFARjyPwOy+cnm1H0Mm7EKHxrvZO8zDw0kganz0mdo45MS+99kcIF0IqihDRArd1pHQMBqBAtMCVnKvavvqL5v/8Tup5grpk2I4OkK68g6fLL0WVk9HqfBhqmSZMA8DY68blktN3ogNQKUKlHTIIdCxWgPtd+hw8fzqJFi1i0aFGM/EQJavXHpDXhahXkoNIj/CCi1QIDGJ+bhFEncyjoNTHQQmj1oBApvygzoYcJ1icx0uIMFKdZcEggGzSgtOsWoD0SY0PNBt7Z+w7+IGnuDdQKkNoGCPgDoc+pWRNgaHocZn0/pxADAdj4kpjc2vo6IMHEq4WN/727hbNt8WkDQ346Y/x34Lq3QR8PpavhhXOhtbJfq5w1JBW3DPZga6UpTAeUakqlKKEIBYXbl93OVYuv4ouyLzokxx8Oja5G3H63qKKaszqMwB+x/ZUzGTQ6PGVltH60GIC0227r47s8dqEvKCD7j38AoPHpp3Hpgu3g0q5tMFmWhCFiKBLDiU6jCx2jVlas7NM+hHsAOawefN4ACsEWWKTPqXEvuFtxNMZz4OZfUX7zLdiWLYNAAPPMmeT+4x8MW/Y56bfffkySHwBNQgKG4aLC6WjQi4BWX9dAbUtwEkx1THfZhV3EYGJwh/Bj6ABV/5NuSsemukAHT2b99QAKh14rM6UgmdrgP2JjpQ2/tz1FWdUBRUsI3d34bqsciAmge4gpBckggc8ivrLhV08T0ycyNnUsDp+D3339O65efDUbazf2eN0evyf0GaltgLYmN4GAgiKDTYqCA3RdCbx4nvCkcbVA1ni4ZZmIZBh6eIO3AcOQefC9j4XDbt1OeO4ssZ99xOjsBOINWhpU1+5OOqAXz3mRm8behElrYmfjTu5afheXf3g5nx76lIBy+BRzVQCdbkpHp9GFjg8iBb47AvSNuM8X7a+Gp58Gvx/L3LmYxo/r69s8ppFw7rlY5s4FRcHREky1L/s64rJiEiw4Ch+8oJiRJVLS/7j2j9yx7I7QhUFPES6AVi8g2qQAAakbr7MgSW3Ymy7cmy0Wkq+9liEffUjhSy+SsPDsqEdYDARMk4Ue2NmSILLmqrdGXG54Zhw+CSTLsZEJFiNAxxBUD6A0Y1qHK7yMeEPU85dmFqfSKiv4tRIBv0JjVfsItToJtqtp1xEPzD1BdyaIsRH4nkP1A6qXxOcRnqWj1+h55dxX+Om0nxKvi6ekqYQbl97IT1f+NCScPRwqbZUElABmrZlUoxjnVvU/LoMMUj/0Px4HfP57ePJUIUjVWeDsP8GtKyCvf9EdUUHWeJGgnTpctEueXygmh/oAjSwxrSiZBrUC1EkHlGpK5Z5p97D0sqXcMv4WLDoLu5t3c+/Ke7nsg8tYcmBJt9W7cAE09DAFPmwCzFtVRet77wMnZvUnHKZJYrTI1RB8omKDENp3wsiwUfiWoBD659N/zk3jbkIraVlZsZKL37uYJ7Y8gcvXM8+aEAGKz+sQgZFi0ZMaF8EAM0iAXI1iP/KffYas3/4Gw7BhPXuzxwhCfkAtwTZ5eeRgVNUR2mXqeiE3GIgRoGMIqgt0li4nZJ/eFgzQizZmDkkBCWo14oQa3gYbkjgEg8aA3WuPSjhguAmi3xfA3qpevSqxClAPoRKgAy7xt+sshNZpdNww9gY+uvQjLh9xObIk88mhT1j03iL+tflfOLzdH2jUz7hDCnzQBFH1Lxmf1wcCtPdzeHwWrP4bBHzC0v/2dTDnjsGp+HSH5EJBgvJnCs3MyxcLYXYfMHNIKo3B71R3PicpxhTumnIXn1z2CT+c+EPidfHsa9nHL1b9govfv5gP93+IL9DRaiRcAA3Q1hxOgCIIoN1toqoFkDedxmefBZ8P88yZmKdM7tN7O15gHC0q2K5DVWBMAq9DiKE7YURWPC2dUuHNOjP3TL2Htxe9zcysmXgCHh7/9nEufv9iVpYfuS0W3gILD0Ht1gG6YgM+l4y/zQ2ShPE4lZOo/1OuGhcBP90mw6s6oEa1ShqrAMWgIlQB8ourPAwyviiaIIZjUn4Seo1MOeJAG+4IrZW1jEwWRpfRaIOpJog5cTnYml2gCFt4h3SYCTBbPTy3EJ45E96+Bb54ADb/Bw59JbQagf5XpvoEvxes1aLEu28ZfPs6fP0YfPY7eO92+O9VsOL/gbMlqpsdnhFHvEFLbZCQtHRz4EgxpnD/7Pt544I3mJ41HbffzdNbn+bCdy/kw/0fRqzohUJQwwXQwRNCreJH6q0DdFsNvHkTvHqZGEdPyIUrX4Wr/wdJ+Ud+/WDAnALXvw8jzxcl/DeuFwaKvcTM4hQagyfVpqrDG70lGhK5fdLtfPKdT7hj0h0k6BM4ZD3Er1b/ikXvLeLdve/iDer01ApQVlwWQChioVsX6KrNoAQgsQCvQ6blrbeBE7/6A2AcLcTd7v0HCOTMFE9GqOp1qACFaepAWEs8c/YzPDTvITLMGVTaKrnjizv48bIfd9sWCyiBDiLocAH0sEjHuSBJdbeIiwFdfv6gj7T3FbqCAjQpKSi+AK4mvRiFj6BvUytA2wIepp1XRNG41KO9qx3Q68uwoqIivve973HjjTdSUFBw5BfE0GOoFaAkbwYewGsQ/DSaI/AqjDoNk/KTqNnTIrZd2kkInTqarQ1bKWks4dziriONvYFaAcqJy6GtOnhQCCZYd+uNseOd9jJq5Yauv9cYxJV7chEkF4v7lOB9UiHoO/3NAgHwucDrFFeEPpe4V3/2Ojs+djSBvT54a2h/HO5R0x32fAxrH4c5d8LMH4Kh/20+WZaYXJjMjhJR12+pdRx2VHdkykieO/s5lpUt4+END1Npq+RXq3/Fa7te4xczftHBLyhUAQobgVcnwFrkAEPSLD2LYQkEYOPz8PkfwN0Kkgwzb4PT7wPDcVDp05ngyldgyU9hw/Pi3loFZ97fY8PEcbmJOIwS2ESbyuv2ozMcvn0dr4/nBxN/wHVjruO1Xa/x0o6XKG8r5/6v7+eprU/xvXHfC5FUtQLUGjy5KiYNqRZ915WWB/U/edNoev45FI8H05QpmGfO6OEf4/iFNisLTVIS/pYW3JoRmPhEtF9P6ZhTmZlgwGuWwQb2ZneHyUoQDt7nFJ3Dabmn8eTWJ3llxyusqFjB11Vfc8v4W7hp3E0Yte32CfWOerwBLxpJQ6Y5kw0NQtPZqummAhQkqW53OqBgGNE1Q+t4gSRJmKZMxvb5MpxNJszp9dB0AFI7ThGrx/utPjcjzswjOdL/7lFErwnQ3XffzYsvvsgf/vAHTj/9dG6++WYuueQSDIa+BfzF0A61AmRxJeJB+HugDEwFCEQb7OX9zQA0Vtnw+wJotIJ0RWsSzOqxYvMKfVG2JZuDjWJ7VlkhPd5AkrmbL4BqYDb+cqHTaDoIzYeg+SC0lIurdNXULhLiMkHWtRManzPycn2BpBF2+5b04H1G+2OdSUw61ZfAF38UROjUe2D6zeJ3/cDUgmS+2l2PAnhcfhxWD5bE7r93kiSxoHABc/Pm8srOV3hm6zNsbdjKtUuu5YIhF3D3lLvJtGS2ewCFmSCqFYYWWWFiT/Q/Ndvho7s7Th5d8CjkTOrbmx0syBo4/2/CdXr5A6J911YNi/4FmiOLUXUambHFyTgabZgViZZaB+kFPSN/Fp2Fm8ffzNWjrubNPW/ywvYXqLRV8se17RmLOXE5eD1+vA5RuU3LMEcmwUH9j5I7jda/vSqW/cH3jxnvmIGEJEkYRo/CsWYtbnsSJhDHk0Cgg+mlJEkUZsXhqnNhRIzCp+Z2JSpqW+zioRfz53V/Zl3NOh7/9nE+2P8B9828j9PyTgPa9T9Zliy0sjakdWztzgRR1f+404B6jCMGNrl9oGGeLAiQoy2VVFpFFagTAVInwSpbnOyts4WyKQcLfSJAd999N5s2beLFF1/kxz/+MT/60Y+45ppr+N73vhdyh46h91ArQAZnHOClMdiuGAgNEAgh9L/kfbhlMPgUmqrsoYO1KoTe2bSzz6Zg0F66TzGmYNKaaGsS1aBWWQmVQ7tAUdpHV6feCEWndvy93wfWio6kqPlQ+89uK9gOE8ui0QsyojO332uNYc8ZwZQSJDXp7WQnLkh0jEmHdw+e8X3Y/g6s+LO4Cvr01/D1v+C0n8KU60Hbt4uFqYXJ+CWwaSHeJ6pAhyNAKgwaA7eMv4WLhl7EPzb9g/f3v89HBz5iWdkyvjfue6GATrUCpChKWAVIObwA2mMXLb81/wbFL8bKz/wtTL/l6IyzDwQkCeb9DBKy4YM74dv/ga0Orni5R9W8mcUpVGyyYvZraKq295gAqTDrzNww9gauHHklb+99m+e3PR+aEM2LzwtNUXpQKMyKsD+KEjq5+kzD8Dc3g0aDedasXu3H8Qzj6DE41qzFVW0TPkjOZmjYDRmjOyw3IjuB5h1Osv3dEyAValvsk0Of8ND6h6iwVXD7stuZnz+fX0z/RYf2VyCghE27djPsESSp7iZxbDUcp/ofFaFJsBoFRQGpbC1MuqbLciMy46hscbKntu34I0AqpkyZwpQpU3jkkUd4/PHH+cUvfsETTzzB+PHjufPOO7nppptOiquNaEKtAEl2PeCl2usF3cBVgKYUJqHVSNTIfgoDGupKraGD9fCk4WhlLW2eNiptleTF5/VpG+ERGNBxemV0dwLo5kMitkDWQW6ESSGNtt3ptzMURRzsWkrF43CSozOJ20CfmGUNTLhcmPp9+z9Y+RdoLRctla/+CfN+LvxveikEnpifiCxBPX7i0dBS6yB3RHKPX59uTueBUx/g6lFX8/+++X9sqd/Cv7f8O/R7tQLksHrweQIEUGiVle5H4L0ueHZBu9h2zEVwzv/rELp5XGPydaKS+Mb1sH8ZvHg+XPumIMKHwcwhqXyrOUi+v2Moam9h1Bq5dvS1fGfEd/ho/0e4/C6GJA6hvFI4A7fKCkMjXUQ0HwRHA2j0uJvF/7q+uAj5JKrSqzog1+69cPY0OLRK6IA6EaCRmfF8I1eT7ZdpqTuyIFeSJM4pPoe5eXN56tuneGXnK6woX8GaqjUMSRTBxHlxedhb3AT8Cn4UMGrISujkNK4oUP4NSgDc1S0AGEYe3wTIOHYMkk6H3+bGa9OgV9uwnTA8M57lu+vZW9sW8fdHE30WQXu9Xt544w0WLVrEvffey7Rp03j22We57LLL+NWvfsW1114bzf084RHuAu0L5j82KX5kCbITB4YAmfVaxucltk+ClbePwus0OoYniZ50f4TQIQLUyQSx26siaJ8gyJnc+7aRJAlBa85kyJ0iMqKSCyEuXVy9H82qhEYLU74LP94I5z0s/GZay0Ri+b+nw9Y3RJhnDxFv1DEyKyFkJNZ5EqynGJs2lpfPfZm/nvZXMs3C7TZOF0e6KR1o1/9YZeEDNLY7AnTwS0F+jElwzRuiQnKikB8Vw8+CGz8STtXVW4RXUOPhA00n5CXSGuyWVZRa+70LBo2By0ZcxrWjxTFVtcjoNgRVHX/Pnohrr3AHN448cVyfewLjaEF03Lt2oeTPEU9GyAUbkRlPSygVvudtcovOwj3T7uGtRW8xI2sGbr87dJwUAmixrjZZYUiGpWsxoPkQOBrwOIwobg+S0Yj+ONfUygYDxnHCX8rRoBcyAGdzl+XUSbA9tbYuvzva6DUB2rRpEz/+8Y/Jzs7mjjvuYOzYsWzfvp3Vq1dz00038dvf/pbPP/+cd999dyD294RFuAu0o0X0962yQlaCEb124Ib1ZhanUhM8odZ3Olirhoj90QFV2SMToMOOwKvGZQUnSMlea4AZt8JdW4QHjjlVtMbeuRWemAM73+/xVNu0wmSaZNULqO8jpJIkcW7xuXx4yYf8csYv+ctpfwkdpFUPoFZZoTjNQlx3Aug9IlOKcZfCiIV93pdjHrlTxZh8cpE4cb1wnmj9dQODVkNKtmhbN1RG/yCv+st0G7AZlv/l3r1b7NOo41tf0lvoi4uRjEYCDgdefdBTJ4Ij9IjMuJAbdFNt76t1Q5OG8uzZz/LQaQ+RYRKVwTGpYzrpfyIc59T2lySqRoZhw5A0x2nLOAwhP6C2NPFE+fouy6jSh711xyEBmj59Onv37uWJJ56gsrKShx9+mFGdMmWKi4u56qqroraTJwNCLtCGjA4+OXkDpP9RMbM4JVQBaqy04/e3n4hVIXRJY98rQOEeQAF/AFuLSoACoSuBLlArQAWz+7zdYxI6k/DAuWsrnPFbMCZC/S7RYnl6Huz5JOLoaDimFibTpI7uRsFDw6Q1ce3oa0NCTqCj/ienm+qPosDeT8Xj4Scw+VGROhRu/gwS8kR7Vn3v3WDkCDHe67d6O7isRwN1QeJr1yrkR2qPhxEgV5AAGUeeXARI0mhCmhpXix5krdANtnT0NUuNM6AEXYmb+vh9UttiH17yIe9d9B5zcuaEPIBajkBS3W5RgT3e9T8qVD8gZ2Ow5RfBEFH9ezTY3DTbj7MojAMHDrB06VIuv/zyUBp8Z1gsFl544YV+79zJBFUAnUMhKIAsfHIGSv+jYlpRMlaNggsFvy/QwbskPBKjp5lFnRHuAm1v9aAEwI+CMd4QeQTS3tA+2XWiVIA6wxAnBNF3bYV5vwB9nEgp/+8V8MGPD/vSqYXJNAUJq7XBGfWTK7QToObD6X/qdgpdk9Yo8rtOBsRliAwxOKJR4owxabhRkIDmPlQWDoemoFbFkKBHq+l0CPc6oWYbAIG0CXgOCoG74SRrgUF7G8y19yBkC3foSFWgjBzhveNp8+L19D5HT4VZZw6F3raF+TRFvNBTCVCL+PyO5xH4cKjBqO5aO36PBGVdg1HVSTCAPYOsA+o1ASosLByI/TjpoQqgMxURF+E1igiCgfAACke8UcfY3ERqtV0doYcnD0cjaWhyNVHrOMxU1WHQIcE6rHQ/ItL0CrRXf9JHCS3PiQxTEpz+K0GE5gQ9Sjb/RxgtdoO8ZBPmBANuxKSFSlaiiXAPoG4nwPZ8Iu6L53X1XDqRMWaRuN/7qSAb3WBqYQpNWnHRsHdfVx1Ef+BoERXi5PQIf/eqLcJ1Oy4Ld70TAgE0ycloM9Kjug/HA0JC6JKS9mpyhFywIbnxuCTxWVmj9H1qPdwIvNcpLngAV5UQfJ4oFTptWhq6QqFlcjbooXJjxBgS1QB3sNtgPSJAycnJpKSk9OgWQ9+gVoCSvaKP7AjKLga6AgQd22DhhohGrZEhSaJH3RcdkMProMXdAogpsI76n+4IUPAK7URrfx0OllQ4+48iigEFdr7X7aKSJAWrQME22ABk6ajTMC0ahbG53ThAqwRoxNlR3/4xjZwpkJgPHhvs/6LbxYw6DUqCqJDv2t0Utc0HAgoBu9AI5uREcA0Otb+m4d4jKqmGUSNPyoncUAWopAQKg0LoSDqgrISQI3RvhNCHg5otZtdFsDGp3goBHwF9Bt5KcbFzorTAAMyTRBvM0ZIg/NciBKOGdECDXAHq0Rzuo48+OsC7EYNaAYr3JOMFmgbYAygcM4pTWK0RvfHwSAwQOqC9zXv56MBH2L12dBodOlmHXtaj14ibTg4+F/xZL4vn1PZXgj6BOH0c1kbxHltlhQndeQCdqPqfnmDspcI8bPs7MKv7yIKphcls+aaRbL8cbK9E7+re7fDiCZrsJaabSDBGaHM7mtqTxk8G/U84JAlGXygMLnd+AKPO73bRtBwLNFmpi6IQ2mn1ICkQQKE4P0J1LpQAPwPXiqD+5zg32OsrDCNGgCzjb2jAZx4hTnYNu8HeKC46ghiRGc9qOdDjUfgjwe8P4GwV2pbkdBMauRP5VNtf+jGg7EGTloY2dXAjIaIJ05QptL7/Pk5rElAvjmmdgo8vmpTL5IIkxnanMTxK6BEBuuGGGwZ6P056qARI74jDC9T4vKA5OhWgGcUpoRZYQ0Ubfn8ATVBbMDZ1LB/s/4DPSj/js9LP+rR+NQU+POE+YgXIYxejxnDi6n8OhzEXwdJfipNYS3m3uVlTCpP5QqOAN/oVILX9ZZcURud34zG073ORM5Ux9tjN9hpIjF4kCNDuj8HnAW1kN/Phw5Ip3W7F1xw9oWdoBF5SGNb5IkJR2qdu8qbj3vUkAIZRJ5/+B0A2mdAXF+PZvx/XoWri0keJoYOyNTD6gtByIzLjQqPw9UfIb+sJbE1uUMCLQn72YRygPZnAHowniP5HhWnyJACcVW6UAEjla2H2jzosMyYngTE5vcgXHCD0iABZrVYSEhJCjw8HdbkYege1BaaxGwE/LShoZamrgdYAIMmsJzPbgtvqw+CD5mo7aXni4Hrh0AvZ27KXBkcDnoAHb8CLx+/B4xePw3/2BDx4/eI5v9IuJlQnjFrC/GUiukBXbhT6hYRcSDq+PTH6hIRsKDwFSlfDjne7ZBepGJebgDX4za2piG4PvYP+p7sDlDr+fpRG3wMOB97aWny1dfjqasXjunokWUZXWIC+oBB9YQG67Gwk7VFImc+fKQwSbbXCC2n4goiLTZuQSel7pcR7Fcoa7BSk9T/osjaYMN8mKwzpnAJvrQwaiGpRsia2T4CdZCPw4TCOGiUIUMku4gpmRyRA8UYdxOvADbXV/f8+dbApiHScU0fgg2ZRhuEnTvsLxEi/HB9PoK0NV4sOU1kwGPUYbMP26GiRnJxMdXU1GRkZJCUlRewnq3EJfn/fVfQnM9QKkL9NBvxYZYXsJGPXKY8BwswhqdTur6bAp6GutC1EgOL18fxu9u96vT5/wI8n4CGgBLDoxIG6WRUYWjSRJ8BC7a9Zx+SX5ahg3CVBAvROtwTIoNWI9soeTyi1PVoIzwC7IJIA2u8TFSCAEef0a1tKIIC/sRFvkNj4amvbiU5tLd468TjQ1kOdgFaLPje3nRQVFAhiVFCAPjcXSR+l4EVZhlEXwIbnoOT9bglQZpYFvwQaRWLt1loKzhjS702Xl4sLUI9BFifucKj6n8xx+JqsBKxW8TcZOpSTFcYxo7EuXix0QLPnwMYXIibDJ2eYoMGJraH/GqC2hnYB9NTOHkDWKjGOL8m4q8RnaThBBNAqJFnGNGkS9lWrcDYaMaXUCGf+SM79g4weEaAvvvgiJHBevnz5gO7QyYiQC7QC7hZBIK2ywpgBngALx8whqbyhqaTAp6GhrA1O6d/6NLIGk9zevlMCCq5gXzw9o5srYfXAdDLqf1SMvgiW/EwkRTcdgJTIJ82Rw1JgTw2KO4DT5sEUF52Te12wBdCsUSI7QJevA1eryErLm9bn7Tg2bKDqV7/GW1Z25IUByWxGl5GBNjMTbWYGusxMFK8PT1kZnrJSvGXlKB4PntJSPKWl2FnVcQWyjC4nB31BAbrCAuLmziX+jDP6vP+MuUgQoF2L4fy/R4w1kWQJErTQ6mPnrkaIAgGqC47U6xIiaLPC2l+uXbsAMBQXI0eL+B2HCDlCl5RAwT3iyepvwW3rkOuWV5gAO50oDj9ejx+dvu+mhOpFRMQJMDWkNn0M7o/3ASeWAFqFecpkQYBs6UCbGIc/XgnQvHnzIj6OITpQXaATlRR8HqHFaZOVo6L/UTGjOIV/BSeLqg71376/MxxWDwQUAigU5EUoC/t97VewBbPxt7RQ+dOfEbDZ0CQliVtiIprksMdJHR9LJtPxP+0Sly58dQ6sEG2wufdGXGzKkBQ2SVUkKjItNQ5Mw6JzkqutEi0AbYKORFOEk6za/hp+Vp9iRRSPh/rH/k3jM8+Isrgso01NDRKbTHSZGWgzwh4Hn5ctEeIEwtcbCOCrrcVTqhKisuBjcVOcTrwVFXgrKuDrr2n532tk/uY3pFzXx8iewlMECXQ0itHqbryQkrMtWFtbqamIzrRLW6MLLZCQcngDRPca1QH65NT/qDAECZCntBS/NgVNYr7wr6pYD0NPDy03Ij+RA1INJkXCeoRQ1COhTm1TahSK0jpdxKohtYkT8LesAFnGMOzEq9CpjtCO2uB3tnwtTLxyEPcoMvrcMHc4HJSVleHxdBT4TZgwod87dbJB1f/kScUA+PUS/qPgARSOtDgDhgwjHArQVGkj4A8gR7H9plrDt0kKw7MiEKDa7WK02JAIGaOxvvEW9tWre7UNSa/vQIy06WloM7PQZWWGnWAz0aanI3Vj4tkTKB4P3ro6vFVV+Kqr8VZX462uwVtdha++AcuMGaT/5G5kYx/1W2MvPSIBmlqYzDKNQqIPqirayB6W1Of3Ew578HPKzOnmBKA6IPdB/+M+cJCqn/0M144dACReeimZv/oVmrj+a2MkWUaXnY0uOxvLrJkdfqcoCr76+hApcnzzDa3vv0/tAw8gx1lIuvji3m9QoxUTYJtfEVEm3RCgIUOT2bKrFU2bj1qri8x+avq8bV60QEZWp2ODzy0qGwB503Dtegw4ufU/ANrkZLRZWfhqanDv2Y25YDZsKxc6oHAClBnPJlnB5JdornX0iwA1BgcT9Il6DNpOFwmq/sebLZYpLOz7ceIYhmn8eNBo8LU48dpldBEMEY8F9JoA1dfXc9NNN/Hxxx9H/H1MA9R7qDEYmQGRuO7QCtZ8NEbgwzFmRAruQ/VCCF3Tv4NAZ7Q1tZeFz4gkDFT9f/JngKzBuV242cafcw6WU+bgb2kRt9bW0ONAayu+lhb8La3g9aJ4PPjq6/HV1x9+ZyQJTVoquswstFmZ6DIy0WZlBSsOWWgzMoTwtlolODVBklOFr6oaX0PDYSMr3CUl2NetI/fvf8NQXNz7P9boC2HxPcLRt2EfpA3rskh6vAGvWQNW2Levmanz+z+N5fP4wSm+v0OLk7ou0HRQiEglDQw9s8frVRSFltdfp/b//QXF5UKTmEjWH/5AwsKj4yEkSRK6jAx0GRmYp00j8dJLkBMTaH75Fap/9Wtks5mEs/uwL2MuEgSo5CM49yGhDeqE7Px4tgApAZl1B5tYNLF/YbEal6gQFxZ0EqjXbAO/W+TMpQxpzwA7SUfgw2EcNQpbTQ2unSWYR82GbW900QENy4ijRSNG4asqrAybktHn7TlVo8qMTsdvv1e0tgG31QCcePofFbLFgnHkSFw7d+Js1KOz7BStc+Pgjr13Rq8J0N13301LSwvr1q1j/vz5vPvuu9TW1vLAAw/wyCOP9Gkn/v3vf/PQQw9RU1PDxIkT+de//sWMGTMiLjt//nxWrlzZ5fnzzjuPxYsXA3DjjTfy0ksvdfj9woULWbp0aZ/2b6ChCqBTfSIXphlxkDuaLTAQOqA1mlry/UIIHU0C1BS8Kup2BF4lQIVC/+Path2AhPPPI+Gssw67bkVRUBwO/C0t+FRi1NwsyFBtHb7aGrw1QZFtXR14vfjrG/DXN8D27X16P5Jejy47G21ONrrsHHRZWehyskHWUPfww7h37eLQZd8h+4E/knDeeb1buTkFhpwO+z4TYuh5P4+4WFKmGawOaqPkM6PmF7lRmF4cYQRerf4UzBYu1j2Ar7GR6t/8FltQO2iZM5vsBx9El5kZjV3uEyRJIvOXvyRgs9P6zjtU3vtT5CeeIO7UXgrfiueJiqWtRlgXRLBuSMkW1a1Uv8S6/Q39IkB2mwd9MPlk1JBOn09Y+yvgcuEpLQViFSAQQmjbihW4SnbC2d8VT1Zs6GBhYNRpRCaYByrL+/598nn8KMGLiLzOrf7a7cIY0JiIu1KYY54oERiRYJoyBdfOnTja0kigQvyPDos8MDBY6DUB+uKLL3j//feZNm0asixTWFjIWWedRUJCAg8++CDnn9+9MVgkvP7669xzzz08+eSTzJw5k0cffZSFCxeye/duMjK6svB33nmnQ9utsbGRiRMncvnll3dY7pxzzumQR2YwGHr5To8e1BZYvEeYYdUFhBHd0a4AzSxO5T2tQr4fKg+2MHpOdtTWXRXUlniNMqlxnT4LRelggBhwOnHvEwJB0/jxR1y3JElIFguyxYIuN/ewyyqBAP7mZrw1Nfhqg6RIJUe1NaEJJNls7kJutNnBn3Oy0SQnd6tJsZxyClX33otjwwYq77kX+/r1ZP7yl8i9+R8cd6kgQNu7J0BFxYnY9zpwN7t7vt7DoKpSaL9aNArj8yJcqfXS/dm2ciVVv/o1/sZGJJ2O9HvvIeX665EiVEqONiRZJvuPfyBgt9P2ySdU3HEHBc8/h3nKlJ6vRKuHkefA1teFKWIEApSQZgRZQheAbXv75wi991ALAC5JIa/zCHy4A/TevSICIzUVbfrJF4HRGaoOyl2yC9JHCu2Ws0m0DPOnh5aLSzNBs5PmfnhrqT5NHhSG5nYiQMH2F7nTcK3bC4DxBBRAqzBNnkTzf/6Dsyl4Hitbd/wTILvdHiImycnJ1NfXM2LECMaPH8+mTZt6vQN/+9vfuPXWW7npppsAePLJJ1m8eDHPP/88v/zlL7ss3zlu47XXXsNsNnchQAaDgaysrF7vz2BArQCZXPH4gVYpgF4rkx5OFPxeoQkxxEPGGOGTE2XBb1aiEV+iFuqgbF9LVNfdFLSGNydHIAFNB4SnikYPOVNwbdsJfj+a9DS0Ua4USKroNjUVxo6N6rpV6DIzKHjxBer/9RiNTz1Fy/9ew/XtVnIf/Tv6gh76G408T/w96kugrgQyRndZZOKYdL7+tBqDK4DH60ev6/vkCsDeAy0AuI0ySeZOomq3DQ4FJ6uOMP4ecDqpe+ghmv/7PwAMw4eT8/BDx1zekaTRkPvQXyl3OLCvWkX5939A4csvYRwzpucrGb1IEKCSD2Hhn7p8J2WNTFKmiZZqB7Y6Jw02N2mdLwB6iIOHRG6UxyB3Jd/hCfAbxATYsfb3Hiyon6d7714Unw+pYDbsXizE62EEKCs3DvY68bb23bjSGpYBNiKzU5sy+BkpOdPw7HsNOHFbYEDoYsJVbSfglZAjJMMPNnp9KTZy5Eh2B/vLEydO5KmnnqKyspInn3yS7OzeVQw8Hg8bN25kwYJ2VijLMgsWLGDNmq6ZLZHw3HPPcdVVV2GxdLwiWrFiBRkZGYwcOZLbbruNxsbGbtfhdruxWq0dbkcTHU0QwSop5CWZkMMt1Df/B965Ff53FfxjAjyYD8+dDR/eDd88I3razpZ+70v+0CQAHHVOAv7oJY07gm646ZkRBK9q9SdnCuiMuIJtKdO48cftVJek1ZLxk7vJf+ZpNElJuHbu5OCll2H99NOercCU1K6z2f5OxEXGD0vBi4IGia27uv//7imqg6aKpkgk9eBK8HvEKGta91etzh07OHjZd0LkJ+WG6yl6681j9mQs6fXk/fMfmKZNJWCzUXbzLbgPHOj5CoadCToLtJaF9B2dkRYUlKf6ZdYf7HsVqCpo0idbOl23ttVCSxkgQe5U3LuDGWDH6N/8aEOXm4scH4/i9YrPNthm75wLNmRIEgBadwCvu29aVnWKskVWGNrZ7iNIgDxSPorXK6wdjlCxPp6hy85Gm5UFAQVnkw4qNopp32MIvSZAd911F9XVIsDtd7/7HR9//DEFBQX885//5M9//nOv1tXQ0IDf7yez01V+ZmYmNTU1R3z9N998w/bt27nllls6PH/OOefw8ssvs2zZMv7yl7+wcuVKzj333G4F2g8++CCJiYmhW37+0bX3VytAgTZxBW+VFXI763+qgtU1UwrIOvC0CU+WjS/Akp/CC+fCXwrhb2Ph1cvhs9/B1jehdofodfcQU8am40FB8gshdDSgKApSMF8qP9IIfCgAVbQQnEH9j3H8uKhsfzARN3cuxe++g2nyZAI2G5V33kXNn/+M4unBZzLuUnG/452IomutVsZjFv8zW3c19Htf24L+JenZEUhqaPx9YcTKo+L30/DMMxy66mo8Bw6gTU8n/7lnybzvvt61/gYBsslE/hNPYBwzBn9zM2U3fQ9PRWXPXqwztbcEd74fcZFkVQcUkFjXDwKkOqlbOhNUtfqTMQYM8bh2BytAMf0PIFrkxmAbzLWzBAqCwahlayDQfpE3pjAJZzAVvq+ZYOVBu4OASYNZH0ZU7Y2i0g24reJC1zh8+DHRDh5ImKeIcXhncwJ47VC7bZD3qCN6/de/7rrruPHGGwGYOnUqpaWlrF+/nvLycq688ujO+T/33HOMHz++i2D6qquuYtGiRYwfP56LL76Yjz76iPXr17NixYqI67nvvvtobW0N3crLy4/C3rej3lmPHNDgC2rv2mSlq/6nXlTdOO8h+HU1/GgtXPYcnHqPOCklBkmbtUKIVb96FN65BZ6YA3/OgSdOgb1HzvKaNTQ1lAxfEWyJ9BfONi9yABQUhkeaLgoJoMWBybVNfEl6ov85HqDLzqbw5ZdIufl7ADS//AqHrvvukU+yI88FrREa94kpnwgwpoiTYXmwPdIfKDZBUocUddL/KArs6X783VtVRdmNN1H/yN/A6yX+rLMo/uB94k7pp5vmUYQmPp78555FP2wovtpayr73PSGY7wlGLxL3JR9EJKrtQmiZtQf6XqlTAzZTO08Xhel/FEVprwCd5B5A4TCOCRoi7iqB7AmgM4OrRUw1BlGUZqEl6IX28bPbObS1AeUw056RoI7AG5M7tZArg/qftBG4Dorv/YlogNgZJjUZvi0oXTnGxuH7TT/NZjNTpkwhLS2t169NS0tDo9FQW1vb4fna2toj6nfsdjuvvfYaN9988xG3M2TIENLS0tgXFNZ2hsFgICEhocPtaMHpc9LmaSPOkwRAQAan1GkCTFGgLvhFTR8FGp3QhIz/Diz4HVz7BvxkO/yiFG5aCuc/AtO+B/mzwJAAAa+YQPjkV4cd3wbISzZjt4iqQsnO/rdVABqC7rU2CUZ2zpey1YsTPED+DPxWK55DhwAwjjv+K0AqJJ2OzJ/9jLzHH0dOTMS1dSsHL72Uti++6P5FhnhhOAiiChQB2cGKWmtd/yz8m20ezF7xvzF+ZKdk6upvxaSTzgJFp3b4lXXpJxy46GIc69cjmc1k/+kBcv/5D7TJ3QSpHsPQJidT8Nzz6PLy8JaVUX7zzfiam4/8wuFnC6LadEBUXDshOejZk+qX2FXdxo//t5mv9/fu5KooCgSrqLldxLVBApQ/A19VlYgO0en6ZsFwgsIwShAg184ScfzMC2p/ytrH4XUamYpMLU5Joa3WyeLHt/LuI5uoOdDziwtns9AApXRLUqfj3nPytChNQR2Qs9ojTj3lxxYB6pEI+p577unxCv/2t7/1eFm9Xs/UqVNZtmwZFwfNyAKBAMuWLeOOO+447GvffPNN3G4311133RG3U1FRQWNjY681SkcDqgt0ilcQPqdOgs4miG014G4FSYbUrp4wIZiSRH9b7XGDIDxNB0QFqGGP+CLmR7YYUJGcawGrnbrS6LjX7g9WJxw6ugpA1epPxhgwJePaIn7W5eYelyfRIyH+jNMZ8s7bVPzkHlxbt1Lxo9tJuekmMu75SWRzxrGXCoHt9nfgzN91aT+NGJ5M3do69A4/dVYXGX002tuyqwENEn4gJ6fTCVad/hp6OmjbPz9vbR2VP/sZeL0YJ04g969/RV9Y2KftHyvQZWZQ8MLzlF57He69+yj//g8oeOGFw5s1GuKEXmv3YlEFyupI3JMyzEiyhCEAcQp8+G0VH35bRXGahaum53PZ1LwjCqPrbW4sQflEUbgHkN8HlcH2eN50XNuC/j9DhkQv++wEgFoBcu3aJVryhXOErq10DUxvl1DMO6OQhz4sYZpTwxS3lup9rbz9140UTUxj9sVDQ9W87qDYj0BS86bh3vNf4MQegVdhHDkCyWQi4HDisWoxHGMEqEcVoM2bN3e4Pffcczz11FOsWLGCFStW8PTTT/Pcc8+xZcuWXu/APffcwzPPPMNLL71ESUkJt912G3a7PTQVdv3113Pfffd1ed1zzz3HxRdfTGpqx6tVm83Gz372M9auXcuhQ4dYtmwZF110EcOGDWPhwqOTXt0bqALobEW0sFqlCB5A9SXiPmUI6Hp5gpMkSB0KYy8WP296+YgvGTk6+Ddt8RAI9K4EHAkVwfFqOos3ocP4O4Trf06M9lck6HJzKfrPK6TccD0ATS+8QOl3r8cb1NZ1wIiFolzfUtquAwuDWgFKCchsKutBtaIb7D4gtClek9xRfA+wVx1/7/j9caxdA14vhlGjKPrPf4578qNCn59PwfPPCfH6tm1U3HYbAZfr8C8aE2yD7fygy680OpnEdPF9fvzCCVw7s4A4g5aDDXYe/HgXsx9cxu2vbmLV3vpuv2/7qm3EKeJz6dACq9shvGUMiZA6PJQBFtP/dIRhyBAknY5AWxveysr2vMGyNR2q4jedUswnP5/P0AV5vJrqZaveRwCFQ9828L8/rOOTF3di68Z2wuPyoQuS1OFBQTUAAb8QAAP+5LFi+5zYI/AqJJ0OUzAdwtFgBGsltBxdicnh0CMCtHz58tDtwgsvZN68eVRUVLBp0yY2bdpEeXk5p59+eq89gACuvPJKHn74Ye6//34mTZrEli1bWLp0aUgYXVZWFhJdq9i9ezerV6+O2P7SaDRs3bqVRYsWMWLECG6++WamTp3KqlWrjkkvINUFOtUvKkANASHU7qABUvU/6f3o6U8OGoDteFeMNB8GcyZl4UFBE4DaKGQYqX1xS+e+OIQJoFUDRFX/c+K0vyJB0uvJvO8+cv/1T+T4eJxbtnDw4kuwr+00Kqq3tI+dR5gGS8oU/ycWRWLTvr63LFXzN2NSp8/IVgeV4uDN8I7+P/a14moubu6p/YoWORZhGDaM/GefRbZYcKxfT+Vddx9euD7iHDGcUF8C9Xu6/FqtHCR64E+XjGfdr87kr5dNYFJ+El6/wuJt1Xz3uW+Y9/By/r18H3VtHQnXvtIWZCQCEpjjwz6jUGVhKsgy7l3BCtDImP4nHJJOh2G4qLi4SkpEC0zWBk/IHQN581PM/O7CsXz26zOYcMkQ3stU2KvzgwL71tbw4q+/4vPXduOyezu8rrZatPqdksKo/DAdXcMeMbSis+BuFvICbWYmmqSkgXvDxxBMkycB4LQHff2OoSpQrzVAjzzyCA8++CDJYe2J5OTkfjlB33HHHZSWluJ2u1m3bh0zZ7Zn+axYsYIXX3yxw/IjR45EURTOiuAQbDKZ+OSTT6irq8Pj8XDo0CGefvrpLpNmxwpCQageoaFqlRVMOg2plrCDXH2Y/qevKJwjKkgeG+x877CLFqVbaA5ufuOW2sMu2xM4WlTxZqfysdvWnl+kToAFR+CN407cClA4Es46i+J33sY4diz+1lYqf/YzlLDJFCBsGuy9LhouvVGLHJwE27CtjnUHGnst3ARoDfo0pWZ1+oxU9+fsSRDfrstTFAX7OkHWzDO7GgCeCDCNG0v+k08gGQzYVq6k8he/QOku6seUBEOCQdElXafBVB1QczAo02LQcsX0fN67/RQ+vmsuN8wuJN6opbzJyUOf7GbOg1/wg1c2sGJ3Hf6AQnmluBBRTBqRMq8ilAAv2tqhCIyRJ351obcwjFYNEUtAb4YcIdANXYR1QpJZz+2nD2Pxb05n1ndHsjxPokLjRwrA7hWVPP2L1Xz6zh58XvE/sfeAqMDatXT00VJJau6UkMHrySCAVqH6ATnqgj5lZWsPs/TRRa8JkNVqpT5C1lJ9fT1tbdHRjJxMUCtAJpfo66sp8B38b+qiQIAkCSYH9VKb/3OERSV06aLVdmBP39sqIQT74nmd++KVG0Dxiwm2pHx8DQ34qqtBkjAOkEnhsQh9fj6Fr/4H2WzGX98grlDDMews0MeLCT/1YBqG1GB1wd3s4cqn13LmIyt5YsX+LlWE7mB1eZHs4iBeVNhpAizk/tzR/NBbUYGvqhp0utCo64kI8/Tp5D32L9DpaPt4KTX/93/dE8zR3bfB1FH4pmCVoMPLshP4/UXj+OZXC3j48olMLUzGF1D4ZEctN76wntP+upyd+8T30JDYqUIXHoHhcOApE9UMY2wCrAuMo4Uhomtn8PultsE65YJ1eZ1Ow1UzCnjjV/M540fj2V6sp0EOoPEp7P20gn/e+yUfvb+XivLg+c/SOQA1XP8jqoPGk4igmiZOBMDbYMfnkkUy/DGCXhOgSy65hJtuuol33nmHiooKKioqePvtt7n55pu59NJLB2IfT2ioFSCdQ1whWiWl6wRYqALUz77+xKuFkLpsjQjZPAxyi8WJ0NZPLyCnx4fRI04YHfriEKb/6Vj90Q8ZEpWE8OMJstGIebY4INtXre74S50RRgXzxCK0wTKDxPLUzETMeg0HGuz8ZekuZj/4Bbe+vIFlJbX4DmNquaPSSpJfEO6s8Pw3nwf2B6fUOsVfONaJMrZpwgRk89GNbDnaiJs7l9yHRNhpy5tvUffQw5EXHHWBCIqt2SqCY8OQEkaAlG50Pia9hu9MzePt2+bwyd2nceOcIhKMWipbnPhtot2SlBZ2bHA0QdN+8Thvqji5KopwUO+kjYwBjMEKkKqTUm03uqsAdYYsS5w1NosnfnEqF/58ChXDTVilAAaPQunH5bSsFRezxqTOPk3BEfi86bjUCbCTqAKkSUzEMFwM7zgb9GJS0n1sFEt6TYCefPJJzj33XK655hoKCwspLCzkmmuu4ZxzzuHxxx8fiH08oVHnrAMFsAmBsLWzB5CtTvhVSDKk9XNqICFHVBNApFgfBpMnin6tyebH5embKyrA7vJWDIiTa0FnE0T1yitIgNQAVNMJNP7eG8TNFSPmttWruv5y7CXifud7HczboF0HNDkpjvW/XsBfL5vAlIIk/AGFz3bWcvNLGzjlL1/w0Ce7KG3sWoHYVtFCUkB8RqpYF4DSr0TL1JIB2R2rPKr+xzLz8BOFJwoSzllI9h//CEDT88+HwkY7wJIKRUHvo5IPO/wqKcsMErjtPp6+ayWv/+kbPnt+Bxs+PsSBLfW01Do6OK+PzIrn/xaN5ZtfL+DvV05kuEVUZDt8h9TKQtoIMUEZ1P8YY/qfiFB1Ub6aGmFvkB+UWjTsAXvvjESnFKXw4L2zueL+mVhHWnBJwpEdOo3Au1pFlA2g5IS5dJ9EBAjC/YBSQQm0k8JBRq8JkNls5vHHH6exsTE0FdbU1MTjjz/eJY4ihiOj3lGP0ReH4pNQaG+BtS8QLNcmFwnX2f5CbYN9+7/D2pJPGJOGV1LQI7F+a991QHuCZooeLegMYVNgfm/7lyDozOrcLgTQJ/IE2OFgOVUQIOfmLfg7t5OHniEmfdqqu1yxJgX1JTUHWmmrtHPF9Hze+dEpfPaT07jl1GJSLHpqrW7+vXw/8x5awdVPr+W9zZW4gtqFkkMt6BH/fwmpYf9jqv5nxNkQ5lirKEqoAnSi6n8iIemySzHPFu/X1o2pagdTxDDo9BrGnZaLRivj8wZoKLex55ta1r1/gI+f3Marv1vLU3et5LU/ruOTZ7ezfvFB9m2sw1HvYtH4HGZkiopsYngFKKz9BeDeE9P/HA6aOAu6QpHF5y4pAXOKsN+AHleBOmNodjz3/WQm1/x+Fv6R8TiStFxw3tD2BSo3AQokFeCzK8KjSatFP2RIP9/N8YWQH1BzsMJ8jAihex2GqsJisTAhON4WQ99R76wn3i0E5W4tBDp7AIUmwLqGYfYJI84Bc5oIH933mXAbjgCNRsYTp0XX5mfrtnrmTsvp0+YqKqwYoOsIfM02YY1uTIT0USiKElYBOnn0P+HQ5+WhLy7Gc/Ag9jVrSDg7rO2kNcDoC2DLq8IUUa00AOn58Wh0MvZWD2//dSPZwxKZsrCQYeNS+c0FY/j5OaP4vKSW19eX8+XeetYcaGTNgUYS3tdy8eRcDhxsZRigT9Ch0YVdE4XHX4TBc/Agvvp6JL0e06SJA/gXOfYQP38+jjVraVuxgpQbbui6wOgLYcnPBDlprYTE9qyneVePZO6VI7A2OGmuttNUbae52iHua+z4PAEaK+00Vnas0oXbEsSnhtlghGlLgPYKUEz/0y2Mo8fgLS3DVVKCZc4coQOq2yn8gEZf2Of1ZmdYuPMn07v+Irz9pQrUi4uQTzKPJnNwEsxV5SDgB/kYEUKf2EEkxzhcPpdwgQ4SIKsstAF54QQoWD7tt/5HhVYPE68Sj48ghk7MERW96kN9D4dtCI7Amzr3xdUrrvxZIMv4qqrwNzWBVothdJTI3nEIS7AN1kUHBMIUEUTmVFj1zpyg56rfzGD0KdnIGonqfa0s/vdWXvvjN+xeV4NGgvPGZ/PS92aw+hdn8JMFI8hNMmF1+Xh5TSk+q5jSSw4v3TfsEwaask4YIIZBHdU3TZlyzOd8RRtx8+cD4Fi/oWuVDsSknNpa6dQGA0FmkjLMFE9MZ+o5RSy4aQxX/Go63390Ht99YDbn3z6BOZcOY9ScbDKKEtAZNAQCCoGAgqyRSFGn9MK8ZcibjhIIhE2AxTyAuoMxeGxxlXTWAR1eCN1nVLRP6bn37AXAMOLk+3x0hYVoUlJQfH5czTpBDAN9l1ZEC32uAMXQf6gmiMleobdpVFQPoPAWWBQ8gDpj8nWw5jFxhW+rg7iMiIsNG5XCrt1WlCY3Pn8Arab3fNkRNA3rkl8Uyv/qaIBoGDH8pDuphiNu7lyaX34F2+rVwrE2fBpwyDwRhmuvh9LVMGR+6FdJmWbO+O5oZlwwhG+/KGfHl5U0Vdn5/IWdrH1/P5PPKmD0nBxyk0zctWA4Pz5jGF/tb+C19eW0rRf6h7TwEXi1+lN0iojkCINj3TcAWGbN5GSDvrCwvUr31VcknHNO14XGLBKTLiUfwKwf9mi9kiyRkGYiIc1EUVgHWFEUbM1umqvtmOL1WNQLifrdIW8ZMsbgrawkYLcLv5tYBEa3CAmhSzpNglVvFbYchrhuXtkHKErHCIy3XgNOPv0PiMli0+TJ2JYtw9kcjzmtSYihswe3ixSrAA0i1BT4NL+I6GiTFeIMWhJNQVM5RWnXAGVEkQBljIbcaRDwwbevdbvYhPHpYv+8Etsqex+26fL6wSFIXW74dJGidHGAdgX1P6aTxP+nO5inT0cyGPBVV+PZv7/jLzW69jJ9hGkwgLhkA6dcNozr/zyHmRcNwRSvw9bkZtXre3n5V1/zzUcHcdm8yLLE3OHp/PuaKVwzVrQ3OwigVQLUafxdCQTa9T8zTj4CBO1VINvyFZEXUD+j0q/FBUY/IEkS8SlGCsamkl4QQQCdOwVkTaj6ox827IQzpYwm1AqQ5+BBAk6naFEmFQg7jopvoruxpgPgbAKNAbLGn/QeTapdhqMtmBt6DOiAYhWgQYRaAUryCqJhlRXyks3tV/32enA2AxKkRjk3Zsp3hQ/P5ldgzo+7ZEyB8JcJyKAPSLz6+X52jElHAVAUFCAQEPeKQvBetPDEzwqNdg8Jwemi7JwwAtS4X7w3jSFkRtYegXFyToCpkI1GzNOnY1+9Gtuq1RiGdcp+G3cpbHpJVBfOf0SQoggwWnRMO7eISWfms2ttDZs/LcXa4GL9RwfZ/GkpY07NYdKCAuJTjNgahV9QiAC5WtsrdJ3iL9x79+JvaUEym094t+7uEDd/Pk0vvIDtyy9R/H4kTSffl6QCyJkiokt2fSSCiaONTgLo9gmwk6+90hto09PRpKXhb2jAvWeP8KgpmCPcoEu/FsMG0YKq/8meiBIA90FhjXAyRGBEgmly8Fhf7UNRQCpbCzNuHdR9ihGgQYRaATK7xYSHVVYo7iCADvapkwuFc2k0MfZS+PiXhw1IlTUymhQDSoObHdsbeGtP76fB7ggI0WaH6SL15Jo7FbQGlEAA1w6Rom06SSfAwhE391Tsq1djX7WK1Jtu7PjLwlPBki4I5IGVMHzBYdelDU4fjTklm/2b69n0SSkN5Ta2flHB9hWVDJ+RSUtQp5WgEqD9X4jqYOpw4R4eBkdQ/2OeOvWkrTSYp0xGjo/H39yMc+tWzJMjGEGOWSQI0M4PBpYA5asO0OJYYYhlgB0RxtGjsa9ahaukRBCgwtmw9TUhhI4mwttfBw+Cz4ccH4/2GAzlPhowjh2LpNPhb3PhtWnQxypAJzdUF2i9Q2gvhAdQGFEIOUAPgCjYmCACUr/9n6gCdZMQP3xUCntWV3O6bGJaghavQcZnkPEbZLxGGTQSkiQhIYpI4l5CkkD2KZi+FA62HaZXOhkgeg6VErDZkIzGrhWPkxCWuXPhwf+HY8MGAk4nsinsf0KjhTEXwfpnRa7bEQiQClkjM3xaJsOmZlBe0sSmT8qo3N3M7rU1oWVCFaA96vh71/Bg+0ms/1Eh6XTEzT0V65KPsa1YGZkAjV4En/8fHPxSGBaaU6K3A86W9ouj3OAEWNBfJjYBdmQYR40KEqDg3zBow0HlBvC5xcRlNBDBAdowYkRHXd9JBNlgwDh2LM4tW3A0GtDHl3eZlDzq+zRoW46BBkcDGr8O2SWupK2S0mkEPkoO0N1BDUjd/k63AalFI8WB22Lzk1XmJn+vk+LtdoZtbGP0V61MXG9j9h4PC+olLvIYudqcyC056dw5Oo/bRol/bINFi94YxrXViYvgBIaq/zGOHo2kjXFyfXExupwcFI8Hx/qu0RehabBdHwq35l5AkiQKxqRy8U8m851fTmPo5HSQICXHIj6jgD/M/6eT/sfnw/GNIEAnk/9PJIR0QN35AaUOhcxxQluye0l0N161SdwnF0FcOn6bHW8wAiM2AXZkGMeok2BBfWXacGEN4nNB1ZbobMTjgFrR1idverv+Z0SUpQzHGUJ+QPZgruAgV4FiZ5tBRJ2zjjhPEgA+GdwSnUwQgxNgGaNxfvstZd+7GV1ONpZT5xJ32lxMU6f2z09CDUhtOiBGqydf22WRoVMzOEtRaK5xYGt2YW9xY2t2Y29x43H58Th9eJw+miNkHKno0P5qqxXbQwrpF2L6n46QJAnLqafS8sYb2FatJu600zouUDAL4rLAViPaVSMjTCL1AJlFCZzzg/HYW91o9UEdS+UmcDQI08WCjiTHVVJCwGZDTkgITdOcrLDMnSvS13fvxltVhS4ngk/W6EXiJLjzg3YD0migvKP+R60uaDMy0IaFVMcQGYZglcy9ezeKzycuugpmCb1W2ddQEIXqZvW3oo0clwWJeaEIjJNdoxVKhq8Pts/L17WHPQ8CYgRoENHgaCDeLSosbbICEh1jMNQJsPSRWF/6mIDdjnvvPtx799H0wgtIZjOWmTOJO20ulrmnoc/rZSlRDUhd9gfRBotAgGRZYsSMrAgvBo/Th63F3YEU2Vrc2Jtdoedddh9DJqe3v0gNwsscKxK0Adc2dQIsRoBUWOYKAmRfFSEWQ9aI9uW6J4UpYh8JUGhbiWEl/73B8NNhZ3QRWIemv6ZP7yr8PcmgTU7GNHkyzo0bsa1cSfLVV3ddaMwiWPFnOLAcXFbRdo4GwrxlIKb/6S30hYXIZrMIjz10SLTdC+cIAlS6Bk79Sf83Em5SKUlhHkAnpwBahdoudtfa8M+4F8348wd1f2IEaBBR56wj1y1cj5sROUChCpC9ARyN4nHaCFw7/w5A0hVXoHi92Favwl/fgG35cmzLlwOidaKSIfP0aT3z05l4NXzxQHtAalrPNTh6k5YUkzYU9BgJSkBBCnOyDQkNg+PvitcbKkUbT/IR+HBYZs0CrRbPoUN4ysvR5+d3XGDspYIA7VoCXpcITI0GunF/hpMv/+tIiJs/D+fGjbStWBGZAKWPEkLyxr2w5xOYcHn/N9rBW0bV/8QmwHoDSZYxjBqFc9MmXCW7BAFS/YDK14qsPbmf6pAwAbS/tRVfjdDaGYaf3C0wbVoauoICvGVlOONPJy5v6qDuT0wDNEgIuUB72l2gk8w64o3Bq25V/5NUiKI1hUhC8rXXkvPgnxm+ciXF77xN+k9+gmnaVNBo8Bw8SNNLL1N+yy3smTmLsh/8gKb/vBo5uFFFLwJS+4IO5AfaJ8CC7RX3vn0objdyXBz6osKob/94hSY+HvOkSQDYV0dwhc6bDgl5wgxv32fR2ai1SkSUIMHwszr8SvF4cGwS2pOTXf+jIl51hV6zloDD0XUBSRJVIICS96Oz0cb9IhxZaxQaI8C9S/WXObnbkr2BKhYP6YCyJoA+LhheurP/GwiLwFBblLqcHDTx8Yd50ckBtQrk3Lx5kPckRoAGDaoHUKInFVA9gMInwNT21yi85eViSspgwDBUjCVLsoxxzBjSfvB9iv7zH0as+ZrcRx8l8TuXoc3IQHG5sK/8ktoHHmD/wnPYt3AhtkjtFOhxQGq/4W6Dmq3icYHqAB0UQI8bh9Tfq64TDJa5cwGwRYrFkGXRBoNuTRF7jT3B9lfedLCkdfiVc/t2FIcDTXIyhuGxST0QpoO63FwUjydUHeuCMReJ+72fg6d7nVyPoZr15UwGrV5YSKj6klgLrMdQhdDuXcHjrEYb0lT1NRg1hNYKaKsCSQM5k0ITeid7+0uF6gfk2BQjQCctVA8gNQbDKneeAFMF0KNw7RRXJIaRI7udktIkJJBwzkJyHniAYStXUPz+e6Tfew/mGTNAq8VbWkbdI3+LvDOdA1IHChXrQQkIo7jg6KNru+r/E9P/dEacmgu2di2KJ8K0lyoe3LM0OidXlQCNOLvLr9rT32fGiGoQkiQdeRosawIkFYLPCXuj8N3q1P7ylpejOBxIej36oqL+r/8kgWFUcBJsZ0nIwDWUC1baz1ww9TPKHAt6S/sIfKxFCYAp6Ajt3LoVxTeAF9w9QOxINkhQK0DhLbCOE2DqCHw7AVKvWo4ESZIwjhxJ2q23UvjySwxf/gXodLh37cLdOV4BehWQ2i90ir8AcKoj8DH9TxcYRo1Ck5aG4nBEvlrKmSJGob2OdvLSV3idcHCleDyiq6g6pP85if1/IiGcAIVOpOHo0Ab7oP8b7OwArY5XDxsWs5DoBQzDh4FWi7+lJaTPCR2XytYIrVVfEdb+AsI8gE5u/Y8Kw7BhyPHxKA5H6P93sBAjQIOEekc9KBIGZ7gJYmQPINcOlQCN6dO2tOnpxJ1yitjO4m48SdQ2mBqQOhDopP8JuN2h6YhYBagrJFkOfW721RHal5IEYy8Rj3f0sw12aLUgUgm5IW2JioDbHerXn6z5X93BPGM6ktmMr64Ot6on6YzRwTbYnk+EYL2vcNtEgCS0n1xV/U/MALFXkA0GDEOEnCBkiJg3DWQdtFW3V+D7gjCXbiUQCBGgmEhdQJJlTEF9o3PzlkHdlxgBGiTUO+sxe+ORAhoUwCaFVYDsjSLqAFBSR4RVgMb2eXsJ558HgHXx4shXqj0MSO0z/N72K6Og86q7pAR8PjSpqSetPfyRENIBrf4q8gKqKeLez4TGqq8ITX+d3SUXzrl5C4rHgzYjA31xUd+3cQJCNhiwzBGVg7bu2mC5UyE+Bzw2MRLfV1RtFi3khDwxvEDYBFhM/9NrqMGorpKg6FlnanfEf+YM+OJPQhTdG/g87WaKedPxVlURcDiQdDr0hbEhDxWqH5B7795B3Y8YARok1DvqiXOL9pdNVlAk2jVAavUnsQBfUxv+lhbQavtVQo0/4wwkoxFPaWmIUHWBWgXa/J/+lYAjoXqrqDCYkiFNiAFDBojjxp609vBHguWUOcJHZNcuvLURKnNZ4yF1mHCx3f1x3zaiKGHxFxHaX+uC+V8zZ8Y+pwiID7XBVkZeQJbbE+J39qMN1kn/A+DeFfQAik2A9RqGoJmn+jcE4IJHBWH12uHLv8I/JsHXj/W8cle7DfxucZxLGRJygNYPHXrSZudFQvLllzNs2edk/d/vBnU/YgRokFDvrA/pf1ol4QGUq1aAVAIULoAePrxfrs+yxRLSK3TbBht3GWhN0LC7/WAbLajtr/xZIY8NNQLDFNP/dAttcjLGYECs/asIVSBJaq8Crf67OFjvWwbW6p6T2LoSaC0To9XFp3X5tSOW/3VYWIJO3a6tW/E1NEReSNUB7fpI5Lj1pc3cSVvib2vDW1kJgHFkbMKotzCOFpIC186w1mX6CLhlGVz5H3Gh5myCT38N/5oqLgyPNCUb/hlJUlj7K/b5hEObno4uN3fQL6hiBGiQEF4BssoKaXF6zPqgiFHtP6eP7LUA+nAItcE+/hglEOi6gBqQCtH3BFIJUGGYADoWgdEjxJ0anAaLpAMCGP8dQBL+JZ/+Gv5zKfxtFPylCJ4/Fz66B755Bg59JYI5O0N1fy4+DfTmDr8K2O04twrrAvPMGAGKBF1GBsagi7lt5ZeRFyqYDYn54LbC4nvhkZHw4gU9J0OK0j4CrybAqxEYWVlokpL6+zZOOqhxLt7KSvxWa/svJElU7G5bA4seE7o4awW8fzs8MQdKPuz+4qKzSH1PbAT+WEaMAA0S6px1xIcRoNwOI/DtHkD9FUCHI+6005Dj4vBVV3dvQtWDgNReQ1HCBNCCAPltNjwHDwJgGh+rAB0OliABsn31NYrf33WB9JFw40cw/z7hO5M2QniQuFpEttGG52DJT+HF8+CvxfDwSHj5Ylj6K9j0ikiVh4jp745Nm8HnQ5ebiz4vb+De5HGOI47Dyxq45XM46w/Cw0cJwKFVPSdDLaVCFyjrxGg94Aq2bmLi2r5Bk5CALjdox1GyK8ICWpjyXfjxJjj7T6Kt1bAbXr8Onl0AByNckHRqU7pDHkCxz+hYRGxuchAQcoEOEqA2WSE/Ughq+mhcO58GokOAZIOB+AULaH3vPayLl2CeGsGGvAcBqb1G4z4R66E1QvYkIOj/oyhoc7LRpqb2fxsnMEwTxiMnJBBobcW1bVtogqIDik4VNxVel4hgqCsRlSH1vqVMhKjaaroKciPEXzhU/U+s/XVYxM2fT8Njj2H/6isCHk/kdnV8Fpxyl7g1HxLfrx3viXT3Q6vEbcnPoPAUUYkdvQjihE9YqLWSPSEUexI6ucYmwPoM45jReCsrce8q6T7iRWeEOXcIMvT1v2DNv6FyA7x0AQw9E868H3Imga1efK5IkDuVgNuN59AhIFYBOlYRqwANAlQPoISgC3SrrJCnVoAcTcKQEPCSgq++HmQ5ald5oTbYJ59ENqFSA1Ihem0w1Vgsd5rwHAJcO0T7K6b/OTIkrRbLHDE5F9EVOhJ0RiGQnnAFLPg/uOZ1uHsb3FchNA6L/gWzfgRD5kN8Nky8BpLyu6ymPf8rRoAOB+OY0WjT0wk4HDjW90A/l1wkiND3l8Nd3wYrQ1O6rwypJoqqWzHg2q1WgGIn175CJY8ddEDdwZgIZ/xGfF4zvi+qcfuXwdPz4M2bYNsbYrn0kWBMxL1vHwQCaBIT0WakH37dMQwKYgRoENDgFELJeE97Enx+iiqADlZ/EvNx7y8DQD+kGNls7rKevsAyaxaa5GT8jY3Y13Vj3z/xapDk9oDU/iJkgNieIRXT//QOqiu0rTsdUE9hiBfl+SnXwzkPwvXvw7274JInuizqt1pDGrSY/ufwkGSZuPnzgMNMg3WHDmRoK5z1x65kaGvQmiJIgBS/vz1hPFYB6jNCQujuPJwiIS4DznsI7lgP468AJOHD9cmvxO/V9pf6+YwcOehi3xgiI0aABgF1jjq0fj16ryA91vAKULgB4s7o6X9USDod8QtF1EG302DRDkiNIIB2BTPAYvqfnkHVAbm2bsPX3HxUtunYsAECAfRFRegyM4/KNo9nhHRAy5dH9trqCZIL4ZQ7O5Kh3GCrWmcJtTk9ZWUoTieSwRDzl+kHVCG0+8ABAm53716cUgyXPQM/XNWxfaz6nKku3bH21zGLGAEaBDQ4G0L6H7ek4JFo1wBFjMCIHgECSDz/fADaPvuMQKSMKYheQGpbDTQfFBWlPNFj9zU1tY/vRvm9najQZWaKA6miYP+6n1lFPUQo/yum/+kRLLNmIen1eCsq8ESKnOktVDJ06xfwkx2i4hCfBYTpf4YPR9Jo+r+tkxShCTqfD/fePla7s8bDtW/ATUvhgr+HYoXaM8BiBOhYRYwADQLqHHVhHkDiSjEnKQIBiuIEWDhMU6eizcwk0NaGvbuE+GgFpKrVn8yxYswecG0X7S99URGahIS+r/skg0UNR+2pDqifiOl/egfZYgm1CrudBusrEvNCAcLQrv8xxByg+wVJksIMEXvRBouEwtkw7Xti4g9w7Q16AMUqQMcsYgRoEFDvqO8wAp+ZYMCoC17FBTVAPkMu3qoqoN2yPVqQZJmEc88V2++uDRatgNSQ/mdO6ClnsP1ljLW/eoU4NRbjq9V9b7H0EL7m5lAJ3zyjm+mYGLpA1QF1G4sRJagZYMaYA3S/EdEQsZ/wNTXhr28AScIwbFjU1htDdBEjQIOAemd9hxH4kP7H2SKC+AB3vfB70RUWoImPj/o+qNNgbcuXE3A4Ii8UjYBUdQIsTADtCgqgYwGovYNpyhQksxl/fUOInAwUVPdnw4gRMZuCXiBu3nwAnJs2iwibAUJoAixWAeo3Qplg4ZEY/YTa/tLl5yNbLFFbbwzRRcwHaBBQ76inwD0UCKbAJ3eaAEvIxbWvFBg4jYxx3Dh0BQV4y8poW748pAvqADUgtXKDMP9KGSoCA3Um0Jm7uQ97LElQK8iOaoCoKArO7WoGWKwC1BvIej2WmTOxLV+ObdUqjAM4/ROe/xVDz6HPy8UwfDjuvXuxrVpN4oUXRH0bfqsVX5W4UIoJbPsPY1gmmBIIIMn9rwu0C6D7nt8Yw8AjRoAGAfXOZUzhyAAAV9xJREFUesa6xQi8VVaYmaJOgKkO0CNxbR4Y/Y8KSZJIOO9cGp98CuviJZEJEMDUGwUBKl8nbn1BchEkiLR3X00N/oYG0GhCB54Yeg7L3FOxLV+OfdVq0m69dcC2E8v/6jvi5s8XBGjFigEhQOrJVZuTjSYxMerrP9mgLy5GMhoJOBx4y8rQFxX1e51qBIYx5gB9TOOYaIH9+9//pqioCKPRyMyZM/nmm2+6XfbFF19EkqQON6PR2GEZRVG4//77yc7OxmQysWDBAvbu3TvQb6NHcPlcWD3WkAhajMB3qgCljx4wAXQ4Es4TbTDbqlX4W1sjLzTpWrjiZTj3IVjwe5j3S5hzJ0y/FSZdJ4I4R5wLxfPElFfmeFEpis8BYxLo42D6LaHVqdUfw/DhyCZT5G3G0C1UHZBj0yb8NvuAbMNbW4fnwAGQJMzTph35BTF0QNzp8wHxvYpoNtpPuGL6n6hC0mhClbRe+QEdBiEPoFiF7pjGoFeAXn/9de655x6efPJJZs6cyaOPPsrChQvZvXs3GRkZEV+TkJDA7jANRGeTqb/+9a/885//5KWXXqK4uJjf/va3LFy4kJ07d3YhS0cbDc4GJEUizpMEgFUOkN/JA8hvKcJTKvKZBpIAGUeMCJXr2z7/nKTLLuu6kCyLfKkoIab/6R/0+fnoCgvwlpbhWLeW+DPPjPo2HMELEOOYMbEKQx9gmjgRTVIS/pYWnJs3Y54+/cgv6gViE2DRh3H0aFxbt+LaWRIaEOkrFL8f917VBDFGgI5lDHoF6G9/+xu33norN910E2PGjOHJJ5/EbDbz/PPPd/saSZLIysoK3TLDTNoUReHRRx/lN7/5DRdddBETJkzg5Zdfpqqqivfee+8ovKPDo95Zj9mTgKxo8KNgl2gXQdeJA5u7VZA0bU422uTkAd2fUDRGd9NgUYZre3ACLKb/6TPiTg1Og3VnYdBP2GP5X/2CpNEQN+80YGCmwVQPoFgIavSgtuOjIYT2lpejuFxIRiP6goJ+ry+GgcOgVoA8Hg8bN27kvvvuCz0nyzILFixgzZo13b7OZrNRWFhIIBBgypQp/PnPf2bs2LEAHDx4kJqaGhYsWBBaPjExkZkzZ7JmzRquuuqqLutzu924w1xArVZrNN5eRIR7ALXJCpIM2UlGcLVCmxh7d9WIqayjYRKYcN551D/6D+xr1+JraECbljZg2xIC6B0AGMeNHbDtnOiwzD2V5ldfxb5KjMNH22Y/pP+JCaD7jLj582l9/wNsK1aS+bOfRW29HasLA0uAmqoq+OqNV6ndvwdTfALmxKTQzZKYhCl4b05IxJyUjCkuPioC4sFAaBIsCi0wl2pSOWxYzKTyGMegEqCGhgb8fn+HCg5AZmYmu7ph4iNHjuT5559nwoQJtLa28vDDDzNnzhx27NhBXl4eNTU1oXV0Xqf6u8548MEH+f3vfx+Fd3RkhLtAt0kK2YkmdBoZqoItvfhsXHsPAkeHAOkLCjCOH49r2zasn3xCyrVRSH/vBt7SUgJWK5JeHzMH6wcsM2Yg6XR4KyvxHDyEYUhx1NbtqajEW14OWi2mKVOjtt6TDZZTTgGtFs/+/XjKyqJWCfCUlorqgsk0YNUFW3MTa976L9u++BQlEACgta72iK+TJBlTQkIHchSXmkZafiHpBUWk5Oah0eoGZJ/7C8OIESDL+Bsa8NXXo03ve3hpyAE6dow75jHoGqDeYvbs2cye3Z4pNWfOHEaPHs1TTz3FH//4xz6t87777uOee+4J/Wy1WsnP75qMHQ3UOeo6mCDmRYrAWDHwAuhwJJx/niBAi5cMKAFSA1ANo0ch6Y7NA+HxANlsxjx9Gvav12BfvSqqBEiNvzCNG4cmLuZf0ldoEhIwT52KY906bCtWknL9d6Oy3vDx6mhXFzxOB+s/fIcNH72LL1gRHzptJpMXXojX48bR2oyjtRVHawv21hacwXtHawsuWxuKEsAR/DkSZI2GlJw80gqKSC8sJr2giLTCIuKSUwc9LFQ2mdAXF+PZvx9XSQlxUSFAsRH4Yx2DSoDS0tLQaDTU1na8uqitrSUrK6tH69DpdEyePJl9+0SOi/q62tpasrOzO6xz0qRJEddhMBgwGAx9eAe9h6gAtY/At4egigNbIHEY7v0fA0eRAJ17LnV/+SvOTZvwVlWhy8kZkO2o+h9TTP/Tb1hOnYv96zXYVq8m5frro7Zexzex/K9oIW7+/CABWhE1AhSaAIvieLXf52Xr50tZ8/ZrOK1iGjR7+EhOu/Ym8kb3bFjB7/PhtLaGCJF6a62rpaH8EPWlh/A4HTSUl9JQXsqur1aGXmu0xJFWWER6QXGQHBWRlleI7igPrBhHjQoSoF3EnXZan9fj2qNO6cU0Wsc6BpUA6fV6pk6dyrJly7j44osBCAQCLFu2jDvuuKNH6/D7/Wzbto3zgiPdxcXFZGVlsWzZshDhsVqtrFu3jttuu20g3kavUOeoI8kjCIBVVpiWEqwA1Ynes9uZDIEAmvQ0dN1MwUUbusxMzNOm4Vi/HuvHS0m9+XsDsh21AmSMTYD1G3FzT6Xur3/F8c16Ai4XchROFoqixPK/ooi4+fOo+8tfsK9fj99mj0pFzb0rehNgiqKwe80qvnrtFVpqhbFicnYuc6++gWEzZveqKqPRaolLSSUuJbJruKIotDXUU192iIayQ9SXHqShvJSmqgpcdhsVO7dTsXN7+wskidTcfCaefR7j5i04KmTIOGY01sWL+6UDEl5C5cCx3wJTFAVbcyO2pkYM5jjMiYkYzJZBr8YdTQx6C+yee+7hhhtuYNq0acyYMYNHH30Uu93OTTfdBMD1119Pbm4uDz74IAB/+MMfmDVrFsOGDaOlpYWHHnqI0tJSbrlFeM1IksTdd9/NAw88wPDhw0Nj8Dk5OSGSNZhocDaQ16EF1rEC5GwQ/3xHOyU94fzzBAFavHhACJDi84UOLKZYBli/oR82DG1WFr6aGhwbNhJ36in9Xqe3tBRfbS2STodp8uQo7OXJDUNxMfrCQjylpdi//oqEs8/u9zpdwRZYf13Ay7Zv5ctXX6D2gBBUmxOTmHP5NYw7/Ww02uifFiRJIiE9g4T0DIZObc+W83k8NFaWC1IUJEcNZYewtzTTWFHGF88/yddvvMqks89j0sILsCQN3FSsKoR294MAufftA0VBk5Z2zETIeF0umqoraa6qoKmqkubqSpqqKmiursLrcnZYVtZoMSUkYE5IxJSQKATu6uPE4H18++PjnTANOgG68sorqa+v5/7776empoZJkyaxdOnSkIi5rKwMOWyyoLm5mVtvvZWamhqSk5OZOnUqX3/9NWPCCMPPf/5z7HY73//+92lpaeHUU09l6dKlg+4BBF01QPnJJnBZwVoBgKuiBTj6BCh+4UJq/vgArp07cR88iKE4eroSAPf+AyhOJ7LFgj7K6z4ZIUkScXNPpeXNt7CvWhUVAqRWf0yTJ0elohSDaIM1vfQSthUr+02A/C0t+IKDHH2tLtSXHWLVqy9wcMtGAHRGE9MvvJSpF1yM3nj0jUm1ej2ZxUPJLB7a4XlHawu7165m4+L3aK2tYe07r7P+w3cYM/d0pp5/Cal50ddoGoIEyFNa2ueKnTvkAH109T9KIEBbYwNNIZITvK+qpK2xvtvXSbKMJTkFj8OOx+kk4Pdhb27C3tzUo+3qTSYuvPuXFE06PgcmBp0AAdxxxx3dtrxWdPLR+Pvf/87f//73w65PkiT+8Ic/8Ic//CFauxgVuHwunA43Br+o+rTJCnkpZmgQo+HEZeHaKrRMR5sAaZOTscyZg33VKqxLlpB+++1RXX/I/2fs2ON2VPZYg+XUubS8+Ra21avJPPLiR0RI/zMzlv4eLcSdHiRAK1f2O2dKHa/W5eb2OiDZ2lDH12+8yo4vvwBFQdZomLDgHGZdetWAVlX6CnNiEpMXXsDEs85l3/q1bPjgHar37WbbF5+y7YtPGTJlOtMuvJS80eOiVoHQJieHqqoNjz1Gyo03oOuhFlVFaAR++MC2vxzWVqr27KJq906q9pRQe2A/Po+72+WN8QmkZOeSnJNLSk5e6D4pMys0mefzeHBYW3FaWzvcd3iutRVHWyuO1la8Licep5MvX32BwolTjstK0DFBgE4WNDgbiPMIAbRTUlA0ElkJRjgo+vqB5OG49woCZDrKBAhEG8y+ahXWxUtI+9GPovoP7dymGiDG9D/RgmX2LNBo8Ozfj7eyEl1ubp/X1UH/M2tWtHbxpId5yhTkuDj8jY24tm/HNGFCn9flDjlA9679teWTxax45Vn8Xi8AI2adyqlXfZfk7L7/vxwtyLKGETNPYfiMOVTtLmHDR++wb8M6Dmxaz4FN68kcMpxpF17CiJmnIEdhKs4yaxat771H04sv0vTyy8TNnUvSFZcTN28eUg9ag6EJsCgKoJVAgKaqSiqDZKdqdwnN1ZVdlpM1WpIys0jOySMlRHTEY1N8whG3o9XrSUhLJyGtZxNwjtYWnr3zVurLDnFg0zcMnXr86QZjBOgoot5Z36H9lZNkQiNL7QJofw5496NJTEQ7QJNYh0P8mWdSo9fjOXAA9+7dUU0bj0VgRB+ahARMkybh3LgR2+qvSL7yij6vy713L/6mJiSTKabRiiIkvR7LqafStnQpthUr+kWA2jPAen5yddraWPHyM/h9PvLGjOO0a28ie9jxN50kSRK5o8aQO2oMTVWVbFryHjtWLKP2wF4W/+OvrErPYOp5FzHu9LPQm8x93k72H36P5ZQ5tLzxJo7167GtXIlt5Uq06ekkXnIJSd+5rFv/JUVRwmwK+l4B8rpd1OzfS9XukhDhcdltXZZLyc0nZ8RockeOJnv4KJKzc6JCAnsKc2ISkxaez/r332LtO68zZMqM464KFCNARxH1jvqQCaJVUshXJ8CCAmhXq/jiGseOGZR/JE18PHHzTqPts8+xLl4SNQIU8Hja05FjJ9eoIm7uqTg3bsS+elW/CJDq/myeMgVJr4/W7sWAmAZrW7qUthUrSL/zzj6vJ3Ry7QUB2v31Kvw+H+mFxVxx/4PH3QkqElJycllwy+3MueI6vv10CZs/+QhrfR3LX3qGr9/8LxPOOpcp51zY7UTa4SDp9SReeCGJF16I++BBWt9+m5Z338NXX0/j00/T+PTTmGfPIuk73yH+rLOQw74rvvp6/C0tIMsYhg3tfiMR0FpXw6aPP6Rq907qDh0g4Pd3+L1WbyBr2PAg4RlD9vCRParqDDSmnX8xmz/+kJp9eyjdtoWiCcfX8ESMAB1F1DvDCJCskJfUcQLMVSt6uEdb/xOOhPPPFwRoyRLS7/lJVA6Y7l27wOtFk5TUrzZNDF1hOeVUEWXy9RoUr7fPBpOh/K/Y+HvUEXfaaSBJuHeW4K2tRZfZe8WW4vOFIjCMvRiB37lyGQBj5515QpCfcJgTEpn9nauZtuhSSr5czoaP3qW5upL177/Flk8Wc80fHyKtoKjP6zcUF5Px05+SfuedtC1fQctbb2FfvRrHmrU41qxFk5RE4kUXkXT5dzAMGxZKgNcXFvZ6iODTp/5J2fatoZ/jklPIGTmG3JGjyRkxmvSiIQMynddfmBOTmHDmQjZ9/AHr3nk9RoBi6B5tnjbiw3LApqSYwG2D1jIAXIfqgMElQHHz5iGbzXgrK3F9+y2mbswje4OQ/mf8+BPuIDzYMI4dgyY5GX9zM84tW/qUPK74/TjWbwDAchQNED0uJ20NDXhcDjxOJx6XE6/T2f7Y1f44/N7rEo9lWYM5MVHkUyUEM6qS2h+L3yWjN5kG9f9Om5KCaeJEnFu2YFuxsk+VOs+hQygeD7LZjK6HLvVNVRVU79uNJMuMOmVer7d5vECnNzBhwTmMP+Ns9m9az1evv0JD2SE2fPQu5/zoJ/1ev6TXk7DwbBIWno23spKWt9+h5Z138NXU0PTSSzS99BKmyZPRpAh9Z2/1Py21NYL8SBILf3gXBWMnEJ+WftwcK6ddeCnffraEipLtVJRs77F55rGAGAE6ivjhxB/y9qcbqGmw0ioHhAdQg6j+KKYM3Hv3A4NLgGSTibgzz8T64Ye0Ll4SFQIU0/8MHCRZxnLqqVg//BDb6q/6RIBcu3YRaG1Fjos7Kv97Nfv38u1nS9j11ZeHnVzpCSKJQTtDq9NjSkwUwZ3BW3rhEMbNP7NfepHeIG7+fEGAli/vEwFS9T+GESN6PEm2I1j9KZ409Zic9Io2JFlm2LSZWBKT+O9v7mXX119y2nXfw5yQGLVt6HJzSb/zx6Td/iPsq1fT/Oab2JavwLl5c2iZ3kZg7Fj5OQCF4ycxbv6CIyx97CE+NY2x8xawddlS1r37RowAxdA9bE3igN8mBzVAwfaXWy5CcVcgWyzoBijksKdIOO9crB9+iHXpx2T+8hf9zhxy7Qg6QPciAsPrdrH6fy/j83qwJCVjSUoR98nJwZ+Tj9lgxaONuLmCADW9/DKOb75BX1yMvqgIfXERhqIidIWFHbQKnRHS/0yb1qNJl77A63ax6+sv+fbTj0PmewAGiwWD2YLeaEJnMqE3Bm8mE7rgfXc/+32+YDZVsxjXbQlmU1mDUQwtLXjdLnxeD20N9bQ1dPRD+fqN/zDx7POYcu6iAScIcafPp/7RR7GtXs3By69Al5WJNjMLXXaWuM/KRJuVjS4jPaIGq7f6HyUQYOeq5QCMOe3MqL2P4wFZw0aQOWQYtQf2se2LT5l58eVR34ak0RA3bx5x8+bhq6+n5d33aHnrLbzV1cSffnqP1xMI+NmxQhDV45H8qJh+0XfYtvxTDn27iZp9e8gadmy7YKuIEaCjiIA/gL1FEKCQC/RuMQHmcqQBFRhHjx50n5y4U05BTkzEX9+AY/36fo1FB+x23PsPAL2rAG39fCmbPv7gsMsY40XytCU5JUSKBElKwZKYjMFsRmc0oTMa0BmM6AzGY7KP3l/EnXYamrQ0/A0NODdv7nA1CoAso8vNDZEifVGRcCkuKkKbmdmu/xmA9ldjZTlbP/uYHV8uw223AyI2YcTsuUw86zxyRowa0FK/1+UKEqL2nCp7cxMlX62kuaqCb957k42L32PsaWcy7cJLBmw03DBiBIbRo3GXlODatg1XsC0cCZq0NHRZWWizMtEFSZL9q6+Anut/ynZsxdbYgMFi6eC8fDJAkiQmLbyAT554lG8/W8L0RZciywM3HaVNTyft+7eSeustEAj06oKxbNu3tDXWY7BYGDZ99pFfcIwiKTOL0afOZ+eXX7D23Te4+Ge/Gexd6hFOvLPBMQx7qwdFAR8KXp1MepyhXQDdJEiPcezgtb9USHo9CWefRcubb2FdvKRfBMi1cycEAmizstD2MGFZURS2ffEpACNmnoIxPh57S7NwKG1pwd7STMDvw9VmxdVmpbGirMf7I2u0HQiRzmBs/9nY/pwkSygBBVBQAgqKEkBRlLDngj8rwccAgQAKCpnFwxh/5sKolt4PB01SEsOWfY7nwAE8hw7hPngQz8FDeA4dwnPwIAGbDW95Od7ycuyrVnV4rWQyoXg8QPTyv/w+H/s3rGXLp0so39Eu7EzMyGTCgnMZd/pZR+1vozMaSTRmkZjR0dBu1qVXsm/jOta//xbVe3ezddlStn7xCcNnzGb6osuiPiouSRJFr7+Ge+9efLW1eKur8dXU4q2twVddg7e2Fl9NDYrHg7+hAX9DA2zf3mU9hpE9m8xUxc8jZ89FexJO9Y2cM5eV/3metoZ6Dmxcz7DpA+9tJUkS9LJavn35ZwCMPnX+cf85zbj4cnauWs7+DWupLztEej8E6EcLMQJ0FNHW6BL3skJesglZlqA+WAGqbAMGV/8TjoTzzxcE6NNPyfrtb/o8Gh0KQB03tsevqd67m8aKMrR6A2f/8E4M5o6W9EoggMtuCyNETYIgtQR/bhY/e5wOvG43XrcrNFYa8Ptw232hasRAYO+6r1nz9v8YNec0Jp9zIZlDhg3YtlTIBgPG0aNDeUYqFEXB39iI5+BBQYwOleI5eFCQo/JyFKfIAtJmZ/fbvM3aUM+2Lz5h2xefhqz0JUlmyNTpTDzrPIomTB706qYKSZYZPn02w6bNonL3Tta//xYHNq1n77qv2bvua/LHjGf6ossomjQ1ahUqWa/HNHYsjI38XVAUBX9zM76aGrzBmyBHNfhqatFlZ2OaeGQfIY/LyZ5vvgbE9NfJCJ3ewPgzzmb9+2+x+ZOPjgoB6i2ctjb2rV8DwLj5Zw3y3vQfqbn5jJh5CnvWrmbdu29wwV0/H+xdOiJiBOgooq1JECCrGoHhsUNLGYoC7gMiC+xYIUDm6dPRpKfhr2/A9vXXxM+f36f1qBEYpl7of0LVn1mndCE/IE5epvgETPEJpPVQLuX3efG6BBnyul14XcGb+rPb3eFnRRFXdKGbLEP4z8HnJEkCSUaSJSQk/D4vu75aSc3+vexYuYwdK5eRM2I0k8+5gOEzTznqLThJktCmpaFNS+sikFa8XjwVFXjLyjAMG9YncqIEApRu3cyWzz7mwMZvUJQAIMZjx5+xkAkLFpKQlhGV9zIQkCSJvFFjyRs1lobyUjZ8+A4lq1dQvnMb5Tu3kVZQxPRFlzFy9twB/+wkSUKbkoI2JaVfx4G9677G53aTnJ1D9vDomZkeb5i44Fw2fPAOZdu20FhZTmpu9PPD+oNdq1eEPJoyinvnG3SsYuYlV7Bn7Wp2r1nFnMuvISUnb7B36bCIEaCjCFtzsAIkBUNQG4Q5oMeXRsDhRDIaj5mgUEmjIeGcc2l+5RWsi5f0mQCFKkA91P94nA52f/0lAOPP6H96tgqNVocmTocxLi5q6+wOU8+/mOq9u9n08QfsWfuVcHPdU4Il+TkmnnUuE84855iYypF0OgzFxX0OvvV63Lz5+19RvW936Ln8MeOZePZ5DJs+67gTqaflF3LOj37CnCuuY9OS99m67BMayg7x8WOPsPp/LzP1/IsZf+bZgxIa2huo019jTjvxvH96g8SMTIZMnc7+Dev49tMlnHHTDwZ7lzpg+3Ix/TXu9LNOmM8po2gIQ6bO4MDGb/jmvbc450d3D/YuHRbHRj36JEFGYQItuQYO6oIj8HUi28flFSzZOHLkgE3h9AUJ550LgG3ZMgLBVklv4GtuxlteDoCphxlgu9esxut2kZydS+6onrfNjjVkDx/J+Xf+jO8//gKzv3MNlqRk7M1NfP3Gqzxz+00seewRavbtGezd7Be2L/+M6n270RlNTD7nQm585HGu+N2DwWrJ8UV+wpGQls7862/h+/9+gVOvuh5zYhJtjfWsePkZnvnRTWz48B0URRns3YwIa31dSHc1Zm7Pp5FOVExaeAEgRs09Tscg7007ag/up+7QfjRaLaNPnT/YuxNVzLrkSgB2rvqC1rraQd6bw+PYOdueBMgfncLmNIlddj93pJigNkiArHFA3TEhgA6HadIkdDk5eKuqaHjyKfQF+QTsdgJ2O36bTTy22UPPBYLP+YM/q/oSXWEBmsSeiV63LRftrxPlqsiSlMycy69h5iWXs2ftV2xe+iHVe3dTsmo5JauWkz1sJJPPuYARs089rkiD3+dj/QdvA3DaNTcyaeH5g7xH0YcxLo6Zl1zB1PMvZueXX7D+w7dpqalm5X+eJ62giKKJUwZ7F7tAHX3PHzuBhPRjt/V4tFA4biLJOXk0V1Ww88vlx8z/6Y4VovozdNqsYyLSIprIHj6SgvGTKNu2hfUfvM2CW3402LvULWIE6CijolmQgrxkM2wPToDV+YBjR/+jQpIkEs4/j8ZnnqXxqaf6uhKSLrmkR4s2VpRRvWcXkiyfcOJNjVbH6FPnM/rU+dTs28PmpR+ye80qqvftpvqx3az8z/NMWHAOkxZecNQmpPqDktUraGuox5yYxNjTj1//kp5Aq9czYcE5jDvjLL54/km+/exjVv33JQrHTzpmRN0gRNQ7v2yPvohB6AUnnX0+y198is2ffMTEs88b9Asrn8dDSZCojjv9+Bc/R8KsS6+kbNsWti//lFmXXtmnXLajgRgBOopw+/zUtgkdUH6yCepLUBRwlTUAxx4BAki+7ru4duwk4HEjWyxoLBZkSxyyxSJucXHIFjOauM7PhT3+/+3deVyU9do/8M8sMOzIIqCIAiKbC6vgviSJWiktSHrccumov+qYJy21NB8rzLQnO/lknSzN1MrKJS3LSFxRRBAQBAUlFGTfZIBZv78/hhklcQFm5p7ler9evtSbm5nrI+Nwcd/f5RFnkKkHP/eNiDKIMTK64uHnj4kv/RujZsxFVtIRZB79FeLaGqT8sAf5Z05i1gf/MeirQUqlAqkHfgCgGu9kYSniuCL94PMFGDZ1Bi6fSkZFUSHyU04a1BYTt67mofZWKYQiEfpFD+O6HIPRf/Q4nPr2a9SU3MCNnCz0HhDCaT2FF86hRdwIO2cX9BkUymktutIraAA8A4NRkpeLtEM/YcysBVyX1C5qgPSotE41u8jaQgBnSzlQ+xdkYgGUjU2AhQVEfrqfLt1RFu5u6P3lNp0/j0IuQ+6JPwEAA8Zqb/CzIbPt5oShz05D1JR4XE09g2PbP0dN6U1c/O0wIp6I47q8+ypITUFt6U2IbG0ROn4S1+XolY2DIyKfegZnvt+F0999w8nMvvtRD372jxpm8AO19UlkY4PgkWORefQXXPztMOcN0KXW21/9R8fodIFGLvF4PAx5OgE/Jq5B5tEjiIqbapBXtg3n+q0ZuFGjGoTn5WwNXtVVAAwtTaoN9Kz69ev0WjumoDDtHJpvN8DOyRk+oRFcl6NXAqEQgcNGYXjCTADA2R+/RXPjbY6rah9jDOf27QUAhE2YrLe9tAxJxBNxsHHshrryW5qrllyTS6XIT1EtchlMt7/uoR77U3D+LBqqKjiro6GqEkWZ6QCA/mNM++vUJyQc7r5+kEslSP/lANfltIsaID1qM/5HvQJ0sysAw1gBmkvqbyT9x8SA38W9x4zVgLExcO3tjRZxI87+sIfrctpVdPECKooKYSGyQvjEp7guhxOWVtYY8uzzAICzP+6BrKWF44qAwgupkIjFsHfpjt79H75Yorlx9eoDr/6DwJgSWX8c4ayO3BN/AoyhV/AAOHn05KwOfeDxeIh+RjUjLOPIz2hpbOS4ontRA6RHN2pbrwA5WQOVrTPAalVjPQxx/I++NFRVoChLtX+VKayI2ll8vgCjZ84DAFz8/TBqSh++07m+ndv/PQBgUMwEk5u90hGDxsXC0d0D4rpaXDCAn27Vg5+DR401qIHZhiSsdUp8VtJvkMtken9+plTiUrJq6wtzeZ/zi4iGq1cfSJubkfHbz1yXcw/DuHltJl55rB+eDfeESCgAfstTDYAuVTVFD2uAGiorkPTlp5BJJBAIhRBYWEAgUP3OFwohbP1dILRo/dV6TusxoUiEvhFRBvlN69KxPwDG4NV/ELp59OC6HE55DwqDT1gkrmek4eTurzDlNcPZVPDm5UsoycuFQChE5JOPNrPPVAmEFhg+dQZ++c9GnD/4I0Ien8jZ/y1xXS2uX7wAAAge9RgnNRiDvpHRsHNxRWN1Fa6cPaX3dZJu5uWgvrwMltbW8I8ertfn5gqPz0f001Nx+OMPkP7LQURMmmJQt82pAdIja0sB/NzsVX+pzIO8hQ9FQxMgEDx0H6bU1r2KuqJX8ABMXZ3I+TTQuymVCs1PRdpc+dmYjZ4xF0WZ6Sg4fxY3crPhFfzo24jo0rl9qqs//cfEGOy0Vn0KHDYK53/+CZVF13Bu3/cYM2s+J3XknT4OplSih1+AwW89wCW+QICQmIk4/d1OXDxySO8NkHrj04Bho2BhZaXX5+aS/9AROLN3F2pvlSLz6K8YPPlZrkvSoAaIC7JmoOY6WmpUg55Fvr7gP+A/hFwm02wPMTxhJuxdXKGQy6CQy6GQ3fldqZBDLpNBefdxuQwKuQzXMy7gZu4lXD6VbFArxBZnZ+J2VSVEtrbwixrKdTkGwaVXbwwaNwGZR39B8tdfYMZ7/8v5bY3yawUoykwHj8fH4MnPcVqLoeDx+Rg5bTZ+SlyDi78fRvikyZzse6bZ+oIGPz/UoHGxOPvjHtwqyEdZwRV4+Pnr5XklTU24cvY0APO5/aXG5wsQNSUev23djLRD+xA64UmDWTqDGiAuqGeANaoumT/s9tf1jPOqdSOcnBEV91ynpk6e2/c9Tn37NY7v3Abf8MGwstX9nliPIrv1p6KgEWMN5j+FIRgWPx2XTx1DxfVCVdPK8a0N9difwOGj0M3dg9NaDIl3SDi8ggfiRm42zuzdjQmLluj1+SuKrqHyr+sQCIUIGDZSr89tjGwcu8F/6EhcPnkMF38/jAl6aoDyU05ALpXA2dMLPfo9+Gq/KQoaORZnftiN21WVuPTn7wibYBgTKGi0HBfUM8ButzZAD5kBlntCtWpo0MixnV43IvKpp+Hcsxea6utw+rtvOvUY2tbUUI+C1BQAdPvr72wcuyG6dU+dk3t2QCbhbqZR9c0buNr6dYqKi+esDkPE4/EwcvocAEDu8T9RfbNYr8+vHvzsGxEFazt7vT63sVIPhs47cwJNDfV6eU717S9T2eKnowRCIaKmqN47Ug/+CIVc/4PQ20MNEBcqLwMAWqpUGyo+6ApQc+Ntzdifrty6EggtMG7eIgBA5u+/oPxaQacfS1sun0yGUiGHu68f3Lx9uS7H4IRPnAyH7m5orKnGhUP7Oasj9cBegDH4DR4CV68+nNVhqHr0C0C/qGFgTImTe77W2/MqFQpcPnUcAG190REefv5w9/WDQibTNCa6VH2zGLeu5oPH5xvU8AN9GzAmBrZOzmisrkLO8T+5LgcANUDcqMyHvIUPeZ3qp3pRYNB9T80/cxJKhRzdvX3h2tu7S0/be0AIAoaNAmNKJG37FEyp7NLjdQVjDNl//gaArv7cj9DSEiOnzQagGgTfWFuj9xrqK8px+VQyALr68yDDE2aCx+OjMO0sSvIv6+U5izLT0VRfB2sHR3iHmNfioV3B4/E0u8Rf/P0wlEqFTp9PvfKzb/hgk97i52GElpaa2aOpB/ZCqdDtv/ujoAaIC5V5mvV/LL29IbCzve+puSdVnbK2fnIYM3MeLK2tcasgX7PzOhfKCq6g+mYxhJYig9pPydAEDBuFHn4BkElaOLl1ef7nn8CUSvQeGIoefuY3duFRufTyQv8xqk1hT+7eDsaYzp9TPfg5aPhog9mOw1gEDBsJK3sH3K6qxLULXZtd+yAKufzOFj9mNvi5PSExE2Fl74D68jLNxB4uUQOkb7IWoObaIy2AWFtWqtodncdH0IgxWnl6O2cXDIufAQA4uXuH3u6B/5366o//kOEQ2dy/ATR3PB4Po1unV19KPoqKomt6e25xXS0utTbJQ56eqrfnNVbD4qdDaGGJkrwcXM9I0+lztTQ2ojDtLACa/dUZFpYizZXnjN8O6ex5rmekoam+DjaO3eATFqmz5zEWFlZWiGzd5/Dsvu85vQsBUAOkf9UFAFOipUG1GNSDBkCrBz/3CQnT6qXTsAlPontvb7Q03sbJ3Tu09riPStrSjLwzqn2LBprJxqdd4RkQBP+hIwHGcPybL/VydQEA0g7tg0ImQw//QPQykLWIDJm9iytCJ6hurZzcs0Onb+75KSehkMvh2tubxs91UkjMRIDHQ3H2RVSX3NDJc6jXOAse9RhdpWsVGvsERDa2qCm5gavnUzithRogfVNvgVGnWvfnfleAGGO4fErVAGl74BxfIMC4eYsBAJeO/Y7SK/oZs6CWn3ISspZmOPXoCc+g/np9bmM1avpsCIRCFGdfxPWLur26AKgG32ce/RUAEB031SxnrnRGVFw8RLa2qCouwuXTx3X2PDmts7/6j3qMvjad5Ojmjr4RUQBUE0O0TVxXq5nAMqD19igBRDa2CGv9QUE9C5gr1ADpW2UeFFIeZHVyAIBVUPsDoEvzL6O+vAwWVtbwGzxE62V4BgZrxiz8se1TvQ5Iu/SnekroeHrzfkSObh4ImzgZAHB855c6/3pdPHIIspZmdO/tDd/wwTp9LlNibWevWSjy9Hff6GTPqZrSEs2t8UAt3Ro3V+rB0DnH/4C0uUmrj5174k/VCt39AuDSq7dWH9vYhU2cjLjlqzHxpX9zWgc1QPp21wBoC09PCLp1a/c09eBn/+jhsBDpZtn0Uf94AVa2dqgsuoaLvx/WyXP8XfXNGyi9chk8Pp+m7nZQ9NNTYWXvgJqSG8hK+k1nzyNtaUb6rwcBAFFP09Wfjgqf+BRsnZzRUFmOrD9+1frjqwfVeoeEwc7JWeuPb076DAiBU89ekDY3a4YcaANjrM3aP6QtGwdH9I2I4vy9hRogfavIe+gAaLlUivwU1RiZ4FG6WzfCxsERI1qnWZ/+7hu9TLNWzzzzDY8y6ymhnWFla4dh8dMBAGe+/waSJrFOnifr6K9oabyNbh494D/EPDZt1CYLkRWGPjsNAHD2p++0emWBKZV3ZobSDxBdxuPzETr+CQCqKfHaGl9362oeakpvQmgpQsDQUVp5TKJ91ADpk1zSdgbYfQZAX8s4D4lYDDsXV51vhDlw3Hh49O0HaXMTju/cptPnUshlyG2duktr/3TOoHET4NSzF5pvN+Dc/r1af3y5VIq0w/sBAFFT4ju98ri5GzD2cTj16InmhnqkHdqntce9kXtJtXeejS36RkZr7XHNWf/Rj8FCZIXqm8W4kZOtlcdUX/1RzXI1nN3PSVsG0QBt2bIF3t7esLKyQnR0NFJTU+977n//+1+MHDkSTk5OcHJyQkxMzD3nz5kzBzwer82vCRMm6DrGw1UXAkzx0AHQmq0vRozR+SaYfL4AMfP/H8DjIe/0cRRfytLZcxVeSEXz7QbYOjnDJ5QWbusMgVCI0TPmAgDSD+9HfUW5Vh8/53gSxLU1sHNx1enVR1MnEAoxPGEWACDt0H401ddp5XHVW1/4Dx1Be+dpicjGVrPX3kUtTImXtbRoZrnS7S/DxnkD9N1332Hp0qVYs2YN0tPTERISgtjYWFRUVLR7fnJyMqZNm4Zjx44hJSUFXl5eGD9+PEpKStqcN2HCBNy6dUvza8+ePfqI82CVl6GU8yCtV/2zt9cANTXUa9YQ0dey6e6+fgh5fBIAIOnLT3W2T0v2n6rbXwPGxIAvoCsLneUbPhi9BwyCQi7HyT3aW8ZAqVDg/MEfAACDn3oGAqGF1h7bHPkPGQ53Xz/IWppxdt93XX48aUuzZkfx/qPo9pc2hcaqboMVnD+LhqrKLj3WlXOnIWtpRjf3HugVNEAb5REd4bwB+vDDD7FgwQK88MILCA4OxtatW2FjY4Mvv/yy3fN37dqFxYsXIzQ0FIGBgfjiiy+gVCqRlJTU5jyRSAQPDw/NLycnAxhvUpmPljrVWhBCNzcIXV3vOSU/RbX1hZtPX73uuzQiYSasHRxRU3IDFw4f0PrjN1RVoigzHQCtiNpVPB4Po2fOB3g85J85gdIreVp53LwzJ1BfUQ5rewe6RakFPB4PI6fNAQBk/v4r6ivKuvR4BakpkEla0M2jB3oG3H/7HNJxrl594NV/EBhTIuuPI116LPXtr/5jYjgf5EsejNMGSCqV4sKFC4iJubNGAp/PR0xMDFJSHm19gKamJshkMjg7t50NkZycDDc3NwQEBGDRokWorq6+72NIJBI0NDS0+aUTQZPR4qHaT+l+t78un1Cv/fOYbmq4Dys7O82tlZQf96Chqv0rcJ2Vk/wHwBi8+g9CN48eWn1sc+Tm7auZRZe884suD95kSiVSW8cURTwRp7OZh+amz6BQ9B4YCqVCjtPf7+rSY6m3vgimtX90Qr1LfFbSEVSX3OjUQpa1t0pw8/IlgMejWa5GgNMGqKqqCgqFAu7u7m2Ou7u7o6zs0X5aev3119GzZ882TdSECRPw9ddfIykpCe+//z6OHz+OiRMnQnGftVMSExPh6Oio+eXl5dX5UA/iMQAtTapGrb0GqKa0BLcKVLsGBw7X/8yB4FGPwTOwP+QSCY5t/6/WHpcplZoVUQfSPXGtGZEwE0KRCLeu5OHK2VNdeqyCC+dQfbMYltY2CBk/SUsVEgAYNX0OAODyqWRU/nW9U4/RUFWJ4hzV+Dx9/3BkLvpGRsPOxRXNDfXYvnQRtsybhr3rVuHknh24ej4Ft2uqHvoY6ibVOyQc9i73XuEnhsWo1+Zev349vv32WyQnJ8PK6s5PrM8//7zmzwMHDsSgQYPQt29fJCcnY9y4e7vyFStWYOnSpZq/NzQ06KwJasnNBdD+DDD1ys/eIeGcTBHn8XgYN28Rdr7+CgrOp+B6RppW9q/561ImGiorILK1hV/0MC1USgDVvm6Dn3oWKT/sxsnd29E3IhpCS8sOPw5jDKn7vgeg2ibFytZO26WaNXdfP/gPHYkrKSdx9PNP4Bc1FAKhBQQWFhAIhRAIheC3/i4QWrT5s/rjuaeSAcbQK3gAHN3cH/qcpOP4AgHGv/gyzv74LSquF0LSJEbxpUwUX8rUnGPn5AwPP3949FX9cu/rp/n/olQqNA0Q3eY3Dpw2QK6urhAIBCgvbzuTpby8HB4eHg/83I0bN2L9+vX4448/MGjQoAee6+vrC1dXVxQUFLTbAIlEIohEup9RoZRIICkoAHDvFSCmVGpmf+lr8HN7uvf2RvikKbhwaB+SvtqK2f23dHm2yaXWwc9BI8bQzBUtG/zUM8hOOoL6inL8+dVWeA0IgZWNLUS2thDZ2sHK1g4iWzsILe4/oPmv7IsoK7wKoaUI4a2rTRPtGpEwAwWpZ3CrIB+3CvI7/Tg0+Fm3fEIj4BMaAYVcjuqbxSgruIJbBVdQXngFVTeK0Vhbg4LzZ1Fw/qzmc5x69oJH336wsrNDY001rOzsaYkCI8FpA2RpaYmIiAgkJSUhLi4OADQDml966aX7ft6GDRvw7rvv4rfffkNk5MOvUNy8eRPV1dXo0YPbsSeSK1cBuRwCJycI/9bgleTnoqGyHJbW1uirg60vOmLYc9OQf/o46svLcP7ADxgW/49OP1bz7QYUtG54N/CxWG2VSFpZWFlh+POz8NunHyH7z981M+3+TmhhCZGdHUStzZGVrerPVnZ2uJl7CYBqTSgbx256rN58OPXwxBP/Wo6izHQo5Qoo5LLWX3Io5XIo5HIoZOq/q35XKO4+JodTj57wHzqC6yhmQSAUws3bF27evhgUo1pCRdbSgvKiQpQVXFH9unYV9eVlqC29idrSm5rPDRo55oE/cBDDwfktsKVLl2L27NmIjIxEVFQUPvroI4jFYrzwwgsAgFmzZsHT0xOJiYkAgPfffx+rV6/G7t274e3trRkrZGdnBzs7OzQ2NmLt2rV49tln4eHhgcLCQixfvhx+fn6IjeX2G7Dm9ldw8D2DGHNPqq7+9IsezvlVEktrG4yZvQCHPnofqQd+QPDIxzo9cPnyyWNQyFWz2mjXat3oP+ox3K6qROVf19EiboRELIakqVH156YmgDHIZVLIa2sgvs9q33yBEJFPPqPnys2Lf/Rw+EfTytrGysLKCr0C+6NX4J0NnJsa6lF+rUDVEBVegaRJjMgnn+awStIRnDdACQkJqKysxOrVq1FWVobQ0FAcOXJEMzC6uLgY/LsWA/z0008hlUrx3HPPtXmcNWvW4O2334ZAIEBWVhZ27NiBuro69OzZE+PHj8e6dev0cpvrQeRVlYBAcM/tL7lUiispqkGs/UcZxgBH/yEj0GfQ7/grKwNJX23FM2+83eGZJ4wxzRUJuvqjOzw+H0Ofm9bux5hSCUlzEyRi8b3NkVgMibgRLWIxevcfBAfX7nqunBDjZuPgqLltRowPj2lr8xMT0tDQAEdHR9TX18PBwUGrj62USMAkEgjuetz8lFM49NF62Lt2x4L/bNP56s+Pqqa0BF8v+39QyOXoGzkEAqFQdWleLoNSLoNc1nq5vvUyvfqSfpu/y2QQWorwz607aHAtIYQQnerI92/OrwCZG75IBPztSpR6c0N9bH3REc49PRH51LM4t+87FKadffgn3MegmAnU/BBCCDEo1ABxrKmhHkUXLwAwzPU9hj43DfYurpBJWlRTci0s7pqeq5rKyxcKIVRP31VP7W39XWgpgo2DI9cxCCGEkDaoAeJY3ukTUCoUcPftB5deOlqAsQsEQiFCHp/IdRmEEEKIVhnO/RYzdbn19hftvE0IIYToDzVAHKopvYmywquqrS+G6X/rC0IIIcRcUQPEIfXKzz6hEbQAHSGEEKJH1ABxhCmVmtlfwQay9g8hhBBiLqgB4sjNvBzcrqqEpbUNfCOiuC6HEEIIMSvUAHFEffvLf8gIzre+IIQQQswNNUAckEkluHJWtfUFzf4ihBBC9I8aIA4Upp2DtLkJDt3d2mysRwghhBD9oAaIA5dbd34PGjHWoLa+IIQQQswFfffVs6b6OlxXb31Bt78IIYQQTlADpGd5p4+DKZXw8POHc89eXJdDCCGEmCVqgPQst/X2V/BIuvpDCCGEcIUaID2qvnkD5dcKwBcIEEBbXxBCCCGcoQZIj9QrP3uHRsDGwZHjagghhBDzJeS6AHPSNyIK4rpa9IsaynUphBBCiFmjBkiPevoHoad/ENdlEEIIIWaPboERQgghxOxQA0QIIYQQs0MNECGEEELMDjVAhBBCCDE71AARQgghxOxQA0QIIYQQs0MNECGEEELMDjVAhBBCCDE71AARQgghxOxQA0QIIYQQs0MNECGEEELMDjVAhBBCCDE71AARQgghxOxQA0QIIYQQs0MNECGEEELMDjVAhBBCCDE7BtEAbdmyBd7e3rCyskJ0dDRSU1MfeP7evXsRGBgIKysrDBw4EL/88kubjzPGsHr1avTo0QPW1taIiYnB1atXdRmBEEIIIUaE8wbou+++w9KlS7FmzRqkp6cjJCQEsbGxqKioaPf8M2fOYNq0aZg3bx4yMjIQFxeHuLg4XLp0SXPOhg0b8PHHH2Pr1q04d+4cbG1tERsbi5aWFn3FIoQQQogB4zHGGJcFREdHY/Dgwfjkk08AAEqlEl5eXnj55Zfxxhtv3HN+QkICxGIxDh06pDk2ZMgQhIaGYuvWrWCMoWfPnvj3v/+N1157DQBQX18Pd3d3bN++Hc8///xDa2poaICjoyPq6+vh4OCgpaSEEEII0aWOfP8W6qmmdkmlUly4cAErVqzQHOPz+YiJiUFKSkq7n5OSkoKlS5e2ORYbG4v9+/cDAK5fv46ysjLExMRoPu7o6Ijo6GikpKS02wBJJBJIJBLN3+vr6wGo/iEJIYQQYhzU37cf5doOpw1QVVUVFAoF3N3d2xx3d3dHXl5eu59TVlbW7vllZWWaj6uP3e+cv0tMTMTatWvvOe7l5fVoQQghhBBiMG7fvg1HR8cHnsNpA2QoVqxY0eaqklKpRE1NDVxcXMDj8TisTHsaGhrg5eWFGzdumMVtPcpr2iivaaO8hs9Qa2aM4fbt2+jZs+dDz+W0AXJ1dYVAIEB5eXmb4+Xl5fDw8Gj3czw8PB54vvr38vJy9OjRo805oaGh7T6mSCSCSCRqc6xbt24diWI0HBwcDOrFqmuU17RRXtNGeQ2fIdb8sCs/apzOArO0tERERASSkpI0x5RKJZKSkjB06NB2P2fo0KFtzgeAo0ePas738fGBh4dHm3MaGhpw7ty5+z4mIYQQQswL57fAli5ditmzZyMyMhJRUVH46KOPIBaL8cILLwAAZs2aBU9PTyQmJgIA/vWvf2H06NHYtGkTnnjiCXz77bdIS0vD559/DgDg8XhYsmQJ3nnnHfTr1w8+Pj5466230LNnT8TFxXEVkxBCCCEGhPMGKCEhAZWVlVi9ejXKysoQGhqKI0eOaAYxFxcXg8+/c6Fq2LBh2L17N958802sXLkS/fr1w/79+zFgwADNOcuXL4dYLMaLL76Iuro6jBgxAkeOHIGVlZXe8xkKkUiENWvW3HOrz1RRXtNGeU0b5TV8xljz33G+DhAhhBBCiL5xvhI0IYQQQoi+UQNECCGEELNDDRAhhBBCzA41QIQQQggxO9QAEUIIIcTsUANECCGEELNDDRDpkvr6eq5LIESrCgoKsH79eq7LIDpC71nGQR8r9FADRDrt4sWLGDRoEHJycrguRS9KS0tx/vx5HD58GLW1tVyXo3PFxcXYtWsXPv74Y5w/f57rcvQiKysL0dHR+OSTT1BVVcV1OTonkUigVCq5LkNv6D3L8DU2NkImk4HH4+m8CaIGiHRKZmYmhg0bhueffx79+/cHoJ+OnSvqb4zLly9HfHw84uLisGbNGq7L0pns7GwMHz4cX331FdasWYNly5YhIyOD67J0KjMzE0OGDMGUKVPQ3NyMnTt3cl2STuXm5mLWrFk4e/asSf/fVaP3LMN/z7p8+TKefvppfPfdd5BKpbpvghghHZSdnc2sra3ZW2+9pTnW0NDACgoKOKxKd0pKSpi/vz978803WW1tLSstLWUzZsxgAoGAzZs3j+vytC4vL495eHiwVatWsebmZlZSUsJcXV3Zrl27uC5NZzIyMpi1tTV74403GGOMvfzyy2zIkCHs5s2bHFemG9euXWO+vr6Mx+OxqKgolpaWxpRKJddl6Qy9Zxn+e1ZRURELCgpilpaWbMiQIWzv3r1MIpEwxpjOXpvUAJEOqampYZGRkczb21tz7B//+AeLiIhgFhYWbPLkyeynn37isELtO3ToEIuMjGQ1NTWa/4gpKSmse/furG/fvuzFF1/kuELtEYvFbMGCBezFF19kMpmMKRQKxhhj8fHx7H/+53/YmjVrTK4RunbtGuvWrRtbsWKF5tj+/fuZvb09+/333xljTPPvYAokEglbu3Yti4+PZzk5OSwoKIgNGjSoTRNkSs0QvWcZ/nuWXC5nmzZtYk899RS7ePEimzBhAgsLC9N5E0S3wEiH8Pl8TJkyBS4uLli8eDEee+wx1NXVYeHChTh48CBqa2vx4Ycf4tixY1yXqjX19fWora1FS0sLeDweAEChUMDf3x/PPfcczp49i9OnT3NcpXYIBAJMmTIFixcvhlAoBJ/Px7p16/DDDz/gypUrSEpKwvvvv48lS5ZwXarWCIVCfPzxx3jvvfc0x6ZMmYJx48Zh7dq1aG5ubrMhs7Hj8/mIjo7Gc889h+DgYGRlZUEmk2Hu3LlIT0+HUqnUvM5NAb1nGf57lkAgwGOPPYZZs2YhJCQEhw8fhru7O9577z0cPHgQEolEN7fDtN5SEZNXXV3NNm7cyPr06cPGjBnDysrKNB8rLy9nfn5+7OWXX+awQu3Ky8tjNjY27F//+hc7efIkS01NZQ4ODuzdd99ljDHm4+PD1q9fz3GVXaf+CUv9ExdjqlsHdnZ27MCBA5pjK1euZOHh4W2+7sZKLpffc0z97/D1118zX19fdu7cOcaYaV0Famlpuefvd18JYkz175CcnMxFeVpXU1Njlu9Zr7zyitG8Z0ml0jZ/l0gkba4EqT++f/9+rT2nULvtFDFFdXV1qK6uhoODA2xsbODs7IxZs2bBwcEBvXv3hpubGwDVTxhubm6Ijo7G9evXOa668+7Oa21tjYCAAPz000+YOXMm9u/fD7FYjAULFmDlypUAgICAAJSUlHBcdefJ5XIIhULNT4qWlpaajw0YMABXr16Fh4cHlEol+Hw++vbti5aWFohEIq5K7jJ1ZoFAcM/H1P8O06ZNw7p167BlyxZERUUZ9VWgpqYmNDU1wdraGlZWVm2+dnK5HCKRCOnp6QgPD8fcuXPx2WefYceOHUhJScHRo0fRvXt3DqvvuLvzikQiODk5Yc6cOXB0dISXl5fJvWfdndfS0hIBAQHYv38/ZsyYgQMHDhjke1ZVVRVu3LgBGxsbuLm5wcnJSfMeI5fLYWlpif379yMuLg7vvfceFAoFjh07hoMHD2Lw4MHo2bNnl2ugBog8UFZWFmbOnImmpiYolUqEh4dj7dq1CA4OxvPPPw+RSKT5hiEQCKBUKtHY2IiQkBCOK++cv+cNCwvD2rVrERsbi7S0NNTX10OhUCA0NBQA0NLSAolEgn79+gFQzSoxptsHV69exbZt2zBv3jxNhr9zd3cHAE0DkJmZieDgYKNtgB4ls0KhgFAoxPLly/HBBx/g/PnzGDx4sJ4r1Y6cnBwsWbIEZWVlAIAFCxbghRdegL29PQDVLUCZTAYrKytkZGRg8ODBGDlyJCwsLHDq1Cmja37+nnf+/PmYPXs2XFxc8I9//KNNs28K71n3y/v4448jPT0dtbW1kMvlBvWelZWVhfj4eCgUCkgkEri7u+OTTz7BkCFDAKhek+rG/MCBA3j66acxc+ZMWFpa4sSJE1ppfgDQLTByfzdu3GAeHh7s1VdfZWfPnmWbN29msbGxzNHRkZ05c4Yx1va2gFwuZ6tWrWKenp7sypUrXJXdaQ/Ke/LkyXvOr66uZitXrmTu7u6ssLCQg4q7pqCggLm5uTEHBwe2ZMmSh86IEYvFbOXKlax79+7s0qVLeqpSuzqaOT8/n4lEIrZp0yY9Vahdubm5rHv37uzll19m+/btYwsWLGBBQUEsNTX1nnNlMhljjLGFCxcyFxcXlpOTo+9yu+x+edW3Mf/O2N+z2ssbGBh437yG8J5169Yt1rt3b7Z8+XKWn5/P9u3bx55//nlmYWHB9uzZ0+Zc9S3qRYsWMWdnZ62/71ADRO4rKSmJRUREsOrqas2xgoICNm3aNGZjY8PS09MZY6qxArt372bPPPMM8/Dw0Bw3Ng/Ka21trcmlUChYdnY2W7ZsGXNzczPKvI2NjWz69Ols2rRpbO3atSwsLIy99NJL920IDh48yGbPns169+5tlHkZ63hmtY0bNxplw1dTU8PGjx/PFi9e3OZ4eHg4W7hwYbufs2nTJsbj8Yzya9zRvHv27DHq96yO5r106ZJBvGdlZGSwAQMGsOvXr2uONTU1sddee41ZWlqyQ4cOMcbu/HC9ZcsWnb0m6RYYua+6ujpcvHgRMplMc6xv377YuHEjZDIZ4uPjcezYMXh5eWHo0KE4d+4ckpOTERAQwGHVndeRvH369MHjjz+OxYsXw9vbm7uiO0kkEmH06NGwsbHBjBkz4OzsjC+//BIAsGTJEvTt27fN+eHh4SgsLMRbb711z8eMRUczq8cj/Pvf/+ai3C4rKSmBg4MDEhISAABSqRSWlpYYN24cqqur7zlfqVRizJgxyM/Pv++tQUPW0bzR0dFISUkx2vesjubt3bs3YmJiOH/Pqq+vR05OjmZGl1KphLW1NTZs2IDm5mZMnz4daWlpmtdgQkICJkyYAF9fX+0Xo/WWipiMW7dusaioKLZixQrW0NDQ5mMpKSksMjKSffPNN5pj7c2oMSYdzWvsmpub26ytsXnzZs1VEfXlcYlEwsrLyxljpjEL6lEyS6VSVllZyVWJWqNUKtkPP/yg+bv665eYmMimTp3a5tzGxka91qYLHcmr/v9tzO9ZHcl7+/Ztvdb2IHK5nI0aNYolJCRorrara7958yYbNWoUW7t2LVMqlTp/zzHeaQ1E5zw8PDB69Gj89ttv+Omnn9DS0qL52JAhQ6BQKNqsJdHejBpj0tG8xs7Kygo8Hg8KhQIA8Morr2DOnDk4ffo0/vd//xd5eXlYvnw5Jk+erFmW3tg9SuZly5bhySefhFQqNdqtEtRr+Tz77LMAVANd1YPYxWIxKisrNedu2LABa9as0fybGKOO5l27di3kcrnRzuzraN63334bCoXCIF7PAoEACQkJKCoqwscff4yGhgZN7Z6enrCzs0NeXh54PJ7Ovz50C4y0S335f/369Zg6dSo++OADNDc3Y86cObCysgIA+Pj4aG80PsfMLS9wZ/aHQCCATCaDhYUFXnnlFQDAzp078csvv6CiogLHjh1rMzXemJlLZvU3DnVeHo+nmfpvb28PR0dHAMBbb72Fd999FxcvXjTqH2A6k1coNN5vf8b69VXXu2jRIhQWFuLAgQNobm7GqlWr4ODgAABwcXGBk5MTFAoF+Hy+Tn/w4jFDaAkJp9Tf/O+mUCja/IeZO3cuMjMz4eLigvHjxyMvLw/ff/89UlNTERgYqO+Su4Ty3snb2NgIOzu7e84bMmQIrly5guPHj2PgwIF6r7mrzC3zo+YFgM2bNyMrKwt9+vRBYmIiTp06hYiICH2X3CWU1zjzqmtW51m3bh0OHz6Muro6TJ48GTdu3MChQ4dw9uxZzYa1OqXTG2zE4F2+fJl9+OGHbY6pp8MWFRWxUaNGsaysLKZUKtmOHTvY9OnTWXR0NIuLi2OZmZlclNwllLdt3nHjxrWZ4i+VStn8+fMZj8djWVlZeq1VW8wtc0fzvvvuu4zH4zFbW1vNqs/GhPIafl65XH7Pys531xwcHMyOHTvGGGMsOTmZvfzyy2zChAls9uzZLDs7W291UgNkxrKysphIJGI8Ho+dPXu2zccKCwuZl5eXZlPMu7W0tNzz4jYGlPeOu/P+fZPBrVu3trtOjDEwt8yPmvdu27ZtY97e3iw3N1efpWoF5b3DUPPm5eWxhQsXsscff5y9/fbbbZYVKSoqYp6enuyf//znPe+z+hj0/HfUAJmpixcvMisrKzZr1iw2ZswY9uabbzLG7nTp48ePZ9OnTzeZXaEp78PzGnt2c8vc2de0UqlkpaWleq+3qyiv4efNzs5mrq6ubOrUqWzx4sXMwsKCJSYmaj4+Z84cNn/+fIP5P0gNkBlKT09n9vb2bNWqVYwxxpYtW8a6d+/O6urqNOdIJBKj/uZwN8pr2nkZM7/Mnc1rrEsZUF7Dz1tbW8uGDBnCVqxYoTm2evVqtnTpUk3TZmjLDlADZGbKy8uZtbU1e+211zTHiouLWUBAAFu7di1jzPBepF1BeU07L2Pml5nyUl5DzFtaWspCQkLYr7/+qjn2wgsvsBEjRrDw8HC2YMEC9ssvv3BY4b1oFpiZqa2tRXZ2NkaNGqU5JpVKMXv2bNy4cQOnTp0CYHybet4P5TXtvID5Zaa8lBcwvLx//fUXgoODsXTpUsTHx+PgwYN477338MYbb8DJyQk7d+6Em5sbvvjiC3h4eHBdrgpnrRcxCOpLppcuXWIikYht27aN44p0i/Kadl7GzC8z5aW8hmL79u3MxsaGTZo0idnb27dZqTo7O5vxeDx28OBBDitsyziXwSQdUlpaivPnz+PIkSOQy+VQKpUA7qwtwRiDj48PnnzySfz6669oaWkxiBVDO4vymnZewPwyU17Ka2h5765ZJpNBLpdj9uzZyM/Px2effYaAgACEhoZCqVRCoVCgW7duCAsLg729Pad1t8FF10X0JzMzk3l5ebHg4GAmFApZWFgY+/TTTzV7w9w9aG7Xrl1MJBIZ5XRgNcpr2nkZM7/MlJfyqhlK3vZq3rJli2aPtWvXrjFXV1f2xx9/aD5nzZo1zM/Pj5WUlHBV9j2oATJhlZWVLCgoiL3++uvs+vXrrKKigk2bNo1FR0ezJUuWtLshYFhYGJs5cyZTKBRGN2OG8pp2XsbMLzPlpbyMGVbeh9Wsnqm2cOFCJhQK2aRJk9jEiROZu7s7y8jI0Hu9D0INkAnLzs5m3t7ebVYwlkgkbPXq1SwqKoqtWrWKNTc3t/mczZs3s6tXr+q7VK2gvKadlzHzy0x5Ka+h5X2UmqVSKaupqWFbtmxh8fHxbOXKlSw/P5+zmu+HGiATlp+fz3x8fNjPP//MGLuzgJZMJmPLli1joaGh7MSJE20+Zswor2nnZcz8MlNeymtoeR9Wc0hICDt16pTmfEO+KkcNkAlraWlhkZGR7Mknn9RcQlW/WJVKJRs4cCCbNWsWlyVqFeU17byMmV9mykt5DS3vo9Q8c+ZMLkt8ZDQLzEQplUqIRCJ89dVXOHHiBBYtWgQAEAqFmvUjJk+ejIqKCo4r1Q7Ka9p5AfPLTHkpr6HlfdSaKysrOa700VADZKL4fD4UCgUGDBiAHTt2YM+ePZg1axbKy8s151y/fh1OTk5QKBQcVqodlNe08wLml5nyUl5Dy2uMNT8IrQRtItTrRajJ5XIIhUI0NjZCIpHg4sWLmD59Ovr06QNnZ2e4uLjgwIEDSElJwcCBAzmsvHMor2nnBcwvM+WlvIaW1xhr7gi6AmTkqqqqANzpzAFAoVBAKBSiqKgI/v7+OH/+PMaNG4ecnBxMmjQJnp6ecHNzQ2pqqlG8SO9GeU07L2B+mSkv5TW0vMZYc6dwNfiIdF1+fj6zt7dnCxYs0BxTD0orLi5mrq6ubN68eUypVGqOq0fkG+MuyZTXtPMyZn6ZKS/lNbS8xlhzZ9EVICOWm5sLa2trZGdn45///CcAQCAQQCqV4uDBg5g5cyY+++wz8Hg8CASCNp9rSJvoPSrKa9p5AfPLTHkpr6HlNcaaO4saICMmEonQrVs3xMXFISUlBQsXLgQAWFpaYsqUKfjwww/v+wI1thcqQHlNPS9gfpkpL+U1tLzGWHNnCbkugHTewIEDERERgfnz58PS0hLbt2/H0qVLUV9fj6ioKMydOxcWFhZcl6k1lNe08wLml5nyUl5Dy2uMNXca1/fgSOeJxWI2aNAglpGRwcRiMfv888+Zi4sL4/F4LCsrizHWdg8ZY0d5TTsvY+aXmfJSXkPLa4w1dxbdAjNSMpkMIpEIHh4eaGxshI2NDZKSkiCTyeDn54cvvvgCAO65VGmsKK9p5wXMLzPlpbyAYeU1xpq7gm6BGYHS0lKkp6dDKpXC29sb4eHhmkuQERERKCgowOeff44TJ07g559/RnZ2NtavXw+hUIhNmzZxXH3HUV7TzguYX2bKS3kNLa8x1qx1XF+CIg+WlZXFfH19WVRUFHN1dWWRkZFs7969mo+//fbbjMfjMR8fH3bhwgXGGGO1tbXs//7v/1hhYSFXZXca5TXtvIyZX2bKS3kNLa8x1qwL1AAZsIKCAtarVy+2fPlyVldXx9LS0tjs2bPZ3Llz2+zAu3jxYpaamsoYM971GBijvKaelzHzy0x5Ka+h5TXGmnWFGiADJZFI2NKlS9nUqVOZRCLRHN+2bRtzcXFhVVVVHFanfZRXxVTzMmZ+mSmvCuU1HMZYsy7RGCADpVQq0atXLwQFBcHS0lKz0+6wYcNgZ2cHmUzW7ufcvW+LMaG8pp0XML/MlJfytvc5XOY1xpp1iRogA2VlZYW4uDj4+Pi0Od6tWzdYWFi0eaFmZGQgLCzMqF+klFfFVPMC5peZ8qpQXsPJa4w165LpJjNCt27dQmpqKo4cOQKlUql5kSoUCs0Km/X19aitrdV8zurVqzFu3DhUV1eDMcZJ3Z1FeU07L2B+mSkv5TW0vMZYs97o/64baU9mZibr06cP8/f3Z46OjiwwMJDt3r2bVVdXM8buDELLz89n3bt3ZzU1NWzdunXM2tqapaWlcVl6p1Be087LmPllpryUlzHDymuMNesTNUAGoKKiggUGBrKVK1eywsJCVlJSwhISElhQUBBbs2YNq6io0JxbXl7OwsLCWEJCArO0tDTKFynlNe28jJlfZspLedUMJa8x1qxv1AAZgJycHObt7X3Pi+71119nAwcOZBs2bGBisZgxxlhubi7j8XjM2tqaZWRkcFBt11FeFVPNy5j5Zaa8KpTXcPIaY836RmOADIBMJoNcLkdTUxMAoLm5GQCwfv16jB07Fp9++ikKCgoAAE5OTli8eDHS09MRGhrKVcldQnlNOy9gfpkpL+U1tLzGWLO+8Rgz5RFOxiMqKgp2dnb4888/AQASiQQikQgAMHjwYPj5+WHPnj0AgJaWFlhZWXFWqzZQXtPOC5hfZspLeQ0trzHWrE90BYgDYrEYt2/fRkNDg+bYZ599hpycHEyfPh0AIBKJIJfLAQCjRo2CWCzWnGtsL1LKa9p5AfPLTHkpr6HlNcaauUYNkJ7l5ubimWeewejRoxEUFIRdu3YBAIKCgrB582YcPXoU8fHxkMlkmvUXKioqYGtrC7lcbnRTEimvaecFzC8z5aW8hpbXGGs2CFwNPjJHOTk5zMXFhb366qts165dbOnSpczCwoKlp6czxhgTi8Xs4MGDrFevXiwwMJDFxcWxqVOnMltbW5adnc1x9R1HeU07L2Pml5nyUl5Dy2uMNRsKGgOkJzU1NZg2bRoCAwOxefNmzfGxY8di4MCB+PjjjzXHbt++jXfeeQc1NTWwsrLCokWLEBwczEXZnUZ5VUw1L2B+mSmvCuU1nLzGWLMhoa0w9EQmk6Gurg7PPfccgDv7q/j4+KCmpgYAwFTLEsDe3h7vv/9+m/OMDeU17byA+WWmvJTX0PIaY82GhP4F9MTd3R3ffPMNRo4cCUC1DDkAeHp6al6IPB4PfD6/zSA29VLlxobymnZewPwyU17Ka2h5jbFmQ0INkB7169cPgKr7trCwAKDqzisqKjTnJCYm4osvvtCM1DfmFyrlNe28gPllpryU19DyGmPNhoJugXGAz+eDMaZ5Eao79dWrV+Odd95BRkYGhELT+dJQXtPOC5hfZspLeQ0trzHWzDW6AsQR9dhzoVAILy8vbNy4ERs2bEBaWhpCQkI4rk77KK9p5wXMLzPlpbyGxhhr5hK1gxxRd+cWFhb473//CwcHB5w6dQrh4eEcV6YblNe08wLml5nyUl5DY4w1c4muAHEsNjYWAHDmzBlERkZyXI3uUV7TZ26ZKa9pM8a8xlgzF2gdIAMgFotha2vLdRl6Q3lNn7llprymzRjzGmPN+kYNECGEEELMDt0CI4QQQojZoQaIEEIIIWaHGiBCCCGEmB1qgAghhBBidqgBIoQQQojZoQaIEEIIIWaHGiBCiEkZM2YMlixZwnUZhBADRw0QIcRsJScng8fjoa6ujutSCCF6Rg0QIYQQQswONUCEEKMlFosxa9Ys2NnZoUePHti0aVObj+/cuRORkZGwt7eHh4cHpk+fjoqKCgBAUVERxo4dCwBwcnICj8fDnDlzAABKpRKJiYnw8fGBtbU1QkJC8MMPP+g1GyFEt6gBIoQYrWXLluH48eM4cOAAfv/9dyQnJyM9PV3zcZlMhnXr1iEzMxP79+9HUVGRpsnx8vLCjz/+CADIz8/HrVu3sHnzZgBAYmIivv76a2zduhU5OTl49dVXMWPGDBw/flzvGQkhukF7gRFCjFJjYyNcXFzwzTffID4+HgBQU1ODXr164cUXX8RHH310z+ekpaVh8ODBuH37Nuzs7JCcnIyxY8eitrYW3bp1AwBIJBI4Ozvjjz/+wNChQzWfO3/+fDQ1NWH37t36iEcI0TEh1wUQQkhnFBYWQiqVIjo6WnPM2dkZAQEBmr9fuHABb7/9NjIzM1FbWwulUgkAKC4uRnBwcLuPW1BQgKamJjz++ONtjkulUoSFhekgCSGEC9QAEUJMklgsRmxsLGJjY7Fr1y50794dxcXFiI2NhVQqve/nNTY2AgAOHz4MT0/PNh8TiUQ6rZkQoj/UABFCjFLfvn1hYWGBc+fOoXfv3gCA2tpaXLlyBaNHj0ZeXh6qq6uxfv16eHl5AVDdArubpaUlAEChUGiOBQcHQyQSobi4GKNHj9ZTGkKIvlEDRAgxSnZ2dpg3bx6WLVsGFxcXuLm5YdWqVeDzVXM7evfuDUtLS/znP//BwoULcenSJaxbt67NY/Tp0wc8Hg+HDh3CpEmTYG1tDXt7e7z22mt49dVXoVQqMWLECNTX1+P06dNwcHDA7NmzuYhLCNEymgVGCDFaH3zwAUaOHImnnnoKMTExGDFiBCIiIgAA3bt3x/bt27F3714EBwdj/fr12LhxY5vP9/T0xNq1a/HGG2/A3d0dL730EgBg3bp1eOutt5CYmIigoCBMmDABhw8fho+Pj94zEkJ0g2aBEUIIIcTs0BUgQgghhJgdaoAIIYQQYnaoASKEEEKI2aEGiBBCCCFmhxogQgghhJgdaoAIIYQQYnaoASKEEEKI2aEGiBBCCCFmhxogQgghhJgdaoAIIYQQYnaoASKEEEKI2fn/KsWBNintTJUAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pd_timeseries = pd_df.unstack()\n", + "pd_timeseries.plot.line(rot=45, ylabel=\"daily downloads\", ylim=(0, 2e7))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From ca26fe5f9edec519788c276a09eaff33ecd87434 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 5 Aug 2024 10:37:27 -0700 Subject: [PATCH 740/921] feat: Allow windowing in 'partial' ordering mode (#861) --- bigframes/core/__init__.py | 26 +++++++++-- bigframes/core/blocks.py | 4 ++ bigframes/core/compile/compiled.py | 2 +- bigframes/core/groupby/__init__.py | 38 ++++++++-------- bigframes/core/indexes/base.py | 8 ++-- bigframes/core/nodes.py | 42 +++++++++++++++++ bigframes/core/validations.py | 17 ++++++- bigframes/dataframe.py | 54 +++++++++++----------- bigframes/exceptions.py | 4 ++ bigframes/series.py | 52 ++++++++++----------- bigframes/session/__init__.py | 5 ++ tests/system/small/test_dataframe.py | 2 +- tests/system/small/test_unordered.py | 68 +++++++++++++++------------- 13 files changed, 205 insertions(+), 117 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index aa66129572c..2e9b5fa994a 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -194,8 +194,17 @@ def promote_offsets(self, col_id: str) -> ArrayValue: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. """ - if self.node.order_ambiguous and not self.session._strictly_ordered: - raise ValueError("Generating offsets not supported in unordered mode") + if self.node.order_ambiguous and not (self.session._strictly_ordered): + if not self.session._allows_ambiguity: + raise ValueError( + "Generating offsets not supported in partial ordering mode" + ) + else: + warnings.warn( + "Window ordering may be ambiguous, this can cause unstable results.", + bigframes.exceptions.AmbiguousWindowWarning, + ) + return ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)) def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: @@ -347,9 +356,16 @@ def project_window_op( # TODO: Support non-deterministic windowing if window_spec.row_bounded or not op.order_independent: if self.node.order_ambiguous and not self.session._strictly_ordered: - raise ValueError( - "Order-dependent windowed ops not supported in unordered mode" - ) + if not self.session._allows_ambiguity: + raise ValueError( + "Generating offsets not supported in partial ordering mode" + ) + else: + warnings.warn( + "Window ordering may be ambiguous, this can cause unstable results.", + bigframes.exceptions.AmbiguousWindowWarning, + ) + return ArrayValue( nodes.WindowOpNode( child=self.node, diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 1b7b2314032..65a89b4516f 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -280,6 +280,10 @@ def index_name_to_col_id(self) -> typing.Mapping[Label, typing.Sequence[str]]: mapping[label] = (*mapping.get(label, ()), id) return mapping + @property + def explicitly_ordered(self) -> bool: + return self.expr.node.explicitly_ordered + def cols_matching_label(self, partial_label: Label) -> typing.Sequence[str]: """ Unlike label_to_col_id, this works with partial labels for multi-index. diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index c822dd331cf..538789f9d74 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -263,7 +263,7 @@ def to_sql( ordered: bool = False, ) -> str: if offset_column or ordered: - raise ValueError("Cannot produce sorted sql in unordered mode") + raise ValueError("Cannot produce sorted sql in partial ordering mode") sql = ibis_bigquery.Backend().compile( self._to_ibis_expr( col_id_overrides=col_id_overrides, diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 02bf201ca0c..2b80d0389e0 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -109,7 +109,7 @@ def __getitem__( dropna=self._dropna, ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def head(self, n: int = 5) -> df.DataFrame: block = self._block if self._dropna: @@ -235,25 +235,25 @@ def count(self) -> df.DataFrame: def nunique(self) -> df.DataFrame: return self._aggregate_all(agg_ops.nunique_op) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumsum(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: if not numeric_only: self._raise_on_non_numeric("cumsum") return self._apply_window_op(agg_ops.sum_op, numeric_only=True) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummin(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.min_op, numeric_only=numeric_only) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummax(self, *args, numeric_only: bool = False, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.max_op, numeric_only=numeric_only) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumprod(self, *args, **kwargs) -> df.DataFrame: return self._apply_window_op(agg_ops.product_op, numeric_only=True) - @validations.requires_strict_ordering() + @validations.requires_ordering() def shift(self, periods=1) -> series.Series: window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), @@ -262,7 +262,7 @@ def shift(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def diff(self, periods=1) -> series.Series: window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), @@ -271,7 +271,7 @@ def diff(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.DiffOp(periods), window=window) - @validations.requires_strict_ordering() + @validations.requires_ordering() 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( @@ -287,7 +287,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: block, window_spec, self._selected_cols, drop_null_groups=self._dropna ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = window_specs.cumulative_rows( grouping_keys=tuple(self._by_col_ids), @@ -532,7 +532,7 @@ def __init__( def _session(self) -> core.Session: return self._block.session - @validations.requires_strict_ordering() + @validations.requires_ordering() def head(self, n: int = 5) -> series.Series: block = self._block if self._dropna: @@ -650,31 +650,31 @@ def agg(self, func=None) -> typing.Union[df.DataFrame, series.Series]: aggregate = agg - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumsum(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.sum_op, ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumprod(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.product_op, ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummax(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.max_op, ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummin(self, *args, **kwargs) -> series.Series: return self._apply_window_op( agg_ops.min_op, ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumcount(self, *args, **kwargs) -> series.Series: return ( self._apply_window_op( @@ -684,7 +684,7 @@ def cumcount(self, *args, **kwargs) -> series.Series: - 1 ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def shift(self, periods=1) -> series.Series: """Shift index by desired number of periods.""" window = window_specs.rows( @@ -694,7 +694,7 @@ def shift(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def diff(self, periods=1) -> series.Series: window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), @@ -703,7 +703,7 @@ def diff(self, periods=1) -> series.Series: ) return self._apply_window_op(agg_ops.DiffOp(periods), window=window) - @validations.requires_strict_ordering() + @validations.requires_ordering() 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( @@ -723,7 +723,7 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: is_series=True, ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def expanding(self, min_periods: int = 1) -> windows.Window: window_spec = window_specs.cumulative_rows( grouping_keys=tuple(self._by_col_ids), diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 8b039707c21..0376e37f960 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -184,7 +184,7 @@ def empty(self) -> bool: return self.shape[0] == 0 @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def is_monotonic_increasing(self) -> bool: """ Return a boolean if the values are equal or increasing. @@ -198,7 +198,7 @@ def is_monotonic_increasing(self) -> bool: ) @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def is_monotonic_decreasing(self) -> bool: """ Return a boolean if the values are equal or decreasing. @@ -348,7 +348,7 @@ def max(self) -> typing.Any: def min(self) -> typing.Any: return self._apply_aggregation(agg_ops.min_op) - @validations.requires_strict_ordering() + @validations.requires_ordering() def argmax(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( @@ -361,7 +361,7 @@ def argmax(self) -> int: return typing.cast(int, series.Series(block.select_column(row_nums)).iloc[0]) - @validations.requires_strict_ordering() + @validations.requires_ordering() def argmin(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index a979e079728..30edc7740a6 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -135,6 +135,14 @@ def order_ambiguous(self) -> bool: """ ... + @property + @abc.abstractmethod + def explicitly_ordered(self) -> bool: + """ + Whether row ordering is potentially ambiguous. For example, ReadTable (without a primary key) could be ordered in different ways. + """ + ... + @functools.cached_property def total_variables(self) -> int: return self.variables_introduced + sum( @@ -180,6 +188,10 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: def schema(self) -> schemata.ArraySchema: return self.child.schema + @property + def explicitly_ordered(self) -> bool: + return self.child.explicitly_ordered + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -212,6 +224,10 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: def order_ambiguous(self) -> bool: return True + @property + def explicitly_ordered(self) -> bool: + return False + def __hash__(self): return self._node_hash @@ -267,6 +283,10 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: def order_ambiguous(self) -> bool: return any(child.order_ambiguous for child in self.children) + @property + def explicitly_ordered(self) -> bool: + return all(child.explicitly_ordered for child in self.children) + def __hash__(self): return self._node_hash @@ -317,6 +337,10 @@ def variables_introduced(self) -> int: def order_ambiguous(self) -> bool: return False + @property + def explicitly_ordered(self) -> bool: + return True + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -378,6 +402,10 @@ def relation_ops_created(self) -> int: def order_ambiguous(self) -> bool: return len(self.total_order_cols) == 0 + @property + def explicitly_ordered(self) -> bool: + return len(self.total_order_cols) > 0 + @functools.cached_property def variables_introduced(self) -> int: return len(self.schema.items) + 1 @@ -449,6 +477,12 @@ def hidden_columns(self) -> typing.Tuple[str, ...]: def order_ambiguous(self) -> bool: return not isinstance(self.ordering, orderings.TotalOrdering) + @property + def explicitly_ordered(self) -> bool: + return (self.ordering is not None) and len( + self.ordering.all_ordering_columns + ) > 0 + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -523,6 +557,10 @@ def relation_ops_created(self) -> int: # Doesnt directly create any relational operations return 0 + @property + def explicitly_ordered(self) -> bool: + return True + @dataclass(frozen=True) class ReversedNode(UnaryNode): @@ -636,6 +674,10 @@ def variables_introduced(self) -> int: def order_ambiguous(self) -> bool: return False + @property + def explicitly_ordered(self) -> bool: + return True + @dataclass(frozen=True) class WindowOpNode(UnaryNode): diff --git a/bigframes/core/validations.py b/bigframes/core/validations.py index c5761f4e09d..9c03ddb9309 100644 --- a/bigframes/core/validations.py +++ b/bigframes/core/validations.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from bigframes import Session + from bigframes.core.blocks import Block class HasSession(Protocol): @@ -31,8 +32,12 @@ class HasSession(Protocol): def _session(self) -> Session: ... + @property + def _block(self) -> Block: + ... + -def requires_strict_ordering(suggestion: Optional[str] = None): +def requires_ordering(suggestion: Optional[str] = None): def decorator(meth): @functools.wraps(meth) def guarded_meth(object: HasSession, *args, **kwargs): @@ -47,8 +52,16 @@ def guarded_meth(object: HasSession, *args, **kwargs): def enforce_ordered( object: HasSession, opname: str, suggestion: Optional[str] = None ) -> None: - if not object._session._strictly_ordered: + session = object._session + if session._strictly_ordered or not object._block.expr.node.order_ambiguous: + # No ambiguity for how to calculate ordering, so no error or warning + return None + if not session._allows_ambiguity: suggestion_substr = suggestion + " " if suggestion else "" raise bigframes.exceptions.OrderRequiredError( f"Op {opname} not supported when strict ordering is disabled. {suggestion_substr}{bigframes.constants.FEEDBACK_LINK}" ) + if not object._block.explicitly_ordered: + raise bigframes.exceptions.OrderRequiredError( + f"Op {opname} requires an ordering. Use .sort_values or .sort_index to provide an ordering. {bigframes.constants.FEEDBACK_LINK}" + ) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9d3b153d3a2..649b097e927 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -282,12 +282,12 @@ def loc(self) -> indexers.LocDataFrameIndexer: return indexers.LocDataFrameIndexer(self) @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def iloc(self) -> indexers.ILocDataFrameIndexer: return indexers.ILocDataFrameIndexer(self) @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def iat(self) -> indexers.IatDataFrameIndexer: return indexers.IatDataFrameIndexer(self) @@ -344,12 +344,12 @@ def _has_index(self) -> bool: return len(self._block.index_columns) > 0 @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def T(self) -> DataFrame: return DataFrame(self._get_block().transpose()) @requires_index - @validations.requires_strict_ordering() + @validations.requires_ordering() def transpose(self) -> DataFrame: return self.T @@ -1296,11 +1296,11 @@ def _compute_dry_run(self) -> bigquery.QueryJob: def copy(self) -> DataFrame: return DataFrame(self._block) - @validations.requires_strict_ordering(bigframes.constants.SUGGEST_PEEK_PREVIEW) + @validations.requires_ordering(bigframes.constants.SUGGEST_PEEK_PREVIEW) def head(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[:n]) - @validations.requires_strict_ordering() + @validations.requires_ordering() def tail(self, n: int = 5) -> DataFrame: return typing.cast(DataFrame, self.iloc[-n:]) @@ -1540,7 +1540,7 @@ def rename_axis( labels = [mapper] return DataFrame(self._block.with_index_labels(labels)) - @validations.requires_strict_ordering() + @validations.requires_ordering() def equals(self, other: typing.Union[bigframes.series.Series, DataFrame]) -> bool: # Must be same object type, same column dtypes, and same label values if not isinstance(other, DataFrame): @@ -1938,7 +1938,7 @@ def _reindex_columns(self, columns): def reindex_like(self, other: DataFrame, *, validate: typing.Optional[bool] = None): return self.reindex(index=other.index, columns=other.columns, validate=validate) - @validations.requires_strict_ordering() + @validations.requires_ordering() @requires_index def interpolate(self, method: str = "linear") -> DataFrame: if method == "pad": @@ -1964,12 +1964,12 @@ def replace( lambda x: x.replace(to_replace=to_replace, value=value, regex=regex) ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> DataFrame: window = window_spec.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> DataFrame: window = window_spec.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) @@ -2235,16 +2235,16 @@ def agg( aggregate.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.agg) @requires_index - @validations.requires_strict_ordering() + @validations.requires_ordering() def idxmin(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmin(self._block)) @requires_index - @validations.requires_strict_ordering() + @validations.requires_ordering() def idxmax(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmax(self._block)) - @validations.requires_strict_ordering() + @validations.requires_ordering() def melt( self, id_vars: typing.Optional[typing.Iterable[typing.Hashable]] = None, @@ -2349,7 +2349,7 @@ def _pivot( return DataFrame(pivot_block) @requires_index - @validations.requires_strict_ordering() + @validations.requires_ordering() def pivot( self, *, @@ -2364,7 +2364,7 @@ def pivot( return self._pivot(columns=columns, index=index, values=values) @requires_index - @validations.requires_strict_ordering() + @validations.requires_ordering() def pivot_table( self, values: typing.Optional[ @@ -2464,7 +2464,7 @@ def _stack_multi(self, level: LevelsType = -1): return DataFrame(block) @requires_index - @validations.requires_strict_ordering() + @validations.requires_ordering() def unstack(self, level: LevelsType = -1): if not utils.is_list_like(level): level = [level] @@ -2675,7 +2675,7 @@ def _perform_join_by_index( block, _ = self._block.join(other._block, how=how, block_identity_join=True) return DataFrame(block) - @validations.requires_strict_ordering() + @validations.requires_ordering() 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 = window_spec.rows( @@ -2685,7 +2685,7 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window self._block, window_def, self._block.value_columns ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: window = window_spec.cumulative_rows(min_periods=min_periods) return bigframes.core.window.Window( @@ -2788,7 +2788,7 @@ def notna(self) -> DataFrame: notnull = notna notnull.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.notna) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumsum(self): is_numeric_types = [ (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) @@ -2801,7 +2801,7 @@ def cumsum(self): window_spec.cumulative_rows(), ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumprod(self) -> DataFrame: is_numeric_types = [ (dtype in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) @@ -2814,21 +2814,21 @@ def cumprod(self) -> DataFrame: window_spec.cumulative_rows(), ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummin(self) -> DataFrame: return self._apply_window_op( agg_ops.min_op, window_spec.cumulative_rows(), ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummax(self) -> DataFrame: return self._apply_window_op( agg_ops.max_op, window_spec.cumulative_rows(), ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def shift(self, periods: int = 1) -> DataFrame: window = window_spec.rows( preceding=periods if periods > 0 else None, @@ -2836,7 +2836,7 @@ def shift(self, periods: int = 1) -> DataFrame: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def diff(self, periods: int = 1) -> DataFrame: window = window_spec.rows( preceding=periods if periods > 0 else None, @@ -2844,7 +2844,7 @@ def diff(self, periods: int = 1) -> DataFrame: ) return self._apply_window_op(agg_ops.DiffOp(periods), window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def pct_change(self, periods: int = 1) -> DataFrame: # Future versions of pandas will not perfrom ffill automatically df = self.ffill() @@ -2862,7 +2862,7 @@ def _apply_window_op( ) return DataFrame(block.select_columns(result_ids)) - @validations.requires_strict_ordering() + @validations.requires_ordering() def sample( self, n: Optional[int] = None, @@ -3678,7 +3678,7 @@ def _optimize_query_complexity(self): _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") - @validations.requires_strict_ordering() + @validations.requires_ordering() def dot(self, other: _DataFrameOrSeries) -> _DataFrameOrSeries: if not isinstance(other, (DataFrame, bf_series.Series)): raise NotImplementedError( diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index b1af96c9c4e..00abb887b02 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -63,5 +63,9 @@ class TimeTravelDisabledWarning(Warning): """A query was reattempted without time travel.""" +class AmbiguousWindowWarning(Warning): + """A query may produce nondeterministic results as the window may be ambiguously ordered.""" + + class UnknownDataTypeWarning(Warning): """Data type is unknown.""" diff --git a/bigframes/series.py b/bigframes/series.py index 9e338018347..d41553d0d7e 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -93,12 +93,12 @@ def loc(self) -> bigframes.core.indexers.LocSeriesIndexer: return bigframes.core.indexers.LocSeriesIndexer(self) @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def iloc(self) -> bigframes.core.indexers.IlocSeriesIndexer: return bigframes.core.indexers.IlocSeriesIndexer(self) @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def iat(self) -> bigframes.core.indexers.IatSeriesIndexer: return bigframes.core.indexers.IatSeriesIndexer(self) @@ -163,7 +163,7 @@ def struct(self) -> structs.StructAccessor: return structs.StructAccessor(self._block) @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def T(self) -> Series: return self.transpose() @@ -175,7 +175,7 @@ def _info_axis(self) -> indexes.Index: def _session(self) -> bigframes.Session: return self._get_block().expr.session - @validations.requires_strict_ordering() + @validations.requires_ordering() def transpose(self) -> Series: return self @@ -271,7 +271,7 @@ def equals( return False return block_ops.equals(self._block, other._block) - @validations.requires_strict_ordering() + @validations.requires_ordering() def reset_index( self, *, @@ -459,13 +459,13 @@ def case_when(self, caselist) -> Series: ignore_self=True, ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumsum(self) -> Series: return self._apply_window_op( agg_ops.sum_op, bigframes.core.window_spec.cumulative_rows() ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> Series: window = bigframes.core.window_spec.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) @@ -473,30 +473,30 @@ def ffill(self, *, limit: typing.Optional[int] = None) -> Series: pad = ffill pad.__doc__ = inspect.getdoc(vendored_pandas_series.Series.ffill) - @validations.requires_strict_ordering() + @validations.requires_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> Series: window = bigframes.core.window_spec.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummax(self) -> Series: return self._apply_window_op( agg_ops.max_op, bigframes.core.window_spec.cumulative_rows() ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cummin(self) -> Series: return self._apply_window_op( agg_ops.min_op, bigframes.core.window_spec.cumulative_rows() ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def cumprod(self) -> Series: return self._apply_window_op( agg_ops.product_op, bigframes.core.window_spec.cumulative_rows() ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def shift(self, periods: int = 1) -> Series: window = bigframes.core.window_spec.rows( preceding=periods if periods > 0 else None, @@ -504,7 +504,7 @@ def shift(self, periods: int = 1) -> Series: ) return self._apply_window_op(agg_ops.ShiftOp(periods), window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def diff(self, periods: int = 1) -> Series: window = bigframes.core.window_spec.rows( preceding=periods if periods > 0 else None, @@ -512,13 +512,13 @@ def diff(self, periods: int = 1) -> Series: ) return self._apply_window_op(agg_ops.DiffOp(periods), window) - @validations.requires_strict_ordering() + @validations.requires_ordering() def pct_change(self, periods: int = 1) -> Series: # Future versions of pandas will not perfrom ffill automatically series = self.ffill() return Series(block_ops.pct_change(series._block, periods=periods)) - @validations.requires_strict_ordering() + @validations.requires_ordering() def rank( self, axis=0, @@ -610,7 +610,7 @@ def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): ) return Series(block.select_column(result)) - @validations.requires_strict_ordering() + @validations.requires_ordering() @requires_index def interpolate(self, method: str = "linear") -> Series: if method == "pad": @@ -633,11 +633,11 @@ def dropna( result = result.reset_index() return Series(result) - @validations.requires_strict_ordering(bigframes.constants.SUGGEST_PEEK_PREVIEW) + @validations.requires_ordering(bigframes.constants.SUGGEST_PEEK_PREVIEW) def head(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[0:n]) - @validations.requires_strict_ordering() + @validations.requires_ordering() def tail(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[-n:]) @@ -1138,7 +1138,7 @@ def clip(self, lower, upper): ) return Series(block.select_column(result_id).with_column_labels([self.name])) - @validations.requires_strict_ordering() + @validations.requires_ordering() def argmax(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( @@ -1151,7 +1151,7 @@ def argmax(self) -> int: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def argmin(self) -> int: block, row_nums = self._block.promote_offsets() block = block.order_by( @@ -1217,14 +1217,14 @@ def idxmin(self) -> blocks.Label: return indexes.Index(block).to_pandas()[0] @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def is_monotonic_increasing(self) -> bool: return typing.cast( bool, self._block.is_monotonic_increasing(self._value_column) ) @property - @validations.requires_strict_ordering() + @validations.requires_ordering() def is_monotonic_decreasing(self) -> bool: return typing.cast( bool, self._block.is_monotonic_decreasing(self._value_column) @@ -1332,7 +1332,7 @@ def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: block = block.order_by(ordering) return Series(block) - @validations.requires_strict_ordering() + @validations.requires_ordering() 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 = bigframes.core.window_spec.rows( @@ -1342,7 +1342,7 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window self._block, window_spec, self._block.value_columns, is_series=True ) - @validations.requires_strict_ordering() + @validations.requires_ordering() def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: window_spec = bigframes.core.window_spec.cumulative_rows( min_periods=min_periods @@ -1615,7 +1615,7 @@ def drop_duplicates(self, *, keep: str = "first") -> Series: block = block_ops.drop_duplicates(self._block, (self._value_column,), keep) return Series(block) - @validations.requires_strict_ordering() + @validations.requires_ordering() def unique(self) -> Series: return self.drop_duplicates() @@ -1806,7 +1806,7 @@ def map( result_df = self_df.join(map_df, on="series") return result_df[self.name] - @validations.requires_strict_ordering() + @validations.requires_ordering() def sample( self, n: Optional[int] = None, diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index f449b52fbfc..dc1da488a15 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -314,6 +314,7 @@ def __init__( self._compiler = bigframes.core.compile.SQLCompiler( strict=self._strictly_ordered ) + self._allow_ambiguity = not self._strictly_ordered self._remote_function_session = bigframes_rf._RemoteFunctionSession() @@ -378,6 +379,10 @@ def slot_millis_sum(self): """The sum of all slot time used by bigquery jobs in this session.""" return self._slot_millis_sum + @property + def _allows_ambiguity(self) -> bool: + return self._allow_ambiguity + def _add_bytes_processed(self, amount: int): """Increment bytes_processed_sum by amount.""" self._bytes_processed_sum += amount diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 3a7eff621f1..d838251dca6 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2273,7 +2273,7 @@ def test_series_binop_add_different_table( def test_join_same_table(scalars_dfs_maybe_ordered, how): bf_df, pd_df = scalars_dfs_maybe_ordered if not bf_df._session._strictly_ordered and how == "cross": - pytest.skip("Cross join not supported in unordered mode.") + pytest.skip("Cross join not supported in partial ordering mode.") bf_df_a = bf_df.set_index("int64_too")[["string_col", "int64_col"]] bf_df_a = bf_df_a.sort_index() diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 7d7097ceb37..9f85ec99f95 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -11,6 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import warnings + import pandas as pd import pyarrow as pa import pytest @@ -99,7 +101,6 @@ def test_unordered_mode_read_gbq(unordered_session): [ pytest.param( "first", - marks=pytest.mark.xfail(raises=bigframes.exceptions.OrderRequiredError), ), pytest.param( False, @@ -138,37 +139,6 @@ def test_unordered_merge(unordered_session): assert_pandas_df_equal(bf_result.to_pandas(), pd_result, ignore_order=True) -@pytest.mark.parametrize( - ("function"), - [ - pytest.param( - lambda x: x.cumsum(), - id="cumsum", - ), - pytest.param( - lambda x: x.idxmin(), - id="idxmin", - ), - pytest.param( - lambda x: x.a.iloc[1::2], - id="series_iloc", - ), - pytest.param( - lambda x: x.head(3), - id="head", - ), - ], -) -def test_unordered_mode_blocks_windowing(unordered_session, function): - pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) - df = bpd.DataFrame(pd_df, session=unordered_session) - with pytest.raises( - bigframes.exceptions.OrderRequiredError, - match=r"Op.*not supported when strict ordering is disabled", - ): - function(df) - - def test_unordered_mode_cache_preserves_order(unordered_session): pd_df = pd.DataFrame( {"a": [1, 2, 3, 4, 5, 6], "b": [4, 5, 9, 3, 1, 6]}, dtype=pd.Int64Dtype() @@ -181,3 +151,37 @@ def test_unordered_mode_cache_preserves_order(unordered_session): # B is unique so unstrict order mode result here should be equivalent to strictly ordered assert_pandas_df_equal(bf_result, pd_result, ignore_order=False) + + +def test_unordered_mode_no_ordering_error(unordered_session): + pd_df = pd.DataFrame( + {"a": [1, 2, 3, 4, 5, 1], "b": [4, 5, 9, 3, 1, 6]}, dtype=pd.Int64Dtype() + ) + pd_df.index = pd_df.index.astype(pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session) + + with pytest.raises(bigframes.exceptions.OrderRequiredError): + df.merge(df, on="a").head(3) + + +def test_unordered_mode_ambiguity_warning(unordered_session): + pd_df = pd.DataFrame( + {"a": [1, 2, 3, 4, 5, 1], "b": [4, 5, 9, 3, 1, 6]}, dtype=pd.Int64Dtype() + ) + pd_df.index = pd_df.index.astype(pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session) + + with pytest.warns(bigframes.exceptions.AmbiguousWindowWarning): + df.merge(df, on="a").sort_values("b_x").head(3) + + +def test_unordered_mode_no_ambiguity_warning(unordered_session): + pd_df = pd.DataFrame( + {"a": [1, 2, 3, 4, 5, 1], "b": [4, 5, 9, 3, 1, 6]}, dtype=pd.Int64Dtype() + ) + pd_df.index = pd_df.index.astype(pd.Int64Dtype()) + df = bpd.DataFrame(pd_df, session=unordered_session) + + with warnings.catch_warnings(): + warnings.simplefilter("error") + df.groupby("a").head(3) From 5317327f8bf7751688f3ad4cc0c96f719cf2b062 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:43:08 -0700 Subject: [PATCH 741/921] chore(main): release 1.13.0 (#876) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 354c356c7c8..3209391f44c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.13.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.12.0...v1.13.0) (2024-08-05) + + +### Features + +* `df.apply(axis=1)` to support remote function with mutiple params ([#851](https://github.com/googleapis/python-bigquery-dataframes/issues/851)) ([2158818](https://github.com/googleapis/python-bigquery-dataframes/commit/2158818e53e09e55c87ffd574e3ebc2e201285fb)) +* Allow windowing in 'partial' ordering mode ([#861](https://github.com/googleapis/python-bigquery-dataframes/issues/861)) ([ca26fe5](https://github.com/googleapis/python-bigquery-dataframes/commit/ca26fe5f9edec519788c276a09eaff33ecd87434)) +* Create a separate OrderingModePartialPreviewWarning for more fine-grained warning filters ([#879](https://github.com/googleapis/python-bigquery-dataframes/issues/879)) ([8753bdd](https://github.com/googleapis/python-bigquery-dataframes/commit/8753bdd1e44701e56eae914ebc0e91d9b1a6adf1)) + + +### Bug Fixes + +* Fix issue with invalid sql generated by ml distance functions ([#865](https://github.com/googleapis/python-bigquery-dataframes/issues/865)) ([9959fc8](https://github.com/googleapis/python-bigquery-dataframes/commit/9959fc8fcba93441fdd3d9c17e8fdbe6e6a7b504)) + + +### Documentation + +* Create sample notebook using `ordering_mode="partial"` ([#880](https://github.com/googleapis/python-bigquery-dataframes/issues/880)) ([c415eb9](https://github.com/googleapis/python-bigquery-dataframes/commit/c415eb91eb71dea53d245ba2bce416062e3f02f8)) +* Update streaming notebook ([#875](https://github.com/googleapis/python-bigquery-dataframes/issues/875)) ([e9b0557](https://github.com/googleapis/python-bigquery-dataframes/commit/e9b05571123cf13079772856317ca3cd3d564c5a)) + ## [1.12.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.11.1...v1.12.0) (2024-07-31) diff --git a/bigframes/version.py b/bigframes/version.py index 29cf036f426..b474f021d40 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0" +__version__ = "1.13.0" From 52b7786c3a28da6c29e3ddf12629802215194ad9 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 5 Aug 2024 16:21:51 -0700 Subject: [PATCH 742/921] fix: Fix caching from generating row numbers in partial ordering mode (#872) --- bigframes/series.py | 2 +- bigframes/session/__init__.py | 4 +++- tests/system/small/test_unordered.py | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bigframes/series.py b/bigframes/series.py index d41553d0d7e..069c469a85d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -641,7 +641,7 @@ def head(self, n: int = 5) -> Series: def tail(self, n: int = 5) -> Series: return typing.cast(Series, self.iloc[-n:]) - def peek(self, n: int = 5, *, force: bool = True) -> pandas.DataFrame: + def peek(self, n: int = 5, *, force: bool = True) -> pandas.Series: """ Preview n arbitrary elements from the series without guarantees about row selection or ordering. diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index dc1da488a15..8ff5862bfcd 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1997,8 +1997,10 @@ def _cache_with_session_awareness(self, array_value: core.ArrayValue) -> None: ) if len(cluster_cols) > 0: self._cache_with_cluster_cols(core.ArrayValue(target), cluster_cols) - else: + elif self._strictly_ordered: self._cache_with_offsets(core.ArrayValue(target)) + else: + self._cache_with_cluster_cols(core.ArrayValue(target), []) def _simplify_with_caching(self, array_value: core.ArrayValue): """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 9f85ec99f95..5e124d73cdd 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -19,7 +19,11 @@ import bigframes.exceptions import bigframes.pandas as bpd -from tests.system.utils import assert_pandas_df_equal, skip_legacy_pandas +from tests.system.utils import ( + assert_pandas_df_equal, + assert_series_equal, + skip_legacy_pandas, +) def test_unordered_mode_sql_no_hash(unordered_session): @@ -51,6 +55,15 @@ def test_unordered_mode_cache_aggregate(unordered_session): assert_pandas_df_equal(bf_result, pd_result, ignore_order=True) +def test_unordered_mode_series_peek(unordered_session): + pd_series = pd.Series([1, 2, 3, 4, 5, 6], dtype=pd.Int64Dtype()) + bf_series = bpd.Series(pd_series, session=unordered_session) + pd_result = pd_series.groupby(pd_series % 4).sum() + bf_peek = bf_series.groupby(bf_series % 4).sum().peek(2) + + assert_series_equal(bf_peek, pd_result.reindex(bf_peek.index)) + + def test_unordered_mode_single_aggregate(unordered_session): pd_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.Int64Dtype()) bf_df = bpd.DataFrame(pd_df, session=unordered_session) From 6e6f9df55d435afe0b3ade728ca06826e92a6ee6 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:22:53 -0700 Subject: [PATCH 743/921] docs: update streaming notebook (#887) --- notebooks/streaming/streaming_dataframe.ipynb | 203 +++++++++++------- 1 file changed, 127 insertions(+), 76 deletions(-) diff --git a/notebooks/streaming/streaming_dataframe.ipynb b/notebooks/streaming/streaming_dataframe.ipynb index d4cc255fa5e..9b52c2d71e6 100644 --- a/notebooks/streaming/streaming_dataframe.ipynb +++ b/notebooks/streaming/streaming_dataframe.ipynb @@ -17,10 +17,22 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'1.13.0'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "import bigframes\n", - "import bigframes.streaming as bst" + "# make sure bigframes version >= 1.12.0\n", + "bigframes.__version__" ] }, { @@ -29,10 +41,46 @@ "metadata": {}, "outputs": [], "source": [ - "bigframes.options._bigquery_options.project = \"bigframes-load-testing\"\n", + "import bigframes.pandas as bpd\n", + "import bigframes.streaming as bst\n", + "bigframes.options._bigquery_options.project = \"bigframes-load-testing\" # Change to your own project ID\n", "job_id_prefix = \"test_streaming_\"" ] }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 65df3a2f-cda8-405d-8b38-20a755f9b9a0 is DONE. 28.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'birds.penguins_bigtable_streaming'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Copy a table from the public dataset for streaming jobs. Any changes to the table can be reflected in the streaming destination.\n", + "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", + "df.to_gbq(\"birds.penguins_bigtable_streaming\", if_exists=\"replace\")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -43,13 +91,15 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/session/__init__.py:773: PreviewWarning: The bigframes.streaming module is a preview feature, and subject to change.\n", + " warnings.warn(\n", "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/blocks.py:126: NullIndexPreviewWarning: Creating object with Null Index. Null Index is a preview feature.\n", " warnings.warn(\n" ] @@ -61,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -82,7 +132,7 @@ { "data": { "text/html": [ - "Query job d57200dd-e6f1-42c7-876b-7f4a54994ae6 is DONE. 0 Bytes processed. Open Job" + "Query job dd20bd9d-4844-43e4-86ab-95759d7e673a is DONE. 2.7 kB processed. Open Job" ], "text/plain": [ "" @@ -104,7 +154,7 @@ { "data": { "text/html": [ - "Query job 1decce4a-eb32-49f4-8e47-7bda0220037a is DONE. 28.9 kB processed. Open Job" + "Query job 873e44ee-76e9-4254-83d3-04cf36fbd140 is DONE. 28.9 kB processed. Open Job" ], "text/plain": [ "" @@ -144,151 +194,151 @@ " 0\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Torgersen\n", - " 3875\n", + " 3875.0\n", " \n", " \n", " 1\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Torgersen\n", - " 2900\n", + " 2900.0\n", " \n", " \n", " 2\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Biscoe\n", - " 3725\n", + " 3725.0\n", " \n", " \n", " 3\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 2975\n", + " 2975.0\n", " \n", " \n", " 4\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Torgersen\n", - " 3050\n", + " 3050.0\n", " \n", " \n", " 5\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 2700\n", + " 2700.0\n", " \n", " \n", " 6\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 3900\n", + " 3900.0\n", " \n", " \n", " 7\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Biscoe\n", - " 3825\n", + " 3825.0\n", " \n", " \n", " 8\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3775\n", + " 3775.0\n", " \n", " \n", " 9\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 3350\n", + " 3350.0\n", " \n", " \n", " 10\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Biscoe\n", - " 3900\n", + " 3900.0\n", " \n", " \n", " 11\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Torgersen\n", - " 3650\n", + " 3650.0\n", " \n", " \n", " 12\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Biscoe\n", - " 3200\n", + " 3200.0\n", " \n", " \n", " 13\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3650\n", + " 3650.0\n", " \n", " \n", " 14\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 3700\n", + " 3700.0\n", " \n", " \n", " 15\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3800\n", + " 3800.0\n", " \n", " \n", " 16\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3950\n", + " 3950.0\n", " \n", " \n", " 17\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3350\n", + " 3350.0\n", " \n", " \n", " 18\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 3100\n", + " 3100.0\n", " \n", " \n", " 19\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3750\n", + " 3750.0\n", " \n", " \n", " 20\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Biscoe\n", - " 3550\n", + " 3550.0\n", " \n", " \n", " 21\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3400\n", + " 3400.0\n", " \n", " \n", " 22\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Torgersen\n", - " 3450\n", + " 3450.0\n", " \n", " \n", " 23\n", " Adelie Penguin (Pygoscelis adeliae)\n", " Torgersen\n", - " 3600\n", + " 3600.0\n", " \n", " \n", " 24\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 3650\n", + " 3650.0\n", " \n", " \n", "\n", @@ -297,37 +347,37 @@ ], "text/plain": [ " species rowkey body_mass_g\n", - " Adelie Penguin (Pygoscelis adeliae) Torgersen 3875\n", - " Adelie Penguin (Pygoscelis adeliae) Torgersen 2900\n", - " Adelie Penguin (Pygoscelis adeliae) Biscoe 3725\n", - " Adelie Penguin (Pygoscelis adeliae) Dream 2975\n", - " Adelie Penguin (Pygoscelis adeliae) Torgersen 3050\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 2700\n", - " Adelie Penguin (Pygoscelis adeliae) Dream 3900\n", - " Adelie Penguin (Pygoscelis adeliae) Biscoe 3825\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3775\n", - " Adelie Penguin (Pygoscelis adeliae) Dream 3350\n", - " Adelie Penguin (Pygoscelis adeliae) Biscoe 3900\n", - " Adelie Penguin (Pygoscelis adeliae) Torgersen 3650\n", - " Adelie Penguin (Pygoscelis adeliae) Biscoe 3200\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3650\n", - " Adelie Penguin (Pygoscelis adeliae) Dream 3700\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3800\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3950\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3350\n", - " Adelie Penguin (Pygoscelis adeliae) Dream 3100\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3750\n", - " Adelie Penguin (Pygoscelis adeliae) Biscoe 3550\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3400\n", - " Adelie Penguin (Pygoscelis adeliae) Torgersen 3450\n", - " Adelie Penguin (Pygoscelis adeliae) Torgersen 3600\n", - "Chinstrap penguin (Pygoscelis antarctica) Dream 3650\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3875.0\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 2900.0\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3725.0\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 2975.0\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3050.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 2700.0\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3900.0\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3825.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3775.0\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3350.0\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3900.0\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3650.0\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3200.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3650.0\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3700.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3800.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3950.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3350.0\n", + " Adelie Penguin (Pygoscelis adeliae) Dream 3100.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3750.0\n", + " Adelie Penguin (Pygoscelis adeliae) Biscoe 3550.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3400.0\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3450.0\n", + " Adelie Penguin (Pygoscelis adeliae) Torgersen 3600.0\n", + "Chinstrap penguin (Pygoscelis antarctica) Dream 3650.0\n", "...\n", "\n", "[165 rows x 3 columns]" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -351,22 +401,22 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/garrettwu/src/bigframes/bigframes/streaming/dataframe.py:338: PreviewWarning: The bigframes.streaming module is a preview feature, and subject to change.\n", + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/streaming/dataframe.py:341: PreviewWarning: The bigframes.streaming module is a preview feature, and subject to change.\n", " warnings.warn(\n" ] } ], "source": [ - "job = sdf.to_bigtable(instance=\"streaming-testing-instance\",\n", - " table=\"garrettwu-no-col-family\",\n", - " service_account_email=\"streaming-testing-admin@bigframes-load-testing.iam.gserviceaccount.com\",\n", + "job = sdf.to_bigtable(instance=\"streaming-testing-instance\", # Change to your own Bigtable instance name\n", + " table=\"garrettwu-no-col-family\", # Change to your own Bigtable table name\n", + " service_account_email=\"streaming-testing-admin@bigframes-load-testing.iam.gserviceaccount.com\", # Change to your own service account\n", " app_profile=None,\n", " truncate=True,\n", " overwrite=True,\n", @@ -378,7 +428,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -397,7 +447,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -406,7 +456,7 @@ "True" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -419,13 +469,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### PubSub\n", - "Create Pubsub streaming job" + "### Pub/Sub\n", + "Create Pub/Sub streaming job" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -438,27 +488,28 @@ } ], "source": [ + "# Pub/Sub requires a single column\n", "sdf = sdf[[\"rowkey\"]]" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/garrettwu/src/bigframes/bigframes/streaming/dataframe.py:453: PreviewWarning: The bigframes.streaming module is a preview feature, and subject to change.\n", + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/streaming/dataframe.py:456: PreviewWarning: The bigframes.streaming module is a preview feature, and subject to change.\n", " warnings.warn(\n" ] } ], "source": [ "job = sdf.to_pubsub(\n", - " topic=\"penguins\",\n", - " service_account_email=\"streaming-testing@bigframes-load-testing.iam.gserviceaccount.com\",\n", + " topic=\"penguins\", # Change to your own Pub/Sub topic ID\n", + " service_account_email=\"streaming-testing@bigframes-load-testing.iam.gserviceaccount.com\", # Change to your own service account\n", " job_id=None,\n", " job_id_prefix=job_id_prefix,\n", " )" @@ -466,7 +517,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -485,7 +536,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -494,7 +545,7 @@ "True" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } From eb6080460344aff2fabb7864536ea4fe24c5fbef Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 6 Aug 2024 15:02:19 -0700 Subject: [PATCH 744/921] perf: Generate SQL with fewer CTEs (#877) --- bigframes/core/compile/compiled.py | 135 +++++++++++++++++------------ bigframes/core/compile/compiler.py | 1 - bigframes/core/window_spec.py | 13 ++- tests/unit/session/test_session.py | 3 +- 4 files changed, 93 insertions(+), 59 deletions(-) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 538789f9d74..cae527931cd 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -24,6 +24,7 @@ import ibis.backends.bigquery as ibis_bigquery import ibis.common.deferred # type: ignore import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.operations as ibis_ops import ibis.expr.types as ibis_types import pandas @@ -36,7 +37,6 @@ from bigframes.core.ordering import ( ascending_over, encode_order_string, - IntegerEncoding, join_orderings, OrderingExpression, RowOrdering, @@ -71,19 +71,16 @@ def __init__( # Allow creating a DataFrame directly from an Ibis table expression. # TODO(swast): Validate that each column references the same table (or # no table for literal values). - self._columns = tuple(columns) + self._columns = tuple( + column.resolve(table) + # TODO(https://github.com/ibis-project/ibis/issues/7613): use + # public API to refer to Deferred type. + if isinstance(column, ibis.common.deferred.Deferred) else column + for column in columns + ) # To allow for more efficient lookup by column name, create a # dictionary mapping names to column values. - self._column_names = { - ( - column.resolve(table) - # TODO(https://github.com/ibis-project/ibis/issues/7613): use - # public API to refer to Deferred type. - if isinstance(column, ibis.common.deferred.Deferred) - else column - ).get_name(): column - for column in self._columns - } + self._column_names = {column.get_name(): column for column in self._columns} @property def columns(self) -> typing.Tuple[ibis_types.Value, ...]: @@ -139,10 +136,6 @@ def projection( for expression, id in expression_id_pairs ] result = self._select(tuple(values)) # type: ignore - - # Need to reproject to convert ibis Scalar to ibis Column object - if any(exp_id[0].is_const for exp_id in expression_id_pairs): - result = result._reproject_to_table() return result @abc.abstractmethod @@ -300,8 +293,6 @@ def _to_ibis_expr( ArrayValue objects are sorted, so the following options are available to reflect this in the ibis expression. - * "offset_col": Zero-based offsets are generated as a column, this will - not sort the rows however. * "string_encoded": An ordered string column is provided in output table. * "unordered": No ordering information will be provided in output. Only value columns are projected. @@ -355,6 +346,10 @@ def _to_ibis_expr( return table def filter(self, predicate: ex.Expression) -> UnorderedIR: + if any(map(is_window, map(self._get_ibis_column, predicate.unbound_variables))): + # ibis doesn't support qualify syntax, so create CTE if filtering over window expression + # https://github.com/ibis-project/ibis/issues/9775 + return self._reproject_to_table().filter(predicate) bindings = {col: self._get_ibis_column(col) for col in self.column_ids} condition = op_compiler.compile_expression(predicate, bindings) return self._filter(condition) @@ -785,15 +780,33 @@ def promote_offsets(self, col_id: str) -> OrderedIR: """ # Special case: offsets already exist ordering = self._ordering + # Case 1, already have offsets, just create column from them + if ordering.is_sequential and (ordering.total_order_col is not None): + expr_builder = self.builder() + expr_builder.columns = [ + self._compile_expression( + ordering.total_order_col.scalar_expression + ).name(col_id), + *self.columns, + ] + return expr_builder.build() + # Cannot nest analytic expressions, so reproject to cte first if needed. + # Also ibis cannot window literals, so need to reproject those (even though this is legal in googlesql) + # Seee: https://github.com/ibis-project/ibis/issues/9773 + can_directly_window = not any( + map(lambda x: is_literal(x) or is_window(x), self._ibis_order) + ) + if not can_directly_window: + return self._reproject_to_table().promote_offsets(col_id) - if (not ordering.is_sequential) or (not ordering.total_order_col): - return self._project_offsets().promote_offsets(col_id) + window = ibis.window(order_by=self._ibis_order) + if self._predicates: + window = window.group_by(self._reduced_predicate) + offsets = ibis.row_number().over(window) expr_builder = self.builder() expr_builder.columns = [ - self._compile_expression(ordering.total_order_col.scalar_expression).name( - col_id - ), *self.columns, + offsets.name(col_id), ] return expr_builder.build() @@ -806,7 +819,6 @@ def project_window_op( output_name=None, *, never_skip_nulls=False, - skip_reproject_unsafe: bool = False, ) -> OrderedIR: """ Creates a new expression based on this expression with unary operation applied to one column. @@ -815,8 +827,25 @@ def project_window_op( window_spec: a specification of the window over which to apply the operator output_name: the id to assign to the output of the operator, by default will replace input col if distinct output id not provided never_skip_nulls: will disable null skipping for operators that would otherwise do so - skip_reproject_unsafe: skips the reprojection step, can be used when performing many non-dependent window operations, user responsible for not nesting window expressions, or using outputs as join, filter or aggregation keys before a reprojection """ + # Cannot nest analytic expressions, so reproject to cte first if needed. + # Also ibis cannot window literals, so need to reproject those (even though this is legal in googlesql) + # See: https://github.com/ibis-project/ibis/issues/9773 + used_exprs = map( + self._get_any_column, [column_name, *window_spec.all_referenced_columns] + ) + can_directly_window = not any( + map(lambda x: is_literal(x) or is_window(x), used_exprs) + ) + if not can_directly_window: + return self._reproject_to_table().project_window_op( + column_name, + op, + window_spec, + output_name, + never_skip_nulls=never_skip_nulls, + ) + column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) window = self._ibis_window_from_spec( window_spec, require_total_order=op.uses_total_row_ordering @@ -861,8 +890,7 @@ def project_window_op( window_op = case_statement result = self._set_or_replace_by_id(output_name or column_name, window_op) - # TODO(tbergeron): Automatically track analytic expression usage and defer reprojection until required for valid query generation. - return result._reproject_to_table() if not skip_reproject_unsafe else result + return result def _reproject_to_table(self) -> OrderedIR: table = self._to_ibis_expr( @@ -944,7 +972,7 @@ def _to_ibis_expr( expose_hidden_cols: bool = False, fraction: Optional[float] = None, col_id_overrides: typing.Mapping[str, str] = {}, - ordering_mode: Literal["string_encoded", "offset_col", "unordered"], + ordering_mode: Literal["string_encoded", "unordered"], order_col_name: Optional[str] = ORDER_ID_COLUMN, ): """ @@ -953,8 +981,7 @@ def _to_ibis_expr( ArrayValue objects are sorted, so the following options are available to reflect this in the ibis expression. - * "offset_col": Zero-based offsets are generated as a column, this will - not sort the rows however. + * "string_encoded": An ordered string column is provided in output table. * "unordered": No ordering information will be provided in output. Only value columns are projected. @@ -981,10 +1008,9 @@ def _to_ibis_expr( """ assert ordering_mode in ( "string_encoded", - "offset_col", "unordered", ) - if expose_hidden_cols and ordering_mode in ("ordered_col", "offset_col"): + if expose_hidden_cols and ordering_mode in ("ordered_col"): raise ValueError( f"Cannot expose hidden ordering columns with ordering_mode {ordering_mode}" ) @@ -1034,6 +1060,10 @@ def _to_ibis_expr( return table def filter(self, predicate: ex.Expression) -> OrderedIR: + if any(map(is_window, map(self._get_ibis_column, predicate.unbound_variables))): + # ibis doesn't support qualify syntax, so create CTE if filtering over window expression + # https://github.com/ibis-project/ibis/issues/9775 + return self._reproject_to_table().filter(predicate) bindings = {col: self._get_ibis_column(col) for col in self.column_ids} condition = op_compiler.compile_expression(predicate, bindings) return self._filter(condition) @@ -1174,27 +1204,6 @@ def _bake_ordering(self) -> OrderedIR: predicates=self._predicates, ) - def _project_offsets(self) -> OrderedIR: - """Create a new expression that contains offsets. Should only be executed when - offsets are needed for an operations. Has no effect on expression semantics.""" - if self._ordering.is_sequential: - return self - table = self._to_ibis_expr( - ordering_mode="offset_col", order_col_name=ORDER_ID_COLUMN - ) - columns = [table[column_name] for column_name in self._column_names] - ordering = TotalOrdering( - ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), - integer_encoding=IntegerEncoding(True, is_sequential=True), - ) - return OrderedIR( - table, - columns=columns, - hidden_ordering_columns=[table[ORDER_ID_COLUMN]], - ordering=ordering, - ) - def _create_order_columns( self, ordering_mode: str, @@ -1202,9 +1211,7 @@ def _create_order_columns( expose_hidden_cols: bool, ) -> typing.Sequence[ibis_types.Value]: # Generate offsets if current ordering id semantics are not sufficiently strict - if ordering_mode == "offset_col": - return (self._create_offset_column().name(order_col_name),) - elif ordering_mode == "string_encoded": + if ordering_mode == "string_encoded": return (self._create_string_ordering_column().name(order_col_name),) elif expose_hidden_cols: return self._hidden_ordering_columns @@ -1328,6 +1335,22 @@ def build(self) -> OrderedIR: ) +def is_literal(column: ibis_types.Value) -> bool: + # Unfortunately, Literals in ibis are not "Columns"s and therefore can't be aggregated. + return not isinstance(column, ibis_types.Column) + + +def is_window(column: ibis_types.Value) -> bool: + matches = ( + (column) + .op() + .find_topmost( + lambda x: isinstance(x, (ibis_ops.WindowFunction, ibis_ops.Relation)) + ) + ) + return any(isinstance(op, ibis_ops.WindowFunction) for op in matches) + + def _reduce_predicate_list( predicate_list: typing.Collection[ibis_types.BooleanValue], ) -> ibis_types.BooleanValue: diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index c7f8c5ab596..8fb1f7ab3a3 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -304,7 +304,6 @@ def compile_window(self, node: nodes.WindowOpNode, ordered: bool = True): node.window_spec, node.output_name, never_skip_nulls=node.never_skip_nulls, - skip_reproject_unsafe=node.skip_reproject_unsafe, ) return result if ordered else result.to_unordered() diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py index 57c57b451a7..f011e2848de 100644 --- a/bigframes/core/window_spec.py +++ b/bigframes/core/window_spec.py @@ -14,7 +14,8 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, Tuple, Union +import itertools +from typing import Optional, Set, Tuple, Union import bigframes.core.ordering as orderings @@ -162,3 +163,13 @@ def row_bounded(self): to calculate deterministically. """ return isinstance(self.bounds, RowsWindowBounds) + + @property + def all_referenced_columns(self) -> Set[str]: + """ + Return list of all variables reference ind the window. + """ + ordering_vars = itertools.chain.from_iterable( + item.scalar_expression.unbound_variables for item in self.ordering + ) + return set(itertools.chain(self.grouping_keys, ordering_vars)) diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 31029abd67f..2f7eaa567a5 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -246,7 +246,8 @@ def test_default_index_warning_not_raised_by_read_gbq_index_col_sequential_int64 index_col=bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64, ) - # We expect a window operation because we specificaly requested a sequential index. + # We expect a window operation because we specificaly requested a sequential index and named it. + df.index.name = "named_index" generated_sql = df.sql.casefold() assert "OVER".casefold() in generated_sql assert "ROW_NUMBER()".casefold() in generated_sql From 171da6cb33165b49d46ea6528038342abd89e9fa Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:35:05 -0700 Subject: [PATCH 745/921] docs: add streaming html docs (#884) * docs: add streaming html docs * add beta icon * add beta icon in toc.yml --- bigframes/session/__init__.py | 4 +++- bigframes/streaming/dataframe.py | 11 ++++++++++- docs/reference/bigframes.streaming/dataframe.rst | 6 ++++++ docs/reference/bigframes.streaming/index.rst | 13 +++++++++++++ docs/reference/index.rst | 1 + docs/templates/toc.yml | 7 +++++++ 6 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 docs/reference/bigframes.streaming/dataframe.rst create mode 100644 docs/reference/bigframes.streaming/index.rst diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 8ff5862bfcd..2da788292b5 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -760,7 +760,9 @@ def read_gbq_table_streaming( ) -> streaming_dataframe.StreamingDataFrame: """Turn a BigQuery table into a StreamingDataFrame. - Note: The bigframes.streaming module is a preview feature, and subject to change. + .. note:: + + The bigframes.streaming module is a preview feature, and subject to change. **Examples:** diff --git a/bigframes/streaming/dataframe.py b/bigframes/streaming/dataframe.py index 64a4898c57c..b83ae5d822e 100644 --- a/bigframes/streaming/dataframe.py +++ b/bigframes/streaming/dataframe.py @@ -191,7 +191,16 @@ def to_pubsub( @log_adapter.class_logger class StreamingDataFrame(StreamingBase): - __doc__ = _curate_df_doc(dataframe.DataFrame.__doc__) + __doc__ = ( + _curate_df_doc(dataframe.DataFrame.__doc__) + + """ + .. note:: + + The bigframes.streaming module is a preview feature, and subject to change. + + Currently only supports basic projection, filtering and preview operations. + """ + ) # Private constructor _create_key = object() diff --git a/docs/reference/bigframes.streaming/dataframe.rst b/docs/reference/bigframes.streaming/dataframe.rst new file mode 100644 index 00000000000..79ec64961c5 --- /dev/null +++ b/docs/reference/bigframes.streaming/dataframe.rst @@ -0,0 +1,6 @@ +bigframes.streaming.dataframe +============================= + +.. autoclass:: bigframes.streaming.dataframe.StreamingDataFrame + :members: + :inherited-members: diff --git a/docs/reference/bigframes.streaming/index.rst b/docs/reference/bigframes.streaming/index.rst new file mode 100644 index 00000000000..20a22072e5a --- /dev/null +++ b/docs/reference/bigframes.streaming/index.rst @@ -0,0 +1,13 @@ + +============================ +BigQuery DataFrame Streaming +============================ + +.. automodule:: bigframes.streaming + :members: + :undoc-members: + +.. toctree:: + :maxdepth: 2 + + dataframe diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 387e9b5cedf..eb5a774b29a 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -11,3 +11,4 @@ packages. bigframes.pandas/index bigframes.ml/index bigframes.bigquery/index + bigframes.streaming/index diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 95bded9a600..736ffba2861 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -203,4 +203,11 @@ - name: BigQuery built-in functions uid: bigframes.bigquery name: bigframes.bigquery + - items: + - name: Overview + uid: bigframes.streaming + - name: StreamingDataFrame + uid: bigframes.streaming.dataframe.StreamingDataFrame + name: bigframes.streaming + status: beta name: BigQuery DataFrames From 8c352ce6991b3635222a285d1eee9a56cd57d0c6 Mon Sep 17 00:00:00 2001 From: mattyopl <90574735+mattyopl@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:31:21 -0400 Subject: [PATCH 746/921] chore: clean up OWNERS (#886) - remove inactive users - add myself Co-authored-by: Matthew Laurence Chen --- OWNERS | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/OWNERS b/OWNERS index f86ad551efa..562ee0f19b9 100644 --- a/OWNERS +++ b/OWNERS @@ -1,12 +1,8 @@ -ashleyxu@google.com -bmil@google.com chelsealin@google.com garrettwu@google.com -henryjsolberg@google.com -hormati@google.com huanc@google.com jiaxun@google.com -kemppeterson@google.com +mlaurencechen@google.com shobs@google.com swast@google.com -tbergeron@google.com +tbergeron@google.com \ No newline at end of file From 3dbf84bd1531c1f8d41ba57c2c38b3ba6abfb812 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:03:50 -0700 Subject: [PATCH 747/921] feat: bigframes.bigquery.json_extract (#868) * feat: bigframes.bigquery.json_extract * fixing tests --- bigframes/bigquery/__init__.py | 35 ++++++++++++++++++++ bigframes/core/compile/scalar_op_compiler.py | 12 +++++++ bigframes/operations/__init__.py | 16 +++++++++ tests/system/small/bigquery/test_json.py | 27 +++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index ec26d14f33e..7c409839b18 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -208,6 +208,41 @@ def json_set( return series +def json_extract( + series: series.Series, + json_path: str, +) -> series.Series: + """Extracts a JSON value and converts it to a SQL JSON-formatted `STRING` or `JSON` + value. This function uses single quotes and brackets to escape invalid JSONPath + characters in JSON keys. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['{"class": {"students": [{"id": 5}, {"id": 12}]}}']) + >>> bbq.json_extract(s, json_path="$.class") + 0 "{\\\"students\\\":[{\\\"id\\\":5},{\\\"id\\\":12}]}" + dtype: string + + Args: + series (bigframes.series.Series): + The Series containing JSON data (as native JSON objects or JSON-formatted strings). + json_path (str): + The JSON path identifying the data that you want to obtain from the input. + + Returns: + bigframes.series.Series: A new Series with the JSON or JSON-formatted STRING. + """ + return series._apply_unary_op(ops.JSONExtract(json_path=json_path)) + + +# Search functions defined from +# https://cloud.google.com/bigquery/docs/reference/standard-sql/search_functions + + def vector_search( base_table: str, column_to_search: str, diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 67d0dac436f..32749b32a6f 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -922,6 +922,11 @@ def json_set_op_impl(x: ibis_types.Value, y: ibis_types.Value, op: ops.JSONSet): ).to_expr() +@scalar_op_compiler.register_unary_op(ops.JSONExtract, pass_op=True) +def json_extract_op_impl(x: ibis_types.Value, op: ops.JSONExtract): + return json_extract(json_obj=x, json_path=op.json_path) + + ### Binary Ops def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): """Wraps a binary operator to generate nulls of the expected type if either input is a null scalar.""" @@ -1549,6 +1554,13 @@ def json_set( """Produces a new SQL JSON value with the specified JSON data inserted or replaced.""" +@ibis.udf.scalar.builtin(name="json_extract") +def json_extract( + json_obj: ibis_dtypes.JSON, json_path: ibis_dtypes.str +) -> ibis_dtypes.JSON: + """Extracts a JSON value and converts it to a SQL JSON-formatted STRING or JSON value.""" + + @ibis.udf.scalar.builtin(name="ML.DISTANCE") def vector_distance(vector1, vector2, type: str) -> ibis_dtypes.Float64: """Computes the distance between two vectors using specified type ("EUCLIDEAN", "MANHATTAN", or "COSINE")""" diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 523882c14e6..4d4e40643d7 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -602,6 +602,22 @@ def output_type(self, *input_types): return dtypes.STRING_DTYPE +## JSON Ops +@dataclasses.dataclass(frozen=True) +class JSONExtract(UnaryOp): + name: typing.ClassVar[str] = "json_extract" + json_path: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_json_like(input_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {input_type}" + ) + return input_type + + # Binary Ops fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COERCE) maximum_op = create_binary_op(name="maximum", type_signature=op_typing.COERCE) diff --git a/tests/system/small/bigquery/test_json.py b/tests/system/small/bigquery/test_json.py index 9e0c06e0bdb..059b8eea878 100644 --- a/tests/system/small/bigquery/test_json.py +++ b/tests/system/small/bigquery/test_json.py @@ -110,3 +110,30 @@ def test_json_set_w_invalid_value_type(): def test_json_set_w_invalid_series_type(): with pytest.raises(TypeError): bbq.json_set(bpd.Series([1, 2]), json_path_value_pairs=[("$.a", 1)]) + + +def test_json_extract_from_json(): + s = _get_series_from_json([{"a": {"b": [1, 2]}}, {"a": {"c": 1}}, {"a": {"b": 0}}]) + actual = bbq.json_extract(s, "$.a.b") + # After the introduction of the JSON type, the output should be a JSON-formatted series. + expected = _get_series_from_json(["[1,2]", None, "0"]) + pd.testing.assert_series_equal( + actual.to_pandas(), + expected.to_pandas(), + ) + + +def test_json_extract_from_string(): + s = bpd.Series(['{"a": {"b": [1, 2]}}', '{"a": {"c": 1}}', '{"a": {"b": 0}}']) + actual = bbq.json_extract(s, "$.a.b") + expected = _get_series_from_json(["[1,2]", None, "0"]) + pd.testing.assert_series_equal( + actual.to_pandas(), + expected.to_pandas(), + check_names=False, + ) + + +def test_json_extract_w_invalid_series_type(): + with pytest.raises(TypeError): + bbq.json_extract(bpd.Series([1, 2]), "$.a") From 0603e62fcdf513ca4207909af4dbd8d036af0b0c Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:38:58 -0700 Subject: [PATCH 748/921] chore: fix docs build on owlbot.py (#888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: fix docs build on owlbot.py * 🦉 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 --- .kokoro/publish-docs.sh | 3 +++ owlbot.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index 233205d580e..297b14ac906 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -58,5 +58,8 @@ python3.10 -m docuploader create-metadata \ cat docs.metadata +# Replace toc.yml template file +mv docs/templates/toc.yml docs/_build/html/docfx_yaml/toc.yml + # upload docs python3.10 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}" diff --git a/owlbot.py b/owlbot.py index ddc578c3a25..f9d9410d6d9 100644 --- a/owlbot.py +++ b/owlbot.py @@ -112,14 +112,14 @@ re.escape("# upload docs") + "\n" + re.escape( - 'python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}"' + 'python3.10 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}"' ) ), ( "# Replace toc.yml template file\n" + "mv docs/templates/toc.yml docs/_build/html/docfx_yaml/toc.yml\n\n" + "# upload docs\n" - + 'python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}"' + + 'python3.10 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}"' ), ) From 3eb6a17a5823faf5ecba92cb9a554df74477871d Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 8 Aug 2024 12:03:30 -0700 Subject: [PATCH 749/921] docs: fix the `DisplayOptions` doc rendering (#893) --- third_party/bigframes_vendored/pandas/core/config_init.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/third_party/bigframes_vendored/pandas/core/config_init.py b/third_party/bigframes_vendored/pandas/core/config_init.py index 84ab90a322d..4bca3f3c751 100644 --- a/third_party/bigframes_vendored/pandas/core/config_init.py +++ b/third_party/bigframes_vendored/pandas/core/config_init.py @@ -18,6 +18,7 @@ **Examples:** Define Repr mode to "deferred" will prevent job execution in repr. + >>> import bigframes.pandas as bpd >>> df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") @@ -26,25 +27,32 @@ Computation deferred. Computation will process 28.9 kB Users can also get a dry run of the job by accessing the query_job property before they've run the job. This will return a dry run instance of the job they can inspect. + >>> df.query_job.total_bytes_processed 28947 User can execute the job by calling .to_pandas() + >>> # df.to_pandas() Reset repr_mode option + >>> bpd.options.display.repr_mode = "head" Can also set the progress_bar option to see the progress bar in terminal, + >>> bpd.options.display.progress_bar = "terminal" notebook, + >>> bpd.options.display.progress_bar = "notebook" or just remove it. + >>> bpd.options.display.progress_bar = None Setting to default value "auto" will detect and show progress bar automatically. + >>> bpd.options.display.progress_bar = "auto" Attributes: From 0c011a8212a8c0824a12c560b6d63048362275e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 8 Aug 2024 14:28:26 -0500 Subject: [PATCH 750/921] test: enable tests on Windows (#857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: enable tests on Windows * use windows subdirectory for scripts * maybe fix for windows * fix working directory * use nox so test dependencies are installed * add 3.10 to system tests * disable system tests * add more goto errors * 🦉 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 --- .kokoro/continuous/windows.cfg | 3 +++ .kokoro/presubmit/windows.cfg | 3 +++ noxfile.py | 6 +++-- scripts/windows/build.bat | 38 ++++++++++++++++++++++++++++++++ scripts/windows/test.bat | 40 ++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 .kokoro/continuous/windows.cfg create mode 100644 .kokoro/presubmit/windows.cfg create mode 100644 scripts/windows/build.bat create mode 100644 scripts/windows/test.bat diff --git a/.kokoro/continuous/windows.cfg b/.kokoro/continuous/windows.cfg new file mode 100644 index 00000000000..806986138dd --- /dev/null +++ b/.kokoro/continuous/windows.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "python-bigquery-dataframes/scripts/windows/build.bat" diff --git a/.kokoro/presubmit/windows.cfg b/.kokoro/presubmit/windows.cfg new file mode 100644 index 00000000000..806986138dd --- /dev/null +++ b/.kokoro/presubmit/windows.cfg @@ -0,0 +1,3 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "python-bigquery-dataframes/scripts/windows/build.bat" diff --git a/noxfile.py b/noxfile.py index d69c16e69cc..c464b47270f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -62,7 +62,8 @@ UNIT_TEST_EXTRAS: List[str] = [] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} -SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.12"] +# 3.10 is needed for Windows tests. +SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.12"] SYSTEM_TEST_STANDARD_DEPENDENCIES = [ "jinja2", "mock", @@ -100,7 +101,8 @@ "docfx", "unit", "unit_noextras", - "system", + "system-3.9", + "system-3.12", "cover", ] diff --git a/scripts/windows/build.bat b/scripts/windows/build.bat new file mode 100644 index 00000000000..d599702c98e --- /dev/null +++ b/scripts/windows/build.bat @@ -0,0 +1,38 @@ +@rem Copyright 2024 Google LLC +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. + +:; Change directory to repo root. +SET script_dir="%~dp0" +cd "%~dp0"\..\.. + +echo "Listing available Python versions' +py -0 || goto :error + +py -3.10 -m pip install --upgrade pip || goto :error +py -3.10 -m pip install --upgrade pip setuptools wheel || goto :error + +echo "Building Wheel" +py -3.10 -m pip wheel . --wheel-dir wheels || goto :error/ + +echo "Built wheel, now running tests." +call "%script_dir%"/test.bat 3.10 || goto :error + +echo "Windows build has completed successfully" + +:; https://stackoverflow.com/a/46813196/101923 +:; exit 0 +exit /b 0 + +:error +exit /b %errorlevel% diff --git a/scripts/windows/test.bat b/scripts/windows/test.bat new file mode 100644 index 00000000000..bcd605bd129 --- /dev/null +++ b/scripts/windows/test.bat @@ -0,0 +1,40 @@ +@rem Copyright 2024 Google LLC +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. + +@rem This test file runs for one Python version at a time, and is intended to +@rem be called from within the build loop. + +:; Change directory to repo root. +SET script_dir="%~dp0" +cd "%~dp0"\..\.. + +set PYTHON_VERSION=%1 +if "%PYTHON_VERSION%"=="" ( + echo "Python version was not provided, using Python 3.10" + set PYTHON_VERSION=3.10 +) + +py -%PYTHON_VERSION%-64 -m pip install nox || goto :error + +py -%PYTHON_VERSION%-64 -m nox -s unit-"%PYTHON_VERSION%" || goto :error + +:; TODO(b/358148440): enable system tests on windows +:; py -%PYTHON_VERSION%-64 -m nox -s system-"%PYTHON_VERSION%" || goto :error + +:; https://stackoverflow.com/a/46813196/101923 +:; exit 0 +exit /b 0 + +:error +exit /b %errorlevel% From 7117e33f2b6bc89ae8ce9b168d98bbcb21c08e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Thu, 8 Aug 2024 16:31:23 -0500 Subject: [PATCH 751/921] chore: require Windows unit tests and re-enable owlbot check (#895) --- .github/sync-repo-settings.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index e098468da69..c2f3673fccc 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -7,8 +7,7 @@ branchProtectionRules: requiresCodeOwnerReviews: true requiresStrictStatusChecks: false requiredStatusCheckContexts: -# TODO(b/347075426): Restore owlbot as required check -# - 'OwlBot Post Processor' + - 'OwlBot Post Processor' - 'conventionalcommits.org' - 'cla/google' - 'docs' @@ -19,6 +18,7 @@ branchProtectionRules: - 'unit (3.12)' - 'cover' - 'Kokoro presubmit' + - 'Kokoro windows' permissionRules: - team: actools-python permission: admin From e0b11bc8c038db7b950b1653ed4cd44a6246c713 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 9 Aug 2024 16:00:36 -0700 Subject: [PATCH 752/921] perf: Speed up compilation by reducing redundant type normalization (#896) --- bigframes/bigquery/__init__.py | 2 +- bigframes/core/compile/compiled.py | 23 +++-------------------- bigframes/core/compile/ibis_types.py | 1 + tests/system/small/bigquery/test_json.py | 20 +++++++++++--------- 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 7c409839b18..fb9503dc725 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -224,7 +224,7 @@ def json_extract( >>> s = bpd.Series(['{"class": {"students": [{"id": 5}, {"id": 12}]}}']) >>> bbq.json_extract(s, json_path="$.class") - 0 "{\\\"students\\\":[{\\\"id\\\":5},{\\\"id\\\":12}]}" + 0 {"students":[{"id":5},{"id":12}]} dtype: string Args: diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index cae527931cd..5492502f210 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -152,12 +152,7 @@ def _get_ibis_column(self, key: str) -> ibis_types.Value: raise ValueError( "Column name {} not in set of values: {}".format(key, self.column_ids) ) - return typing.cast( - ibis_types.Value, - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - self._column_names[key] - ), - ) + return typing.cast(ibis_types.Value, self._column_names[key]) def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: ibis_type = typing.cast( @@ -327,12 +322,7 @@ def _to_ibis_expr( if not columns: return ibis.memtable([]) - # Make sure all dtypes are the "canonical" ones for BigFrames. This is - # important for operations like UNION where the schema must match. - table = self._table.select( - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type(column) - for column in columns - ) + table = self._table.select(columns) base_table = table if self._reduced_predicate is not None: table = table.filter(base_table[PREDICATE_COLUMN]) @@ -1039,14 +1029,7 @@ def _to_ibis_expr( # Make sure we don't have any unbound (deferred) columns. table = self._table.select(columns) - # Make sure all dtypes are the "canonical" ones for BigFrames. This is - # important for operations like UNION where the schema must match. - table = table.select( - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - table[column] - ) - for column in table.columns - ) + table = table.select(table[column] for column in table.columns) base_table = table if self._reduced_predicate is not None: table = table.filter(base_table[PREDICATE_COLUMN]) diff --git a/bigframes/core/compile/ibis_types.py b/bigframes/core/compile/ibis_types.py index f3221f605fd..0b3038c9c7b 100644 --- a/bigframes/core/compile/ibis_types.py +++ b/bigframes/core/compile/ibis_types.py @@ -208,6 +208,7 @@ def ibis_value_to_canonical_type(value: ibis_types.Value) -> ibis_types.Value: name = value.get_name() if ibis_type.is_json(): value = vendored_ibis_ops.ToJsonString(value).to_expr() + value = value.case().when("null", ibis.null()).else_(value).end() return value.name(name) # Allow REQUIRED fields to be joined with NULLABLE fields. nullable_type = ibis_type.copy(nullable=True) diff --git a/tests/system/small/bigquery/test_json.py b/tests/system/small/bigquery/test_json.py index 059b8eea878..18ccadd9f5e 100644 --- a/tests/system/small/bigquery/test_json.py +++ b/tests/system/small/bigquery/test_json.py @@ -23,11 +23,13 @@ def _get_series_from_json(json_data): + # Note: converts None to sql "null" and not to json none. + values = [ + f"JSON '{json.dumps(data)}'" if data is not None else "NULL" + for data in json_data + ] sql = " UNION ALL ".join( - [ - f"SELECT {id} AS id, JSON '{json.dumps(data)}' AS data" - for id, data in enumerate(json_data) - ] + [f"SELECT {id} AS id, {value} AS data" for id, value in enumerate(values)] ) df = bpd.read_gbq(sql).set_index("id").sort_index() return df["data"] @@ -114,19 +116,19 @@ def test_json_set_w_invalid_series_type(): def test_json_extract_from_json(): s = _get_series_from_json([{"a": {"b": [1, 2]}}, {"a": {"c": 1}}, {"a": {"b": 0}}]) - actual = bbq.json_extract(s, "$.a.b") + actual = bbq.json_extract(s, "$.a.b").to_pandas() # After the introduction of the JSON type, the output should be a JSON-formatted series. - expected = _get_series_from_json(["[1,2]", None, "0"]) + expected = _get_series_from_json([[1, 2], None, 0]).to_pandas() pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual, + expected, ) def test_json_extract_from_string(): s = bpd.Series(['{"a": {"b": [1, 2]}}', '{"a": {"c": 1}}', '{"a": {"b": 0}}']) actual = bbq.json_extract(s, "$.a.b") - expected = _get_series_from_json(["[1,2]", None, "0"]) + expected = _get_series_from_json([[1, 2], None, 0]) pd.testing.assert_series_equal( actual.to_pandas(), expected.to_pandas(), From 991bb0a25e1e424de38abd065f9d79ab20c24ed2 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 12 Aug 2024 14:32:53 -0700 Subject: [PATCH 753/921] refactor: reorganize `remote_function` code for readability (#885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: reorganize `remote_function` code for readability * refactor out RemoteFunctionSession * rename newly introduced modules to make them private symbolically --------- Co-authored-by: Tim Sweña (Swast) --- .../functions/_remote_function_client.py | 476 +++++++ .../functions/_remote_function_session.py | 546 ++++++++ bigframes/functions/_utils.py | 214 +++ bigframes/functions/remote_function.py | 1174 +---------------- bigframes/pandas/__init__.py | 4 +- bigframes/session/__init__.py | 3 +- tests/system/large/test_remote_function.py | 10 +- tests/system/small/test_remote_function.py | 7 +- tests/system/utils.py | 4 +- 9 files changed, 1261 insertions(+), 1177 deletions(-) create mode 100644 bigframes/functions/_remote_function_client.py create mode 100644 bigframes/functions/_remote_function_session.py create mode 100644 bigframes/functions/_utils.py diff --git a/bigframes/functions/_remote_function_client.py b/bigframes/functions/_remote_function_client.py new file mode 100644 index 00000000000..6ef482ecda3 --- /dev/null +++ b/bigframes/functions/_remote_function_client.py @@ -0,0 +1,476 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import inspect +import logging +import os +import random +import shutil +import string +import sys +import tempfile +from typing import cast, Tuple, TYPE_CHECKING + +import requests + +from bigframes import constants +import bigframes.functions.remote_function_template + +if TYPE_CHECKING: + from bigframes.session import Session + +import google.api_core.exceptions +import google.api_core.retry +from google.cloud import bigquery, functions_v2 + +from . import _utils + +logger = logging.getLogger(__name__) + + +class RemoteFunctionClient: + # Wait time (in seconds) for an IAM binding to take effect after creation + _iam_wait_seconds = 120 + + def __init__( + self, + gcp_project_id, + cloud_function_region, + cloud_functions_client, + bq_location, + bq_dataset, + bq_client, + bq_connection_id, + bq_connection_manager, + cloud_function_service_account, + cloud_function_kms_key_name, + cloud_function_docker_repository, + *, + session: Session, + ): + self._gcp_project_id = gcp_project_id + self._cloud_function_region = cloud_function_region + self._cloud_functions_client = cloud_functions_client + self._bq_location = bq_location + self._bq_dataset = bq_dataset + self._bq_client = bq_client + self._bq_connection_id = bq_connection_id + self._bq_connection_manager = bq_connection_manager + self._cloud_function_service_account = cloud_function_service_account + self._cloud_function_kms_key_name = cloud_function_kms_key_name + self._cloud_function_docker_repository = cloud_function_docker_repository + self._session = session + + def create_bq_remote_function( + self, + input_args, + input_types, + output_type, + endpoint, + bq_function_name, + max_batching_rows, + ): + """Create a BigQuery remote function given the artifacts of a user defined + function and the http endpoint of a corresponding cloud function.""" + if self._bq_connection_manager: + self._bq_connection_manager.create_bq_connection( + self._gcp_project_id, + self._bq_location, + self._bq_connection_id, + "run.invoker", + ) + + # Create BQ function + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 + bq_function_args = [] + bq_function_return_type = output_type + + # We are expecting the input type annotations to be 1:1 with the input args + for name, type_ in zip(input_args, input_types): + bq_function_args.append(f"{name} {type_}") + + remote_function_options = { + "endpoint": endpoint, + "max_batching_rows": max_batching_rows, + } + + remote_function_options_str = ", ".join( + [ + f'{key}="{val}"' if isinstance(val, str) else f"{key}={val}" + for key, val in remote_function_options.items() + if val is not None + ] + ) + + create_function_ddl = f""" + CREATE OR REPLACE FUNCTION `{self._gcp_project_id}.{self._bq_dataset}`.{bq_function_name}({','.join(bq_function_args)}) + RETURNS {bq_function_return_type} + REMOTE WITH CONNECTION `{self._gcp_project_id}.{self._bq_location}.{self._bq_connection_id}` + OPTIONS ({remote_function_options_str})""" + + logger.info(f"Creating BQ remote function: {create_function_ddl}") + + # Make sure the dataset exists. I.e. if it doesn't exist, go ahead and + # create it + dataset = bigquery.Dataset( + bigquery.DatasetReference.from_string( + self._bq_dataset, default_project=self._gcp_project_id + ) + ) + dataset.location = self._bq_location + try: + # This check does not require bigquery.datasets.create IAM + # permission. So, if the data set already exists, then user can work + # without having that permission. + self._bq_client.get_dataset(dataset) + except google.api_core.exceptions.NotFound: + # This requires bigquery.datasets.create IAM permission + self._bq_client.create_dataset(dataset, exists_ok=True) + + # TODO(swast): plumb through the original, user-facing api_name. + _, query_job = self._session._start_query(create_function_ddl) + logger.info(f"Created remote function {query_job.ddl_target_routine}") + + def get_cloud_function_fully_qualified_parent(self): + "Get the fully qualilfied parent for a cloud function." + return self._cloud_functions_client.common_location_path( + self._gcp_project_id, self._cloud_function_region + ) + + def get_cloud_function_fully_qualified_name(self, name): + "Get the fully qualilfied name for a cloud function." + return self._cloud_functions_client.function_path( + self._gcp_project_id, self._cloud_function_region, name + ) + + def get_remote_function_fully_qualilfied_name(self, name): + "Get the fully qualilfied name for a BQ remote function." + return f"{self._gcp_project_id}.{self._bq_dataset}.{name}" + + def get_cloud_function_endpoint(self, name): + """Get the http endpoint of a cloud function if it exists.""" + fully_qualified_name = self.get_cloud_function_fully_qualified_name(name) + try: + response = self._cloud_functions_client.get_function( + name=fully_qualified_name + ) + return response.service_config.uri + except google.api_core.exceptions.NotFound: + pass + return None + + def generate_cloud_function_code( + self, + def_, + directory, + *, + input_types: Tuple[str], + output_type: str, + package_requirements=None, + is_row_processor=False, + ): + """Generate the cloud function code for a given user defined function. + + Args: + input_types (tuple[str]): + Types of the input arguments in BigQuery SQL data type names. + output_type (str): + Types of the output scalar as a BigQuery SQL data type name. + """ + + # requirements.txt + if package_requirements: + requirements_txt = os.path.join(directory, "requirements.txt") + with open(requirements_txt, "w") as f: + f.write("\n".join(package_requirements)) + + # main.py + entry_point = bigframes.functions.remote_function_template.generate_cloud_function_main_code( + def_, + directory, + input_types=input_types, + output_type=output_type, + is_row_processor=is_row_processor, + ) + return entry_point + + def create_cloud_function( + self, + def_, + cf_name, + *, + input_types: Tuple[str], + output_type: str, + package_requirements=None, + timeout_seconds=600, + max_instance_count=None, + is_row_processor=False, + vpc_connector=None, + memory_mib=1024, + ): + """Create a cloud function from the given user defined function. + + Args: + input_types (tuple[str]): + Types of the input arguments in BigQuery SQL data type names. + output_type (str): + Types of the output scalar as a BigQuery SQL data type name. + """ + + # Build and deploy folder structure containing cloud function + with tempfile.TemporaryDirectory() as directory: + entry_point = self.generate_cloud_function_code( + def_, + directory, + package_requirements=package_requirements, + input_types=input_types, + output_type=output_type, + is_row_processor=is_row_processor, + ) + archive_path = shutil.make_archive(directory, "zip", directory) + + # We are creating cloud function source code from the currently running + # python version. Use the same version to deploy. This is necessary + # because cloudpickle serialization done in one python version and + # deserialization done in another python version doesn't work. + # TODO(shobs): Figure out how to achieve version compatibility, specially + # when pickle (internally used by cloudpickle) guarantees that: + # https://docs.python.org/3/library/pickle.html#:~:text=The%20pickle%20serialization%20format%20is,unique%20breaking%20change%20language%20boundary. + python_version = "python{}{}".format( + sys.version_info.major, sys.version_info.minor + ) + + # Determine an upload URL for user code + upload_url_request = functions_v2.GenerateUploadUrlRequest( + kms_key_name=self._cloud_function_kms_key_name + ) + upload_url_request.parent = self.get_cloud_function_fully_qualified_parent() + upload_url_response = self._cloud_functions_client.generate_upload_url( + request=upload_url_request + ) + + # Upload the code to GCS + with open(archive_path, "rb") as f: + response = requests.put( + upload_url_response.upload_url, + data=f, + headers={"content-type": "application/zip"}, + ) + if response.status_code != 200: + raise RuntimeError( + "Failed to upload user code. code={}, reason={}, text={}".format( + response.status_code, response.reason, response.text + ) + ) + + # Deploy Cloud Function + create_function_request = functions_v2.CreateFunctionRequest() + create_function_request.parent = ( + self.get_cloud_function_fully_qualified_parent() + ) + create_function_request.function_id = cf_name + function = functions_v2.Function() + function.name = self.get_cloud_function_fully_qualified_name(cf_name) + function.build_config = functions_v2.BuildConfig() + function.build_config.runtime = python_version + function.build_config.entry_point = entry_point + function.build_config.source = functions_v2.Source() + function.build_config.source.storage_source = functions_v2.StorageSource() + function.build_config.source.storage_source.bucket = ( + upload_url_response.storage_source.bucket + ) + function.build_config.source.storage_source.object_ = ( + upload_url_response.storage_source.object_ + ) + function.build_config.docker_repository = ( + self._cloud_function_docker_repository + ) + function.service_config = functions_v2.ServiceConfig() + if memory_mib is not None: + function.service_config.available_memory = f"{memory_mib}Mi" + if timeout_seconds is not None: + if timeout_seconds > 1200: + raise ValueError( + "BigQuery remote function can wait only up to 20 minutes" + ", see for more details " + "https://cloud.google.com/bigquery/quotas#remote_function_limits." + ) + function.service_config.timeout_seconds = timeout_seconds + if max_instance_count is not None: + function.service_config.max_instance_count = max_instance_count + if vpc_connector is not None: + function.service_config.vpc_connector = vpc_connector + function.service_config.service_account_email = ( + self._cloud_function_service_account + ) + function.kms_key_name = self._cloud_function_kms_key_name + create_function_request.function = function + + # Create the cloud function and wait for it to be ready to use + try: + operation = self._cloud_functions_client.create_function( + request=create_function_request + ) + operation.result() + + # Cleanup + os.remove(archive_path) + except google.api_core.exceptions.AlreadyExists: + # If a cloud function with the same name already exists, let's + # update it + update_function_request = functions_v2.UpdateFunctionRequest() + update_function_request.function = function + operation = self._cloud_functions_client.update_function( + request=update_function_request + ) + operation.result() + + # Fetch the endpoint of the just created function + endpoint = self.get_cloud_function_endpoint(cf_name) + if not endpoint: + raise ValueError( + f"Couldn't fetch the http endpoint. {constants.FEEDBACK_LINK}" + ) + + logger.info( + f"Successfully created cloud function {cf_name} with uri ({endpoint})" + ) + return endpoint + + def provision_bq_remote_function( + self, + def_, + input_types, + output_type, + reuse, + name, + package_requirements, + max_batching_rows, + cloud_function_timeout, + cloud_function_max_instance_count, + is_row_processor, + cloud_function_vpc_connector, + cloud_function_memory_mib, + ): + """Provision a BigQuery remote function.""" + # Augment user package requirements with any internal package + # requirements + package_requirements = _utils._get_updated_package_requirements( + package_requirements, is_row_processor + ) + + # Compute a unique hash representing the user code + function_hash = _utils._get_hash(def_, package_requirements) + + # If reuse of any existing function with the same name (indicated by the + # same hash of its source code) is not intended, then attach a unique + # suffix to the intended function name to make it unique. + uniq_suffix = None + if not reuse: + # use 4 digits as a unique suffix which should suffice for + # uniqueness per session + uniq_suffix = "".join( + random.choices(string.ascii_lowercase + string.digits, k=4) + ) + + # Derive the name of the cloud function underlying the intended BQ + # remote function. Use the session id to identify the GCF for unnamed + # functions. The named remote functions are treated as a persistant + # artifacts, so let's keep them independent of session id, which also + # makes their naming more stable for the same udf code + session_id = None if name else self._session.session_id + cloud_function_name = _utils.get_cloud_function_name( + function_hash, session_id, uniq_suffix + ) + cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) + + # Create the cloud function if it does not exist + if not cf_endpoint: + cf_endpoint = self.create_cloud_function( + def_, + cloud_function_name, + input_types=input_types, + output_type=output_type, + package_requirements=package_requirements, + timeout_seconds=cloud_function_timeout, + max_instance_count=cloud_function_max_instance_count, + is_row_processor=is_row_processor, + vpc_connector=cloud_function_vpc_connector, + memory_mib=cloud_function_memory_mib, + ) + else: + logger.info(f"Cloud function {cloud_function_name} already exists.") + + # Derive the name of the remote function + remote_function_name = name + if not remote_function_name: + remote_function_name = _utils.get_remote_function_name( + function_hash, self._session.session_id, uniq_suffix + ) + rf_endpoint, rf_conn = self.get_remote_function_specs(remote_function_name) + + # Create the BQ remote function in following circumstances: + # 1. It does not exist + # 2. It exists but the existing remote function has different + # configuration than intended + created_new = False + if not rf_endpoint or ( + rf_endpoint != cf_endpoint or rf_conn != self._bq_connection_id + ): + input_args = inspect.getargs(def_.__code__).args + if len(input_args) != len(input_types): + raise ValueError( + "Exactly one type should be provided for every input arg." + ) + self.create_bq_remote_function( + input_args, + input_types, + output_type, + cf_endpoint, + remote_function_name, + max_batching_rows, + ) + + created_new = True + else: + logger.info(f"Remote function {remote_function_name} already exists.") + + return remote_function_name, cloud_function_name, created_new + + def get_remote_function_specs(self, remote_function_name): + """Check whether a remote function already exists for the udf.""" + http_endpoint = None + bq_connection = None + routines = self._bq_client.list_routines( + f"{self._gcp_project_id}.{self._bq_dataset}" + ) + try: + for routine in routines: + routine = cast(bigquery.Routine, routine) + if routine.reference.routine_id == remote_function_name: + rf_options = routine.remote_function_options + if rf_options: + http_endpoint = rf_options.endpoint + bq_connection = rf_options.connection + if bq_connection: + bq_connection = os.path.basename(bq_connection) + break + except google.api_core.exceptions.NotFound: + # The dataset might not exist, in which case the http_endpoint doesn't, either. + # Note: list_routines doesn't make an API request until we iterate on the response object. + pass + return (http_endpoint, bq_connection) diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py new file mode 100644 index 00000000000..0ab19ca353c --- /dev/null +++ b/bigframes/functions/_remote_function_session.py @@ -0,0 +1,546 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import collections.abc +import inspect +import sys +import threading +from typing import Any, cast, Dict, Mapping, Optional, Sequence, TYPE_CHECKING, Union +import warnings + +import google.api_core.exceptions +from google.cloud import ( + bigquery, + bigquery_connection_v1, + functions_v2, + resourcemanager_v3, +) + +from bigframes import clients, constants + +if TYPE_CHECKING: + from bigframes.session import Session + +import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes +import ibis +import pandas + +from . import _remote_function_client as rf_client +from . import _utils + + +class RemoteFunctionSession: + """Session to manage remote functions.""" + + def __init__(self): + # Session level mapping of remote function artifacts + self._temp_artifacts: Dict[str, str] = dict() + + # Lock to synchronize the update of the session artifacts + self._artifacts_lock = threading.Lock() + + def _update_temp_artifacts(self, bqrf_routine: str, gcf_path: str): + """Update remote function artifacts in the current session.""" + with self._artifacts_lock: + self._temp_artifacts[bqrf_routine] = gcf_path + + def clean_up( + self, + bqclient: bigquery.Client, + gcfclient: functions_v2.FunctionServiceClient, + session_id: str, + ): + """Delete remote function artifacts in the current session.""" + with self._artifacts_lock: + for bqrf_routine, gcf_path in self._temp_artifacts.items(): + # Let's accept the possibility that the remote function may have + # been deleted directly by the user + bqclient.delete_routine(bqrf_routine, not_found_ok=True) + + # Let's accept the possibility that the cloud function may have + # been deleted directly by the user + try: + gcfclient.delete_function(name=gcf_path) + except google.api_core.exceptions.NotFound: + pass + + self._temp_artifacts.clear() + + # Inspired by @udf decorator implemented in ibis-bigquery package + # https://github.com/ibis-project/ibis-bigquery/blob/main/ibis_bigquery/udf/__init__.py + # which has moved as @js to the ibis package + # https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py + def remote_function( + self, + input_types: Union[None, type, Sequence[type]] = None, + output_type: Optional[type] = None, + session: Optional[Session] = None, + bigquery_client: Optional[bigquery.Client] = None, + bigquery_connection_client: Optional[ + bigquery_connection_v1.ConnectionServiceClient + ] = None, + cloud_functions_client: Optional[functions_v2.FunctionServiceClient] = None, + resource_manager_client: Optional[resourcemanager_v3.ProjectsClient] = None, + dataset: Optional[str] = None, + bigquery_connection: Optional[str] = None, + reuse: bool = True, + name: Optional[str] = None, + packages: Optional[Sequence[str]] = None, + cloud_function_service_account: Optional[str] = None, + cloud_function_kms_key_name: Optional[str] = None, + cloud_function_docker_repository: Optional[str] = None, + max_batching_rows: Optional[int] = 1000, + cloud_function_timeout: Optional[int] = 600, + cloud_function_max_instances: Optional[int] = None, + cloud_function_vpc_connector: Optional[str] = None, + cloud_function_memory_mib: Optional[int] = 1024, + ): + """Decorator to turn a user defined function into a BigQuery remote function. + + .. deprecated:: 0.0.1 + This is an internal method. Please use :func:`bigframes.pandas.remote_function` instead. + + .. note:: + Please make sure following is setup before using this API: + + 1. Have the below APIs enabled for your project: + + * BigQuery Connection API + * Cloud Functions API + * Cloud Run API + * Cloud Build API + * Artifact Registry API + * Cloud Resource Manager API + + This can be done from the cloud console (change `PROJECT_ID` to yours): + https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID + + Or from the gcloud CLI: + + `$ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com` + + 2. Have following IAM roles enabled for you: + + * BigQuery Data Editor (roles/bigquery.dataEditor) + * BigQuery Connection Admin (roles/bigquery.connectionAdmin) + * Cloud Functions Developer (roles/cloudfunctions.developer) + * Service Account User (roles/iam.serviceAccountUser) on the service account `PROJECT_NUMBER-compute@developer.gserviceaccount.com` + * Storage Object Viewer (roles/storage.objectViewer) + * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) + + 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: + + 1. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection + 2. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function + + Alternatively, the IAM could also be setup via the gcloud CLI: + + `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. + + Args: + input_types (None, type, or sequence(type)): + For scalar user defined function it should be the input type or + sequence of input types. For row processing user defined function, + type `Series` should be specified. + output_type (Optional[type]): + Data type of the output in the user defined function. + session (bigframes.Session, Optional): + BigQuery DataFrames session to use for getting default project, + dataset and BigQuery connection. + bigquery_client (google.cloud.bigquery.Client, Optional): + Client to use for BigQuery operations. If this param is not provided + then bigquery client from the session would be used. + bigquery_connection_client (google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional): + Client to use for BigQuery connection operations. If this param is + not provided then bigquery connection client from the session would + be used. + cloud_functions_client (google.cloud.functions_v2.FunctionServiceClient, Optional): + Client to use for cloud functions operations. If this param is not + provided then the functions client from the session would be used. + resource_manager_client (google.cloud.resourcemanager_v3.ProjectsClient, Optional): + Client to use for cloud resource management operations, e.g. for + getting and setting IAM roles on cloud resources. If this param is + not provided then resource manager client from the session would be + used. + dataset (str, Optional.): + Dataset in which to create a BigQuery remote function. It should be in + `.` or `` format. If this + parameter is not provided then session dataset id is used. + bigquery_connection (str, Optional): + Name of the BigQuery connection in the form of `CONNECTION_ID` or + `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID`. + If this param is not provided then the bigquery connection from the session + would be used. If it is pre created in the same location as the + `bigquery_client.location` then it would be used, otherwise it is created + dynamically using the `bigquery_connection_client` assuming the user has necessary + priviliges. The PROJECT_ID should be the same as the BigQuery connection project. + reuse (bool, Optional): + Reuse the remote function if already exists. + `True` by default, which will result in reusing an existing remote + function and corresponding cloud function that was previously + created (if any) for the same udf. + Please note that for an unnamed (i.e. created without an explicit + `name` argument) remote function, the BigQuery DataFrames + session id is attached in the cloud artifacts names. So for the + effective reuse across the sessions it is recommended to create + the remote function with an explicit `name`. + Setting it to `False` would force creating a unique remote function. + If the required remote function does not exist then it would be + created irrespective of this param. + name (str, Optional): + Explicit name of the persisted BigQuery remote function. Use it with + caution, because two users working in the same project and dataset + could overwrite each other's remote functions if they use the same + persistent name. When an explicit name is provided, any session + specific clean up (``bigframes.session.Session.close``/ + ``bigframes.pandas.close_session``/ + ``bigframes.pandas.reset_session``/ + ``bigframes.pandas.clean_up_by_session_id``) does not clean up + the function, and leaves it for the user to manage the function + and the associated cloud function directly. + packages (str[], Optional): + Explicit name of the external package dependencies. Each dependency + is added to the `requirements.txt` as is, and can be of the form + supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. + cloud_function_service_account (str, Optional): + Service account to use for the cloud functions. If not provided then + the default service account would be used. See + https://cloud.google.com/functions/docs/securing/function-identity + for more details. Please make sure the service account has the + necessary IAM permissions configured as described in + https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. + cloud_function_kms_key_name (str, Optional): + Customer managed encryption key to protect cloud functions and + related data at rest. This is of the format + projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY. + Read https://cloud.google.com/functions/docs/securing/cmek for + more details including granting necessary service accounts + access to the key. + cloud_function_docker_repository (str, Optional): + Docker repository created with the same encryption key as + `cloud_function_kms_key_name` to store encrypted artifacts + created to support the cloud function. This is of the format + projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. + For more details see + https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. + max_batching_rows (int, Optional): + The maximum number of rows to be batched for processing in the + BQ remote function. Default value is 1000. A lower number can be + passed to avoid timeouts in case the user code is too complex to + process large number of rows fast enough. A higher number can be + used to increase throughput in case the user code is fast enough. + `None` can be passed to let BQ remote functions service apply + default batching. See for more details + https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. + cloud_function_timeout (int, Optional): + The maximum amount of time (in seconds) BigQuery should wait for + the cloud function to return a response. See for more details + https://cloud.google.com/functions/docs/configuring/timeout. + Please note that even though the cloud function (2nd gen) itself + allows seeting up to 60 minutes of timeout, BigQuery remote + function can wait only up to 20 minutes, see for more details + https://cloud.google.com/bigquery/quotas#remote_function_limits. + By default BigQuery DataFrames uses a 10 minute timeout. `None` + can be passed to let the cloud functions default timeout take effect. + cloud_function_max_instances (int, Optional): + The maximumm instance count for the cloud function created. This + can be used to control how many cloud function instances can be + active at max at any given point of time. Lower setting can help + control the spike in the billing. Higher setting can help + support processing larger scale data. When not specified, cloud + function's default setting applies. For more details see + https://cloud.google.com/functions/docs/configuring/max-instances. + cloud_function_vpc_connector (str, Optional): + The VPC connector you would like to configure for your cloud + function. This is useful if your code needs access to data or + service(s) that are on a VPC network. See for more details + https://cloud.google.com/functions/docs/networking/connecting-vpc. + cloud_function_memory_mib (int, Optional): + The amounts of memory (in mebibytes) to allocate for the cloud + function (2nd gen) created. This also dictates a corresponding + amount of allocated CPU for the function. By default a memory of + 1024 MiB is set for the cloud functions created to support + BigQuery DataFrames remote function. If you want to let the + default memory of cloud functions be allocated, pass `None`. See + for more details + https://cloud.google.com/functions/docs/configuring/memory. + """ + # Some defaults may be used from the session if not provided otherwise + import bigframes.exceptions as bf_exceptions + import bigframes.pandas as bpd + import bigframes.series as bf_series + import bigframes.session + + session = cast(bigframes.session.Session, session or bpd.get_global_session()) + + # A BigQuery client is required to perform BQ operations + if not bigquery_client: + bigquery_client = session.bqclient + if not bigquery_client: + raise ValueError( + "A bigquery client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A BigQuery connection client is required to perform BQ connection operations + if not bigquery_connection_client: + bigquery_connection_client = session.bqconnectionclient + if not bigquery_connection_client: + raise ValueError( + "A bigquery connection client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A cloud functions client is required to perform cloud functions operations + if not cloud_functions_client: + cloud_functions_client = session.cloudfunctionsclient + if not cloud_functions_client: + raise ValueError( + "A cloud functions client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # A resource manager client is required to get/set IAM operations + if not resource_manager_client: + resource_manager_client = session.resourcemanagerclient + if not resource_manager_client: + raise ValueError( + "A resource manager client must be provided, either directly or via session. " + f"{constants.FEEDBACK_LINK}" + ) + + # BQ remote function must be persisted, for which we need a dataset + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#:~:text=You%20cannot%20create%20temporary%20remote%20functions. + if dataset: + dataset_ref = bigquery.DatasetReference.from_string( + dataset, default_project=bigquery_client.project + ) + else: + dataset_ref = session._anonymous_dataset + + bq_location, cloud_function_region = _utils.get_remote_function_locations( + bigquery_client.location + ) + + # A connection is required for BQ remote function + # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function + if not bigquery_connection: + bigquery_connection = session._bq_connection # type: ignore + + bigquery_connection = clients.resolve_full_bq_connection_name( + bigquery_connection, + default_project=dataset_ref.project, + default_location=bq_location, + ) + # Guaranteed to be the form of .. + ( + gcp_project_id, + bq_connection_location, + bq_connection_id, + ) = bigquery_connection.split(".") + if gcp_project_id.casefold() != dataset_ref.project.casefold(): + raise ValueError( + "The project_id does not match BigQuery connection gcp_project_id: " + f"{dataset_ref.project}." + ) + if bq_connection_location.casefold() != bq_location.casefold(): + raise ValueError( + "The location does not match BigQuery connection location: " + f"{bq_location}." + ) + + # If any CMEK is intended then check that a docker repository is also specified + if ( + cloud_function_kms_key_name is not None + and cloud_function_docker_repository is None + ): + raise ValueError( + "cloud_function_docker_repository must be specified with cloud_function_kms_key_name." + " For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin" + ) + + bq_connection_manager = session.bqconnectionmanager + + def wrapper(func): + nonlocal input_types, output_type + + if not callable(func): + raise TypeError("f must be callable, got {}".format(func)) + + if sys.version_info >= (3, 10): + # Add `eval_str = True` so that deferred annotations are turned into their + # corresponding type objects. Need Python 3.10 for eval_str parameter. + # https://docs.python.org/3/library/inspect.html#inspect.signature + signature_kwargs: Mapping[str, Any] = {"eval_str": True} + else: + signature_kwargs = {} + + signature = inspect.signature( + func, + **signature_kwargs, + ) + + # Try to get input types via type annotations. + if input_types is None: + input_types = [] + for parameter in signature.parameters.values(): + if (param_type := parameter.annotation) is inspect.Signature.empty: + raise ValueError( + "'input_types' was not set and parameter " + f"'{parameter.name}' is missing a type annotation. " + "Types are required to use @remote_function." + ) + input_types.append(param_type) + elif not isinstance(input_types, collections.abc.Sequence): + input_types = [input_types] + + if output_type is None: + if ( + output_type := signature.return_annotation + ) is inspect.Signature.empty: + raise ValueError( + "'output_type' was not set and function is missing a " + "return type annotation. Types are required to use " + "@remote_function." + ) + + # The function will actually be receiving a pandas Series, but allow both + # BigQuery DataFrames and pandas object types for compatibility. + is_row_processor = False + if len(input_types) == 1 and ( + (input_type := input_types[0]) == bf_series.Series + or input_type == pandas.Series + ): + warnings.warn( + "input_types=Series is in preview.", + stacklevel=1, + category=bf_exceptions.PreviewWarning, + ) + + # we will model the row as a json serialized string containing the data + # and the metadata representing the row + input_types = [str] + is_row_processor = True + elif isinstance(input_types, type): + input_types = [input_types] + + # TODO(b/340898611): fix type error + ibis_signature = _utils.ibis_signature_from_python_signature( + signature, input_types, output_type # type: ignore + ) + + remote_function_client = rf_client.RemoteFunctionClient( + dataset_ref.project, + cloud_function_region, + cloud_functions_client, + bq_location, + dataset_ref.dataset_id, + bigquery_client, + bq_connection_id, + bq_connection_manager, + cloud_function_service_account, + cloud_function_kms_key_name, + cloud_function_docker_repository, + session=session, # type: ignore + ) + + # In the unlikely case where the user is trying to re-deploy the same + # function, cleanup the attributes we add below, first. This prevents + # the pickle from having dependencies that might not otherwise be + # present such as ibis or pandas. + def try_delattr(attr): + try: + delattr(func, attr) + except AttributeError: + pass + + try_delattr("bigframes_cloud_function") + try_delattr("bigframes_remote_function") + try_delattr("input_dtypes") + try_delattr("output_dtype") + try_delattr("is_row_processor") + try_delattr("ibis_node") + + ( + rf_name, + cf_name, + created_new, + ) = remote_function_client.provision_bq_remote_function( + func, + input_types=tuple( + third_party_ibis_bqtypes.BigQueryType.from_ibis(type_) + for type_ in ibis_signature.input_types + ), + output_type=third_party_ibis_bqtypes.BigQueryType.from_ibis( + ibis_signature.output_type + ), + reuse=reuse, + name=name, + package_requirements=packages, + max_batching_rows=max_batching_rows, + cloud_function_timeout=cloud_function_timeout, + cloud_function_max_instance_count=cloud_function_max_instances, + is_row_processor=is_row_processor, + cloud_function_vpc_connector=cloud_function_vpc_connector, + cloud_function_memory_mib=cloud_function_memory_mib, + ) + + # TODO: Move ibis logic to compiler step + node = ibis.udf.scalar.builtin( + func, + name=rf_name, + schema=f"{dataset_ref.project}.{dataset_ref.dataset_id}", + signature=(ibis_signature.input_types, ibis_signature.output_type), + ) + func.bigframes_cloud_function = ( + remote_function_client.get_cloud_function_fully_qualified_name(cf_name) + ) + func.bigframes_remote_function = ( + remote_function_client.get_remote_function_fully_qualilfied_name( + rf_name + ) + ) + func.input_dtypes = tuple( + [ + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( + input_type + ) + for input_type in ibis_signature.input_types + ] + ) + func.output_dtype = ( + bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( + ibis_signature.output_type + ) + ) + func.is_row_processor = is_row_processor + func.ibis_node = node + + # If a new remote function was created, update the cloud artifacts + # created in the session. This would be used to clean up any + # resources in the session. Note that we need to do this only for + # the case where an explicit name was not provided by the user and + # we used an internal name. For the cases where the user provided an + # explicit name, we are assuming that the user wants to persist them + # with that name and would directly manage their lifecycle. + if created_new and (not name): + self._update_temp_artifacts( + func.bigframes_remote_function, func.bigframes_cloud_function + ) + return func + + return wrapper diff --git a/bigframes/functions/_utils.py b/bigframes/functions/_utils.py new file mode 100644 index 00000000000..537473bed8a --- /dev/null +++ b/bigframes/functions/_utils.py @@ -0,0 +1,214 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import hashlib +import inspect +from typing import cast, List, NamedTuple, Optional, Sequence, Set + +import cloudpickle +import google.api_core.exceptions +from google.cloud import bigquery, functions_v2 +import ibis.expr.datatypes.core +import numpy +import pandas +import pyarrow + +import bigframes.core.compile.ibis_types + +# Naming convention for the remote function artifacts +_BIGFRAMES_REMOTE_FUNCTION_PREFIX = "bigframes" +_BQ_FUNCTION_NAME_SEPERATOR = "_" +_GCF_FUNCTION_NAME_SEPERATOR = "-" + +# Protocol version 4 is available in python version 3.4 and above +# https://docs.python.org/3/library/pickle.html#data-stream-format +_pickle_protocol_version = 4 + + +def get_remote_function_locations(bq_location): + """Get BQ location and cloud functions region given a BQ client.""" + # TODO(shobs, b/274647164): Find the best way to determine default location. + # For now let's assume that if no BQ location is set in the client then it + # defaults to US multi region + bq_location = bq_location.lower() if bq_location else "us" + + # Cloud function should be in the same region as the bigquery remote function + cloud_function_region = bq_location + + # BigQuery has multi region but cloud functions does not. + # Any region in the multi region that supports cloud functions should work + # https://cloud.google.com/functions/docs/locations + if bq_location == "us": + cloud_function_region = "us-central1" + elif bq_location == "eu": + cloud_function_region = "europe-west1" + + return bq_location, cloud_function_region + + +def _get_updated_package_requirements( + package_requirements=None, is_row_processor=False +): + requirements = [f"cloudpickle=={cloudpickle.__version__}"] + if is_row_processor: + # bigframes remote function will send an entire row of data as json, + # which would be converted to a pandas series and processed + # Ensure numpy versions match to avoid unpickling problems. See + # internal issue b/347934471. + requirements.append(f"numpy=={numpy.__version__}") + requirements.append(f"pandas=={pandas.__version__}") + requirements.append(f"pyarrow=={pyarrow.__version__}") + + if package_requirements: + requirements.extend(package_requirements) + + requirements = sorted(requirements) + return requirements + + +def _clean_up_by_session_id( + bqclient: bigquery.Client, + gcfclient: functions_v2.FunctionServiceClient, + dataset: bigquery.DatasetReference, + session_id: str, +): + """Delete remote function artifacts for a session id, where the session id + was not necessarily created in the current runtime. This is useful if the + user worked with a BigQuery DataFrames session previously and remembered the + session id, and now wants to clean up its temporary resources at a later + point in time. + """ + + # First clean up the BQ remote functions and then the underlying + # cloud functions, so that at no point we are left with a remote function + # that is pointing to a cloud function that does not exist + + endpoints_to_be_deleted: Set[str] = set() + match_prefix = "".join( + [ + _BIGFRAMES_REMOTE_FUNCTION_PREFIX, + _BQ_FUNCTION_NAME_SEPERATOR, + session_id, + _BQ_FUNCTION_NAME_SEPERATOR, + ] + ) + for routine in bqclient.list_routines(dataset): + routine = cast(bigquery.Routine, routine) + + # skip past the routines not belonging to the given session id, or + # non-remote-function routines + if ( + routine.type_ != bigquery.RoutineType.SCALAR_FUNCTION + or not cast(str, routine.routine_id).startswith(match_prefix) + or not routine.remote_function_options + or not routine.remote_function_options.endpoint + ): + continue + + # Let's forgive the edge case possibility that the BQ remote function + # may have been deleted at the same time directly by the user + bqclient.delete_routine(routine, not_found_ok=True) + endpoints_to_be_deleted.add(routine.remote_function_options.endpoint) + + # Now clean up the cloud functions + bq_location = bqclient.get_dataset(dataset).location + bq_location, gcf_location = get_remote_function_locations(bq_location) + parent_path = gcfclient.common_location_path( + project=dataset.project, location=gcf_location + ) + for gcf in gcfclient.list_functions(parent=parent_path): + # skip past the cloud functions not attached to any BQ remote function + # belonging to the given session id + if gcf.service_config.uri not in endpoints_to_be_deleted: + continue + + # Let's forgive the edge case possibility that the cloud function + # may have been deleted at the same time directly by the user + try: + gcfclient.delete_function(name=gcf.name) + except google.api_core.exceptions.NotFound: + pass + + +def _get_hash(def_, package_requirements=None): + "Get hash (32 digits alphanumeric) of a function." + # There is a known cell-id sensitivity of the cloudpickle serialization in + # notebooks https://github.com/cloudpipe/cloudpickle/issues/538. Because of + # this, if a cell contains a udf decorated with @remote_function, a unique + # cloudpickle code is generated every time the cell is run, creating new + # cloud artifacts every time. This is slow and wasteful. + # A workaround of the same can be achieved by replacing the filename in the + # code object to a static value + # https://github.com/cloudpipe/cloudpickle/issues/120#issuecomment-338510661. + # + # To respect the user code/environment let's make this modification on a + # copy of the udf, not on the original udf itself. + def_copy = cloudpickle.loads(cloudpickle.dumps(def_)) + def_copy.__code__ = def_copy.__code__.replace( + co_filename="bigframes_place_holder_filename" + ) + + def_repr = cloudpickle.dumps(def_copy, protocol=_pickle_protocol_version) + if package_requirements: + for p in sorted(package_requirements): + def_repr += p.encode() + return hashlib.md5(def_repr).hexdigest() + + +def routine_ref_to_string_for_query(routine_ref: bigquery.RoutineReference) -> str: + return f"`{routine_ref.project}.{routine_ref.dataset_id}`.{routine_ref.routine_id}" + + +def get_cloud_function_name(function_hash, session_id=None, uniq_suffix=None): + "Get a name for the cloud function for the given user defined function." + parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX] + if session_id: + parts.append(session_id) + parts.append(function_hash) + if uniq_suffix: + parts.append(uniq_suffix) + return _GCF_FUNCTION_NAME_SEPERATOR.join(parts) + + +def get_remote_function_name(function_hash, session_id, uniq_suffix=None): + "Get a name for the BQ remote function for the given user defined function." + parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX, session_id, function_hash] + if uniq_suffix: + parts.append(uniq_suffix) + return _BQ_FUNCTION_NAME_SEPERATOR.join(parts) + + +class IbisSignature(NamedTuple): + parameter_names: List[str] + input_types: List[Optional[ibis.expr.datatypes.core.DataType]] + output_type: ibis.expr.datatypes.core.DataType + + +def ibis_signature_from_python_signature( + signature: inspect.Signature, + input_types: Sequence[type], + output_type: type, +) -> IbisSignature: + + return IbisSignature( + parameter_names=list(signature.parameters.keys()), + input_types=[ + bigframes.core.compile.ibis_types.ibis_type_from_python_type(t) + for t in input_types + ], + output_type=bigframes.core.compile.ibis_types.ibis_type_from_python_type( + output_type + ), + ) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index b3c6aee1b32..7e9df74e763 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -14,664 +14,29 @@ from __future__ import annotations -import collections.abc -import hashlib -import inspect import logging -import os -import random -import shutil -import string -import sys -import tempfile -import threading -from typing import ( - Any, - cast, - Dict, - List, - Mapping, - NamedTuple, - Optional, - Sequence, - Set, - Tuple, - TYPE_CHECKING, - Union, -) +from typing import cast, Optional, TYPE_CHECKING import warnings import ibis -import numpy -import pandas -import pyarrow -import requests if TYPE_CHECKING: from bigframes.session import Session -import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes -import cloudpickle import google.api_core.exceptions import google.api_core.retry -from google.cloud import ( - bigquery, - bigquery_connection_v1, - functions_v2, - resourcemanager_v3, -) +from google.cloud import bigquery import google.iam.v1 -from ibis.expr.datatypes.core import DataType as IbisDataType -from bigframes import clients import bigframes.constants as constants import bigframes.core.compile.ibis_types import bigframes.dtypes import bigframes.functions.remote_function_template -logger = logging.getLogger(__name__) - -# Naming convention for the remote function artifacts -_BIGFRAMES_REMOTE_FUNCTION_PREFIX = "bigframes" -_BQ_FUNCTION_NAME_SEPERATOR = "_" -_GCF_FUNCTION_NAME_SEPERATOR = "-" - -# Protocol version 4 is available in python version 3.4 and above -# https://docs.python.org/3/library/pickle.html#data-stream-format -_pickle_protocol_version = 4 - - -def _clean_up_by_session_id( - bqclient: bigquery.Client, - gcfclient: functions_v2.FunctionServiceClient, - dataset: bigquery.DatasetReference, - session_id: str, -): - """Delete remote function artifacts for a session id, where the session id - was not necessarily created in the current runtime. This is useful if the - user worked with a BigQuery DataFrames session previously and remembered the - session id, and now wants to clean up its temporary resources at a later - point in time. - """ - - # First clean up the BQ remote functions and then the underlying - # cloud functions, so that at no point we are left with a remote function - # that is pointing to a cloud function that does not exist - - endpoints_to_be_deleted: Set[str] = set() - match_prefix = "".join( - [ - _BIGFRAMES_REMOTE_FUNCTION_PREFIX, - _BQ_FUNCTION_NAME_SEPERATOR, - session_id, - _BQ_FUNCTION_NAME_SEPERATOR, - ] - ) - for routine in bqclient.list_routines(dataset): - routine = cast(bigquery.Routine, routine) - - # skip past the routines not belonging to the given session id, or - # non-remote-function routines - if ( - routine.type_ != bigquery.RoutineType.SCALAR_FUNCTION - or not cast(str, routine.routine_id).startswith(match_prefix) - or not routine.remote_function_options - or not routine.remote_function_options.endpoint - ): - continue - - # Let's forgive the edge case possibility that the BQ remote function - # may have been deleted at the same time directly by the user - bqclient.delete_routine(routine, not_found_ok=True) - endpoints_to_be_deleted.add(routine.remote_function_options.endpoint) - - # Now clean up the cloud functions - bq_location = bqclient.get_dataset(dataset).location - bq_location, gcf_location = get_remote_function_locations(bq_location) - parent_path = gcfclient.common_location_path( - project=dataset.project, location=gcf_location - ) - for gcf in gcfclient.list_functions(parent=parent_path): - # skip past the cloud functions not attached to any BQ remote function - # belonging to the given session id - if gcf.service_config.uri not in endpoints_to_be_deleted: - continue - - # Let's forgive the edge case possibility that the cloud function - # may have been deleted at the same time directly by the user - try: - gcfclient.delete_function(name=gcf.name) - except google.api_core.exceptions.NotFound: - pass - - -def get_remote_function_locations(bq_location): - """Get BQ location and cloud functions region given a BQ client.""" - # TODO(shobs, b/274647164): Find the best way to determine default location. - # For now let's assume that if no BQ location is set in the client then it - # defaults to US multi region - bq_location = bq_location.lower() if bq_location else "us" - - # Cloud function should be in the same region as the bigquery remote function - cloud_function_region = bq_location - - # BigQuery has multi region but cloud functions does not. - # Any region in the multi region that supports cloud functions should work - # https://cloud.google.com/functions/docs/locations - if bq_location == "us": - cloud_function_region = "us-central1" - elif bq_location == "eu": - cloud_function_region = "europe-west1" - - return bq_location, cloud_function_region - - -def _get_hash(def_, package_requirements=None): - "Get hash (32 digits alphanumeric) of a function." - # There is a known cell-id sensitivity of the cloudpickle serialization in - # notebooks https://github.com/cloudpipe/cloudpickle/issues/538. Because of - # this, if a cell contains a udf decorated with @remote_function, a unique - # cloudpickle code is generated every time the cell is run, creating new - # cloud artifacts every time. This is slow and wasteful. - # A workaround of the same can be achieved by replacing the filename in the - # code object to a static value - # https://github.com/cloudpipe/cloudpickle/issues/120#issuecomment-338510661. - # - # To respect the user code/environment let's make this modification on a - # copy of the udf, not on the original udf itself. - def_copy = cloudpickle.loads(cloudpickle.dumps(def_)) - def_copy.__code__ = def_copy.__code__.replace( - co_filename="bigframes_place_holder_filename" - ) - - def_repr = cloudpickle.dumps(def_copy, protocol=_pickle_protocol_version) - if package_requirements: - for p in sorted(package_requirements): - def_repr += p.encode() - return hashlib.md5(def_repr).hexdigest() - - -def _get_updated_package_requirements( - package_requirements=None, is_row_processor=False -): - requirements = [f"cloudpickle=={cloudpickle.__version__}"] - if is_row_processor: - # bigframes remote function will send an entire row of data as json, - # which would be converted to a pandas series and processed - # Ensure numpy versions match to avoid unpickling problems. See - # internal issue b/347934471. - requirements.append(f"numpy=={numpy.__version__}") - requirements.append(f"pandas=={pandas.__version__}") - requirements.append(f"pyarrow=={pyarrow.__version__}") - - if package_requirements: - requirements.extend(package_requirements) - - requirements = sorted(requirements) - return requirements - - -def routine_ref_to_string_for_query(routine_ref: bigquery.RoutineReference) -> str: - return f"`{routine_ref.project}.{routine_ref.dataset_id}`.{routine_ref.routine_id}" - - -class IbisSignature(NamedTuple): - parameter_names: List[str] - input_types: List[Optional[IbisDataType]] - output_type: IbisDataType - - -def get_cloud_function_name(function_hash, session_id=None, uniq_suffix=None): - "Get a name for the cloud function for the given user defined function." - parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX] - if session_id: - parts.append(session_id) - parts.append(function_hash) - if uniq_suffix: - parts.append(uniq_suffix) - return _GCF_FUNCTION_NAME_SEPERATOR.join(parts) - - -def get_remote_function_name(function_hash, session_id, uniq_suffix=None): - "Get a name for the BQ remote function for the given user defined function." - parts = [_BIGFRAMES_REMOTE_FUNCTION_PREFIX, session_id, function_hash] - if uniq_suffix: - parts.append(uniq_suffix) - return _BQ_FUNCTION_NAME_SEPERATOR.join(parts) - - -class RemoteFunctionClient: - # Wait time (in seconds) for an IAM binding to take effect after creation - _iam_wait_seconds = 120 - - def __init__( - self, - gcp_project_id, - cloud_function_region, - cloud_functions_client, - bq_location, - bq_dataset, - bq_client, - bq_connection_id, - bq_connection_manager, - cloud_function_service_account, - cloud_function_kms_key_name, - cloud_function_docker_repository, - *, - session: Session, - ): - self._gcp_project_id = gcp_project_id - self._cloud_function_region = cloud_function_region - self._cloud_functions_client = cloud_functions_client - self._bq_location = bq_location - self._bq_dataset = bq_dataset - self._bq_client = bq_client - self._bq_connection_id = bq_connection_id - self._bq_connection_manager = bq_connection_manager - self._cloud_function_service_account = cloud_function_service_account - self._cloud_function_kms_key_name = cloud_function_kms_key_name - self._cloud_function_docker_repository = cloud_function_docker_repository - self._session = session - - def create_bq_remote_function( - self, - input_args, - input_types, - output_type, - endpoint, - bq_function_name, - max_batching_rows, - ): - """Create a BigQuery remote function given the artifacts of a user defined - function and the http endpoint of a corresponding cloud function.""" - if self._bq_connection_manager: - self._bq_connection_manager.create_bq_connection( - self._gcp_project_id, - self._bq_location, - self._bq_connection_id, - "run.invoker", - ) - - # Create BQ function - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function_2 - bq_function_args = [] - bq_function_return_type = output_type - - # We are expecting the input type annotations to be 1:1 with the input args - for name, type_ in zip(input_args, input_types): - bq_function_args.append(f"{name} {type_}") - - remote_function_options = { - "endpoint": endpoint, - "max_batching_rows": max_batching_rows, - } - - remote_function_options_str = ", ".join( - [ - f'{key}="{val}"' if isinstance(val, str) else f"{key}={val}" - for key, val in remote_function_options.items() - if val is not None - ] - ) - - create_function_ddl = f""" - CREATE OR REPLACE FUNCTION `{self._gcp_project_id}.{self._bq_dataset}`.{bq_function_name}({','.join(bq_function_args)}) - RETURNS {bq_function_return_type} - REMOTE WITH CONNECTION `{self._gcp_project_id}.{self._bq_location}.{self._bq_connection_id}` - OPTIONS ({remote_function_options_str})""" - - logger.info(f"Creating BQ remote function: {create_function_ddl}") - - # Make sure the dataset exists. I.e. if it doesn't exist, go ahead and - # create it - dataset = bigquery.Dataset( - bigquery.DatasetReference.from_string( - self._bq_dataset, default_project=self._gcp_project_id - ) - ) - dataset.location = self._bq_location - try: - # This check does not require bigquery.datasets.create IAM - # permission. So, if the data set already exists, then user can work - # without having that permission. - self._bq_client.get_dataset(dataset) - except google.api_core.exceptions.NotFound: - # This requires bigquery.datasets.create IAM permission - self._bq_client.create_dataset(dataset, exists_ok=True) - - # TODO(swast): plumb through the original, user-facing api_name. - _, query_job = self._session._start_query(create_function_ddl) - logger.info(f"Created remote function {query_job.ddl_target_routine}") - - def get_cloud_function_fully_qualified_parent(self): - "Get the fully qualilfied parent for a cloud function." - return self._cloud_functions_client.common_location_path( - self._gcp_project_id, self._cloud_function_region - ) - - def get_cloud_function_fully_qualified_name(self, name): - "Get the fully qualilfied name for a cloud function." - return self._cloud_functions_client.function_path( - self._gcp_project_id, self._cloud_function_region, name - ) - - def get_remote_function_fully_qualilfied_name(self, name): - "Get the fully qualilfied name for a BQ remote function." - return f"{self._gcp_project_id}.{self._bq_dataset}.{name}" - - def get_cloud_function_endpoint(self, name): - """Get the http endpoint of a cloud function if it exists.""" - fully_qualified_name = self.get_cloud_function_fully_qualified_name(name) - try: - response = self._cloud_functions_client.get_function( - name=fully_qualified_name - ) - return response.service_config.uri - except google.api_core.exceptions.NotFound: - pass - return None - - def generate_cloud_function_code( - self, - def_, - directory, - *, - input_types: Tuple[str], - output_type: str, - package_requirements=None, - is_row_processor=False, - ): - """Generate the cloud function code for a given user defined function. - - Args: - input_types (tuple[str]): - Types of the input arguments in BigQuery SQL data type names. - output_type (str): - Types of the output scalar as a BigQuery SQL data type name. - """ - - # requirements.txt - if package_requirements: - requirements_txt = os.path.join(directory, "requirements.txt") - with open(requirements_txt, "w") as f: - f.write("\n".join(package_requirements)) - - # main.py - entry_point = bigframes.functions.remote_function_template.generate_cloud_function_main_code( - def_, - directory, - input_types=input_types, - output_type=output_type, - is_row_processor=is_row_processor, - ) - return entry_point - - def create_cloud_function( - self, - def_, - cf_name, - *, - input_types: Tuple[str], - output_type: str, - package_requirements=None, - timeout_seconds=600, - max_instance_count=None, - is_row_processor=False, - vpc_connector=None, - memory_mib=1024, - ): - """Create a cloud function from the given user defined function. - - Args: - input_types (tuple[str]): - Types of the input arguments in BigQuery SQL data type names. - output_type (str): - Types of the output scalar as a BigQuery SQL data type name. - """ - - # Build and deploy folder structure containing cloud function - with tempfile.TemporaryDirectory() as directory: - entry_point = self.generate_cloud_function_code( - def_, - directory, - package_requirements=package_requirements, - input_types=input_types, - output_type=output_type, - is_row_processor=is_row_processor, - ) - archive_path = shutil.make_archive(directory, "zip", directory) - - # We are creating cloud function source code from the currently running - # python version. Use the same version to deploy. This is necessary - # because cloudpickle serialization done in one python version and - # deserialization done in another python version doesn't work. - # TODO(shobs): Figure out how to achieve version compatibility, specially - # when pickle (internally used by cloudpickle) guarantees that: - # https://docs.python.org/3/library/pickle.html#:~:text=The%20pickle%20serialization%20format%20is,unique%20breaking%20change%20language%20boundary. - python_version = "python{}{}".format( - sys.version_info.major, sys.version_info.minor - ) - - # Determine an upload URL for user code - upload_url_request = functions_v2.GenerateUploadUrlRequest( - kms_key_name=self._cloud_function_kms_key_name - ) - upload_url_request.parent = self.get_cloud_function_fully_qualified_parent() - upload_url_response = self._cloud_functions_client.generate_upload_url( - request=upload_url_request - ) - - # Upload the code to GCS - with open(archive_path, "rb") as f: - response = requests.put( - upload_url_response.upload_url, - data=f, - headers={"content-type": "application/zip"}, - ) - if response.status_code != 200: - raise RuntimeError( - "Failed to upload user code. code={}, reason={}, text={}".format( - response.status_code, response.reason, response.text - ) - ) - - # Deploy Cloud Function - create_function_request = functions_v2.CreateFunctionRequest() - create_function_request.parent = ( - self.get_cloud_function_fully_qualified_parent() - ) - create_function_request.function_id = cf_name - function = functions_v2.Function() - function.name = self.get_cloud_function_fully_qualified_name(cf_name) - function.build_config = functions_v2.BuildConfig() - function.build_config.runtime = python_version - function.build_config.entry_point = entry_point - function.build_config.source = functions_v2.Source() - function.build_config.source.storage_source = functions_v2.StorageSource() - function.build_config.source.storage_source.bucket = ( - upload_url_response.storage_source.bucket - ) - function.build_config.source.storage_source.object_ = ( - upload_url_response.storage_source.object_ - ) - function.build_config.docker_repository = ( - self._cloud_function_docker_repository - ) - function.service_config = functions_v2.ServiceConfig() - if memory_mib is not None: - function.service_config.available_memory = f"{memory_mib}Mi" - if timeout_seconds is not None: - if timeout_seconds > 1200: - raise ValueError( - "BigQuery remote function can wait only up to 20 minutes" - ", see for more details " - "https://cloud.google.com/bigquery/quotas#remote_function_limits." - ) - function.service_config.timeout_seconds = timeout_seconds - if max_instance_count is not None: - function.service_config.max_instance_count = max_instance_count - if vpc_connector is not None: - function.service_config.vpc_connector = vpc_connector - function.service_config.service_account_email = ( - self._cloud_function_service_account - ) - function.kms_key_name = self._cloud_function_kms_key_name - create_function_request.function = function - - # Create the cloud function and wait for it to be ready to use - try: - operation = self._cloud_functions_client.create_function( - request=create_function_request - ) - operation.result() - - # Cleanup - os.remove(archive_path) - except google.api_core.exceptions.AlreadyExists: - # If a cloud function with the same name already exists, let's - # update it - update_function_request = functions_v2.UpdateFunctionRequest() - update_function_request.function = function - operation = self._cloud_functions_client.update_function( - request=update_function_request - ) - operation.result() - - # Fetch the endpoint of the just created function - endpoint = self.get_cloud_function_endpoint(cf_name) - if not endpoint: - raise ValueError( - f"Couldn't fetch the http endpoint. {constants.FEEDBACK_LINK}" - ) - - logger.info( - f"Successfully created cloud function {cf_name} with uri ({endpoint})" - ) - return endpoint - - def provision_bq_remote_function( - self, - def_, - input_types, - output_type, - reuse, - name, - package_requirements, - max_batching_rows, - cloud_function_timeout, - cloud_function_max_instance_count, - is_row_processor, - cloud_function_vpc_connector, - cloud_function_memory_mib, - ): - """Provision a BigQuery remote function.""" - # Augment user package requirements with any internal package - # requirements - package_requirements = _get_updated_package_requirements( - package_requirements, is_row_processor - ) - - # Compute a unique hash representing the user code - function_hash = _get_hash(def_, package_requirements) - - # If reuse of any existing function with the same name (indicated by the - # same hash of its source code) is not intended, then attach a unique - # suffix to the intended function name to make it unique. - uniq_suffix = None - if not reuse: - # use 4 digits as a unique suffix which should suffice for - # uniqueness per session - uniq_suffix = "".join( - random.choices(string.ascii_lowercase + string.digits, k=4) - ) - - # Derive the name of the cloud function underlying the intended BQ - # remote function. Use the session id to identify the GCF for unnamed - # functions. The named remote functions are treated as a persistant - # artifacts, so let's keep them independent of session id, which also - # makes their naming more stable for the same udf code - session_id = None if name else self._session.session_id - cloud_function_name = get_cloud_function_name( - function_hash, session_id, uniq_suffix - ) - cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) - - # Create the cloud function if it does not exist - if not cf_endpoint: - cf_endpoint = self.create_cloud_function( - def_, - cloud_function_name, - input_types=input_types, - output_type=output_type, - package_requirements=package_requirements, - timeout_seconds=cloud_function_timeout, - max_instance_count=cloud_function_max_instance_count, - is_row_processor=is_row_processor, - vpc_connector=cloud_function_vpc_connector, - memory_mib=cloud_function_memory_mib, - ) - else: - logger.info(f"Cloud function {cloud_function_name} already exists.") - - # Derive the name of the remote function - remote_function_name = name - if not remote_function_name: - remote_function_name = get_remote_function_name( - function_hash, self._session.session_id, uniq_suffix - ) - rf_endpoint, rf_conn = self.get_remote_function_specs(remote_function_name) - - # Create the BQ remote function in following circumstances: - # 1. It does not exist - # 2. It exists but the existing remote function has different - # configuration than intended - created_new = False - if not rf_endpoint or ( - rf_endpoint != cf_endpoint or rf_conn != self._bq_connection_id - ): - input_args = inspect.getargs(def_.__code__).args - if len(input_args) != len(input_types): - raise ValueError( - "Exactly one type should be provided for every input arg." - ) - self.create_bq_remote_function( - input_args, - input_types, - output_type, - cf_endpoint, - remote_function_name, - max_batching_rows, - ) - - created_new = True - else: - logger.info(f"Remote function {remote_function_name} already exists.") - - return remote_function_name, cloud_function_name, created_new +from . import _remote_function_session as rf_session +from . import _utils - def get_remote_function_specs(self, remote_function_name): - """Check whether a remote function already exists for the udf.""" - http_endpoint = None - bq_connection = None - routines = self._bq_client.list_routines( - f"{self._gcp_project_id}.{self._bq_dataset}" - ) - try: - for routine in routines: - routine = cast(bigquery.Routine, routine) - if routine.reference.routine_id == remote_function_name: - rf_options = routine.remote_function_options - if rf_options: - http_endpoint = rf_options.endpoint - bq_connection = rf_options.connection - if bq_connection: - bq_connection = os.path.basename(bq_connection) - break - except google.api_core.exceptions.NotFound: - # The dataset might not exist, in which case the http_endpoint doesn't, either. - # Note: list_routines doesn't make an API request until we iterate on the response object. - pass - return (http_endpoint, bq_connection) +logger = logging.getLogger(__name__) class UnsupportedTypeError(ValueError): @@ -680,34 +45,16 @@ def __init__(self, type_, supported_types): self.supported_types = supported_types -def ibis_signature_from_python_signature( - signature: inspect.Signature, - input_types: Sequence[type], - output_type: type, -) -> IbisSignature: - - return IbisSignature( - parameter_names=list(signature.parameters.keys()), - input_types=[ - bigframes.core.compile.ibis_types.ibis_type_from_python_type(t) - for t in input_types - ], - output_type=bigframes.core.compile.ibis_types.ibis_type_from_python_type( - output_type - ), - ) - - class ReturnTypeMissingError(ValueError): pass # TODO: Move this to compile folder -def ibis_signature_from_routine(routine: bigquery.Routine) -> IbisSignature: +def ibis_signature_from_routine(routine: bigquery.Routine) -> _utils.IbisSignature: if not routine.return_type: raise ReturnTypeMissingError - return IbisSignature( + return _utils.IbisSignature( parameter_names=[arg.name for arg in routine.arguments], input_types=[ bigframes.core.compile.ibis_types.ibis_type_from_type_kind( @@ -748,515 +95,12 @@ def get_routine_reference( return dataset_ref.routine(routine_ref_str) -class _RemoteFunctionSession: - """Session to manage remote functions.""" - - def __init__(self): - # Session level mapping of remote function artifacts - self._temp_artifacts: Dict[str, str] = dict() - - # Lock to synchronize the update of the session artifacts - self._artifacts_lock = threading.Lock() - - def _update_temp_artifacts(self, bqrf_routine: str, gcf_path: str): - """Update remote function artifacts in the current session.""" - with self._artifacts_lock: - self._temp_artifacts[bqrf_routine] = gcf_path - - def clean_up( - self, - bqclient: bigquery.Client, - gcfclient: functions_v2.FunctionServiceClient, - session_id: str, - ): - """Delete remote function artifacts in the current session.""" - with self._artifacts_lock: - for bqrf_routine, gcf_path in self._temp_artifacts.items(): - # Let's accept the possibility that the remote function may have - # been deleted directly by the user - bqclient.delete_routine(bqrf_routine, not_found_ok=True) - - # Let's accept the possibility that the cloud function may have - # been deleted directly by the user - try: - gcfclient.delete_function(name=gcf_path) - except google.api_core.exceptions.NotFound: - pass - - self._temp_artifacts.clear() - - # Inspired by @udf decorator implemented in ibis-bigquery package - # https://github.com/ibis-project/ibis-bigquery/blob/main/ibis_bigquery/udf/__init__.py - # which has moved as @js to the ibis package - # https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/udf/__init__.py - def remote_function( - self, - input_types: Union[None, type, Sequence[type]] = None, - output_type: Optional[type] = None, - session: Optional[Session] = None, - bigquery_client: Optional[bigquery.Client] = None, - bigquery_connection_client: Optional[ - bigquery_connection_v1.ConnectionServiceClient - ] = None, - cloud_functions_client: Optional[functions_v2.FunctionServiceClient] = None, - resource_manager_client: Optional[resourcemanager_v3.ProjectsClient] = None, - dataset: Optional[str] = None, - bigquery_connection: Optional[str] = None, - reuse: bool = True, - name: Optional[str] = None, - packages: Optional[Sequence[str]] = None, - cloud_function_service_account: Optional[str] = None, - cloud_function_kms_key_name: Optional[str] = None, - cloud_function_docker_repository: Optional[str] = None, - max_batching_rows: Optional[int] = 1000, - cloud_function_timeout: Optional[int] = 600, - cloud_function_max_instances: Optional[int] = None, - cloud_function_vpc_connector: Optional[str] = None, - cloud_function_memory_mib: Optional[int] = 1024, - ): - """Decorator to turn a user defined function into a BigQuery remote function. - - .. deprecated:: 0.0.1 - This is an internal method. Please use :func:`bigframes.pandas.remote_function` instead. - - .. note:: - Please make sure following is setup before using this API: - - 1. Have the below APIs enabled for your project: - - * BigQuery Connection API - * Cloud Functions API - * Cloud Run API - * Cloud Build API - * Artifact Registry API - * Cloud Resource Manager API - - This can be done from the cloud console (change `PROJECT_ID` to yours): - https://console.cloud.google.com/apis/enableflow?apiid=bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,cloudbuild.googleapis.com,artifactregistry.googleapis.com,cloudresourcemanager.googleapis.com&project=PROJECT_ID - - Or from the gcloud CLI: - - `$ gcloud services enable bigqueryconnection.googleapis.com cloudfunctions.googleapis.com run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudresourcemanager.googleapis.com` - - 2. Have following IAM roles enabled for you: - - * BigQuery Data Editor (roles/bigquery.dataEditor) - * BigQuery Connection Admin (roles/bigquery.connectionAdmin) - * Cloud Functions Developer (roles/cloudfunctions.developer) - * Service Account User (roles/iam.serviceAccountUser) on the service account `PROJECT_NUMBER-compute@developer.gserviceaccount.com` - * Storage Object Viewer (roles/storage.objectViewer) - * Project IAM Admin (roles/resourcemanager.projectIamAdmin) (Only required if the bigquery connection being used is not pre-created and is created dynamically with user credentials.) - - 3. Either the user has setIamPolicy privilege on the project, or a BigQuery connection is pre-created with necessary IAM role set: - - 1. To create a connection, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_connection - 2. To set up IAM, follow https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function - - Alternatively, the IAM could also be setup via the gcloud CLI: - - `$ gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:CONNECTION_SERVICE_ACCOUNT_ID" --role="roles/run.invoker"`. - - Args: - input_types (None, type, or sequence(type)): - For scalar user defined function it should be the input type or - sequence of input types. For row processing user defined function, - type `Series` should be specified. - output_type (Optional[type]): - Data type of the output in the user defined function. - session (bigframes.Session, Optional): - BigQuery DataFrames session to use for getting default project, - dataset and BigQuery connection. - bigquery_client (google.cloud.bigquery.Client, Optional): - Client to use for BigQuery operations. If this param is not provided - then bigquery client from the session would be used. - bigquery_connection_client (google.cloud.bigquery_connection_v1.ConnectionServiceClient, Optional): - Client to use for BigQuery connection operations. If this param is - not provided then bigquery connection client from the session would - be used. - cloud_functions_client (google.cloud.functions_v2.FunctionServiceClient, Optional): - Client to use for cloud functions operations. If this param is not - provided then the functions client from the session would be used. - resource_manager_client (google.cloud.resourcemanager_v3.ProjectsClient, Optional): - Client to use for cloud resource management operations, e.g. for - getting and setting IAM roles on cloud resources. If this param is - not provided then resource manager client from the session would be - used. - dataset (str, Optional.): - Dataset in which to create a BigQuery remote function. It should be in - `.` or `` format. If this - parameter is not provided then session dataset id is used. - bigquery_connection (str, Optional): - Name of the BigQuery connection in the form of `CONNECTION_ID` or - `LOCATION.CONNECTION_ID` or `PROJECT_ID.LOCATION.CONNECTION_ID`. - If this param is not provided then the bigquery connection from the session - would be used. If it is pre created in the same location as the - `bigquery_client.location` then it would be used, otherwise it is created - dynamically using the `bigquery_connection_client` assuming the user has necessary - priviliges. The PROJECT_ID should be the same as the BigQuery connection project. - reuse (bool, Optional): - Reuse the remote function if already exists. - `True` by default, which will result in reusing an existing remote - function and corresponding cloud function that was previously - created (if any) for the same udf. - Please note that for an unnamed (i.e. created without an explicit - `name` argument) remote function, the BigQuery DataFrames - session id is attached in the cloud artifacts names. So for the - effective reuse across the sessions it is recommended to create - the remote function with an explicit `name`. - Setting it to `False` would force creating a unique remote function. - If the required remote function does not exist then it would be - created irrespective of this param. - name (str, Optional): - Explicit name of the persisted BigQuery remote function. Use it with - caution, because two users working in the same project and dataset - could overwrite each other's remote functions if they use the same - persistent name. When an explicit name is provided, any session - specific clean up (``bigframes.session.Session.close``/ - ``bigframes.pandas.close_session``/ - ``bigframes.pandas.reset_session``/ - ``bigframes.pandas.clean_up_by_session_id``) does not clean up - the function, and leaves it for the user to manage the function - and the associated cloud function directly. - packages (str[], Optional): - Explicit name of the external package dependencies. Each dependency - is added to the `requirements.txt` as is, and can be of the form - supported in https://pip.pypa.io/en/stable/reference/requirements-file-format/. - cloud_function_service_account (str, Optional): - Service account to use for the cloud functions. If not provided then - the default service account would be used. See - https://cloud.google.com/functions/docs/securing/function-identity - for more details. Please make sure the service account has the - necessary IAM permissions configured as described in - https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration. - cloud_function_kms_key_name (str, Optional): - Customer managed encryption key to protect cloud functions and - related data at rest. This is of the format - projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY. - Read https://cloud.google.com/functions/docs/securing/cmek for - more details including granting necessary service accounts - access to the key. - cloud_function_docker_repository (str, Optional): - Docker repository created with the same encryption key as - `cloud_function_kms_key_name` to store encrypted artifacts - created to support the cloud function. This is of the format - projects/PROJECT_ID/locations/LOCATION/repositories/REPOSITORY_NAME. - For more details see - https://cloud.google.com/functions/docs/securing/cmek#before_you_begin. - max_batching_rows (int, Optional): - The maximum number of rows to be batched for processing in the - BQ remote function. Default value is 1000. A lower number can be - passed to avoid timeouts in case the user code is too complex to - process large number of rows fast enough. A higher number can be - used to increase throughput in case the user code is fast enough. - `None` can be passed to let BQ remote functions service apply - default batching. See for more details - https://cloud.google.com/bigquery/docs/remote-functions#limiting_number_of_rows_in_a_batch_request. - cloud_function_timeout (int, Optional): - The maximum amount of time (in seconds) BigQuery should wait for - the cloud function to return a response. See for more details - https://cloud.google.com/functions/docs/configuring/timeout. - Please note that even though the cloud function (2nd gen) itself - allows seeting up to 60 minutes of timeout, BigQuery remote - function can wait only up to 20 minutes, see for more details - https://cloud.google.com/bigquery/quotas#remote_function_limits. - By default BigQuery DataFrames uses a 10 minute timeout. `None` - can be passed to let the cloud functions default timeout take effect. - cloud_function_max_instances (int, Optional): - The maximumm instance count for the cloud function created. This - can be used to control how many cloud function instances can be - active at max at any given point of time. Lower setting can help - control the spike in the billing. Higher setting can help - support processing larger scale data. When not specified, cloud - function's default setting applies. For more details see - https://cloud.google.com/functions/docs/configuring/max-instances. - cloud_function_vpc_connector (str, Optional): - The VPC connector you would like to configure for your cloud - function. This is useful if your code needs access to data or - service(s) that are on a VPC network. See for more details - https://cloud.google.com/functions/docs/networking/connecting-vpc. - cloud_function_memory_mib (int, Optional): - The amounts of memory (in mebibytes) to allocate for the cloud - function (2nd gen) created. This also dictates a corresponding - amount of allocated CPU for the function. By default a memory of - 1024 MiB is set for the cloud functions created to support - BigQuery DataFrames remote function. If you want to let the - default memory of cloud functions be allocated, pass `None`. See - for more details - https://cloud.google.com/functions/docs/configuring/memory. - """ - # Some defaults may be used from the session if not provided otherwise - import bigframes.exceptions as bf_exceptions - import bigframes.pandas as bpd - import bigframes.series as bf_series - import bigframes.session - - session = cast(bigframes.session.Session, session or bpd.get_global_session()) - - # A BigQuery client is required to perform BQ operations - if not bigquery_client: - bigquery_client = session.bqclient - if not bigquery_client: - raise ValueError( - "A bigquery client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) - - # A BigQuery connection client is required to perform BQ connection operations - if not bigquery_connection_client: - bigquery_connection_client = session.bqconnectionclient - if not bigquery_connection_client: - raise ValueError( - "A bigquery connection client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) - - # A cloud functions client is required to perform cloud functions operations - if not cloud_functions_client: - cloud_functions_client = session.cloudfunctionsclient - if not cloud_functions_client: - raise ValueError( - "A cloud functions client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) - - # A resource manager client is required to get/set IAM operations - if not resource_manager_client: - resource_manager_client = session.resourcemanagerclient - if not resource_manager_client: - raise ValueError( - "A resource manager client must be provided, either directly or via session. " - f"{constants.FEEDBACK_LINK}" - ) - - # BQ remote function must be persisted, for which we need a dataset - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#:~:text=You%20cannot%20create%20temporary%20remote%20functions. - if dataset: - dataset_ref = bigquery.DatasetReference.from_string( - dataset, default_project=bigquery_client.project - ) - else: - dataset_ref = session._anonymous_dataset - - bq_location, cloud_function_region = get_remote_function_locations( - bigquery_client.location - ) - - # A connection is required for BQ remote function - # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#create_a_remote_function - if not bigquery_connection: - bigquery_connection = session._bq_connection # type: ignore - - bigquery_connection = clients.resolve_full_bq_connection_name( - bigquery_connection, - default_project=dataset_ref.project, - default_location=bq_location, - ) - # Guaranteed to be the form of .. - ( - gcp_project_id, - bq_connection_location, - bq_connection_id, - ) = bigquery_connection.split(".") - if gcp_project_id.casefold() != dataset_ref.project.casefold(): - raise ValueError( - "The project_id does not match BigQuery connection gcp_project_id: " - f"{dataset_ref.project}." - ) - if bq_connection_location.casefold() != bq_location.casefold(): - raise ValueError( - "The location does not match BigQuery connection location: " - f"{bq_location}." - ) - - # If any CMEK is intended then check that a docker repository is also specified - if ( - cloud_function_kms_key_name is not None - and cloud_function_docker_repository is None - ): - raise ValueError( - "cloud_function_docker_repository must be specified with cloud_function_kms_key_name." - " For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin" - ) - - bq_connection_manager = session.bqconnectionmanager - - def wrapper(func): - nonlocal input_types, output_type - - if not callable(func): - raise TypeError("f must be callable, got {}".format(func)) - - if sys.version_info >= (3, 10): - # Add `eval_str = True` so that deferred annotations are turned into their - # corresponding type objects. Need Python 3.10 for eval_str parameter. - # https://docs.python.org/3/library/inspect.html#inspect.signature - signature_kwargs: Mapping[str, Any] = {"eval_str": True} - else: - signature_kwargs = {} - - signature = inspect.signature( - func, - **signature_kwargs, - ) - - # Try to get input types via type annotations. - if input_types is None: - input_types = [] - for parameter in signature.parameters.values(): - if (param_type := parameter.annotation) is inspect.Signature.empty: - raise ValueError( - "'input_types' was not set and parameter " - f"'{parameter.name}' is missing a type annotation. " - "Types are required to use @remote_function." - ) - input_types.append(param_type) - elif not isinstance(input_types, collections.abc.Sequence): - input_types = [input_types] - - if output_type is None: - if ( - output_type := signature.return_annotation - ) is inspect.Signature.empty: - raise ValueError( - "'output_type' was not set and function is missing a " - "return type annotation. Types are required to use " - "@remote_function." - ) - - # The function will actually be receiving a pandas Series, but allow both - # BigQuery DataFrames and pandas object types for compatibility. - is_row_processor = False - if len(input_types) == 1 and ( - (input_type := input_types[0]) == bf_series.Series - or input_type == pandas.Series - ): - warnings.warn( - "input_types=Series is in preview.", - stacklevel=1, - category=bf_exceptions.PreviewWarning, - ) - - # we will model the row as a json serialized string containing the data - # and the metadata representing the row - input_types = [str] - is_row_processor = True - elif isinstance(input_types, type): - input_types = [input_types] - - # TODO(b/340898611): fix type error - ibis_signature = ibis_signature_from_python_signature( - signature, input_types, output_type # type: ignore - ) - - remote_function_client = RemoteFunctionClient( - dataset_ref.project, - cloud_function_region, - cloud_functions_client, - bq_location, - dataset_ref.dataset_id, - bigquery_client, - bq_connection_id, - bq_connection_manager, - cloud_function_service_account, - cloud_function_kms_key_name, - cloud_function_docker_repository, - session=session, # type: ignore - ) - - # In the unlikely case where the user is trying to re-deploy the same - # function, cleanup the attributes we add below, first. This prevents - # the pickle from having dependencies that might not otherwise be - # present such as ibis or pandas. - def try_delattr(attr): - try: - delattr(func, attr) - except AttributeError: - pass - - try_delattr("bigframes_cloud_function") - try_delattr("bigframes_remote_function") - try_delattr("input_dtypes") - try_delattr("output_dtype") - try_delattr("is_row_processor") - try_delattr("ibis_node") - - ( - rf_name, - cf_name, - created_new, - ) = remote_function_client.provision_bq_remote_function( - func, - input_types=tuple( - third_party_ibis_bqtypes.BigQueryType.from_ibis(type_) - for type_ in ibis_signature.input_types - ), - output_type=third_party_ibis_bqtypes.BigQueryType.from_ibis( - ibis_signature.output_type - ), - reuse=reuse, - name=name, - package_requirements=packages, - max_batching_rows=max_batching_rows, - cloud_function_timeout=cloud_function_timeout, - cloud_function_max_instance_count=cloud_function_max_instances, - is_row_processor=is_row_processor, - cloud_function_vpc_connector=cloud_function_vpc_connector, - cloud_function_memory_mib=cloud_function_memory_mib, - ) - - # TODO: Move ibis logic to compiler step - node = ibis.udf.scalar.builtin( - func, - name=rf_name, - schema=f"{dataset_ref.project}.{dataset_ref.dataset_id}", - signature=(ibis_signature.input_types, ibis_signature.output_type), - ) - func.bigframes_cloud_function = ( - remote_function_client.get_cloud_function_fully_qualified_name(cf_name) - ) - func.bigframes_remote_function = ( - remote_function_client.get_remote_function_fully_qualilfied_name( - rf_name - ) - ) - func.input_dtypes = tuple( - [ - bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( - input_type - ) - for input_type in ibis_signature.input_types - ] - ) - func.output_dtype = ( - bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( - ibis_signature.output_type - ) - ) - func.is_row_processor = is_row_processor - func.ibis_node = node - - # If a new remote function was created, update the cloud artifacts - # created in the session. This would be used to clean up any - # resources in the session. Note that we need to do this only for - # the case where an explicit name was not provided by the user and - # we used an internal name. For the cases where the user provided an - # explicit name, we are assuming that the user wants to persist them - # with that name and would directly manage their lifecycle. - if created_new and (not name): - self._update_temp_artifacts( - func.bigframes_remote_function, func.bigframes_cloud_function - ) - return func - - return wrapper - - def remote_function(*args, **kwargs): - remote_function_session = _RemoteFunctionSession() + remote_function_session = rf_session.RemoteFunctionSession() return remote_function_session.remote_function(*args, **kwargs) -remote_function.__doc__ = _RemoteFunctionSession.remote_function.__doc__ +remote_function.__doc__ = rf_session.RemoteFunctionSession.remote_function.__doc__ def read_gbq_function( diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 21f75eb82c9..08d808572d2 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -63,7 +63,7 @@ import bigframes.core.tools import bigframes.dataframe import bigframes.enums -import bigframes.functions.remote_function as bigframes_rf +import bigframes.functions._utils as functions_utils import bigframes.operations as ops import bigframes.series import bigframes.session @@ -817,7 +817,7 @@ def clean_up_by_session_id( session.bqclient, dataset, session_id ) - bigframes_rf._clean_up_by_session_id( + functions_utils._clean_up_by_session_id( session.bqclient, session.cloudfunctionsclient, dataset, session_id ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 2da788292b5..8cef869a321 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -95,6 +95,7 @@ import bigframes.dtypes import bigframes.exceptions import bigframes.formatting_helpers as formatting_helpers +import bigframes.functions._remote_function_session as bigframes_rf_session import bigframes.functions.remote_function as bigframes_rf import bigframes.session._io.bigquery as bf_io_bigquery import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table @@ -316,7 +317,7 @@ def __init__( ) self._allow_ambiguity = not self._strictly_ordered - self._remote_function_session = bigframes_rf._RemoteFunctionSession() + self._remote_function_session = bigframes_rf_session.RemoteFunctionSession() @property def bqclient(self): diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 095f7059cd8..d6eefc1e31a 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -31,7 +31,7 @@ import bigframes.dataframe import bigframes.dtypes import bigframes.exceptions -import bigframes.functions.remote_function as bigframes_rf +import bigframes.functions._utils as functions_utils import bigframes.pandas as bpd import bigframes.series from tests.system.utils import ( @@ -595,9 +595,11 @@ def add_one(x): add_one_uniq, add_one_uniq_dir = make_uniq_udf(add_one) # Expected cloud function name for the unique udf - package_requirements = bigframes_rf._get_updated_package_requirements() - add_one_uniq_hash = bigframes_rf._get_hash(add_one_uniq, package_requirements) - add_one_uniq_cf_name = bigframes_rf.get_cloud_function_name( + package_requirements = functions_utils._get_updated_package_requirements() + add_one_uniq_hash = functions_utils._get_hash( + add_one_uniq, package_requirements + ) + add_one_uniq_cf_name = functions_utils.get_cloud_function_name( add_one_uniq_hash, session.session_id ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 8ecf9eb3682..db573efa40e 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -23,6 +23,7 @@ import bigframes import bigframes.dtypes import bigframes.exceptions +from bigframes.functions import _utils as rf_utils from bigframes.functions import remote_function as rf from tests.system.utils import assert_pandas_df_equal @@ -89,12 +90,12 @@ def get_rf_name(func, package_requirements=None, is_row_processor=False): """Get a remote function name for testing given a udf.""" # Augment user package requirements with any internal package # requirements - package_requirements = rf._get_updated_package_requirements( + package_requirements = rf_utils._get_updated_package_requirements( package_requirements, is_row_processor ) # Compute a unique hash representing the user code - function_hash = rf._get_hash(func, package_requirements) + function_hash = rf_utils._get_hash(func, package_requirements) return f"bigframes_{function_hash}" @@ -714,7 +715,7 @@ def test_read_gbq_function_reads_udfs(session, bigquery_client, dataset_id): src = {"x": [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]} - routine_ref_str = rf.routine_ref_to_string_for_query(routine.reference) + routine_ref_str = rf_utils.routine_ref_to_string_for_query(routine.reference) direct_sql = " UNION ALL ".join( [f"SELECT {x} AS x, {routine_ref_str}({x}) AS y" for x in src["x"]] ) diff --git a/tests/system/utils.py b/tests/system/utils.py index 9fbf191a3a2..e9054d04c9e 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -26,7 +26,7 @@ import pyarrow as pa # type: ignore import pytest -from bigframes.functions import remote_function +import bigframes.functions._utils as functions_utils import bigframes.pandas ML_REGRESSION_METRICS = [ @@ -340,7 +340,7 @@ def get_cloud_functions( not name or not name_prefix ), "Either 'name' or 'name_prefix' can be passed but not both." - _, location = remote_function.get_remote_function_locations(location) + _, location = functions_utils.get_remote_function_locations(location) parent = f"projects/{project}/locations/{location}" request = functions_v2.ListFunctionsRequest(parent=parent) page_result = functions_client.list_functions(request=request) From 6dff860758bd5de08f0692703f27906e1efbe7e6 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:15:52 -0700 Subject: [PATCH 754/921] chore: update owlbot script to prevent silent failures at s.replace (#889) * chore: update owlbot script to prevent silent failures at s.replace * fix errors * removing s.replace for CONTRIBUTING.rst because it was excluded from templated files --- owlbot.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/owlbot.py b/owlbot.py index f9d9410d6d9..b29384d4625 100644 --- a/owlbot.py +++ b/owlbot.py @@ -61,7 +61,7 @@ # ---------------------------------------------------------------------------- # Encourage sharring all relevant versions in bug reports. -s.replace( +assert 1 == s.replace( [".github/ISSUE_TEMPLATE/bug_report.md"], re.escape("#### Steps to reproduce\n"), textwrap.dedent( @@ -90,7 +90,7 @@ ) # Make sure build includes all necessary files. -s.replace( +assert 1 == s.replace( ["MANIFEST.in"], re.escape("recursive-include google"), "recursive-include third_party/bigframes_vendored *\nrecursive-include bigframes", @@ -98,7 +98,7 @@ # Even though BigQuery DataFrames isn't technically a client library, we are # opting into Cloud RAD for docs hosting. -s.replace( +assert 1 == s.replace( [".kokoro/docs/common.cfg"], re.escape('value: "docs-staging-v2-staging"'), 'value: "docs-staging-v2"', @@ -106,7 +106,7 @@ # Use a custom table of contents since the default one isn't organized well # enough for the number of classes we have. -s.replace( +assert 1 == s.replace( [".kokoro/publish-docs.sh"], ( re.escape("# upload docs") @@ -124,19 +124,12 @@ ) # Fixup the documentation. -s.replace( +assert 1 == s.replace( ["docs/conf.py"], re.escape("Google Cloud Client Libraries for bigframes"), "BigQuery DataFrames provides DataFrame APIs on the BigQuery engine", ) -# Update the contributing guide to reflect some differences in this repo. -s.replace( - ["CONTRIBUTING.rst"], - re.escape("blacken"), - "format", -) - # ---------------------------------------------------------------------------- # Samples templates # ---------------------------------------------------------------------------- From e027b7e9d29f628d058611106014a1790459958c Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:24:21 -0700 Subject: [PATCH 755/921] feat: Series.str.__getitem__ (#897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [X] Make sure to open an issue as internal issue: 358459166 - [X] Ensure the tests and linter pass - [X] Code coverage does not decrease (if any source code was changed) - [X] Appropriate docs were updated (if necessary) Fixes internal issue: 358459166 🦕 --- bigframes/core/compile/scalar_op_compiler.py | 20 ++- bigframes/operations/__init__.py | 34 ++++ bigframes/operations/strings.py | 27 +++ tests/system/small/operations/test_strings.py | 157 +++++++++++++++--- .../pandas/core/strings/accessor.py | 31 ++++ 5 files changed, 241 insertions(+), 28 deletions(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 32749b32a6f..e70c49e337e 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -902,6 +902,24 @@ def array_to_string_op_impl(x: ibis_types.Value, op: ops.ArrayToStringOp): return typing.cast(ibis_types.ArrayValue, x).join(op.delimiter) +@scalar_op_compiler.register_unary_op(ops.ArrayIndexOp, pass_op=True) +def array_index_op_impl(x: ibis_types.Value, op: ops.ArrayIndexOp): + res = typing.cast(ibis_types.ArrayValue, x)[op.index] + if x.type().is_string(): + return _null_or_value(res, res != ibis.literal("")) + else: + return res + + +@scalar_op_compiler.register_unary_op(ops.ArraySliceOp, pass_op=True) +def array_slice_op_impl(x: ibis_types.Value, op: ops.ArraySliceOp): + res = typing.cast(ibis_types.ArrayValue, x)[op.start : op.stop : op.step] + if x.type().is_string(): + return _null_or_value(res, res != ibis.literal("")) + else: + return res + + # JSON Ops @scalar_op_compiler.register_binary_op(ops.JSONSet, pass_op=True) def json_set_op_impl(x: ibis_types.Value, y: ibis_types.Value, op: ops.JSONSet): @@ -984,7 +1002,7 @@ def ne_op( def _null_or_value(value: ibis_types.Value, where_value: ibis_types.BooleanValue): - return ibis.where( + return ibis.ifelse( where_value, value, ibis.null(), diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 4d4e40643d7..fb333d7a53b 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -602,6 +602,40 @@ def output_type(self, *input_types): return dtypes.STRING_DTYPE +@dataclasses.dataclass(frozen=True) +class ArrayIndexOp(UnaryOp): + name: typing.ClassVar[str] = "array_index" + index: int + + def output_type(self, *input_types): + input_type = input_types[0] + if dtypes.is_string_like(input_type): + return dtypes.STRING_DTYPE + elif dtypes.is_array_like(input_type): + return dtypes.arrow_dtype_to_bigframes_dtype( + input_type.pyarrow_dtype.value_type + ) + else: + raise TypeError("Input type must be an array or string-like type.") + + +@dataclasses.dataclass(frozen=True) +class ArraySliceOp(UnaryOp): + name: typing.ClassVar[str] = "array_slice" + start: int + stop: typing.Optional[int] = None + step: typing.Optional[int] = None + + def output_type(self, *input_types): + input_type = input_types[0] + if dtypes.is_string_like(input_type): + return dtypes.STRING_DTYPE + elif dtypes.is_array_like(input_type): + return input_type + else: + raise TypeError("Input type must be an array or string-like type.") + + ## JSON Ops @dataclasses.dataclass(frozen=True) class JSONExtract(UnaryOp): diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 22c325d7e06..d3e9c7edc67 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -38,6 +38,33 @@ class StringMethods(bigframes.operations.base.SeriesMethods, vendorstr.StringMethods): __doc__ = vendorstr.StringMethods.__doc__ + def __getitem__(self, key: Union[int, slice]) -> series.Series: + if isinstance(key, int): + if key < 0: + raise NotImplementedError("Negative indexing is not supported.") + return self._apply_unary_op(ops.ArrayIndexOp(index=key)) + elif isinstance(key, slice): + if key.step is not None and key.step != 1: + raise NotImplementedError( + f"Only a step of 1 is allowed, got {key.step}" + ) + if (key.start is not None and key.start < 0) or ( + key.stop is not None and key.stop < 0 + ): + raise NotImplementedError( + "Slicing with negative numbers is not allowed." + ) + + return self._apply_unary_op( + ops.ArraySliceOp( + start=key.start if key.start is not None else 0, + stop=key.stop, + step=key.step, + ) + ) + else: + raise ValueError(f"key must be an int or slice, got {type(key).__name__}") + def find( self, sub: str, diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index b8a8ad2d1ed..3191adf9206 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -14,10 +14,13 @@ import re +import packaging.version import pandas as pd +import pyarrow as pa import pytest -import bigframes.series +import bigframes.dtypes as dtypes +import bigframes.pandas as bpd from ...utils import assert_series_equal @@ -25,7 +28,7 @@ def test_find(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.find("W").to_pandas() pd_result = scalars_pandas_df[col_name].str.find("W") @@ -50,7 +53,7 @@ def test_find(scalars_dfs): def test_str_contains(scalars_dfs, pat, case, flags, regex): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.contains( pat, case=case, flags=flags, regex=regex @@ -72,7 +75,7 @@ def test_str_contains(scalars_dfs, pat, case, flags, regex): def test_str_extract(scalars_dfs, pat): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.extract(pat).to_pandas() pd_result = scalars_pandas_df[col_name].str.extract(pat) @@ -101,7 +104,7 @@ def test_str_extract(scalars_dfs, pat): def test_str_replace(scalars_dfs, pat, repl, case, flags, regex): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.replace( pat, repl=repl, case=case, flags=flags, regex=regex @@ -132,7 +135,7 @@ def test_str_replace(scalars_dfs, pat, repl, case, flags, regex): def test_str_startswith(scalars_dfs, pat): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] pd_series = scalars_pandas_df[col_name].astype("object") bf_result = bf_series.str.startswith(pat).to_pandas() @@ -157,7 +160,7 @@ def test_str_startswith(scalars_dfs, pat): def test_str_endswith(scalars_dfs, pat): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] pd_series = scalars_pandas_df[col_name].astype("object") bf_result = bf_series.str.endswith(pat).to_pandas() @@ -169,7 +172,7 @@ def test_str_endswith(scalars_dfs, pat): def test_len(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.len().to_pandas() pd_result = scalars_pandas_df[col_name].str.len() @@ -188,7 +191,7 @@ def test_len_with_array_column(nested_df, nested_pandas_df): See: https://stackoverflow.com/a/41340543/101923 """ col_name = "event_sequence" - bf_series: bigframes.series.Series = nested_df[col_name] + bf_series: bpd.Series = nested_df[col_name] bf_result = bf_series.str.len().to_pandas() pd_result = nested_pandas_df[col_name].str.len() @@ -204,7 +207,7 @@ def test_len_with_array_column(nested_df, nested_pandas_df): def test_lower(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.lower().to_pandas() pd_result = scalars_pandas_df[col_name].str.lower() @@ -217,7 +220,7 @@ def test_lower(scalars_dfs): def test_reverse(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.reverse().to_pandas() pd_result = scalars_pandas_df[col_name].copy() for i in pd_result.index: @@ -239,7 +242,7 @@ def test_reverse(scalars_dfs): def test_slice(scalars_dfs, start, stop): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.slice(start, stop).to_pandas() pd_series = scalars_pandas_df[col_name] pd_result = pd_series.str.slice(start, stop) @@ -253,7 +256,7 @@ def test_slice(scalars_dfs, start, stop): def test_strip(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.strip().to_pandas() pd_result = scalars_pandas_df[col_name].str.strip() @@ -266,7 +269,7 @@ def test_strip(scalars_dfs): def test_upper(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.upper().to_pandas() pd_result = scalars_pandas_df[col_name].str.upper() @@ -375,7 +378,7 @@ def test_isupper(weird_strings, weird_strings_pd): def test_rstrip(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.rstrip().to_pandas() pd_result = scalars_pandas_df[col_name].str.rstrip() @@ -388,7 +391,7 @@ def test_rstrip(scalars_dfs): def test_lstrip(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.lstrip().to_pandas() pd_result = scalars_pandas_df[col_name].str.lstrip() @@ -402,7 +405,7 @@ def test_lstrip(scalars_dfs): def test_repeat(scalars_dfs, repeats): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.repeat(repeats).to_pandas() pd_result = scalars_pandas_df[col_name].str.repeat(repeats) @@ -415,7 +418,7 @@ def test_repeat(scalars_dfs, repeats): def test_capitalize(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.capitalize().to_pandas() pd_result = scalars_pandas_df[col_name].str.capitalize() @@ -428,9 +431,9 @@ def test_capitalize(scalars_dfs): def test_cat_with_series(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_filter: bigframes.series.Series = scalars_df["bool_col"] - bf_left: bigframes.series.Series = scalars_df[col_name][bf_filter] - bf_right: bigframes.series.Series = scalars_df[col_name] + bf_filter: bpd.Series = scalars_df["bool_col"] + bf_left: bpd.Series = scalars_df[col_name][bf_filter] + bf_right: bpd.Series = scalars_df[col_name] bf_result = bf_left.str.cat(others=bf_right).to_pandas() pd_filter = scalars_pandas_df["bool_col"] pd_left = scalars_pandas_df[col_name][pd_filter] @@ -447,7 +450,7 @@ def test_str_match(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" pattern = "[A-Z].*" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.match(pattern).to_pandas() pd_result = scalars_pandas_df[col_name].str.match(pattern) @@ -461,7 +464,7 @@ def test_str_fullmatch(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" pattern = "[A-Z].*!" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.fullmatch(pattern).to_pandas() pd_result = scalars_pandas_df[col_name].str.fullmatch(pattern) @@ -474,7 +477,7 @@ def test_str_fullmatch(scalars_dfs): def test_str_get(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.get(8).to_pandas() pd_result = scalars_pandas_df[col_name].str.get(8) @@ -487,7 +490,7 @@ def test_str_get(scalars_dfs): def test_str_pad(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.pad(8, side="both", fillchar="%").to_pandas() pd_result = scalars_pandas_df[col_name].str.pad(8, side="both", fillchar="%") @@ -510,7 +513,7 @@ def test_str_zfill(weird_strings, weird_strings_pd): def test_str_ljust(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.ljust(7, fillchar="%").to_pandas() pd_result = scalars_pandas_df[col_name].str.ljust(7, fillchar="%") @@ -523,7 +526,7 @@ def test_str_ljust(scalars_dfs): def test_str_rjust(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "string_col" - bf_series: bigframes.series.Series = scalars_df[col_name] + bf_series: bpd.Series = scalars_df[col_name] bf_result = bf_series.str.rjust(9, fillchar="%").to_pandas() pd_result = scalars_pandas_df[col_name].str.rjust(9, fillchar="%") @@ -562,3 +565,103 @@ def test_str_split_raise_errors(scalars_dfs, pat, regex): pd_result = pd_result.apply(lambda x: [] if pd.isnull(x) is True else x) assert_series_equal(pd_result, bf_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("index"), + [ + pytest.param( + "first", id="invalid_type", marks=pytest.mark.xfail(raises=ValueError) + ), + pytest.param( + -1, id="neg_index", marks=pytest.mark.xfail(raises=NotImplementedError) + ), + pytest.param( + slice(0, 2, 2), + id="only_allow_one_step", + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + pytest.param( + slice(-1, None, None), + id="neg_slicing", + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + ], +) +def test_getitem_raise_errors(scalars_dfs, index): + scalars_df, _ = scalars_dfs + col_name = "string_col" + scalars_df[col_name].str[index] + + +@pytest.mark.parametrize( + ("index"), + [ + pytest.param(2, id="int"), + pytest.param(slice(None, None, None), id="default_start_slice"), + pytest.param(slice(0, None, 1), id="default_stop_slice"), + pytest.param(slice(0, 2, None), id="default_step_slice"), + ], +) +def test_getitem_w_string(scalars_dfs, index): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = scalars_df[col_name].str[index].to_pandas() + pd_result = scalars_pandas_df[col_name].str[index] + + assert_series_equal(pd_result, bf_result) + + +@pytest.mark.parametrize( + ("index"), + [ + pytest.param(2, id="int"), + pytest.param(slice(None, None, None), id="default_start_slice"), + pytest.param(slice(0, None, 1), id="default_stop_slice"), + pytest.param(slice(0, 2, None), id="default_step_slice"), + pytest.param(slice(0, 0, None), id="single_one_slice"), + ], +) +def test_getitem_w_array(index): + data = [[1], [2, 3], [], [4, 5, 6]] + s = bpd.Series(data) + pd_s = pd.Series(data) + + bf_result = s.str[index].to_pandas() + pd_result = pd_s.str[index] + # Skip dtype checks here because pandas returns `int64` while BF returns `Int64`. + assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) + + +def test_getitem_w_struct_array(): + if packaging.version.Version(pd.__version__) <= packaging.version.Version("1.5.0"): + pytest.skip("https://github.com/googleapis/python-bigquery/issues/1992") + + pa_struct = pa.struct( + [ + ("name", pa.string()), + ("age", pa.int64()), + ] + ) + data: list[list[dict]] = [ + [ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25}, + ], + [ + {"name": "Charlie", "age": 35}, + {"name": "David", "age": 40}, + {"name": "Eva", "age": 28}, + ], + [], + [{"name": "Frank", "age": 50}], + ] + s = bpd.Series(data, dtype=bpd.ArrowDtype(pa.list_(pa_struct))) + + result = s.str[1] + assert dtypes.is_struct_like(result.dtype) + + expected_data = [item[1] if len(item) > 1 else None for item in data] + expected = bpd.Series(expected_data, dtype=bpd.ArrowDtype((pa_struct))) + + assert_series_equal(result.to_pandas(), expected.to_pandas()) diff --git a/third_party/bigframes_vendored/pandas/core/strings/accessor.py b/third_party/bigframes_vendored/pandas/core/strings/accessor.py index b02c23f945a..bd5e78f4159 100644 --- a/third_party/bigframes_vendored/pandas/core/strings/accessor.py +++ b/third_party/bigframes_vendored/pandas/core/strings/accessor.py @@ -13,6 +13,37 @@ class StringMethods: R's stringr package. """ + def __getitem__(self, key: typing.Union[int, slice]): + """ + Index or slice string or list in the Series. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['Alice', 'Bob', 'Charlie']) + >>> s.str[0] + 0 A + 1 B + 2 C + dtype: string + + >>> s.str[0:3] + 0 Ali + 1 Bob + 2 Cha + dtype: string + + Args: + key (int | slice): + Index or slice of indices to access from each string or list. + + Returns: + bigframes.series.Series: The list at requested index. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def extract(self, pat: str, flags: int = 0): """ Extract capture groups in the regex `pat` as columns in a DataFrame. From ae07274ea3b49f0350da77c3f8fdb44e4cda6778 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 19:21:06 -0700 Subject: [PATCH 756/921] chore(main): release 1.14.0 (#882) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3209391f44c..754658c5e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.14.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.13.0...v1.14.0) (2024-08-14) + + +### Features + +* Implement `bigframes.bigquery.json_extract` ([#868](https://github.com/googleapis/python-bigquery-dataframes/issues/868)) ([3dbf84b](https://github.com/googleapis/python-bigquery-dataframes/commit/3dbf84bd1531c1f8d41ba57c2c38b3ba6abfb812)) +* Implement `Series.str.__getitem__` ([#897](https://github.com/googleapis/python-bigquery-dataframes/issues/897)) ([e027b7e](https://github.com/googleapis/python-bigquery-dataframes/commit/e027b7e9d29f628d058611106014a1790459958c)) + + +### Bug Fixes + +* Fix caching from generating row numbers in partial ordering mode ([#872](https://github.com/googleapis/python-bigquery-dataframes/issues/872)) ([52b7786](https://github.com/googleapis/python-bigquery-dataframes/commit/52b7786c3a28da6c29e3ddf12629802215194ad9)) + + +### Performance Improvements + +* Generate SQL with fewer CTEs ([#877](https://github.com/googleapis/python-bigquery-dataframes/issues/877)) ([eb60804](https://github.com/googleapis/python-bigquery-dataframes/commit/eb6080460344aff2fabb7864536ea4fe24c5fbef)) +* Speed up compilation by reducing redundant type normalization ([#896](https://github.com/googleapis/python-bigquery-dataframes/issues/896)) ([e0b11bc](https://github.com/googleapis/python-bigquery-dataframes/commit/e0b11bc8c038db7b950b1653ed4cd44a6246c713)) + + +### Documentation + +* Add streaming html docs ([#884](https://github.com/googleapis/python-bigquery-dataframes/issues/884)) ([171da6c](https://github.com/googleapis/python-bigquery-dataframes/commit/171da6cb33165b49d46ea6528038342abd89e9fa)) +* Fix the `DisplayOptions` doc rendering ([#893](https://github.com/googleapis/python-bigquery-dataframes/issues/893)) ([3eb6a17](https://github.com/googleapis/python-bigquery-dataframes/commit/3eb6a17a5823faf5ecba92cb9a554df74477871d)) +* Update streaming notebook ([#887](https://github.com/googleapis/python-bigquery-dataframes/issues/887)) ([6e6f9df](https://github.com/googleapis/python-bigquery-dataframes/commit/6e6f9df55d435afe0b3ade728ca06826e92a6ee6)) + ## [1.13.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.12.0...v1.13.0) (2024-08-05) diff --git a/bigframes/version.py b/bigframes/version.py index b474f021d40..2e135689ed5 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0" From 1053d56260eef1cff6e7c419f6c86be8f7e74373 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 15 Aug 2024 18:05:21 -0700 Subject: [PATCH 757/921] docs: Remove duplicate description for `kms_key_name` (#898) --- bigframes/_config/bigquery_options.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 34b9a3128fd..502f103bb50 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -224,13 +224,9 @@ def use_regional_endpoints(self, value: bool): @property def kms_key_name(self) -> Optional[str]: """ - Customer-managed encryption key - used to control encryption of the data at rest in BigQuery. This key - takes the format projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY - Customer managed encryption key used to control encryption of the data-at-rest in BigQuery. This is of the format - projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY + projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY. For more information, see https://cloud.google.com/bigquery/docs/customer-managed-encryption Customer-managed Cloud KMS keys From 92fdb937220146f770c4ce32f8317eb55ece434d Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 16 Aug 2024 10:01:31 -0700 Subject: [PATCH 758/921] refactor: Extract DataFrame execution to class (#899) --- bigframes/core/blocks.py | 33 +- bigframes/core/compile/api.py | 12 +- bigframes/core/compile/compiled.py | 34 +- bigframes/dataframe.py | 51 ++- bigframes/series.py | 9 - bigframes/session/__init__.py | 417 +++++---------------- bigframes/session/_io/bigquery/__init__.py | 110 +++--- bigframes/session/clients.py | 15 + bigframes/session/executor.py | 346 +++++++++++++++++ bigframes/session/metrics.py | 88 +++++ bigframes/session/temp_storage.py | 96 +++++ tests/system/large/test_session.py | 18 +- tests/system/small/test_dataframe.py | 4 +- tests/system/small/test_encryption.py | 6 +- tests/system/small/test_session.py | 3 +- tests/unit/session/test_io_bigquery.py | 7 +- 16 files changed, 769 insertions(+), 480 deletions(-) create mode 100644 bigframes/session/executor.py create mode 100644 bigframes/session/metrics.py create mode 100644 bigframes/session/temp_storage.py diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 65a89b4516f..9361543d5f2 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -488,12 +488,7 @@ def to_arrow( list(self.value_columns) + list(self.index_columns) ) - _, query_job = self.session._query_to_destination( - self.session._to_sql(expr, ordered=ordered), - list(self.index_columns), - api_name="cached", - do_clustering=False, - ) + _, query_job = self.session._execute(expr, ordered=ordered) results_iterator = query_job.result() pa_table = results_iterator.to_arrow() @@ -582,12 +577,7 @@ def to_pandas_batches( see https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob#google_cloud_bigquery_job_QueryJob_result""" dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) - _, query_job = self.session._query_to_destination( - self.session._to_sql(self.expr, ordered=True), - list(self.index_columns), - api_name="cached", - do_clustering=False, - ) + _, query_job = self.session._execute(self.expr, ordered=True) results_iterator = query_job.result( page_size=page_size, max_results=max_results ) @@ -617,11 +607,8 @@ def _materialize_local( ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. - _, query_job = self.session._query_to_destination( - self.session._to_sql(self.expr, ordered=materialize_options.ordered), - list(self.index_columns), - api_name="cached", - do_clustering=False, + _, query_job = self.session._execute( + self.expr, ordered=materialize_options.ordered ) results_iterator = query_job.result() @@ -797,8 +784,7 @@ def _compute_dry_run( self, value_keys: Optional[Iterable[str]] = None ) -> bigquery.QueryJob: expr = self._apply_value_keys_to_expr(value_keys=value_keys) - job_config = bigquery.QueryJobConfig(dry_run=True) - _, query_job = self.session._execute(expr, job_config=job_config, dry_run=True) + _, query_job = self.session._dry_run(expr) return query_job def _apply_value_keys_to_expr(self, value_keys: Optional[Iterable[str]] = None): @@ -2404,12 +2390,15 @@ def to_sql_query( def cached(self, *, force: bool = False, session_aware: bool = False) -> None: """Write the block to a session table.""" # use a heuristic for whether something needs to be cached - if (not force) and self.session._is_trivially_executable(self.expr): + if (not force) and self.session._executor._is_trivially_executable(self.expr): return elif session_aware: - self.session._cache_with_session_awareness(self.expr) + bfet_roots = [obj._block._expr.node for obj in self.session.objects] + self.session._executor._cache_with_session_awareness( + self.expr, session_forest=bfet_roots + ) else: - self.session._cache_with_cluster_cols( + self.session._executor._cache_with_cluster_cols( self.expr, cluster_cols=self.index_columns ) diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index 468c5522d96..4e833411aef 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -13,7 +13,9 @@ # limitations under the License. from __future__ import annotations -from typing import Mapping, Tuple, TYPE_CHECKING +from typing import Mapping, Sequence, Tuple, TYPE_CHECKING + +import google.cloud.bigquery as bigquery import bigframes.core.compile.compiler as compiler @@ -58,11 +60,13 @@ def compile_ordered( def compile_raw( self, node: bigframes.core.nodes.BigFrameNode, - ) -> Tuple[str, bigframes.core.ordering.RowOrdering]: + ) -> Tuple[ + str, Sequence[bigquery.SchemaField], bigframes.core.ordering.RowOrdering + ]: """Compile node into sql that exposes all columns, including hidden ordering-only columns.""" ir = self._compiler.compile_ordered_ir(node) - sql = ir.raw_sql() - return sql, ir._ordering + sql, schema = ir.raw_sql_and_schema() + return sql, schema, ir._ordering def test_only_try_evaluate(node: bigframes.core.nodes.BigFrameNode): diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 5492502f210..512238440cf 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -20,11 +20,14 @@ from typing import Collection, Literal, Optional, Sequence import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +import google.cloud.bigquery import ibis import ibis.backends.bigquery as ibis_bigquery +import ibis.backends.bigquery.datatypes import ibis.common.deferred # type: ignore import ibis.expr.datatypes as ibis_dtypes import ibis.expr.operations as ibis_ops +import ibis.expr.schema as ibis_schema import ibis.expr.types as ibis_types import pandas @@ -531,7 +534,8 @@ def __init__( for column in self._columns } self._hidden_ordering_column_names = { - column.get_name(): column for column in self._hidden_ordering_columns + typing.cast(str, column.get_name()): column + for column in self._hidden_ordering_columns } ### Validation value_col_ids = self._column_names.keys() @@ -947,14 +951,28 @@ def to_sql( ) return typing.cast(str, sql) - def raw_sql(self) -> str: - """Return sql with all hidden columns. Used to cache with ordering information.""" - return ibis_bigquery.Backend().compile( - self._to_ibis_expr( - ordering_mode="unordered", - expose_hidden_cols=True, - ) + def raw_sql_and_schema( + self, + ) -> typing.Tuple[str, typing.Sequence[google.cloud.bigquery.SchemaField]]: + """Return sql with all hidden columns. Used to cache with ordering information. + + Also returns schema, as the extra ordering columns are determined compile-time. + """ + all_columns = (*self.column_ids, *self._hidden_ordering_column_names.keys()) + as_ibis = self._to_ibis_expr( + ordering_mode="unordered", + expose_hidden_cols=True, + ).select(all_columns) + + # Ibis will produce non-nullable schema types, but bigframes should always be nullable + fixed_ibis_schema = ibis_schema.Schema.from_tuples( + (name, dtype.copy(nullable=True)) + for (name, dtype) in as_ibis.schema().items() + ) + bq_schema = ibis.backends.bigquery.datatypes.BigQuerySchema.from_ibis( + fixed_ibis_schema ) + return ibis_bigquery.Backend().compile(as_ibis), bq_schema def _to_ibis_expr( self, diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 649b097e927..dabe85c9236 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1214,7 +1214,6 @@ def to_arrow( category=bigframes.exceptions.PreviewWarning, ) - self._optimize_query_complexity() pa_table, query_job = self._block.to_arrow(ordered=ordered) self._set_internal_query_job(query_job) return pa_table @@ -1255,7 +1254,6 @@ def to_pandas( downsampled rows and all columns of this DataFrame. """ # TODO(orrbradford): Optimize this in future. Potentially some cases where we can return the stored query job - self._optimize_query_complexity() df, query_job = self._block.to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, @@ -1285,7 +1283,6 @@ def to_pandas_batches( form the original dataframe. Results stream from bigquery, see https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.table.RowIterator#google_cloud_bigquery_table_RowIterator_to_arrow_iterable """ - self._optimize_query_complexity() return self._block.to_pandas_batches( page_size=page_size, max_results=max_results ) @@ -3046,12 +3043,6 @@ def to_gbq( ordering_id: Optional[str] = None, clustering_columns: Union[pandas.Index, Iterable[typing.Hashable]] = (), ) -> str: - dispositions = { - "fail": bigquery.WriteDisposition.WRITE_EMPTY, - "replace": bigquery.WriteDisposition.WRITE_TRUNCATE, - "append": bigquery.WriteDisposition.WRITE_APPEND, - } - temp_table_ref = None if destination_table is None: @@ -3063,7 +3054,7 @@ def to_gbq( ) if_exists = "replace" - temp_table_ref = self._session._random_table( + 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, @@ -3086,10 +3077,11 @@ def to_gbq( if if_exists is None: if_exists = "fail" - if if_exists not in dispositions: + valid_if_exists = ["fail", "replace", "append"] + if if_exists not in valid_if_exists: raise ValueError( f"Got invalid value {repr(if_exists)} for if_exists. " - f"Valid options include None or one of {dispositions.keys()}." + f"Valid options include None or one of {valid_if_exists}." ) try: @@ -3101,16 +3093,25 @@ def to_gbq( clustering_columns, index=index ) - job_config = bigquery.QueryJobConfig( - write_disposition=dispositions[if_exists], - destination=bigquery.table.TableReference.from_string( - destination_table, - default_project=default_project, - ), - clustering_fields=clustering_fields if clustering_fields else None, + export_array, id_overrides = self._prepare_export( + index=index and self._has_index, ordering_id=ordering_id + ) + destination = bigquery.table.TableReference.from_string( + destination_table, + default_project=default_project, + ) + _, query_job = self._session._export( + export_array, + destination=destination, + col_id_overrides=id_overrides, + cluster_cols=clustering_fields, + if_exists=if_exists, ) + self._set_internal_query_job(query_job) - self._run_io_query(index=index, ordering_id=ordering_id, job_config=job_config) + # The query job should have finished, so there should be always be a result table. + result_table = query_job.destination + assert result_table is not None if temp_table_ref: bigframes.session._io.bigquery.set_table_expiration( @@ -3402,19 +3403,16 @@ def _run_io_query( self, index: bool, ordering_id: Optional[str] = None, - job_config: Optional[bigquery.job.QueryJobConfig] = None, ) -> bigquery.TableReference: """Executes a query job presenting this dataframe and returns the destination table.""" session = self._block.expr.session - self._optimize_query_complexity() export_array, id_overrides = self._prepare_export( index=index and self._has_index, ordering_id=ordering_id ) _, query_job = session._execute( export_array, - job_config=job_config, ordered=False, col_id_overrides=id_overrides, ) @@ -3669,13 +3667,6 @@ def _cached(self, *, force: bool = False) -> DataFrame: self._block.cached(force=force) return self - def _optimize_query_complexity(self): - """Reduce query complexity by caching repeated subtrees and recursively materializing maximum-complexity subtrees. - May generate many queries and take substantial time to execute. - """ - # TODO: Move all this to session - self._session._simplify_with_caching(self._block.expr) - _DataFrameOrSeries = typing.TypeVar("_DataFrameOrSeries") @validations.requires_ordering() diff --git a/bigframes/series.py b/bigframes/series.py index 069c469a85d..7ba4858b5eb 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -188,7 +188,6 @@ def __len__(self): __len__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__len__) def __iter__(self) -> typing.Iterator: - self._optimize_query_complexity() return itertools.chain.from_iterable( map(lambda x: x.squeeze(axis=1), self._block.to_pandas_batches()) ) @@ -358,7 +357,6 @@ def to_pandas( 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. """ - self._optimize_query_complexity() df, query_job = self._block.to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, @@ -1892,13 +1890,6 @@ def _cached(self, *, force: bool = True, session_aware: bool = True) -> Series: self._block.cached(force=force, session_aware=session_aware) return self - def _optimize_query_complexity(self): - """Reduce query complexity by caching repeated subtrees and recursively materializing maximum-complexity subtrees. - May generate many queries and take substantial time to execute. - """ - # TODO: Move all this to session - self._block.session._simplify_with_caching(self._block.expr) - def _is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: return pandas.api.types.is_list_like(obj) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 8cef869a321..0868ef202ae 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -20,7 +20,6 @@ import datetime import itertools import logging -import math import os import secrets import typing @@ -40,7 +39,6 @@ Tuple, Union, ) -import uuid import warnings import weakref @@ -81,12 +79,8 @@ import bigframes.core.blocks as blocks import bigframes.core.compile import bigframes.core.guid -import bigframes.core.nodes as nodes -import bigframes.core.ordering as order import bigframes.core.pruning import bigframes.core.schema as schemata -import bigframes.core.tree_properties as traversals -import bigframes.core.tree_properties as tree_properties import bigframes.core.utils as utils # Even though the ibis.backends.bigquery import is unused, it's needed @@ -100,7 +94,10 @@ import bigframes.session._io.bigquery as bf_io_bigquery import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table import bigframes.session.clients +import bigframes.session.executor +import bigframes.session.metrics import bigframes.session.planner +import bigframes.session.temp_storage import bigframes.version # Avoid circular imports. @@ -112,8 +109,6 @@ _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" -_TEMP_TABLE_ID_FORMAT = "bqdf{date}_{session_id}_{random_id}" - _MAX_CLUSTER_COLUMNS = 4 # TODO(swast): Need to connect to regional endpoints when performing remote @@ -133,11 +128,6 @@ # Also must assume that text encoding as literals is much less efficient than in-memory representation. MAX_INLINE_DF_BYTES = 5000 -# Max complexity that should be executed as a single query -QUERY_COMPLEXITY_LIMIT = 1e7 -# Number of times to factor out subqueries before giving up. -MAX_SUBTREE_FACTORINGS = 5 - logger = logging.getLogger(__name__) # Excludes geography, bytes, and nested (array, struct) datatypes @@ -277,7 +267,6 @@ def __init__( # only needs to be unique among sessions created by the same user # at the same time in the same region self._session_id: str = "session" + secrets.token_hex(3) - self._table_ids: List[str] = [] # store table ids and delete them when the session is closed self._objects: list[ @@ -289,14 +278,6 @@ def __init__( ] ] ] = [] - self._cached_executions: weakref.WeakKeyDictionary[ - nodes.BigFrameNode, nodes.BigFrameNode - ] = weakref.WeakKeyDictionary() - - # performance logging - self._bytes_processed_sum = 0 - self._slot_millis_sum = 0 - self._execution_count = 0 # Whether this session treats objects as totally ordered. # Will expose as feature later, only False for internal testing self._strictly_ordered: bool = context.ordering_mode != "partial" @@ -312,12 +293,24 @@ def __init__( if self._strictly_ordered else bigframes.enums.DefaultIndexKind.NULL ) - self._compiler = bigframes.core.compile.SQLCompiler( - strict=self._strictly_ordered - ) self._allow_ambiguity = not self._strictly_ordered + self._metrics = bigframes.session.metrics.ExecutionMetrics() self._remote_function_session = bigframes_rf_session.RemoteFunctionSession() + self._temp_storage_manager = ( + bigframes.session.temp_storage.TemporaryGbqStorageManager( + self._clients_provider.bqclient, + dataset=self._anonymous_dataset, + session_id=self._session_id, + kms_key=self._bq_kms_key_name, + ) + ) + self._executor = bigframes.session.executor.BigQueryCachingExecutor( + bqclient=self._clients_provider.bqclient, + storage_manager=self._temp_storage_manager, + strictly_ordered=self._strictly_ordered, + metrics=self._metrics, + ) @property def bqclient(self): @@ -373,48 +366,26 @@ def _project(self): @property def bytes_processed_sum(self): """The sum of all bytes processed by bigquery jobs using this session.""" - return self._bytes_processed_sum + return self._metrics.bytes_processed @property def slot_millis_sum(self): """The sum of all slot time used by bigquery jobs in this session.""" - return self._slot_millis_sum + return self._metrics.slot_millis @property def _allows_ambiguity(self) -> bool: return self._allow_ambiguity - def _add_bytes_processed(self, amount: int): - """Increment bytes_processed_sum by amount.""" - self._bytes_processed_sum += amount - - def _add_slot_millis(self, amount: int): - """Increment slot_millis_sum by amount.""" - self._slot_millis_sum += amount - - def _add_execution(self, amount: int = 1): - """Increment slot_millis_sum by amount.""" - self._execution_count += amount - def __hash__(self): # Stable hash needed to use in expression tree return hash(str(self._anonymous_dataset)) - def _clean_up_tables(self): - """Delete tables that were created with this session's session_id.""" - client = self.bqclient - project_id = self._anonymous_dataset.project - dataset_id = self._anonymous_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) - def close(self): """Delete resources that were created with this session's session_id. This includes BigQuery tables, remote functions and cloud functions serving the remote functions.""" - self._clean_up_tables() + self._temp_storage_manager.clean_up_tables() self._remote_function_session.clean_up( self.bqclient, self.cloudfunctionsclient, self.session_id ) @@ -485,7 +456,6 @@ def _query_to_destination( configuration: dict = {"query": {"useQueryCache": True}}, do_clustering=True, ) -> Tuple[Optional[bigquery.TableReference], bigquery.QueryJob]: - self._add_execution(1) # If a dry_run indicates this is not a query type job, then don't # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. dry_run_config = bigquery.QueryJobConfig() @@ -500,16 +470,15 @@ def _query_to_destination( # Create a table to workaround BigQuery 10 GB query results limit. See: # internal issue 303057336. # Since we have a `statement_type == 'SELECT'`, schema should be populated. - schema = typing.cast(Iterable[bigquery.SchemaField], dry_run_job.schema) + schema = dry_run_job.schema + assert schema is not None if do_clustering: - cluster_cols = [ - item.name - for item in schema - if (item.name in index_cols) and _can_cluster_bq(item) - ][:_MAX_CLUSTER_COLUMNS] + cluster_cols = bf_io_bigquery.select_cluster_cols( + schema, cluster_candidates=index_cols + ) else: cluster_cols = [] - temp_table = self._create_empty_temp_table(schema, cluster_cols) + temp_table = self._temp_storage_manager.create_temp_table(schema, cluster_cols) timeout_ms = configuration.get("jobTimeoutMs") or configuration["query"].get( "timeoutMs" @@ -1211,8 +1180,7 @@ def _read_pandas_load_job( pandas_dataframe_copy.columns = pandas.Index(new_col_ids) pandas_dataframe_copy[ordering_col] = np.arange(pandas_dataframe_copy.shape[0]) - job_config = self._prepare_load_job_config() - + job_config = bigquery.LoadJobConfig() # Specify the datetime dtypes, which is auto-detected as timestamp types. schema: list[bigquery.SchemaField] = [] for column, dtype in zip(new_col_ids, pandas_dataframe.dtypes): @@ -1228,7 +1196,7 @@ def _read_pandas_load_job( job_config.labels = {"bigframes-api": api_name} - load_table_destination = self._random_table() + load_table_destination = self._temp_storage_manager._random_table() load_job = self.bqclient.load_table_from_dataframe( pandas_dataframe_copy, load_table_destination, @@ -1289,7 +1257,7 @@ def read_csv( encoding: Optional[str] = None, **kwargs, ) -> dataframe.DataFrame: - table = self._random_table() + table = self._temp_storage_manager._random_table() if engine is not None and engine == "bigquery": if any(param is not None for param in (dtype, names)): @@ -1349,7 +1317,7 @@ def read_csv( f"{constants.FEEDBACK_LINK}" ) - job_config = self._prepare_load_job_config() + job_config = bigquery.LoadJobConfig() job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED job_config.source_format = bigquery.SourceFormat.CSV job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY @@ -1426,10 +1394,10 @@ def read_parquet( *, engine: str = "auto", ) -> dataframe.DataFrame: - table = self._random_table() + table = self._temp_storage_manager._random_table() if engine == "bigquery": - job_config = self._prepare_load_job_config() + job_config = bigquery.LoadJobConfig() job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED job_config.source_format = bigquery.SourceFormat.PARQUET job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY @@ -1463,7 +1431,7 @@ def read_json( engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", **kwargs, ) -> dataframe.DataFrame: - table = self._random_table() + table = self._temp_storage_manager._random_table() if engine == "bigquery": @@ -1487,7 +1455,7 @@ def read_json( "'lines' keyword is only valid when 'orient' is 'records'." ) - job_config = self._prepare_load_job_config() + job_config = bigquery.LoadJobConfig() job_config.create_disposition = bigquery.CreateDisposition.CREATE_IF_NEEDED job_config.source_format = bigquery.SourceFormat.NEWLINE_DELIMITED_JSON job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY @@ -1553,25 +1521,6 @@ def _check_file_size(self, filepath: str): "for large files to avoid loading the file into local memory." ) - def _create_empty_temp_table( - self, - schema: Iterable[bigquery.SchemaField], - cluster_cols: List[str], - ) -> bigquery.TableReference: - # Can't set a table in _SESSION as destination via query job API, so we - # run DDL, instead. - expiration = ( - datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION - ) - - table = bf_io_bigquery.create_temp_table( - self, - expiration, - schema=schema, - cluster_columns=cluster_cols, - ) - return bigquery.TableReference.from_string(table) - def _sql_to_temp_table( self, sql: str, @@ -1851,42 +1800,6 @@ def read_gbq_function( session=self, ) - def _prepare_query_job_config( - self, - job_config: Optional[bigquery.QueryJobConfig] = None, - ) -> bigquery.QueryJobConfig: - if job_config is None: - job_config = bigquery.QueryJobConfig() - else: - # Create a copy so that we don't mutate the original config passed - job_config = typing.cast( - bigquery.QueryJobConfig, - bigquery.QueryJobConfig.from_api_repr(job_config.to_api_repr()), - ) - - if bigframes.options.compute.maximum_bytes_billed is not None: - job_config.maximum_bytes_billed = ( - bigframes.options.compute.maximum_bytes_billed - ) - - if self._bq_kms_key_name: - job_config.destination_encryption_configuration = ( - bigquery.EncryptionConfiguration(kms_key_name=self._bq_kms_key_name) - ) - - return job_config - - def _prepare_load_job_config(self) -> bigquery.LoadJobConfig: - # Create a copy so that we don't mutate the original config passed - job_config = bigquery.LoadJobConfig() - - if self._bq_kms_key_name: - job_config.destination_encryption_configuration = ( - bigquery.EncryptionConfiguration(kms_key_name=self._bq_kms_key_name) - ) - - return job_config - def _prepare_copy_job_config(self) -> bigquery.CopyJobConfig: # Create a copy so that we don't mutate the original config passed job_config = bigquery.CopyJobConfig() @@ -1908,26 +1821,23 @@ def _start_query( ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ Starts BigQuery query job and waits for results. + + Do not execute dataframe through this API, instead use the executor. """ - job_config = self._prepare_query_job_config(job_config) - if not self._strictly_ordered: - job_config.labels = {"bigframes-mode": "unordered"} - try: - return bigframes.session._io.bigquery.start_query_with_client( - self, - sql, - job_config, - max_results, - timeout, - api_name=api_name, + job_config = bigquery.QueryJobConfig() if job_config is None else job_config + if bigframes.options.compute.maximum_bytes_billed is not None: + # Maybe this should be pushed down into start_query_with_client + job_config.maximum_bytes_billed = ( + bigframes.options.compute.maximum_bytes_billed ) - except google.api_core.exceptions.BadRequest as e: - # Unfortunately, this error type does not have a separate error code or exception type - if "Resources exceeded during query execution" in e.message: - new_message = "Computation is too complex to execute as a single query. Try using DataFrame.cache() on intermediate results, or setting bigframes.options.compute.enable_multi_query_execution." - raise bigframes.exceptions.QueryComplexityError(new_message) from e - else: - raise + return bf_io_bigquery.start_query_with_client( + self.bqclient, + sql, + job_config, + max_results, + timeout, + api_name=api_name, + ) def _start_query_ml_ddl( self, @@ -1937,162 +1847,60 @@ def _start_query_ml_ddl( Starts BigQuery ML DDL query job (CREATE MODEL/ALTER MODEL/...) and waits for results. """ - job_config = self._prepare_query_job_config() + job_config = typing.cast(bigquery.QueryJobConfig, bigquery.QueryJobConfig()) + if bigframes.options.compute.maximum_bytes_billed is not None: + job_config.maximum_bytes_billed = ( + bigframes.options.compute.maximum_bytes_billed + ) # BQML expects kms_key_name through OPTIONS and not through job config, # so we must reset any encryption set in the job config # https://cloud.google.com/bigquery/docs/customer-managed-encryption#encrypt-model job_config.destination_encryption_configuration = None - return bigframes.session._io.bigquery.start_query_with_client( - self, sql, job_config - ) - - def _cache_with_cluster_cols( - self, array_value: core.ArrayValue, cluster_cols: typing.Sequence[str] - ): - """Executes the query and uses the resulting table to rewrite future executions.""" - # TODO: Use this for all executions? Problem is that caching materializes extra - # ordering columns - - sql, ordering_info = self._compiler.compile_raw( - self._with_cached_executions(array_value.node) - ) - tmp_table = self._sql_to_temp_table( - sql, cluster_cols=cluster_cols, api_name="cached" - ) - cached_replacement = array_value.as_cached( - cache_table=self.bqclient.get_table(tmp_table), - ordering=ordering_info, - ).node - self._cached_executions[array_value.node] = cached_replacement - - def _cache_with_offsets(self, array_value: core.ArrayValue): - """Executes the query and uses the resulting table to rewrite future executions.""" - # TODO: Use this for all executions? Problem is that caching materializes extra - # ordering columns - if not self._strictly_ordered: - raise ValueError( - "Caching with offsets only supported in strictly ordered mode." - ) - offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") - sql = self._compiler.compile_unordered( - self._with_cached_executions( - array_value.promote_offsets(offset_column).node - ) - ) - - tmp_table = self._sql_to_temp_table( - sql, cluster_cols=[offset_column], api_name="cached" - ) - cached_replacement = array_value.as_cached( - cache_table=self.bqclient.get_table(tmp_table), - ordering=order.TotalOrdering.from_offset_col(offset_column), - ).node - self._cached_executions[array_value.node] = cached_replacement - - def _cache_with_session_awareness(self, array_value: core.ArrayValue) -> None: - # this is the occurence count across the whole session - forest = [obj._block.expr.node for obj in self.objects] - # These node types are cheap to re-compute - target, cluster_cols = bigframes.session.planner.session_aware_cache_plan( - array_value.node, forest - ) - if len(cluster_cols) > 0: - self._cache_with_cluster_cols(core.ArrayValue(target), cluster_cols) - elif self._strictly_ordered: - self._cache_with_offsets(core.ArrayValue(target)) - else: - self._cache_with_cluster_cols(core.ArrayValue(target), []) - - def _simplify_with_caching(self, array_value: core.ArrayValue): - """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" - # Apply existing caching first - if not bigframes.options.compute.enable_multi_query_execution: - return - - for _ in range(MAX_SUBTREE_FACTORINGS): - node_with_cache = self._with_cached_executions(array_value.node) - if node_with_cache.planning_complexity < QUERY_COMPLEXITY_LIMIT: - return - - did_cache = self._cache_most_complex_subtree(array_value.node) - if not did_cache: - return - - def _cache_most_complex_subtree(self, node: nodes.BigFrameNode) -> bool: - # TODO: If query fails, retry with lower complexity limit - selection = traversals.select_cache_target( - node, - min_complexity=(QUERY_COMPLEXITY_LIMIT / 500), - max_complexity=QUERY_COMPLEXITY_LIMIT, - cache=dict(self._cached_executions), - # Heuristic: subtree_compleixty * (copies of subtree)^2 - heuristic=lambda complexity, count: math.log(complexity) - + 2 * math.log(count), - ) - if selection is None: - # No good subtrees to cache, just return original tree - return False - - self._cache_with_cluster_cols(core.ArrayValue(selection), []) - return True - - def _with_cached_executions(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: - return traversals.replace_nodes(node, (dict(self._cached_executions))) - - def _is_trivially_executable(self, array_value: core.ArrayValue): - """ - Can the block be evaluated very cheaply? - If True, the array_value probably is not worth caching. - """ - # Once rewriting is available, will want to rewrite before - # evaluating execution cost. - return traversals.is_trivially_executable( - self._with_cached_executions(array_value.node) - ) + return bf_io_bigquery.start_query_with_client(self.bqclient, sql, job_config) def _execute( self, array_value: core.ArrayValue, - job_config: Optional[bigquery.job.QueryJobConfig] = None, *, ordered: bool = True, - dry_run=False, col_id_overrides: Mapping[str, str] = {}, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - if not dry_run: - self._add_execution(1) - sql = self._to_sql( - array_value, ordered=ordered, col_id_overrides=col_id_overrides - ) # type:ignore - if job_config is None: - job_config = bigquery.QueryJobConfig(dry_run=dry_run) - else: - job_config.dry_run = dry_run + return self._executor.execute( + array_value, + ordered=ordered, + col_id_overrides=col_id_overrides, + ) - # TODO(swast): plumb through the api_name of the user-facing api that - # caused this query. - return self._start_query( - sql=sql, - job_config=job_config, + def _export( + self, + array_value: core.ArrayValue, + destination: bigquery.TableReference, + *, + if_exists: Literal["fail", "replace", "append"] = "fail", + col_id_overrides: Mapping[str, str] = {}, + cluster_cols: Sequence[str], + ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + # Note: cluster_cols use pre-override column ids + return self._executor.export( + array_value, + destination=destination, + col_id_overrides=col_id_overrides, + if_exists=if_exists, + cluster_cols=cluster_cols, ) + def _dry_run( + self, array_value: core.ArrayValue, ordered: bool = True + ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + return self._executor.dry_run(array_value, ordered=ordered) + def _peek( self, array_value: core.ArrayValue, n_rows: int ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """A 'peek' efficiently accesses a small number of rows in the dataframe.""" - if not tree_properties.peekable(self._with_cached_executions(array_value.node)): - warnings.warn("Peeking this value cannot be done efficiently.") - sql = self._compiler.compile_peek( - self._with_cached_executions(array_value.node), n_rows - ) - - # TODO(swast): plumb through the api_name of the user-facing api that - # caused this query. - return self._start_query( - sql=sql, - ) + return self._executor.peek(array_value, n_rows) def _to_sql( self, @@ -2102,18 +1910,9 @@ def _to_sql( ordered: bool = False, enable_cache: bool = True, ) -> str: - if offset_column: - array_value = array_value.promote_offsets(offset_column) - node = ( - self._with_cached_executions(array_value.node) - if enable_cache - else array_value.node + return self._executor.to_sql( + array_value, offset_column, col_id_overrides, ordered, enable_cache ) - if ordered: - return self._compiler.compile_ordered( - node, col_id_overrides=col_id_overrides - ) - return self._compiler.compile_unordered(node, col_id_overrides=col_id_overrides) def _get_table_size(self, destination_table): table = self.bqclient.get_table(destination_table) @@ -2135,57 +1934,11 @@ def _start_generic_job(self, job: formatting_helpers.GenericJob): else: job.result() - def _random_table(self, skip_cleanup: bool = False) -> 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. - - Args: - skip_cleanup (bool, default False): - If True, do not add the generated ID to the list of tables - to clean up when the session is closed. - - Returns: - google.cloud.bigquery.TableReference: - Fully qualified table ID of a table that doesn't exist. - """ - dataset = self._anonymous_dataset - session_id = self.session_id - now = datetime.datetime.now(datetime.timezone.utc) - random_id = uuid.uuid4().hex - table_id = _TEMP_TABLE_ID_FORMAT.format( - date=now.strftime("%Y%m%d"), session_id=session_id, random_id=random_id - ) - if not skip_cleanup: - self._table_ids.append(table_id) - return dataset.table(table_id) - def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: return Session(context) -def _can_cluster_bq(field: bigquery.SchemaField): - # https://cloud.google.com/bigquery/docs/clustered-tables - # Notably, float is excluded - type_ = field.field_type - return type_ in ( - "INTEGER", - "INT64", - "STRING", - "NUMERIC", - "DECIMAL", - "BIGNUMERIC", - "BIGDECIMAL", - "DATE", - "DATETIME", - "TIMESTAMP", - "BOOL", - "BOOLEAN", - ) - - def _transform_read_gbq_configuration(configuration: Optional[dict]) -> dict: """ For backwards-compatibility, convert any previously client-side only diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 3a33352a672..a77729cef97 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -18,7 +18,6 @@ import datetime import itertools -import os import re import textwrap import types @@ -35,14 +34,17 @@ import bigframes.core.compile.googlesql as googlesql import bigframes.core.sql import bigframes.formatting_helpers as formatting_helpers +import bigframes.session.metrics + +CHECK_DRIVE_PERMISSIONS = "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." + IO_ORDERING_ID = "bqdf_row_nums" MAX_LABELS_COUNT = 64 _LIST_TABLES_LIMIT = 10000 # calls to bqclient.list_tables # will be limited to this many tables -LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" -CHECK_DRIVE_PERMISSIONS = "\nCheck https://cloud.google.com/bigquery/docs/query-drive-data#Google_Drive_permissions." +_MAX_CLUSTER_COLUMNS = 4 def create_job_configs_labels( @@ -129,24 +131,28 @@ def table_ref_to_sql(table: bigquery.TableReference) -> str: def create_temp_table( - session: bigframes.session.Session, + bqclient: bigquery.Client, + table_ref: bigquery.TableReference, expiration: datetime.datetime, *, schema: Optional[Iterable[bigquery.SchemaField]] = None, cluster_columns: Optional[list[str]] = None, + kms_key: Optional[str] = None, ) -> str: """Create an empty table with an expiration in the desired session. The table will be deleted when the session is closed or the expiration is reached. """ - bqclient: bigquery.Client = session.bqclient - table_ref = session._random_table() destination = bigquery.Table(table_ref) destination.expires = expiration destination.schema = schema if cluster_columns: destination.clustering_fields = cluster_columns + if kms_key: + destination.encryption_configuration = bigquery.EncryptionConfiguration( + kms_key_name=kms_key + ) # Ok if already exists, since this will only happen from retries internal to this method # as the requested table id has a random UUID4 component. bqclient.create_table(destination, exists_ok=True) @@ -222,17 +228,17 @@ def add_labels(job_config, api_name: Optional[str] = None): def start_query_with_client( - session: bigframes.session.Session, + bq_client: bigquery.Client, sql: str, job_config: bigquery.job.QueryJobConfig, max_results: Optional[int] = None, timeout: Optional[float] = None, api_name: Optional[str] = None, + metrics: Optional[bigframes.session.metrics.ExecutionMetrics] = None, ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: """ Starts query job and waits for results. """ - bq_client: bigquery.Client = session.bqclient add_labels(job_config, api_name=api_name) try: @@ -250,64 +256,11 @@ def start_query_with_client( else: results_iterator = query_job.result(max_results=max_results) - stats = get_performance_stats(query_job) - if stats is not None: - bytes_processed, slot_millis = stats - session._add_bytes_processed(bytes_processed) - session._add_slot_millis(slot_millis) - if LOGGING_NAME_ENV_VAR in os.environ: - # when running notebooks via pytest nbmake - write_stats_to_disk(bytes_processed, slot_millis) - + if metrics is not None: + metrics.count_job_stats(query_job) return results_iterator, query_job -def get_performance_stats(query_job: bigquery.QueryJob) -> Optional[Tuple[int, int]]: - """Parse the query job for performance stats. - - Return None if the stats do not reflect real work done in bigquery. - """ - bytes_processed = query_job.total_bytes_processed - if not isinstance(bytes_processed, int): - return None # filter out mocks - if query_job.configuration.dry_run: - # dry run stats are just predictions of the real run - bytes_processed = 0 - - slot_millis = query_job.slot_millis - if not isinstance(slot_millis, int): - return None # filter out mocks - if query_job.configuration.dry_run: - # dry run stats are just predictions of the real run - slot_millis = 0 - - return bytes_processed, slot_millis - - -def write_stats_to_disk(bytes_processed: int, slot_millis: int): - """For pytest runs only, log information about the query job - to a file in order to create a performance report. - """ - if LOGGING_NAME_ENV_VAR not in os.environ: - raise EnvironmentError( - "Environment variable {env_var} is not set".format( - env_var=LOGGING_NAME_ENV_VAR - ) - ) - test_name = os.environ[LOGGING_NAME_ENV_VAR] - current_directory = os.getcwd() - - # store bytes processed - bytes_file = os.path.join(current_directory, test_name + ".bytesprocessed") - with open(bytes_file, "a") as f: - f.write(str(bytes_processed) + "\n") - - # store slot milliseconds - bytes_file = os.path.join(current_directory, test_name + ".slotmillis") - with open(bytes_file, "a") as f: - f.write(str(slot_millis) + "\n") - - def delete_tables_matching_session_id( client: bigquery.Client, dataset: bigquery.DatasetReference, session_id: str ) -> None: @@ -504,3 +457,34 @@ def compile_filters(filters: third_party_pandas_gbq.FiltersType) -> str: filter_string = and_expression return filter_string + + +def select_cluster_cols( + schema: typing.Sequence[bigquery.SchemaField], + cluster_candidates: typing.Sequence[str], +) -> typing.Sequence[str]: + return [ + item.name + for item in schema + if (item.name in cluster_candidates) and _can_cluster_bq(item) + ][:_MAX_CLUSTER_COLUMNS] + + +def _can_cluster_bq(field: bigquery.SchemaField): + # https://cloud.google.com/bigquery/docs/clustered-tables + # Notably, float is excluded + type_ = field.field_type + return type_ in ( + "INTEGER", + "INT64", + "STRING", + "NUMERIC", + "DECIMAL", + "BIGNUMERIC", + "BIGDECIMAL", + "DATE", + "DATETIME", + "TIMESTAMP", + "BOOL", + "BOOLEAN", + ) diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index 85664d8dc8b..7b53d40f74a 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -144,6 +144,21 @@ def _create_bigquery_client(self): project=self._project, location=self._location, ) + if self._bq_kms_key_name: + # Note: Key configuration only applies automatically to load and query jobs, not copy jobs. + encryption_config = bigquery.EncryptionConfiguration( + kms_key_name=self._bq_kms_key_name + ) + default_load_job_config = bigquery.LoadJobConfig() + default_query_job_config = bigquery.QueryJobConfig() + default_load_job_config.destination_encryption_configuration = ( + encryption_config + ) + default_query_job_config.destination_encryption_configuration = ( + encryption_config + ) + bq_client.default_load_job_config = default_load_job_config + bq_client.default_query_job_config = default_query_job_config return bq_client diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py new file mode 100644 index 00000000000..539658a18c4 --- /dev/null +++ b/bigframes/session/executor.py @@ -0,0 +1,346 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import math +from typing import cast, Iterable, Literal, Mapping, Optional, Sequence, Tuple +import warnings +import weakref + +import google.api_core.exceptions +import google.cloud.bigquery as bigquery +import google.cloud.bigquery.job as bq_job + +import bigframes.core +import bigframes.core.compile +import bigframes.core.guid +import bigframes.core.nodes as nodes +import bigframes.core.ordering as order +import bigframes.core.tree_properties as tree_properties +import bigframes.formatting_helpers as formatting_helpers +import bigframes.session._io.bigquery as bq_io +import bigframes.session.metrics +import bigframes.session.planner +import bigframes.session.temp_storage + +# Max complexity that should be executed as a single query +QUERY_COMPLEXITY_LIMIT = 1e7 +# Number of times to factor out subqueries before giving up. +MAX_SUBTREE_FACTORINGS = 5 + +_MAX_CLUSTER_COLUMNS = 4 + + +class BigQueryCachingExecutor: + """Computes BigFrames values using BigQuery Engine. + + This executor can cache expressions. If those expressions are executed later, this session + will re-use the pre-existing results from previous executions. + + This class is not thread-safe. + """ + + def __init__( + self, + bqclient: bigquery.Client, + storage_manager: bigframes.session.temp_storage.TemporaryGbqStorageManager, + strictly_ordered: bool = True, + metrics: Optional[bigframes.session.metrics.ExecutionMetrics] = None, + ): + self.bqclient = bqclient + self.storage_manager = storage_manager + self.compiler: bigframes.core.compile.SQLCompiler = ( + bigframes.core.compile.SQLCompiler(strict=strictly_ordered) + ) + self.strictly_ordered: bool = strictly_ordered + self._cached_executions: weakref.WeakKeyDictionary[ + nodes.BigFrameNode, nodes.BigFrameNode + ] = weakref.WeakKeyDictionary() + self.metrics = metrics + + def to_sql( + self, + array_value: bigframes.core.ArrayValue, + offset_column: Optional[str] = None, + col_id_overrides: Mapping[str, str] = {}, + ordered: bool = False, + enable_cache: bool = True, + ) -> str: + """ + Convert an ArrayValue to a sql query that will yield its value. + """ + if offset_column: + array_value = array_value.promote_offsets(offset_column) + node = ( + self._with_cached_executions(array_value.node) + if enable_cache + else array_value.node + ) + if ordered: + return self.compiler.compile_ordered( + node, col_id_overrides=col_id_overrides + ) + return self.compiler.compile_unordered(node, col_id_overrides=col_id_overrides) + + def execute( + self, + array_value: bigframes.core.ArrayValue, + *, + ordered: bool = True, + col_id_overrides: Mapping[str, str] = {}, + ): + """ + Execute the ArrayValue, storing the result to a temporary session-owned table. + """ + if bigframes.options.compute.enable_multi_query_execution: + self._simplify_with_caching(array_value) + + sql = self.to_sql( + array_value, ordered=ordered, col_id_overrides=col_id_overrides + ) + job_config = bigquery.QueryJobConfig() + # TODO(swast): plumb through the api_name of the user-facing api that + # caused this query. + return self._run_execute_query( + sql=sql, + job_config=job_config, + ) + + def export( + self, + array_value, + col_id_overrides: Mapping[str, str], + destination: bigquery.TableReference, + if_exists: Literal["fail", "replace", "append"] = "fail", + cluster_cols: Sequence[str] = [], + ): + """ + Export the ArrayValue to an existing BigQuery table. + """ + dispositions = { + "fail": bigquery.WriteDisposition.WRITE_EMPTY, + "replace": bigquery.WriteDisposition.WRITE_TRUNCATE, + "append": bigquery.WriteDisposition.WRITE_APPEND, + } + sql = self.to_sql(array_value, ordered=False, col_id_overrides=col_id_overrides) + job_config = bigquery.QueryJobConfig( + write_disposition=dispositions[if_exists], + destination=destination, + clustering_fields=cluster_cols if cluster_cols else None, + ) + # TODO(swast): plumb through the api_name of the user-facing api that + # caused this query. + return self._run_execute_query( + sql=sql, + job_config=job_config, + ) + + def dry_run(self, array_value: bigframes.core.ArrayValue, ordered: bool = True): + """ + Dry run executing the ArrayValue. + + Does not actually execute the data but will get stats and indicate any invalid query errors. + """ + sql = self.to_sql(array_value, ordered=ordered) + job_config = bigquery.QueryJobConfig(dry_run=True) + bq_io.add_labels(job_config) + query_job = self.bqclient.query(sql, job_config=job_config) + results_iterator = query_job.result() + return results_iterator, query_job + + def peek( + self, array_value: bigframes.core.ArrayValue, n_rows: int + ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """A 'peek' efficiently accesses a small number of rows in the dataframe.""" + if not tree_properties.peekable(self._with_cached_executions(array_value.node)): + warnings.warn("Peeking this value cannot be done efficiently.") + sql = self.compiler.compile_peek( + self._with_cached_executions(array_value.node), n_rows + ) + + # TODO(swast): plumb through the api_name of the user-facing api that + # caused this query. + return self._run_execute_query( + sql=sql, + ) + + # Helpers + def _run_execute_query( + self, + sql: str, + job_config: Optional[bq_job.QueryJobConfig] = None, + api_name: Optional[str] = None, + ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """ + Starts BigQuery query job and waits for results. + """ + job_config = bq_job.QueryJobConfig() if job_config is None else job_config + if bigframes.options.compute.maximum_bytes_billed is not None: + job_config.maximum_bytes_billed = ( + bigframes.options.compute.maximum_bytes_billed + ) + # Note: add_labels is global scope which may have unexpected effects + bq_io.add_labels(job_config, api_name=api_name) + + if not self.strictly_ordered: + job_config.labels["bigframes-mode"] = "unordered" + try: + query_job = self.bqclient.query(sql, job_config=job_config) + opts = bigframes.options.display + if opts.progress_bar is not None and not query_job.configuration.dry_run: + results_iterator = formatting_helpers.wait_for_query_job( + query_job, progress_bar=opts.progress_bar + ) + else: + results_iterator = query_job.result() + + if self.metrics is not None: + self.metrics.count_job_stats(query_job) + return results_iterator, query_job + + except google.api_core.exceptions.BadRequest as e: + # Unfortunately, this error type does not have a separate error code or exception type + if "Resources exceeded during query execution" in e.message: + new_message = "Computation is too complex to execute as a single query. Try using DataFrame.cache() on intermediate results, or setting bigframes.options.compute.enable_multi_query_execution." + raise bigframes.exceptions.QueryComplexityError(new_message) from e + else: + raise + + def _with_cached_executions(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: + return tree_properties.replace_nodes(node, (dict(self._cached_executions))) + + def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): + """ + Can the block be evaluated very cheaply? + If True, the array_value probably is not worth caching. + """ + # Once rewriting is available, will want to rewrite before + # evaluating execution cost. + return tree_properties.is_trivially_executable( + self._with_cached_executions(array_value.node) + ) + + def _cache_with_cluster_cols( + self, array_value: bigframes.core.ArrayValue, cluster_cols: Sequence[str] + ): + """Executes the query and uses the resulting table to rewrite future executions.""" + + sql, schema, ordering_info = self.compiler.compile_raw( + self._with_cached_executions(array_value.node) + ) + tmp_table = self._sql_as_cached_temp_table( + sql, + schema, + cluster_cols=bq_io.select_cluster_cols(schema, cluster_cols), + ) + cached_replacement = array_value.as_cached( + cache_table=self.bqclient.get_table(tmp_table), + ordering=ordering_info, + ).node + self._cached_executions[array_value.node] = cached_replacement + + def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): + """Executes the query and uses the resulting table to rewrite future executions.""" + + if not self.strictly_ordered: + raise ValueError( + "Caching with offsets only supported in strictly ordered mode." + ) + offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") + node_w_offsets = array_value.promote_offsets(offset_column).node + sql = self.compiler.compile_unordered( + self._with_cached_executions(node_w_offsets) + ) + + tmp_table = self._sql_as_cached_temp_table( + sql, + node_w_offsets.schema.to_bigquery(), + cluster_cols=[offset_column], + ) + cached_replacement = array_value.as_cached( + cache_table=self.bqclient.get_table(tmp_table), + ordering=order.TotalOrdering.from_offset_col(offset_column), + ).node + self._cached_executions[array_value.node] = cached_replacement + + def _cache_with_session_awareness( + self, + array_value: bigframes.core.ArrayValue, + session_forest: Iterable[nodes.BigFrameNode], + ) -> None: + # These node types are cheap to re-compute + target, cluster_cols = bigframes.session.planner.session_aware_cache_plan( + array_value.node, list(session_forest) + ) + if len(cluster_cols) > 0: + self._cache_with_cluster_cols( + bigframes.core.ArrayValue(target), cluster_cols + ) + elif self.strictly_ordered: + self._cache_with_offsets(bigframes.core.ArrayValue(target)) + else: + self._cache_with_cluster_cols(bigframes.core.ArrayValue(target), []) + + def _simplify_with_caching(self, array_value: bigframes.core.ArrayValue): + """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" + # Apply existing caching first + for _ in range(MAX_SUBTREE_FACTORINGS): + node_with_cache = self._with_cached_executions(array_value.node) + if node_with_cache.planning_complexity < QUERY_COMPLEXITY_LIMIT: + return + + did_cache = self._cache_most_complex_subtree(array_value.node) + if not did_cache: + return + + def _cache_most_complex_subtree(self, node: nodes.BigFrameNode) -> bool: + # TODO: If query fails, retry with lower complexity limit + selection = tree_properties.select_cache_target( + node, + min_complexity=(QUERY_COMPLEXITY_LIMIT / 500), + max_complexity=QUERY_COMPLEXITY_LIMIT, + cache=dict(self._cached_executions), + # Heuristic: subtree_compleixty * (copies of subtree)^2 + heuristic=lambda complexity, count: math.log(complexity) + + 2 * math.log(count), + ) + if selection is None: + # No good subtrees to cache, just return original tree + return False + + self._cache_with_cluster_cols(bigframes.core.ArrayValue(selection), []) + return True + + def _sql_as_cached_temp_table( + self, + sql: str, + schema: Sequence[bigquery.SchemaField], + cluster_cols: Sequence[str], + ) -> bigquery.TableReference: + assert len(cluster_cols) <= _MAX_CLUSTER_COLUMNS + temp_table = self.storage_manager.create_temp_table(schema, cluster_cols) + + # TODO: Get default job config settings + job_config = cast( + bigquery.QueryJobConfig, + bigquery.QueryJobConfig.from_api_repr({}), + ) + job_config.destination = temp_table + _, query_job = self._run_execute_query( + sql, + job_config=job_config, + api_name="cached", + ) + return query_job.destination diff --git a/bigframes/session/metrics.py b/bigframes/session/metrics.py new file mode 100644 index 00000000000..85a7f6aa4b0 --- /dev/null +++ b/bigframes/session/metrics.py @@ -0,0 +1,88 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import dataclasses +import os +from typing import Optional, Tuple + +import google.cloud.bigquery as bigquery +import google.cloud.bigquery.job as bq_job + +LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" + + +@dataclasses.dataclass +class ExecutionMetrics: + execution_count: int = 0 + slot_millis: int = 0 + bytes_processed: int = 0 + + def count_job_stats(self, query_job: bq_job.QueryJob): + stats = get_performance_stats(query_job) + if stats is not None: + bytes_processed, slot_millis = stats + self.execution_count += 1 + self.bytes_processed += bytes_processed + self.slot_millis += slot_millis + if LOGGING_NAME_ENV_VAR in os.environ: + # when running notebooks via pytest nbmake + write_stats_to_disk(bytes_processed, slot_millis) + + +def get_performance_stats(query_job: bigquery.QueryJob) -> Optional[Tuple[int, int]]: + """Parse the query job for performance stats. + + Return None if the stats do not reflect real work done in bigquery. + """ + bytes_processed = query_job.total_bytes_processed + if not isinstance(bytes_processed, int): + return None # filter out mocks + if query_job.configuration.dry_run: + # dry run stats are just predictions of the real run + bytes_processed = 0 + + slot_millis = query_job.slot_millis + if not isinstance(slot_millis, int): + return None # filter out mocks + if query_job.configuration.dry_run: + # dry run stats are just predictions of the real run + slot_millis = 0 + + return bytes_processed, slot_millis + + +def write_stats_to_disk(bytes_processed: int, slot_millis: int): + """For pytest runs only, log information about the query job + to a file in order to create a performance report. + """ + if LOGGING_NAME_ENV_VAR not in os.environ: + raise EnvironmentError( + "Environment variable {env_var} is not set".format( + env_var=LOGGING_NAME_ENV_VAR + ) + ) + test_name = os.environ[LOGGING_NAME_ENV_VAR] + current_directory = os.getcwd() + + # store bytes processed + bytes_file = os.path.join(current_directory, test_name + ".bytesprocessed") + with open(bytes_file, "a") as f: + f.write(str(bytes_processed) + "\n") + + # store slot milliseconds + bytes_file = os.path.join(current_directory, test_name + ".slotmillis") + with open(bytes_file, "a") as f: + f.write(str(slot_millis) + "\n") diff --git a/bigframes/session/temp_storage.py b/bigframes/session/temp_storage.py new file mode 100644 index 00000000000..fb8c4bac7a8 --- /dev/null +++ b/bigframes/session/temp_storage.py @@ -0,0 +1,96 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +from typing import List, Optional, Sequence +import uuid + +import google.cloud.bigquery as bigquery + +import bigframes.constants as constants +import bigframes.session._io.bigquery as bf_io_bigquery + +_TEMP_TABLE_ID_FORMAT = "bqdf{date}_{session_id}_{random_id}" + + +class TemporaryGbqStorageManager: + """ + Responsible for allocating and cleaning up temporary gbq tables used by a BigFrames session. + """ + + def __init__( + self, + bqclient: bigquery.Client, + dataset: bigquery.DatasetReference, + session_id: str, + *, + kms_key: Optional[str] = None + ): + self.bqclient = bqclient + self.dataset = dataset + self.session_id = session_id + self._table_ids: List[str] = [] + self._kms_key = kms_key + + def 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. + expiration = ( + datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION + ) + table = bf_io_bigquery.create_temp_table( + self.bqclient, + self._random_table(), + expiration, + schema=schema, + cluster_columns=list(cluster_cols), + kms_key=self._kms_key, + ) + return bigquery.TableReference.from_string(table) + + def _random_table(self, skip_cleanup: bool = False) -> 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. + + Args: + skip_cleanup (bool, default False): + If True, do not add the generated ID to the list of tables + to clean up when the session is closed. + + Returns: + google.cloud.bigquery.TableReference: + Fully qualified table ID of a table that doesn't exist. + """ + now = datetime.datetime.now(datetime.timezone.utc) + random_id = uuid.uuid4().hex + 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) diff --git a/tests/system/large/test_session.py b/tests/system/large/test_session.py index 2b82d0133ba..9f42c4ae942 100644 --- a/tests/system/large/test_session.py +++ b/tests/system/large/test_session.py @@ -59,7 +59,7 @@ def test_read_gbq_for_large_tables( assert len(df.columns) != 0 -def test_close(session): +def test_close(session: bigframes.Session): # we will create two tables and confirm that they are deleted # when the session is closed @@ -69,8 +69,12 @@ def test_close(session): datetime.datetime.now(datetime.timezone.utc) + bigframes.constants.DEFAULT_EXPIRATION ) - full_id_1 = bigframes.session._io.bigquery.create_temp_table(session, expiration) - full_id_2 = bigframes.session._io.bigquery.create_temp_table(session, expiration) + 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 + ) # check that the tables were actually created assert bqclient.get_table(full_id_1).created is not None @@ -101,8 +105,12 @@ def test_clean_up_by_session_id(): datetime.datetime.now(datetime.timezone.utc) + bigframes.constants.DEFAULT_EXPIRATION ) - bigframes.session._io.bigquery.create_temp_table(session, expiration) - bigframes.session._io.bigquery.create_temp_table(session, expiration) + bigframes.session._io.bigquery.create_temp_table( + session.bqclient, session._temp_storage_manager._random_table(), expiration + ) + bigframes.session._io.bigquery.create_temp_table( + session.bqclient, session._temp_storage_manager._random_table(), expiration + ) # check that some table exists with the expected session_id tables_before = bqclient.list_tables( diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index d838251dca6..e1644c20b40 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2179,10 +2179,10 @@ def test_binop_with_self_aggregate(session, scalars_dfs): df_columns = ["int64_col", "float64_col", "int64_too"] # Ensure that this takes the optimized single-query path by counting executions - execution_count_before = session._execution_count + execution_count_before = session._metrics.execution_count bf_df = scalars_df[df_columns] bf_result = (bf_df - bf_df.mean()).to_pandas() - execution_count_after = session._execution_count + execution_count_after = session._metrics.execution_count pd_df = scalars_pandas_df[df_columns] pd_result = pd_df - pd_df.mean() diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 428a6a28bf0..65a98b014dc 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -86,10 +86,10 @@ 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._random_table() + load_table = session_with_bq_cmek._temp_storage_manager._random_table() df = pandas.DataFrame({"col0": [1, 2, 3]}) - load_job_config = session_with_bq_cmek._prepare_load_job_config() + load_job_config = bigquery.LoadJobConfig() load_job_config.schema = [ bigquery.SchemaField(df.columns[0], bigquery.enums.SqlTypeNames.INT64) ] @@ -186,7 +186,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._random_table() + output_table_ref = session_with_bq_cmek._temp_storage_manager._random_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) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 2f779f337e4..5b5db74ea6a 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -556,7 +556,8 @@ def test_read_gbq_with_custom_global_labels( bigframes.options.compute.assign_extra_query_labels(test1=1, test2="abc") bigframes.options.compute.extra_query_labels["test3"] = False - job_labels = session.read_gbq(scalars_table_id).query_job.labels # type:ignore + query_job = session.read_gbq(scalars_table_id).query_job + job_labels = query_job.labels # type:ignore expected_labels = {"test1": "1", "test2": "abc", "test3": "false"} # All jobs should include a bigframes-api key. See internal issue 336521938. diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index 1d6be3dff8a..46c3c920369 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -155,7 +155,12 @@ def test_create_temp_table_default_expiration(): ) session = resources.create_bigquery_session() - bigframes.session._io.bigquery.create_temp_table(session, expiration) + table_ref = bigquery.TableReference.from_string( + "test-project.test_dataset.bqdf_new_random_table" + ) + bigframes.session._io.bigquery.create_temp_table( + session.bqclient, table_ref, expiration + ) session.bqclient.create_table.assert_called_once() call_args = session.bqclient.create_table.call_args From 6bc6a41426fbbb60e77cd77f80860f88a1751a4b Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:14:10 -0700 Subject: [PATCH 759/921] feat: add llm.TextEmbeddingGenerator to support new embedding models (#905) * feat: add llm.TextEmbeddingGenerator to support new embedding models * fix docs --- bigframes/ml/llm.py | 166 +++++++++++++++++++++++++++++- bigframes/ml/loader.py | 3 + tests/system/small/ml/test_llm.py | 41 ++++++++ 3 files changed, 207 insertions(+), 3 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 2517178d890..45634423c68 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -40,11 +40,18 @@ _EMBEDDING_GENERATOR_GECKO_ENDPOINT = "textembedding-gecko" _EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT = "textembedding-gecko-multilingual" -_EMBEDDING_GENERATOR_ENDPOINTS = ( +_PALM2_EMBEDDING_GENERATOR_ENDPOINTS = ( _EMBEDDING_GENERATOR_GECKO_ENDPOINT, _EMBEDDING_GENERATOR_GECKO_MULTILINGUAL_ENDPOINT, ) +_TEXT_EMBEDDING_004_ENDPOINT = "text-embedding-004" +_TEXT_MULTILINGUAL_EMBEDDING_002_ENDPOINT = "text-multilingual-embedding-002" +_TEXT_EMBEDDING_ENDPOINTS = ( + _TEXT_EMBEDDING_004_ENDPOINT, + _TEXT_MULTILINGUAL_EMBEDDING_002_ENDPOINT, +) + _GEMINI_PRO_ENDPOINT = "gemini-pro" _GEMINI_1P5_PRO_PREVIEW_ENDPOINT = "gemini-1.5-pro-preview-0514" _GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT = "gemini-1.5-flash-preview-0514" @@ -57,6 +64,7 @@ _ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" _ML_EMBED_TEXT_STATUS = "ml_embed_text_status" +_ML_GENERATE_EMBEDDING_STATUS = "ml_generate_embedding_status" @log_adapter.class_logger @@ -387,6 +395,10 @@ def to_gbq(self, model_name: str, replace: bool = False) -> PaLM2TextGenerator: class PaLM2TextEmbeddingGenerator(base.BaseEstimator): """PaLM2 text embedding generator LLM model. + .. note:: + Models in this class are outdated and going to be deprecated. To use the most updated text embedding models, go to the TextEmbeddingGenerator class. + + Args: model_name (str, Default to "textembedding-gecko"): The model for text embedding. “textembedding-gecko” returns model embeddings for text inputs. @@ -447,9 +459,9 @@ def _create_bqml_model(self): iam_role="aiplatform.user", ) - if self.model_name not in _EMBEDDING_GENERATOR_ENDPOINTS: + if self.model_name not in _PALM2_EMBEDDING_GENERATOR_ENDPOINTS: raise ValueError( - f"Model name {self.model_name} is not supported. We only support {', '.join(_EMBEDDING_GENERATOR_ENDPOINTS)}." + f"Model name {self.model_name} is not supported. We only support {', '.join(_PALM2_EMBEDDING_GENERATOR_ENDPOINTS)}." ) endpoint = ( @@ -551,6 +563,154 @@ def to_gbq( return new_model.session.read_gbq_model(model_name) +@log_adapter.class_logger +class TextEmbeddingGenerator(base.BaseEstimator): + """Text embedding generator LLM model. + + Args: + model_name (str, Default to "text-embedding-004"): + The model for text embedding. Possible values are "text-embedding-004" or "text-multilingual-embedding-002". + text-embedding models returns model embeddings for text inputs. + text-multilingual-embedding models returns model embeddings for text inputs which support over 100 languages. + Default to "text-embedding-004". + session (bigframes.Session or None): + BQ session to create the model. If None, use the global default session. + connection_name (str or None): + Connection to connect with remote service. str of the format ... + If None, use default connection in session context. + """ + + def __init__( + self, + *, + model_name: Literal[ + "text-embedding-004", "text-multilingual-embedding-002" + ] = "text-embedding-004", + session: Optional[bigframes.Session] = None, + connection_name: Optional[str] = None, + ): + self.model_name = model_name + self.session = session or bpd.get_global_session() + self._bq_connection_manager = self.session.bqconnectionmanager + + connection_name = connection_name or self.session._bq_connection + self.connection_name = clients.resolve_full_bq_connection_name( + connection_name, + default_project=self.session._project, + default_location=self.session._location, + ) + + self._bqml_model_factory = globals.bqml_model_factory() + self._bqml_model: core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + # Parse and create connection if needed. + if not self.connection_name: + raise ValueError( + "Must provide connection_name, either in constructor or through session options." + ) + + if self._bq_connection_manager: + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", + ) + + if self.model_name not in _TEXT_EMBEDDING_ENDPOINTS: + raise ValueError( + f"Model name {self.model_name} is not supported. We only support {', '.join(_TEXT_EMBEDDING_ENDPOINTS)}." + ) + + options = { + "endpoint": self.model_name, + } + return self._bqml_model_factory.create_remote_model( + session=self.session, connection_name=self.connection_name, options=options + ) + + @classmethod + def _from_bq( + cls, session: bigframes.Session, bq_model: bigquery.Model + ) -> TextEmbeddingGenerator: + assert bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in bq_model._properties + assert "endpoint" in bq_model._properties["remoteModelInfo"] + assert "connection" in bq_model._properties["remoteModelInfo"] + + # Parse the remote model endpoint + bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] + model_connection = bq_model._properties["remoteModelInfo"]["connection"] + model_endpoint = bqml_endpoint.split("/")[-1] + + model = cls( + session=session, + model_name=model_endpoint, # type: ignore + connection_name=model_connection, + ) + + model._bqml_model = core.BqmlModel(session, bq_model) + return model + + def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: + """Predict the result from input DataFrame. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame, which needs to contain a column with name "content". Only the column will be used as input. Content can include preamble, questions, suggestions, instructions, or examples. + + Returns: + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. + """ + + # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models + (X,) = utils.convert_to_dataframe(X) + + if len(X.columns) != 1: + raise ValueError( + f"Only support one column as input. {constants.FEEDBACK_LINK}" + ) + + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "content"}) + + options = { + "flatten_json_output": True, + } + + df = self._bqml_model.generate_embedding(X, options) + + if (df[_ML_GENERATE_EMBEDDING_STATUS] != "").any(): + warnings.warn( + f"Some predictions failed. Check column {_ML_GENERATE_EMBEDDING_STATUS} for detailed status. You may want to filter the failed rows and retry.", + RuntimeWarning, + ) + + return df + + def to_gbq(self, model_name: str, replace: bool = False) -> TextEmbeddingGenerator: + """Save the model to BigQuery. + + Args: + model_name (str): + The name of the model. + replace (bool, default False): + Determine whether to replace if the model already exists. Default to False. + + Returns: + TextEmbeddingGenerator: Saved model.""" + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) + + @log_adapter.class_logger class GeminiTextGenerator(base.BaseEstimator): """Gemini text generator LLM model. diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 515fb50c6fb..bd01342152e 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -63,6 +63,8 @@ llm._GEMINI_PRO_ENDPOINT: llm.GeminiTextGenerator, llm._GEMINI_1P5_PRO_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, llm._GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, + llm._TEXT_EMBEDDING_004_ENDPOINT: llm.TextEmbeddingGenerator, + llm._TEXT_MULTILINGUAL_EMBEDDING_002_ENDPOINT: llm.TextEmbeddingGenerator, } ) @@ -84,6 +86,7 @@ def from_bq( imported.XGBoostModel, llm.PaLM2TextGenerator, llm.PaLM2TextEmbeddingGenerator, + llm.TextEmbeddingGenerator, pipeline.Pipeline, compose.ColumnTransformer, preprocessing.PreprocessingType, diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index b926004fd80..c2f62096d0e 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -304,6 +304,47 @@ def test_embedding_generator_predict_series_success( assert len(value) == 768 +@pytest.mark.parametrize( + "model_name", + ("text-embedding-004", "text-multilingual-embedding-002"), +) +def test_create_load_text_embedding_generator_model( + dataset_id, model_name, session, bq_connection +): + text_embedding_model = llm.TextEmbeddingGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + assert text_embedding_model is not None + assert text_embedding_model._bqml_model is not None + + # save, load to ensure configuration was kept + reloaded_model = text_embedding_model.to_gbq( + f"{dataset_id}.temp_text_model", replace=True + ) + assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.connection_name == bq_connection + assert reloaded_model.model_name == model_name + + +@pytest.mark.parametrize( + "model_name", + ("text-embedding-004", "text-multilingual-embedding-002"), +) +@pytest.mark.flaky(retries=2) +def test_gemini_text_embedding_generator_predict_default_params_success( + llm_text_df, model_name, session, bq_connection +): + text_embedding_model = llm.TextEmbeddingGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + df = text_embedding_model.predict(llm_text_df).to_pandas() + assert df.shape == (3, 4) + assert "ml_generate_embedding_result" in df.columns + series = df["ml_generate_embedding_result"] + value = series[0] + assert len(value) == 768 + + @pytest.mark.parametrize( "model_name", ("gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514"), From d9b8ef56deb0c776edeeb0112bd9d35d5ed1b70e Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:32:58 -0700 Subject: [PATCH 760/921] docs: update embedding model notebooks (#906) * feat: add llm.TextEmbeddingGenerator to support new embedding models * fix docs * docs: update embedding notebooks --- .../bq_dataframes_llm_code_generation.ipynb | 4 +- .../bq_dataframes_llm_kmeans.ipynb | 1091 +++++++++-------- 2 files changed, 553 insertions(+), 542 deletions(-) diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index 4ea766604de..c0c3c58a3c5 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -89,7 +89,7 @@ "\n", "The steps include:\n", "\n", - "- Defining an LLM model in BigQuery DataFrames, specifically the [`text-bison` model of the PaLM API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text), using `bigframes.ml.llm`.\n", + "- Defining an LLM model in BigQuery DataFrames, specifically the [Gemini Model](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models), using `bigframes.ml.llm`.\n", "- Creating a DataFrame by reading in data from Cloud Storage.\n", "- Manipulating data in the DataFrame to build LLM prompts.\n", "- Sending DataFrame prompts to the LLM model using the `predict` method.\n", @@ -385,7 +385,7 @@ "source": [ "# Define the LLM model\n", "\n", - "BigQuery DataFrames provides integration with [`text-bison` model of the PaLM API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) via Vertex AI.\n", + "BigQuery DataFrames provides integration with [Gemini Models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) via Vertex AI.\n", "\n", "This section walks through a few steps required in order to use the model in your notebook." ] diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index ab6fd93f9a3..d49a44a7804 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -57,9 +57,9 @@ "source": [ "## Overview\n", "\n", - "The goal of this notebook is to demonstrate a comment characterization algorithm for an online business. We will accomplish this using [Google's PaLM 2](https://ai.google/discover/palm2/) and [KMeans clustering](https://en.wikipedia.org/wiki/K-means_clustering) in three steps:\n", + "The goal of this notebook is to demonstrate a comment characterization algorithm for an online business. We will accomplish this using [Google's Embedding Models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#models) and [KMeans clustering](https://en.wikipedia.org/wiki/K-means_clustering) in three steps:\n", "\n", - "1. Use PaLM2TextEmbeddingGenerator to [generate text embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) for each of 10000 complaints sent to an online bank. If you're not familiar with what a text embedding is, it's a list of numbers that are like coordinates in an imaginary \"meaning space\" for sentences. (It's like [word embeddings](https://en.wikipedia.org/wiki/Word_embedding), but for more general text.) The important point for our purposes is that similar sentences are close to each other in this imaginary space.\n", + "1. Use TextEmbeddingGenerator to [generate text embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) for each of 10000 complaints sent to an online bank. If you're not familiar with what a text embedding is, it's a list of numbers that are like coordinates in an imaginary \"meaning space\" for sentences. (It's like [word embeddings](https://en.wikipedia.org/wiki/Word_embedding), but for more general text.) The important point for our purposes is that similar sentences are close to each other in this imaginary space.\n", "2. Use KMeans clustering to group together complaints whose text embeddings are near to eachother. This will give us sets of similar complaints, but we don't yet know _why_ these complaints are similar.\n", "3. Prompt GeminiTextGenerator in English asking what the difference is between the groups of complaints that we got. Thanks to the power of modern LLMs, the response might give us a very good idea of what these complaints are all about, but remember to [\"understand the limits of your dataset and model.\"](https://ai.google/responsibility/responsible-ai-practices/#:~:text=Understand%20the%20limitations%20of%20your%20dataset%20and%20model)\n", "\n", @@ -289,7 +289,7 @@ { "data": { "text/html": [ - "Query job 952b852e-7cf0-493d-8258-fe60daf45ebf is DONE. 2.3 GB processed. Open Job" + "Query job 960f637d-89eb-4bbf-a34c-36ed624e8e9a is DONE. 2.3 GB processed. Open Job" ], "text/plain": [ "" @@ -301,7 +301,7 @@ { "data": { "text/html": [ - "Query job f9939880-6c66-4da5-9e90-daf8d9a9d83c is DONE. 50.3 MB processed. Open Job" + "Query job 59bb207c-98e1-4dab-8686-320f276b09df is DONE. 63.7 MB processed. Open Job" ], "text/plain": [ "" @@ -336,24 +336,24 @@ " \n", " \n", " \n", - " 1799560\n", - " Thursday, XX/XX/XXXX, unauthorized charges wer...\n", + " 2557016\n", + " I've been disputing fraud accounts on my credi...\n", " \n", " \n", - " 1800272\n", - " The credit reporting company is reporting inac...\n", + " 2557686\n", + " American Express Platinum totally messed up my...\n", " \n", " \n", - " 1800409\n", - " In accordance with the Fair Credit Reporting a...\n", + " 2558170\n", + " I recently looked at my credit report and noti...\n", " \n", " \n", - " 1800550\n", - " I told the credit bureaus to \" investigate eve...\n", + " 2558545\n", + " Select Portfolio Servicing contacted my insura...\n", " \n", " \n", - " 1800818\n", - " Im writing in reference regarding XXXXXXXX XXX...\n", + " 2558652\n", + " I checked my credit report and I am upset on w...\n", " \n", " \n", "\n", @@ -361,11 +361,11 @@ ], "text/plain": [ " consumer_complaint_narrative\n", - "1799560 Thursday, XX/XX/XXXX, unauthorized charges wer...\n", - "1800272 The credit reporting company is reporting inac...\n", - "1800409 In accordance with the Fair Credit Reporting a...\n", - "1800550 I told the credit bureaus to \" investigate eve...\n", - "1800818 Im writing in reference regarding XXXXXXXX XXX..." + "2557016 I've been disputing fraud accounts on my credi...\n", + "2557686 American Express Platinum totally messed up my...\n", + "2558170 I recently looked at my credit report and noti...\n", + "2558545 Select Portfolio Servicing contacted my insura...\n", + "2558652 I checked my credit report and I am upset on w..." ] }, "execution_count": 7, @@ -418,7 +418,7 @@ { "data": { "text/html": [ - "Query job e3ff0549-f0ee-4508-bb4f-beea14bf54f5 is DONE. 0 Bytes processed. Open Job" + "Query job e4616b5e-b4c0-490c-a249-484f373f89d9 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -429,9 +429,9 @@ } ], "source": [ - "from bigframes.ml.llm import PaLM2TextEmbeddingGenerator\n", + "from bigframes.ml.llm import TextEmbeddingGenerator\n", "\n", - "model = PaLM2TextEmbeddingGenerator() # No connection id needed" + "model = TextEmbeddingGenerator() # No connection id needed" ] }, { @@ -444,7 +444,7 @@ { "data": { "text/html": [ - "Query job 5b3d8f8c-9e8d-4378-b4df-e3328300f17a is DONE. 1.3 GB processed. Open Job" + "Query job 89f96e88-2dd5-4326-8912-925b237e2877 is DONE. 1.3 GB processed. Open Job" ], "text/plain": [ "" @@ -454,21 +454,17 @@ "output_type": "display_data" }, { - "data": { - "text/html": [ - "Query job f35c2982-4953-45fa-84bd-d0ce04e13c5e is DONE. 80.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/__init__.py:108: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] }, { "data": { "text/html": [ - "Query job b70c55a3-b18b-4313-86b0-31f5b3b570fb is DONE. 20.0 kB processed. Open Job" + "Query job bcdbfe96-2cce-4269-81f4-0334033b458b is DONE. 20.0 kB processed. Open Job" ], "text/plain": [ "" @@ -480,7 +476,7 @@ { "data": { "text/html": [ - "Query job 2b2cfd9f-c713-4411-a3ca-1916cec84ff0 is DONE. 0 Bytes processed. Open Job" + "Query job 3b89850f-4491-4343-912a-7a2fd3137790 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -492,7 +488,7 @@ { "data": { "text/html": [ - "Query job 09cadae1-1c66-43cf-a76f-7495b0123006 is DONE. 71.9 MB processed. Open Job" + "Query job a2999e90-8d14-4f4a-99dc-4e769df01837 is DONE. 72.0 MB processed. Open Job" ], "text/plain": [ "" @@ -522,187 +518,188 @@ " \n", " \n", " \n", - " text_embedding\n", - " statistics\n", - " ml_embed_text_status\n", + " ml_generate_embedding_result\n", + " ml_generate_embedding_statistics\n", + " ml_generate_embedding_status\n", " content\n", " \n", " \n", " \n", " \n", - " 782\n", - " [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-...\n", - " {\"token_count\":121,\"truncated\":false}\n", + " 415\n", + " [ 2.56774724e-02 -1.06168222e-02 3.06945704e-...\n", + " {\"token_count\":171,\"truncated\":false}\n", " \n", - " I 've sent multiple letters to this agency abo...\n", + " DEPT OF EDUCATION/XXXX is stating I was late ...\n", " \n", " \n", - " 795\n", - " [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-...\n", - " {\"token_count\":141,\"truncated\":false}\n", + " 596\n", + " [ 5.90653270e-02 -9.31344274e-03 -7.12460047e-...\n", + " {\"token_count\":668,\"truncated\":false}\n", " \n", - " I receive social security XXXX funds in my XXX...\n", + " I alerted my credit card company XX/XX/2017 th...\n", " \n", " \n", - " 861\n", - " [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-...\n", - " {\"token_count\":160,\"truncated\":false}\n", + " 706\n", + " [ 0.01298233 0.00130001 0.01800315 0.037078...\n", + " {\"token_count\":252,\"truncated\":false}\n", " \n", - " Hello, My name is XXXX XXXX XXXX. I have a pro...\n", + " Sallie mae is corrupt. \n", + "I have tried to talk t...\n", " \n", " \n", - " 1103\n", - " [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-...\n", - " {\"token_count\":31,\"truncated\":false}\n", + " 804\n", + " [-1.39777679e-02 1.68943349e-02 5.53999236e-...\n", + " {\"token_count\":412,\"truncated\":false}\n", " \n", - " The debt occurred more than 7 years in the pas...\n", + " In accordance with the Fair Credit Reporting a...\n", " \n", " \n", - " 1241\n", - " [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-...\n", - " {\"token_count\":23,\"truncated\":false}\n", + " 861\n", + " [ 2.33309343e-02 -2.36528926e-03 3.37129943e-...\n", + " {\"token_count\":160,\"truncated\":false}\n", " \n", - " UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA...\n", + " Hello, My name is XXXX XXXX XXXX. I have a pro...\n", " \n", " \n", - " 1729\n", - " [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-...\n", - " {\"token_count\":382,\"truncated\":false}\n", + " 1030\n", + " [ 0.06060313 -0.06495965 -0.03605044 -0.028016...\n", + " {\"token_count\":298,\"truncated\":false}\n", " \n", - " XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort...\n", + " Hello, I would like to complain about PayPal H...\n", " \n", " \n", - " 2167\n", - " [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-...\n", - " {\"token_count\":556,\"truncated\":false}\n", + " 1582\n", + " [ 0.01255985 -0.01652482 -0.02638046 0.036858...\n", + " {\"token_count\":814,\"truncated\":false}\n", " \n", - " This is the third such complaint I have submit...\n", + " Transunion is listing personal information ( n...\n", " \n", " \n", - " 2219\n", - " [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-...\n", - " {\"token_count\":196,\"truncated\":false}\n", + " 1600\n", + " [ 5.13355099e-02 4.01246967e-03 5.72342947e-...\n", + " {\"token_count\":653,\"truncated\":false}\n", " \n", - " Found and add online for a Prepaid Credit card...\n", + " On XX/XX/XXXX, I called Citizen Bank at XXXX t...\n", " \n", " \n", - " 2392\n", - " [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-...\n", - " {\"token_count\":641,\"truncated\":false}\n", + " 2060\n", + " [ 6.44792162e-04 4.95899878e-02 4.67925966e-...\n", + " {\"token_count\":136,\"truncated\":false}\n", " \n", - " I am furnishing this complaint against Fed Loa...\n", + " Theses names are the known liars that I have s...\n", " \n", " \n", - " 2528\n", - " [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-...\n", - " {\"token_count\":176,\"truncated\":false}\n", + " 2283\n", + " [ 4.71848622e-02 -8.68239347e-03 5.80501892e-...\n", + " {\"token_count\":478,\"truncated\":false}\n", " \n", - " Despite multiple written requests, the unverif...\n", + " My house was hit by a tree XX/XX/2018. My insu...\n", " \n", " \n", - " 2737\n", - " [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-...\n", - " {\"token_count\":230,\"truncated\":false}\n", + " 2421\n", + " [-2.90394691e-03 -1.81679502e-02 -7.99657404e-...\n", + " {\"token_count\":389,\"truncated\":false}\n", " \n", - " After unsatisfying communication in the messag...\n", + " I became aware of a credit inquiry on my XXXX...\n", " \n", " \n", - " 2859\n", - " [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-...\n", - " {\"token_count\":238,\"truncated\":false}\n", + " 2422\n", + " [-6.70500053e-03 1.51133696e-02 4.94448021e-...\n", + " {\"token_count\":124,\"truncated\":false}\n", " \n", - " Good Morning. My name is XXXX XXXX. My account...\n", + " I have sent numerous letters, police reports a...\n", " \n", " \n", - " 3439\n", - " [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-...\n", - " {\"token_count\":197,\"truncated\":false}\n", + " 2658\n", + " [ 6.70989677e-02 -3.53626162e-02 1.08648362e-...\n", + " {\"token_count\":762,\"truncated\":false}\n", " \n", - " I have ongoing disputes that are preventing me...\n", + " This letter concerns two disputes ( chargeback...\n", " \n", " \n", - " 3738\n", - " [ 0.01422119 -0.01114973 -0.04438976 -0.024421...\n", - " {\"token_count\":160,\"truncated\":false}\n", + " 2883\n", + " [-1.28255319e-02 -1.89735275e-02 5.68657108e-...\n", + " {\"token_count\":71,\"truncated\":false}\n", " \n", - " I had a loan with national Collegiate Trust. i...\n", + " It is very frustrating that this has been goin...\n", " \n", " \n", - " 3805\n", - " [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-...\n", - " {\"token_count\":477,\"truncated\":false}\n", + " 2951\n", + " [ 3.23301251e-03 -2.61142217e-02 1.31891826e-...\n", + " {\"token_count\":95,\"truncated\":false}\n", " \n", - " Hi I am submitting this XXXX XXXX this isn't a...\n", + " I, the consumer, in fact, have a right to priv...\n", " \n", " \n", - " 3915\n", - " [-7.23852217e-03 -4.69538383e-02 -5.60489520e-...\n", - " {\"token_count\":116,\"truncated\":false}\n", + " 2992\n", + " [-2.22910382e-03 -1.07050659e-02 4.74211425e-...\n", + " {\"token_count\":407,\"truncated\":false}\n", " \n", - " portfolio is showin on my credit report with a...\n", + " XXXX XXXX XXXX should not be reporting to Expe...\n", " \n", " \n", - " 3917\n", - " [-8.92711710e-03 -4.49132621e-02 -4.29662578e-...\n", - " {\"token_count\":71,\"truncated\":false}\n", + " 3969\n", + " [ 1.58297736e-02 3.01055871e-02 5.60088176e-...\n", + " {\"token_count\":287,\"truncated\":false}\n", " \n", - " the company shared my information with another...\n", + " DEAR CFPB ; XXXX ; XXXX ; AND TRANSUNION ; SEE...\n", " \n", " \n", - " 4281\n", - " [-1.69487391e-02 -1.89835522e-02 -3.80971469e-...\n", - " {\"token_count\":130,\"truncated\":false}\n", + " 4087\n", + " [ 1.99207035e-03 -7.62321474e-03 7.92114343e-...\n", + " {\"token_count\":88,\"truncated\":false}\n", " \n", - " I tried to submit a teacher loan forgiveness a...\n", + " This debt was from my identity being stolen I ...\n", " \n", " \n", - " 4470\n", - " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", - " {\"token_count\":200,\"truncated\":false}\n", + " 4326\n", + " [ 3.44273262e-02 -3.36350128e-02 1.91939529e-...\n", + " {\"token_count\":52,\"truncated\":false}\n", " \n", - " in accordance with the Fair Credit Reporting a...\n", + " The items that are reflected on my credit repo...\n", " \n", " \n", - " 4915\n", - " [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-...\n", - " {\"token_count\":23,\"truncated\":false}\n", + " 4682\n", + " [ 2.47727744e-02 -1.77769139e-02 4.63737026e-...\n", + " {\"token_count\":284,\"truncated\":false}\n", " \n", - " XXXX XXXX did not give me a receipt or a copy ...\n", + " I filed for chapter XXXX bankruptcy on XXXX...\n", " \n", " \n", - " 4928\n", - " [-4.43694415e-03 -3.66776163e-04 -9.08496231e-...\n", - " {\"token_count\":83,\"truncated\":false}\n", + " 5005\n", + " [ 2.51834448e-02 -4.92606424e-02 -1.37688573e-...\n", + " {\"token_count\":17,\"truncated\":false}\n", " \n", - " This company has filed a civil suit during a g...\n", + " There are 2 Inquires on my credit report that ...\n", " \n", " \n", - " 5338\n", - " [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-...\n", - " {\"token_count\":1279,\"truncated\":false}\n", + " 5144\n", + " [ 3.26358266e-02 -3.67171178e-03 3.65621522e-...\n", + " {\"token_count\":105,\"truncated\":false}\n", " \n", - " My credit report contains errors that is keepi...\n", + " My mortgage was sold from XXXX XXXX to freed...\n", " \n", " \n", - " 5582\n", - " [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-...\n", - " {\"token_count\":396,\"truncated\":false}\n", + " 6090\n", + " [ 2.47520711e-02 1.09149124e-02 1.35175223e-...\n", + " {\"token_count\":545,\"truncated\":false}\n", " \n", - " Coast Professional, XXXX, LA contacted me by m...\n", + " On XX/XX/XXXX this company received certified...\n", " \n", " \n", - " 6386\n", - " [ 3.33276950e-02 1.53224478e-02 -1.89354066e-...\n", - " {\"token_count\":79,\"truncated\":false}\n", + " 6449\n", + " [ 1.86854266e-02 1.31238240e-03 -4.96791191e-...\n", + " {\"token_count\":104,\"truncated\":false}\n", " \n", - " Cares act refund requested in XXXX, called mul...\n", + " After hours on the phone with multiple agents,...\n", " \n", " \n", - " 6956\n", - " [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-...\n", - " {\"token_count\":194,\"truncated\":false}\n", + " 6486\n", + " [ 1.56347770e-02 2.23377198e-02 -1.32683543e-...\n", + " {\"token_count\":211,\"truncated\":false}\n", " \n", - " n accordance with the Fair Credit Reporting ac...\n", + " On XX/XX/2019 two charges one for XXXX and one...\n", " \n", " \n", "\n", @@ -710,86 +707,87 @@ "[10000 rows x 4 columns in total]" ], "text/plain": [ - " text_embedding \\\n", - "782 [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-... \n", - "795 [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-... \n", - "861 [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-... \n", - "1103 [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-... \n", - "1241 [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-... \n", - "1729 [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-... \n", - "2167 [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-... \n", - "2219 [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-... \n", - "2392 [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-... \n", - "2528 [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-... \n", - "2737 [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-... \n", - "2859 [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-... \n", - "3439 [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-... \n", - "3738 [ 0.01422119 -0.01114973 -0.04438976 -0.024421... \n", - "3805 [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-... \n", - "3915 [-7.23852217e-03 -4.69538383e-02 -5.60489520e-... \n", - "3917 [-8.92711710e-03 -4.49132621e-02 -4.29662578e-... \n", - "4281 [-1.69487391e-02 -1.89835522e-02 -3.80971469e-... \n", - "4470 [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-... \n", - "4915 [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-... \n", - "4928 [-4.43694415e-03 -3.66776163e-04 -9.08496231e-... \n", - "5338 [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-... \n", - "5582 [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-... \n", - "6386 [ 3.33276950e-02 1.53224478e-02 -1.89354066e-... \n", - "6956 [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-... \n", + " ml_generate_embedding_result \\\n", + "415 [ 2.56774724e-02 -1.06168222e-02 3.06945704e-... \n", + "596 [ 5.90653270e-02 -9.31344274e-03 -7.12460047e-... \n", + "706 [ 0.01298233 0.00130001 0.01800315 0.037078... \n", + "804 [-1.39777679e-02 1.68943349e-02 5.53999236e-... \n", + "861 [ 2.33309343e-02 -2.36528926e-03 3.37129943e-... \n", + "1030 [ 0.06060313 -0.06495965 -0.03605044 -0.028016... \n", + "1582 [ 0.01255985 -0.01652482 -0.02638046 0.036858... \n", + "1600 [ 5.13355099e-02 4.01246967e-03 5.72342947e-... \n", + "2060 [ 6.44792162e-04 4.95899878e-02 4.67925966e-... \n", + "2283 [ 4.71848622e-02 -8.68239347e-03 5.80501892e-... \n", + "2421 [-2.90394691e-03 -1.81679502e-02 -7.99657404e-... \n", + "2422 [-6.70500053e-03 1.51133696e-02 4.94448021e-... \n", + "2658 [ 6.70989677e-02 -3.53626162e-02 1.08648362e-... \n", + "2883 [-1.28255319e-02 -1.89735275e-02 5.68657108e-... \n", + "2951 [ 3.23301251e-03 -2.61142217e-02 1.31891826e-... \n", + "2992 [-2.22910382e-03 -1.07050659e-02 4.74211425e-... \n", + "3969 [ 1.58297736e-02 3.01055871e-02 5.60088176e-... \n", + "4087 [ 1.99207035e-03 -7.62321474e-03 7.92114343e-... \n", + "4326 [ 3.44273262e-02 -3.36350128e-02 1.91939529e-... \n", + "4682 [ 2.47727744e-02 -1.77769139e-02 4.63737026e-... \n", + "5005 [ 2.51834448e-02 -4.92606424e-02 -1.37688573e-... \n", + "5144 [ 3.26358266e-02 -3.67171178e-03 3.65621522e-... \n", + "6090 [ 2.47520711e-02 1.09149124e-02 1.35175223e-... \n", + "6449 [ 1.86854266e-02 1.31238240e-03 -4.96791191e-... \n", + "6486 [ 1.56347770e-02 2.23377198e-02 -1.32683543e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "782 {\"token_count\":121,\"truncated\":false} \n", - "795 {\"token_count\":141,\"truncated\":false} \n", - "861 {\"token_count\":160,\"truncated\":false} \n", - "1103 {\"token_count\":31,\"truncated\":false} \n", - "1241 {\"token_count\":23,\"truncated\":false} \n", - "1729 {\"token_count\":382,\"truncated\":false} \n", - "2167 {\"token_count\":556,\"truncated\":false} \n", - "2219 {\"token_count\":196,\"truncated\":false} \n", - "2392 {\"token_count\":641,\"truncated\":false} \n", - "2528 {\"token_count\":176,\"truncated\":false} \n", - "2737 {\"token_count\":230,\"truncated\":false} \n", - "2859 {\"token_count\":238,\"truncated\":false} \n", - "3439 {\"token_count\":197,\"truncated\":false} \n", - "3738 {\"token_count\":160,\"truncated\":false} \n", - "3805 {\"token_count\":477,\"truncated\":false} \n", - "3915 {\"token_count\":116,\"truncated\":false} \n", - "3917 {\"token_count\":71,\"truncated\":false} \n", - "4281 {\"token_count\":130,\"truncated\":false} \n", - "4470 {\"token_count\":200,\"truncated\":false} \n", - "4915 {\"token_count\":23,\"truncated\":false} \n", - "4928 {\"token_count\":83,\"truncated\":false} \n", - "5338 {\"token_count\":1279,\"truncated\":false} \n", - "5582 {\"token_count\":396,\"truncated\":false} \n", - "6386 {\"token_count\":79,\"truncated\":false} \n", - "6956 {\"token_count\":194,\"truncated\":false} \n", + " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", + "415 {\"token_count\":171,\"truncated\":false} \n", + "596 {\"token_count\":668,\"truncated\":false} \n", + "706 {\"token_count\":252,\"truncated\":false} \n", + "804 {\"token_count\":412,\"truncated\":false} \n", + "861 {\"token_count\":160,\"truncated\":false} \n", + "1030 {\"token_count\":298,\"truncated\":false} \n", + "1582 {\"token_count\":814,\"truncated\":false} \n", + "1600 {\"token_count\":653,\"truncated\":false} \n", + "2060 {\"token_count\":136,\"truncated\":false} \n", + "2283 {\"token_count\":478,\"truncated\":false} \n", + "2421 {\"token_count\":389,\"truncated\":false} \n", + "2422 {\"token_count\":124,\"truncated\":false} \n", + "2658 {\"token_count\":762,\"truncated\":false} \n", + "2883 {\"token_count\":71,\"truncated\":false} \n", + "2951 {\"token_count\":95,\"truncated\":false} \n", + "2992 {\"token_count\":407,\"truncated\":false} \n", + "3969 {\"token_count\":287,\"truncated\":false} \n", + "4087 {\"token_count\":88,\"truncated\":false} \n", + "4326 {\"token_count\":52,\"truncated\":false} \n", + "4682 {\"token_count\":284,\"truncated\":false} \n", + "5005 {\"token_count\":17,\"truncated\":false} \n", + "5144 {\"token_count\":105,\"truncated\":false} \n", + "6090 {\"token_count\":545,\"truncated\":false} \n", + "6449 {\"token_count\":104,\"truncated\":false} \n", + "6486 {\"token_count\":211,\"truncated\":false} \n", "\n", " content \n", - "782 I 've sent multiple letters to this agency abo... \n", - "795 I receive social security XXXX funds in my XXX... \n", + "415 DEPT OF EDUCATION/XXXX is stating I was late ... \n", + "596 I alerted my credit card company XX/XX/2017 th... \n", + "706 Sallie mae is corrupt. \n", + "I have tried to talk t... \n", + "804 In accordance with the Fair Credit Reporting a... \n", "861 Hello, My name is XXXX XXXX XXXX. I have a pro... \n", - "1103 The debt occurred more than 7 years in the pas... \n", - "1241 UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA... \n", - "1729 XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort... \n", - "2167 This is the third such complaint I have submit... \n", - "2219 Found and add online for a Prepaid Credit card... \n", - "2392 I am furnishing this complaint against Fed Loa... \n", - "2528 Despite multiple written requests, the unverif... \n", - "2737 After unsatisfying communication in the messag... \n", - "2859 Good Morning. My name is XXXX XXXX. My account... \n", - "3439 I have ongoing disputes that are preventing me... \n", - "3738 I had a loan with national Collegiate Trust. i... \n", - "3805 Hi I am submitting this XXXX XXXX this isn't a... \n", - "3915 portfolio is showin on my credit report with a... \n", - "3917 the company shared my information with another... \n", - "4281 I tried to submit a teacher loan forgiveness a... \n", - "4470 in accordance with the Fair Credit Reporting a... \n", - "4915 XXXX XXXX did not give me a receipt or a copy ... \n", - "4928 This company has filed a civil suit during a g... \n", - "5338 My credit report contains errors that is keepi... \n", - "5582 Coast Professional, XXXX, LA contacted me by m... \n", - "6386 Cares act refund requested in XXXX, called mul... \n", - "6956 n accordance with the Fair Credit Reporting ac... \n", + "1030 Hello, I would like to complain about PayPal H... \n", + "1582 Transunion is listing personal information ( n... \n", + "1600 On XX/XX/XXXX, I called Citizen Bank at XXXX t... \n", + "2060 Theses names are the known liars that I have s... \n", + "2283 My house was hit by a tree XX/XX/2018. My insu... \n", + "2421 I became aware of a credit inquiry on my XXXX... \n", + "2422 I have sent numerous letters, police reports a... \n", + "2658 This letter concerns two disputes ( chargeback... \n", + "2883 It is very frustrating that this has been goin... \n", + "2951 I, the consumer, in fact, have a right to priv... \n", + "2992 XXXX XXXX XXXX should not be reporting to Expe... \n", + "3969 DEAR CFPB ; XXXX ; XXXX ; AND TRANSUNION ; SEE... \n", + "4087 This debt was from my identity being stolen I ... \n", + "4326 The items that are reflected on my credit repo... \n", + "4682 I filed for chapter XXXX bankruptcy on XXXX... \n", + "5005 There are 2 Inquires on my credit report that ... \n", + "5144 My mortgage was sold from XXXX XXXX to freed... \n", + "6090 On XX/XX/XXXX this company received certified... \n", + "6449 After hours on the phone with multiple agents,... \n", + "6486 On XX/XX/2019 two charges one for XXXX and one... \n", "...\n", "\n", "[10000 rows x 4 columns]" @@ -816,13 +814,13 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 2c99b34a-1956-4de7-8330-898f1f25560b is DONE. 71.9 MB processed. Open Job" + "Query job 16915c47-ab13-4d06-94aa-9ebdb65d91fe is DONE. 72.0 MB processed. Open Job" ], "text/plain": [ "" @@ -834,7 +832,7 @@ { "data": { "text/html": [ - "Query job 3ffed5f8-935a-4a3f-a560-6416445e4868 is DONE. 0 Bytes processed. Open Job" + "Query job 4ab4fbf0-6fd3-4936-9915-cfd7ccd106d1 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -846,7 +844,7 @@ { "data": { "text/html": [ - "Query job 7b55783a-6d8f-41b9-b404-73253140029a is DONE. 72.3 MB processed. Open Job" + "Query job b11d3794-6bb8-4c47-a91b-dcc472cf4d69 is DONE. 72.4 MB processed. Open Job" ], "text/plain": [ "" @@ -876,187 +874,188 @@ " \n", " \n", " \n", - " text_embedding\n", - " statistics\n", - " ml_embed_text_status\n", + " ml_generate_embedding_result\n", + " ml_generate_embedding_statistics\n", + " ml_generate_embedding_status\n", " content\n", " \n", " \n", " \n", " \n", - " 782\n", - " [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-...\n", - " {\"token_count\":121,\"truncated\":false}\n", + " 415\n", + " [ 2.56774724e-02 -1.06168222e-02 3.06945704e-...\n", + " {\"token_count\":171,\"truncated\":false}\n", " \n", - " I 've sent multiple letters to this agency abo...\n", + " DEPT OF EDUCATION/XXXX is stating I was late ...\n", " \n", " \n", - " 795\n", - " [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-...\n", - " {\"token_count\":141,\"truncated\":false}\n", + " 596\n", + " [ 5.90653270e-02 -9.31344274e-03 -7.12460047e-...\n", + " {\"token_count\":668,\"truncated\":false}\n", " \n", - " I receive social security XXXX funds in my XXX...\n", + " I alerted my credit card company XX/XX/2017 th...\n", " \n", " \n", - " 861\n", - " [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-...\n", - " {\"token_count\":160,\"truncated\":false}\n", + " 706\n", + " [ 0.01298233 0.00130001 0.01800315 0.037078...\n", + " {\"token_count\":252,\"truncated\":false}\n", " \n", - " Hello, My name is XXXX XXXX XXXX. I have a pro...\n", + " Sallie mae is corrupt. \n", + "I have tried to talk t...\n", " \n", " \n", - " 1103\n", - " [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-...\n", - " {\"token_count\":31,\"truncated\":false}\n", + " 804\n", + " [-1.39777679e-02 1.68943349e-02 5.53999236e-...\n", + " {\"token_count\":412,\"truncated\":false}\n", " \n", - " The debt occurred more than 7 years in the pas...\n", + " In accordance with the Fair Credit Reporting a...\n", " \n", " \n", - " 1241\n", - " [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-...\n", - " {\"token_count\":23,\"truncated\":false}\n", + " 861\n", + " [ 2.33309343e-02 -2.36528926e-03 3.37129943e-...\n", + " {\"token_count\":160,\"truncated\":false}\n", " \n", - " UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA...\n", + " Hello, My name is XXXX XXXX XXXX. I have a pro...\n", " \n", " \n", - " 1729\n", - " [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-...\n", - " {\"token_count\":382,\"truncated\":false}\n", + " 1030\n", + " [ 0.06060313 -0.06495965 -0.03605044 -0.028016...\n", + " {\"token_count\":298,\"truncated\":false}\n", " \n", - " XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort...\n", + " Hello, I would like to complain about PayPal H...\n", " \n", " \n", - " 2167\n", - " [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-...\n", - " {\"token_count\":556,\"truncated\":false}\n", + " 1582\n", + " [ 0.01255985 -0.01652482 -0.02638046 0.036858...\n", + " {\"token_count\":814,\"truncated\":false}\n", " \n", - " This is the third such complaint I have submit...\n", + " Transunion is listing personal information ( n...\n", " \n", " \n", - " 2219\n", - " [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-...\n", - " {\"token_count\":196,\"truncated\":false}\n", + " 1600\n", + " [ 5.13355099e-02 4.01246967e-03 5.72342947e-...\n", + " {\"token_count\":653,\"truncated\":false}\n", " \n", - " Found and add online for a Prepaid Credit card...\n", + " On XX/XX/XXXX, I called Citizen Bank at XXXX t...\n", " \n", " \n", - " 2392\n", - " [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-...\n", - " {\"token_count\":641,\"truncated\":false}\n", + " 2060\n", + " [ 6.44792162e-04 4.95899878e-02 4.67925966e-...\n", + " {\"token_count\":136,\"truncated\":false}\n", " \n", - " I am furnishing this complaint against Fed Loa...\n", + " Theses names are the known liars that I have s...\n", " \n", " \n", - " 2528\n", - " [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-...\n", - " {\"token_count\":176,\"truncated\":false}\n", + " 2283\n", + " [ 4.71848622e-02 -8.68239347e-03 5.80501892e-...\n", + " {\"token_count\":478,\"truncated\":false}\n", " \n", - " Despite multiple written requests, the unverif...\n", + " My house was hit by a tree XX/XX/2018. My insu...\n", " \n", " \n", - " 2737\n", - " [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-...\n", - " {\"token_count\":230,\"truncated\":false}\n", + " 2421\n", + " [-2.90394691e-03 -1.81679502e-02 -7.99657404e-...\n", + " {\"token_count\":389,\"truncated\":false}\n", " \n", - " After unsatisfying communication in the messag...\n", + " I became aware of a credit inquiry on my XXXX...\n", " \n", " \n", - " 2859\n", - " [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-...\n", - " {\"token_count\":238,\"truncated\":false}\n", + " 2422\n", + " [-6.70500053e-03 1.51133696e-02 4.94448021e-...\n", + " {\"token_count\":124,\"truncated\":false}\n", " \n", - " Good Morning. My name is XXXX XXXX. My account...\n", + " I have sent numerous letters, police reports a...\n", " \n", " \n", - " 3439\n", - " [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-...\n", - " {\"token_count\":197,\"truncated\":false}\n", + " 2658\n", + " [ 6.70989677e-02 -3.53626162e-02 1.08648362e-...\n", + " {\"token_count\":762,\"truncated\":false}\n", " \n", - " I have ongoing disputes that are preventing me...\n", + " This letter concerns two disputes ( chargeback...\n", " \n", " \n", - " 3738\n", - " [ 0.01422119 -0.01114973 -0.04438976 -0.024421...\n", - " {\"token_count\":160,\"truncated\":false}\n", + " 2883\n", + " [-1.28255319e-02 -1.89735275e-02 5.68657108e-...\n", + " {\"token_count\":71,\"truncated\":false}\n", " \n", - " I had a loan with national Collegiate Trust. i...\n", + " It is very frustrating that this has been goin...\n", " \n", " \n", - " 3805\n", - " [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-...\n", - " {\"token_count\":477,\"truncated\":false}\n", + " 2951\n", + " [ 3.23301251e-03 -2.61142217e-02 1.31891826e-...\n", + " {\"token_count\":95,\"truncated\":false}\n", " \n", - " Hi I am submitting this XXXX XXXX this isn't a...\n", + " I, the consumer, in fact, have a right to priv...\n", " \n", " \n", - " 3915\n", - " [-7.23852217e-03 -4.69538383e-02 -5.60489520e-...\n", - " {\"token_count\":116,\"truncated\":false}\n", + " 2992\n", + " [-2.22910382e-03 -1.07050659e-02 4.74211425e-...\n", + " {\"token_count\":407,\"truncated\":false}\n", " \n", - " portfolio is showin on my credit report with a...\n", + " XXXX XXXX XXXX should not be reporting to Expe...\n", " \n", " \n", - " 3917\n", - " [-8.92711710e-03 -4.49132621e-02 -4.29662578e-...\n", - " {\"token_count\":71,\"truncated\":false}\n", + " 3969\n", + " [ 1.58297736e-02 3.01055871e-02 5.60088176e-...\n", + " {\"token_count\":287,\"truncated\":false}\n", " \n", - " the company shared my information with another...\n", + " DEAR CFPB ; XXXX ; XXXX ; AND TRANSUNION ; SEE...\n", " \n", " \n", - " 4281\n", - " [-1.69487391e-02 -1.89835522e-02 -3.80971469e-...\n", - " {\"token_count\":130,\"truncated\":false}\n", + " 4087\n", + " [ 1.99207035e-03 -7.62321474e-03 7.92114343e-...\n", + " {\"token_count\":88,\"truncated\":false}\n", " \n", - " I tried to submit a teacher loan forgiveness a...\n", + " This debt was from my identity being stolen I ...\n", " \n", " \n", - " 4470\n", - " [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-...\n", - " {\"token_count\":200,\"truncated\":false}\n", + " 4326\n", + " [ 3.44273262e-02 -3.36350128e-02 1.91939529e-...\n", + " {\"token_count\":52,\"truncated\":false}\n", " \n", - " in accordance with the Fair Credit Reporting a...\n", + " The items that are reflected on my credit repo...\n", " \n", " \n", - " 4915\n", - " [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-...\n", - " {\"token_count\":23,\"truncated\":false}\n", + " 4682\n", + " [ 2.47727744e-02 -1.77769139e-02 4.63737026e-...\n", + " {\"token_count\":284,\"truncated\":false}\n", " \n", - " XXXX XXXX did not give me a receipt or a copy ...\n", + " I filed for chapter XXXX bankruptcy on XXXX...\n", " \n", " \n", - " 4928\n", - " [-4.43694415e-03 -3.66776163e-04 -9.08496231e-...\n", - " {\"token_count\":83,\"truncated\":false}\n", + " 5005\n", + " [ 2.51834448e-02 -4.92606424e-02 -1.37688573e-...\n", + " {\"token_count\":17,\"truncated\":false}\n", " \n", - " This company has filed a civil suit during a g...\n", + " There are 2 Inquires on my credit report that ...\n", " \n", " \n", - " 5338\n", - " [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-...\n", - " {\"token_count\":1279,\"truncated\":false}\n", + " 5144\n", + " [ 3.26358266e-02 -3.67171178e-03 3.65621522e-...\n", + " {\"token_count\":105,\"truncated\":false}\n", " \n", - " My credit report contains errors that is keepi...\n", + " My mortgage was sold from XXXX XXXX to freed...\n", " \n", " \n", - " 5582\n", - " [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-...\n", - " {\"token_count\":396,\"truncated\":false}\n", + " 6090\n", + " [ 2.47520711e-02 1.09149124e-02 1.35175223e-...\n", + " {\"token_count\":545,\"truncated\":false}\n", " \n", - " Coast Professional, XXXX, LA contacted me by m...\n", + " On XX/XX/XXXX this company received certified...\n", " \n", " \n", - " 6386\n", - " [ 3.33276950e-02 1.53224478e-02 -1.89354066e-...\n", - " {\"token_count\":79,\"truncated\":false}\n", + " 6449\n", + " [ 1.86854266e-02 1.31238240e-03 -4.96791191e-...\n", + " {\"token_count\":104,\"truncated\":false}\n", " \n", - " Cares act refund requested in XXXX, called mul...\n", + " After hours on the phone with multiple agents,...\n", " \n", " \n", - " 6956\n", - " [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-...\n", - " {\"token_count\":194,\"truncated\":false}\n", + " 6486\n", + " [ 1.56347770e-02 2.23377198e-02 -1.32683543e-...\n", + " {\"token_count\":211,\"truncated\":false}\n", " \n", - " n accordance with the Fair Credit Reporting ac...\n", + " On XX/XX/2019 two charges one for XXXX and one...\n", " \n", " \n", "\n", @@ -1064,102 +1063,103 @@ "[10000 rows x 4 columns in total]" ], "text/plain": [ - " text_embedding \\\n", - "782 [ 2.78223325e-02 -1.71949025e-02 -5.01214415e-... \n", - "795 [ 1.39164589e-02 -5.08313216e-02 -4.53360938e-... \n", - "861 [ 7.80681521e-03 -3.23560014e-02 -6.76454604e-... \n", - "1103 [ 4.18044440e-02 -4.28444222e-02 -6.26875088e-... \n", - "1241 [ 7.74183637e-03 -6.50701150e-02 -2.13856809e-... \n", - "1729 [ 3.15782428e-02 -1.23979878e-02 -3.70296165e-... \n", - "2167 [ 9.87241510e-03 -1.81103535e-02 -4.17162031e-... \n", - "2219 [ 2.56749280e-02 -4.92163002e-02 -5.67202382e-... \n", - "2392 [ 2.34611966e-02 -4.74611111e-02 -3.59710641e-... \n", - "2528 [ 1.90760177e-02 -4.90266569e-02 -5.60806654e-... \n", - "2737 [ 1.81887485e-02 -8.74284655e-03 -2.73009986e-... \n", - "2859 [ 3.52482982e-02 -3.30757573e-02 -4.48422395e-... \n", - "3439 [ 3.40348878e-03 -2.72301212e-02 -2.03482248e-... \n", - "3738 [ 0.01422119 -0.01114973 -0.04438976 -0.024421... \n", - "3805 [ 1.08179580e-02 -3.44337188e-02 -5.08812033e-... \n", - "3915 [-7.23852217e-03 -4.69538383e-02 -5.60489520e-... \n", - "3917 [-8.92711710e-03 -4.49132621e-02 -4.29662578e-... \n", - "4281 [-1.69487391e-02 -1.89835522e-02 -3.80971469e-... \n", - "4470 [ 1.28689921e-02 -3.25881056e-02 -6.53645024e-... \n", - "4915 [ 5.19403480e-02 -7.32436478e-02 -4.60561663e-... \n", - "4928 [-4.43694415e-03 -3.66776163e-04 -9.08496231e-... \n", - "5338 [ 2.19908613e-03 -3.93951498e-02 -6.52823672e-... \n", - "5582 [ 2.86326781e-02 -4.89189997e-02 -8.68150592e-... \n", - "6386 [ 3.33276950e-02 1.53224478e-02 -1.89354066e-... \n", - "6956 [ 1.47060463e-02 -3.36431377e-02 -6.56675845e-... \n", + " ml_generate_embedding_result \\\n", + "415 [ 2.56774724e-02 -1.06168222e-02 3.06945704e-... \n", + "596 [ 5.90653270e-02 -9.31344274e-03 -7.12460047e-... \n", + "706 [ 0.01298233 0.00130001 0.01800315 0.037078... \n", + "804 [-1.39777679e-02 1.68943349e-02 5.53999236e-... \n", + "861 [ 2.33309343e-02 -2.36528926e-03 3.37129943e-... \n", + "1030 [ 0.06060313 -0.06495965 -0.03605044 -0.028016... \n", + "1582 [ 0.01255985 -0.01652482 -0.02638046 0.036858... \n", + "1600 [ 5.13355099e-02 4.01246967e-03 5.72342947e-... \n", + "2060 [ 6.44792162e-04 4.95899878e-02 4.67925966e-... \n", + "2283 [ 4.71848622e-02 -8.68239347e-03 5.80501892e-... \n", + "2421 [-2.90394691e-03 -1.81679502e-02 -7.99657404e-... \n", + "2422 [-6.70500053e-03 1.51133696e-02 4.94448021e-... \n", + "2658 [ 6.70989677e-02 -3.53626162e-02 1.08648362e-... \n", + "2883 [-1.28255319e-02 -1.89735275e-02 5.68657108e-... \n", + "2951 [ 3.23301251e-03 -2.61142217e-02 1.31891826e-... \n", + "2992 [-2.22910382e-03 -1.07050659e-02 4.74211425e-... \n", + "3969 [ 1.58297736e-02 3.01055871e-02 5.60088176e-... \n", + "4087 [ 1.99207035e-03 -7.62321474e-03 7.92114343e-... \n", + "4326 [ 3.44273262e-02 -3.36350128e-02 1.91939529e-... \n", + "4682 [ 2.47727744e-02 -1.77769139e-02 4.63737026e-... \n", + "5005 [ 2.51834448e-02 -4.92606424e-02 -1.37688573e-... \n", + "5144 [ 3.26358266e-02 -3.67171178e-03 3.65621522e-... \n", + "6090 [ 2.47520711e-02 1.09149124e-02 1.35175223e-... \n", + "6449 [ 1.86854266e-02 1.31238240e-03 -4.96791191e-... \n", + "6486 [ 1.56347770e-02 2.23377198e-02 -1.32683543e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "782 {\"token_count\":121,\"truncated\":false} \n", - "795 {\"token_count\":141,\"truncated\":false} \n", - "861 {\"token_count\":160,\"truncated\":false} \n", - "1103 {\"token_count\":31,\"truncated\":false} \n", - "1241 {\"token_count\":23,\"truncated\":false} \n", - "1729 {\"token_count\":382,\"truncated\":false} \n", - "2167 {\"token_count\":556,\"truncated\":false} \n", - "2219 {\"token_count\":196,\"truncated\":false} \n", - "2392 {\"token_count\":641,\"truncated\":false} \n", - "2528 {\"token_count\":176,\"truncated\":false} \n", - "2737 {\"token_count\":230,\"truncated\":false} \n", - "2859 {\"token_count\":238,\"truncated\":false} \n", - "3439 {\"token_count\":197,\"truncated\":false} \n", - "3738 {\"token_count\":160,\"truncated\":false} \n", - "3805 {\"token_count\":477,\"truncated\":false} \n", - "3915 {\"token_count\":116,\"truncated\":false} \n", - "3917 {\"token_count\":71,\"truncated\":false} \n", - "4281 {\"token_count\":130,\"truncated\":false} \n", - "4470 {\"token_count\":200,\"truncated\":false} \n", - "4915 {\"token_count\":23,\"truncated\":false} \n", - "4928 {\"token_count\":83,\"truncated\":false} \n", - "5338 {\"token_count\":1279,\"truncated\":false} \n", - "5582 {\"token_count\":396,\"truncated\":false} \n", - "6386 {\"token_count\":79,\"truncated\":false} \n", - "6956 {\"token_count\":194,\"truncated\":false} \n", + " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", + "415 {\"token_count\":171,\"truncated\":false} \n", + "596 {\"token_count\":668,\"truncated\":false} \n", + "706 {\"token_count\":252,\"truncated\":false} \n", + "804 {\"token_count\":412,\"truncated\":false} \n", + "861 {\"token_count\":160,\"truncated\":false} \n", + "1030 {\"token_count\":298,\"truncated\":false} \n", + "1582 {\"token_count\":814,\"truncated\":false} \n", + "1600 {\"token_count\":653,\"truncated\":false} \n", + "2060 {\"token_count\":136,\"truncated\":false} \n", + "2283 {\"token_count\":478,\"truncated\":false} \n", + "2421 {\"token_count\":389,\"truncated\":false} \n", + "2422 {\"token_count\":124,\"truncated\":false} \n", + "2658 {\"token_count\":762,\"truncated\":false} \n", + "2883 {\"token_count\":71,\"truncated\":false} \n", + "2951 {\"token_count\":95,\"truncated\":false} \n", + "2992 {\"token_count\":407,\"truncated\":false} \n", + "3969 {\"token_count\":287,\"truncated\":false} \n", + "4087 {\"token_count\":88,\"truncated\":false} \n", + "4326 {\"token_count\":52,\"truncated\":false} \n", + "4682 {\"token_count\":284,\"truncated\":false} \n", + "5005 {\"token_count\":17,\"truncated\":false} \n", + "5144 {\"token_count\":105,\"truncated\":false} \n", + "6090 {\"token_count\":545,\"truncated\":false} \n", + "6449 {\"token_count\":104,\"truncated\":false} \n", + "6486 {\"token_count\":211,\"truncated\":false} \n", "\n", " content \n", - "782 I 've sent multiple letters to this agency abo... \n", - "795 I receive social security XXXX funds in my XXX... \n", + "415 DEPT OF EDUCATION/XXXX is stating I was late ... \n", + "596 I alerted my credit card company XX/XX/2017 th... \n", + "706 Sallie mae is corrupt. \n", + "I have tried to talk t... \n", + "804 In accordance with the Fair Credit Reporting a... \n", "861 Hello, My name is XXXX XXXX XXXX. I have a pro... \n", - "1103 The debt occurred more than 7 years in the pas... \n", - "1241 UNAUTHORIZED CREDIT REPORTING NO ACCOUNT TO VA... \n", - "1729 XXXX on XXXX XX/XX/2021 I have Mr. Cooper mort... \n", - "2167 This is the third such complaint I have submit... \n", - "2219 Found and add online for a Prepaid Credit card... \n", - "2392 I am furnishing this complaint against Fed Loa... \n", - "2528 Despite multiple written requests, the unverif... \n", - "2737 After unsatisfying communication in the messag... \n", - "2859 Good Morning. My name is XXXX XXXX. My account... \n", - "3439 I have ongoing disputes that are preventing me... \n", - "3738 I had a loan with national Collegiate Trust. i... \n", - "3805 Hi I am submitting this XXXX XXXX this isn't a... \n", - "3915 portfolio is showin on my credit report with a... \n", - "3917 the company shared my information with another... \n", - "4281 I tried to submit a teacher loan forgiveness a... \n", - "4470 in accordance with the Fair Credit Reporting a... \n", - "4915 XXXX XXXX did not give me a receipt or a copy ... \n", - "4928 This company has filed a civil suit during a g... \n", - "5338 My credit report contains errors that is keepi... \n", - "5582 Coast Professional, XXXX, LA contacted me by m... \n", - "6386 Cares act refund requested in XXXX, called mul... \n", - "6956 n accordance with the Fair Credit Reporting ac... \n", + "1030 Hello, I would like to complain about PayPal H... \n", + "1582 Transunion is listing personal information ( n... \n", + "1600 On XX/XX/XXXX, I called Citizen Bank at XXXX t... \n", + "2060 Theses names are the known liars that I have s... \n", + "2283 My house was hit by a tree XX/XX/2018. My insu... \n", + "2421 I became aware of a credit inquiry on my XXXX... \n", + "2422 I have sent numerous letters, police reports a... \n", + "2658 This letter concerns two disputes ( chargeback... \n", + "2883 It is very frustrating that this has been goin... \n", + "2951 I, the consumer, in fact, have a right to priv... \n", + "2992 XXXX XXXX XXXX should not be reporting to Expe... \n", + "3969 DEAR CFPB ; XXXX ; XXXX ; AND TRANSUNION ; SEE... \n", + "4087 This debt was from my identity being stolen I ... \n", + "4326 The items that are reflected on my credit repo... \n", + "4682 I filed for chapter XXXX bankruptcy on XXXX... \n", + "5005 There are 2 Inquires on my credit report that ... \n", + "5144 My mortgage was sold from XXXX XXXX to freed... \n", + "6090 On XX/XX/XXXX this company received certified... \n", + "6449 After hours on the phone with multiple agents,... \n", + "6486 On XX/XX/2019 two charges one for XXXX and one... \n", "...\n", "\n", "[10000 rows x 4 columns]" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "successful_rows = (\n", - " (predicted_embeddings[\"ml_embed_text_status\"] == \"\")\n", + " (predicted_embeddings[\"ml_generate_embedding_status\"] == \"\")\n", " # Series.str.len() gives the length of an array.\n", " # See: https://stackoverflow.com/a/41340543/101923\n", - " & (predicted_embeddings[\"text_embedding\"].str.len() != 0)\n", + " & (predicted_embeddings[\"ml_generate_embedding_result\"].str.len() != 0)\n", ")\n", "predicted_embeddings = predicted_embeddings[successful_rows]\n", "predicted_embeddings\n" @@ -1185,7 +1185,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": { "id": "AhNTnEC5FRz2" }, @@ -1206,7 +1206,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": { "id": "6poSxh-fGJF7" }, @@ -1214,19 +1214,7 @@ { "data": { "text/html": [ - "Query job 46da96c8-c454-44d3-8b98-0e1bfeca69dd is DONE. 61.7 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job dc6fe7cf-329d-4274-aff9-0b8dc2e56230 is DONE. 0 Bytes processed. Open Job" + "Query job 3e01544b-9bc2-4298-8f7d-1e9f186ac72f is DONE. 61.6 MB processed. Open Job" ], "text/plain": [ "" @@ -1238,7 +1226,7 @@ { "data": { "text/html": [ - "Query job 8c25a14a-af39-40a9-add5-de0f14bce9ce is DONE. 72.4 MB processed. Open Job" + "Query job 8aca135c-65c3-4804-9c25-0d47fad0beb5 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1250,7 +1238,7 @@ { "data": { "text/html": [ - "Query job 0a6a45b2-7c35-4be8-91a3-391a5381553e is DONE. 80.0 kB processed. Open Job" + "Query job 0b15374d-d34b-4f2e-8a48-b77d7e7757ab is DONE. 72.7 MB processed. Open Job" ], "text/plain": [ "" @@ -1262,7 +1250,7 @@ { "data": { "text/html": [ - "Query job b5e00edd-de21-40c1-bf61-9f1affdea318 is DONE. 73.1 MB processed. Open Job" + "Query job fed90511-76f8-4aec-a988-e1a4dab711b0 is DONE. 73.2 MB processed. Open Job" ], "text/plain": [ "" @@ -1294,57 +1282,57 @@ " \n", " CENTROID_ID\n", " NEAREST_CENTROIDS_DISTANCE\n", - " text_embedding\n", - " statistics\n", - " ml_embed_text_status\n", + " ml_generate_embedding_result\n", + " ml_generate_embedding_statistics\n", + " ml_generate_embedding_status\n", " content\n", " \n", " \n", " \n", " \n", - " 1094645\n", + " 3172121\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.530282685572...\n", - " [ 7.32792774e-03 -7.59598315e-02 -4.49591577e-...\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.756634267893...\n", + " [ 3.18095312e-02 -3.54472063e-02 -7.13569671e-...\n", " {\"token_count\":10,\"truncated\":false}\n", " \n", - " I do not have an account with this creditor\n", + " Company did not provide verification and detai...\n", " \n", " \n", - " 3372485\n", + " 2137420\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.643931578310...\n", - " [-0.00161087 -0.04956109 -0.07371692 -0.057822...\n", - " {\"token_count\":10,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.606628249825...\n", + " [ 1.91578846e-02 5.55988774e-02 8.88887007e-...\n", + " {\"token_count\":100,\"truncated\":false}\n", " \n", - " Hard inquiries in my report that I do not reco...\n", + " I have already filed a dispute with Consumer A...\n", " \n", " \n", - " 2669308\n", + " 2350775\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.599709344244...\n", - " [ 5.50241247e-02 -1.50039541e-02 -2.08624080e-...\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.606676295233...\n", + " [ 2.25369893e-02 2.29400061e-02 -6.42273854e-...\n", " {\"token_count\":100,\"truncated\":false}\n", " \n", - " I purchase {$25.00} for stock on the cash app ...\n", + " I informed Central Financial Control & provide...\n", " \n", " \n", - " 133816\n", + " 2904146\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.618444281124...\n", - " [ 1.77251529e-02 -3.89547497e-02 -3.82236368e-...\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.596729348974...\n", + " [ 9.35115516e-02 4.27814946e-03 4.62085977e-...\n", " {\"token_count\":100,\"truncated\":false}\n", " \n", - " BBVA fees I am in The Texas snow storm where I...\n", + " I received a letter from a collections agency ...\n", " \n", " \n", - " 2697156\n", + " 1075571\n", " 1\n", - " [{'CENTROID_ID': 1, 'DISTANCE': 0.500398902102...\n", - " [-1.28429877e-02 -1.85956229e-02 -3.93197313e-...\n", - " {\"token_count\":1011,\"truncated\":false}\n", + " [{'CENTROID_ID': 1, 'DISTANCE': 0.453806107968...\n", + " [-1.93953840e-03 -5.80236455e-03 8.49655271e-...\n", + " {\"token_count\":100,\"truncated\":false}\n", " \n", - " After paying on my student loan for years, I o...\n", + " I have not done business with this company, i ...\n", " \n", " \n", "\n", @@ -1352,42 +1340,42 @@ ], "text/plain": [ " CENTROID_ID NEAREST_CENTROIDS_DISTANCE \\\n", - "1094645 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.530282685572... \n", - "3372485 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.643931578310... \n", - "2669308 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.599709344244... \n", - "133816 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.618444281124... \n", - "2697156 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.500398902102... \n", + "3172121 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.756634267893... \n", + "2137420 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.606628249825... \n", + "2350775 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.606676295233... \n", + "2904146 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.596729348974... \n", + "1075571 1 [{'CENTROID_ID': 1, 'DISTANCE': 0.453806107968... \n", "\n", - " text_embedding \\\n", - "1094645 [ 7.32792774e-03 -7.59598315e-02 -4.49591577e-... \n", - "3372485 [-0.00161087 -0.04956109 -0.07371692 -0.057822... \n", - "2669308 [ 5.50241247e-02 -1.50039541e-02 -2.08624080e-... \n", - "133816 [ 1.77251529e-02 -3.89547497e-02 -3.82236368e-... \n", - "2697156 [-1.28429877e-02 -1.85956229e-02 -3.93197313e-... \n", + " ml_generate_embedding_result \\\n", + "3172121 [ 3.18095312e-02 -3.54472063e-02 -7.13569671e-... \n", + "2137420 [ 1.91578846e-02 5.55988774e-02 8.88887007e-... \n", + "2350775 [ 2.25369893e-02 2.29400061e-02 -6.42273854e-... \n", + "2904146 [ 9.35115516e-02 4.27814946e-03 4.62085977e-... \n", + "1075571 [-1.93953840e-03 -5.80236455e-03 8.49655271e-... \n", "\n", - " statistics ml_embed_text_status \\\n", - "1094645 {\"token_count\":10,\"truncated\":false} \n", - "3372485 {\"token_count\":10,\"truncated\":false} \n", - "2669308 {\"token_count\":100,\"truncated\":false} \n", - "133816 {\"token_count\":100,\"truncated\":false} \n", - "2697156 {\"token_count\":1011,\"truncated\":false} \n", + " ml_generate_embedding_statistics ml_generate_embedding_status \\\n", + "3172121 {\"token_count\":10,\"truncated\":false} \n", + "2137420 {\"token_count\":100,\"truncated\":false} \n", + "2350775 {\"token_count\":100,\"truncated\":false} \n", + "2904146 {\"token_count\":100,\"truncated\":false} \n", + "1075571 {\"token_count\":100,\"truncated\":false} \n", "\n", " content \n", - "1094645 I do not have an account with this creditor \n", - "3372485 Hard inquiries in my report that I do not reco... \n", - "2669308 I purchase {$25.00} for stock on the cash app ... \n", - "133816 BBVA fees I am in The Texas snow storm where I... \n", - "2697156 After paying on my student loan for years, I o... " + "3172121 Company did not provide verification and detai... \n", + "2137420 I have already filed a dispute with Consumer A... \n", + "2350775 I informed Central Financial Control & provide... \n", + "2904146 I received a letter from a collections agency ... \n", + "1075571 I have not done business with this company, i ... " ] }, - "execution_count": 13, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Use KMeans clustering to calculate our groups. Will take ~3 minutes.\n", - "cluster_model.fit(predicted_embeddings[[\"text_embedding\"]])\n", + "cluster_model.fit(predicted_embeddings[[\"ml_generate_embedding_result\"]])\n", "clustered_result = cluster_model.predict(predicted_embeddings)\n", "# Notice the CENTROID_ID column, which is the ID number of the group that\n", "# each complaint belongs to.\n", @@ -1422,7 +1410,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": { "id": "2E7wXM_jGqo6" }, @@ -1430,7 +1418,7 @@ { "data": { "text/html": [ - "Query job 8d4f24d6-dc37-47d3-8b4d-4505a55c4ccc is DONE. 10.4 MB processed. Open Job" + "Query job d6c61334-255f-43fe-9a8f-9fbf6cdcb2be is DONE. 10.5 MB processed. Open Job" ], "text/plain": [ "" @@ -1442,7 +1430,7 @@ { "data": { "text/html": [ - "Query job c1f979ee-1f5d-4f37-8595-ee2167c06e63 is DONE. 10.4 MB processed. Open Job" + "Query job 03a12383-6752-45ca-9b01-36eecc74fb8a is DONE. 10.5 MB processed. Open Job" ], "text/plain": [ "" @@ -1468,7 +1456,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "metadata": { "id": "ZNDiueI9IP5e" }, @@ -1478,38 +1466,45 @@ "output_type": "stream", "text": [ "comment list 1:\n", - "1. This is the third such complaint I have submitted regarding the same type of issue over the past 12-18 months. \n", + "1. This debt was from my identity being stolen I didnt open any account that resulted in this collection i have completed a police report which can be verified with the XXXX police @ XXXX report # XXXX and i have a notarized identity theft affidavit from ftc please remove this off of my credit and close my file ASAP\n", + "2. On XX/XX/XXXX this company received certified mail asking for validation of debt. On XX/XX/XXXX the company still did not validate debt owed and they did not mark the debt disputed by XX/XX/XXXX through the major credit reporting bureaus. This is a violation of the FDCPA and FCRA. I did send a second letter which the company received on XX/XX/XXXX . A lady from the company called and talked to me about the debt on XX/XX/XXXX but again did not have the credit bureaus mark the item as disputed. The company still violated the laws. Section [ 15 U.S.C. 1681s-2 ] ( 3 ) duty to provide notice of dispute. If the completeness or accuracy of any information furnished by any person to any consumer reporting agency is disputed to such person by a consumer, the person may not furnish the information to any consumer reporting agency without notice that such information is disputed. ( B ) ti me of notice! The notice required under sub paragraph ( A ) shall be provided to the customer prior to, or no later than 30 days after, furnishing the negative information to a consumer reporting agency described in section 603 ( p ). This company violated the state laws. I received no information until XX/XX/XXXX . Therefore by law the company should have the item removed from the credit agencies such as transunion and XXXX . I tried to call the company back about the laws that was broken and left my name no return call. The copy of my credit reports are below and as you can see the items was n't marked disputed. XXXX is marked disputed because on XX/XX/XXXX I myself disputed the information with the credit bureau. The lady stated they did n't receive my dispute letter until XX/XX/XXXX . Included is certified mail reciepts with date, time stamp, and signature of the person who signed for the certified mail on XX/XX/XXXX and XX/XX/XXXX . So again the company violated the laws and I have all the proof. If I have a contract with this company please send to me by mail a contract bearing my signature of the contract.\n", + "3. On XX/XX/2022, Pioneer Credit Recovery of XXXX, NY identified an alleged debt, which I do not owe. \n", + "\n", + "On XX/XX/2022, I wrote a dispute letter to Pioneer, requesting that they stop communication with me, record my dispute, and provide verification of the debt if they believe otherwise. \n", "\n", - "On XX/XX/XXXX, my co-signed account was flagged by Navient as past due. The XXXX payment was mailed priority on XX/XX/XXXX and received by Navient on XX/XX/XXXX and delivered to \" an individual '' per the post office. \n", - "I called Navient on XX/XX/XXXX to talk to them about why my account was flagged since they received the payment long before the due date. The payment is sent via XXXX money orders under the same cover. The XXXX money order ( {$160.00} ) was cashed on XX/XX/XXXX per XXXX XXXX, the second money order ( {$250.00} ) which was sent in the same priority envelope and received the same time has not been cashed. \n", - "When I called the customer service agent at Navient she told me that my account was past due and wanted me to send another payment. When I explained that they had received the payment she argued with me that if they received it, the payment would have been cashed. I asked to speak with a supervisor. \n", - "I was connected with supervisor, XXXX XXXX, who asked that I send copies of the payments to him so he could submit for a missing payment request. I faxed the proof on XXXX @ XXXX with a receipt acknowledgment. \n", - "On XX/XX/XXXX, the payment was still not applied to the account. When I called XXXX XXXX, the money order was still not cashed. I called Navient again. Because of an argumentative customer service rep again, I requested to speak with a supervisor. I spoke with XXXX XXXX. She states that payment was not received. I explained the situation again. She said the missing payment request had not been submitted. She had me upload the documents so she could request a missing payment search. I have done everything I have been asked. \n", - "This issues continues to occur. For approximately 6 months at a time, Navient gets this right then there are issues again and again. I have submitted CFPB complaints about this in the past. \n", - "I was told it would take 5-7 business days to be resolved.\n", - "2. I tried to submit a teacher loan forgiveness application and they lost my application. I submitted the application again after talking to XXXX people at Nelnet. Then when I called back to check on the status they told me that I needed to submit another form for a different school that I worked at. I had already called previously and asked if I needed to submit any other papers and they told me \" no ''. Therefore, I have been paying my loan for 5 months and it should be forgiven. I am still paying my loan because I have to wait for them to approve the new forgiveness paperwork.\n", - "3. PayPal continues to overcharge their currency rate. It it always inflated in their favor so that they collect the difference.\n", - "4. My government feeds are not coming on to my card and I need the problem fix today\n", - "5. Paypal Credit 's website is confusing and does not accurately reflect all activity. When speaking with representatives like XXXX, it 's confusing to them and they can barely follow along with it. I am not receiving statements, which proves it difficult to determine the due dates on the accounts. The Reps are n't knowledgeable and the only thing they repeat to you is the amount due on the screen.\n", + "Pioneer has not responded with verification, but has attempted to collect the debt since then by phone ( XX/XX/2022 ) and mail ( XX/XX/2022 ).\n", + "4. Disputed with the company on several occasions and they still havent provided proof in a timely manner. The FCRA gives the company 30 days to respond. I have not gotten a response.\n", + "5. I am not aware of this XXXX XXXX XXXX XXXX XXXX , XXXX balance. I have never seen anything dealing with this lender. Also, I have been threated that in 30 days they will seek to make a judgement on debt that does not belong to me. I understand that they are looking to offer me a settlement. However, I do not believe the validity of such debt accusation. Furthermore, I will not be limited to the action of court threats when I did not receive any notice of debt based on communication. The amount is {$880.00} from MBNA which was acquired by Bank of America in 2006. I do not claim debt.\n", "\n", "comment list 2:\n", - "1. XXXX on XXXX XX/XX/2021 I have Mr. Cooper mortgage for years now. On XXXX XXXX XXXX I made an additional payment of $ XXXX towards my principal. More than 4 days - it's not reflected in the Amortization schedule ( Amortization schedule is not even visible ). Even after so many additional principal payments, Payoff calculator is way off and it still shows XXXX maturity date while it should start showing something like XX/XX/XXXX/XX/XX/XXXX as the initial date. There are lots of discrepancies on their website to reflect the balance and total. When called customer service on Friday and also chatted - i was assured of fixing this - but no fix till this point of time. Customer service there is a long wait. Auto bot doesn't let customers talk to the real person. \n", - "Finally after a lots of follow-up I got the amortization schedule via email but it is not reflecting another additional principal payment of {$4700.00} made on XX/XX/2021. \n", - "I did numerous chats and phone calls. Why i should depend on inefficinent humans to see my revised amortization schedule? \n", - "Why the online amortization schedule is not visible now? \n", + "1. My name is XXXX XXXX XXXX. This issue with a Loan Till Payday account was previously reported to you for collection practices, etc. I had a pay day loan in 2013. At the time, I banked with XXXX XXXX, who advised me that pay day loans are not good, and in the end XXXX closed my bank account, it was involuntary. In the interim, I made payments to the agency. XXXX and XXXX were the primary contacts. On the last payment, due to the fact that I told him I was coming in to pay cash, and they withdrew the funds, electronically, my account was affected. XXXX advised me that the payment made was the last payment and the other ( which was primarily interest remaining ) would be charged off. XXXX later called me and advised that XXXX was not authorized to make that decision and demanded the payment. I do n't understand how one person can cancel the arrangements made by someone else. \n", + "\n", + "In the end, they sold my account. It was reported to you, and that creditor then stated no further collection activity would occur. \n", + "\n", + "Last week I began receiving calls from a collection agency, XXXX XXXX stating I would called for a civil deposition on this account. I do n't even know this agency. Later, I then received another call stating that I needed to hold, and after several clicks was connected to someone at a Mediaction service. I denied the owing the loan and stated it was paid. \n", + "\n", + "Today, I received a call from an outsource service courier about a missed appointment or hearing??? What?? I have no idea who these people are. I called Loan Till Payday and was advised the loan was sold and I needed to settle with the new company. So, does this mean they are continuing to attempt to collect {$200.00}. \n", "\n", - "Worst thing, after turning on escrow account, there is no transparency. Amount of escrow account is not in sync with all the sections of my online account. It seems that there are too many internal buggy systems in Mr. Cooper and everybody from different IT department is showing a different $ $ figure at various places. \n", - "Highly inefficient organization.\n", - "2. I had a loan with national Collegiate Trust. i did n't make any payments on the loan for 6 years and due to the \" statute to limitations '' the loan collector should not be contacting me any more, by they still are in both forms phone call 's and letters. I am also trying to buy a house for my family and found out that i can not get a USDA loan for the house because National Collegiate Trust has filled the loan as a government delinquent loan. The problem with that is the loan is a private loan not a federal/Government loan. due to the way they filled the lion on my credit report i am not able to buy my first home.\n", - "3. Cares act refund requested in XXXX, called multiple times given conflicting information every time. Its now XXXX and looking like its going to be even longer potentially months before the refund arrives. Blatant disregard for the cares act signed into law and terrible customer service. This company is ill suited to service the federal government contract it enjoys.\n", - "4. In an attempt to pay off my loan with Ocwen, I sent XXXX large payments to the institution on XXXX XXXX that would have decreased my loan amount to within a couple of hundred dollars ( you can not send XXXX payment that exceeds 80 % of the loan balance so I broke the payments up into XXXX XXXX. I scheduled the payments for the same day because I did not want any interest to accrue. After a few days, I noticed where the XXXX payments were withdrawn from my bank but that my Ocwen balance had not changed to reflect my XXXX payments. I called Ocwen on XXXX XXXX to ask about the payment. The Ocwen rep explained that because the XXXX payments were scheduled for the same day, that it exceeded the 80 % max limit. I asked Ocwen to return my XXXX payments. The rep said it would take XXXX business days. I called Ocwen back on XXXX XXXX when my funds had not been returned to my bank account. I gave them my account number and routing number to my bank, information that I am sure they already had since my monthly mortgage payments are debited from my account. They asked me to wait a couple more days. I called Ocwen back on XXXX XXXX. The rep asked me to be patient and assured me that the funds would be returned by XXXX XXXX. There were no funds returned to my account on XXXX XXXX. I called Ocwen again. I was asked to wait 40+ minutes on hold while the Ocwen rep put me in touch with an escalation specialist ( ES ). The ES told me that my funds would be reurned within XXXX hours and that he had put it in as a priority because I had called so often. There were no funds on XXXX XXXX. I called Ocwen again to see if there was a problem. There was. After speaking to a rep and another ES, I was told that my funds could not be returned to me! The ES said that he did not see my funds! He claimed to put me in touch with someone who XXXX be able to address my concerns. So that 's where I am now, on hold waiting to speak to yet another person! This is a significant amount of money and I fear that Ocwen is trying to get away with keeping my XXXX payments!\n", - "5. In XX/XX/XXXX we received our first bill from XXXX XXXX for XXXX. ( attached ) We promptly paid the bill. Again, in XX/XX/XXXX we paid our second bill for XXXX. Again, both on time. Then when XX/XX/XXXX statement came we were billed XXXX. My husband called XXXX XXXX to find out what the issue was. We were told there was a loan shortfall caused by us paying XXXX ( the amount we were billed ) and that our loan was re-amoritized. I argued I had a fixed rate loan, had never missed a payment, had never made a late payment, and paid exactly what was billed. Well, after double checking my promissory note ( attached ) and TILA ( attached ) I was to always be billed XXXX. XXXX changed the monthly payment and thus caused a shortfall. When I told them this information they refused to correct the shortfall. Not only did they not correct the mistake they refused to return my calls or emails to provide answers for these issues. Around 90 days later and this issue still exists and they still refuse to answer. Additionally I offered to make up the shortfall myself by offering a check for the missing amount and they just applied it to interest. Thus the shortfall still exists. The extra amount would have gone directly to principal. Additionally, in XX/XX/XXXX we made an extra payment amount on top of the monthly payment. This was made all at the same time. The letter we sent contained directions to only apply extra payments beyond the monthly requirement be applied to principal and not the next months payment. This was ignored. Then XXXX \" a higher up '' as she calls herself lied and told me it went to principal when clearly it did not. We requested this be fixed and it has not been done. No one has offered to fix anything either. A certified letter is attached that I mailed. I also made dozens of calls.\n", + "I attempted to call the numbers, and now no one picks up just a voicemail. I called the supposed service courier and advised that their number was showing up as a spam/fraud number and that if they were a legitimate company then they should leave their name, location, a number ( not a voicemail ), and the case they are calling me about. I have not been served with any collection documents - why am I being threatened with a deposition??? \n", + "\n", + "Telephone number recently calling me : ( XXXX ) XXXX. \n", + "\n", + "Please help.\n", + "2. I receive 2 or 3 phone calls every day since early XXXX, my references receive calls. I will gladly satisfy this debt however even after 1st telling them the calls haven't stopped as though they are going to intimidate me. If the calls stopped for just 3 or 4 days I would satisfy my obligation but not because they keep calling me as well as my references.\n", + "3. Last month I received a phone call for my husband from XXXX XXXX XXXX saying he owed money and if I did not pay today it would be sent to litigation. The debt was Wachovia/wells Fargo, and account that we have never had. I had my husband call to get more information and they became very nasty with him. I called back asking for documentation on the debt because i did not think it was our debt and they became aggressive. They did email my husband something saying how much he owed, and I called back and asked to be emailed a copy, and the dollar amounts did not match. I called Wells Fargo and went over the above and verified that we have never had an account with them and I sent them the emails the XXXX sent to us and they started a fraud investigation. Yesterday I received another collections letter in the mail from the. Still trying to collect this debt. These people have my husbands full social security number ( we did not give it to them )\n", + "4. A company call XXXX XXXX XXXX came onto my private property on XX/XX/2018 and stole my automobile. I did receive any type of notice saying they collecting on a debt. If they take or threaten to take any nonjudicial action ( i.e, without a court order ) to repossess property when there is no present right to possession of the property they is in violation. l did not receive any type of notice asking if they can enter onto my private property and steal my private automobile.\n", + "5. Navient financial continues to send me erroneous debt collection emails. I have repeatedly asked them to remove my email address and to cease all communication with me. \n", + "I have no relationship with Navient and their continued threatening email is very unsettling. \n", + "\n", + "I just want their erroneous threats to stop. \n", + "\n", + "Below is the latest email I have received from them : Last Day to call this office XXXX by XXXX Regards, XXXX XXXX Team Lead Specialist Charge off Unit XXXX XXXX\n", "\n" ] } ], "source": [ - "# Build plain-text prompts to send to PaLM 2. Use only 5 complaints from each group.\n", + "# Build plain-text prompts to send to Gemini. Use only 5 complaints from each group.\n", "prompt1 = 'comment list 1:\\n'\n", "for i in range(5):\n", " prompt1 += str(i + 1) + '. ' + \\\n", @@ -1526,7 +1521,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "metadata": { "id": "BfHGJLirzSvH" }, @@ -1537,37 +1532,44 @@ "text": [ "Please highlight the most obvious difference between the two lists of comments:\n", "comment list 1:\n", - "1. This is the third such complaint I have submitted regarding the same type of issue over the past 12-18 months. \n", + "1. This debt was from my identity being stolen I didnt open any account that resulted in this collection i have completed a police report which can be verified with the XXXX police @ XXXX report # XXXX and i have a notarized identity theft affidavit from ftc please remove this off of my credit and close my file ASAP\n", + "2. On XX/XX/XXXX this company received certified mail asking for validation of debt. On XX/XX/XXXX the company still did not validate debt owed and they did not mark the debt disputed by XX/XX/XXXX through the major credit reporting bureaus. This is a violation of the FDCPA and FCRA. I did send a second letter which the company received on XX/XX/XXXX . A lady from the company called and talked to me about the debt on XX/XX/XXXX but again did not have the credit bureaus mark the item as disputed. The company still violated the laws. Section [ 15 U.S.C. 1681s-2 ] ( 3 ) duty to provide notice of dispute. If the completeness or accuracy of any information furnished by any person to any consumer reporting agency is disputed to such person by a consumer, the person may not furnish the information to any consumer reporting agency without notice that such information is disputed. ( B ) ti me of notice! The notice required under sub paragraph ( A ) shall be provided to the customer prior to, or no later than 30 days after, furnishing the negative information to a consumer reporting agency described in section 603 ( p ). This company violated the state laws. I received no information until XX/XX/XXXX . Therefore by law the company should have the item removed from the credit agencies such as transunion and XXXX . I tried to call the company back about the laws that was broken and left my name no return call. The copy of my credit reports are below and as you can see the items was n't marked disputed. XXXX is marked disputed because on XX/XX/XXXX I myself disputed the information with the credit bureau. The lady stated they did n't receive my dispute letter until XX/XX/XXXX . Included is certified mail reciepts with date, time stamp, and signature of the person who signed for the certified mail on XX/XX/XXXX and XX/XX/XXXX . So again the company violated the laws and I have all the proof. If I have a contract with this company please send to me by mail a contract bearing my signature of the contract.\n", + "3. On XX/XX/2022, Pioneer Credit Recovery of XXXX, NY identified an alleged debt, which I do not owe. \n", "\n", - "On XX/XX/XXXX, my co-signed account was flagged by Navient as past due. The XXXX payment was mailed priority on XX/XX/XXXX and received by Navient on XX/XX/XXXX and delivered to \" an individual '' per the post office. \n", - "I called Navient on XX/XX/XXXX to talk to them about why my account was flagged since they received the payment long before the due date. The payment is sent via XXXX money orders under the same cover. The XXXX money order ( {$160.00} ) was cashed on XX/XX/XXXX per XXXX XXXX, the second money order ( {$250.00} ) which was sent in the same priority envelope and received the same time has not been cashed. \n", - "When I called the customer service agent at Navient she told me that my account was past due and wanted me to send another payment. When I explained that they had received the payment she argued with me that if they received it, the payment would have been cashed. I asked to speak with a supervisor. \n", - "I was connected with supervisor, XXXX XXXX, who asked that I send copies of the payments to him so he could submit for a missing payment request. I faxed the proof on XXXX @ XXXX with a receipt acknowledgment. \n", - "On XX/XX/XXXX, the payment was still not applied to the account. When I called XXXX XXXX, the money order was still not cashed. I called Navient again. Because of an argumentative customer service rep again, I requested to speak with a supervisor. I spoke with XXXX XXXX. She states that payment was not received. I explained the situation again. She said the missing payment request had not been submitted. She had me upload the documents so she could request a missing payment search. I have done everything I have been asked. \n", - "This issues continues to occur. For approximately 6 months at a time, Navient gets this right then there are issues again and again. I have submitted CFPB complaints about this in the past. \n", - "I was told it would take 5-7 business days to be resolved.\n", - "2. I tried to submit a teacher loan forgiveness application and they lost my application. I submitted the application again after talking to XXXX people at Nelnet. Then when I called back to check on the status they told me that I needed to submit another form for a different school that I worked at. I had already called previously and asked if I needed to submit any other papers and they told me \" no ''. Therefore, I have been paying my loan for 5 months and it should be forgiven. I am still paying my loan because I have to wait for them to approve the new forgiveness paperwork.\n", - "3. PayPal continues to overcharge their currency rate. It it always inflated in their favor so that they collect the difference.\n", - "4. My government feeds are not coming on to my card and I need the problem fix today\n", - "5. Paypal Credit 's website is confusing and does not accurately reflect all activity. When speaking with representatives like XXXX, it 's confusing to them and they can barely follow along with it. I am not receiving statements, which proves it difficult to determine the due dates on the accounts. The Reps are n't knowledgeable and the only thing they repeat to you is the amount due on the screen.\n", + "On XX/XX/2022, I wrote a dispute letter to Pioneer, requesting that they stop communication with me, record my dispute, and provide verification of the debt if they believe otherwise. \n", + "\n", + "Pioneer has not responded with verification, but has attempted to collect the debt since then by phone ( XX/XX/2022 ) and mail ( XX/XX/2022 ).\n", + "4. Disputed with the company on several occasions and they still havent provided proof in a timely manner. The FCRA gives the company 30 days to respond. I have not gotten a response.\n", + "5. I am not aware of this XXXX XXXX XXXX XXXX XXXX , XXXX balance. I have never seen anything dealing with this lender. Also, I have been threated that in 30 days they will seek to make a judgement on debt that does not belong to me. I understand that they are looking to offer me a settlement. However, I do not believe the validity of such debt accusation. Furthermore, I will not be limited to the action of court threats when I did not receive any notice of debt based on communication. The amount is {$880.00} from MBNA which was acquired by Bank of America in 2006. I do not claim debt.\n", "comment list 2:\n", - "1. XXXX on XXXX XX/XX/2021 I have Mr. Cooper mortgage for years now. On XXXX XXXX XXXX I made an additional payment of $ XXXX towards my principal. More than 4 days - it's not reflected in the Amortization schedule ( Amortization schedule is not even visible ). Even after so many additional principal payments, Payoff calculator is way off and it still shows XXXX maturity date while it should start showing something like XX/XX/XXXX/XX/XX/XXXX as the initial date. There are lots of discrepancies on their website to reflect the balance and total. When called customer service on Friday and also chatted - i was assured of fixing this - but no fix till this point of time. Customer service there is a long wait. Auto bot doesn't let customers talk to the real person. \n", - "Finally after a lots of follow-up I got the amortization schedule via email but it is not reflecting another additional principal payment of {$4700.00} made on XX/XX/2021. \n", - "I did numerous chats and phone calls. Why i should depend on inefficinent humans to see my revised amortization schedule? \n", - "Why the online amortization schedule is not visible now? \n", + "1. My name is XXXX XXXX XXXX. This issue with a Loan Till Payday account was previously reported to you for collection practices, etc. I had a pay day loan in 2013. At the time, I banked with XXXX XXXX, who advised me that pay day loans are not good, and in the end XXXX closed my bank account, it was involuntary. In the interim, I made payments to the agency. XXXX and XXXX were the primary contacts. On the last payment, due to the fact that I told him I was coming in to pay cash, and they withdrew the funds, electronically, my account was affected. XXXX advised me that the payment made was the last payment and the other ( which was primarily interest remaining ) would be charged off. XXXX later called me and advised that XXXX was not authorized to make that decision and demanded the payment. I do n't understand how one person can cancel the arrangements made by someone else. \n", + "\n", + "In the end, they sold my account. It was reported to you, and that creditor then stated no further collection activity would occur. \n", + "\n", + "Last week I began receiving calls from a collection agency, XXXX XXXX stating I would called for a civil deposition on this account. I do n't even know this agency. Later, I then received another call stating that I needed to hold, and after several clicks was connected to someone at a Mediaction service. I denied the owing the loan and stated it was paid. \n", + "\n", + "Today, I received a call from an outsource service courier about a missed appointment or hearing??? What?? I have no idea who these people are. I called Loan Till Payday and was advised the loan was sold and I needed to settle with the new company. So, does this mean they are continuing to attempt to collect {$200.00}. \n", + "\n", + "I attempted to call the numbers, and now no one picks up just a voicemail. I called the supposed service courier and advised that their number was showing up as a spam/fraud number and that if they were a legitimate company then they should leave their name, location, a number ( not a voicemail ), and the case they are calling me about. I have not been served with any collection documents - why am I being threatened with a deposition??? \n", + "\n", + "Telephone number recently calling me : ( XXXX ) XXXX. \n", + "\n", + "Please help.\n", + "2. I receive 2 or 3 phone calls every day since early XXXX, my references receive calls. I will gladly satisfy this debt however even after 1st telling them the calls haven't stopped as though they are going to intimidate me. If the calls stopped for just 3 or 4 days I would satisfy my obligation but not because they keep calling me as well as my references.\n", + "3. Last month I received a phone call for my husband from XXXX XXXX XXXX saying he owed money and if I did not pay today it would be sent to litigation. The debt was Wachovia/wells Fargo, and account that we have never had. I had my husband call to get more information and they became very nasty with him. I called back asking for documentation on the debt because i did not think it was our debt and they became aggressive. They did email my husband something saying how much he owed, and I called back and asked to be emailed a copy, and the dollar amounts did not match. I called Wells Fargo and went over the above and verified that we have never had an account with them and I sent them the emails the XXXX sent to us and they started a fraud investigation. Yesterday I received another collections letter in the mail from the. Still trying to collect this debt. These people have my husbands full social security number ( we did not give it to them )\n", + "4. A company call XXXX XXXX XXXX came onto my private property on XX/XX/2018 and stole my automobile. I did receive any type of notice saying they collecting on a debt. If they take or threaten to take any nonjudicial action ( i.e, without a court order ) to repossess property when there is no present right to possession of the property they is in violation. l did not receive any type of notice asking if they can enter onto my private property and steal my private automobile.\n", + "5. Navient financial continues to send me erroneous debt collection emails. I have repeatedly asked them to remove my email address and to cease all communication with me. \n", + "I have no relationship with Navient and their continued threatening email is very unsettling. \n", "\n", - "Worst thing, after turning on escrow account, there is no transparency. Amount of escrow account is not in sync with all the sections of my online account. It seems that there are too many internal buggy systems in Mr. Cooper and everybody from different IT department is showing a different $ $ figure at various places. \n", - "Highly inefficient organization.\n", - "2. I had a loan with national Collegiate Trust. i did n't make any payments on the loan for 6 years and due to the \" statute to limitations '' the loan collector should not be contacting me any more, by they still are in both forms phone call 's and letters. I am also trying to buy a house for my family and found out that i can not get a USDA loan for the house because National Collegiate Trust has filled the loan as a government delinquent loan. The problem with that is the loan is a private loan not a federal/Government loan. due to the way they filled the lion on my credit report i am not able to buy my first home.\n", - "3. Cares act refund requested in XXXX, called multiple times given conflicting information every time. Its now XXXX and looking like its going to be even longer potentially months before the refund arrives. Blatant disregard for the cares act signed into law and terrible customer service. This company is ill suited to service the federal government contract it enjoys.\n", - "4. In an attempt to pay off my loan with Ocwen, I sent XXXX large payments to the institution on XXXX XXXX that would have decreased my loan amount to within a couple of hundred dollars ( you can not send XXXX payment that exceeds 80 % of the loan balance so I broke the payments up into XXXX XXXX. I scheduled the payments for the same day because I did not want any interest to accrue. After a few days, I noticed where the XXXX payments were withdrawn from my bank but that my Ocwen balance had not changed to reflect my XXXX payments. I called Ocwen on XXXX XXXX to ask about the payment. The Ocwen rep explained that because the XXXX payments were scheduled for the same day, that it exceeded the 80 % max limit. I asked Ocwen to return my XXXX payments. The rep said it would take XXXX business days. I called Ocwen back on XXXX XXXX when my funds had not been returned to my bank account. I gave them my account number and routing number to my bank, information that I am sure they already had since my monthly mortgage payments are debited from my account. They asked me to wait a couple more days. I called Ocwen back on XXXX XXXX. The rep asked me to be patient and assured me that the funds would be returned by XXXX XXXX. There were no funds returned to my account on XXXX XXXX. I called Ocwen again. I was asked to wait 40+ minutes on hold while the Ocwen rep put me in touch with an escalation specialist ( ES ). The ES told me that my funds would be reurned within XXXX hours and that he had put it in as a priority because I had called so often. There were no funds on XXXX XXXX. I called Ocwen again to see if there was a problem. There was. After speaking to a rep and another ES, I was told that my funds could not be returned to me! The ES said that he did not see my funds! He claimed to put me in touch with someone who XXXX be able to address my concerns. So that 's where I am now, on hold waiting to speak to yet another person! This is a significant amount of money and I fear that Ocwen is trying to get away with keeping my XXXX payments!\n", - "5. In XX/XX/XXXX we received our first bill from XXXX XXXX for XXXX. ( attached ) We promptly paid the bill. Again, in XX/XX/XXXX we paid our second bill for XXXX. Again, both on time. Then when XX/XX/XXXX statement came we were billed XXXX. My husband called XXXX XXXX to find out what the issue was. We were told there was a loan shortfall caused by us paying XXXX ( the amount we were billed ) and that our loan was re-amoritized. I argued I had a fixed rate loan, had never missed a payment, had never made a late payment, and paid exactly what was billed. Well, after double checking my promissory note ( attached ) and TILA ( attached ) I was to always be billed XXXX. XXXX changed the monthly payment and thus caused a shortfall. When I told them this information they refused to correct the shortfall. Not only did they not correct the mistake they refused to return my calls or emails to provide answers for these issues. Around 90 days later and this issue still exists and they still refuse to answer. Additionally I offered to make up the shortfall myself by offering a check for the missing amount and they just applied it to interest. Thus the shortfall still exists. The extra amount would have gone directly to principal. Additionally, in XX/XX/XXXX we made an extra payment amount on top of the monthly payment. This was made all at the same time. The letter we sent contained directions to only apply extra payments beyond the monthly requirement be applied to principal and not the next months payment. This was ignored. Then XXXX \" a higher up '' as she calls herself lied and told me it went to principal when clearly it did not. We requested this be fixed and it has not been done. No one has offered to fix anything either. A certified letter is attached that I mailed. I also made dozens of calls.\n", + "I just want their erroneous threats to stop. \n", + "\n", + "Below is the latest email I have received from them : Last Day to call this office XXXX by XXXX Regards, XXXX XXXX Team Lead Specialist Charge off Unit XXXX XXXX\n", "\n" ] } ], "source": [ - "# The plain English request we will make of PaLM 2\n", + "# The plain English request we will make of Gemini\n", "prompt = (\n", " \"Please highlight the most obvious difference between \"\n", " \"the two lists of comments:\\n\" + prompt1 + prompt2\n", @@ -1585,7 +1587,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "metadata": { "id": "mL5P0_3X04dE" }, @@ -1593,7 +1595,7 @@ { "data": { "text/html": [ - "Query job de5da6c9-96b5-42a1-b199-42687392fe37 is DONE. 0 Bytes processed. Open Job" + "Query job 3a46cad4-14e5-4137-a042-14380733b467 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1611,11 +1613,24 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "metadata": { "id": "ICWHsqAW1FNk" }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Load job 939037f0-66df-42a4-b301-0b3ba26bae7c is DONE. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Make a DataFrame containing only a single row with our prompt for Gemini\n", "df = bf.DataFrame({\"prompt\": [prompt]})" @@ -1623,7 +1638,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "metadata": { "id": "gB7e1LXU1pst" }, @@ -1631,7 +1646,7 @@ { "data": { "text/html": [ - "Query job 1363c327-00b5-4835-a902-da84882bc996 is DONE. 0 Bytes processed. Open Job" + "Query job c662b2c7-7185-4681-b7c6-60c81e9c8cd4 is DONE. 8.2 kB processed. Open Job" ], "text/plain": [ "" @@ -1641,21 +1656,17 @@ "output_type": "display_data" }, { - "data": { - "text/html": [ - "Query job c5996f1e-a140-4e7d-8775-091e1a73d882 is DONE. 8 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/bigframes/core/__init__.py:108: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] }, { "data": { "text/html": [ - "Query job db1de3ab-2e6e-4b3f-8e6a-01bad33ac45f is DONE. 2 Bytes processed. Open Job" + "Query job 9a4d6735-c307-4a60-96f9-d81330925e6c is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1667,7 +1678,7 @@ { "data": { "text/html": [ - "Query job 38d9a9d0-7f03-4091-858b-f864da30987e is DONE. 375 Bytes processed. Open Job" + "Query job 17bde6e6-8b26-48a7-9c57-b7b9752c1f54 is DONE. 1.8 kB processed. Open Job" ], "text/plain": [ "" @@ -1679,10 +1690,10 @@ { "data": { "text/plain": [ - "'The most obvious difference between the two lists of comments is the subject matter. The first list of comments is primarily focused on issues with financial institutions, such as Navient, Nelnet, PayPal, and Mr. Cooper. The second list of comments is primarily focused on issues with government agencies, such as the National Collegiate Trust, the USDA, and Ocwen.'" + "\"## Key Differences between Comment Lists 1 and 2:\\n\\n**Comment List 1:**\\n\\n* **Focuses on Legal Violations:** The comments in List 1 primarily focus on how the debt collectors violated specific laws, such as the FDCPA and FCRA, by not validating debt, not marking accounts as disputed, and using illegal collection tactics.\\n* **Detailed Evidence:** Commenters provide detailed evidence of their claims, including dates, reference numbers, police reports, and copies of communications.\\n* **Formal Tone:** The language in List 1 is more formal and uses legal terminology, suggesting the commenters may have a deeper understanding of their rights.\\n* **Emphasis on Debt Accuracy:** Many comments explicitly deny owing the debt and question its validity, requesting proof and demanding removal from credit reports. \\n\\n**Comment List 2:**\\n\\n* **Focus on Harassment and Intimidation:** The comments in List 2 highlight the harassing and intimidating behavior of the debt collectors, such as making multiple calls, contacting references, and threatening legal action.\\n* **Emotional Language:** Commenters express frustration, fear, and anger towards the debt collectors' behavior.\\n* **Less Legal Detail:** While some commenters mention specific laws, they provide less detailed evidence than List 1.\\n* **Uncertainty About Debt:** Several commenters are unsure whether they actually owe the debt, questioning its origin and validity. \\n\\n**Overall:**\\n\\n* List 1 focuses on legal arguments and violations, while List 2 emphasizes emotional distress and improper collection tactics.\\n* List 1 provides more concrete evidence of wrongdoing, while List 2 relies more on personal experiences and descriptions.\\n* Both lists highlight the negative impacts of debt collection practices on individuals.\\n\"" ] }, - "execution_count": 19, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1690,7 +1701,7 @@ "source": [ "# Send the request for Gemini to generate a response to our prompt\n", "major_difference = q_a_model.predict(df)\n", - "# PaLM 2's response is the only row in the dataframe result \n", + "# Gemini's response is the only row in the dataframe result \n", "major_difference[\"ml_generate_text_llm_result\"].iloc[0]" ] }, From d2fc51a30c4fff6fe0b98df61eec70ddb28b37ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 20 Aug 2024 12:15:27 -0500 Subject: [PATCH 761/921] docs: add columns for "requires ordering/index" to supported APIs summary (#892) --- bigframes/core/validations.py | 15 ++++++++++++- bigframes/dataframe.py | 40 +++++++++++++-------------------- bigframes/operations/base.py | 10 --------- bigframes/series.py | 35 ++++++++++++++--------------- scripts/publish_api_coverage.py | 24 +++++++++++++++++++- 5 files changed, 69 insertions(+), 55 deletions(-) diff --git a/bigframes/core/validations.py b/bigframes/core/validations.py index 9c03ddb9309..daa12528243 100644 --- a/bigframes/core/validations.py +++ b/bigframes/core/validations.py @@ -17,7 +17,7 @@ from __future__ import annotations import functools -from typing import Optional, Protocol, TYPE_CHECKING +from typing import Optional, Protocol, TYPE_CHECKING, Union import bigframes.constants import bigframes.exceptions @@ -25,6 +25,8 @@ if TYPE_CHECKING: from bigframes import Session from bigframes.core.blocks import Block + from bigframes.dataframe import DataFrame + from bigframes.operations.base import SeriesMethods class HasSession(Protocol): @@ -37,6 +39,16 @@ def _block(self) -> Block: ... +def requires_index(meth): + @functools.wraps(meth) + def guarded_meth(df: Union[DataFrame, SeriesMethods], *args, **kwargs): + df._throw_if_null_index(meth.__name__) + return meth(df, *args, **kwargs) + + guarded_meth._validations_requires_index = True # type: ignore + return guarded_meth + + def requires_ordering(suggestion: Optional[str] = None): def decorator(meth): @functools.wraps(meth) @@ -44,6 +56,7 @@ def guarded_meth(object: HasSession, *args, **kwargs): enforce_ordered(object, meth.__name__, suggestion) return meth(object, *args, **kwargs) + guarded_meth._validations_requires_ordering = True # type: ignore return guarded_meth return decorator diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index dabe85c9236..a174ef0b0f6 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -17,7 +17,6 @@ from __future__ import annotations import datetime -import functools import inspect import re import sys @@ -92,15 +91,6 @@ ) -def requires_index(meth): - @functools.wraps(meth) - def guarded_meth(df: DataFrame, *args, **kwargs): - df._throw_if_null_index(meth.__name__) - return meth(df, *args, **kwargs) - - return guarded_meth - - # Inherits from pandas DataFrame so that we can use the same docstrings. @log_adapter.class_logger class DataFrame(vendored_pandas_frame.DataFrame): @@ -261,7 +251,7 @@ def _sql_names( return results @property - @requires_index + @validations.requires_index def index( self, ) -> indexes.Index: @@ -277,7 +267,7 @@ def index(self, value): self.index.name = value.name if hasattr(value, "name") else None @property - @requires_index + @validations.requires_index def loc(self) -> indexers.LocDataFrameIndexer: return indexers.LocDataFrameIndexer(self) @@ -292,7 +282,7 @@ def iat(self) -> indexers.IatDataFrameIndexer: return indexers.IatDataFrameIndexer(self) @property - @requires_index + @validations.requires_index def at(self) -> indexers.AtDataFrameIndexer: return indexers.AtDataFrameIndexer(self) @@ -348,7 +338,7 @@ def _has_index(self) -> bool: def T(self) -> DataFrame: return DataFrame(self._get_block().transpose()) - @requires_index + @validations.requires_index @validations.requires_ordering() def transpose(self) -> DataFrame: return self.T @@ -417,7 +407,7 @@ def memory_usage(self, index: bool = True): column_sizes = pandas.concat([index_size, column_sizes]) return column_sizes - @requires_index + @validations.requires_index def info( self, verbose: Optional[bool] = None, @@ -1682,7 +1672,7 @@ def set_index( col_ids_strs: List[str] = [col_id for col_id in col_ids if col_id is not None] return DataFrame(self._block.set_index(col_ids_strs, append=append, drop=drop)) - @requires_index + @validations.requires_index def sort_index( self, ascending: bool = True, na_position: Literal["first", "last"] = "last" ) -> DataFrame: @@ -1884,7 +1874,7 @@ def reindex( if columns is not None: return self._reindex_columns(columns) - @requires_index + @validations.requires_index def _reindex_rows( self, index, @@ -1931,12 +1921,12 @@ def _reindex_columns(self, columns): result_df.columns = new_column_index return result_df - @requires_index + @validations.requires_index def reindex_like(self, other: DataFrame, *, validate: typing.Optional[bool] = None): return self.reindex(index=other.index, columns=other.columns, validate=validate) @validations.requires_ordering() - @requires_index + @validations.requires_index def interpolate(self, method: str = "linear") -> DataFrame: if method == "pad": return self.ffill() @@ -2231,12 +2221,12 @@ def agg( aggregate = agg aggregate.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.agg) - @requires_index + @validations.requires_index @validations.requires_ordering() def idxmin(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmin(self._block)) - @requires_index + @validations.requires_index @validations.requires_ordering() def idxmax(self) -> bigframes.series.Series: return bigframes.series.Series(block_ops.idxmax(self._block)) @@ -2345,7 +2335,7 @@ def _pivot( ) return DataFrame(pivot_block) - @requires_index + @validations.requires_index @validations.requires_ordering() def pivot( self, @@ -2360,7 +2350,7 @@ def pivot( ) -> DataFrame: return self._pivot(columns=columns, index=index, values=values) - @requires_index + @validations.requires_index @validations.requires_ordering() def pivot_table( self, @@ -2460,7 +2450,7 @@ def _stack_multi(self, level: LevelsType = -1): block = block.stack(levels=len(level)) return DataFrame(block) - @requires_index + @validations.requires_index @validations.requires_ordering() def unstack(self, level: LevelsType = -1): if not utils.is_list_like(level): @@ -2711,7 +2701,7 @@ def groupby( else: raise TypeError("You have to supply one of 'by' and 'level'") - @requires_index + @validations.requires_index def _groupby_level( self, level: LevelsType, diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index f339345971d..1daa1ea5aef 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -14,7 +14,6 @@ from __future__ import annotations -import functools import typing from typing import List, Sequence @@ -35,15 +34,6 @@ import bigframes.session -def requires_index(meth): - @functools.wraps(meth) - def guarded_meth(df: SeriesMethods, *args, **kwargs): - df._throw_if_null_index(meth.__name__) - return meth(df, *args, **kwargs) - - return guarded_meth - - class SeriesMethods: def __init__( self, diff --git a/bigframes/series.py b/bigframes/series.py index 7ba4858b5eb..c2137bca35d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -52,7 +52,6 @@ import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.operations.base -from bigframes.operations.base import requires_index import bigframes.operations.datetimes as dt import bigframes.operations.plotting as plotting import bigframes.operations.strings as strings @@ -88,7 +87,7 @@ def dtypes(self): return self._dtype @property - @requires_index + @validations.requires_index def loc(self) -> bigframes.core.indexers.LocSeriesIndexer: return bigframes.core.indexers.LocSeriesIndexer(self) @@ -103,7 +102,7 @@ def iat(self) -> bigframes.core.indexers.IatSeriesIndexer: return bigframes.core.indexers.IatSeriesIndexer(self) @property - @requires_index + @validations.requires_index def at(self) -> bigframes.core.indexers.AtSeriesIndexer: return bigframes.core.indexers.AtSeriesIndexer(self) @@ -142,7 +141,7 @@ def values(self) -> numpy.ndarray: return self.to_numpy() @property - @requires_index + @validations.requires_index def index(self) -> indexes.Index: return indexes.Index.from_frame(self) @@ -245,7 +244,7 @@ def rename( raise ValueError(f"Unsupported type of parameter index: {type(index)}") - @requires_index + @validations.requires_index def rename_axis( self, mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]], @@ -404,12 +403,12 @@ def drop( block = block.drop_columns([condition_id]) return Series(block.select_column(self._value_column)) - @requires_index + @validations.requires_index def droplevel(self, level: LevelsType, axis: int | str = 0): resolved_level_ids = self._resolve_levels(level) return Series(self._block.drop_levels(resolved_level_ids)) - @requires_index + @validations.requires_index def swaplevel(self, i: int = -2, j: int = -1): level_i = self._block.index_columns[i] level_j = self._block.index_columns[j] @@ -419,7 +418,7 @@ def swaplevel(self, i: int = -2, j: int = -1): ] return Series(self._block.reorder_levels(reordering)) - @requires_index + @validations.requires_index def reorder_levels(self, order: LevelsType, axis: int | str = 0): resolved_level_ids = self._resolve_levels(order) return Series(self._block.reorder_levels(resolved_level_ids)) @@ -609,7 +608,7 @@ def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): return Series(block.select_column(result)) @validations.requires_ordering() - @requires_index + @validations.requires_index def interpolate(self, method: str = "linear") -> Series: if method == "pad": return self.ffill() @@ -1162,7 +1161,7 @@ def argmin(self) -> int: scalars.Scalar, Series(block.select_column(row_nums)).iloc[0] ) - @requires_index + @validations.requires_index def unstack(self, level: LevelsType = -1): if isinstance(level, int) or isinstance(level, str): level = [level] @@ -1186,7 +1185,7 @@ def unstack(self, level: LevelsType = -1): ) return bigframes.dataframe.DataFrame(pivot_block) - @requires_index + @validations.requires_index def idxmax(self) -> blocks.Label: block = self._block.order_by( [ @@ -1200,7 +1199,7 @@ def idxmax(self) -> blocks.Label: block = block.slice(0, 1) return indexes.Index(block).to_pandas()[0] - @requires_index + @validations.requires_index def idxmin(self) -> blocks.Label: block = self._block.order_by( [ @@ -1314,7 +1313,7 @@ def sort_values( ) return Series(block) - @requires_index + @validations.requires_index def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: # TODO(tbergeron): Support level parameter once multi-index introduced. if na_position not in ["first", "last"]: @@ -1377,7 +1376,7 @@ def groupby( else: raise TypeError("You have to supply one of 'by' and 'level'") - @requires_index + @validations.requires_index def _groupby_level( self, level: int | str | typing.Sequence[int] | typing.Sequence[str], @@ -1518,11 +1517,11 @@ def combine( materialized_series = result_series._cached() return materialized_series - @requires_index + @validations.requires_index def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_prefix(prefix)) - @requires_index + @validations.requires_index def add_suffix(self, suffix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_suffix(suffix)) @@ -1574,7 +1573,7 @@ def filter( else: raise ValueError("Need to provide 'items', 'like', or 'regex'") - @requires_index + @validations.requires_index def reindex(self, index=None, *, validate: typing.Optional[bool] = None): if validate and not self.index.is_unique: raise ValueError("Original index must be unique to reindex") @@ -1603,7 +1602,7 @@ def reindex(self, index=None, *, validate: typing.Optional[bool] = None): )._block return Series(result_block) - @requires_index + @validations.requires_index def reindex_like(self, other: Series, *, validate: typing.Optional[bool] = None): return self.reindex(other.index, validate=validate) diff --git a/scripts/publish_api_coverage.py b/scripts/publish_api_coverage.py index 25fbfbf9884..0292d4880d3 100644 --- a/scripts/publish_api_coverage.py +++ b/scripts/publish_api_coverage.py @@ -116,7 +116,15 @@ def generate_pandas_api_coverage(): """Inspect all our pandas objects, and compare with the real pandas objects, to see which methods we implement. For each, generate a regex that can be used to check if its present in a notebook""" - header = ["api", "pattern", "kind", "is_in_bigframes", "missing_parameters"] + header = [ + "api", + "pattern", + "kind", + "is_in_bigframes", + "missing_parameters", + "requires_index", + "requires_ordering", + ] api_patterns = [] indexers = ["loc", "iloc", "iat", "ix", "at"] for name, pandas_obj, bigframes_obj in PANDAS_TARGETS: @@ -156,6 +164,13 @@ def generate_pandas_api_coverage(): token_type = "property" is_in_bigframes = hasattr(bigframes_obj, member) + requires_index = False + requires_ordering = False + + if is_in_bigframes: + attr = getattr(bigframes_obj, member) + requires_index = hasattr(attr, "_validations_requires_index") + requires_ordering = hasattr(attr, "_validations_requires_ordering") api_patterns.append( [ @@ -164,6 +179,8 @@ def generate_pandas_api_coverage(): token_type, is_in_bigframes, missing_parameters, + requires_index, + requires_ordering, ] ) @@ -287,6 +304,7 @@ def generate_api_coverage(df, api_prefix): dataframe_apis["missing_parameters"].str.len() != 0 ) & dataframe_apis["is_in_bigframes"] not_implemented = ~dataframe_apis["is_in_bigframes"] + dataframe_table = pd.DataFrame( { "API": format_api( @@ -295,12 +313,16 @@ def generate_api_coverage(df, api_prefix): api_prefix, ), "Implemented": "", + "Requires index": "", + "Requires ordering": "", "Missing parameters": dataframe_apis["missing_parameters"], } ) dataframe_table.loc[fully_implemented, "Implemented"] = "Y" dataframe_table.loc[partial_implemented, "Implemented"] = "P" dataframe_table.loc[not_implemented, "Implemented"] = "N" + dataframe_table.loc[dataframe_apis["requires_index"], "Requires index"] = "Y" + dataframe_table.loc[dataframe_apis["requires_ordering"], "Requires ordering"] = "Y" return dataframe_table From 7050038eeee258452860941aa6b01d6a8ae10c6f Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:41:18 -0700 Subject: [PATCH 762/921] feat: add ml.llm.Claude3TextGenerator model (#901) * feat: add ml.llm.Claude3TextGenerator model * add in toc.yml * fix mypy * add models --- bigframes/ml/llm.py | 233 ++++++++++++++++++++++++++++++ bigframes/ml/loader.py | 5 + docs/templates/toc.yml | 2 + tests/system/conftest.py | 10 ++ tests/system/small/ml/test_llm.py | 65 ++++++++- 5 files changed, 313 insertions(+), 2 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 45634423c68..35bcf0a33c5 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -61,6 +61,17 @@ _GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT, ) +_CLAUDE_3_SONNET_ENDPOINT = "claude-3-sonnet" +_CLAUDE_3_HAIKU_ENDPOINT = "claude-3-haiku" +_CLAUDE_3_5_SONNET_ENDPOINT = "claude-3-5-sonnet" +_CLAUDE_3_OPUS_ENDPOINT = "claude-3-opus" +_CLAUDE_3_ENDPOINTS = ( + _CLAUDE_3_SONNET_ENDPOINT, + _CLAUDE_3_HAIKU_ENDPOINT, + _CLAUDE_3_5_SONNET_ENDPOINT, + _CLAUDE_3_OPUS_ENDPOINT, +) + _ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" _ML_EMBED_TEXT_STATUS = "ml_embed_text_status" @@ -1020,3 +1031,225 @@ def to_gbq(self, model_name: str, replace: bool = False) -> GeminiTextGenerator: new_model = self._bqml_model.copy(model_name, replace) return new_model.session.read_gbq_model(model_name) + + +@log_adapter.class_logger +class Claude3TextGenerator(base.BaseEstimator): + """Claude3 text generator LLM model. + + Go to Google Cloud Console -> Vertex AI -> Model Garden page to enabe the models before use. Must have the Consumer Procurement Entitlement Manager Identity and Access Management (IAM) role to enable the models. + https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-partner-models#grant-permissions + + .. note:: + + This product or feature 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 + (https://cloud.google.com/products#product-launch-stages). + + + .. note:: + + The models only availabe in specific regions. Check https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions for details. + + Args: + model_name (str, Default to "claude-3-sonnet"): + The model for natural language tasks. Possible values are "claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet" and "claude-3-opus". + "claude-3-sonnet" is Anthropic's dependable combination of skills and speed. It is engineered to be dependable for scaled AI deployments across a variety of use cases. + "claude-3-haiku" is Anthropic's fastest, most compact vision and text model for near-instant responses to simple queries, meant for seamless AI experiences mimicking human interactions. + "claude-3-5-sonnet" is Anthropic's most powerful AI model and maintains the speed and cost of Claude 3 Sonnet, which is a mid-tier model. + "claude-3-opus" is Anthropic's second-most powerful AI model, with strong performance on highly complex tasks. + https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#available-claude-models + Default to "claude-3-sonnet". + session (bigframes.Session or None): + BQ session to create the model. If None, use the global default session. + connection_name (str or None): + Connection to connect with remote service. str of the format ... + If None, use default connection in session context. BigQuery DataFrame will try to create the connection and attach + permission if the connection isn't fully set up. + """ + + def __init__( + self, + *, + model_name: Literal[ + "claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus" + ] = "claude-3-sonnet", + session: Optional[bigframes.Session] = None, + connection_name: Optional[str] = None, + ): + self.model_name = model_name + self.session = session or bpd.get_global_session() + self._bq_connection_manager = self.session.bqconnectionmanager + + connection_name = connection_name or self.session._bq_connection + self.connection_name = clients.resolve_full_bq_connection_name( + connection_name, + default_project=self.session._project, + default_location=self.session._location, + ) + + self._bqml_model_factory = globals.bqml_model_factory() + self._bqml_model: core.BqmlModel = self._create_bqml_model() + + def _create_bqml_model(self): + # Parse and create connection if needed. + if not self.connection_name: + raise ValueError( + "Must provide connection_name, either in constructor or through session options." + ) + + if self._bq_connection_manager: + connection_name_parts = self.connection_name.split(".") + if len(connection_name_parts) != 3: + raise ValueError( + f"connection_name must be of the format .., got {self.connection_name}." + ) + self._bq_connection_manager.create_bq_connection( + project_id=connection_name_parts[0], + location=connection_name_parts[1], + connection_id=connection_name_parts[2], + iam_role="aiplatform.user", + ) + + if self.model_name not in _CLAUDE_3_ENDPOINTS: + raise ValueError( + f"Model name {self.model_name} is not supported. We only support {', '.join(_CLAUDE_3_ENDPOINTS)}." + ) + + options = { + "endpoint": self.model_name, + } + + return self._bqml_model_factory.create_remote_model( + session=self.session, connection_name=self.connection_name, options=options + ) + + @classmethod + def _from_bq( + cls, session: bigframes.Session, bq_model: bigquery.Model + ) -> Claude3TextGenerator: + assert bq_model.model_type == "MODEL_TYPE_UNSPECIFIED" + assert "remoteModelInfo" in bq_model._properties + assert "endpoint" in bq_model._properties["remoteModelInfo"] + assert "connection" in bq_model._properties["remoteModelInfo"] + + # Parse the remote model endpoint + bqml_endpoint = bq_model._properties["remoteModelInfo"]["endpoint"] + model_connection = bq_model._properties["remoteModelInfo"]["connection"] + model_endpoint = bqml_endpoint.split("/")[-1] + + kwargs = utils.retrieve_params_from_bq_model( + cls, bq_model, _BQML_PARAMS_MAPPING + ) + + model = cls( + **kwargs, + session=session, + model_name=model_endpoint, + connection_name=model_connection, + ) + model._bqml_model = core.BqmlModel(session, bq_model) + return model + + @property + def _bqml_options(self) -> dict: + """The model options as they will be set for BQML""" + options = { + "data_split_method": "NO_SPLIT", + } + return options + + def predict( + self, + X: Union[bpd.DataFrame, bpd.Series], + *, + max_output_tokens: int = 128, + top_k: int = 40, + top_p: float = 0.95, + ) -> bpd.DataFrame: + """Predict the result from input DataFrame. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + Input DataFrame or Series, which contains only one column of prompts. + Prompts can include preamble, questions, suggestions, instructions, or examples. + + max_output_tokens (int, default 128): + Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. + A token may be smaller than a word. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. + Default 128. Possible values are in the range [1, 4096]. + + top_k (int, default 40): + Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens + in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature). + For each token selection step, the top K tokens with the highest probabilities are sampled. Then tokens are further filtered based on topP with the final token selected using temperature sampling. + Specify a lower value for less random responses and a higher value for more random responses. + Default 40. Possible values [1, 40]. + + top_p (float, default 0.95):: + Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value. + For example, if tokens A, B, and C have a probability of 0.3, 0.2, and 0.1 and the top-p value is 0.5, then the model will select either A or B as the next token (using temperature) + and not consider C at all. + Specify a lower value for less random responses and a higher value for more random responses. + Default 0.95. Possible values [0.0, 1.0]. + + + Returns: + bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. + """ + + # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models + if max_output_tokens not in range(1, 4097): + raise ValueError( + f"max_output_token must be [1, 4096], but is {max_output_tokens}." + ) + + if top_k not in range(1, 41): + raise ValueError(f"top_k must be [1, 40], but is {top_k}.") + + if top_p < 0.0 or top_p > 1.0: + raise ValueError(f"top_p must be [0.0, 1.0], but is {top_p}.") + + (X,) = utils.convert_to_dataframe(X) + + if len(X.columns) != 1: + raise ValueError( + f"Only support one column as input. {constants.FEEDBACK_LINK}" + ) + + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "prompt"}) + + options = { + "max_output_tokens": max_output_tokens, + "top_k": top_k, + "top_p": top_p, + "flatten_json_output": True, + } + + df = self._bqml_model.generate_text(X, options) + + if (df[_ML_GENERATE_TEXT_STATUS] != "").any(): + warnings.warn( + f"Some predictions failed. Check column {_ML_GENERATE_TEXT_STATUS} for detailed status. You may want to filter the failed rows and retry.", + RuntimeWarning, + ) + + return df + + def to_gbq(self, model_name: str, replace: bool = False) -> Claude3TextGenerator: + """Save the model to BigQuery. + + Args: + model_name (str): + The name of the model. + replace (bool, default False): + Determine whether to replace if the model already exists. Default to False. + + Returns: + Claude3TextGenerator: Saved model.""" + + new_model = self._bqml_model.copy(model_name, replace) + return new_model.session.read_gbq_model(model_name) diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index bd01342152e..7d75f4c65a8 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -63,6 +63,10 @@ llm._GEMINI_PRO_ENDPOINT: llm.GeminiTextGenerator, llm._GEMINI_1P5_PRO_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, llm._GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, + llm._CLAUDE_3_HAIKU_ENDPOINT: llm.Claude3TextGenerator, + llm._CLAUDE_3_SONNET_ENDPOINT: llm.Claude3TextGenerator, + llm._CLAUDE_3_5_SONNET_ENDPOINT: llm.Claude3TextGenerator, + llm._CLAUDE_3_OPUS_ENDPOINT: llm.Claude3TextGenerator, llm._TEXT_EMBEDDING_004_ENDPOINT: llm.TextEmbeddingGenerator, llm._TEXT_MULTILINGUAL_EMBEDDING_002_ENDPOINT: llm.TextEmbeddingGenerator, } @@ -86,6 +90,7 @@ def from_bq( imported.XGBoostModel, llm.PaLM2TextGenerator, llm.PaLM2TextEmbeddingGenerator, + llm.Claude3TextGenerator, llm.TextEmbeddingGenerator, pipeline.Pipeline, compose.ColumnTransformer, diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 736ffba2861..bab4ad9aac5 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -157,6 +157,8 @@ uid: bigframes.ml.llm.PaLM2TextGenerator - name: PaLM2TextEmbeddingGenerator uid: bigframes.ml.llm.PaLM2TextEmbeddingGenerator + - name: Claude3TextGenerator + uid: bigframes.ml.llm.Claude3TextGenerator name: llm - items: - name: metrics diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 83c8baac394..05ff80dc330 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -145,6 +145,16 @@ def session() -> Generator[bigframes.Session, None, None]: session.close() # close generated session at cleanup time +@pytest.fixture(scope="session") +def session_us_east5() -> Generator[bigframes.Session, None, None]: + context = bigframes.BigQueryOptions( + location="us-east5", + ) + session = bigframes.Session(context=context) + yield session + session.close() # close generated session at cleanup time + + @pytest.fixture(scope="session") def session_load() -> Generator[bigframes.Session, None, None]: context = bigframes.BigQueryOptions(location="US", project="bigframes-load-testing") diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index c2f62096d0e..1647eb879f4 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -18,7 +18,7 @@ from tests.system import utils -def test_create_text_generator_model( +def test_create_load_text_generator_model( palm2_text_generator_model, dataset_id, bq_connection ): # Model creation doesn't return error @@ -34,7 +34,7 @@ def test_create_text_generator_model( assert reloaded_model.connection_name == bq_connection -def test_create_text_generator_32k_model( +def test_create_load_text_generator_32k_model( palm2_text_generator_32k_model, dataset_id, bq_connection ): # Model creation doesn't return error @@ -405,6 +405,67 @@ def test_gemini_text_generator_predict_with_params_success( assert all(series.str.len() > 20) +# TODO(garrettwu): add tests for claude3.5 sonnet and claude3 opus as they are only available in other regions. +@pytest.mark.parametrize( + "model_name", + ("claude-3-sonnet", "claude-3-haiku"), +) +def test_claude3_text_generator_create_load( + dataset_id, model_name, session, bq_connection +): + claude3_text_generator_model = llm.Claude3TextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + assert claude3_text_generator_model is not None + assert claude3_text_generator_model._bqml_model is not None + + # save, load to ensure configuration was kept + reloaded_model = claude3_text_generator_model.to_gbq( + f"{dataset_id}.temp_text_model", replace=True + ) + assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.connection_name == bq_connection + assert reloaded_model.model_name == model_name + + +@pytest.mark.parametrize( + "model_name", + ("claude-3-sonnet", "claude-3-haiku"), +) +@pytest.mark.flaky(retries=2) +def test_claude3_text_generator_predict_default_params_success( + llm_text_df, model_name, session, bq_connection +): + claude3_text_generator_model = llm.Claude3TextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + df = claude3_text_generator_model.predict(llm_text_df).to_pandas() + assert df.shape == (3, 3) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + +@pytest.mark.parametrize( + "model_name", + ("claude-3-sonnet", "claude-3-haiku"), +) +@pytest.mark.flaky(retries=2) +def test_claude3_text_generator_predict_with_params_success( + llm_text_df, model_name, session, bq_connection +): + claude3_text_generator_model = llm.Claude3TextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + df = claude3_text_generator_model.predict( + llm_text_df, max_output_tokens=100, top_k=20, top_p=0.5 + ).to_pandas() + assert df.shape == (3, 3) + assert "ml_generate_text_llm_result" in df.columns + series = df["ml_generate_text_llm_result"] + assert all(series.str.len() > 20) + + @pytest.mark.flaky(retries=2) def test_llm_palm_score(llm_fine_tune_df_default_index): model = llm.PaLM2TextGenerator(model_name="text-bison") From e43e0e53c10b2f7629f04de3e00204ad150e3337 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:44:14 -0700 Subject: [PATCH 763/921] chore(main): release 1.15.0 (#902) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 15 +++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754658c5e1b..e77062dfa13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.15.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.14.0...v1.15.0) (2024-08-20) + + +### Features + +* Add llm.TextEmbeddingGenerator to support new embedding models ([#905](https://github.com/googleapis/python-bigquery-dataframes/issues/905)) ([6bc6a41](https://github.com/googleapis/python-bigquery-dataframes/commit/6bc6a41426fbbb60e77cd77f80860f88a1751a4b)) +* Add ml.llm.Claude3TextGenerator model ([#901](https://github.com/googleapis/python-bigquery-dataframes/issues/901)) ([7050038](https://github.com/googleapis/python-bigquery-dataframes/commit/7050038eeee258452860941aa6b01d6a8ae10c6f)) + + +### Documentation + +* Add columns for "requires ordering/index" to supported APIs summary ([#892](https://github.com/googleapis/python-bigquery-dataframes/issues/892)) ([d2fc51a](https://github.com/googleapis/python-bigquery-dataframes/commit/d2fc51a30c4fff6fe0b98df61eec70ddb28b37ec)) +* Remove duplicate description for `kms_key_name` ([#898](https://github.com/googleapis/python-bigquery-dataframes/issues/898)) ([1053d56](https://github.com/googleapis/python-bigquery-dataframes/commit/1053d56260eef1cff6e7c419f6c86be8f7e74373)) +* Update embedding model notebooks ([#906](https://github.com/googleapis/python-bigquery-dataframes/issues/906)) ([d9b8ef5](https://github.com/googleapis/python-bigquery-dataframes/commit/d9b8ef56deb0c776edeeb0112bd9d35d5ed1b70e)) + ## [1.14.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.13.0...v1.14.0) (2024-08-14) diff --git a/bigframes/version.py b/bigframes/version.py index 2e135689ed5..f0f332d182f 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.14.0" +__version__ = "1.15.0" From d1883cc04ce5b2944d87a00c79b99a406001ba8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 21 Aug 2024 12:58:04 -0500 Subject: [PATCH 764/921] docs: use unstack() from BigQuery DataFrames instead of pandas in the PyPI sample notebook (#890) Now that ambiguous orderings are allowed, unstack() "just works". --- notebooks/dataframes/pypi.ipynb | 46 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/notebooks/dataframes/pypi.ipynb b/notebooks/dataframes/pypi.ipynb index 3022dc71737..3777e98d428 100644 --- a/notebooks/dataframes/pypi.ipynb +++ b/notebooks/dataframes/pypi.ipynb @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -58,7 +58,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -120,7 +120,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -167,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -185,7 +185,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -205,7 +205,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -229,7 +229,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -245,13 +245,13 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 5aa35b9c-459a-4b46-b70c-36e6418b61eb is DONE. 920.8 GB processed. Open Job" + "Query job 88c93524-d74f-4bbd-b6b1-0fb752ebedde is DONE. 28.6 GB processed. Open Job" ], "text/plain": [ "" @@ -266,37 +266,46 @@ "# unique key post-aggregation that is used for ordering. By aggregating over\n", "# a time series, the line plots will render in the expexted order.\n", "pandas_pypi = pandas_pypi.assign(date=pandas_pypi[\"timestamp\"].dt.date)\n", - "downloads_per_day = pandas_pypi.groupby([\"date\", \"project\"]).size()\n", - "\n", - "# Convert to a pandas DataFrame for further transformation and visualization.\n", - "pd_df = downloads_per_day.to_pandas()" + "downloads_per_day = pandas_pypi.groupby([\"date\", \"project\"]).size().unstack()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Once you've downloaded the time series with the `to_pandas()` method, you can use typical pandas methods to visualize the data." + "BigQuery DataFrames has several built-in visualization methods. Alternatively, download the time series with the `to_pandas()` method for further analysis and visualization." ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "metadata": {}, "outputs": [ + { + "data": { + "text/html": [ + "Query job 09bcc1a0-def3-474f-acd5-1d55b9653e07 is DONE. 28.6 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, { "data": { "text/plain": [ "" ] }, - "execution_count": 20, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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", + "image/png": "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LmdvvfWWgyq0vVOnTrGAgAD21FNPse+//54tXLiQxcbGsszMzKtuq9PpGGOMLVq0iPn5+bH8/HxHl2u1a+U1n8q8kl6vZy+88AILCwtjZ86ccXC11usqb0xMzDXzOsNrVmVlJevTpw9btmwZKywsZN9//z176KGHmFQqZV999VWn25pPUz/xxBPM19fXJV93rpd306ZNV93e/DobHBzMCgoKeKi4a9QAubndu3ez4cOHs7q6OsuxoqIiNmvWLKZUKi3r4BgMBnbixAn23HPPscDAQJdcH6e1tZXNnj2bzZo1i61atYolJiayJ5988ppNwfbt29mcOXNYnz59BJHX7M0333TJF13GTPNeJk+ezBYvXtzp+LBhw9iiRYu6/Jq33nqLcRznkj/jW8371Vdfsfvvv58FBwcLIu/Jkyed4jUrOzubDR48mJWUlFiOtbW1sWeffZbJZDL2448/MsYuzbtbv369yz4nGbu1vN9//z2bNWsWCwkJcbq8dArMzTU2NiInJwc6nc5yLCoqCm+++SZ0Oh2mT5+OPXv2IDw8HH379sWkSZOwePFiRERE8Fd0N8nlcqSmpsLDwwMPP/wwfH198emnnwIAlixZgqioqE63HzZsGIqLi/HSSy9d9TlXcKt5zefdn3nmGT7KtYny8nJ4e3tj5syZAACtVguZTIYJEyagrq7uqtsbjUaMGzcOhYWF1zw96MxuNW9KSgoyMjKQnp6O6OhoR5drtVvN26dPH0ycOJH316ympibk5+dbrugyGo1QKpV4/fXX0d7ejtmzZ+PYsWOW5+DMmTMxZcoU9OvXj7earXEreZOSknD69Gn885//RP/+/Xmu/Ap8d2DEviorK1lycjJ7/vnnWXNzc6fPZWRksKSkJPbll1/yVJ3ttbe3d1pbY926dZaREfPwuEajYdXV1Ywx178S6mbyarVaVltby1eJNmU0GtnmzZstH5t/fmvWrGEzZszodNvW1laH1mYPt5LX/Pvd1ZWAruJW8ra0tDi0tuvR6/Vs7NixbObMmZbRdnPtFy5cYGPHjmWrVq1iRqPR5V9zGLu5vC+//LLlueismfmfhk3sKjg4GKmpqdi1axe2bNkCtVpt+dzIkSNhMBicau0MaykUCnAcB4PBAAD461//irlz5+LgwYP497//jYKCAixbtgz33HMPtFqt80zG66abyfvcc8/h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x0ptg/K6d15AeJkpr/PndcWarSG0vNdDp8CciHl9DDPzkGRrays0Gg1ycnIwe/Zs9O3bF76+vvDz88O2bduQkZGBIUOG8Fh591Be984LCC8z5XX+vK5YszWElvdW0AiQE7h48SKAS505ABgMBkgkEpSWlmLgwIE4evQoJkyYgPz8fEydOhVhYWEIDAxEZmamyz1JKa975wWEl5nyOn9eV6zZGkLL2y2OnXNNrlRYWMi8vLzYwoULLcfM62OUlZUxf39/9thjjzGj0Wg5bp6R74q7QlNe987LmPAyU17nz+uKNVtDaHm7i0aAeHbq1CkolUqcOHECf/nLXwCYNgXUarXYvn07HnnkEXz44YfgOA5isbjT13Icx0fJVqG87p0XEF5myuv8eV2xZmsILW93UQPEM7lcjp49e2LatGnIyMjAokWLAAAymQz33nsv3n777Ws+QV3xiUp53TsvILzMlNf587pizdYQWt7ukvBdgNANGTIEw4cPx4IFCyCTyfDZZ59h6dKlaGpqQnJyMubPnw+pVMp3mTZDed07LyC8zJTX+fO6Ys3WEFrebuP7HJzQqVQqNnToUJadnc1UKhX76KOPmJ+fH+M4juXl5THGOu+Z4+oor3vnZUx4mSmv8+d1xZqtIbS83UWnwHik0+kgl8sRHByM1tZWeHh4YPfu3dDpdOjfvz8+/vhjALhqqNJVUV73zgsILzPldf68rlizNYSW1xp0CsxBKioqkJWVBa1Wi4iICAwbNswyBDl8+HAUFRXho48+wr59+/DDDz/gxIkTWLt2LSQSCd566y2eq791lNe98wLCy0x5nT+vK9ZsDaHltTm+h6CEIC8vj/Xr148lJyczf39/lpSUxL799lvL519++WXGcRyLjIxkx48fZ4wx1tDQwP73f/+XFRcX81V2t1Fe987LmPAyU17nz+uKNVtDaHntgRogOysqKmK9e/dmy5YtY42NjezYsWNszpw5bP78+Z124F28eDHLzMxkjLn2egyU173zMia8zJTX+fO6Ys3WEFpee6EGyI40Gg1bunQpmzFjBtNoNJbjn3zyCfPz82MXL17ksTrbo7wm7pqXMeFlprwmzpzXFWu2htDy2hPNAbIjo9GI3r17IzY2FjKZzLLT7m233QZPT0/odLouv+byfVtcCeV177yA8DJTXufP64o1W0Noee2JGiA7UigU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", "text/plain": [ "
" ] @@ -306,8 +315,7 @@ } ], "source": [ - "pd_timeseries = pd_df.unstack()\n", - "pd_timeseries.plot.line(rot=45, ylabel=\"daily downloads\", ylim=(0, 2e7))" + "downloads_per_day.plot.line(rot=45, ylabel=\"daily downloads\", ylim=(0, 2e7))" ] } ], From 303190331d3194562c5ed44fefc2c9fd1d73bedd Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:05:16 -0700 Subject: [PATCH 765/921] docs: create sample notebook to manipulate struct and array data (#883) * docs: create sample notebook to manipulate struct and array data * typo * address comments --- .../dataframes/struct_and_array_dtypes.ipynb | 656 ++++++++++++++++++ 1 file changed, 656 insertions(+) create mode 100644 notebooks/dataframes/struct_and_array_dtypes.ipynb diff --git a/notebooks/dataframes/struct_and_array_dtypes.ipynb b/notebooks/dataframes/struct_and_array_dtypes.ipynb new file mode 100644 index 00000000000..3bcdaf40f7b --- /dev/null +++ b/notebooks/dataframes/struct_and_array_dtypes.ipynb @@ -0,0 +1,656 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A Guide to Array and Struct Data Types in BigQuery DataFrames" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Set up your environment\n", + "\n", + "To get started, follow the instructions in the notebooks within the `getting_started` folder to set up your environment. Once your environment is ready, you can import the necessary packages by running the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "import bigframes.bigquery as bbq\n", + "import pyarrow as pa" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}\n", + "\n", + "bpd.options.display.progress_bar = None\n", + "bpd.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Array Data Types\n", + "\n", + "In BigQuery, an [array](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#array_type) (also called a repeated column) is an ordered list of zero or more elements of the same data type. Arrays cannot contain other arrays or `NULL` elements.\n", + "\n", + "BigQuery DataFrames map BigQuery array types to `pandas.ArrowDtype(pa.list_())`. The following code examples illustrate how to work with array columns in BigQuery DataFrames." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create DataFrames with array columns\n", + "\n", + "Create a DataFrame in BigQuery DataFrames from local sample data. Use a list of lists to create a column with the `list[pyarrow]` dtype, which corresponds to the `ARRAY` type in BigQuery." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameScores
0Alice[95 88 92]
1Bob[78 81]
2Charlie[ 82 89 94 100]
\n", + "

3 rows × 2 columns

\n", + "
[3 rows x 2 columns in total]" + ], + "text/plain": [ + " Name Scores\n", + "0 Alice [95 88 92]\n", + "1 Bob [78 81]\n", + "2 Charlie [ 82 89 94 100]\n", + "\n", + "[3 rows x 2 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({\n", + " 'Name': ['Alice', 'Bob', 'Charlie'],\n", + " 'Scores': [[95, 88, 92], [78, 81], [82, 89, 94, 100]],\n", + "})\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Name string[pyarrow]\n", + "Scores list[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Operate on array data\n", + "\n", + "While pandas offers vectorized operations and lambda expressions for array manipulation, BigQuery DataFrames leverages the computational power of BigQuery itself. You can access a variety of native BigQuery array operations, such as [`array_agg`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_array_agg) and [`array_length`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_array_length), through the [`bigframes.bigquery`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery) package (abbreviated as `bbq` in the following code samples)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 3\n", + "1 2\n", + "2 4\n", + "Name: Scores, dtype: Int64" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Find the length in each array.\n", + "bbq.array_length(df['Scores'])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 95\n", + "0 88\n", + "0 92\n", + "1 78\n", + "1 81\n", + "2 82\n", + "2 89\n", + "2 94\n", + "2 100\n", + "Name: Scores, dtype: Int64" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Transforms array elements into individual rows, preserving original order when in ordering\n", + "# mode. If an array has multiple elements, exploded rows are ordered by the element's index\n", + "# within its original array.\n", + "scores = df['Scores'].explode()\n", + "scores" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 100.0\n", + "0 93.0\n", + "0 97.0\n", + "1 83.0\n", + "1 86.0\n", + "2 87.0\n", + "2 94.0\n", + "2 99.0\n", + "2 105.0\n", + "Name: Scores, dtype: Float64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Adjust the scores.\n", + "adj_scores = scores + 5.0\n", + "adj_scores" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 [100. 93. 97.]\n", + "1 [83. 86.]\n", + "2 [ 87. 94. 99. 105.]\n", + "Name: Scores, dtype: list[pyarrow]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Aggregate adjusted scores back into arrays.\n", + "adj_scores_arr = bbq.array_agg(adj_scores.groupby(level=0))\n", + "adj_scores_arr" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameScoresNewScores
0Alice[95 88 92][100. 93. 97.]
1Bob[78 81][83. 86.]
2Charlie[ 82 89 94 100][ 87. 94. 99. 105.]
\n", + "

3 rows × 3 columns

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

3 rows × 2 columns

\n", + "
[3 rows x 2 columns in total]" + ], + "text/plain": [ + " Name Address\n", + "0 Alice {'City': 'New York', 'State': 'NY'}\n", + "1 Bob {'City': 'San Francisco', 'State': 'CA'}\n", + "2 Charlie {'City': 'Seattle', 'State': 'WA'}\n", + "\n", + "[3 rows x 2 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names = bpd.Series(['Alice', 'Bob', 'Charlie'])\n", + "address = bpd.Series(\n", + " [\n", + " {'City': 'New York', 'State': 'NY'},\n", + " {'City': 'San Francisco', 'State': 'CA'},\n", + " {'City': 'Seattle', 'State': 'WA'}\n", + " ],\n", + " dtype=bpd.ArrowDtype(pa.struct(\n", + " [('City', pa.string()), ('State', pa.string())]\n", + " )))\n", + "\n", + "df = bpd.DataFrame({'Name': names, 'Address': address})\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Name string[pyarrow]\n", + "Address struct[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Operate on struct data\n", + "\n", + "Similar to pandas, BigQuery DataFrames provides a [`StructAccessor`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.operations.structs.StructAccessor). Use the methods provided in this accessor to manipulate struct data." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "City string[pyarrow]\n", + "State string[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Return the dtype object of each child field of the struct.\n", + "df['Address'].struct.dtypes()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 New York\n", + "1 San Francisco\n", + "2 Seattle\n", + "Name: City, dtype: string" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Extract a child field as a Series\n", + "city = df['Address'].struct.field(\"City\")\n", + "city" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CityState
0New YorkNY
1San FranciscoCA
2SeattleWA
\n", + "

3 rows × 2 columns

\n", + "
[3 rows x 2 columns in total]" + ], + "text/plain": [ + " City State\n", + "0 New York NY\n", + "1 San Francisco CA\n", + "2 Seattle WA\n", + "\n", + "[3 rows x 2 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Extract all child fields of a struct as a DataFrame.\n", + "address_df = df['Address'].struct.explode()\n", + "address_df" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 64044e6da6810ae19f0b457c330dba7ec758e1b5 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:24:55 -0700 Subject: [PATCH 766/921] test: add claude models tests in us-east5 (#908) * test: add claude models tests in us-east5 * docs fix --- docs/templates/toc.yml | 2 ++ tests/system/small/ml/test_llm.py | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index bab4ad9aac5..47d9e97d7a8 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -157,6 +157,8 @@ uid: bigframes.ml.llm.PaLM2TextGenerator - name: PaLM2TextEmbeddingGenerator uid: bigframes.ml.llm.PaLM2TextEmbeddingGenerator + - name: TextEmbeddingGenerator + uid: bigframes.ml.llm.TextEmbeddingGenerator - name: Claude3TextGenerator uid: bigframes.ml.llm.Claude3TextGenerator name: llm diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 1647eb879f4..0c8a1956dbb 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -405,14 +405,15 @@ def test_gemini_text_generator_predict_with_params_success( assert all(series.str.len() > 20) -# TODO(garrettwu): add tests for claude3.5 sonnet and claude3 opus as they are only available in other regions. @pytest.mark.parametrize( "model_name", - ("claude-3-sonnet", "claude-3-haiku"), + ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), ) def test_claude3_text_generator_create_load( - dataset_id, model_name, session, bq_connection + dataset_id, model_name, session, session_us_east5, bq_connection ): + if model_name in ("claude-3-5-sonnet", "claude-3-opus"): + session = session_us_east5 claude3_text_generator_model = llm.Claude3TextGenerator( model_name=model_name, connection_name=bq_connection, session=session ) @@ -430,12 +431,14 @@ def test_claude3_text_generator_create_load( @pytest.mark.parametrize( "model_name", - ("claude-3-sonnet", "claude-3-haiku"), + ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), ) @pytest.mark.flaky(retries=2) def test_claude3_text_generator_predict_default_params_success( - llm_text_df, model_name, session, bq_connection + llm_text_df, model_name, session, session_us_east5, bq_connection ): + if model_name in ("claude-3-5-sonnet", "claude-3-opus"): + session = session_us_east5 claude3_text_generator_model = llm.Claude3TextGenerator( model_name=model_name, connection_name=bq_connection, session=session ) @@ -448,12 +451,14 @@ def test_claude3_text_generator_predict_default_params_success( @pytest.mark.parametrize( "model_name", - ("claude-3-sonnet", "claude-3-haiku"), + ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), ) @pytest.mark.flaky(retries=2) def test_claude3_text_generator_predict_with_params_success( - llm_text_df, model_name, session, bq_connection + llm_text_df, model_name, session, session_us_east5, bq_connection ): + if model_name in ("claude-3-5-sonnet", "claude-3-opus"): + session = session_us_east5 claude3_text_generator_model = llm.Claude3TextGenerator( model_name=model_name, connection_name=bq_connection, session=session ) From 149d5ff822da3d7fda18dbed4814e0406708cf07 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:03:56 -0700 Subject: [PATCH 767/921] fix: struct field non-nullable type issue. (#914) * fix: struct field non-nullable type issue. * update logic --- bigframes/core/compile/scalar_op_compiler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index e70c49e337e..4818d3ca766 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -754,7 +754,9 @@ def struct_field_op_impl(x: ibis_types.Value, op: ops.StructFieldOp): name = op.name_or_index else: name = struct_value.names[op.name_or_index] - return struct_value[name].name(name) + + result = struct_value[name] + return result.cast(result.type()(nullable=True)).name(name) def numeric_to_datetime(x: ibis_types.Value, unit: str) -> ibis_types.TimestampValue: From e837f6ea3299849ef046ddd55abf3db6bbe15f35 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:29:21 -0700 Subject: [PATCH 768/921] chore: update benchmark logic (#900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update benchmark logic * fix import * Update scripts/run_and_publish_benchmark.py --------- Co-authored-by: Tim Sweña (Swast) --- .kokoro/continuous/notebook.cfg | 5 + .kokoro/load/benchmark.cfg | 5 + bigframes/session/metrics.py | 30 +- noxfile.py | 166 ++------ .../db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py | 14 - .../groupby/G1_1e9_1e2_5_0/q10.py | 16 - .../db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py | 14 - .../db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py | 14 - .../db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py | 16 - .../db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py | 16 - .../db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py | 16 - .../db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py | 18 - .../db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py | 20 - .../db-benchmark/join/J1_1e9_NA_0_0/q1.py | 16 - .../db-benchmark/join/J1_1e9_NA_0_0/q2.py | 16 - .../db-benchmark/join/J1_1e9_NA_0_0/q3.py | 16 - .../db-benchmark/join/J1_1e9_NA_0_0/q4.py | 16 - .../db-benchmark/join/J1_1e9_NA_0_0/q5.py | 16 - .../db-benchmark/sort/J1_1e9_NA_0_0/q1.py | 15 - scripts/run_and_publish_benchmark.py | 371 +++++++++++++++++ tests/benchmark/__init__.py | 13 + .../benchmark/db_benchmark/sort/config.jsonl | 2 + .../benchmark/db_benchmark/sort/q1.py | 21 +- tests/benchmark/utils.py | 95 +++++ .../bigframes_vendored/db_benchmark/LICENSE | 373 ++++++++++++++++++ .../bigframes_vendored/db_benchmark/METADATA | 18 + .../bigframes_vendored/db_benchmark/README.md | 76 ++++ .../db_benchmark/__init__.py | 0 .../db_benchmark/sort_queries.py | 16 + 29 files changed, 1044 insertions(+), 386 deletions(-) delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py delete mode 100644 scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py delete mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py delete mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py delete mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py delete mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py delete mode 100644 scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py delete mode 100644 scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py create mode 100644 scripts/run_and_publish_benchmark.py create mode 100644 tests/benchmark/__init__.py create mode 100644 tests/benchmark/db_benchmark/sort/config.jsonl rename scripts/benchmark/simple_benchmark.py => tests/benchmark/db_benchmark/sort/q1.py (58%) create mode 100644 tests/benchmark/utils.py create mode 100644 third_party/bigframes_vendored/db_benchmark/LICENSE create mode 100644 third_party/bigframes_vendored/db_benchmark/METADATA create mode 100644 third_party/bigframes_vendored/db_benchmark/README.md create mode 100644 third_party/bigframes_vendored/db_benchmark/__init__.py create mode 100644 third_party/bigframes_vendored/db_benchmark/sort_queries.py diff --git a/.kokoro/continuous/notebook.cfg b/.kokoro/continuous/notebook.cfg index 94e2a3c686e..ca3d98b58be 100644 --- a/.kokoro/continuous/notebook.cfg +++ b/.kokoro/continuous/notebook.cfg @@ -6,6 +6,11 @@ env_vars: { value: "notebook" } +env_vars: { + key: "BENCHMARK_AND_PUBLISH" + value: "true" +} + env_vars: { key: "GOOGLE_CLOUD_PROJECT" value: "bigframes-load-testing" diff --git a/.kokoro/load/benchmark.cfg b/.kokoro/load/benchmark.cfg index a489e05bbc7..bc2d7a26559 100644 --- a/.kokoro/load/benchmark.cfg +++ b/.kokoro/load/benchmark.cfg @@ -6,6 +6,11 @@ env_vars: { value: "benchmark" } +env_vars: { + key: "BENCHMARK_AND_PUBLISH" + value: "true" +} + env_vars: { key: "GOOGLE_CLOUD_PROJECT" value: "bigframes-load-testing" diff --git a/bigframes/session/metrics.py b/bigframes/session/metrics.py index 85a7f6aa4b0..579cac1ac3e 100644 --- a/bigframes/session/metrics.py +++ b/bigframes/session/metrics.py @@ -33,16 +33,18 @@ class ExecutionMetrics: def count_job_stats(self, query_job: bq_job.QueryJob): stats = get_performance_stats(query_job) if stats is not None: - bytes_processed, slot_millis = stats + bytes_processed, slot_millis, exec_seconds = stats self.execution_count += 1 self.bytes_processed += bytes_processed self.slot_millis += slot_millis if LOGGING_NAME_ENV_VAR in os.environ: # when running notebooks via pytest nbmake - write_stats_to_disk(bytes_processed, slot_millis) + write_stats_to_disk(bytes_processed, slot_millis, exec_seconds) -def get_performance_stats(query_job: bigquery.QueryJob) -> Optional[Tuple[int, int]]: +def get_performance_stats( + query_job: bigquery.QueryJob, +) -> Optional[Tuple[int, int, float]]: """Parse the query job for performance stats. Return None if the stats do not reflect real work done in bigquery. @@ -57,14 +59,21 @@ def get_performance_stats(query_job: bigquery.QueryJob) -> Optional[Tuple[int, i slot_millis = query_job.slot_millis if not isinstance(slot_millis, int): return None # filter out mocks + if query_job.configuration.dry_run: # dry run stats are just predictions of the real run slot_millis = 0 - return bytes_processed, slot_millis + exec_seconds = ( + (query_job.ended - query_job.created).total_seconds() + if query_job.created is not None and query_job.ended is not None + else None + ) + + return bytes_processed, slot_millis, exec_seconds -def write_stats_to_disk(bytes_processed: int, slot_millis: int): +def write_stats_to_disk(bytes_processed: int, slot_millis: int, exec_seconds: float): """For pytest runs only, log information about the query job to a file in order to create a performance report. """ @@ -83,6 +92,13 @@ def write_stats_to_disk(bytes_processed: int, slot_millis: int): f.write(str(bytes_processed) + "\n") # store slot milliseconds - bytes_file = os.path.join(current_directory, test_name + ".slotmillis") - with open(bytes_file, "a") as f: + slot_file = os.path.join(current_directory, test_name + ".slotmillis") + with open(slot_file, "a") as f: f.write(str(slot_millis) + "\n") + + # store execution time seconds + exec_time_file = os.path.join( + current_directory, test_name + ".bq_exec_time_seconds" + ) + with open(exec_time_file, "a") as f: + f.write(str(exec_seconds) + "\n") diff --git a/noxfile.py b/noxfile.py index c464b47270f..9ed85290faf 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,10 +16,8 @@ from __future__ import absolute_import -from multiprocessing import Process import os import pathlib -from pathlib import Path import re import shutil from typing import Dict, List @@ -42,7 +40,6 @@ "third_party", "noxfile.py", "setup.py", - os.path.join("scripts", "benchmark"), ] DEFAULT_PYTHON_VERSION = "3.10" @@ -686,7 +683,7 @@ def notebook(session: nox.Session): "seaborn", ) - notebooks_list = list(Path("notebooks/").glob("*/*.ipynb")) + notebooks_list = list(pathlib.Path("notebooks/").glob("*/*.ipynb")) denylist = [ # Regionalized testing is manually added later. @@ -698,7 +695,7 @@ def notebook(session: nox.Session): # With the notebooks_fill_params.py script, we are able to find and # replace the PROJECT_ID parameter, but not the others. # - # TODO(ashleyxu): Test these notebooks by replacing parameters with + # TODO(b/357904266): Test these notebooks by replacing parameters with # appropriate values and omitting cleanup logic that may break # our test infrastructure. "notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb", # Needs DATASET. @@ -748,17 +745,6 @@ def notebook(session: nox.Session): for nb in notebooks + list(notebooks_reg): assert os.path.exists(nb), nb - # TODO(shobs): For some reason --retries arg masks exceptions occurred in - # notebook failures, and shows unhelpful INTERNALERROR. Investigate that - # and enable retries if we can find a way to surface the real exception - # bacause the notebook is running against real GCP and something may fail - # due to transient issues. - pytest_command = [ - "py.test", - "--nbmake", - "--nbmake-timeout=900", # 15 minutes - ] - try: # Populate notebook parameters and make a backup so that the notebooks # are runnable. @@ -767,22 +753,23 @@ def notebook(session: nox.Session): CURRENT_DIRECTORY / "scripts" / "notebooks_fill_params.py", *notebooks, ) - - # Run notebooks in parallel session.run's, since each notebook - # takes an environment variable for performance logging - processes = [] for notebook in notebooks: - process = Process( - target=session.run, - args=(*pytest_command, notebook), - kwargs={"env": {LOGGING_NAME_ENV_VAR: os.path.basename(notebook)}}, + session.run( + "python", + "scripts/run_and_publish_benchmark.py", + "--notebook", + f"--benchmark-path={notebook}", ) - process.start() - processes.append(process) - - for process in processes: - process.join() + for notebook, regions in notebooks_reg.items(): + for region in regions: + session.run( + "python", + "scripts/run_and_publish_benchmark.py", + "--notebook", + f"--benchmark-path={notebook}", + f"--region={region}", + ) finally: # Prevent our notebook changes from getting checked in to git # accidentally. @@ -791,116 +778,37 @@ def notebook(session: nox.Session): CURRENT_DIRECTORY / "scripts" / "notebooks_restore_from_backup.py", *notebooks, ) - - # Additionally run regionalized notebooks in parallel session.run's. - # Each notebook takes a different region via env param. - processes = [] - for notebook, regions in notebooks_reg.items(): - for region in regions: - process = Process( - target=session.run, - args=(*pytest_command, notebook), - kwargs={ - "env": { - "BIGQUERY_LOCATION": region, - LOGGING_NAME_ENV_VAR: os.path.basename(notebook), - } - }, - ) - process.start() - processes.append(process) - - for process in processes: - process.join() - - # when the environment variable is set as it is above, - # notebooks output a .bytesprocessed and .slotmillis report - # collect those reports and print a summary - _print_performance_report("notebooks/") + session.run( + "python", + "scripts/run_and_publish_benchmark.py", + "--notebook", + "--publish-benchmarks=notebooks/", + ) @nox.session(python=DEFAULT_PYTHON_VERSION) def benchmark(session: nox.Session): session.install("-e", ".[all]") - base_path = os.path.join("scripts", "benchmark") - - benchmark_script_list = list(Path(base_path).rglob("*.py")) - # Run benchmarks in parallel session.run's, since each benchmark - # takes an environment variable for performance logging - processes = [] - for benchmark in benchmark_script_list: - process = Process( - target=session.run, - args=("python", benchmark), - kwargs={"env": {LOGGING_NAME_ENV_VAR: benchmark.as_posix()}}, - ) - process.start() - processes.append(process) - - for process in processes: - process.join() - - # when the environment variable is set as it is above, - # notebooks output a .bytesprocessed and .slotmillis report - # collect those reports and print a summary - _print_performance_report(base_path) + base_path = os.path.join("tests", "benchmark") + benchmark_script_list = list(pathlib.Path(base_path).rglob("*.py")) -def _print_performance_report(path: str): - """Add an informational report about http queries, bytes - processed, and slot time to the testlog output for purposes - of measuring bigquery-related performance changes. - - Looks specifically for output files in subfolders of the - passed path. (*/*.bytesprocessed and */*.slotmillis) - """ - print("---BIGQUERY USAGE REPORT---") - results_dict = {} - bytes_reports = sorted(Path(path).rglob("*.bytesprocessed")) - for bytes_report in bytes_reports: - with open(bytes_report, "r") as bytes_file: - filename = bytes_report.relative_to(path).with_suffix("") - lines = bytes_file.read().splitlines() - query_count = len(lines) - total_bytes = sum([int(line) for line in lines]) - results_dict[filename] = [query_count, total_bytes] - os.remove(bytes_report) - - millis_reports = sorted(Path(path).rglob("*.slotmillis")) - for millis_report in millis_reports: - with open(millis_report, "r") as millis_file: - filename = millis_report.relative_to(path).with_suffix("") - lines = millis_file.read().splitlines() - total_slot_millis = sum([int(line) for line in lines]) - results_dict[filename] += [total_slot_millis] - os.remove(millis_report) - - cumulative_queries = 0 - cumulative_bytes = 0 - cumulative_slot_millis = 0 - for name, results in results_dict.items(): - if len(results) != 3: - raise IOError( - "Mismatch in performance logging output. " - "Expected one .bytesprocessed and one .slotmillis " - "file for each notebook." + try: + for benchmark in benchmark_script_list: + if benchmark.name in ("__init__.py", "utils.py"): + continue + session.run( + "python", + "scripts/run_and_publish_benchmark.py", + f"--benchmark-path={benchmark}", ) - query_count, total_bytes, total_slot_millis = results - cumulative_queries += query_count - cumulative_bytes += total_bytes - cumulative_slot_millis += total_slot_millis - print( - f"{name} - query count: {query_count}," - f" bytes processed sum: {total_bytes}," - f" slot millis sum: {total_slot_millis}" + finally: + session.run( + "python", + "scripts/run_and_publish_benchmark.py", + f"--publish-benchmarks={base_path}", ) - print( - f"---total queries: {cumulative_queries}, " - f"total bytes: {cumulative_bytes}, " - f"total slot millis: {cumulative_slot_millis}---" - ) - @nox.session(python="3.10") def release_dry_run(session): diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py deleted file mode 100644 index cc5f77b49b4..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q1.py +++ /dev/null @@ -1,14 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 1: sum v1 by id1") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = x.groupby("id1", as_index=False, dropna=False).agg({"v1": "sum"}) -print(ans.shape) -chk = [ans["v1"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py deleted file mode 100644 index 83d5d4ee147..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q10.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 10: sum v3 count by id1:id6") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = x.groupby( - ["id1", "id2", "id3", "id4", "id5", "id6"], as_index=False, dropna=False -).agg({"v3": "sum", "v1": "size"}) -print(ans.shape) -chk = [ans["v3"].sum(), ans["v1"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py deleted file mode 100644 index 734a17242b8..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q2.py +++ /dev/null @@ -1,14 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 2: sum v1 by id1:id2") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = x.groupby(["id1", "id2"], as_index=False, dropna=False).agg({"v1": "sum"}) -print(ans.shape) -chk = [ans["v1"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py deleted file mode 100644 index 242902de64d..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q3.py +++ /dev/null @@ -1,14 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 3: sum v1 mean v3 by id3") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = x.groupby("id3", as_index=False, dropna=False).agg({"v1": "sum", "v3": "mean"}) -print(ans.shape) -chk = [ans["v1"].sum(), ans["v3"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py deleted file mode 100644 index e4f769545e5..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q4.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 4: mean v1:v3 by id4") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = x.groupby("id4", as_index=False, dropna=False).agg( - {"v1": "mean", "v2": "mean", "v3": "mean"} -) -print(ans.shape) -chk = [ans["v1"].sum(), ans["v2"].sum(), ans["v3"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py deleted file mode 100644 index d34a6c055f1..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q5.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 5: sum v1:v3 by id6") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = x.groupby("id6", as_index=False, dropna=False).agg( - {"v1": "sum", "v2": "sum", "v3": "sum"} -) -print(ans.shape) -chk = [ans["v1"].sum(), ans["v2"].sum(), ans["v3"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py deleted file mode 100644 index 0f3240a1298..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q6.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 6: median v3 sd v3 by id4 id5") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = x.groupby(["id4", "id5"], as_index=False, dropna=False).agg( - {"v3": ["median", "std"]} -) -print(ans.shape) -chk = [ans["v3"]["median"].sum(), ans["v3"]["std"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py deleted file mode 100644 index 78e1e94b853..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q7.py +++ /dev/null @@ -1,18 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 7: max v1 - min v2 by id3") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = ( - x.groupby("id3", as_index=False, dropna=False) - .agg({"v1": "max", "v2": "min"}) - .assign(range_v1_v2=lambda x: x["v1"] - x["v2"])[["id3", "range_v1_v2"]] -) -print(ans.shape) -chk = [ans["range_v1_v2"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py b/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py deleted file mode 100644 index 7a57d03efed..00000000000 --- a/scripts/benchmark/db-benchmark/groupby/G1_1e9_1e2_5_0/q8.py +++ /dev/null @@ -1,20 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py - -import bigframes.pandas as bpd - -print("Groupby benchmark 8: largest two v3 by id6") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.G1_1e9_1e2_5_0") - -ans = ( - x[~x["v3"].isna()][["id6", "v3"]] - .sort_values("v3", ascending=False) - .groupby("id6", as_index=False, dropna=False) - .head(2) -) -ans = ans.reset_index(drop=True) -print(ans.shape) -chk = [ans["v3"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py deleted file mode 100644 index 429dc72ad0a..00000000000 --- a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q1.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py - -import bigframes.pandas as bpd - -print("Join benchmark 1: small inner on int") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") -small = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e3_0_0") - -ans = x.merge(small, on="id1") -print(ans.shape) - -chk = [ans["v1"].sum(), ans["v2"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py deleted file mode 100644 index 210c29f844f..00000000000 --- a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q2.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py - -import bigframes.pandas as bpd - -print("Join benchmark 2: medium inner on int") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") -medium = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e6_0_0") - -ans = x.merge(medium, on="id2") -print(ans.shape) - -chk = [ans["v1"].sum(), ans["v2"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py deleted file mode 100644 index d88d9436044..00000000000 --- a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q3.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py - -import bigframes.pandas as bpd - -print("Join benchmark 3: medium outer on int") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") -medium = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e6_0_0") - -ans = x.merge(medium, how="left", on="id2") -print(ans.shape) - -chk = [ans["v1"].sum(), ans["v2"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py deleted file mode 100644 index 9167043d9aa..00000000000 --- a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q4.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py - -import bigframes.pandas as bpd - -print("Join benchmark 4: medium inner on factor") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") -medium = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e6_0_0") - -ans = x.merge(medium, on="id5") -print(ans.shape) - -chk = [ans["v1"].sum(), ans["v2"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py b/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py deleted file mode 100644 index 39eb23ac457..00000000000 --- a/scripts/benchmark/db-benchmark/join/J1_1e9_NA_0_0/q5.py +++ /dev/null @@ -1,16 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py - -import bigframes.pandas as bpd - -print("Join benchmark 5: big inner on int") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") -big = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_1e9_0_0") - -ans = x.merge(big, on="id3") -print(ans.shape) - -chk = [ans["v1"].sum(), ans["v2"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py b/scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py deleted file mode 100644 index 45cac7b5439..00000000000 --- a/scripts/benchmark/db-benchmark/sort/J1_1e9_NA_0_0/q1.py +++ /dev/null @@ -1,15 +0,0 @@ -# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/sort-pandas.py - -import bigframes.pandas as bpd - -print("Sort benchmark 1: sort by int id2") - -x = bpd.read_gbq("bigframes-dev-perf.dbbenchmark.J1_1e9_NA_0_0") - -ans = x.sort_values("id2") -print(ans.shape) - -chk = [ans["v1"].sum()] -print(chk) - -bpd.reset_session() diff --git a/scripts/run_and_publish_benchmark.py b/scripts/run_and_publish_benchmark.py new file mode 100644 index 00000000000..675db39493e --- /dev/null +++ b/scripts/run_and_publish_benchmark.py @@ -0,0 +1,371 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import datetime +import json +import os +import pathlib +import subprocess +import sys +from typing import Dict, List, Union + +import numpy as np +import pandas as pd +import pandas_gbq + +LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" +CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() + + +def run_benchmark_subprocess(args, log_env_name_var, filename=None, region=None): + """ + Runs a benchmark subprocess with configured environment variables. Adjusts PYTHONPATH, + sets region-specific BigQuery location, and logs environment variables. + + This function terminates the benchmark session if the subprocess exits with an error, + due to `check=True` in subprocess.run, which raises CalledProcessError on non-zero + exit status. + """ + env = os.environ.copy() + current_pythonpath = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = ( + os.path.join(os.getcwd(), "tests") + os.pathsep + current_pythonpath + ) + + if region: + env["BIGQUERY_LOCATION"] = region + env[LOGGING_NAME_ENV_VAR] = log_env_name_var + subprocess.run(args, env=env, check=True) + + +def collect_benchmark_result(benchmark_path: str) -> pd.DataFrame: + """Generate a DataFrame report on HTTP queries, bytes processed, slot time and execution time from log files.""" + path = pathlib.Path(benchmark_path) + try: + results_dict: Dict[str, List[Union[int, float, None]]] = {} + bytes_files = sorted(path.rglob("*.bytesprocessed")) + millis_files = sorted(path.rglob("*.slotmillis")) + bq_seconds_files = sorted(path.rglob("*.bq_exec_time_seconds")) + + local_seconds_files = sorted(path.rglob("*.local_exec_time_seconds")) + has_local_seconds = len(local_seconds_files) > 0 + + if has_local_seconds: + if not ( + len(bytes_files) + == len(millis_files) + == len(local_seconds_files) + == len(bq_seconds_files) + ): + raise ValueError( + "Mismatch in the number of report files for bytes, millis, and seconds." + ) + else: + if not (len(bytes_files) == len(millis_files) == len(bq_seconds_files)): + raise ValueError( + "Mismatch in the number of report files for bytes, millis, and seconds." + ) + + for idx in range(len(bytes_files)): + bytes_file = bytes_files[idx] + millis_file = millis_files[idx] + bq_seconds_file = bq_seconds_files[idx] + filename = bytes_file.relative_to(path).with_suffix("") + + if filename != millis_file.relative_to(path).with_suffix( + "" + ) or filename != bq_seconds_file.relative_to(path).with_suffix(""): + raise ValueError( + "File name mismatch among bytes, millis, and seconds reports." + ) + + if has_local_seconds: + local_seconds_file = local_seconds_files[idx] + if filename != local_seconds_file.relative_to(path).with_suffix(""): + raise ValueError( + "File name mismatch among bytes, millis, and seconds reports." + ) + + with open(bytes_file, "r") as file: + lines = file.read().splitlines() + query_count = len(lines) + total_bytes = sum(int(line) for line in lines) + + with open(millis_file, "r") as file: + lines = file.read().splitlines() + total_slot_millis = sum(int(line) for line in lines) + + if has_local_seconds: + # 'local_seconds' captures the total execution time for a benchmark as it + # starts timing immediately before the benchmark code begins and stops + # immediately after it ends. Unlike other metrics that might accumulate + # values proportional to the number of queries executed, 'local_seconds' is + # a singular measure of the time taken for the complete execution of the + # benchmark, from start to finish. + with open(local_seconds_file, "r") as file: + local_seconds = float(file.readline().strip()) + else: + local_seconds = None + + with open(bq_seconds_file, "r") as file: + lines = file.read().splitlines() + bq_seconds = sum(float(line) for line in lines) + + results_dict[str(filename)] = [ + query_count, + total_bytes, + total_slot_millis, + local_seconds, + bq_seconds, + ] + finally: + for files_to_remove in ( + path.rglob("*.bytesprocessed"), + path.rglob("*.slotmillis"), + path.rglob("*.local_exec_time_seconds"), + path.rglob("*.bq_exec_time_seconds"), + ): + for log_file in files_to_remove: + log_file.unlink() + + columns = [ + "Query_Count", + "Bytes_Processed", + "Slot_Millis", + "Local_Execution_Time_Sec", + "BigQuery_Execution_Time_Sec", + ] + + benchmark_metrics = pd.DataFrame.from_dict( + results_dict, + orient="index", + columns=columns, + ) + + print("---BIGQUERY USAGE REPORT---") + for index, row in benchmark_metrics.iterrows(): + print( + f"{index} - query count: {row['Query_Count']}," + f" bytes processed sum: {row['Bytes_Processed']}," + f" slot millis sum: {row['Slot_Millis']}," + f" local execution time: {round(row['Local_Execution_Time_Sec'], 1)} seconds," + f" bigquery execution time: {round(row['BigQuery_Execution_Time_Sec'], 1)} seconds" + ) + + geometric_mean_queries = geometric_mean(benchmark_metrics["Query_Count"]) + geometric_mean_bytes = geometric_mean(benchmark_metrics["Bytes_Processed"]) + geometric_mean_slot_millis = geometric_mean(benchmark_metrics["Slot_Millis"]) + geometric_mean_local_seconds = geometric_mean( + benchmark_metrics["Local_Execution_Time_Sec"] + ) + geometric_mean_bq_seconds = geometric_mean( + benchmark_metrics["BigQuery_Execution_Time_Sec"] + ) + + print( + f"---Geometric mean of queries: {geometric_mean_queries}, " + f"Geometric mean of bytes processed: {geometric_mean_bytes}, " + f"Geometric mean of slot millis: {geometric_mean_slot_millis}, " + f"Geometric mean of local execution time: {geometric_mean_local_seconds} seconds, " + f"Geometric mean of BigQuery execution time: {geometric_mean_bq_seconds} seconds---" + ) + + return benchmark_metrics.reset_index().rename(columns={"index": "Benchmark_Name"}) + + +def geometric_mean(data): + """ + Calculate the geometric mean of a dataset, rounding the result to one decimal place. + Returns NaN if the dataset is empty or contains only NaN values. + """ + data = data.dropna() + if len(data) == 0: + return np.nan + log_data = np.log(data) + return round(np.exp(log_data.mean()), 1) + + +def get_repository_status(): + current_directory = os.getcwd() + subprocess.run( + ["git", "config", "--global", "--add", "safe.directory", current_directory], + check=True, + ) + + git_hash = subprocess.check_output( + ["git", "rev-parse", "--short", "HEAD"], text=True + ).strip() + bigframes_version = subprocess.check_output( + ["python", "-c", "import bigframes; print(bigframes.__version__)"], text=True + ).strip() + release_version = ( + f"{bigframes_version}dev{datetime.datetime.now().strftime('%Y%m%d')}+{git_hash}" + ) + + return { + "benchmark_start_time": datetime.datetime.now().isoformat(), + "git_hash": git_hash, + "bigframes_version": bigframes_version, + "release_version": release_version, + "python_version": sys.version, + } + + +def find_config(start_path): + """ + Searches for a 'config.jsonl' file starting from the given path and moving up to parent + directories. + + This function ascends from the initial directory specified by `start_path` up to 3 + levels or until it reaches a directory named 'benchmark'. The search moves upwards + because if there are multiple 'config.jsonl' files in the path hierarchy, the closest + configuration to the starting directory (the lowest level) is expected to take effect. + It checks each directory for the presence of 'config.jsonl'. If found, it returns the + path to the configuration file. If not found within the limit or upon reaching + the 'benchmark' directory, it returns None. + """ + target_file = "config.jsonl" + current_path = pathlib.Path(start_path).resolve() + if current_path.is_file(): + current_path = current_path.parent + + levels_checked = 0 + while current_path.name != "benchmark" and levels_checked < 3: + config_path = current_path / target_file + if config_path.exists(): + return config_path + if current_path.parent == current_path: + break + current_path = current_path.parent + levels_checked += 1 + + return None + + +def run_benchmark_from_config(benchmark: str): + print(benchmark) + config_path = find_config(benchmark) + + if config_path: + benchmark_configs = [] + with open(config_path, "r") as f: + for line in f: + config = json.loads(line) + python_args = [f"--{key}={value}" for key, value in config.items()] + suffix = ( + config["benchmark_suffix"] + if "benchmark_suffix" in config + else "_".join(f"{key}_{value}" for key, value in config.items()) + ) + benchmark_configs.append((suffix, python_args)) + else: + benchmark_configs = [(None, [])] + + for benchmark_config in benchmark_configs: + args = ["python", str(benchmark)] + args.extend(benchmark_config[1]) + log_env_name_var = str(benchmark) + if benchmark_config[0] is not None: + log_env_name_var += f"_{benchmark_config[0]}" + run_benchmark_subprocess(args=args, log_env_name_var=log_env_name_var) + + +def run_notebook_benchmark(benchmark_file: str, region: str): + export_file = f"{benchmark_file}_{region}" if region else benchmark_file + log_env_name_var = os.path.basename(export_file) + # TODO(shobs): For some reason --retries arg masks exceptions occurred in + # notebook failures, and shows unhelpful INTERNALERROR. Investigate that + # and enable retries if we can find a way to surface the real exception + # bacause the notebook is running against real GCP and something may fail + # due to transient issues. + pytest_command = [ + "py.test", + "--nbmake", + "--nbmake-timeout=900", # 15 minutes + ] + benchmark_args = (*pytest_command, benchmark_file) + + run_benchmark_subprocess( + args=benchmark_args, + log_env_name_var=log_env_name_var, + filename=export_file, + region=region, + ) + + +def parse_arguments(): + parser = argparse.ArgumentParser( + description="Run benchmarks for different scenarios." + ) + parser.add_argument( + "--notebook", + action="store_true", + help="Set this flag to run the benchmark as a notebook. If not set, it assumes a Python (.py) file.", + ) + + parser.add_argument( + "--benchmark-path", + type=str, + default=None, + help="Specify the file path to the benchmark script, either a Jupyter notebook or a Python script.", + ) + + parser.add_argument( + "--region", + type=str, + default=None, + help="Specify the region where the benchmark will be executed or where the data resides. This parameter is optional.", + ) + + parser.add_argument( + "--publish-benchmarks", + type=str, + default=None, + help="Set the benchmarks to be published to BigQuery.", + ) + + return parser.parse_args() + + +def main(): + args = parse_arguments() + + if args.publish_benchmarks: + bigquery_table = ( + "bigframes-metrics.benchmark_report.notebook_benchmark" + if args.notebook + else "bigframes-metrics.benchmark_report.benchmark" + ) + benchmark_metrics = collect_benchmark_result(args.publish_benchmarks) + + if os.getenv("BENCHMARK_AND_PUBLISH", "false") == "true": + repo_status = get_repository_status() + for idx, col in enumerate(repo_status.keys()): + benchmark_metrics.insert(idx, col, repo_status[col]) + + pandas_gbq.to_gbq( + dataframe=benchmark_metrics, + destination_table=bigquery_table, + if_exists="append", + ) + print("Results have been successfully uploaded to BigQuery.") + elif args.notebook: + run_notebook_benchmark(args.benchmark_path, args.region) + else: + run_benchmark_from_config(args.benchmark_path) + + +if __name__ == "__main__": + main() diff --git a/tests/benchmark/__init__.py b/tests/benchmark/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/tests/benchmark/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/benchmark/db_benchmark/sort/config.jsonl b/tests/benchmark/db_benchmark/sort/config.jsonl new file mode 100644 index 00000000000..72884d6c5ab --- /dev/null +++ b/tests/benchmark/db_benchmark/sort/config.jsonl @@ -0,0 +1,2 @@ +{"benchmark_suffix": "50g_ordered", "table_id": "J1_1e9_NA_0_0", "ordered": true} +{"benchmark_suffix": "50g_unordered", "table_id": "J1_1e9_NA_0_0", "ordered": false} diff --git a/scripts/benchmark/simple_benchmark.py b/tests/benchmark/db_benchmark/sort/q1.py similarity index 58% rename from scripts/benchmark/simple_benchmark.py rename to tests/benchmark/db_benchmark/sort/q1.py index 53b35c52adc..f17a8431923 100644 --- a/scripts/benchmark/simple_benchmark.py +++ b/tests/benchmark/db_benchmark/sort/q1.py @@ -12,16 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import bigframes.pandas as bpd +import pathlib -# This is a placeholder benchmark. -# TODO(340278185): Add more data analysis tasks and benchmark files -# like this one. +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.sort_queries as vendored_dbbenchmark_sort_queries -print("Performing simple benchmark.") -df = bpd.DataFrame() -df["column_1"] = bpd.Series([i for i in range(100000)]) -df["column_2"] = bpd.Series([i * 2 for i in range(100000)]) -df["column_3"] = df["column_1"] + df["column_2"] -df.__repr__() -bpd.reset_session() +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_sort_queries.q1, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/utils.py b/tests/benchmark/utils.py new file mode 100644 index 00000000000..c286d4e2292 --- /dev/null +++ b/tests/benchmark/utils.py @@ -0,0 +1,95 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 argparse +import time + +import bigframes + + +def get_dbbenchmark_configuration(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--table_id", + type=str, + required=True, + help="The BigQuery table ID to query.", + ) + parser.add_argument( + "--ordered", + type=str, + help="Set to True (default) to have an ordered session, or False for an unordered session.", + ) + parser.add_argument( + "--benchmark_suffix", + type=str, + help="Suffix to append to benchmark names for identification purposes.", + ) + args = parser.parse_args() + session = _initialize_session(_str_to_bool(args.ordered)) + return args.table_id, session, args.benchmark_suffix + + +def get_tpch_configuration(): + parser = argparse.ArgumentParser(description="Process TPC-H Query using BigFrames.") + parser.add_argument( + "--dataset_id", + type=str, + required=True, + help="The BigQuery dataset ID to query.", + ) + parser.add_argument( + "--ordered", + type=str, + help="Set to True (default) to have an ordered session, or False for an unordered session.", + ) + parser.add_argument( + "--benchmark_suffix", + type=str, + help="Suffix to append to benchmark names for identification purposes.", + ) + + args = parser.parse_args() + session = _initialize_session(_str_to_bool(args.ordered)) + return args.dataset_id, session, args.benchmark_suffix + + +def get_execution_time(func, current_path, suffix, *args, **kwargs): + start_time = time.perf_counter() + func(*args, **kwargs) + end_time = time.perf_counter() + runtime = end_time - start_time + + clock_time_file_path = f"{current_path}_{suffix}.local_exec_time_seconds" + + with open(clock_time_file_path, "w") as log_file: + log_file.write(f"{runtime}\n") + + +def _str_to_bool(value): + if value == "True": + return True + elif value == "False": + return False + else: + raise argparse.ArgumentTypeError('Only "True" or "False" expected.') + + +def _initialize_session(ordered: bool): + context = bigframes.BigQueryOptions( + location="US", ordering_mode="strict" if ordered else "partial" + ) + session = bigframes.Session(context=context) + print(f"Initialized {'ordered' if ordered else 'unordered'} session.") + return session diff --git a/third_party/bigframes_vendored/db_benchmark/LICENSE b/third_party/bigframes_vendored/db_benchmark/LICENSE new file mode 100644 index 00000000000..a612ad9813b --- /dev/null +++ b/third_party/bigframes_vendored/db_benchmark/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/third_party/bigframes_vendored/db_benchmark/METADATA b/third_party/bigframes_vendored/db_benchmark/METADATA new file mode 100644 index 00000000000..6163ac69b79 --- /dev/null +++ b/third_party/bigframes_vendored/db_benchmark/METADATA @@ -0,0 +1,18 @@ +name: "db-benchmark" +description: + "This repository contains a reproducible benchmarking suite for evaluating " + "database-like operations in single-node environments. It assesses " + "scalability across varying data volumes and complexities." + +third_party { + identifier { + type: "Git" + value: "https://github.com/h2oai/db-benchmark" + primary_source: true + version: "Latest Commit on Main Branch as of Access" + } + version: "Latest Commit on Main Branch as of Access" + last_upgrade_date { year: 2024 month: 7 day: 12 } + license_type: RECIPROCAL + local_modifications: "Modified the queries to test and benchmark the BigFrames project" +} diff --git a/third_party/bigframes_vendored/db_benchmark/README.md b/third_party/bigframes_vendored/db_benchmark/README.md new file mode 100644 index 00000000000..aba227b0ebf --- /dev/null +++ b/third_party/bigframes_vendored/db_benchmark/README.md @@ -0,0 +1,76 @@ +Repository for reproducible benchmarking of database-like operations in single-node environment. +Benchmark report is available at [h2oai.github.io/db-benchmark](https://h2oai.github.io/db-benchmark). +We focused mainly on portability and reproducibility. Benchmark is routinely re-run to present up-to-date timings. Most of solutions used are automatically upgraded to their stable or development versions. +This benchmark is meant to compare scalability both in data volume and data complexity. +Contribution and feedback are very welcome! + +# Tasks + + - [x] groupby + - [x] join + - [x] groupby2014 + +# Solutions + + - [x] [dask](https://github.com/dask/dask) + - [x] [data.table](https://github.com/Rdatatable/data.table) + - [x] [dplyr](https://github.com/tidyverse/dplyr) + - [x] [DataFrames.jl](https://github.com/JuliaData/DataFrames.jl) + - [x] [pandas](https://github.com/pandas-dev/pandas) + - [x] [(py)datatable](https://github.com/h2oai/datatable) + - [x] [spark](https://github.com/apache/spark) + - [x] [cuDF](https://github.com/rapidsai/cudf) + - [x] [ClickHouse](https://github.com/yandex/ClickHouse) + - [x] [Polars](https://github.com/ritchie46/polars) + - [x] [Arrow](https://github.com/apache/arrow) + - [x] [DuckDB](https://github.com/duckdb/duckdb) + +More solutions has been proposed. Status of those can be tracked in issues tracker of our project repository by using [_new solution_](https://github.com/h2oai/db-benchmark/issues?q=is%3Aissue+is%3Aopen+label%3A%22new+solution%22) label. + +# Reproduce + +## Batch benchmark run + +- edit `path.env` and set `julia` and `java` paths +- if solution uses python create new `virtualenv` as `$solution/py-$solution`, example for `pandas` use `virtualenv pandas/py-pandas --python=/usr/bin/python3.6` +- install every solution, follow `$solution/setup-$solution.sh` scripts +- edit `run.conf` to define solutions and tasks to benchmark +- generate data, for `groupby` use `Rscript _data/groupby-datagen.R 1e7 1e2 0 0` to create `G1_1e7_1e2_0_0.csv`, re-save to binary format where needed (see below), create `data` directory and keep all data files there +- edit `_control/data.csv` to define data sizes to benchmark using `active` flag +- ensure SWAP is disabled and ClickHouse server is not yet running +- start benchmark with `./run.sh` + +## Single solution benchmark + +- install solution software + - for python we recommend to use `virtualenv` for better isolation + - for R ensure that library is installed in a solution subdirectory, so that `library("dplyr", lib.loc="./dplyr/r-dplyr")` or `library("data.table", lib.loc="./datatable/r-datatable")` works + - note that some solutions may require another to be installed to speed-up csv data load, for example, `dplyr` requires `data.table` and similarly `pandas` requires (py)`datatable` +- generate data using `_data/*-datagen.R` scripts, for example, `Rscript _data/groupby-datagen.R 1e7 1e2 0 0` creates `G1_1e7_1e2_0_0.csv`, put data files in `data` directory +- run benchmark for a single solution using `./_launcher/solution.R --solution=data.table --task=groupby --nrow=1e7` +- run other data cases by passing extra parameters `--k=1e2 --na=0 --sort=0` +- use `--quiet=true` to suppress script's output and print timings only, using `--print=question,run,time_sec` specify columns to be printed to console, to print all use `--print=*` +- use `--out=time.csv` to write timings to a file rather than console + +## Running script interactively + +- install software in expected location, details above +- ensure data name to be used in env var below is present in `./data` dir +- source python virtual environment if needed +- call `SRC_DATANAME=G1_1e7_1e2_0_0 R`, if desired replace `R` with `python` or `julia` +- proceed pasting code from benchmark script + +## Extra care needed + +- `cudf` uses `conda` instead of `virtualenv` + +# Example environment + +- setting up r3-8xlarge: 244GB RAM, 32 cores: [Amazon EC2 for beginners](https://github.com/Rdatatable/data.table/wiki/Amazon-EC2-for-beginners) +- (slightly outdated) full reproduce script on clean Ubuntu 16.04: [_utils/repro.sh](https://github.com/h2oai/db-benchmark/blob/master/_utils/repro.sh) + +# Acknowledgment + +Timings for some solutions might be missing for particular data sizes or questions. Some functions are not yet implemented in all solutions so we were unable to answer all questions in all solutions. Some solutions might also run out of memory when running benchmark script which results the process to be killed by OS. Lastly we also added timeout for single benchmark script to run, once timeout value is reached script is terminated. +Please check [_exceptions_](https://github.com/h2oai/db-benchmark/issues?q=is%3Aissue+is%3Aopen+label%3Aexceptions) label in our repository for a list of issues/defects in solutions, that makes us unable to provide all timings. +There is also [_no documentation_](https://github.com/h2oai/db-benchmark/labels/no%20documentation) label that lists issues that are blocked by missing documentation in solutions we are benchmarking. diff --git a/third_party/bigframes_vendored/db_benchmark/__init__.py b/third_party/bigframes_vendored/db_benchmark/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/db_benchmark/sort_queries.py b/third_party/bigframes_vendored/db_benchmark/sort_queries.py new file mode 100644 index 00000000000..600df103cf8 --- /dev/null +++ b/third_party/bigframes_vendored/db_benchmark/sort_queries.py @@ -0,0 +1,16 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/sort-pandas.py + +import bigframes +import bigframes.session + + +def q1(table_id: str, session: bigframes.Session) -> None: + print("Sort benchmark 1: sort by int id2") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.sort_values("id2") + print(ans.shape) + + chk = [ans["v1"].sum()] + print(chk) From 575a29e77d50d60d7e9a84ebb87abcdb993adef1 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 22 Aug 2024 23:56:12 -0700 Subject: [PATCH 769/921] feat: implement `bigframes.bigquery.json_extract_array` (#910) * feat: implement `bigframes.bigquery.json_extract_array` This id needed to implement support for array return types in remote functions. * actually return, make tests pass * add negative test case --- bigframes/bigquery/__init__.py | 32 ++++++++++++++++++++ bigframes/core/compile/scalar_op_compiler.py | 12 ++++++++ bigframes/operations/__init__.py | 17 +++++++++++ tests/system/small/bigquery/test_json.py | 25 +++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index fb9503dc725..bec7b5ff0ea 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -239,6 +239,38 @@ def json_extract( return series._apply_unary_op(ops.JSONExtract(json_path=json_path)) +def json_extract_array( + series: series.Series, + json_path: str = "$", +) -> series.Series: + """Extracts a JSON array and converts it to a SQL array of JSON-formatted `STRING` or `JSON` + values. This function uses single quotes and brackets to escape invalid JSONPath + characters in JSON keys. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['[1, 2, 3]', '[4, 5]']) + >>> bbq.json_extract_array(s) + 0 ['1' '2' '3'] + 1 ['4' '5'] + dtype: list[pyarrow] + + Args: + series (bigframes.series.Series): + The Series containing JSON data (as native JSON objects or JSON-formatted strings). + json_path (str): + The JSON path identifying the data that you want to obtain from the input. + + Returns: + bigframes.series.Series: A new Series with the JSON or JSON-formatted STRING. + """ + return series._apply_unary_op(ops.JSONExtractArray(json_path=json_path)) + + # Search functions defined from # https://cloud.google.com/bigquery/docs/reference/standard-sql/search_functions diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 4818d3ca766..86501214ad7 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -947,6 +947,11 @@ def json_extract_op_impl(x: ibis_types.Value, op: ops.JSONExtract): return json_extract(json_obj=x, json_path=op.json_path) +@scalar_op_compiler.register_unary_op(ops.JSONExtractArray, pass_op=True) +def json_extract_array_op_impl(x: ibis_types.Value, op: ops.JSONExtractArray): + return json_extract_array(json_obj=x, json_path=op.json_path) + + ### Binary Ops def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None): """Wraps a binary operator to generate nulls of the expected type if either input is a null scalar.""" @@ -1581,6 +1586,13 @@ def json_extract( """Extracts a JSON value and converts it to a SQL JSON-formatted STRING or JSON value.""" +@ibis.udf.scalar.builtin(name="json_extract_array") +def json_extract_array( + json_obj: ibis_dtypes.JSON, json_path: ibis_dtypes.str +) -> ibis_dtypes.Array[ibis_dtypes.String]: + """Extracts a JSON array and converts it to a SQL ARRAY of JSON-formatted STRINGs or JSON values.""" + + @ibis.udf.scalar.builtin(name="ML.DISTANCE") def vector_distance(vector1, vector2, type: str) -> ibis_dtypes.Float64: """Computes the distance between two vectors using specified type ("EUCLIDEAN", "MANHATTAN", or "COSINE")""" diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index fb333d7a53b..cd9e70819e6 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -652,6 +652,23 @@ def output_type(self, *input_types): return input_type +@dataclasses.dataclass(frozen=True) +class JSONExtractArray(UnaryOp): + name: typing.ClassVar[str] = "json_extract_array" + json_path: str + + def output_type(self, *input_types): + input_type = input_types[0] + if not dtypes.is_json_like(input_type): + raise TypeError( + "Input type must be an valid JSON object or JSON-formatted string type." + + f" Received type: {input_type}" + ) + return pd.ArrowDtype( + pa.list_(dtypes.bigframes_dtype_to_arrow_dtype(dtypes.STRING_DTYPE)) + ) + + # Binary Ops fillna_op = create_binary_op(name="fillna", type_signature=op_typing.COERCE) maximum_op = create_binary_op(name="maximum", type_signature=op_typing.COERCE) diff --git a/tests/system/small/bigquery/test_json.py b/tests/system/small/bigquery/test_json.py index 18ccadd9f5e..68356f4a159 100644 --- a/tests/system/small/bigquery/test_json.py +++ b/tests/system/small/bigquery/test_json.py @@ -139,3 +139,28 @@ def test_json_extract_from_string(): def test_json_extract_w_invalid_series_type(): with pytest.raises(TypeError): bbq.json_extract(bpd.Series([1, 2]), "$.a") + + +def test_json_extract_array_from_json_strings(): + s = bpd.Series(['{"a": [1, 2, 3]}', '{"a": []}', '{"a": [4,5]}']) + actual = bbq.json_extract_array(s, "$.a") + expected = bpd.Series([["1", "2", "3"], [], ["4", "5"]]) + pd.testing.assert_series_equal( + actual.to_pandas(), + expected.to_pandas(), + ) + + +def test_json_extract_array_from_array_strings(): + s = bpd.Series(["[1, 2, 3]", "[]", "[4,5]"]) + actual = bbq.json_extract_array(s) + expected = bpd.Series([["1", "2", "3"], [], ["4", "5"]]) + pd.testing.assert_series_equal( + actual.to_pandas(), + expected.to_pandas(), + ) + + +def test_json_extract_array_w_invalid_series_type(): + with pytest.raises(TypeError): + bbq.json_extract_array(bpd.Series([1, 2])) From ad2f75ecbc3660459814716eec7d1f88d1188942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 23 Aug 2024 11:03:48 -0500 Subject: [PATCH 770/921] feat: add `DataFrame.struct.explode` to add struct subfields to a DataFrame (#916) * feat: add `DataFrame.struct.explode` to add struct subfields to a DataFrame * add tests for multiple columns and custom separator * fix system test --- bigframes/core/explode.py | 36 +++++++++++++ bigframes/dataframe.py | 16 +++--- bigframes/operations/structs.py | 23 ++++++++ tests/data/nested.jsonl | 2 +- tests/data/nested_schema.json | 28 ++++++++++ tests/system/small/operations/test_struct.py | 40 ++++++++++++++ tests/system/small/test_dataframe.py | 16 ++++++ .../pandas/core/arrays/arrow/accessors.py | 53 +++++++++++++++++++ 8 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 bigframes/core/explode.py create mode 100644 tests/system/small/operations/test_struct.py diff --git a/bigframes/core/explode.py b/bigframes/core/explode.py new file mode 100644 index 00000000000..142536a931c --- /dev/null +++ b/bigframes/core/explode.py @@ -0,0 +1,36 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for implementing 'explode' functions.""" + +from typing import cast, Sequence, Union + +import bigframes.core.blocks as blocks +import bigframes.core.utils as utils + + +def check_column( + column: Union[blocks.Label, Sequence[blocks.Label]], +) -> Sequence[blocks.Label]: + if not utils.is_list_like(column): + column_labels = cast(Sequence[blocks.Label], (column,)) + else: + column_labels = cast(Sequence[blocks.Label], tuple(column)) + + if not column_labels: + raise ValueError("column must be nonempty") + if len(column_labels) > len(set(column_labels)): + raise ValueError("column must be unique") + + return column_labels diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a174ef0b0f6..cb9c9041213 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -55,6 +55,7 @@ import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.convert +import bigframes.core.explode import bigframes.core.expression as ex import bigframes.core.groupby as groupby import bigframes.core.guid @@ -71,6 +72,7 @@ import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.operations.plotting as plotting +import bigframes.operations.structs import bigframes.series import bigframes.series as bf_series import bigframes.session._io.bigquery @@ -2875,15 +2877,7 @@ def explode( *, ignore_index: Optional[bool] = False, ) -> DataFrame: - if not utils.is_list_like(column): - column_labels = typing.cast(typing.Sequence[blocks.Label], (column,)) - else: - column_labels = typing.cast(typing.Sequence[blocks.Label], tuple(column)) - - if not column_labels: - raise ValueError("column must be nonempty") - if len(column_labels) > len(set(column_labels)): - raise ValueError("column must be unique") + column_labels = bigframes.core.explode.check_column(column) column_ids = [self._resolve_label_exact(label) for label in column_labels] missing = [ @@ -3751,6 +3745,10 @@ def __matmul__(self, other) -> DataFrame: __matmul__.__doc__ = inspect.getdoc(vendored_pandas_frame.DataFrame.__matmul__) + @property + def struct(self): + return bigframes.operations.structs.StructFrameAccessor(self) + def _throw_if_null_index(self, opname: str): if not self._has_index: raise bigframes.exceptions.NullIndexError( diff --git a/bigframes/operations/structs.py b/bigframes/operations/structs.py index d222f0993b6..051023c2994 100644 --- a/bigframes/operations/structs.py +++ b/bigframes/operations/structs.py @@ -57,3 +57,26 @@ def dtypes(self) -> pd.Series: ], index=[pa_type.field(i).name for i in range(pa_type.num_fields)], ) + + +@log_adapter.class_logger +class StructFrameAccessor(vendoracessors.StructFrameAccessor): + __doc__ = vendoracessors.StructAccessor.__doc__ + + def __init__(self, data: bigframes.dataframe.DataFrame) -> None: + self._parent = data + + def explode(self, column, *, separator: str = ".") -> bigframes.dataframe.DataFrame: + df = self._parent + column_labels = bigframes.core.explode.check_column(column) + + for label in column_labels: + position = df.columns.to_list().index(label) + df = df.drop(columns=label) + subfields = self._parent[label].struct.explode() + for subfield in reversed(subfields.columns): + df.insert( + position, f"{label}{separator}{subfield}", subfields[subfield] + ) + + return df diff --git a/tests/data/nested.jsonl b/tests/data/nested.jsonl index a71e9b1db1a..751ad0df78b 100644 --- a/tests/data/nested.jsonl +++ b/tests/data/nested.jsonl @@ -1,4 +1,4 @@ -{"rowindex":0,"customer_id":"jkl","day":"2023-12-18","flag":1,"event_sequence":[{"category":"B","timestamp":"2023-12-18 03:43:58","data":[{"key":"x","value":20.2533015856},{"key":"y","value":42.8363462389}]},{"category":"D","timestamp":"2023-12-18 07:15:37","data":[{"key":"x","value":62.0762664928},{"key":"z","value":83.6655402432}]}]} +{"rowindex":0,"customer_id":"jkl","day":"2023-12-18","flag":1,"label":{"key": "my-key","value":"my-value"},"event_sequence":[{"category":"B","timestamp":"2023-12-18 03:43:58","data":[{"key":"x","value":20.2533015856},{"key":"y","value":42.8363462389}]},{"category":"D","timestamp":"2023-12-18 07:15:37","data":[{"key":"x","value":62.0762664928},{"key":"z","value":83.6655402432}]}],"address":{"street":"123 Test Lane","city":"Testerchon"}} {"rowindex":1,"customer_id":"def","day":"2023-12-18","flag":2,"event_sequence":[{"category":"D","timestamp":"2023-12-18 23:11:11","data":[{"key":"w","value":36.1388065179}]},{"category":"B","timestamp":"2023-12-18 07:12:50","data":[{"key":"z","value":68.7673488304}]},{"category":"D","timestamp":"2023-12-18 09:09:03","data":[{"key":"x","value":57.4139647019}]},{"category":"C","timestamp":"2023-12-18 13:05:30","data":[{"key":"z","value":36.087871201}]}]} {"rowindex":2,"customer_id":"abc","day":"2023-12-6","flag":0,"event_sequence":[{"category":"C","timestamp":"2023-12-06 10:37:11","data":[]},{"category":"A","timestamp":"2023-12-06 03:35:44","data":[]},{"category":"D","timestamp":"2023-12-06 13:10:57","data":[{"key":"z","value":21.8487807658}]},{"category":"B","timestamp":"2023-12-06 01:39:16","data":[{"key":"y","value":1.6380505139}]}]} {"rowindex":3,"customer_id":"mno","day":"2023-12-16","flag":2,"event_sequence":[]} diff --git a/tests/data/nested_schema.json b/tests/data/nested_schema.json index c3fa39b36d2..2b843bb395d 100644 --- a/tests/data/nested_schema.json +++ b/tests/data/nested_schema.json @@ -19,6 +19,20 @@ "name": "flag", "type": "INTEGER" }, + { + "fields": [ + { + "name": "key", + "type": "STRING" + }, + { + "name": "value", + "type": "STRING" + } + ], + "name": "label", + "type": "RECORD" + }, { "fields": [ { @@ -52,5 +66,19 @@ "mode": "REPEATED", "name": "event_sequence", "type": "RECORD" + }, + { + "fields": [ + { + "name": "street", + "type": "STRING" + }, + { + "name": "city", + "type": "STRING" + } + ], + "name": "address", + "type": "RECORD" } ] diff --git a/tests/system/small/operations/test_struct.py b/tests/system/small/operations/test_struct.py new file mode 100644 index 00000000000..ddb65248d08 --- /dev/null +++ b/tests/system/small/operations/test_struct.py @@ -0,0 +1,40 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_dataframe_struct_explode_multiple_columns(nested_df): + got = nested_df.struct.explode(["label", "address"]) + assert got.columns.to_list() == [ + "customer_id", + "day", + "flag", + "label.key", + "label.value", + "event_sequence", + "address.street", + "address.city", + ] + + +def test_dataframe_struct_explode_separator(nested_df): + got = nested_df.struct.explode("label", separator="__sep__") + assert got.columns.to_list() == [ + "customer_id", + "day", + "flag", + "label__sep__key", + "label__sep__value", + "event_sequence", + "address", + ] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e1644c20b40..ddcf044911a 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1436,6 +1436,14 @@ def test_get_dtypes_array_struct_table(nested_df): "customer_id": pd.StringDtype(storage="pyarrow"), "day": pd.ArrowDtype(pa.date32()), "flag": pd.Int64Dtype(), + "label": pd.ArrowDtype( + pa.struct( + [ + ("key", pa.string()), + ("value", pa.string()), + ] + ), + ), "event_sequence": pd.ArrowDtype( pa.list_( pa.struct( @@ -1457,6 +1465,14 @@ def test_get_dtypes_array_struct_table(nested_df): ), ), ), + "address": pd.ArrowDtype( + pa.struct( + [ + ("street", pa.string()), + ("city", pa.string()), + ] + ), + ), } ), ) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py index bd6e50d0961..4cba928bb6d 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py @@ -121,3 +121,56 @@ def dtypes(self): A *pandas* Series with the data type of all child fields. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +class StructFrameAccessor: + """ + Accessor object for structured data properties of the DataFrame values. + """ + + def explode(self, column, *, separator: str = "."): + """ + Extract all child fields of struct column(s) and add to the DataFrame. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pyarrow as pa + >>> bpd.options.display.progress_bar = None + >>> countries = bpd.Series(["cn", "es", "us"]) + >>> files = bpd.Series( + ... [ + ... {"version": 1, "project": "pandas"}, + ... {"version": 2, "project": "pandas"}, + ... {"version": 1, "project": "numpy"}, + ... ], + ... dtype=bpd.ArrowDtype(pa.struct( + ... [("version", pa.int64()), ("project", pa.string())] + ... )) + ... ) + >>> downloads = bpd.Series([100, 200, 300]) + >>> df = bpd.DataFrame({"country": countries, "file": files, "download_count": downloads}) + >>> df.struct.explode("file") + country file.version file.project download_count + 0 cn 1 pandas 100 + 1 es 2 pandas 200 + 2 us 1 numpy 300 + + [3 rows x 4 columns] + + Args: + column: + Column(s) to explode. For multiple columns, specify a non-empty + list with each element be str or tuple, and all specified + columns their list-like data on same row of the frame must + have matching length. + separator: + Separator/delimiter to use to separate the original column name + from the sub-field column name. + + + Returns: + DataFrame: + Original DataFrame with exploded struct column(s). + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 361d89af536807a71dc614d4140b2e70fde210de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 23 Aug 2024 11:35:23 -0500 Subject: [PATCH 771/921] chore: remove accidental `typing.py` (#915) --- bigframes/typing.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 bigframes/typing.py diff --git a/bigframes/typing.py b/bigframes/typing.py deleted file mode 100644 index e69de29bb2d..00000000000 From 514be7b53580b3a0da41f3ad081a6fa084775433 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:31:26 -0700 Subject: [PATCH 772/921] chore: fix benchmark metric calculation. (#922) * chore: benchmark fix. * chore: benchmark fix. --- scripts/run_and_publish_benchmark.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/run_and_publish_benchmark.py b/scripts/run_and_publish_benchmark.py index 675db39493e..70ef8d91033 100644 --- a/scripts/run_and_publish_benchmark.py +++ b/scripts/run_and_publish_benchmark.py @@ -156,11 +156,16 @@ def collect_benchmark_result(benchmark_path: str) -> pd.DataFrame: print("---BIGQUERY USAGE REPORT---") for index, row in benchmark_metrics.iterrows(): + formatted_local_exec_time = ( + f"{round(row['Local_Execution_Time_Sec'], 1)} seconds" + if not np.isnan(row["Local_Execution_Time_Sec"]) + else "N/A" + ) print( f"{index} - query count: {row['Query_Count']}," f" bytes processed sum: {row['Bytes_Processed']}," f" slot millis sum: {row['Slot_Millis']}," - f" local execution time: {round(row['Local_Execution_Time_Sec'], 1)} seconds," + f" local execution time: {formatted_local_exec_time} seconds," f" bigquery execution time: {round(row['BigQuery_Execution_Time_Sec'], 1)} seconds" ) From 99e3fabac17e2e39144681ee66a77c3c14354fdd Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:51:09 -0700 Subject: [PATCH 773/921] chore: update db_benchmark groupby queries. (#920) --- .../db_benchmark/groupby/config.jsonl | 2 + tests/benchmark/db_benchmark/groupby/q1.py | 26 ++++ tests/benchmark/db_benchmark/groupby/q10.py | 30 +++++ tests/benchmark/db_benchmark/groupby/q2.py | 26 ++++ tests/benchmark/db_benchmark/groupby/q3.py | 26 ++++ tests/benchmark/db_benchmark/groupby/q4.py | 26 ++++ tests/benchmark/db_benchmark/groupby/q5.py | 26 ++++ tests/benchmark/db_benchmark/groupby/q6.py | 26 ++++ tests/benchmark/db_benchmark/groupby/q7.py | 26 ++++ tests/benchmark/db_benchmark/groupby/q8.py | 26 ++++ .../db_benchmark/groupby_queries.py | 123 ++++++++++++++++++ 11 files changed, 363 insertions(+) create mode 100644 tests/benchmark/db_benchmark/groupby/config.jsonl create mode 100644 tests/benchmark/db_benchmark/groupby/q1.py create mode 100644 tests/benchmark/db_benchmark/groupby/q10.py create mode 100644 tests/benchmark/db_benchmark/groupby/q2.py create mode 100644 tests/benchmark/db_benchmark/groupby/q3.py create mode 100644 tests/benchmark/db_benchmark/groupby/q4.py create mode 100644 tests/benchmark/db_benchmark/groupby/q5.py create mode 100644 tests/benchmark/db_benchmark/groupby/q6.py create mode 100644 tests/benchmark/db_benchmark/groupby/q7.py create mode 100644 tests/benchmark/db_benchmark/groupby/q8.py create mode 100644 third_party/bigframes_vendored/db_benchmark/groupby_queries.py diff --git a/tests/benchmark/db_benchmark/groupby/config.jsonl b/tests/benchmark/db_benchmark/groupby/config.jsonl new file mode 100644 index 00000000000..dd881e76ac9 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/config.jsonl @@ -0,0 +1,2 @@ +{"benchmark_suffix": "50g_ordered", "table_id": "G1_1e9_1e2_5_0", "ordered": true} +{"benchmark_suffix": "50g_unordered", "table_id": "G1_1e9_1e2_5_0", "ordered": false} diff --git a/tests/benchmark/db_benchmark/groupby/q1.py b/tests/benchmark/db_benchmark/groupby/q1.py new file mode 100644 index 00000000000..1ea64991701 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q1.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q1, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/groupby/q10.py b/tests/benchmark/db_benchmark/groupby/q10.py new file mode 100644 index 00000000000..c3bde06d08d --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q10.py @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q10, + current_path, + suffix, + table_id, + session, + ) diff --git a/tests/benchmark/db_benchmark/groupby/q2.py b/tests/benchmark/db_benchmark/groupby/q2.py new file mode 100644 index 00000000000..a4dad4dbfa3 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q2.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q2, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/groupby/q3.py b/tests/benchmark/db_benchmark/groupby/q3.py new file mode 100644 index 00000000000..2eca13313da --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q3.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q3, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/groupby/q4.py b/tests/benchmark/db_benchmark/groupby/q4.py new file mode 100644 index 00000000000..7db1286a373 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q4.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q4, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/groupby/q5.py b/tests/benchmark/db_benchmark/groupby/q5.py new file mode 100644 index 00000000000..869d091e713 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q5.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q5, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/groupby/q6.py b/tests/benchmark/db_benchmark/groupby/q6.py new file mode 100644 index 00000000000..c81746a3946 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q6.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q6, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/groupby/q7.py b/tests/benchmark/db_benchmark/groupby/q7.py new file mode 100644 index 00000000000..462b015cc64 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q7.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q7, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/groupby/q8.py b/tests/benchmark/db_benchmark/groupby/q8.py new file mode 100644 index 00000000000..21aab166fe2 --- /dev/null +++ b/tests/benchmark/db_benchmark/groupby/q8.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_groupby_queries.q8, current_path, suffix, table_id, session + ) diff --git a/third_party/bigframes_vendored/db_benchmark/groupby_queries.py b/third_party/bigframes_vendored/db_benchmark/groupby_queries.py new file mode 100644 index 00000000000..672a9b5d5f6 --- /dev/null +++ b/third_party/bigframes_vendored/db_benchmark/groupby_queries.py @@ -0,0 +1,123 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/groupby-pandas.py + +import bigframes +import bigframes.session + + +def q1(table_id: str, session: bigframes.Session): + print("Groupby benchmark 1: sum v1 by id1") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.groupby("id1", as_index=False, dropna=False).agg({"v1": "sum"}) + print(ans.shape) + chk = [ans["v1"].sum()] + print(chk) + + +def q2(table_id: str, session: bigframes.Session): + print("Groupby benchmark 2: sum v1 by id1:id2") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.groupby(["id1", "id2"], as_index=False, dropna=False).agg({"v1": "sum"}) + print(ans.shape) + chk = [ans["v1"].sum()] + print(chk) + + +def q3(table_id: str, session: bigframes.Session): + print("Groupby benchmark 3: sum v1 mean v3 by id3") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.groupby("id3", as_index=False, dropna=False).agg( + {"v1": "sum", "v3": "mean"} + ) + print(ans.shape) + chk = [ans["v1"].sum(), ans["v3"].sum()] + print(chk) + + +def q4(table_id: str, session: bigframes.Session): + print("Groupby benchmark 4: mean v1:v3 by id4") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.groupby("id4", as_index=False, dropna=False).agg( + {"v1": "mean", "v2": "mean", "v3": "mean"} + ) + print(ans.shape) + chk = [ans["v1"].sum(), ans["v2"].sum(), ans["v3"].sum()] + print(chk) + + +def q5(table_id: str, session: bigframes.Session): + print("Groupby benchmark 5: sum v1:v3 by id6") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.groupby("id6", as_index=False, dropna=False).agg( + {"v1": "sum", "v2": "sum", "v3": "sum"} + ) + print(ans.shape) + chk = [ans["v1"].sum(), ans["v2"].sum(), ans["v3"].sum()] + print(chk) + + +def q6(table_id: str, session: bigframes.Session): + print("Groupby benchmark 6: median v3 sd v3 by id4 id5") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.groupby(["id4", "id5"], as_index=False, dropna=False).agg( + {"v3": ["median", "std"]} + ) + print(ans.shape) + chk = [ans["v3"]["median"].sum(), ans["v3"]["std"].sum()] + print(chk) + + +def q7(table_id: str, session: bigframes.Session): + print("Groupby benchmark 7: max v1 - min v2 by id3") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = ( + x.groupby("id3", as_index=False, dropna=False) + .agg({"v1": "max", "v2": "min"}) + .assign(range_v1_v2=lambda x: x["v1"] - x["v2"])[["id3", "range_v1_v2"]] + ) + print(ans.shape) + chk = [ans["range_v1_v2"].sum()] + print(chk) + + +def q8(table_id: str, session: bigframes.Session): + print("Groupby benchmark 8: largest two v3 by id6") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = ( + x[~x["v3"].isna()][["id6", "v3"]] + .sort_values("v3", ascending=False) + .groupby("id6", as_index=False, dropna=False) + .head(2) + ) + ans = ans.reset_index(drop=True) + print(ans.shape) + chk = [ans["v3"].sum()] + print(chk) + + +def q10(table_id: str, session: bigframes.Session): + print("Groupby benchmark 10: sum v3 count by id1:id6") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + + ans = x.groupby( + ["id1", "id2", "id3", "id4", "id5", "id6"], as_index=False, dropna=False + ).agg({"v3": "sum", "v1": "size"}) + print(ans.shape) + chk = [ans["v3"].sum(), ans["v1"].sum()] + print(chk) From 81b77181b68b6ea40aad1e3f412336b976bcd2d7 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:07:18 -0700 Subject: [PATCH 774/921] chore: add tpch benchmark q1-3 (#921) * chore: add tpch benchmark q1-3 * dataset update * update config * update config * update benchmark --- tests/benchmark/tpch/config.jsonl | 10 +++ tests/benchmark/tpch/q1.py | 25 ++++++++ tests/benchmark/tpch/q2.py | 25 ++++++++ tests/benchmark/tpch/q3.py | 25 ++++++++ third_party/bigframes_vendored/tpch/LICENSE | 19 ++++++ third_party/bigframes_vendored/tpch/METADATA | 19 ++++++ third_party/bigframes_vendored/tpch/README.md | 34 ++++++++++ .../bigframes_vendored/tpch/__init__.py | 0 .../tpch/queries/__init__.py | 0 .../bigframes_vendored/tpch/queries/q1.py | 42 +++++++++++++ .../bigframes_vendored/tpch/queries/q2.py | 62 +++++++++++++++++++ .../bigframes_vendored/tpch/queries/q3.py | 42 +++++++++++++ 12 files changed, 303 insertions(+) create mode 100644 tests/benchmark/tpch/config.jsonl create mode 100644 tests/benchmark/tpch/q1.py create mode 100644 tests/benchmark/tpch/q2.py create mode 100644 tests/benchmark/tpch/q3.py create mode 100644 third_party/bigframes_vendored/tpch/LICENSE create mode 100644 third_party/bigframes_vendored/tpch/METADATA create mode 100644 third_party/bigframes_vendored/tpch/README.md create mode 100644 third_party/bigframes_vendored/tpch/__init__.py create mode 100644 third_party/bigframes_vendored/tpch/queries/__init__.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q1.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q2.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q3.py diff --git a/tests/benchmark/tpch/config.jsonl b/tests/benchmark/tpch/config.jsonl new file mode 100644 index 00000000000..151100594ad --- /dev/null +++ b/tests/benchmark/tpch/config.jsonl @@ -0,0 +1,10 @@ +{"benchmark_suffix": "1g_ordered", "dataset_id": "tpch_0001g", "ordered": true} +{"benchmark_suffix": "1g_unordered", "dataset_id": "tpch_0001g", "ordered": false} +{"benchmark_suffix": "10g_ordered", "dataset_id": "tpch_0010g", "ordered": true} +{"benchmark_suffix": "10g_unordered", "dataset_id": "tpch_0010g", "ordered": false} +{"benchmark_suffix": "100g_ordered", "dataset_id": "tpch_0100g", "ordered": true} +{"benchmark_suffix": "100g_unordered", "dataset_id": "tpch_0100g", "ordered": false} +{"benchmark_suffix": "1t_ordered", "dataset_id": "tpch_0001t", "ordered": true} +{"benchmark_suffix": "1t_unordered", "dataset_id": "tpch_0001t", "ordered": false} +{"benchmark_suffix": "10t_ordered", "dataset_id": "tpch_0010t", "ordered": true} +{"benchmark_suffix": "10t_unordered", "dataset_id": "tpch_0010t", "ordered": false} diff --git a/tests/benchmark/tpch/q1.py b/tests/benchmark/tpch/q1.py new file mode 100644 index 00000000000..633f498a9cc --- /dev/null +++ b/tests/benchmark/tpch/q1.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q1 as vendored_tpch_q1 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q1.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q2.py b/tests/benchmark/tpch/q2.py new file mode 100644 index 00000000000..deb966dc1ea --- /dev/null +++ b/tests/benchmark/tpch/q2.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q2 as vendored_tpch_q2 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q2.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q3.py b/tests/benchmark/tpch/q3.py new file mode 100644 index 00000000000..20be10c259b --- /dev/null +++ b/tests/benchmark/tpch/q3.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib import Path + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q3 as vendored_tpch_q3 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q3.q, current_path, suffix, dataset_id, session + ) diff --git a/third_party/bigframes_vendored/tpch/LICENSE b/third_party/bigframes_vendored/tpch/LICENSE new file mode 100644 index 00000000000..06d01f6abfb --- /dev/null +++ b/third_party/bigframes_vendored/tpch/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Ritchie Vink + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/bigframes_vendored/tpch/METADATA b/third_party/bigframes_vendored/tpch/METADATA new file mode 100644 index 00000000000..65dd8cab935 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/METADATA @@ -0,0 +1,19 @@ +name: "polars-tpch" +description: + "This repository contains modified TPC-H benchmark queries that are " + "specifically adapted to evaluate the performance of the BigFrames library. " + "These benchmarks are designed to test complex data processing workflows " + "that are typical in decision support systems." + +third_party { + identifier { + type: "Git" + value: "https://github.com/pola-rs/tpch" + primary_source: true + version: "Latest Commit on Main Branch as of Access" + } + version: "Latest Commit on Main Branch as of Access" + last_upgrade_date { year: 2024 month: 7 day: 12 } + license_type: PERMISSIVE + local_modifications: "Modified the queries to test and benchmark the BigFrames project" +} diff --git a/third_party/bigframes_vendored/tpch/README.md b/third_party/bigframes_vendored/tpch/README.md new file mode 100644 index 00000000000..ef0b77d7d3c --- /dev/null +++ b/third_party/bigframes_vendored/tpch/README.md @@ -0,0 +1,34 @@ +polars-tpch +=========== + +This repo contains the code used for performance evaluation of polars. The benchmarks are TPC-standardised queries and data designed to test the performance of "real" workflows. + +From the [TPC website](https://www.tpc.org/tpch/): +> TPC-H is a decision support benchmark. It consists of a suite of business-oriented ad hoc queries and concurrent data modifications. The queries and the data populating the database have been chosen to have broad industry-wide relevance. This benchmark illustrates decision support systems that examine large volumes of data, execute queries with a high degree of complexity, and give answers to critical business questions. + +## Generating TPC-H Data + +### Project setup + +```shell +# clone this repository +git clone https://github.com/pola-rs/tpch.git +cd tpch/tpch-dbgen + +# build tpch-dbgen +make +``` + +### Execute + +```shell +# change directory to the root of the repository +cd ../ +./run.sh +``` + +This will do the following, + +- Create a new virtual environment with all required dependencies. +- Generate data for benchmarks. +- Run the benchmark suite. diff --git a/third_party/bigframes_vendored/tpch/__init__.py b/third_party/bigframes_vendored/tpch/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/tpch/queries/__init__.py b/third_party/bigframes_vendored/tpch/queries/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/tpch/queries/q1.py b/third_party/bigframes_vendored/tpch/queries/q1.py new file mode 100644 index 00000000000..3f155ec1c9d --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q1.py @@ -0,0 +1,42 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q1.py + +from datetime import datetime +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = datetime(1998, 9, 2) + lineitem = lineitem[lineitem["L_SHIPDATE"] <= var1.date()] + + lineitem["DISC_PRICE"] = lineitem["L_EXTENDEDPRICE"] * ( + 1.0 - lineitem["L_DISCOUNT"] + ) + lineitem["CHARGE_PRICE"] = ( + lineitem["L_EXTENDEDPRICE"] + * (1.0 - lineitem["L_DISCOUNT"]) + * (1.0 + lineitem["L_TAX"]) + ) + + result = lineitem.groupby(["L_RETURNFLAG", "L_LINESTATUS"], as_index=False).agg( + SUM_QTY=bpd.NamedAgg(column="L_QUANTITY", aggfunc="sum"), + SUM_BASE_PRICE=bpd.NamedAgg(column="L_EXTENDEDPRICE", aggfunc="sum"), + SUM_DISC_PRICE=bpd.NamedAgg(column="DISC_PRICE", aggfunc="sum"), + SUM_CHARGE=bpd.NamedAgg(column="CHARGE_PRICE", aggfunc="sum"), + AVG_QTY=bpd.NamedAgg(column="L_QUANTITY", aggfunc="mean"), + AVG_PRICE=bpd.NamedAgg(column="L_EXTENDEDPRICE", aggfunc="mean"), + AVG_DISC=bpd.NamedAgg(column="L_DISCOUNT", aggfunc="mean"), + COUNT_ORDER=bpd.NamedAgg(column="L_QUANTITY", aggfunc="count"), + ) + result = typing.cast(bpd.DataFrame, result).sort_values( + ["L_RETURNFLAG", "L_LINESTATUS"] + ) + + result.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q2.py b/third_party/bigframes_vendored/tpch/queries/q2.py new file mode 100644 index 00000000000..0586a5d1a53 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q2.py @@ -0,0 +1,62 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q2.py + +import bigframes + + +def q(dataset_id: str, session: bigframes.Session): + region = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.REGION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + partsupp = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + jn = ( + part.merge(partsupp, left_on="P_PARTKEY", right_on="PS_PARTKEY") + .merge(supplier, left_on="PS_SUPPKEY", right_on="S_SUPPKEY") + .merge(nation, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + .merge(region, left_on="N_REGIONKEY", right_on="R_REGIONKEY") + ) + + jn = jn[jn["P_SIZE"] == 15] + jn = jn[jn["P_TYPE"].str.endswith("BRASS")] + jn = jn[jn["R_NAME"] == "EUROPE"] + + gb = jn.groupby("P_PARTKEY", as_index=False) + agg = gb["PS_SUPPLYCOST"].min() + jn2 = agg.merge(jn, on=["P_PARTKEY", "PS_SUPPLYCOST"]) + + sel = jn2[ + [ + "S_ACCTBAL", + "S_NAME", + "N_NAME", + "P_PARTKEY", + "P_MFGR", + "S_ADDRESS", + "S_PHONE", + "S_COMMENT", + ] + ] + + sort = sel.sort_values( + by=["S_ACCTBAL", "N_NAME", "S_NAME", "P_PARTKEY"], + ascending=[False, True, True, True], + ) + + result_df = sort.head(100) + result_df.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q3.py b/third_party/bigframes_vendored/tpch/queries/q3.py new file mode 100644 index 00000000000..94b85f20543 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q3.py @@ -0,0 +1,42 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q3.py + +from datetime import date + +import bigframes + + +def q(dataset_id: str, session: bigframes.Session): + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + date_var = date(1995, 3, 15) + + fcustomer = customer[customer["C_MKTSEGMENT"] == "BUILDING"] + + jn1 = fcustomer.merge(orders, left_on="C_CUSTKEY", right_on="O_CUSTKEY") + jn2 = jn1.merge(lineitem, left_on="O_ORDERKEY", right_on="L_ORDERKEY") + + jn2 = jn2[jn2["O_ORDERDATE"] < date_var] + jn2 = jn2[jn2["L_SHIPDATE"] > date_var] + jn2["REVENUE"] = jn2["L_EXTENDEDPRICE"] * (1 - jn2["L_DISCOUNT"]) + + gb = jn2.groupby(["O_ORDERKEY", "O_ORDERDATE", "O_SHIPPRIORITY"], as_index=False) + agg = gb["REVENUE"].sum() + + sel = agg[["O_ORDERKEY", "REVENUE", "O_ORDERDATE", "O_SHIPPRIORITY"]] + sel = sel.rename(columns={"O_ORDERKEY": "L_ORDERKEY"}) + + sorted_sel = sel.sort_values(by=["REVENUE", "O_ORDERDATE"], ascending=[False, True]) + result_df = sorted_sel.head(10) + + result_df.to_gbq() From 8fab1e8cef00fb8402191f833dd53f2b51144ac9 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:14:28 -0700 Subject: [PATCH 775/921] chore: bench metric fix (#924) * chore: benchmark fix. * chore: benchmark fix. * fix --- scripts/run_and_publish_benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run_and_publish_benchmark.py b/scripts/run_and_publish_benchmark.py index 70ef8d91033..4b3d4f4d962 100644 --- a/scripts/run_and_publish_benchmark.py +++ b/scripts/run_and_publish_benchmark.py @@ -158,7 +158,7 @@ def collect_benchmark_result(benchmark_path: str) -> pd.DataFrame: for index, row in benchmark_metrics.iterrows(): formatted_local_exec_time = ( f"{round(row['Local_Execution_Time_Sec'], 1)} seconds" - if not np.isnan(row["Local_Execution_Time_Sec"]) + if not pd.isna(row["Local_Execution_Time_Sec"]) else "N/A" ) print( From 0a90b1191fc64bb81b9810f3a5988954900e593c Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:33:49 -0700 Subject: [PATCH 776/921] chore: update db_benchmark join queries. (#919) * chore: update db_benchmark join queries. * update import * update import * update import * update * fix config --- tests/benchmark/db_benchmark/groupby/q1.py | 4 +- tests/benchmark/db_benchmark/groupby/q10.py | 4 +- tests/benchmark/db_benchmark/groupby/q2.py | 4 +- tests/benchmark/db_benchmark/groupby/q3.py | 4 +- tests/benchmark/db_benchmark/groupby/q4.py | 4 +- tests/benchmark/db_benchmark/groupby/q5.py | 4 +- tests/benchmark/db_benchmark/groupby/q6.py | 4 +- tests/benchmark/db_benchmark/groupby/q7.py | 4 +- tests/benchmark/db_benchmark/groupby/q8.py | 4 +- .../benchmark/db_benchmark/join/config.jsonl | 2 + tests/benchmark/db_benchmark/join/q1.py | 27 ++++++ tests/benchmark/db_benchmark/join/q2.py | 27 ++++++ tests/benchmark/db_benchmark/join/q3.py | 27 ++++++ tests/benchmark/db_benchmark/join/q4.py | 27 ++++++ tests/benchmark/db_benchmark/join/q5.py | 27 ++++++ tests/benchmark/tpch/q1.py | 4 +- tests/benchmark/tpch/q2.py | 4 +- tests/benchmark/tpch/q3.py | 4 +- .../db_benchmark/join_queries.py | 91 +++++++++++++++++++ 19 files changed, 252 insertions(+), 24 deletions(-) create mode 100644 tests/benchmark/db_benchmark/join/config.jsonl create mode 100644 tests/benchmark/db_benchmark/join/q1.py create mode 100644 tests/benchmark/db_benchmark/join/q2.py create mode 100644 tests/benchmark/db_benchmark/join/q3.py create mode 100644 tests/benchmark/db_benchmark/join/q4.py create mode 100644 tests/benchmark/db_benchmark/join/q5.py create mode 100644 third_party/bigframes_vendored/db_benchmark/join_queries.py diff --git a/tests/benchmark/db_benchmark/groupby/q1.py b/tests/benchmark/db_benchmark/groupby/q1.py index 1ea64991701..02a709def9d 100644 --- a/tests/benchmark/db_benchmark/groupby/q1.py +++ b/tests/benchmark/db_benchmark/groupby/q1.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q1, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/groupby/q10.py b/tests/benchmark/db_benchmark/groupby/q10.py index c3bde06d08d..0cd195b04a2 100644 --- a/tests/benchmark/db_benchmark/groupby/q10.py +++ b/tests/benchmark/db_benchmark/groupby/q10.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q10, diff --git a/tests/benchmark/db_benchmark/groupby/q2.py b/tests/benchmark/db_benchmark/groupby/q2.py index a4dad4dbfa3..398c63e09f9 100644 --- a/tests/benchmark/db_benchmark/groupby/q2.py +++ b/tests/benchmark/db_benchmark/groupby/q2.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q2, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/groupby/q3.py b/tests/benchmark/db_benchmark/groupby/q3.py index 2eca13313da..9863b969d8e 100644 --- a/tests/benchmark/db_benchmark/groupby/q3.py +++ b/tests/benchmark/db_benchmark/groupby/q3.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q3, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/groupby/q4.py b/tests/benchmark/db_benchmark/groupby/q4.py index 7db1286a373..ce29e3ceafb 100644 --- a/tests/benchmark/db_benchmark/groupby/q4.py +++ b/tests/benchmark/db_benchmark/groupby/q4.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q4, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/groupby/q5.py b/tests/benchmark/db_benchmark/groupby/q5.py index 869d091e713..27a4a52a8fe 100644 --- a/tests/benchmark/db_benchmark/groupby/q5.py +++ b/tests/benchmark/db_benchmark/groupby/q5.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q5, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/groupby/q6.py b/tests/benchmark/db_benchmark/groupby/q6.py index c81746a3946..f1befc68406 100644 --- a/tests/benchmark/db_benchmark/groupby/q6.py +++ b/tests/benchmark/db_benchmark/groupby/q6.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q6, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/groupby/q7.py b/tests/benchmark/db_benchmark/groupby/q7.py index 462b015cc64..c0791612e8e 100644 --- a/tests/benchmark/db_benchmark/groupby/q7.py +++ b/tests/benchmark/db_benchmark/groupby/q7.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q7, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/groupby/q8.py b/tests/benchmark/db_benchmark/groupby/q8.py index 21aab166fe2..46dd8c45eb2 100644 --- a/tests/benchmark/db_benchmark/groupby/q8.py +++ b/tests/benchmark/db_benchmark/groupby/q8.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": table_id, session, suffix = utils.get_dbbenchmark_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q8, current_path, suffix, table_id, session diff --git a/tests/benchmark/db_benchmark/join/config.jsonl b/tests/benchmark/db_benchmark/join/config.jsonl new file mode 100644 index 00000000000..72884d6c5ab --- /dev/null +++ b/tests/benchmark/db_benchmark/join/config.jsonl @@ -0,0 +1,2 @@ +{"benchmark_suffix": "50g_ordered", "table_id": "J1_1e9_NA_0_0", "ordered": true} +{"benchmark_suffix": "50g_unordered", "table_id": "J1_1e9_NA_0_0", "ordered": false} diff --git a/tests/benchmark/db_benchmark/join/q1.py b/tests/benchmark/db_benchmark/join/q1.py new file mode 100644 index 00000000000..ce053597893 --- /dev/null +++ b/tests/benchmark/db_benchmark/join/q1.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_join_queries.q1, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/join/q2.py b/tests/benchmark/db_benchmark/join/q2.py new file mode 100644 index 00000000000..6c9141b316f --- /dev/null +++ b/tests/benchmark/db_benchmark/join/q2.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_join_queries.q2, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/join/q3.py b/tests/benchmark/db_benchmark/join/q3.py new file mode 100644 index 00000000000..284ab6a2b3e --- /dev/null +++ b/tests/benchmark/db_benchmark/join/q3.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_join_queries.q3, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/join/q4.py b/tests/benchmark/db_benchmark/join/q4.py new file mode 100644 index 00000000000..1504e0a6638 --- /dev/null +++ b/tests/benchmark/db_benchmark/join/q4.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_join_queries.q4, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/db_benchmark/join/q5.py b/tests/benchmark/db_benchmark/join/q5.py new file mode 100644 index 00000000000..575b3711e6e --- /dev/null +++ b/tests/benchmark/db_benchmark/join/q5.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries + +if __name__ == "__main__": + table_id, session, suffix = utils.get_dbbenchmark_configuration() + + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_dbbenchmark_join_queries.q5, current_path, suffix, table_id, session + ) diff --git a/tests/benchmark/tpch/q1.py b/tests/benchmark/tpch/q1.py index 633f498a9cc..398e9bf9b29 100644 --- a/tests/benchmark/tpch/q1.py +++ b/tests/benchmark/tpch/q1.py @@ -11,14 +11,14 @@ # 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 pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.tpch.queries.q1 as vendored_tpch_q1 if __name__ == "__main__": dataset_id, session, suffix = utils.get_tpch_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_tpch_q1.q, current_path, suffix, dataset_id, session diff --git a/tests/benchmark/tpch/q2.py b/tests/benchmark/tpch/q2.py index deb966dc1ea..57151e36bdd 100644 --- a/tests/benchmark/tpch/q2.py +++ b/tests/benchmark/tpch/q2.py @@ -11,14 +11,14 @@ # 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 pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.tpch.queries.q2 as vendored_tpch_q2 if __name__ == "__main__": dataset_id, session, suffix = utils.get_tpch_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_tpch_q2.q, current_path, suffix, dataset_id, session diff --git a/tests/benchmark/tpch/q3.py b/tests/benchmark/tpch/q3.py index 20be10c259b..28cdfed49c0 100644 --- a/tests/benchmark/tpch/q3.py +++ b/tests/benchmark/tpch/q3.py @@ -11,14 +11,14 @@ # 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 pathlib import Path +import pathlib import benchmark.utils as utils import bigframes_vendored.tpch.queries.q3 as vendored_tpch_q3 if __name__ == "__main__": dataset_id, session, suffix = utils.get_tpch_configuration() - current_path = Path(__file__).absolute() + current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_tpch_q3.q, current_path, suffix, dataset_id, session diff --git a/third_party/bigframes_vendored/db_benchmark/join_queries.py b/third_party/bigframes_vendored/db_benchmark/join_queries.py new file mode 100644 index 00000000000..0c01e427a6f --- /dev/null +++ b/third_party/bigframes_vendored/db_benchmark/join_queries.py @@ -0,0 +1,91 @@ +# Contains code from https://github.com/duckdblabs/db-benchmark/blob/master/pandas/join-pandas.py +# and https://github.com/duckdblabs/db-benchmark/blob/main/_helpers/helpers.py + +import bigframes + + +def q1(table_id: str, session: bigframes.Session): + print("Join benchmark 1: small inner on int") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + small = session.read_gbq( + f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'small')}" + ) + + ans = x.merge(small, on="id1") + print(ans.shape) + + chk = [ans["v1"].sum(), ans["v2"].sum()] + print(chk) + + +def q2(table_id: str, session: bigframes.Session): + print("Join benchmark 2: medium inner on int") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + medium = session.read_gbq( + f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'medium')}" + ) + + ans = x.merge(medium, on="id2") + print(ans.shape) + + chk = [ans["v1"].sum(), ans["v2"].sum()] + print(chk) + + +def q3(table_id: str, session: bigframes.Session): + print("Join benchmark 3: medium outer on int") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + medium = session.read_gbq( + f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'medium')}" + ) + + ans = x.merge(medium, how="left", on="id2") + print(ans.shape) + + chk = [ans["v1"].sum(), ans["v2"].sum()] + print(chk) + + +def q4(table_id: str, session: bigframes.Session): + print("Join benchmark 4: medium inner on factor") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + medium = session.read_gbq( + f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'medium')}" + ) + + ans = x.merge(medium, on="id5") + print(ans.shape) + + chk = [ans["v1"].sum(), ans["v2"].sum()] + print(chk) + + +def q5(table_id: str, session: bigframes.Session): + print("Join benchmark 5: big inner on int") + + x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + big = session.read_gbq( + f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'big')}" + ) + + ans = x.merge(big, on="id3") + print(ans.shape) + + chk = [ans["v1"].sum(), ans["v2"].sum()] + print(chk) + + +def _get_join_table_id(table_id, join_size): + x_n = int(float(table_id.split("_")[1])) + + if join_size == "small": + y_n = "{:.0e}".format(x_n / 1e6) + elif join_size == "medium": + y_n = "{:.0e}".format(x_n / 1e3) + else: + y_n = "{:.0e}".format(x_n) + return table_id.replace("NA", y_n).replace("+0", "") From 4208044222c6a8494004ec6f511a3b85f4eb4180 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 26 Aug 2024 09:45:40 -0700 Subject: [PATCH 777/921] fix: Improve `Series.replace` for dict input (#907) --- bigframes/series.py | 8 +++++++- tests/system/small/test_series.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/bigframes/series.py b/bigframes/series.py index c2137bca35d..a166680f855 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -581,6 +581,9 @@ def _simple_replace(self, to_replace_list: typing.Sequence, value): return Series(block.select_column(result_col)) def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): + if not mapping: + return self.copy() + tuples = [] lcd_types: list[typing.Optional[bigframes.dtypes.Dtype]] = [] for key, value in mapping.items(): @@ -597,6 +600,7 @@ def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): result_dtype = functools.reduce( lambda t1, t2: bigframes.dtypes.lcd_type(t1, t2) if (t1 and t2) else None, lcd_types, + self.dtype, ) if not result_dtype: raise NotImplementedError( @@ -605,7 +609,9 @@ def _mapping_replace(self, mapping: dict[typing.Hashable, typing.Hashable]): block, result = self._block.apply_unary_op( self._value_column, ops.MapOp(tuple(tuples)) ) - return Series(block.select_column(result)) + replaced = Series(block.select_column(result)) + replaced.name = self.name + return replaced @validations.requires_ordering() @validations.requires_index diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index fe6e0017973..7458187a825 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -458,6 +458,29 @@ def test_series_replace_list_scalar(scalars_dfs): ) +@pytest.mark.parametrize( + ("replacement_dict",), + ( + ({"Hello, World!": "Howdy, Planet!", "T": "R"},), + ({},), + ), + ids=[ + "non-empty", + "empty", + ], +) +def test_series_replace_dict(scalars_dfs, replacement_dict): + scalars_df, scalars_pandas_df = scalars_dfs + col_name = "string_col" + bf_result = scalars_df[col_name].replace(replacement_dict).to_pandas() + pd_result = scalars_pandas_df[col_name].replace(replacement_dict) + + pd.testing.assert_series_equal( + pd_result, + bf_result, + ) + + @pytest.mark.parametrize( ("method",), ( From adfaddcc0fd9f495368e56eead4c1983d1cdf434 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:53:20 -0700 Subject: [PATCH 778/921] chore: add tpch q9-13 (#927) * chore: add tpch q9-13 * update import --- tests/benchmark/tpch/q10.py | 25 +++++++ tests/benchmark/tpch/q11.py | 25 +++++++ tests/benchmark/tpch/q12.py | 25 +++++++ tests/benchmark/tpch/q13.py | 25 +++++++ tests/benchmark/tpch/q9.py | 25 +++++++ .../bigframes_vendored/tpch/queries/q10.py | 75 +++++++++++++++++++ .../bigframes_vendored/tpch/queries/q11.py | 40 ++++++++++ .../bigframes_vendored/tpch/queries/q12.py | 49 ++++++++++++ .../bigframes_vendored/tpch/queries/q13.py | 37 +++++++++ .../bigframes_vendored/tpch/queries/q9.py | 68 +++++++++++++++++ 10 files changed, 394 insertions(+) create mode 100644 tests/benchmark/tpch/q10.py create mode 100644 tests/benchmark/tpch/q11.py create mode 100644 tests/benchmark/tpch/q12.py create mode 100644 tests/benchmark/tpch/q13.py create mode 100644 tests/benchmark/tpch/q9.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q10.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q11.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q12.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q13.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q9.py diff --git a/tests/benchmark/tpch/q10.py b/tests/benchmark/tpch/q10.py new file mode 100644 index 00000000000..e500a657b60 --- /dev/null +++ b/tests/benchmark/tpch/q10.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q10 as vendored_tpch_q10 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q10.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q11.py b/tests/benchmark/tpch/q11.py new file mode 100644 index 00000000000..46bd2f022c0 --- /dev/null +++ b/tests/benchmark/tpch/q11.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q11 as vendored_tpch_q11 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q11.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q12.py b/tests/benchmark/tpch/q12.py new file mode 100644 index 00000000000..d2fab97699d --- /dev/null +++ b/tests/benchmark/tpch/q12.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q12 as vendored_tpch_q12 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q12.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q13.py b/tests/benchmark/tpch/q13.py new file mode 100644 index 00000000000..24eec7090c3 --- /dev/null +++ b/tests/benchmark/tpch/q13.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q13 as vendored_tpch_q13 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q13.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q9.py b/tests/benchmark/tpch/q9.py new file mode 100644 index 00000000000..c098e9a8dac --- /dev/null +++ b/tests/benchmark/tpch/q9.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q9 as vendored_tpch_q9 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q9.q, current_path, suffix, dataset_id, session + ) diff --git a/third_party/bigframes_vendored/tpch/queries/q10.py b/third_party/bigframes_vendored/tpch/queries/q10.py new file mode 100644 index 00000000000..95b7b87b0e7 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q10.py @@ -0,0 +1,75 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q10.py + +from datetime import date +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = date(1993, 10, 1) + var2 = date(1994, 1, 1) + + q_final = customer.merge + + q_final = ( + customer.merge(orders, left_on="C_CUSTKEY", right_on="O_CUSTKEY") + .merge(lineitem, left_on="O_ORDERKEY", right_on="L_ORDERKEY") + .merge(nation, left_on="C_NATIONKEY", right_on="N_NATIONKEY") + ) + + q_final = typing.cast( + bpd.DataFrame, + q_final[ + (q_final["O_ORDERDATE"] >= var1) + & (q_final["O_ORDERDATE"] < var2) + & (q_final["L_RETURNFLAG"] == "R") + ], + ) + q_final["INTERMEDIATE_REVENUE"] = ( + q_final["L_EXTENDEDPRICE"] * (1 - q_final["L_DISCOUNT"]) + ).round(2) + + q_final = q_final.groupby( + [ + "C_CUSTKEY", + "C_NAME", + "C_ACCTBAL", + "C_PHONE", + "N_NAME", + "C_ADDRESS", + "C_COMMENT", + ], + as_index=False, + ).agg(REVENUE=bpd.NamedAgg(column="INTERMEDIATE_REVENUE", aggfunc="sum")) + + q_final[ + [ + "C_CUSTKEY", + "C_NAME", + "REVENUE", + "C_ACCTBAL", + "N_NAME", + "C_ADDRESS", + "C_PHONE", + "C_COMMENT", + ] + ].sort_values(by="REVENUE", ascending=False).head(20).to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q11.py b/third_party/bigframes_vendored/tpch/queries/q11.py new file mode 100644 index 00000000000..269728f6f1c --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q11.py @@ -0,0 +1,40 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/duckdb/q11.py + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + partsupp = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + merged_df = partsupp.merge(supplier, left_on="PS_SUPPKEY", right_on="S_SUPPKEY") + merged_df = merged_df.merge(nation, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + + filtered_df = merged_df[merged_df["N_NAME"] == "GERMANY"] + + filtered_df["VALUE"] = filtered_df["PS_SUPPLYCOST"] * filtered_df["PS_AVAILQTY"] + grouped = filtered_df.groupby("PS_PARTKEY", as_index=False).agg( + VALUE=bpd.NamedAgg(column="VALUE", aggfunc="sum") + ) + + grouped["VALUE"] = grouped["VALUE"].round(2) + + total_value = (filtered_df["PS_SUPPLYCOST"] * filtered_df["PS_AVAILQTY"]).sum() + threshold = total_value * 0.0001 + + result_df = grouped[grouped["VALUE"] > threshold] + + result_df = result_df.sort_values(by="VALUE", ascending=False) + + result_df.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q12.py b/third_party/bigframes_vendored/tpch/queries/q12.py new file mode 100644 index 00000000000..1c392759323 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q12.py @@ -0,0 +1,49 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q12.py + +from datetime import date +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = "MAIL" + var2 = "SHIP" + var3 = date(1994, 1, 1) + var4 = date(1995, 1, 1) + + q_final = orders.merge(lineitem, left_on="O_ORDERKEY", right_on="L_ORDERKEY") + + q_final = q_final[ + (q_final["L_SHIPMODE"].isin([var1, var2])) + & (q_final["L_COMMITDATE"] < q_final["L_RECEIPTDATE"]) + & (q_final["L_SHIPDATE"] < q_final["L_COMMITDATE"]) + & (q_final["L_RECEIPTDATE"] >= var3) + & (q_final["L_RECEIPTDATE"] < var4) + ] + + q_final["HIGH_LINE_COUNT"] = ( + q_final["O_ORDERPRIORITY"].isin(["1-URGENT", "2-HIGH"]) + ).astype("Int64") + q_final["LOW_LINE_COUNT"] = ( + ~q_final["O_ORDERPRIORITY"].isin(["1-URGENT", "2-HIGH"]) + ).astype("Int64") + + agg_results = q_final.groupby("L_SHIPMODE", as_index=False).agg( + HIGH_LINE_COUNT=bpd.NamedAgg(column="HIGH_LINE_COUNT", aggfunc="sum"), + LOW_LINE_COUNT=bpd.NamedAgg(column="LOW_LINE_COUNT", aggfunc="sum"), + ) + + agg_results = typing.cast(bpd.DataFrame, agg_results).sort_values("L_SHIPMODE") + + agg_results.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q13.py b/third_party/bigframes_vendored/tpch/queries/q13.py new file mode 100644 index 00000000000..dd69cf509f6 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q13.py @@ -0,0 +1,37 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q13.py + +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = "special" + var2 = "requests" + + regex_pattern = f"{var1}.*{var2}" + orders = orders[~orders["O_COMMENT"].str.contains(regex_pattern, regex=True)] + + q_final = ( + customer.merge(orders, left_on="C_CUSTKEY", right_on="O_CUSTKEY", how="left") + .groupby("C_CUSTKEY", as_index=False) + .agg(C_COUNT=bpd.NamedAgg(column="O_ORDERKEY", aggfunc="count")) + .groupby("C_COUNT", as_index=False) + .agg("size") + .rename(columns={"size": "CUSTDIST"}) + ) + q_final = typing.cast(bpd.DataFrame, q_final).sort_values( + ["CUSTDIST", "C_COUNT"], ascending=[False, False] + ) + + q_final.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q9.py b/third_party/bigframes_vendored/tpch/queries/q9.py new file mode 100644 index 00000000000..d04117f0f13 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q9.py @@ -0,0 +1,68 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q9.py + +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + partsupp = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + q_final = ( + part.merge(partsupp, left_on="P_PARTKEY", right_on="PS_PARTKEY") + .merge(supplier, left_on="PS_SUPPKEY", right_on="S_SUPPKEY") + .merge( + lineitem, + left_on=["P_PARTKEY", "PS_SUPPKEY"], + right_on=["L_PARTKEY", "L_SUPPKEY"], + ) + .merge(orders, left_on="L_ORDERKEY", right_on="O_ORDERKEY") + .merge(nation, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + ) + + q_final = q_final[q_final["P_NAME"].str.contains("green")] + + q_final = q_final.rename(columns={"N_NAME": "NATION"}) + q_final["O_YEAR"] = q_final["O_ORDERDATE"].dt.year + q_final["AMOUNT"] = ( + q_final["L_EXTENDEDPRICE"] * (1 - q_final["L_DISCOUNT"]) + - q_final["PS_SUPPLYCOST"] * q_final["L_QUANTITY"] + ) + + q_final = q_final[["NATION", "O_YEAR", "AMOUNT"]] + + q_final = q_final.groupby(["NATION", "O_YEAR"], as_index=False).agg( + SUM_PROFIT=bpd.NamedAgg(column="AMOUNT", aggfunc="sum") + ) + + q_final["SUM_PROFIT"] = q_final["SUM_PROFIT"].round(2) + + q_final = typing.cast(bpd.DataFrame, q_final).sort_values( + ["NATION", "O_YEAR"], ascending=[True, False] + ) + + q_final.to_gbq() From 612271d35675353effa465a797d6e3a1285d4d37 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:28:02 -0700 Subject: [PATCH 779/921] fix: NullIndex in ML model.predict error (#917) --- bigframes/ml/core.py | 3 ++- tests/system/large/ml/test_linear_model.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index d570945f16a..b41426f7d70 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -77,7 +77,8 @@ def _apply_ml_tvf( result_sql = apply_sql_tvf(input_sql) df = self._session.read_gbq(result_sql, index_col=index_col_ids) - df.index.names = index_labels + if df._has_index: + df.index.names = index_labels # Restore column labels df.rename( columns={ diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 2f4c07fa28f..f593ac29834 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -111,7 +111,7 @@ def test_linear_regression_customized_params_fit_score( assert reloaded_model.learning_rate == 0.2 -def test_unordered_mode_regression_configure_fit_score( +def test_unordered_mode_linear_regression_configure_fit_score_predict( unordered_session, penguins_table_id, dataset_id ): model = bigframes.ml.linear_model.LinearRegression() @@ -154,6 +154,14 @@ def test_unordered_mode_regression_configure_fit_score( assert reloaded_model.max_iterations == 20 assert reloaded_model.tol == 0.01 + pred = reloaded_model.predict(df) + utils.check_pandas_df_schema_and_index( + pred, + columns=("predicted_body_mass_g",), + col_exact=False, + index=334, + ) + # TODO(garrettwu): add tests for param warm_start. Requires a trained model. From 6c3f46c7b1edc192e89a7f3cf2d6b01f5ae048ba Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 28 Aug 2024 08:21:49 -0700 Subject: [PATCH 780/921] test: make read_pandas encryption tests non-deterministic (#933) * test: make read_pandas encryption tests non-deterministic This ensure that there is always a fresh result table is created and avoids any unwanted encryption mismatch due to BQ caching. * take care of gcs path issue --- tests/system/small/test_encryption.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 65a98b014dc..960752a9350 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -13,6 +13,8 @@ # limitations under the License. +import random + from google.cloud import bigquery import pandas import pytest @@ -153,14 +155,19 @@ def test_read_csv_gcs( if not bq_cmek: # pragma: NO COVER pytest.skip("no cmek set for testing") # pragma: NO COVER - # Create a csv in gcs + # Let's make the source data non-deterministic so that the test doesn't run + # into a BQ caching path + df = scalars_df_index.copy() + df["int_random"] = random.randint(0, 1_000_000_000) + + # Export the dataframe to a csv in gcs write_path = gcs_folder + "test_read_csv_gcs_bigquery_engine*.csv" read_path = ( utils.get_first_file_from_wildcard(write_path) if engine is None else write_path ) - scalars_df_index.to_csv(write_path) + df.to_csv(write_path) - # Read the BQ table + # Read the gcs csv df = session_with_bq_cmek.read_csv(read_path, engine=engine) # Assert encryption @@ -205,7 +212,9 @@ def test_read_pandas(bq_cmek, session_with_bq_cmek): pytest.skip("no cmek set for testing") # pragma: NO COVER # Read a pandas dataframe - df = session_with_bq_cmek.read_pandas(pandas.DataFrame([1])) + df = session_with_bq_cmek.read_pandas( + pandas.DataFrame([random.randint(0, 1_000_000_000)]) + ) # Assert encryption _assert_bq_table_is_encrypted(df, bq_cmek, session_with_bq_cmek) From 2b355e2f7f8a3319b5d04c43cca6d63ec8650342 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 28 Aug 2024 08:24:00 -0700 Subject: [PATCH 781/921] test: allow more retries on claude tests (#934) This should accommodate more flakiness in the not-so-stable claude endpoints. --- tests/system/small/ml/test_llm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 0c8a1956dbb..2f1a16f23c6 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -409,6 +409,7 @@ def test_gemini_text_generator_predict_with_params_success( "model_name", ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), ) +@pytest.mark.flaky(retries=3, delay=120) def test_claude3_text_generator_create_load( dataset_id, model_name, session, session_us_east5, bq_connection ): @@ -433,7 +434,7 @@ def test_claude3_text_generator_create_load( "model_name", ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), ) -@pytest.mark.flaky(retries=2) +@pytest.mark.flaky(retries=3, delay=120) def test_claude3_text_generator_predict_default_params_success( llm_text_df, model_name, session, session_us_east5, bq_connection ): @@ -453,7 +454,7 @@ def test_claude3_text_generator_predict_default_params_success( "model_name", ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), ) -@pytest.mark.flaky(retries=2) +@pytest.mark.flaky(retries=3, delay=120) def test_claude3_text_generator_predict_with_params_success( llm_text_df, model_name, session, session_us_east5, bq_connection ): From 189f1479a02dd3c0933939664d9b444bc27a860c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 28 Aug 2024 10:25:13 -0500 Subject: [PATCH 782/921] chore: fix api coverage script in nightly build, ensure scripts tests run (#932) * chore: fix api coverage script in nightly build, ensure scripts tests run * use consistent units for timestamp column * handle functions we cant inspect * skip python 3.9 * typo * fix skip --- noxfile.py | 2 + scripts/__init__.py | 13 ++++++ scripts/publish_api_coverage.py | 37 +++++++++++----- scripts/test_publish_api_coverage.py | 64 +++++++++++++++++----------- 4 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 scripts/__init__.py diff --git a/noxfile.py b/noxfile.py index 9ed85290faf..efe5a53082a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -195,6 +195,7 @@ def run_unit(session, install_test_extra): install_unittest_dependencies(session, install_test_extra, "-c", constraints_path) # Run py.test against the unit tests. + scripts_path = "scripts" tests_path = os.path.join("tests", "unit") third_party_tests_path = os.path.join("third_party", "bigframes_vendored") session.run( @@ -209,6 +210,7 @@ def run_unit(session, install_test_extra): "--cov-fail-under=0", tests_path, third_party_tests_path, + scripts_path, *session.posargs, ) diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/publish_api_coverage.py b/scripts/publish_api_coverage.py index 0292d4880d3..8f305bcc0fa 100644 --- a/scripts/publish_api_coverage.py +++ b/scripts/publish_api_coverage.py @@ -107,8 +107,15 @@ def names_from_signature(signature): def calculate_missing_parameters(bigframes_function, target_function): - bigframes_params = names_from_signature(inspect.signature(bigframes_function)) - target_params = names_from_signature(inspect.signature(target_function)) + # Some built-in functions can't be inspected. These raise a ValueError. + try: + bigframes_signature = inspect.signature(bigframes_function) + target_signature = inspect.signature(target_function) + except ValueError: + return {} + + bigframes_params = names_from_signature(bigframes_signature) + target_params = names_from_signature(target_signature) return target_params - bigframes_params @@ -164,13 +171,20 @@ def generate_pandas_api_coverage(): token_type = "property" is_in_bigframes = hasattr(bigframes_obj, member) - requires_index = False - requires_ordering = False + requires_index = "" + requires_ordering = "" if is_in_bigframes: attr = getattr(bigframes_obj, member) - requires_index = hasattr(attr, "_validations_requires_index") - requires_ordering = hasattr(attr, "_validations_requires_ordering") + + # TODO(b/361101138): Add check/documentation for partial + # support (e.g. with some parameters). + requires_index = ( + "Y" if hasattr(attr, "_validations_requires_index") else "" + ) + requires_ordering = ( + "Y" if hasattr(attr, "_validations_requires_ordering") else "" + ) api_patterns.append( [ @@ -279,9 +293,12 @@ def build_api_coverage_table(bigframes_version: str, release_version: str): sklearn_cov_df["module"] = "bigframes.ml" combined_df = pd.concat([pandas_cov_df, sklearn_cov_df]) combined_df["timestamp"] = pd.Timestamp.now() + # BigQuery only supports microsecond precision timestamps. + combined_df["timestamp"] = combined_df["timestamp"].astype("datetime64[us]") combined_df["bigframes_version"] = bigframes_version combined_df["release_version"] = release_version - return combined_df.infer_objects().convert_dtypes() + combined_df = combined_df.infer_objects().convert_dtypes() + return combined_df def format_api(api_names, is_in_bigframes, api_prefix): @@ -313,16 +330,14 @@ def generate_api_coverage(df, api_prefix): api_prefix, ), "Implemented": "", - "Requires index": "", - "Requires ordering": "", + "Requires index": dataframe_apis["requires_index"], + "Requires ordering": dataframe_apis["requires_ordering"], "Missing parameters": dataframe_apis["missing_parameters"], } ) dataframe_table.loc[fully_implemented, "Implemented"] = "Y" dataframe_table.loc[partial_implemented, "Implemented"] = "P" dataframe_table.loc[not_implemented, "Implemented"] = "N" - dataframe_table.loc[dataframe_apis["requires_index"], "Requires index"] = "Y" - dataframe_table.loc[dataframe_apis["requires_ordering"], "Requires ordering"] = "Y" return dataframe_table diff --git a/scripts/test_publish_api_coverage.py b/scripts/test_publish_api_coverage.py index 061cc1c25ce..0b87563482c 100644 --- a/scripts/test_publish_api_coverage.py +++ b/scripts/test_publish_api_coverage.py @@ -12,36 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys + import pandas -import publish_api_coverage +import pytest + +from . import publish_api_coverage + + +@pytest.fixture +def api_coverage_df(): + return publish_api_coverage.build_api_coverage_table("my_bf_ver", "my_release_ver") -def test_api_coverage_produces_expected_schema(): - df = publish_api_coverage.build_api_coverage_table("my_bf_ver", "my_release_ver") +def test_api_coverage_produces_expected_schema(api_coverage_df): + if sys.version.split(".")[:2] == ["3", "9"]: + pytest.skip( + "Python 3.9 uses older pandas without good microsecond timestamp support." + ) + pandas.testing.assert_series_equal( - df.dtypes, + api_coverage_df.dtypes, pandas.Series( - data=[ - "string", - "string", - "string", - "boolean", - "string", - "string", - "datetime64[ns]", - "string", - "string", - ], - index=[ - "api", - "pattern", - "kind", - "is_in_bigframes", - "missing_parameters", - "module", - "timestamp", - "bigframes_version", - "release_version", - ], + data={ + # Note to developer: if you update this test, you will also + # need to update schema of the API coverage BigQuery table in + # the bigframes-metrics project. + "api": "string", + "pattern": "string", + "kind": "string", + "is_in_bigframes": "boolean", + "missing_parameters": "string", + "requires_index": "string", + "requires_ordering": "string", + "module": "string", + "timestamp": "datetime64[us]", + "bigframes_version": "string", + "release_version": "string", + }, ), ) + + +def test_api_coverage_produces_missing_parameters(api_coverage_df): + """Make sure at least some functions have reported missing parameters.""" + assert (api_coverage_df["missing_parameters"].str.len() > 0).any() From 3d71913b3cf357fc9e94304ca0c94070e0a16f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 28 Aug 2024 11:25:16 -0500 Subject: [PATCH 783/921] deps: re-introduce support for numpy 1.24.x (#931) --- bigframes/dataframe.py | 7 +++- bigframes/dtypes.py | 79 ++++++++++++++++++++---------------- bigframes/operations/base.py | 6 +-- setup.py | 1 + testing/constraints-3.9.txt | 1 + 5 files changed, 53 insertions(+), 41 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index cb9c9041213..17dde7021b8 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -112,12 +112,15 @@ def __init__( *, session: typing.Optional[bigframes.session.Session] = None, ): + global bigframes + if copy is not None and not copy: raise ValueError( f"DataFrame constructor only supports copy=True. {constants.FEEDBACK_LINK}" ) - # just ignore object dtype if provided - if dtype in {numpy.dtypes.ObjectDType, "object"}: + # Ignore object dtype if provided, as it provides no additional + # information about what BigQuery type to use. + if dtype is not None and bigframes.dtypes.is_object_like(dtype): dtype = None # Check to see if constructing from BigQuery-backed objects before diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 563904fbb6d..45c1e7e4e2f 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -205,67 +205,74 @@ class SimpleDtypeInfo: ## dtype predicates - use these to maintain consistency -def is_datetime_like(type: ExpressionType) -> bool: - return type in (DATETIME_DTYPE, TIMESTAMP_DTYPE) +def is_datetime_like(type_: ExpressionType) -> bool: + return type_ in (DATETIME_DTYPE, TIMESTAMP_DTYPE) -def is_date_like(type: ExpressionType) -> bool: - return type in (DATETIME_DTYPE, TIMESTAMP_DTYPE, DATE_DTYPE) +def is_date_like(type_: ExpressionType) -> bool: + return type_ in (DATETIME_DTYPE, TIMESTAMP_DTYPE, DATE_DTYPE) -def is_time_like(type: ExpressionType) -> bool: - return type in (DATETIME_DTYPE, TIMESTAMP_DTYPE, TIME_DTYPE) +def is_time_like(type_: ExpressionType) -> bool: + return type_ in (DATETIME_DTYPE, TIMESTAMP_DTYPE, TIME_DTYPE) -def is_binary_like(type: ExpressionType) -> bool: - return type in (BOOL_DTYPE, BYTES_DTYPE, INT_DTYPE) +def is_binary_like(type_: ExpressionType) -> bool: + return type_ in (BOOL_DTYPE, BYTES_DTYPE, INT_DTYPE) -def is_string_like(type: ExpressionType) -> bool: - return type in (STRING_DTYPE, BYTES_DTYPE) +def is_object_like(type_: Union[ExpressionType, str]) -> bool: + # See: https://stackoverflow.com/a/40312924/101923 and + # https://numpy.org/doc/stable/reference/generated/numpy.dtype.kind.html + # for the way to identify object type. + return type_ in ("object", "O") or getattr(type_, "kind", None) == "O" -def is_array_like(type: ExpressionType) -> bool: - return isinstance(type, pd.ArrowDtype) and isinstance( - type.pyarrow_dtype, pa.ListType +def is_string_like(type_: ExpressionType) -> bool: + return type_ in (STRING_DTYPE, BYTES_DTYPE) + + +def is_array_like(type_: ExpressionType) -> bool: + return isinstance(type_, pd.ArrowDtype) and isinstance( + type_.pyarrow_dtype, pa.ListType ) -def is_array_string_like(type: ExpressionType) -> bool: +def is_array_string_like(type_: ExpressionType) -> bool: return ( - isinstance(type, pd.ArrowDtype) - and isinstance(type.pyarrow_dtype, pa.ListType) - and pa.types.is_string(type.pyarrow_dtype.value_type) + isinstance(type_, pd.ArrowDtype) + and isinstance(type_.pyarrow_dtype, pa.ListType) + and pa.types.is_string(type_.pyarrow_dtype.value_type) ) -def is_struct_like(type: ExpressionType) -> bool: - return isinstance(type, pd.ArrowDtype) and isinstance( - type.pyarrow_dtype, pa.StructType +def is_struct_like(type_: ExpressionType) -> bool: + return isinstance(type_, pd.ArrowDtype) and isinstance( + type_.pyarrow_dtype, pa.StructType ) -def is_json_like(type: ExpressionType) -> bool: +def is_json_like(type_: ExpressionType) -> bool: # TODO: Add JSON type support - return type == STRING_DTYPE + return type_ == STRING_DTYPE -def is_json_encoding_type(type: ExpressionType) -> bool: +def is_json_encoding_type(type_: ExpressionType) -> bool: # Types can be converted into JSON. # https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#json_encodings - return type != GEO_DTYPE + return type_ != GEO_DTYPE -def is_numeric(type: ExpressionType) -> bool: - return type in NUMERIC_BIGFRAMES_TYPES_PERMISSIVE +def is_numeric(type_: ExpressionType) -> bool: + return type_ in NUMERIC_BIGFRAMES_TYPES_PERMISSIVE -def is_iterable(type: ExpressionType) -> bool: - return type in (STRING_DTYPE, BYTES_DTYPE) or is_array_like(type) +def is_iterable(type_: ExpressionType) -> bool: + return type_ in (STRING_DTYPE, BYTES_DTYPE) or is_array_like(type_) -def is_comparable(type: ExpressionType) -> bool: - return (type is not None) and is_orderable(type) +def is_comparable(type_: ExpressionType) -> bool: + return (type_ is not None) and is_orderable(type_) _ORDERABLE_SIMPLE_TYPES = set( @@ -273,9 +280,9 @@ def is_comparable(type: ExpressionType) -> bool: ) -def is_orderable(type: ExpressionType) -> bool: +def is_orderable(type_: ExpressionType) -> bool: # On BQ side, ARRAY, STRUCT, GEOGRAPHY, JSON are not orderable - return type in _ORDERABLE_SIMPLE_TYPES + return type_ in _ORDERABLE_SIMPLE_TYPES _CLUSTERABLE_SIMPLE_TYPES = set( @@ -283,15 +290,15 @@ def is_orderable(type: ExpressionType) -> bool: ) -def is_clusterable(type: ExpressionType) -> bool: +def is_clusterable(type_: ExpressionType) -> bool: # https://cloud.google.com/bigquery/docs/clustered-tables#cluster_column_types # This is based on default database type mapping, could in theory represent in non-default bq type to cluster. - return type in _CLUSTERABLE_SIMPLE_TYPES + return type_ in _CLUSTERABLE_SIMPLE_TYPES -def is_bool_coercable(type: ExpressionType) -> bool: +def is_bool_coercable(type_: ExpressionType) -> bool: # TODO: Implement more bool coercions - return (type is None) or is_numeric(type) or is_string_like(type) + return (type_ is None) or is_numeric(type_) or is_string_like(type_) BIGFRAMES_STRING_TO_BIGFRAMES: Dict[DtypeString, Dtype] = { diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 1daa1ea5aef..2f870454157 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -18,7 +18,6 @@ from typing import List, Sequence import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing -import numpy import pandas as pd import bigframes.constants as constants @@ -49,8 +48,9 @@ def __init__( ): import bigframes.pandas - # just ignore object dtype if provided - if dtype in {numpy.dtypes.ObjectDType, "object"}: + # Ignore object dtype if provided, as it provides no additional + # information about what BigQuery type to use. + if dtype is not None and bigframes.dtypes.is_object_like(dtype): dtype = None read_pandas_func = ( diff --git a/setup.py b/setup.py index 79baf1fb23a..0e0be5fd775 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ "google-cloud-storage >=2.0.0", "ibis-framework[bigquery] >=8.0.0,<9.0.0dev", "jellyfish >=0.8.9", + "numpy >=1.24.0", # TODO: Relax upper bound once we have fixed `system_prerelease` tests. "pandas >=1.5.0", "pyarrow >=8.0.0", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 5a766985766..0d3f16e95f9 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -14,6 +14,7 @@ google-cloud-resource-manager==1.10.3 google-cloud-storage==2.0.0 ibis-framework==8.0.0 jellyfish==0.8.9 +numpy==1.24.0 pandas==1.5.0 pyarrow==8.0.0 pydata-google-auth==1.8.2 From 794a2ac448e8bb1de66e178f3ec2a279bcb82509 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:05:41 -0700 Subject: [PATCH 784/921] test: stop checking text generation contents (#935) --- tests/system/small/ml/test_core.py | 20 ++------ tests/system/small/ml/test_llm.py | 77 +++++++++++++----------------- tests/system/utils.py | 5 ++ 3 files changed, 43 insertions(+), 59 deletions(-) diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 95719ea0db3..6b852e87af9 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -14,7 +14,6 @@ from datetime import datetime import typing -from unittest import TestCase import pandas as pd import pyarrow as pa @@ -24,7 +23,7 @@ import bigframes import bigframes.features from bigframes.ml import core -import tests.system.utils +from tests.system import utils def test_model_eval( @@ -212,7 +211,7 @@ def test_pca_model_principal_components(penguins_bqml_pca_model: core.BqmlModel) .reset_index(drop=True) ) - tests.system.utils.assert_pandas_df_equal_pca_components( + utils.assert_pandas_df_equal_pca_components( result, expected, check_exact=False, @@ -234,7 +233,7 @@ def test_pca_model_principal_component_info(penguins_bqml_pca_model: core.BqmlMo "cumulative_explained_variance_ratio": [0.469357, 0.651283, 0.812383], }, ) - tests.system.utils.assert_pandas_df_equal( + utils.assert_pandas_df_equal( result, expected, check_exact=False, @@ -349,18 +348,9 @@ def test_model_generate_text( llm_text_df, options=options ).to_pandas() - TestCase().assertSequenceEqual(df.shape, (3, 4)) - TestCase().assertSequenceEqual( - [ - "ml_generate_text_llm_result", - "ml_generate_text_rai_result", - "ml_generate_text_status", - "prompt", - ], - df.columns.to_list(), + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False ) - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) def test_model_forecast(time_series_bqml_arima_plus_model: core.BqmlModel): diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 2f1a16f23c6..43e756019d2 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -74,10 +74,9 @@ def test_create_text_generator_model_default_session( llm_text_df = bpd.read_pandas(llm_text_pandas_df) df = model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.flaky(retries=2) @@ -104,10 +103,9 @@ def test_create_text_generator_32k_model_default_session( llm_text_df = bpd.read_pandas(llm_text_pandas_df) df = model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.flaky(retries=2) @@ -131,10 +129,9 @@ def test_create_text_generator_model_default_connection( ) df = model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) # Marked as flaky only because BQML LLM is in preview, the service only has limited capacity, not stable enough. @@ -143,10 +140,9 @@ def test_text_generator_predict_default_params_success( palm2_text_generator_model, llm_text_df ): df = palm2_text_generator_model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.flaky(retries=2) @@ -154,10 +150,9 @@ def test_text_generator_predict_series_default_params_success( palm2_text_generator_model, llm_text_df ): df = palm2_text_generator_model.predict(llm_text_df["prompt"]).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.flaky(retries=2) @@ -166,10 +161,9 @@ def test_text_generator_predict_arbitrary_col_label_success( ): llm_text_df = llm_text_df.rename(columns={"prompt": "arbitrary"}) df = palm2_text_generator_model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.flaky(retries=2) @@ -179,10 +173,9 @@ def test_text_generator_predict_with_params_success( df = palm2_text_generator_model.predict( llm_text_df, temperature=0.5, max_output_tokens=100, top_k=20, top_p=0.5 ).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) def test_create_embedding_generator_model( @@ -379,10 +372,9 @@ def test_gemini_text_generator_predict_default_params_success( model_name=model_name, connection_name=bq_connection, session=session ) df = gemini_text_generator_model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.parametrize( @@ -399,10 +391,9 @@ def test_gemini_text_generator_predict_with_params_success( df = gemini_text_generator_model.predict( llm_text_df, temperature=0.5, max_output_tokens=100, top_k=20, top_p=0.5 ).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.parametrize( @@ -444,10 +435,9 @@ def test_claude3_text_generator_predict_default_params_success( model_name=model_name, connection_name=bq_connection, session=session ) df = claude3_text_generator_model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 3) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.parametrize( @@ -466,10 +456,9 @@ def test_claude3_text_generator_predict_with_params_success( df = claude3_text_generator_model.predict( llm_text_df, max_output_tokens=100, top_k=20, top_p=0.5 ).to_pandas() - assert df.shape == (3, 3) - assert "ml_generate_text_llm_result" in df.columns - series = df["ml_generate_text_llm_result"] - assert all(series.str.len() > 20) + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) @pytest.mark.flaky(retries=2) diff --git a/tests/system/utils.py b/tests/system/utils.py index e9054d04c9e..26e3e97e245 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -45,6 +45,11 @@ "log_loss", "roc_auc", ] +ML_GENERATE_TEXT_OUTPUT = [ + "ml_generate_text_llm_result", + "ml_generate_text_status", + "prompt", +] def skip_legacy_pandas(test): From 85d7c21b4bd5dc669098342fc60d66d89ef06b2b Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:59:19 -0700 Subject: [PATCH 785/921] fix: unordered mode errors in ml train_test_split (#925) * fix: unordered errors in ml train_test_split * fix tests --- bigframes/ml/model_selection.py | 31 +++++++--------- tests/system/conftest.py | 8 +++++ tests/system/small/ml/test_model_selection.py | 36 +++++++++++-------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index 6220e899aee..a6553d13dc8 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -17,7 +17,6 @@ https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection.""" -import typing from typing import cast, List, Union from bigframes.ml import utils @@ -87,7 +86,7 @@ def _stratify_split(df: bpd.DataFrame, stratify: bpd.Series) -> List[bpd.DataFra merged_df = df.join(stratify.to_frame(), how="outer") train_dfs, test_dfs = [], [] - uniq = stratify.unique() + uniq = stratify.value_counts().index for value in uniq: cur = merged_df[merged_df["bigframes_stratify_col"] == value] train, test = train_test_split( @@ -107,26 +106,20 @@ def _stratify_split(df: bpd.DataFrame, stratify: bpd.Series) -> List[bpd.DataFra ) return [train_df, test_df] + joined_df = dfs[0] + for df in dfs[1:]: + joined_df = joined_df.join(df, how="outer") if stratify is None: - split_dfs = dfs[0]._split( + joined_df_train, joined_df_test = joined_df._split( fracs=(train_size, test_size), random_state=random_state ) else: - split_dfs = _stratify_split(dfs[0], stratify) - train_index = split_dfs[0].index - test_index = split_dfs[1].index - - split_dfs += typing.cast( - List[bpd.DataFrame], - [df.loc[index] for df in dfs[1:] for index in (train_index, test_index)], - ) - - # convert back to Series. - results: List[Union[bpd.DataFrame, bpd.Series]] = [] - for i, array in enumerate(arrays): - if isinstance(array, bpd.Series): - results += utils.convert_to_series(split_dfs[2 * i], split_dfs[2 * i + 1]) - else: - results += (split_dfs[2 * i], split_dfs[2 * i + 1]) + joined_df_train, joined_df_test = _stratify_split(joined_df, stratify) + + results = [] + for array in arrays: + columns = array.name if isinstance(array, bpd.Series) else array.columns + results.append(joined_df_train[columns]) + results.append(joined_df_test[columns]) return results diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 05ff80dc330..b382a5593ce 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -606,6 +606,14 @@ def penguins_df_default_index( return session.read_gbq(penguins_table_id) +@pytest.fixture(scope="session") +def penguins_df_null_index( + penguins_table_id: str, unordered_session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return unordered_session.read_gbq(penguins_table_id) + + @pytest.fixture(scope="session") def time_series_df_default_index( time_series_table_id: str, session: bigframes.Session diff --git a/tests/system/small/ml/test_model_selection.py b/tests/system/small/ml/test_model_selection.py index ea9220feb45..47529565b7f 100644 --- a/tests/system/small/ml/test_model_selection.py +++ b/tests/system/small/ml/test_model_selection.py @@ -19,15 +19,20 @@ import bigframes.pandas as bpd -def test_train_test_split_default_correct_shape(penguins_df_default_index): - X = penguins_df_default_index[ +@pytest.mark.parametrize( + "df_fixture", + ("penguins_df_default_index", "penguins_df_null_index"), +) +def test_train_test_split_default_correct_shape(df_fixture, request): + df = request.getfixturevalue(df_fixture) + X = df[ [ "species", "island", "culmen_length_mm", ] ] - y = penguins_df_default_index[["body_mass_g"]] + y = df[["body_mass_g"]] X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y) # even though the default seed is random, it should always result in this shape @@ -236,17 +241,18 @@ def test_train_test_split_value_error(penguins_df_default_index, train_size, tes ) -def test_train_test_split_stratify(penguins_df_default_index): - X = penguins_df_default_index[ - [ - "species", - "island", - "culmen_length_mm", - ] - ] - y = penguins_df_default_index[["species"]] +@pytest.mark.parametrize( + "df_fixture", + ("penguins_df_default_index", "penguins_df_null_index"), +) +def test_train_test_split_stratify(df_fixture, request): + df = request.getfixturevalue(df_fixture) + X = df[["species", "island", "culmen_length_mm",]].rename( + columns={"species": "x_species"} + ) # Keep "species" col just for easy checking. Rename to avoid conflicts. + y = df[["species"]] X_train, X_test, y_train, y_test = model_selection.train_test_split( - X, y, stratify=penguins_df_default_index["species"] + X, y, stratify=df["species"] ) # Original distribution is [152, 124, 68]. All the categories follow 75/25 split @@ -277,12 +283,12 @@ def test_train_test_split_stratify(penguins_df_default_index): name="count", ) pd.testing.assert_series_equal( - X_train["species"].value_counts().to_pandas(), + X_train["x_species"].rename("species").value_counts().to_pandas(), train_counts, check_index_type=False, ) pd.testing.assert_series_equal( - X_test["species"].value_counts().to_pandas(), + X_test["x_species"].rename("species").value_counts().to_pandas(), test_counts, check_index_type=False, ) From cfd16c1278023bd2c3dce9c0cb378615aa00e58d Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:43:22 -0700 Subject: [PATCH 786/921] docs: add Claude3 ML and RemoteFunc notebooks (#930) --- ...bq_dataframes_llm_claude3_museum_art.ipynb | 1019 ++ notebooks/generative_ai/museum_art.csv | 14930 ++++++++++++++++ .../remote_function_vertex_claude_model.ipynb | 484 + 3 files changed, 16433 insertions(+) create mode 100644 notebooks/generative_ai/bq_dataframes_llm_claude3_museum_art.ipynb create mode 100644 notebooks/generative_ai/museum_art.csv create mode 100644 notebooks/remote_functions/remote_function_vertex_claude_model.ipynb diff --git a/notebooks/generative_ai/bq_dataframes_llm_claude3_museum_art.ipynb b/notebooks/generative_ai/bq_dataframes_llm_claude3_museum_art.ipynb new file mode 100644 index 00000000000..a1bb1e9d89d --- /dev/null +++ b/notebooks/generative_ai/bq_dataframes_llm_claude3_museum_art.ipynb @@ -0,0 +1,1019 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "9A9NkTRTfo2I" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8fK_rdvvx1iZ" + }, + "source": [ + "## Overview\n", + "\n", + "## Objective\n", + "\n", + "This notebook shows how to conecct BigQuery dataset to Claude models on Vertex AI using BigQuery DataFrames.\n", + "\n", + "### Claude on Vertex AI\n", + "\n", + "Anthropic Claude models on Vertex AI offer fully managed and serverless models. To use a Claude model on Vertex AI, send a request directly to the Vertex AI API endpoint.\n", + "\n", + "For more information, see the [Use Claude](https://cloud.devsite.corp.google.com/vertex-ai/generative-ai/docs/third-party-models/use-claude) documentation.\n", + "\n", + "### BigQuery DataFrames\n", + "BigQuery DataFrames provides a Pythonic DataFrame and machine learning (ML) API powered by the BigQuery engine. BigQuery DataFrames is an open-source package.\n", + "\n", + "For more information, see this documentation\n", + "https://cloud.google.com/bigquery/docs/reference/bigquery-dataframes\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nwYvaaW25jYS" + }, + "source": [ + "### Getting Started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hVi8v2mxBkeG" + }, + "source": [ + "#### Authenticate your notebook environment (Colab only)\n", + "If you are running this notebook on Google Colab, uncomment and run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "OHfMDNI76_Pz" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gI3KlxQQ_F_T" + }, + "source": [ + "## Using Anthropic's Vertex SDK + BQ for *Python*" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E0x3GO6M_O3_" + }, + "source": [ + "### Getting Started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_CJrqUvqAfR7" + }, + "source": [ + "#### Install the latest bigframes package if bigframes version < 1.15.0\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "executionInfo": { + "elapsed": 11539, + "status": "ok", + "timestamp": 1724257409246, + "user": { + "displayName": "Annie Xu", + "userId": "11935526703047498014" + }, + "user_tz": 420 + }, + "id": "fi_HLdat_Pce", + "outputId": "020149f0-9fe8-45de-f160-abe488c0bed2" + }, + "outputs": [], + "source": [ + "# !pip install bigframes --upgrade" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hUiAYUFbBCpR" + }, + "source": [ + "#### Restart current runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which will restart the current kernel." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "jcqgcj_DBFgt" + }, + "outputs": [], + "source": [ + "# # Restart kernel after installs so that your environment can access the new packages\n", + "# import sys\n", + "\n", + "# if \"google.colab\" in sys.modules:\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "shZgRl6qbZYP" + }, + "source": [ + "#### Define Google Cloud project and region information" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "JZLqMJ6va9fc" + }, + "outputs": [], + "source": [ + "# Input your project id\n", + "PROJECT_ID = \"bigframes-dev\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "czcmJpKPBMVC" + }, + "source": [ + "#### Select Claude Model and Region Availability:\n", + "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#anthropic_claude_quotas_and_supported_context_length" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "wIBkGcFkK0Ci" + }, + "outputs": [], + "source": [ + "REGION = \"us-east5\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F3UmCLerH0t0" + }, + "source": [ + "### Load raw sample data to a bigquery dataset\n", + "\n", + "Create a BigQuery Dataset and table. You can use the sample museum data in CSV from [here](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks/generative_ai/museum_art.csv).\n", + "\n", + "The dataset should be in the **same region** as your chosen claude model. Let's say you selected us-east5 for claude 'haiku', then load the sample data to a dataset in us-east5." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gijJ2vr5B5nV" + }, + "source": [ + "### Text generation for BQ Tables using Python BigFrames\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "collapsed": true, + "executionInfo": { + "elapsed": 756, + "status": "ok", + "timestamp": 1724260427446, + "user": { + "displayName": "Annie Xu", + "userId": "11935526703047498014" + }, + "user_tz": 420 + }, + "id": "cU3Gq7TqHFdi", + "outputId": "aa5ec159-a91b-4349-e56a-400e90935edc" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "object_number string[pyarrow]\n", + "is_highlight boolean\n", + "is_public_domain boolean\n", + "object_id Int64\n", + "department string[pyarrow]\n", + "object_name string[pyarrow]\n", + "title string[pyarrow]\n", + "culture string[pyarrow]\n", + "period string[pyarrow]\n", + "dynasty string[pyarrow]\n", + "reign string[pyarrow]\n", + "portfolio string[pyarrow]\n", + "artist_role string[pyarrow]\n", + "artist_prefix string[pyarrow]\n", + "artist_display_name string[pyarrow]\n", + "artist_display_bio string[pyarrow]\n", + "artist_suffix string[pyarrow]\n", + "artist_alpha_sort string[pyarrow]\n", + "artist_nationality string[pyarrow]\n", + "artist_begin_date string[pyarrow]\n", + "artist_end_date string[pyarrow]\n", + "object_date string[pyarrow]\n", + "object_begin_date Int64\n", + "object_end_date Int64\n", + "medium string[pyarrow]\n", + "dimensions string[pyarrow]\n", + "credit_line string[pyarrow]\n", + "geography_type string[pyarrow]\n", + "city string[pyarrow]\n", + "state string[pyarrow]\n", + "county string[pyarrow]\n", + "country string[pyarrow]\n", + "region string[pyarrow]\n", + "subregion string[pyarrow]\n", + "locale string[pyarrow]\n", + "locus string[pyarrow]\n", + "excavation string[pyarrow]\n", + "river string[pyarrow]\n", + "classification string[pyarrow]\n", + "rights_and_reproduction string[pyarrow]\n", + "link_resource string[pyarrow]\n", + "metadata_date timestamp[us, tz=UTC][pyarrow]\n", + "repository string[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import bigframes\n", + "import bigframes.pandas as bpd\n", + "bigframes.options._bigquery_options.project = PROJECT_ID # replace to user project\n", + "bigframes.options._bigquery_options.location = REGION #choice a region which the claude model you choice allows\n", + "df = bpd.read_gbq(\"bigframes-dev.garrettwu_us_east5.museum_art\") # replace with your table\n", + "df.dtypes" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 461 + }, + "executionInfo": { + "elapsed": 4568, + "status": "ok", + "timestamp": 1724271168583, + "user": { + "displayName": "Annie Xu", + "userId": "11935526703047498014" + }, + "user_tz": 420 + }, + "id": "exWNXEzLHHaU", + "outputId": "1b33b64c-c8bd-42e6-ecc3-0ea0b5e492be" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 1998408a-4e29-4381-9229-cf8585a47dbe is DONE. 7.7 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 817a5321-9852-45da-8b14-004affc20c38 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 36aa1b30-acb5-4188-8377-b9f544443db8 is DONE. 955 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
object_idtitle
0285844Addie Card, 12 years. Spinner in North Pownal ...
1437141Portrait of a Man
2670650[Snow Crystal]
3268450Newhaven Fisherman
4646996전(傳) 오원 장승업 (1843–1897) 청동기와 화초가 있는 정물화 조선|傳 吾...
5287958Bridge of Augustus at Nani
6435869Antoine Dominique Sauveur Aubert (born 1817), ...
755834<NA>
845087<NA>
956883<NA>
\n", + "

10 rows × 2 columns

\n", + "
[10 rows x 2 columns in total]" + ], + "text/plain": [ + " object_id title\n", + "0 285844 Addie Card, 12 years. Spinner in North Pownal ...\n", + "1 437141 Portrait of a Man\n", + "2 670650 [Snow Crystal]\n", + "3 268450 Newhaven Fisherman\n", + "4 646996 전(傳) 오원 장승업 (1843–1897) 청동기와 화초가 있는 정물화 조선|傳 吾...\n", + "5 287958 Bridge of Augustus at Nani\n", + "6 435869 Antoine Dominique Sauveur Aubert (born 1817), ...\n", + "7 55834 \n", + "8 45087 \n", + "9 56883 \n", + "\n", + "[10 rows x 2 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# @title query: select top 10 records from table and put into dataframe\n", + "\n", + "df = df[[\"object_id\", \"title\"]].head(10)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_UZNsP_WDlyr" + }, + "source": [ + "### Enable Claude model on Vertex AI and Create a BQ External Model Connection\n", + "\n", + "\n", + "* Step 1: Visit the Vertex AI Model Garden console and select the model tile for Claude model of your choice. Following this doc [link](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude). Click on the **“Enable”** button and follow the instructions.\n", + "\n", + "* Step 2: Create a BQ External Connection\n", + "Follow the same process like this one: [link](https://cloud.google.com/bigquery/docs/generate-text#create_a_connection). Pay attention to the **supported region** of Claude models and make your conenction follow the same region for example us-east5 for Claude 3.5.\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8Q4aff5m9QOn" + }, + "source": [ + "### Use BigQuery DataFrames ML package with Claude LLM \n", + "\n", + "In this example, we are using the Claude3TextGenerator class from BigQuery DataFrames to translate title of art piece to english.\n", + "\n", + "Documentation for the Claude3TextGenerator Class: https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.Claude3TextGenerator" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 513 + }, + "executionInfo": { + "elapsed": 25662, + "status": "ok", + "timestamp": 1724271197922, + "user": { + "displayName": "Annie Xu", + "userId": "11935526703047498014" + }, + "user_tz": 420 + }, + "id": "1pdyI5KBTyTD", + "outputId": "8f1e976b-1fd0-49ba-e068-f480eafb1765" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 514f5afe-15e0-4474-9e09-fbf94f0fe8ca is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5d4df544-e8a4-42f3-8a94-5f7e79b23562 is DONE. 635 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 25288d94-b10c-4b39-a272-3969ccb19af3 is DONE. 14 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d5693878-1037-4798-8aa0-f568ec0be9e3 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 54080328-ba8b-4715-bf2b-3e5b7affa90b is DONE. 4.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ml_generate_text_llm_resultml_generate_text_statusprompt
0This text is already in English. It appears to...translate this into English: Addie Card, 12 ye...
1The phrase \"Portrait of a Man\" is already in E...translate this into English: Portrait of a Man
2The phrase \"[Snow Crystal]\" is already in Engl...translate this into English: [Snow Crystal]
3The phrase \"Newhaven Fisherman\" is already in ...translate this into English: Newhaven Fisherman
4Here's the English translation:\n", + "\n", + "\"Attributed t...translate this into English: 전(傳) 오원 장승업 (1843...
5I apologize, but I'm not sure which language \"...translate this into English: Bridge of Augustu...
6This title is already in English. It describes...translate this into English: Antoine Dominique...
7<NA><NA><NA>
8<NA><NA><NA>
9<NA><NA><NA>
\n", + "

10 rows × 3 columns

\n", + "
[10 rows x 3 columns in total]" + ], + "text/plain": [ + " ml_generate_text_llm_result ml_generate_text_status \\\n", + "0 This text is already in English. It appears to... \n", + "1 The phrase \"Portrait of a Man\" is already in E... \n", + "2 The phrase \"[Snow Crystal]\" is already in Engl... \n", + "3 The phrase \"Newhaven Fisherman\" is already in ... \n", + "4 Here's the English translation:\n", + "\n", + "\"Attributed t... \n", + "5 I apologize, but I'm not sure which language \"... \n", + "6 This title is already in English. It describes... \n", + "7 \n", + "8 \n", + "9 \n", + "\n", + " prompt \n", + "0 translate this into English: Addie Card, 12 ye... \n", + "1 translate this into English: Portrait of a Man \n", + "2 translate this into English: [Snow Crystal] \n", + "3 translate this into English: Newhaven Fisherman \n", + "4 translate this into English: 전(傳) 오원 장승업 (1843... \n", + "5 translate this into English: Bridge of Augustu... \n", + "6 translate this into English: Antoine Dominique... \n", + "7 \n", + "8 \n", + "9 \n", + "\n", + "[10 rows x 3 columns]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from bigframes.ml import llm\n", + "model = llm.Claude3TextGenerator(model_name=\"claude-3-5-sonnet\",\n", + " connection_name=\"bigframes-dev.us-east5.bigframes-rf-conn\" ) # replace with your connection\n", + "df[\"input_prompt\"] = \"translate this into English: \" + df[\"title\"]\n", + "result = model.predict(df[\"input_prompt\"])\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 461 + }, + "executionInfo": { + "elapsed": 5249, + "status": "ok", + "timestamp": 1724274172557, + "user": { + "displayName": "Annie Xu", + "userId": "11935526703047498014" + }, + "user_tz": 420 + }, + "id": "Ux1VI5qujHOB", + "outputId": "7b859943-5e7c-4cc0-d9c2-bb3d44682010" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 6b6eceaa-e713-493e-beac-481a3d777a5c is DONE. 4.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5c660da9-318c-424e-9412-43f09e44a8b3 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 82b61007-8370-4514-addb-258d7c48d66c is DONE. 4.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
object_idtitleml_generate_text_llm_resultprompt
0285844Addie Card, 12 years. Spinner in North Pownal ...This text is already in English. It appears to...translate this into English: Addie Card, 12 ye...
1437141Portrait of a ManThe phrase \"Portrait of a Man\" is already in E...translate this into English: Portrait of a Man
2670650[Snow Crystal]The phrase \"[Snow Crystal]\" is already in Engl...translate this into English: [Snow Crystal]
3268450Newhaven FishermanThe phrase \"Newhaven Fisherman\" is already in ...translate this into English: Newhaven Fisherman
4646996전(傳) 오원 장승업 (1843–1897) 청동기와 화초가 있는 정물화 조선|傳 吾...Here's the English translation:\n", + "\n", + "\"Attributed t...translate this into English: 전(傳) 오원 장승업 (1843...
5287958Bridge of Augustus at NaniI apologize, but I'm not sure which language \"...translate this into English: Bridge of Augustu...
6435869Antoine Dominique Sauveur Aubert (born 1817), ...This title is already in English. It describes...translate this into English: Antoine Dominique...
755834<NA><NA><NA>
845087<NA><NA><NA>
956883<NA><NA><NA>
\n", + "

10 rows × 4 columns

\n", + "
[10 rows x 4 columns in total]" + ], + "text/plain": [ + " object_id title \\\n", + "0 285844 Addie Card, 12 years. Spinner in North Pownal ... \n", + "1 437141 Portrait of a Man \n", + "2 670650 [Snow Crystal] \n", + "3 268450 Newhaven Fisherman \n", + "4 646996 전(傳) 오원 장승업 (1843–1897) 청동기와 화초가 있는 정물화 조선|傳 吾... \n", + "5 287958 Bridge of Augustus at Nani \n", + "6 435869 Antoine Dominique Sauveur Aubert (born 1817), ... \n", + "7 55834 \n", + "8 45087 \n", + "9 56883 \n", + "\n", + " ml_generate_text_llm_result \\\n", + "0 This text is already in English. It appears to... \n", + "1 The phrase \"Portrait of a Man\" is already in E... \n", + "2 The phrase \"[Snow Crystal]\" is already in Engl... \n", + "3 The phrase \"Newhaven Fisherman\" is already in ... \n", + "4 Here's the English translation:\n", + "\n", + "\"Attributed t... \n", + "5 I apologize, but I'm not sure which language \"... \n", + "6 This title is already in English. It describes... \n", + "7 \n", + "8 \n", + "9 \n", + "\n", + " prompt \n", + "0 translate this into English: Addie Card, 12 ye... \n", + "1 translate this into English: Portrait of a Man \n", + "2 translate this into English: [Snow Crystal] \n", + "3 translate this into English: Newhaven Fisherman \n", + "4 translate this into English: 전(傳) 오원 장승업 (1843... \n", + "5 translate this into English: Bridge of Augustu... \n", + "6 translate this into English: Antoine Dominique... \n", + "7 \n", + "8 \n", + "9 \n", + "\n", + "[10 rows x 4 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "output_df=df.drop(columns=[\"input_prompt\"]).join(result.drop(columns=\"ml_generate_text_status\"))\n", + "output_df" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "ej70vFMvelsg" + }, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 8c3f1d21-9033-4224-b6f3-4f2414f4ed18 is DONE. 4.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'bigframes-dev.garrettwu_us_east5.museum_art_translate'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# prompt: load the dataframe output to another Bigquery table\n", + "\n", + "# @title Save results to BigQuery\n", + "\n", + "output_df.to_gbq(\"bigframes-dev.garrettwu_us_east5.museum_art_translate\", if_exists=\"replace\") # replace with your table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/generative_ai/museum_art.csv b/notebooks/generative_ai/museum_art.csv new file mode 100644 index 00000000000..37acae65d26 --- /dev/null +++ b/notebooks/generative_ai/museum_art.csv @@ -0,0 +1,14930 @@ +object_number,is_highlight,is_public_domain,object_id,department,object_name,title,culture,period,dynasty,reign,portfolio,artist_role,artist_prefix,artist_display_name,artist_display_bio,artist_suffix,artist_alpha_sort,artist_nationality,artist_begin_date,artist_end_date,object_date,object_begin_date,object_end_date,medium,dimensions,credit_line,geography_type,city,state,county,country,region,subregion,locale,locus,excavation,river,classification,rights_and_reproduction,link_resource,metadata_date,repository +2014.247,false,true,646996,Asian Art,Screen,전(傳) 오원 장승업 (1843–1897) 청동기와 화초가 있는 정물화 조선|傳 吾園 張承業 器皿折枝圖 朝鮮|Still life with bronze vessels and flowering plants,Korea,Joseon dynasty (1392–1910),,,,Artist,Attributed to,Jang Seung-eop (pen name: Owon),"Korean, 1843–1897",,Jang Seung-eop,Korean,1843,1897,1894,1894,1894,Ten-panel folding screen; ink on paper,Overall: 77 in. × 14 ft. 2 in. (195.6 × 431.8 cm),"Gift of Mrs. Anita H. Berger, in memory of Ambassador Samuel D. Berger, 2014",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/646996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.329,false,true,77916,Asian Art,Hanging scroll,석지 채용신 학자 초상|石芝 蔡龍臣 學者肖像|Portrait of a scholar,Korea,,,,,Artist,,Chae Yongsin (artist name: Seokji) (ASA),"Korean, 1850–1941",,Chae Yongsin,Korean,1850,1941,dated by inscription to 1924,1924,1924,Hanging scroll; ink and color on silk,Image: 38 1/8 × 21 1/8 in. (96.8 × 53.7 cm) Overall with mounting: 48 × 24 7/16 in. (121.9 × 62.1 cm) Overall with knobs: 48 × 24 3/4 in. (121.9 × 62.9 cm),"Purchase, Friends of Asian Art Gifts, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.377,false,true,38009,Asian Art,Painting,,"India (Himachal Pradesh, Jasrota)",,,,,Artist,Attributed to,Nainsukh,active ca. 1735–78,,Nainsukh,Indian,1725,1778,ca. 1745–50,1735,1760,"Ink, opaque watercolor, and gold on paper",Overall: 7 3/4 x 6 1/8 in. (19.7 x 15.6 cm),"Rogers Fund, 1994",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/38009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.424.13,false,true,37988,Asian Art,Folio,,"India (Punjab Hills, Guler)",,,,,Artist,Workshop active in the generation after,Nainsukh,active ca. 1735–78,,Nainsukh,Indian,1725,1778,ca. 1790,1780,1800,Ink and opaque watercolor on paper,9 15/16 x 13 15/16 in. (25.2 x 35.4 cm),"Gift of Cynthia Hazen Polsky, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.13,false,true,74660,Asian Art,Painting,,"India (Himachal Pradesh, Guler)",,,,,Artist,Attributed to a first-generation master after,Nainsukh,active ca. 1735–78,,Nainsukh,Indian,1725,1778,ca. 1780,1770,1790,Charcoal and opaque watercolor on paper,Image (sight): 5 3/8 x 7 5/8 in. (13.7 x 19.4 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.22,false,true,74674,Asian Art,Painting,,"India (Himachal Pradesh, Guler)",,,,,Artist,Attributed to a first-generation master after,Nainsukh,active ca. 1735–78,,Nainsukh,Indian,1725,1778,ca. 1780,1770,1790,"Ink, ocher and underdrawing",Image (sight): 8 3/4 x 11 3/4 in. (22.2 x 29.8 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.23,false,true,74675,Asian Art,Drawing,,"India (Pahari Hills, Guler or Kangra)",,,,,Artist,Attributed to a first-generation master after,Nainsukh,active ca. 1735–78,,Nainsukh,Indian,1725,1778,ca. 1775–80,1775,1780,Red ochre and wash on paper,8 3/8 x 11 3/8 in. (21.3 x 28.9 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.24,false,true,74677,Asian Art,Painting,,"India (Pahari Hills, Guler or Kangra)",,,,,Artist,Attributed to a follower of,Nainsukh,active ca. 1735–78,,Nainsukh,Indian,1725,1778,ca. 1780,1770,1790,Ink and wash on paper,Image (sight): 10 3/8 x 9 in. (26.4 x 22.9 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.30,false,true,74685,Asian Art,Painting,,"India (Pahari Hills, Kangra)",,,,,Artist,Attributed to,Nainsukh,active ca. 1735–78,,Nainsukh,Indian,1725,1778,ca. 1775–80,1765,1790,Ink and transparent watercolor on paper,Image (sight): 6 5/8 x 10 1/4 in. (16.8 x 26 cm) Framed: 16 x 20 in. (40.6 x 50.8 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.375,false,true,65595,Asian Art,Painting,,"India (Rajasthan, Mewar)",,,,,Artist,,Chokha,"Indian, active 1799–ca. 1826",,Chokha,Indian,1799,1826,ca. 1820,1810,1830,"Ink, opaque watercolor, silver, and gold on paper",8 1/4 x 13 1/4 in. (21 x 33.7 cm),"Cynthia Hazen Polsky and Leon B. Polsky Fund, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.451,false,true,73261,Asian Art,Painting,,"India (Rajasthan, Mewar)",,,,,Artist,Attributed to,Chokha,"Indian, active 1799–ca. 1826",,Chokha,Indian,1799,1826,ca. 1800–10,1800,1810,"Opaque watercolor, ink and gold on paper",Image: 11 1/2 x 14 7/8 in. (29.2 x 37.8 cm) Page: 12 3/16 x 15 15/16 in. (31 x 40.5 cm),"Purchase, Friends of Asian Art Gifts, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.100.5,false,true,38042,Asian Art,Painting,,"Western India, Rajasthan, Udaipur or Devgarh",,,,,Artist,Attributed to,Chokha,"Indian, active 1799–ca. 1826",,Chokha,Indian,1799,1826,ca. 1805–10,1795,1820,"Ink, opaque watercolor, and gold on paper",15 x 16 1/8 in. (38.1 x 41 cm),"Fletcher Fund, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/38042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.161,false,true,39894,Asian Art,Painting,,"India (Udaipur, Rajasthan)",,,,,Artist,Attributed to,Stipple Master,"Indian, active ca. 1690–1715",,Stipple Master,Indian,1680,1725,ca. 1707–8,1697,1718,"Opaque watercolor, ink and gold on paper",Page: 18 7/8 x 14 7/8 in. (47.9 x 37.8 cm) Image: 9 1/2 x 7 5/16 in. (24.1 x 18.6 cm),"Friends of Asian Art, Purchase, Mrs. Vincent Astor Gift, 1998",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.177,false,true,64891,Asian Art,Painting,,"Western India, Rajasthan, Udaipur",,,,,Artist,Attributed to,Stipple Master,"Indian, active ca. 1690–1715",,Stipple Master,Indian,1680,1725,ca. 1700–1710,1690,1720,Opaque watercolor and ink on paper,Page: 14 11/16 x 12 1/8 in. (37.3 x 30.8 cm) Image: 13 3/16 x 10 3/4 in. (33.5 x 27.3 cm),"Cynthia Hazen Polsky and Leon B. Polsky Fund, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/64891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.238,false,true,65590,Asian Art,Painting,,"India (Rajasthan, Mewar)",,,,,Artist,,Stipple Master,"Indian, active ca. 1690–1715",,Stipple Master,Indian,1680,1725,ca. 1705,1695,1715,"Ink, opaque watercolor, silver, and gold on paper",18 x 13 in. (45.7 x 33 cm),"Cynthia Hazen Polsky and Leon B. Polsky Fund, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2003.178a, b",false,true,65594,Asian Art,Painting,,"India (Rajasthan, Kota)",,,,,Artist,,The Kota Master,"Indian, active early 18th century",,Kota Master,Indian,1700,1733,ca. 1720 (recto); ca. 1750–75 (verso),1710,1785,"Ink, opaque watercolor, and gold on paper",recto: 7 1/2 x 4 3/8 in. (19.1 x 11.1 cm) verso: 9 x 5 7/8 in. (22.9 x 14.9 cm),"Cynthia Hazen Polsky and Leon B. Polsky Fund, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.230,false,true,40445,Asian Art,Hanging scroll,"이유원, 매화도 조선|李裕元 梅花圖 朝鮮|Plum Branch",Korea,Joseon dynasty (1392–1910),,,,Artist,,Yi Yuwon,"Korean, 1814–1888",,Yi Yuwon,Korean,1814,1888,dated 1888,1888,1888,Hanging scroll; ink on paper,58 1/16 x 37 3/16 in. (147.5 x 94.5 cm),"Purchase, Seymour and Rogers Funds and Bequest of Dorothy Graham Bennett, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.448,false,true,45063,Asian Art,Screen panel,,Korea,Joseon dynasty (1392–1910),,,,Artist,,Nam Kye-u,"Korean, 1811–1888",,Nam Kye-u,Korean,1811,1888,,1392,1910,Panel from a six-panel folding screen; ink and color on paper,36 1/4 x 12 3/16 in. (92.1 x 31 cm),"Anonymous Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.123.1,false,true,40069,Asian Art,Hanging scroll,,China or Korea (?),Joseon dynasty (1392–1910),,,,Artist,In the style of,Muqi,"Chinese, ca. 1210–after 1269",,Muqi,Chinese,1210,1269,,1392,1910,Hanging scroll; ink and color on silk,51 1/4 x 27 1/2 in. (130.2 x 69.9 cm),"Rogers Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.123.2,false,true,40070,Asian Art,Hanging scroll,,China or Korea (?),Joseon dynasty (1392–1910),,,,Artist,In the style of,Muqi,"Chinese, ca. 1210–after 1269",,Muqi,Chinese,1210,1269,,1392,1910,Hanging scroll; ink and color on silk,51 1/2 x 27 1/2 in. (130.8 x 69.9 cm),"Rogers Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.1,false,true,37964,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.2,false,true,75084,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.3,false,true,75085,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.4,false,true,75086,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.5,false,true,75087,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.6,false,true,75088,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.7,false,true,75089,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.8,false,true,75090,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.9,false,true,75091,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.10,false,true,75092,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.11,false,true,75093,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.12,false,true,75094,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.13,false,true,75095,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.14,false,true,75096,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.15,false,true,75097,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.16,false,true,75098,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.17,false,true,75099,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.18,false,true,75100,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.19,false,true,75101,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.20,false,true,75102,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.21,false,true,75103,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.22,false,true,75104,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.23,false,true,75105,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.24,false,true,75106,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.25,false,true,75107,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.26,false,true,75108,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.27,false,true,75109,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.28,false,true,75110,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.29,false,true,75111,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.30,false,true,75112,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.31,false,true,75113,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.32,false,true,75114,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.33,false,true,75115,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.34,false,true,75116,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.35,false,true,75117,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.36,false,true,75118,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.37,false,true,75119,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.38,false,true,75120,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.39,false,true,75121,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.40,false,true,75122,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.41,false,true,75123,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.42,false,true,75124,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.43,false,true,75125,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.44,false,true,75126,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.45,false,true,75127,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.46,false,true,75128,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.47,false,true,75129,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.48,false,true,75130,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.49,false,true,75131,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.50,false,true,75132,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.51,false,true,75133,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.52,false,true,75134,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.53,false,true,75135,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.54,false,true,75136,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.55,false,true,75137,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.56,false,true,75138,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.57,false,true,75139,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.58,false,true,75140,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.59,false,true,75141,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.60,false,true,75142,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.61,false,true,75143,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.63,false,true,75145,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.64,false,true,75146,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.65,false,true,75147,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.66,false,true,75148,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.67,false,true,75149,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.68,false,true,75150,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.69,false,true,75151,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.70,false,true,75152,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",(Average size .1–.71): 4 1/2 x 11 3/8 in. (11.4 x 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.38.71,false,true,75153,Asian Art,Folio,,India (Gujarat),,,,,Artist,,Bhadrabahu,"Indian, died ca. 356 B.C.",,Bhadrabahu,Indian,-0356,-0356,15th century,1400,1499,"Ink, opaque watercolor, and gold on paper",Overall (each): 4 1/2 × 11 3/8 in. (11.4 × 28.9 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.96.4,false,true,54912,Asian Art,Painting,,Korea,Joseon dynasty (1392–1910),,,,Artist,,Zhao Songxue,"Korean, 17th century",,Zhao Songxue,Korean,1600,1699,17th century,1600,1699,Framed painting; ink on silk,23 5/8 x 14 3/8 in. (60 x 36.5 cm),"Rogers Fund, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.123,false,true,38448,Asian Art,Figure,,Tibet,,,,,Artist,Attributed to,Chosying Dorje (the Tenth Karmapa),1604–1674,,Chosying Dorje,Tibetan,1604,1674,17th century,1600,1699,Ivory,H. 6 3/4 in. (17.1 cm); W. 3 3/8 in. (8.6 cm); D. 3 1/2 in. (8.9 cm); Wt. 1 lb (.5 kg),"Louis V. Bell and Dodge Funds, 1972",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/38448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.96.2,false,true,40429,Asian Art,Painting,,Korea,Joseon dynasty (1392–1910),,,,Artist,,Samoje,active late 18th century,,Samoje,Korean,1771,1779,late 18th century,1767,1799,Framed painting; ink and color on silk,17 7/8 x 10 7/8 in. (45.4 x 27.6 cm),"Rogers Fund, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.96.6,false,true,40432,Asian Art,Painting,,Korea,Joseon dynasty (1392–1910),,,,Artist,,Samoje,active late 18th century,,Samoje,Korean,1771,1779,late 18th century,1767,1799,Framed painting; ink and color on silk,17 7/8 x 10 7/8 in. (45.4 x 27.6 cm),"Rogers Fund, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.104,false,true,73188,Asian Art,Hanging scroll,,Korea,Joseon dynasty (1392–1910),,,,Artist,,Kim Sugyu,"Korean, active late 18th–early 19th century",,Kim Sugyu,Korean,1767,1833,late 18th century,1767,1799,Hanging scroll; ink and color on cotton,Image: 10 x 13 3/4 in. (25.4 x 34.9 cm) Overall with mounting: 43 1/2 x 18 7/8 in. (110.5 x 47.9 cm) Overall with knobs: 43 1/2 x 21 1/8 in. (110.5 x 53.7 cm),"Purchase, Friends of Asian Art Gifts, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.29,false,true,74683,Asian Art,Painting,,"India (Pahari Hills, Kangra)",,,,,Artist|Artist,First generation after|Possibly,Nainsukh|Fattu,active ca. 1735–78,,Nainsukh|Fattu,Indian,1725,1778,ca. 1785–90,1785,1790,Ochre on paper,Image (sight): 5 1/2 x 8 1/8 in. (14 x 20.6 cm) Framed: 16 x 20 in. (40.6 x 50.8 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.403,false,true,72589,Asian Art,Painting,,"India (Rajasthan, Mewar)",,,,,Artist|Artist,,Stipple Master|Jai Ram,"Indian, active ca. 1690–1715",(?),Stipple Master|Ram Jai,Indian,1680,1725,ca. 1712,1712,1712,"Ink, opaque watercolor, gold, and Basra pearls on paper",8 1/4 x 7 1/2 in. (21 x 19 cm),"Cynthia Hazen Polsky and Leon B. Polsky Fund, 2004",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.9,false,true,74656,Asian Art,Drawing,,"India (Pahari Hills, Guler)",,,,,Artist|Artist,Attributed to|or attributed to,Pandit Seu|Manaku,Indian|active ca. 1725–60,,Seu Pandit|Manaku,Indian,1715,1770,late 18th century,1767,1799,Ink on paper,Image (sight): 6 1/4 x 5 5/8 in. (15.9 x 14.3 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1977.375.31a, b",false,true,64046,Asian Art,Rubbing,張旭 「肚痛帖」; 懷素 「寄邊衣」詩|Ji bianyi|Letter about a Stomachache (Du tong tie),China,,,,,Calligrapher,,Zhang Xu,"Chinese, ca. 675–759",,Zhang Xu,Chinese,0665,0769,19th century rubbing of a 10th century stone carving,1800,1899,Ink on paper,"a (sheet, pre-conservation): 13 × 20 1/2 in. (33 × 52 cm) b (sheet, pre-conservation): 12 3/16 × 20 1/2 in. (31 × 52 cm)","Seymour and Rogers Funds, 1977",,,,,,,,,,,,Rubbing,,http://www.metmuseum.org/art/collection/search/64046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.380,false,true,39899,Asian Art,Rubbing,東晉 王羲之 十七日帖 十三世紀拓本|On the Seventeenth Day,China,,,,,Calligrapher,,Wang Xizhi,"Chinese, ca. 303–ca. 361",,WANG XIZHI,Chinese,0303,0361,13th century rubbing of a 4th century text,1200,1299,Album of thirty leaves; ink on paper,Each leaf: 9 5/8 x 5 in. (24.4 x 12.7 cm),"Gift of Mr. and Mrs. Wan-go H. C. Weng, 1991",,,,,,,,,,,,Rubbing,,http://www.metmuseum.org/art/collection/search/39899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.22,false,true,36145,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Calligrapher|Calligrapher,,Wang Shu|Yan Zhenqing,"Chinese, 1688–1743|Chinese, 709–785",,Wang Shu|Yan Zhenqing,Chinese|Chinese,1688 |0709,1743 |0785,dated 1729,1729,1729,Handscroll in six sections; ink on paper,Image: 13 7/8 x 282 5/8 in. (35.2 x 717.9 cm) Overall with mounting: 14 1/2 x 457 1/8 in. (36.8 x 1161.1 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1986",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/36145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.255,false,true,49665,Asian Art,Folding fan mounted as an album leaf,近代 丁輔之 荔枝 扇面|Lychees,China,,,,,Artist|Calligrapher,,Ding Fuzhi|Shou Xi,"Chinese, 1879–1946|Chinese",,Ding Fuzhi|Shou Xi,Chinese|Chinese,1879,1946,dated 1941,1941,1941,Folding fan mounted as an album leaf; ink and color on alum paper,7 3/4 x 22 in. (19.7 x 55.9 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.426.1a–l,false,true,36130,Asian Art,Album,清 倣龔賢 十二月令山水圖 冊|Landscapes of the Twelve Months,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,,Gong Xian|Zhu Xia,"Chinese, 1619–1689|Chinese",,GONG XIAN|Zhu Xia,Chinese|Chinese,1619,1689,ca. 1685,1675,1695,Album of twelve painting leaves; ink on paper,11 7/8 x 24 3/4 in. (30.2 x 62.9 cm),"Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.408.1a–j,false,true,36435,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,,Zheng Min|Wang Quan,"Chinese, 1633–1683|Chinese",,Zheng Min|Wang Quan,Chinese|Chinese,1633,1683,dated 1688,1688,1688,Album of ten paintings; ink on paper,Each leaf: 10 1/4 x 7 1/2 in. (26 x 19.1 cm),"Edward Elliott Family Collection; Gift of Douglas Dillon, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.92,false,true,36036,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,Attributed to,Jiang Tingxi|Jiang Wuyang,"Chinese, 1669–1732|Chinese",,Jiang Tingxi|Jiang Wuyang,Chinese|Chinese,1669,1732,dated 1724,1724,1724,Hanging scroll; ink and color on silk,44 1/2 x 23 3/4 in. (113 x 60.3 cm),"Gift of Mrs. Anna Woerishoffer, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.212,false,true,36072,Asian Art,Handscroll,清 程正揆 江山臥遊圖 卷|Dream Landscape,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,,Cheng Zhengkui|Wu Dacheng,"Chinese, 1604–1676|Chinese, 1835–1902",(Frontispiece),Cheng Zhengkui|Wu Dacheng,Chinese|Chinese,1604 |1835,1676 |1902,dated 1674,1600,1750,Handscroll; ink and color on silk,9 1/8 x 69 3/4 in. (23.2 x 177.2 cm),"Gift of Harry Lenart, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.609,false,true,49133,Asian Art,Hanging scroll,清 倣龔賢 雲山隱居圖 軸|Dwelling among Mountains and Clouds,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,Inscribed by,Gong Xian|Gong Xian,"Chinese, 1619–1689|Chinese, 1619–1689",,GONG XIAN|GONG XIAN,Chinese|Chinese,1619 |1619,1689 |1689,dated 1685,1685,1685,Hanging scroll; ink on paper,Image: 128 x 44 1/4 in. (325.1 x 112.4 cm) Overall with mounting: 163 x 55 1/4 in. (414 x 140.3 cm) Overall with knobs: 163 x 59 3/4 in. (414 x 151.8 cm),"Purchase, The Dillon Fund Gift, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.242.16–.21,false,true,65620,Asian Art,Album,清 倣龔賢 山水圖 冊|Landscapes,China,Qing dynasty (1644–1911),,,,Calligrapher|Artist,Inscribed by,Gong Xian|Gong Xian,"Chinese, 1619–1689|Chinese, 1619–1689",,GONG XIAN|GONG XIAN,Chinese|Chinese,1619 |1619,1689 |1689,ca. 1688,1678,1698,Album of six paintings; ink on paper,Each: 8 3/4 x 17 3/8 in. (22.2 x 44.1 cm),"The Sackler Fund, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.13,false,true,40021,Asian Art,Hanging scroll,清 王翬 倣李成雪霽圖 軸 紙本|Snow Clearing: Landscape after Li Cheng,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,Inscribed by,Wang Hui|Wang Hui,"Chinese, 1632–1717|Chinese, 1632–1717",,Wang Hui|Wang Hui,Chinese|Chinese,1632 |1632,1717 |1717,dated 1669,1669,1669,Hanging scroll; ink and color on paper,Image: 44 3/8 x 14 1/8 in. (112.7 x 35.9 cm) Overall with mounting: 89 1/4 x 20 7/8 in. (226.7 x 53 cm) Overall with knobs: 89 1/4 x 24 1/8 in. (226.7 x 61.3 cm),"Ex coll.: C. C. Wang Family, Gift of Mr. and Mrs. Earl Morse, in honor of Professor Wen Fong, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.30,false,true,49453,Asian Art,Hanging scroll,清 王緣 趙之謙肖像 軸|Portrait of Zhao Zhiqian,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,,Wang Yuan|Zhao Zhiqian,"Chinese, active ca. 1862–1908|Chinese, 1829–1884",,Wang Yuan|Zhao Zhiqian,Chinese|Chinese,1862 |1829,1908 |1884,dated 1871,1871,1871,Hanging scroll; ink and color on paper,41 3/4 x 13 1/4 in. (106 x 33.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.134,false,true,49134,Asian Art,Handscroll,清 柳堉 溪山行旅圖 卷|Traveling Amid Streams and Mountains,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,Inscribed by,Liu Yu|Liu Yu,"Chinese, 1620–after 1689|Chinese, 1620–after 1689",,Liu Yu|Liu Yu,Chinese|Chinese,1640 |1640,1700 |1700,dated 1680,1680,1680,Handscroll; ink on paper,10 3/8 x 200 1/4 in. (26.4 x 508.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.665.2a–p,false,true,65075,Asian Art,Album,王翬、楊晉、顧昉、王雲、徐玫 仿古山水圖 冊 紙本|Landscapes after old masters,China,Qing dynasty (1644–1911),,,,Artist|Artist|Artist|Artist|Artist,"leaves a, o, p by|leaves e,f,g,h,i,j by|leaves k,l,m,n by|leaf b by|leaves c, d by",Wang Hui|Yang Jin|Gu Fang|Xu Mei|Wang Yun,"Chinese, 1632–1717|Chinese, 1644–1728|Chinese, active ca. 1690–1720|Chinese, active ca. 1690–1722|Chinese, 1652–after 1735",,Wang Hui|Yang Jin|Gu Fang|Xu Mei|Wang Yun,Chinese,1632 |1644 |1690 |1690 |1652,1717 |1728 |1720 |1722 |1735,dated 1692,1692,1692,Album of sixteen leaves; ink and color on paper,Image (each leaf): 11 x 12 1/8 in. (27.9 x 30.8 cm),"Gift of Marie-Hélène and Guy A. Weill, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.33,false,true,41468,Asian Art,Handscroll,元 吳鎮 蘆灘釣艇圖 卷|Fisherman,China,Yuan dynasty (1271–1368),,,,Artist|Calligrapher,,Wu Zhen|Wu Zhen,"Chinese, 1280–1354|Chinese, 1280–1354",,Wu Zhen|Wu Zhen,Chinese|Chinese,1280 |1280,1354 |1354,ca. 1350,1340,1354,Handscroll; ink on paper,Image: 12 1/4 x 21 3/16 in. (31.1 x 53.8 cm) Overall with mounting: 13 1/4 x 136 1/2 in. (33.7 x 346.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.135,false,true,40507,Asian Art,Handscroll,元 趙孟頫 趙雍 趙麟 吳興趙氏三世人馬圖 卷|Grooms and Horses,China,Yuan dynasty (1271–1368),,,,Artist|Artist|Artist,,Zhao Mengfu|Zhao Yong|Zhao Lin,"Chinese, 1254–1322|Chinese, 1289–after 1360|Chinese, active second half of the 14th century",,Zhao Mengfu|Zhao Yong|ZHAO LIN,Chinese|Chinese|Chinese,1254 |1289 |1350,1322 |1360 |1399,date 1296 and 1359,1296,1359,Handscroll; ink and color on paper,Image: 11 7/8 x 70 1/8 in. (30.2 x 178.1 cm) Overall with mounting: 12 1/4 in. x 29 ft. 2 1/2 in. (31.1 x 890.3 cm),"Gift of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.475.1,false,true,40052,Asian Art,Handscroll,"南宋 馬和之 詩經小雅鴻雁之什六篇圖 卷|Courtly Odes, Beginning with ""Wild Geese""",China,Southern Song dynasty (1127–1279),,,,Artist|Calligrapher,,Ma Hezhi|Emperor Gaozong,"Chinese, ca. 1130–ca. 1170|Chinese, 1107–1187, r. 1127–1162",and Assistants,MA HEZHI|Gaozong Emperor,Chinese|Chinese,1130 |1107,1170 |1187,12th century,1130,1170,Handscroll in six sections; ink and color on silk,Overall with mounting: 12 3/4 in. × 42 ft. 9 3/4 in. (32.4 × 1304.9 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.84,false,true,35995,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Calligrapher,,Wen Congjian,"Chinese, 1574–1648",,Wen Congjian,Chinese,1574,1648,,1575,1644,Folding fan mounted as an album leaf; ink on gold paper,6 1/2 x 20 31/32 in. (16.5 x 53.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.88,false,true,35999,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Calligrapher,,Yang Wencong,"Chinese, 1597–1645/46",,Yang Wencong,Chinese,1597,1646,,1597,1644,Folding fan mounted as an album leaf; ink on paper,6 3/5 x 20 31/32 in. (16.8 x 53.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.94,false,true,36003,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Calligrapher,,Rüan Dacheng,"Chinese, active early 17th century",,Rüan Dacheng,Chinese,1550,1700,,1600,1633,Folding fan mounted as an album leaf; ink on gold paper,7 x 19 3/4 in. (17.8 x 50.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/36003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.62,false,true,45786,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist|Artist,,Wen Zhengming|Wang Shou|Wang Guxiang,"Chinese, 1470–1559|Chinese, active early 16th century|Chinese, 1501–1568",,Wen Zhengming|Wang Shou|Wang Guxiang,Chinese|Chinese|Chinese,1470 |1500 |1501,1559 |1600 |1568,,1470,1559,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 3/8 x 20 1/4 in. (16.2 x 51.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.55,false,true,73669,Asian Art,Hanging scroll,清 查昇 行書七律詩 軸|Poem,China,Qing dynasty (1644–1911),,,,Calligrapher,,Zha Sheng,"Chinese, 1650–1707",,Zha Sheng,Chinese,1650,1707,,1650,1707,Hanging scroll; ink on silk,H. 63 1/4 in. (160.6 cm); W. 18 11/16 in. (47.5 cm),"Purchase, C. C. Wang Gift, 2007",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/73669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.154a–h,false,true,49179,Asian Art,Album,清 石濤(朱若極) 山水圖 冊|Searching for Immortals,China,Qing dynasty (1644–1911),,,,Artist|Calligrapher,Inscribed by,Shitao (Zhu Ruoji)|Shitao (Zhu Ruoji),"Chinese, 1642–1707|Chinese, 1642–1707",,Shitao|Shitao,Chinese|Chinese,1642 |1642,1707 |1707,,1644,1707,Album of eight leaves; ink and color on paper,5 7/8 x 10 3/4 in. (14.9 x 27.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.11,false,true,49226,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist|Artist,In the Style of|Formerly Attributed to,Li Yin|Unidentified Artist|Ma Kui,"Chinese, active second half of the 17th–early 18th century|Chinese, active ca. 1194–1225",,Li Yin|Unidentified Artist|Ma Kui,Chinese|Chinese,1650 |1194,1825 |1225,,1644,1911,Hanging scroll; ink and color on silk,Image: 65 7/8 x 20 7/8 in. (167.3 x 53 cm); Overall with mounting: 107 3/4 x 28 5/8 in. (273.7 x 72.7 cm); Overall with knobs: 107 3/4 x 32 5/8 in. (273.7 x 82.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.120,false,true,51727,Asian Art,Handscroll,,China,Yuan dynasty (1271–1368) (?),,,,Artist|Calligrapher|Artist,Copy after,Unidentified Artist|Guo Zongjiang|Li Gonglin,"Chinese|Chinese, ca. 1041–1106",Chinese|(Frontispiece),Unidentified Artist|Guo Zongjiang|Li Gonglin,Chinese|Chinese,1041,1106,,1271,1368,Handscroll; ink and color on silk,12 5/8 x 184 in. (32.1 x 467.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.93,false,true,51705,Asian Art,Handscroll,,China,Northern Song dynasty (960–1127),,,,Artist|Calligrapher|Artist,Copy after,Unidentified Artist|Emperor Huizong|Han Gan,"Chinese, 1082–1135; r. 1100–25|Chinese, active ca. 742–756",,Unidentified Artist|Huizong Emperor|HAN GAN,Chinese|Chinese,1082 |0742,1082 |0756,,960,1127,Handscroll; ink and color on silk,Image: 12 in. × 20 1/8 in. (30.5 × 51.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.124.6,false,true,51528,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist|Calligrapher|Calligrapher|Calligrapher|Calligrapher|Calligrapher|Calligrapher|Calligrapher|Calligrapher|Calligrapher,Formerly Attributed to,Unidentified Artist|Ni Zan|Li Dongyang|Wang Da|Jin Xüan|Wen Peng|Wen Jia|Zhou Tianqiu|Huang Jishui|Dong Qichang|Wang Zhideng,"Chinese, 1306–1374|Chinese, 1447–1503|Chinese, 14th century|Chinese|Chinese, 1498–1573|Chinese, 1501–1583|Chinese, 1514–1595|Chinese|Chinese, 1555–1636|Chinese, 1535–1612",,Unidentified Artist|NI ZAN|Li Dongyang|Wang Da|Jin Xüan|Wen Peng|Wen Jia|Zhou Tianqiu|Huang Jishui|Dong Qichang|Wang Zhideng,Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese,1306 |1447 |1300 |1498 |1501 |1514 |1555 |1535,1374 |1503 |1399 |1573 |1583 |1595 |1636 |1612,,1368,1911,Handscroll; color on paper,11 x 57 1/2 in. (27.9 x 146.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.33,false,true,51656,Asian Art,Album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist|Artist,In the Style of|Formerly Attributed to,Unidentified Artist|Li Zhaodao|Qiu Ying,"act 670–730|Chinese, ca. 1495–1552",,Unidentified Artist|Li Zhaodao|Qiu Ying,Chinese|Chinese,0670 |1485,0730 |1562,,1368,1911,Album leaf; ink and color on silk,14 1/8 x 10 5/8 in. (35.9 x 27.0 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"47.18.13a, b",false,true,51644,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist|Artist,b) Formerly Attributed to|a) Formerly Attributed to,Unidentified Artist|Fang Congyi|Xia Gui,"Chinese, ca. 1301–after 1378|Chinese, active ca. 1195–1230",,Unidentified Artist|Fang Congyi|Xia Gui,Chinese|Chinese,1301 |1195,1399 |1230,,1368,1911,Two handscrolls; ink on paper,Image (a): 3 5/8 in. × 46 in. (9.2 × 116.8 cm) Image (b): 3 7/8 in. × 35 in. (9.8 × 88.9 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.500.1,false,true,36129,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Calligrapher|Calligrapher,,Wang Jian|Wu Li|Tong Bin|Gao Jian|Zhu Zun|Zhang Shi|Jin Kan|Shen Lang|Xü Fu|Shen Ho,"Chinese, 1609–1677 or 1688|Chinese, 1632–1718|Chinese, 17th century|Chinese, 1634–after 1715|Chinese, 17th century|Chinese, 17th century|Chinese, died 1703|Chinese, 17th century|Chinese, 19th century|Chinese, 17th century",(title page),Wang Jian|WU LI|Tong Bin|Gao Jian|Zhu Zun|Zhang Shi|Jin Kan|Shen Lang|Xü Fu|Shen Ho,Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese,1609 |1632 |1600 |1634 |1600 |1600 |1600 |1800 |1600,1677 |1718 |1699 |1725 |1699 |1699 |1703 |1699 |1899 |1699,17th century,1644,1699,Album of eight painted leaves; ink and color on paper,Each leaf: 9 x 12 1/4 in. (22.9 x 31.1 cm),"Gift of Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.14,false,true,51770,Asian Art,Folding fan,,China,,,,,Artist|Calligrapher,,Giuseppe Castiglione|Wang Yudun,"Italian, Milan 1688–1766 Beijing|Chinese, 1692–1758",,"Castiglione, Giuseppe|Wang Yudun",Italian|Chinese,1688 |1692,1766 |1758,18th century,1700,1766,Folding fan; ink and color on paper,H. (sticks) 12 1/4 in. (31.1 cm); W. (open) 20 in. (50.8 cm),"Rogers Fund, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.375.14a–d,false,true,64029,Asian Art,Rubbing,唐 颜真卿 顏家廟碑 現代拓片 紙本|Yan Family Temple Stele,China,,,,,Artist,,Yan Zhenqing,"Chinese, 709–785",,Yan Zhenqing,Chinese,0709,0785,20th century,1900,1999,20th-century rubbing of a stele dated 780; ink on paper,98 x 52 x 1 3/4 in. (248.9 x 132.1 x 4.4 cm),"Seymour and Rogers Funds, 1977",,,,,,,,,,,,Rubbing,,http://www.metmuseum.org/art/collection/search/64029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CIB8,false,true,60764,Asian Art,Illustrated book,太平山水圖|Landscapes of Taiping Prefecture (Taiping shanshui tu),China,,,,,Artist,,Xiao Yuncong,"Chinese, 1596–1673",,Xiao Yuncong,Chinese,1596,1673,ca. 1650,1640,1660,Woodblock-printed books; ink on paper,H. 8 7/8 in. (22.5 cm); W. 6 in. (15.2 cm); D. 1 in. (2.5 cm),"Rogers Fund, 1924",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/60764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1989.363.205a, b",false,true,51902,Asian Art,Hanging scrolls,,China,,,,,Artist,,Wu Changshuo,"Chinese, 1844–1927",,Wu Changshuo,Chinese,1844,1927,20th century,1900,1927,Pair of hanging scrolls; ink on paper,Image (each scroll): 57 13/16 x 10 1/8 in. (146.8 x 25.7 cm) Overall (each scroll): 68 1/2 x 12 11/16 in. (174 x 32.2 cm) Overall with knobs (each scroll): 68 1/2 x 15 in. (174 x 38.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.375.30a–c,false,true,64045,Asian Art,Rubbing,,China,,,,,Artist,,Wang Xizhi,"Chinese, ca. 303–ca. 361",,WANG XIZHI,Chinese,0303,0361,20th century,1900,1999,Ink on paper,,"Seymour and Rogers Funds, 1977",,,,,,,,,,,,Rubbing,,http://www.metmuseum.org/art/collection/search/64045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.141.1a–i,false,true,36439,Asian Art,Album,唐 傳鍾紹京 楷書靈飛經 冊|Spiritual Flight Sutra,China,Tang dynasty (618–907),,,,Artist,Attributed to,Zhong Shaojing,"Chinese, active ca. 713–41",,Zhong Shaojing,Chinese,0713,0741,ca. 738,728,741,Album of nine leaves; ink on paper,Each leaf: 8 3/16 x 3 1/2 in. (20.8 x 8.9 cm),"Purchase, The Dillon Fund Gift, 1989",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/36439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.28,false,true,40285,Asian Art,Handscroll,南宋 趙孟堅 行書梅竹詩譜 卷|Poems on Painting Plum Blossoms and Bamboo,China,Song dynasty (960–1279),,,,Artist,,Zhao Mengjian,"Chinese, 1199–before 1267",,Zhao Mengjian,Chinese,1199,1267,dated 1260,1260,1260,Handscroll; ink on paper,Image: 13 3/8 in. × 11 ft. 7 in. (34 × 353.1 cm) Overall with mounting: 13 5/8 in. × 40 ft. 5 11/16 in. (34.6 × 1233.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.18,false,true,40107,Asian Art,Fan mounted as an album leaf,南宋 理宗趙昀 行書北宋梅堯臣 依韻和資政侍郎雪後登看山亭詩 團扇|Quatrain on Snow-covered West Lake,China,Song dynasty (960–1279),,,,Artist,,Emperor Lizong,"Chinese, 1205–64, r. 1224–64",,LIZONG EMPEROR,Chinese,1205,1264,ca. 1250–60,1250,1260,Round fan mounted as an album leaf; ink on silk,Image: 9 7/8 × 9 7/8 in. (25.1 × 25.1 cm) Mat: 15 1/2 × 14 1/2 in. (39.4 × 36.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.19,false,true,40180,Asian Art,Fan mounted as an album leaf,,China,Song dynasty (960–1279),,,,Artist,,Emperor Lizong,"Chinese, 1205–64, r. 1224–64",,LIZONG EMPEROR,Chinese,1205,1264,ca. 1260–64,1260,1264,Fan mounted as an album leaf; ink on silk,Image: 9 × 9 5/8 in. (22.9 × 24.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.23a–c,false,true,40194,Asian Art,Fan mounted as an album leaf,南宋 理宗 趙昀 楷書韓翊 《 潮聲山翠》 聯句 團扇|Couplet from a Poem by Han Hong,China,Song dynasty (960–1279),,,,Artist,,Emperor Lizong,"Chinese, 1205–64, r. 1224–64",,LIZONG EMPEROR,Chinese,1205,1264,1261,1261,1261,"Fan mounted as an album leaf; a) ink on silk, b) ink on paper, c) ink on paper",8 3/16 x 8 11/16 in. (20.8 x 22.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.9,false,true,40060,Asian Art,Fan mounted as an album leaf,南宋 孝宗 行楷書池上水邊聯句 團扇|Couplet on pond scenery,China,Song dynasty (960–1279),,,,Artist,Attributed to,Emperor Xiaozong,"Chinese, 1127–1194; r. 1163–89",,Xiaozong Emperor,Chinese,1127,1194,ca. 12th century,1127,1194,Fan mounted as an album leaf; ink on silk,9 x 9 5/8 in. (22.9 x 24.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.6,false,true,40058,Asian Art,Fan mounted as an album leaf,南宋 高宗 草書天山陰雨七絕詩 團扇|Quatrain on Heavenly Mountain,China,Song dynasty (960–1279),,,,Artist,,Emperor Gaozong,"Chinese, 1107–1187, r. 1127–1162",,Gaozong Emperor,Chinese,1107,1187,after 1162,1162,1187,Fan mounted as album leaf; ink on silk,9 1/4 x 9 5/8 in. (23.5 x 24.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.47,false,true,45684,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,dated 1493,1493,1493,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 1/2 x 18 1/2 in. (16.5 x 47 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.48,false,true,45746,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wu Kuan,"Chinese, 1435–1504",,Wu Kuan,Chinese,1435,1504,ca. 1498,1488,1508,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 3/4 x 19 in. (17.1 x 49.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.438.2,false,true,39766,Asian Art,Hanging scroll,明 王鏊 贈別詩 軸|Farewell Poem,China,Ming dynasty (1368–1644),,,,Artist,,Wang Ao,"Chinese, 1450–1524",,Wang Ao,Chinese,1450,1524,dated 1498,1498,1498,Hanging scroll; ink on paper,Image: 81 1/2 x 25 in. (207 x 63.5 cm) Overall: 130 x 34 1/2 in. (330.2 x 87.6 cm) Overall with rollers: 130 x 36 1/2 in. (330.2 x 92.7 cm),"The C. C. Wang Family Collection, Gift of C. C. Wang, 1997",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/39766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.51,false,true,45749,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Zhu Yunming,"Chinese, 1461–1527",,Zhu Yunming,Chinese,1461,1527,dated 1507,1507,1507,Handscroll; ink on gold-flecked paper,Image: 12 7/8 x 371 1/2 in. (32.7 x 943.6 cm) Overall with mounting: 14 5/8 x 463 1/8 in. (37.1 x 1176.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.394.2,false,true,44574,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,dated 1522,1522,1522,Folding fan mounted as an album leaf; ink on gold-flecked paper,Image: 6 13/16 x 19 5/8 in. (17.3 x 49.8 cm),"Edward Elliott Family Collection, Douglas Dillon Gift, 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/44574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.59,false,true,45775,Asian Art,Album leaf,"明 文徵明 致丈人吳愈書 冊頁|Letter to the Artist's Father-in-law, Wu Yu",China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,ca. 1506–1510,1506,1510,Album leaf; ink on patterned paper,9 x 12 1/8 in. (22.9 x 30.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.63,false,true,45780,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,dated 1544 and 1547,1544,1547,Handscroll; ink on paper,Image: 9 1/16 x 46 3/8 in. (23 x 117.8 cm) Overall with mounting: 9 5/16 x 288 1/16 in. (23.7 x 731.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.229a–ff,false,true,51865,Asian Art,Album,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,dated 1543,1543,1543,Album of thirty-two pages; ink on paper,H. 13 1/2 in. (34.3 cm); W. 10 3/9 in. (26.2 cm) Album: H. 15 7/8 in. (40.3 cm); W. 10 7/8 in. (27.6 cm); D. 1 in. (2.5 cm),"Gift of John M. Crawford Jr., 1982",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.166,false,true,77914,Asian Art,Hanging scroll mounted as panel,文彭 行草七律 水墨絹本 鏡框|Poem on Promulgating the Almanac at New Year’s,China,Ming dynasty (1368–1644),,,,Artist,,Wen Peng,"Chinese, 1498–1573",,Wen Peng,Chinese,1498,1573,undated,1498,1573,Hanging scroll remounted as a panel; ink on paper,Image: 63 1/4 x 30 1/2 in. (160.7 x 77.5 cm) Overall with mounting: 77 3/4 x 40 1/4 in. (197.5 x 102.2 cm),"Purchase, Friends of Asian Art Gifts, 2012",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/77914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.70,false,true,48875,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Peng,"Chinese, 1498–1573",,Wen Peng,Chinese,1498,1573,dated 1567,1567,1567,Folding fan mounted as an album leaf; ink on gold paper,6 x 19 in. (15.2 x 48.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.500.3,false,true,48955,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,ca. 1622–25,1612,1635,Hanging scroll; ink on silk,Image: 64 7/8 x 19 5/8 in. (164.8 x 49.8 cm) Overall with mounting: 98 1/2 x 25 5/8 in. (250.2 x 65.1 cm) Overall with knobs: 98 1/2 x 29 5/8 in. (250.2 x 75.2 cm),"Gift of Douglas Dillon, 1979",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.100,false,true,48954,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,after 1632,1632,1636,Hanging scroll; ink on paper,Image: 74 3/8 x 29 1/4 in. (188.9 x 74.3 cm) Overall: 103 3/4 x 34 1/4 in. (263.5 x 87 cm) Overall with knobs: 103 3/4 x 38 in. (263.5 x 96.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.115,false,true,49025,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wang Duo,"Chinese, 1592–1652",,Wang Duo,Chinese,1592,1652,dated 1637,1637,1637,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 5/8 x 20 3/4 in. (16.8 x 52.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.116,false,true,49029,Asian Art,Hanging scroll,明/清 王鐸 行草書日暮孤亭五律詩 軸|Poem on a Riverside Pavilion,China,Ming dynasty (1368–1644),,,,Artist,,Wang Duo,"Chinese, 1592–1652",,Wang Duo,Chinese,1592,1652,dated 1641,1641,1641,Hanging scroll; ink on paper,Image: 138 1/4 x 29 1/4 in. (351.2 x 74.3 cm) Overall: 169 1/2 x 39 in. (430.5 x 99.1 cm) Overall with knobs: 169 1/2 x 43 1/2 in. (430.5 x 110.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.93,false,true,56553,Asian Art,Qin,,China,Ming dynasty (1368–1644),,,,Artist,,Prince Lu,"Chinese, 1628–1644",,Prince Lu,Chinese,1628,1644,1634,1634,1634,"Wood, lacquer, jade, silk strings",W. 10 1/2 in. (26.6 cm); D. 4 1/4 in. (11 cm); L. 46 5/8 in. (118.5 cm),"Purchase, Clara Mertens Bequest, in memory of André Mertens, Seymour Fund, The Boston Foundation Gift, Gift of Elizabeth M. Riley, by exchange, and funds from various donors, 1999",,,,,,,,,,,,Musical instruments,,http://www.metmuseum.org/art/collection/search/56553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.201,false,true,41780,Asian Art,Ink tablet,,China,Ming dynasty (1368–1644),,,,Artist,Workshop of,Fang Yulu,active ca. 1570–1619,,Fang Yulu,Chinese,1570,1619,dated 1576,1576,1576,Pine soot and binding medium,Diam. 3 1/2 in. (8.9 cm),"Rogers Fund, 1929",,,,,,,,,,,,Ink,,http://www.metmuseum.org/art/collection/search/41780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.208,false,true,39625,Asian Art,Brush holder,晚明 張希黃 留青竹刻 《醉翁亭記》 詩意圖筆筒|Brush holder with “Ode to the Pavilion of the Inebriated Old Man”,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Xihuang,active early 17th century,,Zhang Xihuang,Chinese,1600,1633,early 17th century,1600,1633,Bamboo,H. 5 1/4 in. (13.4 cm),"Purchase, Mr. and Mrs. John A. Wiley Gift, Seymour Fund, Bequest of Dorothy Graham Bennett and Erich O. Grunebaum Bequest, 1994",,,,,,,,,,,,Bamboo,,http://www.metmuseum.org/art/collection/search/39625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.87,false,true,48931,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Liu Xiang,"Chinese, active mid-17th century",,Liu Xiang,Chinese,1636,1670,16th–17th century,1500,1699,Hanging scroll; ink on paper,Image: 38 9/16 x 10 3/16 in. (97.9 x 25.9 cm) Overall: 72 1/4 x 18 3/16 in. (183.5 x 46.2 cm) Overall with knobs: 72 1/4 x 21 1/4 in. (183.5 x 54 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.81,false,true,48928,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Xue Mingyi,"Chinese, active ca. 1538–after 1597",,Xue Mingyi,Chinese,1538,1598,dated 1597,1597,1597,Folding fan mounted as an album leaf; ink on gold-flecked paper,5 3/4 x 18 in. (14.6 x 45.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.72,false,true,48959,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,After,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,17th century or later,1600,1911,Folding fan mounted as an album leaf; ink on gold paper,6 11/16 x 20 1/2 in. (17.0 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.130,false,true,49129,Asian Art,Handscroll,清 法若真 行草書畫說 卷|Discourse on Painting,China,Qing dynasty (1644–1911),,,,Artist,,Fa Ruozhen,"Chinese, 1613–1696",,Fa Ruozhen,Chinese,1613,1696,dated 1667,1667,1667,Handscroll; ink on paper,12 1/4 x 144 1/2 in. (31.1 x 367 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.139,false,true,49146,Asian Art,Folding fan mounted as an album leaf,清 朱耷 (八大山人) 李治書 扇頁|Letter by Li Zhi,China,Qing dynasty (1644–1911),,,,Artist,,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,dated 1702,1702,1702,Folding fan mounted as an album leaf; ink on paper,6 3/4 x 19 1/2 in. (17.1 x 49.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.458a–e,false,true,49147,Asian Art,Album,明/清 朱耷(八大山人) 致方士琯書 冊 紙本|Letters to Fang Shiguan,China,Qing dynasty (1644–1911),,,,Artist,,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,datable to ca. 1688–1705,1688,1705,Album of ten leaves; ink on patterned and plain paper,"Image (leaf a, right letter): 7 3/4 x 5 7/16 in. (19.7 x 13.8 cm) Image (leaf a, left letter): 7 1/4 x 4 9/16 in. (18.4 x 11.6 cm) Image (leaf b, right letter): 7 1/2 x 5 3/16 in. (19.1 x 13.2 cm) Image (leaf b, left letter): 7 x 4 9/16 in. (17.8 x 11.6 cm) Image (leaf c, right letter): 7 1/2 x 5 3/16 in. (19.1 x 13.2 cm) Image (leaf c, left letter): 7 1/4 x 4 9/16 in. (18.4 x 11.6 cm) Image (leaf d, right letter): 7 1/2 x 5 3/16 in. (19.1 x 13.2 cm) Image (leaf d, left letter): 7 1/4 x 4 9/16 in. (18.4 x 11.6 cm) Image (leaf e, right letter): 7 5/8 x 5 3/16 in. (19.4 x 13.2 cm) Image (leaf e, left letter): 7 1/4 x 4 3/4 in. (18.4 x 12.1 cm)","Gift of John M. Crawford Jr., 1982",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.147,false,true,49163,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,dated 1680,1680,1680,Folding fan mounted as an album leaf; ink on paper,6 11/16 x 20 in. (17 x 50.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.196,false,true,51894,Asian Art,Album,"清 金農 論畫雜詩 冊|Poems on Paintings, Written for Ma Yueguan",China,Qing dynasty (1644–1911),,,,Artist,,Jin Nong,"Chinese, 1687–1773",,Jin Nong,Chinese,1687,1773,dated 1754,1754,1754,Album of eleven double leaves; ink on paper,Image: 6 5/8 x 10 3/8 in. (16.8 x 26.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.531,false,true,36132,Asian Art,Hand scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Liu Yong,"Chinese, 1719–1805",,Liu Yong,Chinese,1719,1805,dated 1803,1803,1803,Hand scroll; ink on paper,30 3/4 x 95 1/2 in. (78.1 x 242.6 cm),"Gift of Dr. and Mrs. George Fan, 1980",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/36132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.203,false,true,51900,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Wang Wenzhi,"Chinese, 1730–1802",,Wang Wenzhi,Chinese,1730,1802,dated 1801,1801,1801,Hanging scroll; ink on paper,Image: 65 3/4 x 18 1/2 in. (167 x 47 cm) Overall: 85 x 23 3/4 in. (215.9 x 60.3 cm) Overall with knobs: 85 x 26 1/2 in. (215.9 x 67.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.327a–d,false,true,75814,Asian Art,Hanging scrolls,吳煕載 篆書詩經南山有臺|Nutgrass Grows on the Southern Hills,China,Qing dynasty (1644–1911),,,,Artist,,Wu Xizai,"Chinese, 1799–1870",,Wu Xizai,Chinese,1799,1870,before 1862,1799,1862,Set of four hanging scrolls; ink on paper,each: 12 3/16 x 51 9/16 in. (31 x 131 cm) Overall with mounting (each): 16 1/8 x 64 15/16 in. (41 x 165 cm),"Gift of Judith G. and F Randall Smith, in honor of Wen C. Fong, 2010",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/75814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.514.50,false,true,54308,Asian Art,Panel,清 趙之謙 隸書引首 卷|The Lingshouhua Studio,China,Qing dynasty (1644–1911),,,,Artist,,Zhao Zhiqian,"Chinese, 1829–1884",,Zhao Zhiqian,Chinese,1829,1884,1863,1863,1863,Horizontal panel; ink on paper,13 x 40 3/4 in. (33 x 103.5 cm),"Gift of Shou-cheng Zhang and Xiu-ping Loh Zhang, 2000",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/54308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2000.345.1, .2",false,true,55495,Asian Art,Hanging scrolls,清 趙之謙 篆書五言 對聯|Couplet,China,Qing dynasty (1644–1911),,,,Artist,,Zhao Zhiqian,"Chinese, 1829–1884",,Zhao Zhiqian,Chinese,1829,1884,dated 1867,1867,1867,Pair of hanging scrolls; ink on paper,Image (each): 71 5/8 x 18 15/16 in. (181.9 x 48.1 cm) Overall with mounting (each): 92 1/2 x 23 1/2 in. (235 x 59.7 cm),"Gift of Judith G. and F Randall Smith, in honor of Maxwell K. Hearn, 2000",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/55495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.165,false,true,49473,Asian Art,Hanging scroll,清 翁同龢 軸|Tiger Calligraphy,China,Qing dynasty (1644–1911),,,,Artist,,Weng Tonghe,"Chinese, 1830–1904",,Weng Tonghe,Chinese,1830,1904,dated 1890,1890,1890,Hanging scroll; ink on silver-flecked red paper,Image: 51 x 26 1/2 in. (129.5 x 67.3 cm) Overall: 83 1/2 x 31 3/4 in. (212.1 x 80.6 cm) Overall with knobs: 83 1/2 x 35 1/8 in. (212.1 x 89.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.69,false,true,35987,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Yu Dongru,"Chinese, active Ming dynasty",,Yu Dongru,Chinese,1350,1650,late 17th century,1667,1699,Folding fan mounted as an album leaf; ink on paper,6 5/8 x 19 5/8 in. (16.8 x 49.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.324.6,false,true,44570,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Pan Zenggui,"Chinese, active late 19th century",,Pan Zenggui,Chinese,1800,1899,ca. late 19th century,1867,1899,Album leaf of calligraphy in running script; ink and color on paper,9 3/4 x 13 1/2 in. (24.8 x 34.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/44570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.17,false,true,40105,Asian Art,Handscroll,元 耶律楚材 行書贈別劉滿詩 卷|Poem of Farewell to Liu Man,China,Yuan dynasty (1271–1368),,,,Artist,,Yelü Chucai,"Khitan, 1190–1244",,YELÜ CHUCAI,Khitan,1190,1244,dated 1240,1230,1250,Handscroll; ink on paper,Image: 14 1/2 x 111 3/4 in. (36.8 x 283.8 cm) Overall with mounting: 15 in. x 37 ft. 7 11/16 in. (38.1 x 1147.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.30,false,true,40509,Asian Art,Handscroll,元 趙孟頫 行書右軍四事 卷|Four anecdotes from the life of Wang Xizhi,China,Yuan dynasty (1271–1368),,,,Artist,,Zhao Mengfu,"Chinese, 1254–1322",,Zhao Mengfu,Chinese,1254,1322,1310s,1310,1319,Handscroll; ink on paper,Image: 9 5/8 x 46 1/16 in. (24.4 x 117 cm) Overall with mounting: 10 7/16 in. x 27 ft. 1 1/8 in. (26.5 x 851.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.308,false,true,42329,Asian Art,Handscroll,元 鄭元祐 楷書師孺齋記 卷|Record of the Following One's Ancestor Studio,China,Yuan dynasty (1271–1368),,,,Artist,,Zheng Yuanyou,"Chinese, 1292–1364",,ZHENG YUANYOU,Chinese,1292,1364,dated 1345,1345,1345,Handscroll; ink on paper,10 3/4 x 37 3/4 in. (27.3 x 96 cm),"Purchase, Friends of Asian Art Gifts, 1994",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/42329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.4,true,true,39918,Asian Art,Handscroll,北宋 黃庭堅 草書廉頗藺相如傳 卷|Biographies of Lian Po and Lin Xiangru,China,Northern Song dynasty (960–1127),,,,Artist,,Huang Tingjian,"Chinese, 1045–1105",,HUANG TINGJIAN,Chinese,1045,1105,ca. 1095,1085,1105,Handscroll; ink on paper,Image: 13 1/4 in. × 60 ft. 4 1/2 in. (33.7 × 1840.2 cm) Overall with mounting: 13 1/2 in. × 71 ft. 5 5/8 in. (34.3 × 2178.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/39918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.174,false,true,39919,Asian Art,Handscroll,北宋 米芾 草書吳江舟中詩 卷|Poem Written in a Boat on the Wu River,China,Northern Song dynasty (960–1127),,,,Artist,,Mi Fu,"Chinese, 1052–1107",,MI FU,Chinese,1052,1107,ca. 1095,1085,1105,Handscroll; ink on paper,12 1/4 in. × 18 ft. 3 1/4 in. (31.1 × 556.9 cm),"Gift of John M. Crawford Jr., in honor of Professor Wen Fong, 1984",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/39919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.12,false,true,40103,Asian Art,Fan mounted as an album leaf,南宋 楊皇后 楷書薄薄殘妝七絕 團扇冊頁 絹本|Quatrain on spring’s radiance,China,Southern Song dynasty (1127–1279),,,,Artist,,Empress Yang Meizi,"Chinese, 1162–1232",,YANG MEIZI,Chinese,1162,1232,early 13th century,1200,1215,Round fan mounted as an album leaf; ink on silk,Image: 9 1/8 x 9 5/8 in. (23.2 x 24.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.13,false,true,40104,Asian Art,Fan mounted as an album leaf,南宋 楊皇后 楷書瀹雪凝酥七絕 團扇|Quatrain on yellow roses,China,Southern Song dynasty (1127–1279),,,,Artist,,Empress Yang Meizi,"Chinese, 1162–1232",", r. 1202–24;",YANG MEIZI,Chinese,1162,1232,early 13th century,1200,1232,Round fan mounted as an album leaf; ink on silk,9 1/4 x 9 5/8 in. (23.5 x 24.5 cm); with mat: 14 1/2 x 15 1/2 in. (36.8 x 39.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.198,false,true,41777,Asian Art,Ink tablet,,China,"late Qing dynasty (1644–1911, early Republic period",,,,Artist,Workshop of,Fang Yulu,active ca. 1570–1619,,Fang Yulu,Chinese,1570,1619,late 19th–early 20th century,1871,1933,Black ink,H. 4 3/8 in. (11.1 cm); W. 4 1/4 in. (10.8 cm),"Rogers Fund, 1929",,,,,,,,,,,,Ink,,http://www.metmuseum.org/art/collection/search/41777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.280,false,true,41860,Asian Art,Ink tablet,,China,"late Qing dynasty (1644–1911, early Republic period",,,,Artist,,Fang Yulu,active ca. 1570–1619,,Fang Yulu,Chinese,1570,1619,late 19th–early 20th century,1871,1933,Ink,Diam. 4 7/8 in. (12.4 cm),"Rogers Fund, 1929",,,,,,,,,,,,Ink,,http://www.metmuseum.org/art/collection/search/41860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.196,false,true,40018,Asian Art,Ink tablet,,China,"late Qing dynasty (1644–1911, early Republic period",,,,Artist,Workshop of,Cheng Junfang,"Chinese, 1541–ca. 1620",,Cheng Junfang,Chinese,1541,1620,late 19th–early 20th century,1871,1933,Pine soot and binding medium,Diam. 4 1/2 in. (11.4 cm),"Rogers Fund, 1929",,,,,,,,,,,,Ink,,http://www.metmuseum.org/art/collection/search/40018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.199,false,true,41778,Asian Art,Ink tablet,,China,"late Qing dynasty (1644–1911, early Republic period",,,,Artist,,Cheng Junfang,"Chinese, 1541–ca. 1620",,Cheng Junfang,Chinese,1541,1620,late 19th–early 20th century,1875,1933,Ink,Diam. 3 1/8 in. (7.9 cm.),"Rogers Fund, 1929",,,,,,,,,,,,Ink,,http://www.metmuseum.org/art/collection/search/41778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.203,false,true,41782,Asian Art,Ink tablet,,China,"late Qing dynasty (1644–1911, early Republic period",,,,Artist,,Cheng Junfang,"Chinese, 1541–ca. 1620",,Cheng Junfang,Chinese,1541,1620,late 19th–early 20th century,1871,1933,Ink,Diam. 5 5/8 in. (14.3 cm),"Rogers Fund, 1929",,,,,,,,,,,,Ink,,http://www.metmuseum.org/art/collection/search/41782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP7,false,true,63196,Asian Art,Print,,China,,,,,Artist,Original painted by,Li Gonglin,"Chinese, ca. 1041–1106",,Li Gonglin,Chinese,1041,1106,1677,1677,1677,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP6,false,true,63185,Asian Art,Print,芥子園畫傳|Mountainside View: Page from The Mustard Seed Garden Manual of Painting,China,,,,,Artist,Original painted by,Juran,"Chinese, active 10th century",,Juran,Chinese,0010,0010,probably 1878 edition,1878,1878,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.125,false,true,49047,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Zhao Wenchu,"Chinese, 1595–1654",,Zhao Wenchu,Chinese,1595,1654,dated 1627,1627,1627,Hanging scroll; ink and color on paper,Image: 49 13/16 x 20 3/16 in. (126.5 x 51.3 cm) Overall: 98 1/2 x 26 1/2 in. (250.2 x 67.3 cm) Overall with knobs: 98 1/2 x 30 in. (250.2 x 76.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.163,false,true,50549,Asian Art,Figure,,China,Ming dynasty (1368–1644),,,,Artist,,Qiao Bin,"Chinese, active 1481–1507",,Qiao Bin,Chinese,1481,1507,dated 1482,1482,1482,Glazed stoneware,H. 23 3/4 in. (60.3 cm); W. 15 in. (38.1 cm); D. 8 3/8 in. (21.3 cm),"Bequest of Harrison Cady, 1970",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/50549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.227.1,false,true,53924,Asian Art,Group,,China,Ming dynasty (1368–1644),,,,Artist,,Qiao Bin,"Chinese, active 1481–1507",,Qiao Bin,Chinese,1481,1507,dated 1503,1503,1503,Glazed stoneware,H. 14 in. (35.6 cm); W. 17 1/8 in. (43.5 cm); D. 9 in. (22.9 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.227.2,false,true,53925,Asian Art,Figure,,China,Ming dynasty (1368–1644),,,,Artist,,Qiao Bin,"Chinese, active 1481–1507",,Qiao Bin,Chinese,1481,1507,dated 1503,1503,1503,Glazed pottery,H. 11 in. (27.9 cm); W. 4 1/2 in. (11.4 cm); D. 3 5/8 in. (9.2 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.227.3,false,true,53926,Asian Art,Figure,,China,Ming dynasty (1368–1644),,,,Artist,,Qiao Bin,"Chinese, active 1481–1507",,Qiao Bin,Chinese,1481,1507,dated 1503,1503,1503,Glazed pottery,H. 11 in. (27.9 cm); W. 4 5/8 in. (11.7 cm); D. 4 1/2 in. (11.4 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.227.4,false,true,53927,Asian Art,Figure,,China,Ming dynasty (1368–1644),,,,Artist,,Qiao Bin,"Chinese, active 1481–1507",,Qiao Bin,Chinese,1481,1507,dated 1503,1503,1503,Glazed pottery,H. 11 1/2 in. (29.2 cm); W. 4 5/8 in. (11.7 cm); D. 4 1/2 in. (11.4 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.227.5,false,true,53928,Asian Art,Figure,,China,Ming dynasty (1368–1644),,,,Artist,,Qiao Bin,"Chinese, active 1481–1507",,Qiao Bin,Chinese,1481,1507,dated 1503,1503,1503,Glazed pottery,H. 11 1/2 in. (29.2 cm); W. 4 5/8 in. (11.7 cm); D. 4 3/4 in. (12.1 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.227.6,false,true,53929,Asian Art,Figure,,China,Ming dynasty (1368–1644),,,,Artist,,Qiao Bin,"Chinese, active 1481–1507",,Qiao Bin,Chinese,1481,1507,dated 1503,1503,1503,Glazed pottery,H. 7 in. (17.8 cm); W. 4 1/4 in. (10.8 cm); D. 6 1/2 in. (16.5 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1982.362a, b",false,true,42323,Asian Art,Teapot,,China,Qing dynasty (1644–1911),,,,Artist,,Shi Dabin,"Chinese, active 1620–40",,Shi Dabin,Chinese,1620,1640,early 17th century,1600,1633,Stoneware (Yixing ware),H. 3 3/4 in. (9.5 cm),"Purchase, Ann Eden Woodward Foundation Gift, 1982",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/42323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.165,false,true,36236,Asian Art,Folding fan mounted as an album leaf,近代 張善子 黃山奇松圖 扇頁|Strange Pine in the Yellow Mountain,China,,,,,Artist,,Zhang Shanzi,(1882–1940),,ZHANG SHANZI,Chinese,1882,1940,dated 1935,1935,1935,Folding fan mounted as an album leaf; ink and color on alum paper,7 5/16 x 20 3/16 in. (18.6 x 51.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.163,false,true,36098,Asian Art,Hanging scroll,,China,,,,,Artist,After,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,20th century,1912,1971,Hanging scroll; ink and color on silk,Image: 84 3/8 x 30 3/4 in. (214.3 x 78.1 cm) Overall with mounting: 127 5/8 x 32 in. (324.2 x 81.3 cm) Overall with rollers: 127 5/8 x 35 in. (324.2 x 88.9 cm),"Gift of Zhang Daqian, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.557.2a–d,false,true,72711,Asian Art,Album leaves,清 湯貽汾 墨梅 四冊頁|Blossoming Plum,China,,,,,Artist,,Tang Yifen,"Chinese, 1778–1853",,Tang Yifen,Chinese,1778,1853,Dated 1840,1840,1840,Four album leaves; ink on gold-flecked paper,Image: 9 1/4 x 8 1/2 in. (23.5 x 21.6 cm) Overall with mounting: 17 1/2 x 14 1/2 in. (44.5 x 36.8 cm),"Gift of Mark Shrum Pratt, 2004",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.557.3a–d,false,true,72712,Asian Art,Hanging scrolls,清 胡遠 山水 四軸|Landscapes of the Four Seasons,China,,,,,Artist,,Hu Yuan,"Chinese, 1823–1886",,Hu Yuan,Chinese,1823,1886,Dated 1875,1875,1875,Set of four hanging scrolls; ink and color on paper,Image: 104 1/2 x 22 1/8 in. (265.4 x 56.2 cm) Overall with mounting (a): 143 x 29 in. (363.2 x 73.7 cm) Overall with mounting (b): 143 3/8 x 29 1/16 in. (364.2 x 73.8 cm) Overall with mounting (c): 143 1/8 x 29 1/16 in. (363.5 x 73.8 cm) Overall with mounting (d): 143 1/2 x 29 in. (364.5 x 73.7 cm) Overall with rollers (each): 33 1/4 in. (84.5 cm),"Gift of Mark Shrum Pratt, 2004",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.103,false,true,36201,Asian Art,Folding fan mounted as an album leaf,近代 王振聲 友梅圖 扇面|Still-life with Plum,China,,,,,Artist,,Wang Zhensheng,"Chinese, 1842–1922",,Wang Zhensheng,Chinese,1842,1922,early 20th century,1900,1922,Folding fan mounted as an album leaf; ink and color on alum paper,6 1/8 x 19 in. (15.6 x 48.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.324.2,false,true,44567,Asian Art,Hanging scroll,近代 吳昌碩 仙芝天竹圖 軸|Spring Offerings,China,,,,,Artist,,Wu Changshuo,"Chinese, 1844–1927",,Wu Changshuo,Chinese,1844,1927,dated 1919,1919,1919,Hanging scroll; ink and color on paper,Image: 58 x 31 1/2 in. (147.3 x 80 cm) Overall with mounting: 100 3/8 x 39 3/8 in. (255 x 100 cm) Overall with knobs: 100 3/8 x 43 in. (255 x 109.2 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.124,false,true,49617,Asian Art,Painting,近代 吳昌碩 烹茶圖 橫軸|Brewing Tea,China,,,,,Artist,,Wu Changshuo,"Chinese, 1844–1927",,Wu Changshuo,Chinese,1844,1927,dated 1918,1918,1918,Horizontal painting; ink on paper,15 9/16 x 54 in. (39.5 x 137.2 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.125,false,true,36208,Asian Art,Folding fan mounted as an album leaf,近代 吳昌碩 蘭花 扇面|Orchid,China,,,,,Artist,,Wu Changshuo,"Chinese, 1844–1927",,Wu Changshuo,Chinese,1844,1927,early 20th century,1900,1927,Folding fan mounted as an album leaf; ink and color on alum paper,7 3/8 x 20 3/8 in. (18.7 x 51.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.117,false,true,36205,Asian Art,Folding fan mounted as an album leaf,近代 林紓 合掌峰 扇面|Hezhang Peak,China,,,,,Artist,,Lin Shu,"Chinese, 1852–1924",,Lin Shu,Chinese,1852,1924,dated 1921,1921,1921,Folding fan mounted as an album leaf; ink and color on alum paper,7 11/16 x 21 1/4 in. (19.5 x 54 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.118,false,true,36206,Asian Art,Handscroll,近代 林紓 詩廬圖 卷|The Poetry Cottage,China,,,,,Artist,,Lin Shu,"Chinese, 1852–1924",,Lin Shu,Chinese,1852,1924,dated 1914,1914,1914,Handscroll; ink on paper,Image: 8 7/8 x 39 in. (22.5 x 99.1 cm) Overall with mounting: 11 x 320 3/8 in. (27.9 x 813.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.128,false,true,36211,Asian Art,Folding fan mounted as an album leaf,近代 吳觀岱 山水人物 扇面|Landscape and Figure,China,,,,,Artist,,Wu Guandai,"Chinese, 1862–1929",,Wu Guandai,Chinese,1862,1929,early 20th century,1900,1929,Folding fan mounted as an album leaf; ink and color on alum paper,7 5/16 x 20 3/4 in. (18.6 x 52.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.127,false,true,36210,Asian Art,Folding fan mounted as an album leaf,近代 楊逸 青松 扇面|Pine,China,,,,,Artist,,Yang Yi,"Chinese, 1864–1929",,Yang Yi,Chinese,1864,1929,dated 1923,1923,1923,Folding fan mounted as an album leaf; ink and color on alum paper,8 x 21 1/2 in. (20.3 x 54.6 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.136,false,true,36216,Asian Art,Folding fan mounted as an album leaf,近代 顧麟士 臨溪亭子圖 扇面|Pavilion Beside a Rock Garden and Stream,China,,,,,Artist,,Gu Linshi,"Chinese, 1865–1930",,Gu Linshi,Chinese,1865,1930,dated 1921,1921,1921,Folding fan mounted as an album leaf; ink and color on alum paper,7 x 19 5/8 in. (17.8 x 49.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.154,false,true,49629,Asian Art,Hanging scroll,近代 王震 山羊 軸|Two Goats,China,,,,,Artist,,Wang Zhen,"Chinese, 1867–1938",,WANG ZHEN,Chinese,1867,1938,dated 1914,1914,1914,Hanging scroll; ink and color on paper,57 1/2 x 15 5/8 in. (146.1 x 39.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.155,false,true,49630,Asian Art,Hanging scroll,近代 王震 漁翁圖 軸|Returning Fisherman,China,,,,,Artist,,Wang Zhen,"Chinese, 1867–1938",,WANG ZHEN,Chinese,1867,1938,dated 1917,1917,1917,Hanging scroll; ink and color on paper,Image: 70 1/8 x 37 1/8 in. (178.1 x 94.3 cm) Overall with mounting: 109 x 43 in. (276.9 x 109.2 cm) Overall with knobs: 109 x 46 3/4 in. (276.9 x 118.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.156,false,true,41504,Asian Art,Hanging scroll,近代 王震 佛祖圖 軸|Buddhist Sage,China,,,,,Artist,,Wang Zhen,"Chinese, 1867–1938",,WANG ZHEN,Chinese,1867,1938,dated 1928,1928,1928,Hanging scroll; ink and color on paper,78 1/2 x 36 7/8 in. (199.4 x 93.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.104,false,true,49603,Asian Art,Album leaf,近代 陳衡恪 臨水書閣 冊頁|Studio by the Water,China,,,,,Artist,,Chen Hengke,"Chinese, 1876–1923",,Chen Hengke,Chinese,1876,1923,dated 1921,1921,1921,Album leaf; ink and color on paper,13 1/4 x 18 3/4 in. (33.7 x 47.6 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.105,false,true,49604,Asian Art,Folding fan mounted as an album leaf,近代 陳衡恪 水仙蘭花 扇面|Narcissus and Orchid,China,,,,,Artist,,Chen Hengke,"Chinese, 1876–1923",,Chen Hengke,Chinese,1876,1923,dated 1920,1920,1920,Folding fan mounted as an album leaf; ink and color on alum paper,7 7/8 x 21 2/3 in. (20.0 x 55.0 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.106,false,true,49605,Asian Art,Folding fan mounted as an album leaf,近代 陳衡恪 秋山蕭寺 扇面|Remote Temple on the Autumn Mountain,China,,,,,Artist,,Chen Hengke,"Chinese, 1876–1923",,Chen Hengke,Chinese,1876,1923,dated 1920,1920,1920,Folding fan mounted as an album leaf; ink and color on alum paper,7 15/16 x 22 in. (20.2 x 55.9 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.108,false,true,49607,Asian Art,Album leaf,近代 陳衡恪 玉簪花 冊頁|Plantain Lily,China,,,,,Artist,,Chen Hengke,"Chinese, 1876–1923",,Chen Hengke,Chinese,1876,1923,early 20th century,1900,1923,Album leaf; ink and color on paper,11 x 17 1/4 in. (27.9 x 43.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.109,false,true,36202,Asian Art,Album leaf,近代 陳衡恪 梅花 冊頁|Plum,China,,,,,Artist,,Chen Hengke,"Chinese, 1876–1923",,Chen Hengke,Chinese,1876,1923,early 20th century,1900,1923,Album leaf; ink and color on paper,11 x 17 1/4 in. (27.9 x 43.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.110,false,true,49608,Asian Art,Album leaf,近代 陳衡恪 梨花 軸|Pear-blossoms,China,,,,,Artist,,Chen Hengke,"Chinese, 1876–1923",,Chen Hengke,Chinese,1876,1923,early 20th century,1900,1923,Album leaf; ink and color on paper,11 x 17 1/4 in. (27.9 x 43.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1986.267.107a, b",false,true,49606,Asian Art,Folding fan mounted as album leaves,近代 陳衡恪 山水 扇面兩幀|Landscapes,China,,,,,Artist,,Chen Hengke,"Chinese, 1876–1923",,Chen Hengke,Chinese,1876,1923,early 20th century,1900,1923,Two sides of a folding fan mounted as two album leaves; ink and color on alum paper,5 1/8 x 17 1/8 in. (13.0 x 43.5 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.121,false,true,49614,Asian Art,Folding fan mounted as an album leaf,近代 金城 山水 扇面|Landscape,China,,,,,Artist,,Jin Cheng,"Chinese, 1878–1926",,Jin Cheng,Chinese,1878,1926,early 20th century,1900,1926,Folding fan mounted as an album leaf; ink and color on alum paper,8 3/8 x 26 7/8 in. (21.3 x 68.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.122,false,true,49615,Asian Art,Folding fan mounted as an album leaf,近代 金城 牡丹 扇面|Peony,China,,,,,Artist,,Jin Cheng,"Chinese, 1878–1926",,Jin Cheng,Chinese,1878,1926,early 20th century,1900,1926,Folding fan mounted as an album leaf; ink and color on alum paper,8 3/8 x 26 7/8 in. (21.3 x 68.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.253,false,true,49663,Asian Art,Album leaf,近代 丁輔之 雜果圖 冊頁|Fruit,China,,,,,Artist,,Ding Fuzhi,"Chinese, 1879–1946",,Ding Fuzhi,Chinese,1879,1946,dated 1945,1945,1945,Album leaf; ink and color on paper,12 1/8 x 25 1/8 in. (30.8 x 63.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.254,false,true,49664,Asian Art,Album leaf,近代 丁輔之 冰盤進夏圖 冊頁|Dish of Iced Summer Fruit,China,,,,,Artist,,Ding Fuzhi,"Chinese, 1879–1946",,Ding Fuzhi,Chinese,1879,1946,dated 1945,1945,1945,Album leaf; ink and color on paper,12 1/8 x 25 1/8 in. (30.8 x 63.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.145,false,true,49626,Asian Art,Folding fan mounted as an album leaf,近代 俞明 桐窗仕女 扇面|A Beauty at the Window by a Wutong Tree,China,,,,,Artist,,Yu Ming,"Chinese, 1884–1935",,Yu Ming,Chinese,1884,1935,dated 1923,1923,1923,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/2 x 21 7/16 in. (19.1 x 54.5 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.146,false,true,49627,Asian Art,Folding fan mounted as an album leaf,近代 俞明 絲路行旅圖 扇面|Travel on the Silk Road,China,,,,,Artist,,Yu Ming,"Chinese, 1884–1935",,Yu Ming,Chinese,1884,1935,early 20th century,1900,1933,Folding fan mounted as an album leaf; ink and color on alum paper,8 x 21 3/4 in. (20.3 x 55.2 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.147,false,true,36222,Asian Art,Album leaf,近代 俞明 堆雪獅子圖 冊頁|Making a Snow-lion,China,,,,,Artist,,Yu Ming,"Chinese, 1884–1935",,Yu Ming,Chinese,1884,1935,dated 1921,1921,1921,Album leaf; ink and color on paper,14 7/8 x 9 1/4 in. (37.8 x 23.5 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.149,false,true,36224,Asian Art,Hanging scroll,近代 俞明 米芾拜石圖 軸|Mi Fu at Stone Worship,China,,,,,Artist,,Yu Ming,"Chinese, 1884–1935",,Yu Ming,Chinese,1884,1935,early 20th century,1900,1933,Hanging scroll; ink and color on paper,Overall with mounting: 69 3/4 × 18 1/8 in. (177.2 × 46 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.141,false,true,49623,Asian Art,Hanging scroll,近代 高奇峰 啄木鳥 軸|Woodpecker,China,,,,,Artist,,Gao Qifeng,"Chinese, 1889–1933",,Gao Qifeng,Chinese,1889,1933,dated 1927,1927,1927,Hanging scroll; ink and color on alum paper,Image: 32 5/8 × 13 3/8 in. (82.9 × 34 cm) Overall with mounting: 75 1/4 × 20 5/16 in. (191.1 × 51.6 cm) Overall with knobs: 75 1/4 × 24 1/8 in. (191.1 × 61.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.124,false,true,40311,Asian Art,Handscroll,元/明 倣錢選 鮮于樞 歸去來辭 卷|Ode on Returning Home,China,,,,,Artist,After,Qian Xuan,"Chinese, ca. 1235–before 1307",,QIAN XUAN,Chinese,1235,1307,14th–15th century,1300,1499,"Handscroll; ink, color, and gold on paper",Image: 42 x 10 1/4 in. (106.7 x 26 cm) Overall with mounting: 12 1/4 in. x 13 ft. 6 in. (31.1 x 411.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.4,false,true,45652,Asian Art,Handscroll,元 方從義 雲山圖 卷|Cloudy Mountains,China,Yuan (1271–1368),,,,Artist,,Fang Congyi,"Chinese, ca. 1301–after 1378",,Fang Congyi,Chinese,1301,1399,ca. 1360–70,1360,1370,Handscroll; ink and color on paper,Image: 10 3/8 x 57 in. (26.4 x 144.8 cm) Overall with mounting: 10 5/8 x 336 1/4 in. (27 x 854.1 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.78,true,true,39901,Asian Art,Handscroll,唐 韓幹 照夜白圖 卷|Night-Shining White,China,Tang dynasty (618–907),,,,Artist,,Han Gan,"Chinese, active ca. 742–756",,HAN GAN,Chinese,0742,0756,ca. 750,740,760,Handscroll; ink on paper,Image: 12 1/8 x 13 3/8 in. (30.8 x 34 cm) Overall with mounting: 14 in. x 37 ft. 5 1/8 in. (35.4 cm x 11.4 m),"Purchase, The Dillon Fund Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1.1,false,true,39959,Asian Art,Handscroll,金 傳楊邦基 聘金圖 巻|A Diplomatic Mission to the Jin,China,Jin dynasty (1115–1234),,,,Artist,Attributed to,Yang Bangji,"Chinese, ca. 1110–1181",,Yang Bangji,Chinese,1100,1191,ca. late 1150s,1156,1159,Handscroll; ink and color on silk,Image: 10 1/2 in. × 56 in. (26.7 × 142.2 cm) Overall with mounting: 11 5/8 in. × 26 ft. 11 3/4 in. (29.5 × 822.3 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.6.12,false,true,51762,Asian Art,Album leaf,,China,Song dynasty (960–1279),,,,Artist,Attributed to,Xu Daoning,active ca. 1030–67,,Xu Daoning,Chinese,1020,1077,ca. 1000,1030,1067,Album leaf; painted silk,14 x 12 1/4 in. (35.6 x 31.1 cm),"Bequest of Ellis Gray Seymour, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1973.121.12a, b",false,true,40062,Asian Art,Fan mounted as an album leaf,南宋 傳閻次于 松壑隱棲圖 團扇|Hermitage by a Pine-covered Bluff,China,Song dynasty (960–1279),,,,Artist,Attributed to,Yan Ciyu,"Chinese, act. ca. 1164–81",,YAN CIYU,Chinese,1164,1181,second half of the 12th century,1164,1181,Fan mounted as an album leaf; ink and color on silk,"(a): 8 7/16 x 9 1/16 in. (21.4 x 23 cm), b): 8 1/2 x 9 1/16 in. (21.6 x 23 cm)","Ex coll.: C. C. Wang Family, Purchase, Gift of Mr. and Mrs. Jeremiah Milbank and Gift of Mary Phelps Smith, in memory of Howard Caswell Smith, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1989.363.10a, b",false,true,40057,Asian Art,Handscroll,南宋 李結 西塞漁社圖 卷 |Fisherman's Lodge At Mount Xisai,China,Song dynasty (960–1279),,,,Artist,,Li Jie,"Chinese, 1124– before 1197",,LI JIE,Chinese,1124,1197,ca. 1170,1160,1180,Handscroll; ink and color on silk,Image (a): 16 in. × 53 3/4 in. (40.6 × 136.5 cm) Overall with mounting (a): 16 3/8 in. × 26 ft. 1/8 in. (41.6 × 792.8 cm) Overall with mounting (b): 16 3/8 in. × 27 ft. 9 5/16 in. (41.6 × 846.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.32,false,true,40094,Asian Art,Fan mounted as an album leaf,南宋 傳陳居中 胡騎春獵圖 團扇|Nomads hunting with falcons,China,Song dynasty (960–1279),,,,Artist,Attributed to,Chen Juzhong,"Chinese, active ca. 1200–30",,Chen Juzhong,Chinese,1200,1230,early 13th century,1200,1233,Fan mounted as an album leaf; ink and color on silk,9 1/2 x 10 3/4 in. (24.1 x 27.3 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.30,false,true,36052,Asian Art,Fan mounted as an album leaf,南宋 佚名 倣馬遠 洞天論道圖 團扇|Conversation in a Cave,China,Song dynasty (960–1279),,,,Artist,After,Ma Yuan,"Chinese, active ca. 1190–1225",,MA YUAN,Chinese,1190,1225,13th century,1200,1225,Fan mounted as an album leaf; ink and color on silk,Image: 9 3/4 × 9 15/16 in. (24.8 × 25.2 cm) Mat: 15 1/2 × 15 1/2 in. (39.4 × 39.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.6.11,false,true,40053,Asian Art,Album leaf,,China,Song dynasty (960–1279),,,,Artist,In the style of,Xia Gui,"Chinese, active ca. 1195–1230",,Xia Gui,Chinese,1195,1230,ca. 1200,1190,1210,Album leaf; ink and color silk,9 1/8 x 9 3/8 in. (23.2 x 23.8 cm),"Bequest of Ellis Gray Seymour, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.63,false,true,36055,Asian Art,Album leaf,南宋 馬麟 松下高士圖 冊頁 絹本|Landscape with great pine,China,Song dynasty (960–1279),,,,Artist,,Ma Lin,"Chinese, ca. 1180– after 1256",,MA LIN,Chinese,1180,1256,second quarter of the 13th century,1226,1250,Album leaf; ink and color on silk,9 15/16 x 10 1/4 in. (25.2 x 26 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.290,false,true,44506,Asian Art,Hanging scroll,南宋 金處士 十王圖 軸|Ten Kings of Hell,China,Song dynasty (960–1279),,,,Artist,,Jin Chushi,"Chinese, active late 12th century",,JIN CHUSHI,Chinese,1167,1199,before 1195,1167,1194,One of five of a set of ten hanging scrolls; ink and color on silk,Image: 51 x 19 1/2 in. (129.5 x 49.5 cm) Overall with mounting: 80 × 25 3/8 in. (203.2 × 64.5 cm) Overall with knobs: 80 x 27 1/2 in. (203.2 x 69.9 cm),"Rogers Fund, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.291,false,true,44507,Asian Art,Hanging scroll,南宋 金處士 十王圖 軸|Ten Kings of Hell,China,Song dynasty (960–1279),,,,Artist,,Jin Chushi,"Chinese, active late 12th century",,JIN CHUSHI,Chinese,1167,1199,before 1195,1167,1194,One of five of a set of ten hanging scrolls; ink and color on silk,Image: 51 x 19 1/2 in. (129.5 x 49.5 cm) Overall with knobs: 80 x 27 1/2 in. (203.2 x 69.9 cm),"Rogers Fund, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.292,false,true,44508,Asian Art,Hanging scroll,南宋 金處士 十王圖 軸(之三)|Ten Kings of Hell,China,Song dynasty (960–1279),,,,Artist,,Jin Chushi,"Chinese, active late 12th century",,JIN CHUSHI,Chinese,1167,1199,before 1195,1167,1194,One of five of a set of ten hanging scrolls; ink and color on silk,Image: 51 x 19 1/2 in. (129.5 x 49.5 cm) Overall with knobs: 80 x 27 1/2 in. (203.2 x 69.9 cm),"Rogers Fund, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.293,false,true,44509,Asian Art,Hanging scroll,南宋 金處士 十王圖 軸|Ten Kings of Hell,China,Song dynasty (960–1279),,,,Artist,,Jin Chushi,"Chinese, active late 12th century",,JIN CHUSHI,Chinese,1167,1199,before 1195,1167,1194,One of five of a set of ten hanging scrolls; ink and color on silk,Image: 51 x 19 1/2 in. (129.5 x 49.5 cm) Overall with knobs: 80 x 27 1/2 in. (203.2 x 69.9 cm),"Rogers Fund, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.294,false,true,44510,Asian Art,Hanging scroll,南宋 金處士 十王圖 軸|Ten Kings of Hell,China,Song dynasty (960–1279),,,,Artist,,Jin Chushi,"Chinese, active late 12th century",,JIN CHUSHI,Chinese,1167,1199,before 1195,1167,1194,One of five of a set of ten hanging scrolls; ink and color on silk,Image: 51 x 19 1/2 in. (129.5 x 49.5 cm) Overall with knobs: 80 x 27 1/2 in. (203.2 x 69.9 cm),"Rogers Fund, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.2.1,false,true,40278,Asian Art,Hanging scroll,南宋 傳直翁 藥山李翱問道圖 軸|Meeting between Yaoshan and Li Ao,China,Song dynasty (960–1279),,,,Artist,Attributed to,Zhiweng,"Chinese, active first half of the 13th century",,ZHIWENG,Chinese,1200,1250,before 1256,1200,1255,Horizontal painting mounted as a hanging scroll; ink on paper,Image: 12 1/2 x 33 1/4 in. (31.8 x 84.5 cm) Overall with mounting: 49 1/8 x 34 in. (124.8 x 86.4 cm) Overall with knobs: 49 1/8 x 36 1/4 in. (124.8 x 92.1 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.318,false,true,41479,Asian Art,Handscroll,明 董其昌 荊谿招隱圖 卷|Invitation to Reclusion at Jingxi,China,Ming Dinasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,dated 1611,1611,1611,Handscroll; ink on paper,Image: 10 1/4 × 36 7/16 in. (26 × 92.6 cm),"Gift of Mr. and Mrs. Wan-go H. C. Weng, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.2.3,false,true,45661,Asian Art,Handscroll,明 王紱 江山漁樂圖 卷|Joys of the Fisherman,China,Ming dynasty (1368–1644),,,,Artist,,Wang Fu,"Chinese, 1362–1416",,Wang Fu,Chinese,1362,1416,ca. 1410,1400,1420,Handscroll; ink on paper,Image: 10 5/8 x 22 ft. 7 1/8 in. (27 x 688.7 cm) Overall with mounting: 10 15/16 in. x 38 ft. 3 11/16 in. (27.8 x 1167.6 cm),"Ex coll.: C. C. Wang Family, Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.141.3,false,true,41478,Asian Art,Handscroll,明 傳謝環 杏園雅集圖 卷|Elegant Gathering in the Apricot Garden,China,Ming dynasty (1368–1644),,,,Artist,After,Xie Huan,"Chinese, 1377–1452",,Xie Huan,Chinese,1377,1452,ca. 1437,1427,1447,Handscroll; ink and color on silk,Image: 14 5/8 x 95 3/4 in. (37.1 x 243.2 cm) Overall with mounting: 14 3/4 in. x 41 ft. 11 1/4 in. (37.5 x 1278.3 cm),"Purchase, The Dillon Fund Gift, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.132,false,true,44699,Asian Art,Hanging scroll,明 戴進 雪歸圖 軸|Returning home through the snow,China,Ming dynasty (1368–1644),,,,Artist,,Dai Jin,"Chinese, 1388–1462",,Dai Jin,Chinese,1388,1462,ca. 1455,1445,1465,Hanging scroll; ink and color on silk,Image: 66 x 32 1/2 in. (167.6 x 82.6 cm) Overall with mounting: 118 3/4 x 38 1/2 in. (301.6 x 97.8 cm) Overall with knobs: 118 3/4 x 42 3/4 in. (301.6 x 108.6 cm),"Purchase, John M. Crawford Jr. Bequest, 1992",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.235.1,false,true,44590,Asian Art,Hanging scroll,明 夏昶 清風高節圖 軸|Bamboo in Wind,China,Ming dynasty (1368–1644),,,,Artist,,Xia Chang,"Chinese, 1388–1470",,Xia Chang,Chinese,1388,1470,ca. 1460,1450,1470,Hanging scroll; ink on paper,Image: 80 1/16 x 23 1/2 in. (203.4 x 59.7 cm) Overall with mounting: 118 x 29 3/8 in. (299.7 x 74.6 cm) Overall with knobs: 118 x 32 13/16 in. (299.7 x 83.3 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.44,false,true,45675,Asian Art,Handscroll,明 姚綬 文飲圖 卷|Drinking and Composing Poetry,China,Ming dynasty (1368–1644),,,,Artist,,Yao Shou,"Chinese, 1423–1495",,Yao Shou,Chinese,1423,1495,1485,1485,1485,Handscroll; ink on paper,Image: 9 3/16 x 30 3/8 in. (23.3 x 77.2 cm) Colophon: 9 3/16 x 30 in. (23.3 x 76.2 cm) Overall with mounting: 9 11/16 x 299 3/16 in. (24.6 x 759.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.5,false,true,36123,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,After,Zhang Bi,"Chinese, 1425–1487",,Zhang Bi,Chinese,1425,1487,dated 1478,1478,1478,Handscroll; ink on paper,Image: 12 1/4 x 297 1/2 in. (31.1 x 755.7 cm) Overall with mounting: 12 3/4 x 341 in. (32.4 x 866.1 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.75.1,false,true,45683,Asian Art,Handscroll,明 沈周 溪山秋色圖 卷|明 沈周 溪山秋色圖 卷明 楷書溪山秋色圖 引首|Autumn Colors among Streams and Mountains,China,Ming dynasty (1368–1644),,,,Artist,,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,ca. 1490–1500,1480,1510,Handscroll; ink on paper,Image: 8 1/8 in. x 21 ft. 1/4 in. (20.6 x 640.7 cm) Overall with mounting: 10 1/4 in. x 36 ft. 4 1/4 in. (26 x 1108.1 cm),"Gift of Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.46,false,true,45682,Asian Art,Hanging scroll,明 沈周 秋林閒釣圖 軸|Silent Angler in an Autumn Wood,China,Ming dynasty (1368–1644),,,,Artist,,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,dated 1475,1475,1475,Hanging scroll; ink and color on paper,Image: 60 x 24 3/4 in. (152.4 x 62.9 cm) Overall with mounting: 107 x 32 1/8 in. (271.8 x 81.6 cm) Overall with knobs: 107 x 35 1/4 in. (271.8 x 89.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.166,false,true,39483,Asian Art,Hanging scroll,明 郭詡 稱書圖 軸|Weighing Books,China,Ming dynasty (1368–1644),,,,Artist,,Guo Xu,"Chinese, 1456–1532",,Guo Xu,Chinese,1456,1532,early 16th century,1500,1532,Hanging scroll; ink and color on silk,Image: 48 3/8 x 28 in. (122.9 x 71.1 cm) Overall with mounting: 103 x 34 1/8 in. (261.6 x 86.7 cm) Overall with knobs: 103 x 38 3/8 in. (261.6 x 97.5 cm),"Friends of Asian Art, Purchase, The B. Y. Lam Foundation Gift, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.6.1,false,true,36093,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,After,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,late 15th–early 16th century,1470,1524,Hanging scroll; ink and color on silk,Image: 28 x 55 in. (71.1 x 139.7 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.53,false,true,45752,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,After,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,ca. 1508,1498,1518,Handscroll; ink on paper,Image: 11 11/16 x 42 3/8 in. (29.7 x 107.6 cm) Overall with mounting: 12 x 375 1/4 in. (30.5 x 953.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.7.1,false,true,45779,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,dated 1543,1543,1543,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,9 3/4 x 20 3/4 in. (24.8 x 52.7 cm),"Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.303,false,true,44601,Asian Art,Handscroll,明 文徵明 叢桂齋圖 卷|The Cassia Grove Studio,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,ca. 1532,1522,1542,Handscroll; ink and color on paper,Image: 12 7/16 x 22 1/8 in. (31.6 x 56.2 cm) Overall with mounting: 14 x 322 11/16 in. (35.6 x 819.6 cm),"Gift of Douglas Dillon, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.60,false,true,45776,Asian Art,Handscroll,明 文徵明 東林避暑圖 卷 |Summer Retreat in the Eastern Grove,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,datable to before 1515,1470,1514,Handscroll; ink on paper,Image (painting): 12 1/2 x 42 1/2 in. (31.8 x 108 cm) Image (colophon): 12 1/2 x 38 3/8 in. (31.8 x 97.5 cm) Overall with mounting: 13 x 117 1/2 in. (33 x 298.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.458.1a–ii,false,true,39654,Asian Art,Album,"明 文徵明 拙政園圖詩 冊|Garden of the Inept Administrator",China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,dated 1551,1551,1551,Album of eight painted leaves with facing leaves inscribed with poems; ink on paper,Image: 10 3/8 × 10 3/4 in. (26.4 × 27.3 cm) Image with mounting: 15 3/8 × 16 3/4 in. (39.1 × 42.5 cm) Double leaf unfolded: 15 3/8 × 33 1/2 in. (39.1 × 85.1 cm) Mat: 18 5/16 × 34 in. (46.5 × 86.4 cm),"Gift of Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.266.2,false,true,45802,Asian Art,Hanging scroll,明 陳淳 暑園圖 軸|Summer Garden,China,Ming dynasty (1368–1644),,,,Artist,,Chen Chun,"Chinese, 1483–1544",,Chen Chun,Chinese,1483,1544,ca. 1530,1520,1540,Hanging scroll; ink and color on paper,Image: 126 1/8 x 39 1/4 in. (320.4 x 99.7 cm) Overall with mounting: 171 x 48 3/8 in. (434.3 x 122.9 cm) Overall with knobs: 171 x 52 7/8 in. (434.3 x 134.3 cm),"Gift of Douglas Dillon, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.266.1a–u,false,true,45797,Asian Art,Album,明 仿陳淳 雜花圖 冊|Garden Flowers,China,Ming dynasty (1368–1644),,,,Artist,After,Chen Chun,"Chinese, 1483–1544",,Chen Chun,Chinese,1483,1544,dated 1540,1540,1540,Album of sixteen paintings and one leaf of calligraphy; ink and color on paper,Image (six leaves): 12 13/16 x 22 9/16 in. (32.5 x 57.3 cm) Image (ten leaves): 13 1/8 x 22 3/4 in. (33.3 x 57.8 cm),"Gift of Douglas Dillon, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.371,false,true,44606,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Lu Zhi,"Chinese, 1495–1576",,Lu Zhi,Chinese,1495,1576,dated 1556,1556,1556,Hanging scroll; ink and color on paper,Image: 52 3/8 x 24 3/4 in. (133 x 62.9 cm) Overall with mounting: 90 1/2 x 31 in. (229.9 x 78.7 cm) Overall with knobs: 90 1/2 x 35 in. (229.9 x 88.9 cm),"Gift of Michael B. Weisbrod, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.266.3,false,true,45815,Asian Art,Hanging scroll,明 陸治 種菊圖 軸|Planting Chrysanthemums,China,Ming dynasty (1368–1644),,,,Artist,,Lu Zhi,"Chinese, 1495–1576",,Lu Zhi,Chinese,1495,1576,mid-16th century,1534,1566,Hanging scroll; ink and pale color on paper,Image: 42 x 10 3/4 in. (106.7 x 27.3 cm) Overall with mounting: 107 1/4 x 19 in. (272.4 x 48.3 cm) Overall with knobs: 107 1/4 x 22 1/2 in. (272.4 x 57.2 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.9,false,true,36135,Asian Art,Hanging scroll,"明 文嘉 為項元汴畫山水圖 軸|Landscape Dedicated to Xiang Yuanbian",China,Ming dynasty (1368–1644),,,,Artist,,Wen Jia,"Chinese, 1501–1583",,Wen Jia,Chinese,1501,1583,dated 1578,1578,1578,Hanging scroll; ink and color on paper,Image: 46 1/4 x 15 11/16 in. (117.5 x 39.8 cm) Overall with mounting: 90 3/4 x 23 3/8 in. (230.5 x 59.4 cm) Overall with knobs: 90 3/4 x 26 in. (230.5 x 66 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.83,false,true,45032,Asian Art,Handscroll,明 傳項元汴 秋江圖 卷|River Landscape,China,Ming dynasty (1368–1644),,,,Artist,,Xiang Yuanbian,"Chinese, 1525–1590",,Xiang Yuanbian,Chinese,1525,1590,dated 1578,1578,1578,Handscroll; ink on paper,Image: 11 7/8 x 36 1/2 in. (30.2 x 92.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.214.148,false,true,48934,Asian Art,Hanging scroll,明 莫是龍 倣黃公望山水圖 軸|Landscape in the Style of Huang Gongwang,China,Ming dynasty (1368–1644),,,,Artist,,Mo Shilong,"Chinese, 1537–1587",,Mo Shilong,Chinese,1537,1587,dated 1581,1581,1581,Hanging scroll; ink and color on paper,Image: 46 7/8 x 16 1/8 in. (119.1 x 41 cm) Overall with mounting: 90 x 23 1/4 in. (228.6 x 59.1 cm) Overall with knobs: 90 x 26 3/4 in. (228.6 x 67.9 cm),"Gift of Ernest Erickson Foundation, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1.7,false,true,48932,Asian Art,Hanging scroll,明 傳馬守真 蘭石圖 軸|Orchid and Rock,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Ma Shouzhen,"Chinese, 1548–1604",,Ma Shouzhen,Chinese,1548,1604,dated 1572,1572,1572,Hanging scroll; ink on paper,Image: 20 3/4 x 11 1/2 in. (52.7 x 29.2 cm) Image (with inscription): 27 5/8 x 11 1/2 in. (70.2 x 29.2 cm) Overall with mounting: 81 1/2 x 18 3/4 in. (207 x 47.6 cm) Overall with knobs: 81 1/2 x 23 in. (207 x 58.4 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.211.1,false,true,36070,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,dated 1611 and 1612,1350,1650,Handscroll; ink on satin,10 1/5 x 86 1/4 in. (25.9 x 219.1 cm),"Gift of Wan-go H. C. Weng, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.75.2,false,true,48949,Asian Art,Hanging scroll,明 董其昌 溪山樾館圖 軸 紙本|Shaded Dwellings among Streams and Mountains,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,ca. 1622–25,1612,1635,Hanging scroll; ink on paper,Image: 62 3/8 x 28 3/8 in. (158.4 x 72.1 cm) Overall with mounting: 99 x 33 1/8 in. (251.5 x 84.1 cm) Overall with knobs: 99 x 37 in. (251.5 x 94 cm),"Gift of Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.500.2,false,true,48952,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,dated 1632,1632,1632,Hanging scroll; ink on paper,Image: 60 7/8 x 21 1/2 in. (154.6 x 54.6 cm) Overall with mounting: 105 3/4 x 28 1/4 in. (268.6 x 71.8 cm) Overall with knobs: 105 3/4 x 32 1/4 in. (268.6 x 81.9 cm),"Gift of Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.99,false,true,48953,Asian Art,Hanging scroll,明 董其昌 倣倪瓚山水圖 軸|Landscape with Trees in the Manner of Ni Zan (1301–1374),China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,ca. 1622–25,1612,1635,Hanging scroll; ink on paper,Image: 45 1/2 x 18 in. (115.6 x 45.7 cm) Overall with mounting: 94 7/8 x 27 1/2 in. (241 x 69.9 cm) Overall with knobs: 94 7/8 x 31 1/2 in. (241 x 80 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.102,false,true,48951,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",(and assistants),Dong Qichang,Chinese,1555,1636,dated 1630,1630,1630,Hanging scroll; ink and color on silk,Overall with mounting: 130 7/8 x 46 1/8 in. (332.4 x 117.2 cm) Image: 91 x 37 in. (231.1 x 94 cm) Overall with knobs: 130 7/8 x 50 1/2 in. (332.4 x 128.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.372a–h,false,true,41480,Asian Art,Album,明 董其昌 山水圖詩 冊|Landscapes and Poems,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,"17th century, probably after 1625",1625,1636,"Album of eight paintings and eight calligraphies; ink, gold and color on gold-flecked paper",12 5/8 x 9 1/8 in. (32.1 x 23.2 cm),"Gift of Mr. and Mrs. Wan-go H. C. Weng, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.266.5a–k,false,true,48950,Asian Art,Album,明 董其昌 山水圖 冊 紙本|Landscapes after old masters,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,dated 1630,1630,1630,Album of eight leaves; ink on paper,Image (each): 9 5/8 x 6 5/16 in. (24.4 x 16 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.108,false,true,48969,Asian Art,Handscroll,明 張瑞圖 後赤壁賦圖 卷|Second Ode on the Red Cliff,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Ruitu,"Chinese, 1570–1641",,Zhang Ruitu,Chinese,1570,1641,dated 1628,1628,1628,Handscroll; ink on satin,Image: 11 in. x 10 ft. 6 in. (27.9 x 320 cm) Overall with mounting: 12 1/4 in. x 34 ft. 2 3/4 in. (31.1 x 1043.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.70,false,true,35988,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Li Liufang,"Chinese, 1575–1629",,Li Liufang,Chinese,1575,1629,dated 1613,1613,1613,Folding fan mounted as an album leaf; ink on gold paper,7 7/32 x 21 7/8 in. (18.4 x 55.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.26,false,true,39713,Asian Art,Hanging scroll,明 藍瑛 紅友圖 軸|Red Friend,China,Ming dynasty (1368–1644),,,,Artist,,Lan Ying,"Chinese, 1585–1664",,Lan Ying,Chinese,1585,1664,16th– mid-17th century,1500,1650,Hanging scroll; ink and color on paper,Image: 58 5/8 x 18 5/8 in. (148.9 x 47.3 cm) Overall with mounting: 85 3/4 x 25 5/8 in. (217.8 x 65.1 cm) Overall with knobs: 85 3/4 x 29 in. (217.8 x 73.7 cm),"Gift of Mr. and Mrs. Earl Morse, in honor of Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.114,false,true,49019,Asian Art,Hanging scroll,明 藍瑛 春江漁隱圖 軸|Hermit-Fisherman on a Spring River,China,Ming dynasty (1368–1644),,,,Artist,,Lan Ying,"Chinese, 1585–1664",,Lan Ying,Chinese,1585,1664,dated 1632,1632,1632,Hanging scroll; ink and color on silk,Image: 72 3/4 x 35 3/4 in. (184.8 x 90.8 cm) Overall with mounting: 98 3/4 x 43 1/8 in. (250.8 x 109.5 cm) Overall with knobs: 98 3/4 x 46 3/8 in. (250.8 x 117.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.2.2a–l,false,true,49021,Asian Art,Album,明 藍瑛 仿宋元山水圖 冊 紙本|Landscapes after Song and Yuan masters,China,Ming dynasty (1368–1644),,,,Artist,,Lan Ying,"Chinese, 1585–1664",,Lan Ying,Chinese,1585,1664,dated 1642,1642,1642,Album of twelve leaves; ink and color on paper,12 7/16 x 9 3/4 in. (31.6 x 24.8 cm),"The Sackler Fund, 1970",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.120,false,true,39717,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Ni Yuanlu,"Chinese, 1593–1644",,Ni Yuanlu,Chinese,1593,1644,16th–mid-17th century,1593,1644,Hanging scroll; ink on silk,Image: 51 1/2 x 17 7/8 in. (130.8 x 45.4 cm) Overall with mounting: 86 3/4 x 25 3/8 in. (220.3 x 64.5 cm) Overall with knobs: 86 3/4 x 28 3/8 in. (220.3 x 72.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.3a–h,false,true,49074,Asian Art,Album,"明/清 項聖謨 山水花鳥圖 冊|Landscapes, Flowers and Birds",China,Ming dynasty (1368–1644),,,,Artist,,Xiang Shengmo,"Chinese, 1597–1658",,Xiang Shengmo,Chinese,1597,1658,dated 1639,1639,1639,Album of eight paintings; ink and color on paper,11 1/8 x 8 7/8 in. (28.3 x 22.5 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.197,false,true,41470,Asian Art,Hanging scroll,明/清 陳洪綬 準提佛母法像圖 軸|Bodhisattva Guanyin in the Form of the Buddha Mother,China,Ming dynasty (1368–1644),,,,Artist,,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,dated 1620,1620,1620,Hanging scroll; ink on paper,Image: 49 1/4 x 19 1/16 in. (125.1 x 48.4 cm) Overall with mounting: 96 3/4 x 24 9/16 in. (245.7 x 62.4 cm) Overall with knobs: 96 3/4 x 28 1/2 in. (245.7 x 72.4 cm),"Purchase, Friends of Asian Art Gifts, 1992",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.1,false,true,39558,Asian Art,Hanging scroll,明/清 傳陳洪綬 青綠山水圖 軸|Landscape in the Blue-and-Green Manner,China,Ming dynasty (1368–1644),,,,Artist,,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,dated 1633,1633,1633,Hanging scroll; ink and color on silk,Image: 92 3/4 x 30 5/8 in. (235.6 x 77.8 cm) Overall with mounting: 126 5/8 x 34 5/8 in. (321.6 x 87.9 cm) Overall with rollers: 126 5/8 x 37 5/8 in. (321.6 x 95.6 cm),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.121a–l,false,true,44759,Asian Art,Album,"明/清 陳洪綬 山水人物花卉圖 冊|Landscapes, Figures, and Flowers",China,Ming dynasty (1368–1644),,,,Artist,,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,dated 1618–1622,1618,1622,Album of twelve paintings; ink and color on paper,Each leaf: 8 3/4 x 3 5/8 in. (22.2 x 9.2 cm),"Purchase, Friends of Far Eastern Art Gifts, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.112a–l,false,true,37395,Asian Art,Album,明/清 陳洪綬 橅古圖 冊|Miscellaneous Studies,China,Ming dynasty (1368–1644),,,,Artist,,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,one leaf dated 1619,1619,1619,Album of twelve paintings; ink on paper,Image (each leaf): 7 x 7 in. (17.8 x 17.8 cm),"Gift of Mr. and Mrs. Wan-go H. C. Weng, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.87,false,true,35998,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Shao Mi,active ca 1620–1660,,Shao Mi,Chinese,1620,1660,dated 1640,1640,1640,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,6 1/2 x 18 23/32 in. (16.5 x 47.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.77,false,true,35990,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wu Bin,active ca. 1583–1626,,Wu Bin,Chinese,1583,1626,dated 1603,1603,1603,Folding fan mounted as an album leaf; ink and color on paper,6 3/8 x 18 23/32 in. (16.2 x 47.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.266.4,true,true,48948,Asian Art,Handscroll,明 吳彬 十六羅漢圖 卷|The Sixteen Luohans,China,Ming dynasty (1368–1644),,,,Artist,,Wu Bin,active ca. 1583–1626,,Wu Bin,Chinese,1583,1626,dated 1591,1591,1591,Handscroll; ink and color on paper,Image: 12 5/8 x 163 9/16 in. (32.1 x 415.4 cm) Overall with mounting: 13 1/4 x 398 1/16 in. (33.7 x 1011.1 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.111,false,true,36022,Asian Art,Hanging scroll,明 顧懿德 倣王蒙玩月圖 軸|Enjoying the Moon: Landscape in the Manner of Wang Meng,China,Ming dynasty (1368–1644),,,,Artist,,Gu Yide,active ca. 1620–1630,,Gu Yide,Chinese,1620,1630,dated 1628,1368,1644,Hanging scroll; ink and color on paper,Image: 61 x 18 1/4 in. (154.9 x 46.4 cm) Overall with mounting: 123 3/4 x 26 3/4 in. (314.3 x 67.9 cm) Overall with knobs: 123 3/4 x 31 in. (314.3 x 78.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.104,false,true,36021,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,Formerly attributed to,Guanxiu,"Chinese, active ca. 940",,Guanxiu,Chinese,0930,0950,15th century?,1400,1499,Hanging scroll; ink and color on silk,Image: 46 3/8 × 18 1/2 in. (117.8 × 47 cm) Overall with mounting: 83 × 24 1/2 in. (210.8 × 62.2 cm) Overall with knobs: 83 × 28 1/4 in. (210.8 × 71.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.177.20,false,true,36067,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Xie Shichen,"Chinese, 1487–ca. 1567",,Xie Shichen,Chinese,1487,1567,dated 1548,1548,1548,Hanging scroll; ink and color on silk,Image: 71 7/8 x 41 7/8 in. (182.6 x 106.4 cm),"Bequest of Katherine S. Dreier, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.590a–d,false,true,75250,Asian Art,Hanging scrolls,明 謝時臣 四時佳興圖 軸 四幅|Landscapes of the Four Seasons,China,Ming dynasty (1368–1644),,,,Artist,,Xie Shichen,"Chinese, 1487–ca. 1567",,Xie Shichen,Chinese,1487,1567,dated 1560,1560,1560,Set of four hanging scrolls; ink and color on paper,Image (a): 126 3/8 x 37 in. (321 x 94 cm) Image (b): 127 1/4 x 36 7/8 in. (323.2 x 93.7 cm) Image (c): 126 3/4 x 36 7/8 in. (321.9 x 93.7 cm) Image (d): 126 7/8 x 36 7/8 in. (322.3 x 93.7 cm) Overall with mounting (b): 141 3/4 x 42 3/8 in. (360 x 107.6 cm),"Purchase, The Vincent Astor Foundation Gift, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.80,false,true,48901,Asian Art,Handscroll,明 錢榖 蘭亭修禊圖 卷|Gathering at the Orchid Pavilion,China,Ming dynasty (1368–1644),,,,Artist,,Qian Gu,"Chinese, 1508–ca. 1578",,Qian Gu,Chinese,1508,1578,datable to 1560,1560,1560,Handscroll; ink and color on paper,Image: 9 1/2 x 171 1/2 in. (24.1 x 435.6 cm) Overall with mounting: 9 3/4 x 491 3/8 in. (24.8 x 1248.1 cm),"Ex coll.: C. C. Wang Family, Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.385,false,true,45676,Asian Art,Hanging scroll,明 林良 二鷹圖 軸|Two hawks in a thicket,China,Ming dynasty (1368–1644),,,,Artist,,Lin Liang,"Chinese, ca. 1416–1480",,Lin Liang,Chinese,1416,1480,mid- 15th century,1416,1480,Hanging scroll; ink and color on silk,Image: 58 5/8 x 32 3/4 in. (148.9 x 83.2 cm) Overall with mounting: 108 1/2 x 39 in. (275.6 x 99.1 cm) Overall with knobs: 108 1/2 x 43 7/8 in. (275.6 x 111.4 cm),"Gift of Bei Shan Tang Foundation, 1993",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.51,false,true,51375,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Song Xu,"Chinese, 1525–after 1606",,Song Xu,Chinese,1525,1610,dated 1587,1587,1587,Fan mounted as an album leaf; ink and color on gold paper,7 1/2 x 21 3/4 in. (19.1 x 55.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.240,false,true,39554,Asian Art,Handscroll,明 丁雲鵬 十八羅漢圖 卷|Eighteen Luohans,China,Ming dynasty (1368–1644),,,,Artist,,Ding Yunpeng,"Chinese, 1547–after 1621",,Ding Yunpeng,Chinese,1547,1621,dated 1609,1609,1609,Handscroll; ink on paper,8 1/4 x 92 in. (21 x 233.7 cm),"Purchase, Friends of Asian Art Gifts, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.6.2,false,true,36094,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Hong,"Chinese, 1577–after 1652",,Zhang Hong,Chinese,1577,1652,dated 1639,1639,1639,Handscroll; ink on paper,10 3/4 x 173 23/32 in. (27.3 x 441.3 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.148.1,false,true,36080,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,After,Zhu Duan,"Chinese, act. ca. 1500–21",,Zhu Duan,Chinese,1490,1531,dated 1518,1518,1518,Hanging scroll; ink and color on silk,Image: 35 1/4 x 76 in. (89.5 x 193 cm) Overall with mounting: 116 5/8 x 41 3/16 in. (296.2 x 104.6 cm) Overall with rollers: 116 5/8 x 46 1/8 in. (296.2 x 117.2 cm),"Gift of Alan Priest, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.335,false,true,48947,Asian Art,Handscroll,明 王上宮 忠孝圖 卷|Paragons of Loyalty and Filial Piety,China,Ming dynasty (1368–1644),,,,Artist,,Wang Shanggong,"Chinese, active 16th century",,Wang Shanggong,Chinese,1500,1599,dated 1593,1593,1593,Handscroll; ink on paper,Image: 11 1/8 in. x 14 ft. 8 3/4 in. (28.3 x 448.9 cm) Overall with mounting: 12 1/4 in. x 38 ft. 1 15/16 in. (31.1 x 1163.2 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.83,false,true,35994,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Qi Zhijia,"Chinese, ca. 1595–ca. 1670",,Qi Zhijia,Chinese,1585,1680,dated Spring 1643,1643,1643,Folding fan mounted as an album leaf; ink on paper,6 1/2 x 19 1/2 in. (16.5 x 49.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.95,false,true,48902,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Guan,"Chinese, active ca. 1610–40",,Chen Guan,Chinese,1600,1650,dated 1629,1368,1644,Folding fan mounted as an album leaf; ink and color on paper,6 1/2 x 19 5/8 in. (16.5 x 49.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.127,false,true,49095,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Bian Wenyu,"Chinese, active ca. 1611–71",,Bian Wenyu,Chinese,1611,1671,dated 1634,1634,1634,Handscroll; ink on paper,10 1/4 x 42 1/2 in. (26 x 108 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.219,false,true,48966,Asian Art,Handscroll,明 趙左 谿山無盡圖 卷|Streams and Mountains without End,China,Ming dynasty (1368–1644),,,,Artist,,Zhao Zuo,"Chinese, ca. 1570–after 1630",,Zhao Zuo,Chinese,1560,1630,Dated 1611–12,1611,1612,Handscroll; ink and color on paper,Image: 9 5/8 x 248 5/8 in. (24.4 x 631.5 cm) Overall with mounting: 11 3/4 x 465 3/4 in. (29.8 x 1183 cm),"Purchase, The Dillon Fund Gift, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.37,false,true,51871,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,Formerly Attributed to,Wang Zhenpeng,"Chinese, active ca. 1275–1330",,WANG ZHENPENG,Chinese,1265,1340,early 15th century,1400,1433,Handscroll; ink on paper,Image: 12 1/4 x 269 in. (31.1 x 683.3 cm) Overall with mounting: 13 3/8 x 443 in. (34 x 1125.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.117.2,false,true,39667,Asian Art,Hanging scroll,明 杜堇 伏生授經圖 軸|The Scholar Fu Sheng Transmitting the Book of Documents,China,Ming dynasty (1368–1644),,,,Artist,,Du Jin,"Chinese, active ca. 1465–1509",,Du Jin,Chinese,1465,1509,15th–mid-16th century,1465,1509,Hanging scroll; ink and color on silk,Image: 57 7/8 × 41 1/8 in. (147 × 104.5 cm) Overall with mounting: 9 ft. 10 3/4 in. × 50 1/4 in. (301.6 × 127.6 cm) Overall with knobs: 9 ft. 10 3/4 in. × 53 in. (301.6 × 134.6 cm),"Gift of Douglas Dillon, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.438.4,false,true,44703,Asian Art,Hanging scroll,明 鍾禮 觀瀑圖 軸|Scholar looking at a waterfall,China,Ming dynasty (1368–1644),,,,Artist,,Zhong Li,"Chinese, active ca. 1480–1500",,Zhong Li,Chinese,1480,1500,late 15th century,1480,1499,Hanging scroll; ink and color on silk,Image: 70 x 40 5/8 in. (177.8 x 103.2 cm) Overall with mounting: 10 ft. 5 1/2 in. × 49 5/8 in. (318.8 × 126 cm) Overall with knobs: 10 ft. 5 1/2 in. × 53 1/4 in. (318.8 × 135.3 cm),"From the P. Y. and Kinmay W. Tang Family Collection, Gift of Oscar L. Tang, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.116,false,true,36024,Asian Art,Hanging scroll,明 居節 石泉圖 軸|The Waterfall,China,Ming dynasty (1368–1644),,,,Artist,,Ju Jie,"Chinese, active ca. 1531–1585",,Ju Jie,Chinese,1531,1585,dated 1559,1368,1644,Hanging scroll; ink on silk,43 1/2 x 9 3/4 in. (110.5 x 24.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.82,false,true,48910,Asian Art,Hanging scroll,"明 侯懋功 高山圖 軸 |High Mountains",China,Ming dynasty (1368–1644),,,,Artist,,Hou Maogong,"Chinese, active ca. 1540–1580",,Hou Maogong,Chinese,1540,1580,dated 1569,1569,1569,Hanging scroll; ink and color on paper,Image: 46 5/8 x 11 in. (118.4 x 27.9 cm) Overall with mounting: 78 3/4 x 17 in. (200 x 43.2 cm) Overall with knobs: 78 3/4 x 20 1/2 in. (200 x 52.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.408.2a–n,false,true,36436,Asian Art,Album,清 張風 山水圖 冊 紙本|Landscapes,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Feng,"Chinese, active ca. 1628–1662",,Zhang Feng,Chinese,1618,1662,dated 1644,1644,1644,Album of twelve leaves; ink and color on paper,Each leaf: 6 1/16 x 9 in. (15.4 x 22.9 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.328,false,true,65013,Asian Art,Handscroll,清 那爾敦布 順治皇帝進京之隊伍賽馬全圖 卷|Horsemanship Competition for the Shunzhi Emperor,China,Ming dynasty (1368–1644),,,,Artist,,Nardunbu,"Manchu, active mid-17th century",,Nardunbu,Manchu,1600,1699,dated 1662,1662,1662,Handscroll; ink and color on paper,8 x 655 in. (20.3 x 1663.7 cm) Height of painting with paper mounting: 9 3/8 in. (23.8 cm); height of painting with brocade mounting: 12 in. (30.5 cm); frontispiece: 8 x 12 3/8 in. (20.3 x 31.4 cm),"Purchase, The Dillon Fund Gift, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.130,false,true,45670,Asian Art,Hanging scroll,"一鱖禾蟹圖|Flowers, fish, and crabs",China,Ming dynasty (1368–1644),,,,Artist,,Liu Jie,"Chinese, active mid-16th century",,Liu Jie,Chinese,1534,1566,mid-16th century,1534,1566,Hanging scroll; ink and color on silk,Image: 69 1/4 × 49 1/2 in. (175.9 × 125.7 cm) Overall with knobs: 8 ft. 6 7/8 in. × 54 in. (261.3 × 137.2 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.75,false,true,35989,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Jichun,"Chinese, active mid-17th century",,Chen Jichun,Chinese,1633,1667,1635,1635,1635,Folding fan mounted as an album leaf; ink and color on gold paper,7 11/32 x 21 23/32 in. (18.7 x 55.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.85,false,true,41191,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Ni Jing,"Chinese, active late 14th century",,Ni Jing,Chinese,1367,1399,14th century,1367,1399,Hanging scroll; ink and pale color on paper,Image: 41 15/16 x 9 3/4 in. (106.5 x 24.8 cm) Overall with mounting: 75 3/4 x 14 1/8 in. (192.4 x 35.9 cm) Overall with knobs: 75 3/4 x 15 15/16 in. (192.4 x 40.5 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.37.134,false,true,45655,Asian Art,Hanging scroll,"元/明 呂敬甫 花蝶圖 軸 |Flowers",China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Lü Jingfu,"Chinese, active late 14th century",,Lü Jingfu,Chinese,1367,1399,14th century,1368,1399,Hanging scroll; ink and color on silk,40 x 20 7/8 in. (101.6 x 53 cm),"Rogers Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.494.4,false,true,39553,Asian Art,Hanging scroll,明 陳子和 古木酒仙圖 軸|Drunken Immortal beneath an old tree,China,Ming dynasty (1368–1644),,,,Artist,,Chen Zihe,"Chinese, active early 16th century",,Chen Zihe,Chinese,1500,1533,early 16th century,1500,1533,Hanging scroll; ink on silk,Image: 69 in. × 40 1/4 in. (175.3 × 102.2 cm) Overall with mounting: 9 ft. 9 in. × 47 3/8 in. (297.2 × 120.3 cm) Overall with knobs: 9 ft. 9 in. × 52 in. (297.2 × 132.1 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.49.1,false,true,36081,Asian Art,Hanging scroll,明 阮祖德 抑齋曾叔祖八十五齡壽像 軸|Portrait of the Artist's Great-Granduncle Yizhai at the Age of Eighty-Five,China,Ming dynasty (1368–1644),,,,Artist,,Ruan Zude,"Chinese, 16th or early 17th century",,Ruan Zude,Chinese,1500,1699,"dated ""xinyou"" (1561 or 1621?)",1561,1621,Hanging scroll; ink and color on silk,Image: 61 3/4 x 37 7/8 in. (156.8 x 96.2 cm),"Seymour Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.49.2,false,true,36082,Asian Art,Hanging scroll,明 阮祖德 老婦像 軸|Portrait of an Old Lady,China,Ming dynasty (1368–1644),,,,Artist,,Ruan Zude,"Chinese, 16th or early 17th century",,Ruan Zude,Chinese,1500,1699,"dated ""xinyou"" (1561 or 1621?)",1561,1621,Hanging scroll; ink and color on silk,Image: 61 3/4 x 37 7/8 in. (156.8 x 96.2 cm),"Seymour Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.494.3,false,true,39552,Asian Art,Hanging scroll,明 劉俊 納諫圖 軸|Remonstrating with the emperor,China,Ming dynasty (1368–1644),,,,Artist,,Liu Jun,"Chinese, active ca. 1475–ca. 1505",,Liu Jun,Chinese,1475,1505,late 15th–early 16th century,1475,1505,Hanging scroll; ink and color on silk,Image: 65 1/2 x 41 3/4 in. (166.4 x 106 cm) Overall with mounting: 116 x 50 in. (294.6 x 127 cm) Overall with knobs: 116 x 54 in. (294.6 x 137.2 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.124,false,true,45680,Asian Art,Handscroll,明 傳蔣嵩 冬景山水圖 卷|Winter Landscape,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Jiang Song,"Chinese, first half of 16th century",,Jiang Song,Chinese,1500,1550,first half of 16th century,1500,1549,Handscroll; ink and color on paper,Image: 12 1/4 x 277 in. (31.1 x 703.6 cm),"Seymour Fund, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.111,false,true,36058,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Zhou Zonglian,probably Ming dynasty (1368–1644),,Zhou Zonglian,Chinese,1368,1644,dated 1532 or 1592,1532,1592,Hanging scroll; ink on silk,Image: 67 3/4 x 37 1/8 in. (172.1 x 94.3 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.195,false,true,36095,Asian Art,Handscroll,明 魏之克(魏克) 金陵四時圖 卷 紙本|Views of Nanjing in the Four Seasons,China,Ming dynasty (1368–1644),,,,Artist,,Wei Zhike,"Chinese, active ca. 1600–after 1636",,Wei Zhike,Chinese,1600,1636,dated 1635,1635,1635,Handscroll; ink and color on paper,Overall with mounting: 12 5/8 in. × 38 ft. 10 in. (32.1 × 1183.6 cm),"Gift of J. T. Tai, 1968",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.79,false,true,35992,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Pan Yunyu,"Chinese, active ca. 15th–16th century",,Pan Yunyu,Chinese,1400,1599,dated 1604?1664?,1604,1604,Folding fan mounted as an album leaf; ink and color on gold paper,6 1/4 x 19 in. (15.9 x 48.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1.5,false,true,45671,Asian Art,Hanging scroll,明 徐敬 歲寒清白圖 軸|The Pure Whiteness of Winter,China,Ming dynasty (1368–1644),,,,Artist,,Xu Jing,"Chinese, active first half 15th century",,Xu Jing,Chinese,1400,1450,dated 1441,1441,1441,Hanging scroll; ink on silk,Image: 58 7/8 x 30 in. (149.5 x 76.2 cm) Overall with mounting: 107 3/4 x 37 1/4 in. (273.7 x 94.6 cm) Overall with knobs: 107 3/4 x 41 1/4 in. (273.7 x 104.8 cm),"Ex coll.: C. C. Wang Family, Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.475.3,false,true,45659,Asian Art,Hanging scroll,"元/明 吳伯理 龍松圖 軸|Dragon Pine",China,Ming dynasty (1368–1644),,,,Artist,,Wu Boli,"Chinese, active late 14th–early 15th century",,Wu Boli,Chinese,1350,1450,ca. 1400,1390,1410,Hanging scroll; ink on paper,Image: 48 x 13 1/4 in. (121.9 x 33.7 cm) Overall with mounting: 100 x 18 5/8 in. (254 x 47.3 cm) Overall with knobs: 100 x 21 in. (254 x 53.3 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.97.9,false,true,51621,Asian Art,Album,清 陸漢 山水八開 冊|Eight Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Lu Han,"Chinese, died 1722",,Lu Han,Chinese,,1722,1699,1699,1699,Album of eight leaves; ink and color on paper,Image (album): 14 × 10 in. (35.6 × 25.4 cm) Image (double leaf): 14 × 20 in. (35.6 × 50.8 cm) Image: 12 × 9 in. (30.5 × 22.9 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.214.149,false,true,49030,Asian Art,Hanging scroll,明/清 王鐸 山水圖 軸|Mountain Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Wang Duo,"Chinese, 1592–1652",,Wang Duo,Chinese,1592,1652,dated 1651,1651,1651,Hanging scroll; ink on paper,46 1/8 x 21 5/8 in. (117.2 x 54.9 cm),"Gift of Ernest Erickson Foundation, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.117,false,true,44626,Asian Art,Hanging scroll,明/清 王鐸 山水圖 軸|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Wang Duo,"Chinese, 1592–1652",,Wang Duo,Chinese,1592,1652,dated 1649,1649,1649,Hanging scroll; ink on satin,Image: 22 x 10 5/8 in. (55.9 x 27 cm) Overall with mounting: 67 x 16 3/4 in. (170.2 x 42.5 cm) Overall with knobs: 67 x 20 3/8 in. (170.2 x 51.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.426.2,false,true,40019,Asian Art,Hanging scroll,清 王時敏 仿黃公望山水圖 軸 絹本|Landscape in the style of Huang Gongwang,China,Qing dynasty (1644–1911),,,,Artist,,Wang Shimin,"Chinese, 1592–1680",,WANG SHIMIN,Chinese,1592,1680,dated 1666,1666,1666,Hanging scroll; ink on paper,Image: 53 x 22 1/4 in. (134.6 x 56.5 cm) Overall with mounting: 87 3/4 x 28 1/4 in. (222.9 x 71.8 cm) Overall with knobs: 87 3/4 x 31 in. (222.9 x 78.7 cm),"Ex coll.: C. C. Wang Family, Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.119,false,true,49106,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Wang Shimin,"Chinese, 1592–1680",,WANG SHIMIN,Chinese,1592,1680,dated 1677,1677,1677,Folding fan mounted as an album leaf; ink and color on white paper,6 3/16 x 19 1/2 in. (15.7 x 49.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.394a–i,false,true,65555,Asian Art,Album,清 蕭雲從 山水圖 冊|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Xiao Yuncong,"Chinese, 1596–1673",,Xiao Yuncong,Chinese,1596,1673,dated 1668,1668,1668,Album of eight paintings; ink and color on paper,Each leaf: 9 1/8 x 6 3/4 in. (23.2 x 17.1 cm),"Gift of Florence and Herbert Irving Collection, in memory of Douglas Dillon, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.268.1,false,true,36089,Asian Art,Album leaf,明/清 項聖謨 秋景圖 冊頁|Autumn Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Xiang Shengmo,"Chinese, 1597–1658",,Xiang Shengmo,Chinese,1597,1658,datable to 1654–55,1654,1655,Leaf from a collective album of many leaves; ink and color on paper,9 3/4 x 13 in. (24.8 x 33 cm),"Seymour Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.4,false,true,49080,Asian Art,Hanging scroll,明/清 項聖謨 白菊圖 軸|White Chrysanthemums,China,Qing dynasty (1644–1911),,,,Artist,,Xiang Shengmo,"Chinese, 1597–1658",,Xiang Shengmo,Chinese,1597,1658,dated 1654,1654,1654,Hanging scroll; color on paper,Image: 30 7/16 x 15 1/2 in. (77.3 x 39.4 cm) Overall with mounting: 60 x 21 in. (152.4 x 53.3 cm) Overall with knobs: 60 x 24 in. (152.4 x 61 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.133,false,true,40020,Asian Art,Hanging scroll,清 倣弘仁 幽谷泉聲圖 軸|The Sound of Spring in a Lonely Valley,China,Qing dynasty (1644–1911),,,,Artist,After,Hongren,"Chinese, 1610–1664",,HONGREN,Chinese,1610,1664,dated 1661,1661,1661,Hanging scroll; ink on paper,Image: 40 1/2 x 16 1/8 in. (102.9 x 41 cm) Overall with mounting: 76 1/2 x 22 in. (194.3 x 55.9 cm) Overall with knobs: 76 1/2 x 24 1/2 in. (194.3 x 62.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.129,false,true,39557,Asian Art,Hanging scroll,清 倣髡殘 蒸嵐昏巒圖 軸|Wooded Mountains at Dusk,China,Qing dynasty (1644–1911),,,,Artist,,Kuncan,"Chinese, 1612–1673",,KUNCAN,Chinese,1612,1673,dated 1666,1666,1666,Hanging scroll; ink and color on paper,Image: 49 1/2 x 24 in. (125.7 x 61 cm) Overall with mounting: 101 1/4 x 31 5/8 in. (257.2 x 80.3 cm) Overall with knobs: 101 1/4 x 34 3/4 in. (257.2 x 88.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.54,false,true,75745,Asian Art,Hanging scroll,清 法若真 雲山圖軸|Cloudy Mountains,China,Qing dynasty (1644–1911),,,,Artist,,Fa Ruozhen,"Chinese, 1613–1696",,Fa Ruozhen,Chinese,1613,1696,1684,1684,1684,"Hanging scroll, ink and color on silk",54 1/8 x 27 3/8 in. (137.5 x 69.5 cm),"Purchase, The Vincent Astor Foundation Gift, 2010",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.132,false,true,49132,Asian Art,Hanging scroll,清 龔賢 冬景山水圖 軸 紙本|Wintry Mountains,China,Qing dynasty (1644–1911),,,,Artist,,Gong Xian,"Chinese, 1619–1689",,GONG XIAN,Chinese,1619,1689,datable ca. 1679–89,1679,1689,Hanging scroll; ink on paper,Image: 65 1/4 x 19 1/4 in. (165.7 x 48.9 cm) Overall with mounting: 114 1/4 x 26 1/2 in. (290.2 x 67.3 cm) Overall with knobs: 114 1/4 x 30 1/4 in. (290.2 x 76.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.499a–l,false,true,41483,Asian Art,Album,清 龔賢 山水圖 冊 紙本|Landscapes and trees,China,Qing dynasty (1644–1911),,,,Artist,,Gong Xian,"Chinese, 1619–1689",,GONG XIAN,Chinese,1619,1689,ca. 1679,1669,1689,Album of twelve leaves; ink on paper,Image (each leaf): 6 1/4 x 7 1/2 in. (15.9 x 19.1 cm),"From the P. Y. and Kinmay W. Tang Family Collection, Gift of Wen and Constance Fong, in honor of Mr. and Mrs. Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.4.1a–o,true,true,36131,Asian Art,Album,清 龔賢 自題 山水十六開 冊|Ink Landscapes with Poems,China,Qing dynasty (1644–1911),,,,Artist,,Gong Xian,"Chinese, 1619–1689",,GONG XIAN,Chinese,1619,1689,dated 1688,1688,1688,Album of sixteen paintings; ink on paper,10 3/4 x 16 1/8 in. (27.3 x 41 cm),"Gift of Douglas Dillon, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.516.2a–c,false,true,51864,Asian Art,Album,清 龔賢 自題山水十六開 冊|Ink Landscapes with Poems,China,Qing dynasty (1644–1911),,,,Artist,,Gong Xian,"Chinese, 1619–1689",,GONG XIAN,Chinese,1619,1689,dated 1688,1688,1688,Album of sixteen paintings; ink on paper,10 13/16 x 14 1/16in. (27.5 x 35.7cm) Overall with mounting: 16 1/8 x 22 7/8in. (41 x 58.1cm) painting/calligraphy: 13 15/16 x 20 9/16in. (35.4 x 52.2cm),"Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.256,false,true,41488,Asian Art,Hanging scroll,清 戴本孝 天台異松圖 軸 紙本|The Strange Pines of Mount Tiantai,China,Qing dynasty (1644–1911),,,,Artist,,Dai Benxiao,"Chinese, 1621–1693",,DAI BENXIAO,Chinese,1621,1693,dated 1687,1687,1687,Hanging scroll; ink on paper,Image: 66 7/8 x 30 in. (169.9 x 76.2 cm) Overall with mounting: 122 3/4 x 37 1/16 in. (311.8 x 94.1 cm) Overall with knobs: 122 3/4 x 41 in. (311.8 x 104.1 cm),"Gift of Marie-Hélène and Guy Weill, in honor of Douglas Dillon, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.13,false,true,51769,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist,Attributed to,Luo Mu,"Chinese, 1622–1706",,Luo Mu,Chinese,1622,1706,dated 1661,1661,1661,Handscroll; ink on paper,12 7/8 x 262 in. (32.7 x 665.5 cm),"Rogers Fund, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.284,false,true,36453,Asian Art,Hanging scroll,清 梅清 雲谷曳杖圖 軸|Strolling in a Misty Valley,China,Qing dynasty (1644–1911),,,,Artist,,Mei Qing,"Chinese, 1623–1697",,MEI QING,Chinese,1623,1697,dated 1649,1649,1649,Hanging scroll; ink on satin,Image: 61 3/8 x 20 1/2 in. (155.9 x 52.1 cm) Overall with mounting: 100 x 26 3/4 in. (254 x 67.9 cm) Overall with knobs: 100 x 30 1/4 in. (254 x 76.8 cm),"Purchase, Soong Family Gift, in memory of Dr. T.V. Soong, 1994",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.208.1,false,true,37394,Asian Art,Hanging scroll,清 倣梅清 響山泛舟圖 軸|Boating beneath Echo Hill,China,Qing dynasty (1644–1911),,,,Artist,After,Mei Qing,"Chinese, 1623–1697",,MEI QING,Chinese,1623,1697,datable to 1673,1673,1673,Hanging scroll; ink on paper,Image: 53 x 23 1/4 in. (134.6 x 59.1 cm) Overall with mounting: 113 1/2 x 32 1/4 in. (288.3 x 81.9 cm) Overall with knobs: 113 1/2 x 36 1/4 in. (288.3 x 92.1 cm),"Gift of Cécile and Sandy Mactaggart, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.721,false,true,39540,Asian Art,Hanging scroll,清 朱耷 (八大山人) 二鷹圖 軸|Two eagles,China,Qing dynasty (1644–1911),,,,Artist,,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,dated 1702,1702,1702,Hanging scroll; ink on paper,Image: 73 3/4 x 35 1/2 in. (187.3 x 90.2 cm) Overall with mounting: 122 3/4 x 42 1/2 in. (311.8 x 108 cm) Overall with knobs: 122 3/4 x 46 1/2 in. (311.8 x 118.1 cm),"Gift of Oscar L. Tang Family, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.135,false,true,49143,Asian Art,Handscroll,清 八大山人 (朱耷) 蓮塘戲禽圖 卷|Birds in a lotus pond,China,Qing dynasty (1644–1911),,,,Artist,,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,ca. 1690,1680,1700,Handscroll; ink on satin,10 3/4 x 80 3/4 in. (27.3 x 205.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.136,false,true,49144,Asian Art,Hanging scroll,"清 朱耷 (八大山人) 倣王羲之 蘭亭序 軸|After Wang Xizhi's (303?-361?) ""Preface to the Orchid Pavilion Gathering""",China,Qing dynasty (1644–1911),,,,Artist,,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,ca. 1694–96,1684,1705,Hanging scroll; ink on paper,Image: 18 3/8 x 10 in. (46.7 x 25.4 cm) Overall with mounting: 66 x 14 1/4 in. (167.6 x 36.2 cm) Overall with knobs: 66 x 17 1/2 in. (167.6 x 44.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.137,false,true,41491,Asian Art,Hanging scroll,清 八大山人(朱耷) 魚石圖 軸|Fish and rocks,China,Qing dynasty (1644–1911),,,,Artist,,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,dated 1699,1699,1699,Hanging scroll; ink on paper,Image: 53 1/4 x 24 in. (135.3 x 61 cm) Overall with mounting: 92 1/2 x 31 1/2 in. (235 x 80 cm) Overall with knobs: 92 1/2 x 34 1/4 in. (235 x 87 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.138a–l,false,true,49145,Asian Art,Album,清 朱耷 (八大山人) 山水圖 冊|Landscape album,China,Qing dynasty (1644–1911),,,,Artist,,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,dated 1699,1699,1699,Album of twelve leaves; ink and color on paper,Image (each): 7 3/8 x 9 1/8 in. (18.7 x 23.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.190,false,true,49149,Asian Art,Hanging scroll,"倣趙伯駒山水圖 軸|Landscape in the Style of Zhao Boju (Fang Zhao Boju shanshui)",China,Qing dynasty (1644–1911),,,,Artist,After,Wang Hui,"Chinese, 1632–1717",,Wang Hui,Chinese,1632,1717,dated 1654,1654,1654,Hanging scroll; ink and color on paper,Image: 23 1/2 x 14 7/8 in. (59.7 x 37.8 cm) Overall with mounting: 76 5/8 x 19 1/2 in. (194.6 x 49.5 cm) Overall with knobs: 76 5/8 x 23 in. (194.6 x 58.4 cm),"Purchase, The Dillon Fund Gift, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.423,false,true,49151,Asian Art,Handscroll,清 王翬 太行山色圖 卷 絹本|The Colors of Mount Taihang,China,Qing dynasty (1644–1911),,,,Artist,,Wang Hui,"Chinese, 1632–1717",,Wang Hui,Chinese,1632,1717,Dated 1669,1669,1669,Handscroll; ink and color on silk,Image: 10 x 82 1/2 in. (25.3 x 209.4 cm) Overall with mounting: 11 3/4 x 348 in. (29.8 x 883.9 cm),"Ex coll.: C. C. Wang Family, Gift of Douglas Dillon, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.33,false,true,51483,Asian Art,Tapestry,,China,Qing dynasty (1644–1911),,,,Artist,In the Style of,Wang Hui,"Chinese, 1632–1717",,Wang Hui,Chinese,1632,1717,dated 1702,1702,1702,Hanging scroll; ink and color on silk,Image: 46 3/4 x 22 1/4 in. (118.7 x 56.5 cm) Overall with mounting: 100 1/2 x 29 5/8 in. (255.3 x 75.2 cm) Overall with knobs: 100 1/2 x 33 3/4 in. (255.3 x 85.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.458.2,false,true,49157,Asian Art,Handscroll,清 王翬 倣巨然燕文貴山水圖 卷|Landscape in the Style of Juran and Yan Wengui,China,Qing dynasty (1644–1911),,,,Artist,,Wang Hui,"Chinese, 1632–1717",,Wang Hui,Chinese,1632,1717,Dated 1713,1713,1713,Handscroll; ink and color on paper,Image: 12 1/4 x 158 3/8 in. (31.1 x 402.3 cm) Overall with mounting: 12 7/8 x 359 1/8 in. (32.7 x 912.2 cm),"Ex coll.: C. C. Wang Family, Gift of Douglas Dillon, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.5a–d,true,true,49156,Asian Art,Handscroll,"清 王翬 等 康熙南巡圖 (卷三: 濟南至泰山) 卷|The Kangxi Emperor's Southern Inspection Tour, Scroll Three: Ji'nan to Mount Tai",China,Qing dynasty (1644–1911),,,,Artist,,Wang Hui,"Chinese, 1632–1717",and assistants,Wang Hui,Chinese,1632,1717,datable to 1698,1698,1698,Handscroll; ink and color on silk,Image: 26 3/4 in. x 45 ft. 8 3/4 in. (67.9 x 1393.8 cm),"Purchase, The Dillon Fund Gift, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.141,false,true,49150,Asian Art,Hanging scroll,清 王翬 溪山雨霽圖 軸|Clearing after Rain over Streams and Mountains,China,Qing dynasty (1644–1911),,,,Artist,,Wang Hui,"Chinese, 1632–1717",,Wang Hui,Chinese,1632,1717,dated 1662,1662,1662,Hanging scroll; ink on paper,Image: 44 7/8 x 17 7/8 in. (114 x 45.4 cm) Overall with mounting: 103 1/2 x 24 1/4 in. (262.9 x 61.6 cm) Overall with knobs: 103 1/2 x 27 1/2 in. (262.9 x 69.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.144,false,true,49154,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Wang Hui,"Chinese, 1632–1717",,Wang Hui,Chinese,1632,1717,dated 1695,1695,1695,Folding fan mounted as an album leaf; ink and color on paper,6 7/16 x 19 3/8 in. (16.4 x 49.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.81,false,true,49158,Asian Art,Handscroll,清 吳歷 墨井草堂消夏圖 卷|Whiling Away the Summer,China,Qing dynasty (1644–1911),,,,Artist,,Wu Li,"Chinese, 1632–1718",,WU LI,Chinese,1632,1718,dated 1679,1679,1679,Handscroll; ink on paper,Image: 14 5/16 x 105 3/4 in. (36.4 x 268.6 cm) Overall with mounting: 14 11/16 x 393 1/16 in. (37.3 x 998.4 cm),"Ex coll.: C. C. Wang Family, Purchase, Douglas Dillon Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.117,false,true,36025,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,In the Style of,Wu Li,"Chinese, 1632–1718",,WU LI,Chinese,1632,1718,spuriously dated 1703,1703,1703,Hanging scroll; ink on paper,39 5/8 x 19 in. (100.6 x 48.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.167a–l,false,true,77870,Asian Art,Album,鄭旼 黃山八景 水墨紙本 九開冊|Eight views of the Yellow Mountains,China,Qing dynasty (1644–1911),,,,Artist,,Zheng Min,"Chinese, 1633–1683",,Zheng Min,Chinese,1633,1683,1681,1681,1681,Album of nine leaves of painting and calligraphy; ink on paper,Image (each leaf): 9 1/2 x 5 1/2 in. (24.1 x 14 cm) Overall with mounting (each double leave): 12 1/8 x 14 3/8 in. (30.8 x 36.5 cm),"Purchase, The Vincent Astor Foundation Gift and Susan Dillon Gift, in honor of James C. Y. Watt, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.470,false,true,41486,Asian Art,Hanging scroll,清 傳惲壽平 夏夜清荷圖 軸|Lotuses on a Summer Evening,China,Qing dynasty (1644–1911),,,,Artist,,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,dated 1684,1684,1684,Hanging scroll; ink and color on paper,Image: 82 5/16 x 38 11/16 in. (209.1 x 98.3 cm) Overall with mounting: 129 x 40 3/8 in. (327.7 x 102.6 cm) Overall with knobs: 129 x 44 3/4 in. (327.7 x 113.7 cm),"Gift of Marie-Hélène and Guy Weill, in honor of Professor Wen Fong, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.12a–m,false,true,49162,Asian Art,Album,清 惲壽平 倣宋元山水圖 冊|Landscapes in the Manner of Song and Yuan Masters,China,Qing dynasty (1644–1911),,,,Artist,,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,dated 1667,1667,1667,Album of ten paintings; ink and color on paper,10 5/8 x 15 5/8 in. (27 x 39.7 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.1.1,false,true,49181,Asian Art,Hanging scroll,清 石濤(朱若極) 黃山三十六峰意圖 軸|Thirty-six Peaks of Mount Huang Recollected,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1705,1695,1707,Hanging scroll; ink on paper,Image: 81 1/16 x 31 in. (205.9 x 78.7 cm) Overall with mounting: 126 x 38 in. (320 x 96.5 cm) Overall with knobs: 126 x 41 1/2 in. (320 x 105.4 cm),"Gift of Douglas Dillon, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.254,false,true,41492,Asian Art,Hanging scroll,"清 石濤 (朱若極) 花石圖 軸|Hibiscus, Lotus, and Rock",China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1705–7,1695,1717,Hanging scroll; ink on paper,Image: 45 9/16 x 22 in. (115.7 x 55.9 cm) Overall with mounting: 88 x 28 1/2 in. (223.5 x 72.4 cm) Overall with knobs: 88 x 31 3/4 in. (223.5 x 80.6 cm),"Gift of Mr. and Mrs. David M. Levitt, by exchange, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.126,false,true,49177,Asian Art,Handscroll,清 石濤(朱若極) 遊張公洞圖 卷|Outing to Zhang Gong's Grotto,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1700,1690,1707,Handscroll; ink and color on paper,Image: 18 1/16 x 112 3/4 in. (45.9 x 286.4 cm) Overall with mounting: 18 7/16 x 363 11/16 in. (46.8 x 923.8 cm),"Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.202,false,true,49178,Asian Art,Hanging scroll,清 石濤 (朱若極) 秋林人醉圖 軸 紙本|Drunk in Autumn Woods,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1702,1692,1712,Hanging scroll; ink and color on paper,Image: 63 3/8 x 27 3/4 in. (161 x 70.5 cm) Overall with mounting: 107 1/2 x 33 3/8 in. (273.1 x 84.8 cm) Overall with knobs: 107 1/2 x 36 7/8 in. (273.1 x 93.7 cm),"Gift of John M. Crawford Jr., 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.475.2,false,true,49172,Asian Art,Hanging scroll,清 石濤(朱若極) 風雨竹圖 軸|Bamboo in Wind and Rain,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1694,1684,1704,Hanging scroll; ink on paper,Image: 87 3/4 x 30 in. (222.9 x 76.2 cm) Overall with mounting: 132 1/4 x 37 3/8 in. (335.9 x 94.9 cm) Overall with knobs: 132 1/4 x 41 in. (335.9 x 104.1 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.227.1,false,true,49170,Asian Art,Handscroll,清 石濤 (朱若極) 十六羅漢圖 卷|The Sixteen Luohans,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,dated 1667,1667,1667,Handscroll; ink on paper,Image: 18 1/4 x 235 3/4 in. (46.4 x 598.8 cm) Overall with mounting: 22 5/16 x 895 in. (56.7 x 2273.3 cm),"Gift of Douglas Dillon, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.13,false,true,49183,Asian Art,Hanging scroll,清 石濤(朱若極) 重陽山水圖 軸|Landscape Painted on the Double Ninth Festival,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,dated 1705,1705,1705,Hanging scroll; ink and color on paper,Image: 28 3/16 x 16 5/8 in. (71.6 x 42.2 cm) Overall with mounting: 86 1/8 x 23 in. (218.8 x 58.4 cm) Overall with knobs: 86 1/8 x 26 3/4 in. (218.8 x 67.9 cm),"Ex coll.: C. C. Wang Family, Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.151,false,true,49171,Asian Art,Album leaf,清 石濤(朱若極) 山水人物圖 冊頁|Landscape with Figure,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1678,1668,1688,Album leaf; ink on paper,8 1/2 x 11 1/4 in. (21.6 x 28.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.153,false,true,49175,Asian Art,Folding fan mounted as an album leaf,清 石濤 (朱若極) 山水圖 扇頁|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,dated 1699,1699,1699,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,6 7/8 x 17 1/2 in. (17.5 x 44.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"13.220.37a, b",false,true,36018,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist,In the Style of,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,19th–early 20th century,1800,1913,Handscroll; ink on paper,19 5/8 in. × 20 ft. 9 3/4 in. (49.8 × 634.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.122a–l,false,true,49176,Asian Art,Album,清 石濤 (朱若極) 野色圖 冊|Wilderness Colors,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1700,1690,1707,Album of twelve paintings: ink and color on paper,Image (each leaf): 10 7/8 x 9 1/2 in. (27.6 x 24.1 cm),"The Sackler Fund, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.280a–n,false,true,49173,Asian Art,Album,清 石濤 (朱若極) 歸棹 冊 紙本|Returning Home,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,ca. 1695,1685,1715,Album of twelve leaves; ink and color on paper,Image (each): 6 1/2 × 4 1/8 in. (16.5 × 10.5 cm) Each leaf with painting: 8 5/16 × 5 5/16 in. (21.1 × 13.5 cm) Each double leaf unfolded: 8 5/16 × 10 5/8 in. (21.1 × 27 cm),"From the P. Y. and Kinmay W. Tang Family, Gift of Wen and Constance Fong, in honor of Mr. and Mrs. Douglas Dillon, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.80,false,true,49187,Asian Art,Handscroll,清 王原祁 輞川圖 卷|Wangchuan Villa,China,Qing dynasty (1644–1911),,,,Artist,,Wang Yuanqi,"Chinese, 1642–1715",,WANG YUANQI,Chinese,1642,1715,dated 1711,1711,1711,Handscroll; ink and color on paper,Image: 14 in. x 17 ft. 10 3/4 in. (35.6 x 545.5 cm) Overall with mounting: 14 3/8 in. x 34 ft. 7 1/2 in. (36.5 x 1055.4 cm),"Ex coll.: C. C. Wang Family, Purchase, Douglas Dillon Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.574,false,true,62743,Asian Art,Hanging scroll,清 王原祁 為瞻亭畫七發妙劑圖 軸|Landscape for Zhanting,China,Qing dynasty (1644–1911),,,,Artist,,Wang Yuanqi,"Chinese, 1642–1715",,WANG YUANQI,Chinese,1642,1715,dated 1710,1710,1710,Hanging scroll; ink and color on paper,Image: 37 1/2 × 18 1/2 in. (95.3 × 47 cm) Overall with mounting: 92 5/8 × 25 1/4 in. (235.3 × 64.1 cm) Overall with knobs: 92 5/8 × 29 in. (235.3 × 73.7 cm),"Gift of Marie-Hélène Weill and Guy A. Weill, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/62743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.6,false,true,41487,Asian Art,Hanging scroll,清 王原祁 倣黃公望高克恭山水圖 軸|Landscape in the Styles of Huang Gongwang and Gao Kegong,China,Qing dynasty (1644–1911),,,,Artist,,Wang Yuanqi,"Chinese, 1642–1715",,WANG YUANQI,Chinese,1642,1715,dated 1705,1705,1705,Hanging scroll; ink on paper,Image: 45 1/8 x 21 1/4 in. (114.6 x 54 cm) Overall with mounting: 102 1/4 x 29 in. (259.7 x 73.7 cm) Overall with knobs: 102 1/4 x 32 3/4 in. (259.7 x 83.2 cm),"Ex coll.: C. C. Wang Family, Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.156,false,true,49185,Asian Art,Hanging scroll,清 王原祁 倣吳鎮山水圖 軸|Landscape after Wu Zhen,China,Qing dynasty (1644–1911),,,,Artist,,Wang Yuanqi,"Chinese, 1642–1715",,WANG YUANQI,Chinese,1642,1715,dated 1695,1695,1695,Hanging scroll; ink on paper,Image: 42 3/4 x 20 1/4 in. (108.6 x 51.4 cm) Overall with mounting: 84 x 27 1/2 in. (213.4 x 69.9 cm) Overall with knobs: 84 x 31 3/4 in. (213.4 x 80.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.157,false,true,49186,Asian Art,Handscroll,清 王原祁 江國垂綸圖 卷|Fishing in River Country at Blossom Time,China,Qing dynasty (1644–1911),,,,Artist,,Wang Yuanqi,"Chinese, 1642–1715",,WANG YUANQI,Chinese,1642,1715,dated 1709,1709,1709,Handscroll; ink and color on paper,10 1/4 x 57 1/2 in. (26 x 146.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.31,false,true,36015,Asian Art,Hanging scroll,清 陳書 白鸚鵡圖 軸|Cockatoo,China,Qing dynasty (1644–1911),,,,Artist,,Chen Shu,"Chinese, 1660–1736",,Chen Shu,Chinese,1660,1736,dated 1721,1721,1721,Hanging scroll; ink and color on paper,37 1/16 x 17 3/16 in. (94.1 x 43.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.119,false,true,49239,Asian Art,Hanging scroll,清 華喦 白芍藥圖 軸|White Peony and Rocks,China,Qing dynasty (1644–1911),,,,Artist,,Hua Yan,"Chinese, 1682–1756",,Hua Yan,Chinese,1682,1756,dated 1752,1752,1752,Hanging scroll; ink and color on paper,Image: 50 1/4 x 22 1/2 in. (127.6 x 57.2 cm) Overall with mounting: 115 × 30 3/8 in. (292.1 × 77.2 cm) Overall with knobs: 115 × 34 3/4 in. (292.1 × 88.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.8a–f,false,true,49237,Asian Art,Album,清 傳高鳳翰 書畫合璧 冊|Landscapes and Calligraphy,China,Qing dynasty (1644–1911),,,,Artist,,Gao Fenghan,"Chinese, 1683–1749",,Gao Fenghan,Chinese,1683,1749,dated 1736,1736,1736,Album of six paintings; ink and color on paper,Each leaf: 12 1/2 x 9 3/4 in. (31.8 x 24.8 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.123,false,true,36027,Asian Art,Hanging scroll,清 張庚 倣王蒙山水圖 軸|Landscape After Wang Meng,China,Qing dynasty (1644–1911),,,,Artist,,Zhang Geng,"Chinese, 1685–1760",,Zhang Geng,Chinese,1685,1760,dated 1759,1759,1759,Hanging scroll; ink and color on paper,49 x 15 31/32 in. (124.5 x 40.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.74.1a–h,false,true,36047,Asian Art,Album,清 張宗蒼 山水 冊 紙本|Miniature landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Zhang Zongcang,"Chinese, 1686–1756",,Zhang Zongcang,Chinese,1686,1756,datable to 1751–54,1741,1764,Album of eight leaves; ink and color on paper,1 7/16 x 1 3/4 in. (3.7 x 4.4 cm),"Fletcher Fund, 1942",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.426.5a–h,false,true,49240,Asian Art,Album,清 汪士慎 山水花卉圖 冊|Landscapes and Flowers,China,Qing dynasty (1644–1911),,,,Artist,,Wang Shishen,"Chinese, 1686–1759",,Wang Shishen,Chinese,1686,1759,dated 1745,1745,1745,Album of eight paintings; ink and color on paper,Image: 8 x 9 7/8 in. (20.3 x 25.1 cm),"Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.438.1,false,true,39767,Asian Art,Hanging scroll,清 傳金農 番馬圖 軸|Grooms and Foreign Horses,China,Qing dynasty (1644–1911),,,,Artist,,Jin Nong,"Chinese, 1687–1773",,Jin Nong,Chinese,1687,1773,17th–18th century,1687,1763,Hanging scroll; ink and color on silk,Image: 27 1/2 x 21 3/4 in. (69.9 x 55.2 cm) Overall with mounting: 92 1/2 x 22 7/8 in. (235 x 58.1 cm) Overall with knobs: 92 1/2 x 26 1/4 in. (235 x 66.7 cm),"The C. C. Wang Family Collection, Gift of C. C. Wang, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.160,false,true,49243,Asian Art,Hanging scroll,清 金農 墨梅圖 軸|Blossoming Prunus,China,Qing dynasty (1644–1911),,,,Artist,,Jin Nong,"Chinese, 1687–1773",,Jin Nong,Chinese,1687,1773,dated 1759,1759,1759,Hanging scroll; ink on paper,Image: 49 3/8 x 17 in. (125.4 x 43.2 cm) Overall with mounting: 85 1/2 x 21 in. (217.2 x 53.3 cm) Overall with knobs: 85 1/2 x 23 3/4 in. (217.2 x 60.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.495a–l,false,true,36432,Asian Art,Album,清 金農 梅花圖 冊|Plum Blossoms,China,Qing dynasty (1644–1911),,,,Artist,,Jin Nong,"Chinese, 1687–1773",,Jin Nong,Chinese,1687,1773,dated 1757,1757,1757,Album of twelve leaves; ink on paper,Image: 10 x 11 3/4 in. (25.4 x 29.8 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.439a–m,false,true,39764,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist,,Jin Nong,"Chinese, 1687–1773",,Jin Nong,Chinese,1687,1773,dated 1754,1754,1754,Album of twelve paintings; ink and color on paper,Each 11 1/4 x 9 3/8 in. (28.6 x 23.8 cm),"Gift of Mr. and Mrs. Wan-go H. C. Weng, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.7,false,true,49244,Asian Art,Handscroll,清 鄭燮 蘭竹圖 卷|Orchids and Bamboo,China,Qing dynasty (1644–1911),,,,Artist,,Zheng Xie,"Chinese, 1693–1765",,Zheng Xie,Chinese,1693,1765,dated 1742,1742,1742,Handscroll; ink on paper,Image: 13 3/4 x 147 1/2 in. (34.9 x 374.7 cm) Overall with mounting: 15 1/8 x 295 1/4 in. (38.4 x 749.9 cm),"Edward Elliott Family Collection, Purchase, Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.322a–d,false,true,44620,Asian Art,Hanging scrolls,清 鄭燮 遠山煙竹圖 軸|Misty Bamboo on a Distant Mountain,China,Qing dynasty (1644–1911),,,,Artist,,Zheng Xie,"Chinese, 1693–1765",,Zheng Xie,Chinese,1693,1765,dated 1753,1753,1753,Set of four hanging scrolls; ink on paper,Overall with mounting (each): 107 1/4 × 27 in. (272.4 × 68.6 cm),"From the P. Y. and Kinmay W. Tang Family Collection, Gift of Oscar L. Tang, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.256,false,true,49247,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Li Fangying,"Chinese, 1696–1754",,Li Fangying,Chinese,1696,1754,Dated 1743,1743,1743,Hanging scroll; ink and color on paper,Image: 44 7/8 x 23 3/8 in. (114 x 59.4 cm) Overall with mounting: 99 1/2 x 29 5/8 in. (252.7 x 75.2 cm) Overall with rollers: 99 1/2 x 33 1/4 in. (252.7 x 84.5 cm),"From the P. Y. and Kinmay W. Tang Family Collection, Gift of Professor Wen Fong, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.163,false,true,49253,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Wang Chen,"Chinese, 1720–1797",,Wang Chen,Chinese,1720,1797,dated 1788,1788,1788,Folding fan mounted as an album leaf; ink and color on paper,7 1/8 x 20 1/4 in. (18.1 x 51.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.510.1,false,true,73161,Asian Art,Hanging scroll,清 浸月圖 軸|Plum Blossoms in Moonlight,China,Qing dynasty (1644–1911),,,,Artist,,Tong Yu,"Chinese, 1721–1782",,Tong Yu,Chinese,1721,1782,second half of the 18th century,1751,1799,Hanging scroll; ink on paper,Image: 49 5/8 x 11 3/4 in. (126 x 29.8 cm),"The Lin Yutang Family Collection, Gift of Hsiang Ju Lin, in memory of Taiyi Lin Lai, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.34,false,true,49254,Asian Art,Hanging scroll,清 羅聘 篠園飲酒圖 軸|Drinking in the Bamboo Garden,China,Qing dynasty (1644–1911),,,,Artist,,Luo Ping,"Chinese, 1733–1799",,Luo Ping,Chinese,1733,1799,dated 1773,1773,1773,Hanging scroll; ink and color on paper,Image: 31 1/2 x 21 1/2 in. (80 x 54.6 cm) Overall with mounting: 116 x 27 7/16 in. (294.6 x 69.7 cm) Overall with knobs: 116 x 32 5/8 in. (294.6 x 82.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.438.3,false,true,39765,Asian Art,Hanging scroll,清 雙駿圖 軸|Two Horses,China,Qing dynasty (1644–1911),,,,Artist,,Qian Feng,"Chinese, 1740–1795",,Qian Feng,Chinese,1740,1795,dated 1793,1793,1793,Hanging scroll; ink and color on paper,Image: 48 5/8 x 20 1/4 in. (123.5 x 51.4 cm) Overall with mounting: 87 1/2 x 25 3/4 in. (222.3 x 65.4 cm) Overall with knobs: 87 1/2 x 29 15/16 in. (222.3 x 76 cm),"The C. C. Wang Family Collection, Gift of C. C. Wang, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.2a–h,false,true,41494,Asian Art,Album,清 伊秉綬 山水 冊頁八開|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Yi Bingshou,"Chinese, 1754–1815",,YI BINGSHOU,Chinese,1754,1815,dated 1814,1814,1814,Album of eight leaves; ink on paper,9 3/4 x 11 5/8 in. (24.8 x 29.5 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.36,false,true,36017,Asian Art,Hanging scroll,清 湯貽汾 山水 軸|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Tang Yifen,"Chinese, 1778–1853",,Tang Yifen,Chinese,1778,1853,dated 1845,1845,1845,Hanging scroll; ink on paper,Image: 46 x 10 3/4 in. (116.8 x 27.3 cm) Overall with mounting: 97 1/2 x 17 1/2 in. (247.7 x 44.5 cm) Overall with knobs: 97 1/2 x 20 1/8 in. (247.7 x 51.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.26,false,true,36160,Asian Art,Hanging scroll,清 王素 東山報捷圖 軸|Bringing the Message of Victory to Dongshan,China,Qing dynasty (1644–1911),,,,Artist,,Wang Su,"Chinese, 1794–1877",,Wang Su,Chinese,1794,1877,dated 1862,1862,1862,Hanging scroll; ink and color on paper,56 1/2 x 31 3/4 in. (143.5 x 80.6 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.16,false,true,36155,Asian Art,Album leaf,清 程庭鷺 山水 冊頁|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Cheng Tinglu,"Chinese, 1796–1858",,Cheng Tinglu,Chinese,1796,1858,dated 1827,1827,1827,Album leaf; ink and color on paper,9 3/4 x 13 1/2 in. (24.8 x 34.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.21,false,true,49449,Asian Art,Folding fan mounted as an album leaf,清 吳熙載 蟬柳 扇面|Cicada,China,Qing dynasty (1644–1911),,,,Artist,,Wu Xizai,"Chinese, 1799–1870",,Wu Xizai,Chinese,1799,1870,dated 1852,1852,1852,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/8 x 20 3/8 in. (18.1 x 51.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.24,false,true,49451,Asian Art,Hanging scroll,清 吳熙載 山水 軸|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Wu Xizai,"Chinese, 1799–1870",,Wu Xizai,Chinese,1799,1870,dated 1858,1858,1858,Hanging scroll; ink and color on paper,61 1/4 x 17 3/16 in. (155.6 x 43.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.215a–m,false,true,49434,Asian Art,Album,清 戴熙 山水 冊頁八開|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Dai Xi,"Chinese, 1801–1860",,Dai Xi,Chinese,1801,1860,dated 1848,1848,1848,Album of eight paintings; ink and color on paper,12 1/4 x 7 5/8 in. (31.1 x 19.4 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.40,false,true,49458,Asian Art,Album leaf,清 張熊 山水 冊頁|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Zhang Xiong,"Chinese, 1803–1886",,Zhang Xiong,Chinese,1803,1886,dated 1827,1827,1827,Album leaf; ink and color on paper,9 3/4 x 13 1/2 in. (24.8 x 34.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.19,false,true,36157,Asian Art,Folding fan mounted as an album leaf,清 劉德六 松鼠葡萄 扇面|Squirrel and Grape,China,Qing dynasty (1644–1911),,,,Artist,,Liu Deliu,"Chinese, 1806–1875",,Liu Deliu,Chinese,1806,1875,dated 1868,1868,1868,Folding fan mounted as an album leaf; ink and color on alum paper,6 15/16 x 20 1/16 in. (17.6 x 51 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.31,false,true,49454,Asian Art,Folding fan mounted as an album leaf,清 胡遠 山水 扇面|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Hu Yuan,"Chinese, 1823–1886",,Hu Yuan,Chinese,1823,1886,dated 1885,1885,1885,Folding fan mounted as an album leaf; ink and color on alum paper,6 15/16 x 20 1/4 in. (17.6 x 51.4 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.53,false,true,49468,Asian Art,Album leaf,清 虛谷 秋帆圖 冊頁|Sailing in Autumn,China,Qing dynasty (1644–1911),,,,Artist,,Xu Gu,"Chinese, 1823–1896",,Xu Gu,Chinese,1823,1896,dated 1893,1893,1893,Album leaf; ink and color on paper,14 1/8 x 35 7/8 in. (35.9 x 91.1 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.54,false,true,49469,Asian Art,Folding fan mounted as an album leaf,清 虛谷 松鼠 扇面|Squirrel on an Autumn Branch,China,Qing dynasty (1644–1911),,,,Artist,,Xu Gu,"Chinese, 1823–1896",,Xu Gu,Chinese,1823,1896,ca. 1880s,1870,1890,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/4 x 19 in. (18.4 x 48.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.27,false,true,36161,Asian Art,Folding fan mounted as an album leaf,清 趙之謙 芍藥 扇面|Peony,China,Qing dynasty (1644–1911),,,,Artist,,Zhao Zhiqian,"Chinese, 1829–1884",,Zhao Zhiqian,Chinese,1829,1884,dated 1862,1862,1862,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,7 x 20 3/4 in. (17.8 x 52.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.28,false,true,49452,Asian Art,Folding fan mounted as an album leaf,清 趙之謙 芍藥 桃花 扇面|Peach Blossoms and Peony,China,Qing dynasty (1644–1911),,,,Artist,,Zhao Zhiqian,"Chinese, 1829–1884",,Zhao Zhiqian,Chinese,1829,1884,ca. 1860,1850,1870,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,7 1/2 x 21 1/2 in. (19.1 x 54.6 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.77,false,true,36187,Asian Art,Folding fan mounted as an album leaf,清 沙馥 松鼠葡萄 扇面|Squirrel and Grapes,China,Qing dynasty (1644–1911),,,,Artist,,Sha Fu,"Chinese, 1831–1906",,Sha Fu,Chinese,1831,1906,dated 1894,1894,1894,Folding fan mounted as an album leaf; ink and color on alum paper,6 5/8 x 19 3/4 in. (16.8 x 50.2 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.78,false,true,49474,Asian Art,Folding fan mounted as an album leaf,清 沙馥 人物 扇面|The Peach and Plum Garden,China,Qing dynasty (1644–1911),,,,Artist,,Sha Fu,"Chinese, 1831–1906",,Sha Fu,Chinese,1831,1906,dated 1879,1879,1879,Folding fan mounted as an album leaf; ink and color on alum paper,7 x 21 3/8 in. (17.8 x 54.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.80,false,true,49476,Asian Art,Folding fan mounted as an album leaf,清 錢慧安 仕女 扇面|A Beauty,China,Qing dynasty (1644–1911),,,,Artist,,Qian Huian,"Chinese, 1833–1911",,Qian Huian,Chinese,1833,1911,dated 1876,1876,1876,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/4 x 21 in. (18.4 x 53.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.45,false,true,49460,Asian Art,Folding fan mounted as an album leaf,清 任薰 高士臨風 扇面|Scholar in the Wind,China,Qing dynasty (1644–1911),,,,Artist,,Ren Xun,"Chinese, 1835–1893",,Ren Xun,Chinese,1835,1893,ca. 1880,1870,1890,Folding fan mounted as an album leaf; ink and color on alum paper,6 9/16 x 9 3/8 in. (16.7 x 23.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.46,false,true,49461,Asian Art,Folding fan mounted as an album leaf,清 任薰 海棠小鳥 扇面|Bird on a Rock by a Flowering Branch,China,Qing dynasty (1644–1911),,,,Artist,,Ren Xun,"Chinese, 1835–1893",,Ren Xun,Chinese,1835,1893,dated 1879,1879,1879,Folding fan mounted as an album leaf; ink and color on alum paper,7 x 20 7/8 in. (17.8 x 53.0 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.131.2,false,true,49464,Asian Art,Album leaf,清 任頤 冊頁|Two Birds Perched on a Flowering Rose Bush,China,Qing dynasty (1644–1911),,,,Artist,,Ren Yi (Ren Bonian),"Chinese, 1840–1896",,Ren Yi (Ren Bonian),Chinese,1840,1896,late 19th century,19,19,Album leaf; ink and color on paper,10 7/8 x 10 5/8 in. (27.6 x 27.0 cm),"Bequest of Louisa L. McNeary, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.324.1,false,true,44566,Asian Art,Hanging scroll,"清 任頤 松鶴圖 軸|Cranes, Pine Tree, and Lichen",China,Qing dynasty (1644–1911),,,,Artist,,Ren Yi (Ren Bonian),"Chinese, 1840–1896",,Ren Yi (Ren Bonian),Chinese,1840,1896,dated 1885,1800,1940,Hanging scroll; ink and color on paper,Image: 57 3/4 x 14 3/4 in. (146.7 x 37.5 cm) Overall with mounting: 95 1/4 x 21 5/8 in. (241.9 x 54.9 cm) Overall with knobs: 95 1/4 x 23 3/4 in. (241.9 x 60.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.208.2,false,true,37393,Asian Art,Hanging scroll,清 任頤 鍾馗 軸|Zhong Kui,China,Qing dynasty (1644–1911),,,,Artist,,Ren Yi (Ren Bonian),"Chinese, 1840–1896",,Ren Yi (Ren Bonian),Chinese,1840,1896,dated 1883,1883,1883,Hanging scroll; ink and color on paper,Image: 67 5/8 x 36 3/4 in. (171.8 x 93.3 cm) Overall with mounting: 120 x 40 5/8 in. (304.8 x 103.2 cm) Overall with knobs: 120 x 44 1/2 in. (304.8 x 113 cm),"Gift of Cécile and Sandy Mactaggart, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.48,false,true,49463,Asian Art,Folding fan mounted as an album leaf,清 任頤 童子水牛 扇面|Herdboy and Buffalo,China,Qing dynasty (1644–1911),,,,Artist,,Ren Yi (Ren Bonian),"Chinese, 1840–1896",,Ren Yi (Ren Bonian),Chinese,1840,1896,dated 1890,1890,1890,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/2 x 21 3/8 in. (19.1 x 54.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.49,false,true,39715,Asian Art,Folding fan mounted as an album leaf,清 任頤 石上讀書 扇面|Scholar on a Rock,China,Qing dynasty (1644–1911),,,,Artist,,Ren Yi (Ren Bonian),"Chinese, 1840–1896",,Ren Yi (Ren Bonian),Chinese,1840,1896,ca. 1880,1870,1890,Folding fan mounted as an album leaf; ink and color on paper,Image: 7 1/2 x 21 3/16 in. (19.1 x 53.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.50,false,true,49462,Asian Art,Hanging scroll,清 任頤 軸|Man on a Bridge,China,Qing dynasty (1644–1911),,,,Artist,,Ren Yi (Ren Bonian),"Chinese, 1840–1896",,Ren Yi (Ren Bonian),Chinese,1840,1896,dated 1889,1889,1889,Hanging scroll; ink on bark paper,36 7/8 x 24 1/4 in. (93.7 x 61.6 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.79,false,true,49475,Asian Art,Folding fan mounted as an album leaf,清 金? 梅花 扇面|Plum,China,Qing dynasty (1644–1911),,,,Artist,,Jin Lan,"Chinese, 1841–1910",,Jin Lan,Chinese,1841,1910,dated 1886,1886,1886,Folding fan mounted as an album leaf; ink and color on alum paper,7 x 21 in. (17.8 x 53.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.68,false,true,36181,Asian Art,Folding fan mounted as an album leaf,清 吳榖祥 山水 扇面|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Wu Guxiang,"Chinese, 1848–1903",,Wu Guxiang,Chinese,1848,1903,dated 1894,1894,1894,Folding fan mounted as an album leaf; ink on gold paper,7 1/4 x 20 5/16 in. (18.4 x 51.6 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.66,false,true,36179,Asian Art,Folding fan mounted as an album leaf,清 徐祥 小鳥水仙 扇面|Birds and Narcissus,China,Qing dynasty (1644–1911),,,,Artist,,Xu Xiang,"Chinese, 1850–1899",,Xu Xiang,Chinese,1850,1899,dated 1883,1883,1883,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/4 x 21 in. (18.4 x 53.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.101,false,true,49602,Asian Art,Folding fan mounted as an album leaf,近代 高邕 楊柳 扇面|Willow,China,Qing dynasty (1644–1911),,,,Artist,,Gao Yong,"Chinese, 1850–1921",,Gao Yong,Chinese,1850,1921,dated 1895,1895,1895,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/4 x 19 3/8 in. (18.4 x 49.2 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.143,false,true,41905,Asian Art,Hanging scroll,近代 吳淑娟 仙姑圖 軸|Female Immortals,China,Qing dynasty (1644–1911),,,,Artist,,Wu Shujuan,"Chinese, 1853–1930",,Wu Shujuan,Chinese,1853,1930,dated 1909,1909,1909,Hanging scroll; ink and color on paper,47 3/4 x 20 5/8 in. (121.3 x 52.4 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.89,false,true,36194,Asian Art,Folding fan mounted as an album leaf,近代 倪田 橋頭隱士 扇面|Scholar on the Bridge,China,Qing dynasty (1644–1911),,,,Artist,,Ni Tian,"Chinese, 1855–1919",,Ni Tian,Chinese,1855,1919,dated 1901,1901,1901,Folding fan mounted as an album leaf; ink and color on alum paper,6 3/8 x 19 5/8 in. (16.2 x 49.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.91,false,true,49594,Asian Art,Hanging scroll,近代 倪田 雙馬圖 軸|Two Horses,China,Qing dynasty (1644–1911),,,,Artist,,Ni Tian,"Chinese, 1855–1919",,Ni Tian,Chinese,1855,1919,dated 1904,1904,1904,Hanging scroll; ink and color on paper,42 7/8 x 19 9/16 in. (108.9 x 49.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.17,false,true,36156,Asian Art,Album leaf,清 沈焯 山水 冊頁|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Shen Zhuo,(active 19th century),,Shen Zhuo,Chinese,1800,1899,dated 1827,1827,1827,Album leaf; ink and color on paper,9 3/4 x 13 1/2 in. (24.8 x 34.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.55a–l,false,true,36171,Asian Art,Album,清 張之萬 山水 冊頁十二開|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Zhang Zhiwan,"Chinese, 1811–1897",,Zhang Zhiwan,Chinese,1811,1897,dated 1875,1875,1875,Album of twelve leaves; ink and color on paper,9 5/16 x 9 13/16 in. (23.7 x 24.9 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.161,false,true,49241,Asian Art,Hanging scroll,清 傳李鱓 墨竹圖 軸|Ink Bamboo,China,Qing dynasty (1644–1911),,,,Artist,,Li Shan,"Chinese, 1686–ca. 1756",,Li Shan,Chinese,1686,1756,dated 1749,1749,1749,Hanging scroll; ink on paper,Image: 52 x 29 1/8 in. (132.1 x 74 cm) Overall with mounting: 99 1/2 x 32 1/8 in. (252.7 x 81.6 cm) Overall with knobs: 99 1/2 x 35 1/2 in. (252.7 x 90.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.41,false,true,36167,Asian Art,Folding fan mounted as an album leaf,清 居巢 蠶蛾 扇面|Silkworm,China,Qing dynasty (1644–1911),,,,Artist,,Zhü Chao,"Chinese, ca. 1823–1889",,Zhü Chao,Chinese,1823,1889,dated 1859,1859,1859,Folding fan mounted as an album leaf; ink and color on alum paper,7 1/4 x 21 in. (18.4 x 53.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.461,false,true,39714,Asian Art,Handscroll,清 佚名 臨袁江瞻園圖 卷|View of a Garden Villa,China,Qing dynasty (1644–1911),,,,Artist,After,Yuan Jiang,active ca.1680–ca.1730,,Yuan Jiang,Chinese,1680,1730,18th century (?),1700,1740,Handscroll; ink and color on silk,Image: 20 1/2 x 116 1/8 in. (52.1 x 295 cm) Overall with mounting: 256 5/8 x 24 7/8 in. (651.8 x 63.2 cm),"From the P. Y. and Kinmay W. Tang Family Collection, Gift of Constance Tang Fong, in honor of her mother, Mrs. P. Y. Tang, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.125a–l,false,true,49227,Asian Art,Hanging scrolls,清 袁江 九成宮圖 屏|The Palace of Nine Perfections,China,Qing dynasty (1644–1911),,,,Artist,,Yuan Jiang,active ca.1680–ca.1730,,Yuan Jiang,Chinese,1680,1730,dated 1691,1691,1691,Set of twelve hanging scrolls; ink and color on silk,Image: 81 1/2 x 18 ft. 5 3/4 in. (207 cm x 563.2 cm) Overall with mounting: 94 1/4 x 19 ft. (239.4 x 579.1 cm),"Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.149,false,true,44581,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Feng Qiyong,"Chinese, active ca. 1730s",,Feng Qiyong,Chinese,1730,1739,dated 1733,1733,1733,Hanging scroll; ink on silk,Image: 45 x 25 5/16 in. (114.3 x 64.3 cm) Overall with mounting: 98 x 32 1/2 in. (248.9 x 82.6 cm) Overall with knobs: 98 x 36 1/4 in. (248.9 x 92.1 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.242.8–.15,false,true,72263,Asian Art,Album,清 樊圻 山水圖 冊 紙本|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Fan Qi,"Chinese, 1616–after 1694",,Fan Qi,Chinese,1616,1694,dated 1646,1646,1646,Album of eight leaves; ink and color on paper,Each leaf: 6 5/8 x 8 in. (16.8 x 20.3 cm),"The Sackler Fund, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.131a–h,false,true,49131,Asian Art,Album,清 樊圻 為玉翁作山水圖 冊|Landscapes Painted for Yuweng,China,Qing dynasty (1644–1911),,,,Artist,,Fan Qi,"Chinese, 1616–after 1694",,Fan Qi,Chinese,1616,1694,dated 1673,1673,1673,Album of eight leaves; ink and color on paper,6 x 7 7/16 in. (15.2 x 18.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.270,false,true,74818,Asian Art,Handscroll,"清 高簡 說詩圖 卷|Discourse on Poetry",China,Qing dynasty (1644–1911),,,,Artist,,Gao Jian,"Chinese, 1634–after 1715",,Gao Jian,Chinese,1634,1725,dated 1698,1698,1698,Handscroll; ink on paper,Image: 13 11/16 x 35 7/16 in. (34.8 x 90 cm),"Purchase, The Vincent Astor Foundation Gift and The Dillon Fund Gift, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.7,false,true,49234,Asian Art,Hanging scroll,清 傳沈銓 高堂雙壽圖 軸|Flowering Crabapple and Pair of Birds,China,Qing dynasty (1644–1911),,,,Artist,,Shen Nanpin (Japanese: Shin Nanpin),"Chinese, 1682–after 1762",,Shen Nanpin,Chinese,1682,1762,dated 1744,1744,1744,Hanging scroll; ink and color on silk,37 x 42 1/4 in. (94.0 x 107.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.81,false,true,36105,Asian Art,Hanging scroll,"桃と月季花(長春花)に鶴図|Cranes, Peach Tree, and Chinese Roses",China,Qing dynasty (1644–1911),,,,Artist,After,Shen Nanpin (Japanese: Shin Nanpin),"Chinese, 1682–after 1762",,Shen Nanpin,Chinese,1682,1762,early 18th century,1700,1733,Hanging scroll; ink and color on silk,Image: 78 1/4 x 39 3/4 in. (198.8 x 101 cm) Overall with mounting: 97 3/4 x 48 1/8 in. (248.3 x 122.2 cm) Overall with knobs: 97 3/4 x 51 1/8 in. (248.3 x 129.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.82,false,true,36106,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Shen Nanpin (Japanese: Shin Nanpin),"Chinese, 1682–after 1762",,Shen Nanpin,Chinese,1682,1762,1750,1750,1750,Hanging scroll; ink and color on silk,Image: 45 13/16 x 19 11/16 in. (116.4 x 50 cm) Overall with mounting: 82 x 26 1/4 in. (208.3 x 66.7 cm) Overall with knobs: 82 x 28 5/16 in. (208.3 x 71.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.510.2,false,true,73159,Asian Art,Hanging scroll,清 李香君小影 軸|Portrait of Li Xiangjun,China,Qing dynasty (1644–1911),,,,Artist,,Cui He,"Chinese, active 1800–1850",,Cui He,Chinese,1800,1850,dated 1817,1817,1817,Hanging scroll; ink and color on paper,Image: 20 5/8 x 49 in. (52.4 x 124.5 cm) Overall with knobs: 104 1/16 x 30 1/4 in. (264.3 x 76.8 cm),"The Lin Yutang Family Collection, Gift of Hsiang Ju Lin, in memory of Taiyi Lin Lai, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.4,false,true,36100,Asian Art,Hanging scroll,清 王鑑 倣黃公望秋山圖 軸 紙本|Landscape in the style of Huang Gongwang,China,Qing dynasty (1644–1911),,,,Artist,,Wang Jian,"Chinese, 1609–1677 or 1688",,Wang Jian,Chinese,1609,1677,dated 1657,1657,1657,Hanging scroll; ink and color on paper,Image: 45 5/8 x 22 1/8 in. (115.9 x 56.2 cm) Overall with mounting: 103 1/4 x 29 in. (262.3 x 73.7 cm) Overall with rollers: 103 1/4 x 32 1/2 in. (262.3 x 82.6 cm),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.76,false,true,36186,Asian Art,Handscroll,清 松年 山水 手卷|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Song Nian,"Chinese, active 19th century",,Song Nian,Chinese,1800,1899,dated 1898,1898,1898,Handscroll in six sections; ink and color on paper,Image: 13 3/8 x 107 7/8 in. (34 x 274 cm) Overall with mounting: 13 5/8 x 298 3/4 in. (34.6 x 758.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.665.1,false,true,59025,Asian Art,Hanging scroll,清 祁豸佳 倣倪瓚冬林亭子圖 軸|Pavilion amongst Wintry Trees after Ni Zan,China,Qing dynasty (1644–1911),,,,Artist,,Qi Zhijia,"Chinese, ca. 1595–ca. 1670",,Qi Zhijia,Chinese,1585,1680,dated 1661,1661,1661,Hanging scroll; ink on paper,45 5/8 x 20 in. (116 x 50.8 cm),"Gift of Marie-Hélène and Guy A. Weill, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/59025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.268.3,false,true,36091,Asian Art,Album leaf,清 施霖 山莊圖 冊頁|Mountain Retreat,China,Qing dynasty (1644–1911),,,,Artist,,Shi Lin,"Chinese, active ca. 1630–60",,Shi Lin,Chinese,1610,1670,datable to 1654–55,1654,1655,Leaf from a collective album of many leaves; ink and color on paper,9 3/4 x 12 3/4 in. (24.8 x 32.4 cm),"Seymour Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.202,false,true,44720,Asian Art,Hanging scroll,清 倣張風 石橋圖 軸|The Stone Bridge,China,Qing dynasty (1644–1911),,,,Artist,,Zhang Feng,"Chinese, active ca. 1628–1662",,Zhang Feng,Chinese,1618,1662,dated 1661,1661,1661,Hanging scroll; ink on paper,Image: 60 3/4 x 18 5/16 in. (154.3 x 46.5 cm) Overall with mounting: 86 3/4 x 25 3/4 in. (220.3 x 65.4 cm) Overall with knobs: 86 3/4 x 29 1/2 in. (220.3 x 74.9 cm),"Purchase, Friends of Asian Art Gifts, The Dillon Fund Gift and Anonymous Gift, 1993",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.746.1,false,true,48946,Asian Art,Hanging scroll,清 倣張風 覓菊圖 軸|Plucking Chrysanthemums,China,Qing dynasty (1644–1911),,,,Artist,After,Zhang Feng,"Chinese, active ca. 1628–1662",,Zhang Feng,Chinese,1618,1662,dated 1658,1658,1658,Hanging scroll; ink on paper,Image: 32 1/2 x 12 in. (82.6 x 30.5 cm) Overall with mounting: 70 1/2 x 17 3/8 in. (179.1 x 44.1 cm) Overall with knobs: 70 1/2 x 21 in. (179.1 x 53.3 cm),"The C. C. Wang Family Collection, Gift of C. C. Wang, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.268.2,false,true,36090,Asian Art,Album leaf,清 葉欣 白鶴嶺圖 冊頁|White Crane Mountain,China,Qing dynasty (1644–1911),,,,Artist,,Ye Xin,"Chinese, active ca. 1640–1673",,Ye Xin,Chinese,1640,1640,datable to 1654–55,1654,1655,Leaf from a collective album of many leaves; ink and color on paper,9 3/4 x 13 in. (24.8 x 33 cm),"Seymour Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.223a–d,false,true,44521,Asian Art,Album,清 葉欣 山水圖 冊 絹本|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Ye Xin,"Chinese, active ca. 1640–1673",,Ye Xin,Chinese,1640,1640,dated 1652,1652,1652,Album of four leaves; ink and color on silk,4 5/8 x 5 1/2in. (11.7 x 14cm),"Purchase, Mrs. C. Y. Chen Gift, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.91,false,true,36001,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Wen Zhi,"Chinese, active mid-17th century",,Wen Zhi,Chinese,1634,1699,dated 1670,1670,1670,Folding fan mounted as an album leaf; ink and color on gold paper,6 1/2 x 19 3/4 in. (16.5 x 50.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.50,false,true,73646,Asian Art,Album,清 高岑 擬古山水圖 冊 絹本|Landscapes in the styles of old masters,China,Qing dynasty (1644–1911),,,,Artist,,Gao Cen,"Chinese, active 1643–after 1682",,Gao Cen,Chinese,1643,1682,dated 1667,1667,1667,Album of ten leaves; ink and color on silk,Each leaf: 8 1/8 x 8 3/8 in. (20.5 x 21.3 cm),"Purchase, C. C. Wang Gift, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.246,false,true,65640,Asian Art,Hanging scroll,清 袁耀 雪棧行旅圖 軸|Hostelry and Travelers in Snowy Mountains,China,Qing dynasty (1644–1911),,,,Artist,,Yuan Yao,"Chinese, active 1730–after 1778",,Yuan Yao,Chinese,1730,1778,dated 1745,1745,1745,Hanging scroll; ink and color on silk,Image: 67 1/4 x 48 3/4 in. (170.8 x 123.8 cm) Overall with mounting: 121 3/4 x 50 1/2 in. (309.2 x 128.3 cm) Overall with knobs: 121 3/4 x 54 1/2 in. (309.2 x 138.4 cm),"Gift of Mr. and Mrs. C. C. Wang and Family, in memory of Douglas Dillon, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"13.220.127a, b",false,true,49250,Asian Art,Handscrolls,清 弘曆(乾隆皇帝) 鹿角雙幅 卷|Two Paintings of Deer Antlers,China,Qing dynasty (1644–1911),,,,Artist,,Qianlong Emperor,"Chinese, (1711–1799; r. 1736–95)",,Qianlong Emperor,Chinese,1711,1799,dated 1762 and 1767,1762,1767,Two handscrolls; ink and color on paper,a: 9 3/4 × 81 1/4 in. (24.8 × 206.4 cm) b: 9 7/8 × 81 1/4 in. (25.1 × 206.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.16a–c,false,true,49251,Asian Art,Handscroll,"清 徐揚等 乾隆南巡圖, 第四卷﹕黃淮交流|The Qianlong Emperor's Southern Inspection Tour, Scroll Four: The Confluence of the Huai and Yellow Rivers (Qianlong nanxun, juan si: Huang Huai jiaoliu)",China,Qing dynasty (1644–1911),,,,Artist,,Xu Yang,"Chinese, active ca. 1750–after 1776",and assistants,XU YANG,Chinese,1750,1776,dated 1770,1770,1770,"Handscroll; ink and color on silk, lacquer box",27 1/8 x 431 1/4 in. (68.8 x 1096.17 cm),"Purchase, The Dillon Fund Gift, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.350a–d,false,true,41493,Asian Art,Handscroll,"清 徐揚 等 乾隆南巡圖 (第六卷﹕大運河至蘇州) 卷|The Qianlong Emperor's Southern Inspection Tour, Scroll Six: Entering Suzhou along the Grand Canal",China,Qing dynasty (1644–1911),,,,Artist,,Xu Yang,"Chinese, active ca. 1750–after 1776",and assistants,XU YANG,Chinese,1750,1776,dated 1770,1770,1770,Handscroll; ink and color on silk,Image: 27 1/8 in. x 784 1/2 in. (68.8 x 1994 cm),"Purchase, The Dillon Fund Gift, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.122,false,true,36133,Asian Art,Hanging scroll,清 李寅 高閣觀瀑圖 軸|View From a Mountain Pavilion,China,Qing dynasty (1644–1911),,,,Artist,,Li Yin,"Chinese, active second half of the 17th–early 18th century",,Li Yin,Chinese,1650,1825,dated 1700,1700,1700,Hanging scroll; ink and color on silk,Image: 87 3/4 x 44 3/4 in. (222.9 x 113.7 cm) Overall with mounting: 123 1/2 x 47 3/4 in. (313.7 x 121.3 cm) Overall with knobs: 123 1/2 x 52 in. (313.7 x 132.1 cm),"Anonymous Gift, in memory of Maitland F. Griggs, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.227.2,false,true,40454,Asian Art,Handscroll,元 張羽材 霖雨圖 卷|Beneficent Rain,China,Yuan dynasty (1271–1368),,,,Artist,,Zhang Yucai,"Chinese, died 1316",,Zhang Yucai,Chinese,,1316,late 13th–early 14th century,1295,1316,Handscroll; ink on silk,Image: 10 9/16 x 107 in. (26.8 x 271.8 cm) Overall with mounting: 11 in. x 24 ft. 11 13/16 in. (27.9 x 753.9 cm),"Gift of Douglas Dillon, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.186,false,true,36459,Asian Art,Hanging scroll,元 邊魯 孔雀芙蓉圖 軸|Peacock and Hollyhocks,China,Yuan dynasty (1271–1368),,,,Artist,,Bian Lu,"Chinese, died 1356",,Bian Lu,Chinese,,1356,mid-14th century,1334,1366,Hanging scroll; ink and color on silk,Image: 66 7/8 x 40 1/4 in. (169.9 x 102.2 cm) Overall with mounting: 103 x 41 1/4 in. (261.6 x 104.8 cm) Overall with knobs: 103 x 45 7/8 in. (261.6 x 116.5 cm),"Purchase, The Dillon Fund and The B. Y. Lam Foundation Gifts, 1995",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1973.120.7a, b",false,true,40456,Asian Art,Hanging scrolls,元 李衎 竹石圖 對軸|Bamboo and rocks,China,Yuan dynasty (1271–1368),,,,Artist,,Li Kan,"Chinese, 1245–1320",,LI KAN,Chinese,1245,1320,dated 1318,1318,1318,Pair of hanging scrolls; ink and color on silk,Image (each): 74 3/4 x 21 3/4 in. (189.9 x 55.2 cm) Overall with mounting (each): 106 3/16 x 22 3/8 in. (269.7 x 56.8 cm) Overall with knobs (each): 106 3/16 x 23 in. (269.7 x 58.4 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.5,false,true,40508,Asian Art,Handscroll,"元 趙孟頫 雙松平遠圖 卷|Twin Pines, Level Distance",China,Yuan dynasty (1271–1368),,,,Artist,,Zhao Mengfu,"Chinese, 1254–1322",,Zhao Mengfu,Chinese,1254,1322,ca. 1310,1310,1310,Handscroll; ink on paper,Image: 10 9/16 x 42 5/16 in. (26.8 x 107.5 cm) Overall with mounting: 10 15/16 x 25 ft. 7 11/16 in. (27.8 x 781.5 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.15a–p,false,true,40511,Asian Art,Album,元 佚名 倣趙孟頫 九歌圖 冊|Nine Songs,China,Yuan dynasty (1271–1368),,,,Artist,After,Zhao Mengfu,"Chinese, 1254–1322",,Zhao Mengfu,Chinese,1254,1322,14th century (?),1300,1368,Album of eleven paintings; ink on paper,10 3/8 x 6 1/4 in. (26.4 x 15.9 cm),"Ex coll.: C. C. Wang Family, Fletcher Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.120.1,false,true,41462,Asian Art,Hanging scroll,元 吳鎮 老松圖 軸|Crooked Pine,China,Yuan dynasty (1271–1368),,,,Artist,,Wu Zhen,"Chinese, 1280–1354",,Wu Zhen,Chinese,1280,1354,dated 1335,1335,1335,Hanging scroll; ink on silk,Image: 65 3/8 x 32 1/2 in. (166.1 x 82.6 cm) Overall with mounting: 100 1/2 x 37 1/8 in. (255.3 x 94.3 cm) Overall with knobs: 100 1/2 x 40 1/2 in. (255.3 x 102.9 cm),"Purchase, The Dillon Fund Gift, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.526.1,false,true,39547,Asian Art,Hanging scroll,"元 吳鎮 高節凌雲圖 軸|Bamboo, old tree, and rock",China,Yuan dynasty (1271–1368),,,,Artist,,Wu Zhen,"Chinese, 1280–1354",,Wu Zhen,Chinese,1280,1354,dated 1338,1338,1338,Hanging scroll; ink on silk,Image: 65 5/8 x 38 1/2 in. (166.7 x 97.8 cm) Overall with mounting: 117 1/2 x 39 1/2 in. (298.5 x 100.3 cm) Overall with knobs: 117 1/2 x 43 7/8 in. (298.5 x 111.4 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.571,false,true,39546,Asian Art,Hanging scroll,元 柯九思 臨文同墨竹圖 軸|Bamboo after Wen Tong,China,Yuan dynasty (1271–1368),,,,Artist,,Ke Jiusi,"Chinese, 1290–1343",,Ke Jiusi,Chinese,1290,1343,dated 1343,1343,1343,Hanging scroll; ink on silk,Image: 42 3/8 x 18 3/4 in. (107.6 x 47.6 cm) Overall with mounting: 98 1/2 x 26 5/8 in. (250.2 x 67.6 cm) Overall with knobs: 98 1/2 x 30 in. (250.2 x 76.2 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.8,false,true,45636,Asian Art,Hanging scroll,元 倪瓚 虞山林壑圖 軸|Woods and Valleys of Mount Yu,China,Yuan dynasty (1271–1368),,,,Artist,,Ni Zan,"Chinese, 1306–1374",,NI ZAN,Chinese,1306,1374,dated 1372,1372,1372,Hanging scroll; ink on paper,Image: 37 1/4 x 14 1/8 in. (94.6 x 35.9 cm) Overall with mounting: 82 x 20 5/8 in. (208.3 x 52.4 cm) Overall with knobs: 82 x 24 5/8 in. (208.3 x 62.5 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.38,false,true,45635,Asian Art,Hanging scroll,元 倪瓚 秋林野興圖 軸|Enjoying the Wilderness in an Autumn Grove,China,Yuan dynasty (1271–1368),,,,Artist,,Ni Zan,"Chinese, 1306–1374",,NI ZAN,Chinese,1306,1374,dated 1339,1339,1339,Hanging scroll; ink on paper,Image: 38 5/8 x 27 1/8 in. (98.1 x 68.9 cm) Overall with mounting: 106 7/8 x 35 7/8 in. (271.5 x 91.1 cm) Overall with knobs: 106 7/8 x 40 in. (271.5 x 101.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.39,false,true,41154,Asian Art,Hanging scroll,元 倪瓚 江渚風林圖 軸|Wind among the Trees on the Riverbank,China,Yuan dynasty (1271–1368),,,,Artist,,Ni Zan,"Chinese, 1306–1374",,NI ZAN,Chinese,1306,1374,dated 1363,1363,1363,Hanging scroll; ink on paper,Image: 23 1/4 x 12 1/4 in. (59.1 x 31.1 cm) Overall with mounting: 102 1/4 x 22 1/4 in. (259.7 x 56.5 cm) Overall with knobs: 102 1/4 x 27 1/2 in. (259.7 x 69.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.426.3,false,true,41185,Asian Art,Hanging scroll,元 張羽 松軒春靄圖 軸|Spring Clouds at the Pine Studio,China,Yuan dynasty (1271–1368),,,,Artist,,Zhang Yu,"Chinese, 1333–1385",,Zhang Yu,Chinese,1333,1333,dated 1366,1366,1366,Hanging scroll; ink and color on paper,Image: 36 1/4 x 12 1/2 in. (92.1 x 31.8 cm) Overall with mounting: 79 1/2 x 18 1/2 in. (201.9 x 47 cm) Overall with knobs: 79 1/2 x 20 7/8 in. (201.9 x 53 cm),"Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1.2,false,true,40515,Asian Art,Hanging scroll,元 李堯夫 蘆葉達摩圖 軸|Bodhidharma crossing the Yangzi River on a reed,China,Yuan dynasty (1271–1368),,,,Artist,,Li Yaofu,"Chinese, active ca. 1300",,LI YAOFU,Chinese,1300,1300,before 1317,1271,1316,Hanging scroll; ink on paper,Image: 33 3/4 × 13 5/16 in. (85.7 × 33.8 cm) Overall with mounting: 61 1/4 × 14 in. (155.6 × 35.6 cm) Overall with knobs: 61 1/4 × 15 7/8 in. (155.6 × 40.3 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.5,false,true,41195,Asian Art,Hanging scroll,元 唐棣 松溪歸漁圖 軸|Returning Fishermen,China,Yuan dynasty (1271–1368),,,,Artist,,Tang Di,"Chinese, ca. 1287–1355",,TANG DI,Chinese,1277,1365,dated 1342,1342,1342,Hanging scroll; ink and color on silk,Image: 52 7/8 x 33 7/8 in. (134.3 x 86 cm) Overall with mounting: 98 3/4 x 38 1/2 in. (250.8 x 97.8 cm) Overall with knobs: 98 3/4 x 42 3/4 in. (250.8 x 108.6 cm),"Ex coll.: C. C. Wang Family, Purchase, Bequest of Joseph H. Durkee, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.36,false,true,40134,Asian Art,Handscroll,元 唐棣 滕王閣圖 卷|The Pavilion of Prince Teng,China,Yuan dynasty (1271–1368),,,,Artist,,Tang Di,"Chinese, ca. 1287–1355",,TANG DI,Chinese,1277,1365,dated 1352,1352,1352,Handscroll; ink on paper,Image: 10 13/16 x 33 1/4 in. (27.5 x 84.5 cm) Overall with mounting: 11 1/8 x 310 11/16 in. (28.3 x 789.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.214.147,false,true,41194,Asian Art,Hanging scroll,元 唐棣 摩詰詩意圖 軸|Landscape after a poem by Wang Wei,China,Yuan dynasty (1271–1368),,,,Artist,,Tang Di,"Chinese, ca. 1287–1355",,TANG DI,Chinese,1277,1365,dated 1323,1323,1323,Hanging scroll; ink and color on silk,Image: 50 3/4 x 27 1/16 in. (128.9 x 68.7 cm) Overall with mounting: 8 ft. 8 1/8 in. x 33 3/4 in. (264.5 x 85.7 cm) Overall with knobs: 8 ft. 8 1/8 in. x 37 in. (264.5 x 94 cm),"Gift of Ernest Erickson Foundation, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.526.2,false,true,39550,Asian Art,Hanging scroll,元 王蒙 素庵圖 軸|The Simple Retreat,China,Yuan dynasty (1271–1368),,,,Artist,,Wang Meng,"Chinese, ca. 1308–1385",,Wang Meng,Chinese,1308,1385,ca. 1370,1360,1380,Hanging scroll; ink and color on paper,Image: 53 3/4 x 17 5/8 in. (136.5 x 44.8 cm) Overall with mounting: 101 1/4 x 24 3/4 in. (257.2 x 62.9 cm) Overall with knobs: 101 1/4 x 28 11/16 in. (257.2 x 72.9 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.32,false,true,41451,Asian Art,Hanging scroll,,China,Yuan dynasty (1271–1368),,,,Artist,After,Zhao Yong,"Chinese, 1289–after 1360",,Zhao Yong,Chinese,1289,1360,dated 1349,1349,1349,Hanging scroll; ink on paper,Image: 45 x 14 7/8 in. (114.3 x 37.8 cm) Overall with mounting: 83 1/2 x 20 3/8 in. (212.1 x 51.8 cm) Overall with knobs: 83 1/2 x 23 in. (212.1 x 58.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.6,false,true,41193,Asian Art,Hanging scroll,元 羅稚川 古木寒鴉圖 軸|Crows in Old Trees,China,Yuan dynasty (1271–1368),,,,Artist,,Luo Zhichuan,"Chinese, active ca. 1300–30",,Luo Zhichuan,Chinese,1290,1340,early 14th century,1300,1330,Hanging scroll; ink and color on silk,Image: 52 x 31 5/8 in. (132.1 x 80.3 cm) Overall with mounting: 9 ft. 6 1/4 in. x 38 3/4 in. (290.2 x 98.4 cm) Overall with knobs: 9 ft. 6 1/4 in. x 42 1/2 in. (290.2 x 108 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.345.1,false,true,36430,Asian Art,Hanging scroll,元 佚名 龍虎圖 軸|Tiger,China,Yuan dynasty (1271–1368),,,,Artist,In the style of,Muqi,"Chinese, ca. 1210–after 1269",,Muqi,Chinese,1210,1269,late 13th–14th century,1271,1368,Hanging scroll; ink on silk,31 11/16 x 15 7/8 in. (80.5 x 40.3 cm),"Gift of Mr. and Mrs. Kwan S. Wong, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.345.2,false,true,36431,Asian Art,Hanging scroll,元 龍虎圖 軸|Dragon,China,Yuan dynasty (1271–1368),,,,Artist,In the style of,Muqi,"Chinese, ca. 1210–after 1269",,Muqi,Chinese,1210,1269,late 13th–14th century,1271,1368,Hanging scroll; ink on silk,31 11/16 x 15 7/8 in. (80.5 x 40.3 cm),"Gift of Mr. and Mrs. Kwan S. Wong, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.438.1,false,true,44701,Asian Art,Hanging scroll,元 鄧宇 竹石圖 軸|Bamboo and rock,China,Yuan dynasty (1271–1368),,,,Artist,,Deng Yu,"Chinese, ca. 1300–after 1378",,Teng Yu,Chinese,1300,1378,ca. 1360–67,1350,1377,Hanging scroll; ink on paper,Image: 53 3/16 x 16 5/8 in. (135.1 x 42.2 cm) Overall with mounting: 104 1/4 x 23 7/8 in. (264.8 x 60.6 cm) Overall with knobs: 104 1/4 x 27 1/8 in. (264.8 x 68.9 cm),"From the P. Y. and Kinmay W. Tang Family Collection, Gift of Oscar L. Tang, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.174a,false,true,40514,Asian Art,Handscroll,元 佚名 臨王振鵬 金明池圖 卷|Dragon Boat Regatta on Jinming Lake,China,Yuan dynasty (1271–1368),,,,Artist,After,Wang Zhenpeng,"Chinese, active ca. 1275–1330",,WANG ZHENPENG,Chinese,1265,1340,14th century (?),1300,1368,Handscroll; ink on silk,Image: 13 1/2 x 17 ft 6 in. (34.3 cm x 53.8 m),"Purchase, Bequest of Dorothy Graham Bennett, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.276,false,true,40513,Asian Art,Handscroll,元 王振鵬 維摩不二圖 卷|Vimalakirti and the Doctrine of Nonduality,China,Yuan dynasty (1271–1368),,,,Artist,,Wang Zhenpeng,"Chinese, active ca. 1275–1330",,WANG ZHENPENG,Chinese,1265,1340,dated 1308,1308,1308,Handscroll; ink on silk,Image: 15 7/16 x 85 15/16 in. (39.2 x 218.3 cm) Overall with mounting: 15 13/16 in. x 29 ft. 4 5/8 in. (40.2 x 895.7 cm),"Purchase, The Dillon Fund Gift, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.13,false,true,42260,Asian Art,Fan mounted as an album leaf,"元 盛懋 秋林漁隱圖 團扇|Recluse Fisherman, Autumn Trees",China,Yuan dynasty (1271–1368),,,,Artist,,Sheng Mou,"Chinese, active ca. 1310–1360",,Sheng Mou,Chinese,1310,1360,dated 1349,1349,1349,Fan mounted as an album leaf; ink and color on silk,Image: 10 1/2 × 13 1/4 in. (26.7 × 33.7 cm),"Ex coll.: C. C. Wang Family, Purchase, Florance Waterbury Bequest and Gift of Mr. and Mrs. Nathan Cummings, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.35,false,true,41146,Asian Art,Hanging scroll,元 盛懋 秋林漁隱圖 軸|Recluse Fishing by Autumn Trees,China,Yuan dynasty (1271–1368),,,,Artist,,Sheng Mou,"Chinese, active ca. 1310–1360",,Sheng Mou,Chinese,1310,1360,dated 1350,1350,1350,Hanging scroll; ink on paper,Image: 40 3/8 x 13 1/8 in. (102.6 x 33.3 cm) Overall with mounting: 90 1/2 x 19 3/4 in. (229.9 x 50.2 cm) Overall with knobs: 90 1/2 x 23 in. (229.9 x 58.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.79,false,true,40309,Asian Art,Handscroll,元 錢選 梨花圖 卷|Pear Blossoms,China,Yuan dynasty (1271–1368),,,,Artist,,Qian Xuan,"Chinese, ca. 1235–before 1307",,QIAN XUAN,Chinese,1235,1307,ca. 1280,1270,1290,Handscroll; ink and color on paper,Image: 12 5/16 x 37 7/8 in. (31.3 x 96.2 cm) Overall with mounting: 12 5/8 x 34 ft. 9 1/8 in. (32.1 x 1059.5 cm),"Purchase, The Dillon Fund Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.6,true,true,40081,Asian Art,Handscroll,元 錢選 王羲之觀鵝圖 卷|Wang Xizhi watching geese,China,Yuan dynasty (1271–1368),,,,Artist,,Qian Xuan,"Chinese, ca. 1235–before 1307",,QIAN XUAN,Chinese,1235,1307,ca. 1295,1285,1305,"Handscroll; ink, color, and gold on paper",Image: 9 1/8 x 36 1/2 in. (23.2 x 92.7 cm) Overall with mounting: 11 x 418 13/16 in. (27.9 x 1063.8 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.438.3,false,true,41476,Asian Art,Album leaf,元 夏永 黃樓圖 冊頁|The Yellow Pavilion,China,Yuan dynasty (1271–1368),,,,Artist,,Xia Yong,"Chinese, active mid-14th century",,XIA YONG,Chinese,1336,1370,ca. 1350,1340,1360,Album leaf; ink on silk,Image: 8 1/8 x 10 1/2 in. (20.6 x 26.7 cm),"Ex coll.: C. C. Wang Family, From the P. Y. and Kinmay W. Tang Family Collection, Gift of Oscar L. Tang, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.10,false,true,40393,Asian Art,Handscroll,元 周東卿 魚樂圖 卷|The Pleasures of Fishes,China,Yuan dynasty (1271–1368),,,,Artist,,Zhou Dongqing,"Chinese, active late 13th century",,ZHOU DONGQING,Chinese,0013,0013,dated 1291,1291,1291,Handscroll; ink and color on paper,Image: 12 1/8 x 19 ft 4 in. (30.8 cm x 593.7 cm) Overall with mounting: 12 5/8 x 441 3/4 in. (32.1 x 1122 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.14,false,true,42261,Asian Art,Fan mounted as an album leaf,元 盛著 秋江垂釣圖 團扇|Angling in the Autumn River,China,Yuan dynasty (1271–1368),,,,Artist,,Sheng Zhu,"Chinese, active late 14th century",,Sheng Zhu,Chinese,1300,1400,ca. 1370,1360,1380,Fan mounted as an album leaf; ink and color on silk,Image: 10 3/16 × 10 13/16 in. (25.9 × 27.5 cm),"Ex coll.: C. C. Wang Family, Purchase, Bequest of Martha T. Fiske Collord, in memory of Josiah M. Fiske, Bequest of Mrs. Beekman Hoppin, and Gift of Herman Cooper, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.8,false,true,45647,Asian Art,Hanging scroll,元 趙原 (元) 晴川送客圖 軸|Farewell by a Stream on a Clear Day,China,Yuan dynasty (1271–1368),,,,Artist,,Zhao Yuan,"Chinese, active ca. 1350–75",,Zhao Yuan,Chinese,1350,1350,second half of the 14th century,1350,1399,Hanging scroll; ink on paper,Image: 37 1/2 x 13 7/8 in. (95.3 x 35.2 cm) Overall with mounting: 77 1/4 x 20 1/2 in. (196.2 x 52.1 cm) Overall with knobs: 77 1/4 x 23 3/8 in. (196.2 x 59.4 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.22,false,true,35970,Asian Art,Hanging scroll,明 丁雲鵬 山水圖 軸|The Lute-song: Farewell at Xunyang,China,late Ming dynasty (1368–1644),,,,Artist,,Ding Yunpeng,"Chinese, 1547–after 1621",,Ding Yunpeng,Chinese,1547,1621,dated 1585,1585,1585,Hanging scroll; ink and color on paper,Image: 55 5/8 x 18 1/8 in. (141.3 x 46 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.438.2,false,true,44702,Asian Art,Fan mounted as an album leaf,元 王蒙 蕭林寂亭圖 團扇|Sparse Trees and Pavilion,China,late Yuan dynasty (1271–1368),,,,Artist,,Wang Meng,"Chinese, ca. 1308–1385",,Wang Meng,Chinese,1308,1385,ca. 1361,1351,1371,Fan mounted as an album leaf; ink on silk,Image: 9 7/8 x 11 1/8 in. (25.1 x 28.3 cm),"Ex coll.: C. C. Wang Family, From the P. Y. and Kinmay W. Tang Family Collection, Gift of Oscar L. Tang, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.479a–c,false,true,39895,Asian Art,Handscrolls,北宋 李公麟 孝經圖 卷|The Classic of Filial Piety,China,Northern Song dynasty (960–1127),,,,Artist,,Li Gonglin,"Chinese, ca. 1041–1106",,Li Gonglin,Chinese,1041,1106,ca. 1085,1075,1095,Handscroll; ink and color on silk,"Overall (a, painting): 8 5/8 x 187 1/4 in. (21.9 x 475.6 cm) Overall (b, colophons): 10 3/8 x 208 5/8 in. (26.4 x 529.9 cm) Overall (c, modern copy preserving seventeenth century silk restorations): 9 1/8 x 196 in. (23.2 x 497.8 cm)","Ex coll.: C. C. Wang Family, From the P. Y. and Kinmay W. Tang Family Collection, Gift of Oscar L. Tang Family, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.276,true,true,39668,Asian Art,Handscroll,"北宋 郭熙 樹色平遠圖 卷 |Old Trees, Level Distance",China,Northern Song dynasty (960–1127),,,,Artist,,Guo Xi,"Chinese, ca. 1000–ca. 1090",,Guo Xi,Chinese,1000,1090,ca. 1080,1070,1090,Handscroll; ink and color on silk,Image: 14 in. × 41 1/8 in. (35.6 × 104.4 cm) Overall with mounting: 14 3/4 in. × 28 ft. 1/8 in. (37.5 × 853.8 cm),"Gift of John M. Crawford Jr., in honor of Douglas Dillon, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.278,true,true,39936,Asian Art,Handscroll,北宋 徽宗 竹禽圖 卷|Finches and bamboo,China,Northern Song dynasty (960–1127),,,,Artist,,Emperor Huizong,"Chinese, 1082–1135; r. 1100–25",,Huizong Emperor,Chinese,1082,1082,early 12th century,1100,1127,Handscroll; ink and color on silk,Image: 13 1/4 × 21 13/16 in. (33.7 × 55.4 cm) Overall with mounting: 13 3/4 in. × 27 ft. 6 5/16 in. (34.9 × 839 cm),"John M. Crawford Jr. Collection, Purchase, Douglas Dillon Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.1,true,true,39915,Asian Art,Handscroll,北宋 傳屈鼎 夏山圖 卷|Summer Mountains,China,Northern Song dynasty (960–1127),,,,Artist,Attributed to,Qu Ding,"Chinese, active ca. 1023–ca. 1056",,QU DING,Chinese,0950,1150,ca. 1050,1040,1060,Handscroll; ink and color on silk,Image: 17 7/8 × 45 3/8 in. (45.4 × 115.3 cm) Overall with mounting: 18 1/4 in. × 23 ft. 2 in. (46.4 × 706.1 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.1,false,true,40007,Asian Art,Handscroll,南宋 米友仁 雲山圖 卷|Cloudy Mountains,China,Southern Song dynasty (1127–1279),,,,Artist,,Mi Youren,"Chinese, 1074–1151",,MI YOUREN,Chinese,1074,1151,before 1200,1127,1199,Handscroll; ink on paper,Image: 10 7/8 × 22 7/16 in. (27.6 × 57 cm) Overall with mounting: 11 3/16 in. × 24 ft. 6 3/16 in. (28.4 × 747.2 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.4,false,true,40284,Asian Art,Handscroll,南宋 趙孟堅 水仙圖 卷|Narcissus,China,Southern Song dynasty (1127–1279),,,,Artist,,Zhao Mengjian,"Chinese, 1199–before 1267",,Zhao Mengjian,Chinese,1199,1267,mid-13th century,1234,1266,Handscroll; ink on paper,Image: 13 1/16 in. × 12 ft. 3 1/4 in. (33.2 × 374 cm) Overall with mounting: 13 7/16 in. × 32 ft. 7 3/16 in. (34.1 × 993.6 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.3,false,true,40054,Asian Art,Handscroll,南宋 馬和之 詩經豳風圖 卷|Odes of the State of Bin,China,Southern Song dynasty (1127–1279),,,,Artist,,Ma Hezhi,"Chinese, ca. 1130–ca. 1170",", and Assistants",MA HEZHI,Chinese,1130,1170,mid-12th century,1134,1166,"Handscroll; ink, color, gold and silver on silk",Image: 10 15/16 in. × 21 ft. 9 1/4 in. (27.8 × 663.6 cm) Overall with mounting: 13 13/16 in. × 45 ft. 10 1/2 in. (35.1 × 1398.3 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.2,false,true,40051,Asian Art,Handscroll,南宋 傳李唐 晉文公復國圖 卷|Duke Wen of Jin Recovering His State,China,Southern Song dynasty (1127–1279),,,,Artist,Attributed to,Li Tang,"Chinese, ca. 1070s–ca. 1150s",,LI TANG,Chinese,1070,1159,mid-12th century,1134,1166,Handscroll; ink and color on silk,Image: 11 9/16 in. × 27 ft. 2 in. (29.4 × 828 cm) Overall with mounting: 11 7/8 in. × 40 ft. 9 1/16 in. (30.2 × 1242.2 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.33.5,false,true,36037,Asian Art,Album leaf,南宋 佚名 倣馬遠 松陰玩月圖 冊頁|Viewing the Moon under a Pine Tree,China,Southern Song dynasty (1127–1279),,,,Artist,After,Ma Yuan,"Chinese, active ca. 1190–1225",,MA YUAN,Chinese,1190,1225,early 13th century,1200,1233,Album leaf; ink and color on silk,10 x 10 in. (25.4 x 25.4 cm),"Rogers Fund, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.9,false,true,40086,Asian Art,Album leaf,南宋 馬遠 高士觀瀑圖 冊頁 絹本|Scholar viewing a waterfall,China,Southern Song dynasty (1127–1279),,,,Artist,,Ma Yuan,"Chinese, active ca. 1190–1225",,MA YUAN,Chinese,1190,1225,late 12th–early 13th century,1190,1225,Album leaf; ink and color on silk,Image: 9 7/8 x 10 1/4 in. (25.1 x 26 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.493.2,false,true,44638,Asian Art,Folding fan mounted as an album leaf,南宋 馬遠 月下賞梅圖 團扇|Viewing plum blossoms by moonlight,China,Southern Song dynasty (1127–1279),,,,Artist,,Ma Yuan,"Chinese, active ca. 1190–1225",,MA YUAN,Chinese,1190,1225,early 13th century,1200,1233,Fan mounted as an album leaf; ink and color on silk,Image: 9 7/8 x 10 1/2 in. (25.1 x 26.7 cm),"Gift of John M. Crawford Jr., in honor of Alfreda Murck, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.102,false,true,36005,Asian Art,Album leaf,"南宋 夏珪 山市晴嵐圖 冊頁|Mountain Market, Clearing Mist",China,Southern Song dynasty (1127–1279),,,,Artist,,Xia Gui,"Chinese, active ca. 1195–1230",,Xia Gui,Chinese,1195,1230,early 13th century,1200,1230,Album leaf; ink on silk,9 3/4 x 8 3/8 in. (24.8 x 21.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.120.10,false,true,40133,Asian Art,Album leaf,南宋 馬麟 蘭花圖 冊頁 絹本|Orchids,China,Southern Song dynasty (1127–1279),,,,Artist,,Ma Lin,"Chinese, ca. 1180– after 1256",,MA LIN,Chinese,1180,1256,second quarter of the 13th century,1226,1250,Album leaf; ink and color on silk,Image: 10 7/16 x 8 7/8 in. (26.5 x 22.5 cm),"Ex coll.: C. C. Wang Family, Gift of The Dillon Fund, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.110,false,true,39937,Asian Art,Album leaf,宋 傳趙克敻 藻魚圖 冊頁|Fish at play,China,Southern Song dynasty (1127–1279),,,,Artist,Attributed to,Zhao Kexiong,"Chinese, active early 12th century",,Zhao Kexiong,Chinese,0012,0012,12th–late 13th century,1127,1279,Album leaf; ink and color on silk,Image: 8 7/8 x 9 7/8 in. (22.5 x 25.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.14,false,true,40090,Asian Art,Fan mounted as an album leaf,南宋 梁楷 澤畔行吟圖 團扇|Poet strolling by a marshy bank,China,Southern Song dynasty (1127–1279),,,,Artist,,Liang Kai,"Chinese, active early 13th century",,Liang Kai,Chinese,1200,1225,early 13th century,1200,1225,Fan mounted as an album leaf; ink on silk,Image: 9 x 9 9/16 in. (22.9 x 24.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.98,false,true,36019,Asian Art,Handscroll,,China,Ming (1368–1644)–Qing (1644–1911) dynasty,,,,Artist,In the Style of,Wen Boren,"Chinese, 1502–ca.1575",,Wen Boren,Chinese,1502,1575,dated 1558,1558,1558,Handscroll; ink and color on paper,Image: 7 1/16 × 48 1/2 in. (17.9 × 123.2 cm) Overall with mounting: 9 9/16 in. × 10 ft. 11 1/2 in. (24.3 × 334 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.171a–j,false,true,49017,Asian Art,Album,明/清 惲向 仿古山水圖 冊 紙本|Landscapes after old masters,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,,Yun Xiang,"Chinese, 1586–1655",,Yun Xiang,Chinese,1586,1655,datable to 1638 or 1650,1638,1650,Album of ten leaves; ink and color on paper,10 1/4 x 6 in. (26 x 15.2 cm),"Purchase, Douglas Dillon Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.16,false,true,45658,Asian Art,Handscroll,元/明 沈巽 竹石圖 卷|Bamboo grove,China,Yuan (1271–1368) or Ming (1368–1644) dynasty,,,,Artist,,Shen Xun,(active ca. 1370–1400),,Shen Xun,Chinese,1370,1400,late 14th century,1367,1399,Handscroll; ink and color on paper,Image: 9 13/16 x 25 in. (24.9 x 63.5 cm) Overall with mounting: 10 1/8 x 347 5/16 in. (25.7 x 882.2 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.15,false,true,45650,Asian Art,Handscroll,元 趙原 (元) 倣燕文貴范寬山水圖 卷|Landscape in the Style of Yan Wengui and Fan Kuan,China,Yuan (1271–1368) or Ming (1368–1644) dynasty,,,,Artist,,Zhao Yuan,"Chinese, active ca. 1350–75",,Zhao Yuan,Chinese,1350,1350,late 14th century,1367,1399,Handscroll; ink on paper,Image: 9 13/16 x 30 1/2 in. (24.9 x 77.5 cm) Overall with mounting: 10 1/8 x 347 5/16 in. (25.7 x 882.2 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.132,false,true,65385,Asian Art,Folding fan mounted as an album leaf,"清 徐揚 仙山樓閣圖 扇面|Palaces of the Immortals",China,"Qing dynasty (1644–1911), Qianlong period (1736–95)",,,,Artist,,Xu Yang,"Chinese, active ca. 1750–after 1776",,XU YANG,Chinese,1750,1776,dated 1753,1753,1753,"Folding fan mounted as an album leaf; ink, color, and gold on paper",Image: 6 1/4 x 18 1/2 in. (15.9 x 47 cm) Sheet: 15 × 24 1/2 in. (38.1 × 62.2 cm),"Purchase, The B. D. G. Leviton Foundation Gift, in honor of Marie-Hélène and Guy Weill, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.364a–l,false,true,44873,Asian Art,Folding fans mounted as album leaves,明/清 陳洪綬 竹石蛺蝶圖 扇面|Landscapes and Flowers,China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist,,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,first half of the 17th century,1600,1649,Twelve folding fans mounted as album leaves; ink and color on gold paper,9 1/2 x 20 1/2 in. (24.1 x 52.1 cm),"Gift of Douglas Dillon, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.7,false,true,45637,Asian Art,Hanging scroll,元 王蒙 丹崖翠壑圖 軸|Red Cliffs and Green Valleys,China,late Yuan (1271–1368)–early Ming (1368–1644) dynasty,,,,Artist,,Wang Meng,"Chinese, ca. 1308–1385",,Wang Meng,Chinese,1308,1385,ca. 1367,1357,1377,Hanging scroll; ink on paper,Image: 26 3/4 x 13 1/2 in. (67.9 x 34.3 cm) Overall with mounting: 91 1/4 x 21 3/8 in. (231.8 x 54.3 cm) Overall with knobs: 91 1/4 x 25 1/8 in. (231.8 x 63.8 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of Darius Ogden Mills and Gift of Mrs. Robert Young, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.139,false,true,39488,Asian Art,Figure,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Yanqing,active 15th century,,Chen Yanqing,Chinese,1400,1499,dated 1438,1438,1438,Gilt brass; lost-wax cast,H. 7 1/2 in. (19 cm); W. 4 3/4 in. (12 cm); D. 2 3/4 in. (7 cm),"Purchase, Friends of Asian Art Gifts, 1997",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/39488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.221.1,false,true,48837,Asian Art,Figure,,China,Tang dynasty (618–907),,,,Artist,,Jin Renrui,"Chinese, 1608–1661",,Jin Renrui,Chinese,1608,1661,,618,907,Whitish earthenware with brown glaze,H. 15 7/8 in. (40.3 cm),"Rogers Fund, 1910",,,,,,,,,,,,Tomb Pottery,,http://www.metmuseum.org/art/collection/search/48837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.20,false,true,40185,Asian Art,Fan mounted as an album leaf,南宋 理宗 行書秋深雨過聯句 團扇|Couplet on an Autumn Sky,China,Song dynasty (960–1279),,,,Artist,,Emperor Lizong,"Chinese, 1205–64, r. 1224–64",,LIZONG EMPEROR,Chinese,1205,1264,,1205,1264,Fan mounted as an album leaf; ink on silk,Image: 9 5/16 x 7 in. (23.7 x 17.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.21,false,true,40187,Asian Art,Fan mounted as an album leaf,南宋 理宗 趙昀 行書錄光宗趙惇題楊補之 《紅梅圖》 賜貴妃詩 團扇|Quatrain on a Spring Garden,China,Song dynasty (960–1279),,,,Artist,,Emperor Lizong,"Chinese, 1205–64, r. 1224–64",,LIZONG EMPEROR,Chinese,1205,1264,,1205,1264,Fan mounted as an album leaf; ink on silk,11 x 9 1/2 in. (27.9 x 24.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.22,false,true,40188,Asian Art,Fan mounted as an album leaf,南宋 理宗 趙昀 行書 《長苦春來》 七絕詩 團扇|Quatrain on Late Spring,China,Song dynasty (960–1279),,,,Artist,,Emperor Lizong,"Chinese, 1205–64, r. 1224–64",,LIZONG EMPEROR,Chinese,1205,1264,,1205,1264,Fan mounted as an album leaf; ink on silk,Image: 9 5/8 × 9 1/4 in. (24.4 × 23.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.11,false,true,40061,Asian Art,Fan mounted as an album leaf,南宋 傳光宗 行楷書高標貞色聯句 團扇|Couplet by Han Yu,China,Song dynasty (960–1279),,,,Artist,Attributed to,Emperor Guangzong,"Chinese, 1147–1200, r. 1190–94",,Emperor Guangzong,Chinese,1147,1200,,1147,1200,Fan mounted as an album leaf; ink on silk,8 3/8 x 8 1/4 in. (21.3 x 21 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.110,false,true,49006,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Song Jue,(1576–1632),,Song Jue,Chinese,1576,1632,,1575,1632,Album leaf; ink on paper,11 1/2 x 18 in. (29.2 x 45.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.111,false,true,49009,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Song Jue,(1576–1632),,Song Jue,Chinese,1576,1632,,1576,1632,Album leaf; ink on paper,8 13/16 x 9 5/8 in. (22.4 x 24.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.94,false,true,48944,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Xing Tong,"Chines, 1551–1612",,Xing Tong,Chinese,1551,1612,,1551,1612,Folding fan mounted as an album leaf; ink on gold paper,6 x 19 in. (15.2 x 48.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.41,false,true,45660,Asian Art,Album leaf,明 沈度 致鏞翁書 冊頁|Letter to Liang Zhongren,China,Ming dynasty (1368–1644),,,,Artist,,Shen Du,"Chinese, 1357–1434",,Shen Du,Chinese,1357,1434,,1357,1434,Album leaf; ink on paper,Image: 10 1/4 x 13 1/4 in. (26 x 33.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.49,false,true,45747,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wang Ao,"Chinese, 1450–1524",,Wang Ao,Chinese,1450,1524,,1450,1524,Folding fan mounted as an album leaf; ink on gold-patterned paper,6 3/4 x 19 3/4 in. (17.1 x 50.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.50,false,true,45748,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wang Ao,"Chinese, 1450–1524",,Wang Ao,Chinese,1450,1524,,1450,1524,Album leaf; ink on patterned paper,9 1/2 x 11 1/2 in. (24.1 x 29.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.52,false,true,42683,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Zhu Yunming,"Chinese, 1461–1527",,Zhu Yunming,Chinese,1461,1527,,1461,1527,Folding fan mounted as an album leaf; ink on gold paper,7 1/2 x 19 1/2 in. (19.1 x 49.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/42683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.66,false,true,45795,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Xu Lin,"Chinese, 1462–1548",,Xu Lin,Chinese,1462,1548,,1462,1548,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 3/8 x 19 in. (16.2 x 48.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.56,false,true,45763,Asian Art,Album leaf,明 唐寅 致若容書 冊頁|Letter to Xu Shangde,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Album leaf; ink on paper,Image: 10 3/4 × 25 1/4 in. (27.3 × 64.1 cm) Image (with title strip): 10 3/4 × 25 7/8 in. (27.3 × 65.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.57,false,true,45764,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Album leaf; ink on gold-flecked paper,10 5/8 x 9 7/8 in. (27 x 25.1cm),"Bequest Of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.58,false,true,53796,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Album leaf; ink on gold-flecked paper,H. 12 1/8 in. (30.8 cm); W. 12 1/8 in. (30.8 cm),"Bequest Of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/53796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.230,false,true,45777,Asian Art,Hanging scroll,明 恭候大駕還自南郊詩 軸|Awaiting the Emperor's Return from the Southern Suburbs,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,,1470,1559,Hanging scroll; ink on paper,Overall: 136 x 39 1/4 in. (345.4 x 99.7 cm) Overall with rollers: 40 3/4 in. (103.5 cm),"Anonymous Gift, 1950",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.61,false,true,45783,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,,1470,1559,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 3/8 x 19 1/2 in. (16.2 x 49.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.50,false,true,45804,Asian Art,Folding fan mounted as an album leaf,明 陳淳 行草重九詩 扇面|Poem on the Double Ninth Festival,China,Ming dynasty (1368–1644),,,,Artist,,Chen Chun,"Chinese, 1483–1544",,Chen Chun,Chinese,1483,1544,,1483,1544,Folding fan mounted as an album leaf; ink on gold paper,Image: 6 3/4 x 20 1/4 in. (17.1 x 51.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.67,false,true,45812,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wang Chong,"Chinese, 1494–1533",,Wang Chong,Chinese,1494,1533,,1494,1533,Album leaf; ink on paper,10 1/8 x 13 3/8 in. (25.7 x 34.0 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.56,false,true,48876,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,After,Wen Peng,"Chinese, 1498–1573",,Wen Peng,Chinese,1498,1573,,1498,1573,Folding fan mounted as an album leaf; ink on gold-flecked paper,7 1/2 x 21 in. (19.1 x 53.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.69,false,true,48874,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Peng,"Chinese, 1498–1573",,Wen Peng,Chinese,1498,1573,,1498,1573,Album leaf; ink on paper,11 1/2 x 15 5/8 in. (29.2 x 39.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.71,false,true,48877,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wang Guxiang,"Chinese, 1501–1568",,Wang Guxiang,Chinese,1501,1568,,1501,1568,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 3/8 x 18 5/8 in. (16.2 x 47.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.72,false,true,48879,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Jia,"Chinese, 1501–1583",,Wen Jia,Chinese,1501,1583,,1501,1583,Album leaf; ink on paper,8 1/2 x 15 1/4 in. (21.6 x 38.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.82,false,true,35993,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Yuan Zhi,"Chinese, 1502–1547",,Yuan Zhi,Chinese,1502,1547,,1502,1547,Folding fan mounted as an album leaf; ink on paper,7 3/8 x 21 in. (18.7 x 53.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.79,false,true,48904,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Zhou Tianqiu,"Chinese, 1514–1595",,Zhou Tianqiu,Chinese,1514,1595,,1514,1595,Album leaf; ink on paper,7 3/8 x 12 in. (18.7 x 30.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.80,false,true,48905,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Zhou Tianqiu,"Chinese, 1514–1595",,Zhou Tianqiu,Chinese,1514,1595,,1514,1595,Folding fan mounted as an album leaf; ink on gold-flecked paper,7 3/16 x 21 1/8 in. (18.3 x 53.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.88,false,true,48915,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Fengyi,"Chinese, 1527–1613",,Zhang Fengyi,Chinese,1527,1613,,1527,1613,Folding fan mounted as an album leaf; ink on gold paper,6 1/4 x 19 1/2 in. (15.9 x 49.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.84,false,true,48917,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wang Zhideng,"Chinese, 1535–1612",,Wang Zhideng,Chinese,1535,1612,,1535,1612,Folding fan mounted as an album leaf; ink on gold paper,7 1/8 x 20 1/4 in. (18.1 x 51.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.85,false,true,48926,Asian Art,Manuscript fragment,明 王穉登 行草書山水窟殘稿 冊頁|Fragment of a Manuscript,China,Ming dynasty (1368–1644),,,,Artist,,Wang Zhideng,"Chinese, 1535–1612",,Wang Zhideng,Chinese,1535,1612,,1535,1612,Album leaf; ink on ruled paper,Image: 9 5/8 x 15 13/16 in. (24.4 x 40.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.86,false,true,48927,Asian Art,Album leaf,明 王穉登 行草書札 冊頁|Letter,China,Ming dynasty (1368–1644),,,,Artist,,Wang Zhideng,"Chinese, 1535–1612",,Wang Zhideng,Chinese,1535,1612,,1535,1612,Album leaf; ink on paper,Image: 9 11/16 x 5 5/16 in. (24.6 x 13.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.90,false,true,48935,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Mo Shilong,"Chinese, 1537–1587",,Mo Shilong,Chinese,1537,1587,,1537,1587,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 x 19 1/16 in. (15.2 x 48.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.92,false,true,48943,Asian Art,Album leaf,明 焦竤 行草書札 冊頁|Letter,China,Ming dynasty (1368–1644),,,,Artist,,Jiao Hong,"Chinese, 1541–1620",,Jiao Hong,Chinese,1541,1620,,1541,1620,Album leaf; ink on paper,10 5/8 x 6 5/8 in. (27 x 16.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.93,false,true,45384,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Tu Long,"Chinese, 1542–1605",,Tu Long,Chinese,1542,1605,,1542,1605,Hanging scroll; ink on paper,Image: 49 3/4 x 10 3/4 in. (126.4 x 27.3 cm) Overall: 80 1/2 x 18 in. (204.5 x 45.7 cm) Overall with knobs: 80 1/2 x 19 3/4 in. (204.5 x 50.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.96,false,true,48956,Asian Art,Album leaf,"明 董其昌 行草致陳繼儒書 冊頁 |Letter to Chen Jiru (1558-1635)",China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,,1555,1636,Album leaf; ink on ruled paper,9 1/16 x 9 1/16 in. (23 x 23 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.97,false,true,48957,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,,1555,1636,Folding fan mounted as an album leaf; ink on gold paper,6 7/8 x 19 7/8 in. (17.5 x 50.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.98,false,true,48958,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,,1555,1636,Handscroll; ink on satin,10 1/2 x 72 5/16 in. (26.7 x 183.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.76,false,true,48963,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Jiru,"Chinese, 1558–1635",,Chen Jiru,Chinese,1558,1635,,1368,1644,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 1/2 x 19 1/2 in. (16.5 x 49.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.103,false,true,48960,Asian Art,Album,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Jiru,"Chinese, 1558–1635",,Chen Jiru,Chinese,1558,1635,,1558,1635,Album of eight leaves; ink on gold-flecked paper,11 13/16 x 6 1/8 in. (30 x 15.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.104,false,true,48961,Asian Art,Album,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Jiru,"Chinese, 1558–1635",,Chen Jiru,Chinese,1558,1635,,1558,1635,Album of eight double leaves; ink on paper,Overall (double leaf): 8 5/8 × 11 3/4 in. (21.9 × 29.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.105,false,true,48962,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Jiru,"Chinese, 1558–1635",,Chen Jiru,Chinese,1558,1635,,1558,1635,Folding fan mounted as an album leaf; ink on gold-flecked paper,7 1/4 x 20 1/4 in. (18.4 x 51.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.106,false,true,48964,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wang Heng,"Chinese, 1561–1609",,Wang Heng,Chinese,1561,1609,,1561,1609,Folding fan mounted as an album leaf; ink on gold-flecked paper,7 1/2 x 22 1/4 in. (19.1 x 56.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1989.363.109a, b",false,true,48970,Asian Art,Hanging scrolls,,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Ruitu,"Chinese, 1570–1641",,Zhang Ruitu,Chinese,1570,1641,,1570,1641,Pair of hanging scrolls; ink on paper,Image (Each): 111 x 18 in. (281.9 x 45.7 cm) Overall (Each): 127 x 21 in. (322.6 x 53.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.121,false,true,49043,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Ni Yuanlu,"Chinese, 1593–1644",,Ni Yuanlu,Chinese,1593,1644,,1593,1644,Folding fan mounted as an album leaf; ink on gold paper,6 5/8 x 18 7/8 in. (16.8 x 47.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.122,false,true,49044,Asian Art,Hanging scroll,明 倪元璐 行草書七絕詩 軸|Calligraphy,China,Ming dynasty (1368–1644),,,,Artist,,Ni Yuanlu,"Chinese, 1593–1644",,Ni Yuanlu,Chinese,1593,1644,,1593,1644,Hanging scroll; ink on paper,Image: 54 7/8 x 24 1/4 in. (139.4 x 61.6 cm) Overall with mounting: 111 x 31 1/2 in. (281.9 x 80 cm) Overall with knobs: 111 x 34 1/2 in. (281.9 x 87.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.52,false,true,49097,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Mingxia,"Chinese, 1601–1654",,Chen Mingxia,Chinese,1601,1654,,1601,1654,Folding fan mounted as an album leaf; ink on paper,6 1/2 x 20 1/2 in. (16.5 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.113,false,true,49015,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Zou Zhilin,"Chinese, 1574–ca. 1654",,Zou Zhilin,Chinese,1574,1664,,1574,1644,Folding fan mounted as an album leaf; ink on mica-flecked paper,6 5/16 x 20 5/8 in. (16 x 52.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.86,false,true,35997,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Li Qiao,"Chinese, active Ming dynasty",,Li Qiao,Chinese,1350,1650,,1368,1644,Folding fan mounted as an album leaf; ink on paper,6 x 20 1/4 in. (16.5 x 51.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.75,false,true,48909,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Ju Jie,"Chinese, active ca. 1531–1585",,Ju Jie,Chinese,1531,1585,,1531,1585,Folding fan mounted as an album leaf; ink on gold paper,6 1/4 x 18 13/16 in. (15.9 x 47.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.77,false,true,48930,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Chen Yuansu,"Chinese, active late 16th century",,Chen Yuansu,Chinese,1500,1599,,1567,1599,Folding fan mounted as an album leaf; ink on gold paper,6 7/8 x 20 7/8 in. (17.5 x 53 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.73,false,true,48897,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Shen Shi,"Chinese, active early 16th century",,Shen Shi,Chinese,1500,1533,,1500,1533,Folding fan mounted as an album leaf; ink on gold-flecked paper,7 1/4 x 19 1/2 in. (18.4 x 49.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.107,false,true,48965,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Lu Yingyang,"Chinese, active early 17th century",,Lu Yingyang,Chinese,1600,1650,,1600,1633,Folding fan mounted as an album leaf; ink on gold-flecked paper,5 15/16 x 18 3/8 in. (15.1 x 46.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.150,false,true,49169,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Sun Yueban,"Chinese, 1639–1708",,Sun Yueban,Chinese,1639,1708,,1644,1708,Hanging scroll; ink on paper,Image: 52 x 17 in. (132.1 x 43.2 cm) Overall: 77 x 22 3/4 in. (195.6 x 57.8 cm) Overall with knobs: 77 x 26 5/8 in. (195.6 x 67.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.152,false,true,49174,Asian Art,Folding fan mounted as an album leaf,清 石濤 (朱若極) 五詩 扇頁|Five Poems,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,,1644,1707,Folding fan mounted as an album leaf; ink on paper,6 7/8 x 17 1/2 in. (17.5 x 44.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.158,false,true,49219,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Wang Shihong,"Chinese, 1658–1723",,Wang Shihong,Chinese,1658,1723,,1658,1723,Folding fan mounted as an album leaf; ink on paper,6 3/8 x 19 1/4 in. (16.2 x 48.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.204,false,true,51901,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Chen Hongshou,"Chinese, 1768–1822",,Chen Hongshou,Chinese,1768,1822,,1786,1822,Hanging scroll; ink on green paper,Image: 64 7/8 x 15 in. (164.8 x 38.1 cm) Overall with mounting: 88 1/4 x 21 3/4 in. (224.2 x 55.2 cm) Overall with knobs: 88 1/4 x 26 in. (224.2 x 66 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1976.384.2a, b",false,true,36122,Asian Art,Hanging scrolls,清 翁同龢 對軸|Regular-script Calligraphic Couplet,China,Qing dynasty (1644–1911),,,,Artist,,Weng Tonghe,"Chinese, 1830–1904",,Weng Tonghe,Chinese,1830,1904,,1830,1904,Pair of hanging scrolls; ink on red printed paper,Image (each): 92 5/8 x 17 7/8 in. (235.3 x 45.4 cm) Overall with mounting (each): 114 1/2 x 22 in. (290.8 x 55.9 cm),"Seymour Fund, 1976",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/36122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.134.3,false,true,51807,Asian Art,Handscroll fragment,,China,Ming dynasty (?) (1368–1644),,,,Artist,,Wang Zhideng,"Chinese, 1535–1612",,Wang Zhideng,Chinese,1535,1612,,1535,1612,Fragment of scroll; ink on paper,71 1/4 x 14 1/4 in. (181.0 x 36.2 cm),"Gift of the Pierpont Morgan Library, 1954",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.74,false,true,51379,Asian Art,Folding fan mounted as an album leaf,,China,late Ming dynasty (1368–1644),,,,Artist,,Fan Yunlin,"Chinese, 1558–1641",,Fan Yunlin,Chinese,1558,1641,,1558,1641,Folding fan mounted as an album leaf; ink and color on gold paper,7 x 21 in. (17.8 x 53.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.325,false,true,39534,Asian Art,Handscroll mounted as hanging scroll,南宋 張即之 行書杜甫樂遊原詩殘卷 軸|Excerpt from “Song of Leyou Park”,China,Southern Song dynasty (1127–1279),,,,Artist,,Zhang Jizhi,"Chinese, 1186–1266",,Zhang Jizhi,Chinese,1186,1266,,1186,1266,Section of a handscroll mounted as a hanging scroll; ink on paper,Image: 12 3/4 x 30 1/4 in. (32.4 x 76.8 cm) Overall with mounting: 48 1/2 x 35 1/2 in. (123.2 x 90.2 cm) Overall with knobs: 48 1/2 x 37 1/2 in. (123.2 x 95.3 cm),"Gift of Sylvan Barnet and William Burto, in honor of Tajima Mitsuru, 2000",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/39534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.382,false,true,42158,Asian Art,Brush Holder,清康熙 顧玨 竹雕筆筒|Brush Holder,China,"Qing dynasty (1644–1911), Kangxi period (1662–1722)",,,,Artist,,Gu Jue,active late 17th century,,Gu Jue,Chinese,1675,1700,,1667,1699,Bamboo with hardwood rim and base,H. 7 in. (17.8 cm); D. 6 1/2 in. (16.5 cm),"Purchase, Eileen W. Bamberger Bequest, in memory of her husband, Max Bamberger, 1994",,,,,,,,,,,,Bamboo,,http://www.metmuseum.org/art/collection/search/42158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.118,false,true,49033,Asian Art,Album leaf,明/清 王鐸 書札 冊頁|Letter,China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist,,Wang Duo,"Chinese, 1592–1652",,Wang Duo,Chinese,1592,1652,,1592,1652,Album leaf; ink on gold-flecked paper,10 3/4 x 6 in. (27.3 x 15.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/49033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.40,false,true,45657,Asian Art,Handscroll,元/明 宋克 草書負郭堂成七律詩 卷|Poem on Retirement,China,late Yuan (1271–1368)–early Ming (1368–1644) dynasty,,,,Artist,,Song Ke,"Chinese, 1327–1387",,Song Ke,Chinese,1327,1387,,1327,1387,Handscroll; ink on gold-flecked paper,Image: 10 1/2 x 27 1/4 in. (26.7 x 69.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.231,false,true,43294,Asian Art,Book,,China,Qing dynasty (1644–1911),,,,Artist,,Dong Gao,"Chinese, 1740–1818",,Dong Gao,Chinese,1740,1818,,1644,1911,"Jade, rosewood",H. 7 5/8 in. (19.4 cm); W. 5 1/2 in. (14 cm); D. 1 5/8 in. (4.1 cm),"Gift of Edward R. Finch, Jr., 1976",,,,,,,,,,,,Jade,,http://www.metmuseum.org/art/collection/search/43294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP37,false,true,63331,Asian Art,Print,,China,,,,,Artist,Original painted by,Wang Youcheng,"Chinese, 698–759",,Wang Youcheng,Chinese,0698,0759,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP22,false,true,63216,Asian Art,Print,,China,,,,,Artist,Original painted by,Li Cheng,"Chinese, 919–967",,LI CHENG,Chinese,0919,0967,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP48,false,true,63351,Asian Art,Print,芥子園畫傳|Study Pavilion and Plum Trees: Page from The Mustard Seed Garden Manual of Painting,China,,,,,Artist,Original painted by,Li Cheng,"Chinese, 919–967",,LI CHENG,Chinese,0919,0967,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP33,false,true,63230,Asian Art,Print,,China,,,,,Artist,Original painted by,Wen Youke,"Chinese, died 1079",,Wen Youke,Chinese,1079,1079,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP24,false,true,63218,Asian Art,Print,,China,,,,,Artist,Original painted by,Mi Youren,"Chinese, 1074–1151",,MI YOUREN,Chinese,1074,1151,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP40,false,true,63336,Asian Art,Print,,China,,,,,Artist,In the Style of,Huang Zujiu,"Chinese, 1269–1354",,Huang Zujiu,Chinese,1269,1354,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 5 13/16 in. (24.4 x 14.8 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP34,false,true,63325,Asian Art,Print,,China,,,,,Artist,Original painted by,Ke Jiusi,"Chinese, 1290–1343",,Ke Jiusi,Chinese,1290,1343,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP10,false,true,63204,Asian Art,Print,,China,,,,,Artist,Original painted by,Yunlin,"Chinese, 1301–1374",,Yunlin,Chinese,1301,1374,,1301,1374,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP25,false,true,63219,Asian Art,Print,,China,,,,,Artist,Original painted by,Ni Zan,"Chinese, 1306–1374",,NI ZAN,Chinese,1306,1374,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP42,false,true,63338,Asian Art,Print,,China,,,,,Artist,Original painted by,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 5 7/8 in. (24.4 x 14.9 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP8,false,true,63201,Asian Art,Print,,China,,,,,Artist,Original by,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP19,false,true,63213,Asian Art,Print,,China,,,,,Artist,Original painted by,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP46,false,true,63345,Asian Art,Print,,China,,,,,Artist,Original painted by,Xu Wei,"Chinese, 1521–1593",,Xu Wei,Chinese,1521,1593,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP41,false,true,63337,Asian Art,Print,,China,,,,,Artist,Original painted by,Li Liufang,"Chinese, 1575–1629",,Li Liufang,Chinese,1575,1629,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 5 7/8 in. (24.4 x 14.9 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP21,false,true,63215,Asian Art,Print,,China,,,,,Artist,Original painted by,Lan Ying,"Chinese, 1585–1664",,Lan Ying,Chinese,1585,1664,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP43,false,true,63339,Asian Art,Print,,China,,,,,Artist,Original painted by,Hongren,"Chinese, 1610–1664",,HONGREN,Chinese,1610,1664,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 5 7/8 in. (24.4 x 14.9 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP44,false,true,63341,Asian Art,Print,,China,,,,,Artist,Original painted by,Yang Wencong,"Chinese, 1597–1645/46",,Yang Wencong,Chinese,1597,1646,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 5 7/8 in. (24.4 x 14.9 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP18,false,true,63212,Asian Art,Print,,China,,,,,Artist,Original painted by,Hu Changbo,"Chinese, active ca. 1601",,Hu Changbo,Chinese,1601,1601,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP20,false,true,63214,Asian Art,Print,,China,,,,,Artist,Original painted by,Li Gonglin,"Chinese, ca. 1041–1106",,Li Gonglin,Chinese,1041,1106,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP23,false,true,63217,Asian Art,Print,,China,,,,,Artist,Original painted by,Wang Meng,"Chinese, ca. 1308–1385",,Wang Meng,Chinese,1308,1385,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP35,false,true,63327,Asian Art,Print,,China,,,,,Artist,Original painted by,Wang Meng,"Chinese, ca. 1308–1385",,Wang Meng,Chinese,1308,1385,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP47,false,true,63348,Asian Art,Print,,China,,,,,Artist,Original painted by,Qing Ji,"Chinese, fl. 1630–1650",,Qing Ji,Chinese,1630,1650,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP15,false,true,63209,Asian Art,Print,,China,,,,,Artist,Original painted by,Huang Guzu,"Chinese, active ca. 900–960",,Huang Guzu,Chinese,0900,0960,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP29,false,true,63226,Asian Art,Print,,China,,,,,Artist,Original painted by,Fan Kuan,"Chinese, active ca. 990–1030",,Fan Kuan,Chinese,0990,1030,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP9,false,true,63203,Asian Art,Print,,China,,,,,Artist,Original painted by,Xia Gui,"Chinese, active ca. 1195–1230",,Xia Gui,Chinese,1195,1230,,1195,1230,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP3,false,true,63078,Asian Art,Print,,China,"Qing dynasty (1644–1911), Kangxi period (1662–1722)",,,,Artist,Workshop of,Ding Liangxian,active first half of the 18th century,,Ding Liang-xian,Chinese,1700,1749,,1662,1722,Polychrome woodblock print; ink and color on paper,Image: 13 1/4 × 10 7/8 in. (33.7 × 27.6 cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.44,false,true,51548,Asian Art,Handscroll,壺人樂聚|Merry Gatherings in the Magic Jar,China,Song dynasty (960–1279),,,,Artist,,Gong Kai,"Chinese, 1222–after 1304",,Gong Kai,Chinese,1222,1310,,1222,1279,Handscroll; ink on paper,11 3/4 in. × 14 ft. 2 in. (29.8 × 431.8 cm),"Rogers Fund, 1924",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.2,false,true,39920,Asian Art,Handscroll,北宋 傳趙令穰 江村秋曉圖 卷|River Village in Autumn Dawn,China,Song dynasty (960–1279),,,,Artist,Attributed to,Zhao Lingrang,"Chinese, active ca. 1070– after 1100",,ZHAO LINGRANG,Chinese,1070,1100,,1070,1170,Handscroll; ink and color on silk,Image: 9 5/16 x 41 in. (23.7 x 104.1 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.205,false,true,45685,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1427,1509,Handscroll; ink and color on paper,10 13/16 x 198 3/4 in. (27.5 x 504.8 cm),"Purchase, The Dillon Fund Gift, in memory of Phyllis E. Dillon, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.65,false,true,45793,Asian Art,Handscroll,明 佚名 怡松圖 卷|Enjoying the Pines,China,Ming dynasty (1368–1644),,,,Artist,Unidentified Artist after,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1368,1644,Handscroll frontispiece; ink on gold-flecked paper,Image: 12 3/4 x 57 in. (32.4 x 144.8 cm) Overall with mounting: 13 3/16 x 252 15/16 in. (33.5 x 642.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.81,false,true,45773,Asian Art,Album leaves mounted as a handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Eight album leaves mounted as a handscroll; ink and color on silk,Image (All leaves): 12 3/4 x 162 7/8 in. (32.4 x 413.7 cm) Overall with mounting: 13 1/16 x 306 5/16 in. (33.2 x 778 cm) Image (Leaf 1): 12 3/4 x 17 in. (32.4 x 43.2 cm) Image (Leaf 2): 12 3/4 x 17 in. (32.4 x 43.2 cm) Image (Leaf 3): 12 3/4 x 16 7/8 in. (32.4 x 42.9 cm) Image (Leaf 4): 12 3/4 x 16 3/4 in. (32.4 x 42.5 cm) Image (Leaf 5): 12 3/4 x 16 7/8 in. (32.4 x 42.9 cm) Image (Leaf 6): 12 3/4 x 16 13/16 in. (32.4 x 42.7 cm) Image (Leaf 7): 12 3/4 x 16 7/8 in. (32.4 x 42.9 cm) Image (Leaf 8): 12 3/4 x 16 7/8 in. (32.4 x 42.9 cm),"Ex coll.: C. C. Wang Family, Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.7.2,false,true,45772,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,20 3/4 x 9 3/4 in. (52.7 x 24.8 cm),"Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.129,false,true,36062,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Handscroll; ink on silk,Image: 8 × 24 1/4 in. (20.3 × 61.6 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.394.1,false,true,44573,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 13/16 x 19 5/8 in. (17.3 x 49.8 cm),"Edward Elliott Family Collection, Douglas Dillon Gift, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.54,false,true,45756,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,After,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Handscroll; ink on paper,Image: 11 7/16 x 59 1/8 in. (29.1 x 150.2 cm) Overall with mounting: 11 7/8 x 289 9/16 in. (30.2 x 735.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.55,false,true,45760,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Tang Yin,"Chinese, 1470–1524",,Tang Yin,Chinese,1470,1524,,1470,1524,Hanging scroll; ink on paper,Image: 28 7/16 x 14 9/16 in. (72.2 x 37 cm) Overall with mounting: 64 3/4 x 21 in. (164.5 x 53.3 cm) Overall with knobs: 64 3/4 x 23 3/4 in. (164.5 x 60.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.9a–t,false,true,36103,Asian Art,Album,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhengming,"Chinese, 1470–1559",,Wen Zhengming,Chinese,1470,1559,,1470,1559,"Album of eight painting leaves with facing sheets inscribed with poems, preceded by a four-sheet title piece; ink on paper",8 1/4 x 7 3/4 in. (21.0 x 19.7 cm) (each album leaf); 8 1/4 x 16 7/8 in. (21.0 x 42.9 cm) (two double calligraphy leaves),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.273,false,true,45821,Asian Art,Hanging scroll,明 陸治 枚乘獨坐圖 軸|Mei Cheng Sitting Alone,China,Ming dynasty (1368–1644),,,,Artist,,Lu Zhi,"Chinese, 1495–1576",,Lu Zhi,Chinese,1495,1576,,1495,1576,Hanging scroll; ink and color on paper,Image: 48 7/8 x 15 1/8 in. (124.1 x 38.4 cm) Overall with mounting: 87 3/8 x 30 1/8 in. (221.9 x 76.5 cm) Overall with knobs: 87 3/8 x 34 1/2 in. (221.9 x 87.6 cm),"Gift of Herbert and Jeanine Coyne, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.68,false,true,48873,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Lu Zhi,"Chinese, 1495–1576",,Lu Zhi,Chinese,1495,1576,,1495,1576,Folding fan mounted as an album leaf; ink and color on gold paper,6 1/2 x 18 3/8 in. (16.5 x 46.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.95,false,true,49256,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Zhu Lu,"Chinese, 1553–1632",,Zhu Lu,Chinese,1553,1632,,1552,1632,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 5/8 x 20 5/8 in. (16.8 x 52.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.101,false,true,51872,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,,1555,1636,Hanging scroll; ink on paper,Image: 37 5/8 x 16 3/4 in. (95.6 x 42.5 cm) Overall with mounting: 84 5/8 x 23 in. (214.9 x 58.4 cm) Overall with knobs: 84 5/8 x 25 3/4 in. (214.9 x 65.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.2,false,true,48968,Asian Art,Hanging scroll,明 張瑞圖 山水圖 軸|Mountains Along Riverbanks,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Ruitu,"Chinese, 1570–1641",,Zhang Ruitu,Chinese,1570,1641,,1570,1641,Hanging scroll; ink on satin,Image: 65 3/4 x 20 1/4 in. (167 x 51.4 cm) Overall with mounting: 105 x 27 1/2 in. (266.7 x 69.9 cm) Overall with knobs: 105 x 31 1/2 in. (266.7 x 80 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.55,false,true,49077,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Xiang Shengmo,"Chinese, 1597–1658",,Xiang Shengmo,Chinese,1597,1658,,1597,1644,Folding fan mounted as an album leaf; ink on gold paper,6 1/2 x 20 in. (16.5 x 50.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.2,false,true,36099,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,After,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,,1350,1750,Hanging scroll; ink and color on silk,Image: 63 7/8 x 23 5/8 in. (162.2 x 60 cm) Overall with mounting: 93 1/2 x 27 1/2 in. (237.5 x 69.9 cm) Overall with rollers: 93 1/2 x 31 1/4 in. (237.5 x 79.4 cm),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.308,false,true,39628,Asian Art,Hanging scroll,明 藍孟 溪山雪泛圖 軸 紙本|Boating amid Snowy Streams and Mountains,China,Ming dynasty (1368–1644),,,,Artist,,Lan Meng,ca. 1614–after 1671,,Lan Meng,Chinese,1614,1671,,1614,1644,Hanging scroll; ink and color on silk,Image: 90 x 37 1/4 in. (228.6 x 94.6 cm),"Gift of John and Lili Bussel Family,1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.664,false,true,50168,Asian Art,Hanging scroll,明 史忠 雪景山水圖 軸|Winter Landscape with Fisherman,China,Ming dynasty (1368–1644),,,,Artist,,Shi Zhong,"Chinese, 1438–ca. 1517",,Shi Zhong,Chinese,1438,1517,,1438,1517,Hanging scroll; ink on paper,Image: 56 x 12 5/8 in. (142.2 x 32.1 cm) Overall with mounting: 83 1/2 x 19 1/8 in. (212.1 x 48.6 cm) Overall with knobs: 83 1/2 x 22 1/2 in. (212.1 x 57.2 cm),"Ex coll.: C. C. Wang Family, Gift of C. C. Wang, in honor of Wen C. Fong, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.342,false,true,45810,Asian Art,Hanging scroll,明 謝時臣 谿山春曉圖 軸|Spring Morning in the Mountains,China,Ming dynasty (1368–1644),,,,Artist,,Xie Shichen,"Chinese, 1487–ca. 1567",,Xie Shichen,Chinese,1487,1567,,1487,1567,Hanging scroll; ink and color on silk,Image: 76 1/8 x 41 1/4 in. (193.4 x 104.8 cm) Overall with mounting: 112 3/4 x 42 9/16 in. (286.4 x 108.1 cm) Overall with knobs: 112 3/4 x 46 3/8 in. (286.4 x 117.8 cm),"Gift of John N. Loomis, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.81,false,true,45807,Asian Art,Folding fan mounted as an album leaf,明 謝時臣 隱居圖 扇面|Landscape with Figure,China,Ming dynasty (1368–1644),,,,Artist,,Xie Shichen,"Chinese, 1487–ca. 1567",,Xie Shichen,Chinese,1487,1567,,1487,1577,Folding fan mounted as an album leaf; ink on gold paper,7 x 19 3/4 in. (17.8 x 50.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1982.1.9a, b",false,true,36138,Asian Art,Hanging scroll,明 傳謝時臣 松溪琴客圖 軸|Listening to the Zither Among Streams and Pines,China,Ming dynasty (1368–1644),,,,Artist,,Xie Shichen,"Chinese, 1487–ca. 1567",,Xie Shichen,Chinese,1487,1567,,1487,1567,Hanging scroll; ink and color on silk,Image: 55 1/2 x 28 in. (141 x 71.1 cm) Overall with mounting: 124 x 36 in. (315 x 91.4 cm) Overall with knobs: 124 x 38 3/4 in. (315 x 98.4 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.112,false,true,49013,Asian Art,Folding fan mounted as an album leaf,"明 鄒之麟 山水圖 扇面 |Landscape",China,Ming dynasty (1368–1644),,,,Artist,,Zou Zhilin,"Chinese, 1574–ca. 1654",,Zou Zhilin,Chinese,1574,1664,,1574,1644,Folding fan mounted as an album leaf; ink on gold paper,Image: 9 1/2 × 20 1/8 in. (24.1 × 51.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.296,false,true,64972,Asian Art,Hanging scroll,明 仿林良 喜上梅梢圖 軸|Birds Amidst Blossoming Plum and Bamboo,China,Ming dynasty (1368–1644),,,,Artist,After,Lin Liang,"Chinese, ca. 1416–1480",,Lin Liang,Chinese,1416,1480,,1416,1480,Hanging scroll; ink and color on silk,40 15/16 x 18 11/16 in. (104 x 47.4 cm),"Purchase, C. C. Wang Gift, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/64972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.19,false,true,36050,Asian Art,Hanging scroll,明 倣林良 蘆鴨圖 軸|Ducks and Reeds,China,Ming dynasty (1368–1644),,,,Artist,After,Lin Liang,"Chinese, ca. 1416–1480",,Lin Liang,Chinese,1416,1480,,1416,1644,Hanging scroll; ink and color on silk,Image: 61 5/8 × 34 5/8 in. (156.5 × 87.9 cm) Overall with mounting: 10 ft. 3/8 in. × 40 3/8 in. (305.8 × 102.6 cm) Overall with knobs: 10 ft. 3/8 in. × 44 1/2 in. (305.8 × 113 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.134.8,false,true,51285,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Qiu Ying,"Chinese, ca. 1495–1552",,Qiu Ying,Chinese,1485,1562,,1495,1552,Hanging scroll; ink and color on silk,Image: 33 in. × 30 3/4 in. (83.8 × 78.1 cm) Overall with mounting: 73 3/4 × 35 5/8 in. (187.3 × 90.5 cm) Overall with knobs: 73 3/4 × 39 in. (187.3 × 99.1 cm),"Rogers Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.15,false,true,36013,Asian Art,Hanging scroll,明 傳仇英 文玉圖 軸|Lady in a Bamboo Grove,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Qiu Ying,"Chinese, ca. 1495–1552",,Qiu Ying,Chinese,1485,1562,,1495,1552,Hanging scroll; ink and color on silk,Image: 49 in. × 17 1/2 in. (124.5 × 44.5 cm) Overall with mounting: 9 ft. 5 1/8 in. × 25 3/8 in. (287.3 × 64.5 cm) Overall with knobs: 9 ft. 5 1/8 in. × 29 1/2 in. (287.3 × 74.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.22,false,true,36051,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist,,Ma Shida,"Chinese, 15th century (?)",,Ma Shida,Chinese,1400,1500,,1400,1499,Hanging scroll; ink on paper,Overall: 43 3/8 x 18 3/8 in. (110.2 x 46.7 cm) Overall with mounting: 24 1/2 in. (62.2 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1.8,false,true,48942,Asian Art,Hanging scroll,明 劉世儒 雪梅圖 軸|Plum in Snow,China,Ming dynasty (1368–1644),,,,Artist,,Liu Shiru,"Chinese, active 1550–1600",,Liu Shiru,Chinese,1550,1600,,1550,1600,Hanging scroll; ink on silk,Image: 68 3/4 x 24 3/8 in. (174.6 x 61.9 cm) Overall with mounting: 106 1/4 x 30 7/8 in. (269.9 x 78.4 cm) Overall with knobs: 106 1/4 x 33 9/16 in. (269.9 x 85.2 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.115,false,true,36023,Asian Art,Hanging scroll,明 傳張路 溪畔漁家圖 軸|Fisherman and Family,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Zhang Lu,"Chinese, ca. 1490–ca. 1563",,Zhang Lu,Chinese,1490,1563,,1490,1563,Hanging scroll; ink and color on silk,43 3/8 x 31 7/8 in. (110.2 x 81.0 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.746.2,false,true,50167,Asian Art,Hanging scrolls,明 杜堇 陶淵明菊花圖 軸|Tao Yuanming Enjoying Chrysanthemums,China,Ming dynasty (1368–1644),,,,Artist,,Du Jin,"Chinese, active ca. 1465–1509",,Du Jin,Chinese,1465,1509,,1465,1509,Hanging scrolls; ink and color on paper,Image: 58 1/4 x 14 3/16 in. (148 x 36 cm) Overall with mounting: 105 1/4 x 21 3/16 in. (267.3 x 53.8 cm) Overall with knobs: 105 1/4 x 24 1/2 in. (267.3 x 62.2 cm),"The C. C. Wang Family Collection, Gift of C. C. Wang, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.85,false,true,35996,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Chong,"Chinese, active ca. 1570–1610",,Zhang Chong,Chinese,1570,1610,,1570,1610,Folding fan mounted as an album leaf; ink on gold paper,6 1/2 x 19 1/4 in. (16.5 x 48.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.11,false,true,36136,Asian Art,Hanging scroll,明 張祐 萬古春風圖 軸|Spring Breeze of Myriad Pasts,China,Ming dynasty (1368–1644),,,,Artist,,Zhang You,"Chinese, active mid 15th century",,Zhang You,Chinese,1400,1499,,1434,1466,Hanging scroll; ink on silk,Image: 61 3/4 x 27 1/2 in. (156.8 x 69.9 cm) Overall with mounting: 102 1/4 x 34 3/4 in. (259.7 x 88.3 cm) Overall with knobs: 102 1/4 x 38 in. (259.7 x 96.5 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.414,false,true,41471,Asian Art,Hanging scroll,明 呂紀 秋景花鳥圖 軸|Autumn Landscape with Herons and Ducks,China,Ming dynasty (1368–1644),,,,Artist,,Lü Ji,"Chinese, active late 15th century",,Lü Ji,Chinese,1430,1504,,1467,1499,Hanging scroll; ink and color on silk,Image: 58 1/8 x 21 1/2 in. (147.6 x 54.6 cm) Overall with mounting: 106 1/4 x 27 1/2 in. (269.9 x 69.9 cm) Overall with knobs: 106 1/4 x 30 in. (269.9 x 76.2 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.494.2,false,true,39551,Asian Art,Hanging scroll,明 呂紀 鴛鴦芙蓉圖 軸|Mandarin ducks and cotton rose hibiscus,China,Ming dynasty (1368–1644),,,,Artist,,Lü Ji,"Chinese, active late 15th century",,Lü Ji,Chinese,1430,1504,,1467,1499,Hanging scroll; ink and color on silk,Image: 68 x 39 in. (172.7 x 99.1 cm) Overall with mounting: 116 1/4 x 40 1/4 in. (295.3 x 102.2 cm) Overall with knobs: 116 1/4 x 44 1/2 in. (295.3 x 113 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.151,false,true,36030,Asian Art,Hanging scroll,明 沈碩 仿仇英文玉圖 軸|Lady in a Bamboo Grove after Qiu Ying,China,Ming dynasty (1368–1644),,,,Artist,,Shen Shuo,"Chinese, active 16th–17th century",,Shen Shuo,Chinese,1500,1600,,1544,1644,Hanging scroll; ink and color on silk,Image: 46 1/2 × 19 1/2 in. (118.1 × 49.5 cm) Overall with mounting: 9 ft. 4 in. × 27 5/8 in. (284.5 × 70.2 cm) Overall with knobs: 9 ft. 4 in. × 31 3/4 in. (284.5 × 80.6 cm),"Gift of John C. Ferguson, 1916",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.93,false,true,36002,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Wu Shantao,1609–1690,,Wu Shantao,Chinese,1609,1690,,1644,1690,Folding fan mounted as an album leaf; ink on gold paper,6 1/4 x 18 5/8 in. (15.9 x 47.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.89,false,true,36000,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Zhuang Jiongsheng,"Chinese, born 1626",,Zhuang Jiongsheng,Chinese,1626,1726,,1644,1676,Folding fan mounted as an album leaf; ink on gold paper,6 3/8 x 19 3/4 in. (16.2 x 50.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.82,false,true,36188,Asian Art,Folding fan mounted as an album leaf,近代 吳石僊 春江煙雨 扇面|Misty Rain on the River in Spring,China,Qing dynasty (1644–1911),,,,Artist,,Wu Shixian,"Chinese, died 1916",,Wu Shixian,Chinese,,1916,,1816,1916,Folding fan mounted as an album leaf; ink on gold-flecked paper,7 1/8 x 20 1/4 in. (18.1 x 51.4 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.242.7,false,true,49130,Asian Art,Hanging scroll,清 傳查士標 泛棹圖 軸|Old Man Boating on a River,China,Qing dynasty (1644–1911),,,,Artist,,Zha Shibiao,"Chinese, 1615–1698",,Zha Shibiao,Chinese,1615,1698,,1644,1698,Hanging scroll; ink on paper,69 x 26 7/8 in. (175.3 x 68.3 cm),"The Sackler Fund, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.142a–l,false,true,39931,Asian Art,Album,清 戴本孝 山水圖 冊 紙本|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Dai Benxiao,"Chinese, 1621–1693",,DAI BENXIAO,Chinese,1621,1693,,1621,1691,Album of twelve leaves; ink on paper,Each: 8 7/16 × 6 9/16 in. (21.4 × 16.7 cm),"Purchase, The Dillon Fund Gift, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.5,false,true,36134,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Mei Qing,"Chinese, 1623–1697",,MEI QING,Chinese,1623,1697,,1623,1697,Folding fan mounted as an album leaf; ink and color on gold paper,Image (each leaf): 7 1/4 x 20 1/16 in. (18.4 x 51 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.34a–l,false,true,51803,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist,Attributed to,Bada Shanren (Zhu Da),"Chinese, 1626–1705",,Bada Shanren (Zhu Da),Chinese,1626,1705,,1644,1705,Album of twelve paintings; ink wash on paper,Image: 7 3/4 in. × 6 in. (19.7 × 15.2 cm) Sheet: 11 in. × 7 1/4 in. (27.9 × 18.4 cm) double leaf: 11 × 14 5/8 in. (27.9 × 37.1 cm),"Seymour Fund, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.140,false,true,49148,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Wang Wu,"Chinese, 1632–1690",,Wang Wu,Chinese,1632,1690,,1632,1690,Folding fan mounted as an album leaf; ink and color on paper,6 3/4 x 20 1/8 in. (17.1 x 51.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.6,false,true,41485,Asian Art,Hanging scroll,清 吳歷 倣王蒙溪山行旅圖 軸|Travelers Among Streams and Mountains,China,Qing dynasty (1644–1911),,,,Artist,,Wu Li,"Chinese, 1632–1718",,WU LI,Chinese,1632,1718,,1644,1718,Hanging scroll; ink on paper,Image: 23 1/4 x 10 5/8 in. (59.1 x 27 cm) Overall with mounting: 73 1/2 x 18 in. (186.7 x 45.7 cm) Overall with knobs: 73 1/2 x 21 1/4 in. (186.7 x 54 cm),"Ex coll.: C. C. Wang Family, Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.16,false,true,49165,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,After,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,,1644,1690,Hanging scroll; ink and color on silk,Image: 69 5/8 x 35 in. (176.8 x 88.9 cm) Overall with mounting: 121 1/2 x 43 in. (308.6 x 109.2 cm) Overall with knobs: 121 1/2 x 46 1/2 in. (308.6 x 118.1 cm),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.35,false,true,36016,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,Attributed to,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,,1644,1690,Hanging scroll; ink on paper,38 7/8 x 13 1/8 in. (98.7 x 33.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.5,false,true,52225,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,,1644,1690,Hanging scroll; ink and color on silk,Image: 52 3/4 x 25 1/8 in. (134 x 63.8 cm) Overall with mounting: 88 3/8 x 31 1/4 in. (224.5 x 79.4 cm) Overall with rollers: 88 3/8 x 34 1/2 in. (224.5 x 87.6 cm),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/52225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.148,false,true,49164,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,,1644,1690,Folding fan mounted as an album leaf; color on paper,6 11/16 x 20 in. (17 x 50.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.191,false,true,36120,Asian Art,Hanging scroll,清 陳字 掃象圖 軸|Washing the White Elephant,China,Qing dynasty (1644–1911),,,,Artist,,Chen Zi,"Chinese, 1634–1711",,Chen Zi,Chinese,1634,1711,,1644,1711,Hanging scroll; ink on paper,Image: 34 5/16 x 15 1/16 in. (87.2 x 38.3 cm) Overall with mounting: 86 3/4 x 23 in. (220.3 x 58.4 cm) Overall with knobs: 86 3/4 x 27 1/4 in. (220.3 x 69.2 cm),"Purchase, The Dillon Fund Gift, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.426.4,false,true,49182,Asian Art,Hanging scroll,清 石濤(朱若極) 廬山草堂圖 軸|Hermitage in Mount Lu,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,,1644,1707,Hanging scroll; ink on paper,Image: 37 5/16 x 19 11/16 in. (94.8 x 50 cm) Overall with mounting: 76 1/4 x 26 in. (193.7 x 66 cm) Overall with knobs: 76 1/4 x 29 1/2 in. (193.7 x 74.9 cm),"Gift of Douglas Dillon, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.155a–h,false,true,49180,Asian Art,Album,清 石濤(朱若極) 四季山水圖 冊|Landscapes of the Four Seasons,China,Qing dynasty (1644–1911),,,,Artist,,Shitao (Zhu Ruoji),"Chinese, 1642–1707",,Shitao,Chinese,1642,1707,,1644,1707,Album of eight leaves; ink and color on paper,8 1/4 x 12 3/8 in. (21 x 31.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.159,false,true,49238,Asian Art,Hanging scroll,清 高鳳翰 菊石圖 軸|Chrysanthemums by a Rock,China,Qing dynasty (1644–1911),,,,Artist,,Gao Fenghan,"Chinese, 1683–1749",,Gao Fenghan,Chinese,1683,1749,,1683,1749,Hanging scroll; ink and color on paper,Image: 45 1/4 x 21 1/2 in. (114.9 x 54.6 cm) Overall with mounting: 76 x 27 1/2 in. (193 x 69.9 cm) Overall with knobs: 76 x 31 3/4 in. (193 x 80.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.8,false,true,36102,Asian Art,Hanging scroll,清 方士庶 山水圖 軸|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Fang Shishu,"Chinese, 1692–1751",,Fang Shishu,Chinese,1692,1751,,1692,1751,Hanging scroll; ink and color on paper,Image: 26 7/8 x 18 1/4 in. (68.3 x 46.4 cm) Overall with mounting: 95 7/8 x 24 7/8 in. (243.5 x 63.2 cm) Overall with knobs: 95 7/8 x 29 in. (243.5 x 73.7 cm),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.212.2,false,true,36096,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,After,Zheng Xie,"Chinese, 1693–1765",,Zheng Xie,Chinese,1693,1765,,1693,1911,Hanging scroll; ink on paper,67 1/8 x 17 15/16 in. (170.5 x 45.6 cm),"The C. C. Wang Family Collection, Gift of C. C. Wang, 1968",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.214.150,false,true,52946,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Zheng Xie,"Chinese, 1693–1765",,Zheng Xie,Chinese,1693,1765,,1693,1765,Hanging scroll; ink on paper,Image: 55 1/8 x 15 1/2 in. (140 x 39.4 cm) Overall with mounting: 86 x 21 in. (218.4 x 53.3 cm) Overall with knobs: 85 x 24 in. (215.9 x 61 cm),"Gift of Ernest Erickson Foundation, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/52946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.177.15,false,true,51790,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Qian Weicheng,"Chinese, 1720–1772",,Qian Weicheng,Chinese,1720,1772,,1720,1772,Hanging scroll; ink and color on paper,Image: 16 3/4 × 12 1/2 in. (42.5 × 31.8 cm) Overall with mounting: 77 1/4 × 18 3/4 in. (196.2 × 47.6 cm) Overall with knobs: 77 1/4 × 18 7/8 in. (196.2 × 47.9 cm),"Bequest of Katherine S. Dreier, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.153.1a–l,false,true,44527,Asian Art,Album,清 錢維城 景數四氣,冬景圖 冊|Winter Landscapes and Flowers,China,Qing dynasty (1644–1911),,,,Artist,,Qian Weicheng,"Chinese, 1720–1772",,Qian Weicheng,Chinese,1720,1772,,1720,1772,Album of twelve paintings; ink and color on paper,Image (each): 8 5/8 × 11 7/8 in. (21.9 × 30.2 cm),"Purchase, The Dillon Fund Gift, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.118,false,true,36026,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,After,Wang Wenzhi,"Chinese, 1730–1802",,Wang Wenzhi,Chinese,1730,1802,,1644,1911,Hanging scroll; ink and color on silk,17 1/4 x 19 1/2 in. (43.8 x 49.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.131.1–.9,false,true,42559,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist,,Prince Yongxing,"Chinese, 1752–1823",,Yongxing Prince,Chinese,1752,1823,,1752,1823,Album with nine paintings and nine leaves of calligraphy; ink and color on heavy paper,Each leaf: 4 7/8 x 3 7/8 in. (12.4 x 9.8 cm),"Anonymous Gift, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.4,false,true,36147,Asian Art,Hanging scroll,清 改琦 插花圖 軸|Girl Arranging Flowers,China,Qing dynasty (1644–1911),,,,Artist,,Gai Qi,"Chinese, 1773–1828",,Gai Qi,Chinese,1773,1828,,1774,1829,Hanging scroll; ink and color on silk,40 7/8 x 13 1/8 in. (103.8 x 33.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.39,false,true,49457,Asian Art,Fan mounted as an album leaf,清 張熊 蟾蜍 團扇|Flower and Toad,China,Qing dynasty (1644–1911),,,,Artist,,Zhang Xiong,"Chinese, 1803–1886",,Zhang Xiong,Chinese,1803,1886,,1803,1886,Circular fan-shaped album leaf; ink and color on silk,Diam. 10 1/2 in. (26.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.9,false,true,49433,Asian Art,Hanging scroll,清 蘇仁山 李凝陽像 軸|The Immortal Li Tieguai,China,Qing dynasty (1644–1911),,,,Artist,,Su Renshan,"Chinese, 1814–1849",,Su Renshan,Chinese,1814,1850,,1814,1849,Hanging scroll; ink on paper,45 1/2 x 15 3/4 in. (115.6 x 40 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.32,false,true,49455,Asian Art,Folding fan mounted as an album leaf,清 胡遠 山水 扇面|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Hu Yuan,"Chinese, 1823–1886",,Hu Yuan,Chinese,1823,1886,,1823,1886,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,7 1/8 x 20 1/4 in. (18.1 x 51.4 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.33,false,true,49456,Asian Art,Folding fan mounted as an album leaf,清 胡遠 芍藥 扇面|Herbaceous Peony,China,Qing dynasty (1644–1911),,,,Artist,,Hu Yuan,"Chinese, 1823–1886",,Hu Yuan,Chinese,1823,1886,,1823,1886,Folding fan mounted as an album leaf; ink and color on gold paper,7 1/8 x 20 3/4 in. (18.1 x 52.7 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.73a–h,false,true,36184,Asian Art,Album,清 居廉 花卉蟲草 冊頁 八開|Insects and Flowers,China,Qing dynasty (1644–1911),,,,Artist,,Ju Lian,"Chinese, 1828–1904",,Ju Lian,Chinese,1828,1904,,1828,1904,Album of eight leaves; ink and color on paper,12 3/8 x 14 1/4 in. (31.4 x 36.2 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.74,false,true,49472,Asian Art,Folding fan mounted as an album leaf,清 吳大澂 山水 扇面|Fragrant Mountains,China,Qing dynasty (1644–1911),,,,Artist,,Wu Dacheng,"Chinese, 1835–1902",,Wu Dacheng,Chinese,1835,1902,,1835,1902,Folding fan mounted as an album leaf; ink on alum paper,6 7/8 x 20 7/8 in. (17.5 x 53 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.56,false,true,36172,Asian Art,Fan mounted as an album leaf,清 舒浩 周敦頤愛蓮圖 團扇|Admiring Lotus,China,Qing dynasty (1644–1911),,,,Artist,,Shu Hao,"Chinese, 1850–1899",,Shu Hao,Chinese,1850,1899,,1867,1899,Circular fan-shaped album leaf; ink and color on silk,Diam. 10 1/4 in. (26 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.71,false,true,49471,Asian Art,Hanging scroll,清 任預 石室參禪圖 軸|Meditation in a Cave,China,Qing dynasty (1644–1911),,,,Artist,,Ren Yu,"Chinese, 1853–1901",,REN YU,Chinese,1853,1901,,1853,1901,Hanging scroll; ink and color on paper,44 7/8 x 15 3/8 in. (114.0 x 39.1 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.162,false,true,49242,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Li Shan,"Chinese, 1686–ca. 1756",,Li Shan,Chinese,1686,1756,,1686,1756,Hanging scroll; ink and color on paper,Image: 53 1/8 x 13 in. (134.9 x 33 cm) Overall with mounting: 75 1/2 x 18 7/8 in. (191.8 x 47.9 cm) Overall with knobs: 75 1/2 x 22 3/8 in. (191.8 x 56.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.42,false,true,49459,Asian Art,Folding fan mounted as an album leaf,清 居巢 鳥圖 扇面|Bird,China,Qing dynasty (1644–1911),,,,Artist,,Zhü Chao,"Chinese, ca. 1823–1889",,Zhü Chao,Chinese,1823,1889,,1823,1889,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,7 x 20 7/8 in. (17.8 x 53 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.117.1,false,true,41484,Asian Art,Hanging scroll,清 王鑑 倣巨然《溪山高士圖》 軸 紙本|Lofty Scholar among Streams and Mountains after Juran,China,Qing dynasty (1644–1911),,,,Artist,,Wang Jian,"Chinese, 1609–1677 or 1688",,Wang Jian,Chinese,1609,1677,,1644,1688,Hanging scroll; ink on paper,Image: 72 x 33 in. (182.9 x 83.8 cm) Overall with mounting: 107 1/2 x 38 1/4 in. (273.1 x 97.2 cm) Overall with knobs: 107 1/2 x 42 1/2 in. (273.1 x 108 cm),"Gift of Douglas Dillon, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.164,false,true,49236,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Bian Shoumin,"Chinese, active ca. 1729–50",,Bian Shoumin,Chinese,1729,1750,,1729,1750,Album leaf; ink on paper,8 5/8 x 18 11/16 in. (21.9 x 47.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.30,false,true,36014,Asian Art,Hanging scroll,清 山水圖 軸|Landscape,China,Qing dynasty (1644–1911),,,,Artist,Attributed to,Zhao Zuo,"Chinese, ca. 1570–after 1630",,Zhao Zuo,Chinese,1560,1630,,1630,1644,Hanging scroll; ink on silk,58 1/4 x 26 1/8 in. (148.0 x 66.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1.10,false,true,36139,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Gao Xiang,"Chinese, active ca. 1700–1730",,Gao Xiang,Chinese,1700,1730,,1700,1730,Hanging scroll; ink on paper,Image: 34 1/2 x 15 5/16 in. (87.6 x 38.9 cm) Overall with mounting: 80 7/8 x 20 3/4 in. (205.4 x 52.7 cm) Overall with knobs: 80 7/8 x 23 3/8 in. (205.4 x 59.4 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.57,false,true,35981,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Shen Hao,"Chinese, active late 17th century",,Shen Hao,Chinese,1671,1699,,1667,1699,Folding fan mounted as an album leaf; ink on gold paper,6 5/8 x 20 1/4 in. (16.8 x 51.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.38a–l,false,true,51484,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist,,Cao Jian,"Chinese, active early 18th century",,Cao Jian,Chinese,1700,1733,,1700,1733,Album of twelve leaves; ink and color on paper,Overall (b–l): 10 1/2 x 11 1/2 in. (26.7 x 29.2 cm) Overall (a): 10 3/8 x 11 5/8 in. (26.4 x 29.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.9,false,true,40080,Asian Art,Hanging scroll,元 王冕 墨梅圖 軸|Fragrant Snow at Broken Bridge,China,Yuan dynasty (1271–1368),,,,Artist,,Wang Mian,"Chinese, 1287–1359",,WANG MIAN,Chinese,1287,1359,,1287,1359,Hanging scroll; ink on silk,Image: 44 1/2 x 19 3/4 in. (113 x 50.2 cm) Overall with colophons: 68 x 19 3/4 in. (172.7 x 50.2 cm) Overall with mounting: 106 1/8 x 25 1/4 in. (269.6 x 64.1 cm) Overall with knobs: 106 1/8 x 29 1/2 in. (269.6 x 74.9 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.573,false,true,39549,Asian Art,Hanging scroll,"元 姚彥卿 (廷美) 雪山行旅圖 軸|요언경, 눈 덮인 산 속 나그네 중국 원|Traveling through Snow-Covered Mountains",China,Yuan dynasty (1271–1368),,,,Artist,,Yao Yanqing (Tingmei),"Chinese, ca. 1300–after 1360",,YAO YANQING (TINGMEI),Chinese,1300,1370,,1300,1368,Hanging scroll; ink on silk,Image: 38 5/8 x 21 1/4 in. (98.1 x 54 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.494.1,false,true,39545,Asian Art,Handscroll,元 趙蒼雲 劉晨阮肇入天台山圖 卷|Liu Chen and Ruan Zhao Entering the Tiantai Mountains,China,Yuan dynasty (1271–1368),,,,Artist,,Zhao Cangyun,"Chinese, active late 13th–early 14th century",,ZHAO CANGYUN,Chinese,1267,1333,,1271,1333,Handscroll; ink on paper,Image: 8 7/8 in. x 18 ft. 5 in. (22.5 cm x 564 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.47,false,true,35977,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644) (?),,,,Artist,,Lu Kezheng (Ming dynasty?),,,Lu Kezheng,Chinese,1350,1650,,1368,1644,Folding fan mounted as an album leaf; ink on gold paper,6 1/2 x 20 in. (16.5 x 50.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.6.13,false,true,51763,Asian Art,Album leaf,,China,Qing dynasty (?) (1644–1911),,,,Artist,Attributed to,Xü Daoguang,"Chinese, 13th century",,Xü Daoguang,Chinese,1200,1299,,1200,1299,Album leaf; black and grey wash on paper,7 x 11 5/8 in. (17.8 x 29.5 cm),"Bequest of Ellis Gray Seymour, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.11,false,true,40087,Asian Art,Fan mounted as an album leaf,南宋 傳夏珪 澤畔疾風圖 團扇|Windswept Lakeshore,China,Southern Song dynasty (1127–1279),,,,Artist,Attributed to,Xia Gui,"Chinese, active ca. 1195–1230",,Xia Gui,Chinese,1195,1230,,1195,1230,Fan mounted as an album leaf; ink on silk,10 1/4 x 10 5/8 in. (26 x 27 cm),"Ex coll.: C. C. Wang Family, Purchase, Theodore M. Davis Collection, Bequest of Theodore M. Davis, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.65,false,true,35986,Asian Art,Folding fan mounted as an album leaf,明 (傳)程嘉燧 金箋水墨公鷄圖扇頁|A Rooster near Trees,China,Ming (1368–1644)–Qing (1644–1911) dynasty,,,,Artist,Attributed to,Cheng Jiasui,"Chinese, 1565–1644",,Cheng Jiasui,Chinese,1565,1644,,1368,1911,Folding fan mounted as an album leaf; ink on gold paper,6 1/2 x 20 3/8 in. (16.5 x 51.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.7,false,true,51639,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1427,1509,Handscroll; ink on paper,Image: 11 1/2 in. × 14 ft. 10 3/8 in. (29.2 × 453.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.212.1,false,true,51856,Asian Art,Album leaf mounted as a hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,In the style of,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1644,1911,Album leaf mounted as a hanging scroll; ink and color on paper,60 5/8 x 23 3/4 in. (154.0 x 60.3 cm),"The C. C. Wang Family Collection, Gift of C. C. Wang, 1968",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.131.9,false,true,51858,Asian Art,Album leaf,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1427,1509,Album leaf; ink and color on paper,9 5/8 x 14 7/8 in. (24.4 x 37.8 cm),"Bequest of Walter Carlebach, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.131.10,false,true,53601,Asian Art,Album leaf,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1427,1509,Album leaf; ink and color on paper,9 9/16 x 14 3/4 in. (24.3 x 37.5 cm),"Bequest of Walter Carlebach, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.131.11,false,true,53602,Asian Art,Album leaf,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1427,1509,Album leaf; ink and color on paper,9 5/8 x 14 13/16 in. (24.4 x 37.6 cm),"Bequest of Walter Carlebach, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.131.12,false,true,53603,Asian Art,Album leaf,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Shen Zhou,"Chinese, 1427–1509",,Shen Zhou,Chinese,1427,1509,,1427,1509,Album leaf; ink and color on paper,9 5/8 x 14 13/16 in. (24.4 x 37.6 cm),"Bequest of Walter Carlebach, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.146,false,true,42187,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Chen Chun,"Chinese, 1483–1544",,Chen Chun,Chinese,1483,1544,,1483,1544,Handscroll; ink and color on paper,12 7/8 in. × 24 ft. 7 1/2 in. (32.7 × 750.6 cm),"Rogers Fund, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.102,false,true,51850,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Dong Qichang,"Chinese, 1555–1636",,Dong Qichang,Chinese,1555,1636,,1555,1636,Hanging scroll; ink on paper,Image: 45 x 19 1/2 in. (114.3 x 49.5 cm),"Gift of Mary Griggs Burke, 1962",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.8,false,true,51640,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Zha Shibiao,"Chinese, 1615–1698",,Zha Shibiao,Chinese,1615,1698,,1615,1698,Handscroll; ink and color on paper,Image: 11 3/8 in. × 10 ft. 10 1/4 in. (28.9 × 330.8 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.157,false,true,51768,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist,Attributed to,Wang Yuan,"Chinese, ca. 1280–after 1349",,Wang Yuan,Chinese,1270,1359,,1280,1380,Handscroll; ink and color on silk,97 1/2 x 12 5/8 in. (247.7 x 32.1 cm),"Gift of A. W. Bahr, in memory of Dr. Arnold Genthe, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.142.1,false,true,51546,Asian Art,Hanging scroll,,China,Song (960–1279) or Qing (1644–1911) dynasty (?),,,,Artist,In the style of,Chen Rong,active 1235–62,,Chen Rong,Chinese,1235,1262,,960,1911,"Hanging scroll, framed; ink on silk",63 x 38 1/2 in. (160 x 97.8 cm),"Fletcher Fund, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.126,false,true,49055,Asian Art,Folding fan mounted as an album leaf,,China,late Ming (1368–1644)– early Qing (1644–1911) dynasty,,,,Artist,,Chen Hongshou,"Chinese, 1599–1652",,Chen Hongshou,Chinese,1599,1652,,1599,1652,Folding fan mounted as an album leaf; ink and mineral color on gold paper,8 x 22 in. (20.3 x 55.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.200.5,false,true,51604,Asian Art,Album leaf,,China,Ming dynasty (?) (1368–1644),,,,Artist,,Wen Liang,"Chinese, active 15th century",,Wen Liang,Chinese,1400,1499,15th century,1400,1499,Miniature from album of eleven paintings; ink and color on silk,Image: 5 x 6 3/4 in. (12.7 x 17.1 cm),"Rogers Fund, 1942",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.48,false,true,35978,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist,,Wen Zhongyi,"Chinese, active mid–16th century",,Wen Zhongyi,Chinese,1500,1599,16th century,1534,1566,Folding fan mounted as an album leaf; ink on paper,6 3/5 x 19 3/8 in. (16.8 x 49.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.165,false,true,36035,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Wen Tong,"Chinese, 1019–1079",,Wen Tong,Chinese,1019,1079,16th century,1500,1599,Handscroll; ink and color on silk,Image: 22 x 101 in. (55.9 x 256.5 cm),"Rogers Fund, 1919",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.167,false,true,51584,Asian Art,Handscroll,明 佚名 蘇蕙璇璣圖 卷|Lady Su Hui and Her Verse Puzzle,China,Ming dynasty (1368–1644),,,,Artist,In the style of,Qiu Ying,"Chinese, ca. 1495–1552",,Qiu Ying,Chinese,1485,1562,16th century,1500,1599,Handscroll; ink and color on silk,Image: 10 1/16 in. x 10 ft. 2 3/4 in. (25.6 x 311.8 cm),"Gift of George D. Pratt, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.235.3,false,true,44592,Asian Art,Handscroll,明 傳仇英 十六羅漢圖 卷|The Sixteen Luohans,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Qiu Ying,"Chinese, ca. 1495–1552",,Qiu Ying,Chinese,1485,1562,16th century,1500,1599,Handscroll; ink on paper,Image: 13 9/16 x 206 3/8 in. (34.4 x 524.2 cm) Overall with mounting: 13 7/8 x 468 3/4 in. (35.2 x 1190.6 cm),"Gift of Douglas Dillon, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.235.4,false,true,44593,Asian Art,Handscroll,明 傳仇英 五星二十八宿神形圖 卷|Divinities of the Planets and Constellations,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Qiu Ying,"Chinese, ca. 1495–1552",,Qiu Ying,Chinese,1485,1562,16th century,1500,1599,Handscroll; ink and color on paper,Image: 7 9/16 x 158 1/4 in. (19.2 x 402 cm) Overall with mounting: 9 3/4 x 376 15/16 in. (24.8 x 957.4 cm),"Gift of Douglas Dillon, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.117,false,true,36084,Asian Art,Hanging scroll,明 劉世儒 月下雪梅圖 軸|Flowering Plum in Moonlight and Snow,China,Ming dynasty (1368–1644),,,,Artist,,Liu Shiru,"Chinese, active 1550–1600",,Liu Shiru,Chinese,1550,1600,16th century,1550,1599,Hanging scroll; ink on silk,Image: 60 x 35 5/16 in. (152.4 x 89.7 cm),"Gift of Alan Priest, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.6,false,true,44612,Asian Art,Hanging scroll,明 張路 觀畫圖 軸|Studying a Painting,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Lu,"Chinese, ca. 1490–ca. 1563",,Zhang Lu,Chinese,1490,1563,16th century,1500,1563,Hanging scroll; ink and color on silk,Image: 58 5/8 x 38 7/8 in. (148.9 x 98.7 cm) Overall with mounting: 103 x 42 1/8 in. (261.6 x 107 cm) Overall with knobs: 103 x 46 1/4 in. (261.6 x 117.5 cm),"Ex coll.: C. C. Wang Family, Purchase, Bequest of Dorothy Graham Bennett, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.115,false,true,51816,Asian Art,Album,,China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist,,Zhang Ruitu,"Chinese, 1570–1641",,Zhang Ruitu,Chinese,1570,1641,17th century,1600,1641,Album of sixteen calligraphies; ink on tan-coated paper,Overall (leaves): 11 3/8 x 7 1/2 in. (28.9 x 19.1 cm) Overall (wood covers): 14 1/2 x 9 1/4 in. (36.8 x 23.5 cm),"Gift of George G. Cobean, 1956",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.235.2a–u,false,true,44591,Asian Art,Album,清 王鑑 仿古山水圖 冊 紙本|Landscapes in the styles of ancient masters,China,Qing dynasty (1644–1911),,,,Artist,,Wang Jian,"Chinese, 1609–1677 or 1688",,Wang Jian,Chinese,1609,1677,17th century,1600,1699,Album of eighteen leaves; ink and color on paper,11 3/4 x 12 3/8 in. (29.8 x 31.4 cm),"Edward Elliott Family Collection, Gift of Douglas Dillon Gift, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.412.1,false,true,49167,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Yun Bing,"Chinese, active late 17th– early 18th century",,Yun Bing,Chinese,1650,1750,17th century,1644,1699,Album leaf; ink and color on paper,12 1/4 x 13 1/2 in. (31.1 x 34.3 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.412.2,false,true,49168,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist,,Yun Bing,"Chinese, active late 17th– early 18th century",,Yun Bing,Chinese,1650,1750,17th century,1667,1699,Album leaf; ink and color on paper,12 1/4 x 13 1/2 in. (31.1 x 34.3 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.23a, b",false,true,36124,Asian Art,Hanging scrolls,,China,Qing dynasty (1644–1911),,,,Artist,,Liang Tongshu,"Chinese, 1723–1815",,Liang Tongshu,Chinese,1723,1815,18th century,1723,1799,Pair of hanging scrolls; ink on paper,Image (Each): 51 1/4 x 10 1/8 in. (130.2 x 25.7 cm) Overall with mounting (Each): 65 1/4 x 13 in. (165.7 x 33 cm),"Gift of Chan-hua Mao, 1978",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/36124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.99.1a–d,false,true,40017,Asian Art,Inkstone,清 十八世紀顧二娘款鳳紋硯|Inkstone with phoenix design,China,Qing dynasty (1644–1911),,,,Artist,Attributed to,Gu Erniang,"Chinese, active early 18th century",,Gu Erniang,Chinese,1700,1733,18th century,1700,1799,Limestone,L. 5 1/16 in. (12.9 cm); W. 3 3/4 in. (9.5 cm),"Gift of Lily and Baird Hastings, 1989",,,,,,,,,,,,Inkstone,,http://www.metmuseum.org/art/collection/search/40017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.472,false,true,42324,Asian Art,Vase,,China,Qing dynasty (1644–1911),,,,Artist,,Chen Jinhou,"Chinese, active 18th century",,Chen Jinhou,Chinese,1700,1799,18th century,1700,1799,Stoneware with relief decoration (Yixing ware),H. 10 1/8 in. (25.7 cm),"Gift of Michael Abraham, 1984",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/42324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.149,false,true,49166,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist,After,Yun Shouping,"Chinese, 1633–1690",,YUN SHOUPING,Chinese,1633,1690,18th century,1700,1799,Handscroll; ink and color on silk,16 1/2 x 255 1/2 in. (41.9 x 649 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.421,false,true,44294,Asian Art,Table screen,"大理石插屏|Table Screen, now converted to a wall panel",China,Qing dynasty (1644–1911),,,,Artist,,Ruan Yuan,"Chinese, 1764–1849",,Ruan Yuan,Chinese,1764,1849,19th century,1800,1899,Marble mounted in wooden frame,11 3/8 x 15 3/4 in. (28.9 x 40 cm),"Purchase, Judith G. and F Randall Smith Gift, 1995",,,,,,,,,,,,Furniture,,http://www.metmuseum.org/art/collection/search/44294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.7a–h,false,true,49432,Asian Art,Album,清 潘思牧 山水 冊頁八開|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Pan Simu,"Chinese, 1756–1842",,Pan Simu,Chinese,1756,1842,19th century,1800,1842,Album of eight leaves; ink and color on paper,10 7/8 x 13 3/4 in. (27.6 x 34.9 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.20,false,true,49448,Asian Art,Folding fan mounted as an album leaf,清 吳熙載 杏花 扇面|Apricot,China,Qing dynasty (1644–1911),,,,Artist,,Wu Xizai,"Chinese, 1799–1870",,Wu Xizai,Chinese,1799,1870,19th century,1800,1870,Folding fan mounted as an album leaf; ink and color on alum paper,7 3/8 x 21 1/2 in. (18.7 x 54.6 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.15,false,true,36154,Asian Art,Folding fan mounted as an album leaf,清 汪廷儒 山水 扇頁|Landscape,China,Qing dynasty (1644–1911),,,,Artist,,Wang Tingru,"Chinese, 1804–1852",,Wang Tingru,Chinese,1804,1852,19th century,1804,1852,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,6 1/8 x 20 5/8 in. (15.6 x 52.4 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.18,false,true,49447,Asian Art,Folding fan mounted as an album leaf,清 任熊 牡丹 扇面|Peony,China,Qing dynasty (1644–1911),,,,Artist,,Ren Xiong,"Chinese, 1823–1857",,Ren Xiong,Chinese,1823,1857,19th century,1823,1857,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,6 15/16 x 21 in. (17.6 x 53.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.52,false,true,49467,Asian Art,Hanging scroll,清 虛谷 蝶貓圖 軸|Cat and Butterfly,China,Qing dynasty (1644–1911),,,,Artist,,Xu Gu,"Chinese, 1823–1896",,Xu Gu,Chinese,1823,1896,19th century,1823,1896,Hanging scroll; ink and color on paper,52 1/2 x 25 3/4 in. (133.4 x 65.4 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.47a–h,false,true,36170,Asian Art,Album,"清 任頤 動物花鳥 冊頁八開|Animals, Flowers and Birds",China,Qing dynasty (1644–1911),,,,Artist,,Ren Yi (Ren Bonian),"Chinese, 1840–1896",,Ren Yi (Ren Bonian),Chinese,1840,1896,19th century,1840,1896,Album of eight leaves; ink and color on paper,10 3/16 x 12 3/4 in. (25.9 x 32.4 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.129.3,false,true,36075,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Zhu Ling,"Chinese, active ca. 1820–1850",,Zhu Ling,Chinese,1820,1850,19th century,1820,1850,Hanging scroll; ink and color on paper,36 7/8 x 14 1/2 in. (93.7 x 36.8 cm),"Rogers Fund, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.43a–g,false,true,36168,Asian Art,Album,清 杜湘 山水 冊頁七開|Landscapes,China,Qing dynasty (1644–1911),,,,Artist,,Du Xiang,"Chinese, active late 19th century",,Du Xiang,Chinese,1850,1899,19th century,1867,1899,Album of seven leaves; ink and color on paper,10 1/8 x 6 3/8 in. (25.7 x 16.2 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.201,false,true,51898,Asian Art,Hanging scroll,,China,,,,,Artist|Artist,After,Zheng Xie|Unidentified Artist,"Chinese, 1693–1765",,Zheng Xie|Unidentified Artist,Chinese,1693,1765,19th–20th century,1800,1999,Hanging scroll; ink on paper,Image: 53 7/16 x 26 in. (135.7 x 66 cm) Overall: 53 3/8 x 26in. (135.6 x 66cm) Overall with knobs: 84 1/4 x 34 5/8 in. (214 x 87.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.7,false,true,40059,Asian Art,Fan mounted as an album leaf,南宋 傳高宗/孝宗 行楷書輕舠依岸七絕詩 團扇|Quatrain on fishermen,China,Song dynasty (960–1279),,,,Artist|Artist,Attributed to,Emperor Gaozong|Emperor Xiaozong,"Chinese, 1107–1187, r. 1127–1162|Chinese, 1127–1194; r. 1163–89",or,Gaozong Emperor|Xiaozong Emperor,Chinese|Chinese,1107 |1127,1187 |1194,12th century,1107,1187,Fan mounted as album leaf; ink on silk,9 1/4 x 10 in. (23.5 x 25.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.91,false,true,48912,Asian Art,Hanging scroll,明 傳徐渭 校靜菴文有感詩 軸|Poem Composed after Editing Jingan's Literary Works,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Xu Wei|Unidentified Artist,"Chinese, 1521–1593",,Xu Wei|Unidentified Artist,Chinese,1521,1593,dated 1576,1576,1576,Hanging scroll; ink on silk,Image: 54 3/8 x 19 in. (138.1 x 48.3 cm) Overall: 87 1/4 x 26 1/4 in. (221.6 x 66.7 cm) Overall with knobs: 87 1/4 x 28 1/2 in. (221.6 x 72.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.58,false,true,35982,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Wang Zhideng|Unidentified Artist,"Chinese, 1535–1612",,Wang Zhideng|Unidentified Artist,Chinese,1535,1612,spurious date of 1569,1569,1644,Folding fan mounted as an album leaf; ink on paper,6 5/8 x 19 15/32 in. (16.8 x 49.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.64,false,true,35985,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wu Kuan|Unidentified Artist,"Chinese, 1435–1504",,Wu Kuan|Unidentified Artist,Chinese,1435,1504,"18th century or later, spurious date of 1492",1700,1911,Folding fan mounted as an album leaf; ink on gold paper,6 7/8 x 19 15/32 in. (17.5 x 49.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.66,false,true,45784,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wen Zhengming|Unidentified Artist,"Chinese, 1470–1559",,Wen Zhengming|Unidentified Artist,Chinese,1470,1559,17th century or later,1644,1911,Folding fan mounted as an album leaf; ink on gold-flecked paper,7 1/8 x 20 7/8 in. (18.1 x 53 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.54,false,true,35980,Asian Art,Folding fan mounted as an album leaf,明/清 傳王鐸 行草書 扇頁|Calligraphy,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Duo|Unidentified Artist,"Chinese, 1592–1652",,Wang Duo|Unidentified Artist,Chinese,1592,1652,"17th century or later, spurious date of 1649",1649,1911,Folding fan mounted as an album leaf; ink on paper,6 3/8 x 20 1/4 in. (16.2 x 51.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.46,false,true,35976,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Zha Shibiao|Unidentified Artist,"Chinese, 1615–1698",,Zha Shibiao|Unidentified Artist,Chinese,1615,1698,18th century or later,1700,1911,Folding fan mounted as an album leaf; ink on paper,6 1/4 x 20 7/32 in. (15.9 x 51.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.124,false,true,51859,Asian Art,Hanging scroll,,China,,,,,Artist|Artist,After,Hongren|Unidentified Artist,"Chinese, 1610–1664",,HONGREN|Unidentified Artist,Chinese,1610,1664,20th century,1900,1999,Hanging scroll; ink and color on paper,Image: 55 x 30 3/8 in. (139.7 x 77.2 cm) Overall with mounting: 111 x 36 7/8 in. (281.9 x 93.7 cm) Overall with rollers: 111 x 41 in. (281.9 x 104.1 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.81.1,false,true,36066,Asian Art,Album,,China,,,,,Artist|Artist,Formerly Attributed to,Yao Wenhan|Unidentified Artist,"Chinese, active ca. 1760–1790","Chinese, early 20th century (?)",Yao Wenhan|Unidentified Artist,Chinese,1760,1790,early 20th century (?),1900,1933,Album of twenty-four leaves; ink and color on silk,11 1/8 x 8 3/4 in. (28.3 x 22.2 cm),"Gift of Mrs. Edward S. Harkness, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.81.2,false,true,49252,Asian Art,Album,,China,,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Yao Wenhan,"Chinese, active ca. 1760–1790","Chinese, early 20th century (?)",Unidentified Artist|Yao Wenhan,Chinese,1760,1790,early 20th century (?),1900,1933,Album of twenty leaves; ink and color on silk,11 1/8 x 8 3/4 in. (28.3 x 22.2 cm),"Gift of Mrs. Edward S. Harkness, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.34,false,true,51657,Asian Art,Album leaf,,China,20th century,,,,Artist|Artist,In the style of,Unidentified Artist|Cui Bai,"Chinese, active ca. 1040–70",,Unidentified Artist|Cui Bai,Chinese,1030,1080,20th century,1900,1947,Album leaf; ink and color on silk,12 3/8 x 10 1/8 in. (31.4 x 25.7 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.121,false,true,40003,Asian Art,Hanging scroll,宋 佚名 倣李成 寒林策驢圖 軸|Travelers in a Wintry Forest,China,Song dynasty (960–1279),,,,Artist|Artist,Traditionally attributed to,Li Cheng|Unidentified Artist,"Chinese, 919–967","Chinese, active early 12th century",LI CHENG|Unidentified Artist,Chinese,0919,0967,early 12th century,1100,1133,Hanging scroll; ink and color on silk,Image: 63 3/4 × 39 1/2 in. (161.9 × 100.3 cm) Overall with mounting: 10 ft. 3 1/2 in. × 49 in. (313.7 × 124.5 cm) Overall with knobs: 10 ft. 3 1/2 in. × 53 3/8 in. (313.7 × 135.6 cm),"Purchase, Fletcher Fund and Bequest of Dorothy Graham Bennett, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.12,false,true,40106,Asian Art,Handscroll,南宋 佚名 倣燕文貴 秋山蕭寺圖 卷|Buddhist Temples amid Autumn Mountains,China,Song dynasty (960–1279),,,,Artist|Artist,After,Yan Wengui|Unidentified Artist,"Chinese, 970–1030",,YAN WENGUI|Unidentified Artist,Chinese,0970,1030,late 12th–mid-13th century,1167,1266,Handscroll; ink and pale color on silk,Image: 12 7/8 in. × 10 ft. 6 1/2 in. (32.7 × 321.3 cm) Overall with mounting: 13 1/8 in. × 37 ft. 2 1/8 in. (33.3 × 1133.2 cm),"Purchase, The Dillon Fund Gift, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.124.4,false,true,36033,Asian Art,Handscroll,南宋 佚名 百牛圖 卷|One Hundred Buffaloes,China,Song dynasty (960–1279),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Jiangcan,"Chinese, active ca. 1200","Chinese, 13th century",Unidentified Artist|Jiangcan,Chinese,1190,1210,13th century,1200,1299,Handscroll; ink on paper,12 1/2 x 87 5/16 in. (31.8 x 221.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.4,false,true,40004,Asian Art,Handscroll,宋 佚名 倣周文矩 宮中圖 卷|In the Palace,China,Song dynasty (960–1279),,,,Artist|Artist,After,Zhou Wenju|Unidentified Artist,"Chinese, active 940–975","Chinese, active early 12th century",ZHOU WENJU|Unidentified Artist,Chinese,0940,0975,before 1140,960,1139,Handscroll; ink and touches of color on silk,Image: 10 1/4 × 57 3/4 in. (26 × 146.7 cm) Overall with mounting: 10 11/16 in. × 29 ft. 11 7/8 in. (27.1 × 914.1 cm),"Purchase, Douglas Dillon Gift, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.151,false,true,40002,Asian Art,Hanging scroll,北宋 佚名 倣范寬山水圖 軸|Landscape in the style of Fan Kuan,China,Song dynasty (960–1279),,,,Artist|Artist,after,Fan Kuan|Unidentified Artist,"Chinese, active ca. 990–1030","Chinese, active 12th century",Fan Kuan|Unidentified Artist,Chinese,0990,1030,early 12th century,1100,1133,Hanging scroll; ink and color on silk,Image: 65 3/8 × 41 1/8 in. (166.1 × 104.5 cm) Overall with mounting: 9 ft. 8 1/2 in. × 49 3/4 in. (295.9 × 126.4 cm) Overall with knobs: 9 ft. 8 1/2 in. × 53 3/4 in. (295.9 × 136.5 cm),"Gift of Irene and Earl Morse, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.16,false,true,40196,Asian Art,Fan mounted as an album leaf,,China,Song dynasty (960–1279),,,,Artist|Artist,Formerly Attributed to,Li Tang|Unidentified Artist,"Chinese, ca. 1070s–ca. 1150s",,LI TANG|Unidentified Artist,Chinese,1070,1159,13th century,1200,1299,Fan mounted as an album leaf; ink and color on silk,9 7/8 x 10 1/8 in. (25.1 x 25.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.15,false,true,40078,Asian Art,Fan mounted as an album leaf,,China,Song dynasty (960–1279),,,,Artist|Artist,Formerly Attributed to,Lidi|Unidentified Artist,"Chinese, ca. 1110– after 1197",,LIDI|Unidentified Artist,Chinese,1110,1197,13th century,1200,1279,Fan mounted as an album leaf; ink and color on silk,9 3/8 x 9 1/2 in. (23.8 x 24.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +40.148,false,true,39935,Asian Art,Handscroll,宋 佚名 倣周昉 戲嬰圖 卷|Palace Ladies Bathing Children,China,Song dynasty (960–1279),,,,Artist|Artist,After,Zhou Fang|Unidentified Artist,"Chinese, active ca. 780– ca.810",,ZHOU FANG|Unidentified Artist,Chinese,0780,0810,11th century,1000,1099,Handscroll; ink and color on silk,Image: 12 in. × 19 1/8 in. (30.5 × 48.6 cm) Overall with mounting: 12 7/16 × 44 in. (31.6 × 111.8 cm),"Fletcher Fund, 1940",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.64,false,true,45781,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Wen Zhengming|Unidentified Artist,"Chinese, 1470–1559",,Wen Zhengming|Unidentified Artist,Chinese,1470,1559,dated 1549,1549,1549,Handscroll; color on paper,Image: 11 x 52 3/8 in. (27.9 x 133 cm) Overall with mounting: 11 5/16 x 240 3/4 in. (28.7 x 611.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.89,false,true,48933,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Mo Shilong|Unidentified Artist,"Chinese, 1537–1587",,Mo Shilong|Unidentified Artist,Chinese,1537,1587,dated 1577,1577,1577,Handscroll; ink and color on paper,Image: 8 5/16 x 31 7/8 in. (21.1 x 81 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.63,false,true,49020,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Lan Ying|Unidentified Artist,"Chinese, 1585–1664",,Lan Ying|Unidentified Artist,Chinese,1585,1664,"17th century or later, spurious date of 1633",1633,1911,Folding fan mounted as an album leaf; ink and color on gold paper,6 5/8 x 20 1/2 in. (16.8 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.123,false,true,51873,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Ni Yuanlu|Unidentified Artist,"Chinese, 1593–1644",,Ni Yuanlu|Unidentified Artist,Chinese,1593,1644,dated 1638,1638,1638,Hanging scroll; ink and color on paper,Image: 67 x 28 3/4 in. (170.2 x 73 cm) Overall with mounting: 100 1/2 x 34 5/8 in. (255.3 x 87.9 cm) Overall with knobs: 100 1/2 x 38 1/2 in. (255.3 x 97.8 cm),"Bequest of John M. Crawford, Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.74,false,true,48888,Asian Art,Hanging scroll,明 文伯仁 溪山僊館圖 軸|Dwellings of the Immortals Amid Streams and Mountains,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Wen Boren|Unidentified Artist,"Chinese, 1502–ca.1575",,Wen Boren|Unidentified Artist,Chinese,1502,1575,dated 1531,1531,1531,Hanging scroll; ink and color on paper,Image: 71 x 24 7/8 in. (180.3 x 63.2 cm) Overall with mounting: 105 7/8 x 31 3/16 in. (268.9 x 79.2 cm) Overall with knobs: 105 7/8 x 35 3/4 in. (268.9 x 90.8 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.97,false,true,44451,Asian Art,Fan mounted as an album leaf,"元/明 佚名 舊傳趙雍 倣盛懋 蘇軾後赤壁賦圖 扇|Illustration of Su Shi's ""Second Ode on the Red Cliff""",China,Ming dynasty (1368–1644),,,,Artist|Artist,In the style of,Sheng Mou|Unidentified Artist,"Chinese, active ca. 1310–1360",,Sheng Mou|Unidentified Artist,Chinese,1310,1360,late 14th–early 15th century,1367,1433,Fan mounted as an album leaf; ink and color on silk,Image: 12 3/8 x 12 3/8 in. (31.4 x 31.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.54,false,true,39556,Asian Art,Handscroll,"明 沈周 , 文徵明 合璧山水圖 卷|Joint Landscape",China,Ming dynasty (1368–1644),,,,Artist|Artist,,Shen Zhou|Wen Zhengming,"Chinese, 1427–1509|Chinese, 1470–1559",,Shen Zhou|Wen Zhengming,Chinese|Chinese,1427 |1470,1509 |1559,ca. 1509 and 1546,1509,1546,Handscroll; ink on paper,"Image: 14 1/2 x 56 ft., 8 5/6 in. (36.8 x 1729.3 cm)","Purchase, The Dillon Fund Gift, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.67,false,true,48884,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wen Jia|Unidentified Artist,"Chinese, 1501–1583",,Wen Jia|Unidentified Artist,Chinese,1501,1583,17th century or later,1600,1911,Folding fan mounted as an album leaf; ink on gold paper,6 7/8 x 19 1/2 in. (17.5 x 49.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.43,false,true,35974,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Shimin|Unidentified Artist,"Chinese, 1592–1680",,WANG SHIMIN|Unidentified Artist,Chinese,1592,1680,"18th century or later, spurious date of 1648",1700,1911,Folding fan mounted as an album leaf; ink on gold paper,6 1/2 x 20 1/2 in. (16.5 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.2.1a–q,false,true,42560,Asian Art,Album,清 倣黃向堅 尋親紀行圖 冊|A Journey in Search of the Artist's Parents,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Huang Xiangjian|Unidentified Artist,"Chinese, 1609–1673",,Huang Xiangjian|Unidentified Artist,Chinese,1609,1673,Dated 1656,1656,1656,Album of fourteen leaves; ink on paper,15 1/2 x 11 3/8 in. (39.4 x 28.9 cm),"The Sackler Fund, 1970",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.1.2,false,true,49127,Asian Art,Hanging scroll,清 倣弘仁 黃山蟠龍松圖 軸|Dragon Pine on Mount Huang,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Hongren|Unidentified Artist,"Chinese, 1610–1664",,HONGREN|Unidentified Artist,Chinese,1610,1664,ca. 1660,1650,1670,Hanging scroll; ink and pale color on paper,Image: 76 1/4 x 31 in. (193.7 x 78.7 cm) Overall with mounting: 120 1/4 x 38 1/2 in. (305.4 x 97.8 cm) Overall with knobs: 120 1/4 x 41 3/4 in. (305.4 x 106 cm),"Gift of Douglas Dillon, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.185,false,true,51887,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Hongren|Unidentified Artist,"Chinese, 1610–1664",,HONGREN|Unidentified Artist,Chinese,1610,1664,dated 1661,1661,1661,Handscroll; ink on paper,11 1/4 x 152 1/4 in. (28.6 x 386.7cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.25,false,true,35971,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Hui|Unidentified Artist,"Chinese, 1632–1717",,Wang Hui|Unidentified Artist,Chinese,1632,1717,"18th century or later, spurious date of 1680",1700,1911,Hanging scroll; ink on silk,14 1/2 x 11 1/8 in. (36.8 x 28.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.40,false,true,35972,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Hui|Unidentified Artist,"Chinese, 1632–1717",,Wang Hui|Unidentified Artist,Chinese,1632,1717,"18th century or later, spurious date of 1706",1700,1911,Folding fan mounted as an album leaf; ink and color on paper,6 3/4 x 20 in. (17.1 x 50.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.45,false,true,35975,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Hui|Unidentified Artist,"Chinese, 1632–1717",,Wang Hui|Unidentified Artist,Chinese,1632,1717,"18th century or later, spurious date of 1707",1707,1911,Folding fan mounted as an album leaf; ink and color on paper,6 1/2 x 19 1/4 in. (16.5 x 48.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.142,false,true,49152,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Hui|Unidentified Artist,"Chinese, 1632–1717",,Wang Hui|Unidentified Artist,Chinese,1632,1717,dated 1682,1682,1682,Hanging scroll; ink and color on paper,Image: 52 7/8 x 21 1/8 in. (134.3 x 53.7 cm) Overall with mounting: 86 1/2 x 25 1/2 in. (219.7 x 64.8 cm) Overall with knobs: 86 1/2 x 29 3/8 in. (219.7 x 74.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.143,false,true,49153,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Hui|Unidentified Artist,"Chinese, 1632–1717",,Wang Hui|Unidentified Artist,Chinese,1632,1717,dated 1686,1686,1686,Hanging scroll; ink on paper,Image: 22 1/2 x 16 in. (57.2 x 40.6 cm) Overall with mounting: 72 x 21 5/8 in. (182.9 x 54.9 cm) Overall with knobs: 72 x 25 1/4 in. (182.9 x 64.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.138,false,true,53552,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,In the style of,Unidentified Artist|Yun Shouping,"Chinese, 1633–1690",,Unidentified Artist|YUN SHOUPING,Chinese,1633,1690,probably 18th–19th century,1700,1899,Handscroll; ink and color on silk,11 15/16 x 102 3/8 in. (30.3 x 260 cm),"Anonymous Gift, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.41,false,true,51369,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Yun Shouping|Unidentified Artist,"Chinese, 1633–1690",,YUN SHOUPING|Unidentified Artist,Chinese,1633,1690,18th century or later,1700,1911,Folding fan mounted as an album leaf; ink on paper,6 7/8 x 20 1/2 in. (17.5 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.44,false,true,49188,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Yuanqi|Unidentified Artist,"Chinese, 1642–1715",,WANG YUANQI|Unidentified Artist,Chinese,1642,1715,"18th century or later, spurious date of 1715",1715,1911,Folding fan mounted as an album leaf; ink on paper,6 1/2 x 19 1/2 in. (16.5 x 49.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.24,false,true,39913,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Zhao Boju,"Chinese, 11th century",Chinese|in the style of Li Longmian,Unidentified Artist|Zhao Boju,Chinese,1000,1099,18th century or later,1700,1800,Handscroll; ink and color on golden paper,10 3/4 x 41 3/4 in. (27.3 x 106 cm),"Rogers Fund, 1927",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.198a–h,false,true,51896,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Unidentified Artist|Li Shan,"Chinese, 1686–ca. 1756",,Unidentified Artist|Li Shan,Chinese,1686,1756,dated 1740,1740,1740,Album of eight double leaves; ink and color on paper,Image: 10 5/8 × 13 3/16 in. (27 × 33.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.42,false,true,35973,Asian Art,Folding fan mounted as an album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Jian|Unidentified Artist,"Chinese, 1609–1677 or 1688",,Wang Jian|Unidentified Artist,Chinese,1609,1677,"18th century or later, spurious date of 1676",1700,1911,Folding fan mounted as an album leaf; ink and color on paper,6 3/8 x 20 1/2 in. (16.2 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.141.4a–rr,false,true,41481,Asian Art,Album,清 王翬 王時敏 仿古山水圖 冊 紙本|Landscapes after old masters,China,Qing dynasty (1644–1911),,,,Artist|Artist,"leaves k, l by",Wang Hui|Wang Shimin,"Chinese, 1632–1717|Chinese, 1592–1680",,Wang Hui|WANG SHIMIN,Chinese|Chinese,1632 |1592,1717 |1680,dated 1674 and 1677,1674,1677,Album of twelve leaves; ink and color on paper,"Ten paintings by Wang Hui (a–j): 8 5/8 x 13 1/4 in. (22 x 33.8 cm); two paintings by Wang Shimin (k, l): 10 x 13 in. (25.4 x 33 cm)","Purchase, The Dillon Fund Gift, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/41481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.439a–o,false,true,49113,Asian Art,Album,清 王鑑 仿古山水圖 冊 紙本|Landscapes in the styles of old masters,China,Qing dynasty (1644–1911),,,,Artist|Artist,Title piece by,Wang Jian|Wang Shimin,"Chinese, 1609–1677 or 1688|Chinese, 1592–1680",,Wang Jian|WANG SHIMIN,Chinese|Chinese,1609 |1592,1677 |1680,dated 1668,1668,1668,Album of ten paintings; ink and color on paper,Each leaf: 10 1/8 x 6 1/2 in. (25.7 x 16.5 cm),"Purchase, The Dillon Fund Gift, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.149,false,true,36433,Asian Art,Handscroll,"清 石濤(朱若極), 張子為 忍菴居士像 卷|Portrait of Ren'an in a Landscape",China,Qing dynasty (1644–1911),,,,Artist|Artist,,Shitao (Zhu Ruoji)|Zhang Ziwei,"Chinese, 1642–1707|Chinese, active late 17th century",,Shitao|Zhang Ziwei,Chinese|Chinese,1642 |1600,1707 |1700,dated 1684,1684,1684,Handscroll; ink and color on paper,22 15/18 x 53 7/8 in. (58 x 136.8 cm),"Purchase, The Dillon Fund Gift, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.481,false,true,51567,Asian Art,Handscroll,,China,Yuan dynasty (1271–1368),,,,Artist|Artist,In the style of,Unidentified Artist|Zhao Mengfu,"Chinese, 1254–1322",,Unidentified Artist|Zhao Mengfu,Chinese,1254,1322,dated 1309,1309,1309,Handscroll; ink and color on silk,15 3/8 in. × 13 ft. 2 1/2 in. (39.1 × 402.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.476,false,true,36041,Asian Art,Hanging scroll,元 佚名 仕女像 軸|Portrait of a Lady,China,Yuan dynasty (1271–1368),,,,Artist|Artist,In the style of,Unidentified Artist|Li Gonglin,"Chinese, ca. 1041–1106",Chinese,Unidentified Artist|Li Gonglin,Chinese,1041,1106,14th century,1300,1368,Hanging scroll; ink on silk,Image: 31 × 15 in. (78.7 × 38.1 cm) Overall with mounting: 64 × 20 7/8 in. (162.6 × 53 cm) Overall with knobs: 64 × 22 3/16 in. (162.6 × 56.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.49,false,true,51668,Asian Art,Handscroll,,China,Song dynasty (?) (960–1279),,,,Artist|Artist,After,Unidentified Artist|Wang Qihan,"Chinese, 10th century",,Unidentified Artist|Wang Qihan,Chinese,0900,0999,10th century,900,999,Handscroll; ink and color on silk,Image: 9 7/8 × 18 3/4 in. (25.1 × 47.6 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.26,false,true,40088,Asian Art,Fan mounted as an album leaf,南宋 佚名 舊傳閻次于 風雨維舟圖 團扇|Boats Moored in Wind and Rain,China,Southern Song dynasty (1127–1279),,,,Artist|Artist,Formerly Attributed to,Yan Ciyu|Unidentified Artist,"Chinese, act. ca. 1164–81","Chinese, 13th century",YAN CIYU|Unidentified Artist,Chinese,1164,1181,13th century,1200,1299,Fan mounted as an album leaf; ink and color on silk,9 3/4 x 10 3/4 in. (24.8 x 27.3 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.7.3,false,true,36141,Asian Art,Fan mounted as an album leaf,南宋 佚名 倣夏珪 冒雨尋莊圖 團扇|Returning Home in a Driving Rain,China,Southern Song dynasty (1127–1279),,,,Artist|Artist,After,Xia Gui|Unidentified,"Chinese, active ca. 1195–1230",,Xia Gui|Unidentified,Chinese,1195,1230,early 13th century,1200,1233,Fan mounted as an album leaf; ink and color on silk,10 1/16 x 10 3/8 in. (25.6 x 26.4 cm),"Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.274,false,true,40987,Asian Art,Handscroll,南宋 傳劉松年 倣高克明溪山雪意圖 卷|Streams and Mountains Under Fresh Snow,China,Southern Song dynasty (1127–1279),,,,Artist|Artist,Attributed to|traditionally attributed to,Liu Songnian|Gao Keming,"Chinese, active ca 1175–after 1195|ca 1000–1053",,LIU SONGNIAN|GAO KEMING,Chinese,1175 |1000,1195 |1053,ca. late 12th century,1175,1199,Handscroll; ink and color on silk,Image: 16 3/8 in. × 95 in. (41.6 × 241.3 cm) Overall with mounting: 16 3/8 in. × 42 ft. 11 5/16 in. (41.6 × 1308.9 cm),"Gift of John M. Crawford Jr., 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.73,false,true,51378,Asian Art,Folding fan mounted as an album leaf,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,After,Shen Zhou|Unidentified Artist,"Chinese, 1427–1509",,Shen Zhou|Unidentified Artist,Chinese,1427,1509,16th century or later,1500,1911,Folding fan mounted as an album leaf; ink on gold paper,6 7/8 x 20 3/4 in. (17.5 x 52.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.93,false,true,51490,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Ni Zan|Unidentified Artist,"Chinese, 1306–1374",,NI ZAN|Unidentified Artist,Chinese,1306,1374,dated 1374,1374,1374,Hanging scroll; ink on paper,33 7/8 x 12 7/8 in. (86.0 x 32.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.28,false,true,40422,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,After,Unidentified Artist|Tang Yin,"Chinese, 1470–1524",,Unidentified Artist|Tang Yin,Chinese,1470,1524,ca. 1525,1515,1535,Hanging scroll; ink and color on paper,Image: 89 x 40 1/4 in. (226.1 x 102.2 cm) Overall with mounting: 137 3/4 x 44 in. (349.9 x 111.8 cm) Overall with knobs: 137 3/4 x 48 5/8 in. (349.9 x 123.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.166,false,true,51535,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Qiu Ying,"Chinese, ca. 1495–1552",,Unidentified Artist|Qiu Ying,Chinese,1485,1562,ca. 1530,1520,1540,Handscroll; ink and color on silk,H. 12 3/8 in. (31.4 cm),"Gift of William Hu, 1919",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.27,false,true,51293,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,After,Guo Xi|Unidentified Artist,"Chinese, ca. 1000–ca. 1090",,Guo Xi|Unidentified Artist,Chinese,1000,1090,possibly 17th century,1600,1699,Handscroll; ink and color on silk,19 3/8 in. × 19 ft. 1 1/2 in. (49.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.87,false,true,48945,Asian Art,Handscroll,,China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Li Zai|Unidentified Artist,"Chinese, active 15th century",,Li Zai|Unidentified Artist,Chinese,1400,1500,"Dated ""yiwei"" (1595? 1655?)",1595,1655,Handscroll; ink and color on paper,Image: 11 x 126 in. (27.9 x 320 cm) Overall with mounting: 13 1/2 x 365 5/8 in. (34.3 x 928.7 cm),"Purchase, John M. Crawford Jr. Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.20.1,false,true,48941,Asian Art,Hanging scroll,清 佚名 肖像 軸|Portrait of a Scholar,China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Jin Chushi|Unidentified Artist,"Chinese, active late 12th century",,JIN CHUSHI|Unidentified Artist,Chinese,1167,1199,17th–18th century,1600,1799,Hanging scroll; ink and color on paper,Image: 35 3/4 in. × 15 in. (90.8 × 38.1 cm) Overall with mounting: 68 1/2 × 20 3/8 in. (174 × 51.8 cm) Overall with knobs: 68 1/2 × 22 1/16 in. (174 × 56 cm),"Gift of George D. Pratt, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.521a–k,false,true,37396,Asian Art,Album,"明/清 陳洪綬、陳字 雜畫 冊 絹本|Figures, flowers, and landscapes",China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist|Artist,leaves a–d by|leaves e–k by,Chen Hongshou|Chen Zi,"Chinese, 1599–1652|Chinese, 1634–1711",,Chen Hongshou|Chen Zi,Chinese|Chinese,1599 |1634,1652 |1711,one leaf dated 1627,1600,1711,Album of eleven leaves; ink and color on silk,Image: 8 3/4 x 8 9/16 in. (22.2 x 21.7 cm),"Gift of Mr. and Mrs. Wan-go H. C. Weng, 1999",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.62,false,true,45814,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Wang Chong|Unidentified Artist,"Chinese, 1494–1533",,Wang Chong|Unidentified Artist,Chinese,1494,1533,,1368,1644,Folding fan mounted as an album leaf; ink on paper,6 3/4 x 20 in. (17.1 x 50.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/45814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.60,false,true,35983,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Peng Nian|Unidentified Artist,"Chinese, 1505–1566",,Peng Nian|Unidentified Artist,Chinese,1505,1566,,1368,1644,Folding fan mounted as an album leaf; ink on paper,6 1/2 x 18 1/2 in. (16.5 x 47.0 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.78,false,true,48903,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Hai Rui|Unidentified Artist,"Chinese, 1514–1587",,Hai Rui|Unidentified Artist,Chinese,1514,1587,,1514,1587,Hanging scroll; ink on paper,Overall with mounting: 108 3/4 x 28 in. (276.2 x 71.1 cm) Image: 82 3/8 x 20 1/8 in. (209.2 x 51.1 cm) Overall with knobs: 108 3/4 x 31 3/4 in. (276.2 x 80.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.31,false,true,40510,Asian Art,Hanging scroll,明 佚名 倣趙孟頫 草書湘簾疏織七絕詩 軸|A Summer Idyll,China,Yuan dynasty (1271–1368),,,,Artist|Artist,After,Zhao Mengfu|Unidentified Artist,"Chinese, 1254–1322",Chinese,Zhao Mengfu|Unidentified Artist,Chinese,1254,1322,,1319,1368,"Hanging scroll; ink on silk, 3 columns in large running-cursive script",Image: 52 1/2 x 20 7/8 in. (133.4 x 53 cm) Overall with mounting: 111 1/2 x 29 in. (283.2 x 73.7 cm) Overall with knobs: 111 1/2 x 32 1/4 in. (283.2 x 81.9 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.34,false,true,51870,Asian Art,Handscroll,,China,Yuan dynasty (1271–1368),,,,Artist|Artist,Copy after,Zhang Yu|Unidentified Artist,"Chinese, 1283–1350",Chinese,ZHANG YU|Unidentified Artist,Chinese,1283,1350,,1271,1368,Handscroll; ink on paper,Image: 11 3/4 x 63 in. (29.8 x 160 cm) Overall with mounting: 12 1/16 x 192 9/16 in. (30.6 x 489.1 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.1,false,true,51869,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly attributed to,Huaisu|Unidentified Artist,"Chinese, 725–785","Chinese, 16th century",Huaisu|Unidentified Artist,Chinese,0725,0785,,1368,1911,Handscroll; ink on silk,Image (1): 10 5/8 x 6 1/8 in. (27 x 15.6 cm) Image (2): 10 3/8 x 7 1/4 in. (26.4 x 18.4 cm) Image (3): 10 5/8 x 13 3/8 in. (27 x 34 cm) Overall with mounting: 11 15/16 x 229 1/8 in. (30.3 x 582 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.80,false,true,48906,Asian Art,Folding fan mounted as an album leaf,,China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist|Artist,After,Zhou Tianqiu|Unidentified Artist,"Chinese, 1514–1595",,Zhou Tianqiu|Unidentified Artist,Chinese,1514,1595,,1368,1644,Folding fan mounted as an album leaf; ink on gold-flecked paper,6 7/8 x 21 1/4 in. (17.5 x 54.0 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.78,false,true,35991,Asian Art,Folding fan mounted as an album leaf,,China,late Ming (1368–1644)–early Qing (1644–1911) dynasty,,,,Artist|Artist,After,Chen Hongshou|Unidentified Artist,"Chinese, 1599–1652",,Chen Hongshou|Unidentified Artist,Chinese,1599,1652,,1368,1911,Folding fan mounted as an album leaf; ink on paper,6 3/8 x 19 7/8 in. (16.2 x 50.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/35991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP26,false,true,63220,Asian Art,Print,,China,,,,,Artist|Artist,In the Style of|Original painted by,Guo Songzheng|Xiao Yuncong,"Chinese|Chinese, 1596–1673",,Guo Songzheng|Xiao Yuncong,Chinese|Chinese,1596,1673,,0,0,Circular fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP28,false,true,63225,Asian Art,Print,,China,,,,,Artist|Artist,Original painted by|In the Style of,Shi Daoren|Li Cheng,"Chinese, Qing dynasty|Chinese, 919–967",,Shi Daoren|LI CHENG,Chinese|Chinese,0919,0967,,0,0,Fan-shaped woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP11,false,true,63205,Asian Art,Print,,China,,,,,Artist|Artist,In the Style of|Original painted by,Hwang Yifeng|Xiao Yuncong,"Chinese, 1269–1354|Chinese, 1596–1673",,Hwang Yifeng|Xiao Yuncong,Chinese|Chinese,1269 |1596,1354 |1673,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP12,false,true,63206,Asian Art,Print,,China,,,,,Artist|Artist,Original painted by|In the style of,Hwa Dang|Wu Zhen,"Chinese, Ming dynasty|Chinese, 1280–1354",,Hwa Dang|Wu Zhen,Chinese|Chinese,1280,1354,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP14,false,true,63208,Asian Art,Print,,China,,,,,Artist|Artist,Original by|In the Style of,Gao Fangshan|Mi Yuanzhang,"Chinese, Yuan dynasty|Chinese, 1051–1107",,Gao Fangshan|Mi Yuanzhang,Chinese|Chinese,1051,1107,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +CP31,false,true,63228,Asian Art,Print,,China,,,,,Artist|Artist,Original painted by|In the Style of,Gao Fangshan|Mi Yuanzhang,"Chinese, Yuan dynasty|Chinese, 1051–1107",,Gao Fangshan|Mi Yuanzhang,Chinese|Chinese,1051,1107,,0,0,Polychrome woodblock print; ink and color on paper,9 5/8 x 11 13/16 in. (24.4 x 30 cm),"Rogers Fund, 1924",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.121.10,false,true,40102,Asian Art,Fan mounted as an album leaf,,China,,,,,Artist|Artist,After,Li Song|Unidentified Artist,"Chinese, ca. 1190–1260","Chinese, 13th–15th century?",Li Song|Unidentified Artist,Chinese,1190,1260,,1200,1499,Fan mounted as an album leaf; ink and color on silk,10 3/8 x 10 1/2 in. (26.4 x 26.7 cm),"Ex coll.: C. C. Wang Family, Purchase, Gift of J. Pierpont Morgan, by exchange, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.3,false,true,39917,Asian Art,Hanging scroll mounted as handscroll,,China,Song dynasty (960–1279),,,,Artist|Artist,After,Su Shi|Unidentified Artist,"Chinese, 1037–1101",,Su Shi|Unidentified Artist,Chinese,1037,1101,,1037,1101,Hanging scroll mounted as a handscroll; ink on paper,Image: 21 3/8 in. × 13 in. (54.3 × 33 cm) Overall with mounting: 14 3/4 in. × 34 ft. 10 3/16 in. (37.5 × 1062.2 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.113,false,true,51396,Asian Art,Fan mounted as an album leaf,,China,Song dynasty (960–1279),,,,Artist|Artist,After,Emperor Huizong|Unidentified Artist,"Chinese, 1082–1135; r. 1100–25",,Huizong Emperor|Unidentified Artist,Chinese,1082,1082,,1082,1135,Fan mounted as an album leaf; ink and color on silk,9 x 9 1/4 in. (22.9 x 23.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.33.1,false,true,51540,Asian Art,Album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Gu Kaizhi,344–405,,Unidentified Artist|Gu Kaizhi,Chinese,0344,0405,,1368,1644,Album leaf; ink and color on silk,7 1/8 x 8 7/8 in. (18.1 x 22.5 cm),"Rogers Fund, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.148,false,true,51746,Asian Art,Fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,Copy after,Unidentified Artist|Li Anzhong,active 12th century,,Unidentified Artist|Li Anzhong,Chinese,0012,0012,,1368,1644,Fan mounted as an album leaf; ink and color on silk,9 3/4 x 10 in. (24.8 x 25.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.4.2,false,true,45754,Asian Art,Hanging scroll,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Tang Yin|Unidentified Artist,"Chinese, 1470–1524",,Tang Yin|Unidentified Artist,Chinese,1470,1524,,1368,1644,Hanging scroll; ink and color on paper,Image: 53 5/8 x 23 1/8 in. (136.2 x 58.7 cm) Overall with mounting: 109 1/4 x 30 1/4 in. (277.5 x 76.8 cm) Overall with knobs: 109 1/4 x 34 in. (277.5 x 86.4 cm),"Gift of Douglas Dillon, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.53,false,true,45774,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Tang Yin|Unidentified Artist,"Chinese, 1470–1524",,Tang Yin|Unidentified Artist,Chinese,1470,1524,,1368,1644,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,7 1/4 x 20 1/8 in. (18.4 x 51.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.59,false,true,48872,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Lu Zhi|Unidentified Artist,"Chinese, 1495–1576",,Lu Zhi|Unidentified Artist,Chinese,1495,1576,,1368,1644,Folding fan mounted as an album leaf; ink on gold paper,7 1/4 x 19 1/4 in. (18.4 x 48.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.45,false,true,51664,Asian Art,Fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,In the style of,Unidentified Artist|Yanxiu,"Chinese, early 10th century",,Unidentified Artist|Yanxiu,Chinese,0900,0999,,1368,1644,Fan mounted as an album leaf; ink and color on silk,8 x 8 3/16 in. (20.3 x 20.8 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.64,false,true,51679,Asian Art,Fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Hao Cheng,"Chinese, early 11th century",,Unidentified Artist|Hao Cheng,Chinese,1000,1035,,1000,1035,Fan mounted as an album leaf; ink and color on silk,8 5/8 x 9 3/16 in. (21.9 x 23.3 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.62,false,true,51678,Asian Art,Fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Unidentified Artist|Ma Lin,"Chinese, ca. 1180– after 1256",,Unidentified Artist|MA LIN,Chinese,1180,1256,,1368,1644,Fan mounted as an album leaf; ink and color on silk,9 3/4 x 10 1/16 in. (24.8 x 25.6 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.41,false,true,45677,Asian Art,Album leaf,元 佚名 倣夏永 呂洞賓過岳陽樓 冊頁|The Immortal Lü Dongbin Appearing over the Yueyang Pavilion,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Unidentified Artist|Xia Yong,"Chinese, active mid-14th century","Chinese, 15th–16th century?",Unidentified Artist|XIA YONG,Chinese,1336,1370,,1400,1599,Album leaf; ink on silk,Image: 8 5/8 x 7 3/8 in. (21.9 x 18.7 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.61,false,true,35984,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Song Maojin|Unidentified Artist,"Chinese, active late 16th century–early 17th century",,Song Maojin|Unidentified Artist,Chinese,1571,1635,,1368,1644,Folding fan mounted as an album leaf; ink and color on gold paper,6 1/4 x 18 1/4 in. (15.9 x 46.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.99e,false,true,51498,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Cao Zhibai|Unidentified Artist,"Chinese, died 1355",,Cao Zhibai|Unidentified Artist,Chinese,,1355,,1644,1911,Album leaf; ink and color on silk,11 7/16 x 14 5/8 in. (29.1 x 37.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.20,false,true,51428,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Unidentified Artist|Zhao Mengfu,"Chinese, 1254–1322",,Unidentified Artist|Zhao Mengfu,Chinese,1254,1322,,1644,1911,Hanging scroll; ink and color on silk,21 1/2 x 13 3/4 in. (54.6 x 34.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.99g,false,true,51500,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Zhao Mengfu|Unidentified Artist,"Chinese, 1254–1322",,Zhao Mengfu|Unidentified Artist,Chinese,1254,1322,,1644,1911,Album leaf; ink and color on silk,10 1/16 x 12 in. (25.6 x 30.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.126,false,true,51515,Asian Art,Handscroll,清 倣米芾雲山圖 卷|Mountain Scenery,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Dong Qichang|Unidentified Artist,"Chinese, 1555–1636",,Dong Qichang|Unidentified Artist,Chinese,1555,1636,,1644,1911,Handscroll; ink on silk,10 1/2 x 86 in. (26.7 x 218.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.145,false,true,49159,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Yun Shouping|Unidentified Artist,"Chinese, 1633–1690",,YUN SHOUPING|Unidentified Artist,Chinese,1633,1690,,1644,1911,Album leaf; ink and color on paper,8 1/8 x 12 in. (20.6 x 30.5 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.146,false,true,49160,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Yun Shouping|Unidentified Artist,"Chinese, 1633–1690",,YUN SHOUPING|Unidentified Artist,Chinese,1633,1690,,1644,1911,Album leaf; ink and color on paper,11 7/8 x 8 1/8 in. (30.2 x 20.6 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.19,false,true,51427,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Fang-hu,"Chinese, active ca. 1380",,Unidentified Artist|Fang-hu,Chinese,1380,1380,,1644,1911,Hanging scroll; ink on paper,33 1/8 x 18 5/8 in. (84.1 x 47.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.170,false,true,51284,Asian Art,Handscroll,清 佚名 清明上河圖|Going Upriver on the Qingming Festival,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Unidentified Artist|Qiu Ying,"Chinese, ca. 1495–1552","Chinese, 18th century?",Unidentified Artist|Qiu Ying,Chinese,1485,1562,,1644,1911,Handscroll; ink and color on silk,11 1/2 in. × 21 ft. 2 in. (29.2 × 645.2 cm),"Rogers Fund, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.124.5,false,true,51527,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Gong Kai,"Chinese, 1222–after 1304",,Unidentified Artist|Gong Kai,Chinese,1222,1310,,1644,1911,Handscroll; black and white on paper,H. 12 1/2 in. (31.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.131a–j,false,true,51519,Asian Art,Album,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Song Xu|Unidentified Artist,"Chinese, 1525–after 1606",,Song Xu|Unidentified Artist,Chinese,1525,1610,,1644,1911,Album of ten leaves; ink and color on silk,Each painting: 11 1/4 x 11 1/2 in. (28.6 x 29.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.28,false,true,51574,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly attributed to,Gong Ji|Unidentified Artist,"Chinese, Northern Song dynasty",,Gong Ji|Unidentified Artist,Chinese,0960,1127,,1644,1911,Hanging scroll; ink and color on silk,26 1/2 x 16 in. (67.3 x 40.6 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.99f,false,true,51499,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Wang Yuan|Unidentified Artist,"Chinese, ca. 1280–after 1349",,Wang Yuan|Unidentified Artist,Chinese,1270,1359,,1644,1911,Album leaf; ink and color on silk,19 3/4 x 12 3/8 in. (50.2 x 31.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.89,false,true,51701,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Unidentified Artist|Xia Gui,"Chinese, active ca. 1195–1230",,Unidentified Artist|Xia Gui,Chinese,1195,1230,,1644,1911,Handscroll; ink on silk,14 3/8 x 69 3/4 in. (36.5 x 177.2 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.5,false,true,51637,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Zhao Chang,"Chinese, active 10th–11th century",,Unidentified Artist|Zhao Chang,Chinese,0925,1025,,1644,1911,Handscroll; ink and color on silk,Image: 9 15/16 × 79 5/8 in. (25.2 × 202.2 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.16,false,true,51425,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly attributed to,Unidentified Artist|Zhao Lingrang,"Chinese, active ca. 1070– after 1100",,Unidentified Artist|ZHAO LINGRANG,Chinese,1070,1100,,1644,1911,Hanging scroll; ink and color on silk,Image: 34 x 14 1/2 in. (86.4 x 36.8 cm) Overall with mounting: 94 1/2 x 20 5/8 in. (240 x 52.4 cm) Overall with knobs: 94 1/2 x 24 3/4 in. (240 x 62.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.461,false,true,50360,Asian Art,Hanging scroll,,China,Yuan dynasty (1271–1368),,,,Artist|Artist,In the style of,Unidentified Artist|Wu Daozi,"Chinese, 689–after 755",,Unidentified Artist|Wu Daozi,Chinese,0689,0760,,1271,1368,Hanging scroll; ink and color on silk,Image: 40 1/2 × 20 3/8 in. (102.9 × 51.8 cm) Overall with mounting: 72 × 27 3/8 in. (182.9 × 69.5 cm) Overall with knobs: 72 × 29 3/8 in. (182.9 × 74.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.72,false,true,51687,Asian Art,Album leaf,,China,Song dynasty (?) (960–1279),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Mao I,active 12th century,,Unidentified Artist|Mao I,Chinese,0012,0012,,960,1279,Album leaf; ink and color on silk,9 3/4 x 9 15/16 in. (24.8 x 25.2 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.28,false,true,51654,Asian Art,Album leaf,,China,Song dynasty (?) (960–1279),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Ma Lin,"Chinese, ca. 1180– after 1256",,Unidentified Artist|MA LIN,Chinese,1180,1256,,960,1279,Album leaf; ink on silk,9 9/16 x 10 in. (24.3 x 25.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.114,false,true,51397,Asian Art,Album leaf,,China,Song dynasty (?) (960–1279),,,,Artist|Artist,in the style of,Unidentified Artist|Zhao Chang,"Chinese, active 10th–11th century",,Unidentified Artist|Zhao Chang,Chinese,0925,1025,,960,1279,Album leaf; ink and color on silk,8 1/8 x 9 3/8 in. (20.6 x 23.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.95,false,true,51707,Asian Art,Handscroll,,China,Song dynasty (?) (960–1279),,,,Artist|Artist,In the style of,Unidentified Artist|Emperor Gaozong,"Chinese, 1107–1187, r. 1127–1162",,Unidentified Artist|Gaozong Emperor,Chinese,1107,1187,,960,1279,Handscroll; ink and color on silk,10 7/8 x 83 1/8 in. (27.6 x 211.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.34,false,true,51307,Asian Art,Handscroll,,China,Ming dynasty (1368–1644) (?),,,,Artist|Artist,After,Wu Zhen|Unidentified Artist,"Chinese, 1280–1354",,Wu Zhen|Unidentified Artist,Chinese,1280,1354,,1368,1644,Handscroll; ink on silk,13 1/2 in. × 17 ft. 3 3/4 in. (34.3 × 527.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.124.2,false,true,36032,Asian Art,Handscroll,,China,Ming dynasty (1368–1644) (?),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Li Gonglin,"Chinese, ca. 1041–1106",,Unidentified Artist|Li Gonglin,Chinese,1041,1106,,1368,1644,Handscroll; ink on silk,13 3/8 × 85 in. (34 × 215.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.86,false,true,51698,Asian Art,Handscroll,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,Copy after,Unidentified Artist|Chen Rong,active 1235–62,,Unidentified Artist|Chen Rong,Chinese,1235,1262,,1368,1644,Handscroll; ink on silk,Overall: 15 5/8 x 115 1/4 in. (39.7 x 292.7 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.145,false,true,51744,Asian Art,Album leaf,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,Copy after,Unidentified Artist|Yan Liben,"Chinese, 640–680",,Unidentified Artist|Yan Liben,Chinese,0640,0680,,1368,1644,Album leaf; ink and color on silk,10 x 5 15/16 in. (25.4 x 15.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.124.1,false,true,51526,Asian Art,Handscroll,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,In the Style of,Unidentified Artist|Wu Zongyuan,"Chinese, died 1050",,Unidentified Artist|Wu Zongyuan,Chinese,0950,1050,,1368,1644,Handscroll; ink and color on silk,20 1/2 in. × 16 ft. 3 15/16 in. (52.1 × 497.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.5,false,true,51522,Asian Art,Hanging scroll,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Li Anzhong,active 12th century,,Unidentified Artist|Li Anzhong,Chinese,0012,0012,,1368,1644,Hanging scroll; color on silk,Image: 44 1/4 × 18 1/4 in. (112.4 × 46.4 cm) Overall with mounting: 79 1/2 × 23 3/4 in. (201.9 × 60.3 cm) Overall with knobs: 79 1/2 × 25 1/2 in. (201.9 × 64.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.117,false,true,51724,Asian Art,Handscroll,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,In the style of,Unidentified Artist|Wu Zhen,"Chinese, 1280–1354",,Unidentified Artist|Wu Zhen,Chinese,1280,1354,,1368,1644,Handscroll; ink on silk,12 1/2 x 262 in. (31.8 x 665.5 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.143,false,true,51742,Asian Art,Album leaf,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,Copy after (?),Unidentified Artist|Huang Qüan,"Chinese, active ca. 950",,Unidentified Artist|Huang Qüan,Chinese,0940,0960,,1368,1644,Album leaf; ink and color on silk,8 3/4 x 9 1/2 in. (22.2 x 24.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"54.134.2a, b",false,true,51806,Asian Art,Handscroll,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,In the style of,Unidentified Artist|Qiu Ying,"Chinese, ca. 1495–1552",,Unidentified Artist|Qiu Ying,Chinese,1485,1562,,1368,1644,Handscroll; ink and color on silk,(a.) 14 15/16 x 94 1/2 in. (37.9 x 240 cm); (b.) 51 x 14 3/8 in. (129.5 x 36.5 cm),"Gift of the Pierpont Morgan Library, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.38,false,true,51660,Asian Art,Fan mounted as an album leaf,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,In the style of,Unidentified Artist|Luo Zonggui,"Chinese, active 1228–1234",,Unidentified Artist|Luo Zonggui,Chinese,1228,1234,,1368,1644,Fan mounted as an album leaf; ink and color on silk,9 1/8 x 7 1/8 in. (23.2 x 18.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.40,false,true,51662,Asian Art,Fan mounted as an album leaf,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,In the style of,Unidentified Artist|Xia Gui,"Chinese, active ca. 1195–1230",,Unidentified Artist|Xia Gui,Chinese,1195,1230,,1368,1644,Fan mounted as an album leaf; ink on silk,9 1/8 x 9 1/2 in. (23.2 x 24.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.146,false,true,51745,Asian Art,Album leaf,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,Copy after (?),Unidentified Artist|Zhou Fang,"Chinese, active ca. 780– ca.810",,Unidentified Artist|ZHOU FANG,Chinese,0780,0810,,1368,1644,Album leaf; ink and color on silk,7 1/2 x 8 1/2 in. (19.1 x 21.6 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.105,false,true,51715,Asian Art,Hanging scroll,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,In the style of,Unidentified Artist|Lü Ji,"Chinese, active late 15th century",,Unidentified Artist|Lü Ji,Chinese,1430,1504,,1368,1644,Hanging scroll; ink and color on silk,75 x 38 7/8 in. (190.5 x 98.7 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.107,false,true,51393,Asian Art,Fan mounted as an album leaf,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,After,Emperor Huizong|Unidentified Artist,"Chinese, 1082–1135; r. 1100–25",,Huizong Emperor|Unidentified Artist,Chinese,1082,1082,,1368,1644,Fan mounted as an album leaf; ink and color on silk,9 1/4 x 9 5/8 in. (23.5 x 24.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.48,false,true,51667,Asian Art,Handscroll,,China,Qing dynasty (?) (1644–1911),,,,Artist|Artist,In the style of,Unidentified Artist|Mi Fu,"Chinese, 1052–1107",,Unidentified Artist|MI FU,Chinese,1052,1107,,1644,1911,Handscroll; ink on silk,13 1/4 x 110 3/16 in. (33.7 x 279.9 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.139,false,true,51738,Asian Art,Album leaf,,China,Yuan dynasty (?) (1271–1368),,,,Artist|Artist,Copy after,Unidentified Artist|Li Gonglin,"Chinese, ca. 1041–1106",,Unidentified Artist|Li Gonglin,Chinese,1041,1106,,1271,1368,Album leaf; ink and color on silk,12 x 12 5/16 in. (30.5 x 31.3 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.17,false,true,51426,Asian Art,Hanging scroll,,China,late Qing dynasty (1644–1911),,,,Artist|Artist,In the Style of,Wen Zhengming|Unidentified Artist,"Chinese, 1470–1559",,Wen Zhengming|Unidentified Artist,Chinese,1470,1559,,1500,1599,Hanging scroll; ink and color on paper,26 1/8 x 10 in. (66.4 x 25.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.98,false,true,51710,Asian Art,Handscroll,,China,early Ming dynasty (1368–1644),,,,Artist|Artist,Copy after,Unidentified Artist|Huang Qüan,"Chinese, active ca. 950",,Unidentified Artist|Huang Qüan,Chinese,0940,0960,,1368,1644,Handscroll; ink and color on silk,14 x 87 1/2 in. (35.6 x 222.3 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.103,false,true,51389,Asian Art,Fan mounted as an album leaf,,China,Song dynasty (960–1279) or later,,,,Artist|Artist,Formerly attributed to,Unidentified Artist|Wu Bing,"Chinese, active 1190–1194",,Unidentified Artist|Wu Bing,Chinese,1190,1194,,960,1300,Fan mounted as an album leaf; Ink and color on silk,12.25 x 12.25 in. (31.1 x 31.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.49,false,true,35979,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644) or later,,,,Artist|Artist,After,Wen Zhengming|Unidentified Artist,"Chinese, 1470–1559",,Wen Zhengming|Unidentified Artist,Chinese,1470,1559,,1368,1644,Folding fan mounted as an album leaf; ink and color on gold-flecked paper,7 1/8 x 20 1/2 in. (18.1 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/35979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.31.2,false,true,51586,Asian Art,Handscroll,,China,Ming dynasty (1368–1644) or earlier,,,,Artist|Artist,In the style of,Unidentified Artist|Zhao Boju,"Chinese, 11th century",,Unidentified Artist|Zhao Boju,Chinese,1000,1099,,1300,1644,Handscroll; ink and color on silk,11 5/8 x 95 1/2 in. (29.5 x 242.6 cm),"Fletcher Fund, 1938",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.98,false,true,51384,Asian Art,Fan mounted as an album leaf,,China,Song (960–1279)–Ming (1368–1644) dynasty,,,,Artist|Artist,After,Emperor Huizong|Unidentified Artist,"Chinese, 1082–1135; r. 1100–25",,Huizong Emperor|Unidentified Artist,Chinese,1082,1082,,960,1644,Fan mounted as an album leaf; ink and color on silk,9 1/8 x 9 1/2 in. (23.2 x 24.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.110,false,true,45782,Asian Art,Hanging scroll,,China,Ming (1368–1644)–Qing (1644–1911) dynasty,,,,Artist|Artist,After,Wen Zhengming|Unidentified Artist,"Chinese, 1470–1559",,Wen Zhengming|Unidentified Artist,Chinese,1470,1559,,1368,1911,Hanging scroll; ink and color on paper,25 x 11 1/2 in. (63.5 x 29.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.24,false,true,52080,Asian Art,Handscroll,,China,Ming (1368–1644)–Qing (1644–1911) dynasty,,,,Artist|Artist,In the Style of,Li Gonglin|Unidentified Artist,"Chinese, ca. 1041–1106",,Li Gonglin|Unidentified Artist,Chinese,1041,1106,,1368,1911,Handscroll; ink on paper,11 7/8 in. × 19 ft. 5 1/2 in. (30.2 × 593.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/52080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.99a,false,true,51495,Asian Art,Album leaf,,China,Ming (1368–1644)–Qing (1644–1911) dynasty,,,,Artist|Artist,After,Lu Qing|Unidentified Artist,"Chinese, active 1190–1195",,Lu Qing|Unidentified Artist,Chinese,1190,1195,,1368,1911,Album leaf; ink on silk,11 13/16 x 16 in. (30 x 40.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.99b,false,true,36020,Asian Art,Album leaf,,China,Ming (1368–1644)–Qing (1644–1911) dynasty,,,,Artist|Artist,In the Style of,Ma Lin|Unidentified Artist,"Chinese, ca. 1180– after 1256",,MA LIN|Unidentified Artist,Chinese,1180,1256,,1368,1911,Album leaf; ink and color on silk,10 3/4 x 10 3/4 in. (27.3 x 27.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.18,false,true,51537,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Wu Zongyuan,"Chinese, died 1050",,Unidentified Artist|Wu Zongyuan,Chinese,0950,1050,,1368,1911,Handscroll; ink and color on silk,20 1/2 x 75 3/4 in. (52.1 x 192.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.100a–l,false,true,51505,Asian Art,Album leaves,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Border by|Formerly attributed to,Unidentified Artist|Yi Yuanji,"Chinese, died 1066",,Unidentified Artist|Yi Yuanji,Chinese,0966,1066,,1368,1911,Set of twelve album leaves; ink and color on silk,Each: 11 1/2 x 10 5/8 in. (29.2 x 27 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.53,false,true,51544,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the Style of,Unidentified Artist|Zhao Mengfu,"Chinese, 1254–1322",,Unidentified Artist|Zhao Mengfu,Chinese,1254,1322,,1368,1911,Handscroll; ink and color on silk,10 5/8 × 34 1/2 in. (27 × 87.6 cm),"Gift of Lewis Cass Ledyard, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.95,false,true,51492,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Zhao Mengfu,"Chinese, 1254–1322",,Unidentified Artist|Zhao Mengfu,Chinese,1254,1322,,1368,1911,Hanging scroll; ink and color on silk,56 x 23 1/2 in. (142.2 x 59.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.480,false,true,51566,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Zhao Mengfu,"Chinese, 1254–1322",,Unidentified Artist|Zhao Mengfu,Chinese,1254,1322,,1368,1911,Handscroll; ink on silk,13 1/8 in. × 11 ft. 5 in. (33.3 × 348 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.29,false,true,51481,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,After,Unidentified Artist|Wen Zhengming,"Chinese, 1470–1559",,Unidentified Artist|Wen Zhengming,Chinese,1470,1559,,1368,1911,Hanging scroll; ink on silk,69 x 30 1/2 in. (175.3 x 77.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.113,false,true,51510,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Lu Zhi,"Chinese, 1495–1576",,Unidentified Artist|Lu Zhi,Chinese,1495,1576,,1368,1911,Hanging scroll; ink and color on paper,25 3/8 x 16 1/2 in. (64.5 x 41.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.530,false,true,51572,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Jing Hao,active ca. 900–960,,Unidentified Artist|Jing Hao,Chinese,0900,0960,,1368,1911,Handscroll; ink and color on silk,16 3/8 x 52 in. (41.6 x 132.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.4,false,true,53586,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Zhao Boju,"Chinese, 11th century",,Unidentified Artist|Zhao Boju,Chinese,1000,1099,,1368,1911,Handscroll; ink and color on silk,Image: 6 3/8 × 34 1/2 in. (16.2 × 87.6 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.106,false,true,51507,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Zhao Boju,"Chinese, 11th century",,Unidentified Artist|Zhao Boju,Chinese,1000,1099,,1368,1911,Hanging scroll; ink and color on silk,Image: 15 3/4 x 12 1/8 in. (40 x 30.8 cm) Overall (with colophons): 35 1/4 x 12 1/8 in. (89.5 x 30.8 cm) Overall with mounting: 57 1/4 x 14 7/8 in. (145.4 x 37.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.88,false,true,40424,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Huang Qüan,"Chinese, active ca. 950",,Unidentified Artist|Huang Qüan,Chinese,0940,0960,,1368,1911,Hanging scroll,Image: 77 x 37 in. (195.6 x 94 cm) Overall with mounting: 123 x 40 7/8 in. (312.4 x 103.8 cm) Overall with knobs: 123 x 45 1/4 in. (312.4 x 114.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.25.1,false,true,51524,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Li Gonglin,"Chinese, ca. 1041–1106",,Unidentified Artist|Li Gonglin,Chinese,1041,1106,,1368,1911,Handscroll; ink on silk,11 3/8 × 58 in. (28.9 × 147.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.25.2,false,true,51525,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Li Gonglin,"Chinese, ca. 1041–1106",,Unidentified Artist|Li Gonglin,Chinese,1041,1106,,1368,1911,Handscroll; ink on silk,13 3/8 x 85 in. (34 x 215.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.443,false,true,51559,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Li Gonglin,"Chinese, ca. 1041–1106",,Unidentified Artist|Li Gonglin,Chinese,1041,1106,,1368,1911,Hanging scroll; color on silk,Image: 47 3/4 × 18 1/4 in. (121.3 × 46.4 cm) Overall with mounting: 84 × 24 1/4 in. (213.4 × 61.6 cm) Overall with knobs: 84 × 26 1/2 in. (213.4 × 67.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.62,false,true,51523,Asian Art,Album leaf,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Li Gonglin|Unidentified Artist,"Chinese, ca. 1041–1106",,Li Gonglin|Unidentified Artist,Chinese,1041,1106,,1368,1911,Albun leaf; ink and color on silk,Image: 10 1/4 × 10 1/4 in. (26 × 26 cm) Sheet: 12 1/4 × 15 3/4 in. (31.1 × 40 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.296,false,true,51580,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Lin Liang,"Chinese, ca. 1416–1480",,Unidentified Artist|Lin Liang,Chinese,1416,1480,,1368,1911,Hanging scroll; ink on silk,Image: 37 1/2 × 18 7/8 in. (95.3 × 47.9 cm) Overall with mounting: 68 1/8 × 23 5/8 in. (173 × 60 cm) Overall with knobs: 68 1/8 × 25 5/8 in. (173 × 65.1 cm),"Rogers Fund, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.76.297,false,true,53515,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Lin Liang,"Chinese, ca. 1416–1480",,Unidentified Artist|Lin Liang,Chinese,1416,1480,,1368,1911,Hanging scroll; ink on silk,37 1/2 x 18 7/8 in. (95.3 x 47.9 cm),"Rogers Fund, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.112,false,true,40420,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Signature of,Unidentified Artist|Cui Zizhong,"Chinese, ca. 1595–1644",,Unidentified Artist|Cui Zizhong,Chinese,1595,1644,,1368,1911,Hanging scroll; ink and color on silk,57 x 33 3/4 in. (144.8 x 85.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.96,false,true,51493,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Guo Xi|Unidentified Artist,"Chinese, ca. 1000–ca. 1090",,Guo Xi|Unidentified Artist,Chinese,1000,1090,,1368,1911,Handscroll; ink on silk,19 in. × 22 ft. 7 in. (48.3 × 688.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.107,false,true,51508,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Fang Congyi,"Chinese, ca. 1301–after 1378",,Unidentified Artist|Fang Congyi,Chinese,1301,1399,,1368,1911,Hanging scroll; ink on paper,Image: 45 × 18 1/2 in. (114.3 × 47 cm) Overall with mounting: 109 × 25 1/4 in. (276.9 × 64.1 cm) Overall with knobs (only one knob): 109 × 29 1/2 in. (276.9 × 74.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.125,false,true,51514,Asian Art,Panels mounted as a handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Spurious signature of,Unidentified Artist|Wang Zhenpeng,"Chinese, active ca. 1275–1330",,Unidentified Artist|WANG ZHENPENG,Chinese,1265,1340,,1600,1913,Ten panels mounted as a handscroll; ink and color on silk,"Image (each panel, approx.): 13 3/8 × 16 1/4 in. (34 × 41.3 cm)","John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.521,false,true,51569,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Qian Xuan,"Chinese, ca. 1235–before 1307",,Unidentified Artist|QIAN XUAN,Chinese,1235,1307,,1368,1911,Hanging scroll; ink and color on silk,23 1/8 x 30 in. (58.7 x 76.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.159,false,true,51534,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Emperor Huizong,"Chinese, 1082–1135; r. 1100–25",,Unidentified Artist|Huizong Emperor,Chinese,1082,1082,,1368,1911,Hanging scroll; on silk,46 x 21 in. (116.8 x 53.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.89,false,true,51487,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Emperor Huizong,"Chinese, 1082–1135; r. 1100–25",,Unidentified Artist|Huizong Emperor,Chinese,1082,1082,,1368,1911,Handscroll; ink and color on silk,12 3/8 x 42 3/4 in. (31.4 x 108.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.22,false,true,51477,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,After,Unidentified Artist|Liu Songnian,"Chinese, active ca 1175–after 1195",,Unidentified Artist|LIU SONGNIAN,Chinese,1175,1195,,1368,1911,Handscroll; Ink and color on silk,8 1/2 x 56 1/8 in. (21.6 x 142.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.30,false,true,51575,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Liu Songnian,"Chinese, active ca 1175–after 1195",,Unidentified Artist|LIU SONGNIAN,Chinese,1175,1195,,1368,1911,Handscroll; ink and color on silk,14 3/8 in. × 11 ft. 1 in. (36.5 × 337.8 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.97,false,true,51709,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing (1644–1911) dynasty,,,,Artist|Artist,Attributed to|Copy after,Zhao Mengfu|Shi Daoshi,"Chinese, 1254–1322|Chinese, active 4th century",,Zhao Mengfu|Shi Daoshi,Chinese|Chinese,1254 |0300,1322 |0399,,1368,1911,Handscroll; ink and color on silk,11 5/8 x 104 1/4 in. (29.5 x 264.8 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.170.2,false,true,40409,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the Style of,Unidentified Artist|Wang Wei,"Chinese, 699–759",,Unidentified Artist|Wang Wei,Chinese,0699,0759,,1368,1911,Handscroll; ink and color on silk,10 1/2 x 80 3/16 in. (26.7 x 203.7 cm),"Gift of Robert Lehman, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.73,false,true,51688,Asian Art,Fan mounted as an album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,After,Li Cheng|Unidentified Artist,"Chinese, 919–967",,LI CHENG|Unidentified Artist,Chinese,0919,0967,,1368,1911,Fan mounted as an album leaf; ink and color on silk,8 7/8 x 9 3/4 in. (22.5 x 24.8 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.118,false,true,51400,Asian Art,Fan mounted as an album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Yan Wengui|Unidentified Artist,"Chinese, 970–1030",,YAN WENGUI|Unidentified Artist,Chinese,0970,1030,,1368,1911,Fan mounted as an album leaf; ink and color on silk,9 1/4 x 9 3/8 in. (23.5 x 23.8 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.11,false,true,50663,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the style of,Unidentified Artist|Su Shi,"Chinese, 1037–1101",,Unidentified Artist|Su Shi,Chinese,1037,1101,,1368,1911,Handscroll; ink on paper,Image: 11 1/16 in. × 9 ft. 1 7/8 in. (28.1 × 279.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.100,false,true,51387,Asian Art,Fan mounted as an album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Su Shi|Unidentified Artist,"Chinese, 1037–1101",,Su Shi|Unidentified Artist,Chinese,1037,1101,,1368,1911,Fan mounted as an album leaf; ink on silk,8 x 8 3/8 in. (20.3 x 21.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.4,false,true,51418,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the Style of,Zhao Mengfu|Unidentified Artist,"Chinese, 1254–1322",,Zhao Mengfu|Unidentified Artist,Chinese,1254,1322,,1368,1911,Handscroll; ink and color on silk,12 x 40 1/2 in. (30.5 x 102.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.115,false,true,51723,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Copy after,Unidentified Artist|Zhao Mengfu,"Chinese, 1254–1322",,Unidentified Artist|Zhao Mengfu,Chinese,1254,1322,,1368,1644,Handscroll; ink and color on silk,9 x 35 1/4 in. (22.9 x 89.5 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.26,false,true,51479,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Wang Fu,"Chinese, 1362–1416",,Unidentified Artist|Wang Fu,Chinese,1362,1416,,1368,1911,Hanging scroll; ink on paper,Image: 43 7/8 x 14 5/8 in. (111.4 x 37.1 cm) Overall with mounting: 78 1/2 x 20 7/8 in. (199.4 x 53 cm) Overall with knobs: 78 1/2 x 25 3/4 in. (199.4 x 65.4 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.23,false,true,51291,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,After,Wu Wei|Unidentified Artist,"Chinese, 1459–1508",,Wu Wei|Unidentified Artist,Chinese,1459,1508,,1368,1911,Hanging scroll; ink and color on paper,45 x 17 3/4 in. (114.3 x 45.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.68,false,true,51683,Asian Art,Album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Copy after,Unidentified Artist|Cui Que,"Chinese, 11th century",,Unidentified Artist|Cui Que,Chinese,1000,1099,,1368,1911,Album leaf; ink and color on silk,9 3/4 x 8 3/4 in. (24.8 x 22.2 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.136.2,false,true,51751,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the style of,Unidentified Artist|Li Gonglin,"Chinese, ca. 1041–1106",,Unidentified Artist|Li Gonglin,Chinese,1041,1106,,1368,1911,Handscroll; gold on black paper,10 1/2 x 151 1/4 in. (26.7 x 384.2 cm),"Anonymous Gift, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.193,false,true,51808,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the style of,Unidentified Artist|Gao Kegong,"Chinese (Hui), 1248–1310",,Unidentified Artist|Gao Kegong,Chinese (Hui),1248,1310,,1368,1911,Hanging scroll; ink on silk,47 x 109 1/4 in. (119.4 x 277.5 cm),"Gift of Edgar Worch, in memory of his uncle, Adolphe Worch, of Paris, 1954–1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.12,false,true,51643,Asian Art,Handscroll,壺天聚樂圖|Merry Gatherings in the Magic Jar,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the Style of,Gong Kai|Unidentified Artist,"Chinese, 1222–after 1304",,Gong Kai|Unidentified Artist,Chinese,1222,1310,,1368,1911,Handscroll; ink on paper,Image: 11 1/2 in. × 12 ft. 3 3/8 in. (29.2 × 374.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.56,false,true,51674,Asian Art,Hanging scroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Dong Yuan,"Chinese, active 930s–960s",,Unidentified Artist|DONG YUAN,Chinese,0930,0960,,1368,1911,Hanging scroll; ink on silk,63 1/8 x 19 15/16 in. (160.3 x 50.6 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.51,false,true,51670,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Copy after,Unidentified Artist|Lin Jun,"Chinese, active ca. 1174–90",,Unidentified Artist|Lin Jun,Chinese,1164,1200,,1368,1911,Handscroll; ink and color on silk,11 3/4 x 83 5/8 in. (29.8 x 212.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.14,false,true,51645,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Fang Congyi,"Chinese, ca. 1301–after 1378",,Unidentified Artist|Fang Congyi,Chinese,1301,1399,,1368,1911,Handscroll; ink and color on silk,Overall: 10 1/2 x 106 in. (26.7 x 269.2 cm) Overall with mounting: 11 1/8 in. (28.3 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.35,false,true,51658,Asian Art,Fan mounted as an album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the style of,Unidentified Artist|Ma Yuan,"Chinese, active ca. 1190–1225",,Unidentified Artist|MA YUAN,Chinese,1190,1225,,1368,1911,Fan mounted as an album leaf; ink and white pigment on silk,9 5/8 x 10 in. (24.4 x 25.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.138,false,true,51737,Asian Art,Fan mounted as an album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Copy after (?),Unidentified Artist|Xia Gui,"Chinese, active ca. 1195–1230",,Unidentified Artist|Xia Gui,Chinese,1195,1230,,1368,1911,Fan mounted as an album leaf; ink on silk,8 7/16 x 9 7/8 in. (21.4 x 25.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.96,false,true,51708,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Copy after,Unidentified Artist|Qian Xuan,"Chinese, ca. 1235–before 1307",,Unidentified Artist|QIAN XUAN,Chinese,1235,1307,,1368,1911,Handscroll; ink and color on silk,13 3/8 x 22 1/2 in. (34.0 x 57.2 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.82,false,true,51695,Asian Art,Fan mounted as an album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,After,Unidentified Artist|Emperor Huizong,"Chinese, 1082–1135; r. 1100–25",,Unidentified Artist|Huizong Emperor,Chinese,1082,1082,,1368,1911,Fan mounted as an album leaf; ink and color on silk,8 5/16 x 9 1/2 in. (21.1 x 24.1 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.144,false,true,51743,Asian Art,Fan mounted as an album leaf,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,Copy after (?),Unidentified Artist|Liang Kai,"Chinese, active early 13th century",,Unidentified Artist|Liang Kai,Chinese,1200,1225,,1368,1911,Album leaf; ink and color on silk,9 1/4 x 9 1/3 in. (23.5 x 23.7 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.135.1,false,true,51749,Asian Art,Handscroll,,China,Yuan (1271–1368) or Ming (1368–1644) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Zhao Yong,"Chinese, 1289–after 1360",Chinese,Unidentified Artist|Zhao Yong,Chinese,1289,1360,,1500,1599,Handscroll; ink and color on silk,10 1/2 x 68 in. (26.7 x 172.7 cm),"Gift of A. W. Bahr, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.99,false,true,40195,Asian Art,Fan mounted as an album leaf,明 佚名 (舊傳)吳炳 枇杷 扇|Bird on a Loquat Tree,China,probably mid- to late Ming dynasty (1368–1644),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Wu Bing,"Chinese, active 1190–1194",,Unidentified Artist|Wu Bing,Chinese,1190,1194,,1368,1644,Fan mounted as an album leaf; ink and color on silk,10 1/2 x 10 3/4 in. (26.7 x 27.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.522,false,true,51570,Asian Art,Hanging scroll,,China,Song (960–1279) or Yuan (1271–1368) dynasty (?),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Wu Daozi,"Chinese, 689–after 755",,Unidentified Artist|Wu Daozi,Chinese,0689,0760,,960,1368,"Hanging scroll; ink, color, and gold on silk",39 1/2 x 19 5/8 in. (100.3 x 49.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.3.1,false,true,39938,Asian Art,Handscroll,北宋/金 傳黃宗道 舊傳李贊華 獵鹿圖 卷|Stag Hunt,China,Northern Song (960–1127) or Jin (1115–1234) dynasty,,,,Artist|Artist,Attributed to|Formerly Attributed to,Huang Zongdao|Li Zanhua,"Chinese, active ca. 1120|Chinese, 899–936",,HUANG ZONGDAO|LI ZANHUA,Chinese,1120 |0899,1120 |0936,,960,1234,Handscroll; ink and color on paper,Image: 9 11/16 × 31 1/16 in. (24.6 × 78.9 cm) Overall with mounting: 10 1/8 in. × 22 ft. 7 1/4 in. (25.7 × 689 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.43a,false,true,36053,Asian Art,Fan mounted as an album leaf,,China,late Yuan (1271–1368)–early Ming (1368–1644) dynasty,,,,Artist|Artist,In the style of,Unidentified Artist|Wang Shen,"Chinese, active 1060–1080",,Unidentified Artist|Wang Shen,Chinese,1060,1080,,1271,1644,Fan mounted as an album leaf; ink and color on silk,9 3/4 x 10 in. (24.8 x 25.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.119,false,true,51726,Asian Art,Handscroll,,China,Five Dynasties (907–960)–early Song (906–1279) dynasty,,,,Artist|Artist,Formerly attributed to,Unidentified Artist|Xiao Zhao,"Chinese, active ca. 1150","Chinese, 10th century (?)",Unidentified Artist|Xiao Zhao,Chinese,1140,1160,,906,1279,Handscroll; ink and color on silk,12 3/4 x 51 3/4 in. (32.4 x 131.4 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.1,false,true,36011,Asian Art,Handscroll,明 佚名 (舊傳)夏珪 《長江萬里圖》 (前半卷)|River Landscape After Xia Gui,China,Ming dynasty (1368–1644),,,,Artist|Artist,Formerly Attributed to,Xia Gui|Unidentified Artist,"Chinese, active ca. 1195–1230",,Xia Gui|Unidentified Artist,Chinese,1195,1230,15th century,1400,1499,Handscroll; ink and color on silk,Image: 23 3/8 in. × 17 ft. 1 3/8 in. (59.4 × 521.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.18,false,true,45674,Asian Art,Handscroll,明? 佚名 (舊傳)夏珪 長江萬里圖 (後半卷)|River Landscape after Xia Gui,China,Ming dynasty (1368–1644),,,,Artist|Artist,Formerly Attributed to,Xia Gui|Unidentified Artist,"Chinese, active ca. 1195–1230",,Xia Gui|Unidentified Artist,Chinese,1195,1230,15th century,1400,1499,Handscroll; ink and color on silk,Image: 23 3/8 in. × 16 ft. (59.4 × 487.7 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.3,false,true,36049,Asian Art,Handscroll,清 佚名 倣郭熙 溪山無盡圖 卷|Streams and Mountains Without End,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Guo Xi,"Chinese, ca. 1000–ca. 1090",,Unidentified Artist|Guo Xi,Chinese,1000,1090,17th century,1600,1699,Handscroll; ink and color on silk,11 3/16 x 76 3/8 in. (28.4 x 194 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.3,false,true,36146,Asian Art,Hanging scroll,清 汪恭 摹趙孟頫肖像 軸|Copy of a Portrait of Zhao Mengfu,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly attributed to,Wang Gong|Unidentified Artist,"Chinese, active early 19th century",,Wang Gong|Unidentified Artist,Chinese,1800,1899,19th century,1800,1899,Hanging scroll; ink and color on silk,25 1/8 x 12 1/8 in. (63.8 x 30.8 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.28,false,true,51305,Asian Art,Handscroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,After,Dong Yuan|Unidentified Artist,"Chinese, active 930s–960s",,DONG YUAN|Unidentified Artist,Chinese,0930,0960,18th–19th century,1700,1899,Handscroll; ink on silk,15 3/8 in. × 23 ft. 6 1/2 in. (39.1 × 717.6 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.423,false,true,61941,Asian Art,Folding screen,伝近衛信尋書・伝長谷川宗也絵 葛下絵色紙貼付『和漢朗詠集』屏風|Anthology of Japanese and Chinese Poems (Wakan rōeishū) with Underpainting of Arrowroot Vines,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Underpainting attributed to,Konoe Nobuhiro|Hasegawa Sōya,"Japanese, 1599–1649|Japanese, born 1590",,Konoe Nobuhiro|Hasegawa Sōya,Japanese|Japanese,1599 |1590,1649 |1690,early 17th century,1600,1633,Six-panel folding screen; ink and color on gilt paper,Image: 65 3/4 x 148 in. (167 x 375.9 cm),"Purchase, several members of The Chairman's Council Gifts, 2001",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/61941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.72, .73",false,true,45234,Asian Art,Screen,"花鳥山水人物図屏風|Calligraphy with Landscapes, Figures, Flowers, and Birds",Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Nagasawa Rosetsu|Shishin Sōgin,"Japanese, 1754–1799|Japanese, 1726–1786",,Nagasawa Rosetsu|Shishin Sōgin,Japanese|Japanese,1754 |1726,1799 |1786,"second month, 1785",1785,1785,Sheets with calligraphy and painting attached to a pair of six-panel folding screens; ink on paper,Image (each screen): 62 3/8 in. x 11 ft. 7 5/8 in. (158.4 x 354.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.827,false,true,78717,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Yamaguchi Soken|Minagawa Kien,"Japanese, 1759–1818|Japanese, 1734–1807",,Yamaguchi Soken|Minagawa Kien,Japanese|Japanese,1759 |1734,1818 |1807,1804,1804,1804,Woodblock printed book; ink on paper,10 7/16 × 7 5/16 in. (26.5 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.826a–c,false,true,78716,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Yamaguchi Soken|Minagawa Kien,"Japanese, 1759–1818|Japanese, 1734–1807",,Yamaguchi Soken|Minagawa Kien,Japanese|Japanese,1759 |1734,1818 |1807,1800,1800,1800,Set of three woodblock printed books; ink on paper,each: 10 7/16 × 7 3/16 in. (26.5 × 18.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.891,false,true,78781,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Nagayama Koin (Hirotora)|Shokusanjin (Ōta Nanpo),"Japanese, 1765–1849|Japanese, 1749–1823",", and author",Nagayama Koin (Hirotora),Japanese|Japanese,1765 |1749,1849 |1823,1824,1824,1824,Woodblock printed book; ink and color on paper,10 1/16 × 7 3/16 in. (25.5 × 18.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.787a–c,false,true,78689,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Calligrapher|Calligrapher,After|After,Hasegawa Myōtei|Sankinshi,"Japanese, active late 17th century|Japanese",,Hasegawa Myōtei|Sankinshi,Japanese|Japanese,1667,1699,1838,1838,1838,Set of three woodblock printed books; ink on paper,each: 10 7/16 × 7 3/8 in. (26.5 × 18.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.794,false,true,78810,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,,Rankō|Watanabe Nangaku|Tetsuzan Sōdon,"Japanese|Japanese, 1763–1813|Japanese, 1532–1617",,Rankō|Watanabe Nangaku|Tetsuzan Sōdon,Japanese|Japanese|Japanese,1763 |1532,1813 |1617,1806,1806,1806,"Woodblock printed book (orihon, accordion-style); ink and color on paper with metallic pigments (?)",9 13/16 × 7 1/16 in. (25 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.743,false,true,78645,Asian Art,Illustrated book,『花月帖』|Album of Flowers and the Moon (Kagetsu jō),Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,,Mori Tetsuzan|Maruyama Ōshin|Kawamura Kihō,"Japanese, 1775–1841|Japanese, 1790–1838|Japanese, 1778–1852",,Mori Tetsuzan|Maruyama Ōshin|Kawamura Kihō,Japanese|Japanese|Japanese,1775 |1790 |1778,1841 |1838 |1852,1836,1836,1836,"Woodblock-printed book (orihon, accordion-style); ink and color on paper",9 1/16 × 6 5/16 in. (23 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.876,false,true,78766,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist,,Genki (Komai Ki)|Matsumura Goshun|Watanabe Nangaku|Miguma Shiko|Fusetsu Yujo,"Japanese, 1747–1797|Japanese, 1752–1811|Japanese, 1763–1813|Japanese|Japanese",,Genki|Matsumura Goshun|Watanabe Nangaku|Miguma Shiko|Fusetsu Yujo,Japanese|Japanese|Japanese|Japanese|Japanese,1747 |1752 |1763,1797 |1811 |1813,1793,1793,1793,"Woodblock printed book (orihon, accordion-style); ink and color on paper",11 × 8 3/8 in. (28 × 21.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.781a, b",false,true,78683,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist,,Yosa Buson|Sō Shiseki|Nankei|Rantei|Ōishi Matora,"Japanese, 1716–1783|Japanese, 1715–1786|Japanese|Japanese,|Japanese, 1793–1833",et al,Yosa Buson|Sō Shiseki|Nankei|Rantei|Ōishi Matora,Japanese|Japanese|Japanese|Japanese|Japanese,1716 |1715 |1793,1783 |1786 |1833,"1812, 1814",1812,1814,Set of two woodblock printed books; ink and color on paper,each: 11 × 7 1/2 in. (28 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.815,false,true,78705,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist,,Tsukioka Settei|Ōnishi Chinnen|Teisai Hokuba|Ikeda Koson|Nankō,"Japanese, 1710–1786|1792–1851|Japanese, 1771–1844|Japanese, 1803–1868|Japanese",,Tsukioka Settei|Ōnishi Chinnen|Teisai Hokuba|Ikeda Koson|Nankō,Japanese|Japanese|Japanese|Japanese,1710 |1792 |1771 |1803,1786 |1851 |1844 |1868,1830,1830,1830,"Woodblock printed book (orihon, accordion-style); ink and color on paper",10 1/16 × 6 11/16 in. (25.5 × 17 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.843a, b",false,true,78733,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist,,Nukina Kaioku|Shiokawa Bunrin|Kawabata Gyokushō|Musō Soseki,"Japanese, 1778–1863|Japanese, 1808–1877|Japanese, 1842–1913|Japanese, 1275–1351",", et al",Nukina Kaioku|Shiokawa Bunrin|Kawabata Gyokushō|Musō Soseki,Japanese|Japanese|Japanese|Japanese,1778 |1808 |1842 |1275,1863 |1877 |1913 |1351,postscript dated 1861,1861,1861,Set of two woodblock printed books; ink and color on paper,each: 10 3/16 × 6 11/16 in. (25.8 × 17 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.873,false,true,78763,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist,,Matsumura Goshun|Matsumura Keibun|Suzuki Harushige|Aikawa Minwa|Nagasawa Roshu|Yamaguchi Soken|Okamoto Toyohiko|Oku Bunmei|Ki Chikudō|Gan Tai|Sō Geppō|Shibata Gitō|Fukuchi Hakuei|Kawamura Kihō|Shiba Kōkan|Yoshimura Kōkei|Hatta Koshū|Watanabe Nangaku|Kinoshita Ōju|Maruyama Ōzui|Hara Zaimei,"Japanese, 1752–1811|Japanese, 1779–1843|1747–1818|Japanese, active 1806–1821|Japanese, 1767–1847|Japanese, 1759–1818|1773–1845|Japanese|Japanese, died 1825|Japanese, 1760–1839|Japanese|Japanese, active early 19th century|Japanese, 1778–1852|Japanese, 1747–1818|Japanese|Japanese|Japanese, 1763–1813|Japanese|Japanese|Japanese",", et al",Matsumura Goshun|Matsumura Keibun|Suzuki Harushige|Aikawa Minwa|Nagasawa Roshu|Yamaguchi Soken|Okamoto Toyohiko|Oku Bunmei|Ki Chikudō|Gan Tai|Sō Geppō|Shibata Gitō|Fukuchi Hakuei|Kawamura Kihō|Shiba Kōkan|Yoshimura Kōkei|Hatta Koshū|Watanabe Nangaku|Kinoshita Ōju|Maruyama Ōzui|Hara Zaimei,Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese,1752 |1779 |1747 |1806 |1767 |1759 |1773 |1760 |1800 |1778 |1747 |1763,1811 |1843 |1818 |1821 |1847 |1818 |1845 |1825 |1839 |1833 |1852 |1818 |1813,1814,1814,1814,Woodblock printed book; ink and color on paper,10 3/8 × 7 1/2 in. (26.3 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.793,false,true,78809,Asian Art,Illustrated book,『男踏歌』|Men’s Stomping Dances (Otoko dōka),Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist,,Kitagawa Utamaro|Rekisentei Eiri|Katsushika Hokusai|Chōbunsai Eishi,"Japanese, 1753?–1806|Japanese, active ca. 1789–1801|Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)|Japanese, 1756–1829",,Kitagawa Utamaro|Rekisentei Eiri|Katsushika Hokusai|Chōbunsai Eishi,Japanese|Japanese|Japanese|Japanese,1753 |1789 |1760 |1756,1806 |1801 |1849 |1829,1798,1798,1798,Woodblock printed book; ink and color on paper,10 1/16 × 7 1/2 in. (25.5 × 19 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.870,false,true,78760,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist,,Kuwagata Keisai|Sakai Hōitsu|Tani Bunchō|Katsushika Hokusai|Ōnishi Chinnen,"Japanese, 1764–1824|Japanese, 1761–1828|Japanese, 1763–1840|Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)|1792–1851",", et al",Kuwagata Keisai|Sakai Hōitsu|Tani Bunchō|Katsushika Hokusai|Ōnishi Chinnen,Japanese|Japanese|Japanese|Japanese|Japanese,1764 |1761 |1763 |1760 |1792,1824 |1828 |1840 |1849 |1851,ca. 1822–34,1822,1834,Woodblock printed book; ink on paper,7 3/8 × 5 1/8 in. (18.7 × 13 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.744,false,true,78646,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist|Artist|Artist,,Mori Tetsuzan|Maruyama Ōshin|Kawamura Kihō,"Japanese, 1775–1841|Japanese, 1790–1838|Japanese, 1778–1852",and others,Mori Tetsuzan|Maruyama Ōshin|Kawamura Kihō,Japanese|Japanese|Japanese,1775 |1790 |1778,1841 |1838 |1852,late 19th century reprint of original edition of 1836,1875,1899,"Woodblock printed book (orihon, accordion-style); ink and color on paper",10 1/8 × 6 1/2 in. (25.7 × 16.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2832,false,true,53719,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist|Artist,or,Yokogawa Horitake|Utagawa Kunisada|Utagawa Toyokuni II,"Japanese, 1786–1865|Japanese, 1777–1835",,Yokogawa Horitake|Utagawa Kunisada|Utagawa Toyokuni II,Japanese|Japanese,1786 |1777,1865 |1835,1786–1864,1786,1864,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1856,false,true,45029,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,after,Senseki|Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Senseki|Katsushika Hokusai,Japanese|Japanese,1760,1849,ca. 1816–20,1816,1820,Polychrome woodblock print; ink and color on paper,H. 14 3/16 in. (36 cm); W. 9 5/8 in. (24.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3116,false,true,37370,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Tōshūsai Sharaku|Ueda Shikibuchi,"Japanese, active 1794–95|Japanese, 1819–1879",,Tōshūsai Sharaku|Ueda Shikibuchi,Japanese|Japanese,1794 |1819,1795 |1879,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",UMPN BV,"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37370,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1855,false,true,45028,Asian Art,Print,古今書画鑑 熊谷蓮生坊真跡|Bird-and-Flower Paintings,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,after,Katsushika Hokusai|Kumagai Naozane,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)|Japanese, 1141–1208",,Katsushika Hokusai|Kumagai Naozane,Japanese|Japanese,1760 |1141,1849 |1208,ca. 1816–20,1816,1820,Polychrome woodblock print; ink and color on paper,H. 14 7/16 in. (36.7 cm); W. 10 1/16 in. (25.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP206,false,true,36683,Asian Art,Print,小倉擬百人一首|Album of Eighty-eight Prints from the series Ogura Imitations of One Hundred Poems by One Hundred Poets (Ogura nazorae hyakunin isshu),Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,,Utagawa Kuniyoshi|Utagawa Hiroshige|Utagawa Kunisada,"Japanese, 1797–1861|Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)|Japanese, 1786–1865",,"Utagawa Kuniyoshi|Utagawa, Hiroshige|Utagawa Kunisada",Japanese|Japanese|Japanese,1797 |1797 |1786,1861 |1858 |1865,about 1845–48,1845,1848,Album of 88 polychrome woodblock prints; ink and color on paper,14 × 9 1/4 × 1 in. (35.6 × 23.5 × 2.5 cm),"Gift of Mary L. Cassilly, 1894",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.129a–e,false,true,76559,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist,,Jukakudō Masakuni|Jugyōdō Umekuni|Juyōdō Minekuni|Jushōdō Fujikuni|Hōgadō Kishikuni,"Japanese, active 1820s|Japanese, active 1820s|Japanese, active 1820s|Japanese, active 1820s|Japanese, active 1820s",,Jukakudō Masakuni|Jugyōdō Umekuni|Juyōdō Minekuni|Jushōdō Fujikuni|Hōgadō Kishikuni,Japanese|Japanese|Japanese|Japanese|Japanese,1820 |1820 |1820 |1820 |1820,1829 |1829 |1829 |1829 |1829,1824,1824,1824,Pentaptych of polychrome woodblock prints,Each sheet (ôban tat-e pentaptych): 14 3/4 x 10 5/8 in. (37.5 x 27 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.457.2,false,true,53711,Asian Art,Prints,,Japan,Meiji period (1868–1912),,,,Artist|Artist|Artist|Artist|Artist,"prints 13,17,18,20,26,29 by|print 2 by|print 28 by|prints 25,27,31,32 by",Yōshū (Hashimoto) Chikanobu|Toyohara Kunichika|Morikawa Chikashige|Hiroaki|Kunimasa,"Japanese, 1838–1912|Japanese, 1835–1900|Japanese, second half of 19th century|Japanese, 1871–1945|Japanese",,Yōshū (Hashimoto) Chikanobu|Toyohara Kunichika|Morikawa Chikashige|Hiroaki|Kunimasa,Japanese|Japanese|Japanese,1838 |1835 |1850 |1871,1912 |1900 |1899 |1945,1883–86,1883,1886,Album of thirty-two triptychs of polychrome woodblock prints; ink and color on paper,Each H. 14 in. (35.6 cm); W. 9 1/4 in. (23.5 cm),"Gift of Eliot C. Nolen, 1999",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.3,false,true,663886,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,,Mokuan Shōtō|Unidentified Artist,"Chinese, 1611–1684","Japanese, active late 17th century",Mokuan Shōtō|Unidentified Artist,Chinese,1611,1684,1676,1676,1676,Hanging scroll; ink and color on paper,Image: 47 in. × 22 3/4 in. (119.4 × 57.8 cm) Overall with mounting: 76 1/4 × 27 15/16 in. (193.7 × 71 cm) Overall with knobs: 76 1/4 × 30 1/4 in. (193.7 × 76.8 cm),"Purchase, Brooke Russell Astor Bequest, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/663886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.131,false,true,45790,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Katsukawa Shunshō|Tegara no Okamochi,"Japanese, 1726–1792|1734–1812",,Katsukawa Shunshō|Tegara no Okamochi,Japanese|Japanese,1726 |1734,1792 |1812,1798,1798,1798,Hanging scroll; ink and color on paper,33 3/4 x 11 5/16 in. (85.7 x 28.8 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.115,false,true,73192,Asian Art,Hanging scroll,布袋図 拄杖擊破三千界。彌勒撫掌笑呵呵,明月清風無。」|Hotei,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Kano Takanobu|Tetsuzan Sōdon,"Japanese, 1571–1618|Japanese, 1532–1617",,Kano Takanobu|Tetsuzan Sōdon,Japanese|Japanese,1571 |1532,1618 |1617,dated 1616,1616,1616,Hanging scroll; ink and color on paper,Image: 27 1/2 x 15 in. (69.9 x 38.1 cm) Overall with mounting: 59 1/2 x 18 3/4 in. (151.1 x 47.6 cm) Overall with rollers: 59 1/2 x 20 1/2 in. (151.1 x 52.1 cm),"Funds from various donors, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.407.1,false,true,45335,Asian Art,Hanging scroll,松花堂昭乗書・伝俵屋宗達下絵 立葵下絵和歌色紙 藤原興風|Poem by Onakatomi Yoshinobu with Underpainting of Hollyhocks,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Calligraphy by|Underpainting attributed to,Shōkadō Shōjō|Tawaraya Sōtatsu,"Japanese, 1584?–1639|Japanese, died ca. 1640",,Shōkadō Shōjō|Tawaraya Sōtatsu,Japanese|Japanese,1584 |1540,1639 |1640,early 17th century,1600,1633,"Poem card (shikishi) mounted as a hanging scroll; ink, gold, and silver on colored paper",Image: 7 15/16 x 6 15/16 in. (20.2 x 17.6 cm) Overall with mounting: 53 1/4 x 20 5/8 in. (135.3 x 52.4 cm) Overall with knobs: 53 1/4 x 23 in. (135.3 x 58.4 cm),"Purchase, Mrs. Jackson Burke Gift, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.133,false,true,45183,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist,,Tani Bunchō|Watanabe Kazan|Sakai Hōitsu|sixty-six others,"Japanese, 1763–1840|Japanese, 1793–1841|Japanese, 1761–1828|Japanese",,Tani Bunchō|Watanabe Kazan|Sakai Hōitsu|sixty-six others,Japanese|Japanese|Japanese|Japanese,1763 |1793 |1761,1840 |1841 |1828,1820,1820,1820,Hanging scroll; ink and color on paper,Image: 31 3/8 x 23 3/8 in. (79.7 x 59.4 cm) Overall with mounting: 64 5/8 x 25 3/4 in. (164.1 x 65.4 cm) Overall with knobs: 64 5/8 x 28 1/8 in. (164.1 x 71.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.39,false,true,39659,Asian Art,Hanging scroll,『東坡笠屐図』|Su Shi (Dongpo) in a Bamboo Hat and Clogs,Japan,Muromachi period (1392–1573),,,,Artist|Artist|Artist|Artist|Artist,Inscribed by|Inscribed by|Inscribed by|Inscribed by|Inscribed by,Kyūen Ryūchin|Kōshi Ehō|Nankō Sōgen|Zuigan Ryūsei|Chikkō Zengo,"Japanese, died 1498|Japanese, 1414–ca.1465|Japanese, 1378–1463|Japanese, 1384–1460|Japanese, died after 1464",,Kyūen Ryūchin|Kōshi Ehō|Nankō Sōgen|Zuigan Ryūsei|Chikkō Zengo,Japanese|Japanese|Japanese|Japanese|Japanese,1414 |1378 |1384,1498 |1465 |1463 |1460 |1464,before 1460,1392,1459,Hanging scroll; ink on paper,Image: 42 3/4 x 13 1/8 in. (108.6 x 33.3 cm) Overall with mounting: 74 1/2 x 17 5/8 in. (189.2 x 44.8 cm) Overall with rollers: 74 1/2 x 17 5/8 x 19 3/4 in. (189.2 x 44.8 x 50.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB50,false,true,57650,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,3 unknown artists:,Aoigaoka Keisei|Utagawa Kuniyoshi|Keisai Eisen,"Japanese, active 1820s–1830s|Japanese, 1797–1861|Japanese, 1790–1848",", Keishin, and Yanagawa",Aoigaoka Keisei|Utagawa Kuniyoshi|Keisai Eisen,Japanese|Japanese|Japanese,1810 |1797 |1790,1840 |1861 |1848,,1615,1868,Ink on paper,8 5/8 × 6 × 3/4 in. (21.9 × 15.2 × 1.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.116,false,true,44899,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,,Shōkadō Shōjō|Maruyama Ōkyo,"Japanese, 1584?–1639|Japanese, 1733–1795",,Shōkadō Shōjō|Maruyama Ōkyo,Japanese|Japanese,1584 |1733,1639 |1795,17th century,1600,1699,Eight-panel folding screen; ink and gold,43 x 129 in. (109.2 x 327.7 cm),"Gift of Mrs. H. F. Stone and Mrs. Leon Durand Bonnet, 1956",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.62a–f,false,true,45701,Asian Art,Screen,狩野探幽・狩野尚信・清原雪信 花鳥図屏風|Birds and Flowers,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,,Kano Tan'yū|Kiyohara Yukinobu|Kano Naonobu,"Japanese, 1602–1674|Japanese, 1643–1682|Japanese, 1607–1650",,Kano Tan'yū|Kiyohara Yukinobu|Kano Naonobu,Japanese|Japanese|Japanese,1602 |1643 |1607,1674 |1682 |1650,17th century,1600,1699,Six-panel folding screen; ink and color on silk,70 in. x 12 ft. 11 1/2 in. (177.8 x 395 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.213.1–.30,false,true,74463,Asian Art,Album,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist,.16: After|.2: After|.3: After|.4: After|.6: After|.7: After|.8: After|.9: After|.10: After|.14: After|.15: After|.17: After|.18: After|.19: After|.20: After|.21: After|.22: After|.23: After|.24: After|.25: After|.26: After|.27: After|.28: After|.29: After|.30: After|.1. After|.13 After,Yan Ciping|Kano Tsunenobu|Yintuoluo|Wang Lipen|Po Citing|Puming|Sheng Mou|Danzhirui|Jin Dashou|Wang Yuan|Muqi|Guo Xi|Zhang Yüehu|Luochuang|Ren Renfa|Luo Xinzhong|Wen Tong|Xia Yong|Fan Anren|Cinshan|Kongshan|Su Xianzu|Li Anzhong|Xuejian|Wang Moji|Daisong|Zhao Mengfu|Chen Rong,"Chibese, active 12th century|Japanese, 1636–1713|Chinese, active 13th century|Chinese, born late Yuan, active early Ming dynasty|Chinese, died 1337(?); active first half of the 14th century|Chinese, active 14th century|Chinese, active ca. 1310–1360|active early 14th century|active 13th century|Chinese, ca. 1280–after 1349|Chinese, ca. 1210–after 1269|Chinese, ca. 1000–ca. 1090|active 13th century|active 13th century|Chinese, 1255–1328|active 13th century|Chinese, 1019–1079|Chinese, active mid-14th century|active 13th century|active 12th century|active 14th century|699–759|Chinese, 727–779|Chinese, 1254–1322|active 1235–62",,Yan Ciping|Kano Tsunenobu|Yintuoluo|Wang Lipen|Po Citing|Puming|Sheng Mou|Danzhirui|Jin Dashou|Wang Yuan|Muqi|Guo Xi|Zhang Yüehu|Luochuang|Ren Renfa|Luo Xinzhong|Wen Tong|XIA YONG|Fan Anren|Cinshan|Kongshan|Su Xianzu|Li Anzhong|Xuejian|Wang Moji|Daisong|Zhao Mengfu|Chen Rong,Chinese|Japanese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese|Chinese,1100 |1636 |0013 |1237 |1300 |1310 |0014 |0013 |1270 |1210 |1000 |0013 |0013 |1255 |0013 |1019 |1336 |0013 |0012 |0014 |0699 |0727 |1254 |1235,1199 |1713 |0013 |1337 |1399 |1360 |0014 |0013 |1359 |1269 |1090 |0013 |0013 |1328 |0013 |1079 |1370 |0013 |0012 |0014 |0759 |0779 |1322 |1262,17th century,1636,1699,Album of thirty paintings; ink and color on silk,11 7/16 x 16 3/4 in. (29 x 42.5 cm),"Gift of Mr. and Mrs. Harry Rubin, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.97,false,true,40462,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Inscribed by,Tangai Jōgi|Itō Jakuchū,"Japanese, 1693–1764|Japanese, 1716–1800",,Tangai Jōgi|Itō Jakuchū,Japanese|Japanese,1693 |1716,1764 |1800,18th century,1716,1764,Hanging scroll; ink on paper,Image: 49 1/2 x 18 7/8 in. (125.7 x 47.9 cm) Overall with mounting: 75 5/8 x 24 1/4 in. (192.1 x 61.6 cm) Overall with knobs: 75 5/8 x 26 1/2 in. (192.1 x 67.3 cm),"Purchase, Lita Annenberg Hazen Charitable Trust Gift, in honor of Cynthia and Leon Polsky, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB94,false,true,45442,Asian Art,Illustrated book,Kyoka Kijin Gazo-shu|Poems on Portraits of the Famous and the Infamous,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,Attributed to,Utagawa Kunisada|Utagawa Hiroshige|Ryūsen,"Japanese, 1786–1865|Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)|Japanese, active mid–19th century",,"Utagawa Kunisada|Utagawa, Hiroshige|Ryūsen",Japanese|Japanese|Japanese,1786 |1797 |1834,1865 |1858 |1866,19th century,1700,1868,Polychrome Woodblock printed book; gold lacquer on red lacquer ground,8 1/4 × 5 3/4 × 1/2 in. (21 × 14.6 × 1.3 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2342,false,true,54127,Asian Art,Woodblock print,,Japan,,,,,Artist|Calligrapher,,Totoya Hokkei|Shibayamadō,"Japanese, 1780–1850|Japanese",,Totoya Hokkei|Shibayamadō,Japanese|Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2260,false,true,54030,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,,Yuyu Hanko|Teisai Hokuba,"Japanese|Japanese, 1771–1844",,Yuyu Hanko|Teisai Hokuba,Japanese|Japanese,1771,1844,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 7/16 x 7 3/8 in. (13.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2261,false,true,54031,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,,Yuyu Hanko|Teisai Hokuba,"Japanese|Japanese, 1771–1844",,Yuyu Hanko|Teisai Hokuba,Japanese|Japanese,1771,1844,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP205,false,true,36682,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,Nineteen prints by|Two prints by|Three prints by,Kikugawa Eizan|Utagawa Kunisada|Utagawa Kunimaru,"Japanese, 1787–1867|Japanese, 1786–1865|Japanese, 1793–1829",,Kikugawa Eizan|Utagawa Kunisada|Kunimaru,Japanese|Japanese|Japanese,1787 |1786 |1793,1867 |1865 |1829,19th century,1800,1868,Album of 24 polychrome woodblock prints; ink and color on paper,14 5/8 × 9 3/4 × 1 1/4 in. (37.1 × 24.8 × 3.2 cm),"Gift of Mary L. Cassilly, 1894",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP211,false,true,36688,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,,Utagawa Kuniyoshi|Utagawa Kunisada|Utagawa Hiroshige,"Japanese, 1797–1861|Japanese, 1786–1865|Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa Kuniyoshi|Utagawa Kunisada|Utagawa, Hiroshige",Japanese|Japanese|Japanese,1797 |1786 |1797,1861 |1865 |1858,19th century,1800,1865,Album of 98 polychrome woodblock prints; ink and color on paper,14 1/2 × 9 1/2 × 1 3/4 in. (36.8 × 24.1 × 4.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP208,false,true,36685,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist,Twenty-eight prints by|Twenty-six prints by|Three prints by|Three prints by|Three prints by,Utagawa Kuniyoshi|Utagawa Kunisada|Utagawa Kunimaro|Utagawa Hiroshige|Utagawa Kuniteru,"Japanese, 1797–1861|Japanese, 1786–1865|Japanese, active ca. 1840–70|Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)|Japanese, 1830–1874",,"Utagawa Kuniyoshi|Utagawa Kunisada|Utagawa, Kunimaro|Utagawa, Hiroshige|Utagawa Kuniteru",Japanese|Japanese|Japanese|Japanese|Japanese,1797 |1786 |1840 |1797 |1830,1861 |1865 |1870 |1858 |1874,19th century,1800,1868,Album of 58 polychrome woodblock prints; ink and color on paper,14 × 10 × 1 1/2 in. (35.6 × 25.4 × 3.8 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1115,false,true,55037,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist,,Utagawa Kuniyoshi|Utagawa Kunisada|Utagawa Yoshikazu|Utagawa Yoshitsuna|Utagawa Yoshitora|Utagawa Yoshitsuru|Utagawa Yoshihide|Utagawa (Gountei) Sadahide|Utagawa Yoshitsuya,"Japanese, 1797–1861|Japanese, 1786–1865|Japanese, active ca. 1850–1870|Japanese, active ca. 1850–1860|Japanese, active ca. 1850–80|Japanese, active ca. 1840–1850|Japanese, 1832–1902|Japanese, 1807–1878/79|Japanese, 1822–1866",,"Utagawa Kuniyoshi|Utagawa Kunisada|Utagawa Yoshikazu|Utagawa Yoshitsuna|Utagawa Yoshitora|Utagawa, Yoshitsuru|Utagawa, Yoshihide|Utagawa (Goutei) Sadahide|Utagawa Yoshitsuya",Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese|Japanese,1797 |1786 |1845 |1850 |1845 |1840 |1832 |1807 |1822,1861 |1865 |1870 |1860 |1880 |1850 |1902 |1879 |1866,19th century,1800,1899,Album of 15 triptychs of polychrome woodblock prints; ink and color on paper,13 15/16 × 9 7/8 × 3/4 in. (35.4 × 25.1 × 1.9 cm),"Gift of Harold de Raasloff, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.347,false,true,49001,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher|Calligrapher,Painted and inscribed by,Rai San'yō|Rai Kyohei (Shunsō)|Rai Baishi,"1780–1832|Japanese, 1756–1834|Japanese, 1759–1843",,Rai San'yō|Rai Kyohei|Rai Baishi,Japanese|Japanese|Japanese,1780 |1756 |1759,1832 |1834 |1843,19th century,1800,1832,Hanging scroll; ink on paper,18 1/2 x 11 5/16 in. (47 x 28.7 cm),"Purchase, Bequest of John L Cadwaldader, Gift of Mrs. Russell Sage, and Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, by exchange, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.74,false,true,49035,Asian Art,Hanging scroll,"長沢蘆筆・皆川淇園賛 白鶏図|Rooster, Hen and Chicks",Japan,Edo period (1615–1868),,,,Artist|Calligrapher,Attributed to,Nagasawa Rosetsu|Minagawa Kien,"Japanese, 1754–1799|Japanese, 1734–1807",,Nagasawa Rosetsu|Minagawa Kien,Japanese|Japanese,1754 |1734,1799 |1807,late 18th century,1767,1799,Hanging scroll; ink on paper,Image: 49 1/8 x 10 3/4 in. (124.8 x 27.3 cm) Overall with mounting: 81 1/4 x 11 7/8 in. (206.4 x 30.2 cm) Overall with knobs: 81 1/4 x 13 7/8 in. (206.4 x 35.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.72,false,true,62891,Asian Art,Teabowl,,Japan,,,,,Artist,,Widow of Ameya,active early 16th century,,Ameya,Korean,0016,0016,ca. 1550,1540,1560,Clay covered with glaze (Amayaki Raku),H. 3 in. (7.6 cm); Diam. 4 1/2 in. (11.4 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.215.5,false,true,63092,Asian Art,Cake dish,,Japan,Edo period (1615–1868),,,,Artist,,Gempin,"Chinese, died 1771",,Gempin,Chinese,1671,1771,1645,1645,1645,Porcelaneous clay covered with a transparent glaze over blue decoration,H. 2 1/2 in. (6.4 cm); W. 6 3/4 in. (17.1 cm); L. 9 in. (22.9 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.83,false,true,49071,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Hi Kangen,active 18th century,,Hi Kangen,Chinese,0018,0018,dated 1756,1756,1756,Hanging scroll; ink and color on paper,35 5/16 x 13 7/16 in. (89.7 x 34.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.439,false,true,54893,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,in the tradition of,Wu Daozi,"Chinese, 689–after 755",,Wu Daozi,Chinese,0689,0760,late 19th century,1871,1899,Hanging scroll; color on silk,48 1/2 x 23 1/8 in. (123.2 x 58.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.86,false,true,49069,Asian Art,Hanging scroll,西園方済筆 梧桐下錦鶏図|Pheasant beneath Paulownia Tree,Japan,Edo period (1615–1868),,,,Artist,,Saien Hōsai (Xiyua Fangqi),1736?–?1795,,Hō Sai (Fang Qi),Chinese,1736,1795,18th century,1736,1795,Hanging scroll; ink on paper,38 1/2 x 12 3/16 in. (97.8 x 30.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.87,false,true,49070,Asian Art,Hanging scroll,西園方済筆 梅に叭叭鳥図|Mynah Bird on Plum Branch,Japan,Edo period (1615–1868),,,,Artist,,Saien Hōsai (Xiyua Fangqi),1736?–?1795,,Hō Sai (Fang Qi),Chinese,1736,1795,18th century,1736,1795,Hanging scroll; ink on paper,36 3/16 x 10 1/2 in. (91.9 x 26.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.61,false,true,49101,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Zhou Wenju,"Chinese, active 940–975",,ZHOU WENJU,Chinese,0940,0975,19th century,1800,1868,Handscroll; ink and color on paper,10 3/16 in. × 18 ft. 15/16 in. (25.8 × 551 cm),"Fletcher Fund, 1942",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.651,false,true,78572,Asian Art,Illustrated book,新玉帖|Album of the New Year (Aratama jō),Japan,Edo period (1615–1868),,,,Artist|Artist,,Ōnishi Chinnen|Tani Bunchō,"1792–1851|Japanese, 1763–1840",", various artists",Ōnishi Chinnen|Tani Bunchō,Japanese|Japanese,1792 |1763,1851 |1840,"early 19th century, before 1829",1800,1825,"Woodblock printed book (orihon, accordion-style; bound); ink, color, and metallic pigments on paper",9 15/16 × 7 5/16 in. (25.3 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.674,false,true,78595,Asian Art,Illustrated book,艶本 婦慈のゆき|Early to Dawn (Akeyasuki),Japan,Edo period (1615–1868),,,,Artist|Artist,,Ōhara Donshū|Tanaka Nikka,"Japanese, died 1857|Japanese, died 1845",,Ōhara Donshū|Tanaka Nikka,Japanese|Japanese,1757,1857 |1845,ca. 1837,1832,1842,Woodblock printed book; ink and color on paper,9 3/4 × 7 5/16 in. (24.7 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.93,false,true,45440,Asian Art,Inrō,冨士形蒔絵印籠|Inrō in the Shape of Mount Fuji with a Crossing Ferry and Procession,Japan,Edo period (1615–1868),,,,Artist|Artist,After|Maki-e by,Hanabusa Itchō|Kajikawa,"Japanese, 1652–1724|Japanese, 1652–1724",,Hanabusa Itchō|Kajikawa,Japanese|Japanese,1652 |1652,1724 |1724,late 18th–early 19th century,1767,1833,"Two cases; lacquered wood with gold and silver takamaki-e, hiramaki-e, togidashimaki-e, cut-out gold foil on nashiji lacquer ground Netsuke: ivory; Ryūgūjō (The Dragon King's undersea palace) in a clam Ojime: agate bead",4 1/8 x 4 11/16 x 1 1/4 in. (10.5 x 11.9 x 3.2 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB32,false,true,57563,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Katsukawa Shunshō|Kitao Shigemasa,"Japanese, 1726–1792|Japanese, 1739–1820",,Katsukawa Shunshō|Kitao Shigemasa,Japanese|Japanese,1726 |1739,1792 |1820,1776,1776,1776,Ink and color on paper,11 1/8 × 7 3/8 × 3/8 in. (28.3 × 18.7 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB88,false,true,57754,Asian Art,Illustrated book,画本宝能縷|Picture Book of Brocades with Precious Threads (Ehon takara no itosuji),Japan,Edo period (1615–1868),,,,Artist|Artist,,Katsukawa Shunshō|Kitao Shigemasa,"Japanese, 1726–1792|Japanese, 1739–1820","(nos. 1, 3, 6, 8, 11, 12)|(nos. 2, 4, 5, 7, 9, 10)",Katsukawa Shunshō|Kitao Shigemasa,Japanese|Japanese,1726 |1739,1792 |1820,"1786, first month",1786,1786,Polychrome woodblock printed book; ink and color on paper,Overall: 11 5/16 × 8 1/8 in. (28.8 × 20.6 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.822,false,true,78712,Asian Art,Illustrated book,『青楼美人合 姿鏡』|Mirror of Yoshiwara Beauties (Seirō bijin awase sugata kagami),Japan,Edo period (1615–1868),,,,Artist|Artist,,Katsukawa Shunshō|Kitao Shigemasa,"Japanese, 1726–1792|Japanese, 1739–1820",,Katsukawa Shunshō|Kitao Shigemasa,Japanese|Japanese,1726 |1739,1792 |1820,1776,1776,1776,"Woodblock printed book; ink, color, and mica on paper",11 × 7 3/8 in. (28 × 18.8 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB31,false,true,45021,Asian Art,Illustrated book,Seiro Bijin Awase Sugata Kagami|Mirror of the Beautiful Women of the Yoshiwara Brothels,Japan,Edo period (1615–1868),,,,Artist|Artist,,Kitao Shigemasa|Katsukawa Shunshō,"Japanese, 1739–1820|Japanese, 1726–1792",,Kitao Shigemasa|Katsukawa Shunshō,Japanese|Japanese,1739 |1726,1820 |1792,1776,1776,1776,Polychrome woodblock printed book; ink and color on paper,11 × 7 1/4 × 5/8 in. (27.9 × 18.4 × 1.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.334,false,true,73587,Asian Art,Illustrated book,紅毛雜話|Chats on Novelties of Foreign Lands (Kōmōzatsuwa),Japan,Edo period (1615–1868),,,,Artist|Artist,,Shiba Kōkan|Kuwagata Keisai,"Japanese, 1747–1818|Japanese, 1764–1824",,Shiba Kōkan|Kuwagata Keisai,Japanese|Japanese,1747 |1764,1818 |1824,1797,1797,1797,"Five volumes of woodblock printed books bound as one; ink on paper,",Image (a): 8 7/8 x 6 1/4 x 7/8 in. (22.5 x 15.9 x 2.2 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/73587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.757a–d,false,true,78659,Asian Art,Illustrated books,『光琳百圖』|One Hundred Paintings by Kōrin (Kōrin hyakuzu),Japan,Edo period (1615–1868),,,,Artist|Artist,After,Sakai Hōitsu|Ogata Kōrin,"Japanese, 1761–1828|Japanese, 1658–1716",,Sakai Hōitsu|Kōrin,Japanese|Japanese,1761 |1658,1828 |1716,1815,1815,1815,Set of four woodblock printed books; ink on paper,each: 10 1/4 × 7 3/16 in. (26 × 18.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.819,false,true,78709,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Katsukawa Shun'ei|Katsukawa Shunshō,"Japanese, 1762–1819|Japanese, 1726–1792",,Katsukawa Shun'ei|Katsukawa Shunshō,Japanese|Japanese,1762 |1726,1819 |1792,1790,1790,1790,Woodblock printed book; ink and color on paper,8 13/16 × 6 1/4 in. (22.4 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.806,false,true,78696,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist,or,Hasegawa Settan|Tsukioka Settei,"Japanese, 1778–1843|Japanese, 1710–1786",,Hasegawa Settan|Tsukioka Settei,Japanese|Japanese,1778 |1710,1843 |1786,1829,1829,1829,Woodblock printed book; ink on paper,9 × 6 5/16 in. (22.8 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.659,false,true,78580,Asian Art,Illustrated book,南岳文鳳街道雙畫|Nangaku- Bunpō Highway Pictures (Nangaku Bunpō kaidō sōga),Japan,Edo period (1615–1868),,,,Artist|Artist,,Kawamura Bunpō|Watanabe Nangaku,"Japanese, 1779–1821|Japanese, 1763–1813",,Kawamura Bunpō|Watanabe Nangaku,Japanese|Japanese,1779 |1763,1821 |1813,ca. 1811,1806,1816,Woodblock printed book; ink and color on paper,10 1/4 × 6 7/8 in. (26 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.761,false,true,78663,Asian Art,Illustrated book,『役者三十六歌仙』|The Thirty-Six Immortals of Poetry as Kabuki Actors (Yakusha sanjūrokkasen),Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Kunisada|Totoya Hokkei,"Japanese, 1786–1865|Japanese, 1780–1850",,Utagawa Kunisada|Totoya Hokkei,Japanese|Japanese,1786 |1780,1865 |1850,1835,1835,1835,Woodblock printed book; ink and color on paper,10 1/16 × 7 5/16 in. (25.6 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.171a, b",false,true,58349,Asian Art,Box,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Shibata Zeshin|Ikeda Taishin,"Japanese, 1807–1891|Japanese, 1825–1903",,Shibata Zeshin|Ikeda Taishin,Japanese|Japanese,1807 |1825,1891 |1903,1862,1862,1862,"Mokume-nuri, gold, silver, red, black lacquer, takamaki-e, hiramaki-e",H. 2 1/2 in. (6.4 cm); W. 6 in. (15.2 cm); D. 4 1/2 in. (11.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB5,false,true,57540,Asian Art,Illustrated book,"Nishikizuri onna sanjūrokkasen|Courtiers and Urchins, frontispiece for the album Brocade Prints of the Thirty-six Poetesses",Japan,Edo period (1615–1868),,,,Artist|Artist,,Chōbunsai Eishi|Katsushika Hokusai,"Japanese, 1756–1829|Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Chōbunsai Eishi|Katsushika Hokusai,Japanese|Japanese,1756 |1760,1829 |1849,1801,1801,1801,Ink and color on paper,9 7/8 × 7 3/8 × 3/4 in. (25.1 × 18.7 × 1.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.707,false,true,78628,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Totoya Hokkei|Utagawa Hiroshige,"Japanese, 1780–1850|Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Totoya Hokkei|Utagawa, Hiroshige",Japanese|Japanese,1780 |1797,1850 |1858,1840,1840,1840,Woodblock printed book; ink and color on paper,8 15/16 × 6 5/16 in. (22.7 × 16.1 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.240,false,true,58926,Asian Art,Inrō,五代目市川団十郎肖像蒔絵印籠|Inrō with Kabuki Actor Ichikawa Danjūrō V,Japan,Edo period (1615–1868),,,,Artist|Artist,Maki-e by|Design by,Jōsensai|Katsukawa Shunshō,"Japanese, active late 18th–early 19th century|Japanese, 1726–1792",,Jōsensai|Katsukawa Shunshō,Japanese|Japanese,1767 |1726,1833 |1792,late 18th–early 19th century,1767,1833,"Five cases; lacquered wood with gold, silver, black, and red togidashimaki-e on black lacquer ground Netsuke: ivory; Nō mask Ojime: lacquer bead",3 1/2 x 1 7/8 x 1 1/4 in. (8.9 x 4.8 x 3.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB51,false,true,57651,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Hiroshige|Uoya Eikichi,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)|Japanese, ca. 1855–1866",,"Utagawa, Hiroshige|Uoya Eikichi",Japanese|Japanese,1797 |1855,1858 |1855,1856–58,1856,1858,Polychrome woodblock print; ink and color on paper,14 1/2 × 10 × 1 1/4 in. (36.8 × 25.4 × 3.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB118a–d,false,true,57810,Asian Art,Illustrated book,酒井抱一画『光琳百圖』|One Hundred Paintings by Kōrin (Kōrin hyakuzu),Japan,Edo period (1615–1868),,,,Artist|Artist,After,Ogata Kōrin|Sakai Hōitsu,"Japanese, 1658–1716|Japanese, 1761–1828",,Kōrin|Sakai Hōitsu,Japanese|Japanese,1658 |1761,1716 |1828,1815 (first two volumes) and–1826 (two sequel volumes),1815,1826,Four volumes of woodblock printed books; ink on paper,Overall (each volume): H. 10 3/8 in. (26.4 cm); W. 7 3/16 in. (18.3 cm); D. 1 in. (2.5 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2795,false,true,57094,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist|Artist,or,Kitao Masanobu (Santō Kyōden)|Kitao Shigemasa,"Japanese, 1761–1816|Japanese, 1739–1820",,Kitao Masanobu (Santō Kyōden)|Kitao Shigemasa,Japanese|Japanese,1761 |1739,1816 |1820,1739–1820,1739,1820,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 7/8 in. (35.9 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.146,false,true,73432,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Hiroshige II|Utagawa Kunisada,"Japanese, 1829–1869|Japanese, 1786–1865",,Utagawa Hiroshige II|Utagawa Kunisada,Japanese|Japanese,1829 |1786,1869 |1865,"3rd month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (b): 14 5/8 x 10 1/8 in. (37.1 x 25.7 cm) Image (c): 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP186,false,true,36664,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Chōkōsai Eishō|Yamaguchiya Chūsuke,"Japanese, 1793–99|Japanese, ca. 1793–1809",,Chōkōsai Eishō|Yamaguchiya Chūsuke,Japanese|Japanese,1793 |1783,1799 |1819,probably 1798,1796,1800,Polychrome woodblock print; ink and color on paper,15 5/8 x 10 in. (39.7 x 25.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP202,false,true,36679,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Toyokuni I|Wakasaya Yoichi,"Japanese, 1769–1825|Japanese, ca. 1794–1897",,Utagawa Toyokuni I|Wakasaya Yoichi,Japanese|Japanese,1769 |1784,1825 |1907,ca. 1800,1790,1810,Triptych of polychrome woodblock prints; ink and color on paper,14 x 30 1/8 in. (35.6 x 76.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.276a–c,false,true,73559,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Kunisada|Utagawa Kunitoki,"Japanese, 1786–1865|Japanese, active ca. 1860",,Utagawa Kunisada|Utagawa Kunitoki,Japanese|Japanese,1786 |1850,1865 |1870,"5th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 10 1/4 x 14 3/4 in. (26 x 37.5 cm) Image (b): 10 1/8 x 14 3/4 in. (25.7 x 37.5 cm) Image (c): 10 1/4 x 14 3/4 in. (26 x 37.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3332,false,true,55480,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Kuniyoshi|Wakasaya Yoichi,"Japanese, 1797–1861|Japanese, ca. 1794–1897",,Utagawa Kuniyoshi|Wakasaya Yoichi,Japanese|Japanese,1797 |1784,1861 |1907,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Oban yoko-e; 8 7/8 x 14 in. (22.5 x 35.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2751,false,true,57017,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Kunisada|Ichiransai Kunitsuna,"Japanese, 1786–1865|Japanese, mid-nineteenth century",,Utagawa Kunisada|Ichiransai Kunitsuna,Japanese|Japanese,1786 |1800,1865 |1899,after 1844,1845,1868,Triptych of polychrome woodblock prints; ink and color on thin paper,14 1/4 x 30 1/2 in. (36.2 x 77.5 cm),"Fletcher Fund, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2753,false,true,57019,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Kunisada|Ichiransai Kunitsuna,"Japanese, 1786–1865|Japanese, mid-nineteenth century",,Utagawa Kunisada|Ichiransai Kunitsuna,Japanese|Japanese,1786 |1800,1865 |1899,after 1844,1844,1868,Drawings intended as design for woodblock prints (triptych); ink and color on paper,14 1/4 x 30 1/2 in. (36.2 x 77.5 cm),"Fletcher Fund, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1418,false,true,55446,Asian Art,Print,木曽海道六拾九次之内 軽井沢|Karuizawa,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Hiroshige|Keisai Eisen,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)|Japanese, 1790–1848",,"Utagawa, Hiroshige|Keisai Eisen",Japanese|Japanese,1797 |1790,1858 |1848,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,H. 8 9/16 in. (21.7 cm); W. 13 1/2 in. (34.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1023,false,true,37282,Asian Art,Woodblock print,"歌川広重画 「名所江戸百景 駒形堂吾嬬橋」|“Azuma Bridge from Komagatadō Temple,” from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei, Komagatadō Azumabashi)",Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Hiroshige|Uoya Eikichi,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)|Japanese, ca. 1855–1866",,"Utagawa, Hiroshige|Uoya Eikichi",Japanese|Japanese,1797 |1855,1858 |1855,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 13 3/8 in. (34 cm); W. 8 3/4 in. (22.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.181a, b",false,true,47419,Asian Art,Teapot,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Kentei|Shuhei,"Japanese, 16th–17th century|Japanese, 1788–1839",,Kentei|Shuhei,Japanese|Japanese,1500 |1788,1800 |1839,1790,1700,1799,"Clay, fine and thin, decorated in polychrome enamels (Kyoto ware)",H. 4 1/4 in. (10.8 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.84,false,true,57249,Asian Art,Hanging scroll,,Japan,,,,,Artist|Artist,Attributed to|Formerly Attributed to,Keison|Keishoki,"Japanese, active late 15th– early 16th century|Japanese, active ca. 1500",,Keison|Keishoki,Japanese|Japanese,1467 |1450,1533 |1550,15th–16th century,1467,1533,Hanging scroll; ink on paper,36 x 14 3/8 in. (91.4 x 36.5 cm),"Rogers Fund, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57249,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.80a, b",false,true,49068,Asian Art,Hanging scrolls,雪竹図|Bamboo in Snow,Japan,Edo period (1615–1868),,,,Artist|Artist,Calligrapher:,Hakujun Shōkō|Taihō Shōkon,1695–1776|1691–1774,,Hakujun Shōkō|Taihō Shōkon,Chinese|Chinese,1695 |1691,1776 |1774,1774,1771,1774,Pair of hanging scrolls; ink on silk,Image (each): 47 1/2 in. × 20 in. (120.7 × 50.8 cm) Overall with mounting (each): 73 3/8 × 25 3/8 in. (186.4 × 64.5 cm) Overall with knobs (each): 73 3/8 × 27 5/8 in. (186.4 × 70.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.132,false,true,49093,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Shokusanjin (Ōta Nanpo)|Utamaro II,"Japanese, 1749–1823|Japanese (died 1831?)",", and others",Utamaro II,Japanese|Japanese,1749 |1750,1823 |1850,ca. 1801–6,1791,1806,Hanging scroll; ink and color on paper,Image: 35 5/8 x 12 in. (90.5 x 30.5 cm) Overall: 67 3/4 x 15 5/8 in. (172.1 x 39.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.112,false,true,49004,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Tani Bunchō|Shokusanjin (Ōta Nanpo),"Japanese, 1763–1840|Japanese, 1749–1823",,Tani Bunchō,Japanese|Japanese,1763 |1749,1840 |1823,1814,1814,1814,Hanging scroll; ink and color on paper,Image: 47 1/2 × 11 3/16 in. (120.7 × 28.4 cm) Overall with mounting: 76 × 12 1/16 in. (193 × 30.7 cm) Overall with knobs: 76 × 13 13/16 in. (193 × 35.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.88,false,true,52988,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist,Painting by|Inscribed by,Tawaraya Sōtatsu|Takeuchi Toshiharu,"Japanese, died ca. 1640|Japanese, 1611–1647",,Tawaraya Sōtatsu|Takeuchi Toshiharu,Japanese|Japanese,1540 |1611,1640 |1647,ca. 1634,1624,1644,"Poem card (shikishi) mounted as a hanging scroll; ink, color, and gold on paper",Image: 9 11/16 × 8 3/16 in. (24.6 × 20.8 cm) Overall with mounting: 49 5/16 × 16 11/16 in. (125.3 × 42.4 cm) Overall with knobs: 49 5/16 × 18 3/8 in. (125.3 × 46.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/52988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.407.2,false,true,45334,Asian Art,Hanging scroll,松花堂昭乗書・伝俵屋宗達下絵 鉄線下絵和歌色紙 藤原興風|Poem by Fujiwara no Okikaze with Underpainting of Clematis,Japan,Edo period (1615–1868),,,,Artist|Artist,Calligraphy by|Underpainting attributed to,Shōkadō Shōjō|Tawaraya Sōtatsu,"Japanese, 1584?–1639|Japanese, died ca. 1640",,Shōkadō Shōjō|Tawaraya Sōtatsu,Japanese|Japanese,1584 |1540,1639 |1640,early 17th century,1600,1633,"Hanging scroll; ink, gold, and silver on colored paper",Image: 7 15/16 x 6 15/16 in. (20.2 x 17.6 cm) Overall with mounting: 53 x 17 15/16 in. (134.6 x 45.6 cm) Overall with knobs: 53 x 20 5/8 in. (134.6 x 52.4 cm),"Purchase, Gift of Mrs. Russell Sage, by exchange, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.86,false,true,53246,Asian Art,Hanging scroll,小倉百人一首和歌巻断簡|Two Poems from One Hundred Poems by One Hundred Poets (Ogura hyakunin isshu),Japan,Momoyama period (1573–1615),,,,Artist|Artist,Painting by,Hon'ami Kōetsu|Tawaraya Sōtatsu,"Japanese, 1558–1637|Japanese, died ca. 1640",,Hon'ami Kōetsu|Tawaraya Sōtatsu,Japanese|Japanese,1558 |1540,1637 |1640,ca. 1615–20,1615,1620,"Section of scroll; ink, silver, and gold on paper",Image: 13 in. × 23 3/4 in. (33 × 60.4 cm) Overall with mounting: 49 5/8 × 29 1/8 in. (126 × 73.9 cm) Overall with knobs: 49 5/8 × 31 5/16 in. (126 × 79.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.59,false,true,44861,Asian Art,Hanging scroll,本阿見光悦書・俵屋宗達下絵 桜下絵和歌色紙 鴨長明|Poem by Kamo no Chōmei with Underpainting of Cherry Blossoms,Japan,Momoyama period (1573–1615),,,,Artist|Artist,Underpainting attributed to|Calligraphy by,Tawaraya Sōtatsu|Hon'ami Kōetsu,"Japanese, died ca. 1640|Japanese, 1558–1637",,Tawaraya Sōtatsu|Hon'ami Kōetsu,Japanese|Japanese,1540 |1558,1640 |1637,dated 1606,1606,1606,"Poem card (shikishi) mounted as a hanging scroll; ink, gold, and silver on paper",Overall: 7 15/16 x 7in. (20.2 x 17.8cm) Overall with mounting: 53 x 14 3/4 in. (134.6 x 37.5 cm) Overall with knobs: 53 x 16 1/2 in. (134.6 x 41.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3497a–uu,false,true,55727,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Toyohara Kunichika|Utagawa Kunisada,"Japanese, 1835–1900|Japanese, 1786–1865",(tt and uu),Toyohara Kunichika|Utagawa Kunisada,Japanese|Japanese,1835 |1786,1900 |1865,,1769,1900,Polychrome woodblock print; ink and color on paper,a–t: 14 x 9 1/4 in. (35.6 x 23.5 cm) (each) tt –uu: 6 3/4 x 9 1/4 in. (17.1 x 23.5 cm) (each),"Gift of Dr. and Mrs. Harold B. Bilsky, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2752,false,true,57018,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Kunisada|Ichiransai Kunitsuna,"Japanese, 1786–1865|Japanese, mid-nineteenth century",,Utagawa Kunisada|Ichiransai Kunitsuna,Japanese|Japanese,1786 |1800,1865 |1899,,1786,1864,Preliminary sketch for drawing intended as design for woodblock print; ink on thin paper,14 1/4 x 30 1/2 in. (36.2 x 77.5 cm),"Fletcher Fund, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2754,false,true,57020,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Utagawa Kunisada|Ichiransai Kunitsuna,"Japanese, 1786–1865|Japanese, mid-nineteenth century",,Utagawa Kunisada|Ichiransai Kunitsuna,Japanese|Japanese,1786 |1800,1865 |1899,,1786,1864,Ink on thin paper,14 1/4 x 30 1/2 in. (36.2 x 77.5 cm),"Fletcher Fund, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3439,false,true,55643,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,or,Utagawa Hiroshige II|Utagawa Hiroshige,"Japanese, 1829–1869|Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa Hiroshige II|Utagawa, Hiroshige",Japanese|Japanese,1829 |1797,1869 |1858,,1615,1868,Polychrome woodblock print; ink and color on paper,9 x 14 in. (22.9 x 35.6 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.36,false,true,55354,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist,Attributed to,Matsumura Keibun|Matsumura Keibun,"Japanese, 1779–1843|Japanese, 1779–1843",,Matsumura Keibun|Matsumura Keibun,Japanese|Japanese,1779 |1779,1843 |1843,,1779,1843,Hanging scroll; ink and color on paper,40 1/8 x 11 7/8 in. (101.9 x 30.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.37,false,true,55364,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist,Attributed to,Matsumura Keibun|Matsumura Keibun,"Japanese, 1779–1843|Japanese, 1779–1843",,Matsumura Keibun|Matsumura Keibun,Japanese|Japanese,1779 |1779,1843 |1843,,1779,1843,Hanging scroll; color on paper,31 5/8 x 12 5/8 in. (80.3 x 32.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.477,false,true,54974,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist|Artist,Attributed to|In the Style of,Kano Tambi|Li Gonglin,"Japanese, 1840–1893|Chinese, ca. 1041–1106",,Kano Tambi|Li Gonglin,Japanese|Chinese,1840 |1041,1893 |1106,,1840,1893,Hanging scroll; ink on paper,31 x 15 in. (78.7 x 38.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.905,false,true,58829,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Hosoya Hansai|Tomioka Tessai,"Japanese|Japanese, 1836–1924",,Hosoya Hansai|Tomioka Tessai,Japanese|Japanese,1836,1924,19th century,1800,1899,"Lacquer, roiro, black hiramakie, incised; Interior: roiro and hirame",2 9/16 x 1 7/8 x 7/8 in. (6.5 x 4.7 x 2.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.44,false,true,58585,Asian Art,Inrō,原羊遊斎作・狩野晴川院筆 満月に鵞鳥図印籠|Inrō with Goose Flying across the Full Moon,Japan,Edo period (1615–1868),,,,Artist|Artist,After,Kano Seisen’in|Hara Yōyūsai,"1775–1828|Japanese, 1772–1845",,Kano Seisen’in|Hara Yōyūsai,Japanese|Japanese,1775 |1772,1828 |1845,19th century,1800,1899,"Lacquer, kinji, gold, silver, and black hiramaki-e, takamaki-e, and togidashi",Overall (inro): H. 3 11/16 in. (9.4 cm); W. 1 7/8 in. (4.8 cm); D. 1 1/16 in. (2.7 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.245,false,true,58931,Asian Art,Inrō,原羊遊斎作・酒井抱一下絵 梅木蒔絵印籠|Inrō with Design of Blossoming Plum Tree,Japan,Edo period (1615–1868),,,,Artist|Artist,After a design by,Hara Yōyūsai|Sakai Hōitsu,"Japanese, 1772–1845|Japanese, 1761–1828",,Hara Yōyūsai|Sakai Hōitsu,Japanese|Japanese,1772 |1761,1845 |1828,19th century,1800,1899,"Sprinkled gold lacquer with gold, silver, and red makie, takamakie, and coral inlay Ojime: bead; tortoiseshell Netsuke: box with decoration of violets; gold makie lacquer with gold and silver makie",Overall (inro): H. 3 11/16 in. (9.3 cm); W. 2 1/4 in. (5.7 cm); D. 7/8 in. (2.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.249,false,true,45456,Asian Art,Inrō,柴田是真作 波文印籠|Inrō with Stylized Waves,Japan,Edo period (1615–1868),,,,Artist|Artist,After a design by,Shibata Zeshin|Ogata Kōrin,"Japanese, 1807–1891|Japanese, 1658–1716",,Shibata Zeshin|Kōrin,Japanese|Japanese,1807 |1658,1891 |1716,19th century,1790,1899,Gold lacquer with pewter inlay; Ojime: bronze and gold jar; Netsuke: carved tortoiseshell turtle,Overall (inro): H. 1 15/16 in. (5 cm); W. 1 3/4 in. (4.4 cm); D. 11/16 in. (1.7 cm) Overall (netsuke): H. 11/16 in. (1.7 cm); W. 1 3/16 in. (3 cm); L. 1 9/16 in. (4 cm) Overall (ojime): H. 3/8 in. (1 cm); W. 3/16 in. (0.5 cm); D. 3/16 in. (0.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.251,false,true,45446,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist|Artist,Attributed to|After an inro by,Shibata Zeshin|Ogawa Haritsu (Ritsuō),"Japanese, 1807–1891|Japanese, 1663–1747",,Shibata Zeshin|Ritsuō,Japanese|Japanese,1807 |1663,1891 |1747,19th century,1790,1899,Roiro ('waxen') lacquer with black hiramakie sprinkled and polished lacquer and takamakie sprinkled and polished lacquer relief; Interior: roiro; Netsuke: nut carved with Chinese sages and attendants; Ojime: carved peach stone with landscape and figures,2 7/16 x 2 7/16 x 9/16 in. (6.2 x 6.2 x 1.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP210,false,true,36687,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist|Artist,Fourteen prints by|Sixteen prints by,Utagawa Kuniyoshi|Utagawa Kunisada,"Japanese, 1797–1861|Japanese, 1786–1865",,Utagawa Kuniyoshi|Utagawa Kunisada,Japanese|Japanese,1797 |1786,1861 |1865,19th century,1800,1865,Album of 30 polychrome woodblock prints; ink and color on paper,14 1/4 × 9 5/8 × 1/2 in. (36.2 × 24.4 × 1.3 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.63,false,true,49100,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist,In the Style of,Kano Osanobu|Yoden,"1796–1846|Chinese, active early Yuan period (1279–1368)",,Kano Osanobu|Yoden,Japanese|Chinese,1796 |1279,1846 |1368,19th century,1800,1846,Hanging scroll; ink on silk,37 1/2 x 17 1/4 in. (95.3 x 43.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.33,false,true,58575,Asian Art,Inrō,原羊遊斎作・酒井抱一下絵 南天に雀印籠|Inrō with Sparrows in Snow-covered Nandina,Japan,Edo period (1615–1868),,,,Artist|Artist,After a design by,Hara Yōyūsai|Sakai Hōitsu,"Japanese, 1772–1845|Japanese, 1761–1828",,Hara Yōyūsai|Sakai Hōitsu,Japanese|Japanese,1772 |1761,1845 |1828,early 19th century,1800,1833,"Lacquer with gold and silver makie, coral, and applied stained ivory Ojime: bead with stylized medallions; green stone with gold makie lacquer Netsuke: round box with iris design",Overall (inro): H. 3 11/16 in. (9.3 cm); W. 2 3/16 in. (5.6 cm); D. 13/16 in. (2.1 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.300,false,true,72471,Asian Art,Hanging scroll,해강 김규진 난초|海岡 金圭鎭 墨蘭圖|Orchids in hanging basket,Korea,,,,,Artist,,Kim Gyujin,"Korean, 1868–1933",,Kim Gyujin,,1868,1933,early 20th century,1900,1933,Hanging scroll; ink on paper,Image: 55 1/4 × 15 3/4 in. (140.3 × 40 cm) Overall with mounting: 81 1/8 × 21 5/8 in. (206 × 55 cm) Overall with knobs: 81 1/8 × 23 7/8 in. (206 × 60.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.103,false,true,38027,Asian Art,Folio,,"India (Rajasthan, Mewar)",,,,,Artist,,Sahibdin,active ca. 1628–55,,Sahibdin,,1618,1665,ca. 1665,1655,1675,Ink and opaque watercolor on paper,H. 9 5/8 in (24.4 cm); W. 7 3/4 in. (19.7 cm),"Gift of Ernest Erickson Foundation, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/38027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.24.1,false,true,37947,Asian Art,Folio,,"India (Guler, Himachal Pradesh)",,,,,Artist,Attributed to,Manaku,active ca. 1725–60,,Manaku,,1715,1770,ca. 1725,1715,1735,"Opaque watercolor, ink and gold on paper",Page: 23 1/2 x 32 3/4 in. (59.7 x 83.2 cm) Image: 22 1/4 x 31 1/4 in. (56.5 x 79.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.24.2,false,true,37948,Asian Art,Folio,,"India (Punjab Hills, Guler)",,,,,Artist,Attributed to,Manaku,active ca. 1725–60,,Manaku,,1715,1770,ca. 1725,1715,1735,Ink and opaque watercolor on paper,22 5/16 x 33 in. (56.7 x 83.8 cm),"Rogers Fund, 1919",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.24.3,false,true,37949,Asian Art,Drawing,,"India (Himachal Pradesh, Guler)",,,,,Artist,Attributed to,Manaku,active ca. 1725–60,,Manaku,,1715,1770,ca. 1725,1715,1735,Ink on paper,24 1/2 x 32 5/8 in. (62.2 x 82.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.24.4,false,true,37950,Asian Art,Drawing,,"India (Guler, Himachal Pradesh)",,,,,Artist,Attributed to,Manaku,active ca. 1725–60,,Manaku,,1715,1770,ca. 1725,1715,1735,Ink on paper,Page: 23 1/2 x 33 in. (59.7 x 83.8 cm) Image: 22 3/8 x 31 1/2 in. (56.8 x 80 cm),"Rogers Fund, 1919",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.398.12,false,true,37906,Asian Art,Folio,,"India (Rajasthan, Mewar)",,,,,Artist,Style of,Manohar,active ca. 1582–1624,,Manohar,,1577,1624,ca. 1655–60,1655,1660,Ink and opaque watercolor on paper,Image: 9 1/8 x 7 1/4 in. (23.2 x 18.4 cm); Page: 10 1/4 x 8 3/8 in. (26 x 21.3 cm),"Gift of Cythian Hazen Polsky, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.100.6,false,true,38043,Asian Art,Painting,,"India (Jaipur, Rajasthan)",,,,,Artist,Generation of,Bagta,active ca. 1761–1814,,Bagta,,1751,1824,ca. 1810–18,1800,1818,Opaque watercolor and ink on paper,Overall: 29 x 40 5/8 in. (73.7 x 103.2 cm) Framed: 39 3/4 x 50 3/4 in. (101 x 128.9 cm),"Fletcher Fund, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/38043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.540.2,false,true,37863,Asian Art,Folio,,"India (Bikaner, Rajasthan)",,,,,Artist,,Ruknuddin,active late 17th century,,Ruknuddin,,1667,1695,ca. 1690–95,1680,1705,Opaque watercolor and ink on paper,Image: 6 in. × 4 11/16 in. (15.2 × 11.9 cm) Sheet: 10 1/8 × 7 1/4 in. (25.7 × 18.4 cm),"Gift of Mr. and Mrs. Peter Findlay, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.526.2,false,true,37870,Asian Art,Folio,,"India (Rajasthan, Bikaner)",,,,,Artist,,Mohamed,active early 18th century,,Mohamed,,1700,1733,1714,1714,1714,Ink and opaque watercolor on paper,6 x 4 3/4 in. (15.2 x 12.1 cm),"Gift of John and Evelyn Kossak, The Kronos Collections, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.344,true,true,61429,Asian Art,Painting,,"Western India, Rajasthan, Mewar",,,,,Artist,,Tara,"Indian, active 1836–1870",,Tara,,1836,1870,1845–46,1845,1846,"Opaque watercolor, ink, and gold on paper",Image (painting): 16 3/4 x 22 3/4 in. (42.5 x 57.8 cm) Sheet: 19 x 24 7/8 in. (48.3 x 63.2 cm),"Cynthia Hazen Polsky and Leon B. Polsky Fund, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/61429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.310,false,true,38010,Asian Art,Folio,,"India (Bahu, Jammu)",,,,,Artist,,Bahu Masters,active ca. 1680–ca. 1720,,Bahu Masters,,1670,1730,ca. 1690–1710,1690,1710,Opaque watercolor and ink on paper,Page: 8 5/8 x 12 1/2 in. (21.9 x 31.8 cm) Image: 7 3/4 x 11 5/8 in. (19.7 x 29.5 cm),"Purchase, The Dillon Fund, Evelyn Kranes Kossak, and Anonymous Gifts, 1994",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/38010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.185.2,true,true,37942,Asian Art,Folio,,"India (Basohli, Jammu)",,,,,Artist,,Devidasa of Nurpur,active ca. 1680–ca. 1720,,Devidasa of Nurpur,,1670,1730,dated 1694–95,1694,1695,"Opaque watercolor, ink, silver, and gold on paper",Image: 6 1/2 x 10 7/8 in. (16.5 x 27.6 cm) Sheet: 8 x 12 1/4 in. (20.3 x 31.1 cm) Framed: 15 5/8 x 20 1/2 in. (39.7 x 52.1 cm),"Gift of Dr. J. C. Burnett, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.51.14,false,true,37941,Asian Art,Folio,,"India (Punjab Hills, Basohli)",,,,,Artist,,Devidasa of Nurpur,active ca. 1680–ca. 1720,,Devidasa of Nurpur,,1670,1730,dated 1694–95,1694,1695,"Opaque watercolor, ink, silver, and gold on paper",Overall: 8 5/8 x 12 3/4in. (21.9 x 32.4cm) Painting within rules: 6 3/4 x 11 1/4 in. (17.2 x 28.6 cm),"Bequest of Cora Timken Burnett, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.85.2,false,true,37877,Asian Art,Painting,,"India (Rajasthan, Jaipur)",,,,,Artist,Attributed to,Sahib Ram,"active reign of Maharaja Sawai Pratap Singh, 1778–1803",,SAHIB RAM,,1778,1803,ca. 1800,1790,1810,Ink and opaque watercolor on paper,H. 27 1/4 in. (69.2 cm); W. 18 1/2 in. (47 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/37877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.85.4,false,true,38465,Asian Art,Drawing,,"India (Rajasthan, Jaipur)",,,,,Artist,Attributed to,Sahib Ram,"active reign of Maharaja Sawai Pratap Singh, 1778–1803",,SAHIB RAM,,1778,1803,ca. 1800,1790,1810,Ink on paper,26 x 18 1/4 in. (66 x 46.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/38465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.299,false,true,50486,Asian Art,Hanging scroll,"탄은 이정 대나무 조선|灘隱 李霆 墨竹圖 朝鮮|Bamboo in the wind",Korea,Joseon dynasty (1392–1910),,,,Artist,,Yi Jeong,"Korean, 1541–1626",,Yi Jeong,,1541,1626,early 17th century,1600,1633,Hanging scroll; ink on silk with gold on colophon,Image: 45 1/2 x 21 in. (115.6 x 53.3 cm) Overall with mounting: 87 13/16 × 26 5/16 in. (223 × 66.8 cm) Overall with knobs: 87 13/16 × 28 3/4 in. (223 × 73 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.419,true,true,38648,Asian Art,Torso,,"Pakistan (ancient region of Gandhara, mondern Peshawar region)",,,,,Artist,Probably,Sahri-Bahlol Workshop,,,Sahri-Bahlol Workshop,,0350,0550,ca. 5th century,350,550,Schist,H. 64 1/2 in. (163.8 cm),"Purchase, Lila Acheson Wallace Gift, 1995",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/38648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.4,false,true,74649,Asian Art,Drawing,,"India (Pahari Hills, Guler)",,,,,Artist|Artist,Attributed to the|possibly,Seu Family|Manaku,active ca. 1725–60,,Seu Family|Manaku,,1715,1770,mid-18th century,1736,1770,Ink and wash on paper,Image (sight): 8 1/2 x 12 3/8 in. (21.6 x 31.4 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.5,false,true,74650,Asian Art,Painting,,"India (Pahari Hills, Guler)",,,,,Artist|Artist,Attributed to the|possibly,Seu Family|Manaku,active ca. 1725–60,,Seu Family|Manaku,,1715,1770,mid-18th century,1736,1770,Ink and wash on paper,Image (sight): 8 1/2 x 12 1/8 in. (21.6 x 30.8 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.359.6,false,true,74651,Asian Art,Painting,,"India (Pahari Hills, Guler)",,,,,Artist|Artist,Attributed to the|possibly,Seu Family|Manaku,active ca. 1725–60,,Seu Family|Manaku,,1715,1770,mid-18th century,1736,1770,Ink and wash on paper,Image (sight): 8 3/4 x 13 in. (22.2 x 33 cm),"Gift of Subhash Kapoor, in memory of his parents, Smt Shashi Kanta and Shree Parshotam Ram Kapoor, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.90,false,true,51383,Asian Art,Folding fan mounted as an album leaf,,China,Ming dynasty (1368–1644),,,,Calligrapher,,Gong Dingzi,"Chinese, 1615–1673",,Gong Dingzi,,1615,1673,,1368,1644,Folding fan mounted as an album leaf; ink on gold paper,6 1/2 x 20 1/2 in. (16.5 x 52.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.271,false,true,39626,Asian Art,Incense holder,,China,Ming dynasty (1368–1644),,,,Artist,,Zhu Sansong,active ca. 1573–1619,,Zhu Sansong,,1573,1619,late 16th–early 17th century,1567,1633,Bamboo,Overall (including new wooden ends) H. 7 in. (17.8 cm),"Purchase, Friends of Asian Art Gifts, 1995",,,,,,,,,,,,Bamboo,,http://www.metmuseum.org/art/collection/search/39626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.784.9,false,true,65630,Asian Art,Hanging scroll,清 鄭簠 隷書軸|Poetic Maxim,China,Qing dynasty (1644–1911),,,,Artist,,Zheng Fu,"Chinese, 1622–1693",,Zheng Fu,,1622,1693,dated 1691,1691,1691,Hanging scroll; ink on paper,Image: 45 1/2 × 17 3/4 in. (115.6 × 45.1 cm) Overall with mounting: 97 3/4 × 25 1/2 in. (248.3 × 64.8 cm) Overall with knobs: 97 3/4 × 29 1/4 in. (248.3 × 74.3 cm),"Gift of Julia and John Curtis, in memory of Marie-Hélène and Guy Weill, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/65630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.74a–h,false,true,64086,Asian Art,Screen,清晚期 盧葵生款 剔紅郭子儀賀壽圖屏風|Screen with birthday celebration for General Guo Ziyi,China,Qing dynasty (1644–1911),,,,Artist,,Lu Guisheng,"Chinese, active 1821–50",,"Lu, Guisheng",,1821,1850,mid-19th century,1834,1850,Carved red and black lacquer,Open flat: 84 1/8 in. × 12 ft. 4 in. (213.7 × 375.9 cm) Open curved: 84 1/8 in. × 11 ft. 3 13/16 in. × 35 7/16 in. (213.7 × 345 × 90 cm),"Gift of Mrs. Henry-George J. McNeary, 1971",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/64086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.29,false,true,40512,Asian Art,Handscroll,元 鮮于樞 草書石鼓歌 卷|Song of the Stone Drums,China,Yuan dynasty (1271–1368),,,,Artist,,Xianyu Shu,"Chinese, 1246–1302",,Xianyu Shu,,1246,1302,dated 1301,1301,1301,Handscroll; ink on paper,Image: 17 11/16 in. x 15 ft. 1 1/16 in. (44.9 x 459.9 cm) Overall with mounting: 18 in. x 38 ft. 11 3/16 in. (45.7 x 1186.7 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/40512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +34.13,false,true,51042,Asian Art,Figure,,China,Qing dynasty (1644–1911),,,,Artist,,Su Xuejin,1869–1919,,Su Xuejin,,1869,1919,late 19th–early 20th century,1867,1933,"Porcelain with clear glaze (Dehua ware, Fujian Province)",H. 15 1/2 in. (39.4 cm); W. 5 in. (12.7 cm); D. 4 in. (10.2 cm),"Purchase, Anita M. Linzee Bequest, 1934",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/51042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.181,false,true,36458,Asian Art,Table screen,"大理石插屏|Table Screen Inscribed with the Poem ""Lisao,"" now converted to a wall panel",China,Ming dynasty (1368–1644),,,,Artist,,Li Mi,active early 17th century,,Li Mi,,1600,1699,dated 1624,1624,1624,Veined marble with wood frame,H. 16 5/8 (42.2 cm); W. 14 3/8in. (36.5cm),"Purchase, Eileen W. Bamberger Bequest, in memory of her husband, Max Bamberger, 1995",,,,,,,,,,,,Furniture,,http://www.metmuseum.org/art/collection/search/36458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.6.22,false,true,39879,Asian Art,Incense burner,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Hu Wenming,"Chinese, active late 16th–early 17th century",,Hu Wenming,,1550,1650,late 16th–17th century,1567,1699,Copper with gilding,H. 6 1/4 in. (15.9 cm); W. 2 7/8 in. (7.3 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Metalwork,,http://www.metmuseum.org/art/collection/search/39879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.6.24,false,true,39878,Asian Art,Vase,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Hu Wenming,"Chinese, active late 16th–early 17th century",,Hu Wenming,,1550,1650,16th–17th century,1500,1799,Bronze with gilding,H. 3 7/8 in. (9.8 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Metalwork,,http://www.metmuseum.org/art/collection/search/39878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.6.23a–c,false,true,39877,Asian Art,Incense box,,China,Ming dynasty (1368–1644),,,,Artist,Attributed to,Hu Wenming,"Chinese, active late 16th–early 17th century",,Hu Wenming,,1550,1650,16th–17th century,1500,1799,Bronze with gilding,H. 1 1/4 in.; Diam. 2 7/8 in.,"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Metalwork,,http://www.metmuseum.org/art/collection/search/39877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.273,false,true,684605,Asian Art,Vase,十七/十八世紀 銅錯銀絲題竇常詩文饕餮耳方壺|Vase,China,Qing dynasty (1644–1911),,,,Artist,Attributed to,Shisou,"Chinese, active first half the 17th century",,Shisou,,1600,1649,17th–18th century,1600,1799,Bronze,H. 5 1/4 in. (13.3 cm); W. 2 1/2 in. (6.4 cm); D. 2 5/16 in. (5.9 cm),"Purchase, Seymour Fund, Barbara and Sorrell Mathes Gift, and various donors, 2015",,,,,,,,,,,,Metalwork,,http://www.metmuseum.org/art/collection/search/684605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.784.5,false,true,65624,Asian Art,Album leaf,"明/清 惲向 欲雪圖 冊頁|Snowscape, from Album for Zhou Lianggong",China,,,,,Artist,,Yun Xiang,"Chinese, 1586–1655",,Yun Xiang,,1586,1655,Undated,1586,1655,Double album leaf from a collective album of twelve paintings and facing pages of calligraphy; ink and color on paper,9 3/4 x 13 in. (24.8 x 33 cm),"Gift of Julia and John Curtis, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.784.3,false,true,73650,Asian Art,Handscroll,明/清 張宏 桃源勝概圖 卷|Peach Blossom Spring,China,Ming dynasty (1368–1644),,,,Artist,,Zhang Hong,"Chinese, 1577–after 1652",,Zhang Hong,,1577,1652,dated 1638,1638,1638,Handscroll; ink and color on paper,Image: 9 1/2 × 74 5/8 in. (24.1 × 189.5 cm) Overall with mounting: 11 in. × 21 ft. 1 5/8 in. (28 × 644.2 cm),"Gift of Julia and John Curtis, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.242.1–.6,false,true,72262,Asian Art,Album,明 盛茂曄 唐詩意山水圖 冊|Landscapes after Tang Poems,China,Ming dynasty (1368–1644),,,,Artist,,Sheng Maoye,"Chinese, active ca. 1615–ca. 1640",,Sheng Maoye,,1615,1640,mid 17th century,1634,1666,Album of six paintings; ink and color on silk,.1: 11 1/4 x 12 in. (28.6 x 30.5 cm) .2: 11 1/4 x 12 in. (28.6 x 30.5 cm) .3: 11 5/8 x 12 in. (29.5 x 30.5 cm) .4: 11 1/4 x 12 in. (28.6 x 30.5 cm) .5: 11 7/8 x 12 in. (30.2 x 30.5 cm) .6: 11 5/8 x 12 in. (29.5 x 30.5 cm),"The Sackler Fund, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.363.128,false,true,49125,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Li Yu,1611–1680,,Li Yu,,1611,1680,dated 1648,1648,1648,Hanging scroll; ink on satin,Image: 33 5/8 x 11 1/4 in. (85.4 x 28.6 cm) Overall with mounting: 85 x 17 1/8 in. (215.9 x 43.5 cm) Overall with knobs: 85 x 20 5/8 in. (215.9 x 52.4 cm),"Bequest of John M. Crawford Jr., 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.324.5,false,true,44569,Asian Art,Album leaf,清 顧澐 冊頁|Clouds and Spring Trees at Dusk,China,Qing dynasty (1644–1911),,,,Artist,,Gu Yun,1835–1896,,Gu Yun,,1835,1896,dated 1888,1888,1888,Album leaf; ink on paper,9 3/4 x 13 1/2 in. (24.8 x 34.3 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.267.57,false,true,36173,Asian Art,Folding fan mounted as an album leaf,清 顧澐 懷素蕉林午睡圖 扇面|Huai Su in the Banana Grove,China,Qing dynasty (1644–1911),,,,Artist,,Gu Yun,1835–1896,,Gu Yun,,1835,1896,dated 1869,1869,1869,Folding fan mounted as an album leaf; ink and color on alum paper,6 3/4 x 20 1/8 in. (17.1 x 51.1 cm),"Gift of Robert Hatfield Ellsworth, in memory of La Ferne Hatfield Ellsworth, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.784.4,false,true,73652,Asian Art,Handscroll,明/清 黃向堅 萬里尋親圖 卷|Searching for My Parents,China,Qing dynasty (1644–1911),,,,Artist,,Huang Xiangjian,"Chinese, 1609–1673",,Huang Xiangjian,,1609,1673,dated 1656,1656,1656,Handscroll; ink and color on silk,Image: 14 3/8 in. × 18 ft. 2 in. (36.5 × 553.7 cm) Overall with mounting: 14 3/4 in. × 27 ft. 1 in. (37.5 × 825.5 cm),"Gift of Julia and John Curtis, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.278.7,false,true,36101,Asian Art,Hanging scroll,清 楊晉 滄洲牧牛圖 軸|Landscape with Figures,China,Qing dynasty (1644–1911),,,,Artist,,Yang Jin,"Chinese, 1644–1728",,Yang Jin,,1644,1728,Dated 1726,1726,1726,Hanging scroll; ink and color on paper,Image: 47 x 21 1/2 in. (119.4 x 54.6 cm) Overall with mounting: 92 1/8 x 28 1/8 in. (234 x 71.4 cm) Overall with knobs: 92 1/8 x 31 7/8 in. (234 x 81 cm),"Gift of Mr. and Mrs. Earl Morse, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.784.10,false,true,646992,Asian Art,Album,清 汪鋆 阮元遺事十景圖 冊 紙本|Ten Sites Associated with Ruan Yuan,China,Qing dynasty (1644–1911),,,,Artist,,Wang Jun,"Chinese, 1816–after 1883",,Wang Jun,,1816,1883,dated 1883,1883,1883,Album of ten paintings; ink and color on paper,Image (each): 11 × 13 1/4 in. (27.9 × 33.7 cm),"Gift of Julia and John Curtis, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/646992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.784.6,false,true,65625,Asian Art,Album leaf,"清 葉欣 聼雪圖 冊頁|Snowscape, from Album for Zhou Lianggong",China,Qing dynasty (1644–1911),,,,Artist,,Ye Xin,"Chinese, active ca. 1640–1673",,Ye Xin,,1640,1640,undated,1640,1673,Double album leaf from a collective album of twelve paintings and facing pages of calligraphy; ink and color on paper,9 3/4 x 13 in. (24.8 x 33 cm),"Gift of Julia and John Curtis, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +02.18.623e,false,true,51234,Asian Art,Handscroll,敬芝軒第貳圖|Second View of the Studio for Respecting the Fungus of Longevity,China,Qing dynasty (1644–1911),,,,Artist,,Yang Tianbi,"Chinese, active early 19th century",,Yang Tianbi,,1800,1833,1825,1825,1825,Handscroll; ink and color on silk,8 3/8 x 31 in. (21.3 x 78.7 cm),"Gift of Heber R. Bishop, 1902",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.673,false,true,39548,Asian Art,Hanging scroll,元 張遜 石上松花圖 軸|Rocky Landscape with Pines,China,Yuan dynasty (1271–1368),,,,Artist,,Zhang Xun,"Chinese, ca. 1295–after 1349",,ZHANG XUNG,,1295,1349,before 1346,1295,1345,Hanging scroll; ink on silk,Image: 35 3/4 × 16 3/4 in. (90.8 × 42.5 cm) Overall with mounting: 76 5/8 × 22 3/4 in. (194.6 × 57.8 cm) Overall with knobs: 76 5/8 × 26 1/8 in. (194.6 × 66.4 cm),"Ex coll.: C. C. Wang Family, Gift of Oscar L. Tang Family, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.2.2,false,true,42328,Asian Art,Hanging scroll,元 陸廣 丹臺春曉圖 軸|Spring Dawn Over the Elixir Terrace,China,early Ming dynasty (1368–1644),,,,Artist,,Lu Guang,"Chinese, ca. 1300–after 1371",,Lu Guang,,1300,1371,ca. 1369,1359,1379,Hanging scroll; ink on paper,Image: 24 1/4 x 10 1/4 in. (61.6 x 26 cm) Overall with mounting: 87 1/2 x 17 5/8 in. (222.3 x 44.8 cm) Overall with knobs: 87 1/2 x 20 5/8 in. (222.3 x 52.4 cm),"Ex coll.: C. C. Wang Family, Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.8.3,false,true,78125,Asian Art,Handscroll,,China,Ming dynasty (1368–1644),,,,Artist,,Zhan Jingfeng,"Chinese, 1520–1602",,Zhan Jingfeng,,1520,1602,,1520,1602,Handscroll; ink on paper,Image: 12 1/4 x 22 in. (31.1 x 55.9 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.500.8.2a, b",false,true,78085,Asian Art,Two leaves from an album,,China,Ming dynasty (1368–1644),,,,Artist,,Cheng Jiasui,"Chinese, 1565–1644",,Cheng Jiasui,,1565,1644,,1368,1644,Two leaves from an album; ink and color on paper,Each: 14 x 14 in. (35.6 x 35.6 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.10,false,true,45669,Asian Art,Hanging scroll,明 周文靖 漁隱圖 軸|Rustic Retreat among Fishermen,China,Ming dynasty (1368–1644),,,,Artist,,Zhou Wenjing,"Chinese, active ca. 1430–after 1463",,Zhou Wenjing,,1430,1463,,1430,1530,Hanging scroll; ink and color on silk,Image: 35 15/16 x 16 1/2 in. (91.3 x 41.9 cm) Overall with mounting: 79 1/2 x 21 5/16 in. (201.9 x 54.1 cm) Overall with knobs: 79 1/2 x 23 1/8 in. (201.9 x 58.7 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.94,false,true,51491,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Zheng Zhong,"Chinese, active ca. 1612–48",,Zheng Zhong,,1612,1648,,1612,1648,Hanging scroll; ink and color on silk,Image: 34 × 12 3/8 in. (86.4 × 31.4 cm) Overall with mounting: 49 3/4 × 17 3/4 in. (126.4 × 45.1 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.6,false,true,51638,Asian Art,Handscroll,,China,Ming dynasty (?) (1368–1644),,,,Artist,Attributed to,Tai Wan,"Chinese, active 1111–25",,Tai Wan,,1111,1125,,1368,1644,Handscroll; ink and color on silk,Image: 11 in. × 21 ft. 1 1/8 in. (27.9 × 642.9 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.14,false,true,44630,Asian Art,Handscroll,明 鄭重 搜山圖 卷|Searching the Mountains for Demons,China,late Ming dynasty (1368–1644),,,,Artist,,Zheng Zhong,"Chinese, active ca. 1612–48",,Zheng Zhong,,1612,1648,,1612,1644,Handscroll; ink and color on paper,10 5/8 in. x 27 ft. 9 1/2 in. (27 x 847.1 cm),"Purchase, Bequest of Dorothy Graham Bennett, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.177.9,false,true,51784,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist,,Empress Dowager Cixi,"Chinese, 1835–1908",,Cixi Empress Dowager,,1835,1908,18th–19th century,1700,1899,Hanging scroll; ink on gold-flecked paper,45 x 24 1/2 in. (114.3 x 62.2 cm),"Bequest of Katherine S. Dreier, 1952",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/51784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.784.8,false,true,73330,Asian Art,Handscroll,明 二家法書合卷|Joint Calligraphy,China,Ming dynasty (1368–1644),,,,Artist|Artist,,Ni Yuanlu|Huang Daozhou,"Chinese, 1593–1644|Chinese, 1585–1646",,Ni Yuanlu|Huang Daozhou,,1593 |1585,1644 |1646,dated 1632,1632,1632,Handscroll; ink on satin,Image: 10 11/16 x 57 3/4 in. (27.1 x 146.7 cm) Overall with mounting: 10 13/16 x 357 7/16 in. (27.5 x 907.9 cm),"Gift of Julia and John Curtis, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/73330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.115,false,true,51398,Asian Art,Fan mounted as an album leaf,,China,Song dynasty (?) (960–1279),,,,Artist|Artist,Formerly Attributed to,Huang Jucai|Unidentified Artist,"Chinese, Song dynasty, 933–after 993",,Huang Jucai|Unidentified Artist,,0933,1000,ca. 1000,990,1010,Fan mounted as an album leaf; ink and color on silk,7 7/8 x 8 7/8 in. (20.0 x 22.5 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.100.104,false,true,40071,Asian Art,Fan mounted as an album leaf,南宋 佚名 “三元得祿”圖扇頁|Gibbons Raiding an Egret's Nest,China,Southern Song dynasty (1127–1279),,,,Artist|Artist,Formerly Attributed to,Yi Yuanji|Unidentified Artist,"Chinese, died 1066",Chinese,Yi Yuanji|Unidentified Artist,,0966,1066,late 12th century,1167,1199,Fan mounted as an album leaf; ink and color on silk,Image: 9 1/2 in. × 9 in. (24.1 × 22.9 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.285.1,false,true,48967,Asian Art,Hanging scroll,明 倣米萬鍾 行書開襟揮手聯句 軸|Poem,China,Ming dynasty (1368–1644),,,,Artist|Artist,After,Unidentified Artist|Mi Wanzhong,"Chinese, 1570–1628",,Unidentified Artist|Mi Wanzhong,,1570,1628,,1368,1644,Hanging scroll; ink on paper,Image: 134 3/4 x 39 1/8 in. (342.3 x 99.4 cm) Overall: 186 3/4 x 48 in. (474.3 x 121.9 cm) Overall with knobs: 186 3/4 x 53 3/4 in. (474.3 x 136.5 cm),"Edward Elliott Family Collection, Purchase, The Dillon Fund Gift, 1981",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/48967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.18.92,false,true,51704,Asian Art,Handscroll,明 佚名(仿)吳育 新安汪氏譜牒 卷|Portrait of a Member and Record of the Wang Family,China,Ming dynasty (1368-1644),,,,Artist|Artist,after,Unidentified Artist|Wu Yu,"Chinese, 1004–1058",,"Unidentified Artist|Wu, Yu",,1004,1058,,1368,1644,"Handscroll; calligraphy, ink on paper; portrait, ink and color on paper",13 1/8 x 9 3/4 in. (33.3 x 24.8 cm),"From the Collection of A. W. Bahr, Purchase, Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.99d,false,true,51497,Asian Art,Album leaf,,China,Ming (1368–1644)–Qing (1644–1911) dynasty,,,,Artist|Artist,After,Guan Daosheng|Unidentified Artist,1262–1319,,Guan Daosheng|Unidentified Artist,,1262,1319,,1368,1911,Album leaf; ink on silk,10 3/4 x 10 3/4 in. (27.3 x 27.3 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.5,false,true,51419,Asian Art,Handscroll,,China,Ming (1368–1644) or Qing dynasty (1644–1911),,,,Artist|Artist,In the Style of,Guo Zhongshu|Unidentified Artist,"Chinese, died 977",,Guo Zhongshu|Unidentified Artist,,0877,0977,,1571,1599,Handscroll; ink and color on silk,12 1/2 in. × 16 ft. 1 in. (31.8 × 490.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.81.1, .2",false,true,671030,Asian Art,Screens,『伊勢物語』|Forty-nine scenes from the Tales of Ise (Ise monogatari),Japan,Edo period (1615–1868),,,,Calligrapher,Attributed to,Satomura Genchin,"Japanese, 1591–1665",,Satomura Genchin,,1591,1665,mid-17th century,1634,1666,"Pair of six-panel folding screens, with ninety-eight paintings and poem cards (shikishi) applied to gold leaf on paper; paintings: ink and red ink on paper, text: ink on paper",Image (each): 42 1/4 in. × 8 ft. 9 7/8 in. (107.3 × 268.9 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/671030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.247,false,true,671039,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,1759,1759,1759,Hanging scroll; ink on paper,Image: 9 5/16 × 13 7/16 in. (23.7 × 34.1 cm) Overall with mounting: 41 1/4 × 18 3/8 in. (104.7 × 46.7 cm) Overall with knobs: 41 1/4 × 20 5/16 in. (104.7 × 51.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.248,false,true,671043,Asian Art,Album,「大雅堂画弁題詩」|“Paintings by Taigadō with Colophons”,Japan,Edo period (1615–1868),,,,Calligrapher,,Ike Taiga,"Japanese, 1723–1776",", colophons by eight calligraphers",Ike Taiga,,1723,1776,late 18th–early 19th century,1776,1833,Album,Image: 11 7/8 × 16 1/4 in. (30.2 × 41.2 cm) Album: 11 7/8 × 15 7/8 × 1 3/16 in. (30.2 × 40.4 × 3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.237,false,true,687621,Asian Art,Hanging scroll,『古今和歌集』断簡|Three poems from the Collection of Poems Ancient and Modern (Kokin wakashū),Japan,Kamakura period (1185–1333),,,,Calligrapher,Traditionally attributed to,Fujiwara no Tameyori,"Japanese, 939?–998",,Fujiwara no Tameyori,,0939,0998,13th century,1200,1299,Page from a book mounted as a hanging scroll; ink on paper,Image: 9 3/16 × 5 9/16 in. (23.4 × 14.1 cm) Overall with mounting: 54 5/16 × 11 in. (138 × 28 cm) Overall with knobs: 54 5/16 × 13 3/8 in. (138 × 34 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/687621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.240,false,true,670905,Asian Art,Book,"詠歌大概|Manuscript Version of “Fundamentals of Poetic Composition” (Eiga taigai), compiled by Fujiwara no Teika (1162–1241)",Japan,Muromachi period (1392–1573),,,,Calligrapher,,Konoe Taneie,"Japanese, 1503–1566",,Konoe Taneie,,1503,1566,1531,1531,1531,Book of 102 waka by various poets; ink on paper,Image (closed): 10 1/4 × 7 1/16 in. (26 × 18 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/670905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.164a, b",false,true,53455,Asian Art,Album leaves mounted as hanging scrolls,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Ike Taiga|Minagawa Kien,"Japanese, 1723–1776|Japanese, 1734–1807",,Ike Taiga|Minagawa Kien,,1723 |1734,1776 |1807,late 18th–early 19th century,1767,1807,Album leaves mounted as hanging scrolls; ink on paper,Image (a): 9 in. × 14 7/16 in. (22.8 × 36.6 cm) Overall with mounting (a): 39 3/8 × 19 7/16 in. (100 × 49.4 cm) Overall with knobs (a): 39 3/8 × 21 7/16 in. (100 × 54.5 cm) Image (b): 9 5/16 × 14 15/16 in. (23.6 × 38 cm) Overall with mounting (b): 39 3/8 × 19 1/2 in. (100 × 49.6 cm) Overall with knobs (b): 39 3/8 × 21 9/16 in. (100 × 54.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.167a, b",false,true,670542,Asian Art,Album leaves mounted as hanging scrolls,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,,Rokunyo|Ike Taiga,"Japanese, 1737–1801|Japanese, 1723–1776",,Rokunyo|Ike Taiga,,1737 |1723,1801 |1776,late 18th–early 19th century,1767,1833,Album leaves mounted as hanging scrolls; ink on paper,Image (each): 9 × 14 5/8 in. (22.9 × 37.1 cm) Overall with mounting (each): 39 1/4 × 21 1/2 in. (99.7 × 54.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.19,false,true,78056,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,,Kameda Bōsai|Sakai Hōitsu,"Japanese, 1752–1826|Japanese, 1761–1828",,Kameda Bōsai|Sakai Hōitsu,,1752 |1761,1826 |1828,ca. 1821,1821,1821,Hanging scroll; ink on paper,Image: 44 3/8 × 14 3/4 in. (112.7 × 37.5 cm) Overall with mounting: 68 1/4 × 14 3/4 in. (173.4 × 37.5 cm) Overall with knobs: 16 7/8 in. (42.9 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.80,false,true,670935,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,Calligraphy by|Calligraphy by,Kano Tōun|Ichiki Konzan|Saskai Genryō,"Japanese, 1625–1694|Japanese|Japanese, 1650–1723",,Kano Tōun|Ichiki Konzan|Saskai Genryō,,1625 |1650,1694 |1723,1675,1675,1675,Handscroll; ink and color on paper,Image: 10 5/8 in. × 16 ft. 10 1/16 in. (27 × 513.3 cm) Overall with knobs: 11 7/16 in. × 16 ft. 10 1/16 in. (29 × 513.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.141,false,true,53447,Asian Art,Hanging scroll,柳下美人図|Courtesan and her Attendants under a Willow Tree,Japan,Edo period (1615–1868),,,,Artist|Artist|Artist,Inscribed by|Inscribed by,Unchō|Kyokutei Bakin|Kitao Masanobu (Santō Kyōden),"Japanese, active late 18th century|1767–1848|Japanese, 1761–1816",,Unchō|Kyokutei Bakin|Kitao Masanobu (Santō Kyōden),,1740 |1767 |1761,1820 |1848 |1816,1796,1600,1870,"Hanging scroll; ink, color, and gold on silk",36 1/2 x 13 3/8 in. (92.7 x 34 cm) Overall with mounting: 72 13/16 × 18 7/8 in. (185 × 48 cm) Overall with knobs: 72 13/16 × 20 5/8 in. (185 × 52.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.8a–c,false,true,78167,Asian Art,Set of three hanging scrolls,,Japan,Edo period (1615–1868),,,,Calligrapher|Calligrapher|Calligrapher,a)|c),Hiin Tsūyō|Sokuhi Nyoitsu (Jifei Ruyi)|Mokuan Shōtō,"Chinese/ Japanese, 1593–1661|Chinese/ Japanese, 1616–1671|Chinese, 1611–1684",,Hiin Tsūyō|Sokuhi Nyoitsu|Mokuan Shōtō,,1593 |1616 |1611,1661 |1671 |1684,17th century,1600,1699,Set of three hanging scrolls; ink on paper,Image (a (right)): 48 7/8 in. × 11 in. (124.1 × 27.9 cm) Overall with mounting (a (right)): 79 3/4 × 15 3/8 in. (202.6 × 39.1 cm) Overall with knobs (a (right)): 79 3/4 × 17 1/4 in. (202.6 × 43.8 cm) Image (b (center)): 48 7/8 × 14 1/2 in. (124.1 × 36.8 cm) Overall with mounting (b (center)): 79 5/8 × 15 1/2 in. (202.2 × 39.4 cm) Overall with knobs (b (center)): 79 5/8 × 17 3/8 in. (202.2 × 44.1 cm) Image (c (left)): 48 7/8 in. × 11 in. (124.1 × 27.9 cm) Overall with mounting (c (left)): 79 3/4 × 15 3/8 in. (202.6 × 39.1 cm) Overall with knobs (c (left)): 79 3/4 × 17 1/4 in. (202.6 × 43.8 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/78167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.244,false,true,671036,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink on paper,Image: 10 1/16 × 9 1/8 in. (25.5 × 23.2 cm) Overall with mounting: 41 9/16 × 14 3/4 in. (105.5 × 37.4 cm) Overall with knobs: 41 9/16 × 16 5/8 in. (105.5 × 42.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.245,false,true,671037,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink on paper,Image: 9 5/16 × 8 1/8 in. (23.6 × 20.6 cm) Overall with mounting: 42 15/16 × 13 11/16 in. (109 × 34.8 cm) Overall with knobs: 42 15/16 × 15 3/4 in. (109 × 40 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.246,false,true,671038,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink on paper,Image: 9 5/16 × 13 1/8 in. (23.7 × 33.4 cm) Overall with mounting: 40 1/4 × 17 13/16 in. (102.3 × 45.3 cm) Overall with knobs: 40 1/4 × 19 3/4 in. (102.3 × 50.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.195,false,true,670944,Asian Art,Album of fifty-four sketches,,Japan,Edo (1615–1868)–Meiji period (1868–1912),,,,Artist|Artist|Artist,,Tsubaki Chinzan|Watanabe Kazan|Takagi Goan,"Japanese, 1801–1854|Japanese, 1793–1841|Japanese, active 19th century",,Tsubaki Chinzan|Watanabe Kazan|Takagi Goan,,1801 |1793 |1800,1854 |1841 |1899,19th century,1800,1899,Album of fifty-four sketches; ink and color on paper,Album: 11 7/16 × 12 3/8 × 3 13/16 in. (29 × 31.4 × 9.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.166a, b",false,true,670541,Asian Art,Two album leaves mounted as two hanging scrolls,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,,Kameda Bōsai|Ike Taiga,"Japanese, 1752–1826|Japanese, 1723–1776",,Kameda Bōsai|Ike Taiga,,1752 |1723,1826 |1776,18th–19th century,1723,1826,Album leaves mounted as hanging scrolls; ink on paper,Image (a): 8 7/8 × 14 3/4 in. (22.6 × 37.5 cm) Overall with mounting (a): 39 3/8 × 19 1/2 in. (100 × 49.5 cm) Overall with knobs (a): 39 3/8 × 21 1/2 in. (100 × 54.6 cm) Image (b): 9 3/16 × 14 1/2 in. (23.4 × 36.9 cm) Overall with mounting (b): 39 3/8 × 19 9/16 in. (100 × 49.7 cm) Overall with knobs (b): 39 3/8 × 21 7/8 in. (100 × 55.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.165a–c,false,true,670540,Asian Art,Three album leaves mounted as three hanging scrolls,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist|Calligrapher,,Shinozaki Shōchiku|Ike Taiga|Ōkubo Shibutsu,"Japanese, 1781–1851|Japanese, 1723–1776|Japanese, 1766–1837",,Shinozaki Shōchiku|Ike Taiga|Ōkubo Shibutsu,,1781 |1723 |1766,1851 |1776 |1837,18th–19th century,1723,1851,Album leaves mounted as hanging scrolls; ink on paper,Image (a): 8 3/4 × 14 7/16 in. (22.2 × 36.7 cm) Overall with mounting (a): 39 3/16 × 19 1/2 in. (99.5 × 49.5 cm) Overall with knobs (a): 39 3/16 × 21 7/16 in. (99.5 × 54.5 cm) Image (b): 9 1/8 × 14 7/16 in. (23.1 × 36.7 cm) Overall with mounting (b): 39 5/16 × 19 7/16 in. (99.8 × 49.3 cm) Overall with knobs (b): 39 5/16 × 21 7/16 in. (99.8 × 54.5 cm) Image (c): 8 11/16 × 14 7/16 in. (22 × 36.7 cm) Overall with mounting (c): 39 3/8 × 19 7/16 in. (100 × 49.4 cm) Overall with knobs (c): 39 3/8 × 21 1/2 in. (100 × 54.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.73,false,true,58906,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Fang Shi Mopu,ca. 1588,,Fang Shi Mopu,,1588,1588,1721,1721,1721,"Lacquer, roiro, black hiramakie, takamakie; Interior: roiro and gold leaf",3 x 2 15/16 x 13/16 in. (7.6 x 7.5 x 2 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.903,false,true,58827,Asian Art,Inrō,古墨形印籠 (文章司命)|Inrō with Chinese Scholars and Characters,Japan,Edo period (1615–1868),,,,Artist,based on design by,Kitajima Setsuzan,1636–1697,,Kitajima Setsuzan,,1636,1697,early to mid-18th century,1733,1766,"Three cases; lacquered wood with black and brown takamaki-e, togidashimaki-e Netsuke: manjū type, ivory; writing implements and books Ojime: malachite bead",3 9/16 x 1 11/16 x 5/8 in. (9.1 x 4.3 x 1.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.226a, b",false,true,671019,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Nakagawa Tenju,"Japanese, died 1795",,Nakagawa Tenju,,,1795,1803,1803,1803,Two woodblock printed books; ink on paper,Each book 10 1/2 × 7 1/4 in. (26.7 × 18.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/671019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.745a, b",false,true,78647,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Wang Gai,"Chinese, 1645–1710",,Wang Gai,,1645,1710,ca. 1748,1743,1753,Set of two woodblock printed books; ink and color on paper,11 1/8 × 7 1/16 in. (28.3 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.746a–e,false,true,78648,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Wang Gai,"Chinese, 1645–1710",,Wang Gai,,1645,1710,1753,1753,1753,Set of five woodblock printed books; ink and color on paper,each: 11 1/8 × 7 1/8 in. (28.3 × 18.1 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.74.1, .2",false,true,671028,Asian Art,Screens,金山西湖図屏風|Jinshan Island and West Lake,Japan,Edo period (1615–1868),,,,Artist,,Kano Sanraku,"Japanese, 1559–1635",,Kano Sanraku,,1559,1635,1630,1630,1630,"Pair of six-panel folding screens; ink, color, and gold on paper",Image: 60 1/16 in. × 11 ft. 9 in. (152.5 × 358.2 cm) Overall with mounting: 67 5/16 in. × 12 ft. 4 1/4 in. (170.9 × 376.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/671028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.2,false,true,78058,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ōbaku Ingen,"Japanese, 1594–1673",,Ōbaku Ingen,,1594,1673,1615–1868,1615,1868,Hanging scroll; ink on paper,Image: 47 × 11 in. (119.4 × 27.9 cm) Overall with mounting: 80 1/2 × 15 1/2 in. (204.5 × 39.4 cm) Overall with knobs: 80 1/2 × 17 5/8 in. (204.5 × 44.8 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/78058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.77.1, .2",false,true,53009,Asian Art,Screens,四季山水図屏風|Landscapes of the Four Seasons,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,,1602,1674,1630s,1600,1700,Pair of six-panel folding screens; ink and color on paper,Image (each): 60 3/8 in. × 11 ft. 6 7/8 in. (153.4 × 352.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.242a,false,true,54779,Asian Art,Hanging scroll,桜下絵和歌色紙 紀 貫之|Poem by Ki no Tsurayuki (ca. 872–945) on Decorated Paper with Cherry Blossoms,Japan,Edo period (1615–1868),,,,Artist,,Ogata Sōken,"Japanese, 1621–1687",,Ogata Sōken,,1621,1687,mid- to late 17th century,1621,1687,Poem card (shikishi) mounted as a hanging scroll; ink and gold on paper,Image: 8 3/8 × 7 11/16 in. (21.2 × 19.5 cm) Overall with mounting (a): 41 1/4 × 12 1/2 in. (104.8 × 31.7 cm) Overall with knobs (a): 41 1/4 × 14 1/4 in. (104.8 × 36.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/54779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.242b,false,true,727181,Asian Art,Hanging scroll,萩下絵和歌色紙 藤原家隆|Poem by Fujiwara no Ietaka (1158–1237) on Decorated Paper with Bush Clover,Japan,Edo period (1615–1868),,,,Artist,,Ogata Sōken,"Japanese, 1621–1687",,Ogata Sōken,,1621,1687,mid- to late 17th century,1621,1687,Poem card (shikishi) mounted as a hanging scroll; ink and gold on paper,Image: 8 3/8 × 7 5/8 in. (21.2 × 19.4 cm) Overall with mounting (b): 38 1/4 × 11 15/16 in. (97.2 × 30.4 cm) Overall with knobs (b): 38 1/4 × 13 7/8 in. (97.2 × 35.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/727181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.83,false,true,53401,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Hanabusa Itchō,"Japanese, 1652–1724",,Hanabusa Itchō,,1652,1724,after 1709,1700,1850,Six-panel folding screen; ink and color on paper,47 3/4 x 124 1/2 in. (121.3 x 316.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.366,false,true,72416,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Watanabe Shikō,"Japanese, 1683–1755",,Watanabe Shikō,,1683,1755,first half of the 18th century,1700,1750,Two-panel folding screen; ink on paper,Image: 59 7/16 x 66 9/16 in. (151 x 169.1 cm),"Purchase, Friends of Asian Art Gifts, 2004",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/72416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.224a–c,false,true,670927,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Hanabusa Ippō,"Japanese, 1691–1760",,Hanabusa Ippō,,1691,1760,May 1751,1751,1751,Three woodblock printed books; ink on paper,Each book: 10 1/8 × 7 5/16 in. (25.7 × 18.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/670927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.116,false,true,671018,Asian Art,Screen,白拍子・遊女図|Shirabyōshi Dancer and Female Servant; Courtesan and Girl Attendant,Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Settei,"Japanese, 1710–1786",,Tsukioka Settei,,1710,1786,mid-18th century,1734,1766,"Hanging scroll paintings, remounted as a two‑panel folding screen; ink, color, and gold on silk",Image (each): 45 in. × 16 1/2 in. (114.3 × 41.9 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/671018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.157.1, .2",false,true,671023,Asian Art,Screens,山野行旅図屏風|Travels through Mountains and Fields,Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,,1716,1783,ca. 1765,1755,1775,Pair of six-panel folding screens; ink and color on silk,Image (each): 62 1/2 in. × 11 ft. 9 3/4 in. (158.8 × 360 cm) Overall (each): 63 7/8 in. × 11 ft. 11 1/16 in. (162.2 × 363.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/671023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.601,false,true,690313,Asian Art,Hanging scroll,至誠心|Profound Sincerity,Japan,Edo period (1615–1868),,,,Artist,,Jiun Sonja,"Japanese, 1718–1804",,Sonja Jiun,,1718,1804,ca.1780–90,1775,1800,Hanging scroll; ink and color on paper,Image: 44 × 17 7/8 in. (111.8 × 45.4 cm) Overall with mounting: 72 1/2 × 23 1/2 in. (184.2 × 59.7 cm) Overall with knobs: 26 1/2 in. (67.3 cm),"Gift of Joan B. Mirviss, in memory of T. Richard Fishbein, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/690313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.66,false,true,74363,Asian Art,Screen,"楓橋夜泊|Calligraphy of a Tang-dynasty poem, ""Maple Bridge Night Mooring""",Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,ca. 1770,1760,1780,Two-panel folding screen; ink on paper,Image (each panel): 53 1/4 x 22 1/8 in. (135.3 x 56.2 cm) Overall: 68 3/4 x 72 3/4 in. (174.6 x 184.8 cm),"Purchase, Friends of Asian Art Gifts, 2008",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/74363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1987.81a, b",false,true,44865,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,After,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,probably 19th century,1800,1899,"Two-panel screen, originally sliding-door panels; ink on paper",65 1/2 x 70 1/2 in. (166.4 x 179 cm),"Purchase, The Charles Engelhard Foundation Gift, 1987",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.163.1, .2",false,true,53456,Asian Art,Folding screens,蘭亭曲水図屏風. 秋社図屏風|Orchid Pavilion Gathering; Autumn Landscape,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,ca. 1763,1600,1870,Pair of six-panel folding screens; ink and color on paper,Image (each): 63 1/4 in. × 11 ft. 8 3/16 in. (160.7 × 356 cm) Overall with mounting: 69 9/16 in. × 12 ft. 2 7/16 in. (176.7 × 372 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.672,false,true,78593,Asian Art,Illustrated book,絵本松のしらべ|Picture Book on the Music of the Pine Trees (Ehon matsu no shirabe),Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,,1726,1792,1795,1795,1795,Woodblock printed book; ink and color on paper,9 5/16 × 6 11/16 in. (23.7 × 17 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.197.1, .2",false,true,671024,Asian Art,Screens,芦雁図屏風; 柳に水上月図屏風|Goose and Reeds; Willows and Moon,Japan,Edo period (1615–1868),,,,Artist,,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,,1733,1795,right screen: 1774; left screen: 1793,1774,1793,"Pair of six-panel folding screens; ink, color and gold on paper",Each: Image: 60 9/16 in. × 11 ft. 7 1/2 in. (153.9 × 354.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/671024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.206.1, .2",false,true,53406,Asian Art,Screens,山樵漁夫図屏風|Woodcutters and Fishermen,Japan,Edo period (1615–1868),,,,Artist,,Matsumura Goshun,"Japanese, 1752–1811",,Matsumura Goshun,,1752,1811,ca. 1790–95,1600,1800,Pair of six-panel folding screens; ink and color on paper,Image (each): 65 15/16 in. × 12 ft. 2 7/16 in. (167.5 × 372 cm) Overall with mounting: 67 1/2 in. × 12 ft. 4 in. (171.5 × 376 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.889,false,true,78779,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Masanobu (Santō Kyōden),"Japanese, 1761–1816",,Kitao Masanobu (Santō Kyōden),,1761,1816,1786,1786,1786,Woodblock printed book; ink and color on paper,10 7/16 × 7 1/16 in. (26.5 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.93.1, .2",false,true,53423,Asian Art,Screens,,Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,,1761,1828,ca. 1805,1720,1920,"Pair of six-panel folding screens; ink, color, and gold on gilded paper",Image: 38 x 82 3/16 in. (96.5 x 208.8 cm) Overall with mounting: 39 9/16 × 83 3/4 in. (100.5 × 212.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.225a–h,false,true,670895,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,,1763,1840,1809,1809,1809,Eight woodblock printed books; ink and color on paper,Each book: 9 1/4 × 6 1/4 in. (23.5 × 15.9 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/670895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.667,false,true,78588,Asian Art,Illustrated book,融齋畫譜|Yūsai Picture Album (Yūsai gafu),Japan,Edo period (1615–1868),,,,Artist,,Nakabayashi Chikutō,"Japanese, 1776–1853",,Nakabayashi Chikutō,,1776,1853,1831,1831,1831,"Woodblock printed book (orihon, accordion-style); ink and color on paper",10 15/16 × 6 5/16 in. (27.8 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2016.254.1, .2",false,true,640049,Asian Art,Folding screens,四季琵琶湖図屏風|Lake Biwa in Four Seasons,Japan,Edo period (1615–1868),,,,Artist,,Nukina Kaioku,"Japanese, 1778–1863",,Nukina Kaioku,,1778,1863,1834,1834,1834,Pair of six-panel folding screens; ink and color on paper,Image: 24 9/16 × 60 5/8 in. (62.4 × 154 cm) Overall with mounting: 67 1/2 × 25 3/8 in. (171.4 × 64.5 cm),"Gift of Robert and Betsy Feinberg, 2016",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/640049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.892a–c,false,true,78782,Asian Art,Illustrated book,天の浮橋|Floating Bridge of Heaven (Ama no ukihashi),Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,,1787,1832,ca. 1830s,1830,1839,Set of three woodblock printed books; ink and color on paper,each: 9 13/16 × 7 1/16 in. (25 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.763a, b",false,true,78665,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,,1797,1861,ca. 1850,1845,1855,Set of two woodblock printed books; ink and color on paper,each: 7 1/16 × 4 3/4 in. (18 × 12 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.717,false,true,58488,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Matsuda Sukenaga,"Japanese, 1800–1871",,"Sukenaga, Matsuda",,1800,1871,early–mid 19th century,1823,1866,"Lacquer, carved wood imitating leather, gold metal clasp; Interior: plain",H. 3 1/16 in. (7.7 cm); W. 1 15/16 in. (5 cm); D. 1 1/8 in. (2.8 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB98a, b",false,true,57785,Asian Art,Illustrated book,池田孤邨画 『抱一上人真蹟鏡』|Mirror of Genuine Work of Monk Hōitsu (Hōitsu shōnin shinseki kagami),Japan,Edo period (1615–1868),,,,Artist,,Ikeda Koson,"Japanese, 1803–1868",,Ikeda Koson,,1803,1868,1817,1817,1817,Set of two woodblock printed books; ink and color on paper,Image (a): 10 1/16 x 7 3/16 x 5/16 in. (25.5 x 18.2 x 0.8 cm) Image (b): 10 1/16 x 7 3/16 x 5/16 in. (25.6 x 18.3 x 0.8 cm) Overall: 13 3/4 in. (35 cm) (open for both volumes),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB71,false,true,57669,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Ōishi Shuga,"Japanese, born 1752 (?)",,Ōishi Shuga,,1752,1852,Spring 1822,1822,1822,Ink and color on paper,6 7/8 × 9 1/4 × 5/8 in. (17.5 × 23.5 × 1.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB61,false,true,57661,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kokan,late 17th–early 18th century,,Kokan,,1650,1750,1724,1724,1724,Ink on paper,10 5/8 × 7 1/8 × 3/8 in. (27 × 18.1 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.222,false,true,53008,Asian Art,Paintings mounted on folding screen,,Japan,Edo period (1615–1868),,,,Artist,,Soga Nichokuan,"Japanese, active mid-17th century",,Soga Nichokuan,,1600,1700,mid-17th century,1600,1700,Pair of fan-shaped paintings mounted on two-panel folding screen; ink on paper,73 1/2 x 67 3/4 in. (186.7 x 172.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1999.204.1, .2",false,true,50843,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Kano Sanboku,"Japanese, active late 17th–early 18th century",,Kano Sanboku,,1671,1725,late 17th century,1667,1699,"Pair of six-panel folding screens; ink, color, and gold on paper",Image: 59 in. x 12 ft. 1/2 in. (149.9 x 367 cm),"Purchase, Friends of Asian Art Gifts, 1999",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/50843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.745,false,true,78458,Asian Art,Handscroll,"白居易作「醉吟先生傳」断簡|Excerpts from Bai Juyi's ""Biography of a Master of Drunken Poetry"" (Suigin sensei den)",Japan,Heian period (794–1185),,,,Artist,,Fujiwara no Yukinari (Kōzei),"Japanese, 972–1027",,Fujiwara no Yukinari,,0972,1027,early 11th century,1000,1027,Detached section of a handscroll mounted as a hanging scroll; ink on paper,Image: 10 13/16 × 3 3/8 in. (27.5 × 8.6 cm) Overall with mounting: 53 1/8 × 11 1/16 in. (134.9 × 28.1 cm) Overall with knobs: 53 1/8 × 12 7/8 in. (134.9 × 32.7 cm),"Gift of Raymond and Priscilla Vickers, 2016",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/78458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.232,false,true,670938,Asian Art,"Page from book, mounted as hanging scroll",,Japan,Heian period (794–1185),,,,Artist,Traditionally attributed to,Fujiwara no Sadayori,"Japanese, 995–1045",,Fujiwara no Sadayori,,0995,1045,early 12th century,1100,1133,"Page from book, mounted as hanging scroll; ink on paper",Image: 8 1/8 in. × 5 in. (20.6 × 12.7 cm) Overall with mounting: 48 7/16 × 14 3/16 in. (123 × 36 cm) Overall with knobs: 48 7/16 × 15 11/16 in. (123 × 39.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/670938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.235,false,true,671049,Asian Art,"Page from book, mounted as hanging scroll","『三宝絵詞』断簡 (東大寺切)|Page from Illustrations and Explanations of the Three Jewels (Sanbō e-kotoba), one of the “Tōdaiji Fragments” (Tōdaiji-gire)",Japan,Heian period (794–1185),,,,Artist,Calligraphy attributed to,Minamoto no Toshiyori,"Japanese, 1055–1129",,Minamoto no Toshiyori,,1055,1129,1120,1120,1120,Page from a book; ink on decorated paper,Image: 9 1/4 × 5 7/8 in. (23.5 × 15 cm) Overall with mounting: 49 3/16 × 13 3/8 in. (125 × 34 cm) Overall with knobs: 49 3/16 × 15 3/8 in. (125 × 39 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.6,false,true,78166,Asian Art,Hanging scroll,"伝源俊頼 『三宝絵詞』 (東大寺切)|Page from the Illustrations and Explanations of the Three Jewels (Sanbō ekotoba), known as the Tōdaiji Fragment (Tōdaiji-gire)",Japan,Heian period (794–1185),,,,Artist,Calligraphy attributed to,Minamoto no Toshiyori,"Japanese, 1055–1129",,Minamoto no Toshiyori,,1055,1129,1120,1120,1120,Detached page from a book mounted as a hanging scroll; ink on mica paper,Image: 9 1/2 x 5 3/8 in. (24.1 x 13.7 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/78166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.233,false,true,671014,Asian Art,"Page from book, mounted as hanging scroll",,Japan,Heian period (794–1185),,,,Artist,Traditionally attributed to,Monk Saigyō,"Japanese, 1118–1190",,,,1118,1190,late 12th century,1167,1185,"Page from book, mounted as hanging scroll; ink on paper",Image: 6 7/8 × 5 11/16 in. (17.4 × 14.5 cm) Overall with mounting: 51 5/16 × 14 1/2 in. (130.3 × 36.8 cm) Overall with knobs: 51 5/16 × 16 1/4 in. (130.3 × 41.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.2.53a–g,false,true,40491,Asian Art,Box,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,,1807,1891,second half of the 19th century,1850,1899,"Gold maki-e on black and brown lacquer, with mother-of-pearl inlay and pewter",L. 9 5/8 in. (24.4 cm); W. 9 3/8 in. (23.8 cm); H. 16 1/2 in. (41.9 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/40491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.2.54a–o,false,true,40498,Asian Art,Writing box; table,2代由木尾雪雄作|Writing Box (Suzuribako) and Writing Table (Bundai) with Pines at Takasago and Sumiyoshi,Japan,Meiji period (1868–1912),,,,Artist,,Yukio Yukio II,"Japanese, 1860–1929",,Yukio Yukio II,,1860,1929,early 20th century,1900,1912,"Lacquered wood with gold, silver takamaki-e, hiramaki-e, cut-out gold foil on nashiji ground, silver inlay, silver fittings",Writing box: H. 2 in.; W. 9 in.; L. 9 3/4 in. Table: H. 4 7/8 in.; W.14 1/2 in.; L. 24 in.,"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/40498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.228,false,true,670953,Asian Art,illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa (Baidō) Kokunimasa,"Japanese, 1874–1944",,Utagawa (Baidō) Kokunimasa,,1874,1944,1879,1879,1879,Woodblock printed book; ink and color on paper,Image: 8 1/8 × 5 1/2 in. (20.6 × 14 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/670953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.585,false,true,688515,Asian Art,Folding screen,,Japan,Taishō period (1912–26),,,,Artist,,Imazu Tatsuyuki,"Japanese, active early 20th century",,Imazu Tatsuyuki,,1900,1926,ca. 1925,1915,1926,Two-panel folding screen; mineral colors and metallic powders on paper,Image: 80 1/8 × 72 13/16 in. (203.5 × 185 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/688515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.236,false,true,670885,Asian Art,Hanging scroll,"『続古今和歌集』断簡|Two Poems from the Collection of Poems Ancient and Modern, Continued (Zoku kokin wakashū)",Japan,Kamakura period (1185–1333),,,,Artist,,Nun Abutsu,"Japanese, died 1283",,Abutsu,,,1283,13th century,1200,1299,Page from a book; ink on paper,Image: 9 1/4 × 5 1/2 in. (23.5 × 14 cm) Overall with mounting: 54 5/16 × 11 in. (138 × 27.9 cm) Overall with knobs: 54 5/16 × 13 1/2 in. (138 × 34.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/670885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.3,false,true,60437,Asian Art,Hanging scroll,明恵房高弁筆 『夢の記』|Section of the Dream Diary (Yume no ki) with a Sketch of Mountains,Japan,Kamakura period (1185–1333),,,,Artist,,Myōe Kōben,"Japanese, 1173–1232",,Myōe Kōben,,1173,1232,ca. 1203–10,1203,1210,Hanging scroll; ink on paper,Image: 12 x 19 in. (30.5 x 48.3 cm) Overall with mounting: 44 5/8 x 19 5/8 in. (113.3 x 49.8 cm) Overall with knobs: 44 5/8 x 21 1/2 in. (113.3 x 54.6 cm),"Gift of Sylvan Barnet and William Burto, in honor of Saretta and Howard Barnet, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.4,false,true,60435,Asian Art,Hanging scroll,明恵房高弁筆 『夢の記』 断簡|Section of the Dream Diary (Yume no ki),Japan,Kamakura period (1185–1333),,,,Artist,,Myōe Kōben,"Japanese, 1173–1232",,Myōe Kōben,,1173,1232,dated 1225,1225,1225,Hanging scroll; ink on paper,Image: 13 1/4 x 21 5/8 in. (33.7 x 54.9 cm) Overall with mounting: 48 x 27 in. (121.9 x 68.6 cm) Overall with knobs: 48 x 29 1/8 in. (121.9 x 74 cm),"Gift of Sylvan Barnet and William Burto, in honor of Saretta and Howard Barnet, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.5,false,true,60451,Asian Art,Hanging scroll,,Japan,Kamakura period (1185–1333),,,,Artist,,Myōe Kōben,"Japanese, 1173–1232",,Myōe Kōben,,1173,1232,ca. 1221,1221,1221,Hanging scroll; ink on paper,Image: 8 1/4 x 17 in. (21 x 43.2 cm) Overall with mounting: 39 7/8 x 21 1/16 in. (101.3 x 53.5 cm) Overall with knobs: 39 7/8 x 22 7/8 in. (101.3 x 58.1 cm),"Gift of Sylvan Barnet and William Burto, in honor of Saretta and Howard Barnet, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.73.1, .2",false,true,53240,Asian Art,Screens,,Japan,Momoyama period (1573–1615),,,,Artist,,Unkoku Tōgan,"Japanese, 1547–1618",,Unkoku Tōgan,,1547,1618,late 16th–early 17th century,1573,1615,"Pair of six-panel folding screens; ink, color, and gold dust on paper",Image (each): 61 15/16 in. × 11 ft. 8 1/4 in. (157.3 × 356.2 cm) Overall with mounting: 68 15/16 in. × 12 ft. 3 7/8 in. (175.1 × 375.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.261,false,true,72569,Asian Art,Calligraphy,,Japan,Momoyama period (1573–1615),,,,Artist,,Konoe Nobutada,"Japanese, 1565–1614",,Konoe Nobutada,,1565,1614,early 17th century,1600,1633,Hanging scroll; ink on paper,Image: 16 x 12 in. (40.6 x 30.5 cm),"Gift of Tomohiko and Kyoko Horie, 2004",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/72569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.70.1, .2",false,true,72585,Asian Art,Screens,,Japan,Momoyama period (1573–1615),,,,Artist,Circle of,Kano Mitsunobu,"Japanese, ca. 1561–1608",,Kano Mitsunobu,,1561,1608,late 16th century,1567,1599,"Pair of six-panel folding screens; ink, color, and gold leaf on paper",Each: 59 15/16 in. × 11 ft. 7 7/16 in. (152.3 × 354.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/72585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.718,false,true,60472,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,,Zekkai Chūshin,"Japanese, 1336–1405",,Zekkai Chūshin,,1336,1405,14th century,1392,1399,Hanging scroll; ink on paper,Image: 12 1/4 × 16 5/16 in. (31.1 × 41.4 cm) Overall with mounting: 45 3/8 × 21 1/2 in. (115.3 × 54.6 cm) Overall with knobs: 45 3/8 × 23 3/4 in. (115.3 × 60.3 cm),"Gift of Sylvan Barnet and William Burto, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.10,false,true,60469,Asian Art,Hanging scroll,策彦周良筆 墨跡|Account of the Three Springs of Jiangsu Province in China,Japan,Muromachi period (1392–1573),,,,Artist,,Sakugen Shūryō,"Japanese, 1501–1579",,Sakugen Shūryō,,1501,1579,late 16th century,1567,1573,Hanging scroll; ink on paper,Image: 11 1/2 x 16 3/4 in. (29.2 x 42.5 cm) Overall with mounting: 44 1/4 x 21 7/8 in. (112.4 x 55.6 cm) Overall with knobs: 44 1/4 x 23 3/4 in. (112.4 x 60.3 cm),"Gift of Sylvan Barnet and William Burto, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.241,false,true,671054,Asian Art,Book mounted as handscroll,"柳江書 連歌集『老葉』より「旅」|Manuscript Version of the “Travel” Section of the Linked Verse (Renga) Collection “Aged Leaves” (Wakuraba), compiled by Sōgi (1421–1502)",Japan,Muromachi period (1392–1573),,,,Artist,Calligraphy by,Ryūkō,"Japanese, active 16th century",,Ryūkō,,1500,1573,1533,1533,1533,Book mounted as handscroll; ink on paper,Overall: 9 1/16 in. × 10 ft. 15/16 in. (23 × 307.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/671054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.51.1, .2",false,true,53223,Asian Art,Screens,山水図屏風|Landscape after Xia Gui,Japan,Muromachi period (1392–1573),,,,Artist,Traditionally attributed to,Tenshō Shūbun,"Japanese, active 1414–before 1463",,Tenshō Shūbun,,1414,1463,early–mid-15th century,1414,1463,Two six-panel folding screens; ink and color on paper,60 5/8 in. × 9 ft. 6 3/16 in. (154 × 290 cm) Overall with mounting: 68 in. × 9 ft. 11 11/16 in. (172.7 × 304 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.7,false,true,60470,Asian Art,Hanging scroll,墨蹟|Poem on the Theme of a Monk’s Life,Japan,Nanbokuchō period (1336–92),,,,Artist,,Sesson Yūbai,"Japanese, 1290–1346",,Sesson Yūbai,,1290,1346,14th century,1336,1392,Hanging scroll; ink on paper,Image: 16 x 23 3/8 in. (40.6 x 59.4 cm) Overall with mounting: 51 x 29 1/8 in. (129.5 x 74 cm) Overall with knobs: 51 x 31 3/16 in. (129.5 x 79.2 cm),"Gift of Sylvan Barnet and William Burto, in honor of Miyeko Murase, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.8,false,true,60473,Asian Art,Hanging scroll,絶海中津筆 「山空松子落」|“The Mountain is Empty; A Pinecone Falls”,Japan,Nanbokuchō period (1336–92),,,,Artist,,Zekkai Chūshin,"Japanese, 1336–1405",,Zekkai Chūshin,,1336,1405,late 14th century,1367,1405,Hanging scroll; ink on paper,Image: 34 1/2 x 8 9/16 in. (87.6 x 21.7 cm) Overall with mounting: 65 7/8 x 9 7/16 in. (167.3 x 24 cm) Overall with knobs: 65 7/8 x 11 3/8 in. (167.3 x 28.9 cm),"Gift of Sylvan Barnet and William Burto, in honor of Elizabeth ten Grotenhuis and Merton Flemings, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.289a–g,false,true,53416,Asian Art,Box,里芋菊蒔絵重箱|Stacked Food Box (Jūbako) with Taro Plants and Chrysanthemums,Japan,late Edo (1615–1868)–early Meiji (1868–1912) period,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,,1807,1891,mid-19th century,1834,1866,"Lacquered wood, gold and silver hiramaki-e, takamaki-e, and colored togidashimaki-e",H. 16 1/2 in. (41.9 cm); W. 9 in. (22.9 cm); D. 9 5/8 in. (24.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/53416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3284,false,true,53345,Asian Art,Woodblock print,Hakurai taizo no zu|View of the Large Imported Elephant,Japan,Edo period (1615–1868),,,,Artist,,Taguchi (Utagawa) Yoshimori,1830–1884,,Taguchi (Utagawa) Yoshimori,,1830,1884,"1863 (Bunkyo 3, 4th month)",1863,1863,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 10 3/16 in. (37.5 × 25.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1036,false,true,54321,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shuntei,"Japanese, 1770–1820",,Katsukawa Shuntei,,1770,1820,ca. 1815,1795,1825,Polychrome woodblock print (surimono); ink and color on paper,7 5/8 x 8 7/8 in. (19.4 x 22.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1933,false,true,54473,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shuntei,"Japanese, 1770–1820",,Katsukawa Shuntei,,1770,1820,ca. 1816,1806,1826,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 7 in. (19.7 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2060,false,true,54840,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shuntei,"Japanese, 1770–1820",,Katsukawa Shuntei,,1770,1820,ca. 1815,1805,1825,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 7 1/4 in. (19.7 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2336,false,true,54120,Asian Art,Print,花魁と梅盆栽|Courtesan and her Child Attendant with a Potted Plum Tree,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shuntei,"Japanese, 1770–1820",,Katsukawa Shuntei,,1770,1820,ca. 1815,1805,1825,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2910,false,true,56025,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shuntei,"Japanese, 1770–1820",,Katsukawa Shuntei,,1770,1820,1770–1820,1770,1820,Polychrome woodblock print; ink and color on paper,14 7/8 x 9 7/8 in. (37.8 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2167,false,true,55076,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Taisosai Hokushu,"Japanese, 18th–19th century",,Taisosai Hokushu,,1700,1899,probably 1819,1819,1819,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/4 in. (20.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3324,false,true,53702,Asian Art,Print,外国人之図|Views of Foreigners (Gaikokujin no zu),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitomi,"Japanese, active mid-19th century",,Utagawa Yoshitomi,,1836,1870,1861,1861,1861,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 10 in. (25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3329,false,true,55477,Asian Art,Print,Ikiutsushi Americajin no zu|生写亜墨利加人之図|An American Drawn from Life,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitomi,"Japanese, active mid-19th century",,Utagawa Yoshitomi,,1836,1870,1861 (2nd month),1861,1861,Polychrome woodblock print; ink and color on paper,14 1/4 x 10 in. (36.2 x 25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.235,false,true,73526,Asian Art,Woodblock print,Oroshia|Russian Horseman,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitomi,"Japanese, active mid-19th century",,Utagawa Yoshitomi,,1836,1870,"10th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 1/2 in. (37.5 x 26.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.236,false,true,73527,Asian Art,Print,Orandasen|Dutch Ship,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitomi,"Japanese, active mid-19th century",,Utagawa Yoshitomi,,1836,1870,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.227a–f,false,true,53449,Asian Art,Prints,『諸國六玉川』|Six Tamagawa Rivers from Various Provinces (Shokoku Mu Tamagawa),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",,1797,1858,1857,1750,1900,Six polychrome woodblock prints; ink and color on paper,Image (each): 14 1/4 × 9 5/8 in. (36.2 × 24.4 cm) Mat (each): 22 13/16 × 15 9/16 in. (58 × 39.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.197,false,true,45917,Asian Art,Vase,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,(Zengoro Hozen),Eiraku Hozen,,1795,1854,1820,1820,1820,White porcelain decorated with gold on an iron red ground,H. 3 5/8 in. (9.2 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.297,false,true,47986,Asian Art,Bowl,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1830,1830,1830,"White porcelain; reproduction of Chinese Song, Dingyao bowl (Kairakuen ware)",H. 1 3/8 in. (3.5 cm); Diam. of rim 3 7/8 in. (9.8 cm); Diam. of base 1 1/4 in. (3.2 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.427,false,true,46697,Asian Art,Washer,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Clay covered with colored and transparent glazes over relief (Kyoto ware),H. 5 7/8 in. (14.9 cm); Diam. of rim 7 1/4 in. (18.4 cm); Diam. of base 3 7/8 in. (9.8 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.191a, b",false,true,44450,Asian Art,Jar with lid,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1820,1820,1820,White porcelain decorated with gold and iron red (Kyoto ware),H. 2 3/4 in. (7 cm); Diam. 2 3/4 in. (7 cm); Diam. of rim 1 1/8 in. (2.9 cm); Diam. of base 1 1/4 in. (3.2 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/44450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.203a, b",false,true,46506,Asian Art,Wine kettle,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,(Zengoro Hozen),Eiraku Hozen,,1795,1854,1810,1810,1810,White porcelain decorated with iron red and gold (Kyoto ware),H. 5 1/4 in. (13.3 cm); W. at spout 6 3/8 in. (16.2 cm); Diam. 5 in. (12.7 cm); Diam. of base 3 in. (7.6 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.381a, b",false,true,46497,Asian Art,Incense box,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Clay covered inside with a transparent crackled glaze and outside with polychrome glazes (Kyoto ware),H. 2 1/2 in. (6.4 cm); Diam. 1 3/4 in. (4.4 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.63,false,true,62883,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Ichigen,died 1722,,Ichigen,,1622,1722,ca. 1720,1710,1730,"Clay covered with a pitted, black glaze (Raku ware)",H. 3 3/8 in. (8.6 cm); Diam. 4 1/4 in. (10.8 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.216,false,true,62794,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1790,1790,1790,"Clay covered with a fine crackled glaze; decorated with white slip, colored enamels and gold and silver (Kyoto ware)",H. 3 1/4 in. (8.3 cm); Diam. of rim 5 in. (12.7 cm); Diam. of base 1 7/8 in. (4.8 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.251,false,true,63023,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain decorated with enamel and gold (Kyoto ware),H. 2 in. (5.1 cm); W. 6 3/8 in. (16.2 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.253,false,true,63025,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain decorated with enamel and gold (Kyoto ware),H. 3 1/4 in. (8.3 cm); W. sq. 5 3/4 in. (14.6 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.259,false,true,63027,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain glazed with recessed design in enamels (Kyoto ware),H. 3 5/8 in. (9.2 cm); Diam. 6 3/4 in. (17.1 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.261,false,true,63028,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain decorated in enamels and blue under the glaze,H. 3 1/8 in. (7.9 cm); Diam. 5 1/8 in. (13 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.265,false,true,63029,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain decorated with glaze and gold (Kyoto ware),H. 4 1/8 in. (10.5 cm); Diam. 7 in. (17.8 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.266,false,true,63030,Asian Art,Cup,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Stoneware with lustrous glaze; the decoration in enamels (Kyoto ware),H. 3 1/2 in. (8.9 cm); Diam. 3 1/2 in. (8.9 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.270,false,true,47332,Asian Art,Vase,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain; white glaze covered with design in high relief (Kyoto ware),H. 3 3/16 in. (8.1 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.271,false,true,63031,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain decorated with red under the glaze; bottom unglazed (Kyoto ware),H. 2 3/8 in. (6 cm); Diam. 4 1/4 in. (10.8 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.272,false,true,48577,Asian Art,Vase,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,"Faience with glaze, the design incised and gilt (Kyoto ware)",H. 14 1/4 in. (36.2 cm),"Ex coll.: V. Everit Macy, Gift of Mrs. Everit Macy",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/48577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.380,false,true,47195,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1830,1830,1830,"Faience(?) covered with rich green glaze, craquelé (Kyoto ware)",H. 3 1/16 in. (7.8 cm); Diam. 5 3/4 in. (14.6 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.635,false,true,45354,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,ca. 1825,1815,1835,Clay decorated with crackled glaze and enamels and gold (Kyo ware),H. 2 3/4 in. (7 cm); Diam. of rim 4 7/8 in. (12.4 cm); Diam. of base 1 7/8 in. (4.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.503,false,true,63157,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,ca. 1830,1820,1840,Clay with creamy white glaze (Eiraku pottery),H. 3 3/8 in. (8.6 cm); Diam. 4 7/8 in. (12.4 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"25.60.31a, b",false,true,63087,Asian Art,Tea jar,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,ca. 1820,1810,1830,Pottery with design in relief and colored enamels; pewter cover (Kyoto ware),H. 6 in. (15.2 cm); Diam. 5 in. (12.7 cm),"Rogers Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.159a, b",false,true,47074,Asian Art,Bottle,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1790,1790,1790,Paste decorated with polychrome and transparent enamels (Kyoto ware),H. 5 3/4 in. (14.6 cm); Diam. 4 in. (10.2 cm); Diam. of rim 1 1/4 in. (3.2 cm); Diam. of base 2 1/2 in. (6.4 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.199a, b",false,true,47421,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,(Zenichiro),Eiraku Hozen,,1795,1854,1840,1840,1840,"White porcelain decorated with blue under the glaze, polychrome enamels (Kyoto ware)",H. 6 1/2 in. (16.5 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"23.225.252a, b",false,true,63024,Asian Art,Covered box,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1800,1800,1800,Porcelain decorated with enamel and gold (Kyoto ware),H. 2 3/8 in. (6 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"23.225.275a, b",false,true,47362,Asian Art,Censer,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,1820,1820,1820,Faience with enamels (Kyoto ware),H. 5 1/8 in. (13 cm); Diam. 4 1/4 in. (10.8 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.78,false,true,62902,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Ichinyu,"Japanese, died 1682",,Ichinyu,,1582,1682,ca. 1675,1665,1685,Clay covered with a black glaze dappled with red (Raku ware),H. 3 in. (7.6 cm); Diam. 4 1/2 in. (11.4 cm),"Rogers Fund, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.79,false,true,62903,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Ichinyu,"Japanese, died 1682",,Ichinyu,,1582,1682,ca. 1675,1665,1685,Clay covered with a dull black glaze (Raku ware),H. 3 1/4 in. (8.3 cm); Diam. 4 1/4 in. (10.8 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.269,false,true,77863,Asian Art,Dish,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,,1663,1743,early 18th century,1700,1733,Glazed stoneware with enamels,H. 2 1/16 in. (5.2 cm); W. 14 7/16 in. (36.7 cm); L. 12 7/16 in. (31.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/77863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.500.9.28a, b",false,true,667248,Asian Art,Incense container,,Japan,Edo period (1615–1868),,,,Artist,,Miyagawa Chozo,"Japanese, 1797–1860",,Miyagawa Chozo,,1797,1860,1797–1860,1797,1860,Stoneware with polychrome enamels,H. 1 13/16 in. (4.6 cm); Diam. 2 13/16 in. (7.2 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/667248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.9,false,true,78156,Asian Art,Hanging scroll,,Japan,Momoyama (1573–1615),,,,Artist,,Unkoku Tōgan,"Japanese, 1547–1618",,Unkoku Tōgan,,1547,1618,early 17th century,1600,1618,Hanging scroll; ink on paper,Image: 39 1/2 x 14 1/8 in. (100.3 x 35.9 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.68a, b",false,true,53237,Asian Art,Hanging scrolls,四季花鳥図|Pheasants among Trees: Flowers of the Four Seasons,Japan,Muromachi (1392–1573),,,,Artist,,Kano Shōei,"Japanese, 1519–1592",,Kano Shōei,,1519,1592,probably 1560s,1560,1569,Pair of hanging scrolls; ink and color on paper,Image (a): 37 3/8 × 18 3/8 in. (95 × 46.6 cm) Overall with mounting (a): 84 3/4 × 26 5/8 in. (215.2 × 67.6 cm) Overall with knobs (a): 84 3/4 × 28 15/16 in. (215.2 × 73.5 cm) Image (b): 37 3/8 × 18 3/8 in. (95 × 46.6 cm) Overall with mounting (b): 84 15/16 × 26 5/8 in. (215.8 × 67.7 cm) Overall with knobs (b): 84 15/16 × 28 15/16 in. (215.8 × 73.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.92,false,true,48988,Asian Art,Hanging scroll,雪竹図|Bamboo and Rock in Snow,Japan,Edo period (1615–1868),,,,Artist,,Sakaki Hyakusen,1697–1752,,Sakaki Hyakusen,,1697,1752,spring 1750,1750,1750,Hanging scroll; ink on paper,Image: 53 1/8 x 16in. (134.9 x 40.6cm) Overall with mounting: 80 7/8 x 19 5/8 in. (205.4 x 49.8 cm) Overall with rollers: 80 7/8 x 21 7/8 in. (205.4 x 55.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.155,false,true,53451,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sakaki Hyakusen,1697–1752,,Sakaki Hyakusen,,1697,1752,1744,1620,1880,Hanging scroll; ink and color on paper,47 3/4 × 19 3/4 in. (121.3 × 50.2 cm) 76 9/16 × 27 3/16 in. (194.5 × 69 cm) Overall with knobs: 76 9/16 × 27 3/8 in. (194.5 × 69.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.180,false,true,53461,Asian Art,Folding fan mounted,,Japan,Edo period (1615–1868),,,,Artist,,Aoki Mokubei,1767–1833,,Aoki Mokubei,,1767,1833,1825,1700,1899,Folding fan mounted as a hanging scroll; ink and color on paper,Image: 8 1/4 × 19 7/16 in. (21 × 49.4 cm) Overall with mounting: 39 3/8 × 25 1/16 in. (100 × 63.7 cm) Overall with knobs: 39 3/8 × 27 7/16 in. (100 × 69.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.95,false,true,45229,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,after 1848,1849,1854,Hanging scroll; ink on paper,Image: 38 1/2 x 11 1/4 in. (97.8 x 28.6 cm) Overall with mounting: 68 1/2 x 11 3/4 in. (174 x 29.8 cm) Overall with knobs: 68 1/2 x 14 in. (174 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.524,false,true,48974,Asian Art,Hanging scroll,芥子図|Poppies,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kitagawa Sōsetsu,active 1639–50,,Kitagawa Sōsetsu,,1639,1650,mid-17th century,1634,1666,Hanging scroll; color and gold on paper,34 1/4 x 14 5/8 in. (87 x 37.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.34a, b",false,true,53892,Asian Art,Albums,,Japan,Edo period (1615–1868),,,,Artist,,Tosa Mitsunori,"Japanese, 1583–1638",,Tosa Mitsunori,,1583,1638,early 17th century,1600,1700,"Two albums, thirty leaves in each; ink, red pigment, and gold on paper",Image (each leaf): 5 5/16 × 5 1/8 in. (13.5 × 13 cm) Album: 7 1/8 × 6 1/8 × 1 3/16 in. (18.1 × 15.6 × 3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.221,false,true,671056,Asian Art,Hanging scroll,周茂叔愛蓮図|Zhou Maoshu Admiring Lotuses,Japan,Edo period (1615–1868),,,,Artist,,Kaihō Yūsetsu,"Japanese, 1598–1677",,Kaihō Yūsetsu,,1598,1677,mid-17th century,1634,1666,Hanging scroll; ink on silk,Image: 12 3/4 × 19 5/8 in. (32.4 × 49.8 cm) Overall with mounting: 47 3/16 × 24 7/16 in. (119.8 × 62 cm) Overall with knobs: 47 3/16 × 26 1/4 in. (119.8 × 66.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.78,false,true,53010,Asian Art,Hanging scroll,笛吹地蔵図|Jizō Bosatsu Playing a Flute,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,,1602,1674,mid- 17th century,1634,1666,Hanging scroll; ink and color on paper,Image: 38 3/4 in. × 15 in. (98.5 × 38.1 cm) Overall with mounting: 80 1/2 × 21 7/16 in. (204.5 × 54.5 cm) Overall with knobs: 80 1/2 × 23 5/8 in. (204.5 × 60 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.24,false,true,670915,Asian Art,Album of thirty-six paintings and thirty-six poems,,Japan,Edo period (1615–1868),,,,Artist,,Sumiyoshi Gukei,"Japanese, 1631–1705",,Sumiyoshi Gukei,,1631,1705,1674–92,1674,1692,"Album of thirty-six paintings and thirty-six poems; ink, color and gold on silk and paper",Image (each leaf): 6 7/8 × 6 5/16 in. (17.4 × 16 cm) Album: 8 3/4 × 7 11/16 × 2 5/8 in. (22.3 × 19.5 × 6.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.82a–c,false,true,54774,Asian Art,Hanging scrolls,"滝見業平図|Nunobiki Waterfall, Mount Yoshino, and Tatsuta River",Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,,1636,1713,after 1709,1709,1709,Triptych of hanging scrolls; ink and color on silk,Image (a): 58 9/16 × 31 1/2 in. (148.7 × 80 cm) Overall with mounting (a): 9 ft. 8 9/16 in. × 40 9/16 in. (296 × 103 cm) Overall with knobs (a): 9 ft. 8 9/16 in. × 43 1/2 in. (296 × 110.5 cm) Image (b): 58 9/16 × 31 1/2 in. (148.7 × 80 cm) Overall with mounting (b): 9 ft. 7 15/16 in. × 40 9/16 in. (294.5 × 103 cm) Overall with knobs (b): 9 ft. 7 15/16 in. × 43 9/16 in. (294.5 × 110.7 cm) Image (c): 58 3/4 × 31 1/2 in. (149.3 × 80 cm) Overall with mounting (c): 9 ft. 8 9/16 in. × 40 9/16 in. (296 × 103 cm) Overall with knobs (c): 9 ft. 8 9/16 in. × 43 5/8 in. (296 × 110.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.89,false,true,53422,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,,1658,1716,after 1704,1650,1850,Hanging scroll; ink on paper,Image: 11 1/4 × 14 1/2 in. (28.5 × 36.8 cm) Overall with mounting: 42 1/2 × 26 9/16 in. (108 × 67.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.90a, b",false,true,53421,Asian Art,Panels,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,,1658,1716,shortly after 1701,1650,1850,Pair of panels; ink and color on cryptomeria wood,Image: 54 in. × 7 7/8 in. (137.2 × 20 cm) Overall with mounting: 81 × 13 in. (205.7 × 33 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.212,false,true,670916,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,,1663,1747,1741,1741,1741,Hanging scroll; ink and color on silk,Image: 15 1/4 × 21 5/16 in. (38.7 × 54.2 cm) Overall with mounting: 51 11/16 × 26 7/8 in. (131.3 × 68.3 cm) Overall with knobs: 51 11/16 × 29 1/16 in. (131.3 × 73.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.49,false,true,667348,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,,1671,1750,ca. 1716–1736,1716,1736,Hanging scroll; ink and color on silk,Image: 23 in. × 32 3/4 in. (58.4 × 83.2 cm) Overall with mounting: 60 1/2 × 38 1/2 in. (153.7 × 97.8 cm) Overall with knobs: 60 1/2 × 41 5/8 in. (153.7 × 105.7 cm),,,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/667348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.154,false,true,53450,Asian Art,Hanging scroll,「竹窗雨日」図|“Window onto Bamboo on a Rainy Day”,Japan,Edo period (1615–1868),,,,Artist,,Gion Nankai,"Japanese, 1677–1751",,Gion Nankai,,1677,1751,first half of the 18th century,1700,1749,Hanging scroll; ink on paper,Image: 52 5/8 × 22 13/16 in. (133.7 × 58 cm) Overall with mounting: 84 1/16 × 29 7/16 in. (213.5 × 74.8 cm) Overall with knobs: 84 1/16 × 31 13/16 in. (213.5 × 80.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.3,false,true,78145,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Hakuin Ekaku,"Japanese, 1685–1768",,Hakuin Ekaku,,1685,1768,1685–1769,1685,1769,Hanging scroll; ink on paper,Image: 46 1/4 × 21 1/4 in. (117.5 × 54 cm) Overall with mounting: 74 × 27 1/2 in. (188 × 69.9 cm) Overall with knobs: 74 × 30 in. (188 × 76.2 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.132,false,true,670926,Asian Art,Hanging scroll,島原の節分図|Shimabara Courtesans Exorcizing Demons,Japan,Edo period (1615–1868),,,,Artist,,Miyagawa Isshō,"Japanese, 1689–1779",,Miyagawa Isshō,,1689,1779,second half of the 18th century,1750,1799,Hanging scroll; ink and color on paper,Image: 34 1/4 × 10 1/2 in. (87 × 26.6 cm) Overall with mounting: 72 5/8 × 15 13/16 in. (184.5 × 40.2 cm) Overall with knobs: 72 5/8 × 18 1/8 in. (184.5 × 46 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.20,false,true,78060,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Miyagawa Isshō,"Japanese, 1689–1779",,Miyagawa Isshō,,1689,1779,late 1730s–early 1740s,1736,1743,"Hanging scroll; ink, color and gold on silk",Image: 35 7/16 x 14 9/16 in. (90 x 37 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.91,false,true,77799,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Fukae Roshū,"Japanese, 1699–1757",,Fukae Roshū,,1699,1757,early 18th century,1700,1733,Fan mounted as a hanging scroll,Image: 8 13/16 × 18 1/8 in. (22.4 × 46 cm) Overall with mounting: 43 11/16 × 25 9/16 in. (111 × 65 cm) Overall with knobs: 43 11/16 × 27 5/8 in. (111 × 70.2 cm),Property of Mary Griggs Burke,,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.156,false,true,670942,Asian Art,Hanging scroll,山水図|Landscape,Japan,Edo period (1615–1868),,,,Artist,,Yanagisawa Kien,"Japanese, 1704–1758",,Yanagisawa Kien,,1704,1758,first half of the 18th century,1700,1749,Hanging scroll; ink and color on paper,Image: 53 7/8 × 12 1/2 in. (136.8 × 31.8 cm) Overall with mounting: 82 × 18 1/4 in. (208.3 × 46.4 cm) Overall with knobs: 82 × 21 5/16 in. (208.3 × 54.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.134,false,true,671034,Asian Art,Hanging scroll,文を読む遊女図|Courtesan Reading a Letter,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,,1711,1785,mid-18th century,1734,1766,Hanging scroll; ink and color on paper,Image: 32 7/8 × 8 9/16 in. (83.5 × 21.7 cm) Overall with mounting: 67 1/2 × 12 11/16 in. (171.5 × 32.2 cm) Overall with knobs: 67 1/2 × 14 5/16 in. (171.5 × 36.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.135a–c,false,true,53445,Asian Art,Hanging scrolls,"「翁」図|The Auspicious Noh Dance ""Okina""",Japan,Edo period (1615–1868),,,,Artist,,Toriyama Sekien,"Japanese, 1712–1788",,Toriyama Sekien,,1712,1788,ca. 1790–95,1790,1795,"Triptych of hanging scrolls; ink, color, and gold on paper",Image (a): 34 5/16 × 10 3/4 in. (87.2 × 27.3 cm) Overall with mounting (a): 67 5/16 × 15 in. (171 × 38.1 cm) Overall with knobs (a): 67 5/16 × 16 3/4 in. (171 × 42.5 cm) Image (b): 34 1/2 × 10 15/16 in. (87.6 × 27.8 cm) Overall with mounting (b): 67 1/16 × 15 1/16 in. (170.3 × 38.3 cm) Overall with knobs (b): 67 1/16 × 16 5/8 in. (170.3 × 42.2 cm) Image (c): 34 1/2 × 10 11/16 in. (87.6 × 27.2 cm) Overall with mounting (c): 67 5/16 × 15 in. (171 × 38.1 cm) Overall with knobs (c): 67 5/16 × 16 5/8 in. (171 × 42.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.159,false,true,53453,Asian Art,Folding fan mounted as a hanging scroll,奥の細道図扇面|Scene from The Narrow Road to the Deep North (Oku no hosomichi),Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,,1716,1783,ca. 1780,1600,1870,Folding fan mounted as a hanging scroll; ink and color on paper,Image: 8 7/8 × 11 3/16 in. (22.6 × 28.4 cm) Overall with mounting: 40 in. × 20 15/16 in. (101.6 × 53.2 cm) Overall with knobs: 40 × 23 3/8 in. (101.6 × 59.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.18,false,true,78134,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,,1716,1783,1760s,1760,1769,Hanging scroll; ink and color on paper,Overall with mounting: 73 3/4 × 15 1/2 in. (187.3 × 39.4 cm) Image: 84 1/4 in. × 11 in. (214 × 27.9 cm) Overall with knobs: 17 5/8 in. (44.8 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.168,false,true,671035,Asian Art,Hanging scroll,蘇鉄図|Cycad,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,mid-18th century,1734,1766,Hanging scroll; ink on paper,Image: 11 in. × 12 5/8 in. (28 × 32.1 cm) Overall with mounting: 41 9/16 × 18 1/16 in. (105.5 × 45.8 cm) Overall with knobs: 41 9/16 × 19 15/16 in. (105.5 × 50.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.243,false,true,53454,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,1734,1600,1870,Hanging scroll; ink on paper,Image: 10 1/2 × 13 1/8 in. (26.7 × 33.3 cm) Overall with mounting: 42 15/16 × 17 7/8 in. (109 × 45.4 cm) Overall with knobs: 42 15/16 × 20 1/2 in. (109 × 52 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.139,false,true,53446,Asian Art,Hanging scroll,立姿美人図|Woman in a Black Kimono,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,,1726,1792,1783–89,1600,1870,"Hanging scroll; ink, color, and gold on silk",Image: 33 1/2 × 11 1/4 in. (85.1 × 28.6 cm) Overall with mounting: 67 11/16 × 15 7/8 in. (172 × 40.4 cm) Overall with knobs: 67 11/16 × 17 13/16 in. (172 × 45.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.4,false,true,78146,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Soga Shōhaku,"Japanese, 1730–1781",,Soga Shōhaku,,1730,1781,ca. 1770s,1770,1779,Hanging scroll; ink on paper,Image: 52 1/2 x 21 in. (133.4 x 53.3 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.198a, b",false,true,53404,Asian Art,Pair of hanging scrolls,鮎図|Sweetfish in Summer and Autumn,Japan,Edo period (1615–1868),,,,Artist,,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,,1733,1795,1785,1600,1850,"Pair of hanging scrolls; ink, gold, and color on silk",Image (a): 40 15/16 × 14 9/16 in. (104 × 37 cm) Overall with mounting (a): 75 3/8 × 20 1/4 in. (191.5 × 51.5 cm) Overall with knobs (a): 75 3/8 × 22 5/8 in. (191.5 × 57.4 cm) Image (b): 40 15/16 × 14 1/2 in. (104 × 36.8 cm) Overall with mounting (b): 75 3/16 × 20 1/4 in. (191 × 51.5 cm) Overall with knobs (b): 75 3/16 × 22 9/16 in. (191 × 57.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.217,false,true,73359,Asian Art,Hanging scroll,羅漢図|Ten Rakan Examining a Painting of White-Robed Kannon,Japan,Edo period (1615–1868),,,,Artist,,Katō Nobukiyo,"Japanese, 1734–1810",,Katō Nobukiyo,,1734,1810,1792,1792,1792,Hanging scroll; ink and color on paper,Image: 55 1/4 × 22 3/4 in. (140.3 × 57.8 cm) Overall with mounting: 91 1/8 × 32 5/8 in. (231.5 × 82.9 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.176,false,true,53458,Asian Art,Folding fan mounted,「残雨半村」図|“Lingering Rain over Half the Village”,Japan,Edo period (1615–1868),,,,Artist,,Uragami Gyokudō,"Japanese, 1745–1820",,Uragami Gyokudō,,1745,1820,ca. 1815–20,1700,1899,Folding fan mounted as a hanging scroll; ink on paper,Image: 8 9/16 × 18 7/8 in. (21.7 × 48 cm) Overall with mounting: 41 3/4 × 23 1/2 in. (106 × 59.7 cm) Overall with knobs: 41 3/4 × 25 11/16 in. (106 × 65.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.177,false,true,53460,Asian Art,Hanging scroll,"「野橋抱琴図」|“On an Earthen Bridge, Carrying a Zither” (Yakyō hōkin zu)",Japan,Edo period (1615–1868),,,,Artist,,Uragami Gyokudō,"Japanese, 1745–1820",,Uragami Gyokudō,,1745,1820,1814,1600,1880,Hanging scroll; ink on paper,Image: 50 1/4 × 21 5/16 in. (127.7 × 54.2 cm) Overall with mounting: 73 1/4 × 27 7/16 in. (186 × 69.7 cm) Overall with knobs: 73 1/4 × 32 5/16 in. (186 × 82 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.201,false,true,64872,Asian Art,Handscroll,華洛四季遊楽図巻|Scenes of the Four Seasons in Kyoto,Japan,Edo period (1615–1868),,,,Artist,,Genki (Komai Ki),"Japanese, 1747–1797",,Genki,,1747,1797,1778,1778,1778,Handscroll; ink and color on silk,Image: 12 5/16 in. × 16 ft. 7 15/16 in. (31.3 × 507.8 cm) Overall with mounting: 12 13/16 in. × 16 ft. 7 15/16 in. (32.5 × 507.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/64872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.202a, b",false,true,53405,Asian Art,Pair of hanging scrolls,燕姞・楊貴妃図|Yanji with Orchids and Yang Guifei with Peonies,Japan,Edo period (1615–1868),,,,Artist,,Genki (Komai Ki),"Japanese, 1747–1797",,Genki,,1747,1797,1785,1600,1800,Pair of hanging scrolls; ink and color on silk,Image (a): 43 3/16 × 21 15/16 in. (109.7 × 55.8 cm) Overall with mounting (a): 78 7/8 × 27 1/16 in. (200.3 × 68.8 cm) Overall with knobs (a): 78 7/8 × 29 5/16 in. (200.3 × 74.4 cm) Image (b): 43 3/16 × 21 15/16 in. (109.7 × 55.7 cm) Overall with mounting (b): 78 7/8 × 27 1/8 in. (200.3 × 68.9 cm) Overall with knobs (b): 78 7/8 × 29 5/16 in. (200.3 × 74.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.173,false,true,670940,Asian Art,Handscroll section,,Japan,Edo period (1615–1868),,,,Artist,,Noro Kaiseki,"Japanese, 1747–1828",,Noro Kaiseki,,1747,1828,1826,1826,1826,Handscroll section mounted as a hanging scroll; ink and color on silk,Image: 13 7/8 × 36 7/16 in. (35.2 × 92.6 cm) Overall with mounting: 48 13/16 × 42 1/4 in. (124 × 107.3 cm) Overall with knobs: 48 13/16 × 46 5/8 in. (124 × 118.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.175a, b",false,true,670892,Asian Art,Albums,,Japan,Edo period (1615–1868),,,,Artist,,Totoki Baigai,"Japanese, 1749–1804",,Totoki Baigai,,1749,1804,1800,1800,1800,"Two albums, each with ten leaves; ink and color on paper",Image (each leaf): 7 3/8 × 7 5/8 in. (18.8 × 19.4 cm) Each album: 9 5/16 × 8 11/16 × 15/16 in. (23.6 × 22 × 2.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.191,false,true,53465,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kameda Bōsai,"Japanese, 1752–1826",,Kameda Bōsai,,1752,1826,ca. 1817,1600,1899,Hanging scroll; ink and color on silk,Image: 41 7/8 × 19 1/8 in. (106.4 × 48.6 cm) Overall with mounting: 71 5/8 × 23 3/4 in. (182 × 60.4 cm) Overall with knobs: 71 5/8 × 26 in. (182 × 66 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.16,false,true,78143,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,,1756,1829,1756–1815,1756,1815,Hanging scroll; ink and color on silk,Image: 31 5/8 x 13 1/16 in. (80.4 x 33.2 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.94,false,true,670923,Asian Art,Handscroll,三十六歌仙図|The Thirty-Six Poetic Immortals (Sanjūrokkasen),Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,,1761,1828,1824,1824,1824,Handscroll; ink and color on paper,Image: 11 5/8 in. × 27 ft. 1 13/16 in. (29.5 × 827.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.13,false,true,78135,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,,1761,1828,1761–1828,1761,1828,Hanging scroll; ink and color on silk,Image: 46 3/4 x 21 1/2 in. (118.7 x 54.6 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.15,false,true,78139,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,,1761,1828,1761–1828,1761,1828,"Hanging scroll; ink, color and gold on silk",Image: 72 1/4 x 18 5/16 in. (183.5 x 46.5 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.149,false,true,671045,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,,1763,1828,late 18th–early 19th century,1763,1828,Hanging scroll; ink and color on silk,Image: 34 5/8 in. × 11 in. (88 × 28 cm) Overall with mounting: 66 3/4 × 15 7/8 in. (169.5 × 40.3 cm) Overall with knobs: 66 3/4 × 18 1/16 in. (169.5 × 45.9 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.192,false,true,53466,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,,1763,1840,1828,1600,1899,Hanging scroll; ink on silk,Image: 49 13/16 × 23 1/4 in. (126.5 × 59.1 cm) Overall with mounting: 88 3/16 × 29 1/8 in. (224 × 74 cm) Overall with knobs: 88 3/16 × 31 13/16 in. (224 × 80.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.193,false,true,670897,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,,1763,1840,1828,1828,1828,Hanging scroll; ink and color on silk,Image: 52 3/8 × 27 13/16 in. (133 × 70.7 cm) Overall with mounting: 81 1/8 × 32 15/16 in. (206 × 83.7 cm) Overall with knobs: 81 1/8 × 35 7/8 in. (206 × 91.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.147,false,true,671044,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,,1769,1825,late 18th–early 19th century,1769,1825,Hanging scroll; ink and color on paper,Image: 23 9/16 × 10 3/8 in. (59.8 × 26.4 cm) Overall with mounting: 55 11/16 × 14 13/16 in. (141.5 × 37.7 cm) Overall with knobs: 55 11/16 × 17 1/8 in. (141.5 × 43.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.71,false,true,45792,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shuntei,"Japanese, 1770–1820",,Katsukawa Shuntei,,1770,1820,dated 1795,1795,1795,Hanging scroll; ink and color on silk,35 5/8 x 10 3/4 in. (90.5 x 27.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.140,false,true,671006,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shuntei,"Japanese, 1770–1820",,Katsukawa Shuntei,,1770,1820,late 18th–early 19th century,1770,1820,"Hanging scroll; ink, color and gold on paper",Image: 50 9/16 × 23 7/16 in. (128.4 × 59.6 cm) Overall with mounting: 82 7/8 × 28 1/16 in. (210.5 × 71.3 cm) Overall with knobs: 82 7/8 × 30 5/8 in. (210.5 × 77.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.152,false,true,670924,Asian Art,Hanging scroll,雨宿り図|Taking Shelter from the Rain,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,,1771,1844,early to mid-19th century,1800,1844,Hanging scroll; ink and color on silk,Image: 15 1/2 × 27 5/16 in. (39.4 × 69.3 cm) Overall with mounting: 51 15/16 × 32 5/16 in. (132 × 82 cm) Overall with knobs: 51 15/16 × 34 3/4 in. (132 × 88.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.12,false,true,667347,Asian Art,Hanging scroll,五美人図|Five Beauties,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,,1771,1844,1840,1840,1840,Hanging scroll; ink and color on silk,Image: 15 3/8 × 20 3/4 in. (39.1 × 52.7 cm) Overall with mounting: 47 3/4 × 25 3/4 in. (121.3 × 65.4 cm) Overall with knobs: 47 3/4 × 28 in. (121.3 × 71.1 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/667347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.185,false,true,670900,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nakabayashi Chikutō,"Japanese, 1776–1853",,Nakabayashi Chikutō,,1776,1853,1840,1840,1840,Hanging scroll; ink and color on paper,Image: 42 13/16 × 17 3/8 in. (108.8 × 44.1 cm) Overall with mounting: 71 13/16 × 18 5/16 in. (182.4 × 46.5 cm) Overall with knobs: 71 13/16 × 22 5/8 in. (182.4 × 57.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.181,false,true,53462,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tanomura Chikuden,"Japanese, 1777–1835",,Tanomura Chikuden,,1777,1835,late 18th–early 19th century,1777,1835,Hanging scroll; ink and color on paper,Image: 52 1/8 x 16 5/8 in. (132.4 x 42.2 cm) Overall with mounting: 85 13/16 × 23 3/8 in. (218 × 59.3 cm) Overall with knobs: 85 13/16 × 25 7/8 in. (218 × 65.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.186,false,true,670943,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nukina Kaioku,"Japanese, 1778–1863",,Nukina Kaioku,,1778,1863,1833,1833,1833,Hanging scroll; ink and color on silk,Image: 11 7/8 in. × 7 in. (30.2 × 17.8 cm) Overall with mounting: 39 7/8 × 12 5/8 in. (101.3 × 32 cm) Overall with knobs: 39 7/8 × 14 7/8 in. (101.3 × 37.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.187,false,true,670947,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nukina Kaioku,"Japanese, 1778–1863",,Nukina Kaioku,,1778,1863,1844,1844,1844,Hanging scroll; ink and color on paper,Image: 53 in. × 20 5/8 in. (134.6 × 52.4 cm) Overall with mounting: 79 1/8 × 27 1/16 in. (201 × 68.8 cm) Overall with knobs: 79 1/8 × 29 5/16 in. (201 × 74.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.84,false,true,670939,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Nonoyama Kōzan,"Japanese, 1780–1847",,Nonoyama Kōzan,,1780,1847,1822,1822,1822,"Handscroll; ink, color and gold on paper",Image: 13 5/8 × 54 15/16 in. (34.6 × 139.5 cm) Overall with mounting: 14 15/16 × 93 1/2 in. (38 × 237.5 cm) Overall with knobs: 16 5/16 × 93 1/2 in. (41.5 × 237.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.178,false,true,73358,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okada Hankō,"Japanese, 1782–1846",,Okada Hankō,,1782,1846,1843,1843,1843,Hanging scroll; ink and color on paper,Image: 50 3/8 × 23 1/8 in. (128 × 58.8 cm) Overall with mounting: 87 3/16 × 29 5/16 in. (221.5 × 74.5 cm) Overall with knobs: 87 3/16 × 31 13/16 in. (221.5 × 80.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.179,false,true,670921,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okada Hankō,"Japanese, 1782–1846",,Okada Hankō,,1782,1846,1833,1833,1833,Hanging scroll; ink and color on paper,Image: 11 13/16 × 25 1/16 in. (30 × 63.7 cm) Overall with mounting: 50 3/16 × 31 3/4 in. (127.5 × 80.6 cm) Overall with knobs: 50 3/16 × 34 3/16 in. (127.5 × 86.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.760,false,true,77196,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Baiitsu,"Japanese, 1783–1856",,Yamamoto Baiitsu,,1783,1783,dated 10th month of 1852,1852,1852,Hanging scroll; ink and color on silk,Image: 50 × 21 13/16 in. (127 × 55.4 cm) Overall with mounting: 84 × 27 9/16 in. (213.4 × 70 cm) Overall with knobs: 84 × 30 in. (213.4 × 76.2 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.188,false,true,670891,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Baiitsu,"Japanese, 1783–1856",,Yamamoto Baiitsu,,1783,1783,1843,1843,1843,Hanging scroll; ink and color on silk,Image: 45 9/16 × 16 1/8 in. (115.8 × 40.9 cm) Overall with mounting: 78 1/16 × 22 5/8 in. (198.2 × 57.5 cm) Overall with knobs: 78 1/16 × 25 1/16 in. (198.2 × 63.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.189,false,true,670893,Asian Art,Folding fan,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Baiitsu,"Japanese, 1783–1856",,Yamamoto Baiitsu,,1783,1783,1832,1832,1832,Folding fan; ink and color on paper,Image: 6 11/16 × 18 11/16 in. (17 × 47.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.47,false,true,78149,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Baiitsu,"Japanese, 1783–1856",,Yamamoto Baiitsu,,1783,1783,dated 1851,1851,1851,Hanging scroll; ink on silk,Image: 45 1/4 x 15 3/4 in. (114.9 x 40 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.190a–d,false,true,53463,Asian Art,Four hanging scrolls,四季山水図|Landscapes of the Four Seasons,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Baiitsu,"Japanese, 1783–1856",,Yamamoto Baiitsu,,1783,1783,1848,1700,1899,Set of four hanging scrolls; ink and color on silk,Image (a): 40 3/8 × 13 7/8 in. (102.6 × 35.2 cm) Overall with mounting (a): 75 1/16 × 19 3/4 in. (190.7 × 50.1 cm) Overall with knobs (a): 75 1/16 × 21 5/8 in. (190.7 × 55 cm) Image (b): 40 3/8 × 13 7/8 in. (102.6 × 35.3 cm) Overall with mounting (b): 75 1/4 × 19 11/16 in. (191.2 × 50 cm) Overall with knobs (b): 75 1/4 × 21 11/16 in. (191.2 × 55.1 cm) Image (c): 40 7/16 × 13 7/8 in. (102.7 × 35.3 cm) Overall with mounting (c): 75 1/16 × 19 3/4 in. (190.6 × 50.1 cm) Overall with knobs (c): 75 1/16 × 21 3/4 in. (190.6 × 55.3 cm) Image (d): 40 1/2 × 13 15/16 in. (102.8 × 35.4 cm) Overall with mounting (d): 75 3/8 × 19 11/16 in. (191.5 × 50 cm) Overall with knobs (d): 75 3/8 × 21 11/16 in. (191.5 × 55.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.194,false,true,670882,Asian Art,Album,,Japan,Edo period (1615–1868),,,,Artist,,Takaku Aigai,"Japanese, 1796–1843",,Takaku Aigai,,1796,1843,1833,1833,1833,Album with twelve leaves; ink and color on paper,Album: 13 × 7 1/2 × 1 in. (33 × 19 × 2.5 cm) Image (each leaf): 11 7/16 × 13 1/8 in. (29 × 33.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.95,false,true,53424,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Kiitsu,"Japanese, 1796–1858",,Suzuki Kiitsu,,1796,1858,ca. 1805,1720,1920,Hanging scroll; ink and color on silk,Image: 39 7/8 × 12 15/16 in. (101.3 × 32.8 cm) Overall with mounting: 77 3/16 × 18 3/8 in. (196 × 46.6 cm) Overall with knobs: 77 3/16 × 20 9/16 in. (196 × 52.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.98,false,true,670946,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Kiitsu,"Japanese, 1796–1858",,Suzuki Kiitsu,,1796,1858,1857,1857,1857,Hanging scroll; ink and color on paper,Image: 39 1/16 × 11 5/16 in. (99.2 × 28.7 cm) Overall with mounting: 74 5/16 × 12 1/2 in. (188.8 × 31.8 cm) Overall with knobs: 74 5/16 × 14 5/8 in. (188.8 × 37.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.100a–f,false,true,53427,Asian Art,Handscrolls,,Japan,Edo period (1615–1868),,,,Artist,,Sakai Ōho,"Japanese, 1808–1841",,Sakai Ōho,,1808,1841,ca. 1839,1800,1920,"Six handscrolls; ink, color, and gold on silk",Overall (a): 3 9/16 x 46 7/16 in. (9 x 118 cm) Overall (b): 3 9/16 x 46 15/16 in. (9 x 119.2 cm) Overall (c): 3 1/2 x 46 7/16 in. (8.9 x 118 cm) Overall (d): 3 9/16 x 48 13/16 in. (9 x 124 cm) Overall (e): 3 5/8 x 46 1/2 in. (9.2 x 118.1 cm) Overall (f): 3 1/2 x 46 7/8 in. (8.9 x 119.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.196,false,true,671048,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Hine Taizan,"Japanese, 1813–1869",,Hine Taizan,,1813,1869,1859,1859,1859,Hanging scroll; ink and color on silk,Image: 53 15/16 × 20 3/16 in. (137 × 51.3 cm) Overall with mounting: 88 × 26 11/16 in. (223.5 × 67.8 cm) Overall with knobs: 88 × 30 1/2 in. (223.5 × 77.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.126,false,true,671003,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ryūkadō,"Japanese, active 1740s",,Ryūkadō,,1740,1749,1740s,1740,1749,Hanging scroll; ink and color on paper,Image: 28 11/16 in. × 13 in. (72.8 × 33 cm) Overall with mounting: 60 13/16 × 14 3/4 in. (154.5 × 37.4 cm) Overall with knobs: 60 13/16 × 17 1/16 in. (154.5 × 43.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.182,false,true,671012,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Takahashi Sōhei,"Japanese, 1804?–?1835",,Takahashi Sōhei,,1804,1835,1824,1824,1824,Hanging scroll; ink and color on paper,Image: 53 15/16 × 19 5/16 in. (137 × 49 cm) Overall with mounting: 77 1/16 × 21 5/8 in. (195.8 × 55 cm) Overall with knobs: 77 1/16 × 23 11/16 in. (195.8 × 60.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.183,false,true,671017,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Takahashi Sōhei,"Japanese, 1804?–?1835",,Takahashi Sōhei,,1804,1835,1831,1831,1831,Hanging scroll; ink and color on paper,Image: 39 1/2 × 11 13/16 in. (100.3 × 30 cm) Overall with mounting: 63 3/4 × 16 7/16 in. (162 × 41.7 cm) Overall with knobs: 63 3/4 × 18 11/16 in. (162 × 47.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.184,false,true,671011,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Takahashi Sōhei,"Japanese, 1804?–?1835",,Takahashi Sōhei,,1804,1835,1832,1832,1832,Hanging scroll; ink and color on paper,Image: 40 1/16 × 16 1/2 in. (101.8 × 41.9 cm) Overall with mounting: 69 11/16 × 22 1/16 in. (177 × 56 cm) Overall with knobs: 69 11/16 × 24 7/16 in. (177 × 62 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.169,false,true,53457,Asian Art,Hanging scroll,牡丹に竹図|Peony and Bamboo by a Rock,Japan,Edo period (1615–1868),,,,Artist,,Tokuyama Gyokuran,"Japanse, ca. 1728–1784",,Tokuyama Gyokuran,,1728,1784,ca. 1768,1600,1870,Hanging scroll; ink and color on paper,Image: 36 5/8 × 16 7/16 in. (93 × 41.7 cm) Overall with mounting: 68 7/8 × 22 3/16 in. (175 × 56.4 cm) Overall with knobs: 68 7/8 × 24 7/16 in. (175 × 62 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.75,false,true,671042,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ono Otsū,"Japanese, 1568–ca. 1631",,Ono Otsū,,1568,1631,1624,1624,1624,Hanging scroll; ink on paper,Image: 24 5/8 × 16 1/16 in. (62.6 × 40.8 cm) Overall with mounting: 68 1/2 × 21 9/16 in. (174 × 54.7 cm) Overall with knobs: 68 1/2 × 23 11/16 in. (174 × 60.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.136,false,true,670933,Asian Art,Hanging scroll,花魁と禿の初詣図|Courtesan and Two Attendants on New Year's Day,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,,1735,1790,ca. 1780s,1780,1789,"Hanging scroll; ink, color and gold on paper",Image: 33 11/16 × 13 11/16 in. (85.6 × 34.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.137,false,true,670932,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,,1735,1790,1764–88,1764,1788,Hanging scroll; ink and color on silk,Image: 32 in. × 13 1/2 in. (81.3 × 34.3 cm) Overall with mounting: 66 15/16 × 18 13/16 in. (170 × 47.8 cm) Overall with knobs: 66 15/16 × 20 11/16 in. (170 × 52.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.120,false,true,670997,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tōsendō Rifū,"Japanese, active ca. 1730",,Tōsendō Rifū,,1720,1740,ca. 1730,1720,1740,Hanging scroll; ink and color on silk,Image: 26 7/8 × 12 1/8 in. (68.3 × 30.8 cm) Overall with mounting: 60 1/4 × 16 15/16 in. (153 × 43 cm) Overall with knobs: 60 1/4 × 18 7/8 in. (153 × 48 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.118,false,true,53443,Asian Art,Hanging scroll mounted as panel,立姿遊女図|Standing Courtesan,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudō Ando,"Japanese, ca. 1671–1743",,Kaigetsudō Ando,,1671,1743,early 18th century,1700,1733,"Hanging scroll, mounted as panel; ink and color on paper",Image: 40 15/16 × 16 5/8 in. (104 × 42.2 cm) Overall with mounting: 44 7/8 × 20 1/2 in. (114 × 52 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.125,false,true,670954,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kakondō,"Japanese, active 1716–36",,Kakondō,,1716,1736,early 18th century,1716,1736,"Hanging scroll; ink, color and gold on paper",Image: 31 1/8 × 15 13/16 in. (79 × 40.2 cm) Overall with mounting: 51 3/16 × 19 5/16 in. (130 × 49 cm) Overall with knobs: 51 3/16 × 21 1/4 in. (130 × 54 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.127,false,true,671021,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Takizawa Shigenobu,"Japanese, active 1720–40",,Takizawa Shigenobu,,1700,1799,ca. 1730,1720,1740,Hanging scroll; ink and color on silk,Image: 39 15/16 × 18 3/8 in. (101.5 × 46.6 cm) Overall with mounting: 71 7/16 × 22 11/16 in. (181.5 × 57.6 cm) Overall with knobs: 71 7/16 × 24 7/8 in. (181.5 × 63.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.133,false,true,671020,Asian Art,Hanging scroll,見立松風図|The Brine Maiden Matsukaze,Japan,Edo period (1615–1868),,,,Artist,,Nishimura Shigenobu,"Japanese, active 1729–39",,Nishimura Shigenobu,,1729,1739,early 18th century,1700,1733,Hanging scroll; ink and color on paper,Image: 31 9/16 × 11 3/16 in. (80.2 × 28.4 cm) Overall with mounting: 63 3/8 × 15 5/8 in. (161 × 39.7 cm) Overall with knobs: 63 3/8 × 17 15/16 in. (161 × 45.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.138,false,true,671005,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Miyagawa (Katsukawa) Shunsui,"Japanese, active 1744–64",,Miyagawa (Katsukawa) Shunsui,,1744,1764,mid-18th century,1734,1766,"Hanging scroll; ink, color and gold on silk",Image: 30 7/8 × 13 7/8 in. (78.5 × 35.2 cm) Overall with mounting: 67 5/16 × 15 1/16 in. (171 × 38.3 cm) Overall with knobs: 67 5/16 × 17 1/4 in. (171 × 43.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.119,false,true,53444,Asian Art,Hanging scroll,文を書く遊女図|Courtesan Writing a Letter,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudō Doshin,"Japanese, active 1711–1736",,Kaigetsudō Doshin,,1711,1736,ca. 1715,1705,1725,Hanging scroll; ink and color on paper,Image: 19 1/2 × 23 5/8 in. (49.5 × 60 cm) Overall with mounting: 53 × 28 13/16 in. (134.6 × 73.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.131,false,true,670931,Asian Art,Hanging scroll,朝比奈義秀図|The Warrior Asahina Yoshihide Lifting a Puppet of a Courtesan on a Go Board,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyoshige,"Japanese, active ca. 1716–1759",,Torii Kiyoshige,,1716,1759,mid-18th century,1734,1766,"Hanging scroll; ink, color and gold on paper",Image: 30 in. × 7 15/16 in. (76.2 × 20.1 cm) Overall with mounting: 65 3/16 × 12 11/16 in. (165.5 × 32.2 cm) Overall with knobs: 65 3/16 × 14 7/8 in. (165.5 × 37.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.7,false,true,78155,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kita Genki,"Japanese, active late 17th century",,Kita Genki,,1667,1699,dated 1674,1674,1674,Hanging scroll; ink and color on silk,Image: 43 1/8 × 16 1/2 in. (109.5 × 41.9 cm) Overall with mounting: 79 5/8 × 23 3/8 in. (202.2 × 59.4 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.43,false,true,671053,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Waō,"Japanese, active early 18th century",,Hishikawa Waō,,1700,1733,early 18th century,1700,1733,"Hanging scroll; ink, color and gold on silk",Image: 14 5/8 × 17 15/16 in. (37.1 × 45.6 cm) Overall with mounting: 49 1/2 × 22 7/8 in. (125.8 × 58.1 cm) Overall with knobs: 49 1/2 × 24 15/16 in. (125.8 × 63.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.123,false,true,670907,Asian Art,Hanging scroll,立姿遊女図|Standing Courtesan,Japan,Edo period (1615–1868),,,,Artist,,Baiōken Eishun,"Japanese, active early 18th century",,Baiōken Eishun,,1710,1730,probably 1720s,1720,1729,Hanging scroll; ink and color on silk,Image: 39 7/16 × 16 1/8 in. (100.2 × 41 cm) Overall with mounting: 77 9/16 × 22 3/8 in. (197 × 56.8 cm) Overall with knobs: 77 9/16 × 24 5/16 in. (197 × 61.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.143,false,true,671050,Asian Art,Hanging scroll,見立寒山拾得図|Courtesans Parodying Kanzan and Jittoku,Japan,Edo period (1615–1868),,,,Artist,,Kinpūsha Toyomaro,"Japanese, active early 19th century",,Kinpūsha Toyomaro,,1800,1833,late 18th–early 19th century,1767,1833,"Hanging scroll; ink, color and gold on paper",Image: 48 1/4 × 22 1/4 in. (122.5 × 56.5 cm) Overall with mounting: 85 1/4 × 27 13/16 in. (216.5 × 70.7 cm) Overall with knobs: 85 1/4 × 30 5/16 in. (216.5 × 77 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.122,false,true,671031,Asian Art,Hanging scroll,蚊帳美人図|Woman Reading under a Mosquito Net,Japan,Edo period (1615–1868),,,,Artist,,Fuhiken Tokikaze,active first half of the 18th century,,Fuhiken Tokikaze,,1700,1749,ca. 1720,1710,1730,Hanging scroll; ink and color on silk,Image: 27 1/2 × 14 5/8 in. (69.8 × 37.1 cm) Overall with mounting: 60 3/16 × 22 1/8 in. (152.8 × 56.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.150a, b",false,true,670920,Asian Art,Diptych of hanging scrolls,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",,1797,1858,1848–54,1848,1854,Diptych of hanging scrolls; ink and color on silk,Image (a): 36 7/16 in. × 13 in. (92.5 × 33 cm) Overall with mounting (a): 67 11/16 × 17 11/16 in. (172 × 44.9 cm) Overall with knobs (a): 67 11/16 × 19 1/2 in. (172 × 49.6 cm) Image (b): 36 7/16 × 12 15/16 in. (92.5 × 32.9 cm) Overall with mounting (b): 67 5/16 × 17 11/16 in. (171 × 44.9 cm) Overall with knobs: 67 5/16 × 19 5/8 in. (171 × 49.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.172a, b",false,true,670883,Asian Art,Hanging scrolls,,Japan,Edo period (1615–1868),,,,Artist,,Aiseki,"Japanese, active first half of the 19th century",,Aiseki,,1800,1849,first half of the 19th century,1800,1849,Pair of hanging scrolls; ink and color on paper,Image (a): 51 3/4 × 17 15/16 in. (131.5 × 45.5 cm) Overall with mounting (a): 81 1/2 × 22 15/16 in. (207 × 58.2 cm) Overall with knobs (a): 81 1/2 × 25 1/16 in. (207 × 63.6 cm) Image (b): 51 13/16 × 17 7/8 in. (131.6 × 45.4 cm) Overall with mounting (b): 81 1/2 × 22 15/16 in. (207 × 58.2 cm) Overall with knobs (b): 81 1/2 × 25 1/16 in. (207 × 63.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.113,false,true,670973,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Furuyama Moroshige,"Japanese, active second half of the 17th century",,Furuyama Moroshige,,1650,1699,second half of the 17th century,1650,1699,Hanging scroll; ink and color on silk,Image: 11 7/16 × 18 3/16 in. (29 × 46.2 cm) Overall with mounting: 38 × 20 1/4 in. (96.5 × 51.5 cm) Overall with knobs: 38 × 22 5/8 in. (96.5 × 57.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.8,false,true,45618,Asian Art,Hanging scroll,"月光菩薩像 「金胎仏画帖」断簡|Gakkō Bosatsu, from “Album of Buddhist Deities from the Diamond World and Womb World Mandalas” (“Kontai butsugajō”)",Japan,Heian period (794–1185),,,,Artist,Attributed to,Takuma Tametō,"Japanese, active ca. 1132–74",,Takuma Tametō,,1132,1174,mid-12th century,1134,1166,"Page from a book mounted as a hanging scroll; ink, color, and gold on paper",10 in. × 5 3/8 in. (25.4 × 13.7 cm) Overall with mounting: 49 1/4 × 15 1/2 in. (125.1 × 39.4 cm) Overall with knobs: 49 1/4 × 17 3/4 in. (125.1 × 45.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.4,false,true,53164,Asian Art,Hanging scroll,"大精進菩薩  「金胎仏画帖」断簡|Daishōjin Bosatsu, from “Album of Buddhist Deities from the Diamond World and Womb World Mandalas” (“Kontai butsugajō”)",Japan,Heian period (794–1185),,,,Artist,Attributed to,Takuma Tametō,"Japanese, active ca. 1132–74",,Takuma Tametō,,1132,1174,12th century,1100,1185,"Hanging scroll; ink, color, and gold on paper",Image: 9 3/4 in. × 5 in. (24.7 × 12.7 cm) Overall with mounting: 46 1/16 × 14 3/16 in. (117 × 36 cm) Overall with knobs: 46 1/16 × 15 7/8 in. (117 × 40.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.31,false,true,670972,Asian Art,Album leaf remounted as a hanging scroll,"源氏物語図色紙 「柏木」|Scene from “The Oak Tree” (“Kashiwagi”), from The Tale of Genji (Genji monogatari)",Japan,Momoyama period (1573–1615),,,,Artist,,Tosa Mitsuyoshi,"Japanese, 1539–1613",,TOSA MITSUYOSHI,,1539,1613,late 16th–early 17th century,1567,1613,"Album leaf remounted as a hanging scroll; ink, color and gold on paper",Image: 9 3/4 × 8 3/16 in. (24.7 × 20.8 cm) Overall with mounting: 54 5/16 × 15 1/2 in. (138 × 39.4 cm) Overall with knobs: 54 5/16 × 17 1/4 in. (138 × 43.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.33a, b",false,true,53245,Asian Art,Album leaves mounted as a pair of hanging scrolls,"玉鬘図 (『源氏物語』画帖の内)|“The Jeweled Chaplet” (“Tamakazura”), from The Tale of Genji (Genji monogatari)",Japan,Momoyama period (1573–1615),,,,Artist,Circle of,Tosa Mitsuyoshi,"Japanese, 1539–1613",,TOSA MITSUYOSHI,,1539,1613,early 17th century,1600,1615,"Album leaves mounted as a pair of hanging scrolls; ink, gold, silver, and color on paper",Image (a): 9 5/8 × 8 3/8 in. (24.4 × 21.3 cm) Overall with mounting (a): 53 1/4 × 15 11/16 in. (135.3 × 39.8 cm) Overall with knobs (a): 53 1/4 × 17 3/8 in. (135.3 × 44.2 cm) Image (b): 9 7/16 × 8 3/8 in. (24 × 21.2 cm) Overall with mounting (b): 53 1/8 × 15 5/8 in. (135 × 39.7 cm) Overall with knobs (b): 53 1/8 × 17 3/8 in. (135 × 44.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.72,false,true,670979,Asian Art,Hanging scroll,,Japan,Momoyama period (1573–1615),,,,Artist,,Konoe Nobutada,"Japanese, 1565–1614",,Konoe Nobutada,,1565,1614,late 16th century,1573,1599,Hanging scroll; ink on paper,Image: 38 7/16 × 16 13/16 in. (97.7 × 42.7 cm) Overall with mounting: 72 1/16 × 17 13/16 in. (183 × 45.3 cm) Overall with knobs: 72 1/16 × 19 13/16 in. (183 × 50.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.130,false,true,670930,Asian Art,Hanging scroll,羽根突き美人図|Woman with Battledore and Shuttlecock,Japan,Momoyama period (1573–1615),,,,Artist,,Torii Kiyotomo,"Japanese, active early 19th century",,Torii Kiyotomo,,1815,1820,1815–20,1815,1820,"Hanging scroll; ink, color and gold on paper",Image: 45 3/16 × 20 11/16 in. (114.8 × 52.5 cm) Overall with mounting: 71 1/4 × 24 1/8 in. (181 × 61.2 cm) Overall with knobs: 71 1/4 × 26 3/8 in. (181 × 67 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.50,false,true,53231,Asian Art,Hanging scroll,竹林七聖図|Seven Sages of the Bamboo Grove,Japan,Muromachi period (1392–1573),,,,Artist,,Sesson Shūkei,ca. 1504–ca. 1589,,Sesson Shūkei,,1504,1589,1550s,1550,1559,Hanging scroll; ink and color on paper,Image: 40 5/16 × 20 3/8 in. (102.4 × 51.7 cm) Overall with mounting: 79 3/4 × 26 7/16 in. (202.5 × 67.2 cm) Overall with knobs: 79 3/4 × 28 9/16 in. (202.5 × 72.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.49,false,true,53212,Asian Art,Hanging scroll,蘇軾騎驢図|Su Shi Riding a Donkey,Japan,Muromachi period (1392–1573),,,,Artist,,Bokudō Sojun,"Japanese, 1373–1459",,Bokudō Sojun,,1373,1459,early 15th century,1400,1433,Hanging scroll; ink and gold on paper,Image: 22 1/2 × 10 1/4 in. (57.2 × 26 cm) Overall with mounting: 57 11/16 × 14 7/8 in. (146.5 × 37.8 cm) Overall with knobs: 57 11/16 × 16 11/16 in. (146.5 × 42.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.67,false,true,53233,Asian Art,Hanging scroll,伯牙鍾子期図|Bo Ya Plays the Qin as Zhong Ziqi Listens,Japan,Muromachi period (1392–1573),,,,Artist,Circle of,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,,1476,1559,1530s,1530,1539,Hanging scroll; ink and color on paper,Image: 65 1/16 × 34 1/4 in. (165.2 × 87 cm) Overall with mounting: 8 ft. 10 7/8 in. × 40 13/16 in. (271.5 × 103.7 cm) Overall with knobs: 8 ft. 10 7/8 in. × 43 3/16 in. (271.5 × 109.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.40,false,true,45638,Asian Art,Hanging scroll,粟に燕図|Millet and Sparrows,Japan,Muromachi period (1392–1573),,,,Artist,,Geiai,active mid-16th century,,Geiai,,1534,1566,mid-16th century,1534,1566,Hanging scroll; ink on paper,Image: 39 9/16 × 17 5/8 in. (100.5 × 44.8 cm) Overall with knobs: 75 3/4 × 24 7/8 in. (192.4 × 63.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.46a, b",false,true,65394,Asian Art,Hanging scrolls,"豊干寒山拾得図|Fenggan, Hanshan, and Shide",Japan,Muromachi period (1392–1573),,,,Artist,,Reisai,"Japanese, active ca. 1430–50",,Reisai,,1430,1450,first half of the 15th century,1400,1449,Pair of hanging scrolls; ink and color on paper,Image (a): 37 7/8 × 13 5/8 in. (96.2 × 34.6 cm) Overall with mounting (a): 72 15/16 × 18 11/16 in. (185.3 × 47.5 cm) Overall with knobs (a): 72 15/16 × 20 5/16 in. (185.3 × 51.6 cm) Image (b): 37 15/16 × 13 9/16 in. (96.3 × 34.5 cm) Overall with mounting (b): 72 5/8 × 18 11/16 in. (184.5 × 47.4 cm) Overall with knobs (b): 72 5/8 × 20 1/4 in. (184.5 × 51.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.58,false,true,53219,Asian Art,Hanging scroll,葡萄蝉図|Cicada on a Grapevine,Japan,Muromachi period (1392–1573),,,,Artist,,Bokurin Guan,"Japanese, active late 14th century",,Bokurin Guan,,1367,1399,late 14th century,1392,1399,Hanging scroll; ink on paper,Image: 25 1/4 × 12 1/8 in. (64.2 × 30.8 cm) Overall with mounting: 57 5/16 × 15 1/2 in. (145.5 × 39.3 cm) Overall with knobs: 57 5/16 × 17 5/16 in. (145.5 × 44 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.45,false,true,670971,Asian Art,Hanging scroll,騎獅文殊図|Monju on a Lion,Japan,Muromachi period (1392–1573),,,,Artist,,Shūsei,"Japanese, active late 15th century",(reading unsure),Shūsei,,1467,1499,late 15th century,1467,1499,Hanging scroll; ink on paper,Image: 32 1/16 in. × 13 in. (81.5 × 33 cm) Overall with mounting: 65 9/16 × 19 1/8 in. (166.5 × 48.6 cm) Overall with knobs: 65 9/16 × 20 7/8 in. (166.5 × 53 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.64a, b",false,true,53221,Asian Art,Hanging scrolls,夏秋花鳥図|Birds and Flowers of Summer and Autumn,Japan,Muromachi period (1392–1573),,,,Artist,,Shikibu Terutada,"Japanese, active mid–16th century",,Shikibu Terutada,,1534,1566,mid-16th century,1534,1566,Pair of hanging scrolls; ink and color on paper,Image (a): 37 11/16 × 17 5/8 in. (95.8 × 44.8 cm) Overall with mounting (a): 74 7/16 × 23 3/8 in. (189 × 59.3 cm) Overall with knobs (a): 74 7/16 × 25 3/16 in. (189 × 64 cm) Image (b): 37 11/16 × 17 5/8 in. (95.8 × 44.8 cm) Overall with mounting (b): 74 5/8 × 23 3/8 in. (189.5 × 59.3 cm) Overall with knobs (b): 74 5/8 × 25 3/16 in. (189.5 × 64 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.194.2,false,true,57338,Asian Art,Painted panel,,Japan,Muromachi period (1392–1573),,,,Artist,After,Kenkō Shokei,"Japanese, active ca. 1470–after 1523",,Kenkō Shokei,,1460,1523,late 15th century,1467,1499,Paint on paper,33 x 12 1/8 in. (83.8 x 30.8 cm),"Gift of Nathan V. Hammer, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.194.3,false,true,57350,Asian Art,Painted panel,,Japan,Muromachi period (1392–1573),,,,Artist,After,Kenkō Shokei,"Japanese, active ca. 1470–after 1523",,Kenkō Shokei,,1460,1523,late 15th century,1467,1499,Paint on paper,33 x 12 in. (83.3 x 30.5 cm),"Gift of Nathan V. Hammer, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.63,false,true,670904,Asian Art,Hanging scroll,瓜図|Melons,Japan,Muromachi period (1392–1573),,,,Artist,,Yamada Dōan,"Japanese, second half of the 16th century",,Yamada Dōan,,1550,1599,late 16th century,1567,1599,Hanging scroll; ink on paper,Image: 13 1/4 × 18 1/8 in. (33.6 × 46 cm) Overall with mounting: 47 1/2 × 23 7/8 in. (120.7 × 60.6 cm) Overall with knobs: 47 1/2 × 25 13/16 in. (120.7 × 65.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.52a, b",false,true,53226,Asian Art,Hanging scrolls,,Japan,Muromachi period (1392–1573),,,,Artist,,Kantei,"Japanese, active second half of 15th century",,Kantei,,1400,1499,early 16th century,1500,1533,Pair of hanging scrolls; ink and color on paper,Image (a): 18 1/16 × 11 3/4 in. (45.8 × 29.9 cm) Overall with mounting (a): 51 15/16 × 16 3/8 in. (132 × 41.6 cm) Overall with knobs (a): 51 15/16 × 18 1/8 in. (132 × 46 cm) Image (b): 18 1/8 × 11 3/4 in. (46 × 29.9 cm) Overall with mounting (b): 52 1/16 × 16 5/16 in. (132.2 × 41.5 cm) Overall with knobs (b): 52 1/16 × 18 1/8 in. (132.2 × 46 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.66,false,true,53222,Asian Art,Hanging scroll,麝香猫図|Musk Cat,Japan,Muromachi period (1392–1573),,,,Artist,,Uto Gyoshi,"Japanese, active second half of 16th century",,Uto Gyoshi,,1550,1599,second half of the 16th century,1550,1599,Hanging scroll; ink and color on paper,Image: 29 15/16 × 18 5/16 in. (76 × 46.5 cm) Overall with mounting: 66 1/8 × 24 1/8 in. (168 × 61.3 cm) Overall with knobs: 66 1/8 × 25 7/8 in. (168 × 65.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.47,false,true,670981,Asian Art,Hanging scroll,牧牛図|Oxherding,Japan,Muromachi period (1392–1573),,,,Artist,,Sekkyakushi,"Japanese, active first half of the 15th century",,Sekkyakushi,,1400,1499,first half of the 15th century,1400,1449,Hanging scroll; ink on paper,Image: 21 1/16 × 11 9/16 in. (53.5 × 29.4 cm) Overall with mounting: 54 3/4 × 15 5/8 in. (139 × 39.7 cm) Overall with knobs: 54 3/4 × 17 5/8 in. (139 × 44.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.55,false,true,53228,Asian Art,Hanging scroll,破墨山水図|Splashed-Ink Landscape,Japan,Muromachi period (1392–1573),,,,Artist,,Bokushō Shūshō,"Japanese, active late 15th–early 16th century",,Bokushō Shūshō,,1450,1600,early 16th century,1500,1533,Hanging scroll; ink on paper,Image: 31 1/2 × 13 3/8 in. (80 × 33.9 cm) Overall with mounting: 59 13/16 × 14 3/16 in. (152 × 36 cm) Overall with knobs: 59 13/16 × 16 1/16 in. (152 × 40.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.53a, b",false,true,65392,Asian Art,Hanging scrolls,四季山水図|Landscapes of the Four Seasons,Japan,Muromachi period (1392–1573),,,,Artist,,Keison,"Japanese, active late 15th– early 16th century",,Keison,,1467,1533,late 15th–early 16th century,1467,1533,Pair of hanging scrolls; ink on paper,Image (a): 38 5/16 × 19 9/16 in. (97.3 × 49.7 cm) Overall with mounting (a): 75 13/16 × 25 11/16 in. (192.5 × 65.3 cm) Overall with knobs (a): 75 13/16 × 27 1/2 in. (192.5 × 69.8 cm) Image (b): 38 3/8 × 19 5/8 in. (97.4 × 49.8 cm) Overall with mounting (b): 75 9/16 × 25 11/16 in. (192 × 65.3 cm) Overall with knobs (b): 75 9/16 × 28 1/8 in. (192 × 71.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.61,false,true,53199,Asian Art,Hanging scroll,"蘭竹図|Orchids, Bamboo, Briars, and Rocks",Japan,Nanbokuchō period (1336–92),,,,Artist,,Tesshū Tokusai,"Japanese, died 1366",,Tesshū Tokusai,,1342,1366,mid-14th century,1300,1400,Hanging scroll; ink on paper,Image: 28 3/8 × 14 1/2 in. (72 × 36.8 cm) Overall with mounting: 60 1/16 × 19 5/16 in. (152.5 × 49 cm) Overall with knobs: 60 1/16 × 21 in. (152.5 × 53.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.56,false,true,53198,Asian Art,Hanging scroll,岩に鶺鴒図|Wagtail on a Rock,Japan,Nanbokuchō period (1336–92),,,,Artist,Attributed to,Taikyo Genju,"Japanese, active mid-14th century",,Taikyo Genju,,1300,1366,mid-14th century,1334,1366,Hanging scroll; ink on silk,Image: 32 3/4 × 13 3/4 in. (83.2 × 34.9 cm) Overall with mounting: 63 3/4 × 18 7/16 in. (161.9 × 46.8 cm) Overall with knobs: 63 3/4 × 20 1/4 in. (162 × 51.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.234,false,true,670928,Asian Art,"Page from book, mounted as a hanging scroll",,Japan,late Heian period (794–1185),,,,Artist,Calligraphy by,Fujiwara no Norinaga,"Japanese, 1109–1180",,Fujiwara no Norinaga,,1109,1180,mid-to late 12th century,1134,1199,"Page from book, mounted as hanging scroll; ink on paper",Image: 9 15/16 × 6 1/4 in. (25.3 × 15.9 cm) Overall with mounting: 51 3/16 × 14 3/16 in. (130 × 36 cm) Overall with knobs: 51 3/16 × 16 1/8 in. (130 × 41 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.230,false,true,53172,Asian Art,Album leaf mounted as a hanging scroll,,Japan,late Heian period (ca. 900–1185),,,,Artist,Calligraphy traditionally attributed to,Fujiwara no Yukinari (Kōzei),"Japanese, 972–1027",,Fujiwara no Yukinari,,0972,1027,2nd half of the 11th century,900,1200,Album leaf mounted as a hanging scroll; ink on paper,Image: 8 in. × 5 3/8 in. (20.3 × 13.7 cm) Overall with mounting: 49 3/4 × 14 3/16 in. (126.3 × 36 cm) Overall with knobs: 49 3/4 × 15 7/8 in. (126.3 × 40.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.5,false,true,78147,Asian Art,Hanging scroll,,Japan,Momoyama (1573–1615)–Edo (1615–1868) period,,,,Artist,,Fūgai Ekun,"Japanese, 1568–1654",,Fūgai Ekun,,1568,1654,1568–1654,1568,1654,Hanging scroll; ink on paper,Image: 30 1/2 x 12 1/8 in. (77.5 x 30.8 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.363.1,false,true,711870,Asian Art,Sculpture,不動明王像|Fudō Myōō,Japan,Edo period (1615–1868),,,,Artist,,Mokujiki Shōnin 木喰上人,"Japanese, 1718–1810",,Mokujiki Shōnin 木喰上人,,1718,1810,1805,1805,1805,Chisel-carved (natabori) wood,H. 35 7/16 in. (90 cm); W. 14 9/16 in. (37 cm); D. 9 13/16 in. (25 cm),"Purchase, Friends of Asian Art Gifts, 2016",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/711870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.250a, b",false,true,53175,Asian Art,Sculpture,地蔵菩薩立像|Jizō Bosatsu,Japan,Kamakura period (1185–1333),,,,Artist,,Kaikei,"Japanese, active 1183–1223",,Kaikei,,1183,1223,ca. 1202,1050,1400,"Lacquered Japanese cypress, color, gold, cut gold leaf, and inlaid crystal eyes",H. of figure incl. base 22 in. (55.9 cm); H. to top of spear 22 7/8 in. (58.1 cm); W. 6 3/4 in. (17.1 cm); D. 6 3/4 in. (17.1 cm); Diam. of base 6 3/4 in. (17.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/53175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.252a, b",false,true,53176,Asian Art,Figure,不動明王坐像|Fudō Myōō,Japan,Kamakura period (1185–1333),,,,Artist,,Kaikei,"Japanese, active 1183–1223",,Kaikei,,1183,1223,early 13th century,1100,1400,"Lacquered Japanese cypress, color, gold, cut gold (kirikane), and inlaid crystal eyes",H. 21 in. (53.3 cm); H. to top of sword 21 1/2 in. (54.6 cm); W. 16 3/4 in. (42.5 cm); D. 15 in. (38.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/53176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.2.46,false,true,39671,Asian Art,Tray,,Japan,,,,,Artist,Attributed to,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,,1807,1891,,1807,1891,"Wood, maki-e",L. 10 11/16 in. (27.1 cm); W. 10 5/8 in. (27 cm); H. 1 3/4 in. (4.4 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/39671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.97,false,true,53426,Asian Art,Screens,,Japan,Edo period (1615–1868),,,,Artist,,Ikeda Koson,"Japanese, 1803–1868",,Ikeda Koson,,1803,1868,,1720,1920,Two-panel folding screen; ink on paper,Image: 59 5/16 x 63 1/16 in. (150.6 x 160.2 cm) Overall with mounting: 67 1/2 x 70 1/2 in. (171.5 x 179.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/53426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.96,false,true,45648,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Wagyoku Yogetsu,active 1521–1530,,Wagyoku Yogetsu,,1521,1530,,1521,1530,Hanging scroll; ink on paper,36 1/2 x 14 1/4 in. (92.7 x 36.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.44,false,true,53012,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Ishiyama Moroka,"Japanese, 1669–1734",,Ishiyama Moroka,,1669,1734,,1600,1800,"Handscroll; ink, color, and gold on silk",Image: 13 in. × 25 ft. (33 × 762 cm) Overall with mounting: 14 in. (35.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.129,false,true,670901,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Miyagawa Chōshun,"Japanese, 1683–1753",,Miyagawa Chōshun,,1683,1753,,1615,1868,Hanging scroll; ink and color on paper,Image: 49 3/4 × 20 7/8 in. (126.4 × 53 cm) Overall with mounting: 86 1/4 × 26 1/8 in. (219 × 66.4 cm) Overall with knobs: 86 1/4 × 28 5/8 in. (219 × 72.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.158,false,true,670888,Asian Art,Hanging scrol,,Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,,1716,1783,,1615,1868,Hanging scroll; ink and color on paper,Image: 12 3/16 × 18 3/4 in. (31 × 47.7 cm) Overall with mounting: 44 5/16 × 21 3/16 in. (112.5 × 53.8 cm) Overall with knobs: 44 5/16 × 24 13/16 in. (112.5 × 63 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.233,false,true,75347,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,,1723,1776,Hanging scroll; ink on paper,Image: 42 3/8 x 10 7/8 in. (107.6 x 27.6 cm),"Purchase, Friends of Asian Art Gifts, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.96,false,true,670950,Asian Art,Folding fan mounted as an album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Kiitsu,"Japanese, 1796–1858",,Suzuki Kiitsu,,1796,1858,,1615,1868,"Folding fan mounted as an album leaf; ink and color on paper, framed",Image: 9 7/16 × 20 5/8 in. (24 × 52.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.208,false,true,670894,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Shiokawa Bunrin,"Japanese, 1808–1877",,Shiokawa Bunrin,,1808,1877,,1615,1868,"Hanging scroll; ink, color and gold on silk",Image: 37 3/16 × 13 11/16 in. (94.4 × 34.8 cm) Overall with mounting: 74 13/16 × 19 7/16 in. (190 × 49.3 cm) Overall with knobs: 74 13/16 × 21 5/16 in. (190 × 54.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.144,false,true,73355,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kaseki,"Japanese, active 18th century",,Kaseki,,1700,1799,,1615,1868,Hanging scroll; ink and color on silk,Image: 14 1/2 in. × 21 in. (36.8 × 53.3 cm) Overall with mounting: 50 × 24 5/8 in. (127 × 62.5 cm) Overall with knobs: 50 × 27 3/16 in. (127 × 69 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.124,false,true,670956,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kengetsudō,"Japanese, active 18th century",,Kengetsudō,,1700,1799,,1615,1868,Hanging scroll; ink and color on paper,Image: 41 7/8 × 18 3/8 in. (106.4 × 46.7 cm) Overall with mounting: 65 7/8 × 23 3/16 in. (167.3 × 58.9 cm) Overall with knobs: 65 7/8 × 25 3/8 in. (167.3 × 64.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.209,false,true,671057,Asian Art,Folding fan,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,,1807,1891,,1868,1912,Folding fan; lacquer on paper,Image: 12 3/16 × 19 7/16 in. (31 × 49.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.30,false,true,670968,Asian Art,Album leaf mounted as a hanging scroll,"源氏物語図色紙 「藤袴」|Scene from “Purple Trousers” (“Fujibakama”), from The Tale of Genji (Genji monogatari)",Japan,Momoyama period (1573–1615),,,,Artist,,Tosa Mitsuyoshi,"Japanese, 1539–1613",,TOSA MITSUYOSHI,,1539,1613,,1573,1615,"Album leaf mounted as a hanging scroll; ink, color and gold on paper",Image: 10 1/8 × 8 3/8 in. (25.7 × 21.2 cm) Overall with mounting: 54 5/8 × 15 1/4 in. (138.7 × 38.7 cm) Overall with knobs: 54 5/8 × 17 3/16 in. (138.7 × 43.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.194.4,false,true,57351,Asian Art,Painted panel,,Japan,Muromachi period (1392–1573),,,,Artist,After,Kenkō Shokei,"Japanese, active ca. 1470–after 1523",,Kenkō Shokei,,1460,1523,,1467,1533,Paint on paper,33 1/8 x 12 1/4 in. (84.1 x 31.1 cm),"Gift of Nathan V. Hammer, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.239,false,true,670887,Asian Art,Hanging scroll,「離離原上草一歳一枯榮」 (白居易『草』より)|Couplet from the Chinese Poem “Grasses” by Bai Juyi,Japan,Muromachi period (1392–1573),,,,Artist,,Motsurin Jōtō (Bokusai),"Japanese, died 1491",,Motsurin Jōtō,,,1491,15th century,1400,1499,Hanging scroll; ink on paper,Image: 46 3/4 × 10 3/8 in. (118.8 × 26.4 cm) Overall with mounting: 74 11/16 × 10 15/16 in. (189.7 × 27.8 cm) Overall with knobs: 74 11/16 × 12 1/2 in. (189.7 × 31.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/670887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.9,false,true,60465,Asian Art,Hanging scroll,愚極礼才書 「極重悪人無他方便・唯稱弥陀得生極楽」|Buddhist Maxim on the Saving Power of Amida,Japan,Nanbokuchō period (1336–92),,,,Artist,,Gukyoku Reisai,"Japanese, 1369–1452",,Gukyoku Reisai,,1369,1452,15th century,1400,1452,Pair of hanging scrolls; ink on paper,Image (each scroll): 36 3/4 x 8 3/4 in. (93.4 x 22.3 cm) Overall with mounting (a): 67 1/8 x 9 1/2 in. (170.5 x 24.1 cm) Overall with knobs (a): 67 1/8 x 11 1/4 in. (170.5 x 28.6 cm) Overall with mounting (b): 67 x 9 1/2 in. (170.2 x 24.1 cm) Overall with knobs (b): 67 x 11 3/16 in. (170.2 x 28.4 cm),"Gift of Sylvan Barnet and William Burto, in memory of John M. Rosenfield, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.56,false,true,45245,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Shinno Noami,1397–1471,,Shinno Noami,,1397,1471,15th century,1400,1471,Hanging scroll; ink on paper,31 1/4 x 12 1/2 in. (79.4 x 31.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.62,false,true,670896,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,,Motsurin Jōtō (Bokusai),"Japanese, died 1491",,Motsurin Jōtō,,,1491,15th century,1400,1491,Hanging scroll; ink on paper,Image: 10 3/4 × 16 11/16 in. (27.3 × 42.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.2,false,true,670899,Asian Art,Hanging scroll,"不動明王四童子種字像|Fudō Myōō with Four Attendants, Outlined in Seed Syllables",Japan,Muromachi period (1392–1573),,,,Artist,,Chikai,"Japanese, ca. 1422–ca. 1503",,Chikai,,1422,1503,15th century,1400,1499,Hanging scroll; ink and color on paper,Image: 39 15/16 × 17 1/16 in. (101.5 × 43.4 cm) Overall with mounting: 67 11/16 × 22 15/16 in. (172 × 58.3 cm) Overall with knobs: 67 11/16 × 25 3/16 in. (172 × 64 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.54,false,true,53230,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,,Sesson Shūkei,ca. 1504–ca. 1589,,Sesson Shūkei,,1504,1589,16th century,1504,1589,Hanging scroll; ink and color on paper,Image: 11 7/8 × 18 3/8 in. (30.2 × 46.7 cm) Overall with mounting: 44 in. × 22 5/8 in. (111.8 × 57.4 cm) Overall with knobs: 44 × 24 1/2 in. (111.8 × 62.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.39,false,true,42343,Asian Art,Hanging scroll,禅機(鳥窠道林・白居昜)|Zen Encounter (Niaoke Daolin and Bai Juyi),Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Kenkō Shokei,"Japanese, active ca. 1470–after 1523",,Kenkō Shokei,,1460,1523,16th century,1467,1533,Hanging scroll; ink on paper,12 x 18 3/4 in. (30.5 x 47.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.211a–g,false,true,670969,Asian Art,Fourteen figures on seven folded sheets,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Iwasa Matabei,"Japanese, 1578–1650",,Iwasa Matabei,,1578,1650,17th century,1615,1650,Fourteen figures on seven folded sheets; ink on paper,Image (a): 8 3/4 × 15 11/16 in. (22.3 × 39.9 cm) Image (b): 8 3/4 × 11 1/2 in. (22.3 × 29.2 cm) Image (c): 8 3/4 × 11 9/16 in. (22.3 × 29.3 cm) Image (d): 8 3/4 × 11 3/8 in. (22.3 × 28.9 cm) Image (e): 8 3/4 × 11 9/16 in. (22.3 × 29.4 cm) Image (f): 8 3/4 × 11 3/16 in. (22.3 × 28.4 cm) Image (g): 8 3/4 × 12 3/16 in. (22.3 × 30.9 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/670969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.39a–x,false,true,670914,Asian Art,Twenty-four volumes of printed text and illustrations,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Shunshō,"Japanese, 1610–1682",,Yamamoto Shunshō,,1610,1682,17th century,1610,1682,Twenty-four volumes of printed text and illustrations; black ink on paper,Each book: 10 1/2 × 7 3/8 in. (26.7 × 18.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/670914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.79,false,true,670911,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Kōya,"Japanese, died 1673",,Kano Kōya,,,1673,17th century,1615,1673,Hanging scroll; ink on paper,Image: 35 15/16 × 16 1/16 in. (91.3 × 40.8 cm) Overall with mounting: 67 13/16 × 19 13/16 in. (172.2 × 50.4 cm) Overall with knobs: 67 13/16 × 22 3/16 in. (172.2 × 56.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.38a, b",false,true,76463,Asian Art,Handscrolls,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kaihō Yūsetsu,"Japanese, 1598–1677",,Kaihō Yūsetsu,,1598,1677,17th century,1600,1699,Set of two handscrolls; ink and color on paper,Image (each scroll): 9 7/16 in. × 63 ft. 8 9/16 in. (24 × 1942 cm) Overall with knobs: 11 7/16 in. × 63 ft. 8 9/16 in. (29 × 1942 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/76463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.220,false,true,671033,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Unkoku Tōban,"Japanese, 1633–1724",,Unkoku Tōban,,1633,1724,17th century,1615,1699,Handscroll; ink on silk,Image: 12 3/4 × 19 5/8 in. (32.4 × 49.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.679,false,true,58458,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Gion Nankai,"Japanese, 1677–1751",,Gion Nankai,,1677,1751,18th century,1700,1799,"Lacquer sprinkled with gold and silver makie, and foil; Ojima: floral scrolls in openwork; silver and silver wire; Netsuke: chrysanthemum medallion; ivory",H. 3 1/8 in. (7.9 cm); W. 2 5/8 in. (6.7 cm); D. 13/16 in. (2.1 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.2098,false,true,59281,Asian Art,Netsuke,,Japan,Edo period (1615–1868),,,,Artist,,Matsuda Sukenaga,"Japanese, 1800–1871",,"Sukenaga, Matsuda",,1800,1871,18th century,1700,1799,Wood,H. 1 3/4 in. (4.4 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.27,false,true,48979,Asian Art,Folding screen,俵屋宗理筆 朝顔図屏風|Morning Glories,Japan,Edo period (1615–1868),,,,Artist,,Tawaraya Sōri,active late 18th century,,Tawaraya Sori,,1764,1780,18th century,1700,1799,Two-panel folding screen; ink and color on paper,19 15/16 x 65 3/16 in. (50.6 x 165.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/48979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.42,false,true,671032,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kawamata Tsunemasa,active 1716–48,,Kawamata Tsunemasa,,1706,1758,18th century,1700,1799,Hanging scroll; ink and color on paper,Image: 13 1/8 × 21 9/16 in. (33.4 × 54.7 cm) Overall with mounting: 46 7/16 × 25 3/16 in. (118 × 64 cm) Overall with knobs: 46 7/16 × 27 5/16 in. (118 × 69.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.115,false,true,671008,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,,1671,1750,18th century,1700,1750,Hanging scroll; ink and color on paper,Image: 15 1/16 × 22 7/16 in. (38.3 × 57 cm) Overall with mounting: 49 13/16 × 27 5/16 in. (126.5 × 69.4 cm) Overall with knobs: 49 13/16 × 29 7/16 in. (126.5 × 74.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.8,false,true,45767,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Miyagawa Chōshun,"Japanese, 1683–1753",,Miyagawa Chōshun,,1683,1753,18th century,1700,1753,Hanging scroll; ink and color on silk,24 7/8 x 10 11/16 in. (63.2 x 27.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.128,false,true,670967,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,,1686,1764,18th century,1700,1799,Hanging scroll; ink and color on silk,Image: 12 5/16 × 19 5/8 in. (31.3 × 49.9 cm) Overall with mounting: 49 1/8 × 25 1/8 in. (124.8 × 63.8 cm) Overall with knobs: 49 1/8 × 27 5/16 in. (124.8 × 69.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.219,false,true,670917,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Sumiyoshi Hiromori,"Japanese, 1705–1777",,Sumiyoshi Hiromori,,1705,1777,18th century,1705,1777,"Handscroll; ink, color and gold on paper",Image: 13 3/8 in. × 11 ft. 9 7/16 in. (33.9 × 359.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.40,false,true,671015,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Settei,"Japanese, 1710–1786",,Tsukioka Settei,,1710,1786,18th century,1700,1799,Hanging scroll; ink and color on silk,Image: 34 13/16 × 12 7/16 in. (88.5 × 31.6 cm) Overall with mounting: 70 1/4 × 17 13/16 in. (178.5 × 45.2 cm) Overall with knobs: 70 1/4 × 19 3/4 in. (178.5 × 50.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.55.1,false,true,48990,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink on silk,38 3/4 x 18 3/16 in. (98.4 x 46.2 cm),"Fletcher Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.55.2,false,true,48991,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink on silk,38 3/4 x 18 in. (98.4 x 45.7 cm),"Fletcher Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.93,false,true,48989,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink and color on paper,Image: 37 13/16 x 10 11/16 in. (96 x 27.2 cm) Overall with mounting: 67 3/8 x 15 3/4 in. (171.1 x 40 cm) Overall with knobs: 67 3/8 x 17 7/8 in. (171.1 x 45.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.94,false,true,48993,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink on paper,38 1/16 x 9 15/16 in. (96.7 x 25.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.162,false,true,671046,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Taiga,"Japanese, 1723–1776",,Ike Taiga,,1723,1776,18th century,1723,1776,Hanging scroll; ink and color on paper,Image: 39 in. × 12 1/2 in. (99 × 31.7 cm) Overall with mounting: 75 × 17 15/16 in. (190.5 × 45.5 cm) Overall with knobs: 75 × 20 in. (190.5 × 50.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.199,false,true,670983,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,,1733,1795,18th century,1733,1799,Handscroll; ink on paper,Image: 15 1/2 in. × 18 ft. 2 7/16 in. (39.4 × 554.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.174,false,true,670890,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Totoki Baigai,"Japanese, 1749–1804",,Totoki Baigai,,1749,1804,18th century,1700,1799,Hanging scroll; ink and color on paper,Image: 10 11/16 × 22 5/16 in. (27.1 × 56.7 cm) Overall with mounting: 46 1/4 × 23 15/16 in. (117.5 × 60.8 cm) Overall with knobs: 46 1/4 × 26 in. (117.5 × 66 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.92,false,true,670922,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,,1761,1828,18th century,1700,1799,Fan mounted as a hanging scroll; ink and color on paper,Image: 8 11/16 × 17 11/16 in. (22 × 45 cm) Overall with mounting: 46 1/8 × 20 13/16 in. (117.2 × 52.8 cm) Overall with knobs: 46 1/8 × 22 11/16 in. (117.2 × 57.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.117,false,true,671013,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Sessai,"Japanese, 1761–1839",,Tsukioka Sessai,,1761,1839,18th century,1761,1799,Hanging scroll; ink on silk,Image: 32 × 12 in. (81.3 × 30.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.223,false,true,670902,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Chōgō,"Japanese, active 18th century",,Chōgō,,1700,1799,18th century,1700,1799,Hanging scroll; ink and color on paper,Image: 18 7/8 × 21 5/8 in. (48 × 55 cm) Overall with mounting: 54 1/2 × 24 3/4 in. (138.5 × 62.8 cm) Overall with knobs: 54 1/2 × 26 15/16 in. (138.5 × 68.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.41,false,true,670898,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Matsuno Chikanobu,"Japanese, active early 18th century",,Matsuno Chikanobu,,1700,1733,18th century,1700,1799,Hanging scroll; ink and color on paper,Image: 31 7/16 × 11 5/8 in. (79.9 × 29.6 cm) Overall with mounting: 64 9/16 × 13 7/16 in. (164 × 34.2 cm) Overall with knobs: 64 9/16 × 15 3/4 in. (164 × 40 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.751,false,true,58663,Asian Art,Inrō,,Japan,,,,,Artist,,Kano Terunobu,1717–63,", Yusei",Kano Terunobu,,1717,1763,19th century,1800,1899,"Lacquer, roiro, white lacquer, gold and coloured hiramakie; Interior: nashiji and fundame",3 1/4 x 1 5/8 x 13/16 in. (8.3 x 4.1 x 2.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.894,false,true,78784,Asian Art,Illustrated book,小磯前雪窓先生画帖 完|Album [of works] by the master Koiso Zensetsusō (complete) (Koiso Zensentsusō sensei gajō–kan),Japan,,,,,Artist,,Koiso Zensetsusō,"Japanese, 1832–1902",,"Koiso, Zensetsusō",,1832,1902,19th century,1832,1902,Accordion album; ink and color on paper,11 × 6 1/2 in. (28 × 16.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.736,false,true,58649,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Louisine W. Havemeyer,,,"Havemeyer, Louisine W.",,1855,1929,19th century,1800,1899,Aogai shell and gold foil inlay on black lacquer,3 1/2 x 2 7/16 x 1 in. (8.9 x 6.2 x 2.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.762,false,true,58673,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Louisine W. Havemeyer,,,"Havemeyer, Louisine W.",,1855,1929,19th century,1800,1899,"Lacquer, gold, hirame, gold and coloured hiramakie, takamakie, nashiji; Interior: nashiji and fundame",3 5/16 x 1 15/16 x 1 in. (8.4 x 5 x 2.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.771,false,true,58681,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Louisine W. Havemeyer,,,"Havemeyer, Louisine W.",,1855,1929,19th century,1800,1899,"Lacquer, roiro, hirame, gold and coloured hiramakie, nashiji, various inlay; Interior: fundame",3 3/16 x 2 x 7/8 in. (8.1 x 5.1 x 2.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.840,false,true,58767,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Hogen Dohaku,died 1851,,Hogen Dohaku,,1851,1851,19th century,1800,1899,"Lacquer, fundame, sumie togidashi, applied metals; Interior: nashiji and fundame",3 3/16 x 2 5/16 x 3/4 in. (8.1 x 5.8 x 1.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB76,false,true,57674,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Reizen Saburo Tametaka,died 1864,,Reizen Saburo Tametaka,,1864,1864,19th century,1800,1900,Ink and color on paper,11 × 7 3/4 × 1 3/8 in. (27.9 × 19.7 × 3.5 cm),"Gift of Yamanaka Co., 1926",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.246,false,true,58932,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Tanzan,1655–1729?,,Tanzan,,1655,1729,19th century,1800,1899,"Sprinkled gold and silver lacquer, makie, and takamakie Ojime: bead; tortoiseshell Netuske: box with decoration of violets; gold makie lacquer with gold and silver makie",3 1/2 x 2 3/16 x 1 in. (8.9 x 5.5 x 2.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.778,false,true,58688,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Fang Shi Mopu,ca. 1588,,Fang Shi Mopu,,1588,1588,19th century,1800,1899,"Black hiramaki-e, takamaki-e, ceramic and mother-of-pearl inlay; Interior: Roiro and fundame",3 1/16 x 1 7/8 x 3/4 in. (7.8 x 4.8 x 1.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.2.56a–g,false,true,40489,Asian Art,Writing box,,Japan,Meiji period (1868–1912),,,,Artist,Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,,1663,1747,19th century,1700,1900,Gold maki-e on black lacquer,L. 8 3/4 in. (22.2 cm); W. 9 in. (22.9 cm); H. 2 in. (5.1 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/40489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +79.2.954,false,true,47053,Asian Art,Vase,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1899,Clay covered with polychrome glazes on ornaments outlined in relief (Kairakuen ware),H. 6 7/8 in. (17.5 cm); Diam. 4 3/8 in. (11.1 cm); Diam. of rim 3 1/8 in. (7.9 cm); Diam. of base 2 1/2 in. (6.4 cm),"Purchase by subscription, 1879",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +79.2.978,false,true,46687,Asian Art,Vase,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1899,Clay covered with glazes (Kairakuen ware),H. 5 3/4 in. (14.6 cm); Diam. 3 3/8 in. (8.6 cm); Diam. of rim 2 1/8 in. (5.4 cm); Diam. of base 2 1/4 in. (5.7 cm),"Purchase by subscription, 1879",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.168,false,true,47519,Asian Art,Flower pot,,Japan,,,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1899,Paste covered with a transparent crackled glaze (Kyoto ware),H. 7 1/8 in. (18.1 cm); Diam. 7 5/8 in. (19.4 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"79.2.1338a, b",false,true,62536,Asian Art,Covered bowl,,Japan,,,,,Artist,in the style of,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1899,Porcelain with polychrome glaze (Kairakuen ware),H. 3/1/4 in. (8.3 cm); W. 5 in. (12.7 cm),"Purchase by subscription, 1879",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.490,false,true,45869,Asian Art,Vase,,Japan,,,,,Artist,,Tanzan,1655–1729?,,Tanzan,,1655,1729,19th century,1800,1899,Clay decorated with slip under a transparent glaze andcolored enamels (Nabeshima ware),H. 18 1/8 in. (46 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.491,false,true,45870,Asian Art,Vase,,Japan,,,,,Artist,,Tanzan,1655–1729?,,Tanzan,,1655,1729,19th century,1800,1899,Clay decorated with slip under a transparent glaze andcolored enamels (Nabeshima ware),H. 18 1/8 in. (46 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.43,false,true,667265,Asian Art,Bowl,,Japan,Edo (1615–1868),,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,,1783,1855,19th century,1800,1868,Stoneware with polychrome enamels,H. 4 in. (10.2 cm); Diam. 7 3/16 in. (18.2 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/667265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.41,false,true,667263,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,,1783,1855,19th century,1800,1855,Stoneware with light blue glaze (Kyoto ware),W. 8 1/4 in. (21 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/667263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.44,false,true,667266,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,,1783,1855,19th century,1800,1899,Stoneware with polychrome enamels,Diam. 6 1/2 in. (16.5 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/667266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.84,false,true,49052,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Okamoto Toyohiko,1773–1845,,Okamoto Toyohiko,,1773,1845,19th century,1800,1845,Hanging scroll; ink and color on silk,15 3/4 x 27 3/4 in. (40 x 70.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.85,false,true,40010,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okamoto Toyohiko,1773–1845,,Okamoto Toyohiko,,1773,1845,19th century,1800,1845,Hanging scroll; ink and color on silk,43 x 16 1/2 in. (109.2 x 41.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.77.2,false,true,49086,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1854,Hanging scroll; ink and color on paper,Overall: 47 1/2 x 16 7/8 in. (120.7 x 42.9 cm) Overall with mounting: 67 3/8 x 21 3/4 in. (171.1 x 55.2 cm) Overall with knobs: 67 3/8 x 23 3/4 in. (171.1 x 60.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.77.3,false,true,49087,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1854,Hanging scroll; ink and color on paper,Overall: 47 1/4 x 16 7/8 in. (120 x 42.9 cm) Overall with mounting: 67 x 21 3/4 in. (170.2 x 55.2 cm) Overall with knobs: 67 x 23 3/4 in. (170.2 x 60.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.77.4,false,true,49088,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1854,Hanging scroll; ink and color on paper,Overall: 47 1/2 x 16 7/8 in. (120.7 x 42.9 cm) Overall with mounting: 67 x 21 3/4 in. (170.2 x 55.2 cm) Overall with knobs: 67 x 23 3/4 in. (170.2 x 60.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.77.5,false,true,49089,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1854,Hanging scroll; ink and color on paper,Overall: 47 1/2 x 16 7/8 in. (120.7 x 42.9 cm) Overall with mounting: 67 1/4 x 21 7/8 in. (170.8 x 55.6 cm) Overall with knobs: 67 1/4 x 23 3/4 in. (170.8 x 60.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.77.6,false,true,49090,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1854,Hanging scroll; ink and color on paper,Overall: 47 1/2 x 16 7/8 in. (120.7 x 42.9 cm) Overall with mounting: 67 1/4 x 21 7/8 in. (170.8 x 55.6 cm) Overall with knobs: 67 1/4 x 23 3/4 in. (170.8 x 60.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.77.7,false,true,49091,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Hozen,1795–1854,,Eiraku Hozen,,1795,1854,19th century,1800,1854,Hanging scroll; ink and color on paper,Overall: 47 1/2 x 16 3/4 in. (120.7 x 42.5 cm) Overall with mounting: 67 1/4 x 21 7/8 in. (170.8 x 55.6 cm) Overall with knobs: 67 1/4 x 23 3/4 in. (170.8 x 60.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.83,false,true,48999,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Senkaku Toshu,1804–1871,,Senkaku Toshu,,1804,1871,19th century,1804,1871,Hanging scroll; ink and color on silk,37 x 14 in. (94.0 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.218a, b",false,true,671051,Asian Art,Two folding fans mounted on panels,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Okada Tamechika,1823–1864,,Okada Tamechika,,1823,1864,19th century,1823,1868,"Two folding fans mounted on panels; ink, color and gold on paper",Image (a): 9 5/16 × 19 1/8 in. (23.7 × 48.6 cm) Frame (a): 16 1/8 × 29 1/8 in. (41 × 74 cm) Image (b): 9 1/4 × 19 3/16 in. (23.5 × 48.7 cm) Frame (b): 16 1/8 × 29 1/8 in. (41 × 74 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.160,false,true,670934,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yokoi Kinkoku,"Japanese, 1761–1832",,Yokoi Kinkoku,,1761,1832,19th century,1800,1899,Hanging scroll; ink and color on paper,Image: 31 1/2 × 59 1/8 in. (80 × 150.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.161,false,true,670945,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yokoi Kinkoku,"Japanese, 1761–1832",,Yokoi Kinkoku,,1761,1832,19th century,1800,1868,Hanging scroll; ink and light color on paper,Image: 42 3/4 × 17 5/8 in. (108.6 × 44.8 cm) Overall with mounting: 72 5/8 × 24 7/16 in. (184.5 × 62 cm) Overall with knobs: 72 5/8 × 26 7/8 in. (184.5 × 68.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.151,false,true,670925,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,,1771,1844,19th century,1800,1844,Hanging scroll; ink and color on paper,Image: 41 1/8 in. × 11 in. (104.5 × 28 cm) Overall with mounting: 76 × 11 1/4 in. (193 × 28.5 cm) Overall with knobs: 76 × 17 1/4 in. (193 × 43.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.148,false,true,671047,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni II,"Japanese, 1777–1835",,Utagawa Toyokuni II,,1777,1835,19th century,1800,1868,Hanging scroll; ink and color on silk,Image: 15 7/8 × 21 7/16 in. (40.3 × 54.4 cm) Overall with mounting: 44 11/16 × 25 5/16 in. (113.5 × 64.3 cm) Overall with knobs: 44 11/16 × 27 3/8 in. (113.5 × 69.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.99a, b",false,true,54773,Asian Art,Hanging scrolls,,Japan,Edo period (1615–1868),,,,Artist,,Ikeda Koson,"Japanese, 1803–1868",,Ikeda Koson,,1803,1868,19th century,1801,1866,"Pair of hanging scrolls; ink, color, and gold on silk",Image (a): 42 3/8 × 14 3/16 in. (107.7 × 36 cm) Overall with mounting (a): 76 3/16 × 18 11/16 in. (193.5 × 47.5 cm) Overall with knobs: 76 3/16 × 20 3/4 in. (193.5 × 52.7 cm) Image (b): 42 5/8 × 14 1/4 in. (108.3 × 36.2 cm) Overall with mounting (b): 76 3/8 × 18 3/4 in. (194 × 47.6 cm) Overall with knobs (b): 76 3/8 × 20 13/16 in. (194 × 52.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.101,false,true,670980,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sakai Ōho,"Japanese, 1808–1841",,Sakai Ōho,,1808,1841,19th century,1808,1841,Hanging scroll; ink and color on silk,Image: 40 1/2 in. × 14 in. (102.9 × 35.6 cm) Overall with mounting: 76 1/8 × 19 7/16 in. (193.3 × 49.3 cm) Overall with knobs: 76 1/8 × 21 5/16 in. (193.3 × 54.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.207,false,true,670951,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishiyama Kan'ei,"Japanese, 1834–1897",,Nishiyama Kan'ei,,1834,1897,19th century,1834,1897,Hanging scroll; ink and color on silk,Image: 13 1/8 × 32 1/8 in. (33.4 × 81.6 cm) Overall with mounting: 51 × 33 1/8 in. (129.5 × 84.2 cm) Overall with knobs: 51 × 35 5/16 in. (129.5 × 89.7 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.85,false,true,670919,Asian Art,Hanging scroll,,Japan,Edo (1615–1868)–Meiji period (1868–1912),,,,Artist,,Kano Hōgai,"Japanese, 1828–1888",,Kano Hōgai,,1828,1888,19th century,1868,1899,Hanging scroll; ink on paper,Image: 50 7/8 × 12 11/16 in. (129.2 × 32.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.203.1, .2",false,true,671026,Asian Art,Sliding panels,雪狗子図襖|Puppies in the Snow,Japan,Edo period (1615–1868),,,,Artist,,Nagasawa Rosetsu,"Japanese, 1754–1799",,Nagasawa Rosetsu,,1754,1799,late 18th century,1767,1799,Set of four sliding panels hinged together as a pair of two-panel screens; ink and color on paper,Image: 66 7/16 × 72 1/16 in. (168.7 × 183 cm) Overall with mounting: 68 × 74 5/8 in. (172.7 × 189.6 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/671026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1508,false,true,53342,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harushige,1747–1818,,Suzuki Harushige,,1747,1818,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,H. 11 1/4 in. (28.6 cm); 8 1/4 in. (21 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.114,false,true,671007,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,,1671,1750,late 18th century,1767,1799,Hanging scroll; ink and color on paper,Image: 33 3/4 in. × 13 in. (85.8 × 33 cm) Overall with mounting: 65 9/16 × 14 7/16 in. (166.5 × 36.7 cm) Overall with knobs: 65 9/16 × 17 1/16 in. (166.5 × 43.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.602,false,true,679649,Asian Art,Hanging scroll,百年無事人|For a hundred years [I have been] a person with no attachments,Japan,Edo period (1615–1868),,,,Artist,,Jiun Sonja,"Japanese, 1718–1804",,Sonja Jiun,,1718,1804,late 18th century,1766,1799,Hanging scroll; ink on paper,Image: 46 1/2 × 10 7/8 in. (118.1 × 27.6 cm) Overall with mounting: 74 × 11 5/8 in. (188 × 29.5 cm) Overall with knobs: 13 5/8 in. (34.6 cm),"Gift of Morton Berman, in honor of Sylvan Barnet and William Burto, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/679649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.170,false,true,670910,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kuwayama Gyokushū,"Japanese, 1746–1799",,Kuwayama Gyokushū,,1746,1799,late 18th century,1767,1799,Hanging scroll; ink on silk,Image: 39 11/16 × 14 9/16 in. (100.8 × 37 cm) Overall with mounting: 79 5/16 × 22 3/16 in. (201.5 × 56.3 cm) Overall with knobs: 79 5/16 × 24 7/16 in. (201.5 × 62 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.204,false,true,670999,Asian Art,Hanging scroll,飲中八仙図|Drinking Festival of the Eight Immortals,Japan,Edo period (1615–1868),,,,Artist,,Nagasawa Rosetsu,"Japanese, 1754–1799",,Nagasawa Rosetsu,,1754,1799,late 18th century,1767,1799,Hanging scroll; ink and color on paper,Image: 51 5/8 × 23 11/16 in. (131.1 × 60.1 cm) Overall with mounting: 88 3/4 × 29 11/16 in. (225.5 × 75.4 cm) Overall with knobs: 88 3/4 × 32 1/16 in. (225.5 × 81.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.205,false,true,671000,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nagasawa Rosetsu,"Japanese, 1754–1799",,Nagasawa Rosetsu,,1754,1799,late 18th century,1767,1799,Hanging scroll; ink and color on silk,Image: 39 3/4 × 13 15/16 in. (101 × 35.4 cm) Overall with mounting: 74 3/16 × 19 5/16 in. (188.5 × 49 cm) Overall with knobs: 74 3/16 × 21 1/4 in. (188.5 × 54 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.14,false,true,78136,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Miyagawa (Katsukawa) Shunsui,"Japanese, active 1744–64",,Miyagawa (Katsukawa) Shunsui,,1744,1764,late 18th century,1744,1764,Hanging scroll; ink and color on silk,Image: 50 3/8 x 24 7/16 in. (128 x 62 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.500.2.43a, b",false,true,40493,Asian Art,Smoking set,刻み煙草入れ|Portable Smoking Set,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,,1807,1891,early 19th century,1800,1849,"Pipe: iron, gold, silver on wood; Pipe case: gold, silver hiramaki-e on black; Tobacco case: dyed cotton with metal fitting of a snail; Netsuke: carved staghorn with paulownia pattern",Pipe case: W. 3/4 in.; D. 5/8 in.; L. 11 1/4 in.; Pipe: L.7 3/4 in.; Tobacco case: H. 2 3/4 in.; W. 4 1/8 in.; D. 1 1/2 in.; Netsuke: Diam. 1 1/2 in.,"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/40493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.146,false,true,671041,Asian Art,Hanging scroll,桜下遊女と禿|Courtesan and her Attendant under a Cherry Tree,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,,1735,1814,early 19th century,1800,1833,Hanging scroll; ink and color on silk,Image: 35 5/16 in. × 14 in. (89.7 × 35.5 cm) Overall with mounting: 67 1/2 × 18 3/8 in. (171.5 × 46.7 cm) Overall with knobs: 67 1/2 × 20 1/2 in. (171.5 × 52.1 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.17,false,true,78055,Asian Art,Hanging scroll,"「天満宮」 渡唐天神図|“Tenmangū,” Sugawara no Michizane as Tenjin Traveling to China",Japan,Edo period (1615–1868),,,,Artist,,Sengai Gibon,"Japanese, 1750–1837",,Sengai Gibon,,1750,1837,early 19th century,1800,1833,Hanging scroll; ink on paper,Image: 150 13/16 x 17 11/16 in. (383 x 45 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.145,false,true,670908,Asian Art,Handscroll,三幅神吉原通い図巻 「全盛季春遊戯」|Three Gods of Good Fortune Visit the Yoshiwara; or “Scenes of Pleasure at the Height of Spring”,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,,1756,1829,early 19th century,1800,1833,Handscroll; ink and color on silk,Image: 13 1/8 in. × 29 ft. 2 9/16 in. (33.3 × 890.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.142a–c,false,true,53448,Asian Art,Hanging scrols,"蜀山人(大田 南畝)賛 雪・月・花図 |Snow, Moon, and Cherry Blossoms (Yoshiwara in Three Seasons)",Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,,1756,1829,early 19th century,1804,1815,"Triptych of hanging scrolls; ink, color, and gold on silk",Image (a): 32 3/8 × 11 13/16 in. (82.3 × 30 cm) Overall with mounting (a): 65 1/2 × 16 3/8 in. (166.3 × 41.6 cm) Overall with knobs (a): 65 1/2 × 18 9/16 in. (166.3 × 47.2 cm) Image (b): 32 3/8 × 11 13/16 in. (82.2 × 30 cm) Overall with mounting (b): 65 1/2 × 16 7/16 in. (166.3 × 41.7 cm) Overall with knobs (b): 65 1/2 × 18 9/16 in. (166.3 × 47.2 cm) Image (c): 32 3/8 × 11 3/4 in. (82.2 × 29.9 cm) Overall with mounting (c): 65 3/8 × 16 7/16 in. (166 × 41.7 cm) Overall with knobs (c): 65 3/8 × 18 9/16 in. (166 × 47.2 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.45,false,true,78065,Asian Art,Hanging Scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kawahara Keiga,"Japanese, 1786–1860",,"Keiga, Kawahara",,1786,1860,early 19th century,1800,1833,"Hanging scroll; ink and color on silk, negoro lacquer roller knobs",Image: 19 3/4 x 9 7/8 in. (50.2 x 25.1 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/78065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.200,false,true,670982,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Maruyama Ōshin,"Japanese, 1790–1838",,Maruyama Ōshin,,1790,1838,early 19th century,1800,1838,Hanging scroll; ink and color on silk,Image: 39 1/4 × 14 5/16 in. (99.7 × 36.3 cm) Overall with mounting: 77 3/8 × 19 13/16 in. (196.5 × 50.4 cm) Overall with knobs: 77 3/8 × 22 3/16 in. (196.5 × 56.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.755,false,true,58666,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Fang Shi Mopu,ca. 1588,,Fang Shi Mopu,,1588,1588,18th–19th century,1700,1899,"Lacquer, roiro, black, gold, red hiramakie, takamakie, ceramic inlay; Interior: roiro and fundame",2 15/16 x 1 3/4 x 11/16 in. (7.5 x 4.4 x 1.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.46,false,true,48898,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Suga Mitsusada,1738–1806,,Suga Mitsusada,,1738,1806,18th–19th century,1738,1806,Hanging scroll; ink and color on silk,40 x 12 3/16 in. (101.6 x 30.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.47,false,true,48899,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Suga Mitsusada,1738–1806,,Suga Mitsusada,,1738,1806,18th–19th century,1738,1806,Hanging scroll; ink and color on silk,40 x 12 3/16 in. (101.6 x 30.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.48,false,true,48900,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Suga Mitsusada,1738–1806,,Suga Mitsusada,,1738,1806,18th–19th century,1738,1806,Hanging scroll; ink and color on silk,40 x 12 3/16 in. (101.6 x 30.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.171,false,true,670949,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Minagawa Kien,"Japanese, 1734–1807",,Minagawa Kien,,1734,1807,18th–19th century,1734,1807,Hanging scroll; ink on paper,Image: 41 1/4 × 12 1/2 in. (104.7 × 31.7 cm) Overall with mounting: 68 7/16 × 18 1/4 in. (173.8 × 46.4 cm) Overall with knobs: 68 7/16 × 20 3/8 in. (173.8 × 51.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.135,false,true,49022,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Unpō,"Japanese, 1765–1848",,Unpō,,1765,1848,18th–19th century,1749,1848,Hanging scroll; ink on paper,49 1/2 x 11 9/16 in. (125.7 x 29.3 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.9.46,false,true,78165,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist|Artist,Calligraphy by|Underpainting attributed to,Hon'ami Kōetsu|Tawaraya Sōtatsu,"Japanese, 1558–1637|Japanese, died ca. 1640",,Hon'ami Kōetsu|Tawaraya Sōtatsu,,1558 |1540,1637 |1640,mid-1620s,1624,1626,"Section of a handscroll, mounted as a hanging scroll",Image: 12 5/8 x 20 3/4 in. (32.1 x 52.7 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/78165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.216,false,true,53410,Asian Art,Hanging scroll,天台山石橋図|Lions at the Stone Bridge of Mount Tiantai,Japan,Edo period (1615–1868),,,,Artist|Artist,Inscribed by,Soga Shōhaku|Gazan Yō Nansō,"Japanese, 1730–1781|Japanese, 1727–1797",,Soga Shōhaku|Gazan Yō Nansō,,1730 |1727,1781 |1797,1779,1600,1850,Hanging scroll; ink on silk,Image: 44 7/8 in. × 20 in. (114 × 50.8 cm) Overall with mounting: 79 1/8 × 25 3/16 in. (201 × 64 cm) Overall with knobs: 79 1/8 × 27 1/2 in. (201 × 69.8 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53410,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.87,false,true,52987,Asian Art,Handscroll,木版下絵和歌巻断簡|Twelve Poems from the New Collection of Poems Ancient and Modern (Shin kokin wakashū),Japan,Edo period (1615–1868),,,,Artist|Artist,Calligraphy by|Printed designs by a follower of,Hon'ami Kōetsu|Tawaraya Sōtatsu,"Japanese, 1558–1637|Japanese, died ca. 1640",,Hon'ami Kōetsu|Tawaraya Sōtatsu,,1558 |1540,1637 |1640,ca. 1620,1550,1700,Handscroll; ink and gold on silk,Image: 13 3/8 in. × 16 ft. 1/8 in. (34 × 488 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/52987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.121,false,true,671001,Asian Art,Hanging scroll,立美人図|Standing Courtesan,Japan,Edo period (1615–1868),,,,Artist|Artist,Inscribed by,Kamo no Suketame|Tōsendō Rifū,"Japanese, 1740–1801|Japanese, active ca. 1730",,Kamo no Suketame|Tōsendō Rifū,,1740 |1720,1801 |1740,ca. 1720,1710,1730,Hanging scroll; ink and color on silk,Image: 28 1/8 × 13 1/8 in. (71.4 × 33.4 cm) Overall with mounting: 59 1/16 in. (150 cm) Overall with knobs: 59 1/16 × 17 7/16 in. (150 × 44.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.300.69a, b",false,true,53238,Asian Art,Hanging scrolls,政黄牛・郁山主図|Zheng Huangniu and Yushanzhu,Japan,Momoyama period (1573–1615),,,,Artist|Artist,Inscribed by,Takuan Sōhō|Kano Naizen,"1573–1645|Japanese, 1570–1616",,Takuan Sōhō|Kano Naizen,,1573 |1570,1645 |1616,early 17th century,1600,1633,Pair of hanging scrolls; ink on paper,Image (a): 44 in. × 18 11/16 in. (111.7 × 47.4 cm) Overall with mounting (a): 78 11/16 × 24 5/8 in. (199.8 × 62.6 cm) Overall with knobs (a): 78 11/16 × 26 9/16 in. (199.8 × 67.5 cm) Image (b): 44 1/16 × 18 11/16 in. (111.9 × 47.4 cm) Overall with mounting (b): 78 15/16 × 24 1/2 in. (200.5 × 62.3 cm) Overall with knobs (b): 78 15/16 × 26 9/16 in. (200.5 × 67.4 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.71,false,true,671040,Asian Art,Hanging scroll,達磨図|Bodhidharma,Japan,Momoyama period (1573–1615),,,,Artist|Artist,Inscribed by,Unkoku Tōgan|Gyokuho Jōsō,"Japanese, 1547–1618|Japanese, 1546–1613",,Unkoku Tōgan|Gyokuho Jōsō,,1547 |1546,1618 |1613,late 16th–early 17th century,1567,1633,Hanging scroll; ink on paper,Image: 35 1/4 × 13 1/8 in. (89.6 × 33.4 cm) Overall with mounting: 70 7/8 × 18 9/16 in. (180 × 47.2 cm) Overall with knobs: 70 7/8 × 20 9/16 in. (180 × 52.3 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.59,false,true,671052,Asian Art,Hanging scroll,枝に小禽図|Bird on a Branch,Japan,Muromachi period (1392–1573),,,,Artist|Artist,Inscribed by,Daiko Shōkaku|Unkei Eii,"Japanese, died 1535|Japanese, active first half of the 16th century",,Daiko Shōkaku|Unkei Eii,,1500,1535 |1549,early 16th century,1500,1535,Hanging scroll; ink on paper,Image: 9 5/16 × 10 13/16 in. (23.7 × 27.5 cm) Overall with mounting: 45 7/16 × 16 1/8 in. (115.4 × 41 cm) Overall with knobs: 45 7/16 × 17 15/16 in. (115.4 × 45.5 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.60,false,true,671055,Asian Art,Hanging scroll,藻鯉図|Carp and Waterweeds,Japan,Muromachi period (1392–1573),,,,Artist|Artist,Inscribed by,Yōgetsu|Mokumoku Dōjin,"Japanese, active late 15th century|Japanese, active late 15th century",,Yōgetsu|Mokumoku Dōjin,,1467 |1467,1499 |1499,late 15th century,1467,1499,Hanging scroll; ink on silk,Image: 33 7/16 × 13 7/8 in. (85 × 35.2 cm) Overall with mounting: 65 3/4 × 19 1/16 in. (167 × 48.4 cm) Overall with knobs: 65 3/4 × 20 7/8 in. (167 × 53 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.500.2.44,false,true,40494,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist|Artist,copied from a design by,Hon'ami Kōetsu|Shibata Zeshin,"Japanese, 1558–1637|Japanese, 1807–1891",,Hon'ami Kōetsu|Shibata Zeshin,,1558 |1807,1637 |1891,19th century,1800,1899,Gold maki-e with mother-of-pearl inlay on black lacquer; netsuke of hardwood with cloisonne,H.1 7/8 in. (4.8 cm); W. 1 1/2 in. (3.8 cm),"Gift of Florence and Herbert Irving, 2015",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/40494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.300.215,false,true,670929,Asian Art,Hanging scroll,伊藤若冲筆 伝池大雅賛 寒山拾得図|Hanshan and Shide (Japanese: Kanzan and Jittoku),Japan,Edo period (1615–1868),,,,Artist|Artist,Calligraphy attributed to,Itō Jakuchū|Ike Taiga,"Japanese, 1716–1800|Japanese, 1723–1776",,Itō Jakuchū|Ike Taiga,,1716 |1723,1800 |1776,late 18th century,1767,1799,Hanging scroll; ink on paper,Image: 39 1/2 × 11 15/16 in. (100.4 × 30.3 cm) Overall with mounting: 69 11/16 × 15 3/8 in. (177 × 39 cm) Overall with knobs: 69 11/16 × 17 5/16 in. (177 × 44 cm),"Mary Griggs Burke Collection, Gift of the Mary and Jackson Burke Foundation, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2002.447.120a, b",false,true,49420,Asian Art,Teapot,,Japan,Edo period (1615–1868),,,,Artist,Design attributed to,Olfert Dapper,"Dutch, 1635–1689",,"Dapper, Olfert",Dutch,1639,1689,early 18th century,1700,1733,Porcelain painted with cobalt blue under transparent glaze (Jingdezhen ware),H. 6 1/2 in. (16.5 cm); W. 9 3/4 in. (24.8 cm),"Dr. and Mrs. Roger G. Gerry Collection, Bequest of Dr. and Mrs. Roger G. Gerry, 2000",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/49420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.447.121,false,true,49421,Asian Art,Plate,,Japan,Edo period (1615–1868),,,,Artist,Design attributed to,Cornelis Pronk,"Dutch, Amsterdam 1691–1759 Amsterdam",,"Pronk, Cornelis",Dutch,1691,1759,ca. 1734–37,1734,1737,Porcelain painted with cobalt blue under and colored enamels over transparent glaze (Hizen ware; Imari type),H. 1 1/4 in. (3.2 cm); Diam. 10 1/2 in. (26.7 cm),"Dr. and Mrs. Roger G. Gerry Collection, Bequest of Dr. and Mrs. Roger G. Gerry, 2000",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/49421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.447.122,false,true,49422,Asian Art,Dish,,Japan,Edo period (1615–1868),,,,Artist,Design attributed to,Cornelis Pronk,"Dutch, Amsterdam 1691–1759 Amsterdam",,"Pronk, Cornelis",Dutch,1691,1759,ca. 1734–37,1734,1737,Porcelain painted with cobalt blue under and colored enamels over transparent glaze (Hizen ware; Imari type),H. 1 in. (2.5 cm); Diam. 9 3/8 in. (23.9 cm),"Dr. and Mrs. Roger G. Gerry Collection, Bequest of Dr. and Mrs. Roger G. Gerry, 2000",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/49422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.447.123,false,true,49423,Asian Art,Plate,,Japan,Edo period (1615–1868),,,,Artist,Design attributed to,Cornelis Pronk,"Dutch, Amsterdam 1691–1759 Amsterdam",,"Pronk, Cornelis",Dutch,1691,1759,ca. 1734–37,1734,1737,Porcelain painted with cobalt blue under and colored enamels over transparent glaze (Hizen ware; Imari type),Diam. 10 1/2 in. (26.7 cm),"Dr. and Mrs. Roger G. Gerry Collection, Bequest of Dr. and Mrs. Roger G. Gerry, 2000",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/49423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.447.72,false,true,49324,Asian Art,Barber's bowl,,Japan,Edo period (1615–1868),,,,Artist,Design by,Cornelis Pronk,"Dutch, Amsterdam 1691–1759 Amsterdam",,"Pronk, Cornelis",Dutch,1691,1759,18th century,1700,1799,"Porcelain with underglaze blue (Hizen ware, Ko Imari type)",H. 3 7/16 in. (8.7 cm); Diam. 12 1/16 in. (30.6 cm),"Dr. and Mrs. Roger G. Gerry Collection, Bequest of Dr. and Mrs. Roger G. Gerry, 2000",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/49324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.134,false,true,44632,Asian Art,Handscroll,"清 郎世寧 百駿圖白描稿 卷|One Hundred Horses",China,Qing dynasty (1644–1911),,,,Artist,,Giuseppe Castiglione,"Italian, Milan 1688–1766 Beijing",,"Castiglione, Giuseppe",Italian,1688,1766,datable to 1723–25,1723,1725,Handscroll; ink on paper,Image: 37 in. x 25 ft. 10 3/4 in. (94 x 789.3 cm) Overall with mounting: H. 38 1/2 in. (97.8 cm),"Purchase, Friends of Asian Art Gifts, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.84,false,true,51582,Asian Art,Album leaf,,China,Qing dynasty (1644–1911),,,,Artist|Artist,In the style of,Unidentified Artist|Giuseppe Castiglione,"Italian, Milan 1688–1766 Beijing",,"Unidentified Artist|Castiglione, Giuseppe",Italian,1688,1766,,1644,1911,Album leaf; color on silk,12 3/4 x 11 1/4 in. (32.4 x 28.6 cm),"Rogers Fund, 1930",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.220.121,false,true,51513,Asian Art,Hanging scroll,,China,Qing dynasty (1644–1911),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Giuseppe Castiglione,"Italian, Milan 1688–1766 Beijing",,"Unidentified Artist|Castiglione, Giuseppe",Italian,1688,1766,,1644,1911,Hanging scroll,Overall: 24 3/4 x 54 in. (62.9 x 137.2 cm),"John Stewart Kennedy Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.513,false,true,48907,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist|Artist,Attributed to|In the Style of,Tsukioka Sessai|Giuseppe Castiglione,"Japanese, 1761–1839|Italian, Milan 1688–1766 Beijing",,"Tsukioka Sessai|Castiglione, Giuseppe",Italian,1761 |1688,1839 |1766,19th century,1800,1839,Framed painting; ink and color on silk,35 3/4 x 27 in. (90.8 x 68.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.468,false,true,51563,Asian Art,Painting,,China,Ming dynasty (?) (1368–1644),,,,Artist|Artist,Formerly Attributed to,Unidentified Artist|Gessen,1721–1809,,Unidentified Artist|Gessen,Japanese,1721,1809,,1368,1644,Painting; ink and color on silk,Image: 10 × 23 in. (25.4 × 58.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/51563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB164,false,true,57855,Asian Art,Illustrated book,Hyakunin joro shinasadame|百人女郎品定|One Hundred Women Classified According to Their Rank (Hyakunin joro shinasadame),Japan,Edo period (1615–1868),,,,Artist|Calligrapher,Preface by,Nishikawa Sukenobu|Jisho,"Japanese, 1671–1750",,Nishikawa Sukenobu|Jisho,Japanese,1671,1750,1723,1723,1723,"Vol. I: 9 double and 2 single page illustrations; vol. II: 15 double, 1 single page illustrations; ink on paper",Overall: 10 x 7 1/2in. (25.4 x 19.1cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.797,false,true,78813,Asian Art,Illustrated book,,Japan,Momoyama period (1573–1615),,,,Artist|Calligrapher|Calligrapher,in the style of,Unidentified Artist|Unidentified Artist|Hon'ami Kōetsu,"Japanese, 1558–1637",Japanese|Japanese,Unidentified Artist|Unidentified Artist|Hon'ami Kōetsu,Japanese,1558,1637,possibly ca. 1610,1600,1620,Woodblock printed book; ink on paper,13 3/4 × 10 1/8 in. (35 × 25.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1550,false,true,55714,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1551,false,true,55715,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1553,false,true,55716,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1554,false,true,55717,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1555,false,true,55718,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1556,false,true,55719,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1557,false,true,44999,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852–53,1852,1853,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1558,false,true,55722,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1559,false,true,55724,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1560,false,true,55725,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 5/8 in. (24.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1561,false,true,55728,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Calligrapher|Artist,Engraved by,Yokogawa Horitake|Utagawa Kuniyoshi,"Japanese, 1797–1861",,Yokogawa Horitake|Utagawa Kuniyoshi,Japanese,1797,1861,1852,1852,1852,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.34,false,true,44908,Asian Art,Hanging scroll,,Japan,Kamakura period (1185–1333),,,,Calligrapher,Calligraphy attributed to,Fujiwara no Nobuzane,"Japanese, 1176–1265",,Fujiwara no Nobuzane,Japanese,1176,1265,13th century,1200,1265,Section of a handscroll mounted as a hanging scroll; ink on paper,Image: 11 3/4 x 7 1/2 in. (29.8 x 19.1 cm) Overall: 57 1/2 x 17 7/8in. (146.1 x 45.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.162,false,true,49081,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Calligrapher,,Kobori Enshū,1579–1647,,Kobori Enshū,Japanese,1579,1579,17th century,1600,1647,Hanging scroll; ink on paper,8 3/4 x 11 3/4 in. (22.3 x 29.9 cm),"Gift of Mr. and Mrs. H. Jack Lang, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +94.18.1a–xx,false,true,45427,Asian Art,Album,源氏物語画帖|The Tale of Genji (Genji Monogatari),Japan,Edo period (1615–1868),,,,Calligrapher,,Shōren'in Sonjun Shinnō,"Japanese, 1581–1653",,Shōren'in Sonjun Shinnō,Japanese,1581,1653,17th century,1738,1806,"Set of twenty-four album leaves; ink, gold and color on paper",10 3/8 x 9 1/4 in. (26.3 x 23.5 cm),"Gift of Mary L. Cassilly, 1894",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2075,false,true,54906,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist|Calligrapher,,Teisai Hokuba|Chou,"Japanese, 1771–1844",,Teisai Hokuba|Chou,Japanese,1771,1844,early 19th century,1800,1833,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 1/2 in. (14.1 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.2319,false,true,59380,Asian Art,Netsuke,,Japan,,,,,Artist,,Sanshō,1871–1936,,Sanshō,Japanese,1871,1936,early 20th century,1900,1933,Wood,H. 4 1/4 in. (10.8 cm); W. 1 1/4 in. (3.2 cm); D. 1 in. (2.5 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.2353,false,true,59404,Asian Art,Netsuke,,Japan,,,,,Artist,,Sanshō,1871–1936,,Sanshō,Japanese,1871,1936,late 19th century,1867,1899,Wood,H. 3 1/4 in. (8.3 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.687,false,true,60230,Asian Art,Kyogen mask,,Japan,,,,,Artist,,Tenkaichi Taiko,"Japanese, died 1616",,Tenkaichi Taiko,Japanese,1516,1616,early 17th century,1600,1633,Lacquered wood,H. 7 5/8 in. (19.4 cm); W. 5 3/4 in. (14.6 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Masks,,http://www.metmuseum.org/art/collection/search/60230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.25.107a–e,false,true,58280,Asian Art,Writing box,,Japan,,,,,Artist,In the Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,late 19th century,1871,1899,"Colored lacquer, gold maki-e, and inlaid pewter",H. 2 3/8 in. (6 cm); W. 10 1/2 in. (26.7 cm); L. 12 in. (30.5 cm),"Gift of Mrs. George A. Crocker (Elizabeth Masten), 1937",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB182,false,true,57912,Asian Art,Illustrated book,,Japan,,,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,1793,1793,1793,Ink on paper,13 13/16 x 9 1/16 in. (35.1 x 23 cm),"The Harry G. G. Packard Collection of Asian Art, Gift of Harry G. G. Packard and Purchase, Fletcher, Rogers, Harris Brisbane Dick and Louis V. Bell Funds, Joseph Pulitzer Bequest and The Annenberg Fund, Inc. Gift, 1975",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.399.24,false,true,59676,Asian Art,Netsuke,,Japan,,,,,Artist,,Minkō,"Japanese, ca. 1735–1816",,Minkō,Japanese,1735,1816,late 18th–early 19th century,1767,1833,"Wood, horn",H. 1 1/16 in. (2.7 cm); W. 1 7/16 in. (3.7 cm),"Gift of Alvin H. Schechter, 1985",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.83,false,true,45585,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Yamada Jōkasai (1681–1704),,,Yamada Jōkasai,Japanese,1681,1704,second half of the 19th century,1850,1899,Gold and colored lacquer with inlaid mother-of-pearl on ro-iro black lacquer; ojime: metal bead inlaid with silver; netsuke: carved ebony inlaid with ivory design of Daruma,3 7/8 x 1 15/16 x 1 1/4 in. (9.8 x 4.9 x 3.2 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB97,false,true,57784,Asian Art,Illustrated book,職人盡歌合|Poetry Contest by Various Artisans (Shokunin zukushi uta-awase),Japan,Edo period (1615–1868),,,,Artist,After,Tosa Mitsunobu,1434–1525,,Tosa Mitsunobu,Japanese,1434,1525,ca. 1744,1734,1754,Woodblock printed book; ink on paper,Overall: 10 1/8 × 7 3/16 × 5/8 in. (25.7 × 18.3 × 1.6 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.784,false,true,78686,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Tosa Mitsunari,1648–1710,,Tosa Mitsunari,Japanese,1648,1710,late 17th century,1675,1699,Accordion album; ink and color on silk,9 9/16 × 8 7/8 in. (24.3 × 22.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.817a–g,false,true,78707,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Ōoka Shunboku,1680–1763,,Ōoka Shunboku,Japanese,1680,1763,1750,1750,1750,Set of six woodblock printed books bound as one with additional volume; ink on paper,10 3/16 × 7 3/16 in. (25.8 × 18.2 cm) 10 3/16 × 7 1/16 in. (25.8 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.818a–c,false,true,78708,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Ōoka Shunboku,1680–1763,,Ōoka Shunboku,Japanese,1680,1763,ca. 1812,1807,1817,Set of three woodblock printed books; ink and color on paper,each: 10 13/16 × 7 in. (27.5 × 17.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.89,false,true,57224,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Rai San'yō,1780–1832,,Rai San'yō,Japanese,1780,1832,dated 1824,1824,1824,Handscroll; ink on paper,11 7/8 x 116 3/4 in. (30.2 x 296.5 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/57224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB113,false,true,57806,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Ōnishi Chinnen,1792–1851,,Ōnishi Chinnen,Japanese,1792,1851,1832,1832,1832,Ink on paper,10 1/2 × 7 3/16 × 3/8 in. (26.7 × 18.3 × 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.669,false,true,78590,Asian Art,Illustrated book,楚南画譜|Sōnan (Chinnen) Picture Album (Sōnan gafu),Japan,Edo period (1615–1868),,,,Artist,,Ōnishi Chinnen,1792–1851,,Ōnishi Chinnen,Japanese,1792,1851,1834,1834,1834,Woodblock printed book; ink and color on paper,10 13/16 × 7 5/16 in. (27.5 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.670,false,true,78591,Asian Art,Illustrated book,あづまの手ぶり|Customs of the Eastern Capital (Edo) (Azuma no teburi),Japan,Edo period (1615–1868),,,,Artist,,Ōnishi Chinnen,1792–1851,,Ōnishi Chinnen,Japanese,1792,1851,1829,1829,1829,Woodblock printed book; ink and color with hand-coloring (?) on paper,10 15/16 × 7 3/8 in. (27.8 × 18.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB64,false,true,57664,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1695, fourth month",1695,1695,Woodblock printed book; ink on paper,8 1/4 × 6 1/8 × 3/8 in. (21 × 15.6 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB65,false,true,45061,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1694,1684,1704,Woodblock printed book; ink on paper,10 5/8 × 7 1/4 × 1/8 in. (27 × 18.4 × 0.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB66,false,true,57665,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1683, fifth month",1683,1683,Woodblock printed book; ink and color on paper,10 1/4 × 7 × 3/8 in. (26 × 17.8 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB74,false,true,57672,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1685, second month",1685,1685,Woodblock printed book; ink and color on paper,10 3/8 × 7 3/8 × 3/16 in. (26.4 × 18.7 × 0.5 cm),"Rogers Fund, 1923",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB75,false,true,57673,Asian Art,Illustrated book,美人絵づくし|Illustrations of Beautiful Women (Bijin e-zukushi),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1683, fifth month",1683,1683,Woodblock printed book; ink and color on paper,10 5/8 × 7 1/4 × 3/8 in. (27 × 18.4 × 1 cm),"Rogers Fund, 1923",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB84,false,true,57743,Asian Art,Illustrated book,大和絵づくし|Compendium of Yamato-e Painting Themes (Yamato-e zukushi),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1686, ninth month",1686,1686,Woodblock printed book; ink on paper,10 3/4 x 7 3/8 in. (27.3 x 18.7 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB86,false,true,57745,Asian Art,Illustrated book,築山図庭画畫 余慶作り庭の図|A Compendium of Model Gardens (Tsukiyama no zu niwa zukushi; Yokei tsukuri niwa no zu),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1691, fifth month",1691,1691,Woodblock printed book; ink on paper,10 1/2 x 7 1/2 in. (26.7 x 19.1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.887,false,true,78777,Asian Art,Illustrated book,『當世雛形』|Contemporary Kimono Patterns (Tōsei hiinagata),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,1677,1677,1677,Woodblock printed book; ink on paper,10 11/16 × 7 1/2 in. (27.2 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB122a, b",false,true,57814,Asian Art,Illustrated book,ぶんしやう物語|The Tale of Bunshō (Bunshō monogatari),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1685,1675,1695,Set of two woodblock printed books; ink on paper,Each: 8 3/4 × 6 1/8 × 1/8 in. (22.2 × 15.6 × 0.3 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB26a–c,false,true,57559,Asian Art,Illustrated book,姿絵百人一首|Portraits for One Hundred Poems about One Hundred Poets (Sugata-e hyakunin isshu),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1695, fourth month",1695,1695,Set of three woodblock printed books; ink on paper,Each: 8 3/4 × 6 3/8 × 1/4 in. (22.2 × 16.2 × 0.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB67a–c,false,true,57666,Asian Art,Illustrated book,美人絵づくし|Illustrations of Beautiful Women (Bijin e-zukushi),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1683, fifth month",1683,1683,Set of three woodblock printed books; ink and color on paper,Each: 10 3/8 × 7 5/16 × 1/4 in. (26.4 × 18.6 × 0.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB85a–c,false,true,57744,Asian Art,Illustrated book,伊勢物語頭書抄|Tales of Ise with Annotations (Ise Monogatari tōsho shō),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,"1679, third month",1679,1679,Set of 3 woodblock printed books; ink on paper,Each: 10 1/2 × 7 3/8 × 3/8 in. (26.7 × 18.7 × 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.786a–c,false,true,78688,Asian Art,Illustrated books,『和国諸職絵尽 諸織 絵本鏡』|A Picture Book Mirror of Various Occupations (Wakoku shoshoku ezukushi),Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,1685,1685,1685,Set of three woodblock printed books; ink on paper,each: 10 5/8 × 7 5/16 in. (27 × 18.5 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.82.3,false,true,45574,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Koma Kōryū,"Japanese, died 1796",,Koma Kōryū,Japanese,,1796,late 18th–19th century,1771,1899,"Roiro (waxen) lacquer with decoration in togisashi sprinkled and polished lacquer; Ojime: gold lacquer bead; Netsuke: ivory ""Dream of a Clam""",H. 3 3/16 in. (8.1 cm); W. 1 7/8 in. (4.8 cm); D. 15/16 in. ( 2.4 cm),"Gift of Wilton Lloyd-Smith and his wife, Marjorie Fleming Lloyd-Smith, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.824,false,true,78714,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Shimokōbe Shūsui,"Japanese, died 1797",,Shimokōbe Shūsui,Japanese,,1797,1797,1797,1797,Woodblock printed book; ink on paper,6 1/4 × 4 5/16 in. (15.8 × 11 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.697a, b",false,true,78618,Asian Art,Album,『光琳画譜』|Kōrin Picture Album (Kōrin gafu),Japan,Edo period (1615–1868),,,,Artist,,Nakamura Hōchū,"Japanese, died 1819",,Nakamura Hōchū,Japanese,,1819,after 1826 (reprinted posthumously),1827,1868,"Woodblock-printed book in two volumes (orihon, accordion-style); ink and color on paper",each: 10 5/8 × 7 11/16 in. (27 × 19.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.890a, b",false,true,78780,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Ki Chikudō,"Japanese, died 1825",,Ki Chikudō,Japanese,,1825,1815,1815,1815,Set of two woodblock printed books; ink and color on paper,10 13/16 × 6 15/16 in. (27.5 × 17.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.689,false,true,45562,Asian Art,Writing box,伝本阿弥光悦 橅夫蒔絵硯箱|Writing Box (Suzuribako) with Woodcutter,Japan,Edo period (1615–1868),,,,Artist,In the style of,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,18th or early 19th century,1700,1833,Gold maki-e on black lacquer with mother-of-pearl inlay,H. 3 3/4 in. (9.5 cm); W. 9 in. (22.9 cm); L. 9 3/8 in. (23.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.177a, b",false,true,45481,Asian Art,Basket,,Japan,Edo period (1615–1868),,,,Artist,Style of,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,late 17th century,1667,1699,"Rattan body, gold, silver hiramaki-e",H. 4 1/8 in. (10.5 cm); W. 8 7/8 in. (22.5 cm); L. 9 7/8 in. (25.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.753,false,true,58664,Asian Art,Inrō,馬蒔絵印籠 自適斎筆|Inrō with Two Standing Horses; Seal and Inscription (reverse),Japan,Edo period (1615–1868),,,,Artist,After,Kano Naonobu,"Japanese, 1607–1650",,Kano Naonobu,Japanese,1607,1650,mid- 18th century,1734,1766,"Four cases; lacquered wood with gold hiramaki-e, gold foil application, and mother-of-pearl inlay on black ground Netsuke: horse; ivory Ojime: iron bead with cricket and flower in gold overlay",3 1/8 x 1 7/16 x 1 in. (7.9 x 3.7 x 2.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.57, .58",false,true,45264,Asian Art,Folding screens,舞楽図屏風 ・唐獅子図屏風|Bugaku Dances (front); Chinese Lions (reverse),Japan,Edo period (1615–1868),,,,Artist,,Hanabusa Itchō,"Japanese, 1652–1724",,Hanabusa Itchō,Japanese,1652,1724,early 18th century,1700,1733,"Pair of six-panel screens; ink, color, and gold leaf on paper",Image (each screen): 72 1/8 in. x 14 ft. 9 5/8 in. (183.2 x 451.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB100a–c,false,true,36460,Asian Art,Illustrated book,一蝶画譜|Itchō Picture Album (Itchō gafu),Japan,Edo period (1615–1868),,,,Artist,,Hanabusa Itchō,"Japanese, 1652–1724",,Hanabusa Itchō,Japanese,1652,1724,"1770, first month",1770,1770,Set of three woodblock printed books; ink on paper,Overall (vols. 1–3 each): 10 1/16 × 6 7/8 in. (25.6 × 17.5 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/36460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.812,false,true,58718,Asian Art,Inrō,寒山拾得蒔絵印籠|Inrō with Rinpa Style Kanzan and Jittoku,Japan,Edo period (1615–1868),,,,Artist,School of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,first half of the 19th century,1800,1849,Three cases; lacquered wood with mother-of-pearl and pewter inlay on gold lacquer ground; Pouch: printed cotton with sarasa pattern; Ojime: metal bead,H. 2 3/16 in. (5.5 cm); W. 1 15/16 in. (4.9 cm); D. 3/4 in. (1.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.117,true,true,44918,Asian Art,Folding screen,波濤図屏風|Rough Waves,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,ca. 1704–9,1694,1719,"Two-panel folding screen; ink, color, and gold leaf on paper",Image: 57 11/16 x 65 1/8 in. (146.5 x 165.4 cm) Overall: 59 1/4 x 66 1/2 in. (150.5 x 168.9 cm),"Fletcher Fund, 1926",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"53.7.1, .2",false,true,39664,Asian Art,Folding screen,八橋図屏風 |Irises at Yatsuhashi (Eight Bridges),Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,after 1709,1710,1716,Pair of six-panel folding screens; ink and color on gold leaf on paper,Image (each screen): 64 7/16 in. x 11ft. 6 3/4 in. (163.7 x 352.4 cm) Overall (each screen): 70 1/2 in. x 12 ft. 2 1/4 in. (179.1 x 371.5 cm),"Purchase, Louisa Eldridge McBurney Gift, 1953",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/39664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.62, .63",false,true,44896,Asian Art,Folding screen,"尾形光琳筆 松竹に鶴図屏風|Cranes, Pines, and Bamboo",Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,late 17th century,1671,1699,Pair of folding screens; ink and light color on paper,Right screen (4-panel): 65 3/4 x 101 1/4 in. (167 x 257.2 cm) Left screen (6-panel): 65 3/4 x 151 1/4 in. (167 x 384.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB124a, b",false,true,57816,Asian Art,Illustrated books,池田孤邨画 『光琳新選百図』|One Hundred Newly Selected Designs by Kōrin (Kōrin shinsen hyakuzu),Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,1864,1864,1864,Two volumes of Woodblock printed books; ink on paper,Overall (each): 10 1/16 x 7 3/8 x 3/8 in. (25.6 x 18.7 x 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.149,false,true,45729,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Kano Chikanobu,"Japanese, 1660–1728",,Kano Chikanobu,Japanese,1660,1728,17th–18th century,1600,1799,Six-panel folding screen; ink and color on silk,Image: 61 1/8 x 139 3/4 in. (155.3 x 355 cm),"Gift of Richard W. Courts, in memory of Mr. and Mrs. Thomas J. Watson, 1966",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.498,false,true,45217,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Kano Chikanobu,"Japanese, 1660–1728",,Kano Chikanobu,Japanese,1660,1728,17th–18th century,1600,1799,"One of a pair of six-panel folding screens; ink, color, and gilt on paper; Reverse side: ink, color, and gold on paper",69 1/4 x 153 in. (175.9 x 388.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.702,false,true,58238,Asian Art,Writing box,,Japan,Edo period (1615–1868),,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,17th–18th century,1600,1799,Lacquer with design in pottery and pewter,H. 1 5/8 in. (4.1 cm); W. 8 3/4 in. (22.2 cm); L. 9 1/4 in. (23.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB34,false,true,45356,Asian Art,Illustrated book,Onna Ichidai Fūzoku Ehon Masukagami|絵本十寸鑑|True Reflections on the Life and Manners of a Woman,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,January 1748,1748,1748,Monochrome woodblock printed book; ink on paper,Overall: 8 3/4 × 6 1/4 in. (22.2 × 15.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB35,false,true,53815,Asian Art,Illustrated book,Ehon Asakayama|絵本浅香山|Picture Book: Mount Asaka (Ehon Asakayama),Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,January 1739,1739,1739,Bound book of monochrome woodblock prints; ink on paper,Overall: 10 3/8 × 7 in. (26.4 × 17.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/53815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB90,false,true,57756,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1740,1740,1740,Woodblock print; ink on paper,Overall: 9 x 6 1/4 in. (22.9 x 15.9 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB99,false,true,45062,Asian Art,Illustrated book,"絵本末摘花|Picture Book: Flowers Yet to be Picked, Vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1756,1615,1868,Woodblock printed book; ink and color on paper,Overall: 9 × 6 in. (22.9 × 15.2 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB151,false,true,57842,Asian Art,Illustrated book,Ehon Ogurayama|絵本小倉山|Picture Book: Ogura Hill (Ehon Ogurayama),Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1749,1749,1749,Thirteen double-page monochrome illustrations; ink on paper,Overall: 8 7/8 × 6 1/4 in. (22.5 × 15.9 cm),"Bequest of W. Gedney Beatty, 1941",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB152,false,true,57843,Asian Art,Illustrated book,Ehon Kai kasen|絵本貝歌仙|Illustrated Poems,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1747,1747,1747,Nineteen pages of illustrations and poems; ink on paper,Overall: 8 5/8 × 6 3/8 in. (21.9 × 16.2 cm),"Bequest of W. Gedney Beatty, 1941",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB153,false,true,57844,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1741,1741,1741,Ink on paper,Overall: 10 3/4 x 7 1/2in. (27.3 x 19.1cm),"Bequest of W. Gedney Beatty, 1941",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB163,false,true,57854,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1731,1731,1731,"One volume, black and white, twenty-five double-page sheets; ink on paper",Overall: 10 1/4 x 6 1/2in. (26 x 16.5cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB167,false,true,57858,Asian Art,Illustrated book,Ehon Himetsubaki|繪本女貞木|Picture Book: Camellia (Ehon Himetsubaki),Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1745(?),1745,1745,Bound book of monochrome woodblock prints; ink on paper,Overall: 10 1/2 x 7in. (26.7 x 17.8cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.833,false,true,78723,Asian Art,Illustrated book,『繪本常盤草』|Picture Book of the Evergreens (Ehon tokiwagusa),Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1731,1731,1731,Woodblock printed book; ink and hand-coloring on paper,10 7/8 × 7 9/16 in. (27.7 × 19.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.834,false,true,78724,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1739,1739,1739,Woodblock printed book; ink on paper,10 1/2 × 7 3/16 in. (26.7 × 18.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.835,false,true,78725,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1740,1740,1740,Set of three woodblock printed books bound as one volume; ink on paper,10 5/8 × 7 1/2 in. (27 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.836,false,true,78726,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1746,1746,1746,Woodblock printed book; ink on paper,9 1/8 × 6 7/16 in. (23.2 × 16.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.838,false,true,78728,Asian Art,Illustrated books,『繪本小倉山』|Picture Book of Ogura Hill (Ehon ogurayama),Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1749,1749,1749,Set of three woodblock-printed books bound as one volume; ink on paper,each: 9 1/16 × 6 1/2 in. (23 × 16.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.839,false,true,78729,Asian Art,Illustrated book,『繪本小松原』|Picture Book of Komatsubara (Ehon komatsubara),Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1761,1761,1761,Woodblock-printed book; ink on paper,10 5/8 × 7 1/2 in. (27 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.832a–c,false,true,78722,Asian Art,Illustrated book,絵本常盤草 上・中・下|Picture Book of the Evergreens (Ehon tokiwagusa),Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1731,1731,1731,Woodblock-printed book; ink and hand-coloring on paper,10 3/4 × 7 1/2 in. (27.3 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.837a–c,false,true,78727,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1748,1748,1748,Set of three woodblock printed books; ink on paper,each: 8 7/8 × 6 5/16 in. (22.5 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.88,false,true,44864,Asian Art,Overrobe,白繻子地墨竹図打掛 祇園南海筆|Overrobe (Uchikake) with Bamboo,Japan,Edo period (1615–1868),,,,Artist,,Gion Nankai,"Japanese, 1677–1751",,Gion Nankai,Japanese,1677,1751,first half of the 18th century,1700,1749,Ink and gold powder on silk satin,Overall: 64 3/4 x 48 7/8 in. (164.5 x 124.2 cm) Sleeve length: 37 3/4 in. (95.9 cm); sleeve width: 12 3/4 in. (32.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Costumes,,http://www.metmuseum.org/art/collection/search/44864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB121a, b",false,true,57813,Asian Art,Illustrated book,唐土訓蒙図彙|Illustrated Encyclopedia of China (Morokoshi kinmō zui),Japan,Edo period (1615–1868),,,,Artist,,Tachibana Morikuni,"Japanese, 1679–1748",,Tachibana Morikuni,Japanese,1679,1748,1719; preface dated 1718,1718,1719,Set of two woodblock printed books; ink on paper,Overall (vol. 1): 11 1/16 × 10 3/16 × 9/16 in. (28.1 × 25.8 × 1.5 cm) Overall (vol. 2): 11 1/16 × 10 1/8 × 13/16 in. (28.1 × 25.7 × 2 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.785a–c,false,true,78687,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Tachibana Morikuni,"Japanese, 1679–1748",,Tachibana Morikuni,Japanese,1679,1748,1749,1749,1749,Set of three woodblock printed books; ink on paper,each: 10 1/16 × 7 1/16 in. (25.5 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.768.2,false,true,77197,Asian Art,Hanging scroll,大字「悳」|Virtue,Japan,Edo period (1615–1868),,,,Artist,,Hakuin Ekaku,"Japanese, 1685–1768",,Hakuin Ekaku,Japanese,1685,1768,mid-18th century,1734,1766,Hanging scroll; ink on paper,Image: 41 3/4 × 20 1/2 in. (106 × 52 cm) Overall with mounting: 73 1/8 × 25 3/16 in. (185.8 × 64 cm) Overall with knobs: 73 1/8 × 27 3/8 in. (185.8 × 69.5 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/77197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.883a–d,false,true,78773,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Settei,"Japanese, 1710–1786",,Tsukioka Settei,Japanese,1710,1786,1764,1764,1764,Set of four woodblock printed books; ink and color (vol. 5 only) on paper,10 5/8 × 7 1/2 in. (27 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.803,false,true,78693,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Toriyama Sekien,"Japanese, 1712–1788",,Toriyama Sekien,Japanese,1712,1788,1781,1781,1781,Set of three woodblock printed books bound as one volume; ink on paper,8 7/8 × 6 5/16 in. (22.5 × 16 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.814a–c,false,true,78704,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Sō Shiseki,"Japanese, 1715–1786",,Sō Shiseki,Japanese,1715,1786,1764,1764,1764,"Set of three woodblock printed books; ink, color, and hand-coloring (vol. 2) on paper",each: 10 1/2 × 6 3/4 in. (26.7 × 17.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.665,false,true,78586,Asian Art,Illustrated book,俳諧三十六歌僊|The Thirty-six Immortals of Haikai Verse (Haikai sanjūrokkasen),Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,Japanese,1716,1783,1799,1799,1799,Woodblock printed book; ink with hand-coloring on paper,10 5/8 × 7 1/2 in. (27 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.666,false,true,78587,Asian Art,Illustrated book,三十六歌仙|The Thirty-six Immortals of Poetry (Sanjūrokkasen),Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,Japanese,1716,1783,1799,1799,1799,Woodblock printed book; ink on paper,10 15/16 × 7 1/2 in. (27.8 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.751,false,true,78653,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kanyōsai,"Japanese, 1719–1774",,Kanyōsai,Japanese,1719,1774,1762,1762,1762,Set of three woodblock printed books; ink on paper,each: 10 13/16 × 6 15/16 in. (27.5 × 17.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.752a–e,false,true,78654,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kanyōsai,"Japanese, 1719–1774",,Kanyōsai,Japanese,1719,1774,1762,1762,1762,Set of five woodblock printed books; ink on paper,each: 10 3/8 × 7 1/16 in. (26.3 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.884,false,true,78774,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1767,1761,1771,Woodblock printed book; ink and color on paper,6 1/4 × 8 5/16 in. (15.9 × 21.1 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB7a–c,false,true,57542,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1788,1788,1788,Three volumes; ink on paper,Each: 9 × 6 1/4 × 1/16 in. (22.9 × 15.9 × 0.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.685a–c,false,true,78606,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1763,1763,1763,Set of three woodblock printed books; ink on paper,each: 8 7/8 × 6 5/16 in. (22.5 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.687a–c,false,true,78608,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1767,1761,1771,Set of three woodblock printed books; ink on paper,each: 12 3/16 × 8 1/4 in. (31 × 21 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.821,false,true,78711,Asian Art,Illustrated book,『錦百人一首東織』|Eastern Brocade of One Hundred Poems by One Hundred Poets (Nishiki hyakunin isshu azuma-ori),Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1775,1775,1775,Woodblock printed book; ink and color on paper,11 5/8 × 7 11/16 in. (29.5 × 19.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.823,false,true,78713,Asian Art,illustrated book,"『役者夏の富士』|Actors [Out of Costume] Like Mount Fuji [Without Snow] in Summer (Yakusha natsu no Fuji), by Ichiba Tsūshō",Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1830,1825,1835,Woodblock printed book; ink on paper,8 9/16 × 6 1/8 in. (21.8 × 15.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.566,false,true,40022,Asian Art,Screen,近江八景|Eight Views of Ōmi (Ōmi hakkei),Japan,Edo period (1615–1868),,,,Artist,After,Soga Shōhaku,"Japanese, 1730–1781",,Soga Shōhaku,Japanese,1730,1781,Probably late 18th or early 19th century,1700,1833,Six-panel folding screen; ink and paper,Image: 46 in. x 13 ft. (116.8 x 396.2 cm),"Gift of William and Marjorie Normand, 1994",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/40022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.156.1,false,true,45422,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,Japanese,1733,1795,dated 1773,1773,1773,Two-panel folding screen; ink and color on paper,Image: 22 x 53 3/8 in. (55.9 x 135.6 cm) Overall: 29 1/4 x 74 in. (74.3 x 188 cm),"Seymour Fund, 1957",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.792,false,true,78808,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,After,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,Japanese,1733,1795,1850,1850,1850,"Woodblock printed book (orihon, accordion-style); ink and color on paper",11 3/16 × 7 9/16 in. (28.4 × 19.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.791a, b",false,true,78807,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,After,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,Japanese,1733,1795,1837,1837,1837,Set of two woodblock printed books; ink and color on paper,each: 10 1/2 × 7 5/16 in. (26.7 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.654,false,true,78575,Asian Art,Illustrated book,九老画譜|Kyūrō (Baitei) Picture Album (Kyūrō gafu),Japan,Edo period (1615–1868),,,,Artist,,Ki Baitei,"Japanese, 1734–1810",,Ki Baitei,Japanese,1734,1810,preface and postscript dated 1797,1797,1797,Woodblock printed book; ink on paper,10 3/16 × 7 1/16 in. (25.8 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.807,false,true,78697,Asian Art,Illustrated book,閨暦大雑書玉門大成|Erotica; Compendium Guide to the Brothels of Osaka (Keiryaku ōzassho gyokumon taisei),Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1770,1765,1775,Woodblock printed book; ink and color on paper,10 9/16 × 7 5/16 in. (26.8 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.808,false,true,78698,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1787,1787,1787,Woodblock printed book; ink and color on paper,10 7/16 × 7 1/16 in. (26.5 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.809a–c,false,true,78699,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1802,1802,1802,Set of three woodblock printed books; ink and color on paper,each: 8 7/8 × 6 1/8 in. (22.5 × 15.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.810a–c,false,true,78700,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",", et al",Kitao Shigemasa,Japanese,1739,1820,1805,1805,1805,Set of three woodblock printed books; ink on paper,each: 8 7/8 × 6 1/4 in. (22.5 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.820a–d,false,true,78710,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1847,1847,1847,Set of four woodblock printed books; ink and color on paper,9 1/16 × 6 5/16 in. (23 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.340a–f,false,true,73593,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Shiba Kōkan,"Japanese, 1747–1818",,Shiba Kōkan,Japanese,1747,1818,1803,1803,1803,Five volumes of woodblock printed books; ink on paper,Overall (each volume): 10 1/8 x 7 1/8 x 1/4 in. (25.7 x 18.1 x 0.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/73593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.679,false,true,78600,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Fuyō,"Japanese, 1749–1816",,Suzuki Fuyō,Japanese,1749,1816,1809,1809,1809,Set of three woodblock printed books bound as a single volume; ink on paper,10 13/16 × 7 5/16 in. (27.5 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB6,false,true,57541,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1780,1800,Ink and color on paper,10 × 7 1/2 × 3/8 in. (25.4 × 19.1 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB22a, b",false,true,45271,Asian Art,Illustrated book,Ehon monomi ga oka|Watchtower Hill,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1785,1600,1868,Two volumes; polychrome woodblock printed book; ink on paper,Each: 8 3/4 × 6 3/8 × 1/4 in. (22.2 × 16.2 × 0.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.656,false,true,78577,Asian Art,Illustrated book,胸中山|Mountains of the Heart (Kyōchūzan),Japan,Edo period (1615–1868),,,,Artist,,Kameda Bōsai,"Japanese, 1752–1826",,Kameda Bōsai,Japanese,1752,1826,1816,1816,1816,Woodblock printed book; ink and color on paper,10 15/16 × 7 3/8 in. (27.8 × 18.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.657,false,true,78578,Asian Art,Illustrated book,胸中山|Mountains of the Heart (Kyōchūzan),Japan,Edo period (1615–1868),,,,Artist,,Kameda Bōsai,"Japanese, 1752–1826",,Kameda Bōsai,Japanese,1752,1826,1816,1816,1816,Woodblock printed book; ink and color on paper,10 1/4 × 6 13/16 in. (26 × 17.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78578,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.75, .76",false,true,39658,Asian Art,Screen,山水唐人物図屏風|Landscapes with the Chinese Literati Su Shi and Tao Qian,Japan,Edo period (1615–1868),,,,Artist,,Nagasawa Rosetsu,"Japanese, 1754–1799",,Nagasawa Rosetsu,Japanese,1754,1799,1795–99,1795,1799,Pair of six-panel folding screens; ink on gold leaf on paper,Image (each screen): 67 3/8 in. x 12 ft. 2 3/4 in. (171.1 x 372.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/39658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.828,false,true,78718,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Yamaguchi Soken,"Japanese, 1759–1818",,Yamaguchi Soken,Japanese,1759,1818,1807,1807,1807,Woodblock printed book; ink on paper,Other: 10 3/8 × 7 3/8 in. (26.3 × 18.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.829a, b",false,true,78719,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Yamaguchi Soken,"Japanese, 1759–1818",,Yamaguchi Soken,Japanese,1759,1818,1818,1818,1818,Set of two woodblock printed books; ink and hand-coloring (vol. 2) on paper,each: 10 1/16 × 7 3/16 in. (25.5 × 18.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB91,false,true,45278,Asian Art,Illustrated book,「吉原傾城」新美人合自筆鏡|Yoshiwara Courtesans: A New Mirror Comparing the Calligraphy of Beauties (Yoshiwara keisei: Shin bijin awase jihitsu kagami),Japan,Edo period (1615–1868),,,,Artist,,Kitao Masanobu (Santō Kyōden),"Japanese, 1761–1816",,Kitao Masanobu (Santō Kyōden),Japanese,1761,1816,"1784, first month",1784,1784,Polychrome woodblock printed book; ink and color on paper,Overall: 14 7/8 × 10 3/16 in. (37.8 × 25.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB101,false,true,57786,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Masanobu (Santō Kyōden),"Japanese, 1761–1816",,Kitao Masanobu (Santō Kyōden),Japanese,1761,1816,1844,1844,1844,Two volumes; ink on paper,Each: 11 1/8 × 7 3/4 × 3/8 in. (28.3 × 19.7 × 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.768,false,true,78670,Asian Art,Illustrated book,『吉原傾城新美人自筆鏡』|New Mirror Comparing the Handwriting of the Courtesans of the Yoshiwara (Yoshiwara keisei shin bijin jihitsu kagami),Japan,Edo period (1615–1868),,,,Artist,,Kitao Masanobu (Santō Kyōden),"Japanese, 1761–1816",,Kitao Masanobu (Santō Kyōden),Japanese,1761,1816,1784,1784,1784,Woodblock printed book; ink and color on paper,14 15/16 × 10 1/4 in. (38 × 26 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.156.3,false,true,45392,Asian Art,Folding screen,酒井抱一筆 柿図屏風|The Persimmon Tree,Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,1816,1816,1816,Two-panel folding screen; ink and color on paper,Image: 56 9/16 x 56 5/8 in. (143.7 x 143.8 cm) Overall: 65 1/4 x 64 in. (165.7 x 162.6 cm),"Rogers Fund, 1957",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB57,false,true,57657,Asian Art,Illustrated book,酒井抱一画 『乾山遺墨』|Ink Traces of Kenzan (Kenzan iboku),Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,1823,1823,1823,Woodblock printed book; ink and color on paper,Image: 9 3/4 x 6 15/16 x 1/4 in. (24.8 x 17.7 x 0.7 cm) Overall (open): 9 3/4 x 13 in. (24.8 x 33 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB58,false,true,57658,Asian Art,Illustrated book,酒井抱一 画 『鶯邨画譜』|Ōson (Hōitsu) Picture Album (Ōson gafu),Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,1817,1817,1817,Woodblock printed book; ink and color on paper,Image: 10 7/8 x 7 3/8 x 1/2 in. (27.7 x 18.8 x 1.2 cm) Overall: 14 in. (35.5 cm) (open),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB95,false,true,57776,Asian Art,Illustrated book,乾山遺墨|Ink Traces of Kenzan (Kenzan iboku),Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,1823,1823,1823,Woodblock printed book; ink and color on paper,Image: 9 5/8 x 6 7/8 x 1/4 in. (24.4 x 17.5 x 0.7 cm) Overall (Open): 9 5/8 x 13 in. (24.4 x 33 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB116,false,true,57809,Asian Art,Illustrated book,池田孤邨画 『抱一上人真蹟鏡』|Ōson (Hōitsu) Picture Album (Ōson gafu),Japan,Edo period (1615–1868),,,,Artist,,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,1817,1817,1817,Woodblock printed book; ink and color on paper,Image: 10 15/16 x 7 3/8 x 1/16 in. (27.8 x 18.8 x 0.1 cm) Overall (Open): 10 15/16 x 13 7/8 in. (27.8 x 35.3 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB37,false,true,45272,Asian Art,Illustrated book,Edo hakkei|Eight Views of Edo,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,ca. 1771,1761,1781,Bound book of polychrome woodblock prints; ink and color on paper,Overall: 9 3/4 x 14 7/8 in. (24.8 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.664,false,true,78585,Asian Art,Illustrated book,『寫山樓畫本』|Shazanrō (Bunchō) Picture Book (Shazanrō ehon),Japan,Edo period (1615–1868),,,,Artist,,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,Japanese,1763,1840,1817,1817,1817,Woodblock-printed book; ink and color on paper,10 13/16 × 7 3/8 in. (27.5 × 18.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.776a–d,false,true,78678,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,ca. 1800,1790,1810,Four woodblocks for printed books,framed each: 13 3/4 × 9 1/16 in. (35 × 23 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Woodblocks,,http://www.metmuseum.org/art/collection/search/78678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB21,false,true,57554,Asian Art,Illustrated book,略画式 (人物)|Abbreviated Drawing Styles (Ryakuga shiki)(Figures),Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,"1795, twelfth month",1795,1795,Woodblock printed book; ink and color on paper,Overall: 9 3/4 × 6 15/16 in. (24.7 × 17.6 cm),"Rogers Fund, 1923",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB55,false,true,57655,Asian Art,Illustrated book,鳥獣略画式|Abbreviated Drawing Styles for Birds and Animals (Chōjū ryakuga shiki),Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1797,1797,1797,Woodblock printed book; ink and color on paper,Overall: 10 1/8 × 7 in. (25.7 × 17.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB56,false,true,57656,Asian Art,Illustrated book,人物略画式|Abbreviated Drawing Styles for Figures (Jinbutsu ryakuga shiki),Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1795,1795,1795,Woodblock printed book; ink and color on paper,Overall: 11 × 7 3/8 in. (28 × 18.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.770,false,true,78672,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1795,1795,1795,Woodblock printed book; ink and color on paper,10 9/16 × 7 5/16 in. (26.8 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.771,false,true,78673,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1797,1797,1797,Woodblock printed book; ink and color on paper,10 7/16 × 7 3/16 in. (26.5 × 18.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.772,false,true,78674,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1799,1799,1799,Set of three woodblock printed books; ink and color on paper,10 1/2 × 7 3/16 in. (26.7 × 18.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.773,false,true,78675,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,dated 1812,1812,1812,Woodblock printed book; ink and color on paper,9 3/16 × 6 9/16 in. (23.3 × 16.6 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.774,false,true,78676,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1800,1800,1800,Woodblock printed book; ink and color on paper,10 3/8 × 7 5/16 in. (26.4 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.775,false,true,78677,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1813,1813,1813,Woodblock printed book; ink and color on paper,10 5/8 × 7 1/16 in. (27 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.777,false,true,78679,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1802,1802,1802,"Woodblock printed book; ink, color and mica on paper",10 7/16 × 7 1/16 in. (26.5 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.779,false,true,78681,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1823,1823,1823,Woodblock printed book; ink and color on paper,10 11/16 × 7 3/16 in. (27.2 × 18.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.880,false,true,78770,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,ca. 1815–42,1810,1847,Woodblock printed book; ink and color on paper,9 × 6 5/16 in. (22.9 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.812a–c,false,true,78702,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Shakuyakutei Nagane,"Japanese, 1767–1845",,Shakuyakutei Nagane,Japanese,1767,1845,1834,1834,1834,Set of three woodblock printed books; ink and color on paper,each: 9 × 6 3/8 in. (22.8 × 16.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB38,false,true,45228,Asian Art,Illustrated book,"『俳優三階興』|Amusements of Kabuki Actors of the “Third Floor” [Dressing Room] (Yakusha sangaikyō), by Shikitei Sanba",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1801,1801,1801,Set of polychrome woodblock-printed books; ink and color on paper,8 1/2 × 6 in. (21.6 × 15.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.848,false,true,78738,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1798,1798,1798,Woodblock printed book; ink and color on paper,5 × 6 7/8 in. (12.7 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.877,false,true,78767,Asian Art,Illustrated book,"『俳優三階興』|Amusements of Kabuki Actors of the “Third Floor” [Dressing Room] (Yakusha sangaikyō), by Shikitei Sanba",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1801,1801,1801,First volume of a two-volume set of woodblock-printed books; ink and color on paper,8 7/16 × 6 1/8 in. (21.5 × 15.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB39a, b",false,true,57566,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1802,1802,1802,Two volumes; polychrome woodblock printed book; ink and color on paper,Each: 8 1/2 × 6 1/16 × 1/2 in. (21.6 × 15.4 × 1.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.849a, b",false,true,78739,Asian Art,Illustrated books,『絵本時世粧』|Picture Book of Modern Figures of Fashion (Ehon imayō sugata),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1802,1802,1802,Set of two woodblock-printed books with hand-written names in volume two; ink and color on paper,each: 8 9/16 × 6 1/8 in. (21.8 × 15.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.850a, b",false,true,78740,Asian Art,illustrated books,『役者相貌鏡』|Mirror Images of Kabuki Actors (Yakusha awase kagami),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1804,1804,1804,Set of two woodblock printed books; ink and color on paper,each: 10 7/16 × 7 1/16 in. (26.5 × 18 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.709,false,true,78630,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,1815,1815,1815,Woodblock printed book; ink on paper,8 11/16 × 6 1/8 in. (22 × 15.6 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.682,false,true,78603,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Chō Gesshō,"Japanese, 1772–1832",,Chō Gesshō,Japanese,1772,1832,1817,1817,1817,Woodblock printed book; ink and color on paper,10 5/16 × 7 5/16 in. (26.2 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.788a–d,false,true,78690,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Nishimura Nantei,"Japanese, 1775–1834",,Nishimura Nantei,Japanese,1775,1834,1823,1823,1823,"Set of three woodblock printed books; ink on paper and ink and color on paper (vol. ""kan"")","vol. ""plum"" each: 10 1/16 × 7 5/16 in. (25.5 × 18.5 cm) vol. ""kan"": 10 5/16 × 7 3/16 in. (26.2 × 18.2 cm)","Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.750a, b",false,true,78652,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Okada Kanrin,"Japanese, 1775–1849",,Okada Kanrin,Japanese,1775,1849,1845,1845,1845,Set of two woodblock printed books; ink and color on paper,each: 10 7/8 × 7 3/8 in. (27.7 × 18.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.668,false,true,78589,Asian Art,Illustrated books,"竹洞四君子|The Four Worthies (Plum, Bamboo, Orchid, Chrysanthemum) (Shikunshi)",Japan,Edo period (1615–1868),,,,Artist,,Nakabayashi Chikutō,"Japanese, 1776–1853",,Nakabayashi Chikutō,Japanese,1776,1853,1853,1853,1853,Set of two woodblock printed books bound as one; ink on paper,10 9/16 × 6 7/8 in. (26.8 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.755,false,true,78657,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kawamura Kihō,"Japanese, 1778–1852",,Kawamura Kihō,Japanese,1778,1852,after 1825,1825,1850,Woodblock printed book; ink and color on paper,10 3/16 × 7 1/8 in. (25.8 × 18.1 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.756,false,true,78658,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kawamura Kihō,"Japanese, 1778–1852",,Kawamura Kihō,Japanese,1778,1852,1827,1827,1827,Woodblock printed book; ink and color on paper,10 3/8 × 7 1/16 in. (26.3 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.660,false,true,78581,Asian Art,Illustrated book,金波園画譜|Kinpaen (Bunpō) Picture Album (Kinpaen gafu),Japan,Edo period (1615–1868),,,,Artist,,Kawamura Bunpō,"Japanese, 1779–1821",,Kawamura Bunpō,Japanese,1779,1821,1820,1820,1820,Woodblock printed book; ink and color on paper,10 7/16 × 4 1/2 in. (26.5 × 11.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.661,false,true,78582,Asian Art,Illustrated book,金波園画譜|Kinpaen (Bunpō) Picture Album (Kinpaen gafu),Japan,Edo period (1615–1868),,,,Artist,,Kawamura Bunpō,"Japanese, 1779–1821",,Kawamura Bunpō,Japanese,1779,1821,1820,1820,1820,Woodblock printed book; ink and color on paper,10 7/16 × 6 11/16 in. (26.5 × 17 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.662,false,true,78583,Asian Art,Illustrated book,文鳳山水画譜|Bunpō Landscape Picture Album (Bunpō sansui gafu),Japan,Edo period (1615–1868),,,,Artist,,Kawamura Bunpō,"Japanese, 1779–1821",,Kawamura Bunpō,Japanese,1779,1821,1824,1824,1824,Woodblock printed book; ink and color on paper,10 3/16 × 6 13/16 in. (25.8 × 17.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.658a–c,false,true,78579,Asian Art,Illustrated books,"文鳳画譜|Bunpō Picture Album (Bunpō gafu), First Series",Japan,Edo period (1615–1868),,,,Artist,,Kawamura Bunpō,"Japanese, 1779–1821",,Kawamura Bunpō,Japanese,1779,1821,1807,1807,1807,Set of three woodblock printed books; ink and color on paper,each: 10 5/16 × 7 1/16 in. (26.2 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.663a–c,false,true,78584,Asian Art,Illustrated book,"漢画指南二編|Guide to Chinese Painting (Kanga shinan nihen), Second Series",Japan,Edo period (1615–1868),,,,Artist,,Kawamura Bunpō,"Japanese, 1779–1821",,Kawamura Bunpō,Japanese,1779,1821,1811,1811,1811,Set of three woodblock printed books; ink and color on paper,each: 10 7/16 × 7 in. (26.5 × 17.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.830,false,true,78720,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Komatsubara Suikei,"Japanese, 1780–1833",,Komatsubara Suikei,Japanese,1780,1833,ca. 1831,1826,1836,"Woodblock printed book; ink, color, and metallic pigments on paper",8 7/8 × 6 5/16 in. (22.5 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB9,false,true,57543,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1833,1833,1833,Ink and color on paper,10 × 6 7/8 × 1/2 in. (25.4 × 17.5 × 1.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.702,false,true,78623,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1824,1824,1824,Woodblock printed book; ink and color on paper,8 15/16 × 6 5/16 in. (22.7 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.703,false,true,78624,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",(et al),Totoya Hokkei,Japanese,1780,1850,1826,1826,1826,Woodblock printed book; ink and color on paper,9 × 6 5/8 in. (22.8 × 16.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.704,false,true,78625,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1829,1829,1829,Woodblock printed book; ink and color on paper,9 1/16 × 6 13/16 in. (23 × 17.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.705,false,true,78626,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1830,1830,1830,Woodblock printed book; ink and color on paper,9 × 6 5/16 in. (22.8 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.706,false,true,78627,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1830s,1830,1839,Woodblock printed book; ink and color on paper,9 × 6 1/2 in. (22.8 × 16.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.708,false,true,78629,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1845,1845,1845,Woodblock printed book; ink and color on paper,8 1/4 × 6 in. (21 × 15.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB8a–c,false,true,58813,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1828–30,1828,1830,Three volumes; ink and color on paper,Each: 8 7/8 × 6 1/4 × 3/8 in. (22.5 × 15.9 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/58813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB1,false,true,57536,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1830,1830,1830,Ink and color on paper,9 × 12 1/4 × 1/2 in. (22.9 × 31.1 × 1.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.759,false,true,78661,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1837,1837,1837,Woodblock printed book; ink and color on paper,9 15/16 × 6 7/8 in. (25.2 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.760,false,true,78662,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1840s,1840,1849,Woodblock printed book; ink and color on paper,9 13/16 × 6 3/4 in. (25 × 17.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.758a–c,false,true,78660,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1830s,1830,1839,Set of three woodblock printed books; ink and color on paper,each: 10 1/16 × 7 5/16 in. (25.5 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.762a–c,false,true,78664,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1850,1845,1855,Set of three woodblock printed books; ink and color on paper,each: 9 1/8 × 6 5/16 in. (23.2 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.813,false,true,78703,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,1839,1839,1839,Set of three woodblock printed books; ink and color on paper,each: 9 1/16 × 6 5/16 in. (23 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.811a, b",false,true,78701,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,1823,1823,1823,Set of two woodblock printed books; ink and color on paper,each: 8 7/8 × 6 1/4 in. (22.6 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.802,false,true,78818,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsura Seiyō,"Japanese, 1787–1860",,Katsura Seiyō,Japanese,1787,1860,1831,1831,1831,Woodblock printed book; ink and color on paper,Other: 9 × 6 1/2 in. (22.8 × 16.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.676,false,true,78597,Asian Art,Illustrated book,艶本 婦慈のゆき|An Erotic Picture Book of Snow on Fuji (Enpon fuji no yuki),Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1824,1824,1824,Woodblock printed book; ink and color on paper,8 3/4 × 6 1/4 in. (22.2 × 15.9 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.675a–c,false,true,78596,Asian Art,Illustrated books,浮世画譜|Picture Album of the Floating World (Ukiyo efu),Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1820s,1820,1829,Set of three woodblock printed books; ink and color on paper,each: 9 1/16 × 6 1/4 in. (23 × 15.9 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.677a–c,false,true,78598,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1822,1822,1822,Set of three woodblock printed books; ink and color on paper,each: 8 7/8 × 6 5/16 in. (22.5 × 16.1 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.780,false,true,78682,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Ōishi Matora,"Japanese, 1793–1833",,Ōishi Matora,Japanese,1793,1833,1829,1829,1829,Woodblock printed book; ink and color on paper,9 × 6 3/16 in. (22.8 × 15.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.650a, b",false,true,78571,Asian Art,Illustrated books,持子鼠花山姥|Stories of a Fortunate Rat (Komochi nezumi hana no yamauba),Japan,Edo period (1615–1868),,,,Artist,,Akatsuki no Kanenari,"Japanese, 1793–1861",,Akatsuki no Kanenari,Japanese,1793,1861,1827,1827,1827,Set of two woodblock printed books; ink and color on paper,each: 8 3/4 × 6 in. (22.2 × 15.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.671,false,true,78592,Asian Art,Illustrated book,椿山翁畫譜|Chinzan Picture Album (Chinzan-ō gafu),Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,1851,1851,1851,Accordion album; ink and color on paper,11 1/4 × 7 1/16 in. (28.5 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB117,false,true,45227,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",", and others",Shibata Zeshin,Japanese,1807,1891,1867,1615,1868,Polychrome Woodblock printed book,,"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.866,false,true,78756,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",", et al",Shibata Zeshin,Japanese,1807,1891,1867,1867,1867,Woodblock printed book; ink and color on paper,9 3/4 × 7 3/16 in. (24.8 × 18.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.975,false,true,59689,Asian Art,Netsuke,,Japan,Edo period (1615–1868),,,,Artist,,Mitsuhiro Ōhara,"Japanese, 1810–1875",,Mitsuhiro Ōhara,Japanese,1810,1875,mid-19th century,1825,1875,Ivory,H. 1 in. (2.5 cm); W. 1 in. (2.5 cm); D. 1 3/4 in. (4.4 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.845,false,true,78735,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Hine Taizan,"Japanese, 1813–1869",,Hine Taizan,Japanese,1813,1869,ca. 1850,1845,1855,Accordion album; ink on paper,8 11/16 × 6 11/16 in. (22 × 17 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.862a–c,false,true,78752,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshimune,"Japanese, 1817–1880",,Utagawa Yoshimune,Japanese,1817,1880,ca. 1860,1855,1865,Three books (in hanshita-e form); ink and light colors on paper,each: 6 3/4 × 4 3/4 in. (17.2 × 12 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.696,false,true,78617,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,1862,1862,1862,Woodblock printed book; ink and color on paper,6 15/16 × 4 5/16 in. (17.7 × 11 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB143,false,true,57834,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1881,1881,1881,Woodblock printed book; ink and color on paper,8 1/2 x 5 3/4 in. (21.6 x 14.6 cm),"Bequest of W. Gedney Beatty, 1941",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB104a, b",false,true,57789,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",and many others,Kawanabe Kyōsai,Japanese,1831,1889,1814,1814,1814,Ink and color on paper,Overall: 11 x 7 1/2 x 1/4 in. (27.9 x 19.1 x 0.7 cm) Overall (open): 11 x 13 3/4 in. (27.9 x 35 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB41,false,true,45273,Asian Art,Illustrated book,絵本吾妻遊|Ehon Azuma asobi|Picture Book of Amusements of the Eastern Capital (Ehon Azuma asobi),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"1790, first month",1790,1790,Woodblock printed book; ink on paper,Overall: 8 11/16 × 6 1/4 in. (22 × 15.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB43,false,true,57568,Asian Art,Illustrated book,Ehon Momochidori Kyōka-awase|百千鳥狂歌合|Myriad Birds: Picture Book of Playful Verse (Momo chidori kyōka-awase),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1791,1791,1791,Woodblock printed books (vols. 1 and 2); ink and color on paper,Overall: 10 x 14 7/8 x (25.4 x 37.8 cm); L. (open) 29 3/4 in. (75.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB45,false,true,57605,Asian Art,Illustrated book,絵本和歌夷 「龢謌夷」|Picture Book with Playful Poems for the Young God Ebisu,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1792,1792,1792,Polychrome woodblock printed book; ink and color on paper,Overall: 10 1/16 × 7 1/2 in. (25.5 × 19 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB46,false,true,45323,Asian Art,Illustrated book,"狂月坊|The Moon-Mad Monk, or Crazy Gazing at the Moon (Kyōgetsubō)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"1789, eighth month",1789,1789,Polychrome woodblock printed book; ink and color on paper,Overall: 9 15/16 × 7 1/2 in. (25.2 × 19 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB47,false,true,57648,Asian Art,Illustrated book,潮干のつと|Gifts from the Ebb Tide (The Shell Book) (Shiohi no tsuto),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1789,1789,1789,Polychrome woodblock printed book; ink and color on paper,10 1/4 x 7 1/2 in. (26 x 19.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB48,false,true,45324,Asian Art,Illustrated book,銀世界|The Silver World (Gin sekai),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"1790, first month",1790,1790,Polychrome woodblock printed book; ink and color on paper,Overall: 10 1/8 × 7 1/2 in. (25.7 × 19 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB49,false,true,57649,Asian Art,Illustrated book,普賢像|Statue of the Bodhisattva Fugen (Fugenzō),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"1790, third month",1790,1790,Polychrome woodblock printed book; ink and color on paper,Overall: 10 1/16 × 7 5/16 in. (25.6 × 18.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB89,false,true,57755,Asian Art,Illustrated book,銀世界|The Silver World (Gin sekai),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,preface dated 1790,1790,1790,Polychrome woodblock printed book; ink and color on paper,10 1/16 x 7 1/2 in. (25.5 x 19 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB44a,false,true,57569,Asian Art,Illustrated book,画本虫撰|Picture Book of Crawling Creatures (The Insect Book) (Ehon mushi erami),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,probably 1823 (later edition),1823,1823,One from a set of two polychrome woodblock printed books; ink and color on paper,Overall: 9 15/16 × 7 3/16 in. (25.2 × 18.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB44b,false,true,57570,Asian Art,Illustrated book,画本虫撰|Picture Book of Crawling Creatures (The Insect Book) (Ehon mushi erami),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,probably 1823 (later edition),1823,1823,One from a set of two polychrome woodblock printed books; ink and color on paper,Overall: 9 5/8 × 7 1/16 in. (24.5 × 18 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.853,false,true,78743,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1789,1789,1789,"Woodblock printed book; ink, color, and brass dust on paper",11 5/16 × 7 3/8 in. (28.7 × 18.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.858,false,true,78748,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,preface dated 1794,1794,1795,"Woodblock printed book; ink, color, and metallic pigments on paper",10 3/16 × 7 1/2 in. (25.8 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.859,false,true,78749,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1789,1789,1789,"Woodblock printed book; ink, color, and brass dust on paper",10 1/16 × 7 1/2 in. (25.5 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.897,false,true,78787,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,probably 1789,1789,1789,"Woodblock printed book (orihon, accordion-style); ink, color, mica, and gold-leaf on paper",10 11/16 × 7 9/16 in. (27.2 × 19.2 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB42a, b",false,true,45322,Asian Art,Illustrated book,繪本四季花|Picture Book of Flowers of the Four Seasons (Ehon shiki no hana),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"1801, first month",1801,1801,Set of two polychrome woodblock printed books; ink and color on paper,Overall (each volume): 8 1/4 × 5 13/16 in. (20.9 × 14.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB77a, b",false,true,57675,Asian Art,Illustrated book,"『百千鳥狂歌合』|Myriad Birds:A Playful Poetry Contest (Momo chidori kyōka-awase), 2 vols.",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Two volumes; woodblock printed books; ink and color on paper,Each: 10 × 7 1/2 × 1/4 in. (25.4 × 19.1 × 0.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.852a, b",false,true,78742,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,"Set of two woodblock printed books; ink, color, and mica (vol. 2) on paper",each: 9 13/16 × 7 3/16 in. (25 × 18.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.854a, b",false,true,78744,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Set of two woodblock printed books; ink and color on paper,each: 8 1/2 × 6 1/16 in. (21.6 × 15.4 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.856a, b",false,true,78746,Asian Art,Illustrated books,『青楼繪本年中行事』|Yoshiwara Picture Book of New Year’s Festivities (Seirō ehon nenjū gyōji),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1804,1804,1804,Set of two woodblock printed books; ink and color on paper,each: 9 × 6 5/16 in. (22.8 × 16 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.857a, b",false,true,78747,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1804,1804,1804,Set of two woodblock printed books; ink on paper,each: 9 × 6 5/16 in. (22.8 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.855a–c,false,true,78745,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1803,1798,1808,Set of three woodblock printed books; ink on paper,each: 8 7/16 × 6 in. (21.5 × 15.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB3,false,true,57538,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1823,1823,1823,Ink and color on paper,9 × 6 1/8 × 3/8 in. (22.9 × 15.6 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.680,false,true,78601,Asian Art,Illustrated book,一老画譜|Ichirō Picture Album (Ichirō gafu),Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1823,1823,1823,Woodblock printed book; ink and color on paper,9 × 6 5/16 in. (22.8 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.686,false,true,78607,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1815,1825,Woodblock printed book; ink and color on paper,9 × 6 3/8 in. (22.8 × 16.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.681a–c,false,true,78602,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1825,1835,Set of three woodblock printed books; ink and color on paper,8 11/16 × 6 3/16 in. (22 × 15.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.742a, b",false,true,78644,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Nichōsai,"Japanese, active 1780s",,Nichōsai,Japanese,1780,1789,1803,1803,1803,"Set of two woodblock printed books; one volume ink and color on paper, other volume ink on paper",10 3/16 × 7 1/16 in. (25.8 × 17.9 cm) 9 13/16 × 7 in. (25 × 17.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.863a, b",false,true,78753,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Yoshishige,"Japanese, active 1840s",,Yoshishige,Japanese,1840,1849,1848,1848,1848,Set of two woodblock printed books; ink and color on paper,each: 9 13/16 × 6 13/16 in. (25 × 17.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.35.3,false,true,48921,Asian Art,Folding screen,波に舟図屏風|Boats upon Waves,Japan,Edo period (1615–1868),,,,Artist,Studio of,Tawaraya Sōtatsu,"Japanese, died ca. 1640",,Tawaraya Sōtatsu,Japanese,1540,1640,mid- to late 17th century,1634,1699,"Six-panel folding screen; ink, color, and gold on paper",61 1/8 x 141 3/4 in. (155.2 x 360 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1949",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/48921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"55.94.3, .4",false,true,45388,Asian Art,Folding screen,"俵屋宗達工房 大原御幸図屏風|Royal Visit to Ōhara, from The Tale of the Heike",Japan,Edo period (1615–1868),,,,Artist,Studio of,Tawaraya Sōtatsu,"Japanese, died ca. 1640",,Tawaraya Sōtatsu,Japanese,1540,1640,first half of the 17th century,1600,1650,Pair of six-panel folding screens; ink and color on paper,Image (each screen): 63 7/16 x 143 5/16 in. (161.1 x 364 cm),"Fletcher Fund, 1955",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.60, .61",false,true,48923,Asian Art,Folding screen,月に秋草図屏風|Moon and Autumn Grasses,Japan,Edo period (1615–1868),,,,Artist,Studio of,Tawaraya Sōtatsu,"Japanese, died ca. 1640",,Tawaraya Sōtatsu,Japanese,1540,1640,mid- to late 17th century,1634,1699,"Pair of six-panel folding screens; ink, color, silver, and gold flecks on paper",Overall (each screen): 59 7/16 in. x 11 ft. 10 3/8 in. (151 x 361.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/48923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.335,false,true,73588,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"February–March, 1860",1860,1860,Polychrome woodblock prints; ink and color on paper,Image: 9 1/4 x 6 1/2 x 3/8 in. (23.5 x 16.5 x 1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/73588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.341a–f,false,true,73594,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,Illustrations attributed to,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1853,1853,1853,Five volumes of woodblock printed books; ink on paper,Overall (each volume): 10 x 7 1/8 x 1/4 in. (25.4 x 18.1 x 0.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/73594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.82.8,false,true,58638,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Shiomi Masanari,"Japanese, 1647–ca. 1722",,Shiomi Masanari,Japanese,1647,1732,early 18th century,1700,1733,"Case: gold on black lacquer with mother-of-pearl inlay; Fastener (ojime): gold with design of crabs, reed, and rock; Toggle (netsuke): ivory carved in the shape of a reclining bull (signed: Ran’ichi)",H. 2 13/16 in. (7.2 cm); W. 2 3/8 in. (6 cm); D. 7/8 in. (2.2 cm),"Gift of Wilton Lloyd-Smith and his wife, Marjorie Fleming Lloyd-Smith, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.99,false,true,58622,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Shiomi Masanari,"Japanese, 1647–ca. 1722",,Shiomi Masanari,Japanese,1647,1732,early 18th century,1700,1733,"Case: gold, silver, and blue lacquer on black lacquer; Fastener (ojime): pierced gold with floral design; Toggle (netsuke): crystal carved in the shape of a bucket",H. 3 1/16 in. (7.8 cm); W. 2 5/16 in. (5.9 cm); D. 1 1/16 in. (2.7 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB92,false,true,57766,Asian Art,Illustrated book,Kotori Ruishō|A Compendium of Small Birds,Japan,Edo period (1615–1868),,,,Artist,,Nantō,active early 19th century,,Nantō,Japanese,1800,1835,1836,1836,1836,Polychrome woodblock printed book,10 1/2 × 7 3/4 × 1/4 in. (26.7 × 19.7 × 0.6 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.796,false,true,78812,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Sadatoshi,"Japanese, active 1716–36",,Sadatoshi,Japanese,1716,1736,1751,1751,1751,Woodblock printed book; ink and hand-coloring (tanroku bon) on paper,10 1/4 × 7 1/16 in. (26 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.874,false,true,78764,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Mori Shunkei,"Japanese, active 1800–20",,Mori Shunkei,Japanese,1800,1820,1820,1820,1820,"Woodblock printed book (orihon, accordion-style); ink and color on paper",8 1/4 × 5 11/16 in. (21 × 14.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB60,false,true,57660,Asian Art,Illustrated book,水石画譜|Suiseki Picture Album (Suiseki gafu),Japan,Edo period (1615–1868),,,,Artist,,Satō Suiseki,"Japanese, active 1806–40",,Satō Suiseki,Japanese,1806,1840,"1814, sixth month; preface dated 1814, fourth month",1814,1814,Woodblock printed book; ink and color on paper,Overall: 10 × 6 7/8 in. (25.4 × 17.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.831,false,true,78721,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Satō Suiseki,"Japanese, active 1806–40",,Satō Suiseki,Japanese,1806,1840,1820,1820,1820,Woodblock printed book; ink and color on paper,10 3/16 × 7 in. (25.8 × 17.8 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.865,false,true,78755,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Yamada Yoshitsuna,"Japanese, active 1848–68",,Yamada Yoshitsuna,Japanese,1848,1868,1828,1828,1828,"Woodblock printed book (orihon, accordion-style but bound); ink and color on paper",7 3/16 × 9 1/4 in. (18.3 × 23.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.31,false,true,58573,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Kyūho,"Japanese, active 1789–1801",,Kyūho,Japanese,1789,1801,ca. 1800,1790,1810,Gold maki-e with black lacquer Ojime: metal or lacquered bead Netsuke: ivory carved with design of chrysanthemum and waves,H. 3 3/16 in. (8.1 cm); W. 2 1/16 in. (5.3 cm); D. 3/4 in. (1.9 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.782,false,true,78684,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Aikawa Minwa,"Japanese, active 1806–1821",,Aikawa Minwa,Japanese,1806,1821,1814,1814,1814,Woodblock printed book; ink and color on paper,10 3/8 × 7 1/16 in. (26.3 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.783,false,true,78685,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Aikawa Minwa,"Japanese, active 1806–1821",,Aikawa Minwa,Japanese,1806,1821,1818,1818,1818,Woodblock printed book; ink and color on paper,10 1/4 × 7 5/16 in. (26 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB59a, b",false,true,57659,Asian Art,Illustrated book,合川珉和画『光琳画式』|Kōrin's Painting Style (Kōrin gashiki),Japan,Edo period (1615–1868),,,,Artist,,Aikawa Minwa,"Japanese, active 1806–1821",,Aikawa Minwa,Japanese,1806,1821,1818,1818,1818,Set of two Woodblock printed books; ink and color on paper,Image: 10 1/16 x 7 1/16 x 3/16 in. (25.5 x 18 x 0.5 cm) Overall: 13 1/8 in. (33.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.653,false,true,78574,Asian Art,Illustrated book,"勝景眺望山水画譜|Prospects and Views, Picture Album of Landscapes (Shōkei chōbō, Sansui gafu)",Japan,Edo period (1615–1868),,,,Artist,,Kōkunsai Bairin,"Japanese, early 19th century",,Kōkunsai Bairin,Japanese,1800,1833,preface dated 1826,1826,1826,Woodblock printed book; ink and color on paper,7 3/16 × 4 13/16 in. (18.3 × 12.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB27,false,true,45447,Asian Art,Illustrated book,Kyoka Hyaku Monogatari|Poems on One Hundred Ghost Stories,Japan,Edo period (1615–1868),,,,Artist,,Masazumi Ryusai,"Japanese, active 19th century",,Masazumi Ryusai,Japanese,1800,1899,1853,1853,1853,Polychrome woodblock printed book; ink and color on paper,9 × 6 3/8 × 1 1/2 in. (22.9 × 16.2 × 3.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB30,false,true,57562,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1790,1780,1800,Ink on paper,8 1/2 × 6 × 3/8 in. (21.6 × 15.2 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB29a, b",false,true,57560,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,1790,1790,1790,Two volumes; ink and color on paper,Each: 8 1/2 × 6 × 1/4 in. (21.6 × 15.2 × 0.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.864,false,true,78754,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1858,1858,1858,Woodblock printed book; ink and color on paper,8 5/8 × 6 1/8 in. (21.9 × 15.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB23,false,true,57555,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotsune,"Japanese, active ca. 1757–1779",,Torii Kiyotsune,Japanese,1757,1779,1774,1774,1774,Ink and color on paper,8 3/4 × 6 × 3/8 in. (22.2 × 15.2 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.893,false,true,78783,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Hasegawa Myōtei,"Japanese, active late 17th century",,Hasegawa Myōtei,Japanese,1667,1699,1714,1714,1714,Woodblock printed book; ink on paper,10 11/16 × 7 1/2 in. (27.2 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.861,false,true,78751,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Umemaru Yūzen,"Japanese, active late 18th century",,Umemaru Yūzen,Japanese,1767,1799,ca. 1820s,1820,1829,"Woodblock printed book; ink, color, and mica on paper",4 15/16 × 7 5/16 in. (12.5 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.868,false,true,78758,Asian Art,Illustrated book,(榎木新右衛門)画 『雛形伊勢乃海』|Book of Kosode Patterns (Hiinagata Ise no umi),Japan,Edo period (1615–1868),,,,Artist,,Enoki Hironobu,"Japanese, active mid- 18th century",,Enoki Hironobu,Japanese,1734,1766,1751,1751,1751,Three woodblock-printed books bound as one; ink on paper,10 3/4 × 7 3/8 in. (27.3 × 18.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.795a–c,false,true,78811,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Takagi Sadatake,"Japanese, active early 18th century",,Takagi Sadatake,Japanese,1700,1733,1734,1734,1734,Set of two woodblock printed books; ink on paper,each: 10 5/8 × 7 5/8 in. (27 × 19.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.683,false,true,78604,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Fukuchi Hakuei,"Japanese, active early 19th century",,Fukuchi Hakuei,Japanese,1800,1833,ca. 1814,1804,1824,Woodblock printed book; ink and color on paper,9 15/16 × 6 1/2 in. (25.3 × 16.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.107,false,true,59824,Asian Art,Netsuke,文房具牙彫根付|Writing Utensils,Japan,Edo period (1615–1868),,,,Artist,,Ryūsen,"Japanese, active mid–19th century",,Ryūsen,Japanese,1834,1866,mid-19th century,1825,1875,Ivory,H. 1 1/2 in. (3.8 cm); W. 1 3/8 in. (3.5 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB12,false,true,57546,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1802,1802,1802,Ink and color on paper,7 1/2 × 5 1/4 × 3/8 in. (19.1 × 13.3 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB13,false,true,57547,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1845,1835,1855,Polychrome woodblock printed book; ink and color on paper,4 7/8 × 7 3/4 × 5/8 in. (12.4 × 19.7 × 1.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB14,false,true,57548,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1840,1830,1850,Two volumes; ink and color on paper,Each: 9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB16,false,true,45445,Asian Art,Illustrated book,Onna Imagawa|Precepts for Women,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1820s,1820,1829,Book of polychrome woodblock prints; ink and color on paper,9 × 6 1/4 × 1/2 in. (22.9 × 15.9 × 1.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB17,false,true,57550,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1814,1804,1824,Polychrome woodblock prints in a book; ink and color on paper,10 × 13 1/4 × 1/4 in. (25.4 × 33.7 × 0.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB18,false,true,57551,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1834,1834,1834,Woodblock print; ink on paper,9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB20,false,true,57553,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1847,1837,1857,Woodblock print; ink on paper,9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB69,false,true,57667,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1811,1811,1811,Woodblock print; ink on paper,8 3/4 × 6 1/16 × 1 in. (22.2 × 15.4 × 2.5 cm),"Gift of Julius Mahn, 1919",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB80,false,true,57677,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1804,1794,1814,Pentaptych of polychrome woodblock prints,14 1/2 x 48 1/2 in. (36.8 x 123.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB82,false,true,57678,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1816,1816,1816,Woodblock printed book; ink and color on paper,Overall: 8 7/8 × 6 1/4 × 1/2 in. (22.5 × 15.9 × 1.3 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB106,false,true,57791,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1833,1833,1833,Five volumes; ink on paper,Each: 9 × 6 3/16 × 1/4 in. (22.9 × 15.7 × 0.6 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB107,false,true,45444,Asian Art,Illustrated book,Ehon Musashi no Abumi|A Picture Book of Japanese Warriors,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1836,1700,1868,Polychrome Woodblock printed book,8 7/8 × 6 1/8 × 3/8 in. (22.5 × 15.6 × 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB108,false,true,57793,Asian Art,Illustrated book,富岳百景|Mount Fuji of the Mists (Vol. 1); Mount Fuji of the Ascending Dragon (Vol. 2),Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1834–35,1834,1835,Woodblock print (first and second volumes with 100 pages of illustrations); ink and color on paper,9 x 6 1/4 in. (22.9 x 15.9 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB109,false,true,57794,Asian Art,Illustrated book,富岳百景|Fugaku Hyakkei,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1834–35,1834,1835,Woodblock print; ink and color on paper,9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB110,false,true,57796,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1850,1850,1850,Ink on paper,8 7/8 × 6 1/4 × 3/8 in. (22.5 × 15.9 × 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB112,false,true,57805,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1834,1834,1834,Woodblock printed book; ink and color on paper,Overall: 9 × 6 1/4 × 1/2 in. (22.9 × 15.9 × 1.3 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB120,false,true,57812,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1818,1818,1818,Ink on paper,10 1/2 × 7 1/16 × 3/8 in. (26.7 × 17.9 × 1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.1,false,true,57679,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,after 1828,1828,1868,Woodblock printed book; ink and color on paper,Overall: 9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.2,false,true,57680,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,after 1814,1815,1868,Woodblock printed book; ink and color on paper,9 x 6 1/4 x 1/2 in. (22.9 x 15.9 x 1.3 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.4,false,true,57682,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1819,1819,1819,Woodblock printed book; ink and color on paper,Overall: 9 × 6 × 3/8 in. (22.9 × 15.2 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.7,false,true,57684,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1819,1819,1819,Woodblock printed book; ink and color on paper,Overall: 9 × 6 × 3/8 in. (22.9 × 15.2 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.8,false,true,57685,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1817,1817,1817,Woodblock printed book; ink and color on paper,Overall: 9 × 6 × 3/8 in. (22.9 × 15.2 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.9,false,true,57686,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1819,1819,1819,Woodblock printed book; ink and color on paper,Overall: 9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.711,false,true,78632,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1799,1799,1799,Woodblock printed book; ink on paper,10 7/16 × 7 1/16 in. (26.5 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.713,false,true,78634,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1802,1802,1802,Woodblock printed book; ink and color on paper,7 5/8 × 5 1/4 in. (19.3 × 13.4 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.714,false,true,78635,Asian Art,Illustrated book,『画本狂歌山満多山』|Picture Book of Kyōka Poems: Mountains upon Mountains (Ehon kyōka yama mata yama),Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1804,1804,1804,Woodblock printed book; ink and color on paper,10 7/16 × 6 7/8 in. (26.5 × 17.5 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.719,false,true,78790,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1814,1814,1814,"Woodblock printed book (orihon, accordion-style); ink and color on paper",10 1/4 × 6 13/16 in. (26 × 17.3 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.721,false,true,78792,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1815,1815,1815,Woodblock printed book; ink and color on paper,10 1/4 × 6 7/8 in. (26 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.726,false,true,78797,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1820,1820,1820,Woodblock printed book; ink and color on paper,10 3/8 × 6 3/4 in. (26.4 × 17.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.728,false,true,78799,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1823,1823,1823,Woodblock printed book; ink and color on paper,9 × 6 1/4 in. (22.8 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.730,false,true,78801,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1829,1829,1829,Woodblock printed book; ink and color on paper,8 15/16 × 6 1/4 in. (22.7 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.733,false,true,78804,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1836,1836,1836,Woodblock printed book; ink on paper,8 13/16 × 6 1/8 in. (22.4 × 15.6 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.734,false,true,78805,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1840s,1840,1849,Woodblock printed book; ink on paper,8 7/8 × 6 5/16 in. (22.5 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.737,false,true,78639,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1843,1843,1843,Woodblock printed book; ink and color on paper,8 3/4 × 6 1/8 in. (22.3 × 15.6 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.875,false,true,78765,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1843,1843,1843,Woodblock printed book; ink on paper,8 15/16 × 6 1/8 in. (22.7 × 15.6 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.878,false,true,78768,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1815,1815,1815,Woodblock printed book; ink on paper,8 7/8 × 6 3/8 in. (22.5 × 16.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.881,false,true,78771,Asian Art,Illustrated book,"繪本彩色通 初編|Picture Book on the Use of Coloring, first volume (Ehon saishikitsū shohen)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1848,1848,1848,Woodblock printed book; ink on paper and color scribbles,7 3/16 × 5 1/16 in. (18.3 × 12.9 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.882,false,true,78772,Asian Art,Illustrated book,『絵本和漢誉』|Picture Book on Heroes of China and Japan (Ehon wakan no homare),Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1850 (designed ca. 1836; published posthumously),1836,1850,Woodblock printed book; ink on paper,9 1/16 × 6 3/8 in. (23 × 16.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.10,false,true,57687,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1819,1819,1819,Woodblock printed book; ink and color on paper,Overall: 9 × 6 × 3/8 in. (22.9 × 15.2 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.12,false,true,57689,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1834,1834,1834,Woodblock printed book; ink and color on paper,Overall: 9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.13,false,true,57741,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1849,1839,1859,Woodblock printed book; ink and color on paper,Overall (JIB81.13 and .14 combined): 9 × 6 1/4 × 3/4 in. (22.9 × 15.9 × 1.9 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.14,false,true,57742,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1875–78,1875,1878,Woodblock printed book; ink and color on paper,See JIB81.13,"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB19a, b",false,true,57552,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1835,1835,1835,Two volumes; woodblock print; ink on paper,Each: 9 × 6 1/4 × 1/2 in. (22.9 × 15.9 × 1.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB79a, b",false,true,45274,Asian Art,Illustrated book,Edo meisho|Famous Sites of Edo,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1800,1700,1868,Two volumes; polychrome woodblock print; ink and color on paper,Each: 10 1/16 × 6 5/8 × 1/4 in. (25.6 × 16.8 × 0.6 cm),"Gift of Mary L. Cassilly, 1894",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/45274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB11a–c,false,true,57545,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1828,1818,1838,Three volumes; ink on paper,9 × 6 1/4 × 1/2 in. (22.9 × 15.9 × 1.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB15a–c,false,true,57549,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1803,1793,1813,Three volumes; ink and color on paper,Each: 10 × 6 3/4 × 1/4 in. (25.4 × 17.1 × 0.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.710a, b",false,true,78631,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1802,1797,1807,Set of two woodblock printed books; ink and color on paper,each: 10 1/2 × 6 7/8 in. (26.6 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.712a, b",false,true,78633,Asian Art,Illustrated books,『東都名所一覧』|Fine Views of the Eastern Capital at a Glance (Tōto meisho ichiran),Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1800,1800,1800,Set of two woodblock printed books; ink and color on paper,each: 10 3/16 × 6 7/8 in. (25.8 × 17.5 cm),"Purchase, Mary and James G. Wallach Family Foundation Gift, in honor of John T. Carpenter, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.722a, b",false,true,78793,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,"part I, 1817 and II, 1819",1817,1819,Woodblock printed books; ink on paper,"vol. ""kan"": 7 3/16 × 4 15/16 in. (18.2 × 12.6 cm) vol. ""zen"": 8 3/8 × 5 7/8 in. (21.2 × 14.9 cm)","Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.738a, b",false,true,78640,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1850,1850,1850,Set of two woodblock printed books; ink on paper,each: 9 × 6 3/16 in. (22.8 × 15.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.715a–d,false,true,78636,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1805–38,1805,1838,Set of four woodblock printed books; ink on paper,each: 9 13/16 × 7 1/16 in. (25 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.716a–c,false,true,78637,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1806,1801,1811,Set of three woodblock printed books; ink and color on paper,each: 9 1/4 × 6 5/8 in. (23.5 × 16.9 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.717a–e,false,true,78788,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1809,1809,1809,Set of five woodblock printed books; ink on paper,each: 8 15/16 × 6 5/16 in. (22.7 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.718a–f,false,true,78789,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1810,1810,1810,Set of six woodblock printed books; ink on paper,Other (each): 9 × 6 1/4 in. (22.8 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.720a–r,false,true,78791,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1814–78,1814,1878,Set of nineteen woodblock printed books; ink and color on paper,each approximately: 9 × 6 1/4 in. (22.8 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.729a–c,false,true,78800,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,"part I, 1828, part II, 1830s, and part III, ca. 1848",1828,1848,Set of three woodblock printed books; ink on paper,each approximately: 8 7/8 × 6 1/16 in. (22.5 × 15.4 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.732a–c,false,true,78803,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1834; 1835; ca. 1849,1834,1849,Set of three woodblock printed books; ink on paper,each: 8 15/16 × 6 1/4 in. (22.7 × 15.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.735a–e,false,true,78806,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1833,1833,1833,Set of five woodblock printed books; ink on paper,each: 9 × 6 1/8 in. (22.8 × 15.6 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.689,false,true,78610,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1839,1839,1839,"Woodblock printed book; ink, color, and white paint on paper",8 15/16 × 6 5/16 in. (22.7 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.690,false,true,78611,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1847,1847,1847,Woodblock printed book; ink and color on paper,7 1/16 × 4 13/16 in. (18 × 12.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.691,false,true,78612,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1849,1849,1849,Woodblock printed book; ink and color on paper,each: 7 1/16 × 4 13/16 in. (18 × 12.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.693,false,true,78614,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Woodblock printed book; ink and color on paper,10 3/8 × 7 1/16 in. (26.3 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.694,false,true,78615,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1855,1855,1855,Woodblock printed book; ink and color on paper,9 1/16 × 6 5/16 in. (23 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.695,false,true,78616,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1859,1859,1859,Woodblock printed book; ink and color on paper,8 9/16 × 6 1/8 in. (21.7 × 15.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.692a–j,false,true,78613,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1850–67,1850,1867,Set of ten woodblock printed books; ink and color on paper,each: 7 1/16 × 4 13/16 in. (18 × 12.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.25.128,false,true,58950,Asian Art,Inrō,兎月秋草蒔絵鞘印籠|Inrō with Rabbit in the Moon and Autumn Grasses,Japan,Edo period (1615–1868),,,,Artist,,Kyūkoku,"Japanese, active first half of the 19th century",,Kyūkoku,Japanese,1800,1849,first half of the 19th century,1800,1849,"Two-part (sheath type); lacquered wood with gold, silver, and pewter hiramaki-e, togidashimaki-e, on red lacquer ground Netsuke: ivory; rabbit Ojime: pierced metal with floral design",3 3/8 x 1 7/8 x 1 1/8 in. (8.5 x 4.7 x 2.8 cm),"Gift of Mrs. George A. Crocker (Elizabeth Masten), 1937",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.40.878a, b",false,true,58628,Asian Art,Inrō,七夕蒔絵印籠|Inrō with Tanabata Story of the Weaver and the Herdboy,Japan,Edo period (1615–1868),,,,Artist,,Nomura Kyūkoku,"Japanese, active first half of the 19th century",,Nomura Kyūkoku,Japanese,1800,1849,active first half of the19th century,1800,1849,Three cases; lacquered wood with gold hiramaki-e and ivory inlay on mother-of-pearl ground; Netsuke: carved ivory; flowers and grasses with silver butterflies; Ojime: silver and gold quail in autumn grasses,Overall (inro): H. 3 7/8 in. (9.8 cm); W. 2 11/16 in. (6.9 cm); D. 13/16 in. (2 cm) Overall (netsuke): H. 11/16 in. (1.7 cm); Diam. 1 11/16 in. (4.3 cm) Overall (ojime): H. 9/16 in. (1.5 cm); W. 9/16 in. (1.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.582,false,true,45437,Asian Art,Sake cup,,Japan,Edo period (1615–1868),,,,Artist,,Shomosai,"Japanese, active late 18th–early 19th century",,Shomosai,Japanese,0018,0019,mid-19th century,1834,1866,Gold lacquer on red lacquer ground,H. 1 1/16 in. (2.7 cm); W. 4 3/4 in. (12.1 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.587,false,true,45439,Asian Art,Sake cup,,Japan,Edo period (1615–1868),,,,Artist,,Shomosai,"Japanese, active late 18th–early 19th century",,Shomosai,Japanese,0018,0019,mid-19th century,1834,1866,Gold lacquer on red lacquer ground,H. 1 in. (2.5 cm); W. 4 1/8 in. (10.5 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.40,false,true,59162,Asian Art,Netsuke,牛牙彫根付|Ox,Japan,Edo period (1615–1868),,,,Artist,,Tomotada,"Japanese, active late 18th–early 19th century",,Tomotada,Japanese,1777,1833,late 18th–early 19th century,1777,1833,Ivory,H. 1 in. (2.5 cm); W. 2 1/4 in. (5.7 cm); D. 1 1/8 in. (2.9 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.91,false,true,59180,Asian Art,Netsuke,猪牙彫根付|Boar,Japan,Edo period (1615–1868),,,,Artist,,Tomotada,"Japanese, active late 18th–early 19th century",,Tomotada,Japanese,1777,1833,late 18th–early 19th century,1767,1833,Ivory,H. 1 in. (2.5 cm); W. 2 1/8 in. (5.4 cm); D. 1 1/4 in. (3.2 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.918,false,true,59612,Asian Art,Netsuke,狼牙彫根付|Wolf,Japan,Edo period (1615–1868),,,,Artist,,Tomotada,"Japanese, active late 18th–early 19th century",,Tomotada,Japanese,1777,1833,late 18th–early 19th century,1767,1833,Carved ivory,H. 1 1/4 in. (3.2 cm); W. 7/8 in. (2.2 cm); D. 2 1/8 in. (5.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1106,false,true,60290,Asian Art,Netsuke,犬牙彫根付|Dog,Japan,Edo period (1615–1868),,,,Artist,,Tomotada,"Japanese, active late 18th–early 19th century",,Tomotada,Japanese,1777,1833,late 18th century–early 19th century,1771,1835,Ivory,H. 2 1/2 in. (6.4 cm); W. 1 1/4 in. (3.2 cm); D. 1/2 in. (1.3 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/60290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1154,false,true,60305,Asian Art,Netsuke,枝豆牙彫根付|Edamame (Soy Beans),Japan,Edo period (1615–1868),,,,Artist,,Okatomo,"Japanese, active late 18th–early 19th century",,Okatomo,Japanese,1767,1833,late 18th–early 19th century,1771,1835,Ivory,H. 3/4 in. (1.9 cm); W. 2 1/4 in. (5.7 cm); D. 1 1/2 in. (3.8 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/60305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1441,false,true,60386,Asian Art,Netsuke,東方朔仙人牙彫根付|Daoist Immortal Tōbōsaku Sennin,Japan,Edo period (1615–1868),,,,Artist,,Ryūminsai,"Japanese, active late 18th–early 19th century",,Ryūminsai,Japanese,1767,1833,late 18th–early 19th century,1767,1833,Ivory,H. 3 1/4 in. (8.3 cm); W. 1 1/2 in. (3.8 cm); D. 1 in. (2.5 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/60386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.2362,false,true,59409,Asian Art,Netsuke,鉄拐仙人木彫根付|Daoist Immortal Tekkai,Japan,Edo period (1615–1868),,,,Artist,,Chikusai,"Japanese, active late 18th–early 19th century",,Chikusai,Japanese,1767,1833,late 18th–early 19th century,1767,1833,Wood,H. 2 3/4 in. (7 cm); W. 1 in. (2.5 cm); D. 1 1/4 in. (3.2 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.23,false,true,58569,Asian Art,Inrō,蜻蛉蒔絵印籠|Inrō with Dragonflies,Japan,Edo period (1615–1868),,,,Artist,,Kōami Nagataka,"Japanese, active second half of the 18th century",,Kōami Nagataka,Japanese,1750,1799,second half of the 18th century,1750,1799,"Three cases; lacquered wood with gold, red lacquer takamaki-e, hiramaki-e with mother-of-pearl inlay on black lacquer ground; Netsuke: carved ivory; dog; Ojime: carved ivory; persimmon",H. 2 11/16 in. (6.8 cm); W. 2 3/4 in. (7 cm); D. 7/8 in. (2.2 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.92,false,true,58617,Asian Art,Inrō,掛軸文字散蒔絵印籠|Inrō with Carp Hanging Scroll and Characters,Japan,Edo period (1615–1868),,,,Artist,,Kōami Chōkō,"Japanese, active second half of the 18th century",,Kōami Chōkō,Japanese,1750,1799,second half of the 18th century,1750,1799,"Three cases; lacquered wood with gold and silver takamaki-e, hiramaki-e, togidashimaki-e, gold foil cut-outs, and mother-of-pearl inlay on black ground Netsuke: kagamibuta with Shōjō design Ojime: agate bead",3 7/8 x 3 5/16 x 15/16 in. (9.8 x 8.4 x 2.4 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.877,false,true,45573,Asian Art,Inrō,祭蒔絵印籠|Inrō with Street Festival (obverse); People Watching a Puppet Show (reverse),Japan,Edo period (1615–1868),,,,Artist,,Tatsuke Takamasu,"Japanese, active second half of the 18th century",,Tatsuke Takamasu,Japanese,1750,1799,second half of the 18th century,1750,1799,"Three cases; lacquered wood with gold, silver, and color (iroko) togidashimaki-e on black lacquer ground Netsuke: carved teakwood; peach with a monkey inside (signed: Kagetoshi) Ojime: carved teakwood with floral design",2 13/16 x 2 15/16 x 1 1/8 in. (7.1 x 7.5 x 2.9 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.883,false,true,58803,Asian Art,Inrō,葡萄蒔絵印籠 銘「樗平」|Inrō with Grapevine,Japan,Edo period (1615–1868),,,,Artist,,Nomura Choheisai,"Japanese, active second half of the 18th century",,Nomura Choheisai,Japanese,1750,1799,second half of the 18th century,1750,1799,"One case; lacquered wood with gold hiramaki-e, gold foil application with green stained ivory, mother-of-pearl, amber, and horn inlays on black lacquer ground Netsuke: ivory; kagamibuta with inlaid design of gourd and vine Ojime: ivory bead with inlaid design of branch with fruits",H. 3 1/16 in. (7.7 cm); W. 3 13/16 in. (9.7 cm); D. 1 3/16 in. (3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1436,false,true,60382,Asian Art,Netsuke,狸牙彫根付|Tanuki (Japanese Racoon Dog) with a Sake Bottle and Bills for Sake,Japan,Edo period (1615–1868),,,,Artist,,Garaku,"Japanese, active second half of the 18th century",,Garaku,Japanese,1750,1799,second half of the 18th century,1750,1799,Ivory,H. 2 3/4 in. (7 cm); W. 1 5/8 in. (4.1 cm); D. 1 in. (2.5 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/60382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.846,false,true,78736,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Ogura Tōkei,"Japanese, active second half of the 18th century",,Ogura Tōkei,Japanese,1750,1799,1809,1809,1809,Woodblock printed book; ink on paper,9 13/16 × 6 7/8 in. (25 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.847,false,true,78737,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Shōfusai Tōsen,"Japanese, active second half of the 19th century",,Shōfusai Tōsen,Japanese,1850,1899,mid-19th century,1825,1875,Woodblock printed book; ink and color on paper,11 1/4 × 7 7/8 in. (28.5 × 20 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB105a, b",false,true,57790,Asian Art,Illustrated book,中野期明画 『尾形流百図』|One Hundred Paintings of the Ogata Lineage (Ogata ryu hyakuzu),Japan,Meiji period (1868-1912),,,,Artist,,Nakano Kimei,"Japanese, 1834–1892",,Nakano Kimei,Japanese,1834,1892,1892,1892,1892,Set of two Woodblock printed books; ink on paper,Image (a): 10 x 7 3/8 x 1/2 in. (25.4 x 18.8 x 1.2 cm) Overall: 13 7/16 in. (34.2 cm) (open) Image (b): 10 x 7 3/8 x 7/16 in. (25.4 x 18.8 x 1.1 cm) Overall: 13 9/16 in. (34.5 cm) (open),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.343a–c,false,true,73596,Asian Art,Illustrated book,橫濱開港見聞誌|Observations on the Opening of Yokohama (Yokohama kaiko kanbunshi),Japan,Shōwa period (1926–89),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1967,1967,1967,Book; ink on paper,Image (a): 8 1/2 x 6 1/8 x 7/8 in. (21.6 x 15.6 x 2.2 cm) Image (b): 8 1/4 x 5 7/8 x 1/16 in. (21 x 14.9 x 0.2 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/73596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.642,false,true,57779,Asian Art,Box,,Japan,Meiji period (1868–1912),,,,Artist,Style of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,second half of the 19th century,1850,1899,"Gold inlaid with mother-of-pearl and tin"" to ""Gold hiramaki-e, takamaki-e, tin and mother-of-pearl inlay on gold ground",H. 1 7/8 in. (4.8 cm); W. 3 9/16 in. (9 cm); D. 2 1/16 in. (5.2 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/57779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.162a, b",false,true,53643,Asian Art,Box,,Japan,Meiji period (1868–1912),,,,Artist,Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,second half of 19th century,1850,1899,"Bottle gourd; gold, red takamaki-e, mother-of-pearl, tin, ceramic inlay",H. 4 in. (10.2 cm); Diam. at top 3 in. (7.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/53643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.754,false,true,78656,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,After,Seizei Kigyoku,"Japanese, 1732–1756",,Seizei Kigyoku,Japanese,1732,1756,1901,1901,1901,Woodblock printed book; ink and color on paper,11 1/8 × 7 1/2 in. (28.2 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.805,false,true,78695,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Sengai Gibon,"Japanese, 1750–1837",,Sengai Gibon,Japanese,1750,1837,1894,1894,1894,Woodblock printed book; ink on paper,Other: 9 1/16 × 5 7/8 in. (23 × 15 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.181,false,true,53644,Asian Art,Chest,,Japan,Meiji period (1868–1912),,,,Artist,Attributed to,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,ca. 1878,1868,1888,Wood with black and gold lacquer,H. 25 1/2 in. (64.8 cm); W. 28 3/4 in. (73 cm); D. 10 1/8 in. (25.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/53644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.143a–g,false,true,75746,Asian Art,Tiered box,明治時代 柴田是真 果蔬蒔絵重箱|Tiered Food Box with Summer and Autumn Fruits,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,ca. 1868–90,1850,1900,"Brown lacquer with gold, silver, and colored lacquer maki-e",H. 16 1/8 in. (41 cm); W. 9 in. (22.9 cm); D. 9 5/8 in. (24.4 cm),"Purchase, The Vincent Astor Foundation Gift and Parnassus Foundation/Jane and Raphael Bernstein Gift, 2010",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/75746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.142a–j,false,true,44868,Asian Art,Writing box,明治時代 柴田是真派 蒲公英酒瓢蒔絵 硯箱|Writing Box with Gourd,Japan,Meiji period (1868–1912),,,,Artist,School of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1886,1886,1886,"Black lacquer with gold and silver hiramaki-e, colored lacquer application",H. 1 1/2 in. (3.8 cm); W. 7 5/8 in. (19.4 cm); L. 8 7/8 in. (22.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/44868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.56,false,true,57347,Asian Art,Screen,花鳥図風炉先屏風|Folding Screen for Tea Ceremony with Six Bird-and-Flower Paintings,Japan,Meiji period (1868–1912),,,,Artist,In the style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1899,"Two-panel folding screen; lacquer, color, and silver on paper",Overall (each panel): 14 1/2 x 31 in. (36.8 x 78.7 cm),"Gift of Mr. and Mrs. Nathan V. Hammer, 1954",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/57347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.119,false,true,57345,Asian Art,Folding screen,,Japan,Meiji period (1868–1912),,,,Artist,Style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1868–1912,1868,1912,Two-panel folding screen; color on silk,26 3/4 x 69 in. (67.9 x 175.3 cm),"Rogers Fund, 1953",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/57345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.572.3,false,true,77169,Asian Art,Screen,柴田是真筆 烏鷺図屏風|Three Crows in Flight and Two Egrets at Rest,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1899,Two-panel folding screen; colored lacquer and white pigment on gilt paper,Image (each panel): 53 1/2 x 36 in. (135.9 x 91.4 cm) Overall (each panel): 60 7/8 x 39 1/4 in. (154.6 x 99.7 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2011",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/77169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.137,false,true,45080,Asian Art,Folding screen,月に秋草図屏風|Autumn Grasses in Moonlight,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,second half of the 19th century,1850,1899,"Two-panel folding screen; ink, lacquer, silver, and silver leaf on paper","Image (each panel): 18 in. × 33 1/4 in. (45.7 × 84.5 cm) Each panel, with frame: 26 1/8 × 34 3/8 in. (66.4 × 87.3 cm) Overall with frame (both panels): 26 1/8 in. × 69 in. (66.4 × 175.3 cm)","The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.186,false,true,45443,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Nakayama Komin,"Japanese, 1808–1870",,Nakayama Komin,Japanese,1808,1870,late 19th century,1871,1899,"Gold lacquer with gold hiramkie sprinkled and polished lacquer, nashiji (pear skin) lacquer, and mother-of-pearl, ivory, and wood inlay; Interior: nashiji and fundame; Netsuke: wood-framed ivory plaque with bird and flower inlay; Ojime: lacquer Daikoku's hammer",3 9/16 x 2 13/16 x 1 1/8 in. (9.1 x 7.1 x 2.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1998.189.1, .2",false,true,40006,Asian Art,Folding screens,,Japan,Meiji period (1868–1912),,,,Artist,,Shiokawa Bunrin,"Japanese, 1808–1877",,Shiokawa Bunrin,Japanese,1808,1877,1875,1875,1875,Pair of six-panel folding screens; ink and gold on paper,Image (each): 59 5/8 in. x 11 ft. 6 5/8 in. (151.4 x 352.1 cm),"Purchase, Friends of Asian Art Gifts, 1998",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/40006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.148,false,true,45581,Asian Art,Box,果蔬蒔絵菓子箱|Two-Tiered Box with Design of Autumn Fruits,Japan,Meiji period (1868–1912),,,,Artist,,Ikeda Taishin,"Japanese, 1825–1903",,Ikeda Taishin,Japanese,1825,1903,second half of the 19th century,1850,1899,"Lacquered wood with gold, silver, black, and red takamaki-e, hiramaki-e, and e-nashiji on black lacquer ground",H. 4 1/4 in. (10.8 cm); W. 4 1/4 in. (10.8 cm); L. 6 1/8 in. (15.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.767,false,true,78669,Asian Art,Illustrated book,『暁斎百鬼画談』|Kyōsai’s Pictures of One Hundred Demons (Kyōsai hyakki gadan),Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1890 (first edition published posthumously in 1889),1890,1890,"Woodblock printed book (orihon, accordion-style); ink and color on paper",7 13/16 × 4 3/4 in. (19.8 × 12 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.764a–d,false,true,78666,Asian Art,illustrated books,『暁斎画談』|Kyōsai’s Treatise on Painting (Kyōsai gadan),Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1887,1887,1887,Set of four woodblock printed books; ink and color on paper,each: 9 15/16 × 6 7/8 in. (25.3 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.748,false,true,78650,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Nishiyama Ken,"Japanese, 1833–1897",,Nishiyama Ken,Japanese,1833,1897,1886,1886,1886,"Woodblock printed book (orihon, accordion-style); ink and color on paper",10 5/8 × 5 1/2 in. (27 × 14 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB4a–c,false,true,57539,Asian Art,Illustrated book,梅嶺百鳥畫譜|Bairei Picture Album of One Hundred Birds (Bairei hyakuchō gafu),Japan,Meiji period (1868–1912),,,,Artist,,Kōno Bairei,"Japanese, 1844–1895",,Kōno Bairei,Japanese,1844,1895,1881–84,1881,1884,Set of three polychrome woodblock printed books; ink and color on paper,Overall (vol. 1): 9 5/8 × 6 3/8 in. (24.5 × 16.2 cm) Overall (vol. 2): 9 3/4 × 6 9/16 in. (24.8 × 16.7 cm) Overall (vol. 3): 9 3/4 × 6 7/16 in. (24.8 × 16.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.652a–c,false,true,78573,Asian Art,Illustrated books,楳嶺百鳥畫譜|Bairei Picture Album of One Hundred Birds (Bairei hyakuchō gafu),Japan,Meiji period (1868–1912),,,,Artist,,Kōno Bairei,"Japanese, 1844–1895",,Kōno Bairei,Japanese,1844,1895,1881–84,1881,1884,Set of three woodblock printed books; ink and color on paper,each: 9 1/2 × 6 13/16 in. (24.2 × 17.3 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2003.317.1, .2",false,true,65603,Asian Art,Screen,,Japan,Meiji period (1868–1912),,,,Artist,,Suzuki Shōnen,"Japanese, 1849–1918",,Suzuki Shōnen,Japanese,1849,1918,late 19th century,1867,1899,Pair of six-panel folding screens; ink on gold-leaf,Image (each): 68 1/16 in. x 12 ft. 1 3/16 in. (172.9 x 368.8 cm),"Purchase, The B. D. G. Leviton Foundation Gift, 2003",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/65603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.800,false,true,78816,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,1891,1891,1891,Woodblock printed book; ink and color on paper,9 13/16 × 6 3/4 in. (25 × 17.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.320.2,false,true,60501,Asian Art,Vase,菊紋楓枝文七宝瓶 (一対)|Imperial Presentation Vase with Maple Branches and Imperial Chrysanthemum Crest (one of a pair),Japan,Meiji period (1868–1912),,,,Artist,,Kawade Shibatarō,"Japanese, 1861–1921",,Kawade Shibatarō,Japanese,1861,1921,ca. 1906,1896,1916,Standard and repoussé cloisonné enamel; silver wires and rims; signed: combined marks of Andō Cloisonné Company and Kawade Shibatarō,H. 17 1/8 in. (43.5 cm); W. 6 3/4 in. (17.1 cm); D. 4 1/2 in. (11.4 cm),"Gift of Barbara S. McKenna, 1976",,,,,,,,,,,,Cloisonné,,http://www.metmuseum.org/art/collection/search/60501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.799,false,true,78815,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Seihō,"Japanese, 1864–1942",,Takeuchi Seihō,Japanese,1864,1942,ca. 1905–6,1905,1906,"Woodblock printed book (orihon, accordion-style); ink, color and metallic pigments on paper",14 1/4 × 9 13/16 in. (36.2 × 25 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB202,false,true,65710,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,1898–1899,1898,1899,Polychrome woodblock prints; ink and color on paper,Overall: 9 1/2 x 14 1/4 in. (24.1 x 36.2 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/65710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.789a, b",false,true,78691,Asian Art,Illustrated books,,Japan,Meiji period (1868–1912),,,,Artist,,Ogino Issui,"Japanese, active 1900–10",,Ogino Issui,Japanese,1900,1910,1903,1903,1903,Set of two woodblock printed books; ink and color on paper,each: 10 1/16 × 7 1/16 in. (25.5 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB83,false,true,57390,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1878,1878,1878,Woodblock printed book; ink and color on paper,Overall: 9 1/8 × 6 × 3/8 in. (23.2 × 15.2 × 1 cm),"Rogers Fund, 1932",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.365,false,true,44603,Asian Art,Calligraphy,Sono gi o aege|Embrace Righteousness,Japan,Taishō period (1912–26),,,,Artist,,Tomioka Tessai,"Japanese, 1836–1924",,Tomioka Tessai,Japanese,1836,1924,20th century,1912,1926,Framed calligraphy; ink on paper,Image: 12 3/8 × 36 3/16 in. (31.4 × 91.9 cm) Framed: 17 3/4 × 49 1/2 in. (45.1 × 125.7 cm),"Gift of Dr. Yukikazu Iwasa, in honor of Mrs. Shizuko Iwasa, 1989",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/44603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.2068,false,true,62167,Asian Art,Basket,掛花籃|Large Flower Basket,Japan,Meiji period (1868–1912),,,,Artist,,Hayakawa Shōkōsai I,"Japanese, 1815–1897",,Hayakawa Shōkōsai I,Japanese,1815,1897,second half of the 19th century,1850,1899,Bamboo (madake) with rattan accents,H. 19 3/4 in. (50.2 cm); Diam. 16 in. (40.6 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Basketry,,http://www.metmuseum.org/art/collection/search/62167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"55.94.1, .2",false,true,39665,Asian Art,Screen,"源氏物語図屏風「御幸」・「浮船」・「関谷」|Scenes from The Tale of Genji: “The Royal Outing,” “Ukifune,” and “The Gatehouse”",Japan,Momoyama period (1573–1615),,,,Artist,,Tosa Mitsuyoshi,"Japanese, 1539–1613",,TOSA MITSUYOSHI,Japanese,1539,1613,mid-16th–early 17th century,1550,1633,"Pair of four-panel folding screens; ink, color, and gold leaf on paper",Image (each screen): 65 1/2 in. × 11 ft. 8 in. (166.4 × 355.6 cm),"Fletcher Fund, 1955",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/39665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2006.42.1, .2",false,true,73167,Asian Art,Folding screens,,Japan,Momoyama period (1573–1615),,,,Artist,Attributed to,Kano Takanobu,"Japanese, 1571–1618",,Kano Takanobu,Japanese,1571,1618,ca. 1600,1590,1610,"Pair of six-panel folding screens; ink, color, and gold on gilded paper",Image (each screen): 66 7/16 x 149 1/2 in. (168.8 x 379.7 cm),"Purchase, Gift of Mrs. Russell Sage, Bequest of Stephen Whitney Phoenix, and other gifts, bequests and funds from various donors, by exchange, Joseph Pulitzer Bequest, and Rogers, Fletcher, Harris Brisbane Dick, and Louis V. Bell Funds, 2006",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/73167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.46, .47",false,true,45180,Asian Art,Screen,商山四皓・蘇東坡風水洞|The Return to Court of the Four Graybeards of Mount Shang (left); Su Shi’s Visit to the Wind and Water Cave (right,Japan,Momoyama period (1573–1615),,,,Artist,In the Style of,Kano Mitsunobu,"Japanese, ca. 1561–1608",,Kano Mitsunobu,Japanese,1561,1608,late 16th century,1571,1599,"Pair of six-panel folding screens; ink, color, gold, and gold leaf on paper",Image (each screen): 68 3/4 in. x 12 ft. 4 7/8 in. (174.6 x 378.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.736a–f,false,true,78638,Asian Art,Illustrated books,,Japan,Edo period (1615–1868) 1845.,,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1845,1845,1845,Set of six woodblock printed books; ink on paper,10 1/16 × 6 15/16 in. (25.6 × 17.7 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.268.44, .45",false,true,45258,Asian Art,Screen,四季竹図屏風|Bamboo in the Four Seasons,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Tosa Mitsunobu,1434–1525,,Tosa Mitsunobu,Japanese,1434,1525,late 15th–early 16th century,1480,1525,"Pair of six-panel screens; ink, color, and gold leaf on paper",Image: 61 13/16 x 9 ft. 9 3/4 in. (157 x 360 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"41.59.1, .2",false,true,42344,Asian Art,Painting,四季山水画 (瀟湘八景)|Landscape of the Four Seasons (Eight Views of the Xiao and Xiang Rivers),Japan,Muromachi period (1392–1573),,,,Artist,,Sōami,"Japanese, died 1525",,Sōami,Japanese,,1525,early 16th century,1500,1533,Pair of six-panel folding screens; ink on paper,Each: 68 1/4 × 146 in. (173.4 × 370.8 cm),"Gift of John D. Rockefeller Jr., 1941",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/42344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1992.8.1, .2",true,true,44696,Asian Art,Screen,猿猴捉月図屏風|Gibbons in a Landscape,Japan,Muromachi period (1392–1573),,,,Artist,,Sesson Shūkei,ca. 1504–ca. 1589,,Sesson Shūkei,Japanese,1504,1589,ca. 1570,1560,1580,Pair of six-panel screens; ink on paper,Image (each screen): 62 in. x 11 ft. 5 in. (157.5 x 348 cm),"Purchase, Rogers Fund and The Vincent Astor Foundation, Mary Livingston Griggs and Mary Griggs Burke Foundation, and Florence and Herbert Irving Gifts, 1992",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1991.480.1, .2",false,true,44673,Asian Art,Screen,琴棋書画図屏風|The Four Accomplishments,Japan,Muromachi period (1392–1573),,,,Artist,,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,Japanese,1476,1559,mid-16th century,1534,1566,Pair of six-panel folding screens; ink and color on paper,Image (each screen): 67 x 150 in. (170.2 x 381 cm),"Dr. and Mrs. Roger G. Gerry Collection, Gift of Dr. and Mrs. Roger G. Gerry, 1991",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.534,false,true,60467,Asian Art,Hanging scroll,墨蹟 「雪」|Poem on the Theme of Snow,Japan,Nanbokuchō period (1336–92),,,,Artist,,Musō Soseki,"Japanese, 1275–1351",,Musō Soseki,Japanese,1275,1351,14th century,1336,1392,Hanging scroll; ink on paper,Image: 11 3/4 x 32 1/2 in. (29.8 x 82.6 cm) Overall with mounting: 48 7/8 x 38 in. (124.1 x 96.5 cm) Overall with knobs: 48 7/8 x 40 1/4 in. (124.1 x 102.2 cm),"Gift of Sylvan Barnet and William Burto, in honor of Maxwell K. Hearn, 2011",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.12,false,true,60468,Asian Art,Hanging scroll,"夢窓疎石筆 消息|Letter to Suwa Daishin, Officer of the Shogun",Japan,Nanbokuchō period (1336–92),,,,Artist,,Musō Soseki,"Japanese, 1275–1351",,Musō Soseki,Japanese,1275,1351,ca. 1339–51,1329,1361,Hanging scroll; ink on paper,Image: 11 5/16 × 14 1/16 in. (28.7 × 35.7 cm) Overall with mounting: 44 1/2 × 18 1/2 in. (113 × 47 cm) Overall with knobs: 44 1/2 × 20 1/4 in. (113 × 51.4 cm),"Gift of Sylvan Barnet and William Burto, in honor of John T. Carpenter, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.6,false,true,42693,Asian Art,Hanging scroll,墨跡「糖」|Poem in Chinese about Sugar,Japan,Nanbokuchō period (1336–92),,,,Artist,,Kokan Shiren,"Japanese, 1278–1346",,Kokan Shiren,Japanese,1278,1346,14th century,1336,1392,Hanging scroll; ink on paper,Image: 12 1/4 x 18 5/8 in. (31.1 x 47.3 cm) Overall with mounting: 47 x 24 in. (119.4 x 61 cm) Overall with knobs: 47 x 25 13/16 in. (119.4 x 65.6 cm),"Gift of Sylvan Barnet and William Burto, in honor of Elizabeth and Neil Swinton, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/42693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.840,false,true,78730,Asian Art,Illustrated book,,Japan,Edo period (1615–1868) Kihei.,,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1768,1768,1768,Woodblock printed book; ink and color on paper,9 1/16 × 6 5/16 in. (23 × 16 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.765,false,true,78667,Asian Art,illustrated book,『暁斎漫画』|Kyōsai Sketchbook (Kyōsai manga),Japan,Edo period (1615–1868Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1881,1881,1881,Woodblock printed book; ink and color on paper,8 3/4 × 5 7/8 in. (22.3 × 15 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.766a, b",false,true,78668,Asian Art,illustrated books,『暁斎楽画』|Kyōsai’s Drawings for Pleasure (Kyōsai rakuga),Japan,Edo period (1615–1868Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1881,1881,1881,"Set of two woodblock-printed books (one volume orihon, accordion-style); ink and color on paper",each: 8 7/8 × 5 7/8 in. (22.5 × 15 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.36.1,false,true,44914,Asian Art,Screen,芥子図屏風|Red and White Poppies,Japan,Momoyama (1573–1615)– Edo (1615–1868) period,,,,Artist,Traditionally attributed to,Tosa Mitsumochi,active 1525–ca. 1559,,Tosa Mitsumochi,Japanese,1525,1559,early 17th century,1600,1633,"Six-panel folding screen; ink, color, and gold leaf on paper",65 3/4 x 147 1/2 in. (167.0 x 374.7 cm),"H. O. Havemeyer Collection, Gift of Mrs. Dunbar W. Bostwick, John C. Wilmerding, J. Watson Webb Jr., Harry H. Webb, and Samuel B. Webb, 1962",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"51.89.1, .2",false,true,44941,Asian Art,Screen,見立琴碁書画屏風|Parody of the Four Accomplishments,Japan,late Edo (1615–1868) or Meiji (1868–1912) period,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,second half of the 19th century,1850,1899,Pair of six-panel folding screens; ink and color on gold leaf on paper,Overall (each screen): 47 1/2 x 112 1/2 in. (120.7 x 285.8 cm),"Fletcher Fund, 1951",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/44941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.790,false,true,78692,Asian Art,Illustrated book,,Japan,Edo period (1615–1868) or Meiji period (1868–1912),,,,Artist,Attributed to,Rinsai Ōkubo,"Japanese, 19th century",,Rinsai Ōkubo,Japanese,1800,1899,1810 or 1870,1810,1870,Woodblock printed book; ink and color on paper,11 1/8 × 7 1/16 in. (28.3 × 18 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.684a, b",false,true,78605,Asian Art,Illustrated books,,Japan,Edo period (1615–1868) Osaka. 1837. Publishers: Eirakuya Tōshirō,,,,Artist,,Hanzan (Matsukawa),"Japanese, 1820–1882",,Hanzan,Japanese,1820,1882,1837,1837,1837,Set of two woodblock printed books; ink on paper,each: 8 11/16 × 6 1/8 in. (22 × 15.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +31.101.2,false,true,55007,Asian Art,DUPLICATE: this is JP207,,Japan,,,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769,1769,1769,Polychrome woodblock print; ink and color on paper,12 x 5 3/4 in. (30.5 x 14.6 cm),"Gift of Louis V. Ledoux, 1931",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2271,false,true,54044,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,probably 1815,1815,1815,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2272,false,true,54045,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,probably 1815,1815,1815,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/4 in. (20.2 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2273,false,true,54046,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,probably 1815,1815,1815,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 in. (20.2 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2274,false,true,54047,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,probably 1815,1815,1815,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2277,false,true,54050,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,probably 1815,1815,1815,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/16 in. (20.2 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2279,false,true,54052,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,probably 1815,1815,1815,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 7 1/8 in. (20.3 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2306,false,true,54090,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,probably 1814,1814,1814,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/8 x 7 in. (13 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3685,false,true,55916,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1797–1800,1797,1800,Polychrome woodblock print; ink and color on paper,16 1/4 x 11 1/8 in. (41.3 x 28.3 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3686,false,true,55917,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1854,1854,1854,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 5/8 x 10 in. (37.1 x 25.4 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP3683a, b",false,true,55914,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1858,1858,1858,Diptych of polychrome woodblock prints; ink and color on paper,Each 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3684a–c,false,true,55915,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1858,1858,1858,Triptych of polychrome woodblock prints; ink and color on paper,Each: 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2636,false,true,54203,Asian Art,Woodblock print,,Japan,,,,,Artist,,Watanabe Kazan,"Japanese, 1793–1841",,Watanabe Kazan,Japanese,1793,1841,ca. 1840,1830,1850,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 7 1/8 in. (19.7 x 18.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3170,false,true,56723,Asian Art,Print,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,ca. 1860,1850,1870,Polychrome woodblock print; ink and color on paper,Image: 8 1/2 × 11 5/8 in. (21.6 × 29.5 cm) Mat: 15 1/4 × 22 3/4 in. (38.7 × 57.8 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1955",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3171,false,true,56724,Asian Art,Print,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,ca. 1860,1850,1870,Polychrome woodblock print; ink and color on paper,Image: 7 1/8 × 9 1/2 in. (18.1 × 24.1 cm) Mat: 15 1/4 × 22 3/4 in. (38.7 × 57.8 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1955",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3608a–c,false,true,55913,Asian Art,Print,横浜 岩亀見込の図|The Interior of the Gankiro Tea House in Yokohama,Japan,,,,,Artist,,Suzuki Hiroshige II,"Japanese, 1826–1869",,Suzuki Hiroshige II,Japanese,1826,1869,1861 (April),1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Oban triptych: 14 /14 x 28 7/8 in. (35.6 x 73.3 cm),"The Howard Mansfield Collection, Rogers Fund, by exchange, 1982",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3308,false,true,55412,Asian Art,Print,,Japan,,,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,ca. 1863,1853,1873,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3315,false,true,55455,Asian Art,Print,浅草観音境内ニ於イテ興行仕候 佛蘭西曲馬|French Equestrian Circus on the grounds of Asakusa Kannon temple (Asakusa kannon keidai ni oite kōgyō tsukawashi sōrō-Furansu kyokuba),Japan,,,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,1871,1871,1871,Polychrome woodblock print; ink and color on paper,14 1/8 x 28 13/16 in. (35.9 x 73.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3316,false,true,55457,Asian Art,Print,"港崎横浜一覧|A Glance at Miyosaki, Yokohama",Japan,,,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,ca. 1860,1850,1870,Diptych of polychrome woodblock prints; ink and color on paper,Oban; 13 7/8 x 18 7/8 in. (35.2 x 47.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3200,false,true,55184,Asian Art,Print,,Japan,,,,,Artist,Attributed to,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,ca. 1880,1870,1890,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 3/8 in. (36.8 x 23.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3702,false,true,55950,Asian Art,Print,"Keinen kachō gafu 景年花鳥畫譜|Two Birds and Crysanthemums, from Keinen kachō gafu (Keinen’s Flower-and-Bird Painting Manual)",Japan,,,,,Artist,,Imao Keinen,"Japanese, 1845–1924",,Imao Keinen,Japanese,1845,1924,1891,1891,1891,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 1/16 in. (36.8 x 25.6 cm),"Bequest of Grace M. Pugh, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3703,false,true,55951,Asian Art,Print,"Keinen kachō gafu 景年花鳥畫譜|Cormorant and Kerria Rose (Yamabuki), from Keinen kachō gafu (Keinen’s Flower-and-Bird Painting Manual)",Japan,,,,,Artist,,Imao Keinen,"Japanese, 1845–1924",,Imao Keinen,Japanese,1845,1924,1891,1891,1891,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 1/16 in. (36.8 x 25.6 cm),"Bequest of Grace M. Pugh, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3203,false,true,55187,Asian Art,Print,,Japan,,,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,ca. 1903,1893,1913,Polychrome woodblock print; ink and color on paper,8 1/2 x 11 3/8 in. (21.6 x 28.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3205,false,true,55189,Asian Art,Print,Sakamoto Otasuku Kankyo ni tachite tekijo o nozomu zu|Sakamoto Otasuku,Japan,,,,,Artist,Attributed to,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,ca. 1894,1884,1904,Polychrome woodblock print; ink and color on paper,7 x 9 1/4 in. (17.8 x 23.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2353,false,true,54138,Asian Art,Print,"詩人と富士山『春雨集』 摺物帖|A Poet and Mount FujiFrom the Spring Rain Collection (Harusame shū), vol. 3",Japan,,,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820s,1820,1829,Part of an album of woodblock prints (surimono); ink and color on paper,4 7/8 x 11 1/8 in. (12.4 x 28.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2380,false,true,54164,Asian Art,Woodblock print,,Japan,,,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,probably 1817,1817,1817,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/16 x 7 1/8 in. (20.5 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3208,false,true,55192,Asian Art,Print,,Japan,,,,,Artist,,Shōsai Ikkei,"Japanese, active ca. 1870",,Shōsai Ikkei,Japanese,1870,1870,ca. 1875,1865,1885,Polychrome woodblock print; ink and color on paper,14 x 9 1/2 in. (35.6 x 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3227,false,true,55219,Asian Art,Print,,Japan,,,,,Artist,,Shōsai Ikkei,"Japanese, active ca. 1870",,Shōsai Ikkei,Japanese,1870,1870,ca. 1870,1860,1880,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 1/16 x 28 1/8 in. (35.7 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3228,false,true,55220,Asian Art,Print,,Japan,,,,,Artist,,Shōsai Ikkei,"Japanese, active ca. 1870",,Shōsai Ikkei,Japanese,1870,1870,ca. 1870,1860,1880,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 1/16 x 28 1/8 in. (35.7 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3229,false,true,55221,Asian Art,Print,,Japan,,,,,Artist,,Shōsai Ikkei,"Japanese, active ca. 1870",,Shōsai Ikkei,Japanese,1870,1870,ca. 1870,1860,1880,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 1/16 x 28 1/8 in. (35.7 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2351,false,true,54136,Asian Art,Woodblock print,"胴乱印籠と懐中時計根付『春雨集』 摺物帖|Dōran (Square Leather Box Used as an Inrō) with a Watch as a NetsukeFrom the Spring Rain Collection (Harusame shū), vol. 3",Japan,,,,,Artist,,Hokusen Taigaku,"Japanese, active 1805–1825",,Hokusen Taigaku,Japanese,1805,1825,probably 1817,1817,1817,Part of an album of woodblock prints (surimono); ink and color on paper,5 9/16 x 7 1/4 in. (14.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3383,false,true,55549,Asian Art,Print,"Shunshoku, Onkyoku no Shirabe|Spring Scenery; Melody of a Musical Performance",Japan,,,,,Artist,,Utagawa Fusatane,"Japanese, active ca. 1849–80",,Utagawa Fusatane,Japanese,1849,1880,1877? (ink stain renders date partly illegible),1877,1877,Triptych of polychrome woodblock prints; ink and color on paper,14 1/2 x 28 5/8 in. (36.8 x 72.7 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3380,false,true,55547,Asian Art,Print,亜米利加国|American Balloon Ascension (Amerikakoku),Japan,,,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"6th month, 1867",1867,1867,Polychrome woodblock print; ink and color on paper,14 x 28 1/2 in. (35.6 x 72.4 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3074,false,true,56554,Asian Art,Print,,Japan,,,,,Artist,Attributed to,Kondo Kiyoharu,"Japanese, active ca. 1704–1720",,Kondo Kiyoharu,Japanese,1704,1720,ca. 1715,1705,1725,Polychrome woodblock print (hand colored); ink and color on paper,12 1/4 x 5 3/4 in. (31.1 x 14.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2315,false,true,54099,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1816,1816,1816,Part of an album of woodblock prints (surimono); ink and color on paper,5 7/16 x 3 5/16 in. (13.8 x 8.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3312,false,true,55423,Asian Art,Print,Igirisujin|英吉利人|Englishmen,Japan,,,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,ca. 1862,1852,1872,Polychrome woodblock print; ink and color on paper,14 x 9 3/4 in. (35.6 x 24.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3313,false,true,55437,Asian Art,Print,亜墨利加人|Amerikajin,Japan,,,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1862,1862,1862,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3321,false,true,55465,Asian Art,Print,魯西亜|Russians Reading and Writing,Japan,,,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"1861 (Bunkyu 1st year, 2nd month)",1861,1861,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 1/8 in. (37.5 x 25.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3441,false,true,55648,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3442,false,true,55649,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3443,false,true,55650,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3444,false,true,55651,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3445,false,true,55652,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3446,false,true,55653,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3447,false,true,55654,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3448,false,true,55655,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3449,false,true,55656,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3450,false,true,55657,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3451,false,true,55658,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3452,false,true,55659,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3453,false,true,55660,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3454,false,true,55661,Asian Art,Print,,Japan,,,,,Artist,Formerly Attributed to,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3455,false,true,55662,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3456,false,true,55663,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3457,false,true,55664,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3458,false,true,55665,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3459,false,true,55666,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3460,false,true,55667,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3461,false,true,55668,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3462,false,true,55669,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3463,false,true,55670,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3464,false,true,55671,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3465,false,true,55672,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3466,false,true,55673,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3467,false,true,55674,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3468,false,true,55675,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3469,false,true,55676,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3470,false,true,55677,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3471,false,true,55678,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3472,false,true,55679,Asian Art,Print,"東海道五十三次 袋井|Fukuroi, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3473,false,true,55680,Asian Art,Print,"東海道五十三次 白須賀|Shirasuka, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3474,false,true,55681,Asian Art,Print,"東海道五十三次 二川|Futakawa, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3475,false,true,55682,Asian Art,Print,"東海道五十三次 吉田|Yoshida, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3476,false,true,55683,Asian Art,Print,"東海道五十三次 御油|Goyu, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3477,false,true,55684,Asian Art,Print,"東海道五十三次 赤坂|Akasaka, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3478,false,true,55685,Asian Art,Print,"東海道五十三次 藤川|Fujikawa, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3479,false,true,55686,Asian Art,Print,"東海道五十三次 岡崎|Okazaki, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3480,false,true,55687,Asian Art,Print,"東海道五十三次 池鯉鮒|Chiryūshuku, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3481,false,true,55688,Asian Art,Print,"東海道五十三次 鳴海|Narumi, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3482,false,true,55689,Asian Art,Print,"東海道五十三次 宮|Miya, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3483,false,true,55690,Asian Art,Print,"東海道五十三次 桑名|Kuwana, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3484,false,true,55691,Asian Art,Print,"東海道五十三次 四日市|Yokkaichi, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3485,false,true,55692,Asian Art,Print,"東海道五十三次 石薬師|Ishiyakushi, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3486,false,true,55693,Asian Art,Print,"東海道五十三次 庄野|Shōno, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3487,false,true,55694,Asian Art,Print,"東海道五十三次 亀山|Kameyama, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3488,false,true,55695,Asian Art,Print,"東海道五十三次 関|Seki, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3489,false,true,55696,Asian Art,Print,"東海道五十三次 阪之下|Sakanoshita, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3490,false,true,55697,Asian Art,Print,"東海道五十三次 土山|Tsuchiyama, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3491,false,true,55698,Asian Art,Print,"東海道五十三次 水口|Mizukuchi, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3492,false,true,55699,Asian Art,Print,"東海道五十三次 石部|Ishibe, from the series The Fifty-three Stations of the Tōkaidō Road",Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3440a–c,false,true,55647,Asian Art,Woodblock print,木曾路之山川|Mountains and Rivers Along the Kisokaidō,Japan,,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1930s,1930,1939,Triptych of polychrome woodblock prints; ink and color on paper,14 3/4 x 10 in. (37.5 x 25.4 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3364,false,true,55532,Asian Art,Print,,Japan,Edo (1615–1868),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1885",,Utagawa Yoshimori,Japanese,1830,1885,ca. 1865,1855,1875,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 5/16 in. (24.4 x 36.4 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP669,false,true,37116,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shigenaga,1697–1756,,Shigenaga,Japanese,1697,1756,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,27 7/32 x 6 15/32 in. (69.2 x 16.4 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP819,false,true,37263,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shigenaga,1697–1756,,Shigenaga,Japanese,1697,1756,ca. 1722,1712,1732,Polychrome woodblock print; ink and color on paper (Urushi-e),Overall: 11 3/4 x 6in. (29.8 x 15.2cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2657,false,true,56836,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shigenaga,1697–1756,,Shigenaga,Japanese,1697,1756,ca. 1738,1728,1748,Polychrome woodblock print; ink and color on paper,13 1/4 x 6 1/8 in. (33.7 x 15.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3102,false,true,45056,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudō Dohan,active 1710–16,,Kaigetsudō Dohan,Japanese,1710,1716,ca. 1714,1704,1724,Polychrome woodblock print (sumizuri-e); ink and color on paper,22 1/4 x 12 in. (56.5 x 30.5 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3103,false,true,45057,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudō Dohan,active 1710–16,,Kaigetsudō Dohan,Japanese,1710,1716,ca. 1714,1704,1724,Polychrome woodblock print (sumizuri-e); ink and color on paper,24 1/8 x 12 1/2 in. (61.3 x 31.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3104,false,true,45058,Asian Art,Print,短冊持立美人図|Courtesan with Poetry Card (Tanzaku) at New Year,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudō Dohan,active 1710–16,,Kaigetsudō Dohan,Japanese,1710,1716,ca. 1714,1704,1724,Polychrome woodblock print (sumizuri-e); ink and color on paper,23 1/2 x 12 1/2 in. (59.7 x 31.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1477,false,true,52008,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōkōsai Eishō,"Japanese, 1793–99",,Chōkōsai Eishō,Japanese,1793,1799,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,14 15/16 x 10 in. (37.9 x 25.4cm),"Fletcher Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/52008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1571,false,true,55748,Asian Art,Print,「扇屋昼見世畧」|Interior of the House called Ōgiya,Japan,Edo period (1615–1868),,,,Artist,,Chōkōsai Eishō,"Japanese, 1793–99",,Chōkōsai Eishō,Japanese,1793,1799,ca. 1800,1790,1810,Triptych of polychrome woodblock prints; ink and color on paper,Each H. 15 5/16 in. (38.9 cm); W. 9 5/8 in. (24.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2425a–c,false,true,56676,Asian Art,Woodblock print,「丁子屋畧見世」|The Chōjiya Pleasure House by Day (Chōjiya hiru-mise),Japan,Edo period (1615–1868),,,,Artist,,Chōkōsai Eishō,"Japanese, 1793–99",,Chōkōsai Eishō,Japanese,1793,1799,ca. 1798,1788,1808,Triptych of polychrome woodblock prints; ink and color on paper,Each H. 15 in. (38.1 cm); W. 9 3/4 in. (24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP229,false,true,36701,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,late 17th century,1667,1699,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP647,false,true,37097,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1675–80,1673,1682,Polychrome woodblock print; ink and color on paper (sumi-e (ink print),11 1/4 x 20 2/3 in. (28.6 x 52.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP676,false,true,37123,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1675–80,1665,1690,Polychrome woodblock print; ink and color on paper,9 x 13 1/4 in. (22.9 x 33.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP677,false,true,37124,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1690,1680,1700,Polychrome woodblock print; ink and color on paper,10 x 13 27/32 in. (25.4 x 35.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP807,false,true,37251,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1680,1670,1690,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/5 in. (25.7 x 38.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP808,false,true,37252,Asian Art,Print,"よしわらの躰 揚屋町入り口|The Entrance to Ageya-machi, from the series Scenes in the Yoshiwara (Yoshiwara no tei)",Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,1681–84,1681,1684,Woodblock print (sumizuri-e); ink on paper,10 1/8 x 15 1/5 in. (25.7 x 38.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP809,false,true,37253,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1680,1670,1690,Monochrome woodblock print; ink on paper,10 1/8 x 15 1/5 in. (25.7 x 38.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP810,false,true,37254,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1680,1670,1690,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/5 in. (25.7 x 38.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP811,false,true,37255,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1680,1670,1680,Monochrome woodblock print; ink and color on paper,10 1/8 x 15 1/5 in. (25.7 x 38.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP812,false,true,37256,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1680,1670,1690,Woodblock print; ink on paper,10 1/8 x 15 1/5 in. (25.7 x 38.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP813,false,true,37257,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1680,1670,1690,Polychrome woodblock print; ink and color on paper,11 1/4 x 17 31/32 in. (28.6 x 45.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP814,false,true,37258,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1685,1675,1695,Polychrome woodblock print; ink and color on paper,8 5/8 x 12 7/8 in. (21.9 x 32.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1590,false,true,45045,Asian Art,Print,Wakoku Hyakujo|Leaf from One Hundred Japanese Women,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1695,1685,1705,Monochrome woodblock print; ink on paper,H. 6 1/2 in. (16.5 cm); W. 6 3/4 in. (17.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1594,false,true,55758,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,late 17th century,1667,1699,Monochrome woodblock print; ink on paper,H. 10 1/16 in. (25.6 cm); W. 15 3/16 in. (38.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2643,false,true,56825,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,ca. 1685,1675,1695,Monochrome woodblock print (sumie); ink on paper,11 x 16 1/4 in. (27.9 x 41.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3069,false,true,56550,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,1680s,1680,1689,Woodblock print; ink on paper,9 1/4 × 13 1/4 in. (23.5 × 33.7 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3402,false,true,55584,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,late 17th century,1667,1694,Monochrome woodblock print; ink on paper,10 3/4 x 15 1/4 in. (27.3 x 38.7 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP123,false,true,36602,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,11 3/8 x 5 1/2 in. (28.9 x 14.0 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP386,false,true,36850,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,"2nd month, 1782",1782,1782,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 5/8 in. (30.2 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP388,false,true,36851,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,1779,1779,1779,Diptych of polychrome woodblock prints; ink and color on paper,a: H. 12 13/16 in (32.5 cm); W. 5 3/4 in. (14.6 cm) b: H. 12 3/4 in. (32.4 cm); W. 5 11/16 in. (14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1303,false,true,55242,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1402,false,true,55410,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 12 5/8 in. (32.1 cm); W. 6 1/4 in. (15.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55410,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1541,false,true,55706,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,H. 13 in. (33 cm); W. 5 3/4 in. (14.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2678,false,true,56862,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,13 x 5 1/2 in. (33 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2679,false,true,56863,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2905,false,true,56020,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunjō,"Japanese, died 1787",,Katsukawa Shunjō,Japanese,1700,1787,1700–1787,1700,1787,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/2 in. (30.8 x 14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP191,false,true,36668,Asian Art,Woodblock print,"『浅草観音奉掛額之 図』 「扇屋内滝川 富川、粂川、玉川、津川、歌川、清川 め浪、お浪」|A Votive Picture to Be Donated to the Kannon of Asakusa (Asakusa Kannon hō kakegaku no zu), by Takigawa of the Ōgiya, Kamuro Menami and Onami, with Tomikawa, Kumegawa, Tamagawa, Tsugawa, Utagawa, and Kiyokawa",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Kikumaro,"Japanese, died 1830",,Kitagawa Kikumaro,Japanese,,1830,ca. 1800,1790,1810,Triptych of polychrome woodblock prints; ink and color on paper,Overall: H. 14 3/4 in. (37.5 cm); W. 23 3/4 in. (60. 3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1091,false,true,55028,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Kikumaro,"Japanese, died 1830",,Kitagawa Kikumaro,Japanese,,1830,ca. 1815,1805,1825,Polychrome woodblock print; ink and color on paper,Aiban; H. 13 3/4 in. (34.9 cm); W. 9 1/16 in. (23 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1968,false,true,54552,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Kikumaro,"Japanese, died 1830",,Kitagawa Kikumaro,Japanese,,1830,probably 1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.135,false,true,76565,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunbaisai Hokuei,"Japanese, died 1837",,Shunbaisai Hokuei,Japanese,,1837,1832,1832,1832,Polychrome woodblock print,Image (ôban tate-e): 14 7/8 x 10 1/8 in. (37.8 x 25.7 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76565,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.138,false,true,76568,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunbaisai Hokuei,"Japanese, died 1837",,Shunbaisai Hokuei,Japanese,,1837,1835,1835,1835,Polychrome woodblock print,Image (ôban tate-e): 14 5/8 x 10 1/8 in. (37.1 x 25.7 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.139,false,true,76569,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunbaisai Hokuei,"Japanese, died 1837",,Shunbaisai Hokuei,Japanese,,1837,1837,1837,1837,Polychrome woodblock print,Image (ôban tate-e): 15 1/4 x 10 in. (38.7 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.137a–d,false,true,76567,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunbaisai Hokuei,"Japanese, died 1837",,Shunbaisai Hokuei,Japanese,,1837,1835,1835,1835,Tetraptych of polychrome woodblock prints,Each sheet (ôban tate-e tetraptych): 14 5/8 x 10 1/8 in. (37.1 x 25.7 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1000,false,true,54877,Asian Art,Print,"風流六玉川 紀伊 高野の玉川|The Kōya no Tamagawa, Province of Kii",Japan,Edo period (1615–1868),,,,Artist,,Utamaro II,Japanese (died 1831?),,Utamaro II,Japanese,1750,1850,ca. 1806,1796,1816,Polychrome woodblock print; ink and color on paper,H. 13 7/8 in. (35.2 cm); W. 8 7/8 in. (22.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1001,false,true,54878,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utamaro II,Japanese (died 1831?),,Utamaro II,Japanese,1750,1850,ca. 1806,1796,1816,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 9 5/8 in. (24.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1002,false,true,54879,Asian Art,Print,遊君出そめ初衣裳 扇屋内花扇|The Oiran Hanaogi of Ogiya attended by Two Shinzo and Her Kamuro Yoshino,Japan,Edo period (1615–1868),,,,Artist,,Utamaro II,Japanese (died 1831?),,Utamaro II,Japanese,1750,1850,ca. 1806,1796,1816,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 1/2 in. (24.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.341,false,true,54231,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utamaro II,Japanese (died 1831?),,Utamaro II,Japanese,1750,1850,ca. 1807,1797,1817,Polychrome woodblock print (surimono); ink and color on paper,15 3/4 x 21 5/8 in. (40 x 54.9 cm) (unfolded),"Gift of Joan B. Mirviss and Robert J. Levine, in memory of Carolyn D. Solomon, 1991",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP648,false,true,37098,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1701–06,1699,1708,Polychrome woodblock print; ink and color on paper,20 31/32 x 11 7/8 in. (53.3 x 30.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP649,false,true,37099,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1700–05,1695,1715,Polychrome woodblock print; ink and color on paper,22 x 12 3/4 in. (55.9 x 32.4 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP650,false,true,37100,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1725,1715,1735,Polychrome woodblock print; ink and color on paper,27 15/32 x 6 1/8 in. (69.8 x 15.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP680,false,true,37127,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1705,1695,1715,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 3/4 in. (30.2 x 14.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP681,false,true,37128,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1705,1695,1715,Polychrome woodblock print; ink and color on paper,11 3/5 x 5 31/32 in. (29.5 x 15.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP828,false,true,54482,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,1748,1748,1748,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP829,false,true,54483,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1744,1734,1754,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); 5 5/8 in. (14.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP830,false,true,54484,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1745,1735,1755,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 6 in. (15.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP831,false,true,54485,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1745,1735,1755,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 6 in. (15.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1448,false,true,55494,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1750,1740,1760,Polychrome woodblock print; ink and color on paper,H. 11 5/8 in. (29.5 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1464,false,true,37326,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1742,1732,1752,Polychrome woodblock print; ink and color on paper,H. 10 7/8 in. (27.6 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2623,false,true,56766,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1720–25,1720,1725,Polychrome woodblock print; ink and color on paper,13 1/4 x 6 1/4 in. (33.7 x 15.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3071,false,true,56552,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,1703,1703,1703,Polychrome woodblock print (hand-colored); ink and color on paper,10 3/4 x 14 1/2 in. (27.3 x 36.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3097,false,true,45060,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,1698,1688,1708,Polychrome woodblock print (sumizuri-e); ink and color on paper,23 1/4 x 12 1/2 in. (59.1 x 31.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3098,false,true,56610,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,ca. 1708,1698,1718,Polychrome woodblock print; ink and color on paper,21 3/4 x 11 1/2 in. (55.2 x 29.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP672,false,true,37119,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1731,1731,1731,Polychrome woodblock print; ink and color on paper,10 3/4 x 13 1/10 in. (27.3 x 33.3 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2717,false,true,56985,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,ca. 1730,1720,1740,Monochrome woodblock print; ink on paper,10 3/8 x 16 1/2 in. (26.4 x 41.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2755,false,true,57021,Asian Art,Book illustration,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,1671–1751,1671,1751,Monochrome woodblock print; ink on paper,14 1/2 x 10 1/4 in. (36.8 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP188,false,true,36658,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1738,1728,1748,"Polychrome woodblock print; ink, color, and hand-coloring on paper",11 1/4 x 16 1/2 in. (28.6 x 41.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP189,false,true,36666,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1748,1738,1758,Polychrome woodblock print; ink and color on paper,27 5/8 x 9 7/8 in. (70.2 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP539,false,true,36990,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1709,1699,1719,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP540,false,true,36991,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,probably 1709,1707,1711,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP541,false,true,36992,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,probably 1709,1707,1711,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP542,false,true,36993,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,1710–13,1710,1713,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP543,false,true,36994,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,probably 1709,1707,1711,Polychrome woodblock print; ink and color on paper,10 7/32 x 14 3/4 in. (26.0 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP544,false,true,36995,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,probably 1709,1707,1711,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP545,false,true,36996,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,probably 1709,1707,1711,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP546,false,true,36997,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,1710–13,1710,1713,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP547,false,true,36998,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1710,1690,1700,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP548,false,true,36999,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,probably 1709,1707,1711,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP549,false,true,37000,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,probably 1709,1707,1711,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP550,false,true,37001,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,1710–13,1710,1713,Polychrome woodblock print; ink and color on paper,10 3/8 x 13 27/32 in. (26.4 x 35.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP666,false,true,37113,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1763,1753,1773,Polychrome woodblock print; ink and color on paper,28 23/32 x 5 7/32 in. (73.0 x 13.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP678,false,true,37125,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1700,1690,1710,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 3/4 in. (26.4 x 37.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP679,false,true,37126,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1700–1703,1690,1713,Polychrome woodblock print; ink and color on paper,10 1/8 x 13 23/32 in. (25.7 x 34.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP682,false,true,37129,Asian Art,Print,"見立紫式部図|Parody of Murasaki Shikibu, Author of The Tale of Genji",Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,early 18th century,1700,1733,Monochrome woodblock print; ink on paper,10 3/8 x 14 1/8 in. (26.4 x 35.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP683,false,true,37130,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,early 18th century,1700,1733,Monochrome woodblock print; ink and color on paper,10 3/8 x 14 in. (26.4 x 35.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP684,false,true,37131,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1705–7,1695,1717,Polychrome woodblock print; ink and color on paper,10 x 14 15/32 in. (25.4 x 36.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP685,false,true,37132,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1705–7,1695,1717,Polychrome woodblock print; ink and color on paper,9 15/32 x 13 1/4 in. (24.1 x 33.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP686,false,true,37133,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1730,1720,1740,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 31/32 in. (31.1 x 15.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP687,false,true,37134,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1749,1739,1759,Polychrome woodblock print; ink and color on paper,4 1/4 x 6 7/8 in. (10.8 x 17.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP816,false,true,37260,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,early 18th century,1700,1735,Monochrome woodblock print; ink and color on paper,9 7/8 x 14 3/4 in. (25.1 x 37.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1308,false,true,37318,Asian Art,Lacquer print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,1740,1740,1740,Polychrome lacquer print (urushi-e),12 7/8 x 6 1/4 in. (32.7 x 15.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1499,false,true,55597,Asian Art,Print,"『閨の雛形』 正月|Plate from the Erotic Book Mounds of Dyed Colors: A Pattern Book for the Boudoir (Someiro no yama neya no hinagata), First Month",Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1736–44,1736,1744,Hand-colored woodblock illustration; ink and color on paper,H. 9 9/16 in. (24.3 cm); W. 14 1/4 in. (36.2 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2637,false,true,56780,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1715,1705,1725,Monochrome woodblock print (sumie); ink on paper,11 3/8 x 16 1/4 in. (28.9 x 41.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2638,false,true,56782,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,1750,1750,1750,Polychrome woodblock print; ink and color on paper,16 x 11 1/4 in. (40.6 x 28.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2639,false,true,56781,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1715,1705,1725,Monochrome woodblock print (sumie); ink on paper,10 x 14 in. (25.4 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2640,false,true,45050,Asian Art,Print,Sanpuku Tsui|Moon in Musashi Province,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1740,1730,1750,"Center sheet of a triptych of polychrome woodblock prints; ink and applied color (""tan-e"") on paper",12 1/2 x 6 1/8 in. (31.8 x 15.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2641,false,true,56821,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1744,1734,1754,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/2 in. (30.8 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3025,false,true,56423,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 5/8 in. (27.3 x 39.7 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3026,false,true,56424,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 in. (27.3 x 38.1 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3027,false,true,56425,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/4 in. (27.3 x 38.7 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3028,false,true,56426,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3029,false,true,56427,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3030,false,true,56428,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3031,false,true,56429,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3032,false,true,56490,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3033,false,true,56491,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3034,false,true,56492,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3035,false,true,56493,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 3/8 in. (27.3 x 39.1 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3036,false,true,56494,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1739,1729,1749,Polychrome woodblock print; ink and color on paper (hand colored),10 3/4 x 15 1/8 in. (27.3 x 38.4 cm),"Anonymous Gift, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3077,false,true,45239,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1730,1720,1740,Polychrome woodblock print (hand colored); ink and color on paper,12 3/4 x 6 1/4 in. (32.4 x 15.9 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3078,false,true,56588,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1755,1745,1765,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/2 in. (30.8 x 14 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3079,false,true,51993,Asian Art,Print,見立『平家物語』 紅葉焚図|Parody of Palace Servants Heating Sake over a Fire of Maple Leaves,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1750,1740,1760,Red-colored woodblock print (benizuri-e); ink and color on paper,Image: 16 5/16 × 11 3/4 in. (41.4 × 29.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3107,false,true,56613,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1745,1735,1755,Polychrome woodblock print; ink and color on paper,28 1/2 x 6 1/4 in. (72.4 x 15.9 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3169,false,true,56722,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,early 18th century,1700,1733,Polychrome woodblock print; ink and color on paper (Beni-e),H. 27 in. (68.6 cm); W. 10 in. (25.4 cm),"Bequest of Katherine S. Dreier, 1952",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1307,false,true,55265,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu II,"Japanese, 1706–1763",,Torii Kiyomasu II,Japanese,1706,1763,ca. 1748,1738,1758,Polychrome lacquer print (urushi-e),12 1/2 x 5 7/8 in. (31.8 x 14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1447,false,true,51088,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu II,"Japanese, 1706–1763",,Torii Kiyomasu II,Japanese,1706,1763,early 18th century,1700,1733,"Polychrome woodblock print; (beni-e); black, red and green on paper",12 1/2 x 6 1/8 in. (31.8 x 15.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2758,false,true,54861,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Torii Kiyomasu II,"Japanese, 1706–1763",,Torii Kiyomasu II,Japanese,1706,1763,ca. 1730–40,1720,1750,Tan-e (hand-colored print); ink and color on paper,H. 11 3/8 in. (28.9 cm); W. 6 in. (15.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3081,false,true,56594,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Torii Kiyomasu II,"Japanese, 1706–1763",,Torii Kiyomasu II,Japanese,1706,1763,before 1763,1663,1763,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP656,false,true,37106,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1750,1740,1760,Polychrome woodblock print; ink and color on paper,17 7/32 x 12 7/32 in. (43.8 x 31.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP691,false,true,37138,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1748,1738,1758,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP844,false,true,45240,Asian Art,Print,Go Gatsu|The Fifth Month,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1748,1738,1758,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 2 15/16 in. (7.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP845,false,true,45288,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1750,1740,1760,Polychrome woodblock print; ink and color on paper,H. 7 1/4 in. (18.4 cm); W. 11 3/8 in. (28.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP846,false,true,54499,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1750,1740,1760,Polychrome woodblock print; ink and color on paper,H. 7 1/4 in. (18.4 cm); W. 11 3/8 in. (28.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP847,false,true,54501,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1752,1742,1762,Polychrome woodblock print; ink and color on paper,H. 15 1/2 in. (39.4 cm); W. 11 3/16 in. (28.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP848,false,true,54505,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1758,1748,1768,Polychrome woodblock print; ink and color on paper,H. 11 1/2 in. (29.2 cm); W. 17 1/8 in. (43.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1616,false,true,55781,Asian Art,Print,初代瀬川菊之丞の傾城図|The Kabuki Actor Segawa Kikunojo in the Role of a Courtesan Reading a Letter,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,late 1740s,1745,1749,Polychrome woodblock print; ink and color on paper,H. 26 3/4 in. (67.9 cm); W. 9 5/8 in. (24.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2727,false,true,56999,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,probably January 1749,1749,1749,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 3/8 in. (29.2 x 13.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2728,false,true,57001,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,1748–1750,1748,1750,Polychrome woodblock print; ink and color on paper,16 3/4 x 12 in. (42.5 x 30.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2729,false,true,57002,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,1744,1744,1744,Polychrome woodblock print; ink and color on paper,15 x 11 1/4 in. (38.1 x 28.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2759,false,true,57023,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,1711–1785,1711,1785,Polychrome woodblock print; ink and color on paper,15 5/8 x 6 7/8 in. (39.7 x 17.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2760,false,true,57024,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,1711–1785,1711,1785,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 3/4 in. (30.8 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3085,false,true,56598,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1728,1718,1738,Polychrome woodblock print (hand-colored); ink and color on paper,10 1/2 x 15 in. (26.7 x 38.1 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3086,false,true,56599,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,1758,1758,1758,Polychrome woodblock print; ink and color on paper,15 7/8 x 6 7/8 in. (40.3 x 17.5 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3087,false,true,56600,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,1761,1761,1761,Polychrome woodblock print; ink and color on paper,15 3/8 x 6 1/2 in. (39.1 x 16.5 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3088,false,true,56601,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,1750 or 1751,1750,1751,Triptych of polychrome woodblock prints; ink and color on paper,12 x 17 1/2 in. (30.5 x 44.5 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3108,false,true,56614,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1745,1735,1755,Polychrome woodblock print (hand colored); ink and color on paper,25 1/2 x 6 in. (64.8 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3109,false,true,56615,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1748,1738,1758,Polychrome woodblock print (hand colored); ink and color on paper,19 3/4 x 9 in. (50.2 x 22.9 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3110,false,true,56617,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,ca. 1743,1733,1753,Polychrome woodblock print (hand colored); ink and color on paper,24 x 9 3/4 in. (61 x 24.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP655,false,true,37105,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Furuyama Moromasa,"Japanese, 1712–1772",,Furuyama Moromasa,Japanese,1712,1772,ca. 1740,1730,1750,Polychrome woodblock print; ink and color on paper,Oban: 12 31/32 x 18 1/2 in. (33.0 x 47.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1399,false,true,55407,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Toriyama Sekien,"Japanese, 1712–1788",,Toriyama Sekien,Japanese,1712,1788,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 9 7/16 in. (24 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP154,false,true,36633,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca.1766–70,1766,1770,Polychrome woodblock print; ink and color on paper,11 1/4 x 8 7/16 in. (28.6 x 21.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP155,false,true,36634,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,11 3/8 x 8 17/32 in. (28.9 x 21.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP156,false,true,36635,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1799,1797,1801,Polychrome woodblock print; ink and color on paper,11 1/4 x 8 1/2 in. (28.6 x 21.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP157,false,true,36636,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,25 1/2 x 4 3/4 in. (64.8 x 12.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP158,false,true,36637,Asian Art,Print,風俗六玉川|A Young Komuso,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,26 3/5 x 4 3/4 in. (67.6 x 12.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP226,false,true,45090,Asian Art,Print,"青楼美人合|The Courtesans, from the Series, ""Seiro Bijin Awase Carver End Shigoro"" (sic.)",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 8 1/2 in. (21.6 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP227,false,true,48892,Asian Art,Print,"青楼美人合|The Courtesans, from the series, ""Seiro Bijin Awase Carver End Shigoro"" (sic)",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,8 1/2 x 5 3/4 in. (21.6 x 14.6 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/48892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP565,false,true,37016,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1789,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 15/16 in. (27.3 x 20.2 cm) medium-size print (chu-ban),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP566,false,true,37017,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1769 or 1770,1769,1770,Polychrome woodblock print; ink and color on paper,10 1/2 x 5 5/8 in. (26.7 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP668,false,true,37115,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1763,1753,1773,Polychrome woodblock print; ink and color on paper,27 11/32 x 4 in. (69.5 x 10.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP670,false,true,37117,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,25 1/8 x 4 15/32 in. (63.8 x 11.4 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP671,false,true,37118,Asian Art,Print,風流七小町|Visiting,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,25 31/32 x 4 1/4 in. (66.0 x 10.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP697,false,true,37144,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1764,1754,1774,Polychrome woodblock print; ink and color on paper,7 3/4 x 12 1/8 in. (19.7 x 30.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP698,true,true,37145,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,"Polychrome woodblock print (first edition); ink and colors on paper, medium-sized print (chuban)",11 1/4 x 8 in. (28.6 x 20.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP699,false,true,37146,Asian Art,Print,風流七小町 しみず|Shimizu Temple,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper; Benizuri-e; small print (hosoban),12 7/32 x 5 1/2 in. (31.1 x 14.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP861,false,true,54540,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 1/8 in. (20.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP862,false,true,54541,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,H. 10 3/4 in. (27.3 cm); W. 8 1/8 in. (20.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP863,false,true,54542,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1765,1760,1770,Polychrome woodblock print; ink and color on paper,H. 11 1/8 in. (28.3 cm); W. 8 3/8 in. (21.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP864,false,true,54543,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1777,Polychrome woodblock print; ink and color on paper,H. 8 3/16 in. (20.8 cm); W. 2 3/4 in. (7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP865,false,true,54546,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1766,1761,1771,Polychrome woodblock print; ink and color on paper,H. 10 5/8 in. (27 cm); W. 8 1/8 in. (20.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP866,false,true,54550,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1777,Polychrome woodblock print; ink and color on paper,H. 11 3/16 in. (28.4 cm); W. 8 3/8 in. (21.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP867,false,true,54551,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,late 1760s,1765,1769,Polychrome woodblock print; ink and color on paper,H. 7 11/16 in. (19.5 cm); W. 9 9/16 in. (24.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP868,false,true,37268,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,10 1/4 x 7 1/4 in. (26 x 18.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP869,false,true,54554,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 11/16 in. (27.1 cm); W. 8 1/4 in. (21 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP870,false,true,45078,Asian Art,Print,百人一首 天智天皇|Sympathy,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 5/ 16 in. (18.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP871,false,true,45079,Asian Art,Print,"百人一首 小式の内持|Koshikibu no Naishi (999–1025), from ""Hyakunin Isshu"" (One Hundred Poems by One Hundred Poets)",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 1/8 in. (20.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP872,false,true,45075,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 in. (20.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP873,false,true,45069,Asian Art,Print,百人一首 藤原元真|Poem by Fujiwara no Motozane (ca. 860) from the Series Thirty-Six Poets,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper with embossing (karazuri),Image: 10 7/8 x 8 1/8 in. (27.6 x 20.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP875,false,true,51994,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1768,1768,1768,Polychrome woodblock print; ink and color on paper,H. 8 1/8 in. (20.6 cm); W. 11 1/4 in. (28.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP876,false,true,54569,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–72,1764,1772,Polychrome woodblock print; ink and color on paper,H. 11 1/8 in. (28.3 cm); W. 8 1/4 in. (21 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP877,false,true,54570,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 20 1/8 in. (51.5 cm); W. 4 1/2 in. (11.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP878,false,true,54571,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 25 7/8 in. (65.7 cm); W. 4 1/2 in. (11.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP879,false,true,54572,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,School of,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 11 1/4 in. (28.6 cm); W. 8 3/8 in. (21.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1010,false,true,54886,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 11 5/8 in. (29.5 cm); W. 8 3/8 in. (21.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1219,false,true,55142,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1770,1770,1770,Polychrome woodblock print; ink and color on paper,H. 11 1/8 in. (28.3 cm); W. 8 1/8 in. (20.6 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1220,false,true,55143,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1770,1770,1770,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 8 1/8 in. (20.6 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1223,false,true,55145,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1770,1770,1770,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 5/16 in. (21.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1273,false,true,55194,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,10 5/8 x 8 1/16 in. (27 x 20.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1274,false,true,45073,Asian Art,Print,Aki|風俗四季歌仙 立秋|First Day of Autumn (Risshu),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1865,1855,1875,Polychrome woodblock print; ink and color on paper,Image: 11 × 8 in. (27.9 × 20.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1275,false,true,55204,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1760,1750,1770,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 8 1/8 in. (20.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1276,false,true,45089,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; pillar print (hashira-e); ink and color on paper,H. 25 7/8 in. (65.7 cm); W. 4 7/8 in. (12.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1375,false,true,55356,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 27 1/2 in. (69.9 cm); W. 4 3/4 in. (12.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1376,false,true,55358,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1770,1760,1780,Polychrome woodblock print (pillar print); ink and color on paper,H. 26 3/4 in. (67.9 cm); W. 5 in. (12.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1384,false,true,55371,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 8 in. (20.3 cm); W. 10 1/4 in. (26 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1435,false,true,55468,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 27 5/8 in. (70.2 cm); W. 4 3/4 in. (12.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1504,false,true,53895,Asian Art,Print,回文歌 京 大阪 江戸|Palindromic Poems (Kaibunka): Kyo,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Right-hand sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/8 x 5 3/8 in. (31.4 x 13.7 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1505,false,true,55608,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 1/8 in. (20.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1625,false,true,54867,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 11 1/4 in. (28.6 cm); W. 8 1/8 in. (20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1627,false,true,45086,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 7 1/2 in. (19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1632,false,true,55796,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 1/2 in. (21.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1633,false,true,55797,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–72,1764,1772,Polychrome woodblock print; ink and color on paper,H. 10 3/4 in. (27.3 cm); W. 8 1/4 in. (21 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1634,false,true,45068,Asian Art,Print,Uzuki|風俗四季歌仙 卯月|The Fourth Month (April),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 3/16 in. (20.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1636,false,true,45083,Asian Art,Print,百人一首 僧正遍昭|Poem by Henjō Sojō,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,H. 10 9/16 in. (26.8 cm); W. 8 3/16 in. (20.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1637,false,true,45084,Asian Art,Print,百人一首 陽成院|Yozei no In,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,10 7/8 x 8 in. (27.6 x 20.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1638,false,true,45065,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1768–70,1700,1868,Polychrome woodblock print; ink and color on paper,10 5/16 x 7 9/1 6in. (26.2 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1640,false,true,54863,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–72,1764,1772,Polychrome woodblock print; ink and color on paper,H. 11 1/8 in. (28.3 cm); W. 8 1/8 in. (20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1643,false,true,55801,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,H. 10 11/16 in. (27.1 cm); W. 8 in. (20.3 cm) medium-size print (chu-ban),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1644,false,true,55802,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 11 1/4 in. (28.6 cm); W. 8 1/8 in. (20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1646,false,true,55804,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765–70,1755,1780,Polychrome woodblock print; chuban yoko-e; ink and color on paper,H. 8 in. (20.3 cm); W. 12 5/16 in. (31.3 cm) medium-size print (chu-ban),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1649,false,true,53608,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–70,1764,1770,Polychrome woodblock print; ink and color on paper,H. 10 3/4 in. (27.3 cm); W. 7 13/16 in. (19.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1654,false,true,55814,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–72,1764,1772,Polychrome woodblock print; ink and color on paper,H. 26 1/2 in. (67.3 cm); W. 4 7/16 in. (11.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1655,false,true,55815,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765–70,1755,1780,Polychrome woodblock print; pillar print (hashira-e); ink and color on paper,H. 27 in. (68.6 cm); W. 4 13/16 in. (12.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2434,false,true,42563,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 10 1/2 in. (26.7 cm) medium-size block (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/42563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2435,false,true,45268,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,10 3/8 x 7 1/2 in. (26.4 x 19.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2436,false,true,44943,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,"H. 7 15/16 in. (20.2 cm); W. 11 1/8 in. (28.3 cm) Medium-size block (""chuban"")","The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2437,false,true,54181,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1765,1765,1765,Polychrome woodblock print; ink and color on paper,10 1/2 x 7 7/8 in. (26.7 x 20 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2438,false,true,56874,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1765,1765,1765,Polychrome woodblock print; ink and color on paper,11 1/8 x 8 1/8 in. (28.3 x 20.6 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2439,false,true,56789,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 11 1/8 in. (28.3 cm); W. 8 3/8 in. (21.3 cm) medium-size print (chuban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2440,false,true,56791,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–72,1764,1772,Polychrome woodblock print; ink and color on paper,H. 10 3/8 in. (26.4 cm); W. 8 1/8 in. (20.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2441,false,true,45066,Asian Art,Print,百人一首 素性法師|Poem by the Monk Sosei (act. 850-97),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1767–68,1762,1775,"Polychrome woodblock print; ink and color on paper, with embossing (karazuri)",10 7/8 x 8 in. (27.6 x 20.3 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2442,false,true,51995,Asian Art,Print,萩|The Bush Clover (Hagi),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,10 7/8 x 7 7/8 in. (27.6 x 20 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2443,false,true,56875,Asian Art,Print,"井手の玉川|The Tama River at Ide, Yamashiro Province",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1766,1766,1766,Polychrome woodblock print; ink and color on paper,10 1/4 x 7 3/4 in. (26 x 19.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2444,false,true,56876,Asian Art,Print,"六玉川 「千鳥の玉川 陸奥名所」|“The Jewel River of Plovers, a Famous Place in Mutsu Province,” from the series Six Jewel Rivers (Mu Tamagawa: Chidori no Tamagawa, Mutsu meisho)",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1766,1766,1766,Polychrome woodblock print; ink and color on paper,10 7/8 x 8 in. (27.6 x 20.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2445,false,true,40991,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 7/8 in. (27.3 x 20 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/40991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2446,false,true,56877,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1762,1752,1772,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 7/8 in. (27.3 x 20 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2447,false,true,56792,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; chuban; ink and color on paper,H. 11 1/8 in. (28.3 cm); W. 7 1/16 in. (17.9 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2448,false,true,56878,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1770,1770,1770,Polychrome woodblock print; ink and color on paper,Hashirae: 25 x 5 in. (63.5 x 12.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2450,false,true,56880,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,11 1/4 x 8 in. (28.6 x 8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2451,false,true,56881,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 7/8 in. (27.3 x 20 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2452,false,true,56882,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,11 1/2 x 8 3/8 in. (29.2 x 21.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2453,false,true,45085,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–72,1764,1772,"Polychrome woodblock print; ink and color on paper, with embossing (karazuri)",H. 11 1/4 in. (28.6 cm); W. 8 1/8 in. (20.6 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2454,false,true,56883,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1770,1770,1770,Polychrome woodblock print; ink and color on paper,Hashire: 27 1/2 x 5 in. (69.9 x 12.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2455,false,true,56884,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,probably 1770,1770,1770,Polychrome woodblock print; ink and color on paper,Hashirae: 28 1/8 x 5 in. (71.4 x 12.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2767,false,true,45074,Asian Art,Print,未月|The Seventh Month (Fumizuki),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1865,1855,1875,Polychrome woodblock print; ink and color on paper,14 x 3 in. (35.6 x 7.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2768,false,true,45091,Asian Art,Print,青楼美人合|The Courtesan Kasugano Writing a Letter,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print (Yonkyokuban); ink and color on paper,8 3/8 x 5 3/4 in. (21.3 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2769,false,true,45092,Asian Art,Print,青楼美人合|The Courtesan Itsuhata with Her Pipe,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765,1755,1775,Polychrome woodblock print (Yonkyokuban); ink and color on paper,8 3/8 x 5 3/4 in. (21.3 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2771,false,true,45067,Asian Art,Print,Sakura-gari|桜狩|Cherry Blossom Viewing,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1764–70,1764,1770,Polychrome woodblock print; ink and color on paper,H. 11 3/8 in. (28.9 cm); W. 8 3/8 in. (21.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2772,false,true,45077,Asian Art,Print,百人一首 西行法師|Poem by the Monk Saigyō (1118-1190),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1767–68,1767,1768,Polychrome woodblock print; ink and color on paper,11 1/6 x 8 1/4 in. (28.4 x 21 cm) medium-size block (chu-ban),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2773,false,true,42564,Asian Art,Print,百人一首 中納言兼輔|Man and Woman Playing Shogi,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,mid-18th century,1734,1766,Polychrome woodblock print; ink and color on paper,11 1/8 x 8 1/8 in. (28.3 x 20.6 cm),"Henry J. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/42564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2774,false,true,57031,Asian Art,Print,Yuki|雪月花 雪|Snow,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,11 1/8 x 8 3/8 in. (28.3 x 21.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2775,false,true,57032,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1725–1770,1725,1770,Polychrome woodblock print; ink and color on paper,11 1/4 x 8 5/8 in. (28.6 x 21.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2776,false,true,45064,Asian Art,Print,風俗四季歌仙 立春|The First Day of Spring (Risshun),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1778,1798,Polychrome woodblock print; ink and color on paper,11 x 8 1/4 in. (27.9 x 21 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2777,false,true,57033,Asian Art,Woodblock print,遊女と新造|Courtesan and Shinzō,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1725–1770,1725,1770,Polychrome woodblock print; ink and color on paper,11 1/4 x 8 1/2 in. (28.6 x 21.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2778,false,true,57034,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768–69,1768,1769,Polychrome woodblock print; ink and color on paper,11 1/4 x 8 5/8 in. (28.6 x 21.9 cm) medium-size print (chu-ban),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2779,false,true,57035,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1725–1770,1725,1770,Polychrome woodblock print; ink and color on paper,11 1/8 x 8 1/2 in. (28.3 x 21.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2780,false,true,57036,Asian Art,Print,Sumidagawa no rakugan|風俗江戸八景 隅田川落雁|Wild Geese Flying Down the Sumida River,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,11 1/4 x 8 3/8 in. (28.6 x 21.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3019,false,true,56417,Asian Art,Print,井手の玉川|Jewel River at Ide (Ide no Tamagawa),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 7/16 in. (27.3 x 18.9 cm) medium-size print (chu-ban),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3021,false,true,56419,Asian Art,Woodblock print,Kōromō Uchi Tamagawa|Fulling Cloth at the Jewel River (Kinuta no Tamagawa),Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 7/8 in. (27.3 x 20 cm) medium-size print (chu-ban),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3022,false,true,56420,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,10 5/8 x 7 3/4 in. (27 x 19.7 cm) medium-size print (chu-ban),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3043,false,true,45041,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1760,1750,1770,Polychrome woodblock print; ink and color on paper,13 3/4 x 5 3/4 in. (34.9 x 14.6 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3052,false,true,56507,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1768,1758,1778,Polychrome woodblock print; chuban; ink and color on paper,10 x 15 1/2 in. (25.4 x 39.4 cm) medium-size print (chu-ban),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3406,false,true,55593,Asian Art,Print,"風流江戸八景 真乳山の暮雪|Evening Snow on Matsuchi Hilll, from the series Eight Fashionable Views of Edo (Furyu Edo hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,ca. 1765–70,1755,1780,Polychrome woodblock print; ink and color on paper,11 x 8 1/8 in. (27.9 x 20.6 cm) medium-size print (chu-ban),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP2770a, b",false,true,57030,Asian Art,Print,青楼美人合|Two Girls Play the Finger Game of Kitsume Ken,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,1725–1770,1725,1770,Diptych of polychrome woodblock prints; ink and color on paper,a Left sheet: 8 3/8 x 5 3/4 in. (21.3 x 14.6 cm); b Right sheet: 8 3/8 x 5 3/4 in. (21.3 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP111,false,true,36590,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1785,1785,1785,Polychrome woodblock print; ink and color on paper,14 1/3 x 9 7/16 in. (36.4 x 24.0 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP112,false,true,36591,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 5/8 in. (30.2 x 14.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP113,false,true,36592,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"9th month, 1774",1774,1774,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 2/3 in. (30.8 x 14.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP114,false,true,36593,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1782,1782,1782,Middle sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP115,false,true,36594,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,dated October or November 1778,1778,1778,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 5/8 in. (32.1 x 14.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP116,false,true,36595,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1773,1763,1783,Polychrome woodblock print; ink and color on paper,12 x 5 3/8 in. (30.5 x 13.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP117,false,true,36596,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 13/16 in. (31.3 x 14.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP118,false,true,36597,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773 or 1774,1773,1774,Polychrome woodblock print; ink and color on paper,11 3/8 x 5 1/5 in. (28.9 x 13.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP119,false,true,36598,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1782,1772,1792,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 3/4 in. (32.5 x 14.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP120,false,true,36599,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,12 x 5 5/8 in. (30.5 x 14.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP121,false,true,36600,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773 or 1774,1773,1774,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14.0 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP122,false,true,36601,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773 or 1774,1773,1774,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 3/4 in. (30.2 x 14.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP124,false,true,36603,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1781,1781,1781,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 31/32 in. (30.2 x 15.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP125,false,true,36604,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 1/2 in. (29.2 x 14.0 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP126,false,true,36605,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1785",1785,1785,Polychrome woodblock print; ink and color on paper,10 31/32 x 5 1/2 in. (27.9 x 14 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP127,false,true,36606,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 1/5 in. (29.2 x 13.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP207,false,true,36684,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,dated 1769,1769,1769,Polychrome woodblock print; ink and color on paper,Overall: 12 1/16 x 5 13/16 in. (30.6 x 14.8 cm),"Gift of Louis V. Ledoux, 1931",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP342,false,true,36807,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1778,1782,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 3/4 in. (31.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP343,false,true,36808,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"6th month, 1783",1783,1783,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 17/32 in. (31.4 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP344,false,true,36809,Asian Art,Woodblock print,五代目市川団十郎|Kabuki Actor Ichikawa Danjūrō V as Sakata Kintoki in the Play Raikō’s Four Intrepid Retainers in the Costume of the Night Watch (Shitennō tonoi no kisewata),Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"11th month, 1781",1781,1781,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 1/2 in. (31.3 x 14 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP345,false,true,36810,Asian Art,Woodblock print,五代目市川団十郎|Kabuki Actor Ichikawa Danjūrō V in a Shibaraku (Stop Right There!) Role,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"11th month, 1779",1779,1779,Polychrome woodblock print; ink and color on paper,Hosoban 12 1/2 x 5 7/8 in. (31.8 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP346,false,true,36811,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1785?,1783,1787,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 1/2 in. (31.4 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP347,false,true,36812,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1785",1785,1785,Polychrome woodblock print; ink and color on paper,Hosoban 12 7/8 x 5 3/4 in. (32.7 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP348,false,true,36813,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 9/16 x 5 17/32 in. (31.9 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP349,false,true,36814,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1776,1780,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 3/4 in. (31.4 x 14.6 cm),"Purchase, Joseph Pultizer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP350,false,true,36815,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1783–84,1781,1785,Polychrome woodblock print; ink and color on paper,12 7/16 x 5 7/8 in. (31.6 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP351,false,true,36816,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"10th month, 1777",1777,1777,Polychrome woodblock print; ink and color on paper,11 3/10 x 5 3/4 in. (28.7 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP352,false,true,48895,Asian Art,Print,"二代目市川八百藏・二代目中島三甫右衛門・三代目市川海老藏・九代目市村羽左衛門|Kabuki Actors Ichikawa Yaozō II, Nakajima Mihoemon II, Ichikawa Ebizō III, and Ichimura Uzaemon IX in the Play Sugawara’s Secrets of Calligraphy (Sugawara denju tenarai kagami)",Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"7th month, 1776",1776,1776,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Overall: 12 5/8 x 17 7/16 in. (32.1 x 44.3 cm); Image: 12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/48895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP353,false,true,36817,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1787,1777,1797,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 2/3 in. (30.2 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP354,false,true,36818,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1780",1780,1780,Left-hand sheet of a diptych of polychrome woodblock prints; ink and color on paper,12 1/2 x 5 7/8 in. (31.8 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP355,false,true,36819,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1780,1780,1780,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 7/8 in. (32.5 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP356,false,true,36820,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1787 or 1788,1787,1788,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 13/16 in. (32.5 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP357,false,true,36821,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,11 23/32 x 5 3/4 in. (29.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP358,false,true,36822,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779–83,1777,1785,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 13/16 in. (31.8 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP359,false,true,36823,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1778,1778,1778,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 9/16 x 5 7/8 in. (31.9 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP360,false,true,36824,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1776,1776,1776,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"Purchase, Joseph Pultizer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP361,false,true,36825,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,12 9/16 x 5 13/16 in. (31.9 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP362,false,true,36826,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP363,false,true,36827,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1782,1780,1784,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 2/3 in. (32.1 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP364,false,true,36828,Asian Art,Print,三代目大谷廣右衛門|The Third Otani Hiroemon as an Outlaw Standing Near a Willow Tree,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"3rd month, 1777",1777,1777,Polychrome woodblock print; ink and color on paper,11 15/16 x 5 5/8 in. (30.3 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP365,false,true,36829,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 2/3 in. (31.3 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP366,false,true,36830,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1783,1773,1793,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP367,false,true,36831,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1782,1772,1792,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 7/8 in. (32.5 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP368,false,true,36832,Asian Art,Woodblock print,五代目市川団十郎|Kabuki Actor Ichikawa Danjūrō V in a Shibaraku (Stop Right There!) Role as Hannya no Gorō,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,11th month 1776,1776,1776,Polychrome woodblock print; ink and color on paper,Hosoban 12 9/16 x 5 7/8 in. (31.9 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP369,false,true,36833,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP370,false,true,36834,Asian Art,Print,初代中村仲蔵|The First Nakamura Nakazō in the Role of Shimada no Hachizō,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1783,1783,1783,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 13/16 in. (32.7 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP371,false,true,36835,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1775,1773,1777,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 7/8 in. (32.7 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP372,false,true,36836,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1777,1767,1787,Polychrome woodblock print; ink and color on paper,12 1/32 x 5 3/4 in. (30.6 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP373,false,true,36837,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1786",1786,1786,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 1/2 in. (31.1 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP374,false,true,36838,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1781,1771,1791,Polychrome woodblock print; ink and color on paper,12 7/16 x 5 5/8 in. (31.6 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP375,false,true,36839,Asian Art,Print,二代目中村助五郎|Kabuki Actor Nakamura Sukegorō II as Kaminari Shōkurō,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1780",1780,1780,One sheet of a pentaptych; polychrome woodblock print; ink and color on paper,12 11/32 x 5 5/8 in. (31.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP376,false,true,36840,Asian Art,Print,三代目瀬川菊之丞|Kabuki Actor Segawa Kikunojō III in a Female Role (Shizuka Gozen),Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1777,1767,1787,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 17/32 in. (31.4 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP377,false,true,36841,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1777,1767,1787,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP378,false,true,36842,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP379,false,true,36843,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1784,1784,1784,Polychrome woodblock print; ink and color on paper,12 11/16 x 5 5/8 in. (32.2 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP380,false,true,36844,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1787–90,1785,1792,Polychrome woodblock print; ink and color on paper,12 x 5 5/8 in. (30.5 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP381,false,true,36845,Asian Art,Print,初代中村富十郎|Kabuki Actor Nakamura Tomijūrō I in a Female Dance Role,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1777,1767,1787,Polychrome woodblock print; ink and color on paper,Hosoban 11 7/8 x 5 17/32 in. (30.2 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP382,false,true,36846,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1773",1773,1773,Polychrome woodblock print; ink and color on paper,Overall: 13 x 5 3/4in. (33 x 14.6cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP383,false,true,36847,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1780,1780,1780,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 1/2 in. (31.4 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP384,false,true,36848,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1786–87,1784,1789,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP385,false,true,36849,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"summer, 1772",1772,1772,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 3/4 in. (30.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP392,false,true,36852,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 3/4 in. (31.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP395,false,true,36853,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1778,1776,1780,Diptych (probably two sheets of a triptych) of polychrome woodblock prints; ink and color on paper,A: H. 12 11/16 in. (32.2 cm); W. 5 11/16 in. (14.4 cm) B: H. 12 9/16 in. (31.9 cm); W. 5 3/4 in. (14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP399,false,true,36854,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774,1774,1774,Diptych of polychrome woodblock prints; ink and color on paper,A: H. 12 11/16 in. (32.2 cm); W. 5 5/8 in. (14.3 cm) B: H. 12 13/16 in (32.5 cm); W. 5 9/16 in. (14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP402,false,true,36855,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773,1773,1773,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 1/2 in. (31.4 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP403,false,true,36856,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773,1773,1773,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 13/16 in. (32.4 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP404,false,true,36857,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1775",1775,1775,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 1/2 in. (31.3 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP405,false,true,36858,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774 or 1775,1774,1775,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 3/8 in. (31.4 x 13.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP406,false,true,36859,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 13/16 in. (32.4 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP407,false,true,36860,Asian Art,Print,四代目市川団十郎|Kabuki Actor Ichikawa Ebizō III (Ichikawa Danjūrō IV),Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1774",1774,1774,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP408,false,true,36861,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1773,1771,1775,Polychrome woodblock print; ink and color on paper,11 23/32 x 5 5/8 in. (29.8 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP409,false,true,36862,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1774,1764,1784,Polychrome woodblock print; ink and color on paper,12 11/16 x 5 7/8 in. (32.2 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP410,false,true,36863,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773 or 1774,1773,1774,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 7/8 in. (32.5 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP411,false,true,36864,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774 or 1775,1774,1775,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP412,false,true,36865,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 13/16 in. (32.1 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP413,false,true,36866,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,One sheet of a diptych or triptych of polychrome woodblock prints; ink and color on paper,12 x 5 7/8 in. (30.5 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP414,false,true,36867,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773 ?,1771,1775,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 13/16 in. (31.3 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP415,false,true,36868,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 7/8 in. (30.8 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP416,false,true,36869,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 13/16 in. (31.8 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP417,false,true,36870,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773 or 1774,1773,1774,Polychrome woodblock print; ink and color on paper,12 11/16 x 5 13/16 in. (32.2 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP418,false,true,36871,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1771",1771,1771,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 15/16 in. (32.7 x 15.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP419,false,true,36872,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1774",1774,1774,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 1/2 in. (31.4 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP420,false,true,36873,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1772 or 1773,1772,1773,Polychrome woodblock print; ink and color on paper,11 11/16 x 5 2/3 in. (29.7 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP422,false,true,36875,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1773,1771,1775,Polychrome woodblock print; ink and color on paper,Hosoban; 12 5/16 x 5 9/16 in. (31.3 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP423,false,true,36876,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774 or 1775,1774,1775,Polychrome woodblock print; ink and color on paper,12 11/16 x 5 3/4 in. (32.2 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP424,false,true,36877,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1773 or 1774,1773,1774,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP425,false,true,36878,Asian Art,Print,鳥高斎栄昌画 「丁子屋畧見世」|The First Nakamura Tomijuro as an Oiran Standing in a Room,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1771,1771,1771,Polychrome woodblock print; ink and color on paper,12 1/5 x 5 13/16 in. (31.0 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP426,false,true,36879,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1774,1764,1784,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 7/8 in. (32.1 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP427,false,true,36880,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1774,1772,1776,Left-hand sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 11/32 x 5 13/16 in. (31.4 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP428,false,true,36881,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774 or 1775,1774,1775,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 11/32 x 5 13/16 in. (31.4 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP429,false,true,36882,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,11 7/8 x 5 5/8 in. (30.2 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP430,false,true,36883,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP431,false,true,36884,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1774,1772,1776,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 17/32 in. (31.4 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP432,false,true,36885,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1771",1771,1771,Polychrome woodblock print; ink and color on paper,12 x 5 5/8 in. (30.5 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP433,false,true,36886,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1775,1773,1777,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 13/16 in. (31.1 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP434,false,true,36887,Asian Art,Print,四代目市川団十郎|Kabuki Actor Ichikawa Danjūrō IV,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1771",1771,1771,Polychrome woodblock print; ink and color on paper,12 31/32 x 5 13/16 in. (33.0 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP435,false,true,36888,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1771",1771,1771,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 7/8 in. (32.5 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP436,false,true,36889,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1770",1770,1770,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 5/8 in. (32.1 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP437,false,true,36890,Asian Art,Woodblock print,九代目市村羽左衛門・三代目大谷廣次|Kabuki Actors Ichimura Uzaemon IX as Ko-kakeyama and Ōtani Hiroji III as Kōga Saburō,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,12 x 5 5/8 in. (30.5 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP438,false,true,36891,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 17/32 in. (31.4 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP439,false,true,36892,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1768",1768,1768,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP440,false,true,36893,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1768 or 1769,1768,1769,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 2/3 in. (32.4 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP441,false,true,36894,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1773,1763,1783,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 7/8 in. (32.1 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP442,false,true,36895,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1770,1770,1770,Polychrome woodblock print; ink and color on paper,12 9/16 x 5 7/8 in. (31.9 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP443,false,true,36896,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769 or 1770,1769,1770,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 13/16 in. (32.1 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP444,false,true,36897,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769 Autumn,1769,1769,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 17/32 in. (31.4 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP445,false,true,36898,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,probably 1770,1768,1772,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 1/2 in. (31.4 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP446,false,true,36899,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1769",1769,1769,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 7/16 in. (31.4 x 13.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP447,false,true,36900,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1770,1770,1770,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP448,false,true,36901,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1770 or 1771,1770,1771,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 13/16 in. (31.8 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP449,false,true,36902,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769 or 1770,1769,1770,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP450,false,true,36903,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769 or 1770,1769,1770,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP451,false,true,36904,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1770",1770,1770,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 5/8 in. (30.8 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP452,false,true,36905,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"summer, 1768",1768,1768,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 17/32 in. (30.8 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP453,false,true,36906,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769,1769,1769,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP454,false,true,36907,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1768 or 1769,1768,1769,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP455,false,true,36908,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1770",1770,1770,Polychrome woodblock print; ink and color on paper,12 7/16 x 5 7/32 in. (31.6 x 13.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP456,false,true,36909,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,12 7/16 x 5 1/2 in. (31.6 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP458,false,true,36911,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1768",1768,1768,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 1/2 in. (31.4 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP460,false,true,36913,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1768 or 1769,1768,1769,Polychrome woodblock print; ink and color on paper,12 7/16 x 5 1/2 in. (31.6 x 14 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP461,false,true,36914,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1768 or 1769,1768,1769,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 2/3 in. (31.4 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP462,false,true,36915,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1791,1791,1791,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP463,false,true,36916,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,11 3/5 x 5 3/4 in. (29.5 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP464,false,true,48894,Asian Art,Print,"二代目市川八百藏・二代目中島三甫右衛門・三代目市川海老藏・九代目市村羽左衛門|Kabuki Actors Ichikawa Yaozō II, Nakajima Mihoemon II, Ichikawa Ebizō III, and Ichimura Uzaemon IX in the Play Sugawara’s Secrets of Calligraphy (Sugawara denju tenarai kagami)",Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"7th month, 1776",1776,1776,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Overall: 12 5/8 x 17 7/16 in. (32.1 x 44.3 cm); Image: 12 11/16 x 5 15/16 in. (32.2 x 15.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/48894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP465,false,true,36917,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1759,1779,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 3/4 in. (30.2 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP551,false,true,37002,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769 spring,1769,1769,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP552,false,true,37003,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775 or 1776,1173,1778,Polychrome woodblock print; ink and color on paper,11 15/16 x 5 3/8 in. (30.3 x 13.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP553,false,true,37004,Asian Art,Print,"二代目市川八百藏・二代目中島三甫右衛門・三代目市川海老藏・九代目市村羽左衛門|Kabuki Actors Ichikawa Yaozō II, Nakajima Mihoemon II, Ichikawa Ebizō III, and Ichimura Uzaemon IX in the Play Sugawara’s Secrets of Calligraphy (Sugawara denju tenarai kagami)",Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"7th month, 1776",1776,1776,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Overall: 12 5/8 x 17 7/16 in. (32.1 x 44.3 cm); Image: 12 9/16 x 5 3/4 in. (31.9 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP703,false,true,37150,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP704,false,true,37152,Asian Art,Print,"「風流六く歌仙紀友則 十」|“Two Young Women on a Verandah Watching Plovers,” from the series Stylish Six Poetic Immortals (Fūryū rokkasen: Ki no Tomonori, jū)",Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,9 7/8 x 7 1/8 in. (25.1 x 18.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP705,false,true,37153,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1772,1772,1772,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 7/32 in. (29.2 x 13.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP706,false,true,37154,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1777,1767,1787,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP707,false,true,37155,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 31/32 x 5 7/8 in. (33.0 x 14.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP708,false,true,37156,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 7/32 x 17 1/5 in. (31.1 x 43.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP709,false,true,37157,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 3/4 x 8 27/32 in. (32.4 x 22.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP710,false,true,37158,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 7/32 x 8 1/10 in. (31.1 x 20.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP711,false,true,37159,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP893,false,true,54587,Asian Art,Print,"花形見風折烏帽子瀬川富三郎の娘道成寺|Segawa Tomisaburo in the Role of Musume Dojoji in ""Hanagatami Kazaori Eboshi""",Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,February 1774,1769,1779,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP894,false,true,54588,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1773,1763,1783,Polychrome woodblock print; ink and color on paper,H. 11 3/8 in. (28.9 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP895,false,true,54589,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1774,1764,1784,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP896,false,true,54590,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774,1774,1774,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP897,false,true,54591,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1776,1776,1776,Polychrome woodblock print; ink and color on paper,H. 8 1/2 in. (21.6 cm); W. 11 3/4 in. (29.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP898,false,true,54592,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1776,1766,1786,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 8 1/4 in. (21 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP899,false,true,54595,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1776,1766,1786,Polychrome woodblock print; ink and color on paper,H. 11 3/4 in. (29.8 cm); W. 8 1/4 in. (21 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP900,false,true,54596,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1777,1767,1787,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP901,false,true,54597,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 5/8 in. (14.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP902,false,true,54599,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP903,false,true,54600,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Diptych of polychrome woodblock prints; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 6 11/16 in. (17 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP904,false,true,54601,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP905,false,true,54602,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 1/2 in. (14 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP906,false,true,54647,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 6 9/16 in. (16.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1270,false,true,55167,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Diptych of polychrome woodblock prints; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 6 11/16 in. (17 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1298,false,true,55239,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1781,1771,1791,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1355,false,true,55331,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,H. 12 13/16 in. (32.5 cm); W. 5 9/16 in. (14.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1356,false,true,55333,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1768,1768,1768,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 5 11/16 in. (14.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1357,false,true,55334,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1769,1769,1769,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1358,false,true,55335,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774,1774,1774,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1359,false,true,55336,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1360,false,true,55337,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 11/16 in. (14.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1366,false,true,55317,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1776,1766,1786,Polychrome woodblock print; ink and color on paper,Uncut double hosoban 12 3/4 x 11 15/16 in. (32.4 x 30.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1445,false,true,55489,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1770,1770,1770,Polychrome woodblock print; ink and color on paper,H. 5 7/16 in. (13.8 cm); W. 6 3/16 in. (15.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1450,false,true,55497,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1781,1771,1791,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1451,false,true,55498,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1781,1771,1791,Polychrome woodblock print; ink and color on paper,H. 12 13/16 in. (32.5 cm); W. 5 11/16 in. (14.4 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1458,false,true,39719,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,14 3/8 x 19 1/4 in. (36.5 x 48.9 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1489,false,true,39720,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,17 7/8 x W. Top 12 13/16 in. (45.4 x 32.5 cm) 17 7/8 x W. Bot 12 7/8 in (45.4 x 32.7 cm),"Rogers Fund, 1927",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1532,false,true,45036,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 13/16 x 5 3/4 in. (32.5 x 14.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1533,false,true,55645,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 6 in. (15.2 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1534,false,true,55646,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1771–72,1771,1772,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 5 15/16 in. (15.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1535,false,true,55700,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1771–72,1771,1772,Polychrome woodblock print; ink and color on paper,H. 12 13/16 in. (32.5 cm); W. 5 13/16 in. (14.8 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1757,false,true,56063,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1771,1771,1771,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 5/8 in. (14.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2405,false,true,56808,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1770,1770,1770,Polychrome woodblock print; ink and color on paper,11 1/4 x 5 1/4 in. (28.6 x 13.3 cm),"Gift of Louis V. Ledoux, 1931",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2406,false,true,56809,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 3/8 x 6 in. (31.4 x 15.2 cm),"Gift of Louis V. Ledoux, 1931",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2680,false,true,56864,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1768 or 1769,1768,1769,Polychrome woodblock print; ink and color on paper,12 3/8 x 5 5/8 in. (31.4 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2682,false,true,56866,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1786,1776,1796,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2683,false,true,56867,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2684,false,true,56868,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1772",1772,1772,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 5/8 in. (30.8 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2685,false,true,56869,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1786 (?),1786,1786,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 3/4 in. (32.7 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2686,false,true,56870,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2687,false,true,56871,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2688,false,true,56872,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"12th month, 1780",1780,1780,Polychrome woodblock print; ink and color on paper,12 3/4 x 6 in. (32.4 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2689,false,true,56947,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2690,false,true,56948,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 7/8 in. (31.1 x 14.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2691,false,true,56952,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 3/4 in. (31.1 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2692,false,true,56953,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1782,1782,1782,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 1/2 in. (31.1 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2693,false,true,56954,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"5th month, 1781",1781,1781,Polychrome woodblock print; ink and color on paper,12 1/8 x 8 3/4 in. (30.8 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2695,false,true,56957,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2696,false,true,56959,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1774,1774,1774,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 5/8 in. (32.1 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2697,false,true,56960,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,11 3/4 x 5 1/2 in. (29.8 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2698,false,true,56962,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 5/8 in. (31.1 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2699,false,true,56963,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/8 in. (31.8 x 13.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2700,false,true,56964,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,12 3/8 x 5 5/8 in. (31.4 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2701,false,true,56966,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 x 5 3/4 in. (30.5 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2702,false,true,56968,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,12 x 5 in. (30.5 x 12.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2703,false,true,56969,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1772,1772,1772,Polychrome woodblock print; ink and color on paper,12 1/2 x 6 in. (31.8 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2704,false,true,44995,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 3/4 in. (30.8 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2705,false,true,56970,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1772 (?),1762,1782,Polychrome woodblock print; ink and color on paper,12 x 5 3/4 in. (30.5 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2706,false,true,56971,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",", New York, NY (1936; sold to MMA).",Katsukawa Shunshō,Japanese,1726,1792,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,13 x 6 in. (33 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2783,false,true,57037,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2784,false,true,57105,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,14 5/8 x 6 5/8 in. (37.1 x 16.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2886,false,true,56001,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 1/2 in. (29.2 x 14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2887,false,true,56002,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,11 3/4 x 5 1/2 in. (29.8 x 14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2888,false,true,56003,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2890,false,true,56005,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2891,false,true,56006,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 3/4 in. (30.2 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2892,false,true,56007,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 7/8 in. (30.8 x 14.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2893,false,true,56008,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1726–1792,1726,1792,Polychrome woodblock print; ink and color on paper,12 3/4 x 6 in. (32.4 x 15.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP703.5,false,true,37151,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1771,1771,1771,Polychrome woodblock print; ink and color on paper,12 7/8 x 6 1/8 in. (32.7 x 15.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP387a, b",false,true,36473,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Diptych of polychrome woodblock prints; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 11 3/8 in. (28.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP390a, b",false,true,36471,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1776,1776,1776,Diptych of polychrome woodblock prints; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 11 1/2 in. (29.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP391a, b",false,true,36470,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Diptych of polychrome woodblock prints; ink and color on paper,A. H. 12 5/16 in. (31.3 cm); W. 5 1/2 in. (14 cm) B. H. 12 1/16 in. (30.6 cm); W. 5 11/16 in. (14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP393a, b",false,true,36469,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,Hosoban diptych 12 1/4 x 11 1/4 in. (31.1 x 28.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP394a, b",false,true,36468,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1775,1765,1785,Diptych of polychrome woodblock prints; ink and color on paper,H. 12 5/16 in. (31.3 cm); W. 5 1/2 in. (14 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP400a, b",false,true,36464,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1778,1768,1788,Diptych of polychrome woodblock prints; ink and color on paper,A: H. 12 in. (30 cm); W. 5 5/8 in. (14.3 cm) B: H. 11 15/16 in. (30.3 cm); W. 5 5/8 in. (14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP401a, b",false,true,36463,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1773,1763,1783,Diptych of polychrome woodblock prints; ink and color on paper,Overall: H. 12 5/16 in. (31.3 cm); W. 11 in. (27.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP466a, b",false,true,36462,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1780,1780,1780,Diptych of polychrome woodblock prints; ink and color on paper,A: 12 5/8 x 5 3/4 in. (32.1 x 14.6 cm) B: 12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP2681a, b",false,true,56865,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,ca. 1780,1770,1790,Diptych of polychrome woodblock prints; ink and color on paper,Image (left): 10 7/8 × 5 1/2 in. (27.6 × 14 cm) Image (right): 10 7/8 × 5 3/4 in. (27.6 × 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP389a–c,false,true,36472,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1775,1778,1782,Triptych of polychrome woodblock prints; ink and color on paper,Overall: 12 5/8 x 5 1/2 in. (32.1 x 14 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP396a–e,false,true,36467,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"2nd month, 1780",1780,1780,Pentaptych of polychrome woodblock prints; ink and color on paper,Overall: H. 12 5/16 (31.3 cm); W. 27 1/2 in. (69.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918 (JP396a-d) Rogers Fund, 1922 (JP396e)",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP398a–d,false,true,36465,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1780,1780,1780,Tetraptych of polychrome woodblock prints; ink and color on paper,Overall: H. 13 in. (33 cm); W. 22 3/4 in. (57.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP459a–d,false,true,36912,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Designed by,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,1768,1768,1768,Polychrome woodblock prints; ink and color on paper,Four of a set of five hosoban 12 1/2 x 22 in. (31.8 x 55.9 cm) B.12 5/16 x 5 7/16 in.(32.4 x 14 cm) C.12 1/8 x 5 1/2 in. (30.8 x 14 cm) D.12 5/6 x 5 7/16 in. (32.4 x 14 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1342a–c,false,true,55307,Asian Art,Print,"三代目市川八百藏・初代目尾上松助・三代目澤村宗十郎|Kabuki Actors Ichikawa Yaozō III, Onoe Matsusuke I, and Sawamura Sōjūrō III",Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,"11th month, 1786",1786,1786,Triptych of polychrome woodblock prints; ink and color on paper,a: 12 5/8 in. (32.1 cm); W. 5 3/4 in. (14.6 cm) b: 12 5/8 in. (32.1 cm); W. 5 13/16 in. (14.8 cm) c: 12 11/16 in. (32.2 cm); W. 5 13/16 in. (14.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2889a–c,false,true,56004,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,third month of 1780,1780,1780,Triptych of polychrome woodblock prints; ink and color on paper,A: 12 7/8 x 5 5/8 in. (32.7 x 14.3 cm) B: 12 3/4 x 5 3/4 in. (32.4 x 14.6 cm) C: 12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP199,false,true,36676,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,probably 1767,1765,1769,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 7/8 in. (31.1 x 14.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP468,false,true,36919,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1760?,1750,1760,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 7/16 in. (31.1 x 13.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP469,false,true,36920,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,"2nd month, 1771",1771,1771,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 1/8 in. (31.1 x 13.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP667,false,true,37114,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1764,1754,1774,Polychrome woodblock print; ink and color on paper,27 11/32 x 4 in. (69.5 x 10.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP692,false,true,37139,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1755,1745,1765,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP693,false,true,37140,Asian Art,Print,「岩井半四郎図」|The Actor Iwai Hanshiro as a Courtesan Reading a Love Letter while Mounted on a Black Ox,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1763,1753,1773,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 1/2 in. (29.2 x 14.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP694,false,true,37141,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1763,1753,1773,Polychrome woodblock print; ink and color on paper,12 7/32 x 17 in. (31.1 x 43.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP695,false,true,37142,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1763,1753,1773,Polychrome woodblock print; ink and color on paper,12 1/8 x 16 1/2 in. (30.8 x 41.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP835,false,true,54490,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1757,1747,1767,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 5 1/2 in. (14 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP836,false,true,54491,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1761,1751,1771,Polychrome woodblock print; ink and color on paper,H. 11 7/8 in. (30.2 cm); W. 5 5/8 in. (14.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP837,false,true,54492,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1761,1751,1771,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP838,false,true,54493,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1762,1752,1772,Polychrome woodblock print; ink and color on paper,H. 23 1/2 in. (59.7 cm); W. 4 1/8 in. (10.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP839,false,true,54494,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,probably 1765,1760,1770,Polychrome woodblock print; ink and color on paper,H. 11 7/8 in. (30.2 cm); W. 5 1/2 in. (14 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1378,false,true,55362,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1760,1750,1770,Polychrome woodblock print; ink and color on paper,H. 11 15/16 in. (30.3 cm); W. 5 1/2 in. (14 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1385,false,true,55383,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1760,1750,1770,Polychrome woodblock print; ink and color on paper,H. 12 7/16 in. (31.6 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1500,false,true,55602,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 11 7/8 in. (30.2 cm); W. 5 3/8 in. (13.7 cm),"Rogers Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1501,false,true,55606,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1767,1767,1777,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 1/2 in. (14 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1502,false,true,55607,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1768,1758,1778,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 3/8 in. (13.7 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1507,false,true,51090,Asian Art,Print,Amagoi Komachi|Komachi Praying for Rain,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 11 5/16 in. (28.7 cm); W. 8 1/2 in. (21.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1568,false,true,55745,Asian Art,Print,「二代目瀬川菊之丞図」|The Kabuki Actor Segawa Kikunojō II as a Woman Reading a Letter,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1760s,1750,1770,Polychrome woodblock print; ink and color on paper,H. 28 1/8 in. (71.4 cm); W. 4 3/8 in. (11.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2588,false,true,56734,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,1757 or 1758,1757,1758,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2590,false,true,56736,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1759,1749,1769,Polychrome woodblock print; ink and color on paper,14 3/8 x 6 5/8 in. (36.5 x 16.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2591,false,true,56737,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1764,1754,1774,Polychrome woodblock print; ink and color on paper,21 3/8 x 4 1/8 in. (54.3 x 10.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2592,false,true,56738,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1763,1753,1773,Polychrome woodblock print; ink and color on paper,27 7/8 x 4 in. (70.8 x 10.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2763,false,true,57027,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,1735–1785,1735,1785,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/2 in. (30.8 x 14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2764,false,true,57028,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,1735–1785,1735,1785,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 3/8 in. (30.8 x 13.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2765,false,true,57029,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,1761,1761,1761,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 1/2 in. (30.2 x 14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3042,false,true,56498,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,ca. 1756,1746,1766,Polychrome woodblock print; ink and color on paper,12 x 5 3/4 in. (30.5 x 14.6 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3093,false,true,56606,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,1768,1768,1768,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 1/2 in. (31.1 x 14 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3094,false,true,56607,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,1756,1756,1756,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 5/8 in. (31.1 x 14.3 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3095,false,true,56608,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomitsu,"Japanese, 1735–1785",,Torii Kiyomitsu,Japanese,1735,1785,1766,1766,1766,Polychrome woodblock print; ink and color on paper,15 x 7 in. (38.1 x 17.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP192,false,true,36669,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,10 x 14 3/4 in. (25.4 x 37.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP224,false,true,36698,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,9 7/16 x 14 3/4 in. (24.0 x 37.5 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP675,false,true,37122,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1771,1761,1781,Polychrome woodblock print (pillar print); ink and color on paper,26 1/4 x 4 1/4 in. (66.7 x 10.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP740,false,true,37188,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,15 1/8 x 5 1/8 in. (38.4 x 13.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1417,false,true,55445,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1805,1795,1815,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 1/6 in. (36 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1420,false,true,55448,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1566,false,true,55744,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1790,1780,1800,Triptych of polychrome woodblock prints (surimono enriched with gold); ink and color on paper,Aiban; H. 13 7/8 in. (35.2 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1605,false,true,55769,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 8 3/4 in. (22.2 cm); W. 14 3/4 in. (37.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2855,false,true,45259,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,10 1/2 x 15 1/8 in. (26.7 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP194,false,true,36671,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1782,1772,1792,Polychrome woodblock print; ink and color on paper,14 11/16 x 10 in. (37.3 x 25.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP195,false,true,36672,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1782,1772,1792,Polychrome woodblock print; ink and color on paper,14 3/5 x 9 7/8 in. (37.1 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP662,false,true,37109,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,15 x 20 in. (38.1 x 50.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP696,false,true,37143,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1767,1757,1757,Polychrome woodblock print; ink and color on paper,11 1/8 x 5 1/2 in. (28.3 x 14.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP932,false,true,54802,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1759,1749,1769,Polychrome woodblock print; ink and color on paper,H. 11 5/8 in. (29.5 cm); W. 5 1/8 in. (13 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP933,false,true,54803,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,H. 9 7/16 in. (24 cm); W. 13 7/8 in. (35.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP934,false,true,45237,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 14 9/16 in. (37 cm); W. 9 13 /16 in. (24.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP936,false,true,54806,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 8 2/4 in. (21.6 cm); W. 12 3/8 in. (31.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1269,false,true,55166,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1764,1754,1774,Polychrome woodblock print; ink and color on paper,H. 11 1/8 in. (28.3 cm); W. 5 1/8 in. (13 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2123,false,true,54977,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1813,1813,1813,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 3 5/16 in. (21.1 x 8.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2126,false,true,54981,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1813,1813,1813,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 3 7/16 in. (20.8 x 8.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2338,false,true,54123,Asian Art,Print,判子と赤肉箱|Seals and a Carved Lacquer Container for Seal Ink,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1817,1817,1817,Part of an album of woodblock prints (surimono); ink and color on paper,5 9/16 x 7 5/16 in. (14.1 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2651,false,true,56829,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1766,1756,1776,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 1/2 in. (31.1 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2652,false,true,56831,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1769 (early),1769,1769,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/4 in. (30.8 x 13.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2653,false,true,45248,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,10 5/8 x 7 3/4 in. (27 x 19.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2654,false,true,56833,Asian Art,Woodblock print,Daibutsu no bansho|Vesper Bell of the Temple of Great Buddha,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,15 x 9 7/8 in. (38.1 x 25.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2655,false,true,56834,Asian Art,Woodblock print,Tesage andon|The Hand Lantern,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,14 5/8 x 9 3/4 in. (37.1 x 24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2656,false,true,56835,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,15 x 10 1/8 in. (38.1 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2794,false,true,57095,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,1739–1820,1739,1820,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 1/8 in. (37.5 x 25.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP159,false,true,36638,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP160,false,true,36639,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,probably 1777,1767,1787,Polychrome woodblock print; ink and color on paper,12 31/32 x 5 15/16 in. (33.0 x 15.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP161,false,true,36640,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 2/3 in. (31.8 x 14.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP162,false,true,36641,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1784–88,1784,1788,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 3/8 in. (31.1 x 13.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP163,false,true,36642,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,11 3/5 x 5 3/8 in. (29.5 x 13.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP164,false,true,36643,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 x 5 3/8 in. (30.5 x 13.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP165,false,true,36644,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,June 1786,1786,1786,Polychrome woodblock print; ink and color on paper,11 15/32 x 5 in. (29.1 x 12.7 cm) (trimmed),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP166,false,true,36645,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,April 1783,1783,1783,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 15/16 in. (32.5 x 15.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP167,false,true,36646,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,dated 1788,1788,1788,Polychrome woodblock print; ink and color on paper,12 x 5 3/4 in. (30.5 x 14.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP168,false,true,36647,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,"ca. 12th month, 1779",1769,1789,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 7/32 in. (29.2 x 13.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP169,false,true,36648,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,dated 1787,1787,1787,Middle sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP170,false,true,36649,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 1/2 in. (29.2 x 14 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP171,false,true,36650,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,December 1785,1785,1785,Polychrome woodblock print; ink and color on paper,14 9/16 x 9 11/16 in. (37.0 x 24.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP274,false,true,36746,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1784,1784,1784,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 3/4 in. (30.2 x 14.6 cm) (trimmed),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP275,false,true,36747,Asian Art,Print,六代目中山小十郎|Kabuki Actor Nakayama Kojūrō VI,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1786,1786,1786,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Beqeust, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP276,false,true,36748,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1784–88,1782,1790,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP277,false,true,36749,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,"2nd month, 1788",1788,1788,Polychrome woodblock print; ink and color on paper,12 1/32 x 5 3/8 in. (30.6 x 13.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP278,false,true,36750,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 2/3 in. (31.1 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP279,false,true,36751,Asian Art,Woodblock print,五代目市川団十郎|Scene from the Play Yoshitsune and the Thousand Cherry Trees (Yoshitsune senbon zakura),Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,"8th or 9th month, 1784",1784,1784,Polychrome woodblock print; ink and color on paper,12 7/16 x 5 3/4 in. (31.6 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP280,false,true,36752,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1788?,1778,1798,Polychrome woodblock print; ink and color on paper,11 13/16 x 5 5/8 in. (30.0 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP281,false,true,36753,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 1/2 in. (32.4 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP282,false,true,36754,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1787,1787,1787,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP283,false,true,36755,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1779?,1769,1789,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 7/8 in. (32.7 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP284,false,true,36756,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,Spring of 1785,1785,1785,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 7/8 in. (31.4 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP285,false,true,36757,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,11 3/5 x 5 5/8 in. (29.5 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP286,false,true,36758,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1780,1790,Polychrome woodblock print; ink and color on paper,Hosoban 12 3/8 x 5 15/16 in. (31.4 x 15.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP287,false,true,36759,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1785–90,1775,1800,Polychrome woodblock print; ink and color on paper,Overall: 12 1/4 x 5 9/16in. (31.1 x 14.1cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP288,false,true,36760,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1774,1764,1784,Polychrome woodblock print; ink and color on paper,12 x 5 3/4 in. (30.5 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP289,false,true,36761,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790?,1780,1800,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP290,false,true,36762,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,late 1777 or early in 1778,1777,1778,Polychrome woodblock print; ink and color on paper,11 23/32 x 5 3/4 in. (29.8 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP295,false,true,36763,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,11 23/32 x 5 1/2 in. (29.8 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP296,false,true,36764,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,"4th month, 1783",1783,1783,Polychrome woodblock print; ink and color on paper,12 11/16 x 5 3/4 in. (32.2 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP297,false,true,36765,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780–85,1770,1795,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 3/4 in. (32.7 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP298,false,true,36766,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1785?,1775,1795,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP299,false,true,36767,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 5/8 in. (32.1 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP341,false,true,36806,Asian Art,Print,二代目嵐龍藏・三代目瀬川菊之丞|Kabuki Actors Arashi Ryūzō II and Segawa Kikunojō III,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1788,1792,Polychrome woodblock print; ink and color on paper,11 3/8 x 5 5/16 in. (28.9 x 13.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP712,false,true,37160,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 3/4 in. (31.1 x 14.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP713,false,true,37161,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP714,false,true,37162,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1786,1776,1796,Polychrome woodblock print; ink and color on paper,14 1/3 x 10 in. (36.4 x 25.4 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP908,false,true,54648,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1776,1776,1776,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP909,false,true,54649,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 6 in. (15.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP910,false,true,54650,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1773,1763,1783,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP911,false,true,54651,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1777,1767,1787,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1304,false,true,55243,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,H. 11 15/16 (30.3 cm); W. 5 3/8 in. (13.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1343,false,true,55308,Asian Art,Print,二代目市川門之助|Kabuki Actor Ichikawa Monnosuke II as Shinozuka in a Shibaraku (Stop Right There!) Scene,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,"11th month, 1790",1790,1790,Polychrome woodblock print; ink and color on paper,Hosoban 12 7/8 x 5 3/4 in. (32.7 x 14.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1344,false,true,55309,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1786,1786,1786,Polychrome woodblock print; ink and color on paper,H. 12 5/8 in. (32.1 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1345,false,true,55311,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1795,1795,1795,Polychrome woodblock print; ink and color on paper,H. 12 13/16 in. (32.5 cm); W. 5 5/8 in. (14.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1346,false,true,55312,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 12 9/16 in. (31.9 cm); W. 5 13/16 in. (14.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1352,false,true,55328,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 13 in. (33 cm); W. 5 15/16 in. (15.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1364,false,true,55344,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 12 9/16 in. (31.9 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1479,false,true,45221,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1789,1779,1899,Polychrome woodblock print; ink and color on paper,H. 12 11/16 in. (32.2 cm); W. 8 3/4 in. (22.2 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1480,false,true,55566,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 8 5/8 in. (21.9 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1494,false,true,55589,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1795,1795,1795,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 5 5/8 in. (14.3 cm),"Fletcher Fund, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1542,false,true,55707,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 5 3/4 in. (14.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1543,false,true,55708,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 3/4 in. (14.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2666,false,true,56844,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 3/4 in. (31.1 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2667,false,true,56845,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,13 x 6 in. (33 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2668,false,true,56846,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,13 x 5 3/4 in. (33 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2669,false,true,56847,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2671,false,true,56849,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2672,false,true,56850,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 7/8 in. (30.2 x 14.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2673,false,true,56851,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1784,1784,1784,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2674,false,true,56852,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1786,1776,1796,Polychrome woodblock print; ink and color on paper,12 3/8 x 5 5/8 in. (31.4 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2675,false,true,56853,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 1/2 in. (31.1 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2830,false,true,57070,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1743–1812,1743,1812,Polychrome woodblock print; ink and color on paper,15 3/16 x 10 1/4 in. (38.6 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2907,false,true,56022,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1743–1812,1743,1812,Polychrome woodblock print; ink and color on paper,13 7/8 x 6 in. (35.2 x 15.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2908,false,true,56023,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1743–1812,1743,1812,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 1/2 in. (31.1 x 14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2909,false,true,56024,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,1743–1812,1743,1812,Polychrome woodblock print; ink and color on paper,12 3/4 x 8 5/8 in. (32.4 x 21.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP293a, b",false,true,36478,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1787,1777,1797,Diptych of polychrome woodblock prints; ink and color on paper,H. 11 7/8 in. (30.2 cm); W. 11 3/8 in. (28.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP294a, b",false,true,36477,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1789,1779,1799,Diptych of polychrome woodblock prints; ink and color on paper,H. 12 9/16 in. (31.9 cm); W. 11 11/16 in. (29.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP2670a, b",false,true,56848,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1780,1770,1790,Diptych of polychrome woodblock prints; ink and color on paper,L. 12 3/8 x 5 3/4 in. (31.4 x 14.6 cm) R. 12 1/4 x 5 7/8 in. (31.1 x 14.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP291a–c,false,true,36480,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1788,1778,1798,Triptych of polychrome woodblock prints; ink and color on paper,Mat measurements: H. 22 3/4 in. (57.8 cm); W. 27 1/2 in. (69.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP292a–c,false,true,36479,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,ca. 1785,1775,1795,Triptych of polychrome woodblock prints; ink and color on paper,H. 12 1/4 (31.1 cm); W. 17 3/16 in. (43.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP128,false,true,36607,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1783,1773,1793,Diptych of polychrome woodblock prints; ink and color on paper,15 x 20 3/8 in. (38.1 x 51.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP129,false,true,36608,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Polychrome woodblock print; ink and color on paper,14 11/16 x 10 1/8 in. (37.3 x 25.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP130,false,true,36609,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,9 3/5 x 7 1/5 in. (24.4 x 18.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP131,false,true,36610,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,10 x 7 1/8 in. (25.4 x 18.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP132,false,true,36611,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,25 31/32 x 4 15/32 in. (66.0 x 11.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP133,false,true,36612,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,26 7/8 x 4 3/4 in. (68.3 x 12.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP134,false,true,36613,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,26 3/5 x 4 3/5 in. (67.6 x 11.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP135,false,true,36614,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1788–90,1786,1792,Triptych of polychrome woodblock prints; ink and color on paper,14 3/4 x 10 in. (37.5 x 25.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP564,false,true,37015,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,14 3/5 x 9 15/32 in. (37.1 x 24.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP717,false,true,37165,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1763,1753,1775,Polychrome woodblock print; ink and color on paper,12 x 5 7/32 in. (30.5 x 13.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP718,false,true,37166,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1773,1773,1773,Polychrome woodblock print; ink and color on paper,11 3/8 x 5 3/8 in. (28.9 x 13.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP719,false,true,37167,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 17/32 in. (31.4 x 14.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP720,false,true,37168,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,10 x 7 11/32 in. (25.4 x 18.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP721,false,true,37169,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1782,1772,1792,Polychrome woodblock print; ink and color on paper,15 x 9 1/16 in. (38.1 x 23.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP722,false,true,37170,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1782,1772,1792,Right-hand sheet of a diptych of polychrome woodblock prints; ink and color on paper,14 15/32 x 9 3/4 in. (36.8 x 24.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP723,false,true,37171,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1783,1773,1793,Polychrome woodblock print; ink and color on paper,15 3/8 x 10 7/32 in. (39.1 x 26 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP724,false,true,37172,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1781–89,1781,1789,Polychrome woodblock print; ink and color on paper,Aiban; 13 1/4 x 9 7/8 in. (33.7 x 25.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP725,false,true,37173,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 1/8 in. (38.4 x 25.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP726,false,true,37174,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,14 15/32 x 10 1/8 in. (36.8 x 25.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP727,false,true,37175,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Polychrome woodblock print; ink and color on paper,12 31/32 x 5 3/4 in. (33.0 x 14.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP728,false,true,37176,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 1/8 in. (38.4 x 25.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP729,false,true,37177,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1780,1800,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,15 x 10 1/8 in. (38.1 x 25.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP730,false,true,37178,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1780,1800,Middle sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 1/3 x 10 in. (36.4 x 25.4 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP806,false,true,37250,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,9 3/4 x 7 1/2 in. (24.8 x 19.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP918,false,true,54658,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,H. 8 1/16 in. (20.5 cm); W. 8 1/16 in. (20.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP919,false,true,54659,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,H. 10 1/2 in. (26.7 cm); W. 7 3/4 in. (19.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP920,false,true,54660,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1781,1771,1791,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 1/2 in. (19.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP921,false,true,54661,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1781,1776,1786,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 7 1/4 in. (18.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP922,false,true,54662,Asian Art,Print,玉花子栄茂図|Gyoku-kashi Eimo Preparing Calligraphy Offerings,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1782,1772,1792,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 1/4 in. (26 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP923,false,true,54663,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 9 5/16 in. (23.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP924,false,true,54664,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1782,1772,1792,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 9/16in. (35.9 x 24.3cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP925,false,true,54665,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Polychrome woodblock print; ink and color on paper,H. 13 1/8 in. (33.3 cm); W. 10 1/8 in. (25.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP926,false,true,54666,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 9 5/8 in. (24.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP927,false,true,45241,Asian Art,Print,"Kodomo Mando, Kojimachi 1,2, 3-chome, Sanno Go-sairei|Childrens' Lantern Float, Kojimachi 1,2, 3-chome Block Association, Sanno Festival",Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1780,1771,1790,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 1/2 in. (19.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP928,false,true,54667,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 1/2 in. (24.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP929,false,true,54668,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,H. 10 3/8 in. (26.4 cm); W. 7 1/2 in. (19.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP930,false,true,54669,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 7 1/8 in. (18.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP931,false,true,54670,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1792,1782,1802,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 10 in. (25.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1211,false,true,55134,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 27 7/16 in. (69.7 cm); W. 4 7/8 in. (12.4 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1372,false,true,55352,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 9 3/4 in. (24.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1373,false,true,45038,Asian Art,Print,Shiokumi|The Dance of the Beach Maidens from the series Brocade of the East,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,Image: 15 3/16 x 9 13/16 in. (38.6 x 24.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1377,false,true,55360,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1780,1780,1780,Polychrome woodblock print; ink and color on paper,H. 27 3/8 in. (69.5 cm); W. 4 5/8 in. (11.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1463,false,true,55510,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,H. 10 3/8 in. (26.4 cm); W. 7 1/2 in. (19.1 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1509,false,true,45031,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 19 1/4 in. (48.9 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1510,false,true,55610,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 in. (25.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1511,false,true,55613,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,H. 15 5/16 in. (38.9 cm); W. 10 3/8 in. (26.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1512,false,true,51998,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790–91,1660,1900,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 9 15/16 in. (37.5 x 25.2 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1513,false,true,55615,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 1/2 in. (24.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1514,false,true,55616,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,H. 14 15/16 in. (37.9 cm); W. 9 3/4 in. (24.8 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1713,false,true,55966,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1782,1779,1789,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 in. (37.1 x 25.4cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1724,false,true,56035,Asian Art,Print,"「美南見十二候」 五月 |The Fifth Month, from the series Twelve Months in the Southern Pleasure District (Minami jūni kō)",Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1784,1784,1784,Left sheet of a diptych of polychrome woodblock prints; ink and color on paper,14 7/8 x 9 15/16in. (37.8 x 25.2cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1734,false,true,45256,Asian Art,Print,Okawabata yu-suzumi|Enjoying the Evening Cool on the Banks of the Sumida River,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Diptych of polychrome woodblock prints; ink and color on paper,A: H. 15 in. (38.1 cm); W. 10 in. (25.4 cm) B: H. 14 5/8 in. (37.1 cm); W. 9 15/16 in. (25.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2593,false,true,56739,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1787,1777,1797,Polychrome woodblock print; ink and color on paper,10 1/4 x 7 5/8 in. (26 x 19.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2594,false,true,56740,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1779,1769,1789,Polychrome woodblock print; ink and color on paper,10 1/2 x 7 3/4 in. (26.7 x 19.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2595,false,true,56741,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,10 5/16 x 7 3/4 in. (26.2 x 19.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2596,false,true,56742,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,10 1/4 x 7 3/8 in. (26 x 18.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2597,false,true,56743,Asian Art,Print,Shiba: Atago|Atago Hill at Shiba,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1783,1773,1793,Polychrome woodblock print; ink and color on paper,10 7/16 x 7 5/8 in. (26.5 x 19.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2598,false,true,56744,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,12 5/8 x 8 3/4 in. (32.1 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2599,false,true,56745,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1783,1773,1793,Polychrome woodblock print; ink and color on paper,12 3/4 x 8 3/4 in. (32.4 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2600,false,true,56746,Asian Art,Print,二階座敷に三人の女|Three Women on a Balcony,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1786,1776,1796,Upper sheet of a vertical diptych of polychrome woodblock prints; ink and color on paper,14 7/8 x 10 1/16 in. (37.8 x 25.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2602,false,true,56748,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,11 3/4 x 5 7/8 in. (29.8 x 14.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2603,false,true,56749,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1784,1784,1784,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/4 in. (38.7 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2604,false,true,45246,Asian Art,Print,"「風俗東之錦」 姫君と侍女四人|High-Ranking Samurai Girl with Four Attendants, from the series A Brocade of Eastern Manners (Fūzoku Azuma no nishiki)",Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2605,false,true,56750,Asian Art,Print,Kitchugi|Geisha of the Tachibana Street,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1786,1776,1796,Polychrome woodblock print; ink and color on paper,14 7/8 x 10 1/4 in. (37.8 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2607,false,true,56751,Asian Art,Print,"「風俗東之錦」  武家の息女と侍女と若党 |A Lady from a Samurai Household with Three Attendants, from the series A Brocade of Eastern Manners (Fūzoku Azuma no nishiki)",Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1820,1810,1830,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 3/8 in. (38.7 x 26.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2608,false,true,56752,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1782,1782,1782,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 in. (38.7 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2609,false,true,56753,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1784–85,1784,1785,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 1/4 in. (38.4 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2610,false,true,56754,Asian Art,Print,四代目松本幸四郎とその家庭|The Kabuki Actor Matsumoto Kōshirō IV,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1783,1773,1793,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/2 in. (38.7 x 26.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2611,false,true,56755,Asian Art,Woodblock print,三代目沢村宗十郎と遊女|The Kabuki Actor Sawamura Sōjūrō III and Courtesans,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1783–84,1783,1784,Polychrome woodblock print; ink and color on paper,14 7/8 x 10 in. (37.8 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2612,false,true,56756,Asian Art,Print,Nakasu no suzumi|Enjoying the Evening Cool at Nakasu,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1782–83,1782,1783,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 1/2 in. (37.5 x 26.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2613,false,true,56757,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1781,1771,1791,Polychrome woodblock print; ink and color on paper,15 x 10 1/8 in. (38.1 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2614,false,true,56758,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1786,1776,1796,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 1/8 in. (38.4 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2615,false,true,52003,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1790,1810,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/52003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2616,false,true,56760,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,15 x 10 1/8 in. (38.1 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2617,false,true,56761,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1784,1774,1794,Diptych of polychrome woodblock prints; ink and color on paper,Each sheet: 15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2618,false,true,56762,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,26 1/4 x 4 3/4 in. (66.7 x 12.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2797,false,true,57092,Asian Art,Print,Kitchugi|Dancers of Tachibana Street,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1742–1815,1742,1815,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 1/8 in. (38.4 x 25.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2798,false,true,57091,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1783,1783,1783,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2799,false,true,53904,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1783,1750,1868,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/8 in. (38.7 x 25.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2800,false,true,57090,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1784,1784,1784,Polychrome woodblock print; ink and color on paper,14 15/16 x 9 3/4 in. (37.9 x 24.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2801,false,true,57089,Asian Art,Print,Sako no Suzumi|Cooling Off at Nakazu,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1782,1782,1782,Left-hand sheet of a diptych of polychrome woodblock prints; ink and color on paper,14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2802,false,true,57088,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1785,1785,1785,Polychrome woodblock print; ink and color on paper,15 3/8 x 10 1/8 in. (39.1 x 25.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP1268a, b",false,true,55165,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1783,1773,1793,Diptych of polychrome woodblock prints; ink with color reprinted and revamped on paper,Oban diptych 15 1/2 x 20 in. (39.4 x 50.8 cm),"Gift of Frank L. Wright, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP2622a, b",false,true,56765,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1785,1795,Diptych of polychrome woodblock prints; ink and color on paper,Each sheet: 15 1/4 x 10 1/4 in. (38.7 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2619a–c,false,true,56763,Asian Art,Print,仲の町の牡丹|The Peony Show,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1790,1780,1810,Triptych of polychrome woodblock prints; ink and color on paper,Each sheet: 15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2620a–c,false,true,45257,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Triptych of polychrome woodblock prints; ink and color on paper,Each sheet: 15 1/4 x 10 1/4 in. (38.7 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2621a–c,false,true,56764,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,ca. 1785,1775,1795,Triptych of polychrome woodblock prints; ink and color on paper,Each sheet: 15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3393a–c,false,true,56670,Asian Art,Print,三俳優隅田川船遊び|Sumida River Holiday,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,1788–90,1788,1790,Triptych of polychrome woodblock prints; ink and color on paper,Triptych; each H. 14 1/2 in. (36.8 cm); W. 10 1/8 in. (25.7 cm),"Gift of Mr. and Mrs. Arthur J. Steel, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP178,false,true,36656,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,25 7/8 x 4 7/8 in. (65.7 x 12.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP179,false,true,36657,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,25 7/32 x 4 3/5 in. (64.1 x 11.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP180,false,true,36481,Asian Art,Print,新大橋橋下の涼み船|Pleasure Boats on the Sumida River beneath Shin-Ōhashi Bridge,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1792,1782,1802,Pentaptych of polychrome woodblock prints; ink and color on paper,15 3/8 x 49 7/8 in. (39.1 x 126.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP181,false,true,36659,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1790,1780,1800,Two sheets from a triptych of polychrome woodblock prints; ink and color on paper,15 1/8 x 20 1/2 in. (38.4 x 52.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP182,false,true,36660,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1794,1784,1804,Triptych of polychrome woodblock prints; ink and color on paper,15 7/32 x 29 15/32 in. (38.7 x 74.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP183,false,true,36661,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1793,1783,1803,Triptych of polychrome woodblock prints; ink and color on paper,14 15/32 x 29 1/4 in. (36.8 x 74.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP184,false,true,36662,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1794,1784,1804,Triptych of polychrome woodblock prints (trimmed); ink and color on paper,14 3/8 x 28 23/32 in. (36.5 x 73.0 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP185,false,true,36663,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1797,1787,1807,Triptych of polychrome woodblock prints; ink and color on paper,15 7/32 x 30 in. (38.7 x 76.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP944,false,true,54830,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 7 1/16 in. (17.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP945,false,true,54833,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 9 7/8 in. (25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP946,false,true,54834,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 7 3/16 in. (18.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP947,false,true,54837,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hosoda Eishi,"Japanese, 1756–1829",,Hosoda Eishi,Japanese,1756,1829,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,15 1/2 x 10 1/8 in. (39.4 x 25.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP948,false,true,54839,Asian Art,Print,和歌三神図|Honoring the Three Gods of Poetry: Women Composing Poems,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1792,1782,1802,Triptych of polychrome woodblock prints; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 30 in. (76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP949,false,true,54841,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); 9 7/8 in. (25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP950,false,true,54844,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1794,1789,1799,Polychrome woodblock print; ink and color on paper,H. 15 1/2 in. (39.4 cm); W. 10 1/4 in. (26 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP951,false,true,54845,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1795,1785,1805,Triptych of polychrome woodblock prints; ink and color on paper,H. 14 3/8 in. (36.5 cm); W. 30 in. (76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP952,false,true,54846,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1797,1787,1807,Triptych of polychrome woodblock prints; ink and color on paper,14 3/8 x 30 in. (36.5 x 76.2cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1124,false,true,55044,Asian Art,Print,"『青楼美人六花仙』「扇屋花扇」|“Hanaōgi of the Ōgiya,” from the series Beauties of the Yoshiwara as Six Floral Immortals (Seirō bijin rokkasen)",Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,H. 14 9/16 in. (37 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1442,false,true,55486,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 13/16 in. (24.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1524,false,true,53657,Asian Art,Print,"「青楼美撰合 初売座敷之図 扇屋 滝川」|Takigawa of the Ōgiya House, from the series A Comparison of Selected Beauties of the Pleasure Quarters (Seirō bisen awase)",Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 in. (25.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1569,false,true,55746,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1790,1780,1800,Triptych of polychrome woodblock prints; ink and color on paper,a: H. 14 7/8 in. (37.8 cm); W. 9 7/8 in. (25.1 cm); b: H. 14 7/8 in. (37.8 cm); W. 9 3/4 in. (24.8 cm); c: H. 15 in. (38.1 cm); W. 10 in. (25.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1786,false,true,45215,Asian Art,Print,"「畧六花撰」|Matching Shells (Kaiawase), from the series Six Immortal Poets in Modern Guise (Yatsushi Rokkasen)",Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1796–98,1786,1808,Polychrome woodblock print; ink and color on paper,14 15/16 x 9 3/4 in. (37.9 x 24.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2420,false,true,51091,Asian Art,Print,Amagoi|Ono no Komachi Praying for Rain,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1791,1781,1801,Monochrome woodblock print; ink on paper,H. 15 1/4 in. (38.7 cm); W. 10 1/4 in. (26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2421,false,true,56824,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1792,1782,1802,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 1/2 x 9 5/8 in. (36.8 x 24.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2422,false,true,56828,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/4 in. (38.7 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2423,false,true,52000,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1790,1780,1800,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,15 1/8 x 10 1/8 in. (38.4 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/52000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2424,false,true,56827,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/4 in. (38.7 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2815,false,true,53655,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,1756–1829,1756,1829,Polychrome woodblock print; ink and color on paper,14 5/8 x 9 5/8 in. (37.1 x 24.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2816,false,true,57079,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,13 7/8 x 9 5/8 in. (35.2 x 24.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2817,false,true,57078,Asian Art,Print,Seiryu Edo|Courtesan District of Edo,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,1756–1829,1756,1829,Polychrome woodblock print; ink and color on paper,15 x 10 1/8 in. (38.1 x 25.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2819,false,true,57076,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,1756–1829,1756,1829,Polychrome woodblock print; ink and color on paper,14 7/8 x 9 9/16 in. (37.8 x 24.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2820,false,true,57075,Asian Art,Print,Onono Komachi|Parrot Komachi,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,14 5/8 x 9 7/8 in. (37.1 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3132,false,true,56680,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1781–1800,1781,1800,Triptych of polychrome woodblock prints; ink and color on paper,Triptych; Overall: 14 1/4 x 30 1/8 in. (36.2 x 76.5 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1096c,false,true,639381,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1796 (Kansei 8),1796,1796,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 9 1/2 in. (37.5 × 24.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/639381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2818a–c,false,true,57077,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,ca. 1794,1784,1804,Triptych of polychrome woodblock prints; ink and color on paper,A. (left): 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm) B. (center): 14 5/8 x 10 in. (37.1 x 25.4 cm) C. (right): 14 5/8 x 10 in. (37.1 x 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP177,false,true,36655,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1790,1770,1790,Polychrome woodblock print; ink and color on paper,15 x 9 3/5 in. (38.1 x 24.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP731,false,true,37179,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,14 7/8 x 9 1/8 in. (37.8 x 23.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP958,false,true,54852,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1788,1778,1798,Diptych of polychrome woodblock prints; ink and color on paper,H. 15 in. (38.1 cm); W. 20 1/2 in. (52.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP959,false,true,54853,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 8 7/16 in. (21.4 cm); W. 14 7 /16 in. (36.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP960,false,true,54314,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 1/16 in. (20 x 17.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1106,false,true,54335,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 3/16 in. (20.2 x 18.3 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1254,false,true,54386,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,possibly,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 1/8 in. (20 x 18.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1907,false,true,54446,Asian Art,Print,"窪俊満画 煙草入れ袋と煙管『春雨集』 摺物帖|Pipe and Tobacco PouchFrom the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1810s,1810,1819,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 1/8 in. (14.1 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1912,false,true,54451,Asian Art,Print,"「松風台七番之内柄」|“Hilt of a Sword,” from the series of Seven Prints for the Shōfudai Poetry Circle",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1810s,1810,1819,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 5/16 in. (13.7 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1927,false,true,54467,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 5/8 in. (14 x 19.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1931,false,true,54471,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 5 1/2 in. (14.1 x 14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1932,false,true,54472,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 6 5/8 in. (14 x 16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1951,false,true,54519,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,probably 1820,1820,1820,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 in. (20 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1953,false,true,54521,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,probably 1810,1810,1810,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1954,false,true,54523,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1813,1813,1813,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 1/8 in. (20 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1971,false,true,54556,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2035,false,true,54801,Asian Art,Print,"『春雨集』 摺物帖窪俊満画 『鎌倉志』 「影向石」|Spring Rain Collection (Harusame shū), vol. 1: “Offering Incense to the Deity of the Stone” (Yōgōishi), from the series History of Kamakura (Kamakura shi)",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,early to mid-1810s,1810,1816,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,7 7/8 x 7 1/8 in. (20 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2038,false,true,54809,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,early to mid-1810s,1810,1816,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,7 11/16 x 7 3/16 in. (19.5 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2050,false,true,54823,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 11 3/16 in. (21.1 x 28.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2052,false,true,54826,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 3/16 in. (13.8 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2113,false,true,54965,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1808,1808,1808,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 1/2 in. (14 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2139,false,true,54994,Asian Art,Print,"『浅草側いせ暦』 節分の悪霊ばらい『春雨集』 摺物帖 |“Beans for Tossing During Setsubun Exorcism Ceremony,” from the series Ise Calendars for the Asakusa Group (Asakusa-gawa Ise goyomi)From the Spring Rain Collection (Harusame shū), vol. 2",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,1810s,1810,1819,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2140,false,true,54995,Asian Art,Print,"『浅草側いせ暦』 文房具と梅熨斗『春雨集』 摺物帖 |“Desk with Writing Set and Plum Flowers,” from the series Ise Calendars for the Asakusa Group (Asakusa-gawa Ise goyomi)From the Spring Rain Collection (Harusame shū), vol. 2",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,probably 1814 (Year of the Dog),1814,1814,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2141,false,true,54996,Asian Art,Print,"『名物革仝印籠仝根付』 人形手金唐革 菖蒲革『春雨集』 摺物帖|“Gold-decorated Leather with Figure of a Chinese Boy” and “Patterned Leather,” from the series Famous Leathers, Inrō, and Netsuke Meibutsu kawa, inrō, netsuke)From the Spring Rain Collection (Harusame shū), vol. 2",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1810s,1810,1819,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 1/8 in. (14 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2142,false,true,54997,Asian Art,Print,"『名物革仝印籠仝根付』 印籠根付『春雨集』 摺物帖|“Inrō and Netsuke,” from the series Famous Leathers, Inrō, and Netsuke (Meibutsu kawa, inrō, netsuke)From the Spring Rain Collection (Harusame shū), vol. 2",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1810s,1810,1819,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 1/4 in. (13.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2143,false,true,54998,Asian Art,Print,"『浅草側いせ暦』 弓道具『春雨集』 摺物帖|“Bow, Arrows, Target, and Other Outfits for Archery,” from the series Ise Calendars for the Asakusa Group (Asakusa-gawa Ise goyomi)From the Spring Rain Collection (Harusame shū), vol. 2",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,ca. 1814,1804,1824,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2152,false,true,55056,Asian Art,Print,"『春雨集』 摺物帖窪俊満画 胡蝶舞の衣装|Spring Rain Collection (Harusame shū), vol. 2: Costume for the Butterfly Dance (Kochō no mai)",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1810s,1810,1819,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2155,false,true,55062,Asian Art,Print,"『春雨集』 摺物帖窪俊満画 クサボタン、萩、シャ ガ、ツバキ、モチツバキ|Spring Rain Collection (Harusame shū), vol. 2: Cut Flowers: Clematis, Bush Clover, Iris, Camellia, and Azalea",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1815 (Year of the Ox),1815,1815,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 5/16 x 11 1/16 in. (21.1 x 28.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2179,false,true,55093,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 5 3/8 in. (21 x 13.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2197,false,true,55114,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1813,1813,1813,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 10 13/16 in. (20 x 27.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2214,false,true,53980,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,8 7/16 x 11 1/4 in. (21.4 x 28.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2291,false,true,54064,Asian Art,Print,"石橋物|Two Dancers Performing a “Shakkyōmono” Kabuki Dance, from Spring Rain Surimono Album (Harusame surimono-jō), vol. 3",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1805–10,1795,1820,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,8 5/16 x 5 1/2 in. (21.1 x 14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2292,false,true,54065,Asian Art,Print,"『鳥合』 桜草に雲雀|Skylarks and Primroses,” from the Series An Array of Birds (Tori awase), from Spring Rain Surimono Album (Harusame surimono-jō, vol. 3)",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1805–10,1800,1820,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,8 1/4 x 5 3/8 in. (21 x 13.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2293,false,true,54066,Asian Art,Woodblock print,"『鳥合』 桃花に目白|Japanese White-eyes on a Branch of Peach Tree,” from the Series An Array of Birds (Tori awase), from Spring Rain Surimono Album (Harusame surimono-jō, vol. 3)",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1805–10,1800,1820,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,8 3/16 x 5 3/8 in. (20.8 x 13.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2294,false,true,54067,Asian Art,Woodblock print,"梅と柳に目白|Japanese White-eyes with Plum Tree and Willow, from Spring Rain Surimono Album (Harusame surimono-jō, vol. 3)",Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,ca. 1810,1800,1820,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2318,false,true,54102,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,"1816, year of the rat",1816,1816,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 8 1/4 in. (14 x 21 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2340,false,true,54125,Asian Art,Print,舞楽|Courtier Playing a Flute to Accompany a Bugaku Dance,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,probably 1810,1810,1810,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 8 3/16 in. (14 x 20.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.1,false,true,77872,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1789,1789,1789,Polychrome woodblock print (surimono); ink and color on paper,Image: 4 1/8 x 6 1/2 in. (10.5 x 16.5 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.2,false,true,77873,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1793,1793,1793,Polychrome woodblock print (surimono); ink and color on paper,Image: 6 5/8 x 10 in. (16.8 x 25.4 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.3,false,true,77874,Asian Art,Print,書初め図|Young Woman Writing Calligraphy,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1793 (Year of the Goat),1793,1793,Polychrome woodblock print (surimono); ink and color on paper,Image: 4 1/4 x 6 5/8 in. (10.8 x 16.8 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.4,false,true,77875,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,"1794, year of the tiger",1794,1794,Polychrome woodblock print (surimono); ink and color on paper,Image: 5 3/8 x 6 1/2 in. (13.7 x 16.5 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.5,false,true,77876,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,"1795, year of the rabbit",1795,1795,Polychrome woodblock print (surimono); ink and color on paper,Image: 5 3/8 x 11 in. (13.7 x 27.9 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.6,false,true,77877,Asian Art,Print,女官図|Court Woman at her Desk with Poem Cards,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1795 (Year of the Rabbit),1795,1795,Polychrome woodblock print (surimono); ink and color on paper,Image: 5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.9,false,true,77880,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,"1795, year of the rabbit",1795,1795,Polychrome woodblock print (surimono); ink and color on paper,Image: 4 x 5 3/4 in. (10.2 x 14.6 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP2821a, b",false,true,57074,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1757–1820,1757,1820,Polychrome woodblock print; ink and color on paper,A. 15 5/16 x 10 6/16 in. (38.9 x 26.4 cm); B. 15 1/2 x 10 5/16 in. (38.9 x 26.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.10,false,true,77881,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,"1795, year of the rabbit",1795,1795,Polychrome woodblock print (surimono); ink and color on paper,Image: 4 1/8 x 7 1/8 in. (10.5 x 18.1 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.248.32,false,true,77903,Asian Art,Print,遊女と詩人|Courtesan with Client before a Tokonoma Alcove,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,1798,1798,1798,Polychrome woodblock print (surimono); ink and color on paper,Image: 5 1/2 x 11 in. (14 x 27.9 cm),"Purchase, Marjorie H. Holden Gift, 2012",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/77903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP937,false,true,54808,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Masanobu (Santō Kyōden),"Japanese, 1761–1816",,Kitao Masanobu (Santō Kyōden),Japanese,1761,1816,ca. 1783,1773,1793,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 7 1/4 in. (18.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2796,false,true,57093,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Masanobu (Santō Kyōden),"Japanese, 1761–1816",,Kitao Masanobu (Santō Kyōden),Japanese,1761,1816,1761–1816,1761,1816,Polychrome woodblock print; ink and color on paper,10 3/8 x 8 3/4 in. (26.4 x 22.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP174,false,true,36652,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1796,1786,1806,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 3/8 in. (31.4 x 13.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP301,false,true,36769,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1792?,1782,1802,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 5/8 in. (32.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP302,false,true,36770,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1787,1777,1797,Polychrome woodblock print; ink and color on paper,11 15/16 x 5 5/16 in. (30.3 x 13.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP303,false,true,36771,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,"2nd month, 1792",1792,1792,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 5/8 in. (31.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP304,false,true,36772,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793?,1783,1883,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 1/2 in. (31.1 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP305,false,true,36773,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1792–93,1782,1803,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 2/3 in. (32.1 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP306,false,true,36774,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793?,1783,1803,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 5/16 in. (29.2 x 13.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP307,false,true,36775,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,12 7/16 x 5 17/32 in. (31.6 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP308,false,true,36776,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 5/8 in. (32.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP309,false,true,36777,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP310,false,true,36778,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1787,1787,1787,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP311,false,true,36779,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1792?,1782,1804,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 1/2 in. (31.1 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP312,false,true,36780,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1794?,1784,1804,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 1/2 in. (31.3 x 14.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP313,false,true,36781,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,"2nd month, 1789",1789,1789,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 7/8 in. (32.1 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP314,false,true,36782,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1781,1771,1791,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 1/2 x 5 2/3 in. (31.8 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP315,false,true,36783,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP316,false,true,36784,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 5/8 in. (32.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP317,false,true,36785,Asian Art,Print,三代目大谷鬼次|Kabuki Actor Ōtani Oniji III as a Samurai,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1792,1782,1802,Polychrome woodblock print; ink and color on paper,12 15/16 x 5 2/3 in. (32.9 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP318,false,true,36786,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 5/8 in. (30.8 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP319,false,true,36787,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 3/4 in. (32.7 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP320,false,true,36788,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP321,false,true,36789,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,December 1790,1790,1790,Polychrome woodblock print; ink and color on paper,11 9/16 x 5 5/8 in. (29.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP322,false,true,36790,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,"12th month, 1788",1788,1788,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 17/32 in. (32.7 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP323,false,true,36791,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP324,false,true,36792,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,12 x 5 3/8 in. (30.5 x 13.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP325,false,true,36793,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793?,1783,1803,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 5/8 in. (32.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP326,false,true,36794,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1788,1778,1798,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP327,false,true,36795,Asian Art,Print,三代目坂田半五郎|Kabuki Actor Sakata Hangorō III as an Outlaw,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,12 4/5 x 5 2/3 in. (32.5 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP328,false,true,36796,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,"5th month, 1795",1795,1795,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 7/16 in. (32.1 x 13.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP329,false,true,36797,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP330,false,true,36798,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1796,1786,1806,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 5/8 x 5 5/8 in. (32.1 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP331,false,true,36799,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1795?,1785,1805,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 5/8 in. (31.3 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP333,false,true,36801,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790?,1780,1800,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 17/32 in. (32.4 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP334,false,true,36802,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 17/32 in. (31.4 x 14.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP335,false,true,36803,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1791?,1781,1801,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/4 x 5 5/8 in. (32.4 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP336,false,true,36804,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1797,1787,1807,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 1/2 x 5 2/3 in. (31.8 x 14.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP337,false,true,36805,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,"2nd month, 1794",1794,1794,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 7/16 in. (30.2 x 13.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP715,false,true,37163,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 3/4 in. (30.8 x 14.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP716,false,true,37164,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1799,1789,1809,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 7/8 in. (35.9 x 25.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP912,false,true,54652,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 10 5/16 in. (26.2 cm); W. 7 9/16 in. (19.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP913,false,true,54653,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1796,1796,1796,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP914,false,true,54654,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1703,1693,1713,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 5 1/2 in. (14 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP915,false,true,54655,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP916,false,true,54656,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 3/4 in. (24.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1221,false,true,55144,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1302,false,true,55241,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1794 (Kansei 6),1794,1794,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 9/16 in. (14.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1347,false,true,55313,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1785,1785,1785,Polychrome woodblock print; ink and color on paper,Hosoban 12 5/16 x 5 1/2 in. (31.3 x 14 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1348,false,true,55315,Asian Art,Print,三代目瀬川菊之丞|Kabuki Actor Segawa Kikunojō III in a Female Role,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1788,1788,1788,Polychrome woodblock print; ink and color on paper,Hosoban 12 5/16 x 5 1/2 in. (31.3 x 14 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1349,false,true,55321,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1794,1794,1794,Polychrome woodblock print; ink and color on paper,12 13/16 x 5 13/16 in. (32.5 x 14.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1350,false,true,55322,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1789,1789,1789,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 11/16 in. (14.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1354,false,true,55330,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1782,1782,1782,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 5 1/4 in. (13.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1386,false,true,55387,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1794,1794,1794,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1401,false,true,45242,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1788,1778,1798,Polychrome woodblock print; uchiwa fan format; ink and color on paper,W. 10 1/4 in. (26 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1493,false,true,55588,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 5 in. (12.7 cm),"Fletcher Fund, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1536,false,true,55701,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,H. 8 in. (20.3 cm); W. 9 9/16 in. (24.3 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1537,false,true,55702,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1792,1782,1802,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 3/4 in. (14.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1538,false,true,55703,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 12 9/16 in. (31.9 cm); W. 5 5/8 in. (14.3 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1539,false,true,55704,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 6 in. (15.2 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1540,false,true,55705,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 13 1/8 in. (33.3 cm); W. 5 3/4 in. (14.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2404,false,true,56807,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1789,1789,1789,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Gift of Louis V. Ledoux, 1931",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2707,false,true,56972,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,12 x 6 in. (30.5 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2708,false,true,56974,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1791,1781,1811,Polychrome woodblock print; ink and color on paper,12 5/6 x 5 5/8 in. (32.6 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2709,false,true,56975,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790–1797,1790,1797,Polychrome woodblock print; ink and color on paper,11 3/4 x 5 3/8 in. (29.8 x 13.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2710,false,true,56976,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,12 3/8 x 5 9/16 in. (31.4 x 14.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2711,false,true,56978,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/2 in. (30.8 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2712,false,true,56980,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2713,false,true,56981,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,12 3/8 x 5 1/2 in. (31.4 x 14 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2714,false,true,56982,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1792,1782,1802,Probably the middle sheet of a triptych or the right-hand sheet of a diptych of polychrome woodblock prints; ink and color on paper,12 3/4 x 5 5/8 in. (32.4 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2715,false,true,56983,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1793,1793,1793,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2824,false,true,45037,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/4 in. (38.7 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2894,false,true,56009,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1762–1819,1762,1819,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2895,false,true,56010,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1762–1819,1762,1819,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 1/4 in. (30.2 x 13.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2897,false,true,56012,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1762–1819,1762,1819,Polychrome woodblock print; ink and color on paper,12 3/4 x 6 in. (32.4 x 15.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2898,false,true,56013,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1762–1819,1762,1819,Polychrome woodblock print; ink and color on paper,12 x 5 1/4 in. (30.5 x 13.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2899,false,true,56014,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1762–1819,1762,1819,Polychrome woodblock print; ink and color on paper,12 x 5 3/8 in. (30.5 x 13.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP338a, b",false,true,36476,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1792,1782,1802,Diptych of polychrome woodblock prints; ink and color on paper,A: H. 12 13/16 (32.5 cm); W. 5 11/16 in. (14.4 cm) B: H. 13 in. (33 cm); W. 5 3/4 in. (14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP340a, b",false,true,36474,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Diptych of polychrome woodblock prints; ink and color on paper,Overall: H. 12 9/16 in. (31.9 cm); W. 11 1/2 in. (29.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP339a–c,false,true,36475,Asian Art,Woodblock prints,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,ca. 1793,1783,1803,Triptych of polychrome woodblock prints; ink and color on paper,A: H. 12 3/8 in. (31.4 cm); W. 5 1/2 in. (14 cm) B: H. 12 5/8 in. (32.1 cm); W. 5 5/8 in. (14.3 cm) C: H. 13 in. (33 cm); W. 5 7/8 in. (14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2896a–c,false,true,56011,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,1762–1819,1762,1819,Triptych of polychrome woodblock prints; ink and color on paper,A: 12 3/4 x 5 5/8 in. (32.4 x 14.3 cm) B: 12 3/4 x 5 3/4 in. (32.4 x 14.6 cm) C: 12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1005,false,true,54881,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,ca. 1804,1794,1814,Two sheets of a pentaptych of polychrome woodblock prints; ink and color on paper,Aiban; H. 13 3/8 in. (34 cm); W. 19 1/2 in. (49.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1006,false,true,54882,Asian Art,Print,"Horinouchi Myōhōji Eho Mairi no Zu|Pilgrimage to Myōhōji in Horinouchi, Edo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,ca. 1804,1794,1814,Two sheets of a pentaptych of polychrome woodblock prints; ink and color on paper,H. 14 3/8 in. (36.5 cm); W. 19 in. (48.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1007,false,true,54883,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,ca. 1805,1795,1815,One sheet of a triptych(?)of polychrome woodblock prints; ink and color on paper,H. 15 in. (38.1 cm); W. 10 1/4 in. (26 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1009,false,true,54885,Asian Art,Print,Tsurigitsune|Trapping the Fox,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,1808,1808,1808,Polychrome woodblock print; ink and color on paper,H. 14 in. (35.6 cm); W. 9 1/4 in. (23.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2718,false,true,56986,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,ca. 1802,1792,1812,Probably one sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/8 x 8 3/8 in. (31.4 x 21.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP193,false,true,36670,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,10 x 7 7/32 in. (25.4 x 18.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP527,false,true,36978,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP528,false,true,36979,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP529,false,true,36980,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP530,false,true,36981,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP531,false,true,36982,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP532,false,true,36983,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP533,false,true,36984,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP534,false,true,36985,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP535,false,true,36986,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP536,false,true,36987,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP537,false,true,36988,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP538,false,true,36989,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,1789,1789,1789,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP938,false,true,45243,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 8 5/8 in. (21.9 cm); W. 6 3/8 in. (16.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3053,false,true,56508,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,11 5/8 x 16 5/8 in. (29.5 x 42.2 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP200,false,true,36677,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1797,1787,1807,Pentaptych of polychrome woodblock prints; ink and color on paper,15 7/32 x 50 1/2 in. (38.7 x 128.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP201,false,true,36678,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1791,1781,1801,Triptych of polychrome woodblock prints; ink and color on paper,15 3/8 x 29 3/4 in. (39.1 x 75.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP203,false,true,36680,Asian Art,Print,雪こかし|Courtesans and Attendants Playing in the Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1796,1786,1806,Triptych of polychrome woodblock prints; ink and color on paper,14 15/32 x 29 15/32 in. (36.8 x 74.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP204,false,true,36681,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1791,1781,1801,Triptych of polychrome woodblock prints; ink and color on paper,15 5/8 x 30 5/8 in. (39.7 x 77.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP235,false,true,36707,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,10 1/8 x 7 5/8 in. (25.7 x 19.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP664,false,true,37111,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1801,1791,1821,Two sheets of a triptych of polychrome woodblock prints; ink and color on paper,14 1/8 x 19 15/32 in. (35.9 x 49.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP665,false,true,37112,Asian Art,Woodblock prints,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1800,1790,1810,Triptych of polychrome woodblocks print; ink and color on paper,15 1/2 x 30 1/8 in. (39.4 x 76.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1003,false,true,54880,Asian Art,Print,吉原仲の町花魁道中|Courtesans Promenading on the Nakanochō in Yoshiwara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1785,1805,Triptych of polychrome woodblock prints; ink and color on paper,H. 15 3/8 in. (39.1 cm); W. 50 in. (127 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1004,false,true,45226,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1796,1615,1868,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 10 1/8 in. (25.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1119,false,true,55041,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1798–99,1798,1799,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 9 5/8 in. (24.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1371,false,true,55350,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1387,false,true,55389,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 10 1/4 in. (26 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1388,false,true,55394,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 10 1/4 in. (26 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1389,false,true,55395,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1798,1798,1798,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 5/8 in. (14.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1390,false,true,55397,Asian Art,Print,"『祇園神輿洗 ねり物姿』「いろは歌の売」|“The Geisha To’e as a Vendor of Poems,” from the series Gion Festival Costume Parade (Gion mikoshi arai nerimono sugata)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 13 1/4 in. (33.7 cm); W. 6 1/4 in. (15.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1391,false,true,55398,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1810,1800,1820,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 9 3/4 in. (24.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1407,false,true,55420,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1804,1804,1804,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 3/16 in. (25.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1408,false,true,55421,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1804,1794,1814,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1492,false,true,45290,Asian Art,Print,"Tokaido Yotsuya Kaidan|Onoe Matsusuke as the Ghost of the Murdered Wife Oiwa, in ""A Tale of Horror from the Yotsuya Station on the Tokaido Road""",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1812,1615,1868,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 10 1/8 in. (25.7 cm),"Gift of Louis V. Ledoux, 1927",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1529,false,true,55640,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1798 (Kansei 9),1798,1798,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 9 1/2 in. (24.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1530,false,true,55641,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper; mica background,14 3/4 x 10 in. (37.5 x 25.4cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1531,false,true,55644,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,15 3/8 x 10 1/8 in. (39.1 x 25.7cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1570,false,true,55747,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1797,1787,1807,Triptych of polychrome woodblock prints; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1743,false,true,56050,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 10 1/8 in. (25.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1747,false,true,56054,Asian Art,Print,南四季 夏景|The Four Seasons in Southern Edo: A Summer Scene (Minami shiki; Natsu [no] kei),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,late 1780s,1787,1789,Right and center sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 10 1/8 in. (36.5 x 25.7 cm) Image (b): 14 1/2 x 9 15/16 in. (36.8 x 25.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1749,false,true,56056,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1800,1790,1810,Triptych of polychrome woodblock prints; ink and color on paper,A: H. 14 13/16 in. (37.6 cm); W. 10 1/16 in. (25.6 cm) B: H. 14 3/4 in. (37.5 cm); W. 10 in. (25.4 cm) C: H. 14 3/16 in. (36 cm); W. 9 15/16 in. (25.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1910,false,true,54449,Asian Art,Print,娘道成寺図の錦絵|Print of a Kabuki Dancer from the Maiden of the Dōjōji Temple (Musume Dōjōji),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1810s,1810,1819,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/8 in. (13.7 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2719,false,true,56987,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1796,1786,1806,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2720,false,true,56989,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 1/2 in. (35.9 x 24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2721,false,true,56991,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,14 7/8 x 10 in. (37.8 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2722,false,true,56992,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1774,1794,1794,Polychrome woodblock print; ink and color on paper,14 7/8 x 9 5/8 in. (37.8 x 24.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2723,false,true,56993,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1796,1796,1796,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2725,false,true,51136,Asian Art,Print,御影堂扇屋図|The Mieidō Fan Shop,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1785–93,1785,1793,Triptych of polychrome woodblock prints; ink and color on paper,Each H. 15 1/8 in. (38.4 cm); W. 10 1/8 in. (25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2726,false,true,56997,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1800,1790,1810,Diptych of polychrome woodblock prints; ink and color on paper,Each sheet: 15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2825,false,true,57073,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1790s,1785,1805,Polychrome woodblock print; ink and color on paper,15 13/16 x 9 3/8 in. (40.2 x 23.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2826,false,true,57072,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1769–1825,1769,1825,Polychrome woodblock print; ink and color on paper,14 7/8 x 9 7/8 in. (37.8 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2828,false,true,44997,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 3/4 in. (24.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2829,false,true,57071,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1769–1825,1769,1825,Polychrome woodblock print; ink and color on paper,14 11/16 x 10 in. (37.3 x 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2900,false,true,56015,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1769–1825,1769,1825,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/2 in. (30.8 x 14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2901,false,true,56016,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1769–1825,1769,1825,Polychrome woodblock print; ink and color on paper,13 x 6 in. (33 x 15.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2902,false,true,56017,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1769–1825,1769,1825,Polychrome woodblock print; ink and color on paper,12 3/8 x 5 1/4 in. (31.4 x 13.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3391,false,true,55567,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 1/4 in. (38.4 x 26 cm),"Gift of Mr. and Mrs. Arthur J. Steel, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.125,false,true,76555,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,1812,1812,1812,Polychrome woodblock print with metallic pigment and lacquer details,Image (ôban tate-e): 14 7/8 x 10 1/8 in. (37.8 x 25.7 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2724a–c,false,true,56995,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1799,1789,1809,Triptych of polychrome woodblock prints; ink and color on paper,Each sheet: 15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2026,false,true,54786,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,probably 1808,1808,1808,Polychrome woodblock print (surimono); ink and color on paper,5 1/8 x 7 3/16 in. (13 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2362,false,true,54146,Asian Art,Print,"摺物帖 『春雨集』 『花鳥六番之内 下野宇都宮』 牡丹に燕|Spring Rain Collection (Harusame shū), vol. 3: Swallows and Peonies",Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,ca. 1820,1810,1830,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 1/4 x 7 1/16 in. (21 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2363,false,true,54147,Asian Art,Print,"摺物帖 『春雨集』 『花鳥六番之内 下野宇都宮』 海棠に山雀|Spring Rain Collection (Harusame shū), vol. 3: Marsh-tits and Crab Apple Flowers",Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,ca. 1820,1810,1830,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 x 7 3/8 in. (20.3 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2364,false,true,54148,Asian Art,Print,"摺物帖 『春雨集』 『花鳥六番之内 下野宇都宮』 タンポポに雀|Spring Rain Collection (Harusame shū), vol. 3: Sparrows and Dandelions",Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,ca. 1820,1810,1830,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 3/8 x 7 1/2 in. (21.3 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2365,false,true,54149,Asian Art,Woodblock print,"摺物帖 『春雨集』 『花鳥六番之内 下野宇都宮』 桃に山鳩|Spring Rain Collection (Harusame shū), vol. 3: Mountain Dove and Peach Flowers",Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,ca. 1820,1810,1830,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 1/4 x 7 7/16 in. (21 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2635,false,true,45225,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunimasa,"Japanese, 1773–1810",,Utagawa Kunimasa,Japanese,1773,1810,late 1790s,1796,1799,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1416,false,true,51086,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni II,"Japanese, 1777–1835",,Utagawa Toyokuni II,Japanese,1777,1835,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 5/16 in. (36.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1750,false,true,54396,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni II,"Japanese, 1777–1835",,Utagawa Toyokuni II,Japanese,1777,1835,ca. 1834,1824,1844,Polychrome woodblock print (surimono); ink and color on paper,7 x 6 3/4 in. (17.8 x 17.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1153,false,true,54359,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Takashima Chiharu,"Japanese, 1777–1859",,Takashima Chiharu,Japanese,1777,1859,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/16 in. (20.5 x 17.9 cm),"Gift of T. Ito, Chicago, Ill, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2064,false,true,54892,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Takashima Chiharu,"Japanese, 1777–1859",,Takashima Chiharu,Japanese,1777,1859,probably 1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 7/16 in. (14.1 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP749,false,true,54307,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1830,1820,1840,Diptych of woodblock prints (surimono); ink and color on paper,8 1/8 x 14 7/16 in. (20.6 x 36.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1032,false,true,54317,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 in. (20.5 x 17.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1033,false,true,54318,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1825,1825,1825,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 3/16 in. (21.6 x 18.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1035,false,true,54320,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1825,1815,1835,Polychrome woodblock print (surimono); ink and color on paper,8 5/8 x 7 5/8 in. (21.9 x 19.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1098,false,true,54327,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 3/4 in. (19.7 x 17.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1100,false,true,54329,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,16 3/16 x 7 1/4 in. (41.1 x 18.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1144,false,true,54351,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",(?),Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/8 in. (21 x 19.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1145,false,true,54352,Asian Art,Print,Sumidagawa|Sumida River,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,"Year of the Dragon, probably 1832",1832,1832,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 3/8 in. (20 x 18.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1146,false,true,44978,Asian Art,Print,Tosa no umi|Inland Sea near Tosa,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1150,false,true,54356,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 7 1/4 in. (19.7 x 18.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1154,false,true,54360,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 7/8 in. (19.7 x 17.5 cm),"Gift of T. Ito, Chicago, Ill, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1240,false,true,54371,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1830,1830,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1241,false,true,54372,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1830,1830,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1242,false,true,54373,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1830,1830,1830,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/8 in. (20.3 x 18.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1253,false,true,54385,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 11 in. (20.5 x 27.9 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1297,false,true,54387,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 1/8 in. (21.3 x 18.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1434,false,true,54392,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1825,1815,1835,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 in. (20.6 x 17.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1811,false,true,54399,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1815–20,1815,1820,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/4 in. (20.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1909,false,true,54448,Asian Art,Print,"松・滝に孔雀図と緋毛氈|Painting of Peacocks, Pines, a Waterfall, and a Roll of Red Fabric",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1818,1808,1828,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 1/4 in. (21.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1914,false,true,54453,Asian Art,Woodblock print,鮭頭|Head of a Salmon,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1917,false,true,54456,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",(?),Totoya Hokkei,Japanese,1780,1850,probably 1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1929,false,true,54469,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 1/4 in. (21.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1945,false,true,54513,Asian Art,Print,"印鑑と朱に孔雀羽根|Seal-stone and Seal-ink with Peacock Feathers, from Spring Rain Surimono Album (Harusame surimono-jō), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,probably 1817,1817,1817,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,7 9/16 x 5 1/8 in. (19.2 x 13 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1948,false,true,54516,Asian Art,Print,"元日仕度|Preparations for the New Year, from Spring Rain Surimono Album (Harusame surimono-jō, vol. 1)",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1817,1817,1817,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1977,false,true,54562,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,8 9/16 x 7 3/8 in. (21.7 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2011,false,true,54731,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1811,1811,1811,Polychrome woodblock print (surimono); ink and color on paper,5 11/16 x 7 1/2 in. (14.4 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2033,false,true,54799,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 5/16 in. (13.7 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2046,false,true,54817,Asian Art,Print,"「文齊側五行火 花街細見合」|Courtesan by a Lantern, “Fire,” from the series Five Elements for the Bunsai Poetry Group, a Guide to the Yoshiwara Pleasure Quarters",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 3/16 in. (20 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2107,false,true,54959,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 1/4 x 7 3/16 in. (13.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2288,false,true,54061,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1816,1816,1816,Part of an album of woodblock prints (surimono); ink and color on paper,5 11/16 x 7 1/2 in. (14.4 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2309,false,true,54093,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1800,1790,1810,Part of an album of woodblock prints (surimono); ink and color on paper,7 1/8 x 6 5/8 in. (18.1 x 16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2350,false,true,54135,Asian Art,Woodblock print,"印籠と牛根付『春雨集』 摺物帖|Lacquer Inrō with Waterbirds and Ox-shaped Netsuke in a BoxFrom the Spring Rain Collection (Harusame shū), vol. 3",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,probably 1817,1817,1817,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/4 x 7 3/8 in. (13.3 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2371,false,true,54155,Asian Art,Print,"「三ひらの内」松・牡丹に孔雀|Peacock on Pine Tree and Peonies, from the series Three Sheets (Mihira no uchi)",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,mid- 1810s,1814,1816,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/8 x 7 3/8 in. (21.3 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2372,false,true,54156,Asian Art,Album leaf,"「三ひらの内」汀五羽の鶴|Five Cranes by the Water’s Edge, from the series Three Sheets (Mihira no uchi)",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,mid- 1810s,1814,1816,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/8 x 7 5/16 in. (21.3 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2373,false,true,54157,Asian Art,Print,"「三ひらの内」日輪に烏|Three Crows against the Rising Sun, from the series Three Sheets (Mihira no uchi)",Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,mid- 1810s,1814,1816,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2544,false,true,45040,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1825,1815,1835,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 1/4 in. (21.6 x 18.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2545,false,true,54182,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,ca. 1825,1815,1835,Polychrome woodblock print (surimono); ink and color on paper,10 x 7 in. (25.4 x 17.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3005,false,true,54221,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1780–1850,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3006,false,true,54222,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1780–1850,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 6 11/16 in. (20 x 17 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3007,false,true,54224,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1780–1850,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 3/16 in. (20.6 x 18.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3009,false,true,54226,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1831,1831,1831,Polychrome woodblock print (surimono); ink and color on paper,16 5/8 x 7 in. (42.2 x 17.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1126.3,false,true,633305,Asian Art,woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,1835,1835,1835,Polychrome woodblock print (shikishiban surimono); ink and color on paper,Image: 7 1/4 × 6 3/4 in. (18.4 × 17.1 cm),"Gift of Dorothy Tapper Goldman, 2013",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/633305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP225,false,true,36699,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 3/8 in. (24.1 x 36.5 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1147,false,true,54353,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1819,1809,1829,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/4 in. (20.8 x 18.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1216,false,true,55139,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1850,1840,1860,Polychrome woodblock print; ink and color on paper,H. 14 5/16 in. (36.4 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1251,false,true,54383,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1824,1814,1834,Polychrome woodblock print (surimono); ink and color on paper,8 7/16 x 7 3/8 in. (21.4 x 18.7 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1267,false,true,55164,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1833–47,1833,1847,Polychrome woodblock print; ink and color on paper,14 1/8 x 10 1/8 in. (35.9 x 25.7 cm),"Gift of Albert Gallatin, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1454,false,true,54395,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1840,1830,1850,Polychrome woodblock print (surimono); ink and color on paper,8 7/16 x 7 3/8 in. (21.4 x 18.7 cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1497,false,true,44979,Asian Art,Print,"Futami-ga-ura akebono-no kuni|Futami-ga-ura Rocks at Ise, Land of Dawn",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 in. (35.6 cm),"Fletcher Fund, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2051,false,true,54824,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,probably 1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 5 5/16 in. (20 x 13.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2396,false,true,54176,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1819,1819,1819,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 7/16 in. (20.8 x 18.9 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2631,false,true,56679,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,mid-19th century,1834,1866,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/2 in. (36.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2863,false,true,56611,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,mid-19th century,1834,1866,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/2 in. (36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2911,false,true,56026,Asian Art,Print,O Ateri Kyogen Uchi|Wild Words - a Play,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1786–1864,1786,1864,Polychrome woodblock print; ink and color on paper,14 7/8 x 9 7/8 in. (37.8 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3358,false,true,55526,Asian Art,Print,"「湯灌場子僧吉三 市川竹之丞」(五代目) 「近世水滸伝」|Ichimura Takenojō V as Yukanba Kozō Kichiza, from A Modern Water Margin (Kinsei suikoden)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1862,1862,1862,Polychrome woodblock print; ink and color on paper,13 5/18 x 9 9/16 in. (33.7 x 24.3 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3359,false,true,55527,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,"1854 (year of the Horse, 7th month)",1854,1854,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3360,false,true,55528,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,"1854 (year of the Horse, 7th month)",1854,1854,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 3/4 in. (35.9 x 24.8 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3361,false,true,55529,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,"1854 (year of the Horse, 7th month)",1854,1854,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 3/4 in. (35.9 x 24.8 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3376,false,true,55543,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1863 (5th month),1863,1863,Polychrome woodblock print; ink and color on paper,14 1/8 x 28 1/2 in. (35.9 x 72.4 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.15,false,true,58000,Asian Art,Print,「籠細工 浪花細工人一田庄七郎」|Basketry Work: By the Craftsman Ichida Shōshichirō of Naniwa (Kagosaiku Naniwa saikujin Ichida Shōshichirō),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1819,1819,1819,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.16,false,true,58001,Asian Art,Print,「籠細工 浪花細工人一田庄七郎」|Basketry Work: By the Craftsman Ichida Shōshichirō of Naniwa (Kagosaiku Naniwa saikujin Ichida Shōshichirō),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1819,1819,1819,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.17,false,true,58002,Asian Art,Print,「籠細工 浪花細工人一田庄七郎」|Basketry Work: By the Craftsman Ichida Shōshichirō of Naniwa (Kagosaiku Naniwa saikujin Ichida Shōshichirō),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1819,1819,1819,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.18,false,true,58003,Asian Art,Print,「籠細工 浪花細工人一田庄七郎」|Basketry Work: By the Craftsman Ichida Shōshichirō of Naniwa (Kagosaiku Naniwa saikujin Ichida Shōshichirō),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1819,1819,1819,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.58,false,true,58042,Asian Art,Print,"三代目関三十郎の大寺正兵衛 小袖曽我薊色縫|Seki Sanjūrō III as Ōdera Shōbei from ""Kosode Soga azami no ironui""",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1859,1859,1859,Right panel of a tryptich of polychrome woodblock prints; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.11,false,true,58263,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1830s (Tenpô era),1830,1839,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 10 1/8 in. (37.5 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.12,false,true,58264,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,mid-19th century,1834,1866,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 × 10 1/2 in. (36.5 × 26.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.457.1,false,true,53710,Asian Art,Print,"重陽後の月宴 十二月ノ内|Banquet of the Next Full Moon at the Chrysanthemum Festival, from the series The Twelve Months (Chōyō nochi no tsuki no en, Jūni tsuki no uchi)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1854,1854,1854,Triptych of polychrome woodblock prints; ink and color on paper,Overall: 14 5/8 x 30 1/8in. (37.1 x 76.5cm),"Gift of Eliot C. Nolen, 1999",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.3,false,true,63357,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1820,1810,1830,"Polychrome woodblock print (surimono); ink, silver, and color on paper",8 1/2 x 7 1/2 in. (21.6 x 19.1 cm),"Purchase, Jack Greene Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.13,false,true,63384,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,"Oban tate-e, 14 3/4 x 10 1/4 in. (37.5 x 26 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.271,false,true,73611,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1864,1864,1864,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 10 in. (36.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1126.1,false,true,633299,Asian Art,woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,probably 1829,1829,1829,Polychrome woodblock print (shikishiban surimono); ink and color on paper,Image: 8 1/4 × 7 1/4 in. (21 × 18.4 cm),"Gift of Dorothy Tapper Goldman, 2013",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/633299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1126.2,false,true,633302,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,ca. 1830,1820,1840,"Polychrome woodblock print (shikishiban surimono); ink, color and metallic pigments on paper",Image: 8 1/4 × 7 1/4 in. (21 × 18.4 cm),"Gift of Dorothy Tapper Goldman, 2013",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/633302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.12a–c,false,true,63381,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1811,1801,1821,Triptych of polychrome woodblock prints; ink and color on paper,"Oban tate-e; triptych, each: 15 1/8 x 10 1/8 in. (38.4 x 25.7 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1217,false,true,55140,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1218,false,true,55141,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 3/4 in. (24.8 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1244,false,true,54375,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,1828,1828,1828,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1245,false,true,54376,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,1830,1830,1830,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 5/16 in. (21.1 x 18.6 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1281,false,true,55215,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1944,false,true,54512,Asian Art,Print,"鶏図衝立を見る鶏|Cock Eyeing a Free-standing Screen Painted with Cock, Hen, and Chicks, from Spring Rain Surimono Album (Harusame surimono-jō), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,probably 1813,1813,1813,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,7 11/16 x 10 7/8 in. (19.5 x 27.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2659,false,true,56838,Asian Art,Print,Shōkei|Celebrated Waterfall,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,1820–1830,1820,1830,Polychrome woodblock print; ink and color on paper,13 x 6 3/4 in. (33 x 17.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1017,false,true,54926,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,ca. 1805,1795,1815,Triptych of polychrome woodblock prints; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 30 in. (76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1962,false,true,54537,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,probably 1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,5 x 7 1/16 in. (12.7 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1018,false,true,54927,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomine,"Japanese, 1787–1868",,Torii Kiyomine,Japanese,1787,1868,ca. 1820,1810,1830,Triptych of monochrome woodblock prints; ink on paper,H. 15 in. (38.1 cm); W. 30 in. (76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1019,false,true,54316,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomine,"Japanese, 1787–1868",,Torii Kiyomine,Japanese,1787,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 7/16 x 7 3/16 in. (21.4 x 18.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1801,false,true,56103,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomine,"Japanese, 1787–1868",,Torii Kiyomine,Japanese,1787,1868,ca. 1808,1798,1818,Polychrome woodblock print; ink and color on paper,14 15/16 x 9 7/8 in. (37.9 x 25.1cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1802,false,true,56104,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomine,"Japanese, 1787–1868",,Torii Kiyomine,Japanese,1787,1868,ca. 1804,1794,1814,Polychrome woodblock print; ink and color on paper,19 1/16 x 9 1/16in. (48.4 x 23cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP222,false,true,36696,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 in. (24.8 x 35.6 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1031,false,true,54951,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1838,1833,1843,Polychrome woodblock print; ink and color on paper,H. 8 5/8 in. (21.9 cm); W. 13 1/2 in. (34.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1065,false,true,55012,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1824,1824,1824,Polychrome woodblock print; ink and color on paper,H. 7 in. (17.8 cm); W. 5 in. (12.7 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1066,false,true,55013,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1824,1824,1824,Polychrome woodblock print; ink and color on paper,H. 7 in. (17.8 cm); W. 5 in. (12.7 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1067,false,true,55014,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1824,1824,1824,Polychrome woodblock print; ink and color on paper,H. 7in. (17.8 cm); W. 5 in. (12.7 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1068,false,true,55015,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 7 in. (17.8 cm); W. 5 in. (12.7 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1069,false,true,55016,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1824,1824,1824,Polychrome woodblock print; ink and color on paper,H. 7 in. (17.8 cm); W. 5 in. (12.7 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1070,false,true,55017,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 7 in. (17.8 cm); W. 5 in. (12.7 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1071,false,true,55018,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 7 in. (17.8 cm); W. 5 in. (12.7 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1148,false,true,54354,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 7/8 in. (21 x 20 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1201,false,true,55119,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,late 1830s,1830,1840,Polychrome woodblock print; ink and color on paper,Oban 9 5/8 x 14 7/16 in. (24.4 x 36.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1282,false,true,55218,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1845,1835,1855,Polychrome woodblock print; ink and color on paper,H. 6 5/8 in. (16.8 cm); W. 12 1/4 in. (31.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1283,false,true,55222,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,mid-19th century,1834,1866,Polychrome woodblock print; ink and color on paper,H. 9 in. (22.9 cm); W. 14 in. (35.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1419,false,true,55447,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2220,false,true,53986,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,probably 1812,1812,1812,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 9/16 in. (19.7 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2742,false,true,54204,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1840,1830,1850,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 1/4 in. (21.3 x 18.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2870,false,true,57041,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1790–1848,1790,1848,Polychrome woodblock print; ink and color on paper,8 5/8 x 13 1/2 in. (21.9 x 34.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2874,false,true,57012,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,1790–1848,1790,1848,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 1/2 in. (24.1 x 36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.14,false,true,63397,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,"Oban yoko-e, 10 1/4 x 15 1/8 in. (26 x 38.4 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2231,false,true,53997,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ōishi Matora,"Japanese, 1793–1833",,Ōishi Matora,Japanese,1793,1833,1827,1827,1827,Polychrome woodblock print (surimono); ink and color on paper,9 7/8 x 5 3/4 in. (25.1 x 14.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1936,false,true,54498,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuninao,"Japanese, 1793–1854",,Utagawa Kuninao,Japanese,1793,1854,1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 3 1/4 in. (21 x 8.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1959,false,true,54532,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuninao,"Japanese, 1793–1854",,Utagawa Kuninao,Japanese,1793,1854,1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2037,false,true,54807,Asian Art,Print,"『春雨集』 摺物帖歌川国直画 元禄美人|Spring Rain Collection (Harusame shū), vol. 1: Genroku-style Courtesan",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuninao,"Japanese, 1793–1854",,Utagawa Kuninao,Japanese,1793,1854,probably 1810s,1810,1819,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 7/16 x 3 7/16 in. (21.4 x 8.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.4a–c,false,true,63358,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyasu,"Japanese, 1794–1834",,Utagawa Kuniyasu,Japanese,1794,1834,ca. 1823,1813,1833,Triptych of polychrome woodblock prints (surimono); ink and color on paper,"Triptych, each: 7 1/4 x 8 3/8 in. (18.4 x 21.3 cm)","Purchase, Jack Greene Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP221,false,true,36695,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,10 1/2 x 15 1/8 in. (26.7 x 38.4 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP237,false,true,36709,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,8 7/10 x 13 5/8 in. (22.1 x 34.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP750,false,true,37194,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 15/32 in. (25.7 x 36.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1118,false,true,55040,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1840,1840,1840,Polychrome woodblock print; ink and color on paper,H. 8 5/8 in. (21.9 cm); W. 13 9/16 in. (34.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1120,false,true,55042,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1845,1845,1845,Polychrome woodblock print; ink and color on paper,H. 13 1/8 in. (33.3 cm); W. 8 15/16 in. (22.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1149,false,true,54355,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 3/8 in. (21.3 x 18.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1252,false,true,54384,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1840,1840,1840,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1421,false,true,55449,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1840,1840,1840,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/16 in. (38.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1422,false,true,55451,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,Oban: H. 9 3/4 in. (24.8 cm); W. 14 11/16 in. (37.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1423,false,true,55452,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1850,1840,1860,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 1/16 in. (35.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1424,false,true,55453,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1850,1840,1860,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 14 in. (35.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1465,false,true,55514,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1845,1835,1855,Polychrome woodblock print; ink and color on paper,H. 14 11/16 in. (37.3 cm); W. 10 1/16 in. (25.6 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1563,false,true,45282,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1850,1840,1860,Diptych of polychrome woodblock prints; ink and color on paper,A: H. 14 3/8 in. (36.5 cm); W. 10 in. (25.4 cm) B: H. 14 3/8 in. (36.5 cm); W. 10 in. (25.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1564,false,true,45281,Asian Art,Print,Ryugu Tamatori Hime no su|Recovering the Stolen Jewel from the Palace of the Dragon King,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1853,1615,1868,Triptych of polychrome woodblock prints; ink and color on paper,A: H. 14 3/8 in. (36.5 cm); W. 9 3/4 in. (24.8 cm) B: H. 14 3/8 in. (36.5 cm); W. 9 7/8 in. (25.1 cm) C: H. 14 3/8 in. (36.5 cm); W. 9 3/4 in. (24.8 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1565,false,true,55743,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1843–47,1843,1847,Triptych of polychrome woodblock prints; ink and color on paper,Oban triptych: Each H. 14 3/4 in. (37.5 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1805,false,true,51085,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1830–44,1830,1844,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 14 9/16 in. (37 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2633,false,true,51084,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,mid-19th century,1834,1866,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 13 3/4 in. (34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2634,false,true,56779,Asian Art,Print,"Sashu Tsukahara setchu|Nichiren in Snow at Tsukahara, Sodo Province",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,"8 7/8 x 13 3/5 in. (22.5 x 34.5 cm), excluding margins","The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2749,false,true,56790,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,mid-19th century,1834,1866,Polychrome woodblock print; ink on thin paper,H. 8 1/2 in. (21.6 cm); W. 13 3/4 in. (34.9 cm),"Fletcher Fund, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2856,false,true,57049,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1835–36,1835,1836,Polychrome woodblock print; ink and color on paper,Oban: 9 3/4 x 14 3/4 in. (24.8 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2857,false,true,57048,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1797–1861,1797,1861,Polychrome woodblock print; ink and color on paper,10 3/4 x 14 7/8 in. (27.3 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2858,false,true,57047,Asian Art,Print,Toto Mitsumata no zu|Picture of Mitsumata,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1797–1861,1797,1861,Polychrome woodblock print; ink and color on paper,10 x 14 1/2 in. (25.4 x 36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2859,false,true,45286,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 5/8 in. (22.2 x 34.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2860,false,true,45287,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2861,false,true,45284,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2862,false,true,57046,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1797–1861,1797,1861,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 in. (37.5 x 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3008,false,true,54225,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,1797–1861,1797,1861,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 1/2 in. (21.6 x 19.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3136,false,true,56684,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1844–46,1834,1856,"Triptych of polychrome woodblock prints; ink, silver, and color on paper",9 3/4 x 22 1/8 in. (24.8 x 56.2 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.612.1,false,true,62028,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1845–46,1835,1856,Polychrome woodblock print; ink and color on paper,13 1/2 x 9 in. (34.3 x 22.9 cm),"Gift of Judith Underwood Stewart, in memory of Martha Davenport Heard, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/62028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.612.2,false,true,62029,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1845–46,1835,1856,Polychrome woodblock print; ink and color on paper,13 1/2 x 9 in. (34.3 x 22.9 cm),"Gift of Judith Underwood Stewart, in memory of Martha Davenport Heard, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/62029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.612.3,false,true,62030,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1845–46,1835,1856,Polychrome woodblock print; ink and color on paper,13 1/2 x 9 in. (34.3 x 22.9 cm),"Gift of Judith Underwood Stewart, in memory of Martha Davenport Heard, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/62030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.612.4,false,true,62031,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1845–46,1835,1856,Polychrome woodblock print; ink and color on paper,13 1/2 x 9 in. (34.3 x 22.9 cm),"Gift of Judith Underwood Stewart, in memory of Martha Davenport Heard, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/62031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.612.5,false,true,62032,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1845–46,1835,1856,Polychrome woodblock print; ink and color on paper,13 1/2 x 9 in. (34.3 x 22.9 cm),"Gift of Judith Underwood Stewart, in memory of Martha Davenport Heard, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/62032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.6,false,true,63361,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1848,1838,1858,Polychrome woodblock print; ink and color on paper,"Oban tate-e, 14 7/8 x 10 in. (37.8 x 25.4 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.7,false,true,63364,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1848,1838,1858,Polychrome woodblock print; ink and color on paper,"Oban tate-e, 14 3/4 x 10 1/8 in. (37.5 x 25.7 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.8,false,true,63371,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,"Oban tate-e, 17 7/8 x 10 1/8 in. (45.4 x 25.7 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.9,false,true,63374,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,"1856, 2nd month",1856,1856,Polychrome woodblock print; ink and color on paper,"Oban tate-e, 14 5/8 x 10 in. (37.1 x 25.4 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.277,false,true,73560,Asian Art,Woodblock print,"二十四孝童子鑑  楊香|Yang Xiang (Yō Kō), from the series A Child’s Mirror of the Twenty-four Paragons of Filial Piety (Nijūshi kō dōji kagami)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1843,1843,1843,Polychrome woodblock print; ink and color on paper,Image: 8 5/8 x 13 7/8 in. (21.9 x 35.2 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1130a–c,false,true,55050,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,ca. 1850,1840,1860,"Triptych of polychrome woodblock prints; ink, silver, and color on paper",a) H. 14 7/8 in. (37.8 cm); W. 10 1/8 in. (25.7 cm) b): H. 14 3/4 in. (37.5 cm); W. 10 3/8 in. (26.4 cm) c): H. 14 13/16 in. (37.6 cm); W. 10 1/16 in. (25.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3518a–j,false,true,45005,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,mid-19th century,1834,1866,Polychrome woodblock print; ink and color on paper,H. 5 7/8 in. (14.9 cm); W. 4 1/8 in. (10.5 cm),"Gift of Lincoln Kirstein, 1966",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2011.133a, b",false,true,76563,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Gyokuryūtei Shigeharu,"Japanese, 1803–1853",,Gyokuryūtei Shigeharu,Japanese,1803,1853,1830,1830,1830,Diptych of polychrome woodblock prints,Each sheet ( ôban tate-e diptych): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2743,false,true,45265,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1860,1700,1868,Polychrome woodblock print; ink and color on paper,9 1/8 x 6 3/8 in. (23.2 x 16.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2746,false,true,54205,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1862 (Dog Year),1862,1862,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 10 3/4 in. (20 x 27.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3172,false,true,73598,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,ca. 1860,1860,1860,Polychrome woodblock print; ink and color on paper,Image: 7 3/4 × 10 7/8 in. (19.7 × 27.6 cm) Mat: 15 1/4 × 22 3/4 in. (38.7 × 57.8 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1955",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3367,false,true,55534,Asian Art,Print,Fusen zu|写真鏡・風船図|Picture of a Balloon,Japan,Edo period (1615–1868),,,,Artist,,Miyagi Gengyo,"Japanese, 1817–1880",,Gengyo,Japanese,1817,1880,1860,1860,1860,Polychrome woodblock print; ink and color on paper,14 x 9 3/8 in. (35.6 x 23.8 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.3,false,true,58255,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitsuya,"Japanese, 1822–1866",,Utagawa Yoshitsuya,Japanese,1822,1866,mid-19th century,1822,1866,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 1/2 in. (36.2 × 24.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.255,false,true,73546,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitsuya,"Japanese, 1822–1866",,Utagawa Yoshitsuya,Japanese,1822,1866,"5th month, 1863",1863,1863,Polychrome woodblock print; ink and color on paper,Image: 13 7/8 x 9 3/4 in. (35.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2632,false,true,54197,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada II,"Japanese, 1823–1880",,Utagawa Kunisada II,Japanese,1823,1880,1852–64,1852,1864,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/8 in. (20.6 x 18.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.59,false,true,58043,Asian Art,Print,"岩井紫若(二代目)の道具屋娘おかめ|Iwai Shijaku II as Okame, the Daughter of a Furniture Store",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada II,"Japanese, 1823–1880",,Utagawa Kunisada II,Japanese,1823,1880,1864,1864,1864,Right panel of a triptych of polychrome woodblock prints; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.434.5,false,true,75302,Asian Art,Print,大坂下り早竹虎吉|Hayatake Torakichi from Osaka: Performance in Ryōgoku,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada II,"Japanese, 1823–1880",,Utagawa Kunisada II,Japanese,1823,1880,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 in. (36.8 x 25.4 cm),"Gift of Takemitsu Oba, 2009",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/75302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3320,false,true,55464,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshifuji,"Japanese, 1828–1887",,Utagawa Yoshifuji,Japanese,1828,1887,1861,1861,1861,Polychrome woodblock print; ink and color on paper,14 x 9 in. (35.6 x 22.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3322,false,true,55467,Asian Art,Print,Worosiiazin yuko|魯西亜人遊行|Russians Strolling,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshifuji,"Japanese, 1828–1887",,Utagawa Yoshifuji,Japanese,1828,1887,February 1861,1861,1861,Polychrome woodblock print; ink and color on paper,14 x 9 1/2 in. (35.6 x 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3424,false,true,37390,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshifuji,"Japanese, 1828–1887",,Utagawa Yoshifuji,Japanese,1828,1887,1867,1867,1867,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 14 1/8 x 28 3/4 in. (35.9 x 73 cm),"Gift of Lincoln Kirstein, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.251,false,true,73542,Asian Art,Print,Amerikajin|An American Family,Japan,Edo period (1615–1868),,,,Artist,,Ippōsai Yoshifuji,"Japanese, 1828–1887",,Ippōsai Yoshifuji,Japanese,1828,1887,"2nd, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.252,false,true,73543,Asian Art,Print,Oroshiajin|Russian Soldier with His Family,Japan,Edo period (1615–1868),,,,Artist,,Ippōsai Yoshifuji,"Japanese, 1828–1887",,Ippōsai Yoshifuji,Japanese,1828,1887,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 3/4 in. (35.9 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.253,false,true,73544,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippōsai Yoshifuji,"Japanese, 1828–1887",,Ippōsai Yoshifuji,Japanese,1828,1887,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.254,false,true,73545,Asian Art,Woodblock print,Gojin jūshin no hataraku|Five People Working Like Ten,Japan,Edo period (1615–1868),,,,Artist,,Ippōsai Yoshifuji,"Japanese, 1828–1887",,Ippōsai Yoshifuji,Japanese,1828,1887,"3rd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 10 in. (36.2 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.434.1,false,true,75298,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiharu,"Japanese, 1828–1888",,Utagawa Yoshiharu,Japanese,1828,1888,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/4 x 10 in. (36.2 x 25.4 cm),"Gift of Takemitsu Oba, 2009",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/75298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.434.2,false,true,75299,Asian Art,Print,大坂下り早竹虎吉|The Plum Blossom that Flew on Lightning from Chikushino,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiharu,"Japanese, 1828–1888",,Utagawa Yoshiharu,Japanese,1828,1888,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Gift of Takemitsu Oba, 2009",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/75299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.434.3,false,true,75300,Asian Art,Print,大坂下り早竹虎吉|Hayatake Torakichi from Osaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiharu,"Japanese, 1828–1888",,Utagawa Yoshiharu,Japanese,1828,1888,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/4 x 10 in. (36.2 x 25.4 cm),"Gift of Takemitsu Oba, 2009",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/75300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.434.4,false,true,75301,Asian Art,Print,大坂下り早竹虎吉|Hayatake Torakichi from Osaka: Spinning Tops in Ryogoku,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiharu,"Japanese, 1828–1888",,Utagawa Yoshiharu,Japanese,1828,1888,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/4 x 10 1/8 in. (36.2 x 25.7 cm),"Gift of Takemitsu Oba, 2009",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/75301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP236,false,true,36708,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,1859,1859,1859,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 9 1/2 in. (24.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1195,false,true,45263,Asian Art,Print,Kyoto Shijo yu-suzumi|Cooling Off at the Kamo River near Shijo in Kyoto,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,ca. 1860,1850,1870,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1196,false,true,55113,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,1859–61,1859,1861,Polychrome woodblock print; ink and color on paper,H. 14 7/16 in. (36.7 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1485,false,true,55579,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,1862,1862,1862,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1549,false,true,55713,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,ca. 1859,1849,1899,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 9 1/2 in. (24.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2524,false,true,56937,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,1859,1859,1859,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 in. (37.5 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.7,false,true,58259,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,mid-19th century,1834,1866,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 9 7/8 in. (37.5 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.136,false,true,73422,Asian Art,Print,諸国名所百景 肥前長崎唐船の津|Dutch and Chinese Ships in the Harbor at Nagasaki in Hizen Province,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"3rd month, 1859",1859,1859,Polychrome woodblock print; ink and color on paper,Overall: 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.137,false,true,73423,Asian Art,Print,諸国名所百景 對州海岸|Dutch Ship at Anchor off the Coast of Tsushima,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"3rd month, 1859",1859,1859,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.138,false,true,73424,Asian Art,Print,"Bushu Yokohama Gankirō|諸国名所百景 武州横浜岩亀楼|Entrance to the Gankirō Tea House in the Miyozaki District, Yokohama, Bushu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"3rd month, 1859",1859,1859,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.139,false,true,73425,Asian Art,Woodblock print,"横浜賣物図会の内 唐犬|Copper Plate Engraving of a Woman Riding a Horse, a Goat and a Dog",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"3rd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.142,false,true,73428,Asian Art,Woodblock print,「亞墨利加」|America: A Woman on Horseback in the Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"10th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 7/8 in. (35.6 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.143,false,true,73429,Asian Art,Print,"Furansu|ふらんす|French Woman, Her Child and Pet Dog",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"10th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 1/4 in. (37.5 x 26 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.144,false,true,73430,Asian Art,Print,南京 於魯西亜|Russians and a Chinese Inscribing a Fan,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.145,false,true,73431,Asian Art,Print,"亜墨利加 横浜本村本牧道|American Woman Riding Side-Saddle on the Road at Hommoku, Motomura, Yokohama",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.147,false,true,73433,Asian Art,Print,Tōtō Takanawa Kaigen|Foreigners Riding Along the Coast at Takanawa in the Eastern Capital,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"9th month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 10 5/8 in. (36.2 x 27 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.148,false,true,73434,Asian Art,Print,Tōtō Takanawa Kaigen|Foreigners Riding Along the Coast at Takanawa in the Eastern Capital,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"9th month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 10 in. (36.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.140a–c,false,true,73426,Asian Art,Print,Yokohama Gankirō no zu|View of the Interior of the Gankirō Tea House in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"4th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/4 x 10 in. (37.5 x 25.4 cm) Image (b): 14 5/8 x 10 in. (37.1 x 25.4 cm) Image (c): 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.141a–c,false,true,73427,Asian Art,Print,Yokohama Gankirō age|横浜岩亀楼上|Upper Floor of the Gankirō Tea House in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"4th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/4 x 10 in. (37.5 x 25.4 cm) Image (b): 14 3/4 x 10 in. (37.5 x 25.4 cm) Image (c): 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.149a–c,false,true,73435,Asian Art,Print,Yokohama ijin kyaku no zu|Foreigner's Residence in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,"10th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 1/2 in. (35.6 x 24.1 cm) Image (b): 14 x 9 1/2 in. (35.6 x 24.1 cm) Image (c): 14 x 10 in. (35.6 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3305,false,true,55406,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,1863 (3rd month),1863,1863,Diptych of polychrome woodblock prints; ink and color on paper,,"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3334,false,true,55482,Asian Art,Print,Furansu-jin Yukyo|フランス人遊興|French Pastimes,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"1860 (Man–en, 1st year)",1860,1860,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 in. (36.8 x 25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.244,false,true,73535,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"7th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.245,false,true,73536,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"7th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.246,false,true,73537,Asian Art,Print,Igirisujin ryōkō no zu|Englishman Walking for Pleasure,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 13 7/8 x 10 in. (35.2 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.247,false,true,73538,Asian Art,Print,英吉利人之図|Illustration of English People (Igirisujin no zu),Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 13 7/8 x 9 5/8 in. (35.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.248,false,true,73539,Asian Art,Print,Orandajin no zu|Dutchman with Black Servant,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 9 3/4 in. (36.5 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.250,false,true,73541,Asian Art,Woodblock print,Chū tenjiku Maruka koku shūsshō daizō zu|Picture of an Elephant Born in Maruka in Central India,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"2nd month, 1863",1863,1863,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 1/2 in. (35.9 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2007.49.249a, b",false,true,73540,Asian Art,Woodblock print,Shintō hakurai no daizō|Newly Imported Great Elephant,Japan,Edo period (1615–1868),,,,Artist,,Ichiryūsai Yoshitoyo,"Japanese, 1830–1866",,Ichiryūsai Yoshitoyo,Japanese,1830,1866,"2nd month, 1863",1863,1863,Diptych of polychrome woodblock prints; ink and color on paper,"Image (a): 14 1/4 x 9 1/2 in. (36.2 cm, 24130 g) Image (b): 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm)","Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.8,false,true,58260,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,1853,1853,1853,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 × 9 3/4 in. (36.8 × 24.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.9,false,true,58261,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,1853,1853,1853,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 9 7/8 in. (37.5 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.237,false,true,73528,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,"4th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 9 3/4 in. (37.5 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.238,false,true,73529,Asian Art,Woodblock print,"歌川芳盛画 「鳥獣図會」豹と阿蘭陀婦人|“Dutchwoman with Leopard,” from the series Pictures of Birds and Animals (Chōjū zue)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,"7th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.239,false,true,73530,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,"11th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 9 7/8 in. (37.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.240,false,true,73531,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,"11th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.241,false,true,73532,Asian Art,Print,Amerikajin|Mounted American Woman,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.262,false,true,73553,Asian Art,Woodblock print,"「今昔未見 生物猛虎之真図」|Never Seen Before: True Picture of a Live Wild Tiger (Konjaku miken, Ikimono mōko no shinzu)",Japan,Edo period (1615–1868),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,sixth month 1860,1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3335,false,true,55483,Asian Art,Print,Roshiajin|Russians,Japan,Edo period (1615–1868),,,,Artist,,Kunihisa,"Japanese, 1832–1891",,Kunihisa,Japanese,1832,1891,1861 (10th month),1861,1861,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 in. (37.5 x 25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3181,false,true,55129,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,1860,1860,1860,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3184,false,true,53243,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,1861,1861,1861,Polychrome woodblock print; ink and color on paper,Oban 13 3/4 x 9 3/4 in. (34.9 x 24.8 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3197,false,true,55181,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,1865,1865,1865,Polychrome woodblock print; ink and color on paper,9 5/16 x 14 in. (23.6 x 35.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3682,false,true,45280,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,1864,1615,1868,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 in. (36.8 x 25.4 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3314a,false,true,55439,Asian Art,Woodblock print,亜米利加・南京|An American on Horseback and a Chinese with a Furled Umbrella,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,14 x 9 in. (35.6 x 22.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3314b,false,true,55440,Asian Art,Print,魯西亞 ・英吉利|An English Man and a Russian Woman,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.225,false,true,73516,Asian Art,Print,Sumo no homane|Sumo Wrestler Tossing a Foreigner,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 3/8 in. (36.2 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.226,false,true,73517,Asian Art,Print,Bijin zu|Beauties,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 9 3/8 in. (37.1 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.227,false,true,73518,Asian Art,Print,Oroshia|Russian Couple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.228,false,true,73519,Asian Art,Print,Oroshia|Chinese Servant and Frenchman,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 15 3/4 x 11 1/4 in. (40 x 28.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.231,false,true,73522,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"10th month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.232,false,true,73523,Asian Art,Print,Igirisu to korombojin|English Woman with Black Man,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"11th month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.233,false,true,73524,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"7th month, 1863",1863,1863,Polychrome woodblock print; ink and color on paper,Image: 13 x 8 5/8 in. (33 x 21.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.224a–c,false,true,73515,Asian Art,Print,Gok'koku o Gankirō sakamori no zu|The Five Nations Enjoying a Drunken Revel at the Gankirō Tea House,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"12th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/4 x 10 in. (37.5 x 25.4 cm) Image (b): 14 3/4 x 10 in. (37.5 x 25.4 cm) Image (c): 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.229a–c,false,true,73520,Asian Art,Print,万国男女人物図絵|Picture of Men and Women from all Nations (Bankoku danjo jinbutsu zue),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"4th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 3/4 in. (35.6 x 24.8 cm) Image (b): 14 x 9 3/4 in. (35.6 x 24.8 cm) Image (c): 14 x 9 3/4 in. (35.6 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.230a–c,false,true,73521,Asian Art,Print,Bankoku danjō jimbutsu zue|Picture of Men and Women from all nations (Bankoku danjo jinbutsu zue),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"4th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/4 x 10 in. (37.5 x 25.4 cm) Image (b): 14 3/4 x 10 in. (37.5 x 25.4 cm) Image (c): 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1094a,false,true,55029,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniaki II,"Japanese, 1835–1888",,Utagawa Kuniaki II,Japanese,1835,1888,"1862 (Bunkyū 2), 6th month",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 1/4 in. (36.2 × 23.5 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1094b,false,true,638607,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniaki II,"Japanese, 1835–1888",,Utagawa Kuniaki II,Japanese,1835,1888,"1862 (Bunkyū 2), 6th month",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 in. × 9 1/2 in. (35.6 × 24.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/638607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1094c,false,true,638609,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniaki II,"Japanese, 1835–1888",,Utagawa Kuniaki II,Japanese,1835,1888,"1862 (Bunkyū 2), 6th month",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 in. × 9 5/8 in. (35.6 × 24.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/638609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1094d,false,true,638610,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniaki II,"Japanese, 1835–1888",,Utagawa Kuniaki II,Japanese,1835,1888,"1862 (Bunkyū 2), 6th month",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 5/8 in. (36.2 × 24.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/638610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1094e,false,true,638611,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniaki II,"Japanese, 1835–1888",,Utagawa Kuniaki II,Japanese,1835,1888,"1862 (Bunkyū 2), 6th month",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/638611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1094f,false,true,638612,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniaki II,"Japanese, 1835–1888",,Utagawa Kuniaki II,Japanese,1835,1888,"1862 (Bunkyū 2), 6th month",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 1/16 × 9 5/8 in. (35.7 × 24.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/638612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1094g,false,true,638613,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniaki II,"Japanese, 1835–1888",,Utagawa Kuniaki II,Japanese,1835,1888,"1862 (Bunkyū 2), 6th month",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 × 9 5/8 in. (35.9 × 24.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/638613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3178,false,true,55125,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,ca. 1865,1855,1875,Polychrome woodblock print; ink and color on paper,9 7/16 x 13 7/16 in. (23.9 x 34.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.257,false,true,73548,Asian Art,Print,仏蘭西英吉利三兵大調錬之図|Maneuvers by Three Categories of French and English Soldiers (Furansu Igirisu sanhei ōchōren no zu),Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"8th month, 1867",1867,1867,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/2 x 28 3/16 in. (36.8 x 71.6 cm) Overall (mat): 19 x 32 in. (48.3 x 81.3 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.259,false,true,73550,Asian Art,Print,"騎兵体歩兵体大調練之図|Illustration of Cavalry, Infantry and Soldiers Retreating (Kiheitai, hoheitai, daichōren no zu)",Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"8th month, 1867",1867,1867,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/4 x 28 3/4 in. (36.2 x 73 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.348a–c,false,true,72826,Asian Art,Print,"武勇雪月花之内 ゑびらの梅|Plum Blossoms in the Forrest of Ikuta, from the series Bravery-Beauty of the four seasons (Buyū setsugekka no uchi-Ikuta no mori, Ebira no ume)",Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1867,1867,1867,Triptych of polychrome woodblock prints; ink and color on paper,Overall (a): 14 3/4 x 10 in. (37.5 x 25.4 cm) Overall (b): 14 9/16 x 10 in. (37 x 25.4 cm) Overall (c): 14 9/16 x 10 in. (37 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/72826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.351a–c,false,true,72829,Asian Art,Print,"太平記正清難戦之図|Masakiyo's Challenging Battle, from the series Taiheiki (Taiheiki, Masakiyo nansen no zu)",Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1866,1866,1866,Triptych of polychrome woodblock prints; ink and color on paper,Overall (a): 14 3/8 x 10 in. (36.5 x 25.4 cm) Overall (b): 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm) Overall (c): 14 9/16 x 10 in. (37 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/72829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.256a–c,false,true,73547,Asian Art,Print,仏蘭西大湊諸国交易図|Illustration of a Large French Port Trading with Many Nations (Furansukoku oominato shokoku kōeki zu),Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"4th month, 1866",1866,1866,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 1/2 in. (35.6 x 24.1 cm) Image (b): 14 x 9 1/4 in. (35.6 x 23.5 cm) Image (c): 14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.258a–c,false,true,73549,Asian Art,Print,仏蘭西英吉利三兵大調錬之図|Maneuvers by Three Categories of French and English Soldiers (Furansu Igirisu sanhei ōchōren no zu),Japan,Edo period (1615–1868),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"8th month, 1867",1867,1867,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 1/4 in. (36.8 x 23.5 cm) Image (b): 14 3/8 x 9 1/4 in. (36.5 x 23.5 cm) Image (c): 14 3/8 x 9 1/2 in. (36.5 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3101,false,true,45055,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudo Anchi,"Japanese, active 1714",,Kaigetsudo Anchi,Japanese,1714,1714,ca. 1714,1704,1724,Polychrome woodblock print (sumizuri-e); ink and color on paper,22 3/4 x 12 3/4 in. (57.8 x 32.4 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3106,false,true,56612,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudo Anchi,"Japanese, active 1714",,Kaigetsudo Anchi,Japanese,1714,1714,ca. 1714,1704,1724,Polychrome woodblock print; ink and color on paper,22 3/4 x 12 3/4 in. (57.8 x 32.4 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP142,false,true,36621,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1796,1786,1806,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP143,false,true,36622,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,20 x 8 1/2 in. (50.8 x 21.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP144,false,true,36623,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 5/8 in. (24.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP145,false,true,36624,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,13 3/4 x 9 7/8 in. (34.9 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP146,false,true,36625,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1796,1786,1806,One sheet of a hexaptych of polychrome woodblock prints; ink and color on paper,15 3/32 x 10 1/5 in. (38.3 x 25.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP147,false,true,36626,Asian Art,Print,歌撰恋之部 夜毎に逢恋|A Young Woman Reading A Letter,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 1/8 in. (36.8 x 25.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP148,false,true,36627,Asian Art,Woodblock print,女織蚕手業草 十二終|Women Weaving Silk Cloth,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,14 7/8 x 9 3/4 in. (37.8 x 24.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP149,false,true,36628,Asian Art,Print,三囲神社の御開帳|Display of Treasures at Mimeguri Shrine (Mimeguri jinja no onkaichō),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1799,1799,1799,Triptych of polychrome woodblock prints; ink and color on paper,15 3/8 x 9 7/8 x 10 in. (39.1 x 25.1 x 25.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP150,false,true,36629,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"Rat year, i.e. 1804",1804,1804,Triptych of polychrome woodblock prints; ink and color on paper,15 7/32 x 30 in. (38.7 x 76.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP151,false,true,36630,Asian Art,Print,蛍狩|Catching fireflies (Hotaru gari),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1796–97,1796,1797,Triptych of polychrome woodblock prints; ink and color on paper,Image (triptych): 13 7/8 x 28 5/8 in. (35.2 x 72.7 cm) Image (each sheet): 13 7/8 x 9 1/2 in. (35.2 x 24.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP152,false,true,36631,Asian Art,Woodblock print,"婦人手業操鏡|A Woman Weaving, Seated at a Hand Loom",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1796,1786,1806,Polychrome woodblock print; ink and color on paper,14 3/4 x 9 7/8 in. (37.5 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP153,false,true,36632,Asian Art,Print,実競色乃美名家見 三浦屋小紫 白井権八|The Lovers Miura-ya Komurasaki and Shirai Gonpachi.,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,probably 1797,1795,1799,Polychrome woodblock print; ink and color on paper,15 x 9 15/32 in. (38.1 x 24.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP555,false,true,37006,Asian Art,Print,江戸の花 娘浄瑠璃|A Woman Playing with a Young Boy,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1804,1794,1814,Polychrome woodblock print; ink and color on paper,14 3/5 x 9 29/32 in. (37.1 x 25.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP556,false,true,37007,Asian Art,Print,名君 閨中の粧ひ|The Oiran Yoso-oi Seated at Her Toilet,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1799,1789,1809,Polychrome woodblock print; ink and color on paper,14 3/5 x 9 29/32 in. (37.1 x 25.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP663,false,true,37110,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Two sheets of a hexaptych of polychrome woodblock prints; ink and color on paper,14 1/4 x 20 1/8 in. (36.2 x 51.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP735,false,true,37183,Asian Art,Print,新吉原仮宅両国之図|Picture of the Temporary Lodgings of the New Yoshiwara Pleasure Quarter at Ryōgoku (Shin Yoshiwara Karitaku Ryogoku no zu),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1784,1784,1784,Polychrome woodblock print; ink and color on paper,12 1/8 x 17 in. (30.8 x 43.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP736,false,true,37184,Asian Art,Print,扇屋内春日野わかな こてう|The Oiran Kasugano of Ōgiya on Parade,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 1/8 in. (30.8 x 13.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP737,false,true,37185,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Left-hand of a triptych of polychrome woodblock prints; ink and color on paper,H. 15 in. (38.1 cm); W. 10 1/4 in. (26 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP738,false,true,37186,Asian Art,Print,丁子屋内雛鶴|The Oiran Hinazuru of Chojiya Standing upon a Pile of Futon,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,12 1/8 x 8 1/2 in. (30.8 x 21.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP961,false,true,37269,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1789,1789,1789,Polychrome woodblock print; ink and color on paper,H. 8 1/4 in. (21 cm); W. 14 1/4 in. (36.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP962,false,true,37270,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1789,1789,1789,Polychrome woodblock print; ink and color on paper,H. 9 in. (22.9 cm); W. 14 5/8 in. (37.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP963,false,true,54854,Asian Art,Print,扇屋内春日野|The Oiran Kasugano of Ogiya on Parade under Blossoming Cherry Trees,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 10 in. (25.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP964,false,true,45250,Asian Art,Print,Mando|子供遊に和賀 万度|Lantern Float,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,early 1800s,1800,1833,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 7 in. (17.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP966,false,true,37272,Asian Art,Print,狂月坊|A Party of Merrymakers in a House in the Yoshiwara on a Moonlight Night,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1789,1789,1789,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 3/5 in. (23.2 x 37.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP967,false,true,37273,Asian Art,Print,狂月坊|The Palace in the Moon,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"8th month, 1789",1789,1789,Polychrome woodblock print; ink and color on paper,9 3/8 x 14 11/16 in. (23.8 x 37.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP968,false,true,37274,Asian Art,Print,狂月坊|The Full Moon at the Time of the Imo Harvest,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,"8th month, 1789",1789,1789,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 3/4 in. (23.5 x 37.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP969,false,true,37275,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1789,1789,1789,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 3/5 in. (23.5 x 37.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP970,false,true,37276,Asian Art,Woodblock print,"喜多川歌麿画 『男踏歌』 鶯の餌すり|“Preparing Food for the Warbler,” from the album Men’s Stamping Dance (Otoko dōka, uguisu no esa suri)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1798,1798,1798,Page from a woodblock printed book; ink and color on paper,8 1/2 x 14 in. (21.6 x 35.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP971,false,true,45473,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 7/8 in. (25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP973,false,true,54855,Asian Art,Print,"音曲比翼の番組 小波 力弥|Rikiya and Konami, from the series A Program with Music about Loving Couples (Ongyoku hiyoku no bangumi)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1798,1798,1798,Polychrome woodblock print; ink and color on paper,Aiban; H. 13 3/16 in. (33.5 cm); W. 9 in. (22.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP974,false,true,45009,Asian Art,Print,"『五色染六歌仙』 僧正遍昭|“The Poet Sōjō Henjō (816–890) Slipping a Letter into a Woman’s Sleeve,” from the series Five Colors of Love for the Six Poetic Immortals (Goshiki-zome rokkasen)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,H. 12 5/8 in. (32.1 cm); W. 8 1/2 in. (21.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP975,false,true,45012,Asian Art,Print,"『五色染六歌仙』 在原業平と小野小町|“The Poet Ariwara no Narihira (825–880) and Ono no Komachi,” from the series Five Colors of Love for the Six Poetic Immortals (Goshiki-zome rokkasen)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,H. 12 5/8 in. (32.1 cm); W. 8 1/2 in. (21.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP977,false,true,45013,Asian Art,Print,Ushi no Koku|青楼十二時 続 丑の刻|The Hour of the Ox (1 A.M.–3 A.M.),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 5/8 in. (24.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP978,false,true,54857,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Polychrome woodblock print; ink and color on paper,H. 15 1/2 in. (39.4 cm); W. 10 in. (25.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP979,false,true,54858,Asian Art,Print,山姥と金太郎|Yamauba Combing Her Hair and Kintoki,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1801,1791,1811,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 in. (25.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP980,false,true,45100,Asian Art,Print,山姥と金太郎|Yamauba and Kintoki,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 8 7/8 in. (22.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP981,false,true,54859,Asian Art,Print,山姥と金太郎|Yamauba Playing with the Young Kintoki,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1795,1805,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 9 in. (22.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP982,false,true,54860,Asian Art,Print,山姥と金太郎|Yamauba and Kintarō,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 9 3/8 in. (23.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP983,false,true,54869,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 7/8 in. (25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP984,false,true,37277,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1797,1787,1807,Triptych of polychrome woodblock prints; ink and color on paper,Overall: 15 1/8 x 30 in. (38.4 x 76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP985,false,true,54870,Asian Art,Print,絵兄弟|A Woman Dressing a Girl for a Kabuki Dance (E-kyodai),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1800,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 10 1/8 in. (25.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP986,false,true,54871,Asian Art,Print,二葉草七小町 清水小町|Kiyomizu Komachi,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1800,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 1/2 in. (24.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP987,false,true,54872,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,H. 13 in. (33 cm); W. 8 7/8 in. (22.5 cm); Diam. of circle 8 3/8 in. (21.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP988,false,true,45474,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 in. (25.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP989,false,true,54873,Asian Art,Print,吉原時計 夜の七つ|The Seventh Hour of the Night,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 9 1/4 in. (23.5 cm); W. 6 3/4 in. (17.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP990,false,true,45292,Asian Art,Print,江戸仕入大津土産|Young Woman with an Otsue Demon Dressed as an Itinerant Priest,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1804,1794,1814,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP991,false,true,45291,Asian Art,Print,"江戸仕入大津土産 槍持奴 鷹匠|Souvenir Paintings from Ōtsu, Stocked in Edo (Edo shi-ire Ōtsu miyage) Foot-soldier with a Spear and Hawk-handler (Yari mochi yakko to taka shō)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1802–3,1802,1803,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 1/2 in. (24.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP992,false,true,37278,Asian Art,Print,太閤五妻洛東遊観之図|A View of the Pleasures of the Taiko and His Five Wives at Rakutō,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1804,1804,1804,Triptych of polychrome woodblock prints; ink and color on paper,15 x 30 in. (38.1 x 76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP993,false,true,37279,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1805,1795,1815,Triptych of polychrome woodblock prints; ink and color on paper,14 3/8 x 29 in. (36.5 x 73.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP994,false,true,37280,Asian Art,Print,柿もぎ|Picking Persimmons ((Kaki mogi)),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1803–4,1793,1814,Triptych of polychrome woodblock prints; ink and color on paper,15 x 30 in. (38.1 x 76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP996,false,true,37281,Asian Art,Print,忠臣蔵三段目|Chushingura Act III,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 1/2 in. (26.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1043,false,true,37283,Asian Art,Woodblock print,"『画本虫撰』 「蜂」「毛虫」|Paper Wasp (Hachi); Hairy Caterpillar (Kemushi), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1044,false,true,37284,Asian Art,Woodblock print,"『画本虫撰』 「馬追虫」「むかて」|Katydid (Umaoi-mushi); Centipede, (Mukade), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1045,false,true,37285,Asian Art,Woodblock print,"『画本虫撰』 「けら」「はさみむし」|Mole Cricket (Kera); Earwig, (Hasami-mushi), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1046,false,true,37286,Asian Art,Woodblock print,"『画本虫撰』 「蝶」「蜻蛉」|Butterfly (Chō); Dragonfly (Kagerō or Tonbo), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 1/4 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1047,false,true,37287,Asian Art,Woodblock print,"『画本虫撰』 「虻」「芋虫」|Horsefly (abu); Green Caterpillar, imomushi, from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1048,false,true,37288,Asian Art,Woodblock print,"『画本虫撰』 「松虫」「虫蛍」|Tree cricket (Matsumushi); Firefly (Hotaru), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1049,false,true,37289,Asian Art,Woodblock print,"画本虫撰 バッタと蟷螂|Cone-headed Grasshopper or Locust, (batta); Praying Mantis (Tōrō or Kamakiri), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1050,false,true,37290,Asian Art,Print,"『画本虫撰』 「ひくらし」「くも」|Evening Cicada, Higurashi; Spider, Kumo, from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1051,false,true,37291,Asian Art,Woodblock print,"『画本虫撰』「赤蜻蛉」「いなこ」|Red Dragonfly (Akatonbo); Locust (Inago), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock-printed book; ink and color on paper,Overall: 10 1/2 x 7 1/4 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1052,false,true,37292,Asian Art,Woodblock print,"『画本虫撰』「虵」「とかけ」|Rat Snake (Hebi); Lizard or Skink (Tokage), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock-printed book; ink and color on paper,Overall: 10 1/2 x 7 1/4in. (26.7 x 18.4cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1053,false,true,37293,Asian Art,Woodblock print,"『画本虫撰』「蓑虫」「兜虫」|Bagworm (Minomushi); Horned Scarab Beetle (Kabutomushi), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock-printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1054,false,true,37294,Asian Art,Woodblock print,"『画本虫撰』「蝸牛」「轡虫」|Land Snail (Katatsumuri); Giant Katydid (Kutsuwamushi), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock-printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1055,false,true,37295,Asian Art,Woodblock print,"『画本虫撰』「きりきりす」「蝉」|Grasshopper (Kirigirisu); Cicada (Semi), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock-printed book; ink and color on paper,10 1/2 x 7 1/4 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1056,false,true,37296,Asian Art,Woodblock print,"『画本虫撰』「蚓」「こうろき」|Earthworm (Mimizu); Cricket (Kōrogi), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock-printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1058,false,true,37298,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 in. (36.8 x 22.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1059,false,true,37299,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 7/8 in. (23.2 x 37.8 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1060,false,true,37300,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 15/32 in. (23.2 x 36.8 cm),"Gift of Estate of Samuel Isham, 1915",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1061,false,true,37301,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,9 x 14 3/4 in. (22.9 x 37.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1062,false,true,37302,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 1/2 in. (23.2 x 36.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1063,false,true,37303,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 7/8 in. (23.2 x 37.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1064,false,true,37304,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 3/5 in. (23.2 x 37.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1072,false,true,37305,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 13/16 x 10 5/16 in. (17.3 x 26.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1073,false,true,37306,Asian Art,Woodblock print,"四季の花|The Coming Thunderstorm, from the illustrated book Flowers of the Four Seasons",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 7/8 x 9 7/8 in. (17.5 x 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1074,false,true,37307,Asian Art,Woodblock print,"四季の花|Women on a Bridge, from the illustrated book Flowers of the Four Seasons",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 3/4 × 9 7/8 in. (17.1 × 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1075,false,true,37308,Asian Art,Woodblock print,"四季の花|A Child Lighting Fireworks, from the illustrated book Flowers of the Four Seasons",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 3/4 × 9 7/8 in. (17.1 × 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1076,false,true,37309,Asian Art,Woodblock print,"四季の花|Girls Getting on Board a Boat, from the illustrated book Flowers of the Four Seasons",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 3/4 x 9 7/8 in. (17.1 x 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1077,false,true,37310,Asian Art,Woodblock print,"四季の花|Girls Picking Green Leaves, from the illustrated book Flowers of the Four Seasons",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 13/16 x 9 7/8 in. (17.3 x 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1078,false,true,37311,Asian Art,Woodblock print,"四季の花|New Year's Games, from the printed book Flowers of the Four Seasons (Shiki no hana)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 7/8 x 9 7/8 in. (17.5 x 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1079,false,true,37312,Asian Art,Woodblock print,"四季の花|Girls Entertained by Performers, from the illustrated book Flowers of the Four Seasons",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 7/8 x 9 7/8 in. (17.5 x 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1080,false,true,37313,Asian Art,Woodblock print,"四季の花|Poetry, from the illustrated book Flowers of the Four Seasons",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1801,1801,1801,Polychrome woodblock print; ink and color on paper,6 13/16 x 9 4/5 in. (17.3 x 24.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1111,false,true,37314,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1112,false,true,52009,Asian Art,Print,吾妻美人ゑらみ|A Tea-house Waitress,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Album of eighty-nine prints; ink and color on paper,H. 13 in. (33 cm); W. 14 in. (35.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/52009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1113,false,true,45096,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 9 1/2 in. (24.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1121,false,true,55043,Asian Art,Print,当世風俗通 女房風|Mother and Child,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790,1780,1810,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1122,false,true,37315,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1806–20,1806,1820,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1123,false,true,39647,Asian Art,Print,三勝と半七|Sankatsu and Hanshichi,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 24 1/2 in. (62.2 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1214,false,true,55137,Asian Art,Print,逢身八契 権八小紫の床の通気|Gonpachi ni Komurasaki no Toko no Tsuki,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1215,false,true,55138,Asian Art,Print,"『青楼七小町』 「玉屋内花紫」|“Hanamurasaki of the Tamaya,” from the series Seven Komachi of the Pleasure Quarters (Seirō Nana Komachi)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1272,false,true,55193,Asian Art,Print,青楼仁和嘉女芸者之部 扇売 団扇売 麦つき|The Niwaka Performers,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 3/16 in. (38.6 cm); W. 10 1/8 in. (25.7 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1277,false,true,55208,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1795,1795,1795,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1278,false,true,51137,Asian Art,Print,風俗美人時計 子ノ刻 妾|Midnight: Mother and Sleepy Child,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,14 3/8 x 9 5/8 in. (36.5 x 24.4cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1367,false,true,53689,Asian Art,Print,南国美人合|Courtesan Holding a Fan,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper 02_18_60---(Mica ground),14 1/2 x 9 5/16 in. (36.8 x 23.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1368,false,true,55318,Asian Art,Print,"『北国五色墨』「おいらん」|“High-Ranking Courtesan” (Oiran), from the series Five Shades of Ink in the Northern Quarter (Hokkoku goshiki-zumi),",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,Oban 14 3/4 x 9 3/4 in. (37.5 x 24.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1392,false,true,55399,Asian Art,Print,富本豊ひな|The Lady Tomimoto Toyohina Reading a Letter,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 3/8 in. (23.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1438,false,true,45099,Asian Art,Print,山姥と金太郎|Yamauba and Kintoki,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 1/16 in. (38.3 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1439,false,true,45015,Asian Art,Print,当世恋歌八契 お七と吉三郎|The Lovers Oshichi and Kichisaburo,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 9 3/4 in. (24.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1440,false,true,45016,Asian Art,Print,Gompachi Komurasaki no Toko no Tsuki|逢身八契 権八小紫の床の通気|Shared Feelings in the Bedchamber of Komurasaki and Gompachi,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 14 15/16 in. (37.9 cm); W. 10 3/16 in. (25.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1441,false,true,55485,Asian Art,Print,八百屋お七 寺小姓吉三郎|O Shichi and Kichisaburo,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 24 3/4 in. (62.9 cm); W. 5 5/16 in. (13.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1461,false,true,54862,Asian Art,Print,"名取酒六家選 兵庫屋華妻 坂上の剣菱|""Hanazuma of Hyōgoya, Kenbishi of Sakagami” from the series The Peers of Saké Likened to Select Denizens of Six Houses (Natori zake rokkasen: Hyōgoya Hanazuma, Sakagami no Kenbishi)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 9 1/2 in. (24.1 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1462,false,true,45098,Asian Art,Print,江戸の園花合 東屋の花|Azumaya no Hana,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,H. 13 1/16 in. (33.2 cm); W. 8 7/8 in. (22.5 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1478,false,true,55565,Asian Art,Print,娘日時計 申ノ刻|Seru no Koku,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 14 15/16 in. (37.9 cm); W. 10 1/4 in. (26 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55565,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1525,false,true,51093,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1800,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 9 5/8 in. (24.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1526,false,true,54865,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 7/8 in. (25.1 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1527,false,true,55636,Asian Art,Print,"「扇屋内花扇図」|The Courtesan Hanaōgi of the Ōgiya Brothel in Yoshiwara (Ōgiya uchi Hanaōgi, Yoshino, Tatsuta)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1793–94,1793,1794,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 13/16 in. (27.5 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1528,false,true,55639,Asian Art,Print,三婦艶|Three Beauties of the Kwansei Period,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 10 1/8 in. (25.7 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1657,false,true,55818,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1805,1795,1815,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 9 13/16 in. (24.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1658,false,true,55819,Asian Art,Print,藤棚下の遊女たち|Courtesans Beneath a Wisteria Arbor (Fuji dana shita no yūjo tachi),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 7/8 in. (27.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1659,false,true,55434,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1800,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 10 in. (25.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1660,false,true,44990,Asian Art,Print,実競色乃美名家見 紙屋次兵衛 紀ノ国屋小春|Jihei of Kamiya Eloping with Koharu of Kinokuniya,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,early 1800s,1800,1833,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 x 10 in. (34.9 x 25.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1661,false,true,45477,Asian Art,Print,行水|Bathtime (Gyōzui),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1801,1791,1811,Polychrome woodblock print; ink and color on paper,14 11/16 x 9 7/8 in. (37.3 x 25.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1662,false,true,45017,Asian Art,Print,婦女人相十品 相観歌麿考画|Woman with a Glass Noisemaker (Popen),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,early 1790s,1790,1794,Polychrome woodblock print; ink and color on paper,15 5/16 x 10 13/16 in. (38.9 x 27.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1664,false,true,37335,Asian Art,Print,当世美人三遊 芸妓|“Geisha” from the series Three Amusements of Contemporary Beauties (Tōsei bijin san’yū: Geigi),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,20 7/8 x 9 9/16 in. (53.0 x 24.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1665,false,true,55862,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1797,1787,1807,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 9 1/4 in. (23.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1666,false,true,37336,Asian Art,Print,雪の桟橋|Landing-stage in the Snow (Yuki no sanbashi),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1800,1800,1800,Polychrome woodblock print; ink and color on paper,H. 20 1/2 in. (52.1 cm); W. 7 7/16 in. (18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1667,false,true,55863,Asian Art,Print,青楼仁和嘉女芸者之部 唐人 獅子 角力|Seiro Niwaka Onna Geisha no Bu Tojin Shishi Sumo,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 15 1/8 in. (38.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1668,false,true,55867,Asian Art,Print,難波屋おきた|Okita of the Naniwa-ya Tea-house,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Polychrome woodblock print; ink and color on paper,H. 14 5/16 in. (36.4 cm); W. 9 1/2 in. (24.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1669,false,true,37337,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Polychrome woodblock print; ink and color on paper,Image: 8 7/8 in. × 14 in. (22.5 × 35.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1672,false,true,53653,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1793–94,1793,1794,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 3/16 in. (25.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1673,false,true,54864,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Polychrome woodblock print; ink and color on paper,H. 18 5/8 in. (47.3 cm); W. 7 5/8 in. (19.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1674,false,true,45479,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,20 3/4 x 7 5/8 in. (52.7 x 19.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1675,false,true,37338,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1794–95,1794,1795,Right-hand sheet of a diptych of polychrome woodblock prints; ink and color on paper,Image: 15 × 10 in. (38.1 × 25.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1676,false,true,53654,Asian Art,Print,"美人気量競 五明楼 花扇|“Hanaōgi of the Gomeirō,” from the series Comparing the Charms of Beauties (Bijin kiryō kurabe: Gomeirō Hanaōgi)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,H. 14 15/16 in. (37.9 cm); W. 9 11/16 in. (24.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1677,false,true,37339,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1791,1791,1791,Triptych of polychrome woodblock prints; ink and color on paper,7 7/16 x 14 3/5 in. (18.9 x 37.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1679,false,true,45018,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 24 7/8 in. (63.2 cm); W. 5 5/8 in. (14.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1685,false,true,53897,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Three sheets of a hexaptych of polychrome woodblock prints; ink and color on paper,15 x 29 3/4in. (38.1 x 75.6cm) Framed: 38 1/8 × 38 1/8 in. (96.8 × 96.8 cm) shares frame with JP1686,"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1686,false,true,37344,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Three sheets of a hexaptych of polychrome woodblock prints; ink and color on paper,15 x 29 3/4in. (38.1 x 75.6cm) Framed: 38 1/8 × 38 1/8 in. (96.8 × 96.8 cm) shares frame with JP1685,"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1687,false,true,55922,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1800,Triptych of polychrome woodblock prints; ink and color on paper,a: H. 14 7/8 in. (37.8 cm); W. 9 7/8 in. (25.1 cm) b: H. 15 in. (38.1 cm); W. 9 3/4 in. (24.8 cm) c: H. 14 5/16 in. (36.4 cm); W. 9 15/16 in. (25.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2393,false,true,56798,Asian Art,Print,忠臣蔵四段目|A Woman and a Man Arranging Flowers for the Tsukimi (Moon Festival),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1802,1802,1802,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 1/8 in. (38.4 x 25.7 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2395,false,true,56800,Asian Art,Print,玉屋内 志津賀|A Courtesan,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1807,1807,1807,Polychrome woodblock print; ink and color on paper,14 3/16 x 9 7/8 in. (36 x 25.1 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2730,false,true,55319,Asian Art,Print,婦女人相十品 相観|“Woman Holding Up a Parasol” from the series Ten Classes of Women’s Physiognomy (Fujo ninsō juppen: Higasa o sasu onna),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1792–93,1792,1793,Polychrome woodblock print; ink and color on paper,15 x 10 1/8 in. (38.1 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2731,false,true,52001,Asian Art,Print,女織蚕手業草 十|The Making of Silk Floss,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/52001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2732,false,true,37354,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1795,1795,1795,Right-hand sheet of a triptych of polychrome woodblock prints; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2734,false,true,45019,Asian Art,Print,Naniwaya Okita|Teahouse Waitress,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1793,1783,1803,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 7/8 in. (35.9 x 25.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2735,false,true,52002,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,15 3/8 x 10 1/8 in. (39.1 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/52002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2736,false,true,53656,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,15 x 9 3/4 in. (38.1 x 24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2737,false,true,37355,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790,1780,1800,Right-hand sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 7/8 x 9 3/4 in. (37.8 x 24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2738,false,true,57011,Asian Art,Print,実競色乃美名家見 紙屋次兵衛 紀ノ国屋小春|Jihei of Kamiya Eloping with the Geisha Koharu of Kinokuniya,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,15 1/8 x 9 5/8 in. (38.4 x 24.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2739,false,true,37356,Asian Art,Print,青楼仁和嘉女芸者之部 茶せん売 黒木売 さいもん|Three Niwaka Performers,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,15 x 9 3/4 in. (38.1 x 24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2740,false,true,37357,Asian Art,Print,契情三人酔 三幅之内 腹立上戸 泣上戸 笑上戸|Three Intoxicated Courtesans,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Triptych of polychrome woodblock prints; ink and color on paper,Each H. 12 7/8 in. (32.7 cm); W. 8 1/2 in. (21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2741,false,true,57013,Asian Art,Print,六玉川 扇屋内 花扇|The Oiran Hanaogi of Ogiya,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1796,1786,1806,Polychrome woodblock print; ink and color on paper,15 1/8 x 10 in. (38.4 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2809,false,true,57081,Asian Art,Print,Uguisu|風流小鳥合 鶯|Japanese Bush Warbler,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1754–1806,1754,1806,Polychrome woodblock print; ink and color on paper,8 3/4 x 6 1/4 in. (22.2 x 15.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2810,false,true,57080,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1615–1806,1615,1806,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 1/8 in. (37.1 x 25.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2811,false,true,54866,Asian Art,Print,児戯意乃三笑 恵恩芳子|Mother and Child,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 14 5/16 in. (36.4 cm); W. 9 9/16 in. (24.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2812,false,true,45020,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1794–95,1750,1850,Polychrome woodblock print; ink and color on paper,14 7/8 x 10 in. (37.8 x 25.4 cm),"Henry L. Phillips Collection; Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2813,false,true,56793,Asian Art,Print,"喜多川歌麿画 『美人花合』 「兵庫屋内 花妻図」|“The Courtesan Hanazuma Reading a Letter,” from the series Beauties Compared to Flowers (Bijin hana awase)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790s,1790,1799,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 10 1/4 in. (26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2814,false,true,45097,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,14 7/16 x 9 3/4 in. (36.7 x 24.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2853,false,true,37360,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1754–1806,1754,1806,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm) (including margins),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2854,false,true,37361,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1754–1806,1754,1806,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm) (including margins),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3018,false,true,51135,Asian Art,Print,姿見七人化粧|Naniwa Okita Admiring Herself in a Mirror,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1790–95,1790,1795,"Polychrome woodblock print; ink and color on paper, mica ground",14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3060,false,true,37364,Asian Art,Print,仮宅の後朝|Scene in the Yoshiwara,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1790,1790,1790,Triptych of polychrome woodblock prints; ink and color on paper,Triptych; each H. 14 3/4 in. (37.5 cm); W. 11 5/8 in. (29.5 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.85,false,true,39615,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1800,1790,1810,Polychrome woodblock print; mineral pigments and ink on paper,14 x 9 3/4 in. (35.6 x 24.8 cm); oban size 14 3/8 x 9 1/2 in. (36.5 x 24.1 cm),"Gift of Mr. and Mrs. Horace H. Wilson, 1996",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.463,false,true,40244,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1795,1785,1805,Ink on mulberry paper,Oban: 14 3/4 x 9 3/4 in. (37.5 x 24.8 cm),"Gift of John and Lili Bussel, 1996",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/40244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP1369a, b",false,true,37325,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1794–95,1784,1805,Diptych of polychrome woodblock prints; ink and color on paper,Image (each): 14 5/8 × 9 7/8 in. (37.1 × 25.1 cm) Image (diptych): 14 5/8 × 19 3/4 in. (37.1 × 50.2 cm) Framed: 24 1/4 × 30 3/4 in. (61.6 × 78.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP965a–c,false,true,37271,Asian Art,Print,琴棋書画図|The Four Elegant Accomplishments (Kin ki sho ga),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1788,1778,1798,Triptych of polychrome woodblock prints; ink and color on paper,15 x 30 in. (38.1 x 76.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1057a–c,false,true,37297,Asian Art,Woodblock print,"『画本虫撰』「蛙」「こかねむし」|Frog (Kaeru); Gold Beetle (Kogane mushi), from the Picture Book of Crawling Creatures (Ehon mushi erami)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,1788,1788,1788,Page from woodblock-printed book; ink and color on paper,10 1/2 x 7 7/32 in. (26.7 x 18.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1037,false,true,54322,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1835,1835,1835,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1038,false,true,54323,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Formerly attributed to,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1835,1835,1835,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 8 3/4 in. (20 x 22.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1039,false,true,54324,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1835?,1835,1835,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 in. (20 x 17.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1101,false,true,54330,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Diptych of Polychrome woodblock print (surimono); ink and color on paper,"8 3/16 x 7 1/8 in. (20.8 x 18.1 cm); dyptych, vertical","Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1107,false,true,54336,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1825,1815,1835,Polychrome woodblock print (surimono); ink and color on paper,8 x 6 15/16 in. (20.3 x 17.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1131,false,true,54338,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 3/8 in. (20.8 x 18.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1132,false,true,54339,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1133,false,true,54340,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 3/16 in. (20.5 x 18.3 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1134,false,true,54341,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 5/16 in. (21.1 x 18.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1135,false,true,54342,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1825,1815,1835,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 5/16 in. (21.1 x 18.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1136,false,true,54343,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 7/16 in. (21.1 x 18.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1137,false,true,54344,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 7/16 in. (21.1 x 18.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1138,false,true,54345,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 5/16 in. (20.3 x 18.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1139,false,true,54346,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 5/8 x 7 1/4 in. (21.9 x 18.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1231,false,true,54361,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/2 in. (21 x 19.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1232,false,true,54362,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 6 3/4 in. (21 x 17.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1233,false,true,54363,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1234,false,true,54364,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1810,1800,1820,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/16 in. (21 x 18.3 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1235,false,true,54365,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 5/16 in. (21.3 x 18.6 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1236,false,true,54366,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 7 1/8 in. (19.7 x 18.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1237,false,true,54367,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 3/16 in. (20.8 x 18.3 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1238,false,true,54369,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1239,false,true,54370,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 1/2 in. (14 x 19.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54370,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1250,false,true,54382,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1840,1840,1840,Polychrome woodblock print (surimono); ink and color on paper,7 1/8 x 6 13/16 in. (18.1 x 17.3 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1300,false,true,54388,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 9/16 x 7 3/8 in. (21.7 x 18.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1411,false,true,55441,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1838,1838,1838,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 14 15/16 in. (37.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1412,false,true,55442,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1838,1838,1838,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 14 15/16 in. (37.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1413,false,true,55443,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1838,1838,1838,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 14 13/16 in. (37.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1414,false,true,55444,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1838,1838,1838,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 14 7/8 in. (37.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1415,false,true,45266,Asian Art,Print,Osaka Tenmangu sairei no zu|The Tenmangu Festival at Osaka,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1834,1700,1868,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 in. (38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1452,false,true,54393,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/4 in. (20.8 x 18.4 cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1903,false,true,54442,Asian Art,Print,"『和歌三神』柿本人麻呂『春雨集』 摺物帖|Kakinomoto no Hitomaro (ca. 662–710), One of the Three Gods of PoetryFrom the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820s,1820,1829,Polychrome woodblock print (surimono); ink and color on paper,8 x 5 1/4 in. (20.3 x 13.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1904,false,true,54443,Asian Art,Print,"『和歌三神』衣通姫『春雨集』 摺物帖|Sotoori-hime (early 5th century), One of the Three Gods of PoetryFrom the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820s,1820,1829,Polychrome woodblock print (surimono); ink and color on paper,8 x 5 3/8 in. (20.3 x 13.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1905,false,true,54444,Asian Art,Print,"『和歌三神』山部赤人『春雨集』 摺物帖|Yamabe no Akahito (active 724–736), One of the Three Gods of PoetryFrom the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820s,1820,1829,Polychrome woodblock print (surimono); ink and color on paper,8 x 5 1/4 in. (20.3 x 13.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1940,false,true,54504,Asian Art,Print,"「一陽連文房四友 硯 伯英」『春雨集』 摺物帖|The Chinese Calligrapher Boying (Japanese: Hakuei; also known as the “Sage of Cursive Script”); “Inkstone” (Suzuri), from Four Friends of the Writing Table for the Ichiyō Poetry Circle (Ichiyō-ren Bunbō shiyū) From the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1827,1817,1837,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/8 in. (20.3 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1941,false,true,54508,Asian Art,Print,"「一陽連文房四友 筆 道風」『春雨集』 摺物帖|The Heian Court Calligrapher Ono no Tōfū (894–966); “Calligraphy Brush” (Fude), from Four Friends of the Writing Table for the Ichiyō Poetry Circle (Ichiyō-ren Bunbō shiyū)From the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1827,1817,1837,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/16 in. (20.3 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1942,false,true,54509,Asian Art,Print,"「一陽連文房四友 紙 女凢」『春雨集』 摺物帖 |Nuji (Japanese: Joki; female attendant who compiled writings by Daoist sages); “Paper” (Kami), from Four Friends of the Writing Table for the Ichiyō Poetry Circle (Ichiyō-ren Bunbō shiyū)From the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1827,1817,1837,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/16 in. (20.3 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1943,false,true,54510,Asian Art,Print,"「一陽連文房四友 墨 玄宗皇帝」『春雨集』 摺物帖 |Emperor Xuanzong (Japanese: Gensō) and Daoist Magician Lo Gongyuan Arising from an Inkstone; “Ink” (Sumi), from Four Friends of the Writing Table for the Ichiyō Poetry Circle (Ichiyō-ren Bunbō shiyū)From the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1827,1817,1837,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/16 in. (20.3 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1952,false,true,54520,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,probably 1819,1819,1819,Polychrome woodblock print (surimono); ink and color on paper,7 1/8 x 6 1/2 in. (18.1 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2029,false,true,54790,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1819,1819,1819,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/8 in. (20.5 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2047,false,true,54818,Asian Art,Print,"孫悟空|The Monkey King Songokū, from the Chinese novel Journey to the West",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,probably 1824,1824,1824,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 3/16 in. (20.6 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2049,false,true,54822,Asian Art,Woodblock print,玉藻前と三浦介|The Warrior Miura-no-suke Confronting the Court Lady Tamamo-no-mae as She Turns into an Evil Fox with Nine Tails,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,late 1820s,1826,1829,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2232,false,true,53998,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,probably 1828,1828,1828,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2339,false,true,54124,Asian Art,Print,三味線の調弦|Woman Tuning a Shamisen and a Cat Looking at its Own Reflection,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,mid- 1820s,1824,1826,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2352,false,true,54137,Asian Art,Print,"墨をする官女『春雨集』 摺物帖|Court Lady at Her Writing TableFrom the Spring Rain Collection (Harusame shū), vol. 3",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1820s,1820,1829,Part of an album of woodblock prints (surimono); ink and color on paper,4 15/16 x 11 1/8 in. (12.5 x 28.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2374,false,true,54158,Asian Art,Woodblock print,鯉の滝登り|Red Carp Ascending a Waterfall,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,late 1820s,1826,1829,Part of an album of woodblock prints (surimono); ink and color on paper,7 1/4 x 6 9/16 in. (18.4 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2426,false,true,54178,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1840,1830,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2427,false,true,54179,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1840,1830,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2428,false,true,54180,Asian Art,Woodblock print,"Kamakura no Koshi|The Filial Son at Kamakura, From the Book: Sasekishu",Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,ca. 1835,1825,1845,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2747,false,true,54206,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,first half of the 19th century,1800,1849,"Triptych of polychrome woodblock prints(surimono); gold, copper and silver on paper",Each print: 8 3/8 x 7 1/2 in. (21.3 x 19.1 cm),"Gift of Louis V. Ledoux, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2999,false,true,54215,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,1786–1868,1786,1868,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 7/16 in. (21.1 x 18.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2054,false,true,54829,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūgetsusai Shinkō,"Japanese, active 1810s",,Ryūgetsusai Shinkō,Japanese,1810,1819,probably 1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 7/16 in. (13.7 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2156,false,true,55063,Asian Art,Print,"『春雨集』 摺物帖柳月斎辰光 少女に鳥かご|Spring Rain Collection (Harusame shū), vol. 2: Young Woman with a Birdcage",Japan,Edo period (1615–1868),,,,Artist,,Ryūgetsusai Shinkō,"Japanese, active 1810s",,Ryūgetsusai Shinkō,Japanese,1810,1819,1810s,1810,1819,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 3/8 x 4 1/4 in. (21.3 x 10.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.132,false,true,76562,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Juyōdō Minekuni,"Japanese, active 1820s",,Juyōdō Minekuni,Japanese,1820,1829,1826,1826,1826,Polychrome woodblock print,Image (ôban tate-e): 14 5/8 x 10 1/8 in. (37.1 x 25.7 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.263,false,true,73554,Asian Art,Woodblock print,"「広影写生 両国の虎」|“The Tiger of Ryōkoku,” from the series True Scenes by Hirokage",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hirokage,"Japanese, active 1860s",,Utagawa Hirokage,Japanese,1860,1869,"8th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP817,false,true,37261,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1730,1720,1740,Monochrome woodblock print; ink and color on paper,9 x 12 in. (22.9 x 30.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP818,false,true,37262,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1728,1718,1738,Polychrome woodblock print; ink and color on paper (Urushi-e),Overall: 13 x 6in. (33 x 15.2cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1310,false,true,45044,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1728,1718,1738,Polychrome woodblock print (urushi-e); ink and color on paper,H. 12 5/16 in. (31.3 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1606,false,true,45046,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1730,1720,1740,Polychrome woodblock print (urushi-e); ink and color on paper,H. 12 in. (30.5 cm); W. 5 1/2 in. (14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1607,false,true,45047,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1730,1720,1740,Polychrome woodblock print (urushi-e); ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 3/4 in. (14.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1608,false,true,45048,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1730,1720,1740,Polychrome woodblock print (urushi-e); ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 6 1/16 in. (15.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1609,false,true,45049,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1730,1720,1740,Polychrome woodblock print (urushi-e); ink and color on paper,H. 12 7/16 in. (31.6 cm); W. 6 in. (15.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2757,false,true,45051,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1793,1783,1803,Tan-e (hand-colored print); ink and color on paper,H. 10 3/4 in. (27.3 cm); W. 6 in. (15.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3080,false,true,56593,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,ca. 1730,1720,1740,Polychrome woodblock print (hand colored); ink and color on paper,13 1/4 x 6 1/2 in. (33.7 x 16.5 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3002,false,true,54218,Asian Art,Print,"うし和歌春|Two Women, from the series Spring Poems on Ushiwaka for the Year of the Ox (Ushiwaka haru)",Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Sōri,active ca. 1797–1813,,Hishikawa Sōri,Japanese,1797,1813,1805,1805,1805,Polychrome woodblock print (surimono); ink and color on paper,5 5/16 x 6 11/16 in. (13.5 x 17 cm) (trimmed),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP172,false,true,36651,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunsen,"Japanese, 1762–ca.1830",,Katsukawa Shunsen,Japanese,1762,1830,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1351,false,true,55326,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunsen,"Japanese, 1762–ca.1830",,Katsukawa Shunsen,Japanese,1762,1830,1786,1786,1786,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 1/2 in. (14 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP173a–c,false,true,36482,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunsen,"Japanese, 1762–ca.1830",,Katsukawa Shunsen,Japanese,1762,1830,ca. 1785,1775,1795,Triptych of polychrome woodblock prints; ink and color on paper,Overall H. 12 1/4 in. (31.1 cm); W. 16 5/8 in. (42.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3179,false,true,55127,Asian Art,Print,『横浜異 人商館座敷之図』|Drawing Room of a Foreign Business Establishment in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"9th month, 1861",1861,1861,Center and right sheets of a triptych of polychrome woodblock prints; ink and color on paper,Image: 13 7/8 × 18 1/4 in. (35.2 × 46.4 cm) Mat: 18 3/4 × 23 1/4 in. (47.6 × 59.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3245,false,true,55257,Asian Art,Print,Yokohama Shukan Shin no zu|A True View of a Trading House of a Yokohama Merchant,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"1st month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,14 3/8 x 29 1/4 in. (36.5 x 74.3 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3246,false,true,55258,Asian Art,Print,Uchoren no zu|Great Military Drill,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"1866 (Keio 2, 2nd month)",1866,1866,Triptych of polychrome woodblock prints; ink and color on paper,13 5/8 x 27 7/16 in. (34.6 x 69.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3247,false,true,55259,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1861,1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Oban,"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3265,false,true,37389,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1861,1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Oban 14 1/4 x 28 5/8 in. (36.2 x 72.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3268,false,true,55332,Asian Art,Print,Amerika Karuhorunia Ko shuppan no zu|Sailing from a California Port,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1862,1862,1862,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/4 x 28 15/16 in. (36.2 x 73.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3298,false,true,55396,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,November 1860,1860,1860,Polychrome woodblock print; ink and color on paper,14 x 10 in. (35.6 x 25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3331,false,true,55479,Asian Art,Print,Yokohama torai Amerika shonin ryoko no zu|横浜渡來亜墨利加商人旅行之図|American Merchant Strolling in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1861 (1st month),1861,1861,Polychrome woodblock print; ink and color on paper,14 x 10 in. (35.6 x 25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3419,false,true,55614,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"1861 (Bunkyu 1, 1st month)",1861,1861,Polychrome woodblock print; ink and color on paper,14 x 9 7/8 in. (35.6 x 25.1 cm),"Gift of Lincoln Kirstein, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.101,false,true,73391,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 x 12 3/4 in. (24.1 x 32.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.102,false,true,73602,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 x 12 5/8 in. (24.1 x 32.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.103,false,true,73603,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 x 12 3/4 in. (24.1 x 32.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.104,false,true,73604,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 x 12 5/8 in. (24.1 x 32.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.105,false,true,73605,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 10 x 14 5/8 in. (25.4 x 37.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.106,false,true,73606,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 x 12 7/8 in. (24.1 x 32.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.107,false,true,73392,Asian Art,Print,「神名川横浜新開港圖」|“The Newly Opened Port of Yokohama in Kanagawa Prefecture”,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/2 x 29 in. (36.8 x 73.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.109,false,true,73394,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"3rd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.112,false,true,73397,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"3rd month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 7/8 x 10 in. (37.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.113,false,true,73398,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"3rd month,1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 10 1/4 x 14 3/4 in. (26 x 37.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.114,false,true,73399,Asian Art,Print,Doban-e Jōgyō shiki|Color Print of a Copperplate Picture of a Toy Shop,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1860,1860,1860,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 x 13 3/4 in. (24.1 x 34.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.116,false,true,73401,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"4th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 1/8 in. (36.8 x 25.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.118,false,true,73403,Asian Art,Print,"「生寫異國人物」|American Woman Playing a Concertina, from the series Life Drawings of People from Foreign Nations",Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1860,1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 10 in. (37.1 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.119,false,true,73405,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"11th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 10 in. (36.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.120,false,true,73406,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1860,1860,1860,Polychrome woodblock print; ink and color on pape22,Image: 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.121,false,true,73407,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"11th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.122,false,true,73408,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1861,1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.123,false,true,73409,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1861,1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.125,false,true,73411,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.128,false,true,73413,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"7th month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.132,false,true,73417,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"9th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/8 x 29 1/16 in. (35.9 x 73.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3389a–e,false,true,55564,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,April 1861,1861,1861,Oban pentaptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/8 x 49 13/16 in. (35.9 x 126.5 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.108a–c,false,true,73393,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/4 x 9 7/8 in. (37.5 x 25.1 cm) Image (b): 14 3/4 x 9 7/8 in. (37.5 x 25.1 cm) Image (c): 14 3/4 x 9 7/8 in. (37.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.110a–c,false,true,73395,Asian Art,Print,「横浜買物圖繒 唐物店之圖」|Curio Shop in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"3rd month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (b): 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm) Image (c): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.111a–c,false,true,73396,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"3rd month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/8 x 9 5/8 in. (35.9 x 24.4 cm) Image (b): 14 1/8 x 9 5/8 in. (35.9 x 24.4 cm) Image (c): 14 1/8 x 9 5/8 in. (35.9 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.115a–c,false,true,73400,Asian Art,Print,Yokohama Hon-chō...ni Miyozaki...kenkin zu|Detailed Print of Yokohama Hon-chō and the Miyozaki Pleasure Quarter,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"4th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 9 5/8 in. (36.5 x 24.4 cm) Image (b): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (c): 14 1/4 x 9 3/8 in. (36.2 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.117a–c,false,true,73402,Asian Art,Print,Kanagwa Yokohama minato...zue|Pictorial Guide to Yokohama Harbor,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"7th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm) Image (b): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (c): 14 1/4 x 9 3/8 in. (36.2 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.124a–f,false,true,73410,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"2nd month, 1861",1861,1861,Hexaptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (b): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm) Image (c): 14 5/8 x 10 in. (37.1 x 25.4 cm) Image (d): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm) Image (e): 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm) Image (f): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73410,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.126a–c,false,true,73572,Asian Art,Print,Gok'koku jimbutsu gyo...no zu|Picture of a Parade of the Five Nations,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"3rd month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (c): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.127a–e,false,true,73412,Asian Art,Woodblock print,「横浜交易西洋人荷物運送之圖」|“Yokohama Trade: Westerners Loading Cargo”,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"4th month, 1861",1861,1861,Pentaptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm) Image (b): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm) Image (c): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (d): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm) Image (e): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.129a–c,false,true,73414,Asian Art,Print,『横浜異 人商館写真之図』|Foreign Business Establishment in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,1861,1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 13 3/4 x 9 1/4 in. (34.9 x 23.5 cm) Image (b): 14 x 9 7/8 in. (35.6 x 25.1 cm) Image (c): 14 x 9 1/4 in. (35.6 x 23.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.130a–c,false,true,73415,Asian Art,Print,「横浜異人商館買場之圖」|Foreign Business Establishment in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"9th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (b): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (c): 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.131a–c,false,true,73416,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"9th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 3/4 in. (35.6 x 24.8 cm) Image (b): 14 x 9 3/8 in. (35.6 x 23.8 cm) Image (c): 14 x 10 7/8 in. (35.6 x 27.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.133a–c,false,true,73419,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"3rd month, 1862",1862,1862,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm) Image (b): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (c): 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.290a–c,false,true,73567,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,ca. 1862–63,1862,1863,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm) Image (b): 14 1/2 x 9 3/8 in. (36.8 x 23.8 cm) Image (c): 14 3/8 x 9 1/2 in. (36.5 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1490,false,true,55586,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūkōsai,active late 18th century,,Ryūkōsai,Japanese,1771,1799,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,12 13/16 x 5 11/16 in. (32.5 x 14.4 cm),"Gift of S. C. Bosch-Reitz, 1927",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP136,false,true,36615,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,26 23/32 x 4 3/4 in. (67.9 x 12.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP137,false,true,36616,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,26 1/8 x 4 3/4 in. (66.4 x 12.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP138,false,true,36617,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,26 3/8 x 4 3/5 in. (67.0 x 11.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP139,false,true,36618,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,27 15/32 x 4 3/8 in. (69.8 x 11.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP140,false,true,36619,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1775,1765,1785,Polychrome woodblock print; ink and color on paper,14 3/5 x 9 7/8 in. (37.1 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP141,false,true,36620,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,10 3/8 x 7 5/8 in. (26.4 x 19.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP220,false,true,36694,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,28 1/2 x 4 3/5 in. (72.4 x 11.7 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP558,false,true,37009,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,10 x 7 11/32 in. (25.4 x 18.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP559,false,true,37010,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1768,1758,1778,Polychrome woodblock print; ink and color on paper,10 x 7 7/32 in. (25.4 x 18.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP560,false,true,37011,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,10 x 7 1/8 in. (25.4 x 18.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP673,false,true,37120,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,27 x 4 1/8 in. (68.6 x 10.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP674,false,true,37121,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1790,Polychrome woodblock print; ink and color on paper,27 3/4 x 4 5/32 in. (70.5 x 10.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP700,false,true,37147,Asian Art,Print,"『雛形若菜の初模様 つるや内 すがたみ』|The Courtesan Sugatami of the Tsuruya Brothel, from the series “A Pat-tern Book of the Year’s First Designs, Fresh as Spring Herbs” (“Hinagata wakana no hatsu moyō”)",Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1777–78,1777,1778,Polychrome woodblock print; ink and color on paper,15 7/32 x 10 1/8 in. (38.7 x 25.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP881,false,true,54574,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 7 1/2 in. (19.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP882,false,true,54575,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1760,1750,1770,Polychrome woodblock print; ink and color on paper,Image: 8 5/8 × 8 1/4 in. (21.9 × 21 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP883,false,true,54577,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 5/8 in. (19.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP884,false,true,54578,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 27 1/5 in. (69.1 cm); W. 3 7/8 in. (9.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54578,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP885,false,true,54579,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca 1773–75,1763,1785,Polychrome woodblock print; ink and color on paper,H. 10 3/8 in. (26.4 cm); W. 7 5/8 in. (19.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP886,false,true,54580,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1780,1780,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 6 7/8 in. (22.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP887,false,true,54581,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 10 3/8 in. (26.4 cm); W. 7 5/8 in. (19.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP888,false,true,54582,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,H. 10 5/8 in. (27 cm); W. 7 5/8 in. (19.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP889,false,true,54583,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1773,1763,1783,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 5/8 in. (19.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP890,false,true,54584,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1773,1763,1783,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (26.7 cm); W. 7 1/2 in. (19.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP892,false,true,54586,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1773,1763,1783,Polychrome woodblock print; ink and color on paper,H. 8 3/8 in. (21.3 cm); W. 14 3/4 in. (37.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1125,false,true,55045,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,27 3/8 x 4 7/8in. (69.5 x 12.4cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1208,false,true,55132,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 25 9/16 in. (64.9 cm); W. 4 3/4 in. (12.1 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1209,false,true,55133,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 27 1/4 in. (69.2 cm); W. 4 5/8 in. (11.7 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1230,false,true,55151,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1765,1755,1775,Polychrome woodblock print; ink and color on paper,H. 10 3/16 in. (25.9 cm); W. 7 9/16 in. (19.2 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1271,false,true,55168,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 28 1/8 in. (71.4 cm); W. 5 in. (12.7 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1291,false,true,55233,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1770,1770,1770,Polychrome woodblock print; ink and color on paper,H. 27 3/4 in. (70.5 cm); W. 4 3/8 in. (11.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1459,false,true,55506,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,probably 18th century,1700,1799,Hand-colored print (ishizuri-e); ink and color on paper,29 15/16 x 10 1/4 in. (76 x 26 cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1703,false,true,55944,Asian Art,Print,"『雛形若菜初模様 玉や内 しら玉』|The Courtesan Shiratama of the Tamaya Brothel, from the series “A Pat-tern Book of the Year’s First Designs, Fresh as Spring Herbs” (“Hinagata wakana hatsu moyō”)",Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1777–78,1777,1778,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 10 1/2 in. (26.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2626,false,true,56775,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,10 1/2 x 7 1/4 in. (26.7 x 18.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2627,false,true,51996,Asian Art,Print,"『雛形若菜初模様 四ツ目屋内 にしき木』|The Courtesan Nishikigi of the Yotsumeya Brothel, from the series “A Pattern Book of the Year’s First Designs, Fresh as Spring Herbs” (“Hinagata wakana hatsu moyō”)",Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1776,1776,1776,Polychrome woodblock print; ink and color on paper,14 7/8 x 10 1/8 in. (37.8 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2628,false,true,56776,Asian Art,Print,"『雛形若菜初模様 四ツ目屋内 さよぎぬ』|The Courtesan Sayoginu of the Yotsumeya Brothel, from the series “A Pattern Book of the Year’s First Designs, Fresh as Spring Herbs” (“Hinagata wakana hatsu moyō”)",Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1776,1776,1776,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 3/8 in. (38.7 x 26.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2629,false,true,56777,Asian Art,Print,"『雛形若菜の初模様 つたや内 人まち』|The Courtesan Hitomachi of the Tsutaya Brothel, from the series “A Pat-tern Book of the Year’s First Designs, Fresh as Spring Herbs” (“Hinagata wakana no hatsu moyō”)",Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1777–78,1777,1778,Polychrome woodblock print; ink and color on paper,15 1/2 x 10 3/8 in. (39.4 x 26.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2630,false,true,56778,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,10 3/8 x 7 5/8 in. (26.4 x 19.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2786,false,true,57103,Asian Art,Print,Umemi Tsuki|Plum-Seeing Month: Second Month,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1735–1790,1735,1790,Polychrome woodblock print; ink and color on paper,10 3/8 x 7 3/4 in. (26.4 x 19.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2787,false,true,57102,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1735–1790,1735,1790,Polychrome woodblock print; ink and color on paper,10 1/4 x 7 5/8 in. (26 x 19.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2788,false,true,57101,Asian Art,Woodblock print,"『雛形若菜の初模様 扇屋内 七越』|The Courtesan Nanakoshi of the Ōgiya Brothel, from the series “A Pat-tern Book of the Year’s First Designs, Fresh as Spring Herbs” (“Hinagata wakana no hatsu moyō”)",Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1777–78,1777,1778,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/2 in. (38.7 x 26.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3408,false,true,55596,Asian Art,Print,"『雛形若菜の初模様 扇屋内 からうた』|The Courtesan Karauta of the Ōgiya Brothel, from the series “A Pattern Book of the Year’s First Designs, Fresh as Spring Herbs” (“Hinagata wakana no hatsu moyō”)",Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,1777–78,1777,1778,Polychrome woodblock print; ink and color on paper,15 x 10 1/4 in. (38.1 x 26 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1436,false,true,55470,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Haruji,"Japanese, active ca. 1770",,Suzuki Haruji,Japanese,1770,1770,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 27 13/16 in. (70.6 cm); W. 4 5/8 in. (11.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2456,false,true,56885,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Haruji,"Japanese, active ca. 1770",,Suzuki Haruji,Japanese,1770,1770,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,26 3/4 x 4 5/8 in. (67.9 x 11.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2624,false,true,56768,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu II,"Japanese, ca. 1702–1752",,Torii Kiyonobu II,Japanese,1702,1752,1735 or 1736,1735,1736,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 3/4 in. (30.8 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2625,false,true,56769,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu II,"Japanese, ca. 1702–1752",,Torii Kiyonobu II,Japanese,1702,1752,1747,1747,1747,Polychrome woodblock print; ink and color on paper,11 1/2 x 5 9/16 in. (29.2 x 14.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1486,false,true,55580,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kashosai Shunsen,"Japanese, died after 1830",,Kashosai Shunsen,Japanese,1830,1830,ca. 1810,1800,1820,Polychrome woodblock print; ink and color on paper,H. 7 13/16 in. (19.8 cm); W. 15 3/16 in. (38.6 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP972,false,true,45475,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okimura Toshinobu,"Japanese, active 1725–50",,Okimura Toshinobu,Japanese,1725,1750,ca. 1793,1783,1803,Tan-e (hand-colored print); ink and color on paper,Aiban; H. 13 3/16 in. (33.5 cm); W. 9 in. (22.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2658,false,true,56837,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishimura Shigenobu,"Japanese, active 1729–39",,Nishimura Shigenobu,Japanese,1729,1739,ca. 1738,1728,1748,Polychrome woodblock print; ink and color on paper,13 1/4 x 6 1/4 in. (33.7 x 15.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2756,false,true,57022,Asian Art,Print,Odari Kafu|Style of the Dancer,Japan,Edo period (1615–1868),,,,Artist,,Nishimura Shigenobu,"Japanese, active 1729–39",,Nishimura Shigenobu,Japanese,1729,1739,first half of 18th century,1700,1749,Tan-e print (hand-colored); ink and color on paper,11 7/8 x 6 in. (30.2 x 15.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2766,false,true,56671,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Yoshinobu,"Japanese, active 1748–63",,Yamamoto Yoshinobu,Japanese,1748,1763,ca. 1750,1740,1760,Beni-e (rouge) woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 1/2 in. (14 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP196,false,true,36673,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1792,1796,"Polychrome woodblock print; ink, color, white mica on paper",14 7/8 x 9 7/8 in. (37.8 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP197,false,true,36674,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,ca. 1794,1784,1804,Polychrome woodblock print; ink and color on paper,15 1/8 x 9 7/8 in. (38.4 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP732,false,true,37180,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–75,1794,1795,Middle sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image: 12 1/2 x 6 in. (31.8 x 15.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP733,false,true,37181,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,After,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,Probably late 1880s or early 1890s,1880,1899,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 1/8 in. (37.5 x 25.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP734,false,true,37182,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,After,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,Probably late 1880s or early 1890s,1880,1899,Polychrome woodblock print; ink and color on paper with mica ground,15 x 10 1/5 in. (38.1 x 25.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1495,false,true,37327,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",H. 14 1/2 in. (36.8 cm); W. 9 1/2 in. (24.1 cm),"Fletcher Fund, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1519,false,true,37328,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Possibly one sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 1/2 x 5 7/8 in. (31.8 x 14.9 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1520,false,true,37329,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,Image: 12 3/4 x 6 in. (32.4 x 15.2 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1521,false,true,37330,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",H. 14 1/2 in. (36.8 cm); W. 9 11/16 in. (24.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1522,false,true,37331,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,Polychrome woodblock print; ink and color on paper with mica ground,15 x 10 in. (38.1 x 25.4 cm),"Fletcher Fund, 1912",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1523,false,true,37332,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",14 3/5 x 9 3/5 in. (37.1 x 24.4 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1737,false,true,37347,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",14 1/8 x 9 7/16 in. (35.9 x 24.0 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2645,false,true,37348,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,12 7/8 x 5 31/32 in. (32.7 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2646,false,true,37349,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–75,1794,1975,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2647,false,true,37350,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,ca. 1794,1784,1804,"Polychrome woodblock print; ink and color on paper (hoso-e, Yellow ground)",12 31/32 x 6 11/32 in. (33 x 16.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2648,false,true,37351,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,Left-hand sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/4 x 5 31/32 in. (32.4 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2649,false,true,37352,Asian Art,Print,三代目市川高麗蔵の志賀大七|Kabuki Actor Ichikawa Komazō III as Shiga Daishichi in the Play A Medley of Tales of Revenge (Katakiuchi noriaibanashi),Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,"5th month, 1794",1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2650,false,true,37353,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",14 7/16 x 9 1/3 in. (36.7 x 23.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2822,false,true,37358,Asian Art,Print,三代目大谷鬼次の奴江戸兵衛|Kabuki Actor Ōtani Oniji III as Yakko Edobei in the Play The Colored Reins of a Loving Wife (Koi nyōbō somewake tazuna),Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,"6th month, 1794",1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",15 x 9 7/8 in. (38.1 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2823,false,true,37359,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",H. 15 in. (38.1 cm); W. 9 7/8 in. (25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3017,false,true,37363,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper; white mica ground,14 3/5 x 9 3/4 in. (37.1 x 24.8 cm) Oban,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3111,false,true,37365,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",10 x 14 3/8 in. (25.4 x 36.5 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3112,false,true,37366,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",H. 14 5/8 in. (37.1 cm); W. 9 7/8 in. (25.1 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3113,false,true,37367,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",H. 15 in. (38.1 cm); W. 9 15/16 in. (25.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3114,false,true,37368,Asian Art,Print,市川鰕蔵の竹村定之進|Kabuki Actor Ichikawa Ebizō (Ichikawa Danjūrō V) in the play The Colored Reins of a Loving Wife (Koi nyōbō somewake tazuna),Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,"5th month, 1794",1794,1794,Polychrome woodblock print; ink and color on paper with mica ground,14 15/32 x 9 3/5 in. (36.8 x 24.4 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37368,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3115,false,true,37369,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",H. 14 (35.6 cm); W. 9 in. (22.9 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3117,false,true,37371,Asian Art,Print,八代目守田勘弥の鴬の次郎作|Kabuki Actor Morita Kan’ya VIII as the Palanquin-Bearer in the Play A Medley of Tales of Revenge (Katakiuchi noriaibanashi),Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,"5th month, 1794",1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",15 x 10 in. (38.1 x 25.4 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3118,false,true,37372,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",15 1/8 x 10 in. (38.4 x 25.4 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3119,false,true,37373,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper; white mica ground,Image: 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3120,false,true,37374,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 31/32 in. (32.4 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3121,false,true,37375,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,12 31/32 x 5 31/32 in. (33 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3122,false,true,37376,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,Polychrome woodblock print; white mica ground; ink and color on paper,14 3/4 x 9 3/4 in. (37.5 x 24.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3123,false,true,37377,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 5/8 in. (31.4 x 14.3 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3124,false,true,37378,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,Image: 12 3/4 × 5 15/16 in. (32.4 × 15.1 cm) Mat: 22 3/4 × 15 1/2 in. (57.8 × 39.4 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3125,false,true,37379,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 3/4 in. (31.1 x 14.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3126,false,true,37380,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,Image: 12 1/2 x 6 in. (31.8 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3127,false,true,37381,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper,14 11/16 x 5 3/4 in. (37.3 x 14.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3129,false,true,37383,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794,1794,1794,"Polychrome woodblock print; ink, color, white mica on paper",12 x 8 1/2 in. (30.5 x 21.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3130,false,true,37384,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,One sheet of a pentaptych of polychrome woodblock prints; ink and color on paper,12 7/8 x 5 3/4 in. (32.7 x 14.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3131,false,true,37385,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Polychrome woodblock print; ink and color on paper with mica ground,12 11/32 x 5 3/4 in. (31.4 x 14.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP3128a, b",false,true,37382,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,1794–95,1794,1795,Diptych of polychrome woodblock prints; ink and color on paper,12 1/2 x 5 31/32 in. (31.8 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.127,false,true,76557,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunkōsai Hokushū,"Japanese, active 1808–32",,Shunkōsai Hokushū,Japanese,1808,1832,1822,1822,1822,Polychrome woodblock print,Image (ôban tate-e): 14 3/4 x 10 1/8 in. (37.5 x 25.7 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.128,false,true,76558,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunkōsai Hokushū,"Japanese, active 1808–32",,Shunkōsai Hokushū,Japanese,1808,1832,1821,1821,1821,Polychrome woodblock print,Image (ôban tate-e): 15 x 10 in. (38.1 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.130,false,true,76560,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunkōsai Hokushū,"Japanese, active 1808–32",,Shunkōsai Hokushū,Japanese,1808,1832,1825,1825,1825,Polychrome woodblock print,Image (ôban tate-e): 15 x 10 1/8 in. (38.1 x 25.7 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2011.126a, b",false,true,76556,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shunkōsai Hokushū,"Japanese, active 1808–32",,Shunkōsai Hokushū,Japanese,1808,1832,1822,1822,1822,Diptych of polychrome woodblock prints,Each sheet (ôban tate-e diptych): 15 x 10 in. (38.1 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.143,false,true,76573,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Sadahiro,"Japanese, active 1825–75",,Utagawa Sadahiro,Japanese,1825,1875,1841,1841,1841,Polychrome woodblock print,Image (chûban tate-e): 10 x 7 1/4 in. (25.4 x 18.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.147,false,true,76577,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hirosada,"Japanese, active 1825–75",,Utagawa Hirosada,Japanese,1825,1875,ca. 1852,1842,1862,Polychrome woodblock print,Image (chûban tate-e): 9 1/2 x 7 1/8 in. (24.1 x 18.1 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.146a–e,false,true,76576,Asian Art,Woodblock prints,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hirosada,"Japanese, active 1825–75",,Utagawa Hirosada,Japanese,1825,1875,ca. 1852,1842,1862,Nine polychrome woodblock prints,Each (chûban tate-e): 10 x 7 1/4 in. (25.4 x 18.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.144,false,true,76574,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunimasu,"Japanese, active 1830–52",,Utagawa Kunimasu,Japanese,1830,1852,ca. 1849,1839,1859,Polychrome woodblock print,Image (ôban tate-e): 15 1/4 x 10 in. (38.7 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.145,false,true,76575,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunimasu,"Japanese, active 1830–52",,Utagawa Kunimasu,Japanese,1830,1852,1850,1850,1850,Polychrome woodblock print,Image (chûban tate-e): 9 7/8 x 7 in. (25.1 x 17.8 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP661,false,true,37108,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotada,"Japanese, fl. ca. 1720–50",,Torii Kiyotada,Japanese,1720,1750,ca. early 1740s,1740,1745,Polychrome woodblock print; ink and color on paper,Oban: 17 1/8 x 25 3/8 in. (43.5 x 64.5 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP833,false,true,54487,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotada,"Japanese, fl. ca. 1720–50",,Torii Kiyotada,Japanese,1720,1750,ca. 1735,1725,1745,Polychrome woodblock print; ink and color on paper (Urushi-e),H. 11 in. (27.9 cm); W. 4 7/8 in. (12.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP834,false,true,54488,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotada,"Japanese, fl. ca. 1720–50",,Torii Kiyotada,Japanese,1720,1750,ca. 1735,1725,1745,Polychrome woodblock print; ink and color on paper (Urushi-e),H. 12 1/4 in. (31.1 cm); 6 3/8 in. (16.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1567,false,true,45035,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotada,"Japanese, fl. ca. 1720–50",,Torii Kiyotada,Japanese,1720,1750,ca. 1738,1728,1748,Polychrome woodblock print; ink and color on paper,H. 16 3/4 in. (42.5 cm); W. 25 in. (63.5 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3075,false,true,56555,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotada,"Japanese, fl. ca. 1720–50",,Torii Kiyotada,Japanese,1720,1750,ca. 1715,1705,1725,Woodblock print; ink and hand-painted color (tan-e) on paper,11 1/4 x 6 in. (28.6 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP198,false,true,36675,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,"5th month, 1736",1736,1736,Polychrome woodblock print; ink and color on paper,11 1/4 x 5 13/16 in. (28.6 x 14.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP230,false,true,36702,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1748,1738,1758,Polychrome woodblock print; ink and color on paper,10 1/2 x 5 17/32 in. (26.7 x 14.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP231,false,true,36703,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1748,1738,1758,Polychrome woodblock print; ink and color on paper,12 1/32 x 5 5/8 in. (30.6 x 14.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP232,false,true,36704,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,dated 1749,1749,1749,Polychrome woodblock print; ink and color on paper,10 17/32 x 5 1/2 in. (26.8 x 14.0 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP651,false,true,37101,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1705,1695,1715,Polychrome woodblock print; ink and color on paper,12 x 20 3/8 in. (30.5 x 51.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP652,false,true,37102,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1705,1695,1715,Polychrome woodblock print; ink and color on paper,21 7/8 x 12 3/4 in. (55.6 x 32.4 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP653,false,true,37103,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1713,1703,1723,Polychrome woodblock print; ink and color on paper,20 31/32 x 12 5/8 in. (53.3 x 32.1 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP654,false,true,37104,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1710,1700,1720,Polychrome woodblock print; ink and color on paper,20 3/4 x 12 5/8 in. (52.7 x 32.1 cm),"The Francis Lathorp Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP688,false,true,37135,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1730,1720,1740,Polychrome woodblock print; ink and color on paper,12 1/8 x 5 3/4 in. (30.8 x 14.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP689,false,true,37136,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1750,1740,1760,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 7/8 in. (31.8 x 14.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP690,false,true,37137,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1755,1745,1765,Polychrome woodblock print; ink and color on paper,11 3/8 x 5 7/32 in. (28.9 x 13.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP821,false,true,37265,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1730,1720,1740,Polychrome woodblock print; ink and color on paper (Urushi-e),13 1/10 x 6 1/8 in. (33.3 x 15.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP822,false,true,37266,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1735,1725,1745,Polychrome woodblock print; ink and color on paper (Urushi-e),Overall: 12 1/4 x 5 7/8in. (31.1 x 14.9cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP823,false,true,37267,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1745,1735,1755,Polychrome woodblock print; ink and color on paper,12 11/32 x 5 7/8 in. (31.4 x 14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP824,false,true,54478,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1745,1735,1755,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Ishma, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP825,false,true,54479,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1748,1738,1758,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP826,false,true,54480,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1748,1738,1758,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP827,false,true,54481,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1752,1742,1762,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1110,false,true,55035,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1745,1735,1755,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 1/2 in. (14 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2583,false,true,56728,Asian Art,Lacquer print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,"12th month, 1743 or 1st month, 1744",1743,1744,Urushi-e (lacquer) print,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2584,false,true,56729,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1734,1724,1744,Urushi-e (lacquer) print,11 1/2 x 5 3/4 in. (29.2 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2585,false,true,56730,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1741,1731,1751,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3072,false,true,45247,Asian Art,Print,"Kairaishi|The Actor Ichimura Takenojo VIII in the Role of a Puppeteer, showing Puppets to a Courtesan",Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1715,1705,1725,Monochrome woodblock print; ink on paper,11 x 16 in. (27.9 x 40.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3073,false,true,45052,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1712,1702,1722,"Polychrome ""tan-e"" woodblock print; ink and color on paper",12 1/4 x 6 in. (31.1 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3099,false,true,45053,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1712,1702,1722,Monochrome woodblock (tan-e) print; ink on paper,22 1/2 x 12 1/4 in. (57.2 x 31.1 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3100,false,true,45054,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,ca. 1716,1706,1726,Polychrome woodblock print (tan-e); ink and color on paper,23 1/4 x 12 1/4 in. (59.1 x 31.1 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3105,false,true,45059,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudō Doshin,"Japanese, active 1711–1736",,Kaigetsudō Doshin,Japanese,1711,1736,ca. 1714,1704,1724,Polychrome woodblock print (sumizuri-e); ink and color on paper,23 1/2 x 12 1/2 in. (59.7 x 31.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1311,false,true,55270,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kondo Katsunobu,"Japanese, active 1716–1736",,Kondo Katsunobu,Japanese,1716,1736,ca. 1730,1720,1740,Polychrome woodblock print (urushi-e); ink and color on paper,H. 11 3/4 in. (29.8 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3082,false,true,56595,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kiyonobu II,"Japanese, active 1720–1750",,Kiyonobu II,Japanese,1720,1750,1739,1739,1739,Polychrome woodblock print (hand-colored); ink and color on paper,12 x 6 in. (30.5 x 15.2 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP240,false,true,36712,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,12 1/32 x 5 5/8 in. (30.6 x 14.3 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP241,false,true,36713,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 17/32 in. (31.1 x 14.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP242,false,true,36714,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,probably 1770,1768,1772,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 7/8 in. (32.1 x 14.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP470,false,true,36921,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,11 3/5 x 5 1/2 in. (29.5 x 14 cm),"Gift of Frank Lloyd Wright, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP561,false,true,37012,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1770,1770,1770,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 5/8 in. (31.1 x 14.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP562,false,true,37013,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,"12th month, 1768",1768,1768,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 7/8 in. (32.1 x 14.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP563,false,true,37014,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1770 or 1771,1770,1771,Polychrome woodblock print; ink and color on paper,12 5/16 x 5 13/16 in. (31.3 x 14.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP701,false,true,37148,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,12 7/32 x 5 3/4 in. (31.1 x 14.6 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP702,false,true,37149,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,11 23/32 x 5 1/2 in. (29.8 x 14.0 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP907,false,true,51997,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 1/2 in. (14 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1116,false,true,55038,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 10 1/2 in. (26.7 cm); W. 5 1/2 in. (14 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1226,false,true,55148,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 7 3/16 in. (18.3 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1227,false,true,55149,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 11 1/4 in. (28.6 cm); W. 5 5/16 in. (13.5 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1279,false,true,55210,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1769,1769,1769,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 5/8 in. (14.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1361,false,true,55339,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1362,false,true,55341,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 11 7/16 in. (29.1 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1363,false,true,55342,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1764–71,1764,1771,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 5 7/8 in. (14.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1365,false,true,55346,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1460,false,true,55507,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,H. 11 3/4 in. (29.8 cm); W. 5 1/2 in. (14 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1544,false,true,55709,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 5/8 in. (14.3 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1545,false,true,55710,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 5 5/8 in. (14.3 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2407,false,true,56810,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1770,1760,1780,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 3/4 in. (31.1 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2408,false,true,56811,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,Hosoe; 12 x 5 3/4 in. (30.5 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2409,false,true,56812,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,Hosoe: 12 1/8 x 5 3/4 in. (30.8 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2410,false,true,56813,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1772,1762,1782,Polychrome woodblock print; ink and color on paper,Hosoe: 12 3/8 x 5 3/4 in. (31.4 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2411,false,true,56814,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1771,1761,1781,Polychrome woodblock print; ink and color on paper,Hosoe: 12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2412,false,true,56815,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,Hosoe: 12 3/4 x 6 in. (32.4 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2413,false,true,56816,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1769,1759,1779,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 5/8 in. (31.8 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2414,false,true,56817,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,Hosoe: 11 3/4 x 5 5/8 in. (29.8 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2415,false,true,56818,Asian Art,Print,"初世尾上菊五郎の戸無瀬|Onoe Kikugorō as Tonase, from Kanadehon Chūshingura (Kanadehon Chūshingura, Shosei Onoe Kikugorō no Tonase)",Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1773,1773,1773,Polychrome woodblock print; ink and color on paper,Hosoe: 11 3/4 x 5 3/4 in. (29.8 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2416,false,true,56819,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,ca. 1772,1762,1802,Polychrome woodblock print; ink and color on paper,Hosoe: 12 7/16 x 5 7/8 in. (31.6 x 14.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2789,false,true,57100,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1770,1770,1770,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2790,false,true,57099,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1723–1792,1723,1792,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 7/8 in. (31.8 x 14.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2791,false,true,57098,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1723–1792,1723,1792,Polychrome woodblock print; ink and color on paper,10 3/8 x 7 1/2 in. (26.4 x 19.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2792,false,true,57097,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1723–1792,1723,1792,Polychrome woodblock print; ink and color on paper,12 3/4 x 6 1/8 in. (32.4 x 15.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2793,false,true,57096,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,1723–1792,1723,1792,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 7/8 in. (32.4 x 14.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP849,false,true,54506,Asian Art,Print,Shō gatsu|The First Month,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP850,false,true,54507,Asian Art,Print,Inari-ko|The Second Month,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP851,false,true,54511,Asian Art,Print,Hina no Sekku|The Festival of Dolls (Third Month),Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP852,false,true,54522,Asian Art,Print,Shi Gatsu|The Fourth Month,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP853,false,true,54524,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP854,false,true,54526,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP855,false,true,54528,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP856,false,true,54531,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP857,false,true,54534,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP858,false,true,54536,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP859,false,true,54538,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP860,false,true,54539,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyomasa,"Japanese, active 1770–1790",,Ishikawa Toyomasa,Japanese,1770,1790,ca. 1767,1757,1777,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 3/8 in. (18.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2716,false,true,56984,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunzan,"Japanese, active 1782–1798",,Katsukawa Shunzan,Japanese,1782,1798,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,15 x 9 3/4 in. (38.1 x 24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1292,false,true,55234,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shotei Hokuju,"Japanese, active 1790–1820",,Shotei Hokuju,Japanese,1790,1820,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,Oban 9 5/8 x 14 1/4 in. (24.4 x 36.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1394,false,true,55400,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shotei Hokuju,"Japanese, active 1790–1820",,Shotei Hokuju,Japanese,1790,1820,ca. 1830,1820,1830,Polychrome woodblock print; ink and color on paper,H. 10 5/16 in. (26.2 cm); W. 15 1/4 in. (38.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1409,false,true,55424,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shotei Hokuju,"Japanese, active 1790–1820",,Shotei Hokuju,Japanese,1790,1820,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 14 1/2 in. (36.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2582,false,true,54196,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Kazan,"Japanese, active 1810–1823",,Ishikawa Kazan,Japanese,1810,1823,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,7 1/4 x 6 5/8 in. (18.4 x 16.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.142,false,true,76572,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Hasegawa Sadamasu,"Japanese, active 1830s–40s",,Hasegawa Sadamasu,Japanese,1830,1849,1841,1841,1841,Polychrome woodblock print,Image (chûban tate-e): 10 x 7 1/4 in. (25.4 x 18.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.136a–d,false,true,76566,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Hasegawa Sadamasu,"Japanese, active 1830s–40s",,Hasegawa Sadamasu,Japanese,1830,1849,1834,1834,1834,Tetraptych of polychrome woodblock prints,Each sheet (ôban tate-e tetraptych): 14 1/8 x 10 in. (35.9 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.11a–c,false,true,63378,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshifusa,"Japanese, active 1837–1860",,Utagawa Yoshifusa,Japanese,1837,1860,"1856, 2nd month",1856,1856,Triptych of polychrome woodblock prints; ink and color on paper,Oban tate-e; triptych (right): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) (middle): 10 3/8 x 9 3/4 in. (26.4 x 24.8 cm) (left): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP223,false,true,36697,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoshige,"Japanese, active 1802?–?1835",,Utagawa Toyoshige,Japanese,1802,1835,ca. 1830,1820,1850,Polychrome woodblock print; ink and color on paper,9 4/5 x 14 15/32 in. (24.9 x 36.8 cm),"Rogers Fund, 1917",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1370,false,true,55349,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoshige,"Japanese, active 1802?–?1835",,Utagawa Toyoshige,Japanese,1802,1835,ca. 1828,1818,1838,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 13 7/8 in. (35.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.5,false,true,63359,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoshige,"Japanese, active 1802?–?1835",,Utagawa Toyoshige,Japanese,1802,1835,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,"Oban tate-e, 15 1/2 x 10 3/4 in. (39.4 x 27.3 cm)","Purchase, Jack Greene Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2048,false,true,54820,Asian Art,Print,騎龍弁財天|Benzaiten (Goddess of Music and Good Fortune) Seated on a White Dragon,Japan,Edo period (1615–1868),,,,Artist,,Aoigaoka Keisei,"Japanese, active 1820s–1830s",,Aoigaoka Keisei,Japanese,1810,1840,1832,1832,1832,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 1/4 in. (21.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP176,false,true,36654,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1789,1779,1799,Middle sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 9/16 x 10 in. (37 x 25.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP940,false,true,54819,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,13 × 9 in. (33 × 22.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP941,false,true,54821,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1778,1768,1788,Polychrome woodblock print; ink and color on paper,13 × 9 in. (33 × 22.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP942,false,true,54825,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,9 11/16 × 7 3/4 in. (24.6 × 19.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP943,false,true,54827,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,14 11/16 × 9 7/8 in. (37.3 × 25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1042,false,true,54952,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1787,1777,1797,Triptych of polychrome woodblock prints; ink and color on paper,12 3/4 × 25 1/2 in. (32.4 × 64.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1515,false,true,55618,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1788,1778,1798,Polychrome woodblock print; ink and color on paper,15 3/8 × 10 3/16 in. (39.1 × 25.9 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1516,false,true,55622,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1790,1780,1800,Polychrome woodblock print; ink and color on paper,15 1/8 × 10 1/16 in. (38.4 × 25.6 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1517,false,true,55625,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1790,1790,1800,Polychrome woodblock print; ink and color on paper,15 1/8 × 10 1/8 in. (38.4 × 25.7 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1518,false,true,55626,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,14 1/2 × 9 3/4 in. (36.8 × 24.8 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1834,false,true,56119,Asian Art,Print,あやめ燈籠図|Three Women Enjoying Literary Pursuits,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. late 1780s,1785,1789,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 3/4 × 9 5/8 in. (37.5 × 24.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2660,false,true,56839,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1789,1779,1799,Polychrome woodblock print; ink and color on paper,14 3/8 × 10 in. (36.5 × 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2661,false,true,56840,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1789,1779,1799,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2662,false,true,56841,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,1787,1787,1787,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2665,false,true,56843,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1786,1776,1796,Polychrome woodblock print; ink and color on paper,15 x 30 in. (38.1 x 76.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2804,false,true,57086,Asian Art,Print,Ukiyoe Ga|The Beauty of the Floating World,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,1780–1795,1780,1795,Polychrome woodblock print; ink and color on paper,15 x 10 in. (38.1 x 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2805,false,true,57085,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,1780–1795,1780,1795,Polychrome woodblock print; ink and color on paper,14 3/4 x 9 7/8 in. (37.5 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2806,false,true,57084,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,1780–1795,1780,1795,Polychrome woodblock print; ink and color on paper,10 5/16 x 7 9/16 in. (26.2 x 19.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2807,false,true,57083,Asian Art,Print,Junigatsu|The Twelfth Month: December,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,1780–1795,1780,1795,Polychrome woodblock print; ink and color on paper,9 3/4 x 6 7/8 in. (24.8 x 17.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2808,false,true,57082,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,1780–1795,1780,1795,Polychrome woodblock print; ink and color on paper,15 1/2 x 10 3/8 in. (39.4 x 26.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP2664a, b",false,true,56842,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1789,1779,1799,Diptych of polychrome woodblock prints; ink and color on paper,Image (each): 14 7/8 × 10 in. (37.8 × 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP2803a, b",false,true,57087,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,ca. 1780–95,1780,1795,Diptych of polychrome woodblock prints; ink and color on paper,Image (each): 15 3/8 × 10 3/8 in. (39.1 × 26.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.261a–c,false,true,73552,Asian Art,Woodblock print,Daizō shasei|Big Elephants Being Attacked,Japan,Edo period (1615–1868),,,,Artist,,Isshinsai Yoshikata,"Japanese, active ca. 1841–64",,Isshinsai Yoshikata,Japanese,1841,1864,"2nd month, 1863",1863,1863,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (b): 14 1/2 x 9 5/8 in. (36.8 x 24.4 cm) Image (c): 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3183,false,true,55169,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,ca. 1861,1851,1871,Polychrome woodblock print; ink and color on paper,Oban 14 x 9 1/2 in. (35.6 x 24.1 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3185,false,true,55170,Asian Art,Print,Amerika jin Yuko Sakamori|An American Carousing,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1861 (Bunkyu 1, first month)",1861,1861,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3186,false,true,55171,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1860,1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 1/2 in. (35.6 x 24.1 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3243,false,true,55256,Asian Art,Print,"東都芝浦之風景|View of Shibaura, from the series Eastern Capital (Tōto, Shibaura no fūkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"6th month, 1863",1863,1863,Sheet from a triptych of polychrome woodblock prints; ink and color on paper,13 x 28 1/8 in. (33 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3248,false,true,55260,Asian Art,Print,Goko Kokujinmotsu-Don Taku no zu|View of Eating and Drinking by People of Five Countries,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1861, 12th month (?)",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,14 x 28 1/2 in. (35.6 x 72.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3311,false,true,55422,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,January 1861,1861,1861,Polychrome woodblock print; ink and color on paper,14 x 9 1/2 in. (35.6 x 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3330,false,true,55478,Asian Art,Print,五箇国人物呑託之図|People of the Five Nations,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1861,1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/4 x 29 1/2 in. (36.2 x 74.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3339,false,true,55493,Asian Art,Print,英吉利国倫敦図|Illustration of London in England (Igirisukoku rondon zu),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1866,1866,1866,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 29 5/8 x 14 9/16 in. (75.2 x 37 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3362,false,true,55530,Asian Art,Print,Orandajin|Dutchmen,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1860,1860,1860,Polychrome woodblock print; ink and color on paper,14 3/4 x 9 3/4 in. (37.5 x 24.8 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3368,false,true,55535,Asian Art,Print,Orandajin|A Dutch Couple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1863 (Bunkyu 3),1863,1863,Polychrome woodblock print; ink and color on paper,14 3/8 x 9 15/16 in. (36.5 x 25.2 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3369,false,true,55536,Asian Art,Print,英吉利人|Ingirisu-jin,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1863, 7th month (Bunkyu 3)",1863,1863,Polychrome woodblock print; ink and color on paper,14 1/8 x 10 in. (35.9 x 25.4 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3378,false,true,55545,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1870,1870,1870,Triptych of polychrome woodblock prints; ink and color on paper,13 3/4 x 27 1/2 in. (34.9 x 69.9 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.25,false,true,57985,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,ca. 1858,1848,1868,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/4 in. (34.3 × 23.5 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.715.10,false,true,63376,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1859, 2nd month",1859,1859,Polychrome woodblock print; ink and color on paper,"Obant tate-e, 14 5/8 x 10 1/4 in. (37.1 x 26 cm)","Purchase, Arnold Weinstein Gift, 2001",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/63376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.180,false,true,73467,Asian Art,Print,Porosiajin|A Prussian Couple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.181,false,true,73468,Asian Art,Print,Karajin|Two Chinese Women,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.182,false,true,73469,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.183,false,true,73470,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"12th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.184,false,true,73471,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.185,false,true,73472,Asian Art,Print,Nōgei no seiran|Returning Sails at Nōgei [American couple riding over the Nōgei Bridge],Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 3/4 in. (35.9 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.186,false,true,73473,Asian Art,Woodblock print,Michiyuki no embō|Evening Glow on a Traveling Drama [Chinese watching a Kabuki play],Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.187,false,true,73474,Asian Art,Print,Asa ichi no yuki|Snow at an Early Morning Market [Chinese shopping for vegetables],Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 9 3/8 in. (37.1 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.188,false,true,73475,Asian Art,Print,Motomura no yūdachi|Evening Glow at Motomura [Two Englishmen looking at the sunset],Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 3/4 in. (35.9 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.189,false,true,73476,Asian Art,Print,Miyozaki no shūgetsu|Autumn Moon at Miyozaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.190,false,true,73477,Asian Art,Print,Gankirō yoru no ame|Night Rain at Gankirō,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 10 in. (36.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.191,false,true,73478,Asian Art,Print,Hatoba no kihan|Returning Sails at the Wharves [American couple],Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 7/8 x 9 15/16 in. (37.8 x 25.2 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.192,false,true,73480,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 10 1/16 x 14 7/16 in. (25.6 x 36.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.193,false,true,73481,Asian Art,Woodblock print,Amerika|American Horseman,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.194,false,true,73482,Asian Art,Woodblock print,Amerika nyōjin|American Horsewoman,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.195,false,true,73483,Asian Art,Print,Oranda|Dutch Couple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 9 5/8 in. (36.5 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.196,false,true,73484,Asian Art,Print,Igirisu|Englishmen Dining,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.197,false,true,73485,Asian Art,Woodblock print,Igirisu nyōjin|Englishmen Woman on Horseback,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 1/16 in. (36.8 x 25.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.198,false,true,73486,Asian Art,Print,Amerika nyōjin|American Woman with Her Child on Stilts,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.199,false,true,73487,Asian Art,Print,Furansu nyōjin|French Housewife and Her Husband,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.203,false,true,73491,Asian Art,Print,Oranda|Dutch Printers,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 1/8 in. (37.5 x 25.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.205,false,true,73493,Asian Art,Print,Orosia|Mounted Russian,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"5th month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 × 10 in. (35.6 × 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.209,false,true,73499,Asian Art,Print,Tōsen no zu|Chinese Junk,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1862",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 10 1/4 in. (37.1 x 26 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.210,false,true,73500,Asian Art,Print,Igirisu fune|English Ship,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1862",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 3/4 in. (35.6 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.211,false,true,73501,Asian Art,Print,Kita Amerka fune no zu|North American Ship,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"4th month, 1862",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 1/8 in. (37.5 x 25.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.214,false,true,73504,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"3rd month, 1864",1864,1864,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 3/8 in. (36.2 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.215,false,true,73505,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"3rd month, 1864",1864,1864,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.216,false,true,73506,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"3rd month, 1864",1864,1864,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.217,false,true,73507,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"3rd month, 1864",1864,1864,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.222,false,true,73512,Asian Art,Print,「亜墨利加國」|“America”: Enjoying Hot Air Balloons,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1867,1867,1867,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 3/4 x 28 3/4 in. (37.5 x 73 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3275a–c,false,true,55357,Asian Art,Print,亜米利加国|American Balloon Ascension (Amerikakoku),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,1867,1867,1867,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 5/8 × 9 5/8 in. (37.1 × 24.4 cm) Image (b): 14 3/4 in. × 10 in. (37.5 × 25.4 cm) Image (c): 14 3/4 in. × 10 in. (37.5 × 25.4 cm) Mat: 22 7/8 in. × 37 in. (58.1 × 94 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2007.49.200a, b",false,true,73488,Asian Art,Print,Gaikokujin yūkyō no zu|Foreigners Enjoying a Party,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image (a): 14 3/8 x 10 in. (36.5 x 25.4 cm) Image (b): 14 1/4 x 19 1/4 in. (36.2 x 48.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2007.49.204a, b",false,true,73492,Asian Art,Print,Yokohama hatoba keshiki|A View of the Wharves in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1861",1861,1861,Diptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 9 3/4 in. (36.5 x 24.8 cm) Image (b): 14 5/8 x 9 3/4 in. (37.1 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.177a–c,false,true,73465,Asian Art,Print,"神奈川横浜港崎町遊女屋光景|View of the Miyozaki Brothel District in Yokohama, Kanagawa (Kanagwa Yokohama Miyozaki machi yūjoya kōkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"4th month, 1864",1864,1864,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 9 3/4 in. (36.5 x 24.8 cm) Image (b): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm) Image (c): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.178a–c,false,true,73466,Asian Art,Print,Kanagawa yōri Yokohama...ken no zu|A Distant View of Yokohama from Kanagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"5th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 9 5/8 in. (36.5 x 24.4 cm) Image (b): 14 5/8 x 9 3/4 in. (37.1 x 24.8 cm) Image (c): 14 1/4 x 10 1/8 in. (36.2 x 25.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.179a–c,false,true,73691,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"5th month, 1860",1860,1860,Triptych of polychrome woodblock prints; ink and color on paper,Image (d): 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm) Image (e): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (f): 14 1/8 x 9 7/8 in. (35.9 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.201a–c,false,true,73489,Asian Art,Print,"Bushu Yokohama gaikokujin yūkyō no zu|A View of the Amusements of the Foreigners in Yokohama, Bushu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (b): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (c): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.202a–c,false,true,73490,Asian Art,Print,"Bushu Yokohama gaikokujin yūkyō no zu|A View of the Amusements of the Foreigners in Yokohama, Bushu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (b): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (c): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.206a–c,false,true,73494,Asian Art,Print,Amerika Washinton fu|City of Washington in America,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"1st month, 1862",1862,1862,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (b): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (c): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.207a–c,false,true,73495,Asian Art,Print,Igirisu Rondon no kaiko|The Port of London England,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1862",1862,1862,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (b): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (c): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.208a–c,false,true,73497,Asian Art,Print,Daishin Nankin fu no shiō|Nankin in China,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1862",1862,1862,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 5/8 x 10 in. (37.1 x 25.4 cm) Image (b): 14 5/8 x 10 in. (37.1 x 25.4 cm) Image (c): 14 3/4 x 9 5/8 in. (37.5 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.212a–c,false,true,73502,Asian Art,Print,"Furansu Paris no fu|Paris, France",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"6th month, 1862",1862,1862,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 10 in. (36.2 x 25.4 cm) Image (b): 14 1/4 x 10 in. (36.2 x 25.4 cm) Image (c): 14 1/4 x 10 in. (36.2 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.213a–c,false,true,73503,Asian Art,Woodblock print,Tenjiku maru no zu|A View of Indian Elephants,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"6th month, 1863",1863,1863,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (b): 14 3/8 x 9 5/8 in. (36.5 x 24.4 cm) Image (c): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.218a–c,false,true,73508,Asian Art,Print,"万国名勝尽競之内魯西亜本都伯徳|Heidoru (St. Petersberg), Capital of Russia, from the series Famous Places from All Nations (Bankoku meisho zukushi - Oroshiya miyako heidoru)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1865",1865,1865,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 5/8 x 10 in. (37.1 x 25.4 cm) Image (b): 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm) Image (c): 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.219a–c,false,true,73509,Asian Art,Print,仏蘭西国|France (furansukoku),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1865",1865,1865,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 9 5/8 in. (36.5 x 24.4 cm) Image (b): 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm) Image (c): 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.220a–c,false,true,73510,Asian Art,Print,英吉利国|England (Igirisukoku),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"2nd month, 1865",1865,1865,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 13 3/4 x 9 3/4 in. (34.9 x 24.8 cm) Image (b): 13 1/2 x 9 1/2 in. (34.3 x 24.1 cm) Image (c): 13 3/4 x 9 3/4 in. (34.9 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.221a–c,false,true,73511,Asian Art,Print,"英吉利国倫敦図|Illustration of London, England (Igirisukoku rondon zu)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"3rd month, 1866",1866,1866,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (c): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3070,false,true,56551,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Sugimura Jihei Masataka,"Japanese, active ca. 1680–1698",,Sugimura Jihei Masataka,Japanese,1680,1698,ca. 1685,1675,1695,Monochrome woodblock print; ink on paper,10 3/4 x 15 1/2 in. (27.3 x 39.4 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2642,false,true,56823,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Morofusa,"Japanese, active ca. 1685–1715",(?),Hishikawa Morofusa,Japanese,1685,1715,early 18th century,1700,1733,Monochrome woodblock print (sumie); ink on paper,9 1/2 x 13 3/4 in. (24.1 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP467,false,true,36918,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyoshige,"Japanese, active ca. 1716–1759",,Torii Kiyoshige,Japanese,1716,1759,"2nd month, 1763",1763,1763,Polychrome woodblock print; ink and color on paper,11 7/8 x 5 7/16 in. (30.2 x 13.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2761,false,true,57025,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyoshige,"Japanese, active ca. 1716–1759",,Torii Kiyoshige,Japanese,1716,1759,1716–1759,1716,1759,Polychrome woodblock print; ink and color on paper,14 7/8 x 6 7/8 in. (37.8 x 17.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3083,false,true,56596,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hasegawa Mitsunobu,"Japanese, active ca. 1724–1754",,Hasegawa Mitsunobu,Japanese,1724,1754,ca. 1730,1720,1740,Monochrome woodblock print; ink on paper,10 1/2 x 15 in. (26.7 x 38.1 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3084,false,true,56597,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hasegawa Mitsunobu,"Japanese, active ca. 1724–1754",,Hasegawa Mitsunobu,Japanese,1724,1754,ca. 1730,1720,1740,Monochrome woodblock print; ink on paper,10 1/2 x 15 in. (26.7 x 38.1 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1309,false,true,45043,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tamura Sadanobu,"Japanese, active ca. 1725–1740",,Tamura Sadanobu,Japanese,1725,1740,ca. 1730,1720,1740,Polychrome lacquer print (urushi-e),H. 12 1/16 in. (30.6 cm); W. 6 in. (15.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP233,false,true,36705,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,"2nd month, 1746",1746,1746,Polychrome woodblock print; ink and color on paper,11 15/32 x 5 7/16 in. (29.1 x 13.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP840,false,true,54495,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1757,1747,1767,Polychrome woodblock print; ink and color on paper,H. 11 7/8 in. (30.2 cm); W. 5 9/16 in. (14.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP841,false,true,54496,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1756,1746,1766,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 1/2 in. (14 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP842,false,true,51089,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1756,1746,1766,Polychrome woodblock print (beni-e); ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP843,false,true,54497,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1756,1746,1766,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 3/4 in. (14.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1109,false,true,55033,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1754,1744,1764,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1449,false,true,55496,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1755,1745,1765,Polychrome woodblock print; ink and color on paper,H. 16 1/4 in. (41.3 cm); W. 11 1/4 in. (28.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2586,false,true,56732,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,"12th month, 1754 or 1st month, 1755",1754,1755,Polychrome woodblock print; ink and color on paper,12 1/2 x 5 3/4 in. (31.8 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2587,false,true,56733,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1754,1734,1754,Polychrome woodblock print; ink and color on paper,Overall: 11 7/8 x 5 5/8 in. (30.2 x 14.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2762,false,true,57026,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,1737–1766,1737,1766,Polychrome woodblock print; ink and color on paper,15 1/8 x 6 3/4 in. (38.4 x 17.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3089,false,true,56602,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,ca. 1755,1745,1765,Polychrome woodblock print; ink and color on paper,17 x 11 in. (43.2 x 27.9 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3090,false,true,56603,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,1756,1756,1756,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 3/4 in. (31.1 x 14.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3091,false,true,56604,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,1754,1754,1754,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3092,false,true,56605,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyohiro,"Japanese, active ca. 1737–1766",,Torii Kiyohiro,Japanese,1737,1766,1757,1757,1757,Polychrome woodblock print; ink and color on paper,15 x 7 in. (38.1 x 17.8 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1562,false,true,55729,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Masafusa,"Japanese, active ca. 1750–1770",,Masafusa,Japanese,1750,1770,ca. 1750–70,1750,1770,Polychrome woodblock print; ink and color on paper,H. 11 1/2 in. (29.2 cm); W. 6 in. (15.2 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1503,false,true,53894,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotsune,"Japanese, active ca. 1757–1779",,Torii Kiyotsune,Japanese,1757,1779,ca. 1768,1758,1778,Left sheet of a triptych of polychrome woodblock prints; ink and color on paper,12 3/8 x 5 3/8 in. (31.4 x 13.7 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3096,false,true,56609,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotsune,"Japanese, active ca. 1757–1779",,Torii Kiyotsune,Japanese,1757,1779,1767,1767,1767,Polychrome woodblock print; ink and color on paper,12 x 5 1/2 in. (30.5 x 14 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2785,false,true,57104,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Haruhiro,"Japanese, active ca. 1765–1784",,Haruhiro,Japanese,1765,1784,1765–1784,1765,1784,Polychrome woodblock print; ink and color on paper,11 3/8 x 8 3/16 in. (28.9 x 20.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP187,false,true,36665,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1797,1787,1807,Pentaptych of polychrome woodblock prints; ink and color on paper,15 5/8 x 9 5/8 in. (39.7 x 24.46 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP739,false,true,37187,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,12 3/4 x 8 5/8 in. (32.4 x 21.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP953,false,true,54847,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 7 1/4 in. (18.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP954,false,true,54848,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1791,1781,1801,Polychrome woodblock print; ink and color on paper,H. 9 3/16 in. (23.3 cm); W. 7 1/16 in. (17.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP955,false,true,54849,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 8 3/8 in. (21.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1212,false,true,55135,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 12 5/8 in. (32.1 cm); W. 8 5/8 in. (21.9 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1305,false,true,55244,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkaku,"Japanese, active ca. 1789–1801",,Katsukawa,Japanese,1789,1801,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 9/16 in. (14.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1496,false,true,45224,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 15 1/2 in. (39.4 cm); W. 10 1/4 in. (26 cm),"Fletcher Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1788,false,true,45216,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 9 1/2 in. (24.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2419,false,true,45223,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,15 3/8 x 10 1/4 in. (39.1 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP557,false,true,37008,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ichirakutei Eisui,"Japanese, active ca. 1793–1801",,Ichirakutei Eisui,Japanese,1793,1801,ca. 1797,1787,1807,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 1/8 in. (36.2 x 23.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1794,false,true,56096,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ichirakutei Eisui,"Japanese, active ca. 1793–1801",,Ichirakutei Eisui,Japanese,1793,1801,1790s,1790,1790,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 15/16 in. (25.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1280,false,true,55212,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Momokawa Shiko II,"Japanese, active ca. 1797–1810",,Momokawa Shiko II,Japanese,1797,1810,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 7/8 in. (25.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1481,false,true,55568,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Momokawa Shiko II,"Japanese, active ca. 1797–1810",,Momokawa Shiko II,Japanese,1797,1810,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 5 9/16 in. (14.1 cm); W. 12 3/8 in. (31.4 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1140,false,true,54347,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 1/2 in. (21.6 x 19.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1141,false,true,54348,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 1/8 in. (14.1 x 18.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1248,false,true,54379,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1830,1820,1840,Polychrome woodblock print (surimono); ink and color on paper,8 5/8 x 7 5/8 in. (21.9 x 19.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1410,false,true,55427,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1820,1810,1830,Polychrome woodblock print; ink and color on paper,9 15/16 x 14 1/2 in. (25.2 x 36.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1906,false,true,54445,Asian Art,Print,"舞楽衣装『春雨集』 摺物帖|Costume for Bugaku Court DanceFrom the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1818 (Year of the Tiger),1818,1818,Polychrome woodblock print (surimono); ink and color on paper,7 5/8 x 6 7/16 in. (19.4 x 16.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1908,false,true,54447,Asian Art,Print,"柳々居辰斎画 子春 嫁入の具『春雨集』 摺物帖|Accoutrements for a BrideFrom the Spring Rain Collection (Harusame shū), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1816 (Year of the Rat),1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1913,false,true,54452,Asian Art,Print,"「楽器其三」|Biwa with Brocade Cover, from the series Musical Instruments",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1808,1808,1808,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1946,false,true,54514,Asian Art,Print,"文具一式|Writing Set and Poem Card Box (Shikishi-bako), from Spring Rain Surimono Album (Harusame surimono-jō), vol. 1",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1805–10,1800,1820,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,4 15/16 x 6 13/16 in. (12.5 x 17.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1947,false,true,54515,Asian Art,Print,"衝立、虎図、文具一式|Desk Screen, Writing Set, Painting of Tiger, and Mounting Paraphernalia, from Spring Rain Surimono Album (Harusame surimono-jō, vol. 1)",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1818,1818,1818,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1956,false,true,54527,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1813,1813,1813,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2023,false,true,54772,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2024,false,true,54784,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1808,1808,1808,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 7/16 in. (14.1 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2036,false,true,54804,Asian Art,Print,"『春雨集』 摺物帖柳々居辰斎画 紅梅|Spring Rain Collection (Harusame shū), vol. 1: Plum Tree in Bloom",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1805–10,1805,1810,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,8 1/4 x 5 1/2 in. (21 x 14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2039,false,true,54810,Asian Art,Print,"『春雨集』 摺物帖柳々居辰斎画 蟹と蓮華|Spring Rain Collection (Harusame shū), vol. 1: Crabs and Lotus Blossoms",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1805–10,1805,1810,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2040,false,true,54811,Asian Art,Woodblock print,"摺物帖 『春雨集』 鴨と葱|Spring Rain Collection (Harusame shū), vol. 1: Duck and Scallions",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1810s,1810,1819,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,5 5/8 x 7 9/16 in. (14.3 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2043,false,true,54814,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",(?),Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2068,false,true,54898,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1819,1819,1819,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2108,false,true,54960,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1819,1819,1819,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/16 in. (20.2 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2110,false,true,54962,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1815,1815,1815,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 5/16 in. (14.3 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2135,false,true,54990,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 7/16 in. (14.3 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2136,false,true,54991,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1816,1816,1816,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 1/2 in. (14.3 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2153,false,true,55058,Asian Art,Print,"『春雨集』 摺物帖柳々居辰斎画 鎌倉の鶴岡八幡宮に鶴|Spring Rain Collection (Harusame shū), vol. 2: Cranes at Tsurugaoka Hachimangō Shrine in Kamakura",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1810s,1810,1819,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,5 1/2 x 7 1/4 in. (14 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2196,false,true,55112,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1810,1810,1810,Polychrome woodblock print (surimono); ink and color on paper,4 1/8 x 5 11/16 in. (10.5 x 14.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2256,false,true,54025,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1816,1800,1900,Part of an album of woodblock prints (surimono); ink and color on paper,5 5/8 x 7 9/16 in. (14.3 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2257,false,true,54026,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1815,1815,1815,Part of an album of woodblock prints (surimono); ink and color on paper,5 5/8 x 7 9/16 in. (14.3 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2295,false,true,54068,Asian Art,Print,"鉢梅、懐中日時計、羅針盤|Bonsai Plum, Compass, and Pocket Sundial with Design of Calendar, from Spring Rain Surimono Album (Harusame surimono-jō, vol. 3)",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1806,1806,1806,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,5 1/2 x 7 1/4 in. (14 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2296,false,true,54069,Asian Art,Woodblock print,"『蝶揃・寵愛』帯と簪|Roll of Cloth for an Obi and Tortoise-shell Hair Ornaments (“Presents for One’s Beloved”), from the Butterfly Series, from Spring Rain Surimono Album (Harusame surimono-jō, vol. 3)",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1805–10,1800,1820,Privately published polychrome woodblock prints (surimono) mounted in an album; ink and color on paper,5 7/8 x 7 9/16 in. (14.9 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2337,false,true,54122,Asian Art,Woodblock print,「牛和歌十二段矢矧長者」|Dance Robe and Koto (Zither) Representing the Wealthy Man of Yahagi from the Jōruri Play Ushiwaka (Minamoto no Yoshitsune),Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,probably 1810,1810,1810,Part of an album of woodblock prints (surimono); ink and color on paper,5 9/16 x 7 3/8 in. (14.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2341,false,true,54126,Asian Art,Print,"「職人合香聞 」|Utensils for the Incense Ceremony, “Incense Master” (Kōgiki), from the series An Array of Artisans",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,ca. 1810s,1805,1820,Part of an album of woodblock prints (surimono); ink and color on paper,5 11/16 x 7 9/16 in. (14.4 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2344,false,true,54129,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1811,1811,1811,Part of an album of woodblock prints (surimono); ink and color on paper,4 1/6 x 7 7/16 in. (10.6 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3000,false,true,54216,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1750–1835,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3004,false,true,54220,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,1750–1835,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 7/16 in. (20.8 x 18.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.5,false,true,58257,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Toyohara Sadatora,"Japanese, active ca. 1818–1844",,Toyohara Sadatora,Japanese,1818,1844,first half of the 19th century,1818,1844,Polychrome woodblock print; ink and color on paper,Image: 15 in. × 10 1/8 in. (38.1 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3264,false,true,37388,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,ca. 1861,1851,1871,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/2 in. × 30 in. (36.8 × 76.2 cm) Mat: 20 3/4 in. × 37 in. (52.7 × 94 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3303,false,true,53701,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1861,1861,1861,Polychrome woodblock print; ink and color on paper,H. 14 in. 35.6 cm); W. 9 1/2 in. ( 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3327,false,true,55475,Asian Art,Print,Furansukoku|仏蘭西国|France,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1861 (10th month),1861,1861,Polychrome woodblock print; ink and color on paper,13 3/4 x 9 5/8 in. (34.9 x 24.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3333,false,true,55481,Asian Art,Print,Gaikokujin kodomo choai no zu|A Foreigner Enjoying Her Children,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1860 (10th month),1860,1860,Polychrome woodblock print; ink and color on paper,13 1/2 x 9 in. (34.3 x 22.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3344,false,true,55508,Asian Art,Print,Amerika Koku Jokisen naka no zu|View Inside an American Steamship,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"1861 (Bunkyu, 1st year, 4th month)",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 10 1/4 x 27 1/4 in. (26 x 69.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3363,false,true,55531,Asian Art,Woodblock print,Gaikokujin ifuku shitate no zu|Picture of a Foreigner Making Clothes,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1860,1860,1860,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 x 9 in. (34.3 x 22.9 cm),"Gift of Lincoln Kirstein, 1970",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3365,false,true,53596,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,dated 1861,1800,1899,Polychrome woodblock print; ink and color on paper,14 x 9 1/4 in. (35.6 x 23.5 cm),"Gift of Lincoln Kirstein, 1970",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3366,false,true,55533,Asian Art,Print,Igirisujin|Englishmen,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1861,1861,1861,Polychrome woodblock print; ink and color on paper,14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3323a,false,true,55469,Asian Art,Print,亜墨利加|American Family with a Dancing Daughter,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"1861 (Bunkyu 1, 2nd month)",1861,1861,Polychrome woodblock print; ink and color on paper,Oban tate-e,"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3323b,false,true,55471,Asian Art,Print,仏蘭西|French Photographer with His Wife,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"1861 (Bunkyu 1, 2nd month)",1861,1861,Polychrome woodblock print; ink and color on paper,Oban tate-e,"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.152,false,true,73438,Asian Art,Print,"Gaikokujin Sake no zu|A Foreigner's Wine Party (Gaikokujin shuen no zu), from an untitled series of foreigners at home",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"10th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 13 3/8 x 9 in. (34 x 22.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.153,false,true,73439,Asian Art,Print,Ijiin Yashiki ryōri no zu|Inside a Foreign Restaurant,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"10th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 x 9 in. (34.3 x 22.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.154,false,true,73440,Asian Art,Print,Gaikokujin yoru benkyo no zu|Foreigners Studying at Night,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1861,1861,1861,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 x 9 in. (34.3 x 22.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.155,false,true,73441,Asian Art,Print,Gaikoku shashin kagami no zu|Foreigners Employing a Camera,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"11th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 13 1/4 x 8 7/8 in. (33.7 x 22.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.156,false,true,73442,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1860,1860,1860,Polychrome woodblock print; ink and color on paper,Image: 13 1/4 x 8 7/8 in. (33.7 x 22.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.157,false,true,73443,Asian Art,Print,"Yokohama kenbutsu zue|Picture of Sights in Yokohama: Woman with a Ringer, Lamp Post, a Steamboat at Full Sail and a Woman with a Sewing Machine",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"11th month, 1860",1860,1860,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 in. (37.5 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.159,false,true,73445,Asian Art,Print,「横浜異人屋敷之圖」|A Foreign Residence in Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"1st month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image: 29 1/2 x 14 3/4 in. (74.9 x 37.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.161,false,true,73447,Asian Art,Print,Yokohama Gankirō gaikokujin gyōretsu no zu|Picture of a Procession of Foreigners at Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"2nd month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/8 x 29 1/16 in. (35.9 x 73.8 cm) Overall (Mat): 20 x 35 in. (50.8 x 88.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.165,false,true,73451,Asian Art,Print,Furansu|French Photographer,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1861,1861,1861,Polychrome woodblock print; ink and color on paper,Image: 13 7/8 x 9 1/2 in. (35.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.166,false,true,73453,Asian Art,Print,Orosia|Russian Printers,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 7/8 in. (35.9 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.167,false,true,73454,Asian Art,Woodblock print,Oranda|A Dutch Group,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"2nd month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.169,false,true,73457,Asian Art,Print,Amerika koku jōkisen naka no zu|Interior of an American Steamship,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"4th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Overall: 14 x 28 3/4 in. (35.6 x 73 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.171,false,true,73459,Asian Art,Print,Furansujin|French Couple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"4th month, 1861",1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.172,false,true,73460,Asian Art,Print,"Igirisujin|An English Woman with a Chinese Servant in the Foreign District, from the series Famous Places in Yokohama",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1861,1861,1861,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 9 3/4 in. (36.5 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.175,false,true,73463,Asian Art,Print,Orandajin|Dutch Couple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"10th month, 1862",1862,1862,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.158a–c,false,true,73444,Asian Art,Print,Yokohama Miyozaki-kaku Gankirō ijin yūkyō no zu|Foreigners Enjoying a Party at the Gankirō Tea House,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"1st month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm) Image (b): 14 1/4 x 10 in. (36.2 x 25.4 cm) Image (c): 14 1/8 x 9 1/2 in. (35.9 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.160a–c,false,true,73446,Asian Art,Print,Yokohama Gankirō kodomo te odori no zu|Foreigners Enjoying Children's Kabuki at the Gankirō Tea House,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"1st month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm) Image (b): 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm) Image (c): 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.162a–c,false,true,73448,Asian Art,Print,Yokohama Gankirō gaikokujin gyōretsu no zu|Picture of a Procession of Foreigners at Yokohama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"2nd month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (c): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.163a–c,false,true,73449,Asian Art,Print,Kanagawa Gongenyama Gaikokujin yūran|Foreigners Visiting the Famous Site of Mt. Gongen in Kanagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"2nd month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm) Image (c): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.164a–c,false,true,73450,Asian Art,Print,Kanagawa Gongenyama Gaikokujin yūran|Foreigners Visiting the Famous Site of Mt. Gongen in Kanagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"2nd month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (c): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.168a–c,false,true,73455,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"3rd month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): H. 14 1/4 in. (36.2 cm); W. 9 5/8 in. (24.4 cm) Image (b): H. 14 1/4 in. (36.2 cm); W. 9 5/8 in. (24.4 cm) Image (c): 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.170a–c,false,true,73458,Asian Art,Print,Amerika koku jōkisen naka no zu|Interior of an American Steamship,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"4th month, 1861",1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm) Image (b): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (c): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.173a–c,false,true,73461,Asian Art,Print,「亞墨利加國蒸氣車往来」|“America”: A Steamship in Transit,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,1861,1861,1861,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (b): 14 3/8 x 9 3/4 in. (36.5 x 24.8 cm) Image (c): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.174a–c,false,true,73462,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"2nd month, 1862",1862,1862,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 5/8 in. (35.6 x 24.4 cm) Image (b): 17 5/8 x 9 3/4 in. (44.8 x 24.8 cm) Image (c): 13 7/8 x 9 5/8 in. (35.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.176a–c,false,true,73464,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,"4th month, 1864",1864,1864,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 7/8 in. (35.6 x 25.1 cm) Image (b): 14 x 9 7/8 in. (35.6 x 25.1 cm) Image (c): 14 x 9 3/4 in. (35.6 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1911,false,true,54450,Asian Art,Woodblock print,蒔絵櫛|Box with a Lacquer Comb,Japan,Edo period (1615–1868),,,,Artist,,Uematsu Tōshū,"Japanese, active late 1810s–20s",,Uematsu Tōshū,Japanese,1810,1830,1812,1812,1812,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 6 7/8 in. (13.7 x 17.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1432,false,true,54423,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Sadakage,"Japanese, active mid-19th century",,Utagawa Sadakage,Japanese,1800,1899,ca. 1840,1830,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/8 in. (20.5 x 18.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1103,false,true,54332,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kurokawa Michita,"Japanese, active early 19th century",,Kurokawa Michita,Japanese,1800,1899,probably 1820,1820,1820,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/16 in. (20.5 x 17.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1151,false,true,54357,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kosetsu,"Japanese, active early 19th century",(?),Kosetsu,Japanese,0019,0019,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/16 in. (21 x 18.3 cm),"Gift of T. Ito, Chicago, Ill, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1152,false,true,54358,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kosetsu,"Japanese, active early 19th century",,Kosetsu,Japanese,0019,0019,ca. 1820,1810,1830,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/16 in. (20.8 x 17.9 cm),"Gift of T. Ito, Chicago, Ill, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1824,false,true,56112,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyotomo,"Japanese, active early 19th century",,Torii Kiyotomo,Japanese,1815,1820,ca. 1721,1711,1731,Polychrome woodblock print; ink and color on paper,H. 12 7/16 in. (31.6 cm); W. 6 1/2 in. (16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3076,false,true,56556,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Torii Kiyotomo,"Japanese, active early 19th century",,Torii Kiyotomo,Japanese,1815,1820,ca. 1720,1710,1730,Polychrome woodblock print (hand colored); ink and color on paper,13 1/4 x 6 1/4 in. (33.7 x 15.9 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1243,false,true,54374,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu II,"Japanese, active ca. 1820s–late 1850s",,Yanagawa Shigenobu II,Japanese,1820,1859,1830,1830,1830,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.131,false,true,76561,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Gigadō Ashiyuki,"Japanese, active first half of 19th century",,Gigadō Ashiyuki,Japanese,1800,1849,1826,1826,1826,Polychrome woodblock print,Image (ôban tate-e): 14 5/8 x 10 1/4 in. (37.1 x 26 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.134,false,true,76564,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Gigadō Ashiyuki,"Japanese, active first half of 19th century",,Gigadō Ashiyuki,Japanese,1800,1849,1832,1832,1832,Polychrome woodblock print,Image (ôban tate-e): 14 3/4 x 10 1/4 in. (37.5 x 26 cm),"Purchase, Friends of Asian Art Gifts, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/76564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1,false,true,37391,Asian Art,Print,鞠子|Mariko,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1805,1795,1815,Polychrome woodblock print; ink and color on paper,4 15/16 x 14 3/16 in. (12.5 x 36 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2,false,true,36484,Asian Art,Woodblock print,"諸國瀧廻リ 相州大山ろうべんの瀧|Rōben Waterfall at Ōyama in Sagami Province (Sōshū Ōyama Rōben no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1827,1817,1837,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 1/4 in. (37.5 x 26 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3,false,true,36485,Asian Art,Woodblock print,"百人一首 うはかゑとき 持統天皇|Poem by Jitō Tenno (Empress Jitō), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 3/4 in. (25.7 x 37.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP4,false,true,36486,Asian Art,Woodblock print,"百人一首 乳母かゑとき 柿本人麿|Poem by Kakinomoto Hitomaro, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 in. (25.1 x 35.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP5,false,true,36487,Asian Art,Woodblock print,"百人一首 うはかゑとき 菅家|Poem by Kanke (Sugawara Michizane), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1839,1829,1849,Polychrome woodblock print; ink and color on paper,10 x 14 3/5 in. (25.4 x 37.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP6,false,true,53920,Asian Art,Woodblock print,"百人一首 うはかゑとき 源宗于朝臣|Poem by Minamoto no Muneyuki Ason, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP7,false,true,36488,Asian Art,Woodblock print,"百人一首 姥か恵と起 大中臣能宣朝臣|Poem by Ōnakatomi no Yoshinobu Ason, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,10 x 14 1/8 in. (25.4 x 35.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP8,false,true,36489,Asian Art,Print,"諸國名橋奇覧 すほうの国きんたいはし|Kintai Bridge in Suō Province (Suō no kuni Kintaibashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1827–30,1827,1830,Polychrome woodblock print; ink and color on paper,10 7/32 x 15 7/32 in. (26.0 x 38.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP9,false,true,36490,Asian Art,Woodblock print,"「富嶽三十六景 凱風快晴」|South Wind, Clear Sky (Gaifū kaisei), also known as Red Fuji, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 in. (24.4 x 35.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP10,false,true,36491,Asian Art,Woodblock print,"冨嶽三十六景 神奈川沖浪裏|Under the Wave off Kanagawa (Kanagawa oki nami ura), also known as The Great Wave, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 1/16 in. (24.4 x 35.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP11,false,true,36492,Asian Art,Woodblock print,"冨嶽三十六景 山下白雨|Storm below Mount Fuji (Sanka no haku u), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/8 in. (25.7 x 38.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP12,false,true,36493,Asian Art,Woodblock print,"冨嶽三十六景 駿州江尻|Ejiri in Suruga Province (Sunshū Ejiri), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 14 3/5 in. (25.4 x 37.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP13,false,true,36494,Asian Art,Woodblock print,"冨嶽三十六景 身延川裏不二|View from the Other Side of Fuji from the Minobu River (Minobugawa ura Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 3/4 in. (25.1 x 37.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP14,false,true,36495,Asian Art,Woodblock print,"冨嶽三十六景 甲州三坂水面|Reflection in Lake at Misaka in Kai Province (Kōshū Misaka suimen), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 4/5 x 14 9/16 in. (24.9 x 37.0 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP15,false,true,36496,Asian Art,Woodblock print,"冨嶽三十六景 相州七里浜|Shichirigahama in Sagami Province (Sōshū Shichirigahama), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 in. (25.7 x 38.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP16,false,true,36497,Asian Art,Woodblock print,"冨嶽三十六景 武州玉川|Tama River in Musashi Province (Bushū Tamagawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 3/4 in. (25.7 x 37.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP17,false,true,36498,Asian Art,Woodblock print,"冨嶽三十六景 相州箱根湖水|The Lake at Hakone in Sagami Province (Sōshū Hakone kosui), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 in. (25.7 x 38.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP18,false,true,36499,Asian Art,Woodblock print,"冨嶽三十六景 甲州三島越|Mishima Pass in Kai Province (Kōshū Mishima goe), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1832,Polychrome woodblock print; ink and color on paper,9 3/5 x 14 3/8 in. (24.4 x 36.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP19,false,true,36500,Asian Art,Woodblock print,"冨嶽三十六景 尾州不二見原|Fujimigahara in Owari Province (Bishū Fujimigahara), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 15/32 x 15 5/16 in. (24.1 x 38.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP20,false,true,36501,Asian Art,Woodblock print,"「冨嶽三十六景 相州梅沢左」|“Umezawa Manor in Sagami Province,” from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei, Sōshū Umezawa zai)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/8 in. (25.7 x 38.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP21,false,true,36502,Asian Art,Woodblock print,"冨嶽三十六景 上総の海路|At Sea off Kazusa (Kazusa no kairo), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 3/32 x 14 3/4 in. (25.6 x 37.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP22,false,true,36503,Asian Art,Woodblock print,"冨嶽三十六景 相州江の島|Enoshima in Sagami Province (Sōshū Enoshima), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 7/8 in. (25.7 x 37.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP23,false,true,36504,Asian Art,Woodblock print,"冨嶽三十六景 武陽佃島|Tsukudajima in Musashi Province (Buyō Tsukudajima), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/5 x 15 1/8 in. (25.9 x 38.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP24,false,true,36505,Asian Art,Woodblock print,"冨嶽三十六景 遠江山中|In the Mountains of Tōtomi Province (Tōtomi sanchū), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 3/5 in. (24.1 x 37.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP25,false,true,36506,Asian Art,Woodblock print,"冨嶽三十六景 遠江山中|In the Mountains of Tōtomi Province (Tōtomi sanchū), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,9 3/5 x 14 29/32 in. (24.4 x 37.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP26,false,true,36507,Asian Art,Woodblock print,"冨嶽三十六景 武陽佃島|Tsukudajima in Musashi Province (Buyō Tsukudajima), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 3/8 in. (24.1 x 36.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP27,false,true,36508,Asian Art,Woodblock print,"冨嶽三十六景 常州牛掘|Ushibori in Hitachi Province (Jōshū Ushibori), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 11/16 x 14 7/16 in. (24.6 x 36.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP28,false,true,36509,Asian Art,Woodblock print,"冨嶽三十六景 青山円座松|Cushion Pine at Aoyama (Aoyama enza no matsu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP657,false,true,54301,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1801,1791,1811,Polychrome woodblock print (surimono); ink and color on paper,7 1/8 x 20 1/4 in. (18.1 x 51.4 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP658,false,true,54302,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1801,1791,1811,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 22 1/8 in. (21 x 56.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP659,false,true,54303,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1799–1810,1799,1810,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 22 1/8 in. (21 x 56.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP660,false,true,37107,Asian Art,Woodblock print,雪松に鶴|Cranes on Branch of Snow-covered Pine,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,late 1820s,1825,1829,Polychrome woodblock print; ink and color on paper,20 3/8 x 9 1/8 in. (51.8 x 23.2 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911 Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP741,false,true,37189,Asian Art,Print,高根山与一右ェ門 千田川吉五郎|The Sumo Wrestlers Takaneyama Yoichiemon and Sendagawa Kichigorō,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1790–93,1790,1793,Polychrome woodblock print; ink and color on paper,H. 12 1/3 ( 30.6 cm; W. 5 3/32 in. (12.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP742,false,true,37190,Asian Art,Print,和田原甚四郎 花項山五郎吉|The Sumo Wrestlers Wadagahara Jinshirō and Kachōzan Gorokichi,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1783,1773,1793,Polychrome woodblock print; ink and color on paper,12 1/32 x 5 3/32 in. (30.6 x 12.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP743,false,true,37191,Asian Art,Print,唐子書画図|Chinese Boys Learning to Write and Paint,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1785,1775,1795,Polychrome woodblock print; ink and color on paper,15 3/8 x 10 1/8 in. (39.1 x 25.7 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP744,false,true,54304,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,7 3/8 x 13 1/8 in. (18.7 x 33.3 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP745,false,true,54305,Asian Art,Print,"露草に鶏と雛|Rooster, Hen and Chicken with Spiderwort",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–33,1830,1833,Polychrome woodblock print; ink and color on paper,Image: 9 x 11 1/2 in. (22.9 x 29.2 cm) Overall with paper mount: 12 7/8 x 18 1/2 in. (32.7 x 47 cm) Overall with matt: 15 1/2 x 22 3/4 in. (39.4 x 57.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP746,false,true,54306,Asian Art,Woodblock print,葛飾北斎画 桔梗に蜻蛉|Dragonfly and Bellflower,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,late 1820s,1820,1830,Polychrome woodblock print (surimono); ink and color on paper,9 3/4 x 14 3/16 in. (24.8 x 36 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP747,false,true,37192,Asian Art,Print,葛飾北斎画 燕子花|Grasshopper and Iris,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,late 1820s,1824,1829,Polychrome woodblock print; ink and color on paper,Overall: 9 3/4 x 14 3/16in. (24.8 x 36 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP748,false,true,37193,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,7 31/32 x 12 1/2 in. (20.3 x 31.8 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1011,false,true,54315,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,7 11/16 x 20 3/8 in. (19.5 x 51.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1012,false,true,54887,Asian Art,Print,Fuji Tohō|The Top of Mount Fuji,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/4 in. (38.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1013,false,true,54888,Asian Art,Print,今戸川|Imadogawa,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1801–4,1801,1804,Polychrome woodblock print; ink and color on paper,H. 10 3/16 in. (25.9 cm); W. 15 1/4 in. (38.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1014,false,true,54889,Asian Art,Woodblock print,"百人一首 乳母かえ説 元良親王|Poem by Motoyoshi Shinnō, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1839,1829,1849,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 14 1/2 in. (36.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1016,false,true,54925,Asian Art,Print,葛飾北斎画 菊に雀|Sparrows and Chrysanthemums,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 8 1/2 in. (21.6 cm); W. 10 13/16 in. (27.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1081,false,true,55019,Asian Art,Woodblock print,"諸國瀧廻リ 下野黒髪山 きりふりの滝|Kirifuri Waterfall at Kurokami Mountain in Shimotsuke (Shimotsuke Kurokamiyama Kirifuri no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 10 1/4 in. (37.5 × 26 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1082,false,true,55020,Asian Art,Woodblock print,"諸國瀧廻リ 木曾海道小野ノ瀑布|Ono Waterfall on the Kisokaidō (Kisokaidō Ono no bakufu), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,14 7/8 x 10 5/16 in. (37.8 x 26.2 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1083,false,true,55021,Asian Art,Woodblock print,"諸國瀧廻リ 東海道坂ノ下 清瀧くわんおん|Kiyotaki Kannon Waterfall at Sakanoshita on the Tōkaidō (Tōkaidō Sakanoshita Kiyotaki kannon), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 3/16 in. (25.9 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1084,false,true,45027,Asian Art,Woodblock print,"諸國瀧廻リ 和州吉野義経馬洗滝|The Waterfall Where Yoshitsune Washed His Horse at Yoshino in Yamato Province (Washū Yoshino Yoshitsune uma arai no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1085,false,true,55022,Asian Art,Woodblock print,"諸國瀧廻リ 木曽路ノ奥 阿彌陀ヶ瀧|The Amida Falls in the Far Reaches of the Kisokaidō Road (Kisoji no oku Amida-ga-taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1827,1817,1837,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 10 in. (36.8 x 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1086,false,true,55023,Asian Art,Print,"諸國瀧廻 東都葵ヶ岡の瀧|Fall of Aoiga Oka, Yedo",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1827,1817,1837,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 10 3/16 in. (25.9 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1087,false,true,55024,Asian Art,Woodblock print,"諸國瀧廻リ 相州大山ろうべんの瀧|Rōben Waterfall at Ōyama in Sagami Province (Sōshū Ōyama Rōben no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1827,1817,1837,Polychrome woodblock print; ink and color on paper,H. 14 3/8 in. (36.5 cm); W. 10 1/8 in. (25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1088,false,true,55025,Asian Art,Woodblock print,"諸國瀧廻リ 美濃ノ国養老の滝|Yōrō Waterfall in Mino Province (Mino no Yōrō no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1827,1817,1837,Polychrome woodblock print; ink and color on paper,H. 14 7/16 in. (36.7 cm); W. 10 3/16 in. (25.9 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1117,false,true,55039,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1825,1815,1835,Polychrome woodblock print; ink and color on paper,H. 13 7/8 in. (35.2 cm); W. 4 9/16 in. (11.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1142,false,true,54349,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1810,1800,1820,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 1/4 in. (21.3 x 18.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1246,false,true,54377,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1814,1804,1824,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 5 9/16 in. (21.1 x 14.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1247,false,true,54378,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1820,1820,1820,Polychrome woodblock print (surimono); ink and color on paper,8 x 10 3/8 in. (20.3 x 26.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1284,false,true,55223,Asian Art,Woodblock print,"冨嶽三十六景 東海道品川御殿山の不二|Fuji from Gotenyama on the Tōkaidō at Shinagawa (Tōkaidō Shinagawa Gotenyama no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 7/16 in. (36.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1285,false,true,55225,Asian Art,Woodblock print,"冨嶽三十六景 本所立川|Tatekawa in Honjō (Honjō Tatekawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 1/8 in. (24.8 x 35.9cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1286,false,true,55226,Asian Art,Woodblock print,"冨嶽三十六景 下目黒|Lower Meguro (Shimo Meguro), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 11/16 in. (24.6 cm); W. 14 3/8 in. (36.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1287,false,true,55227,Asian Art,Woodblock print,"冨嶽三十六景 東都駿台|Surugadai in Edo (Tōto Sundai), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 3/8 in. (36.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1288,false,true,55228,Asian Art,Woodblock print,"冨嶽三十六景 東海道江尻田子の浦略図|Tago Bay near Ejiri on the Tōkaidō (Tōkaidō Ejiri Tago no ura ryaku zu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/8 in. (24.8 x 36.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1289,false,true,55231,Asian Art,Woodblock print,"冨嶽三十六景 武州千住|Senju in Musashi Province (Bushū Senju), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 11/16 in. (24.6 cm); W. 14 3/8 in. (36.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1290,false,true,45218,Asian Art,Print,"風流無くてななくせ|Squeaking a Ground Cherry, from the series Seven Fashionable Useless Habits (Furyu nakute nana kuse)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1798,1788,1808,Polychrome woodblock print; ink and color on paper,14 5/16 x 9 3/4 in. (36.4 x 24.8cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1294,false,true,55236,Asian Art,Woodblock print,"冨嶽三十六景 東海道金谷の不二|Fuji Seen from Kanaya on the Tōkaidō (Tōkaidō Kanaya no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 1/2 in. (36.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1295,false,true,55237,Asian Art,Woodblock print,"冨嶽三十六景 江戸日本橋|Nihonbashi in Edo (Edo Nihonbashi), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 1/2 in. (36.8 cm),"Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1296,false,true,55238,Asian Art,Woodblock print,"冨嶽三十六景 江都駿河町三井見世略図|Mitsui Shop at Surugachō in Edo (Edo Surugachō Mitsui mise ryaku zu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 15 1/8 in. (38.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1322,false,true,55281,Asian Art,Woodblock print,"冨嶽三十六景 諸人登山|Groups of Mountain Climbers (Shojin tozan), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1323,false,true,55282,Asian Art,Woodblock print,"冨嶽三十六景 東都浅草本願寺|Honganji at Asakusa in Edo (Tōto Asakusa Honganji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/8 in. (38.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1324,false,true,55283,Asian Art,Woodblock print,"冨嶽三十六景 東海道吉田|Yoshida on the Tōkaidō (Tōkaidō Yoshida), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/4 in. (38.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1325,false,true,55284,Asian Art,Woodblock print,"冨嶽三十六景 相州仲原|Nakahara in Sagami Province (Sōshū Nakahara), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1326,false,true,55285,Asian Art,Woodblock print,"冨嶽三十六景 従千住花街眺望の不二|Fuji Seen in the Distance from Senju Pleasure Quarter (Senju kagai yori chōbō no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1327,false,true,39656,Asian Art,Woodblock print,"冨嶽三十六景 甲州石班沢|Kajikazawa in Kai Province (Kōshū Kajikazawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1328,false,true,55286,Asian Art,Woodblock print,"冨嶽三十六景 甲州伊沢暁|Dawn at Isawa in Kai Province (Kōshū Isawa no akatsuki), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 5/16 in. (26.2 cm); W. 15 in. (38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1329,false,true,55287,Asian Art,Woodblock print,"冨嶽三十六景 江都駿河町三井見世略図|Mitsui Shop at Surugachō in Edo (Edo Surugachō Mitsui mise ryaku zu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,Oban 10 x 15 in. (25.4 x 38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1330,false,true,55288,Asian Art,Woodblock print,"冨嶽三十六景 礫川雪の旦|Morning after the Snow at Koishikawa in Edo (Koishikawa yuki no ashita), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/16 in. (38.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1331,false,true,37319,Asian Art,Woodblock print,"冨嶽三十六景 御厩川岸より両国橋夕陽見|Viewing the Sunset over Ryōgoku Bridge from the Onmaya Embankment (Onmayagashi yori Ryōgokubashi sekiyō o miru), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1823,1843,Polychrome woodblock print; ink and color on paper,9 15/16 x 14 11/16 in. (25.2 x 37.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1332,false,true,55289,Asian Art,Woodblock print,"冨嶽三十六景 深川万年橋下|Under the Mannen Bridge at Fukagawa (Fukagawa Mannenbashi shita), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 3/16 in. (25.7 x 38.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1333,false,true,55290,Asian Art,Woodblock print,"冨嶽三十六景 駿州片倉茶園の不二|Fuji from the Katakura Tea Fields in Suruga (Sunshū Katakura chaen no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 15 in. (38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1334,false,true,37320,Asian Art,Woodblock print,"冨嶽三十六景 駿州片倉茶園の不二|Fuji from the Katakura Tea Fields in Suruga (Sunshū Katakura chaen no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1335,false,true,37321,Asian Art,Woodblock print,"冨嶽三十六景 五百らかん寺さざゐどう|Sazai Hall at the Temple of the Five Hundred Arhats (Gohyaku Rakanji Sazaidō), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1336,false,true,37322,Asian Art,Woodblock print,"冨嶽三十六景 隠田の水車|The Waterwheel at Onden (Onden no suisha), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 7/8 in. (37.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1337,false,true,39655,Asian Art,Woodblock print,"冨嶽三十六景 登戸浦|Noboto Bay (Noboto no ura), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 3/16 in. (26.0 x 38.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1338,false,true,55291,Asian Art,Woodblock print,"冨嶽三十六景 隅田川関屋の里|Sekiya Village on the Sumida River (Sumidagawa Sekiya no sato), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 15 1/4 in. (38.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1339,false,true,37323,Asian Art,Woodblock print,"百人一首 宇波か縁説 藤原道信朝臣|Poem by Fujiwara no Michinobu Ason, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,H. 9 15/16 in. (25.2 cm); W. 14 3/8 in. (36.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1340,false,true,37324,Asian Art,Woodblock print,"百人一首 うばがゑとき 伊勢|Poem by Ise, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,H. 9 15/16 in. (25.2 cm); W. 14 3/8 in. (36.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1341,false,true,55306,Asian Art,Woodblock print,"百人一首 うはかゑとき 天智天皇|Poem by Tenchi Tennō, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,H. 9 15/16 in. (25.2 cm); W. 14 5/16 in. (36.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1380,false,true,50916,Asian Art,Woodblock print,"諸國名橋奇覧 東海道岡崎矢はぎのはし|Yahagi Bridge at Okazaki on the Tōkaidō (Tōkaidō Okazaki Yahagi no hashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1827–30,1827,1830,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/2 in. (36.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/50916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1381,false,true,53699,Asian Art,Woodblock print,"諸國名橋奇覧 かうつけ佐野ふなはしの古づ|Old View of the Boat-bridge at Sano in Kōzuke Province (Kōzuke Sano funabashi no kozu), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1827–30,1827,1830,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 9/16 in. (37 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1382,false,true,50924,Asian Art,Woodblock print,"諸國名橋奇覧 ゑちぜんふくゐの橋|Fukui Bridge in Echizen Province (Echizen Fukui no hashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1827–30,1827,1830,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/2 in. ( 36.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/50924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1383,false,true,55369,Asian Art,Woodblock print,"冨嶽三十六景 東海道保土ケ谷|Hodogaya on the Tōkaidō (Tōkaidō Hodogaya), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 9/16 in. (37 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1393,false,true,54389,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1822,1822,1822,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1395,false,true,55402,Asian Art,Woodblock print,"雪月花 吉野|Cherry Blossoms at Yoshino (Yoshino), from the series Snow, Moon, and Flowers (Setsugekka)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 14 15/16 in. (37.9 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1396,false,true,53788,Asian Art,Woodblock print,"諸國名橋奇覧 足利行道山くものかけはし|The Hanging-cloud Bridge at Mount Gyōdō near Ashikaga (Ashikaga Gyōdōzan kumo no kakehashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 1/16 in. (35.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1397,false,true,53789,Asian Art,Woodblock print,"諸國名橋奇覧 飛越の堺つりはし|The Suspension Bridge on the Border of Hida and Etchū Provinces (Hietsu no sakai tsuribashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 15 1/8 in. (38.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1398,false,true,45026,Asian Art,Woodblock print,"諸國名橋奇覧 三河の八ツ橋の古図|Ancient View of Yatsuhashi in Mikawa Province (Mikawa no Yatsuhashi no kozu), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 5/8 in. (23.2 x 37.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1425,false,true,55454,Asian Art,Woodblock print,"百人一首 乳母か縁説 在原業平|Poem by Ariwara no Narihira, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 in. (38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1426,false,true,55456,Asian Art,Woodblock print,"冨嶽三十六景 甲州犬目峠|The Inume Pass in Kai Province (Kōshū Inume tōge), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 11/16 in. (24.6 cm); W. 14 1/4 in. (36.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1427,false,true,55458,Asian Art,Woodblock print,"冨嶽三十六景 東海道保土ケ谷|Hodogaya on the Tōkaidō (Tōkaidō Hodogaya), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 15/16 in. (25.2 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1428,false,true,55461,Asian Art,Woodblock print,"諸國瀧廻リ 東海道坂ノ下 清瀧くわんおん|Kiyotaki Kannon Waterfall at Sakanoshita on the Tōkaidō (Tōkaidō Sakanoshita Kiyotaki kannon), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 10 in. (25.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1429,false,true,55463,Asian Art,Woodblock print,"雪月花 淀川|Moonlight on the Yodo River (Yodogawa), from the series Snow, Moon, and Flowers (Setsugekka)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 9/16 in. (37 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1430,false,true,55466,Asian Art,Woodblock print,"琉球八景 城嶽霊泉|The Sacred Spring at Jōgaku (Jōgaku reisen), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1832,1822,1842,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); 14 5/8 in. (37.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1456,false,true,55503,Asian Art,Woodblock print,"冨嶽三十六景 駿州大野新田|The New Fields at Ōno in Suruga Province (Sunshū Ōno shinden), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8in. (25.1 x 37.8cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1457,false,true,55505,Asian Art,Woodblock print,"諸國名橋奇覧 摂洲阿治川口天保山|Tenpōzan at the Mouth of the Aji River in Settsu Province (Sesshū Ajikawaguchi Tenpōzan), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1827–30,1827,1830,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1482,false,true,55571,Asian Art,Woodblock print,"冨嶽三十六景 上総の海路|At Sea off Kazusa (Kazusa no kairo), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 15 1/16 in. (38.3 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1843,false,true,56128,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1830s,1830,1839,Monochrome woodblock print; ink on paper,H. 10 1/2 in. (26.7 cm); W. 14 3/4 in. (37.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1846,false,true,56131,Asian Art,Woodblock print,"冨嶽三十六景 隠田の水車|The Waterwheel at Onden (Onden no suisha), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 in. (35.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1847,false,true,45434,Asian Art,Woodblock print,"「富嶽三十六景 神奈川沖浪裏」|Under the Wave off Kanagawa (Kanagawa oki nami ura), also known as The Great Wave, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 15/16 in. (25.7 x 37.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1848,false,true,56132,Asian Art,Woodblock print,"冨嶽三十六景 常州牛掘|Ushibori in Hitachi Province (Jōshū Ushibori), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 13 7/8 in. (35.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1849,false,true,56133,Asian Art,Woodblock print,"冨嶽三十六景 信州諏訪湖|Lake Suwa in Shinano Province (Shinshū Suwako), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 14 13/16 in. (37.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1850,false,true,56135,Asian Art,Woodblock print,"冨嶽三十六景 東海道品川御殿山の不二|Fuji from Gotenyama at Shinagawa on the Tōkaidō (Tōkaidō Shinagawa Gotenyama no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1842,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 1/8 in. (35.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1851,false,true,53700,Asian Art,Woodblock print,"「諸國名橋寄覧 東海道岡崎 矢はぎのはし」|Yahagi Bridge at Okazaki on the Tōkaidō (Tōkaidō Okazaki Yahagi no hashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,H. 10 3/16 in. (25.9 cm); W. 15 1/16 in. (38.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1852,false,true,56136,Asian Art,Woodblock print,"百人一首 乳母かゑとき 猿丸太夫|Poem by Sarumaru Dayū, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 3/8 in. (36.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1854,false,true,56172,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,H. 9 9/16 in. (24.3 cm); W. 14 7/16 in. (36.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1859,false,true,56178,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1825,1815,1835,Sketch for a woodblock print; ink and color on paper,Image: 7 1/16 x 11 7/8 in. (17.9 x 30.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1860,false,true,56183,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1820–34,1820,1834,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/4 in. (36.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1865,false,true,54430,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1800–1815,1800,1815,Polychrome woodblock print (surimono); ink and color on paper,7 5/16 x 19 7/8 in. (18.6 x 50.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1868,false,true,54433,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1820–33,1820,1833,Polychrome woodblock print (surimono); ink and color on paper,8 9/16 x 7 1/16 in. (21.7 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1870,false,true,54435,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1804–13,1804,1813,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 10 11/16 in. (20.6 x 27.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1873,false,true,54438,Asian Art,Print,"元禄歌仙貝合|Ashi Clam, from the series ""Genroku Kasen Kai-awase""",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1821,1821,1821,Polychrome woodblock print; ink and color on paper,7 15/16 x 7 in. (20.2 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1900,false,true,54439,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1808–27,1808,1827,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/2 in. (21 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2019,false,true,54768,Asian Art,Print,初詣|Young Women Visiting a Shinto Shrine,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1814,1814,1814,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 5 7/16 in. (21.3 x 13.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2224,false,true,53990,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,probably 1807,1807,1807,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 11 1/8 in. (13.8 x 28.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2330,false,true,54114,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1816,1816,1816,Part of an album of woodblock prints (surimono); ink and color on paper,8 7/16 x 7 1/2 in. (21.4 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2403,false,true,56806,Asian Art,Woodblock print,"百人一首 乳母かゑとき 猿丸太夫|Poem by Sarumaru Dayū, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,10 x 14 3/8 in. (25.4 x 36.5 cm),"Gift of Louis V. Ledoux, 1931",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2546,false,true,56961,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1795,1785,1805,Polychrome woodblock print; ink and color on paper,10 x 15 1/2 in. (25.4 x 39.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2547,false,true,53787,Asian Art,Woodblock print,"諸國名橋奇覧 かうつけ佐野ふなはしの古づ|Old View of the Boat-bridge at Sano in Kōzuke Province (Kōzuke Sano funabashi no kozu), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2548,false,true,56965,Asian Art,Woodblock print,"百人一首 うばがゑとき 伊勢|Poem by Ise, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,10 1/2 x 14 3/4 in. (26.7 x 37.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2549,false,true,56973,Asian Art,Woodblock print,"百人一首 うばがゑとき 大納言経信|Poem by Dainagon Tsunenobu (Minamoto no Tsunenobu, Katsura no Dainagon), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Polychrome woodblock print; ink and color on paper,Image: 9 15/16 × 14 3/8 in. (25.2 × 36.5 cm) Mat: 22 3/4 × 15 1/2 in. (57.8 × 39.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2550,false,true,56967,Asian Art,Woodblock print,"百人一首 うはかゑとき 持統天皇|Poem by Jitō Tenno (Empress Jitō), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1839,1829,1849,Polychrome woodblock print; ink and color on paper,10 x 14 5/8 in. (25.4 x 37.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2552,false,true,56979,Asian Art,Woodblock print,"百人一首 うはかゑとき 源宗于朝臣|Poem by Minamoto no Muneyuki Ason, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,Overall: 9 7/8 x 14 1/2 in. (25.1 x 36.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2553,false,true,56988,Asian Art,Woodblock print,"冨嶽三十六景 駿州江尻|Ejiri in Suruga Province (Sunshū Ejiri), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/4 in. (24.4 x 37.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2554,false,true,53845,Asian Art,Woodblock print,"冨嶽三十六景 御厩川岸より両国橋夕陽見|Viewing the Sunset over Ryōgoku Bridge from the Onmayagashi Embankment (Onmayagashi yori Ryōgokubashi sekiyō o miru), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 14 7/8 in. (37.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2555,false,true,56990,Asian Art,Woodblock print,"冨嶽三十六景 礫川雪の旦|Morning after the Snow at Koishikawa in Edo (Koishikawa yuki no ashita), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2556,false,true,56786,Asian Art,Woodblock print,"冨嶽三十六景 甲州三島越|Mishima Pass in Kai Province (Kōshū Mishima goe), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 × 14 3/4 in. (24.8 × 37.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2557,false,true,56787,Asian Art,Woodblock print,"冨嶽三十六景 尾州不二見原|Fujimigahara in Owari Province (Bishū Fujimigahara), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 15 in. (38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2558,false,true,56994,Asian Art,Woodblock print,"冨嶽三十六景 甲州犬目峠|The Inume Pass in Kai Province (Kōshū Inume tōge), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2559,false,true,56996,Asian Art,Woodblock print,"冨嶽三十六景 相州江の島|Enoshima in Sagami Province (Sōshū Enoshima), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 3/4 in. (25.7 x 37.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2560,false,true,56998,Asian Art,Woodblock print,"冨嶽三十六景 武州玉川|Tama River in Musashi Province (Bushū Tamagawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2561,false,true,57000,Asian Art,Woodblock print,"冨嶽三十六景 東海道江尻田子の浦略図|Tago Bay near Ejiri on the Tōkaidō (Tōkaidō Ejiri Tago no ura ryaku zu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2562,false,true,57003,Asian Art,Woodblock print,"冨嶽三十六景 東海道保土ケ谷|Hodogaya on the Tōkaidō (Tōkaidō Hodogaya), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 in. (25.7 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2563,false,true,57004,Asian Art,Woodblock print,"冨嶽三十六景 武陽佃島|Tsukudajima in Musashi Province (Buyō Tsukudajima), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 1/4 in. (25.4 x 38.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2564,false,true,39798,Asian Art,Woodblock print,"冨嶽三十六景 信州諏訪湖|Lake Suwa in Shinano Province (Shinshū Suwako), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 7/8 in. (24.8 x 37.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2565,false,true,56785,Asian Art,Woodblock print,"冨嶽三十六景 常州牛掘|Ushibori in Hitachi Province (Jōshū Ushibori), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 15 in. (38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2566,false,true,57005,Asian Art,Woodblock print,"冨嶽三十六景 本所立川|Tatekawa in Honjō (Honjō Tatekawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2567,false,true,57006,Asian Art,Woodblock print,"冨嶽三十六景 山下白雨|Storm below Mount Fuji (Sanka no haku u), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/4 in. (24.8 x 37.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2568,false,true,57007,Asian Art,Woodblock print,"冨嶽三十六景 凱風快晴|South Wind, Clear Sky (Gaifū kaisei), also known as Red Fuji, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,Oban nishiki-e triptych: 10 x 15 in. (25.4 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2569,false,true,39799,Asian Art,Woodblock print,"「富嶽三十六景 神奈川沖浪裏」|Under the Wave off Kanagawa (Kanagawa oki nami ura), or The Great Wave, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2570,false,true,45494,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",", New York, NY (1936; sold to MMA).",Katsushika Hokusai,Japanese,1760,1849,ca. 1796,1786,1806,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 in. (21.6 x 15.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2571,false,true,57008,Asian Art,Woodblock print,松本幸四郎|Matsumoto Koshiro IV as Tsurifune no Sabu,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1796,1786,1806,Polychrome woodblock print; ink and color on paper,10 3/4 x 5 3/4 in. (27.3 x 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2572,false,true,57009,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1800,1790,1810,Polychrome woodblock print; ink and color on paper,7 7/8 x 12 7/8 in. (20 x 32.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2573,false,true,54188,Asian Art,Print,Horimono-shi|職人三十六番|The Metal Carver,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1802,1802,1802,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 1/2 in. (14 x 19.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2574,false,true,54189,Asian Art,Print,Tachi-shi|職人三十六番|The Swordsmith,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1802,1802,1802,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 3/8 in. (13.7 x 18.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2575,false,true,54191,Asian Art,Print,Hata-ori|職人三十六歌仙|The Weaving Factory,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1802,1792,1812,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 3/8 in. (13.7 x 18.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2576,false,true,54192,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1841,1841,1841,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2577,false,true,54193,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1800,1790,1810,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 10 1/2 in. (14 x 26.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2578,false,true,54194,Asian Art,Print,"Sakura-gai|元禄歌仙貝合|Cherry Shell, from the series Genroku Poetry Shell Games",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,probably 1821,1821,1821,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 7/8 in. (19.7 x 17.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2579,false,true,54195,Asian Art,Print,Miyako-gai|元禄歌仙貝合|Miyako Shell,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,probably 1821,1821,1821,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 in. (20 x 17.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2580,false,true,57010,Asian Art,Print,新柳橋の白雨|Shower at the New Yanagi Bridge,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1806,1806,1806,Woodblock print ; ink and color on paper,7 7/8 x 11 3/4 in. (20 x 29.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2581,false,true,56727,Asian Art,Print,"冨嶽三十六景 甲州石班沢|Kajikazawa in Kai Province (Kōshū Kajikazawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,10 x 15 1/8 in. (25.4 x 38.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2831,false,true,57069,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,14 x 6 3/8 in. (35.6 x 16.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2903,false,true,56018,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1781–1801,1781,1801,Polychrome woodblock print; ink and color on paper,11 3/4 x 5 1/4 in. (29.8 x 13.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2904,false,true,56019,Asian Art,Print,"二代中村野塩|The Actor Nakamura Noshio II, in Female Role, Holding a Shakuhachi (Bamboo Flute)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,"1796 (Kansei, 6th year)",1796,1796,Polychrome woodblock print; ink and color on paper,11 3/4 x 5 1/4 in. (29.8 x 13.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2912,false,true,49934,Asian Art,Woodblock print,"琉球八景 泉崎夜月|Evening Moon at Izumizaki (Izaumizaki yagetsu), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2913,false,true,49935,Asian Art,Woodblock print,"琉球八景 中島蕉園|Banana Garden at Nakashima (Nakashima shōen), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2914,false,true,49936,Asian Art,Woodblock print,"琉球八景 粂村竹籬|Bamboo Hedge at Kumemura (Kumemura chikuri), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2915,false,true,49937,Asian Art,Woodblock print,"琉球八景 城嶽霊泉|The Sacred Spring at Jōgaku (Jōgaku reisen), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2916,false,true,49938,Asian Art,Woodblock print,"琉球八景 臨海潮(湖)聲|Sound of the Lake at Rinkai (Rinkai kosei), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,Oban 9 3/4 x 14 1/2 in. (24.8 x 36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2917,false,true,49939,Asian Art,Woodblock print,"琉球八景 筍崖夕照|Evening Glow at Jungai (Jungai sekishō), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 1/2 in. (25.1 x 36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2918,false,true,49940,Asian Art,Woodblock print,"琉球八景 長虹秋霽|Autumn Sky at Chōkō (Chōkō shūsei), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2919,false,true,49941,Asian Art,Woodblock print,"琉球八景 龍洞松濤|Pines and Waves at Ryūtō (Ryūtō shōtō), from the series Eight Views of the Ryūkyū Islands (Ryūkyū hakkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2920,false,true,55739,Asian Art,Print,"千絵の海 下総登戸|Noboto at Shimōsa (Shimōsa Noboto), from the series One Thousand Pictures of the Sea (Chie no umi)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1832–33,1832,1833,Polychrome woodblock print; ink and color on paper,H. 7 1/4 in. (18.4 cm); W. 10 in. (25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2921,false,true,44981,Asian Art,Woodblock print,"雪月花 隅田|Snow on the Sumida River (Sumida), from the series, Snow, Moon, and Flowers (Setsugekka)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 1/2 in. (24.8 x 36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2922,false,true,56137,Asian Art,Woodblock print,"雪月花 淀川|Moonlight on the Yodo River (Yodogawa), from the series Snow, Moon, and Flowers (Setsugekka)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2923,false,true,56138,Asian Art,Woodblock print,"雪月花 吉野|Cherry Blossoms at Yoshino (Yoshino), from the series Snow, Moon, and Flowers (Setsugekka)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2924,false,true,56139,Asian Art,Woodblock print,"諸國瀧廻リ 下野黒髪山 きりふりの滝|Kirifuri Waterfall at Kurokami Mountain in Shimotsuke (Shimotsuke Kurokamiyama Kirifuri no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1827,1837,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 5/16 in. (37.1 x 26.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2925,false,true,56140,Asian Art,Woodblock print,"諸國瀧廻リ 木曾海道小野ノ瀑布|Ono Waterfall on the Kisokaidō (Kisokaidō Ono no bakufu), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 1/4 in. (37.5 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2926,false,true,56141,Asian Art,Woodblock print,"諸國瀧廻リ 東海道坂ノ下 清瀧くわんおん|Kiyotaki Kannon Waterfall at Sakanoshita on the Tōkaidō (Tōkaidō Sakanoshita Kiyotaki kannon), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 1/4 in. (37.1 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2927,false,true,56142,Asian Art,Woodblock print,"諸國瀧廻リ 和州吉野義経馬洗滝|The Waterfall Where Yoshitsune Washed His Horse at Yoshino in Yamato Province (Washū Yoshino Yoshitsune uma arai no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 1/4 in. (37.1 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2928,false,true,56143,Asian Art,Woodblock print,"諸國瀧廻リ 木曽路ノ奥 阿彌陀ヶ瀧|The Amida Falls in the Far Reaches of the Kisokaidō Road (Kisoji no oku Amida-ga-taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1830,1833,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 10 1/4 in. (37.5 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2930,false,true,56145,Asian Art,Woodblock print,"諸國瀧廻リ 相州大山ろうべんの瀧|Rōben Waterfall at Ōyama in Sagami Province (Sōshū Ōyama Rōben no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 in. (36.8 x 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2931,false,true,56146,Asian Art,Woodblock print,"諸國瀧廻リ 美濃ノ国養老の滝|Yōrō Waterfall in Mino Province (Mino no Yōrō no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 1/4 in. (37.1 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2932,false,true,56147,Asian Art,Woodblock print,"百人一首 宇波か縁説 参儀等|Poem by Sangi Hitoshi (Minamoto no Hitoshi), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 1/4 in. (25.1 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2933,false,true,56148,Asian Art,Woodblock print,"百人一首 乳母かゑとき 柿の本人麿|Poem by Kakinomoto Hitomaro, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 5/8 in. (25.7 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2934,false,true,56149,Asian Art,Woodblock print,"百人一首 乳母か縁説 中納言家持|Poem by Chūnagon Yakamochi (Ōtomo no Yakamochi), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/8 in. (24.8 x 36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2935,false,true,56150,Asian Art,Woodblock print,"百人一首 うはかゑとき 源宗于朝臣|Poem by Minamoto no Muneyuki Ason, from the series One Hundred poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/8 in. (24.4 x 36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2936,false,true,56151,Asian Art,Woodblock print,"百人一首 宇波か縁説 藤原道信朝臣|Poem by Fujiwara no Michinobu Ason, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1845,1760,1845,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 7/8 in. (26.4 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2937,false,true,56157,Asian Art,Woodblock print,"百人一首 宇波か縁説 権中納言定家|Poem by Gon-Chūnagon Sadaie, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 in. (26 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2938,false,true,56171,Asian Art,Woodblock print,"百人一首 うはかゑとき 文屋朝康|Poem by Funya no Asayasu, from the series One Hundred Poems Explained by a Nurse (Hyakunin isshu ubaga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/8 in. (24.8 x 36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2939,false,true,45261,Asian Art,Woodblock print,"百人一首 うはか縁説 清原深養父|Poem by Kiyohara no Fukayabu, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,10 5/8 x 14 1/2 in. (27 x 36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2940,false,true,56175,Asian Art,Woodblock print,"百人一首 宇波かゑとき 安部仲麿|Poem by Abe no Nakamaro, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,10 3/8 x 14 7/8 in. (26.4 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2941,false,true,55734,Asian Art,Woodblock print,"百人一首 乳母か絵とき 参議篁|Poem by Sangi no Takamura (Ono no Takamura), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,Oban 10 1/4 x 14 3/4 in. (26 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2942,false,true,53191,Asian Art,Woodblock print,"諸國名橋奇覧 足利行道山くものかけはし|The Hanging-cloud Bridge at Mount Gyōdō near Ashikaga (Ashikaga Gyōdōzan kumo no kakehashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/8 in. (38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2943,false,true,53698,Asian Art,Woodblock print,"諸國名橋奇覧 かうつけ佐野ふなはしの古づ|Old View of the Boat-bridge at Sano in Kōzuke Province (Kōzuke Sano funabashi no kozu), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/2 in. (25.7 x 39.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2944,false,true,53192,Asian Art,Woodblock print,"諸國名橋奇覧 飛越の堺つりはし|The Suspension Bridge on the Border of Hida and Etchū Provinces (Hietsu no sakai tsuribashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/8 in. (38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2945,false,true,53791,Asian Art,Woodblock print,"諸國名橋奇覧 東海道岡崎矢はぎのはし|Yahagi Bridge at Okazaki on the Tōkaidō (Tōkaidō Okazaki Yahagi no hashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/8 in. (25.7 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2946,false,true,53790,Asian Art,Woodblock print,"諸國名橋奇覧 すほうの国きんたいはし|Kintai Bridge in Suō Province (Suō no kuni Kintaibashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/8 in. (26 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2947,false,true,53792,Asian Art,Woodblock print,"諸國名橋奇覧 かめゐど天神たいこはし|The Arched Bridge at Kameido Tenjin Shrine (Kameido Tenjin Taikobashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/8 in. (25.7 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2948,false,true,53786,Asian Art,Woodblock print,"諸國名橋奇覧 山城あらし山吐月橋 |Togetsu Bridge at Arashiyama in Yamashiro, from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,10 3/8 x 15 1/4 in. (26.4 x 38.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2949,false,true,53193,Asian Art,Woodblock print,"諸國名橋奇覧 摂洲天満橋|Tenman Bridge at Settsu Province (Sesshū Tenmanbashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 15 1/8 in. (38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2950,false,true,56202,Asian Art,Woodblock print,"諸國名橋奇覧 摂洲阿治川口天保山|Tenpōzan at the Mouth of the Aji River in Settsu Province (Sesshū Ajikawaguchi Tenpōzan), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/8 in. (26 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2951,false,true,53793,Asian Art,Woodblock print,"諸國名橋奇覧 三河の八ツ橋の古図|Ancient View of Yatsuhashi in Mikawa Province (Mikawa no Yatsuhashi no kozu), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2952,false,true,56210,Asian Art,Woodblock print,"諸國名橋奇覧 ゑちぜんふくゐの橋|Fukui Bridge in Echizen Province (Echizen Fukui no hashi), from the series Remarkable Views of Bridges in Various Provinces (Shokoku meikyō kiran)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 1/8 in. (25.7 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2953,false,true,55735,Asian Art,Woodblock print,"冨嶽三十六景 駿州江尻|Ejiri in Suruga Province (Sunshū Ejiri), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,Oban 9 7/8 x 14 3/4 in. (25.1 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2954,false,true,56212,Asian Art,Woodblock print,"冨嶽三十六景 駿州大野新田|The New Fields at Ōno in Suruga Province (Sunshū Ōno shinden), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 14 3/4 in. (25.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2955,false,true,56213,Asian Art,Woodblock print,"冨嶽三十六景 駿州片倉茶園の不二|Fuji from the Katakura Tea Fields in Suruga (Sunshū Katakura chaen no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/4 in. (24.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2956,false,true,56214,Asian Art,Woodblock print,"冨嶽三十六景 尾州不二見原|Fujimigahara in Owari Province (Bishū Fujimigahara), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,10 1/16 x 14 7/8 in. (25.6 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2957,false,true,56215,Asian Art,Woodblock print,"冨嶽三十六景 礫川雪の旦|Morning after the Snow at Koishikawa in Edo (Koishikawa yuki no ashita), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 in. (25.7 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2958,false,true,56216,Asian Art,Woodblock print,"冨嶽三十六景 登戸浦|Noboto Bay (Noboto no ura), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/8 in. (24.8 x 36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2959,false,true,56217,Asian Art,Woodblock print,"冨嶽三十六景 身延川裏不二|View from the Other Side of Fuji from the Minobu River (Minobugawa ura Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 14 5/8 in. (25.4 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2960,false,true,55736,Asian Art,Woodblock print,"冨嶽三十六景 凱風快晴|South Wind, Clear Sky (Gaifū kaisei), also known as Red Fuji, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,Oban 10 x 14 7/8 in. (25.4 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2961,false,true,56229,Asian Art,Print,"冨嶽三十六景 山下白雨|Storm below Mount Fuji (Sanka no haku u), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,10 x 14 3/4 in. (25.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2962,false,true,56235,Asian Art,Woodblock print,"冨嶽三十六景 諸人登山|Groups of Mountain Climbers (Shojin tozan), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 5/8 in. (24.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2963,false,true,56238,Asian Art,Woodblock print,"冨嶽三十六景 上総の海路|At Sea off Kazusa (Kazusa no kairo), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 7/8 in. (24.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2964,false,true,56239,Asian Art,Woodblock print,"冨嶽三十六景 常州牛掘|Ushibori in Hitachi Province (Jōshū Ushibori), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/4 x 14 7/8 in. (26 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2965,false,true,56240,Asian Art,Woodblock print,"冨嶽三十六景 信州諏訪湖|Lake Suwa in Shinano Province (Shinshū Suwako), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/8 in. (26 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2966,false,true,56241,Asian Art,Woodblock print,"冨嶽三十六景 遠江山中|In the Mountains of Tōtomi Province (Tōtomi sanchū), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 3/8 x 15 1/4 in. (26.4 x 38.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2967,false,true,56242,Asian Art,Woodblock print,"冨嶽三十六景 隠田の水車|The Waterwheel at Onden (Onden no suisha), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/4 in. (26 x 38.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2968,false,true,45030,Asian Art,Woodblock print,"冨嶽三十六景 甲州犬目峠|The Inume Pass in Kai Province (Kōshū Inume tōge), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1831–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2969,false,true,54868,Asian Art,Woodblock print,"冨嶽三十六景 甲州三坂水面|Reflection in Lake at Misaka in Kai Province (Kōshū Misaka suimen), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/4 in. (24.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2970,false,true,56346,Asian Art,Woodblock print,"冨嶽三十六景 甲州三島越|Mishima Pass in Kai Province (Kōshū Mishima goe), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1830,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 7/8 in. (24.4 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2971,false,true,56349,Asian Art,Woodblock print,"冨嶽三十六景 甲州伊沢暁|Dawn at Isawa in Kai Province (Kōshū Isawa no akatsuki), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2972,false,true,56353,Asian Art,Woodblock print,"冨嶽三十六景 神奈川沖浪裏|Under the Wave off Kanagawa (Kanagawa oki nami ura), also known as The Great Wave, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2973,false,true,56357,Asian Art,Woodblock print,"冨嶽三十六景 東海道保土ケ谷|Hodogaya on the Tōkaidō (Tōkaidō Hodogaya), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 7/8 in. (24.4 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2974,false,true,56360,Asian Art,Woodblock print,"冨嶽三十六景 東海道吉田|Yoshida on the Tōkaidō (Tōkaidō Yoshida), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 3/8 x 15 1/8 in. (26.4 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2975,false,true,56365,Asian Art,Woodblock print,"冨嶽三十六景 東海道金谷の不二|Fuji Seen from Kanaya on the Tōkaidō (Tōkaidō Kanaya no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/4 in. (26 x 38.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2976,false,true,56373,Asian Art,Woodblock print,"冨嶽三十六景 東海道江尻田子の浦略図|Tago Bay near Ejiri on the Tōkaidō (Tōkaidō Ejiri Tago no ura ryaku zu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/4 in. (24.8 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2977,false,true,56376,Asian Art,Woodblock print,"冨嶽三十六景 相州江の島|Enoshima in Sagami Province (Sōshū Enoshima), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 3/4 in. (25.1 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2978,false,true,56384,Asian Art,Woodblock print,"冨嶽三十六景 相州仲原|Nakahara in Sagami Province (Sōshū Nakahara), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2979,false,true,55737,Asian Art,Woodblock print,"冨嶽三十六景 相州七里浜|Shichirigahama in Sagami Province (Sōshū Shichirigahama), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,Oban 10 x 14 3/4 in. (25.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2980,false,true,56385,Asian Art,Woodblock print,"冨嶽三十六景 相州箱根湖水|The Lake at Hakone in Sagami Province (Sōshū Hakone kosui), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 7/8 in. (24.8 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2981,false,true,55738,Asian Art,Woodblock print,"冨嶽三十六景 相州梅沢左|Umezawa Manor in Sagami Province (Sōshū Umezawa zai), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,Oban 10 1/8 x 15 in. (25.7 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2982,false,true,56386,Asian Art,Woodblock print,"冨嶽三十六景 本所立川|Tatekawa in Honjō (Honjō Tatekawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,10 1/16 x 15 in. (25.6 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2983,false,true,56387,Asian Art,Woodblock print,"冨嶽三十六景 深川万年橋下|Under the Mannen Bridge at Fukagawa (Fukagawa Mannenbashi shita), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,10 x 14 3/8 in. (25.4 x 36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2984,false,true,55740,Asian Art,Woodblock print,"冨嶽三十六景 五百らかん寺さざゐどう|Sazai Hall at the Temple of the Five Hundred Arhats (Gohyaku Rakanji Sazaidō), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,Oban 10 1/4 x 15 1/4 in. (26 x 38.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2985,false,true,56388,Asian Art,Woodblock print,"冨嶽三十六景 青山円座松|Cushion Pine at Aoyama (Aoyama enza no matsu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/4 in. (24.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2986,false,true,39800,Asian Art,Woodblock print,"冨嶽三十六景 甲州石班沢|Kajikazawa in Kai Province (Kōshū Kajikazawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/8 in. (26 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2987,false,true,56389,Asian Art,Woodblock print,"冨嶽三十六景 下目黒|Lower Meguro (Shimo Meguro), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/4 in. (26 x 38.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2988,false,true,56390,Asian Art,Woodblock print,"冨嶽三十六景 武州千住|Senju in Musashi Province (Bushū Senju), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 15 in. (24.4 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2989,false,true,56391,Asian Art,Woodblock print,"冨嶽三十六景 従千住花街眺望の不二|Fuji Seen in the Distance from Senju Pleasure Quarter (Senju kagai yori chōbō no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/4 in. (26 x 38.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2990,false,true,55741,Asian Art,Woodblock print,"冨嶽三十六景 武陽佃島|Tsukudajima in Musashi Province (Buyō Tsukudajima), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,Oban 10 1/8 x 15 in. (25.7 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2991,false,true,56392,Asian Art,Woodblock print,"冨嶽三十六景 武州玉川|Tama River in Musashi Province (Bushū Tamagawa), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2992,false,true,56393,Asian Art,Woodblock print,"冨嶽三十六景 東海道品川御殿山の不二|Fuji from Gotenyama at Shinagawa on the Tōkaidō (Tōkaidō Shinagawa Gotenyama no Fuji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2993,false,true,56394,Asian Art,Woodblock print,"冨嶽三十六景 江戸日本橋|Nihonbashi in Edo (Edo Nihonbashi), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 3/4 in. (25.1 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2994,false,true,53692,Asian Art,Woodblock print,"冨嶽三十六景 江都駿河町三井見世略図|Mitsui Shop at Surugachō in Edo (Edo Surugachō Mitsui mise ryaku zu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 7/8 in. (25.7 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2995,false,true,56395,Asian Art,Woodblock print,"冨嶽三十六景 東都駿台|Surugadai in Edo (Tōto Sundai), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/4 in. (24.8 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2996,false,true,56396,Asian Art,Woodblock print,"冨嶽三十六景 東都浅草本願寺|Honganji at Asakusa in Edo (Tōto Asakusa Honganji), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2997,false,true,37362,Asian Art,Woodblock print,"冨嶽三十六景 御厩川岸より両国橋夕陽見|Viewing the Sunset over Ryōgoku Bridge from the Onmaya Embankment (Onmayagashi yori Ryōgokubashi sekiyō o miru), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–31,1820,1842,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/4 in. (24.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2998,false,true,55742,Asian Art,Woodblock print,"冨嶽三十六景 隅田川関屋の里|Sekiya Village on the Sumida River (Sumidagawa Sekiya no sato), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1830,1832,Polychrome woodblock print; ink and color on paper,Oban 10 1/4 x 15 1/8 in. (26 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3001,false,true,54217,Asian Art,Print,衣食住|Attire,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 1/2 in. (14.3 x 19.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3003,false,true,54219,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1760–1849,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,5 1/8 x 6 13/16 in. (13 x 17.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3138,false,true,56686,Asian Art,Print,"冨嶽三十六景 武州玉川|Fuji—The Tama River, Musashi Province, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1830–32,1820,1842,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 3/4 in. (25.7 x 37.5 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3516,false,true,44900,Asian Art,Print,詩歌写真鏡 李白|Ri Haku from the series Mirrors of Japanese and Chinese Poems (Shiika shashin kyō),Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,20 3/8 x 9 in. (51.8 x 22.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP29a–e,false,true,36483,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1804,1794,1814,Pentaptych of polychrome woodblock prints; ink and color on paper,"Oban, pentaptych: 14 15/32 x 48 1/2 in. (36.8 x 123.2 cm)","Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP30,false,true,36510,Asian Art,Print,東都名所 御殿山之夕桜|Evening Cherries on Gotem Yama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830,1830,1830,Polychrome woodblock print; ink and color on paper,9 x 13 15/16 in. (22.9 x 35.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP31,false,true,36511,Asian Art,Print,金沢八景 乙艫帰帆|Otomo Kihan,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 15/32 in. (23.5 x 36.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP32,false,true,36512,Asian Art,Print,江戸近郊八景之内 羽根田落雁|Haneda Rakugan,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 15/32 x 13 27/32 in. (24.1 x 35.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP33,false,true,36513,Asian Art,Print,木曽海道六拾九次之内 大井|Ōi Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 15/32 in. (23.5 x 36.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP35,false,true,36515,Asian Art,Print,"京都名所之内 あらし山満花|Cherry Blossoms at Arashiyama, from the series Famous Places of Kyōto",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 3/8 x 14 15/32 in. (23.8 x 36.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP36,false,true,36516,Asian Art,Print,京都名所之内 八瀬之里|Yase no Sato,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,10 7/32 x 15 1/8 in. (26.0 x 38.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP37,false,true,36517,Asian Art,Print,京都名所之内 淀川|Yodogawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 29/32 x 14 7/8 in. (25.2 x 37.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP38,false,true,36518,Asian Art,Print,東海道五十三次 見附 天竜川|Mitsukei Tenryugawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 29/32 in. (24.8 x 37.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP40,false,true,36520,Asian Art,Print,東海道五十三次 三島 朝霧|Morning Mist at Mishima,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 7/8 in. (37.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP41,false,true,36521,Asian Art,Print,"東海道五十三次・庄野 白雨|Sudden Shower at Shōno, from the series Fifty-three Stations of the Tōkaidō",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834–35,1834,1835,Polychrome woodblock print; ink and color on paper,Image: 9 3/4 x 14 1/4 in. (24.8 x 36.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP42,false,true,36522,Asian Art,Print,木曽海道六拾九次之内 和田|Wada Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/4 in. (24.1 x 36.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP43,false,true,36523,Asian Art,Print,木曽海道六拾九次之内 下諏訪|Shimono Suwa Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 9/16 x 14 3/8 in. (24.3 x 36.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP44,false,true,36524,Asian Art,Print,吾妻の森|View of Azuma Wood,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1858,1848,1868,Polychrome woodblock print (surimono); ink and color on paper,20 3/8 x 7 7/32 in. (51.8 x 18.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP45,false,true,45321,Asian Art,Print,Asakusa Kinryuzan shita Azumabashi uchu nozomi|東都名所 浅草金龍山下東橋雨中望|View of the Asakusa Kinryuzan Temple from the Azuma Bridge in the Rain,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,"Polychrome woodblock print; ink and color on paper, tanzaku format",14 9/16 x 5 in. (37.0 x 12.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP46,false,true,36525,Asian Art,Print,東都名所 佃島海辺朧月|Tsukudajima Kaihin Rōgetsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,13 15/16 x 4 3/4 in. (35.4 x 12.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP47,false,true,36526,Asian Art,Print,東都名所 新吉原衣紋阪秋月|Shin Yoshiwara Emonzaka Aki no Tsuki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,14 3/4 x 4 7/8 in. (37.5 x 12.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP48,false,true,36527,Asian Art,Print,近江八景之内 石山秋月|The Autumn Moon at Ishiyama on Lake Biwa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 15/32 in. (25.1 x 36.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP49,false,true,36528,Asian Art,Print,"近江八景之内 瀬田夕照|Seta no Sekisho. Sunset, Seta. Lake Biwa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 23/32 x 13 5/8 in. (22.2 x 34.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP50,false,true,36529,Asian Art,Print,"近江八景之内 粟津晴嵐|Clearing Weather at Awazu, Lake Biwa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 23/32 x 13 3/8 in. (22.2 x 34.0 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP51,false,true,36530,Asian Art,Print,"近江八景之内 堅田落雁|Geese Alighting at Katada, Lake Biwa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 13/16 x 13 5/8 in. (22.4 x 34.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP52,false,true,36531,Asian Art,Print,"近江八景之内 唐崎夜雨|Night Rain at Karasaki, from the series Eight Views of Ō-mi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,Image: 8 3/4 × 13 5/8 in. (22.2 × 34.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP53,false,true,36532,Asian Art,Woodblock print,"近江八景之内 三井晩鐘|Vesper Bell at Mii Temple, Lake Biwa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 23/32 x 13 1/2 in. (22.2 x 34.3 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP54,false,true,36533,Asian Art,Print,"近江八景之内 比良暮雪|Evening Snow on Hira, Lake Biwa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 23/32 in. (22.5 x 34.9 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP55,false,true,36534,Asian Art,Woodblock print,"近江八景之内 矢橋帰帆|Sailing Boats Returning to Yabase, Lake Biwa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 9/16 x 14 in. (24.3 x 35.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP56,false,true,36535,Asian Art,Print,江戸名所 御茶之水|Ochanomizu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"dated 11th month, Ox year, 1853",11,1853,Polychrome woodblock print; ink and color on paper,Oban 9 1/8 x 14 in. (23.2 x 35.6 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP57,false,true,36536,Asian Art,Print,"富士三十六景 東都両ごく|Toto, Ryogoku, from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"4th month, Horse year 1858",1858,1858,Polychrome woodblock print; ink and color on paper,14 15/32 x 9 15/32 in. (36.8 x 24.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP58,false,true,36537,Asian Art,Print,"富士三十六景 武蔵越かや在|View of Mount Fuji from Koshigaya, Province of Musashi (Musashi, Koshigaya Zai), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"4th month, Horse year 1858",1858,1858,Polychrome woodblock print; ink and color on paper,13 27/32 x 9 1/4 in. (35.2 x 23.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP59,false,true,36538,Asian Art,Print,"富士三十六景 相模七里ケ浜|View of Mount Fuji from Seven-ri Beach, Province of Sagami (Sōshū: Shichi-ri ga hama), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"dated 4th month, Horse year 1858",1858,1858,Polychrome woodblock print; ink and color on paper,13 27/32 x 9 1/4 in. (35.2 x 23.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP60,false,true,36539,Asian Art,Print,"名所江戸百景 浅草田圃酉の町詣|Revelers Returned from the Tori no Machi Festival at Asakusa, from the series One Hundred Famous Views of Edo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,13 1/16 x 8 11/16 in. (33.2 x 22.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP61,false,true,36540,Asian Art,Print,"Ukeji, Akiba no Keidai|名所江戸百景 請地秋葉の境内|Inside the Akiba Shrine at Ukeji",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"8th month, Snake year 1857",1857,1857,Polychrome woodblock print; ink and color on paper,13 1/4 x 8 23/32 in. (33.7 x 22.2 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP62,false,true,36541,Asian Art,Print,"名所江戸百景 市中繁栄七夕祭|The Tanabata Festival, from the series One Hundred Famous Views of Edo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 x 9 3/5 in. (35.6 x 24.4 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP63,false,true,36542,Asian Art,Woodblock print,"「名所江戸百景 蓑輪 金杉 三河しま」|“Minowa, Kanasugi at Mikawashima,” from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei, Minowa Kanasugi, Mikawashima)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,snake year 1857,1857,1857,Polychrome woodblock print; ink and color on paper,13 1/4 x 8 17/32 in. (33.7 x 21.7 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP64,false,true,36543,Asian Art,Print,"名所江戸百景 真崎辺より水神の森内川関屋の里を見る|The Suijin Temple Grove, Uchikawa, and the Village of Sekiya",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,8th month 1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 13 1/8 in. (33.3 cm); W. 8 11/16 in. (22.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP65,false,true,36544,Asian Art,Print,名所江戸百景 廓中東雲|Kakuchu Shinonome,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"4th month, Snake year 1857",1857,1857,Polychrome woodblock print; ink and color on paper,14 3/5 x 9 7/8 in. (37.1 x 25.1 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP66,false,true,36545,Asian Art,Woodblock print,Kisojo no San Sen|木曽路之山川|The Kiso Mountains in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"dated 8th month of the Snake year, 1857",1857,1857,Triptych of woodblock prints; ink and color on paper,Each H. 14 1/4 in. (36.2 cm); W. 9 3/4 in. (24.8 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP67,false,true,36546,Asian Art,Print,東海道五十三次 品川|Shinagawa Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP68,false,true,36547,Asian Art,Print,東海道五十三次 川崎|Kawasaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP69,false,true,36548,Asian Art,Print,東海道五十三次 神奈川|Kanagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP70,false,true,36549,Asian Art,Print,東海道五十三次 保土ヶ谷|Hodogaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP71,false,true,36550,Asian Art,Print,東海道五十三次 戸塚|Totsuka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP72,false,true,36551,Asian Art,Print,東海道五十三次 藤沢|Fujisawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP73,false,true,36552,Asian Art,Print,東海道五十三次 平塚|Hiratsuka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP74,false,true,36553,Asian Art,Print,東海道五十三次 大磯|Ōiso,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP75,false,true,36554,Asian Art,Print,東海道五十三次 小田原|Odawara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP76,false,true,36555,Asian Art,Print,東海道五十三次 箱根|Hakone,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP77,false,true,36556,Asian Art,Print,"東海道五十三次 三島|Mishima, from the series Fifty-three Stations of the Tōkaidō Road (Tōkaidō gojūsan tsugi, Mishima), also known as the Kyōka (Witty Verse) Tōkaidō",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP78,false,true,36557,Asian Art,Print,東海道五十三次 沼津|Numazu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP79,false,true,36558,Asian Art,Print,東海道五十三次 原|Hara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP80,false,true,36559,Asian Art,Woodblock print,東海道五十三次 蒲原|Kambara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP81,false,true,36560,Asian Art,Print,東海道五十三次 興津|Okitsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP82,false,true,36561,Asian Art,Print,東海道五十三次 江尻|Ejiri,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP83,false,true,36562,Asian Art,Print,東海道五十三次 府中|Fuchu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP84,false,true,36563,Asian Art,Print,東海道五十三次 鞠子|Mariko,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP85,false,true,36564,Asian Art,Print,東海道五十三次 岡部|Okabe,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP86,false,true,36565,Asian Art,Print,東海道五十三次 藤枝|Fujieda,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36565,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP87,false,true,36566,Asian Art,Print,東海道五十三次 金谷|Kanaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP88,false,true,36567,Asian Art,Print,東海道五十三次 袋井|Fukuroi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP89,false,true,36568,Asian Art,Print,東海道五十三次 見附|Mitsuki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP90,false,true,36569,Asian Art,Print,東海道五十三次 舞阪|Maizaka Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP91,false,true,36570,Asian Art,Print,東海道五十三次 荒井|Arai,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP92,false,true,36571,Asian Art,Print,東海道五十三次 白須賀|Shirasuka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP93,false,true,36572,Asian Art,Print,東海道五十三次 赤坂|Akasaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP94,false,true,36573,Asian Art,Print,東海道五十三次 岡崎|Okazaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP95,false,true,36574,Asian Art,Print,東海道五十三次 池鯉鮒|Chiryu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP96,false,true,36575,Asian Art,Print,東海道五十三次 鳴海|Narumi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP97,false,true,36576,Asian Art,Print,東海道五十三次 宮|Miya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP98,false,true,36577,Asian Art,Print,東海道五十三次 桑名|Kuwana,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP99,false,true,36578,Asian Art,Print,東海道五十三次 四日市|Yokkaichi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36578,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP100,false,true,36579,Asian Art,Print,東海道五十三次 石薬師|Ishiyakushi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP101,false,true,36580,Asian Art,Print,東海道五十三次 庄野|Shono,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP102,false,true,36581,Asian Art,Print,東海道五十三次 亀山|Kameyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP103,false,true,36582,Asian Art,Print,東海道五十三次 関|Seki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP104,false,true,36583,Asian Art,Print,東海道五十三次 坂下|Saka-no-shita,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP105,false,true,36584,Asian Art,Print,東海道五十三次 土山 鈴鹿山の図|Tsuchiyama: Suzuka-yama no zu.,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP106,false,true,36585,Asian Art,Print,東海道五十三次 水口|Mizukuchi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP107,false,true,36586,Asian Art,Print,東海道五十三次 草津|Kusatsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP108,false,true,36587,Asian Art,Print,東海道五十三次 大津|Otsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP109,false,true,36588,Asian Art,Print,東海道五十三次 京 三条大橋図|Kyoto: Sanju Ohashi no zu.,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP110,false,true,36589,Asian Art,Print,東海道五十三 京 内裏|Kyoto: Dairi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 1/2 in. (21.6 x 16.5 cm),"Rogers Fund, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP217,false,true,36693,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 in. × 10 in. (36.5 × 25.4 cm) Mat: 12 5/8 × 18 1/2 in. (32.1 × 47 cm),"Gift of Mrs. Russell Sage, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP238,false,true,36710,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 1830s,1830,1835,Polychrome woodblock print; ink and color on paper,13 1/2 x 4 15/32 in. (34.3 x 11.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP239,false,true,36711,Asian Art,Print,歌川広重画 罌栗に鶉|Quails and Poppies,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,14 x 5 in. (35.6 x 12.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP243,false,true,36715,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,Overall: 14 7/8 x 5 1/16 in. (37.8 x 12.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP244,false,true,36716,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,10 x 4 7/8 in. (25.4 x 12.4 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP245,false,true,36717,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,9 7/8 x 4 11/16 in. (25.1 x 11.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP246,false,true,36718,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,8 1/2 x 6 3/8 in. (21.6 x 16.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP247,false,true,36719,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,8 27/32 x 6 3/8 in. (22.5 x 16.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP248,false,true,36720,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1837,1827,1847,Polychrome woodblock print,10 1/8 x 7 1/2 in. (25.7 x 19.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP249,false,true,36721,Asian Art,Woodblock print,紅蜀葵に燕と川蝉図|Swallows and Kingfisher with Rose Mallows,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print,10 7/16 x 7 11/32 in. (26.5 x 18.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP250,false,true,36722,Asian Art,Woodblock print,椿に目白と四十雀図|Japanese White-eye and Titmouse on a Camellia Branch,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print,10 7/32 x 7 7/32 in. (26.0 x 18.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP251,false,true,36723,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,8 15/16 x 6 11/16 in. (22.7 x 17.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP252,false,true,36724,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 15/16 x 6 3/5 in. (22.7 x 16.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP253,false,true,36725,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 27/32 x 6 9/16 in. (22.5 x 16.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP254,false,true,36726,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,mid-1840s,1843,1846,Polychrome woodblock print; ink and color on paper,8 27/32 x 6 3/5 in. (22.5 x 16.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP255,false,true,36727,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 23/32 x 6 9/16 in. (22.2 x 16.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP256,false,true,36728,Asian Art,Print,歌川広重画 芙蓉に高麗鶯|Black-naped Oriole Perched on a Stem of Rose Mallow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,14 11/16 x 4 29/32 in. (37.3 x 12.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP257,false,true,36729,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print,14 15/32 x 5 in. (36.8 x 12.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP258,false,true,36730,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,12 31/32 x 4 1/4 in. (33.0 x 10.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP259,false,true,36731,Asian Art,Print,歌川広重画 菊に百舌鳥|Shrike and Chrysanthemums,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1830,1820,1840,Polychrome woodblock print (hosoban); ink and color on paper,14 7/8 x 5 in. (37.8 x 12.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP260,false,true,36732,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,13 3/32 x 4 7/16 in. (33.2 x 11.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP261,false,true,36733,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,15 x 5 in. (38.1 x 12.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP262,false,true,36734,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,13 1/10 x 4 3/8 in. (33.3 x 11.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP263,false,true,36735,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,13 3/32 x 4 15/32 in. (33.2 x 11.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP264,false,true,36736,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,14 3/5 x 4 29/32 in. (37.1 x 12.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP265,false,true,36737,Asian Art,Print,歌川広重画|Morning Glories with Poem by Gyōkō,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1843,1833,1853,Polychrome woodblock print (hosoban); ink and color on paper,12 15/16 x 4 3/8 in. (32.9 x 11.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP266,false,true,36738,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1843,1841,1845,Polychrome woodblock print; ink and color on paper,13 1/4 x 4 15/32 in. (33.7 x 11.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP267,false,true,36739,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1831,1835,Polychrome woodblock print; ink and color on paper,14 15/32 x 6 11/16 in. (36.8 x 17.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP268,false,true,36740,Asian Art,Print,波に丹頂鶴|Crane and Surf,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,14 1/8 x 6 3/8 in. (35.9 x 16.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP269,false,true,36741,Asian Art,Print,歌川広重画 楓に孔雀|Peacock Perched on a Maple Tree,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print (hosoban); ink and color on paper,14 13/16 x 5 in. (37.6 x 12.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP270,false,true,36742,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,15 x 4 29/32 in. (38.1 x 12.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP271,false,true,36743,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,14 29/32 x 4 7/8 in. (37.9 x 12.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP272,false,true,36744,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,10 x 4 3/4 in. (25.4 x 12.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP273,false,true,36745,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,10 1/5 x 4 29/32 in. (25.9 x 12.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP471,false,true,36922,Asian Art,Print,東海道五十三次之内 日本橋 朝之景|Stations One: Morning View of Nihonbashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 13 7/8 in. (35.2 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP472,false,true,36923,Asian Art,Print,東海道五十三次之内 品川 日之出|Daybreak at Shinagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 3/5 in. (24.1 x 37.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP473,false,true,36924,Asian Art,Print,東海道五十三次之内 川崎 六郷渡舟|Ferry Boat Crossing the Rokugo River,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 3/4 in. (24.1 x 37.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP474,false,true,36925,Asian Art,Print,東海道五十三次之内 神奈川 宿台之景|View of the Kanagawa station at sunset,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP475,false,true,36926,Asian Art,Print,東海道五十三次之内 保土ヶ谷 新町橋|Hodogaya Station and Shinkame Bridge,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP476,false,true,36927,Asian Art,Print,東海道五十三次之内 戸塚 元町別道|Totsuka; Moto Machi Betsudo,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP477,false,true,36928,Asian Art,Print,東海道五十三次之内 藤澤 遊行寺|Fujiwara; Yugyoji,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP478,false,true,36929,Asian Art,Print,東海道五十三次之内 平塚 縄手道|Hiratsuka; Nawate Do,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP479,false,true,36930,Asian Art,Woodblock print,東海道五十三次之内 大磯 虎ケ雨|Tiger Rain at Ōiso Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP480,false,true,36931,Asian Art,Print,東海道五十三次之内 小田原 酒匂川|Odawara; Sakogawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP481,false,true,36932,Asian Art,Print,東海道五十三次之内 箱根 湖水図|Hakone; Kosui,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP482,false,true,36933,Asian Art,Print,"東海道五十三次之内 三島 朝霧|Mishima, Asa Kiri",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 7/8 in. (24.1 x 37.8 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP483,false,true,36934,Asian Art,Print,東海道五十三次之内 沼津 黄昏図|Numazu Ki Kure,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP484,false,true,36935,Asian Art,Print,東海道五十三次之内 原 朝の富士|Hara; Asa no Fuji,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP485,false,true,36936,Asian Art,Print,"東海道五十三次之内 吉原 左富士|Yoshiwara, Hidari Fuji",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP486,false,true,36937,Asian Art,Woodblock print,東海道五十三次之内 蒲原 夜の雪|A Snowy Evening at Kambara Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 × 14 1/8 in. (24.1 × 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP487,false,true,36938,Asian Art,Print,東海道五十三次之内 由井 薩埵嶺|Satta Peak at Yui,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP488,false,true,36939,Asian Art,Print,"東海道五十三次之内 奥津 興津川|Okitsu, Okitsugawa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP489,false,true,36940,Asian Art,Print,東海道五十三次之内 江尻 三保遠望|Distant View of Miho Beach from Ejiri,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP490,false,true,36941,Asian Art,Print,東海道五十三次之内 府中 安部川|Travellers Fording the Abe River at Fuchu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP491,false,true,36942,Asian Art,Print,東海道五十三次之内 鞠子 名物茶店|Mariko; Meibutsu Chaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP492,false,true,36943,Asian Art,Print,東海道五十三次之内 岡部 宇津の山|Okabe; Utsu no Yama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP493,false,true,36944,Asian Art,Print,東海道五十三次之内 藤枝 人馬継立|Fujieda; Hito Uma Keitatsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP494,false,true,36945,Asian Art,Print,"東海道五十三次之内 嶋田 大井川駿岸|Shimada, Oigawa Shun Gan",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP495,false,true,36946,Asian Art,Print,"東海道五十三次之内 金谷 大井川遠岸|Kanaya, Oigawa Em Gan",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP496,false,true,36947,Asian Art,Print,"東海道五十三次之内 日坂 佐夜の中山|Nissaka, Sayo Nakayama",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP497,false,true,36948,Asian Art,Print,"東海道五十三次之内 掛川 秋葉山遠望|Kakegawa, Akihasan Empo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 3/5 x 14 1/8 in. (24.4 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP498,false,true,36949,Asian Art,Print,東海道五十三次之内 袋井 出茶屋の図|Fukuroi; De Chaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP500,false,true,36951,Asian Art,Print,"東海道五十三次之内 濱松 冬枯の図|Hamamatsu, Toko no Zu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP501,false,true,36952,Asian Art,Print,東海道五十三次之内 舞坂 今切真景|View of Imaki Point from Maizaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP502,false,true,36953,Asian Art,Print,"東海道五十三次之内 荒井 渡舟の図|Arai, Tosen",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP503,false,true,36954,Asian Art,Print,"東海道五十三次之内 白須賀 汐見阪図|Shirasuka, Shio-mi Zaka",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP504,false,true,36955,Asian Art,Print,"東海道五十三次之内 二川 猿ヶ馬場|Futagawa, Saru ga Baba",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP505,false,true,36956,Asian Art,Print,"東海道五十三次之内 吉田 豊川橋|Yoshida, Toyokawa Hashi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP506,false,true,36957,Asian Art,Print,"東海道五十三次之内 御油 旅人留女|Goyu, Tabibito Ryujo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP507,false,true,36958,Asian Art,Print,"東海道五十三次之内 赤阪 旅舎招婦の図|Akasaka, Ryosha Sho-fu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP508,false,true,36959,Asian Art,Print,"東海道五十三次之内 藤川 棒鼻の図|Fujikawa, Bo Bana",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP509,false,true,36960,Asian Art,Print,"東海道五十三次之内 岡崎 矢矧の橋|Okazaki, Tenshin no Hashi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP510,false,true,36961,Asian Art,Print,"東海道五十三次之内 池鯉鮒 首夏馬市|Chiryu, Shuka Uma Ichi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP511,false,true,36962,Asian Art,Print,"東海道五十三次之内 鳴海 名物有松絞|Narumi, Meibutsu Arimatsu Shibori",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP512,false,true,36963,Asian Art,Print,"東海道五十三次之内 宮 熱田神事|Miya, Atsuta Shin Ji",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP513,false,true,36964,Asian Art,Print,"東海道五十三次之内 桑名 七里渡口|Kuwana, Shichi-Ri Watashi Guchi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP514,false,true,36965,Asian Art,Print,"東海道五十三次之内 四日市 三重川|Yokkaichi, Sanchokawa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP515,false,true,36966,Asian Art,Print,"東海道五十三次之内 石薬師 石薬師寺|Ishiyakushi, Ishiyakushi Ji",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP516,false,true,36967,Asian Art,Print,東海道五十三次之内 庄野 白雨|Sudden Shower in Shōno,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1832,1836,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP517,false,true,36968,Asian Art,Print,"東海道五十三次之内 亀山 雪晴|Kameyama, Yuki Hare",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP518,false,true,36969,Asian Art,Print,"東海道五十三次之内 関 本陣早立|Seki, Honjin Sotatsu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP519,false,true,36970,Asian Art,Print,"東海道五十三次之内 阪之下 筆捨嶺|Saka-no-shita, Fude-sute Mine",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,Image: 9 1/2 × 14 1/8 in. (24.1 × 35.9 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP520,false,true,36971,Asian Art,Print,"東海道五十三次 土山 春の雨|Spring Rain at Tsuchiyama, from the series Fifty-three Stations of the Tōkaidō",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834–35,1834,1835,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 in. (35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP521,false,true,36972,Asian Art,Print,"東海道五十三次之内 水口 名物干瓢|Mizukuchi, Meibutsu Kampyo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP522,false,true,36973,Asian Art,Print,"東海道五十三次之内 石部 目川の里|Ishibe, Megawa Sato",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 1/16 in. (24.1 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP523,false,true,36974,Asian Art,Print,"東海道五十三次之内 草津 名物立場|Kusatsu: Famous Post House (Kusatsu, Meibutsu tateba), from the series Fifty-three Stations of the Tōkaidō Road (Tōkaidō gojūsan tsugi no uchi)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP524,false,true,36975,Asian Art,Print,"東海道五十三次之内 大津 走井茶屋|Otsu, Soii Chaya",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 in. (24.1 x 35.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP525,false,true,36976,Asian Art,Print,"東海道五十三次之内 大尾 京師 三条大橋|Kyoto, Sanjo Ohashi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 3/5 x 14 1/16 in. (24.4 x 35.7 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP526,false,true,36977,Asian Art,Print,東都名所 日本橋の白雨|Sunshower at Nihonbashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 in. (25.7 x 38.1 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP567,false,true,37018,Asian Art,Woodblock print,忠臣蔵 十一段目 夜打 押寄|The Loyal Ronin Crossing the Long Bridge to Embark for the Night Attack upon Moronao,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,9 1/8 x 14 in. (23.2 x 35.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP568,false,true,37019,Asian Art,Print,東海道五十三次之内 荒井 海上壹リ半舟渡之図|Arai,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 x 13 5/8 in. (22.9 x 34.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP569,false,true,37020,Asian Art,Print,東海道五十三次之内 吉原|Yoshiwara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 x 13 5/8 in. (22.9 x 34.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP570,false,true,37021,Asian Art,Print,東海道五十三次之内 川崎 六郷の渡し舟|Kawasaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,Overall: 9 x 13 3/4in. (22.9 x 34.9cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP571,false,true,37022,Asian Art,Print,東海道五十三次之内 亀山|Kameyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 1/16 x 13 7/10 in. (23.0 x 34.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP572,false,true,37023,Asian Art,Print,東海道五十三次之内 赤阪|Akasaka Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 1/2 in. (22.5 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP573,false,true,37024,Asian Art,Print,東海道五十三次之内 石薬師|Gyosho Tokaido: Ishikushi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 13/16 x 13 17/32 in. (22.4 x 34.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP574,false,true,37025,Asian Art,Print,東海道五十三次之内 蒲原 岩淵よりふじ川を見る圖|Kambura Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 1/2 in. (22.5 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP575,false,true,37026,Asian Art,Print,東海道五十三次之内 品川 鮫洲朝之景|Shinagawa Samesu Asa no Kei,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 1/2 in. (22.5 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP576,false,true,37027,Asian Art,Print,東海道五十三次之内 岡部 宇津の山之図|Okabe,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 1/8 x 13 23/32 in. (23.2 x 34.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP577,false,true,37028,Asian Art,Print,東海道五十三次之内 平塚 馬入川舟渡しの図|Hiratsuka; Banyugawa Funa Watashi no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 1/2 in. (22.5 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP578,false,true,37029,Asian Art,Print,東海道五十三次之内 嶋田 大井川駿岸|Shimada,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 x 13 5/8 in. (22.9 x 34.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP579,false,true,37030,Asian Art,Print,東海道五十三次之内 草津|Kusatsu Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 1/2 in. (22.5 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP580,false,true,37031,Asian Art,Print,東海道五十三次之内 大津|Otsu Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 1/2 in. (22.5 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP581,false,true,37032,Asian Art,Print,東海道五十三次之内 神奈川 浅間下より台を見る図|Kanazawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 1/8 x 13 23/32 in. (23.2 x 34.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP582,false,true,37033,Asian Art,Print,東海道五十三次之内 江尻 清水之湊遠望|Ejiri,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 27/32 x 13 1/2 in. (22.5 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP583,false,true,37034,Asian Art,Print,東海道五十三次之内 袋井 出茶屋の図|Fukuroi; De Chaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 1/3 in. (24.8 x 36.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP584,false,true,37035,Asian Art,Print,東海道五十三次之内 品川 諸侯出立|Shinagawa; Shoko Detachi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 1/4 in. (23.5 x 36.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP585,false,true,37036,Asian Art,Print,木曽海道六拾九次之内 望月|Mochizuki Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 7/8 in. (24.8 x 37.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP586,false,true,37037,Asian Art,Print,木曽海道六拾九次之内 芦田|Ashida Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,Oban 10 x 14 29/32 in. (25.4 x 37.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP587,false,true,37038,Asian Art,Print,"本朝名所 相州七里ヶ浜|Soshu, Shichi-ri ga Hama",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,9 x 14 1/3 in. (22.9 x 36.4 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP588,false,true,37039,Asian Art,Print,"本朝名所 相州江ノ嶋岩屋之図|Soshu, Enoshima Iwaya no Zu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 3/5 in. (25.1 x 37.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP589,false,true,37040,Asian Art,Print,"六十余州名所図会 隠岐 焚火の社|The Takihi Shrine, Oki Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,14 x 9 7/16 in. (35.6 x 24 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP590,false,true,37041,Asian Art,Print,"六十余州名所図会 大和 立田山 龍田川|Yamato, Tatsutayama, Tatsutagawa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,7th month ox year 1853,1853,1853,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 3/4 in. (35.9 x 24.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP591,false,true,37042,Asian Art,Print,"「六十余州名所図絵 伊予 西条」|“Iyo Province, Saijō ,” from the series Views of Famous Places in the Sixty-odd Provinces (Rokujū yoshū meisho zu-e, Iyo, Saijō)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 15/32 in. (36.2 x 24.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP592,false,true,37043,Asian Art,Woodblock print,六十余州名所図会 対馬 海岸夕晴|Tsushima Kaigan Yubare,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,3rd month dragon year 1856,1856,1856,Polychrome woodblock print; ink and color on paper,14 x 9 3/5 in. (35.6 x 24.4 cm),"Purchase, Joseph Pultizer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP593,false,true,37044,Asian Art,Print,"六十余州名所図会 因幡 か路小山|View of Kajikoyama, Inaba Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,Aiban; 13 15/16 x 9 1/4 in. (35.4 x 23.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP594,false,true,37045,Asian Art,Print,"六十余州名所図会 武蔵 隅田川 雪の朝|Morning after a Snowfall, the Sumida River, Musashi Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,14 1/8 x 9 1/3 in. (35.9 x 23.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP595,false,true,37046,Asian Art,Print,"六十余州名所図会 播磨 舞子の浜|Harima, Maiko no Hama",Japan,Edo period (1615–1868),,,,Artist,Designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1853–56,1853,1856,Polychrome woodblock print; ink and color on paper,Oban tate-e 14 x 9 7/16 in. (35.6 x 24 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP596,false,true,37047,Asian Art,Print,"Kazusa Yazashi-ga-ura tsumei|六十余州名所図会 上総 矢さしか浦 通名九十九里|Yasashi Beach, known as Kujūkuri, Kazusa Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,Oban tate-e 14 3/5 x 10 1/8 in. (37.1 x 25.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP597,false,true,37048,Asian Art,Print,"東都名所 二丁町芝居の図|View of the Kabuki Theaters at Sakai-cho on Opening Day of the New Season (Sakai-cho Shibai no Zu), from the series, ""Toto Meisho""",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 7/8 in. (25.7 x 37.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP598,false,true,37049,Asian Art,Print,東都名所 二丁町芝居の図|Sakai Cho Shibai no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,10 x 14 29/32 in. (25.4 x 37.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP599,false,true,37050,Asian Art,Print,江都名所 新橋の図|Shimbashi no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1837,1827,1847,Polychrome woodblock print; ink and color on paper,10 1/8 x 15 in. (25.7 x 38.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP600,false,true,37051,Asian Art,Print,東都名所 芝増上寺山内の図|Shiba Zōjōji Sannai no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1831,1836,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 7/8 in. (25.7 x 37.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP601,false,true,37052,Asian Art,Print,東都名所 駿河町の図|Suruga Street,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,9 9/16 x 14 1/2 in. (24.3 x 36.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP602,false,true,37053,Asian Art,Print,東都名所 亀戸天満宮境内雪|Kameido Tenmangu Keidai no Yuki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1831,1836,Polychrome woodblock print; ink and color on paper,10 x 14 5/8 in. (25.4 x 37.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP603,false,true,37054,Asian Art,Print,東都名所 亀戸天満宮境内雪|Kameido Tenmangu Keidai no Yuki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1831,1836,Polychrome woodblock print; ink and color on paper,9 3/8 x 13 11/16 in. (23.8 x 34.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP604,false,true,37055,Asian Art,Print,"東都名所 芝赤羽根之雪|Shiba, Akabane no Yuki",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1837,1827,1847,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 15/16 in. (25.7 x 37.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP605,false,true,37056,Asian Art,Print,東都名所 高輪の夕景|Takanawa no Yukei,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 1/2 in. (24.1 x 36.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP606,false,true,37057,Asian Art,Print,東都名所 芝愛宕山上の図|Shiba Atago Sanjo no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1831,1836,Polychrome woodblock print; ink and color on paper,9 13/16 x 14 11/16 in. (24.9 x 37.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP607,false,true,37058,Asian Art,Print,東都名所 浅草金龍山年の市|Year End Fair at Kinryuzan Temple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,9 15/16 x 14 5/8 in. (25.2 x 37.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP608,false,true,37059,Asian Art,Print,東都名所 吉原夜桜の図|Yoshiwara Yo Zakura no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1841,1831,1851,Polychrome woodblock print; ink and color on paper,9 13/16 x 14 3/8 in. (24.9 x 36.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP609,false,true,37060,Asian Art,Print,東都名所 浅草金龍山年の市|Asakusa Kinryusan Toshi no Ichi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,9 7/16 x 14 7/16 in. (24 x 36.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP610,false,true,37061,Asian Art,Print,江都名所 飛鳥山はな見|Asakayama Hanami,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 3/4 in. (23.5 x 37.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP611,false,true,37062,Asian Art,Print,東都名所 両国花火の図|Ryogoku Hanabi no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1841,1831,1851,Polychrome woodblock print; ink and color on paper,10 3/32 x 14 3/4 in. (25.6 x 37.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP612,false,true,37063,Asian Art,Print,新撰江戸名所 高輪廿六夜之図|Takanawa Ni-ju-roku Ya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1838,1842,Polychrome woodblock print; ink and color on paper,8 1/2 x 13 5/8 in. (21.6 x 34.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP613,false,true,37064,Asian Art,Print,東都名所 高輪之図|Takanawa no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1841,1831,1851,Polychrome woodblock print; ink and color on paper,10 5/32 x 14 11/16 in. (25.8 x 37.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP614,false,true,37065,Asian Art,Print,東都名所 新吉原日本堤衣紋坂曙|Shin Yoshiwara Nihon Tsutsumi Emonzaka Akatsuki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,10 1/5 x 14 7/16 in. (25.9 x 36.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP616,false,true,37067,Asian Art,Print,東都名所 新吉原|Shin Yoshiwara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,10 5/16 x 14 7/8 in. (26.2 x 37.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP617,false,true,37068,Asian Art,Print,東都名所 佃島初郭公|Tsukudajima Hatsu Hototogisu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,probably 1830,1828,1832,Polychrome woodblock print; ink and color on paper,8 23/32 x 14 1/16 in. (22.2 x 35.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP618,false,true,37069,Asian Art,Print,"東都名所 外桜田弁慶桜の井|Soto Sakurada, Benkei Bori, Sakura-no-i",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 3/4 in. (25.7 x 37.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP620,false,true,37071,Asian Art,Print,日本湊尽 相州浦賀|Uraga Harbor,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1837,1827,1847,Polychrome woodblock print; ink and color on paper,8 5/8 x 13 5/8 in. (21.9 x 34.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP621,false,true,37072,Asian Art,Print,"日本湊尽 東都品川|Toto, Shinagawa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 15/32 in. (24.8 x 36.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP622,false,true,37073,Asian Art,Print,"江戸名所 上野不忍の池|Ueno, Shinobazu no Ike",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1833,1837,Polychrome woodblock print; ink and color on paper,8 5/8 x 13 1/2 in. (21.9 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP623,false,true,37074,Asian Art,Print,"諸国六玉河 陸奥 野田の玉川|Mutsu, Noda no Tamagawa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/5 in. (24.8 x 37.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP624,false,true,37075,Asian Art,Print,諸国六玉河 武蔵 調布の玉川|Musashi: Chōfu no Tamagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/4 in. (24.8 x 37.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP625,false,true,37076,Asian Art,Print,"山海見立相撲 摂津安治川口|The Harbor of Ajikawa, Settsu Province",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1858,1858,1858,Polychrome woodblock print; ink and color on paper,9 11/16 x 14 1/4 in. (24.6 x 36.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP626,false,true,37077,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,6th month ox year 1853,1853,1853,Polychrome woodblock print; ink and color on paper,9 7/16 x 14 11/16 in. (24.0 x 37.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP627,false,true,37078,Asian Art,Print,東都名所 神田明神境内雪晴之図|Kanda Myojin Kyodai Yuki Hare no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1852,Polychrome woodblock print; ink and color on paper,9 15/32 x 14 3/5 in. (24.1 x 37.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP628,false,true,37079,Asian Art,Print,東都名所 永代橋佃沖漁舟|Eitai Bashi Tsukudajima Ryosen,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 7/16 x 14 1/16 in. (24.0 x 35.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP629,false,true,37080,Asian Art,Print,東都名所 上野東叡山ノ圖|Ueno Toezan no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,9 7/16 x 13 27/32 in. (24.0 x 35.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP630,false,true,37081,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,9 11/16 x 14 1/4 in. (24.6 x 36.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP631,false,true,37082,Asian Art,Print,東都名所 飛鳥山花盛|Asukayama Hana Zakari,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1844,1834,1854,Polychrome woodblock print; ink and color on paper,9 4/5 x 14 7/8 in. (24.9 x 37.8 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP632,false,true,37083,Asian Art,Print,江戸名所 外桜田弁慶堀|Soto Sakurada Benkei Bori,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1843,1833,1853,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 3/5 in. (25.1 x 37.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP633,false,true,37084,Asian Art,Print,江戸名所 高輪月の景|Takanawa Tsuki no Kei,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,7th month tiger year 1854,1854,1854,Polychrome woodblock print; ink and color on paper,9 3/5 x 14 3/8 in. (24.4 x 36.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP635,false,true,37086,Asian Art,Print,近江八景之内 石山秋月|The Autumn Moon at Ishiyama on Lake Biwa.,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,10 x 14 9/16 in. (25.4 x 37.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP636,false,true,37087,Asian Art,Print,五十三次名所図会 藤川 山中の里別名宮路山|Fujikawa; Sanchu Yamanaka no Sato Miyajiyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,7th month Hare year 1855,1855,1855,Polychrome woodblock print; ink and color on paper,14 x 8 27/32 in. (35.6 x 22.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP637,false,true,37088,Asian Art,Print,"五十三次名所図会 鳴海 名產有松しぼり店|Narumi, Meisan Arimatsu Shibori Mise",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,7th month Hare year 1855,1855,1855,Polychrome woodblock print; ink and color on paper,Aiban; 13 23/32 x 9 1/4 in. (34.9 x 23.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP638,false,true,37089,Asian Art,Print,"富士三十六景 甲斐大月の原|Kai, Otsuki no Hara",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,4th month horse year 1858,1858,1858,Polychrome woodblock print; ink and color on paper,14 x 9 15/32 in. (35.6 x 24.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP639,false,true,37090,Asian Art,Print,"富士三十六景 武蔵越かや在|Musashi, Koshigaya Zai",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,4th month horse year 1858,1858,1858,Polychrome woodblock print; ink and color on paper,14 1/16 x 9 1/3 in. (35.7 x 23.7 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP640,false,true,37091,Asian Art,Print,東都三十六景 佃しま漁舟|Tsukudajima Gyoshoi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1868,1868,1868,Polychrome woodblock print; ink and color on paper,14 15/32 x 9 7/8 in. (36.8 x 25.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP641,false,true,37092,Asian Art,Print,"東都三十六景 隅田川三囲り堤|Sumidagawa, Mimeguri",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,3rd month dragon year 1868,1868,1868,Polychrome woodblock print; ink and color on paper,14 15/32 x 9 4/5 in. (36.8 x 24.9 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP642,false,true,37093,Asian Art,Print,"名所江戶百景 両国花火|Fireworks at Ryōgoku Bridge, from the series One Hundred Famous Views of Edo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1858,1858,1858,Polychrome woodblock print; ink and color on paper,Image: 13 1/4 × 8 3/4 in. (33.7 × 22.2 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP643,false,true,36461,Asian Art,Print,"Ōhashi Atake no yūdachi|名所江戶百景 大はしあたけの夕立|Sudden Shower over Shin-Ōhashi Bridge and Atake (Ōhashi Atake no yūdachi), from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,13 3/8 x 9 1/2 in. (34 x 24.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP644,false,true,37094,Asian Art,Woodblock print,"名所江戶百景 大はしあたけの夕立|Sudden Shower over Shin-Ōhashi Bridge and Atake (Ōhashi Atake no yūdachi), from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 3/8 x 9 9/16 in. (36.5 x 24.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP645,false,true,37095,Asian Art,Print,"名所江戶百景 目黒太鼓橋夕ひの岡|Taiko Bridge, Meguro, on a Snowy Evening",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,4th month snake year 1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 1/4 in. (36.2 x 23.5 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP646,false,true,37096,Asian Art,Print,"名所江戸百景 真間の紅葉手古那の社継はし|Maples at Mama, from the series One Hundred Famous Views of Edo",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,Oban 14 1/8 x 9 1/16 in. (35.9 x 23.0 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP751,false,true,37195,Asian Art,Print,東海道五十三次 日本橋|Nihon bashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1848–49,1848,1849,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP752,false,true,37196,Asian Art,Print,東海道五十三次 品川 鮫州の茶や|Shinagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP753,false,true,37197,Asian Art,Print,東海道五十三次 川崎 六郷のわたし|Kawasaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,8 23/32 x 13 23/32 in. (22.2 x 34.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP754,false,true,37198,Asian Art,Print,東海道五十三次 神奈川 台の茶や|Kanagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP755,false,true,37199,Asian Art,Print,東海道五十三次 程ヶ谷 かたびら橋 かたびら川|Hodogaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,8 23/32 x 13 23/32 in. (22.2 x 34.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP756,false,true,37200,Asian Art,Print,東海道五十三次 戸塚|Totsuka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP757,false,true,37201,Asian Art,Print,東海道五十三次 藤澤|Fujisawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP758,false,true,37202,Asian Art,Print,東海道五十三次 平塚|Hiratsuka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP759,false,true,37203,Asian Art,Print,東海道五十三次 大磯 鴫立沢西行庵|Ōiso,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,8 23/32 x 13 23/32 in. (22.2 x 34.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP760,false,true,37204,Asian Art,Print,東海道五十三次 小田原 酒匂川|Odawara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP761,false,true,37205,Asian Art,Print,東海道五十三次 箱根 夜中松明とり|Hakone,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1848–49,1848,1849,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP762,false,true,37206,Asian Art,Print,東海道五十三次 三島|Mishima,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP763,false,true,37207,Asian Art,Print,東海道五十三次 沼津|Numazu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP764,false,true,37208,Asian Art,Print,東海道五十三次 原|Hara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1848–49,1848,1849,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP765,false,true,37209,Asian Art,Print,東海道五十三次 吉原 名所左り不二|Yoshiwara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP766,false,true,37210,Asian Art,Woodblock print,東海道五十三次 蒲原 富士川渡舟|Kambara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP767,false,true,37211,Asian Art,Print,東海道五十三次 由井|Yui,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP768,false,true,37212,Asian Art,Print,東海道五十三次 奥津 清見かせき 清見寺|Okitsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP769,false,true,37213,Asian Art,Print,東海道五十三次 江尻|Ejiri,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP770,false,true,37214,Asian Art,Print,東海道五十三次 府中|Fuchu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP771,false,true,37215,Asian Art,Print,"東海道五十三次 鞠子|Mariko, from the series Tokaidō (popularly known as the Reisho Tokaidō)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP772,false,true,37216,Asian Art,Print,東海道五十三次 岡部 宇津の山|Okabe,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP773,false,true,37217,Asian Art,Print,東海道五十三次 藤枝|Fujieda,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP774,false,true,37218,Asian Art,Print,東海道五十三次 島田 大井川|Shimada,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP775,false,true,37219,Asian Art,Print,東海道五十三次 金谷 かなや駅 大井川|Kanaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP776,false,true,37220,Asian Art,Print,東海道五十三次 日阪 夜啼石 無間山 小夜の中山|Nissaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1848–49,1848,1849,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP777,false,true,37221,Asian Art,Print,東海道五十三次 懸川 秋葉山別道|Kakegawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP778,false,true,37222,Asian Art,Print,東海道五十三次 袋井 名物遠川だこ|Fukoroi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP779,false,true,37223,Asian Art,Print,東海道五十三次 見附 天竜川渡舟|Mitsuke,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP780,false,true,37224,Asian Art,Print,東海道五十三次 はま松|Hamamatsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP781,false,true,37225,Asian Art,Print,東海道五十三次 舞坂|Maizaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP782,false,true,37226,Asian Art,Print,東海道五十三次 荒井|Arai,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP783,false,true,37227,Asian Art,Print,東海道五十三次 白須賀 汐見坂|Shirasuke,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1850,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP784,false,true,37228,Asian Art,Print,東海道五十三次 二川  猿ヶ馬場|Futagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP785,false,true,37229,Asian Art,Print,東海道五十三次 吉田 六月十五日天王祭|Yoshida,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP786,false,true,37230,Asian Art,Print,東海道五十三次 御油 古街道本野ヶ原|Goyu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP787,false,true,37231,Asian Art,Print,東海道五十三次 赤阪|Akasaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP788,false,true,37232,Asian Art,Print,東海道五十三次 藤川|Fujikawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP789,false,true,37233,Asian Art,Print,東海道五十三次 岡崎 矢はぎ川|Okazaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP790,false,true,37234,Asian Art,Print,東海道五十三次 池鯉鮒|Chiryu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP791,false,true,37235,Asian Art,Print,東海道五十三次 鳴海 名産絞り店|Narumi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP792,false,true,37236,Asian Art,Print,東海道五十三次 宮 七里の渡し 熱田鳥居 寝覚の里|Miya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP793,false,true,37237,Asian Art,Print,東海道五十三次 桑名 七里の渡舟|Kuwana,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP794,false,true,37238,Asian Art,Print,東海道五十三次 四日市 日永村追分 参宮道|Yokkaichi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP795,false,true,37239,Asian Art,Print,東海道五十三次 石薬師|Ishiyakushi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP796,false,true,37240,Asian Art,Print,東海道五十三次 庄野|Shono,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP797,false,true,37241,Asian Art,Print,東海道五十三次 亀山|Kameyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP798,false,true,37242,Asian Art,Print,東海道五十三次 関|Seki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1848–49,1848,1849,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP799,false,true,37243,Asian Art,Print,東海道五十三次 坂の下|Saka no Shita,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP800,false,true,37244,Asian Art,Print,東海道五十三次 土山|Tsuchiyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP801,false,true,37245,Asian Art,Print,東海道五十三次 水口 平松山美松|Minaguchi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP802,false,true,37246,Asian Art,Print,東海道五十三次 石部|Ishibe,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP803,false,true,37247,Asian Art,Print,東海道五十三次 草津 矢ばせの渡口 琵琶湖風景|Kusatsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP804,false,true,37248,Asian Art,Print,東海道五十三次 大津|Otsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4in. (22.2 x 34.9cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP805,false,true,37249,Asian Art,Print,東海道五十三次 京 三条大はし|Kyoto,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1848–49,1848,1849,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Francis Lathrop Collection, Purchase, Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37249,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1021,false,true,53697,Asian Art,Print,六十余州名所図会 壱岐 志作|Winter View of Shimasaku in the Province of Iki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1856,1856,1856,Polychrome woodblock print; ink and color on paper,H. 13 1/2 in. (34.3 cm); W. 8 13/16 in. (22.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1022,false,true,54930,Asian Art,Woodblock print,"富士三十六景 伊豆の山中|View of Fuji san from the Mountains in the Province of Izu (Izu no Sanchu), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1858,1858,1858,Polychrome woodblock print; ink and color on paper,H. 12 5/8 in. (32.1 cm); W. 8 1/4 in. (21 cm) (trimmed),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1024,false,true,54931,Asian Art,Print,名所江戸百景・廓中東雲|The Entrance to the Yoshiwara at Dawn,Japan,Edo period (1615–1868),,,,Artist,Designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 13 3/8 in. (34 cm); W. 8 3/4 in. (22.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1025,false,true,53680,Asian Art,Print,名所江戸百景 浅草 金龍山|The Kinryusan Temple at Asakusa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1856,1856,1856,Polychrome woodblock print; ink and color on paper,H. 13 3/8 in. (34 cm); W. 8 3/4 in. (22.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1026,false,true,54935,Asian Art,Print,"名所江戸百景 真崎辺より水神の森内川関屋の里を見る|Susaki Hen-yori Suijin no Mori, Uchikawa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 13 3/8 in. (34 cm); W. 8 3/4 in. (22.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1027,false,true,54938,Asian Art,Woodblock print,道中膝栗毛|The Practical Jokers Yajirobei and Kitahachi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Monochrome woodblock print; ink on paper,H. 9 in. (22.9 cm); W. 13 7/8 in. (35.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1028,false,true,54940,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Monochrome woodblock print; ink on paper,H. 8 3/4 in. (22.2 cm); W. 13 3/4 in. (34.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1029,false,true,54942,Asian Art,Woodblock print,道中膝栗毛 四日市追分|The Branch Road at Yokkaichi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 in. (22.9 cm); W. 13 7/8 in. (35.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1030,false,true,54944,Asian Art,Print,道中膝栗毛 京都の町|Street in Kyoto,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 in. (22.9 cm); W. 13 7/8 in. (35.2 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1129,false,true,55049,Asian Art,Print,"東海道五十三次之内 平塚 縄手道|Hiratsuka, Nawate Do",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,H. 9 15/16 in. (25.2 cm); W. 14 13/16 in. (37.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1155,false,true,45303,Asian Art,Print,江戸高名会亭尽 両国|The Aoyagi in Ryogoku,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print (surimono); ink and color on paper,H. 9 15/16 in. (25.2 cm); W. 14 7/16 in. (36.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1156,false,true,45304,Asian Art,Print,江戸高名会亭尽 浅草雷門前 かめや|Asakusa Kaminarimon Mae (Kameya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1157,false,true,45305,Asian Art,Print,江戸高名会亭尽 両国柳橋 大のし|The Ono at Ryogoku Yanagibashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 14 9/16 in. (37 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1158,false,true,45306,Asian Art,Print,江戸高名会亭尽 柳ばし夜景 万八|The Manpachi at Evening in Yanagibashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 14 9/16 in. (37 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1159,false,true,45307,Asian Art,Print,江戸高名会亭尽 柳島の図 橋本|Yanagishima no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 5/16 in. (23.7 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1160,false,true,45308,Asian Art,Print,Ryogoku Yanagibashi (Umegawa)|江戸高名会亭尽 柳ばし|The Umegawa at Ryogoku Yanagibashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 7/16 in. (24 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1161,false,true,45309,Asian Art,Print,Mukōjima (Daikokuya)|江戸高名会亭尽 向島 大七|The Daikokuya at Mukojima,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 5/16 in. (23.7 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1162,false,true,45310,Asian Art,Print,江戸高名会亭尽 大をんし前 田川屋|Daisenji Mae (Tagawaya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 14 9/16 in. (37 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1163,false,true,45311,Asian Art,Print,江戸高名会亭尽 隅田川橋場渡之図 柳屋|Sumidagawa Hashiba Watashi Zu (Yanagiya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 in. (22.9 cm); W. 13 7/8 in. (35.2 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1164,false,true,45312,Asian Art,Print,Ryogoku Yanagibashi (Kawachiya)|江戸高名会亭尽 両国柳橋 河内屋|Tea-house at the Willow Bridge,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 5/16 in. (23.7 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1165,false,true,45313,Asian Art,Print,Fukagawa Hachiman Keidai (Niken Jyaya)|江戸高名会亭尽 深川八幡境内 二軒茶屋|Tea-house inside Hachiman Shrine,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1166,false,true,45314,Asian Art,Print,Shitaya Hirokōji (Oike)|江戸高名会亭尽 下谷広小路 河内楼|Teahouse at Hirokōji,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1615,1868,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 14 1/2 in. (36.8 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1167,false,true,45315,Asian Art,Print,Shinyoshiwara Emonzaka Nihonzutsumi (Harimaya)|江戸高名会亭尽 新吉原衣紋坂日本堤 播磨屋|The Harimaya at Shinyoshiwara Emonzaka Nihonzutsumi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 3/4 in. (37.5 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1168,false,true,45316,Asian Art,Print,Hakusan Keiseiga Kubo (Daisen)|江戸高名会亭尽 白山傾城ヶ窪|Tea house in Hakusen district,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 3/8 in. (36.5 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1169,false,true,45317,Asian Art,Print,Mokuboji Yukimi (Uekiya)|江戸高名会亭尽 木母寺雪見 植木屋|Uekiya Restaurant at Mokuboji,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1825,1852,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1170,false,true,45318,Asian Art,Print,江戸高名会亭尽 王子 扇屋|The Ōgiya at Ōji,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–42,1615,1868,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 3/16 in. (36.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1171,false,true,55051,Asian Art,Print,江戸高名会亭尽 芝神明社内 車轍楼|Shiba Shinmeisha Uchi (Shatetsu-ro),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/2 in. (36.8 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1172,false,true,55052,Asian Art,Woodblock print,江戸高名会亭尽 亀戸裏門 玉屋|Kameido Uramon (Tama-ya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1173,false,true,55053,Asian Art,Print,江戸高名会亭尽 牛嶋 武蔵屋|Ushijima (Musashi-ya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1174,false,true,55054,Asian Art,Print,江戸高名会亭尽 向島の図 平岩|Mukojima no Zu (Hira-Iwa),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 8 13/16 in. (22.4 cm); W. 13 3/4 in. (34.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1175,false,true,55057,Asian Art,Print,江戸高名会亭尽 本所小梅 小倉庵|Honjo Komme (Ogura-an),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 7/16 in. (36.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1176,false,true,55059,Asian Art,Print,江戸高名会亭尽 湯島 松琴亭|Yushima (Matsu Kane-ya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 15/16 in. (25.2 cm); W. 14 7/16 in. (36.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1177,false,true,55061,Asian Art,Print,江戸高名会亭尽 池之端 蓬莱亭 青楼花見の休み|Ikeno Mata (Horai-ya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1178,false,true,55075,Asian Art,Print,江戸高名会亭尽 今戸橋之図 金波楼|Imadobashi no Zu (Tama-Sho),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 7/16 in. (24 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1179,false,true,55077,Asian Art,Print,江戸高名会亭尽 三囲之景 出羽屋|Mimeguri no Kei (Toyoha-ya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1180,false,true,55078,Asian Art,Print,江戸高名会亭尽 雑司ヶ谷の図 茗荷屋|Zoshigaya no Zu (Myoga-ya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 7/16 in. (24 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1181,false,true,55079,Asian Art,Woodblock print,江戸高名会亭尽 山谷 八百善|San-ya (Yaozen),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1182,false,true,55081,Asian Art,Print,名所江戸百景 千駄木団子坂花屋敷|Sendagi Dangozaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1856,1856,1856,Polychrome woodblock print; ink and color on paper,Aiban; H. 13 7/8 in. (35.2 cm); W. 9 3/4 in. (24.8 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1183,false,true,55084,Asian Art,Woodblock print,"名所江戸百景 馬喰町初音の馬場|The First Race Course, Horse-Dealer's Street",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 14 1/16 in. (35.7 cm); W. 9 1/4 in. (23.5 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1184,false,true,37316,Asian Art,Print,"名所江戸百景 深川萬年橋|Mannen Bridge, Fukagawa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1858,1858,1858,Polychrome woodblock print; ink and color on paper,H. 14 1/16 in. (35.7 cm); W. 9 7/16 in. (24 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1185,false,true,55095,Asian Art,Print,名所江戸百景 月の岬|Moon Viewing Point,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,Oban 13 7/8 x 9 5/8 in. (35.2 x 24.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1186,false,true,55096,Asian Art,Print,"富士三十六景 相州三浦の海上|Fuji from Miura, Sagami (Soshu Miura no Kaijo), from the series Thirty-six Views of Mount Fuji (Fugaku sanjūrokkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1858,1858,1858,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 9 7/16 in. (24 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1187,false,true,49926,Asian Art,Print,近江八景 辛崎夜雨|Evening Rain at Karasaki Pine Tree,Japan,Edo period (1615–1868),,,,Artist,Designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,H. 6 1/2 in. (16.5 cm); W. 9 1/16 in. (23 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1188,false,true,49927,Asian Art,Print,近江八景 石山秋月|Autumn Moon at Ishiyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 6 5/8 in. (16.8 cm); W. 9 in. (22.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1189,false,true,49928,Asian Art,Print,近江八景 比良暮雪|Evening Snow at Mt. Hira,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,H. 6 5/8 in. (16.8 cm); W. 8 3/4 in. (22.2 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1190,false,true,49929,Asian Art,Print,近江八景 堅田落雁|Returning Geese at Katata,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 6 13/16 in. (17.3 cm); W. 9 in. (22.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1191,false,true,49930,Asian Art,Print,近江八景 瀬田夕照|Sunset at Seta,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,H. 6 5/8 in. (16.8 cm); W. 9 in. (22.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1192,false,true,49931,Asian Art,Woodblock print,近江八景 三井晩鍾|Vesper Bells at Mii Temple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,H. 6 11/16 in. (17 cm); W. 9 1/8 in. (23.2 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1193,false,true,49932,Asian Art,Woodblock print,近江八景 矢橋帰帆|Fishing Boats Sailing Back to Yabase,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,H. 6 11/16 in. (17 cm); W. 9 1/16 in. (23 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1194,false,true,49933,Asian Art,Print,近江八景 粟津晴嵐|Clearing Weather at Awazu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 6 13/16 in. (17.3 cm); W. 9 1/8 in. (23.2 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/49933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1197,false,true,53781,Asian Art,Print,"六十余州名所図会 肥後 五ヶの庄|Goka no Shō, Higo Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,H. 13 5/8 in. (34.6 cm); W. 9 1/4 in. (23.5 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1198,false,true,53783,Asian Art,Print,"六十余州名所図会 阿波 鳴門の風波|Naruto Whirlpool, Awa Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,Oban tate-e 14 x 9 5/8 in. (35.6 x 24.4 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1199,false,true,53784,Asian Art,Print,"六十余州名所図会 備後 阿武門観音堂|Kannondo, Abuto, Bingo Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 1/15 in. (25.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1200,false,true,55116,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1833–43,1833,1843,Polychrome woodblock print; ink and color on paper,H. 8 5/8 in. (21.9 cm); W. 13 1/2 in. (34.3 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1202,false,true,55120,Asian Art,Print,浪花名所図会 順慶町夜見世の図|Junkei machi Yomise no Zu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1828,1818,1838,Polychrome woodblock print; ink and color on paper,H. 9 1/4 in. (23.5 cm); W. 14 3/16 in. (36 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1203,false,true,55122,Asian Art,Print,近江八景 石山秋月|The Autumn Moon on Ishiyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 15/16 in. (25.2 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1204,false,true,55123,Asian Art,Print,五十三次名所図会 藤川 山中の里別名宮路山|Fujikawa; Sanchu Yamanaka no Sato Miyajiyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1855,1855,1855,Polychrome woodblock print; ink and color on paper,H. 14 7/16 in. (36.7 cm); W. 9 3/4 in. (24.8 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1205,false,true,55126,Asian Art,Print,五十三次名所図会 御油 本野ヶ原本坂ごへ|Goyu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1855,1855,1855,Polychrome woodblock print; ink and color on paper,H. 14 1/16 in. (35.7 cm); W. 9 1/4 in. (23.5 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1206,false,true,55130,Asian Art,Print,五十三次名所図会 見付 天竜川舟渡し|Mitsuke,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1855,1855,1855,Polychrome woodblock print; ink and color on paper,H. 14 in. (35.6 cm); W. 9 1/4 in. (23.5 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1207,false,true,55131,Asian Art,Print,五十三次名所図会 水口|Mizukuchi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1855,1855,1855,Polychrome woodblock print; ink and color on paper,H. 14 9/16 in. (37 cm); W. 9 13/16 in. (24.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1255,false,true,55152,Asian Art,Print,東海道五十三次 日本橋|Nihon Bashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 9 1/8 in. (23.2 cm); W. 6 5/8 in. (16.8 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1256,false,true,55153,Asian Art,Print,東海道五十三次 吉原 左リ富士ノ縄手|Yoshiwara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 11/16 in. (22.1 cm); W. 6 5/16 in. (16 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1257,false,true,55154,Asian Art,Print,東海道五十三次 由井 由井川|Yui,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 1/2 in. (21.6 cm); W. 6 1/2 in. (16.5 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1258,false,true,55155,Asian Art,Print,"東海道五十三次 嶋田 大井川|Shimada; Oigawa Shun-Gan, Banks of the Oi River",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 7/8 in. (22.5 cm); W. 6 1/2 in. (16.5 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1259,false,true,55156,Asian Art,Woodblock print,"東海道五十三次 日阪 倭園琴桜|Nissaka; Sayo no Naka Yama, pass in the Bayo Mountains",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 5/8 in. (21.9 cm); W. 6 3/8 in. (16.2 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1260,false,true,55157,Asian Art,Print,東海道五十三次 掛川|Kakegawa; Akiba-san Embo,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 9 1/8 in. (23.2 cm); W. 6 5/8 in. (16.8 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1261,false,true,55158,Asian Art,Print,東海道五十三次 浜松|Hamamatsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 15/16 in. (22.7 cm); W. 6 11/16 in. (17 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1262,false,true,55159,Asian Art,Print,東海道五十三次 二川|Futagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 11/16 in. (22.1 cm); W. 6 7/16 in. (16.4 cm),"Rogers Fund, 1912",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1263,false,true,55160,Asian Art,Print,"東海道五十三次 吉田|Yoshida; Toyokawa-Bashi, Toyokawa Bridge",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 1/2 in. (21.6 cm); W. 6 7/16 in. (16.4 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1264,false,true,55161,Asian Art,Print,東海道五十三次 御油|Goyu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 7/8 in. (22.5 cm); W. 6 1/2 in. (16.5 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1265,false,true,55162,Asian Art,Print,東海道五十三次 藤川|Fujikawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 9 1/8 in. (23.2 cm); W. 6 11/16 in. (17 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1266,false,true,55163,Asian Art,Print,東海道五十三次 石部|Ishibe,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 8 5/8 in. (21.9 cm); W. 6 1/2 in. (16.5 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1293,false,true,55235,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 4 3/8 (11.1 cm); W. 6 1/2 in. (16.5 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1313,false,true,45293,Asian Art,Print,Takanawa no Meigetsu|東都名所 高輪之明月|Full Moon at Takanawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1831,1821,1841,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 15 1/8 in. (38.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1317,false,true,55275,Asian Art,Print,木曽海道六拾九次之内 三渡野|Santono Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 15 in. (38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1319,false,true,55278,Asian Art,Print,京都名所之内 四条河原夕涼|Cooling off in the Evening at Shijogawara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,Image: 8 5/8 x 13 7/8 in. (21.9 x 35.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1468,false,true,55551,Asian Art,Print,忠臣蔵 五段目|Sadakuro Threatening to Kill Yoichibei,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 1/8 in. (23.2 cm); W. 13 3/4 in. (34.9 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1469,false,true,55552,Asian Art,Print,名所江戸百景 馬喰町初音の馬場|Hatsune no Baba; Bakurocho,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,H. 13 1/2 in. (34.3 cm); W. 8 13/16 in. (22.4 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1470,false,true,55553,Asian Art,Print,"名所江戸百景 王子装束ゑの木大晦日の狐火|New Year's Eve Foxfires at the Changing Tree, Ōji",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,Image: 12 13/16 × 8 5/8 in. (32.5 × 21.9 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1473,false,true,55557,Asian Art,Print,東海道五十三次之内 神奈川 台之景|View of Kangawa at Sunset,Japan,Edo period (1615–1868),,,,Artist,Designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 in. (35.6 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1474,false,true,55558,Asian Art,Print,"東海道五十三次之内 御油 旅人留女|Goyu, Tabibito Ryujo",Japan,Edo period (1615–1868),,,,Artist,Designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,H. 13 7/8 in. (35.2 cm); W. 10 3/4 in. (27.3 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1475,false,true,55560,Asian Art,Print,"東海道五十三次之内 鳴海 名物有松絞|Narumi, Meibutsu Arimatsu Shibori",Japan,Edo period (1615–1868),,,,Artist,Designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1734,1724,1744,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/2 in. (36.8 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1476,false,true,55563,Asian Art,Print,五十三次名所図会 草津|Kusatsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1855,1855,1855,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 3/4 in. (24.8 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1483,false,true,55574,Asian Art,Print,江戸高名会亭尽 深川八幡境内 二軒茶屋|Fukagawa Hachiman Keidai (Niken Jya-ya),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1484,false,true,55577,Asian Art,Print,浪花名所図会 今宮 十日恵比寿|Imamiya Toka Ebisu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1828,1818,1838,Polychrome woodblock print; ink and color on paper,H. 9 11/16 in. (24.6 cm); W. 14 11/16 in. (37.3 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1487,false,true,55583,Asian Art,Print,歌川広重画 桜花に都鳥|Hooded Gulls and Cherry Blossoms,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,probably 1850–62,1850,1862,Polychrome woodblock print; ink and color on paper,H. 7 7/8 in. (20 cm); W. (top) 9 in. (22.9 cm); (bottom) 7 3/4 in. (19.7 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1488,false,true,55585,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840–50,1840,1850,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 4 13/16 in. (12.2 cm),"Rogers Fund, 1926",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1491,false,true,55587,Asian Art,Print,"東海道五十三次之内 桑名 七里渡口|Station Forty-Three: Kuwana, Seven-Ri Ferry at the Port, from the Fifty-Three Stations of the Tokaido",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 7/8 in. (37.8 cm),"Gift of Louis V. Ledoux, 1927",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1498,false,true,55594,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,H. 8 1/2 in. (21.6 cm); W. 11 1/2 in. (29.2 cm),"Fletcher Fund, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1546,false,true,55711,Asian Art,Print,木曽海道六拾九次之内 洗馬|Senba Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",(?),"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835–48,1835,1848,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 7/16 in. (36.7 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1547,false,true,55712,Asian Art,Print,"名所江戸百景 御厩河岸|Ommayagashi, Sumida River",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 10 1/8 in. (25.7 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1548,false,true,53682,Asian Art,Print,"名所江戸百景 京橋竹がし|Full Moon Over Canal, with Bridge and Huge Stacks of Bamboo along the Bank",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1857,1847,1867,Polychrome woodblock print; ink and color on paper,Aiban; H. 13 7/8 in. (35.2 cm); W. 9 1/4 in. (23.5 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1876,false,true,56196,Asian Art,Print,"東海道五十三次 池鯉鮒 首夏馬市|Chiriu, Station No. 40",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,H. 9 in. (22.9 cm); W. 13 7/8 in. (35.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1877,false,true,56258,Asian Art,Print,"東海道五十三次 関 本陣早立|Seki, Stations No. 48",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 7/16 in. (24.8 x 36.7cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1880,false,true,44980,Asian Art,Print,Shinshu-sarashina tagoto no tsuki|本朝名所 信州更科田毎之月|Reflections of the Moon in the Rice Fields of Sarashina in Shinshu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,Image: 9 1/4 × 14 1/8 in. (23.5 × 35.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1887,false,true,56519,Asian Art,Print,江戸近郊八景之内 玉川秋月|Autumn Moon on the Tama River,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 5/16 in. (36.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1889,false,true,44623,Asian Art,Print,四季江都名所 冬 隅田川之雪|Snow on the Sumida River,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,14 5/8 x 5 in. (37.1 x 12.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1892,false,true,56590,Asian Art,Print,雪月花 阿波鳴門之風景|Rapids at Naruto,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Triptych of polychrome woodblock prints; ink and color on paper,A: H. 14 1/2 in. (36.8 cm); W. 9 11/16 in. (24.6 cm) B: H. 14 1/2 in. (36.8 cm); W. 9 11/16 in. (24.6 cm) C: H. 14 1/2 in. (36.8 cm); W. 9 11/16 in. (24.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1893,false,true,56591,Asian Art,Woodblock print,雪月花 武陽金沢八勝夜景|Panorama of the Eight Views of Kanasawa under a Full Moon,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Triptych of polychrome woodblock prints; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 29 7/8 in. (75.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1894,false,true,53688,Asian Art,Woodblock print,雪中芦に鴨|A Wild Duck near a Snow-laden Shore,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1843,1833,1853,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 6 3/4 in. (17.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1895,false,true,45023,Asian Art,Woodblock print,歌川広重画 雪中芦に鴨|Mallard Ducks and Snow-covered Reeds,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1843,1833,1853,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 5 1/16 in. (12.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1898,false,true,56796,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,14 11/16 x 4 15/16 in. (37.3 x 12.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1899,false,true,53687,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840s,1840,1849,Polychrome woodblock print; ink and color on paper,H. 8 15/16 in. (22.7 cm); W. 11 3/16 in. (28.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2349,false,true,54134,Asian Art,Woodblock print,"西洋風の懐中時計『春雨集』 摺物帖|Western Pocket WatchFrom the Spring Rain Collection (Harusame shū), vol. 3",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,dated 1823,1823,1823,Part of an album of woodblock prints (surimono); ink and color on paper,7 11/16 x 6 1/4 in. (19.5 x 15.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2457,false,true,45298,Asian Art,Print,Gotenyama no yu-zakura|東都名所 御殿山之夕桜|Evening Cherry Blossoms at Gotenyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1831,1831,1831,Polychrome woodblock print; ink and color on paper,13 3/4 x 8 1/2 in. (34.9 x 21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2458,false,true,45300,Asian Art,Print,"Sumidagawa, hazakura no kei|東都名所 隅田川葉桜之景|A View of Cherry Trees in Leaf along the Sumida River",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1831,1831,1831,Polychrome woodblock print; ink and color on paper,13 3/4 x 8 1/2 in. (34.9 x 21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2459,false,true,45299,Asian Art,Print,"Masaki, boshun no kei|東都名所 真崎暮春之景|A View of Late Spring at Masaki",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1831,1831,1831,Polychrome woodblock print; ink and color on paper,13 3/4 x 8 1/2 in. (34.9 x 21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2461,false,true,45301,Asian Art,Print,Tsukudajima hatsuhotogizu|東都名所 佃島初郭公|The Year's First Song of the Cuckoo at Tsukudajima,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1831,1831,1831,Polychrome woodblock print; ink and color on paper,13 3/4 x 8 1/2 in. (34.9 x 21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2462,false,true,56887,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1834,1824,1844,Polychrome woodblock print; ink and color on paper,13 1/2 x 4 1/2 in. (34.3 x 11.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2463,false,true,45295,Asian Art,Print,Susaki yuki no hatsuhi|東都名所 洲崎雪之初日|New Year's Sunrise after Snow at Susaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1831,1821,1841,Polychrome woodblock print; ink and color on paper,8 1/2 x 14 in. (21.6 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2464,false,true,56888,Asian Art,Print,京都名所之内 八瀬之里|Village of Yase,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 3/4 x 14 in. (22.2 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2465,false,true,56889,Asian Art,Print,京都名所之内 祇園社雪中|The Gion Shrine in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 3/4 x 14 in. (22.2 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2466,false,true,45302,Asian Art,Print,Kameido Tenmangu keidai no yuki|東都名所 亀戸天満宮境内雪|Tenmangū Shrine at Kameido in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–43,1615,1868,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2467,false,true,56890,Asian Art,Print,金沢八景 内川暮雪|Evening Snow at Uchikawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,9 x 13 7/8 in. (22.9 x 35.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2468,false,true,56891,Asian Art,Woodblock print,"山海見立相撲 備前偸賀山|Mount Yuga in Bizen Province (Bizen Yugasan), from the series Wrestling Matches between Mountains and Seas (Sankai mitate zumō)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,"8th month, 1858",1858,1858,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 in. (22.2 x 33 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2469,false,true,56892,Asian Art,Woodblock print,近江八景之内 三井晩鐘|Vesper Bells at Mii Temple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,9 x 13 3/4 in. (22.9 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2470,false,true,56893,Asian Art,Print,近江八景之内 堅田落雁|Returning Geese at Katada,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2471,false,true,56894,Asian Art,Print,近江八景之内 粟津晴嵐|Clearing Weather at Awazu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2472,false,true,56895,Asian Art,Woodblock print,近江八景之内 矢橋帰帆|Fishing Boats Returning to Yabase,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2473,false,true,56896,Asian Art,Print,近江八景之内 瀬田夕照|Sunset at Seta,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2474,false,true,56897,Asian Art,Print,近江八景之内 比良暮雪|Evening Snow on Mount Hira,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 7/8 x 15 15/16 in. (22.5 x 40.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2475,false,true,56898,Asian Art,Print,近江八景之内 石山秋月|The Autumn Full Moon at Ishiyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 11/16 in. (22.5 x 34.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2476,false,true,56899,Asian Art,Print,近江八景之内 唐崎夜雨|Evening Rain at Karasaki-Pine Tree,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2477,false,true,56900,Asian Art,Print,江戸近郊八景之内 芝浦晴嵐|Clearing Weather at Shibaura,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2478,false,true,56901,Asian Art,Print,江戸近郊八景之内 行徳帰帆|Boats Returning to Gyotoku,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2479,false,true,56902,Asian Art,Print,江戸近郊八景之内 飛鳥山暮雪|Asukayama in Evening Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2480,false,true,56903,Asian Art,Print,江戸近郊八景之内 玉川秋月|Autumn Moon on the Tama River,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2481,false,true,56904,Asian Art,Woodblock print,東海道五十三次之内 江尻 三保遠望|Panorama of Miwo Pine Wood from Ejiri,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834,1834,1834,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2482,false,true,56905,Asian Art,Print,東海道五十三次之内 石薬師 石薬師寺|The Ishiyakushi Temple at Ishiyakushi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834,1834,1834,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 5/8 in. (22.5 x 34.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2483,false,true,56906,Asian Art,Print,"東海道五十三次之内 藤川 棒鼻の図|Station Thirty-Eight: Fujikawa, Scene at the Border, from the Fifty-Three Stations of the Tokaido",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 x 14 in. (22.9 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2484,false,true,56907,Asian Art,Print,東海道五十三次之内 四日市 三重川|Mie River at Yokkaichi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834,1834,1834,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2485,false,true,56908,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,14 7/8 x 5 1/8 in. (37.8 x 13 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2487,false,true,56910,Asian Art,Woodblock print,"東海道五十三次之内 関 本陣早立|Station Forty-Eight: Seki, Early Departure from the Headquarters Inn, from the Fifty-Three Stations of the Tokaido",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,8 7/8 13 5/8 in. (22.5 x 34.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2489,false,true,56912,Asian Art,Print,東海道五十三次之内 岡部 宇津の山|Utsu Hill at Okabe,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834,1834,1834,Polychrome woodblock print; ink and color on paper,9 x 14 in. (22.9 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2490,false,true,56913,Asian Art,Print,東海道五十三次之内 金谷 大井川遠岸|The Far Bank of the Ōi River at Kanaya,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834,1834,1834,Polychrome woodblock print; ink and color on paper,9 x 14 in. (22.9 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2492,false,true,56915,Asian Art,Woodblock print,"東海道五十三次之内 蒲原 夜の雪|Evening Snow at Kanbara, from the series ""Fifty-three Stations of the Tōkaidō""",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1823,1844,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/4 in. (22.5 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2493,false,true,56916,Asian Art,Print,東海道五十三次之内 庄野 白雨|Shower at Shōno,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834,1834,1834,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 5/8 in. (22.5 x 34.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2494,false,true,56917,Asian Art,Print,東海道五十三次 はま松|Hamamatsu Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1842,1832,1852,Polychrome woodblock print; ink and color on paper,8 9/16 x 13 5/8 in. (21.7 x 34.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2495,false,true,56918,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833,1823,1843,Uncut triptych of polychrome woodblock prints; ink and color on paper,9 1/4 x 14 1/2 in. (23.5 x 36.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2496,false,true,56919,Asian Art,Print,木曽海道六拾九次之内 あし田|Ashida Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2497,false,true,56920,Asian Art,Woodblock print,歌川広重画 雪中芦に鴨|Mallard Duck and Snow-covered Reeds,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1832,1822,1842,Polychrome woodblock print; ink and color on paper,15 x 6 7/8 in. (38.1 x 17.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2498,false,true,56921,Asian Art,Print,木曽海道六拾九次之内 長久保|Nagakubo Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2499,false,true,56922,Asian Art,Print,木曽海道六拾九次之内 福しま|Fukushima Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1837,1827,1847,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2500,false,true,56923,Asian Art,Print,木曽海道六拾九次之内 宮ノ越|Miyanokoshi Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/8 in. (22.5 x 34 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2501,false,true,56924,Asian Art,Print,木曽海道六拾九次之内 大井|Ōi Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1837,1827,1847,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 3/8 in. (22.5 x 34 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2502,false,true,56925,Asian Art,Print,"歌川広重画 朝顔に鶏と傘|Rooster, Umbrella, and Morning Glories",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1830,1820,1840,Polychrome woodblock print; ink and color on paper,14 3/4 x 6 1/2 in. (37.5 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2503,false,true,45320,Asian Art,Print,"Sumidagawa no yuki|Sumida River in the Snow, from the series ""Famous Places in Edo in the Four Seasons""",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1834,1834,1834,Polychrome woodblock print (tanzaku size); ink and color on paper,Tanzaku 14 3/4 x 5 in. (37.5 x 12.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2504,false,true,56926,Asian Art,Print,月二拾八景之内 弓張月|Bow Moon,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1832,1832,1832,Polychrome woodblock print; ink and color on paper,14 3/4 x 6 7/8 in. (37.5 x 17.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2506,false,true,56928,Asian Art,Print,"富士三十六景 房州保田の海岸|Seashore at Hoda, Province of Awa",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1858–59,1858,1859,Polychrome woodblock print; ink and color on paper,13 1/4 x 8 3/4 in. (33.7 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2507,false,true,56929,Asian Art,Woodblock print,富士三十六景 駿河三保の松原|Pine Groves of Miho in Suruga Province,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1858,1858,1858,Polychrome woodblock print; ink and color on paper,13 3/8 x 8 3/4 in. (34 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2508,false,true,53785,Asian Art,Woodblock print,"六十余州名所図会  対馬 海岸 夕晴|Evening Glow, Tsushima Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,13 3/8 x 9 in. (34 x 22.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2509,false,true,53780,Asian Art,Print,"六十余州名所図会 播磨 舞子の浜|Maiko Beach, Harima Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,Oban tate-e 12 3/8 x 9 in. (31.4 x 22.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2510,false,true,53681,Asian Art,Print,Kamata no Baien|名所江戸百景 蒲田の梅園|Plum Garden at Kamata,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,13 1/4 x 8 5/8 in. (33.7 x 21.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2511,false,true,56930,Asian Art,Print,歌川広重画 鉄線花に鳥|Clematis and Bird,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1852,1852,1852,Polychrome woodblock print; ink and color on paper,6 7/8 x 9 in. (17.5 x 22.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2512,false,true,53679,Asian Art,Print,"名所江戸百景 昌平橋 聖堂 神田川|Shohei Bridge, Seido Temple and Kanda River",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,13 1/4 x 8 3/4 in. (33.7 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2513,false,true,56692,Asian Art,Print,"名所江戸百景 神田紺屋町|Dye House at Konya-cho, Kanda",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 13 in. (33 cm); W. 8 1/2 in. (21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2514,false,true,56931,Asian Art,Print,"「名所江戸百景 よし原 日本堤」|“Nihon Embankment at Yoshiwara,” from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei, Yoshiwara, Nihonzutsumi)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2515,false,true,56932,Asian Art,Print,名所江戸百景 深川木場|The Lumber Yard at Fukagawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1856,1856,1856,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 3/8 in. (36.2 x 23.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2516,false,true,56933,Asian Art,Print,名所江戸百景 目黒太鼓橋夕日の岡|The Taiko (Drum) Bridge and the Yuhi Mound at Meguro,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,Oban: 14 1/8 x 9 1/8 in. (35.9 x 23.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2517,false,true,53659,Asian Art,Print,名所江戸百景 亀戸天神境内|In the Kameido Tenjin Shrine Compound,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1856,1856,1856,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 3/4 in. (36.2 x 24.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2518,false,true,56934,Asian Art,Print,名所江戸百景 請地秋葉の境内|Inside Akiba Shrine at Ukeji,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,13 1/4 x 8 3/4 in. (33.7 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2519,false,true,56689,Asian Art,Print,"名所江戸百景 浅草金龍山|Kinryūsan Temple at Asakusa, from the series ""One Hundred Famous Views of Edo""",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1856,1856,1856,Polychrome woodblock print; ink and color on paper,H. 14 1/16 in. (35.7 cm); W. 9 1/2 in. (24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2520,false,true,55733,Asian Art,Print,"「名所江戸百景 深川洲崎十万坪」|“Jūmantsubo Plain at Fukagawa Susaki,” from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei, Fukagawa Susaki Jūmantsubo)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1856,1856,1856,Polychrome woodblock print; ink and color on paper,Oban 14 1/16 x 9 1/2 in. (35.7 x 24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2521,false,true,56935,Asian Art,Print,名所江戸百景 京橋竹がし|Bamboo Market at Capital Bridge,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/16 x 9 1/2 in. (35.7 x 24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2522,false,true,55433,Asian Art,Print,"Ōhashi Atake no yūdachi|名所江戸百景 大はしあたけの夕立|Sudden Shower over Shin-Ōhashi Bridge and Atake (Ōhashi Atake no yūdachi), from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,Oban 13 3/8 x 9 1/2 in. (34 x 24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2523,false,true,56936,Asian Art,Print,名所江戸百景 王子装束ゑの木大晦日の狐火|Shozokuenoki Tree at Oji: Fox–fires on New Years Eve,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 1/16 x 9 1/2 in. (35.7 x 24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2525,false,true,56938,Asian Art,Woodblock print,"五十三次名所図会 藤川 山中の里別名宮路山|Fujikawa, a Village in the Mountains Formerly Called Miyajiyama",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1855,1855,1855,Polychrome woodblock print; ink and color on paper,13 1/2 x 8 7/8 in. (34.3 x 22.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2526,false,true,56939,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1854,1854,1854,Polychrome woodblock print; ink and color on paper,13 1/8 x 4 3/8 in. (33.3 x 11.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2527,false,true,56940,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1847,1837,1857,Polychrome woodblock print; ink and color on paper,13 5/8 x 4 1/2 in. (34.6 x 11.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2528,false,true,56941,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,14 1/2 x 5 in. (36.8 x 12.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2529,false,true,56942,Asian Art,Woodblock print,歌川広重画 梅に鶯|Warbler on a Plum Branch,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,14 1/2 x 5 1/8 in. (36.8 x 13 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2530,false,true,56943,Asian Art,Woodblock print,歌川広重画 燕子花に川蝉|Kingfisher and Iris,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1832–34,1832,1834,Polychrome woodblock print; ink and color on paper,15 x 5 1/8 in. (38.1 x 13 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2531,false,true,56944,Asian Art,Woodblock print,歌川広重画|Five Swallows above a Branch,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1845,1855,Polychrome woodblock print; ink and color on paper,15 1/8 x 5 1/8 in. (38.4 x 13 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2532,false,true,56945,Asian Art,Print,歌川広重画 罌栗に瑠璃鳥|Bluebird and Flowering Poppies,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1832–34,1832,1834,Polychrome woodblock print; ink and color on paper,13 1/4 x 4 1/4 in. (33.7 x 10.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2533,false,true,56946,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,14 1/4 x 5 in. (36.2 x 12.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2534,false,true,39648,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1847,1837,1857,Polychrome woodblock print; ink and color on paper,13 x 4 3/8 in. (33 x 11.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2535,false,true,56949,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1845,1835,1855,Polychrome woodblock print; ink and color on paper,13 3/4 x 4 1/2 in. (34.9 x 11.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2536,false,true,45024,Asian Art,Print,歌川広重画 蔦に柄長鳥|Long-tailed Tit on Autumn Ivy,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print (hosoban); ink and color on paper,14 3/4 x 4 3/4 in. (37.5 x 12.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2537,false,true,56950,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,8 3/8 x 13 3/8 in. (21.3 x 34 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2538,false,true,56951,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Polychrome woodblock print; ink and color on paper,10 3/8 x 7 1/4 in. (26.4 x 18.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2539,false,true,44894,Asian Art,Print,歌川広重画 雪中小松に錦雉|Golden Pheasant and Pine Shoots in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,14 1/2 x 6 1/2 in. (36.8 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2542,false,true,56956,Asian Art,Print,木曽海道六拾九次之内 洗馬|Senba Station,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1836,1826,1846,Polychrome woodblock print; ink and color on paper,8 3/4 x 13 3/4 in. (22.2 x 34.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2543,false,true,56958,Asian Art,Print,歌川広重画 芦に鷺|White Heron Standing among Reeds,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print; ink and color on paper,15 x 5 1/8 in. (38.1 x 13 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2836,false,true,57066,Asian Art,Print,Zumihari Zuki|月二拾八景之内 弓張月|Bow Moon,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,14 7/8 x 6 15/16 in. (37.8 x 17.6 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2837,false,true,57065,Asian Art,Print,Azuma Mori Yau|江戸近郊八景之内 吾嬬杜夜雨|Evening Rain in Azuma Wood,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/4 in. (24.4 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2838,false,true,57064,Asian Art,Woodblock print,Ikegami Bansho|江戸近郊八景之内 池上晩鐘|Vesper Bells at Ikegami,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,10 x 14 5/8 in. (25.4 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2839,false,true,57063,Asian Art,Print,江戸近郊八景之内 小金井橋夕照|Evening Glow at Koganei Border,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 1/4 in. (25.1 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2840,false,true,57062,Asian Art,Print,Gyotoku Kihan|江戸近郊八景之内 行徳帰帆|Boats Returning to Gyotoku,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 1/8 in. (22.5 x 33.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2841,false,true,57061,Asian Art,Print,Asukayama Bosetsu|江戸近郊八景之内 飛鳥山暮雪|Asukayama in the Snow at Evening,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 1/4 in. (23.5 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2842,false,true,57060,Asian Art,Print,Shibaura Seiran|江戸近郊八景之内 芝浦晴嵐|Clearing Weather at Shibaura,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 1/4 in. (23.5 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2843,false,true,57059,Asian Art,Print,Haneda Rakugan|江戸近郊八景之内 羽根田落雁|Wild Geese at Haneda,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 in. (37.1 x 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2844,false,true,57058,Asian Art,Print,Tamagawa Shugetsu|江戸近郊八景之内 玉川秋月|Autumn Moon on the Tama River,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,9 3/8 x 14 3/8 in. (23.8 x 36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2845,false,true,57057,Asian Art,Print,Awazu no Seiran|近江八景之内 粟津晴嵐|Clearing Weather at Awazu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2846,false,true,57056,Asian Art,Woodblock print,Yabase no Kihan|近江八景之内 矢橋帰帆|Fishing Boats Returning to Yabase,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 7/8 in. (24.8 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2847,false,true,57055,Asian Art,Print,Ishiyama no Shūgetsu|近江八景之内 石山秋月|Autumn Full Moon at Ishiyama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,Image: 9 3/4 × 14 3/4 in. (24.8 × 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2848,false,true,57054,Asian Art,Print,Hira no Bosetsu|近江八景の内 比良暮雪|Evening Snow on Mount Hira,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/4 in. (24.8 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2849,false,true,57053,Asian Art,Print,"Karasaki no Yau|近江八景の内 唐崎夜雨|Evening Rain at Karasaki, Pine Tree",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 7/8 in. (24.8 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2850,false,true,57052,Asian Art,Woodblock print,Mii no Bansho|近江八景の内 三井晩鍾|Vesper Bells at Mii Temple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 7/8 in. (24.8 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2851,false,true,57051,Asian Art,Print,Katada no Rakugan|近江八景之内 堅田落雁|Returning Geese at Katada,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 3/4 x 15 in. (24.8 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2852,false,true,57050,Asian Art,Print,Seta no Yusho|近江八景の内 瀬田夕照|Sunset at Seta,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 5/8 in. (24.4 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2865,false,true,57045,Asian Art,Woodblock print,東海道五十三次之内 亀山 雪晴|Clear Weather after Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1861,1797,1861,Polychrome woodblock print; ink and color on paper,9 3/8 x 14 1/8 in. (23.8 x 35.9 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2866,false,true,57044,Asian Art,Woodblock print,東海道五十三次之内 蒲原 夜の雪|Evening Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1861,1797,1861,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 5/8 in. (24.4 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2867,false,true,57043,Asian Art,Print,Shōno Hakuu|東海道五十三次之内 庄野 白雨|White Rain at Shōno,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1861,1797,1861,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 1/2 in. (24.1 x 36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2868,false,true,56678,Asian Art,Print,"木曽海道六拾九次之内 須原|Suhara, from The Sixty-nine Stations of the Kisokaidō",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 1/2 in. (36.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2869,false,true,57042,Asian Art,Print,木曽海道六拾九次之内 長久保|Nagakubo,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 7/8 in. (25.1 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2871,false,true,56677,Asian Art,Print,"木曽海道六拾九次之内 宮ノ越|Moonlit Night at Miyanokoshi, from The Sixty-nine Stations of the Kisokaidō",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1838,1828,1848,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 14 3/4 in. (37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2872,false,true,57040,Asian Art,Print,木曽海道六拾九次之内 大井|Ōi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,10 x 14 7/8 in. (25.4 x 37.8 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2875,false,true,55989,Asian Art,Print,東海道五十三次 沼津|Numazu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 3/4 in. (25.1 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2876,false,true,55991,Asian Art,Print,東海道五十三次 はま松|Hamamatsu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 1/4 in. (24.4 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2877,false,true,55992,Asian Art,Print,Miya Atsuta Shinji|東海道五十三次之内 宮 熱田神事|Festival at Atsuta Temple,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 1/4 in. (24.4 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2878,false,true,55993,Asian Art,Woodblock print,"Nissaka-sayo no Naka Yama|東海道五十三次之内 日坂 佐夜の中山|Station Twenty-six: Nissaka, Sayo no Nakayama, from the Fifty-three Stations of the Tokaido",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1833–34,1833,1834,Polychrome woodblock print; ink and color on paper,9 3/8 x 14 3/8 in. (23.8 x 36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2879,false,true,55994,Asian Art,Print,Mishima Asa-Giri|東海道五十三次之内 三島 朝霧|Morning Mist,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 1/4 in. (24.1 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2880,false,true,55995,Asian Art,Print,Tsuchiyama Haru no Ame|東海道五十三次之内 土山 春の雨|Spring Rain at Tsuchiyama (50th Station of the Tōkaidō),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 3/8 x 14 1/4 in. (23.8 x 36.2 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2881,false,true,55996,Asian Art,Print,Arashiyama|京都名所之内 あらし山満花|Full Blossom at Arashiyama on the Oi River,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2882,false,true,55997,Asian Art,Print,Yodogawa|京都名所之内 淀川|On the Yodo River,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 in. (26.0 x 38.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2883,false,true,55998,Asian Art,Print,京都名所之内 祇園社雪中|Gion Shrine in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,9 3/4 x 15 1/8 in. (24.8 x 38.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2884,false,true,55999,Asian Art,Woodblock print,京都名所之内 嶋原出口之柳|Gate of the Shimbara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1797–1858,1797,1858,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 3/8 in. (26 x 39.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2885,false,true,56000,Asian Art,Woodblock print,歌川広重画 雪中芦に鴨|Mallard and Snow-covered Reeds,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1843,1833,1853,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 5/8 in. (25.1 x 37.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3010,false,true,56411,Asian Art,Print,Karasaki ya'u|近江八景 唐崎夜雨|Pine Tree at Karasaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 3/4 x 10 in. (37.5 x 25.4 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3037,false,true,53782,Asian Art,Print,"六十余州名所図会 壱岐 志作|Snowfall at Shimasaku, Iki Province, from the series Views of Famous Places in the Sixty-Odd Provinces",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1853,1843,1863,Polychrome woodblock print; ink and color on paper,14 3/8 x 9 1/4 in. (36.5 x 23.5 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3038,false,true,56495,Asian Art,Print,Atagoshita Yabu-Kōji|東都名所 芝赤羽根之雪|Winter Landscape,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1846,1846,1846,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 1/2 in. (24.8 x 36.8 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3039,false,true,56496,Asian Art,Print,東都司馬八景 高輪帰帆|River View at Takanawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1856,1846,1866,Polychrome woodblock print; ink and color on paper,9 x 14 in. (22.9 x 35.6 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3040,false,true,45319,Asian Art,Woodblock print,Toto meisho Sumidagawa zenzu settchukei|東都名所 隅田川全図雪中景|Celebrated Places in the Eastern Capital: Panoramic View of the Sumida River in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1840,1830,1850,Triptych of polychrome woodblock prints; ink and color on paper,14 1/2 x 10 in. (36.8 x 25.4 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3148,false,true,56699,Asian Art,Print,歌川広重画 菊に雉|Pheasant with Chrysanthemums,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1835,1825,1845,Polychrome woodblock print (hosoban); ink and color on paper,14 3/4 x 6 3/4 in. (37.5 x 17.1 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3174,false,true,37386,Asian Art,Print,"Ōhashi Atake no yūdachi|名所江戸百景 大はしあたけの夕立|Sudden Shower over Shin-Ōhashi Bridge and Atake (Ōhashi Atake no yūdachi), from the series One Hundred Famous Views of Edo (Meisho Edo hyakkei)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 9 5/8 in. (37.5 × 24.4 cm) Mat: 22 3/4 × 15 1/2 in. (57.8 × 39.4 cm),"Gift of Mr. and Mrs. A. I. Sherr, 1956",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3413,false,true,55603,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1850,1850,1850,Polychrome woodblock print; ink and color on paper,9 1/4 x 7 in. (23.5 x 17.8 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3414,false,true,55604,Asian Art,Print,江戸名所四十八景 神田明神|Kanda Temple Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1861,1861,1861,Polychrome woodblock print; ink and color on paper,7 1/4 x 9 1/4 in. (18.4 x 23.5 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3435,false,true,55635,Asian Art,Print,名所江戸百景 目黒太鼓橋夕ひの岡|Ochanomizu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,probably late 19th century,1867,1899,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 1/2 in. (24.1 x 36.8 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3510,false,true,55803,Asian Art,Print,「甲陽猿橋之図」|The Monkey Bridge in Kai Province (Kōyō Saruhashi no zu),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1841–42,1841,1842,Vertical ōban diptych mounted as a hanging scroll; ink and color on paper,Overall: 38 3/8 x 13 3/8 in. (97.5 x 34 cm); painting: 28 3/4 x 9 5/8 in. (73 x 24.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3512,false,true,53691,Asian Art,Print,名所江戸百景 王子装束えの木大晦日の狐火|Foxes Meeting at Oji,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 in. (25.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3592,false,true,55897,Asian Art,Woodblock print,"魚づくし こちに茄子|Kochi Fish with Eggplant, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,11 1/4 x 14 9/16 in. (28.6 x 37 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3593,false,true,55898,Asian Art,Woodblock print,"魚づくし ぼらにうど|Bora Fish with Camellia, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 9/16 in. (25.1 x 37 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3594,false,true,55900,Asian Art,Woodblock print,"魚づくし 鰹に桜|Katsuo Fish with Cherry Buds, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,10 1/16 x 14 9/16 in. (25.6 x 37 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3595,false,true,55901,Asian Art,Woodblock print,"魚づくし 車海老 鯵にたで|Aji Fish and Kuruma-ebi, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 1/2 in. (25.7 x 36.9 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3596,false,true,55902,Asian Art,Woodblock print,"魚づくし 黒鯛 小鯛に山椒|Kurodai and Kodai Fish with Bamboo Shoots and Berries, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 1/2 in. (25.7 x 36.9 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3597,false,true,55903,Asian Art,Woodblock print,"魚づくし 伊勢海老 芝蝦|Ise-ebi and Shiba-ebi, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,9 15/16 x 14 3/16 in. (25.3 x 36.1 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3598,false,true,55904,Asian Art,Woodblock print,"魚づくし かれい かながしらに笹|Kanagashira and Karei Fish, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 1/2 in. (25.7 x 36.9 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3599,false,true,55905,Asian Art,Woodblock print,"魚づくし かさご いさきに生姜|Isaki and Kasago Fish, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1830s,1830,1839,Polychrome woodblock print; ink and color on paper,10 1/4 x 14 7/8 in. (26 x 37.8 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3600,false,true,55906,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840s,1840,1849,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 5/8 in. (25.7 x 37.2 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3601,false,true,55907,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840s,1840,1849,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 9/16 in. (25.7 x 37 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3602,false,true,55908,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840s,1840,1849,Polychrome woodblock print; ink and color on paper,10 1/16 x 14 15/16 in. (25.5 x 38 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3603,false,true,55909,Asian Art,Woodblock print,"魚づくし|Medetai Fush and Sasaki Bamboo, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840s,1840,1849,Polychrome woodblock print; ink and color on paper,10 x 14 5/8 in. (25.4 x 37.2 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3604,false,true,55911,Asian Art,Woodblock print,魚づくし いなだ ふぐに梅|Suzuki and Kinmedai Fish from the series Uozukushi (Every Variety of Fish),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840s,1840,1849,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 9/16 in. (25.7 x 37 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3605,false,true,55912,Asian Art,Woodblock print,"魚づくし ひらめ めばるに桜|Hirame and Mebaru Fish with Cherry Blossoms, from the series Uozukushi (Every Variety of Fish)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1840s,1840,1849,Polychrome woodblock print; ink and color on paper,10 1/8 x 14 9/16 in. (25.7 x 37 cm),"Gift of Mr. and Mrs. Bryan Holme, 1980",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3701,false,true,55946,Asian Art,Print,名所江戸百景 川口のわたし善光寺|Kawaguchi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Polychrome woodblock print; ink and color on paper,14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of Grace M. Pugh, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1093.10,false,true,58262,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1854,1854,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 in. × 10 in. (36.8 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2540a–c,false,true,45283,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,ca. 1845,1835,1855,Triptych of polychrome woodblock prints; ink and color on paper,15 x 29 in. (38.1 x 73.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2833a–c,false,true,57068,Asian Art,Print,"Buyō Kanazawa Hasshō Yakei|雪月花 武陽金沢八勝夜景|Full Moon at Kanazawa, Province of Musashi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Triptych of polychrome woodblock prints; ink and color on paper,Each page: 14 7/8 x 10 1/8 in. (37.8 x 25.7 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2834a–c,false,true,45025,Asian Art,Print,Awa no Naruto|雪月花 阿波鳴門之風景|The Whirlpools of Awa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Triptych of polychrome woodblock prints; ink and color on paper,Image (triptych): 14 1/2 × 29 1/2 in. (36.8 × 74.9 cm) Image (each): 14 1/2 in. × 10 in. (36.8 × 25.4 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2835a–c,false,true,57067,Asian Art,Woodblock print,Kisoji no Sansen|雪月花 木曽路之山川|Kisō Mountains in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,1857,1857,1857,Triptych of polychrome woodblock prints; ink and color on paper,A and B (each): 14 5/8 x 10 in. (37.8 x 25.7 cm) C: 14 5/8 x 9 7/8 in. (37.8 x 25.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1040,false,true,54325,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keiri,"Japanese, active first half of the 19th century",,Keiri,Japanese,1800,1899,1835?,1835,1835,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 5/16 in. (21.6 x 18.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP815,false,true,37259,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Furuyama Moroshige,"Japanese, active second half of the 17th century",,Furuyama Moroshige,Japanese,1650,1699,ca. 1690,1680,1700,Polychrome woodblock print; ink and color on paper,10 1/2 x 13 1/2 in. (26.7 x 34.3 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1443,false,true,55487,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shunsui,"Japanese, active last quarter of the 18th century",,Yanagawa Shunsui,Japanese,1700,1799,ca. 1780,1770,1790,Polychrome woodblock print; ink and color on paper,H. 8 1/8 in. (20.6 cm); W. 6 in. (15.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1961,false,true,54535,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusen,"Japanese, active first half of the nineteenth century",,Katsushika Hokusen,Japanese,1800,1850,probably 1813,1813,1813,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3214,false,true,55200,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Munakata,,,Munakata,Japanese,1868,1912,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,12 1/2 x 8 7/8 in. (31.8 x 22.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3218,false,true,55205,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Naoyoshi,,,Hashimoto Naoyoshi,Japanese,1838,1912,dated 1882,1882,1882,Two panels of a triptych of polychrome woodblock prints; ink and color on paper,14 x 9 1/8 in. (35.6 x 23.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3219,false,true,55206,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Naoyoshi,,,Hashimoto Naoyoshi,Japanese,1838,1912,dated 1882,1882,1882,Two panels of a triptych of polychrome woodblock prints; ink and color on paper,14 x 9 1/8 in. (35.6 x 23.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP3,false,true,57114,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 19 15/16 in. (36.8 x 50.6 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3717,false,true,55981,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Genki (Komai Ki),"Japanese, 1747–1797",,Genki,Japanese,1747,1797,1895,1895,1895,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2744,false,true,57014,Asian Art,Woodblock print,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,ca. 1875,1865,1885,Polychrome woodblock print; ink and color on paper,6 7/8 x 10 1/8 in. (17.5 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2745,false,true,57015,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,ca. 1875,1865,1885,Polychrome woodblock print; ink and color on paper,7 3/4 x 10 in. (19.7 x 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3215,false,true,55201,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kunisada II,"Japanese, 1823–1880",,Utagawa Kunisada II,Japanese,1823,1880,Feb. 1889 (Meiji 22),1889,1889,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 13 3/4 x 27 3/4 in. (35 x 70.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3270,false,true,55340,Asian Art,Print,東京高輪鉄道蒸気車走行之図|Illustration of a Steam Locomotive Running on the Takanawa Railroad in Tokyo (Tōkyō takanawa tetsudō jōkisha sōkō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,ca. 1873,1863,1883,Triptych of polychrome woodblock prints; ink and color on paper,14.2 x 29.1 in. (36.1 x 73.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3347,false,true,55512,Asian Art,Print,東京汐留鉄道蒸気車通行図|Illustration of a Steam Locomotive Passing Shiodome in Tokyo (Tōkyō Shiodome testudō jōkisha tsūkō zu),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,1872,1872,1872,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 28 1/2 x 14 1/2 in. (72.4 x 36.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3348,false,true,55513,Asian Art,Print,東京高輪鉄道蒸気車走行之全図|Tokyo /Takanawa Steam Railway,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,"1879 (Meiji 3, 2nd month)",1879,1879,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 14 3/16 x 30 in. (36 x 76.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.243,false,true,73534,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,"October, 1872",1872,1872,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.242a–c,false,true,73533,Asian Art,Print,"Kak'koku han e sukushi Igirisu Rondon|""View in London,"" the Prosperity of Countries: London, England",Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshimori,"Japanese, 1830–1884",,Utagawa Yoshimori,Japanese,1830,1884,September 1872,1872,1872,Polychrome woodblock print; ink and color on paper,Image (a): 14 1/4 x 9 3/8 in. (36.2 x 23.8 cm) Image (b): 14 1/4 x 9 3/8 in. (36.2 x 23.8 cm) Image (c): 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3249,false,true,45001,Asian Art,Print,Yokohama Igirisu Shokan hanei no zu|Picture of a Prosperous English Trading Firm in Yokohama,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,January 1871,1871,1871,Triptych of polychrome woodblock prints; ink and color on paper,14 1/4 x 29 3/4 in. (36.2 x 75.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.234a–c,false,true,73525,Asian Art,Print,「横浜英吉利西商館繁栄圖」|“The Flourishing of an English Trading Firm in Yokohama”,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,"9th month, 1870",1870,1870,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/8 x 9 5/8 in. (35.9 x 24.4 cm) Image (b): 14 x 9 5/8 in. (35.6 x 24.4 cm) Image (c): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3198,false,true,55182,Asian Art,Print,"『東風俗福づくし 大 礼ふく』|“Ceremonial Attire” from the series An Array of Auspicious Customs of Eastern Japan (Azuma fūzoku, fukuzukushi: Tairei fuku)",Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,ca. 1889,1879,1899,Triptych of polychrome woodblock prints; ink and color on paper,Oban 14 5/8 x 9 11/16 in. (37.1 x 24.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3201,false,true,55185,Asian Art,Woodblock print,"『東風俗福づくし 洋 ふく』|Western Clothing from the series An Array of Auspicious Customs of Eastern Japan (Azuma fūzoku, fukuzukushi-Yōfuku)",Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1889,1889,1889,Triptych of polychrome woodblock prints; ink and color on paper,Oban 12 15/16 x 8 15/16 in. (32.9 x 22.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3221,false,true,55209,Asian Art,Print,"勲功之将天杯賜之図|Illustration of the Honored Commanders, Receiving the Emperor's Gift Cup (Kunkō no shō tenpai o tamau no zu)",Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"September, 1877",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image (each): 13 7/8 × 9 1/4 in. (35.2 × 23.5 cm) Mat: 18 3/4 × 23 1/4 in. (47.6 × 59.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3222,false,true,55211,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"Sept. 5, 1877 (Meiji 10)",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image (each): 13 7/8 × 9 1/4 in. (35.2 × 23.5 cm) Mat: 23 × 37 in. (58.4 × 94 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3223,false,true,55213,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"Sept. 5, 1877 (Meiji 10)",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image: 14 in. × 9 1/4 in. (35.6 × 23.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3236,false,true,55249,Asian Art,Print,大山綱良糾問の図|Illustration of the Inquisition of Ōyama Tsunayoshi (Ōyama Tsunayoshi kyūmon no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"August, 1877",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 13 15/16 x 28 1/8 in. (35.4 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55249,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3237,false,true,55250,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"August 27, 1877 (Meiji 10)",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 13 15/16 x 28 1/8 in. (35.4 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3238,false,true,55251,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"August 27, 1877 (Meiji 10)",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 13 15/16 x 28 1/8 in. (35.4 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3239,false,true,55252,Asian Art,Print,チャリネ大曲馬御遊覧ノ図|Illustration of the Imperial Excursion to see the Charini's Circus (Charine daikyokuba goyūran no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"November, 1886",1886,1886,Polychrome woodblock print; ink and color on paper,14 3/4 x 27 3/4 in. (37.5 x 70.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3240,false,true,55253,Asian Art,Print,憲法発布式之図|Illustration of the Ceremony Issuing the Constitution (Kenpō happu shiki no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1889,1889,1889,Polychrome woodblock print; ink and color on paper,Oban 14 1/2 x 29 5/8 in. (36.8 x 75.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3250,false,true,55261,Asian Art,Print,『鹿児島賊徒鎮静依諸将 天杯頂戴之図』|Leaders of the Pacification of the Kagoshima Rebels Celebrating with Cups of Wine from the Emperor (Kagoshima zokuto chinsei ni yotte shoshō tenhai chōdai no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"September 20th, 1877",1877,1877,Triptych of polychrome woodblock prints; ink and color on paper,Image (each): 14 1/2 × 9 3/4 in. (36.8 × 24.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3263,false,true,55323,Asian Art,Woodblock print,"西国鎮静撫諸将天杯賜之図|Illustration of the Commanders who Pacified Western Japan, Receiving the Emperor's Gift Cups (Saigoku chinbu shoshō tenpai o tamau no zu)",Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"July, 1877",1877,1877,Polychrome woodblock print; ink and color on paper,Oban 14 1/4 x 28 3/4 in. (36.2 x 73 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3269,false,true,55338,Asian Art,Print,世上各国写画帝王鏡|Mirror of Portraits of All Sovereigns in the World (Sejō kakkoku shaga teiō kagami),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"April, 1879",1879,1879,Polychrome woodblock print; ink and color on paper,14 5/8 x 29 in. (37.1 x 73.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3273,false,true,55347,Asian Art,Woodblock print,『上野不忍競馬図』|View of the Horse Track at Shinobazu in Ueno Park (Ueno shinobazu keiba zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1885,1885,1885,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/2 × 29 5/8 in. (36.8 × 75.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3276,false,true,53314,Asian Art,Print,『欧州管絃楽合奏之図』|Concert of European Music (Ōshū kangengaku gassō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1889,1889,1889,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/2 x 29 in. (36.8 x 73.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3306,false,true,55408,Asian Art,Print,『踏舞会 上野桜花観 遊ノ図』|A Dance Party: Enjoying Cherry Blossom Viewing at Ueno (Tōbukai Ueno ōka yūran no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"March, 1887",1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 in. × 28 1/4 in. (35.6 × 71.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3338,false,true,55492,Asian Art,Print,『高貴納涼ノ図』|Nobility in the Evening Cool (Koki nōryō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1887,1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,Image: 13 3/4 x 27 1/2 in. (34.9 x 69.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3340,false,true,55499,Asian Art,Woodblock prints,『女官洋服裁縫之図』|Court Ladies Sewing Western Clothing (Jokan yōfuku saihō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"August 23rd, 1887",1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 5/16 x 29 11/16 in. (36.4 x 75.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3341,false,true,55500,Asian Art,Woodblock print,『雪中梅荘群児遊戯 図』|Children Playing in the Snow under Plum Trees in Bloom (Secchū baisō gunji yūgi zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,November 1887,1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 x 28 3/4 in. (35.6 x 73 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3342,false,true,55502,Asian Art,Print,『雨過洗庭之図』|A Garden Refreshed by the Passing Rain (Ukasentei no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,July 1888,1888,1888,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 x 28 1/4 in. (37.5 x 71.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3343,false,true,55504,Asian Art,Print,『開花貴婦人競』|A Contest of Elegant Ladies among the Cherry Blossoms (Kaika kifujin kisoi),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,September 1887,1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 5/8 x 29 1/8 in. (37.1 x 74 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3386,false,true,55559,Asian Art,Print,"西国諸将鎮静天杯賜ル之図|Illustration of the Commanders who Pacified Western Japan, Receiving the Emperor's Gift Cups (Saigoku chinsei shoshō tenpai o tamawaru no zu)",Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"September, 1887",1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,14 1/4 x 29 1/4 in. (36.2 x 74.3 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3422,false,true,55623,Asian Art,Print,帝国議会貴族院之図|Illustration of The Imperial Assembly of the House of Peers (Teikoku gikai kizokuin no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1890,1890,1890,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 14 x 29 3/4 in. (35.6 x 75.6 cm),"Gift of Lincoln Kirstein, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3519,false,true,55820,Asian Art,Print,千代田の大奥|The Inner Palace of Chiyoda (Chiyoda no Ōoku),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Diptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3520,false,true,55821,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3521,false,true,55822,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3522,false,true,55823,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3523,false,true,55824,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3524,false,true,55825,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3525,false,true,55826,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3526,false,true,55827,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3527,false,true,55828,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3528,false,true,55829,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3529,false,true,55830,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3530,false,true,55831,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3531,false,true,55832,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3532,false,true,55833,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3533,false,true,55834,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3534,false,true,55835,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3535,false,true,55836,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3536,false,true,55837,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3537,false,true,55838,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3538,false,true,55839,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3540,false,true,55841,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3541,false,true,55842,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Quintiptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3542,false,true,55843,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3543,false,true,55844,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3544,false,true,55845,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3545,false,true,55846,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3546,false,true,55847,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3547,false,true,55848,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3548,false,true,55849,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3549,false,true,55850,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3550,false,true,55851,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3551,false,true,55852,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3552,false,true,55853,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3553,false,true,55854,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3554,false,true,55855,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3555,false,true,55856,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych from an album of polychrome woodblock prints; ink and color on paper,13 7/8 x 9 1/4 in. (35.2 x 23.5 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3556,false,true,55857,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych from an album of polychrome woodblock prints; ink and color on paper,9 1/4 x 13 7/8 in. (23.5 x 35.2 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3557,false,true,55858,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3558,false,true,55859,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3559,false,true,55860,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1895,1895,1895,Pentaptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3560,false,true,55861,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Diptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3561,false,true,55864,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Hexaptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3562,false,true,55865,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3563,false,true,55866,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3564,false,true,55868,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3565,false,true,55869,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3566,false,true,55870,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3567,false,true,55871,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3568,false,true,55872,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3569,false,true,55873,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3570,false,true,55874,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3571,false,true,55875,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3572,false,true,55876,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3573,false,true,55877,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3574,false,true,55878,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3576,false,true,55880,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3577,false,true,55881,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3578,false,true,55882,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3579,false,true,55883,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3580,false,true,55884,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3581,false,true,55885,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3582,false,true,55886,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3583,false,true,55887,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3584,false,true,55888,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3585,false,true,55889,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3586,false,true,55890,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3587,false,true,55891,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3588,false,true,55892,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3589,false,true,55893,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3590,false,true,55894,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3591,false,true,55895,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3241a–c,false,true,55254,Asian Art,Print,"『扶桑高貴鑑』|A Mirror of Japan’s Nobility: The Emperor Meiji, His Wife, and Prince Haru (Fūsō kōki kagami)",Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,"August 8th, 1887",1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,14 3/4 x 29 5/8 in. (37.5 x 75.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3253a–c,false,true,55264,Asian Art,Print,『上野第三回内国勧業 博覧会御幸之図』|Visit of the Empress to the Third National Industrial Promotional Exhibition at Ueno Park (Ueno dai sankai naikoku kangyō hakuran kai gyokō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1889,1889,1889,Triptych of polychrome woodblock prints; ink and color on paper,Image (each): 14 1/2 × 9 1/2 in. (36.8 × 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3688a–c,false,true,55920,Asian Art,Print,"二品親王女三宮|The Third Princess and Kashiwagi, from Chapter 34, “New Herbs I (Wakana I)” (Nihon shinnō onna sannomiya)",Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1890,1890,1890,Triptych of polychrome woodblock prints; ink and color on paper,Each 14 1/4 x 9 1/4 in. (36.2 x 23.5 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP3539.1, .2",false,true,55840,Asian Art,Print,千代田の大奥 茶の湯辺り花|Chiyoda Inner Palace: No.20 Flower Arranging in Turn (Chiyoda no Ōoku: Chanoyu mawaribana),Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Chikanobu,"Japanese, 1838–1912",,Hashimoto Chikanobu,Japanese,1838,1912,August 1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP3575.1, .2",false,true,55879,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1897,1897,1897,Triptych of polychrome woodblock prints; ink and color on paper,L. (page) 13 7/8 in. (35.2 cm); W. (page) 9 1/4 in. (23.4 cm); thickness of album 1 9/16 in. (4 cm),"Gift of Mrs. W. Walton Butterworth, 1979",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.352a–c,false,true,72830,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1898,1898,1898,Triptych of polychrome woodblock prints; color on paper,Overall (a): 14 5/8 x 9 15/16 in. (37.1 x 25.2 cm) Overall (b): 14 7/8 x 9 7/8 in. (37.8 x 25.1 cm) Overall (c): 14 5/8 x 9 15/16 in. (37.1 x 25.2 cm),"Purchase, Friends of Asian Art Gifts, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/72830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2007.49.331a, b",false,true,73583,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,late 19th century,1867,1899,Polychrome woodblock prints; ink and color on paper,Image: 13 1/8 x 9 in. (33.3 x 22.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3277,false,true,55359,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,September 1877,1877,1877,Triptych of polychrome woodblock prints; ink and color on paper,14 3/16 x 29 1/4 in. (36 x 74.3 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3387,false,true,55561,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,September 1877,1877,1877,Polychrome woodblock print; ink and color on paper,13 1/2 x 28 1/2 in. (34.3 x 72.4 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.308,false,true,73575,Asian Art,Print,"東錦浮世稿談-幡随院長兵衛|Banzuiin Chōbei, from the series Story of Brocades of the East in the Floating World (Azuma no hana ukiyo kōdan - Banzuiin Chōbei)",Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"10th month, 1867",1867,1867,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.310,false,true,73623,Asian Art,Print,月百姿 - 煙中月|Moon in the Flame from the Series One Hundred Images of the Moon (Tsuki hyaku sugata-enchūgetsu),Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"February, 1886",1886,1886,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 3/8 in. (35.6 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.311,false,true,73624,Asian Art,Print,"郵便報知新聞 645号|Postal Hōchi Newspaper no. 645, Englishman raping a wine shopkeeper's daughter (Yūbin Hōchi shinbun, roppyaku yonjū gogō)",Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"August, 1875",1875,1875,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 3/8 in. (35.9 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.312,false,true,73625,Asian Art,Print,皇国一新見聞誌|Chronicle of the Imperial Restoration (Kōkoku isshin kenbunshi),Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"June, 1876",1876,1876,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 3/8 in. (36.8 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.313,false,true,73626,Asian Art,Print,"新柳二十四時 午前十二時|Twenty-Four Hours at Shinbashi/Yanagibashi: 12 Noon. (Shinyanagi nijūyo-ji, gozen jūni-ji)",Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1880,1880,1880,Polychrome woodblock print; ink and color on paper,Image: 10 x 14 5/8 in. (25.4 x 37.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.315,false,true,73628,Asian Art,Print,"見立多以尽 - 洋行がしたい|Collection of Desires, Wish for Foreign Travel (Mitate Tai zukushi-yōkō ga shitai)",Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,"January, 1878",1878,1878,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.316,false,true,73622,Asian Art,Print,"新柳二十四時 午前十二時|Twenty-Four Hours at Shinbashi/Yanagibashi: 12 Noon. (Shinyanagi nijūyo-ji, gozen jūni-ji)",Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1880,1800,1899,Polychrome woodblock print; ink and color on paper,Image: 17 x 11 in. (43.2 x 27.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3252a–c,false,true,55263,Asian Art,Print,官女ステーション着車図|Illustration of Ladies-in-waiting boarding at a station (Kanjo sutēshon chakusha zu),Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1879,1879,1879,Polychrome woodblock print; ink and color on paper,13 5/8 x 27 3/4 in. (34.6 x 70.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.350a–c,false,true,72828,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1885,1885,1885,Triptych of polychrome woodblock prints; ink and color on paper,Overall (a): 14 3/4 x 10 in. (37.5 x 25.4 cm) Overall (b): 14 5/8 x 10 in. (37.1 x 25.4 cm) Overall (c): 14 15/16 x 10 1/16 in. (37.9 x 25.6 cm),"Purchase, Friends of Asian Art Gifts, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/72828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.260a–c,false,true,73551,Asian Art,Print,"東京名勝高輪 蒸気車鉄道之全図|Illustration of Steam Locomotive Tracks at Takanawa, from the series Famous Places in Tokyo (Tōkyō meishō Takanawa-jōki kikansha no zen zu)",Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1872,1872,1872,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 1/2 in. (35.6 x 24.1 cm) Image (b): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (c): 14 1/8 x 9 3/4 in. (35.9 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.307a–c,false,true,73574,Asian Art,Print,清盛入道布引滝遊覧悪源太義平霊討難波次郎|Kiyomori and the History of Nunobiki Waterfall: The spirit of Akugenta Yoshihira strikes Nanba Jirō. (Kiyomori nyūdō nunobiki no taki yūran akugenta yoshihira no rei nanba jirō o utsu),Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1868,1868,1868,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 9 1/2 x 14 in. (24.1 x 35.6 cm) Image (b): 9 1/2 x 14 3/4 in. (24.1 x 37.5 cm) Image (c): 9 1/2 x 14 in. (24.1 x 35.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3259,false,true,55274,Asian Art,Print,"東京名所従 上野公園不忍池中嶋弁天之景|View of Benten Shrine on Nakanoshima Island in Shinobazu Pond, Ueno Park, from the series Famous Views of Tokyo (Tōkyō Tokyo meisho yori Ueno kōen Shinobazu no ike Nakanoshima Benten no kei)",Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,"May, 1881",1881,1881,Polychrome woodblock print; ink and color on paper,Image (each): 14 3/4 × 9 1/2 in. (37.5 × 24.1 cm) 14 3/4 × 29 1/8 in. (37.5 × 74 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3267,false,true,55327,Asian Art,Print,"東京名所 上野公園内国勧業第二博覧会美術館図|Illustration of the Museum at the Second National Industrial Exhibition in Ueno, from the series Famous Places in Tokyo (Tokyo meisho-Ueno kōen naikoku kangyō daini hakurankai bijutsukan zu)",Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,1881,1881,1881,Triptych of polychrome woodblock prints; ink and color on paper,13 3/4 x 28 1/8 in. (34.9 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3349,false,true,55515,Asian Art,Print,横浜商館天主堂ノ図|Illustration of Foreign Residences and the Catholic Church in Yokohama (Yokohama shōkan tenshudō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,"10th month, 1870",1870,1870,Triptych of polychrome woodblock prints; ink and color on paper,28 3/8 x 14 1/2 in. (72.1 x 36.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3385,false,true,55556,Asian Art,Woodblock print,"諸国名所図会内 西京東山一覧|A Glimpse of Higayashiyama the Western Capital, from the series Famous Places in the Nation (Shokoku meisho zukai no uchi-Saikyō Higashiyama ichiran)",Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,"May, 1880",1880,1880,Triptych of polychrome woodblock prints; ink and color on paper,14 1/2 x 29 1/8 in. (36.8 x 74 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3388,false,true,55562,Asian Art,Woodblock print,不二詣諸品下山之図|View of the Descent from a Mountain by Many from Pilgrimage to Mt. Fuji (Fuji mōde shoshina gesan no zu),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,"November, 1883",1883,1883,Triptych of polychrome woodblock prints; ink and color on paper,14 1/4 x 28 1/4 in. (36.2 x 71.8 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.280,false,true,73563,Asian Art,Print,横浜海岸通り之風景|View of the Seafront in Yokohama (Yokohama Kagandori no fūkei),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,"5th month, 1870",1870,1870,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/2 x 28 5/8 in. (36.8 x 72.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.150a–c,false,true,73436,Asian Art,Print,東都築地ホテル館庭前の図|Illustration of the Front Garden of the Tsukiji Hotel in the Eastern Capital (Tōto Tsukiji hoteru kan niwa mae no zu),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,ca. 1868–72,1868,1872,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 3/8 x 10 in. (36.5 x 25.4 cm) Image (b): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (c): 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.151a–c,false,true,73437,Asian Art,Print,東京築地ホテル館|The Tsukiji Hotel in Tokyo (Tokyo Tsukiji hoteru kan),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Hiroshige III,"Japanese, 1843–1894",,Utagawa Hiroshige III,Japanese,1843,1894,"5th month, 1870",1870,1870,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 5/8 in. (36.8 x 24.4 cm) Image (b): 14 1/2 x 9 5/8 in. (36.8 x 24.4 cm) Image (c): 14 1/2 x 9 5/8 in. (36.8 x 24.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3384,false,true,55550,Asian Art,Print,Kempo happu shiki no zu|View of the Issuance of the Constitution,Japan,Meiji period (1868–1912),,,,Artist,,Baiju Kunitoshi,"Japanese, 1847–1899",,Baiju Kunitoshi,Japanese,1847,1899,March 1889,1889,1889,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 14 1/2 x 28 5/8 in. (36.8 x 72.7 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP228,false,true,36700,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,ca. 1878,1868,1888,Polychrome woodblock print; ink and color on paper,9 5/32 x 13 5/8 in. (23.3 x 34.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3209,false,true,55195,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,September 1904,1904,1904,Polychrome woodblock print; ink and color on paper,Oban 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3210,false,true,55196,Asian Art,Print,Kitai taihora|The Spiraling (Effect) of the Fundamental Law on the Fearful Party (Russians),Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,September 1904,1904,1904,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3211,false,true,55197,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,October 1904,1904,1904,Polychrome woodblock print; ink and color on paper,14 1/2 x 9 7/8 in. (36.8 x 25.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3212,false,true,55198,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,"1895 (Meiji 28, 7th month)",1895,1895,Polychrome woodblock print; ink and color on paper,14 x 9 5/16 in. (35.6 x 23.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3258,false,true,55272,Asian Art,Print,凱旋新橋ステーション御着之図|Illustration of the Arrival of the Emperor at Shinbashi Station Following a Victory (Gaisen Shinbashi stēshon gochaku no zu),Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,Each panel: 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3266,false,true,55325,Asian Art,Print,参謀本部行啓之図|Illustration of the Empress Visiting the General Staff Headquarters [to present a tray of bandages] (Sanbō honbu gyōkei no zu),Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,Oban 14 x 27 3/4 in. (35.6 x 70.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3274,false,true,55348,Asian Art,Print,野戦病院行幸之図|Illustration of the Empress Visiting a Field Hospital [in Hiroshima] (Yasen byōin gyōkō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,1895,1895,1895,Triptych of polychrome woodblock prints; ink and color on paper,14 3/4 x 30 in. (37.5 x 76.2cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3310,false,true,55419,Asian Art,Print,朝鮮大戦争之図|Illustration of the Great Korean War (Chōsen dai sensō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,"August, 1882",1882,1882,Right-hand sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 1/8 x 9 in. (35.9 x 22.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3336,false,true,55484,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,October 1878,1878,1878,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 14 1/8 x 28 1/8 in. (35.9 x 71.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3420,false,true,55617,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,October 1885,1885,1885,Polychrome woodblock print; ink and color on paper,Oban tat-e; 14 x 9 1/4 in. (35.6 x 23.5 cm),"Gift of Lincoln Kirstein, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.310,false,true,55988,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,dated 1874,1874,1874,Polychrome woodblock print; ink and color on paper,14 x 8 5/16 in. (35.6 x 21.1 cm),"Bequest of Gustave von Groschwitz, 1993",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.328,false,true,73581,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,late 19th century,1867,1899,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 3/4 x 28 1/4 in. (37.5 x 71.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.322a–c,false,true,73632,Asian Art,Print,『万国衣装鑑』|Mirror of National Costumes of All Nations (Bankoku ishō kagami),Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,1882,1882,1882,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 1/4 in. (35.6 x 23.5 cm) Image (b): 13 7/8 x 9 in. (35.2 x 22.9 cm) Image (c): 14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3287,false,true,55375,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",(?),Watanabe Seitei,Japanese,1851,1918,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 7/8 x 11 9/16 in. (22.5 x 29.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3283,false,true,55370,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mishima Shōsō,"Japanese, 1856–1928",,Mishima Shōsō,Japanese,1856,1928,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 3/4 x 11 1/8 in. (22.2 x 28.3 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55370,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3289,false,true,55382,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Suzuki Kason,"Japanese, 1860–1919",,Suzuki Kason,Japanese,1860,1919,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 15/16 x 10 3/4 in. (22.7 x 27.3 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3296,false,true,55393,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Suzuki Kason,"Japanese, 1860–1919",,Suzuki Kason,Japanese,1860,1919,1904,1904,1904,Frontispiece; polychrome woodblock print; ink and color on paper,Image: 8 3/4 x 12 1/4 in. (22.2 x 31.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3190,false,true,55175,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Keishū,"Japanese, 1861–1943",,Takeuchi Keishū,Japanese,1861,1943,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 3/4 x 12 1/16 in. (22.2 x 30.6 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3193,false,true,55177,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Keishū,"Japanese, 1861–1943",,Takeuchi Keishū,Japanese,1861,1943,ca. 1905,1895,1915,Polychrome woodblock print; ink and color on paper,12 x 8 1/4 in. (30.5 x 21 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3279,false,true,45279,Asian Art,Print,"(Frontispiece to) ""Fukuro Monogatari"", (by) Izumi Kyoka|Frontispiece to ""An Owl's Story"", by Izumi Kyoka",Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Keishū,"Japanese, 1861–1943",,Takeuchi Keishū,Japanese,1861,1943,ca. 1900,1890,1910,Polychrome woodblock print; ink and color on paper,Overall: 8 3/4 x 11 1/4 in. (22.2 x 28.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3286,false,true,55373,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Keishū,"Japanese, 1861–1943",(?),Takeuchi Keishū,Japanese,1861,1943,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 1/2 x 11 1/4 in. (21.6 x 28.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3319,false,true,55462,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Keishū,"Japanese, 1861–1943",,Takeuchi Keishū,Japanese,1861,1943,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,11 3/4 x 8 1/2 in. (29.8 x 21.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3242,false,true,55255,Asian Art,Woodblock print,青上練兵場観兵式之図|Illustration of Emperor's Military Review of a Parade Ground at Aoyama (Aoyama renpeijō kanpeishiki no zu),Japan,Meiji period (1868–1912),,,,Artist,,Inoue Yasuji,"Japanese, 1864–1889",,INOUE YASUJI,Japanese,1864,1889,"June, 1888",1888,1888,Triptych of polychrome woodblock prints; ink and color on paper,Oban 14 1/2 x 28 3/4 in. (36.8 x 73 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3244,false,true,37387,Asian Art,Print,東京名所之内吾妻橋新築之図|Illustration of the Opening of Azuma Bridge in Tokyo (Tokyo meisho no uchi azuma bashi shinchiku no zu),Japan,Meiji period (1868–1912),,,,Artist,,Inoue Yasuji,"Japanese, 1864–1889",,INOUE YASUJI,Japanese,1864,1889,1887,1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,14 1/8 x 28 1/2 in. (35.9 x 72.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3337,false,true,55490,Asian Art,Print,共楽泰平貴顕図|Illustration of the Emperor Enjoying a Moment of Peace with his Family (Kyōraku taihei kiken zu),Japan,Meiji period (1868–1912),,,,Artist,,Inoue Yasuji,"Japanese, 1864–1889",,INOUE YASUJI,Japanese,1864,1889,1887,1887,1887,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 29 3/8 in. (36.5 x 74.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3196,false,true,55180,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tomioka Eisen,"Japanese, 1864–1905",,Tomioka Eisen,Japanese,1864,1905,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 5/8 x 12 3/8 in. (21.9 x 31.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3282,false,true,55368,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tomioka Eisen,"Japanese, 1864–1905",,Tomioka Eisen,Japanese,1864,1905,early 20th century,1900,1933,Polychrome woodblock print; ink and color on paper,8 1/2 x 10 3/8 in. (21.6 x 26.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55368,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3294,false,true,55391,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tomioka Eisen,"Japanese, 1864–1905",,Tomioka Eisen,Japanese,1864,1905,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 5/8 x 12 1/4 in. (21.9 x 31.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3202,false,true,55186,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobori Tomoto,"Japanese, 1864–1931",,Kobori Tomoto,Japanese,1864,1931,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,11 5/8 x 8 1/2 in. (29.5 x 21.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3206,false,true,55190,Asian Art,Print,"Ito Chujo tekidan o mite shi warau suru zu|Vice Admiral Ito Mocks, Points and Looks at the Enemy Bullets",Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,ca. 1894,1884,1904,Polychrome woodblock print; ink and color on paper,6 11/16 x 9 1/8 in. (17 x 23.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3207,false,true,55191,Asian Art,Print,Kabayama Chujo furu yumo shin no zu|Vice Admiral Kabayama Advancing Bravely and Heartily,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,December 1894,1894,1894,Polychrome woodblock print; ink and color on paper,7 x 9 1/8 in. (17.8 x 23.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3260,false,true,55276,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,1894,1894,1894,Triptych of polychrome woodblock prints; ink and color on paper,Oban 13 7/8 x 27 3/4 in. (35.2 x 70.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3261,false,true,55316,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,1894 (Meiji 27),1894,1894,Triptych of polychrome woodblock prints; ink and color on paper,Oban 14 x 27 3/4 in. (35.6 x 70.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3281,false,true,55366,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 11/16 x 11 13/16 in. (22.1 x 30 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3288,false,true,55381,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,ca. 1905,1895,1915,Polychrome woodblock print; ink and color on paper,8 3/4 x 11 3/4 in. (22.2 x 29.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3292,false,true,55388,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,ca. 1906,1896,1916,Frontispiece; polychrome woodblock print; ink and color on paper,Image: 8 1/2 x 11 1/4 in. (21.6 x 28.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.210.1–.72,false,true,57107,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,1891–93,1891,1893,Album of 72 polychrome woodblock prints; ink and color on paper,Overall (each): 14 x 9 1/2 in. (35.6 x 24.1 cm),"Gift of Mr. and Mrs. Malcolm P. Aldrich, 1984",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3297,false,true,45252,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Terazaki,"Japanese, 1866–1919",,Terazaki,Japanese,1866,1919,1906,1868,1912,Polychrome woodblock print; ink and color on paper,12 1/2 x 17 3/4 in. (31.8 x 45.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP2,false,true,57113,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 20 in. (36.8 x 50.8 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP4,false,true,45495,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 19 7/8 in. (36.8 x 50.5 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP5,false,true,57115,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 20 in. (36.8 x 50.8 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP6,false,true,57116,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 9/16 x 19 15/16 in. (37 x 50.6 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP7,false,true,53816,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 9/16 x 20 in. (37 x 50.8 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP8,false,true,45277,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 9/16 x 19 15/16 in. (37 x 50.6 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP9,false,true,57117,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 5/8 x 20 in. (37.1 x 50.8 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP10,false,true,57118,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 9/16 x 20 in. (37 x 50.8 cm),"Gift of Teiji Ito, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP17,false,true,45496,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 20 in. (36.8 x 50.8 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP18,false,true,57125,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 20 in. (36.8 x 50.8 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP20,false,true,57127,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 20 in. (36.8 x 50.8 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP21,false,true,57128,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 19 15/16 in. (36.8 x 50.6 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP22,false,true,57129,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 5/8 x 19 13/16 in. (37.1 x 50.3 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP23,false,true,57130,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 1/2 x 20 1/16 in. (36.8 x 51 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP24,false,true,57131,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 5/8 x 19 13/16 in. (37.1 x 50.3 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP25,false,true,57132,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 13/16 x 19 in. (37.6 x 48.3 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP26,false,true,57133,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 9/16 x 20 1/16 in. (37 x 51 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP27,false,true,57134,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Kōgyo,"Japanese, 1869–1927",,Tsukioka Kōgyo,Japanese,1869,1927,ca. 1910,1900,1920,Polychrome woodblock print; ink and color on paper,14 13/16 x 19 in. (37.6 x 48.3 cm),"Gift of Frederick E Church, 1928",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3187,false,true,55172,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kajita Hanko,"Japanese, 1870–1917",,Kajita Hanko,Japanese,1870,1917,ca. 1900,1890,1910,Polychrome woodblock print; ink and color on paper,8 15/16 x 11 5/6 in. (22.7 x 30.1 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3204,false,true,55188,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kajita Hanko,"Japanese, 1870–1917",,Kajita Hanko,Japanese,1870,1917,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 5/8 x 8 7/8 in. (21.9 x 22.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3290,false,true,55386,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kajita Hanko,"Japanese, 1870–1917",,Kajita Hanko,Japanese,1870,1917,November 1901,1901,1901,Polychrome woodblock print; ink and color on paper,8 3/4 x 11 7/8 in. (22.2 x 30.2 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3295,false,true,55392,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kajita Hanko,"Japanese, 1870–1917",,Kajita Hanko,Japanese,1870,1917,January 1906,1906,1906,Polychrome woodblock print; ink and color on paper,8 3/4 x 11 3/4 in. (22.2 x 29.8 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3318,false,true,55460,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kajita Hanko,"Japanese, 1870–1917",,Kajita Hanko,Japanese,1870,1917,1902,1902,1902,Frontispiece; polychrome woodblock print; ink and color on paper,Image: 8 5/8 x 12 1/16 in. (21.9 x 30.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3278,false,true,55361,Asian Art,Print,御世の栄東の粧|Imperial Prosperity: Ceremony in the Eastern Capital (Miyo no sakae azuma no kewai),Japan,Meiji period (1868–1912),,,,Artist,,Yōsai Nobukazu 楊斎延一,"Japanese, 1872–1944",,Yōsai Nobukazu,Japanese,1872,1944,ca. 1900,1890,1910,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 5/8 x 29 in. (37.1 x 73.7 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3271a–c,false,true,55343,Asian Art,Print,東京名所帝国議事堂|Famous Places in Tokyo: The Imperial Diet Building (Tōkyō Meisho: Teikoku Kokkai Gijidō),Japan,Meiji period (1868–1912),,,,Artist,,Yōsai Nobukazu 楊斎延一,"Japanese, 1872–1944",,Yōsai Nobukazu,Japanese,1872,1944,1899,1899,1899,Triptych of polychrome woodblock prints; ink and color on paper,14.3 x 28.7 in. (36.3 x 72.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.134a–f,false,true,73420,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,ca. 1869,1859,1879,Hexaptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/8 x 58 1/2 in. (35.9 x 148.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.135a–f,false,true,73421,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,"7th–10th month, 1870",1870,1870,Hexaptych of polychrome woodblock prints; ink and color on paper,Image (a): 9 7/8 x 14 1/2 in. (25.1 x 36.8 cm) Image (b): 9 7/8 x 14 1/2 in. (25.1 x 36.8 cm) Image (c): 9 7/8 x 14 1/2 in. (25.1 x 36.8 cm) Image (d): 9 5/8 x 14 1/8 in. (24.4 x 35.9 cm) Image (e): 9 5/8 x 14 1/8 in. (24.4 x 35.9 cm) Image (f): 9 1/2 x 14 1/8 in. (24.1 x 35.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3421a–c,false,true,55619,Asian Art,Print,Doitsukokukan naibu kikai|The Interior Works of an Armed Japanese Battleship,Japan,Meiji period (1868–1912),,,,Artist,,Unsen,"Japanese, active ca. 1875",,Unsen,Japanese,1875,1875,1874 (Meiji 7),1874,1874,Triptych of polychrome woodblock prints; ink and color on paper,Oban; 14 1/8 x 28 3/8 in. (35.9 x 72.1 cm),"Gift of Lincoln Kirstein, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3233,false,true,55232,Asian Art,Print,新皇居於テ正殿憲法発布式之図|Illustration of the Issuing of the State Constitution in the State Chamber of the New Imperial Palace (Shin kōkyo ni oite seiden kenpō happushiki no zu),Japan,Meiji period (1868–1912),,,,Artist,,Adachi (Shōsai) Ginkō,"Japanese, active 1874–97",,Adachi Ginkō,Japanese,1874,1897,"March 14, 1889",1889,1889,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image: 14 5/8 in. × 10 in. (37.1 × 25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3234,false,true,55246,Asian Art,Print,Shinkokyo Oite Seiden Kempo Happu no zu|View of the Issuance of the State Constitution in the State Chamber of the New Imperial Palace,Japan,Meiji period (1868–1912),,,,Artist,,Adachi (Shōsai) Ginkō,"Japanese, active 1874–97",,Adachi Ginkō,Japanese,1874,1897,"March 2, 1889 (Meiji 22)",1889,1889,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image: 14 3/4 × 9 1/2 in. (37.5 × 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3235,false,true,55247,Asian Art,Print,Shinkokyo Oite Seiden Kempo Happu no zu|View of the Issuance of the State Constitution in the State Chamber of the New Imperial Palace,Japan,Meiji period (1868–1912),,,,Artist,,Adachi (Shōsai) Ginkō,"Japanese, active 1874–97",,Adachi Ginkō,Japanese,1874,1897,"March 2, 1889 (Meiji 22)",1889,1889,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image: 14 3/4 × 9 1/2 in. (37.5 × 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3272,false,true,55345,Asian Art,Print,『貴女裁縫之図』|Ladies Sewing (Kijo saihō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Adachi (Shōsai) Ginkō,"Japanese, active 1874–97",,Adachi Ginkō,Japanese,1874,1897,"September 3rd, 1887",1887,1887,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 in. × 28 1/2 in. (35.6 × 72.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.323,false,true,73585,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Adachi (Shōsai) Ginkō,"Japanese, active 1874–97",,Adachi Ginkō,Japanese,1874,1897,late 19th century,1871,1899,Polychrome woodblock print; ink and color on paper,Image: 13 7/8 x 9 in. (35.2 x 22.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.324,false,true,73390,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Adachi (Shōsai) Ginkō,"Japanese, active 1874–97",,Adachi Ginkō,Japanese,1874,1897,late 19th century,1867,1899,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3346,false,true,55511,Asian Art,Print,『東京築地舶来ぜんま い大仕かけきぬ糸をとる図』|Imported Silk Reeling Machine at Tsukiji in Tokyo,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"4th month, 1872",1872,1872,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/2 x 28 3/4 in. (36.8 x 73 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.223a–c,false,true,73514,Asian Art,Print,風船昇遥図|Illustration of a Balloon Ascending (Fūsen shōyō no zu),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,"November, 1872",1872,1872,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 7/8 in. (36.8 x 25.1 cm) Image (b): 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm) Image (c): 14 3/8 x 9 3/4 in. (36.5 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.330a–c,false,true,73582,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Masanobu,"Japanese, active ca. 1882–87",,Utagawa Masanobu,Japanese,1882,1887,"September 4, 1886",1886,1886,Three single polychrome woodblock prints; ink and color on paper,Image (a): 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm) Image (b): 14 1/2 x 10 in. (36.8 x 25.4 cm) Image (c): 14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3224,false,true,55214,Asian Art,Woodblock print,Saigo Ryusei Kubi jitsu ken|Presentation of the Head of Saigo to the Prince Arisogawa,Japan,Meiji period (1868–1912),,,,Artist,,Yamazaki Toshinobu,"Japanese, active ca. 1857–1886",,Yamazaki Toshinobu,Japanese,1857,1886,"Oct. 16, 1877 (Meiji 10)",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,14 in. × 9 1/2 in. (35.6 × 24.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3225,false,true,55216,Asian Art,Woodblock print,Saigo Ryusei Kubi jitsu ken|Presentation of the Head of Saigo to the Prince Arisogawa,Japan,Meiji period (1868–1912),,,,Artist,,Yamazaki Toshinobu,"Japanese, active ca. 1857–1886",,Yamazaki Toshinobu,Japanese,1857,1886,"Oct. 16, 1877 (Meiji 10)",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 x 27 11/16 in. (35.5 x 70.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3226,false,true,55217,Asian Art,Woodblock print,Saigo Ryusei Kubi jitsu ken|Presentation of the Head of Saigo to the Prince Arisogawa,Japan,Meiji period (1868–1912),,,,Artist,,Yamazaki Toshinobu,"Japanese, active ca. 1857–1886",,Yamazaki Toshinobu,Japanese,1857,1886,"Oct. 16, 1877 (Meiji 10)",1877,1877,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 x 27 11/16 in. (35.5 x 70.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3251,false,true,55262,Asian Art,Woodblock prints,Taiseikan Shosho Shiten|Commanders Receiving the Emperor's Drinking Cups,Japan,Meiji period (1868–1912),,,,Artist,,Yamazaki Toshinobu,"Japanese, active ca. 1857–1886",,Yamazaki Toshinobu,Japanese,1857,1886,1886 (Meiji 19),1886,1886,Triptych of polychrome woodblock prints; ink and color on paper,13 1/8 x 29 1/8 in. (33.3 x 74 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3372,false,true,55539,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Hamada Josen,"Japanese, active turn of 20th century",,Hamada Josen,Japanese,1880,1930,ca. 1906,1896,1916,Polychrome woodblock print; ink and color on paper,8 1/16 x 11 5/8 in. (20.5 x 29.5 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3230,false,true,55224,Asian Art,Print,御鳳輦之図|Illustration of the Imperial Carriage (Gohōren no zu),Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kunitoshi,"Japanese, active 2nd half of 19th century",,Utagawa Kunitoshi,Japanese,1850,1899,1889,1889,1889,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 9/16 x 29 in. (37 x 73.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3231,false,true,55229,Asian Art,Print,Go Horen no zu|View of the Imperial Carriage,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kunitoshi,"Japanese, active 2nd half of 19th century",,Utagawa Kunitoshi,Japanese,1850,1899,1889 (Meiji 22),1889,1889,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 9/16 x 29 in. (37 x 73.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3232,false,true,55230,Asian Art,Print,Go Horen no zu|View of the Imperial Carriage,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kunitoshi,"Japanese, active 2nd half of 19th century",,Utagawa Kunitoshi,Japanese,1850,1899,1889 (Meiji 22),1889,1889,One sheet of a triptych of polychrome woodblock prints; ink and color on paper,Triptych 14 9/16 x 29 in. (37 x 73.6 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP46,false,true,57155,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Natori Shunsen,"Japanese, 1886–1960, born in Kushigata machi, Yamanishi Prefecture",,Natori Shunsen,Japanese,1886,1960,1929,1929,1929,Polychrome woodblock print; ink and color on paper,14 3/4 x 9 7/8 (37.5 x 25.1 cm),"Gift of H. J. Isaacson, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2431,false,true,44586,Asian Art,Print,長襦袢の女|Woman Dressing,Japan,Taishō period (1912–26),,,,Artist,,Hashiguchi Goyō,"Japanese, 1881–1921",,Hashiguchi Goyō,Japanese,1881,1921,1920,1920,1920,Polychrome woodblock print; ink and color on paper,Image: 17 3/4 × 5 3/4 in. (45.1 × 14.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2432,false,true,44587,Asian Art,Print,夏衣の女|Woman in Summer Clothing,Japan,Taishō period (1912–26),,,,Artist,,Hashiguchi Goyō,"Japanese, 1881–1921",,Hashiguchi Goyō,Japanese,1881,1921,1920,1920,1920,Polychrome woodblock print; ink and color on paper,Image: 17 3/4 × 11 1/2 in. (45.1 × 29.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2433,false,true,56873,Asian Art,Woodblock print,,Japan,Taishō period (1912–26),,,,Artist,,Hashiguchi Goyō,"Japanese, 1881–1921",,Hashiguchi Goyō,Japanese,1881,1921,1920,1920,1920,Polychrome woodblock print; ink and color on paper,9 x 15 1/2 in. (22.9 x 39.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3415,false,true,55605,Asian Art,Woodblock print,"百人一首 うはかゑとき 権中納言匡房|Poem by Gon-chūnagon Masafusa (Ōe no Masafusa), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Taishō period (1912–26),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1921,1921,1921,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 5/8 in. (24.8 x 37.1 cm),"Gift of Mrs. Carll Tucker, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3416,false,true,55609,Asian Art,Woodblock print,"百人一首 うはかゑとき 赤染衛門|Poem by Akazome Emon, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Taishō period (1912–26),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1921,1921,1921,Polychrome woodblock print; ink and color on paper,9 5/8 x 14 3/8 in. (24.4 x 36.5 cm),"Gift of Mrs. Carll Tucker, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3417,false,true,55611,Asian Art,Woodblock print,"百人一首 乳かゑとき 中納言敦忠|Poem by Chūnagon Atsutada (Fujiwara no Asatada), from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Taishō period (1912–26),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1921,1921,1921,Polychrome woodblock print; ink and color on paper,Image: 10 1/4 × 15 1/4 in. (26 × 38.7 cm) Mat: 15 5/8 × 22 7/8 in. (39.7 × 58.1 cm),"Gift of Mrs. Carll Tucker, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3418,false,true,55612,Asian Art,Woodblock print,"百人一首 乳母か縁説 素胜法師|Poem by Sōsei Hōshi, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Taishō period (1912–26),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1921,1921,1921,Polychrome woodblock print; ink and color on paper,9 3/4 x 14 3/4 in. (24.8 x 37.5 cm),"Gift of Mrs. Carll Tucker, 1962",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3494,false,true,55721,Asian Art,Woodblock print,,Japan,Meiji (1868–1912)–Taishō (1912–26) period,,,,Artist,,Kahō,"Japanese, early 20th century",,Kahō,Japanese,1900,1930,early 20th century,1900,1912,Polychrome woodblock print; ink and color on paper,7 x 9 in. (17.8 x 22.9 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3438,false,true,55642,Asian Art,Print,江戸名所 御茶の水|Meguro,Japan,Meiji (1868–1912) or Taishō (1912–26) period,,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,early 20th century,1900,1926,Polychrome woodblock print; ink and color on paper,10 x 14 1/2 in. (25.4 x 36.8 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.74,false,true,62898,Asian Art,Teabowl,,Japan,,,,,Artist,,Chōjirō,(1516–?1592),,Chōjirō,Japanese,1516,1592,ca. 1575,1565,1585,Clay covered with a dull black glaze (Raku ware),H. 3 5/8 in. (9.2 cm); Diam. 3 3/4 in. (9.5 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.76,false,true,62900,Asian Art,Teabowl,,Japan,,,,,Artist,,Raku Donyu,"Japanese, died 1656",,"Raku, Donyu",Japanese,1556,1656,ca. 1650,1640,1660,Clay covered with a shiny black glaze and frothy edge of glaze (Raku ware),H. 3 in. (7.6 cm); Diam. 5 in. (12.7 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.305a, b",false,true,47110,Asian Art,Bowl,,Japan,,,,,Artist,,Minpei,active 19th century,,Minpei,Japanese,0019,0019,1840,1840,1840,Porcelaneous ware covered with a finely crackled glaze over which is a black enameled glaze (Awaji ware),H. 3 3/4 in. (9.5 cm); Diam. 5 1/2 in. (14 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.13.1,false,true,62879,Asian Art,Teabowl,,Japan,,,,,Artist,,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,ca. 1600,1590,1610,"Clay covered with glaze, except on lower part where it is left bare",H. 4 1/2 in. (11.4 cm); Diam. 4 5/8 in. (11.7 cm),"Rogers Fund, 1916",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.217a, b",false,true,47328,Asian Art,Teapot,,Japan,,,,,Artist,,Shuhei,"Japanese, 1788–1839",,Shuhei,Japanese,1788,1839,1800,1800,1899,"Clay covered with a partly crackled glaze and decorated with enamels on a gold ground (Kyoto ware, Satsuma style)",H. 3 1/4 in. (8.3 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.1.143a, b",false,true,62678,Asian Art,Teabowl,,Japan,,,,,Artist,,Eiraku Wazen,"Japanese, 1821–1896",,Eiraku Wazen,Japanese,1821,1896,1850,1850,1850,"Clay with speckled glaze (Kyoto ware, Bizen type)",H. 3 1/8 in. (7.9 cm); Diam. 4 3/8 in. (11.1 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.26,false,true,62620,Asian Art,Cup,,Japan,,,,,Artist,,Kenya,"Japanese, 1825–1889",,Miura Kenya,Japanese,1825,1889,ca. 1840,1830,1850,Crackled porcelain covered with glaze and decoration in enamels,H. 2 3/4 in. (7 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.15a, b",false,true,47814,Asian Art,Pot,,Japan,,,,,Artist,,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,1890,1890,1890,"White porcelain decorated in red under the glaze (Kyoto ware, Makuzu type)",H. inc. lid 8 1/2 in. (21.6 cm); H. w/o lid 7 3/4 in. (19.7 cm); Diam. 8 1/4 in. (21 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.424a, b",false,true,62836,Asian Art,Censer,,Japan,,,,,Artist,,Takemoto,"Japanese, 1845–1892",,Takemoto,Japanese,1845,1892,1892,1892,1892,"White porcelain covered with a blue glaze, run like Zhun",H. 3 3/4 in. (9.5 cm); Diam. 4 7/8 in. (12.4 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.392.2,false,true,63938,Asian Art,Teabowl,,Japan,,,,,Artist,,Ōhi Chōzaemon,"Japanese, 1850–1927",,Ōhi Chōzaemon,Japanese,1850,1927,20th century,1850,1927,Pottery (brown Raku ware),H. 3 1/4 in. (8.3 cm); Diam. 4 1/2 in. (11.4 cm),"Gift of Toshiro Ohi, 1984",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.39.11,false,true,63332,Asian Art,Bowl,,Japan,,,,,Artist,,Yabu Meizan,"Japanese, 1853–1934",,Yabu Meizan,Japanese,1853,1934,late 19th century,1871,1899,"Porcelain; exterior, spiral millefleurs bands in a variety of enamels and gold; geometric bands in black and rust enamels and gold on lower part of bowl; interior, closely covered with tiny butterflies in red, black, yellow and pale blue enamels, with gold; gold lip rim (Satsuma ware)",H. 1 5/8 in. (4.1 cm); Diam. 3 in. (7.6 cm),"Gift of Philip A. Rollins, 1946",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.221a, b",false,true,62795,Asian Art,Incense box,,Japan,,,,,Artist,,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,1670,1670,1670,"Paste covered with a transparent crackled glaze and decorated with colored enamels and gold (Kyoto ware, Satsuma type)",H. 1 1/4 in. (3.2 cm); Diam. 3 1/8 in. (7.9 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.46,false,true,62006,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,In the style of,Shunzan,,,Shunzan,Japanese,0019,0019,ca. 1770,1760,1780,"Seto ware, Oribe Revival type; glazed stoneware",H. 3 1/2 in. (8.9 cm); L. 10 3/4 in. (27.3 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.70,false,true,62889,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Ryōnyu,died 1835,,Ryōnyu,Japanese,1735,1835,ca. 1800,1790,1810,Clay covered with a shiny black glaze (Raku ware),H. 3 3/4 in. (9.5 cm); Diam. 4 in. (10.2 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.545,false,true,63233,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Ryōnyu,died 1835,,Ryōnyu,Japanese,1735,1835,ca. 1790,1780,1800,"Rounded body, well defined foot; brown clay, yellow underglaze, black, red and green overglaze (Raku ware)",H. 2 3/4 in. (7 cm); Diam. 4 1/2 in. (11.4 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.537,false,true,63197,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Keinyu,died 1893,,Keinyu,Japanese,1793,1893,ca. 1852,1842,1862,"Cylindrical with small foot; horizontal lines incised below uneven lip; brown clay with creamy glaze and green overglaze; brown latticed strokes; inside, three flying birds suggested (Raku ware)",H. 3 in. (7.6 cm); Diam. 4 1/4 in. (10.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.248,false,true,63021,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Aoki Mokubei,1767–1833,,Aoki Mokubei,Japanese,1767,1833,ca. 1800,1790,1810,Porcelain decorated in enamels and gold (Kyoto ware),H. 4 in. (10.2 cm); Diam. 9 in. (22.9 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.257,false,true,47204,Asian Art,Fire box,,Japan,Edo period (1615–1868),,,,Artist,,Aoki Mokubei,1767–1833,,Aoki Mokubei,Japanese,1767,1833,1800,1800,1800,Modeled and unglazed (Kyoto ware),H. 9 in. (22.9 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"23.225.258a, b",false,true,47249,Asian Art,Teapot,,Japan,Edo period (1615–1868),,,,Artist,,Aoki Mokubei,1767–1833,(?),Aoki Mokubei,Japanese,1767,1833,1800,1800,1800,Faience decorated with design in relief and glaze (Kyoto ware),H. 4 5/8 in. (11.7 cm); L. 5 3/4 in. (14.6 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47249,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"23.225.267a, b",false,true,47250,Asian Art,Teapot,,Japan,Edo period (1615–1868),,,,Artist,,Aoki Mokubei,1767–1833,,Aoki Mokubei,Japanese,1767,1833,1800,1800,1800,"Faience decorated in relief, celadon glaze (Kyoto ware)",H. (with cover) 4 5/8 in. (11.7 cm); L. 6 1/8 in. (15.6 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.182a, b",false,true,47329,Asian Art,Teapot,,Japan,Edo period (1615–1868),,,,Artist,,Bizan,died 1838 (?),,Bizan,Japanese,1738,1838,ca. 1850,1840,1860,"Claycovered with a finely crackled, smooth glaze and decorated in enamels and gold (Kyoto ware)",H. 3 3/4 in. (9.5 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.77,false,true,62901,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Raku Donyu,"Japanese, died 1656",,"Raku, Donyu",Japanese,1556,1656,ca. 1650,1640,1660,Clay covered with a dull red glaze (Raku ware),H. 3 1/4 in. (8.3 cm); Diam. 4 1/2 in. (11.4 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"25.60.18a, b",false,true,47113,Asian Art,Covered incense box,,Japan,Edo period (1615–1868),,,,Artist,,Raku Donyu,"Japanese, died 1656",,"Raku, Donyu",Japanese,1556,1656,ca. 1650,1640,1660,"Pottery incised in scroll pattern, in imitation of Chinese carved lacquer; brownish white glaze (Raku ware)",H. 1 1/2 in. (3.8 cm); W. sq. 2 1/2 in. (6.4 cm),"Rogers Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.73,false,true,62897,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Sonyu,"Japanese, died 1725",,Sonyu,Japanese,1625,1725,ca. 1710,1700,1720,Clay covered with glaze (Raku ware),H. 4 in. (10.2 cm); Diam. 4 1/8 in. (10.5 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.523,false,true,63177,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Sonyu,"Japanese, died 1725",,Sonyu,Japanese,1625,1725,ca. 1710,1700,1720,Clay with lustrous black glaze (Raku ware),H. 3 1/8 in. (7.9 cm); Diam. 4 3/8 in. (11.1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.491,false,true,63147,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Minpei,active 19th century,,Minpei,Japanese,0019,0019,ca. 1830,1820,1840,White glaze which stops short of foot; elaborate decoration in enamel colors (Agano ware),H. 3 in. (7.6 cm); Diam. 3 7/8 in. (9.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.100.615a, b",false,true,63109,Asian Art,Jar,,Japan,Edo period (1615–1868),,,,Artist,,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,ca. 1620,1610,1630,"Clay, red and green glaze (Kyoto ware)",H. 5 1/2 in. (14 cm); Diam. 7 in. (17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.361,false,true,56158,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,Style of,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,mid- to late18th century,1734,1799,Pottery covered with glaze and decorated with designs in slip (Kyoto ware),H. 3 in. (7.6 cm); Diam. 4 3/4 in. (12.1 cm); Diam. of foot 2 1/2 in. (6.4 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/56158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.89,false,true,44929,Asian Art,Dish,乾山様式 色絵竹文皿|Kenzan-style Dish with Bamboo Leaves,Japan,Edo period (1615–1868),,,,Artist,In the style of,Ogata Kenzan,"Japanese, 1663–1743",(?),Ogata Kenzan,Japanese,1663,1743,17th–18th century,1600,1799,Buff stoneware decorated with white and iron-brown slip and underglaze cobalt blue; gold lacquer repair,H. 1 3/8 in. (3.5 cm); Diam. 6 7/8 in. (17.5 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/44929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.90,false,true,45514,Asian Art,Dish,乾山様式 色絵竹文皿|Kenzan-style Dish with Bamboo Leaves,Japan,Edo period (1615–1868),,,,Artist,In the style of,Ogata Kenzan,"Japanese, 1663–1743",(?),Ogata Kenzan,Japanese,1663,1743,17th–18th century,1600,1799,Buff stoneware decorated with white and iron-brown slip and underglaze cobalt blue; gold lacquer repair,H. 1 3/8 in. (3.5 cm); Diam. 6 7/8 in. (17.5 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.635,false,true,63314,Asian Art,Tray,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,ca. 1720,1710,1730,"Oblong with upturned rim; gray clay; crackled cream glaze; inside, chrysanthemum sprays and poem, in brown; outer rim, conventional floral diaper in blue (Tokyo ware)",H. 1 1/2 in. (3.8 cm); W. 8 7/8 in. (22.5 cm); L. 9 3/4 in. (24.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.655,false,true,63318,Asian Art,Reading screen,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,ca. 1740,1730,1750,"Oblong; dark brown clay; moulded frame and shaped supports glazed black; both faces of screen with ivory glaze, decorated with landscapes in black (Tokyo ware)",H. 10 1/2 in. (26.7 cm); L. 15 3/4 in. (40 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.100.614a, b",false,true,52514,Asian Art,Jar,伝尾形乾山 松文水差|Water Jar (Mizusashi) with Pine Trees,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,ca. 1720,1710,1730,Stoneware with underglaze iron oxide; lacquer cover,H. 4 1/2 in. (11.4 cm); Diam. 6 1/2 in. (16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/52514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.120.634a, b",false,true,63313,Asian Art,Water jar,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,ca. 1705,1695,1715,"Cylindrical, the flat cover inset; hard, light clay; bluish-gray glaze, streaked showing white underglaze; snowy landscape with figures in boat modeled in white and brown slip, in low relief; meander borders in white and blue (Tokyo ware)",H. 5 3/4 in. (14.6 cm); Diam. 5 3/4 in. (14.6 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.517,false,true,63171,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Raku Sanyū,"Japanese, 1685–1739",,Sanyū,Japanese,1685,1739,ca. 1730,1720,1740,"Clay pitted; glaze on outside, mottled within; small unglazed space on foot (Raku ware)",H. 2 7/8 in. (7.3 cm); Diam. 3 3/8 in. (8.6 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.68,false,true,62887,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Raku Chōnyū,"Japanese, 1714–1770",,"Raku, Chōnyū",Japanese,1714,1770,ca. 1750,1740,1760,"Clay partly covered with a black, pitted glaze (Raku ware)",H. 3 1/2 in. (8.9 cm); Diam. 5 1/4 in. (13.3 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"23.225.277a, b",false,true,45946,Asian Art,Jar with cover,,Japan,Edo period (1615–1868),,,,Artist,,Kiyomizu Rokubei I,"Japanese, 1737–1799",,Kiyomizu Rokubei I,Japanese,1737,1799,1820,1820,1820,Faience boldly craquelé with design in colored enamels (Kiyomizu ware),H. 7 7/8 in. (20 cm); Diam. 8 1/8 in. (20.6 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.518a–e,false,true,58854,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Kiyomizu Rokubei I,"Japanese, 1737–1799",,Kiyomizu Rokubei I,Japanese,1737,1799,mid- to late 18th century,1750,1799,Stoneware with inlaid design (Kyoto ware),H. 3 3/4 in. (9.5 cm); Diam. of rim 3 7/8 in. (9.8 cm); Diam. of foot 2 1/4 in. (5.7 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/58854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.210,false,true,58850,Asian Art,Cup,,Japan,Edo period (1615–1868),,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,Japanese,1783,1855,1770,1700,1799,Earthenware with colored enamels and gold (Kyoto ware),H. 3 in. (7.6 cm); Diam. 4 3/4 in. (12.1 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/58850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.634,false,true,63115,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,Japanese,1783,1855,ca. 1850,1840,1860,"Clay, glaze decorated with in overglaze enamels (Kiyomizu ware)",H. 3 1/8 in. (7.9 cm); Diam. 4 3/4 in. (12.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.546,false,true,63234,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,Japanese,1783,1855,ca. 1850,1840,1860,"Distinct wheelmarks; unglazed foot; fawn-colored clay, very thin; pinkish glaze (Kiyomizu ware)",H. 3 in. (7.6 cm); Diam. 4 1/4 in. (10.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.547,false,true,63235,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,Japanese,1783,1855,ca. 1850,1840,1860,"Slightly curved outline, small unglazed foot; lightweight fawn-colored clay; thin lustrous black glaze with pine boughs and poem in white (Kiyomizu ware)",H. 3 1/8 in. (7.9 cm); Diam. 4 3/8 in. (11.1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.80,false,true,62904,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Raku Tannyu,"Japanese, 1795–1854",,"Raku, Tannyu",Japanese,1795,1854,ca. 1840,1830,1850,Clay partly covered with a shiny black glaze,H. 3 in. (7.6 cm); Diam. 4 1/2 in. (11.4 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.500,false,true,63154,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Raku Tannyu,"Japanese, 1795–1854",,"Raku, Tannyu",Japanese,1795,1854,ca. 1810,1800,1820,"Clay completely covered with uniform glaze, over a gray glaze (Raku ware)",H. 3 3/8 in. (8.6 cm); Diam. 4 3/8 in. (11.1 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.522,false,true,63176,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Raku Tannyu,"Japanese, 1795–1854",,"Raku, Tannyu",Japanese,1795,1854,ca. 1810,1800,1820,"Clay covered, except for spots on foot, by glaze and overglaze which has turned black (Raku ware)",H. 2 3/4 in. (7 cm); Diam. 4 5/8 in. (11.7 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.87,false,true,45971,Asian Art,Cup,,Japan,Edo period (1615–1868),,,,Artist,,Shuntai,"Japanese, 1799–1878",,Shuntai,Japanese,1799,1799,1825,1825,1825,"Clay covered with a transparent crackled glaze with streaks (Mino ware, Ofuke type)",H. 5 in. (12.7 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.516,false,true,63170,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Eiraku Wazen,"Japanese, 1821–1896",,Eiraku Wazen,Japanese,1821,1896,ca. 1850,1840,1860,"Clay; thick glaze, sufflé with touches of black inside and out (Eiraku ware)",H. 3 in. (7.6 cm); Diam. 4 1/4 in. (10.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.531,false,true,63190,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,ca. 1850,1840,1860,Clay with flecks of crackle inside and out (Awata ware),H. 3 in. (7.6 cm); Diam. 4 5/8 in. (11.7 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.195a, b",false,true,47267,Asian Art,Teapot,,Japan,Edo period (1615–1868),,,,Artist,,Takahashi Sōhei,"Japanese, 1804?–?1835",,Takahashi Sōhei,Japanese,1804,1835,1840,1840,1840,"Clay covered with a finely crackled transparent glaze and decorated in colored enamels (Kyoto ware, after Ninsei)",H. 2 1/2 in. (6.4 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.60.34,false,true,63089,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Tokunyu,"Japanese, died ca. 1775",,Tokunyu,Japanese,1675,1775,ca. 1770,1760,1780,Pottery with black glaze (Raku ware),Diam. 4 3/8 in. (11.1 cm),"Rogers Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.69,false,true,62888,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Tokunyu,"Japanese, died ca. 1775",,Tokunyu,Japanese,1675,1775,ca. 1770,1760,1780,Clay partly covered with a dull black glaze (Raku ware),H. 3 1/4 in. (8.3 cm); Diam. 5 in. (12.7 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.204,false,true,45962,Asian Art,Jar,,Japan,Edo period (1615–1868),,,,Artist,,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,1730,1730,1730,"Clay covered with a transparent crackled glaze; decorated colored enamels and gold (Kyoto ware, Takamatsu type)",H. 8 1/2 in. (21.6 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.666,false,true,63141,Asian Art,Tea jar,,Japan,Edo period (1615–1868),,,,Artist,,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,ca. 1660,1650,1670,Clay; black glaze with two bands of crackled white glaze; (Awata ware),H. 3 1/2 in. (8.9 cm); Diam. 2 1/8 in. (5.4 cm); Diam. of rim 1 in. (2.5 cm); Diam. of base 1 1/8 in. (2.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.668,false,true,63143,Asian Art,Incense burner,野々村仁清工房 四季花文香炉|Ninsei-style Incense Burner with Flowers of the Four Seasons,Japan,Edo period (1615–1868),,,,Artist,Workshop of,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,mid-17th century,1634,1666,Stoneware with overglaze enamels,H. 6 3/4 in. (17.1 cm); W. 7 1/4 in. (18.4 cm); D. 7 1/4 in. (18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"23.225.236a, b",false,true,44928,Asian Art,Incense box,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,ca. 1648–1649,1648,1649,"Glazed stoneware, colored enamels and gold",H. 1 1/4 in. (3.2 cm); Diam. 3 1/4 in. (8.3 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/44928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.559a–f,false,true,53832,Asian Art,Tea caddy,,Japan,Edo period (1615–1868),,,,Artist,,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,ca. 1650,1640,1660,"Stoneware with red, brown, and black glazes",H. (with lid) 6 in. (15.2 cm); Diam. 1 7/8 in. (4.8 cm); Diam. of rim 1 1/8 in. (2.9 cm); Diam. of base 1 1/4 in. (3.2 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.71,false,true,62890,Asian Art,Teabowl,,Japan,Meiji period (1868–1912),,,,Artist,,Keinyu,died 1893,,Keinyu,Japanese,1793,1893,ca. 1870,1860,1880,Clay covered with a black glaze; on the lower part with a transparent brown glaze (Raku ware),H. 4 in. (10.2 cm); Diam. 4 3/8 in. (11.1 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.103,false,true,57186,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/57186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.111,false,true,46691,Asian Art,Flower vase,紫馬麒麟卍紗綾形紋瓶 (一対)|Vase with Horse and Kirin on Geometric Sayagata (key fret) Pattern (one of a pair),Japan,Meiji period (1868–1912),,,,Artist,Style of,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,late 19th century,1867,1899,Porcelain with incised design and underglaze red,H. 4 3/4 in. (12.1 cm); Diam. of rim 1 1/4 in. (3.2 cm); Diam. 4 3/4 in. (12.1 cm); Diam. of base 2 1/4 in. (5.7 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.112,false,true,46692,Asian Art,Flower vase,紫馬麒麟卍紗綾形紋瓶 (一対)|Vase with Horse and Kirin on Geometric Sayagata (key fret) Pattern (one of a pair),Japan,Meiji period (1868–1912),,,,Artist,Style of,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,late 19th century,1867,1899,Porcelain with incised design and underglaze red,H. 4 3/4 in. (12.1 cm); Diam. of rim 1 1/4 in. (3.2 cm); Diam. 4 3/4 in. (12.1 cm); Diam. of base 2 1/4 in. (5.7 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.408.1,false,true,63939,Asian Art,Vase,波に鯉文瓶|Vase with Carps in Waves,Japan,Meiji period (1868–1912),,,,Artist,,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,late 19th–early 20th century,1867,1933,Porcelain with metal fittings,H. 10 in. (25.4 cm); Diam. 3 1/4 in. (8.3 cm) Diam. of rim: 3 3/4 in. (9.5 cm) Diam. of foot: 4 7/8 in. (12.4 cm),"Gift of Stanley J. Love, 1984",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"91.1.367a, b",false,true,53613,Asian Art,Water jar,バッタ行列文水指|Water Jar with Procession of Grasshoppers,Japan,Meiji period (1868–1912),,,,Artist,,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,late 19th century,1868,1899,Stoneware with polychrome overglaze enamels and gold with a wood lid and ivory knob (Makuzu ware),H. 5 3/4 in. (14.6 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.364a, b",false,true,45997,Asian Art,Wine pot,染付龍文水注|Ewer with Dragon,Japan,Meiji period (1868–1912),,,,Artist,Style of,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,late 19th century,1867,1899,Porcelain with underglaze blue,H. 9 1/4 in. (23.5 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.446,false,true,46627,Asian Art,Vase,蝶文瓶|Vase with Butterflies,Japan,Meiji period (1868–1912),,,,Artist,,Katō Tomotarō,"Japanese, 1851–1916",,Katō Tomotarō,Japanese,1851,1916,late 19th century,1867,1899,"Porcelain with underglaze blue, overglaze pink and white slip",H. 6 1/4 in. (15.9 cm); Diam. 4 in. (10.2 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.35a, b",false,true,62722,Asian Art,Covered pot,青磁月に梅樹文水指|Water Jar with Plum Tree,Japan,Meiji period (1868–1912),,,,Artist,,Katō Tomotarō,"Japanese, 1851–1916",,Katō Tomotarō,Japanese,1851,1916,ca. 1890,1880,1900,Porcelain with blue-and-white slip under celadon glaze,H. 7 3/4 in. (19.7 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"25.60.28a, b",false,true,63085,Asian Art,Tea jar,,Japan,Kamakura period (1185–1333),,,,Artist,Attributed to,Tôshiro,,,Tôshiro,Japanese,1200,1230,ca. 1245,1235,1255,Crackled brown glaze on a clay that has burned very dark; specimen of class known as Manako Tubutsu (Seto ware),H. 3 in. (7.6 cm),"Rogers Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"93.3.147a, b",false,true,62763,Asian Art,Tea jar,,Japan,Kamakura period (1185–1333),,,,Artist,,Tôshiro,,,Tôshiro,Japanese,1200,1230,ca. 1320 (?),1310,1330,Clay covered with a mottled glaze (Seto ware),H. 4 1/4 in. (10.8 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.64,false,true,53833,Asian Art,Teabowl,,Japan,Momoyama period (1573–1615),,,,Artist,,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,early 17th century,1600,1633,Earthenware covered with light red glaze (Raku ware),H. 3 7/8 in. (9.8 cm); Diam. 5 in. (12.7 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.492a–c,false,true,63148,Asian Art,Teabowl,,Japan,Momoyama period (1573–1615),,,,Artist,Attributed to,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,ca. 1600,1590,1610,"Cclay, pitted, covered with glaze having patches; vertical incisions near lip, under glaze (Raku ware)",H. 3 3/4 in. (9.5 cm); Diam. 5 1/8 in. (13 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.68a–c,false,true,60505,Asian Art,Cabinet,,Japan,,,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,1663–1747,1663,1747,Lacquered wood,Cabinet and stools: H. 66 3/4 in. (169.5 cm); W. 41 1/4 in. (104.8 cm); D. 19 1/2 in. (49.5 cm) Cabinet: H. 36 1/4 in. (92.1 cm); W. 41 1/4 in. (104.8 cm); D. 9 1/2 in. (49.5 cm) Each stool: H. 30 1/2 in. (77.5 cm); W. 18 1/2 in. (47 cm); D. 8 1/2 in. (47 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Furniture,,http://www.metmuseum.org/art/collection/search/60505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.408.2,false,true,60138,Asian Art,Water jug,,Japan,Meiji period (1868–1912),,,,Artist,,Tō Kai Ko,"Japanese, active late 19th century",,Tō Kai Ko,Japanese,1871,1899,late 19th century,1871,1899,Silver,H. 6 1/2 in. (16.5 cm); W. 5 1/2 in. (14 cm); W. (at base) 3 in. (7.6 cm),"Gift of Stanley J. Love, 1984",,,,,,,,,,,,Metalwork,,http://www.metmuseum.org/art/collection/search/60138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.177.22,false,true,57332,Asian Art,Hanging scroll,,Japan,,,,,Artist,"Anonymous, in the style of",Mori Sosen,"Japanese, 1747–1821",,Mori Sosen,Japanese,1747,1821,style of 18th century,1700,1799,Hanging scroll; ink and color on silk,40 1/2 x 13 3/4 in. (102.9 x 34.9 cm),"Bequest of Katherine S. Dreier, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.38,false,true,55367,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Imao Keinen,"Japanese, 1845–1924",,Imao Keinen,Japanese,1845,1924,19th–20th century,1845,1924,Hanging scroll; ink and color on silk,45 3/4 x 10 1/2 in. (116.2 x 26.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.4,false,true,57330,Asian Art,Painting,,Japan,,,,,Artist,,Hashimoto Kansetsu,"Japanese, 1883–1945",,Hashimoto Kansetsu,Japanese,1883,1945,20th century,1900,1945,Painting; watercolor on paper,19 x 22 in. (48.3 x 55.9 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.19,false,true,49053,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Gyokusen,died 1852,,Gyokusen,Japanese,1752,1852,1812,1615,1868,Hanging scroll; ink on paper,34 x 15 1/2 in. (86.4 x 39.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.110,false,true,49072,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Hidaka Tetsuo,1791–1875,,Hidaka Tetsuo,Japanese,1791,1875,1862,1862,1862,Hanging scroll; ink on satin,45 1/2 x 20 5/8 in. (115.5 x 52.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.99,false,true,45736,Asian Art,Hanging scroll,西王母図|Queen Mother of the West,Japan,Edo period (1615–1868),,,,Artist,,Kano Osanobu,1796–1846,,Kano Osanobu,Japanese,1796,1846,first half of the 19th century,1800,1846,Hanging scroll; ink and color on silk,39 3/4 x 14 7/8 in. (101 x 37.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.78,false,true,45378,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kita Genki,active 1664–98,,Kita Genki,Japanese,1664,1698,1666,1666,1666,Hanging scroll; ink and color on silk,Image: 43 1/8 × 17 5/16 in. (109.5 × 44 cm) Overall with mounting: 79 1/4 × 22 1/4 in. (201.3 × 56.5 cm) Overall with knobs: 79 1/4 × 24 3/8 in. (201.3 × 61.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.138.1,false,true,45380,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kaigetsudō Dohan,active 1710–16,,Kaigetsudō Dohan,Japanese,1710,1716,ca. 1715,1710,1716,Hanging scroll; ink and color on paper,51 x 17 3/4 in. (129.5 x 45.1 cm),"Seymour Fund, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.87,false,true,45770,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kawamata Tsunemasa,active 1716–48,,Kawamata Tsunemasa,Japanese,1706,1758,first half of the 18th century,1716,1749,"Hanging scroll; ink, color, and gold on silk",33 3/4 x 10 5/8 in. (85.7 x 27 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.174.1–.3,false,true,45389,Asian Art,Triptych of hanging scrolls,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Kōi,"Japanese, died 1636",,Kano Kōi,Japanese,,1636,early 17th century,1600,1636,Triptych of hanging scrolls; ink and color on paper,46 1/2 x 19 in. (118.1 x 48.3 cm),"Gift of Mr. and Mrs. Benjamin J. Levy, 1963",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.64,false,true,57175,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,After,Sesson Shūkei,ca. 1504–ca. 1589,,Sesson Shūkei,Japanese,1504,1589,probably 19th century,1800,1868,Hanging scroll; ink on paper,28 1/2 x 16 in. (72.4 x 40.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.453,false,true,40353,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,After,Sesson Shūkei,ca. 1504–ca. 1589,,Sesson Shūkei,Japanese,1504,1589,probably 19th century,1800,1868,Matted painting; ink on paper,Image: 16 1/2 x 20 in. (41.9 x 50.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1133.2,false,true,77188,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōei,"Japanese, 1519–1592",,Kano Shōei,Japanese,1519,1592,late 16th century,1567,1599,Hanging scroll; ink and color on paper,Image: 25 1/2 × 12 15/16 in. (64.7 × 32.9 cm) Overall with knobs: 59 1/8 × 19 3/4 in. (150.1 × 50.1 cm) Overall with mounting: 59 1/8 × 17 13/16 in. (150.1 × 45.3 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2013",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1133.1,false,true,77179,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Fūgai Ekun,"Japanese, 1568–1654",,Fūgai Ekun,Japanese,1568,1654,datable to 1650,1650,1650,Hanging scroll; ink on paper,Image: 12 15/16 × 17 3/16 in. (32.9 × 43.7 cm) Overall with knobs: 45 9/16 × 20 1/16 in. (115.8 × 50.9 cm) Overall with mounting: 45 9/16 × 18 1/16 in. (115.8 × 45.9 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2013",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.124,false,true,45340,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Iwasa Matabei,"Japanese, 1578–1650",,Iwasa Matabei,Japanese,1578,1650,early 17th century,1600,1633,"Hanging scroll; ink, color, gold and silver on paper",Image: 11 1/4 × 12 3/4 in. (28.6 × 32.4 cm); Overall with mounting: 47 3/4 × 16 5/8 in. (121.3 × 42.2 cm); Overall with knobs: 47 3/4 × 18 3/4 in. (121.3 × 47.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.142,false,true,44761,Asian Art,Handscroll,烏丸光広筆 十牛図歌賛図巻|Ten Oxherding Songs,Japan,Edo period (1615–1868),,,,Artist,,Karasumaru Mitsuhiro,"Japanese, 1579–1638",,Karasumaru Mitsuhiro,Japanese,1579,1638,ca. 1634,1624,1638,Handscroll; ink on dyed paper with stenciled decoration in gold and silver,11 3/4 x 107 in. (29.9 x 271.8 cm),"Purchase, Friends of Asian Art Gifts, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.48a–d,true,true,44858,Asian Art,Door panels,老梅図襖|Old Plum,Japan,Edo period (1615–1868),,,,Artist,,Kano Sansetsu,"Japanese, 1590–1651",,Kano Sansetsu,Japanese,1590,1651,1646,1646,1646,"Four sliding-door panels (fusuma); ink, color, gold, and gold leaf on paper",Overall (of all four panels): 68 3/4 x 191 1/8 in. (174.6 x 485.5 cm) Overall (a): 68 3/8 x 47 5/8 in. (173.7 x 121 cm) Overall (b): 68 3/8 x 48 3/4 in. (173.7 x 123.8 cm) Overall (c): 68 1/2 x 47 3/4 in. (174 x 121.3 cm) Overall (d): 68 1/2 x 47 3/4 in. (174 x 121.3 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.26,false,true,45698,Asian Art,Hanging scroll,神農・夏冬山水図|Winter Landscape,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,1662,1662,1662,One from a set of three hanging scrolls; ink and color on silk,44 1/2 x 15 1/2 in. (113 x 39.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.27,false,true,45699,Asian Art,Hanging scroll,神農・夏冬山水図|Portrait of Emperor Shennong,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,1665,1665,1665,One from a set of three hanging scrolls; ink and color on silk,41 1/2 x 18 3/16 in. (105.4 x 46.2 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.28,false,true,45700,Asian Art,Hanging scroll,神農・夏冬山水図|Summer Landscape,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,1662,1662,1662,One from a set of three hanging scrolls; ink and color on silk,44 1/2 x 15 1/2 in. (113 x 39.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.174,false,true,73193,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,1635–45,1635,1645,Hanging scroll; ink on paper,Image: 40 x 9 1/2 in. (101.6 x 24.1 cm) Overall with mounting: 70 1/2 x 10 in. (179.1 x 25.4 cm) Overall with rollers: W. 12 in. (30.5 cm),"The Miriam and Ira D. Wallach Foundation Fund, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.464.1,false,true,44297,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Kano artist After,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,dated 1666,1666,1666,Hanging scroll mounted as a panel; ink and color on paper,Image: 33 1/8 x 16 1/4 in. (84.1 x 41.3 cm) Overall with mounting: 36 1/2 x 18 7/8 in. (92.7 x 47.9 cm) Framed: 37 3/4 x 19 7/8 in. (95.9 x 50.5 cm),"Gift of John and Lili Bussel, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.464.2,false,true,56098,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Kano artist after,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,dated 1666,1666,1666,Hanging scroll mounted as a panel; ink and color on paper,Image: 33 1/8 x 16 in. (84.1 x 40.6 cm) Overall with mounting: 36 1/2 x 18 5/8 in. (92.7 x 47.3 cm) Framed: 37 7/16 x 19 7/8 in. (95.1 x 50.5 cm),"Gift of John and Lili Bussel, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/56098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.61,false,true,54704,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,ca. 1650,1640,1650,Album leaf; ink on paper,8 3/8 x 10 3/4 in. (21.3 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.64,false,true,54707,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,ca. 1650,1640,1660,Album leaf; ink and color on silk,10 x 14 in. (25.4 x 35.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.49,false,true,45418,Asian Art,Hanging scroll,月夜山水図|Landscape in Moonlight,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,after 1662,1663,1674,One of a triptych of hanging scrolls; ink on silk,Image: 39 5/8 x 16 3/4 in. (100.6 x 42.5 cm) Overall: 75 3/8 x 23 1/2 in. (191.5 x 59.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.50,false,true,45419,Asian Art,Hanging scroll,月夜山水図|Landscape in Moonlight,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,after 1662,1663,1674,One of a triptych of hanging scrolls; ink on silk,Image: 39 5/8 x 16 3/4 in. (100.6 x 42.5 cm) Overall: 75 1/4 x 23 1/2 in. (191.1 x 59.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.51,false,true,45420,Asian Art,Hanging scroll,月夜山水図|Landscape in Moonlight,Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,after 1662,1663,1674,One of a triptych of hanging scrolls; ink on silk,Image: 39 5/8 x 16 3/4 in. (100.6 x 42.5 cm) Overall: 75 1/4 x 23 1/2 in. (191.1 x 59.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.52,false,true,49098,Asian Art,Handscroll,狩野探幽筆 『画苑』|Famous Themes for Painting Study Known as “The Garden of Painting” (Gaen),Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,1670,1600,1670,One of a pair of handscrolls; ink on paper,10 7/8 in. x 10 ft. 7 5/8 in. (27.6 x 324.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.53,false,true,49099,Asian Art,Handscroll,狩野探幽筆 『画苑』|Famous Themes for Painting Study Known as “The Garden of Painting” (Gaen),Japan,Edo period (1615–1868),,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,1670,1670,1670,Handscroll; ink on paper,10 7/8 in. x 11 ft. 7 7/16 in. (27.6 x 354.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.768.1,false,true,77184,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tosa Mitsuoki,"Japanese, 1617–1691",,Tosa Mitsuoki,Japanese,1617,1691,mid- to late 17th century,1634,1699,Hanging scroll; color on silk,Image: 46 5/8 × 22 3/16 in. (118.5 × 56.3 cm) Overall with knobs: 69 × 28 1/16 in. (175.2 × 71.2 cm) Overall with mounting: 69 × 26 in. (175.2 × 66.1 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.63,false,true,57239,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,late 17th–early 18th century,1667,1713,One of a triptych of hanging scrolls; ink and color on paper,31 x 11 3/8 in. (78.7 x 28.9 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1961",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.146,false,true,44897,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,17th–18th century,1636,1713,Handscroll; ink and color on paper,12 3/4 in. × 16 ft. 1 1/4 in. (32.4 × 490.9 cm),"Purchase, Fletcher Fund and J. Pierpont Morgan Gift, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.88,false,true,45721,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,17th–18th century,1636,1713,Hanging scroll; ink and color on silk,35 3/4 x 13 in. (90.8 x 33 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.89,false,true,45722,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,17th–18th century,1636,1713,Hanging scroll; ink and color on silk,16 3/4 x 28 1/2 in. (42.5 x 72.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.55,false,true,45042,Asian Art,Hanging scroll,四愛図|Four Admirers,Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,late 17th–early 18th century,1667,1713,Hanging scroll; ink and color on silk,Image: 21 5/16 x 46 in. (54.1 x 116.8 cm) Overall with mounting: 67 5/8 x 55 1/2 in. (171.8 x 141 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.1398a–j,false,true,45723,Asian Art,Album leaves,十鷹書画冊|Album of Hawks and Calligraphy,Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,17th–18th century,1636,1713,Album of ten paintings; ink and color on silk,Each leaf: 10 3/4 x 9 7/16 in. (27.3 x 23.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.45,false,true,45734,Asian Art,Hanging scroll,粟に鶉図|Quail and Millet,Japan,Edo period (1615–1868),,,,Artist,,Kiyohara Yukinobu,"Japanese, 1643–1682",,Kiyohara Yukinobu,Japanese,1643,1682,late 17th century,1667,1682,Hanging scroll; ink and color on silk,46 5/8 x 18 3/4 in. (118.4 x 47.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.522.2,false,true,77204,Asian Art,Hanging scroll,"小川に連雀桜竹図|Waxwings, Cherry Blossoms, and Bamboo by a Stream",Japan,Edo period (1615–1868),,,,Artist,,Kiyohara Yukinobu,"Japanese, 1643–1682",,Kiyohara Yukinobu,Japanese,1643,1682,late 17th century,1667,1682,Hanging scroll; ink and color on silk,Image: 39 1/8 x 16 3/8 in. (99.4 x 41.6 cm) Overall with knobs: 23 1/2 in. (59.7 cm) Overall with mounting: 71 7/8 x 21 9/16 in. (182.5 x 54.7 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.33,false,true,45733,Asian Art,Hanging scroll,地蔵菩薩像|Jizō Bosatsu,Japan,Edo period (1615–1868),,,,Artist,,Hanabusa Itchō,"Japanese, 1652–1724",,Hanabusa Itchō,Japanese,1652,1724,1667–98,1667,1698,"Hanging scroll; ink, color, and gold on paper",24 3/4 x 10 1/2 in. (62.9 x 26.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.23,false,true,45724,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tanshin Morimasa,"Japanese, 1653–1718",,Tanshin Morimasa,Japanese,1653,1718,late 17th century,1667,1699,One of a triptych of hanging scrolls; ink and color on silk,60 x 30 1/4 in. (152.4 x 76.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.24,false,true,45725,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tanshin Morimasa,"Japanese, 1653–1718",,Tanshin Morimasa,Japanese,1653,1718,late 17th century,1667,1699,One of a triptych of hanging scrolls; ink and color on silk,59 1/4 x 30 1/4 in. (150.5 x 76.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.25,false,true,45726,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tanshin Morimasa,"Japanese, 1653–1718",,Tanshin Morimasa,Japanese,1653,1718,17th–18th century,1653,1718,One of a triptych of hanging scrolls; ink and color on silk,58 3/4 x 30 1/4 in. (149.2 x 76.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.78,false,true,45727,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Tanshin Morimasa,"Japanese, 1653–1718",,Tanshin Morimasa,Japanese,1653,1718,17th–18th century,1653,1718,Hanging scroll; ink and color on silk,37 13/16 x 15 13/16 in. (96 x 40.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.526,false,true,40347,Asian Art,Folding fan mounting as a hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,17th–18th century,1658,1716,"Folding fan mounting as a hanging scroll; ink, color, and gold on paper",6 3/4 x 20 3/4 in. (17.2 x 52.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.6,false,true,45728,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Chikanobu,"Japanese, 1660–1728",,Kano Chikanobu,Japanese,1660,1728,17th–18th century,1660,1728,Hanging scroll; ink and color on silk,40 1/4 x 17 5/8 in. (102.2 x 44.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.7,false,true,45730,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Chikanobu,"Japanese, 1660–1728",,Kano Chikanobu,Japanese,1660,1728,17th–18th century,1660,1728,Hanging scroll; ink and color on silk,Image: 50 1/4 × 21 1/4 in. (127.6 × 54 cm) Overall with mounting: 85 1/8 × 26 1/8 in. (216.2 × 66.4 cm) Overall with knobs: 85 1/8 × 28 1/2 in. (216.2 × 72.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.118.1,false,true,48908,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Chikanobu,"Japanese, 1660–1728",,Kano Chikanobu,Japanese,1660,1728,17th–18th century,1660,1728,Hanging scroll; ink and color on paper Reverse side: ink and color on silk,40 x 13 9/16 in. (101.6 x 34.4 cm),"Rogers Fund, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.512,false,true,40354,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,dated 1741,1741,1741,Hanging scroll; ink and color on paper,12 x 17 3/4 in. (30.5 x 45.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.65,false,true,45275,Asian Art,Hanging scroll,定家詠十二ヶ月和歌花鳥図『拾遺愚草』より四月|“Fourth Month” from Fujiwara no Teika’s “Birds and Flowers of the Twelve Months”,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,1743,1743,1743,Hanging scroll; ink and color on paper,Image: 6 5/16 x 8 15/16 in. (16 x 22.7 cm) Overall with mounting: 43 1/4 x 19 in. (109.9 x 48.3 cm) Overall with knobs: 43 1/4 x 20 5/8 in. (109.9 x 52.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.66,false,true,45276,Asian Art,Hanging scroll,尾形乾山筆 定家詠十二ヶ月和歌花鳥図「拾遺愚草』より六月|“Sixth Month” from Fujiwara no Teika’s “Birds and Flowers of the Twelve Months”,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,1743,1743,1743,Hanging scroll; ink and color on paper,Image: 6 1/4 x 9 1/8 in. (15.9 x 23.2 cm) Overall with mounting: 43 1/4 x 19 in. (109.9 x 48.3 cm) Overall with knobs: 43 1/4 x 20 5/8 in. (109.9 x 52.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.67,false,true,45076,Asian Art,Hanging scroll,蔦紅葉図|Autumn Ivy,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,after 1732,1733,1743,"Album leaf mounted as a hanging scroll; ink, color, and gold on paper",Image: 8 3/8 x 10 7/8 in. (21.3 x 27.6 cm) Overall: 44 7/8 x 22 1/4 in. (114 x 56.5 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.58,false,true,49085,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,1740,1700,1800,Hanging scroll; ink and color on silk,37 1/4 x 12 1/4 in. (94.6 x 31.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.73,false,true,49084,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yokoya Sōmin,"Japanese, 1669–1733",,Yokoya Sōmin,Japanese,1669,1733,early 18th century,1700,1733,Hanging scroll; ink on paper,Image: 27 1/8 x 8 1/8 in. (68.9 x 20.6 cm) Overall with mounting: 60 1/2 x 12 15/16 in. (153.7 x 32.9 cm) Overall with knobs: 60 1/2 x 14 13/16 in. (153.7 x 37.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.24,false,true,45376,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Furunobu,"Japanese, 1696–1731",,Kano Furunobu,Japanese,1696,1731,early 18th century,1700,1731,Hanging scroll; ink and color on silk,19 1/2 x 41 1/8 in. (49.5 x 104.5 cm),"Gift of August Belmont, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.97,false,true,45352,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,Japanese,1716,1783,ca. 1780,1770,1783,"Hanging scroll, color on satin",14 3/16 x 9 15/16 in. (36.1 x 25.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.572.1,false,true,73348,Asian Art,Hanging scroll,曽我蕭白筆 寿老人図|The God of Good Fortune Jurōjin,Japan,Edo period (1615–1868),,,,Artist,,Soga Shōhaku,"Japanese, 1730–1781",,Soga Shōhaku,Japanese,1730,1781,mid- to late 18th century,1734,1799,Hanging scroll; ink and color on paper,Image: 52 3/4 x 22 5/16 in. (134 x 56.7 cm) Overall with mounting: 81 1/2 x 28 1/2 in. (207 x 72.4 cm) Overall with knobs: 81 1/2 x 30 3/4 in. (207 x 78.1 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.71,false,true,49031,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,Japanese,1733,1795,1769,1769,1769,Hanging scroll; ink on paper,14 x 22 5/16 in. (35.5 x 56.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.245,false,true,54038,Asian Art,Hanging scroll,宋紫山筆 嵐に鶏図|Rooster in a Storm,Japan,Edo period (1615–1868),,,,Artist,,Sō Shizan,"Japanese, 1733–1805",,Sō Shizan,Japanese,1733,1805,1783,1783,1783,"Hanging scroll; ink, color, and gold on silk",38 7/8 x 17 1/4 in. (98.7 x 43.8 cm),"Friends of Asian Art, Purchase, The Dillon Fund Gift, in honor of Wen C. Fong, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.103,false,true,45426,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okada Beisanjin,"Japanese, 1744–1820",,Okada Beisanjin,Japanese,1744,1820,1817,1817,1817,Hanging scroll; ink and color on paper,Image: 70 1/8 x 35 5/8 in. (178.1 x 90.5 cm) Overall: 91 x 43 1/4in. (231.1 x 109.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.99,false,true,45393,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Noro Kaiseki,"Japanese, 1747–1828",,Noro Kaiseki,Japanese,1747,1828,1822,1822,1822,Hanging scroll; ink and color on silk,Image: 53 3/4 x 26 7/8 in. (136.5 x 68.3 cm) Overall: 86 3/8 x 36 7/8in. (219.4 x 93.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.134,false,true,49083,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Shokusanjin (Ōta Nanpo),"Japanese, 1749–1823",,,Japanese,1749,1823,1820,1820,1820,Hanging scroll; ink on silk,38 1/8 x 13 5/16 in. (96.8 x 33.8 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.21,false,true,49061,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ganku,"Japanese, 1749–1838",,Ganku,Japanese,1749,1838,dated 1790,1615,1868,Hanging scroll; ink and color on silk,38 3/4 x 13 3/4 in. (98.4 x 34.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2011.572.2a, b",false,true,77168,Asian Art,Hanging scrolls,鶴図|Cranes,Japan,Edo period (1615–1868),,,,Artist,,Nagasawa Rosetsu,"Japanese, 1754–1799",,Nagasawa Rosetsu,Japanese,1754,1799,1780s,1780,1789,Pair of hanging scrolls; ink and color on paper,Image (each scroll): 61 7/16 x 35 7/8 in. (156 x 91.2 cm) Overall with mounting (each scroll): 84 x 36 3/4 in. (213.4 x 93.3 cm) Overall with knobs (each scroll): 84 x 39 1/4 in. (213.4 x 99.7 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.9,false,true,45398,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,late 18th–19th century,1767,1829,Hanging scroll; ink and color on silk,12 5/8 x 27 5/8 in. (32.1 x 70.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.49,false,true,45800,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,dated 1780,1615,1868,Ink and color on silk,29 7/8 x 11 11/16 in. (75.9 x 29.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.727,false,true,675695,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,ca. 1810,1810,1810,Hanging scroll; ink and color on silk,36 × 12 3/4 in. (91.4 × 32.4 cm),"Gift of Sebastian Izzard and Masaharu Nagano, in memory of T. Richard Fishbein, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/675695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.68,false,true,48981,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,After,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,1815,1815,1815,Handscroll; ink and color on paper,13 3/8 in. x 30 ft. 4 3/16 in. (33.9 x 925 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.51,false,true,49003,Asian Art,Hanging scroll,牡丹に孔雀図|Peacocks and Peonies,Japan,Edo period (1615–1868),,,,Artist,,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,Japanese,1763,1840,1820,1810,1830,Hanging scroll; ink and color on silk,Image: 60 1/4 × 34 3/4 in. (153 × 88.2 cm) Overall with mounting: 79 15/16 × 41 3/4 in. (203 × 106 cm) Overall with knobs: 79 15/16 × 46 3/4 in. (203 × 118.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.113,false,true,45395,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,Japanese,1763,1840,1791,1791,1791,Hanging scroll; ink and color on paper,Image: 52 15/16 in. × 11 in. (134.5 × 27.9 cm) Overall with mounting: 81 7/8 × 16 3/16 in. (208 × 41.1 cm) Overall with knobs: 81 7/8 × 18 9/16 in. (208 × 47.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.86,false,true,39629,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,ca. 1795,1695,1895,Hanging scroll; ink and color on silk,34 3/4 x 10 1/2 in. (88.3 x 26.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.107,false,true,49024,Asian Art,Hanging scroll,観瀑山水図|Landscape with Waterfall,Japan,Edo period (1615–1868),,,,Artist,,Nakabayashi Chikutō,"Japanese, 1776–1853",,Nakabayashi Chikutō,Japanese,1776,1853,1841,1841,1841,Hanging scroll; ink on paper,Image: 61 15/16 x 34 3/16 in. (157.3 x 86.8 cm) Overall with mounting: 111 1/4 x 41 15/16 in. (282.5 x 106.5 cm) Overall with knobs: 111 1/4 x 45 1/16 in. (282.5 x 114.5 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.108,false,true,49026,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nakabayashi Chikutō,"Japanese, 1776–1853",,Nakabayashi Chikutō,Japanese,1776,1853,ca. 1840,1830,1850,Hanging scroll; ink on paper,51 3/4 x 23 11/16 in. (131.5 x 60.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.105,false,true,48998,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Okada Hankō,"Japanese, 1782–1846",,Okada Hankō,Japanese,1782,1846,1831,1831,1831,Handscroll; ink and color on paper,6 3/4 in. × 12 ft. 7 5/16 in. (17.2 × 384.3 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.247,false,true,44285,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Baiitsu,"Japanese, 1783–1856",,Yamamoto Baiitsu,Japanese,1783,1783,dated 1847,1847,1847,Handscroll; ink and color on silk,Image: 8 1/4 x 95 in. (21 x 241.3 cm),"Purchase, Friends of Asian Art Gifts, 1995",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.346,false,true,45430,Asian Art,Hanging scroll,三樹坡曉色图|View of the Kamo River from Sanbogi at Dawn,Japan,Edo period (1615–1868),,,,Artist,,Oda Kaisen,"Japanese, 1785–1862",,Oda Kaisen,Japanese,1785,1862,1829,1829,1829,Hanging scroll; ink and color on paper,Image: 26 x 9 1/8 in. (66 x 23.2 cm) Overall with mounting: 43 5/8 x 28 3/4 in. (110.8 x 73 cm) Overall with rollers: 32 1/2 in. (82.6 cm),"Purchase, Bequest of John L. Cadwalader, Gift of Mrs. Russell Sage, and Charles Sewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, by exchange, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.23,false,true,75265,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,1836,1836,1836,Hanging scroll: ink and color on silk,Image: 35 1/4 x 13 5/8 in. (89.5 x 34.6 cm) Overall with mounting: 67 3/4 x 17 1/4 in. (172.1 x 43.8 cm) Overall with knobs: 19 1/4 x 67 3/4 in. (48.9 x 172.1 cm),"Purchase, Friends of Asian Art Gifts, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.98,false,true,55295,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Yosai,"Japanese, 1788–1878",,Yosai,Japanese,1788,1878,dated 1867,1867,1867,Hanging scroll; ink and color on silk,46 5/8 x 15 1/2 in. (118.4 x 39.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.611,false,true,61278,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ōtagaki Rengetsu,"Japanese, 1791–1871",,Ōtagaki Rengetsu,Japanese,1791,1871,1868,1868,1868,Hanging scroll; ink and color on paper,11 3/8 x 49 1/8 in. (28.9 x 124.8 cm),"Gift of Professor Donald Keene, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/61278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.156.7,false,true,44854,Asian Art,Handscroll,,Japan,Edo period (1615–1868),,,,Artist,,Ukita Ikkei,"Japanese, 1795–1859",,Ukita Ikkei,Japanese,1795,1859,ca. 1858,1848,1859,Handscroll; ink and color on paper,11 3/4 in. x 25 ft. 6 in. (29.8 x 777.2 cm),"Rogers Fund, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.123,false,true,45332,Asian Art,Folding fan,墨竹扇面図|Bamboo and Rocks by a Stream,Japan,Edo period (1615–1868),,,,Artist,,Takaku Aigai,"Japanese, 1796–1843",,Takaku Aigai,Japanese,1796,1843,1832,1832,1832,"Folding fan; ink on paper, wood ribs",Overall: 11 5/16 x 18 in. (28.7 x 45.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.522.3,false,true,77202,Asian Art,Hanging scroll,芥子図|Poppies,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Kiitsu,"Japanese, 1796–1858",,Suzuki Kiitsu,Japanese,1796,1858,mid-19th century,1834,1866,Hanging scroll; ink and color on silk,Image: 38 9/16 x 13 1/8 in. (98 x 33.3 cm) Overall with knobs: 40 13/16 x 21 in. (103.7 x 53.3 cm) Overall with mounting: 40 13/16 x 18 11/16 in. (103.7 x 47.5 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.117,false,true,49010,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,dated Fall 1850,1850,1850,Hanging scroll; ink and color on paper,Image: 43 3/8 x 17 1/2 in. (110.2 x 44.4 cm) Overall: 75 3/8 x 30 3/8in. (191.5 x 77.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.120,false,true,49014,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,1854,1854,1854,Hanging scroll; ink and color on paper,51 1/8 x 11 5/16 in. (129.9 x 28.8 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.121,false,true,49016,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,1854,1854,1854,Hanging scroll; ink and color on paper,51 x 11 5/16 in. (129.5 x 28.8 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.112,false,true,57197,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Album leaf; lacquer on gold paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.113,false,true,57198,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Album leaf; lacquer on silver paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.70b,false,true,45803,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,ca. 1801–4,1791,1814,Hanging scroll; ink and color on silk,Image: 40 3/4 x 12 1/2 in. (103.5 x 31.8 cm) Overall with mounting: 69 11/16 x 19 1/2 in. (177 x 49.5 cm) with ivory rollers (dia. 15/16 in.),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.172,false,true,72722,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Takahashi Sōhei,"Japanese, 1804?–?1835",,Takahashi Sōhei,Japanese,1804,1835,1827,1827,1827,Hanging scroll; ink on paper,Image: 53 3/8 x 16 3/8 in. (135.5 x 41.6 cm) Overall with mounting: 83 15/16 x 21 9/16 in. (213.2 x 54.8 cm) Overall with rollers: 83 15/16 x 25 in. (213.2 x 63.5 cm),"Gift of Gitter-Yelen Collection, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.51,false,true,49000,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Takahashi Sōhei,"Japanese, 1804?–?1835",,Takahashi Sōhei,Japanese,1804,1835,1832,1700,1900,Hanging scroll; ink and color on paper,48 1/8 x 11 5/8 in. (122.2 x 29.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.570,false,true,65586,Asian Art,Hanging scroll,"伝俵屋宗達筆 源氏物語図「宿木」|Scene from ""The Ivy"" (Yadorigi), chapter 49 of the Tale of Genji",Japan,Edo period (1615–1868),,,,Artist,Studio of,Tawaraya Sōtatsu,"Japanese, died ca. 1640",,Tawaraya Sōtatsu,Japanese,1540,1640,early 17th century,1615,1633,"Hanging scroll; ink, color, and gold on paper",10 x 21 3/4 in. (25.4 x 55.2 cm),"Gift of Chizuko and Frank Korn, in honor of Miyeko Murase, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.32,false,true,45396,Asian Art,Hanging scroll,雪兎図|Painting the Eyes on a Snow Rabbit,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,ca. 1780,1770,1790,Hanging scroll; ink and color on silk,Image: 23 3/4 in. × 16 in. (60.3 × 40.6 cm) Overall with mounting: 62 1/4 in. × 23 in. (158.1 × 58.4 cm) Overall with knobs: 62 1/4 × 24 3/4 in. (158.1 × 62.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.525,false,true,76811,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Shunkōsai Hokushū,"Japanese, active 1808–32",,Shunkōsai Hokushū,Japanese,1808,1832,ca. 1812,1802,1822,Hanging scroll; ink and color on silk,Image: 26 x 13 1/2 in. (66 x 34.3 cm) Overall with mounting: 58 1/2 x 17 1/8 in. (148.6 x 43.5 cm),"Gift of Miki and Sebastian Izzard, in honor of James C. Y. Watt, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/76811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.522.1,false,true,77198,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Soga Nichokuan,"Japanese, active mid-17th century",,Soga Nichokuan,Japanese,1600,1700,early 17th century,1615,1633,Hanging scroll; ink on paper,Image: 45 3/16 x 20 3/16 in. (114.8 x 51.2 cm) Overall with mounting: 81 5/16 x 21 1/8 in. (206.5 x 53.7 cm) Overall with knobs: 22 13/16 in. (58 cm),"Fishbein-Bender Collection, Gift of T. Richard Fishbein and Estelle P. Bender, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/77198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.156.2,false,true,40450,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Unkoku Tōetsu,"Japanese, active second half 17th century",,Unkoku Tōetsu,Japanese,1650,1699,late 17th century,1667,1699,Hanging scroll; ink on paper,50 3/4 x 18 3/4 in. (128.9 x 47.6 cm),"Seymour Fund, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.30,false,true,45822,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,dated 1790,1790,1790,Hanging scroll; ink and color on silk,Image: 33 3/4 × 12 5/16 in. (85.8 × 31.3 cm) Overall with mounting: 67 5/16 × 12 5/16 in. (171 × 31.3 cm) Overall with knobs: 67 5/16 × 19 1/8 in. (171 × 48.5 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.34,false,true,48878,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,dated 1780,1780,1780,Hanging scroll; ink and color on silk,Image: 49 3/16 × 19 5/8 in. (125 × 49.8 cm) Overall with mounting: 90 3/16 × 28 1/16 in. (229 cm) Overall with knobs: 90 3/16 × 30 3/16 in. (229 × 76.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.36,false,true,45811,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,dated 1847,1847,1847,Hanging scroll; ink and color on silk,Image: 31 3/4 × 12 1/2 in. (80.7 × 31.7 cm) Overall with mounting: 64 7/16 × 17 9/16 in. (163.7 × 44.6 cm) Overall with knobs: 64 7/16 × 19 5/16 in. (163.7 × 49 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.37,false,true,45818,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,dated 1847,1847,1847,Hanging scroll; ink and color on silk,Image: 23 1/4 × 11 7/8 in. (59.1 × 30.2 cm) Overall with mounting: 61 × 19 1/2 in. (154.9 × 49.6 cm) Overall with knobs: 61 in. × 21 5/16 in. (154.9 × 54.1 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.44,false,true,48885,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,dated 1790,1615,1868,Hanging scroll; ink and color on silk,12 3/4 x 22 1/2 in. (32.4 x 57.2 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.56,false,true,45816,Asian Art,Hanging scroll,軍鶏図|Gamecocks,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,dated 1838,1838,1838,Hanging scroll; ink and color on silk,Image: 21 3/4 × 33 7/16 in. (55.3 × 85 cm) Overall with mounting: 59 7/16 × 40 3/16 in. (151 × 102 cm) Overall with knobs: 59 7/16 × 63 3/8 in. (151 × 160.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.57,false,true,45819,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1849,1615,1868,Hanging scroll; ink on paper,Image: 42 1/4 × 14 3/16 in. (107.3 × 36 cm) Overall with mounting: 72 5/8 × 18 1/2 in. (184.5 × 47 cm) Overall with knobs: 72 5/8 × 20 1/2 in. (184.5 × 52 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.151,false,true,44634,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1810,1800,1820,Fan mounted as hanging scroll; ink and color on paper,Image: 9 7/16 × 20 3/16 in. (24 × 51.3 cm) Overall with mounting: 54 3/4 × 28 3/4 in. (139 × 73 cm) Overall with knobs: 54 3/4 × 31 in. (139 × 78.7 cm),"Purchase, Friends of Asian Art Gifts, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.1403,false,true,48880,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,1839,1839,1839,Hanging scroll; ink and color on paper,Image: 11 3/16 × 27 9/16 in. (28.4 × 70 cm) Overall with mounting: 45 3/8 × 30 7/16 in. (115.3 × 77.3 cm) Overall with knobs: 45 3/8 × 30 13/16 in. (115.3 × 78.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.58.1–.25,false,true,50915,Asian Art,Album leaves,『画本葛飾振』|Picture Book in the Katsushika Style (Ehon Katsushika-buri),Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,ca. 1836,1826,1846,Album of twenty-five preparatory drawings (hanshita-e) for book illustrations; ink on paper,10 x 15 1/2 in. (25.4 x 39.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.517,false,true,54978,Asian Art,Hanging scroll,地獄太夫図|The Hell Courtesan,Japan,Edo period (1615–1868),,,,Artist,,Seikei,"Japanese, active second half of the 19th century",,Seikei,Japanese,1850,1899,late 19th century,1868,1899,Hanging scroll; ink and color on silk,Image: 39 1/8 × 20 1/8 in. (99.4 × 51.1 cm) Overall with mounting: 81 1/4 × 26 1/4 in. (206.4 × 66.7 cm) Overall with knobs: 26 1/4 × 29 in. (66.7 × 73.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.116,false,true,44614,Asian Art,Hanging scroll,法華経断簡|Segment of the Lotus Sutra (Hokekyō),Japan,Heian period (794–1185),,,,Artist,Attributed to,Kujō Kanezane,"Japanese, 1149–1207",,Kujo Kanezane,Japanese,1149,1207,12th century,1149,1185,"Hanging scroll; ink on colored paper decorated with cut gold (kirikane), sprinkled gold (sunago), and silver leaf",10 x 3 9/16 in. (25.4 x 9.1 cm),"Purchase, Friends of Asian Art Gifts, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.4,false,true,45616,Asian Art,Handscroll,宗観筆 九曜秘暦|The Secrets of the Nine Luminaries (Kuyō hiryaku),Japan,Heian period (794–1185),,,,Artist,,Sōkan,"Japanese, active late 11th–early 12th century",,Sōkan,Japanese,1050,1150,1125,1125,1125,Handscroll; ink and color on paper,11 1/4 in. x 26 ft. 6 1/4 in. (28.5 x 808.3 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6a,false,true,56616,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 6 3/8 x 19 1/2 in. (16.2 x 49.5 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/56616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6b,false,true,75156,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 7/16 x 20 5/8 in. (18.9 x 52.4 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6c,false,true,75157,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 1/2 x 20 5/8 in. (19.1 x 52.4 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6d,false,true,75158,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 x 20 7/8 in. (17.8 x 53 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6e,false,true,75159,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 1/2 x 20 5/8 in. (19.1 x 52.4 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6f,false,true,75160,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 9/16 x 20 3/4 in. (19.2 x 52.7 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6g,false,true,75161,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 7/16 x 20 3/4 in. (18.9 x 52.7 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6h,false,true,75162,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 6 7/8 x 20 1/8 in. (17.5 x 51.1 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6i,false,true,75163,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 1/4 x 20 1/8 in. (18.4 x 51.1 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6j,false,true,75164,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 1/2 x 20 7/8 in. (19.1 x 53 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6k,false,true,75165,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 1/2 x 20 1/2 in. (19.1 x 52.1 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.6l,false,true,75166,Asian Art,Fan mounted as an album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,late 19th century,1867,1891,Fan painting mounted as album leaf; tempera on paper,Overall: 13 x 23 5/8 in. (33 x 60 cm) Image: 7 x 20 1/4 in. (17.8 x 51.4 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.100,false,true,57195,Asian Art,Hanging scroll,旭日図|Rising Sun,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,second half of the 19th century,1850,1891,Hanging scroll; ink and color on silk,31 1/2 x 9 1/4 in. (80 x 23.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.101,false,true,57196,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,dated 1890,1890,1890,Mounted and hanging scroll; ink on paper,14 1/8 x 18 5/8 in. (35.9 x 57.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.102,false,true,57185,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.104,false,true,57187,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.105,false,true,57188,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.106,false,true,57189,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,"Album leaf; ink, color, and lacquer on paper",7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.107,false,true,57190,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.108,false,true,57191,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.109,false,true,57192,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.110,false,true,57193,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,1882,1882,1882,Lacquer and gold on paper,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.111,false,true,57194,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,dated 1881,1881,1881,Lacquer and mother-of-pearl fragments,7 1/2 x 6 1/2 in. (19.1 x 16.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.136,false,true,57161,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,dated 1879,1879,1879,"Hanging scroll; colored lacquer with mother-of-pearl, gold and ink on paper",11 3/8 x 16 1/8 in. (28.9 x 41 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.77,false,true,57180,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,Attributed to,Tazaki Soun,"Japanese, 1815–1898",,Tazaki Soun,Japanese,1815,1898,dated 1827,1827,1827,Hanging scroll; ink and color on silk,38 3/8 x 13 in. (97.5 x 33 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.26,false,true,57168,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Taki Katei,"Japanese, 1830–1901",,Taki Katei,Japanese,1830,1901,dated January 1896,1896,1896,Hanging scroll; ink and color on silk,43 1/16 x 16 3/8 in. (109.4 x 41.6 cm),"Gift of Daniel Slott, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.138,false,true,57238,Asian Art,Painting,,Japan,Meiji period (1868–1912),,,,Artist,Attributed to,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,late 19th century,1867,1889,Ink on paper,8 x 5 in. (20.3 x 12.7 cm),"Gift of F. Tikotin, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.70.4,false,true,50771,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,after 1888,1888,1912,Hanging scroll; ink and color on silk,38 7/8 x 14 1/4 in. (98.7 x 36.2 cm),"Gift of Edward M. Bratter, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.119.2,false,true,57229,Asian Art,Handscroll,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,"late 19th century, before 1870",1868,1869,Handscroll; ink on paper,17 ft 3 1/2 in. x 10 3/4 in. (516.9 x 27.3 cm),"Fletcher Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.119.5,false,true,57231,Asian Art,Wash drawing,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,late 19th century,1867,1889,On paper,9 1/8 x 13 1/4 in. (23.2 x 33.7 cm),"Fletcher Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.119.6,false,true,57233,Asian Art,Wash drawing,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,late 19th century,1867,1889,Ink on paper,6 x 10 1/4 in. (15.2 x 26 cm),"Fletcher Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.64a,false,true,54604,Asian Art,Hanging scroll,猿を襲う鷲図|Eagle Attacking a Monkey,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1885,1885,1885,Hanging scroll; ink and color on paper,Image: 65 1/2 x 33 in. (166.4 x 83.8 cm) Overall with mounting: 111 1/2 x 43 1/2 in. (283.2 x 110.5 cm) Overall with knobs: 111 1/2 x 47 1/2 in. (283.2 x 120.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.64b,false,true,54605,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1885,1885,1885,Hanging scroll; ink and color on paper,Image: 65 1/2 x 33 in. (166.4 x 83.8 cm) Overall with mounting: 111 1/2 x 43 1/2 in. (283.2 x 110.5 cm) Overall with knobs: 111 1/2 x 47 1/2 in. (283.2 x 120.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.64c,false,true,54606,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1885,1885,1885,Hanging scroll; ink and color on paper,Image: 65 1/2 x 33 in. (166.4 x 83.8 cm) Overall with mounting: 111 1/2 x 43 1/2 in. (283.2 x 110.5 cm) Overall with knobs: 111 1/2 x 43 1/2 in. (283.2 x 110.5 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.64d,false,true,54607,Asian Art,Hanging scroll,兎を追う鷲図|Eagle Pursuing Rabbit,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1885,1885,1885,Hanging scroll; ink and color on paper,Image: 65 1/2 x 33 in. (166.4 x 83.8 cm) Overall with mounting: 111 1/2 x 43 1/2 in. (283.2 x 110.5 cm) Overall with knobs: 111 1/2 x 47 1/2 in. (283.2 x 120.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.44,false,true,55378,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,late 19th century,1868,1889,Hanging scroll; ink on silk,49 3/8 x 19 1/2 in. (125.4 x 49.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.1,false,true,54608,Asian Art,Album leaf,白衣観音図|White-Robed Kannon,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 3/8 x 11 in. (36.5 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.2,false,true,54609,Asian Art,Album leaf,松に鴉図|Two Crows on a Pine Branch,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 5/16 x 10 3/4 in. (36.4 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.3,false,true,54610,Asian Art,Album leaf,富士図|Mount Fuji,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/8 x 10 3/4 in. (35.9 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.4,false,true,54611,Asian Art,Album leaf,旭に群鴉図|Flock of Crows at Dawn,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 3/8 x 10 1/4 in. (36.5 x 26.0 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.5,false,true,54612,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.6,false,true,54613,Asian Art,Album leaf,滝に燕図|Swallows by a Waterfall,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 5/16 x 10 1/2 in. (36.4 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.7,false,true,54614,Asian Art,Album leaf,水辺に鴉図|Crow and Reeds by a Stream,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.8,false,true,54615,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 5/8 in. (36.2 x 27 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.9,false,true,54616,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,March 1888,1888,1888,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.10,false,true,54617,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/8 x 10 3/4 in. (35.9 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.11,false,true,54618,Asian Art,Album leaf,竹に鴉図|Crow on a Bamboo Branch,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.12,false,true,54619,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.13,false,true,54620,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.14,false,true,54621,Asian Art,Album leaf,木に鴉図|Crow on a Branch,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.15,false,true,54622,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 11 1/4 in. (36.2 x 28.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.16,false,true,45526,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.17,false,true,54623,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.18,false,true,54624,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/8 in. (36.2 x 26.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.19,false,true,54625,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.20,false,true,54626,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/8 x 10 5/8 in. (35.9 x 27 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.21,false,true,54627,Asian Art,Album leaf,岩に鴉図|Crow on a Rock,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.22,false,true,54628,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.23,false,true,54629,Asian Art,Album leaf,瓜に鼠図|Mice in a Melon,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.24,false,true,54630,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.25,false,true,54631,Asian Art,Album leaf,月に鴉図|Crow and the Moon,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.26,false,true,54632,Asian Art,Album leaf,木に鴉図|Crow on a Branch,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.27,false,true,54633,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and gold on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.28,false,true,54634,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and red on silk,14 1/8 x 10 3/4 in. (35.9 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.29,false,true,54635,Asian Art,Album leaf,ムクドリ図|Starlings on a Branch,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.30,false,true,54636,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.31,false,true,54637,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/2 x 10 1/2 in. (36.8 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.32,false,true,54638,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.33,false,true,54639,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.34,false,true,54640,Asian Art,Album leaf,雪中鴉図|Crow Flying in the Snow,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.35,false,true,54641,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.36,false,true,54642,Asian Art,Album leaf,柳に鴉図|Crow and Willow Tree,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,November 1887,1887,1887,Album leaf; ink and color on silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.40,false,true,54646,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Gahō,"Japanese, 1835–1908",,Hashimoto Gahō,Japanese,1835,1908,ca. 1885–89,1885,1889,Album leaf; ink and color on silk,Image: 14 1/4 × 10 1/2 in. (36.2 × 26.7 cm) Mat: 22 7/8 × 15 1/2 in. (58.1 × 39.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.47,false,true,54677,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Gahō,"Japanese, 1835–1908",,Hashimoto Gahō,Japanese,1835,1908,ca. 1885–89,1885,1889,Album leaf; ink and color on silk,Image: 14 1/4 × 10 3/8 in. (36.2 × 26.4 cm) Mat: 22 7/8 × 15 1/2 in. (58.1 × 39.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.55,false,true,54696,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Gahō,"Japanese, 1835–1908",,Hashimoto Gahō,Japanese,1835,1908,ca. 1885–89,1885,1889,Album leaf; ink and color on silk,14 x 10 in. (35.6 x 25.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.59,false,true,54701,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Gahō,"Japanese, 1835–1908",,Hashimoto Gahō,Japanese,1835,1908,ca. 1885–89,1885,1889,Album leaf; ink and color on silk,Image: 14 in. × 10 1/2 in. (35.6 × 26.7 cm) Mat: 22 7/8 × 15 1/2 in. (58.1 × 39.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.73,false,true,50826,Asian Art,Album leaf,猫に蜘蛛図|Cat Watching a Spider,Japan,Meiji period (1868–1912),,,,Artist,,Ōide Tōkō,"Japanese, 1841–1905",,Ōide Tōkō,Japanese,1841,1905,ca. 1888–92,1888,1892,Album leaf; ink and color on silk,14 3/4 x 11 in. (37.5 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.10,false,true,54476,Asian Art,Panel,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,20th century,1900,1912,Ink and color on silk,51 7/8 x 21 5/8 in. (131.8 x 54.9 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.11,false,true,54477,Asian Art,Panel,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,20th century,1900,1912,Ink and color on silk,50 7/16 x 19 11/16 in. (128.1 x 50 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.65,false,true,54708,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1800,1800,1800,Album leaf; ink and color on silk,14 1/2 x 11 1/8 in. (36.8 x 28.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.66,false,true,54709,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1800,1800,1800,Album leaf; ink and color on silk,14 1/2 x 11 in. (36.8 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.67,false,true,54710,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1800,1800,1800,Album leaf; ink and color on silk,14 1/2 x 11 1/2 in. (36.8 x 29.2 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.68,false,true,54711,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1868,1868,1868,Album leaf; ink and color on silk,14 1/2 x 11 in. (36.8 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.85,false,true,54745,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1887–92,1887,1892,Album leaf; ink on silk,13 5/8 x 10 7/8 in. (34.6 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.86,false,true,54746,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1887–92,1887,1892,Album leaf; silk,13 1/2 x 11 in. (34.3 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.87,false,true,54747,Asian Art,Album leaf,猫図|Cat Seen from Behind,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1868,1868,1868,Album leaf; ink and color on silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.88,false,true,54748,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1887–92,1887,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.89,false,true,54749,Asian Art,Album leaf,狗児図|A Pair of Puppies,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1868,1868,1868,Album leaf; ink on silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.90,false,true,54750,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1887–92,1887,1892,Album leaf; ink on silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.91,false,true,54751,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1887–92,1887,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.92,false,true,54752,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawabata Gyokushō,"Japanese, 1842–1913",,Kawabata Gyokushō,Japanese,1842,1913,1887–92,1887,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.37,false,true,54643,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 7/8 in. (36.2 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.38,false,true,54644,Asian Art,Album leaf,桜に小禽図|Birds on a Flowering Branch,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.39,false,true,54645,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.41,false,true,54671,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 3/16 x 10 7/8 in. (36.0 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.42,false,true,54672,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.43,false,true,54673,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/8 in. (36.2 x 26.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.44,false,true,54674,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/8 x 10 3/4 in. (35.9 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.45,false,true,54675,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/8 x 10 3/4 in. (35.9 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.46,false,true,54676,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/8 x 10 3/4 in. (35.9 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.48,false,true,54684,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 1/4 x 10 3/4 in. (36.2 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.52,false,true,54692,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 x 10 3/4 in. (35.6 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.53,false,true,54694,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 x 10 1/2 in. (35.6 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.54,false,true,54695,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 x 10 1/2 in. (35.6 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.56,false,true,54698,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 x 10 3/4 in. (35.6 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.57,false,true,54699,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 x 10 3/4 in. (35.6 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.58,false,true,54700,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 x 10 3/4 in. (35.6 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.60,false,true,54703,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,14 x 10 3/4 in. (35.6 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.75,false,true,54735,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,13 7/8 x 10 1/2 in. (35.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.76,false,true,54736,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,13 7/8 x 10 1/2 in. (35.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.79,false,true,54739,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,13 7/8 x 10 1/2 in. (35.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.80,false,true,54740,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,ca. 1887,1877,1897,Album leaf; ink and color on silk,13 7/8 x 10 3/8 in. (35.2 x 26.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.70,false,true,54714,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,"Summer, 1891",1891,1891,Album leaf; ink and color on silk,14 3/4 x 11 1/4 in. (37.5 x 28.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.74,false,true,54734,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; ink and color on silk,14 3/4 x 11 in. (37.5 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.77,false,true,54737,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,1892,1892,1892,Album leaf; ink and color on silk,13 7/8 x 10 1/2 in. (35.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.78,false,true,54738,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,1892,1892,1892,Album leaf; silk,13 7/8 x 10 1/2 in. (35.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.81,false,true,54741,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,14 1/4 x 10 5/8 in. (36.2 x 27.0 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.82,false,true,54742,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; ink and color silk,14 1/4 x 10 1/2 in. (36.2 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.83,false,true,54743,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; ink and color silk,14 1/4 x 10 5/8 in. (36.2 x 27.0 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.84,false,true,54744,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; ink and color silk,14 1/4 x 10 5/8 in. (36.2 x 27.0 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.93,false,true,54753,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 5/8 x 10 3/4 in. (34.6 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.94,false,true,54754,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.95,false,true,54755,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.96,false,true,54756,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 5/8 x 10 7/8 in. (34.6 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.97,false,true,54757,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.98,false,true,54758,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 1/2 x 10 3/4 in. (34.3 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.99,false,true,54759,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.100,false,true,54760,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Seki Shūkō,"Japanese, 1858–1915",,Seki Shūkō,Japanese,1858,1915,ca. 1890–92,1890,1892,Album leaf; silk,13 1/2 x 10 7/8 in. (34.3 x 27.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.69,false,true,54712,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Okada Baison,"Japanese, 1864–1913",,Okada Baison,Japanese,1864,1913,ca. 1891–92,1891,1892,Album leaf; ink and color on silk,14 3/4 x 11 1/2 in. (37.5 x 29.2 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.71,false,true,54732,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Okada Baison,"Japanese, 1864–1913",,Okada Baison,Japanese,1864,1913,ca. 1891–92,1891,1892,Album leaf; ink and color on silk,14 5/8 x 11 in. (37.1 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.72,false,true,54733,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Okada Baison,"Japanese, 1864–1913",,Okada Baison,Japanese,1864,1913,ca. 1891–92,1891,1892,Album leaf; ink and color on silk,14 5/8 x 11 in. (37.1 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.510,false,true,75612,Asian Art,Hanging scroll,,Japan,Taishō period (1912–26),,,,Artist,,Fukuda Kodōjin,"Japanese, 1865–1944",,Fukuda Kodōjin,Japanese,1865,1944,1922,1922,1922,Hanging scroll; ink on paper,Image: 57 3/4 x 12 1/4 in. (146.7 x 31.1 cm) Overall with mounting: 81 x 17 5/16 in. (205.7 x 44 cm) Overall with knobs: 81 x 19 1/4 in. (205.7 x 48.9 cm),"Gift of the Gitter-Yelen Collection, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/75612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.7.3,false,true,44849,Asian Art,Handscroll,"『妙法蓮華経』「観世音菩薩普門品」|“Universal Gateway,” Chapter 25 of the Lotus Sutra",Japan,Kamakura period (1185–1333),,,,Artist,Calligrapher:,Sugawara Mitsushige,"Japanese, active mid- 13th century",,Sugawara Mitsushige,Japanese,1234,1266,dated 1257,1257,1257,"Handscroll; ink, color, and gold on paper",Overall with mounting: 9 11/16 in. × 30 ft. 8 1/16 in. (24.6 × 934.9 cm),"Purchase, Louisa Eldridge McBurney Gift, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.29,false,true,45372,Asian Art,Hanging scroll,玄奘三蔵像|Portrait of Xuanzang (Genjō) with Attendant,Japan,Kamakura period (1185–1333),,,,Artist,In the Style of,Kasuga Motomitsu,"Japanese, active early 11th century",,Kasuga Motomitsu,Japanese,1000,1099,14th century,1300,1333,Hanging scroll; ink and color on silk,Image: 48 3/4 x 29 1/4 in. (123.8 x 74.3 cm) Overall with mounting: 87 1/2 x 37 1/2 in. (222.3 x 95.3 cm) Overall with knobs: 87 1/2 x 39 1/2 in. (222.3 x 100.3 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.29,false,true,73645,Asian Art,Hanging scroll,芦葉達磨図|Bodhidharma Crossing the Yangzi River on a Reed,Japan,Momoyama period (1573–1615),,,,Artist,,Kano Sōshū,"Japanese, 1551–1601",,Kano Sōshū,Japanese,1551,1601,late 16th century,1567,1599,Hanging scroll; ink on paper,Image: 31 3/4 x 12 3/4 in. (80.6 x 32.4 cm) Overall with mounting: 64 x 17 1/2 in. (162.6 x 44.5 cm) Overall with knobs: 64 x 19 1/4 in. (162.6 x 48.9 cm),"Purchase, Friends of Asian Art Gifts, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.11,false,true,54576,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Sesshū Tōyō,"Japanese, 1420–1506",,Sesshū Tōyō,Japanese,1420,1506,1480,1334,1573,Hanging scroll; ink on paper,39 1/4 x 16 in. (99.7 x 40.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.3,false,true,63956,Asian Art,Hanging scroll,張騫図|The Chinese Explorer Zhang Qian on a Raft,Japan,Muromachi period (1392–1573),,,,Artist,,Maejima Sōyū,active mid-16th century,,Maejima Sōyū,Japanese,1536,1570,mid-16th century,1534,1566,Hanging scroll; ink on paper,20 5/16 x 13 11/16 in. (51.6 x 34.7cm),"Purchase, Mary Livingston Griggs and Mary Griggs Burke Foundation Gift, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/63956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.123.1,false,true,45643,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Shubun,"Japanese, active ca. 1414",,Shubun,Japanese,1414,1414,first half of the 15th century,1400,1424,Hanging scroll; ink and color on paper,37 1/8 x 14 7/16 in. (94.3 x 36.7 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.123.2,false,true,45644,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Shubun,"Japanese, active ca. 1414",,Shubun,Japanese,1414,1414,first half of the 15th century,1400,1424,Hanging scroll; ink and color on paper,36 3/8 x 14 7/16 in. (92.4 x 36.7 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.385,false,true,60786,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,,Kano Yukinobu,"Japanese, ca. 1513–1575",,Kano Yukinobu,Japanese,1513,1575,mid-16th century,1534,1566,Hanging scroll; ink and color on paper,18 3/8 x 15 3/4 in. (46.7 x 40 cm) Overall with mounting: 52 5/8 x 20 3/8 in. (133.7 x 51.8 cm),"Purchase, Friends of Asian Art Gifts, in honor of Douglas Dillon, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/60786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.38,false,true,44857,Asian Art,Hanging scroll,蘭蕙同芳図|Orchids and Rock,Japan,Muromachi period (1392–1573),,,,Artist,,Gyokuen Bonpō,"Japanese, ca. 1348–after 1420",,Gyokuen Bonpo,Japanese,1348,1420,late 14th–early 15th century,1367,1433,Hanging scroll; ink on paper,39 9/16 x 13 1/8 in. (100.5 x 33.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.514,false,true,45326,Asian Art,Hanging scroll,ガマズミに山鵲図|Magpie on Viburnum Branch,Japan,Muromachi period (1392–1573),,,,Artist,,Genga,"Japanese, active early 16th century",", in Song tradition",Genga,Japanese,1500,1550,early 16th century,1500,1533,Hanging scroll; ink and color on paper,Image: 18 1/4 in. × 14 in. (46.3 × 35.5 cm) Overall with mounting: 53 1/2 × 19 1/2 in. (135.9 × 49.5 cm) Overall with knobs: 53 1/2 × 21 1/2 in. (135.9 × 54.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.172,false,true,44856,Asian Art,Hanging scroll,芦雁図|Reeds and Geese,Japan,Nanbokuchō period (1336–92),,,,Artist,,Tesshū Tokusai,"Japanese, died 1366",,Tesshū Tokusai,Japanese,1342,1366,"dated 11th month, 1343",1343,1343,One of a pair of hanging scrolls; ink on silk,Image: 43 7/16 x 17 5/16 in. (110.4 x 44 cm),"Purchase, Mrs. Jackson Burke Gift, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.37,false,true,44855,Asian Art,Hanging scroll,芦雁図|Reeds and Geese,Japan,Nanbokuchō period (1336–92),,,,Artist,,Tesshū Tokusai,"Japanese, died 1366",,Tesshū Tokusai,Japanese,1342,1366,"dated 11th month, 1343",1343,1343,One of a pair of hanging scrolls; ink on silk,Image: 43 1/2 x 17 3/8 in. (110.5 x 44.1 cm) Overall: 71 3/4 x 20 1/4 in. (182.2 x 51.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.508,false,true,45630,Asian Art,Hanging scroll,,Japan,Nanbokuchō period (1336–92),,,,Artist,In the Style of,Toba Sōjō,"Japanese, 1053–1140",,Toba Sōjō,Japanese,1053,1140,14th century,1336,1392,"Hanging scroll; ink, color, and gold on silk",Image: 32 1/2 × 20 9/16 in. (82.6 × 52.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.25,false,true,45606,Asian Art,Hanging scroll,衿羯羅童子像 |Kongara-doji,Japan,Nanbokuchō period (1336–92),,,,Artist,,Ryūshū Shūtaku (Myōtaku),"Japanese, 1307–1388",,Ryūshū Shūtaku,Japanese,1307,1388,1387,1387,1387,Hanging scroll; ink and color on silk,43 x 15 3/4 in. (109.2 x 40 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.476,false,true,45230,Asian Art,Hanging scroll,,Japan,Meiji (1868–1912)–Taishō (1912–26) period,,,,Artist,,Tomioka Tessai,"Japanese, 1836–1924",,Tomioka Tessai,Japanese,1836,1924,19th–20th century,1868,1924,Hanging scroll; ink on paper,20 1/4 x 13 1/4 in. (51.4 x 33.7 cm),"Gift of Dr. Yukikazu Iwasa, in honor of Shizuko Iwasa, 1993",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.1,false,true,57327,Asian Art,Hanging scroll,,Japan,Late Edo (1615–1868) or Meiji (1868–1912) period,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,second half of the 19th century,1850,1891,Hanging scroll; ink and color on silk,Image: 35 1/2 x 13 in. (90.2 x 33 cm) Overall with knobs: 70 1/4 x 19 3/4 in. (178.4 x 50.2 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.69,false,true,40009,Asian Art,Figure,羅漢像|Rakan,Japan,Edo period (1615–1868),,,,Artist,,Shōun Genkei,"Japanese, 1648–1710",,Shōun Genkei,Japanese,1648,1710,1688–95,1688,1695,"One of a set of five hundred; wood with lacquer, gold leaf, and paint",H. 33 1/2 in. (85.1 cm); W. 28 3/4 in. (73 cm); D. 26 1/4 in. (66.7 cm),"Fletcher Fund, 1927",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/40009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"57.157.1, .2",false,true,45693,Asian Art,Pair of folding screens,伝狩野山楽筆 粟に小禽図屏風|Autumn Millet and Small Birds,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Sanraku,"Japanese, 1559–1635",,Kano Sanraku,Japanese,1559,1635,,1559,1635,"Pair of eight-panel foldingscreens; ink, color, and gold on gilt paper",Image (each screen): 33 1/2 x 134 1/2 in. (85.1 x 341.6 cm),"Purchase, Joseph Pulitzer Bequest, 1957",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB149,false,true,57840,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,After,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,,1615,1868,Monochrome woodblock print; ink on paper,10 x 7 1/2 in. (25.4 x 19.1 cm),"Bequest of W. Gedney Beatty, 1941",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.453,false,true,55450,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Kano Yasunobu,"Japanese, 1613–1685",,Kano Yasunobu,Japanese,1613,1685,,1615,1868,"Six-panel screen; ink, color, and gold on gilded paper",Image: 69 3/8 x 146 3/8 in. (176.2 x 371.8 cm),"Gift of James L. Greenfield, in memory of Margaret Greenfield, 2000",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/55450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.35.1,false,true,57344,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,School of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,,1615,1868,Six-panel folding screen; color on paper,67 x 12 ft. 7 in. (170.2 x 383.5 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1949",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/57344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.784,false,true,58693,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,,1615,1868,"Ceramic, mother-of-pearl, pewter on brown lacquer with sprinkled gold Ojime: bead; agate Netsuke: ivory and wood",2 15/16 x 2 1/8 x 7/8 in. (7.5 x 5.4 x 2.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.671,false,true,47413,Asian Art,Figure,,Japan,Edo period (1615–1868),,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,,1615,1868,Lacquered pottery,H. 8 3/4 in. (22.2 cm); W. 9 1/2 in. (24.1 cm); D. 6 1/2 in. (16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/47413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.672,false,true,47405,Asian Art,Figure,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,,1615,1868,"Ceramic body covered in lacquer, gold foil and covered in lacquer again",H. 9 in. (22.9 cm); W. 9 in. (22.9 cm); D. 7 in. (17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/47405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.161a, b",false,true,56165,Asian Art,Tea caddy,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,,1663,1747,Lacquer,H. 3 1/8 in. (7.9 cm); Diam. 2 5/8 in. (6.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/56165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.104,false,true,39581,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Soga Shōhaku,"Japanese, 1730–1781",,Soga Shōhaku,Japanese,1730,1781,,1615,1868,Two-panel folding screen; ink and gold paint on paper,Image: 61 3/4 x 68 3/8 in. (156.8 x 173.7 cm),"Purchase, Barbara and William Karatz Gift and Rogers Fund, 1996",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/39581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.778,false,true,78680,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,,1764,1824,Woodblock printed book; ink and color on paper,9 3/4 × 6 7/8 in. (24.7 × 17.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.511.1, .2",false,true,75552,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Maruyama Ōshin,"Japanese, 1790–1838",,Maruyama Ōshin,Japanese,1790,1838,,1790,1838,"Pair of six-panel folding screens; ink, color, and gold on paper",Image (each screen): 32 5/16 x 103 3/16 in. (82 x 262.1 cm),"Gift of the Gitter-Yelen Foundation, 2009",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/75552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.3,false,true,57681,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1615,1868,Woodblock printed book; ink and color on paper,Overall: 9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.6,false,true,57683,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1615,1868,Woodblock printed book; ink and color on paper,Overall: 9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB81.11,false,true,57688,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1615,1868,Woodblock printed book; ink and color on paper,Overall: 9 × 6 1/4 × 3/8 in. (22.9 × 15.9 × 1 cm),"Rogers Fund, 1931",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB111a–k,false,true,57804,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1615,1868,Eleven volumes of Woodblock printed books; ink and color on paper,Overall (each volume): 8 15/16 x 6 1/4 x 1/2 in. (22.7 x 15.8 x 1.3 cm) Image: 7 x 4 7/8 in. (17.8 x 12.4 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB52,false,true,57652,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1615,1868,Ink on paper,4 1/2 × 6 1/2 × 2 3/8 in. (11.4 × 16.5 × 6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.69,false,true,57975,Asian Art,Panel,,Japan,late Edo period (1615–1868)–early Meiji period (1868–1912) ?,,,,Artist,Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,,1615,1912,Lacquer,L. 67 in. (170.2 cm); W. 8 1/2 in. (21.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/57975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.70,false,true,50225,Asian Art,Panel,,Japan,late Edo period (1615–1868)–early Meiji period (1868–1912) ?,,,,Artist,Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,,1615,1912,Lacquer,W. 5 3/8 in. (13.7 cm); L. 64 1/2 in. (163.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/50225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3156,false,true,56709,Asian Art,Print,,Japan,,,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,,1664,1729,Polychrome woodblock print; ink and color on paper,5 3/4 x 12 1/2 in. (14.6 x 31.8 cm),"Gift of Mrs. Francis Ormond, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2346,false,true,54131,Asian Art,Print,,Japan,,,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,,1700,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 7/16 x 5 7/16 in. (21.4 x 13.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3285,false,true,55372,Asian Art,Print,,Japan,,,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,,1790,1848,Polychrome woodblock print; ink and color on paper,Overall: 8 11/16 x 11 15/16 in. (22.1 x 30.3 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3176,false,true,56726,Asian Art,Print,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1891,Polychrome woodblock print; ink and color on paper,Image: 7 1/2 × 10 1/8 in. (19.1 × 25.7 cm) Mat: 15 1/4 × 22 3/4 in. (38.7 × 57.8 cm),"Gift of Roland Koscherak, 1957",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3153,false,true,56706,Asian Art,Print,,Japan,,,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,,1831,1899,Polychrome woodblock print; ink and color on paper,9 7/8 x 14 1/8 in. (25.1 x 35.9 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3133,false,true,56681,Asian Art,Woodblock print,,Japan,,,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,,1831,1889,Triptych of polychrome woodblock prints; ink and color on paper,14 1/4 x 29 5/8 in. (36.2 x 75.2 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2297,false,true,54070,Asian Art,Print,,Japan,,,,,Artist,,Ryūgetsusai Shinkō,"Japanese, active 1810s",,Ryūgetsusai Shinkō,Japanese,1810,1819,,1700,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 3/16 x 7 1/8 in. (13.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3397,false,true,55575,Asian Art,Print,,Japan,,,,,Artist,Attributed to,Yumiaki Toriyama,"Japanese, active ca. 1800",,Yumiaki Toriyama,Japanese,1790,1810,,1790,1810,Polychrome woodblock print; ink and color on paper,26 3/4 x 4 3/4 in. (67.9 x 12.1 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2367,false,true,54151,Asian Art,Print,,Japan,,,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Part of an album of woodblock prints (surimono); ink and color on paper,7 1/2 x 10 1/4 in. (19.1 x 26 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2929,false,true,56144,Asian Art,Woodblock print,"諸國瀧廻 東都葵ヶ岡の瀧|The Falls at Aoigaoka in the Eastern Capital (Tōto Aoigaoka no taki), from the series A Tour of Waterfalls in Various Provinces (Shokoku taki meguri)",Japan,,,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 1/4 in. (37.1 x 26 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3146,false,true,56697,Asian Art,Print,冨嶽三十六景 甲州犬目峠|Fuji from Inume (?) Pass,Japan,,,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,14 1/4 x 9 1/4 in. (36.2 x 23.5 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3175,false,true,54227,Asian Art,Print,,Japan,,,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,Image: 7 5/8 × 21 1/16 in. (19.4 × 53.5 cm) Mat: 12 1/2 in. × 37 in. (31.8 × 94 cm),"Gift of Mrs. Henry L. Phillips, in memory of her husband, Henry L. Phillips, 1957",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1620,false,true,55786,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shigenaga,1697–1756,,Shigenaga,Japanese,1697,1756,,1697,1756,Polychrome woodblock print; ink and color on paper,H. 11 1/4 in. (28.6 cm); W. 5 7/8 in. (14.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1374,false,true,55355,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Komatsuken,1710–1792,,Komatsuken,Japanese,1710,1792,,1710,1792,Polychrome woodblock print; ink and color on paper,H. 10 9/16 in. (26.8 cm); W. 8 7/16 in. (21.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1790,false,true,56092,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōkōsai Eishō,"Japanese, 1793–99",,Chōkōsai Eishō,Japanese,1793,1799,,1792,1801,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 10 1/8 in. (25.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1791,false,true,56093,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōkōsai Eishō,"Japanese, 1793–99",,Chōkōsai Eishō,Japanese,1793,1799,,1792,1801,Monochrome woodblock print; ink on paper,H. 10 5/8 in. (27 cm); W. 7 1/2 in. (19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3064,false,true,56541,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōkōsai Eishō,"Japanese, 1793–99",,Chōkōsai Eishō,Japanese,1793,1799,,1615,1868,Polychrome woodblock print; ink and color on paper,13 x 8 3/4 in. (33 x 22.2 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1591,false,true,55755,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1625,1694,Monochrome woodblock print; ink on paper,H. 6 1/2 in. (16.5 cm); W. 6 7/16 in. (16.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1592,false,true,55756,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1625,1694,Monochrome woodblock print; ink on paper,H. 6 5/16 in. (16 cm); W. 6 7/16 in. (16.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1593,false,true,55757,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1625,1694,Monochrome woodblock print; ink on paper,H. 5 1/8 in. (13 cm); 8 13/16 in. (22.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3048,false,true,56503,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1615,1868,Monochrome woodblock print; ink on paper,9 3/4 x 13 1/4 in. (24.8 x 33.7 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3049,false,true,56504,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1615,1868,Monochrome woodblock print; ink on paper,9 5/8 x 13 in. (24.4 x 33 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3066,false,true,56546,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1615,1868,Monochrome woodblock print; ink on paper,10 1/4 x 16 3/4 in. (26 x 42.5 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3067,false,true,56548,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1615,1868,Monochrome woodblock print; ink on paper,10 1/2 x 16 1/4 in. (26.7 x 41.3 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3068,false,true,56549,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1615,1868,Monochrome woodblock print; ink on paper,10 1/2 x 16 in. (26.7 x 40.6 cm),"Harris Brisbane Dick Fund and Rogers Fund, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1691,false,true,55926,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Kikumaro,"Japanese, died 1830",,Kitagawa Kikumaro,Japanese,,1830,,1789,1829,Polychrome woodblock print; ink and color on paper,H. 13 1/4 in. (33.7 cm); W. 8 3/4 in. (22.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1692,false,true,55927,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Kikumaro,"Japanese, died 1830",,Kitagawa Kikumaro,Japanese,,1830,,1789,1829,Polychrome woodblock print; ink and color on paper,H. 23 1/8 in. (58.7 cm); W. 4 1/4 in. (10.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1089,false,true,55026,Asian Art,Print,青楼美人 六花撰 岡本屋内 重岡|A Courtesan with Morning-glories on the Background,Japan,Edo period (1615–1868),,,,Artist,,Utamaro II,Japanese (died 1831?),,Utamaro II,Japanese,1750,1850,,1615,1831,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 9 3/8 in. (23.8),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1090,false,true,55027,Asian Art,Print,青楼美人 六花撰 扇屋内花扇|A Courtesan with Wisteria on the Background,Japan,Edo period (1615–1868),,,,Artist,,Utamaro II,Japanese (died 1831?),,Utamaro II,Japanese,1750,1850,,1615,1831,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 9 1/4 in. (23.5 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3394,false,true,55570,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utamaro II,Japanese (died 1831?),,Utamaro II,Japanese,1750,1850,,1804,1817,Triptych of polychrome woodblock prints; ink and color on paper,15 x 30 in. (38.1 x 76.2 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1828,false,true,56114,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,,1658,1716,Polychrome woodblock print (leaf from an album); ink and color on paper,H. 10 in. (25.4 cm); W. 14 3/16 in. (36 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1829,false,true,56115,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,,1658,1716,Polychrome woodblock print (album leaf); ink and color on paper,H. 10 in. (25.4 cm); W. 14 7/16 in. (36.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1597,false,true,55761,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,,1664,1729,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 6 in. (15.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1598,false,true,55762,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonobu,"Japanese, 1664–1729",,Torii Kiyonobu,Japanese,1664,1729,,1664,1729,Polychrome woodblock print; ink and color on paper,H. 11 3/4 in. (29.8 cm); W. 5 3/4 in. (14.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1584,false,true,55749,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,,1671,1751,Monochrome woodblock print; ink on paper,H. 10 3/16 in. (25.9 cm); W. 14 1/2 in. (36.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1585,false,true,55750,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,,1671,1751,Monochrome woodblock print; ink on paper,H. 10 3/16 in. (25.9 cm); W. 14 1/2 in. (36.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3050,false,true,56505,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,,1615,1868,Monochrome woodblock print; ink on paper,9 1/4 x 12 3/4 in. (23.5 x 32.4 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3051,false,true,56506,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,,1615,1868,Monochrome woodblock print; ink on paper,9 1/2 x 12 3/4 in. (24.1 x 32.4 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3065,false,true,56544,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,,1615,1868,Polychrome woodblock print (hand-colored); ink and color on paper,8 3/4 x 6 1/2 in. (22.2 x 16.5 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3405,false,true,55592,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,,1671,1751,Monochrome woodblock print; ink on paper,6 1/2 x 5 in. (16.5 x 12.7 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1586,false,true,55751,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1691,1768,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 17 in. (43.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1587,false,true,55752,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1691,1768,Undivided triptych of polychrome woodblock prints; ink and color on paper,H. 11 3/4 in. (29.8 cm); W. 16 15/16 in. (43 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1588,false,true,55753,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1691,1768,Undivided triptych of polychrome woodblock prints; ink and color on paper,H. 11 7/8 in. (30.2 cm); W. 17 3/8 in. (44.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1595,false,true,55759,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1691,1768,Monochrome woodblock print; ink on paper,H. 11 1/2 in. (29.2 cm); W. 15 3/4 in. (40 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1596,false,true,55760,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1691,1768,Monochrome woodblock print; ink on paper,H. 11 1/2 in. (29.2 cm); W. 15 3/4 in. (40 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3059,false,true,56534,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1615,1868,Polychrome woodblock print; ink and color on paper,12 x 7 in. (30.5 x 17.8 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3411,false,true,55600,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1686,1764,Monochrome woodblock print; ink on paper,11 x 12 1/4 in. (27.9 x 31.1 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1600,false,true,55764,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu II,"Japanese, 1706–1763",,Torii Kiyomasu II,Japanese,1706,1763,,1706,1763,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 5 3/4 in. (14.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3396,false,true,55573,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu II,"Japanese, 1706–1763",,Torii Kiyomasu II,Japanese,1706,1763,,1706,1763,Monochrome woodblock print (probably hand colored); ink and color on paper,12 x 6 1/4 in. (30.5 x 15.9 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1225,false,true,55147,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,,1711,1785,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 10 5/16 in. (26.2 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1611,false,true,55771,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,,1711,1785,Polychrome woodblock print; ink and color on paper,H. 11 3/8 (28.9 cm); W. 5 5/8 in. (14.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1612,false,true,55773,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,,1711,1785,Polychrome woodblock print; ink and color on paper,H. 11 3/8 in. (28.9 cm); W. 5 1/4 in. (13.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1613,false,true,55776,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,,1711,1785,Polychrome woodblock print; ink and color on paper,H. 14 1/4 in. (36.2 cm); W. 6 in. (15.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1614,false,true,55778,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,,1711,1785,Polychrome woodblock print; ink and color on paper,H. 27 3/4 in. (70.5 cm); W. 4 3/16 in. (10.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1615,false,true,55779,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,,1711,1785,Polychrome woodblock print; ink and color on paper,H. 25 9/16 in. (64.9 cm); W. 4 1/16 in. (10.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3142,false,true,56691,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Toyonobu,"Japanese, 1711–1785",,Ishikawa Toyonobu,Japanese,1711,1785,,1711,1785,Polychrome woodblock print; ink and color on paper,11 x 5 1/4 in. (27.9 x 13.3 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP874,false,true,54568,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 7 5/16 in. (18.6 cm); W. 10 3/16 in. (25.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1224,false,true,55146,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 7 7/16 in. (18.9 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1506,false,true,41057,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print with embossing (karazuri); ink and color on paper,Image: 12 3/4 x 8 1/4 in. (32.4 x 21 cm),"Fletcher Fund, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/41057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1623,false,true,55790,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 17 7/16 in. (44.3 cm); W. 8 1/8 in. (20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1624,false,true,55791,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 13/16 in. (27.5 cm); W. 8 1/8 in. (20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1626,false,true,55792,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 3/8 in. (21.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1629,false,true,42562,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 1/8 in. (20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/42562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1630,false,true,55794,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 13/16 in. (19.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1631,false,true,55795,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 7 5/16 in. (18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1635,false,true,45071,Asian Art,Print,すだれ貝|The Curtain Clam,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 11 1/4 in. (28.6 cm); W. 8 9/16 in. (21.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1639,false,true,55798,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 5/8 in. (19.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1641,false,true,55799,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 8 in. (20.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1642,false,true,55800,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 11/16 in. (27.1 cm); W. 8 1/4 in. (21 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1645,false,true,45087,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,"H. 7 1/4 in. (18.4 cm); W. 12 3/8 in. (31.4 cm) Medium-size block (""chuban"")","H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1647,false,true,55805,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 7/16 in. (26.5 cm); W. 7 15/16 in. (20.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1648,false,true,55806,Asian Art,Print,風俗江戸八景 浅草晴嵐|Asakusa Seiran,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 3/4 in. (27.3 cm); W. 7 13/16 in. (19.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1650,false,true,55807,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 3/4 in. (27.3 cm); W. 8 7/16 in. (21.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1652,false,true,55811,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 1/4 in. (21 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1653,false,true,55813,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,H. 10 15/16 in. (27.8 cm); W. 8 1/16 in. (20.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3020,false,true,56418,Asian Art,Print,"六玉川 「千鳥の玉川 陸奥名所」|“The Jewel River of Plovers, a Famous Place in Mutsu Province,” from the series Six Jewel Rivers (Mu Tamagawa: Chidori no Tamagawa, Mutsu meisho)",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,10 13/16 x 7 13/16 in. (27.5 x 19.8 cm),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3023,false,true,56421,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 7/8 in. (27.3 x 20 cm),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3024,false,true,56422,Asian Art,Print,"Koya no Tamagawa|Boy, Girl and Viewing Glass",Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,10 3/4 x 7 3/4 in. (27.3 x 19.7 cm),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3139,false,true,56687,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,11 x 8 in. (27.9 x 20.3 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3403,false,true,55590,Asian Art,Print,梅|Blowing Soap Bubbles Under the Plum Blossom,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,,1725,1770,Polychrome woodblock print; ink and color on paper,11 x 8 in. (27.9 x 20.3 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP457,false,true,36910,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,12 3/4 x 5 3/4 in. (32.4 x 14.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1353,false,true,55329,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 12 11/16 in. (32.2 cm); W. 5 15/16 in. (15.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1444,false,true,55488,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 5 1/2 in. (14 cm); W. 6 3/16 in. (15.7 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1446,false,true,55491,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 12 1/2 in. (31.8 cm); W. 5 13/16 in. (14.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1455,false,true,55501,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 12 7/16 in. (31.6 cm); 5 7/8 in. (14.9 cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1751,false,true,56057,Asian Art,Design for a fan,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Monochrome woodblock print; ink on paper,H. 9 3/4 in. (24.8 cm); W. 13 1/2 in. (34.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1752,false,true,56058,Asian Art,Design for a fan,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Monochrome woodblock print; ink on paper,H. 9 3/4 in. (24.8 cm); W. 13 1/2 in. (34.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1753,false,true,56059,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 12 15/16 in. (32.9 cm); W. 5 15/16 in. (15.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1754,false,true,56060,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 6 in. (15.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1755,false,true,56061,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 11 in. (27.9 cm); W. 8 3/8 in. (21.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1756,false,true,56062,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 8 15/16 in. (22.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1758,false,true,56064,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Left-hand sheet of a diptych of polychrome woodblock prints; ink and color on paper,H. 8 7/8 in. (22.5 cm); W. 6 7/16 in. (16.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3057,false,true,56512,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1615,1868,Polychrome woodblock print; ink and color on paper,12 5/8 x 5 3/4 in. (32.1 x 14.6 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3058,false,true,56513,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1615,1868,Polychrome woodblock print; ink and color on paper,12 x 5 3/8 in. (30.5 x 13.7 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3061,false,true,56536,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1615,1868,Diptych of polychrome woodblock prints; ink and color on paper,10 1/8 x 7 3/8 in. (25.7 x 18.7 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JP397a, b",false,true,36466,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,,1726,1792,Diptych of polychrome woodblock prints; ink and color on paper,H. 12 1/4 in. (31.1 cm); W. 11 1/4 in. (28.6 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1601,false,true,55765,Asian Art,Print,新板浮絵忍ヶ岡之圖|Perspective Print: Shinobazu Pond,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,,1734,1815,Polychrome woodblock print; ink and color on paper,H. 9 3/16 in. (23.3 cm); W. 13 1/2 in. (34.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1602,false,true,55766,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,,1735,1814,Polychrome woodblock print; ink and color on paper,H. 4 1/2 in. (11.4 cm); W. 12 3/8 in. (31.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1603,false,true,55767,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,,1735,1814,Polychrome woodblock print; ink and color on paper,H. 9 3/16 in. (23.3 cm); W. 14 7/8 in. (37.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1604,false,true,55768,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,,1735,1814,Polychrome woodblock print; ink and color on paper,H. 4 1/2 in. (11.4 cm); W. 12 3/8 in. (31.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP935,false,true,54805,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,,1739,1820,Polychrome woodblock print; ink and color on paper,H. 9 in. (22.9 cm); W. 14 in. (35.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1622,false,true,55789,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,,1739,1820,Polychrome woodblock print; ink and color on paper,H. 8 3/4 in. (22.2 cm); W. 14 13/16 in. (37.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1726,false,true,56037,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,,1739,1820,Polychrome woodblock print; ink and color on paper,H. 14 9/16 in. (37 cm); W. 9 7/8 in. (25.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1835,false,true,56120,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,,1739,1820,Monochrome woodblock print; ink on paper,H. 10 13/16 in. (27.5 cm); W. 7 9/16 in. (19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2178,false,true,55092,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,,1739,1820,Polychrome woodblock print (surimono); ink and color on paper,8 x 3 9/16 in. (20.3 x 9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2505,false,true,56927,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,,1739,1820,Polychrome woodblock print; ink and color on paper,Image: 20 1/8 × 14 1/4 in. (51.1 × 36.2 cm) Mat: 22 3/4 × 27 1/2 in. (57.8 × 69.9 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1127,false,true,55047,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,,1743,1812,Polychrome woodblock print; ink and color on paper,H. 12 5/8 in. (32.1 cm); W. 5 5/8 in. (14.3 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1761,false,true,56066,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,,1743,1812,Polychrome woodblock print; ink and color on paper,H. 11 7/8 in. (30.2 cm); W. 5 7/16 in. (13.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1762,false,true,56067,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,,1743,1812,Polychrome woodblock print; ink and color on paper,H. 11 1/2 in. (29.2 cm); W. 5 1/2 in. (14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1763,false,true,56068,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,,1743,1812,Polychrome woodblock print; ink and color on paper,H. 12 7/8 in. (32.7 cm); W. 5 5/8 in. (14.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1764,false,true,56069,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,,1743,1812,Polychrome woodblock print; ink and color on paper,H. 8 in. (20.3 cm); W. 13 1/4 in. (33.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3063,false,true,56540,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunkō,"Japanese, 1743–1812",,Katsukawa Shunkō,Japanese,1743,1812,,1615,1868,Polychrome woodblock print; ink and color on paper,15 1/4 x 10 1/8 in. (38.7 x 25.7 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1437,false,true,55472,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 28 5/16 in. (71.9 cm); W. 4 7/8 in. (12.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1712,false,true,55964,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 15 1/16 in. (38.3 cm); W. 9 7/8 in. (25.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1714,false,true,55968,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 9 11/16 in. (24.6 cm); W. 7 1/4 in. (18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1716,false,true,56027,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 7 1/16 in. (17.9 cm); W. 9 1/2 in. (24.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1717,false,true,56028,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 10 5/16 in. (26.2 cm); W. 7 9/16 in. (19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1718,false,true,56029,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 10 1/16 in. (25.6 cm); W. 7 9/16 in. (19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1719,false,true,56030,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 15 3/8 in. (39.1 cm); W. 10 15/16 in. (27.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1720,false,true,56031,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Diptych of polychrome woodblock prints; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 1/8 in. (25.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1721,false,true,56032,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); w. 9 7/8 in. (25.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1722,false,true,56033,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 14 13/16 in. (37.6 cm); W. 10 1/16 in. (25.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1723,false,true,56034,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 15 1/4 in. (38.7 cm); W. 10 5/16 in. (26.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1725,false,true,56036,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 14 13/16 in. (37.6 cm); W. 10 in. (25.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1727,false,true,56038,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 27 in. (68.6 cm); W. 4 9/16 in. (11.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1728,false,true,56039,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 27 1/8 in. (68.9 cm); W. 4 1/4 in. (10.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1730,false,true,56041,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 27 1/8 in. (68.9 cm); W. 4 13/16 in. (12.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1731,false,true,45270,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print (hashira-e); ink and color on paper,H. 27 3/8 in. (69.5 cm); W. 4 13/16 in. (12.2 cm),"H. O. Havemeyer Collection; Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1732,false,true,56042,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,Overall: 28 1/4 x 4 13/16 in. (71.8 x 12.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1733,false,true,56043,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,H. 25 5/8 in. (65.1 cm); W. 4 5/8 in. (11.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1735,false,true,56044,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Triptych of polychrome woodblock prints; ink and color on paper,a: H. 14 5/8 in. (37.1 cm); W. 9 11/16 in. (24.6 cm) b: H. 14 5/8 in. (37.1 cm); W. 9 11/16 in. (24.6 cm) c: H. 14 5/8 in. (37.1 cm); W. 9 15/16 in. (25.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2399,false,true,56802,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,15 x 9 13/16 in. (38.1 x 24.9 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3404,false,true,55591,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,,1742,1815,Polychrome woodblock print; ink and color on paper,10 1/4 x 7 5/8 in. (26 x 19.4 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2180,false,true,55094,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yomo no Utagaki Magao,"Japanese, 1753–1829",,Yomo no Utagaki Magao,Japanese,1753,1829,,1753,1829,Polychrome woodblock print (surimono); ink and color on paper,7 9/16 x 2 3/8 in. (19.2 x 6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1222,false,true,37317,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Diptych of polychrome woodblock prints; ink and color on paper,H. 14 3/8 in. (36.5 cm); W. 9 1/2 in. (24.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1781,false,true,56085,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Monochrome woodblock print; ink on paper,H. 10 5/8 in. (27 cm); W. 15 3/16 in. (38.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1782,false,true,56086,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Monochrome woodblock print; ink on paper,H. 13 5/8 in. (34.6 cm); W. 8 15/16 in. (22.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1783,false,true,56087,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Monochrome woodblock print; ink on paper,H. 14 in. (35.6 cm); W. 10 in. (25.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1784,false,true,56088,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 1/2 in. (24.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1785,false,true,56089,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Monochrome woodblock print; ink on paper,H. 14 3/4 in. (37.5 cm); W. 10 in. (25.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1787,false,true,56090,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Polychrome woodblock print; ink and color on paper,24 1/4 x 4 7/16 in. (61.6 x 11.3cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2186,false,true,55102,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Polychrome woodblock print (surimono); ink and color on paper,4 3/16 x 6 1/4 in. (10.6 x 15.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2398,false,true,56801,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Polychrome woodblock print; ink and color on paper,14 13/16 x 9 5/8 in. (37.6 x 24.4 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1096a,false,true,55031,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 9 7/8 in. (37.5 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1096b,false,true,639380,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,,1756,1829,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 9 3/4 in. (37.5 × 24.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/639380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1818,false,true,54406,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,,1757,1820,Polychrome woodblock print (surimono); ink and color on paper,7 13/16 x 7 1/16 in. (19.8 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1902,false,true,54441,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,,1757,1820,Polychrome woodblock print (surimono); ink and color on paper,5 1/8 x 7 3/16 in. (13 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1922,false,true,54461,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,,1757,1820,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 3/16 in. (20.6 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2032,false,true,54798,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,,1757,1820,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2181,false,true,55097,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,7 5/8 x 6 3/4 in. (19.4 x 17.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2184,false,true,55100,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,,1615,1868,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 1/8 in. (14.1 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2198,false,true,55115,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,,1757,1820,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 5 1/2 in. (21.1 x 14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1780,false,true,56084,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,,1762,1819,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 9 7/16 in. (24 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3041,false,true,56497,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,,1762,1819,Polychrome woodblock print; ink and color on paper,14 1/2 x 91/2 in. (36.8 x 24.1 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3137,false,true,56685,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shun'ei,"Japanese, 1762–1819",,Katsukawa Shun'ei,Japanese,1762,1819,,1762,1819,Polychrome woodblock print; ink and color on paper,13 3/4 x 9 in. (34.9 x 22.9 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1008,false,true,54884,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,,1763,1828,Polychrome woodblock print; ink and color on paper,H. 14 3/8 in. (36.5 cm); W. 10 in. (25.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1819,false,true,56109,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,,1763,1828,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 11/16 in. (24.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1820,false,true,56110,Asian Art,Print,Yahashi Kiho|Sails Returning to Yahashi,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,,1763,1828,Polychrome woodblock print; ink and color on paper,H. 8 13/16 in. (22.4 cm); W. 5 15/16 in. (15.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1821,false,true,54407,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,,1763,1828,Polychrome woodblock print (surimono); ink and color on paper,6 13/16 x 8 1/8 in. (17.3 x 20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2228,false,true,53994,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,,1763,1828,Polychrome woodblock print (surimono); ink and color on paper,8 9/16 x 7 9/16 in. (21.7 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1741,false,true,56048,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Middle sheet of a triptych of polychrome woodblock prints; ink and color on paper,H. 15 1/2 in. (39.4 cm); W. 10 1/8 in. (25.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1742,false,true,56049,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 10 in. (25.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1744,false,true,56051,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 1/4 in. (26 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1745,false,true,56052,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 1/8 in. (25.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1748,false,true,56055,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Triptych of polychrome woodblock prints; ink and color on paper,A: H. 15 1/16 in. (38.3 cm); W. 9 3/4 in. (24.8 cm) B: H. 15 1/16 in. (38.3 cm); W. 9 13/16 in. (24.9 cm) C: H. 15 1/16 in. (38.3 cm); W. 9 15/16 in. (25.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1831,false,true,56116,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Monochrome woodblock print; ink on paper,H. 10 1/2 in. (26.7 cm); W. 7 7/16 in. (18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1832,false,true,56117,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Monochrome woodblock print; ink on paper,H. 10 1/2 in. (26.7 cm); W. 7 1/8 in. (18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2211,false,true,53977,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2401,false,true,56804,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Diptych of polychrome woodblock prints; ink and color on paper,Diptych; each 15 x 10 1/16 in. (38.1 x 25.6 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3062,false,true,56538,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1615,1868,Polychrome woodblock print; ink and color on paper,14 3/4 x 9 3/4 in. (37.5 x 24.8 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3154,false,true,56707,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Polychrome woodblock print; ink and color on paper,10 1/4 x 14 5/8 in. (26 x 37.1 cm),"Gift of Mrs. Francis Ormond, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3155,false,true,56708,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1769,1825,Polychrome woodblock print; ink and color on paper,10 x 14 in. (25.4 x 35.6 cm),"Gift of Mrs. Francis Ormond, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3392,false,true,55569,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Woodblock for a print designed by,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,,1615,1868,Polychrome woodblock print; ink and color on paper,14 x 8 7/8 in. (35.6 x 22.5 cm),"Gift of Mr. and Mrs. Arthur J. Steel, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1830,false,true,54427,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,,1771,1844,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 11 1/8 in. (14 x 28.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2194,false,true,55110,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,,1771,1844,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 5/16 in. (20.6 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2216,false,true,53982,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,,1771,1844,Polychrome woodblock print (surimono); ink and color on paper,8 7/16 x 7 5/8 in. (21.4 x 19.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2161,false,true,55068,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Harukawa Goshichi,"Japanese, 1776–1831",,Harukawa Goshichi,Japanese,1776,1831,,1776,1831,Polychrome woodblock print (surimono); ink and color on paper,8 9/16 x 7 7/16 in. (21.7 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1099,false,true,54328,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kita Busei,"Japanese, 1776–1856",,Kita Busei,Japanese,1776,1856,,1776,1856,Polychrome woodblock print (surimono); ink and color on paper,7 3/16 x 10 3/4 in. (18.3 x 27.3 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3054a–e,false,true,56509,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni II,"Japanese, 1777–1835",,Utagawa Toyokuni II,Japanese,1777,1835,,1615,1868,Pentaptych of polychrome woodblock prints; ink and color on paper,a: 15 1/2 x 10 1/4 in. (39.4 x 26 cm); b: 15 1/4 x 10 in. (38.7 x 25.4 cm); c: 15 3/8 x 10 1/2 in. (39.1 x 26.7 cm); d: 15 3/8 x 10 1/8 in. (39.1 x 25.7 cm); e: 15 1/4 x 10 1/2 in. (38.7 x 26.7 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1034,false,true,54319,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/8 in. (21 x 18.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1466,false,true,55516,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print; ink and color on paper,H. 10 1/2 in. (26.7 cm); W. 7 1/2 in. (19.1 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1467,false,true,55517,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print; ink and color on paper,H. 10 1/2 in. (26.7 cm); W. 8 in. (20.3 cm),"Rogers Fund, 1925",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1810,false,true,54398,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Diptych of polychrome woodblock prints (surimono); ink and color on paper,Each print: 8 1/4 x 7 1/8 in. (21 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1812,false,true,54400,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/16 in. (21.1 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1813,false,true,54401,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 6 13/16 in. (20.6 x 17.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1871,false,true,54436,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/8 in. (21 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1915,false,true,54454,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 1/2 in. (14.3 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1937,false,true,54500,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 5/16 in. (20.3 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2185,false,true,55101,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 11/16 in. (14.3 x 19.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2219,false,true,53985,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/4 in. (20.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2222,false,true,53988,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,,1780,1850,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 1/4 in. (14 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.1,false,true,58198,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 × 10 1/8 in. (37.1 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.2,false,true,58199,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 in. × 10 1/16 in. (35.6 × 25.6 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.3,false,true,58200,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 × 10 1/8 in. (35.9 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.4,false,true,58201,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 × 9 7/8 in. (37.1 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.5,false,true,58202,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 in. × 10 in. (37.1 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.6,false,true,58203,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 × 9 7/8 in. (35.9 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.7,false,true,58204,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/16 × 9 7/8 in. (35.7 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.8,false,true,58205,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 in. × 10 in. (37.1 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.9,false,true,58206,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 × 9 7/8 in. (35.9 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.10,false,true,58207,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 × 9 7/8 in. (36.5 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.11,false,true,58208,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 7/8 in. (36.2 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.12,false,true,58209,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 × 10 1/16 in. (37.1 × 25.6 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.13,false,true,58210,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 × 9 7/8 in. (36.5 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.14,false,true,58211,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 in. × 10 in. (36.2 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.15,false,true,58212,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/16 × 9 3/4 in. (35.7 × 24.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.16,false,true,58213,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 × 9 3/4 in. (36.8 × 24.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.17,false,true,58214,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 7/8 in. × 10 in. (37.8 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.18,false,true,58215,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 13 15/16 × 9 3/8 in. (35.4 × 23.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.19,false,true,58216,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 7/8 in. (36.2 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.20,false,true,58217,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 7/8 in. (36.2 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.21,false,true,58218,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/16 × 9 3/4 in. (36 × 24.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.22,false,true,58219,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/16 in. × 10 in. (36 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.23,false,true,58220,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 10 1/8 in. (36.2 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.24,false,true,58221,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 10 1/8 in. (37.5 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.25,false,true,58222,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 × 10 1/8 in. (37.5 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.26,false,true,58223,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 7/8 × 10 1/8 in. (37.8 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.27,false,true,58224,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 × 9 7/8 in. (35.9 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.28,false,true,58225,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 in. × 10 in. (36.2 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.29,false,true,58226,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 3/4 in. (36.2 × 24.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.30,false,true,58227,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 × 9 7/8 in. (35.9 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.31,false,true,58228,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 in. × 10 1/16 in. (35.6 × 25.6 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.32,false,true,58229,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 in. × 10 in. (37.1 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.33,false,true,58230,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/16 × 9 7/8 in. (35.7 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.34,false,true,58231,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 7/8 in. (34.6 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.35,false,true,58232,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 in. × 10 1/16 in. (35.6 × 25.6 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.36,false,true,58233,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/4 in. × 10 in. (37.5 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.37,false,true,58234,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/4 × 9 1/2 in. (36.2 × 24.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.38,false,true,58235,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 × 9 7/8 in. (36.5 × 25.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.39,false,true,58237,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 in. × 10 in. (35.9 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1092.40,false,true,58239,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,,1786,1854,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 in. × 10 in. (36.5 × 25.4 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1795,false,true,56097,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,,1787,1867,Polychrome woodblock print; ink and color on paper,H. 14 15/16 in. (37.9 cm); W. 10 3/4 in. (27.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1796,false,true,56099,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,,1787,1867,Polychrome woodblock print; ink and color on paper,H. 24 1/16 in. (61.1 cm); W. 4 1/4 in. (10.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1797,false,true,56100,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,,1787,1867,Polychrome woodblock print; ink and color on paper,24 x 4 1/16 in. (61 x 10.3cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1798,false,true,56101,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,,1787,1867,Polychrome woodblock print; ink and color on paper,H. 23 9/16 in. (59.8 cm); W. 3 15/16 in. 910 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1799,false,true,56102,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,,1787,1867,Triptych of polychrome woodblock prints; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 9 7/8 in (25.1 cm) H. 14 5/8 in. (37.1 cm); W. 9 13/16 in. (24.9 cm) H. 14 5/8 in. (37.1 cm); W. 10 1/8 in. (25.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3055,false,true,56510,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,,1615,1868,Triptych of polychrome woodblock prints; ink and color on paper,11 5/8 x 16 5/8 in. (29.5 x 42.2 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP554,false,true,37005,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,,1790,1848,Polychrome woodblock print; ink and color on paper,10 x 15 in. (25.4 x 38.1 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB170,false,true,57861,Asian Art,Prints,"「傾城道中双六 見立よしはら五十三つい」|Album of prints from the series A Tōkaidō Board Game of Courtesans, Fifty-three Pairings in the Yoshiwara (Keisei dōchū sugoroku, Mitate Yoshiwara gojūsan tsui)",Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,,1790,1848,Fifty-six polychrome woodblock prints mounted as an album; ink and color on paper,11 in. × 14 3/4 in. (27.9 × 37.5 cm) Image (each): 9 3/8 × 13 1/4 in. (23.8 × 33.7 cm),"Gift of Suizan Miki, 1952",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1229,false,true,55150,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,,1790,1848,Polychrome woodblock print; ink and color on paper,H. 13 1/2 in. (34.3 cm); W. 8 11/16 in. (22.1 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1822,false,true,56111,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,,1790,1848,Polychrome woodblock print; ink and color on paper,H. 14 3/16 in. (36 cm); W. 8 13/16 in. (22.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1823,false,true,54424,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,,1790,1848,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 3/16 in. (20.5 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1301,false,true,42651,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ichikawa Danjuro VII,"Japanese, 1791–1859",,Danjuro VII,Japanese,1791,1859,,1790,1860,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 in. (20.3 x 17.8 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/42651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1833,false,true,56118,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyasu,"Japanese, 1794–1834",,Utagawa Kuniyasu,Japanese,1794,1834,,1794,1834,Monochrome woodblock print; ink on paper,H. 10 5/8 in. (27 cm); W. 7 7/16 in. (18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1431,false,true,54390,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Kiitsu,"Japanese, 1796–1858",,Suzuki Kiitsu,Japanese,1796,1858,,1798,1810,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 3/16 in. (20.6 x 18.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1804,false,true,54397,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,,1797,1861,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/8 in. (20.5 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP634,false,true,37085,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,,1829,1869,Polychrome woodblock print; ink and color on paper,8 7/16 x 13 1/2 in. (21.4 x 34.3 cm),"Purchase, Joseph Pulitzer Bequest, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1128,false,true,55048,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,,1829,1869,Polychrome woodblock print; ink and color on paper,H. 14 3/8 in. (36.5 cm); W. 9 1/2 in. (24.1 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP976,false,true,54856,Asian Art,Print,青楼歌舞妓やつし画尽 十番続|The Oiran Yoyogiku of Matsubaya Standing under a Cherry Tree,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 9 7/8 in. (25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP995,false,true,54874,Asian Art,Print,忠臣蔵二段目|A Young Man at the Side of a House,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 10 3/8 in. (26.4 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP997,false,true,54875,Asian Art,Print,忠臣蔵五段目|A Woman Snatching a Bag of Sweetmeats from Her MotHer,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 14 1/8 in. (35.9 cm); W. 9 7/8 in. (25.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP998,false,true,54876,Asian Art,Print,"忠臣蔵八段目|Two Tori-oi, or Itinerant Women Musicians of the Eta Class",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 15 in. (38.1 cm); W. 10 1/2 in. (26.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP999,false,true,45476,Asian Art,Print,"忠臣蔵九段目|A Woman at Her Toilet Seated before a Mirror, Having Her Hair combed by a Kameyui (Woman Hairdresser)",Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 15 1/8 in. (38.4 cm); W. 10 1/2 in. (26.7 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1663,false,true,40601,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 14 3/4 in. (37.5 cm); W. 9 3/4 in. (24.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/40601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1670,false,true,55896,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 12 3/4 in. (32.4 cm); W. 7 3/4 in. (19.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1671,false,true,45478,Asian Art,Print,風流七小町 通ひ|Young MotHer Nursing Her Baby,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 11/16 in. (37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1678,false,true,55910,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 23 1/2 in. (59.7 cm); W. 4 3/8 in. (11.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1680,false,true,37340,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Triptych of polychrome woodblock prints; ink and color on paper,14 1/8 x 28 23/32 in. (35.9 x 73.0 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1681,false,true,37341,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Triptych of polychrome woodblock prints; ink and color on paper,14 15/32 x 29 7/16 in. (36.8 x 74.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1682,false,true,37342,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Triptych of polychrome woodblock prints; ink and color on paper,15 1/8 x 28 13/16 in. (38.4 x 73.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1688,false,true,37345,Asian Art,Print,風流六玉川 武蔵 紀伊 陸奥|Women and a Man in the Country; Some pageant(?),Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Triptych of polychrome woodblock prints; ink and color on paper,A: H. 14 5/8 in. (37.21 cm); W. 9 3/8 in. (23.8 cm) B: H. 14 11/16 in. (37.31 cm); W. 9 9/16 in. (24. 31 cm) C: H. 14 11/16 in. (37.31 cm); W. 9 7/16 in. (24 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1689,false,true,37346,Asian Art,Print,風流六玉川 山城 近江 摂津|Women and Children on the Banks of a Stream,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Triptych of polychrome woodblock prints; ink and color on paper,14 5/8 x 9 7/16 in. (37.21 x 23.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1690,false,true,55925,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,H. 29 1/2 in. (74.9 cm); W. 9 5/8 in. (24.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2394,false,true,56799,Asian Art,Print,名取酒六家選 大もんぢや内浅じふ 木綿屋七ッ梅|A Courtesan,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Polychrome woodblock print; ink and color on paper,14 3/8 x 9 11/16 in. (36.5 x 24.6 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3157,false,true,56710,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1951",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3158,false,true,56711,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Image: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1951",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3159,false,true,56712,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1951",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3160,false,true,56713,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1951",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3161,false,true,56714,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1952",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3162,false,true,56715,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1952",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3163,false,true,56716,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1952",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3164,false,true,56717,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1952",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3165,false,true,56718,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3166,false,true,56719,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3167,false,true,56720,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3168,false,true,56721,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1753,1806,Polychrome woodblock print; ink and color on paper,Image: 10 in. × 15 1/8 in. (25.4 × 38.4 cm) Mat: 15 1/2 × 22 3/4 in. (39.4 × 57.8 cm),"Anonymous Gift, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1433,false,true,54391,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,,1786,1868,Polychrome woodblock print (surimono); ink and color on paper,8 11/16 x 7 1/2 in. (22.1 x 19.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1825,false,true,54425,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,,1786,1868,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 6 7/8 in. (20 x 17.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1826,false,true,54426,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,,1786,1868,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/4 in. (20.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3606,false,true,54229,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,,1786,1868,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 6 1/3 in. (20 x 16.1 cm),"Seymour Fund, 1981",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3607,false,true,54230,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,,1776,1878,Polychrome woodblock print (surimono); ink and color on paper,7 7/16 x 7 3/16 in. (18.9 x 18.3 cm),"Seymour Fund, 1981",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1926,false,true,54466,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūgetsusai Shinkō,"Japanese, active 1810s",,Ryūgetsusai Shinkō,Japanese,1810,1819,,1810,1819,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 10 9/16 in. (21.1 x 26.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1610,false,true,55770,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,,1715,1760,Polychrome woodblock print; ink and color on paper,H. 12 5/16 in. (31.3 cm); W. 5 5/8 in. (14.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3409,false,true,55598,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Toshinobu,active ca. 1725–1750,,Okumura Toshinobu,Japanese,1725,1750,,1725,1750,Polychrome woodblock print; ink and color on paper,12 1/4 x 5 3/4 in. (31.1 x 14.6 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3328,false,true,55476,Asian Art,Print,生写異国人物 清朝南京人感賞皇州扇之図|Two Chinese Men,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,,1807,1879,Polychrome woodblock print; ink and color on paper,14 1/4 x 10 in. (36.2 x 25.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1306,false,true,55245,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūkōsai,active late 18th century,,Ryūkōsai,Japanese,1771,1799,,1771,1799,Polychrome woodblock print; ink and color on paper,H. 12 1/16 in. (30.6 cm); W. 5 9/16 in. (14.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP880,false,true,54573,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1735,1790,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 7 in. (17.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP891,false,true,54585,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1735,1790,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 7 1/2 in. (19.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1693,false,true,55928,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1735,1790,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 7 9/16 in. (19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1694,false,true,55929,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 10 1/8 in. (25.7 cm); W. 7 9/16 in. (19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1695,false,true,55930,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 3/4 in. (19.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1696,false,true,55932,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 8 1/8 in. (20.6 cm); W. 5 7/8 in. (14.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1697,false,true,55934,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 7 7/16 in. (18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1698,false,true,55936,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,10 × 7 1/2 in. (25.4 × 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1699,false,true,55939,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 7 1/8 in. (18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1700,false,true,55941,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 5/8 in. (19.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1701,false,true,55942,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 7 11/16 in. (19.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1702,false,true,55943,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 10 5/16 in. (26.2 cm); W. 7 3/4 in. (19.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1705,false,true,55947,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 27 3/4 in. (70.5 cm); W. 4 5/8 in. (11.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1706,false,true,55948,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 26 3/8 in. (67 cm); W. 4 5/8 in. (11.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1707,false,true,55949,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 26 1/8 in. (66.4 cm); W. 4 7/16 in. (11.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1708,false,true,55953,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 26 1/4 in. (66.7 cm); W. 4 3/4 in. (12.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1709,false,true,55955,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 27 9/16 in. (70 cm); W. 4 7/8 in. (12.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1710,false,true,55957,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1770,1790,Polychrome woodblock print; ink and color on paper,H. 27 5/8 in. (70.2 cm); W. 4 7/16 in. (11.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3140,false,true,56688,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1735,1790,Polychrome woodblock print; ink and color on paper,12 5/8 x 8 3/8 in. (32.1 x 21.3 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3412,false,true,55601,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,,1735,1790,Polychrome woodblock print; ink and color on paper,10 1/2 x 7 1/2 in. (26.7 x 19.1 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2095,false,true,54941,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Shūchōdō Monoyana,"Japanese, 1761–ca. 1830",,Shūchōdō Monoyana,Japanese,1761,1830,,1761,1830,Polychrome woodblock print (surimono); ink and color on paper,5 7/8 x 6 15/16 in. (14.9 x 17.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1379,false,true,55365,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suikōdō Sakei,"Japanese, active ca. 1764",,Suikōdō Sakei,Japanese,1754,1774,,1754,1774,Polychrome woodblock print; ink and color on paper,Image: 10 x 7 5/8 in. (25.4 x 19.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3398,false,true,55576,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tosen,"Japanese, active ca. 1770",,Tosen,Japanese,1770,1770,,1760,1780,Polychrome woodblock print; ink and color on paper,27 x 5 1/4 in. (68.6 x 13.3 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3395,false,true,55572,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hanekawa Chinchō,"Japanese, ca. 1679–1754",,Hanekawa Chinchō,Japanese,1679,1754,,1669,1754,Monochrome woodblock print; ink on paper,12 1/4 x 21 3/4 in. (31.1 x 55.2 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1808,false,true,56107,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishimura Shigenobu,"Japanese, active 1729–39",,Nishimura Shigenobu,Japanese,1729,1739,,1711,1785,Polychrome woodblock print; ink and color on paper,12 9/16 x 5 7/8 in. (31.9 x 14.9cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3056,false,true,56511,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nishimura Shigenobu,"Japanese, active 1729–39",,Nishimura Shigenobu,Japanese,1729,1739,,1615,1868,Polychrome woodblock print; ink and color on paper,12 1/2 in. x 6 in. (31.8 x 15.2 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1736,false,true,56045,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,,1749,1795,Monochrome woodblock print; ink on paper,H. 15 3/8 in. (39.1 cm); W. 10 7/8 in. (27.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1738,false,true,56046,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tōshūsai Sharaku,"Japanese, active 1794–95",,Tōshūsai Sharaku,Japanese,1794,1795,,1749,1795,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 9 3/4 in. (24.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1599,false,true,55763,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyomasu I,"Japanese, active 1696–1716",,Torii Kiyomasu I,Japanese,1696,1716,,1696,1716,Polychrome woodblock print; ink and color on paper,H. 12 1/8 in. (30.8 cm); W. 5 3/4 in. (14.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1774,false,true,56078,Asian Art,Design for a fan,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,,1723,1792,Monochrome woodblock print; ink on paper,H. 9 3/4 in. (24.8 cm); W. 13 7/16 in. (34. 1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1775,false,true,56079,Asian Art,Design for a fan,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,,1723,1792,Monochrome woodblock print; ink on paper,H. 9 3/4 in. (24.8 cm); W. 13 1/2 in. (34.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1776,false,true,56080,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,,1723,1792,Monochrome woodblock print; ink on paper,H. 9 14/16 in. (25.1 cm); W. 13 9/16 in. (34.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1777,false,true,56081,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,,1723,1792,Polychrome woodblock print; ink and color on paper,H. 11 5/8 in. (29.5 cm); W. 5 5/8 in. (14.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1778,false,true,56082,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,,1723,1792,Polychrome woodblock print; ink and color on paper,H. 11 1/2 in. (29.2 cm); W. 5 3/8 in. (13.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1779,false,true,56083,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,,1723,1792,Polychrome woodblock print; ink and color on paper,H. 5 7/8 in. (14.9 cm); W. 11 1/2 in. (29.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3044,false,true,56499,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ippitsusai Bunchō,"Japanese, active 1760–1794",,Ippitsusai Bunchō,Japanese,1760,1794,,1615,1868,Polychrome woodblock print; ink and color on paper,11 3/4 x 5 5/8 in. (29.8 x 14.3 cm),"Gift of Mrs. Morris Manges, in memory of her husband, Dr. Morris Manges, 1947",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3410,false,true,55599,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunzan,"Japanese, active 1782–1798",,Katsukawa Shunzan,Japanese,1782,1798,,1782,1798,Polychrome woodblock print; ink and color on paper,15 x 9 7/8 in. (38.1 x 25.1 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1809,false,true,56108,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shotei Hokuju,"Japanese, active 1790–1820",,Shotei Hokuju,Japanese,1790,1820,,1790,1820,Polychrome woodblock print; ink and color on paper,10 x 14 15/16 in. (25.4 x 37.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3399,false,true,55578,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Yuko,"Japanese, early 18th century",,Yuko,Japanese,1700,1750,,1700,1735,Polychrome woodblock print; ink and color on paper,12 x 5 3/8 in. (30.5 x 13.7 cm),"Bequest of Julia H. Manges, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55578,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1935,false,true,54475,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shungensai,"Japanese, 18th–19th century",,Shungensai,Japanese,1700,1899,,1768,1868,Polychrome woodblock print (surimono); ink and color on paper,8 x 10 7/8 in. (20.3 x 27.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2091,false,true,54934,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yukeisha,"Japanese, 18th–19th century",,Yukeisha,Japanese,1700,1899,,1768,1868,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/16 in. (13.7 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1925,false,true,54465,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoshige,"Japanese, active 1802?–?1835",,Utagawa Toyoshige,Japanese,1802,1835,,1802,1835,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 5 1/2 in. (20.6 x 14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1765,false,true,56070,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Polychrome woodblock print; ink and color on paper,14 × 9 11/16 in. (35.6 × 24.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1766,false,true,56071,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Middle sheet of a triptych of polychrome woodblock prints; ink and color on paper,15 1/8 × 9 7/8 in. (38.4 × 25.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1767,false,true,56072,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Polychrome woodblock print; ink and color on paper,12 1/2 × 8 7/8 in. (31.8 × 22.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1768,false,true,56073,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Polychrome woodblock print; ink and color on paper,25 3/8 × 4 1/2 in. (64.5 × 11.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1769,false,true,56074,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Polychrome woodblock print; ink and color on paper,25 7/8 × 4 9/16 in. (65.7 × 11.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1771,false,true,56075,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Polychrome woodblock print; ink and color on paper,27 3/4 × 4 13/16 in. (70.5 × 12.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1772,false,true,56076,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Polychrome woodblock print; ink and color on paper,27 1/8 × 4 9/16 in. (68.9 × 11.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1773,false,true,56077,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Diptych of polychrome woodblock prints; ink and color on paper,(a): 15 3/16 × 10 in. (38.6 × 25.4 cm) (b): 14 7/8 × 9 7/8 in. (37.8 × 25.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2402,false,true,56805,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,,1780,1795,Diptych of polychrome woodblock prints; ink and color on paper,Image (each): 14 7/16 × 9 3/4 in. (36.7 × 24.8 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3182,false,true,53244,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,,1615,1868,Polychrome woodblock print; ink and color on paper,14 3/4 x 9 3/4 in. (37.5 x 24.8 cm),"GIft of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3325,false,true,55473,Asian Art,Print,"外国人物図畫仏蘭西|France, from the series Pictures of People from Foreign Lands (Gaikoku jinbutsu zuga)",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,,1850,1880,Polychrome woodblock print; ink and color on paper,14 x 9 5/8 in. (35.6 x 24.4 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1807,false,true,56106,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Tsunegawa Shigenobu,"Japanese, active ca. 1724–1735",,Tsunegawa Shigenobu,Japanese,1724,1735,,1724,1735,Polychrome woodblock print; ink and color on paper,12 9/16 x 6 3/16 in. (31.9 x 15.7cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1213,false,true,55136,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Eiju,"Japanese, active ca. 1789–1801",,Eiju,Japanese,1789,1801,,1789,1801,Polychrome woodblock print; ink and color on paper,H. 15 7/16 in. (39.2 cm); W. 10 1/8 in. (25.7 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1792,false,true,56094,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ichirakutei Eisui,"Japanese, active ca. 1793–1801",,Ichirakutei Eisui,Japanese,1793,1801,,1793,1801,Polychrome woodblock print; ink and color on paper,H. 13 3/16 in. (33.5 cm); W. 9 3/16 in. (23.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1793,false,true,56095,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ichirakutei Eisui,"Japanese, active ca. 1793–1801",,Ichirakutei Eisui,Japanese,1793,1801,,1793,1801,Polychrome woodblock print; ink and color on paper,Image: 14 15/16 x 9 11/16 in. (37.9 x 24.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1105,false,true,54334,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1801,1813,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 7/16 in. (13.8 x 18.9 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1249,false,true,54380,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 1/16 in. (21.3 x 17.9 cm),"Rogers Fund, 1921",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1814,false,true,54402,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,7 5/16 x 10 7/16 in. (18.6 x 26.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1815,false,true,54403,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 3/8 in. (13.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1816,false,true,54404,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono) in shape of a twofold screen; ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1817,false,true,54405,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 3/8 in. (13.7 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1916,false,true,54455,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 1/4 x 7 1/4 in. (13.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1918,false,true,54457,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1919,false,true,54458,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 5/16 in. (21.3 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1923,false,true,54462,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1799,1823,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 3 5/8 in. (21 x 9.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1928,false,true,54468,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1799,1823,Polychrome woodblock print (surimono); ink and color on paper,6 x 7 3/8 in. (15.2 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1938,false,true,54502,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 11/16 x 7 1/2 in. (14.4 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1939,false,true,54503,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 13/16 x 7 3/4 in. (14.8 x 19.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2053,false,true,54828,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 3 7/8 in. (14.3 x 9.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2183,false,true,55099,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 11 in. (20.8 x 27.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2188,false,true,55104,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2191,false,true,55107,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2193,false,true,55109,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 3/4 x 7 1/2 in. (14.6 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1803,false,true,56105,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Hakusanjin Hokui,"Japanese, active ca. 1830–1840",,Hakusanjin Hokui,Japanese,1830,1840,,1830,1840,Polychrome woodblock print; ink and color on paper,9 x 12 1/2 in. (22.9 x 31.8cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3326,false,true,55474,Asian Art,Print,Furansukoku|仏蘭西国|France,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,,1840,1860,Polychrome woodblock print; ink and color on paper,13 x 9 1/4 in. (33 x 23.5 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2130,false,true,54985,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Uematsu Tōshū,"Japanese, active late 1810s–20s",,Uematsu Tōshū,Japanese,1810,1830,,1615,1868,Polychrome woodblock print (surimono); ink and color on paper,5 x 7 1/8 in. (12.7 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2131,false,true,54986,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Uematsu Tōshū,"Japanese, active late 1810s–20s",,Uematsu Tōshū,Japanese,1810,1830,,1615,1868,Polychrome woodblock print (surimono); ink and color on paper,5 1/8 x 7 in. (13 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2170,false,true,55083,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Uematsu Tōshū,"Japanese, active late 1810s–20s",,Uematsu Tōshū,Japanese,1810,1830,,1615,1868,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 3/16 in. (20.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2223,false,true,53989,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Uematsu Tōshū,"Japanese, active late 1810s–20s",,Uematsu Tōshū,Japanese,1810,1830,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,5 1/8 x 10 3/16 in. (13 x 25.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1020,false,true,54928,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ippyotei Yoshikuni,"Japanese, active mid-19th century",,Ippyotei Yoshikuni,Japanese,1800,1899,,1836,1870,Polychrome woodblock print; ink and color on paper,H. 14 5/8 in. (37.1 cm); W. 10 1/4 in. (26 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1836,false,true,56121,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ranshu,"Japanese, latter half of the 18th century",,Ranshu,Japanese,0018,0018,,1750,1799,Monochrome woodblock print; ink on paper,H. 9 3/4 in. (24.8 cm); W. 13 1/2 in. (34.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1015,false,true,54890,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,H. 9 3/4 in. (24.8 cm); W. 14 1/2 in. (36.8 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1108,false,true,54337,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1850,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 13 5/16 in. (21.6 x 33.8 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1844,false,true,56129,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Monochrome woodblock print; ink on paper,H. 14 15/16 in. (37.9 cm); W. 10 5/16 in. (26.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1845,false,true,56130,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Monochrome woodblock print; ink on paper,H. 15 in. (38.1 cm); W. 10 3/8 in. (26.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1853,false,true,54428,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,6 11/16 x 18 3/16 in. (17 x 46.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1857,false,true,45034,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,H. 7 7/8 in. (20 cm); W. 21 7/8 in. (55.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1858,false,true,56177,Asian Art,Print,市川団十郎|The Actor Ichikawa Danjuro I 1660–1704,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,H. 12 in. (30.5 cm); W. 5 3/4 in. (14.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1862,false,true,56185,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,H. 13 1/16 in. (33.2 cm); W. 9 3/8 in. (23.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1863,false,true,56186,Asian Art,Print,七日夜|Wave,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Monochrome woodblock print; ink on paper,H. 14 11/16 in. (37.3 cm); W. 10 11/16 in. (27.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1864,false,true,54429,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 27 7/16 in. (19.7 x 69.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1866,false,true,54431,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,5 1/8 x 7 5/16 in. (13 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1867,false,true,54432,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 1/4 in. (20 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1869,false,true,54434,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,6 5/16 x 12 7/16 in. (16 x 31.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1872,false,true,54437,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2150,false,true,55011,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,7 1/2 x 10 3/8 in. (19.1 x 26.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2177,false,true,55091,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print (surimono); ink and color on paper,7 1/2 x 21 1/4 in. (19.1 x 54 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2392,false,true,56797,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,9 13/16 x 13 1/16 in. (24.9 x 33.2 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2551,false,true,56977,Asian Art,Woodblock print,"百人一首 うばがゑとき 三条院|Poem by Sanjō-in, from the series One Hundred Poems Explained by the Nurse (Hyakunin isshu uba ga etoki)",Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Polychrome woodblock print; ink and color on paper,Overall: 10 1/4 x 14 3/4 in. (26 x 37.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1312,false,true,45294,Asian Art,Print,Ryogoku no yoizuki|東都名所 両国之宵月|Twilight Moon at Ryōgoku Bridge,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 5/8 in. (24.4 cm); W. 15 3/16 in. (38.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1314,false,true,55271,Asian Art,Print,東都名所 新吉原朝桜之図|Morning Cherries at Yoshiwara,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 7/16 in. (24 cm); W. 15 1/8 in. (38.4 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1315,false,true,45296,Asian Art,Print,"Shibaura, shiohi-gari no zu|東都名所 芝浦汐干之図|Shell Gathering at Shibaura",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 15 in. (38.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1316,false,true,55273,Asian Art,Print,江都名所 飛鳥山はな見|Asukayama Hanami,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 13/16 in. (24.9 cm); W. 14 5/8 in. (37.1 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1318,false,true,55277,Asian Art,Print,京都名所之内 清水|Kiyomizu,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 11/16 in. (37.3 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1320,false,true,55279,Asian Art,Print,東都名所 目黒行人阪之図|Meguro Gionin Zaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 1/2 in. (24.1 cm); W. 14 1/4 in. (36.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1321,false,true,55280,Asian Art,Print,東都名所 品川大井 八景坂鎧掛松|Shinagawa Hakkei Zaka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 7/16 in. (24 cm); W. 14 1/4 in. (36.2 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1861,false,true,56184,Asian Art,Print,東都名所 芝増上寺雪中ノ図|Zojoji Temple at Shiba in Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 10 in. (25.4 cm); W. 14 3/4 in. (37.5 cm),"Gift of Mrs. Henry J. Bernheim, 1945",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1874,false,true,56188,Asian Art,Print,近江八景之内 唐崎夜雨|Evening Rain on the Karasaki Pine,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 10 1/4 in. (26 cm); W. 15 in. (38.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1875,false,true,56191,Asian Art,Print,近江八景之内 瀬田夕照|Sunset at Seta,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 8 15/16 in. (22.7 cm); W. 13 7/8 in. (35.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1878,false,true,56259,Asian Art,Print,"東海道五十三次 見附 天竜川図|Mitsuke; Tenryugawa Ferry, Station No. 29",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,8 7/8 x 13 11/16 in. (22.5 x 34.8cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1879,false,true,56293,Asian Art,Print,"木曽海道六拾九次之内 長久保|Nagakubo, Station No. 28",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 8 5/8 in. (21.9 cm); W. 13 1/8 in. (33.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1881,false,true,56294,Asian Art,Print,"本朝名所 相州江ノ嶋岩屋之図|Sōshū, Enoshima Iwaya no Zu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 8 7/8 in. (22.5 cm); W. 14 in. (35.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1882,false,true,56514,Asian Art,Print,"本朝名所 相州七里ヶ浜|Seven-ri Beach, Province of Soshu",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 1/8 in. (23.2 cm); W. 14 1/16 in. (35.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1883,false,true,56515,Asian Art,Print,東都名所 猿若町芝居の図|Picture of the Theatres in Sakai Cho,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 8 15/16 in. (22.7 cm); W. 13 5/8 in. (34.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1884,false,true,56516,Asian Art,Print,東都名所 真崎雪晴ノ図|Clearing Weather after Snow at Massaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 9 7/8 in. (25.1 cm); W. 13 7/16 in. (34.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1885,false,true,56517,Asian Art,Print,京都名所之内 糺川原之夕立|Tea-houses on the Bank of the Tadasu River in a Shower,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 8 1/2 in. (21.6 cm); W. 13 11/16 in. (34.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1886,false,true,56518,Asian Art,Print,京都名所之内 あらし山満花|Arashiyama Manka,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 8 3/4 in. (22.2 cm); W. 14 in. (35.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1888,false,true,56520,Asian Art,Print,東都名所 佃島海辺朧月|Tsukudajima no Oborozuki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 14 3/8 in. (36.5 cm); W. 4 15/16 in. (12.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1890,false,true,56521,Asian Art,Print,江戸十二景 隅田川|Gotenyama-no Hana,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 4 7/16 in. (11.3 cm); W. 6 5/8 in. (16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1891,false,true,56589,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 4 7/16 in. (11.3 cm); W. 6 5/8 in. (16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1896,false,true,56592,Asian Art,Print,小松にきじ|Pheasant and Pine-trees on Snowy Hillside,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,H. 14 7/8 in. (37.8 cm); W. 6 3/4 in. (17.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1897,false,true,56795,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,13 1/16 x 4 5/16 in. (33.2 x 11 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2400,false,true,56803,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,9 1/16 x 4 1/16 in. (23 x 10.3 cm) Probably harimazé (mixed print),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2460,false,true,56886,Asian Art,Print,東都名所 両国之宵月|Twilight Moon at Ryōgoku Bridge,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,13 3/4 x 8 1/2 in. (34.9 x 21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2873,false,true,57039,Asian Art,Print,木曽海道六拾九次之内 望月|Mochizuki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,9 1/4 x 14 3/4 in. (23.5 x 37.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3134,false,true,56682,Asian Art,Print,雪月花 木曽路之山川|Kiso Gorge in the Snow,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Triptych of polychrome woodblock prints; ink and color on paper,14 x 29 5/8 in. (35.6 x 75.2 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3135,false,true,56683,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1615,1868,Polychrome woodblock print; ink and color on paper,28 3/4 x 9 1/2 in. (73 x 24.1 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3141,false,true,56690,Asian Art,Print,月二拾八景之内 弓張月|Bow Moon,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,14 7/8 x 6 1/4 in. (37.8 x 15.9 cm),"Gift of Francis M. Weld, 1948",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3144,false,true,56695,Asian Art,Print,近江八景之内 堅田落雁|Geese Flying Down to Katada,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,13 3/8 x 8 5/8 in. (34 x 21.9 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3145,false,true,56696,Asian Art,Print,近江八景之内 瀬田夕照|Long Bridge of Seta,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,13 3/4 x 8 3/4 in. (34.9 x 22.2 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3147,false,true,56698,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,14 5/8 x 5 in. (37.1 x 12.7 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3149,false,true,56700,Asian Art,Print,梅に三光鳥|Long Tailed Bird,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,14 1/2 x 6 3/4 in. (36.8 x 17.1 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3150,false,true,56702,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,12 3/4 x 2 3/8 in. (32.4 x 6 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3151,false,true,56703,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1615,1868,Polychrome woodblock print; ink and color on paper,13 1/8 x 4 5/16 in. (33.3 x 11 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3152,false,true,56705,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,8 7/8 x 6 1/2 in. (22.5 x 16.5 cm),"Bequest of Ellis G. Seymour, 1949",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3426,false,true,55628,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,27 3/4 x 9 3/8 in. (70.5 x 23.8 cm),"Bequest of Gertrude Abbot Phillips, 1965",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3436,false,true,55637,Asian Art,Print,江戸名所 芝愛宕山|Shiba Atogayama,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1615,1868,Polychrome woodblock print; ink and color on paper,10 x 14 1/2 in. (25.4 x 36.8 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3437,false,true,55638,Asian Art,Print,江都名所 飛鳥山はな見|Asukayama Hanami,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1615,1868,Polychrome woodblock print; ink and color on paper,9 1/2 x 14 1/2 in. (24.1 x 36.8 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3511,false,true,45297,Asian Art,Print,"Shin-Yoshiwara, asazukura no zu|東都名所 新吉原朝桜之図|Morning Cherry Blossoms at Shin-Yoshiwara",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Polychrome woodblock print; ink and color on paper,10 1/4 x 15 1/8 in. (26 x 38.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3513,false,true,55808,Asian Art,Woodblock print,梅に三光鳥|Bird on a Plum Branch,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1615,1868,Polychrome woodblock print; ink and color on paper,15 1/4 x 6 3/4 in. (38.7 x 17.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3514,false,true,55809,Asian Art,Print,花菖蒲に白鷺|White Heron and Iris,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1800,1858,Polychrome woodblock print; ink and color on paper,15 3/8 x 6 1/2 in. (39.1 x 16.5 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3515,false,true,55812,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1615,1868,Polychrome woodblock print; ink and color on paper,14 1/2 x 4 7/8 in. (36.8 x 12.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3509a–c,false,true,55787,Asian Art,Print,"雪月花 武陽金沢八勝夜景|Full Moon at Kanazawa, Province of Musashi",Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,,1797,1858,Triptych of polychrome woodblock prints; ink and color on paper,L. sheet: 9 15/16 x 14 5/8 in. (25.2 x 37.1 cm); C. sheet: 14 21/32 x 9 29/32 in. (37.2 x 25.2 cm); R. sheet: 14 21/32 x 9 15/16 in. (37.2 x 25.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP28,false,true,57135,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP29,false,true,57136,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP30,false,true,57137,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP31,false,true,57140,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP32,false,true,57141,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP33,false,true,57142,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP34,false,true,57143,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP35,false,true,57144,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP36,false,true,57145,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP37,false,true,57146,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP38,false,true,57147,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +MJP39,false,true,57148,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,After,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,,1868,1912,Monochrome woodblock print; ink on paper,10 1/4 x 14 11/16 in. (26 x 37.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3712,false,true,55976,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Watanabe Seitei,"Japanese, 1851–1918",,Watanabe Seitei,Japanese,1851,1918,,1851,1918,Frontispiece; polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3709,false,true,55967,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mishima Shōsō,"Japanese, 1856–1928",,Mishima Shōsō,Japanese,1856,1928,,1856,1928,Frontispiece; polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3710,false,true,55970,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mishima Shōsō,"Japanese, 1856–1928",,Mishima Shōsō,Japanese,1856,1928,,1868,1912,Frontispiece; polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3714,false,true,55978,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Suzuki Kason,"Japanese, 1860–1919",,Suzuki Kason,Japanese,1860,1919,,1868,1912,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3715,false,true,55979,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Suzuki Kason,"Japanese, 1860–1919",,Suzuki Kason,Japanese,1860,1919,,1860,1919,Frontispiece; polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3718,false,true,55982,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Keishū,"Japanese, 1861–1943",,Takeuchi Keishū,Japanese,1861,1943,,1868,1912,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3719,false,true,55983,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Keishū,"Japanese, 1861–1943",,Takeuchi Keishū,Japanese,1861,1943,,1868,1912,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3708,false,true,55962,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tomioka Eisen,"Japanese, 1864–1905",,Tomioka Eisen,Japanese,1864,1905,,1864,1905,Frontispiece; polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3711,false,true,55975,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tomioka Eisen,"Japanese, 1864–1905",,Tomioka Eisen,Japanese,1864,1905,,1864,1905,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3721,false,true,55985,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tomioka Eisen,"Japanese, 1864–1905",,Tomioka Eisen,Japanese,1864,1905,,1864,1905,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3713,false,true,55977,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Mizuno Toshikata,"Japanese, 1866–1908",,Mizuno Toshikata,Japanese,1866,1908,,1866,1908,Frontispiece; polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3716,false,true,55980,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kajita Hanko,"Japanese, 1870–1917",,Kajita Hanko,Japanese,1870,1917,,1870,1917,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3720,false,true,55984,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kajita Hanko,"Japanese, 1870–1917",,Kajita Hanko,Japanese,1870,1917,,1868,1912,Polychrome woodblock print; ink and color on paper,Album: 8 1/2 x 5 5/16 in. (21.6 x 13.5 cm),"Gift of Donald Keene, in honor of Julia Meech-Pekarik, 1986",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3173,false,true,56725,Asian Art,Print,,Japan,Taishō period (1912–26),,,,Artist,,Hashiguchi Goyō,"Japanese, 1881–1921",,Hashiguchi Goyō,Japanese,1881,1921,,1880,1921,Polychrome woodblock print; ink and color on paper,Image: 16 1/4 × 20 1/2 in. (41.3 × 52.1 cm) Mat: 22 3/4 × 27 5/8 in. (57.8 × 70.2 cm),"Gift of Mr. and Mrs. A. I. Sherr, 1956",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3370,false,true,55537,Asian Art,Print,Furansukoku|France,Japan,Edo (1615–1868)–Meiji period (1868–1912),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,,1850,1870,Polychrome woodblock print; ink and color on paper,13 1/2 x 9 1/4 in. (34.3 x 23.5 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.669,false,true,45511,Asian Art,Writer's box,,Japan,Edo period (1615–1868),,,,Artist,In the style of,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,,1615,1868,"Clay with crackled glaze, decorated on outside and inside (Kyoto ware, Kenzan style)",H. 3 1/2 in. (8.9 cm); W. 8 in. (20.3 cm); L. 9 in. (22.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.670,false,true,45512,Asian Art,Writer's box,,Japan,Edo period (1615–1868),,,,Artist,In the style of,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,,1615,1868,"Crackled glaze; design modelled in relief (Kyoto ware, Kenzan style)",H. 3 1/4 in. (8.3 cm); W. 7 1/4 in. (18.4 cm); L. 9 1/4 in. (23.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/45512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.603,false,true,52308,Asian Art,Teabowl,,Japan,late Edo period (1615–1868),,,,Artist,,Eiraku Wazen,"Japanese, 1821–1896",,Eiraku Wazen,Japanese,1821,1896,,1615,1868,Stoneware with gilt and polychrome enamels (Kyoto ware),H. 2 3/4 in. (7 cm); Diam. 4 7/8 in. (12.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/52308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.254,false,true,57244,Asian Art,Hanging scroll,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1891,Hanging scroll; ink on silk,32 1/8 x 9 3/8 in. (81.6 x 23.8 cm),"Gift of Mr. and Mrs. Harold G. Henderson, 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.92.1,false,true,57310,Asian Art,Fan,,Japan,,,,,Artist,,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,Japanese,1476,1559,,1466,1569,Color on paper,7 3/4 x 19 1/8 in. (19.7 x 48.6 cm),"Purchase, Harris Brisbane Dick Fund, 1938",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.92.2,false,true,57311,Asian Art,Fan,,Japan,,,,,Artist,,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,Japanese,1476,1559,,1466,1569,Color on paper,8 1/4 x 19 3/4 in. (21 x 50.2 cm),"Purchase, Harris Brisbane Dick Fund, 1938",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.92.3,false,true,57312,Asian Art,Fan,,Japan,,,,,Artist,,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,Japanese,1476,1559,,1466,1569,Color on paper,7 11/16 x 19 in. (19.5 x 48.3 cm),"Purchase, Harris Brisbane Dick Fund, 1938",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.92.4,false,true,57313,Asian Art,Fan,,Japan,,,,,Artist,,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,Japanese,1476,1559,,1466,1569,Color on paper,8 x 19 1/4 in. (20.3 x 48.9 cm),"Purchase, Dick Fund, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.116,false,true,44625,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Gessen,1721–1809,,Gessen,Japanese,1721,1809,,1721,1809,Hanging scroll; ink and color on silk,41 9/16 x 14 1/4 in. (105.5 x 36.2 cm),"Gift of Akiko Kobayashi Bowers, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.446,false,true,54897,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,School of,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1618,1694,Hanging scroll; color on paper,Painting only: 30 3/4 x 16 1/2 in. (78.1 x 41.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.454,false,true,40349,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Hishikawa Moronobu,"Japanese, died 1694",,Hishikawa Moronobu,Japanese,,1694,,1618,1694,Matted painting; ink and color on paper,Overall: 25 5/8 x 12 13/16 in. (65.1 x 32.5 cm) Image: 23 7/16 x 11 3/8 in. (59.5 x 28.9 cm) Mat: 29 3/8 x 17 1/4 in. (74.6 x 43.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.61,false,true,57173,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Sansetsu,"Japanese, 1590–1651",,Kano Sansetsu,Japanese,1590,1651,,1615,1651,Hanging scroll; ink on paper,29 1/8 x 9 3/8 in. (74 x 23.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.53,false,true,55380,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Naonobu,"Japanese, 1607–1650",,Kano Naonobu,Japanese,1607,1650,,1607,1650,Hanging scroll; ink on paper,40 3/4 x 15 1/2 in. (103.5 x 39.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.54,false,true,55384,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Naonobu,"Japanese, 1607–1650",,Kano Naonobu,Japanese,1607,1650,,1607,1650,Hanging scroll; ink on paper,41 x 15 1/2 in. (104.1 x 39.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.63,false,true,54706,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Kano Naonobu,"Japanese, 1607–1650",,Kano Naonobu,Japanese,1607,1650,,1607,1650,Album leaf; ink and color on paper,10 x 13 5/8 in. (25.4 x 34.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.93,false,true,55292,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Yasunobu,"Japanese, 1613–1685",,Kano Yasunobu,Japanese,1613,1685,,1613,1685,Hanging scroll; ink on silk,32 1/4 x 16 in. (81.9 x 40.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.94,false,true,55293,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Yasunobu,"Japanese, 1613–1685",,Kano Yasunobu,Japanese,1613,1685,,1613,1685,Hanging scroll; ink and color on silk,47 1/8 x 18 3/4 in. (119.7 x 47.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.52,false,true,45327,Asian Art,Hanging scroll,秋の花に鶉図|Quail Under Autumn Flowers,Japan,Edo period (1615–1868),,,,Artist,,Tosa Mitsuoki,"Japanese, 1617–1691",,Tosa Mitsuoki,Japanese,1617,1691,,1617,1691,Hanging scroll; ink and color on silk,38 1/2 x 16 3/8 in. (97.8 x 41.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.86.1,false,true,57232,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,,1636,1713,One of a triptych of hanging scrolls; ink and color on paper,35 1/4 x 11 3/8 in. (89.5 x 28.9 cm),"Rogers Fund, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.86.2,false,true,72771,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,,1636,1713,One of a triptych of hanging scrolls; ink and color on paper,35 1/4 x 11 3/8 in. (89.5 x 28.9 cm),"Rogers Fund, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.48,false,true,55379,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,,1658,1716,Hanging scroll; color on paper,38 3/4 x 14 in. (98.4 x 35.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.117,false,true,48984,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,,1658,1716,Hanging scroll; ink and color on silk,11 3/4 x 16 1/2 in. (29.8 x 41.9 cm),"Rogers Fund, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.64,false,true,45329,Asian Art,Folding fan mounted as a hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Follower of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,,1658,1716,Folding fan remounted as a hanging scroll; ink and color on paper,Image: 15 1/4 × 22 3/8 in. (38.7 × 56.8 cm) Overall with mounting: 51 1/8 × 27 3/4 in. (129.9 × 70.5 cm) Overall with knobs: 51 1/8 × 29 3/4 in. (129.9 × 75.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.3,false,true,55297,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katō Bunrei,"Japanese, 1706–1782",,Katō Bunrei,Japanese,1706,1782,,1706,1782,Hanging scroll; ink and color on silk,38 1/4 x 14 3/8 in. (97.2 x 36.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.84,false,true,49073,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sō Shiseki,"Japanese, 1715–1786",,Sō Shiseki,Japanese,1715,1786,,1715,1786,Hanging scroll; ink and color on silk,Image: 39 1/16 x 11 3/16 in. (99.2 x 28.4 cm) Overall: 69 x 18 7/8in. (175.3 x 47.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.96,false,true,48995,Asian Art,Hanging scroll,与謝蕪村筆 柳緑桃紅図|Birds in Willows and Blossoming Peach Tree,Japan,Edo period (1615–1868),,,,Artist,,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,Japanese,1716,1783,,1716,1783,Hanging scroll; ink and color on silk,51 1/2 x 24 9/16 in. (130.8 x 62.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.102,false,true,45394,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okada Beisanjin,"Japanese, 1744–1820",,Okada Beisanjin,Japanese,1744,1820,,1744,1820,Hanging scroll; ink and color on paper,Image: 53 11/16 x 11 9/16 in. (136.4 x 29.4 cm) Overall: 72 3/4 x 18 3/8in. (184.8 x 46.7cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.74,false,true,57178,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Mori Sosen,"Japanese, 1747–1821",,Mori Sosen,Japanese,1747,1821,,1747,1821,Hanging scroll; ink on silk,40 3/8 x 12 1/2 in. (102.6 x 31.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.75,false,true,57179,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Mori Sosen,"Japanese, 1747–1821",,Mori Sosen,Japanese,1747,1821,,1747,1821,Hanging scroll; ink on silk,40 3/8 x 12 1/2 in. (102.6 x 31.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.18,false,true,45222,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Matsumura Goshun,"Japanese, 1752–1811",,Matsumura Goshun,Japanese,1752,1811,,1752,1811,Hanging scroll ;ink on paper,14 15/16 x 21 7/16 in. (38 x 54.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.20,false,true,55304,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Matsumura Goshun,"Japanese, 1752–1811",,Matsumura Goshun,Japanese,1752,1811,,1752,1811,Hanging scroll; ink on silk,16 1/8 x 10 3/8 in. (41.0 x 26.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.59,false,true,55385,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Nagasawa Rosetsu,"Japanese, 1754–1799",,Nagasawa Rosetsu,Japanese,1754,1799,,1754,1799,Hanging scroll; color on paper,37 5/8 x 11 in. (95.6 x 27.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.23,false,true,55305,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,,1761,1828,Hanging scroll; ink and color on paper,34 1/4 x 13 in. (87x 33 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.24,false,true,55310,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,,1761,1828,Hanging scroll; ink and color on silk,44 1/4 x 16 in. (112.4 x 40.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.25,false,true,55314,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Sakai Hōitsu,"Japanese, 1761–1828",,Sakai Hōitsu,Japanese,1761,1828,,1761,1828,Hanging scroll; ink and color on paper,44 5/8 x 20 1/4 in. (113.3 x 51.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.1,false,true,55300,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,Japanese,1763,1840,,1763,1840,Hanging scroll; ink on silk,Image: 35 13/16 × 8 7/16 in. (91 × 21.5 cm) Overall with mounting: 67 11/16 × 16 7/8 in. (172 × 42.9 cm) Overall with knobs: 67 11/16 × 18 3/4 in. (172 × 47.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.2,false,true,55301,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,Japanese,1763,1840,,1763,1840,Hanging scroll; ink on silk,Image: 35 13/16 × 8 1/2 in. (91 × 21.6 cm) Overall with mounting: 68 1/8 × 16 7/8 in. (173 × 42.9 cm) Overall with knobs: 68 1/8 × 18 3/4 in. (173 × 47.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.214,false,true,53707,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nagasawa Roshu,"Japanese, 1767–1847",,Nagasawa Roshu,Japanese,1767,1847,,1767,1847,Hanging scroll; ink and color on silk,34 7/8 x 14 1/8 in. (88.6 x 35.9 cm),"Anonymous Gift, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.109,false,true,49027,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yamamoto Baiitsu,"Japanese, 1783–1856",,Yamamoto Baiitsu,Japanese,1783,1783,,1783,1856,Hanging scroll; color on silk,Image: 57 1/2 x 28 3/16 in. (146.1 x 71.6 cm) Overall: 88 3/4 x 37 1/4 in. (225.4 x 94.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.97,false,true,55294,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yosai,"Japanese, 1788–1878",,Yosai,Japanese,1788,1878,,1788,1868,Hanging scroll; ink and color on silk,43 1/4 x 15 3/4 in. (109.9 x 40 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.661.1,false,true,53824,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ōtagaki Rengetsu,"Japanese, 1791–1871",,Ōtagaki Rengetsu,Japanese,1791,1871,,1791,1871,Hanging scroll; ink and color on paper,44 1/16 x 10 3/4 in. (111.9 x 27.3 cm),"Gift of Donald Keene, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/53824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.661.2,false,true,59004,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ōtagaki Rengetsu,"Japanese, 1791–1871",,Ōtagaki Rengetsu,Japanese,1791,1871,,1791,1871,Hanging scroll; ink and color on paper,44 1/16 x 10 3/4 in. (111.9 x 27.3 cm),"Gift of Donald Keene, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/59004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.661.3,false,true,59005,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ōtagaki Rengetsu,"Japanese, 1791–1871",,Ōtagaki Rengetsu,Japanese,1791,1871,,1791,1871,Hanging scroll; ink and color on paper,44 1/16 x 10 3/4 in. (111.9 x 27.3 cm),"Gift of Donald Keene, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/59005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.115,false,true,49007,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,,1801,1854,Hanging scroll; color on paper,Image: 55 1/2 x 14 5/8 in. (141 x 37.1 cm) Overall with mounting: 85 3/4 x 24 1/4 in. (217.8 x 61.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.116,false,true,49008,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,,1801,1854,Hanging scroll; color on paper,Image: 56 7/8 x 14 13/16 in. (144.5 x 37.6 cm) Overall with mounting: 85 1/4 x 24 3/16 in. (216.5 x 61.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.114,false,true,57199,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on gold paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.115,false,true,57200,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; brown and gold lacquer on silver paper,3 1/2 x 4 3/4 in. (8.9 x 12.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.116,false,true,57201,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on gold paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.117,false,true,57202,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on silver paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.118,false,true,57203,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on gold paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.119,false,true,57204,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on silver paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.120,false,true,57205,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on gold paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.121,false,true,57206,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on silver paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.122,false,true,57207,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on gold paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.123,false,true,57208,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on silver paper,4 3/4 x 3 1/2 in. (12.1 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.124,false,true,57209,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.125,false,true,57210,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.126,false,true,57211,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.127,false,true,57212,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.128,false,true,57213,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.129,false,true,57214,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.130,false,true,57215,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.131,false,true,57216,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.132,false,true,57217,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.133,false,true,57218,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.134,false,true,57219,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.135,false,true,57220,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.136,false,true,57221,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.137,false,true,57222,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.138,false,true,57223,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,,1807,1868,Album leaf; lacquer on paper,4 1/2 x 3 1/2 in. (11.4 x 8.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.4,false,true,55298,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Shiokawa Bunrin,"Japanese, 1808–1877",,Shiokawa Bunrin,Japanese,1808,1877,,1808,1877,Hanging scroll; ink and color on silk,12 x 16 1/4 in. (30.5 x 41.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.210,false,true,54037,Asian Art,Folding fan,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Isai,"Japanese, 1821–1880",,Katsushika Isai,Japanese,1821,1880,,1821,1868,"Folding fan; ink and color on paper, with mounting in ivory",H. 16 1/8 in. (41 cm); W. 29 1/2 in. (75 cm),"Purchase, Friends of Asian Art Gifts, in honor of Wen C. Fong, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.561.1,false,true,61942,Asian Art,Folding fan mounted as hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Isai,"Japanese, 1821–1880",,Katsushika Isai,Japanese,1821,1880,,1821,1868,Folding fan remounted as a hanging scroll; ink and color on silk,7 3/4 x 27 in. (19.7 x 68.6 cm),"Gift of Rosemarie and Leighton Longhi, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/61942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.561.2,false,true,64505,Asian Art,Folding fan mounted as hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Isai,"Japanese, 1821–1880",,Katsushika Isai,Japanese,1821,1880,,1821,1868,"Folding fan, remounted as a hanging scroll; ink and color on silk",7 3/4 x 27 in. (19.7 x 68.6 cm),"Gift of Rosemarie and Leighton Longhi, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/64505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.122,false,true,45333,Asian Art,Folding fan,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Kakoku,"Japanese, 1823–1848",,Tsubaki Kakoku,Japanese,1823,1848,,1823,1848,Folding fan; ink and color on paper,10 1/4 x 17 3/8 in. (26 x 44.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.67,false,true,57177,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Shōkadō Shōjō,"Japanese, 1584?–1639",,Shōkadō Shōjō,Japanese,1584,1639,,1615,1639,Hanging scroll; ink on paper,10 1/4 x 23 1/8 in. (26 x 58.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.516,false,true,54967,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,,1754,1806,Painting; color on silk,34 3/4 x 14 1/4 in. (88.3 x 36.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.95,false,true,48994,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ike Gyokuran,"Japanese, 1727/8–1784",,Ike Gyokuran,Japanese,1727,1784,,1728,1784,Fan mounted as a hanging scroll; ink and color on paper,Image: 7 1/2 x 20 9/16 in. (19.1 x 52.3 cm) Overall with mounting: 52 1/4 x 30 3/4 in. (132.7 x 78.1 cm) Overall with knobs: 52 1/4 x 33 1/8 in. (132.7 x 84.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.511,false,true,54953,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,,1760,1849,Painting; color on paper,23 7/8 x 15 3/4 in. (60.6 x 40 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.526.1a–d,false,true,76806,Asian Art,Door panels,神坂雪佳筆 竹波図襖|Bamboo and Waves,Japan,Shōwa period (1926–89),,,,Artist,,Kamisaka Sekka,"Japanese, 1866–1942",,Kamisaka Sekka,Japanese,1866,1942,,1926,1942,Set of four sliding-door panels (fusuma); ink and gold on paper,Overall (for four panels): 68 1/2 x 190 3/8 in. (174 x 483.6 cm),"Gift of Gitter-Yelen Foundation, in honor of Maxwell K. Hearn, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/76806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.526.2,false,true,76924,Asian Art,Hanging scroll,神坂雪佳筆 寿老人図|Jurōjin,Japan,Meiji period (1866–1912),,,,Artist,,Kamisaka Sekka,"Japanese, 1866–1942",,Kamisaka Sekka,Japanese,1866,1942,,1866,1912,Hanging scroll; ink and color on silk,Image: 48 1/2 x 16 7/16 in. (123.2 x 41.8 cm) Overall with mounting: 84 x 22 in. (213.4 x 55.9 cm) Overall with knobs: 85 1/4 x 24 1/8 in. (216.5 x 61.3 cm),"Gift of Gitter-Yelen Foundation, in honor of John T. Carpenter, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/76924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.49,false,true,54686,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Gahō,"Japanese, 1835–1908",,Hashimoto Gahō,Japanese,1835,1908,,1868,1912,Album leaf; ink and color on silk,14 1/8 x 10 3/8 in. (35.9 x 26.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.50,false,true,54687,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Hashimoto Gahō,"Japanese, 1835–1908",,Hashimoto Gahō,Japanese,1835,1908,,1868,1912,Album leaf; ink and color on silk,14 1/8 x 10 3/8 in. (35.9 x 26.4 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.50,false,true,57165,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Tomioka Tessai,"Japanese, 1836–1924",,Tomioka Tessai,Japanese,1836,1924,,1836,1924,Hanging scroll; ink and color on paper,11 1/2 x 26 3/8 in. (29.2 x 67 cm),"Gift of Dr. and Mrs. Joseph Kurstin, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.72,false,true,57166,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Suzuki Shōnen,"Japanese, 1849–1918",,Suzuki Shōnen,Japanese,1849,1918,,1868,1912,"Hanging scroll; ink, color, and gold on silk",Image: 48 3/4 x 19 1/2 in. (123.8 x 49.5 cm) Overall with mounting: 79 1/2 x 25 3/4 in. (201.9 x 65.4 cm) Overall with knobs: 79 1/4 x 28 in. (201.3 x 71.1 cm),"Purchase, Gift of Mrs. Russell Sage, by exchange, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.62,false,true,57174,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Takeuchi Seihō,"Japanese, 1864–1942",,Takeuchi Seihō,Japanese,1864,1942,,1868,1912,Hanging scroll; ink and color on paper,Overall: 40 3/4 x 13 1/2in. (103.5 x 34.3cm) Overall with mounting: 71 x 14 3/4 in. (180.3 x 37.5 cm) Overall with knobs: 71 x 17 1/8 in. (180.3 x 43.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.460,false,true,36040,Asian Art,Hanging scroll,,Japan,Momoyama period (1573–1615),,,,Artist,In the Style of,Hasegawa Tōhaku,"Japanese, 1539–1610",,Hasegawa Tōhaku,Japanese,1539,1610,,1573,1615,Hanging scroll; ink and color on silk,28 1/2 x 22 31/32 in. (72.4 x 58.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.31,false,true,55324,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,In the Style of,Ikkyu Sojun,"Japanese, 1394–1481",,Ikkyu Sojun,Japanese,1394,1481,,1392,1573,Hanging scroll; ink on paper,34 1/4 x 10 1/4 in. (87 x 26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.8,false,true,54567,Asian Art,Painting,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Sesshū Tōyō,"Japanese, 1420–1506",,Sesshū Tōyō,Japanese,1420,1506,,1420,1506,India ink on paper,Image: 10 1/2 × 15 3/4 in. (26.7 × 40 cm) Overall with mounting: 44 7/8 × 20 1/2 in. (114 × 52.1 cm) Overall with knobs: 44 7/8 × 22 3/8 in. (114 × 56.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.10,false,true,45642,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Gakuo Zokyu,"Japanese, active ca. 1500",,Gakuo Zokyu,Japanese,1500,1500,,1490,1510,Hanging scroll; ink on paper,27 1/2 x 12 1/2 in. (69.9 x 31.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.65,false,true,57176,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Shūkō,"Japanese, active 1504–20",,Shūkō,Japanese,1504,1520,,1504,1520,Hanging scroll; ink on paper,40 1/2 x 12 in. (102.9 x 30.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.450,false,true,45696,Asian Art,Painting,,Japan,Muromachi period (1392–1573),,,,Artist,,Isei,"Japanese, mid-16th century",,Isei,Japanese,1536,1570,,1534,1566,Framed painting; ink and color on paper,18 1/2 x 12 1/2 in. (47 x 31.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.451,false,true,45697,Asian Art,Painting,,Japan,Muromachi period (1392–1573),,,,Artist,,Isei,"Japanese, mid-16th century",,Isei,Japanese,1536,1570,,1534,1573,Framed painting; ink and color on paper,18 1/2 x 12 1/2 in. (47 x 31.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.26,false,true,45202,Asian Art,Hanging scroll,不動明王二童子像|Fudō Myōō and Two Attendants,Japan,Nanbokuchō period (1336–92),,,,Artist,After,Ryūshū Shūtaku (Myōtaku),"Japanese, 1307–1388",,Ryūshū Shūtaku,Japanese,1307,1388,,1336,1388,One of a triptych of hanging scrolls; hand-colored woodblock print on paper,Image: 40 5/16 x 14 in. (102.4 x 35.6 cm) Overall: 71 1/8 x 22 1/2 in. (180.7 x 57.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.27,false,true,45203,Asian Art,Hanging scroll,不動明王二童子像|Fudō Myōō and Two Attendants,Japan,Nanbokuchō period (1336–92),,,,Artist,After,Ryūshū Shūtaku (Myōtaku),"Japanese, 1307–1388",,Ryūshū Shūtaku,Japanese,1307,1388,,1336,1388,One of a triptych of hanging scrolls; hand-colored woodblock print on paper,Image: 40 1/4 x 14 in. (102.3 x 35.6 cm) Overall: 71 x 22 1/2 in. (180.3 x 57.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.28,false,true,45204,Asian Art,Hanging scroll,不動明王二童子像|Fudō Myōō and Two Attendants,Japan,Nanbokuchō period (1336–92),,,,Artist,After,Ryūshū Shūtaku (Myōtaku),"Japanese, 1307–1388",,Ryūshū Shūtaku,Japanese,1307,1388,,1336,1388,One of a triptych of hanging scrolls; hand-colored woodblock print on paper,Image: 40 3/8 x 14 in. (102.6 x 35.6 cm) Overall: 71 3/8 x 22 1/2 in. (181.3 x 57.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.35,false,true,55353,Asian Art,Hanging scroll,,Japan,Nanbokuchō (1336–92)–Muromachi (1392–1573) period,,,,Artist,Attributed to,Kao,,(Ryozen?),Kao,Japanese,,1345,,1336,1573,Hanging scroll; ink on silk,33 x 14 1/4 in. (83.8 x 36.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB53,false,true,57653,Asian Art,Illustrated book,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,1888,1888,1888,Album of 29 leaves; ink and color on paper,8 7/8 x 5 7/8 in. (22.5 x 14.9 cm),"Gift of Albert Gallatin, 1922",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3199,false,true,55183,Asian Art,Print,『上野公園開花図』|Blossoming Cherry Trees in Ueno Park (Ueno kōen kaika zu),Japan,Meiji period (1868–1912),,,,Artist,,Yōshū (Hashimoto) Chikanobu,"Japanese, 1838–1912",,Yōshū (Hashimoto) Chikanobu,Japanese,1838,1912,1888,1888,1888,Triptych of polychrome woodblock prints; ink and color on paper,Oban 14 3/4 x 9 7/8 in. (37.5 x 25.1 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3687a,false,true,55918,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1888,1888,1888,Polychrome woodblock print; ink and color on paper,14 x 9 3/8 in. (35.6 x 23.8 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3687b,false,true,55919,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1888,1888,1888,Polychrome woodblock print; ink and color on paper,14 9/16 x 10 in. (37 x 25.4 cm),"Gift of Lincoln Kirstein, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.349,false,true,72827,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,1888,1888,1888,Polychrome woodblock print; ink and color on paper,14 5/8 x 10 in. (37.1 x 25.4 cm),"Purchase, Friends of Asian Art Gifts, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/72827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3307a–c,false,true,55411,Asian Art,Print,九段坂上靖国神社庭内真図|True View of the Courtyard of Yasukuni Shrine at Kudan Sakaue (Kudan Sakaue Yasukuni jinsha teinai shin zu),Japan,Meiji period (1868–1912),,,,Artist,,Inoue Yasuji,"Japanese, 1864–1889",,INOUE YASUJI,Japanese,1864,1889,1888,1888,1888,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 x 29 1/2 in. (35.6 x 74.9 cm),"Gift of Lincoln Kirstein, 1959",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.61.51,false,true,54688,Asian Art,Album leaf,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,ca. 1888,1878,1898,Album leaf mounted as a hanging scroll; ink and color on silk,14 1/2 x 11 in. (36.8 x 27.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.13,false,true,45692,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,Japanese,1476,1559,15th century,1400,1499,Hanging scroll; ink on paper,25 1/8 x 12 1/2 in. (63.8 x 31.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.244.16,false,true,45072,Asian Art,Screen,,Japan,Muromachi period (1392–1573),,,,Artist,,Sesson Shūkei,ca. 1504–ca. 1589,,Sesson Shūkei,Japanese,1504,1589,16th century,1500,1599,Six-panel folding screen; ink on paper,60 1/2 in. x 11 ft. 8 1/4 in. (153.7 x 356.2 cm),"Bequest of Hope Skillman Schary, 1981",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.100.495g, h",false,true,45219,Asian Art,Hanging scroll,,Japan,Momoyama period (1573–1615),,,,Artist,In the Style of,Kano Eitoku,"Japanese, 1534–1590",,Kano Eitoku,Japanese,1543,1590,16th century,1534,1590,"Hanging scroll; ink, color, and gold on gilded paper",61 1/8 x 45 1/2in. (155.3 x 115.6cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.95,false,true,45070,Asian Art,Painting,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Kano Motonobu,"Japan, ca. 1476–1559",,Kano Motonobu,Japanese,1476,1559,16th century,1500,1573,Framed painting; ink on paper,23 1/4 x 42 in. (59.1 x 106.7 cm),"Fletcher Fund,1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.41,false,true,42342,Asian Art,Hanging scroll,山水図|Landscape,Japan,Muromachi period (1392–1573),,,,Artist,,Maejima Sōyū,active mid-16th century,,Maejima Sōyū,Japanese,1536,1570,16th century,1500,1573,Hanging scroll; ink and color on paper,Image: 20 1/4 × 13 5/8 in. (51.4 × 34.6 cm) Overall with mounting: 55 3/4 × 18 3/4 in. (141.6 × 47.6 cm) Overall with knobs: 55 3/4 × 20 3/4 in. (141.6 × 52.7 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.10,false,true,45374,Asian Art,Fan mounted as a hanging scroll,伝狩野之信 列子御風図扇面|Resshi Flying on a Cloud,Japan,Muromachi period (1392–1573),,,,Artist,,Kano Yukinobu,"Japanese, ca. 1513–1575",,Kano Yukinobu,Japanese,1513,1575,16th century,1513,1575,Fan mounted as hanging scroll; ink on paper,8 1/4 x 20 1/2 in. (21 x 52 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.140,false,true,39487,Asian Art,Fan mounted as hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Kano Yukinobu,"Japanese, ca. 1513–1575",,Kano Yukinobu,Japanese,1513,1575,16th century,1500,1573,"Originally a fan mounted as a hanging scroll; ink, color, and silver on gilded paper",9 3/4 x 19 1/2 in. (24.8 x 49.5 cm),"Purchase, Friends of Asian Art Gifts, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.91,false,true,45206,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,Attributed to,Kano Yukinobu,"Japanese, ca. 1513–1575",,Kano Yukinobu,Japanese,1513,1575,16th century,1513,1575,Hanging scroll; ink on paper,39 1/8 x 16 3/4 in. (99.4 x 42.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.92,false,true,45207,Asian Art,Hanging scroll,,Japan,Muromachi period (1392–1573),,,,Artist,,Kano Yukinobu,"Japanese, ca. 1513–1575",,Kano Yukinobu,Japanese,1513,1575,16th century,1513,1575,Hanging scroll; ink on paper,39 1/8 x 16 3/4 in. (99.4 x 42.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"53.156.1, .2",false,true,57346,Asian Art,Screen,,Japan,,,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,17th century,1600,1699,Pair of six-panel folding screens; paint and gilt on paper,Overall (each screen): 70 1/4 x 77 1/4 in. (178.4 x 196.2 cm),"Gift of Major General R. B. Woodruff, 1953",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/57346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"17.118.114, .115",false,true,45423,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Unkoku Tōeki,1591–1644,,Unkoku Tōeki,Japanese,1591,1644,17th century,1600,1699,Pair of six-panel screens; ink and gold on paper,Overall (each screen): 67 1/2 x 147 in. (171.5 x 373.4 cm),"Rogers Fund, 1917",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1972.179.1, .2",false,true,45082,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Kusumi Morikage,ca. 1620–1690,,Kusumi Morikage,Japanese,1610,1700,17th century,1600,1699,Pair of six-panel folding screens ; ink and light color on paper,Overall (.1): 68 3/4 x 146 3/8 in. (174.6 x 371.8 cm) Overall (.2): 69 x 146 3/8 in. (175.3 x 371.8 cm),"Purchase, Joseph Pulitzer Bequest, 1972",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.496,false,true,45651,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Sesson Shūkei,ca. 1504–ca. 1589,,Sesson Shūkei,Japanese,1504,1589,17th century,1600,1699,Two-panel folding screen; ink on paper,59 1/4 x 66 7/16 in. (150.5 x 168.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.215.53,false,true,45654,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Kano Eitoku,"Japanese, 1534–1590",,Kano Eitoku,Japanese,1543,1590,17th century,1600,1699,"Two-panel folding screen; ink, color, and gold on paper (obverse); ink and color on paper (reverse side)",22 x 73 1/2 in. (55.9 x 186.7 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.495a–f,false,true,45695,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Kano Eitoku,"Japanese, 1534–1590",,Kano Eitoku,Japanese,1543,1590,17th century,1600,1699,Six-panel folding screen; ink and color on gilt paper,65 1/4 x 146 1/4in. (165.8 x 371.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.100.491, .492",false,true,45341,Asian Art,Folding screens,伝三谷等宿筆 松と椿に鷹・柳と椿に小禽図屏風|Pine and Camellia with Hawks and Willow and Camellia with Small Birds,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Mitani Tōshuku,"Japanese, 1577–1654",,Mitani Tōshuku,Japanese,1577,1654,17th century,1600,1699,Pair of six-panel folding screens; ink and color on paper,Image (each): 58 7/16 in. x 10 ft. 8 13/16 in. (148.5 x 327.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.719.11,false,true,60485,Asian Art,Hanging scroll,清巌宗渭 「地獄」|Hell,Japan,Edo period (1615–1868),,,,Artist,,Seigan Sōi,"Japanese, 1588–1661",,Seigan Sōi,Japanese,1588,1661,17th century,1600,1699,Hanging scroll; ink on paper,Image: 12 x 35 1/2 in. (30.5 x 90.2 cm),"Gift of Sylvan Barnet and William Burto, in honor of Setsu Isao, 2014",,,,,,,,,,,,Calligraphy,,http://www.metmuseum.org/art/collection/search/60485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.260.1, .2",false,true,75372,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Kano Einō,"Japanese, 1631–1697",,Kano Einō,Japanese,1631,1697,17th century,1600,1699,"Pair of six-panel folding screens; ink, color, and gold on paper",Image (each screen): 44 7/8 x 111 in. (114 x 282 cm),"Purchase, Lila Acheson Wallace Gift, Mary and James G. Wallach Foundation Gift, Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, and Dodge Fund, 2009",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/75372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.89,false,true,45373,Asian Art,Screen,,Japan,Momoyama period (1573–1615),,,,Artist,,Hasegawa Tōchō,"Japanese, active 1624–43",,Hasegawa Tōcho,Japanese,1624,1643,17th century,1600,1699,Six-panel screen; ink on paper,58 1/4 x 121 3/4 in. (147.9 x 309.2 cm),"Fletcher Fund, 1933",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/45373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.120.643a, b",false,true,63316,Asian Art,Incense burner,,Japan,,,,,Artist,,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,17th century,1600,1699,"Square with sunken panels on all sides and bottom; four moulded feet; light brown clay; yellow glaze; decoration of flowers, grasses and butterflies in green and dark blue; perforated iron cover (Kyoto ware)",H. 4 1/4 in. (10.8 cm); W. sq. 5 in. (12.7 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"66.155.6a, b",false,true,53214,Asian Art,Incense burner with lid,,Japan,Edo period (1615–1868),,,,Artist,,Jokei,"Japanese, died 1636",,Jokei,Japanese,1536,1636,17th century,1600,1700,Earthenware with glaze; lid of network brass grille (Raku ware),H. 4 1/4 in. (10.8 cm); Gr. Diam. 4 1/4 in. (10.8 cm),"Gift of Harold G. Henderson, 1966",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.628,false,true,63111,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Hon'ami Kōetsu,"Japanese, 1558–1637",,Hon'ami Kōetsu,Japanese,1558,1637,17th century,1600,1699,Clay; crackled glaze with markings; (Kyoto ware),H. 3 3/4 in. (9.5 cm); Diam. 4 7/8 in. (12.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.320,false,true,39576,Asian Art,Water jar,,Japan,Edo period (1615–1868),,,,Artist,,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,17th century,1600,1699,Pottery covered with glaze; handles at sides (Kyoto ware),H. 6 1/3 in. (16.1 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/39576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.29,false,true,54593,Asian Art,Hanging scroll,,Japan,,,,,Artist,In the Style of,Kano Sanraku,"Japanese, 1559–1635",,Kano Sanraku,Japanese,1559,1635,17th century,1600,1699,Hanging scroll; monochrome on silk,40 1/2 x 21 in. (102.9 x 53.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.47,false,true,57228,Asian Art,Hanging scroll,,Japan,,,,,Artist,,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,17th century,1602,1674,Hanging scroll; ink on paper,5 1/4 x 19 3/8 in. (13.3 x 49.2 cm),"Rogers Fund, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.35,false,true,54594,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Kano Tan'yū,"Japanese, 1602–1674",,Kano Tan'yū,Japanese,1602,1674,17th century,1600,1699,Hanging scroll; india ink on paper,48 x 21 1/8 in. (121.9 x 53.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/54594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.118.2,false,true,57336,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Kano Naonobu,"Japanese, 1607–1650",,Kano Naonobu,Japanese,1607,1650,17th century,1607,1650,Hanging scroll; ink on paper,38 3/8 x 11 3/16 in. (97.5 x 28.4 cm),"Rogers Fund, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.5,false,true,45702,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1799,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.6,false,true,45703,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.7,false,true,45704,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.8,false,true,45705,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.9,false,true,45706,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.10,false,true,658681,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1799,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/658681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.11,false,true,45708,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.12,false,true,45709,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.13,false,true,45710,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.14,false,true,45711,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.15,false,true,45712,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.16,false,true,45713,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.17,false,true,45714,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.18,false,true,45715,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.19,false,true,45716,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.20,false,true,45717,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.21,false,true,45718,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.140.22,false,true,45719,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Kano Shōun,1637–1702,,Kano Shōun,Japanese,1637,1702,17th century,1600,1699,Unmounted shikisi leaf; ink and color on silk,7 1/4 × 6 1/2 in. (18.4 × 16.5 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.56,false,true,45731,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kusumi Morikage,ca. 1620–1690,,Kusumi Morikage,Japanese,1610,1700,17th century,1620,1690,Hanging scroll; ink on paper,48 9/16 x 20 7/8 in. (123.4 x 53.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.80,false,true,45646,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Formerly attributed to,Sōami,"Japanese, died 1525",,Sōami,Japanese,,1525,17th century,1600,1699,Hanging scroll; ink on paper,17 3/4 x 10 1/2 in. (45.1 x 26.7 cm),"Gift of Mrs. John D. Rockefeller 3rd, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.72,false,true,45645,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Sōami,"Japanese, died 1525",,Sōami,Japanese,,1525,17th century,1600,1699,Hanging scroll; ink on paper,13 1/4 x 27 3/8 in. (33.7 x 69.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.529,false,true,45649,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Sesshū Tōyō,"Japanese, 1420–1506",,Sesshū Tōyō,Japanese,1420,1506,17th century,1615,1699,Hanging scroll; ink on paper,33 1/4 x 11 5/8 in. (84.5 x 29.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.224.1–.31,false,true,74462,Asian Art,Album leaf,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tsunenobu,"Japanese, 1636–1713",,Kano Tsunenobu,Japanese,1636,1713,17th century,1636,1699,Album leaf; ink and color on silk,11 7/16 x 16 3/4 in. (29 x 42.5 cm),"Gift of Mr. and Mrs. Harry Rubin, 1974",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/74462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.22.1,false,true,57339,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Matsuo Basho,"Japanese, 1644–1694",,Matsuo Basho,Japanese,1644,1694,17th century,1644,1694,Ink wash and color on paper,43 x 11 1/2 in. (109.2 x 29.2 cm),"Gift of Dr. Robert Pollak, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.440,false,true,48971,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Tawaraya Sōtatsu,"Japanese, died ca. 1640",,Tawaraya Sōtatsu,Japanese,1540,1640,17th century,1600,1640,Hanging scroll; ink and color on paper,33 7/8 x 17 1/8 in. (86 x 43.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.464,false,true,48972,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,School of,Tawaraya Sōtatsu,"Japanese, died ca. 1640",,Tawaraya Sōtatsu,Japanese,1540,1640,17th century,1600,1650,Hanging scroll; ink and color on paper,49 3/4 x 18 1/4 in. (126.4 x 46.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.68,false,true,45641,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Shubun,"Japanese, active ca. 1414",,Shubun,Japanese,1414,1414,17th century,1600,1699,Hanging scroll; ink and color on paper,25 3/4 x 12 1/8 in. (65.4 x 30.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.79,false,true,49067,Asian Art,Hanging scroll,即非如一筆 墨蹟|Reading a Sutra by Moonlight,Japan,Edo period (1615–1868),,,,Artist,,Sokuhi Nyoitsu (Jifei Ruyi),"Chinese/ Japanese, 1616–1671",,Sokuhi Nyoitsu,Japanese,1616,1671,17th century,1616,1671,Hanging scroll; ink on paper,Image: 10 13/16 x 23 7/8 in. (27.5 x 60.6 cm) Overall with mounting: 44 1/4 × 24 5/8 in. (112.4 × 62.5 cm) Overall with knobs: 44 1/4 × 26 3/4 in. (112.4 × 68 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"51.132.1a, b",false,true,58328,Asian Art,Coal container,,Japan,,,,,Artist,School of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,18th century,1700,1799,"Wood, metal, mother-of-pearl, lacquer",H. (with cover) 13 3/4 in. (34.9 cm); Diam. 16 1/4 in. (41.3 cm),"Gift of Mrs. David Randall-MacIver and Mrs. Natalie Tuttle Martin in memory of her husband Dr. George Montgomery Tuttle, 1951",,,,,,,,,,,,Woodwork,,http://www.metmuseum.org/art/collection/search/58328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.132.2a–c,false,true,56533,Asian Art,Coal container,,Japan,,,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1799,"Wood, metal, mother-of-pearl",H. (w/cover and base) 14 in. (35.6 cm); Diam. (grt.) 13 in. (33 cm),"Gift of Mrs. David Randall-MacIver and Mrs. Natalie Tuttle Martin in memory of her husband, Dr. George Montgomery Tuttle, 1951",,,,,,,,,,,,Woodwork,,http://www.metmuseum.org/art/collection/search/56533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.117.1,false,true,57108,Asian Art,Printer's woodblock,,Japan,,,,,Artist,Original illustration designed by,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,18th century,1700,1799,Wood,10 5/8 x 9 11/16 in. (27 x 24.6 cm),"Rogers Fund, 1927",,,,,,,,,,,,Woodblocks,,http://www.metmuseum.org/art/collection/search/57108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.192,false,true,59880,Asian Art,Netsuke,,Japan,,,,,Artist,Formerly Attributed to,Garaku,"Japanese, active second half of the 18th century",,Garaku,Japanese,1750,1799,18th century,1700,1799,Ivory,H. 13/16 in. (2.1 cm); W. 1 1/2 in. (3.8 cm); L. 1 13/16 in. (4.6 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.226,false,true,58914,Asian Art,Inrō,古墨形鞘印籠 (宝露臺)|Inrō Imitating an Old Chinese Ink Cake,Japan,Edo period (1615–1868,,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1799,"Two-part (sheath-type); lacquered wood, metal, leather, with black and brown togidashimaki-e, takamaki-e on black ground Netsuke: manjū type with maki-e Daruma Ojime: ceramic bead with incense and ash design",3 1/16 x 2 1/16 x 11/16 in. (7.7 x 5.3 x 1.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.528,false,true,49082,Asian Art,Screen,,Japan,Edo period (1615–1868),,,,Artist,,Miwa Zaiei,died 1789,,Miwa Zaiei,Japanese,,1789,18th century,1700,1799,"Two-panel folding screen; ink, color, and gold on paper",40 x 49 1/4 in. (101.6 x 125.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/49082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.2325,false,true,59383,Asian Art,Netsuke,,Japan,Edo period (1615–1868),,,,Artist,,Sanshō,1871–1936,,Sanshō,Japanese,1871,1936,18th century,1700,1799,Wood with ivory insert,H. 2 3/4 in. (7 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.206,false,true,53936,Asian Art,Inrō,古満休伯作 群鶴蒔絵印籠|Inrō with Cranes and Pines,Japan,Edo period (1615–1868),,,,Artist,,Koma Kyūhaku V,"Japanese, died 1794",,Koma Kyūhaku V,Japanese,,1794,18th century,1701,1800,"Gold togidashi lacquer ground with gold and silver takamaki-e and hiramaki-e, and black and red lacquer Netsuke: hat and mask; lacquer on wood Ojime: jade bead",H. 3 1/16 in. (7.7 cm); W. 2 3/16 in. ( 5.5 cm); D. 7/8 in. (2.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/53936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.72,false,true,45462,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,After,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,18th century,1700,1799,Gold lacquer with mother-of-pearl and pewter inlay Ojime: carnelian bead Netsuke: carved wood deer,2 1/16 x 1 15/16 x 3/4 in. (5.3 x 4.9 x 1.9 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.229,false,true,45457,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,After,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,18th century,1700,1799,Gold maki-e and mother-of-pearl inlay Ojime: wood-and-gilt bead in shape of sake bottle Netsuke: ivory with lacquer design of toys,H. 2 3/8 in. (6.1 cm); W. 2 3/16 in. (5.5 cm); D. 13/16 in. (2.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.139a–d,false,true,57925,Asian Art,Writing box,,Japan,Edo period (1615–1868),,,,Artist,Formerly Attributed to,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,18th century,1700,1799,Lacquer with pewter and gold,H. 1 5/8 in. (4.1 cm); W. 8 1/4 in. (21 cm); L. 9 in. (22.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/57925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.703,false,true,44867,Asian Art,Writing box,鼠扇蒔絵象嵌硯箱|Writing Box (suzuribako) with Mice and Fan,Japan,Edo period (1615–1868),,,,Artist,School of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1799,"Lacquered wood with gold, silver hiramaki-e, ceramic, mother-of-pearl, and pewter inlays on wood ground",H. 1 3/4 in. (4.4 cm); W. 8 1/8 in. (20.6 cm); L. 10 1/8 in. (25.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/44867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.151,false,true,58335,Asian Art,Panel,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1799,Lacquered wood,L. 10 3/4 in. (27.3 cm); W. 14 3/4 in. (37.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.146a, b",false,true,57937,Asian Art,Box,,Japan,Edo period (1615–1868),,,,Artist,,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1799,Lacquer,H. 4 5/8 in. (11.7 cm); L. 9 3/8 in. (23.8 cm); W. 8 3/4 in. (22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/57937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.141a–e,false,true,44940,Asian Art,Writing box,,Japan,Edo period (1615–1868),,,,Artist,Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1799,Gold and glazed pottery on colored lacquer inlaid with mother-of-pearl,H. 1 1/2 in. (3.8 cm); W. 7 in. (17.8 cm); L 9 1/2 in. (24.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/44940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.769a–c,false,true,78671,Asian Art,Illustrated books,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,18th century,1700,1764,Set of three woodblock printed books; ink and color on paper,each: 5 9/16 × 8 1/16 in. (14.2 × 20.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.188,false,true,44866,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Mochizuki Hanzan,"Japanese, 1743–?1790",,Mochizuki Hanzan,Japanese,1743,1790,18th century,1700,1799,"Case: powdered gold (maki-e) on lacquer with ceramic, lead, and mother-of-pearl inlays; ceramic seal on base; Fastener (ojime): alloy of silver and copper with design of mice in rice cakes; Toggle (netsuke): lacquer with design of lotus leaf and caterpillar in lead and ivory inlays",H. 3 in. (7.6 cm); W. 2 3/16 in. (5.5 cm); D. 13/16 in. (2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/44866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.145a, b",false,true,57929,Asian Art,Sutra chest,,Japan,Edo period (1615–1868),,,,Artist,,Mochizuki Hanzan,"Japanese, 1743–?1790",,Mochizuki Hanzan,Japanese,1743,1790,18th century,1700,1799,"Colored lacquer, gold, and ceramic on natural wood",H. 10 3/4 in. (27.3 cm); L. 17 3/4 in. (45.1 cm); W. 10 1/4 in. (26 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/57929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.147a–c,false,true,57939,Asian Art,Document box,,Japan,Edo period (1615–1868),,,,Artist,,Nagata Yūji,"Japanese, acive 1711–36",,Nagata Yūji,Japanese,1711,1736,18th century,1700,1799,Lacquer with gold and pewter,H. 6 in. (15.2 cm); W. 8 1/4 in. (21 cm); L. 10 3/4 in. (27.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/57939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1981,false,true,59141,Asian Art,Netsuke,虎木彫根付|Tiger,Japan,Edo period (1615–1868),,,,Artist,,Minkō,"Japanese, ca. 1735–1816",,Minkō,Japanese,1735,1816,18th century,1700,1799,Wood,H. 1 1/8 in. (2.9 cm); W. 1 5/8 in. (4.1 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.2278,false,true,59357,Asian Art,Netsuke,獏木彫根付|Crouching Baku (Mythical Creature),Japan,Edo period (1615–1868),,,,Artist,,Sadatake,"Japanese, active 18th century",,Sadatake,Japanese,1700,1799,18th century,1700,1799,Wood,H. 2 in. (5.1 cm); W. 2 1/4 in. (5.7 cm); D. 1 1/2 in. (3.8 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.2383,false,true,59420,Asian Art,Netsuke,ガルダ木彫根付|Garuda,Japan,Edo period (1615–1868),,,,Artist,,Tori,"Japanese, active 18th century",,Tori,Japanese,1700,1799,18th century,1700,1799,Wood; dark brown,H. 1 7/8 in. (4.8 cm); W. 1 5/8 in. (4.1 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.64,false,true,45586,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Tōyō,"Japanese, active ca. 1764–71",,Tōyō,Japanese,1764,1771,18th century,1700,1799,Aventurine lacquer with gold sprinkled and polished lacquer; Netsuke: Box with flowers; Ojime: coral bead,2 3/8 x 2 5/16 x 15/16 in. (6 x 5.8 x 2.4 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.341.2,false,true,45493,Asian Art,Noh mask,,Japan,Edo period (1615–1868),,,,Artist,,Genkyu Michinaga,"Japanese, active second half of the 17th century",,Genkyu Michinaga,Japanese,1650,1699,18th century,1700,1799,Painted wood,W. 5 1/2 in. (14 cm); L. 8 1/2 in. (21.6 cm),"Purchase, Lila Acheson Wallace Gift, 1993",,,,,,,,,,,,Masks,,http://www.metmuseum.org/art/collection/search/45493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1617,false,true,37333,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shigenaga,1697–1756,,Shigenaga,Japanese,1697,1756,18th century,1700,1756,Polychrome woodblock print; ink and color on paper,H. 5 3/4 in. (14.6 cm); W. 12 11/16 in. (32.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1618,false,true,37334,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Shigenaga,1697–1756,,Shigenaga,Japanese,1697,1756,18th century,1700,1756,Polychrome woodblock print; ink and color on paper,H. 5 1/4 in. (13.3 cm); W. 12 11/16 in. (32.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1589,false,true,55754,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,18th century,1700,1764,"""White–line"" woodblock print; ink and color on paper",H. 11 7/16 in. (29.1 cm); W. 5 1/2 in. (14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1628,false,true,55793,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,18th century,1764,1772,Polychrome woodblock print; ink and color on paper,H. 11 1/2 in. (29.2 cm); W. 8 1/4 in. (21 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1651,false,true,55810,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,18th century,1764,1772,Polychrome woodblock print; ink and color on paper,H. 10 7/8 in. (27.6 cm); W. 8 1/8 in. (20.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1656,false,true,55817,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,18th century,1764,1772,Polychrome woodblock print (hashira-e); ink and color on paper,H. 27 1/2 in. (69.9 cm); W. 4 13/16 in. (12.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.265,false,true,73556,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,18th century,1725,1770,Polychrome woodblock print; ink and color on paper,Image: 7 5/8 x 11 in. (19.4 x 27.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2694,false,true,39722,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1700,1799,Polychrome woodblock print; ink and color on paper,12 3/8 x 8 3/4 in. (31.4 x 22.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.266,false,true,73557,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,18th century,1789,1801,Polychrome woodblock print; ink and color on paper,Image: 9 7/8 x 14 3/4 in. (25.1 x 37.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.60.7,false,true,63067,Asian Art,Bowl,,Japan,Edo period (1615–1868),,,,Artist,,Seifu Yohei,1803–1861,,Seifu Yohei,Japanese,1803,1861,18th century,1700,1799,"White porcelain decorated with blue under the glaze, the inside partly unglazed (Kyoto ware)",H. 2 3/4 in. (7 cm); Diam. 5 1/8 in. (13 cm),"Rogers Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.534,false,true,63193,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Sonyu,"Japanese, died 1725",,Sonyu,Japanese,1625,1725,18th century,1700,1799,"Clay, thick black glaze, patch of reddish underglaze (Raku ware)",H. 5 1/4 in. (13.3 cm); Diam. 4 1/4 in. (10.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.27,false,true,62621,Asian Art,Cup,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,18th century,1700,1799,"Porcelain decorated in blue under the glaze, polychrome enamels over the glaze (Kenzan ware)",H. 2 1/4 in. (5.7 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.180,false,true,62690,Asian Art,Water pot,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,18th century,1700,1799,Clay covered with white and black glazes and decorated on the white parts under the glaze (Kenzan style),H. 3 1/2 in. (8.9 cm); Diam. 7 in. (17.8 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.115,false,true,62670,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Kiyomizu Rokubei I,"Japanese, 1737–1799",,Kiyomizu Rokubei I,Japanese,1737,1799,18th century,1700,1799,"Clay covered with glaze and overglaze (Kiyomizu ware, Awata type)",H. 3 5/8 in. (9.2 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.455,false,true,63796,Asian Art,Stem cup,,Japan,Edo period (1615–1868),,,,Artist,,Okuda Eisen,"Japanese, 1753–1811",,Okuda Eisen,Japanese,1753,1811,18th century,1700,1799,Porcelain painted with red and green enamels,H. 4 5/8 in. (11.7 cm); Diam. 6 5/8 in. (16.8 cm),"Gift of Mr. and Mrs. John R. Menke, 1979",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.447.115,false,true,49416,Asian Art,Octagonal bowl,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Shibuemon,"Japanese, active 17th century",,Shibuemon,Japanese,1600,1699,18th century,1700,1799,Porcelain with underglaze cobalt and enamels,Diam. 6 3/4 in. (17.2 cm),"Dr. and Mrs. Roger G. Gerry Collection, Bequest of Dr. and Mrs. Roger G. Gerry, 2000",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/49416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.139,false,true,62674,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,18th century,1700,1799,Clay covered with a warm Seto glaze and a border of white enamel (Awata ware),H. 1 7/8 in. (4.8 cm); Diam. 5 in. (12.7 cm); Diam. of foot 2 in. (5.1 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.90,false,true,44938,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kawamata Tsuneyuki,1676 (?)–1741,,Kawamata Tsuneyuki,Japanese,1676,1741,18th century,1700,1741,Hanging scroll; ink and color on paper,14 3/8 x 18 5/8 in. (36.5 x 47.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.125,false,true,45381,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kaigetsudō Dohan,active 1710–16,,Kaigetsudō Dohan,Japanese,1710,1716,18th century,1710,1716,Hanging scroll; ink and color on paper,Image: 32 3/16 x 13 3/16 in. (81.8 x 33.5 cm) Overall with mounting: 64 3/8 x 20 1/8 in. (163.5 x 51.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.34,false,true,49076,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kakutei,"Japanese, died 1785",,Kakutei,Japanese,,1785,18th century,1700,1785,Hanging scroll; ink and color on silk,29 1/4 x 14 1/2 in. (74.3 x 36.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.49,false,true,48975,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,18th century,1700,1716,Hanging scroll; ink and color on silk,43 5/16 x 16 1/4 in. (110 x 41.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.527,false,true,40351,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,18th century,1700,1743,Hanging scroll; ink and color on paper,10 1/4 x 13 3/8 in. (26 x 34 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.444,false,true,49104,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1747,"Matted painting; color on papier mache in relief, against a paper background",Diam. 10 3/4 in. (27.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.445,false,true,49105,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1747,"Matted painting; color on papier-mâché in relief, against a paper background",Diam. 10 3/4 in. (27.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.945,false,true,36043,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th century,1700,1747,Watercolor on paper,7 1/32 x 10 7/32 in. (17.9 x 26 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/36043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.87.1,false,true,45769,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,18th century,1700,1750,Hanging scroll; ink and color on silk,Image: 11 1/4 × 14 3/4 in. (28.6 × 37.5 cm) Overall with mounting: 44 3/4 × 19 3/4 in. (113.7 × 50.2 cm) Overall with knobs: 44 3/4 × 21 5/8 in. (113.7 × 54.9 cm),"Fletcher Fund, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.42,false,true,45768,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,18th century,1700,1750,Hanging scroll; ink and color on paper,Image: 14 1/4 in. × 21 in. (36.2 × 53.3 cm) Overall with mounting: 51 1/8 × 26 7/8 in. (129.9 × 68.3 cm) Overall with knobs: 51 1/8 × 29 1/4 in. (129.9 × 74.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.126,false,true,44863,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okumura Masanobu,"Japanese, 1686–1764",,Masanobu Okumura,Japanese,1686,1764,18th century,1700,1764,Hanging scroll; ink and color on silk,Image: 33 1/8 x 12 7/8 in. (84.1 x 32.7 cm) Overall with mounting: 66 3/4 x 17 3/4 in. (169.5 x 45.1 cm) Overall with knobs: 66 3/4 x 20 1/4 in. (169.5 x 51.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.31,false,true,45762,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Furuyama Moromasa,"Japanese, 1712–1772",,Furuyama Moromasa,Japanese,1712,1772,18th century,1712,1772,Hanging scroll; ink and color on silk,Image: 16 3/4 × 23 3/8 in. (42.5 × 59.4 cm) Overall with mounting: 55 × 29 3/4 in. (139.7 × 75.6 cm) Overall with knobs: 55 × 31 3/4 in. (139.7 × 80.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.243,false,true,49075,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sō Shiseki,"Japanese, 1715–1786",,Sō Shiseki,Japanese,1715,1786,18th century,1715,1786,Hanging scroll; ink and color on silk,40 7/8 x 13 3/4 in. (103.8 x 34.9 cm),"Purchase, Bequest of Stephen Whitney Phoenix, by exchange, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.22.3,false,true,48996,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Yosa Buson,"Japanese, 1716–1783",,Yosa Buson,Japanese,1716,1783,18th century,1716,1783,Hanging scroll; ink and color on paper,40 1/4 x 20 5/16 in. (102.2 x 51.6 cm),"Gift of Robert Pollak, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.38,false,true,45231,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1726,1792,Hanging scroll; ink and color on silk,28 1/2 x 12 1/8 in. (72.4 x 30.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.39,false,true,45232,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1726,1792,Hanging scroll; ink and color on silk,28 1/2 x 12 1/2 in. (72.4 x 31.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.40,false,true,45233,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1726,1792,Hanging scroll; ink and color on silk,28 1/2 x 12 1/8 in. (72.4 x 30.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.41,false,true,45788,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1726,1792,Hanging scroll; ink and color on silk,38 3/4 x 8 1/4 in. (98.4 x 21 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.70c,false,true,45787,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1726,1792,Hanging scroll; ink and color on silk,40 3/4 x 12 1/2 in. (103.5 x 31.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.70d,false,true,45789,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1726,1792,Hanging scroll; ink and color on silk,40 3/4 x 12 1/2 in. (103.5 x 31.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.70,false,true,39630,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,18th century,1726,1792,Hanging scroll; ink and color on silk,Image: 44 x 12 in. (111.8 x 30.5 cm) Overall with mounting: 74 x 17 11/16 in. (188 x 44.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.66,false,true,49079,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Soga Shōhaku,"Japanese, 1730–1781",,Soga Shōhaku,Japanese,1730,1781,18th century,1730,1781,Hanging scroll; ink and color on silk,Image: 18 x 25 in. (45.7 x 63.5 cm) Overall with mounting: 54 1/2 x 29 5/8 in. (138.4 x 75.2 cm) Overall with knobs: 54 1/2 x 32 in. (138.4 x 81.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.85,false,true,42634,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Sō Shizan,"Japanese, 1733–1805",,Sō Shizan,Japanese,1733,1805,18th century,1733,1799,Hanging scroll; ink and color on silk,Image: 36 1/4 x 13 9/16 in. (92.1 x 34.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.70f,false,true,45805,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyoharu,"Japanese, 1735–1814",,Utagawa Toyoharu,Japanese,1735,1814,18th century,1735,1799,Hanging scroll; ink and color on silk,40 3/4 x 12 1/2 in. (103.5 x 31.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.190,false,true,49050,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Matsumura Goshun,"Japanese, 1752–1811",,Matsumura Goshun,Japanese,1752,1811,18th century,1752,1799,Hanging scroll; ink and color on paper,39 13/16 x 11 5/8 in. (101.2 x 29.5 cm),"Rogers Fund, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.77,false,true,49049,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Matsumura Goshun,"Japanese, 1752–1811",,Matsumura Goshun,Japanese,1752,1811,18th century,1752,1799,Hanging scroll; ink and color on paper,69 x 36 7/8 in. (175.2 x 93.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.70e,false,true,45796,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,18th century,1752,1799,Hanging scroll; ink and color on silk,40 3/4 x 12 1/2 in. (103.5 x 31.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.47,false,true,45794,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,18th century,1752,1799,Hanging scroll; ink and color on silk,39 7/8 x 14 1/2 in. (101.3 x 36.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.127,false,true,45799,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,18th century,1757,1799,Hanging scroll; ink and color on paper,Image: 32 x 11 15/16 in. (81.3 x 30.3 cm) Overall: 63 7/8 x 18 3/4in. (162.2 x 47.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.101,false,true,57160,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kushiro Unsen,"Japanese, 1759–1811",,Kushiro Unsen,Japanese,1759,1811,18th century,1759,1799,Hanging scroll; ink and color on paper,42 5/8 x 11 1/8 in. (108.3 x 28.3 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.55,false,true,48891,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,18th century,1771,1799,"Hanging scroll; ink and color, and gold on silk",29 x 10 3/4 in. (73.7 x 27.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.70a,false,true,45785,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,18th century,1735,1790,Hanging scroll; ink and color on silk,40 3/4 x 12 1/2 in. (103.5 x 31.8 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.404,false,true,45771,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kondo Katsunobu,"Japanese, active 1716–1736",,Kondo Katsunobu,Japanese,1716,1736,18th century,1716,1736,Hanging scroll; ink and color on paper,38 3/16 x 16 5/8 in. (97 x 42.3 cm),"Gift of Francis T. Henderson Jr., in memory of Harold G. Henderson, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.69,false,true,45791,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,18th century,1783,1795,Hanging scroll; ink and color on silk,35 7/8 x 12 1/8 in. (91.1 x 30.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.74,false,true,58908,Asian Art,Inrō,,Japan,,,,,Artist,,Gyokuzan,1737–1812,,Gyokuzan,Japanese,1737,1812,19th century,1800,1899,"Lacquer, roiro, gold and colored hiramakie, takamakie, kirigane, nashiji; Interior: nashiji and fundame",3 3/4 x 2 x 1 1/8 in. (9.5 x 5.1 x 2.9 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.214,false,true,58895,Asian Art,Inrō,,Japan,,,,,Artist,,Koma Kōryū,"Japanese, died 1796",,Koma Kōryū,Japanese,,1796,19th century,1800,1899,"Lacquer, silver ground, gold, black and red togidashi, ivory lid; Interior: nashiji and fundame",2 1/2 x 2 3/16 x 1 1/16 (6.3 x 5.6 x 2.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.399.8,false,true,59650,Asian Art,Netsuke,,Japan,,,,,Artist,,Masakazu,"Japanese, died 1886",,Masakazu,Japanese,,1886,19th century,1800,1899,Wood,H. 1 1/8 in. (2.9 cm); W. 1 1/8 in. (2.9 cm),"Gift of Alvin H. Schechter, 1985",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.82.7,false,true,58637,Asian Art,Inrō,,Japan,,,,,Artist,In the Style of,Hanabusa Itchō,"Japanese, 1652–1724",,Hanabusa Itchō,Japanese,1652,1724,19th century,1800,1899,"Metal and lacquer, dark silver metal, incised, roiro, gold hiramakie, raden, aogai; Interior: fundame, four boxes",4 1/8 x 1 9/16 x 7/8 in. (10.4 x 4 x 2.2 cm),"Gift of Wilton Lloyd-Smith and his wife, Marjorie Fleming Lloyd-Smith, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.879,false,true,78769,Asian Art,Woodblock,,Japan,,,,,Artist,After,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,19th century,1800,1824,Woodblock,8 9/16 × 6 3/4 in. (21.7 × 17.2 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Woodblocks,,http://www.metmuseum.org/art/collection/search/78769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1894,false,true,59113,Asian Art,Netsuke,,Japan,,,,,Artist,,Toyomasa,"Japanese, 1773–1856",,Toyomasa,Japanese,1773,1856,19th century,1800,1899,Wood,H. 2 in. (5.1 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.830,false,true,58735,Asian Art,Inrō,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Bamboo, black, silver and gold hiramakie, takamakie, wood lid; Interior: plain",2 1/4 x 2 5/8 x 1 5/8 in. (5.7 x 6.7 x 4.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.248,false,true,58934,Asian Art,Inrō,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Lacquer, dark brown, imitating leather, relief, light brown rim; Interior: dark brown and nashiji",2 15/16 x 2 5/16 x 11/16 in. (7.4 x 5.9 x 1.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.137.5,false,true,58329,Asian Art,Box,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Lacquer,H. 1 3/8 in. (3.5 cm); W. 2 1/4 in. (5.7 cm); L. 3 1/4 in. (8.3 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1951",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.153,false,true,58336,Asian Art,Tray,,Japan,,,,,Artist,Lacquered by,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Lacquer with gold,H. 1 1/2 in. (3.8 cm); W. 4 5/8 in. (11.7 cm); L. 8 1/2 in. (21.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.154,false,true,56529,Asian Art,Tray,,Japan,,,,,Artist,Lacquered by,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Lacquer,W. 10 in. (25.4 cm); L. 20 3/4 in. (52.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/56529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.170,false,true,56166,Asian Art,Tea caddy,,Japan,,,,,Artist,Lacquered by,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Lacquer,H. 4 1/8 in. (10.5 cm); Diam. 3 in. (7.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/56166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.175,false,true,58353,Asian Art,Box,,Japan,,,,,Artist,Attributed to,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Lacquer,H. 3/8 in. (1 cm); W. 1 1/2 in. (3.8 cm); L. 7 1/8 in. (18.1 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.168a, b",false,true,58345,Asian Art,Box,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Lacquer,H. 1 in. (2.5 cm); W. 3 7/8 in. (9.8 cm); D. 3 7/8 in. (9.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.155.1,false,true,57348,Asian Art,Screen,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Two-panel folding screen; ink on paper,41 1/2 x 55 in. (105.4 x 139.7 cm),"Gift of Harold G. Henderson, 1966",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/57348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.749,false,true,78651,Asian Art,Illustrated book,,Japan,,,,,Artist,,Nishiyama Ken,"Japanese, 1833–1897",,Nishiyama Ken,Japanese,1833,1897,19th century,1833,1897,Accordion album; ink and color on paper,10 5/8 × 5 1/2 in. (27 × 14 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.55.82,false,true,59630,Asian Art,Netsuke,,Japan,,,,,Artist,,Koichi,"Japanese, 19th century",,Koichi,Japanese,1800,1899,19th century,1800,1899,"Wood, horn",H. 1 3/4 in. (4.4 cm); W. 1 1/8 in. (2.9 cm); D. 1 1/4 in. (3.2 cm),"Bequest of Susan Dwight Bliss, 1966",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.459.1,false,true,70647,Asian Art,Pressing board,,Japan,,,,,Artist,,Tabata Sadahiko,"Japanese, 19th century",,Tabata Sadahiko,Japanese,0019,0019,19th century,1800,1899,,9 x 18 1/2 in. (22.86 x 46.99 cm),"Gift of Takami Sugiura and Sadahiko Tabata, 1979",,,,,,,,,,,,Textiles-Methods and Materials,,http://www.metmuseum.org/art/collection/search/70647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.221,false,true,58902,Asian Art,Inrō,,Japan,,,,,Artist,,Tōyō,"Japanese, active ca. 1764–71",,Tōyō,Japanese,1764,1771,19th century,1800,1899,"Lacquer, roiro, gyobu, gold, black, silver and brown hiramakie, takamakie; Interior: nashiji and fundame",3 15/16 x 1 7/8 x 1 1/8 in. (10 x 4.8 x 2.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1001,false,true,60266,Asian Art,Netsuke,,Japan,,,,,Artist,,Ryūsa,"Japanese, active late 18th century",,Ryūsa,Japanese,1767,1799,19th century,1800,1899,Ivory,H. 1 3/16 in. (3 cm); W. 9/16 in. (1.4 cm); L. 1 3/8 in. (3.5 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/60266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1262,false,true,60327,Asian Art,Netsuke,,Japan,,,,,Artist,,Ryūsa,"Japanese, active late 18th century",,Ryūsa,Japanese,1767,1799,19th century,1800,1899,Ivory,H. 3/4 in. (1.9 cm); Diam. 1 7/8 in. (4.8 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/60327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.153,false,true,57110,Asian Art,Printer's woodblock,,Japan,,,,,Artist,Original print designed by,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1800,1899,Wood,13 3/4 x 9 1/2 in. (34.9 x 24.1 cm),"Gift of Harry E. Goldman, 1955",,,,,,,,,,,,Woodblocks,,http://www.metmuseum.org/art/collection/search/57110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.84,false,true,45569,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Yamada Jōkasai (1681–1704),,,Yamada Jōkasai,Japanese,1681,1704,19th century,1800,1899,"Lacquer decorated with sprinkled hiramakie lacquer, sprinkled and polished takamakie lacquer relief, and nashiji (pear skin) lacquer; Ojime: ivory and gold lacquer bead; Netsuke: carved wood bird (signed Shuko)",3 5/16 x 2 5/16 x 1 1/16 in. (8.4 x 5.8 x 2.7 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.889,false,true,58809,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Toyōsai (1772–1845),,,Toyōsai,Japanese,1772,1845,19th century,1800,1899,Black lacquer with sprinkled gold and silver makie and mother-of-pearl Ojime: bead with openwork design of waves; silver Netsuke: fish laid on bamboo branch; stained ivory,2 7/8 x 3 1/16 x 11/16 in. (7.3 x 7.7 x 1.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.198,false,true,45503,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Yamada Jōkasai (1681–1704),,,Yamada Jōkasai,Japanese,1681,1704,19th century,1800,1899,"Sprinkled gold lacquer with mother-of-pearl, stone, and metal Ojime: ovoid gold bead decorated with grasses Netuske: black wood carved as a piece of firewood",3 3/4 x 2 11/16 x 3/4 in. (9.5 x 6.8 x 1.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.157a, b",false,true,45364,Asian Art,Incense box,,Japan,Edo period (1615–1868),,,,Artist,,Yamada Jōkasai (1681–1704),,,Yamada Jōkasai,Japanese,1681,1704,19th century,1800,1899,Gold-speckled aventurine (nashiji) lacquer with sprinkled and polished design (hiramakie),H. 1 1/16 in. (2.7 cm); W. 1 1/8 in. (2.9 cm); L. 3 5/8 in. (9.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.159a, b",false,true,45497,Asian Art,Round box,,Japan,Edo period (1615–1868),,,,Artist,,Yamada Jōkasai (1681–1704),,,Yamada Jōkasai,Japanese,1681,1704,19th century,1800,1899,"Lacquer on wood with gold, mother-of-pearl inlay, and colored lacquer",H. 1 3/8 in. (3.5 cm); Diam. 3 in. (7.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.885a–e,false,true,57953,Asian Art,Writing box,,Japan,Edo period (1615–1868),,,,Artist,,Tatsuke Takanori,1757–1833,,Tatsuke Takanori,Japanese,1757,1833,19th century,1800,1899,Gold and silver maki-e with colored lacquer on black lacquer,H. 2 1/16 in. (5.2 cm); W. 9 in. (22.9 cm); D. 9 1/2 in. (24.2 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/57953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.195,false,true,58881,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Kano Seisen’in,1775–1828,,Kano Seisen’in,Japanese,1775,1828,19th century,1800,1899,"Lacquer with sprinkled gold, silver, and red makie and takamakie Ojime: bead; tortoiseshell Netsuke: basket; woven reeds",3 7/16 x 2 3/8 x 15/16 in. (8.8 x 6 x 2.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.199,false,true,58883,Asian Art,Inrō,山田常嘉斎作・狩野晴川院筆 満月に鵞鳥図印籠|Inrō with Goose Flying across the Full Moon,Japan,Edo period (1615–1868),,,,Artist,After,Kano Seisen’in,1775–1828,,Kano Seisen’in,Japanese,1775,1828,19th century,1800,1899,"Lacquer, kinji, gold, silver, black and red hiramakie, togidashi, aogai inlay; Interior: nashiji and fundame",Overall (inro): H. 3 1/16 in. (7.7 cm); W. 2 1/16 in. (5.2 cm); D. 13/16 in. (2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.745,false,true,58657,Asian Art,Inrō,古満巨柳作 鷹蒔絵印籠|Inrō with Hawks on Perches,Japan,Edo period (1615–1868),,,,Artist,,Koma Kōryū,"Japanese, died 1796",,Koma Kōryū,Japanese,,1796,19th century,1800,1899,"Black lacquer ground with gold and silver togidashi, takamaki-e and hiramaki-e, red lacquer, and applied gold and silver foil Netsuke: gourd; guri lacquer, silver ring and stopper in chrysanthemum shape Ojime: butterfly and flower; cloisonné bead",2 15/16 x 2 5/8 x 3/4 in. (7.4 x 6.7 x 1.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.213,false,true,45575,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Koma Kōryū,"Japanese, died 1796",,Koma Kōryū,Japanese,,1796,19th century,1800,1899,Roiro (waxen) lacquer with decoration in togidashi sprinkled and polished lacquer and nashiji (pear-skin) lacquer; Interior: nashiji and fundame; Ojime: metal (zogan) inlay with spider; Netsuke: carved wood with stone inlay of snail on broken roof tile,3 x 1 7/8 x 1 1/8 in. (7.6 x 4.8 x 2.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.690,false,true,45563,Asian Art,Writing box,橅夫蒔絵硯箱|Writing Box (Suzuribako) with Woodcutter,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Black and gold lacquer on wood with mother-of-pearl and pewter inlay,H. 2 in. (5.1 cm); W. 9 7/8 in. (25.1 cm); L. 9 1/4 in. (23.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.691,false,true,45564,Asian Art,Writing box,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Black and gold lacquer on wood with mother-of-pearl and pewter inlay,H. 1 7/8 in. (4.8 cm); W. 8 3/4 in. (22.2 cm); L. 9 1/2 in. (24.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.697,false,true,58168,Asian Art,Box,,Japan,Edo period (1615–1868),,,,Artist,Style of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Gold inlaid with mother-of-pearl and tin,H. 4 3/4 in. (12.1 cm); W. 4 1/2 in. (11.4 cm); L. 7 3/4 in. (19.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.719,false,true,56523,Asian Art,Incense box,江戸時代 琳派 梅蒔絵螺鈿香合|Incense Box with Flowering Plum Tree,Japan,Edo period (1615–1868),,,,Artist,School of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,"Gold lacquer with gold hiramaki-e, black lacquer, and mother-of-pearl inlay",H. 1 1/2 in. (3.8 cm); W. 2 1/4 in. (5.7 cm); L. 2 1/4 in. (5.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/56523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"58.169a, b",false,true,58334,Asian Art,Writing box,橅夫蒔絵硯箱|Writing Box (Suzuribako) with Woodcutter,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Black and gold lacquer on wood with gold maki-e and mother-of-pearl inlay,(a): H. 8 1/4 in. (21 cm); W. 7 3/8 in. (18.7 cm); D. 1 3/8 in. (3.5 cm) (b): H. 8 9/16 in. (21.7 cm); W. 7 3/4 in. (19.7 cm); D. 15/16 in. (2.4 cm),"Gift of Nathan Hammer, 1958",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.832,false,true,58737,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,In the Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,19th century,1800,1899,Colored togidashi and gold hiramaki-e on black lacquer,3 5/16 x 2 1/2 x 1 1/16 in. (8.4 x 6.4 x 2.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.679,false,true,58096,Asian Art,Document box,,Japan,Edo period (1615–1868),,,,Artist,Style of,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,19th century,1800,1899,"Colored lacquer, gold and silver foil, mother-of-pearl, ivory, tortoiseshell, and ceramic on black lacquer",H. 2 3/4 in. (7 cm); W. 10 in. (25.4 cm); L. 11 in. (27.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.727,false,true,58642,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Sō Shiseki,"Japanese, 1715–1786",,Sō Shiseki,Japanese,1715,1786,19th century,1800,1899,"Lacquer, roiro, gold foil, aogai inlay; Interior: roiro",3 1/16 x 3 1/16 x 15/16 in. (7.8 x 7.8 x 2.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.811,false,true,58717,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Mori Sosen,"Japanese, 1747–1821",,Mori Sosen,Japanese,1747,1821,19th century,1800,1899,"Metal, brass metal, incised, various applied metals; Interior: silver metal",2 5/8 x 1 15/16 x 13/16 in. (6.7 x 5 x 2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.845,false,true,58771,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Mori Sosen,"Japanese, 1747–1821",,Mori Sosen,Japanese,1747,1821,19th century,1800,1899,"Lacquer, gold and silver hirame, gold, red and coloured hiramakie, takamakie; Interior: red lacquer, fundame and decoration; the interior risers decorated in gold and silver togidashi with various brocade pattern, key-fret, waves, etc.",3 1/8 x 3 1/8 x 13/16 in. (7.9 x 8 . 2.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.250,false,true,45576,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Roiro (waxen) lacquer with sprinkled hiramkie lacquer and togidashi sprinkled and polished lacquer; Interior: nashiji and fundame; Ojime: silver shibuichi lacquer bead decorated with design of castle grounds; Netsuke: carved wooden sleeping goose,2 11/16 x 2 1/16 x 15/16 in. (6.8 x 5.3 x 2.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.210,false,true,54240,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Nakayama Komin,"Japanese, 1808–1870",,Nakayama Komin,Japanese,1808,1870,19th century,1800,1899,"Hiramaki-e with nashiji on black lacquer, roiro, nashiji, gold and silver hiramakie, gold and silver foil; Interior: nashiji and fundame; Ojime: ivory bead with vines and grasses in gold lacquer; Netsuke: woven basket with shell and gold lacquer)",3 1/16 x 2 1/16 x 13/16 in. (7.8 x 5.2 x 2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/54240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.879a–n,false,true,44916,Asian Art,Writing box,中山胡民作 伊勢物語硯箱|Writing Box (Suzuribako) with Episodes from the Tales of Ise (Ise monogatari),Japan,Edo period (1615–1868),,,,Artist,,Nakayama Komin,"Japanese, 1808–1870",,Nakayama Komin,Japanese,1808,1870,19th century,1800,1899,Gold maki-e with inlaid silver on lacquer,H. 1 1/2 in. (3.8 cm); W. 7 in. (17.8 cm); D. 7 3/4 in. (19.7 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/44916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.34,false,true,58576,Asian Art,Inrō,山田常嘉斎作 木蓮オウム蒔絵螺鈿印籠|Inrō with Cockatoo and Magnolia,Japan,Edo period (1615–1868),,,,Artist,,Yamada Jōkasai,"Japanese, 1811–1879",,Yamada Jōkasai,Japanese,1811,1879,19th century,1800,1899,"Red lacquer ground with gold maki-e, carved red lacquer, and mother-of-pearl inlay Netsuke: poppy; ivory Ojime: roundels; red and green lacquer in wood",3 5/8 x 1 13/16 x 1 3/16 in. (9.2 x 4.6 x 3 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.673,false,true,78594,Asian Art,Illustrated book,奇観幀|Album of Twelve Nanga-style Landscapes (Kikanchō),Japan,Edo period (1615–1868),,,,Artist,,Tanomura Chokunyū,"Japanese, 1814–1907",,Tanomura Chokunyū,Japanese,1814,1907,19th century,1800,1868,Accordion album; ink and color on paper,8 7/16 × 5 13/16 in. (21.5 × 14.8 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.722,false,true,58289,Asian Art,Pipe-case with pipe,,Japan,Edo period (1615–1868),,,,Artist,,Tamakaji Zōkoku,"Japanese, 1803?–1866",,Tamakaji Zōkoku,Japanese,1803,1866,19th century,1800,1899,"Lacquer, silver and shakudo",W. 1 1/2 in. (3.8 cm); L. 11 in. (27.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.338a–d,false,true,73591,Asian Art,Illustrated book,橫濱開港見聞誌|Observations on the Opening of Yokohama (Yokohama kaikō kenbunshi),Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Three volumes of woodblock printed books; ink on paper,Image (a-c): 9 1/2 x 6 3/4 x 3/4 in. (24.1 x 17.1 x 1.9 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/73591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.339a–c,false,true,73592,Asian Art,Illustrated book,橫濱開港見聞誌|Observations on the Opening of Yokohama (Yokohama kaiko kenbunshi),Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Three volumes of woodblock printed books; ink on paper,Image (a-c): 9 5/8 x 6 7/8 x 1/2 in. (24.4 x 17.5 x 1.3 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/73592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.183,false,true,58871,Asian Art,Inrō,梶川文竜斎作 宝船蒔絵印籠|Inrō with Treasure Boat,Japan,Edo period (1615–1868),,,,Artist,,Kajikawa Bunryūsai,"Japanese, ca. 1751–1817",,Kajikawa Bunryūsai,Japanese,1741,1827,19th century,1800,1899,"Gold and black lacquer and nashiji ground with gold and silver hiramaki-e, red lacquer, and mother-of-pearl inlay Netsuke: kingfisher; carved ivory Ojime: Chinese children; porcelain (Kutani ware",3 7/16 x 2 1/16 x 13/16 in. (8.7 x 5.3 x 2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"JIB141a, b",false,true,48887,Asian Art,Two booklets,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1750,1900,Two volumes of drawings; ink and color on paper,each: 7 3/4 × 5 1/4 in. (19.7 × 13.3 cm),"Fletcher Fund, 1941",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/48887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.569,false,true,45436,Asian Art,Sake cup,,Japan,Edo period (1615–1868),,,,Artist,,Shomosai,"Japanese, active late 18th–early 19th century",,Shomosai,Japanese,0018,0019,19th century,1800,1899,Gold lacquer on red lacquer ground,H. 1 in. (2.5 cm); W. 4 3/8 in. (11.1 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/45436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.82.2,false,true,58633,Asian Art,Inrō,光琳様式 梅鶴蒔絵螺鈿印籠|Inrō with Crane and Plum Tree,Japan,Meiji period (1868–1912),,,,Artist,Style of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Gold lacquer ground with mother-of-pearl and pewter inlay Netsuke: fish on wheels; carved wood Ojime: vajra (thunderbolt); metal,2 7/16 x 2 1/4 x 13/16 in. (6.2 x 5.7 x 2.1 cm),"Gift of Wilton Lloyd-Smith and his wife, Marjorie Fleming Lloyd-Smith, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.46,false,true,36077,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Gold and colored lacquer on wood Signed: Zeshin Ojime: carved wooden dragon (signed: Ichiryūsai Furuta) Netsuke: ceramic figure of Kaduzōsu (signed: Eiraku),2 1/4 x 2 3/4 x 1 in. (5.72 x 6.99 x 2.54 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1957",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/36077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.53,false,true,36078,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,In the style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Colored lacquer and gold maki-e on wood Netsuke: ivory in form of fox reclining on large leaf; Ojima: gold bead; connecting cord, dark green silk Inro with netsuke and ojime",3 1/16 x 2 1/8 x 13/16 in. (7.7 x 5.4 x 2 cm),"Rogers Fund, 1957",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/36078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.13,false,true,58563,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,In the style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Gold, silver, red, and black hiramaki-e, takamaki-e, mother-of-pearl inlay on bamboo; interior: plain wood, drawers",2 13/16 x 1 9/16 x 11/16 in. (7.2 x 4 x 1.8 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.25.148,false,true,59011,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Black lacquer with wood, shell, and glass inlays; Fastener (ojime): metal; Toggle (netsuke): carved ivory in the shape of a demon",H. 3 1/16 in. (9.4 cm); W. 1 7/8 in. (4.8 cm); D. 1 3/16 in. (3 cm),"Gift of Mrs. George A. Crocker (Elizabeth Masten), 1937",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/59011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.247,false,true,58933,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Lacquer, roiro, incised, gold, silver and brown hiramakie, takamakie, aogai; Interior: gyobu nashiji and fundame",2 7/8 x 2 x 13/16 in. (7.3 x 5.1 x 2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.252,false,true,58937,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Gold and silver maki-e with colored lacquer on black lacquer Ojime: gilded metal with fox-monk Netuske: Lacquer oval box with diamond floral design (signed Kiyoharu),3 11/16 x 2 1/16 x 1 1/8 in. (9.4 x 5.2 x 2.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.253,false,true,45448,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Roiro (waxen) lacquer with black hiramakie sprinkled and polished lacquer and takamakie sprinkled and polished lacquer relief; Interior: roiro; Netsuke: lacquer in shape of a worn ink stick; Ojime: black lacquer bead,2 5/16 x 1 1/2 x 11/16 in. (5.9 x 3.8 x 1.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.257,false,true,58940,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Lacquer, roiro, gold black, brown hiramakie, aogai; Interior: nashiji and fundame",2 7/8 x 1 15/16 x 3/4 in. (7.3 x 4.9 x 1.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.77,false,true,56577,Asian Art,Tray,,Japan,Meiji period (1868–1912),,,,Artist,Style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Hiramaki-e, cut-out silver foil, gold, silver application, mother-of-pearl application",H. 1/2 in. (1.3 cm); W. 3 5/16 in. (8.4 cm); L. 6 7/16 in. (16.4 cm),"Gift of Joseph U. Seo, 1954",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/56577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.169a, b",false,true,58346,Asian Art,Tea caddy,,Japan,Meiji period (1868–1912),,,,Artist,In the style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,Colored lacquer and gold maki-e on wood,H. 1 7/8 in. (4.8 cm); Diam. 2 3/4 in. (7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.155,false,true,58337,Asian Art,Tray,,Japan,Meiji period (1868–1912),,,,Artist,,Ikeda Taishin,"Japanese, 1825–1903",,Ikeda Taishin,Japanese,1825,1903,19th century,1800,1899,"Silver, gold takamaki-e, hiramaki-e, kirikane, black lacquer",W. 5 in. (12.7 cm); L. 8 in. (20.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.684,false,true,58463,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist,Signed,Tōyō,"Japanese, active ca. 1764–71",,Tōyō,Japanese,1764,1771,19th century,1800,1899,"Gold hiramaki-e, takamaki-e, cut-out gold foil application and mother-of-pearl inlay on red lacquer ground; Inside: Nashiji and fundame",H. 2 13/16 in. (7.2 cm); W. 2 3/8 in. (6 cm); D. 13/16 in. (2.1 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.100.172a, b",false,true,58350,Asian Art,Box,花熨斗蒔絵短冊箱|Poem Card (Tanzaku) Box with Flower Bouquet,Japan,Meiji period (1869–1912),,,,Artist,Style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1800,1899,"Lacquered wood with gold, silver, color, takamaki-e, hiramaki-e, and togidashimaki-e",H. 7/8 in. (2.2 cm); W. 2 3/8 in. (6 cm); L. 14 1/2 in. (36.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Lacquer,,http://www.metmuseum.org/art/collection/search/58350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1960,false,true,54533,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 3/8 in. (20.6 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2148,false,true,55006,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 3/4 in. (13.7 x 19.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2157,false,true,55064,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/4 in. (20.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2210,false,true,53976,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 3/8 in. (21.3 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2225,false,true,53991,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 15/16 x 4 5/16 in. (22.7 x 11 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2270,false,true,54043,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 5 1/4 in. (14 x 13.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2284,false,true,54057,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 10 3/16 in. (20.3 x 25.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2285,false,true,54058,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/8 x 5 3/8 in. (21.3 x 13.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2286,false,true,54059,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2289,false,true,54062,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/8 x 7 1/4 in. (21.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2290,false,true,54063,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2299,false,true,54072,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 7 3/8 in. (20.6 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2300,false,true,54073,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/4 x 7 1/8 in. (13.3 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2302,false,true,54086,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2303,false,true,54087,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2304,false,true,54088,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/8 x 7 3/8 in. (21.3 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2305,false,true,54089,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/8 x 7 3/8 in. (21.3 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2308,false,true,54092,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2310,false,true,54094,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2311,false,true,54095,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2312,false,true,54096,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2313,false,true,54097,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2316,false,true,54100,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2325,false,true,54109,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2329,false,true,54113,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2334,false,true,54118,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 6 5/8 in. (20.6 x 16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2347,false,true,54132,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,6 1/8 x 11 in. (15.6 x 27.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2348,false,true,54133,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 1/4 x 4 7/8 in. (18.4 x 12.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2354,false,true,54139,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2355,false,true,53813,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 1/8 in. (20 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2356,false,true,54140,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 3/4 x 7 3/16 in. (19.7 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2357,false,true,54141,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2358,false,true,54142,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 13/16 x 7 1/8 in. (19.8 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2359,false,true,54143,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 13/16 x 7 13/16 in. (19.8 x 19.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2360,false,true,54144,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2332,false,true,54116,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",(?),Utagawa Toyokuni I,Japanese,1769,1825,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2361,false,true,54145,Asian Art,Print,,Japan,,,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 5/16 x 7 5/16 in. (21.1 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2227,false,true,53993,Asian Art,Print,,Japan,,,,,Artist,,Takashima Chiharu,"Japanese, 1777–1859",(?),Takashima Chiharu,Japanese,1777,1859,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 13/16 x 3 15/16 in. (22.4 x 10 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2368,false,true,54152,Asian Art,Woodblock print,,Japan,,,,,Artist,,Takashima Chiharu,"Japanese, 1777–1859",,Takashima Chiharu,Japanese,1777,1859,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 7/8 x 3 3/4 in. (22.5 x 9.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2327,false,true,54111,Asian Art,Woodblock print,,Japan,,,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 9/16 x 7 3/8 in. (14.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2345,false,true,54130,Asian Art,Print,,Japan,,,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 10 1/4 in. (20.3 x 26 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2369,false,true,54153,Asian Art,Print,,Japan,,,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,4 1/2 x 10 1/2 in. (11.4 x 26.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2390,false,true,54174,Asian Art,Woodblock print,,Japan,,,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 3/8 x 7 3/8 in. (13.7 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2278,false,true,54051,Asian Art,Print,,Japan,,,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 1/6 in. (21 x 18.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2319,false,true,54103,Asian Art,Print,,Japan,,,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 3/4 x 6 9/16 in. (19.7 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2333,false,true,54117,Asian Art,Woodblock print,,Japan,,,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 11/16 x 6 5/8 in. (19.5 x 16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2162,false,true,55069,Asian Art,Print,,Japan,,,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/8 in. (20.8 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2163,false,true,55070,Asian Art,Print,,Japan,,,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/8 in. (20.8 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2322,false,true,54106,Asian Art,Print,,Japan,,,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 3/8 x 7 3/8 in. (13.7 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2324,false,true,54108,Asian Art,Print,,Japan,,,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/8 x 7 3/8 in. (21.3 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2343,false,true,54128,Asian Art,Print,,Japan,,,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2326,false,true,54110,Asian Art,Print,,Japan,,,,,Artist,,Ishikawa Kazan,"Japanese, active 1810–1823",,Ishikawa Kazan,Japanese,1810,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2158,false,true,55065,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1750,1835,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 1/2 in. (14.3 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2171,false,true,55085,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2207,false,true,53973,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2208,false,true,53974,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2212,false,true,53978,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2226,false,true,53992,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 11 1/6 in. (21 x 28.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2229,false,true,53995,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 x 6 13/16 in. (12.7 x 17.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2230,false,true,53996,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 x 7 1/16 in. (12.7 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2234,false,true,54002,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 10 15/16 in. (21 x 27.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2269,false,true,54042,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,6 3/4 x 8 3/4 in. (17.1 x 22.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2275,false,true,54048,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 7/16 x 7 7/16 in. (13.8 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2276,false,true,54049,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 7/16 x 7 1/2 in. (13.8 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2280,false,true,54053,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2281,false,true,54054,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2282,false,true,54055,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2283,false,true,54056,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,4 1/8 x 5 11/16 in. (10.5 x 14.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2287,false,true,54060,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 7/16 x 7 1/4 in. (13.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2301,false,true,54085,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2314,false,true,54098,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2317,false,true,54101,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 5/16 x 7 3/16 in. (13.5 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2323,false,true,54107,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2328,false,true,54112,Asian Art,Woodblock print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 5/8 x 7 9/16 in. (14.3 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2375,false,true,54159,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 11/16 x 6 9/16 in. (19.5 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2388,false,true,54172,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/2 x 7 1/2 in. (21.6 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2389,false,true,54173,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2391,false,true,54175,Asian Art,Print,,Japan,,,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 7/8 x 7 1/2 in. (14.9 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2307,false,true,54091,Asian Art,Print,,Japan,,,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/8 x 7 3/16 in. (13 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1930,false,true,54470,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1757,1820,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 10 11/16 in. (13.7 x 27.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1949,false,true,54517,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1969,false,true,54553,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 1/4 in. (14.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1974,false,true,54559,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 1/4 in. (13.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1980,false,true,54565,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54565,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1981,false,true,54679,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/4 x 7 1/16 in. (13.3 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1982,false,true,54680,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 3/16 in. (13.7 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1983,false,true,54681,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 3/16 in. (20.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1984,false,true,54682,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1985,false,true,54683,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/8 in. (20.8 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1986,false,true,54685,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 5/16 in. (21.1 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1987,false,true,54689,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 1/4 in. (14.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1988,false,true,54690,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1989,false,true,54691,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 3/16 in. (20.2 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1991,false,true,54697,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1992,false,true,54702,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/8 in. (20.3 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1993,false,true,54705,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1994,false,true,54713,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1995,false,true,54715,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1996,false,true,54716,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1999,false,true,54719,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2000,false,true,54720,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 1/4 in. (21.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2003,false,true,54723,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2004,false,true,54724,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2005,false,true,54725,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 3/16 in. (20.2 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2006,false,true,54726,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 3/16 in. (20.2 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2007,false,true,54727,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/16 in. (20.3 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2009,false,true,54729,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 5/16 in. (13.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2010,false,true,54730,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 3/16 in. (20.5 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2013,false,true,54762,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/4 in. (20.2 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2014,false,true,54763,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2016,false,true,54765,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Polychrome woodblock print (surimono); ink and color on paper,5 1/4 x 10 13/16 in. (13.3 x 27.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2017,false,true,54766,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,6 x 12 5/8 in. (15.2 x 32.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2020,false,true,54769,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2021,false,true,54770,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2027,false,true,54787,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2030,false,true,54793,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/8 in. (21 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2031,false,true,54797,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/16 in. (21 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2044,false,true,54815,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 3/8 in. (14 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2063,false,true,54891,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2066,false,true,54895,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2067,false,true,54896,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 3/16 in. (20.5 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2069,false,true,54899,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/4 in. (20.3 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2072,false,true,54903,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/4 in. (20.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2073,false,true,54904,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/16 in. (20.3 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2076,false,true,54907,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 1/4 in. (21.1 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2077,false,true,54908,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 9/16 x 7 3/8 in. (21.7 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2078,false,true,54909,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 3/16 in. (20.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2079,false,true,54910,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/16 in. (20.2 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2080,false,true,54916,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 1/8 in. (20.3 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2083,false,true,54920,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 3/16 in. (20.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2086,false,true,54923,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 3/16 in. (20.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2087,false,true,54924,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/4 in. (20.2 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2092,false,true,54936,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2096,false,true,54943,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 3/16 in. (20 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2120,false,true,54973,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/16 in. (20.6 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2124,false,true,54979,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 4 1/8 in. (20.3 x 10.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2125,false,true,54980,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 4 3/16 in. (20.6 x 10.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2128,false,true,54983,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 3/16 in. (20 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2129,false,true,54984,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/4 in. (21 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2134,false,true,54989,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2137,false,true,54992,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 1/4 in. (20 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2138,false,true,54993,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2144,false,true,54999,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 7 in. (20 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2145,false,true,55003,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 1/8 in. (13.8 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2149,false,true,55010,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 7/16 in. (14.1 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2166,false,true,55073,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2192,false,true,55108,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 8 1/4 in. (13.8 x 21 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2200,false,true,53812,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,6 3/16 x 7 3/16 in. (15.7 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2236,false,true,54004,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2238,false,true,54007,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 5 5/16 in. (20.6 x 13.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2240,false,true,54009,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 7 3/8 in. (20.6 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2241,false,true,54010,Asian Art,Album,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 5 7/16 in. (20.6 x 13.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2244,false,true,54013,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/16 x 5 1/2 in. (20.8 x 14 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2247,false,true,54016,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1820,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/4 x 5 7/16 in. (21 x 13.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2250,false,true,54019,Asian Art,Print,Asazuma-bune|Courtesan in Ancient Costume Seated in a Boat,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/16 x 5 5/16 in. (20.8 x 13.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2251,false,true,54020,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 10 9/16 in. (20.6 x 26.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2254,false,true,54023,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",(?),Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 5 5/16 in. (20.6 x 13.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2255,false,true,54024,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 5/8 x 5 3/16 in. (19.4 x 13.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2376,false,true,54160,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/16 x 7 1/8 in. (20.5 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2377,false,true,54161,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/16 x 6 15/16 in. (20.5 x 17.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2378,false,true,54162,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 7 1/8 in. (20.3 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2379,false,true,54163,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/16 x 7 3/16 in. (20.5 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2217,false,true,53983,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Nagayama Koin (Hirotora),"Japanese, 1765–1849",,Nagayama Koin (Hirotora),Japanese,1765,1849,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/2 x 7 3/16 in. (21.6 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2215,false,true,53981,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 4 1/2 in. (20.6 x 11.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2397,false,true,54177,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 3/8 x 10 1/4 in. (18.7 x 26 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1955,false,true,54525,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 5 5/8 in. (13.8 x 14.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2012,false,true,54761,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 1/8 in. (14.1 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2022,false,true,54771,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 3/8 in. (20.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2041,false,true,54812,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",(?),Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 in. (20.3 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2045,false,true,54816,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 7 1/4 in. (20.2 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2081,false,true,54918,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/4 x 7 7/16 in. (14.6 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2093,false,true,54937,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1850,Polychrome woodblock print (surimono); ink and color on paper,7 1/16 x 6 5/8 in. (17.9 x 16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2112,false,true,54964,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 9/16 in. (19.7 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2117,false,true,54970,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/8 in. (20.6 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2121,false,true,54975,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/16 in. (20.6 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2160,false,true,55067,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 6 7/8 in. (20 x 17.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2201,false,true,53967,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,3 1/2 x 11 in. (8.9 x 27.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2213,false,true,53979,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,2 3/4 x 9 1/2 in. (7.0 x 24.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2235,false,true,54003,Asian Art,Print,Ushi-no-toki mairi|Woman in the Rain at Midnight Driving a Nail into a Tree to Invoke Evil on Her Unfaithful Lover,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 3/16 x 5 3/8 in. (20.8 x 13.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2248,false,true,54017,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1850,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/4 in. (20.2 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2259,false,true,54029,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 9/16 x 10 5/16 in. (19.2 x 26.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2381,false,true,54165,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 1/8 x 6 11/16 in. (18.1 x 17 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2382,false,true,54166,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Hokucho Joren,"Japanese, 1780–1850",,Hokucho Joren,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/16 in. (20.2 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2383,false,true,54167,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 7 1/4 in. (20.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2384,false,true,54168,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 7 1/4 in. (20.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2385,false,true,54169,Asian Art,Album,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 1/8 x 7 1/8 in. (20.6 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2386,false,true,54170,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 15/16 x 7 1/8 in. (20.2 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2387,false,true,54171,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,8 x 7 3/16 in. (20.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP209,false,true,36686,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Album of 48 polychrome woodblock prints; ink and color on paper,14 3/4 × 10 1/4 × 1/2 in. (37.5 × 26 × 1.3 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/36686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1114,false,true,55036,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Album of 18 polychrome woodblock prints; ink and color on paper,91 in. × 10 3/8 in. × 3/8 in. (231.1 × 26.4 × 1 cm),"Gift of Harold de Raasloff, 1918",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2864,false,true,51087,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,H. 9 3/8 in. (23.8 cm); W. 14 3/8 in. (36.5 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.2,false,true,57987,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.3,false,true,57988,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.4,false,true,57989,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.6,false,true,57991,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.7,false,true,57992,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.8,false,true,57993,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.9,false,true,57994,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.10,false,true,57995,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.11,false,true,57996,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.12,false,true,57997,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.13,false,true,57998,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/8 in. (34.6 × 23.2 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.14,false,true,57999,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.19,false,true,58004,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.20,false,true,58005,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.21,false,true,58006,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.24,false,true,58009,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.26,false,true,58010,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/8 × 8 3/4 in. (34 × 22.2 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.27,false,true,58011,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 5/8 in. (34.3 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.28,false,true,58012,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 5/8 in. (34.3 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.29,false,true,58013,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.31,false,true,58015,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.32,false,true,58016,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.33,false,true,58017,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.34,false,true,58018,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.35,false,true,58019,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.36,false,true,58020,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.37,false,true,58021,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.39,false,true,58023,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.41,false,true,58025,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.43,false,true,58027,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.44,false,true,58028,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.47,false,true,58031,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.48,false,true,58032,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.50,false,true,58034,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.55,false,true,58039,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.56,false,true,58040,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.57,false,true,58041,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.60,false,true,58044,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.61,false,true,58045,Asian Art,Print,相模屋亭主|Actor as Master of Sagamiya (Sagamiya teishu),Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Panel from a triptych of polychrome woodblock prints; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.62,false,true,58046,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.63,false,true,58047,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.64,false,true,58048,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.65,false,true,58049,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.66,false,true,58050,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.67,false,true,58051,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.68,false,true,58052,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.69,false,true,58053,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 1/2 in. (34.9 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.70,false,true,58054,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.71,false,true,58058,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.72,false,true,58059,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.73,false,true,58060,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.74,false,true,58061,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.75,false,true,58062,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.76,false,true,58063,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 3/4 in. (34.9 × 24.8 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.77,false,true,58064,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.78,false,true,58065,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.79,false,true,58066,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.80,false,true,58067,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.81,false,true,58068,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.82,false,true,58069,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.83,false,true,58070,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.84,false,true,58071,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.86,false,true,58073,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 1/2 in. (34.9 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.88,false,true,58076,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.89,false,true,58077,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 1/2 in. (34.9 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.267,false,true,73558,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1786,1865,Polychrome woodblock print; ink and color on paper,Image: 11 3/8 x 8 3/4 in. (28.9 x 22.2 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.268,false,true,73608,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1786,1865,Polychrome woodblock print; ink and color on paper,Image: 11 7/8 x 9 3/8 in. (30.2 x 23.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.269,false,true,73609,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1786,1865,Polychrome woodblock print; ink and color on paper,Image: 14 x 9 3/4 in. (35.6 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.270,false,true,73610,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 14 5/8 x 9 7/8 in. (37.1 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.272,false,true,73612,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 14 3/16 x 9 3/4 in. (36 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.273,false,true,73613,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 1/2 in. (35.9 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.274,false,true,73614,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.275,false,true,73615,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 14 1/8 x 9 1/2 in. (35.9 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3495a–c,false,true,55723,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kunisada,"Japanese, 1786–1865",,Utagawa Kunisada,Japanese,1786,1865,19th century,1800,1865,Triptych of polychrome woodblock prints; ink and color on paper,Each 10 x 14 1/2 in. (25.4 x 36.8 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2202,false,true,53968,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 11 1/16 in. (21 x 28.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2085,false,true,54922,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kikugawa Eizan,"Japanese, 1787–1867",,Kikugawa Eizan,Japanese,1787,1867,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 7/16 in. (14.3 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2133,false,true,54988,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yosai,"Japanese, 1788–1878",,Yosai,Japanese,1788,1878,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/4 x 7 5/8 in. (14.6 x 19.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1963,false,true,54544,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 11/16 x 7 1/8 in. (19.5 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3434,false,true,55634,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Keisai Eisen,"Japanese, 1790–1848",,Keisai Eisen,Japanese,1790,1848,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,14 x 10 in. (35.6 x 25.4 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1041,false,true,54326,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Watanabe Kazan,"Japanese, 1793–1841",,Watanabe Kazan,Japanese,1793,1841,19th century,1800,1841,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/8 in. (21 x 18.1 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2750,false,true,57016,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1899,Woodblock print; ink on thin paper (some corrections have been pasted over the drawing),13 3/4 x 8 1/2 in. (34.9 x 21.6 cm),"Fletcher Fund, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3706,false,true,55956,Asian Art,Print,木曽街道六十九次之内・下諏訪 八重垣姫|Princess Yaegaki,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,14 1/2 x 10 in. (36.8 x 25.4 cm),"Bequest of Grace M. Pugh, 1985",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.22,false,true,58007,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.23,false,true,58008,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.30,false,true,58014,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.38,false,true,58022,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.40,false,true,58024,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1865,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.42,false,true,58026,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 5/8 in. (34.6 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.51,false,true,58035,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.85,false,true,58072,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 1/2 in. (34.9 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.87,false,true,58074,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Polychrome woodblock print; ink and color on paper,Image: 13 3/4 × 9 5/8 in. (34.9 × 24.4 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1806a–e,false,true,45235,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1868,Pentaptych of polychrome woodblock prints; ink and color on paper,14 5/8 x 50 in. (37.1 x 127 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.309,false,true,55987,Asian Art,Print,江戸名所 芝神明|Famous Places of Edo: Shiba Shinmei,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,19th century,1829,1868,Polychrome woodblock print; ink and color on paper,8 1/2 x 13 3/8 in. (21.6 x 34 cm),"Bequest of Gustave von Groschwitz, 1993",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.279,false,true,73562,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige II,"Japanese, 1829–1869",,Utagawa Hiroshige II,Japanese,1829,1869,19th century,1829,1869,Polychrome woodblock print; ink and color on paper,Image: 8 3/8 x 11 1/4 in. (21.3 x 28.6 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.320a–c,false,true,73578,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,19th century,1833,1868,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm) Image (c): 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73578,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1453,false,true,54394,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 1/2 in. (21 x 19.1 cm),"Rogers Fund, 1923",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1934,false,true,54474,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1868,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 3/8 in. (20.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1964,false,true,54545,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1965,false,true,54547,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1966,false,true,54548,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1967,false,true,54549,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1972,false,true,54557,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 1/2 in. (19.7 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1978,false,true,54563,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1868,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 3/16 in. (20.6 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1979,false,true,54564,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1868,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/4 in. (20.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2042,false,true,54813,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 3/16 x 6 1/2 in. (18.3 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2061,false,true,54842,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 7/8 x 6 13/16 in. (20 x 17.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2062,false,true,54843,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 15/16 x 6 3/4 in. (20.2 x 17.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2088,false,true,54929,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 3/8 in. (20.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2089,false,true,54932,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/8 x 7 7/16 in. (21.3 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2090,false,true,54933,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 7/16 x 7 3/8 in. (21.4 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2098,false,true,54947,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1868,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/4 in. (20.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2099,false,true,54948,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1868,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 1/4 in. (20.8 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2100,false,true,54949,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 5/16 in. (20.6 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2101,false,true,54950,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2102,false,true,54954,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2103,false,true,54955,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2104,false,true,54956,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2105,false,true,54957,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 3/16 x 7 5/16 in. (20.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2115,false,true,54968,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 3/16 in. (20.5 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2122,false,true,54976,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/4 in. (20.5 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2127,false,true,54982,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 5/16 in. (20.5 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2164,false,true,55071,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 7/16 x 7 3/8 in. (21.4 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2165,false,true,55072,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 5/16 in. (21 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2262,false,true,54032,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 11/16 x 6 9/16 in. (19.5 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.281,false,true,73564,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Proof line-block print; ink on paper,Image: 11 3/4 x 9 1/2 in. (29.8 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.282,false,true,73637,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Proof line-block print for fan; ink on paper,Image: 11 7/8 x 9 1/2 in. (30.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.283,false,true,73638,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Proof line-block print for fan; ink on paper,Image: 12 x 9 1/2 in. (30.5 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.284,false,true,73639,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Proof line-block print for fan; ink on paper,Image: 12 x 9 1/2 in. (30.5 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.285,false,true,73640,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Proof line-block print for fan; ink on paper,Image: 12 x 9 1/2 in. (30.5 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.286,false,true,73641,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Proof line-block print for fan; ink on paper,Image: 11 3/4 x 9 1/2 in. (29.8 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.287,false,true,73642,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Proof line-block print for fan; ink on paper,Image: 12 x 9 1/2 in. (30.5 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.289,false,true,73566,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Preparatory drawing for triptych of woodblock prints; ink on paper,Image: 14 1/2 x 32 7/8 in. (36.8 x 83.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.297,false,true,73643,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Triptych of polychrome woodblock prints; ink and color on paper,Image: 13 7/8 x 28 5/8 in. (35.2 x 72.7 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.298,false,true,73644,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Polychrome woodblock print; ink and color on paper,Image: 9 3/8 x 14 3/8 in. (23.8 x 36.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.291a–c,false,true,73647,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Triptych of polychrome woodblock prints; ink and color on paper,Image (d): 14 3/8 x 9 3/4 in. (36.5 x 24.8 cm) Image (e): 14 1/2 x 9 5/8 in. (36.8 x 24.4 cm) Image (f): 14 3/8 x 9 1/2 in. (36.5 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.292a–c,false,true,73648,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Triptych of polychrome woodblock prints; ink and color on paper,Image (g): 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm) Image (h): 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm) Image (i): 14 1/8 x 9 1/2 in. (35.9 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.293a–c,false,true,73649,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Triptych of polychrome woodblock prints; ink and color on paper,Image (j): 14 1/8 x 9 5/8 in. (35.9 x 24.4 cm) Image (k): 14 1/8 x 9 1/2 in. (35.9 x 24.1 cm) Image (l): 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.294a–c,false,true,73568,Asian Art,Woodblock print,Tōtō Ryōgokubashi natsu keshiki|Panoramic View of Ryōgoku Bridge in the Summer,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 13 3/4 x 9 1/2 in. (34.9 x 24.1 cm) Image (b): 13 3/4 x 9 1/2 in. (34.9 x 24.1 cm) Image (c): 14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.295a–c,false,true,73569,Asian Art,Woodblock prints,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 13 7/8 x 9 1/2 in. (35.2 x 24.1 cm) Image (b): 13 7/8 x 9 1/4 in. (35.2 x 23.5 cm) Image (c): 13 3/4 x 9 1/2 in. (34.9 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.296a–f,false,true,73570,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Hexaptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (c): 14 1/4 x 9 5/8 in. (36.2 x 24.4 cm) Image (d): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (e): 14 1/4 x 9 7/8 in. (36.2 x 25.1 cm) Image (f): 14 1/8 x 9 7/8 in. (35.9 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.299a–c,false,true,73571,Asian Art,Print,Jōkisen zenzu|Complete Picture of a Steamship off Kanazawa,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/8 x 9 3/8 in. (35.9 x 23.8 cm) Image (b): 14 1/8 x 9 1/4 in. (35.9 x 23.5 cm) Image (c): 14 1/8 x 9 1/4 in. (35.9 x 23.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2097,false,true,54945,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ishikawa Kazan,"Japanese, active 1810–1823",,Ishikawa Kazan,Japanese,1810,1823,19th century,1810,1823,Polychrome woodblock print (surimono); ink and color on paper,8 1/8 x 7 1/4 in. (20.6 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.317,false,true,73576,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,19th century,1850,1880,Woodblock print; ink on paper,Image: 13 1/2 x 9 7/8 in. (34.3 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.319,false,true,73631,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,19th century,1850,1880,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.318a–c,false,true,73577,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshitora,"Japanese, active ca. 1850–80",,Utagawa Yoshitora,Japanese,1845,1880,19th century,1850,1880,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (b): 14 1/4 x 9 3/4 in. (36.2 x 24.8 cm) Image (c): 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1950,false,true,54518,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 1/8 in. (14 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1958,false,true,54530,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 11 3/8 in. (21 x 28.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1970,false,true,54555,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 9/16 in. (14 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1973,false,true,54558,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 7 3/8 in. (21 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1975,false,true,54560,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Polychrome woodblock print (surimono); ink and color on paper,5 3/4 x 6 7/8 in. (14.6 x 17.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1976,false,true,54561,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 1/2 in. (19.7 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1990,false,true,54693,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 7/16 in. (14.3 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1997,false,true,54717,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/8 x 7 in. (13 x 17.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1998,false,true,54718,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 7/16 in. (14.3 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2001,false,true,54721,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 1/2 in. (14.3 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2002,false,true,54722,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 5/8 x 7 1/2 in. (14.3 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2008,false,true,54728,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 5/16 in. (13.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2018,false,true,54767,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 5/8 x 11 1/4 in. (21.9 x 28.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2025,false,true,54785,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 5/16 x 7 5/16 in. (13.5 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2028,false,true,54788,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 7 1/8 in. (20.5 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2034,false,true,54800,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 11/16 x 7 9/16 in. (14.4 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2056,false,true,54832,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 10 3/4 in. (21 x 27.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2058,false,true,54836,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 11/16 x 7 3/8 in. (14.4 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2059,false,true,54838,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/8 x 7 1/4 in. (13.7 x 18.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2065,false,true,54894,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,4 7/8 x 10 3/4 in. (12.4 x 27.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2070,false,true,54901,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 3/8 in. (13.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2071,false,true,54902,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 11/16 x 7 1/2 in. (14.4 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2074,false,true,54905,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 3/8 in. (13.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2082,false,true,54919,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 5/16 in. (13.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2084,false,true,54921,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 1/2 in. (14.1 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2094,false,true,54939,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Polychrome woodblock print (surimono); ink and color on paper,5 11/16 x 7 9/16 in. (14.4 x 19.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2109,false,true,54961,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 1/16 x 6 11/16 in. (17.9 x 17 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2111,false,true,54963,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 7/16 in. (14.1 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2116,false,true,54969,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 3/16 x 6 9/16 in. (18.3 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2118,false,true,54971,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2119,false,true,54972,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 7 3/8 in. (13.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2154,false,true,55060,Asian Art,Print,"『春雨集』 摺物帖柳々居辰斎画 若松|Spring Rain Collection (Harusame shū), vol. 2: Pine Shoots and Accoutrements for New Year’s Celebrations",Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Privately published woodblock prints (surimono) mounted in an album; ink and color on paper,5 7/16 x 7 3/8 in. (13.8 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2159,false,true,55066,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 3/16 x 7 1/16 in. (13.2 x 17.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2172,false,true,55086,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,7 13/16 x 7 5/16 in. (19.8 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2173,false,true,55087,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Polychrome woodblock print (surimono); ink and color on paper,5 5/16 x 7 3/8 in. (13.5 x 18.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2237,false,true,54005,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,3 1/2 x 7 1/8 in. (8.9 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2245,false,true,54014,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Part of an album of woodblock prints (surimono); ink and color on paper,3 1/2 x 7 1/8 in. (8.9 x 18.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2246,false,true,54015,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Part of an album of woodblock prints (surimono); ink and color on paper,7 5/8 x 10 7/8 in. (19.4 x 27.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2252,false,true,54021,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 5/16 in. (14 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2253,false,true,54022,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 13/16 x 7 7/16 in. (14.8 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2263,false,true,54033,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2264,false,true,54034,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 1/2 in. (14 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2265,false,true,54035,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,7 3/4 x 6 9/16 in. (19.7 x 16.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2266,false,true,54039,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 7/16 in. (14 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2267,false,true,54040,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1823,Part of an album of woodblock prints (surimono); ink and color on paper,5 1/2 x 7 1/2 in. (14 x 19.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2370,false,true,54154,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Ryūryūkyo Shinsai,"Japanese, active ca. 1799–1823",,Ryūryūkyo Shinsai,Japanese,1799,1823,19th century,1800,1899,Part of an album of woodblock prints (surimono); ink and color on paper,6 3/8 x 11 5/8 in. (16.2 x 29.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.45,false,true,58029,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,19th century,1850,1870,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.46,false,true,58030,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Yoshikazu,"Japanese, active ca. 1850–1870",,Utagawa Yoshikazu,Japanese,1845,1870,19th century,1850,1870,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.52,false,true,58036,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Sadakage,"Japanese, active mid-19th century",,Utagawa Sadakage,Japanese,1800,1899,19th century,1834,1866,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.53,false,true,58037,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Sadakage,"Japanese, active mid-19th century",,Utagawa Sadakage,Japanese,1800,1899,19th century,1834,1866,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.54,false,true,58038,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Sadakage,"Japanese, active mid-19th century",,Utagawa Sadakage,Japanese,1800,1899,19th century,1834,1866,Polychrome woodblock print; ink and color on paper,Image: 13 5/8 × 9 1/2 in. (34.6 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP212.1,false,true,57986,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1800,1858,Polychrome woodblock print; ink and color on paper,Image: 13 1/2 × 9 1/2 in. (34.3 × 24.1 cm),"Gift of the Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/57986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.278,false,true,73561,Asian Art,Print,六十余州名所図会 薩摩 坊ノ浦 雙剣石|Upright Landscape,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1800,1858,Polychrome woodblock print; ink and color on paper,Image: 14 3/16 x 9 3/4 in. (36 x 24.8 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2748.3,false,true,58267,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,Approx. 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of Mary Martin, 1938",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2748.1,false,true,58265,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,Approx. 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of Mary Martin, 1938",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2748.2,false,true,58266,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,Approx. 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of Mary Martin, 1938",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2748.4,false,true,58268,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,Approx. 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of Mary Martin, 1938",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2748.5,false,true,58269,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,Approx. 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of Mary Martin, 1938",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2748.6,false,true,58270,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Utagawa Yoshiiku,"Japanese, 1833–1904",,Utagawa Yoshiiku,Japanese,1833,1904,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,Approx. 14 1/2 x 9 1/2 in. (36.8 x 24.1 cm),"Bequest of Mary Martin, 1938",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/58270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.309,false,true,73630,Asian Art,Print,真柴秀吉公名護屋陣先手諸将繰出之図|Hideyoshi and His Troops Leaving Nagoya Camp (Mashiba Hideyoshi kō nagoya jin saki te no shoshō kuridashi no zu),Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,19th century,1800,1899,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 13 7/8 x 9 3/4 in. (35.2 x 24.8 cm) Image (b): 13 7/8 x 9 7/8 in. (35.2 x 25.1 cm) Image (c): 13 7/8 x 9 7/8 in. (35.2 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.314,false,true,73627,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Tsukioka Yoshitoshi,"Japanese, 1839–1892",,Tsukioka Yoshitoshi,Japanese,1839,1892,19th century,1800,1899,Polychrome woodblock print; ink and color on paper,Image: 14 9/16 x 9 7/8 in. (37 x 25.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.321a–c,false,true,73579,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Kobayashi Kiyochika,"Japanese, 1847–1915",,Kobayashi Kiyochika,Japanese,1847,1915,19th century,1800,1899,Triptych of polychrome woodblock prints; ink and color on paper,Image (a): 14 x 9 1/2 in. (35.6 x 24.1 cm) Image (b): 9 3/8 x 14 in. (23.8 x 35.6 cm) Image (c): 14 x 9 1/2 in. (35.6 x 24.1 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.325,false,true,73580,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Unsen,"Japanese, active ca. 1875",,Unsen,Japanese,1875,1875,19th century,1800,1899,Triptych of preparatory drawings; ink on paper,Image: 14 1/8 x 27 3/4 in. (35.9 x 70.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.326,false,true,73633,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Unsen,"Japanese, active ca. 1875",,Unsen,Japanese,1875,1875,19th century,1800,1899,Triptych of proof prints; ink on paper,Image: 15 1/8 x 30 1/8 in. (38.4 x 76.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.327,false,true,73634,Asian Art,Print,,Japan,Meiji period (1868–1912),,,,Artist,,Unsen,"Japanese, active ca. 1875",,Unsen,Japanese,1875,1875,19th century,1800,1899,Triptych of polychrome woodblock prints; ink and color on paper,Image: 14 1/4 x 28 7/8 in. (36.2 x 73.3 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/73634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.367,false,true,62816,Asian Art,Bowl,,Japan,,,,,Artist,,Seifu Yohei,1803–1861,,Seifu Yohei,Japanese,1803,1861,19th century,1800,1899,"White porcelain decorated with blue under the glaze, polychrome enamels and gold (Kyoto ware)",H. 2 1/8 in. (5.4 cm); Diam. 5 1/8 in. (13 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.368,false,true,46485,Asian Art,Bowl,,Japan,,,,,Artist,,Seifu Yohei,1803–1861,,Seifu Yohei,Japanese,1803,1861,19th century,1800,1899,"White porcelain decorated with blue under the glaze, polychrome enamels (Kyoto ware)",H. 2 3/8 in. (6 cm); Diam. 5 1/2 in. (14 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/46485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.151,false,true,62679,Asian Art,Bowl,,Japan,,,,,Artist,,Nin'ami Dōhachi (Dōhachi II),"Japanese, 1783–1855",,Dōhachi II,Japanese,1783,1855,19th century,1800,1899,"Clay partly covered with glaze; reserves forming patterns having slip dots in the center, covered with a transparent glaze (Kyoto ware)",H. 3 1/4 in. (8.3 cm); Diam. 6 3/8 in. (16.2 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.215.55,false,true,53585,Asian Art,Tile,,Japan,,,,,Artist,,Raku Tannyu,"Japanese, 1795–1854",,"Raku, Tannyu",Japanese,1795,1854,19th century,1800,1899,Earthenware (Raku ware),H. 4 1/2 (11.4 cm); W. 2 3/4 in. (7 cm),"Fletcher Fund, 1925",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/53585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.13,false,true,62871,Asian Art,Wine bottle,,Japan,,,,,Artist,,Shuntai,"Japanese, 1799–1878",,Shuntai,Japanese,1799,1799,19th century,1800,1899,Clay covered with buff crackled glaze and decoration in overglaze (Shino Oribe type),H. 9 1/8 in. (23.2 cm),"Rogers Fund, 1907",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.3,false,true,62608,Asian Art,Deep bowl,,Japan,,,,,Artist,,Shuntai,"Japanese, 1799–1878",,Shuntai,Japanese,1799,1799,19th century,1800,1899,"Clay covered with a transparent crackled glaze over incised decoration (Mino ware, Ofuke type)",H. 4 in. (10.2 cm); Diam. 5 1/2 in. (14 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.392.1,false,true,63937,Asian Art,Teabowl,,Japan,,,,,Artist,,Mitsutada Ohi,"Japanese, 1835–1896",,Mitsutada Ohi,Japanese,1835,1896,19th century,1800,1899,Pottery (brown raku ware),H. 4 1/4 in. (10.8 cm); Diam. 4 1/4 in. (10.8 cm),"Gift of Toshiro Ohi, 1984",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/63937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.271,false,true,47808,Asian Art,Vase,,Japan,,,,,Artist,,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,19th century,1800,1899,Clay covered with high-fired glazes,H. 6 3/4 in. (17.1 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.423,false,true,47983,Asian Art,Vase,,Japan,,,,,Artist,,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,19th century,1800,1899,Porcelain covered with a dark green glaze and design in white enamel (Kyoto ware),H. 5 3/4 in. (14.6 cm); Diam. 5 1/2 in. (14 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.480,false,true,47970,Asian Art,Vase,,Japan,,,,,Artist,,Makuzu Kōzan I (Miyagawa Toranosuke),"Japanese, 1842–1916",,Makuzu Kōzan,Japanese,1842,1916,19th century,1800,1899,White porcelain covered with a mazarine blue glaze (Kyoto ware),H. 11 3/8 in. (28.9 cm); Diam. 11 in. (27.9 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.321,false,true,47982,Asian Art,Vase,,Japan,,,,,Artist,,Takemoto,"Japanese, 1845–1892",,Takemoto,Japanese,1845,1892,19th century,1800,1899,White porcelain with a dappled red and white glaze,H. 4 1/4 in. (10.8 cm); Diam. 9 5/8 in. (24.4 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.3.382,false,true,48015,Asian Art,Flower vase,,Japan,,,,,Artist,,Takemoto,"Japanese, 1845–1892",,Takemoto,Japanese,1845,1892,19th century,1800,1899,"White porcelain, faintly green, with a flambé collar",H. 8 1/4 in. (21 cm); Diam. 5 in. (12.7 cm),"Gift of Charles Stewart Smith, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/48015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.91,false,true,62646,Asian Art,Teabowl,,Japan,,,,,Artist,,Kiyomizu Rokubei III,"Japanese, active 1820–1880",,Kiyomizu Rokubei III,Japanese,1820,1880,19th century,1800,1899,Clay covered with finely crackled glaze over a design (Kiyomizu ware),H. 2 1/4 in. (5.7 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.135,false,true,47357,Asian Art,Bowl,,Japan,,,,,Artist,,Nonomura Ninsei,"Japanese, active ca. 1646–94",,Ninsei Nonomura,Japanese,1646,1694,19th century,1800,1899,"Clay covered with a transparent crackled glaze and decorated with polychrome enamel (Kyoto ware, Banko style)",H. 3 in. (7.6 cm); Diam. 4 3/4 in. (12.1 cm); Diam. of foot 1 7/8 in. (4.8 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.697,false,true,52317,Asian Art,Wine cup,,Japan,Edo period (1615–1868),,,,Artist,Design attributed to,Aoki Mokubei,1767–1833,,Aoki Mokubei,Japanese,1767,1833,19th century,1800,1899,Porcelain with green and red enamels (Kyoto ware),H. 1 1/2 in. (3.8 cm); Diam. 4 1/4 in. (10.8 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/52317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.698,false,true,52318,Asian Art,Wine cup,,Japan,Edo period (1615–1868),,,,Artist,Design attributed to,Aoki Mokubei,1767–1833,,Aoki Mokubei,Japanese,1767,1833,19th century,1800,1899,Porcelain with green and red enamels (Kyoto ware),H. 1 1/2 in. (3.8 cm); Diam. 3 1/4 in. (8.3 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/52318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.127,false,true,58286,Asian Art,Dish,,Japan,Edo period (1615–1868),,,,Artist,Style of,Ogata Kenzan,"Japanese, 1663–1743",,Ogata Kenzan,Japanese,1663,1743,19th century,1800,1899,Stoneware with colored enamels (Kyoto ware),H. 1 1/2 in. (3.8 cm); W. 6 7/8 in. (17.5 cm),Museum Accession,,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/58286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.34,false,true,62625,Asian Art,Dish,,Japan,Edo period (1615–1868),,,,Artist,,Shuntai,"Japanese, 1799–1878",,Shuntai,Japanese,1799,1799,19th century,1800,1899,"Glazed stoneware (Seto ware, Oribe Revival type)",H. 2 1/4 in. (5.7 cm); W. 9 in. square (22.9 cm square),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"23.225.5a, b",false,true,60782,Asian Art,Incense box,,Japan,Edo (1615–1868) or Meiji period (1868–1912),,,,Artist,,Minpei,active 19th century,,Minpei,Japanese,0019,0019,19th century,1800,1899,"Pottery decorated with colors, black and gold (Awaji ware)",H. 2 1/2 in. (6.4 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/60782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.336,false,true,47856,Asian Art,Figure,,Japan,Edo (1615–1868) or Meiji period (1868–1912),,,,Artist,,Kawamoto Hansuke IV,"Japanese, active first half of the 19th century",,Kawamoto Hansuke,Japanese,1800,1849,19th century,1800,1899,Stoneware and white porcelain (Seto ware),H. 4 1/2 in. (11.4 cm); L. 9 3/8 in. (23.8 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/47856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.225.337,false,true,48557,Asian Art,Figure,,Japan,Edo (1615–1868) or Meiji period (1868–1912),,,,Artist,,Kawamoto Hansuke IV,"Japanese, active first half of the 19th century",,Kawamoto Hansuke,Japanese,1800,1849,19th century,1800,1899,Stoneware and porcelain (Seto ware),H. 6 1/4 in. (15.9 cm); L. 10 1/8 in. (25.7 cm),"Gift of Mrs. V. Everit Macy, 1923",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/48557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.13,false,true,49065,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Kishi Ganryo,1798–1852,,Kishi Ganryo,Japanese,1798,1852,19th century,1800,1899,Hanging scroll; ink and color on silk,38 1/2 x 13 1/2 in. (97.8 x 34.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.2,false,true,57227,Asian Art,Hanging scroll,,Japan,,,,,Artist,,Yusen,"Japanese, 1778–1850",,Yusen,Japanese,1778,1850,19th century,1800,1850,Hanging scroll; ink and color on silk,39 3/8 x 12 1/8 in. (100 x 30.8 cm),"Fletcher Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.70.1,false,true,57246,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; ink on silk,41 1/4 x 11 1/2 in. (104.8 x 29.2 cm),"Gift of Edward M. Bratter, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.70.3,false,true,57248,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; ink on silk,5 1/16 x 4 3/4 in. (12.9 x 12.1 cm),"Gift of Edward M. Bratter, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.31.1,false,true,57242,Asian Art,Painting,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Painting; colored lacquers on paper,8 x 6 3/4 in. (20.3 x 17.1 cm),"Rogers Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.137.1,false,true,57315,Asian Art,Hanging scroll,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; ink on silk,42 7/8 x 16 3/8 in. (108.9 x 41.6 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.124.1,false,true,57294,Asian Art,Painting,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Painting; ink and color on paper (tanzaku),14 3/4 x 2 1/4 in. (37.5 x 5.7 cm),"Funds from various donors, by exchange, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.124.2,false,true,57295,Asian Art,Painting,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Painting; ink and color on paper (tanzaku),14 5/8 x 2 7/16 in. (37.1 x 6.2 cm),"Funds from various donors, by exchange, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.124.3,false,true,57296,Asian Art,Painting,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Painting; ink and color on paper (tanzaku),14 x 2 5/8 in. (35.6 x 6.7 cm),"Funds from various donors, by exchange, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.124.4,false,true,57297,Asian Art,Painting,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Painting; ink and color on paper (tanzaku),14 3/4 x 2 3/8 in. (37.5 x 6 cm),"Funds from various donors, by exchange, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.124.5,false,true,57298,Asian Art,Painting,,Japan,,,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Painting; ink and color on paper (tanzaku),14 x 2 1/2 in. (35.6 x 6.4 cm),"Funds from various donors, by exchange, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.5,false,true,55299,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Shiokawa Bunrin,"Japanese, 1808–1877",,Shiokawa Bunrin,Japanese,1808,1877,19th century,1800,1899,Hanging scroll; ink and color on silk,39 1/4 x 13 7/8 in. (99.7 x 35.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.282.2,false,true,57164,Asian Art,Hanging scroll,,Japan,,,,,Artist,,Tsurana,"Japanese, 1809–1892",,Tsurana,Japanese,1809,1892,19th century,1809,1892,Hanging scroll; ink and color on silk,15 1/2 x 49 1/2 in. (39.4 x 125.7 cm),"Gift of Dr. and Mrs. Harold B. Bilsky, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.16,false,true,55302,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Hasegawa Gyokuho,"Japanese, 1822–1879",,Hasegawa Gyokuho,Japanese,1822,1879,19th century,1822,1879,Hanging scroll; ink and color on silk,Overall: 41 x 16 1/8in. (104.1 x 41cm) Overall with mounting: 41 x 21 3/4 in. (104.1 x 55.2 cm) Overall with knobs: 41 x 24 in. (104.1 x 61 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/55302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.35.2,false,true,57314,Asian Art,Painting,,Japan,,,,,Artist,,Toyosei Kimigi,"Japanese, active 19th century",(?),Toyosei Kimigi,Japanese,1800,1899,19th century,1800,1899,Painting; color on silk,24 5/8 x 34 5/8 in. (62.5 x 87.9 cm),"Gift of Susan Dwight Bliss, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.14,false,true,45737,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tansui,active 19th century,,Kano Tansui,Japanese,1800,1899,19th century,1800,1868,Hanging scroll; ink and color on silk,37 1/2 x 13 1/2 in. (95.3 x 34.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.15,false,true,44876,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tansui,active 19th century,,Kano Tansui,Japanese,1800,1899,19th century,1800,1868,Hanging scroll; ink and color on silk,37 1/2 x 13 1/2 in. (95.3 x 34.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.16,false,true,45738,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tansui,active 19th century,,Kano Tansui,Japanese,1800,1899,19th century,1800,1868,Hanging scroll; ink and color on silk,37 1/2 x 13 1/2 in. (95.3 x 34.3 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.1,false,true,48985,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Matted painting; ink and color on paper,39 7/16 x 14 3/16 in. (100.2 x 36 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.2,false,true,48986,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Matted painting; ink and color on paper,39 7/16 x 14 3/16 in. (100.2 x 36 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.3,false,true,48987,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,19th century,1800,1899,Matted painting; ink and color on paper,39 7/16 x 14 3/16 in. (100.2 x 36 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.5,false,true,48911,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishikawa Sukenobu,"Japanese, 1671–1750",,Nishikawa Sukenobu,Japanese,1671,1750,19th century,1800,1899,Hanging scroll; ink and color on silk,21 1/2 x 25 in. (54.6 x 63.5 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.11,false,true,49063,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ganku,"Japanese, 1749–1838",,Ganku,Japanese,1749,1838,19th century,1800,1838,Hanging scroll; ink and color on silk,42 x 16 1/8 in. (106.7 x 41 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.265,false,true,65574,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,19th century,1800,1899,"Hanging scroll; ink, color, and gold on silk",37 5/16 x 12 7/16 in. (94.7 x 31.6 cm),"Purchase, Friends of Asian Art Gifts, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/65574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.106,false,true,49023,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nakabayashi Chikutō,"Japanese, 1776–1853",,Nakabayashi Chikutō,Japanese,1776,1853,19th century,1800,1853,Hanging scroll; ink on paper,Image: 69 1/2 x 34 in. (176.5 x 86.4 cm) Overall: 92 1/4 x 46 1/4 in. (234.3 x 117.5 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.104,false,true,48997,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Okada Hankō,"Japanese, 1782–1846",,Okada Hankō,Japanese,1782,1846,19th century,1800,1846,Hanging scroll; ink and color on paper,Image: 68 1/2 x 18 7/8 in. (174 x 47.9 cm) Overall: 92 1/4 x 28 3/8in. (234.3 x 72.1cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.54,false,true,45739,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kano Tanshin Moromichi,"Japanese, 1785–1835",,Tanshin Moromichi Kano,Japanese,1785,1835,19th century,1800,1835,Hanging scroll; ink on silk,36 11/16 x 15 in. (93.2 x 38.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.483.2,false,true,49821,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Ōtagaki Rengetsu,"Japanese, 1791–1871",,Ōtagaki Rengetsu,Japanese,1791,1871,19th century,1800,1868,"Hanging scroll; ink, color, and silver on paper",38 1/4 x 11 7/8 in. (97.2 x 30.2 cm),"Gift of Donald Keene, 1998",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.114,false,true,45379,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Watanabe Kazan,"Japanese, 1793–1841",,Watanabe Kazan,Japanese,1793,1841,19th century,1800,1841,Hanging scroll; ink and color on paper,29 1/8 x 17 in. (74 x 43.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.137.1,false,true,48983,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Kiitsu,"Japanese, 1796–1858",,Suzuki Kiitsu,Japanese,1796,1858,19th century,1800,1858,Hanging scroll; ink and color on silk,48 1/2 x 13 5/16 in. (123.2 x 33.8 cm),"Rogers Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.537,false,true,40352,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Utagawa Kuniyoshi,"Japanese, 1797–1861",,Utagawa Kuniyoshi,Japanese,1797,1861,19th century,1800,1861,Matted; ink and color on silk,16 1/2 x 23 1/2 in. (41.9 x 59.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.118,false,true,49011,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,19th century,1800,1854,Hanging scroll; ink and color on silk,43 15/16 x 18 7/8 in. (111.6 x 48 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.119,false,true,49012,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tsubaki Chinzan,"Japanese, 1801–1854",,Tsubaki Chinzan,Japanese,1801,1854,19th century,1800,1854,Hanging scroll; ink and color on paper,65 7/8 x 33 1/4 in. (167.3 x 84.4 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.14,false,true,49066,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Gantoku,"Japanese, 1805–1859",,Gantoku,Japanese,1805,1859,19th century,1805,1859,Hanging scroll; ink on paper,38 1/2 x 12 in. (97.8 x 30.5 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.30,false,true,49054,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nishiyama Hōen,"Japanese, 1807–1867",,Nishiyama Hōen,Japanese,1807,1867,19th century,1807,1867,Hanging scroll; ink and color on silk,39 5/16 x 16 1/8 in. (99.8 x 41 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.137.2,false,true,57317,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1868,Hanging scroll; ink on silk,39 1/8 x 14 in. (99.4 x 35.6 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.137.3,false,true,57318,Asian Art,Hanging scroll,三羽黒鳥図|Three Crows in Flight,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; ink on silk,Image: 28 7/8 × 6 3/4 in. (73.3 × 17.1 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.137.4,false,true,57319,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; ink on paper,Image: 13 5/8 x 21 3/8 in. (34.6 x 54.3 cm) Overall with knobs: 50 1/4 x 25 in. (127.6 x 63.5 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.282.3,false,true,49028,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Nakabayashi Chikkei,"Japanese, 1816–1867",,Nakabayashi Chikkei,Japanese,1816,1867,19th century,1816,1867,Hanging scroll; ink and color on silk,47 1/2 x 16 1/2 in. (120.7 x 41.9 cm),"Gift of Dr. and Mrs. Harold B. Bilsky, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.21,false,true,45188,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Hanzan (Matsukawa),"Japanese, 1820–1882",,Hanzan,Japanese,1820,1882,19th century,1800,1882,Hanging scroll; ink and color on paper,12 1/2 x 16 13/16 in. (31.8 x 42.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.513,false,true,650682,Asian Art,Hanging scroll,神宮皇后|Empress Jingū,Japan,Edo period (1615–1868),,,,Artist,,Kōsai Hokushin,"Japanese, 1824–1876",,Kōsai Hokushin,Japanese,1824,1876,19th century,1800,1899,"Hanging scroll; ink, color, and gold on silk",Image: 28 7/8 × 12 15/16 in. (73.3 × 32.9 cm) Overall with mounting: 63 1/8 × 16 5/16 in. (160.3 × 41.4 cm) Overall with knobs: 63 1/8 × 18 in. (160.3 × 45.7 cm),"Purchase, Friends of Asian Art Gifts, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/650682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.129,false,true,45809,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Kuniteru,"Japanese, 1830–1874",,Utagawa Kuniteru,Japanese,1830,1874,19th century,1800,1868,Matted painting; ink on paper,14 3/8 x 29 5/16 in. (36.5 x 74.5 cm),"Gift of Lincoln Kirstein, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.111,false,true,49002,Asian Art,Folding fan,,Japan,Edo period (1615–1868),,,,Artist,,Takahashi Sōhei,"Japanese, 1804?–?1835",,Takahashi Sōhei,Japanese,1804,1835,19th century,1804,1835,Folding fan; ink on paper,10 3/16 x 16 15/16 in. (25.8 x 43.1 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.49.288,false,true,73565,Asian Art,Sketch,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa (Gountei) Sadahide,"Japanese, 1807–1878/79",,Utagawa (Goutei) Sadahide,Japanese,1807,1879,19th century,1807,1879,Preparatory drawing; ink on paper,Image: 9 1/2 x 12 in. (24.1 x 30.5 cm),"Bequest of William S. Lieberman, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/73565,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.45,false,true,48881,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Hokuga,"Japanese, active early 19th century",,Hokuga,Japanese,1800,1835,19th century,1800,1868,Hanging scroll; ink and color on silk,Image: 12 13/16 × 22 5/8 in. (32.5 × 57.5 cm) Overall with mounting: 50 1/8 × 28 3/8 in. (127.3 × 72 cm) Overall with knobs: 50 1/8 × 30 9/16 in. (127.3 × 77.6 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.37,false,true,45813,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1849,Hanging scroll; ink and color on silk,Image: 33 11/16 × 13 9/16 in. (85.5 × 34.5 cm) Overall with mounting: 59 1/2 × 16 15/16 in. (151.2 × 43 cm) Overall with knobs: 59 1/2 × 19 3/16 in. (151.2 × 48.7 cm),"Seymour Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.119.3,false,true,48889,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1849,Matted painting; ink on paper (wash drawing?),10 3/8 x 14 in. (26.4 x 35.6 cm),"Fletcher Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.28,false,true,45817,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1849,Hanging scroll; ink and color on silk,Image: 27 3/8 × 10 15/16 in. (69.5 × 27.8 cm) Overall with mounting: 62 5/8 × 16 15/16 in. (159 × 43 cm) Overall with knobs: 62 5/8 × 19 in. (159 × 48.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.29,false,true,48883,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1849,Hanging scroll; ink and color on paper,Image: 44 5/16 × 9 3/16 in. (112.5 × 23.4 cm) Overall with mounting: 80 9/16 × 10 11/16 in. (204.7 × 27.2 cm) Overall with knobs: 80 9/16 × 12 1/2 in. (204.7 × 31.8 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.507,false,true,45820,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1849,Hanging scroll; ink and color on paper,Image: 30 7/8 × 9 5/8 in. (78.5 × 24.5 cm) Overall with mounting: 60 3/4 × 13 3/16 in. (154.3 × 33.5 cm) Overall with knobs: 60 3/4 × 15 1/8 in. (154.3 × 38.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.59.1–.102,false,true,40011,Asian Art,Album leaves,葛飾北斎筆 鶏と木材鶏図|Album of Sketches by Katsushika Hokusai and His Disciples,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1868,Album of ninety-seven leaves; ink and color on paper,Each leaf: 15 1/2 x 10 1/2 in. (39.4 x 26.7 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.60.1–.109,false,true,48886,Asian Art,Album leaves,,Japan,Edo period (1615–1868),,,,Artist,,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",and others,Katsushika Hokusai,Japanese,1760,1849,19th century,1800,1868,"Album of one hundred and nine leaves; ink on paper, ink and color on paper",15 3/8 x 10 11/16 in. (39.1 x 27.1 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.4,false,true,40336,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1797,1858,Matted painting; ink and color on silk (Ukiyo-e),13 15/16 x 21 7/16 in. (35.4 x 54.5 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.5,false,true,40337,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1797,1858,Matted painting; ink and color on silk,13 15/16 x 21 7/16 in. (35.4 x 54.4 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.6,false,true,40338,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1797,1858,Matted painting; ink and color on silk,13 15/16 x 21 7/16 in. (35.4 x 54.4 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.7,false,true,40339,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1797,1858,Matted painting; ink and color on silk,13 15/16 x 21 7/16 in. (35.4 x 54.4 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.8,false,true,40340,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1797,1858,Matted painting; ink and color on silk,8 1/4 x 12 3/16 in. (21.0 x 30.9 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.148.9,false,true,40341,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1797,1858,Matted painting; ink and color on silk,8 1/4 x 12 3/16 in. (20.9 x 31 cm),"Gift of Francis Lathrop, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/40341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.130,false,true,45425,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Hiroshige,"Japanese, Tokyo (Edo) 1797–1858 Tokyo (Edo)",,"Utagawa, Hiroshige",Japanese,1797,1858,19th century,1800,1858,Hanging scroll; ink and color on silk,16 3/8 x 22 5/8 in. (41.6 x 57.5 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.2,false,true,57328,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; color on silk,13 3/4 x 5 in. (34.9 x 12.7 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.194.1,false,true,57337,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; black paint on silk,37 3/8 x 11 7/8 in. (30.2 x 94.9 cm),"Gift of Nathan V. Hammer, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.161.2,false,true,57342,Asian Art,Hanging scroll,,Japan,Meiji period (1868–1912),,,,Artist,In the Style of,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Hanging scroll; ink and color on silk,14 3/8 x 10 3/4 in. (36.5 x 27.3 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.160.3a–q,false,true,57329,Asian Art,Album leaves,,Japan,Meiji period (1868–1912),,,,Artist,,Shibata Zeshin,"Japanese, 1807–1891",,Shibata Zeshin,Japanese,1807,1891,19th century,1807,1891,Album of seventeen sketches; watercolor on paper; mounted on natural silk,Image: 11 x 14 in. (27.9 x 35.6 cm),"Purchase, Gifts, Bequests, and Funds from various donors, by exchange, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.282.1a–m,false,true,57163,Asian Art,Album,桃に鶴図|Flowers and Birds,Japan,Meiji period (1868–1912),,,,Artist,,Taki Katei,"Japanese, 1830–1901",,Taki Katei,Japanese,1830,1901,19th century,1800,1899,Album of twelve leaves; ink and color on silk,Each painting: 10 x 12 in. (25.4 x 30.5 cm),"Gift of Dr. and Mrs. Harold B. Bilsky, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.119.1,false,true,50829,Asian Art,Handscroll,,Japan,Meiji period (1868–1912),,,,Artist,,Kawanabe Kyōsai,"Japanese, 1831–1889",,Kawanabe Kyōsai,Japanese,1831,1889,19th century,1831,1889,Handscroll; wash drawing,20 ft. 8 in. x 10 7/8 in. (629.9 x 27.6 cm),"Fletcher Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/50829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.26,false,true,58571,Asian Art,Inrō,印籠扇面蒔絵印籠|Inrō with Inrō and Fan,Japan,Edo period (1615–1868),,,,Artist,,Koma Kyūhaku V,"Japanese, died 1794",,Koma Kyūhaku V,Japanese,,1794,late 18th century,1767,1799,"Four cases; lacquered wood with gold, black, red lacquer takamaki-e, hiramaki-e, tgidashimaki-e on red lacquer ground; Netsuke: carved ivory; beans; Ojime: metal bead with insects",H. 2 15/16 in. (7.5 cm); W. 2 1/2in. (6.3 cm); D. 1 in. (2.5 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.68,false,true,45568,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Tōyō,"Japanese, active ca. 1764–71",,Tōyō,Japanese,1764,1771,late 18th century,1767,1799,Silver shibuichi lacquer decorated with roiro (waxen) lacquer and gold sprinkled and polished hiramakie lacquer; Ojime: coral bead; Netsuke: metal zogan inlay pine tree,3 7/16 x 2 9/16 x 1 1/8 in. (8.7 x 6.5 x 2.9 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.211.1271,false,true,60329,Asian Art,Netsuke,草花螳螂牙彫根付|Flowers and Grasses with a Praying-Mantis,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Ryūsa,"Japanese, active late 18th century",,Ryūsa,Japanese,1767,1799,late 18th century,1767,1799,Ivory,H. 7/8 in. (2.2 cm); Diam. 2 1/8 in. (5.4 cm),"Gift of Mrs. Russell Sage, 1910",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/60329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2449,false,true,56879,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Harunobu,"Japanese, 1725–1770",,Suzuki Harunobu,Japanese,1725,1770,late 18th century,1767,1799,"Polychrome woodblock print with embossing (karazuri), ink and color on paper",11 1/8 x 8 1/8 in. (28.3 x 20.6 cm) medium-size print (chu-ban),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1759,false,true,56065,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,late 18th century,1767,1799,Polychrome woodblock pillar print; ink and color on paper,Image: 28 x 5 3/16 in. (71.1 x 13.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1760,false,true,39721,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,27 7/8 x 6 1/2 in. (70.8 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1770,false,true,51092,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,late 18th century,1767,1799,Polychrome woodblock print (pillar print); ink and color on paper,H. 27 in. (68.6 cm); W. 4 5/8 in. (11.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2781,false,true,39723,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,10 1/2 x 7 1/2 in. (26.7 x 19.1 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2782,false,true,39724,Asian Art,Print,Tatohe uta|Analogy,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunshō,"Japanese, 1726–1792",,Katsukawa Shunshō,Japanese,1726,1792,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,10 1/8 x 7 3/4 in. (25.7 x 19.7 cm),"Henry L.Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1621,false,true,55788,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,H. 8 7/8 in. (22.5 cm); W. 12 3/8 in. (31.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1126,false,true,55046,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,late 18th century,1767,1799,Polychrome woodblock pillar print; ink and color on paper,Image: 26 3/4 x 4 9/16 in. (67.9 x 11.6 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1210,false,true,55118,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,H. 26 5/16 in. (66.8 cm); W. 4 3/4 in. (12.1 cm),"Rogers Fund, 1920",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1228,false,true,45039,Asian Art,Print,Shiokumi|Dance of the Beach Maidens,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,Image: 25 3/4 x 4 9/16 in. (65.4 x 11.6 cm),"Rogers Fund, 1922",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1729,false,true,56040,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,H. 27 7/16 in. (69.7 cm); W. 4 5/8 in. (11.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP939,false,true,44989,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kuwagata Keisai,"Japanese, 1764–1824",,Kuwagata Keisai,Japanese,1764,1824,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,H. 14 1/2 in. (36.8 cm); W. 9 8/12 in. (24.6 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/44989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1684,false,true,42697,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,late 18th century,1767,1799,Diptych of polychrome woodblock prints; ink and color on paper,H. 13 7/16 in. (34.1 cm); W. 17 5/8 in. (44.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/42697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1095b,false,true,639384,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,late 18th century,1754,1806,Polychrome woodblock print; ink and color on paper,Image: 14 in. × 9 1/2 in. (35.6 × 24.1 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/639384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1095c,false,true,639385,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,late 18th century,1754,1806,Polychrome woodblock print; ink and color on paper,Image: 14 1/2 × 9 3/8 in. (36.8 × 23.8 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/639385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1095d,false,true,639386,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,late 18th century,1754,1806,Polychrome woodblock print; ink and color on paper,Image: 15 1/8 × 10 1/8 in. (38.4 × 25.7 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/639386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1704,false,true,55945,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,late 18th century,1767,1799,Polychrome woodblock print; ink and color on paper,H. 27 3/16 in. (69.1 cm); W. 5 in. (12.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1711,false,true,45269,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Isoda Koryūsai,"Japanese, 1735–ca. 1790",,Isoda Koryūsai,Japanese,1735,1790,late 18th century,1767,1799,Polychrome woodblock print (hashira-e); ink and color on paper,H. 26 1/8 in. (66.4 cm); W. 4 1/2 in. (11.4 cm),"H. O. Havemeyer Collection; Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1800,false,true,45260,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunzan,"Japanese, active 1782–1798",,Katsukawa Shunzan,Japanese,1782,1798,late 18th century,1767,1799,Triptych of polychrome woodblock prints; ink and color on paper,A: H. 15 in. (38.1 cm); W. 10 1/4 in. (26 cm) B: H. 14 15/16 in. (37.9 cm); W. 10 1/4 in. (26 cm) C: H. 14 7/8 in. (37.9 cm); W. 10 1/4 in. (26 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2663,false,true,51999,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Katsukawa Shunchō,"Japanese, active ca. 1783–95",,Katsukawa Shunchō,Japanese,1775,1795,late 18th century,1767,1799,Triptych of polychrome woodblock prints; ink and color on paper,Image (each): 15 × 10 in. (38.1 × 25.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/51999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1789,false,true,56091,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Rekisentei Eiri,"Japanese, active ca. 1789–1801",,Rekisentei Eiri,Japanese,1789,1801,late 18th century,1767,1799,Triptych of polychrome woodblock prints; ink and color on paper,Image (each): 14 9/16 x 9 3/4 in. (37 x 24.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1095a,false,true,55030,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Juka Sekijō,"Japanese, active ca. 1789–1817",,Juka Sekijō,Japanese,1789,1817,late 18th century,1754,1806,Polychrome woodblock print; ink and color on paper,Image: 13 7/8 in. × 9 in. (35.2 × 22.9 cm),The Metropolitan Museum of Art,,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.98a–p,false,true,45331,Asian Art,Album,,Japan,Edo period (1615–1868),,,,Artist,,Kuwayama Gyokushū,"Japanese, 1746–1799",,Kuwayama Gyokushū,Japanese,1746,1799,late 18th century,1767,1799,Album of fourteen paintings and one calligraphy; Ink and color on paper,Image (each): 8 7/8 x 5 7/16 in. (22.5 x 13.8 cm) Overall (album opened): 10 1/2 x 13 5/8 in. (26.7 x 34.6 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.399.25,false,true,59677,Asian Art,Netsuke,,Japan,,,,,Artist,,Minkō,"Japanese, ca. 1735–1816",,Minkō,Japanese,1735,1816,early 19th century,1800,1833,Wood; brass and horn inlay,H. 1 3/16 in. (3 cm); W. 1 7/8 in. (4.8 cm),"Gift of Alvin H. Schechter, 1985",,,,,,,,,,,,Netsuke,,http://www.metmuseum.org/art/collection/search/59677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.877,false,true,45504,Asian Art,Inrō,秋蔦蒔絵印籠|Inrō with Autumn Ivy,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Koma Kyūhaku VI,"Japanese, died 1816",,Koma Kyūhaku VI,Japanese,,1816,early 19th century,1800,1833,"Four cases; lacquered wood with gold, silver, and color (iroko) togidashimaki-e on black lacquer ground Netsuke: ivory; folded letter decorated with paulownia and Genji incense symbols Ojime: oblong bead; gilt bronze with openwork design of autumn flowers",3 11/16 x 2 3/8 x 3/4 in. (9.3 x 6 x 1.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2001.768.3, .4",false,true,64397,Asian Art,Folding screen,,Japan,Edo period (1615–1868),,,,Artist,,Mori Shūhō,"Japanese, 1738–1823",,Mori Shūhō,Japanese,1738,1823,early 19th century,1800,1833,"Pair of six-panel folding screens; ink, color, and gold flecks on gilded paper",Image (each screen): 5 ft. 2 3/16 in. x 11 ft. 8 15/16 in. (1.58 x 3.58 m),"Gift of Rosemarie and Leighton Longhi, 2001",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/64397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.22,false,true,53935,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Hara Yōyūsai,"Japanese, 1772–1845",,Hara Yōyūsai,Japanese,1772,1845,early 19th century,1800,1833,Case: powdered gold (maki-e) and colored lacquer on black lacquer with mother-of-pearl and gold inlays; Fastener (ojime): ivory carved with abstract design; Toggle (netsuke): ivory carved in the shape of a crab,H. 3 1/4 (8.3 cm); W. 2 in. ( 5.1 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/53935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"54.69.1, .2",false,true,48982,Asian Art,Folding screen,朝顔図屏風|Morning Glories,Japan,Edo period (1615–1868),,,,Artist,,Suzuki Kiitsu,"Japanese, 1796–1858",,Suzuki Kiitsu,Japanese,1796,1858,early 19th century,1800,1833,"Pair of six-panel folding screens; ink, color, and gold leaf on paper",Image (each screen): 70 3/16 x 12 ft. 5 1/2 in. (178.3 x 379.7 cm),"Seymour Fund, 1954",,,,,,,,,,,,Screens,,http://www.metmuseum.org/art/collection/search/48982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1715,false,true,45251,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Kitao Shigemasa,"Japanese, 1739–1820",,Kitao Shigemasa,Japanese,1739,1820,early 19th century,1800,1833,Polychrome woodblock print; ink and color on paper,H. 8 3/8 in. (21.3 cm); W. 14 3/4 in. (37.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/45251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1746,false,true,56053,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,early 19th century,1800,1833,Polychrome woodblock print; ink and color on paper,H. 14 11/16 in. (37.3 cm); W. 9 7/8 in. (25.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1921,false,true,54460,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,early 19th century,1800,1833,Polychrome woodblock print (surimono); ink and color on paper,7 3/4 x 6 5/8 in. (19.7 x 16.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1957,false,true,54529,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,early 19th century,1800,1833,Polychrome woodblock print (surimono); ink and color on paper,8 1/4 x 5 1/8 in. (21 x 13 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2055,false,true,54831,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Totoya Hokkei,"Japanese, 1780–1850",,Totoya Hokkei,Japanese,1780,1850,early 19th century,1800,1833,Polychrome woodblock print (surimono); ink and color on paper,5 7/16 x 11 1/8 in. (13.8 x 28.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2132,false,true,54987,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist,,Yosai,"Japanese, 1788–1878",,Yosai,Japanese,1788,1878,early 19th century,1800,1833,Polychrome woodblock print (surimono); ink and color on paper,5 9/16 x 7 7/16 in. (14.1 x 18.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1683,false,true,37343,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,early 19th century,1800,1833,Triptych of polychrome woodblock prints; ink and color on paper,Overall: 15 1/4 x 29 1/2 in. (38.7 x 74.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/37343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2733,false,true,56794,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Kitagawa Utamaro,"Japanese, 1753?–1806",,Kitagawa Utamaro,Japanese,1753,1806,early 19th century,1800,1833,Right sheet of a triptych of polychrome woodblock prints; ink and color on paper,Image (oban triptych): 15 1/8 x 10 1/8 in. (38.4 x 25.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1102,false,true,54331,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,early 19th century,1800,1833,Diptych of polychrome woodblock prints (surimono); ink and color on paper,a) 8 7/16 x 7 3/8 in. (21.4 x 18.7 cm) b): 8 5/16 x 7 3/8 in. (21.1 x 18.7 cm),"Rogers Fund, 1919",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1920,false,true,54459,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Yashima Gakutei,"Japanese, 1786?–1868",,Yashima Gakutei,Japanese,1786,1868,early 19th century,1800,1833,Polychrome woodblock print (surimono); ink and color on paper,7 11/16 x 6 1/2 in. (19.5 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP3493,false,true,55720,Asian Art,Print,Seishu Futami ga Ura|View of Futami Beach at Ise,Japan,Edo period (1615–1868),,,,Artist,,Shotei Hokuju,"Japanese, active 1790–1820",,Shotei Hokuju,Japanese,1790,1820,early 19th century,1800,1833,Polychrome woodblock print; ink and color on paper,Oban 10 x 15 in. (25.4 x 38.1 cm),"Gift of Cole J. Younger, 1975",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP1827,false,true,56113,Asian Art,Print,,Japan,Edo period (1615–1868),,,,Artist,,Koikawa Harumasa,"Japanese, active 1800–1820",,Koikawa Harumasa,Japanese,1800,1820,early 19th century,1800,1833,Polychrome woodblock print; ink and color on paper,H. 23 1/2 in. (59.7 cm); W. 4 9/16 in. (11.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/56113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.1.20,false,true,62617,Asian Art,Teabowl,,Japan,,,,,Artist,,Kenzan III,"Japanese, 1767–1810",,Kenzan III,Japanese,1767,1810,early 19th century,1800,1833,"Clay, ribbed and covered a mottled glaze (Kenzan ware)",H. 3 in. (7.6 cm),"Gift of Mr. and Mrs. Samuel Colman, 1893",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.118.67,false,true,62886,Asian Art,Teabowl,,Japan,Edo period (1615–1868),,,,Artist,,Raku Ryōnyū,"Japanese, 1756–1834",,Raku Ryonyu,Japanese,1756,1834,early 19th century,1800,1833,Clay covered with a black glaze (Raku ware),H. 3 3/8 in. (8.6 cm); Diam. 4 in. (10.2 cm),"Rogers Fund, 1917",,,,,,,,,,,,Ceramics,,http://www.metmuseum.org/art/collection/search/62886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.46,false,true,39725,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Torii Kiyonaga,"Japanese, 1752–1815",,Torii Kiyonaga,Japanese,1752,1815,early 19th century,1800,1815,Hanging scroll; ink and color on silk,Image: 15 3/4 × 23 1/4 in. (40 × 59.1 cm) Overall with mounting: 51 1/8 × 28 1/4 in. (129.9 × 71.8 cm) Overall with knobs: 51 1/8 × 30 3/8 in. (129.9 × 77.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/39725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.43,false,true,45806,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,early 19th century,1800,1828,Hanging scroll; ink and color on silk,31 5/16 x 10 1/4 in. (79.6 x 26 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.129,false,true,45808,Asian Art,Hanging scroll,夕顔棚下納涼図|Enjoying the Evening Cool under a Gourd Trellis,Japan,Edo period (1615–1868),,,,Artist,,Utagawa Toyohiro,"Japanese, 1763–1828",,Utagawa Toyohiro,Japanese,1763,1828,early 19th century,1800,1828,Hanging scroll; ink and color on paper,Image: 33 1/4 x 11 in. (84.5 x 27.9 cm) Overall with knobs: 65 1/8 x 16 1/2 in. (165.4 x 41.9 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.445,false,true,49005,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tani Bunchō,"Japanese, 1763–1840",,Tani Bunchō,Japanese,1763,1840,early 19th century,1800,1833,Hanging scroll; ink and color on silk,64 15/16 x 44 3/4 in. (165 x 113.6 cm),"Gift of Mr. and Mrs. Theodore R. Conant, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.26,false,true,42314,Asian Art,Hanging scroll,三味線持つ美人図|Female Entertainer with Shamisen,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Teisai Hokuba,"Japanese, 1771–1844",,Teisai Hokuba,Japanese,1771,1844,early 19th century,1800,1828,Hanging scroll; ink and color on paper,31 1/8 x 11 1/4 in. (79.1 x 28.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.43,false,true,72603,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Tōshū,"Japanese, active ca. 1800",,Tōshū,Japanese,1790,1810,early 19th century,1800,1833,Hanging scroll; ink and color on silk,Image: 39 7/8 x 11 1/8 in. (101.3 x 28.3 cm) Overall with mounting: 62 1/2 x 15 1/2 in. (158.8 x 39.4 cm) Overall with rollers: 62 1/2 x 18 1/8 in. (158.8 x 46 cm),"Gift of Jack Jacoby, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/72603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.197,false,true,58882,Asian Art,Inrō,,Japan,,,,,Artist,,Kano Seisen’in,1775–1828,,Kano Seisen’in,Japanese,1775,1828,18th–19th century,1700,1899,"Lacquer, roiro, yamimakie, black hiramakie, takamakie; Interior: gyobu nashiji and fundame",3 11/16 x 1 7/8 x 1 1/16 in. (9.4 x 4.8 x 2.7 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.795,false,true,58704,Asian Art,Inrō,,Japan,,,,,Artist,,Hanabusa Itchō,"Japanese, 1652–1724",,Hanabusa Itchō,Japanese,1652,1724,18th–19th century,1700,1899,"Lacquer, roiro, gold, silver, brown and red hiramakie, various inlay; Interior: nashiji and fundame",4 1/8 x 1 3/4 x 15/16 in. (10.4 x 4.4 x 2.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.228,false,true,58916,Asian Art,Inrō,,Japan,,,,,Artist,Attributed to,Ogawa Haritsu (Ritsuō),"Japanese, 1663–1747",,Ritsuō,Japanese,1663,1747,18th–19th century,1700,1899,"Lacquer, roiro, gold and silver hiramakie, togidashi, kimpun, ivory, horn inlay; Interior: fundame",3 3/8 x 2 3/16 x 7/8 in. (8.5 x 5.5 x 2.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.191,false,true,58878,Asian Art,Inrō,,Japan,,,,,Artist,,Maruyama Ōkyo,"Japanese, 1733–1795",,Maruyama Ōkyo,Japanese,1733,1795,18th–19th century,1700,1899,"Lacquer, roiro, gold and silver togidashi, mura nashiji; Interior: roiro and fundame",2 15/16 x 2 7/16 x 1 in. (7.4 x 6.2 x 2.6 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.46.1,false,true,53814,Asian Art,Printer's woodblock,,Japan,,,,,Artist,Original print designed by,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,18th–19th century,1700,1899,,15 1/2 x 10 1/2 in. (39.4 x 26.7 cm),"Gift of Mrs. Howard Mansfield, 1949",,,,,,,,,,,,Woodblocks,,http://www.metmuseum.org/art/collection/search/53814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.222,false,true,58910,Asian Art,Inrō,,Japan,,,,,Artist,,Tōyō,"Japanese, active ca. 1764–71",,Tōyō,Japanese,1764,1771,18th–19th century,1700,1899,"Lacquer, silver brown ground, incised; Interior: gyobu nashiji and fundame",4 3/16 x 1 15/16 x 1 5/16 in. (10.7 x 4.9 x 3.4 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.1.701,false,true,58478,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,In the style of,Ogata Kōrin,"Japanese, 1658–1716",,Kōrin,Japanese,1658,1716,18th–19th century,1700,1899,"Gold lacquer with dark gray ishime, gold, red, black, and silver makie, pewter, and mother-of-pearl; Ojime: bead with autumn wild flowers; Netsuke: rat eating peach; boxwood",H. 2 3/8 in. (6 cm); W. 2 1/2 in. (6.4 cm); D. 3/4 in. (1.9 cm),"Edward C. Moore Collection, Bequest of Edward C. Moore, 1891",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.187,false,true,58874,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist,,Mochizuki Hanzan,"Japanese, 1743–?1790",,Mochizuki Hanzan,Japanese,1743,1790,18th–19th century,1700,1899,"Wood, brushed wood ground, gold, red and green hiramakie, takamakie, raden; Interior: nashiji and fundame",3 x 2 5/8 x 7/8 in. (7.6 x 6.7 x 2.3 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2204,false,true,53970,Asian Art,Print,,Japan,,,,,Artist,,Haikairyō Henpuku,"Japanese, 1744–1830",,Haikairyō Henpuku,Japanese,1744,1830,18th–19th century,1700,1899,Polychrome woodblock print (surimono); ink and color on paper,3 7/8 x 5 7/16 in. (9.8 x 13.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2175,false,true,55089,Asian Art,Woodblock print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,18th–19th century,1700,1899,Polychrome woodblock print (surimono); ink and color on paper,8 x 7 3/16 in. (20.3 x 18.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2221,false,true,53987,Asian Art,Print,,Japan,,,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,18th–19th century,1700,1900,Polychrome woodblock print (surimono); ink and color on paper,5 1/4 x 7 7/8 in. (13.3 x 20 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2205,false,true,53971,Asian Art,Print,,Japan,,,,,Artist,,Yanagawa Shigenobu,"Japanese, 1787–1832",,Yanagawa Shigenobu,Japanese,1787,1832,18th–19th century,1700,1899,Polychrome woodblock print (surimono); ink and color on paper,8 9/16 x 7 5/16 in. (21.7 x 18.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/53971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2176,false,true,55090,Asian Art,Print,,Japan,,,,,Artist,,Yanagawa Shigemasa,"Japanese, 18th–19th century",,Yanagawa Shigemasa,Japanese,1700,1899,18th–19th century,1700,1899,Polychrome woodblock print (surimono); ink and color on paper,8 1/16 x 3 5/8 in. (20.5 x 9.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2187,false,true,55103,Asian Art,Woodblock print,,Japan,,,,,Artist,,Reisai,"Japanese, 18th–19th century",,Reisai,Japanese,1700,1899,18th–19th century,1700,1899,Polychrome woodblock print (surimono); ink and color on paper,7 x 6 1/2 in. (17.8 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/55103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP2827,false,true,39584,Asian Art,Print,,Japan,Edo (1615–1868),,,,Artist,,Utagawa Toyokuni I,"Japanese, 1769–1825",,Utagawa Toyokuni I,Japanese,1769,1825,18th–19th century,1700,1899,Polychrome woodblock print; ink and color on paper,14-1/8 x 9-9/16 in. (35.9 x 24.3 cm),"Henry L. Phillips Collection, Bequest of Henry L. Phillips, 1939",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/39584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.70.2,false,true,57247,Asian Art,Hanging scroll,,Japan,,,,,Artist,Attributed to,Chōbunsai Eishi,"Japanese, 1756–1829",,Chōbunsai Eishi,Japanese,1756,1829,18th–19th century,1756,1829,Hanging scroll; ink and color on paper,36 1/2 x 15 3/4 in. (92.7 x 40 cm),"Gift of Edward M. Bratter, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.32,false,true,45735,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Kano Seisen’in,1775–1828,,Kano Seisen’in,Japanese,1775,1828,18th–19th century,1775,1828,Hanging scroll; ink and color on silk,38 x 14 1/4 in. (96.5 x 36.2 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.17,false,true,49058,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Mori Sosen,"Japanese, 1747–1821",,Mori Sosen,Japanese,1747,1821,18th–19th century,1747,1821,Hanging scroll; ink and color on silk,26 3/4 x 6 1/4 in. (67.9 x 15.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.18,false,true,49059,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Mori Sosen,"Japanese, 1747–1821",,Mori Sosen,Japanese,1747,1821,18th–19th century,1747,1821,Hanging scroll; ink and color on silk,26 3/4 x 6 1/4 in. (67.9 x 15.9 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/49059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.128,false,true,45798,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kubo Shunman,"Japanese, 1757–1820",,Kubo Shunman,Japanese,1757,1820,18th–19th century,1757,1820,Hanging scroll; ink and color on paper,36 3/8 x 13 1/16 in. (92.4 x 33.2 cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.268.100,false,true,44895,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Kushiro Unsen,"Japanese, 1759–1811",,Kushiro Unsen,Japanese,1759,1811,18th–19th century,1759,1811,Hanging scroll; ink and color on satin,Image: 58 7/16 x 13 1/4 in. (148.4 x 33.7 cm) Overall: 82 1/2 x 20 3/4in. (209.6 x 52.7cm),"The Harry G. C. Packard Collection of Asian Art, Gift of Harry G. C. Packard, and Purchase, Fletcher, Rogers, Harris Brisbane Dick, and Louis V. Bell Funds, Joseph Pulitzer Bequest, and The Annenberg Fund Inc. Gift, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/44895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.76.33,false,true,45801,Asian Art,Hanging scroll,,Japan,Edo period (1615–1868),,,,Artist,,Yanagi Buncho II,active ca. 1764–1801,,Yanagi Buncho II,Japanese,1764,1801,18th–19th century,1764,1801,Hanging scroll; ink and color on silk,17 5/8 x 20 1/2 in. (44.8 x 52.1 cm),"Charles Stewart Smith Collection, Gift of Mrs. Charles Stewart Smith, Charles Stewart Smith Jr., and Howard Caswell Smith, in memory of Charles Stewart Smith, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/45801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.1,false,true,48890,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Unmounted painting; ink on paper,10 3/4 x 6 7/8 in. (27.3 x 17.5 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/48890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.2,false,true,57252,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,9 7/16 x 12 3/8 in. (24 x 31.4 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.3,false,true,57253,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,6 7/8 x 9 3/8 in. (17.5 x 23.8 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.4,false,true,57254,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,9 1/2 x 12 7/8 in. (24.1 x 32.7 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.5,false,true,57255,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink and color on paper,9 3/8 x 13 15/16 in. (23.8 x 35.4 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.6,false,true,57256,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,10 7/8 x 7 9/16 in. (27.6 x 19.2 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.7,false,true,57257,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,13 1/16 x 9 3/4 in. (33.2 x 24.8 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.8,false,true,57258,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink and color on paper,9 5/8 x 12 3/8 in. (24.4 x 31.4 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.9,false,true,57259,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink and color on paper,13 x 9 1/4 in. (33 x 23.5 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.10,false,true,57260,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,18 1/2 x 12 3/16 in. (47 x 31 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.11,false,true,57261,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,9 7/8 x 7 7/16 in. (25.1 x 18.9 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.12,false,true,57262,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,11 3/8 x 8 3/8 in. (28.9 x 21.3 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.13,false,true,57263,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,8 1/4 x 10 7/8 in. (21 x 27.6 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.14,false,true,57264,Asian Art,Drawings,,Japan,Edo period (1615–1868),,,,Artist,Attributed to,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink and color on paper,Turtle: 5 x 9 5/16 in. (12.7 x 23.7 cm) Other sketch. 5 3/8 x 7 7/8 in. (13.7 x 20 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.15,false,true,57265,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,9 5/8 x 13 1/8 in. (24.4 x 33.3 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.16,false,true,57266,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink and red color on paper,16 13/16 x 10 1/2 in. (42.7 x 26.7 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.17,false,true,57267,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,15 3/8 x 10 13/16 in. (39.1 x 27.5 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.18,false,true,57268,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink and color on brown prepared paper,11 x 16 in. (27.9 x 40.6 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.19,false,true,57269,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,10 11/16 x 7 1/8 in. (27.1 x 18.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.20,false,true,57270,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,12 15/16 x 9 1/2 in. (32.9 x 24.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.21,false,true,57271,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1760,1849,Ink on paper,12 7/8 x 9 1/2 in. (32.7 x 24.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.22,false,true,57272,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,9 3/4 x 13 5/8 in. (24.8 x 34.6 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.23,false,true,57273,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,11 x 16 1/2 in. (27.9 x 41.9 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.24,false,true,57274,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,11 3/8 x 5 7/8 in. (28.9 x 14.9 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.25,false,true,57275,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,9 7/16 x 13 1/2 in. (24 x 34.3 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.26,false,true,57276,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,12 5/16 x 9 3/16 in. (31.3 x 23.3 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.27,false,true,57277,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,11 3/16 x 8 3/4 in. (28.4 x 22.2 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.28,false,true,57278,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,11 x 16 1/2 in. (27.9 x 41.9 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.29,false,true,57279,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,16 1/4 x 10 1/4 in. (41.3 x 26 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.30,false,true,57280,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,16 1/4 x 10 3/8 in. (41.3 x 26.4 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.31,false,true,57281,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,9 9/16 x 5 1/2 in. (24.3 x 14 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.32,false,true,57282,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,16 1/8 x 9 5/16 in. (41 x 23.7 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.33,false,true,57283,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,10 11/16 x 7 in. (27.1 x 17.8 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.34,false,true,57284,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,9 3/4 x 7 3/8 in. (24.8 x 18.7 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.35,false,true,57285,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,12 7/8 x 9 7/8 in. (32.7 x 25.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.36,false,true,57286,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,7 9/16 x 10 1/4 in. (19.2 x 26 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.37,false,true,57287,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,7 9/16 x 5 7/8 in. (19.2 x 14.9 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.38,false,true,57288,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,10 3/4 x 7 1/8 in. (27.3 x 18.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.39,false,true,57289,Asian Art,Drawing,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink on paper,12 1/8 x 5 5/8 in. (30.8 x 14.3 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.40,false,true,57290,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,10 5/8 x 15 3/8 in. (27 x 39.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.41,false,true,57291,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Two paintings mounted together; ink on paper,Left painting: 7 11/16 x 5 1/2 in. (19.5 x 14 cm) Right painting: 7 3/4 x 5 1/2 in. (19.7 x 14 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.42,false,true,57292,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,Mount: 8 5/16 x 9 7/8 in. (21.1 x 25.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.121.43,false,true,57293,Asian Art,Painting,,Japan,Edo period (1615–1868),,,,Artist,School of,Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",,Katsushika Hokusai,Japanese,1760,1849,18th–19th century,1700,1899,Ink and color on paper,8 1/4 x 12 5/8 in. (21 x 32.1 cm),"Gift of Annette Young, in memory of her brother, Innis Young, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/57293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.842,false,true,78732,Asian Art,Illustrated book,『大雅堂画譜』|Taigadō (Taiga Hall at Sōrinji Temple) Picture Album (Taigadō gafu),Japan,Edo period (1615–1868),,,,Artist|Artist,,Ike Taiga|Sō Geppō,"Japanese, 1723–1776|Japanese, 1760–1839",,Ike Taiga|Sō Geppō,Japanese,1723 |1760,1776 |1839,1804,1804,1804,"Woodblock printed book (orihon, accordion-style); ink and color on paper",11 1/4 × 7 1/2 in. (28.5 × 19 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JIB10a–c,false,true,57544,Asian Art,Illustrated book,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Rokuzotei|Katsushika Hokusai,"Japanese, Tokyo (Edo) 1760–1849 Tokyo (Edo)",(assisting draftsman),Rokuzotei|Katsushika Hokusai,Japanese,1760,1849,1802,1802,1802,Three volumes; ink and color on paper,Each: 10 1/4 × 6 3/4 × 1/4 in. (26 × 17.1 × 0.6 cm),"Rogers Fund, 1918",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/57544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.841,false,true,78731,Asian Art,Illustrated books,『山水畫譜』|Picture Album of Landscapes by Yi Fujiu and Ike no Taiga (I Fukyū Ike no Taiga sansui gafu),Japan,Edo period (1615–1868),,,,Artist|Artist,,Ike Taiga|Yi Fujiu,"Japanese, 1723–1776|Japanese, active 1726–50",,Ike Taiga|Yi Fujiu,Japanese,1723 |1726,1776 |1750,1803,1803,1803,Set of two woodblock-printed books bound as one volume; ink on paper,Other (each): 10 3/8 × 7 5/16 in. (26.3 × 18.5 cm),"Purchase, Mary and James G. Wallach Foundation Gift, 2013",,,,,,,,,,,,Illustrated Books,,http://www.metmuseum.org/art/collection/search/78731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.76,false,true,45438,Asian Art,Inrō,,Japan,Meiji period (1868–1912),,,,Artist|Artist,After a painting by,Tokoku Fuzui|Hanabusa Itchō,"Japanese, 1652–1724",,Tokoku Fuzui|Hanabusa Itchō,Japanese,1652,1724,late 19th century,1867,1899,"Gold lacquer with ivory and wood inlay; Netsuke: ivory and lacquered wood figure, Ojime: gold bead with face of Daikoku, god of good fortune",3 1/4 x 2 3/8 x 13/16 in. (8.3 x 6.1 x 2 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +JP832,false,true,54486,Asian Art,Woodblock print,,Japan,Edo period (1615–1868),,,,Artist|Artist,Made by,Estate of Samuel Isham|Torii Kiyonobu,"Japanese, 1664–1729",,Estate of Samuel Isham|Torii Kiyonobu,Japanese,1664,1729,ca. 1749,1739,1759,Polychrome woodblock print; ink and color on paper,H. 12 3/8 in. (31.4 cm); W. 5 7/8 in. (14.9 cm),"Gift of Estate of Samuel Isham, 1914",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/54486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.7,false,true,42346,Asian Art,Hanging scroll,賢江祥啓筆山水図|Landscape with Pavilion,Japan,Muromachi period (1392–1573),,,,Artist|Artist,Inscribed by,Kenkō Shōkei|Tōgen Zuisen,"active ca. 1470–after 1523|Japanese, 1430–1480",,Kenkō Shōkei|Tōgen Zuisen,Japanese,1470 |1430,1550 |1480,1478–80,1478,1480,Hanging scroll; ink and color on paper,Image: 19 13/16 x 13 13/16 in. (50.3 x 35.1 cm) Entire scroll: 58 5/8 x 19 in. (148.9 x 48.3 cm) Width including rollers: 21 in. (53.3 cm),"Purchase, Bequest of Stephen Whitney Phoenix, by exchange, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/42346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.67.25,false,true,45435,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist|Artist,After a painting by,Tachibana Gyokuzan|Hanabusa Itchō,"Japanese, 1652–1724",,Tachibana Gyokuzan|Hanabusa Itchō,Japanese,1652,1724,19th century,1800,1899,"Fundami sprinkled lacquer, gold and colored hiramakie sprinkled and polished lacquer, takamakie sprinkled and polished lacquer relief, and foil decoration; Netsuke: lacquered wood figure of Juro-jin; Ojime: zogan metal",3 5/16 x 2 1/16 x 3/4 in. (8.4 x 5.3 x 1.9 cm),"Rogers Fund, 1913",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.853,false,true,45441,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist|Artist,In the Style of,Nikkōsai|Hanabusa Itchō,"Japanese, 1652–1724",,Nikkōsai|Hanabusa Itchō,Japanese,1652,1724,19th century,1800,1899,Gold lacquer with gold and colored hiramkie sprinkled and polished lacquer and ivory inlay; Netsuke: polished wood button; Ojime: red lacquer bead; Interior: nashiji and fundame,3 3/8 x 2 5/16 x 13/16 in. (8.6 x 5.9 x 2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/45441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.100.212,false,true,58894,Asian Art,Inrō,手長猿蒔絵印籠|Inrō with Gibbons in a Landscape,Japan,Edo period (1615–1868),,,,Artist|Artist,Maki-e by|Design by,Hasegawa Kyorinsai|Kano Sukekiyo,"Japanese, active early 19th century|1787–1840",,Hasegawa Kyorinsai|Kano Sukekiyo,Japanese,1800 |1787,1833 |1840,early 19th century,1800,1833,Four cases; lacquered wood with togidashimaki-e imitating ink painting (togikirimaki-e) on gold lacquer ground Netsuke: ivory; monkey on horseback Ojime: bronze; monkey,3 5/16 x 2 3/16 x 3/4 in. (8.4 x 5.5 x 1.9 cm),"The Howard Mansfield Collection, Purchase, Rogers Fund, 1936",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.838,false,true,58765,Asian Art,Inrō,,Japan,Edo period (1615–1868),,,,Artist|Artist,,Hogen Eisen|Kano Hidenobu,"Japanese, 1588–1672",,Hogen Eisen|Kano Hidenobu,Japanese,1588,1672,18th–19th century,1700,1899,"Lacquer, red ground, gold and black togidashi; Interior: nashiji and fundame",2 13/16 x 2 13/16 x 13/16 in. (7.1 x 7.1 x 2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Inrō,,http://www.metmuseum.org/art/collection/search/58765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.210.3,false,true,456212,Islamic Art,Illustrated album leaf,Page of Calligraphy from a Mantiq al-tair (Language of the Birds),,,,,,Author,,Farid al-Din `Attar,ca. 1142–1220,,Farid al-Din `Attar,,1142,1220,dated A.H. 892/ A.D. 1486,1461,1511,Opaque watercolor and gold on paper,,"Fletcher Fund, 1963",,,,,,,,,,,,Codices,,http://www.metmuseum.org/art/collection/search/456212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.228.23.2,false,true,446600,Islamic Art,Folio from an illustrated manuscript,"""A Religious Devotee Summoned to Pray for the King's Recovery"", Folio from a Bustan (Orchard) of Sa'di",,,,,,Author,,Sa'di,1213/19–92,,Sa'di,,1213,1292,17th century,1600,1699,"Ink, opaque watercolor, and gold on paper",,"Gift of Alexander Smith Cochran, 1913",,,,,,,,,,,,Codices,,http://www.metmuseum.org/art/collection/search/446600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.228.23.3,false,true,454668,Islamic Art,Folio from an illustrated manuscript,"""A Fire-Worshipper Received at the Board of Abraham the Patriarch"", Folio from a Bustan (Orchard) of Sa'di",,,,,,Author,,Sa'di,1213/19–92,,Sa'di,,1213,1292,17th century,1600,1699,"Ink, opaque watercolor, and gold on paper",,"Gift of Alexander Smith Cochran, 1913",,,,,,,,,,,,Codices,,http://www.metmuseum.org/art/collection/search/454668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.228.23.4,false,true,446601,Islamic Art,Folio from an illustrated manuscript,"""King Salih of Syria Entertaining Two Dervishes"", Folio from a Bustan (Orchard) of Sa'di",,,,,,Author,,Sa'di,1213/19–92,,Sa'di,,1213,1292,17th century,1600,1699,"Ink, opaque watercolor, and gold on paper",,"Gift of Alexander Smith Cochran, 1913",,,,,,,,,,,,Codices,,http://www.metmuseum.org/art/collection/search/446601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (1-44),false,true,286316,Photographs,Album,Voyage en Orient et en Espagne Vols 3 & 4 [bound together],,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver prints from paper negatives,Album: 46.4 x 62.2 cm (18 1/4 x 24 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.18 (1-22),false,true,290386,Photographs,Photographically illustrated book,Photographic Facsimiles of the Remains of the Epistles of Clement of Rome. Made from the Unique Copy Preserved in the Codex Alexandrinus.,,,,,,Artist|Author,,Roger Fenton|Frederic Madden,"British, 1819–1869|British, 1801–1873",,"Fenton, Roger|Madden, Frederic",British|British,1819 |1801,1819 |1873,1856,1856,1856,Salted paper prints from glass negatives,Images: 34.3 x 29.8 cm (13 1/2 x 11 3/4 in.),"Purchase, Alfred Stieglitz Society Gifts, 2009",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/290386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37 (18),false,true,289207,Photographs,Letter; Correspondence,[Manuscript Letter from William-Fox Strangways to Antonio Bertoloni],,,,,,Correspondent|Author,,Antonio Bertoloni|William Thomas Horner Fox-Strangways,"Italian, 1775–1869|British, 1795–1865",,"Bertoloni, Antonio|Fox-Strangways, William Thomas Horner",Italian|British,1775 |1795-05-07,1869 |1865-01-10,1839,1839,1839,Ink on paper (manuscript),Sheet: 23.2 x 36.8 cm (9 1/8 x 14 1/2 in.),"Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Manuscript Materials,,http://www.metmuseum.org/art/collection/search/289207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (1-48),false,true,285940,Photographs,Album,[Chinese and Japanese Portraits],,,,,,Artist|Artist|Artist,,Raimund von Stillfried|Unknown|Suzuki Shin'ichi,"Austrian, 1839–1911|Japanese, 1835–1919",", et al","Stillfried, Raimond von|Unknown|Suzuki, Shin'ichi",Austrian|Japanese,1839 |1835,1911 |1919,1870s,1870,1879,Albumen silver prints from glass negatives,28 x 35.3 x 4.8 cm (11 x 13 7/8 x 1 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.502.1 (1-50),false,true,283195,Photographs,Album,"Gardner's Photographic Sketchbook of the War, Volume 1",,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Printer|Author,,Timothy H. O'Sullivan|Alexander Gardner|George N. Barnard|James Gardner|John Reekie|David Knox|William R. Pywell|David B. Woodbury|Alexander Gardner|Alexander Gardner,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, 1819–1902|American, born 1832|American, active 1860s|American|American, died 1866|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"O'Sullivan, Timothy H.|Gardner, Alexander|Barnard, George N.|Gardner, James|Reekie, John|Knox, David|Pywell, William R.|Woodbury, David B.|Gardner, Alexander|Gardner, Alexander","American, born Ireland|American, Scottish|American|American|American|American|American|American, Scottish|American, Scottish",1840 |1821 |1819 |1832 |1860 |1821 |1821,1882 |1882 |1902 |1869 |1866 |1882 |1882,1863,1863,1863,Albumen silver prints from glass negatives,17.8 x 22.7 cm (7 x 8 15/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/283195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.502.2 (1-50),false,true,286128,Photographs,Album,"Gardner's Photographic Sketchbook of the War, Volume 2",,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Printer|Author,,Timothy H. O'Sullivan|Alexander Gardner|James Gardner|George N. Barnard|John Reekie|David Knox|William R. Pywell|David B. Woodbury|Alexander Gardner|Alexander Gardner,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born 1832|American, 1819–1902|American, active 1860s|American|American, died 1866|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"O'Sullivan, Timothy H.|Gardner, Alexander|Gardner, James|Barnard, George N.|Reekie, John|Knox, David|Pywell, William R.|Woodbury, David B.|Gardner, Alexander|Gardner, Alexander","American, born Ireland|American, Scottish|American|American|American|American|American|American, Scottish|American, Scottish",1840 |1821 |1832 |1819 |1860 |1821 |1821,1882 |1882 |1902 |1869 |1866 |1882 |1882,1863,1863,1863,Albumen silver prints from glass negatives,Images approx: 17.4 × 22.5 cm (6 7/8 × 8 7/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.282,false,true,265335,Photographs,Photographically illustrated book,Reichsparteitag der Arbeit,,,,,,Artist|Author|Author,,Heinrich Hoffmann|Pitter Gern|Doctor Otto Dietrich,"German, 1885–1957|German|German, 1897–1952",,"Hoffmann, Heinrich|Gern, Pitter|Dietrich, Doctor Otto",German|German|German,1885 |1897,1957 |1952,1930s,1930,1939,Gelatin silver prints,,"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/265335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.669 (1-40),false,true,290484,Photographs,Photographically illustrated book,Life and Landscape on the Norfolk Broads,,,,,,Artist|Author|Printer,,Peter Henry Emerson|Thomas Frederick Goodall|Valentine of Dundee,"British, born Cuba, 1856–1936|British, 1857–1944",,"Emerson, Peter Henry|Goodall, Thomas Frederick|Valentine of Dundee","British, born Cuba|British",1856 |1857,1936 |1944,1885–86,1885,1886,Platinum prints from glass negatives,Images: 12 x 18 cm (4 3/4 x 7 1/16 in.) to 23 x 30 cm (9 1/16 x 11 13/16 in.) Binding: 30.5 x 41.9 x 5.1 cm (12 x 16 1/2 x 2 in.),"Gift of Joyce F. Menschel, 2008",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/290484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37 (2),false,true,289191,Photographs,Letter; Correspondence,[Manuscript Letter from W. H. Fox Talbot to Antonio Bertoloni],,,,,,Correspondent|Author,,Antonio Bertoloni|William Henry Fox Talbot,"Italian, 1775–1869|British, Dorset 1800–1877 Lacock",,"Bertoloni, Antonio|Talbot, William Henry Fox",Italian|British,1775 |1800,1869 |1800,1839,1839,1839,Ink on paper (manuscript),Sheet: 30 x 37.4 cm (11 13/16 x 14 3/4 in.),"Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Manuscript Materials,,http://www.metmuseum.org/art/collection/search/289191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37 (12),false,true,289201,Photographs,Letter; Correspondence,[Manuscript Letter from W. H. Fox Talbot to Antonio Bertoloni],,,,,,Correspondent|Author,,Antonio Bertoloni|William Henry Fox Talbot,"Italian, 1775–1869|British, Dorset 1800–1877 Lacock",,"Bertoloni, Antonio|Talbot, William Henry Fox",Italian|British,1775 |1800,1869 |1800,1839,1839,1839,Ink on paper (manuscript),Sheet: 22.7 x 37.2 cm (8 15/16 x 14 5/8 in.),"Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Manuscript Materials,,http://www.metmuseum.org/art/collection/search/289201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37 (30),false,true,289218,Photographs,Letter; Correspondence,[Manuscript Letter from W. H. Fox Talbot to Antonio Bertoloni],,,,,,Correspondent|Author,,Antonio Bertoloni|William Henry Fox Talbot,"Italian, 1775–1869|British, Dorset 1800–1877 Lacock",,"Bertoloni, Antonio|Talbot, William Henry Fox",Italian|British,1775 |1800,1869 |1800,1840,1840,1840,Ink on paper (manuscript),Sheet: 22.6 x 37.6 cm (8 7/8 x 14 13/16 in.),"Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Manuscript Materials,,http://www.metmuseum.org/art/collection/search/289218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (1-9),false,true,286154,Photographs,Album,[Duc de Morny Album],,,,,,Artist|Artist|Patron,Painted and retouched by|Commissioned by,"Pierre-Louis Pierson|Marck|Charles-Auguste-Louis-Joseph de Morny, duc de Morny","French, 1822–1913|French (born Switzerland) 1811–1865 Paris",", et al","Pierson, Pierre-Louis|Marck|Morny, Charles-Auguste-Louis-Joseph de, duc de Morny","French|French, born Switzerland",1822 |1811,1913 |1865,before 1865,1855,1865,Albumen silver prints from glass negatives,38 x 31.5 x 8.5 cm (14 15/16 x 12 3/8 x 3 3/8 in.) Album 35 CDVs approximately 8.6 x 5.1 cm 6 photographs various sizes from 10.1 x 7.1 cm to 16.9 x 11.9 cm,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.1–.10,false,true,285625,Photographs,Album,"Photographic Views in Madura, Part I",,,,,,Artist|Author,,Linnaeus Tripe|Martin Norman,"British, Devonport (Plymouth Dock) 1822–1902 Devonport|British",,"Tripe, Linnaeus|Norman, Martin",British|British,1822,1902,1858,1858,1858,Albumen silver prints from paper negatives,Various: approx. 34.6 x 28.3,"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.1–.15,false,true,285624,Photographs,Album,"Photographic Views in Madura, Part III",,,,,,Artist|Author,,Linnaeus Tripe|Martin Norman,"British, Devonport (Plymouth Dock) 1822–1902 Devonport|British",,"Tripe, Linnaeus|Norman, Martin",British|British,1822,1902,1858,1858,1858,Albumen silver prints from paper negatives,Various: approx. 34.6 x 28.3,"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.38,false,true,265767,Photographs,Album,[Collection of British Calotypes and Wood-engravings],,,,,,Artist|Artist|Artist|Artist,,"William John Newton|Sir Thomas Maryon Wilson, 8th Baronet|Arthur James Melhuish|Unknown","British, 1785–1869|British, 1800–1869|British, 1829–1895|British",,"Newton, William John|Wilson, Thomas Maryon, Sir, 8th Baronet|Melhuish, Arthur James|Unknown",British|British|British,1785 |1800 |1829,1869 |1869 |1895,1850s,1850,1859,Salted paper prints and engravings,20.0 x 13.8 cm (7 7/8 x 5 7/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/265767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.2.1–.151,false,true,286628,Photographs,Album,Demi-Monde I 56,,,,,,Artist|Person in Photograph|Person in Photograph,Person in photograph|Person in photograph,André-Adolphe-Eugène Disdéri|Cora Pearl|Emily Fowler,"French, Paris 1819–1889 Paris|British, 1835?–1886|British, 1849–1896",,"Disdéri, André-Adolphe-Eugène|Pearl, Cora|Fowler, Emily",French|British|British,1819 |1835 |1849,1889 |1886 |1896,1858–68,1858,1868,Albumen silver print from glass negative,Image (.2.54-64143): 18.4 x 24.8 cm (7 1/4 x 9 3/4 in.) Album page: 26.2 x 34.8 cm (10 5/16 x 13 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.627.1,false,true,269659,Photographs,Cartes-de-visite,[Carte-de-Visite Album of Prominent Personages],,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist,,Mayer & Pierson|Neurdein Frères|Leon and Lévy|Boulton|Franck|Pierre-Louis Pierson|H. B. Randall|Sergei Luvovich Levitsky|William Downey|Horatio Nelson King|André-Adolphe-Eugène Disdéri|George Washington Wilson|Vernon Heath|Daniel Downey|Sergei Luvovich Levitsky|Robert Jefferson Bingham,"French|French, active Paris, 1870s–1900s|French|French, 1816–1906|French, 1822–1913|Russian, 1819–1898|British, born 1828|British, 1830–1905|French, Paris 1819–1889 Paris|British, Grampian (Baffshire), Scotland 1823–1893 Abedeen, Scotland|British, 1819–1895|British|Russian, 1819–1898|British, active France, 1825–70",,"Mayer & Pierson|Neurdein Frères|Leon and Lévy|Boulton|Franck|Pierson, Pierre-Louis|Randall, H. B.|Levitsky, Sergei Luvovich|Downey, William and Daniel|King, Horatio Nelson|Disdéri, André-Adolphe-Eugène|Wilson, George Washington|Heath, Vernon|Downey, Daniel |Levitsky, Sergei Luvovich|Bingham, Robert Jefferson","French|French|French|French|French|Russian|British|British|French|British, Scottish|British|British|Russian|British, active France",1863 |1816 |1822 |1819 |1828 |1830 |1819 |1823 |1819 |1819 |1825,1910 |1906 |1913 |1898 |1828 |1905 |1889 |1893 |1895 |1898 |1870,1860s–70s,1860,1879,Albumen silver prints,,"Gift of Susanna Myers, 1953",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/269659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1073.97 (1–17),false,true,266381,Photographs,Photographically illustrated book,Pictorial Photographs. A Record of the Photographic Salon of 1897. In Seventeen Plates Reproduced in Photogravure,,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Author,,W. Thomas|Paul Martin|Heinrich Kühn|Robert Demachy|Alfred Horsley Hinton|Alfred Stieglitz|William Crooke|Ernest R. Ashton|Ralph Winwood Robinson|Paul Bergon|J. B. B. Wellington|Reginald W. Craigie|Maitland|Rev. F. C. Lambert|Frederick H. Evans|Frank M. Sutcliffe|Lionel C. Bennett|Walter L. Colls,"British|Austrian (born Germany), Dresden 1866–1944 Birgitz|French, 1859–1936|British, 1863–1908|American, Hoboken, New Jersey 1864–1946 New York|British, born Scotland|British, 1867–1952|British, 1862–1942|French, 1863–1912|British, 1858–1939|British|British, London 1853–1943 London|British, 1853–1941|British|British",,"Thomas, W.|Martin, Paul|Kühn, Heinrich|Demachy, Robert|Hinton, Afred Horsley|Stieglitz, Alfred|Crooke, William|Ashton, Ernest R.|Robinson, Ralph W.|Bergon, Paul|Wellington, J. B. B.|Craigie, Reginald W.|Maitland, Viscount|Lambert, Rev. F. C.|Evans, Frederick Henry|Sutcliffe, Frank Meadow|Bennett, Lionel C.|Colls, Walter L.","British|Austrian, born Germany|French|British|American|British, Scottish|British|British|French|British|British|British|British|British|British",1866 |1859 |1863 |1864 |1867 |1862 |1863 |1858 |1853 |1853,1944 |1936 |1908 |1946 |1952 |1942 |1912 |1939 |1943 |1941,1897,1897,1897,Photogravures,Each sheet: 14 3/4 × 10 1/2 in. (37.5 × 26.7 cm) Plate dimesions vary,"Gift of Alfred Stieglitz, 1922, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.1–.76,false,true,262199,Photographs,Album,The Wilkinson Album,,,,,,Artist|Artist|Artist|Artist|Artist,Attributed to,Unknown|Henri de Couliboeuf de Blocqueville|Frances Carlhian|Antonio Giannuzzi|Luigi Pesce,"French, active 1850s–60s|French, 1818–1870|Italian, 1818–1876|Italian, 1818–1891",,"Unknown|Couliboeuf de Blocqueville, Henri de|Carlhian, Frances|Giannuzzi, Antonio|Pesce, Luigi",French|French|Italian|Italian,1818 |1818 |1818,1870 |1876 |1891,1840s–60s,1840,1869,Albumen silver and salted paper prints,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/262199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.96,false,true,283200,Photographs,Broadside; Photographs,"[Broadside for the Capture of John Wilkes Booth, John Surratt, and David Herold]",,,,,,Maker|Artist|Photography Studio|Photography Studio,,"Unknown|Alexander Gardner|Silsbee, Case & Company|Unknown","American|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, active Boston",,"Unknown|Gardner, Alexander|Silsbee, Case & Company|Unknown","American, Scottish|American",1821,1882,"April 20, 1865",1865,1865,Ink on paper with three albumen silver prints from glass negatives,Sheet: 60.5 x 31.3 cm (23 13/16 x 12 5/16 in.) Each photograph: 8.6 x 5.4 cm (3 3/8 x 2 1/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Collages,,http://www.metmuseum.org/art/collection/search/283200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.585 (1-35),false,true,283216,Photographs,Album,"Wheeler Survey, Season of 1872",,,,,,Artist|Patron,Commissioned by,William Bell|Lieutenant George Montague Wheeler,"American (born England) Liverpool 1831–1910 Philadelphia, Pennsylvania|American, 1842–1905",,"Bell, William|Wheeler, Lieutenant George Montague","American, born Britain|American",1831 |1842,1910 |1905,1872,1872,1872,Albumen silver print from glass negative,"28.2 x 20.2 cm (11 1/8 x 7 15/16 in.), each (approx.)","Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/283216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.495,false,true,286333,Photographs,Panorama,Panorama of Niagara Falls,,,,,,Artist|Former Attribution,,Frederick Langenheim|William Langenheim,"American, born Germany, Schöningen 1809–1879|American, born Germany, Schöningen 1807–1874",,"Langenheim, Frederick|Langenheim, William","American, born Germany",1809 |1807,1879 |1874,July 1845,1845,1845,Daguerreotype,"Each plate, visible: 31/2 x 2 3/4; Unframed: 12 x 18; Framed: 13 x 193/8","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Parts-Stone,,http://www.metmuseum.org/art/collection/search/286333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.586.2,false,true,261590,Photographs,Photographically illustrated book,"The Far East: A Monthly Journal, Illustrated with Photographs",,,,,,Artist|Artist|Artist|Artist|Editor|Artist|Artist,,L.P. Fisler|Christopher T. Gardner|Thomas Child|Unknown|John Reddie Black|William Thomas Saunders|Kameya Tokujirō,"British, 1865–1886|British|British|Chinese|British, 1827–1880|British, 1832–1892|Japanese, 1825–1884",,"Fisler, L.P.|Gardner, Christopher T.|Child, Thomas|Unknown|Black, John Reddie|Saunders, William Thomas|Kameya, Tokujirō",British|British|British|British|British|Japanese,1865 |1827 |1832 |1825,1886 |1880 |1892 |1884,1870s,1870,1879,Albumen silver prints,,"Gift of John J. McKendry, 1975",,,,,,,,,,,,Periodicals,,http://www.metmuseum.org/art/collection/search/261590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640,false,true,269625,Photographs,Photographically illustrated book,A Photographic Tour Among the Abbeys of Yorkshire,,,,,,Artist|Artist|Author|Author,,Joseph Cundall|Philip Henry Delamotte|John Richard Walbran|William Jones,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889|British, 1817–1869|British, 1817–1885",,"Cundall, Joseph|Delamotte, Philip Henry|Walbran, John Richard|Jones, William",British|British|British|British,1818 |1821 |1817 |1817,1895 |1889 |1869 |1885,1850s,1850,1859,Albumen silver prints,"45.3 x 31.8 x 3.4 cm (17 13/16 x 12 1/2 x 1 5/16 in.), closed","David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/269625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1073.98,false,true,266382,Photographs,Photographically illustrated book,Architecture in Dharwar and Mysore,,,,,,Artist|Artist|Artist|Author|Author,,Thomas Biggs|Andrew Charles Brisbane Neill|Doctor William Henry Pigou|Colonel Philip Meadows Taylor|James Fergusson,"British|British, active India, 1814–1891|British, active India, 1818–1858|British, 1808–1876|British, 1808–1886",,"Biggs, Thomas|Neill, Andrew Charles Brisbane|Pigou, Doctor William Henry|Taylor, Colonel Philip Meadows|Fergusson, James","British|British, active India|British, active India|British|British",1814 |1818 |1808 |1808,1891 |1858 |1876 |1886,1860s,1860,1869,Albumen silver prints,,"Rogers Fund, 1920, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.554.11,false,true,270265,Photographs,Portfolio,Productions of the Leeds Photographic Society,,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Editor,,Ramsden and Birch|W. Birch|T. Dixon|Ramsden and Hope|George Fowler Jones|Pumphrey & Fowler|William A. Pumphrey|William Gardam|E. Holliday|Thomas Henry Briggs|William Fieldhouse|John William Ramsden|J. A. Hope|Leeds Photographic Society,"British|British|British|British|British|British|British, 1817–1905|British|British|British|British|British, 1834–1894|British",,"Ramsden and Birch|Birch, W.|Diseon, & Gardam|Ramsden and Hope|Jones, George Fowler|Pumphrey & Fowler|Pumphrey, William A.|Gardam, William|Holliday, E.|Briggs, Thomas Henry|Fieldhouse, William|Ramsden, John William|Hope, J. A.|Leeds Photographic Society",British|British|British|British|British|British|British|British|British|British|British|British|British|British,1817 |1834 |1852,1905 |1894,1852,1852,1852,Salted paper prints,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1960",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/270265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1068,false,true,264745,Photographs,Photographically illustrated book,Paris-Théàtre,,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Editor|Artist|Artist|Artist,,"Nadar|Paul, fils Bacard|Truchelut|Franck|Charles Reutlinger|E. Flamant|Frank|Eugenio Maunoury|Gaston et Mathieu|Alphonse J. Liébert|Louis Ghémar|Saglio|Emile Bondonneau|Joseph Lemercier|Étienne Carjat|Carrette|Buguet|J. M. Lopez|Ulric Grob|Alexandre Quinet|Dupont|J. Tourtin|Pierre Petit|Héribert Mayer|D. H. Mayer|Gougenheim et Forest|Alexander Courtin|Eugène Paz|Ferdinand Mulnier|Ferdinand Mulnier|Émile Tourtin","French, Paris 1820–1910 Paris|French|French, 1816–1906|German, Karlsruhe 1816–1881 Karlsruhe|Peruvian|French|French, 1827–1913|Belgian, 1819–1873|French|French, 1803–1887|French, Fareins 1828–1906 Paris|French|French|French, born 1836|French|French, Aups 1832–1909 Paris|French|German|French|French, active 1850s–70s|French, active 1850s–70s|French, active 1860s–70s",,"Nadar|Bacard, Paul|Truchelut|Franck|Reutlinger, Charles|Flamant, E.|Frank|Maunoury, Eugenio|Gaston et Mathieu|Liébert, Alphonse J.|Ghémar, Louis|Saglio|Bondonneau, Emile|Lemercier, Joseph|Carjat, Étienne|Carrette|Buguet|Lopez, J. M.|Grob, Ulric|Quinet, Alexandre|Dupont|Tourtin, J.|Petit, Pierre|Mayer, Héribert|Mayer, D. H.|Gougenheim et Forest|Courtin, Alexander|Paz, Eugène|Mulnier, Ferdinand|Mulnier, Ferdinand|Tourtin, Émile",French|French|French|German|Peruvian|French|French|Belgian|French|French|French|French|French|French|French|French|French|German|French|French|French|French,1820 |1816 |1816 |1827 |1819 |1803 |1828 |1836 |1832 |1850 |1850 |1860,1910 |1906 |1881 |1913 |1873 |1887 |1906 |1909 |1875 |1875 |1880,1870s,1870,1879,Woodburytypes,,"Purchase, Mary Martin Fund, 1986",,,,,,,,,,,,Periodicals,,http://www.metmuseum.org/art/collection/search/264745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.490.1–.10,false,true,291692,Photographs,Books,"Galerie Contemporaine, Littéraire, Artistique, v. 1 - 5",,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Publisher|Artist|Artist|Artist|Printer,,Unknown|Pierre Petit|Nadar|Lejeune|Valery|Ernest Eugène Appert|Delphin|Emile Courtin|Étienne Carjat|Charles-Albert Arnoux Bertall|Vallois|Franck|Antoine-Samuel Adam-Salomon|G. Fontaine|L. Baschet|Goupil et Cie|Émile Tourtin|Ferdinand Mulnier|Melandri|Goupil et Cie,"French|French, Aups 1832–1909 Paris|French, Paris 1820–1910 Paris|French|French, 1831–1891|French|French|French, Fareins 1828–1906 Paris|French, Paris 1820–1882 Paris|French|French, 1816–1906|French, La Ferté-sous-Jouarre 1811–1881 Paris|French|French|French, active 1850–84|French, active 1860s–70s|French, active 1850s–70s|French, active 1860s|French, active 1850–84",,"Unknown|Petit, Pierre|Nadar|Lejeune|Valery|Appert, Ernest Eugène|Delphin|Courtin, Emile|Carjat, Étienne|Bertall, Charles-Albert Arnoux|Vallois|Franck|Adam-Salomon, Antoine-Samuel|Fontaine, G.|Baschet, L.|Goupil et Cie|Tourtin, Émile|Mulnier, Ferdinand|Melandri|Goupil et Cie",French|French|French|French|French|French|French|French|French|French|French|French|French|French|French|French|French|French,1832 |1820 |1831 |1828 |1820 |1816 |1811 |1850 |1860 |1850 |1850,1909 |1910 |1891 |1906 |1882 |1906 |1881 |1884 |1880 |1875 |1884,1876–1881,1876,1881,Woodburytypes,,"Gift of Samuel P. Avery, transferred from the Library",,,,,,,,,,,,Periodicals,,http://www.metmuseum.org/art/collection/search/291692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (1-85),false,true,283076,Photographs,Album,[Emma Charlotte Dillwyn Llewelyn's Album],,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist,,John Dillwyn Llewelyn|Thereza Dillwyn Llewelyn|Jane Martha St. John|James Knight|Miss Bush|P. W. Fry|M.D.,"British, Swansea, Wales 1810–1882 Swansea, Wales|British, 1803–1882|British|British, active 19th century|British, active 19th century",", et al","Llewelyn, John Dillwyn|Llewelyn, Thereza Dillwyn|St., John Jane Martha|Knight, James|Bush Miss|Fry, P. W.|M.D.","British, Welsh|British|British|British|British",1810 |1803,1882 |1882,1853–56,1853,1856,128 salted paper prints and albumen silver prints from paper and glass negatives,28.8 × 22.3 cm (11 5/16 × 8 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/283076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.170,false,true,268615,Photographs,Photographically illustrated book,"The Philadelphia Photographer, Vol. I & II, Nos. 1-24",,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Editor,,"Wenderoth, Taylor & Brown|Henry P. Moore|John Coates Browne|John Carbutt|J. D. Sergeant|Leon S. Levy|Max Petsch|Edward Livingston Wilson","American, active Philadelphia, 1860s|American, 1833–1911|American, 1838–1918|American, 1832–1905|German, active 1860s|American, 1838–1903",,"Wenderoth, Taylor & Brown|Moore, Henry P.|Browne, John Coates|Carbutt, John|Sergeant, J. D.|Levy, Leon S. & Cornelius Cohen|Loescher & Petsch|Wilson, Edward Livingston",American|American|American|American|German|American,1833 |1838 |1832 |1860 |1838,1911 |1918 |1905 |1870 |1903,1864–65,1864,1865,Albumen silver prints,,"Harris Brisbane Dick Fund, 1938",,,,,,,,,,,,Periodicals,,http://www.metmuseum.org/art/collection/search/268615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.604.5,false,true,271509,Photographs,Cartes-de-visite,[Carte-de-Visite Album of British and European Royalty],,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist,,F. Joubert|Hermann Günther|Hills and Saunders|W. C. Lacy|Alfred Windsor|Cornelius Jabez Hughes|Camille Silvy|John Jabez Edwin Mayall|S. Mauer|Daniel Downey|Frères Ghemar,"British|German|British, active 1856–95|British|British, 1819–1884|French, 1835–1869|British, Oldham, Lancashire 1813–1901 West Sussex|British",,"Joubert, F.|Günther, Hermann|Hills and Saunders|Lacy, W. C.|Windsor, Alfred, Prince of England|Hughes, Jabez|Silvy, Camille|Mayall, John Jabez Edwin|Mauer, S.|Downey, Daniel |Ghemar, Frères",British|German|British|British|British|French|British|British|Belgian,1856 |1819 |1835 |1813,1895 |1884 |1869 |1901,1860s–70s,1860,1879,Albumen silver prints,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/271509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548,false,true,261267,Photographs,Album,La Colombe et Le Tigre. Ma collection de photographies de la Comtesse de Castiglione,,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Person in Photograph,,Duroni et Murer|Adolphe Braun|Alphonse (Jean-Baptiste) Bernoud|Mayer & Pierson|Giovanni Morotti|Pierre-Louis Pierson|André-Adolphe-Eugène Disdéri|Countess Virginia Oldoini Verasis di Castiglione,"Italian, 1807–1870|French, Besançon 1811–1877 Dornach|French, 1820–1889|French|Italian|French, 1822–1913|French, Paris 1819–1889 Paris|1835–1899",,"Duroni et Murer|Braun, Adolphe|Bernoud, Alphonse|Mayer & Pierson|Morotti, Giovanni|Pierson, Pierre-Louis|Disdéri, André-Adolphe-Eugène|Castiglione, di, Virginia Oldoini Verasis Countess",Italian|French|French|French|Italian|French|French,1807 |1811 |1820 |1822 |1819 |1835,1870 |1877 |1889 |1913 |1889 |1899,1860s,1860,1869,Albumen silver prints,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/261267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (1),false,true,675527,Photographs,Album; Artist's book,The Thames near Kelmscott Manor,,,,,,Correspondent|Artist|Artist|Artist|Artist|Author|Correspondent,Bookplate designed by|Bookplate designed by,Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton,"1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914",,"Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore",British|British|British|British|British,1910 |1853 |1872 |1870 |1850 |1832,1957 |1943 |1898 |1951 |1950 |1914,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (4),false,true,675530,Photographs,Album; Artist's book,"Entrance, On Right",,,,,,Correspondent|Artist|Artist|Artist|Artist|Author|Correspondent,Bookplate designed by|Bookplate designed by,Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton,"1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914",,"Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore",British|British|British|British|British,1910 |1853 |1872 |1870 |1850 |1832,1957 |1943 |1898 |1951 |1950 |1914,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (6),false,true,675532,Photographs,Album; Artist's book,Main Entrance,,,,,,Correspondent|Artist|Artist|Artist|Artist|Author|Correspondent,Bookplate designed by|Bookplate designed by,Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton,"1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914",,"Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore",British|British|British|British|British,1910 |1853 |1872 |1870 |1850 |1832,1957 |1943 |1898 |1951 |1950 |1914,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (21),false,true,675547,Photographs,Album; Artist's book,[Tapestry Room],,,,,,Correspondent|Artist|Artist|Artist|Artist|Author|Correspondent,Bookplate designed by|Bookplate designed by,Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton,"1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914",,"Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore",British|British|British|British|British,1910 |1853 |1872 |1870 |1850 |1832,1957 |1943 |1898 |1951 |1950 |1914,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (28),false,true,675554,Photographs,Album; Artist's book,"[Tithe Barn, Great Cokkeswell]",,,,,,Correspondent|Artist|Artist|Artist|Artist|Author|Correspondent,Bookplate designed by|Bookplate designed by,Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton,"1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914",,"Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore",British|British|British|British|British,1910 |1853 |1872 |1870 |1850 |1832,1957 |1943 |1898 |1951 |1950 |1914,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (27),false,true,675553,Photographs,Album; Artist's book,K. Church,,,,,,Correspondent|Artist|Artist|Artist|Author|Correspondent|Artist,Bookplate designed by|Bookplate designed by,Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Ernest Radford|Theodore Watts-Dunton|Frederick Colin Tilney,"1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, active late 19th century|British, 1832–1914|British, 1870–1951",,"Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Radford, Ernest|Watts-Dunton, Theodore|Tilney, Frederick Colin",British|British|British|British|British,1910 |1853 |1872 |1850 |1832 |1870,1957 |1943 |1898 |1950 |1914 |1951,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (16),false,true,675542,Photographs,Album; Artist's book,Passage to Panelled Room,,,,,,Correspondent|Correspondent|Artist|Author|Artist|Artist|Artist,Bookplate designed by|Bookplate designed by,Theodore Watts-Dunton|Sir Sydney Cockerell|Frederick Colin Tilney|Ernest Radford|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley,"British, 1832–1914|1910–1957|British, 1870–1951|British, active late 19th century|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton",,"Watts-Dunton, Theodore|Cockerell Sydney Sir|Tilney, Frederick Colin|Radford, Ernest|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent",British|British|British|British|British,1832 |1910 |1870 |1850 |1853 |1872,1914 |1957 |1951 |1950 |1943 |1898,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (24),false,true,675550,Photographs,Album; Artist's book,From the Tapestry Room,,,,,,Correspondent|Correspondent|Artist|Artist|Artist|Artist|Author,Bookplate designed by|Bookplate designed by,Theodore Watts-Dunton|Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford,"British, 1832–1914|1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century",,"Watts-Dunton, Theodore|Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest",British|British|British|British|British,1832 |1910 |1853 |1872 |1870 |1850,1914 |1957 |1943 |1898 |1951 |1950,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (26),false,true,675552,Photographs,Album; Artist's book,In the Attics,,,,,,Correspondent|Correspondent|Artist|Artist|Artist|Author|Artist,Bookplate designed by|Bookplate designed by,Theodore Watts-Dunton|Sir Sydney Cockerell|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Frederick H. Evans,"British, 1832–1914|1910–1957|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, London 1853–1943 London",,"Watts-Dunton, Theodore|Cockerell Sydney Sir|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Evans, Frederick Henry",British|British|British|British|British,1832 |1910 |1872 |1870 |1850 |1853,1914 |1957 |1898 |1951 |1950 |1943,1896,1896,1896,Platinum print,Image: 15.4 x 21 cm (6 1/16 x 8 1/4 in.),"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (18),false,true,675544,Photographs,Album; Artist's book,The Green Room,,,,,,Artist|Author|Correspondent|Correspondent|Artist|Artist|Artist,Bookplate designed by|Bookplate designed by,Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley,"British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton",,"Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent",British|British|British|British|British,1870 |1850 |1832 |1910 |1853 |1872,1951 |1950 |1914 |1957 |1943 |1898,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (8),false,true,675534,Photographs,Album; Artist's book,[Main Entrance],,,,,,Artist|Artist|Artist|Correspondent|Artist|Author|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Sir Sydney Cockerell|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|1910–1957|British, 1870–1951|British, active late 19th century|British, 1832–1914",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Cockerell Sydney Sir|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore",British|British|British|British|British,1853 |1872 |1910 |1870 |1850 |1832,1943 |1898 |1957 |1951 |1950 |1914,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519,false,true,271329,Photographs,Album; Artist's book,Kelmscott Manor Photographs,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum prints; albumen silver print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/271329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (2),false,true,675528,Photographs,Album; Artist's book,From the Meadows,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (3),false,true,675529,Photographs,Album; Artist's book,From the Fields,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (5),false,true,675531,Photographs,Album; Artist's book,Main Gate Entrance,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (9),false,true,675535,Photographs,Album; Artist's book,[Main Entrance],,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (10),false,true,675536,Photographs,Album; Artist's book,"[Main Entrance, with Hedge]",,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (11),false,true,675537,Photographs,Album; Artist's book,"[Garden, Front]",,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (12),false,true,675538,Photographs,Album; Artist's book,"[Garden, Front]",,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (13),false,true,675539,Photographs,Album; Artist's book,In the Orchard,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (14),false,true,675540,Photographs,Album; Artist's book,Bed Morris Was Born In,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (15),false,true,675541,Photographs,Album; Artist's book,WM's Bedroom,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (17),false,true,675543,Photographs,Album; Artist's book,The Panelled Room,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (20),false,true,675546,Photographs,Album; Artist's book,[Tapestry Room],,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (22),false,true,675548,Photographs,Album; Artist's book,[Tapestry Room],,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (23),false,true,675549,Photographs,Album; Artist's book,The Tapestry Room,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (25),false,true,675551,Photographs,Album; Artist's book,In the Attics,,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (29),false,true,675555,Photographs,Album; Artist's book,"[Tithe Barn, Great Cokkeswell]",,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (30),false,true,675556,Photographs,Album; Artist's book,"Tithe Barn, Great Cokkeswell, Interior",,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (31),false,true,675557,Photographs,Album; Artist's book,[Kelmscott Manor from the Garden],,,,,,Artist|Artist|Artist|Artist|Author|Correspondent|Correspondent,Bookplate designed by|Bookplate designed by,Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney|Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell,"British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951|British, active late 19th century|British, 1832–1914|1910–1957",,"Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin|Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir",British|British|British|British|British,1853 |1872 |1870 |1850 |1832 |1910,1943 |1898 |1951 |1950 |1914 |1957,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (7),false,true,675533,Photographs,Album; Artist's book,[Main Entrance],,,,,,Author|Correspondent|Correspondent|Artist|Artist|Artist|Artist,Bookplate designed by|Bookplate designed by,Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney,"British, active late 19th century|British, 1832–1914|1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951",,"Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin",British|British|British|British|British,1850 |1832 |1910 |1853 |1872 |1870,1950 |1914 |1957 |1943 |1898 |1951,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.519 (19),false,true,675545,Photographs,Album; Artist's book,Tapestry Room,,,,,,Author|Correspondent|Correspondent|Artist|Artist|Artist|Artist,Bookplate designed by|Bookplate designed by,Ernest Radford|Theodore Watts-Dunton|Sir Sydney Cockerell|Frederick H. Evans|Unknown|Aubrey Vincent Beardsley|Frederick Colin Tilney,"British, active late 19th century|British, 1832–1914|1910–1957|British, London 1853–1943 London|British|British, Brighton, Sussex 1872–1898 Menton|British, 1870–1951",,"Radford, Ernest|Watts-Dunton, Theodore|Cockerell Sydney Sir|Evans, Frederick Henry|Unknown|Beardsley, Aubrey Vincent|Tilney, Frederick Colin",British|British|British|British|British,1850 |1832 |1910 |1853 |1872 |1870,1950 |1914 |1957 |1943 |1898 |1951,1896,1896,1896,Platinum print,,"Purchase, David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/675545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.129,false,true,266993,Photographs,Album,[Album of 226 albumen silver prints of Japan],,,,,,Artist|Artist|Editor|Artist|Artist|Photography Studio|Publisher,,Raimund von Stillfried|Felice Beato|Stillfried and Andersen|Kusakabe Kimbei|Tamamura Kōzaburō|Yamamoto Studio|Stillfried and Andersen,"Austrian, 1839–1911|British (born Italy), Venice 1832–1909 Luxor, Egypt|active Japan, 1875–1885|Japanese, 1841–1934|Japanese, 1856–1923?|Japanese|active Japan, 1875–1885",,"Stillfried, Raimond von|Beato, Felice|Stillfried and Andersen|Kusakabe, Kimbei|Tamamura, Kōzaburō|Yamamoto Studio|Stillfried and Andersen","Austrian|British, born Italy|Japanese|Japanese|Japanese",1839 |1832 |1875 |1841 |1856 |1875,1911 |1909 |1885 |1934 |1923 |1885,1860s–90s,1860,1899,Albumen silver prints,,"Gift of H. de Rassloff, 1918, transferred from the Library",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/266993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.641,false,true,269626,Photographs,Photographically illustrated book,"La Photographie ses origines, ses progrès, ses transformations",,,,,,Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist|Artist,,Goupil et Cie|Édouard Baldus|B. Deloose|M. Marion|Henri Garnier|Zurcher|François-Auguste Renard|Joseph Nicéphore Niépce|Maxime Du Camp|Ernest Edwards|Louis-Désiré Blanquart-Évrard|Adolphe Braun|Frères Dujardin,"French, active 1850–84|French, born Prussia, 1813–1889|French|French|French, 1765–1833|French, 1822–1894|British, 1837–1903|French, 1802–1872|French, Besançon 1811–1877 Dornach|French",,"Goupil et Cie|Baldus, Édouard|Deloose, B.|Marion, M.|Garnier, Henri|Zurcher|Renard, François-Auguste|Niépce, Joseph Nicéphore|Du Camp, Maxime|Edwards, Ernest|Blanquart-Évrard, Louis-Désiré|Braun, Adolphe|Dujardin, Frères","French|French, born Prussia|French|French|French|French|British|French|French|French",1850 |1813 |1765 |1822 |1837 |1802 |1811,1884 |1889 |1833 |1894 |1903 |1872 |1877,1870s,1870,1879,Multiple photographic processes,,"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/269626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1158–.1187,false,true,288304,Photographs,Stereographs,"[Group of 30 Stereograph Views of Colorado and Arizona, United States of America]",,,,,,Publisher|Artist|Publisher|Publisher|Artist|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Artist|Publisher|Artist|Person in Photograph|Publisher,,Underwood & Underwood|Benneville Lloyd Singley|H. C. White Company|American Scenery|Strohmeyer & Wyman|Charles Weitfle|Barkalow Brothers|C. H. Graves|Universal Photo Art Co.|American Views|Montgomery Ward & Co.|William H. Rau|Continent Stereoscopic Company|Unknown|Grace Greenwood|Keystone View Company,"American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American|American|American, born Germany, 1836–after 1884|American|American|American|American, Chicago, Illinois|American, 1855–1920|New York|American|American, 1823–1904",,"Underwood & Underwood|Singley, Benneville Lloyd|H. C. White Company|American Scenery|Strohmeyer & Wyman|Weitfle, Charles|Barkalow Brothers|Graves, C. H.|Universal Photo Art Co.|American Views|Montgomery Ward & Co.|Rau, William H.|Continent Stereoscopic Company|Unknown|Greenwood, Grace|Keystone View Company",American|American|American|American|American|American|American|American|American|American|American|New York|American,1864 |1836 |1855 |1823,1938 |1884 |1920 |1904,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.8 x 17.3 cm (3 7/16 x 6 13/16 in.) to 10 x 17.8 cm (3 15/16 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.256–.298,false,true,288121,Photographs,Stereographs,[Group of 43 Stereograph Views of Astronomy Related Scenes],,,,,,Patron|Publisher|Publisher|Artist|Artist|Publisher|Publisher|Publisher|Publisher|Publisher|Photography Studio|Publisher|Publisher|Publisher|Artist|Publisher|Artist|Artist|Publisher|Publisher|Artist|Publisher|Publisher|Artist|Artist|Publisher,Commissioned by,"Nautical Almanac Office|New H Series|Charles Bierstadt|Professor H. Draper|L. M. Rutherford|Popular Series|T. W. Ingersoll|Stereo Gems|Liberty Brand Stereo Views|Underwood & Underwood|Sun Sculpture Works and Studios|Strohmeyer & Wyman|Yerkes Observatory|Carnegie Intitute|Kilburn Brothers|Edward Kilburn|William N. Hobbs|G. R. Proctor|Edward Bierstadt|Whiple|Deloss Barnum|E. & H. T. Anthony|Smith, Beck & Beck|G. W. Thorne|Unknown|Keystone View Company","American|American, 1819–1903|American|American|American|American|American|American|American|American, active ca. 1865–1890|American, 1830–1884|American|American|American, born Germany, born 1824|American|American, 1825–1873 Cortland, New York|American|British|American",,"Nautical Almanac Office|New H Series|Bierstadt, Charles|Draper, H. Professor|Rutherford, L. M.|Popular Series|Ingersoll, T. W.|Stereo Gems|Liberty Brand Stereo Views|Underwood & Underwood|Sun Sculpture Works and Studios|Strohmeyer & Wyman|Yerkes Observatory|Carnegie Intitute|Kilburn Brothers|Kilburn, Edward|Hobbs, William N.|Proctor, G. R.|Bierstadt, Edward|Whiple|Barnum, Deloss|E. & H. T. Anthony|Smith, Beck & Beck|Thorne, G. W.|Unknown|Keystone View Company","American|American|American|American|American|American|American|American|American|American|American|American|American|American, born Germany|American|American|American|British|American",1819 |1863 |1830 |1824 |1825,1903 |1892 |1884 |1873,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.3 x 17.1 cm (3 1/4 x 6 3/4 in.) to 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (1),false,true,679863,Photographs,Photograph,Jérusalem,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 1/8 × 31 1/2 in. (20.7 × 80 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (2),false,true,679864,Photographs,Photograph,"Jérusalem. État actuel du dôme, du St. Sépulcre et Minaret d'Omar",,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 3/16 × 10 7/8 in. (20.8 × 27.7 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (3),false,true,679865,Photographs,Photograph,Jérusalem. Entrée de l'Église du St. Sépulcre,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 13/16 × 8 5/16 in. (27.5 × 21.1 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (4),false,true,679866,Photographs,Photograph,Jérusalem. Tour de David avec ses grandes assises salomoniennes,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 1/8 × 10 7/8 in. (20.6 × 27.7 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (5),false,true,679867,Photographs,Photograph,Jérusalem. Église Ste Anne appartenant à la France,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 7/16 in. × 11 in. (21.4 × 28 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (6),false,true,679868,Photographs,Photograph,Jérusalem. Façade de l'Église Ste. Anne.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 9/16 in. (28 × 21.7 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (7),false,true,679869,Photographs,Photograph,Jérusalem. Intérieur de l'Église Ste Anne.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 7/8 × 8 1/16 in. (27.7 × 20.5 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (8),false,true,679870,Photographs,Photograph,Jérusalem. Hospice autrichien et ancienne Église St Jean,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 7/8 × 8 7/16 in. (27.7 × 21.4 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (9),false,true,679871,Photographs,Photograph,Jérusalem. Tour Antonia et Environs,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 1/8 × 11 1/4 in. (20.7 × 28.5 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (10),false,true,679872,Photographs,Photograph,Jérusalem. Massif de la Tour Antonia,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 13/16 × 8 1/16 in. (27.5 × 20.5 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (11),false,true,679873,Photographs,Photograph,"Jérusalem. Mosquée d'Omar, construite sur l'emplacement su Temple de Salomon",,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 7/16 × 11 1/8 in. (21.5 × 28.2 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (12),false,true,679874,Photographs,Photograph,Jérusalem. Mur oú pleurent les juifs. Grandes Assises du Temple de Salomon,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 5/8 × 11 1/8 in. (21.9 × 28.3 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (13),false,true,679875,Photographs,Photograph,Jérusalem. Une rue de Jérusalem et entrée du Grand Couvent,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 1/8 × 8 3/8 in. (28.2 × 21.3 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (14),false,true,679876,Photographs,Photograph,Jérusalem. Vue des Remparts.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 7 3/8 × 10 3/16 in. (18.8 × 25.8 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (15),false,true,679877,Photographs,Photograph,Jérusalem. Chapelle protestante et environs,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 in. × 10 1/4 in. (20.3 × 26 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (16),false,true,679878,Photographs,Photograph,Jérusalem. Synagogue juive et environs,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 in. × 10 1/4 in. (20.3 × 26 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (17),false,true,679879,Photographs,Photograph,Jérusalem. Porte de Hebron et de Jaffa. (Bab-el-Khalil),,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 3/16 × 8 7/16 in. (28.4 × 21.4 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (18),false,true,679880,Photographs,Photograph,Jérusalem. Porte de Damas ou des colonnes (Bab-el-Ahmoud),,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 7/16 × 11 1/8 in. (21.4 × 28.2 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (19),false,true,679881,Photographs,Photograph,Jérusalem. Porte de Damas (Bab-el-Ahmoud),,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 9/16 in. (28 × 21.7 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (21),false,true,679883,Photographs,Photograph,Jérusalem. (Environs) Montagne de Sion. Cenacle et Maison de Caiphe.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 1/2 in. (28 × 21.6 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (22),false,true,679884,Photographs,Photograph,Jérusalem. (Environs) Jardin Gethsemani et Mont des Oliviers,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 1/2 in. (28 × 21.6 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (23),false,true,679885,Photographs,Photograph,Jérusalem. (Environs) Grotte de Jérémie,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 1/4 in. (28 × 21 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (24),false,true,679886,Photographs,Photograph,Jérusalem. (Environs) Tombeau des Rois,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 13/16 × 8 3/8 in. (27.5 × 21.2 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (25),false,true,679887,Photographs,Photograph,Jérusalem. (Environs) Tombeau de la vierge,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 1/4 in. (28 × 21 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (26),false,true,679888,Photographs,Photograph,Jérusalem. (Environs) Tombeau d'Absalon,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 9/16 × 10 7/8 in. (21.8 × 27.7 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (27),false,true,679889,Photographs,Photograph,Jérusalem. Tombeaux de St Jacques et de Zacharie,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 11/16 × 11 1/4 in. (22 × 28.5 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (28),false,true,679890,Photographs,Photograph,Jérusalem. (Environs) St Jean du Désert,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 3/8 in. (28 × 21.3 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (29),false,true,679891,Photographs,Photograph,Arcade de l'Ecce Homo. Ponce Pilate présente Jésus au Peuple,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 13/16 × 8 3/16 in. (27.4 × 20.8 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (30),false,true,679892,Photographs,Photograph,Entrée d'une caserne turque. C'est a cette porte que les pèlerins font les prières de la 1e Station. N'ayant pas la permission d'entrer dans la caserne,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 9/16 in. × 8 in. (26.8 × 20.3 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (31),false,true,679893,Photographs,Photograph,Ie Station. Jésus est condamné à mort. Une cour intérieure de la caserne turque où la tradition place l'endroit du prêtoire où Jésus fut jugé,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 5/16 × 8 1/2 in. (28.7 × 21.6 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (32),false,true,679894,Photographs,Photograph,"IIe Station. Jésus est chargé de sa croix. Cette Station est placée au bas de la scala santa qui a été entièrement transportée à Rome, il n'en reste que les première assises",,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 11/16 × 8 1/4 in. (27.1 × 20.9 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (33),false,true,679895,Photographs,Photograph,IIIe Station. Jésus tombe pour la première fois. Une colonne brisée et entendue a terre indique la place de cette station au lieu où la voie douloureuse tourne brusquement a gauche,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 1/4 in. (28 × 21 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (34),false,true,679896,Photographs,Photograph,IVe Station. Jésus rencontre sa très Sainte Mère. Cette station est située a quelques pas a peine de la précédente. La tradition la place a l'arcade que l'on voit représentée ici,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 11/16 × 8 1/4 in. (27.2 × 21 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (35),false,true,679897,Photographs,Photograph,Ve Station. Jésus aidé par Simon de Cyrène. Une marque dans le mur indique seule cette station. La maison que l'on voit au fond est celle du mauvais riche,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 3/16 × 8 1/4 in. (28.4 × 21 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (36),false,true,679898,Photographs,Photograph,VIe Station. Ste Véronique essuie la face sanglante de Jésus. Aucune marque extérieure n'indique cette station. La tradition la place au pied du petit escalier,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 7/16 in. (28 × 21.5 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (37),false,true,679899,Photographs,Photograph,"VIIe Station. Jésus tombe pour la seconde fois. Ancienne porte judiciaire, il s'y trouvait une colonne ou l'on exposait les condamnés; Elle éxiste encore cachée dans l'intérieur de la maison a gauche",,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 1/8 in. (28 × 20.7 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (38),false,true,679900,Photographs,Photograph,VIIIe Station. Jésus console les filles de Jérusalem. Ici encore une simple marque faite sur le fut d'une colonne encastrée dans le mur indique la station.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 13/16 × 8 7/16 in. (27.5 × 21.5 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (39),false,true,679901,Photographs,Photograph,IXe Station. Jésus tombe pour la troisième fois. Le fut de colonne qui se trouve au pied du mur indique cette station.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 11 in. × 8 7/16 in. (28 × 21.5 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (40),false,true,679902,Photographs,Photograph,Xe Station. Jésus est dépouillé de ses Vêtements. L'emplacement de cette station est indique par la mosaïque en marbre que l'on voit devant l'autel.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 1/16 × 7 13/16 in. (25.6 × 19.8 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (41),false,true,679903,Photographs,Photograph,XIe Station. Jésus est cloué sur la croix. Cette station se trouve placée au pied même de l'autel.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 1/8 × 7 7/8 in. (25.7 × 20 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (42),false,true,679904,Photographs,Photograph,XIIe Station. Jésus meurt sur la croix. Autel élevé sur le lieu même ou le Christ a été crucifié,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 1/8 × 7 7/8 in. (25.7 × 20 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (43),false,true,679905,Photographs,Photograph,XIIIe Station. Jésus est remis entre les mains de sa mère. Cet autel est construit sur le rocher où se tenait la vierge marie pendant le crucifiement de son fils.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 10 1/8 × 7 7/8 in. (25.7 × 20 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (44),false,true,679906,Photographs,Photograph,XIVe Station. Le corps de Jésus est deposé dans le tombeau. Monument du St Sépulcre où le corps du Christ a été enseveli.,,,,,,Artist|Lithographer|Printer,,Louis de Clercq|H. Jannin|J. Blondeau et Antonin,"French, 1837–1901|French",,"de Clercq, Louis|Jannin, H.|J. Blondeau et Antonin",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 9 5/8 × 7 9/16 in. (24.5 × 19.2 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.500.3 (20),false,true,679882,Photographs,Photograph,Jérusalem. Portes Dorées,,,,,,Lithographer|Printer|Artist,,H. Jannin|J. Blondeau et Antonin|Louis de Clercq,"French|French, 1837–1901",,"Jannin, H.|J. Blondeau et Antonin|de Clercq, Louis",French|French|French,1837,1901,1860 or later,1860,1860,Albumen silver print from paper negative,Image: 8 3/8 × 10 3/4 in. (21.3 × 27.3 cm) Mount: 17 15/16 × 23 1/4 in. (45.5 × 59 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/679882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (3),false,true,287593,Photographs,Photograph,"[The Earl Canning, K.G., K.S.I., G.C.B., Calcutta]",,,,,,Artist|Artist|Artist,,Bourne and Shephard|Samuel Bourne|Charles Shepherd,"British, 1834–1912|British",,"Bourne and Shepherd|Bourne, Samuel|Shepherd, Charles",British|British,1834,1912,1858–61,1858,1861,Albumen silver print from glass negative,Image: 26.5 x 21.7 cm (10 7/16 x 8 9/16 in.) Mount: 33 x 26.4 cm (13 x 10 3/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.69,false,true,290464,Photographs,Photograph,Village de Murols,,,,,,Artist|Printer,,Édouard Baldus|Chicago Albumen Works,"French, born Prussia, 1813–1889",,"Baldus, Édouard|Chicago Albumen Works","French, born Prussia",1813,1889,"1854, printed 1979",1854,1854,Salted paper print from paper negative,Image: 34.2 x 44.3 cm (13 7/16 x 17 7/16 in.) Sheet: 35.3 x 47.2 cm (13 7/8 x 18 9/16 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1112.1,false,true,265967,Photographs,Photograph,[Rocks in the Auvergne],,,,,,Artist|Printer,,Édouard Baldus|Chicago Albumen Works,"French, born Prussia, 1813–1889",,"Baldus, Édouard|Chicago Albumen Works","French, born Prussia",1813,1889,"1854, printed ca. 1981",1854,1854,Salted paper print from paper negative,Image: 33.9 x 44.8 cm. (13 3/6 x 17 5/8 in.),"Museum Accession, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.492–.591,false,true,288149,Photographs,Stereographs,[Group of 100 Stereograph Views of California Nature and Landscapes With a Focus on Yosemite],,,,,,Author|Artist|Artist|Artist|Publisher|Publisher|Publisher|Artist|Publisher|Publisher|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Artist|Publisher|Publisher|Author|Photography Studio|Publisher|Publisher|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher,,"James Mason Hutchings|G. H. Aldrich & Company|Unknown|Unknown|C. W. Woodward|Strohmeyer & Wyman|Underwood & Underwood|H. C. White Company|American Scenery|Views in California|American Stereoscopic Company|Quaker Oats Company|T. W. Ingersoll|S. F. Sanderson|New H Series|S. W. Kelley|George W. Griffith|Griffith & Griffith, American|Popular Series|John Muir|Sun Sculpture Works and Studios|Dodge, Collier, & Perkins|J. Merrill & Son|Continent Stereoscopic Company|Andrew Price|American Series|H. Ropes & Co., American|Canvassers|J. F. Jarvis|Standard Series|A. Fuller|Lovejoy & Foster|American Views|Keystone View Company","American (born England), 1820–1902|American|American|American|American|American|American|American|American|American|American|American|American|American|American, born Scotland, 1838–1914|American|American|American|New York|American|American|American|American|American|American|American",,"Hutchings, James Mason|G. H. Aldrich & Company|Unknown|Unknown|Woodward, C. W.|Strohmeyer & Wyman|Underwood & Underwood|H. C. White Company|American Scenery|Views in California|American Stereoscopic Company|Quaker Oats Company|Ingersoll, T. W.|Sanderson, S. F.|New H Series|Kelley, S. W.|Griffith, George W.|Griffith & Griffith|Popular Series|Muir, John|Sun Sculpture Works and Studios|Dodge, Collier, & Perkins|J. Merrill & Son|Continent Stereoscopic Company|Price, Andrew|American Series|Ropes & Co., H.|Canvassers|Jarvis, J. F.|Standard Series|Fuller, A.|Lovejoy & Foster|American Views|Keystone View Company","American|American|American|American|American|American|American|American|American|American|American|American|American|American|American, born Scotland|American|American|American|New York|American|American|American|American|American|American|American|American",1820 |1838,1902 |1914,1860s–1910s,1860,1919,Albumen silver prints,Mounts approximately: 8.6 x 17.5 cm (3 3/8 x 6 7/8 in.) to 11.4 x 17.8 cm (4 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.22,false,true,285455,Photographs,Photograph,Naser al-Din Shah,,,,,,Artist|Person in Photograph,Person in photograph,Unknown|Naser od-Din Shah,"Iranian, Tehran 1831–1896 Tehran",,"Unknown|Din, Naser od- Shah",Iranian,1831,1896,ca. 1852–55,1852,1855,Salted paper print from glass negative,Image: 33.7 x 21.2 cm (13 1/4 x 8 3/8 in.),"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.82.1,false,true,268623,Photographs,Photograph,[Woman Opening Parasol],,,,,,Artist|Printer,,Eadweard Muybridge|The Photo-Gravure Company,"American, born Britain, 1830–1904",,"Muybridge, Eadweard|Photo-Gravure Company","American, born Britain",1830,1904,"1883–86, printed 1887",1883,1886,Collotype,,"Gift of the Philadelphia Commercial Museum, 1938",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.82.3,false,true,268638,Photographs,Photograph,[Boys Playing Leap Frog],,,,,,Artist|Printer,,Eadweard Muybridge|The Photo-Gravure Company,"American, born Britain, 1830–1904",,"Muybridge, Eadweard|Photo-Gravure Company","American, born Britain",1830,1904,"1883–86, printed 1887",1883,1886,Collotype,,"Gift of the Philadelphia Commercial Museum, 1938",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.9.69,false,true,266441,Photographs,Photograph,[Horse and Rider Galloping],,,,,,Artist|Printer,,Eadweard Muybridge|The Photo-Gravure Company,"American, born Britain, 1830–1904",,"Muybridge, Eadweard|Photo-Gravure Company","American, born Britain",1830,1904,"1883–86, printed 1887",1883,1886,Collotype,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.673,false,true,271667,Photographs,Photograph,Queen Victoria Presiding at the Reopening of the Reconstructed Crystal Palace at Sydenham,,,,,,Former Attribution|Artist,Attributed to,Philip Henry Delamotte|T. R. Williams,"British, 1821–1889|British, born 1825",,"Delamotte, Philip Henry|Williams, T. R.",British|British,1821 |1825,1889 |1825,1854,1854,1854,Albumen silver print from glass negative,,Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1–.14,false,true,288103,Photographs,Stereographs,[Group of 14 stereographs of Africa and Actors],,,,,,Artist|Publisher|Artist|Artist|Publisher|Publisher,,"Kilburn Brothers|Underwood & Underwood|Unknown|H. Ropes & Co., American|F. G. Weller|Keystone View Company","American, active ca. 1865–1890|American",,"Kilburn Brothers|Underwood & Underwood|Unknown|Ropes & Co., H.|Weller., F. G.|Keystone View Company",American|American|American,1863,1892,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.2 x 17.1 cm (3 1/4 x 6 3/4 in.) to 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1163,false,true,263729,Photographs,Photograph,Copacabana,,,,,,Artist|Printer,,Marc Ferrez|Alan B. Newman,"Brazilian, 1843–1923|American, born 1946",,"Ferrez, Marc|Newman, Alan B.",Brazilian|American,1843 |1946,1923,"1880, printed 1983",1880,1880,Platinum print from glass negative,Image: 17.3 x 32.9 cm (6 13/16 x 12 15/16 in.) Mount: 28.4 x 38 cm (11 3/16 x 14 15/16 in.),"Gift of H. L. Hoffenberg, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.16,false,true,269064,Photographs,Photograph,Étude d'arbre,,,,,,Artist|Printer,,"Edward King Tenison|Imprimerie photographique de Blanquart-Évrard, à Lille","Irish, 1805–1878|French, active 1851–55",,"Tenison, Edward King|Imprimerie photographique de Blanquart-Évrard, à Lille",Irish,1805 |1851,1878 |1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (1),false,true,288141,Photographs,Photograph,[Duchesse de Morny],,,,,,Artist|Artist|Person in Photograph,Painted and retouched by|Person in photograph,"Pierre-Louis Pierson|Marck|Sophie Troubetzkoi, duchesse de Morny","French, 1822–1913|Russian, Moscow 1838–1896",", et al","Pierson, Pierre-Louis|Marck|Troubetzkoi, Sophie, duchesse de Morny",French|Russian,1822 |1838,1913 |1896,before 1865,1855,1865,Albumen silver print from glass negative,Image: 6 5/16 × 4 3/4 in. (16 × 12 cm) Mount: 14 3/16 in. × 11 in. (36 × 28 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.41,false,true,676536,Photographs,Photograph,"Zouave, 2nd Division",,,,,,Artist|Publisher|Person in Photograph,Person in photograph,"Roger Fenton|Thomas Agnew & Sons, Ltd.|Roger Fenton","British, 1819–1869|London|British, 1819–1869",,"Fenton, Roger|Agnew, Thomas & Sons, Ltd.|Fenton, Roger",British|British,1819 |1819,1819 |1819,1855,1855,1855,Salted paper print from collodion glass negative,Image: 7 5/8 × 6 7/16 in. (19.4 × 16.4 cm) Mount: 23 5/8 × 17 1/4 in. (60 × 43.8 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/676536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.315–.385,false,true,288138,Photographs,Stereographs,"[Group of 71 Stereograph Views of African-Americans and Early Black American Culture, including Colloquial Black Humor]",,,,,,Publisher|Publisher|Publisher|Publisher|Artist|Artist|Publisher|Publisher|Publisher|Artist|Publisher|Photography Studio|Artist|Artist|Artist|Artist|Artist|Publisher|Publisher|Publisher|Artist|Artist|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Artist|Publisher|Artist|Publisher,,"Francis Hendricks|European and American Views|Webster & Albee|American Stereoscopic Company|J. Mullen|E. F. Smith|American Scenery|Life Groups|America Illustrated|Wilson & Havens|Underwood & Underwood|Sun Sculpture Works and Studios|Strohmeyer & Wyman|George Barker|Kilburn Brothers|H. C. White Company|T. W. Ingersoll|Canvassers|Continent Stereoscopic Company|Littleton View Company|Alfred S. Campbell|James M. Davis|C. H. Graves|Florida Novelties and Views|Griffith & Griffith, American|Metropolitan Series|Ingersoll View Company|Popular Series|Universal View Co.|William H. Rau|M. H. Zahner|Unknown|Keystone View Company","American, Syracuse, New York|American|American|American|American|American|American|American|American|American|American|American|American, born Canada, 1844–1894|American, active ca. 1865–1890|American|New York|American|American|American|American|American|American|American|American, 1855–1920|American",,"Hendricks, Francis|European and American Views|Webster & Albee|American Stereoscopic Company|Mullen, J.|Smith, E. F.|American Scenery|Life Groups|America Illustrated|Wilson & Havens|Underwood & Underwood|Sun Sculpture Works and Studios|Strohmeyer & Wyman|Barker, George|Kilburn Brothers|H. C. White Company|Ingersoll, T. W.|Canvassers|Continent Stereoscopic Company|Littleton View Company|Campbell, Alfred S.|Davis, James M.|Graves, C. H.|Florida Novelties and Views|Griffith & Griffith|Metropolitan Series|Ingersoll View Company|Popular Series|Universal View Co.|Rau, William H.|Zahner, M. H.|Unknown|Keystone View Company",American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|New York|American|American|American|American|American|American|American|American|American|American,1844 |1863 |1855,1894 |1892 |1920,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.5 x 17.5 cm (3 3/8 x 6 7/8 in.) to 10.7 x 17.8 cm (4 3/16 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.196,false,true,285607,Photographs,Photograph,[Portrait in a White Dress],,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Aquilin Schad|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|Austrian, 1817–1866|1835–1899",,"Pierson, Pierre-Louis|Schad, Aquilin|Castiglione, di, Virginia Oldoini Verasis Countess",French|Austrian,1822 |1817 |1835,1913 |1866 |1899,"1856–57, printed 1861–66",1856,1866,Salted paper print from glass negative,Image: 32.5 x 27 cm (12 13/16 x 10 5/8 in.) Mount: 39.1 x 29.7 cm (15 3/8 x 11 11/16 in.) Mat: 61 x 50.8 cm (24 x 20 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.77,false,true,269223,Photographs,Photograph,Auguste Rodin,,,,,,Artist|Person in Photograph,,Gertrude Käsebier|Auguste Rodin,"American, 1852–1934|French, Paris 1840–1917 Meudon",,"Käsebier, Gertrude|Rodin, Auguste",American|French,1852 |1840,1934 |1917,1907,1907,1907,Platinum print,,"Gift of Adele Rollins Clifton, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.185,false,true,306170,Photographs,Photograph,View of the Arruns Pass and Peak from the Pont de Soubé,,,,,,Artist|Printer,,"John Stewart|Imprimerie photographique de Blanquart-Évrard, à Lille","British, Scotland 1814–1887|French, active 1851–55",,"Stewart, John|Imprimerie photographique de Blanquart-Évrard, à Lille","British, Scottish",1814 |1851,1887 |1855,1852,1852,1852,"Salted paper print from paper negative, Blanquart Evrard process",Image: 22.2 × 29.3 cm (8 3/4 × 11 9/16 in.) Mount: 43.6 × 61.9 cm (17 3/16 × 24 3/8 in.),"Purchase, Susan and Thomas Dunn and Peter Bunnell Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.23,false,true,652117,Photographs,Photograph,[Naser al-Din Shah],,,,,,Person in Photograph|Artist,Person in photograph|Possibly by,Naser od-Din Shah|Luigi Pesce,"Iranian, Tehran 1831–1896 Tehran|Italian, 1818–1891",,"Din, Naser od- Shah|Pesce, Luigi",Iranian|Italian,1831 |1818,1896 |1891,1840s–60s,1840,1869,Salted paper print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.24,false,true,285456,Photographs,Photograph,Naser al-Din Shah,,,,,,Artist|Person in Photograph,Possibly by|Person in photograph,Luigi Pesce|Naser od-Din Shah,"Italian, 1818–1891|Iranian, Tehran 1831–1896 Tehran",,"Pesce, Luigi|Din, Naser od- Shah",Italian|Iranian,1818 |1831,1891 |1896,ca. 1855–58,1855,1858,Salted paper print from paper negative,Image: 19.5 x 13.5 cm (7 11/16 x 5 5/16 in.),"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.1,false,true,260977,Photographs,Photograph,LeRoy Beaulieu,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 7 7/16 × 5 11/16 in. (18.9 × 14.5 cm) Sheet: 14 13/16 × 9 15/16 in. (37.6 × 25.2 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.2,false,true,260980,Photographs,Photograph,Luks: Painter,,,,,,Artist|Maker,,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 8 1/16 × 6 1/8 in. (20.4 × 15.6 cm) Sheet: 14 7/8 × 9 15/16 in. (37.8 × 25.2 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.3,false,true,260981,Photographs,Photograph,Josephine (Portrait of Miss B.),,,,,,Artist|Maker,,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,1903,1903,1903,Platinum print,Image: 8 in. × 5 15/16 in. (20.3 × 15.1 cm) Sheet: 14 15/16 in. × 10 in. (37.9 × 25.4 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.4,false,true,260982,Photographs,Photograph,"""When the Sands are Running Low""",,,,,,Artist|Maker,,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.5,false,true,260983,Photographs,Photograph,Mrs. F. H. Evans,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 7 13/16 × 5 7/8 in. (19.8 × 14.9 cm) Sheet: 14 15/16 × 10 1/16 in. (38 × 25.6 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.6,false,true,260984,Photographs,Photograph,Mrs. F. H. Evans,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 6 3/4 × 5 15/16 in. (17.1 × 15.1 cm) Sheet: 14 5/8 in. × 10 in. (37.1 × 25.4 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.7,false,true,260985,Photographs,Photograph,Mrs. F. H. Evans,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 7 1/2 × 5 3/4 in. (19 × 14.6 cm) Sheet: 14 7/8 × 10 1/8 in. (37.8 × 25.7 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.8,false,true,260986,Photographs,Photograph,Frederick H. Evans,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 9 3/4 × 5 9/16 in. (24.8 × 14.1 cm) Sheet: 14 3/4 × 7 7/16 in. (37.5 × 18.9 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.9,false,true,260987,Photographs,Photograph,F. H. Evans,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 8 in. × 5 13/16 in. (20.3 × 14.7 cm) Sheet: 14 13/16 × 10 1/16 in. (37.7 × 25.6 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.10,false,true,260978,Photographs,Photograph,F. H. Evans,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 8 in. × 5 7/8 in. (20.3 × 15 cm) Sheet: 14 13/16 × 10 5/16 in. (37.7 × 26.2 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.613.11,false,true,260979,Photographs,Photograph,F. H. Evans,,,,,,Artist|Maker,Mounted by,Gertrude Käsebier|Frederick H. Evans,"American, 1852–1934|British, London 1853–1943 London",,"Käsebier, Gertrude|Evans, Frederick Henry",American|British,1852 |1853,1934 |1943,ca. 1901,1899,1903,Platinum print,Image: 7 11/16 in. × 6 in. (19.5 × 15.3 cm) Sheet: 14 7/8 × 10 3/8 in. (37.8 × 26.3 cm),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1010.3,false,true,263303,Photographs,Photograph,"View of Transept, Looking South",,,,,,Artist|Printer,,Hugh Owen|Nicolaas Henneman,"British, 1808–1897|Dutch, Heemskerk 1813–1898 London",,"Owen, Hugh|Henneman, Nicolaas",British|Dutch,1808 |1813,1897 |1898,1851,1851,1851,Salted paper print from paper negative,Image: 21.6 x 16.3 cm (8 1/2 x 6 7/16 in.) Mount: 35 x 21.2 cm (13 3/4 x 8 3/8 in.),"Purchase, Emanuel Gerard Gift, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1010.5,false,true,263305,Photographs,Photograph,Greek Slave,,,,,,Artist|Printer,,Hugh Owen|Nicolaas Henneman,"British, 1808–1897|Dutch, Heemskerk 1813–1898 London",,"Owen, Hugh|Henneman, Nicolaas",British|Dutch,1808 |1813,1897 |1898,1851,1851,1851,Salted paper print from paper negative,Image: 8 3/8 × 6 5/16 in. (21.3 × 16.1 cm) Mount: 13 3/4 × 9 13/16 in. (35 × 25 cm),"Purchase, Emanuel Gerard Gift, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.724.2,false,true,271123,Photographs,Photograph,"Mirror Lake, Valley of the Yosemite",,,,,,Artist|Printer|Printer,,Eadweard Muybridge|Bradley and Rulofson|Henry W. Bradley,"American, born Britain, 1830–1904|American, 1813–1891",,"Muybridge, Eadweard|Bradley and Rulofson|Bradley, Henry W.","American, born Britain|American",1830 |1813,1904 |1891,1872,1872,1872,Albumen silver print from glass negative,42.8 x 54.3 cm (16 7/8 x 21 3/8 in. ),"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.40.83,false,true,269270,Photographs,Photograph,"Notre Dame, Paris",,,,,,Artist|Artist|Photography Studio,,Louis-Auguste Bisson|Auguste-Rosalie Bisson|Bisson Frères,"French, 1814–1876|French, 1826–1900|French, active 1852–1863",,"Bisson, Louis-Auguste|Bisson, Auguste-Rosalie|Bisson Frères",French|French|French,1814 |1826 |1852,1876 |1900 |1863,1850s,1850,1859,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (4),false,true,288159,Photographs,Photograph,[Empress Eugénie as an Odalisque],,,,,,Artist|Artist|Person in Photograph,Painted and retouched by|Person in photograph,Pierre-Louis Pierson|Marck|Empress Eugénie de Montijo,"French, 1822–1913|French (born Spain), Granada 1826–1920 Madrid",", et al","Pierson, Pierre-Louis|Marck|Montijo, Eugénie de, Empress","French|French, born Spain",1822 |1826,1913 |1920,1861–65,1861,1865,Albumen silver print,Image: 17 x 12 cm (6 11/16 x 4 3/4 in.) Mount: 38 x 28 cm (14 15/16 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.1,false,true,289273,Photographs,Photograph,Ru-Shan Monastery,,,,,,Former Attribution|Artist,Possibly by,John Thomson|Lai Fong,"British, Edinburgh, Scotland 1837–1921 London|Chinese, 1839–1890",,"Thomson, John|Lai, Fong","British, Scottish",1837 |1839,1921 |1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 15/16 × 11 1/8 in. (22.7 × 28.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.27,false,true,289299,Photographs,Photograph,"Yuen-foo River, View from the Hill",,,,,,Former Attribution|Artist,Possibly by,John Thomson|Lai Fong,"British, Edinburgh, Scotland 1837–1921 London|Chinese, 1839–1890",,"Thomson, John|Lai, Fong","British, Scottish",1837 |1839,1921 |1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/16 × 11 7/16 in. (20.4 × 29 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.30,false,true,289302,Photographs,Photograph,On the Road up to Yuen-foo Monastery,,,,,,Former Attribution|Artist,Possibly by,John Thomson|Lai Fong,"British, Edinburgh, Scotland 1837–1921 London|Chinese, 1839–1890",,"Thomson, John|Lai, Fong","British, Scottish",1837 |1839,1921 |1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 5/16 × 9 3/8 in. (18.6 × 23.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.33,false,true,289305,Photographs,Photograph,Bankers Glen Yuen foo Monastery,,,,,,Former Attribution|Artist,Possibly by,John Thomson|Lai Fong,"British, Edinburgh, Scotland 1837–1921 London|Chinese, 1839–1890",,"Thomson, John|Lai, Fong","British, Scottish",1837 |1839,1921 |1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 1/16 × 11 3/8 in. (23 × 28.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.43,false,true,289315,Photographs,Photograph,"Bowling Alley and Raquet Court, Foochow",,,,,,Former Attribution|Artist,Possibly by,John Thomson|Lai Fong,"British, Edinburgh, Scotland 1837–1921 London|Chinese, 1839–1890",,"Thomson, John|Lai, Fong","British, Scottish",1837 |1839,1921 |1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 15/16 × 11 5/16 in. (22.7 × 28.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.714–.736,false,true,288229,Photographs,Stereographs,[Group of 23 Early Stereograph Views of British Cathedrals],,,,,,Artist|Artist|Publisher|Artist|Publisher|Artist|Publisher|Artist,,Unknown|Budge|Edinburgh Stereographic Company|F. G. O. Stuart|John Browning|Mrs. Charles Lawrence|H. T. Ramsay|Thomas Heaviside,"British|British|British|British|British|British|British, 1828–1886",,"Unknown|Budge|Edinburgh Stereographic Company|Stuart, F. G. O.|Browning, John|Lawrence, Charles Mrs.|Ramsay, H. T.|Heaviside, Thomas","British|British, Scottish|British|British|British|British|British",1828,1886,1860s–80s,1860,1889,Albumen silver prints,Mounts approximately: 8.4 x 16.9 cm (3 5/16 x 6 5/8 in.) to 8.5 x 17.6 cm (3 3/8 x 6 15/16 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.97–.204,false,true,288118,Photographs,Stereographs,[Group of 107 Stereograph Views of Animals],,,,,,Publisher|Publisher|Publisher|Publisher|Publisher|Artist|Photography Studio|Artist|Publisher|Artist|Artist|Artist|Publisher|Publisher|Artist|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Artist|Publisher,,"Griffith & Griffith, American|William H. Rau|C. H. Graves|Universal Photo Art Co.|Underwood & Underwood|George Barker|Sun Sculpture Works and Studios|John L. Lovell|J. A. French|Merrimac Stereoscopic Company|E. M. Van Aken|H. Rikard, American|H. C. White Company|International View Company|C. L. Wasson|Allen|Stereoscopic Gems|H. Werner|Wilder & Williamson|Strohmeyer & Wyman|Union View Company|Benneville Lloyd Singley|Standard Series|J. F. Jarvis|Canvassers|Webster & Albee|T. W. Ingersoll|Metropolitan Series|Littleton View Company|The Whiting View Company|T. C. Fletcher & Company|Unknown|Keystone View Company","American, 1855–1920|American|American|American, born Canada, 1844–1894|American|American, 1825–1903|American|American|American|American|American|American|American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American|American|American|American|American|American",,"Griffith & Griffith|Rau, William H.|Graves, C. H.|Universal Photo Art Co.|Underwood & Underwood|Barker, George|Sun Sculpture Works and Studios|Lovell, John L.|French, J. A.|Merrimac Stereoscopic Company|Van Aken, E. M.|Rikard, H.|H. C. White Company|International View Company|Wasson, C. L.|Allen|Stereoscopic Gems|Werner, H.|Wilder & Williamson|Strohmeyer & Wyman|Union View Company|Singley, Benneville Lloyd|Standard Series|Jarvis, J. F.|Canvassers|Webster & Albee|Ingersoll, T. W.|Metropolitan Series|Littleton View Company|Whiting View Company|T. C. Fletcher & Company|Unknown|Keystone View Company",American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American|American,1855 |1844 |1825 |1864,1920 |1894 |1903 |1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.3 x 17.2 cm (3 1/4 x 6 3/4 in.) to 11.4 x 17.7 cm (4 1/2 x 6 15/16 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1940–.1950,false,true,288315,Photographs,Stereographs,"[Group of 11 Stereograph Views of the 1869 and 1872 World Peace Jubilees, Boston, Massachusetts, United States of America]",,,,,,Publisher|Artist|Subject|Publisher|Artist,Cited in manuscript,Charles Pollock|William G. Preston|Joseph H. Chadwick|John P. Soule|S. Towle,"American|American, 1842–1910|American|American, 1827–1904|American",,"Pollock, Charles|Preston, William G.|Chadwick, Joseph H.|Soule, John P.|Towle, S.",American|American|American|American|American,1842 |1827,1910 |1904,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.1 x 17.3 cm (3 3/16 x 6 13/16 in.) to 8.7 x 17.7 cm (3 7/16 x 6 15/16 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.206,false,true,296346,Photographs,Photograph,"[Distortograph: William Hale ""Big Bill"" Thompson, Mayor of Chicago]",,,,,,Artist|Person in Photograph,,Herbert George Ponting|William Hale Thompson,"British, Salisbury, Wiltshire 1870–1935 London|American, 1869–1944",,"Ponting, Herbert George|Thompson, William Hale",British|American,1870 |1869,1935 |1944,1927,1927,1927,Gelatin silver print,"Image: 9.8 x 7.3 cm (3 7/8 x 2 7/8 in.) Sheet: 16.3 x 12 cm (6 7/16 x 4 3/4 in.) Frame: 50.8 x 40.6 cm (20 x 16 in.) (Framed with 2011.207, .208, .209)","Twentieth-Century Photography Fund, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.207,false,true,296347,Photographs,Photograph,"[Distortograph: William Hale ""Big Bill"" Thompson, Mayor of Chicago]",,,,,,Artist|Person in Photograph,,Herbert George Ponting|William Hale Thompson,"British, Salisbury, Wiltshire 1870–1935 London|American, 1869–1944",,"Ponting, Herbert George|Thompson, William Hale",British|American,1870 |1869,1935 |1944,1927,1927,1927,Gelatin silver print,"Image: 9.8 x 7.3 cm (3 7/8 x 2 7/8 in.) Sheet: 16.2 x 12.1 cm (6 3/8 x 4 3/4 in.) Frame: 50.8 x 40.6 cm (20 x 16 in.) (Framed with 2011.206, .208, .209)","Twentieth-Century Photography Fund, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.208,false,true,296348,Photographs,Photograph,"[Distortograph: William Hale ""Big Bill"" Thompson, Mayor of Chicago]",,,,,,Artist|Person in Photograph,,Herbert George Ponting|William Hale Thompson,"British, Salisbury, Wiltshire 1870–1935 London|American, 1869–1944",,"Ponting, Herbert George|Thompson, William Hale",British|American,1870 |1869,1935 |1944,1927,1927,1927,Gelatin silver print,"Image: 9.8 x 7.3 cm (3 7/8 x 2 7/8 in.) Sheet: 16.3 x 11.9 cm (6 7/16 x 4 11/16 in.) Frame: 50.8 x 40.6 cm (20 x 16 in.) (Framed with 2011.206, .207, .209)","Twentieth-Century Photography Fund, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.209,false,true,296349,Photographs,Photograph,"[Distortograph: William Hale ""Big Bill"" Thompson, Mayor of Chicago]",,,,,,Artist|Person in Photograph,,Herbert George Ponting|William Hale Thompson,"British, Salisbury, Wiltshire 1870–1935 London|American, 1869–1944",,"Ponting, Herbert George|Thompson, William Hale",British|American,1870 |1869,1935 |1944,1927,1927,1927,Gelatin silver print,"Image: 9.8 x 7.3 cm (3 7/8 x 2 7/8 in.) Sheet: 16.3 x 12 cm (6 7/16 x 4 3/4 in.) Frame: 50.8 x 40.6 cm (20 x 16 in.) (Framed with 2011.206, .207, .208)","Twentieth-Century Photography Fund, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.4,false,true,306205,Photographs,Photograph,The Last Sitting,,,,,,Artist|Person in Photograph,Person in photograph,Lewis Carroll|Alice Pleasance Liddell,"British, Daresbury, Cheshire 1832–1898 Guildford|British, 1852–1934",,"Carroll, Lewis|Liddell, Alice",British|British,1832 |1852,1898 |1934,"June 25, 1870",1870,1870,Albumen silver print from glass negative,Sheet: 6 1/4 × 5 9/16 in. (15.9 × 14.1 cm) Image: 5 7/8 × 4 15/16 in. (15 × 12.6 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.20,false,true,283092,Photographs,Photograph,"Alice Liddell as ""The Beggar Maid""",,,,,,Artist|Subject,,Lewis Carroll|Alice Pleasance Liddell,"British, Daresbury, Cheshire 1832–1898 Guildford|British, 1852–1934",,"Carroll, Lewis|Liddell, Alice",British|British,1832 |1852,1898 |1934,1858,1858,1858,Albumen silver print from glass negative,Image: 16.3 x 10.9cm (6 7/16 x 4 5/16in.) Mount: 14 1/8 in. × 10 7/8 in. (35.8 × 27.6 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.6,false,true,306207,Photographs,Photograph,Edith Liddell Seated Beside a Vase of Foxgloves,,,,,,Artist|Person in Photograph,Person in photograph,Lewis Carroll|Edith Mary Liddell,"British, Daresbury, Cheshire 1832–1898 Guildford|British, 1854–1876",,"Carroll, Lewis|Liddell, Edith Mary",British|British,1832 |1854,1898 |1876,1860,1860,1860,Albumen silver print from glass negative,Sheet: 6 1/4 × 5 3/4 in. (15.9 × 14.6 cm) Image: 5 1/4 × 3 3/4 in. (13.3 × 9.5 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.636,false,true,286007,Photographs,Photograph,"""The Prettiest Doll in the World""",,,,,,Artist|Subject,,"Lewis Carroll|Alexandra ""Xie"" Rhoda Kitchin","British, Daresbury, Cheshire 1832–1898 Guildford|British, 1864–1925",,"Carroll, Lewis|Kitchin, Alexandra ""Xie"" Rhoda",British|British,1832 |1864,1898 |1925,"July 5, 1870",1870,1870,Albumen silver print from glass negative,7 3/4 x 5 13/16,"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.408,false,true,267824,Photographs,Photograph,Römische Villa,,,,,,Former Attribution|Artist,,Hugo Henneberg|Heinrich Kühn,"Austrian, 1863–1918|Austrian (born Germany), Dresden 1866–1944 Birgitz",,"Henneberg, Hugo|Kühn, Heinrich","Austrian|Austrian, born Germany",1863 |1866,1918 |1944,1898–1900,1898,1900,Gum bichromate print,55.8 x 74.5 cm. (22 x 29 5/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.353,false,true,267768,Photographs,Photograph,Robert Browning,,,,,,Artist|Printer,,Julia Margaret Cameron|The Autotype Company,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon|British, London",,"Cameron, Julia Margaret|Autotype Company","British, born India|British",1815,1815,"1865, printed ca. 1902",1865,1865,Carbon print,21.3 x 22.1 cm (8 3/8 x 8 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.319,false,true,269428,Photographs,Photograph,"Alfred, Lord Tennyson",,,,,,Artist|Printer,,Julia Margaret Cameron|The Autotype Company,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon|British, London",,"Cameron, Julia Margaret|Autotype Company","British, born India|British",1815,1815,"1869, printed 1905",1869,1869,Carbon print,35.8 x 26.2 cm. (14 1/16 x 10 5/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.320,false,true,269430,Photographs,Photograph,"Alfred, Lord Tennyson",,,,,,Artist|Printer,,Julia Margaret Cameron|The Autotype Company,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon|British, London",,"Cameron, Julia Margaret|Autotype Company","British, born India|British",1815,1815,"1867, printed 1905",1867,1867,Carbon print,34.4 x 25.6 cm. (13 9/16 x 10 1/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.321,false,true,269431,Photographs,Photograph,George Frederick Watts,,,,,,Artist|Printer,,Julia Margaret Cameron|The Autotype Company,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon|British, London",,"Cameron, Julia Margaret|Autotype Company","British, born India|British",1815,1815,"1864, printed ca. 1905",1864,1864,Carbon print,25.5 x 20.2 cm. (10 1/16 x 7 15/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.322,false,true,269432,Photographs,Photograph,Lord Justice James,,,,,,Artist|Printer,,Julia Margaret Cameron|The Autotype Company,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon|British, London",,"Cameron, Julia Margaret|Autotype Company","British, born India|British",1815,1815,"ca. 1870, printed ca. 1905",1868,1872,Carbon print,33.3 x 26.6 cm. (13 1/8 x 10 1/2 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.323,false,true,269433,Photographs,Photograph,"Ellen Terry, at the age of sixteen",,,,,,Artist|Printer,,Julia Margaret Cameron|The Autotype Company,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon|British, London",,"Cameron, Julia Margaret|Autotype Company","British, born India|British",1815,1815,"1864, printed ca. 1913",1864,1864,Carbon print,24 x 26.7 cm (9 7/16 x 10 1/2 in. ),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.324,false,true,269434,Photographs,Photograph,Thomas Carlyle,,,,,,Artist|Printer,,Julia Margaret Cameron|The Autotype Company,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon|British, London",,"Cameron, Julia Margaret|Autotype Company","British, born India|British",1815,1815,1867,1867,1867,Carbon print,35.0 x 28.1 cm. (13 3/4 x 11 1/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.2035–.2037,false,true,288318,Photographs,Stereographs,"[Group of 3 Stereograph Views of the 1901 Pan American Exposition, Buffalo, New York]",,,,,,Subject|Artist|Person in Photograph|Person in Photograph,,James M. Davis|Kilburn Brothers|William McKinley|First Lady Ida Saxton McKinley,"American, active ca. 1865–1890|American, 1843–1901|American, 1847–1907",,"Davis, James M.|Kilburn Brothers|McKinley, William|McKinley, Ida Saxton First Lady",American|American|American,1863 |1843 |1847,1892 |1901 |1907,1850s–1910s,1850,1919,Albumen silver prints,Mounts: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.53,false,true,306337,Photographs,Photograph,Group of Gentlemen Conversing over a Glass of Wine,,,,,,Person in Photograph|Artist,Person in photograph,Antoine-François-Jean Claudet|William Henry Fox Talbot,"French, active Great Britain, 1797–1867|British, Dorset 1800–1877 Lacock",,"Claudet, Antione-François-Jean|Talbot, William Henry Fox","French, active Great Britain|British",1797 |1800,1867 |1800,"February 7, 1846",1846,1846,Salted paper print from paper negative,Mount: 12 3/16 × 9 11/16 in. (31 × 24.6 cm) Image: 5 1/2 × 7 13/16 in. (13.9 × 19.9 cm) Top corners trimmed,"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.245,false,true,268040,Photographs,Photograph,"The Crack Team of the 1st Division, 6th Corps near Hazel River, Virginia",,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|Timothy H. O'Sullivan,"American, born Ireland, 1823?–1896 New York|American, born Ireland, 1840–1882",,"Brady, Mathew B.|O'Sullivan, Timothy H.","American|American, born Ireland",1823 |1840,1896 |1882,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.266,false,true,268063,Photographs,Photograph,"[U.S. Gunboat]. Brady album, p. 161",,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|Timothy H. O'Sullivan,"American, born Ireland, 1823?–1896 New York|American, born Ireland, 1840–1882",,"Brady, Mathew B.|O'Sullivan, Timothy H.","American|American, born Ireland",1823 |1840,1896 |1882,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.3,false,true,268100,Photographs,Photograph,"Yellow House, Warren Station, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.10,false,true,267880,Photographs,Photograph,"General Hospital, Point of Rocks, Appomattox River below Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.12,false,true,267902,Photographs,Photograph,"Poplar Grove Church, built by 50th New York Volunteers",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,16.5 x 20.2 cm (6 1/2 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.13,false,true,267913,Photographs,Panorama,"Confederate Prisoners at Belle Plain, May 12",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.19,false,true,267979,Photographs,Photograph,"White House Landing, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.26,false,true,268056,Photographs,Photograph,Bull Run. Bridge near Union Mills (destroyed seven times),,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,ca. 1862,1860,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.28,false,true,268078,Photographs,Photograph,"Colored Battery, Petersburg, June",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.35,false,true,268156,Photographs,Photograph,"Commodore Perry, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.54,false,true,268246,Photographs,Photograph,"White House Landing, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,13.1 x 20.2 cm (5 3/16 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.55,false,true,268247,Photographs,Photograph,"White House Landing, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.56,false,true,268248,Photographs,Photograph,"Bridge Across Pamunkey River, near White House",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.64,false,true,268257,Photographs,Photograph,"Blackburn's Ford / Rapidan River, The Wilderness",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.65,false,true,268258,Photographs,Photograph,"White House Landing, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.66,false,true,268259,Photographs,Photograph,"White House Landing, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.67,false,true,268260,Photographs,Photograph,"White House Landing, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,12.9 x 19.2 cm (5 1/16 x 7 9/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.80,false,true,268275,Photographs,Photograph,"U.S. Gunboat ""Commodore Perry"" on Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.105,false,true,267886,Photographs,Photograph,Bull Run. Blackburn's Ford,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,ca. 1862,1860,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.111,false,true,267893,Photographs,Photograph,"Swamp near Broadway Landing, Appomattox River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.130,false,true,267914,Photographs,Photograph,"U.S. Gunboat ""Commodore Perry"" on Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.140,false,true,267925,Photographs,Photograph,"Interior of Fort Steadman, front of Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.144,false,true,267929,Photographs,Photograph,"Outer Confederate Line, Petersburg, Captured June 15, 1864",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.145,false,true,267930,Photographs,Photograph,"Fort Burnham, front of Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.146,false,true,267931,Photographs,Photograph,"The Mine, Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,16 x 20.4 cm (6 5/16 x 8 1/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.147,false,true,267932,Photographs,Photograph,"Interior of Fort Sedgwick, before Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.149,false,true,267934,Photographs,Photograph,"The Crater, Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.154,false,true,267940,Photographs,Photograph,In front of Petersburg,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,16.3 x 21.3 cm (6 7/16 x 8 3/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.156,false,true,267942,Photographs,Photograph,"Outer Confederate Line, Petersburg, Captured June 15, 1864",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.157,false,true,267943,Photographs,Photograph,Front of Petersburg Lines,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.160,false,true,267947,Photographs,Photograph,"Outer Confederate Line at Petersburg. Captured by 18th Corps, June 15, 1864",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.162,false,true,267949,Photographs,Photograph,"Warren Station, near Petersburg (graves)",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,14.8 x 20.8 cm (5 13/16 x 8 3/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.168,false,true,267955,Photographs,Photograph,Bull Run. Blackburn's Ford,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,ca. 1862,1860,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.169,false,true,267956,Photographs,Photograph,Bull Run. Blackburn's Ford,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,ca. 1862,1860,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.186,false,true,267975,Photographs,Photograph,Confederate Prisoners at Belle Plain,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.201,false,true,267993,Photographs,Panorama,"Camp of Confederate Prisoners at Belle Plain, May 12, 1863",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,15.1 x 20.7 cm (5 15/16 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.233,false,true,268027,Photographs,Photograph,"Quartermaster and Ambulance Camp, 6th Corps, Brandy Station, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.241,false,true,268036,Photographs,Photograph,"Pennsylvania Light Artillery, Battery B, Petersburg, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.256,false,true,268052,Photographs,Photograph,"Quartermaster and Ambulance Camp, Brandy Station, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,13.2 x 20.6 cm (5 3/16 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.258,false,true,268054,Photographs,Photograph,"Quartermaster and Ambulance Camp, Brandy Station, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.259,false,true,268055,Photographs,Photograph,"Gabions in Engineers' Camp, Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,16.3 x 20.2 cm (6 7/16 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.260,false,true,268057,Photographs,Photograph,Aqueduct near Petersburg,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.261,false,true,268058,Photographs,Photograph,"Fort Sedgwick near Petersburg, interior",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.273,false,true,268071,Photographs,Photograph,Bull Run. Bridge Near Union Mills,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,ca. 1862,1860,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.292,false,true,268092,Photographs,Photograph,"Warren Station, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.294,false,true,268094,Photographs,Photograph,"Quartermaster cargoes and transports, Pamunkey River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.327,false,true,268131,Photographs,Photograph,Appomattox River,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.329,false,true,268133,Photographs,Photograph,"Cobb's Hill, Lookout in Distance",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.330,false,true,268135,Photographs,Photograph,Appomattox River,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.332,false,true,268137,Photographs,Photograph,Warren Station near Petersburg,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.335,false,true,268140,Photographs,Photograph,Fort Sedgwick,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.338,false,true,268143,Photographs,Photograph,Fort Sedgwick in front of Petersburg,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,8.7 x 9.4 cm (3 7/16 x 3 11/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.342,false,true,268148,Photographs,Photograph,Fort Sedgwick in front of Petersburg,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,8.6 x 9.9 cm (3 3/8 x 3 7/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.346,false,true,268152,Photographs,Photograph,Fort Price,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.348,false,true,268154,Photographs,Photograph,"[Fort Sedgwick]/[Fort Price]. Brady album, p. 27",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.355,false,true,268162,Photographs,Photograph,"Signal Tower, Cobb's Hill, Appomattox River",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,10.1 x 9.9 cm (4 x 3 7/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.374,false,true,268183,Photographs,Photograph,New York Herald Headquarters,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.393,false,true,268204,Photographs,Panorama,Gettysburg from the West,,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.398,false,true,268209,Photographs,Photograph,"Signal Corps, Rappidan River/Signal Corps Reconnoitering at Fredericksburg, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland",1840 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,13.4 x 20.2 cm (5 1/4 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1131–.1151,false,true,288302,Photographs,Stereographs,[Group of 21 Stereograph Views of China],,,,,,Artist|Artist|Publisher|Publisher|Publisher|Photography Studio|Publisher|Artist|Artist|Artist|Publisher|Publisher|Editor|Artist,,"T. W. Ingersoll|Kilburn Brothers|James M. Davis|Kawin and Company|Underwood & Underwood|Sun Sculpture Works and Studios|Griffith & Griffith, American|George W. Griffith|Strohmeyer & Wyman|C. H. Graves|Universal Photo Art Co.|J. Good|B. K.|Unknown","American, active ca. 1865–1890|American|American|American|American|American|French",,"Ingersoll, T. W.|Kilburn Brothers|Davis, James M.|Kawin and Company|Underwood & Underwood|Sun Sculpture Works and Studios|Griffith & Griffith|Griffith, George W.|Strohmeyer & Wyman|Graves, C. H.|Universal Photo Art Co.|Good, J.|K., B.|Unknown",American|American|American|American|American|American|American|French,1863,1892,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.4 x 17.1 cm (3 5/16 x 6 3/4 in.) to 10 x 17.8 cm (3 15/16 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1101,false,true,631959,Photographs,Photograph,La Tour Eiffel. – Détail du Campanile.,,,,,,Photography Studio|Artist|Artist,,Neurdein Frères|Louis-Antonin Neurdein|Etienne Neurdein,"French, active Paris, 1870s–1900s|French, 1846–after 1915|French, 1832–after 1915",,"Neurdein Frères|Neurdein, Louis-Antonin|Neurdein, Etienne",French|French|French,1863 |1846 |1832,1910 |1920 |1920,1889,1889,1889,Albumen silver print,Image: 8 5/8 × 10 11/16 in. (21.9 × 27.1 cm) Mount: 12 1/2 × 14 13/16 in. (31.7 × 37.6 cm),"Gift of Stéphane Samuel and Robert Melvin Rubin, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/631959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.188.1,false,true,282759,Photographs,Photograph,"Boutique, Marché aux Halles, Paris",,,,,,Artist|Printer,,Eugène Atget|Berenice Abbott,"French, Libourne 1857–1927 Paris|American, Springfield, Ohio 1898–1991 Monson, Maine",,"Atget, Eugène|Abbott, Berenice",French|American,1857-02-02|1898,1927-08-04|1991,"1925, printed ca. 1929",1925,1925,Gelatin silver print,23.1 x 17 cm (9 1/8 x 6 11/16 in. ),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1999",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.188.2,false,true,282760,Photographs,Photograph,Fête du Trône,,,,,,Artist|Printer,,Eugène Atget|Berenice Abbott,"French, Libourne 1857–1927 Paris|American, Springfield, Ohio 1898–1991 Monson, Maine",,"Atget, Eugène|Abbott, Berenice",French|American,1857-02-02|1898,1927-08-04|1991,"1925, printed ca. 1929",1925,1925,,23.4 x 17 cm (9 3/16 x 6 11/16 in. ),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1999",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.747–.763,false,true,288230,Photographs,Stereographs,[Group of 17 Early Stereograph Views of British Churches],,,,,,Publisher|Artist|Artist|Artist|Artist|Artist|Publisher|Publisher|Artist,,Sedgfield's English Scenery|E. W. Wyatt|Mrs. Charles Lawrence|Unknown|Unknown|Francis Bedford|Catherall and Prichard|London Stereoscopic Company|Taylor,"British|British|British|British|British, London 1816–1894 London|British|British|British",,"Sedgfield's English Scenery|Wyatt, E. W.|Lawrence, Charles Mrs.|Unknown|Unknown|Bedford, Francis|Catherall and Prichard|London Stereoscopic Company|Taylor, Mr.",British|British|British|British|British|British|British,1816,1894,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.5 x 16.9 cm (3 3/8 x 6 5/8 in.) to 8.7 x 17.7 cm (3 7/16 x 6 15/16 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (9a-d),false,true,288168,Photographs,Cartes-de-visite,"[Duchesse d'Albe, Unknown Sitter, Duchesse de Morny, and Duc de Morny]",,,,,,Artist|Artist|Person in Photograph|Person in Photograph,Painted and retouched by|Person in photograph|Person in photograph,"Pierre-Louis Pierson|Marck|Sophie Troubetzkoi, duchesse de Morny|Charles-Auguste-Louis-Joseph de Morny, duc de Morny","French, 1822–1913|Russian, Moscow 1838–1896|French (born Switzerland) 1811–1865 Paris",", et al","Pierson, Pierre-Louis|Marck|Troubetzkoi, Sophie, duchesse de Morny|Morny, Charles-Auguste-Louis-Joseph de, duc de Morny","French|Russian|French, born Switzerland",1822 |1838 |1811,1913 |1896 |1865,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1202,false,true,285645,Photographs,Photograph,"Chief Officer and Clerks of the Ambulance Department, 9th Army Corps, in Front of Petersburg, Virginia",,,,,,Artist|Printer,,Timothy H. O'Sullivan|Alexander Gardner,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"O'Sullivan, Timothy H.|Gardner, Alexander","American, born Ireland|American, Scottish",1840 |1821,1882 |1882,August 1864,1864,1864,Albumen silver print from glass negative,Image: 6 7/8 × 8 7/8 in. (17.5 × 22.5 cm) Mount: 10 5/16 × 15 1/16 in. (26.2 × 38.2 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.502.1 (37),false,true,286882,Photographs,Photograph,"Field Where General Reynolds Fell, Gettysburg",,,,,,Artist|Printer,,Timothy H. O'Sullivan|Alexander Gardner,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"O'Sullivan, Timothy H.|Gardner, Alexander","American, born Ireland|American, Scottish",1840 |1821,1882 |1882,1863,1863,1863,Albumen silver print from glass negative,17.8 x 22.7 cm (7 x 8 15/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.950–.952,false,true,288285,Photographs,Stereographs,"[Group of 3 Stereograph Views of Fleet Street, London, England]",,,,,,Publisher|Artist|Artist|Artist|Publisher,,Underwood & Underwood|J. F. Jarvis|Benneville Lloyd Singley|Unknown|Keystone View Company,"American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania",,"Underwood & Underwood|Jarvis, J. F.|Singley, Benneville Lloyd|Unknown|Keystone View Company",American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1133,false,true,286099,Photographs,Photograph,"Fortifications, Manassas",,,,,,Artist|Artist|Artist|Publisher,,Barnard & Gibson|George N. Barnard|James F. Gibson|Brady & Co.,"American, active 1860s|American, 1819–1902|American, born 1828|American, active 1840s–1880s",,"Barnard & Gibson|Barnard, George N.|Gibson, James F.|Brady & Co.",American|American|American|American,1860 |1819 |1828 |1840,1870 |1902 |1928 |1889,March 1862,1862,1862,Albumen silver print from glass negative,Image: 18.7 × 23.4 cm (7 3/8 × 9 3/16 in.) Mount: 27.1 × 34.6 cm (10 11/16 × 13 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.22,false,true,283094,Photographs,Photograph,"Flora Rankin, Irene MacDonald, and Mary Josephine MacDonald at Elm Lodge",,,,,,Artist|Subject|Subject|Subject,,Lewis Carroll|Flora Rankin|Irene MacDonald|Mary Josephine MacDonald,"British, Daresbury, Cheshire 1832–1898 Guildford|British|British, born 1857|British, 1853–1878",,"Carroll, Lewis|Rankin, Flora|MacDonald, Irene|MacDonald, Mary Josephine",British|British|British|British,1832 |1857 |1853,1898 |1878,July 1863,1863,1863,Albumen silver print from glass negative,22.2 x 18 cm (8 3/4 x 7 1/16 in.),"Gilman Collection, Purchase, Joyce F. Menschel Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.25,false,true,268045,Photographs,Photograph,"Belle Plain, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,James Gardner|Timothy H. O'Sullivan|Mathew B. Brady,"American, born 1832|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Gardner, James|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1832 |1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.139,false,true,267923,Photographs,Photograph,"Belle Plain, Virginia. Potomac River, Upper Wharf",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,James Gardner|Timothy H. O'Sullivan|Mathew B. Brady,"American, born 1832|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Gardner, James|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1832 |1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.165,false,true,267952,Photographs,Photograph,"Belle Plain, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,James Gardner|Timothy H. O'Sullivan|Mathew B. Brady,"American, born 1832|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Gardner, James|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1832 |1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.200,false,true,267992,Photographs,Photograph,"Confederate Earthworks, Belle Plain, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,James Gardner|Timothy H. O'Sullivan|Mathew B. Brady,"American, born 1832|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Gardner, James|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1832 |1840 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.202,false,true,267994,Photographs,Photograph,"Belle Plain, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,James Gardner|Timothy H. O'Sullivan|Mathew B. Brady,"American, born 1832|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Gardner, James|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1832 |1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.255,false,true,268051,Photographs,Photograph,Camp near Brandy Station,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,James Gardner|Timothy H. O'Sullivan|Mathew B. Brady,"American, born 1832|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Gardner, James|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1832 |1840 |1823,1882 |1896,1863–64,1863,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.184,false,true,267973,Photographs,Photograph,"Belle Plain, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|James Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born 1832|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, James|Brady, Mathew B.","American, born Ireland|American",1840 |1832 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.203,false,true,267995,Photographs,Photograph,"Belle Plain, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|James Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born 1832|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, James|Brady, Mathew B.","American, born Ireland|American",1840 |1832 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.204,false,true,267996,Photographs,Photograph,Belle Plain. Distant View of Landing,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|James Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born 1832|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, James|Brady, Mathew B.","American, born Ireland|American",1840 |1832 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.257,false,true,268053,Photographs,Photograph,"Brandy Station, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|James Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born 1832|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, James|Brady, Mathew B.","American, born Ireland|American",1840 |1832 |1823,1882 |1896,1863–64,1863,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.313,false,true,268116,Photographs,Photograph,"Belle Plain, Virginia. Lower Wharf",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|James Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born 1832|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, James|Brady, Mathew B.","American, born Ireland|American",1840 |1832 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1230–.1240,false,true,288308,Photographs,Stereographs,[Group of 11 Stereograph Views of Cowboys],,,,,,Publisher|Publisher|Artist|Photography Studio|Artist|Publisher,,European and American Views|Underwood & Underwood|Benneville Lloyd Singley|Sun Sculpture Works and Studios|Unknown|Keystone View Company,"American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American",,"European and American Views|Underwood & Underwood|Singley, Benneville Lloyd|Sun Sculpture Works and Studios|Unknown|Keystone View Company",American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 3.9 x 17.8 cm (1 9/16 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.22,false,true,268013,Photographs,Photograph,"Ruins of Richmond & Petersburg Railroad Bridge, Richmond, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,Image: 16.3 × 21.4 cm (6 7/16 × 8 7/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.57,false,true,268249,Photographs,Photograph,"St. Peter's Church near White House, Where Washington was Married. General E. V. Sumner and Staff",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268249,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.211,false,true,268004,Photographs,Photograph,"Custom House, Richmond, Virginia (after evacuation)",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.212,false,true,268005,Photographs,Photograph,"Sanitary Commission Headquarters, Richmond, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.213,false,true,268006,Photographs,Photograph,"Richmond, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.214,false,true,268007,Photographs,Photograph,"Penitentiary, Richmond",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.215,false,true,268008,Photographs,Photograph,"Castle Thunder, ex-tobacco factory, Petersburg",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1864,1864,1864,Albumen silver print from glass negative,15.9 x 20.4 cm (6 1/4 x 8 1/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.216,false,true,268009,Photographs,Photograph,"Ruins near Canal Basin, Richmond",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,15.8 x 21 cm (6 1/4 x 8 1/4 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.219,false,true,268012,Photographs,Photograph,"Smokestack of Confederate Ram Merrimac at Richmond/Remains of Ironclad Ram ""Virginia #2"", April, 1865",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.220,false,true,268014,Photographs,Photograph,Richmond after the Evacuation,,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.221,false,true,268015,Photographs,Photograph,"Richmond, Virginia, after Evacuation",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.222,false,true,268016,Photographs,Photograph,"Wharves at Richmond, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.223,false,true,268017,Photographs,Photograph,"Ruins of Arsenal, Richmond, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.224,false,true,268018,Photographs,Photograph,"Ruins on North Bank of Canal, Richmond",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.225,false,true,268019,Photographs,Photograph,"Ruins of R & P Railroad Bridge, Richmond",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.227,false,true,268020,Photographs,Photograph,"Ruins at end of Richmond and Petersburg Railroad Bridge, Richmond",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.229,false,true,268022,Photographs,Photograph,"Richmond, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,17 x 21.1 cm (6 11/16 x 8 5/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.230,false,true,268024,Photographs,Photograph,"Jeff. Davis House, Executive Mansion, C.S.A., Richmond",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.231,false,true,268025,Photographs,Photograph,"Richmond, Virginia. Looking toward Manchester",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.287,false,true,268086,Photographs,Photograph,Headquarters of General Hooker,,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.368,false,true,268176,Photographs,Photograph,Aquia Creek Landing,,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"33.65.11, .226",true,true,267891,Photographs,Panorama,"Ruins of Gallego Flour Mills, Richmond",,,,,,Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|Brady, Mathew B.","American, Scottish",1821 |1823,1882 |1896,1865,1865,1865,Albumen silver prints from glass negatives,16.3 x 36.9 cm (6 7/16 x 14 1/2 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.123,false,true,267906,Photographs,Photograph,Bull Run. Orange and Alexandria R.R. near Union Mills,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,George N. Barnard|Timothy H. O'Sullivan|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1819 |1840 |1823,1902 |1882 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.272,false,true,268070,Photographs,Photograph,"Bull Run, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,George N. Barnard|Timothy H. O'Sullivan|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1819 |1840 |1823,1902 |1882 |1896,1861–62,1861,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.159,false,true,267945,Photographs,Photograph,"Crow's Nest, Battery and Lookout",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Timothy H. O'Sullivan|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American, born Ireland",1830 |1840 |1823,1902 |1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.107,false,true,267888,Photographs,Photograph,Bull Run. Orange and Alexandria R.R. near Union Mills,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|George N. Barnard|Mathew B. Brady,"American, born Ireland, 1840–1882|American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Barnard, George N.|Brady, Mathew B.","American, born Ireland|American",1840 |1819 |1823,1882 |1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.108,false,true,267889,Photographs,Photograph,Bull Run. The Stone Bridge,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|George N. Barnard|Mathew B. Brady,"American, born Ireland, 1840–1882|American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Barnard, George N.|Brady, Mathew B.","American, born Ireland|American",1840 |1819 |1823,1882 |1902 |1896,1861–62,1861,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.134,false,true,267918,Photographs,Photograph,Bull Run. Pontoon Bridge near Blackburn's Ford,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|George N. Barnard|Mathew B. Brady,"American, born Ireland, 1840–1882|American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Barnard, George N.|Brady, Mathew B.","American, born Ireland|American",1840 |1819 |1823,1882 |1902 |1896,1862,1862,1862,Albumen silver print from glass negative,13.1 x 20.2 cm (5 3/16 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.365,false,true,268173,Photographs,Photograph,"Confederate Fortifications, Petersburg",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Andrew Joseph Russell|Mathew B. Brady,"American, born Ireland, 1840–1882|American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Russell, Andrew Joseph|Brady, Mathew B.","American, born Ireland|American",1840 |1830 |1823,1882 |1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.308–.311,false,true,288136,Photographs,Stereographs,[Group of 4 Stereograph Views of Berlin Beer Gardens],,,,,,Publisher|Publisher|Artist|Publisher|Artist|Artist|Publisher,,Strohmeyer & Wyman|Underwood & Underwood|Bert Underwood|Moser Senior|Benneville Lloyd Singley|Unknown|Keystone View Company,"American|American|American|German|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania",,"Strohmeyer & Wyman|Underwood & Underwood|Underwood, Bert|Moser Senior|Singley, Benneville Lloyd|Unknown|Keystone View Company",American|American|American|German|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.401–.407,false,true,288140,Photographs,Stereographs,"[Group of 7 Stereograph Views of the Forth Bridge, Queensferry, Scotland]",,,,,,Publisher|Artist|Publisher|Artist|Photography Studio|Artist|Publisher,,International Stereoscopic View Company|Strohmeyer & Wyman|Underwood & Underwood|Benneville Lloyd Singley|Sun Sculpture Works and Studios|Unknown|Keystone View Company,"American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American",,"International Stereoscopic View Company|Strohmeyer & Wyman|Underwood & Underwood|Singley, Benneville Lloyd|Sun Sculpture Works and Studios|Unknown|Keystone View Company",American|American|American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.917–.920,false,true,288272,Photographs,Stereographs,[Group of 4 Stereograph Views of London Bridges],,,,,,Publisher|Artist|Publisher|Publisher|Artist|Artist|Artist|Publisher,,"Griffith & Griffith, American|George W. Griffith|Strohmeyer & Wyman|Underwood & Underwood|Benneville Lloyd Singley|J. F. Jarvis|Unknown|Keystone View Company","American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American",,"Griffith & Griffith|Griffith, George W.|Strohmeyer & Wyman|Underwood & Underwood|Singley, Benneville Lloyd|Jarvis, J. F.|Unknown|Keystone View Company",American|American|American|American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.5,false,true,306206,Photographs,Photograph,"Edith, Ina and Alice Liddell on a Sofa",,,,,,Artist|Person in Photograph|Person in Photograph|Person in Photograph,Person in photograph|Person in photograph|Person in photograph,Lewis Carroll|Alice Pleasance Liddell|Edith Mary Liddell|Ina Liddell,"British, Daresbury, Cheshire 1832–1898 Guildford|British, 1852–1934|British, 1854–1876|British, 1849–1930",,"Carroll, Lewis|Liddell, Alice|Liddell, Edith Mary|Liddell, Ina",British|British|British|British,1832 |1852 |1854 |1849,1898 |1934 |1876 |1930,Summer 1858,1858,1858,Albumen silver print from glass negative,Mount: 4 3/16 in. × 6 7/16 in. (10.7 × 16.3 cm) Image: 4 1/16 × 6 1/16 in. (10.3 × 15.4 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.627.1,false,true,261925,Photographs,Photograph,Abraham Lincoln,,,,,,Artist|Printer|Person in Photograph,,Alexander Gardner|Rice|Abraham Lincoln,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, Hardin County, Kentucky 1809–1865 Washington, D.C.",,"Gardner, Alexander|Rice|Lincoln, Abraham","American, Scottish|American",1821 |1809,1882 |1865,"1863, printed 1901",1863,1863,Gelatin silver print,Image: 45.7 x 38.1 cm (18 x 15 in.),"Warner Communications Inc. Purchase Fund, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.580.4.4,false,true,288741,Photographs,Carte-de-visite,[Napoleon III and Empress Eugenie],,,,,,Artist|Person in Photograph|Person in Photograph,,André-Adolphe-Eugène Disdéri|Charles-Louis-Napoleon Bonaparte|Empress Eugénie de Montijo,"French, Paris 1819–1889 Paris|French, Paris 1808–1873 Chislehurst, Kent|French (born Spain), Granada 1826–1920 Madrid",,"Disdéri, André-Adolphe-Eugène|Bonaparte, Charles-Louis-Napoleon|Montijo, Eugénie de, Empress","French|French|French, born Spain",1819 |1808 |1826,1889 |1873 |1920,ca. 1865,1865,1865,Albumen silver print,Image: 9.5 x 6 cm (3 3/4 x 2 3/8 in.),"Gift of A. Hyatt Mayor, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.228,false,true,268021,Photographs,Photograph,"Ruins in Carey Street, Richmond",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Thomas C. Roche|Alexander Gardner|Mathew B. Brady,"American, 1826–1895|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Roche, Thomas C.|Gardner, Alexander|Brady, Mathew B.","American|American, Scottish",1826 |1821 |1823,1895 |1882 |1896,1865,1865,1865,Albumen silver print from glass negative,Image: 18.6 x 23.8 cm (7 5/16 x 9 3/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.396,false,true,268207,Photographs,Photograph,"Falmouth, Virginia. Abandoned Camp",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Alexander Gardner|Mathew B. Brady,"American, 1830–1902|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Gardner, Alexander|Brady, Mathew B.","American|American, Scottish",1830 |1821 |1823,1902 |1882 |1896,1862,1862,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.167,false,true,267954,Photographs,Photograph,Bull Run,,,,,,Artist|Artist|Artist|Former Attribution,Formerly attributed to,George N. Barnard|Timothy H. O'Sullivan|Andrew Joseph Russell|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1840–1882|American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|O'Sullivan, Timothy H.|Russell, Andrew Joseph|Brady, Mathew B.","American|American, born Ireland|American",1819 |1840 |1830 |1823,1902 |1882 |1902 |1896,1861–62,1861,1862,Albumen silver print from glass negative,10.2 x 20 cm (4 x 7 7/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.133,false,true,267917,Photographs,Photograph,Bull Run,,,,,,Artist|Artist|Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|George N. Barnard|Timothy H. O'Sullivan|Mathew B. Brady,"American, 1830–1902|American, 1819–1902|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Barnard, George N.|O'Sullivan, Timothy H.|Brady, Mathew B.","American|American|American, born Ireland",1830 |1819 |1840 |1823,1902 |1902 |1882 |1896,1861–62,1861,1862,Albumen silver print from glass negative,16.1 x 20.7 cm (6 5/16 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.109,false,true,267890,Photographs,Photograph,Bull Run,,,,,,Artist|Artist|Artist|Former Attribution,,Andrew Joseph Russell|Timothy H. O'Sullivan|George N. Barnard|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1840–1882|American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|O'Sullivan, Timothy H.|Barnard, George N.|Brady, Mathew B.","American|American, born Ireland|American",1830 |1840 |1819 |1823,1902 |1882 |1902 |1896,1861–62,1861,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.20,false,true,267991,Photographs,Photograph,Wharf opposite Richmond,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,John Reekie|Alexander Gardner|Mathew B. Brady,"American, active 1860s|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"Reekie, John|Gardner, Alexander|Brady, Mathew B.","American|American, Scottish",1860 |1821 |1823,1869 |1882 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.15–.57,false,true,288104,Photographs,Stereographs,[Group of 42 Stereograph Views of Alaska Including the Gold Rush],,,,,,Publisher|Artist|Publisher|Artist|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Publisher,,"George W. Griffith|Benneville Lloyd Singley|Berry, Kelley & Chadwick|Griffith & Griffith, American|Unknown|T. W. Ingersoll|William H. Rau|Universal View Co.|C. H. Graves|Universal Photo Art Co.|Keystone View Company","American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American, 1855–1920|American|American",,"Griffith, George W.|Singley, Benneville Lloyd|Berry, Kelley & Chadwick|Griffith & Griffith|Unknown|Ingersoll, T. W.|Rau, William H.|Universal View Co.|Graves, C. H.|Universal Photo Art Co.|Keystone View Company",American|American|American|American|American|American|American,1864 |1855,1938 |1920,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1285–.1534,false,true,296355,Photographs,Stereographs,"[Group of 250 Stereograph Views From the London Stereoscopic Company, 1860-1870, Many Hand-Colored to Illustrate Books]",,,,,,Publisher|Publisher|Publisher|Artist|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Artist|Artist|Publisher|Publisher|Publisher|Artist|Artist|Publisher|Person in Photograph|Publisher|Publisher|Publisher|Printer|Publisher|Publisher|Publisher|Publisher|Author|Artist|Person in Photograph|Publisher|Publisher,Collaborated with,"New York Stereoscopic Company|L. H. Stockwell|London Stereoscopic Company|Unknown|Unknown|G. W. Thorne|Unknown|M. W. S. Jackson|J. L. Bates|Mrs. Charles Lawrence|F. W. & R. King|Benneville Lloyd Singley|Sir David Brewster|Littleton View Company|Underwood & Underwood|William Hall & Son|C. E. Goodman|J. Elliott|London Stereoscopic and Photographic Company|Henry IV, the Pius, Duke of Saxony|G. Hawgood|J. Eastlake|E. H. Chamberlain|Nachmann|E. Vimard|Gebhardt, Rottmann, & Co.|B. B. Savary|L. J. Cist|William Hepworth Dixon|M. Laroche|Charles-Louis-Napoleon Bonaparte|Keystone View Company|McAllister & Brother","American|American|British|British|American|American|American|American|British|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|British, Jedburgh, Scotland 1781–1868 Melrose|American|American|New York|British|German, 1473–1541|American|French|French|British|American|British, 1821–1879|French, Paris 1808–1873 Chislehurst, Kent|American, active 1860s–1870s",,"New York Stereoscopic Company|Stockwell, L. H.|London Stereoscopic Company|Unknown|Unknown|Thorne, G. W.|Unknown|Jackson, M. W. S.|Bates, J. L.|Lawrence, Charles Mrs.|F. W. & R. King|Singley, Benneville Lloyd|Brewster, David, Sir|Littleton View Company|Underwood & Underwood|William Hall & Son|Goodman, C. E.|Elliott, J.|London Stereoscopic and Photographic Company|Duke of Saxony Henry IV, the Pius,|Hawgood, G.|Eastlake, J.|Chamberlain, E. H.|Nachmann|Vimard, E.|Gebhardt, Rottmann, & Co.|Savary, B. B.|Cist, L. J.|Dixon, William Hepworth|Laroche, M.|Bonaparte, Charles-Louis-Napoleon|Keystone View Company|McAllister & Brother","American|American|British|American|American|American|British|American|British, Scottish|American|American|American|British|German|American|French|French|British|American|British|French|American",1864 |1781 |1473 |1821 |1808 |1850,1938 |1868 |1541 |1879 |1873 |1880,1860–70,1860,1870,Albumen silver prints,Mounts approximately: 8.6 x 17.5 cm (3 3/8 x 6 7/8 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1242–.1283,false,true,288310,Photographs,Stereographs,"[Group of 42 Stereograph Views From the London Stereoscopic Company, 1860-1870, Many Hand-Colored to Illustrate Books]",,,,,,Publisher|Artist|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Artist|Artist|Publisher|Publisher|Publisher|Artist|Artist|Publisher|Person in Photograph|Publisher|Publisher|Publisher|Printer|Publisher|Publisher|Publisher|Publisher|Author|Artist|Publisher|Publisher|Person in Photograph|Publisher|Publisher,Collaborated with,"London Stereoscopic Company|Unknown|Unknown|G. W. Thorne|Unknown|M. W. S. Jackson|J. L. Bates|Mrs. Charles Lawrence|F. W. & R. King|Benneville Lloyd Singley|Sir David Brewster|Littleton View Company|Underwood & Underwood|William Hall & Son|C. E. Goodman|J. Elliott|London Stereoscopic and Photographic Company|Henry IV, the Pius, Duke of Saxony|G. Hawgood|J. Eastlake|E. H. Chamberlain|Nachmann|E. Vimard|Gebhardt, Rottmann, & Co.|B. B. Savary|L. J. Cist|William Hepworth Dixon|M. Laroche|New York Stereoscopic Company|L. H. Stockwell|Charles-Louis-Napoleon Bonaparte|Keystone View Company|McAllister & Brother","British|British|American|American|American|American|British|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|British, Jedburgh, Scotland 1781–1868 Melrose|American|American|New York|British|German, 1473–1541|American|French|French|British|American|British, 1821–1879|American|American|French, Paris 1808–1873 Chislehurst, Kent|American, active 1860s–1870s",,"London Stereoscopic Company|Unknown|Unknown|Thorne, G. W.|Unknown|Jackson, M. W. S.|Bates, J. L.|Lawrence, Charles Mrs.|F. W. & R. King|Singley, Benneville Lloyd|Brewster, David, Sir|Littleton View Company|Underwood & Underwood|William Hall & Son|Goodman, C. E.|Elliott, J.|London Stereoscopic and Photographic Company|Duke of Saxony Henry IV, the Pius,|Hawgood, G.|Eastlake, J.|Chamberlain, E. H.|Nachmann|Vimard, E.|Gebhardt, Rottmann, & Co.|Savary, B. B.|Cist, L. J.|Dixon, William Hepworth|Laroche, M.|New York Stereoscopic Company|Stockwell, L. H.|Bonaparte, Charles-Louis-Napoleon|Keystone View Company|McAllister & Brother","British|American|American|American|British|American|British, Scottish|American|American|American|British|German|American|French|French|British|American|British|American|American|French|American",1864 |1781 |1473 |1821 |1808 |1850,1938 |1868 |1541 |1879 |1873 |1880,1860–70,1860,1870,Albumen silver prints,Mounts approximately: 8.6 x 17.5 cm (3 3/8 x 6 7/8 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.4,false,true,302667,Photographs,Carte-de-visite,Hamilton's Floating Battery Moored at the End of Sullivan's Island the Night Before They Opened Fire upon Fort Sumter,,,,,,Publisher|Artist|Artist,Attributed to|Attributed to,Edward Anthony|Alma A. Pelot|Jesse H. Bolles,"American, 1818–1888|American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Corlina, 1850s–1860s",,"Anthony, Edward|Pelot, Alma A.|Bolles, Jesse H.",American|American|American,1818 |1850 |1850,1888 |1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.7,false,true,302670,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist|Artist,Attributed to|Attributed to,Edward Anthony|Alma A. Pelot|Jesse H. Bolles,"American, 1818–1888|American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Corlina, 1850s–1860s",,"Anthony, Edward|Pelot, Alma A.|Bolles, Jesse H.",American|American|American,1818 |1850 |1850,1888 |1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 2 11/16 in. (5 × 6.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.8,false,true,302671,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist|Artist,Attributed to|Attrobuted to,Edward Anthony|Alma A. Pelot|Jesse H. Bolles,"American, 1818–1888|American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Corlina, 1850s–1860s",,"Anthony, Edward|Pelot, Alma A.|Bolles, Jesse H.",American|American|American,1818 |1850 |1850,1888 |1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.9,false,true,302672,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist|Artist,Attributed to|Attrobuted to,Edward Anthony|Alma A. Pelot|Jesse H. Bolles,"American, 1818–1888|American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Corlina, 1850s–1860s",,"Anthony, Edward|Pelot, Alma A.|Bolles, Jesse H.",American|American|American,1818 |1850 |1850,1888 |1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.11,false,true,302674,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist|Artist,Attributed to|Attributed to,Edward Anthony|Alma A. Pelot|Jesse H. Bolles,"American, 1818–1888|American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Corlina, 1850s–1860s",,"Anthony, Edward|Pelot, Alma A.|Bolles, Jesse H.",American|American|American,1818 |1850 |1850,1888 |1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.13,false,true,302676,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist|Artist,Attributed to|Attributed to,Edward Anthony|Alma A. Pelot|Jesse H. Bolles,"American, 1818–1888|American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Corlina, 1850s–1860s",,"Anthony, Edward|Pelot, Alma A.|Bolles, Jesse H.",American|American|American,1818 |1850 |1850,1888 |1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.151,false,true,267937,Photographs,Photograph,Petersburg,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Alexander Gardner|Timothy H. O'Sullivan|Mathew B. Brady,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"Gardner, Alexander|O'Sullivan, Timothy H.|Brady, Mathew B.","American, Scottish|American, born Ireland",1821 |1840 |1823,1882 |1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.150,false,true,267936,Photographs,Photograph,Petersburg,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Alexander Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, Alexander|Brady, Mathew B.","American, born Ireland|American, Scottish",1840 |1821 |1823,1882 |1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.152,false,true,267938,Photographs,Photograph,Petersburg,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Alexander Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, Alexander|Brady, Mathew B.","American, born Ireland|American, Scottish",1840 |1821 |1823,1882 |1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.153,false,true,267939,Photographs,Photograph,Petersburg,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Timothy H. O'Sullivan|Alexander Gardner|Mathew B. Brady,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Gardner, Alexander|Brady, Mathew B.","American, born Ireland|American, Scottish",1840 |1821 |1823,1882 |1882 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.946–.949,false,true,288283,Photographs,Stereographs,"[Group of 4 Stereograph Views of the Coronation of Edward VII, London, England]",,,,,,Publisher|Artist|Publisher|Publisher|Photography Studio|Artist|Person in Photograph|Publisher,,"H. C. White Company|R. Y. Young|American Stereoscopic Company|Underwood & Underwood|Sun Sculpture Works and Studios|Benneville Lloyd Singley|Edward VII, King of Great Britain and Northern Ireland|Keystone View Company","American|American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|British, London 1841–1910 London",,"H. C. White Company|Young, R. Y.|American Stereoscopic Company|Underwood & Underwood|Sun Sculpture Works and Studios|Singley, Benneville Lloyd|Edward VII, King of Great Britain and Northern Ireland|Keystone View Company",American|American|American|American|American|British,1864 |1841,1938 |1910,1850s–1910s,1850,1919,Albumen silver prints,Mounts: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1003–.1030,false,true,288296,Photographs,Stereographs,[Group of 28 Stereograph Views of Children],,,,,,Publisher|Artist|Publisher|Publisher|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Artist|Artist,,Keystone View Company|Benneville Lloyd Singley|Merrimac Stereoscopic Company|A. Fuller|The Globe Photo Art Company|C. W. Woodward|Life Groups|Popular Series|International Stereoscopic View Company|F. G. Weller|Underwood & Underwood|Littleton View Company|Canvassers|Unknown|Unknown,"American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American|American|American|American|American|American|American",,"Keystone View Company|Singley, Benneville Lloyd|Merrimac Stereoscopic Company|Fuller, A.|Globe Photo Art Company|Woodward, C. W.|Life Groups|Popular Series|International Stereoscopic View Company|Weller., F. G.|Underwood & Underwood|Littleton View Company|Canvassers|Unknown|Unknown",American|American|American|American|American|American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1188–.1224,false,true,288305,Photographs,Stereographs,"[Group of 37 Stereograph Views of the Garden of the Gods and Other Colorado Scenery, United States of America]",,,,,,Publisher|Publisher|Photography Studio|Publisher|Publisher|Artist|Artist|Artist|Publisher|Publisher|Publisher|Artist|Publisher,,"H. C. White Company|Underwood & Underwood|Sun Sculpture Works and Studios|Berry, Kelley & Chadwick|American Colotype Company|J. F. Jarvis|Strohmeyer & Wyman|Benneville Lloyd Singley|Griffith & Griffith, American|Ingersoll View Company|Canvassers|Unknown|Keystone View Company","American|American|American|American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American",,"H. C. White Company|Underwood & Underwood|Sun Sculpture Works and Studios|Berry, Kelley & Chadwick|American Colotype Company|Jarvis, J. F.|Strohmeyer & Wyman|Singley, Benneville Lloyd|Griffith & Griffith|Ingersoll View Company|Canvassers|Unknown|Keystone View Company",American|American|American|American|American|American|American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,,"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.465–.490,false,true,288147,Photographs,Stereographs,"[Group of 26 Stereograph Views of San Francisco, California]",,,,,,Publisher|Publisher|Publisher|Publisher|Publisher|Artist|Publisher|Publisher|Photography Studio|Publisher|Artist|Artist|Publisher,,Continent Stereoscopic Company|American Scenery|Popular Series|American Series|American Views|Benneville Lloyd Singley|Standard Series|Underwood & Underwood|Sun Sculpture Works and Studios|Stereoscopic Views|Unknown|Unknown|Keystone View Company,"New York|American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American|American|American",,"Continent Stereoscopic Company|American Scenery|Popular Series|American Series|American Views|Singley, Benneville Lloyd|Standard Series|Underwood & Underwood|Sun Sculpture Works and Studios|Stereoscopic Views|Unknown|Unknown|Keystone View Company",New York|American|American|American|American|American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.6 cm (3 1/2 x 6 15/16 in.) to 10.5 x 17.8 cm (4 1/8 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1201,false,true,285644,Photographs,Photograph,"A Harvest of Death, Gettysburg, Pennsylvania",,,,,,Artist|Printer|Publisher,,Timothy H. O'Sullivan|Alexander Gardner|Alexander Gardner,"American, born Ireland, 1840–1882|American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"O'Sullivan, Timothy H.|Gardner, Alexander|Gardner, Alexander","American, born Ireland|American, Scottish|American, Scottish",1840 |1821 |1821,1882 |1882 |1882,July 1863,1863,1863,Albumen silver print from glass negative,Image: 17 13/16 × 22 1/2 in. (45.2 × 57.2 cm) Mount: 11 15/16 × 15 5/8 in. (30.4 × 39.7 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.21,false,true,268962,Photographs,Photograph,"William Etty, R.A.",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.37,false,true,268791,Photographs,Photograph,Sir William Allan,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.38,false,true,268792,Photographs,Photograph,"Ogilvie Fairly, Capt. Hamilton, and Gilmore",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.40,false,true,268795,Photographs,Photograph,Mrs. Rigby,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.41,false,true,268796,Photographs,Photograph,Mr. and Mrs. John Thomson,,,,,,Artist|Printer|Artist|Photography Studio,,David Octavius Hill|Ingals|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Ingals|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.42,false,true,268797,Photographs,Photograph,Henning with Parthenon Frieze,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.43,false,true,268798,Photographs,Photograph,"[Man, Full-length]",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.44,false,true,268799,Photographs,Photograph,Newhaven Fishwives,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.46,false,true,268801,Photographs,Photograph,Dr. Jabez Bunting,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.47,false,true,268802,Photographs,Photograph,Hugh Miller,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.49,false,true,268803,Photographs,Photograph,Presbytery of Dumbarton,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.50,false,true,268805,Photographs,Photograph,Rev. Dr. Thomas Chalmers (?),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.51,false,true,268806,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.53,false,true,268808,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.10.54,false,true,268809,Photographs,Photograph,"Prof. Fraser, Rev. Welsh, Rev. Hamilton, and Three Other Men",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Gift of Mrs. Pirie MacDonald and Mr. and Mrs. Everett Tutchings, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.141,false,true,268914,Photographs,Photograph,Kenneth Macleay,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.142,false,true,268915,Photographs,Photograph,Sobieski Stuart,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.143,false,true,268916,Photographs,Photograph,David Roberts,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.153,false,true,268927,Photographs,Photograph,"Kenneth MaCleay, R.S.A.",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.162,false,true,268937,Photographs,Photograph,James Glencairn Burns,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.163,false,true,268938,Photographs,Photograph,Finlay - The Deerstalker,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.165,false,true,268940,Photographs,Photograph,John Ban MacKenzie,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.166,false,true,268941,Photographs,Photograph,Alexander Thompson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.169,false,true,268944,Photographs,Photograph,Sir John McNeill,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.171,false,true,268947,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.172,false,true,268948,Photographs,Photograph,George Moon,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.173,false,true,268949,Photographs,Photograph,Sobieski Stuart,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.174,false,true,268950,Photographs,Photograph,"Dr. Inglis, Halifax",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.1,false,true,268366,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.2,false,true,268393,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.3,false,true,268404,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.4,false,true,268415,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.5,false,true,268426,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.6,false,true,268437,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.7,false,true,268448,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.8,false,true,268459,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.9,false,true,268470,Photographs,Photograph,St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.1,false,true,268482,Photographs,Photograph,Rev. Dr. Thomas Chalmers and Thomas Chalmers Hanna,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.2,false,true,268525,Photographs,Photograph,"William Borthwick Johnstone, William Leighton Leitch and David Scott as ""The Monks of Kennaquhair"" from Sir Walter Scott's ""The Abbott""",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.3,false,true,268536,Photographs,Photograph,Dunlop Esq. of Craigton,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.4,false,true,268547,Photographs,Photograph,"William Etty, R.A.",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.5,false,true,268558,Photographs,Photograph,William Etty,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.6,false,true,268569,Photographs,Photograph,"Sir William Allan, P.R.S.A.",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.7,false,true,268580,Photographs,Photograph,"Thomas Duncan, R.S.A.",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.8,false,true,268591,Photographs,Photograph,Thomas Duncan,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.9,false,true,268602,Photographs,Photograph,Thomas Duncan and His Brother,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.10,false,true,268367,Photographs,Photograph,St. Andrews. The Harbor,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.11,false,true,268378,Photographs,Photograph,St. Andrews. College Church of St. Salvator,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.12,false,true,268385,Photographs,Photograph,St. Andrews. The College Church of St. Salvator,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.13,false,true,268386,Photographs,Photograph,St. Andrews. The Pends,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.14,false,true,268387,Photographs,Photograph,St. Andrews. The Pends,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.15,false,true,268388,Photographs,Photograph,St. Andrews. The West Port,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.16,false,true,268389,Photographs,Photograph,St. Andrews. The Abbey Wall,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.17,false,true,268390,Photographs,Photograph,St. Andrews. The Fore Tower of the Castle,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.18,false,true,268391,Photographs,Photograph,St. Andrews. The Fore Tower of the Castle,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.19,false,true,268392,Photographs,Photograph,St. Andrews. The Fore Tower of the Castle,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.20,false,true,268394,Photographs,Photograph,St. Andrews. The Spindle Rock,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.21,false,true,268395,Photographs,Photograph,St. Andrews. College Church of St. Salvator,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.22,false,true,268396,Photographs,Photograph,St. Andrews. Blackfriars' Chapel,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.23,false,true,268397,Photographs,Photograph,St. Andrews Cathedral,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.24,false,true,268398,Photographs,Photograph,St. Andrews (?). Ships in the Harbor,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.25,false,true,268399,Photographs,Photograph,St. Andrews. Madras College,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.26,false,true,268400,Photographs,Photograph,Edinburgh. The High Street with John Knox's House,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.27,false,true,268401,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.28,false,true,268402,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.29,false,true,268403,Photographs,Photograph,Edinburgh. The Orphan Hospital,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.30,false,true,268405,Photographs,Photograph,Edinburgh. The Scott Monument,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.31,false,true,268406,Photographs,Photograph,Edinburgh. The Scott Monument,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.32,false,true,268407,Photographs,Photograph,Edinburgh. The Royal High School,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.33,false,true,268408,Photographs,Photograph,"[Old Royal High School, Calton Hill, Edinburgh]",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.34,false,true,268409,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.35,false,true,268410,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268410,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.36,false,true,268411,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.37,false,true,268412,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.38,false,true,268413,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.39,false,true,268414,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.40,false,true,268416,Photographs,Photograph,Edinburgh. Greyfriar's Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.41,false,true,268417,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.42,false,true,268418,Photographs,Photograph,Edinburgh. Greyfriars' Churchyard,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.43,false,true,268419,Photographs,Photograph,Bonaly Towers,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.44,false,true,268420,Photographs,Photograph,Bonaly Towers. Home of Lord Cockburn,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.45,false,true,268421,Photographs,Photograph,Bonaly Towers. Home of Lord Cockburn,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.46,false,true,268422,Photographs,Photograph,"John Henning as Edie Ochiltree from Sir Walter Scott's ""The Antiquary""",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.47,false,true,268423,Photographs,Photograph,"Burnside, Fife / Island in the Almond River",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.48,false,true,268424,Photographs,Photograph,Tree at Colinton,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.49,false,true,268425,Photographs,Photograph,Tree at Colinton,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.50,false,true,268427,Photographs,Photograph,Lindlithgow Castle,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.51,false,true,268428,Photographs,Photograph,Newhaven,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.52,false,true,268429,Photographs,Photograph,"St. Andrews. North Street, Fishergate",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.53,false,true,268430,Photographs,Photograph,Newhaven Fishwives,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.54,false,true,268431,Photographs,Photograph,Newhaven Children,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.55,false,true,268432,Photographs,Photograph,Newhaven Family,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.56,false,true,268433,Photographs,Photograph,Newhaven Group,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.57,false,true,268434,Photographs,Photograph,Newhaven Boys,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.58,false,true,268435,Photographs,Photograph,Newhaven Fishwife,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.59,false,true,268436,Photographs,Photograph,Newhaven Fishwives,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.60,false,true,268438,Photographs,Photograph,Newhaven Fishwives,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.61,false,true,268439,Photographs,Photograph,Newhaven Fishwives,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.63,false,true,268441,Photographs,Photograph,Newhaven Fishwife,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.64,false,true,268442,Photographs,Photograph,Fisher Lassies,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.65,false,true,268443,Photographs,Photograph,Newhaven Fishwife,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.66,false,true,268444,Photographs,Photograph,Newhaven Fishwife,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.67,false,true,268445,Photographs,Photograph,Newhaven Group,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.68,false,true,268446,Photographs,Photograph,Newhaven Fishwives,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.69,false,true,268447,Photographs,Photograph,Newhaven Fishwife,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.70,false,true,268449,Photographs,Photograph,Newhaven Fisherman with Two Boys,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.71,false,true,268450,Photographs,Photograph,Newhaven Fisherman,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.72,false,true,268451,Photographs,Photograph,Newhaven Fisherman,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.73,false,true,268452,Photographs,Photograph,Newhaven Boy,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.74,false,true,268453,Photographs,Photograph,Newhaven Group,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.76,false,true,268455,Photographs,Photograph,Newhaven Fisherman,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.77,false,true,268456,Photographs,Photograph,Newhaven Fishermen,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.78,false,true,268457,Photographs,Photograph,Newhaven Fishermen,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.79,false,true,268458,Photographs,Photograph,"The Porthole / Sergeant and Private of the 42nd Gordon Highlanders, Edinburgh Castle",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.80,false,true,268460,Photographs,Photograph,"The 42nd Gordon Highlanders, Edinburgh Castle",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.81,false,true,268461,Photographs,Photograph,"The 42nd Gordon Highlanders, Edinburgh Castle",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.82,false,true,268462,Photographs,Photograph,Lane and Lewis / Lane and Redding,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.83,false,true,268463,Photographs,Photograph,Lane,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.84,false,true,268464,Photographs,Photograph,Rev. Peter Jones,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.85,false,true,268465,Photographs,Photograph,Miss Elizabeth Logan,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.86,false,true,268466,Photographs,Photograph,Jimmy Miller,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.87,false,true,268467,Photographs,Photograph,Jimmy Miller,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.88,false,true,268468,Photographs,Photograph,Jimmy Miller,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.90,false,true,268471,Photographs,Photograph,"John Henning as Edie Ochiltree from Sir Walter Scott's ""The Antiquary""",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,Image: 20.6 x 15.8 cm (8 1/8 x 6 1/4 in.),"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.91,false,true,268472,Photographs,Photograph,Patrick Byrne,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.92,false,true,268473,Photographs,Photograph,Patrick Byrne,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.93,false,true,268474,Photographs,Photograph,Patrick Byrne,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.94,false,true,268475,Photographs,Photograph,The Misses McCandlish,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.95,false,true,268476,Photographs,Photograph,The Misses McCandlish,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.96,false,true,268477,Photographs,Photograph,Finlay Children,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.98,false,true,268479,Photographs,Photograph,Master Finlay,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.99,false,true,268480,Photographs,Photograph,"Lady, Standing",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.10,false,true,268483,Photographs,Photograph,William Leighton Leitch,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.11,false,true,268494,Photographs,Photograph,William Leighton Leitch,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.12,false,true,268505,Photographs,Photograph,"Henning, Handyside Ritchie, & D.O. Hill, R.S.A.",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.13,false,true,268516,Photographs,Photograph,Henning,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.14,false,true,268519,Photographs,Photograph,Sir John Robert Steell,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.15,false,true,268520,Photographs,Photograph,"H.B. Johnston, R.S.A.",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.16,false,true,268521,Photographs,Photograph,D.O. Hill and W.B. Johnstone,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.17,false,true,268522,Photographs,Photograph,Kenneth Macleay,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.18,false,true,268523,Photographs,Photograph,Prof. John Wilson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.19,false,true,268524,Photographs,Photograph,"""Edinburgh Ale"" James Ballentine, Dr. George Bell, D.O. Hill",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.20,false,true,268526,Photographs,Photograph,Moir (?) and John Wilson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.21,false,true,268527,Photographs,Photograph,John Wilson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.22,false,true,268528,Photographs,Photograph,John Wilson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.23,false,true,268529,Photographs,Photograph,Thomas Chalmers,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.24,false,true,268530,Photographs,Photograph,Thomas Chalmers,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.25,false,true,268531,Photographs,Photograph,Dr. Welsh (Retiring Moderator of Gel' Assembly 1843),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.26,false,true,268532,Photographs,Photograph,Dr. Welsh,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.27,false,true,268533,Photographs,Photograph,Dr. Welsh,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.28,false,true,268534,Photographs,Photograph,"Cunningham, Beff, John Hamilton, Guthrie",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.29,false,true,268535,Photographs,Photograph,Dr. Arnold,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.30,false,true,268537,Photographs,Photograph,Guthrie,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.31,false,true,268538,Photographs,Photograph,Lord Robertson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.32,false,true,268539,Photographs,Photograph,Rev. Mr. Smith of Borgue,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.33,false,true,268540,Photographs,Photograph,Swinton,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.34,false,true,268541,Photographs,Photograph,Rev. Henshaw Jones,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.35,false,true,268542,Photographs,Photograph,Dr. Jabez Bunting,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.36,false,true,268543,Photographs,Photograph,Dr. Jabez Bunting,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.37,false,true,268544,Photographs,Photograph,Dr. Monro,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.38,false,true,268545,Photographs,Photograph,Campbell of Monzie,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.39,false,true,268546,Photographs,Photograph,Campbell of Monzie,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.40,false,true,268548,Photographs,Photograph,Rev. John Julius Wood,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.42,false,true,268550,Photographs,Photograph,Robert Dundas Cay,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–44,1843,1844,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.43,false,true,268551,Photographs,Photograph,George Gilfillan and Samuel Brown,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.44,false,true,268552,Photographs,Photograph,Annan Presbytery,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.45,false,true,268553,Photographs,Photograph,Sir James Young Simpson & Wainhouse (or Muirhouse),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.46,false,true,268554,Photographs,Photograph,Sir John Jaffray and Dhanjiobai Nauroji,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.47,false,true,268555,Photographs,Photograph,Presbytery of Dundee,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.48,false,true,268556,Photographs,Photograph,"Sir David Brewster, Earle Monteith, Dr. Welsh & Two Others",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.49,false,true,268557,Photographs,Photograph,"Alexander of Duntocher, McMillan of Cardross and Two Others",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.50,false,true,268559,Photographs,Photograph,[Two Unidentified Men],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.51,false,true,268560,Photographs,Photograph,"Symington, Paisley, and Glasgow",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.52,false,true,268561,Photographs,Photograph,Rev. Miller and His Son Rev. Samuel Miller,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.53,false,true,268562,Photographs,Photograph,"James Gordon, Dr. Hanna, and Mr. Cowan",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.55,false,true,268564,Photographs,Photograph,Rev. Dr. Keith,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.56,false,true,268565,Photographs,Photograph,Rev. Dr. Keith,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268565,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.57,false,true,268566,Photographs,Photograph,Dr. Capadore,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.58,false,true,268567,Photographs,Photograph,Davidson of Aberdeen,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.59,false,true,268568,Photographs,Photograph,Dr. Sampson of York,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.60,false,true,268570,Photographs,Photograph,"Rev. Stephen Hislop, Missionary",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.61,false,true,268571,Photographs,Photograph,[Man Holding Umbrella],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.62,false,true,268572,Photographs,Photograph,Rev. Mr. Elder of Watts,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.63,false,true,268573,Photographs,Photograph,"Dr. George Cook, St. Andrews",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.64,false,true,268574,Photographs,Photograph,[Unidentified Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.65,false,true,268575,Photographs,Photograph,Rev. Dr. William Hamilton Burns,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.66,false,true,268576,Photographs,Photograph,Dr. Foulis,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.67,false,true,268577,Photographs,Photograph,"""Cookie"" Miller",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.68,false,true,268578,Photographs,Photograph,William Scott Moncrieff,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268578,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.69,false,true,268579,Photographs,Photograph,"Thomas Kitchenham Staveley, M.P. Ripon",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.70,false,true,268581,Photographs,Photograph,"Rev. W. W. Duncan, Peebles (Sweet William)",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.71,false,true,268582,Photographs,Photograph,John Murray (Publisher),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.72,false,true,268583,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.73,false,true,268584,Photographs,Photograph,Cookie Miller,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.74,false,true,268585,Photographs,Photograph,Rev. Charles John Brown,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.75,false,true,268586,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.76,false,true,268587,Photographs,Photograph,Frederic Monod (Paris),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.77,false,true,268588,Photographs,Photograph,"Rev. Thomas Jollie, Bowden",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.78,false,true,268589,Photographs,Photograph,"Rev. Robert Aitken, Dundee",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.79,false,true,268590,Photographs,Photograph,Archibald Butler of Faskally,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.80,false,true,268592,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.81,false,true,268593,Photographs,Photograph,James Dymock,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.82,false,true,268594,Photographs,Photograph,Rev. James Scott,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.83,false,true,268595,Photographs,Photograph,Rev. James Scott,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.84,false,true,268596,Photographs,Photograph,"Thomas Bell, Leswalt",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.85,false,true,268597,Photographs,Photograph,"David Maitland Makgill Crichton, Rankeillour",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.86,false,true,268598,Photographs,Photograph,"Dr. Inglis, Halifax",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.87,false,true,268599,Photographs,Photograph,James Aytoun,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.88,false,true,268600,Photographs,Photograph,"Rev. George Lewis, Dundee",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.89,false,true,268601,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.90,false,true,268603,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.91,false,true,268604,Photographs,Photograph,Rev. Thomas Jolly of Bowden,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.92,false,true,268605,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.93,false,true,268606,Photographs,Photograph,"Robert Paul, Commercial Bank",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.94,false,true,268607,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.95,false,true,268608,Photographs,Photograph,Jacob Abbott,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.96,false,true,268609,Photographs,Photograph,"General John Munro, Teanich",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.97,false,true,268610,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.98,false,true,268611,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.99,false,true,268612,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.14,false,true,282017,Photographs,Photograph,Rev. Dr. Thomas Chalmers,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,ca. 1843,1841,1845,Salted paper print from paper negative,16.1 x 11.9 cm (6 5/16 x 4 11/16 in.),"The Rubel Collection, Purchase, Harris Brisbane Dick Fund and Warner Communication Inc. Purchase Fund, by exchange, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.18,false,true,282021,Photographs,Photograph,Lady Ruthven,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,ca. 1845,1843,1847,Salted paper print from paper negative,19.9 x 15 cm (7 13/16 x 5 7/8 in.),"The Rubel Collection, Purchase, Manfred Heiting and Lila Acheson Wallace Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.19,true,true,282022,Photographs,Photograph,[Newhaven Fishwives],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,ca. 1845,1843,1847,Salted paper print from paper negative,29.5 x 21.7 cm (11 5/8 x 8 9/16 in.),"The Rubel Collection, Purchase, Lila Acheson Wallace, Harriette and Noel Levine, and Alexandra R. Marshall Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.25,false,true,282028,Photographs,Photograph,"[Officer of the 92nd Gordon Highlanders Reading to the Troops, Edinburgh Castle]",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,"April 9, 1846",1846,1846,Salted paper print from paper negative,14.5 x 19.2 cm (5 11/16 x 7 9/16 in. ),"The Rubel Collection, Purchase, Lila Acheson Wallace, Ann Tenenbaum and Thomas H. Lee, and Harriette and Noel Levine Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.29,false,true,282032,Photographs,Photograph,The Fairy Tree at Colinton,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1846,1846,1846,Salted paper print from paper negative,20.8 x 15 cm (8 3/16 x 5 7/8 in.),"The Rubel Collection, Purchase, Ann Tenenbaum and Thomas H. Lee and Lila Acheson Wallace Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.100,false,true,268368,Photographs,Photograph,Mrs. Marian Murray,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268368,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.101,false,true,268369,Photographs,Photograph,Mrs. Jameson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.102,false,true,268370,Photographs,Photograph,Lady Elizabeth Eastlake,,,,,,Artist|Artist|Photography Studio|Person in Photograph,Person in photograph,David Octavius Hill|Robert Adamson|Hill and Adamson|Lady Elizabeth Rigby Eastlake,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson|Eastlake, Elizabeth Rigby, Lady","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843 |1809,1870 |1848 |1848 |1893,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268370,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.103,false,true,268371,Photographs,Photograph,Lady Elizabeth Eastlake,,,,,,Artist|Artist|Photography Studio|Person in Photograph,Person in photograph,David Octavius Hill|Robert Adamson|Hill and Adamson|Lady Elizabeth Rigby Eastlake,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson|Eastlake, Elizabeth Rigby, Lady","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843 |1809,1870 |1848 |1848 |1893,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.104,false,true,268372,Photographs,Photograph,John Henning with Group of Ladies,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.105,false,true,268373,Photographs,Photograph,Miss Binney,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.106,false,true,268374,Photographs,Photograph,Unidentified Woman,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.107,false,true,268375,Photographs,Photograph,"Couple Seated, Woman Reading",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.108,false,true,268376,Photographs,Photograph,Mrs. Watson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.109,false,true,268377,Photographs,Photograph,"Mrs. Marian Murray, Lady Stair",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.110,false,true,268379,Photographs,Photograph,Lady Ruthven,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.111,false,true,268380,Photographs,Photograph,Mrs. Shanker,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.112,false,true,268381,Photographs,Photograph,Mrs. Grierson,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.113,false,true,268382,Photographs,Photograph,Mrs. Rigby,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.114,false,true,268383,Photographs,Photograph,Miss Munro,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.1.115,false,true,268384,Photographs,Photograph,Miss Kemp as Ophelia,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.100,false,true,268484,Photographs,Photograph,Rev. Dr. Andrew Sutherland,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.101,false,true,268485,Photographs,Photograph,[George Gordon (?)],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.102,false,true,268486,Photographs,Photograph,Dr. Cook of St. Andrews,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.103,false,true,268487,Photographs,Photograph,Dr. Cook,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.104,false,true,268488,Photographs,Photograph,Dr. Cook,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.105,false,true,268489,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.106,false,true,268490,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.107,false,true,268491,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.108,false,true,268492,Photographs,Photograph,Hartcourt (Brother of Archbishop of York),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.109,false,true,268493,Photographs,Photograph,Sir Charles Lyell - Geologist,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.110,false,true,268495,Photographs,Photograph,"Rev. Dr. John Purves, Jedburgh",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.111,false,true,268496,Photographs,Photograph,Scott (of Peel),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.112,false,true,268497,Photographs,Photograph,Rev D.T.K. Drummond,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.113,false,true,268498,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.114,false,true,268499,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.115,false,true,268500,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.116,false,true,268501,Photographs,Photograph,Earl of Rosemore,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.117,false,true,268502,Photographs,Photograph,Dr. Latham - Editor of Dictionary,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.118,false,true,268503,Photographs,Photograph,Dr. MacCulloch of Kelso and Greenock,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.119,false,true,268504,Photographs,Photograph,Mr. McNab - Botanical Gardens,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.120,false,true,268506,Photographs,Photograph,[Man],,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.121,false,true,268507,Photographs,Photograph,Sir John Boilleau,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.122,false,true,268508,Photographs,Photograph,MacKenzie (Tongue),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.123,false,true,268509,Photographs,Photograph,"Principal Haldane, St. Andrews",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.124,false,true,268510,Photographs,Photograph,Rev. R. Brewster of Craig,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.125,false,true,268511,Photographs,Photograph,Dr. Smyttan,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.126,false,true,268512,Photographs,Photograph,James Nasmyth (Steam Hammer),,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.127,false,true,268513,Photographs,Photograph,Laird of Portmoak,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.128,false,true,268514,Photographs,Photograph,"Rev. Henry Grey, D.D., St. Mary's, Edinburgh",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.129,false,true,268515,Photographs,Photograph,James Ballantine,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.130,false,true,268517,Photographs,Photograph,Sobieski Stuart,,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.98.2.131,false,true,268518,Photographs,Photograph,"Prof. Fraser, Rev. Welsh, Rev. Hamilton, and Three Other Men",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.354,false,true,285726,Photographs,Photograph,"The Morning After ""He Greatly Daring Dined""",,,,,,Artist|Artist|Photography Studio,,David Octavius Hill|Robert Adamson|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843,1870 |1848 |1848,1843–47,1843,1847,Salted paper print from paper negative,Image: 19.7 x 14.6 cm (7 3/4 x 5 3/4 in.) Mount: 14 3/4 in. × 10 3/8 in. (37.4 × 26.3 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.17,false,true,306219,Photographs,Photograph,The Artist and the Grave Digger,,,,,,Artist|Artist|Photography Studio,,Robert Adamson|David Octavius Hill|Hill and Adamson,"British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, active 1843–1848",,"Adamson, Robert|Hill, David Octavius|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1821 |1802 |1843,1848 |1870 |1848,1843–44,1843,1844,Salted paper print from paper negative,Mount: 14 7/8 in. × 10 1/2 in. (37.8 × 26.7 cm) Image: 8 3/8 × 6 5/16 in. (21.2 × 16 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.23,false,true,306226,Photographs,Photograph,"David Young and Unknown Man, Newhaven",,,,,,Artist|Artist|Photography Studio,,Robert Adamson|David Octavius Hill|Hill and Adamson,"British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, active 1843–1848",,"Adamson, Robert|Hill, David Octavius|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish",1821 |1802 |1843,1848 |1870 |1848,1845,1845,1845,Salted paper print from paper negative,Image: 6 1/8 × 4 7/16 in. (15.6 × 11.3 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.2038–.2084,false,true,288319,Photographs,Stereographs,[Group of 47 Stereograph Views of the 1904 St. Louis World's Fair and Louisiana Purchase Exposition],,,,,,Artist|Publisher|Publisher|Photography Studio|Artist|Artist|Publisher|Artist|Subject|Publisher|Artist|Person in Photograph|Publisher,,C. H. Graves|Universal View Co.|Underwood & Underwood|Sun Sculpture Works and Studios|Strohmeyer & Wyman|William H. Rau|Metropolitan Series|Kilburn Brothers|James M. Davis|H. C. White Company|Unknown|Geronimo (Goyaalé)|Keystone View Company,"American|American|American|American|American|American, 1855–1920|American|American, active ca. 1865–1890|American|American|American Indian (Apache), 1829–1909",,"Graves, C. H.|Universal View Co.|Underwood & Underwood|Sun Sculpture Works and Studios|Strohmeyer & Wyman|Rau, William H.|Metropolitan Series|Kilburn Brothers|Davis, James M.|H. C. White Company|Unknown|Geronimo (Goyaalé)|Keystone View Company","American|American|American|American|American|American|American|American|American|American Indian, Apache",1855 |1863 |1829,1920 |1892 |1909,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.411–.433,false,true,288143,Photographs,Stereographs,[Group of 23 Stereograph Views of Railroad Bridges],,,,,,Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Artist|Publisher|Artist|Publisher|Publisher,,E. P. Libby|American Scenery|American Series|American Views|Allen & Hovey|Mrs. M. E. Allen|E. C. Barnum|New H Series|Littleton View Company|Western View Company|Benneville Lloyd Singley|H. L. & G. E. Williams|Keystone View Company,"American|American|American|American|American|American|American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American",,"Libby, E. P.|American Scenery|American Series|American Views|Allen & Hovey|Allen, M. E. Mrs.|Barnum, E. C.|New H Series|Littleton View Company|Western View Company|Singley, Benneville Lloyd|H. L. & G. E. Williams|Keystone View Company",American|American|American|American|American|American|American|American|American|American|American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.2 x 17.1 cm (3 1/4 x 6 3/4 in.) to 10 x 17.8 cm (3 15/16 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1044–.1074,false,true,288300,Photographs,Stereographs,[Group of 31 Stereograph Views of Children With Animals],,,,,,Publisher|Artist|Artist|Publisher|Photography Studio|Publisher|Artist|Artist|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Publisher|Artist|Artist|Publisher,,Life Groups|Benneville Lloyd Singley|Strohmeyer & Wyman|Underwood & Underwood|Sun Sculpture Works and Studios|Universal View Co.|William H. Rau|C. H. Graves|Universal Photo Art Co.|Canvassers|Webster & Albee|Popular Series|European and American Views|Stereoscopic Gems|Comics and Groups|Unknown|Unknown|Keystone View Company,"American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American|American|American|American, 1855–1920|American|American|American",,"Life Groups|Singley, Benneville Lloyd|Strohmeyer & Wyman|Underwood & Underwood|Sun Sculpture Works and Studios|Universal View Co.|Rau, William H.|Graves, C. H.|Universal Photo Art Co.|Canvassers|Webster & Albee|Popular Series|European and American Views|Stereoscopic Gems|Comics and Groups|Unknown|Unknown|Keystone View Company",American|American|American|American|American|American|American|American|American,1864 |1855,1938 |1920,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1152–.1157,false,true,288303,Photographs,Stereographs,[Group of 6 Stereograph Views of Christmas Scenes],,,,,,Publisher|Artist|Publisher|Artist|Artist|Publisher|Artist|Publisher|Photography Studio|Artist|Publisher|Artist|Publisher,,"Hegger|C. H. Graves|Universal Photo Art Co.|Benneville Lloyd Singley|Kilburn Brothers|James M. Davis|Strohmeyer & Wyman|Underwood & Underwood|Sun Sculpture Works and Studios|George W. Griffith|Griffith & Griffith, American|Unknown|Keystone View Company","American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American, active ca. 1865–1890|American|American|American|American|American",,"Hegger|Graves, C. H.|Universal Photo Art Co.|Singley, Benneville Lloyd|Kilburn Brothers|Davis, James M.|Strohmeyer & Wyman|Underwood & Underwood|Sun Sculpture Works and Studios|Griffith, George W.|Griffith & Griffith|Unknown|Keystone View Company",American|American|American|American|American|American|American|American|American,1864 |1863,1938 |1892,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.25,false,true,268323,Photographs,Photograph,Horace Mann,,,,,,Artist|Photography Studio|Artist,,Josiah Johnson Hawes|Southworth and Hawes|Albert Sands Southworth,"American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts",,"Hawes, Josiah Johnson|Southworth and Hawes|Southworth, Albert Sands",American|American|American,1808 |1843 |1811,1901 |1863 |1894,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.34,true,true,268621,Photographs,Photograph,Lemuel Shaw,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1938",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.2,false,true,268317,Photographs,Photograph,Daniel Webster,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.5 x 16.6 cm (8 7/16 x 6 9/16 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.3,false,true,268328,Photographs,Photograph,"[View Down Brattle Street from the Southworth & Hawes Studio at 5 1/2 Tremont Row, Boston]",,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,1855,1855,1855,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.4,false,true,268339,Photographs,Photograph,[Woman in Black Taffeta Dress and Lace Shawl],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.5,false,true,268350,Photographs,Photograph,[Unidentified Woman],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.6,false,true,268359,Photographs,Photograph,[Boston Lawyers or Clergymen (?)],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,16.5 x 21.6 cm (6 1/2 x 8 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.7,false,true,268360,Photographs,Photograph,Niagara Falls from the Canadian Side,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.9,false,true,268362,Photographs,Photograph,Francis Parkman,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,10.8 x 8.3 cm (4 1/4 x 3 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.22.2,false,true,268648,Photographs,Photograph,[Man in a Sheraton Chair],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift Edward S. Hawes and Marion Augusta Hawes, 1939",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.22.4,false,true,268650,Photographs,Photograph,[Unidentified Woman in Nine Oval Views],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of Edward S. Hawes and Marion Augusta Hawes, 1939",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.10,false,true,268307,Photographs,Photograph,Margaret Fuller (Marchioness Ossoli),,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,10.8 x 8.3 cm (4 1/4 x 3 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.11,false,true,268308,Photographs,Photograph,[Nancy Southworth Hawes],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,8.3 x 7.0 cm (3 1/4 x 2 3/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.12,false,true,268309,Photographs,Photograph,[Unidentified Boy in Dark Suit],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.13,false,true,268310,Photographs,Photograph,[Unidentified Woman],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.14,false,true,268311,Photographs,Photograph,Millard Fillmore,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.15,false,true,268312,Photographs,Photograph,Henry Clay,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,14.0 x 10.8 cm (5 1/2 x 4 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.16,false,true,268313,Photographs,Photograph,Albert Sands Southworth,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1845–50,1845,1850,Daguerreotype,8.3 x 7.0 cm (3 1/4 x 2 3/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.17,false,true,268314,Photographs,Photograph,[Elderly Woman in Black Cape and Bonnet with Mourning Crape],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.18,false,true,268315,Photographs,Photograph,[Unidentified Woman],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.22,false,true,268320,Photographs,Photograph,Classroom in the Emerson School for Girls,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.23,false,true,268321,Photographs,Photograph,Commodore Charles Morris,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,14.0 x 10.8 cm (5 1/2 x 4 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.24,false,true,268322,Photographs,Photograph,William Hickling Prescott,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,14.0 x 10.8 cm (5 1/2 x 4 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.27,false,true,268325,Photographs,Photograph,Mrs. James Thomas Fields (Annie Adams),,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,1861,1861,1861,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.29,false,true,268327,Photographs,Photograph,[Unidentified Man Wearing Turban],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,1851–52,1851,1852,Daguerreotype,8.3 x 7.0 cm (3 1/4 x 2 3/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.30,false,true,268329,Photographs,Photograph,George Peabody,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,10.8 x 8.3 cm (4 1/4 x 3 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.31,false,true,268330,Photographs,Photograph,Henry Wadsworth Longfellow,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,10.8 x 8.3 cm (4 1/4 x 3 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.32,false,true,268331,Photographs,Photograph,Zachary Taylor,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1840,1859,Daguerreotype,8.3 x 7.0 cm (3 1/4 x 2 3/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.33,false,true,268332,Photographs,Photograph,John Howard Payne,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,14.0 x 10.8 cm (5 1/2 x 4 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.35,false,true,268334,Photographs,Photograph,[Woman in Profile with Lace Collar and Shawl],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.36,false,true,268335,Photographs,Photograph,James Thomas Fields,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,1861,1861,1861,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.37,false,true,268336,Photographs,Photograph,William Lloyd Garrison,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,14.0 x 10.8 cm (5 1/2 x 4 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.38,false,true,268337,Photographs,Photograph,Josiah Johnson Hawes,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1845–50,1845,1850,Daguerreotype,10.8 x 8.3 cm (4 1/4 x 3 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.39,false,true,268338,Photographs,Photograph,[Boston Doctors],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,16.5 x 21.6 cm (6 1/2 x 8 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.40,false,true,268340,Photographs,Photograph,Harriet Beecher Stowe,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,1850s,1850,1850,Daguerreotype,10.8 x 8.3 cm. (4 1/4 x 3 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.41,false,true,268341,Photographs,Photograph,Lola Montez,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.43,false,true,268343,Photographs,Photograph,James Freeman Clarke,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,14.0 x 10.8 cm (5 1/2 x 4 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.48,false,true,268348,Photographs,Photograph,Rufus Choate,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,20.4 x 15.3 cm (8 x 6 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.50,false,true,268351,Photographs,Photograph,Charles Sprague,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,42.2 x 32.4 cm (16 5/8 x 12 3/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.51,false,true,268352,Photographs,Photograph,[Young Girl with Hand Raised to Shoulder],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.52,false,true,268353,Photographs,Photograph,[Elderly Man; Full Face],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.53,false,true,268354,Photographs,Photograph,[Girl with Portrait of George Washingtion],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.54,false,true,268355,Photographs,Photograph,Dr. John Collins Warren,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.55,false,true,268356,Photographs,Photograph,[Man in Judge's Robes; Seated],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.56,false,true,268357,Photographs,Photograph,[Students from the Emerson School for Girls],,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,16.5 x 21.6 cm (6 1/2 x 8 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.57,false,true,268358,Photographs,Photograph,John L. Tucker,,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1850,1848,1852,Daguerreotype,33.4 x 41.3 cm (13 1/8 x 16 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.40,false,true,282045,Photographs,Photograph,"Sculpture Gallery, Boston Athenaeum",,,,,,Artist|Artist|Photography Studio,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,ca. 1855,1853,1857,Daguerreotype,visible: 18.6 x 13.7 cm (7 5/16 x 5 3/8 in.),"The Rubel Collection, Purchase, Ann Tenenbaum and Thomas H. Lee and Lila Acheson Wallace Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.39,false,true,291792,Photographs,Daguerreotype,[Young Woman with Hair Styled in Two Buns],,,,,,Artist|Artist|Photography Studio,Attributed to|Attributed to,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes",American|American|American,1811 |1808 |1843,1894 |1901 |1863,1850s,1850,1859,Daguerreotype,Image: 12.5 x 9.3 cm (4 15/16 x 3 11/16 in.) Plate: 16.5 x 13.7 cm (6 1/2 x 5 3/8 in.) Case: 1.9 x 18.1 x 15.2 cm (3/4 x 7 1/8 x 6 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.20,false,true,268318,Photographs,Photograph,Miss Hodges of Salem,,,,,,Photography Studio|Artist|Artist,,Southworth and Hawes|Josiah Johnson Hawes|Albert Sands Southworth,"American, active 1843–1863|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts",,"Southworth and Hawes|Hawes, Josiah Johnson|Southworth, Albert Sands",American|American|American,1843 |1808 |1811,1863 |1901 |1894,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.1,false,true,268306,Photographs,Photograph,Donald McKay,,,,,,Photography Studio|Artist|Artist,,Southworth and Hawes|Albert Sands Southworth|Josiah Johnson Hawes,"American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire",,"Southworth and Hawes|Southworth, Albert Sands|Hawes, Josiah Johnson",American|American|American,1843 |1811 |1808,1863 |1894 |1901,ca. 1850–55,1850,1855,Daguerreotype,21.6 x 16.5cm (8 1/2 x 6 1/2in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.19,false,true,268316,Photographs,Photograph,Robert Charles Winthrop,,,,,,Photography Studio|Artist|Artist,,Southworth and Hawes|Albert Sands Southworth|Josiah Johnson Hawes,"American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire",,"Southworth and Hawes|Southworth, Albert Sands|Hawes, Josiah Johnson",American|American|American,1843 |1811 |1808,1863 |1894 |1901,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.26,false,true,268324,Photographs,Photograph,Elias Howe,,,,,,Photography Studio|Artist|Artist,,Southworth and Hawes|Albert Sands Southworth|Josiah Johnson Hawes,"American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire",,"Southworth and Hawes|Southworth, Albert Sands|Hawes, Josiah Johnson",American|American|American,1843 |1811 |1808,1863 |1894 |1901,ca. 1850,1848,1852,Daguerreotype,14.0 x 10.8 cm (5 1/2 x 4 1/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.28,false,true,268326,Photographs,Photograph,Charles Sumner,,,,,,Photography Studio|Artist|Artist,,Southworth and Hawes|Albert Sands Southworth|Josiah Johnson Hawes,"American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire",,"Southworth and Hawes|Southworth, Albert Sands|Hawes, Josiah Johnson",American|American|American,1843 |1811 |1808,1863 |1894 |1901,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.556.1,false,true,301969,Photographs,Photograph,[Augusta Hawes at Four Years Old],,,,,,Photography Studio|Artist|Artist,,Southworth and Hawes|Albert Sands Southworth|Josiah Johnson Hawes,"American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire",,"Southworth and Hawes|Southworth, Albert Sands|Hawes, Josiah Johnson",American|American|American,1843 |1811 |1808,1863 |1894 |1901,1850s,1850,1859,Daguerreotype,Plate: 12.7 x 10.7 cm (5 x 4 3/16 in.),"Gift of Isaac Lagnado, in honor of Director Thomas P. Campbell, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.813–.820,false,true,288237,Photographs,Stereographs,"[Group of 8 Early Stereograph Views of British Monuments, Memorials, and Tombs]",,,,,,Artist|Artist|Artist|Person in Photograph|Publisher|Person in Photograph|Person in Photograph,,Lennie|Unknown|Taylor|Samuel Johnson|Stereoscopic Gems|Sir Walter Scott|Earl Richard Beauchamp,"British, born Scotland|British|British|British, Lichfield, Staffordshire 1709–1784 London|British, Edinburgh, Scotland 1771–1832 Abbotsford, Scotland|British, 1382–1439",,"Lennie|Unknown|Taylor, Mr.|Johnson, Samuel|Stereoscopic Gems|Scott, Walter, Sir|Beauchamp, Richard Earl","British, Scottish|British|British|British, Scottish|British",1709 |1771 |1382,1784 |1832 |1439,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.4 x 17.1 cm (3 5/16 x 6 3/4 in.) to 8.5 x 17.8 cm (3 3/8 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.647–.658,false,true,288154,Photographs,Stereographs,"[Group of 12 Stereograph Views of Celebrities, Including Popes and Presidents]",,,,,,Publisher|Photography Studio|Person in Photograph|Publisher|Publisher|Person in Photograph|Person in Photograph|Person in Photograph|Person in Photograph|Publisher|Artist|Person in Photograph|Publisher|Publisher,,Underwood & Underwood|Sun Sculpture Works and Studios|Pope Pius X|C. H. Graves|Universal Photo Art Co.|William McKinley|President John Calvin Coolidge Jr.|First Lady Grace Anna Goodhue Coolidge|Major General Adna Romanza Chaffee|H. C. White Company|Alfred Hewitt|First Lady Ida Saxton McKinley|William Hibbert|Keystone View Company,"American|American|Italian, 1835–1914|American|American, 1843–1901|American, 1872–1933|American, 1879–1957|American, 1842–1941|American|American, 1847–1907|American",,"Underwood & Underwood|Sun Sculpture Works and Studios|Pius, X Pope|Graves, C. H.|Universal Photo Art Co.|McKinley, William|Coolidge, John Calvin Jr. President|Goodhue, Coolidge Grace Anna First Lady|Chaffee, Adna Romanza Major General|H. C. White Company|Hewitt, Alfred|McKinley, Ida Saxton First Lady|Hibbert, William|Keystone View Company",American|American|Italian|American|American|American|American|American|American|American|American,1835 |1843 |1872 |1879 |1842 |1847,1914 |1901 |1933 |1957 |1941 |1907,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.34,false,true,268333,Photographs,Photograph,John Quincy Adams,,,,,,Artist|Artist|Photography Studio|Artist,After,Josiah Johnson Hawes|Albert Sands Southworth|Southworth and Hawes|Philip Haas,"American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, active 1843–1863|American",,"Hawes, Josiah Johnson|Southworth, Albert Sands|Southworth and Hawes|Haas, Philip",American|American|American|American,1808 |1811 |1843,1901 |1894 |1863,ca. 1850,1848,1852,Daguerreotype,12.0 x 9.0 cm (4 3/4 x 3 9/16 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.13,false,true,263161,Photographs,Photograph,Afghans,,,,,,Artist|Artist|Photography Studio|Person in Photograph,Person in photograph,David Octavius Hill|Robert Adamson|Hill and Adamson|Edward William Lane,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, active 1843–1848|British, Hereford 1801–1876 Worthing",,"Hill, David Octavius|Adamson, Robert|Hill and Adamson|Lane, Edward William","British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1843 |1801,1870 |1848 |1848 |1876,1843,1843,1843,Salted paper print from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1031–.1043,false,true,288297,Photographs,Stereographs,[Group of 13 Stereograph Views of Families and Children],,,,,,Publisher|Artist|Publisher|Publisher|Artist|Publisher|Artist|Artist|Publisher|Publisher|Publisher|Artist|Publisher|Artist|Artist,,Keystone View Company|Benneville Lloyd Singley|Group Series|Universal View Co.|William H. Rau|Charles Moody|C. L. Howe|Kilburn Brothers|James M. Davis|H. C. White Company|Littleton View Company|F. G. Weller|Underwood & Underwood|Unknown|Unknown,"American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American, 1855–1920|American|American|American, active ca. 1865–1890|American|American|American|American",,"Keystone View Company|Singley, Benneville Lloyd|Group Series|Universal View Co.|Rau, William H.|Moody, Charles|Howe, C. L.|Kilburn Brothers|Davis, James M.|H. C. White Company|Littleton View Company|Weller., F. G.|Underwood & Underwood|Unknown|Unknown",American|American|American|American|American|American|American|American|American,1864 |1855 |1863,1938 |1920 |1892,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.2 x 17.1 cm (3 1/4 x 6 3/4 in.) to 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.44,false,true,268344,Photographs,Photograph,William Henry Harrison,,,,,,Artist|Artist|Photography Studio|Artist,,Albert Sands Southworth|Josiah Johnson Hawes|Southworth and Hawes|Albert Gallatin Hoit,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire|American, active 1843–1863|American, 1809–1856",,"Southworth, Albert Sands|Hawes, Josiah Johnson|Southworth and Hawes|Hoit, Albert Gallatin",American|American|American|American,1811 |1808 |1843 |1809,1894 |1901 |1863 |1856,ca. 1850,1848,1852,Daguerreotype,8.3 x 7.0 cm (3 1/4 x 2 3/4 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1951–.2016,false,true,288316,Photographs,Stereographs,[Group of 66 Stereograph Views of the 1893 Chicago World's Fair and Columbian Exposition],,,,,,Artist|Publisher|Artist|Subject|Artist|Artist|Person in Photograph|Publisher|Publisher|Photography Studio|Publisher,,Strohmeyer & Wyman|Underwood & Underwood|Kilburn Brothers|James M. Davis|Charles Dudley Arnold|Benneville Lloyd Singley|Stephen Grover Cleveland|Gust. Holmquist|Webster & Albee|Sun Sculpture Works and Studios|Keystone View Company,"American|American|American, active ca. 1865–1890|American, 1844–1927|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American, 1837–1908|American|American|American",,"Strohmeyer & Wyman|Underwood & Underwood|Kilburn Brothers|Davis, James M.|Arnold, Charles D.|Singley, Benneville Lloyd|Cleveland, Stephen Grover|Holmquist, Gust.|Webster & Albee|Sun Sculpture Works and Studios|Keystone View Company",American|American|American|American|American|American|American|American|American,1863 |1844 |1864 |1837,1892 |1927 |1938 |1908,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.7 x 17.5 cm (3 7/16 x 6 7/8 in.) to 10.6 x 17.8 cm (4 3/16 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.9,false,true,282012,Photographs,Photograph,Sir David Brewster,,,,,,Artist|Artist|Person in Photograph|Photography Studio,,David Octavius Hill|Robert Adamson|Sir David Brewster|Hill and Adamson,"British, Perth, Scotland 1802–1870 Edinburgh, Scotland|British, St. Andrews, Scotland 1821–1848 St. Andrews, Scotland|British, Jedburgh, Scotland 1781–1868 Melrose|British, active 1843–1848",,"Hill, David Octavius|Adamson, Robert|Brewster, David, Sir|Hill and Adamson","British, Scottish|British, Scottish|British, Scottish|British, Scottish",1802 |1821 |1781 |1843,1870 |1848 |1868 |1848,ca. 1844,1842,1846,Salted paper print from paper negative,20.2 x 15.1 cm (7 15/16 x 5 15/16 in. ),"The Rubel Collection, Purchase, Lila Acheson Wallace Gift, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.208–.255,false,true,288120,Photographs,Stereographs,[Group of 48 Stereograph Views of Arizona and the Surrounding Area],,,,,,Publisher|Publisher|Photography Studio|Artist|Publisher|Publisher|Publisher|Publisher|Artist|Publisher|Artist|Publisher|Artist|Publisher,,H. C. White Company|Underwood & Underwood|Sun Sculpture Works and Studios|Kilburn Brothers|Edward Kilburn|American Stereoscopic Company|Quaker Oats Company|Continent Stereoscopic Company|Benneville Lloyd Singley|New H Series|W. S. Conant|Standard Series|Unknown|Keystone View Company,"American|American|American|American, active ca. 1865–1890|American, 1830–1884|American|American|New York|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American",,"H. C. White Company|Underwood & Underwood|Sun Sculpture Works and Studios|Kilburn Brothers|Kilburn, Edward|American Stereoscopic Company|Quaker Oats Company|Continent Stereoscopic Company|Singley, Benneville Lloyd|New H Series|Conant, W. S.|Standard Series|Unknown|Keystone View Company",American|American|American|American|American|American|American|New York|American|American|American,1863 |1830 |1864,1892 |1884 |1938,1880s–1910s,1880,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.) to 10.6 x 17.4 cm (4 3/16 x 6 7/8 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1075–.1129,false,true,288301,Photographs,Stereographs,[Group of 55 Stereograph Views of Groups of Children],,,,,,Artist|Publisher|Publisher|Publisher|Artist|Artist|Artist|Publisher|Publisher|Publisher|Artist|Artist|Publisher|Artist|Publisher|Artist|Publisher|Artist|Publisher|Publisher|Publisher|Publisher|Artist|Artist|Publisher,,T. W. Ingersoll|Comic Series|Popular Series|Comics and Groups|R. B. Lewis|W. E. Sparrow|F. G. Weller|Underwood & Underwood|Littleton View Company|Union View Company|Strohmeyer & Wyman|J. P. King|E. & H. T. Anthony|Benneville Lloyd Singley|Universal Photo Art Co.|C. H. Graves|Universal View Co.|William H. Rau|Hegger|Charles Moody|European and American Views|S. C. Northrop|Unknown|Unknown|Keystone View Company,"American|American|American|American|American|American|American|American|American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania|American|American|American, 1855–1920|American|American|American|American",,"Ingersoll, T. W.|Comic Series|Popular Series|Comics and Groups|Lewis, R. B.|Sparrow, W. E.|Weller., F. G.|Underwood & Underwood|Littleton View Company|Union View Company|Strohmeyer & Wyman|King, J. P.|E. & H. T. Anthony|Singley, Benneville Lloyd|Universal Photo Art Co.|Graves, C. H.|Universal View Co.|Rau, William H.|Hegger|Moody, Charles|European and American Views|Northrop, S. C.|Unknown|Unknown|Keystone View Company",American|American|American|American|American|American|American|American|American|American|American|American|American|American|American,1864 |1855,1938 |1920,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.7 x 17.4 cm (3 7/16 x 6 7/8 in.) to 9.9 x 17.8 cm (3 7/8 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1221,false,true,286616,Photographs,Photograph,"[President Abraham Lincoln, Major General John A. McClernand (right), and E. J. Allen (Allan Pinkerton, left), Chief of the Secret Service of the United States, at Secret Service Department, Headquarters Army of the Potomac, near Antietam, Maryland]",,,,,,Artist|Person in Photograph|Person in Photograph|Person in Photograph,,Alexander Gardner|Abraham Lincoln|John Alexander McClernand|Allan Pinkerton,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, Hardin County, Kentucky 1809–1865 Washington, D.C.|American, Breckinridge County, Kentucky 1812–1900 Springfield, Illinois|American, born Scotland, Glasgow 1819–1884 Chicago",,"Gardner, Alexander|Lincoln, Abraham|McClernand, John Alexander|Pinkerton, Allan","American, Scottish|American|American|American, born Scotland",1821 |1809 |1812-05-30|1819-08-25,1882 |1865 |1900-09-20|1884-07-01,"October 4, 1862",1862,1862,Albumen silver print from glass negative,Image: 22.4 x 18 cm (8 13/16 x 7 1/16 in.) Mount: 29.2 x 20.7 cm (11 1/2 x 8 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1220,false,true,286615,Photographs,Photograph,"[President Abraham Lincoln, Major General John A. McClernand (right), and E. J. Allen (Allan Pinkerton, left), Chief of the Secret Service of the United States, at Secret Service Department, Headquarters Army of the Potomac, near Antietam, Maryland]",,,,,,Artist|Person in Photograph|Person in Photograph|Person in Photograph,,Alexander Gardner|Allan Pinkerton|Abraham Lincoln|John Alexander McClernand,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born Scotland, Glasgow 1819–1884 Chicago|American, Hardin County, Kentucky 1809–1865 Washington, D.C.|American, Breckinridge County, Kentucky 1812–1900 Springfield, Illinois",,"Gardner, Alexander|Pinkerton, Allan|Lincoln, Abraham|McClernand, John Alexander","American, Scottish|American, born Scotland|American|American",1821 |1819-08-25|1809 |1812-05-30,1882 |1884-07-01|1865 |1900-09-20,"October 3, 1862",1862,1862,Albumen silver print from glass negative,Image: 20 x 18.5 cm (7 7/8 x 7 5/16 in.) Mount: 22.8 x 21.3 cm (9 x 8 3/8 in.) Mount: 34.1 x 27 cm (13 7/16 x 10 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.443,false,true,294320,Photographs,Photograph,[Nine Portraits in Original Passe-Partout],,,,,,Artist|Person in Photograph|Person in Photograph|Person in Photograph,,James William Bailey|Charles-Louis-Napoleon Bonaparte|William Ewart Gladstone|Albert Edward Prince of Wales,"British|French, Paris 1808–1873 Chislehurst, Kent|British, 1841–1910",,"Bailey, James William|Bonaparte, Charles-Louis-Napoleon|Gladstone, William Ewart|Edward, Albert Prince of Wales",British|French|British,1808 |1809 |1841,1873 |1898 |1910,1880s,1880,1889,Albumen silver prints from glass negatives with applied color,Frame: 34.9 x 34.9 cm (13 3/4 x 13 3/4 in.) Image: 4 x 3.5 cm (1 9/16 x 1 3/8 in.) Image: 7.7 x 6 cm (3 1/16 x 2 3/8 in.),"Funds from various donors, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1073.105,false,true,266362,Photographs,Album,Views and Costumes of Japan,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1860s,1860,1869,Albumen silver prints,,"Rogers Fund, 1957, transferred from the Library",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/266362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.214 (1-32),false,true,287306,Photographs,Micrograph,Mikroskopisch-Photographischer Atlas der Harnsedimente,,,,,,Artist,,Robert Ultzmann,"Austrian, 1842–1889",,"Ultzmann, Robert",Austrian,1842,1889,1869,1869,1869,Albumen silver prints,Album: 30.5 x 47 x 6.4 cm (12 x 18 1/2 x 2 1/2 in.),"Joyce F. Menschel Photography Library Fund, 2006",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/287306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1098.6,false,true,631030,Photographs,Paper Negative,"David Maitland Makgill Crichton, Rankeillour",,,,,,Artist,,Hill and Adamson,"British, active 1843–1848",,Hill and Adamson,"British, Scottish",1843,1848,1843–47,1843,1847,Waxed paper negative,Image: 8 3/8 × 6 3/16 in. (21.3 × 15.7 cm),"Gift of Joyce F. Menschel, 2013",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/631030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.105.69,false,true,269197,Photographs,Photomechanical print,A Stiff Pull,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,"1880s, printed 1887",1880,1889,Photogravure,20.8 x 28.9 cm. (8 3/16 x 11 3/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/269197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.68,false,true,290463,Photographs,Paper negative,Village de Murols,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1854,1854,1854,Paper negative,Image: 33.9 x 43.9 cm (13 3/8 x 17 5/16 in.) Sheet: 34.4 x 44.5 cm (13 9/16 x 17 1/2 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2009",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/290463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.1–.32,false,true,286010,Photographs,Album,[Album of photographs],,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,"32 salted paper prints, waxed salted paper prints, and albumen silver prints from paper and glass negatives",5 3/4 x 7 5/8 to 9 1/4 x 12,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.47,true,true,265543,Photographs,Photograph,Runner in the City,,,,,,Artist,,El Lissitzky,"Russian, Pochinok 1890–1941 Moscow",,"Lissitzky, El",Russian,1890,1941,ca. 1926,1924,1928,Gelatin silver print,13.1 x 12.8 cm (5 3/16 x 5 1/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs|Collages,,http://www.metmuseum.org/art/collection/search/265543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.551.1,false,true,283188,Photographs,Album,"Rogues, a Study of Characters",,,,,,Artist,,Samuel G. Szabó,"Hungarian, active America ca. 1854–61",,"Szabó, Samuel G.",Hungarian,1854,0061,1857,1850,1860,Salted paper prints from glass negatives,From 8.8 x 6.6 cm (3 7/16 x 2 5/8 in.) to 11.5 x 8.8 cm (4 1/2 x 3 7/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/283188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.3,false,true,283074,Photographs,Negative; Photograph,[Young Man],,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,late 1840s,1846,1849,Paper negative,20.9 x 15.8 cm (8 1/4 x 6 1/4 in.),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/283074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.243,false,true,267653,Photographs,Photomechanical print,The Church or the World,,,,,,Artist,,James Craig Annan,"British, Hamilton, South Lanarkshire, Scotland 1864–1946",,"Annan, James Craig","British, Scottish",1864,1946,1893,1893,1893,Photogravure,10.5 x 12.0 cm. (4 1/8 x 4 3/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/267653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.268,false,true,269385,Photographs,Photomechanical print,Toledo,,,,,,Artist,,James Craig Annan,"British, Hamilton, South Lanarkshire, Scotland 1864–1946",,"Annan, James Craig","British, Scottish",1864,1946,1914,1914,1914,Photogravure,19.9 x 13.4 cm. (7 13/16 x 5 1/4 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/269385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.274,false,true,269392,Photographs,Photomechanical print,"The Riva Schiavoni, Venice",,,,,,Artist,,James Craig Annan,"British, Hamilton, South Lanarkshire, Scotland 1864–1946",,"Annan, James Craig","British, Scottish",1864,1946,1894,1894,1894,Photogravure,14.3 x 19.9 cm. (5 5/8 x 7 13/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/269392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.1–.12,false,true,269582,Photographs,Photographically illustrated book,"Alfred Tennyson's Idylls of the King, and other Poems",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver prints,"45.4 x 35.5 x 2.4 cm (17 7/8 x 14 x 15/16 in.), closed","David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/269582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.634.16,false,true,269947,Photographs,Autochrome,[Pan-Pacific International Exposition],,,,,,Artist,,Arnold Genthe,"American (born Germany), Berlin 1869–1942 New Milford, Connecticut",,"Genthe, Arnold","American, born Germany",1869,1942,1915,1915,1915,Autochrome,12.7 x 17.9 cm (5 x 7 1/16 in.),"Gift of Mrs. Robert Aitken, 1957",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/269947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1134.1,false,true,266025,Photographs,Waxed paper negative,"The Diwan-i Khas from the Mussaman Burj, Agra Palace",,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1862–64,1862,1864,Waxed paper negative with applied media,37.1 x 46.4cm (14 5/8 x 18 1/4in.) Frame: 68.7 x 122.6 cm (27 1/16 x 48 1/4 in.) (Framed with 1988.1134.2),"Purchase, Cynthia Hazen Polsky Gift, 1988",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/266025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.57,false,true,282068,Photographs,Waxed paper negative,Taj Mahal and Gardens,,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,ca. 1855,1853,1857,Waxed paper negative,38.5 x 47.2 cm (15 3/16 x 18 9/16 in.),"The Rubel Collection, Purchase, Anonymous Gift and Cynthia Hazen Polsky Gift, 1997",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/282068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.946,false,true,287604,Photographs,Negative; Photograph,Suttee Ghat Cawnpore,,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1858,1858,1858,Waxed paper negative,Image: 38 x 48 cm (14 15/16 x 18 7/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/287604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1224,false,true,305839,Photographs,Print,Planning the Capture of Booth and Harold,,,,,,Artist,After,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1865,1865,1865,Woodcut,,"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/305839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.594.55,false,true,260915,Photographs,Photograph,"Palais de Gézyret, Pavillon Exterieur",,,,,,Artist,,J. Pascal Sébah,Turkish,,"Sébah, J. Pascal",Turkish,,1890,1870s,1870,1879,Albumen silver print from glass negative,20.0 x 26.7 cm (7 7/8 x 10 1/2 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.12,false,true,269572,Photographs,Photograph,Chillon,,,,,,Artist,,John Joscelyn Coghill,"Irish, 1826–1905",,"Coghill, John Joscelyn",Irish,1826,1905,1855,1855,1855,Albumen silver print from glass negative,Approx. 15.2 x 20.3 cm (6 x 8 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.13,false,true,269573,Photographs,Photograph,Swiss Glacier,,,,,,Artist,,John Joscelyn Coghill,"Irish, 1826–1905",,"Coghill, John Joscelyn",Irish,1826,1905,1850s,1850,1859,Albumen silver print from glass negative,Approx. 20.3 x 15.2 cm (8 x 6 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.14,false,true,269574,Photographs,Photograph,View in Switzerland,,,,,,Artist,,John Joscelyn Coghill,"Irish, 1826–1905",,"Coghill, John Joscelyn",Irish,1826,1905,1850s,1850,1859,Albumen silver print from glass negative,Approx. 15.2 x 20.3 cm (6 x 8 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.15,false,true,269575,Photographs,Photograph,Heidelberg,,,,,,Artist,,John Joscelyn Coghill,"Irish, 1826–1905",,"Coghill, John Joscelyn",Irish,1826,1905,ca. 1855,1853,1857,Albumen silver print from glass negative,Approx. 15.2 x 20.3 cm (6 x 8 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.7,false,true,270859,Photographs,Photograph,The Castle of Chillon,,,,,,Artist,,John Joscelyn Coghill,"Irish, 1826–1905",,"Coghill, John Joscelyn",Irish,1826,1905,1855,1855,1855,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.112,false,true,685454,Photographs,Carte-de-visite,[Alexander Calame],,,,,,Artist,,Vuagnat,"Swiss, active 1860s",,Vuagnat,Swiss,1859,1870,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1041,false,true,266108,Photographs,Photograph,"[Doorway of Predikheevenkerk, Gent, Belgium]",,,,,,Artist,,Charles D'Hoy,"Belgian, 1823–1895",,"D'Hoy, Charles",Belgian,1823,1895,ca. 1858,1856,1860,Albumen silver print from glass negative,23.0 x 27.9 cm. (9 1/16 x 11 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.652,false,true,286263,Photographs,X-Ray,Le pied à travers la chaussure,,,,,,Artist,,Dr. Henri van Heurck,"Belgian, 1838–1909",,"Van Heurck, Dr. Henri",Belgian,1838,1909,1896,1896,1896,Gelatin silver print,Image: 16.2 × 12.1 cm (6 3/8 × 4 3/4 in.) Mount: 18.4 × 13.4 cm (7 1/4 × 5 1/4 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.653,false,true,286772,Photographs,X-Ray,[X-Ray of the Mummy of a Raptor],,,,,,Artist,,Dr. Henri van Heurck,"Belgian, 1838–1909",,"Van Heurck, Dr. Henri",Belgian,1838,1909,1896,1896,1896,Gelatin silver print,Image: 6 3/4 × 4 3/4 in. (17.1 × 12.1 cm),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.654,false,true,287290,Photographs,X-Ray,[X-Ray of a Box Compasses],,,,,,Artist,,Dr. Henri van Heurck,"Belgian, 1838–1909",,"Van Heurck, Dr. Henri",Belgian,1838,1909,1896,1896,1896,Gelatin silver print,Image: 17.2 x 11.9 cm (6 3/4 x 4 11/16 in.) Mount: 18 x 13 cm (7 1/16 x 5 1/8 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.17,false,true,289289,Photographs,Photograph,Piled Stone Mountain Near Sing Chang,,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",Chinese,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 11 5/16 in. (20.7 × 28.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.26,false,true,289298,Photographs,Photograph,Hisiu Peak,,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",Chinese,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 in. × 11 7/16 in. (22.8 × 29 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.37,false,true,289309,Photographs,Photograph,Duck Market,,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",Chinese,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 11 5/16 in. (20.7 × 28.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.39,false,true,289311,Photographs,Photograph,[Group of People Posing near River],,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",Chinese,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 3/16 × 9 1/2 in. (18.2 × 24.1 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.627.1.16,false,true,306151,Photographs,"Photograph, Carte-de-visite",Empress Eugénie,,,,,,Artist,,Sergei Luvovich Levitsky,"Russian, 1819–1898",,"Levitsky, Sergei Luvovich",Russian,1819,1898,ca. 1864,1859,1869,Albumen silver print,Sheet: 10.5 × 6 cm (4 1/8 × 2 3/8 in.),"Gift of Susanna Myers, 1953",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.627.1.17,false,true,306152,Photographs,"Photograph, Carte-de-visite",Empress Eugénie,,,,,,Artist,,Sergei Luvovich Levitsky,"Russian, 1819–1898",,"Levitsky, Sergei Luvovich",Russian,1819,1898,ca. 1864,1859,1869,Albumen silver print,Sheet: 10.5 × 6 cm (4 1/8 × 2 3/8 in.),"Gift of Susanna Myers, 1953",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1152,false,true,285989,Photographs,Photographs,[Two Young Women at Window],,,,,,Artist,,Andrei Osipovich Karelin,"Russian, 1837–1906",,"Karelin, Andrei Osipovich",Russian,1837,1906,ca. 1870,1868,1872,Albumen silver print from glass negative,Image: 8 1/2 × 6 1/8 in. (21.6 × 15.6 cm) Mount: 16 7/16 × 12 5/8 in. (41.7 × 32 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.14,false,true,283086,Photographs,Photograph,"The Hippopotamus at the Zoological Gardens, Regent's Park",,,,,,Artist,,"de Borbón, Juan","Spanish, 1822–1887",,"de Borbón, Juan",Spanish,1822,1887,1852,1852,1852,Salted paper print from glass negative,Image: 11.1 × 12 cm (4 3/8 × 4 3/4 in.) Mount: 43.8 × 30.5 cm (17 1/4 × 12 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.1,false,true,269224,Photographs,Photograph,Gröfin Auersperg,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,21.8 x 17.2 cm. (8 9/16 x 6 3/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.2,false,true,269235,Photographs,Photograph,Gröfin Auersperg,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,24.7 x 19.6 cm. (9 3/4 x 7 3/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.3,false,true,269246,Photographs,Photograph,Gröfin Auersperg,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,25.2 x 19.6 cm. (9 15/16 x 7 3/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.4,false,true,269257,Photographs,Photograph,[Seated Man with Cane and Hat],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,38.2 x 28.7 cm. (15 1/16 x 11 5/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.5,false,true,269263,Photographs,Photograph,[Seated Man in White Vest and Dark Coat],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,25.5 x 21.5 cm. (10 1/16 x 8 7/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.6,false,true,269264,Photographs,Photograph,[Seated Man in White Vest and Dark Coat],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,24.9 x 21.8 cm. (9 13/16 x 8 9/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.7,false,true,269265,Photographs,Photograph,"Betty Held, vereh. Solön-Engelsberg",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,25.1 x 19.4 cm. (9 7/8 x 7 5/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.8,false,true,269266,Photographs,Photograph,[Portrait of an Elderly Woman],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,16.7 x 13.4 cm. (6 9/16 x 5 1/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.9,false,true,269267,Photographs,Photograph,"Marie Antoine, geb. Woes",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,23.8 x 21.2 cm. (9 3/8 x 8 3/6 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.10,false,true,269225,Photographs,Photograph,[Man Seated in Armchair],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,22.5 x 16.8 cm. (8 7/8 x 6 5/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.11,false,true,269226,Photographs,Photograph,[Young Woman in Dotted Dress],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,22.0 x 17.3 cm. (8 11/16 x 6 13/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.12,false,true,269227,Photographs,Photograph,Elisabeth Höusermann,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,25.4 x 19.3 cm. (10 x 7 5/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.14,false,true,269229,Photographs,Photograph,Mathias Höusermann,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,18.7 x 14.8 cm. (7 3/8 x 5 13/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.15,false,true,269230,Photographs,Photograph,Mathias und Elise Höusermann,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,28.5 x 24.3 cm. (11 1/4 x 9 9/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.17,false,true,269232,Photographs,Photograph,[Group Portrait of Five Adults and Two Children in a Garden],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,28.3 x 34.3 cm. (11 1/8 x 13 1/2 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.18,false,true,269233,Photographs,Photograph,[Group Portrait of Six People],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.19,false,true,269234,Photographs,Photograph,"[Group Portrait of Four Women, Two Men and Three Children in a Garden]",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,2.3 x 37.8 cm. (0 15/16 x 14 7/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.20,false,true,269236,Photographs,Photograph,[Group portrait of the Antoine and Höusermann Families],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,31.2 x 36.4 cm. (12 1/4 x 14 5/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.22,false,true,269238,Photographs,Photograph,[Mathias Höusermann seated with elbow on pedestal holding a vase of flowers],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,37.3 x 30.5 cm. (14 11/16 x 12 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.23,false,true,269239,Photographs,Photograph,"Anna Wöss, Marie and Marie Antoine",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,24.5 x 19.5 cm. (9 5/8 x 7 11/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.24,false,true,269240,Photographs,Photograph,[Group Portrait of the Antoine and Höusermann Families],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,28.0 x 32.3 cm. (11 x 12 3/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.25,false,true,269241,Photographs,Photograph,[Group Portrait of Four Women and Three Children],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,22.6 x 19.2 cm. (8 7/8 x 7 9/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.26,false,true,269242,Photographs,Photograph,[Seated Lady in Striped Dress with Four Little Girls],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Coated salted paper print from glass negative,15.5 x 19.4 cm. (6 1/8 x 7 5/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.27,false,true,269243,Photographs,Photograph,"Elise Höusermann, Hermine, Marie and Marie Antoine",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,19.0 x 21.9 cm. (7 1/2 x 8 5/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.28,false,true,269244,Photographs,Photograph,"[Three Women,Two Men, and a Child on a Picnic]",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,16.1 x 21.1 cm. (6 5/16 x 8 5/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.29,false,true,269245,Photographs,Photograph,"[Hermine, Alfons and Eugen Antoine and Mathias Höusermann on a fallen tree]",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,18.6 x 23.3 cm. (7 5/16 x 9 3/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.30,false,true,269247,Photographs,Photograph,Frau Hofrat Josefine Raymond,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,28.5 x 21.5 cm. (11 1/4 x 8 7/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.31,false,true,269248,Photographs,Photograph,Hofrat Raymond,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,34.9 x 25.0 cm. (13 3/4 x 9 13/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.33,false,true,269250,Photographs,Photograph,"[Mathias Höusermann, Marie Antoine, Elise Höusermann, and Pepe Wöss]",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.34,false,true,269251,Photographs,Photograph,[Portrait of Three Women and Men in a Garden],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,23.8 x 19.0 cm (9 3/8 x 7 1/2 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.35,false,true,269252,Photographs,Photograph,"Alfons, Eugen, Marie, and Hermine Antoine",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,23.1 x 18.5 cm. (9 1/16 x 7 1/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.36,false,true,269253,Photographs,Photograph,"Hermine, Marie and Marie Antoine.",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,15.0 x 11.1 cm. (5 15/16 x 4 3/8 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.37,false,true,269254,Photographs,Photograph,Hermine and Marie Antoine,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,19.2 x 11.1 cm (7 9/16 x 4 3/8 in. ),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.38,false,true,269255,Photographs,Photograph,Marie Antoine (Wöss),,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,11.3 x 8.8 cm. (4 7/16 x 3 7/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.39,false,true,269256,Photographs,Photograph,"[Female Portrait, Standing, Looking Left]",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,22.4 x 14.4 cm. (8 13/16 x 5 11/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.40,false,true,269258,Photographs,Photograph,[Portrait of Two Girls],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,20.4 x 14.5 cm. (8 x 5 11/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.41,false,true,269259,Photographs,Photograph,"[Portrait of a Seated Woman Surrounded by Five Girls, Seated and Standing]",,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,18.4 x 13.1 cm. (7 1/4 x 5 3/16 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.42,false,true,269260,Photographs,Photograph,[Ten members of the Antoine family],,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,19.2 x 26.1 cm. (7 9/16 x 10 1/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.83.44,false,true,269262,Photographs,Photograph,Mathias Höusermann,,,,,,Artist,,Franz Antoine,"Austrian, 1814–1882",,"Antoine, Franz",Austrian,1814,1882,1850s–60s,1850,1869,Albumen silver print from glass negative,36.9 x 32.3 cm. (14 1/2 x 12 3/4 in.),"David Hunter McAlpin Fund, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.108,false,true,301891,Photographs,Micrograph,[Frustules of Diatoms],,,,,,Artist,Attributed to,Julius Wiesner,"Austrian, 1838–1916",,"Wiesner, Julius",Austrian,1838,1916,ca. 1870,1865,1875,Cyanotype,9.8 x 7.9 cm (3 7/8 x 3 1/8 in.),"Purchase, Steven Ames Gift, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (1a),false,true,288477,Photographs,Photograph,[Chinese Gentleman],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.1 cm (9 5/16 x 7 1/2 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (2a),false,true,288481,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (3a),false,true,283171,Photographs,Photograph,Interprete de la Legation for the Austro Hongroise,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (4a),false,true,288482,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (5a),false,true,288012,Photographs,Photograph,Soldat de la ligne,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (6a),false,true,288013,Photographs,Photograph,Fille de lanxchow,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (7a),false,true,288485,Photographs,Photograph,[Young Chinese Gentleman],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,25.7 x 19.3 cm (10 1/8 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (8a),false,true,288487,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (9a),false,true,288489,Photographs,Photograph,Vieux mendiant,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (10a),false,true,288491,Photographs,Photograph,Vielle mendiant,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (11a),false,true,288493,Photographs,Photograph,[Chinese Man],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.4 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (12a),false,true,288495,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (13a),false,true,288497,Photographs,Photograph,Vieux Chinoise de Canton,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (14a),false,true,288499,Photographs,Photograph,[Woman from Canton],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (15a),false,true,288501,Photographs,Photograph,Fille de Lanxchow,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (16a),false,true,288503,Photographs,Photograph,Fille de Lanxchow,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (17a),false,true,288506,Photographs,Photograph,Fille de Lanxchow,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (18a),false,true,288508,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (19a),false,true,288510,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (20a),false,true,288512,Photographs,Photograph,Fille de Lanxchow,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (21a),false,true,288014,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (22a),false,true,288515,Photographs,Photograph,[Portrait of an Old Chinese Woman],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (23a),false,true,288517,Photographs,Photograph,[Seated Chinese Woman with Fan],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (24a),false,true,288519,Photographs,Photograph,Femme de Canton,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (25a),false,true,288521,Photographs,Photograph,Négociant,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (26a),false,true,288523,Photographs,Photograph,Fille de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (27a),false,true,288525,Photographs,Photograph,[Two Chinese Men in Matching Traditional Dress],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.6 x 19.3 cm (9 5/16 x 7 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (28a),false,true,288015,Photographs,Photograph,Filles de Shanghai,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (29a),false,true,288528,Photographs,Photograph,[Chinese Man Wearing Hat],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (30a),false,true,288016,Photographs,Photograph,[Chinese Woman Sitting with Basket],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (41a),false,true,288019,Photographs,Photograph,Brouette,,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1870s,1870,1879,Albumen silver print from glass negative,23.7 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.405,false,true,267821,Photographs,Photograph,Sheep,,,,,,Artist,,Hans Watzek,"Austrian, 1848–1903",,"Watzek, Hans",Austrian,1848,1903,1901,1901,1901,Gum bichromate print,50.2 x 63.6 cm (19 3/4 x 25 1/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.411,false,true,267828,Photographs,Photograph,[Italian Villa in Autumn],,,,,,Artist,,Hugo Henneberg,"Austrian, 1863–1918",,"Henneberg, Hugo",Austrian,1863,1918,1898,1898,1898,Gum bichromate print,54.0 x 74.5 cm (21 1/4 x 29 5/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.412,false,true,267829,Photographs,Photograph,Motiv aus Pommern,,,,,,Artist,,Hugo Henneberg,"Austrian, 1863–1918",,"Henneberg, Hugo",Austrian,1863,1918,"1895–96, printed 1902",1895,1896,Gum bichromate print,77.7 x 55.5 cm (30 9/16 x 21 7/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.413,false,true,267830,Photographs,Photograph,Pflügen,,,,,,Artist,,Hugo Henneberg,"Austrian, 1863–1918",,"Henneberg, Hugo",Austrian,1863,1918,"1890–1901, printed 1903",1890,1901,Gum bichromate print,65.8 x 95.8 cm. (25 15/16 x 37 3/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.579,false,true,285639,Photographs,Photograph,Egon Schiele,,,,,,Artist,,Anton Joseph Trcka,"Austrian, 1893–1940",,"Trcka, Anton Joseph",Austrian,1893,1940,1914,1914,1914,Gelatin silver print,Image: 22.6 x 15.3 cm (8 7/8 x 6 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.72,false,true,685414,Photographs,Carte-de-visite,[George Henry Boughton],,,,,,Artist,,Oliver François Xavier Sarony,"Canadian, 1820–1879",,"Sarony, Oliver François Xavier",Canadian,1820,1879,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.320,false,true,685661,Photographs,Carte-de-visite,[William Powell Frith],,,,,,Artist,,Oliver François Xavier Sarony,"Canadian, 1820–1879",,"Sarony, Oliver François Xavier",Canadian,1820,1879,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.288.1,false,true,283777,Photographs,Photograph,"[Claudet Family Group, Chateau de la Roche, Amboise]",,,,,,Artist,,Francis George Claudet,"Canadian, 1837–1906",,"Claudet, Francis George",Canadian,1837,1906,1856,1856,1856,Salted paper print from glass negative,17.5 x 13.4 cm (6 7/8 x 5 1/4 in.) visible,"Gift of Georgina Claudet Gilchrist and Frances Claudet Johnson, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.767,false,true,686106,Photographs,Carte-de-visite,[Sanford Thayer],,,,,,Artist,,G. J. Wood,"American?, active 1860s",,"Wood, G. J.",American?,1859,1870,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1098.5,false,true,631029,Photographs,Photograph,Miss Patricia Morris,,,,,,Artist,,Hill and Adamson,"British, active 1843–1848",,Hill and Adamson,"British, Scottish",1843,1848,1843–47,1843,1847,Salted paper print,Image: 8 3/8 × 6 1/8 in. (21.3 × 15.6 cm) Mount: 14 3/4 × 10 3/8 in. (37.5 × 26.4 cm),"Gift of Joyce F. Menschel, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/631029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.599.1a,false,true,633310,Photographs,Photograph,The Great Elephant Saluting,,,,,,Artist,,Lala Deen Dayal,"Indian, Sardhana 1844–1905",,"Dayal, Lala Deen",Indian,1844,1905,1885–1900,1885,1900,Albumen silver print from glass negative,Image: 21.1 x 27.3 cm (8 5/16 x 10 3/4 in.) Mount: 30 x 37.7 cm (11 13/16 x 14 13/16 in.),"Gift of Cynthia Hazen Polsky, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/633310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.599.1b,false,true,291060,Photographs,Photograph,The Great Elephant,,,,,,Artist,,Lala Deen Dayal,"Indian, Sardhana 1844–1905",,"Dayal, Lala Deen",Indian,1844,1905,1885–1900,1885,1900,Albumen silver print from glass negative,Image: 24.1 x 21.2 cm (9 1/2 x 8 3/8 in.) Mount: 30 x 37.7 cm (11 13/16 x 14 13/16 in.),"Gift of Cynthia Hazen Polsky, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.599.2a,false,true,291679,Photographs,Photograph,Sookh-Vilas Palace Garden,,,,,,Artist,,Lala Deen Dayal,"Indian, Sardhana 1844–1905",,"Dayal, Lala Deen",Indian,1844,1905,1880–90,1880,1890,Albumen silver print from glass negative,Image: 20.3 x 27.5 cm (8 x 10 13/16 in.) Mount: 30 x 37.7 cm (11 13/16 x 14 13/16 in.),"Gift of Cynthia Hazen Polsky, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.599.2b,false,true,633313,Photographs,Photograph,Sookh-Vilas Palace Garden,,,,,,Artist,,Lala Deen Dayal,"Indian, Sardhana 1844–1905",,"Dayal, Lala Deen",Indian,1844,1905,1880–90,1880,1890,Albumen silver print from glass negative,Image: 20.4 x 26.2 cm (8 1/16 x 10 5/16 in.) Mount: 30 x 37.7 cm (11 13/16 x 14 13/16 in.),"Gift of Cynthia Hazen Polsky, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/633313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.368,false,true,267262,Photographs,Photograph,"Hunford Mill, Surrey",,,,,,Artist,,Henry White,"British, Scotland 1819–1903",,"White, Henry","British, Scottish",1819,1903,1855–57,1855,1857,Albumen silver print from glass negative,Image: 19.4 x 23.8 cm (7 5/8 x 9 3/8 in.) Mount: 43.4 x 59.9 cm (17 1/16 x 23 9/16 in.),"Purchase, Mrs. Harrison D. Horblit Gift, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.434,false,true,283227,Photographs,Photograph,Poling the Marsh Hay,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,1886,1886,1886,Platinum print from glass negative,Image: 23.2 x 29 cm (9 1/8 x 11 7/16 in.) Mount: 28.6 x 40.9 cm (11 1/4 x 16 1/8 in.) Sheet ((Interleaving Plate Sheet)): 28 x 40.7 cm (11 x 16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.722,false,true,286469,Photographs,Photograph,Setting the Bownet,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,1886,1886,1886,Platinum print from glass negative,Image: 16.4 x 28.9 cm (6 7/16 x 11 3/8 in.) Mount: 28.6 x 41.1 cm (11 1/4 x 16 3/16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.726,false,true,286418,Photographs,Photograph,Rowing Home the Schoof-Stuff,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,1886,1886,1886,Platinum print from glass negative,Image: 13.8 x 27.9 cm (5 7/16 x 11 in.) Mount: 28.6 x 41 cm (11 1/4 x 16 1/8 in.) Sheet (Interleaving Plate Sheet): 28.2 x 40.8 cm (11 1/8 x 16 1/16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.741,false,true,286671,Photographs,Photograph,Gathering Water-Lilies,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,1886,1886,1886,Platinum print from glass negative,Image: 19.7 x 29.2 cm (7 3/4 x 11 1/2 in.) Mount: 28.5 x 40.9 cm (11 1/4 x 16 1/8 in.) Sheet ((Interleaving Plate Sheet)): 28.1 x 40.6 cm (11 1/16 x 16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.742,false,true,286673,Photographs,Photograph,Gunner Working Up To Fowl,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,1886,1886,1886,Platinum print from glass negative,Image: 18.9 x 28.8 cm (7 7/16 x 11 5/16 in.) Mount: 28.6 x 40.9 cm (11 1/4 x 16 1/8 in.) Sheet ((Interleaving Plate Sheet)): 28.2 x 40.7 cm (11 1/8 x 16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.743,false,true,286676,Photographs,Photograph,Cantley: Wherries Waiting for the Turn of the Tide,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,1886,1886,1886,Platinum print from glass negative,Image: 18.9 x 28.5 cm (7 7/16 x 11 1/4 in.) Mount: 28.6 x 40.9 cm (11 1/4 x 16 1/8 in.) Sheet ((Interleaving Plate Sheet)): 28.2 x 41 cm (11 1/8 x 16 1/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.744,false,true,286677,Photographs,Photograph,Water-Lilies,,,,,,Artist,,Peter Henry Emerson,"British, born Cuba, 1856–1936",,"Emerson, Peter Henry","British, born Cuba",1856,1936,1886,1886,1886,Platinum print from glass negative,Image: 12.3 x 28 cm (4 13/16 x 11 in.) Mount: 28.7 x 41 cm (11 5/16 x 16 1/8 in.) Sheet (Interleaving Plate Sheet): 28 x 40.8 cm (11 x 16 1/16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.10,false,true,291755,Photographs,Daguerreotype,[Elderly Man Holding Ivory-topped Walking Stick],,,,,,Artist,,John Plumbe Jr.,"American, born Wales, 1809–1857",,"Plumbe Jr., John","American, born Wales",1809,1857,1840s,1840,1849,Daguerreotype,Image: 6.7 x 5.5 cm (2 5/8 x 2 3/16 in.) Plate: 8.3 x 7 cm (3 1/4 x 2 3/4 in.) Case: 1.6 x 9.2 x 8.1 cm (5/8 x 3 5/8 x 3 3/16 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.32,false,true,291785,Photographs,Daguerreotype,[Man with Chinstrap Beard],,,,,,Artist,,John Plumbe Jr.,"American, born Wales, 1809–1857",,"Plumbe Jr., John","American, born Wales",1809,1857,1840s,1840,1849,Daguerreotype,Image: 7.6 x 6.6 cm (3 x 2 5/8 in.) Plate: 8.3 x 7 cm (3 1/4 x 2 3/4 in.) Case: 1.6 x 9.5 x 8.1 cm (5/8 x 3 3/4 x 3 3/16 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.60,false,true,291813,Photographs,Daguerreotype,"Elizabeth Page Bakewell and her Grandson, Frank B. James",,,,,,Artist,,John Plumbe Jr.,"American, born Wales, 1809–1857",,"Plumbe Jr., John","American, born Wales",1809,1857,ca. 1846,1845,1848,Daguerreotype,Image: 12 x 9.1 cm (4 3/4 x 3 9/16 in.) Frame: 34.6 x 31.1 cm (13 5/8 x 12 1/4 in.) Mat: 18.1 x 14.6 cm (7 1/8 x 5 3/4 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.61,false,true,291814,Photographs,Daguerreotype,"Elizabeth Bakewell James and her Son, Frank B. James",,,,,,Artist,Attributed to,John Plumbe Jr.,"American, born Wales, 1809–1857",,"Plumbe Jr., John","American, born Wales",1809,1857,ca. 1846,1845,1848,Daguerreotype,Image: 11.9 x 8.9 cm (4 11/16 x 3 1/2 in.) Mat: 18.4 x 14.9 cm (7 1/4 x 5 7/8 in.) Frame: 34.8 x 31.3 cm (13 11/16 x 12 5/16 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.804,false,true,286155,Photographs,Photograph,Dr. Edward Livingston,,,,,,Artist,Attributed to,John Plumbe Jr.,"American, born Wales, 1809–1857",,"Plumbe Jr., John","American, born Wales",1809,1857,ca. 1841,1839,1843,Daguerreotype,Overall: 4 11/16 × 3 11/16 in. (11.9 × 9.4 cm) Image: 3 9/16 × 2 5/8 in. (9.1 × 6.7 cm); visible,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.17.1.216,false,true,282222,Photographs,Photograph,[View of the rooftops and cathedral of Vienna],,,,,,Artist,,Alois Auer,"Austrian, Wels 1813–1869 Vienna",,"Auer, Alois",Austrian,1813,1869,ca. 1853,1851,1855,Albumen silver print,,"Rogers Fund, 1918",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.17.1.217,false,true,259569,Photographs,Micrograph,[Microscopic view of an insect],,,,,,Artist,,Alois Auer,"Austrian, Wels 1813–1869 Vienna",,"Auer, Alois",Austrian,1813,1869,ca. 1853,1851,1855,Albumen silver print,,"Rogers Fund, 1918",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.1,false,true,270438,Photographs,Photograph,[Jerusalem],,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1860s,1860,1869,Albumen silver print from glass negative,Image: 16.3 x 21.8 cm (6 7/16 x 8 9/16 in.) Mount: 27.6 x 37.3 cm (10 7/8 x 14 11/16 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.3,false,true,270441,Photographs,Photograph,[Garden of Gethsemane and View of Jerusalem],,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1860s,1860,1869,Albumen silver print from glass negative,Image: 16.8 x 21.4 cm (6 5/8 x 8 7/16 in.) Mount: 27.8 x 37.2 cm (10 15/16 x 14 5/8 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.4,false,true,270442,Photographs,Photograph,[Jerusalem],,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1857,1857,1857,Albumen silver print from glass negative,Image: 13.6 x 17 cm (5 3/8 x 6 11/16 in.) Mount: 27.9 x 37.3 cm (11 x 14 11/16 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.6,false,true,270443,Photographs,Photograph,"Jerusalem, Court of the Mosque of Omar",,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1857,1857,1857,Albumen silver print from glass negative,Image: 16.9 x 21.4 cm (6 5/8 x 8 7/16 in.) Mount: 28 x 37.5 cm (11 x 14 3/4 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.7,false,true,270444,Photographs,Photograph,"[Dome of the Holy Sepulchre, Jerusalem]",,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1860s,1860,1869,Albumen silver print from glass negative,Image: 13.9 x 17 cm (5 1/2 x 6 11/16 in.) Mount: 27.7 x 37.3 cm (10 7/8 x 14 11/16 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.9,false,true,270445,Photographs,Photograph,"[Garden of Gethsemane and the Tomb of the Virgin, Jerusalem]",,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1860s,1860,1869,Albumen silver print from glass negative,Image: 12.8 x 16.2 cm (5 1/16 x 6 3/8 in.) Mount: 27.7 x 37.5 cm (10 7/8 x 14 3/4 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.2,false,true,270834,Photographs,Photograph,"Jerusalem, Site of the Temple on Mount Moriah",,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1857,1857,1857,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.10,false,true,270439,Photographs,Photograph,"[Tomb of the Virgin, Jerusalem]",,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1860s,1860,1869,Albumen silver print from glass negative,Image: 13.9 x 17.2 cm (5 1/2 x 6 3/4 in.) Mount: 27.7 x 37.3 cm (10 7/8 x 14 11/16 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.641.2.11,false,true,270440,Photographs,Photograph,"[Tomb of Absalom, Zacharias, and St. James]",,,,,,Artist,,John Anthony,"British, born France, 1823–1901",,"Anthony, John","British, born France",1823,1901,1860s,1860,1869,Albumen silver print from glass negative,Image: 15.9 x 21.3 cm (6 1/4 x 8 3/8 in.) Mount: 27.7 x 37.3 cm (10 7/8 x 14 11/16 in.),"Gift of A. Hyatt Mayor, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.45,false,true,268988,Photographs,Photograph,Cecily Hamilton,,,,,,Artist,,Oscar Gustav Rejlander,"British, born Sweden, 1813–1875",,"Rejlander, Oscar Gustav","British, born Sweden",1813,1875,1863–1867,1863,1867,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.24,false,true,283096,Photographs,Photograph,Mr. and Miss Constable,,,,,,Artist,,Oscar Gustav Rejlander,"British, born Sweden, 1813–1875",,"Rejlander, Oscar Gustav","British, born Sweden",1813,1875,1866,1866,1866,Albumen silver print from glass negative,Image: 16.8 x 22.1 cm (6 5/8 x 8 11/16 in.) Mount: 21.2 x 28.9 cm (8 3/8 x 11 3/8 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.47,false,true,306330,Photographs,Photograph,His Country's Hope,,,,,,Artist,,Oscar Gustav Rejlander,"British, born Sweden, 1813–1875",,"Rejlander, Oscar Gustav","British, born Sweden",1813,1875,1850s,1850,1859,Albumen silver print from glass negative,Mount: 10 7/8 in. × 13 7/8 in. (27.6 × 35.2 cm) Image: 6 3/16 in. × 8 in. (15.7 × 20.3 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.33,false,true,270849,Photographs,Photograph,Study of a Head,,,,,,Artist,,Oscar Gustav Rejlander,"British, born Sweden, 1813–1875",,"Rejlander, Oscar Gustav","British, born Sweden",1813,1875,1857,1857,1857,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1106,false,true,285657,Photographs,Photograph,The Scholar's Mate,,,,,,Artist,,Oscar Gustav Rejlander,"British, born Sweden, 1813–1875",,"Rejlander, Oscar Gustav","British, born Sweden",1813,1875,1857–59,1857,1859,Albumen silver print,Mount: 16 in. × 12 7/8 in. (40.6 × 32.7 cm) Image: 7 15/16 × 6 9/16 in. (20.2 × 16.7 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1164,false,true,285658,Photographs,Photograph,Ariadne,,,,,,Artist,,Oscar Gustav Rejlander,"British, born Sweden, 1813–1875",,"Rejlander, Oscar Gustav","British, born Sweden",1813,1875,1857,1857,1857,Albumen silver print from glass negative,Mount: 16 1/16 in. × 13 1/16 in. (40.8 × 33.2 cm) Image: 8 1/4 × 6 1/2 in. (21 × 16.5 cm); oval,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.3,false,true,283627,Photographs,Photograph,David D'Angers,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1853,1853,1853,Salted paper print from paper negative,18.9 x 15 cm (7 7/16 x 5 7/8 in. ),"Purchase, Jennifer and Joseph Duke and Harriette and Noel Levine Gifts, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.52,false,true,271912,Photographs,Photograph,"The Floods of 1856, Avignon",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1856,1856,1856,Salted paper print from paper negative,30.4 x 43.8 cm (11 15/16 x 17 1/4 in. ),"Purchase, Robert Hurst, Paul F. Walter and Anonymous Gifts; Harris Brisbane Dick Fund, Rogers Fund and Gift of Mrs. Claire K. Feins, in memory of Daniel M. Feins and Linda S. Silverman, by exchange; and funds from various donors, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.67,false,true,290462,Photographs,Photograph,Village de Murols,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1854,1854,1854,Salted paper print from paper negative,Image: 33 x 43.5 cm (13 x 17 1/8 in.) Mount: 47.6 x 62.4 cm (18 3/4 x 24 9/16 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.55.1,false,true,268839,Photographs,Photograph,Panorama de la Cité,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1860s,1860,1869,Albumen silver print from glass negative,20.3 x 28.3 cm. (8 x 11 1/8 in.),"David Hunter McAlpin Fund, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.55.4,false,true,268863,Photographs,Photograph,Notre-Dame (Abside),,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1860s,1860,1869,Albumen silver print from glass negative,21.7 x 28.6 cm. (8 9/16 x 11 1/4 in.),"David Hunter McAlpin Fund, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.55.6,false,true,268865,Photographs,Photograph,Arc de triomphe de l'Ètoile,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1860s,1860,1869,Albumen silver print from glass negative,21.1 x 27.0 cm. (8 5/16 x 10 5/8 in.),"David Hunter McAlpin Fund, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.55.7,false,true,268866,Photographs,Photograph,Notre-Dame (façade),,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1860s,1860,1869,Albumen silver print from glass negative,27.6 x 21.1 cm. (10 7/8 x 8 5/16 in.),"David Hunter McAlpin Fund, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.137,false,true,267002,Photographs,Photograph,[Imperial Library of the Louvre],,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1856–57,1856,1857,Salted paper print from glass negative,43.9 x 34.2 cm. (17 1/4 x 13 7/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.228,false,true,282780,Photographs,Photograph,"Château of Princess Mathilde, Enghien",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1854–55,1854,1855,Salted paper print from paper negative,31.7 x 44.4 cm (12 1/2 x 17 1/2 in. ),"Purchase, Louis V. Bell Fund, by exchange, and The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, 1999",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.565,false,true,287985,Photographs,Photograph,Arc antique à Orange,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1853,1853,1853,Salted paper print from paper negative,Image: 32.4 x 42.4 cm (12 3/4 x 16 11/16 in.) Mount: 48.6 x 63.5 cm (19 1/8 x 25 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2006",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.455,false,true,288843,Photographs,Photograph,"The Floods of 1856, Church of Saint-Pothin, Lyon",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,June 1856,1856,1856,Salted paper print from paper negative,Image: 32.5 x 43.5 cm (12 13/16 x 17 1/8 in.),"Funds from various donors, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.55.23,false,true,268854,Photographs,Photograph,Panthéon,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1860s,1860,1869,Albumen silver print from glass negative,21.7 x 27.0 cm. (8 9/16 x 10 5/8 in.),"David Hunter McAlpin Fund, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.55.25,false,true,268856,Photographs,Photograph,Palais de l'Industrie,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1850s–60s,1850,1869,Albumen silver print,21.6 x 27.9 cm. (8 1/2 x 11 in.),"David Hunter McAlpin Fund, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1012,false,true,266222,Photographs,Photograph,Eglise d'Auvers,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,"1855, printed 1855–57",1855,1855,Salted paper print from paper negative,32.4 x 43.2 cm. (12 3/4 x 17 in.),"Purchase, Rogers Fund, Joyce and Robert Menschel Gift and Harriette and Noel Levine Gift, 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1127,false,true,266282,Photographs,Photograph,[Roman Arch at Orange],,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1851,1851,1851,Salted paper print from paper negative,35.3 x 26.2 cm (13 7/8 x 10 5/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, and Edward Pearce Casey Fund, 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1042,false,true,266331,Photographs,Photograph,"Pavillon de l'Horloge, Louvre",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1852–53,1852,1853,Salted paper print from paper negative,39.5 x 27.7 cm. (15 9/16 x 10 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5000,true,true,266644,Photographs,Photograph,[Entrance to the Port of Boulogne],,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1855,1855,1855,Salted paper print from paper negative,28.8 x 43.5 cm (11 5/16 x 17 1/8 in.),"Purchase, Louis V. Bell Fund, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5002,false,true,266646,Photographs,Photograph,Gare d'Enghien,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1855,1855,1855,Salted paper print from paper negative,Image: 31.3 x 44.3 cm. (12 5/16 x 17 7/16 in.),"Louis V. Bell Fund, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5003,false,true,266647,Photographs,Photograph,Pont en Royans,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1859,1857,1861,Salted paper print from paper negative,Image: 43.2 x 33.9 cm. (17 x 13 3/6 in.),"Louis V. Bell Fund, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.125.1,false,true,266882,Photographs,Photograph,Madeleine Bourquelot de Cervignieres,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1858,1858,1858,Albumen silver print from glass negative,Image: 16.0 x 13.3 cm. (6 5/16 x 5 1/4 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.125.2,false,true,266883,Photographs,Photograph,Pierre Bourquelot de Cervignieres,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1858,1858,1858,Albumen silver print from glass negative,Image: 17.4 x 13.7 cm. (6 7/8 x 5 3/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.179.1,false,true,267017,Photographs,Stereograph,"[Portal, Church of Saint-Trophime, Arles]",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1864,1862,1866,Albumen silver print from glass negative,7.0 x 14.2 cm. (2 3/4 x 5 9/16 in.),"Gift of Pierre-Marc Richard, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.179.2,false,true,267018,Photographs,Stereograph,"[Church of Saint-Honorat, Arles]",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1864,1862,1866,Albumen silver print from glass negative,7.0 x 14.2 cm. (2 3/4 x 5 9/16 in.),"Gift of Pierre-Marc Richard, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.50,false,true,283131,Photographs,Photograph,Groupe dans le parc du château de La Faloise,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1857,1857,1857,Salted paper print from glass negative,Mount: 41.6 × 55.3 cm (16 3/8 × 21 3/4 in.) Image: 27.8 × 38.2 cm (10 15/16 × 15 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.1,false,true,287310,Photographs,Photograph,Lyon,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative?,Image: 43.2 x 31.1 cm (17 x 12 1/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.2,false,true,287311,Photographs,Photograph,"Lyon, Hôtel de Ville",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 33 x 42.7 cm (13 x 16 13/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mat: 55.9 x 71.1 cm (22 x 28 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.3,false,true,287312,Photographs,Photograph,"Lyon, Gare de Perrache",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 27.9 x 43.3 cm (11 x 17 1/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.4,false,true,287313,Photographs,Photograph,Pont de la Mulatiere,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 28 x 44.6 cm (11 x 17 9/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.5,false,true,287314,Photographs,Photograph,Pont de la Mulatiere,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 28.3 x 44.3 cm (11 1/8 x 17 7/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.6,false,true,287315,Photographs,Photograph,"Lyon, Viaduc du Rhône",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 24.3 x 42.5 cm (9 9/16 x 16 3/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.7,false,true,287316,Photographs,Photograph,"Lyon, Viaduc du Rhône",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 26 x 43.3 cm (10 1/4 x 17 1/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.8,false,true,287317,Photographs,Photograph,"Givors, Viaduc",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 27.1 x 43 cm (10 11/16 x 16 15/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.9,false,true,287318,Photographs,Photograph,"Givors, Viaduc",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 27.5 x 43.9 cm (10 13/16 x 17 5/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.10,false,true,286796,Photographs,Photograph,"Vienne, Souterrain",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 32.9 x 42.3 cm (12 15/16 x 16 5/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.11,false,true,287319,Photographs,Photograph,"Vienne, St. Colombe",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 32.5 x 42.8 cm (12 13/16 x 16 7/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.12,false,true,287320,Photographs,Photograph,"Vienne, St. Jean",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 30.8 x 43.2 cm (12 1/8 x 17 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.13,false,true,287321,Photographs,Photograph,"Vienne, St. Maurice",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 42.5 x 34.1 cm (16 3/4 x 13 7/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.14,false,true,287322,Photographs,Photograph,"Vienne, Souterrain",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 33 x 43 cm (13 x 16 15/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mat: 55.9 x 71.1 cm (22 x 28 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.15,false,true,287323,Photographs,Photograph,Viaduc de l'Iser,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 26.6 x 43.7 cm (10 1/2 x 17 3/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.17,false,true,287325,Photographs,Photograph,Viaduc de la Voulte,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1861 or after,1861,1862,Albumen silver print from glass negative,Image: 25.4 x 42.5 cm (10 x 16 3/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.18,false,true,287326,Photographs,Photograph,Viaduc de la Voulte,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1861 or after,1861,1862,Albumen silver print from glass negative,Image: 30.5 x 43.2 cm (12 x 17 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.19,false,true,287327,Photographs,Photograph,Viviers,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 32.1 x 43.4 cm (12 5/8 x 17 1/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.20,false,true,283135,Photographs,Photograph,Entrée du Robinet,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 33.3 x 43.2 cm (13 1/8 x 17 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mat: 62.9 x 72.4 cm (24 3/4 x 28 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.21,false,true,287328,Photographs,Photograph,"Orange, Arc Antique",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1864,1863,1865,Albumen silver print from glass negative,Image: 21.7 x 28.3 cm (8 9/16 x 11 1/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.22,false,true,287329,Photographs,Photograph,"Orange, Théâtre Antique",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 42.7 x 33.9 cm (16 13/16 x 13 3/8 in.) Mount: 60.5 x 46 cm (23 13/16 x 18 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.23,false,true,287330,Photographs,Photograph,"Orange, Théâtre Antique",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 44.1 x 34.3 cm (17 3/8 x 13 1/2 in.) Mount: 60.5 x 46 cm (23 13/16 x 18 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.24,false,true,287331,Photographs,Photograph,Avignon,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1859,1862,Albumen silver print from glass negative?,Image: 31.8 x 42.2 cm (12 1/2 x 16 5/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.25,false,true,287332,Photographs,Photograph,"Avignon, Pont St. Bénezet",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1864,1863,1865,Albumen silver print from glass negative,Image: 21.1 x 28.5 cm (8 5/16 x 11 1/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.26,false,true,287333,Photographs,Photograph,Avignon,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1859 or after,1859,1861,Albumen silver print from glass negative,Image: 25.6 x 41.7 cm (10 1/16 x 16 7/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.27,false,true,287334,Photographs,Photograph,Villeneuve les Avignon,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1862,1861,1864,Albumen silver print from glass negative,Image: 21.6 x 28.6 cm (8 1/2 x 11 1/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.28,false,true,287335,Photographs,Photograph,"Avignon, Palais des Papes",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1859 or after,1859,1861,Albumen silver print from glass negative,Image: 33.6 x 43.4 cm (13 1/4 x 17 1/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.29,false,true,287336,Photographs,Photograph,Vaucluse,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1862,1861,1864,Albumen silver print from glass negative?,Image: 21.9 x 27.9 cm (8 5/8 x 11 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.30,false,true,287337,Photographs,Photograph,Saint-Rémy,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1862,1861,1864,Albumen silver print from glass negative,Image: 21 x 27.4 cm (8 1/4 x 10 13/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.31,false,true,287338,Photographs,Photograph,Saint-Rémy,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1862,1861,1864,Albumen silver print from glass negative?,Image: 28.6 x 21.7 cm (11 1/4 x 8 9/16 in.) Mount: 60.5 x 46 cm (23 13/16 x 18 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.32,false,true,287339,Photographs,Photograph,Pont du Gard,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 27.1 x 43.2 cm (10 11/16 x 17 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.33,false,true,287340,Photographs,Photograph,"Durance, Viaduc",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,before 1859,1855,1859,Albumen silver print from paper negative,Image: 31.8 x 53 cm (12 1/2 x 20 7/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.34,false,true,287341,Photographs,Photograph,"Tarascon, Viaduc",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 27 x 44.2 cm (10 5/8 x 17 3/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.35,false,true,287342,Photographs,Photograph,"Tarascon, Viaduc",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,before 1859,1855,1859,Albumen silver print from paper negative,Image: 37.4 x 53.2 cm (14 3/4 x 20 15/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.36,false,true,287343,Photographs,Photograph,"Tarascon, Château",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1862,1861,1864,Albumen silver print from glass negative,Image: 21.6 x 28.3 cm (8 1/2 x 11 1/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.37,false,true,287344,Photographs,Photograph,Maison Carrée à Nîmes,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1853,1853,1853,Albumen silver print from paper negative,Image: 33 x 44 cm (13 x 17 5/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.38,false,true,287345,Photographs,Photograph,"Nîmes, Amphithéâtre",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 32 x 42.4 cm (12 5/8 x 16 11/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.39,false,true,287346,Photographs,Photograph,"Nîmes, Fontaine",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 43.2 x 34 cm (17 x 13 3/8 in.) Mount: 60.5 x 46 cm (23 13/16 x 18 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.40,false,true,287347,Photographs,Photograph,"Nîmes, Temple de Diane",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1862,1861,1864,Albumen silver print from paper negative,Image: 20.9 x 29.8 cm (8 1/4 x 11 3/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.41,false,true,285452,Photographs,Photograph,"Nîmes, Tour Magne",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1853,1853,1853,Albumen silver print from paper negative,Image: 42.9 x 33 cm (16 7/8 x 13 in.) Mount: 60.5 x 46 cm (23 13/16 x 18 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.42,false,true,287348,Photographs,Photograph,"Nîmes, Porte d'Auguste",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1864,1863,1865,Albumen silver print from glass negative,Image: 21.6 x 28.1 cm (8 1/2 x 11 1/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.43,false,true,287349,Photographs,Photograph,Aigues-Mortes,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1859,1856,1862,Albumen silver print from glass negative,Image: 29.6 x 43.2 cm (11 5/8 x 17 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.44,false,true,287350,Photographs,Photograph,St. Gilles,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1853,1853,1853,Albumen silver print from paper negative,Image: 34.5 x 42.9 cm (13 9/16 x 16 7/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.45,false,true,287351,Photographs,Photograph,"Arles, St. Trophime",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1861 or after,1861,1862,Albumen silver print from glass negative,Image: 42.6 x 33.8 cm (16 3/4 x 13 5/16 in.) Mount: 60.5 x 46 cm (23 13/16 x 18 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.46,false,true,283136,Photographs,Photograph,"Arles, Cloitre St. Trophime",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 33.2 x 42.5 cm (13 1/16 x 16 3/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.47,false,true,287352,Photographs,Photograph,"Arles, Cloitre St. Trophime",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 34 x 42.7 cm (13 3/8 x 16 13/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.48,false,true,287353,Photographs,Photograph,"Arles, Amphithéâtre",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,"before June 1, 1860",1858,1860,Albumen silver print from glass negative?,Image: 33.8 x 43.2 cm (13 5/16 x 17 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.49,false,true,287354,Photographs,Photograph,"Arles, Amphithéâtre",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1860 or earlier,1857,1860,Albumen silver print from paper negative,Image: 32.3 x 42.3 cm (12 11/16 x 16 5/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.50,false,true,287355,Photographs,Photograph,Théâtre Romain à Arles,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1860 or earlier,1857,1860,Albumen silver print from paper negative,Image: 33.5 x 42.4 cm (13 3/16 x 16 11/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.51,false,true,287356,Photographs,Photograph,Montmajour,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1862,1861,1864,Albumen silver print from glass negative,Image: 19.6 x 28.1 cm (7 11/16 x 11 1/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.52,false,true,287357,Photographs,Photograph,Viaduc de St. Chamas,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,before 1859,1856,1859,Albumen silver print from paper negative,Image: 36 x 54.3 cm (14 3/16 x 21 3/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.53,false,true,287358,Photographs,Photograph,Roquefavour,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from paper negative,Image: 33.6 x 42.7 cm (13 1/4 x 16 13/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.54,false,true,287359,Photographs,Photograph,Souterrain de la Nerthe,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from paper negative,Image: 32.5 x 43.4 cm (12 13/16 x 17 1/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.55,false,true,287360,Photographs,Photograph,Marseille,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,late 1850s (?),1856,1860,Albumen silver print from paper negative,Image: 30.1 x 40.7 cm (11 7/8 x 16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.56,false,true,287361,Photographs,Photograph,Marseille,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,late 1850s (?),1856,1860,Albumen silver print from paper negative,Image: 32.6 x 42.7 cm (12 13/16 x 16 13/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.57,false,true,287362,Photographs,Photograph,Marseille,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1860,1859,1861,Albumen silver print from glass negative,Image: 27.6 x 42.9 cm (10 7/8 x 16 7/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.58,false,true,287363,Photographs,Photograph,Marseille,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 27.1 x 42.5 cm (10 11/16 x 16 3/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.59,false,true,287364,Photographs,Photograph,Marseille,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 26.7 x 40 cm (10 1/2 x 15 3/4 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.60,false,true,287365,Photographs,Photograph,Marseille,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 27.2 x 41.8 cm (10 11/16 x 16 7/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.62,false,true,287367,Photographs,Photograph,La Ciotat,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1860,1859,1861,Albumen silver print from paper negative,Image: 32.4 x 43 cm (12 3/4 x 16 15/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mount: 55.9 x 71.1 cm (22 x 28 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.63,false,true,287368,Photographs,Photograph,"La Ciotat, Bec de l'Aigle",,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from paper negative,Image: 32.1 x 42.3 cm (12 5/8 x 16 5/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mat: 55.9 x 71.1 cm (22 x 28 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287368,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.64,false,true,287283,Photographs,Photograph,Le Moine,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from paper negative,Image: 33.7 x 42.8 cm (13 1/4 x 16 7/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mat: 55.9 x 71.1 cm (22 x 28 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.65,false,true,287369,Photographs,Photograph,Bandol,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1860,1859,1861,Albumen silver print from paper negative?,Image: 33.1 x 42.9 cm (13 1/16 x 16 7/8 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mat: 55.9 x 71.1 cm (22 x 28 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.66,false,true,287370,Photographs,Photograph,Viaduc de Bandol,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1859 or after,1859,1862,Albumen silver print from paper negative,Image: 26.5 x 43 cm (10 7/16 x 16 15/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287370,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.67,false,true,287371,Photographs,Photograph,St. Nazaire,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1859 or after,1859,1862,Albumen silver print from paper negative,Image: 32 x 43 cm (12 5/8 x 16 15/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.68,false,true,287372,Photographs,Photograph,Gorges d'Ollioules,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1860,1859,1861,Albumen silver print from paper negative?,Image: 32.5 x 42 cm (12 13/16 x 16 9/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.) Mat: 55.9 x 71.1 cm (22 x 28 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.69,false,true,285453,Photographs,Photograph,Toulon,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 30.7 x 43.6 cm (12 1/16 x 17 3/16 in.) Mount: 46 x 60.5 cm (18 1/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.16.1-.2,false,true,287324,Photographs,Panorama,La Voulte,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,1861,1861,1861,Albumen silver print from glass negative,Image: 26.3 x 41.7 cm (10 3/8 x 16 7/16 in.) left Image: 26.3 x 41.4 cm (10 3/8 x 16 5/16 in.) right Mount: 46 x 121 cm (18 1/8 x 47 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.364.61.1-.2,false,true,287366,Photographs,Panorama,Marseille,,,,,,Artist,,Édouard Baldus,"French, born Prussia, 1813–1889",,"Baldus, Édouard","French, born Prussia",1813,1889,ca. 1861,1860,1862,Albumen silver print from glass negative,Image: 29.3 x 40.8 cm (11 9/16 x 16 1/16 in.) left Image: 29.3 x 40.8 cm (11 9/16 x 16 1/16 in.) right Mount: 46 x 121 cm (18 1/8 x 47 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.658.1,false,true,269641,Photographs,Photograph,[Young Male Nude Seated on Leopard Skin],,,,,,Artist,,Guglielmo Plüshow,Italian (born Germany) 1852–1930,,"Plüshow, Guglielmo","Italian, born Germany",1852,1930,1890s–1900s,1890,1909,Albumen silver print from glass negative,22.2 x 16.2 cm. (8 3/4 x 6 3/8 in.),Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.111,false,true,299472,Photographs,Photographs,"[Plaster Casts of Bodies, Pompeii]",,,,,,Artist,,Giorgio Sommer,"Italian, born Germany, 1834–1914",,"Sommer, Giorgio","Italian, born Germany",1834,1914,ca. 1875,1870,1880,Albumen print from glass negative,Image: 27.3 x 38.4 cm (10 3/4 x 15 1/8 in.) Sheet: 27.8 x 38.4 cm (10 15/16 x 15 1/8 in.) Mount: 47.8 x 55.8 cm (18 13/16 x 21 15/16 in.),"Purchase, Harriet Ames Charitable Trust Gift, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.658.2,false,true,269642,Photographs,Photograph,[Reclining Male Nude Beside Vase],,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1890s–1900s,1890,1909,Albumen silver print from glass negative,Image: 16.4 x 23.2cm (6 7/16 x 9 1/8in.) Mount: 27.9 x 35 cm (11 x 13 3/4 in.),Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.1,false,true,263857,Photographs,Photograph,"[Young Girl with Flowers, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1903,1903,1903,Albumen silver print from glass negative,21.9 x 16.9 cm. (8 5/8 x 6 5/8 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.2,false,true,263859,Photographs,Photograph,"[Elderly Man and Young Boy at Garden Fountain, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1902,1902,1902,Albumen silver print from glass negative,16.9 x 22.6 cm. (6 5/8 x 8 7/8 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.3,false,true,263860,Photographs,Photograph,"[Young Woman, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1890s–1900s,1890,1909,Albumen silver print from glass negative,22.1 x 16.6 cm. (8 11/16 x 6 9/16 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.4,false,true,263861,Photographs,Photograph,"[Young Man in White Robe and Head Gear Holding Scabbard, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1890s–1900s,1890,1909,Albumen silver print from glass negative,22.3 x 16.5 cm. (8 3/4 x 6 1/2 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.5,false,true,263862,Photographs,Photograph,"[Young Girl in Checked Dress with Roses, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1899,1899,1899,Albumen silver print from glass negative,22.5 x 17.0 cm. (8 7/8 x 6 11/16 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.6,false,true,263863,Photographs,Photograph,"[Nude Young Child with Dog in Lap, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1890s–1900s,1890,1909,Albumen silver print from glass negative,22.5 x 16.5 cm. (8 7/8 x 6 1/2 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.7,false,true,263864,Photographs,Photograph,"[Young Girl [?] with Cloak of Cloth Over Head, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1906,1906,1906,Albumen silver print from glass negative,21.8 x 16.5 cm. (8 9/16 x 6 1/2 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.8,false,true,263865,Photographs,Photograph,"Mico Lo Giudice-Berbiredolu ""Il mago del mandolino""",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1890s–1900s,1890,1909,Albumen silver print from glass negative,22.3 x 16.9 cm. (8 3/4 x 6 5/8 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.9,false,true,263866,Photographs,Photograph,"[Young Girl Wrapped in Cloth, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1890s–1900s,1890,1909,Albumen silver print from glass negative,21.7 x 16.3 cm. (8 9/16 x 6 7/16 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1201.10,false,true,263858,Photographs,Photograph,"[Nude Study: Woman from Behind, Young Man, Sicily, Italy]",,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,1890s–1900s,1890,1909,Albumen silver print from glass negative,21.9 x 16.6 cm. (8 5/8 x 6 9/16 in.),"Gift of Milton Radutzky, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1138,false,true,286064,Photographs,Photograph,[Two Children],,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,ca. 1900,1898,1902,Gelatin silver print,Image: 14 5/16 × 10 11/16 in. (36.4 × 27.2 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1166,false,true,286608,Photographs,Photograph,[Man],,,,,,Artist,,Wilhelm von Gloeden,"Italian, born Germany, 1886–1931",,"Gloeden, Wilhelm von","Italian, born Germany",1886,1931,ca. 1900,1898,1902,Gelatin silver print,8 3/4 x 6 2/3,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.582,false,true,286232,Photographs,Photograph,"Tadeus Langier, Zakopane",,,,,,Artist,,Stanislaw Ignacy Witkiewicz,"Polish, Warsaw 1885–1939 Jeziory",,"Witkiewicz, Stanislaw Ignacy",Polish,1885,1939,1912–13,1912,1913,Gelatin silver print,Image: 12.6 x 17.6 cm (4 15/16 x 6 15/16 in.) Mount: 17.4 x 23.5 cm (6 7/8 x 9 1/4 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.896,false,true,286234,Photographs,Photograph,"Jadwiga Janczewska, Zakopane",,,,,,Artist,,Stanislaw Ignacy Witkiewicz,"Polish, Warsaw 1885–1939 Jeziory",,"Witkiewicz, Stanislaw Ignacy",Polish,1885,1939,ca. 1913,1912,1914,Gelatin silver print,Image: 12.5 x 17.4 cm (4 15/16 x 6 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.899,false,true,286054,Photographs,Photograph,"[Self-Portrait, ""Collapse, with Lamp"", Zakopane]",,,,,,Artist,,Stanislaw Ignacy Witkiewicz,"Polish, Warsaw 1885–1939 Jeziory",,"Witkiewicz, Stanislaw Ignacy",Polish,1885,1939,ca. 1913,1912,1914,Gelatin silver print,Image: 13 x 18.1 cm (5 1/8 x 7 1/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.867,false,true,286118,Photographs,Photograph,Plan of the Socialist Offensive,,,,,,Artist,,Gustav Klutsis,"Russian, Latvia 1895–1938 Moscow",,"Klutsis, Gustav",Russian,1895,1938,1929–30,1929,1930,Gelatin silver print,Image: 7 3/8 × 5 1/2 in. (18.7 × 13.9 cm) Mount: 11 5/8 in. × 8 3/8 in. (29.6 × 21.2 cm),"Gilman Collection, Purchase, Denise and Andrew Saul Gift and Anonymous Gifts, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.678,false,true,284734,Photographs,Photograph,"[Government Troops Firing on Demonstrators, Corner of Nevsky Prospect and Sadovaya Street, St. Petersburg, Russia]",,,,,,Artist,,Karl Karlovich Bulla,"Russian, born Germany, 1853–1929",,"Bulla, Karl Karlovich","Russian, born Germany",1853,1929,"July 4, 1917",1917,1917,Gelatin silver print,12.2 x 21.5cm (4 13/16 x 8 7/16 in.),"Purchase, Jennifer and Joseph Duke Gift, 2001",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.82,false,true,283182,Photographs,Photograph,"Kno-Shr, Kansas Chief",,,,,,Artist,,John H. Fitzgibbon,"American, born Britain, 1816–1882",,"Fitzgibbon, John H.","American, born Britain",1816,1882,1853,1853,1853,Daguerreotype,Image: 17.9 x 14.8 cm (7 1/16 x 5 13/16 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1203,false,true,266499,Photographs,Photograph,"Sausalito from the N.P.C.R.R. Wharf, Looking South",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,ca. 1868,1866,1870,Albumen silver print from glass negative,9.8 x 9.5cm (3 7/8 x 3 3/4in.) Mount: 22.8 x 17.6 cm (9 x 6 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.780,false,true,286079,Photographs,Photograph,"Crater of Volcano, Quetzaltenango-Guatemala",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1875,1875,1875,Albumen silver print from glass negative,Image: 5 3/8 × 9 1/16 in. (13.7 × 23 cm),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.781,false,true,286562,Photographs,Photograph,"Coffee Harvesting, Las Nubes-Guatemala",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1875,1875,1875,Albumen silver print from glass negative,Image: 5 3/8 × 9 1/8 in. (13.7 × 23.2 cm),"Gilman Collection, Purchase, Gift of Photography in the Fine Arts, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.101,false,true,283211,Photographs,Photograph,"Mormon Emigrant Train, Echo Canyon",,,,,,Artist,,Charles William Carter,"American, born Britain, 1832–1918",,"Carter, Charles William","American, born Britain",1832,1918,ca. 1870,1868,1872,Albumen silver print from glass negative,Image: 6.1 × 10.3 cm (2 3/8 × 4 1/16 in.),"Gilman Collection, Purchase, Marlene Nathan Meyerson Family Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.677,false,true,271673,Photographs,Cabinet card,[Man with Side Whiskers],,,,,,Artist,,Frederick Gutekunst,"American, born Germany, 1832–1917",,"Gutekunst, Frederick","American, born Germany",1832,1917,1870s–80s,1870,1889,Albumen silver print from glass negative,,Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1080.3,false,true,264322,Photographs,Photograph,"[Girl with Ringlets, Half Length]",,,,,,Artist,,Frederick Gutekunst,"American, born Germany, 1832–1917",,"Gutekunst, Frederick","American, born Germany",1832,1917,1890s,1890,1899,Gelatin silver print,14.4 x 9.4 cm. (5 11/16 x 3 11/16 in.),"Museum Accession, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1080.5,false,true,264324,Photographs,Photograph,"[Girl with Ringlets, Seated, Three-Quarter Length]",,,,,,Artist,,Frederick Gutekunst,"American, born Germany, 1832–1917",,"Gutekunst, Frederick","American, born Germany",1832,1917,1890s,1890,1899,Gelatin silver print,14.2 x 9.7 cm. (5 9/16 x 3 13/16 in.),"Museum Accession, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1080.6,false,true,264325,Photographs,Photograph,[Girl in Walking Costume with Hat and Muff],,,,,,Artist,,Frederick Gutekunst,"American, born Germany, 1832–1917",,"Gutekunst, Frederick","American, born Germany",1832,1917,1890s,1890,1899,Gelatin silver print,14.1 x 8.8 cm. (5 9/16 x 3 7/16 in.),"Museum Accession, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1080.15,false,true,264320,Photographs,Photograph,[Girl with White Off-the-Shoulder Dress],,,,,,Artist,,Frederick Gutekunst,"American, born Germany, 1832–1917",,"Gutekunst, Frederick","American, born Germany",1832,1917,1890s,1890,1899,Gelatin silver print,14.5 x 9.5 cm. (5 11/16 x 3 3/4 in.),"Museum Accession, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.753a, b",false,true,286587,Photographs,Photograph,Dr. Joseph Parrish and an Idiot,,,,,,Artist,,Frederick Gutekunst,"American, born Germany, 1832–1917",,"Gutekunst, Frederick","American, born Germany",1832,1917,ca. 1858,1856,1860,Albumen silver print from glass negative,Image: 7 5/16 × 4 3/4 in. (18.6 × 12 cm); (a) Image: 8 in. × 5 1/4 in. (20.3 × 13.3 cm); (b) Mount: 14 7/16 × 10 5/16 in. (36.6 × 26.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.36,false,true,669901,Photographs,Photograph,Mouth of Wisconsin River,,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,"Image: 10 9/16 × 13 1/2 in. (26.8 × 34.3 cm), oval Sheet: 14 7/16 × 17 3/16 in. (36.7 × 43.7 cm)","Purchase, Acquisitions Fund and Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.38,false,true,669903,Photographs,Photograph,"Old Ponton Bridge at N. McGregor, Ia.",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,"Image: 10 7/16 × 13 1/8 in. (26.5 × 33.4 cm), oval Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm)","Purchase, Acquisitions Fund and Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.39,false,true,669906,Photographs,Photograph,Pine Bend,,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,"Image: 10 9/16 × 13 1/2 in. (26.8 × 34.3 cm), oval Sheet: 14 7/16 × 17 3/16 in. (36.7 × 43.7 cm)","Purchase, Acquisitions Fund and Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.1,false,true,669900,Photographs,Photograph,"No. 34. From Bluffs at Merrimac, Minnesota Looking Down Stream",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.2,false,true,669904,Photographs,Photograph,"No. 6. From South Approach of Franklin Ave Bridge, Minneapolis, Minnesota Looking Up Stream (Low Water)",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1890,1890,1890,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.3,false,true,669905,Photographs,Photograph,No. 21. Rocks and Dam below Frenchmans Bar (Low Water),,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1889,1889,1889,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.4,false,true,669907,Photographs,Photograph,"No. 69. Wabasha, Minnesota",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1889,1889,1889,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.5,false,true,669908,Photographs,Photograph,"No. 88. Wingdams below Winona, Minnesota",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1889,1889,1889,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.6,false,true,669909,Photographs,Photograph,"No. 90. From bluffs at Trempealueau, Wisconsin Looking Up Stream",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.7,false,true,669910,Photographs,Photograph,"No. 135. Iowa State Penitentiary - Fort Madison, Iowa",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1891,1891,1891,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.8,false,true,669911,Photographs,Photograph,"No. 139a. Head of Niota Chute with Closing Dam [near Fort Madison, Iowa]",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.9,false,true,669912,Photographs,Photograph,No. 155a. Lower Lock Des Moines Rapids Canal,,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1891,1891,1891,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.10,false,true,669913,Photographs,Photograph,No. 167. Raftboat “David Bronson”,,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.11,false,true,669914,Photographs,Photograph,"No. 181. Marshall Ave. Bridge, Minneapolis & St. Paul",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1889,1889,1889,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.12,false,true,669915,Photographs,Photograph,"No. 185. Chicago, Milwaukee & St. Paul Rail Road Bridge at Hasting, Minnesota",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.13,false,true,669916,Photographs,Photograph,"No. 186. Chicago, Burlington & Northern Rail Road Bridge Across Mouth of La Croix River",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1891,1891,1891,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.14,false,true,669917,Photographs,Photograph,"No. 193a. Old Ponton Bridge at Prairie du chien, Wisconsin",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.15,false,true,669918,Photographs,Photograph,"No. 199. Draw Span of Chicago & North Western Rail Road Bridge at Clinton, Iowa",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.17,false,true,669920,Photographs,Photograph,"No. 204. Iowa Central Railway Bridge at Keithsburg, Illinois",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1889,1889,1889,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.18,false,true,669921,Photographs,Photograph,"No. 207. Wabash Rail Road Bridge at Keokuk, Iowa",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1885,1885,1885,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1027,false,true,264640,Photographs,Photograph,"General Grant's Council of War, Massaponax Church, Virginia",,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,"May 21, 1864",1864,1864,Albumen silver print from glass negative,8.7 x 10.8cm (3 7/16 x 4 1/4in.) Mount: 10.8 x 16.6cm (4 1/4 x 6 9/16in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1054.19,true,true,264711,Photographs,Photograph,"Black Cañon, From Camp 8, Looking Above",,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,1871,1871,1871,Albumen silver print from glass negative,20 x 28.1 cm (7 7/8 x 11 1/16 in. ),"Purchase, Joseph Pulitzer Bequest and The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.105,false,true,283217,Photographs,Photograph,"Fissure Vent at Steamboat Springs, Nevada",,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,1867,1867,1867,Albumen silver print from glass negative,Image: 22.3 x 29 cm (8 3/4 x 11 7/16 in.) Mat: 41.3 x 47.6 cm (16 1/4 x 18 3/4 in.),"Gilman Collection, Purchase, Marlene Nathan Meyerson Family Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1199,false,true,285642,Photographs,Photograph,"[Detachment of 50th N.Y. Volunteer Engineers, Pontoon Wagon and Saddle Boat]",,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,ca. 1864,1862,1866,Albumen silver print from glass negative,Image: 6 3/4 × 8 7/8 in. (17.1 × 22.5 cm) Mount: 12 in. × 17 13/16 in. (30.5 × 45.2 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1200,false,true,285643,Photographs,Photograph,Major General Pleasanton and General Custer,,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,1863,1863,1863,Albumen silver print from glass negative,Mount: 11 9/16 × 15 13/16 in. (29.4 × 40.1 cm) Image: 6 13/16 × 8 7/8 in. (17.3 × 22.5 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1203,false,true,285646,Photographs,Photograph,"Volcanic Islands in Mono Lake, California",,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,1868,1868,1868,Albumen silver print from glass negative,Image: 22.3 x 29.3 cm (8 3/4 x 11 9/16 in.) Mat: 40.6 x 50.8 cm (16 x 20 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1204,false,true,285647,Photographs,Photograph,"Desert Lake, near Ragtown, Nevada",,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,1867,1867,1867,Albumen silver print from glass negative,Image: 22.3 x 29.1 cm (8 3/4 x 11 7/16 in.) Mat: 40.6 x 50.8 cm (16 x 20 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1207,false,true,285785,Photographs,Photograph,"Tufa Rocks, Pyramid Lake, Nevada",,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,1867,1867,1867,Albumen silver print from glass negative,Image: 22.1 x 29.4 cm (8 11/16 x 11 9/16 in.) Mat: 47.9 x 61 cm (18 7/8 x 24 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1222,false,true,285767,Photographs,Photograph,Return of Commander Selfridge and his Reconnaissance Party from an Expedition in the Interior of Darien,,,,,,Artist,,Timothy H. O'Sullivan,"American, born Ireland, 1840–1882",,"O'Sullivan, Timothy H.","American, born Ireland",1840,1882,1870,1870,1870,Albumen silver print from glass negative,9 x 11 3/8 (irregular and torn),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.1,false,true,286782,Photographs,Photograph,Des nouvelles occupations en Styrie,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 20.8 x 15.9 cm (8 3/16 x 6 1/4 in.) Mount: 33.8 x 44.9 cm (13 5/16 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.2,false,true,286781,Photographs,Photograph,The Undersigned Photographer as He Was Before 1848,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 13/16 × 7 15/16 in. (25 × 20.1 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.3,false,true,287239,Photographs,Photograph,La nièce de Mr. Tahon propriétaire de la maison n° 7,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 3/4 × 7 5/16 in. (24.7 × 18.6 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.4,false,true,286780,Photographs,Photograph,"An ""Enfant terrible"" (No One Can Do the Impossible)",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Albumen silver print from paper negative,Image: 8 11/16 × 6 1/2 in. (22.1 × 16.5 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.5,false,true,287240,Photographs,Photograph,Mlle. Jeanne tellement tremblante que le photographe ne peut pas fixer les yeux,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 8 9/16 × 6 7/8 in. (21.8 × 17.5 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.6,false,true,287241,Photographs,Photograph,Autre nièce du propriétaire du no. 7,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 5/16 × 7 1/2 in. (23.7 × 19.1 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.7,false,true,287242,Photographs,Photograph,Visite d'un collègue de Bruxelles,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 5/16 × 7 1/2 in. (23.7 × 19.1 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.8,false,true,287243,Photographs,Photograph,La fille de mon ami de Lille,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1856,1856,1856,Salted paper print from paper negative,Image: 8 3/4 × 7 1/16 in. (22.2 × 17.9 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.9,false,true,287244,Photographs,Photograph,[Elderly Lady Sitting],,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 8 3/4 × 7 1/16 in. (22.2 × 18 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.10,false,true,287245,Photographs,Photograph,[Elderly Lady Sitting],,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 5/16 × 6 3/4 in. (23.7 × 17.2 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.11,false,true,287246,Photographs,Photograph,Madame Gihoul,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Albumen silver print from paper negative,Image: 7 5/16 in. × 6 in. (18.6 × 15.2 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.12,false,true,286784,Photographs,Photograph,[Portrait of a Woman],,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from glass negative,Image: 7 1/4 × 9 1/8 in. (18.4 × 23.2 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.13,false,true,287247,Photographs,Photograph,En soirée - janvier 1856,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1856,1856,1856,Salted paper print from paper negative,Image: 9 3/8 × 6 13/16 in. (23.8 × 17.3 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.14,false,true,287248,Photographs,Photograph,Madame Gihoul,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 10 in. × 7 5/8 in. (25.4 × 19.3 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.15,false,true,286783,Photographs,Photograph,[Women Stacking Carrots],,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 7 1/4 × 9 1/8 in. (18.4 × 23.2 cm) Sheet: 13 3/8 × 18 1/2 in. (34 × 47 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.16,false,true,287249,Photographs,Photograph,"Il vient à Bruxelles, voit un appartement place du Cologne no. 7",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 5/16 × 8 5/16 in. (23.6 × 21.1 cm) Sheet: 13 3/8 × 18 1/2 in. (34 × 47 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287249,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.17,false,true,283129,Photographs,Photograph,View of the Square in Melting Snow,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from glass negative,Image: 18 x 22.2 cm (7 1/16 x 8 3/4 in. ) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.18,false,true,287250,Photographs,Photograph,Vue de droite du balcon,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 8 13/16 × 10 3/4 in. (22.4 × 27.3 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.19,false,true,287251,Photographs,Photograph,Vue de face du balcon avant l'entière construction de la place,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Albumen silver print from paper negative,Image: 9 3/8 × 12 13/16 in. (23.8 × 32.5 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.20,false,true,287252,Photographs,Photograph,La place pendant les fêtes de septembre,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 10 5/8 × 9 3/16 in. (27 × 23.3 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.21,false,true,287253,Photographs,Photograph,Visite d'un ami de Lille,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854,1854,1854,Salted paper print from paper negative,Image: 6 1/16 × 7 1/16 in. (15.4 × 17.9 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.22,false,true,287254,Photographs,Photograph,"Station de Malines, Epreuve instantanée au passage d'un train au soleil couchant",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 6 5/8 × 8 7/16 in. (16.8 × 21.4 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.24,false,true,287255,Photographs,Photograph,Jardin zoologique de Bruxelles,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 8 11/16 × 10 13/16 in. (22.1 × 27.5 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.25,false,true,286899,Photographs,Photograph,Zoological Garden,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 3/16 × 12 1/16 in. (23.4 × 30.6 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.26,false,true,287256,Photographs,Photograph,"[The Bear Enclave, Zoological Gardens, Brussels]",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 5 13/16 × 7 3/16 in. (14.8 × 18.3 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.27,false,true,287257,Photographs,Photograph,"[The Kiosk, Zoological Gardens, Brussels]",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 9 1/4 × 12 1/8 in. (23.5 × 30.8 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.28,false,true,286779,Photographs,Photograph,"The Zoological Garden, Brussels",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854,1854,1854,Salted paper print from paper negative,Image: 9 5/16 × 11 9/16 in. (23.7 × 29.4 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.29,false,true,287258,Photographs,Photograph,Jardin zoologique de Bruxelles; Les trois jumeaux fils de Mr. Lebens et toute la famille,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 5 13/16 × 7 11/16 in. (14.7 × 19.5 cm) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.30,false,true,287259,Photographs,Photograph,"[Heron Pond, Zoological Gardens, Brussels]",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 18 × 21.2 cm (7 1/16 × 8 3/8 in.) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.31,false,true,287260,Photographs,Photograph,"[The Pelicans and Greenhouses, Zoological Gardens, Brussels]",,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854–56,1854,1856,Salted paper print from paper negative,Image: 14.6 × 19.5 cm (5 3/4 × 7 11/16 in.) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.372.32,false,true,283127,Photographs,Photograph,Another Impossible Task,,,,,,Artist,,Louis-Pierre-Théophile Dubois de Nehaut,"French, active Belgium, 1799–1872",,"Dubois de Nehaut, Louis-Pierre-Théophile",Belgian,1799,1872,1854,1854,1854,Salted paper print from glass negative,Image: 16.3 x 21.2 cm (6 7/16 x 8 3/8 in.) Sheet: 13 3/8 × 18 1/8 in. (34 × 46 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.62,false,true,265589,Photographs,Multiple exposure; Photograph,In the Studio,,,,,,Artist,,El Lissitzky,"Russian, Pochinok 1890–1941 Moscow",,"Lissitzky, El",Russian,1890,1941,1923,1923,1923,Gelatin silver print,10.9 x 8.3 cm (4 5/16 x 3 1/4 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.151,false,true,283289,Photographs,Photograph,[Self-Portrait],,,,,,Artist,,El Lissitzky,"Russian, Pochinok 1890–1941 Moscow",,"Lissitzky, El",Russian,1890,1941,1924–25,1924,1925,Gelatin silver print,Image: 17.3 x 12.1 cm (6 13/16 x 4 3/4 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.110,false,true,299469,Photographs,Photographs,[Man in Chainmail Tunic Posing as a Dying Soldier],,,,,,Artist,,Adrien Constant de Rebecque,"Swiss, Lausanne 1806–1876 Lausanne",,"Constant de Rebecque, Adrien",Swiss,1806,1876,ca. 1863,1858,1868,Albumen print from collodion glass negative,Image: 17.9 x 24.2 cm (7 1/16 x 9 1/2 in.) Mount: 27.2 x 37 cm (10 11/16 x 14 9/16 in.),"Gilman Collection, Purchase, The Howard Gilman Foundation Gift, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1168.2,false,true,264528,Photographs,Photograph,"[A Gypsy Dancing-Girl, Kathiawar]",,,,,,Artist,,E. Taurines,"probably French, active ca. 1885–1901",,"Taurines, E.",French ?,1885,1901,ca. 1915,1915,1915,Albumen silver print from glass negative,23.5 x 18.2 cm (9 1/4 x 7 3/16 in.),"Gift of Matthew Dontzin, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.67,false,true,291820,Photographs,Daguerreotype,[Middle-aged Man with Glasses Holding Pocket Watch],,,,,,Artist,,Antoine-François-Jean Claudet,"French, active Great Britain, 1797–1867",,"Claudet, Antione-François-Jean","French, active Great Britain",1797,1867,1844–1859,1844,1859,Daguerreotype,Image: 11.3 x 8.7 cm (4 7/16 x 3 7/16 in.) Plate: 14.6 x 12.2 cm (5 3/4 x 4 13/16 in.) Case: 2.2 x 17.8 x 15.2 cm (7/8 x 7 x 6 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.400.86a, b",false,true,291839,Photographs,Daguerreotype,"[Stereograph, Two Children Standing Between Furniture in a Studio Parlor Setting]",,,,,,Artist,,Antoine-François-Jean Claudet,"French, active Great Britain, 1797–1867",,"Claudet, Antione-François-Jean","French, active Great Britain",1797,1867,ca. 1855,1853,1857,Daguerreotype,"Image: 6.8 x 5.7 cm (2 11/16 x 2 1/4 in.), each Mount: 8.4 x 17.6 cm (3 5/16 x 6 15/16 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.528,false,true,289024,Photographs,Photograph,"Cloisters of St. Paul's, the Basilica, Outside the Walls of Rome",,,,,,Artist,,Robert Macpherson,"British, Tayside, Scotland 1811–1872 Rome",,"Macpherson, Robert","British, Scottish",1811,1811,1858 or earlier,1857,1858,Albumen silver print from glass negative,Image: 34.8 x 25.8 cm (13 11/16 x 10 3/16 in.) Mount (2nd): 52 x 36.8 cm (20 1/2 x 14 1/2 in.),"Purchase, Joyce F. Menschel Gift, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.58,false,true,283141,Photographs,Photograph,Cloaca Maxima,,,,,,Artist,,Robert Macpherson,"British, Tayside, Scotland 1811–1872 Rome",,"Macpherson, Robert","British, Scottish",1811,1811,1858 or earlier,1854,1858,Albumen silver print from glass negative,Image: 31 x 37.2 cm (12 3/16 x 14 5/8 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.59,false,true,283142,Photographs,Photograph,"The Theater of Marcellus, from the Piazza Montanara",,,,,,Artist,,Robert Macpherson,"British, Tayside, Scotland 1811–1872 Rome",,"Macpherson, Robert","British, Scottish",1811,1811,1858 or earlier,1854,1858,Albumen silver print from glass negative,Image: 41.9 x 27.4 cm (16 1/2 x 10 13/16 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.909,false,true,285821,Photographs,Photograph,"The Valley of the Anio, with the Upper and Lower Cascatelle, Mecenas's Villa, and Distant Campagna",,,,,,Artist,,Robert Macpherson,"British, Tayside, Scotland 1811–1872 Rome",,"Macpherson, Robert","British, Scottish",1811,1811,1858 or earlier,1858,1858,Albumen silver print from glass negative,"Image: 30.6 x 40.1 cm (12 1/16 x 15 13/16 in.), oval Mount: 49.4 x 64.7 cm (19 7/16 x 25 1/2 in.)","Gilman Collection, Purchase, W. Bruce and Delaney H. Lundberg Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.963,false,true,285822,Photographs,Photograph,Falls of Terni,,,,,,Artist,,Robert Macpherson,"British, Tayside, Scotland 1811–1872 Rome",,"Macpherson, Robert","British, Scottish",1811,1811,ca. 1860,1858,1862,Albumen silver print from glass negative,"Image: 27.6 x 37.7 cm (10 7/8 x 14 13/16 in.), oval Mount: 49.5 x 60.6 cm (19 1/2 x 23 7/8 in.)","Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1098,false,true,285820,Photographs,Photograph,The Hermaphrodite,,,,,,Artist,,Robert Macpherson,"British, Tayside, Scotland 1811–1872 Rome",,"Macpherson, Robert","British, Scottish",1811,1811,ca. 1861,1858,1864,Albumen silver print from glass negative,Image: 6 3/8 in. × 13 in. (16.2 × 33 cm) Mount: 18 5/8 × 24 5/8 in. (47.3 × 62.5 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.537,false,true,261983,Photographs,Photographs,George Frederick Watts,,,,,,Artist,,David Wilkie Wynfield,"British (born India), 1837–1887 London (?)",,"Wynfield, David Wilkie","British, born India",1837,1887,1860s,1860,1869,Albumen silver print,21.7 x 16.4 cm (8 9/16 x 6 7/16 in.),"Warner Communications Inc. Purchase Fund, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.310,false,true,269420,Photographs,Photograph,Portrait of D.O. Hill,,,,,,Artist,,Thomas Annan,"British, Dairsie, Fife, Scotland 1829–1887",,"Annan, Thomas","British, Scottish",1829,1887,"1867, printed ca. 1900s",1867,1867,Gelatin silver print,23.0 x 17.5 cm. (9 1/16 x 6 7/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.216,false,true,285739,Photographs,Photograph,Frederick Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1849–50,1847,1852,Daguerreotype,Image: 11.7 x 8.6 cm (4 5/8 x 3 3/8 in.) Case: 1.4 x 11.9 x 15.2 cm (9/16 x 4 11/16 x 6 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.217,false,true,285740,Photographs,Photograph,Frederick Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1851–53,1849,1855,Daguerreotype,Image: 8.9 x 7 cm (3 1/2 x 2 3/4 in.) Case: 1.6 x 11.9 x 9.4 cm (5/8 x 4 11/16 x 3 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.219,false,true,285769,Photographs,Photograph,William and Sophia Palmer Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1846–47,1844,1849,Daguerreotype,Image: 12.1 x 8.9 cm (4 3/4 x 3 1/2 in.) Case: 1.6 x 13.5 x 11.7 cm (5/8 x 5 5/16 x 4 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.221,false,true,286328,Photographs,Photograph,William Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1848–50,1846,1852,Daguerreotype,Image: 8.9 x 7 cm (3 1/2 x 2 3/4 in.) Case: 1.4 x 11.6 x 9.2 cm (9/16 x 4 9/16 x 3 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.222,false,true,286329,Photographs,Photograph,William Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,1855–58,1855,1858,Daguerreotype,Image: 6.7 x 5.4 cm (2 5/8 x 2 1/8 in.) Case: 1.6 x 9.4 x 8.3 cm (5/8 x 3 11/16 x 3 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.224,false,true,286330,Photographs,Photograph,William Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1853–55,1851,1857,Daguerreotype,Image: 7.1 x 5.9 cm (2 13/16 x 2 5/16 in.) Frame: 8.4 x 7.1 cm (3 5/16 x 2 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.227,false,true,286326,Photographs,Photograph,William Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1849–51,1847,1853,Salted Paper Print From Paper Negative with Applied Color,Image: 16.5 x 12.1 cm (6 1/2 x 4 3/4 in.) Case: 1.3 x 19.4 x 14.4 cm (1/2 x 7 5/8 x 5 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.228,false,true,286273,Photographs,Photograph,Three Men Playing Cards,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,"March, 1842",1842,1842,Daguerreotype,Image: 7.3 x 5.7 cm (2 7/8 x 2 1/4 in.) Frame: 21.6 x 18.1 cm (8 1/2 x 7 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.230,false,true,286274,Photographs,Photograph,Frederick David Langenheim,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1851–52,1849,1853,Daguerreotype,Image: 12.4 x 9.2 cm (4 7/8 x 3 5/8 in.) Case: 1.6 x 15.1 x 12.1 cm (5/8 x 5 15/16 x 4 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.237,false,true,285719,Photographs,Photograph,Professor Schneider's Children,,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1842,1840,1844,Daguerreotype,Image: 6.7 x 5.4 cm (2 5/8 x 2 1/8 in.) Case: 1.3 x 9.2 x 7.8 cm (1/2 x 3 5/8 x 3 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.870,false,true,285832,Photographs,Photograph,[Mrs. Thomas Ustick Walter and Her Deceased Child],,,,,,Artist,,William Langenheim,"American, born Germany, Schöningen 1807–1874",,"Langenheim, William","American, born Germany",1807,1874,ca. 1846,1844,1848,Daguerreotype,"Visible: 4 11/16 x 3 1/2, Case: 5 15/16 x 4 5/8 x 5/8","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.215,false,true,285721,Photographs,Photograph,Frederick Langenheim,,,,,,Artist,,Frederick Langenheim,"American, born Germany, Schöningen 1809–1879",,"Langenheim, Frederick","American, born Germany",1809,1879,ca. 1850–51,1848,1853,Daguerreotype,Image: 11.4 x 8.4 cm (4 1/2 x 3 5/16 in.) Frame: 17.9 x 14.4 cm (7 1/16 x 5 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.4,false,true,289276,Photographs,Photograph,"Lovers Leap, Foochow",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 3/16 × 11 1/8 in. (23.3 × 28.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.6,false,true,289278,Photographs,Photograph,"The Cemetery, Foochow",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/4 × 10 7/8 in. (21 × 27.6 cm); oval,"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.7,false,true,289279,Photographs,Photograph,Yungfoo River,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 1/2 × 11 1/8 in. (24.1 × 28.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.8,false,true,289280,Photographs,Photograph,"White Pagoda, Foochow",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 3/8 × 10 15/16 in. (23.8 × 27.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.9,false,true,289281,Photographs,Photograph,Teahouse at Peking,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 7/16 × 11 5/16 in. (23.9 × 28.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.10,false,true,289282,Photographs,Photograph,Pagoda Island,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 10 11/16 in. (20.7 × 27.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.11,false,true,289283,Photographs,Photograph,Bowling Alley and Raquet Court at Foochow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 9/16 × 11 7/16 in. (21.8 × 29 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.12,false,true,289284,Photographs,Photograph,Cemetery at Foochow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 3/16 × 11 3/8 in. (20.8 × 28.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.14,false,true,289286,Photographs,Photograph,"Ming-Ming Customhouse, Rieng-Gang City",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 7/8 × 11 1/8 in. (20 × 28.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.15,false,true,289287,Photographs,Photograph,"Missionary Houses, Foochow",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 11 3/8 in. (20.7 × 28.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.16,false,true,289288,Photographs,Photograph,Min-ch'oi Temple & City Wall of Yen-ping,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 15/16 in. × 11 in. (20.1 × 27.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.19,false,true,289291,Photographs,Photograph,Heaven Ascending Peak near Sing-Chang,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 5/16 × 11 3/16 in. (21.1 × 28.4 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.20,false,true,289292,Photographs,Photograph,"Pure Spring Cave, near Sing Chang",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 10 1/16 × 8 3/8 in. (25.5 × 21.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.21,false,true,289293,Photographs,Photograph,[untitled],,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 10 15/16 in. (20.7 × 27.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.22,false,true,289294,Photographs,Photograph,[untitled],,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/16 × 11 5/16 in. (20.4 × 28.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.23,false,true,289295,Photographs,Photograph,[untitled],,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 in. × 11 5/16 in. (20.3 × 28.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.24,false,true,289296,Photographs,Photograph,Flouring Mill at Yen-Ping,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/16 × 11 5/16 in. (20.4 × 28.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.25,false,true,289297,Photographs,Photograph,Chui Nang opposite Kien-yang city,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 15/16 × 11 1/4 in. (20.2 × 28.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.28,false,true,289300,Photographs,Photograph,"Factory and Silkworm Nursery, Foochow",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 15/16 × 9 7/16 in. (17.6 × 23.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.32,false,true,289304,Photographs,Photograph,[Mountain and Rice Fields],,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 3/16 × 11 1/4 in. (20.8 × 28.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.34,false,true,289306,Photographs,Photograph,The Grand Stand Foochow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 1/8 × 11 1/8 in. (23.1 × 28.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.36,false,true,289308,Photographs,Photograph,[Untitled],,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 11 7/16 in. (20.7 × 29 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.40,false,true,289312,Photographs,Photograph,"[Village, River Min]",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 in. × 10 15/16 in. (22.8 × 27.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.44,false,true,289316,Photographs,Photograph,"Tai-Laity Montain, North River, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 5/8 × 9 7/16 in. (16.9 × 24 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.45,false,true,289317,Photographs,Photograph,View from Koong-Yan-Shang Temple,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 5 11/16 × 9 3/16 in. (14.4 × 23.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.46,false,true,289318,Photographs,Photograph,Village Road North River,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 7/8 × 9 3/16 in. (17.5 × 23.4 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.47,false,true,289319,Photographs,Photograph,[Panorama of Hong Kong],,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image(a): 10 5/8 × 8 13/16 in. (27 × 22.4 cm) Image(b): 10 1/2 × 8 13/16 in. (26.6 × 22.4 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.48,false,true,289320,Photographs,Photograph,View of Canton from the River,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 9 13/16 in. (20.6 × 24.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.49,false,true,289321,Photographs,Photograph,View Opposite Canton,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 10 15/16 × 10 9/16 in. (27.8 × 26.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.51,false,true,289323,Photographs,Photograph,"West Gate, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 3/8 × 10 3/4 in. (21.3 × 27.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.52,false,true,289324,Photographs,Photograph,"Garden at the English Consulate, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/4 × 10 13/16 in. (21 × 27.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.53,false,true,286867,Photographs,Photograph,"Beggars at the Gate of a Temple, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 27.3 x 21.3 cm (10 3/4 x 8 3/8 in.),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.56,false,true,289327,Photographs,Photograph,"[Rapid, Yen-Ping]",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 15/16 × 11 1/8 in. (20.1 × 28.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.58,false,true,289329,Photographs,Photograph,"Dwelling on the Water, Canton",,,,,,Artist,,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/2 × 10 11/16 in. (21.6 × 27.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.59,false,true,289330,Photographs,Photograph,A Creek in Canton,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/2 × 10 13/16 in. (21.6 × 27.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.60,false,true,289331,Photographs,Photograph,A Creek in Canton,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 7/16 × 10 7/8 in. (21.5 × 27.6 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.61,false,true,289332,Photographs,Photograph,Mercantile Junks at Canton,,,,,,Artist,,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 7/16 × 10 9/16 in. (21.4 × 26.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.62,false,true,289333,Photographs,Photograph,A Garden in Canton,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 7/16 × 10 7/8 in. (21.5 × 27.6 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.63,false,true,289334,Photographs,Photograph,"A Tea Pavilion, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/2 × 10 5/8 in. (21.6 × 27 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.64,false,true,289335,Photographs,Photograph,"Way to the Theater Pon-Jing-Quais Garden, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 7/16 × 10 5/8 in. (21.4 × 27 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.65,false,true,289336,Photographs,Photograph,Mandarin Dwelling,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 3/8 × 10 5/8 in. (21.2 × 27 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.66,false,true,289337,Photographs,Photograph,Madarin at Home,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 5/16 × 10 11/16 in. (18.6 × 27.1 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.67,false,true,289338,Photographs,Photograph,Teh Hop-Ho (Canton) and his Son,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 10 3/8 × 8 3/8 in. (26.4 × 21.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.68,false,true,289339,Photographs,Photograph,"A Tartar Soldier, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 10 11/16 × 8 3/8 in. (27.1 × 21.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.69,false,true,289340,Photographs,Photograph,"Mandarin Dwelling, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 7/16 × 10 9/16 in. (21.5 × 26.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.70,false,true,289341,Photographs,Photograph,"Garden, Canton",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 11 1/8 in. (20.7 × 28.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.71,false,true,289342,Photographs,Photograph,Macao,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 9/16 × 11 3/16 in. (19.2 × 28.4 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.72,false,true,289343,Photographs,Photograph,Fishing Boats going out Macao,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 5/16 × 10 11/16 in. (18.6 × 27.1 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.73,false,true,289344,Photographs,Photograph,A Street in Macao,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 5/16 × 10 5/8 in. (21.1 × 27 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.74,false,true,289345,Photographs,Photograph,"St. Pauls Cathedral, Macao",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 10 9/16 × 8 3/8 in. (26.8 × 21.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.75,false,true,289346,Photographs,Photograph,"Tomb, Macao",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 10 9/16 × 8 1/8 in. (26.9 × 20.6 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.76,false,true,289347,Photographs,Photograph,View in Camoens Garden Macao,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 10 3/8 × 8 1/4 in. (26.4 × 20.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.77,false,true,289348,Photographs,Photograph,Swatow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 11/16 × 10 9/16 in. (19.5 × 26.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.78,false,true,289349,Photographs,Photograph,View on Rak-Chui opposite Swatow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 1/4 × 10 9/16 in. (15.8 × 26.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.79,false,true,289350,Photographs,Photograph,View on Rak-Chui opposite Swatow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 1/4 × 10 9/16 in. (15.8 × 26.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.80,false,true,289351,Photographs,Photograph,View on Rak-Chui opposite Swatow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 3/8 × 10 15/16 in. (18.8 × 27.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.81,false,true,289352,Photographs,Photograph,View of Swatow Harbour,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 11/16 × 10 11/16 in. (19.6 × 27.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.82,false,true,289353,Photographs,Photograph,Entrance of Amoy Harbour,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 5/8 × 9 5/16 in. (16.8 × 23.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.83,false,true,289354,Photographs,Photograph,Amoy Harbour,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 11/16 × 9 1/4 in. (17 × 23.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.84,false,true,289355,Photographs,Photograph,"View on Koolangsoo Island, Amoy",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 5/8 × 9 1/4 in. (16.9 × 23.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.85,false,true,289356,Photographs,Photograph,Amoy Harbour,,,,,,Artist,,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 1/4 × 10 3/4 in. (18.4 × 27.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.86,false,true,289357,Photographs,Photograph,View of Amoy,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 1/2 × 10 1/16 in. (19 × 25.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.87,false,true,289358,Photographs,Photograph,View of Amoy,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 1/4 × 10 1/8 in. (18.4 × 25.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.88,false,true,289359,Photographs,Photograph,"Lower Harbour, Amoy",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 3/16 × 10 1/8 in. (18.2 × 25.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.89,false,true,289360,Photographs,Photograph,"The Grand Stand, Amoy 1871",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,1871,1871,1871,Albumen silver print from glass negative,Image: 6 3/4 × 10 1/16 in. (17.2 × 25.6 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.90,false,true,289361,Photographs,Photograph,"Great Pagoda at Foochow, the Largest in China",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 3/4 × 13 1/4 in. (24.8 × 33.6 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.91,false,true,289362,Photographs,Photograph,"Peculiar shaped Rocks on Roolangsoo Island, Amoy",,,,,,Artist,,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 3/4 × 9 3/16 in. (17.1 × 23.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.92,false,true,289363,Photographs,Photograph,Nine Arch Bridge outside Foochow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 3/4 × 13 3/16 in. (24.7 × 33.5 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.93,false,true,289364,Photographs,Photograph,Amoy Houses,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 11/16 × 9 7/16 in. (17 × 24 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.94,false,true,289365,Photographs,Photograph,Twin Pagodas at Foochow,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 13 1/16 × 9 3/4 in. (33.2 × 24.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.95,false,true,289366,Photographs,Photograph,Amoy Fishing Boats,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 3/4 × 9 3/16 in. (17.2 × 23.3 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.96,false,true,289367,Photographs,Photograph,"Harbour of Hongkong; St Johns Cathedral Hongkong; Carriage; Tub Mending, North of China",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,"Image (a), (b): 3 5/8 × 6 7/8 in. (9.2 × 17.5 cm) Image (c), (d): 3 7/8 × 5 13/16 in. (9.9 × 14.8 cm)","Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.97,false,true,289368,Photographs,Photograph,"Le-Le- Cong Josshouse, Amoy",,,,,,Artist,,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 11/16 × 9 3/16 in. (17 × 23.4 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289368,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.98,false,true,289369,Photographs,Photograph,Ploughing; Carriage; Cotton Spinning; Selling Sweets,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 3 15/16 × 5 13/16 in. (10 × 14.8 cm); each,"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.99,false,true,289370,Photographs,Photograph,View over Amoy from Pe-Le-Jong,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 7/16 × 10 13/16 in. (21.5 × 27.4 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289370,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.100,false,true,289371,Photographs,Photograph,"Pagoda on the Execution Ground, Foochow",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 13 3/8 × 9 13/16 in. (33.9 × 24.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.101,false,true,289372,Photographs,Photograph,"Temple, Amoy",,,,,,Artist,,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 in. × 6 5/8 in. (22.8 × 16.9 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.102,false,true,289373,Photographs,Photograph,Street in Kadhin; Foochow Creek Bridge; Country View; Shanghai,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 3 1/4 × 6 7/8 in. (8.2 × 17.5 cm); approx. each,"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.103,false,true,289374,Photographs,Photograph,Shoemaker North of China; Wandering Restaurant North of China,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 4 in. × 5 3/4 in. (10.1 × 14.6 cm); each,"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.104,false,true,289375,Photographs,Photograph,"View on the Bund; The Monument; The Club House; View from the Bund, Shanghai",,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 3 11/16 × 6 15/16 in. (9.3 × 17.6 cm); each,"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.105,false,true,289376,Photographs,Photograph,The Bund in Shanghai,,,,,,Artist,Attributed to,John Thomson,"British, Edinburgh, Scotland 1837–1921 London",,"Thomson, John","British, Scottish",1837,1921,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 9 9/16 × 13 1/16 in. (24.3 × 33.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.61,false,true,269006,Photographs,Photograph,"[The Harbor at Valletta, Malta]",,,,,,Artist,Attributed to,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1850s,1850,1859,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.73,false,true,269019,Photographs,Photograph,"[Temple of Concord, Rome]",,,,,,Artist,Attributed to,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1850s,1850,1859,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.74,false,true,269020,Photographs,Photograph,"[Temple of Antonius and Faustina, San Lorenzo in Miranda, Rome]",,,,,,Artist,Attributed to,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1850s,1850,1859,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.109,false,true,268882,Photographs,Photograph,"[Strada Levante, Valletta, Malta]",,,,,,Artist,Attributed to,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1850s,1850,1859,Salted paper print from paper negative,Image: 21.7 x 17.4 cm (8 9/16 x 6 7/8 in.),"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5167,false,true,266875,Photographs,Photograph,The Capitoline,,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1846,1846,1846,Salted paper print from a paper negative,16.2 x 21.2 cm. (6 3/8 x 8 3/6 in.),"Purchase, Hans P. Kraus Jr. Gift, and Mrs. Harrison D. Horblit and Joyce and Robert Menschel Gifts, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.27,false,true,306247,Photographs,Photograph,"Saint Paul's Bay, Malta",,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,Spring 1846,1846,1846,Salted paper print from paper negative,Image: 6 5/8 × 8 9/16 in. (16.9 × 21.7 cm) Sheet: 7 1/4 × 8 7/8 in. (18.4 × 22.6 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.28,false,true,306248,Photographs,Photograph,Seated Lad,,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1845–50,1845,1850,Salted paper print from paper negative,Image: 4 3/16 × 3 7/16 in. (10.6 × 8.7 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.29,false,true,306249,Photographs,Photograph,Family Group Portrait Posed in Doorway,,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,late 1840s,1845,1849,Salted paper print from paper negative,Height: 3 3/8 in. (8.5 cm) Width: 4 1/4 in. (10.8 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306249,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.30,false,true,306250,Photographs,Photograph,"Portrait of the Gardener, possibly David Roderick",,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,late 1840s,1845,1849,Salted paper print from paper negative,Height: 6 15/16 in. (17.7 cm) Width: 8 11/16 in. (22.1 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.31,false,true,306251,Photographs,"Photograph, contact sheet",[Contact sheet of four group portraits in a doorway],,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,ca. 1850,1845,1855,Salted paper print from paper negative,Image: 7 3/16 in. × 9 in. (18.2 × 22.8 cm) Sheet: 7 3/8 × 9 1/8 in. (18.7 × 23.1 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.32,false,true,306252,Photographs,Photograph,Two Young Men Resting on a Pier,,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,late 1840s,1845,1849,Salted paper print from paper negative,Image: 4 5/16 × 3 3/8 in. (11 × 8.6 cm) Sheet: 3 13/16 × 4 11/16 in. (9.7 × 11.9 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.33,false,true,306253,Photographs,Photograph,Duomo Milan,,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1846,1846,1846,Salted paper print from paper negative,Image: 8 1/2 × 6 5/8 in. (21.6 × 16.9 cm) Sheet: 9 7/16 × 7 11/16 in. (24 × 19.6 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.34,false,true,306254,Photographs,Photograph,"67. Colosseum, Rome, Second View",,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,May 1846,1846,1846,Salted paper print from paper negative,Image: 6 7/8 × 8 9/16 in. (17.4 × 21.7 cm) Sheet: 7 5/16 × 8 7/8 in. (18.5 × 22.5 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.35,false,true,306255,Photographs,Photograph,Lady in Open Window with Bird Cage,,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",or his Circle,"Jones, Calvert Richard","British, Welsh",1802,1877,late 1840s,1845,1849,Salted paper print from paper negative,Sheet: 4 7/16 × 3 11/16 in. (11.2 × 9.4 cm) Image: 4 in. × 2 15/16 in. (10.2 × 7.5 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.851,false,true,285837,Photographs,Photograph,[Woman],,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1845–50,1845,1850,Salted paper print from paper negative,Image: 3 15/16 × 3 5/16 in. (10 × 8.4 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.852,false,true,285838,Photographs,Photograph,[Soldier],,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1845–50,1845,1850,Salted paper print from paper negative,Image: 4 3/16 × 3 3/16 in. (10.6 × 8.1 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.947a, b",false,true,285836,Photographs,Photograph,"Santa Lucia, Naples",,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard","British, Welsh",1802,1877,1845–46,1845,1846,Salted paper prints from paper negatives,22.4 x 36.2 cm (8 13/16 x 14 1/4 in.) overall Image: 22.2 x 17 cm (8 3/4 x 6 11/16 in.) each,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.5,false,true,263190,Photographs,Photograph,[Thereza Dillwyn Llewelyn with Her Microscope],,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,ca. 1854,1852,1856,Salted paper print from glass negative,23.5 x 18.7 cm (9 1/4 x 7 3/8 in. ),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.20,false,true,270835,Photographs,Photograph,"Piscator, No. 2",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.573,false,true,285667,Photographs,Photograph,"The Wigwam, a Canadian Scene at Penllergare",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,ca. 1855,1853,1857,Albumen silver print from glass negative,Image: 18.5 x 22.1 cm (7 5/16 x 8 11/16 in.) Mount: 22.1 x 36.9 cm (8 11/16 x 14 1/2 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (2),false,true,287892,Photographs,Photograph,Fishing for Shells,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.6 × 20.5 cm (6 1/8 × 8 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (3),false,true,287893,Photographs,Photograph,Feeding Poor Puss,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 13.5 × 10.9 cm (5 5/16 × 4 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (6),false,true,287896,Photographs,Photograph,Winds & Waves,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 12 × 10.8 cm (4 3/4 × 4 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (7),false,true,287897,Photographs,Photograph,Elinor and Lucy Llewelyn,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 12.4 × 10.5 cm (4 7/8 × 4 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (9),false,true,287899,Photographs,Photograph,Upper End of the Lake Penllergare,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 16.9 × 21.9 cm (6 5/8 × 8 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (11),false,true,287901,Photographs,Photograph,"[Two Women, One Kneeling and One Standing, Looking into Basket Filled with Vegetables]",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Albumen silver print,Image: 19.2 × 14.9 cm (7 9/16 × 5 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (13),false,true,287903,Photographs,Photograph,After the Storm,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 19.5 × 15.3 cm (7 11/16 in. × 6 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (23),false,true,287913,Photographs,Photograph,The Boating Party,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.5 × 20 cm (6 1/8 × 7 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (25),false,true,287915,Photographs,Photograph,Caswell,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.1 × 20.4 cm (5 15/16 × 8 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (26),false,true,287916,Photographs,Photograph,"Remember, remember the 5th of November!",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.6 × 19.5 cm (6 1/8 × 7 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (27),false,true,287917,Photographs,Photograph,Three Cliffs Bay,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 17.8 × 15.8 cm (7 in. × 6 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (31),false,true,287921,Photographs,Photograph,The Lonely Glen,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 16 × 21 cm (6 5/16 × 8 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (33),false,true,287923,Photographs,Photograph,The Sweet Water Fountain,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 14.7 × 12 cm (14.7 × 12 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (34),false,true,287924,Photographs,Photograph,The Great Torr and Crawley Rocks,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 20.6 × 26 cm (8 1/8 × 10 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (45),false,true,287935,Photographs,Photograph,Tenby Lifeboat,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.9 × 20.2 cm (6 1/4 × 7 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (82),false,true,287971,Photographs,Photograph,"The ""Juno"" in Tenby Harbour",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.2 × 20.2 cm (6 in. × 7 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (10a, b)",false,true,287900,Photographs,Photograph,Johnny and Drum; Dead Game,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 12.7 × 10.9 cm (5 in. × 4 5/16 in.) (a) Image: 7.5 × 6.4 cm (2 15/16 × 2 1/2 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (12a, b)",false,true,287902,Photographs,Photograph,Caswell Garden; Geneviève,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 12.8 × 11 cm (5 1/16 × 4 5/16 in.) (a) Image: 11.5 × 8.8 cm (4 1/2 × 3 7/16 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (16a, b)",false,true,287906,Photographs,Photograph,Water Lilies; The Photographer,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 4.7 × 6.7 cm (1 7/8 × 2 5/8 in.) (a) Image: 13 × 10.7 cm (5 1/8 × 4 3/16 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (61a, b)",false,true,287951,Photographs,Photograph,Lanelay; [Untitled],,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print; albumen silver print,Image: 14.7 × 11.1 cm (5 13/16 × 4 3/8 in.) Image: 8.1 × 11.6 cm (3 3/16 × 4 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.120,false,true,283247,Photographs,Photograph,Oscar Wilde,,,,,,Artist,,Napoleon Sarony,"American (born Canada), Quebec 1821–1896 New York",,"Sarony, Napoleon","American, born Canada",1821,1896,1882,1882,1882,Albumen silver print,Image: 30.5 x 18.4 cm (12 x 7 1/4 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.828,false,true,285691,Photographs,Photograph,Oscar Wilde,,,,,,Artist,,Napoleon Sarony,"American (born Canada), Quebec 1821–1896 New York",,"Sarony, Napoleon","American, born Canada",1821,1896,1882,1882,1882,Albumen silver print from glass negative,Image: 12 in. × 7 1/4 in. (30.5 × 18.4 cm) Mount: 12 15/16 × 7 3/8 in. (32.8 × 18.8 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.622,false,true,283242,Photographs,Photograph,Countess Greffulhe,,,,,,Artist,,Otto Wegener,"French (born Sweden), Helsingborg 1849–1922 Paris",,"Wegener, Otto","French, born Sweden",1849,1922,1899,1899,1899,Gelatin silver print,Image: 26 15/16 × 15 3/8 in. (68.4 × 39 cm) Mount: 27 3/16 × 15 11/16 in. (69 × 39.8 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.538.1,false,true,291981,Photographs,Photograph,[Woman with Tea Set Playing the Koto],,,,,,Artist,,Felice Beato,"British (born Italy), Venice 1832–1909 Luxor, Egypt",,"Beato, Felice","British, born Italy",1832,1909,ca. 1860,1860,1860,Albumen silver print from glass negative,Image: 20.8 x 26.2 cm (8 3/16 x 10 5/16 in.) Mount: 34.3 x 48.3 cm (13 1/2 x 19 in.),"Gift of Isaac Lagnado, in honor of Paula J. Giardina, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.538.2,false,true,291982,Photographs,Photograph,"""Shariki,"" or Cart-Pushing Coolies",,,,,,Artist,,Felice Beato,"British (born Italy), Venice 1832–1909 Luxor, Egypt",,"Beato, Felice","British, born Italy",1832,1909,ca. 1860,1860,1860,Albumen silver print from glass negative,Image: 20.8 x 25.4 cm (8 3/16 x 10 in.) Mount: 32.4 x 51.1 cm (12 3/4 x 20 1/8 in.),"Gift of Isaac Lagnado, in honor of Mary Stack, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.72,false,true,283169,Photographs,Photograph,[After the Capture of the Taku Forts],,,,,,Artist,,Felice Beato,"British (born Italy), Venice 1832–1909 Luxor, Egypt",,"Beato, Felice","British, born Italy",1832,1909,1860,1860,1860,Albumen silver print from glass negative,Image: 26 x 29.9 cm (10 1/4 x 11 3/4 in.) Mount: 29.3 x 32.5 cm (11 9/16 x 12 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.566,false,true,285902,Photographs,Photograph,"Samurai, Yokohama",,,,,,Artist,,Felice Beato,"British (born Italy), Venice 1832–1909 Luxor, Egypt",,"Beato, Felice","British, born Italy",1832,1909,1864–65,1864,1865,Albumen silver print from glass negative,Image: 17.9 x 14.6 cm (7 1/16 x 5 3/4 in.),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.263.20,false,true,282192,Photographs,Photograph,"[Man Standing in Front of Movie Theater, Omar, West Virginia]",,,,,,Artist,,Ben Shahn,"American (born Lithuania), Kaunas 1898–1969 New York",,"Shahn, Ben","American, born Lithuania",1898,1969,1935,1935,1935,Gelatin silver print,17.3 x 24.6 cm (6 13/16 x 9 11/16 in. ),"Walker Evans Archive, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.263.21,false,true,282193,Photographs,Photograph,"[""Prepare to Meet God"" Arrow-Shaped Sign in Cornfield, Williamson, West Virginia]",,,,,,Artist,,Ben Shahn,"American (born Lithuania), Kaunas 1898–1969 New York",,"Shahn, Ben","American, born Lithuania",1898,1969,October 1935,1935,1935,Gelatin silver print,18.8 x 22.8 cm (7 3/8 x 9 in. ),"Walker Evans Archive, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.488,false,true,286303,Photographs,Photograph,"Cotton Pickers, Pulaski County, Arkansas",,,,,,Artist,,Ben Shahn,"American (born Lithuania), Kaunas 1898–1969 New York",,"Shahn, Ben","American, born Lithuania",1898,1969,October 1935,1935,1935,Gelatin silver print,Image: 18.8 x 22.5 cm (7 3/8 x 8 7/8 in.) Mount: 35.6 x 27.8 cm (14 x 10 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.227,false,true,265274,Photographs,Photograph,[Smithland Bank],,,,,,Artist,,Ben Shahn,"American (born Lithuania), Kaunas 1898–1969 New York",,"Shahn, Ben","American, born Lithuania",1898,1969,1930s,1930,1939,Gelatin silver print,14.9 x 23.0 cm (5 7/8 x 9 1/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.285,false,true,265337,Photographs,Photograph,"[Street Scene, Natchez, Mississippi: Two Women Walking along Sidewalk before Storefront]",,,,,,Artist,,Ben Shahn,"American (born Lithuania), Kaunas 1898–1969 New York",,"Shahn, Ben","American, born Lithuania",1898,1969,1935,1935,1935,Gelatin silver print,26.6 x 34.1 cm (10 1/2 x 13 7/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.517.3,false,true,269851,Photographs,Photograph,"Mrs. Greenhow and Daughter, Imprisoned in the Old Capitol, Washington",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.1,false,true,259595,Photographs,Photograph,"Burying the Dead on the Battlefield of Antietam, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,Image: 7.1 x 10 cm (2 13/16 x 3 15/16 in.) Mount: 7.3 x 10.6 cm (2 7/8 x 4 3/16 in.),"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.2,false,true,259604,Photographs,Photograph,"View on the Battlefield of Antietam, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,Image: 7.1 x 10 cm (2 13/16 x 3 15/16 in.) Mount: 7.3 x 10.6 cm (2 7/8 x 4 3/16 in.),"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.3,false,true,259605,Photographs,Photograph,"View in the Field, On the West Side of the Hagerstown Road, After the Battle of Antietam, Maryland, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.5,false,true,259607,Photographs,Photograph,"Lutheran Church, Sharpsburgh, Maryland, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.6,false,true,259608,Photographs,Photograph,"Is This Death - Antietam Battlefield, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.7,false,true,259609,Photographs,Photograph,"Military Telegraphic Corps, Army of the Potomac, Berlin, October 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,Image: 6 11/16 in. × 9 in. (17 × 22.8 cm) Sheet: 10 1/16 in. × 12 in. (25.5 × 30.5 cm),"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.8,false,true,259610,Photographs,Photograph,"Group at Secret Service Department, Headquarters, Army of the Potomac, Antietam, October 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.9,false,true,259611,Photographs,Photograph,"Antietam Bridge, On the Sharpsburgh and Boonsboro Turnpike, No. 3, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.10,false,true,259596,Photographs,Photograph,"Antietam Bridge, On the Sharpsburg and Boonsboro Turnpike, No. 1, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.11,false,true,259597,Photographs,Photograph,"Antietam Bridge, On the Sharpsburg and Boonsboro Turnpike, No. 2, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.12,false,true,259598,Photographs,Photograph,"Pontoon Bridge, Across the Potomac, at Berlin, Maryland, November 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.13,false,true,259599,Photographs,Photograph,"Group at Headquarters of the Army of the Potomac, Antietam, October 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.14,false,true,259600,Photographs,Photograph,"Burnside Bridge, Across the Antietam, near Sharpsburg, No. 1, September 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.15,false,true,259601,Photographs,Photograph,"Group at Headquarters of the Army of the Potomac, Antietam, October 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.16,false,true,259602,Photographs,Photograph,"Pontoon Bridge Across the Potomac, Berlin, October 1862",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1158.2,false,true,263042,Photographs,Photograph,"Mrs. Greenhow and Daughter, Imprisoned in the Old Capitol, Washington",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,,"Gift of Mrs. A. Hyatt Mayor, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1166.2,false,true,264880,Photographs,Photograph,"Brigadier General Gustavus A. DeRussy and Staff on Steps of Arlington House, Arlington, Virginia",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,May 1864,1864,1864,Albumen silver print from glass negative,17.2 x 23cm (6 3/4 x 9 1/16in.) Mount: 27.7 x 31.8cm (10 7/8 x 12 1/2in.),"A. Hyatt Mayor Purchase Fund, Marjorie Phelps Starr Bequest, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.90,false,true,283192,Photographs,Photograph,[Antietam Battlefield],,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1862,1862,1862,Albumen silver print from glass negative,Image: 9.1 x 11.8cm (3 9/16 x 4 5/8in.) Mount: 4 3/16 in. × 4 15/16 in. (10.7 × 12.6 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.97,false,true,283201,Photographs,Photograph,Lewis Powell [alias Lewis Payne],,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,"April 27, 1865",1865,1865,Albumen silver print from glass negative,22.4 × 17.4 cm (8 13/16 × 6 7/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.243,false,true,286388,Photographs,Photographs,[Thaddeus Stevens Lying in State in the Rotunda of the Capitol at Washington],,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1867,1867,1867,Albumen silver print from glass negative,Image: 16.8 x 20.5 cm (6 5/8 x 8 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.251,false,true,283202,Photographs,Photograph,Execution of the Conspirators,,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,"July 7, 1865",1865,1865,Albumen silver print from glass negative,Image: 16.8 x 24.2 cm (6 5/8 x 9 1/2 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.563,false,true,286442,Photographs,Photograph,[Four Officers],,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,ca. 1864,1862,1866,Albumen silver print from glass negative,Image: 17.8 x 22.8 cm (7 x 9 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.785,false,true,285881,Photographs,Photograph,Queen Emma of Hawaii and Her Entourage,,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1865,1865,1865,Albumen silver print from glass negative,Image: 14 1/8 × 17 15/16 in. (35.9 × 45.6 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1118,false,true,286710,Photographs,Photographs,"Gardner's Gallery, 7th and D Streets, Washington, D.C.",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 2 15/16 × 4 1/8 in. (7.5 × 10.5 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1191,false,true,285846,Photographs,Photograph,"[Grand Army Review, Washington, D.C.]",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,May 1865,1865,1865,Albumen silver print from glass negative,Image: 3 3/4 × 4 1/2 in. (9.5 × 11.4 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1192,false,true,294721,Photographs,Photograph,"[Grand Army Review, Washington, D.C.]",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,May 1865,1865,1865,Albumen silver print from glass negative,Image: 3 3/4 × 4 1/2 in. (9.5 × 11.4 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1193,false,true,283204,Photographs,Photograph,"[Grand Army Review, Pennsylvania Avenue, Washington]",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,"May 23 or 24, 1865",1865,1865,Albumen silver print from glass negative,"8.8 × 9.9 cm (3 7/16 × 3 7/8 in.), irregularly trimmed","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1194,false,true,294722,Photographs,Photograph,"[Grand Army Review, Washington, D.C.]",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,May 1865,1865,1865,Albumen silver print from glass negative,Image: 3 3/4 × 20 1/2 in. (9.5 × 52.1 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1196,false,true,294725,Photographs,Photograph,"[Grand Army Review, Pennsylvania Avenue, Washington]",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,"May 23 or 24, 1865",1865,1865,Albumen silver print from glass negative,"8.5 × 10.1 cm (3 3/8 × 4 in.), irregularly trimmed","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1197,false,true,294726,Photographs,Photograph,"[Grand Army Review, Washington]",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,"May 23 or 24, 1865",1865,1865,Albumen silver print from glass negative,"8.2 × 10 cm (3 1/4 × 3 15/16 in.), irregularly trimmed","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1198,false,true,294724,Photographs,Photograph,"[Grand Army Review, Washington, D.C.]",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,May 1865,1865,1865,Albumen silver print from glass negative,Image: 3 3/4 × 4 1/2 in. (9.6 × 11.4 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1214,false,true,286583,Photographs,Photograph,[Bearded Man in Tweed Jacket],,,,,,Artist,Attributed to,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,early 1860s,1860,1860,Albumen silver print from glass negative,Image: 11 3/16 × 8 3/4 in. (28.4 × 22.2 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1215,false,true,286584,Photographs,Photograph,Lincoln Inauguration,,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,"March 4 ,1865",1865,1865,Albumen silver print from glass negative,Image: 7 1/4 × 9 1/4 in. (18.4 × 23.5 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1216,false,true,286585,Photographs,Photograph,General McClellan and Staff,,,,,,Artist,Attributed to,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,ca. 1863,1858,1868,Albumen silver print from glass negative,Image: 9 × 14 in. (22.9 × 35.6 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1219,false,true,286614,Photographs,Photographs,"Mill, Richmond, Virginia",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,ca. 1865,1863,1867,Albumen silver print from glass negative,Image: 6 3/4 × 8 7/8 in. (17.1 × 22.5 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1223,false,true,286617,Photographs,Photograph,Planning the Capture of Booth,,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1865,1865,1865,Albumen silver print from glass negative,Image: 27.1 × 24.5 cm (10 11/16 × 9 5/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1225,false,true,286441,Photographs,Photograph,"Cannon, Fortress Monroe",,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,ca. 1864,1859,1869,Albumen silver print from glass negative,Image: 7 15/16 × 9 3/4 in. (20.2 × 24.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1230,false,true,285619,Photographs,Photograph,Naval Blockade,,,,,,Artist,,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,1865,1865,1865,Albumen silver print from glass negative,Image: 7 in. × 9 5/8 in. (17.8 × 24.4 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1244,false,true,285756,Photographs,Photograph,[Black Soldier in Camp],,,,,,Artist,Possibly by,Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander","American, Scottish",1821,1882,ca. 1863,1861,1865,Albumen silver print from glass negative,Image: 6 in. × 8 7/16 in. (15.2 × 21.4 cm) Mount: 9 × 11 in. (22.9 × 27.9 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.92,false,true,268827,Photographs,Photograph,Sir John Herschel,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1867,1867,1867,Carbon Print,,"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.545,false,true,270819,Photographs,Photograph,Pomona,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1872,1872,1872,Albumen silver print from glass negative,Image: 36.4 x 26.3 cm (14 5/16 x 10 3/8 in.) Mount: 49.7 x 37.4 cm (19 9/16 x 14 3/4 in.),"David Hunter McAlpin Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.85.1,false,true,268300,Photographs,Photograph,Henry Taylor,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1867,1867,1867,Albumen silver print from glass negative,,"Gift of Lucy Chauncey, in memory of her father, Henry Chauncey, 1935",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.85.2,false,true,268301,Photographs,Photograph,"[The Lord Bishop of Winchester, Samuel Wilberforce]",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1872,1872,1872,Albumen silver print,,"Gift of Lucy Chauncey, in memory of her father, Henry Chauncey, 1935",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.85.3,false,true,268302,Photographs,Photograph,Sir John Herschel,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1867,1867,1867,Albumen silver print from glass negative,Image: 33.8 x 26.2cm (13 5/16 x 10 5/16in.) Mount: 58.3 x 46.2 cm (22 15/16 x 18 3/16 in.),"Gift of Lucy Chauncey, in memory of her father, Henry Chauncey, 1935",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.2,false,true,268702,Photographs,Photograph,La Madonna Riposata,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.3,false,true,268713,Photographs,Photograph,The Maid of Athens (May Prinsep),,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,28.9 x 23.2 cm (11 3/8 x 9 1/8 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.4,false,true,268714,Photographs,Photograph,Daisy,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.5,false,true,268715,Photographs,Photograph,Minnie Thackeray,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,24.8 x 19.6 cm. (9 3/4 x 7 3/4 in.),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.6,false,true,268716,Photographs,Photograph,Julia Herschel,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.7,false,true,268717,Photographs,Photograph,The Madonna Penserosa,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.8,false,true,268718,Photographs,Photograph,"Alfred, Lord Tennyson",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.9,false,true,268719,Photographs,Photograph,"Charles Hay Cameron, Esq., in His Garden at Freshwater",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865–67,1865,1867,Albumen silver print from glass negative,33.4 x 26.7 cm (13 1/8 x 10 1/2 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.692,false,true,260359,Photographs,Photograph,[Ceylonese Group by a Tree],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1878,1878,1878,Albumen silver print from glass negative,27 x 19.9 cm (10 5/8 x 7 13/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.17.17,false,true,268363,Photographs,Photograph,Thomas Carlyle,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1867,1867,1867,Albumen silver print,,"Gift of Edith Root Grant, Edward W. Root and Eliho Root Jr., 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.11,false,true,268693,Photographs,Photograph,Contemplations,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.12,false,true,268694,Photographs,Photograph,Daughters of Jerusalem,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,18.1 x 27.7 cm (7 1/8 x 10 7/8 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.13,false,true,268695,Photographs,Photograph,[Mary Hillier],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,ca. 1864–66,1864,1866,Albumen silver print from glass negative,19.8 x 14.8 cm (7 13/16 x 5 13/16 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.14,false,true,268696,Photographs,Photograph,Sappho,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,34.4 x 26.1 cm (13 9/16 x 10 1/4 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.15,false,true,268697,Photographs,Photograph,The Mountain Nymph Sweet Liberty,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,36.1 x 28.6 cm (14 3/16 x 11 1/4 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.17,false,true,268699,Photographs,Photograph,[James Rogers],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1867,1867,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.18,false,true,268700,Photographs,Photograph,May Prinsep,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1868,1868,1868,Albumen silver print from glass negative,22.5 x 20.5 cm. (8 7/8 x 8 1/16 in.),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.19,false,true,268701,Photographs,Photograph,The South West Wind,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,25.4 x 21.6 cm (10 x 8 1/2 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.20,false,true,268703,Photographs,Photograph,The Vicar of Freshwater,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,26.4 x 20.8 cm (10 3/8 x 8 3/16 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.22,false,true,268705,Photographs,Photograph,Tennyson Reading,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.23,false,true,268706,Photographs,Photograph,"Henry Taylor. Author of ""Philip Van Artevelde""",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.24,false,true,268707,Photographs,Photograph,Henry Taylor,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,25.5 x 20.1 cm (10 1/16 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.25,false,true,268708,Photographs,Photograph,Henry Taylor,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,26.1 x 20.8 cm (10 1/4 x 8 3/16 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.26,false,true,268709,Photographs,Photograph,Christabel,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,33.2 x 26.9 cm (13 1/16 x 10 9/16 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.27,false,true,268710,Photographs,Photograph,Herr Joachim,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1868,1868,1868,Albumen silver print from glass negative,29.5 x 24.3 cm (11 5/8 x 9 9/16 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.28,false,true,268711,Photographs,Photograph,William Gifford Palgrave,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1868,1868,1868,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.29,false,true,268712,Photographs,Photograph,"Lionel Tennyson, Freshwater",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1869,1869,1869,Albumen silver print from glass negative,Image: 24.5 x 30.4 cm (9 5/8 x 11 15/16 in.),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.633.1,false,true,271077,Photographs,Photograph,"Alfred, Lord Tennyson",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,"Image: 22.9 x 18.4 cm (9 x 7 1/4 in.), rounded top Mount: 40.5 x 24.6 cm (15 15/16 x 9 11/16 in.), irregular","David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.633.2,false,true,271078,Photographs,Photograph,"Alfred, Lord Tennyson",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,ca. 1865,1863,1867,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.1,false,true,271510,Photographs,Photograph,[Unidentified Child],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1873,1873,1873,Albumen silver print from glass negative,33.9 x 24.3 cm (13 3/8 x 9 9/16 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.2,false,true,271515,Photographs,Photograph,[Mary Ryan],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865–66,1865,1866,Albumen silver print from glass negative,33.1 x 24.7 cm (13 1/16 x 9 3/4 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.3,false,true,271516,Photographs,Photograph,Ceylonese Woman,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1875–79,1875,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.4,false,true,271517,Photographs,Photograph,"Mrs. Halford Vaugham, Freshwater",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1873,1873,1873,Albumen silver print,33.5 x 27.3 cm (13 3/16 x 10 3/4 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.7,false,true,271520,Photographs,Photograph,Marie Spartali,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1868,1868,1868,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.8,false,true,271521,Photographs,Photograph,Aubrey de Vere,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866–68,1866,1868,Albumen silver print,32.5 x 26.8 cm (12 13/16 x 10 9/16 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.9,false,true,271522,Photographs,Photograph,Beatrice,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,36.8 x 29.0 cm (14 1/2 x 11 7/16 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.99.2,true,true,267426,Photographs,Photograph,Julia Jackson,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1867,1867,1867,Albumen silver print from glass negative,27.4 x 20.6 cm (10 13/16 x 8 1/8 in.),"Purchase, Joseph Pulitzer Bequest, 1996",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.1.1,false,true,268690,Photographs,Photograph,Charles Hay Cameron,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.1.2,false,true,268691,Photographs,Photograph,A Study,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1864,1864,1864,Albumen silver print from glass negative,21.7 x 17.6 cm (8 9/16 x 6 15/16 in. ),"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.10,false,true,271511,Photographs,Photograph,[Unidentified Woman in Profile],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866–68,1866,1868,Albumen silver print,33.9 x 25.8 cm (13 3/8 x 10 3/16 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.11,false,true,271512,Photographs,Photograph,[Woman in Robes Reading a Book],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1870,1870,1870,Albumen silver print from glass negative,35.1 x 27.3 cm (13 13/16 x 10 3/4 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.12,false,true,271513,Photographs,Photograph,May. Freshwater,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1870,1870,1870,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.607.13,false,true,271514,Photographs,Photograph,[Unidentified Child],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1873,1873,1873,Albumen silver print,32.3 x 24.4 cm (12 11/16 x 9 5/8 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.502.2,false,true,260731,Photographs,Photograph,[Egeria],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,36.2 x 27.6 cm. (14 1/4 x 10 7/8 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.3,false,true,306204,Photographs,Photograph,King Lear Alotting His Kingdom to His Three Daughters,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1872,1872,1872,Albumen silver print from glass negative,Mount: 18 1/2 × 14 7/8 in. (47 × 37.8 cm) Image: 13 1/8 × 11 1/16 in. (33.4 × 28.1 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.1,false,true,282128,Photographs,Photograph,Gareth and Lynette,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,33.9 x 28.2 cm (13 3/8 x 11 1/8 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.2,false,true,282119,Photographs,Photograph,Enid,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,September 1874,1874,1874,Albumen silver print from glass negative,34.2 x 26.7 cm (13 7/16 x 10 1/2 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.3,false,true,282120,Photographs,Photograph,And Enid Sang,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,September 1874,1874,1874,Albumen silver print from glass negative,35.4 x 28 cm (13 15/16 x 11 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.4,false,true,282121,Photographs,Photograph,Vivien and Merlin,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,September 1874,1874,1874,Albumen silver print from glass negative,31.9 x 28 cm (12 9/16 x 11 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.5,false,true,282118,Photographs,Photograph,Vivien and Merlin,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,30.4 x 25.3 cm (11 15/16 x 9 15/16 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.6,false,true,282122,Photographs,Photograph,Elaine the Lily - Maid of Astolat,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,34.3 x 28.4 cm (13 1/2 x 11 3/16 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.7,false,true,282129,Photographs,Photograph,Elaine,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,33.5 x 27.8 cm (13 3/16 x 10 15/16 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.8,false,true,282147,Photographs,Photograph,Sir Galahad and the Pale Nun,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,33.2 x 27.5 cm (13 1/16 x 10 13/16 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.9,false,true,282148,Photographs,Photograph,Queen Guinevere,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,34.1 x 25.5 cm (13 7/16 x 10 1/16 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1074.1,false,true,266254,Photographs,Photograph,The Passing of Arthur,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,35.2 x 25.2 cm. (13 7/8 x 9 15/16 in.),"Bequest of James David Nelson, in memory of Samuel J. Wagstaff Jr., 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1074.2,false,true,266255,Photographs,Photograph,Beatrice,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,34.6 x 26.3 cm. (13 5/8 x 10 3/6 in.),"Bequest of James David Nelson, in memory of Samuel J. Wagstaff Jr., 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1074.3,false,true,266256,Photographs,Photograph,A Study,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865–66,1865,1866,Albumen silver print from glass negative,34.4 x 26.4 cm. (13 9/16 x 10 3/8 in.),"Bequest of James David Nelson, in memory of Samuel J. Wagstaff Jr., 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1074.4,false,true,266257,Photographs,Photograph,Circe,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,25.3 x 20.1 cm. (9 15/16 x 7 15/16 in.),"Bequest of James David Nelson, in memory of Samuel J. Wagstaff Jr., 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1074.5,false,true,266258,Photographs,Photograph,A Lovely Sketch,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1873,1873,1873,Albumen silver print from glass negative,31.0 x 24.3 cm. (12 3/16 x 9 9/16 in.),"Bequest of James David Nelson, in memory of Samuel J. Wagstaff Jr., 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1074.6,false,true,266259,Photographs,Photograph,English Blossoms,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1873,1873,1873,Albumen silver print from glass negative,32.8 x 27.5 cm. (12 15/16 x 10 13/16 in.),"Bequest of James David Nelson, in memory of Samuel J. Wagstaff Jr., 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1074.7,false,true,266260,Photographs,Carte-de-visite,Summer Days,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866–69,1866,1869,Albumen silver print from glass negative,8.4 x 5.7 cm. (3 5/16 x 2 1/4 in.),"Bequest of James David Nelson, in memory of Samuel J. Wagstaff Jr., 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.36,false,true,282041,Photographs,Photograph,"Alfred, Lord Tennyson",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,"July 4, 1866",1866,1866,Albumen silver print from glass negative,35 x 27 cm (13 3/4 x 10 5/8 in. ) irregular,"The Rubel Collection, Purchase, Lila Acheson Wallace, Michael and Jane Wilson, and Harry Kahn Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.37,false,true,282042,Photographs,Photograph,Cassiopeia,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,34.9 x 27.4 cm (13 3/4 x 10 13/16 in. ),"The Rubel Collection, Purchase, Lila Acheson Wallace, Harry Kahn, and Ann Tenenbaum and Thomas H. Lee Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.38,true,true,282043,Photographs,Photograph,"Zoe, Maid of Athens",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,30.1 x 24.5 cm (11 7/8 x 9 5/8 in.),"The Rubel Collection, Purchase, Lila Acheson Wallace, Ann Tenenbaum and Thomas H. Lee, and Muriel Kallis Newman Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.39,false,true,282044,Photographs,Photograph,Sappho,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1865,1865,1865,Albumen silver print from glass negative,35 x 27.3 cm (13 3/4 x 10 3/4 in. ),"The Rubel Collection, Purchase, Jennifer and Joseph Duke and Anonymous Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.25,false,true,283097,Photographs,Photograph,Sir John Herschel,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,April 1867,1867,1867,Albumen silver print from glass negative,"Image: 31.8 x 24.9 cm (12 1/2 x 9 13/16 in.) Mount: 39.9 x 32.9 cm (15 11/16 x 12 15/16 in.), corners clipped","Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.26,false,true,283098,Photographs,Photograph,Mrs. Herbert Duckworth,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1867,1867,1867,Albumen silver print from glass negative,32.8 x 23.7 cm (12 15/16 x 9 5/16 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.27,true,true,283099,Photographs,Photograph,Philip Stanhope Worsley,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,"Image: 30.4 x 25 cm (11 15/16 x 9 13/16 in.) Mount: 40.9 x 30.6 cm (16 1/8 x 12 1/16 in.), irregular","Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.10,false,true,269583,Photographs,Photograph,The Parting of Lancelot and Guinevere,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,Image: 33.2 x 28.8 cm (13 1/16 x 11 5/16 in.) Mount: 44 x 33.3cm (17 5/16 x 13 1/8in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.11,false,true,282149,Photographs,Photograph,King Arthur,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,September 1874,1874,1874,Albumen silver print from glass negative,35.9 x 28 cm (14 1/8 x 11 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.3.12,false,true,282150,Photographs,Photograph,The Passing of King Arthur,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1874,1874,1874,Albumen silver print from glass negative,35 x 27.3 cm (13 3/4 x 10 3/4 in. ),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.18,false,true,263166,Photographs,Photograph,Sir John Herschel,,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1875,1875,1875,Carbon print,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.239,false,true,286351,Photographs,Photograph,"Déjatch Alámayou, King Theodore's Son",,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,July 1868,1868,1868,Albumen silver print from glass negative,Image: 29.2 x 23.3 cm (11 1/2 x 9 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.265,false,true,286404,Photographs,Photograph,[Kate Keown],,,,,,Artist,,Julia Margaret Cameron,"British (born India), Calcutta 1815–1879 Kalutara, Ceylon",,"Cameron, Julia Margaret","British, born India",1815,1815,1866,1866,1866,Albumen silver print from glass negative,Image: 29 x 29 cm (11 7/16 x 11 7/16 in.) circle,"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.275,false,true,285429,Photographs,Photograph,[Climbing the Mast],,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1928,1928,1928,Gelatin silver print,Image: 35.4 x 28 cm (13 15/16 x 11 in.),"Purchase, several members of The Chairman's Council Gifts, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1163,false,true,263068,Photographs,Photograph,[Lucia Moholy; Negative Print],,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1924–28,1924,1928,Gelatin silver print,23.6 x 17.5 cm (9 5/16 x 6 7/8 in.),"Warner Communications Inc. Purchase Fund, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1183,false,true,264891,Photographs,Photograph,Fischernetze auf Isola Bella,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,ca. 1930,1928,1932,Gelatin silver print,17.5 x 23.5 cm. (6 7/8 x 9 1/4 in.),"Gift of Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1150.4,false,true,264515,Photographs,Photograph,"[Cat, Seen From Above]",,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,ca. 1926,1924,1928,Gelatin silver print,23.4 x 17.5 cm. (9 3/16 x 6 7/8 in.),"Gift of Emanuel Gerard, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.23,false,true,265277,Photographs,Photograph,Behind the Back of the Gods,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1928,1928,1928,Gelatin silver print,36.0 x 27.4 cm (14 3/16 x 10 13/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.152,false,true,283290,Photographs,Photograph,"Decorating Work, Switzerland",,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1925,1925,1925,Gelatin silver print,Image: 50.6 x 40.2 cm (19 15/16 x 15 13/16 in. ),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.298,false,true,285944,Photographs,Photograph,"Pont Transbordeur, Marseille",,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1929,1929,1929,Gelatin silver print,Image: 23.7 x 17.9 cm (9 5/16 x 7 1/16 in.) Mount: 25.3 x 19.2 cm (9 15/16 x 7 9/16 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.437,false,true,283291,Photographs,Photograph,Dolls on the Balcony,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1926,1926,1926,Gelatin silver print,23.5 x 17.5 cm (9 1/4 x 6 7/8 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.444,false,true,285731,Photographs,Photograph,"From the Radio Tower, Berlin",,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,"1928, printed ca. 1940",1928,1940,Gelatin silver print,Image: 24.6 x 19.1 cm (9 11/16 x 7 1/2 in.) Mount: 38.5 x 26.8 cm (15 3/16 x 10 9/16 in.),"Gilman Collection, Purchase, Gift of Ford Motor Company and John C. Waddell, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.155,false,true,265194,Photographs,Photograph,7 A.M. (New Year's Morning),,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,ca. 1930,1928,1932,Gelatin silver print,27.8 x 21.3 cm (10 15/16 x 8 3/8 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.158,false,true,265197,Photographs,Photogram,Fotogramm,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1926,1926,1926,Gelatin silver print,23.9 x 17.9 cm (9 7/16 x 7 1/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.159,false,true,265198,Photographs,Photogram,Fotogramm,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1925–1928,1925,1928,Gelatin silver print,23.9 x 17.9 cm. (9 7/16 x 7 1/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.160,false,true,265200,Photographs,Photogram,Fotogramm,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1925,1925,1925,Gelatin silver print,23.8 x 17.8 cm (9 3/8 x 7 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.228,false,true,265275,Photographs,Photogram,Fotogramm,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1922,1922,1922,Gelatin silver print,17.8 x 23.7 cm (7 x 9 5/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.230,false,true,265278,Photographs,Photograph,Target Practice (In the Name of the Law),,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,ca. 1927,1925,1929,Gelatin silver print,24.0 x 18.2 cm (9 7/16 x 7 3/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.231,false,true,265279,Photographs,Photograph,Lucia,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1924–28,1924,1928,Gelatin silver print,8.2 x 5.4 cm (3 1/4 x 2 1/8 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.463,false,true,265536,Photographs,Photograph,Lucia Moholy,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1920s,1920,1929,Gelatin silver print,10.0 x 7.2 cm. (3 15/16 x 2 13/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.499,false,true,283698,Photographs,Photograph,Scandinavia,,,,,,Artist,,László Moholy-Nagy,"American (born Hungary), Borsod 1895–1946 Chicago, Illinois",,"Moholy-Nagy, László","American, born Hungary",1895,1946,1930,1930,1930,Gelatin silver print,23.5 x 17.1 cm (9 1/4 x 6 3/4 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.5,false,true,283077,Photographs,Photograph,[Trees],,,,,,Artist,,Thomas Keith,"British, Kincardine, Aberdeenshire, Scotland 1827–1895 London",,"Keith, Thomas","British, Scottish",1827,1895,1854–57,1854,1857,Salted paper print from paper negative,Image: 30.3 x 22.1 cm (11 15/16 x 8 11/16 in.) Mount: 54.8 x 38.3 cm (21 9/16 x 15 1/16 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.918,false,true,285847,Photographs,Photograph,"Tropical Scenery, Street, Chipigana",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20.3 x 27.9 cm (8 x 11 in.) Mount: 40.6 x 50.8 cm (16 x 20 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.919,false,true,286179,Photographs,Photograph,"Tropical Scenery, Native Hut, Turbo",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20.3 x 27.9 cm (8 x 11 in.) Mount: 40.6 x 50.8 cm (16 x 20 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.920,false,true,286180,Photographs,Photograph,"Tropical Scenery, Tropical Forest",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 27.9 x 20.3 cm (11 x 8 in.) Mount: 50.8 x 40.6 cm (20 x 16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.921,false,true,286181,Photographs,Photograph,"Tropical Scenery, View Near Chipigana",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20.3 x 27.9 cm (8 x 11 in.) Mount: 40.6 x 50.8 cm (16 x 20 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.922,false,true,286182,Photographs,Photograph,"Tropical Scenery, Turbo Village",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20.3 x 27.9 cm (8 x 11 in.) Mount: 40.6 x 50.8 cm (16 x 20 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.923,false,true,286183,Photographs,Photograph,"Tropical Scenery, The Brook El Bano, Chipigana",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 27.9 x 20.3 cm (11 x 8 in.) Mount: 50.8 x 40.6 cm (20 x 16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.926,false,true,286185,Photographs,Photograph,"Tropical Scenery, Landing, Chipigana",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.6 cm (7 7/8 x 10 7/8 in.) Mount: 30.5 x 38.1 cm (12 x 15 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.927,false,true,286186,Photographs,Photograph,"Tropical Scenery, Natural Arch, Cupica Bay",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 27.6 x 20.2 cm (10 7/8 x 7 15/16 in.) Mount: 38.1 x 30.5 cm (15 x 12 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.928,false,true,286187,Photographs,Photograph,"Tropical Scenery, Cascade, Limon River",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 27.3 x 20 cm (10 3/4 x 7 7/8 in.) Mount: 38.1 x 30.5 cm (15 x 12 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.929,false,true,286188,Photographs,Photograph,"Tropical Scenery, Native Hut, Turbo",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.3 cm (7 7/8 x 10 3/4 in.) Mount: 30.5 x 38.1 cm (12 x 15 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.930,false,true,286189,Photographs,Photograph,"Tropical Scenery, Tropical Forest",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 27.6 x 20 cm (10 7/8 x 7 7/8 in.) Mount: 38.1 x 30.5 cm (15 x 12 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.931,false,true,286190,Photographs,Photograph,"Tropical Scenery, Cathedral, Cartagena",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.6 cm (7 7/8 x 10 7/8 in.) Mount: 30.5 x 38.1 cm (12 x 15 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.932,false,true,286191,Photographs,Photograph,"Tropical Scenery, Darien Harbor, Chipigana",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.3 cm (7 7/8 x 10 3/4 in.) Mount: 30.5 x 38.1 cm (12 x 15 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.933,false,true,285991,Photographs,Photograph,"Tropical Scenery, Darien Harbor - Looking South",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.6 cm (7 7/8 x 10 7/8 in.) Mount: 30.5 x 38.3 cm (12 x 15 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.934,false,true,285992,Photographs,Photograph,"Tropical Scenery, The Terminus of the Proposed Canal, Limon Bay",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.3 cm (7 7/8 x 10 3/4 in.) Mount: 30.5 x 38.2 cm (12 x 15 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.935,false,true,285993,Photographs,Photograph,"Tropical Scenery, View of Limon Bay",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print,Image: 20 x 27.3 cm (7 7/8 x 10 3/4 in.) Mount: 30.5 x 38.2 cm (12 x 15 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.936,false,true,286040,Photographs,Photograph,"Tropical Scenery, Santa Maria del Real, Darien",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.3 cm (7 7/8 x 10 3/4 in.) Mount: 30.5 x 38.2 cm (12 x 15 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.937,false,true,286041,Photographs,Photograph,"Tropical Scenery, Cliff - Limon Bay",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.3 cm (7 7/8 x 10 3/4 in.) Mount: 30.5 x 38.2 cm (12 x 15 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.938,false,true,286042,Photographs,Photograph,"Tropical Scenery, Limon Bay - Low Tide",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 20 x 27.6 cm (7 7/8 x 10 7/8 in.) Mount: 30.6 x 38.2 cm (12 1/16 x 15 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.939,false,true,286560,Photographs,Photograph,"Tropical Scenery, Forest Near Turbo",,,,,,Artist,,John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Moran, John","American, born England",1821,1903,1871,1871,1871,Albumen silver print from glass negative,Image: 27.3 x 20 cm (10 3/4 x 7 7/8 in.) Mount: 38.2 x 30.6 cm (15 1/16 x 12 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.680.6,false,true,269669,Photographs,Photograph,A Holiday Visit,,,,,,Artist,,Arnold Genthe,"American (born Germany), Berlin 1869–1942 New Milford, Connecticut",,"Genthe, Arnold","American, born Germany",1869,1942,1895–1908,1895,1908,Gelatin silver print,Image: 34 x 22.8 cm (13 3/8 x 9 in.),"Gift of Mrs. Eustace Seligman, 1953",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.223,false,true,267631,Photographs,Photograph,"After the Earthquake, San Francisco",,,,,,Artist,,Arnold Genthe,"American (born Germany), Berlin 1869–1942 New Milford, Connecticut",,"Genthe, Arnold","American, born Germany",1869,1942,1906,1906,1906,Gelatin silver print,13.3 x 23.5 cm (5 1/4 x 9 1/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.510.5,false,true,261240,Photographs,Photograph,"John D. Rockefeller, Jr.",,,,,,Artist,,Arnold Genthe,"American (born Germany), Berlin 1869–1942 New Milford, Connecticut",,"Genthe, Arnold","American, born Germany",1869,1942,ca. 1925,1923,1927,Gelatin silver print,Image: 19.2 x 23.8 cm (7 9/16 x 9 3/8 in.) Mount: 20.2 x 25 cm (7 15/16 x 9 13/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.16,false,true,283088,Photographs,Photograph,[Stag in Cart],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1856,1860,Albumen silver print from glass negative,Image: 26.7 x 32.6 cm (10 1/2 x 12 13/16 in.) Mount: 35.7 x 43.6 cm (14 1/16 x 17 3/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.17,false,true,283089,Photographs,Photograph,[Tree],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1856,1860,Albumen silver print from glass negative,Mount: 14 1/16 in. × 17 3/4 in. (35.7 × 45.1 cm) Image: 9 3/4 × 12 1/16 in. (24.7 × 30.7 cm),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (1),false,true,288053,Photographs,Photograph,"[Miss Macrae of Inverinate, Wife of Horatio Ross]",,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,"19.8 x 15 cm (7 13/16 x 5 7/8 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (2),false,true,288054,Photographs,Photograph,"[Peel Ross, Son of Horatio Ross]",,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,"19.9 x 15.1 cm (7 13/16 x 5 15/16 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (3),false,true,288055,Photographs,Photograph,"[Edward Ross, Youngest Son of Horatio Ross]",,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,20.7 x 15.1 cm (8 1/8 x 5 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (4),false,true,288056,Photographs,Photograph,[Ned and Colin Ross with Hunt Trophy],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1857,1857,1857,Salted paper print,14.9 x 19.8 cm (5 7/8 x 7 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (5),false,true,288057,Photographs,Photograph,[Mrs. Kennedy],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,18.4 x 14.9 cm (7 1/4 x 5 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (6),false,true,288058,Photographs,Photograph,"[Macrae, Ross, and Warner Families Outdoors]",,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,15.3 x 18.8 cm (6 x 7 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (7),false,true,288059,Photographs,Photograph,[Picnic Near a Stream],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856,1856,1856,Salted paper print,12.9 x 18.1 cm (5 1/16 x 7 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (8),false,true,288060,Photographs,Photograph,"[Stag Trophy Head, Killed by Ned Ross]",,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Albumen silver print,"17.9 x 14.9 cm (7 1/16 x 5 7/8 in.), corners trimmed diagonally","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (9),false,true,288061,Photographs,Photograph,"[Two Stags, One Shot by Mr. Ross and the Other by Mrs. Ross]",,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,"14.3 x 17.8 cm (5 5/8 x 7 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (10),false,true,288062,Photographs,Photograph,[Dead Roe Buck],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Albumen silver print,14.6 x 18.5 cm (5 3/4 x 7 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (11),false,true,288063,Photographs,Photograph,[Two Stags and Roe Buck],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,15.9 x 20.6 cm (6 1/4 x 8 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (12),false,true,288064,Photographs,Photograph,Jack Gralloching a Stag,,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–58,1856,1858,Salted paper print,17 x 19.3 cm (6 11/16 x 7 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (13),false,true,288065,Photographs,Photograph,[Dead Stag],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856,1856,1856,Salted paper print,19.2 x 24.2 cm (7 9/16 x 9 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (14),false,true,286900,Photographs,Photograph,[Dead Stag],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1857,1857,1857,Salted paper print,19.1 x 23.4 cm (7 1/2 x 9 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (15),false,true,288066,Photographs,Photograph,[Dead Female Deer and Game Bird],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Salted paper print,"17.8 x 23.8 cm (7 x 9 3/8 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (16),false,true,288067,Photographs,Photograph,[Stag Trophy Head Killed by Ned Ross],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1857,1857,1857,Albumen silver print,19.6 x 15.5 cm (7 11/16 x 6 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (17),false,true,288068,Photographs,Photograph,[Stags Heads - Dibedale],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856,1856,1856,Salted paper print,16 x 20.4 cm (6 5/16 x 8 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (18),false,true,288069,Photographs,Photograph,[Portrait of a Seated Gentleman],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Albumen silver print,18.1 x 14.3 cm (7 1/8 x 5 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (19),false,true,288070,Photographs,Photograph,[Portrait of Major Anderson],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Albumen silver print,18.9 x 15.6 cm (7 7/16 x 6 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (20),false,true,288071,Photographs,Photograph,[Portrait of Man in Hunting Garb],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Salted paper print,19.5 x 14.2 cm (7 11/16 x 5 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (21),false,true,288072,Photographs,Photograph,[Old Tom],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Salted paper print,20 x 15.1 cm (7 7/8 x 5 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (22),false,true,288073,Photographs,Photograph,[Elegant Group Outdoors],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Salted paper print,"14.8 x 19.7 cm (5 13/16 x 7 3/4 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (23),false,true,288074,Photographs,Photograph,[Peel Ross Fishing],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Albumen silver print,"16.6 x 20.5 cm (6 9/16 x 8 1/16 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (24),false,true,288075,Photographs,Photograph,[Peel Ross with Hunting Trophies],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Albumen silver print,18.7 x 22.2 cm (7 3/8 x 8 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (25),false,true,288076,Photographs,Photograph,[Charlie and Peel Ross with Horse after a Hunt],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Albumen silver print,"21.5 x 18.7 cm (8 7/16 x 7 3/8 in.), arched top","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (26),false,true,288077,Photographs,Photograph,[Colin and Horatio Ross Reading with Jessie Macrae],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Albumen silver print,23.2 x 19.4 cm (9 1/8 x 7 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (27),false,true,288078,Photographs,Photograph,[Man and Boys Fishing],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Salted paper print,19.3 x 23.1 cm (7 5/8 x 9 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (28),false,true,288079,Photographs,Photograph,[Stag Shot by Mrs. Ross],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1857,1857,1857,Salted paper print,13.5 x 23.5 cm (5 5/16 x 9 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (29),false,true,288080,Photographs,Photograph,[View in the Gardens at Netherley],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856–59,1856,1859,Albumen silver print,19.5 x 24.6 cm (7 11/16 x 9 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (30),false,true,288081,Photographs,Photograph,[Colin's Royal Stag],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1856,1856,1856,Salted paper print,19.9 x 14.4 cm (7 13/16 x 5 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (31),false,true,288082,Photographs,Photograph,[Spying in Glenfeshie],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1858,1858,1858,Salted paper print,18.7 x 23.4 cm (7 3/8 x 9 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (32),false,true,288083,Photographs,Photograph,[Hunters Stalking a Deer],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1857,1857,1857,Salted paper print,13.6 x 18.9 cm (5 3/8 x 7 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (33),false,true,288084,Photographs,Photograph,[Prize Cow and Calf],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1859,1859,1859,Salted paper print,15.6 x 19.7 cm (6 1/8 x 7 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.552 (34),false,true,288085,Photographs,Photograph,[Portrait of a Man in Military Regalia],,,,,,Artist,,Horatio Ross,"British, Rossie Castle, near Montrose, Scotland 1801–1886 Scotland",,"Ross, Horatio","British, Scottish",1801,1886,ca. 1859,1859,1859,Salted paper print,19.5 x 15.2 cm (7 11/16 x 6 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.424,false,true,298968,Photographs,Photographs,Aberdeen Portraits No. 1,,,,,,Artist,,George Washington Wilson,"British, Grampian (Baffshire), Scotland 1823–1893 Abedeen, Scotland",,"Wilson, George Washington","British, Scottish",1823,1893,1857,1857,1857,Albumen silver print from glass negative,Image: 21.3 x 17.2 cm (8 3/8 x 6 3/4 in.) Mount: 37.7 x 30 cm (14 13/16 x 11 13/16 in.) Frame: 43.2 x 35.6 cm (17 x 14 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/298968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1197,false,true,265002,Photographs,Photograph,"Excised Knee Joint. A Round Musket Ball in the Inner Condyle of the Right Femur [Gardiner Lewis, Company B, Nineteenth Indiana Volunteers]",,,,,,Artist,,William Bell,"American (born England) Liverpool 1831–1910 Philadelphia, Pennsylvania",,"Bell, William","American, born Britain",1831,1910,1866–67,1866,1867,Albumen silver print from glass negative,19 x 15.3cm (7 1/2 x 6in.) Mount: 35.4 x 27.6cm (13 15/16 x 10 7/8in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1073,false,true,265934,Photographs,Photograph,"Grand Canyon, Colorado River, Near Paria Creek, Looking West",,,,,,Artist,,William Bell,"American (born England) Liverpool 1831–1910 Philadelphia, Pennsylvania",,"Bell, William","American, born Britain",1831,1910,1872,1872,1872,Albumen silver print from glass negative,27.3 x 20.1 cm. (10 3/4 x 7 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.675.3,false,true,262679,Photographs,Photograph,"Limestone Walls, Kanab Wash, Colorado River",,,,,,Artist,,William Bell,"American (born England) Liverpool 1831–1910 Philadelphia, Pennsylvania",,"Bell, William","American, born Britain",1831,1910,1872,1872,1872,Albumen silver print from glass negative,,"Gift of Mr. and Mrs. Weston J. Naef, in memory of Edward Dawes Meanor, 1979",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.675.4,false,true,262680,Photographs,Photograph,"Grand Cañon of the Colorado River, Mouth of Kanab Wash, looking West",,,,,,Artist,,William Bell,"American (born England) Liverpool 1831–1910 Philadelphia, Pennsylvania",,"Bell, William","American, born Britain",1831,1910,1872,1872,1872,Albumen silver print from glass negative,,"Gift of Mr. and Mrs. Weston J. Naef, in memory of Edward Dawes Meanor, 1979",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.585 (10),false,true,287291,Photographs,Photograph,"Cañon of Kanab Wash, Looking South",,,,,,Artist,,William Bell,"American (born England) Liverpool 1831–1910 Philadelphia, Pennsylvania",,"Bell, William","American, born Britain",1831,1910,1872,1872,1872,Albumen silver print from glass negative,Image: 28.2 x 20.2 cm (11 1/8 x 7 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1106,false,true,266273,Photographs,Photograph,"Street in Fatehpur Sikri, India",,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1858–62,1858,1862,Albumen silver print from paper negative,Image: 36.8 x 45 cm (14 1/2 x 17 11/16 in.) Mount: 53.4 x 73 cm (21 x 28 3/4 in.),"Purchase, The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel and Cynthia Hazen Polsky Gift, 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1134.2,false,true,266026,Photographs,Photograph,"The Diwan-i Khas from the Mussaman Burj, Agra Palace",,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1862–64,1862,1864,Albumen silver print from paper negative,40.6 x 44.3cm (16 x 17 7/16in.) Frame: 68.7 x 122.6 cm (27 1/16 x 48 1/4 in.) (Framed with 1988.1134.1),"Purchase, Cynthia Hazen Polsky Gift, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.70,false,true,283161,Photographs,Photograph,The Chowk,,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1856–57,1856,1857,Salted paper print from paper negative,Image: 37.4 x 46.5 cm (14 3/4 x 18 5/16 in.) Mount: 45.4 x 53.9 cm (17 7/8 x 21 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.71,false,true,283162,Photographs,Photograph,[The Taj Mahal from the Banks of the Yamuna River],,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1858–62,1858,1862,Albumen silver print from paper negative,Image: 39.9 x 44 cm (15 11/16 x 17 5/16 in.),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.945,false,true,286085,Photographs,Photograph,"Suttee Ghat, Cawnpore",,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1858,1858,1858,Albumen silver print from paper negative,Image: 33 x 43.1 cm (13 x 16 15/16 in.) Mount: 40.3 x 51.6 cm (15 7/8 x 20 5/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1111,false,true,286082,Photographs,Photograph,"Fort Agra, The Delhi Gate",,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,1850s,1850,1859,Albumen silver print from paper negative,Image: 38.6 x 44.5 cm (15 3/16 x 17 1/2 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.315a–c,false,true,286084,Photographs,Panorama,[The Taj Mahal from the Gateway],,,,,,Artist,,John Murray,"British, Blackhouse, Aberdeenshire, Scotland 1809–1898 Sheringham, Norfolk county, England",,"Murray, John","British, Scottish",1809,1898,January–March 1864,1864,1864,Albumen silver prints from waxed paper negatives,Image: 35 x 127 cm (13 3/4 x 50 in.) overall,"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.37,false,true,669902,Photographs,Photograph,"Second Ave Rock Island, Ill. during high water",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1888,1888,1888,Cyanotype,"Image: 10 3/8 × 13 1/16 in. (26.4 × 33.2 cm), oval Sheet: 14 7/16 × 17 3/16 in. (36.7 × 43.7 cm)","Purchase, Acquisitions Fund and Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.715.16,false,true,669919,Photographs,Photograph,"No. 201. U.S. Government Bridge at Rock Island, Illinois (High Water)",,,,,,Artist,,Henry P. Bosse,"American, born Germany, 1844–1893",,"Bosse, Henry P.","American, born Germany",1844,1893,1888,1888,1888,Cyanotype,Sheet: 14 1/2 × 17 3/16 in. (36.8 × 43.7 cm),"Gift of Charles Wehrenberg and Sally Larsen, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.1,false,true,266429,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation of Consecutive Phases of Animal Movements. Commenced 1872 - Completed 1885. Volume I, Men (Nude)",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.2,false,true,266432,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation of Consecutive Phases of Animal Movements. Commenced 1872 - Completed 1885. Volume II, Men (Nude)",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.4,false,true,266434,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation of Consecutive Phases of Animal Movements. Commenced 1872 - Completed 1885. Volume IV, Women (Nude)",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.5,false,true,266435,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation of Consecutive Phases of Animal Movements. Commenced 1872 - Completed 1885. Volume V, Man (Pelvis Cloth)",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.6,false,true,266436,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation... of Animal Movements. Commenced 1872 - Completed 1885. Volume VI, Woman (Semi-Nude and Transparent Drapery) Children",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.7,false,true,266437,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation... of Animal Movements. Commenced 1872 - Completed 1885. Volume VII, Men and Woman (Draped) Miscellaneous Subjects",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.9,false,true,266439,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation of Consecutive Phases of Animal Movements. Commenced 1872 - Completed 1885. Volume IX, Horses",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1135.11,false,true,266431,Photographs,Photographically illustrated book,"Animal Locomotion. An Electro-Photographic Investigation of Consecutive Phases of Animal Movements. Commenced 1872 - Completed 1885. Volume XI, Wild Animals and Birds",,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard","American, born Britain",1830,1904,1880s,1880,1889,Photogravures,,"Rogers Fund, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.525.1,false,true,287979,Photographs,Photograph,[Studio Portrait of a Japanese Man in Western Clothing],,,,,,Artist,,Raimund von Stillfried,"Austrian, 1839–1911",,"Stillfried, Raimond von",Austrian,1839,1911,1880s,1880,1889,Albumen silver print,Image: 14.2 x 9.7 cm (5 9/16 x 3 13/16 in.) Mount: 16.6 x 10.9 cm (6 9/16 x 4 5/16 in.),"Gift of Sue Cassidy Clark, in honor of Dr. Barbara Brennen Ford, 2006",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.778,false,true,285692,Photographs,Photograph,[Advertisement for Sarony's Photographic Studies],,,,,,Artist,,Napoleon Sarony,"American (born Canada), Quebec 1821–1896 New York",,"Sarony, Napoleon","American, born Canada",1821,1896,1880s,1880,1889,Albumen silver print from glass negative; lithograph,7 3/4 x 9 5/16,"Gilman Collection, Joyce F. Menschel Photography Library Fund, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (1–18),false,true,285805,Photographs,Album,[Jacob Christian Hansen Ellehammer's Experiments in Early Aviation (1905-1919)],,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,9 13/16 × 14 1/8 × 11/16 in. (25 × 35.8 × 1.7 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.639,false,true,269618,Photographs,Album,Progress of the Crystal Palace at Sydenham,,,,,,Artist|Artist,,Philip Henry Delamotte|Henry Angelo Ludovico Negretti,"British, 1821–1889|British, born Italy, 1818–1879",,"Delamotte, Philip Henry|Negretti, and Zambra","British|British, born Italy",1821 |1818,1889 |1879,1854,1854,1854,Albumen silver prints,,"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/269618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.128.1–.2,false,true,266992,Photographs,Photographically illustrated book,"Palais du Louvre et des Tuileries, motifs de décorations tirés des constructions éxécutées au nouveau Louvre et au palais des Tuileries ..., tomes I / II",,,,,,Artist|Artist,,Édouard Baldus|Hector Lefeul,"French, born Prussia, 1813–1889|French, 1810–1880",,"Baldus, Édouard|Lefeul, Hector","French, born Prussia|French",1813 |1810,1889 |1880,1850s–70s,1850,1879,Photogravures,,"Gift of George L. Morse, 1923, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37,false,true,268303,Photographs,Album,Album di disegni fotogenici,,,,,,Artist|Artist,Likely,Sebastiano Tassinari|William Henry Fox Talbot,"Italian, 1814–1888|British, Dorset 1800–1877 Lacock",,"Tassinari, Sebastiano|Talbot, William Henry Fox",Italian|British,1814 |1800,1888 |1800,1839–40,1839,1840,Photogenic drawings,28.5 x 22 cm (11 1/4 x 8 11/16 in. ),"Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/268303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.1–.15,false,true,296322,Photographs,Photogravure; X-Ray,Versuche über Photographie mittelst der Röntgen'schen Strahlen,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravures,50 x 36 cm (19 11/16 x 14 3/16 in.),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Portfolios,,http://www.metmuseum.org/art/collection/search/296322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.550.1,false,true,269744,Photographs,Portfolio,Grotesques by Aubrey Beardsley. Facsimile Platinum Prints by Frederick H. Evans from the Twelve Original Drawings in His Collection with a Portrait Frontispiece,,,,,,Artist|Artist,After,Frederick H. Evans|Aubrey Vincent Beardsley,"British, London 1853–1943 London|British, Brighton, Sussex 1872–1898 Menton",,"Evans, Frederick Henry|Beardsley, Aubrey Vincent",British|British,1853 |1872,1943 |1898,1910s,1910,1919,Platinum prints,Sheet: 9 9/16 × 7 1/2 in. (24.3 × 19 cm) Plate: 4 × 3 1/16 in. (10.2 × 7.7 cm) Image: 1 11/16 × 1 5/16 in. (4.3 × 3.3 cm),"Gift of Gordon Conn, 1954",,,,,,,,,,,,Portfolios,,http://www.metmuseum.org/art/collection/search/269744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (17),false,true,660967,Photographs,Photograph,12 cyl. stjerneformet Motor 160HK. Forsog ikke aflluttede April 1919.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 4 3/16 × 3 1/8 in. (10.7 × 8 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (18),false,true,660968,Photographs,Photograph,3/10 26 Yl El Illk[?],,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/4 × 4 1/4 in. (8.2 × 10.8 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (1a),false,true,660934,Photographs,Photograph,Forsogs Stationar paa Oeu Lindholw 1905,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 9/16 × 4 1/2 in. (9 × 11.4 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (1b),false,true,660936,Photographs,Photograph,Flyvernaskinin paa Banen,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 9/16 × 4 1/2 in. (9.1 × 11.4 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (2a),false,true,660937,Photographs,Photograph,Pendiue Ophanguing of Motor og Flyver.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 9/16 × 4 7/16 in. (9 × 11.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (2b),false,true,660938,Photographs,Photograph,Banen paa Lindholm 1905.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 11/16 × 4 7/16 in. (9.4 × 11.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (3a),false,true,660939,Photographs,Photograph,Havari.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 4 1/8 in. × 3 in. (10.5 × 7.6 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (3b),false,true,660940,Photographs,Photograph,Foroget Bareflade.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 4 1/8 × 2 15/16 in. (10.5 × 7.5 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (4a),false,true,660941,Photographs,Photograph,"Forsog i 12m Vindhastighed, arbydende Motor.",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 4 1/8 × 2 15/16 in. (10.5 × 7.4 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (4b),false,true,660942,Photographs,Photograph,Den 1' Flyvning i Europa Sen 12 September 1906.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 4 9/16 × 3 1/16 in. (11.6 × 7.7 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (5a),false,true,660943,Photographs,Photograph,"Treplau, Kollekolle. 1907.",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 × 4 in. (7.6 × 10.2 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (5b),false,true,660944,Photographs,Photograph,"Treplan, Farum Su 1907.",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 2 15/16 × 4 3/16 in. (7.4 × 10.6 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (6a),false,true,660945,Photographs,Photograph,Flyvring 1907.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/16 × 4 1/16 in. (7.7 × 10.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (6b),false,true,660946,Photographs,Photograph,"Todakker, med denne Maskine forekoges Seu 28 Tuni, Flyvring ved SX 1'officielle Flyve-Stame i Verdeu Kiel 1908. 1'Pris: 5000 Rm.",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/8 × 4 1/8 in. (7.9 × 10.4 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (7a),false,true,660947,Photographs,Photograph,Samme Maskine Sammenfolder.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/16 × 4 1/16 in. (7.7 × 10.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (7b),false,true,660948,Photographs,Photograph,"Samme Maskine, Flyvring paa Eremitageu. 1908",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/16 in. × 4 in. (7.7 × 10.2 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (8d),false,true,660950,Photographs,Photograph,"Flyvebaad, 1909-1910.",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 2 15/16 in. × 4 in. (7.5 × 10.2 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (9a),false,true,660951,Photographs,Photograph,"Flyvebaad, 1909-1910.",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/4 × 4 3/16 in. (8.2 × 10.7 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (9b),false,true,660952,Photographs,Photograph,Flyvebaad 1909-1910.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 4 1/16 × 5 5/8 in. (10.3 × 14.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (10a),false,true,660953,Photographs,Photograph,Samme Flyvebaad SammenfolSet.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 4 in. × 5 5/8 in. (10.1 × 14.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (10b),false,true,660954,Photographs,Photograph,Ieyl. skjerneformet Motor Standard Type. 30HK.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/4 × 4 7/16 in. (8.3 × 11.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (11a),false,true,660955,Photographs,Photograph,Ieyl. stjerneformet Motor Standard Type. 50HK.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 5 5/8 in. × 4 in. (14.3 × 10.1 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (11b),false,true,660956,Photographs,Photograph,Ellehammers roterende Aeroplan for lodret Start. 1911.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/16 in. × 4 in. (7.8 × 10.1 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (12a),false,true,660957,Photographs,Photograph,Samme Aeroplan.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/16 × 4 1/16 in. (7.7 × 10.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (12b),false,true,660958,Photographs,Photograph,Samme Aeroplan.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/8 × 4 1/8 in. (8 × 10.5 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (13a),false,true,660959,Photographs,Photograph,Samme Aeroplan svarende.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 5/16 × 4 7/16 in. (8.4 × 11.2 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (13b),false,true,660960,Photographs,Photograph,Samme Aeroplan svarende.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 7/16 × 4 3/8 in. (8.7 × 11.1 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (14a),false,true,660961,Photographs,Photograph,Samme Aeroplan svarende.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 1/4 × 4 1/16 in. (8.2 × 10.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (14b),false,true,660962,Photographs,Photograph,Standard Motor 80HK. Afbremsnings Prore'paa Orlogsvarftets Flyve Station 1916.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 5/16 × 4 3/8 in. (8.4 × 11.1 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (15a),false,true,660963,Photographs,Photograph,Samme Motor indbygget i en Marine Flyvebaad.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 3/8 × 4 5/16 in. (8.5 × 11 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (15b),false,true,660964,Photographs,Photograph,Samme Motor i Marine Flyvebaad.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 5/16 × 4 5/16 in. (8.4 × 10.9 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (16a),false,true,660965,Photographs,Photograph,Samme Motor i Marine Flyvebaad.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 3/8 × 4 5/16 in. (8.5 × 10.9 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (16b),false,true,660966,Photographs,Photograph,Samme Motor i Marine Flyvebaad.,,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image: 3 5/16 × 4 5/16 in. (8.4 × 10.9 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.662 (8a–c),false,true,660949,Photographs,Photograph,"Standard Type, Monoplan 1909.",,,,,,Artist|Artist,,Jacob Christian Hansen Ellehammer|Vilhelm Ellehammer,"Danish, 1871–1946|Danish",,"Ellehammer, Jacob Christian Hansen|Ellehammer, Vilhelm",Danish|Danish,1871,1946,1905–19,1905,1919,Gelatin silver prints from glass negatives,Image (u.l.): 2 9/16 × 3 3/8 in. (6.5 × 8.6 cm) Image (u.r.): 2 9/16 × 3 7/16 in. (6.5 × 8.7 cm) Image (l.c.): 3 1/8 × 4 1/16 in. (7.9 × 10.3 cm) Sheet: 9 5/8 × 12 13/16 in. (24.4 × 32.6 cm),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.134a,false,true,305832,Photographs,Photomontage,Napoli Tarantella,,,,,,Artist|Artist,,Giorgio Sommer|Unknown,"Italian, born Germany, 1834–1914|Italian",,"Sommer, Giorgio|Unknown","Italian, born Germany",1834,1914,ca. 1870,1865,1875,Albumen silver print,Image: 19.7 × 25 cm (7 3/4 × 9 13/16 in.) Mount: 25.8 × 35 cm (10 3/16 × 13 3/4 in.),"Purchase, Greenwich ART Group Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/305832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (1a, b)",false,true,287891,Photographs,Photograph,Papa & Mama; The Birthday Group,,,,,,Artist|Artist,,Thereza Dillwyn Llewelyn|John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, Thereza Dillwyn|Llewelyn, John Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 7.7 × 8.7 cm (3 1/16 × 3 7/16 in.) (a) Image: 12.6 × 11 cm (4 15/16 × 4 5/16 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (15a, b)",false,true,287905,Photographs,Photograph,The Microscope; Thereza and Elinor,,,,,,Artist|Artist,,John Dillwyn Llewelyn|Thereza Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn|Llewelyn, Thereza Dillwyn","British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 7.6 × 9 cm (3 in. × 3 9/16 in.) (a) Image: 13.8 × 10.5 cm (5 7/16 × 4 1/8 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1236,false,true,286664,Photographs,Photograph,"Copying Maps, Photographic Headquarters, Petersburg, Virginia",,,,,,Artist|Artist,Attributed to,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,March 1865,1865,1865,Albumen silver print from glass negative,Image: 19 x 24.6 cm (7 1/2 x 9 11/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.23,false,true,306591,Photographs,Photograph,[The Wilderness Battlefield],,,,,,Artist|Artist,Possibly by,Unknown|Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Unknown|Gardner, Alexander","American, Scottish",1821,1882,1865–67,1865,1867,Albumen silver print from glass negative,Image: 4 15/16 × 3 1/4 in. (12.5 × 8.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.24,false,true,306592,Photographs,Photograph,[The Wilderness Battlefield],,,,,,Artist|Artist,Possibly by,Unknown|Alexander Gardner,"American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Unknown|Gardner, Alexander","American, Scottish",1821,1882,1865–67,1865,1867,Albumen silver print from glass negative,Image: 5 in. × 3 5/16 in. (12.7 × 8.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (74a-c),false,true,287963,Photographs,Photograph,"Upper Lake, Penllengare; Rhododendrons; Brome, Morphine Tuff",,,,,,Artist|Artist,,James Knight|John Dillwyn Llewelyn,"British|British, Swansea, Wales 1810–1882 Swansea, Wales",,"Knight, James|Llewelyn, John Dillwyn","British|British, Welsh",1810,1882,1853–56,1853,1856,Albumen silver print; salted paper print,Image: 13.4 × 16.5 cm (5 1/4 × 6 1/2 in.) (a) Image: 7.4 × 6 cm (2 15/16 × 2 3/8 in.) (b) Image: 7.2 × 5.8 cm (2 13/16 × 2 5/16 in.) (c),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (8a, b)",false,true,287898,Photographs,Photograph,Penllergare; Printing,,,,,,Artist|Artist,,James Knight|John Dillwyn Llewelyn,"British|British, Swansea, Wales 1810–1882 Swansea, Wales",,"Knight, James|Llewelyn, John Dillwyn","British|British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,"Image: 10.8 × 13.9 cm (4 1/4 × 5 1/2 in.) (a), oval Image: 12.6 × 10.3 cm (4 15/16 × 4 1/16 in.) (b)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1252,false,true,286627,Photographs,Photograph,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 9 x 9.5 cm (3 9/16 x 3 3/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.1,false,true,286347,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver prints from glass negatives,From 12.5 x 7.9 cm (4 15/16 x 3 1/8 in.) to 12.5 x 9.1 cm (4 15/16 x 3 9/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.2,false,true,306596,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 3/16 × 3 7/16 in. (10.7 × 8.7 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.3,false,true,306597,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 5/8 × 3 7/16 in. (11.7 × 8.7 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.4,false,true,306598,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 5 1/16 × 3 1/16 in. (12.8 × 7.8 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.5,false,true,306599,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 7/8 × 3 11/16 in. (12.4 × 9.4 cm) Mount: 7 3/16 in. × 6 5/16 in. (18.3 × 16 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.6,false,true,306600,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 3 1/4 × 4 15/16 in. (8.3 × 12.5 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.7,false,true,306601,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 5 in. × 3 7/16 in. (12.7 × 8.8 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.8,false,true,306602,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 5 1/16 × 3 11/16 in. (12.9 × 9.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.9,false,true,306603,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 15/16 × 3 9/16 in. (12.5 × 9 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.10,false,true,306604,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 5 in. × 3 1/4 in. (12.7 × 8.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.11,false,true,306605,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 7/8 × 3 3/16 in. (12.4 × 8.1 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.12,false,true,306606,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 15/16 × 3 5/16 in. (12.6 × 8.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.13,false,true,306607,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 5 1/16 × 2 3/4 in. (12.8 × 7 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.14,false,true,306608,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 5 in. × 3 3/16 in. (12.7 × 8.1 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.15,false,true,306609,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 5/8 × 3 7/16 in. (11.8 × 8.7 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.16,false,true,306610,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 13/16 × 3 5/16 in. (12.3 × 8.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.17,false,true,306611,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 15/16 × 3 1/4 in. (12.6 × 8.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.18,false,true,306612,Photographs,Photographs,"[The Wilderness Battlefield, near Spotsylvania, Virginia]",,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1865 (?),1865,1865,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.19,false,true,306613,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 5 in. × 3 9/16 in. (12.7 × 9.1 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.20,false,true,306614,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 3/4 × 3 1/8 in. (12.1 × 8 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.944.21,false,true,306615,Photographs,Photographs,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown","American, Scottish",1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 4 1/8 × 3 11/16 in. (10.4 × 9.3 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.924,false,true,286184,Photographs,Photograph,"Infernal Rock, Chincha Islands",,,,,,Artist|Artist,Possibly by,Unknown|John Moran,"American (born England), Bolton, Lancashire 1821–1903 Pennsylvania",,"Unknown|Moran, John","American, born England",1821,1903,ca. 1870,1870,1870,Albumen silver print from glass negative,Image: 18.4 x 25.3 cm (7 1/4 x 9 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.608.2.1,false,true,260973,Photographs,Photograph,[Pyramid at Dahshûr],,,,,,Artist|Artist,,Francis Frith|Francis Frith and Company,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France|British",,"Frith, Francis|Francis Frith and Company",British|British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,15.4 x 20.8 cm. (6 1/16 x 8 3/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.608.2.3,false,true,260975,Photographs,Photograph,"[Valley of the Kings, Thebes]",,,,,,Artist|Artist,,Francis Frith|Francis Frith and Company,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France|British",,"Frith, Francis|Francis Frith and Company",British|British,1822,1898,"ca. 1857, printed 1870s",1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.608.1.26,false,true,260971,Photographs,Photograph,Banks of the Nile at Cairo,,,,,,Artist|Artist,,Francis Frith|Francis Frith and Company,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France|British",,"Frith, Francis|Francis Frith and Company",British|British,1822,1898,"ca. 1857, printed 1870s",1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.138,false,true,282751,Photographs,Photograph,Zaragoza à Pamplona y Barcelona - Puente de Zuera,,,,,,Artist|Artist,,Juan Laurent|José Martinez Sánchez,"French, 1816–1892, active Spain, 1857–1880s|Spanish, 1808–1874",,"Laurent, Juan|Martinez, Sánchez José",French|Spanish,1816 |1808,1892 |1874,ca. 1867,1866,1868,Albumen silver print from glass negative,33.9 x 24.8 cm (13 3/8 x 9 3/4 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, by exchange, 1999",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.171.1, .2",false,true,283065,Photographs,Photograph,[The Reading Establishment],,,,,,Artist|Artist,Attributed to|Attributed to,Nicolaas Henneman|William Henry Fox Talbot,"Dutch, Heemskerk 1813–1898 London|British, Dorset 1800–1877 Lacock",,"Henneman, Nicolaas|Talbot, William Henry Fox",Dutch|British,1813 |1800,1898 |1800,1846,1846,1846,Salted paper prints from paper negatives,Left image: 18.6 x 22.4 cm (7 5/16 x 8 13/16 in.) Right image: 18.1 × 22 cm (7 1/8 × 8 11/16 in.) Overall sheet: 19.9 × 49.1 cm (7 13/16 × 19 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1168.1,false,true,263461,Photographs,Photograph,"[Mezzotint portrait of a Girl in Profile, from The St. Memin Collection of Portraits]",,,,,,Artist|Artist,After,Jeremiah Gurney|Charles B. J. F. de Saint-Mémin,"American, 1812–1895 Coxsackie, New York|French, Dijon 1770–1852 Dijon",,"Gurney, Jeremiah|Saint-Mémin, Charles de",American|French,1812 |1770,1895-04-21|1852,1862,1862,1862,Salted paper print,5.6 x 5.6 cm (2 3/16 x 2 3/16 in. ),"Gift of Mrs. James Anderson, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1168.2,false,true,263462,Photographs,Photograph,"[Mezzotint portrait of a Young Man in Profile, from The St. Memin Collection of Portraits]",,,,,,Artist|Artist,After,Jeremiah Gurney|Charles B. J. F. de Saint-Mémin,"American, 1812–1895 Coxsackie, New York|French, Dijon 1770–1852 Dijon",,"Gurney, Jeremiah|Saint-Mémin, Charles de",American|French,1812 |1770,1895-04-21|1852,1862,1862,1862,Salted paper print,5.6 x 5.6 cm (2 3/16 x 2 3/16 in. ),"Gift of Mrs. James Anderson, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.1,false,true,660039,Photographs,Photogravure; X-Ray,Frauenhand,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 10 3/16 × 4 1/4 in. (25.9 × 10.8 cm) Plate: 11 7/16 × 5 5/16 in. (29 × 13.5 cm) Sheet: 19 5/16 in. × 14 in. (49 × 35.5 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.2,false,true,660040,Photographs,Photogravure; X-Ray,Hand eines 8 jährigen Mädchens,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 6 7/8 × 3 5/16 in. (17.4 × 8.4 cm) Plate: 8 1/16 × 4 9/16 in. (20.5 × 11.6 cm) Sheet: 19 1/2 × 13 3/4 in. (49.5 × 34.9 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.3,false,true,660041,Photographs,Photogravure; X-Ray,Hand eines 4 jährigen Kindes,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 7 13/16 × 3 1/4 in. (19.9 × 8.2 cm) Plate: 8 13/16 × 4 3/16 in. (22.4 × 10.7 cm) Sheet: 19 9/16 × 13 7/8 in. (49.7 × 35.2 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.4,false,true,660042,Photographs,Photogravure; X-Ray,Fuss eines 17 jährigen Jünglings,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 8 1/4 in. × 7 in. (21 × 17.8 cm) Plate: 11 1/4 × 8 1/16 in. (28.5 × 20.4 cm) Sheet: 19 5/8 × 13 3/4 in. (49.9 × 35 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.5,false,true,660043,Photographs,Photogravure; X-Ray,X-Ray of Samples of Various Materials,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 9 1/16 × 6 15/16 in. (23 × 17.7 cm) Plate: 10 5/16 × 8 1/4 in. (26.2 × 20.9 cm) Sheet: 17 5/16 × 13 15/16 in. (44 × 35.4 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.6,false,true,660044,Photographs,Photogravure; X-Ray,Cameen in Goldfassung,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 4 5/8 × 6 5/8 in. (11.7 × 16.8 cm) Plate: 5 1/2 × 7 in. (14 × 17.8 cm) Sheet: 14 1/8 × 19 5/8 in. (35.8 × 49.8 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.7,false,true,660045,Photographs,Photogravure; X-Ray,Grüne Eidechse,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 3 5/8 × 6 1/4 in. (9.2 × 15.9 cm) Plate: 4 15/16 × 7 7/16 in. (12.5 × 18.9 cm) Sheet: 13 7/8 × 19 5/8 in. (35.2 × 49.9 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.8,false,true,660046,Photographs,Photogravure; X-Ray,Chamäleon cristatus,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 4 15/16 × 7 7/8 in. (12.6 × 20 cm) Plate: 6 1/8 × 9 1/8 in. (15.5 × 23.2 cm) Sheet: 13 7/8 × 19 11/16 in. (35.3 × 50 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.9,false,true,660047,Photographs,Photogravure; X-Ray,Zanclus cornutus / Acanthurus nigros,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 9 1/2 × 7 13/16 in. (24.2 × 19.8 cm) Plate: 10 9/16 × 8 13/16 in. (26.8 × 22.4 cm) Sheet: 19 3/4 × 13 15/16 in. (50.2 × 35.4 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.10,false,true,660048,Photographs,Photogravure; X-Ray,Zwei Goldfische und ein Seefisch (Christiceps argentatus),,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 4 15/16 × 6 9/16 in. (12.5 × 16.6 cm) Plate: 5 13/16 × 7 9/16 in. (14.7 × 19.2 cm) Sheet: 13 7/8 × 19 5/8 in. (35.3 × 49.9 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.11,false,true,660049,Photographs,Photogravure; X-Ray,Solfisch (Pleuronectes solea),,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 6 3/4 × 13 5/8 in. (17.1 × 34.6 cm) Plate: 8 × 14 13/16 in. (20.3 × 37.6 cm) Sheet: 13 13/16 × 19 11/16 in. (35.1 × 50 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.12,false,true,660050,Photographs,Photogravure; X-Ray,Frösche in Bauch und Rückenlage,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 9 in. × 6 1/2 in. (22.8 × 16.5 cm) Plate: 9 15/16 × 7 1/2 in. (25.3 × 19 cm) Sheet: 19 9/16 × 13 7/8 in. (49.7 × 35.3 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.13,false,true,660051,Photographs,Photogravure; X-Ray,Ratte,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 5 7/16 × 9 3/4 in. (13.8 × 24.8 cm) Plate: 6 9/16 × 10 7/8 in. (16.6 × 27.6 cm) Sheet: 13 7/8 × 19 5/8 in. (35.3 × 49.9 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.14,false,true,660052,Photographs,Photogravure; X-Ray,Neugeborenes Kaninchen,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 2 11/16 × 5 7/16 in. (6.9 × 13.8 cm) Plate: 4 1/4 × 6 9/16 in. (10.8 × 16.7 cm) Sheet: 14 1/16 × 19 5/8 in. (35.7 × 49.9 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.66.15,false,true,660053,Photographs,Photogravure; X-Ray,Aesculap-Schlange,,,,,,Artist|Artist,,Josef Maria Eder|Eduard Valenta,"Austrian, Krems an der Donau, 1855–1944 Kitzbühel|Austrian, 1857–1937",and,"Eder, Josef Maria|Valenta, Eduard",Austrian|Austrian,1855 |1857,1944 |1937,1896,1896,1896,Photogravure,Image: 10 3/4 × 8 9/16 in. (27.3 × 21.7 cm) Plate: 11 15/16 × 9 3/4 in. (30.3 × 24.8 cm) Sheet: 19 5/8 × 13 7/8 in. (49.9 × 35.3 cm),"Purchase, Alfred Stieglitz Society Gifts, Joyce F. Menschel Photography Library Fund, and Maureen and Noel Testa Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/660053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1098.8,false,true,631032,Photographs,Photograph,"[Dome of the Rock, Jerusalem]",,,,,,Artist|Artist,,Felice Beato|James Robertson,"British (born Italy), Venice 1832–1909 Luxor, Egypt|British, 1813–1881",and,"Beato, Felice|Robertson, James","British, born Italy|British",1832 |1813,1909 |1881,1856–57,1856,1857,Albumen silver print,Image: 9 in. × 11 1/4 in. (22.9 × 28.6 cm) Mount: 17 5/8 in. × 22 1/2 in. (44.8 × 57.2 cm),"Gift of Joyce F. Menschel, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/631032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1246,false,true,285890,Photographs,Photograph,"[Panorama of Camp Winfield Scott, Yorktown, Virginia]",,,,,,Artist|Artist,,Alexander Gardner|James F. Gibson,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, born 1828",,"Gardner, Alexander|Gibson, James F.","American, Scottish|American",1821 |1828,1882 |1928,1863,1863,1863,Albumen silver prints from glass negatives,Image: 3 5/16 × 18 13/16 in. (8.4 × 47.8 cm) Mount: 5 3/8 × 20 5/8 in. (13.6 × 52.4 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (1),false,true,291374,Photographs,Photograph,Fountains Abbey. East Window and Tower,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28 x 23.5 cm (11 x 9 1/4 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (2),false,true,291375,Photographs,Photograph,Fountains Abbey. General Western Front,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 22.5 x 27.9 cm (8 7/8 x 11 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (3),false,true,291376,Photographs,Photograph,"Fountains Abbey. The Church, Cloister and Hospitium",,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28 x 23.4 cm (11 x 9 3/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (4),false,true,291369,Photographs,Photograph,Fountains Abbey. The Refectory and Kitchen,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.5 x 23.6 cm (11 1/4 x 9 5/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (5),false,true,291377,Photographs,Photograph,Fountains Abbey. The Church and Chapter House,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 27.4 x 21.7 cm (10 13/16 x 8 9/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (6),false,true,291378,Photographs,Photograph,Fountains Abbey. Interior of Chapter House,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.5 x 23.6 cm (11 1/4 x 9 5/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (7),false,true,291379,Photographs,Photograph,"Fountains Abbey. The Chapel of the Nine Alters, Exterior",,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 23.5 x 28 cm (9 1/4 x 11 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (8),false,true,291380,Photographs,Photograph,"Fountains Abbey. The Chapel of the Nine Alters, Interior",,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.6 x 23.6 cm (11 1/4 x 9 5/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (9),false,true,291381,Photographs,Photograph,Fountains Abbey. Interior of the Choir,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.2 x 22.8 cm (11 1/8 x 9 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (10),false,true,291382,Photographs,Photograph,Fountains Abbey. The Echo Rock,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 23.5 x 27.9 cm (9 1/4 x 11 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (11),false,true,291383,Photographs,Photograph,Easby Abbey. From the East,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.1 x 23.4 cm (11 1/16 x 9 3/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (12),false,true,291384,Photographs,Photograph,Easby Abbey. The Refectory,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28 x 23.5 cm (11 x 9 1/4 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (13),false,true,291385,Photographs,Photograph,Rivaulx Abbey. General View from the South,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28 x 23.5 cm (11 x 9 1/4 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (14),false,true,291386,Photographs,Photograph,Rivaulx Abbey. Interior of the Choir,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.6 x 23.6 cm (11 1/4 x 9 5/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (15),false,true,291387,Photographs,Photograph,Rivaulx Abbey. Looking Across the Choir,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.1 x 23.5 cm (11 1/16 x 9 1/4 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (16),false,true,291388,Photographs,Photograph,Rivaulx Abbey. The Triforium Arches,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.1 x 23.4 cm (11 1/16 x 9 3/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (17),false,true,291389,Photographs,Photograph,Rivaulx Abbey. Doorway of the Refectory,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.1 x 23.4 cm (11 1/16 x 9 3/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (18),false,true,291390,Photographs,Photograph,Kirkstall Abbey. From the West,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 26.3 x 23.5 cm (10 3/8 x 9 1/4 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (19),false,true,291391,Photographs,Photograph,Kirkstall Abbey. Ruins on the South Side,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.1 x 23.4 cm (11 1/16 x 9 3/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (20),false,true,291392,Photographs,Photograph,Kirkstall Abbey. Doorway on the North Side,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.1 x 23.4 cm (11 1/16 x 9 3/16 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (21),false,true,291393,Photographs,Photograph,Bolton Priory. From the South,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.4 x 21.6 cm (11 3/16 x 8 1/2 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.640 (22),false,true,291394,Photographs,Photograph,Bolton Priory. The Stepping Stones,,,,,,Artist|Artist,,Joseph Cundall|Philip Henry Delamotte,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey|British, 1821–1889",,"Cundall, Joseph|Delamotte, Philip Henry",British|British,1818 |1821,1895 |1889,1850s,1850,1859,Albumen silver print,Image: 28.3 x 24.1 cm (11 1/8 x 9 1/2 in.) Mount: 43.9 x 30 cm (17 5/16 x 11 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.616,false,true,260988,Photographs,Photograph,The Chess Players,,,,,,Artist|Artist,Likely by|Possibly by,Antoine-François-Jean Claudet|Nicolaas Henneman,"French, active Great Britain, 1797–1867|Dutch, Heemskerk 1813–1898 London",,"Claudet, Antione-François-Jean|Henneman, Nicolaas","French, active Great Britain",1797 |1813,1867 |1898,ca. 1845,1840,1850,Salted paper print from paper negative,19.5 x 14.4 cm. (7 11/16 x 5 11/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.40,false,true,306323,Photographs,Photograph,The Chess Players,,,,,,Artist|Artist,Likely by|Possibly by,Antoine-François-Jean Claudet|Nicolaas Henneman,"French, active Great Britain, 1797–1867|Dutch, Heemskerk 1813–1898 London",,"Claudet, Antione-François-Jean|Henneman, Nicolaas","French, active Great Britain|Dutch",1797 |1813,1867 |1898,ca. 1845,1840,1850,Salted paper print from paper negative,Sheet: 9 5/8 × 7 11/16 in. (24.5 × 19.6 cm) Image: 7 13/16 × 5 13/16 in. (19.8 × 14.7 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (4a, b)",false,true,287894,Photographs,Photograph,The Observatory; Thereza Llewelyn,,,,,,Artist|Artist,,Miss Bush|John Dillwyn Llewelyn,"British, active 19th century|British, Swansea, Wales 1810–1882 Swansea, Wales",,"Bush Miss|Llewelyn, John Dillwyn","British|British, Welsh",1810,1882,1853–56,1853,1856,Salted paper print,Image: 9.8 × 15.6 cm (9.8 × 15.6 cm) (a) Image: 12.8 × 10.6 cm (5 1/16 × 4 3/16 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1158.3,false,true,263051,Photographs,Photograph,"White House Landing, Pamunkey River",,,,,,Artist|Artist,,Timothy H. O'Sullivan|Mathew B. Brady,"American, born Ireland, 1840–1882|American, born Ireland, 1823?–1896 New York",,"O'Sullivan, Timothy H.|Brady, Mathew B.","American, born Ireland|American",1840 |1823,1882 |1896,1864,1864,1864,Albumen silver print,,"Gift of Mrs. A. Hyatt Mayor, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.607,false,true,283067,Photographs,Photograph,The Fruit Sellers,,,,,,Artist|Artist,Possibly by|Possibly by,Calvert Richard Jones|William Henry Fox Talbot,"British, Swansea, Wales 1802–1877 Bath, England|British, Dorset 1800–1877 Lacock",,"Jones, Calvert Richard|Talbot, William Henry Fox","British, Welsh|British",1802 |1800,1877 |1800,ca. 1845,1843,1847,Salted paper print from paper negative,17.1 x 21.1 cm (6 3/4 x 8 5/16 in. ),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.637,false,true,261000,Photographs,Photograph,"""God"" by Baroness Elsa von Freytag-Loringhoven and Morton Schamberg",,,,,,Artist|Artist,,Morton Schamberg|Elsa von Freytag-Loringhoven,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania|German, 1874–1927",,"Schamberg, Morton|Freytag-Loringhoven, Elsa von",American|German,1881 |1874,1918 |1927,1917,1917,1917,Gelatin silver print,24.1 x 19.2 cm (9 1/2 x 7 9/16 in.),"Elisha Whittelsey Collection, Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.80,false,true,283179,Photographs,Photograph,Frederick Langenheim,,,,,,Artist|Artist,,William Langenheim|Frederick Langenheim,"American, born Germany, Schöningen 1807–1874|American, born Germany, Schöningen 1809–1879",,"Langenheim, William|Langenheim, Frederick","American, born Germany|American, born Germany",1807 |1809,1874 |1879,ca. 1848–50,1846,1852,Daguerreotype,Image: 9 5/9 x 7 5/8; Frame: 18 x 15 x 1 3/8,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.38,false,true,291791,Photographs,Daguerreotype,"[Middle-aged Man with Chinstrap Beard, Hand Tucked Inside Buttoned Jacket]",,,,,,Artist|Artist,,William Langenheim|Frederick Langenheim,"American, born Germany, Schöningen 1807–1874|American, born Germany, Schöningen 1809–1879",,"Langenheim, William|Langenheim, Frederick","American, born Germany|American, born Germany",1807 |1809,1874 |1879,1840s–50s,1840,1859,Daguerreotype,Image: 12.1 x 8.8 cm (4 3/4 x 3 7/16 in.) Plate: 14 x 10.8 cm (5 1/2 x 4 1/4 in.) Case: 1.6 x 14.9 x 11.9 cm (5/8 x 5 7/8 x 4 11/16 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.56,false,true,291809,Photographs,Daguerreotype,[Seated Man in Floral Vest],,,,,,Artist|Artist,,William Langenheim|Frederick Langenheim,"American, born Germany, Schöningen 1807–1874|American, born Germany, Schöningen 1809–1879",,"Langenheim, William|Langenheim, Frederick","American, born Germany|American, born Germany",1807 |1809,1874 |1879,1840s–50s,1840,1859,Daguerreotype,Image: 6.8 x 5.6 cm (2 11/16 x 2 3/16 in.) Plate: 8.3 x 7 cm (3 1/4 x 2 3/4 in.) Case: 1.7 x 9 x 7.9 cm (11/16 x 3 9/16 x 3 1/8 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.177,false,true,286315,Photographs,Photograph,Frederick Langenheim Looking at Talbotypes,,,,,,Artist|Artist,,William Langenheim|Frederick Langenheim,"American, born Germany, Schöningen 1807–1874|American, born Germany, Schöningen 1809–1879",,"Langenheim, William|Langenheim, Frederick","American, born Germany|American, born Germany",1807 |1809,1874 |1879,ca. 1849–51,1849,1851,Daguerreotype,Image: 12.1 × 8.9 cm (4 3/4 × 3 1/2 in.) Case: 1.6 × 15.2 × 11.9 cm (5/8 in. × 6 in. × 4 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.614a–g,false,true,283180,Photographs,Photograph,Eclipse of the Sun,,,,,,Artist|Artist,,William Langenheim|Frederick Langenheim,"American, born Germany, Schöningen 1807–1874|American, born Germany, Schöningen 1809–1879",,"Langenheim, William|Langenheim, Frederick","American, born Germany|American, born Germany",1807 |1809,1874 |1879,1854,1854,1854,Daguerreotype,From 3.2 x 2.5 cm (1 1/4 x 1 in.) to 7.2 x 5.9 cm (2 13/16 x 2 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.537.4,false,true,259606,Photographs,Carte-de-visite,Confederate Soldier [on the Battlefield at Antietam],,,,,,Artist|Publisher,,Alexander Gardner|Brady & Co.,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American, active 1840s–1880s",,"Gardner, Alexander|Brady & Co.","American, Scottish|American",1821 |1840,1882 |1889,September 1862,1862,1862,Albumen silver print from glass negative,Image: 6.1 x 9.8 cm (2 3/8 x 3 7/8 in.),"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.395,false,true,682875,Photographs,Photograph,La Frayeur,,,,,,Artist|Artist|Person in Photograph,Person in photograph,Aquilin Schad|Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"Austrian, 1817–1866|French, 1822–1913|1835–1899",,"Schad, Aquilin|Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",,1817 |1822 |1835,1866 |1913 |1899,1861–64,1861,1864,Salted paper print with applied color,Image: 22 7/16 × 17 5/16 in. (57 × 44 cm) Mat: 29 1/2 × 23 5/16 in. (75 × 59.2 cm),"Purchase, The Camille M. Lownds Fund, Joyce F. Menschel Gift, Louis V. Bell and 2012 Benefit Funds, and C. Jay Moorhead Foundation Gift, 2015",,,,,,,,,,,,Photographs|Paintings,,http://www.metmuseum.org/art/collection/search/682875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.6,false,true,269075,Photographs,Photograph,Maison au toit de chaume,,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.7,false,true,269076,Photographs,Photograph,Nature morte au lièvre,,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1853,1853,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.8,false,true,269077,Photographs,Photograph,"Vue prise dans la vallée de Changy, aux environs de Fontainebleau",,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.10,false,true,269058,Photographs,Photograph,Trois jeunes enfants assis autour d'un panier,,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.11,false,true,269059,Photographs,Photograph,Sous-bois en automne,,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.13,false,true,269061,Photographs,Photograph,Ane attaché à une charette,,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.15,false,true,269063,Photographs,Photograph,Charette devant l'entrée d'un abri au toit de chaume,,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,17.9 x 22.4 cm (7 1/16 x 8 13/16 in.),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.19,false,true,269067,Photographs,Photograph,Ferme au toit de chaume,,,,,,Artist|Printer,,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Unknown|Imprimerie photographique de Blanquart-Évrard, à Lille",,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.572,false,true,685911,Photographs,Carte-de-visite,[Adolph Menzel],,,,,,Photography Studio|Person in Photograph,Person in photograph,Photographische Gesellschaft|Adolph Menzel,"German, Breslau 1815–1905 Berlin",,"Photographische Gesellschaft|Menzel, Adolph",,1910 |1815,1910 |1905,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.777.1,false,true,705423,Photographs,Photograph,"The Memnonium or Rameseiom, Thebes",,,,,,Publisher|Artist,,J. Hogarth|Robert Murray,"British, Edinburgh 1822–1893 Plymouth",,"Hogarth, J.|Murray, Robert",,1822,1893,"1852–55, printed 1854–56",1852,1855,Albumen silver print from waxed paper negative,Image: 6 7/8 in. × 9 in. (17.5 × 22.9 cm) Mount: 12 in. × 15 3/8 in. (30.5 × 39.1 cm),"Gift of Charles Isaacs and Carol Nigro, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/705423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.49,false,true,263189,Photographs,Photograph,"Gate of Ptolemy Philomeder, B.C. 180, Karnac",,,,,,Publisher|Artist,Attributed to,J. Hogarth|Robert Murray,"British, Edinburgh 1822–1893 Plymouth",,"Hogarth, J.|Murray, Robert",,1822,1893,ca. 1856,1856,1856,Albumen silver print,Image: 9 1/8 × 7 3/8 in. (23.2 × 18.7 cm) Mount: 16 in. × 12 3/16 in. (40.6 × 31 cm),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.50,false,true,263191,Photographs,Photograph,"Nubian Sakkieh, or Water Wheel",,,,,,Publisher|Artist,Attributed to,J. Hogarth|Robert Murray,"British, Edinburgh 1822–1893 Plymouth",,"Hogarth, J.|Murray, Robert",,1822,1893,ca. 1856,1856,1856,Albumen silver print,Image: 6 3/4 × 8 3/16 in. (17.1 × 20.7 cm) Mount: 12 3/16 in. × 15 15/16 in. (31 × 40.5 cm),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.778.34,false,true,700110,Photographs,Photograph,"Nubie, Grand Temple d'Isis, a Philœ, Galerie Orientale",,,,,,Publisher|Artist|Printer,,"Gide et Baudry|Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Gide et Baudry|Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",,1822 |1851,1894 |1855,"April 1850, printed 1852",1850,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 9/16 in. (16.7 × 21.8 cm) Mount: 12 7/16 × 17 5/8 in. (31.6 × 44.7 cm),"Gift of Joyce F. Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/700110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.158,false,true,267944,Photographs,Photograph,"Dead Confederate Soldier at Fort Mahone, Petersburg",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",,1823,1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.173,false,true,685515,Photographs,Carte-de-visite,[Konstantin Cretius],,,,,,Person in Photograph|Photographer,Person in photograph,Konstantin Johannes Franz Cretius|Ernst Milster,"German, Brieg 1814–1901 Berlin|German, born 1835",,"Cretius, Konstantin Johannes Franz|Milster, Ernst",,1814 |1835,1901,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.395,false,true,268206,Photographs,Photograph,"Gettysburg, Pennsylvania",,,,,,Artist|Former Attribution,Formerly attributed to,Unknown|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Unknown|Brady, Mathew B.",,1823,1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.778.38,false,true,700114,Photographs,Photograph,"Head of Cañyon de Chelle, Looking Down",,,,,,Patron|Artist,Commissioned by,Lieutenant George Montague Wheeler|Timothy H. O'Sullivan,"American, 1842–1905|American, born Ireland, 1840–1882",,"Wheeler, Lieutenant George Montague|O'Sullivan, Timothy H.",,1842 |1840,1905 |1882,1873,1873,1873,Albumen silver print,Image: 8 1/16 × 10 13/16 in. (20.5 × 27.5 cm),"Gift of Joyce F. Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/700114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.659–.687,false,true,692531,Photographs,,[29 Glass Stereographs],,,,,,Artist|Artist|Artist|Artist,,Bierstadt Brothers|Various|Franklin White|James McPherson,"American, active 1860s–80s|American, active 1850s–60s|American, active 1860s",,"Bierstadt Brothers|Various|White, Franklin|McPherson, James",,1850 |1850 |1860,1900 |1869 |1869,1850s–1900s,1850,1909,Glass stereographs,Each photograph approx. 3 1/2 in. × 7 in. (8.9 × 17.8 cm),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/692531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.778.37,false,true,700113,Photographs,Photograph,"Perched Rock, Rocker Creek, Arizona",,,,,,Patron|Artist,Commissioned by,Lieutenant George Montague Wheeler|William Bell,"American, 1842–1905|American (born England) Liverpool 1831–1910 Philadelphia, Pennsylvania",,"Wheeler, Lieutenant George Montague|Bell, William",,1842 |1831,1905 |1910,1872,1872,1872,Albumen silver print from glass negative,Image: 10 7/8 × 8 1/16 in. (27.6 × 20.5 cm) Sheet: 21 7/16 in. × 16 in. (54.4 × 40.6 cm),"Gift of Joyce F. Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/700113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.688–.897,false,true,692523,Photographs,,[210 Stereographs],,,,,,Publisher|Artist|Artist|Artist,,New York Stereoscopic Company|Frederick Langenheim|William Langenheim|C. G. Hill,"American|American, born Germany, Schöningen 1809–1879|American, born Germany, Schöningen 1807–1874|American, active 1860s–80s",,"New York Stereoscopic Company|Langenheim, Frederick|Langenheim, William|Hill, C. G.",,1809 |1807 |1860,1879 |1874 |1899,1850s–1890s,1850,1899,Albumen silver prints,Each photograph approx. 3 1/2 in. × 7 in. (8.9 × 17.8 cm),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/692523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.438.2,false,true,294506,Photographs,Album,[Personal Travel Album Made by the Dowager Empress Maria Feoderovna Showing Events in the Daily Life of the Russian Imperial Family],,,,,,Artist,,Dowager Empress Maria Feodorovna,"Russian, born Denmark, Copenhagen 1847–1928 Copenhagen",,"Feodorovna, Maria, Dowager Empress",,1847,1928,1916,1916,1916,Gelatin silver prints; photomechanical prints,29.5 x 42.2 x 5.7cm (11 5/8 x 16 5/8 x 2 1/4in.) each,"Gift of Prince and Princess Alexander Romanoff, 1996",,,,,,,,,,,,Albums|Prints,,http://www.metmuseum.org/art/collection/search/294506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.533,false,true,685873,Photographs,Carte-de-visite,[Ludwig von Löfftz],,,,,,Artist,,Franz Werner,active 1860s,,"Werner, Franz",,1859,1870,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.678,false,true,686017,Photographs,Carte-de-visite,[Riefstahl ?],,,,,,Artist,,Carl Wigand,active 1860s,,"Wigand, Carl",,1859,1870,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.91,false,true,702990,Photographs,Photograph,55. Athènes. Acropole. Ruines et 1ers plans (pour tableau),,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.92,false,true,702991,Photographs,Photograph,49. Athènes. Acropole. Côté O.,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.93,false,true,702992,Photographs,Photograph,62. Athènes. Temple de Minerve Poliade.,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.94,false,true,702993,Photographs,Photograph,56. Athènes. Caryatides. Ereckt,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 9 7/16 × 3 3/4 in. (24 × 9.5 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.95,false,true,702994,Photographs,Photograph,53. Athènes. Temple de Bacchus,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 9 7/16 × 3 3/4 in. (24 × 9.5 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.96,false,true,702995,Photographs,Photograph,73. Alexandrie. Grand Minaret,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 9 7/16 × 3 3/4 in. (24 × 9.5 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.97,false,true,702996,Photographs,Photograph,74. Près d'Alexandrie. Le désert.,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.98,false,true,702997,Photographs,Photograph,68. Alexandrie. Colonne de Pompée,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 9 7/16 × 3 3/4 in. (24 × 9.5 cm),"Purchase, Philippe de Montebello Fund, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel and Annette de la Renta Gifts, and funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.600,false,true,726471,Photographs,Photograph,[Egypt],,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.601,false,true,726472,Photographs,Photograph,"Phile, temple découvert.",,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1844,1844,1844,Daguerreotype,Image: 3 1/8 × 3 3/4 in. (8 × 9.5 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.602,false,true,726473,Photographs,Photograph,[Egypt],,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 3 3/4 × 4 3/4 in. (9.5 × 12 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.603,false,true,726474,Photographs,Photograph,"Kaire. Gama Soultan Ansoun, détails.",,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 9 7/16 × 3 3/4 in. (24 × 9.5 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.604,false,true,726475,Photographs,Photograph,Thebes Rhamseion,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1844,1844,1844,Daguerreotype,Image: 7 1/2 × 9 7/16 in. (19 × 24 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.605,false,true,726476,Photographs,Photograph,Rome. Ponte Rotto,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.606,false,true,726477,Photographs,Photograph,Rome. Graecostato r fac. N. O.,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 9 7/16 × 3 3/4 in. (24 × 9.5 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.607,false,true,726478,Photographs,Photograph,"Toscanella Eglise de S. Pietro, apside",,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842,1842,1842,Daguerreotype,Image: 3 3/4 × 3 1/8 in. (9.5 × 8 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.608,false,true,726479,Photographs,Photograph,[Jerusalem],,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.609,false,true,726480,Photographs,Photograph,"Jerusalem, près de la porte de Jaffa, chap.",,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 4 3/4 × 3 3/4 in. (12 × 9.5 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.610,false,true,726481,Photographs,Photograph,"Atlit Syrie, Chapelle [Damascus Gate, Jerusalem]",,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 9 7/16 × 7 1/2 in. (24 × 19 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.611,false,true,726482,Photographs,Photograph,"[Baalbek, Syria]",,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 3 3/4 × 9 7/16 in. (9.5 × 24 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.612,false,true,726483,Photographs,Photograph,Alep. Prise de Bab Antakieh (publiée),,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1842–44,1842,1844,Daguerreotype,Image: 7 1/2 × 9 7/16 in. (19 × 24 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.613,false,true,726484,Photographs,Photograph,Paris devant l’atelier de Daguerre,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1841,1841,1841,Daguerreotype,Image: 3 3/4 × 4 3/4 in. (9.5 × 12 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.614,false,true,726485,Photographs,Photograph,"[Notre Dame Cathedral, Rose Window, North Transept]",,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1841,1841,1841,Daguerreotype,Image: 9 7/16 × 7 1/2 in. (24 × 19 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.615,false,true,726486,Photographs,Photograph,Sardes. T. de Cybèle,,,,,,Artist,,Joseph-Philibert Girault de Prangey,"French, 1804–1892",,"Girault, de Prangey Joseph-Philibert",,1804,1892,1843,1843,1843,Daguerreotype,Image: 7 1/2 × 9 7/16 in. (19 × 24 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, Joyce F. Menschel Gift, Joseph Pulitzer Bequest, 2016 Benefit Fund, and Gift of Dr. Mortimer D. Sackler, Theresa Sackler and Family, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/726486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.459,false,true,715974,Photographs,Photograph; Daguerreotype,[Profile of a woman with necrosis of the nose],,,,,,Artist,,Louis-Auguste Bisson,"French, 1814–1876",,"Bisson, Louis-Auguste",,1814,1876,1841–48,1841,1848,Daguerreotype,Image: 4 9/16 × 3 1/8 in. (11.6 × 7.9 cm) Plate: 7 3/16 × 5 7/8 in. (18.3 × 14.9 cm),"Funds from various donors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/715974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.352 (3),false,true,701977,Photographs,Photographs,"Assassinat des généraux Clément Tomas et Jules Lecomte, rue des Rosiers 6 à Montmartre deans la journée du 18 mars 1871",,,,,,Artist,,Ernest Eugène Appert,"French, 1831–1891",,"Appert, Ernest Eugène",,1831,1891,1870–71,1870,1871,Albumen silver print from glass negative,Sheet: 36 x 46 cm (14 3/16 x 18 1/8 in.),"Joyce F. Menschel Photography Library Fund, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/701977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.352 (7),false,true,701997,Photographs,Photographs,"Massacre des dominicains d'Arcueil, route d'Italie no. 38, le 25 mai 1871, à 4 heures et demie",,,,,,Artist,,Ernest Eugène Appert,"French, 1831–1891",,"Appert, Ernest Eugène",,1831,1891,1870–71,1870,1871,Albumen silver print from glass negative,Sheet: 36 x 46 cm (14 3/16 x 18 1/8 in.),"Joyce F. Menschel Photography Library Fund, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/701997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.352 (10),false,true,702000,Photographs,Photographs,"Exécution des otages, prison de la Roquette, le 24 mai 1871",,,,,,Artist,,Ernest Eugène Appert,"French, 1831–1891",,"Appert, Ernest Eugène",,1831,1891,1870–71,1870,1871,Albumen silver print from glass negative,Sheet: 36 x 46 cm (14 3/16 x 18 1/8 in.),"Joyce F. Menschel Photography Library Fund, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.352 (11),false,true,702001,Photographs,Photographs,"Prison des Chantiers, le 15 août 1871, Versailles",,,,,,Artist,,Ernest Eugène Appert,"French, 1831–1891",,"Appert, Ernest Eugène",,1831,1891,1870–71,1870,1871,Albumen silver print from glass negative,Sheet: 36 x 46 cm (14 3/16 x 18 1/8 in.),"Joyce F. Menschel Photography Library Fund, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/702001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.6,false,true,705511,Photographs,Photograph,"Notation of Scars, Schematic Drawings",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,ca. 1893,1888,1898,Albumen silver prints,Image (top): 6 in. × 4 1/8 in. (15.2 × 10.4 cm) Image (bottom): 2 3/16 × 4 1/8 in. (5.6 × 10.5 cm) Mount: 11 3/4 × 7 3/4 in. (29.8 × 19.7 cm),"The Horace W. Goldsmith Foundation Fund, Through Joyce and Robert Menschel, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/705511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.7,false,true,705529,Photographs,Photograph,Measurement of Left Middle Finger,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,ca. 1893,1888,1898,Albumen silver prints,Image (top): 5 7/8 × 4 7/16 in. (15 × 11.2 cm) Image (bottom): 2 7/8 × 4 3/8 in. (7.3 × 11.1 cm) Mount: 11 3/4 × 7 3/4 in. (29.8 × 19.7 cm),"The Horace W. Goldsmith Foundation Fund, Through Joyce and Robert Menschel, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/705529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.1,false,true,306624,Photographs,Mugshot,"Adnet. Clotilde. 19 ans, née en décembre 74 à Argentant (Orne). Brodeuse. Anarchiste. Fichée le 7/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.2,false,true,306625,Photographs,Mugshot,"Adnet. Jeanne, Marie. Alphonsine (femme Quesnel). 22 ans, née à Argentan. Couturière. Anarchiste. 8/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.3,false,true,306626,Photographs,Mugshot,"Olguéni Gustave. 24 ans, né à Sala (Suède) le 24-5-69. Artiste-peintre. Anarchiste. 14-3-94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.4,false,true,306627,Photographs,Mugshot,"Alban. Jean-Louis. 35 ans, né à Paris. Plombier. Anarchiste. Fiché le 5/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.5,false,true,306628,Photographs,Mugshot,"Alicante. Philibert. 33 ans, né à Seire (Seine & Oise). Coupeur de talons. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.6,false,true,306629,Photographs,Mugshot,"Anacléto. Joseph, Jean-Baptiste. 36 ans, né le 14/7/57. Coiffeur. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.7,false,true,306630,Photographs,Mugshot,"Anceau. Aimé-Firmin. 20 ans, né le 18/2/74 à Paris XIIe. Sculpteur sur bois. Anarchiste. 17/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.8,false,true,306631,Photographs,Mugshot,"Arnaud. Eugène. 47 ans, né à Villeveyrac (Hérault). Ferblantier. Anarchiste. 20/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.9,false,true,306632,Photographs,Mugshot,"Augendre. Ernest. 37 ans, né à St-Pierre le Moutier (Nièvre). Maçon. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.10,false,true,306633,Photographs,Mugshot,"Aumaréchal. Auguste. 44 ans, né à Chateaumeillant (Cher). Ébéniste. Association de malfaiteurs. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.11,false,true,306634,Photographs,Mugshot,"Auvin. Henri. 37 ans, né à St-Meme (Charente-Inférieure). Chaudronnier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.12,false,true,306635,Photographs,Mugshot,"Baben. Hyppolyte, Antoine. 49 ans, né à St Sermain (Aveyron). Serrurier. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.13,false,true,306636,Photographs,Mugshot,"Baerisvuyl. Frédéric, Jean. 28 ans, né à Fribourg (Suisse). Ébéniste. Anarchiste. 8/1/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.14,false,true,306637,Photographs,Mugshot,"Baerisvuyl. Frédéric, Jean. 28 ans, né à Fribourg (Suisse). Ébéniste. Anarchiste. 8/1/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.15,false,true,306638,Photographs,Mugshot,"Barbichon. Jacques, Émile. 62 ans, né à Provins. Marchand de mouron. Anarchiste. 9/3/91.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1891,1891,1891,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.16,false,true,306639,Photographs,Mugshot,"Barbier. Émile, Alphonse. 36 ans, né à Paris. Peintre en bâtiment. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.17,false,true,306640,Photographs,Mugshot,"Barbier. Louis, Alexandre. 31 ans, né à Jussecourt (Marne). Comptable. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.18,false,true,306641,Photographs,Mugshot,"Barreyre. Alfred. 30 ans, né le 30/6/64 à Brassac (P. de Dôme). Gérant de restaurant. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.19,false,true,306642,Photographs,Mugshot,"Bassille. Maurice, Eugène. 19 ans, né à Paris Ille. Portefeuilliste. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.20,false,true,306643,Photographs,Mugshot,"Bastard. Élisée, Joseph, Michel. 22 ans, né à Birnel (Oise). Polisseur. Anarchiste. 20/8/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.21,false,true,306644,Photographs,Mugshot,"Bastard. Élisée, Joseph, Michel. 22 ans, né à Birnel (Oise). Polisseur. Anarchiste. 20/8/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.22,false,true,306645,Photographs,Mugshot,"Barla. Jean Michel. 46 ans, né à Sirié (Italie). Mécanicien. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.23,false,true,306646,Photographs,Mugshot,"Baudart. Joseph, Philippe. 42 ans, né à Reims le 25/3/51. Boucher. Anarchiste. 15/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.24,false,true,306647,Photographs,Mugshot,"Baumester. Augustin, Etienne. 49 ans, né le 16/1/45 à Paris VIe. Décorateur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.25,false,true,306648,Photographs,Mugshot,"Baur. Pierre. 41 ans, né à St Leonard (Haute-Vienne). Cordonnier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.26,false,true,306649,Photographs,Mugshot,"Bazin. Claudius dit César. 36 ans, né à Chatillon (Ain). Mécanicien. Anarchiste. 11/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.27,false,true,306650,Photographs,Mugshot,"Beaufort. Gilbert. 32 ans, né à Paris Xle. Ébéniste. Anarchiste. 5/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.28,false,true,306651,Photographs,Mugshot,"Beaulieu. Henri, Félix, Camille. 23 ans, né le 30/11/70 à Paris Ve. Comptable. Anarchiste. 23/5/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.29,false,true,306652,Photographs,Mugshot,"Beaulieu. Henri, Félix, Camille. 23 ans, né le 30/11/70 à Paris Ve. Comptable. Anarchiste. 23/5/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.30,false,true,306653,Photographs,Mugshot,"Becu. Lucien. 27 ans, né à la Conté d'Abigny (Pas-de-Calais). Garçon de café. Pas de motif. 22/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.31,false,true,306654,Photographs,Mugshot,"Bedei. Hercule. 21 ans, né à Sorli (Italie). Tailleur d'habits. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.32,false,true,306655,Photographs,Mugshot,"Bellemans. Eugène (ou Michel). 23 ans, né à Gand (Belgique). Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.33,false,true,306656,Photographs,Mugshot,"Bellet. Eléonore. Alexandre. 29 ans, né le 23/9/65 à Salouel (Somme). Teinturier. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.34,false,true,306657,Photographs,Mugshot,"Bellon. Joseph, Alexandre. 54 ans, né à Granville (Meuse) le 19/6/39. Journalier. Anarchiste. 17/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.35,false,true,306658,Photographs,Mugshot,"Belloti. Louis. 28 ans, né à Turin. Camelot. Anarchiste. 18/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.36,false,true,306659,Photographs,Mugshot,"Benoit. Antoine. 29 ans, né à Paris Xle. Journalier. Anarchiste, vagabondage. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.37,false,true,306660,Photographs,Mugshot,"Benoit. Joseph, Alexandre. 33 ans, né le 9/6/61 à Paris XIIIe. Potier d'étain. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.38,false,true,306661,Photographs,Mugshot,"Berard. Adolphe. 52 ans, né le 26/9/41 à Paris Ve. Ébéniste. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.39,false,true,306662,Photographs,Mugshot,"Bernaix. Louis. 29 ans, né à Clichy. Couvreur. Anarchiste. Fiché le 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.40,false,true,306663,Photographs,Mugshot,"Bernard. Paul, Auguste. 32 ans, né à Crest (Drôme). Employé. Excitation au meurtre, anarchiste. 11/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.41,false,true,306664,Photographs,Mugshot,"Bernard. Paul, Auguste. 32 ans, né à Crest (Drôme). Employé. Excitation au meurtre, anarchiste. 11/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.42,false,true,306665,Photographs,Mugshot,"Ber(h)nard. Victor. 43 ans, né à Paris. Coupeur. Anarchiste. Fiché le 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.43,false,true,306666,Photographs,Mugshot,"Berson. Samuel. 28 ans, né le 3/?/65 à Dinabourg (Russie). Tailleur d'habits. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.44,false,true,306667,Photographs,Mugshot,"Bertani. Orsini. 24 ou 25 ans, né à Florence (Italie). Sans profession. Anarchiste. 18/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.45,false,true,306668,Photographs,Mugshot,"Bertani. Orsini. 24 ou 25 ans, né à Florence (Italie). Sans profession. Anarchiste. 18/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.46,false,true,306669,Photographs,Mugshot,"Bertho. François, Élie. 26 ans, né le 30/9/67 à Jallais (Maine & Loire). Employé. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.47,false,true,306670,Photographs,Mugshot,"Bertout. Marie, Ismérie. 41 ans, née à Reims. Marchande de vins. Pas de motif. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.48,false,true,306671,Photographs,Mugshot,"Biais. Fernand, Alphonse. 41 ans, né le 28/6/53 à Laval (Mayenne). Tourneur sur bois. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.49,false,true,306672,Photographs,Mugshot,"Billon. Gabriel, André, Adolphe. 20 ans, né à Boulogne /s/Seine. Typographe. Outrages. 14/8/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.50,false,true,306673,Photographs,Mugshot,"Billot. Eugène. 20 ans, né à La Charité (Nièvre). Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.51,false,true,306674,Photographs,Mugshot,"Billot. Jean. 23 ans, né le 23/1/71 à Bourges (Cher). Tireur en barre. Anarchiste. 1/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.52,false,true,306675,Photographs,Mugshot,"Birilay. Henri, Marc, Julien. 46 ans, né à Chartres (Eure & Loir). Journalier. Anarchiste. 11/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.53,false,true,306676,Photographs,Mugshot,"Bissonier. Sébastien. 19 ans, né à St Bonnet (Allier). Journalier. Outrage à la Gendarmerie. 5/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.54,false,true,306677,Photographs,Mugshot,"Bligny. André, Eugène. 58 ans, né à Vincennes. Serrurier. Anarchiste. Fiché le 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.55,false,true,306678,Photographs,Mugshot,"Blay. François. 53 ans, né à St Gervais (Rhône). Tailleur. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.56,false,true,306679,Photographs,Mugshot,"Bocquet. Alexandre, Émile. 17 ans, né à Paris XVlle. Menuisier. Vol. Fiché le 14/4/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.57,false,true,306680,Photographs,Mugshot,"Bompeix. Eugène. 53 ans, né à St Martin d'Herbus (Haute-Vienne). Conducteur de machines. Anar. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.58,false,true,306681,Photographs,Mugshot,"Borderie. Ferdinand, Jacques. 19 ans, né à Sarlat (Dordogne). Peintre sur métaux. Pas de motif. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.59,false,true,306682,Photographs,Mugshot,"Borderie. Raoul. 18 ans, né à Castelsarazin (Tarn & Garonne). Peintre en bâtiment. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.60,false,true,306683,Photographs,Mugshot,"Bordes. Auguste. 15 ans, né à Paris XVIIIe. Garçon Marchand de vins. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.61,false,true,306684,Photographs,Mugshot,"Bordes. Guillaume, Auguste. 40 ans, né à Centrayes (Aveyron). Tailleur. Pas de motif. 29/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.62,false,true,306685,Photographs,Mugshot,"Borreman. Léontine, Eugénie. 23 ans, née à Paris le 25/12/70. Papetière. Anarchiste. 13/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.63,false,true,306686,Photographs,Mugshot,"Bossant. Edmond, Léon. 52 ans, né à Valenciennes (Nord). Sans profession. Anarchiste. 27/4/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.64,false,true,306687,Photographs,Mugshot,"Bossard. Célestin. 33 ans, né le 5/3/61 à Gonbretière (Vendée). Cordonnier. Anarchiste. 2/7/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.65,false,true,306688,Photographs,Mugshot,"Bouchenez. Adolphe. 36 ans, 27/2/94. (En rouge barrant la fiche: ""Transféré à Mazas. À faire extraire"").",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.66,false,true,306689,Photographs,Mugshot,"Bouchez. Louis. 19 ans, né le 29/8/75 à Paris XXe. Sculpteur. Anarchiste. 6/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.67,false,true,306690,Photographs,Mugshot,"Boulnois. Paul, Cyprien. 20 ans, né à Paris Ille. Employé de commerce. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.68,false,true,306691,Photographs,Mugshot,"Bourbasquet. François. 25 ans, né le 11/3/69 à St Avé (Morbihan). Garçon coiffeur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.69,false,true,306692,Photographs,Mugshot,"Bourguoin. Hubert, Jules. 51 ans, né le 17/8/42 à Jelles (Seine & Marne). Maçon. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.70,false,true,306693,Photographs,Mugshot,"Bourlard. Joseph, Anselme. 45 ou 46 ans, né à Biemme (Belgique). Piqueur de grès. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.71,false,true,306694,Photographs,Mugshot,"Boutel. Joseph. Louis. 34 ans, né à Bonnay (Eure). Corroyeur. Anarchiste. 5/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.72,false,true,306695,Photographs,Mugshot,"Braun. Frédéric, Charles. 28 ans. Fiché le 22/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.73,false,true,306696,Photographs,Mugshot,"Breiner. Jean-Baptiste. 31 ans, né à Bar sur Aube (Aube). Mécanicien. Anarchiste. 5/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.74,false,true,306697,Photographs,Mugshot,"Bresson. Eugène, Marie. 30 ans, né le 4/7/63 à Chaumont (Haute-Marne). Avocat. Anarchiste. 20/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.75,false,true,306698,Photographs,Mugshot,"Breton. Ernest, Jean. 28 ans, 27/10/63. Anarchiste 24/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.76,false,true,306699,Photographs,Mugshot,"Briet. Albert, Louis. 44 ans, né à Lyon (Rhône). Boulanger. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.77,false,true,306700,Photographs,Mugshot,"Brosselin. Jean-Baptiste. 32 ans, né le 10/1/62 à Aulay (Côte d'Or). Menuisier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.78,false,true,306701,Photographs,Mugshot,"Broggio. (Roche), Bernard. 39 ans, né en Italie. Journalier. Association de malfaiteurs. 22/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.79,false,true,306702,Photographs,Mugshot,"Bruchaesen. Etienne. 31 ans, né à Mag Levard (Hongrie). Tailleur d'habits. Anarchiste. 11/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.80,false,true,306703,Photographs,Mugshot,"Bruneau. Amédé, Jean Baptiste. 46 ans, né à Châteauroux (Indre). Cordonnier. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.81,false,true,306704,Photographs,Mugshot,"Brunel. Alexandre. 50 ans, né le 25/12/43 à Renaix (Belgique). Menuisier. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.82,false,true,306705,Photographs,Mugshot,"Brunet. Felix. 21 ans, né le 21/2/73 à Paris XVe. Peintre en voiture. Anarchiste. 7/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.83,false,true,306706,Photographs,Mugshot,"Brunet. Georges. 25 ans, né à Paris. Menuisier. Anarchiste. 4/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.84,false,true,306707,Photographs,Mugshot,"Buhr. Victor. 25 ans, né à Cologne (Allemagne). Peintre décorateur. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.85,false,true,306708,Photographs,Mugshot,"Cabuzac. Jean. 25 ans, né le 23/7/68 à Ivry la Bataille (Eure). Ciseleur. Anarchiste. 12/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.86,false,true,306709,Photographs,Mugshot,"Cana. Eugène, Louis. 22 ans, né à Paris Vllle. Monteur en bronze. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.87,false,true,306710,Photographs,Mugshot,"Cana. Eugène, Pierre. 47 ans, né à Paris XIe. Monteur en bronze. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.88,false,true,306711,Photographs,Mugshot,"Capette. Joseph, Désiré. 56 ans, né à Paris VI. Maroquinier. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.89,false,true,306712,Photographs,Mugshot,"Carraglia. Charles. 39 ans, né à Moceto (It). Homme de lettres. Anar, infraction à la loi du 21/6/73. 13/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.90,false,true,306713,Photographs,Mugshot,"Carteau. Auguste. 23 ans, né à St-Florent (Cher). Verrier. Anarchiste. 1/5/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.91,false,true,306714,Photographs,Mugshot,"Castallou. Charles. 53 ans, né le 4/10/41 à Paris IIe. Tapissier. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.92,false,true,306715,Photographs,Mugshot,"Catty. Nicolas, Pierre, François. 47 ans, né à Fressenneville (Somme). Mécanicien. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.93,false,true,306716,Photographs,Mugshot,"Cazal. Antoinette. 28 ans, née à Salgouz (Cantal). Couturière. Anarchiste. 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.94,false,true,306717,Photographs,Mugshot,"Ceaglio. Alexandre, Joseph. 42 ans, né à Turin (Italie). Employé de commerce. Anarchiste. 3/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.95,false,true,306718,Photographs,Mugshot,"Chambon. Raoul. 20 ans, né le 3/7/73 à Valréas (Vaucluse). Graveur. Anarchiste. 26/5/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.96,false,true,306719,Photographs,Mugshot,"Chapin. Armand, Louis. 30 ans, né à Épeigné (Indre & Loire). Charron. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.97,false,true,306720,Photographs,Mugshot,"Chapuis. Charles, Paul. 17 ans, né le 22/3/76 à Paris VIle. Tapissier. Anarchiste. 7/1/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.98,false,true,306721,Photographs,Mugshot,"Charlier. Ernile, Frédéric. 40 ans, né à Brest (Finistère). Peintre. Vol. 18/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.99,false,true,306722,Photographs,Mugshot,"Charrié. Cyprien. 26 ans, né le 7/10/67 à Paris XVIlle. Imprimeur. Anarchiste 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.100,false,true,306723,Photographs,Mugshot,"Charrié. Léon, Joseph. 27 ans, né à Paris XVllle. Garçon Plombier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.101,false,true,306724,Photographs,Mugshot,"Chatel. Charles. 25 ans, né le 8/10/68 à Paris XVIIle. Anarchiste. 14/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.102,false,true,306725,Photographs,Mugshot,"Chatillon. Jean-Baptiste. 31 ans, né à Toiseron des Minard (Cher). Employé de commerce. Note du cabinet. 10/5/82",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1882,1882,1882,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.103,false,true,306726,Photographs,Mugshot,"Chauman. Nicolas. 38 ans, né à Paris XVe. Puisatier. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.104,false,true,306727,Photographs,Mugshot,"Chaumelin. Odilon. 22 ans, né à Paris VIe. Publiciste. Anarchiste. 24/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.105,false,true,306728,Photographs,Mugshot,"Chauvin. Émile. 18 ans, né à Paris IVe. Employé. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.106,false,true,306729,Photographs,Mugshot,"Chavanne. Gaston. 26 ans, né à Paris VIe. Graveur. Anarchiste. 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.107,false,true,306730,Photographs,Mugshot,"Chericotti. Paul. 35 ans, né à Milan (Italie). Marchand de volailles. Anarchiste/Assoc. de malfaiteurs. 25/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.108,false,true,306731,Photographs,Mugshot,"Chericotti. Paul. 35 ans, né à Milan (Italie). Marchand de volailles. Anarchiste/Assoc. de malfaiteurs. 25/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.109,false,true,306732,Photographs,Mugshot,"Chericotti. Paul. 35 ans, né à Milan (Italie). Marchand de volailles. Anarchiste/Assoc. de malfaiteurs. 25/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.110,false,true,306733,Photographs,Mugshot,"Chevalier. Étienne. 36 ans, né à Gémosac (Charente-Inférieure). Forgeron. Anarchiste. 11/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.111,false,true,306734,Photographs,Mugshot,"Chiroki. Eva (veuve Ortiz). 53 ans, née à Grosbitlech (Autriche). Cuisinière. Anarchiste. 21/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.112,false,true,306735,Photographs,Mugshot,"Chornat. Pierre. 50 ans, né le 20/6/44 à Letrat (Loire). Constructeur-mécanicien. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.113,false,true,306736,Photographs,Mugshot,"Clidière. François. 39 ans, né le 3/2/55 à Miales (Dordogne). Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.114,false,true,306737,Photographs,Mugshot,"Cler. Henri. 31 ans, né à Paris XIe. Ébéniste. Anarchiste. 14/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.115,false,true,306738,Photographs,Mugshot,"Clouard. Paul, Jules. 35 ans, né le 20/6/58 à Peugans (Manche). Rétameur. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.116,false,true,306739,Photographs,Mugshot,"Cluzel. Louis. 30 ans, né le 31/8/63 à Bourg-Argental (Loire). Tailleur d'habits. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.117,false,true,306740,Photographs,Mugshot,"Collet. Edouard, Jean-Baptiste. 44 ans, né le 6/1/50 à Paris XVlle. Ciseleur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.118,false,true,306741,Photographs,Mugshot,"Collot. Marie, Eugénie. 36 ans, né à Paris Xle. Tapissier. Anarchiste. 11/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.119,false,true,306742,Photographs,Mugshot,"Colombo. Joseph (on Jean, Octave). 19 ans, né à Paris Xlle. Monteur en bronze. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.120,false,true,306743,Photographs,Mugshot,"Colombet. Frédéric. 28 ans, né à Prigonnieux (Dordogne). Employé de commerce. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.121,false,true,306744,Photographs,Mugshot,"Couchot. Jean. 49 ans, né à Bidache (Basses-Pyrénées). Tailleurs d'habits. Anarchiste. 23/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.122,false,true,306745,Photographs,Mugshot,"Coudry. Hubert, Louis. 24 ans, né à Paris XVe. Corroyeur. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.123,false,true,306746,Photographs,Mugshot,"Court. Jean-Claude. 58 ans, né à Cherissey-le-M. (Haute-Savoie). Marchand de pains d'épices. Anarchiste. 17/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.124,false,true,306747,Photographs,Mugshot,"Cornu. Eugène. 25 ans, né à Paris XXe le 27/3/94. Cordonnier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.125,false,true,306748,Photographs,Mugshot,"Cornuault. Joseph. 17 ans, né à Angers (Maine & Loire). Peintre eu bâtiment. Anarchiste. 7/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.126,false,true,306749,Photographs,Mugshot,"Cottée. Edouard, Eugène. 37 ans, né à Paris XVle. Artiste-peintre. Vol par complicité. 6/2/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.127,false,true,306750,Photographs,Mugshot,"Crespin. Joseph. 40 ans, né à Roquesteron (Alpes-Maritimes). Employé de banque. 25/3/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.128,false,true,306751,Photographs,Carte-de-visite; Mugshot,"Crespin. Joseph. 40 ans, né à Roquesteron (Alpes-Maritimes). Employé de banque. 25/3/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Gelatin silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.129,false,true,306752,Photographs,Mugshot,"Cros. Jean. 19 ans, né à Négrin (Tarn). Tailleur d'habits. Pas de motif. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.130,false,true,306753,Photographs,Mugshot,"Daguenet. Eugerne, Carolin. 39 ans, né à Granville (Manche). Ébéniste. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.131,false,true,306754,Photographs,Mugshot,"Damalix. Émile, Auguste. 37 ans, né à St-Claude (Doubs). Charpentier. Anarchiste. 19/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.132,false,true,306755,Photographs,Mugshot,"Daressy. Pierre. 39 ans, né à Lherme (Haute-Garonne). Cordonnier. Anarchiste. 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.133,false,true,306756,Photographs,Mugshot,"D'Auby. Henri. 48 (ou 49) ans, né à Montmédy (Meuse). Menuisier. Anarchiste. 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.134,false,true,306757,Photographs,Mugshot,"Dauriac. Henri, Georges. 36 ans, né à Memphis (USA). Agent d'affaires. Extortion de fonds. 22/12/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.135,false,true,306758,Photographs,Mugshot,"Dauriac. Henri, Georges. 36 ans, né à Memphis (USA). Agent d'affaires. Extortion de fonds. 22/12/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.136,false,true,306759,Photographs,Mugshot,"David. Armand, Auguste. 27 ans, né à Gien (Loiret). Faïencier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.137,false,true,306760,Photographs,Mugshot,"Decker. Jacques. 43 ans, né à Grodeskersheim (Bas-Rhin). Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.138,false,true,306761,Photographs,Mugshot,"Deforge. Henri, Walter. 19 ans, né à Bruxelles (Belgique). Porteur de journaux. Anarchiste. 6/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.139,false,true,306762,Photographs,Mugshot,"Defosse. Claude (dit Delfosse ou Lafosse). 29 ans, né à Arbeuf (Nièvre). Cocher. Anarchiste. 2/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.140,false,true,306763,Photographs,Mugshot,"Deguet. Victor, Adonis. 50 ans, né le 27/2/44 à Vuenpont (Aisne). Armurier. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.141,false,true,306764,Photographs,Mugshot,"Deherme. Marie, Adolphe. 26 ans, né à Paris XVIIe. Publiciste. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.142,false,true,306765,Photographs,Mugshot,"Dejernier (ou Degernier). Edouard. 45 ans, né à Gand (Belgique). Tailleur d'habits. Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.143,false,true,306766,Photographs,Mugshot,"Dejoux. Jules. 46 ans, né le 16/6/48 à La Châtre (Indre). Maçon. Délit de presse. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.144,false,true,306767,Photographs,Mugshot,"Delabie. Georges. 43 ans, né le 30/10/50 à Ganaches (Somme). Mécanicien. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.145,false,true,306768,Photographs,Mugshot,"De la Salle. Gabriel. 45 ans, né à Nantes (Loire-Inf.). Publiciste. Disposition du Préfet de Police. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.146,false,true,306769,Photographs,Mugshot,"Delesderrier. Louis. 34 ans, né le 2/3/60 à Paris Ille. Ciseleur. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.147,false,true,306770,Photographs,Mugshot,"Deliège. Nicolas, François. 19 ans, né à Ixelles (Belgique). Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.148,false,true,306771,Photographs,Mugshot,"Della Casa. 36 ans, né le 1/3/58 à Avoglion (Italie). Cordonnier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.149,false,true,306772,Photographs,Mugshot,"Dery. Louis. 60 ans, né à Cobugny (Nièvre). Cordonnier. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.150,false,true,306773,Photographs,Mugshot,"Dodot. Émile (ou Jules). 55 ans, né à Paris Ier. Cordonnier. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.151,false,true,306774,Photographs,Mugshot,"Dufour. Louis. 37 ans, né à Port-Ste-Marie (Lot & Garonne). Bijoutier. Anarchiste.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.152,false,true,306775,Photographs,Mugshot,"Dumout. Henri, Victor. 29 ans, né à Issy (Seine). Mécanicien. Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.153,false,true,306776,Photographs,Mugshot,"Dupit. Paul. 20 ans, né le 13/3/74 à Paris XVIIe. Garçon boucher. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.154,false,true,306777,Photographs,Mugshot,"Duprat. François, Louis. 34 ans, né à St-Martin (Gers). Marchand de vins. Anarchiste. 27/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.155,false,true,306778,Photographs,Mugshot,"Dupuis. Augustin. 53 ans, né le 24/6/41 à Dourdan (Seine & Oise). Charron, forgeron. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.156,false,true,306779,Photographs,Mugshot,"Dupuy. Edmond, Adolphe. 29 ans, né à Paris XIVe. Employé de commerce. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.157,false,true,306780,Photographs,Mugshot,"Durey. François, Louis. 43 ans, né le 25/2/51 à Lyon (Rhône). Architecte. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.158,false,true,306781,Photographs,Mugshot,"Durieux. Aléxis, Alberic. 20 ans, né le 28/2/74 à Stains (Seine). Verrier. Vol. 24/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.159,false,true,306782,Photographs,Mugshot,"Dutheil. Louis. 29 ans, né le 28/7/64 à Maisonnais (Ht Vienne). Tailleur d'habits. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.160,false,true,306783,Photographs,Mugshot,"Etiévant. Henri, Achille. 32 ans, né à Flammanville (Manche). Typographe. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.161,false,true,306784,Photographs,Mugshot,"Fauvel. Louis. 27 ans, né le 14/4/67 à Écouché (Orne). Tourneur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.162,false,true,306785,Photographs,Mugshot,"Favre. Pierre, Maurice. 29 ans, né le 30/11/64. Ciseleur. Anarchiste. 15/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.163,false,true,306786,Photographs,Mugshot,"Favre. Sébastien. 36 ans, né à St Étienne (Loire). Négociant. Port d'arme prohibée, anarchiste. 20/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.164,false,true,302429,Photographs,Mugshot,Feneon. Felix. Clerk of the Galerie Berheim Jeune.,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894–5,1894,1895,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.165,false,true,306787,Photographs,Mugshot,"Ferter. Ernest, Charles. 31 ans, né le 23/10/62 à Melun (Seine & Marne). Fumiste. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.166,false,true,306788,Photographs,Mugshot,"Fétis. Julien. 26 ans, né à New York (USA). Couvreur. Anarchiste. 3/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.167,false,true,306789,Photographs,Mugshot,"Fischter. Joseph. 47 ans, né à Paris Ve. Imprimeur sur papiers-peints. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.168,false,true,306790,Photographs,Mugshot,"Forti. Alfred. 18 ans, né à Milan (Italie). Restaurateur. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.169,false,true,306791,Photographs,Mugshot,"Forti. Ernesta. 45 (ou 46) ans, née à Lodi (Italie). Laitière. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.170,false,true,306792,Photographs,Mugshot,"Fournier. Émile, Christophe. 26 ans, né à St-Martial (Creuse). Serrurier. Vol. 13/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.171,false,true,306793,Photographs,Mugshot,"Foussard. Eugène. 26 ans, né le 7/12/67 à Dangeul (Sarthe). Peintre en bâtiment. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.172,false,true,306794,Photographs,Mugshot,"Francier. Éloi. 41 ans, né le 28/10/53 à Resson-le-Long (Aisne). Ébéniste. Anarchiste. 22/5/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.173,false,true,306795,Photographs,Mugshot,"François dit Francis. 38 ans, né le 3/12/55 à Reims (Marne). Ébéniste. Anarchiste. 5/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.174,false,true,306796,Photographs,Mugshot,"Gaillard. Pierre, Auguste. 47 ans, né à Foulanges (Cantal). Employé de commerce. Anarchiste. 15/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.175,false,true,306797,Photographs,Mugshot,"Galau. Charles. 18 ans, né à Nogent s/Marne (Seine). Charron. Cris séditieux. Anarchiste. 21/2/91.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1891,1891,1891,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.176,false,true,306798,Photographs,Mugshot,"Galau (ou Gallot). Louis. 53 ans, né à Meriziès (Tarn). Charron. Anarchiste. 21/8/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.177,false,true,306799,Photographs,Mugshot,"Gama. Joseph. 41 ans, né le 5/3/42 à Paris IXe. Graveur. Anarchiste. 6/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.178,false,true,306800,Photographs,Mugshot,"Garnier. Anatole, Auguste. 18 ans, né à Montereau (Seine & Marne). Orfèvre. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.179,false,true,306801,Photographs,Mugshot,"Garnier. Auguste. 34 ans, né à Périgny (Côte-d'Or). Journalier. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.180,false,true,306802,Photographs,Mugshot,"Gatinet. Pierre, Adrien. 50 ans, né le 13/10/43 à Bourges (Cher). Charpentier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.181,false,true,306803,Photographs,Mugshot,"Gauche. Henri. 24 ans, né le 7/2/60 à Paris. Rentier. Anarchiste.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1884,1884,1884,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.182,false,true,306804,Photographs,Mugshot,"Gelhausen. Jean. 55 ans, né à Grevennemache (Luxembourg). Cordonnier. Infraction à la loi du 18/12/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.183,false,true,306805,Photographs,Mugshot,"Giroux. Hippolyte. 34 ans, né à Montreuil les Mines (Saone & Loire). Mécanicien. Anarchiste. 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.184,false,true,306806,Photographs,Mugshot,"Godard. Armand, Alexandre. 18 ans, né le 11/3/75 à Paris XVIIe. Électricien. Cris séditieux. 6/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.185,false,true,306807,Photographs,Mugshot,"Gordon. Max. 39 ans, né le 20/8/54 à Vilna (Italie). Employé de commerce. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.186,false,true,306808,Photographs,Mugshot,"Grandidier. Louis, Auguste. 20 ans, né à St-Denis (Seine). Journalier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.187,false,true,302430,Photographs,Mugshot,"Grave. Jean. 38 ans, né le 16/10/54 à Breuil (Puy de Dôme). Typographe. Anarchiste. 9/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.188,false,true,306809,Photographs,Mugshot,"Grégoire. Aimé, Paul. 36 ans, né à Bruxelles (Belgique). Accordeur de piano. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.189,false,true,306810,Photographs,Mugshot,"Grégoire. Alphonse. 27 ans, né à La Montagne (Loire-Inférieure). Mécanicien. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.190,false,true,306811,Photographs,Mugshot,"Grugeau. Alfred, Alphonse. 26 ans, né à Tours (Indre & Loire). Cordonnier. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.191,false,true,306812,Photographs,Mugshot,"Guelle (ou Gueulle, dit St Denis). 72 ans, né à Beauvais (Oise). Matelassier. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.192,false,true,306813,Photographs,Mugshot,"Guénant. Louis, Désiré. 31 ans, né à Paris XVIIIe. Comptable. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.193,false,true,306814,Photographs,Mugshot,"Guerlinger. Pierre. 28 ans, né le 31/5/65 à St-Avold (Moselle). Journalier. Anarchiste. 14/4/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.194,false,true,306815,Photographs,Mugshot,"Guéry. Paul, Alphonse. 37 ans, né le 30/6/56 à Laversine (Aisne). Journalier. Anarchiste. 12/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.195,false,true,306816,Photographs,Mugshot,"Guignard. Georges, Auguste. 36 ans, né le 1/1/58 à Neuilly (Seine). Plombier. Anarchiste. 15/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.196,false,true,306817,Photographs,Mugshot,"Guillemard. Isidore, François. 46 ans, né à St-Michel des Andaines (Orne). Menuisier. Anarchiste 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.197,false,true,306818,Photographs,Mugshot,"Haesig. Léon. 18 ans, né à St-Denis. Chaudronnier. Disposition du Préfet de Police. 14/4/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.198,false,true,306819,Photographs,Mugshot,"Handrock. Frédéric(k). Vilhem. 34 ans, né à Lyeck (Allemagne). Doreur sur bois. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.199,false,true,306820,Photographs,Mugshot,"Hannedouche. François. 31 ans, né à Lilleris (Pas de Calais). Peintre en bâtiments. Anarchiste. 1/1/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.200,false,true,306821,Photographs,Mugshot,"Havard. Octave, Onésime. 25 ans, né à Hedouville (Calvados). Polisseur. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.201,false,true,306822,Photographs,Mugshot,"Hébert. Georges, Henri. 27 (ou 29) ans, né à Bayeux (Calvados). Menuisier. Anarchiste. 23/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.202,false,true,306823,Photographs,Mugshot,"Henon. François. 52 ans, né le 5/5/42 à Lyon (Rhône). Caissier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.203,false,true,306824,Photographs,Mugshot,Henry. Émile. (auteur de l'attentat de l'Hotel St-Lazare),,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1890–94,1890,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.204,false,true,306825,Photographs,Mugshot,Henry. Émile. (auteur de l'attentat de l'Hotel St-Lazare),,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1890–94,1890,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.205,false,true,306826,Photographs,Mugshot,"Herman. Caroline. 33 ans, née à Paris Vllle. Couturière. Disposition du Préfet (Anarchie). 21/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.206,false,true,306827,Photographs,Mugshot,"Herouard. Henri. 17 ans, né à Paris XVIIe. Serrurier. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.207,false,true,306828,Photographs,Mugshot,"Hervy. Marcel, Noël. 19 ans, né à Paris XVIIIe. Raccommodeur de porcelaine. Anarchiste. 27/10/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.208,false,true,306829,Photographs,Mugshot,"Hettich (ou Hettig). Eugène. 17 ans, né le 6/1/77 à Paris XXe. Cocher. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.209,false,true,306830,Photographs,Mugshot,"Heurteaux. Auguste. 31 ans, né à Paris Xe. Polisseur. Anarchiste. 3/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.210,false,true,306831,Photographs,Mugshot,"Hivon. Pierre. 45 ans, né à Bourbon le Chambai (Allier). Lithographe. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.211,false,true,306832,Photographs,Mugshot,"Hostenbock. Joseph, Louis. 22 ans, né à Bruxelles (Belgique). Coiffeur. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.212,false,true,306833,Photographs,Mugshot,"Hourt. Jean. 34 (ou 35) ans, né à Reims (Marne). Menuisier. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.213,false,true,306834,Photographs,Mugshot,"Imhof. Louis, Alfred. 37 ans, né à Mex (Suisse). Journalier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.214,false,true,306835,Photographs,Mugshot,"Iv(w)anowski. Casimir. 57 ans, né à Chalon-sur-Saône (Saône & Loire). Mécanicien. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.215,false,true,306836,Photographs,Mugshot,"Jacob. Georges, Gustave. 43 ans, né à Paris XVIIe. Journalier. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.216,false,true,306837,Photographs,Mugshot,"Jacot. Charles, Émile. 36 ans, né à Allenjoie (Doubs). Colporteur Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.217,false,true,306838,Photographs,Mugshot,"Jacquet. Hippolyte, Edouard. 49 ans, né le 15/3/45 à Paris Ille. Sellier-maroquinier. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.218,false,true,306839,Photographs,Mugshot,"Jaffard. Julien, Ludovic. 37 ans, né le 31/5/57 à Lesterps (Charente). Journalier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.219,false,true,306840,Photographs,Mugshot,"Jamard. Alphonse, Ernest. 51 ans, né à Paris. Distillateur. Anarchiste. 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.220,false,true,306841,Photographs,Mugshot,"Job. Eugène, François. 31 ans, né à Paris Xle. Chaisier. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.221,false,true,306842,Photographs,Mugshot,"Jordy. Baptiste. 61 ans, né le 8/72/32 à Labardes (Aude). Cordonnier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.222,false,true,306843,Photographs,Mugshot,"Jourdan. Numa. 30 ans, né le 27/8/61 à Courbevoie (Seine). Teinturier. Anarchiste. 23/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.223,false,true,306844,Photographs,Mugshot,"Kahn. Rodolphe. 43 ans, né le 15/1/51 à Lyon (Rhône). Courtier de commerce. Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.224,false,true,306845,Photographs,Mugshot,"Kaision. François. 39 ans, né à Reims. Mégissier. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.225,false,true,306846,Photographs,Mugshot,"Kern. Jacob, Hermann. 33 ans, né le 26/8/60 à Berlinger (Suisse). Comptable. Anarchiste. 10/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.226,false,true,306847,Photographs,Mugshot,"Kieffer. Nicolas. 35 ans, né le 8/4/59 à Haltuiller (Meurthe). Menuisier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.227,false,true,306848,Photographs,Mugshot,"Kilchenstein. Dominique. 48 ans, né à Luneville (Meurthe & Moselle). Marchand au panier. Anarchiste. 23/4/92",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.228,false,true,306849,Photographs,Mugshot,"Klein. Louis. 25 ans, né le 8/8/67 à Colmar (Alsace). Employé de commerce. Anarchiste. 11/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.229,false,true,306850,Photographs,Mugshot,"Labeyrie. Romain. 19 ans, né le 13/11/74 à Cauna (Landes). Sculpteur. Anarchiste. 10/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.230,false,true,306851,Photographs,Mugshot,"Labrie. Oscar, Alexandre. 33 ans, né à Charenton (Seine). Marchand de vins. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.231,false,true,306852,Photographs,Mugshot,"Lagane (ou Lagasse). Lucien, Pierre. 35 ans. 22/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.232,false,true,306853,Photographs,Mugshot,"Lamure. Eugène, Clément. 20 ans, né le 5/11/73 à Paris Ier. Gérant de magasin. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.233,false,true,306854,Photographs,Mugshot,"Landoin. Antoine, Eugène. 33 ans, né le 16/11/60 à Quincie (Rhône). Comptable. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.234,false,true,306855,Photographs,Mugshot,"Landschoot. Edouard. 27 ans, né le 6/8/67 à Paris. Bijoutier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.235,false,true,306856,Photographs,Mugshot,"Lapeyre. Louis, Pierre. 29 ans. né à Rodez (Aveyron). Employé. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.236,false,true,306857,Photographs,Mugshot,"Lapointe. Nicolas, Céleste. 45 ans, né à Marbach (Alsace-Lorraine). Cordonnier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.237,false,true,306858,Photographs,Mugshot,"Large. Etienne, Louis. 20 ans, né le 1/1/74 à Lyon (Rhône). Tapissier. Anarchiste. 9/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.238,false,true,306859,Photographs,Mugshot,"Lassalas. Ernest, Auguste. 33 ans, né à Paris IVe. Ébéniste. Anarchiste. 2/1/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.239,false,true,306860,Photographs,Mugshot,"LaumesfeIt. Paul, Mathias. 35 ans, né le 29/3/59. à Paris VIe. Tailleur d'habits. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.240,false,true,306861,Photographs,Mugshot,"Leballeur. Jules, Léon. 29 (ou 30) ans, né à Rouissé Jassée (Sarthe). Cordonnier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.241,false,true,306862,Photographs,Mugshot,"Leboucher. Edouard, Léon. 43 ans, né à Paris XIVe. Cordonnier. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.242,false,true,306863,Photographs,Mugshot,"Ledot. Julien. 41 ans, né à Bourges (Cher). Employé. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.243,false,true,306864,Photographs,Mugshot,"Lefebvre. Eugène, Anatole. 28 ans, né le 2/7/66 à St Pierre (Eure). Sculpteur sur bois. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.244,false,true,306865,Photographs,Mugshot,"Lefrançois. Charles, Albert. 27 ans, né le 10/11/67 à Paris XXe. Horloger. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.245,false,true,306866,Photographs,Mugshot,"Leger. Joseph. 16 ans, né à Marseille (Bouches-du-Rhône). Jardinier. Fabrication d'engins explosifs. 4/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.246,false,true,306867,Photographs,Mugshot,"Lelarge. Louis, Eugène. 46 ans, né le 5/4/48 à Paris Ille. Employé de commerce. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.247,false,true,306868,Photographs,Mugshot,"Leleu. Victor, Louis. 29 ans, né le 19/10/64 à Arras (Pas-de-Calais). Anarchiste. 9/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.248,false,true,306869,Photographs,Mugshot,"Lenfant. Émile, Jules. 26 ans, né à Choisy-le-Roi (Seine). Modeleur. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.249,false,true,306870,Photographs,Mugshot,"Léonard. Aimé. 30 ans, né à Chalonné/s/Loire (Maine & Loire). Mineur ou gazier. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.250,false,true,306871,Photographs,Mugshot,"Lepla. Henri, Florimond. 25 ans, né à Gand (Belgique). Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.251,false,true,306872,Photographs,Mugshot,"Letellier. Louis, Auguste. 29 ans, né à Rouen (Seine-Inférieure). Employé. Anarchiste. 23/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.252,false,true,306873,Photographs,Mugshot,"Leveillé. Louis. 37 ans, né le 7/7/57 à Cliche (Seine). Forgeron. Anarchiste. 7/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.253,false,true,306874,Photographs,Mugshot,"Liégeois (ou Liegois), François. 30 ans, né à Vilette (Meurthe & Moselle). Cordonnier. Anarchiste. 26/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.254,false,true,306875,Photographs,Mugshot,"Livenais (ou Livenay), André. 39 ans, né à Renazé (Mayenne). Garçon de magasin. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.255,false,true,306876,Photographs,Mugshot,"Loth. Clotilde, Caroline (femme Bossant). 43 ans, née à Valenciennes. Sans profession. Anarchiste. 27/4/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.256,false,true,306877,Photographs,Mugshot,"Lothier. Gaston. 27 ans, né le 27/12/66 à St Thomas (Charente-Inférieure). Menuisier. Anarchiste. 1/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.257,false,true,306878,Photographs,Mugshot,"Loutrel. François. 37 ans, né à Paris XVlle. Journalier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.258,false,true,302431,Photographs,Mugshot,"Luce. Maximilien. 36 ans, né le 13/3/58 à Paris VIIe. Artiste-peintre. Anarchiste. 6/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.259,false,true,306879,Photographs,Mugshot,"Lustenberger. Louis, Joseph. 38 ans, le 16/3/56 à Paris XXe. Ciseleur. Anarchiste. 17/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.260,false,true,306880,Photographs,Mugshot,"Lutringer. Pierre, Léopold. 43 ans, né le 25/11/50 à Stenay (Meuse). Cordonnier. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.261,false,true,306881,Photographs,Mugshot,"Mahler. Jacob, Henri, Jean. 61 ans, né à Hanovre (Duché de Hesse). Couvreur. Anarchiste. 3/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.262,false,true,306882,Photographs,Mugshot,"Maillabuau. Auguste, Léon. 30 ans, né le 23/8/93 à Paris Vle. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.263,false,true,306883,Photographs,Mugshot,"Maillard. Louis. 30 ans, né à Rennes (Ille & Vilaine). Employé de commerce. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.264,false,true,306884,Photographs,Mugshot,"Maince. Émile. 19 ans, né à Levallois-Perret (Seine). Réparateur d'objets d'arts. Anarchiste. 6/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.265,false,true,306885,Photographs,Mugshot,"Mainfroy. Albert, Pierre. 41 ans, né le 7/4/52 à Courbevoie (Seine). Imprimeur. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.266,false,true,306886,Photographs,Mugshot,"Malpet. Jeanne (femme Pivier). 51 ans, née en mai 42. Couturière. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.268,false,true,306888,Photographs,Mugshot,"Mangin. Edmond, Émile. 33 ans, né le 19/3/61 à Senon (Meuse). Cimentier. Anarchiste. 8/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.269,false,true,306889,Photographs,Mugshot,"Margerand. Claude. 32 ans, né le 24/3/61 à Beaujeu (Rhône). Cordonnier. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.270,false,true,306890,Photographs,Mugshot,"Marie. Constant. 53 ans, né le 27/8/38 à Ste-Houvrince (Calvados). Garçon rnarçon. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.271,false,true,306891,Photographs,Mugshot,"Marie. Léon, Louis. 23 ans, né le 19/8/70 à Adouzeval (Calvados). Couvreur. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.272,false,true,306892,Photographs,Mugshot,"Martin. Constant. 53 ans, né le 5/4/39 à Santrevaux (Basses-Alpes). Crémier. Anarchiste. 27/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.273,false,true,306893,Photographs,Mugshot,"Martin. Pierre. 22 ans, né à St-Léger (Saône-et-Loire). Employé de commerce. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.274,false,true,306894,Photographs,Mugshot,"Martineau. Jules, Louis. 30 ans, né à Angers (Maine & Loire). Peintre en bâtiment. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.275,false,true,306895,Photographs,Mugshot,"Marty. Louis. 20 ans, né à Lacanne (Tarn). Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.276,false,true,306896,Photographs,Mugshot,"Masini (dit Mazzini). Angelo, Henri. 25 ans, né le 4/9/69 à Milan (Italie). Ébéniste. Anarchiste. 1/9/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.277,false,true,306897,Photographs,Mugshot,Matha.,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1880s–90s,1880,1899,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.278,false,true,306898,Photographs,Mugshot,Matha.,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1880s–90s,1880,1899,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.279,false,true,306899,Photographs,Carte-de-visite; Mugshot,Mathieu. Gustave.,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1880s–90s,1880,1899,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.280,false,true,306900,Photographs,Carte-de-visite; Mugshot,Mathieu. Gustave. (avec une barbe postiche),,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1880s–90s,1880,1899,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.281,false,true,306901,Photographs,Mugshot,"Mathon. Louis Marius. 30 ans, né à St Andiolle (Ardèche). Ferblantier. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.282,false,true,306902,Photographs,Mugshot,"Maurin. Émile, Auguste. 31 ans, né à Marseille (Bouche du Rhône). Ex photographe. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.283,false,true,306903,Photographs,Mugshot,"Mauroy. Alfred, Édouard. 34 ans, né à Paris VIIe. Dessinateur. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.284,false,true,306904,Photographs,Mugshot,"Mayence. Gustave, David. 33 ans, né le 29/5/60 à Paris XVllle. Tapissier. Anarchiste. 17/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.285,false,true,306905,Photographs,Mugshot,"Mazoldi. Frédéric, Jean-Baptiste. 54 ans, né à Bicroz (Autriche). Ferblantier. Anarchiste. 23/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.286,false,true,306906,Photographs,Mugshot,"Mentenich. François, Joseph. 22 ans, né le 3/10/71 à Paris XIIe. Ébéniste. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.287,false,true,306907,Photographs,Mugshot,"Mereaux. Émile-Louis. 33 ans, né à Laon (Aisne). Ébéniste. Anarchiste. 23/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.288,false,true,306908,Photographs,Mugshot,"Mérigeau. Jacques. 35 ans, né à St-Léger-les-Melles (Deux Sèvres). Ébéniste. 19/12/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.289,false,true,306909,Photographs,Mugshot,"Mermin. Camille. 33 ans, né à La Havane (Cn Espagnoles). Rep. de cou. (?). Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.290,false,true,306910,Photographs,Mugshot,"Miaglia. Bernard. 41 ans, né à Giaglione (Italie). Cordonnier. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.291,false,true,306911,Photographs,Mugshot,"Miel. Eugène, Paul, Léon. 38 ans, né à Creil (Oise). Estampeur. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.292,false,true,306912,Photographs,Mugshot,"Millard. Victor. 53 ans, né le 5/6/40 à Moyon (Oise). Cordonnier. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.293,false,true,306913,Photographs,Mugshot,"Mocquet. Georges, Gustave. 17 ans, né le 17/5/76 à Paris IXe. Tapissier. Anarchiste. 6/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.294,false,true,306914,Photographs,Mugshot,"Molmerret. Joseph, Camille. 28 ans, né le 20/11/65 à Lyon (Rhône). Graveur. Anarchiste. 26/5/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.295,false,true,306915,Photographs,Mugshot,"Morane. Antoine. 35 ans, né à Chalinargue (Cantal). Manœvre. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.296,false,true,306916,Photographs,Mugshot,"Moreau. François. 47 ans, né le 19/11/46 à Nevers (Nièvre). Menuisier: Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.297,false,true,306917,Photographs,Mugshot,"Moreau. Louis. 40 ans, né le 22/10/53 à Villiers (Nièvre). Tailleur de pierre. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.298,false,true,306918,Photographs,Mugshot,"Morel. Benoit. 33 ans, né le 6/11/61 à St Laurent d'Orringt (Rhône). Ébéniste. Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.299,false,true,306919,Photographs,Mugshot,"Morvan. Félicien. 45 ans, né le 8/6/49 à Kerity (Côte du Nord). Menuisier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.300,false,true,306920,Photographs,Mugshot,"Moucheraud. Adrien, Eugène. 28 ans, né à Paris IVe. Imprimeur. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.301,false,true,306921,Photographs,Mugshot,"Moucheraud. Pierre, Yves. 27 ans, né à Paris IVe. Imprimeur. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.302,false,true,306922,Photographs,Mugshot,"Mouette. Charles. 32 ans, né à Paris Ile. Peintre en bâtiment. Association de malfaiteurs. 20/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.303,false,true,306923,Photographs,Mugshot,"Monzon. Lucien, Henri, Baptiste. 18 ans, né à Paris XXe. Couvreur. Anarchiste. 23/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.304,false,true,306924,Photographs,Mugshot,Mursch. Eugène. 24 ans à Schlestatt (Bas-Rhin). Ciseleur. Anarchiste. 18/3/94.,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.305,false,true,306925,Photographs,Mugshot,"Naudet. Gervais. 40 ans, né à Echaleau (Côte-d'Or). Menuisier. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.306,false,true,306926,Photographs,Mugshot,"Nic. Celestin. 20 ans, né à Conflans-St-Honorine (Seine & Oise). Emballeur. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.307,false,true,306927,Photographs,Mugshot,"Notelez. Charles, Émile. 29 ans, né à Paris XXe. Portefeuilliste. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.308,false,true,306928,Photographs,Mugshot,"308. Novi. Ernest, Théodore. 32 ans, né à Nice (Alpes-Maritimes). Architecte. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.309,false,true,306929,Photographs,Mugshot,"309. Ochart. Alphonse. 37 ans, né le 24/1/56 à Asbruch (Nord). Fabricant de chaussures. Anarchiste. 22/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.310,false,true,306930,Photographs,Mugshot,"310. Olivier. Philippe, Octave. 25 ans, né le 29/6/68 à Paris XVIIle. Plombier. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.311,false,true,306931,Photographs,Mugshot,"Ortiz. Léon. 25 ans, né à Paris. Commis d'architecte. Anarchiste. Voyage ordinairement en bicyclette.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.312,false,true,306932,Photographs,Mugshot,"Ortiz. Léon. 25 ans, né à Paris. Commis d'architecte. Anarchiste. Voyage ordinairement en bicyclette.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.313,false,true,306933,Photographs,Mugshot,"Ortiz. Léon. 25 ans, né à Paris. Commis d'architecte. Anarchiste. Voyage ordinairement en bicyclette.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.314,false,true,306934,Photographs,Mugshot,"Oudin. Clovis. 49 ans, né à Saint-Hilaire (Marne). Mécanicien. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.315,false,true,306935,Photographs,Mugshot,"Paget. Jean, Louis. 41 ans, né le 15/7/52 à Thonon (Haute-Savoie). Cordonnier. Anarchiste. 12/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.316,false,true,306936,Photographs,Mugshot,"Pallaz (ou Pellaz). Péronne. 28 ans, née le 11/8/66 à Aix-les-Bains (Savoie). Cuisière. Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.317,false,true,306937,Photographs,Mugshot,"Para (ou Parra). Henri. 38 ans, né le 16/5/56 à Paris Ve. Camelot. Anarchiste. 4/9/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.318,false,true,306938,Photographs,Mugshot,"Parisis. Charles. 20 ans, né à Aubervilliers (Seine). Tailleur d'habits. Outrages anarchistes. 10/7/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.319,false,true,306939,Photographs,Mugshot,"Parisot. Louis. 37 ans, né à Saint-Avalet (Moselle). Employé au ""Petit Journal"". Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.320,false,true,306940,Photographs,Mugshot,"Pausader. Jean, Ernest. 27 ans, né le 28/7/66 à Paris Xe. Publiciste. Anarchiste. 2/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.321,false,true,306941,Photographs,Mugshot,"Pelgrom. Elise (femme Schouppe). Deux photographies, dont une légendée: Reprod. faite 22/2/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.322,false,true,306942,Photographs,Mugshot,"Pelgrom. Elise (femme Schouppe). Deux photographies, dont une légendée: Reprod. faite 22/2/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.323,false,true,306943,Photographs,Mugshot,"Pemjean. Lucien, Pierre. 32 ans, né à Lyon (Rhône). Publiciste. Anarchiste. 2/1/93.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.324,false,true,306944,Photographs,Mugshot,"Pennelier. Casimir. Arthur. 36 ans, né à Billeuse (Somme). Clerc d'huissier. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.325,false,true,306945,Photographs,Mugshot,"Percheron. Auguste. 56 ans, né à Poitier (Nièvre). Écrivain public. Anarchiste. 21/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.326,false,true,306946,Photographs,Mugshot,"Pernin. François. 34 ans, né le 11/1/60 au Creuzot (Saône-et-Loire). Forgeron. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.327,false,true,306947,Photographs,Mugshot,"Perot. Gaston, Auguste. 22 ans, né à Paris XVllle. Journalier. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.328,false,true,306948,Photographs,Mugshot,"Perrare. Antoine. 53 ans, né à St Diaur (Rhône). Mécanicien. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.329,false,true,306949,Photographs,Mugshot,"Perrier (dit Theriez). Louis. 35 ans, né le 25/8/58 à Paris Vllle. Ébéniste. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.330,false,true,306950,Photographs,Mugshot,"Perron. Jules. 31 ans, né le 9/8/62 à Saint-Denis (Seine). Journalier. Anarchiste. 75/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.331,false,true,306951,Photographs,Mugshot,"Perrot. Jean. 33 ans, né le 18/11/61 à Tulle (Corrèze). Cordonnier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.332,false,true,306952,Photographs,Mugshot,"Peticolin. Henri. 23 ans, né le 8/6/71 à Goersdorf (Bas-Rhin). Vernisseur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.333,false,true,306953,Photographs,Mugshot,"Pierlay. Louis, Victor. 53 ans, né à Paris XVIle. Sculpteur. Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.334,false,true,306954,Photographs,Mugshot,"Pierre. Joseph, Adrien. 42 ans, né à Rouen (Seine-Inférieure). Canneleur de chaises. Anarchiste. 12/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.335,false,true,306955,Photographs,Mugshot,"Pichon. Ernest. 41 ans, né le 2/11/52 à Villard-Rixoire (Jura). Terrassier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.336,false,true,306956,Photographs,Mugshot,"Pidoux. Jean. Victor. 45 ans, né au Châtelet (Seine-et-Marne). Estampeur de métaux. Anarchiste 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.337,false,true,306957,Photographs,Mugshot,"Pioger. Louise (veuve Lefèvre). 45 ans, né à Mézières (Sarthe). Giletière. Disposition du préfet. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.338,false,true,306958,Photographs,Mugshot,"Pivat. Georges, Léopold. 34 ans, né à Vauxcié (Aisne). Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.339,false,true,306959,Photographs,Mugshot,"Pivier. Alexandre. 53 ans, né à Rochevan (Savoie). Tailleur d'habits. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.340,false,true,306960,Photographs,Mugshot,"Poirier. Jacques, Étienne. 30 ans, né à Gien (Loiret). Garçon de marchand de vins. Anarchiste. 30/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.341,false,true,306961,Photographs,Mugshot,"Poisson. Georges. 38 ans, né à Boulogne (Seine). Chaudronnier. Anarchiste. 6/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.342,false,true,306962,Photographs,Mugshot,"Ponchia. Charles, Albino. 32 ans, né le 1/3/62 à Montanaro (Italie). Menuisier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.343,false,true,306963,Photographs,Mugshot,"Pouget. Émile, Jean, Joseph. 31 ans, né le 12/10/60 à Rodez (Aveyron). Publiciste. Anarchiste. 26/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.344,false,true,306964,Photographs,Mugshot,"Pourry. François, Nicolas. 58 ans, né à Ars-sur-Moselle (Alsace-Lorraine). Ajusteur. Anarchiste. 3/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.345,false,true,306965,Photographs,Mugshot,"Raboin. Émile, Pierre. 41 ans, né à Ouzoir-sur-Loire (Loiret). Distillateur. Assoc. de malfaiteurs. 28/2/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.346,false,true,306966,Photographs,Mugshot,"Raboin. Paul, Pierre, Augustin. 32 ans, né à Ouzoir-sur-Loire (Loiret). Journalier. Anarchiste. 23/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.347,false,true,306967,Photographs,Mugshot,Rampin. Pierre. 3/7/94,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.348,false,true,306968,Photographs,Mugshot,"Ravachol. François Claudius Kœnigstein. 33 ans, né à St-Chamond (Loire). Condamné le 27/4/92.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1892,1892,1892,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.349,false,true,306969,Photographs,Mugshot,"Ravinet. Gaston. 34 ans, né à Paris XIXe. Couvreur. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.350,false,true,306970,Photographs,Mugshot,Reclus. Paul. Pas d'informations sur la fiche. 23/12/93,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.351,false,true,306971,Photographs,Mugshot,"Recco. Grégoire. 35 ans, né à Formia (Italie). Tailleur d'habits. Anarchiste. 11/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.352,false,true,306972,Photographs,Mugshot,"Remond. Émile, Adolphe. 34 ans, né à Bagnolet (Seine). Carrier. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.353,false,true,306973,Photographs,Mugshot,"Renard. Pierre, Alfred. 46 ans, né à Flain (Haute-Saône) le 27/4/46.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1880s–90s,1880,1899,Gelatin silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.354,false,true,306974,Photographs,Mugshot,"Renaud. Jules. 41 ans, né à Anteuil (Doubs). Cordonnier. Anarchiste. 27/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.355,false,true,306975,Photographs,Mugshot,"Retté. Adolphe. 30 ans, né à Paris IXe. Homme de lettre. Cris séditieux. 21/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.356,false,true,306976,Photographs,Mugshot,"Rey. Claude. 23 ans, né le 24/5/70 au Creusot (Saone & Loire). Ébéniste. Anarchiste. 12/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.357,false,true,306977,Photographs,Mugshot,"Reytinat. Jacques, François. 48 ans, né à Mouy (Oise). Colporteur. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.358,false,true,306978,Photographs,Mugshot,"Ricois. Charles-Victor. 48 ans, né le 21/3/46 à Orléans. Employé au Journal Officiel. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.359,false,true,306979,Photographs,Mugshot,"Ridou. Paul, François. 27 (ou 28) ans. Ébéniste. Anarchiste. 8/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.360,false,true,306980,Photographs,Mugshot,"Rigollet. Alexandre. 41 ans, né dans le Loir-et-Cher. Terrassier. Anarchiste.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.361,false,true,306981,Photographs,Mugshot,"Ripert. Thomas. 33 ans, né à Marseille. Cocher. Anarchiste. 5/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.362,false,true,306982,Photographs,Mugshot,"Robert. Fritz, Malatesta. 24 ans, Suisse. Excitation à la haine des citoyens les uns contre les autres, expulsé.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1880s–90s,1880,1899,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.363,false,true,306983,Photographs,Mugshot,"Robillard. Guillaume, Joseph. 24 ans, né le 17/11/68 à Vaucresson. Fondeur en cuivre. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.364,false,true,306984,Photographs,Mugshot,"Robyns. Émile. 36 ans, né à Lumone (Belgique). Marchand de Pierres (?). Anarchiste. 28/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.365,false,true,306985,Photographs,Mugshot,"Rochet. Théophile. 24 ans, né à Rennes (Ille-et-Vilaine) le 7/6/69. Cordonnier. Anarchiste. 18/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.366,false,true,306986,Photographs,Mugshot,"Rodskidski. Eloi, Jean-Baptiste. 37 ans, né à Paris Xlle 13/12/56. Mécanicien. Anarchiste. 2/7/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.367,false,true,306987,Photographs,Mugshot,"Roobin. Joseph. 40 ans, né à Bourgneuf (Loire-Inférieure). Terrassier. Anarchiste. 2/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.368,false,true,306988,Photographs,Mugshot,"Rossi. Guillaume. 20 ans, né le 8/3/71 à Biel (Italie). Serrurier. Vagabondage. 15/1/95.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1895,1895,1895,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.369,false,true,306989,Photographs,Mugshot,Roubichon. Jean-Marie. né le 14/6/52 à Vannes (Morbihan). Maçon. Anarchiste. 2/7/94.,,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.370,false,true,306990,Photographs,Mugshot,"Rouif. Léon. 27 ans, né à Villethierry (Yonne). Garçon boucher. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.371,false,true,306991,Photographs,Mugshot,"Roussel. Henri-Louis. 28 ans, né à Paris le 2/10/65. Ardoiseur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.372,false,true,306992,Photographs,Mugshot,"Ruaud. Jean-Baptiste. 35 ans, né à Limoges le 28/7/58. Cordonnier. Anarchiste. 4/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.373,false,true,306993,Photographs,Mugshot,"Sachet. Edmond. 27 ans, né à Mézières (Ardennes). Typographe. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.374,false,true,306994,Photographs,Mugshot,"Savard. Henri-Auguste. 29 ans, né le 7/5/65 à Paris XXe. Ciseleur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.375,false,true,306995,Photographs,Mugshot,"Saulnier. Alphonse, Joseph. 31 ans, né à Paris XXe. Tourneur sur bois. Anarchiste. 14/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.376,false,true,306996,Photographs,Mugshot,"Schaeffer. Ignace. 42 ans, né à Berheim (Bas-Rhin) le 31/10/51. Ébéniste. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.377,false,true,306997,Photographs,Mugshot,"Schaffer. Charles. 28 ans, né à Paris Xle. Ébéniste. Association de malfaiteurs. 2/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.378,false,true,306998,Photographs,Carte-de-visite; Mugshot,"Schouppe. Placide. (dit Ricken, Franz).",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1889–94,1889,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.379,false,true,306999,Photographs,Mugshot,"Schouppe. Placide. (dit Ricken, Franz). 31 ans, né à Dickenvenne (Belgique). Mécanicien. Vol.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1889,1889,1889,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.380,false,true,307000,Photographs,Mugshot,"Schouppe. Placide. (dit Ricken, Franz). 35 ans, né à Dickenvenne (Belgique). Mécanicien. Vol.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1893,1893,1893,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.381,false,true,307001,Photographs,Mugshot,"Schrader. Minna, Appoline. 19 ans, née à Paris XIe. Sculpteur. Association de malfaiteurs. 24/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.382,false,true,307002,Photographs,Mugshot,"Schulé. Armand. 21 ans, né le 28/2/73 à Choisy-le-Roi. Comptable. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.383,false,true,307003,Photographs,Mugshot,"Schwartz. Auguste. 31 ans, né à Paris Xlle le 23/1/63. Maroquinier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.384,false,true,307004,Photographs,Mugshot,"Segard. Émilien (dit Segard Fils). 18 ans, né à Saloüel (Somme). Peintre en voitures. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.385,false,true,307005,Photographs,Mugshot,"Segard. Philogone. 44 ans (35 ans inscrit sur la photo), né à Salond (Somme). Journaliste. Anarchiste.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1891–95,1891,1895,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.386,false,true,307006,Photographs,Mugshot,"Selle. Louis- Désiré-Honoré. 31 ans, né à Bougival. Cordonnier. Anarchiste. 26/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.387,false,true,307007,Photographs,Mugshot,"Sentenac. Phillipe. 36 ans, né à Soulan (Ariège). Menuisier. Anarchiste. 7/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.388,false,true,307008,Photographs,Mugshot,"Serre. Auguste. 37 ans, né à Anonnay (Ardêche) le 13/10/56. Mégissier. Anarchiste. 20/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.389,false,true,307009,Photographs,Mugshot,"Sicard. André. 32 ans, né à Nîmes le 25/10/62. Bijoutier. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.390,false,true,307010,Photographs,Mugshot,"Sigel. Jacques. 24 ans, né à Kuttalsheim (Bas Rhin). Bijoutier. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.391,false,true,307011,Photographs,Mugshot,"Simonin. Joseph. 26 ans, né à Saint-Maurice (Seine). Gainier. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.392,false,true,307012,Photographs,Mugshot,"Soubrié. François. 39 ans, né à Livignac-le-Haut (Aveyron). Brûleur de café. Anarchiste. 14/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.393,false,true,307013,Photographs,Mugshot,"Soubrier. Annette (femme Chericotti). 28 ans, née à Paris Ille. Coutière. Anarchiste. 25/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.394,false,true,307014,Photographs,Mugshot,"Solier. Auguste. 18 ans, né le 3/3/75 à Cemery-la-Ville. Dessinateur. Anarchiste. 12/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.395,false,true,307015,Photographs,Mugshot,"Sost. Edmond. 30 ans, né à Serqueux (Seine-Inférieure) le 15/11/64. Ciseleur. Anarchiste. 1/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.396,false,true,307016,Photographs,Mugshot,"Soulage. Alphonse, Charles. 30 ans, né à Lyon. Menuisier. Anarchiste. 1/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.397,false,true,307017,Photographs,Mugshot,"Soulas. Honoré, Jules. 33 ans, né à Chatillon le 10/12/55. Peintre en bâtiment. Anarchiste. 27/5/89.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1889,1889,1889,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.398,false,true,307018,Photographs,Mugshot,"Spanagel. Alfred, Vincent. 17 ans, né le 27/5/77 à Paris. Serrurier. Anarchiste. 7/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.399,false,true,307019,Photographs,Mugshot,"Spanagel. Emile, Ignace. 20 ans, né le 28/2/74 à Paris XVlle. Serrurier. Anarchiste. 7/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.400,false,true,307020,Photographs,Mugshot,"Springer. François. 21 ans, né le 17/9/72 à Duisburg (Allemagne). Menuisier. Anarchiste. 17/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.401,false,true,307021,Photographs,Mugshot,"Surgand. Alphonse. 21 ans, né à Lyon. Réparateur de chaussures. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.402,false,true,307022,Photographs,Mugshot,"Tardieu. Marius (ou Maurice). 26 ans, né le 15/7/68 à Piolène (Vaucluse). Ébéniste. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.403,false,true,307023,Photographs,Mugshot,"Tennevin. Alexandre. 48 ans, né à Paris. Comptable. Anarchiste. 19/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.404,false,true,307024,Photographs,Mugshot,"Terrier. Julien, François. 45 ans, né à Saint-Laurent (Mayenne). Menuisier. Anarchiste. 14/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.405,false,true,307025,Photographs,Mugshot,"Theriez. Louis (ou Perriez). 35 ans, né le 29/8/58 à Paris. Ébéniste. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.406,false,true,307026,Photographs,Mugshot,"Theuriet. Jean Baptiste. 30 ans, né à Lyon. Gérant-coiffeur. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.407,false,true,307027,Photographs,Mugshot,"Thibivilliers. Eugène. 23 ans, né à Pinseux-le-Haut-Verger (Oise). Polisseur de métaux. Cris séditieux. 5/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.408,false,true,307028,Photographs,Mugshot,"Thiebaut. Eugène. 35 (ou 36) ans, né à Château-Salin (Menthe & Moselle). Couvreur. Anarchiste. 3/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.409,false,true,307029,Photographs,Mugshot,"Thirion. Louis, Joseph. 31 ans, né à Autrey (Vosges). Journaliste. Anarchiste. 4/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.410,false,true,307030,Photographs,Mugshot,"Tiran. Arthur, Théodore. 29 ans, né à Briec (Finistère). Serrurier. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.411,false,true,307031,Photographs,Mugshot,"Toesca. Calixte. 28 ans, Tour de Breuil (Alpes-Mar.). Étudiant en médecine. Association de malfaiteurs. 27/2/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.412,false,true,307032,Photographs,Mugshot,"Toulet. Guy, Flavien. 41 ans, né à Boufler (Somme). Entrepreneur de peinture. Anarchiste. 23/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.413,false,true,307033,Photographs,Mugshot,"Tournadre. Jacques (ou Eugène). 32 ans, né à Marchal (Cantal). Journaliste. Anarchiste. 3/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.414,false,true,307034,Photographs,Mugshot,"Tournan. Pierre. 49 ans, né à Bouzouville (Moselle). Fabricant de couronnes. Anarchiste. 6/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.415,false,true,307035,Photographs,Mugshot,"Tramcourt. Albert. 27 ans, né le 10/12/66 à Creil. Mécanicien. Anarchiste. 15/1/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.416,false,true,307036,Photographs,Mugshot,"Tropini. Esprit, Antoine. 35 ans, né à San-Bueri (Italie). Tourneur. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.417,false,true,307037,Photographs,Mugshot,"Trucano. Victorine (veuve Belloti). 54 ans, né à St Maurier (Italie). Chapelier. Vol anarchiste. 19/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.418,false,true,307038,Photographs,Mugshot,"Vaury. Charles, Joseph. 43 (ou 44) ans, né le 31/3/59 à Sedan. Mécanicien. Anarchiste. 16/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.419,false,true,307039,Photographs,Mugshot,"Vendel. Jules. 34 (ou 33) ans, né le 2/4/61 à Chevry (Ain). Garçon de cuisine. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.420,false,true,307040,Photographs,Mugshot,"Véret. 0ctave-Jean. 19 ans, né à Paris XXe. Photographe. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.421,false,true,307041,Photographs,Mugshot,"Veysseire. Michel. 25 ans, né à Montreuil. Journaliste. Anarchiste. 5/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.422,false,true,307042,Photographs,Mugshot,"Vidal. Guillaume. 37 ans, né à Tellière (Puy de Dôme). Cocher. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.423,false,true,307043,Photographs,Mugshot,"Vignaud. Antoine. 32 ans, né à Cussey (Allier). Cordonnier. Vol anarchiste. 21/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.424,false,true,307044,Photographs,Mugshot,"Villa. Jean. 29 ans, né à Farini d'Olma (Italie). Manoeuvre. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.425,false,true,307045,Photographs,Mugshot,"Villanneau. Henri, Fernand. 35 ans, né le 11/3/59 à Poitiers. Clerc de notaire. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.426,false,true,307046,Photographs,Mugshot,"Villetard. Jules, Pierre. 42 ans, né à Ligny-le-Chastel (Yonne). Terrassier. Anarchiste. 5/3/94",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.427,false,true,307047,Photographs,Mugshot,"Vuagniaux. Alfred. 41 ans, né à Vucheron (Suisse). Cordonnier. Anarchiste. 2/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.428,false,true,307048,Photographs,Mugshot,"Wagner. Paul, Louis. 38 ans, né le 14/10/55. Ébéniste. Anarchiste. 2/7/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.429,false,true,307049,Photographs,Mugshot,"Wallays. Charles. 29 ans, né à Lille. Tailleur d'habits. Anarchiste. 9/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.430,false,true,307050,Photographs,Mugshot,"Widcoq. Alfred, François, Adolphe. 32 ans, né à Fressenneville (Somme). Mécanicien. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.431,false,true,307051,Photographs,Mugshot,"Widcoq. Fulgence, Ignace. 36 ans, né à Fressenneville (Somme). Mécanicien. Anarchiste. 10/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.432,false,true,307052,Photographs,Mugshot,"Willems. Charles, Louis. 52 ans, né à Houndchocte (Nord). Tailleur d'habits. Anarchiste. 18/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.433,false,true,307053,Photographs,Mugshot,"Zanini. Marie (veuve Milanaccio). 28 ans, née à Turin (Italie). Cuisinière. Vol. 18/3/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.434,false,true,307054,Photographs,Mugshot,"Zisly. Henri, Gabriel. 21 ans, né à Paris IVe. Employé de commerce. Anarchiste. 26/2/94.",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",,1853,1914,1894,1894,1894,Albumen silver print from glass negative,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.51.1,false,true,263192,Photographs,Photograph,Pekin. No. 923,,,,,,Artist,Attributed to,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",,1839,1890,1867,1867,1867,Albumen silver print from glass negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.13,false,true,289285,Photographs,Photograph,"A Portion of the Citywall, Foochow",,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/8 × 11 1/2 in. (20.7 × 29.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.29,false,true,289301,Photographs,Photograph,Left Road up to Yuen foo Monastery,,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 7/8 × 11 5/16 in. (22.6 × 28.7 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.31,false,true,289303,Photographs,Photograph,Temple of Tai-wang at Wu ü near Sing Chang Tea Mart,,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/16 × 11 1/4 in. (20.5 × 28.6 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.35,false,true,289307,Photographs,Photograph,"The Grand Stand, Foochow",,,,,,Artist,,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 7 15/16 × 11 5/16 in. (20.2 × 28.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.38,false,true,289310,Photographs,Photograph,Culling Tea,,,,,,Artist,Attributed to,Lai Fong,"Chinese, 1839–1890",,"Lai, Fong",,1839,1890,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 6 15/16 × 9 3/8 in. (17.6 × 23.8 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.423.1,false,true,294322,Photographs,Postcard,"Astapovo Train Station, On the Right is the House in Which Lev Nikolayevich (Tolstoy) Died",,,,,,Artist,,Aleksey Ivanovich Saveliev,"Russian, 1883–1923",,"Saveliev, Aleksey Ivanovich",,1883,1923,1910,1910,1910,Gelatin silver print,Image: 8.9 x 13.3 cm (3 1/2 x 5 1/4 in.),"Gift of Pierre Apraxine, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.423.2,false,true,294323,Photographs,Postcard,The Lowering of the Coffin into the Grave with Kneeling Mourners,,,,,,Artist,,Aleksey Ivanovich Saveliev,"Russian, 1883–1923",,"Saveliev, Aleksey Ivanovich",,1883,1923,"November 9, 1910",1910,1910,Gelatin silver print,Image: 8.9 x 13.3 cm (3 1/2 x 5 1/4 in.),"Gift of Pierre Apraxine, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.423.3,false,true,294324,Photographs,Postcard,Deputation of the Yasno-Polyanskyi Peasants,,,,,,Artist,,Aleksey Ivanovich Saveliev,"Russian, 1883–1923",,"Saveliev, Aleksey Ivanovich",,1883,1923,1910,1910,1910,Gelatin silver print,Image: 8.9 x 13.3 cm (3 1/2 x 5 1/4 in.),"Gift of Pierre Apraxine, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.423.4,false,true,294325,Photographs,Postcard,"En Route to the House, Visible in the Distance is the Village of Yasnaya Polyana",,,,,,Artist,,Aleksey Ivanovich Saveliev,"Russian, 1883–1923",,"Saveliev, Aleksey Ivanovich",,1883,1923,1910,1910,1910,Gelatin silver print,Image: 8.9 x 13.3 cm (3 1/2 x 5 1/4 in.),"Gift of Pierre Apraxine, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.423.5,false,true,294326,Photographs,Postcard,At the Prepared Grave,,,,,,Artist,,Aleksey Ivanovich Saveliev,"Russian, 1883–1923",,"Saveliev, Aleksey Ivanovich",,1883,1923,1910,1910,1910,Gelatin silver print,Image: 8.9 x 13.3 cm (3 1/2 x 5 1/4 in.),"Gift of Pierre Apraxine, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.423.6,false,true,294327,Photographs,Postcard,Peasant Carts with Funeral Wreaths,,,,,,Artist,,Aleksey Ivanovich Saveliev,"Russian, 1883–1923",,"Saveliev, Aleksey Ivanovich",,1883,1923,1910,1910,1910,Gelatin silver print,Image: 8.9 x 13.3 cm (3 1/2 x 5 1/4 in.),"Gift of Pierre Apraxine, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1033,false,true,711640,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1034,false,true,711641,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1035,false,true,711642,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1036,false,true,711643,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1037,false,true,711644,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1038,false,true,711645,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1039,false,true,711646,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1040,false,true,711647,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1041,false,true,711648,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1042,false,true,711649,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1043,false,true,711650,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1044,false,true,711651,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1045,false,true,711652,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1046,false,true,711653,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1047,false,true,711654,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1048,false,true,711655,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1049,false,true,711656,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1050,false,true,711657,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1054,false,true,711661,Photographs,Photograph,[Civil War View],,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",", et al","Roche, Thomas C.",,1826,1895,1860s,1860,1869,Albumen silver print from glass negative,,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/711661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(1),false,true,732748,Photographs,Photograph,Old Dominium and Uncle Tom's Tavern. Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(2),false,true,732749,Photographs,Photograph,The Sentinels - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(3),false,true,732750,Photographs,Photograph,The Mother of the Forest - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(4),false,true,732751,Photographs,Photograph,The Miners Cabin - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(5),false,true,732752,Photographs,Photograph,The Mother of the Forest From the Father of the Forest - Calavaras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(6),false,true,732753,Photographs,Photograph,The Mammoth Grove Hotel from the Grove - Calaveras,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(7),false,true,732754,Photographs,Photograph,Looking up Among the Sugar Pines - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(8),false,true,732820,Photographs,Photograph,Pioneers Cabin - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(9),false,true,732819,Photographs,Photograph,The Three Graces Seen Through the Bryant and Seward Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(10),false,true,732818,Photographs,Photograph,James King of William. Keyston State etc. Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(11),false,true,732817,Photographs,Photograph,The Pride of the Forest - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(12),false,true,732816,Photographs,Photograph,Eagle's Wing - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(13),false,true,732815,Photographs,Photograph,The Father of the Forest - The Horse Back Side. Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(14),false,true,732814,Photographs,Photograph,Mammoth Three Grove Hotel Calaveras Co Cal.,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(15),false,true,732813,Photographs,Photograph,None,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(16),false,true,732812,Photographs,Photograph,W. C. Bryant - Calaveras Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(17),false,true,732811,Photographs,Photograph,The Father of the Forest - C. Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(18),false,true,732810,Photographs,Photograph,The Empire State - C. Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(19),false,true,732809,Photographs,Photograph,Auld Reckie - C. Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(20),false,true,732808,Photographs,Photograph,Empire State. C. Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(21),false,true,732807,Photographs,Photograph,The Father of the Forest 450 ft C. Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(22),false,true,732806,Photographs,Photograph,Auld Reckie. C. Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(23),false,true,732805,Photographs,Photograph,"Interior of Pavilion Built on the Stump of the Tree, C. Grove",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(24),false,true,732804,Photographs,Photograph,The Sentinels. C. Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(25),false,true,732803,Photographs,Photograph,Warm Springs Hotel Lake Tahoe,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(26),false,true,732802,Photographs,Photograph,"The Father of the Forest 450 Ft Long, C. Grove",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(27),false,true,732801,Photographs,Photograph,Donner Lake. C. P. R. R,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(28),false,true,732800,Photographs,Photograph,In the Yosemite Valley,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(29),false,true,732799,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(30),false,true,732798,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(31),false,true,732797,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(32),false,true,732796,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(33),false,true,732795,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(34),false,true,732794,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(35),false,true,732793,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(36),false,true,732792,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(37),false,true,732791,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(38),false,true,732790,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(39),false,true,732789,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(40),false,true,732788,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(41),false,true,732787,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(42),false,true,732786,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(43),false,true,732785,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(44),false,true,732784,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(45),false,true,732783,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(46),false,true,732782,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(47),false,true,732781,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(48),false,true,732780,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(49),false,true,732779,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(50),false,true,732778,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(51),false,true,732777,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(52),false,true,732776,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(53),false,true,732775,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(54),false,true,732774,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(55),false,true,732773,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(56),false,true,732772,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(57),false,true,732771,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(58),false,true,732770,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(59),false,true,732769,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(60),false,true,732768,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(61),false,true,732767,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(62),false,true,732766,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(63),false,true,732765,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(64),false,true,732764,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(65),false,true,732763,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(66),false,true,732762,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(67),false,true,732761,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(68),false,true,732760,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(69),false,true,732759,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(70),false,true,732758,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(71),false,true,732757,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(72),false,true,732756,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556(73),false,true,732755,Photographs,Photograph,"[Yosemite National Park, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",,1829,1916,ca. 1878,1876,1880,Albumen silver print from glass negative,"Image: 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.), circular Album page: 24 x 25.1 cm (9 7/16 x 9 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/732755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.261.2,false,true,680016,Photographs,Boudoir Card; Cabinet Card,"[Self-Portrait at Glacier Bay, Alaska]",,,,,,Artist,,Frank Jay Haynes,"American, 1853–1921",,"Haynes, F. Jay",,1853,1921,1889–91,1889,1891,Albumen silver print from glass negative,Image: 8 1/8 × 5 1/16 in. (20.7 × 12.8 cm) Mount: 8 7/16 × 5 1/4 in. (21.5 × 13.4 cm),"Gift of Paul M. Hertzmann Inc., 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/680016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.778.29,false,true,700105,Photographs,Photograph,Old Time Freight Brakeman – New York Central,,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",,1874,1940,1921,1921,1921,Gelatin silver print,Image: 6 11/16 × 4 11/16 in. (17 × 11.9 cm) Sheet: 6 15/16 in. × 5 in. (17.7 × 12.7 cm),"Gift of Joyce F. Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/700105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.777.2,false,true,705430,Photographs,Photograph,"Thèbes, Temple de Ramasseum, Colosses brisés",,,,,,Artist,,Émile Béchard,"French, active 1860s–1880s",,"Béchard, Émile",,1859,1899,1870s,1870,1879,Albumen silver print,Image: 10 9/16 × 14 7/8 in. (26.8 × 37.8 cm) Mount: 13 7/8 × 18 1/16 in. (35.2 × 45.9 cm),"Gift of Charles Isaacs and Carol Nigro, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/705430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.48,false,true,291801,Photographs,Daguerreotype,Elizabeth Michael Howell,,,,,,Artist,,Addis's Lancaster Gallery,"American, active 1840s–1860s",,Addis's Lancaster Gallery,,1840,1869,1855–59,1855,1859,Daguerreotype,Image: 8.9 x 6.5 cm (3 1/2 x 2 9/16 in.) Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.) Case: 1.6 x 11.9 x 9.4 cm (5/8 x 4 11/16 x 3 11/16 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.1.114,false,true,704975,Photographs,Photograph; Carte-de-visite,Eugénie Schlosser et Coralli,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",,1819,1889,1863,1863,1863,Albumen silver print from glass negative,Image: 7 3/8 × 9 1/4 in. (18.8 × 23.5 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/704975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.1.115,false,true,704976,Photographs,Photograph; Carte-de-visite,Eugénie Schlosser et Coralli,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",,1819,1889,1863,1863,1863,Albumen silver print from glass negative,Image: 7 3/8 × 9 1/4 in. (18.8 × 23.5 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/704976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.588.1.116a, b",false,true,704977,Photographs,Photograph; Carte-de-visite,Eugénie Schlosser et Coralli,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",,1819,1889,1863,1863,1863,Albumen silver print from glass negative,Image: 7 3/8 × 9 1/4 in. (18.8 × 23.5 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/704977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.424,false,true,684347,Photographs,Photograph,[Spread from an Ornithological Book],,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",,1857-02-02,1927-08-04,ca. 1910,1905,1915,Matte albumen silver print from glass negative,"Image: 6 7/8 × 8 15/16 in. (17.4 × 22.7 cm) Sheet: 7 1/16 × 8 15/16 in. (17.9 × 22.7 cm), irregularly trimmed","The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/684347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.778.31,false,true,700107,Photographs,Photograph,"Hôtel des Ambassadeurs de Hollande, 47 rue Vieille du Temple",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",,1857-02-02,1927-08-04,1898,1898,1898,Albumen silver print,Image: 8 1/2 in. × 7 in. (21.6 × 17.8 cm),"Gift of Joyce F. Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/700107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.778.33,false,true,700109,Photographs,Photograph,Attitudes of Animals in Motion,,,,,,Artist,,Eadweard Muybridge,"American, born Britain, 1830–1904",,"Muybridge, Eadweard",,1830,1904,"1879, printed 1881",1879,1879,Albumen silver print,Image: 6 5/16 × 9 15/16 in. (16 × 25.3 cm) Mount: 8 13/16 × 12 5/8 in. (22.4 × 32 cm),"Gift of Joyce F. Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/700109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.452,false,true,684344,Photographs,Photograph,[Portrait of Living Man beside Dead Man],,,,,,Artist,,Louis Dodero,"Italian, active France, 1840s–60s",,"Dodero, Louis",,1820,1880,ca. 1850,1845,1855,Daguerreotype,Image: 2 5/8 × 3 7/16 in. (6.7 × 8.7 cm),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/684344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.770,false,true,286100,Photographs,Photograph,The Old Gamekeeper,,,,,,Artist,Possibly by,David Kinnebrook,"British, Norwich 1819–1865 New Zealand",,"Kinnebrook, David",,1819,1865,ca. 1844,1842,1846,Salted paper print from paper negative,6 3/4 x 5 7/8,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.21,false,true,296345,Photographs,Photograph,"[Sutar ka Jhopda Cave Interior, Ellora Caves]",,,,,,Artist,,Alfred William Plâté,"German (active Sri Lanka), ca. 1859 –1931 Linz",,"Plâté, Alfred William",,1859,1931,1890–1900,1890,1900,Platinum print,Image: 27.8 x 22.8 cm (10 15/16 x 9 in.),"Purchase, Robert A. Taub Gift, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.56,false,true,306342,Photographs,Photograph,34. Artistical Groups in Various Poses,,,,,,Artist,,Calvert Richard Jones,"British, Swansea, Wales 1802–1877 Bath, England",,"Jones, Calvert Richard",,1802,1877,ca. 1845,1840,1850,Salted paper print from paper negative,Sheet: 3 13/16 × 4 13/16 in. (9.7 × 12.2 cm) Image: 3 9/16 × 4 5/16 in. (9 × 11 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (24),false,true,287914,Photographs,Photograph,[Tree and Brush in Creek Scene],,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 16 × 20.8 cm (6 5/16 × 8 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (32),false,true,287922,Photographs,Photograph,Thereza,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 23.2 × 19 cm (9 1/8 × 7 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (35),false,true,287925,Photographs,Photograph,Yucca Gloriosa,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 20.2 × 15.7 cm (7 15/16 × 6 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (39),false,true,287929,Photographs,Photograph,The Upper Fall,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 24.7 × 17.8 cm (9 3/4 in. × 7 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (46),false,true,287936,Photographs,Photograph,Tenby Sands,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1856,1856,1856,Salted paper print,Image: 15.9 × 21.1 cm (6 1/4 × 8 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (47),false,true,287937,Photographs,Photograph,Upper Lake,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 19.2 × 24.1 cm (7 9/16 × 9 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (48),false,true,287938,Photographs,Photograph,Gipsies,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 15.8 × 21 cm (6 1/4 × 8 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (49),false,true,287939,Photographs,Photograph,"[View of a House in the Woods, with a Waterlogged Road]",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 18.5 × 22.3 cm (7 5/16 × 8 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (50),false,true,287940,Photographs,Photograph,The Lewitha,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 19.4 × 23.9 cm (7 5/8 × 9 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (51),false,true,287941,Photographs,Photograph,Dunraven Cliffs - Low Tide,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.8 × 20.5 cm (6 1/4 × 8 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (52),false,true,287942,Photographs,Photograph,Birthday Group,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1856,1856,1856,Albumen silver print,Image: 15.7 × 20.6 cm (6 3/16 × 8 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (53),false,true,287943,Photographs,Photograph,Sea Pool with Shells and Seaweeds,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,"Image: 15.2 × 20.5 cm (6 in. × 8 1/16 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (54),false,true,287944,Photographs,Photograph,Penrice Garden,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 19.4 × 24.6 cm (7 5/8 × 9 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (55),false,true,287945,Photographs,Photograph,Cureuleo Meadow,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 16.1 × 21.2 cm (6 5/16 × 8 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (56),false,true,287946,Photographs,Photograph,Oakley Cottage with Mr. St. John and Peter and Polly,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 16.4 × 21.2 cm (6 7/16 × 8 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (57),false,true,287947,Photographs,Photograph,Oakley Cottage,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 16.1 × 20.8 cm (6 5/16 × 8 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (58),false,true,287948,Photographs,Photograph,3 Cliffs Bay with a Wave,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 15.6 × 20.5 cm (6 1/8 × 8 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (78),false,true,287967,Photographs,Photograph,The Heron,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,Image: 24.3 × 18.9 cm (9 9/16 × 7 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (84),false,true,287973,Photographs,Photograph,"The Wharfe, Yorkshire",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,"Image: 19.9 cm, 24 7/8 in. (7 13/16 × 24 7/8 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (85),false,true,287974,Photographs,Photograph,Guy Fawkes,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853,1853,1853,Salted paper print,Image: 15 × 20.2 cm (5 7/8 × 7 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (30a-e),false,true,287920,Photographs,Photograph,[Figurine of Young Boy Holding Apples; Cabinet Card of a Man; Figurine of a Young Child with a Hat; Sculpture of a Man with Child; Sculpture with Animal],,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,"Beginning at top, moving clockwise: Image: 5.6 × 5.2 cm (2 3/16 × 2 1/16 in.) (top) Image: 7.3 × 5.4 cm (2 7/8 × 2 1/8 in.) (right) Image: 8.2 × 6.1 cm (3 1/4 × 2 3/8 in.) (bottom) Image: 7.2 × 5.4 cm (2 13/16 × 2 1/8 in.) (left) Image: 4.3 × 4.3 cm (1 11/16 × 1 11/16 in.) (center)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (43a-d),false,true,287933,Photographs,Photograph,Gipsies; Lewitha; Upper Lake; [Untitled],,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,"Image: 5.5 × 5.5 cm (2 3/16 × 2 3/16 in.) (a), circular Image: 5.5 × 5.7 cm (2 3/16 × 2 1/4 in.) (b), circular Image: 5.5 × 5.6 cm (2 3/16 × 2 3/16 in.) (c), circular Image: 14.8 × 10.7 cm (5 13/16 × 4 3/16 in.) (d), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (75a-c),false,true,287964,Photographs,Photograph,"Saccolabium Guttatum; Lizzie, Emily, Alice, Mrs Stratton; Emily, Etty, Alice, Lizzie, Mrs Drake, Mrs Stratton",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print; albumen silver print,"Image: 14.9 × 9.2 cm (5 7/8 × 3 5/8 in.) (a) Image: 9.3 × 7.1 cm (3 11/16 × 2 13/16 in.) (b) Image: 7 11/16 in., 8.3 cm (7 11/16 × 3 1/4 in.) (c)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (83a-h),false,true,287972,Photographs,Photograph,Miss Douglas; Mlle Isaline Motte; Miss Fanny Evans; Miss Catinka Smith; Mrs Leitch née Lloyd; Miss Martin; Capt. & Mrs Hibbert; Miss Cecilia Regnell,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,"Image: 8.9 × 5.5 cm (3 1/2 × 2 3/16 in.) (a) Image: 8 × 4.8 cm (3 1/8 × 1 7/8 in.) (b), oval Image: 8.7 × 5.5 cm (3 7/16 × 2 3/16 in.) (c) Image: 8.4 × 5.3 cm (3 5/16 × 2 1/16 in.) (d), diamond Image: 8.4 × 5.3 cm (3 5/16 × 2 1/16 in.) (e), diamond Image: 8.6 × 5.3 cm (3 3/8 × 2 1/16 in.) (f) Image: 8.1 × 5.5 cm (3 3/16 × 2 3/16 in.) (g), oval Image: 9.2 × 5.7 cm (3 5/8 × 2 1/4 in.) (h)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (37a, b)",false,true,287927,Photographs,Photograph,"Granny, Thereza, Elinor; The Shanty",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print,"Image: 11.1 × 9.5 cm (4 3/8 × 3 3/4 in.) (a), oval Image: 10.4 × 13.8 cm (4 1/8 × 5 7/16 in.) (a), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (41a, b)",false,true,287931,Photographs,Photograph,"Penllergare; Birthday Group, Sept. 23, 1853",,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 15.3 × 16.1 cm (6 in. × 6 5/16 in.) Image: 7.6 × 10.6 cm (3 in. × 4 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (77a, b)",false,true,287966,Photographs,Photograph,Penrice; [Untitled],,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Salted paper print; albumen silver print,Image: 11.8 × 13.9 cm (4 5/8 × 5 1/2 in.) (a) Image: 8.3 × 5.2 cm (3 1/4 × 2 1/16 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (79a, b)",false,true,287968,Photographs,Photograph,Coln Church + Mr Kent; 3 Miss Wallingtons,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1853–56,1853,1856,Albumen silver print,Image: 15.5 × 14.5 cm (6 1/8 × 5 11/16 in.) (a) Image: 8.1 × 6.7 cm (3 3/16 × 2 5/8 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (80a, b)",false,true,287969,Photographs,Photograph,Amy & Essie Dillwyn; TSM Meggie & Mary,,,,,,Artist,,John Dillwyn Llewelyn,"British, Swansea, Wales 1810–1882 Swansea, Wales",,"Llewelyn, John Dillwyn",,1810,1882,1861,1861,1861,Salted paper print; albumen silver print,Image: 13.9 × 10.4 cm (5 1/2 × 4 1/8 in.) (a) Image: 6.8 × 5.5 cm (2 11/16 × 2 3/16 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1254,false,true,728494,Photographs,Photograph,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Alexander Gardner|Unknown,"American, Glasgow, Scotland 1821–1882 Washington, D.C.|American",,"Gardner, Alexander|Unknown",,1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 12.8 × 8.7 cm (5 1/16 × 3 7/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/728494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1253,false,true,728493,Photographs,Photograph,The Wilderness Battlefield,,,,,,Artist|Artist,Possibly by,Unknown|Alexander Gardner,"American|American, Glasgow, Scotland 1821–1882 Washington, D.C.",,"Unknown|Gardner, Alexander",,1821,1882,1864,1864,1864,Albumen silver print from glass negative,Image: 10.9 × 9.5 cm (4 5/16 × 3 3/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/728493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.158.3,false,true,267330,Photographs,Photograph,Marius Bourotte,,,,,,Artist|Publisher,,Unknown|Le Petit Parisien,"French|French, active 1876–1944",,Unknown|Le Petit Parisien,,1876,1944,1929,1929,1929,Gelatin silver print with applied color,11.6 x 16.2 cm. (4 9/16 x 6 3/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1996",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.197.6 (1),false,true,289171,Photographs,Photographically illustrated book,Westminster Abbey,,,,,,Artist,,Nicolaas Henneman,"Dutch, Heemskerk 1813–1898 London",,"Henneman, Nicolaas",Dutch,1813,1898,before May 1845,1845,1846,Salted paper print from paper negative,,"Gift of Jean Horblit, in memory of Harrison D. Horblit, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.440,false,true,685780,Photographs,Carte-de-visite,[Jozef Israels],,,,,,Artist,,Willem Frederik Vinkenbos,"Dutch, Amsterdam 1831–1896 The Hague",,"Vinkenbos, Willem Frederik",Dutch,1831,1896,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.1–.11,false,true,283246,Photographs,Album,Série des Roses,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.) each,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/283246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.1–.13,false,true,286855,Photographs,Album,Série des Roses,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.) each,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.587.1–.29,false,true,286698,Photographs,Album,[Halévy Family Album],,,,,,Artist|Maker|Person in Photograph|Person in Photograph|Artist|Person in Photograph|Person in Photograph|Person in Photograph|Artist|Person in Photograph|Person in Photograph|Person in Photograph|Person in Photograph,Assembled by|Person in photograph|Person in photograph|Person in photograph|Person in photograph|Person in photograph|Person in photograph|Person in photograph|Person in photograph|Person in photograph,Hortense Howland|Ludovic Halévy|Henri-René-Albert-Guy-de Maupassant|Comtesse Laure de Chevigné|Marquis Alfred du Lau d'Allemans|Edgar Degas|Geneviève Halévy|Ludovic Halévy|Unknown|Charles Haas|Marquis Alfred du Lau d'Allemans|Louise Halévy|Émile Straus,"French, 1835–1920|French, 1834–1908|French, Dieppe, Normandy 1850–1893 Paris|1859–1936|1833–1919|French, Paris 1834–1917 Paris|French, 1849–1926|French, 1834–1908|French|1833–1902|1833–1919|French, 1847–1930",,"Howland, Hortense|Halévy, Ludovic|Maupassant, Henri-René-Albert-Guy-de|de Chevigné, Laure Comtesse|du Lau d'Allemans, Alfred Marquis|Degas, Edgar|Halévy, Geneviève|Halévy, Ludovic|Unknown|Haas, Charles|du Lau d'Allemans, Alfred Marquis|Halévy, Louise|Straus, Émile",French,1835 |1834 |1850 |1859 |1833 |1834 |1849 |1834 |1833 |1833 |1847,1920 |1908 |1893 |1936 |1919 |1917 |1926 |1908 |1902 |1919 |1930,1886–89,1886,1889,Gelatin silver print,Album: 10 1/4 x 13 x 1 5/8 inches Print sizes vary: 7.4 x 6.4 to 13 x 18.3,"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.611,false,true,283101,Photographs,Photograph,Louis-Jacques-Mandé Daguerre,,,,,,Artist|Person in Photograph,,Pierre-Ambrose Richebourg|Louis-Jacques-Mandé Daguerre,"French, 1810–1893",,"Richebourg, Pierre-Ambrose|Daguerre, Louis-Jacques-Mandé",French,1810,1893,ca. 1844,1842,1846,Daguerreotype,8.9 x 7 cm (3 1/2 x 2 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.1,false,true,261799,Photographs,Photograph,"Boûlâk, Carrefour (Mosquée, Café, etc.)",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.1 x 30.5 cm (9 1/2 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.2,false,true,261816,Photographs,Photograph,"Le Kaire, Mosquées d'Iscander-Pacha et du Sultan Haçan",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.6 cm. (9 5/16 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.3,false,true,261827,Photographs,Photograph,"Le Kaire, Mosquée du Sultan Haçan (le Tombeau)",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,25.2 x 30.5 cm. (9 15/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.4,false,true,261838,Photographs,Photograph,"Le Kaire, Mosquée Nâcéryeh",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.3 x 30.4 cm. (9 9/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.5,false,true,261849,Photographs,Photograph,"Le Kaire, Mosquée d'Amrou - Intérieur - Côté du Sanctuaire",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.5 cm. (9 7/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.6,false,true,261860,Photographs,Photograph,"Le Kaire, Tombeaux de Sultans Mamelouks",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.3 x 30.7 cm. (9 9/16 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.7,false,true,261871,Photographs,Photograph,"Le Kaire, Cimetière des Mamelouks",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.0 cm. (9 7/16 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.8,false,true,261882,Photographs,Photograph,"Djîzeh (Nécropole de Memphis), Sphinx et Pyramides",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.9 x 30.0 cm. (9 7/16 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.9,false,true,261893,Photographs,Photograph,"Djîzeh (Nécropole de Memphis), Pyramide de Chéops (Grande Pyramide)",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.4 x 30.1 cm. (9 5/8 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.10,false,true,261800,Photographs,Photograph,"Djîzeh (Nécropole de Memphis), Pyramide de Chéphren",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.2 x 30.2 cm. (9 1/2 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.11,false,true,261807,Photographs,Photograph,"Abâzîz, Intérieur d'un Village Arabe",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.3 cm. (9 7/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.12,false,true,261808,Photographs,Photograph,"El-Nâcérah, Dattiers, Rives du Nil et Barques",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.2 x 23.9 cm. (9 1/8 x 9 7/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.13,false,true,261809,Photographs,Photograph,"Béni-Haçan, Architecture Hypogéene - Tombeau d'Amoneï",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.5 x 30.3 cm. (9 5/8 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.14,false,true,261810,Photographs,Photograph,"Béni-Haçan, Architecture Hypogéene - Tombeau de Névothph",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.4 cm. (9 3/8 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.15,false,true,261811,Photographs,Photograph,"Syout, Habitations Arabes sur le Bord du Nil",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.9 x 30.0 cm. (9 7/16 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.16,false,true,261812,Photographs,Photograph,"Syout, Constructions Modernes - le Divan, etc.",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 29.8 cm. (9 5/16 x 11 3/4 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.17,false,true,261813,Photographs,Photograph,"Syout, Pont Sur le Grand Canal",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.4 cm. (9 5/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.18,false,true,261814,Photographs,Photograph,"Syout (Lycopolis), Statue Appartenant au Docteaur Cuny",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.6 x 15.4 cm. (9 5/16 x 6 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.19,false,true,261815,Photographs,Photograph,"Syout, Caravansérail en Ruines",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.7 x 30.3 cm. (9 3/4 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.20,false,true,261817,Photographs,Photograph,"Souâdj, Cimetière Musulman et Tombeau de Mouràd-Bey",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.0 cm. (9 7/16 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.21,false,true,261818,Photographs,Photograph,"Souâdj, Tombeau de Mouràd-Bey - Entrée de la Mosquée",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.9 x 30.5 cm. (9 7/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.22,false,true,261819,Photographs,Photograph,"Djirdjeh, Mosquée en Ruines Sur le Bord du Nil",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,29.7 x 25.1 cm. (11 11/16 x 9 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.23,false,true,261820,Photographs,Photograph,"Dendérah (Tentyris), Temple d'Athôr - Vue Générale",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 29.9 cm. (9 3/8 x 11 3/4 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.24,false,true,261821,Photographs,Photograph,"Dendérah (Tentyris), Temple d'Athôr - Face Postérieure",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.7 x 30.0 cm. (9 3/4 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.25,false,true,261822,Photographs,Photograph,"Dendérah (Tentyris), Temple d'Athôr - Face Postérieure - Cléopatre et Cæsarion",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.0 x 25.4 cm. (11 13/16 x 10 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.26,false,true,261823,Photographs,Photograph,"Dendérah (Tentyris), Temple d'Athôr - Sanctuaire Placé a l'Angle Sud-Ouest de la Plateforme Inférieure",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.1 x 30.8 cm. (9 1/2 x 12 1/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.27,false,true,261824,Photographs,Photograph,"Dendérah (Tentyris), Mammisi - Décoration Extérieure de la Face Sud",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.0 cm. (9 7/16 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.28,false,true,261825,Photographs,Photograph,"Louksor, Petit Bras du Nil - Barque de Voyageurs",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.4 x 30.7 cm. (9 5/8 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.29,false,true,261826,Photographs,Photograph,"Louksor (Thèbes), Vue Générale des Ruines",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.7 x 30.1 cm. (9 3/4 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.30,false,true,261828,Photographs,Photograph,"Louksor (Thèbes), Construction Antérieure - Pylône, Colosses et Obélisque",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 29.9 cm. (9 3/8 x 11 3/4 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.31,false,true,261829,Photographs,Photograph,Louksor (Thèbes). Construction Centrale - Grande Colonnade,,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.8 x 30.3 cm. (9 3/4 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.32,false,true,261830,Photographs,Photograph,"Louksor (Thèbes), Construction Postérieure - Galeries Parallèles",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.2 x 29.7 cm. (9 1/8 x 11 11/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.33,false,true,261831,Photographs,Photograph,"Louksor, Dattiers et Jardin de l'Expédition du Louksor",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.3 cm. (9 5/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.34,false,true,261832,Photographs,Photograph,"Médînet-Abou (Thèbes), Construction Antérieures - Vue Générale",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.2 x 30.2 cm. (9 1/2 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.35,false,true,261833,Photographs,Photograph,"Médînet-Abou (Thèbes), Construction Antérieures - Entrée Principale",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.5 x 30.5 cm. (9 5/8 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.36,false,true,261834,Photographs,Photograph,"Médînet-Abou (Thèbes), Constructions Postérieures - Fragment de Sculptures Sur la Face Nord-Est",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.6 x 30.1 cm. (9 11/16 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.37,false,true,261835,Photographs,Photograph,Médînet-Abou (Thèbes). Constructions Postérieures - Deuxiéme Cour,,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,25.0 x 30.3 cm. (9 13/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.38,false,true,261836,Photographs,Photograph,"Médînet-Abou (Thèbes), Constructions Postérieures - Deuxieme Cour - Galerie Nord-Ouest",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.3 x 24.6 cm. (11 15/16 x 9 11/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.39,false,true,261837,Photographs,Photograph,"Médînet-Abou (Thèbes), Construction Postérieures - Deuxiéme Cour - Galerie Sud-Ouest",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.5 x 25.1 cm. (12 x 9 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.40,false,true,261839,Photographs,Photograph,"Gournah (Thèbes), Colosses (Celui de Droite, Dit de Memnon)",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.9 x 30.5 cm. (9 7/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.41,false,true,261840,Photographs,Photograph,"Gournah (Thèbes), Colosse de Gauche - Décoration de la Face Nord-Est du Trône",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.4 x 30.0 cm. (9 5/8 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.42,false,true,261841,Photographs,Photograph,"Gournah (Thèbes), Palais Dit le Memnonium",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.5 x 30.1 cm. (9 5/8 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.43,false,true,261842,Photographs,Photograph,"Karnak, Groupe de Dattiers Vu du Point A",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.5 x 30.9 cm. (9 5/8 x 12 3/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.44,false,true,261843,Photographs,Photograph,"Karnak (Thèbes), Vue Générale des Ruines Prise du Point B",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.6 x 30.4 cm. (9 5/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.45,false,true,261844,Photographs,Photograph,"Karnak (Thèbes), Grande Porte du Sud Vue du Point C",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.3 cm. (9 7/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.46,false,true,261845,Photographs,Photograph,"Karnak (Thèbes), Vue Générale des Ruines Prise du Sud-Est, en T",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.6 x 30.5 cm. (9 5/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.47,false,true,261846,Photographs,Photograph,"Karnak (Thèbes), Vue Générale des Ruines Prise du Nord-Est, en V",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.3 x 30.4 cm. (9 9/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.48,false,true,261847,Photographs,Photograph,"Karnak (Thèbes), Enciente du Palais Vue du Point H",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.7 cm (9 3/8 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.49,false,true,261848,Photographs,Photograph,"Karnak (Thèbes), Enciente du Palais - Détailes de Sculptures au Point N",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.2 x 30.1 cm. (9 1/2 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.50,false,true,261850,Photographs,Photograph,"Karnak (Thèbes), Enciente du Palais - Détails de Sculptures au Point O",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.5 x 30.2 cm. (9 5/8 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.51,false,true,261851,Photographs,Photograph,"Karnak (Thèbes), Cour du Palais - Vue Prise de Point I",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.7 x 25.2 cm. (12 1/16 x 9 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.52,false,true,261852,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Vue Générale Prise du Point Q",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.7 x 30.5 cm. (9 3/4 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.53,false,true,261853,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Colonnade Centrale Vue du Point J",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.2 x 24.4 cm. (11 7/8 x 9 5/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.54,false,true,261854,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Colonnade Centrale - Décoration d'un Fut",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,25.0 x 16.3 cm. (9 13/16 x 6 7/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.55,false,true,261855,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Colonnade Centrale - Chapiteaux",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.3 x 25.4 cm. (11 15/16 x 10 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.56,false,true,261856,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Fenêtre et Chapiteaux des Galleries Latérales",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.2 x 25.0 cm (11 7/8 x 9 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.57,false,true,261857,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Fenêtre et Chapiteaux des Galeries Latérales",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.7 x 25.5 cm. (12 1/16 x 10 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.58,false,true,261858,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Décoration de la Paroi Intérieure au Point L",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.1 x 25.0 cm. (11 7/8 x 9 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.59,false,true,261859,Photographs,Photograph,"Karnak (Thèbes), Palais - Salle Hypostyle - Décoration de la Paroi Intérieure au Point M",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.3 x 30.5 cm. (9 3/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.60,false,true,261861,Photographs,Photograph,"Karnak (Thèbes), Palais - Construction de Granit - Pilier Sculpté, au Point P",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,26.8 x 13.5 cm. (10 9/16 x 5 5/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.61,false,true,261862,Photographs,Photograph,"Karnak (Thèbes), Palais - Construction de Granit - Décoration Sculptée et Piente au Point R",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.5 cm. (9 5/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.62,false,true,261863,Photographs,Photograph,"Karnak (Thèbes), Palais - Partie Posterieure - Fragment de Colonnades Vu du Point S",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.9 x 30.6 cm. (9 7/16 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.63,false,true,261864,Photographs,Photograph,"Karnak (Thèbes), Édifice en Ruines - Sculptures du la Paroi Intèrieure, en U",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.9 x 30.3 cm. (9 13/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.64,false,true,261865,Photographs,Photograph,"Karnak (Thèbes), Grande Porte du Nord Vue du Point X",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.6 cm. (9 5/16 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.65,false,true,261866,Photographs,Photograph,"Karnak (Thèbes), Troisième Pylône - Colosse de Spath Calcaire, en D",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.3 cm. (9 7/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.66,false,true,261867,Photographs,Photograph,"Karnak (Thèbes), Premier Pylône - Ruines de la Porte et des Colosses, Vues du Point E",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.3 x 30.5 cm. (9 9/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.67,false,true,261868,Photographs,Photograph,"Karnak (Thèbes), Avenue de Sphinx - Vue Générale Prise du Point G",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.1 x 30.5 cm. (9 1/2 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.68,false,true,261869,Photographs,Photograph,"Karnak (Thèbes), Sphinx a Tête Humaine et a Tête de Bélier, en Y",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.0 x 30.6 cm. (9 7/16 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.69,false,true,261870,Photographs,Photograph,"Erment (Hermonthis), Vue Générale des Ruines -Temple et Mammisi",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.3 x 30.2 cm. (9 9/16 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.70,false,true,261872,Photographs,Photograph,"Esneh (Latopolis), Construction Ensablée - Paroi Extérieure - Corniche et Sculptures",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.5 cm. (9 3/8 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.71,false,true,261873,Photographs,Photograph,"Esneh (Latopolis), Construction Ensablée - Architrave, Futs, et Chapiteaux",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,25.0 x 30.7 cm. (9 13/16 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.72,false,true,261874,Photographs,Photograph,"Esneh, Dattiers, Sycomore et Café Sur le Bord du Nil",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.5 cm. (9 3/8 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.73,false,true,261875,Photographs,Photograph,"El-Kab (Éléthya), Vue Générale de l'Hémi-Spéos",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.9 x 30.5 cm. (9 7/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.74,false,true,261876,Photographs,Photograph,"El-Kab (Éléthya), Architecture Hypogéenne - Tombeau de Phapé - Sculptures Pientes",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,25.2 x 30.2 cm. (9 15/16 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.75,false,true,261877,Photographs,Photograph,"Edfou (Apollonopolis Magna), Vue Générale du Temple",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.9 x 30.4 cm. (9 7/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.76,false,true,261878,Photographs,Photograph,"Edfou (Apollonopolis Magna), Galerie Latérale de la Cour et Pronaos",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.7 x 30.8 cm. (9 3/4 x 12 1/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.77,false,true,261879,Photographs,Photograph,"Edfou, Aspect Générale de la Ville Vue de la Plateforme Centrale du Pylône",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.5 x 25.0 cm. (12 x 9 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.78,false,true,261880,Photographs,Photograph,"Djébel Selséleh (Silsilis), Steles Architecturales Taillées Dans les Carriéres",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.5 x 30.3 cm. (9 1/4 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.79,false,true,261881,Photographs,Photograph,"Kôm-Ombou (Ombos), Vue Générale des Ruines",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.7 cm. (9 3/8 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.80,false,true,261883,Photographs,Photograph,"Assouan, Ruines de l'Ancienne Enciente Arabe, au Sud-Est de la Ville",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.4 cm. (9 3/8 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.81,false,true,261884,Photographs,Photograph,"Assouan (Syène), Carrières de Granit - Ancien Système d'Extraction",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.3 cm. (9 3/8 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.82,false,true,261885,Photographs,Photograph,"Assouan, Cimetière Arabe - Inscription Funéraires",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.9 cm. (9 5/16 x 12 3/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.83,false,true,261886,Photographs,Photograph,"Première Cataracte, Vue Générale Prise de la Point Méridionale de l'Ile d'Éléphantine",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.2 x 30.7 cm. (9 1/2 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.84,false,true,261887,Photographs,Photograph,"Première Cataracte, Montagnes Granitiques Couvertes de Sables",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.6 x 30.3 cm. (9 5/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.85,false,true,261888,Photographs,Photograph,"Ile de Fîleh (Philæ), Vue Générale Prise du Sud-Est au Point B",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.2 x 30.5 cm. (9 1/2 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.86,false,true,261889,Photographs,Photograph,"Ile de Fîleh (Philæ), Édifice de l'Est - Vue Générale Prise du Point C",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,20.8 x 30.5 cm. (8 3/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.87,false,true,261890,Photographs,Photograph,"Ile de Fîleh (Philæ), Édifice de l'Est - Face Latérale Vue du Point D",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.5 x 30.3 cm. (9 1/4 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.88,false,true,261891,Photographs,Photograph,"Ile de Fîleh (Philæ), Édifice de l'Est - Façade Occidentale - Vue du Point E",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,25.1 x 30.5 cm. (9 7/8 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.89,false,true,261892,Photographs,Photograph,"Ile de Fîleh (Philæ), Édifice du Sud et Partie de la Colonnade Occidentale Vue du Point V",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.5 cm. (9 5/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.90,false,true,261894,Photographs,Photograph,"Ile de Fîleh (Philæ), Colonnade Occidentale - Ruines Vues du Point L",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.2 x 30.0 cm. (9 1/8 x 11 13/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.91,false,true,261895,Photographs,Photograph,"Ile de Fîleh (Philæ), Premier Pylône - Vue Prise de la Plate-Forme de la Colonnade Orientale en P",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.3 x 30.5 cm. (9 9/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.92,false,true,261896,Photographs,Photograph,"Ile de Fîleh (Philæ), Vue Génèrale Prise du Point I, Sur La Plateforme du Pylône",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.8 x 30.7 cm. (9 3/8 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.93,false,true,261897,Photographs,Photograph,"Ile de Fîleh (Philæ), Deuxième Pylône - Partie Orientale Vue de la Plateforme Inférieure du Premier Pylône, du Point G",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.2 x 23.4 cm. (11 7/8 x 9 3/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.94,false,true,261898,Photographs,Photograph,"Environs de Fîleh, Palmier Doum sur la Rive Orientale du Nil",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.8 cm. (9 5/16 x 12 1/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.95,false,true,261899,Photographs,Photograph,"Débôd (Parembole), Vue Générale des Ruines",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.1 x 30.7 cm. (9 1/2 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.96,false,true,261900,Photographs,Photograph,"Kardâcy, Sanctuaire, Niches et Inscriptions Taillées dans les Carrières",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.6 cm. (9 5/16 x 12 1/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.97,false,true,261901,Photographs,Photograph,"Tâfah, Rochers Granitiques sur les Rives du Nil",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,24.9 x 30.1 cm. (9 13/16 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.98,false,true,261902,Photographs,Photograph,"Kalabcheh (Talmis), Ruines du temple - Façade et Mur d'Enceinte",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.6 x 30.3 cm. (9 5/16 x 11 15/16 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.99,false,true,261903,Photographs,Photograph,"Dandour, Vue Générale des Ruines",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.2 x 30.2 cm. (9 1/8 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.100,false,true,261801,Photographs,Photograph,"Djerf-Hocein (Tutzis), Hemi-Spéos, Colosses de la Partie Extérieure",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.7 x 30.4 cm. (9 5/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.101,false,true,261802,Photographs,Photograph,"Dakkeh (Pselcis), Vue Générale du Temple",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.1 x 30.2 cm. (9 1/16 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.102,false,true,261803,Photographs,Photograph,"Korósko, Sycomores et Campement d'une Caravane pour le Sennâr",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.4 x 29.5 cm. (9 3/16 x 11 5/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.103,false,true,261804,Photographs,Photograph,"Deîr, Carrefour et Habitation Particulière",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.6 x 30.2 cm. (9 5/16 x 11 7/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.104,false,true,261805,Photographs,Photograph,"Abou-Sembil, Petit Spéos - Partie Gauche de la Façade",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,23.3 x 30.5 cm. (9 3/16 x 12 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.607.105,false,true,261806,Photographs,Photograph,"Abou Sembil, Grand Spéos - Statues Colossales, Vues de Trois-Quarts",,,,,,Artist|Printer,,Félix Teynard|Imprimerie Photographique de H. de Fonteny et Cie,"French, 1817–1892",,"Teynard, Félix|Imprimerie Photographique de H. de Fonteny et Cie",French,1817,1892,"1851–52, printed 1853–54",1851,1852,Salted paper print from paper negative,30.0 x 24.4 cm. (11 13/16 x 9 5/8 in.),"Purchase, Lila Acheson Wallace Gift, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.63,false,true,685405,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,Walery Frères,"French, active 1860s–1870s",,Walery Frères,French,1860,1879,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.227,false,true,685568,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,Walery Frères,"French, active 1860s–1870s",,Walery Frères,French,1860,1879,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.200,false,true,285700,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1858,1858,1858,Albumen silver print from glass negative,Image: 27.8 x 20.9 cm (10 15/16 x 8 1/4 in.) Mount: 29 x 22 cm (11 7/16 x 8 11/16 in.) Mount (2nd): 45 x 35.3 cm (17 11/16 x 13 7/8 in.) Mat: 50.8 x 40.6 cm (20 x 16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.392,false,true,285701,Photographs,Photograph,[Countess de Castiglione as Elvira at the Cheval Glass],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1861–67,1861,1867,Salted paper print from glass negative,Image: 14.5 x 15.4 cm (5 11/16 x 6 1/16 in.) Mount: 17.1 x 17.3 cm (6 3/4 x 6 13/16 in.) Mat: 43.2 x 35.6 cm (17 x 14 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.405,false,true,286837,Photographs,Photograph,La Comtesse de Castiglione en Reine de la Nuit,,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1863–67,1863,1867,Albumen silver print from glass negative,Image: 10.5 x 7.4 cm (4 1/8 x 2 15/16 in.) Mount: 12.2 x 9 cm (4 13/16 x 3 9/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.423,false,true,286787,Photographs,Photograph,[Trying for Snapshots],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1861–67,1861,1867,Albumen silver print from glass negative,Image: 9.6 x 7.7 cm (3 3/4 x 3 1/16 in.) Mount: 12.1 x 8.9 cm (4 3/4 x 3 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.1,false,true,288106,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.2,false,true,288107,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.3,false,true,288108,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.4,false,true,288109,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.6,false,true,288111,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.7,false,true,288112,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.8,false,true,288113,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.9,false,true,288114,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.1,false,true,288124,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.2,false,true,288125,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.4,false,true,288127,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.5,false,true,288128,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.6,false,true,288129,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.7,false,true,288130,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.8,false,true,288131,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.9,false,true,288132,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.424.2,false,true,286770,Photographs,Photograph,Ti-fille Brune,,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Image: 14.3 x 9.8 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.10,false,true,288115,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.10,false,true,288133,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.11,false,true,288219,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.12,false,true,288220,Photographs,Photograph,[Countess de Castiglione],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.424.1, .3",false,true,286564,Photographs,Photograph,Ti-fille Brune,,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1895,1895,1895,Albumen silver print from glass negative,Image: 14.3 x 9.8 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.258–.262,false,true,289066,Photographs,Photograph,[Album page with ten photographs of La Comtesse mounted recto and verso],,,,,,Person in Photograph|Artist,,Countess Virginia Oldoini Verasis di Castiglione|Pierre-Louis Pierson,"1835–1899|French, 1822–1913",,"Castiglione, di, Virginia Oldoini Verasis Countess|Pierson, Pierre-Louis",French,1835 |1822,1899 |1913,1861–67,1861,1867,Albumen silver prints from glass negative,10.8 x 8.6 cm (4 1/4 x 3 3/8 in.) to 2.5 x 3.5 cm (1 x 1 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.188,false,true,269214,Photographs,Photograph,Scherzo di Follia,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"1863–66, printed 1940s",1863,1866,Gelatin silver print from glass negative,18.7 x 12.5 cm. (7 3/8 x 4 15/16 in.),"Gift of George Davis, 1948",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.66,false,true,261518,Photographs,Carte-de-visite,Convalescente (autre),,,,,,Artist|Person in Photograph,Person in photograph,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,10.4 x 6.3 cm (4 1/8 x 2 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.68,false,true,261520,Photographs,Carte-de-visite,Convalescente (autre),,,,,,Artist|Person in Photograph,Person in photograph,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,9.9 x 6.9 cm (3 7/8 x 2 11/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.183,false,true,261364,Photographs,Photograph,[La Comtesse at Table with Hand to Face],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,8.9 x 12.1 cm (3 1/2 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.184,false,true,261365,Photographs,Photograph,[La Comtesse],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,11.7 x 16.2 cm (4 5/8 x 6 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.189,false,true,261370,Photographs,Photograph,[La Comtesse in robe de piqué or as Judith (?)],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative with applied color,12.4 x 8.8 cm (4 7/8 x 3 7/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261370,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.190,false,true,261372,Photographs,Photograph,[La Comtesse in robe de piqué‚ or as Judith (?)],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative with applied color,12.3 x 8.7 cm (4 13/16 x 3 7/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.191,false,true,261373,Photographs,Photograph,[La Comtesse in robe de piqué or as Judith (?)],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative with applied color,12.2 x 8.5 cm (4 13/16 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.192,false,true,261374,Photographs,Photograph,[La Comtesse in robe de piqué or as Judith (?)],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative with applied color,12.4 x 8.7 cm (4 7/8 x 3 7/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.257,false,true,261454,Photographs,Photograph,[La Comtesse Reclining in Dark Dress with Chain Around Neck],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–65,1861,1865,Albumen silver print from glass negative,10.2 x 12.0 cm (4 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.268,false,true,261466,Photographs,Photograph,[La Comtesse in Ermine Cape],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,9.2 x 14.9 cm (3 5/8 x 5 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.269,false,true,261467,Photographs,Photograph,"[La Comtesse in Hat with Veil and Cape with Fringe, Serie à la Ristori]",,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,9.5 x 13.7 cm (3 3/4 x 5 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.270,false,true,261469,Photographs,Photograph,[La Comtesse in Cape with Fringe; Serie à la Ristori],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,8.3 x 13.0 cm (3 1/4 x 5 1/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.271,false,true,261471,Photographs,Photograph,[La Comtesse with Group on a Rocky Beach],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Albumen silver print from glass negative,21.3 x 17.8 cm (8 3/8 x 7 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.272,false,true,261472,Photographs,Photograph,[La Comtesse décolletée; Roses mousseuses],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Salted paper print from glass negative,44.8 x 29.2 cm (17 5/8 x 11 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.273,false,true,261473,Photographs,Photograph,[La Comtesse at Table holding Fan],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Salted paper print from glass negative,54.9 x 36.2 cm (21 5/8 x 14 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.274,false,true,261474,Photographs,Photograph,[La Comtesse in Lace Shawl],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Salted paper print from glass negative,54.6 x 36.2 cm (21 1/2 x 14 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.275,false,true,261475,Photographs,Photograph,[La Comtesse at Table with Hand to Face],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1860s,1860,1869,Salted paper print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.119,false,true,283245,Photographs,Photograph,The Gaze,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1856–57,1856,1857,Albumen silver print from glass negative,Image: 9 x 6.6 cm (3 9/16 x 2 5/8 in.) Mount: 12.9 x 9.1 cm (5 1/16 x 3 9/16 in.) Mat: 21.9 x 15.9 cm (8 5/8 x 6 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.194,false,true,285650,Photographs,Photograph,The White Nun,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1856–57,1856,1857,Albumen silver print from glass negative,Image: 19.1 x 13.8 cm (7 1/2 x 5 7/16 in.) Mount: 19.1 x 14.4 cm (7 1/2 x 5 11/16 in.) Mat: 43.2 x 35.6 cm (17 x 14 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.195,false,true,285614,Photographs,Photograph,Béatrix,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1856–57,1856,1857,Albumen silver print from glass negative,Image: 10.5 x 7 cm (4 1/8 x 2 3/4 in.) Mount: 12.2 x 9.1 cm (4 13/16 x 3 9/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.197,false,true,285652,Photographs,Photograph,[The Opera Ball],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"1861–67, printed 1895–1910",1861,1910,Gelatin silver print from glass negative,Image: 36 x 27.9 cm (14 3/16 x 11 in.) Mat: 57.2 x 47 cm (22 1/2 x 18 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.198,false,true,285608,Photographs,Photograph,Scherzo di Follia,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"1861–67, printed ca. 1930",1861,1932,Gelatin silver print from glass negative,Image: 39.8 x 29.8 cm (15 11/16 x 11 3/4 in.) Mat: 61 x 50.8 cm (24 x 20 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.389,false,true,285662,Photographs,Photograph,Le Manteau d'Hermine,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"August 31, 1895",1895,1895,Albumen silver print from glass negative,Image: 14.4 x 10 cm (5 11/16 x 3 15/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.390,false,true,286841,Photographs,Photograph,Rachel,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 14.6 x 9.8 cm (5 3/4 x 3 7/8 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.393,false,true,286827,Photographs,Cabinet card,Sculptural Shoulders,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 12 x 8.5 cm (4 3/4 x 3 3/8 in.) Mount: 15.4 x 9.6 cm (6 1/16 x 3 3/4 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.394,false,true,286828,Photographs,Carte-de-visite,La Dogaresse,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 8.9 x 5.4 cm (3 1/2 x 2 1/8 in.) Mount: 10.7 x 6.2 cm (4 3/16 x 2 7/16 in.) Mat: 25.4 x 20.3 cm (10 x 8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.403,false,true,286835,Photographs,Photograph,La Marquise Mathilde,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–66,1861,1866,Albumen silver print from glass negative,Image: 9.9 x 7.5 cm (3 7/8 x 2 15/16 in.) Mount: 12.2 x 8.9 cm (4 13/16 x 3 1/2 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.404,false,true,286836,Photographs,Photograph,La Dame de Cœurs,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–63,1861,1863,Albumen silver print from glass negative,Image: 10.5 x 7.4 cm (4 1/8 x 2 15/16 in.) Mount: 12.3 x 9.1 cm (4 13/16 x 3 9/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.411,false,true,286785,Photographs,Photograph,[Countess de Castiglione],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"August 31, 1895",1895,1895,Albumen silver print from glass negative,Image: 14.3 x 10.1 cm (5 5/8 x 4 in.) Mat: 59.8 x 49.8 cm (23 9/16 x 19 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.412,false,true,286843,Photographs,Photograph,[Countess de Castiglione],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"August 31, 1895",1895,1895,Albumen silver print from glass negative,Image: 14.3 x 10.1 cm (5 5/8 x 4 in.) Mat: 59.8 x 49.8 cm (23 9/16 x 19 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.413,false,true,286844,Photographs,Photograph,L'Armoire,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 14.8 x 9.8 cm (5 13/16 x 3 7/8 in.) Mat: 59.8 x 49.8 cm (23 9/16 x 19 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.414,false,true,286845,Photographs,Photograph,Ristori,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 14.3 x 9.8 cm (5 5/8 x 3 7/8 in.) Mat: 59.8 x 49.8 cm (23 9/16 x 19 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.415,false,true,286846,Photographs,Photograph,Madame Douane,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 14.4 x 10.1 cm (5 11/16 x 4 in.) Mat: 59.8 x 49.8 cm (23 9/16 x 19 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.416,false,true,286847,Photographs,Photograph,Torino Aosta,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 14.9 x 9.9 cm (5 7/8 x 3 7/8 in.) Mat: 59.8 x 49.8 cm (23 9/16 x 19 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.418,false,true,286822,Photographs,Photograph,[Standing at the Prie-Dieu],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–64,1861,1864,Albumen silver print from glass negative,Image: 10.5 x 8.5 cm (4 1/8 x 3 3/8 in.) Mount: 10.5 x 8.5 cm (4 1/8 x 3 3/8 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.419,false,true,286823,Photographs,Photograph,Anne Boleyn,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–65,1861,1865,Albumen silver print from glass negative,Image: 13.2 x 10.1 cm (5 3/16 x 4 in.) Mount: 14.3 x 10.6 cm (5 5/8 x 4 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.420,false,true,286824,Photographs,Cabinet card,[Standing with a Rosary],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 11.7 x 8.5 cm (4 5/8 x 3 3/8 in.) Mount: 15.5 x 10 cm (6 1/8 x 3 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.421,false,true,286825,Photographs,Photograph,La Reine d'Étrurie,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1863–67,1863,1867,Albumen silver print from glass negative,Image: 11.3 x 9.4 cm (4 7/16 x 3 11/16 in.) Mount: 26.8 x 21 cm (10 9/16 x 8 1/4 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.422,false,true,286826,Photographs,Photograph,"[Profile with Chignon, Large]",,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1859,1859,1859,Albumen silver print from glass negative,Image: 26.3 x 21.2 cm (10 3/8 x 8 3/8 in.) Mount: 30.9 x 23.8 cm (12 3/16 x 9 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.426,false,true,285653,Photographs,Photograph,La Mà,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"August 21, 1895",1895,1895,Albumen silver print from glass negative,Image: 14.1 x 10.1 cm (5 9/16 x 4 in.) Mount: 14.1 x 10.1 cm (5 9/16 x 4 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.427,false,true,286848,Photographs,Photograph,Baisemain,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 14 x 9.9 cm (5 1/2 x 3 7/8 in.) Mount: 14 x 9.9 cm (5 1/2 x 3 7/8 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.428,false,true,286849,Photographs,Photograph,Arrivo,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 14.7 x 9.8 cm (5 13/16 x 3 7/8 in.) Mount: 14.7 x 9.8 cm (5 13/16 x 3 7/8 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.) Frame: 37.3 x 30 cm (14 11/16 x 11 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.429,false,true,286850,Photographs,Photograph,[Countess de Castiglione],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1895,1895,1895,Albumen silver print from glass negative,Image: 15.1 x 10.1 cm (5 15/16 x 4 in.) Mount: 15.2 x 10.2 cm (6 x 4 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.430,false,true,286851,Photographs,Photograph,Au Bureau,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 13.7 x 9.9 cm (5 3/8 x 3 7/8 in.) Mount: 13.7 x 10 cm (5 3/8 x 3 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.431,false,true,286852,Photographs,Photograph,À la Barre,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 13.8 x 10 cm (5 7/16 x 3 15/16 in.) Mount: 13.8 x 10 cm (5 7/16 x 3 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.432,false,true,286853,Photographs,Photograph,Roses Compiègne,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1895,1895,1895,Albumen silver print from glass negative,Image: 14.9 x 10.1 cm (5 7/8 x 4 in.) Mount: 15 x 10.1 cm (5 7/8 x 4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.433,false,true,286854,Photographs,Photograph,Coin Noir de la Colonne,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"September 1, 1893",1893,1893,Albumen silver print from glass negative,Image: 9.8 x 14.2 cm (3 7/8 x 5 9/16 in.) Mount: 9.9 x 14.2 cm (3 7/8 x 5 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.387.5,false,true,288110,Photographs,Photograph,"[Countess de Castiglione, from Série des Roses]",,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.388.3,false,true,288126,Photographs,Photograph,[Countess de Castiglione],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1895,1895,1895,Albumen silver print from glass negative,Approximately 14.3 x 9.9 cm (5 5/8 x 3 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (3a),false,true,288157,Photographs,Photograph,[Countess de Castiglione as the Queen of Etruria],,,,,,Artist|Artist|Person in Photograph,Painted and retouched by|Person in photograph,Pierre-Louis Pierson|Marck|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",", et al","Pierson, Pierre-Louis|Marck|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,before 1865,1855,1865,Albumen silver print from glass negative,Window: 7 1/2 × 5 1/2 in. (19 × 14 cm) Image: 4 in. × 2 13/16 in. (10.2 × 7.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (3b),false,true,288158,Photographs,Photograph,[Countess de Castiglione as Anne Boleyn],,,,,,Artist|Artist|Person in Photograph,Painted and retouched by|Person in photograph,Pierre-Louis Pierson|Marck|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",", et al","Pierson, Pierre-Louis|Marck|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,before 1865,1855,1865,Albumen silver print from glass negative,Window: 7 1/2 × 5 1/2 in. (19 × 14 cm) Image: 4 in. × 2 13/16 in. (10.2 × 7.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.263–.267,false,true,684336,Photographs,Photograph,[Album page with ten photographs of La Comtesse mounted recto and verso],,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver prints from glass negative,10.8 x 8.6 cm (4 1/4 x 3 3/8 in.) to 2.5 x 3.5 cm (1 x 1 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/684336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.391.1–.2,false,true,286842,Photographs,Photograph,Le Pé,,,,,,Artist|Person in Photograph,,Pierre-Louis Pierson|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|1835–1899",,"Pierson, Pierre-Louis|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"August 1, 1894",1894,1894,Albumen silver print from glass negative,Image: 14.4 x 10 cm (5 11/16 x 3 15/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.71,false,true,287478,Photographs,Photograph,[Empress Eugénie's Poodle],,,,,,Photography Studio,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1850s,1850,1859,Salted paper print from collodion glass negative,Image: 20.7 x 20.8 cm (8 1/8 x 8 3/16 in.) Sheet: 22.2 x 29 cm (8 3/4 x 11 7/16 in.),"Gilman Collection, Purchase, The Howard Gilman Foundation Gift, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.20,false,true,269069,Photographs,Photograph,Ruines gothiques,,,,,,Artist|Printer,,"A. Fays|Imprimerie photographique de Blanquart-Évrard, à Lille","French|French, active 1851–55",,"Fays, A.|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1851,1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.395,false,true,285479,Photographs,Photograph,Ermitage de Passy,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1863,1863,1863,Albumen silver print from glass negative,Image: 12 x 8.5 cm (4 3/4 x 3 3/8 in.) Mount: 13 x 9.6 cm (5 1/8 x 3 3/4 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.396,false,true,285649,Photographs,Photograph,The Red Bow,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 18.7 x 13.6 cm (7 3/8 x 5 3/8 in.) Mount: 32.3 x 23.8 cm (12 11/16 x 9 3/8 in.) Mat: 50.8 x 40.6 cm (20 x 16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.397,false,true,286829,Photographs,Photograph,Les Cothurnes,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 5.4 x 7.5 cm (2 1/8 x 2 15/16 in.) Mount: 6.8 x 8.6 cm (2 11/16 x 3 3/8 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.398,false,true,286830,Photographs,Photograph,Les Cothurnes,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 5.4 x 7.5 cm (2 1/8 x 2 15/16 in.) Mount: 6.8 x 8.8 cm (2 11/16 x 3 7/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.399,false,true,286831,Photographs,Photograph,La Reine d'Étrurie,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1863–67,1863,1867,Albumen silver print from glass negative,Image: 10.5 x 8.3 cm (4 1/8 x 3 1/4 in.) Mount: 12.9 x 9.4 cm (5 1/16 x 3 11/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.400,false,true,286832,Photographs,Photograph,[La Finlandaise],,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 12.2 x 8.8 cm (4 13/16 x 3 7/16 in.) Mount: 13.9 x 10.9 cm (5 1/2 x 4 5/16 in.) Mat: 35.6 x 27.9 cm (14 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.401,false,true,286833,Photographs,Photograph,Costigliole,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1862–67,1862,1867,Albumen silver print from glass negative,Image: 12.5 x 9 cm (4 15/16 x 3 9/16 in.) Mount: 14.5 x 10.7 cm (5 11/16 x 4 3/16 in.) Mat: 35.6 x 43.2 cm (14 x 17 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.402,false,true,286834,Photographs,Photograph,Costigliole,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1862–67,1862,1867,Albumen silver print from glass negative,Image: 12.5 x 8.9 cm (4 15/16 x 3 1/2 in.) Mount: 14.4 x 10.9 cm (5 11/16 x 4 5/16 in.) Mat: 35.6 x 43.2 cm (14 x 17 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.406,false,true,285609,Photographs,Photograph,La Frayeur,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Salted paper print from glass negative with applied color,Image: 23.6 x 17 cm (9 5/16 x 6 11/16 in.) Mount: 31.7 x 23.8 cm (12 1/2 x 9 3/8 in.) Mat: 57.2 x 47 cm (22 1/2 x 18 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.407,false,true,286838,Photographs,Photograph,Béatrix,,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,"1856–57, printed 1861–67",1856,1867,Salted paper print from glass negative,Image: 23.2 x 17.8 cm (9 1/8 x 7 in.) Mount: 31.7 x 23.8 cm (12 1/2 x 9 3/8 in.) Mat: 57.2 x 47 cm (22 1/2 x 18 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.408a–d,false,true,286839,Photographs,Photograph,"[Variations on the ""Elvira"" and ""Ritrosetta"" Dresses]",,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 12.4 x 8.8 cm (4 7/8 x 3 7/16 in.) A Image: 12.5 x 8.9 cm (4 15/16 x 3 1/2 in.) B Image: 12.3 x 8.8 cm (4 13/16 x 3 7/16 in.) C Image: 12.4 x 8.9 cm (4 7/8 x 3 1/2 in.) D Mount: 32.2 x 24 cm (12 11/16 x 9 7/16 in.) Mat: 50.8 x 40.6 cm (20 x 16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.409a–d,false,true,286840,Photographs,Photograph,"[Variations on the ""Elvira"" Dress]",,,,,,Artist|Artist|Person in Photograph,Painted and retouched by,Pierre-Louis Pierson|Unknown|Countess Virginia Oldoini Verasis di Castiglione,"French, 1822–1913|French|1835–1899",,"Pierson, Pierre-Louis|Unknown|Castiglione, di, Virginia Oldoini Verasis Countess",French,1822 |1835,1913 |1899,1861–67,1861,1867,Albumen silver print from glass negative,Image: 12.5 x 8.9 cm (4 15/16 x 3 1/2 in.) A Image: 12.5 x 8.7 cm (4 15/16 x 3 7/16 in.) B Image: 12.3 x 8.9 cm (4 13/16 x 3 1/2 in.) C Image: 12.3 x 8.8 cm (4 13/16 x 3 7/16 in.) D Mount: 32.2 x 24 cm (12 11/16 x 9 7/16 in.) Mat: 50.8 x 40.6 cm (20 x 16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.714.1, .2",false,true,286111,Photographs,Photograph,[Auguste Vacquerie at Marine Terrace],,,,,,Artist|Author,,Charles Victor Hugo|Auguste Vacquerie,"French, 1826–1871|French, 1819–1855",,"Hugo, Charles Victor|Vacquerie, Auguste",French,1826 |1819,1871 |1855,1855,1855,1855,Salted paper print from glass negative,Image: 4 1/8 × 2 15/16 in. (10.5 × 7.4 cm) Mount: 8 5/8 × 5 13/16 in. (21.9 × 14.8 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.187,false,true,306174,Photographs,Photograph,Arak el Emir,,,,,,Artist|Printer,Possibly printed by,Louis Vignes|Charles Nègre,"French, 1831–1896|French, 1820–1880",,"Vignes, Louis|Nègre, Charles",French,1831 |1820,1896 |1880,1864,1864,1864,Albumen silver print from glass negative,Image: 19 × 24.7 cm (7 1/2 × 9 3/4 in.) Mount: 27.7 × 35.7 cm (10 7/8 × 14 1/16 in.),"Purchase, Susan and Thomas Dunn Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.9,false,true,269078,Photographs,Photograph,Joueurs de vielle,,,,,,Artist|Printer,,"Louis-Désiré Blanquart-Évrard|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1802–1872|French, active 1851–55",,"Blanquart-Évrard, Louis-Désiré|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1802 |1851,1872 |1855,1850–53,1850,1853,Salted paper print from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.14,false,true,269062,Photographs,Photograph,"Nature morte: chaudron, cruche, et légumes, sur une table à trétaux",,,,,,Artist|Printer,,"Henri Victor Regnault|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1810–1878|French, active 1851–55",,"Regnault, Henri Victor|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1810 |1851,1878 |1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.673.10,false,true,262126,Photographs,Photograph,"Nubie. Grand Temple d'Isis, A Philoe. Vue générale prise du nord",,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,16.8 x 23.1 cm. (6 5/8 x 9 1/16 in.),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.673.11,false,true,262127,Photographs,Photograph,"Nubie. Grand Temple d'Isis, A Philoe",,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,16.0 x 22.4 cm. (6 5/16 x 8 13/16 in.),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.1,false,true,263214,Photographs,Photograph,Égypte Moyenne. Pyramide de Chéphren,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.2,false,true,263225,Photographs,Photograph,Haute-Égypte. Girgeh. Mosquèe d'Aly-Bey,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.4,false,true,263234,Photographs,Photograph,Thebes. Palais de Karnak. Sculptures extérieures du Sanctuaire de granit,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.5,false,true,263235,Photographs,Photograph,Thebes. Palais de Karnak. Sanctuaire de granit et salle Hypostyle,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.6,false,true,263236,Photographs,Photograph,Thebes. Médinet-Habou. Partie orientale du Péristyle du Palais de Ramsès-Méiamoun,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.7,false,true,263237,Photographs,Photograph,Thebes. Médinet-Habou. Runes de la ville de Papa,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.10,false,true,263215,Photographs,Photograph,Haute-Égypte. Entrée de la première Cataracte,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.12,false,true,263217,Photographs,Photograph,Nubie. Philoe,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.13,false,true,263218,Photographs,Photograph,Nubie. Grand Temple D'Isis A Philoe. Muraille occidentale,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.14,false,true,263219,Photographs,Photograph,Nubie. Rive Orientale du Nil (Village de Bab). Vue prise au sud de Philoe,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.15,false,true,263220,Photographs,Photograph,Nubie. Temple et Village de Débôd. Parembole de l'itinéraire d'Antonin,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.16,false,true,263221,Photographs,Photograph,Nubie. Temple de Déböd. Parembole de l'itinéraire d'Antonin,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.17,false,true,263222,Photographs,Photograph,Nubie. Kalabscheh. Sculptures de la Facade postérieure du Temple,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.19,false,true,263224,Photographs,Photograph,Nubie. Hémi-Spéos de Sébour. Pylônes,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.20,false,true,263226,Photographs,Photograph,Nubie. Forteresse D'Ibrym (Ancienne Premmis). Vue prise au sud.,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.21,false,true,263227,Photographs,Photograph,Nubie. Ibsamboul. Partie septentrionale du Spéos d'Hathor,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.22,false,true,263228,Photographs,Photograph,Nubie. Ibsamboul. Entrée du Spéos d'Hathor,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.23,false,true,263229,Photographs,Photograph,Nubie. Vue Cavalière de la Seconde Cataracte,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.24,false,true,263230,Photographs,Photograph,Nubie. Seconde Cataracte. Dgebel-Abouoir,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.25,false,true,263231,Photographs,Photograph,Palestine. Jérusalem. Partie occidentale des Murailles,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.6.26,false,true,263232,Photographs,Photograph,Palestine. Jérusalem. Mosquée d'Omar,,,,,,Artist|Printer,,"Maxime Du Camp|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1822–1894|French, active 1851–55",,"Du Camp, Maxime|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1822 |1851,1894 |1855,1850,1850,1850,Salted paper print (Blanquart-Évrard process) from paper negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.3,false,true,269072,Photographs,Photograph,Homme allongé au pied d'un chàtaignier,,,,,,Artist|Printer,,"Charles Marville|Imprimerie photographique de Blanquart-Évrard, à Lille","French, Paris 1813–1879 Paris|French, active 1851–55",,"Marville, Charles|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1813 |1851,1879 |1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,20.9 x 16.2 cm (8 1/4 x 6 3/8 in.),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.5,false,true,269074,Photographs,Photograph,Allée bordée d'arbres,,,,,,Artist|Printer,,"Charles Marville|Imprimerie photographique de Blanquart-Évrard, à Lille","French, Paris 1813–1879 Paris|French, active 1851–55",,"Marville, Charles|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1813 |1851,1879 |1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.17,false,true,269065,Photographs,Photograph,Carrière,,,,,,Artist|Printer,,"Charles Marville|Imprimerie photographique de Blanquart-Évrard, à Lille","French, Paris 1813–1879 Paris|French, active 1851–55",,"Marville, Charles|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1813 |1851,1879 |1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.4,false,true,269073,Photographs,Photograph,Mt. Liban. Tronc d'un des Cèdres de Salomon,,,,,,Artist|Printer,,"Ernest Benecke|Imprimerie photographique de Blanquart-Évrard, à Lille","German, born England, 1817–1894|French, active 1851–55",,"Benecke, Ernest|Imprimerie photographique de Blanquart-Évrard, à Lille",French,1817 |1851,1894 |1855,1850–53,1850,1853,Salted paper print (Blanquart-Évrard process) from paper negative,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.235,false,true,283727,Photographs,Waxed paper negative,Jacques-Joseph Ebelman on his Deathbed,,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,"March 31, 1852",1852,1852,Waxed paper negative,19 x 24.5 cm (7 1/2 x 9 5/8 in.),"Purchase, The Hite Foundation Gift, 2000",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/283727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.248,false,true,287309,Photographs,Paper negative,[Gardens of the Château de Saint-Cloud],,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,ca. 1853,1851,1855,Paper negative,Sheet: 32.6 x 23.8 cm (12 13/16 x 9 3/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2006",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/287309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.686,false,true,286771,Photographs,Negative; Photograph,"[Village Scene, Brittany]",,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,ca. 1854,1852,1856,Paper negative,Image: 12 3/4 × 10 3/16 in. (32.4 × 25.8 cm) (image only),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/286771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.868,false,true,286247,Photographs,Negative; Photograph,[Fountain at Versailles],,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,ca. 1851,1849,1853,Paper negative,Image: 10 5/16 × 12 11/16 in. (26.2 × 32.3 cm) (image only),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/286247,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.877,false,true,286248,Photographs,Negative; Photograph,[Still Life],,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,1850,1850,1850,Paper negative,Sheet: 10 5/8 × 7 3/8 in. (27 × 18.7 cm) Image: 10 3/8 × 7 1/16 in. (26.4 × 18 cm),"Gilman Collection, Purchase, Anonymous Gifts, by exchange, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/286248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.863,false,true,286571,Photographs,Negative; Photograph,"[Bas-Relief, Arch of Constantine, Rome]",,,,,,Artist,,Frédéric Flachéron,"French, 1813–1883",,"Flachéron, Frédéric",French,1813,1883,1849,1849,1849,Paper negative,Image: 12 3/4 × 9 15/16 in. (32.4 × 25.2 cm),"Gilman Collection, Purchase, Anonymous Gifts, by exchange, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/286571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.208.2,false,true,283722,Photographs,Paper negative,[Boy in Uniform],,,,,,Artist,,Jean-Baptiste Frénet,"French, 1814–1889",,"Frénet, Jean-Baptiste",French,1814,1889,ca. 1855,1853,1857,Paper negative,24.5 x 17.9 cm (9 5/8 x 7 1/16 in. ),"Purchase, Noel and Harriette Levine Gift and Jennifer and Joseph Duke Gift, 2000",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/283722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.21,false,true,269070,Photographs,Photomechanical print,[Man and Boy],,,,,,Artist,,Armand-Hippolyte-Louis Fizeau,"French, 1819–1896",,"Fizeau, Armand-Hippolyte-Louis",French,1819,1896,ca. 1841,1840,1842,Photogravure,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/269070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.122.22,false,true,269071,Photographs,Photomechanical print,"St. Sulpice, Paris",,,,,,Artist,,Armand-Hippolyte-Louis Fizeau,"French, 1819–1896",,"Fizeau, Armand-Hippolyte-Louis",French,1819,1896,ca. 1841,1840,1842,Photogravure,,"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/269071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.475,false,true,282737,Photographs,Album,Le Midi de la France,,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1850s–60s,1850,1869,Albumen silver prints,24.4 x 29.4 x 6.3 cm (9 5/8 x 11 9/16 x 2 1/2 in. ),"Gift of W. Bruce and Delaney H. Lundberg, 1998",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/282737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.562,false,true,267296,Photographs,Photomechanical print,"[Chartres Cathedral, Central Portal of the South Transept; The Last Judgment]",,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,"1855, printed 1857",1855,1855,Photogravure,60.0 x 48.5 cm. (23 5/8 x 19 1/16 in.),"Gift of Charles Isaacs, 1995",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/267296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.1–.175,false,true,286683,Photographs,Portfolio,"Égypte, Nubie, Syrie: Paysages et Monuments",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper prints from paper negatives,49 x 33 x 6 cm (19 5/16 x 13 x 2 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Portfolios,,http://www.metmuseum.org/art/collection/search/286683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.892,false,true,285681,Photographs,Negative; Photograph,[Model of a P.L.M. Locomotive],,,,,,Artist,,Dominique Roman,"French, 1824–1911",,"Roman, Dominique",French,1824,1911,ca. 1855,1853,1857,Paper negative,Image: 12 5/16 × 18 7/8 in. (31.2 × 48 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/285681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.270,false,true,307069,Photographs,Waxed Paper Negative,"[Palace of the Dey of Algiers, Algeria]",,,,,,Artist,,Gustave de Beaucorps,"French, 1825–1906",,"Beaucorps, Gustave de",French,1825,1906,1859,1859,1859,Waxed paper negative,"Image: 28.7 × 38.6 cm (11 5/16 × 15 3/16 in.) Sheet: 29.1 × 39.8 cm (11 7/16 × 15 11/16 in.), irregularly trimmed","The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2013",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/307069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.352 (1–14),false,true,302505,Photographs,Photographs,Souvenir 1870–71,,,,,,Artist,,Ernest Eugène Appert,"French, 1831–1891",,"Appert, Ernest Eugène",French,1831,1891,1870–71,1870,1871,Albumen silver prints from glass negatives,Sheet: 36 x 46 cm (14 3/16 x 18 1/8 in.),"Joyce F. Menschel Photography Library Fund, 2012",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/302505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.483.1–.172,false,true,284718,Photographs,Album,[Album of Paris Crime Scenes],,,,,,Artist,Attributed to,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",French,1853,1914,1901–8,1901,1908,Gelatin silver prints,Overall: 24.3 x 31cm (9 9/16 x 12 3/16in.) Page: 23 x 29 cm (9 1/16 x 11 7/16 in.),"Gilman Collection, Purchase, The Howard Gilman Foundation Gift, 2001",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/284718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1240,false,true,286575,Photographs,Negative; Photograph,The Laundry,,,,,,Artist,,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,1840s,1840,1849,Albumen paper negative,Image: 5 13/16 × 5 5/16 in. (14.7 × 13.5 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/286575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.1–.155,false,true,286390,Photographs,Album,Demi-Monde II 57,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1858–68,1858,1868,Albumen silver print from glass negative,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.371,false,true,283117,Photographs,Photograph; Album,"[Album Containing Photographs, Engravings, Drawings, and Publications Pertaining to Alexandre Dumas]",,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,November 1855,1855,1855,Salted paper print from glass negative,Image: 9 3/8 × 7 5/16 in. (23.8 × 18.5 cm) Sheet: 11 1/2 × 7 5/8 in. (29.2 × 19.4 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/283117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (1-47),false,true,283168,Photographs,Album,Cochinchine et Cambodge,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,44.8 x 37.2 x 3.4 cm (17 5/8 x 14 5/8 x 1 5/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/283168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.590.2,false,true,675524,Photographs,Photograph,"Portail Saint-Trophime; Entrée Du Cloître, Arles, Église Métropolitaine de Saint-Trophime",,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1852,1852,1852,Relief print,Image: 6 15/16 × 5 7/8 in. (17.6 × 15 cm) Mount: 15 7/8 × 7 1/2 in. (40.4 × 19.1 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/675524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.481.18,false,true,301936,Photographs,Daguerreotype,[Woman with Four Children],,,,,,Artist,,Alexandre Bertrand,"French, born 1822",,"Bertrand, Alexandre",French,1822,1922,1850s,1850,1859,Daguerreotype,Image: 14.6 × 11 cm (5 3/4 × 4 5/16 in.) Frame: 37 × 31.5 cm (14 9/16 × 12 3/8 in.),"Gift of Joyce F. Menschel, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.23,true,true,291739,Photographs,Photograph,[The Salon of Baron Gros],,,,,,Artist,,Baron Jean-Baptiste-Louis Gros,"French, 1793–1870",,"Gros, Jean-Baptiste-Louis",French,1793,1870,1850–57,1850,1857,Daguerreotype,Image: 22 x 17.1 cm (8 11/16 x 6 3/4 in.),"Purchase, Fletcher Fund, Joyce F. Menschel Gift, Louis V. Bell Fund, Alfred Stieglitz Society and W. Bruce and Delaney H. Lundberg Gifts, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.69.1,false,true,266980,Photographs,Photograph,[Reclining Female Nude],,,,,,Artist,,Julien Vallou de Villeneuve,"French, 1795–1866",,"Vallou-de-Villeneuve, Julien",French,1795,1866,ca. 1853,1851,1855,Salted paper print from paper negative,Image: 11.8 x 16.0 cm (4 5/8 x 6 5/16 in.),"Purchase, Lila Acheson Wallace Gift, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.69.2,false,true,266981,Photographs,Photograph,[Standing Female Nude],,,,,,Artist,,Julien Vallou de Villeneuve,"French, 1795–1866",,"Vallou-de-Villeneuve, Julien",French,1795,1866,ca. 1853,1851,1855,Salted paper print from paper negative,Image: 12.0 x 16.0 cm. (4 3/4 x 6 5/16 in.),"Purchase, Lila Acheson Wallace Gift, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.69.3,true,true,266982,Photographs,Photograph,"[Female Nude, Reclining, in Profile]",,,,,,Artist,,Julien Vallou de Villeneuve,"French, 1795–1866",,"Vallou-de-Villeneuve, Julien",French,1795,1866,ca. 1853,1851,1855,Salted paper print from paper negative,11.2 x 15.5 cm (4 7/16 x 6 1/8 in.),"Purchase, Lila Acheson Wallace Gift, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.40,false,true,283114,Photographs,Photograph,[Reclining Nude],,,,,,Artist,,Julien Vallou de Villeneuve,"French, 1795–1866",,"Vallou-de-Villeneuve, Julien",French,1795,1866,1851–53,1851,1853,Salted paper print from paper negative,Image: 4 13/16 × 6 5/16 in. (12.3 × 16.1 cm) Mount: 10 11/16 in. × 14 in. (27.2 × 35.5 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.32,false,true,285909,Photographs,Photograph,"[Windmills, Montmartre]",,,,,,Artist,,Hippolyte Bayard,"French, 1801–1887",,"Bayard, Hippolyte",French,1801,1887,1839,1839,1839,Direct positive on paper,"Image: 9.3 × 10.6 cm (3 11/16 × 4 3/16 in.), irregularly trimmed","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.379,false,true,285907,Photographs,Photograph,[Still Life with Statuary],,,,,,Artist,,Hippolyte Bayard,"French, 1801–1887",,"Bayard, Hippolyte",French,1801,1887,Early 1850s,1850,1855,Albumen silver print from glass negative,Image: 26.2 x 20.2 cm (10 5/16 x 7 15/16 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.22,false,true,268620,Photographs,Photograph,The Birth of Venus,,,,,,Artist,,Abel Niépce de St. Victor,"French, 1805–1870",,"Niépce de St. Victor, Abel",French,1805,1870,1855,1855,1855,Photogravure,5.4 x 4.1 cm. (2 1/8 x 1 5/8 in.),"Gift of Harry Stone, 1938",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.189,false,true,285971,Photographs,Photograph,Monsieur Itier's Cange Under Sail on the Nile,,,,,,Artist,,Andre-Victor-Alcide-Jules Itier,"French, 1805–1877",,"Itier, Andre-Victor-Alcide-Jules",French,1805,1877,1845–46,1845,1846,Daguerreotype,Image (visible): 4 3/8 × 5 15/16 in. (11.1 × 15.1 cm) Overall: 6 5/8 × 8 1/4 in. (16.8 × 21 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.39,false,true,283113,Photographs,Photograph,[Gardens of Saint-Cloud],,,,,,Artist,,Henri Victor Regnault,"French, 1810–1878",,"Regnault, Henri Victor",French,1810,1878,before 1855,1855,1855,Salted paper print from paper negative,Image: 16 1/8 × 14 1/16 in. (41 × 35.7 cm) Sheet: 19 in. × 15 3/16 in. (48.3 × 38.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.3,false,true,267230,Photographs,Photograph,[Henriette-Caroline-Victoire Robert],,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,1850s,1850,1859,Salted paper print from paper negative,23.1 x 17.4 cm (9 1/16 x 6 7/8 in.),"Purchase, Joyce and Robert Menschel Gift and The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.104,false,true,267132,Photographs,Photograph,[Gardens of the Chàteau de Saint-Cloud],,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,ca. 1853,1851,1855,Salted paper print from paper negative,31.1 x 20.0 cm. (12 1/4 x 7 7/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1052,false,true,265090,Photographs,Photograph,[Table Top Still Life with Model Cathedral and Small Sculptures],,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,ca. 1856,1854,1858,Salted paper print from glass negative,20.0 x 26.1 cm (7 7/8 x 10 1/4 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1044,true,true,266332,Photographs,Photograph,Alfred Thompson Gobert,,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,1849–55,1849,1855,Salted paper print from paper negative,22.6 x 16.8 cm (8 7/8 x 6 5/8 in.),"Purchase, Joyce and Robert Menschel, Mrs. Harrison D. Horblit and Paul F. Walter Gifts, and Rogers Fund, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.363.2,true,true,271963,Photographs,Photograph,Romesnil,,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,1850–55,1850,1855,Salted paper print from paper negative,27 x 35.2 cm (10 5/8 x 13 7/8 in.),"Purchase, Joseph Pulitzer Bequest, 1996",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.38,false,true,283112,Photographs,Photograph,"[The Large Tree at La Verrerie, Romesnil]",,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,ca. 1852,1850,1854,Salted paper print from paper negative,Image: 10 3/8 × 12 1/2 in. (26.3 × 31.8 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.630,false,true,286246,Photographs,Photograph,"[Village Scene, Brittany]",,,,,,Artist,,Louis-Rémy Robert,"French, 1810–1882",,"Robert, Louis-Rémy",French,1810,1882,ca. 1854,1852,1856,Salted paper print from paper negative,Image: 31.8 x 26.4 cm (12 1/2 x 10 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.334.1,false,true,284087,Photographs,Photograph,"Barricades de la Commune, avril 71. Coin de la place Hotel de Ville & de la rue de Rivoli",,,,,,Artist,,Pierre-Ambrose Richebourg,"French, 1810–1893",,"Richebourg, Pierre-Ambrose",French,1810,1893,1871,1871,1871,Albumen silver print,10.6 x 10 cm (4 3/16 x 3 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1998",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.106,false,true,285343,Photographs,Photograph,[Study of Leaves on a Background of Floral Lace],,,,,,Artist,,Charles Hippolyte Aubry,"French, 1811–1877",,"Aubry, Charles",French,1811,1877,1864,1864,1864,Albumen silver print from glass negative,Image: 46.7 x 36.7 cm (18 3/8 x 14 7/16 in.) Mount: 56.7 x 45.3 cm (22 5/16 x 17 13/16 in.),"Gilman Collection, Purchase, Howard Gilman Foundation Gift, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.600.57,false,true,270200,Photographs,Photograph,[The Tuileries after the Commune],,,,,,Artist,,Hippolyte-Auguste Collard,"French, 1811–1887",,"Collard, Hippolyte-Auguste",French,1811,1887,1871,1871,1871,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1959",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.600.58,false,true,270201,Photographs,Photograph,[The Vendôme Column After Being Torn Down by the Communards],,,,,,Artist,,Hippolyte-Auguste Collard,"French, 1811–1887",,"Collard, Hippolyte-Auguste",French,1811,1887,1871,1870,1879,Albumen silver print from glass negative,21.7 x 30.9 cm (8 9/16 x 12 3/16 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1959",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.600.59,false,true,270202,Photographs,Photograph,[The Hötel de Ville after the Commune],,,,,,Artist,,Hippolyte-Auguste Collard,"French, 1811–1887",,"Collard, Hippolyte-Auguste",French,1811,1887,1871,1871,1871,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1959",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.600.60,false,true,270203,Photographs,Photograph,[The Tuileries After Its Destruction by the Communards],,,,,,Artist,,Hippolyte-Auguste Collard,"French, 1811–1887",,"Collard, Hippolyte-Auguste",French,1811,1887,1871,1871,1871,Albumen silver print from glass negative,20.8 x 27 cm (8 3/16 x 10 5/8 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1959",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.600.61,false,true,270204,Photographs,Photograph,[Barricades pres de Ministere de la Marine et l'Hötel Crillon],,,,,,Artist,,Hippolyte-Auguste Collard,"French, 1811–1887",,"Collard, Hippolyte-Auguste",French,1811,1887,1871,1871,1871,Albumen silver print from glass negative,21.7 x 30.9 cm (8 9/16 x 12 3/16 in. ),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1959",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.436,false,true,285611,Photographs,Photograph,"[Landscape, Arras]",,,,,,Artist,,Adalbert Cuvelier,"French, 1812–1871",,"Cuvelier, Albert",French,1812,1871,1852,1852,1852,Salted paper print from paper negative,Image: 16.3 × 14.1 cm (6 7/16 × 5 9/16 in.) Mount: 20.4 × 17.5 cm (8 1/16 × 6 7/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.1,false,true,282154,Photographs,Photograph,Vue générale de Rouen,,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,21.9 x 33.3 cm (8 5/8 x 13 1/8 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.2,false,true,282155,Photographs,Photograph,Vue générale de la Cathédrale de Rouen,,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,33.6 x 25.1 cm (13 1/4 x 9 7/8 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.3,false,true,282156,Photographs,Photograph,"Bas du Portail, Côté de la Place, Cathédrale de Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,26.2 x 35 cm (10 5/16 x 13 3/4 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.4,false,true,282157,Photographs,Photograph,"Haut de Portail, Côté de la Place, Cathédrale de Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.6 x 26 cm (13 5/8 x 10 1/4 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.5,false,true,282158,Photographs,Photograph,"Portail de la Calende, Rouen Cathédral",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.4 x 26 cm (13 9/16 x 10 1/4 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.6,false,true,282159,Photographs,Photograph,Vue générale de Saint-Ouen,,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,24.8 x 31.1 cm (9 3/4 x 12 1/4 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.7,false,true,282160,Photographs,Photograph,"Portail de Saint-Ouen, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.7 x 26.3 cm (13 11/16 x 10 3/8 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.8,false,true,282161,Photographs,Photograph,"Tour de Saint-Ouen, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,33.7 x 24.9 cm (13 1/4 x 9 13/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.9,false,true,282162,Photographs,Photograph,"Portail des Marmousets, Saint-Ouen de Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34 x 26.1 cm (13 3/8 x 10 1/4 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.10,true,true,282163,Photographs,Photograph,"Saint-Maclou, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.4 x 25.6 cm (13 9/16 x 10 1/16 in.),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.11,false,true,282164,Photographs,Photograph,"Escalier de la Basse Vieille Cour, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,30.5 x 21.9 cm (12 x 8 5/8 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.12,false,true,282165,Photographs,Photograph,"Fontaine de la Croix de Pierre, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,30.9 x 22.6 cm (12 3/16 x 8 7/8 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.13,false,true,267316,Photographs,Photograph,"Cloître Saint-Amand, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–53,1852,1853,Salted paper print from glass negative,34.5 x 26.5 cm (13 9/16 x 10 7/16 in.),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.14,false,true,282166,Photographs,Photograph,"Hôtel du Bourgtheroulde, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,33.6 x 24.9 cm (13 1/4 x 9 13/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.15,false,true,282167,Photographs,Photograph,"Tourelle du Palais de Justice, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.8 x 25.8 cm (13 11/16 x 10 3/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.16,false,true,282168,Photographs,Photograph,"Fragment du Palais de Justice, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.7 x 24.2 cm (13 11/16 x 9 1/2 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.17,false,true,282169,Photographs,Photograph,"Fragment du Palais de Justice, Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.8 x 26 cm (13 11/16 x 10 1/4 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.18,false,true,282170,Photographs,Photograph,"Notre Dame de Bonsecours, près Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,32.8 x 26.1 cm (12 15/16 x 10 1/4 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.19,false,true,282171,Photographs,Photograph,"Saint-Georges de Boscherville, près Rouen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.1 x 25.4 cm (13 7/16 x 10 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.20,false,true,282172,Photographs,Photograph,"Cathédrale de Louviers, vue générale",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,32.6 x 25.4 cm (12 13/16 x 10 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.21,false,true,282173,Photographs,Photograph,Portail de la Cathédrale de Louviers,,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,32.7 x 24.5 cm (12 7/8 x 9 5/8 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.22,false,true,282175,Photographs,Photograph,Portail de la Cathédrale de Louviers,,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.3 x 25.4 cm (13 1/2 x 10 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.23,false,true,282174,Photographs,Photograph,Château de Martainville,,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,24.1 x 31.8 cm (9 1/2 x 12 1/2 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.24,false,true,282176,Photographs,Photograph,"Rue des Petits Murs, Caen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,33.7 x 25.6 cm (13 1/4 x 10 1/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.25,false,true,282177,Photographs,Photograph,"Abside de Saint-Pierre, Caen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,34.2 x 25.3 cm (13 7/16 x 9 15/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.26,false,true,282178,Photographs,Photograph,Vue de l'Odon,,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,33.9 x 25.6 cm (13 3/8 x 10 1/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.27,false,true,282179,Photographs,Photograph,"Poissonerie, Caen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,24.6 x 33.5 cm (9 11/16 x 13 3/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.96.28,false,true,282180,Photographs,Photograph,"Abbaye aux Dames et Hospice, Caen",,,,,,Artist,,Edmond Bacot,"French, 1814–1875",,"Bacot, Edmond",French,1814,1875,1852–54,1852,1854,Salted paper print from glass negative,25.8 x 34.4 cm (10 3/16 x 13 9/16 in. ),"Harris Brisbane Dick Fund, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.28,false,true,283102,Photographs,Photograph,[Dog],,,,,,Artist,,Louis-Auguste Bisson,"French, 1814–1876",,"Bisson, Louis-Auguste",French,1814,1876,1841–49,1841,1849,Daguerreotype,7.6 x 10.3 cm (3 x 4 1/16 in.),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.208.1,false,true,283721,Photographs,Photograph,[Boy in Uniform],,,,,,Artist,,Jean-Baptiste Frénet,"French, 1814–1889",,"Frénet, Jean-Baptiste",French,1814,1889,ca. 1855,1853,1857,Salted paper print from glass negative,24.5 x 17.9 cm (9 5/8 x 7 1/16 in. ),"Purchase, Harriette and Noel Levine Gift and Jennifer and Joseph Duke Gift, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.704.8,false,true,269671,Photographs,Photograph,Colonne Vendôme,,,,,,Artist,,Franck,"French, 1816–1906",,Franck,French,1816,1906,1871,1871,1871,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1953",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.380,false,true,286595,Photographs,Photograph,Canal St. Martin,,,,,,Artist,,Franck,"French, 1816–1906",,Franck,French,1816,1906,1860,1860,1860,Albumen silver print from glass negative,Image: 18.6 x 25.2 cm (7 5/16 x 9 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.98,false,true,288428,Photographs,Photograph,"Deuxième Cataracte, Rocher d'Abouçir, Rapides et Ilots Granitiques",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851,1851,1851,Salted paper print from paper negative,Image: 24.9 x 30.7 cm (9 13/16 x 12 1/16 in.) Mount: 39.9 x 51.9 cm (15 11/16 x 20 7/16 in.),"Purchase, Susan and Thomas Dunn Gift and funds from various donors, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.530,false,true,272026,Photographs,Photograph,"Louksor (Thèbes), Vue Générale des Ruines",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,25.4 x 30.8 cm (10 x 12 1/8 in. ),"Gift of Hans P. Kraus Jr. and Mariana Cooke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/272026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.431,false,true,307071,Photographs,Photograph,"Dandoûr, Nubie",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851,1851,1851,Salted paper print from paper negative,Image: 23.9 × 31.1 cm (9 7/16 × 12 1/4 in.),"Funds from various donors, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.60,false,true,283145,Photographs,Photograph,"Abo-Sembil, Grand Spéos, Statues Colossales vues de Face (Parte Inférieure)",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24.3 x 30.8 cm (9 9/16 x 12 1/8 in.) Mount: 37.9 x 50.1 cm (14 15/16 x 19 3/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.61,false,true,283146,Photographs,Photograph,"Béni Haçan, Architecture Hypogéenne, Tombeau d'Amoneï",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24.4 x 30.6 cm (9 5/8 x 12 1/16 in.) Mount: 40.4 x 52.2 cm (15 7/8 x 20 9/16 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.62,false,true,283147,Photographs,Photograph,"Séboûah, Temple, Colosse et Sphinx de la Partie Gauche de l'Avenue",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24 × 31.1 cm (9 7/16 × 12 1/4 in.) Mount: 37.9 × 50.2 cm (14 15/16 × 19 3/4 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.688,false,true,285952,Photographs,Photograph,"Louksor (Thèbes), Construction Postérieure - Galeries - Parallèles",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24 x 30.2 cm (9 7/16 x 11 7/8 in.) Mount: 40.2 x 52.3 cm (15 13/16 x 20 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.689,false,true,286152,Photographs,Photograph,"Le Kaire, Mosquée Nâcéryeh",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,published 1851,1851,1851,Salted paper print from paper negative,Image: 24 x 30.5 cm (9 7/16 x 12 in.) Mount: 40.3 x 52.2 cm (15 7/8 x 20 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.690,false,true,285955,Photographs,Photograph,"Abou Sembil, Grand Spéos - Statues Colossales, Vues de Trois-Quarts",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 30 x 24.4 cm (11 13/16 x 9 5/8 in.) Mount: 51.6 x 39.9 cm (20 5/16 x 15 11/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.691,false,true,286150,Photographs,Photograph,"Dakkeh, Village et Rives du Nil",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 23.7 x 30.9 cm (9 5/16 x 12 3/16 in.) Mount: 40 x 51.8 cm (15 3/4 x 20 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.693,false,true,285946,Photographs,Photograph,"Louksor (Thèbes), Construction Antérieure - Pylône Colosses et Obélisque",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24.3 x 30.4 cm (9 9/16 x 11 15/16 in.) Mount: 40.3 x 52.1 cm (15 7/8 x 20 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.694,false,true,285945,Photographs,Photograph,"Ile de Fîleh (Philæ), Vue Génèrale Prise du Point I, Sur La Plateforme du Pylône",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24 x 30.5 cm (9 7/16 x 12 in.) Mount: 39.6 x 51.8 cm (15 9/16 x 20 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.695,false,true,285954,Photographs,Photograph,"Louksor, Petit Bras du Nil - Barque de Voyageurs",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24.2 x 30.7 cm (9 1/2 x 12 1/16 in.) Mount: 40.5 x 52.4 cm (15 15/16 x 20 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.696,false,true,285948,Photographs,Photograph,"Abâzîz, Intérieure d'un Village Arabe",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 23.6 x 30.3 cm (9 5/16 x 11 15/16 in.) Mount: 39.7 x 51.7 cm (15 5/8 x 20 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.697,false,true,286091,Photographs,Photograph,"Ile de Fîleh (Philæ), Premier Pylône, Inscription Française Gravée Sur L'Ébrasement Oriental, En M",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 23.7 x 30.6 cm (9 5/16 x 12 1/16 in.) Mount: 39.9 x 51.7 cm (15 11/16 x 20 3/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.925,false,true,286153,Photographs,Photograph,"Sébôuah, Vue Générale du Temple",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,published 1851,1851,1851,Salted paper print from paper negative,Image: 23.8 x 30.6 cm (9 3/8 x 12 1/16 in.) Mount: 39.8 x 51.5 cm (15 11/16 x 20 1/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.940,false,true,286151,Photographs,Photograph,"Louksor (Thèbes), Vue Générale des Ruines",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 23.6 x 30.5 cm (9 5/16 x 12 in.) Mount: 40.2 x 51.8 cm (15 13/16 x 20 3/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.941,false,true,285947,Photographs,Photograph,"Esneh (Latopolis), Construction Ensablée - Architrave, Fûts et Chapiteaux",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 24.9 x 30.7 cm (9 13/16 x 12 1/16 in.) Mount: 40.1 x 51.8 cm (15 13/16 x 20 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.942,false,true,285956,Photographs,Photograph,"Ile de Fîleh (Philæ), Vu Générale Prise du Nord-Ouest au Point A",,,,,,Artist,,Félix Teynard,"French, 1817–1892",,"Teynard, Félix",French,1817,1892,1851–52,1851,1852,Salted paper print from paper negative,Image: 23.9 x 31.2 cm (9 7/16 x 12 5/16 in.) Mount: 39.8 x 51.6 cm (15 11/16 x 20 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.698,false,true,285640,Photographs,Photographs,Young Nuba Woman,,,,,,Artist,,Pierre Trémaux,"French, 1818–1895",,"Trémaux, Pierre",French,1818,1895,1853–54,1853,1854,Salted paper print from paper negative,Image: 10 9/16 × 8 1/4 in. (26.9 × 21 cm) Mount: 13 11/16 × 9 5/16 in. (34.7 × 23.6 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.132,true,true,282234,Photographs,Photograph,The Refectory of the Imperial Asylum at Vincennes,,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1858–59,1858,1859,Salted paper print from glass negative,34.2 x 42.5 cm (13 7/16 x 16 3/4 in.),"Gift of Hans P. Kraus Jr. and Mariana Cook, in honor of André and Marie-Thérèse Jammes, 1998",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.286,false,true,283736,Photographs,Photograph,A Street in Grasse,,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1852,1852,1852,Salted paper print from paper negative,32.9 x 23.8 cm (12 15/16 x 9 3/8 in. ),"Purchase, Jennifer and Joseph Duke Gift and several members of The Chairman's Council Gifts, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.63,false,true,282074,Photographs,Photograph,Tarascon,,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,ca. 1852,1850,1854,Albumen silver print from paper negative,23.6 x 29.5 cm (9 5/16 x 11 5/8 in.),"The Rubel Collection, Purchase, Lila Acheson Wallace Gift, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.52,false,true,283133,Photographs,Photograph,Spartan Soldier,,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1859,1859,1859,Albumen silver print from glass negative,Image: 35.3 x 43.5 cm (13 7/8 x 17 1/8 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.204,false,true,285853,Photographs,Photograph,"Arles, Porte des Châtaignes",,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1852,1850,1854,Salted paper print from a paper negative,Image: 22.1 x 32.1 cm (8 11/16 x 12 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.264,false,true,286170,Photographs,Photograph,"[Lord Brougham and his Family, Cannes]",,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1862,1862,1862,Albumen silver print from glass negative,Image: 24.8 x 34 cm (9 3/4 x 13 3/8 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.314,false,true,286169,Photographs,Photograph,[Still Life with Game Birds],,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1859,1857,1861,Albumen silver print from glass negative,Image: 43.8 x 34.9 cm (17 1/4 x 13 3/4 in.),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.327,false,true,286165,Photographs,Photograph,[The 15th of August. Imperial Asylum at Vincennes],,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1858,1857,1861,Albumen silver print from glass negative,"Image: 32.3 x 44.2 cm (12 11/16 x 17 3/8 in.), oval Mount: 47.5 x 60.5 cm (18 11/16 x 23 13/16 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.328,false,true,286164,Photographs,Photograph,[Self-Portrait in Eastern Costume],,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1855–60,1851,1852,Albumen silver print from glass negative,Image: 18.7 x 13.8 cm (7 3/8 x 5 7/16 in.) Mount: 38.1 x 28.9 cm (15 x 11 3/8 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.560,false,true,286173,Photographs,Photograph,[Family Group],,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1854,1854,1854,Salted paper print from glass negative,Mount: 7 3/8 in. × 8 9/16 in. (18.8 × 21.7 cm) Image: 5 11/16 × 7 1/16 in. (14.4 × 18 cm),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.755,false,true,286171,Photographs,Photograph,"Asile impériale de Vincennes, salle de jeu",,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1858–59,1858,1859,Albumen silver print from glass negative,Image: 8 3/4 × 7 3/16 in. (22.3 × 18.2 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.796,false,true,286167,Photographs,Photograph,"Asile Impériale de Vincennes, la pharmacie",,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1858–59,1858,1859,Albumen silver print from glass negative,"Image: 17 x 17 cm (6 11/16 x 6 11/16 in.), circular","Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.832,false,true,285852,Photographs,Photograph,[Trees and Waterfalls],,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1860–65,1860,1865,Albumen silver print from glass negative,Image: 13.3 x 10; Mount: 18.9 x 15.6,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.590.1a, b",false,true,286168,Photographs,Photograph,"Profil du Portail; Entrée Du Cloitre, Arles, Eglise Metropolitaine de Saint-Trophime",,,,,,Artist,,Charles Nègre,"French, 1820–1880",,"Nègre, Charles",French,1820,1880,1852,1852,1852,Salted paper prints from paper negatives,Image: 6 7/8 × 5 7/8 in. (17.5 × 15 cm) (each) Mount: 15 7/8 × 11 7/16 in. (40.3 × 29 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +40.102.9,false,true,268659,Photographs,Photograph,[Rocky Hillside],,,,,,Artist,,Victor Prevost,"French, 1820–1881",,"Prevost, Victor",French,1820,1881,1850s,1850,1859,Salted paper print from paper negative,Image: 12 5/16 × 9 13/16 in. (31.2 × 24.9 cm),"Gift of John Goldsmith Phillips, 1940",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.13,true,true,283626,Photographs,Photograph,"[Oak Tree and Rocks, Forest of Fontainebleau]",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1849–52,1849,1852,Salted paper print from paper negative,25.2 x 35.7 cm (9 15/16 x 14 1/16 in.),"Purchase, Jennifer and Joseph Duke and Lila Acheson Wallace Gifts, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.646,true,true,261941,Photographs,Photograph,"[The Great Wave, Sète]",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from glass negative,33.7 x 41.4 cm (13 1/4 x 16 5/16 in.),"Gift of John Goldsmith Phillips, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.293,false,true,271964,Photographs,Photograph,"Hotel de Cluny, Paris",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,ca. 1851,1851,1851,Salted paper print from paper negative,32.4 x 24.2 cm (12 3/4 x 9 1/2 in. ),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel and Rogers Fund, 1996",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.257,false,true,284986,Photographs,Photograph,[Soldier and Military Camel],,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1866,1866,1866,Albumen silver print from glass negative,Image: 24.2 x 30.7 cm (9 1/2 x 12 1/16 in.) Mount: 37.7 x 43.4 cm (14 13/16 x 17 1/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2002",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1011,false,true,265065,Photographs,Photograph,"Tree Study, Forest of Fontainebleau",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,ca. 1856,1854,1858,Albumen silver print from glass negative,31.8 x 41.4 cm. (12 1/2 x 16 5/16 in.),"Purchase, Joyce and Robert Menschel, The Howard Gilman Foundation, Harrison D. Horblit, Harriette and Noel Levine and Paul F. Walter Gifts and David Hunter McAlpin Fund; and Gift of Mr. and Mrs. Harry H. Lunn Jr., 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1058,false,true,266351,Photographs,Photograph,Portail milieu d'Aubeterre,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1851,1851,1851,Salted paper print from paper negative,Image: 23.3 x 28.1 cm. (9 3/16 x 11 1/16 in.),"Edward Pearce Casey Fund, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.99.1,false,true,267425,Photographs,Photograph,Mediterranean with Mount Agde,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from two glass negatives,31.8 x 40.9 cm. (12 1/2 x 16 1/8 in.),"Purchase, Joseph Pulitzer Bequest, 1996",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.645.1,false,true,261937,Photographs,Photograph,Brig on the Water,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1856,1856,1856,Albumen silver print from glass negative,32.1 x 40.5 cm (12 5/8 x 15 15/16 in. ),"Gift of A. Hyatt Mayor, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.440.1,false,true,291659,Photographs,Panorama,Pyramides de Gizèh,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1865–69,1865,1869,Albumen silver print from glass negative,Image: 31.1 x 41.8 cm (12 1/4 x 16 7/16 in.) Mount: 45.9 x 62.5 cm (18 1/16 x 24 5/8 in.),"Gift of Robert Shapazian, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.440.2,false,true,291694,Photographs,Panorama,Pyramides de Gizèh,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1865–69,1865,1869,Albumen silver print from glass negative,Image: 31.1 x 41.8 cm (12 1/4 x 16 7/16 in.) Mount: 45.9 x 62.5 cm (18 1/16 x 24 5/8 in.),"Gift of Robert Shapazian, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.440.3,false,true,291695,Photographs,Panorama,Pyramides de Gizèh,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1865–69,1865,1869,Albumen silver print from glass negative,Image: 31.1 x 41.8 cm (12 1/4 x 16 7/16 in.) Mount: 45.9 x 62.5 cm (18 1/16 x 24 5/8 in.),"Gift of Robert Shapazian, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.46,false,true,283122,Photographs,Photograph,Chêne dans les rochers à Fontainebleau,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1849–52,1849,1852,Salted paper print from waxed-paper negative,Image: 25.4 x 36.2 cm (10 x 14 1/4 in.) Mount: 31.8 x 46.2 cm (12 1/2 x 18 3/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.47,false,true,283123,Photographs,Photograph,"Fontainebleau, chemin sablonneux montant",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,ca. 1856,1854,1858,Albumen silver print from glass negative,Image: 29.9 x 37.7 cm (11 3/4 x 14 13/16 in.) Mount: 53.3 x 63.8 cm (21 x 25 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.48,false,true,283124,Photographs,Photograph,Mer Méditerranée - Sète,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from two glass negatives,Image: 32.1 x 41.9 cm (12 5/8 x 16 1/2 in.) Mount: 52.7 × 67.3 cm (20 3/4 × 26 1/2 in.),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.49,false,true,283125,Photographs,Photograph,"[Cavalry Maneuvers, Camp de Châlons]",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from glass negative,Image: 26.7 x 33 cm (10 1/2 x 13 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.37,false,true,306320,Photographs,Photograph,"The French and English Fleets, Cherbourg",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,August 1858,1858,1858,Albumen silver print from glass negative,Mount: 21 in. × 26 3/4 in. (53.3 × 68 cm) Image: 11 13/16 in. × 16 in. (30 × 40.7 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.38,false,true,306321,Photographs,Photograph,"Hollow Oak Tree, Fontainebleau",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1855–57,1855,1857,Albumen silver print from glass negative,Mount: 21 9/16 in. × 27 3/8 in. (54.8 × 69.6 cm) Image: 12 3/8 × 14 13/16 in. (31.5 × 37.7 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.190,false,true,286349,Photographs,Photograph,[View from Photographer's Studio],,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1851–54,1851,1854,Salted paper print from paper negative,26.1 x 35.1 cm (10 1/4 x 13 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.191,false,true,286028,Photographs,Photographs,"Fête de S. A. Ismaïl Pacha à bord des bateaux de LL. A A. les princes, janvier 1867",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1867,1867,1867,Albumen print from paper negative,Image: 30.6 x 40.2 cm (12 1/16 x 15 13/16 in.) Mount: 50 x 64.8 cm (19 11/16 x 25 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.192,false,true,285473,Photographs,Photograph,Temple of Edfu,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1867,1867,1867,Albumen silver print from paper negative,Image: 31.4 x 41 cm (12 3/8 x 16 1/8 in.) Mount: 50.1 x 64.6 cm (19 3/4 x 25 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.258,false,true,285465,Photographs,Photograph,L'impératrice Eugénie en prière,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1856,1856,1856,Albumen silver print from a collodion glass negative,Image: 23.4 x 18.3 cm (9 3/16 x 7 3/16 in.) Mount: 40.1 × 27.5 cm (15 13/16 × 10 13/16 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.273,false,true,285467,Photographs,Photograph,"La Reine Hortense - Yacht de l'empereur, Havre",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1856,1856,1856,Albumen silver print from glass negative,Image: 31.8 x 41.3 cm (12 1/2 x 16 1/4 in.) Mount: 53.1 x 66.3 cm (20 7/8 x 26 1/8 in.),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.274,false,true,285791,Photographs,Photograph,Nu féminin allongé sur un canapé Récamier,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,ca. 1856,1854,1858,Albumen silver print from glass negative,Image: 21.7 x 32.9 cm (8 9/16 x 12 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.311,false,true,285471,Photographs,Photograph,The Salon of 1852,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1852,1852,1852,Salted paper print from waxed-paper negative,Image: 24.1 × 38 cm (9 1/2 × 14 15/16 in.) Mount: 37.5 × 40.9 cm (14 3/4 × 16 1/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.612,false,true,283126,Photographs,Photograph,Scène près d'un étang au moulin du Petit-Mourmelon,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from glass negative,Mount: 18 9/16 × 24 9/16 in. (47.2 × 62.4 cm) Image: 11 in. × 13 7/8 in. (27.9 × 35.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.628,false,true,285461,Photographs,Photograph,"[View of the Seine, Paris]",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from glass negative,Image: 38.5 x 50.9; Mount: 49.5 x 60.7,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.678,false,true,286160,Photographs,Photograph,Portrait de Pitre-Chevalier,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,ca. 1853,1851,1855,Salted paper print from glass negative,"Image: 17.5 x 13.4 cm (6 7/8 x 5 1/4 in.), irregularly trimmed Mount: 17.5 x 13.4 cm (6 7/8 x 5 1/4 in.), irregularly trimmed","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.679,false,true,285792,Photographs,Photograph,Vue de la Plaine de Thèbes prise du temple de Karnac,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1867,1867,1867,Albumen silver print from paper negative,Image: 32 × 41.5 cm (12 5/8 × 16 5/16 in.) Mount: 19 13/16 × 25 9/16 in. (50.3 × 65 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.680,false,true,286350,Photographs,Photograph,[An Italian Street Musician],,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,ca. 1856,1854,1858,Albumen silver print from glass negative,"Image: 34 x 26.4 cm (13 3/8 x 10 3/8 in.), partially obscured by overmat Mat: 49.6 x 38.1 cm (19 1/2 x 15 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.850,false,true,286196,Photographs,Photograph,Portrait de Louis-Napoléon Bonaparte en Prince-Président,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1852,1852,1852,Albumen silver print from paper negative,Image: 20.2 x 14.7 cm (7 15/16 x 5 13/16 in.) Mount: 48.1 x 38.6 cm (18 15/16 x 15 3/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.964,false,true,286195,Photographs,Photographs,La Messe au Camp de Châlons,,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from glass negative,28.6 x 35.8 cm (11 1/4 x 14 1/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.965,false,true,285628,Photographs,Photographs,"[Zouaves, Camp de Châlons]",,,,,,Artist,,Gustave Le Gray,"French, 1820–1884",,"Le Gray, Gustave",French,1820,1884,1857,1857,1857,Albumen silver print from glass negative,Image: 27.9 x 35.6 cm (11 x 14 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.181,false,true,261362,Photographs,Photograph,[La Comtesse in Lace Shawl],,,,,,Artist,,Alphonse (Jean-Baptiste) Bernoud,"French, 1820–1889",,"Bernoud, Alphonse",French,1820,1889,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.1,false,true,289455,Photographs,Photograph,Vue du château de Pau,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm) Image: 9 3/4 × 13 3/4 in. (24.7 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.2,false,true,289456,Photographs,Photograph,Route de Pierrefitte à Luz St Sauveur,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm) Image: 9 3/4 × 13 3/4 in. (24.7 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.3,false,true,289457,Photographs,Photograph,Vallé de Lur prise du chemin de Sasie à St-Sauveur,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1850,1850,1850,Salted paper print from paper negative,Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm) Image: 10 1/4 × 14 7/16 in. (26 × 36.7 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.4,false,true,289458,Photographs,Photograph,Vue des Eaux de Saint-Sauveur,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm) Image: 10 13/16 × 14 5/8 in. (27.4 × 37.1 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.5,false,true,289459,Photographs,Photograph,Vallée d'Argelès près de la ferme de Despourreins. St-Sauveur,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm) Image: 10 9/16 × 14 1/16 in. (26.9 × 35.7 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.6,false,true,289460,Photographs,Photograph,Pont de Sia Route de Gavarnie St Sauveur,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm) Image: 14 1/16 × 10 9/16 in. (35.7 × 26.8 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.7,false,true,289461,Photographs,Photograph,"Le chaos en allant à Gavarnie, St-Sauveur",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 10 7/8 × 14 9/16 in. (27.6 × 37 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.8,false,true,283111,Photographs,Photograph,"Sentier du chaos, St-Sauveur",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 13/16 × 14 1/2 in. (24.9 × 36.9 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.9,false,true,289462,Photographs,Photograph,"Blocs dans le chaos, St-Sauveur",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 10 1/16 × 13 9/16 in. (25.5 × 34.4 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.10,false,true,289463,Photographs,Photograph,"Village de Gèdres, Route de Gavarnie",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm) Image: 9 15/16 × 14 5/16 in. (25.3 × 36.3 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.11,false,true,289464,Photographs,Photograph,"Cirque de Gavarnie, St Sauveur",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 9/16 × 14 9/16 in. (24.3 × 37 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.12,false,true,289465,Photographs,Photograph,"Gorge d'Estaubé prise des gloriettes d'Héas, St-Sauveur",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 5/8 × 14 5/16 in. (24.5 × 36.4 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.13,false,true,289466,Photographs,Photograph,"Vallée et chapelle d'Héas, St-Sauveur",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 1/2 × 14 1/2 in. (24.1 × 36.8 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.14,false,true,289467,Photographs,Photograph,"Cauterets, Pont d'Espagne",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 14 5/16 × 9 1/2 in. (36.4 × 24.2 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.15,false,true,289468,Photographs,Photograph,"Lac de Gaube, Cauterets",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 9/16 × 14 5/16 in. (24.3 × 36.3 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.16,false,true,289469,Photographs,Photograph,Vue de la Vallée de Luchon prise de la tour de Castelvieilh,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 5/8 × 14 5/16 in. (24.5 × 36.4 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.17,false,true,289470,Photographs,Photograph,Bains de la Raillière à Cauterets,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 1/2 × 14 7/16 in. (24.1 × 36.7 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.18,false,true,289471,Photographs,Photograph,"Gorge d'Astos, prise en revenant du lac d'Oo, Luchon",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 3/4 × 14 5/16 in. (24.7 × 36.3 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.19,false,true,289472,Photographs,Photograph,Vallée de Bosost prise de la capilla San Antonio,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 5/8 × 14 9/16 in. (24.4 × 37 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.20,false,true,289473,Photographs,Photograph,"Pont de l'ardoise pris en revenant de la cascade des Parisiens, Luchon",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 5/8 × 13 7/8 in. (24.4 × 35.3 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.21,false,true,289474,Photographs,Photograph,Village de Montaubant pris de Luchon,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 3/4 × 13 3/8 in. (24.8 × 34 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.22,false,true,289475,Photographs,Photograph,"Torrent de la cascade des Demoiselles, Bagnères de Luchon.",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 14 1/8 × 9 13/16 in. (35.8 × 25 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.23,false,true,289476,Photographs,Photograph,"Vue du lac d'Oo ou Seculejo, Bagnère de Luchon",,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 10 1/8 × 14 1/16 in. (25.7 × 35.7 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.503.24,false,true,289477,Photographs,Photograph,Vue de la partie de la Maladetta et des montagnes du haut du port de Vénasque,,,,,,Artist,,Joseph Vigier,"French, 1821–1862",,"Vigier, Joseph",French,1821,1862,1853,1853,1853,Salted paper print from paper negative,Image: 9 5/8 × 14 3/16 in. (24.4 × 36 cm) Sheet: 12 in. × 18 9/16 in. (30.5 × 47.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.1,false,true,287066,Photographs,Photograph,"Vue générale du Kaire, prise de la Mosquée Tegloun",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Mount: 47 x 32 cm (18 1/2 x 12 5/8 in.) Image: 6 1/8 × 5 3/8 in. (15.5 × 13.6 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.2,false,true,287067,Photographs,Photograph,"Vue prise du quartier Franc, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 5 9/16 × 8 1/4 in. (14.2 × 21 cm) Mount: 12 3/16 × 18 11/16 in. (31 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.3,false,true,287068,Photographs,Photograph,"Vue prise d'un Jardin du quartier Franc, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 6 in. × 8 7/8 in. (15.3 × 22.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.5,false,true,287069,Photographs,Photograph,"Dattiers et Maison du quartier Franc, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 8 11/16 × 6 5/16 in. (22 × 16 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.6,false,true,287070,Photographs,Photograph,"Minaret occidental de la Mosqée du Khalif Hakem, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"January 9, 1850",1850,1850,Salted paper print from paper negative,Image: 8 7/8 × 6 1/8 in. (22.5 × 15.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.7,false,true,287071,Photographs,Photograph,"Minaret oriental de la Mosquée du Khalif Hakem, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"December 27, 1849",1849,1849,Salted paper print from paper negative,Image: 8 7/16 × 6 1/8 in. (21.5 × 15.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.8,false,true,287072,Photographs,Photograph,"Sibyl ou Fontaine et Ecole de Souk-el-asr, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 8 3/8 × 6 1/4 in. (21.2 × 15.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.9,false,true,287073,Photographs,Photograph,"Minaret penché de la Mosquée de Bibars, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 8 15/16 × 6 1/4 in. (22.7 × 15.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.10,false,true,287074,Photographs,Photograph,"Vue d'une Mosquée ruinée près de Bab-Saïda, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 6 1/4 × 8 9/16 in. (15.8 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.11,false,true,287075,Photographs,Photograph,"Vue générale de la Mosquée et du Tombeau de Sultan Bezkouk, El-Melek-el-Dâher, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 5/16 in. (15.6 × 21.1 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.12,false,true,287076,Photographs,Photograph,"Entrée du Tombeau de Sultan Bezkouk, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 8 3/4 × 6 5/16 in. (22.3 × 16 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.13,false,true,287077,Photographs,Photograph,"Tombeau du Sultan Kaït-Bay, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 6 in. × 8 9/16 in. (15.3 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.14,false,true,287078,Photographs,Photograph,"Tombeau du Sultan El-Goury, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 11/16 in. (16.6 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.15,false,true,287079,Photographs,Photograph,"Mosquée et Tombeau des Ayoubites, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 6 in. × 8 9/16 in. (15.3 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.16,false,true,287080,Photographs,Photograph,"Tombeau des Sultans Mamelouks, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 6 3/8 × 8 9/16 in. (16.2 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.17,false,true,287081,Photographs,Photograph,Armes et ustensiles du Kaire,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849–January 1850,1849,1850,Salted paper print from paper negative,Image: 8 7/8 × 6 5/16 in. (22.5 × 16 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.18,false,true,287082,Photographs,Photograph,Vue de la grande pyramide (Chéops) prise à l'angle S.E.,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849,1849,1849,Salted paper print from paper negative,Image: 6 3/8 × 8 9/16 in. (16.2 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.19,false,true,287083,Photographs,Photograph,"Vue de la seconde Pyramide, prise au Sud-Est",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849,1849,1849,Salted paper print from paper negative,Image: 5 7/8 × 8 9/16 in. (15 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.20,false,true,287084,Photographs,Photograph,Vue du grand Sphinx et de la grande pyramide de Menkazeh (Mycerinus),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849,1849,1849,Salted paper print from paper negative,Image: 6 3/8 × 8 7/16 in. (16.2 × 21.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.21,false,true,287085,Photographs,Photograph,"Profile du grande Sphinx, pris du Sud",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,December 1849,1849,1849,Salted paper print from paper negative,Image: 6 1/8 × 8 11/16 in. (15.6 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.22,false,true,287086,Photographs,Photograph,Vue prise à Béni-Souef,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1850,1850,1850,Salted paper print from paper negative,Image: 5 3/4 × 8 7/8 in. (14.6 × 22.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.23,false,true,287087,Photographs,Photograph,Vue du Village de Garara,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1850,1850,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 7/16 in. (15.6 × 21.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.24,false,true,287088,Photographs,Photograph,"Tombeau de Sidi-Ambarek, à Garara",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1850,1850,1850,Salted paper print from paper negative,Image: 5 1/2 × 8 3/8 in. (14 × 21.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.25,false,true,287089,Photographs,Photograph,Vue de Djebel-el-teir et du Convent de la Poulie,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1850,1850,1850,Salted paper print from paper negative,Image: 5 5/8 × 8 3/8 in. (14.3 × 21.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.26,false,true,287090,Photographs,Photograph,Vue de Syout - Palais du Pacha,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1850,1850,1850,Salted paper print from paper negative,Image: 5 7/16 × 8 9/16 in. (13.8 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.27,false,true,287091,Photographs,Photograph,"Vue du Divan et du Palais du Gouverneur, à Syout",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 9/16 × 8 1/2 in. (14.2 × 21.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.28,false,true,287092,Photographs,Photograph,Vue générale du Cimetière de Siout,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 9/16 in. (16.7 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.29,false,true,287093,Photographs,Photograph,Tombeaux Musulmans à Siout,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/16 × 8 9/16 in. (16 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.30,false,true,287094,Photographs,Photograph,"Ancienne Nécropole de Lycopolis, à Syout",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/16 × 8 9/16 in. (16 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.31,false,true,287095,Photographs,Photograph,"Mosquée d'El-Arif et Tombeau de Mourad-Bey, à Souhadj",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 1/8 in. × 8 in. (13 × 20.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.32,false,true,287096,Photographs,Photograph,Vue de Girgeh et du littoral enlevé - par l'inondation du Nil,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 5/8 × 8 3/16 in. (14.3 × 20.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.33,false,true,287097,Photographs,Photograph,"Mosquée d'Ali-Bey, à Girgeh",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 3/4 in. (16.3 × 22.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.34,false,true,287098,Photographs,Photograph,Mosquée de Haou (Diospolis parva),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 11/16 × 7 7/8 in. (14.5 × 20 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.35,false,true,287099,Photographs,Photograph,"Vue du Village de Hamarneh, près de Dendérah (Rive droite)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 13/16 × 8 9/16 in. (14.7 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.36,false,true,287100,Photographs,Photograph,"Bois de Dattiers et de Doums, à Hamarneh",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 3/8 × 8 3/8 in. (16.2 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.37,false,true,287101,Photographs,Photograph,Palmiers Doums à Hamarneh,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/8 × 8 3/8 in. (16.9 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.38,false,true,287102,Photographs,Photograph,Façade du Temple d'Athor à Dendérah (Tentyris),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 3/8 in. (16.4 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.39,false,true,287103,Photographs,Photograph,Façade postérieure de grande Temple de Dendérah (Tentyris),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 3/8 in. (16.4 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.40,false,true,287104,Photographs,Photograph,"Hypètre d'Athor, sur la Terrasse du grande Temple de Dendérah (Tentyris)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 3/8 in. (16.4 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.41,false,true,287105,Photographs,Photograph,Bas-reliefs de la façade du Temple de Dendérah (Tentyris),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 9/16 in. (15.5 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.42,false,true,287106,Photographs,Photograph,Sculptures sur la façade postérieure du grande Temple de Dendérah (Tentyris),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 9/16 in. (15.5 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.43,false,true,287107,Photographs,Photograph,"Vue générale des Ruines de Louxor, prise de l'Ouest (Thèbes)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 1/2 × 8 7/16 in. (14 × 21.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.44,false,true,287108,Photographs,Photograph,"Habitation de l'équipage de l'allège de Luxor, bâtie sur la terrasse du Palais",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 13/16 × 8 9/16 in. (14.7 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.45,false,true,287109,Photographs,Photograph,"Pigeonniers bâtis sur la colonnade méridionale du palais d'Aménophis III, à Louxor, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 7/8 × 8 1/2 in. (15 × 21.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.46,false,true,287110,Photographs,Photograph,"Vue d'une partie du village de Louxor, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 5/8 × 8 1/16 in. (14.3 × 20.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.47,false,true,287111,Photographs,Photograph,"Grande Colonnade du Palais d'Aménophis III, à Luxor, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 1/2 in. (16.6 × 21.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.48,false,true,287112,Photographs,Photograph,"Palais et Village de Louxor, pris du Sud, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/16 × 8 3/8 in. (16 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.49,false,true,287113,Photographs,Photograph,"Groupe de colonnes du Palais de Louxor, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 7/16 × 6 9/16 in. (21.4 × 16.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.50,false,true,287114,Photographs,Photograph,"Propylone du Temple de Khons, à Karnac, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 7 11/16 × 6 5/16 in. (19.5 × 16 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.51,false,true,287115,Photographs,Photograph,"Intérieur du Temple de Khons, à Karnac, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 1/4 × 6 1/2 in. (21 × 16.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.52,false,true,287116,Photographs,Photograph,"Vue générale du Temple de Khons, à Karnac, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/16 × 8 1/4 in. (16 × 21 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.53,false,true,287117,Photographs,Photograph,Vue des Pylones du Temple Khons et d'une partie du village de Karnac,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 1/4 × 8 1/8 in. (13.4 × 20.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.54,false,true,287118,Photographs,Photograph,"Vue des propylées du palais de Karnac, prise du Sud-Est",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 1/4 × 8 1/8 in. (13.4 × 20.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.55,false,true,287119,Photographs,Photograph,"Vue générale des ruines du Palais de Karnac, prise du Nord",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 in. × 8 3/8 in. (15.3 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.56,false,true,287120,Photographs,Photograph,"Grand Pylone du Palais de Karnac, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 7/8 × 8 1/8 in. (14.9 × 20.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.57,false,true,287121,Photographs,Photograph,"Cour des bubastites et entrée de la Salle Hypostyle du palais de Karnac, Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 7/16 × 6 9/16 in. (21.5 × 16.6 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.58,false,true,287122,Photographs,Photograph,Ruines de la Salle Hypostyle du Palais de Karnac - Vue prise du Sud-Ouest,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 3/16 × 7 15/16 in. (13.1 × 20.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.59,false,true,287123,Photographs,Photograph,"Vue de la Salle Hypostyle du palais de Karnac, prise sur l'angle N.E.",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 1/4 in. (15.5 × 21 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.60,false,true,287124,Photographs,Photograph,Porte Septentrionale de la Salle Hypostyle du Palais de Karnac,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 1/4 in. (15.5 × 21 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.61,false,true,287125,Photographs,Photograph,Porte méridionale de la Salle Hypostyle du Palais de Karnac (Thèbes),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 in. × 8 3/8 in. (15.3 × 21.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.62,false,true,287126,Photographs,Photograph,Obélisques du Palais de Karnac (Thèbes),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 7 11/16 × 6 5/8 in. (19.5 × 16.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.63,false,true,287127,Photographs,Photograph,Pilier du Sanctuaire de granit du Palais de Karnac (Bas-relief représentant Thotmès III et la Déesse Athor),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 3/16 × 6 1/4 in. (20.8 × 15.8 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.64,false,true,287128,Photographs,Photograph,Pilier du Sanctuaire de granit du Palais de Karnac (Bas-relief représentant le Pharaon Thotmès III et la Déesse Nauth),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 3/16 × 6 1/4 in. (20.8 × 15.8 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.65,false,true,287129,Photographs,Photograph,Sculptures extérieures du Sanctuaire de granit du Palais de Karnac (Thèbes) (Philippe-Aridée conduisant la Barc de Mauth),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 3/8 × 8 1/4 in. (16.2 × 21 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.66,false,true,287130,Photographs,Photograph,Sculptures extérieures du Santuaire de granit du palais de Karnac (Sacre de Philippe-Aridée par les Dieux Thot et Hor-hat),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 1/4 × 6 7/16 in. (21 × 16.3 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.67,false,true,287131,Photographs,Photograph,Sculptures extérieures du Sanctuaire de granit du Palais de Karnac (Ammon assurant la couronne sur la tête de Philippe-Aridée),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/8 in. × 8 in. (15.5 × 20.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.68,false,true,287132,Photographs,Photograph,Ruines du Palais de Karnac - Vue prise à l'extrêmité du Sanctuaire,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 3/8 × 8 7/16 in. (16.2 × 21.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.69,false,true,287133,Photographs,Photograph,Promenoir de Thoutmès III - Dernières galeries du Palais de Karnac (Thèbes),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 13/16 in. × 8 in. (14.8 × 20.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.70,false,true,287134,Photographs,Photograph,Vue générale des Ruines du Palais de Karnac (prise à l'Est) - Thèbes,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 11/16 × 8 7/16 in. (14.4 × 21.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.71,false,true,287135,Photographs,Photograph,Palais de Gournah (Ménephtéun) à Thèbes,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 11/16 × 8 11/16 in. (14.5 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.72,false,true,287136,Photographs,Photograph,"Vue générale des Ruines du Rhamesseum, à Thèbes (Tombeau d'Osymandian)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 11/16 × 8 11/16 in. (14.5 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.73,false,true,286898,Photographs,Photograph,Colosses du Ramesséum,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1850,1850,1850,Salted paper print from paper negative,Image: 16.4 x 21.1 cm (6 7/16 x 8 5/16 in.) Mount: 31.2 x 43.8 cm (12 5/16 x 17 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.74,false,true,287137,Photographs,Photograph,Vue générale de la Nécropole de Thèbes (Gournah),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Mount: Approximately 32 x 47 cm (12 5/8 x 18 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.75,false,true,287138,Photographs,Photograph,"Vue des deux colosses de l'Aménophéum, à Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/8 × 7 7/8 in. (15.5 × 20 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.76,false,true,287139,Photographs,Photograph,"Colosse restauré d' Aménophis III, à Thèbes (Statue vocale ou Colosse de Memnon)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 7 13/16 × 6 5/16 in. (19.8 × 16 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.77,false,true,287140,Photographs,Photograph,"Colosse monolithe d'Amenophis III, à Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 7 11/16 × 6 1/4 in. (19.6 × 15.9 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.78,false,true,287141,Photographs,Photograph,"Siège du colosse monolithe d'Aménophis III, à Thèbes (Détails des sculptures)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 9 1/8 × 6 7/16 in. (23.1 × 16.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.79,false,true,287142,Photographs,Photograph,"Vue générale de Médinet-habou, prise de l'Est (Thèbes)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 1/16 in. (16.3 × 20.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.80,false,true,287143,Photographs,Photograph,"Propylées du Thoutmoséum, à Médinet-habou (Thèbes)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 7/16 × 6 7/16 in. (21.4 × 16.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.81,false,true,287144,Photographs,Photograph,"Pavillon ou Gynecée de Rhamsès-Meiamoun, à Médinet-habou",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/16 × 8 3/4 in. (16 × 22.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.82,false,true,287145,Photographs,Photograph,"Façade latérale du Gynecée de Rhamsès-Meiamoun, Palais de Médinet-Habou, à Thèbes",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 9 in. × 6 9/16 in. (22.8 × 16.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.83,false,true,287146,Photographs,Photograph,"Cour du Palais Rhamsès-Meiamoun, à Médinet-habou (Thèbes) (Restes d'une Eglise Copte)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 3/8 × 8 3/8 in. (16.2 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.84,false,true,287147,Photographs,Photograph,"Piliers dans la cour du palais de Rhamsès-Meiamoun, à Médinet-habou (Thèbes)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 8 3/8 in. × 6 in. (21.2 × 15.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.85,false,true,287148,Photographs,Photograph,"Ruines d'une ville chrétienne, à Médinet-habou (Thèbes)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/2 × 8 15/16 in. (16.5 × 22.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.86,false,true,287149,Photographs,Photograph,Vue du Village d'Herment,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 in. × 8 3/8 in. (15.3 × 21.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.87,false,true,287150,Photographs,Photograph,Ruines du Temple d'Herment (Hermentis),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Mount: Approximately 32 x 47 cm (12 5/8 x 18 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.88,false,true,287151,Photographs,Photograph,Tombeaux Musulmans à Herment,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Mount: Approximately 32 x 47 cm (12 5/8 x 18 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.89,false,true,287152,Photographs,Photograph,"Tombeau de Hadji-Abdallah-el-Marabout, à Herment",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 1/4 × 8 1/2 in. (15.8 × 21.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.90,false,true,287153,Photographs,Photograph,Vue générale d'Esné (No. 1 extrêmité Sud),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 1/2 × 8 1/2 in. (14 × 21.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.91,false,true,287154,Photographs,Photograph,"Vue générale d'Esné (No. 2, partie médiale)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 7/16 × 8 3/4 in. (13.8 × 22.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.92,false,true,287155,Photographs,Photograph,"Vue générale d'Esné (No. 3, pointe Nord)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 7/16 × 7 7/8 in. (13.8 × 20 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.93,false,true,287156,Photographs,Photograph,"Palais de Mehemet-Ali, à Esné",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 13/16 × 8 7/16 in. (14.8 × 21.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.94,false,true,287157,Photographs,Photograph,Vue du Village d'Edfou,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 13/16 × 8 1/8 in. (14.8 × 20.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.95,false,true,287158,Photographs,Photograph,Ruines du Temple de Koum-Ombou (Ombos),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/8 × 8 3/4 in. (16.8 × 22.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.96,false,true,287159,Photographs,Photograph,"Vue de l'île d'Eléphantine, en face d'Assouan",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 11/16 × 8 11/16 in. (14.4 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.97,false,true,287160,Photographs,Photograph,Entrée de la première Cataracte près d'Assouan,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 15/16 × 8 11/16 in. (15.1 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.98,false,true,287161,Photographs,Photograph,Vue prise à la première Cataracte (Rive droite),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/8 × 8 7/8 in. (16.9 × 22.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.99,false,true,287162,Photographs,Photograph,"Vue de la première Cataracte, prise à l'Ouest, entre Assouan et Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 in. × 8 1/4 in. (15.2 × 21 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.100,false,true,287163,Photographs,Photograph,Sortie de la première Cataracte (côté S.E.),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/16 × 8 9/16 in. (16 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.101,false,true,287164,Photographs,Photograph,Vue prise au Nord de Philae - Village de Kounoço,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 11/16 × 8 3/16 in. (14.5 × 20.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.102,false,true,287165,Photographs,Photograph,Vue prise du Nord-Est de Philae - Village de Kolokina,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 11/16 × 8 3/16 in. (14.5 × 20.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.103,false,true,287166,Photographs,Photograph,Vue prise à l'Est de Philae - Village de Abou-Kouli; Route d'Assouan,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 3/8 × 8 3/16 in. (13.6 × 20.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.104,false,true,287167,Photographs,Photograph,Vue prise au Sud-Est de Philae - Cherk-el-Hesseh,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 in. × 7 15/16 in. (15.3 × 20.1 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.105,false,true,287168,Photographs,Photograph,Vue prise au Sud-Est de Philae - Village d'El-Bâb,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 5/8 × 8 1/8 in. (14.3 × 20.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.106,false,true,287169,Photographs,Photograph,"Vue générale de l'île de Philae, prise de la pointe Sud de l'île de Begueh",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 7/16 in. (16.6 × 21.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.107,false,true,287170,Photographs,Photograph,"Vue générale de l'île de Philae, prise de l'île de Begueh (Ouest)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 in. × 7 7/8 in. (15.2 × 20 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.108,false,true,287171,Photographs,Photograph,"Vue générale de l'île de Philae, prise de l'Est",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 5 9/16 × 8 11/16 in. (14.1 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.109,false,true,287172,Photographs,Photograph,"Grand Hypètre ou Typhonium, à Philae - Vue prise de l'Est",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1849–50,1849,1850,Salted paper print from paper negative,Image: 6 5/8 × 8 3/4 in. (16.8 × 22.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.110,false,true,287173,Photographs,Photograph,"Ruines d'un Arc-de-triomphe Romain, à Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 6 3/16 in. × 8 in. (15.7 × 20.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.111,false,true,287174,Photographs,Photograph,Ensemble du Temple d'Isis à Philae - Vue prise au Nord,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 6 in. × 8 3/4 in. (15.2 × 22.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.112,false,true,287175,Photographs,Photograph,"Dromos et Pylones du grand Temple d'Isis, à Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 13, 1850",1850,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 3/4 in. (16.6 × 22.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.113,false,true,287176,Photographs,Photograph,"Colonnade latérale de la cour du Temple d'Isis, à Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 6 5/8 × 8 13/16 in. (16.8 × 22.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.114,false,true,287177,Photographs,Photograph,"Second Pylone du Temple d'Isis, à Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 13, 1850",1850,1850,Salted paper print from paper negative,Image: 9 5/16 × 6 9/16 in. (23.6 × 16.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.115,false,true,287178,Photographs,Photograph,"Proseynème scellé dans le second Pylone du Temple d'Isis, à Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 15, 1850",1850,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 9/16 in. (16.4 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.116,false,true,287179,Photographs,Photograph,"Bas-relief pris sur la muraille occidentale du grand Temple d'Isis, à Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 9 1/4 × 7 1/16 in. (23.5 × 17.9 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.117,false,true,287180,Photographs,Photograph,Thot Ibiocéphale (Dieu des Lettres) à Philae,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 9 1/8 × 6 1/2 in. (23.2 × 16.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.118,false,true,287181,Photographs,Photograph,"Inscription Démotique; second Pylone du Temple d'Isis, à Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 15, 1850",1850,1850,Salted paper print from paper negative,Image: 6 1/4 × 9 1/8 in. (15.8 × 23.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.119,false,true,287182,Photographs,Photograph,"Ruines et Village de Begueh, petite île à l'Ouest de Philae",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 11, 1850",1850,1850,Salted paper print from paper negative,Image: 5 7/8 × 8 3/8 in. (14.9 × 21.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.120,false,true,287183,Photographs,Photograph,"Mosquée de Belal, au Sud de Philae (Rive droite)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 11, 1850",1850,1850,Salted paper print from paper negative,Image: 6 1/4 × 8 9/16 in. (15.9 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.121,false,true,287184,Photographs,Photograph,Vue générale du Temple et du Village de Déboude (Parembole),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 10, 1850",1850,1850,Salted paper print from paper negative,Image: 5 5/8 × 8 11/16 in. (14.3 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.122,false,true,287185,Photographs,Photograph,Pronaos du Temple de Déboude (Parembole),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 10, 1850",1850,1850,Salted paper print from paper negative,Image: 6 5/16 × 8 9/16 in. (16 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.123,false,true,287186,Photographs,Photograph,Vue du Temple de Kardassy,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 9, 1850",1850,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 3/4 in. (16.6 × 22.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.124,false,true,287187,Photographs,Photograph,Vue du Temple de Tafah (Taphis),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 9, 1850",1850,1850,Salted paper print from paper negative,Image: 6 15/16 × 8 9/16 in. (17.7 × 21.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.125,false,true,287188,Photographs,Photograph,"Vue générale du Temple de Kalabcheh (Talmis), prise de la montagne",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 8, 1850",1850,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 11/16 in. (16.6 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.126,false,true,287189,Photographs,Photograph,Temple de Kalabcheh - Entre colonnement médial du Pronaos,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 8, 1850",1850,1850,Salted paper print from paper negative,Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.127,false,true,287190,Photographs,Photograph,Ptolémée-Cæsarion - Bas-relief du Temple de Kalabcheh (Talmis),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 8, 1850",1850,1850,Salted paper print from paper negative,Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.128,false,true,287191,Photographs,Photograph,Isis et Horus-Arsiési - Bas-relief du Temple de Kalabcheh (Talmis),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 8, 1850",1850,1850,Salted paper print from paper negative,Image: 8 9/16 × 6 7/16 in. (21.7 × 16.3 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.129,false,true,287192,Photographs,Photograph,Vue du Village d'Abou-hor (Tropique du Cancer),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 5 3/4 × 8 9/16 in. (14.6 × 21.8 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.130,false,true,287193,Photographs,Photograph,Vue prise au Village d'Abou-hor (Tropique du Cancer),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 6 1/16 × 8 13/16 in. (15.4 × 22.4 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.131,false,true,287194,Photographs,Photograph,Propylon du Temple de Dandour (Tropique du Cancer),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,April 1850,1850,1850,Salted paper print from paper negative,Image: 8 7/8 × 6 5/8 in. (22.5 × 16.8 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.132,false,true,287195,Photographs,Photograph,Vue du pronaos du Temple de Dandour (Tropique du Cancer),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 7, 1850",1850,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 1/2 in. (16.6 × 21.6 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.133,false,true,287196,Photographs,Photograph,"Vue générale du Temple de Dakkeh (Pselcis), prise au Nord",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 5, 1850",1850,1850,Salted paper print from paper negative,Image: 6 in. × 8 7/16 in. (15.3 × 21.4 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.134,false,true,287197,Photographs,Photograph,Vue de la façade du pronaos du Temple de Dakkeh (Pselcis),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 5, 1850",1850,1850,Salted paper print from paper negative,Image: 6 3/4 × 8 3/4 in. (17.1 × 22.3 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.135,false,true,287198,Photographs,Photograph,Vue du Temple de Maharakka (Hiéra-Sycaminos),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 5, 1850",1850,1850,Salted paper print from paper negative,Image: 6 3/16 × 8 3/4 in. (15.7 × 22.2 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.136,false,true,287199,Photographs,Photograph,Dromos du Temple de Sébona,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 3, 1850",1850,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 15/16 in. (15.5 × 22.7 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.137,false,true,287200,Photographs,Photograph,Pylones du Temple de Sébona,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 3, 1850",1850,1850,Salted paper print from paper negative,Image: 6 1/2 × 8 3/4 in. (16.5 × 22.3 cm) Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.138,false,true,287201,Photographs,Photograph,Vue du Temple d'Amada - Coupole ruinée d'une Eglise Copte,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"April 2, 1850",1850,1850,Salted paper print from paper negative,Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.139,false,true,287202,Photographs,Photograph,Vue de la Fortresse d'Ibrym,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"March 31, 1850",1850,1850,Salted paper print from paper negative,Image: 8 7/8 × 6 9/16 in. (22.5 × 16.7 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.140,false,true,287203,Photographs,Photograph,"Vue générale des Spéos de Phré et d'Athor, à Abousembil, prise de l'île",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"March 29, 1850",1850,1850,Salted paper print from paper negative,Image: 5 1/4 × 8 7/16 in. (13.3 × 21.5 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.141,false,true,287204,Photographs,Photograph,"Entrée du Spéos d'Athor, à Abousembil",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 8 5/8 × 6 3/8 in. (21.9 × 16.2 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.142,false,true,287205,Photographs,Photograph,"Moitié de la façade du Spéos d'Athor, à Abousembil (partie Septentrionale)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 6 3/4 × 9 1/16 in. (17.2 × 23 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.143,false,true,287206,Photographs,Photograph,"Moitié de la façade du Spéos d'Athor, à Abousembil (partie méridionale)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"March 28, 1850",1850,1850,Salted paper print from paper negative,Image: 6 5/8 × 8 11/16 in. (16.9 × 22.1 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.144,false,true,287207,Photographs,Photograph,"Vue de la façade du Spéos de Phré, à Abousembil",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 6 5/8 × 8 11/16 in. (16.9 × 22.1 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.145,false,true,287208,Photographs,Photograph,"Colosse oriental du Spéos de Phré, à Abousembil (Portrait de Rhamsès-le-grand)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 8 7/16 × 6 9/16 in. (21.5 × 16.6 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.146,false,true,287209,Photographs,Photograph,"Profil du Colosse oriental du Spéos de Phré, à Abousembil (Portrait de Rhamsès-le-grand)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"March 29, 1850",1850,1850,Salted paper print from paper negative,Image: 8 7/16 × 6 9/16 in. (21.5 × 16.6 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.147,false,true,287210,Photographs,Photograph,Décoration de l'entrée du grand Spéos d'Abousembil (Le Dieu Phré recevant les offrandes de Rhamsès-le-grand),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 8 11/16 × 6 5/8 in. (22 × 16.8 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.148,false,true,287211,Photographs,Photograph,"Colosse médial du Spéos d'Phré, à Abousembil",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"March 29, 1850",1850,1850,Salted paper print from paper negative,Image: 8 3/8 × 6 5/8 in. (21.2 × 16.8 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.149,false,true,283143,Photographs,Photograph,"Westernmost Colossus of the Temple of Re, Abu Simbel",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,1850,1850,1850,Salted paper print from paper negative,Image: 9 in. × 6 1/2 in. (22.8 × 16.5 cm) Mount: 18 11/16 × 12 5/16 in. (47.5 × 31.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.150,false,true,287212,Photographs,Photograph,"Vue cavalière de la seconde cataracte, prise du haut de Djebel-Aboucir",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"March 24, 1850",1850,1850,Salted paper print from paper negative,Image: 7 7/8 × 6 5/16 in. (20 × 16 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.151,false,true,287213,Photographs,Photograph,Vue des rapides de la Seconde Cataracte,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 4 13/16 × 8 1/2 in. (12.3 × 21.6 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.152,false,true,287214,Photographs,Photograph,Vue prise à la Seconde Cataracte,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 5 1/2 × 8 11/16 in. (14 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.3 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.153,false,true,287215,Photographs,Photograph,Batu-el-Hadjar. Vue prise à la Seconde Cataracte,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,March 1850,1850,1850,Salted paper print from paper negative,Image: 6 in. × 8 7/16 in. (15.2 × 21.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.154,false,true,287216,Photographs,Photograph,Djebel-Aboucir - Rive gauche de la Seconde Cataracte,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"March 25, 1850",1850,1850,Salted paper print from paper negative,Image: 6 in. × 8 11/16 in. (15.2 × 22 cm) Mount: 12 5/16 in. × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.155,false,true,287217,Photographs,Photograph,"Vue générale des ruines de Baâlbek, prise à l'Est",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,September 1850,1850,1850,Salted paper print from paper negative,Image: 16.2 × 21.7 cm (6 3/8 × 8 9/16 in.) Mount: 12 5/8 × 18 1/2 in. (32 × 47 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.156,false,true,287218,Photographs,Photograph,Intérieur de l'enceinte du Temple de Baalbek (Héliopolis),,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"September 15, 1850",1850,1850,Salted paper print from paper negative,Image: 6 1/4 × 8 1/8 in. (15.8 × 20.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.157,false,true,287219,Photographs,Photograph,"Hémicycle de l'enceinte des Temples, à Baâlbek",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"September 15, 1850",1850,1850,Salted paper print from paper negative,Image: 8 5/8 × 6 1/2 in. (21.9 × 16.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.158,false,true,287220,Photographs,Photograph,"Colonnade du Temple du Soleil, à Baâlbek (Héliopolis)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"September 15, 1850",1850,1850,Salted paper print from paper negative,Image: 6 9/16 × 8 1/4 in. (16.7 × 21 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.159,false,true,287221,Photographs,Photograph,"Colonnade du Temple du Soleil, à Baâlbek (Héliopolis)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,September 1850,1850,1850,Salted paper print from paper negative,Image: 8 9/16 × 6 9/16 in. (21.7 × 16.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.160,false,true,287222,Photographs,Photograph,"Porte du Temple de Jupiter, à Baalbek (Héliopolis)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,September 1850,1850,1850,Salted paper print from paper negative,Image: 8 3/4 × 6 9/16 in. (22.3 × 16.7 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.161,false,true,287223,Photographs,Photograph,"Colonnade intérieure du Naos du Temple de Jupiter, à Baâlbek (Héliopolis)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"September 15, 1850",1850,1850,Salted paper print from paper negative,Image: 8 3/4 × 6 1/2 in. (22.2 × 16.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.162,false,true,287224,Photographs,Photograph,"Temple de Jupiter, à Baâlbek (Héliopolis)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"September 15, 1850",1850,1850,Salted paper print from paper negative,Image: 8 3/4 × 6 1/2 in. (22.2 × 16.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.163,false,true,287225,Photographs,Photograph,"Vue du Temple de Jupiter, à Baâlbek (Héliopolis)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"September 15, 1850",1850,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 3/8 in. (16.4 × 21.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.164,false,true,287226,Photographs,Photograph,"Colonnade occidental du Temple de Jupiter, à Baâlbek (Héliopolis)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"September 15, 1850",1850,1850,Salted paper print from paper negative,Image: 6 11/16 × 8 11/16 in. (17 × 22 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.165,false,true,287227,Photographs,Photograph,Château de David (Daoud Kalessy) et murailles de Jérusalem,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 6 1/8 × 8 9/16 in. (15.5 × 21.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.166,false,true,287228,Photographs,Photograph,Koubbé-Nébi-Monça - Coupole de Moïse à Jérusalem,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 6 1/4 × 9 3/16 in. (15.8 × 23.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.167,false,true,287229,Photographs,Photograph,Vue prise au Nord-Ouest de Jérusalem,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 6 1/4 × 9 3/16 in. (15.8 × 23.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.168,false,true,287230,Photographs,Photograph,Vue de la Piscine Probatique et d'un quartier de Jérusalem,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 6 1/4 × 9 3/16 in. (15.8 × 23.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.169,false,true,287231,Photographs,Photograph,Vue de la Mosquée d'El-Melouyeh et d'un quartier de Jérusalem,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 6 7/16 × 8 1/4 in. (16.4 × 21 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.170,false,true,287232,Photographs,Photograph,"Façade de l'Eglise du St. Sépulcre, à Jérusalem (No. 1)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"August 19, 1850",1850,1850,Salted paper print from paper negative,Image: 6 11/16 × 9 3/8 in. (17 × 23.8 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.171,false,true,287233,Photographs,Photograph,"Façade de l'Eglise du St. Sépulcre, à Jérusalem (No. 2 partie supérieure)",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 6 11/16 × 9 3/16 in. (17 × 23.4 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.172,false,true,287234,Photographs,Photograph,"La Mosquée d'Omar, à Jérusalem",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 6 11/16 × 9 1/4 in. (17 × 23.5 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.173,false,true,287235,Photographs,Photograph,La Porte dorée à Jérusalem,,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,August 1850,1850,1850,Salted paper print from paper negative,Image: 9 5/16 × 6 3/8 in. (23.7 × 16.2 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.376.174,false,true,287236,Photographs,Photograph,"Mosquée de Sultan Haçan, Place de Roumelich, au Kaire",,,,,,Artist,,Maxime Du Camp,"French, 1822–1894",,"Du Camp, Maxime",French,1822,1894,"December 13, 1849",1849,1849,Salted paper print from paper negative,Image: 8 1/2 × 6 7/16 in. (21.6 × 16.3 cm) Mount: 12 5/16 × 18 11/16 in. (31.2 × 47.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.538,false,true,285271,Photographs,Photograph,Viscountess Vilain,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1857,1857,1857,Salted paper print from glass negative,Image: 29.1 x 22.5 cm (11 7/16 x 8 7/8 in.),"Purchase, Harriette and Noel Levine Gift, 2003",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.1,false,true,261268,Photographs,Photograph,L'Ecstase,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.3,false,true,261478,Photographs,Photograph,Aux écoutes,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.4,false,true,261489,Photographs,Photograph,L'Interrogation,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.5,false,true,261500,Photographs,Photograph,Les beau décolleté,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.6,false,true,261511,Photographs,Photograph,Méditation,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.7,false,true,261522,Photographs,Photograph,Le beau bras,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.8,false,true,261533,Photographs,Photograph,La Psyché,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.9,false,true,261544,Photographs,Photograph,Réverie,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.13,false,true,261302,Photographs,Photograph,Derelitta,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 10.8 cm. (2 7/8 x 4 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.14,false,true,261313,Photographs,Photograph,Derelitta,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 10.2 cm. (2 7/8 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.15,false,true,261324,Photographs,Photograph,Derelitta,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.8 x 9.5 cm. (3 1/16 x 3 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.16,false,true,261335,Photographs,Photograph,Stella,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 10.2 cm. (3 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.17,false,true,261346,Photographs,Photograph,La robe de moiré,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 10.2 cm (2 7/8 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.18,false,true,261359,Photographs,Photograph,Stella (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 10.2 cm. (2 7/8 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.19,false,true,261371,Photographs,Photograph,Les épaules tombantes,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 10.2 cm. (3 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.20,false,true,261391,Photographs,Photograph,Bal,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.9 x 10.8 cm. (3 1/8 x 4 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.21,false,true,261402,Photographs,Photograph,Bal,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 10.2 cm. (2 7/8 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.22,false,true,261413,Photographs,Photograph,Lucréce (ou la Vestale),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 10.2 cm. (3 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.23,false,true,261424,Photographs,Photograph,Les rubans découpé,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,6.7 x 9.5 cm. (2 5/8 x 3 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.24,false,true,261435,Photographs,Photograph,Le dos,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 10.2 cm. (3 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.25,false,true,261446,Photographs,Photograph,Le regard,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.26,false,true,261457,Photographs,Photograph,Le chapeau à brides,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 11.7 cm. (3 x 4 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.27,false,true,261468,Photographs,Photograph,Le chapeau à brides,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 11.1 cm. (2 7/8 x 4 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.28,false,true,261476,Photographs,Photograph,L'Espagnole,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,6.7 x 8.9 cm. (2 5/8 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.29,false,true,261477,Photographs,Photograph,L'Ancre,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.4 x 8.6 cm. (2 1/8 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.30,false,true,261479,Photographs,Photograph,L'Orage,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,6.0 x 8.6 cm. (2 3/8 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.31,false,true,261480,Photographs,Photograph,L'accoudée,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1856–57,1856,1857,Albumen silver print from glass negative,11.1 x 7.6 cm (4 3/8 x 3 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.32,false,true,261481,Photographs,Photograph,Priére,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 10.5 cm. (2 7/8 x 4 1/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.33,false,true,261482,Photographs,Photograph,La robe d'été,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 11.4 cm. (3 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.34,false,true,261483,Photographs,Photograph,La Frayeur,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 10.8 cm. (3 7/8 x 4 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.35,false,true,261484,Photographs,Photograph,La peignoir plisié,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.1 cm. (3 3/8 x 4 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.36,false,true,261485,Photographs,Photograph,Le peignoir plisié,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.2 x 10.2 cm. (4 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.37,false,true,261486,Photographs,Photograph,Le Chapelet,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.9 x 12.4 cm. (3 1/2 x 4 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.38,false,true,261487,Photographs,Photograph,La robe de soie,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.4 cm. (3 3/8 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.39,false,true,261488,Photographs,Photograph,Petite Reine d'Etrurie,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.9 x 11.7 cm. (3 1/2 x 4 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.40,false,true,261490,Photographs,Photograph,La Nonne blanche,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.41,false,true,261491,Photographs,Photograph,La casagne de velours,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.42,false,true,261492,Photographs,Photograph,Le chàle de dentelles,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.43,false,true,261493,Photographs,Photograph,Beatrice,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.44,false,true,261494,Photographs,Photograph,La robe bouffante,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.45,false,true,261495,Photographs,Photograph,La cape,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.46,false,true,261496,Photographs,Photograph,Funerale,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.47,false,true,261497,Photographs,Photograph,Funerale,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.48,false,true,261498,Photographs,Photograph,Le chapeau à plumes,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.49,false,true,261499,Photographs,Photograph,Le peignoir plisie (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.50,false,true,261501,Photographs,Photograph,L'Agrèable,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.51,false,true,261502,Photographs,Photograph,Costigliole,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.52,false,true,261503,Photographs,Photograph,"Le noeud de dentelle. ""Ritrosetta""",,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.53,false,true,261504,Photographs,Photograph,"Le noeud de dentelle. ""Ritrosetta""",,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.54,false,true,261505,Photographs,Photograph,Le Chapelet (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.55,false,true,261506,Photographs,Photograph,Le voile,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.56,false,true,261507,Photographs,Photograph,L'hermine,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.57,false,true,261508,Photographs,Photograph,L'hermine,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,Image: 12.2 x 18.6 cm (4 13/16 x 7 5/16 in.) Mount: 17.4 x 13.6 cm (6 7/8 x 5 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.58,false,true,261509,Photographs,Photograph,Le chapelet (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.59,false,true,261510,Photographs,Photograph,Le chapeau à plume (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.60,false,true,261512,Photographs,Photograph,"La laçon de dessin. ""L'artiste""",,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.61,false,true,261513,Photographs,Photograph,Le Repos,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.2 x 7.7 cm (4 x 3 1/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.62,false,true,261514,Photographs,Photograph,L'Algérienne,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.5 x 11.4 cm (4 1/8 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.63,false,true,261515,Photographs,Photograph,Repos (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.2 x 7.6 cm (4 x 3 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.64,false,true,261516,Photographs,Photograph,Convalescente (colorieè),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1861–67,1861,1867,Albumen silver print from glass negative overpainted with watercolor,22.1 x 16.2 cm (8 11/16 x 6 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.65,false,true,261517,Photographs,Photograph,L'Allongée,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.5 x 10.6 cm (2 15/16 x 4 3/16 in.) oval,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.67,false,true,261519,Photographs,Photograph,Repos (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.69,false,true,261521,Photographs,Photograph,La Chemise,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Salted paper print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.70,false,true,261523,Photographs,Photograph,La Coucher,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Salted paper print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.71,false,true,261524,Photographs,Photograph,La robe de taffetas,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.72,false,true,261525,Photographs,Photograph,La robe de taffetas,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.73,false,true,261526,Photographs,Photograph,Les yeux mirés,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,Image: 17.4 x 12.8 cm (6 7/8 x 5 1/16 in.) Mount: 23.5 x 20.6 cm (9 1/4 x 8 1/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.74,false,true,261527,Photographs,Photograph,Les yeux mirés,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.78,false,true,261531,Photographs,Photograph,Marie Stuart,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.79,false,true,261532,Photographs,Photograph,Marie Stuart,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.80,false,true,261534,Photographs,Photograph,Marie Stuart,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.81,false,true,261535,Photographs,Photograph,Cauchoise,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.82,false,true,261536,Photographs,Photograph,La Marquise Mathilde,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.83,false,true,261537,Photographs,Photograph,Cauchoise (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.84,false,true,261538,Photographs,Photograph,Mathilde (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.85,false,true,261539,Photographs,Photograph,Mathilde,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.86,false,true,261540,Photographs,Photograph,Mathilde,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.87,false,true,261541,Photographs,Photograph,Judith,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.88,false,true,261542,Photographs,Photograph,Judith,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.89,false,true,261543,Photographs,Photograph,Judith,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.90,false,true,261545,Photographs,Photograph,Judith,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.91,false,true,261546,Photographs,Photograph,Judith,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.92,false,true,261547,Photographs,Photograph,Judith,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.93,false,true,261548,Photographs,Photograph,Reine d'Etrurie (colorieè),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.94,false,true,261549,Photographs,Photograph,Reine d'Etrurie (colorieè),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Salted paper print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.96,false,true,261551,Photographs,Photograph,Livetta,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.97,false,true,261552,Photographs,Photograph,Nonne blanche (tete),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.98,false,true,261553,Photographs,Photograph,Nonne blanche (en pied),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.99,false,true,261554,Photographs,Photograph,Soeur Elize,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.100,false,true,261270,Photographs,Photograph,Le noeud rouge,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.101,false,true,261271,Photographs,Photograph,"La Bisi. Boudoir, robe velour gris, moire rose, fleurs roses, feuilles grises (de sa mai, au revers.)",,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,11.4 x 10.7 cm (4 1/2 x 4 3/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.116,false,true,261287,Photographs,Photograph,Ritrosetta,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1861–64,1861,1864,Salted paper print from glass negative with applied color,14.5 x 13.1 cm (5 11/16 x 5 3/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.118,false,true,261289,Photographs,Photograph,Derelitta (peintre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.119,false,true,261290,Photographs,Photograph,La Frayeur,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1861–67,1861,1867,Salted paper print from glass negative with applied color,12.7 x 15.1 cm (5 x 5 15/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.120,false,true,261292,Photographs,Photograph,Virginie,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Salted paper print from glass negative with applied color,8.7 x 13.6 cm (3 7/16 x 5 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.127,false,true,261299,Photographs,Photograph,[Study of Legs],,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1861–67,1861,1867,Albumen silver print from glass negative,11.4 x 13.7 cm (4 1/2 x 5 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.128,false,true,261300,Photographs,Photograph,Les jambes,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,11.7 x 13.0 cm (4 5/8 x 5 1/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.129,false,true,261301,Photographs,Photograph,Les jambes,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,11.1 x 14.9 cm (4 3/8 x 5 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.130,false,true,261303,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.131,false,true,261304,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.132,false,true,261305,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.133,false,true,261306,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.134,false,true,261307,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.135,false,true,261308,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.136,false,true,261309,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.137,false,true,261310,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.138,false,true,261311,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.139,false,true,261312,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.140,false,true,261314,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.141,false,true,261315,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.142,false,true,261316,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.143,false,true,261317,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.144,false,true,261318,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.145,false,true,261319,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.146,false,true,261320,Photographs,Photograph,Sèriè à la Ristori,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.147,false,true,261321,Photographs,Photograph,Le pardessus dècoré,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.148,false,true,261322,Photographs,Photograph,Le pardessus dècoré,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.149,false,true,261323,Photographs,Photograph,Le pardessus dècoré,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.150,false,true,261325,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.151,false,true,261326,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.152,false,true,261327,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.153,false,true,261328,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.154,false,true,261329,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.155,false,true,261330,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.156,false,true,261331,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.157,false,true,261332,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.158,false,true,261333,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.159,false,true,261334,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.160,false,true,261336,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.161,false,true,261337,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.162,false,true,261338,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.163,false,true,261339,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.164,false,true,261340,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.165,false,true,261341,Photographs,Photograph,Le Caracul (L'Astrakhan),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.166,false,true,261342,Photographs,Photograph,Les étoiles de jois,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.167,false,true,261343,Photographs,Photograph,Les edoiles de jois,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.168,false,true,261344,Photographs,Photograph,Les étoiles de jois,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.169,false,true,261345,Photographs,Photograph,Les étoiles de jois,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.170,false,true,261347,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Gelatin silver print,13.7 x 10.2 cm (5 3/8 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.171,false,true,261348,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.2 x 14.3 cm (4 x 5 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.172,false,true,261349,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Gelatin silver print,13.7 x 9.8 cm (5 3/8 x 3 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.173,false,true,261350,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,11.7 x 8.9 cm (4 5/8 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.174,false,true,261352,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.2 x 13.7 cm (4 x 5 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.175,false,true,261354,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.2 x 13.3 cm (3 5/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.176,false,true,261355,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1875–80,1875,1880,Albumen silver print from glass negative,22 x 16.8 cm (8 11/16 x 6 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.177,false,true,261356,Photographs,Photograph,Les dernieres,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.178,false,true,261357,Photographs,Photograph,Les dernieres,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.179,false,true,261358,Photographs,Photograph,Les dernieres,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.180,false,true,261360,Photographs,Photograph,Les dernieres,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.185,false,true,261366,Photographs,Photograph,[Reine d'Etrurie],,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1863–67,1863,1867,Albumen silver print from glass negative retouched with gouache,12.4 x 8.9 cm (4 7/8 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.186,false,true,261367,Photographs,Photograph,[Reine d'Etrurie],,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative with applied color,12.3 x 8.9 cm (4 13/16 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.187,false,true,261368,Photographs,Photograph,[Reine d'Etrurie],,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative with applied color,12.3 x 8.9 cm (4 13/16 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261368,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.188,false,true,261369,Photographs,Photograph,[Reine d'Etrurie],,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative with applied color,12.4 x 8.9 cm (4 7/8 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.194,false,true,261384,Photographs,Photograph,L'Accouchée,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Salted paper print from glass negative,18.1 x 22.5 cm. (7 1/8 x 8 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.195,false,true,261385,Photographs,Photograph,En famille,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Salted paper print from glass negative,17.8 x 21.9 cm. (7 x 8 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.196,false,true,261386,Photographs,Photograph,La Mere et L'Epoux,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,13.3 x 16.8 cm. (5 1/4 x 6 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.197,false,true,261387,Photographs,Photograph,La Colombe et le Tigre,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,14.9 x 17.8 cm. (5 7/8 x 7 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.198,false,true,261388,Photographs,Photograph,La capuche,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 12.7 cm. (3 7/8 x 5 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.199,false,true,261389,Photographs,Photograph,La petite chemise,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.9 x 11.4 cm. (3 1/2 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.200,false,true,261392,Photographs,Photograph,Le derrière,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.5 x 12.7 cm. (3 3/4 x 5 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.201,false,true,261393,Photographs,Photograph,La ?aurier rose,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.8 x 13.0 cm. (4 1/4 x 5 1/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.202,false,true,261394,Photographs,Photograph,La veste de cygne,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.203,false,true,261395,Photographs,Photograph,Autre chaise rustique,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.7 x 7.6 cm. (2 1/4 x 3 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.204,false,true,261396,Photographs,Photograph,Le bournous (colorie),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,10.8 x 13.0 cm. (4 1/4 x 5 1/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.205,false,true,261397,Photographs,Photograph,Le treillage,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.0 x 9.8 cm. (2 3/4 x 3 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.206,false,true,261398,Photographs,Photograph,La veste de Cygne (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 2.5 cm. (2 x 1 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.207,false,true,261399,Photographs,Photograph,La veste de Cygne,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.208,false,true,261400,Photographs,Photograph,Le dos,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.3 x 10.2 cm. (3 1/4 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.209,false,true,261401,Photographs,Photograph,Encore la chaise rustique,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,11.1 x 14.0 cm. (4 3/8 x 5 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.210,false,true,261403,Photographs,Photograph,Le reflet (profile),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 10.2 cm. (3 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.212,false,true,261404,Photographs,Photograph,Le fauteuil,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,6.0 x 9.5 cm. (2 3/8 x 3 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.213,false,true,261406,Photographs,Photograph,La fouriure,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,15.9 x 9.5 cm. (6 1/4 x 3 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.214,false,true,261407,Photographs,Photograph,La fouriure,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.3 x 10.2 cm. (2 7/8 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.215,false,true,261408,Photographs,Photograph,La frisure,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 9.8 cm. (3 x 3 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.216,false,true,261409,Photographs,Photograph,Les jambes croisées,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.9 x 12.4 cm. (3 1/2 x 4 7/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.217,false,true,261410,Photographs,Photograph,L'Ecossais,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.4 cm. (3 3/8 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261410,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.218,false,true,261411,Photographs,Photograph,Le liseur,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.219,false,true,261412,Photographs,Photograph,L'Echevelé,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.220,false,true,261414,Photographs,Photograph,Le chemise Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.221,false,true,261415,Photographs,Photograph,Le chemise Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.4 cm. (3 3/8 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.222,false,true,261416,Photographs,Photograph,Le montagnard,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.4 cm. (3 3/8 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.223,false,true,261417,Photographs,Photograph,Le montagnard,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.7 cm. (3 3/8 x 4 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.224,false,true,261418,Photographs,Photograph,Le montagnard,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.225,false,true,261419,Photographs,Photograph,Le montagnard,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.226,false,true,261420,Photographs,Photograph,L'Enfant blanc,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.227,false,true,261421,Photographs,Photograph,L'Enfant blanc,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 12.1 cm. (3 3/8 x 4 3/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.228,false,true,261422,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.3 x 11.7 cm. (3 1/4 x 4 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.229,false,true,261423,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.3 x 11.4 cm. (3 1/4 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.230,false,true,261425,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.7 cm. (3 3/8 x 4 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.231,false,true,261426,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.232,false,true,261427,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,8.6 x 11.4 cm. (3 3/8 x 4 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.233,false,true,261428,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.5 x 11.7 cm. (3 3/4 x 4 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.234,false,true,261429,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.5 x 13.7 cm. (3 3/4 x 5 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.235,false,true,261430,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.236,false,true,261431,Photographs,Photograph,Le petit Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.237,false,true,261432,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.238,false,true,261433,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.239,false,true,261434,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.240,false,true,261436,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.241,false,true,261437,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.242,false,true,261438,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.243,false,true,261439,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.244,false,true,261440,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.245,false,true,261441,Photographs,Photograph,Le Grand Russe,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,9.8 x 13.3 cm. (3 7/8 x 5 1/4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.251,false,true,261448,Photographs,Photograph,La fillette,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,11.8 x 8.9 cm (4 5/8 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.201,false,true,285651,Photographs,Photograph,Giorgio de Castiglione,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,"1861, printed 1895–1910",1861,1910,Gelatin silver print from glass negative,Image: 36 x 27.9 cm (14 3/16 x 11 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.173a,false,true,261351,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,11.4 x 8.6 cm (4 1/2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.174a,false,true,261353,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Waxed albumen silver print from glass negative with applied color,8.3 x 5 cm (3 1/4 x 1 15/16 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.176a,false,true,282738,Photographs,Photograph,Les Chiens,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1875–80,1875,1880,Albumen silver print from glass negative,22.7 x 16.8 cm (8 15/16 x 6 5/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.212a,false,true,261405,Photographs,Photograph,Le fauteuil,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,7.6 x 10.2 cm. (3 x 4 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.1,false,true,261375,Photographs,Photograph,La chaise rustique,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.2,false,true,261376,Photographs,Photograph,Le tambour,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.3,false,true,261377,Photographs,Photograph,Chaise rustique (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.4,false,true,261378,Photographs,Photograph,Chaise rustique,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.5,false,true,261379,Photographs,Photograph,Chaise rustique,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.6,false,true,261380,Photographs,Photograph,Le furieuse,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.7,false,true,261381,Photographs,Photograph,Le bournous,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.8,false,true,261382,Photographs,Photograph,Tambour (autre),,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.193.9,false,true,261383,Photographs,Photograph,La tête renversée,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1975.548.95a, b",false,true,261550,Photographs,Photograph,Piede de Judith,,,,,,Artist,,Pierre-Louis Pierson,"French, 1822–1913",,"Pierson, Pierre-Louis",French,1822,1913,1860s,1860,1869,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.51,false,true,291804,Photographs,Daguerreotype,[Reclining Female Nude as Danae],,,,,,Artist,Attributed to,Bruno Braquehais,"French, 1823–1875",,"Braquehais, Bruno",French,1823,1875,1850s,1852,1858,Daguerreotype,Image: 8 x 6 cm (3 1/8 x 2 3/8 in.) Mount: 12.9 x 11.1 cm (5 1/16 x 4 3/8 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.279,false,true,286294,Photographs,Photograph,[Nude Before a Mirror],,,,,,Artist,Attributed to,Bruno Braquehais,"French, 1823–1875",,"Braquehais, Bruno",French,1823,1875,ca. 1857,1855,1859,Albumen silver print from glass negative,Image: 22.3 × 17.9 cm (8 3/4 × 7 1/16 in.) Mount: 50.1 × 39.3 cm (19 3/4 × 15 1/2 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.45,false,true,283120,Photographs,Photograph,Emmanuel Frémiet,,,,,,Artist,,Adrien Tournachon,"French, 1825–1903",,"Tournachon, Adrien",French,1825,1903,1854–55,1854,1855,Salted paper print from glass negative,24.6 x 17.3cm (9 11/16 x 6 13/16in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.634,false,true,286272,Photographs,Photograph,"[Bull from Glane, Canton of Fribourg]",,,,,,Artist,,Adrien Tournachon,"French, 1825–1903",,"Tournachon, Adrien",French,1825,1903,1856,1856,1856,Salted paper print from glass negative,Image: 7 1/2 × 10 9/16 in. (19.1 × 26.8 cm) Mount: 10 5/8 × 12 13/16 in. (27 × 32.5 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.176,false,true,286657,Photographs,Photograph,Auguste Vacquerie,,,,,,Artist,,Charles Victor Hugo,"French, 1826–1871",,"Hugo, Charles Victor",French,1826,1871,1853–56,1853,1856,Salted paper print from paper negative,Image: 9.2 x 7.6 cm (3 5/8 x 3 in.) Mount: 24.8 x 18.4 cm (9 3/4 x 7 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.68,false,true,282079,Photographs,Photograph,[Man in a Forest Landscape],,,,,,Artist,,Constant Alexandre Famin,"French, 1827–1888",,"Famin, Constant Alexandre",French,1827,1888,ca. 1870,1868,1872,Albumen silver print from glass negative,33.7 x 25.3 cm (13 1/4 x 9 15/16 in. ),"The Rubel Collection, Purchase, Lila Acheson Wallace Gift, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.256,false,true,289145,Photographs,Photograph,Gustave Le Gray,,,,,,Artist,,Alphonse Delaunay,"French, 1827–1906",,"Delaunay, Alphonse",French,1827,1906,1854,1854,1854,Salted paper print from glass negative,Image: 22.1 x 16.5 cm (8 11/16 x 6 1/2 in.) Mount: 44 x 34.2 cm (17 5/16 x 13 7/16 in.),"Purchase, Daniel Blau Gift and 2007 Benefit Fund, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.236,false,true,680014,Photographs,Photograph,"Second Palace at Mitla, Mexico.",,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,February 1860,1860,1860,Albumen silver print from glass negative,Image: 13 1/8 × 16 7/8 in. (33.3 × 42.9 cm) Mount: 21 3/16 in. × 25 in. (53.8 × 63.5 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/680014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.73,false,true,283172,Photographs,Photograph,"Raharla, Minister to the Queen",,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,1863,1863,1863,Albumen silver print from glass negative,Image: 19.4 × 12 cm (7 5/8 × 4 3/4 in.) Mount: 28.7 × 23.1 cm (11 5/16 × 9 1/8 in.),"Gilman Collection, Purchase, Joyce F. Menschel Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.74,false,true,283173,Photographs,Photograph,[Family Group],,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,1863,1863,1863,Albumen silver print from glass negative,Image: 21.3 × 16.8 cm (8 3/8 × 6 5/8 in.) Mount: 28.8 × 23.2 cm (11 5/16 × 9 1/8 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.565,false,true,286467,Photographs,Photograph,"La Prison, à Chichen-Itza",,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,1857–89,1857,1889,Albumen silver print from glass negative,Image: 33.3 x 42.4 cm (13 1/8 x 16 11/16 in.) Mount: 54 x 70.8 cm (21 1/4 x 27 7/8 in.),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.732,false,true,286280,Photographs,Photograph,"Femmes Betsimisaraka, Madagascar",,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,1863,1863,1863,Albumen silver print from glass negative,Image: 19.7 x 17 cm (7 3/4 x 6 11/16 in.) Mount: 28.7 x 23.1 cm (11 5/16 x 9 1/8 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.733,false,true,286146,Photographs,Photograph,Ile de la Réunion,,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,1863,1863,1863,Albumen silver print from glass negative,Image: 27.8 x 20.4 cm (10 15/16 x 8 1/16 in.) Mount: 35.8 x 27 cm (14 1/8 x 10 5/8 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.734,false,true,286691,Photographs,Photograph,Fougère arborescente,,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,1863,1863,1863,Albumen silver print from glass negative,Image: 28.7 x 22.8 cm (11 5/16 x 9 in.) Mount: 35.5 x 27.4 cm (14 x 10 13/16 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.735,false,true,286282,Photographs,Photograph,Baobab à Mohéli,,,,,,Artist,,Désiré Charnay,"French, 1828–1915",,"Charnay, Désiré",French,1828,1915,1863,1863,1863,Albumen silver print from glass negative,Image: 28.7 x 22.8 cm (11 5/16 x 9 in.) Mount: 35.4 x 26.9 cm (13 15/16 x 10 9/16 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (8),false,true,287607,Photographs,Photograph,"[The Countess Canning, Simla]",,,,,,Artist,,Jean Baptiste Oscar Mallitte,"French, 1829–1905",,"Mallitte, Jean Baptiste Oscar",French,1829,1905,1861,1861,1861,Albumen silver print,Mount: 33 x 26.1 cm (13 x 10 1/4 in.) Image: 20.7 x 14.1 cm (8 1/8 x 5 9/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (46),false,true,287646,Photographs,Photograph,"[Group portrait at the Governor Generals Camp, L-R: Maj Jones, Mr. Walters, The Governor Generals Chaplain, J.C.S. and Sir E.Campbell Bart., 60th Rifles Mry. Sry. to G.G.]",,,,,,Artist,,Jean Baptiste Oscar Mallitte,"French, 1829–1905",,"Mallitte, Jean Baptiste Oscar",French,1829,1905,1858–61,1858,1861,Albumen silver print,Image: 20.5 x 24.7 cm (8 1/16 x 9 3/4 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.) Print mounted vertically.,"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (52),false,true,287652,Photographs,Photograph,[Lady Canning],,,,,,Artist,,Jean Baptiste Oscar Mallitte,"French, 1829–1905",,"Mallitte, Jean Baptiste Oscar",French,1829,1905,1858–61,1858,1861,Albumen silver print,Image: 16 x 19.7 cm (6 5/16 x 7 3/4 in.) Mount: 33.1 x 26 cm (13 1/16 x 10 1/4 in.) Print mounted vertically.,"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (60),false,true,287660,Photographs,Photograph,"[Metcalfe House, Delhi]",,,,,,Artist,,Jean Baptiste Oscar Mallitte,"French, 1829–1905",,"Mallitte, Jean Baptiste Oscar",French,1829,1905,1858–61,1858,1861,Albumen silver print,Image: 21 x 28.4 cm (8 1/4 x 11 3/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.) Pritn mounted vertically.,"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (50a),false,true,287650,Photographs,Photograph,"[Lady Canning on her Black Arab and Lord Clyde, Commander in Chief]",,,,,,Artist,,Jean Baptiste Oscar Mallitte,"French, 1829–1905",,"Mallitte, Jean Baptiste Oscar",French,1829,1905,1858–61,1858,1861,Albumen silver print,Image: 14.9 x 19.2 cm (5 7/8 x 7 9/16 in.) Mount: 33.1 x 26 cm (13 1/16 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (56b),false,true,287705,Photographs,Photograph,"[Campbell Twins in a Shigram, Governor General's Camp]",,,,,,Artist,,Jean Baptiste Oscar Mallitte,"French, 1829–1905",,"Mallitte, Jean Baptiste Oscar",French,1829,1905,1858–61,1858,1861,Albumen silver print,Image: 9.7 x 19.6 cm (3 13/16 x 7 11/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1144,false,true,266168,Photographs,Photographs,[The Corpse of Emperor Maximilian I of Mexico],,,,,,Artist,,François Aubert,"French, 1829–1906",,"Aubert, François",French,1829,1906,1867,1867,1867,Albumen silver print,,"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.212,false,true,285871,Photographs,Photograph,[The Scene of the Execution of Emperor Maximilian I of Mexico],,,,,,Artist,,François Aubert,"French, 1829–1906",,"Aubert, François",French,1829,1906,1867,1867,1867,Albumen silver print from glass negative,Image: 22.1 × 16.1 cm (8 11/16 × 6 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.213,false,true,285712,Photographs,Photographs,"The Shirt of the Emperor, Worn during His Execution",,,,,,Artist,,François Aubert,"French, 1829–1906",,"Aubert, François",French,1829,1906,1867,1867,1867,Albumen silver print from glass negative,Image: 22.2 × 15.8 cm (8 3/4 × 6 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.580.1,false,true,286895,Photographs,Photograph,[Emperor Maximilian's Firing Squad],,,,,,Artist,,François Aubert,"French, 1829–1906",,"Aubert, François",French,1829,1906,1867,1867,1867,Albumen silver print from glass negative,Image: 11.4 x 14.2 cm (4 7/16 x 5 9/16 in.) Mount: 18.2 x 22.1 cm (7 3/16 x 8 11/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.820,false,true,286544,Photographs,Photograph,[Bird in Flight],,,,,,Artist,,Etienne-Jules Marey,"French, 1830–1904",,"Marey, Etienne-Jules",French,1830,1904,1886,1886,1886,Albumen silver print from glass negative,Image: 3.3 x 17.3 cm (1 5/16 x 6 13/16 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.321,false,true,286606,Photographs,Photograph,"Tomb of Ptahmose, Saqqara (Memphis)",,,,,,Artist,,Théodule Deveria,"French, 1831–1871",,"Deveria, Théodule",French,1831,1871,1859,1859,1859,Albumen silver print from paper negative,"Image: 8 3/8 × 11 1/16 in. (21.2 × 28.1 cm), irregularly trimmed Sheet: 8 7/16 × 11 5/16 in. (21.4 × 28.8 cm), irregularly trimmed","Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.317,false,true,292057,Photographs,Photograph,Femme turque en toilette de ville,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,1870s,1870,1879,Albumen silver print from glass negative,Image: 22.2 x 16.4 cm (8 3/4 x 6 7/16 in.),"Funds from various donors, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/292057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.561.1,false,true,261776,Photographs,Photograph,Temple d'Andour,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870,1868,1872,Albumen silver print from glass negative,17.3 x 23.0 cm (6 13/16 x 9 1/16 in.),"Gift of Daniel Wolf, in memory of Diane R. Wolf, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.561.2,false,true,261777,Photographs,Photograph,Pyramides et le Sphinx,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,"1860s–70s, printed ca. 1870",1860,1879,Albumen silver print,17.3 x 23.1 cm. (6 13/16 x 9 1/16 in.),"Gift of Daniel Wolf, in memory of Diane R. Wolf, 1976",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.594.87,false,true,260950,Photographs,Photograph,[The Roman Forum],,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870s,1868,1872,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1128.1,false,true,262989,Photographs,Photograph,The Temple of the Sun at Baalbec,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870,1868,1872,Albumen silver print,,"Gift of Douglas Dillon, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1128.2,false,true,262990,Photographs,Photograph,"[Details of the Colonnade of the Parthenon, Athens]",,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870s,1868,1872,Albumen silver print,Image: 22.3 x 27.7 cm (8 3/4 x 10 7/8 in.) Mount: 40.5 x 50.6 cm (15 15/16 x 19 15/16 in.),"Gift of Douglas Dillon, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.4,false,true,263132,Photographs,Photograph,Caire près du Mokkatam,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.5,false,true,263133,Photographs,Photograph,"Caire. Mosquée el-Arhar, détails de la porte",,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.6,false,true,263134,Photographs,Photograph,Rue du Caire,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.7,false,true,263135,Photographs,Photograph,Saîs coureurs au Caire,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.8,false,true,263136,Photographs,Photograph,Le Caire - Intérieur de la mosquée El Bordei,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1123.8,false,true,264380,Photographs,Photograph,Caire. Allée de Pyramides,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870,1868,1872,Albumen silver print,,"Gift of Weston J. Naef, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1153.13,false,true,263432,Photographs,Photograph,"Cous du Kadisha, Monte Libon",,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,"1860s–80s, printed ca. 1870",1860,1889,Albumen silver print,,"Gift of Mrs. John L. Swayze, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1153.14,false,true,263433,Photographs,Photograph,Intérieur de la Porte de Jaffa,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870,1868,1872,Albumen silver print,Image: 28 x 22.6 cm (11 x 8 7/8 in.),"Gift of Mrs. John L. Swayze, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1153.15,false,true,263434,Photographs,Photograph,Promenade des Pins á Beyrouth,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870,1868,1872,Albumen silver print,,"Gift of Mrs. John L. Swayze, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1153.20,false,true,263440,Photographs,Photograph,Beyrouth. Vu du collège américain,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1870,1868,1872,Albumen silver print,,"Gift of Mrs. John L. Swayze, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1153.21,false,true,263441,Photographs,Photograph,Vue générale de Bethany - General view of Bethany,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1880,1878,1882,Albumen silver print,,"Gift of Mrs. John L. Swayze, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1153.22,false,true,263442,Photographs,Photograph,Les ponts du fleurs du chien,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1880,1878,1882,Albumen silver print,,"Gift of Mrs. John L. Swayze, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1153.23,false,true,263443,Photographs,Photograph,Panorama de Jaffa,,,,,,Artist,,Félix Bonfils,"French, 1831–1885",,"Bonfils, Félix",French,1831,1885,ca. 1880,1878,1882,Albumen silver print,,"Gift of Mrs. John L. Swayze, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.501,false,true,685841,Photographs,Carte-de-visite,[John Leech],,,,,,Artist,,Camille Silvy,"French, 1835–1869",,"Silvy, Camille",French,1835,1869,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1014.2,false,true,264624,Photographs,Photograph,[Roses],,,,,,Artist,,Eugène Chauvigné,"French, 1837–1894",,"Chauvigné, Eugène",French,1837,1894,ca. 1875,1873,1877,Albumen silver print from glass negative,24.0 x 17.9 cm. (9 7/16 x 7 1/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.366,false,true,685706,Photographs,Carte-de-visite,[Francois Adolphe Grison],,,,,,Artist,,Émile Schweitzer,"French, 1837–1903",,"Schweitzer, Émile",French,1837,1903,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.186,false,true,306173,Photographs,Photograph,Prenant un Ris à Bord de L'Astrée,,,,,,Artist,,Félix Auguste Leclerc,"French, 1838–1896",,"Leclerc, Félix Auguste",French,1838,1896,1871,1871,1871,Albumen silver print from glass negative,Image: 25 × 18.7 cm (9 13/16 × 7 3/8 in.) Mount: 44.7 × 32.4 cm (17 5/8 × 12 3/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.848,false,true,285885,Photographs,Photograph,[Emmanuel Frémiet],,,,,,Artist,Possibly by,Edmond Bénard,"French, 1838–1907",,"Bénard, Edmond",French,1838,1907,1880s–90s,1880,1899,Albumen silver print from glass negative,Image: 20.6 × 26.4 cm (8 1/8 × 10 3/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.9,false,true,267314,Photographs,Photograph,Le Nouvel Opéra de Paris (Sculpture Ornementale),,,,,,Artist,,Louis-Emile Durandelle,"French, 1839–1917",,"Durandelle, Louis-Émile",French,1839,1917,1865–72,1865,1872,Albumen silver print from glass negative,38.3 x 27.9 cm. (15 1/16 x 11 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.50.353,false,true,269271,Photographs,Photograph,[Charles Garnier in the Drafting Room While Designing the New Paris Opera],,,,,,Artist,,Louis-Emile Durandelle,"French, 1839–1917",,"Durandelle, Louis-Émile",French,1839,1917,ca. 1870,1868,1872,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1072,false,true,265933,Photographs,Photograph,La Tour St. Jacques La Boucherie à Paris,,,,,,Artist,,Charles Soulier,"French, 1840–1875",,"Soulier, Charles",French,1840,1875,ca. 1867,1865,1869,Albumen silver print from glass negative,40.7 x 30.6 cm. (16 x 12 1/16 in.),"Edward Pearce Casey Fund, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.32.1,false,true,289160,Photographs,Photograph,Grande Salle du Conseil d'Etat,,,,,,Artist,,Charles Soulier,"French, 1840–1875",,"Soulier, Charles",French,1840,1875,May 1871,1871,1871,Albumen silver print from glass negative,Image: 19 x 24.9 cm (7 1/2 x 9 13/16 in.) Mount: 34.7 x 47.2 cm (13 11/16 x 18 9/16 in.),"Gift of Paula and Robert Hershkowitz, in memory of Sam Wagstaff, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.32.2,false,true,289161,Photographs,Photograph,Maisons de la porte d'Auteuil,,,,,,Artist,,Charles Soulier,"French, 1840–1875",,"Soulier, Charles",French,1840,1875,May 1871,1871,1871,Albumen silver print from glass negative,Image: 19.2 x 24.9 cm (7 9/16 x 9 13/16 in.) Mount: 34.7 x 47.5 cm (13 11/16 x 18 11/16 in.),"Gift of Paula and Robert Hershkowitz, in memory of Sam Wagstaff, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.527,false,true,289070,Photographs,Photograph,[Group of Adults and Children on a Village Street in the Auvergne],,,,,,Artist,,Felix Thiollier,"French, 1842–1914",,"Thiollier, Felix",French,1842,1914,ca. 1910,1905,1914,Gelatin silver print,29.4 x 39.4 cm (11 9/16 x 15 1/2 in.),"Twentieth-Century Photography Fund, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.1,false,true,291656,Photographs,Photograph,[Fencer],,,,,,Artist,,Georges Demeny,"French, 1850–1917",,"Demeny, Georges",French,1850,1917,1906,1906,1906,Gelatin silver print,Image: 17.5 x 26.5 x 12.8 cm (6 7/8 x 10 7/16 in.) Mount: 20.1 x 28.7 cm (7 15/16 x 11 5/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.16,false,true,289245,Photographs,Photograph,"Tableau synoptic des traits physionomiques: pour servir a l'étude du ""portrait parlé""",,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",French,1853,1914,ca. 1909,1909,1909,Gelatin silver print,Image: 39.4 x 29.5 cm (15 1/2 x 11 5/8 in.),"Twentieth-Century Photography Fund, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.375.1–.434,false,true,286605,Photographs,Mugshot,[Mugshots of Suspected Anarchists from French Police Files],,,,,,Artist,,Alphonse Bertillon,"French, 1853–1914",,"Bertillon, Alphonse",French,1853,1914,1891–95,1891,1895,Albumen silver prints and gelatin silver prints,10.5 x 7 x 0.5 cm (4 1/8 x 2 3/4 x 3/16 in.) each,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.254,false,true,267665,Photographs,Photograph,[Study in Orange],,,,,,Artist,,René Le Bègue,"French, 1857–1914",,"Le Bègue, René",French,1857,1914,1904,1904,1904,Gum bichromate print,22.0 x 14.3 cm. (8 11/16 x 5 5/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.258,false,true,267669,Photographs,Photograph,Académie,,,,,,Artist,,René Le Bègue,"French, 1857–1914",,"Le Bègue, René",French,1857,1914,1902,1902,1902,Gum bichromate print,24.1 x 18.0 cm. (9 1/2 x 7 1/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.220,false,true,269345,Photographs,Photograph,[Study in Orange],,,,,,Artist,,René Le Bègue,"French, 1857–1914",,"Le Bègue, René",French,1857,1914,1903,1903,1903,Gum bichromate print,25.7 x 19.7 cm. (10 1/8 x 7 3/4 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.118,false,true,283240,Photographs,Photograph,"[Puyo, Robert Demachy, and Paul de Singly with Model]",,,,,,Artist,,Emile Joachim Constant Puyo,"French, 1857–1933",,"Puyo, Emile Joachim Constant",French,1857,1933,1909,1909,1909,Platinum print,Image: 2 11/16 × 3 3/16 in. (6.9 × 8.1 cm) Mount: 3 1/8 × 3 1/2 in. (7.9 × 8.9 cm),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1148,false,true,285751,Photographs,Photograph,[Montmartre],,,,,,Artist,,Emile Joachim Constant Puyo,"French, 1857–1933",,"Puyo, Emile Joachim Constant",French,1857,1933,ca. 1906,1904,1908,Bromoil print,Image: 11 7/16 × 8 7/8 in. (29.1 × 22.6 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.56,false,true,267853,Photographs,Photograph,Panel,,,,,,Artist,,Robert Demachy,"French, 1859–1936",,"Demachy, Robert",French,1859,1936,1898,1898,1898,Gum bichromate print,14.7 x 20.2 cm (5 13/16 x 7 15/16 in. ),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.57,false,true,267854,Photographs,Photograph,Académie,,,,,,Artist,,Robert Demachy,"French, 1859–1936",,"Demachy, Robert",French,1859,1936,1900,1900,1900,Gum bichromate print,22.2 x 17.0 cm. (8 3/4 x 6 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.470,false,true,267843,Photographs,Photograph,Honfleur,,,,,,Artist,,Robert Demachy,"French, 1859–1936",,"Demachy, Robert",French,1859,1936,1905,1900,1909,Gum bichromate print,15.1 x 21.4 cm. (5 15/16 x 8 7/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.205,false,true,269328,Photographs,Photograph,The Crowd,,,,,,Artist,,Robert Demachy,"French, 1859–1936",,"Demachy, Robert",French,1859,1936,1910,1910,1910,Oil print,15.8 x 22.8 cm. (6 1/4 x 9 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.206,false,true,269329,Photographs,Photograph,Dans les coulisses,,,,,,Artist,,Robert Demachy,"French, 1859–1936",,"Demachy, Robert",French,1859,1936,ca. 1897,1895,1899,Gum bichromate print,36.7 x 18.8 cm. (14 7/16 x 7 3/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.666.3,false,true,289550,Photographs,Photograph,Struggle,,,,,,Artist,,Robert Demachy,"French, 1859–1936",,"Demachy, Robert",French,1859,1936,1903 or earlier,1900,1903,Gum bichromate print,Image: 17.4 x 11.6 cm (6 7/8 x 4 9/16 in.) Mount: 17.7 x 11.9 cm (6 15/16 x 4 11/16 in.) Mount (2nd): 39.6 x 29.6 cm (15 9/16 x 11 5/8 in.),"Gift of Isaac Lagnado, in honor of Thomas P. Campbell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.115,false,true,283233,Photographs,Photograph,[Auguste Rodin's The Clenched Hand],,,,,,Artist,,Eugène Druet,"French, 1868–1917",,"Druet, Eugène",French,1868,1917,before 1898,1890,1898,Gelatin silver print,Image: 29.9 x 39.5 cm (11 3/4 x 15 9/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.186,false,true,286722,Photographs,Photograph,"[Nijinsky in ""Danse siamoise"" from the ""Orientales""]",,,,,,Artist,,Eugène Druet,"French, 1868–1917",,"Druet, Eugène",French,1868,1917,1910,1910,1910,Gelatin silver print,Image: 20.3 x 14.6 cm (8 x 5 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.621,false,true,283234,Photographs,Photograph,[Study of a Sculpture],,,,,,Artist,Attributed to,Eugène Druet,"French, 1868–1917",,"Druet, Eugène",French,1868,1917,ca. 1900,1898,1902,Gelatin silver print,39.7 x 29.8 cm (15 5/8 x 11 3/4 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.8,false,true,284433,Photographs,Photograph,[Materialization of a Woman's Face Produced by the Medium Eva C.],,,,,,Artist,,Gustave Geley,"French, 1868–1924",,"Geley, Gustave",French,1868,1924,"February 26, 1918",1918,1918,Gelatin silver print,17.9 x 13 cm (7 1/16 x 5 1/8 in.),"Gilman Collection, Purchase, The Howard Gilman Foundation Gift, 2001",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1090,false,true,286701,Photographs,Photograph,[Construction for the Universal Exhibition of 1855],,,,,,Artist,,Bertsch et D'Arnaud,"French, active 1850s",,Bertsch et D'Arnaud,French,1850,1859,1855,1855,1855,Salted paper print from glass negative,Image: 20.7 × 16.4 cm (8 1/8 × 6 7/16 in.) Mount: 31 × 25.1 cm (12 3/16 × 9 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.651,false,true,287289,Photographs,Photograph,[Spiral of Lightning in a Thunderstorm],,,,,,Artist,,Charles Moussette,"French, active 1880s",,"Moussette, Charles",French,1880,1889,"May 12, 1886",1886,1886,Albumen silver print from glass negative,Mount: 9 13/16 in. × 6 7/8 in. (25 × 17.5 cm) Image: 6 5/8 × 4 5/8 in. (16.9 × 11.7 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.51,false,true,283132,Photographs,Photograph,"Leonardo da Vinci, Drawing for Christ in ""The Last Supper""",,,,,,Artist,,Léon Gérard,"French, active 1857–61",,"Gérard, Léon",French,1857,1861,1857–61,1857,1861,Albumen silver print from paper negative,Image: 36.3 x 26.6 cm (14 5/16 x 10 1/2 in.) Mount: 59.2 x 42.6 cm (23 5/16 x 16 3/4 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.13,false,true,290481,Photographs,Photograph,"Nuremburg, Intérieur de la Cour du Burg impérial",,,,,,Artist,,Léon Gérard,"French, active 1857–61",,"Gérard, Léon",French,1857,1861,1857,1857,1857,Albumen silver print from glass negative,Image: 34.9 x 26.9 cm (13 3/4 x 10 9/16 in.) Mount: 59.1 x 43.2 cm (23 1/4 x 17 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.46,true,true,282051,Photographs,Photograph,[Two Standing Female Nudes],,,,,,Artist,,Félix-Jacques-Antoine Moulin,"French, 1800–after 1875",,"Moulin, Félix-Jacques-Antoine",French,1800,1875,ca. 1850,1848,1852,Daguerreotype,visible: 14.5 x 11.1 cm (5 11/16 x 4 3/8 in.),"The Rubel Collection, Purchase, Anonymous Gift and Lila Acheson Wallace Gift, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1105,false,true,286524,Photographs,Photograph,Nude,,,,,,Artist,,Félix-Jacques-Antoine Moulin,"French, 1800–after 1875",,"Moulin, Félix-Jacques-Antoine",French,1800,1875,ca. 1850,1848,1852,Salted paper print from paper negative,Image: 8 13/16 × 6 3/4 in. (22.4 × 17.2 cm) Mount: 11 5/8 × 8 1/16 in. (29.6 × 20.5 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1075,false,true,266384,Photographs,Photograph,[Standing Male Nude],,,,,,Artist,,Charles Alphonse Marlé,"French, 1821–after 1867",,"Marlé, C.",French,1821,1921,ca. 1855,1853,1857,Salted paper print from paper negative,Image: 25.7 x 17.6 cm (10 1/8 x 6 15/16 in.),"Purchase, Ezra Mack Gift and The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.263,false,true,285463,Photographs,Photograph,"Shelling Beans, Argentelle",,,,,,Artist,,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,1851,1851,1851,Salted paper print from paper negative,Image: 22.3 x 17.7 cm (8 3/4 x 6 15/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.266,false,true,286023,Photographs,Photograph,"[Pont d'Ouilly on the Orne River, Normandy]",,,,,,Artist,,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,1850–51,1850,1851,Salted paper print from paper negative,Image: 17.9 x 22.2 cm (7 1/16 x 8 3/4 in.) Mount: 21 x 24 cm (8 1/4 x 9 7/16 in.),"Gilman Collection, Purchase, Heidi S. Steiger Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.286,false,true,286022,Photographs,Photograph,Self-Portrait,,,,,,Artist,,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,1846–47,1846,1847,Daguerreotype,Image: 13.3 x 10.3 cm (5 1/4 x 4 1/16 in.),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.758,false,true,286127,Photographs,Photograph,Louise-Marie-Julie,,,,,,Artist,,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,1849,1849,1849,Salted paper print from paper negative,16.6 x 12.9 cm (6 9/16 x 5 1/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.797,false,true,286024,Photographs,Photograph,Henriette-Reneé Patu,,,,,,Artist,,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,ca. 1845,1843,1847,Waxed salted paper print from paper negative,Image: 6 7/8 × 5 3/16 in. (17.5 × 13.2 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.898,false,true,286292,Photographs,Photograph,[Female Nude],,,,,,Artist,Circle of,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,ca. 1855,1853,1857,Salted paper print from paper negative,"14 x 11 cm (5 1/2 x 4 5/16 in.), corners clipped","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1241,false,true,302627,Photographs,Photograph,The Laundry,,,,,,Artist,,Louis-Adolphe Humbert de Molard,"French, Paris 1800–1874",,"Humbert de Molard, Louis-Adolphe",French,1800,1874,1840s,1840,1849,Salted paper print,Image: 7 5/16 × 5 1/2 in. (18.6 × 14 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.17,false,true,685359,Photographs,Carte-de-visite,[? Aubert],,,,,,Artist,,Émile Tourtin,"French, active 1860s–70s",,"Tourtin, Émile",French,1860,1880,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.466,false,true,685806,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Artist,,Émile Tourtin,"French, active 1860s–70s",,"Tourtin, Émile",French,1860,1880,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.516,false,true,685856,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Artist,,Émile Tourtin,"French, active 1860s–70s",,"Tourtin, Émile",French,1860,1880,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.784,false,true,686123,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Artist,,Émile Tourtin,"French, active 1860s–70s",,"Tourtin, Émile",French,1860,1880,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.860,false,true,686199,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Artist,,Émile Tourtin,"French, active 1860s–70s",,"Tourtin, Émile",French,1860,1880,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.145,false,true,289044,Photographs,Photograph,"Tubular Jetty, Mouth of the Adour, Port of Bayonne",,,,,,Artist,,Louis Lafon,"French, active 1870s–90s",,"Lafon, Louis",French,1870,1899,1892,1892,1892,Albumen silver print from glass negative,Image: 36.5 x 47.3 cm (14 3/8 x 18 5/8 in.),"Purchase, Alfred Stieglitz Society Gifts, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.146,false,true,289045,Photographs,Photograph,Lessart Viaduct on the Rance River,,,,,,Artist,,Louis Lafon,"French, active 1870s–90s",,"Lafon, Louis",French,1870,1899,October 1879,1879,1879,Albumen silver print from glass negative,Image: 36.9 x 47.6 cm (14 1/2 x 18 3/4 in.),"Purchase, Alfred Stieglitz Society Gifts, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.147,false,true,289106,Photographs,Photograph,The Foundry,,,,,,Artist,,Louis Lafon,"French, active 1870s–90s",,"Lafon, Louis",French,1870,1899,1870s–80s,1870,1889,Albumen silver print from glass negative,Image: 36.6 x 47.9 cm (14 7/16 x 18 7/8 in.),"Purchase, Alfred Stieglitz Society Gifts, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.558.1,false,true,289717,Photographs,Photograph,[Factory Interior],,,,,,Artist,,Louis Lafon,"French, active 1870s–90s",,"Lafon, Louis",French,1870,1899,ca. 1880,1875,1885,Albumen silver print from glass negative,Image: 36.7 x 47 cm (14 7/16 x 18 1/2 in.),"Gift of Charles Isaacs, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.558.2,false,true,289718,Photographs,Photograph,Forges,,,,,,Artist,,Louis Lafon,"French, active 1870s–90s",,"Lafon, Louis",French,1870,1899,ca. 1880,1875,1885,Albumen silver print from glass negative,Image: 35.2 x 45.5 cm (13 7/8 x 17 15/16 in.),"Gift of Charles Isaacs, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.558.3,false,true,289719,Photographs,Photograph,Cour des Ateliers,,,,,,Artist,,Louis Lafon,"French, active 1870s–90s",,"Lafon, Louis",French,1870,1899,ca. 1880,1875,1885,Albumen silver print from glass negative,Image: 37 x 46.6 cm (14 9/16 x 18 3/8 in.),"Gift of Charles Isaacs, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.9,false,true,284818,Photographs,Photograph,"[Angel of the Passion, Sainte-Chapelle, Paris]",,,,,,Artist,,Auguste Mestral,"French, Rans 1812–1884 Rans",,"Mestral, Auguste",French,1812,1884,1852–53,1852,1853,Salted paper print from paper negative,32.9 x 20.3 cm (12 15/16 x 8 in.),"Gilman Collection, Purchase, The Howard Gilman Foundation Gift, 2002",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.290,false,true,283761,Photographs,Photograph,"[Sculpture of Virgin and Child, Notre Dame, Paris]",,,,,,Artist,,Auguste Mestral,"French, Rans 1812–1884 Rans",,"Mestral, Auguste",French,1812,1884,ca. 1851,1850,1852,Salted paper print from paper negative,35.1 x 27.6 cm (13 13/16 x 10 7/8 in. ),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1,true,true,283121,Photographs,Photograph,[Woman Seen from the Back],,,,,,Artist,,Onésipe Aguado de las Marismas,"French, Evry 1830–1893 Paris",,"Aguado de las Marismas, Onésipe",French,1830,1893,ca. 1862,1860,1864,Salted paper print from glass negative,Image: 30.8 × 25.7 cm (12 1/8 × 10 1/8 in.) Mount: 39.4 × 31 cm (15 1/2 × 12 3/16 in.),"Gilman Collection, Purchase, Joyce F. Menschel Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.715,false,true,286694,Photographs,Photograph,[Rustic Building with Man under Trellis],,,,,,Artist,,André Giroux,"French, Paris 1801–1879 Paris",,"Giroux, André",French,1801,1879,ca. 1853,1851,1855,Salted paper print from glass negative,Image: 8 11/16 × 11 3/16 in. (22.1 × 28.4 cm) Mount: 18 9/16 × 23 13/16 in. (47.1 × 60.5 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.107,false,true,282181,Photographs,Photograph,Rue Neuve-Coquenard (from the Rue Lamartine),,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1870s,1870,1879,Albumen silver print,32.6 x 27 cm (12 13/16 x 10 5/8 in. ),"Purchase, Harris Brisbane Dick Fund and Warner Communications Inc. Purchase Fund, by exchange, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.292,false,true,283734,Photographs,Photograph,"[South Portal, Chartres Cathedral]",,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1854,1854,1854,Salted paper print from paper negative,21.5 x 15.5 cm (8 7/16 x 6 1/8 in.),"Purchase, Jennifer and Joseph Duke Gift and The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.167,false,true,288026,Photographs,Photograph,Arts et Métiers (Ancien Modèle),,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1864,1864,1864,Albumen silver print from glass negative,Image: 36.6 x 24.1 cm (14 7/16 x 9 1/2 in.) Mount: 63 x 45 cm (24 13/16 x 17 11/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.668,false,true,289721,Photographs,Photograph,Rue du Haut-Pave (Pantheon in Distance),,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1865–69,1865,1869,Albumen silver print from glass negative,Image: 20.6 x 37 cm (8 1/8 x 14 9/16 in.) Mount: 42.4 x 50.5 cm (16 11/16 x 19 7/8 in.),"Gift of Howard Greenberg, Paula and Robert Hershkowitz, Charles Isaacs, and Hans P. Kraus Jr., in honor of Philippe de Montebello, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1141,false,true,264863,Photographs,Photograph,[Rue de Constantine],,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,ca. 1865,1863,1867,Albumen silver print from glass negative,27.3 x 36.8 cm (10 3/4 x 14 1/2 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1094,false,true,265131,Photographs,Photographs,[Cloud Study over Paris],,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1850s,1850,1859,Albumen silver print,16.7 x 20.6 cm. (6 9/16 x 8 1/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1071,false,true,265932,Photographs,Photograph,La Bièvre,,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,ca. 1865,1863,1867,Albumen silver print from glass negative,27.8 x 37.6 cm. (10 15/16 x 14 13/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.513.2,false,true,283713,Photographs,Photograph,Rue Traversine (from the Rue d'Arras),,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,ca. 1868,1866,1870,Albumen silver print from glass negative,34.8 x 27.5 cm (13 11/16 x 10 13/16 in. ),"Gift of Howard Stein, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.513.3,false,true,283714,Photographs,Photograph,Rue du Chat-qui-Pêche (from the Rue de la Huchette),,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,ca. 1868,1866,1870,Albumen silver print from glass negative,35.9 x 27.2 cm (14 1/8 x 10 11/16 in. ),"Gift of Howard Stein, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.205,false,true,286547,Photographs,Photograph,Le Chat Momifié (trouvé dans les fouilles de Saint-Germain-en-Laye),,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,ca. 1862,1860,1864,Albumen silver print from glass negative,Mount: 19 13/16 × 9 15/16 in. (50.3 × 25.3 cm) Image: 11 1/2 × 11 1/4 in. (29.2 × 28.6 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.351,false,true,286546,Photographs,Photograph,Etude de ciel,,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1855–56,1855,1856,Salted paper print from glass negative,Image: 16 x 21 cm (6 5/16 x 8 1/4 in.) Mount: 25.9 x 31 cm (10 3/16 x 12 3/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.353,false,true,286336,Photographs,Photograph,Etude de ciel,,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1855–56,1855,1856,Albumen silver print from glass negative,Image: 15.4 x 21 cm (6 1/16 x 8 1/4 in.) Mount: 31.1 x 43.5 cm (12 1/4 x 17 1/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.355,false,true,286338,Photographs,Photographs,"Rue du Contrat-Social, de la rue de la Tonnellerie",,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1864–1865,1864,1865,Albumen silver print from glass negative,Image: 22.5 x 37.1 cm (8 7/8 x 14 5/8 in.) Mount: 41.3 x 60.3 cm (16 1/4 x 23 3/4 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.357,false,true,286340,Photographs,Photograph,"[Allegorical Sculpture of Industry, Pont du Carrousel]",,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1852,1852,1852,Salted paper print from paper negative,Image: 21.1 x 15.2 cm (8 5/16 x 6 in.) Mount: 42.7 x 27.8 cm (16 13/16 x 10 15/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.358,false,true,285698,Photographs,Photograph,"Rue Estienne, de la rue Boucher",,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1862–65,1862,1865,Albumen silver print from glass negative,Image: 34.3 x 27.1 cm (13 1/2 x 10 11/16 in.) Mount: 23 11/16 × 16 5/16 in. (60.2 × 41.4 cm),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.362,false,true,285699,Photographs,Photograph,Impasse Briare (de la Cité Coquenard),,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,1860s,1860,1869,Albumen silver print from glass negative,Image: 62.9 x 45.2 cm (24 3/4 x 17 13/16 in.) Mount: 24 13/16 × 17 11/16 in. (63 × 45 cm),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.378,false,true,286545,Photographs,Photograph,Cour Saint-Guillaume,,,,,,Artist,,Charles Marville,"French, Paris 1813–1879 Paris",,"Marville, Charles",French,1813,1879,ca. 1865,1863,1867,Albumen silver print from glass negative,Image: 34.2 x 27.2 cm (13 7/16 x 10 11/16 in.) Mount: 62.9 x 45.2 cm (24 3/4 x 17 13/16 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1130,true,true,266284,Photographs,Photograph,"[Large Figures on the North Porch, Chartres Cathedral]",,,,,,Artist,,Henri-Jean-Louis Le Secq,"French, Paris 1818–1882 Paris",,"Le Secq, Henri-Jean-Louis",French,1818,1882,1852,1852,1852,Salted paper print from paper negative,32.8 x 22.1 cm (12 15/16 x 8 11/16 in.),"Purchase, The Howard Gilman Foundation and Harriette and Noel Levine Gifts, Samuel J. Wagstaff Jr. Bequest, and Rogers Fund, 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.35,false,true,283108,Photographs,Photograph,Wooden Staircase at Chartres,,,,,,Artist,,Henri-Jean-Louis Le Secq,"French, Paris 1818–1882 Paris",,"Le Secq, Henri-Jean-Louis",French,1818,1882,1852,1852,1852,Salted paper print from paper negative,Mount: 19 13/16 in. × 13 7/8 in. (50.3 × 35.2 cm) Image: 12 13/16 × 9 3/16 in. (32.6 × 23.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.661.1,false,true,270873,Photographs,Carte-de-visite,Alphonse Karr,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1850s,1850,1859,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.149.40,false,true,269111,Photographs,Carte-de-visite,The Imperial Court of Napoleon III,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,"ca. 1866, printed 1940s",1861,1871,Gelatin silver print,Image: 12.8 x 8 cm (5 1/16 x 3 1/8 in.) Frame: 35.6 x 27.9 cm (14 x 11 in.),"David Hunter McAlpin Fund, 1947",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.170.1,false,true,267168,Photographs,Photograph,Prince Lobkowitz,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1858,1858,1858,Albumen silver print from glass negative,20.0 x 23.2 cm (7 7/8 x 9 1/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1995",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.10,false,true,261269,Photographs,Photograph,La taille,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.11,false,true,261280,Photographs,Photograph,Le deux roses,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.12,false,true,261291,Photographs,Photograph,La robe écossaise,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.6 cm. (2 x 3 3/8 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.242,false,true,286378,Photographs,Photographs,[Profile of the Prince Imperial],,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1863,1863,1863,Albumen silver print from glass negative,Image: 25.1 x 18.3 cm (9 7/8 x 7 3/16 in.) Mount: 28.2 x 20.5 cm (11 1/8 x 8 1/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.284,false,true,286594,Photographs,Photograph,The Juggler Manoel,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1861,1861,1861,Albumen silver print from glass negative,Image: 19.9 × 23.2 cm (7 13/16 × 9 1/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1163,false,true,286389,Photographs,Photograph,Caroline Rosati,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,ca. 1860,1858,1862,Salted paper print from glass negative,Image: 29.8 × 23.6 cm (11 3/4 × 9 5/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.4,false,true,294508,Photographs,Photograph,Dulare,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,February 1865–75,1865,1875,Albumen silver print from glass negative,Image: 20.3 × 12.1 cm (8 × 4 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.5,false,true,294509,Photographs,Photograph,Honoune Keller,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1864–65,1864,1865,Albumen silver print from glass negative,Image: 19.4 × 12.5 cm (7 5/8 × 4 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.6,false,true,294510,Photographs,Photograph,Louis Revoil,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,February 1865–75,1865,1875,Albumen silver print from glass negative,Image: 19.5 × 24.5 cm (7 11/16 × 9 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.7,false,true,294511,Photographs,Photograph,"du Beaumenil, Forsyth",,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,"October 1862–February 11, 1863",1862,1863,Albumen silver print from glass negative,Image: 19.3 × 23.6 cm (7 5/8 × 9 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.8,false,true,294512,Photographs,Photograph,Berthe,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,"January 22, 1862– April 1862",1862,1862,Albumen silver print from glass negative,Image: 20.1 × 23.3 cm (7 15/16 × 9 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.9,false,true,294513,Photographs,Photograph,Berthe,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,"January 22, 1862–April 1862",1862,1862,Albumen silver print from glass negative,Image: 18.5 × 22.7 cm (7 5/16 × 8 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.10,false,true,294514,Photographs,Photograph; Carte-de-visite,Schneider,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,May–August 1863,1863,1863,Albumen silver print from glass negative,Image: 18.8 × 24.3 cm (7 3/8 × 9 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.11,false,true,294515,Photographs,Photograph; Carte-de-visite,V Queniaus,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,May–June 1860,1860,1860,Albumen silver print from glass negative,Image: 19.9 × 23.1 cm (7 13/16 × 9 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.12,false,true,294727,Photographs,Photograph; Carte-de-visite,Taglione,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,September 1857–November 1858,1857,1858,Albumen silver print from glass negative,Image: 19.9 × 23.3 cm (7 13/16 × 9 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.13,false,true,294728,Photographs,Photograph; Carte-de-visite,Danvers,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,"February–July 14, 1864",1864,1864,Albumen silver print from glass negative,Image: 20 × 24.2 cm (7 7/8 × 9 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.14,false,true,294729,Photographs,Photograph; Carte-de-visite,A Mlle Schlusser,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,May–August 1861,1861,1861,Albumen silver print from glass negative,Image: 19.9 × 23.7 cm (7 13/16 × 9 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.2.2,false,true,306153,Photographs,Photograph,Louise Abigdon,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1866,1866,1866,Albumen silver print from glass negative,"Image: 18.4 × 24.8 cm (7 1/4 in., 24.8 cm) Sheet: 26.2 × 34.8 cm (10 5/16 × 13 11/16 in.)","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.5,false,true,306158,Photographs,Photograph,Finali,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1866,1866,1866,Albumen silver print from glass negative,Image: 7 3/4 × 9 1/8 in. (19.7 × 23.1 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.2.53,false,true,306154,Photographs,Photograph,Beresford,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1867,1867,1867,Albumen silver print from glass negative,Image: 18.4 × 24.8 cm (7 1/4 × 9 3/4 in.) Album page: 26.2 × 34.8 cm (10 5/16 × 13 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.2.78,false,true,306155,Photographs,Photograph,Berthe,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1862,1862,1862,Albumen silver print from glass negative,Image: 18.4 × 24.8 cm (7 1/4 × 9 3/4 in.) Album page: 26.2 × 34.8 cm (10 5/16 × 13 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.10,false,true,306159,Photographs,Photograph,Gabrielle; M. Gutierrez de Estrada,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1867,1867,1867,Albumen silver print from glass negative,Image: 7 9/16 × 9 1/4 in. (19.2 × 23.5 cm) Sheet: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.23,false,true,306160,Photographs,Photograph,Héloise,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1863,1863,1863,Albumen silver print from glass negative,Image: 7 9/16 × 9 1/4 in. (19.2 × 23.5 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.35,false,true,306161,Photographs,Photograph,Valois,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1866,1866,1866,Albumen silver print from glass negative,Image: 7 9/16 × 9 1/4 in. (19.2 × 23.5 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.74,false,true,306162,Photographs,Photograph,Rosalie Léon,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1866,1866,1866,Albumen silver print from glass negative,Image: 7 1/2 × 10 1/16 in. (19 × 25.6 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.85,false,true,306163,Photographs,Photograph,Mangin,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1861,1861,1861,Albumen silver print from glass negative,Image: 7 1/2 × 9 7/16 in. (19 × 24 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.531.1–.164,false,true,299315,Photographs,Album,Costumes V,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1863–67,1863,1867,Albumen silver prints from glass negatives,,"Museum Accession, transferred from the Costume Institute Library",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.2.106,false,true,306156,Photographs,Photograph,Boudet,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1866,1866,1866,Albumen silver print from glass negative,Image: 18.4 × 24.8 cm (7 1/4 × 9 3/4 in.) Album page: 26.2 × 34.8 cm (10 5/16 × 13 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.2.142,false,true,306157,Photographs,Photograph,Esther David,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1866,1866,1866,Albumen silver print from glass negative,Image: 18.4 × 24.8 cm (7 1/4 × 9 3/4 in.) Album page: 26.2 × 34.8 cm (10 5/16 × 13 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.109,false,true,306164,Photographs,Photograph,Amélie and Elise Gitteri,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1866,1866,1866,Albumen silver print from glass negative,Image: 7 1/2 × 9 7/16 in. (19 × 24 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.110,false,true,306165,Photographs,Photograph,Clara Silvois,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1861,1861,1861,Albumen silver print from glass negative,Image: 7 1/2 × 9 7/16 in. (19 × 24 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.134,false,true,306166,Photographs,Photograph,A. Sardou,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1867,1867,1867,Albumen silver print from glass negative,Image: 7 1/2 × 9 7/16 in. (19 × 24 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.588.3.152,false,true,306167,Photographs,Photograph,Léontine Walter,,,,,,Artist,,André-Adolphe-Eugène Disdéri,"French, Paris 1819–1889 Paris",,"Disdéri, André-Adolphe-Eugène",French,1819,1889,1863,1863,1863,Albumen silver print from glass negative,Image: 7 1/2 × 9 1/4 in. (19 × 23.5 cm) Album page: 10 3/8 × 13 3/4 in. (26.3 × 35 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.233,false,true,266902,Photographs,Photograph,Gioacchino Rossini,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,March 1856,1856,1856,Salted paper print from glass negative,Image: 24.6 x 18.3 cm (9 11/16 x 7 3/16 in.),"Purchase, Marie-Thérèse and André Jammes and Annalee Newman Gifts, The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, and Nancy and Edwin Marks Gift, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1174,false,true,266459,Photographs,Photograph,[Standing Female Nude],,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1860–61,1860,1861,Salted paper print from glass negative,Image: 20.2 x 13.3 cm (7 15/16 x 5 1/4 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1198,true,true,266480,Photographs,Photograph,Eugène Pelletan,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1855–59,1855,1859,Salted paper print from glass negative,23.5 x 17.6 cm (9 1/4 x 6 15/16 in.),"Purchase, The Howard Gilman Foundation Gift and Rogers Fund, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1199,false,true,266481,Photographs,Photograph,Pierre-Luc-Charles Cicéri,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1855–60,1855,1860,Salted paper print from glass negative,23.5 x 18.7 cm. (9 1/4 x 7 3/8 in.),"Gift of Marie-Thérèse and André Jammes, in memory of Samuel J. Wagstaff Jr., 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.553.28,false,true,270013,Photographs,Cabinet card,Jean-François Millet,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,"1856–58, printed ca. 1900",1856,1858,Albumen silver print from glass negative,,"Gift of M. Knoedler & Co. Inc., 1958",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.457.1,false,true,294431,Photographs,Photograph,Hermaphrodite,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1860,1860,1860,Albumen silver print from glass negative,Image: 23.9 x 19.2 cm (9 7/16 x 7 9/16 in.),"Bequest of Robert Shapazian, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.42,false,true,283116,Photographs,Photograph,"[Seated Model, Partially Draped]",,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1856–59,1856,1859,Salted paper print from glass negative,Image: 11.2 x 10.5 cm (4 7/16 x 4 1/8 in.) Mount: 12.2 x 10.8 cm (4 13/16 x 4 1/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.46,false,true,306329,Photographs,Photograph,Jean-Francois Millet,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1856–58,1856,1858,Salted paper print from paper negative,Mount: 15 7/8 in. × 11 5/8 in. (40.4 × 29.5 cm) Image: 10 9/16 × 8 3/8 in. (26.8 × 21.3 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.256,false,true,285631,Photographs,Photograph,Théophile Gautier,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,ca. 1856,1854,1858,Salted paper print from glass negative,Image: 24.8 x 19.7 cm (9 3/4 x 7 3/4 in.) oval Mount: 26.9 x 21.7 cm (10 9/16 x 8 9/16 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.267,false,true,285630,Photographs,Photograph,[Self Portrait in American Indian Costume],,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1863,1863,1863,Salted paper print from glass negative,"Image: 22.1 x 13.2 cm (8 11/16 x 5 3/16 in.), arched, unevenly trimmed Mount: 30.8 x 21.3 cm (12 1/8 x 8 3/8 in.)","Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.313,false,true,286163,Photographs,Photograph,"[Nadar with His Wife, Ernestine, in a Balloon]",,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,"ca. 1865, printed 1890s",1863,1867,Gelatin silver print from glass negative,Image: 9 x 7.8 cm (3 9/16 x 3 1/16 in.) Mount: 23 x 19.9 cm (9 1/16 x 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.723,false,true,286070,Photographs,Photograph,Jules Janin,,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,ca. 1856,1854,1858,Albumen silver print from glass negative,Image: 9 1/4 × 7 3/8 in. (23.5 × 18.7 cm) Mount: 9 5/8 × 7 3/4 in. (24.5 × 19.7 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.798,false,true,285632,Photographs,Photograph,[Paul Nadar at the Breast of His Wet Nurse],,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,1856,1856,1856,Salted paper print from glass negative,Image: 10 15/16 × 8 7/8 in. (27.8 × 22.5 cm),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.813,false,true,286161,Photographs,Photograph,"Catacombs, Paris",,,,,,Artist,,Nadar,"French, Paris 1820–1910 Paris",,Nadar,French,1820,1910,April 1862,1862,1862,Albumen silver print from glass negative,Image: 23.7 x 18.6 cm (9 5/16 x 7 5/16 in.) Mount: 46 x 33.6 cm (18 1/8 x 13 1/4 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.56,false,true,282191,Photographs,Photograph,Daniel Halévy,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1895,1895,1895,Gelatin silver print from glass negative,40 x 28.7 cm (15 3/4 x 11 5/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1998",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.335,false,true,285444,Photographs,Photograph,[Self-Portrait with Christine and Yvonne Lerolle],,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,probably 1895–96,1895,1896,Gelatin silver print,Image: 37.1 x 29.3 cm (14 5/8 x 11 9/16 in.) Mount: 55.4 x 45.5 cm (21 13/16 x 17 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel and Rogers Fund, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1092,false,true,263608,Photographs,Photograph,"Paul Poujaud, Mme. Arthur Fontaine, and Degas",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1895,1895,1895,Gelatin silver print,29.4 x 40.5 cm. (11 9/16 x 15 15/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.655.1,false,true,284452,Photographs,Photograph,"Paule Gobillard, Jeannie Gobillard, Julie Manet, and Geneviève Mallarmé",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1895,1895,1895,Gelatin silver print,28.4 x 38.9 cm (11 3/16 x 15 5/16 in.),"Gift of Paul F. Walter, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.655.2,false,true,284453,Photographs,Photograph,"Street Scene, La-Queue-en-Brie (Val-de-Marne)",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1896,1894,1898,Gelatin silver print,28.5 x 39.5 cm (11 1/4 x 15 9/16 in. ),"Gift of Paul F. Walter, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.457.4a,false,true,294434,Photographs,Photograph,[Self-Portrait with Zoé Closier],,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,probably 1895,1895,1895,Gelatin silver print,Image: 5.8 x 8.8 cm (2 5/16 x 3 7/16 in.) Mount: 15 x 12 cm (5 7/8 x 4 3/4 in.),"Bequest of Robert Shapazian, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.457.4b,false,true,296287,Photographs,Photograph,[Self-Portrait in Library (Hand to Chin)],,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,probably 1895,1895,1895,Gelatin silver print,Image: 5.8 x 8.7 cm (2 5/16 x 3 7/16 in.) Mount: 15 x 12 cm (5 7/8 x 4 3/4 in.),"Bequest of Robert Shapazian, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1167,false,true,286039,Photographs,Photograph,Denise Zola,,,,,,Artist,,Émile Zola,"French, Paris 1840–1902 Paris",,"Zola, Émile",French,1840,1902,ca. 1900,1898,1902,Gelatin silver print,Image: 8 7/8 × 6 9/16 in. (22.5 × 16.6 cm) Mount: 9 7/16 × 7 1/16 in. (24 × 18 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (1),false,true,288397,Photographs,Photograph,Porta del Popolo,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (2),false,true,288398,Photographs,Photograph,Piazza del Popolo,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (3),false,true,288399,Photographs,Photograph,Villa Medici,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (4),false,true,288400,Photographs,Photograph,Fontana di Trevi,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 1/4 × 8 9/16 in. (28.6 × 21.7 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (5),false,true,288401,Photographs,Photograph,Monte Cavallo,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (6),false,true,288402,Photographs,Photograph,S. Giovanni Laterano,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (7),false,true,288403,Photographs,Photograph,Panteon,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (8),false,true,288404,Photographs,Photograph,Ponte e Castel S. Angelo,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (9),false,true,288405,Photographs,Photograph,Veduta di Castel St Angelo. S. Pietro,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (10),false,true,288406,Photographs,Photograph,S. Pietro in Vaticano,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (11),false,true,288407,Photographs,Photograph,Foro e Colonna di Trajano,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 11/16 in. (28.8 × 22 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (12),false,true,288408,Photographs,Photograph,Tempio di Vesta,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 6 5/16 × 8 7/16 in. (16 × 21.5 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (13),false,true,288409,Photographs,Photograph,Fontana delle Tartarughe,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 11/16 in. (28.8 × 22 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (14),false,true,288410,Photographs,Photograph,"L'arco degli argentari, adossato alla chiesa di San Giorgio al Velabro",,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 11/16 in. (28.8 × 22 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288410,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (15),false,true,288411,Photographs,Photograph,Uno dei Colossi di Campedoglio,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 11/16 in. (28.8 × 22 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (16),false,true,288412,Photographs,Photograph,Campedoglio,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (17),false,true,288413,Photographs,Photograph,Arco di Settimio Severo,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (18),false,true,288414,Photographs,Photograph,Veduta del Foro Romano,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (19),false,true,288415,Photographs,Photograph,Campo Vaccino (Foro Romano),,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 1/16 × 11 3/16 in. (20.5 × 28.4 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (20),false,true,288416,Photographs,Photograph,Arco di Giano,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (21),false,true,287890,Photographs,Photograph,Tempo della Fortuna Virile,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (22),false,true,288417,Photographs,Photograph,Tempio della Concordia,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (23),false,true,288418,Photographs,Photograph,Rovine del Foro,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 11/16 in. (28.7 × 22 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (24),false,true,288419,Photographs,Photograph,Tempio di Antonino e Faustina,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 11/16 in. (28.7 × 22 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (25),false,true,288420,Photographs,Photograph,Tempio della Pace,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (26),false,true,288421,Photographs,Photograph,Arco di Tito,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Album print from glass negative,Image: 6 7/16 × 8 7/16 in. (16.4 × 21.5 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (27),false,true,288422,Photographs,Photograph,Bassorilievo dell'arco di Tito,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 5/8 in. (28.8 × 21.9 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (28),false,true,288423,Photographs,Photograph,Bassorilievo dell'arco di Tito,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 11 5/16 × 8 5/8 in. (28.8 × 21.9 cm) Sheet: 18 1/2 × 12 1/8 in. (47 × 30.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (29),false,true,288424,Photographs,Photograph,Colosseo (Anfiteatro di Flavio),,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Salted paper print from glass negative,Image: 6 7/16 × 8 11/16 in. (16.3 × 22 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (30),false,true,288425,Photographs,Photograph,Arco di Constantino,,,,,,Artist,,Eugène Constant,"French, active Italy, 1848–55",,"Constant, Eugène",French,1848,1855,1848–52,1848,1852,Albumen print from glass negative,Image: 8 11/16 × 11 5/16 in. (22.1 × 28.7 cm) Sheet: 12 1/8 × 18 1/2 in. (30.8 × 47 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.315,false,true,294767,Photographs,Photograph,Ancienne Ferme à Pérenchies,,,,,,Artist,,Alphonse Le Blondel,"French, Bréhal 1814–1875 Lille",,"Le Blondel, Alphonse",French,1814,1875,1854,1854,1854,Salted paper print from paper negative,Image: 23.5 x 29.4 cm (9 1/4 x 11 9/16 in.) Mount: 26 x 37.4 cm (10 1/4 x 14 3/4 in.),"Purchase, Alfred Stieglitz Society Gifts, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.31,false,true,283105,Photographs,Photograph,[Postmortem],,,,,,Artist,,Alphonse Le Blondel,"French, Bréhal 1814–1875 Lille",,"Le Blondel, Alphonse",French,1814,1875,ca. 1850,1848,1852,Daguerreotype,Image (visible): 3 1/2 × 4 11/16 in. (8.9 × 11.9 cm) Overall: 6 11/16 × 8 1/4 in. (17 × 21 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.648.3a,false,true,270074,Photographs,Photograph,Etienne Carjat,,,,,,Artist,,Étienne Carjat,"French, Fareins 1828–1906 Paris",,"Carjat, Étienne",French,1828,1906,1870s,1870,1879,Albumen silver print,,"Gift of Helen and Janos Scholz, 1958",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.633.1,false,true,261621,Photographs,Photograph,Jean Baptiste Camille Corot,,,,,,Artist,,Étienne Carjat,"French, Fareins 1828–1906 Paris",,"Carjat, Étienne",French,1828,1906,1870,1870,1870,Albumen silver print,,"Gift of M. Roy Fisher, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.446.2,false,true,420964,Photographs,Print,"Henry Monnier, from Galerie contemporaine, littéraire, artistique",,,,,,Artist,,Étienne Carjat,"French, Fareins 1828–1906 Paris",,"Carjat, Étienne",French,1828,1906,1876–1881,1876,1881,Woodburytype,9 1/2 x 7 1/2 in. (24.1 x 19.1 cm),"Gift of Eric G. Carlson, in honor of Patricia Mainardi, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/420964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.41,false,true,283115,Photographs,Photograph,[Seated Female Nude],,,,,,Artist,,Eugène Durieu,"French, Nîmes 1800–1874 Geneva",,"Durieu, Eugène",French,1800,1874,1853–54,1853,1854,Albumen silver print from glass negative,Image: 6 13/16 × 4 11/16 in. (17.3 × 11.9 cm) Mount: 13 13/16 × 10 9/16 in. (35.1 × 26.9 cm),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.569,false,true,286255,Photographs,Photograph,[Nude Study],,,,,,Artist,,Eugène Durieu,"French, Nîmes 1800–1874 Geneva",,"Durieu, Eugène",French,1800,1874,1853–54,1853,1854,Albumen silver print from glass negative,Image: 20.5 x 12.8 cm (8 1/16 x 5 1/16 in.) Mount: 32.3 x 27 cm (12 11/16 x 10 5/8 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.724,false,true,286140,Photographs,Photograph,[Nude],,,,,,Artist,Possibly by,Eugène Durieu,"French, Nîmes 1800–1874 Geneva",,"Durieu, Eugène",French,1800,1874,ca. 1851,1849,1853,Salted paper print from paper negative,4 5/16 x 6 1/8,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.254,false,true,286667,Photographs,Photograph,"Wassileh and Lhedeh, Ghawagea",,,,,,Artist,,Ernest Benecke,"German, born England, 1817–1894",,"Benecke, Ernest",French,1817,1894,1852,1852,1852,Salted paper print from paper negative,Mount: 17 5/8 in. × 12 7/16 in. (44.8 × 31.6 cm) Image: 8 9/16 × 6 7/8 in. (21.8 × 17.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.257,false,true,285462,Photographs,Photograph,"[Children from the Village of Kalabshah, Nubia]",,,,,,Artist,,Ernest Benecke,"German, born England, 1817–1894",,"Benecke, Ernest",French,1817,1894,1852,1852,1852,Salted paper print from paper negative,Image: 17 x 21.5 cm (6 11/16 x 8 7/16 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.262,false,true,286501,Photographs,Photograph,"Autopsy of the First Crocodile Onboard, Upper Egypt",,,,,,Artist,,Ernest Benecke,"German, born England, 1817–1894",,"Benecke, Ernest",French,1817,1894,1852,1852,1852,Salted paper print from paper negative,Image: 17.5 x 21.3 cm (6 7/8 x 8 3/8 in.) Mount: 31.5 x 45.1 cm (12 3/8 x 17 3/4 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.318,false,true,286268,Photographs,Photograph,[Two Women],,,,,,Artist,,Ernest Benecke,"German, born England, 1817–1894",,"Benecke, Ernest",French,1817,1894,1852,1852,1852,Salted paper print from paper negative,Image: 8 1/4 × 6 1/8 in. (21 × 15.6 cm) Mount: 17 1/16 × 12 3/16 in. (43.4 × 30.9 cm),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.319,false,true,286666,Photographs,Photograph,Egyptian Musicians (Rawabí) and Almée,,,,,,Artist,,Ernest Benecke,"German, born England, 1817–1894",,"Benecke, Ernest",French,1817,1894,"February 20, 1852",1852,1852,Salted paper print from paper negative,Image: 6 9/16 × 8 1/8 in. (16.7 × 20.6 cm) Mount: 12 3/8 × 17 5/8 in. (31.4 × 44.8 cm),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.320,false,true,286502,Photographs,Photograph,Abu Nabut and Negro Slaves in Cairo,,,,,,Artist,,Ernest Benecke,"German, born England, 1817–1894",,"Benecke, Ernest",French,1817,1894,"April 22, 1852",1852,1852,Salted paper print from paper negative,Image: 21.8 x 17.2 cm (8 9/16 x 6 3/4 in.) Mount: 44.6 x 31.7 cm (17 9/16 x 12 1/2 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.548,false,true,270485,Photographs,Photograph,"Shop front of ""Courone d'or,"" Quai Bourbon",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1922,1922,1922,Gelatin silver print,Image: 17.7 x 22.4 cm (6 15/16 x 8 13/16 in.),"David Hunter McAlpin Fund, 1962",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.271,true,true,267042,Photographs,Photograph,Avenue des Gobelins,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1927,1927,1927,Gelatin silver print from glass negative,36.8 x 28.6 cm (14 1/2 x 11 1/4 in.),"Purchase, Rogers Fund, and Joyce and Robert Menschel and Harriette and Noel Levine Gifts, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.1,false,true,269874,Photographs,Photograph,St. Cloud,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1915–19, printed 1956",1915,1919,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.2,false,true,269885,Photographs,Photograph,Nenuphars,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1890s–1920s, printed 1956",1890,1929,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.3,false,true,269887,Photographs,Photograph,Men's Fashions,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1925, printed 1956",1925,1925,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.4,false,true,269888,Photographs,Photograph,Eclipse,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1911, printed 1956",1911,1911,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.5,false,true,269889,Photographs,Photograph,Paris Interior,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"ca. 1910, printed 1956",1908,1912,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.6,false,true,269890,Photographs,Photograph,Pompe Funebre (1e Classe),,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1910, printed 1956",1910,1910,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.7,false,true,269891,Photographs,Photograph,Carrousel,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1923, printed 1956",1923,1923,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.8,false,true,269892,Photographs,Photograph,Marchand Abat-Jours,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1899–1900, printed 1956",1899,1900,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.9,false,true,269893,Photographs,Photograph,"Rue St. Rustique, Montmartre",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1922, printed 1956",1922,1922,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1202,false,true,265031,Photographs,Photograph,"A la Biche, rue Geoffrey Hilaire",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1922,1922,1922,Matte albumen silver print,,"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1031,false,true,266094,Photographs,Photograph,"Versailles, La Terre par Massou",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1922–23,1922,1923,Albumen silver print from glass negative,21.7 x 17.6 cm. (8 9/16 x 6 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1233,true,true,266538,Photographs,Photograph,"15, rue Maître-Albert",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1912,1912,1912,Gelatin silver print from glass negative,23.2 x 17.6 cm (9 1/8 x 6 15/16 in.),"Rogers Fund, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266538,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5116,false,true,266754,Photographs,Photograph,"Hôtel de Lauzun, Quai d'Anjou",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1904–05,1904,1905,Albumen silver print from glass negative,21.6 x 17.8 cm. (8 1/2 x 7 in.),"Gift of Virginia M. Zabriskie, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5152,true,true,266853,Photographs,Photograph,"Versailles, France",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1923,1923,1923,Albumen silver print from glass negative,17.8 x 21.9 cm (7 x 8 5/8 in.),"The Samuel J. Wagstaff Jr. Memorial and David Hunter McAlpin Funds, The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, and Paul F. Walter, and Mr. and Mrs. John Walsh Gifts, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.10,false,true,269875,Photographs,Photograph,Maison Close,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1921, printed 1956",1921,1921,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.11,false,true,269876,Photographs,Photograph,Bar de Cabaret,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1900–1911, printed 1956",1900,1911,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.12,false,true,269877,Photographs,Photograph,Street Paver,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1899–1900, printed 1956",1899,1900,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.13,false,true,269878,Photographs,Photograph,"Cour, rue de Valence",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1922, printed 1956",1922,1922,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.14,false,true,269879,Photographs,Photograph,Ragpickers' Hut,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1910, printed 1956",1910,1910,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.15,false,true,269880,Photographs,Photograph,Mannequin,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1926–27, printed 1956",1926,1927,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.16,false,true,269881,Photographs,Photograph,Street Musicians,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1898–99, printed 1956",1898,1899,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.17,false,true,269882,Photographs,Photograph,"Boucherie, Rue Christine",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1920s, printed 1956",1900,1929,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.18,false,true,269883,Photographs,Photograph,"Faucheurs, Somme",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1890–98, printed 1956",1890,1898,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.19,false,true,269884,Photographs,Photograph,Environs of Paris,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1920s, printed 1956",1920,1929,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.610.20,false,true,269886,Photographs,Photograph,Masque Antique,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,"1890s–1920s, printed 1956",1890,1929,Gelatin silver print,,"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.594.1,false,true,259729,Photographs,Photograph,St. Cloud near Paris,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1923,1923,1923,Gelatin silver print,Image: 18.2 x 22.2 cm (7 3/16 x 8 3/4 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, by exchange, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.594.2,false,true,259730,Photographs,Photograph,"Rue Laplace and Rue Valette, Paris",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1926,1926,1926,Gelatin silver print,Image: 22 x 17.6 cm (8 11/16 x 6 15/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, by exchange, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.645.1,false,true,261632,Photographs,Photograph,"Coin rue du Cimitière, Saint-Benoît",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1923,1923,1923,Gelatin silver print,,"Gift of A. Hyatt Mayor, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.398.2,false,true,282111,Photographs,Photograph,"Hotel de Sens, rue de l'Hôtel de Ville, Paris",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,early 1900s,1900,1909,Albumen silver print from glass negative,16.7 x 20.5 cm (6 9/16 x 8 1/16 in. ),"The Rubel Collection, Gift of William Rubel, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1026.1,false,true,266231,Photographs,Photograph,Intérieur Rue de Vaugirard,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1910,1910,1910,Albumen silver print from glass negative,22.0 x 17.9 cm. (8 11/16 x 7 1/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1026.2,false,true,266232,Photographs,Photograph,Cuisine,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1910,1908,1912,Albumen silver print from glass negative,21.4 x 17.2 cm. (8 7/16 x 6 3/4 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1026.3,false,true,266233,Photographs,Photograph,[Atget's Work Room with Contact Printing Frames],,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1910,1908,1912,Albumen silver print from glass negative,20.9 x 17.3 cm. (8 1/4 x 6 13/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1990",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.129,false,true,283261,Photographs,Photograph,Organ-grinder,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1898–99,1898,1899,Matte albumen silver print from glass negative,Image: 22.4 × 17.6 cm (22.4 × 17.6 cm) Sheet: 22.5 × 18 cm (8 7/8 × 7 1/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.132,false,true,283264,Photographs,Photograph,"Versailles, The Orangerie Staircase",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1901,1901,1901,Albumen silver print from glass negative,Image: 16.9 x 21.7cm (6 5/8 x 8 9/16in.) Mount: 32.8 x 25.3 cm (12 15/16 x 9 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.133,false,true,283265,Photographs,Photograph,"Café, Avenue de la Grande-Armée",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1924–25,1924,1925,Matte albumen silver print from glass negative,17.6 × 22.7 cm (6 15/16 × 8 15/16 in.) Sheet: 17.9 × 22.8 cm (17.9 × 22.8 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.134,false,true,283266,Photographs,Photograph,Rue Asselin,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1924–25,1924,1925,Matte albumen silver print from glass negative,22.9 x 17.6cm (9 x 6 15/16in.) Sheet: 23.1 × 18 cm (9 1/8 × 7 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.344,false,true,286477,Photographs,Photograph,Boutique Fleurs,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1925,1924,1924,Matte albumen silver print from glass negative,Image: 22.5 × 17.7 cm (8 13/16 × 6 15/16 in.) Sheet: 22.5 × 18.2 cm (8 7/8 × 7 3/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.435,false,true,285848,Photographs,Photograph,"Etang, Ville-d'Avray",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1923–25,1923,1925,Matte albumen silver print from glass negative,Image: 17.4 x 22.5 cm (6 7/8 x 8 7/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.511,false,true,286216,Photographs,Photograph,"Boulevard de Strasbourg, Corsets, Paris",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1912,1912,1912,Gelatin silver print from glass negative,Image: 22.4 x 17.5 cm (8 13/16 x 6 7/8 in.) Mount: 36.7 x 28.7 cm (14 7/16 x 11 5/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.512,false,true,285679,Photographs,Photograph,Versailles,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1924–25,1924,1925,Salted paper print from glass negative,Image: 17.5 x 21.9 cm (6 7/8 x 8 5/8 in.) Sheet: 18 × 21.9 cm (7 1/16 × 8 5/8 in.) Mat: 16 × 20 in. (40.6 × 50.8 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.513,false,true,286674,Photographs,Photograph,"Boutique Journaux, Rue de Sèvres, Paris",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1910–11,1910,1911,Matte albumen silver print from glass negative,Image: 22.7 x 18 cm (8 15/16 x 7 1/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.514,false,true,285764,Photographs,Photograph,Versailles,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1922–27,1922,1927,Albumen silver print from glass negative,Image: 21.3 x 17.7 cm (8 3/8 x 6 15/16 in.) Mount: 31.4 x 29.3 cm (12 3/8 x 11 9/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.515,false,true,286680,Photographs,Photograph,"Cabaret de l'Homme Armé, 25 rue des Blancs-Manteaux",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1900,1900,1900,Albumen silver print from glass negative,Image: 21.3 x 17.4 cm (8 3/8 x 6 7/8 in.) Mount: 35.4 x 27.3 cm (13 15/16 x 10 3/4 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.516,false,true,286197,Photographs,Photograph,"Rouen, Place Eau-de-Robec",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1908,1908,1908,Matte albumen silver print from glass negative,Image: 22.7 x 17.4 cm (8 15/16 x 6 7/8 in.) Sheet: 22.9 × 17.7 cm (9 in. × 6 15/16 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.517,false,true,285966,Photographs,Photograph,Grand Trianon,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1903,1903,1903,Matte albumen silver print from glass negative,Image: 17.2 x 23.15,"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.518,false,true,286577,Photographs,Photograph,Lys,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,before 1900,1892,1900,Albumen silver print from glass negatives,Image: 21.9 x 17.7 cm (8 5/8 x 6 15/16 in.) Sheet: 22 × 18.2 cm (8 11/16 × 7 3/16 in.),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.519,false,true,286211,Photographs,Photograph,"St. Cloud, Fin mai, 7 h. soir",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1922,1922,1922,Matte albumen silver print from glass negative,Image: 21.8 × 17.3 cm (8 9/16 × 6 13/16 in.) Mount: 35.5 × 28.6 cm (14 in. × 11 1/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.520,false,true,285738,Photographs,Photograph,Notre-Dame depuis le quai de la Tournelle,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1923,1923,1923,Albumen silver print from glass negative,"Image: 17.6 × 22.1 cm (6 15/16 × 8 11/16 in.) Sheet: 18.2 × 22.1 cm (7 3/16 in., 22.1 cm)","Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.521,false,true,286644,Photographs,Photograph,Water Lilies,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1910 or earlier,1910,1910,Gelatin silver print from glass negative,Image: 17.6 x 22.7 cm (6 15/16 x 8 15/16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.522,false,true,286417,Photographs,Photograph,"Maison Close, Versailles",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1921,1919,1923,Albumen silver print from glass negative,Image: 22 × 17.7 cm (8 11/16 × 6 15/16 in.) Sheet: 22 × 18.3 cm (8 11/16 × 7 3/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.523,false,true,285676,Photographs,Photograph,"La Bièvre, Boulevard d'Italie 13ème disparue en 1891; aujourd'hui rue Edmond Gondinet",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1898,1896,1900,Albumen silver print from glass negative,Image: 21.5 x 17.1 cm (8 7/16 x 6 3/4 in.) Mount: 37.2 x 28.8 cm (14 5/8 x 11 5/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.524,false,true,285812,Photographs,Photograph,Joueur de Guitare,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1900,1900,1900,Albumen silver print from glass negative,Image: 22.2 x 17.3 cm (8 3/4 x 6 13/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.525,false,true,285809,Photographs,Photograph,"Parc de Saint-Cloud, Paris",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1915–20,1913,1922,Matte albumen silver print from glass negative,Image: 17.2 x 22.5 cm (6 3/4 x 8 7/8 in.) Mount: 36.7 x 28.6 cm (14 7/16 x 11 1/4 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.526,false,true,285810,Photographs,Photograph,Facteur,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1900,1898,1902,Albumen silver print from glass negative,21.7 x 16.7 cm (8 9/16 x 6 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.527,false,true,286380,Photographs,Photograph,Small Market in Front of the Church in Place Saint-Médard,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1898,1898,1898,Gelatin silver print from glass negative,22.1 x 16.9 cm (8 11/16 x 6 5/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.528,false,true,285850,Photographs,Photograph,Vannier,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1910–12,1910,1912,Gelatin silver print from glass negative,Image: 16.3 x 22.3 cm (6 7/16 x 8 3/4 in.) Mount: 36.8 x 29.6 cm (14 1/2 x 11 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.529,false,true,286679,Photographs,Photograph,Jardin du Luxembourg,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1902,1902,1902,Albumen silver print from glass negative,Image: 22.1 × 17.5 cm (8 11/16 × 6 7/8 in.) Sheet: 22.1 × 17.7 cm (8 11/16 × 6 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.530,false,true,286675,Photographs,Photograph,"Terre-plein du Pont Neuf, Matinée d'Hiver",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1925,1925,1925,Gelatin silver print from glass negative,Image: 17.5 x 22.8 cm (6 7/8 x 9 in.) Sheet: 17.8 × 22.8 cm (7 × 9 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.531,false,true,285678,Photographs,Photograph,"Fontaine, Sceaux",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1921,1919,1923,Gelatin silver print from glass negative,Image: 22.1 x 17.3 cm (8 11/16 x 6 13/16 in.) Mount: 37.1 x 28.8 cm (14 5/8 x 11 5/16 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.532,false,true,286125,Photographs,Photograph,"Le Château, fin Octobre, le soir, effet d'orage, vue prise du Parterre du Nord",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1903,1903,1903,Albumen silver print from glass negative,Image: 17.4 × 21.6 cm (6 7/8 × 8 1/2 in.) Sheet: 17.6 × 21.6 cm (6 15/16 × 8 1/2 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.533,false,true,286669,Photographs,Photograph,Versailles - Cour du Parc,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1902,1902,1902,Albumen silver print from glass negative,Image: 21.5 × 17.5 cm (8 7/16 × 6 7/8 in.) Sheet: 21.5 × 17.9 cm (8 7/16 × 7 1/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.534,false,true,286670,Photographs,Photograph,"St. Denis, Ancien Relais de la Poste d'Ecouen, Hôtel du Grand Cerf",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1900,1898,1902,Albumen silver print from glass negative,Image: 17.4 × 21.8 cm (6 7/8 × 8 9/16 in.) Sheet: 17.6 × 21.8 cm (6 15/16 × 8 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.535,false,true,286693,Photographs,Photograph,Rue de la Montagne-Sainte-Geneviève,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1924,1924,1924,Matte albumen silver print from glass negative,Image: 17.4 x 22.1 cm (6 7/8 x 8 11/16 in.) Sheet: 17.7 × 22.5 cm (17.7 × 22.5 cm),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.536,false,true,286700,Photographs,Photograph,Boulevard de Strasbourg,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1912,1912,1912,Matte albumen silver print from glass negative,Image: 22.7 × 17.7 cm (8 15/16 × 6 15/16 in.) Sheet: 23 × 18 cm (9 1/16 × 7 1/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.537,false,true,286317,Photographs,Photograph,"Quai d'Anjou, 6h du matin",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1924,1924,1924,Albumen silver print from glass negative,17.7 x 22.8 cm (6 15/16 x 8 15/16 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.538,false,true,286682,Photographs,Photograph,"Fête, Avenue de Breteuil",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1913,1913,1913,Matte albumen silver print from glass negative,Image: 17.5 x 23 cm (6 7/8 x 9 1/16 in.) Sheet: 18 × 23 cm (7 1/16 × 9 1/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.539,false,true,286032,Photographs,Photograph,Agave du Mexique,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1920s,1920,1929,Albumen silver print from glass negative,Image: 17.7 x 22.2 cm (6 15/16 x 8 3/4 in.) Sheet: 18 × 22.2 cm (7 1/16 × 8 3/4 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.540,false,true,286678,Photographs,Photograph,97 Rue du Bac,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1903,1903,1903,Albumen silver print from glass negative,21.5 x 17 cm (8 7/16 x 6 11/16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.541,false,true,285706,Photographs,Photograph,Boats at La Rochelle,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,ca. 1896,1894,1898,Matte albumen silver print from glass negative,"Image: 16.6 x 22.5 cm (6 9/16 x 8 7/8 in.) Mount: 35.5 x 30.4 cm (14 x 11 15/16 in.), modern mount","Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.542,false,true,286662,Photographs,Photograph,Jardin des Plantes,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1898–1900,1898,1900,Albumen silver print from glass negative,Image: 17.5 × 22 cm (6 7/8 × 8 11/16 in.) Sheet: 17.8 × 22 cm (17.8 × 22 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.543,false,true,285620,Photographs,Photograph,Trianon,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1923–25,1923,1925,Matte albumen silver print from glass negative,Image: 17 x 22.6 cm (6 11/16 x 8 7/8 in.) Mount: 19 x 25.2 cm (7 1/2 x 9 15/16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.544,false,true,285818,Photographs,Photograph,Blés,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1900,1900,1900,Albumen silver print from glass negative,Image: 22.5 × 17.4 cm (8 7/8 × 6 7/8 in.) Sheet: 22.5 × 17.8 cm (8 7/8 in. × 7 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.545,false,true,285724,Photographs,Photograph,[Window],,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1913,1913,1913,Albumen silver print from glass negative,21.5 x 17.4 cm (8 7/16 x 6 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.546,false,true,285845,Photographs,Photograph,Saint-Cloud,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,July 1921,1921,1921,Albumen silver print from glass negative,Image: 17.5 x 21.5 cm (6 7/8 x 8 7/16 in.) Mount: 37.1 x 28.7 cm (14 5/8 x 11 5/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.547,false,true,286609,Photographs,Photograph,Parc de Sceaux,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1925,1925,1925,Gelatin silver print from glass negative,Image: 17.7 x 22.5 cm (6 15/16 x 8 7/8 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.548,false,true,286045,Photographs,Photograph,"Marchand de Vin, Rue Boyer, Paris",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1910–11,1910,1911,Albumen silver print from glass negative,Image: 21.5 x 17.6 cm (8 7/16 x 6 15/16 in.) Sheet: 21.5 × 18.1 cm (8 7/16 × 7 1/8 in.),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.549,false,true,286476,Photographs,Photograph,"Boutique de fruits et légumes, Rue Mouffetard",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1925,1925,1925,Gelatin silver print from glass negative,Image: 22.6 x 17.9 cm (8 7/8 x 7 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.550,false,true,285891,Photographs,Photograph,"Cour, 7 rue de Valence",,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1922,1922,1922,Gelatin silver print from glass negative,Image: 17.2 x 22.7 cm (6 3/4 x 8 15/16 in.) Mount: 36.7 x 28.7 cm (14 7/16 x 11 5/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.113,false,true,265148,Photographs,Photograph,Avenue des Gobelins,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1925,1925,1925,Gelatin silver print from glass negative,21.9 x 17.3 cm (8 5/8 x 6 13/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.353,false,true,265413,Photographs,Photograph,Versailles,,,,,,Artist,,Eugène Atget,"French, Libourne 1857–1927 Paris",,"Atget, Eugène",French,1857-02-02,1927-08-04,1923,1923,1923,Albumen silver print from glass negative,16.9 x 21.5 cm (6 5/8 x 8 7/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.175,false,true,285969,Photographs,Photograph,"Compiègne, Présentation du Prince Impérial",,,,,,Artist,,Olympe Aguado de las Marismas,"French, Paris 1827–1894 Compiegne",,"Aguado de las Marismas, Olympe",French,1827,1894,1856,1856,1856,Albumen silver print from glass negative,Image: 11.3 × 18.8 cm (4 7/16 × 7 3/8 in.) Mount: 30.7 × 42.3 cm (12 1/16 × 16 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.249,false,true,286647,Photographs,Photograph,"[The Artist, His Mother, and Friends in Fishing Garb]",,,,,,Artist,,Olympe Aguado de las Marismas,"French, Paris 1827–1894 Compiegne",,"Aguado de las Marismas, Olympe",French,1827,1894,ca. 1860,1858,1862,Albumen silver print from glass negative,"Image: 17.7 × 14.6 cm (6 15/16 × 5 3/4 in.) Sheet: 19 × 14.6 cm (7 1/2 × 5 3/4 in.), irregularly trimmed","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.283,false,true,286649,Photographs,Photograph,Quelques familiers de Louis Robert,,,,,,Artist,,Olympe Aguado de las Marismas,"French, Paris 1827–1894 Compiegne",,"Aguado de las Marismas, Olympe",French,1827,1894,ca. 1860,1858,1862,Albumen silver print from glass negative,Image: 17.1 × 14.3 cm (6 3/4 × 5 5/8 in.) Sheet: 17.5 × 14.3 cm (6 7/8 × 5 5/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.825,false,true,286697,Photographs,Photograph,Emperor Napoleon III,,,,,,Artist,,Olympe Aguado de las Marismas,"French, Paris 1827–1894 Compiegne",,"Aguado de las Marismas, Olympe",French,1827,1894,March 1860,1860,1860,Albumen silver print,"Image: 13.7 × 9.1 cm (13.7 × 9.1 cm), arched top Sheet: 14.2 × 9.6 cm (14.2 × 9.6 cm)","Gilman Collection, Purchase, Gift of The Howard Gilman Foundation, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.826,false,true,286113,Photographs,Photograph,Empress Eugénie,,,,,,Artist,,Olympe Aguado de las Marismas,"French, Paris 1827–1894 Compiegne",,"Aguado de las Marismas, Olympe",French,1827,1894,1860,1860,1860,Albumen silver print from glass negative,"Image: 13.6 × 9.2 cm (5 3/8 × 3 5/8 in.), arched top Sheet: 14.1 × 9.5 cm (5 9/16 × 3 3/4 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.882,false,true,286650,Photographs,Photograph,Louis Robert and Olympe Aguado,,,,,,Artist,,Olympe Aguado de las Marismas,"French, Paris 1827–1894 Compiegne",,"Aguado de las Marismas, Olympe",French,1827,1894,ca. 1860,1858,1862,Albumen silver print from glass negative,Image: 14.7 x 19.1 cm (5 13/16 x 7 1/2 in.) Mount: 23.2 x 29.4 cm (9 1/8 x 11 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1161,true,true,265726,Photographs,Photograph,"[Flower Study, Rose of Sharon]",,,,,,Artist,,Adolphe Braun,"French, Besançon 1811–1877 Dornach",,"Braun, Adolphe",French,1811,1877,ca. 1854,1852,1856,Albumen silver print from glass negative,37.5 x 41.9 cm (14 3/4 x 16 1/2 in.),"Gift of Gilman Paper Company, in memory of Samuel J. Wagstaff Jr., 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.149.53,false,true,269118,Photographs,Photograph,Pheasant and Grouse,,,,,,Artist,,Adolphe Braun,"French, Besançon 1811–1877 Dornach",,"Braun, Adolphe",French,1811,1877,ca. 1865,1863,1867,Carbon print,Image: 80.3 x 47.5 cm (31 5/8 x 18 11/16 in.),"Gift of E. S. Herrmann, 1947",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.607.1,false,true,260967,Photographs,Photograph,"Haute-Egypt, Salle Hypostyle à Karnak",,,,,,Artist,,Adolphe Braun,"French, Besançon 1811–1877 Dornach",,"Braun, Adolphe",French,1811,1877,ca. 1870,1868,1872,Albumen silver print,,"Gift of Weston J. Naef, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.537,false,true,296359,Photographs,Photographs,"Château de St.-Germain-en-Laye, Intérieur de la cour, Chapelle",,,,,,Artist,,Médéric Mieusement,"French, Gonneville-la-Mallet 1840–1905 Pornic",,"Mieusement, Médéric",French,1840,1905,1862–67,1862,1867,Albumen silver print from glass negative,Image: 37.5 x 28.2 cm (14 3/4 x 11 1/8 in.) Mount: 60.3 x 48 cm (23 3/4 x 18 7/8 in.),"Purchase, Peter C. Bunnell Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.167,false,true,269086,Photographs,Photograph,[Bearded Man with Magnifying Glass Examining a Manuscript],,,,,,Artist,,Antoine-Samuel Adam-Salomon,"French, La Ferté-sous-Jouarre 1811–1881 Paris",,"Adam-Salomon, Antoine-Samuel",French,1811,1881,1870s,1870,1879,Albumen silver print from glass negative,,"Museum Accession, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.497,false,true,285816,Photographs,Photograph,[Photographic Advertisement],,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1860s,1860,1869,Albumen silver print from glass negative,Image: 23.5 x 31.8 cm (9 1/4 x 12 1/2 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (1),false,true,288171,Photographs,Photograph,"Costumes de Théâtre, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,24.4 x 31.1 cm (9 5/8 x 12 1/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (2),false,true,288172,Photographs,Photograph,"Femme Annamite, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,20 x 16.7 cm (7 7/8 x 6 9/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (3),false,true,288173,Photographs,Photograph,"Prisonniers conduits par un Mata, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,24.2 x 21 cm (9 1/2 x 8 1/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (4),false,true,288174,Photographs,Photograph,"Riche Annamite montant à cheval, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,19.1 x 25 cm (7 1/2 x 9 13/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (5),false,true,288175,Photographs,Photograph,"Enterrement, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,22.5 x 30.9 cm (8 7/8 x 12 3/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (6),false,true,288176,Photographs,Photograph,"Cérémonie religieuse dans la Pagode Chinoise de Cholen, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,22.3 x 33.4 cm (8 3/4 x 13 1/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (7),false,true,288177,Photographs,Photograph,"Cortège d'un mariage, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,20.7 x 31.1 cm (8 1/8 x 12 1/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (8),false,true,288178,Photographs,Photograph,"Vue de la Ville Chinoise (Cholen) Feuille No. 3, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,22.3 x 30.1 cm (8 3/4 x 11 7/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (9),false,true,288179,Photographs,Photograph,"Vue de la Ville Chinoise (Cholen) Feuille No. 2, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,23.5 x 31.4 cm (9 1/4 x 12 3/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (10),false,true,288180,Photographs,Photograph,"Vue de la Ville Chinoise (Cholen) Feuille No. 2, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,22.3 x 30.4 cm (8 3/4 x 11 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (11),false,true,288181,Photographs,Photograph,"Vue de la Ville Chinoise (Cholen) Feuille No. 6, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,23.8 x 31.2 cm (9 3/8 x 12 5/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (12),false,true,288182,Photographs,Photograph,Vue de la Ville Chinoise (Cholen) Feuille No. 4,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,23.9 x 30 cm (9 7/16 x 11 13/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (13),false,true,288183,Photographs,Photograph,"Vue de la Ville Chinoise (Cholen) Feuille No. 5, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,24.4 x 30.4 cm (9 5/8 x 11 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (14),false,true,288184,Photographs,Photograph,"Bonzes de la Pagoda Chinoise (Cholen), Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,24.1 x 21.5 cm (9 1/2 x 8 7/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (15),false,true,288185,Photographs,Photograph,"Femmes Annamites, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,26.3 x 23 cm (10 3/8 x 9 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (16),false,true,288186,Photographs,Photograph,"Musiciens Annamites, Saïgon, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,23.1 x 27.9 cm (9 1/8 x 11 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (17),false,true,288187,Photographs,Photograph,"Chef de Village en Costume officiel, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,20.8 x 16.3 cm (8 3/16 x 6 7/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (18),false,true,288188,Photographs,Photograph,"Marchands de Fruits, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,15.3 x 20.1 cm (6 x 7 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (19),false,true,288189,Photographs,Photograph,"Miliciens mangeant le riz, Cochinchine",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,21.1 x 16.9 cm (8 5/16 x 6 5/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (20),false,true,288190,Photographs,Photograph,Pagode de la Ville Chinoise,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,14.6 x 22.8 cm (5 3/4 x 9 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (21),false,true,288191,Photographs,Photograph,Vue de l'Etablissement des Messageries Impériales,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,13.2 x 25 cm (5 3/16 x 9 13/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (22),false,true,288192,Photographs,Photograph,Tombeau de l'Evêque d'Adran,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,16.9 x 22.7 cm (6 5/8 x 8 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (23),false,true,288193,Photographs,Photograph,Matas (Miliciens indigènes),,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,26.1 x 19.8 cm (10 1/4 x 7 13/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (24),false,true,288194,Photographs,Photograph,Vue de Saïgon (Feuille No. 2),,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,24 x 33.5 cm (9 7/16 x 13 3/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (25),false,true,288195,Photographs,Photograph,Vue de Saïgon (Feuille No. 3),,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,24.7 x 33.7 cm (9 3/4 x 13 1/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (26),false,true,288196,Photographs,Photograph,Vue de Saïgon (Feuille No. 1),,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,24.7 x 34.2 cm (9 3/4 x 13 7/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (27),false,true,288197,Photographs,Photograph,Portique d'entrée de la Grande Galerie de la Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,16.9 x 22.5 cm (6 5/8 x 8 7/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (28),false,true,288198,Photographs,Photograph,Façade Ouest de la Grande Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,25.1 x 30.3 cm (9 7/8 x 11 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (29),false,true,288199,Photographs,Photograph,Bonzerie de la Grande Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,26 x 32 cm (10 1/4 x 12 5/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (30),false,true,288200,Photographs,Photograph,Façade Nord de la Grand Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,25.2 x 31.1 cm (9 15/16 x 12 1/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (31),false,true,288201,Photographs,Photograph,Grande Pagode - Edicule Nord,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,25.6 x 30.9 cm (10 1/16 x 12 3/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (32),false,true,288202,Photographs,Photograph,Edicule extérieur Sud,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,16.5 x 22.1 cm (6 1/2 x 8 11/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (33),false,true,288203,Photographs,Photograph,Grande Pagode Porte Ouest de la 1ère enceinte,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,16.4 x 22.9 cm (6 7/16 x 9 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (34),false,true,286880,Photographs,Photograph,Angle d'Une Cour Intérieure de la Grande Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,22.7 x 17.7cm (8 15/16 x 6 15/16in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (35),false,true,288204,Photographs,Photograph,Grande Pagode - Edicule extérieur Sud,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,16.5 x 22 cm (6 1/2 x 8 11/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (36),false,true,288205,Photographs,Photograph,Grande Pagode - Colonnade de l'Esplanade,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver print from glass negative,17.3 x 12 cm (6 13/16 x 4 3/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (37),false,true,288206,Photographs,Photograph,Bonzes de la Grande Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,17 x 10.9 cm (6 11/16 x 4 5/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (38),false,true,288207,Photographs,Photograph,Bas-relief de la Grande Galerie de la Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,12.1 x 17.2 cm (4 3/4 x 6 3/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (39),false,true,288208,Photographs,Photograph,Bas-relief de la Grande Galerie de la Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,12.1 x 17.2 cm (4 3/4 x 6 3/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (40),false,true,288209,Photographs,Photograph,Grande Pagode - Tour Nord du 2e Etage,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,24.2 x 17 cm (9 1/2 x 6 11/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (41),false,true,288210,Photographs,Photograph,Détail Décoratif de la Grande Pagode,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,16.4 x 11.2 cm (6 7/16 x 4 7/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (42),false,true,288211,Photographs,Photograph,"S. M. Norodon, Roi du Cambodge",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,23.3 x 19.7 cm (9 3/16 x 7 3/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (43),false,true,288212,Photographs,Photograph,"Phra-Kéo-Pha, Frère du Roi",,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,24.8 x 20.8 cm (9 3/4 x 8 3/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (44),false,true,288213,Photographs,Photograph,Prince Cambodgien et son Cortège,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,24.2 x 30 cm (9 1/2 x 11 13/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (45),false,true,288214,Photographs,Photograph,Femmes du Prince Phra-Kéo-Pha,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,19.9 x 27.7 cm (7 13/16 x 10 7/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (46),false,true,288215,Photographs,Photograph,Nam-Vian - Tombeaux,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,16.6 x 22.7 cm (6 9/16 x 8 15/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.501 (47),false,true,288216,Photographs,Photograph,Nam-Vian - Tombeaux,,,,,,Artist,,Emile Gsell,"French, Sainte-Marie-aux-Mines 1838–1879 Vietnam",,"Gsell, Emile",French,1838,1879,1866,1866,1866,Albumen silver prints from glass negatives,15.8 x 22 cm (6 1/4 x 8 11/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.144,false,true,289043,Photographs,Photograph,[Construction Site],,,,,,Artist,,Louis Lafon,"French, active 1870s–90s",,"Lafon, Louis",French,1870,1899,1880s,1880,1889,Albumen silver print from glass negative,Image: 36.8 x 47.8 cm (14 1/2 x 18 13/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (2a),false,true,288155,Photographs,Photograph,[Unknown Sitter],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver print from glass negative,Image: 6 5/16 × 4 3/4 in. (16 × 12 cm) Mount: 14 3/16 in. × 11 in. (36 × 28 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (2b),false,true,288156,Photographs,Photograph,[Marie de Morny],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver print from glass negative,Window: 7 1/2 × 5 1/2 in. (19 × 14 cm) Image: 4 in. × 2 13/16 in. (10.2 × 7.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (5a-d),false,true,288160,Photographs,Cartes-de-visite,[Unknown Sitters and Duchesse de Morny],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (5e-h),false,true,288161,Photographs,Cartes-de-visite,"[Pourtalès, Metternich, Grande Duchesse de Mecklemboury, and Unknown Sitter]",,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (6a-d),false,true,288162,Photographs,Cartes-de-visite,[Anna Murat and Unknown Sitters],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (6e-h),false,true,288163,Photographs,Cartes-de-visite,[Comtesse Walewska and Princesse Jaochim Murat],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (7a-d),false,true,288164,Photographs,Cartes-de-visite,[Unknown Sitters],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (7e-h),false,true,288165,Photographs,Cartes-de-visite,[Unknown Sitters],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (8a-d),false,true,288166,Photographs,Cartes-de-visite,[Unknown Sitters],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.410 (8e-g),false,true,288167,Photographs,Cartes-de-visite,[Madame Demidoff and Unknown Sitters],,,,,,Artist|Artist,Painted and retouched by,Pierre-Louis Pierson|Marck,"French, 1822–1913",", et al","Pierson, Pierre-Louis|Marck",French,1822,1913,before 1865,1855,1865,Albumen silver prints from glass negatives,Image: 3 3/8 in. × 2 in. (8.6 × 5.1 cm) (each),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1097,false,true,285685,Photographs,Photograph,Ambassade Cochinchinoise à Paris,,,,,,Artist|Artist,,Louis Rousseau|Philippe Jacques Potteau,"French, 1807–1876, active 1860s",,"Rousseau, Louis|Potteau, Philippe Jacques",French,1807,1876,1863,1863,1863,Albumen silver print from glass negative,Image: 7 in. × 4 15/16 in. (17.8 × 12.5 cm) Mount: 14 13/16 × 11 1/8 in. (37.7 × 28.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.759.11,false,true,259815,Photographs,Photograph,[Reproduction of Napoleon on the Battlefield of Eylau by Antoine-Jean Gros],,,,,,Artist|Artist,After,Unknown|baron Antoine Jean Gros,"French|French, Paris 1771–1835 Meudon",,"Unknown|Gros, Antoine Jean, baron",French,1771,1835,1850s,1850,1859,Albumen silver print,,"Museum Accession, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.602.2,false,true,262236,Photographs,Photograph,Nigella Damascena Spinnenkopf,,,,,,Artist|Printer,,Karl Blossfeldt|Jürgen Wilde,"German, 1865–1932",,"Blossfeldt, Karl|Wilde, Jürgen",German,1865,1932,"ca. 1932, printed 1976",1930,1934,Gelatin silver print,25.2 x 19.3 cm. (9 15/16 x 7 5/8 in.),"Warner Communications Inc. Purchase Fund, 1978",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.267,false,true,685608,Photographs,Carte-de-visite,[Otto Eurmann?],,,,,,Photography Studio,,G. & A. Overbeck,"German, active 1860s",,G. & A. Overbeck,German,1855,1870,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.39,false,true,685381,Photographs,Carte-de-visite,[Oskar Begas],,,,,,Photography Studio,,Loescher & Petsch,"German, active ca. 1860–90",,Loescher & Petsch,German,1860,1890,before 1883,1860,1883,Albumen silver print,Approx. 10.2 x 6.4 cm (4 in. × 2 1/2 in.) (10.2 × 6.4 cm),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.517,false,true,685857,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,Loescher & Petsch,"German, active ca. 1860–90",,Loescher & Petsch,German,1860,1890,after 1867,1867,1890,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.547,false,true,685887,Photographs,Carte-de-visite,[Charles Mandel],,,,,,Photography Studio,,Loescher & Petsch,"German, active ca. 1860–90",,Loescher & Petsch,German,1860,1890,after 1867,1867,1890,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.629,false,true,685968,Photographs,Carte-de-visite,[Ludwig Johann Passini],,,,,,Photography Studio,,Loescher & Petsch,"German, active ca. 1860–90",,Loescher & Petsch,German,1860,1890,after 1867,1867,1890,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.829,false,true,686168,Photographs,Carte-de-visite,[Rildhauer Wolff],,,,,,Photography Studio,,Loescher & Petsch,"German, active ca. 1860–90",,Loescher & Petsch,German,1860,1890,after 1867,1867,1890,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.856,false,true,686195,Photographs,Carte-de-visite,Carl Becker,,,,,,Person in Photograph|Photography Studio,Person in photograph,Carl Becker|Loescher & Petsch,"German, active ca. 1860–90",,Becker Carl|Loescher & Petsch,German,1898 |1860,1898 |1890,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.890,false,true,686229,Photographs,Carte-de-visite,[Karl Heffeck],,,,,,Photography Studio,,Loescher & Petsch,"German, active ca. 1860–90",,Loescher & Petsch,German,1860,1890,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.675,false,true,686014,Photographs,Carte-de-visite,Gustav Karl Ludwig Richter,,,,,,Person in Photograph|Photography Studio,Person in photograph,Gustav Karl Ludwig Richter|Loescher & Petsch,"German, 1823–1884|German, active ca. 1860–90",,"Richter, Gustav Karl Ludwig|Loescher & Petsch",German,1823 |1860,1884 |1890,after 1867,1867,1890,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.83,false,true,685425,Photographs,Carte-de-visite,[Meyer George von Bremen],,,,,,Artist|Person in Photograph,Person in photograph,Heinrich Graf|Johann Georg Meyer,"German, active 1860s|German, Bremen 1813–1880 Berlin",,"Graf, Heinrich|Meyer, Johann Georg",German,1813,1880,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.84,false,true,685426,Photographs,Carte-de-visite,[Meyer George von Bremen],,,,,,Artist|Person in Photograph,Person in photograph,Heinrich Graf|Johann Georg Meyer,"German, active 1860s|German, Bremen 1813–1880 Berlin",,"Graf, Heinrich|Meyer, Johann Georg",German,1813,1880,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.579,false,true,685918,Photographs,Carte-de-visite,[Paul Friedrich Meyerheim],,,,,,Person in Photograph|Photography Studio,Person in photograph,Paul Friedrich Meyerheim|Loescher & Petsch,"German, Berlin 1842–1915 Berlin|German, active ca. 1860–90",,"Meyerheim, Paul Friedrich|Loescher & Petsch",German,1842 |1860,1915 |1890,after 1867,1867,1890,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.299,false,true,286358,Photographs,Photograph,Blumenbachia hieronymi,,,,,,Artist,,Karl Blossfeldt,"German, 1865–1932",,"Blossfeldt, Karl",German,1865,1932,1915–25,1915,1925,Gelatin silver print,Image: 29.8 x 23.8 cm (11 3/4 x 9 3/8 in.),"Gilman Collection, Purchase, Denise and Andrew Saul Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.246,false,true,265295,Photographs,Photograph,"[Arrangement of 12 Female Mannequin Heads, Each with Distinct Physiognomy and Period Hair Style]",,,,,,Artist,,Peter Weller,"German, 1868–1940",,"Weller, Peter",German,1868,1940,1920s–30s,1920,1939,Gelatin silver print,16.9 x 23.3 cm (6 5/8 x 9 3/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.188,false,true,285709,Photographs,Photograph,[Adolph Hitler Leaving Landsberg Prison],,,,,,Artist,,Heinrich Hoffmann,"German, 1885–1957",,"Hoffmann, Heinrich",German,1885,1957,"December 20, 1924",1924,1924,Gelatin silver print,22.2 x 16.4 cm (8 3/4 x 6 7/16 in.) Mount: 29.3 × 22 cm (11 9/16 × 8 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.334,false,true,265392,Photographs,Photograph,Junge vom Hiddensee,,,,,,Artist,,Aenne Biermann,"German, 1898–1933",,"Biermann, Aenne",German,1898,1933,ca. 1930,1928,1932,Gelatin silver print,23.5 x 17.6 cm (9 1/4 x 6 15/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.182,false,true,286038,Photographs,Photograph,Tanzbar,,,,,,Artist,,Yva (Else Simon),"German, 1900–1942",,"Simon, Else",German,1900,1942,ca. 1930,1928,1932,Gelatin silver print,Image: 24.3 x 17.8 cm (9 9/16 x 7 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1033,false,true,266095,Photographs,Photograph,"Juggernaut Car, Madras",,,,,,Artist,,Frederick Fiebig,"German, active 1840s–50s",,"Fiebig, Frederick",German,1840,1859,1850s,1850,1859,Salted paper print from paper negative,Image: 23.9 x 18.3 cm (9 7/16 x 7 3/16 in.),"Purchase, Cynthia Hazen Polsky Gift and The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.571,false,true,685910,Photographs,Carte-de-visite,Louis Leloir,,,,,,Artist,,W. Severin,"German, active 1840s–70s",,"Severin, W.",German,1800,1890,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.11,false,true,685353,Photographs,Carte-de-visite,[Wilhelm Amberg],,,,,,Artist,,Loescher & Petsch,"German, active ca. 1860–90",,Loescher & Petsch,German,1860,1890,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1020,false,true,263541,Photographs,Photograph,Tête du Sphynx,,,,,,Artist,,Wilhelm Hammerschmidt,"German, born Prussia, died 1869",,"Hammerschmidt, Wilhelm",German,,1869,ca. 1860,1858,1862,Albumen silver print from glass negative,23.8 x 31.5 cm. (9 3/8 x 12 3/8 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1076,false,true,266127,Photographs,Photograph,"Porte de la Mosque, Sultan Hassan. Partie Inférieure",,,,,,Artist,,Wilhelm Hammerschmidt,"German, born Prussia, died 1869",,"Hammerschmidt, Wilhelm",German,,1869,ca. 1860,1858,1862,Albumen silver print from glass negative,23.5 x 31.2 cm. (9 1/4 x 12 1/4 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5050,false,true,266692,Photographs,Photograph,Temple de Dandour en Nubie,,,,,,Artist,,Wilhelm Hammerschmidt,"German, born Prussia, died 1869",,"Hammerschmidt, Wilhelm",German,,1869,1860s,1860,1869,Albumen silver print from glass negative,23.7 x 31.3 cm. (9 5/16 x 12 5/16 in.),"Gift of Ezra Mack, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.61,false,true,265588,Photographs,Photograph,Madame Elena Vacarescu,,,,,,Artist,,Erich Salomon,"German, Berlin 1886–1944 Auschwitz, Poland",,"Salomon, Erich",German,1886,1944,1928,1928,1928,Gelatin silver print,12.6 x 17.6 cm (4 15/16 x 6 15/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.266,false,true,265317,Photographs,Photograph,"[Two Women, Seated and in Conversation, in Interior Setting]",,,,,,Artist,,Erich Salomon,"German, Berlin 1886–1944 Auschwitz, Poland",,"Salomon, Erich",German,1886,1944,1920s–30s,1920,1939,Gelatin silver print,16.9 x 22.8 cm. (6 5/8 x 9 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.268,false,true,265319,Photographs,Photograph,[Five Gentlemen Conversing around Table],,,,,,Artist,,Erich Salomon,"German, Berlin 1886–1944 Auschwitz, Poland",,"Salomon, Erich",German,1886,1944,1920s–30s,1920,1939,Gelatin silver print,17.4 x 23.2 cm. (6 7/8 x 9 1/8 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5013,false,true,266656,Photographs,Photographs,Horse,,,,,,Artist,,Ottomar Anschütz,"German, Lissa (Leszno, Poland) 1846–1907 Berlin",,"Anschütz, Ottomar",German,1846,1907,1884,1884,1884,Albumen silver print,,"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.965,false,true,288290,Photographs,Stereographs,"Statue of Queen Anne, St. Paul's, London",,,,,,Publisher|Artist|Person in Photograph,,European and American Views|Unknown|Queen Anne Stuart,"British, 1665–1714",,"European and American Views|Unknown|Stuart, Anne Queen",British,1665,1714,1850s–1910s,1850,1919,Albumen silver prints,Mount: 8.8 x 17.8 cm (3 7/16 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.13,false,true,685355,Photographs,Carte-de-visite,[Richard Ansdell],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.18,false,true,685360,Photographs,Carte-de-visite,[John Ballantyne],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.19,false,true,685361,Photographs,Carte-de-visite,[Edward Charles Barnes],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.22,false,true,685364,Photographs,Carte-de-visite,[Sir Charles Barry],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.23,false,true,685365,Photographs,Carte-de-visite,[Edward Middleton Barry],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.35,false,true,685377,Photographs,Carte-de-visite,[Charles Baxter],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.48,false,true,685390,Photographs,Carte-de-visite,[Charles Henry Bennett],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.91,false,true,685433,Photographs,Carte-de-visite,[Sir David Brewster],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.101,false,true,685443,Photographs,Carte-de-visite,[John Burnet],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.102,false,true,685444,Photographs,Carte-de-visite,[Frederic William Burton],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.113,false,true,685455,Photographs,Carte-de-visite,[Philip Hermogenes Calderon],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.115,false,true,685457,Photographs,Carte-de-visite,[William Callow],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.135,false,true,685477,Photographs,Carte-de-visite,[Adelaide Claxton],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.136,false,true,685478,Photographs,Carte-de-visite,[Florence Anne Claxton],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.137,false,true,685479,Photographs,Carte-de-visite,[Marshall C. Claxton],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.148,false,true,685490,Photographs,Carte-de-visite,[George Vicat Cole],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.149,false,true,685491,Photographs,Carte-de-visite,[William Collingwood],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.155,false,true,685497,Photographs,Carte-de-visite,[Edward William Cooke],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.158,false,true,685500,Photographs,Carte-de-visite,[Thomas Sidney Cooper],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.159,false,true,685501,Photographs,Carte-de-visite,[Charles West Cope],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.166,false,true,685508,Photographs,Carte-de-visite,[Samuel Cousins],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.181,false,true,685522,Photographs,Carte-de-visite,[James Francis Danby],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.182,false,true,685523,Photographs,Carte-de-visite,[Thomas Danby],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.235,false,true,685576,Photographs,Carte-de-visite,[William Charles Thomas Dobson],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.237,false,true,685578,Photographs,Carte-de-visite,[George Thomas Doo],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685578,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.242,false,true,685583,Photographs,Carte-de-visite,[Richard Doyle],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.247,false,true,685588,Photographs,Carte-de-visite,[Edward Duncan],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.256,false,true,685597,Photographs,Carte-de-visite,[Joseph Durham],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.258,false,true,685599,Photographs,Carte-de-visite,[William Dyce],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.260,false,true,685601,Photographs,Carte-de-visite,[Sir Charles Lock Eastlake],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.266,false,true,685607,Photographs,Carte-de-visite,[Alfred Elmore],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.271,false,true,685612,Photographs,Carte-de-visite,[William Etty],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.273,false,true,685614,Photographs,Carte-de-visite,[Thomas Faed],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.277,false,true,685618,Photographs,Carte-de-visite,[Frederick William Fairholt],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.284,false,true,685625,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.289,false,true,685630,Photographs,Carte-de-visite,[Sir William Boxall],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.290,false,true,685631,Photographs,Carte-de-visite,[George Price Boyce],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.317,false,true,685658,Photographs,Carte-de-visite,[Alfred Downing Fripp],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.321,false,true,685662,Photographs,Carte-de-visite,[William Powell Frith],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.324,false,true,685665,Photographs,Carte-de-visite,[William Edward Frost],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.339,false,true,685679,Photographs,Carte-de-visite,[John Gibson],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.347,false,true,685687,Photographs,Carte-de-visite,[Margaret Gillies],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.354,false,true,685694,Photographs,Carte-de-visite,[Edward Alfred Goodall],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.355,false,true,685695,Photographs,Carte-de-visite,[Frederick Goodall],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.363,false,true,685703,Photographs,Carte-de-visite,[Robert Graves],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.371,false,true,685711,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.372,false,true,685712,Photographs,Carte-de-visite,[Carl Haag],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.378,false,true,685718,Photographs,Carte-de-visite,[Robert Hannah],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.386,false,true,685726,Photographs,Carte-de-visite,[Solomon Alexander Hart],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.388,false,true,685728,Photographs,Carte-de-visite,[William Harvey],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685728,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.391,false,true,685731,Photographs,Carte-de-visite,[Edwin Hayes],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.406,false,true,685746,Photographs,Carte-de-visite,[George Edwards Hering],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.416,false,true,685756,Photographs,Carte-de-visite,[James Clarke Hook],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.420,false,true,685760,Photographs,Carte-de-visite,[John Callcott Horsley],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.426,false,true,685766,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.445,false,true,685785,Photographs,Carte-de-visite,[Samuel Philips Jackson],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.455,false,true,685795,Photographs,Carte-de-visite,[Joseph John Jenkins],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.459,false,true,685799,Photographs,Carte-de-visite,[Alexander Johnston],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.460,false,true,685800,Photographs,Carte-de-visite,[E.B. Jones],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.461,false,true,685801,Photographs,Carte-de-visite,[George Jones],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.464,false,true,685804,Photographs,Carte-de-visite,[HenryJutsum],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.477,false,true,685817,Photographs,Carte-de-visite,[John Prescott Knight],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.482,false,true,685822,Photographs,Carte-de-visite,[Edward S. ? Kuntze],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.493,false,true,685833,Photographs,Carte-de-visite,[Charles Landseer],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.494,false,true,685834,Photographs,Carte-de-visite,[Thomas Landseer],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.495,false,true,685835,Photographs,Carte-de-visite,[Richard James Lane],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.500,false,true,685840,Photographs,Carte-de-visite,[Frederick Richard Lee],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.506,false,true,685846,Photographs,Carte-de-visite,[Sir Frederic Leighton],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.509,false,true,685849,Photographs,Carte-de-visite,[Henry LeJeune],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.515,false,true,685855,Photographs,Carte-de-visite,[George Dunlop Leslie],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.535,false,true,685875,Photographs,Carte-de-visite,[Charles Lucy],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.538,false,true,685878,Photographs,Carte-de-visite,[Egron Sellif Lundgren],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.540,false,true,685880,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.554,false,true,685894,Photographs,Carte-de-visite,[William Calder Marshall],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.555,false,true,685895,Photographs,Carte-de-visite,[George Mason],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.556,false,true,685896,Photographs,Carte-de-visite,[Gerald Massey],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.561,false,true,685900,Photographs,Carte-de-visite,[David Hall McKewan],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.580,false,true,685919,Photographs,Carte-de-visite,[John Everett Millais],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.581,false,true,685920,Photographs,Carte-de-visite,[John Everett Millais],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.587,false,true,685926,Photographs,Carte-de-visite,[John Henry Mole],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.588,false,true,685927,Photographs,Carte-de-visite,[John Henry Mole],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.593,false,true,685932,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.601,false,true,685940,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.606,false,true,685945,Photographs,Carte-de-visite,[Paul Jacob Naftel],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.607,false,true,685946,Photographs,Carte-de-visite,[Joseph Nash],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.609,false,true,685948,Photographs,Carte-de-visite,[Alfred Pizzey Newton],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.610,false,true,685949,Photographs,Carte-de-visite,[Erskine Nicol],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.613,false,true,685952,Photographs,Carte-de-visite,[Matthew Noble],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.617,false,true,685956,Photographs,Carte-de-visite,[William Quiller Orchardson],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.624,false,true,685963,Photographs,Carte-de-visite,[Samuel Palmer],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.637,false,true,685976,Photographs,Carte-de-visite,[John Pettie],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.641,false,true,685980,Photographs,Carte-de-visite,[Frederick Richard Pickersgill],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.642,false,true,685981,Photographs,Carte-de-visite,[Frederick Richard Pickersgill],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.649,false,true,685988,Photographs,Carte-de-visite,[Paul Falconer Poole],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.653,false,true,685992,Photographs,Carte-de-visite,[Edward John Poynter],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.659,false,true,685998,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.660,false,true,685999,Photographs,Carte-de-visite,[James Baker Pyne],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.664,false,true,686003,Photographs,Carte-de-visite,[S.Read],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.665,false,true,686004,Photographs,Carte-de-visite,[Richard Redgrave],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.674,false,true,686013,Photographs,Carte-de-visite,[George Richmond],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.688,false,true,686027,Photographs,Carte-de-visite,[William H. Robinson?],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.701,false,true,686040,Photographs,Carte-de-visite,[George Augustus Sala],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.703,false,true,686042,Photographs,Carte-de-visite,[James Sant],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.718,false,true,686057,Photographs,Carte-de-visite,[George Gilbert Scott],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.729,false,true,686068,Photographs,Carte-de-visite,[Arthur Sketchley],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.732,false,true,686071,Photographs,Carte-de-visite,[Sydney Smirke],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.733,false,true,686072,Photographs,Carte-de-visite,[Collingwood Smith],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.734,false,true,686073,Photographs,Carte-de-visite,[George Smith],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.735,false,true,686074,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.745,false,true,686084,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.750,false,true,686089,Photographs,Carte-de-visite,[Lumb Stocks],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.751,false,true,686090,Photographs,Carte-de-visite,[Marcus Stone],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.754,false,true,686093,Photographs,Carte-de-visite,[George Edward ?],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.764,false,true,686103,Photographs,Carte-de-visite,[Frederick Taylor],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.766,false,true,686105,Photographs,Carte-de-visite,[John Tenniel],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.769,false,true,686108,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.803,false,true,686142,Photographs,Carte-de-visite,[E.M.Ward],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.807,false,true,686146,Photographs,Carte-de-visite,[George Frederick Watts],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.808,false,true,686147,Photographs,Carte-de-visite,[George Frederick Watts],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.810,false,true,686149,Photographs,Carte-de-visite,[Thomas Webster],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.811,false,true,686150,Photographs,Carte-de-visite,[Henry Weekes],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.812,false,true,686151,Photographs,Carte-de-visite,[Henry Weekes],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.814,false,true,686153,Photographs,Carte-de-visite,[Henry Tanworth Wells],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.818,false,true,686157,Photographs,Carte-de-visite,[Richard Westmacott],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.820,false,true,686159,Photographs,Carte-de-visite,[Josiah Wood Whymper],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.826,false,true,686165,Photographs,Carte-de-visite,[Henry Brittan Willis],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.830,false,true,686169,Photographs,Carte-de-visite,[W.F. Woodington],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.835,false,true,686174,Photographs,Carte-de-visite,[William Frederick Yeames],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.863,false,true,686202,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.864,false,true,686203,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.867,false,true,686206,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.892,false,true,686231,Photographs,Carte-de-visite,[Professor Owen],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.917,false,true,686256,Photographs,Carte-de-visite,[Henry O'Neil],,,,,,Photography Studio,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1098.4,false,true,631028,Photographs,Photograph,"Field Marshall Lord Raglan, Crimea",,,,,,Artist|Publisher|Publisher,,"Roger Fenton|P. & D. Colnaghi & Co.|Thomas Agnew & Sons, Ltd.","British, 1819–1869|London",,"Fenton, Roger|Colnaghi & Co., P. & D.|Agnew, Thomas & Sons, Ltd.",British,1819 |1760,1819 |9999,1855,1855,1855,Salted paper print from collodion glass negative,Image: 7 13/16 × 5 7/8 in. (19.9 × 14.9 cm) Mount: 23 5/8 × 17 1/4 in. (60 × 43.8 cm),"Gift of Joyce F. Menschel, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/631028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.635.1,false,true,271528,Photographs,Photograph,Don Quixote in His Study,,,,,,Artist|Printer,,William Frederick Lake Price|J. Spencer,"British, London 1810–1896 Lee, Kent",,"Price, William Frederick Lake|Spencer, J.",British,1810-10-10,1896-12-09,1857,1857,1857,Albumen silver print from glass negative,Image: 31.9 x 28 cm (12 9/16 x 11 in.) Mount: 42.6 x 33.3 cm (16 3/4 x 13 1/8 in.),"Gift of A. Hyatt Mayor, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.772,false,true,285943,Photographs,Photograph,Louis-Jacques-Mandé Daguerre,,,,,,Artist|Person in Photograph,,John Jabez Edwin Mayall|Louis-Jacques-Mandé Daguerre,"British, Oldham, Lancashire 1813–1901 West Sussex",,"Mayall, John Jabez Edwin|Daguerre, Louis-Jacques-Mandé",British,1813,1901,ca. 1860,1858,1862,Albumen silver print from glass negative,Image: 7 7/16 × 5 5/16 in. (18.9 × 13.5 cm) Mount: 9 5/8 in. × 6 13/16 in. (24.5 × 17.3 cm),"Gilman Collection, Purchase, Warner Communications Inc. Purchase Fund, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.553,false,true,685893,Photographs,Carte-de-visite,[Baron Carlo (Charles) Marochetti],,,,,,Photography Studio|Person in Photograph,Person in photograph,John and Charles Watkins|Baron Charles Marochetti,"British, active 1867–71|Italian, Turin 1805–1867",,"John and Charles Watkins|Marochetti, Charles Baron",British,1840 |1805,1875 |1867,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.528,false,true,685868,Photographs,Carte-de-visite,[John Linnel],,,,,,Artist|Person in Photograph,Person in photograph,Eotto's School of Photography|John Linnell,"British, active 1860s|British, London 1792–1882 Redhill, Surrey",,"Eotto's School of Photography|Linnell, John",British,1792,1882,1863,1863,1863,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.523,false,true,685863,Photographs,Carte-de-visite,[John Frederick Lewis],,,,,,Photography Studio|Person in Photograph,Person in photograph,John and Charles Watkins|John Frederick Lewis,"British, active 1867–71|British, London 1805–1876 Walton-on-Thames",,"John and Charles Watkins|Lewis, John Frederick",British,1840 |1805,1875 |1876,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.686,false,true,686025,Photographs,Carte-de-visite,[David Roberts],,,,,,Person in Photograph|Photography Studio,Person in photograph,David Roberts|John and Charles Watkins,"British, Stockbridge, Scotland 1796–1864 London|British, active 1867–71",,"Roberts, David|John and Charles Watkins",British,1796 |1840,1864 |1875,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.180,false,true,685521,Photographs,Carte-de-visite,[Francis Danby],,,,,,Photography Studio|Person in Photograph,Person in photograph,John and Charles Watkins|Francis Danby,"British, active 1867–71|Irish, Killinick, County Wexford 1793–1861 Exmouth",,"John and Charles Watkins|Danby, Francis",British,1840 |1793,1875 |1861,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.55,false,true,283138,Photographs,Negative; Photograph,"Temple of Wingless Victory, Lately Restored",,,,,,Artist,,George Wilson Bridges,"British, 1788–1864",,"Bridges, George Wilson",British,1788,1864,1848,1848,1848,Paper negative,Image: 16.7 x 20.6 cm (6 9/16 x 8 1/8 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/283138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.6,false,true,282010,Photographs,Negatives; Photographs,[Botanical Specimens],,,,,,Artist,Attributed to,Robert Hunt,"British, 1807–1887",,"Hunt, Robert",British,1807,1887,ca. 1841,1839,1843,Photogenic drawing negatives on paper and fabric,"Sheet: 11 1/8 × 11 5/16 in. (28.2 × 28.8 cm), album page Top Left Image: 4 1/8 × 3 15/16 in. (10.5 × 10 cm), irregularly trimmed Top Right Image: 4 1/2 × 3 11/16 in. (11.4 × 9.4 cm), irregularly trimmed Bottom Left Image: 1 7/8 × 3 3/8 in. (4.8 × 8.6 cm) Bottom Right Image: 2 9/16 × 3 11/16 in. (6.5 × 9.3 cm), irregularly trimmed","The Rubel Collection, Purchase, Anonymous Gift, 1997",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/282010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.259,false,true,285469,Photographs,Negative; Photograph,"Crystal Palace, Hyde Park, Transept",,,,,,Artist,,Benjamin Brecknell Turner,"British, 1815–1894",,"Turner, Benjamin Brecknell",British,1815,1894,1852,1852,1852,Paper negative,Image: 30.2 x 40.1 cm (11 7/8 x 15 13/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/285469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.883,false,true,286446,Photographs,Negative; Photograph,"[Rural Manor, Possibly Bredicot]",,,,,,Artist,,Benjamin Brecknell Turner,"British, 1815–1894",,"Turner, Benjamin Brecknell",British,1815,1894,1852–54,1852,1854,Waxed paper negative,11 5/8 x 15 1/2,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/286446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (1–47),false,true,285799,Photographs,Album,Views of the Crystal Palace,,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver prints from glass negatives,12 1/16 × 10 5/16 × 1 in. (30.7 × 26.2 × 2.5 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.5,false,true,282008,Photographs,Paper negative,[Chicken Feathers],,,,,,Artist,,Nevil Story Maskelyne,"British, 1823–1911",,"Maskelyne, Nevil Story",British,1823,1911,ca. 1840,1838,1842,Photogenic drawing negative,"Image: 7 5/16 × 7 5/8 in. (18.6 × 19.3 cm), irregularly trimmed","The Rubel Collection, Purchase, Lila Acheson Wallace and Anonymous Gifts, 1997",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/282008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.284,false,true,288086,Photographs,Cabinet cards,[Follett Family Album of Children Costumed for a Fancy Dress Ball],,,,,,Artist,,Owen Angel,"British, ca. 1821–1909",,"Angel, Owen",British,1821,1909,ca. 1880,1875,1885,Albumen silver prints from glass negatives with applied color,Album: 28.6 x 22.9 x 4.4 cm (11 1/4 x 9 x 1 3/4 in.) Case,"Purchase, Joseph M. Cohen, William Talbott Hillman Foundation, Robert and Joyce Menschel Family Foundation, Robert D. and Virginia R. Joffe, Paula and Ira M. Resnick, and Maureen and Noel Testa Gifts, 2007",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/288086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.623a, b",false,true,283248,Photographs,Photograph; Photomechanical print,Aubrey Beardsley,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,ca. 1894,1892,1896,Platinum print; photogravure,13.6 x 9.7 cm (5 3/8 x 3 13/16 in.); 12.3 x 9.5 cm (4 13/16 x 3 3/4 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/283248,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1073.88,false,true,266372,Photographs,Photographically illustrated book,"Egypt, Sinai and Palestine. Supplementary Volume",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1860s,1860,1869,Albumen silver prints,,"Rogers Fund, 1908, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1073.91,false,true,266375,Photographs,Photographically illustrated book,Sinai and Palestine,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1860s,1860,1869,Albumen silver prints,,"Rogers Fund, 1908, transferred from the Library",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.550.2,false,true,269745,Photographs,Print,[Peacock],,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1925,1925,1925,Lithograph,,"Gift of Gordon Conn, 1954",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/269745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1066.1–.86,false,true,286659,Photographs,Photograph,[86 Stereographic Views of The International Exhibition of 1862],,,,,,Artist,,William England,British,,"England, William",British,,1896,1862,1862,1862,Albumen silver prints from glass negatives,,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.83,false,true,291836,Photographs,Daguerreotype,[Stereograph Still-life of Game with Rake and Onion Jar],,,,,,Artist,,T. R. Williams,"British, born 1825",,"Williams, T. R.",British,1825,1825,1854 or later,1854,1860,Daguerreotype,"Image: 7.1 x 6 cm (2 13/16 x 2 3/8 in.), each Mount: 8.3 x 17.1 cm (3 1/4 x 6 3/4 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.88,false,true,291841,Photographs,Daguerreotype,[Stereograph Still-life of Fowl with Initialed Barrel and Root Vegetables],,,,,,Artist,,T. R. Williams,"British, born 1825",,"Williams, T. R.",British,1825,1825,1850s,1850,1859,Daguerreotype,"Image: 6.8 x 5.6 cm (2 11/16 x 2 3/16 in.), each Mount: 8.3 x 17.5 cm (3 1/4 x 6 7/8 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.92,false,true,291845,Photographs,Daguerreotype,"[Stereograph Still-life with Cockatoo, Ornamental Ball, Lace, Peacock Feathers]",,,,,,Artist,,T. R. Williams,"British, born 1825",,"Williams, T. R.",British,1825,1825,1850s,1850,1859,Daguerreotype,"Image: 5.9 x 7.1 cm (2 5/16 x 2 13/16 in.), each Mount: 8.3 x 17.3 cm (3 1/4 x 6 13/16 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.93,false,true,291846,Photographs,Daguerreotype,"[Stereograph Still-life with Cockatoo, Ornamental Ball, Lace, Statuette]",,,,,,Artist,,T. R. Williams,"British, born 1825",,"Williams, T. R.",British,1825,1825,1850s,1850,1859,Daguerreotype,"Image: 7.1 x 5.9 cm (2 13/16 x 2 5/16 in.), each Mount: 8.4 x 17.3 cm (3 5/16 x 6 13/16 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.94,false,true,291847,Photographs,Daguerreotype,"[Stereograph Still-life with Cockatoo, Mirror, Ornamental Ball, Vases, and Lace]",,,,,,Artist,,T. R. Williams,"British, born 1825",,"Williams, T. R.",British,1825,1825,1850s,1850,1859,Daguerreotype,"Image: 7 x 5.9 cm (2 3/4 x 2 5/16 in.), each Mount: 8.3 x 17.5 cm (3 1/4 x 6 7/8 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.519,false,true,267097,Photographs,Photograph,"Portal of the Convent of Sancti Spiritu, Salamanca",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1853,1853,1853,Albumen silver print from paper negative,Image: 39.9 x 31.4 cm (15 11/16 x 12 3/8 in.) Mount: 61.8 x 47cm (24 5/16 x 18 1/2in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1052,false,true,265920,Photographs,Photograph,"[Puente del Diablo, Martorell]",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,ca. 1856,1854,1858,Albumen silver print from glass negative,Image: 28.4 x 41.5 cm. (11 3/16 x 16 5/16 in.),"Purchase, Joyce and Robert Menschel Gift, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1047,false,true,266109,Photographs,Photograph,"[Cloisters of the Church of Saint John of the Kings, Toledo, Spain]",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,ca. 1858,1856,1860,Albumen silver print from glass negative,41.6 x 31.7 cm. (16 3/8 x 12 1/2 in.),"Purchase, Harriette and Noel Levine Gift, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1048,false,true,266110,Photographs,Photograph,"[Madrid. Facade of the Hospital of ""La Latina""]",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,ca. 1857,1855,1859,Albumen silver print from glass negative,37.7 x 28.2 cm. (14 13/16 x 11 1/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.250.1,false,true,288044,Photographs,Photograph,Zaragoza: Porta de los Gigantes,,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1860,1860,1860,Albumen silver print from glass negative,Image: 41 x 31.7 cm (16 1/8 x 12 1/2 in.) Mount: 51 x 47.5 cm (20 1/16 x 18 11/16 in.),"Gift of C. David and Mary Robinson, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.250.2,false,true,288043,Photographs,Photograph,"[The Lion Court at the Alhambra, Viewed from Beneath the Portico Temple]",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1862,1862,1862,Albumen silver print from glass negative,Image: 42 x 32.1 cm (16 9/16 x 12 5/8 in.) Mount: 63.3 x 46.3 cm (24 15/16 x 18 1/4 in.),"Gift of C. David and Mary Robinson, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.65,false,true,283154,Photographs,Photograph,"Principal Doorway of the Carthusian Monastery, Burgos",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1853,1853,1853,Albumen silver print from paper negative,Image: 33.9 x 28.4cm (13 3/8 x 11 3/16in.) Mat: 71.1 x 55.9 cm (28 x 22 in.) Frame: 81.3 x 66 cm (32 x 26 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.250,false,true,283156,Photographs,Photograph,"The Walnut Tree of Emperor Charles V, Yuste",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1858,1858,1858,Albumen silver print from glass negative,Image: 41.6 × 30 cm (16 3/8 × 11 13/16 in.) Mount: 62.3 × 47.4 cm (24 1/2 × 18 11/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.561,false,true,286661,Photographs,Photograph,[The Armor of Philip III],,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1866,1866,1866,Albumen silver print from glass negative,Image: 33.3 x 23 cm (13 1/8 x 9 1/16 in.) Mount: 57.2 x 45.7 cm (22 1/2 x 18 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.504.12,false,true,292002,Photographs,Photograph,"Baleares, Aldeanos de Palma y sus alrrededores",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1860,1860,1860,Albumen silver print from glass negative,Image: 23.9 × 27.6 cm (9 7/16 × 10 7/8 in.) Mount: 42.3 × 58.1 cm (16 5/8 × 22 7/8 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/292002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.504.31,false,true,292003,Photographs,Photograph,"Monserrat, Vista general de la montaña desde Monistrol",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1860,1860,1860,Albumen silver print from glass negative,Image: 31.7 × 41.5 cm (12 1/2 × 16 5/16 in.) Mount: 42.3 × 58.2 cm (16 5/8 × 22 15/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/292003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.504.46,false,true,283155,Photographs,Photograph,"Zaragoza, Patio de la Casa Conocida con el Nombre de los Infantes",,,,,,Artist,,Charles Clifford,"Welsh, 1819–1863",,"Clifford, Charles",British,1819,1863,1860,1860,1860,Albumen silver print from glass negative,Image: 30.9 × 42.5 cm (12 3/16 × 16 3/4 in.) Mount: 42.3 × 58 cm (16 5/8 × 22 13/16 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.79,false,true,269025,Photographs,Photograph,"[Colosseum, Rome]",,,,,,Artist,,George Wilson Bridges,"British, 1788–1864",,"Bridges, George Wilson",British,1788,1864,1850s,1850,1859,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.1,false,true,306202,Photographs,Photograph,"Garden of Selvia, Syracuse, Sicily",,,,,,Artist,,George Wilson Bridges,"British, 1788–1864",,"Bridges, George Wilson",British,1788,1864,1846,1846,1846,Salted paper print from paper negative,Image: 6 15/16 × 8 9/16 in. (17.7 × 21.7 cm) Sheet: 7 5/16 × 8 13/16 in. (18.5 × 22.4 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.2,false,true,306203,Photographs,Photograph,"Benedictine Convent, Catania",,,,,,Artist,,George Wilson Bridges,"British, 1788–1864",,"Bridges, George Wilson",British,1788,1864,1846,1846,1846,Salted paper print from paper negative,Image: 6 3/4 × 8 9/16 in. (17.1 × 21.7 cm) Sheet: 7 11/16 × 9 13/16 in. (19.5 × 24.9 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.661,false,true,286361,Photographs,Photograph,[The Photographer before his Tent on the Site of the Pyramid of Khafre (Chephren)],,,,,,Artist,,George Wilson Bridges,"British, 1788–1864",,"Bridges, George Wilson",British,1788,1864,1851,1851,1851,Salted paper print from paper negative,Mount: 10 3/8 in. × 13 9/16 in. (26.4 × 34.5 cm) Image: 6 7/16 × 8 7/16 in. (16.3 × 21.4 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.846a,false,true,286359,Photographs,Photograph,Temple of Victory,,,,,,Artist,,George Wilson Bridges,"British, 1788–1864",,"Bridges, George Wilson",British,1788,1864,ca. 1848,1846,1850,Salted paper print from paper negative,Mount: 10 7/16 in. × 13 5/8 in. (26.5 × 34.6 cm) Image: 6 7/8 × 8 1/4 in. (17.5 × 21 cm),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.172,false,true,285421,Photographs,Photogram,Spiraea aruncus (Tyrol),,,,,,Artist,,Anna Atkins,"British, 1799–1871",,"Atkins, Anna",British,1799,1871,1851–54,1851,1854,Cyanotype,Image: 35.1 x 24.6 cm (13 13/16 x 9 11/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.21,false,true,270836,Photographs,Photograph,Still Life and Embroidery,,,,,,Artist,,Robert Wilfred Skeffington Lutwidge,"British, 1802–1873",,"Lutwidge, Robert Wilfred Skeffington",British,1802,1873,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (40),false,true,287930,Photographs,Photograph,Mr and Mrs W. Beach,,,,,,Artist,,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Salted paper print,Image: 15.7 × 13.8 cm (6 3/16 × 5 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (66),false,true,287956,Photographs,Photograph,"Stone Pines, Villa Pamfili Doria, Rome",,,,,,Artist,,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1856,1856,1856,Albumen silver print from paper negative,Image: 20 x 24.8 cm (7 7/8 x 9 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (67),false,true,287957,Photographs,Photograph,"The Forum, Rome",,,,,,Artist,Possibly by,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,Image: 17.5 × 24.9 cm (6 7/8 × 9 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (68),false,true,287727,Photographs,Photograph,The Colosseum,,,,,,Artist,,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1856,1856,1856,Albumen silver print from paper negative,Image: 19.5 x 25 cm (7 11/16 x 9 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (69),false,true,287958,Photographs,Photograph,Bridge of Augustus at Nani,,,,,,Artist,Possibly by,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,Image: 19.2 × 25 cm (7 9/16 × 9 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (70),false,true,287959,Photographs,Photograph,St. Peters from the Pincian Hill,,,,,,Artist,Possibly by,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,Image: 19.1 × 24.7 cm (7 1/2 × 9 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (71),false,true,287960,Photographs,Photograph,"Pincian Garden, Rome",,,,,,Artist,Possibly by,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,Image: 19.3 × 25 cm (7 5/8 × 9 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (72),false,true,287961,Photographs,Photograph,"Pincian Garden, Rome",,,,,,Artist,Possibly by,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,19.5 24.8,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (73),false,true,287962,Photographs,Photograph,"Old Cypress Trees in Carthusian Convent, Rome",,,,,,Artist,Possibly by,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,Image: 24.9 × 19.7 cm (9 13/16 × 7 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.382 (81),false,true,287970,Photographs,Photograph,[Arch of Titus],,,,,,Artist,,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,Image: 19.2 × 25 cm (7 9/16 × 9 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (42a, b)",false,true,287932,Photographs,Photograph,"Harry Strangways, present Lord Ilchester; From a Picture Taken from a Church at Kertch",,,,,,Artist,,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Salted paper print; albumen silver print,Image: 14.8 × 12 cm (5 13/16 × 4 3/4 in.) Image: 10 × 9.1 cm (3 15/16 × 3 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.100.382 (60a, b)",false,true,287950,Photographs,Photograph,Clematis Cerulea; Hollyhocks,,,,,,Artist,,Jane Martha St. John,"British, 1803–1882",,"St., John Jane Martha",British,1803,1882,1853–56,1853,1856,Albumen silver print,Image: 145 × 11.4 cm (57 1/16 × 4 1/2 in.) (a) Image: 14.5 × 10.7 cm (5 11/16 × 4 3/16 in.) (b),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.1,false,true,270823,Photographs,Photograph,"Copy of a Bust of Her Majesty Queen Victoria, by Joseph Durham, Esq. F.S.A.",,,,,,Artist,,Hugh Welch Diamond,"British, 1808–1886",,"Diamond, Hugh Welch",British,1808,1886,1857,1857,1857,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.19,false,true,283091,Photographs,Photograph,"Patient, Surrey County Lunatic Asylum",,,,,,Artist,,Hugh Welch Diamond,"British, 1808–1886",,"Diamond, Hugh Welch",British,1808,1886,1850–58,1850,1858,Albumen silver print from glass negative,Mat: 11 1/4 × 9 1/8 in. (28.6 × 23.2 cm) Image: 7 1/2 × 5 1/2 in. (19.1 × 14 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.812,false,true,285964,Photographs,Photograph,"[Patient, Surrey County Lunatic Asylum]",,,,,,Artist,,Hugh Welch Diamond,"British, 1808–1886",,"Diamond, Hugh Welch",British,1808,1886,1850–55,1850,1855,Albumen silver print from glass negative,Image: 18.2 x 12.9 cm (7 3/16 x 5 1/16 in.),"Gilman Collection, Purchase, Anonymous Gifts, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.16,false,true,270830,Photographs,Photograph,Peasants of the Alto-Douro,,,,,,Artist,,Joseph James Forrester,"British, 1809–1862",,"Forrester, Joseph James",British,1809,1862,1856,1856,1856,Albumen silver print from glass negative,Image: 18.5 x 15.1 cm (7 5/16 x 5 15/16 in.) Mount: 44 x 30.4 cm (17 5/16 x 11 15/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.25,false,true,270840,Photographs,Photograph,Study for a Picture,,,,,,Artist,,Thomas George Mackinlay,"British, 1809–1865",,"Mackinlay, Thomas George Rev.",British,1809,1865,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.31,false,true,270847,Photographs,Photograph,"Church Porch, Earlham, near Norwich",,,,,,Artist,,William Harcourt Ranking,"British, 1814–1867",,"Ranking, William Harcourt Dr.",British,1814,1867,1857,1857,1857,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.38,false,true,270854,Photographs,Photograph,Bonchurch,,,,,,Artist,,Benjamin Brecknell Turner,"British, 1815–1894",,"Turner, Benjamin Brecknell",British,1815,1894,1850s,1850,1859,Gelatin silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.756,false,true,285627,Photographs,Photograph,"Pepperharrow Park, Surrey",,,,,,Artist,,Benjamin Brecknell Turner,"British, 1815–1894",,"Turner, Benjamin Brecknell",British,1815,1894,1852–54,1852,1854,Albumen silver print from paper negative,11 1/4 x 15 1/4,"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.3,false,true,270845,Photographs,Photograph,Wild Flowers,,,,,,Artist,,Mark Anthony,"British, 1817–1886",,"Anthony, Mark",British,1817,1886,ca. 1857,1855,1859,Albumen silver print,Image: 21.4 x 16.1 cm (8 7/16 x 6 5/16 in.) Mount: 44 x 30.4 cm (17 5/16 x 11 15/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.663.1,false,true,271657,Photographs,Stereograph,The Temple. Collection of Antiquities,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1850s,1850,1859,Albumen silver print from glass negative,,Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.663.2,false,true,271658,Photographs,Stereograph,"The Megatherium, British Museum",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1850s,1850,1859,Albumen silver print from glass negative,,Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.663.3,false,true,271659,Photographs,Stereograph,"The Lycian Saloon, British Museum",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1850s,1850,1859,Albumen silver print from glass negative,,Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1027,false,true,265901,Photographs,Photograph,"Falls of the Llugwy, at Pont-y-Pair",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1857,1857,1857,Albumen silver print from glass negative,35.8 x 42.9 cm (14 1/16 x 16 7/8 in.),"Purchase, Louis V. Bell Fund and Mrs. Jackson Burke Gift, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.531.48,false,true,269654,Photographs,Photograph,"The Council of War on the Morning of the Taking of the Mamelon. Lord Raglan, Omar Pasha, Marshal Pélissier",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1953",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.6,false,true,283078,Photographs,Photograph,"Roslin Chapel, South Porch",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1856,1856,1856,Salted paper print from glass negative,Image: 35.8 x 43.3 cm (14 1/8 x 17 1/16 in.) Mount: 41 x 48.2 cm (16 1/8 x 19 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.7,false,true,283079,Photographs,Photograph,Rievaulx Abbey,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1854,1854,1854,Albumen silver print from glass negative,Image: 35.7 x 29.8 cm (14 1/16 x 11 3/4 in.) Mount: 40.3 x 34.5 cm (15 7/8 x 13 9/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.8,false,true,283080,Photographs,Photograph,"Wharfe and Pool, Below the Strid",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1854,1854,1854,Salted paper print,Image: 34.5 x 28.2 cm (13 9/16 x 11 1/8 in.) Mount: 56.8 x 44.9 cm (22 3/8 x 17 11/16 in.),"Gilman Collection, Purchase, W. Bruce and Delaney H. Lundberg Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.9,false,true,283081,Photographs,Photograph,"Salisbury Cathedral - The Nave, from the South Transept",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1858,1858,1858,Albumen silver print from glass negative,30.2 x 31.1 cm (11 7/8 x 12 1/4 in. ),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.8,false,true,290476,Photographs,Photograph,[Royal Children in Tableau of the Seasons],,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1854,1854,1854,Albumen silver print from glass negative,15.5 x 16 cm (6 1/8 x 6 5/16 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.9,false,true,290477,Photographs,Photograph,Group at Head Quarters,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 19.2 x 16.4 cm (7 9/16 x 6 7/16 in.) Mount: 57.7 x 40.2 cm (22 11/16 x 15 13/16 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.33,false,true,282038,Photographs,Photograph,Valley of the Ribble and Pendle Hill,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1859,1859,1859,Albumen silver print from glass negative,32.1 x 42.5 cm (12 5/8 x 16 3/4 in.),"The Rubel Collection, Purchase, Lila Acheson Wallace and Ann Tenenbaum and Thomas H. Lee Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.34,true,true,282039,Photographs,Photograph,[Reclining Odalisque],,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1858,1858,1858,Salted paper print from glass negative,28.5 x 39 cm (11 1/4 x 15 3/8 in.),"The Rubel Collection, Purchase, Lila Acheson Wallace, Anonymous, Joyce and Robert Menschel, Jennifer and Joseph Duke, and Ann Tenenbaum and Thomas H. Lee Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.35,true,true,282040,Photographs,Photograph,[Landscape with Clouds],,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,probably 1856,1856,1856,Salted paper print from glass negative,"Image: 31.4 x 44.3 cm (12 3/8 x 17 7/16 in.), irregular","The Rubel Collection, Purchase, Anonymous Gift, Curator's Discretionary Grant from The Judith Rothschild Foundation, and Thomas Walther Gift, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.15,false,true,283087,Photographs,Photograph,[Still Life with Fruit],,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1860,1860,1860,Albumen silver print from glass negative,35.2 x 43.1cm (13 7/8 x 16 15/16in.) Mount: 46.6 x 60.3 cm (18 3/8 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.66,false,true,283157,Photographs,Photograph,"Moscow, Domes of Churches in the Kremlin",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1852,1852,1852,Salted paper print from paper negative,Image: 17.9 x 21.6 cm (7 1/16 x 8 1/2 in.) Mount: 43.7 x 60.1 cm (17 3/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.67,false,true,283158,Photographs,Photograph,"Landing Place, Railway Stores, Balaklava",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 27.9 x 36.4 cm (11 x 14 5/16 in.) Mount: 43.5 x 58.9 cm (17 1/8 x 23 3/16 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.68,false,true,283159,Photographs,Photograph,Sebastopol from Cathcart's Hill,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 21.9 x 34.7 cm (8 5/8 x 13 11/16 in.) Mount: 41.7 x 57.2 cm (16 7/16 x 22 1/2 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.14,false,true,270828,Photographs,Photograph,Birth of St. John,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1857,1857,1857,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.187,false,true,286419,Photographs,Photograph,Cooking House of the 8th Hussars,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 15.9 x 20.2 cm (6 1/4 x 7 15/16 in.) Mount: 43.7 x 59.4 cm (17 3/16 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.275,false,true,286435,Photographs,Photograph,"Rievaulx Abbey, the High Altar",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1854,1854,1854,Albumen silver print from glass negative,Image: 29.5 x 36.5 cm (11 5/8 x 14 3/8 in.) Mount: 48 x 61.5 cm (18 7/8 x 24 3/16 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.282,false,true,286715,Photographs,Photograph,[Lady on Horseback],,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1850s,1850,1859,Salted paper print from glass negative,"21.3 x 22.9 cm (8 3/8 x 9 in.), irregularly trimmed","Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.285,false,true,286386,Photographs,Photograph,[Self-Portrait],,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,February 1852,1852,1852,Albumen silver print from glass negative,Image: 12.2 x 9 cm (4 13/16 x 3 9/16 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.323,false,true,286569,Photographs,Photograph,Omar Pasha,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 17.9 x 13.4 cm (7 1/16 x 5 1/4 in.) Mount: 59.3 x 42.3 cm (23 3/8 x 16 5/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.348,false,true,286462,Photographs,Photograph,South Front of the Kremlin from the Old Bridge,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1852,1852,1852,Salted paper print from paper negative,Image: 17.7 × 21.1 cm (6 15/16 × 8 5/16 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.554,false,true,286568,Photographs,Photograph,"Landing Place, Ordnance Wharf, Balaklava",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 26 x 35.1 cm (10 1/4 x 13 13/16 in.) Mount: 43.2 x 59.4 cm (17 x 23 3/8 in.),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.663,false,true,285999,Photographs,Photograph,Captain Burnsby of the Grenadier Guards,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 16.3 x 15.6 cm (6 7/16 x 6 1/8 in.) Mount: 16.1 x 16.1 cm (6 5/16 x 6 5/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.664,false,true,286393,Photographs,Photograph,Henry Duberly and Mrs. Duberly,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 15.9 x 15.8 cm (6 1/4 x 6 1/4 in.) Mount: 16 x 15.9 cm (6 5/16 x 6 1/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.665,false,true,286391,Photographs,Photograph,Major General A. H. King,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 17.8 x 14.7 cm (7 x 5 13/16 in.) Mount: 18.3 x 15 cm (7 3/16 x 5 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.666,false,true,286567,Photographs,Photograph,"Cossack Bay, Balaklava",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 28.5 x 36.2 cm (11 1/4 x 14 1/4 in.) Mount: 43.2 x 59.7 cm (17 x 23 1/2 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.667,false,true,286566,Photographs,Photograph,"The Genoese Castle, Balaklava",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 27.4 x 34.8 cm (10 13/16 x 13 11/16 in.) Mount: 43.5 x 59.8 cm (17 1/8 x 23 9/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.668,false,true,286716,Photographs,Photograph,"Moscow, the Kremlin in the Distance",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1852,1852,1852,Salted paper print from paper negative,Image: 17.7 x 21.8 cm (6 15/16 x 8 9/16 in.) Mount (2nd): 43.8 x 59.1 cm (17 1/4 x 23 1/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.669,false,true,286008,Photographs,Photograph,[Orientalist Study of a Woman],,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1858,1858,1858,Albumen silver print from glass negative,"26.2 x 18.2 cm (10 5/16 x 7 3/16 in.), irregularly trimmed","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.671,false,true,286035,Photographs,Photograph,Lieutenant General Sir J. L. Pennefather and Staff,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1855,1855,1855,Salted paper print from glass negative,Image: 14.4 x 20.8 cm (5 11/16 x 8 3/16 in.) Mount: 40.2 x 57.8 cm (15 13/16 x 22 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.672,false,true,286712,Photographs,Photograph,Aelius Caesar,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1854–58,1854,1858,Salted paper print from glass negative,36.8 x 29.2 cm (14 1/2 x 11 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.673,false,true,286719,Photographs,Photograph,Laughing Satyr,,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1854–58,1854,1858,Salted paper print from glass negative,29.7 x 26.6 cm (11 11/16 x 10 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.674,false,true,286565,Photographs,Photograph,"Ely Cathedral, from the Grammar School",,,,,,Artist,,Roger Fenton,"British, 1819–1869",,"Fenton, Roger",British,1819,1819,1857,1857,1857,Albumen silver print from glass negative,Image: 35.4 x 44.3 cm (13 15/16 x 17 7/16 in.) Mount: 40.2 x 48.3 cm (15 13/16 x 19 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286565,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1098.9,false,true,631050,Photographs,Photograph,Harbor Scene,,,,,,Artist,,Thomas Sutton,"British, 1819–1875",,"Sutton, Thomas",British,1819,1875,ca. 1855,1850,1860,Salted paper print from waxed paper negative,Image: 7 3/8 × 9 7/8 in. (18.7 × 25.1 cm) Mount: 10 1/2 in. × 13 in. (26.7 × 33 cm),"Gift of Joyce F. Menschel, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/631050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.36,false,true,270852,Photographs,Photograph,Part of Tenby Town and Harbour,,,,,,Artist,,George Stokes,"British, 1819–1903",,"Stokes, George",British,1819,1903,1853,1853,1853,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.22,false,true,270837,Photographs,Photograph,"Newark Abbey, near Chertsey",,,,,,Artist,,John Richardson Major,"British, 1821–1871",,"Major, John Richardson",British,1821,1871,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.7,false,true,269585,Photographs,Photograph,Evening,,,,,,Artist,,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,1854,1854,1854,Albumen silver print from glass negative,Image: 21.3 x 16.4 cm (8 3/8 x 6 7/16 in.) Mount: 35.3 x 24.9 cm (13 7/8 x 9 13/16 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.639.6,false,true,269623,Photographs,Photograph,[The Grounds Looking Towards Penge],,,,,,Artist,,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,1854,1854,1854,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.639.19,false,true,269619,Photographs,Photograph,"[Storeroom with Artisans and Plaster Casts, Crystal Palace]",,,,,,Artist,,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,1852,1852,1852,Albumen silver print from glass negative,Image: 22.8 × 28.1 cm (9 in. × 11 1/16 in.) Mount: 36.9 × 52.7 cm (14 1/2 × 20 3/4 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.639.34,false,true,269621,Photographs,Photograph,The Upper Gallery,,,,,,Artist,,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,1854,1854,1854,Albumen silver print from glass negative,Image: 27.5 x 23.2 cm (10 13/16 x 9 1/8 in.) Mount: 52.2 x 36.8 cm (20 9/16 x 14 1/2 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.639.59,false,true,269622,Photographs,Photograph,[Carving a Sphinx],,,,,,Artist,,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,1854,1854,1854,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.12,false,true,270826,Photographs,Photograph,"View in Central Hall, Art Treasures Exhibition, Manchester",,,,,,Artist,,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,1857,1857,1857,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (1a–d),false,true,288389,Photographs,Photograph,[Exterior View of Facade and Fountains; Exterior View of Side Pavilion; Exterior Side View of Central Transept; Exterior Side View of Central Trancept with Reclining Figure in Foreground],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (2a–d),false,true,288393,Photographs,Photograph,[Two Gentlemen Seated on the Grounds of the Palace; The Palace from the Rosary; Cascades and North End of Palace; General View of Gardens and Fountains],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (3a–d),false,true,288429,Photographs,Photograph,[View of Fountains; Nave Looking North; Screen of the Kings and Queens of England],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (4a–d),false,true,288433,Photographs,Photograph,"[Mammoth Tree; General View of Nave, Looking South; View from North Gallery; Classical Fountain]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (5a–d),false,true,288437,Photographs,Photograph,"[View of Fountain and Byzantine Court; Bronze Fountain in Northern Nave; Monti's Fountain, and Alhambra Court; Alhambra Court from North Transept]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (6a–d),false,true,288442,Photographs,Photograph,[Alhambra Court Facade Towards the Nave; Entryway to the Alhambra Court; Side View of Alhambra Court; Alhambra Court Looking Towards the North],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (7a–d),false,true,288447,Photographs,Photograph,[Alhambra and Court of Lions; View in South Transept],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (8a–d),false,true,288450,Photographs,Photograph,[View From Music Court to South Transept; View in Tropical Department; View of the Egyptian Court; View of North Transept],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (9a–d),false,true,288454,Photographs,Photograph,[View in Tropical Department; View of Egyptian Sphinxes],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (10a–d),false,true,288458,Photographs,Photograph,[View of Sphinxes Among Foliage; View of Tropical Foliage; Egyptian Court from North-east Gallery; Colossal Egyptian Figures],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (11a–d),false,true,288462,Photographs,Photograph,"[Egyptian Court, Eastern Wall of Principal Court; Egyptian Court, Principal Facade towards the Nave; Lions in the Egyptian Court; [Colonnade Adorned with Egyptian Paintings]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (12a–d),false,true,288469,Photographs,Photograph,[View Across the Egyptian Court; View through Egyptian Columns into Classical Sculpture Gallery; Side View of Egyptian Colonnade; Facade of the Hall of Columns],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (13a–d),false,true,288473,Photographs,Photograph,[Interior of Egyptian Court; Classical Sculpture Gallery with Discus-Thrower; View of Egyptian Court from Classical Sculpture Gallery; Foliage in the Egyptian Court],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (14a–d),false,true,288479,Photographs,Photograph,"[Elevated View of Egyptian Court; Ninevah Court; Monti's Fountain, and Nineveh Court; Assyrian Court with Workers]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (15a–d),false,true,288564,Photographs,Photograph,"[Assyrian Court, Facade Towards the Nave; Elevated View of Assyrian Court; Greek and Roman Sculpture Court; The Three Graces]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (16a–d),false,true,288570,Photographs,Photograph,"[Medieval Court; Statue of Coleoni; Equestrian Statue of Gattamelata by Donatello, from the Court of Monuments of Christian Art; Equestrian Statue of Colleone]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (17a–d),false,true,288575,Photographs,Photograph,"[Statue of a Horse; Roman Court, Portrait Busts of Emperors; Doorway of Roman Court, Flanked by Portrait Bust of Nero; Greek Court of Philosophers, Statesmen, and Generals]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (18a–d),false,true,288579,Photographs,Photograph,[View in Court of Christian Monuments; Views of Greek and Roman Sculpture Court],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (19a–d),false,true,288583,Photographs,Photograph,[Views of Greek Sculpture Court including Bust of Minerva],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (20a–d),false,true,288587,Photographs,Photograph,"[Views in Greek Sculpture Gallery, Including A Pieta, Apollo Belvedere, Niobe and her Family, Priest of Bacchus and Farnese Torso]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (21a–d),false,true,288591,Photographs,Photograph,[Sculptures in Roman Court; Sacrificial Altar; Greek Sculpture Gallery; Statues in Greek and Roman Sculpture Court],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (22a–d),false,true,288595,Photographs,Photograph,"[Greek Court; Statue of Minerva; Ludovisi Mars; Iris, Hecate, or Lucifera]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (23a–d),false,true,288599,Photographs,Photograph,[Greek Court with Farnese Torso of a Youth; View of a Classical Fountain and Pool; Roman Gallery with Apollo Belvedere and Model of the Roman Forum; Roman Sculpture Court],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (24a–d),false,true,288603,Photographs,Photograph,"[Sculpture Court Flanked by Torso of Marsyas and Sacrificial Altar; Sculpture Court with Bust of Caracalla; Greek Court with Sculptures of Mercury, Faun, and Ariadne; Roman Court with Three Sculptures of Venus]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (25a–d),false,true,288607,Photographs,Photograph,"[Greek Court with Sculpture of Discobolus; Roman Court with Sculptures of Gladiator, Mercury and Fauns; Italian Sculpture near the Great Transept]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (26a–d),false,true,288611,Photographs,Photograph,[Roman Court with Sculptures of Posidonius and Wounded Gladiator; Sculpture of Geoffrey Chaucer by Marshall; Sculpture of Shakespeare by Roubilliac],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (27a–d),false,true,288615,Photographs,Photograph,[Sculptures of Abraham Duquesne; an Ancient Briton; David with his Slingshot; a Hunter],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (28a–d),false,true,288619,Photographs,Photograph,"[Sculptures of a Dancing Faun, a Neapolitan Improvisatore, Homer, and Thucydides]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (29a–d),false,true,288623,Photographs,Photograph,"[Sculptures of Hector, a Dancing Girl, Corinna, and Dorothea]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (30a–d),false,true,288627,Photographs,Photograph,"[Sculptures of the Tired Hunter, a Nymph Preparing to Bathe, Godiva, and an Allegorical Figure of Night]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (31a–d),false,true,288631,Photographs,Photograph,"[Sculptures of Andromeda, the Toilet of Atalanta, Corinna, and a Naiad]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (32a–d),false,true,288635,Photographs,Photograph,"[Sculptures of Sabrina, an Allegorical Figure of Morning, a Nereide, and Eve Listening]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (33a–d),false,true,288639,Photographs,Photograph,"[Sculptures of the Medici Venus and Pomona, Venus, Esmeralda, and the Mourners]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (34a–d),false,true,288643,Photographs,Photograph,"[Sculptures of Minerva Protecting a Warrior, Una and the Lion, Children with a Pony and a Hound, and Child Play]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (35a–d),false,true,288647,Photographs,Photograph,"[Sculptures of Cain, a Hunter Defending his Family, the Massacre of the Innocents, and Allegorical Figures of the First Whisper of Love]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (36a–d),false,true,288651,Photographs,Photograph,"[Sculptures of Hylas and the Nymphs, Allegorical Figures of the Three Fates, Zephyr wooing Flora, and Michelangelo's Bacchus and Donatello's St. George]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (37a–d),false,true,288655,Photographs,Photograph,[Court of French and Italian Sculpture; Avenue in Front of Fine Arts Courts; View into Classical Sculpture Gallery; Avenue in Front of Sheffield Court],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (38a–d),false,true,288682,Photographs,Photograph,[Court of Ancient Monuments; German Medieval Court; View with Statue of Albert of Bavaria; Elevated View of Central Transept],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (39a–d),false,true,288686,Photographs,Photograph,[Entryway to the Renaissance Court; Doorway from an Old Palace of the Dorias; The Ghiberti Gates; View in Medieval Court],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (40a–d),false,true,288690,Photographs,Photograph,[Medieval Court; The Walsingham Font; Entrance to English Medieval Court],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (41a–d),false,true,288694,Photographs,Photograph,"[The Rochester Doorway; Vestibule, Garden Side of English Medieval Court; Byzantine Court Exterior and Interior]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (42a–d),false,true,288698,Photographs,Photograph,[Views of Byzantine Court with Royal Effigies],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (43a–d),false,true,288702,Photographs,Photograph,"[Façade, Views, and Entrance Loggia of the Renaissance Court]",,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (44a–d),false,true,288723,Photographs,Photograph,[Tomb of Lorenzo de Medici; Tomb of Giuliano de Medici; Court of Christian Monuments; German Medieval Vestibule],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (45a–d),false,true,288727,Photographs,Photograph,[Medieval Court; Entryway to Byzantine Court; Sheffield Court; French and Italian Mediaeval Vestibule],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (46a–d),false,true,288731,Photographs,Photograph,[Entryway of Renaissance Court; Medieval Vestibule; View of the Renaissance Court; Room of Classical Reliefs and Sarcophagi],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.801 (47a–b),false,true,288735,Photographs,Photograph,[View of Large Plant with Sign Requesting Viewers Not to Touch; Telescope Gallery],,,,,,Artist,Attributed to,Philip Henry Delamotte,"British, 1821–1889",,"Delamotte, Philip Henry",British,1821,1889,ca. 1859,1857,1861,Albumen silver print from glass negative,"7.9 x 8.1 cm (3 1/8 x 3 3/16 in.), each","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.23,false,true,283095,Photographs,Photograph,Photographic Study,,,,,,Artist,,Clementina Hawarden,"British, 1822–1865",,"Hawarden, Lady Clementina",British,1822,1865,early 1860s,1860,1864,Albumen silver print from glass negative,20.1 x 14.4 cm (7 15/16 x 5 11/16 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.775,false,true,286598,Photographs,Photograph,Photographic Study,,,,,,Artist,,Clementina Hawarden,"British, 1822–1865",,"Hawarden, Lady Clementina",British,1822,1865,Early 1860s,1860,1865,Albumen silver print from glass negative,Image: 3 1/4 × 2 9/16 in. (8.3 × 6.5 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.620,false,true,296278,Photographs,Photograph,Frontispiece of Inquiries into Human Faculty and its Development,,,,,,Artist,,Francis Galton,"British, 1822–1911",,"Galton, Francis",British,1822,1911,1883,1883,1883,Albumen silver print from glass negative,Image: 20 x 11.8 cm (7 7/8 x 4 5/8 in.) Frame: 35.6 x 27.9 cm (14 x 11 in.),"Joyce F. Menschel Photography Library Fund, 2002, transferred from the Joyce F. Menschel Photography Library",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.65,false,true,291818,Photographs,Daguerreotype,[Seated Middle-aged Man in Bow Tie and Jacket],,,,,,Artist,,John Watkins,"British, 1823–1874",,"Watkins, John",British,1823,1874,1850s,1850,1859,Daguerreotype,Image: 14 x 10 cm (5 1/2 x 3 15/16 in.) Plate: 19.1 x 15.2 cm (7 1/2 x 6 in.) Case: 1.9 x 20.2 x 16.5 cm (3/4 x 7 15/16 x 6 1/2 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.771,false,true,286549,Photographs,Photograph,Sultan,,,,,,Artist,,Nevil Story Maskelyne,"British, 1823–1911",,"Maskelyne, Nevil Story",British,1823,1911,mid-1850s,1853,1857,Salted paper print from glass negative,Image: 5 3/4 × 6 7/8 in. (14.6 × 17.5 cm); corners trimmed,"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.58,false,true,291811,Photographs,Daguerreotype,"[Two Young Men in Bow Ties, One Seated Holding a Book, One Standing]",,,,,,Artist,,Robert Boning,"British, 1826–1878",,"Boning, Robert",British,1826,1878,1850s,1850,1859,Daguerreotype,Image: 9.3 x 7 cm (3 11/16 x 2 3/4 in.) Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.) Case: 1.1 x 11.7 x 9.5 cm (7/16 x 4 5/8 x 3 3/4 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.27,false,true,270842,Photographs,Photograph,"The Mouth of the East and West Lyn, Lynmouth, North Devon",,,,,,Artist,,Henry Pollock,"British, 1826–1889",,"Pollock, Henry",British,1826,1889,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.112,false,true,301892,Photographs,Photograph,"Oak Struck by Lightning, Badger, 1856.",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,Image: 18 x 22.7 cm (7 1/16 x 8 15/16 in.) Mount: 25.2 x 30.5 cm (9 15/16 x 12 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.2,false,true,290470,Photographs,Photograph,"Hurstmonceaux, Sussex",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1857,1857,1857,Albumen silver print from paper negative,Image: 21.2 x 27.2 cm (8 3/8 x 10 11/16 in.) Mount: 32.5 x 37.9 cm (12 13/16 x 14 15/16 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.3,false,true,290471,Photographs,Photograph,Thornton,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,Image: 21.4 x 26.9 cm (8 7/16 x 10 9/16 in.) Mount: 32.5 x 37.9 cm (12 13/16 x 14 15/16 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.4,false,true,290472,Photographs,Photograph,Montacute House near Yeovil,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1857–60,1857,1860,Albumen silver print from paper negatives,Image: 21 x 27.1 cm (8 1/4 x 10 11/16 in.) (clippped corners) Mount: 32.5 x 37.8 cm (12 13/16 x 14 7/8 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.5,false,true,290473,Photographs,Photograph,"Interior, Tintern",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1857,1857,1857,Albumen silver print from paper negative,Image: 22 x 27.2 cm (8 11/16 x 10 11/16 in.) Mount: 32.5 x 37.9 cm (12 13/16 x 14 15/16 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.39,false,true,265773,Photographs,Photograph,Peterborough,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,21.3 x 26.9 cm. (8 3/8 x 10 9/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.40,false,true,265775,Photographs,Photograph,Buildwas Abbey,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from paper negative,21.3 x 26.8 cm. (8 3/8 x 10 9/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.41,false,true,265776,Photographs,Photograph,Willey,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1850s–60s,1850,1869,Albumen silver print from paper negative,17.4 x 22.0 cm. (6 7/8 x 8 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.42,false,true,265777,Photographs,Photograph,Falaise Castle,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,21.2 x 27.2 cm. (8 3/6 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.43,false,true,265778,Photographs,Photograph,Malmesbury,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1850s–60s,1850,1869,Albumen silver print from paper negative,20.9 x 27.2 cm. (8 1/4 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.44,false,true,265779,Photographs,Photograph,Wenlock Abbey,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from paper negative,21.4 x 27.2 cm. (8 7/16 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.45,false,true,265780,Photographs,Photograph,"[photo-reproduction of Hogarth's print illustrating the Dunciad, Book I, line III]",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1850s–60s,1850,1869,Albumen silver print from paper negative,15.9 x 17.5 cm. (6 1/4 x 6 7/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.46,false,true,265781,Photographs,Photograph,"St. Osyths, Essex",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,20.8 x 26.8 cm. (8 3/16 x 10 9/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.47,false,true,265782,Photographs,Photograph,Wells,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1857,1857,1857,Albumen silver print from paper negative,20.2 x 27.3 cm. (7 15/16 x 10 3/4 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.48,false,true,265783,Photographs,Photograph,Thornton,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,21.5 x 27.4 cm. (8 7/16 x 10 13/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.49,false,true,265784,Photographs,Photograph,"West Front, Wells",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1857,1857,1857,Albumen silver print from paper negative,20.6 x 27.0 cm. (8 1/8 x 10 5/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.50,false,true,265786,Photographs,Photograph,"Crowland Abbey, the West Front Under Repair",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,21.8 x 27.0 cm. (8 9/16 x 10 5/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.51,false,true,265787,Photographs,Photograph,Byland Abbey,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,21.1 x 27.0 cm. (8 5/16 x 10 5/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.52,false,true,265788,Photographs,Photograph,Haughmond Abbey,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from paper negative,21.9 x 27.4 cm. (8 5/8 x 10 13/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.53,false,true,265789,Photographs,Photograph,Thornton College - Lincolnshire,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,21.6 x 27.1 cm. (8 1/2 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.54,false,true,265790,Photographs,Photograph,West Front - Peterboro,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,21.0 x 27.1 cm. (8 1/4 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.55,false,true,265791,Photographs,Photograph,"St. Pierre, Caen",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,19.8 x 25.2 cm. (7 13/16 x 9 15/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265791,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.56,false,true,265792,Photographs,Photograph,"St. Peter's in the East, Oxford",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1859,1859,1859,Albumen silver print from paper negative,21.7 x 27.5 cm. (8 9/16 x 10 13/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.57,false,true,265793,Photographs,Photograph,"Keep of Tattershall Castle, Lincolnshire - 2nd Fortescue",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,21.6 x 26.8 cm. (8 1/2 x 10 9/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.58,false,true,265794,Photographs,Photograph,"""Peter""",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from glass negative,10.2 x 14.2 cm. (4 x 5 9/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.59,false,true,265795,Photographs,Photograph,Reverend L. C. Cure and His Pony,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1859,1859,1859,Albumen silver print from glass negative,11.3 x 14.7 cm. (4 7/16 x 5 13/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.60,false,true,265797,Photographs,Photograph,Blake House,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from glass negative,"17.2 x 23.9 cm. (6 3/4 x 9 7/16 in.), rounded corners at top","Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.61,false,true,265798,Photographs,Photograph,[Vignetted portrait of two children],,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1850s–60s,1850,1869,Albumen silver print from paper negative,"8.9 x 7.9 cm. (3 1/2 x 3 1/8 in.), oval","Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.62,false,true,265799,Photographs,Photograph,"[Vignetted portrait, woman holding a baby]",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1850s–60s,1850,1869,Albumen silver print from glass negative,8.8 x 8.0 cm. (3 7/16 x 3 1/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.63,false,true,265800,Photographs,Photograph,Harry,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from glass negative,13.8 x 9.9 cm. (5 7/16 x 3 7/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.64,false,true,265801,Photographs,Photograph,Nephews,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from glass negative,12.0 x 6.1 cm. (4 3/4 x 2 3/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.65,false,true,265802,Photographs,Photograph,Harry,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,13.9 x 11.0 cm. (5 1/2 x 4 5/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.66,false,true,265803,Photographs,Photograph,Gateway - Bury St. Edmond's,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from paper negative,"21.8 x 26.8 cm. (8 9/16 x 10 9/16 in.), top corners trimmed","Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.67,false,true,265804,Photographs,Photograph,St. Osyth's Priory,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,21.1 x 27.2 cm. (8 5/16 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.68,false,true,265805,Photographs,Photograph,"Village of Andelys - Chateau Gaillard, Coeur de Lion",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,20.6 x 27.2 cm. (8 1/8 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.69,false,true,265806,Photographs,Photograph,Wenlock,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1850s–60s,1850,1869,Albumen silver print from paper negative,21.6 x 26.9 cm. (8 1/2 x 10 9/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.70,false,true,265808,Photographs,Photograph,Nether Hall,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1857,1857,1857,Albumen silver print from paper negative,21.5 x 27.4 cm. (8 7/16 x 10 13/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.71,false,true,265809,Photographs,Photograph,Blake House,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,19.8 x 23.9 cm. (7 13/16 x 9 7/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.72,false,true,265810,Photographs,Photograph,"Cloisters, Magdalen",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1859,1859,1859,Albumen silver print from paper negative,21.1 x 27.1 cm. (8 5/16 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.73,false,true,265811,Photographs,Photograph,"Green Court, Raglan",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from paper negative,21.0 x 27.0 cm. (8 1/4 x 10 5/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.74,false,true,265812,Photographs,Photograph,"Town Hall, Cirencester",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from paper negative,21.8 x 27.1 cm. (8 9/16 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.75,false,true,265813,Photographs,Photograph,"Conventual Buildings, Bury",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1858,1858,1858,Albumen silver print from paper negative,20.2 x 26.9 cm. (7 15/16 x 10 9/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.76,false,true,265814,Photographs,Photograph,Layer Marney,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1857,1857,1857,Albumen silver print from paper negative,21.2 x 27.2 cm. (8 3/6 x 10 11/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.77,false,true,265815,Photographs,Photograph,"Talbot's Tower, Falaise Castle",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from paper negative,21.3 x 27.8 cm. (8 3/8 x 10 15/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.78,false,true,265816,Photographs,Photograph,"American Creeper, Blake House",,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1860,1860,1860,Albumen silver print from paper negative,15.7 x 20.6 cm. (6 3/16 x 8 1/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.79,false,true,265817,Photographs,Photograph,Brevet Lieutenant Colonel Cure,,,,,,Artist,,Alfred Capel Cure,"British, 1826–1896",,"Capel, Cure Alfred",British,1826,1896,1856,1856,1856,Albumen silver print from glass negative,21.0 x 13.6 cm. (8 1/4 x 5 3/6 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.15,false,true,270829,Photographs,Photograph,"The Meeting of the Waters, Killarney",,,,,,Artist,,Lord Otho Fitzgerald,"British, 1827–1882",,"Fitzgerald, Otho Lord",British,1827,1882,1854,1854,1854,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.11,false,true,270825,Photographs,Photograph,"Wood-scene, Norton, Cheshire",,,,,,Artist,,Thomas Davies,"British, 1830–1880",,"Davies, Thomas",British,1830,1880,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.32,false,true,270848,Photographs,Photograph,Sparrowe's House,,,,,,Artist,,Robert Charles Ransome,"British, 1830–1886",,"Ransome, Robert Charles",British,1830,1886,1853,1853,1853,Salted paper print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.35,false,true,270851,Photographs,Photograph,The Time of Promise,,,,,,Artist,,George Shadbolt,"British, 1830–1901",,"Shadbolt, George",British,1830,1901,1857,1857,1857,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1033,false,true,265905,Photographs,Photograph,In the Valley of the Mole,,,,,,Artist,,Robert Howlett,"British, 1831–1858",,"Howlett, Robert",British,1831,1831,1855,1855,1855,Albumen silver print from glass negative,Image: 20.4 x 25.5 cm (8 1/16 x 10 1/16 in.) Mount: 30.5 x 43.7 cm (12 x 17 3/16 in.),"Purchase, Harrison D. Horblit and Harriette and Noel Levine Gifts and David Hunter McAlpin Fund, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.11,false,true,283083,Photographs,Photograph,[Isambard Kingdom Brunel Standing Before the Launching Chains of the Great Eastern],,,,,,Artist,,Robert Howlett,"British, 1831–1858",,"Howlett, Robert",British,1831,1831,"1857, printed 1863–64",1857,1857,Albumen silver print from glass negative,Image: 27.9 x 21.5 cm (11 x 8 7/16 in.) Mount: 36.4 x 26.6 cm (14 5/16 x 10 1/2 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.839,false,true,285910,Photographs,Photograph,"[Men at Work Beside the Launching Chains of the ""Great Eastern""]",,,,,,Artist,,Robert Howlett,"British, 1831–1858",,"Howlett, Robert",British,1831,1831,"November 18, 1857",1857,1857,Albumen silver print from glass negative,Dome topped: 11 1/8 x 13 15/16 Mount: 16 15/16 × 21 5/16 in. (43 × 54.2 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.51.2,false,true,263193,Photographs,Photograph,Opium smoker,,,,,,Artist,,William Thomas Saunders,"British, 1832–1892",,"Saunders, William Thomas",British,1832,1892,1867,1867,1867,Albumen silver print from glass negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.51.3,false,true,263194,Photographs,Photograph,Pekin-car,,,,,,Artist,,William Thomas Saunders,"British, 1832–1892",,"Saunders, William Thomas",British,1832,1892,1867,1867,1867,Albumen silver print from glass negative,,"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.331,false,true,266921,Photographs,Photograph,The Manirung Pass,,,,,,Artist,,Samuel Bourne,"British, 1834–1912",,"Bourne, Samuel",British,1834,1912,1866,1866,1866,Albumen silver print from glass negative,Image: 23.5 x 29.7 cm. (9 1/4 x 11 11/16 in.),"Purchase, Cynthia Hazen Polsky Gift, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.189.2.1,false,true,266893,Photographs,Photograph,[Mussucks for Crossing the Beas River Below Bajoura],,,,,,Artist,,Samuel Bourne,"British, 1834–1912",,"Bourne, Samuel",British,1834,1912,1866,1866,1866,Albumen silver print from glass negative,23.7 x 29.8 cm. (9 5/16 x 11 3/4 in.),"Purchase, Cynthia Hazen Polsky Gift, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.499 (99),false,true,286879,Photographs,Photograph,The Manirung Pass,,,,,,Artist,,Samuel Bourne,"British, 1834–1912",,"Bourne, Samuel",British,1834,1912,1860s,1860,1869,Albumen silver print from glass negative,23.7 x 29.6 cm (9 5/16 x 11 5/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.29,false,true,270844,Photographs,Photograph,"On the Road to Watersmeet, near Lynton, North Devon",,,,,,Artist,,Arthur Julius Pollock,"British, 1835–1890",,"Pollock, Arthur Julius",British,1835,1890,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (9a),false,true,287608,Photographs,Photograph,"[The Viscountess Canning, Barrackpore]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 13.3 x 9.6 cm (5 1/4 x 3 3/4 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (9b),false,true,287681,Photographs,Photograph,"[The Viscountess Canning, Barrackpore]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 15 x 12.5 cm (5 7/8 x 4 15/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (14a),false,true,287613,Photographs,Photograph,"[N.E. Gate of Government House, Calcutta]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 14.1 x 20.7 cm (5 9/16 x 8 1/8 in.) Mount: 33 x 26.4 cm (13 x 10 3/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (14b),false,true,287682,Photographs,Photograph,"[Great Sikh Gun taken at Ferozshah on the Night of December 21, 1845, Government House, Calcutta]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 15.4 x 20.8 cm (6 1/16 x 8 3/16 in.) Mount: 33 x 26.4 cm (13 x 10 3/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (16a),false,true,287616,Photographs,Photograph,"[View of Chowringhee from Government House, Calcutta]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 11.1 x 15.8 cm (4 3/8 x 6 1/4 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (16b),false,true,287684,Photographs,Photograph,[The Maidan from Government House During the Rains],,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 13.8 x 20.9 cm (5 7/16 x 8 1/4 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (17a),false,true,287617,Photographs,Photograph,"[Kitchen and Stables of Government House, Calcutta]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 12.2 x 18.8 cm (4 13/16 x 7 3/8 in.) Mount: 33 x 26.2 cm (13 x 10 5/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (17b),false,true,287685,Photographs,Photograph,"[Spence's Hotel & St. John's Cathedral, Calcutta]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 15.5 x 20.1 cm (6 1/8 x 7 15/16 in.) Mount: 33 x 26.2 cm (13 x 10 5/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (37a),false,true,287637,Photographs,Photograph,"[Gunpowder Agents Bungalow, Ishapoor.]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 12 x 17.2 cm (4 3/4 x 6 3/4 in.) Mount: 33.1 x 26 cm (13 1/16 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (37b),false,true,287693,Photographs,Photograph,"[Gunpowder Agents Bungalow, Ishapoor]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858–61,1858,1861,Albumen silver print,Image: 11 x 18 cm (4 5/16 x 7 1/16 in.) Mount: 33.1 x 26 cm (13 1/16 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (40a),false,true,287640,Photographs,Photograph,"[Government House, Allahabad]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 7.1 x 7.6 cm (2 13/16 x 3 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (40b),false,true,287696,Photographs,Photograph,"[Gardens, Government House, Allahabad]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 7 x 7.8 cm (2 3/4 x 3 1/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (40c),false,true,287697,Photographs,Photograph,"[Countess Canning with Guest, Government House, Allahabad]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 7.6 x 6.9 cm (3 x 2 11/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (40d),false,true,287698,Photographs,Photograph,"[Countess Canning with Guests, Government House, Allahabad]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 7.1 x 7.5 cm (2 13/16 x 2 15/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (40e),false,true,287699,Photographs,Photograph,"[Man and Horse, Government House, Allahabad]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 6.6 x 6.9 cm (2 5/8 x 2 11/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (40f),false,true,287700,Photographs,Photograph,"[Countess Canning with Guests, Government House, Allahabad]",,,,,,Artist,,John Constantine Stanley,"British, 1837–1878",,"Stanley, John Constantine",British,1837,1878,1858,1858,1858,Albumen silver print,Image: 7 x 7.5 cm (2 3/4 x 2 15/16 in.) Mount: 33 x 26 cm (13 x 10 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1017,false,true,266091,Photographs,Photograph,[Indian Barber],,,,,,Artist,,Willoughby Wallace Hooper,"British, 1837–1912",,"Hooper, Willoughby Wallace",British,1837,1912,1860s,1860,1869,Albumen silver print from glass negative,14.9 x 19.4 cm. (5 7/8 x 7 5/8 in.),"Purchase, Cynthia Hazen Polsky Gift, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1123.18,false,true,264378,Photographs,Photograph,"Graia, On the Red Sea, Near Ezion-Geber, Port of King Solomon",,,,,,Artist,,Frank Mason Good,"British, 1839–1928",,"Good, Frank Mason",British,1839,1928,1870s,1870,1879,Albumen silver print,,"Gift of Weston J. Naef, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.21.10,false,true,268692,Photographs,Photograph,Julia Margaret Cameron,,,,,,Artist,,Henry Herschel Hay Cameron,"British, 1852–1911",,"Cameron, Henry Herschel Hay",British,1852,1911,1870,1870,1870,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.74.237,false,true,269472,Photographs,Photograph,"Deir el Bahari, Egypt",,,,,,Artist,,Lord Carnarvon,"British, 1866–1923",,"Carnarvon, Lord",British,1866,1923,1914,1914,1914,Gum bichromate over platinum print,,"Howard Carter Bequest, 1939",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.74.238,false,true,269473,Photographs,Photograph,"Dier el Bahari, Egypt",,,,,,Artist,,Lord Carnarvon,"British, 1866–1923",,"Carnarvon, Lord",British,1866,1923,1914,1914,1914,Gum bichromate over platinum print,,"Howard Carter Bequest, 1939",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.970,false,true,283250,Photographs,Photograph,[Cavorting by the Pool at Garsington],,,,,,Artist,,Lady Ottoline Violet Anne Cavendish-Bentinck Morrell,"British, 1873–1938",,"Morrell, Lady Ottoline",British,1873,1938,ca. 1916,1915,1917,Gelatin silver prints,8.8 x 6.2 cm (3 7/16 x 2 7/16 in.) and 8.8 x 6.3 cm (3 7/16 x 2 7/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.971,false,true,291058,Photographs,Photograph,[Cavorting by the Pool at Garsington],,,,,,Artist,,Lady Ottoline Violet Anne Cavendish-Bentinck Morrell,"British, 1873–1938",,"Morrell, Lady Ottoline",British,1873,1938,ca. 1916,1915,1917,Gelatin silver print,8.8 x 6.3 cm (3 7/16 x 2 7/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.106,false,true,268879,Photographs,Photograph,[Tree in Yard],,,,,,Artist,Attributed to,Samuel Buckle,"British, 1809?–1860",,"Buckle, Samuel",British,1809,1860,1850s,1850,1859,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (2),false,true,287592,Photographs,Photograph,"[The Countess Canning, Calcutta]",,,,,,Artist,,Josiah Rowe,"British, ca. 1809–1874",,"Rowe, Josiah",British,1809,1874,1861,1861,1861,Albumen silver print,Image: 24.6 x 22.9 cm (9 11/16 x 9 in.) Mount: 33 x 26.2 cm (13 x 10 5/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.491.1 (4),false,true,287594,Photographs,Photograph,"[The Countess Canning, Calcutta]",,,,,,Artist,,Josiah Rowe,"British, ca. 1809–1874",,"Rowe, Josiah",British,1809,1874,1861,1861,1861,Albumen silver print from glass negative,Image: 23.6 x 22.4 cm (9 5/16 x 8 13/16 in.) Mount: 33 x 26.2 cm (13 x 10 5/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.499,false,true,685839,Photographs,Carte-de-visite,[Benjamin William Leader],,,,,,Artist,,John and Charles Watkins,"British, active 1867–71",,John and Charles Watkins,British,1840,1875,1867–1870,1867,1870,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1126,false,true,266157,Photographs,Photograph,"[House with Woman on Balcony, Man Standing Below]",,,,,,Artist,,Richard Dykes Alexander,"British, Ipswich 1788–1865",,"Alexander, Richard Dykes",British,1788,1865,ca. 1857,1855,1859,Salted paper print from glass negative,15.2 x 20.6 cm. (6 x 8 1/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.590.2,false,true,259897,Photographs,Photograph,[Rocky Inlet with Seascape],,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1971",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.590.3,false,true,259898,Photographs,Photograph,[View Through Rocks' Of Tower On Hill],,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1971",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.590.4,false,true,259899,Photographs,Photograph,"Monuments and Chancel Steps, Tenby Church",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1971",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.590.5,false,true,259900,Photographs,Photograph,Gosceau Rock and the Croft,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1971",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.1,false,true,260056,Photographs,Photograph,Colwyn Bay. Rustic Bridge in the Wood,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.2,false,true,260067,Photographs,Photograph,"Chargford, Holy S. Mill",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.3,false,true,260069,Photographs,Photograph,Morte Point from Barraoane Bay,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.4,false,true,260070,Photographs,Photograph,Pensarn Beach,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.5,false,true,260071,Photographs,Photograph,Worcester. From the Severn,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.6,false,true,260072,Photographs,Photograph,"Wrexham, Rossett Mill",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.7,false,true,260073,Photographs,Photograph,The Wye and Symond's Yat. From Rocklands,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.8,false,true,260074,Photographs,Photograph,"Stradford-on-Avon Church, from the Avon",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.9,false,true,260075,Photographs,Photograph,"Ilfracombe, Capstone Parade",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.5,false,true,270857,Photographs,Photograph,"At Pont y pair, Bettws-y-Coed, North Wales",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.10,false,true,260057,Photographs,Photograph,"Sidmount, West end of Esplanade",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.11,false,true,260058,Photographs,Photograph,"Ilfracombe, The Victorian Promenade",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.12,false,true,260059,Photographs,Photograph,Glen Lun. The Rustic Bridge,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.13,false,true,260060,Photographs,Photograph,"Rhyl, from the Sea",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.14,false,true,260061,Photographs,Photograph,"Clovelly, The New Inn and Street",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.15,false,true,260062,Photographs,Photograph,Old Barmouth,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.16,false,true,260063,Photographs,Photograph,"Torquay, Hesketh Crescent and Meadfoot",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.17,false,true,260064,Photographs,Photograph,Rhyl. The Pavilion and Pier,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.18,false,true,260065,Photographs,Photograph,Barmouth. Marine Terrace and Esplanade,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.19,false,true,260066,Photographs,Photograph,Colwyn Bay. The Pool in the Wood,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.567.20,false,true,260068,Photographs,Photograph,"Abergele, Tan-yr-ogo Cave",,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.717,false,true,285960,Photographs,Photographs,Pensarn Beach,,,,,,Artist,,Francis Bedford,"British, London 1816–1894 London",,"Bedford, Francis",British,1816,1894,1860s,1860,1869,Albumen silver print,Image: 12.6 × 19.9 cm (4 15/16 × 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.215,false,true,267034,Photographs,Photograph,"Walter Churcher, ""Churcher Smileth""",,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900–1905,1900,1905,Platinum print,22.2 x 11.4 cm. (8 3/4 x 4 1/2 in.),"Gift of Joel Snyder, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.550.3,false,true,269746,Photographs,Photograph,[Angels with Interlace],,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900s–1910s,1900,1919,Platinum print,Image (visible): 3 9/16 × 8 15/16 in. (9.1 × 22.7 cm) Image (overall): 4 × 10 in. (10.1 × 25.4 cm) Overmat: 7 × 13 in. (17.8 × 33 cm),"Gift of Gordon Conn, 1954",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.550.5,false,true,269748,Photographs,Photograph,"[Needlework Altar Cloth, Durham]",,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1911-1912,1911,1912,Platinum print on fabric,,"Gift of Gordon Conn, 1954",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.550.6,false,true,269749,Photographs,Photograph,Un Bon Viveur,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1890s–1900s,1890,1909,Platinum print,,"Gift of Gordon Conn, 1954",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.637.4,false,true,271352,Photographs,Photograph,Alvin Langdon Coburn,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,ca. 1901,1899,1903,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1968",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.637.5,false,true,271353,Photographs,Photograph,Gloucester Cathedral: North Transept,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900s–1910s,1900,1919,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1968",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.637.6,false,true,271354,Photographs,Photograph,Wells Cathedral from the Moat Path,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900s–1910s,1900,1919,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1968",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.637.7,false,true,271355,Photographs,Photograph,"Maison Jeanne d'Arc, Rouen",,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900s,1900,1909,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1968",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.688.1,false,true,271404,Photographs,Photograph,"""Castle in the Air""",,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,ca. 1906,1904,1908,Platinum print,,"David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.688.3,false,true,271406,Photographs,Photograph,[Trees],,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900s–1910s,1900,1919,Gum bichromate print,,"David Hunter McAlpin Fund, 1968",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.616.1,false,true,271523,Photographs,Photograph,In the New Forest,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1919,1919,1919,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.616.2,false,true,271524,Photographs,Photograph,Bude,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1910s–20s,1910,1929,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.616.3,false,true,271525,Photographs,Photograph,Bude,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1910s–20s,1910,1929,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.616.4,false,true,271526,Photographs,Photograph,Dandelions,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900s–1920s,1900,1929,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.616.5,false,true,271527,Photographs,Photograph,A Stalk of Berbery,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1900s–1910s,1900,1919,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1969",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.283,false,true,271766,Photographs,Photograph,Height and Light in Bourges Cathedral,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1903,1903,1903,Platinum print,12.0 x 7.4 cm (4 3/4 x 2 15/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.239,false,true,271764,Photographs,Photograph,George Bernard Shaw,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1901,1901,1901,Platinum print,Image: 9 in. × 6 1/8 in. (22.8 × 15.6 cm) Border 1: 10 11/16 × 7 9/16 in. (27.2 × 19.2 cm) Border 2: 10 7/8 × 7 3/4 in. (27.7 × 19.7 cm) Mount: 14 3/16 × 11 1/4 in. (36 × 28.6 cm),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.398.3,false,true,282112,Photographs,Photograph,Lincoln Cathedral: From the Castle,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1896,1896,1896,Photogravure,21 x 15.8 cm (8 1/4 x 6 1/4 in. ),"The Rubel Collection, Gift of William Rubel, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.8,false,true,306209,Photographs,Photograph,Redlands Woods,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1893,1893,1893,Platinum print,Mount: 9 15/16 in. × 6 11/16 in. (25.3 × 17 cm) Image: 6 1/16 × 4 7/16 in. (15.4 × 11.2 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.1,false,true,265736,Photographs,Photograph,"Organ Screen, York Minster",,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,ca. 1904,1902,1906,Platinum print,20.3 x 24.4 cm. (8 x 9 5/8 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1183.2,false,true,265747,Photographs,Photograph,The Little Cloisters,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,ca. 1900,1898,1902,Platinum print,11.4 x 15.1 cm. (4 1/2 x 5 15/16 in.),"Gift of Paul F. Walter, in memory of Christopher Hemphill, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.779,false,true,286415,Photographs,Photograph,On a French River,,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,ca. 1902,1900,1904,Gelatin silver print,Image: 24.8 x 13.1 cm (9 3/4 x 5 3/16 in.) Mount: 45.5 x 30.2 cm (17 15/16 x 11 7/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.906,false,true,286643,Photographs,Photograph,"""In Sure and Certain Hope."" York Minster",,,,,,Artist,,Frederick H. Evans,"British, London 1853–1943 London",,"Evans, Frederick Henry",British,1853,1943,1902,1902,1902,Platinum print,"Image: 20 x 14.8 cm (7 7/8 x 5 13/16 in.) Mount: 26.4 x 18.5 cm (10 3/8 x 7 5/16 in.), irregularly trimmed","Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.365,false,true,294742,Photographs,Photograph,The Great British Advance in the West: A Raiding Party Waiting for the Word to Go.,,,,,,Artist,,John Warwick Brooke,"British, London 1886–1929 London",,"Brooke, John Warwick",British,1886,1929,1914–18,1914,1918,Gelatin silver print,Image: 14 x 19.1 cm (5 1/2 x 7 1/2 in.),"Twentieth-Century Photography Fund, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5030,false,true,266672,Photographs,Photograph,French Machinery,,,,,,Artist,,Charles Thurston Thompson,"British, Peckham 1816–1868 Paris",,"Thompson, Charles Thurston",British,1816,1868,1855,1855,1855,Salted paper print from glass negative,22.0 x 28.7 cm (8 11/16 x 11 5/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.10,false,true,270824,Photographs,Photograph,"The Court of Lions in the Alhambra, Spain",,,,,,Artist,,John Gregory Crace,"British, London 1809–1889 Dulwich",,"Crace, John Gregory",British,1809,1889,1855,1855,1855,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.21,false,true,283093,Photographs,Photograph,St. George and the Dragon,,,,,,Artist,,Lewis Carroll,"British, Daresbury, Cheshire 1832–1898 Guildford",,"Carroll, Lewis",British,1832,1898,"June 26, 1875",1875,1875,Albumen silver print from glass negative,Image: 11.7 × 16 cm (4 5/8 × 6 5/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.64,false,true,291817,Photographs,Daguerreotype,[Seated Man Pointing to a Passage in an Open Book],,,,,,Artist,,John Jabez Edwin Mayall,"British, Oldham, Lancashire 1813–1901 West Sussex",,"Mayall, John Jabez Edwin",British,1813,1901,1850s,1853,1859,Daguerreotype,Image: 15.3 x 10.8 cm (6 x 4 1/4 in.) Plate: 19.7 x 15.2 cm (7 3/4 x 6 in.) Case: 2.1 x 21 x 16.5 cm (13/16 x 8 1/4 x 6 1/2 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.23,false,true,270838,Photographs,Photograph,Dr. Livingstone,,,,,,Artist,,John Jabez Edwin Mayall,"British, Oldham, Lancashire 1813–1901 West Sussex",,"Mayall, John Jabez Edwin",British,1813,1901,1857,1857,1857,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.1048,false,true,265917,Photographs,Photograph,The Jewels of the Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 26.4 x 36.9 cm (10 3/8 x 14 1/2 in.),"Purchase, Cynthia Hazen Polsky Gift, 1988",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.1152,false,true,266450,Photographs,Photograph,Elliot Marbles and Other Sculpture from the Central Museum Madras: Group 26,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,May–June 1858,1858,1858,Albumen silver print from dry collodion on glass negative,Image: 23.4 x 29.9 cm (9 3/16 x 11 3/4 in.) Mount: 33.1 x 45 cm (13 1/16 x 17 11/16 in.) Mat: 18 1/2 × 22 1/2 in. (47 × 57.1 cm),"Purchase, Cynthia Hazen Polsky Gift and The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1991",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.1,false,true,302510,Photographs,Panorama,No. 1. Prome. General View.,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"August 7, 1855",1855,1855,Albumen silver prints from waxed(?) paper negatives,"Image: 24.4 x 60.8 cm (9 5/8 x 23 15/16 in.), overall Mount: 45.7 x 108 cm (18 x 42 1/2 in.), overall","Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.5,false,true,302635,Photographs,Photograph,No. 7. Ye-nan-gyoung. Pagoda and Kyoung.,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"August 14–16, 1855",1855,1855,Albumen silver print from waxed paper negative,Image: 23.7 x 33.8 cm (9 5/16 x 13 5/16 in.) Mount: 45.6 x 58.3 cm (17 15/16 x 22 15/16 in.),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.6,false,true,302636,Photographs,Photograph,Pugahm Myo: Thapinyu Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"August 20–24, 1855",1855,1855,Albumen silver print from waxed paper negative,Image: 25.1 x 34.5 cm (9 7/8 x 13 9/16 in.) Mount: 45.6 x 58.3 cm (17 15/16 x 22 15/16 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.8,false,true,302638,Photographs,Photograph,Pugahm Myo: Distant View of Gauda-palen Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"August 20–24, 1855",1855,1855,Albumen silver print from waxed(?) paper negative,Image: 25.3 x 34.1 cm (9 15/16 x 13 7/16 in.) Mount: 45.6 x 58.3 cm (17 15/16 x 22 15/16 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.60,false,true,282071,Photographs,Photograph,Virabadra Drug as seen from near the site of the last view,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,December 1857–January 1858,1857,1858,Salted paper print from waxed paper negative,27.7 x 38.2 cm (10 7/8 x 15 1/16 in. ),"The Rubel Collection, Purchase, Cynthia Hazen Polsky and Lila Acheson Wallace Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.61,false,true,282072,Photographs,Photograph,Central Museum Madras: Group 27,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,May–June 1858,1858,1858,Albumen silver print from dry collodion on glass negative,Image: 25.8 x 23.2 cm (10 3/16 x 9 1/8 in.) Mount: 45.1 x 33 cm (17 3/4 x 13 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"The Rubel Collection, Purchase, Lila Acheson Wallace and Richard and Ronay Menschel Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.10,false,true,302640,Photographs,Photograph,Tsagain Myo: A Roadway,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"August 29–30, 1855",1855,1855,Albumen silver print from waxed(?) paper negative,Image: 24.5 x 34.1 cm (9 5/8 x 13 7/16 in.) Mount: 45.6 x 58.3 cm (17 15/16 x 22 15/16 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.11,false,true,302641,Photographs,Photograph,Tsagain Myo: Litters under a shed.,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"August 29–30, 1855",1855,1855,Albumen silver print from waxed(?) paper negative,Image: 27 x 34.5 cm (10 5/8 x 13 9/16 in.) Mount: 45.6 x 58.3 cm (17 15/16 x 22 15/16 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.14,false,true,302644,Photographs,Photograph,Amerapoora: Corner of Mygabhoodee-tee Kyoung,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"September 1–October 21, 1855",1855,1855,Albumen silver print from waxed paper negative,Image: 27.3 x 34.4 cm (10 3/4 x 13 9/16 in.) Mount: 45.6 x 58.4 cm (17 15/16 x 23 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.15,false,true,302645,Photographs,Photograph,Amerapoora: Wooden Bridge,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"September 1–October 21, 1855",1855,1855,"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",Image: 22.3 x 32.4 cm (8 3/4 x 12 3/4 in.) Mount: 45.6 x 58.4 cm (17 15/16 x 23 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.20,false,true,302650,Photographs,Photograph,Amerapoora: Shwe-doung-dyk Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"September 1–October 21, 1855",1855,1855,Albumen silver print from waxed paper negative,Image: 25.8 x 34.6 cm (10 3/16 x 13 5/8 in.) Mount: 45.5 x 58.3 cm (17 15/16 x 22 15/16 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.24,false,true,302654,Photographs,Photograph,Amerapoora: Part of Balcony on the South Side of Maha-oung-meeay-liy-mhan Kyoung,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,"September 1–October 21, 1855",1855,1855,Albumen silver print from waxed paper negative,Image: 26.9 x 34.7 cm (10 9/16 x 13 11/16 in.) Mount: 45.6 x 58.3 cm (17 15/16 x 22 15/16 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.323.30,false,true,302660,Photographs,Photograph,Rangoon: Henzas on the East Side of the Shwe Dagon Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,November 1855,1855,1855,Albumen silver print from waxed paper negative,Image: 26.1 x 34.3 cm (10 1/4 x 13 1/2 in.) Mount: 45.7 x 58.4 cm (18 x 23 in.) Mat: 20 × 24 in. (50.8 × 61 cm),"Purchase, The Buddy Taub Foundation, Dennis A. Roach and Jill Roach, Directors, and Alfred Stieglitz Society Gifts, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.767,false,true,285626,Photographs,Photograph,"Amerapoora, Palace of the White Elephant",,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,1 September–21 October 1855,1855,1855,Salted paper print from waxed(?) paper negative,Image: 24.1 x 33.5 cm (9 1/2 x 13 3/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.776,false,true,285623,Photographs,Photograph,"Amerapoora, Barracks of the Burmese Guard",,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,1 September–21 October 1855,1855,1855,Salted paper print from waxed paper negative,Image: 26 x 34.4 cm (10 1/4 x 13 9/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.1,false,true,287261,Photographs,Photograph,The Elephant Rock,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–February 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 23.7 x 35.5 cm (9 5/16 x 14 in.) Mount: 45.3 x 57.4 cm (17 13/16 x 22 5/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.2,false,true,287262,Photographs,Photograph,South East Angle of the Tirambur Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–February 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 26.3 x 36.4 cm (10 3/8 x 14 5/16 in.) Mount: 45 x 56.5 cm (17 11/16 x 22 1/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.3,false,true,287263,Photographs,Photograph,View of the N. E. Angle of the Tirambur Pagoda.,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–February 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 24.2 x 36.2 cm (9 1/2 x 14 1/4 in.) Mount: 45.2 x 57.3 cm (17 13/16 x 22 9/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.4,false,true,286803,Photographs,Photograph,The Teppa-kulam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,1858,1858,1858,Albumen silver print from waxed paper negative,Image: 26 x 37.5 cm (10 1/4 x 14 3/4 in.) Mount: 45.2 x 56.2 cm (17 13/16 x 22 1/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.5,false,true,287264,Photographs,Photograph,The Tamukkam or Tamkam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxedpaper negative,Image: 26 x 37.2 cm (10 1/4 x 14 5/8 in.) Mount: 45.2 x 57.5 cm (17 13/16 x 22 5/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.6,false,true,287265,Photographs,Photograph,The Causeway Across the Vaigai River,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 24.9 x 35 cm (9 13/16 x 13 3/4 in.) Mount: 45.2 x 57.5 cm (17 13/16 x 22 5/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.7,false,true,287266,Photographs,Photograph,The Neerali Mundapam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from paper negative,Image: 23.9 x 34.8 cm (9 7/16 x 13 11/16 in.) Mount: 45.3 x 57.4 cm (17 13/16 x 22 5/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.8,false,true,287267,Photographs,Photograph,The Raya Gopuram from E.,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 27 x 36.5 cm (10 5/8 x 14 3/8 in.) Mount: 45.4 x 57.2 cm (17 7/8 x 22 1/2 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.9,false,true,286883,Photographs,Photograph,"Pillars in the Recessed Portico in the Roya Gopuram with the Base of One of the Four Sculptured Monoliths, Madura",,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 35.8 x 30.2 cm (14 1/8 x 11 7/8 in.) Mount: 45.3 x 57.5 cm (17 13/16 x 22 5/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.1,false,true,287269,Photographs,Photograph,The Great Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 33.6 x 30.1 cm (13 1/4 x 11 7/8 in.) Mount: 57 x 45 cm (22 7/16 x 17 11/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.2,false,true,287270,Photographs,Photograph,The Viravasuntarayan Munapam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 29.5 x 37 cm (11 5/8 x 14 9/16 in.) Mount: 46 x 57.1 cm (18 1/8 x 22 1/2 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.3,false,true,287271,Photographs,Photograph,Entrance to the Thousand Pillared Mundapam in the Great Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 33 x 30.7 cm (13 x 12 1/16 in.) Mount: 57.1 x 45 cm (22 1/2 x 17 11/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.4,false,true,287272,Photographs,Photograph,The Muduramiar Mundapam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 34.7 x 28.3 cm (13 11/16 x 11 1/8 in.) Mount: 57 x 45.2 cm (22 7/16 x 17 13/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.5,false,true,287273,Photographs,Photograph,Side Colonnade in the Muroothappa Sarvacar Mundapam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 36.5 x 29.1 cm (14 3/8 x 11 7/16 in.) Mount: 57.1 x 45.1 cm (22 1/2 x 17 3/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.6,false,true,287274,Photographs,Photograph,The Inner Facade of the Gateway of the East Gopuram,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 29.2 x 34 cm (11 1/2 x 13 3/8 in.) Mount (2nd): 45 x 57.1 cm (17 11/16 x 22 1/2 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.7,false,true,287275,Photographs,Photograph,The Kulayana Mundapam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 27 x 37.7 cm (10 5/8 x 14 13/16 in.) Mount: 45 x 57.1 cm (17 11/16 x 22 1/2 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.8,false,true,287276,Photographs,Photograph,Tatta Suddhi Mundapam,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 24 x 36.8 cm (9 7/16 x 14 1/2 in.) Mount: 45 x 57.1 cm (17 11/16 x 22 1/2 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.9,false,true,287277,Photographs,Photograph,The Western Gopuram,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 36.1 x 28.5 cm (14 3/16 x 11 1/4 in.) Mount: 42 x 39 cm (16 9/16 x 15 3/8 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.1.10,false,true,287268,Photographs,Photograph,The Raya Gopuram from W,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 27.3 x 36.3 cm (10 3/4 x 14 5/16 in.) Mount: 45 x 57.8 cm (17 11/16 x 22 3/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.10,false,true,287278,Photographs,Photograph,"The Outer Prakarum, or Corridor Around the Temple of the God Sundareshawara",,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from dry collodion on glass negative,Image: 34.8 x 24.2 cm (13 11/16 x 9 1/2 in.) Mount: 57 x 45.1 cm (22 7/16 x 17 3/4 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.11,false,true,283163,Photographs,Photograph,Madura. The Great Pagoda Jewels.,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–February 1858,1858,1858,Albumen silver print from dry collodion on glass negative,Image: 21.9 x 30.1 cm (8 5/8 x 11 7/8 in.) Mount: 45.1 x 57.3 cm (17 3/4 x 22 9/16 in.) Mat: 21 1/8 × 23 15/16 in. (53.6 × 60.8 cm),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.12,false,true,287279,Photographs,Photograph,Outer Prakarum on the North Side of the Temple of the God Sundareshwara,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from dry collodion on glass negative,Image: 26.1 x 34 cm (10 1/4 x 13 3/8 in.) Mount: 45.1 x 57 cm (17 3/4 x 22 7/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.13,false,true,287280,Photographs,Photograph,View of the Sacred Tank in the Great Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 25.8 x 35.1 cm (10 3/16 x 13 13/16 in.) Mount: 45 x 57 cm (17 11/16 x 22 7/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.14,false,true,287281,Photographs,Photograph,View of the Sacred Tank in the Great Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 29.1 x 37.5 cm (11 7/16 x 14 3/4 in.) Mount: 45 x 57 cm (17 11/16 x 22 7/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.381.2.15,false,true,287282,Photographs,Photograph,Entrance to the Temple of Minakshi in the Great Pagoda,,,,,,Artist,,Linnaeus Tripe,"British, Devonport (Plymouth Dock) 1822–1902 Devonport",,"Tripe, Linnaeus",British,1822,1902,January–March 1858,1858,1858,Albumen silver print from waxed paper negative,Image: 30.5 x 37 cm (12 x 14 9/16 in.) Mount: 45.2 x 57 cm (17 13/16 x 22 7/16 in.),"Gilman Collection, Purchase, Cynthia Hazen Polsky Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.422,false,true,292071,Photographs,Photograph,[Grenadier Guards Drummer],,,,,,Artist,,Joseph Cundall,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey",,"Cundall, Joseph",British,1818,1895,ca. 1856,1854,1856,Salted paper print from glass negative,Image: 23.1 x 17.4 cm (9 1/8 x 6 7/8 in.) Mount: 41.5 x 31.9 cm (16 5/16 x 12 9/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/292071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.524.16,false,true,269576,Photographs,Photograph,Cottage at Jersey,,,,,,Artist,,Joseph Cundall,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey",,"Cundall, Joseph",British,1818,1895,1855,1855,1855,Albumen silver print from glass negative,23.5 x 19.1 cm (9 1/4 x 7 1/2 in.),"David Hunter McAlpin Fund, 1952",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.7,false,true,306208,Photographs,Photograph,The Alms House,,,,,,Artist,,Joseph Cundall,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey",,"Cundall, Joseph",British,1818,1895,1855,1855,1855,Albumen silver print from glass negative,Mount: 17 1/4 in. × 11 15/16 in. (43.8 × 30.4 cm) Image: 9 5/16 × 7 5/16 in. (23.7 × 18.5 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.606.1.9,false,true,270861,Photographs,Photograph,Highlanders,,,,,,Artist,,Joseph Cundall,"British, Norwich, Norfolk 1818–1895 Wallington, Surrey",,"Cundall, Joseph",British,1818,1895,1856,1856,1856,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1963",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.949,false,true,283241,Photographs,Photograph,[Loie Fuller Dancing],,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1899,1901,Gelatin silver print,7.7 x 10.2 cm (3 1/16 x 4 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.950,false,true,287806,Photographs,Photograph,[Loie Fuller Dancing],,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1899,1901,Gelatin silver print,"10.3 x 13.3 cm (4 x 5 1/4 in.), irregularly trimmed","Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.951,false,true,287807,Photographs,Photograph,[Loie Fuller Dancing],,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1899,1901,Gelatin silver print,10.1 x 12.5 cm (4 x 4 15/16 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.952,false,true,285696,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,10.9 x 14.3 cm (4 5/16 x 5 5/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.953,false,true,287808,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 4 in. × 5 11/16 in. (10.2 × 14.4 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.954,false,true,287809,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 5 15/16 × 4 3/8 in. (15.1 × 11.1 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.955,false,true,287810,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 3 1/16 in. × 4 in. (7.7 × 10.2 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.956,false,true,287811,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 3 15/16 × 5 13/16 in. (10 × 14.7 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.957,false,true,287812,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 3 × 4 in. (7.6 × 10.1 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.958,false,true,287813,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 4 in. × 5 11/16 in. (10.2 × 14.5 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.959,false,true,287814,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 3 15/16 × 5 3/8 in. (10 × 13.7 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.960,false,true,287815,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 4 in. × 5 1/8 in. (10.2 × 13 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.961,false,true,287816,Photographs,Photograph,Loie Fuller Dancing,,,,,,Artist,,Samuel Joshua Beckett,"British, Shadwell, Stepney [London] 1870–1940 Bournemouth",,"Beckett, Samuel Joshua",British,1870,1940,ca. 1900,1898,1902,Gelatin silver print,Image: 5 15/16 × 4 7/16 in. (15.1 × 11.2 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.217,false,true,285424,Photographs,Photograph,"[Egyptian Obelisk, ""Cleopatra's Needle,"" in Alexandria, Egypt]",,,,,,Artist,Attributed to,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1870,1861,1879,Albumen silver print from glass negative,Image: 15.5 x 20.8 cm (6 1/8 x 8 3/16 in.),"Funds from various donors, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.595.1,false,true,260089,Photographs,Photograph,Distant View of the Cedars of Lebanon,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.595.2,false,true,260090,Photographs,Photograph,Cedars of Lebanon,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,Image: 15.4 x 20.7 cm (6 1/16 x 8 1/8 in.) Mount: 30.5 x 38.5 cm (12 x 15 3/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.674.1,false,true,260348,Photographs,Photograph,[Six East Indian Men],,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.674.2,false,true,260349,Photographs,Photograph,Rajpoots,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.674.3,false,true,260350,Photographs,Photograph,[Three East Indian Women],,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.674.4,false,true,260351,Photographs,Photograph,[Four East Indian Men],,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.674.5,false,true,260352,Photographs,Photograph,Marwaree Brokers,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.674.6,false,true,260353,Photographs,Photograph,Mehmans,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.502.1,false,true,260730,Photographs,Photograph,"Elephanta from Water Cave, Coombe Martin Bay, Watermouth",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.502.4,false,true,260733,Photographs,Photograph,Watermouth,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.502.5,false,true,260734,Photographs,Photograph,Coombe Martin Bay,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1870s,1870,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.597.2,false,true,260957,Photographs,Photograph,"The Written Valley, Sinai",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.597.3,false,true,260958,Photographs,Photograph,"Mount Hermon, The Mount of Transfiguration",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.597.4,false,true,260959,Photographs,Photograph,Bethel,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.597.5,false,true,260960,Photographs,Photograph,The Fountain of Jerico and Probable Site of the City,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.597.6,false,true,260961,Photographs,Photograph,"Mount Moriah, Jerusalem, from the Well of En Rogel",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.597.7,false,true,260962,Photographs,Photograph,"Principal Source of the Jordan, Flowing From a Cave Near Banias, Near the Site of the Northern City of Dan, the Frontier Town of Israel",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,ca. 1857,1855,1859,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.2.1,false,true,271097,Photographs,Photograph,"Wady Kardassy, Nubia",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.2.3,false,true,271098,Photographs,Photograph,"The Largest of the Cedars, Mount Lebannon",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.2.4,false,true,271100,Photographs,Photograph,"Portrait, Turkish Summer Costume",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.2.6,false,true,271101,Photographs,Photograph,"The Sphynx and Great Pyramid, Geezeh",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.2.9,false,true,271102,Photographs,Photograph,"The Mosque of Omar, Jerusalem",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.23,false,true,271085,Photographs,Photograph,The Approach to Phil‘,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.27,false,true,271086,Photographs,Photograph,The Statues of Memnon. Plain of Thebes,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.28,false,true,271087,Photographs,Photograph,"Nablous, The Ancient Shechem",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.30,false,true,271088,Photographs,Photograph,Colossi and Sphynx at Wady Saboua,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.46,false,true,271092,Photographs,Photograph,Damascus,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.53,false,true,271093,Photographs,Photograph,"Valley of the Tombs of the Kings, Thebes",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.54,false,true,271094,Photographs,Photograph,View at Girgeh,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.1.60,false,true,271095,Photographs,Photograph,The Statues of Memnon. Plain of Thebes,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.640.2.32,false,true,271099,Photographs,Photograph,"Abou Simbel, Nubia",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,,"David Hunter McAlpin Fund, 1966",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.555,false,true,286141,Photographs,Photographically illustrated book,The Great Pyramid and The Great Sphinx,,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1858,1858,1858,Albumen silver print from glass negative,Image: 38.7 x 49.3 cm (15 1/4 x 19 7/16 in.),"Gilman Collection, Purchase, William Talbott Hillman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.633,false,true,286142,Photographs,Photograph,"The Rameseum of El-Kurneh, Thebes",,,,,,Artist,,Francis Frith,"British, Chesterfield, Derbyshire 1822–1898 Cannes, France",,"Frith, Francis",British,1822,1898,1857,1857,1857,Albumen silver print from glass negative,Image: 37.9 x 47.7 cm (14 15/16 x 18 3/4 in.) Mount: 59.9 × 73.8 cm (23 9/16 in. × 29 1/16 in.),"Gilman Collection, Purchase, Anonymous Gifts, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.18,false,true,283090,Photographs,Photograph,"""She Never Told Her Love""",,,,,,Artist,,Henry Peach Robinson,"British, Ludlow, Shropshire 1830–1901 Tunbridge Wells, Kent",,"Robinson, Henry Peach",British,1830,1901,1857,1857,1857,Albumen silver print from glass negative,18 x 23.2cm (7 1/16 x 9 1/8in.) Frame: 41.6 x 46.4 cm (16 3/8 x 18 1/4 in.),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1132,false,true,286250,Photographs,Photograph,Fear,,,,,,Artist,,Henry Peach Robinson,"British, Ludlow, Shropshire 1830–1901 Tunbridge Wells, Kent",,"Robinson, Henry Peach",British,1830,1901,ca. 1860,1858,1862,Albumen silver print,Image: 7 15/16 × 6 1/16 in. (20.2 × 15.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.760.4,false,true,259827,Photographs,Photograph,"Carro con buoi, Perugia",,,,,,Artist,,James Anderson,"British, 1813–1877",,"Anderson, James",British,1813,1877,1880s,1880,1889,Albumen silver print,,"Museum Accession, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.384 (1-48),false,true,285734,Photographs,Album,[Album of Spirit Photographs],,,,,,Artist|Artist,,Frederick Hudson|Mr. Reeve,"British, died 1889",,"Hudson, Frederick|Reeve",British,,1889,1872,1870,1880,Albumen silver prints from glass negatives,25.4 x 19.1 cm (10 x 7 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.1.53,false,true,268997,Photographs,Photograph,"Lacock Abbey, Cloisters, September 12, 1855 [?]",,,,,,Artist|Artist,Possibly by,Charles Henry Talbot|Unknown,"British, 1842–1916|British",,"Talbot, Charles Henry|Unknown",British,1842,1916,"September 12, 1855 [?]",1853,1858,Salted paper print from paper negative,,"David Hunter McAlpin Fund, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.134,false,true,283685,Photographs,Photograph,"Catacombs, Convento dei Cappucini, Palermo",,,,,,Photography Studio|Artist,,Eugenio Interguglielmi & Company|Eugenio Interguglielmi,"Italian, active 1895–1981|Italian, Palermo 1850–1911 Palermo",,"Interguglielmi, Eugenio & Company|Interguglielmi, Eugenio",Italian,1850,1911,ca. 1895,1895,1895,Albumen silver print from glass negative,18.6 x 25.3 cm (7 5/16 x 9 15/16 in. ),"Funds from various donors, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.263.168,false,true,284874,Photographs,Print,Veduta del Tempio di Giove Tonante,,,,,,Artist,,Giovanni Battista Piranesi,"Italian, Mogliano Veneto 1720–1778 Rome",,"Piranesi, Giovanni Battista",Italian,1720,1778,1740s–60s,1740,1769,Engraving,Image: 38 x 59.5 cm (14 15/16 x 23 7/16 in.),"Walker Evans Archive, 1994",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/284874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1095,false,true,286681,Photographs,Photograph,Tempio di Vesta,,,,,,Artist,,Pietro Dovizielli,"Italian, 1804–1885",,"Dovizielli, Pietro",Italian,1804,1885,1850s,1850,1859,Salted paper print from paper negative,Mount: 19 3/8 in. × 27 1/16 in. (49.2 × 68.7 cm) Image: 11 9/16 × 15 1/2 in. (29.3 × 39.3 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1096,false,true,286126,Photographs,Photograph,Interno del Colosseo,,,,,,Artist,,Pietro Dovizielli,"Italian, 1804–1885",,"Dovizielli, Pietro",Italian,1804,1885,ca. 1859,1854,1864,Salted paper print from paper negative,Image: 30.4 × 38.7 cm (11 15/16 × 15 1/4 in.) Mount: 49.2 × 63.7 cm (19 3/8 × 25 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.548.2,false,true,261390,Photographs,Photograph,La Simple,,,,,,Artist,,Duroni et Murer,"Italian, 1807–1870",,Duroni et Murer,Italian,1807,1870,1860s,1860,1869,Albumen silver print from glass negative,5.1 x 8.9 cm. (2 x 3 1/2 in.),"David Hunter McAlpin Fund, 1975",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.600.64,false,true,270616,Photographs,Photograph,[Tree in Formal Garden Outside Palazzo],,,,,,Artist,,Giacomo Caneva,"Italian, 1812–1865",,"Caneva, Giacomo",Italian,1812,1865,1860s–70s,1860,1879,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1962",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.600.68,false,true,270620,Photographs,Photograph,[Roman Ruins],,,,,,Artist,Attributed to,Giacomo Caneva,"Italian, 1812–1865",,"Caneva, Giacomo",Italian,1812,1865,1860s,1860,1869,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1962",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.57,false,true,283140,Photographs,Photograph,[Carlotta Cortudino],,,,,,Artist,,Giacomo Caneva,"Italian, 1812–1865",,"Caneva, Giacomo",Italian,1812,1865,ca. 1852,1850,1854,Salted paper print from paper negative,Image: 5 5/16 × 7 13/16 in. (13.5 × 19.9 cm) Mount: 11 5/8 × 11 5/8 in. (29.6 × 29.5 cm),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.570,false,true,286574,Photographs,Photograph,[Vesuvius from Mergellina],,,,,,Artist,,Giacomo Caneva,"Italian, 1812–1865",,"Caneva, Giacomo",Italian,1812,1865,ca. 1855,1853,1857,Salted paper print from paper negative,Image: 13.1 x 27.9 cm (5 3/16 x 11 in.) Mount: 26.7 x 43.5 cm (10 1/2 x 17 1/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.855,false,true,286270,Photographs,Photograph,Veduta d'alla Villa Medici od Academia di Francia,,,,,,Artist,,Giacomo Caneva,"Italian, 1812–1865",,"Caneva, Giacomo",Italian,1812,1865,ca. 1852,1847,1857,Salted paper print from paper negative,Image: 20 × 27.2 cm (7 7/8 × 10 11/16 in.) Mount: 31 × 46.2 cm (12 3/16 × 18 3/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37 (45),false,true,289233,Photographs,Photograph,Primo tentativo fatto in Italia del Sig. Tassinari farmacista e chimico in Castel Bolognese,,,,,,Artist,Likely,Sebastiano Tassinari,"Italian, 1814–1888",,"Tassinari, Sebastiano",Italian,1814,1888,1839–40,1839,1840,Photogenic drawing,"10.2 x 10.5 cm (4 x 4 1/8 in.), irregularly trimmed","Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37 (44a),false,true,289232,Photographs,Photograph,[Photogenic Drawing from Leaf],,,,,,Artist,Likely,Sebastiano Tassinari,"Italian, 1814–1888",,"Tassinari, Sebastiano",Italian,1814,1888,1839–40,1839,1840,Photogenic drawing,"10.6 x 14 cm (4 3/16 x 5 1/2 in.), irregularly trimmed","Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.37 (44b),false,true,289234,Photographs,Photograph,[Photogenic Drawing from Leaf],,,,,,Artist,Likely,Sebastiano Tassinari,"Italian, 1814–1888",,"Tassinari, Sebastiano",Italian,1814,1888,1839–40,1839,1840,Photogenic drawing,"10.6 x 14 cm (4 3/16 x 5 1/2 in.), irregularly trimmed","Harris Brisbane Dick Fund, 1936",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.1,false,true,271262,Photographs,Photograph,Processione sulla facciata della grande scala,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.2,false,true,271273,Photographs,Photograph,Processione sulla facciata della Grande Scala,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.3,false,true,271277,Photographs,Photograph,Processione sulla facciata della Grande Scala,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.4,false,true,271278,Photographs,Photograph,Processione sulla facciata della Grande Scala,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.5,false,true,271279,Photographs,Photograph,Teheran,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.6,false,true,271280,Photographs,Photograph,Amharet es Schah,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.7,false,true,271281,Photographs,Photograph,"[Peacock's Throne Room, Teheran]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.8,false,true,271282,Photographs,Photograph,"Outer Entrance to the King's Palace, Teheran",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.9,false,true,271283,Photographs,Photograph,Teheran. Prime Minister's House (Nezanneh),,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.10,false,true,271263,Photographs,Photograph,Tomb of the Khan of Khiva at Teheran,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1859,1859,1859,Albumen silver print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.11,false,true,271264,Photographs,Photograph,"British Legation, Teheran",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.12,false,true,271265,Photographs,Photograph,"Zerghiandeh. Russian Minister's Country House, Teheran",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.13,false,true,271266,Photographs,Photograph,Bagh-takt a chiraz,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.14,false,true,271267,Photographs,Photograph,"Yran, Piazza a Isphaan",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.15,false,true,271268,Photographs,Photograph,Bassirilieve a Maksci Reste,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.16,false,true,271269,Photographs,Photograph,"Ruine sulla prima terrazza, Persepolis",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.17,false,true,271270,Photographs,Photograph,Tomba sulla rocca a Persepolis,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.18,false,true,271271,Photographs,Photograph,Tombe de Ciro a Morgab,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.19,false,true,271272,Photographs,Photograph,L'antica porta d'ingrezza a Persepolis],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.20,false,true,271274,Photographs,Photograph,"Ruine sulla terza terazza, Persepolis",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.21,false,true,271275,Photographs,Photograph,Porta d'entrata alla ruine de Persepolis,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.606.22,false,true,271276,Photographs,Photograph,Veduta generale di Persepolis presa dalla Montagna,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1858,1858,1858,Salted paper print from paper negative,,"Gift of Kay Gregory, 1967",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.2,false,true,652099,Photographs,Photograph,"[Golestan Palace, Interior, Teheran, Iran]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.3,false,true,652100,Photographs,Photograph,"[Golestan, The Peacock Throne, Teheran, Iran] (Takht-I Taous)",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.4,false,true,652101,Photographs,Photograph,"[Golestan Palace, Teheran, Iran] (Takht-i Marmor)",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.5,false,true,652102,Photographs,Photograph,"[Golestan, Le Salon et Fete de l'equinode, Teheran, Iran (le Pavillion du Trone)]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.6,false,true,652103,Photographs,Photograph,"[Gate of Government, Teheran, Iran]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.7,false,true,652104,Photographs,Photograph,"[Plaza of Canons, Teheran, Iran] (Maydan-i Top-khaneh)",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.8,false,true,262204,Photographs,Photograph,"[The New Gate, Teheran]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1850s,1850,1859,Albumen silver print from paper negative,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.9,false,true,652106,Photographs,Photograph,"[South Gate of the Arq, Teheran, Iran]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.10,false,true,262200,Photographs,Photograph,[Palace of the Shah],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1850s,1850,1859,Albumen silver print from paper negative,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.11,false,true,652107,Photographs,Photograph,"[Summer residence (Qasr) of the Shah, Emarat-e xoruji, Teheran, Iran]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.12,false,true,652108,Photographs,Photograph,"[Palace of the Shah, Teheran, Iran]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.13,false,true,652109,Photographs,Photograph,"[Palace of the Shah, Paying respects to the Shah/Fete de Salam, Teheran, Iran [same as 12] ]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.14,false,true,652110,Photographs,Photograph,"[Palace of the Shah, Teheran, Iran]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.15,false,true,652111,Photographs,Photograph,"[The Sublime Porte, Teheran, Iran]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.16,false,true,652112,Photographs,Photograph,"[Mosque of Nasser-eddin Shah, Teheran, Iran]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.17,false,true,652113,Photographs,Photograph,"[Tomb of Khan of Khiva, Uzbekistan]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.19,false,true,652115,Photographs,Photograph,[Departure for the huntTomb of Khan of Khiva],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Salted paper print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.20,false,true,652116,Photographs,Photograph,[Armenian Woman of Teheran],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.21,false,true,262201,Photographs,Photograph,[In the Mosque of the Damegan/The Eunuchs],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1850s,1850,1859,Albumen silver print from paper negative,16.3 x 23.2 cm. (6 7/16 x 9 1/8 in.),"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.25,false,true,652118,Photographs,Photograph,"Portrait of Ardeshir Mirza, uncle of the king",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Salted paper print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.26,false,true,652119,Photographs,Photograph,"[A Persian revue in a painting that once belonged to Ardeshir Mirza, uncle of the king.]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.27,false,true,652120,Photographs,Photograph,"[Fath-Ali Shah, Painting that Once Belonged to Hmah [?] Saula, Uncle of the King.]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.28,false,true,652121,Photographs,Photograph,"[RAYY, Tower of Toghrul, 1139.]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.29,false,true,652122,Photographs,Photograph,[A General View of MESHED from the roof of a hamam.],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.30,false,true,652123,Photographs,Photograph,[Principal Gate of MESHED],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.31,false,true,652124,Photographs,Photograph,"[New Court of Imam Riza, MESHED]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.32,false,true,302221,Photographs,Photograph,"[Main Gate of Imam Riza, Mashhad, Iran]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1850s,1850,1859,Albumen silver print from paper negative,Image: 16.8 x 21.1 cm (6 5/8 x 8 5/16 in.) Sheet: 17.2 x 21.1 cm (6 3/4 x 8 5/16 in.),"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.33,false,true,652126,Photographs,Photograph,[Old Court of Imam Riza MESHED],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.34,false,true,652127,Photographs,Photograph,"[Court of the mosque Gawhar Shad, MESHED, 1418 (?)]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.35,false,true,652128,Photographs,Photograph,"[Nadir Shah's Golden Gate and Minaret. Otherwise known as the Golden Iwan of Ali Shir Nawai, late 15th Century with Restorations. MESHED]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.36,false,true,652129,Photographs,Photograph,[Mosque of the Shah],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.37,false,true,652130,Photographs,Photograph,[Tomb of Kogin Baba],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.38,false,true,652131,Photographs,Photograph,[Tomb of Seeh-i Mumin],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.39,false,true,652132,Photographs,Photograph,"[Bastam, Tomb Tower (built 1313), Khorasan]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.40,false,true,652133,Photographs,Photograph,"[Tomb of Bayazid, BISTAM]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.41,false,true,652134,Photographs,Photograph,[Cemetry of MESHED],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.42,false,true,652135,Photographs,Photograph,[View of TABRIZ],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.43,false,true,652136,Photographs,Photograph,"[Blue Mosque of TABRIZ, 1465.]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.44,false,true,652137,Photographs,Photograph,[A Persian Citadel in the Environs of Sultaniye],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.45,false,true,652138,Photographs,Photograph,"[Tomb of Oljaetu, 1305-1313.]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.46,false,true,652139,Photographs,Photograph,[Mosque at Sultaniye],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.47,false,true,652140,Photographs,Photograph,"[Mosque at Sultaniye, [same as 46] ]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.48,false,true,652141,Photographs,Photograph,"[View of Kermanshah, Capital of Kurdistan]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Salted paper print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.49,false,true,652142,Photographs,Photograph,[View of Koum],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.50,false,true,652143,Photographs,Photograph,[Mosque of Koum],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.51,false,true,652144,Photographs,Photograph,[The Bridge at Dizfoul],,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Salted paper print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.52,false,true,652145,Photographs,Photograph,"[Ruins, Dizfoul]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.53,false,true,652146,Photographs,Photograph,"[The Tower of 'Chihil Dukhtaran', Mausoleum of 40 daughters, 1056.]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.54,false,true,652147,Photographs,Photograph,"[Minaret of the Mosque of 40 Columns, Chehel Dokhtar, 359b.]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.55,false,true,652148,Photographs,Photograph,"[Minaret of the Chief Mosque at Damghan, 1026–1029]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.56,false,true,652149,Photographs,Photograph,"[Tag-e bustan, Kermanshah, Kurdestan]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.57,false,true,652150,Photographs,Photograph,"[Ruins of Tus, Khorasan]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.58,false,true,652151,Photographs,Photograph,"[Other ruins in the town of Tus, Khorasan]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.59,false,true,652152,Photographs,Photograph,"(1) [Tag-e Bustan, Kermanshah]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.60,false,true,652153,Photographs,Photograph,(2) [Persepolis],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.61,false,true,652154,Photographs,Photograph,(3) [Persepolis (?)],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.62,false,true,652155,Photographs,Photograph,"(4) [Naksh-i Rustam, Near Persepolis]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.63,false,true,652156,Photographs,Photograph,(5) [Persepolis],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.64,false,true,652157,Photographs,Photograph,"(6) [Naksh-i Rustam, Near Persepolis]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.65,false,true,652158,Photographs,Photograph,"(7) [Tag-e Bustan, Crowning Ceremony of Shapour II, Kermanshah]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.67,false,true,652160,Photographs,Photograph,(9) Untitled,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.68,false,true,652161,Photographs,Photograph,"(10) [Gate of all Nations, Persepolis, Fars]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.69,false,true,652162,Photographs,Photograph,(11) [Naksh-i Rustam],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.70,false,true,652163,Photographs,Photograph,"(12) [Persepolis, (W: before restoration)",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.71,false,true,652164,Photographs,Photograph,(13) [Persepolis],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.72,false,true,262203,Photographs,Photograph,[Persepolis],,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1850s,1850,1859,Albumen silver print from paper negative,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.73,false,true,652165,Photographs,Photograph,"(15) [Gate of all Nations, Persepolis, Fars]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.74,false,true,652166,Photographs,Photograph,"(16) [Apadana Hall Eastern Stairway, Persepolis, Fars]",,,,,,Artist,Possibly by,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.75,false,true,652167,Photographs,Photograph,(17) Untitled,,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.683.76,false,true,652168,Photographs,Photograph,"(18) [Inscription, Old Persian in Cuneiform]",,,,,,Artist,,Luigi Pesce,"Italian, 1818–1891",,"Pesce, Luigi",Italian,1818,1891,1840s–60s,1840,1869,Albumen silver print,,"Gift of Charles K. and Irma B. Wilkinson, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/652168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.456,false,true,265528,Photographs,Photograph,"[Shirtless Man with Arm Raised in Fascist Salute with Superimposed Cross, Circle, and Spray of Flowers]",,,,,,Artist,,Mario Castagneri,"Italian, 1892–1940",,"Castagneri, Mario",Italian,1892,1940,ca. 1930,1928,1932,Gelatin silver print,23.0 x 28.8 cm (9 1/16 x 11 5/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.43,false,true,265498,Photographs,Photograph,"Stairs, Mexico City",,,,,,Artist,,Tina Modotti,"Italian, 1896–1942",,"Modotti, Tina",Italian,1896,1942,1924–26,1924,1926,Gelatin silver print,18.4 x 23.9 cm (7 1/4 x 9 7/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.657,false,true,285802,Photographs,Carte-de-visite,Vittorio Emanuele II,,,,,,Artist,,Cesare Bernieri,"Italian, active Turin 1861–70",,"Bernieri, Cesare",Italian,1861,1870,1867,1867,1867,Albumen silver print from glass negative,Image: 6.2 x 4.7 cm (2 7/16 x 1 7/8 in.) oval Sheet: 10.1 x 5.9 cm (4 x 2 5/16 in.) Mount: 10.6 x 6.4 cm (4 3/16 x 2 1/2 in.) Mat: 21.9 x 15.7 cm (8 5/8 x 6 3/16 in.) Case: 22.9 x 17.1 x 1.3 cm (9 x 6 3/4 x 1/2 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.1,false,true,263104,Photographs,Photograph,"Medinet Habou, 2me cour cote sudouest",,,,,,Artist,,Antonio Beato,"British, born Corfu, 1834–1906",,"Beato, Antonio",Italian,1834,1906,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.2,false,true,263115,Photographs,Photograph,"Luxor, vue du temple cote ouest",,,,,,Artist,,Antonio Beato,"British, born Corfu, 1834–1906",,"Beato, Antonio",Italian,1834,1906,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1194.3,false,true,263126,Photographs,Photograph,Temple a Denderah,,,,,,Artist,,Antonio Beato,"British, born Corfu, 1834–1906",,"Beato, Antonio",Italian,1834,1906,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of Mrs. John L. Swayze, 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.562,false,true,286136,Photographs,Photograph,Interno del Colosseo,,,,,,Artist,,Giovanni Battista Altadonna,"Italian, Borgo Valsugana 1824–1890 Trento",,"Altadonna, Giovanni Battista",Italian,1824,1890,1850s,1850,1859,Albumen silver print from glass negative,"Image: 19.1 x 24.9 cm (7 1/2 x 9 13/16 in.), oval Mount: 26.8 x 35.7 cm (10 9/16 x 14 1/16 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.148,false,true,260157,Photographs,Photomechanical print,Minuet,,,,,,Artist|Printer,,"Frank Eugene|F. Bruckmann Verlag, Munich","American, New York 1865–1936 Munich",,"Eugene, Frank|F. Bruckmann Verlag, Munich",American,1865,1936,"1900, printed 1909",1900,1900,Photogravure,12.7 x 17.7 cm (5 x 7 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.46,false,true,301949,Photographs,"Photograph, corsage",[Mourning Corsage with Portrait of Abraham Lincoln],,,,,,Maker|Photography Studio,After,Unknown|Brady & Co.,"American|American, active 1840s–1880s",,Unknown|Brady & Co.,American,1840,1889,April 1865,1865,1865,Black and white silk with tintype set inside brass button,20 x 9 cm (7 7/8 x 3 9/16 in.) Image: 2 x 2 cm (13/16 x 13/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Assemblages,,http://www.metmuseum.org/art/collection/search/301949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.176,false,true,260188,Photographs,Postcard,"Luitpold u. Albrecht, Kgl. Prinzen v. Bayern",,,,,,Artist|Printer,,Frank Eugene|Böhm Publishers,"American, New York 1865–1936 Munich",,"Eugene, Frank|Böhm Publishers",American,1865,1936,1908,1908,1908,Halftone,,"Rogers Fund, 1972",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/260188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.48,false,true,268239,Photographs,Photograph,"Winter Quarters, Fort Brady",,,,,,Former Attribution,Formerly attributed to,William Frank Browne,American,,"Browne, William Frank",American,,1867,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.737.11,false,true,260387,Photographs,Photograph,"Abandoned Farm in the Dustbowl, Coldwater District, near Dalhart, Texas, June",,,,,,Artist|Printer,,Dorothea Lange|Library of Congress,"American, 1895–1965",,"Lange, Dorothea|Library Of Congress",American,1895,1965,"1938, printed ca. 1972",1938,1938,Gelatin silver print,,"Gift of Phyllis D. Massar, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.260,false,true,265311,Photographs,Photograph,"[Open-Air Barber at Work and Waiting Customers before Mileston, Mississippi, Post Office]",,,,,,Artist|Printer,,Marion Post Wolcott|F.S.A. studio,"American, 1910–1990",,"Wolcott, Marion Post|F.S.A. studio",American,1910,1990,1939,1939,1939,Gelatin silver print,25.0 x 32.1 cm (9 13/16 x 12 5/8 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.117,false,true,685459,Photographs,Carte-de-visite,[Charles Calverley],,,,,,Photography Studio,,Thompson Gallery,"American, active 1860s",,Thompson Gallery,American,1860,1869,1864–66,1864,1866,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.263,false,true,685604,Photographs,Carte-de-visite,[Elliott and Palmer ?],,,,,,Photography Studio,,Thompson Gallery,"American, active 1860s",,Thompson Gallery,American,1860,1869,1864–66,1864,1866,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.759,false,true,686098,Photographs,Carte-de-visite,[James Suydam],,,,,,Photography Studio,,Rintoul & Rockwood,"American, active 1860s",,Rintoul & Rockwood,American,1859,1870,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.133,false,true,305831,Photographs,Photomontage; Cabinet card,[Man Cutting Watermelon],,,,,,Photography Studio,,Johnson,"American, active 1898–1903",,Johnson,American,1898,1903,1898–1903,1898,1903,Carbon print,Image: 9.4 × 13.2 cm (3 11/16 × 5 3/16 in.) Mount: 10.9 × 16.6 cm (4 5/16 × 6 9/16 in.),"Purchase, Greenwich ART Group Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/305831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.55,false,true,301993,Photographs,Photograph,[Major General William Tecumseh Sherman Wearing Mourning Armband],,,,,,Photography Studio,,Brady & Co.,"American, active 1840s–1880s",,Brady & Co.,American,1840,1889,1865,1865,1865,Albumen silver print from glass negative,Image: 8.5 x 5.4 cm (3 3/8 x 2 1/8 in.) Mount: 10.2 x 6.1 cm (4 x 2 3/8 in.),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.86,false,true,283187,Photographs,Photograph,[Senator and Mrs. James Henry Lane],,,,,,Photography Studio,,Brady & Co.,"American, active 1840s–1880s",,Brady & Co.,American,1840,1889,1861–66,1861,1866,Albumen silver print from glass negative,22.8 × 19.7 cm (9 × 7 3/4 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.98,false,true,283205,Photographs,Photograph,Relics of Andersonville Prison,,,,,,Photography Studio,,Brady & Co.,"American, active 1840s–1880s",,Brady & Co.,American,1840,1889,June 1866,1866,1866,Albumen silver print from glass negative,Image:22.1 x 18.9cm (8 11/16 x 7 7/16in.) Mount: 13 9/16 × 10 9/16 in. (34.5 × 26.9 cm),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.22,false,true,291775,Photographs,Daguerreotype,[Bearded Man],,,,,,Photography Studio,,Brady & Co.,"American, active 1840s–1880s",,Brady & Co.,American,1840,1889,1853–57,1853,1857,Daguerreotype,Image: 9.2 x 6.8 cm (3 5/8 x 2 11/16 in.) Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.) Case: 1.9 x 12.1 x 9.5 cm (3/4 x 4 3/4 x 3 3/4 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.77,false,true,291830,Photographs,Daguerreotype,Francis Alofsen,,,,,,Photography Studio,,Brady & Co.,"American, active 1840s–1880s",,Brady & Co.,American,1840,1889,1855,1855,1855,Daguerreotype,Image: 7 x 5.8 cm (2 3/4 x 2 5/16 in.) Plate: 8.3 x 7 cm (3 1/4 x 2 3/4 in.) Case: 2.1 x 9.4 x 8.3 cm (13/16 x 3 11/16 x 3 1/4 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.77,false,true,260289,Photographs,Photograph,Minuet,,,,,,Artist|Printer,,"Frank Eugene|F. Bruckmann Verlag, Munich","American, New York 1865–1936 Munich",,"Eugene, Frank|F. Bruckmann Verlag, Munich",American,1865,1936,"1900, printed 1909",1900,1900,Photogravure,12.7 x 17.7 cm (5 x 7 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.58,false,true,302000,Photographs,Carte-de-visite,[Young Woman with Beckers Tabletop Stereoscope],,,,,,Photography Studio,,Porter Photograph Parlors,"American, active Janesville, Wisconsin, 1860s",,Porter Photograph Parlors,American,1860,1869,1864–66,1864,1866,Albumen silver print from glass negative,Image: 9.1 x 5.2 cm (3 9/16 x 2 1/16 in.),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.12,false,true,299481,Photographs,Medal; Button,[Presidential Campaign Medal with portraits of Abraham Lincoln and Hannibal Hamlin],,,,,,Maker|Artist,After,Unknown|Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Unknown|Brady, Mathew B.",American,1823,1896,1860,1860,1860,Tintype,"Image: 1.4 cm (9/16 in.), diameter Overall: 2.4 cm (15/16 in.), diameter","Purchase, The Overbrook Foundation Gift, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.1,false,true,267879,Photographs,Photograph,"U.S. Monitor Onondaga, James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.2,false,true,267990,Photographs,Photograph,"Co. B, 30th Pennsylvania Infantry",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.4,false,true,268211,Photographs,Photograph,"Company C, 9th Indiana Infantry (Sherman's Veterans)",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.6,false,true,268252,Photographs,Photograph,"Potomac Creek Railroad Bridge, A.C. & F. Railroad",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.9,false,true,268285,Photographs,Photograph,"Ironclad fleet on James River below Rebel ""Howlett House Battery""",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1865,1865,1865,Albumen silver print from glass negative,Image: 16.8 x 20.3 cm (6 5/8 x 8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.23,false,true,268023,Photographs,Photograph,"Pennsylvania Light Artillery, Keystone Battery",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.24,false,true,268034,Photographs,Photograph,Camp of 30th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.27,false,true,268067,Photographs,Photograph,"Camp of 34th Massachusetts Infantry near Fort Lyon, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.29,false,true,268089,Photographs,Photograph,"Pennsylvania Light Artillery, Keystone Battery",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.30,false,true,268101,Photographs,Photograph,"Campbell Hospital, D.C.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268101,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.31,false,true,268112,Photographs,Photograph,"Co. E, 21st Michigan Infantry. Sherman's Volunteers",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,16.9 x 22.9 cm (6 5/8 x 9 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.33,false,true,268134,Photographs,Photograph,"Co. D, 21st Michigan Infantry. Sherman's Volunteers",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.36,false,true,268167,Photographs,Photograph,"Pennsylvania Light Artillery, Keystone Battery",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.37,false,true,268178,Photographs,Photograph,Washington. Harewood Hospital,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.38,false,true,268189,Photographs,Photograph,"[Maneuvers, Winter Quarters]. Brady album, p. 129",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,12.6 x 19.8 cm (4 15/16 x 7 13/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.39,false,true,268200,Photographs,Photograph,"[Encampment with shacks and laundry]. Brady album, p. 129",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.41,false,true,268223,Photographs,Photograph,Land Battery of Naval Guns,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.42,false,true,268233,Photographs,Photograph,"[Playing Cards, Winter Quarters]. Brady album, p. 129",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.43,false,true,268234,Photographs,Photograph,"Transports, Tennessee River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.44,false,true,268235,Photographs,Photograph,"Transports, Tennessee River at Chattanooga",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.47,false,true,268238,Photographs,Photograph,"U.S. Monitor Onondaga, James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.78,false,true,268272,Photographs,Photograph,"U.S. Monitor Onondaga, James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.79,false,true,268273,Photographs,Photograph,"U.S. Monitor Onondaga, James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.81,false,true,268276,Photographs,Photograph,"U.S. Monitor ""Saugus"" and Gunboat ""Mendota"", Appomattox River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.82,false,true,268277,Photographs,Photograph,"U.S. Monitor ""Mahopac"" on the Appomattox River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1864,1864,1864,Albumen silver print from glass negative,Image: 12.9 x 20.1 cm (5 1/16 x 7 15/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.83,false,true,268278,Photographs,Photograph,"U.S. Monitor Lehigh, James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.84,false,true,268279,Photographs,Photograph,"""Mendota"", 100 lb. Parrott Gun",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.85,false,true,268280,Photographs,Photograph,"U.S. Gunboat ""Mendota"", James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.86,false,true,268281,Photographs,Photograph,Camp of 44th New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.87,false,true,268282,Photographs,Photograph,"Ward in Hospital. Convalescent Camp, Alexandria Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.88,false,true,268283,Photographs,Photograph,Convalescent Camp near Alexandria,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,12.6 x 20 cm (4 15/16 x 7 7/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.89,false,true,268284,Photographs,Photograph,"Barracks at Alexandria, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,13.1 x 20.1 cm (5 3/16 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.97,false,true,268293,Photographs,Photograph,Camp of 44th New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.116,false,true,267898,Photographs,Photograph,Fortifications at City Point,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.117,false,true,267899,Photographs,Photograph,"Summer Headquarters of General Grant, City Point, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.121,false,true,267904,Photographs,Photograph,"Sanitary Commission Office. Convalescent Camp, Alexandria, Virginia",,,,,,Former Attribution,Former attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.122,false,true,267905,Photographs,Photograph,Convalescent Camp near Alexandria,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,13.2 x 21 cm (5 3/16 x 8 1/4 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.125,false,true,267908,Photographs,Photograph,"U.S. Gunboat ""Mendota"", James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.126,false,true,267909,Photographs,Photograph,"U.S. Gunboat ""Mendota"", James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,15.1 x 21.1 cm (5 15/16 x 8 5/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.127,false,true,267910,Photographs,Photograph,"[Crew of U.S. Monitor ""Saugus""]. Brady album, p. 172",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.128,false,true,267911,Photographs,Photograph,"""General Grant"" at Kingston Gap, Tennessee River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.129,false,true,267912,Photographs,Photograph,"U.S. Ship ""Mendota"", James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.131,false,true,267915,Photographs,Photograph,"Frigates ""Santee"" and ""Constitution"" off Naval Academy, Annapolis",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.132,false,true,267916,Photographs,Photograph,"U.S. Monitor ""Casco"" on James River, taken from a lookout tower on bank.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.135,false,true,267919,Photographs,Photograph,Ruins of RR Bridge. Bull Run,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1862,1860,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.141,false,true,267926,Photographs,Photograph,139th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,12.5 x 19.1 cm (4 15/16 x 7 1/2 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.142,false,true,267927,Photographs,Photograph,"U.S. Transport ""Wau Katchie""",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,17.4 x 21.1 cm (6 7/8 x 8 5/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.155,false,true,267941,Photographs,Photograph,"Fort Burnham, front of Petersburg",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.161,false,true,267948,Photographs,Photograph,"U.S. Gunboat ""Saginaw"" and Monitor ""Onondaga""",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.163,false,true,267950,Photographs,Photograph,164th New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.164,false,true,267951,Photographs,Photograph,"[Regiments - unidentified]. Brady album, p. 156",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,13.5 x 20.2 cm (5 5/16 x 7 15/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.171,false,true,267959,Photographs,Photograph,"Transports, Tennessee River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.173,false,true,267961,Photographs,Photograph,"""Building Winter Quarters""",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.176,false,true,267964,Photographs,Photograph,[Regiments - unidentified],,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,12.7 x 20.1 cm (5 x 7 15/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.177,false,true,267965,Photographs,Photograph,"""At the Sutler's Store""",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.178,false,true,267966,Photographs,Photograph,"Co. B, 170th New York Volunteers",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.179,false,true,267967,Photographs,Photograph,"Camp of 34th Massachusetts Infantry, Miner's Hill, VA. Skirmish Drill.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862–63,1862,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.181,false,true,267970,Photographs,Photograph,"Fort Ellsworth, Alexandria, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.185,false,true,267974,Photographs,Photograph,"Camp of 34th Massachusetts Infantry near Fort Lyon, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.187,false,true,267976,Photographs,Photograph,"[Unidentified camp with ruined chimneys in background]. Brady album, p. 130",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,11 x 20.1 cm (4 5/16 x 7 15/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.188,false,true,267977,Photographs,Photograph,"[Earthworks at the edge of a forest]. Brady album, p. 132",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.189,false,true,267978,Photographs,Photograph,Cannon,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.191,false,true,267981,Photographs,Photograph,"[Maneuvers, Union Cavalry, Winter Quarters]. Brady album, p. 127",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.192,false,true,267982,Photographs,Photograph,139th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.193,false,true,267983,Photographs,Photograph,Heavy Artillery,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.194,false,true,267984,Photographs,Photograph,"[Regiments - unidentified]. Brady album, p. 156",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.195,false,true,267985,Photographs,Photograph,139th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.196,false,true,267986,Photographs,Photograph,Fork,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.197,false,true,267987,Photographs,Photograph,"[Landscape with army encampment in the distance]. Brady album, p. 125",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.198,false,true,267988,Photographs,Photograph,Fortifications,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,15.5 x 20.8 cm (6 1/8 x 8 3/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.199,false,true,267989,Photographs,Photograph,A Photographer's Store (Brady's?),,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.205,false,true,267997,Photographs,Photograph,Heavy Artillery,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.206,false,true,267998,Photographs,Photograph,"[Regiments - unidentified]. Brady album, p. 157",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.208,false,true,268000,Photographs,Photograph,"[Winter Quarters, troops with row of cabins]. Brady album, p. 128",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,12.9 x 20.3 cm (5 1/16 x 8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.209,false,true,268001,Photographs,Photograph,"[Winter Quarters, troops with row of cabins]. Brady album, p. 128",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.217,false,true,268010,Photographs,Photograph,"Unloading Supplies for U.S. Military Railroad opposite Richmond, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.232,false,true,268026,Photographs,Photograph,21st Michigan Infantry. Sherman's Volunteers,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.235,false,true,268029,Photographs,Photograph,30th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,15.9 x 20.3 cm (6 1/4 x 8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.236,false,true,268030,Photographs,Photograph,"Maryland Heights, near Harper's Ferry, New York State Militia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.237,false,true,268031,Photographs,Photograph,"Co. A, 30th Pennsylvania Infantry, Camp Mott Hooton",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.238,false,true,268032,Photographs,Photograph,30th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.239,false,true,268033,Photographs,Photograph,139th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,12.5 x 20.2 cm (4 15/16 x 7 15/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.240,false,true,268035,Photographs,Photograph,Camp of 153rd New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.242,false,true,268037,Photographs,Photograph,"Headquarters, Co. F, 11th Rhode Island Infantry, Miner's Hill, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862,1862,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.243,false,true,268038,Photographs,Photograph,153rd New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.244,false,true,268039,Photographs,Photograph,30th Pennsylvania Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.246,false,true,268041,Photographs,Photograph,Camp of 153rd New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.247,false,true,268042,Photographs,Photograph,44th Indiana Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.248,false,true,268043,Photographs,Photograph,"Stoneman's Station, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.249,false,true,268044,Photographs,Photograph,"Stoneman's Station, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,12.6 x 20 cm (4 15/16 x 7 7/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.250,false,true,268046,Photographs,Photograph,"Stoneman's Station, Virginia. Quartermaster Dept.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,13 x 19.8 cm (5 1/8 x 7 13/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.251,false,true,268047,Photographs,Photograph,"Stoneman's Station, Virginia. Commissary Dept.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.265,false,true,268062,Photographs,Photograph,U.S. Gunboat at Kingston Gap,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.267,false,true,268064,Photographs,Photograph,Camp of 44th New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.268,false,true,268065,Photographs,Photograph,"Hunting Creek, near Alexandria, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,14.8 x 20.6 cm (5 13/16 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.276,false,true,268074,Photographs,Photograph,"[Mitchell's Plantation, Hopewell, Virginia]",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1863–64,1863,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.277,false,true,268075,Photographs,Photograph,Union Cavalry Winter Quarters,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,15.8 x 21 cm (6 1/4 x 8 1/4 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.282,false,true,268081,Photographs,Photograph,"Camp of Construction Corps, U.S. Military Rail Road, near Manchester, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.283,false,true,268082,Photographs,Photograph,"Hanover Junction, Pennsylvania",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.285,false,true,268084,Photographs,Photograph,"Fort Johnson, James Island, looking toward Fort Sumter",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.286,false,true,268085,Photographs,Photograph,"Hanover Junction Station, Pennsylvania",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.288,false,true,268087,Photographs,Photograph,"Falls Church, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,16.6 x 20.6 cm (6 9/16 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.290,false,true,268090,Photographs,Photograph,"Camp Barry near Bladensberg, Maryland. Artillery Depot",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,15.8 x 21 cm (6 1/4 x 8 1/4 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.293,false,true,268093,Photographs,Photograph,"Manchester, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.296,false,true,268096,Photographs,Photograph,"Fort Putnam, South Carolina",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.297,false,true,268097,Photographs,Photograph,"Fort Johnson, James Island, South Carolina",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.298,false,true,268098,Photographs,Photograph,"Geisboro D.C., Barracks at Fort Carroll",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1863–64,1863,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.299,false,true,268099,Photographs,Photograph,"Camp Barry near Bladensberg, Maryland. Stables",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.300,false,true,268102,Photographs,Photograph,"Washington, D.C.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.301,false,true,268103,Photographs,Photograph,Washington. Harewood Hospital,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.302,false,true,268104,Photographs,Photograph,Washington. Harewood Hospital,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.303,false,true,268105,Photographs,Photograph,Washington. Harewood Hospital,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.304,false,true,268106,Photographs,Photograph,Washington. Harewood Hospital,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.305,false,true,268107,Photographs,Photograph,"Campbell Hospital near Washington, D.C.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.309,false,true,268111,Photographs,Photograph,Washington. Harewood Hospital,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,15.8 x 21.1 cm (6 1/4 x 8 5/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.310,false,true,268113,Photographs,Photograph,"Battery Rodgers, Potomac River near Washington",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,16.6 x 20.6 cm (6 9/16 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.311,false,true,268114,Photographs,Photograph,"Distant View of Arsenal, Washington, D.C.",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,15.5 x 20.7 cm (6 1/8 x 8 1/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.314,false,true,268117,Photographs,Photograph,"[Wagon in a landscape with army encampment in the distance]. Brady album, p. 125",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.315,false,true,268118,Photographs,Photograph,[Fortifications],,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.317,false,true,268120,Photographs,Photograph,Fort Sumter,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.318,false,true,268121,Photographs,Photograph,"6th Vermont Infantry, Camp Griffen",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.319,false,true,268122,Photographs,Photograph,"U.S. Monitor ""Saugus""",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.320,false,true,268124,Photographs,Photograph,Officers of U.S.S. Hunchback,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.321,false,true,268125,Photographs,Photograph,Officers of U.S.S. Hunchback,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.322,false,true,268126,Photographs,Photograph,Officers of U.S.S. Hunchback,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.323,false,true,268127,Photographs,Photograph,Officers of U.S.S. Hunchback,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.324,false,true,268128,Photographs,Photograph,Officers of U.S.S. Hunchback,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.326,false,true,268130,Photographs,Photograph,"Officers of ""Mendota""",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.328,false,true,268132,Photographs,Photograph,[Encampment on a bluff],,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,9.8 x 10.7 cm (3 7/8 x 4 3/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.331,false,true,268136,Photographs,Photograph,22nd New York State Militia,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.333,false,true,268138,Photographs,Photograph,Officers of U.S.S. Hunchback,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.336,false,true,268141,Photographs,Photograph,Cannon,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.344,false,true,268150,Photographs,Photograph,"U.S. Monitor Lehigh, James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.345,false,true,268151,Photographs,Photograph,"U.S. Monitor Lehigh, James River",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.350,false,true,268157,Photographs,Photograph,23rd New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.351,false,true,268158,Photographs,Photograph,33rd New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1859,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.354,false,true,268161,Photographs,Photograph,Officers of U.S.S. Hunchback,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.356,false,true,268163,Photographs,Photograph,"[Herd of Horses]. Brady album, p. 123",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.357,false,true,268164,Photographs,Photograph,"[View of a small town with wooden sheds in distance]. Brady album, p. 123",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.360,false,true,268168,Photographs,Photograph,[Four men in camp under a lean-to of pine boughs],,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.363,false,true,268171,Photographs,Photograph,"[Encampment alongside a stand of trees]. Brady album, p. 123",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.364,false,true,268172,Photographs,Photograph,"[White picket fence with buildings in background]. Brady album, p. 123",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.367,false,true,268175,Photographs,Photograph,"[Roads leading into a small town]. Brady album, p. 123",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.369,false,true,268177,Photographs,Photograph,74th New York Infantry,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861,1861,1861,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.370,false,true,268179,Photographs,Photograph,"26th New York Infantry at Fort Lyon, near Alexandria, Virginia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.371,false,true,268180,Photographs,Photograph,Fort Sumter,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.372,false,true,268181,Photographs,Photograph,"Camp Northumberland, 91st Pennsylvania Infantry",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861,1861,1861,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.373,false,true,268182,Photographs,Photograph,"Navy Yard, Washington, 71st New York State Militia",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.375,false,true,268184,Photographs,Photograph,Fort Johnson,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.376,false,true,268185,Photographs,Photograph,"Fort Gaines, Officers of the 55th New York Volunteers",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.377,false,true,268186,Photographs,Photograph,"[Pleasant Valley Winery, New York]",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1860,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.378,false,true,268187,Photographs,Photograph,Fort Sumter,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.379,false,true,268188,Photographs,Photograph,Swamp Angel Battery,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.380,false,true,268190,Photographs,Photograph,"Camp Jameson, Hall's Hill, 22nd Maine Infantry",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.382,false,true,268192,Photographs,Photograph,Sally at Fort Richardson,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862,1862,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.384,false,true,268194,Photographs,Photograph,"Fort Totten, N.W. of Washington",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.385,false,true,268195,Photographs,Photograph,"[Cache of Cannon]. Brady album, p. 123",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.386,false,true,268196,Photographs,Photograph,"[House framed by trees]. Brady album, p. 123",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.390,false,true,268201,Photographs,Photograph,Gettysburg. John Burns House,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.391,false,true,268202,Photographs,Panorama,Gettysburg Wheat Field,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.392,false,true,268203,Photographs,Photograph,"Gettysburg, Pennsylvania",,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.394,false,true,268205,Photographs,Photograph,Gettysburg from the West,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.562,false,true,283979,Photographs,Photograph,Confederate Torpedoes Taken from James River,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1864,1864,1864,Albumen silver print from glass negative,13.3 x 20.6 cm (5 1/4 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"33.65.352, .391",false,true,268159,Photographs,Panorama,Wheat–Field in Which General Reynolds Was Shot,,,,,,Former Attribution,Formerly attributed to,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,July 1863,1863,1863,Albumen silver prints from glass negatives,Panorama: 15.7 × 40.9 cm (6 3/16 × 16 1/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.306,false,true,268108,Photographs,Photograph,Washington. Armory Square Hospital,,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|Unknown,"American, born Ireland, 1823?–1896 New York|American",,"Brady, Mathew B.|Unknown",American,1823,1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.307,false,true,268109,Photographs,Photograph,"Armory Square Hospital, Washington",,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|Unknown,"American, born Ireland, 1823?–1896 New York|American",,"Brady, Mathew B.|Unknown",American,1823,1896,1863–65,1863,1865,Albumen silver print from glass negative,Image: 17.3 × 20 cm (6 13/16 × 7 7/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.102,false,true,267883,Photographs,Photograph,"Pontoon Bridge, Broadway Landing, Appomattox River",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.104,false,true,267885,Photographs,Photograph,"Broadway Landing, Appomattox River",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.110,false,true,267892,Photographs,Photograph,"Broadway Landing, Appomattox River",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.136,false,true,267920,Photographs,Photograph,"Fort Brady, Virginia, near Dutch Gap",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.137,false,true,267921,Photographs,Photograph,"Near Dutch Gap, Virginia. Fort Brady",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,15.8 x 20.6 cm (6 1/4 x 8 1/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.138,false,true,267922,Photographs,Photograph,Distant View of Fort Brady,,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.143,false,true,267928,Photographs,Photograph,Fort Darling. Masked Battery and Obstruction in James River,,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.166,false,true,267953,Photographs,Photograph,"Fort Brady, Building Quarters",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.218,false,true,268011,Photographs,Photograph,"Fort Darling, James River",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,15.1 x 21.4 cm (5 15/16 x 8 7/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.263,false,true,268060,Photographs,Photograph,Obstructions in James River near Drewry's Bluff,,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,ca. 1865,1863,1867,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.270,false,true,268068,Photographs,Photograph,"Broadway Landing, Appomattox River",,,,,,Artist|Former Attribution,Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.480,false,true,283897,Photographs,Photograph,"Fort Darling, James River",,,,,,Artist|Former Attribution,Attributed to|Formerly attributed to,William Frank Browne|Mathew B. Brady,"American|American, born Ireland, 1823?–1896 New York",,"Browne, William Frank|Brady, Mathew B.",American,1823,1867 |1896,1865 (?),1865,1865,Albumen silver print from glass negative,16.4 x 20.9 cm (6 7/16 x 8 1/4 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.5,false,true,268241,Photographs,Photograph,Lewis House. Battlefield of Bull Run,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,1861–62,1861,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.51,false,true,268243,Photographs,Photograph,"St. Michael's Church, Charleston, S.C.",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.53,false,true,268245,Photographs,Photograph,"Hibernian Hall, Charleston",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.106,false,true,267887,Photographs,Photograph,"Ruins of Mrs. Henry's House, Battlefield of Bull Run",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,March 1862,1862,1862,Albumen silver print from glass negative,Image: 15.9 × 20.9 cm (6 1/4 × 8 1/4 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.180,false,true,267969,Photographs,Photograph,Bull Run. Matthews House,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,1861–62,1861,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.207,false,true,267999,Photographs,Photograph,"Bull Run, Mrs. Henry's House, 21 July 1861",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,1861,1861,1861,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.284,false,true,268083,Photographs,Photograph,"Commissary Headquarters, Rocky Face Ridge",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,12.7 x 20.1 cm (5 x 7 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.295,false,true,268095,Photographs,Photograph,Rebel Fortifications in front of Atlanta,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.366,false,true,268174,Photographs,Photograph,View on Tennessee River,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.389,false,true,268199,Photographs,Photograph,Tennessee River at Bridgeport,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.397,false,true,268208,Photographs,Photograph,"U.S. Transport in Rapids, Tennessee River/The Suck - Tennessee River below Chattanooga, looking down stream",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.399,false,true,268210,Photographs,Photograph,"Chattanooga, Tennessee",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.400,false,true,268213,Photographs,Photograph,Camp near Chattanooga,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.401,false,true,268214,Photographs,Photograph,Bridge Builders Camp opposite Chattanooga,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.402,false,true,268215,Photographs,Photograph,"Crutchfield House, Chattanooga, Tennessee",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.403,false,true,268216,Photographs,Photograph,"Chattanooga, Tennessee. Government Stable",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.404,false,true,268217,Photographs,Photograph,"Chattanooga, Tennessee",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.405,false,true,268218,Photographs,Photograph,"Chief Commissary's Office, Chattanooga",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.406,false,true,268219,Photographs,Photograph,Bridge Across Tennessee River at Chattanooga,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,8.5 x 8.9 cm (3 3/8 x 3 1/2 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.407,false,true,268220,Photographs,Photograph,View on Tennessee River looking toward Chattanooga,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,7.9 x 9.1 cm (3 1/8 x 3 9/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.408,false,true,268221,Photographs,Photograph,Bridge over Tennessee River at Chattanooga,,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.409,false,true,268222,Photographs,Photograph,Bridge Across Tennessee River at Chattanooga,,,,,,Former Attribution|Artist,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,13.4 x 20.5 cm (5 1/4 x 8 1/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.410,false,true,268224,Photographs,Photograph,"Chattanooga, Tennessee",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.411,false,true,268225,Photographs,Photograph,Tennessee River at Chattanooga (81 Lookout Mountain Spur),,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.412,false,true,268226,Photographs,Photograph,"Provost Marshals Headquarters, Chattanooga",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.413,false,true,268227,Photographs,Photograph,"Headquarters of General Sherman or Thomas, Chattanooga",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.415,false,true,268229,Photographs,Photograph,"Lookout Mountain, Tennessee",,,,,,Artist|Former Attribution,Formerly attributed to,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American,1819 |1823,1902 |1896,ca. 1864,1862,1866,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.148,false,true,267933,Photographs,Photograph,"Fort Mahone, Petersburg, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Thomas C. Roche|Mathew B. Brady,"American, 1826–1895|American, born Ireland, 1823?–1896 New York",,"Roche, Thomas C.|Brady, Mathew B.",American,1826 |1823,1895 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.334,false,true,268139,Photographs,Photograph,"Dead Confederates, Fort Mahone",,,,,,Artist|Former Attribution,Formerly attributed to,Thomas C. Roche|Mathew B. Brady,"American, 1826–1895|American, born Ireland, 1823?–1896 New York",,"Roche, Thomas C.|Brady, Mathew B.",American,1826 |1823,1895 |1896,1864,1864,1864,Albumen silver print from glass negative,9.9 x 9.4 cm (3 7/8 x 3 11/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.340,false,true,268146,Photographs,Photograph,"Dead Confederates, Fort Mahone",,,,,,Artist|Former Attribution,Formerly attributed to,Thomas C. Roche|Mathew B. Brady,"American, 1826–1895|American, born Ireland, 1823?–1896 New York",,"Roche, Thomas C.|Brady, Mathew B.",American,1826 |1823,1895 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.7,false,true,268263,Photographs,Photograph,"City Point, Virginia. Terminus of U.S. Military Railroad",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,16.7 x 21.8 cm (6 9/16 x 8 9/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.8,false,true,268274,Photographs,Photograph,"Burial of the Dead, Fredericksburg",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.14,false,true,267924,Photographs,Photograph,"Pontoon Bridge at Deep Bottom, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.15,false,true,267935,Photographs,Photograph,"City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.17,false,true,267957,Photographs,Photograph,"End of the Bridge after Burnside's Attack, Fredericksburg, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1863,1863,1863,Albumen silver print from glass negative,Image: 13.1 × 20.6 cm (5 3/16 × 8 1/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.18,false,true,267968,Photographs,Photograph,"Pontoon Bridge at Deep Bottom, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.21,false,true,268002,Photographs,Photograph,"Abandoned Camp, Falmouth, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1862,1862,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.34,false,true,268145,Photographs,Photograph,"Headquarters, 10th Army Corps, Hatcher's Farm, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.45,false,true,268236,Photographs,Photograph,"Pontoon Bridge at Deep Bottom, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.46,false,true,268237,Photographs,Photograph,"Pontoon Bridge at Deep Bottom, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.49,false,true,268240,Photographs,Photograph,"Military Railroad Camp, City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.58,false,true,268250,Photographs,Photograph,Entrenchments on left of Bermuda Hundred Lines,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268250,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.59,false,true,268251,Photographs,Photograph,"Port Royal, Rappahannock River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.60,false,true,268253,Photographs,Photograph,"Port Royal, Rappahannock River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.61,false,true,268254,Photographs,Photograph,"Wagon Train at Port Royal, Rappahannock River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.62,false,true,268255,Photographs,Photograph,Crow's Nest Signal Tower near Bermuda Hundred,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,20.3 x 12.9 cm (8 x 5 1/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.63,false,true,268256,Photographs,Photograph,Extreme Left of Bermuda Hundred Lines,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,16.4 x 21.1 cm (6 7/16 x 8 5/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.70,false,true,268264,Photographs,Photograph,"Pontoon Bridge, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,12.7 x 19.7 cm (5 x 7 3/4 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.71,false,true,268265,Photographs,Photograph,Wilcox Landing,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.72,false,true,268266,Photographs,Photograph,"Confederate Prisoners for Exchange, Cox's Landing, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.73,false,true,268267,Photographs,Photograph,"Pontoon Bridge Train, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.75,false,true,268269,Photographs,Photograph,"Pontoon Bridge, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.76,false,true,268270,Photographs,Photograph,"Bermuda Hundred Landing, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,14.6 x 16.5 cm (5 3/4 x 6 1/2 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.90,false,true,268286,Photographs,Photograph,Street in Fredericksburg,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.92,false,true,268288,Photographs,Photograph,"Headquarters of Capt. E.E. Camp, A.Q.M., at City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.93,false,true,268289,Photographs,Photograph,"City Point, Virginia. James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,12.2 x 22.7 cm (4 13/16 x 8 15/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.94,false,true,268290,Photographs,Photograph,Bridge. U.S. Military Railroad at City Point,,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.95,false,true,268291,Photographs,Photograph,"City Point, Virginia",,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.96,false,true,268292,Photographs,Photograph,"Fredericksburg, Virginia",,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.98,false,true,268294,Photographs,Photograph,"Pontoon Bridge at Deep Bottom, James River",,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.99,false,true,268295,Photographs,Photograph,James River,,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,9 x 20 cm (3 9/16 x 7 7/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.100,false,true,267881,Photographs,Photograph,"City Point, Virginia",,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.101,false,true,267882,Photographs,Photograph,"Terminus of U.S. Military Railroad, City Point, Virginia",,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.103,false,true,267884,Photographs,Photograph,"Excavating for ""Y"" at Devereaux Station, Orange & Alexandria Railroad",,,,,,Artist|Former Attribution,,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.112,false,true,267894,Photographs,Photograph,"City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.113,false,true,267895,Photographs,Photograph,"Headquarters of Capt. H.B. Blood, A.Q.M., at City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,12.8 x 18.9 cm (5 1/16 x 7 7/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.114,false,true,267896,Photographs,Photograph,"[Wharves on the James River, City Point]. Brady album, p. 10",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,13.8 x 20.9 cm (5 7/16 x 8 1/4 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.115,false,true,267897,Photographs,Photograph,"Commissary Department, City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,16 x 22.9 cm (6 5/16 x 9 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.118,false,true,267900,Photographs,Panorama,"Looking Towards Marye's Heights, Fredericksburg",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.119,false,true,267901,Photographs,Photograph,"Burial of the Dead, Fredericksburg",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1863,1863,1863,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.120,false,true,267903,Photographs,Photograph,"Battery Going into Action, Fredericksburg, December 13, 1862",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1862,1862,1862,Albumen silver print from glass negative,14.7 x 19.9 cm (5 13/16 x 7 13/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.170,false,true,267958,Photographs,Photograph,Pontoon Bridge,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,13.3 x 20.4 cm (5 1/4 x 8 1/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.175,false,true,267963,Photographs,Photograph,"Confederate Method of Destroying Rail Roads at McCloud Mill, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1863,1863,1863,Albumen silver print from glass negative,Image: 16.2 × 19.6 cm (6 3/8 × 7 11/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.183,false,true,267972,Photographs,Photograph,"General Butler's Headquarters, Chapin's Farm, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.210,false,true,268003,Photographs,Photograph,Confederate Trestle Work on Alexandria Railroad,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,15.2 x 21.1 cm (6 x 8 5/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.262,false,true,268059,Photographs,Photograph,"Deep Bottom, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.264,false,true,268061,Photographs,Photograph,"Unloading Supplies at Port Royal, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.269,false,true,268066,Photographs,Photograph,"Army Wagon and Forge, City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.271,false,true,268069,Photographs,Photograph,"[Orange and Alexandria Railroad Bridge, near Union Mills, Virginia]",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,ca. 1863,1860,1865,Albumen silver print from glass negative,Image: 12.3 x 20.1 cm (4 13/16 x 7 15/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.275,false,true,268073,Photographs,Photograph,"Removing Dead from Battlefield, Marye's Heights, May 2, 1864",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.278,false,true,268076,Photographs,Photograph,"Bridge on Orange and Alexandria Rail Road, as Repaired by Army Engineers under Colonel Herman Haupt",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.279,false,true,268077,Photographs,Photograph,"Locomotive #56, U.S. Military Railroad/City Point. Troops Ready to be Taken to the Front by Rail",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.280,false,true,268079,Photographs,Photograph,"Locomotive #133, U.S.M.R.R.",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.289,false,true,268088,Photographs,Photograph,Government Saw Mill (Chattanooga),,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.291,false,true,268091,Photographs,Photograph,"Fort Beauregard, Manassas, VA",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.308,false,true,268110,Photographs,Photograph,"Camp of Construction Corps, U.S. Military Railroad at City Point",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,14.4 x 20.8 cm (5 11/16 x 8 3/16 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.316,false,true,268119,Photographs,Photograph,"Lower Pontoon Bridge, Deep Bottom, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.325,false,true,268129,Photographs,Photograph,"Cox's Landing, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.337,false,true,268142,Photographs,Photograph,"Fort Brady, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.339,false,true,268144,Photographs,Photograph,"Artillery Camp, City Point, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,9.6 x 9.9 cm (3 3/4 x 3 7/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.341,false,true,268147,Photographs,Photograph,Extreme Left of Bermuda Hundred Lines,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.343,false,true,268149,Photographs,Photograph,Entrenchments on left of Bermuda Hundred Lines,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.353,false,true,268160,Photographs,Photograph,"Camp of Construction Corps, U.S. Military Railroad, at City Point",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.358,false,true,268165,Photographs,Photograph,"Fort Brady, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.359,false,true,268166,Photographs,Photograph,"Fort Brady, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.361,false,true,268169,Photographs,Photograph,"Fort Brady, James River",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.362,false,true,268170,Photographs,Photograph,"Camp of Laborers, City Point",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,10.4 x 9.5 cm (4 1/8 x 3 3/4 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.381,false,true,268191,Photographs,Photograph,"Camp of Construction Corps, U.S. Military Railroad at City Point",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.383,false,true,268193,Photographs,Photograph,"Manassas, Virginia",,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.387,false,true,268197,Photographs,Photograph,Fortifications at Manassas,,,,,,Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,5.6 x 8.2 cm (2 3/16 x 3 1/4 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.432,false,true,283849,Photographs,Photograph,Bridge. U.S. Military Railroad at City Point,,,,,,Artist|Former Attribution,Attributed to|Formerly attributed to,Andrew Joseph Russell|Mathew B. Brady,"American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Brady, Mathew B.",American,1830 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,16.4 x 21.3 cm (6 7/16 x 8 3/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.142.3,false,true,271897,Photographs,Photograph,Two Pupils in Greek Dress,,,,,,Artist|Printer,,Thomas Eakins|Susan Macdowell Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania|1851–1938",,"Eakins, Thomas|Eakins, Susan Macdowell",American,1844 |1851,1916 |1938,1880s,1880,1889,Platinum print,22.4 x 16.7 cm (8 13/16 x 6 9/16 in.),"Gift of Charles Bregler, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.1067,false,true,266252,Photographs,Photographically illustrated book,Ichnographs from the Sandstone of Connecticut River,,,,,,Artist,,James Deane,"American, 1801–1858",,"Deane, James",American,1801,1858,published 1861,1850,1869,Salted paper prints,,"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1990",,,,,,,,,,,,Books,,http://www.metmuseum.org/art/collection/search/266252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.334,false,true,286059,Photographs,Photograph,California News,,,,,,Artist,,Gabriel Harrison,"American, 1818–1902",,"Harrison, Gabriel",American,1818,1902,ca. 1850,1848,1852,Daguerreotype,Image: 14 x 10.5 cm (5 1/2 x 4 1/8 in.) 6 × 4 13/16 in. (15.3 × 12.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/286059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (1–61),false,true,259583,Photographs,Album,Photographic Views of Sherman's Campaign,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/259583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.787,false,true,285757,Photographs,Negative; Photograph,"Ordnance Wharf, City Point, Virginia",,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",,"Roche, Thomas C.",American,1826,1895,1865,1865,1865,Collodion glass negative,Image: 21.6 × 25.6 cm (8 1/2 × 10 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Negatives,,http://www.metmuseum.org/art/collection/search/285757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.556 (1–73),false,true,286455,Photographs,Album,"[Trees in Calaveras Grove and Views of Yosemite, California]",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,ca. 1878,1876,1880,Albumen silver prints from glass negatives,Image: approximately 12.5 x 12.5 cm (4 15/16 x 4 15/16 in.) each Mount: 24 x 25.1 cm (9 7/16 x 9 7/8 in.) each Album: 24.8 x 26 x 3.2 cm (9 3/4 x 10 1/4 x 1 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/286455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1117,false,true,285972,Photographs,Photomechanical print,[Native American with a Medal of President Garfield],,,,,,Artist,,William Henry Jackson,"American, 1843–1942",,"Jackson, William Henry",American,1843,1942,1890–1910,1890,1910,Photochrom,In mat: 12 x 9 13/16,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/285972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1291,false,true,285857,Photographs,Autochrome,[Bananas],,,,,,Artist,,Frederick Dellenbaugh,"American, 1853–1935",,"Dellenbaugh, Frederick",American,1853,1935,ca. 1908,1906,1910,Autochrome,8.2 x 10.7 cm (3 1/4 x 4 1/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/285857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.561.2,false,true,269751,Photographs,Photomechanical print,Old Woman Praying,,,,,,Artist,,Rudolph Eickemeyer,"American, 1862–1932",,"Eickemeyer, Jr., Rudolph",American,1862,1932,ca. 1900,1898,1902,Photogravure,,"Gift of Mrs. William H. Schubart, 1954",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/269751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.644.5,false,true,260328,Photographs,Photomechanical print,The Bridal Rose,,,,,,Artist,,Rudolph Eickemeyer,"American, 1862–1932",,"Eickemeyer, Jr., Rudolph",American,1862,1932,1900,1900,1900,Photogravure,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.275,false,true,265327,Photographs,Photomechanical print,[Prison Work Crew (ca. 9 Members) Digging Trench and 1 Guard],,,,,,Artist,,Doris Ulmann,"American, 1882–1934",,"Ulmann, Doris",American,1882,1934,"1929–30, printed 1934",1929,1930,Photogravure from glass negative,20.3 x 28.7 cm (8 x 11 5/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/265327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.635.11,false,true,271736,Photographs,Autochrome,"Emmy and Kitty - Tutzing, Bavaria",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1907,1907,1907,Autochrome,18.1 x 13.1 cm,"Alfred Stieglitz Collection, 1955",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/271736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.635.12,false,true,271737,Photographs,Autochrome,Stieglitz and Emmy,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1907,1907,1907,Autochrome,13.2 x 18 cm,"Alfred Stieglitz Collection, 1955",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/271737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.60,false,true,260271,Photographs,Photomechanical print,Baroness von P. --- Kimono,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Photogravure,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.62,false,true,260273,Photographs,Photomechanical print,The Summer Song,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,ca. 1908,1906,1910,Halftone,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.119,false,true,260126,Photographs,Photomechanical print,H.R.H. Princess Rupprecht with Prince Leopold and Albrecht Johann,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,"1900–1908, printed 1910",1900,1908,Photogravure,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.136,false,true,260144,Photographs,Photomechanical print,[Park with Fence],,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1886,1886,1886,Photogravure,16.3 x 11.9 cm (6 7/16 x 4 11/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.139,false,true,260147,Photographs,Photomechanical print,The Man in Armor,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,"1898, printed 1910",1898,1898,Photogravure,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.147,false,true,260156,Photographs,Photomechanical print,The Archer II,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Photogravure,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.152,false,true,260162,Photographs,Photomechanical print,Keep a Movin'! For Monkey * Fan * Tiger or Dove: Dough or Fame: Same Old Game as Love,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Photogravure,12.2 x 17.9 cm (4 13/16 x 7 1/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.153,false,true,260163,Photographs,Photomechanical print,Nude Man with Harp,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1908–10,1908,1910,Photogravure,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/260163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.635.14,false,true,269800,Photographs,Autochrome,Mrs. Selma Schubart,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1907,1907,1907,Autochrome,Plate: 17.8 × 12.8 cm (7 × 5 1/16 in.) Image: 16.5 × 11.6 cm (6 1/2 × 4 9/16 in.),"Alfred Stieglitz Collection, 1955",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/269800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.635.16,false,true,269802,Photographs,Autochrome,Walkowitz at Lake George,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1916,1916,1916,Autochrome,12.7 x 17.8 cm (5 x 7 in.),"Alfred Stieglitz Collection, 1955",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/269802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.635.17,false,true,269803,Photographs,Autochrome,[Man in Red Sweater],,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1907,1907,1907,Autochrome,17.9 x 12.8 cm (7 1/16 x 5 1/16 in.),"Alfred Stieglitz Collection, 1955",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/269803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.635.18,false,true,269804,Photographs,Autochrome,Frank Eugene,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1907,1907,1907,Autochrome,18 x 13 cm (7 1/16 x 5 1/8 in.),"Alfred Stieglitz Collection, 1955",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/269804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.476,false,true,285938,Photographs,Autochrome,[Two Men Playing Chess],,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,June 1907,1907,1907,Autochrome,9 x 12 cm (3.5 x 4.7 in.),"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/285938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.478,false,true,286176,Photographs,Autochrome,Portrait of Kitty,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,ca. 1911,1909,1913,Autochrome,17.8 x 12.7 cm (7x 5 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/286176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.19,false,true,269310,Photographs,Photomechanical print,Two Towers - New York,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1911, printed in or before 1913",1911,1911,Photogravure,32.7 x 25.3 cm. (12 7/8 x 9 15/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/269310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.1,false,true,270030,Photographs,Photomechanical print,Two Towers - New York,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1911, printed 1913",1911,1911,Photogravure,19.5 x 15.9 cm. (7 11/16 x 6 1/4 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.2,false,true,270041,Photographs,Photomechanical print,Old and New New York,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1910, printed 1911",1910,1910,Photogravure,20.2 x 15.8 cm. (7 15/16 x 6 1/4 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.419,false,true,267836,Photographs,Photomechanical print,The Steerage,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1907, printed in or before 1913",1907,1907,Photogravure,32.2 x 25.8 cm (12 11/16 x 10 3/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/267836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.18,false,true,270039,Photographs,Photomechanical print,Snapshot - In the New York Central Yards,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1903, printed 1907",1903,1903,Photogravure,19.4 x 15.9 cm (7 5/8 x 6 1/4 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.20,false,true,270042,Photographs,Photomechanical print,Winter - Fifth Avenue,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1893, printed 1905",1893,1893,Photogravure,21.8 x 15.4 cm. (8 9/16 x 6 1/16 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.21,false,true,270043,Photographs,Photomechanical print,Nearing Land,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1904, printed 1905",1904,1904,Photogravure,21.6 x 17.4 cm. (8 1/2 x 6 7/8 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.28,false,true,270050,Photographs,Photomechanical print,The City across the River,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1910, printed 1911",1910,1910,Photogravure,20.0 x 16.0 cm. (7 7/8 x 6 5/16 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.30,false,true,270053,Photographs,Photomechanical print,Lower Manhattan,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1910, printed 1911",1910,1910,Photogravure,16.0 x 19.8 cm. (6 5/16 x 7 13/16 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.31,false,true,270054,Photographs,Photomechanical print,The Aeroplane,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1910, printed 1911",1910,1910,Photogravure,14.5 x 17.5 cm (5 11/16 x 6 7/8 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.34,false,true,270057,Photographs,Photomechanical print,The Swimming Lesson,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1906, printed 1911",1906,1906,Photogravure,14.8 x 23.0 cm. (5 13/16 x 9 1/16 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.36,false,true,270059,Photographs,Photomechanical print,"A Snapshot, Paris",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1911, printed 1912",1911,1911,Photogravure,13.8 x 17.4 cm. (5 7/16 x 6 7/8 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.116,false,true,265151,Photographs,Photograph,[View of Rooftops],,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1917,1917,1917,Gelatin silver print,23.7 x 19.0 cm (9 5/16 x 7 1/2 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/265151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.83,false,true,283183,Photographs,Photograph,[The Great Man Has Fallen],,,,,,Artist,,Robert H. Vance,"American, died 1876",,"Vance, Robert H.",American,1876,1876,1856,1856,1856,Daguerreotype,Image: 13.5 x 18.8 cm (5 5/16 x 7 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.207,false,true,286346,Photographs,Photograph,[View in New Hampshire],,,,,,Artist,,Samuel Bemis,"American, 1789–1881",,"Bemis, Samuel",American,1789,1881,1840–41,1840,1841,Daguerreotype,Image: 5 3/4 × 7 7/8 in. (14.6 × 20 cm) Frame: 9 5/16 × 11 7/16 in. (23.6 × 29 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1135,false,true,285732,Photographs,Photograph,[Gentleman],,,,,,Artist,,Henry Fitz Jr.,"American, 1808–1863",,"Fitz Jr., Henry",American,1808,1863,ca. 1840,1838,1842,Daguerreotype,Approx. ninth-plate,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.50,false,true,291803,Photographs,Daguerreotype,"[Man Wearing Coat and Gloves, Holding Hat, Seated in Front of Painted Outdoor Backdrop]",,,,,,Artist,Attributed to,Samuel Broadbent Jr.,"American, 1810–1880",,"Broadbent, Samuel, Jr.",American,1810,1880,1840s,1840,1849,Daguerreotype,Image: 12 x 9 cm (4 3/4 x 3 9/16 in.) Plate: 14 x 10.8 cm (5 1/2 x 4 1/4 in.) Case: 2.2 x 15.2 x 12.4 cm (7/8 x 6 x 4 7/8 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.52,false,true,291805,Photographs,Daguerreotype,"Josiah Bunting, 85, with George M. Bunting, 17 Months",,,,,,Artist,,Samuel Broadbent Jr.,"American, 1810–1880",,"Broadbent, Samuel, Jr.",American,1810,1880,1850s,1850,1859,Daguerreotype,Image: 8.8 x 6.8 cm (3 7/16 x 2 11/16 in.) Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.) Case: 2.2 x 11.7 x 9.2 cm (7/8 x 4 5/8 x 3 5/8 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.869,false,true,286550,Photographs,Photograph,"[Landscape, Pride's Crossing]",,,,,,Artist,,Samuel Masury,"American, 1818–1874",,"Masury, Samuel",American,1818,1874,ca. 1856,1854,1858,Salted paper print from paper negative,10 1/8 x 13 1/2,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.1056.1,false,true,262760,Photographs,Stereograph,Broadway on a Rainy Day,,,,,,Artist,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,1859,1859,1859,Albumen silver print,8.3 x 15.7 cm (3 1/4 x 6 3/16 in.),"Warner Communications Inc. Purchase Fund, 1980",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.1056.4,false,true,262763,Photographs,Stereograph,[Broadway with horse-drawn carriages],,,,,,Artist,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,ca. 1860s,1858,1862,Albumen silver print,,"Warner Communications Inc. Purchase Fund, 1980",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.457.3626,false,true,291959,Photographs,Photograph,Return of the Japanese Embassy from City Hall,,,,,,Artist,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,1860,1860,1860,Albumen silver print from glass negative,Image: 7.5 x 15 cm (2 15/16 x 5 7/8 in.) Mount: 8.2 x 17.1 cm (3 1/4 x 6 3/4 in.),"Herbert Mitchell Collection, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.457.3682,false,true,291961,Photographs,Photograph,"The Embassy Leave the Metropolitan for the City Hall, the Seventh Regiment Form a Hollow Square With the Carriages of the Embassy in the Middle",,,,,,Artist,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,1860,1860,1860,Albumen silver print from glass negative,Image: 7.5 x 14.2 cm (2 15/16 x 5 9/16 in.) Mount: 8.2 x 17.4 cm (3 1/4 x 6 7/8 in.),"Herbert Mitchell Collection, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.457.3683,false,true,291962,Photographs,Photograph,The Populace Begin to Gather in Front of the City Hall to Witness the Arrival of the Embassy on Their Visit to the Governor and Mayor,,,,,,Artist,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,1860,1860,1860,Albumen silver print from glass negative,Image: 7.6 x 14.2 cm (3 x 5 9/16 in.) Mount: 8.2 x 17.3 cm (3 1/4 x 6 13/16 in.),"Herbert Mitchell Collection, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.19,false,true,301887,Photographs,Photograph,"Savannah, Georgia, No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1866,1866,1866,Albumen silver print from glass negative,Image: 25.5 x 36.1 cm (10 1/16 x 14 3/16 in.) Mount: 40.9 x 51.1 cm (16 1/8 x 20 1/8 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.611.484,false,true,269550,Photographs,Photograph,"Rebel Works in Front of Atlanta, Georgia, No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1864,1864,1864,Albumen silver print,,"Gift of Mrs. Robert Ingersoll Aitken, 1951",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (1),false,true,294445,Photographs,Photograph,Sherman and His Generals,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (2),false,true,294446,Photographs,Photograph,"The Capitol, Nashville, Tennessee",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (3),false,true,294447,Photographs,Photograph,Nashville from the Capitol,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (4),false,true,294448,Photographs,Photograph,Trestle Bridge at Whiteside,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (5),false,true,294449,Photographs,Photograph,Whiteside Valley Below the Bridge,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (6),false,true,294450,Photographs,Photograph,"Pass in the Racoon Range, Whiteside No. 1",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (7),false,true,294451,Photographs,Photograph,"Pass in the Raccoon Range, Whiteside No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (8),false,true,294452,Photographs,Photograph,Chattanooga from the North,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (9),false,true,294453,Photographs,Photograph,Mission Ridge from Orchard Knob,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.281,false,true,286658,Photographs,Photograph,"Bonaventure Cemetery, Four Miles from Savannah",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1866,1866,1866,Albumen silver print from glass negative,Image: 34 x 26.4 cm (13 3/8 x 10 3/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (10),false,true,294454,Photographs,Photograph,Orchard Knob from Mission Ridge,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (11),false,true,294455,Photographs,Photograph,The Crest of Mission Ridge,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (12),false,true,294456,Photographs,Photograph,Mission Ridge Scene of Sherman's Attack,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (13),false,true,294457,Photographs,Photograph,Chattanooga Valley from Lookout Mountain,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (14),false,true,294458,Photographs,Photograph,Chattanooga Valley from Lookout Mountain No. 2,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (15),false,true,294459,Photographs,Photograph,"Lu-La Lake, Lookout Mountain",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1864 or 1866,1864,1866,Albumen silver print from glass negative,Image: 25.6 × 35.9 cm (10 1/16 × 14 1/8 in.),"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (16),false,true,294460,Photographs,Photograph,"The John Ross House, Ringold, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (17),false,true,294461,Photographs,Photograph,"Ringold, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (18),false,true,294462,Photographs,Photograph,"Buzzard Roost, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (19),false,true,294463,Photographs,Photograph,"Battle Ground of Resacca, Georgia No. 1",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (20),false,true,294464,Photographs,Photograph,"Battle Ground of Resacca, Georgia No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (21),false,true,294465,Photographs,Photograph,"Battle Ground of Resacca, Georgia No. 3",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (22),false,true,294466,Photographs,Photograph,"Battle Ground of Resacca, Georgia No. 4",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (23),false,true,294467,Photographs,Photograph,Defences of the Etawah Bridge,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (24),false,true,294468,Photographs,Photograph,Allatoona from the Etawah,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (25),false,true,294469,Photographs,Photograph,"Battle Field of New Hope Church, Georgia No. 1",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (26),false,true,294470,Photographs,Photograph,"Battle Field of New Hope Church, Georgia No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (27),false,true,294471,Photographs,Photograph,"The ""Hell Hole"" New Hope Church, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (28),false,true,294472,Photographs,Photograph,"The Allatoona Pass, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (29),false,true,294473,Photographs,Photograph,"The Allatoona Pass Looking North, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (30),false,true,294474,Photographs,Photograph,Pine Mountain,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (31),false,true,294475,Photographs,Photograph,"The Front of Kenesaw Mountain, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (32),false,true,294476,Photographs,Photograph,"View of Kenesaw Mountain, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (33),false,true,294477,Photographs,Photograph,"South Bank of the Chattahoochie, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (34),false,true,294478,Photographs,Photograph,"The Battle Field of Peach Tree Creek, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (35),false,true,294479,Photographs,Photograph,Scene of General McPherson's Death,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1864 or 1866,1864,1866,Albumen silver print from glass negative,Image: 25.4 × 36.1 cm (10 × 14 3/16 in.),"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (36),false,true,294480,Photographs,Photograph,"Battle Field of Atlanta, Georgia, July 22nd 1864 No. 1",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (37),false,true,294481,Photographs,Photograph,"Battle Field of Atlanta, Georgia, July 22nd 1864 No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (38),false,true,294482,Photographs,Photograph,"The Potter House, Atlanta",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (39),false,true,294483,Photographs,Photograph,"Rebel Works in Front of Atlanta, Georgia No. 1",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (40),false,true,294484,Photographs,Photograph,"Rebel Works in Front of Atlanta, Georgia No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (41),false,true,294485,Photographs,Photograph,"Rebel Works in Front of Atlanta, Georgia No. 3",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (42),false,true,294486,Photographs,Photograph,"Rebel Works in Front of Atlanta, Georgia No. 4",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (43),false,true,294487,Photographs,Photograph,"Rebel Works in Front of Atlanta, Georgia No. 5",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (44),false,true,294488,Photographs,Photograph,Destruction of Hood's Ordinance Train,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (45),false,true,294489,Photographs,Photograph,"City of Atlanta, Georgia No. 1",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (46),false,true,294490,Photographs,Photograph,"City of Atlanta, Georgia No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1866,1866,1866,Albumen silver print from glass negative,Image: 25.6 × 35.5 cm (10 1/16 × 14 in.),"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (47),false,true,294491,Photographs,Photograph,"Savanah River, Near Savanah, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (48),false,true,294492,Photographs,Photograph,"Buen-Ventura, Savanah, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (49),false,true,294493,Photographs,Photograph,"Savanah, Georgia No. 1",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (50),false,true,294494,Photographs,Photograph,"Savanah, Georgia No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (51),false,true,294495,Photographs,Photograph,"Fountain, Savanah, Georgia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (52),false,true,294496,Photographs,Photograph,"The New Capitol, Columbia, South Carolina",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (53),false,true,294497,Photographs,Photograph,Columbia from the Capitol,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (54),false,true,294498,Photographs,Photograph,"Ruins in Columbia, South Carolina",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (55),false,true,294499,Photographs,Photograph,"Ruins in Columbia, South Carolina No. 2",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (56),false,true,294500,Photographs,Photograph,Fort Sumpter,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (57),false,true,294501,Photographs,Photograph,Interior View of Fort Sumpter,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (58),false,true,294502,Photographs,Photograph,Exterior View of Fort Sumpter,,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (59),false,true,294503,Photographs,Photograph,"Ruins of the Pinckney Mansion, Charleston, South Carolina",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (60),false,true,294504,Photographs,Photograph,"Ruins in Charleston, South Carolina",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.525 (61),false,true,294505,Photographs,Photograph,"Ruins of the R.R. Depot, Charleston, South Carolina",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,1860s,1860,1869,Albumen silver print from glass negative,,"Pfeiffer and Rogers Funds, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1232,false,true,285727,Photographs,Photograph,"Quaker Gun, Centreville, Virginia",,,,,,Artist,,George N. Barnard,"American, 1819–1902",,"Barnard, George N.",American,1819,1902,March 1862,1862,1862,Albumen silver print from glass negative,Image: 8.1 × 9.4 cm (3 3/16 × 3 11/16 in.) Mount: 10.7 × 12.6 cm (4 3/16 × 4 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.52,false,true,282057,Photographs,Photograph,Niagara Falls,,,,,,Artist,Possibly by,Silas A. Holmes,"American, 1820–1886",,"Holmes, Silas S.",American,1820,1886,ca. 1855,1853,1857,Salted paper print from glass negative,30.5 x 40.4 cm (12 x 15 7/8 in. ) irregular,"The Rubel Collection, Purchase, Lila Acheson Wallace Gift, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.556.3,false,true,301973,Photographs,Photograph,[Rev. Mr. Frederick T. Gray and Deacons of Old Bullfinch Street Church],,,,,,Artist,Attributed to,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,ca. 1845,1840,1850,Daguerreotype,Plate: 10.8 x 14 cm (4 1/4 x 5 1/2 in.),"Gift of Isaac Lagnado, in honor of Elliott Cohen, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.41,true,true,282046,Photographs,Photograph,[Cornelius Conway Felton with His Hat and Coat],,,,,,Artist,,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,early 1850s,1850,1853,Daguerreotype,visible: 8.3 x 7 cm (3 1/4 x 2 3/4 in.) each,"The Rubel Collection, Purchase, Lila Acheson Wallace, W. Bruce and Delaney H. Lundberg, and Ann Tenenbaum and Thomas H. Lee Gifts, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.76,false,true,283175,Photographs,Photograph,[Hypnotism],,,,,,Artist,,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,ca. 1845,1843,1847,Daguerreotype,Image: 13.3 x 18.4 cm (5 1/4 x 7 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.14,false,true,290482,Photographs,Photograph,National Congregational Council at Plymouth Rock,,,,,,Artist,,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,"June 22, 1865",1865,1865,Albumen silver print from glass negative,Image: 37.5 x 47.6 cm (14 3/4 x 18 3/4 in.) Mount: 45.7 x 55.9 cm (18 x 22 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.43,false,true,291796,Photographs,Daguerreotype,[Double Plate: Two Men with Sideburns],,,,,,Artist,Possibly by,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,1850s,1850,1859,Daguerreotype,"Image: 6.5 x 5.3 cm (2 9/16 x 2 1/16 in.), each Plate: 8.3 x 7 cm (3 1/4 x 2 3/4 in.), each Case: 1.9 x 9.5 x 8.3 cm (3/4 x 3 3/4 x 3 1/4 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.856,false,true,286231,Photographs,Photograph,[Two Elderly Men Conversing],,,,,,Artist,,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,ca. 1850,1848,1852,Daguerreotype,oberall: 9 1/16 × 6 15/16 in. (23 × 17.7 cm) Image: 7 1/4 × 5 1/4 in. (18.4 × 13.4 cm); visible,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286231,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.857,false,true,286237,Photographs,Photograph,[Self-Portrait with Artist's Brother],,,,,,Artist,,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,1840s,1840,1849,Daguerreotype,Image: 1 7/8 × 1 7/16 in. (4.7 × 3.7 cm); visible,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.858,false,true,286230,Photographs,Photograph,[Self-Portrait with Wife and Two Daughters],,,,,,Artist,,John Adams Whipple,"American, 1822–1891",,"Whipple, John Adams",American,1822,1891,1854,1854,1854,Daguerreotype,Overall: 6 × 4 3/4 in. (15.2 × 12 cm) Image: 4 13/16 × 3 9/16 in. (12.3 × 9 cm); visible,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.56,false,true,301998,Photographs,Photograph,General George McClellan,,,,,,Artist,,Charles DeForest Fredricks,"American, 1823–1894",,"Fredricks, Charles DeForest",American,1823,1894,1862 (?),1862,1862,Albumen silver print from glass negative,Image: 9.1 x 5.4 cm (3 9/16 x 2 1/8 in.) Mount: 10.2 x 6.1 cm (4 x 2 3/8 in.),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.210,false,true,647196,Photographs,Photograph,[Clown],,,,,,Artist,,Charles DeForest Fredricks,"American, 1823–1894",,"Fredricks, Charles DeForest",American,1823,1894,ca. 1860,1855,1865,Albumen silver print,Image: 8 1/16 × 6 13/16 in. (20.5 × 17.3 cm) Mount: 13 5/16 × 10 1/2 in. (33.8 × 26.7 cm),"Gift of Howard Greenberg, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/647196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.49,false,true,301963,Photographs,Carte-de-visite,"[Wounded Soldiers on Cots, possibly at Harewood Hospital]",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,Image: 9.4 x 5.7 cm (3 11/16 x 2 1/4 in.) Mount: 10.2 x 6.1 cm (4 x 2 3/8 in.),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5127,false,true,266764,Photographs,Photograph,Judson C. Albright,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5128,false,true,266765,Photographs,Photograph,Frederick A. Bentley,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5129,false,true,266766,Photographs,Photograph,John A. Dixon,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5130,false,true,266767,Photographs,Photograph,Frederick Hohmann,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5131,false,true,266768,Photographs,Photograph,"Private John Parkhurst, Company E, Second New York Heavy Artillery",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,"Image: 18.9 × 13.1 cm (7 7/16 × 5 3/16 in.), oval","Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5132,false,true,266769,Photographs,Photograph,Stephen D. Wilbur,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5133,false,true,266770,Photographs,Photograph,Herman Rice,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5134,false,true,266771,Photographs,Photograph,"Private Samuel Shoop, Company F, 200th Pennsylvania Infantry",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,April–May 1865,1865,1865,Albumen silver print from glass negative,Image: 18.9 × 13.1 cm (7 7/16 × 5 3/16 in.),"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5135,false,true,266772,Photographs,Photograph,"Private Jacob F. Simmons, Company H, Eighty-second Pennsylvania Volunteers",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,April–May 1865,1865,1865,Albumen silver print from glass negative,"Image: 18.7 × 13 cm (7 3/8 × 5 1/8 in.), oval Mount: 29.5 × 24.1 cm (11 5/8 × 9 1/2 in.)","Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5136,false,true,266773,Photographs,Photograph,"Corporal Israel Spotts, Company G, 200th Pennsylvania Volunteers",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,April–May 1865,1865,1865,Albumen silver print from glass negative,"Image: 18.9 x 13.1 cm (7 7/16 x 5 3/16 in.), oval Mount: 29.8 x 24.3 cm (11 3/4 x 9 9/16 in.)","Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5137,false,true,266774,Photographs,Photograph,"Private James H. Stokes, Company H, 185th New York Volunteers",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,April–May 1865,1865,1865,Albumen silver print from glass negative,"Image: 18.8 × 13 cm (7 3/8 × 5 1/8 in.), oval","Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5138,false,true,266775,Photographs,Photograph,Robert Stevenson,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5139,false,true,266776,Photographs,Photograph,"Privat Dennis Sullivan, Company E, Second Virginia Cavalry",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,April 1865,1865,1865,Albumen silver print from glass negative,"Image: 13.1 × 18.9 cm (5 3/16 × 7 7/16 in.), oval","Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5140,false,true,266777,Photographs,Photograph,Andrew Wagoner,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5141,false,true,266778,Photographs,Photograph,Henry Yon,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5142,false,true,266779,Photographs,Photograph,Frederick Pilgrim,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.5143,false,true,266780,Photographs,Photograph,Frederick Pilgrim,,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865,1865,1865,Albumen silver print from glass negative,,"Gift of Stanley B. Burns, M.D. and The Burns Archive, 1992",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.99,false,true,283206,Photographs,Photograph,"Private George Ruoss, Co. G, 7th New York Volunteers",,,,,,Artist,,Reed Brockway Bontecou,"American, 1824–1907",,"Bontecou, Reed Brockway",American,1824,1907,1865–1866,1865,1866,Albumen silver print from glass negative,Image: 16.6 × 21.7 cm (6 9/16 × 8 9/16 in.) Sheet: 19.3 × 24.2 cm (7 5/8 × 9 1/2 in.) Mount: 27.7 × 35.4 cm (10 7/8 × 13 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.4,false,true,263187,Photographs,Photograph,[Boston from a Hot-Air Balloon],,,,,,Artist,,James Wallace Black,"American, 1825–1896",,"Black, James Wallace",American,1825,1896,1860s,1860,1869,Albumen silver print from glass negative,25.6 x 20.2 cm. (10 1/16 x 7 15/16 in.),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.87,false,true,283189,Photographs,Photograph,"Boston, as the Eagle and the Wild Goose See It",,,,,,Artist,,James Wallace Black,"American, 1825–1896",,"Black, James Wallace",American,1825,1896,1860,1860,1860,Albumen silver print from glass negative,"Image: 18.5 x 16.7 cm (7 5/16 x 6 9/16 in.), irregularly trimmed Mount: 20.3 x 17 cm (8 x 6 11/16 in.), irregularly trimmed","Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1147,false,true,286653,Photographs,Photograph,Four Generations,,,,,,Artist,,James Wallace Black,"American, 1825–1896",,"Black, James Wallace",American,1825,1896,ca. 1860,1858,1862,Salted paper print,Image (Oval): 28.1 × 22.4 cm (11 1/16 × 8 13/16 in.) Mount (Oval): 27.8 × 22.2 cm (10 15/16 × 8 3/4 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1247,false,true,286300,Photographs,Photograph,[Victorian House],,,,,,Artist,,James Wallace Black,"American, 1825–1896",,"Black, James Wallace",American,1825,1896,ca. 1860,1855,1865,Albumen silver print from glass negative,Image: 24.8 × 32.5 cm (9 3/4 × 12 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1248,false,true,286299,Photographs,Photograph,"[Washington Street, Boston]",,,,,,Artist,,James Wallace Black,"American, 1825–1896",,"Black, James Wallace",American,1825,1896,ca. 1860,1855,1865,Albumen silver print from glass negative,"Image: 18 × 15.5 cm (7 1/16 × 6 1/8 in.), dome top","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.94,false,true,283198,Photographs,Photograph,"[Ordnance Wharf, City Point, Virginia]",,,,,,Artist,,Thomas C. Roche,"American, 1826–1895",,"Roche, Thomas C.",American,1826,1895,1865,1865,1865,Albumen silver print from glass negative,Image: 21.7 x 25.5 cm (8 9/16 x 10 1/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.518,false,true,294766,Photographs,Photograph,[Winter Scene with Trestle Bridge Along the Atlantic & Great Western Railway],,,,,,Artist,,James Fitzallen Ryder,"American, 1826–1904",,"Ryder, James Fitzallen",American,1826,1904,1862–64,1862,1864,Albumen silver print from glass negative,Image: 18.7 x 23.8 cm (7 3/8 x 9 3/8 in.),"Gift of Mary and Dan Solomon, in honor of Hans P. Kraus Jr., 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.622,true,true,262612,Photographs,Photograph,"View on the Columbia, Cascades",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1867,1867,1867,Albumen silver print from glass negative,40.0 x 52.4 cm (15 3/4 x 20 5/8 in.),"Warner Communications Inc. Purchase Fund and Harris Brisbane Dick Fund, 1979",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1082,false,true,266132,Photographs,Photograph,"Devil's Canyon, Geysers, Looking Down",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1868–70,1868,1870,Albumen silver print from glass negative,39.8 x 52.4 cm. (15 11/16 x 20 5/8 in.),"Rogers Fund, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1083,true,true,266133,Photographs,Photograph,"The Town on the Hill, New Almaden",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1863,1863,1863,Albumen silver print from glass negative,39.7 x 52.3 cm (15 5/8 x 20 9/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.540.4,false,true,259681,Photographs,Photograph,"El Capitan, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1865–66,1865,1866,Albumen silver print from glass negative,,"Rogers Fund, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.540.6,false,true,259683,Photographs,Photograph,"Section of the Grizzly Giant with Galen Clark, Mariposa Grove, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1865–66,1865,1866,Albumen silver print from glass negative,Image: 20 5/8 × 15 11/16 in. (52.4 × 39.8 cm) Sheet: 23 13/16 × 19 1/16 in. (60.5 × 48.4 cm),"Rogers Fund, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.643.1,false,true,260314,Photographs,Photograph,"Vernal Fall, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1875",1865,1866,Albumen silver print from glass negative,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.643.3,false,true,260317,Photographs,Photograph,"Section of the Grizzly Giant with Galen Clark, Mariposa Grove, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,52.2 x 40.6 cm. (20 9/16 x 16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.1,false,true,264896,Photographs,Photograph,"Residence of Charles Bernard. 312 Oak Street, San Francisco, California",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,ca. 1876,1874,1878,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.2,false,true,264909,Photographs,Photograph,San Francisco,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.3,false,true,264920,Photographs,Photograph,"San Francisco, from California and Powell Street",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.4,false,true,264931,Photographs,Photograph,"San Francisco, from Rincon Hill",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.5,false,true,264942,Photographs,Photograph,"San Francisco, from California and Powell Streets",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.6,false,true,264953,Photographs,Photograph,South Side of California Street,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.7,false,true,264964,Photographs,Photograph,North Side of California Street,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.8,false,true,264975,Photographs,Photograph,"First Street, San Francisco",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.9,false,true,264986,Photographs,Photograph,The Golden Gate,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1868–69, printed ca. 1876",1868,1869,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.10,false,true,264897,Photographs,Photograph,"Alcatraz Island, San Francisco",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1868–69, printed ca. 1876",1868,1869,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.11,false,true,264900,Photographs,Photograph,Cliff House and Seal Rock,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1868–69, printed ca. 1876",1868,1869,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.12,false,true,264901,Photographs,Photograph,Seal Rocks,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1868–69, printed ca. 1876",1868,1869,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.13,false,true,264902,Photographs,Photograph,"Oakland, from Military Academy",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1864, printed ca. 1876",1864,1864,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.14,false,true,264903,Photographs,Photograph,General View of Yosemite,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.15,false,true,264904,Photographs,Photograph,Yosemite Valley,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.16,false,true,264905,Photographs,Photograph,Yosemite Valley from Union Point,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.17,false,true,264906,Photographs,Photograph,Yosemite Valley from Glacier Point,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.18,false,true,264907,Photographs,Photograph,Looking Up Yosemite Valley,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.19,false,true,264908,Photographs,Photograph,Looking Down Yosemite Valley,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.20,false,true,264910,Photographs,Photograph,"Yosemite Falls, 2,634 feet",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.21,false,true,264911,Photographs,Photograph,"Yosemite Falls, 2,634 feet",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.22,false,true,264912,Photographs,Photograph,"Cathedral Rocks, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.23,false,true,264913,Photographs,Photograph,Cathedral Rocks and Spires,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.24,false,true,264914,Photographs,Photograph,"Cathedral Rocks, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.25,false,true,264915,Photographs,Photograph,"The Domes, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.26,false,true,264916,Photographs,Photograph,"The Domes, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.27,false,true,264917,Photographs,Photograph,"North and South Dome, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.28,false,true,264918,Photographs,Photograph,"North and South Dome, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.29,false,true,264919,Photographs,Photograph,"North Dome, 3,725 feet, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.30,false,true,264921,Photographs,Photograph,"South Dome, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.31,false,true,264922,Photographs,Photograph,"South Dome, 6,000 feet",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.32,false,true,264923,Photographs,Photograph,"South Dome, 6,000 feet",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.33,false,true,264924,Photographs,Photograph,"South Dome, 6,000 feet, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,Image: 33.5 x 26.8 cm (13 3/16 x 10 9/16 in.),"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.34,false,true,264925,Photographs,Photograph,Merced River,,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.35,false,true,264926,Photographs,Photograph,"Merced River, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.36,false,true,264927,Photographs,Photograph,"Merced River, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.37,false,true,264928,Photographs,Photograph,"Mirror Lake, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.38,false,true,264929,Photographs,Photograph,"Mirror View of the Three Brothers, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.39,false,true,264930,Photographs,Photograph,"Mirror View of Cathedral Rocks, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.40,false,true,264932,Photographs,Photograph,"Cap of Liberty and Nevada Fall, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.41,false,true,264933,Photographs,Photograph,"Nevada Fall, 700 feet, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,Image: 33.6 × 26.7 cm (13 1/4 × 10 1/2 in.) Sheet: 42.3 × 33 cm (16 5/8 × 13 in.),"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.42,false,true,264934,Photographs,Photograph,"Lower Yosemite Fall, 1,600 feet",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.43,false,true,264935,Photographs,Photograph,"Bridal Veil Fall, 940 feet, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.44,false,true,264936,Photographs,Photograph,"Washington Tower, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.45,false,true,264937,Photographs,Photograph,"Washington Tower, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.46,false,true,264938,Photographs,Photograph,"Eagle Point, 4,000 feet, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.47,false,true,264939,Photographs,Photograph,"Vernal Falls, 350 feet, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.48,false,true,264940,Photographs,Photograph,"Mirror View of El Capitan, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.49,false,true,264941,Photographs,Photograph,"Mirror View of Sentinel Rock, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,Image: 13 1/4 × 10 1/2 in. (33.7 × 26.6 cm) Sheet: 16 5/8 × 13 7/16 in. (42.2 × 34.2 cm),"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.50,false,true,264943,Photographs,Photograph,"Sentinel Rock, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.51,false,true,264944,Photographs,Photograph,"Sentinel Rock, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.52,false,true,264945,Photographs,Photograph,"Magic Tower, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.53,false,true,264946,Photographs,Photograph,"Eagle Point, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.54,false,true,264947,Photographs,Photograph,"The Three Brothers, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.55,false,true,264948,Photographs,Photograph,"The Three Brothers, Yosemite",,,,,,Artist,Attributed to,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.56,false,true,264949,Photographs,Photograph,The Hotel from Mammoth Grove,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.57,false,true,264950,Photographs,Photograph,Mammoth Grove Hotel,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,Image: 25.9 x 32.4 cm (10 3/16 x 12 3/4 in.),"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.58,false,true,264951,Photographs,Photograph,"The Tripod, 94 feet circumference",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.59,false,true,264952,Photographs,Photograph,Cosmopolitan Saloon,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.60,false,true,264954,Photographs,Photograph,The House over a Stump of a Big Tree,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.61,false,true,264955,Photographs,Photograph,The House Built over the Stump of a Big Tree,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.62,false,true,264956,Photographs,Photograph,"The Sentinels, 315 feet, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.63,false,true,264957,Photographs,Photograph,"The Sentinel, 315 feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.64,false,true,264958,Photographs,Photograph,Father of the Forest,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.65,false,true,264959,Photographs,Photograph,"The Father of the Forest, 112 feet circumference, Calaveras Grove",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.66,false,true,264960,Photographs,Photograph,Mother of the Forest,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.67,false,true,264961,Photographs,Photograph,"Section of the Grizzly Giant, 101 feet circumference",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,Image: 33.5 x 26.6 cm (13 3/16 x 10 1/2 in.),"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.68,false,true,264962,Photographs,Photograph,Pioneer's Cabin,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.69,false,true,264963,Photographs,Photograph,"The Three Graces, 272 feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.70,false,true,264965,Photographs,Photograph,Looking Up Pluto's Chimney,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1865–66, printed ca. 1876",1865,1866,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.71,false,true,264966,Photographs,Photograph,"Hutchings Hotel, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"ca. 1872, printed ca. 1876",1870,1874,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.72,false,true,264967,Photographs,Photograph,Geyser Road,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1868–70, printed ca. 1876",1868,1870,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.73,false,true,264968,Photographs,Photograph,Sulphur Creek and Flume-road to Geysers,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1868–70, printed ca. 1876",1868,1870,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.74,false,true,264969,Photographs,Photograph,Sulphur Creek and Road to Geysers,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1868–70, printed ca. 1876",1868,1870,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.75,false,true,264970,Photographs,Photograph,"Hot Sulphur Springs, Santa Barbara",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.76,false,true,264971,Photographs,Photograph,"Hot Sulphur Springs, Santa Barbara",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.77,false,true,264972,Photographs,Photograph,Santa Barbara and Mission Church,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.78,false,true,264973,Photographs,Photograph,"Old Mission Church, Santa Barbara",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.79,false,true,264974,Photographs,Photograph,San Luis Obispo,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.80,false,true,264976,Photographs,Photograph,"Steel's Ranch, San Luis Obispo",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.81,false,true,264977,Photographs,Photograph,San Diego,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.82,false,true,264978,Photographs,Photograph,Los Angeles,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.83,false,true,264979,Photographs,Photograph,"Lake Vineyard and Orange Grove, Los Angeles",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.84,false,true,264980,Photographs,Photograph,Vineyard of Camulos Ranch,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.85,false,true,264981,Photographs,Photograph,Santa Margarita Ranch,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1876, printed ca. 1876",1876,1876,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.86,false,true,264982,Photographs,Photograph,Virginia City,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.87,false,true,264983,Photographs,Photograph,Virginia City,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.88,false,true,264984,Photographs,Photograph,Sutro Tunnel's Road to Virginia City,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.89,false,true,264985,Photographs,Photograph,Road View to Sutro Tunnel,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.90,false,true,264987,Photographs,Photograph,Buildings of Sutro Tunnel,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.91,false,true,264988,Photographs,Photograph,Sutro Tunnel Shaft No. 2,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.92,false,true,264989,Photographs,Photograph,Sutro Tunnel Shaft No. 2,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.93,false,true,264990,Photographs,Photograph,Sutro Tunnel Shaft No. 3,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.94,false,true,264991,Photographs,Photograph,"Eureka Quartz Mill and Flume, Nevada",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.95,false,true,264992,Photographs,Photograph,"Eureka Quartz Mill and Flume, Nevada",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1875, printed ca. 1876",1875,1875,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.96,false,true,264993,Photographs,Photograph,"Passage of the Dalles, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1867, printed ca. 1876",1867,1867,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.97,false,true,264994,Photographs,Photograph,"Cascades, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1867, printed ca. 1876",1867,1867,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.98,false,true,264995,Photographs,Photograph,"Cape Horn, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1867, printed ca. 1876",1867,1867,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.99,false,true,264996,Photographs,Photograph,"Islands in the Upper Cascades, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1867, printed ca. 1876",1867,1867,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.107,false,true,283220,Photographs,Photograph,"Sugar Loaf Islands, Farallons",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1868–69,1868,1869,Albumen silver print from glass negative,Image: 40 x 52.4; Mount: 55.3 x 66.3,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.108,false,true,283221,Photographs,Photograph,"Multnomah Falls Cascade, Columbia River",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1867,1867,1867,Albumen silver print from glass negative,Image: 52.4 x 40 cm (20 5/8 x 15 3/4 in.) Mount: 64.7 x 49.5 cm (25 1/2 x 19 1/2 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.109,true,true,283222,Photographs,Photograph,Cape Horn near Celilo,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1867,1867,1867,Albumen silver print from glass negative,Image: 40 x 52.4cm (15 3/4 x 20 5/8in.) Mount: 54.5 x 68.6 cm (21 7/16 x 27 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.110,false,true,283223,Photographs,Photograph,"Strait of Carquennes, from South Vallejo",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1868–69,1868,1869,Albumen silver print from glass negative,Image: 40.3 x 52.5 cm (15 7/8 x 20 11/16 in.) Mount: 54.4 x 66.4 cm (21 7/16 x 26 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.174,false,true,285468,Photographs,Photograph,"Multnomah Falls, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1867, printed later",1867,1867,Albumen silver print from glass negative,Image: 52.1 x 38.7 cm (20 1/2 x 15 1/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.339,false,true,285987,Photographs,Photograph,"Indian Sweat House, Mendicino County, Colorado",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1863,1863,1863,Albumen silver print from glass negative,Image: 40 x 52.4 cm (15 3/4 x 20 5/8 in.) Mount: 55.1 x 68.3 cm (21 11/16 x 26 7/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.493,false,true,286513,Photographs,Photograph,"Cape Horn, Columbia River, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1867,1867,1867,Albumen silver print from glass negative,Image: 52.1 x 39 cm (20 1/2 x 15 3/8 in.) Frame: 76.2 x 63.5 cm (30 x 25 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.618,false,true,283219,Photographs,Photograph,"The Grisly Giant, Mariposa Grove, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 52.3 x 40.7; Mount: 61.4 x 54.1,"Gilman Collection, Purchase, Gift of The Howard Gilman Foundation, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.100,false,true,264898,Photographs,Photograph,"Tooth Bridge, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1867, printed ca. 1876",1867,1867,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.101,false,true,264899,Photographs,Photograph,"Castle Rock, Oregon",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1867, printed ca. 1876",1867,1867,Albumen silver print from glass negative,,"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1183,false,true,285825,Photographs,Photograph,"2637 Ft. Yosemite Fall, Front View",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,"Image: 40.9 x 51.3 cm (16 1/8 x 20 3/16 in.), arch-topped Mount: 53.6 x 67 cm (21 1/8 x 26 3/8 in.)","Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1184,false,true,285714,Photographs,Photograph,Mt. Broderick and Nevada Fall. Fall = 700 ft.,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1861, Yosemite",1861,1861,Albumen silver print from glass negative,Image: 41.8 × 52.1 cm (16 7/16 × 20 1/2 in.) Mount: 53.9 × 66.9 cm (21 1/4 × 26 5/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1185,false,true,285715,Photographs,Photograph,North Dome on left - Royal Arches - Washington Column,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1861, Yosemite",1861,1861,Albumen silver print from glass negative,Image: 42.5 × 51.5 cm (16 3/4 × 20 1/4 in.) Mount: 53.8 × 67.3 cm (21 3/16 × 26 1/2 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1186,false,true,286077,Photographs,Photograph,Yosemite Fall. Down the Valley. 2637 Ft.,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1861, Yosemite",1861,1861,Albumen silver print from glass negative,Image: 44 × 50.9 cm (17 5/16 × 20 1/16 in.) Mount: 53.5 × 67 cm (21 1/16 × 26 3/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1187,false,true,285860,Photographs,Photograph,"Cascade, Nevada Fall on Left, View above Vernal Fall",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,"Image: 41.2 x 52.4 cm (16 1/4 x 20 5/8 in.), arch-topped Mount: 53.5 x 66.7 cm (21 1/16 x 26 1/4 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1188,false,true,286423,Photographs,Photograph,Sentinel. Front View: 3270,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1861, Yosemite",1861,1861,Albumen silver print from glass negative,Image: 43.1 × 51.6 cm (16 15/16 × 20 5/16 in.) Mount: 53.9 × 67 cm (21 1/4 × 26 3/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1189,false,true,286425,Photographs,Photograph,Cathedral Rock,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,"Image: 42.1 x 50.8 cm (16 9/16 x 20 in.), arch-topped Mount: 53.7 x 66.7 cm (21 1/8 x 26 1/4 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1190,false,true,286427,Photographs,Photograph,"Mt. Broderick in Distant Centre, Piroyac, Falling Chrystals, Vernal Fall",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,"Image: 42.4 x 51.5 cm (16 11/16 x 20 1/4 in.), arch-topped Mount: 53.4 x 67 cm (21 x 26 3/8 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1256,false,true,286052,Photographs,Photograph,"Pohono, Bridal Veil, 900 Feet, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 16 1/2 × 20 9/16 in. (41.9 × 52.3 cm) Mount: 21 5/16 in. × 26 7/16 in. (54.1 × 67.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1257,false,true,285824,Photographs,Photograph,"Up the Valley, North Dome in Center, Sentinel on Left",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 16 1/2 × 20 9/16 in. (41.9 × 52.3 cm) Mount: 21 5/16 in. × 26 7/16 in. (54.1 × 67.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1258,false,true,285994,Photographs,Photograph,"River View, Down the Valley, Cathedral Rock on Left",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1861, Yosemite",1861,1861,Albumen silver print from glass negative,Image: 15 9/16 × 20 9/16 in. (39.5 × 52.2 cm) Mount: 21 1/16 × 26 1/2 in. (53.5 × 67.3 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1259,false,true,285713,Photographs,Photograph,"Camp Grove, Near Sentinel",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1861, Yosemite",1861,1861,Albumen silver print from glass negative,Image: 15 1/2 × 19 5/8 in. (39.3 × 49.9 cm) Mount: 21 1/4 in. × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1260,false,true,286458,Photographs,Photograph,"Tasayac, or the Half Dome, 4967 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,"1861, Yosemite",1861,1861,Albumen silver print from glass negative,Image: 15 13/16 × 20 1/4 in. (40.1 × 51.5 cm) Image: 21 1/4 × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1261,false,true,286459,Photographs,Photograph,"Nevada Fall, 700 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 42.2 x 51; Mount: 53.6 x 67,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1262,false,true,286460,Photographs,Photograph,"Tutucanula, El Capitan",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 51.3 x 41.4; Mount: 66.5 x 53.6,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1263,false,true,286072,Photographs,Photograph,"River View, Sentinel, 3270 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 52.1 x 41.6; Mount: 60.8 x 53.6,"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1264,false,true,286073,Photographs,Photograph,"Three Brothers, Front View, 4480 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 20 3/8 × 16 1/8 in. (51.7 × 40.9 cm) Mount: 26 5/16 in. × 21 1/8 in. (66.9 × 53.6 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1265,false,true,286074,Photographs,Photograph,"Outline View of the Half Dome, 4967 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 20 1/16 × 16 5/16 in. (51 × 41.5 cm) Mount: 21 1/4 × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1266,false,true,286075,Photographs,Photograph,"River View, Sentinel, 3270 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 20 1/4 × 16 7/16 in. (51.5 × 41.7 cm) Mount: 21 1/4 × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1267,false,true,286076,Photographs,Photograph,Cathedral Towers,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 20 3/8 × 16 1/8 in. (51.7 × 40.9 cm) Mount: 26 5/16 in. × 21 1/8 in. (66.9 × 53.6 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1268,false,true,286078,Photographs,Photograph,The Lake at the Foot of Half Dome,,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 16 1/2 × 20 9/16 in. (41.9 × 52.3 cm) Mount: 21 5/16 in. × 26 7/16 in. (54.1 × 67.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1269,false,true,285858,Photographs,Photograph,"Cathedral Rock, Down the Valley",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 16 1/2 × 20 9/16 in. (41.9 × 52.3 cm) Mount: 21 5/16 in. × 26 7/16 in. (54.1 × 67.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1270,false,true,285859,Photographs,Photograph,"Section of Grisly Giant, Mariposa Grove",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 16 5/16 × 20 11/16 in. (41.5 × 52.5 cm) Mount: 21 1/4 × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1271,false,true,285861,Photographs,Photograph,"River View Down Valley, Cathedral Rock on Left",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 15 9/16 × 20 9/16 in. (39.5 × 52.2 cm) Mount: 21 1/16 × 26 1/2 in. (53.5 × 67.3 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1272,false,true,286510,Photographs,Photograph,"Tacoye, The North Dome, 3729 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 19 15/16 × 16 5/16 in. (50.6 × 41.4 cm) Mount: 21 1/4 × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1273,false,true,286424,Photographs,Photograph,"Cathedral Rock, River View",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 41.5 x 59.6; Mount: 53.4 x 67,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1274,false,true,286426,Photographs,Photograph,"Yosemite Falls, River View, 2637 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 16 1/2 × 19 1/2 in. (41.9 × 49.5 cm) Mount: 21 1/4 × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1275,false,true,286428,Photographs,Photograph,"Pompomasos (Leaping Frogs), Three Brothers",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 42.4 x 52.2; Mount: 53.7 x 69.3,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1276,false,true,286457,Photographs,Photograph,"Tutucanula, El Capitan, 4000 Feet",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1861,1861,1861,Albumen silver print from glass negative,Image: 16 1/2 × 19 1/2 in. (41.9 × 49.5 cm) Mount: 21 1/4 × 26 3/8 in. (54 × 67 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1084.1–.3,false,true,689997,Photographs,Panorama,"View from the Sentinel Dome, Yosemite",,,,,,Artist,,Carleton E. Watkins,"American, 1829–1916",,"Watkins, Carleton E.",American,1829,1916,1865–66,1865,1866,Albumen silver prints from glass negatives,Image 1: 40.3 × 52.1 cm (15 7/8 × 20 1/2 in.) Mount 1: 53.4 x 68.7cm (21 x 27 1/16in.) Image 2: 40.0 x 52.5 cm (15 3/4 x 20 11/16 in.) Mount 2: 53.3 x 68.8cm (21 x 27 1/16in.) Image 3: 40.8 x 52.5cm (16 1/16 x 20 11/16in.) Mount 3: 53.2 x 68.7cm (20 15/16 x 27 1/16in.),"Purchase, Joseph Pulitzer Bequest, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/689997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1196,false,true,265001,Photographs,Photograph,"Hanging Rock, Foot of Echo Cañon",,,,,,Artist,,Andrew Joseph Russell,"American, 1830–1902",,"Russell, Andrew Joseph",American,1830,1902,1867–68,1867,1868,Albumen silver print from glass negative,Image: 21.3 x 28.2 cm (8 3/8 x 11 1/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.91,false,true,283193,Photographs,Photograph,"Slave Pen, Alexandria, Virginia",,,,,,Artist,,Andrew Joseph Russell,"American, 1830–1902",,"Russell, Andrew Joseph",American,1830,1902,1863,1863,1863,Albumen silver print from glass negative,Image: 25.6 x 36.5cm (10 1/16 x 14 3/8in.) Mount: 12 3/16 × 16 9/16 in. (31 × 42 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1237,false,true,286065,Photographs,Photograph,"Fort Laramie, Wyoming",,,,,,Artist,Attributed to,Ridgway Glover,"American, 1831–1866",,"Glover, Ridgway",American,1831,1866,ca. 1866,1864,1868,Albumen silver print from glass negative,"Image: 12.8 × 17.8 cm (5 1/16 × 7 in.), oval Mount: 20.3 × 25.3 cm (8 × 9 15/16 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.1056.5,false,true,262764,Photographs,Stereograph,"[Ship in Ice, Greenland Expedition]",,,,,,Artist,,Isaac Israel Hayes,"American, 1832–1881",,"Hayes, Isaac Israel",American,1832,1881,ca. 1859,1857,1861,Albumen silver print from glass negative,,"Warner Communications Inc. Purchase Fund, 1980",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.1056.7,false,true,262766,Photographs,Stereograph,Esquimau,,,,,,Artist,,Isaac Israel Hayes,"American, 1832–1881",,"Hayes, Isaac Israel",American,1832,1881,ca. 1859,1857,1861,Albumen silver print from glass negative,,"Warner Communications Inc. Purchase Fund, 1980",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.458,false,true,685798,Photographs,Carte-de-visite,[E. Johnson],,,,,,Artist,,George Gardner Rockwood,"American, 1832–1911",,"Rockwood, George Gardner",American,1832,1911,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.471,false,true,685811,Photographs,Carte-de-visite,[John Frederick Kensett],,,,,,Artist,,George Gardner Rockwood,"American, 1832–1911",,"Rockwood, George Gardner",American,1832,1911,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.725,false,true,686064,Photographs,Carte-de-visite,[Aaron Draper Shattuck],,,,,,Artist,,George Gardner Rockwood,"American, 1832–1911",,"Rockwood, George Gardner",American,1832,1911,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.752,false,true,686091,Photographs,Carte-de-visite,[W.O.? Stone],,,,,,Artist,,George Gardner Rockwood,"American, 1832–1911",,"Rockwood, George Gardner",American,1832,1911,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.771,false,true,686110,Photographs,Carte-de-visite,[Laurent ?],,,,,,Artist,,George Gardner Rockwood,"American, 1832–1911",,"Rockwood, George Gardner",American,1832,1911,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.804,false,true,686143,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Artist,,George Gardner Rockwood,"American, 1832–1911",,"Rockwood, George Gardner",American,1832,1911,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.855,false,true,686194,Photographs,Carte-de-visite,[Unknown Subject],,,,,,Artist,,George Gardner Rockwood,"American, 1832–1911",,"Rockwood, George Gardner",American,1832,1911,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.391,false,true,288841,Photographs,Photograph,"[Lula Lake and Upper Falls on Rock Creek, near Lookout Mountain, Georgia]",,,,,,Artist,,Isaac H. Bonsall,"American, 1833–1909",,"Bonsall, Isaac H.",American,1833,1909,1864–65,1864,1865,Albumen silver print from glass negative,Image: 26.5 x 33.7 cm (10 7/16 x 13 1/4 in.),"Purchase, Celia Tompkins Hegyi Gift, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.392,false,true,288840,Photographs,Photograph,"Lulah Falls, Lookout Mountain, Georgia",,,,,,Artist,,Isaac H. Bonsall,"American, 1833–1909",,"Bonsall, Isaac H.",American,1833,1909,1864–65,1864,1865,Albumen silver print from glass negative,Image: 21.2 x 26.6 cm (8 3/8 x 10 1/2 in.),"Purchase, Celia Tompkins Hegyi Gift, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.238,false,true,286559,Photographs,Photograph,Deck of U.S. Ship Vermont,,,,,,Artist,,Henry P. Moore,"American, 1833–1911",,"Moore, Henry P.",American,1833,1911,ca. 1863,1861,1865,Albumen silver print from glass negative,Image: 13.3 x 20.2 cm (5 1/4 x 7 15/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.897,false,true,286558,Photographs,Photograph,"Contrabands Aboard U.S. Ship Vermont, Port Royal, South Carolina",,,,,,Artist,,Henry P. Moore,"American, 1833–1911",,"Moore, Henry P.",American,1833,1911,1861,1861,1861,Albumen silver print from glass negative,Image: 12.9 × 20.8 cm (5 1/16 × 8 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1137,false,true,286557,Photographs,Photograph,"Negroes (Gwine to de Field), Hopkinson's Plantation, Edisto Island, South Carolina",,,,,,Artist,,Henry P. Moore,"American, 1833–1911",,"Moore, Henry P.",American,1833,1911,1862,1862,1862,Albumen silver print from glass negative,Image: 15.2 × 20.4 cm (6 × 8 1/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.52,false,true,301982,Photographs,Photograph,"Frank Wyatt, One of General Dodge's Band, Corinth, Mississippi",,,,,,Artist,,George W. Armstead,"American, 1833–1912",,"Armstead, George W.",American,1833,1912,"September 18, 1863",1863,1863,Albumen silver print from glass negative,Image: 9 x 5.4 cm (3 9/16 x 2 1/8 in.),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1139,false,true,285746,Photographs,Photograph,"[Hudson River Seen from United State Military Academy at West Point, New York]",,,,,,Artist,,George Kendall Warren,"American, 1834–1884",,"Warren, George Kendall",American,1834,1884,1867,1867,1867,Albumen silver print,Image: 6 1/4 × 9 1/8 in. (15.9 × 23.2 cm) Mount: 10 1/8 in. × 13 3/16 in. (25.7 × 33.5 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1140,false,true,285745,Photographs,Photograph,"""Sam,"" the Black Peddlar",,,,,,Artist,,George Kendall Warren,"American, 1834–1884",,"Warren, George Kendall",American,1834,1884,ca. 1858,1856,1860,Salted paper print,Image: 6 9/16 in. × 5 in. (16.7 × 12.7 cm) Mount: 11 7/16 in. × 8 1/2 in. (29.1 × 21.6 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.29.9,false,true,269088,Photographs,Carte-de-visite,Steamer R.E. Lee Racing with Natches When Nearing St. Louis,,,,,,Artist,,Robert Benecke,"American, 1835–1903",,"Benecke, Robert",American,1835,1903,ca. 1870,1868,1872,Albumen silver print,,"Gift of Mrs. Lawrence Fowler, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1245,false,true,286312,Photographs,Photograph,"Ma-ni-mic, Cheyenne Chief",,,,,,Artist,,William Stinson Soule,"American, 1836–1908",,"Soule, William Stinson",American,1836,1908,1869–74,1869,1874,Albumen silver print from glass negative,Image: 7 7/16 × 5 3/8 in. (18.9 × 13.7 cm) Mount: 14 3/4 in. × 10 7/8 in. (37.4 × 27.7 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.367.5,false,true,283336,Photographs,Photograph,[Wisconsin Landscape],,,,,,Artist,,Henry Hamilton Bennett,"American, 1843–1908",,"Bennett, Henry Hamilton",American,1843,1908,1889,1889,1889,Albumen silver print,17 x 22 cm (6 11/16 x 8 11/16 in. ),"Bequest of Winthrop Edey, 1999",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.530,false,true,261106,Photographs,Photograph,"Mammoth Hot Springs, Pulpit Terraces",,,,,,Artist,,William Henry Jackson,"American, 1843–1942",,"Jackson, William Henry",American,1843,1942,ca. 1883,1881,1885,Albumen silver print from glass negative,43.0 x 53.1 cm. (16 15/16 x 20 15/16 in.),"Rogers Fund, 1974",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.641.3,false,true,261003,Photographs,Photograph,"Tantalus Cañon, Utah",,,,,,Artist,,William Henry Jackson,"American, 1843–1942",,"Jackson, William Henry",American,1843,1942,1870s,1870,1879,Albumen silver print from glass negative,,"Gift of A. Hyatt Mayor, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1098.7,false,true,631031,Photographs,Photograph,"Hanging Rock, Clear Creek Canyon",,,,,,Artist,,William Henry Jackson,"American, 1843–1942",,"Jackson, William Henry",American,1843,1942,1870s,1870,1879,Albumen silver print,Image: 21 1/4 × 17 7/16 in. (54 × 44.3 cm) Mount: 27 7/8 in. × 22 in. (70.8 × 55.9 cm),"Gift of Joyce F. Menschel, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/631031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.120.1,false,true,268759,Photographs,Photograph,White Sailboat in Long Island Sound,,,,,,Artist,,Charles E. Bolles,"American, 1845–1919",,"Bolles, Charles E.",American,1845,1919,"1890s, printed 1897",1890,1899,Platinum print,,"Gift of Marion P. Bolles, 1942",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.120.2,false,true,268760,Photographs,Photograph,Sailboat,,,,,,Artist,,Charles E. Bolles,"American, 1845–1919",,"Bolles, Charles E.",American,1845,1919,"1890s, printed 1897",1890,1899,Platinum print,,"Gift of Marion P. Bolles, 1942",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.120.3,false,true,268761,Photographs,Photograph,"Hauling in Nets, Long Island Sound",,,,,,Artist,,Charles E. Bolles,"American, 1845–1919",,"Bolles, Charles E.",American,1845,1919,"1890s, printed 1897",1890,1899,Platinum print,,"Gift of Marion P. Bolles, 1942",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.572,false,true,285710,Photographs,Photograph,Otoe Delegation,,,,,,Artist,,Charles Milton Bell,"American, 1848–1893",,"Bell, Charles Milton",American,1848,1893,1881,1881,1881,Albumen silver print from glass negative,24 x 28.8 cm (9 7/16 x 11 5/16 in.),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.619,false,true,283224,Photographs,Photograph,Red Cloud,,,,,,Artist,,Charles Milton Bell,"American, 1848–1893",,"Bell, Charles Milton",American,1848,1893,1880,1880,1880,Albumen silver print from glass negative,Image: 14 × 9.9 cm (5 1/2 × 3 7/8 in.) Mount: 16.5 × 10.8 cm (6 1/2 × 4 1/4 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.164,false,true,269083,Photographs,Photograph,Mr. and Mrs. Charles E. Tiffany in Louis C. Tiffany's Studio,,,,,,Artist,,George Collins Cox,"American, 1851–1902",,"Cox, George Collins",American,1851,1902,ca. 1890,1888,1892,Platinum print (?),18.8 x 23.6 cm (7 3/8 x 9 5/16 in. ),"Museum Accession, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.165,false,true,269084,Photographs,Photograph,"Frances and Ethel de Forest, daughters of Robert de Forest",,,,,,Artist,,George Collins Cox,"American, 1851–1902",,"Cox, George Collins",American,1851,1902,ca. 1890,1888,1892,Albumen silver print,19.2 x 23.7 cm (7 9/16 x 9 5/16 in. ),"Museum Accession, 1946",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.132,true,true,267530,Photographs,Photograph,Blessed Art Thou among Women,,,,,,Artist,,Gertrude Käsebier,"American, 1852–1934",,"Käsebier, Gertrude",American,1852,1934,1899,1899,1899,Platinum print,23 x 13.2 cm (9 1/16 x 5 3/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.136,false,true,267534,Photographs,Photograph,The Sketch,,,,,,Artist,,Gertrude Käsebier,"American, 1852–1934",,"Käsebier, Gertrude",American,1852,1934,1903,1903,1903,Platinum print,15.3 x 20.7 cm. (6 x 8 1/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.142,false,true,267541,Photographs,Photograph,Happy Days,,,,,,Artist,,Gertrude Käsebier,"American, 1852–1934",,"Käsebier, Gertrude",American,1852,1934,1902,1902,1902,Platinum print,19.8 x 14.9 cm. (7 13/16 x 5 7/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.188,false,true,269308,Photographs,Photograph,[F. Holland Day],,,,,,Artist,,Gertrude Käsebier,"American, 1852–1934",,"Käsebier, Gertrude",American,1852,1934,ca. 1898,1896,1900,Platinum print,18.0 x 13.6 cm (7 1/16 x 5 3/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.1024.1,false,true,263962,Photographs,Photograph,"William M. Ivins, Jr.",,,,,,Artist,,Gertrude Käsebier,"American, 1852–1934",,"Käsebier, Gertrude",American,1852,1934,ca. 1910,1905,1915,Platinum print,Image: 7 5/8 × 5 5/16 in. (19.3 × 13.5 cm) Sheet: 7 5/8 × 5 5/16 in. (19.3 × 13.5 cm),"Gift of Barbara Ivins, 1984",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.889,false,true,285990,Photographs,Photograph,Baron Adolph de Meyer,,,,,,Artist,,Gertrude Käsebier,"American, 1852–1934",,"Käsebier, Gertrude",American,1852,1934,1903,1903,1903,Platinum print,"Image: 21.7 x 13.9 cm (8 9/16 x 5 1/2 in.), irregularly trimmed Mount: 22.1 x 14.2 cm (8 11/16 x 5 9/16 in.)","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.905,false,true,286366,Photographs,Photograph,"Turner Family, Woburn, Massachusetts",,,,,,Artist,,Gertrude Käsebier,"American, 1852–1934",,"Käsebier, Gertrude",American,1852,1934,ca. 1910,1908,1912,Gelatin silver print on tissue,33.2 x 26.1 cm (13 1/16 x 10 1/4 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1041.2,false,true,263549,Photographs,Photograph,[Group with Horse-Drawn Carriage],,,,,,Artist,,Christian Barthelmess,"American, 1854–1906",,"Barthelmess, Christian",American,1854,1906,1890s,1890,1899,Albumen silver print from glass negative,10.2 x 15.8 cm. (4 x 6 1/4 in.),"David Hunter McAlpin Fund, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.113,false,true,283229,Photographs,Photograph,[Children Fishing],,,,,,Artist,,William James Mullins,"American, 1860–1917",,"Mullins, William James",American,1860,1917,ca. 1900,1899,1901,Platinum print,Image: 9.1 x 25.7cm (3 9/16 x 10 1/8in.) Mount: 9.8 × 26.4 cm (3 7/8 in. × 10 3/8 in.),"Gilman Collection, Purchase, Jennifer and Joseph Duke Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.1,false,true,267474,Photographs,Photograph,"Oak, Mission Ridge, Santa Barbara, California",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.2,false,true,267481,Photographs,Photograph,"Cypress, Pebble Beach, California",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.3,false,true,267482,Photographs,Photograph,"Cypress, Pebble Beach, California",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.4,false,true,267483,Photographs,Photograph,"Cypress, Pebble Beach, California",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.5,false,true,267484,Photographs,Photograph,"Tortilla Women in the Plaza, Mexico",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.6,false,true,267485,Photographs,Photograph,"Cypress at Pebble Beach, California",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.7,false,true,267486,Photographs,Photograph,"Cypress, Pebble Beach, California",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.8,false,true,267487,Photographs,Photograph,"Cypress, Pebble Beach, California",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267487,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.9,false,true,267488,Photographs,Photograph,Fog and Cypress Trees,,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.10,false,true,267475,Photographs,Photograph,"Jesuit Church, Guanajuato, Mexico",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.11,false,true,267476,Photographs,Photograph,Pottery Sellers by the Church Door,,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.12,false,true,267477,Photographs,Photograph,"A Church Dome at Cuernavaca, Mexico",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.13,false,true,267478,Photographs,Photograph,Lunching in the Market Place,,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum or carbon transfer with applied media,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.14,false,true,267479,Photographs,Photograph,Water and Trees of the Viga Canal near Mexico City,,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.89.15,false,true,267480,Photographs,Photograph,"The Plaza, Market Day, Taxco, Mexico",,,,,,Artist,,Henry Ravell,"American, 1860–1930",,"Ravell, Henry",American,1860,1930,1910s,1910,1919,Gum bichromate print,,"Gift of Mrs. Florence D. R. Lothrop, in memory of her brother, Henry Ravell, 1930",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.644.2,false,true,260325,Photographs,Photograph,"A Study, No. 1",,,,,,Artist,,Rudolph Eickemeyer,"American, 1862–1932",,"Eickemeyer, Jr., Rudolph",American,1862,1932,1901,1901,1901,Gelatin silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.644.3,false,true,260326,Photographs,Photograph,[Ships Dockside in a Harbor],,,,,,Artist,,Rudolph Eickemeyer,"American, 1862–1932",,"Eickemeyer, Jr., Rudolph",American,1862,1932,ca. 1900,1898,1902,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.644.4,false,true,260327,Photographs,Photograph,[Ships on a Beach with Two Long Boats and Two Men Sweeping],,,,,,Artist,,Rudolph Eickemeyer,"American, 1862–1932",,"Eickemeyer, Jr., Rudolph",American,1862,1932,ca. 1900,1898,1902,Platinum print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.482,false,true,286256,Photographs,Photograph,Tired Butterfly,,,,,,Artist,,Rudolph Eickemeyer,"American, 1862–1932",,"Eickemeyer, Jr., Rudolph",American,1862,1932,1902,1902,1902,Carbon print,Image: 19.1 x 24.1 cm (7 1/2 x 9 1/2 in.) Sheet: 22.4 x 25.7 cm (8 13/16 x 10 1/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.150,false,true,669361,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/8 in. (7.5 × 8 cm) Sheet: 3 1/16 × 3 15/16 in. (7.7 × 10 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.151,false,true,670594,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/8 in. (7.5 × 8 cm) Sheet: 2 15/16 in. × 4 in. (7.5 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.152,false,true,670606,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 3/16 in. (7.6 × 8.1 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.153,false,true,670607,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 1/4 in. (7.6 × 8.3 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.154,false,true,670608,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/8 in. (7.4 × 8 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.155,false,true,670609,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 1/8 in. (7.6 × 7.9 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.156,false,true,670610,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 7/8 × 2 15/16 in. (7.3 × 7.4 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.157,false,true,670611,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 in. × 3 in. (7.5 × 7.6 cm) Sheet: 3 1/4 in. × 4 in. (8.3 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.158,false,true,670612,Photographs,Photograph; Photomicrograph,[Dew on a Blade of Grass],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 4 in. × 2 15/16 in. (10.1 × 7.5 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.159,false,true,670613,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 7/16 in. (7.5 × 8.8 cm) Sheet: 2 15/16 in. × 4 in. (7.5 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.160,false,true,670614,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/4 in. (7.4 × 8.2 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.161,false,true,670615,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 7/16 in. (7.4 × 8.8 cm) Sheet: 2 15/16 in. × 4 in. (7.4 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.162,false,true,670616,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 7/8 in. × 3 in. (7.3 × 7.6 cm) Sheet: 2 15/16 in. × 4 in. (7.5 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.163,false,true,670617,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,,"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.164,false,true,670618,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 3/4 × 2 7/8 in. (7 × 7.3 cm) Sheet: 3 × 4 in. (7.6 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.165,false,true,670619,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/4 in. (7.5 × 8.2 cm) Sheet: 2 15/16 × 4 1/16 in. (7.5 × 10.3 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.166,false,true,670620,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 3/16 in. (7.6 × 8.1 cm) Sheet: 3 × 4 in. (7.6 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.167,false,true,670621,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 5/16 in. (7.6 × 8.4 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.168,false,true,670622,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 5/16 in. (7.4 × 8.4 cm) Sheet: 3 × 4 in. (7.6 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.169,false,true,670623,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 in. × 3 in. (7.5 × 7.6 cm) Sheet: 2 15/16 × 3 7/8 in. (7.5 × 9.9 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.170,false,true,670624,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/4 in. (7.5 × 8.3 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.171,false,true,670625,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 1/16 in. (7.6 × 7.7 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.172,false,true,670626,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 2 3/4 in. (7.4 × 7 cm) Sheet: 2 15/16 × 3 15/16 in. (7.5 × 10 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.173,false,true,670627,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 in. × 3 in. (7.4 × 7.6 cm) Sheet: 3 1/16 in. × 4 in. (7.7 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.174,false,true,670628,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/2 in. (7.4 × 8.9 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.175,false,true,670629,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 3/16 in. (7.6 × 8.1 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.176,false,true,670630,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 in. × 3 in. (7.4 × 7.6 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.177,false,true,670631,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 13/16 × 2 15/16 in. (7.2 × 7.5 cm) Sheet: 2 7/8 × 3 5/8 in. (7.3 × 9.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.178,false,true,670632,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 × 3 in. (7.6 × 7.6 cm) Sheet: 3 1/4 in. × 4 in. (8.2 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.179,false,true,670633,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 7/8 in. × 3 in. (7.3 × 7.6 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.180,false,true,670634,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 7/8 × 2 15/16 in. (7.3 × 7.5 cm) Sheet: 2 15/16 in. × 4 in. (7.5 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.181,false,true,670635,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/4 in. (7.5 × 8.3 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.182,false,true,670636,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 2 7/8 in. (7.4 × 7.3 cm) Sheet: 2 15/16 in. × 4 in. (7.5 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.183,false,true,670637,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,,"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.184,false,true,670638,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/8 in. (7.4 × 7.9 cm) Sheet: 3 × 4 in. (7.6 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.185,false,true,670639,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/4 in. (7.4 × 8.2 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.186,false,true,670640,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 13/16 in. × 3 in. (7.1 × 7.6 cm) Sheet: 2 13/16 in. × 4 in. (7.2 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.187,false,true,670641,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 in. × 3 in. (7.5 × 7.6 cm) Sheet: 3 1/8 in. × 4 in. (7.9 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.188,false,true,670642,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 13/16 × 2 7/8 in. (7.2 × 7.3 cm) Sheet: 3 1/16 in. × 4 in. (7.7 × 10.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.189,false,true,670643,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 13/16 × 2 15/16 in. (7.1 × 7.4 cm) Sheet: 2 15/16 in. × 4 in. (7.5 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.190,false,true,670644,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/8 in. (7.5 × 7.9 cm) Sheet: 3 in. × 4 1/16 in. (7.6 × 10.3 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.191,false,true,670645,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 3/4 in. × 3 in. (7 × 7.6 cm) Sheet: 2 13/16 in. × 4 in. (7.2 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.192,false,true,670646,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 13/16 × 2 7/8 in. (7.1 × 7.3 cm) Sheet: 2 15/16 in. × 4 in. (7.5 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.193,false,true,670647,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 1/4 in. (7.6 × 8.3 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.194,false,true,670648,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 3/16 in. (7.5 × 8.1 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.195,false,true,670649,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 13/16 × 2 7/8 in. (7.1 × 7.3 cm) Sheet: 3 in. × 3 11/16 in. (7.6 × 9.4 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.196,false,true,670650,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 3 in. × 3 1/8 in. (7.6 × 8 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.197,false,true,670651,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 7/8 × 3 1/16 in. (7.3 × 7.8 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.198,false,true,670652,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 in. × 3 in. (7.4 × 7.6 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.199,false,true,670653,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/8 in. (7.4 × 8 cm) Sheet: 3 in. × 3 15/16 in. (7.6 × 10 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.200,false,true,670654,Photographs,Photograph; Photomicrograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,1890s–1920s,1890,1929,Gelatin silver print,Image: 2 15/16 × 3 1/8 in. (7.4 × 7.9 cm) Sheet: 3 × 4 in. (7.6 × 10.1 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/670654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.201,false,true,669362,Photographs,Photograph,[Frost],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,ca. 1910,1905,1915,Gelatin silver print,Image: 3 in. × 3 11/16 in. (7.6 × 9.4 cm) Sheet: 3 1/16 × 3 15/16 in. (7.7 × 10 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.202,false,true,669363,Photographs,Photograph,[Dew on a Spider Web],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,ca. 1910,1905,1915,Gelatin silver print,Image: 3 in. × 3 5/8 in. (7.6 × 9.2 cm) Sheet: 3 in. × 4 7/16 in. (7.6 × 11.2 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.203,false,true,669364,Photographs,Photograph,[Self-Portrait with Camera],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,ca. 1910,1905,1915,Gelatin silver print,Image: 2 13/16 × 3 3/4 in. (7.1 × 9.5 cm) Sheet: 3 in. × 4 1/16 in. (7.6 × 10.3 cm),"Purchase, Alfred Stieglitz Society Gifts, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/669364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.55.3,false,true,286768,Photographs,Photograph,[Snow Crystal],,,,,,Artist,,Wilson Alwyn Bentley,"American, 1865–1931",,"Bentley, Wilson Alwyn",American,1865,1931,ca. 1910,1905,1915,Gelatin silver print,Image: 7.4 x 9 cm (2 15/16 x 3 9/16 in.),"Josh Rosenthal Fund, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.188,false,true,292061,Photographs,Postcard,Carving One of Our Watermelons,,,,,,Artist,,William H. Martin,"American, 1865–1940",,"Martin, William H.",American,1865,1940,1909,1909,1909,Gelatin silver print,Image: 8.7 x 14 cm (3 7/16 x 5 1/2 in.) Frame: 55.9 x 71.1 cm (22 x 28 in.) (Multiple postcards in frame),"Twentieth-Century Photography Fund, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/292061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.460.5,false,true,288710,Photographs,Postcard,A Load of Fancy Poultry,,,,,,Artist,,William H. Martin,"American, 1865–1940",,"Martin, William H.",American,1865,1940,1909,1909,1909,Gelatin silver print,Image: 8.4 x 14 cm (3 5/16 x 5 1/2 in.) Frame: 55.9 x 71.1 cm (22 x 28 in.) (Multiple postcards in frame),"Gift of Charles Isaacs and Carol Nigro, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.460.6,false,true,288711,Photographs,Postcard,A Unique Bungalow,,,,,,Artist,,William H. Martin,"American, 1865–1940",,"Martin, William H.",American,1865,1940,1909,1909,1909,Gelatin silver print,Image: 8.4 x 14 cm (3 5/16 x 5 1/2 in.) Frame: 55.9 x 71.1 cm (22 x 28 in.) (Multiple postcards in frame),"Gift of Charles Isaacs and Carol Nigro, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.460.7,false,true,288712,Photographs,Postcard,Great Sport Shooting Rabbits in Iowa,,,,,,Artist,,William H. Martin,"American, 1865–1940",,"Martin, William H.",American,1865,1940,1909,1909,1909,Gelatin silver print,Image: 8.4 x 14 cm (3 5/16 x 5 1/2 in.) Frame: 55.9 x 71.1 cm (22 x 28 in.) (Multiple postcards in frame),"Gift of Charles Isaacs and Carol Nigro, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.460.8,false,true,288713,Photographs,Postcard,Harvesting Wheat in Iowa,,,,,,Artist,,William H. Martin,"American, 1865–1940",,"Martin, William H.",American,1865,1940,1909,1909,1909,Gelatin silver print,Image: 8.4 x 14 cm (3 5/16 x 5 1/2 in.) Frame: 55.9 x 71.1 cm (22 x 28 in.) (Multiple postcards in frame),"Gift of Charles Isaacs and Carol Nigro, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.608.20,false,true,270412,Photographs,Photograph,"Maxim Gorky and Zena Peschkoff, His Adopted Son",,,,,,Artist,,Alice Boughton,"American, 1865–1943",,"Boughton, Alice",American,1865,1943,ca. 1910,1908,1912,Platinum print,,"Gift of Miss Elma Loines, 1961",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.432.1,false,true,265501,Photographs,Photograph,[Man Sitting at Edge of Swimming Pool with Surface Water Reflections],,,,,,Artist,,Louis Fleckenstein,"American, 1866–1943",,"Fleckenstein, Louis",American,1866,1943,1931,1931,1931,Gelatin silver print,25.2 x 20.1 cm (9 15/16 x 7 15/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.432.2,false,true,265502,Photographs,Photograph,[The Boy Scouts Swimming Pool at Idyllwild],,,,,,Artist,,Louis Fleckenstein,"American, 1866–1943",,"Fleckenstein, Louis",American,1866,1943,1931,1931,1931,Gelatin silver print,7.8 x 5.4 cm (3 1/16 x 2 1/8 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.630.45,false,true,262075,Photographs,Photograph,[Road Through Flooded Land],,,,,,Artist,,Morgan Whitney,"American, 1869–1913",,"Whitney, Morgan",American,1869,1913,1890s–1900s,1890,1909,Platinum print,16.7 x 11.5 cm. (6 9/16 x 4 1/2 in.),"Gift of Mr. and Mrs. Morgan Whitney, 1977",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.174,false,true,267576,Photographs,Photograph,A Sioux Chief,,,,,,Artist,,Joseph T. Keiley,"American, 1869–1914",,"Keiley, Joseph T.",American,1869,1914,ca. 1898,1896,1900,Platinum print,19.3 x 14.1 cm. (7 5/8 x 5 9/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.179,false,true,267581,Photographs,Photograph,[The Averted Head - A Study in Flesh Tones],,,,,,Artist,,Joseph T. Keiley,"American, 1869–1914",,"Keiley, Joseph T.",American,1869,1914,1899,1899,1899,Platinum print,16.5 x 10.5 cm. (6 1/2 x 4 1/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.185,false,true,267588,Photographs,Photograph,A Bacchante,,,,,,Artist,,Joseph T. Keiley,"American, 1869–1914",,"Keiley, Joseph T.",American,1869,1914,1899,1899,1899,Platinum-palladium print,24.5 x 19.3 cm. (9 5/8 x 7 5/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.187,false,true,267590,Photographs,Photograph,Indian Head,,,,,,Artist,,Joseph T. Keiley,"American, 1869–1914",,"Keiley, Joseph T.",American,1869,1914,1898,1898,1898,Platinum print,19.8 x 14.5 cm. (7 13/16 x 5 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.417,false,true,267834,Photographs,Photograph,A Sioux Chief,,,,,,Artist,,Joseph T. Keiley,"American, 1869–1914",,"Keiley, Joseph T.",American,1869,1914,1898,1898,1898,Platinum print,19.5 x 13.0 cm. (7 11/16 x 5 1/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.291,false,true,285686,Photographs,Photograph,In the Circus,,,,,,Artist,,Harry Cogswell Rubincam,"American, 1871–1940",,"Rubincam, Harry Cogswell",American,1871,1940,1905,1905,1905,Platinum print,"Image: 21.2 x 15.7 cm (8 3/8 x 6 3/16 in.), irregular Sheet: 21.6 x 16.4 cm (8 1/2 x 6 7/16 in.) Mount: 37.8 x 28.8 cm (14 7/8 x 11 5/16 in.)","Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.126,false,true,283258,Photographs,Photograph,"[Lynching, Russellville, Kentucky]",,,,,,Artist,,Minor B. Wade,"American, 1874–1932",,"Wade, Minor B.",American,1874,1932,1908,1908,1908,Gelatin silver print,Image: 11.8 x 9.1 cm (4 5/8 x 3 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.91,false,true,282210,Photographs,Photograph,Midnight at the Bowery Mission Bread Line,,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1906–7,1906,1907,Gelatin silver print,11.5 x 14.9 cm (4 1/2 x 5 7/8 in. ),"Gift of John C. Waddell, 1998",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.338,false,true,286820,Photographs,Photograph,"Jo Lehman, a 7 year old newsboy. 824 Third Ave., N.Y. City. He was selling in this Saloon. I asked him about the badge he was wearing. ""Oh! Dat's me bruder's,"" he said. Location: New York, New York.",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,July 1910,1910,1910,Gelatin silver print,Image: 11.6 x 9.5 cm (4 9/16 x 3 3/4 in.),"Gilman Collection, Purchase, The Howard Gilman Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.438,false,true,650860,Photographs,Photograph,"The Morning Attendance at the Mill School, Huntsville, Alabama",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,December 1913,1913,1913,Gelatin silver print,Image: 4 7/16 × 6 7/16 in. (11.3 × 16.4 cm) Sheet: 4 15/16 × 6 15/16 in. (12.5 × 17.6 cm),"Purchase, The Overbrook Foundation Gift and funds from various donors, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/650860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.549.56,false,true,269725,Photographs,Photograph,Steamfitter,,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1910s,1910,1919,Gelatin silver print,Image: 34.5 × 24.7 cm (13 9/16 × 9 3/4 in.) Sheet: 35.4 × 27.9 cm (13 15/16 × 11 in.),"Gift of Clarence McK. Lewis, 1954",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.588.1,false,true,259722,Photographs,Photograph,"Boy carrying hats. Blee[c]ker St., N.Y.",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,February 1912,1912,1912,Gelatin silver print,11.5 x 14.7 cm (4 1/2 x 5 13/16 in.),"Bequest of Edwin De T. Bechtel, by exchange, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.588.6,false,true,259727,Photographs,Photograph,"Ivey Mill, Hickory, N.C. Little one, 3 years old, who visits and plays in the mill. Daughter of the overseer.",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,November 1898,1908,1908,Gelatin silver print,Image: 11.7 x 16.9 cm (4 5/8 x 6 5/8 in.),"Bequest of Edwin De T. Bechtel, by exchange, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.727.1,true,true,259797,Photographs,Photograph,"11:00 A.M. Monday, May 9th, 1910. Newsies at Skeeter's Branch, Jefferson near Franklin. They were all smoking. Location: St. Louis, Missouri.",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,"May 9, 1910",1910,1910,Gelatin silver print,9.1 x 11.9 cm (3 9/16 x 4 11/16 in.),"Gift of Phyllis D. Massar, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.553.7,false,true,301919,Photographs,Photograph,"Addie Card, 12 years. Spinner in North Pownal Cotton Mill. Girls in mill say she is ten years. She admitted to me she was twelve; that she started during school vacation and now would ""stay"". Location: Vermont",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1910,1910,1910,Gelatin silver print,Image: 17.7 x 12.7 cm (6 15/16 x 5 in.) Sheet: 16.8 x 11.9 cm (6 5/8 x 4 11/16 in.),"Gift of Joyce F. Menschel, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.553.8,false,true,301920,Photographs,Photograph,"Mill Children #440, South Carolina",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1908,1908,1908,Gelatin silver print,Image: 11.9 x 16.9 cm (4 11/16 x 6 5/8 in.),"Gift of Joyce F. Menschel, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.43.289,false,true,266949,Photographs,Photograph,Three National Child Labor Committee Exhibition Panels,,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1913–14,1913,1914,Gelatin silver print,8.9 x 15.0 cm (3 1/2 x 5 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.128,false,true,283260,Photographs,Photograph,"Newsboy asleep on stairs with papers, Jersey City, New Jersey",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,February 1912,1912,1912,Gelatin silver print,Image: 11.5 x 16.8 cm (4 1/2 x 6 5/8 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.730,false,true,285844,Photographs,Photograph,"Addie Card, 12 years. Spinner in North Pownal Cotton Mill. Girls in mill say she is ten years. She admitted to me she was twelve; that she started during school vacation and now would ""stay"". Location: Vermont",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,August 1910,1910,1910,Gelatin silver print,Image: 24.4 x 19.3 cm (9 5/8 x 7 5/8 in.) Sheet: 25.4 x 20.4 cm (10 x 8 1/16 in.),"Gilman Collection, Purchase, Anonymous Gifts, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.119,false,true,265154,Photographs,Photograph,"Icarus, Empire State Building",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1930,1930,1930,Gelatin silver print,18.7 x 23.7 cm (7 3/8 x 9 5/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.146,false,true,265184,Photographs,Photograph,Steamfitter,,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1921,1921,1921,Gelatin silver print,42.1 x 30.9 cm (16 9/16 x 12 3/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.325,false,true,265382,Photographs,Photograph,Empire State Building,,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1930s,1930,1939,Gelatin silver print,10.0 x 12.2 cm (3 15/16 x 4 13/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.486,false,true,265559,Photographs,Photograph,"Icarus, Empire State Building",,,,,,Artist,,Lewis Hine,"American, 1874–1940",,"Hine, Lewis",American,1874,1940,1930,1930,1930,Gelatin silver print,9.1 x 11.5 cm (3 9/16 x 4 1/2 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.660.11,false,true,259735,Photographs,Photograph,"Financial District, From the Hotel Bossert",,,,,,Artist,,Samuel H. Gottscho,"American, 1875–1971",,"Gottscho, Samuel H.",American,1875,1971,"1933, printed later",1933,1933,Gelatin silver print,16.7 x 24 cm (6 9/16 x 9 7/16 in. ),"Purchase, Florance Waterbury Bequest, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/259735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.96,false,true,265626,Photographs,Photograph,"[Locomotive, with Entrance to Perisphere of 1939 New York World's Fair in Background]",,,,,,Artist,,Samuel H. Gottscho,"American, 1875–1971",,"Gottscho, Samuel H.",American,1875,1971,ca. 1939,1937,1941,Gelatin silver print,33.0 x 25.4 cm (13 x 10 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.402,false,true,265468,Photographs,Photograph,"[Fountains, 1939 New York World's Fair, with Trylon and Perisphere in Background]",,,,,,Artist,,Samuel H. Gottscho,"American, 1875–1971",,"Gottscho, Samuel H.",American,1875,1971,ca. 1939,1937,1941,Gelatin silver print,31.7 x 25.4 cm (12 1/2 x 10 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.403,false,true,265469,Photographs,Photograph,"[1939 New York World's Fair, Entrance to Perisphere]",,,,,,Artist,,Samuel H. Gottscho,"American, 1875–1971",,"Gottscho, Samuel H.",American,1875,1971,ca. 1939,1937,1941,Gelatin silver print,31.4 x 25.5 cm. (12 3/8 x 10 1/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.404,false,true,265470,Photographs,Photograph,"Trylon and Perisphere, New York World's Fair",,,,,,Artist,,Samuel H. Gottscho,"American, 1875–1971",,"Gottscho, Samuel H.",American,1875,1971,ca. 1939,1937,1941,Gelatin silver print,33.0 x 21.6 cm (13 x 8 1/2 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.243,false,true,265292,Photographs,Photograph,[Old Man with Boy],,,,,,Artist,,Doris Ulmann,"American, 1882–1934",,"Ulmann, Doris",American,1882,1934,1920s–30s,1920,1939,Platinum print,20.4 x 15.4 cm (8 x 6 1/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.274,false,true,265326,Photographs,Photograph,[Bell Ringer Outside a Church],,,,,,Artist,,Doris Ulmann,"American, 1882–1934",,"Ulmann, Doris",American,1882,1934,1920s–30s,1920,1939,Platinum print,20.1 x 15.1 cm (7 15/16 x 5 15/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.276,false,true,265328,Photographs,Photograph,[Man with Bridle],,,,,,Artist,,Doris Ulmann,"American, 1882–1934",,"Ulmann, Doris",American,1882,1934,1920s–30s,1920,1939,Platinum print,20.4 x 15.1 cm (8 x 5 15/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.284,false,true,265336,Photographs,Photograph,The Corn Crib,,,,,,Artist,,Doris Ulmann,"American, 1882–1934",,"Ulmann, Doris",American,1882,1934,1918,1918,1918,Bromoil print,20.5 x 15.9 cm (8 1/16 x 6 1/4 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.271,false,true,265323,Photographs,Photograph,Grotesque Shadows,,,,,,Artist,,Roland E. Schneider,"American, 1884–1934",,"Schneider, Roland E.",American,1884,1934,1920s,1920,1929,Gelatin silver print,23.3 x 18.8 cm (9 3/16 x 7 3/8 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.390,false,true,284645,Photographs,Photograph,"Alabama Plow Girl, near Eutaw, Alabama",,,,,,Artist,,Dorothea Lange,"American, 1895–1965",,"Lange, Dorothea",American,1895,1965,1936,1936,1936,Gelatin silver print,19.1 x 19.4 cm (7 1/2 x 7 5/8 in. ),"Purchase, Alfred Stieglitz Society Gifts, 2001",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.351.3,false,true,284150,Photographs,Photograph,"[Migrant Pea Picker's Makeshift Home, Nipomo, California]",,,,,,Artist,,Dorothea Lange,"American, 1895–1965",,"Lange, Dorothea",American,1895,1965,February 1936,1936,1936,Gelatin silver print,18.2 x 24.5 cm (7 3/16 x 9 5/8 in. ),"Purchase, Jennifer and Joseph Duke Gift, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.296,false,true,284650,Photographs,Photograph,"[African American Mother and Child on Bed in their Cabin near Jefferson, Texas]",,,,,,Artist,,Russell Lee,"American, 1903–1986",,"Lee, Russell",American,1903,1986,1939,1939,1939,Gelatin silver print,18.2 x 24.3 cm (7 3/16 x 9 9/16 in. ),"Purchase, Alfred Stieglitz Society Gifts, 2001",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.297,false,true,284653,Photographs,Photograph,"[Cotton Pickers with Knee Pads, Lehi, Arkansas]",,,,,,Artist,,Russell Lee,"American, 1903–1986",,"Lee, Russell",American,1903,1986,1938,1938,1938,Gelatin silver print,16.5 x 24.2 cm (6 1/2 x 9 1/2 in. ),"Purchase, Alfred Stieglitz Society Gifts, 2001",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.298,false,true,284663,Photographs,Photograph,"[African-American Family at Gee's Bend, Alabama]",,,,,,Artist,,Arthur Rothstein,"American, 1915–1985",,"Rothstein, Arthur",American,1915,1985,1937,1937,1937,Gelatin silver print,18.1 x 24.1 cm (7 1/8 x 9 1/2 in. ),"Purchase, Alfred Stieglitz Society Gifts, 2001",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.64,false,true,265591,Photographs,Photograph,"Son of Sharecropper - Mississippi Country, Arkansas",,,,,,Artist,,Arthur Rothstein,"American, 1915–1985",,"Rothstein, Arthur",American,1915,1985,1935,1935,1935,Gelatin silver print,25.5 x 20.2 cm (10 1/16 x 7 15/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.478,false,true,299311,Photographs,Carte-de-visite,"Rebecca, Charley and Rosa, Slave Children from New Orleans",,,,,,Artist,,Myron H. Kimball,"American, active 1860s",,"Kimball, Myron H.",American,1860,1860,1863–64,1863,1864,Albumen silver print from glass negative,Image: 8.4 x 5.4 cm (3 5/16 x 2 1/8 in.) Mount: 10.1 x 6.2 cm (4 x 2 7/16 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.273,false,true,645493,Photographs,Photograph,"A Burial Party, Cold Harbor, Virginia.",,,,,,Artist,,John Reekie,"American, active 1860s",,"Reekie, John",American,1860,1869,April 1865,1865,1865,Albumen silver print from glass negative,Image: 7 × 9 in. (17.8 × 22.9 cm) Mount: 12 5/8 × 17 1/2 in. (32 × 44.4 cm),"Purchase, W. Bruce and Delaney H. Lundberg Gift and The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2014",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/645493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.36,false,true,685378,Photographs,Carte-de-visite,[William Holbrook Beard],,,,,,Artist,,J. T. Upson,"American, active 1860s",,"Upson, J. T.",American,1859,1870,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.50,false,true,685392,Photographs,Carte-de-visite,[Eugene Benson],,,,,,Artist,,Maurice Stadtfeld,"American, active 1860s",,"Stadtfeld, Maurice",American,1860,1860,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.92,false,true,283194,Photographs,Photograph,Emancipated Slaves Brought from Louisiana by Colonel George H. Banks,,,,,,Artist,,Myron H. Kimball,"American, active 1860s",,"Kimball, Myron H.",American,1860,1860,December 1863,1863,1863,Albumen silver print from glass negative,"Image: 13.2 x 18.3cm (5 3/16 x 7 3/16in.), oblong oval Mat: 19.9 x 25.2 cm (7 13/16 x 9 15/16 in.)","Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.286,false,true,685627,Photographs,Carte-de-visite,[George Augustus Baker],,,,,,Artist,,Maurice Stadtfeld,"American, active 1860s",,"Stadtfeld, Maurice",American,1860,1860,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.402,false,true,685742,Photographs,Carte-de-visite,[William John Hennessy],,,,,,Artist,,Maurice Stadtfeld,"American, active 1860s",,"Stadtfeld, Maurice",American,1860,1860,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/685742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.737,false,true,686076,Photographs,Carte-de-visite,[Morrell],,,,,,Artist,,Maurice Stadtfeld,"American, active 1860s",,"Stadtfeld, Maurice",American,1860,1860,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.815,false,true,686154,Photographs,Carte-de-visite,[A.H. Wenzler],,,,,,Artist,,Maurice Stadtfeld,"American, active 1860s",,"Stadtfeld, Maurice",American,1860,1860,1860s,1860,1869,Albumen silver print,Approx. 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/686154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1231,false,true,285894,Photographs,Photograph,"Duryea Zouaves, Fort Schuyler Adjuant Mess",,,,,,Artist,,Stacy,"American, active 1860s",,Stacy,American,1860,1869,"May 18, 1861",1861,1861,Albumen silver print from glass negative,5 9/16 x 7 1/2,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.457.3681,false,true,291274,Photographs,Photographs,Reception of the Japanese Embassy at the Battery,,,,,,Artist,Attributed to,George Stacy,"American, active 1860s",,"Stacy, George",American,1860,1860,1860,1860,1860,Albumen silver print from glass negative,"Image: 7.4 x 14.6 cm (2 15/16 x 5 3/4 in.), arch-topped Mount: 18.4 x 17.5 cm (7 1/4 x 6 7/8 in.)","Herbert Mitchell Collection, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.386,false,true,285781,Photographs,Photograph,James Hyatt Inhaling Chlorine Gas,,,,,,Artist,,Peter Welling,"American, active c. 1850s",,"Welling, Peter",American,1850,1859,1850–55,1850,1855,Daguerreotype,Image: 2 11/16 × 2 3/16 in. (6.9 × 5.6 cm); visible Overall: 3 5/8 × 3 1/8 in. (9.2 × 8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.329,false,true,285986,Photographs,Photographs,"The Lincoln Funeral Train, Philadelphia",,,,,,Artist,,Charles L. Philippi,"American, active 1861–74",,"Philippi, Charles L.",American,1861,1874,"April 22–24, 1865",1865,1865,Albumen silver print from glass negative,Image: 24 x 33.8 cm (9 7/16 x 13 5/16 in.) Mount: 26.5 x 34.8 cm (10 7/16 x 13 11/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.124,false,true,283717,Photographs,Photograph,"Gibson's Breaker, Rushdale, Pennsylvania",,,,,,Artist,,Thomas H. Johnson,"American, active 1860s–70s",,"Johnson, Thomas H.",American,1860,1870,1860s,1860,1869,Albumen silver print from glass negative,30.3 x 38.7 cm (11 15/16 x 15 1/4 in. ),"Purchase, Alfred Stieglitz Society Gifts, 2000",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1145,false,true,286517,Photographs,Cabinet card,Musical Mokes,,,,,,Artist,,J. Wood,"American, active 1870s–80s",,"Wood, J.",American,1870,1889,1860s,1860,1869,Albumen silver print from glass negative,Image: 5 7/8 × 3 7/8 in. (14.9 × 9.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1142,false,true,286633,Photographs,Photograph,12 O'clock in the Deadening,,,,,,Artist,,John Horgan Jr.,"American, active 1880s–90s",,"Horgan Jr, John",American,1880,1899,ca. 1891,1889,1893,Albumen silver print from glass negative,Image: 17 in. × 19 1/4 in. (43.2 × 48.9 cm) Mount: 20 1/4 × 24 3/16 in. (51.5 × 61.5 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.41,false,true,291794,Photographs,Daguerreotype,[Young Man],,,,,,Artist,,Knickerbocker Gallery,"American, active ca. 1841–59",,Knickerbocker Gallery,American,1841,1859,1850s,1850,1859,Daguerreotype,Image: 7 x 5.7 cm (2 3/4 x 2 1/4 in.) Plate: 8.3 x 7 cm (3 1/4 x 2 3/4 in.) Case: 1.6 x 9.2 x 7.9 cm (5/8 x 3 5/8 x 3 1/8 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.477,false,true,299310,Photographs,Carte-de-visite,"Learning is Wealth—Wilson, Charley, Rebecca, and Rosa, Slaves from New Orleans",,,,,,Artist,,Charles Paxson,"American, active New York, 1860s",,"Paxson, Charles",American,1860,1869,1863–64,1863,1864,Albumen silver print from glass negative,Image: 8.5 x 5.3 cm (3 3/8 x 2 1/16 in.) Mount: 10.1 x 6.1 cm (4 x 2 3/8 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.1914,false,true,288312,Photographs,Stereographs,"German's Day, California Midwinter Exposition",,,,,,Artist,,Kilburn Brothers,"American, active ca. 1865–1890",,Kilburn Brothers,American,1863,1892,1850s–1910s,1850,1919,Albumen silver prints,Mount: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.454.2,false,true,285488,Photographs,Photograph,"[Interborough Rapid Transit (IRT) Construction, Broadway Looking North at 101st Street, New York City]",,,,,,Artist,,William B.,"American, active ca. 1900–1939",,"B., William",American,1900,1939,1900,1900,1900,Platinum print,Image: 19 x 24 cm (7 1/2 x 9 7/16 in.),"Purchase, Marlene Nathan Meyerson Family Foundation Gift, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.454.10,false,true,285486,Photographs,Photograph,"[Interborough Rapid Transit (IRT) Construction, 25th Street and Fourth Avenue, New York City]",,,,,,Artist,,W. R. C.,"American, active ca. 1900s–1930s",,"C., W. R.",American,1900,1939,1906,1906,1906,Platinum print,Image: 19 x 24 cm (7 1/2 x 9 7/16 in.),"Purchase, Marlene Nathan Meyerson Family Foundation Gift, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.670.2,false,true,269959,Photographs,Photograph,"[Stern-Wheeler Arriving at Silver Springs, Florida, after an Overnight Run up the St. Johns, Oklawaha, & Silver Rivers]",,,,,,Artist,,George Barker,"American, born Canada, 1844–1894",,"Barker, George",American,1844,1894,1886,1886,1886,Albumen silver print from glass negative,41.3 x 51.3 cm. (16 1/4 x 20 3/16 in.),"Gift of A. Hyatt Mayor, 1957",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.50,false,true,289322,Photographs,Photograph,Canton City,,,,,,Artist,,Milton M. Miller,"American, active China, 1830–1899",,"Miller, Milton",American,1830,1899,ca. 1869,1869,1869,Albumen silver print from glass negative,Image: 8 1/4 × 11 7/16 in. (20.9 × 29 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.54,false,true,289325,Photographs,Photograph,Mandarin Wife,,,,,,Artist,,Milton M. Miller,"American, active China, 1830–1899",,"Miller, Milton",American,1830,1899,1860–1863,1860,1863,Albumen silver print from glass negative,Image: 9 in. × 6 3/4 in. (22.9 × 17.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.55,false,true,289326,Photographs,Photograph,Madarin Wife,,,,,,Artist,,Milton M. Miller,"American, active China, 1830–1899",,"Miller, Milton",American,1830,1899,1861–1863,1861,1863,Albumen silver print from glass negative,Image: 11 1/8 × 8 3/4 in. (28.3 × 22.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.494.57,false,true,289328,Photographs,Photograph,Mandarin with Family,,,,,,Artist,,Milton M. Miller,"American, active China, 1830–1899",,"Miller, Milton",American,1830,1899,1860–1863,1860,1863,Albumen silver print from glass negative,Image: 8 3/16 × 11 1/8 in. (20.8 × 28.2 cm),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/289328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.251,false,true,285438,Photographs,Photograph,"[Waterfall, Constantine]",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1856,1856,1856,Salted paper print from paper negative,Image: 23.3 x 30.1 cm (9 3/16 x 11 7/8 in.),"Purchase, Alfred Stieglitz Society Gifts, Anonymous Foundation Gift, W. Bruce and Delaney H. Lundberg Gift, and Marian and James H. Cohen Gift, in memory of their son, Michael Harrison Cohen, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.1063,true,true,266121,Photographs,Photograph,Medinet-Habu,,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1854,1854,1854,Salted paper print from paper negative,23.4 x 30.1 cm (9 3/16 x 11 7/8 in.),"Purchase, The Howard Gilman Foundation Gift, 1989",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.63,false,true,283148,Photographs,Photograph,[The Nile in front of the Theban Hills],,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,Mount: 18 7/16 × 24 1/8 in. (46.9 × 61.2 cm) Image: 8 3/4 × 11 7/8 in. (22.3 × 30.2 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.64,false,true,283149,Photographs,Photograph,Dakkeh,,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,Image: 9 3/16 × 11 7/8 in. (23.4 × 30.2 cm) Mount: 18 1/2 × 24 1/8 in. (47 × 61.2 cm),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.276,false,true,286665,Photographs,Photograph,[Excavations near the Sphinx],,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853,1853,1853,Salted paper print from paper negative,Image: 22.3 x 30.2 cm (8 3/4 x 11 7/8 in.) Mount: 18 11/16 × 24 5/16 in. (47.5 × 61.8 cm),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.759,false,true,286131,Photographs,Photograph,"Etude de Palmiers, Bords du Nil, Kalabschi",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,9 1/4 x 11 3/4,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.760,false,true,286130,Photographs,Photograph,[Island of Philae],,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,9 x 11 3/4,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.761,false,true,286135,Photographs,Photograph,Temple de Deboud,,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,9 1/8 x 11,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.762,false,true,286690,Photographs,Photograph,"[Boat in Harbor, Algeria]",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,8 7/8 x 11 1/8,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.763,false,true,286689,Photographs,Photograph,"[Antiquities in the Museum at Cherchell, Algeria]",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,Image: 11 7/16 × 8 15/16 in. (29.1 × 22.7 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.764,false,true,286688,Photographs,Photograph,"[Constantine, Algeria]",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,Image: 9 3/16 × 11 13/16 in. (23.4 × 30 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.765,false,true,286129,Photographs,Photograph,"[Fragment of an Egyptian Statue in the Museum at Cherchell, Algeria]",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1856,1856,1856,Salted paper print from paper negative,11 3/8 x 9 1/8,"Gilman Collection, Purchase, Mr. and Mrs. Henry R. Kravis Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.766,false,true,286124,Photographs,Photograph,"[Tents, Algeria]",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1856,1856,1856,Salted paper print from paper negative,Image: 8 9/16 × 11 3/4 in. (21.7 × 29.8 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.783,false,true,286094,Photographs,Photograph,"[Bank of the Rhumel, near Constantine, Algeria]",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,ca. 1855–56,1853,1858,Salted paper print from paper negative,9 3/8 x 12 1/8,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.849,false,true,285974,Photographs,Photograph,"Amada, Temple",,,,,,Artist,,John Beasley Greene,"American, active France, 1832–1856",,"Greene, John Beasley",American,1832,1856,1853–54,1853,1854,Salted paper print from paper negative,Image: 8 3/4 × 11 3/4 in. (22.3 × 29.9 cm) Mount: 18 9/16 × 24 1/8 in. (47.1 × 61.2 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.28,false,true,291781,Photographs,Daguerreotype,[Seated Elderly Woman Wearing Plaid Dress and Bonnet],,,,,,Artist,,William Hardy Kent,"American, England 1819–1907 England",,"Kent, William Hardy",American,1819,1819,1854–60,1854,1860,Daguerreotype,Image: 9.2 x 6.4 cm (3 5/8 x 2 1/2 in.) Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.) Frame: 11.7 x 9.4 cm (4 5/8 x 3 11/16 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.29,false,true,291782,Photographs,Daguerreotype,[Seated Middle-aged Woman Dressed in Finery],,,,,,Artist,,William Hardy Kent,"American, England 1819–1907 England",,"Kent, William Hardy",American,1819,1819,1854–60,1854,1860,Daguerreotype,Image: 9 x 6.3 cm (3 9/16 x 2 1/2 in.) Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.) Frame: 12.1 x 9.2 cm (4 3/4 x 3 5/8 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.66,false,true,267861,Photographs,Photograph,Song of the Lily,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1897,1897,1897,Platinum print,17.0 x 12.1 cm. (6 11/16 x 4 3/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.68,false,true,267863,Photographs,Photograph,Frau Willi Geiger (Clara),,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,11.9 x 17.0 cm. (4 11/16 x 6 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.69,false,true,271730,Photographs,Photograph,Brigitta Wenz,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900,1900,1900,Platinum print,12.0 x 16.8 cm. (4 3/4 x 6 5/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.70,false,true,267865,Photographs,Photograph,Summer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1898,1898,1898,Platinum print,17.7 x 6.3 cm. (7 x 2 1/2 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.74,false,true,271746,Photographs,Photograph,Dr. Emanuel Lasker and His Brother,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1907,1907,1907,Platinum print,15.5 x 12.1 cm. (6 1/8 x 4 3/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.76,false,true,271741,Photographs,Photograph,The Man in Armor (Self-Portrait),,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1898,1898,1898,Platinum print,17.3 x 11.9 cm. (6 13/16 x 4 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.87,false,true,271733,Photographs,Photograph,"Stieglitz, Steichen and Kuehn Admiring the Work of Frank Eugene",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1907,1907,1907,Platinum print,12.1 x 16.8 cm. (4 3/4 x 6 5/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.635.7,false,true,271750,Photographs,Photograph,Alfred Stieglitz,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1907,1907,1907,Platinum print,16.5 x 11.8 cm. (6 1/2 x 4 5/8 in.),"Alfred Stieglitz Collection, 1955",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.33,false,true,260240,Photographs,Photograph,Adam and Eve,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,"1900s, printed 1909",1900,1909,Photogravure,17.8 x 12.8 cm. (7 x 5 1/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.61,false,true,260272,Photographs,Photograph,Miss Gladys Lawrence - The Seashell,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1910–13,1910,1913,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.63,false,true,260274,Photographs,Photograph,The Guitar Player,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,ca. 1908,1906,1910,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.64,false,true,260275,Photographs,Photograph,The Oriental Bride,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.65,false,true,260276,Photographs,Photograph,Joachim's Daughter,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1899,1899,1899,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260276,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.66,false,true,260277,Photographs,Photograph,Snakecharmer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.67,false,true,260278,Photographs,Photograph,Snakecharmer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.68,false,true,260279,Photographs,Photograph,Miss Nan N. - Indian Festival,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.69,false,true,260280,Photographs,Photograph,The Misses Ide in Samoa,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,ca. 1908,1906,1910,Platinum print,11.8 x 16.1 cm (4 5/8 x 6 5/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.70,false,true,260282,Photographs,Photograph,Miss Ide,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1890–1903,1890,1903,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.71,false,true,260283,Photographs,Photograph,Baroness von W. of Vienna,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.72,false,true,260284,Photographs,Photograph,Mirzl Wach. The Sister of Charity,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.73,false,true,260285,Photographs,Photograph,Emmy G.,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.74,false,true,260286,Photographs,Photograph,Emmy Geiger,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,11.9 x 16.4 cm (4 11/16 x 6 7/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.75,false,true,260287,Photographs,Photograph,Emmy G.,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.76,false,true,260288,Photographs,Photograph,Mrs. Wilm,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1909,1909,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.78,false,true,260290,Photographs,Photograph,Lisl Bosse,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.79,false,true,260291,Photographs,Photograph,Hortensia,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1898,1898,1898,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.81,false,true,260294,Photographs,Photograph,Miss Lilian C. Wiver in Her Workshop,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.82,false,true,260295,Photographs,Photograph,Miss Lilian C. Wiver with Her Angora,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,Image: 16.7 × 12 cm (6 9/16 × 4 3/4 in.) Mount: 25.3 × 20 cm (9 15/16 × 7 7/8 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.83,false,true,260296,Photographs,Photograph,Thilda H. - The Veiled Lady,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.84,false,true,260297,Photographs,Photograph,Sweet Alice (Ben Bolt),,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.85,false,true,260298,Photographs,Photograph,The Painter's Wife,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.86,false,true,260299,Photographs,Photograph,The Diva at Home,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.87,false,true,260300,Photographs,Photograph,The Diva and Her Most Trusty Friend and Companion,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.89,false,true,260302,Photographs,Photograph,On the Wabash (Miss D.),,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.90,false,true,260304,Photographs,Photograph,Frau Clara G.,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.91,false,true,260305,Photographs,Photograph,Miss L.L.L.,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.92,false,true,260306,Photographs,Photograph,Lise Lotte Lindström,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.93,false,true,260307,Photographs,Photograph,Baroness Haltvayne,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.94,false,true,260308,Photographs,Photograph,Maria von Seidl,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.95,false,true,260309,Photographs,Photograph,Marie Struthers,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1890–1903,1890,1903,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.96,false,true,260310,Photographs,Photograph,The Pearl Necklace,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.97,false,true,260311,Photographs,Photograph,Dolly Varden,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.98,false,true,260312,Photographs,Photograph,Dolly Varden,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.99,false,true,260313,Photographs,Photograph,Lydia Leslie Lydie - Candlestick,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260313,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.100,false,true,260106,Photographs,Photograph,Fritzi von Derra - The Exotic Dancer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.101,false,true,260107,Photographs,Photograph,Fritzi von Derra - Greek Dancer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.102,false,true,260108,Photographs,Photograph,Fritzi von Derra - The Greek Dancer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.103,false,true,260109,Photographs,Photograph,Fritzi von Derra - The Greek Dancer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.104,false,true,260110,Photographs,Photograph,Fritzi von Derra - The Greek Dancer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.105,false,true,260111,Photographs,Photograph,Fritzi von Derra - The Oriental Dancer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.106,false,true,260112,Photographs,Photograph,The Baronin B. and Miss M. - Rosenkavalier,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.107,false,true,260113,Photographs,Photograph,"""Fisherman's Luck"" - Henry Heyligers and Wife",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.108,false,true,260114,Photographs,Photograph,Frau Frieda and Franz S,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.109,false,true,260115,Photographs,Photograph,Four Sisters,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.110,false,true,260117,Photographs,Photograph,The Cake Walk,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.111,false,true,260118,Photographs,Photograph,Ritual Vestalis,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.112,false,true,260119,Photographs,Photograph,Slumbering Maidens,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.113,false,true,260120,Photographs,Photograph,The Graduating Class,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1913,1913,1913,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.114,false,true,260121,Photographs,Photograph,"Misses Weaver H. Patties' School for Girls, Munich",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.115,false,true,260122,Photographs,Photograph,Marie R. and Cryma,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.116,false,true,260123,Photographs,Photograph,SKH Prinzregent Ludwig von Bayern,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912–13,1912,1913,Platinum print,23.1 x 15.8 cm (9 1/16 x 6 3/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.117,false,true,260124,Photographs,Photograph,Crown Prince Ludwig III of Bavaria,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.118,false,true,260125,Photographs,Photograph,Crown Prince Rupprecht of Bavaria,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.120,false,true,260128,Photographs,Photograph,H.R.H. Prince Leopold and Prince Johann Albrecht of Bavaria,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.121,false,true,260129,Photographs,Photograph,H.R.H. Prince Leopold of Bavaria,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.122,false,true,260130,Photographs,Photograph,H.R.H. Prince Leopold and His Hobbyhorse,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.123,false,true,260131,Photographs,Photograph,"H.R.H. Prince Albrecht Johann. ""With Neither Crown Nor Scepter""",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.124,false,true,260132,Photographs,Photograph,"H.R.H. King Friedrich August of Saxony, Taken Shortly Before Dethronement",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,ca. 1913,1911,1915,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.125,false,true,260133,Photographs,Photograph,Dr. Paul Heyse,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1914,1900,1914,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.126,false,true,260134,Photographs,Photograph,"Sigmund von Hausegger, Kapellmeister",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1910–24,1910,1924,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.127,false,true,260135,Photographs,Photograph,Professor Fritz von Miller,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1910–13,1910,1913,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.128,false,true,260136,Photographs,Photograph,Professor Georg Witkowski,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.129,false,true,260137,Photographs,Photograph,Jesko von Puttkamer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1910–23,1910,1923,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.131,false,true,260139,Photographs,Photograph,Joseph Pennell,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1910–24,1910,1924,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.132,false,true,260140,Photographs,Photograph,"Stieglitz, Steichen, Smith and Kuehn Admiring the Work of Frank Eugene",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1907,1907,1907,Platinum print,Image: 10.2 x 16.2 cm (4 x 6 3/8 in.) Mount: 25.2 x 17.5 cm (9 15/16 x 6 7/8 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.133,false,true,260141,Photographs,Photograph,Josef Geis as Beckmeser,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.134,false,true,260142,Photographs,Photograph,[Farmyard],,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1886,1886,1886,Gum bichromate print,14.3 x 19.4 cm (5 5/8 x 7 5/8 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.135,false,true,260143,Photographs,Photograph,[Landscape with River and Trees],,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1886,1886,1886,Gum bichromate print,19.3 x 16.0 cm (7 5/8 x 6 5/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.137,false,true,260145,Photographs,Photograph,Frank Jefferson,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1898,1898,1898,Platinum print,Image: 16.8 × 11.8 cm (6 5/8 × 4 5/8 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.138,false,true,260146,Photographs,Photograph,The Man in Armor,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1898,1898,1898,Platinum print,17.6 x 12.9 cm (6 15/16 x 5 1/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.140,false,true,260149,Photographs,Photograph,Alfred Stieglitz,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1890s,1890,1899,Platinum print,16.4 x 11.0 cm (6 7/16 x 4 5/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.143,false,true,260152,Photographs,Photograph,Fredy,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1901,1901,1901,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.144,false,true,260153,Photographs,Photograph,Fredy,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1901,1901,1901,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.145,false,true,260154,Photographs,Photograph,Fredy,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1901,1901,1901,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.146,false,true,260155,Photographs,Photograph,Fredy,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1901,1901,1901,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.149,false,true,260158,Photographs,Photograph,Emanuel von Seidl,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1908,1900,1908,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.150,false,true,260160,Photographs,Photograph,The Studio,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1910s,1910,1919,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.151,false,true,260161,Photographs,Postcard,The Studio,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1910s,1910,1919,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.154,false,true,260164,Photographs,Photograph,[Anna Königer Smith?],,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.155,false,true,260165,Photographs,Photograph,Gustel Königer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1909,1909,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.156,false,true,260166,Photographs,Photograph,Gustel Königer,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1909,1909,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.157,false,true,260167,Photographs,Photograph,Anne,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1911,1911,1911,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.158,false,true,260168,Photographs,Photograph,Friedel Wearing a Kimono,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1911,1911,1911,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.159,false,true,260169,Photographs,Photograph,Friedel Wearing a Kimono,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1911,1911,1911,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.160,false,true,260171,Photographs,Photograph,Friedel Wearing a Kimono,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1911,1911,1911,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.161,false,true,260172,Photographs,Photograph,Ellen,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1909,1909,1909,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.162,false,true,260173,Photographs,Photograph,Anne Köninger and Frederick L. Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.164,false,true,260175,Photographs,Photograph,Anne Königer and Frederick L. Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.166,false,true,260177,Photographs,Photograph,Anne Königer and Frederick L. Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.167,false,true,260178,Photographs,Photograph,Anne Königer and Frederick L. Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.168,false,true,260179,Photographs,Photograph,Kal Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,17.0 x 11.8 cm (6 11/16 x 4 5/8 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.169,false,true,260180,Photographs,Photograph,Kal Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.170,false,true,260182,Photographs,Photograph,"Count and Countes LaRosée, Bride and Bridegroom",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900–1911,1900,1911,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.171,false,true,260183,Photographs,Photograph,Johanna,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1919,1919,1919,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.172,false,true,260184,Photographs,Photograph,Johanna,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1921,1921,1921,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.173,false,true,260185,Photographs,Photograph,Frank Eugene with Herr von Martine and Others,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1920,1920,1920,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.174,false,true,260186,Photographs,Photograph,Self-Portrait with Dolls,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1923,1923,1923,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.175,false,true,260187,Photographs,Photograph,"Learning How to ""Sit Up!""",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1900s,1900,1909,Platinum print,Image: 9.5 x 4.4 cm (3 3/4 x 1 3/4 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.177,false,true,260189,Photographs,Photograph,Self-Portrait,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1924,1924,1924,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.179,false,true,260191,Photographs,Photograph,"Mein Grossvater Selinger, 93 Jahre",,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1921,1921,1921,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.191,false,true,260205,Photographs,Photograph,Frank Eugene,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1936,1936,1936,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260205,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.192,false,true,260206,Photographs,Photograph,Frank Eugene,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1936,1936,1936,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.195,false,true,260209,Photographs,Photograph,House and Church in Germany,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1920,1920,1920,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.196,false,true,260210,Photographs,Photograph,Anne Königer Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,1912,1912,1912,Platinum print,,"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.633.197,false,true,260211,Photographs,Photograph,Frederick L. Smith,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,ca. 1915,1915,1915,Platinum print,16.2 x 11.3 cm (6 3/8 x 4 7/16 in.),"Rogers Fund, 1972",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/260211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.721,false,true,286684,Photographs,Photograph,The Great White Cloud,,,,,,Artist,,Frank Eugene,"American, New York 1865–1936 Munich",,"Eugene, Frank",American,1865,1936,ca. 1910,1908,1912,Platinum print,Image: 11.6 × 16.1 cm (4 9/16 × 6 5/16 in.) Mount: 34 × 26.9 cm (13 3/8 × 10 9/16 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.679.1805,false,true,269660,Photographs,Photograph,Apple Blossoms,,,,,,Artist,,Louis Comfort Tiffany,"American, New York 1848–1933 New York",,"Tiffany, Louis Comfort",American,1848,1933,1890s–1900s,1890,1909,Albumen silver print,,"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1953",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.34,false,true,291651,Photographs,Photograph,"Artillery, Quartermaster Sergeant",,,,,,Artist,Attributed to,Oliver H. Willard,"American, active 1850s–70s, died 1875",,"Willard, Oliver H.",American,1775,1875,1866,1866,1866,Albumen silver print from glass negative,Image: 20.3 × 14.8 cm (8 × 5 13/16 in.) Mount: 33.3 x 25.9 cm (13 1/8 x 10 3/16 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.35,false,true,291652,Photographs,Photograph,"Artillery, Musician",,,,,,Artist,Attributed to,Oliver H. Willard,"American, active 1850s–70s, died 1875",,"Willard, Oliver H.",American,1775,1875,1866,1866,1866,Albumen silver print from glass negative,Image: 19.9 x 14.9 cm (7 13/16 x 5 7/8 in.) Mount: 33.3 x 25.9 cm (13 1/8 x 10 3/16 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.36,false,true,291653,Photographs,Photograph,"Light Artillery, Sergeant Major",,,,,,Artist,Attributed to,Oliver H. Willard,"American, active 1850s–70s, died 1875",,"Willard, Oliver H.",American,1775,1875,1866,1866,1866,Albumen silver print from glass negative,Image: 20.3 x 15.2 cm (8 x 6 in.) Mount: 33.3 x 25.7 cm (13 1/8 x 10 1/8 in.),"Purchase, Saundra B. Lane Gift, in honor of Charles Isaacs, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.37,false,true,291654,Photographs,Photograph,"Ordnance, Private",,,,,,Artist,Attributed to,Oliver H. Willard,"American, active 1850s–70s, died 1875",,"Willard, Oliver H.",American,1775,1875,1866,1866,1866,Albumen silver print from glass negative,Image: 20.2 x 15 cm (7 15/16 x 5 7/8 in.) Mount: 33.3 x 25.9 cm (13 1/8 x 10 3/16 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.38,false,true,291655,Photographs,Photograph,"Fatigue, Marching Order",,,,,,Artist,Attributed to,Oliver H. Willard,"American, active 1850s–70s, died 1875",,"Willard, Oliver H.",American,1775,1875,1866,1866,1866,Albumen silver print from glass negative,Image: 20.1 x 15 cm (7 15/16 x 5 7/8 in.) Mount: 33.2 x 25.7 cm (13 1/16 x 10 1/8 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2010",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.185,false,true,282767,Photographs,Photograph,[Man Holding Patent Office Book],,,,,,Artist,Attributed to,Oliver H. Willard,"American, active 1850s–70s, died 1875",,"Willard, Oliver H.",American,1775,1875,ca. 1857,1856,1858,Salted paper print from glass negative,21.6 x 16.5 cm (8 1/2 x 6 1/2 in. ),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1999",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.676,false,true,271672,Photographs,Cabinet card,[The British Soprano Euphrosyne Parepa-Rosa (1836-1874)],,,,,,Artist,,Jeremiah Gurney,"American, 1812–1895 Coxsackie, New York",,"Gurney, Jeremiah",American,1812,1895-04-21,1870s,1870,1879,Albumen silver print from glass negative,13.3 x 8.3 cm (5 1/4 x 3 1/4 in. ),Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +X.701.4,false,true,271712,Photographs,Cabinet card,"[Wohes Family, New York]",,,,,,Artist,,Jeremiah Gurney,"American, 1812–1895 Coxsackie, New York",,"Gurney, Jeremiah",American,1812,1895-04-21,1870s,1870,1870,Albumen silver print from glass negative,13.7 x 9.2 cm. (5 3/8 x 3 5/8 in.),Museum Accession,,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.23,false,true,291776,Photographs,Daguerreotype,[Pair of Portraits of Man and Woman (Husband and Wife?)],,,,,,Artist,,Jeremiah Gurney,"American, 1812–1895 Coxsackie, New York",,"Gurney, Jeremiah",American,1812,1895-04-21,1852–60,1852,1860,Daguerreotype,"Image: 9 x 6.4 cm (3 9/16 x 2 1/2 in.), each Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.), each Case: 1.9 x 11.9 x 9.5 cm (3/4 x 4 11/16 x 3 3/4 in.)","Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.325,false,true,285472,Photographs,Photograph,[Two Girls in Identical Dresses],,,,,,Artist,,Jeremiah Gurney,"American, 1812–1895 Coxsackie, New York",,"Gurney, Jeremiah",American,1812,1895-04-21,ca. 1857,1852,1863,Daguerreotype,Image: 11.3 x 8.2 cm (4 7/16 x 3 1/4 in.),"Gilman Collection, Purchase, Marlene Nathan Meyerson Family Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1136,false,true,286062,Photographs,carte-de-visite,"James B. ""Wild Bill"" Hickock",,,,,,Artist,,Jeremiah Gurney,"American, 1812–1895 Coxsackie, New York",,"Gurney, Jeremiah",American,1812,1895-04-21,ca. 1873,1871,1875,Albumen silver print from glass negative,Image: 3 5/8 × 2 1/4 in. (9.2 × 5.7 cm) Mount: 4 1/8 × 2 1/2 in. (10.4 × 6.3 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.48,false,true,301954,Photographs,Photograph,"[Camp Scene with Soldiers of the 22nd New York State Militia, Harper's Ferry, Virginia]",,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862,1862,1862,Albumen silver print from glass negative,Image: 8.7 x 5.8 cm (3 7/16 x 2 5/16 in.) Mount: 10.2 x 6.1 cm (4 x 2 3/8 in.),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/301954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.517.4,false,true,269852,Photographs,Photograph,President Martin Van Buren,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1855–58,1855,1858,Salted paper print from glass negative,48.3 x 39.7 cm (19 x 15 5/8 in.),"David Hunter McAlpin Fund, 1956",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.382.48,false,true,282053,Photographs,Photograph,Grenville Kane,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,late 1850s,1857,1859,Ambrotype,visible: 12.2 x 8.9 cm (4 13/16 x 3 1/2 in.),"The Rubel Collection, Purchase, Lila Acheson Wallace Gift, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.84,false,true,283184,Photographs,Photograph,[Commodore Matthew Calbraith Perry],,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1856–58,1856,1858,Salted paper print from glass negative,Image : 33.1 x 28.4cm (13 1/16 x 11 3/16in.) Mount: 18 1/8 in. × 14 in. (46 × 35.5 cm),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.85,false,true,283186,Photographs,Photograph,[Portrait of a Man],,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1857,1855,1859,Salted paper print from glass negative,Mount: 11 3/16 in. × 9 1/8 in. (28.4 × 23.2 cm) Image: 9 15/16 × 7 9/16 in. (25.3 × 19.2 cm),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.280,false,true,286046,Photographs,Photographs,Lilliputian Souvenir,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862–63,1862,1863,Albumen silver print from glass negative,Image: 7.7 x 4.5 cm (3 1/16 x 1 3/4 in.) each Mount: 8.8 x 5.2 cm (3 7/16 x 2 1/16 in.) each Mount (2nd): 51.7 x 41.7 cm (20 3/8 x 16 7/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.496,false,true,286624,Photographs,Photograph,Peter Force,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1858,1856,1860,Salted paper print from glass negative,Image: 34.4 × 27.2 cm (13 9/16 × 10 11/16 in.) Mount: 50.4 × 47.3 cm (19 13/16 × 18 5/8 in.),"Gilman Collection, Purchase, Joseph M. Cohen Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.567,false,true,285877,Photographs,Photograph,Madame Medori,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1857,1855,1859,Salted paper print from glass negative,Image: 20.6 x 15.6 cm (8 1/8 x 6 1/8 in.),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.754,false,true,286586,Photographs,Cabinet card,Frederick Douglass,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1880,1878,1882,Albumen silver print from glass negative,Image: 14.7 × 10.2 cm (5 13/16 × 4 in.) Mount: 16.5 × 10.8 cm (6 1/2 × 4 1/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1108,false,true,286620,Photographs,Photograph,"Lieutenent General Scott, General-in-Chief U.S. Army, & Staff",,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,"September 6, 1861",1861,1861,Albumen silver print from glass negative,Image: 26.4 × 36.5 cm (10 3/8 × 14 3/8 in.) Mount: 35.6 × 43.1 cm (14 × 16 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1115,false,true,286097,Photographs,Photograph,"[Japanese Embassy, Navy Yard, Washington, DC]",,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1860,1860,1860,Albumen silver print from glass negative,Image: 37.5 x 48.1 cm (14 3/4 x 18 15/16 in.) Mount: 41.8 x 49.6 cm (16 7/16 x 19 1/2 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1125,false,true,285889,Photographs,Photograph,"Second Corps Hospital, Washington, D.C.",,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1863,1861,1865,Albumen silver print from glass negative,Image: 7.7 × 9.7 cm (3 1/16 × 3 13/16 in.) Mount: 22.9 × 26.8 cm (9 × 10 9/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1129,false,true,285835,Photographs,Photograph,"Fortifications, Manassas, Occupied by 13th Mass.",,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862,1862,1862,Albumen silver print from glass negative,Image: 7 5/16 × 9 1/4 in. (18.6 × 23.5 cm) Mount: 10 5/8 × 13 5/8 in. (27 × 34.6 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1130,false,true,286262,Photographs,Photograph,Ruins of Stone Bridge - Bull Run,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862,1862,1862,Albumen silver print from glass negative,Image: 18.8 × 23.1 cm (7 3/8 × 9 1/8 in.) Mount: 26.9 × 34.7 cm (10 9/16 × 13 11/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1131,false,true,285834,Photographs,Photograph,Stone Bridge - Bull Run,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1862,1862,1862,Albumen silver print from glass negative,Image: 18.4 × 23.1 cm (7 1/4 × 9 1/8 in.) Mount: 27 × 34.7 cm (10 5/8 × 13 11/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1213,false,true,286582,Photographs,Photograph,General Robert E. Lee,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1865,1865,1865,Albumen silver print from glass negative,Image: 14 × 9.3 cm (5 1/2 × 3 11/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1226,false,true,286343,Photographs,Photograph,Major General David E. Twiggs,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1859,1857,1861,Salted paper print from glass negative,Image: 22.5 × 14.9 cm (8 7/8 × 5 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1233,false,true,286629,Photographs,Photograph,Edward Everett,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1858,1853,1863,Salted paper print from glass negative,Image: 47.1 × 38.5 cm (18 9/16 × 15 3/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1234,false,true,285959,Photographs,Photograph,General William Ward and Staff,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,ca. 1861,1856,1866,Albumen silver print from glass negative,Image: 24.6 × 19 cm (9 11/16 × 7 1/2 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1218a,false,true,286612,Photographs,Photographs,Robert E. Lee,,,,,,Artist,,Mathew B. Brady,"American, born Ireland, 1823?–1896 New York",,"Brady, Mathew B.",American,1823,1896,1869,1869,1869,Albumen silver print from glass negative,"Mount: 32.1 × 30.6 cm (12 5/8 × 12 1/16 in.) Image: 20.6 × 15.6 cm (8 1/8 × 6 1/8 in.), oval","Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.126,false,true,267441,Photographs,Multiple exposure; Photograph,Dorothy True,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1919,1919,1919,Gelatin silver print,24.3 x 19.3 cm (9 9/16 x 7 5/8 in.),"Gift of Paul Rosenfeld, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.129,false,true,267460,Photographs,Photograph,Georgia O'Keeffe – Hands and Thimble,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1919,1919,1919,Palladium print,23.5 x 18.4 cm (9 1/4 x 7 1/4 in.),"Gift of Mrs. Rebecca S. Strand, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.6,false,true,269458,Photographs,Photograph,The Terminal,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1893, printed 1920s–30s",1893,1893,Gelatin silver print,8.9 x 11.5 cm (3 1/2 x 4 1/2 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.7,false,true,269459,Photographs,Photograph,An Icy Night,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1898, printed 1920–1939",1898,1898,Gelatin silver print from glass negative,9.2 x 11.8 cm. (3 5/8 x 4 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.8,false,true,269460,Photographs,Photograph,"The Street, Fifth Avenue",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1900–1901, printed 1903–4",1900,1901,Photogravure,30.6 x 23.3 cm. (12 1/16 x 9 3/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.9,false,true,269461,Photographs,Photograph,The Hand of Man,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1902, printed 1910",1902,1902,Photogravure,24.2 x 31.9 cm (9 1/2 x 12 9/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.469,false,true,288987,Photographs,Photograph,Margaret Treadwell,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1921,1921,1921,Platinum-palladium print,25.4 x 20.3 cm (10 x 8 in. ),"Gift of John Pritzker, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.127.1,false,true,267442,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Platinum-palladium print,24.5 x 19.6 cm (9 5/8 x 7 11/16 in. ),"Gift of David A. Schulte, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.127.6,false,true,267447,Photographs,Photograph,"Music – A Sequence of Ten Cloud Photographs, No. 1",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1922,1922,1922,Platinum print,19.2 x 24.1 cm. (7 9/16 x 9 1/2 in.),"Gift of David A. Schulte, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.127.7,false,true,267448,Photographs,Photograph,The Dancing Trees,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1922,1922,1922,Palladium print,24.2 x 19.3 cm (9 1/2 x 7 5/8 in.),"Gift of David A. Schulte, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.128.4,false,true,267454,Photographs,Photograph,Equivalent,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1925,1925,1925,Gelatin silver print,11.8 x 9.2 cm (4 5/8 x 3 5/8 in.),"Alfred Stieglitz Collection, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.128.5,false,true,267455,Photographs,Photograph,Songs of the Sky,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1924,1924,1924,Gelatin silver print,9.2 x 11.8 cm (3 5/8 x 4 5/8 in.),"Alfred Stieglitz Collection, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.128.7,false,true,267457,Photographs,Photograph,Equivalent,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1925,1925,1925,Gelatin silver print,11.8 x 9.2 cm (4 5/8 x 3 5/8 in.),"Alfred Stieglitz Collection, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.128.8,false,true,267458,Photographs,Photograph,Equivalent,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1925,1925,1925,Gelatin silver print,9.3 x 11.9 cm.(3 11/16 x 4 11/16 in.),"Alfred Stieglitz Collection, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.128.9,false,true,267459,Photographs,Photograph,Equivalent,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1925,1925,1925,Gelatin silver print,11.9 x 9.1 cm (4 11/16 x 3 9/16 in.),"Alfred Stieglitz Collection, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.130.1,false,true,267461,Photographs,Photograph,Georgia O'Keeffe — Hand and Breasts,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1919,1919,1919,Palladium print,18.2 x 23.1 cm (7 3/16 x 9 1/8 in. ),"Gift of Mrs. Alma Wertheim, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.130.2,false,true,267462,Photographs,Photograph,Georgia O'Keeffe—Torso,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Gelatin silver print,23.6 x 18.8 cm (9 5/16 x 7 3/8 in.),"Gift of Mrs. Alma Wertheim, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.12,false,true,269280,Photographs,Photograph,After Working Hours - The Ferry Boat,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1910, printed in or before 1913",1910,1910,Photogravure,33.2 x 25.9 cm. (13 1/16 x 10 3/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.13,false,true,269281,Photographs,Photograph,The Steerage,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1907, printed 1915",1907,1907,Photogravure,32.2 x 25.8 cm. (12 11/16 x 10 3/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.15,false,true,269283,Photographs,Photograph,The City of Ambitions,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1910, printed 1910–13",1910,1910,Photogravure,33.8 x 26.0 cm (13 5/16 x 10 1/4 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269283,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.16,false,true,269284,Photographs,Photograph,The Flatiron,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1903, printed in or before 1910",1903,1903,Photogravure,32.8 x 16.7 cm (12 15/16 x 6 9/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269284,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.17,false,true,269290,Photographs,Photograph,Old and New New York,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1910, printed in or before 1913",1910,1910,Photogravure,33.2 x 25.5 cm (13 1/16 x 10 1/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.18,false,true,269300,Photographs,Photograph,The Terminal,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1893, printed 1913 or before",1893,1893,Photogravure,25.5 x 33.5 cm (10 1/16 x 13 3/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.21,false,true,269333,Photographs,Photograph,Shadows in Lake,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1916,1916,1916,Gelatin silver print,11.5 x 9.1 cm (4 1/2 x 3 9/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.22,false,true,269344,Photographs,Photograph,Rebecca Salsbury Strand,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1922,1922,1922,Gelatin silver print,9.3 x 11.5 cm. (3 11/16 x 4 1/2 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.23,false,true,269355,Photographs,Photograph,Katherine,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1921,1921,1921,Gelatin silver print,10.4 x 8.2 cm. (4 1/16 x 3 1/4 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.24,false,true,269360,Photographs,Photograph,Spiritual America,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1923,1923,1923,Gelatin silver print,11.6 x 9.2 cm. (4 9/16 x 3 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.26,false,true,269376,Photographs,Photograph,Equivalent,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1926,1926,1926,Gelatin silver print,11.6 x 9.2 cm (4 9/16 x 3 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.29,false,true,269404,Photographs,Photograph,Equivalent,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1926,1926,1926,Gelatin silver print,11.8 x 9.2 cm (4 5/8 x 3 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.30,false,true,269408,Photographs,Photograph,"Equivalent, Set C2 No. 1",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1929,1929,1929,Gelatin silver print,11.6 x 9.4 cm. (4 9/16 x 3 11/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.31,false,true,269419,Photographs,Photograph,"Equivalent, Set C2 No. 2",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1929,1929,1929,Gelatin silver print,11.7 x 9.3 cm (4 5/8 x 3 11/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.33,false,true,269438,Photographs,Photograph,"Equivalent, Set C2 No. 4",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1929,1929,1929,Gelatin silver print,11.8 x 9.4 cm. (4 5/8 x 3 11/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.34,false,true,269441,Photographs,Photograph,"Equivalent, Set C2 No. 5",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1929,1929,1929,Gelatin silver print,11.7 x 9.3 cm. (4 5/8 x 3 11/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.35,true,true,269442,Photographs,Photograph,From the Back Window – 291,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1915,1915,1915,Platinum print,25.1 x 20.2 cm (9 7/8 x 7 15/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.36,false,true,269443,Photographs,Photograph,291 – Picasso-Braque Exhibition,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1915,1915,1915,Platinum print,19.4 x 24.4 cm (7 5/8 x 9 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.37,false,true,269444,Photographs,Photograph,Leo Stein,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1917,1917,1917,Platinum print,24.6 x 19.7 cm. (9 11/16 x 7 3/4 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.38,false,true,269445,Photographs,Photograph,Hodge Kirnon,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1917,1917,1917,Palladium print,24.6 x 19.9 cm. (9 11/16 x 7 13/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.39,false,true,269446,Photographs,Photograph,John Marin,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1921–22,1921,1922,Palladium print,24.2 x 19.3 cm. (9 1/2 x 7 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.40,false,true,269448,Photographs,Photograph,Katherine Dudley,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1922,1922,1922,Gelatin silver print,24.7 x 19.4 cm. (9 3/4 x 7 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.41,false,true,269449,Photographs,Photograph,House and Grape Leaves,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1934,1934,1934,Gelatin silver print,24.2 x 19.3 cm (9 1/2 x 7 5/8 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.44,false,true,269452,Photographs,Photograph,Equivalent 27C,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1933,1933,1933,Gelatin silver print,24.0 x 19.1 cm. (9 7/16 x 7 1/2 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.45,false,true,269453,Photographs,Photograph,"From My Window at the Shelton, West",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1931,1931,1931,Gelatin silver print,24.2 x 19.1 cm (9 1/2 x 7 1/2 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.46,false,true,269454,Photographs,Photograph,"From My Window at An American Place, North",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1931,1931,1931,Gelatin silver print,18.9 x 24.0 cm (7 7/16 x 9 7/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.47,false,true,269455,Photographs,Photograph,"From My Window at An American Place, North",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1931,1931,1931,Gelatin silver print,23.5 x 18.6 cm (9 1/4 x 7 5/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.48,false,true,269456,Photographs,Photograph,"From My Window at An American Place, Southwest",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1932,1932,1932,Gelatin silver print,19.2 x 24.1 cm (7 9/16 x 9 1/2 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.5,false,true,270062,Photographs,Photograph,The Street - Design for a Poster,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1900–1901, printed 1903",1900,1901,Photogravure,17.7 x 13.3 cm. (7 x 5 1/4 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.2,false,true,271640,Photographs,Photograph,Georgia O'Keeffe—Hands,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1917,1917,1917,Platinum print,22.6 x 16.8 cm (8 7/8 x 6 5/8 in.),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.6,false,true,271591,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Palladium print,24.5 x 19.2 cm (9 5/8 x 7 9/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.128.10,false,true,267450,Photographs,Photograph,Rain Drops,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1927,1927,1927,Gelatin silver print,9.2 x 11.7 cm. (3 5/8 x 4 5/8 in.),"Alfred Stieglitz Collection, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.128.11,false,true,267451,Photographs,Photograph,Equivalents,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1927,1927,1927,Gelatin silver print,9.1 x 11.8 cm. (3 9/16 x 4 5/8 in.),"Alfred Stieglitz Collection, 1928",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.11,false,true,270032,Photographs,Photogram,The Terminal,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1893, printed 1911",1893,1893,Photogravure,12.1 x 16.0 cm (4 3/4 x 6 5/16 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.16,false,true,270037,Photographs,Photograph,"Snapshot - From My Window, Berlin",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1890s–1900s, printed 1907",1890,1909,Photogravure,21.1 x 17.0 cm. (8 5/16 x 6 11/16 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.577.37,false,true,270060,Photographs,Photograph,The Flat-iron,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1903,1903,1903,Photogravure,17.0 x 8.4 cm. (6 11/16 x 3 5/16 in.),"Gift of J. B. Neumann, 1958",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/270060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.11,false,true,271588,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1919–21,1919,1921,Palladium print,24.1 x 19.5 cm (9 1/2 x 7 11/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.18,false,true,271617,Photographs,Photograph,Georgia O'Keeffe — Hands,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1919,1919,1919,Palladium print,22.9 x 18.9 cm (9 x 7 7/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.19,true,true,271615,Photographs,Photograph,Georgia O'Keeffe — Neck,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1921,1921,1921,Palladium print,23.6 x 19.2 cm (9 5/16 x 7 9/16 in.),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.20,false,true,271607,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Palladium print,22.8 x 18.6 cm (9 x 7 5/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.21,false,true,271629,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Palladium print,23.2 x 19.2 cm (9 1/8 x 7 9/16 in.),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.23,false,true,271623,Photographs,Photograph,Georgia O'Keeffe — Breasts,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1919,1919,1919,Palladium print,24.4 x 19.3 cm (9 5/8 x 7 5/8 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.24,false,true,271585,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1924,1924,1924,Palladium print,23.8 x 19.3 cm (9 3/8 x 7 5/8 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.25,true,true,271570,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Palladium print,11.7 x 9 cm (4 5/8 x 3 9/16 in.),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.26,false,true,271592,Photographs,Photograph,Georgia O'Keeffe—Hand,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Platinum print,11.8 x 9.1 cm (4 5/8 x 3 9/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.29,false,true,271605,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1921,1921,1921,Palladium print,23.5 x 18.1 cm (9 1/4 x 7 1/8 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.31,false,true,271598,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Gelatin silver print,23.5 x 19 cm (9 1/4 x 7 1/2 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.36,false,true,271634,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1932,1932,1932,Gelatin silver print,23.5 x 18.9 cm (9 1/4 x 7 7/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.37,false,true,271610,Photographs,Photograph,Georgia O'Keeffe—Hands and Horse Skull,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1931,1931,1931,Gelatin silver print,19.2 x 24 cm (7 9/16 x 9 7/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.39,false,true,271582,Photographs,Photograph,Georgia O'Keeffe—Hand and Wheel,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1933,1933,1933,Gelatin silver print,24.1 x 19.5 cm (9 1/2 x 7 11/16 in.),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.40,false,true,271579,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1933,1933,1933,Gelatin silver print,23.9 x 18.9 cm (9 7/16 x 7 7/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.43,false,true,271600,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1930,1930,1930,Gelatin silver print,24.1 x 18.8 cm (9 1/2 x 7 3/8 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.55,false,true,271572,Photographs,Photograph,Georgia O'Keeffe—Feet,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1918,1918,1918,Platinum print,24.1 x 19.5 cm (9 1/2 x 7 11/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.61,false,true,271604,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1919–21,1919,1921,Palladium print,23.7 x 19 cm (9 5/16 x 7 1/2 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.63,false,true,271603,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,ca. 1920,1918,1922,Gelatin silver print,9.2 x 11.5 cm (3 5/8 x 4 1/2 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.64,false,true,271590,Photographs,Photograph,Georgia O'Keeffe with Matisse Sculpture,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1921,1921,1921,Palladium print,24.4 x 19.2 cm (9 5/8 x 7 9/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.66,false,true,271587,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1922,1922,1922,Palladium print,18.9 x 24 cm (7 7/16 x 9 7/16 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.69,false,true,271602,Photographs,Photograph,Georgia O'Keeffe,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1932,1932,1932,Gelatin silver print,19.1 x 24 cm (7 1/2 x 9 7/16 in.),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.71,false,true,271624,Photographs,Photograph,Georgia O'Keeffe – Torso,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1931,1931,1931,Gelatin silver print,10.2 x 23.7 cm (4 x 9 5/16 in.),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.61.74,false,true,271614,Photographs,Photograph,[Margaret Prosser's Clasped Hands in Lap],,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1933,1933,1933,Gelatin silver print,9.2 x 11.5 cm (3 5/8 x 4 1/2 in. ),"Gift of Georgia O'Keeffe, through the generosity of The Georgia O'Keeffe Foundation and Jennifer and Joseph Duke, 1997",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.159.49,false,true,306332,Photographs,Photograph,"Kitty Stieglitz, Central Park, New York",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1901,1901,1901,Photogravure,Mount: 10 3/8 in. × 6 15/16 in. (26.4 × 17.6 cm) Image: 4 13/16 × 6 3/16 in. (12.2 × 15.7 cm),"Bequest of Maurice B. Sendak, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/306332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.11,false,true,265144,Photographs,Photograph,"From My Window at the Shelton, North",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1931,1931,1931,Gelatin silver print,24.2 x 19.2 cm (9 1/2 x 7 9/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.178,false,true,285933,Photographs,Photograph,"Spring Showers, the Coach",,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1899–1900,1899,1900,Platinum print,Image: 3 3/8 × 1 13/16 in. (8.6 × 4.6 cm) Mount (1): 3 9/16 in. × 2 in. (9 × 5.1 cm) Mount (2): 6 13/16 × 3 15/16 in. (17.3 × 10 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.290,false,true,283271,Photographs,Photograph,Marsden Hartley,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1916,1916,1916,Gelatin silver print,"Image: 24.8 x 19.8cm (9 3/4 x 7 13/16in.) Mount: 55.8 cm, 46 3/16 in. (21 15/16 in., 117.3 cm)","Gilman Collection, Purchase, Gift of Marsden Hartley, by exchange, and Gift of Grace M. Mayer, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.459,false,true,285929,Photographs,Photograph,The Hand of Man,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,"1902, printed 1920s–30s",1902,1902,Gelatin silver print,Image: 8.8 x 11.8 cm (3 7/16 x 4 5/8 in.) Mount: 8.8 x 11.8 cm (3 7/16 x 4 5/8 in.) Mount (2nd): 31.3 x 24.7 cm (12 5/16 x 9 3/4 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.479,false,true,286177,Photographs,Photograph,Grass,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1933,1933,1933,Gelatin silver print,Image: 14.9 x 18.6 cm (5 7/8 x 7 5/16 in.) Mount: 14.9 x 18.6 cm (5 7/8 x 7 5/16 in.) Mount (2nd): 34.7 x 27.5 cm (13 11/16 x 10 13/16 in.),"Gilman Collection, Purchase, Ann Tenenbaum and Thomas H. Lee Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.103,false,true,265137,Photographs,Photograph,Gable and Apples,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1922,1922,1922,Gelatin silver print,11.4 x 9.0 cm (4 1/2 x 3 9/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.457,false,true,265529,Photographs,Photograph,Equivalent No. 314,,,,,,Artist,,Alfred Stieglitz,"American, Hoboken, New Jersey 1864–1946 New York",,"Stieglitz, Alfred",American,1864,1946,1926,1926,1926,Gelatin silver print,11.9 x 9.1 cm (4 11/16 x 3 9/16 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1149,false,true,285942,Photographs,Photograph,"Fragment from Balustrade of the Temple of Athena Nike, Acropolis, Athens",,,,,,Artist,,William James Stillman,"American, Schenectady, New York 1828–1901 Surrey",,"Stillman, William James",American,1828,1901,ca. 1882,1877,1887,Albumen silver print from glass negative,Image: 10 1/2 × 8 1/8 in. (26.7 × 20.6 cm) Mount: 15 7/8 in. × 13 15/16 in. (40.4 × 35.4 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.240,false,true,681809,Photographs,Tintype,[Ku Klux Klansman with Two Pistols and a Sword],,,,,,Artist,,Owen A. Kenefick,"American, Lawrence, Massachusetts ca. 1858–after 1930",,"Kenefick, Owen A.",American,1850,1950,1880–1905,1880,1905,Tintype,Tintype: 3 1/2 × 2 9/16 in. (8.9 × 6.5 cm) Passe-Partout: 4 7/8 × 3 1/8 in. (12.4 × 7.9 cm) Paper Folder (Open): 4 7/8 × 6 1/4 in. (12.4 × 15.9 cm),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2015",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/681809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.75,false,true,283174,Photographs,Photograph,[Portrait of a Young Man],,,,,,Artist,,Samuel F. B. Morse,"American, Charlestown, Massachusetts 1791–1872 New York",,"Morse, Samuel F. B.",American,1791,1872,1840,1840,1840,Daguerreotype,"Image: 5 x 4.2 cm (1 15/16 x 1 5/8 in.), oval","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1110,false,true,286619,Photographs,Photograph,"[Fortifications Near Charleston, South Carolina]",,,,,,Artist,Attributed to,George Smith Cook,"American, Stratford, Connecticut 1819–1902 Bel Air, Virginia",,"Cook, George Smith",American,1819,1902,ca. 1861,1861,1861,Albumen silver print from glass negative,Image: 15 × 20.6 cm (5 7/8 × 8 1/8 in.) Mount: 23.2 × 27.1 cm (9 1/8 × 10 11/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.389,false,true,284644,Photographs,Photograph,Grave,,,,,,Artist,,Walker Evans,"American, St. Louis, Missouri 1903–1975 New Haven, Connecticut",,"Evans, Walker",American,1903,1975,1936,1936,1936,Gelatin silver print,19.4 x 24.2 cm (7 5/8 x 9 1/2 in. ),"Purchase, Alfred Stieglitz Society Gifts, 2001",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/284644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1100.482,true,true,265556,Photographs,Photograph,"Penny Picture Display, Savannah",,,,,,Artist,,Walker Evans,"American, St. Louis, Missouri 1903–1975 New Haven, Connecticut",,"Evans, Walker",American,1903,1975,1936,1936,1936,Gelatin silver print,24.7 x 19.3 cm (9 3/4 x 7 5/8 in.),"Ford Motor Company Collection, Gift of Ford Motor Company and John C. Waddell, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.147,false,true,267546,Photographs,Photograph,[Mrs. James Brown Potter or Mrs Potter Palmer],,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1896,1896,1896,Platinum print,13.4 x 10.8 cm. (5 1/4 x 4 1/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.148,false,true,267547,Photographs,Photograph,Zaida Ben-Yusuf,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1898,1898,1898,Platinum print,Image: 16.3 x 10.9 cm (6 7/16 x 4 5/16 in.) Mount: 17.1 x 11.5 cm (6 3/4 x 4 1/2 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.151,false,true,267551,Photographs,Photograph,Kahlil Gibran with Book,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1896,1896,1896,Platinum print,15.9 x 12.0 cm. (6 1/4 x 4 3/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.157,false,true,267557,Photographs,Photograph,An Ethiopian Chief,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,ca. 1897,1895,1899,Platinum print,18.1 x 18.4 cm. (7 1/8 x 7 1/4 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.158,false,true,267558,Photographs,Photograph,Menelek,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1897,1897,1897,Platinum print,24.5 x 19.5 cm (9 5/8 x 7 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.159,false,true,267559,Photographs,Photograph,Portrait,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1898,1898,1898,Platinum print,15.7 x 9.8 cm. (6 3/16 x 3 7/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.160,false,true,267561,Photographs,Photograph,The Vigil,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1899,1899,1899,Platinum print,Image: 16.2 x 11 cm (6 3/8 x 4 5/16 in.) Mount: 30.9 x 23.1 cm (12 3/16 x 9 1/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.161,false,true,267562,Photographs,Photograph,[Boy Piping],,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,ca. 1896,1894,1898,Platinum print,11.8 x 16.6 cm. (4 5/8 x 6 9/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.165,false,true,267566,Photographs,Photograph,Portrait of a Man with Book,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1896,1896,1896,Platinum print,Image: 16 x 11.7 cm (6 5/16 x 4 5/8 in.) Mount: 16.3 x 11.9 cm (6 7/16 x 4 11/16 in.) Mount (2nd): 16.6 x 12.1 cm (6 9/16 x 4 3/4 in.) Mount (3rd): 19 x 14.6 cm (7 1/2 x 5 3/4 in.) Mount (4th): 19.2 x 14.9 cm (7 9/16 x 5 7/8 in.) Mount (5th): 36 x 27.8 cm (14 3/16 x 10 15/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.166,false,true,267567,Photographs,Photograph,Ebony and Ivory,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,ca. 1897,1895,1899,Platinum print,18.3 x 20.0 cm (7 3/16 x 7 7/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267567,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.355,false,true,267770,Photographs,Photograph,[Draped Nude Lounging on the Grass],,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,ca. 1897,1895,1899,Platinum print,Image: 11.8 x 15.8 cm (4 5/8 x 6 1/4 in.) Mount: 15.2 x 19.3 cm (6 x 7 5/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.356,false,true,267771,Photographs,Photograph,The Entombment,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1898,1898,1898,Platinum print,6.7 x 16.6 cm. (2 5/8 x 6 9/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.357,false,true,267772,Photographs,Photograph,The Honey Gatherer,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,ca. 1898,1896,1900,Platinum print,Image: 12.5 cm (4 15/16 in.) diameter Mount: 14 x 13.6 cm (5 1/2 x 5 3/8 in.) Mount (2nd): 14.7 x 14.4 cm (5 13/16 x 5 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.358,false,true,267773,Photographs,Photograph,[Mrs. James Brown Potter or Mrs. Potter Palmer],,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1896,1896,1896,Platinum print,14.2 x 11.9 cm. (5 9/16 x 4 11/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.361,false,true,267777,Photographs,Photograph,Kahlil Gibran with Book,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1896,1896,1896,Platinum print,16.0 x 11.8 cm. (6 5/16 x 4 5/8 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.367,false,true,267783,Photographs,Photograph,Zaîda Ben-Yusuf,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1890s,1890,1899,Platinum print,16.5 x 12.2 cm. (6 1/2 x 4 13/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.175,false,true,269295,Photographs,Photograph,The Seven Words,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1898,1898,1898,Platinum print,each approx: 14.0 x 11.5 cm (5 1/2 x 4 1/2 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.55.222,false,true,269347,Photographs,Photograph,The Seven Words,,,,,,Artist,,F. Holland Day,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts",,"Day, F. Holland",American,1864,1933,1898,1898,1898,Platinum print,7.9 x 32.8 cm. (3 1/8 x 12 15/16 in.),"Alfred Stieglitz Collection, 1949",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.366,false,true,294845,Photographs,Photograph,[Man Serving Head on a Platter],,,,,,Artist,,William Robert Bowles,"American, Campbellsville, Kentucky 1861–1918 Hopkinsville, Kentucky",,"Bowles, William Robert",American,1861,1918,ca. 1900,1895,1905,Gelatin silver print,Image: 9.8 x 13.9 cm (3 7/8 x 5 1/2 in.) Mount: 14.6 x 17.5 cm (5 3/4 x 6 7/8 in.) Frame: 27.9 x 35.6 cm (11 x 14 in.),"Twentieth-Century Photography Fund, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.75.7,false,true,271868,Photographs,Photograph,[Three Boys Wading in a Creek],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1883,1883,1883,Platinum print,,"Gift of Charles Bregler, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.142.2,false,true,271896,Photographs,Photograph,"[William H. Macdowell and Margaret Eakins in Saltville (or Clinch Mountain), Virginia]",,,,,,Artist,Attributed to,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880–82,1880,1882,Platinum print,"Image: 27.4 x 20 cm (10 13/16 x 7 7/8 in.), irregular","Gift of Charles Bregler, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.17,true,true,271885,Photographs,Photograph,Two Pupils in Greek Dress,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1883,1883,1883,Platinum print,36.8 x 26.7 cm. (14 1/2 x 10 1/2 in.),"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.20,false,true,271887,Photographs,Photograph,"Bill Duckett Nude, at the Art Students’ League of Philadelphia",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1889,1884,1894,Platinum print,23.3 x 22 cm (9 3/16 x 8 11/16 in.),"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.22,false,true,271889,Photographs,Photograph,"[Thomas Eakins, Nude, Playing Pipes]",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1883,1883,1883,Platinum print,22.7 x 16.6 cm (8 15/16 x 6 9/16 in.) irregular,"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.23,true,true,271890,Photographs,Photograph,[Thomas Eakins and John Laurie Wallace on a Beach],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1883,1882,1884,Platinum print,"25.5 x 20.4 cm (10 1/16 x 8 1/16 in.), irregular","David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.24,false,true,271891,Photographs,Photograph,[Nude Men in the Garden],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1870s,1870,1879,Platinum print,,"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.25,false,true,271892,Photographs,Photograph,[Female Nude from the Back],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1889,1887,1891,Platinum print,7.1 x 13.4 cm (2 13/16 x 5 1/4 in.),"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.75.10,false,true,271873,Photographs,Photograph,"[Man Walking, ""Stroboscopic"" Photograph]",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,"1880s, printed 1930s–40s",1880,1889,Gelatin silver print,,"Gift of Charles Bregler, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.1103,false,true,271854,Photographs,Photograph,[Betty Reynolds with Doll on Lap],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1885,1883,1887,Gelatin silver print,5.5 x 5.3 cm (2 3/16 x 2 1/16 in.),"David Hunter McAlpin Fund, 1983",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.547.2,false,true,271856,Photographs,Photograph,[Mrs. Eakins or Her Sister Doll],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s–90s,1880,1899,Albumen silver print,,"Gift of Julius Ravzin, 1979",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.12,false,true,271805,Photographs,Photograph,"Katie Crowell in Avondale, Pennsylvania",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1887,1887,1887,Albumen silver print,,"Gift of Arthur and Carol Goldberg, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.17,false,true,271810,Photographs,Photograph,[Four Cats],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1895,1893,1897,Platinum print,5.5 x 13.6 cm (2 3/16 x 5 3/8 in.),"Gift of Joan and Martin E. Messinger, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.20,false,true,271813,Photographs,Photograph,Self-Portrait,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1889–94,1889,1894,Platinum print,,"Gift of Mimi and Ariel Halpern, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.24,false,true,271817,Photographs,Photograph,William J. Crowell with Ella,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880,1880,1880,Albumen silver print,9.5 x 7.2 cm (3 3/4 x 2 13/16 in.),"Gift of Harry D. Nelson Jr., 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.41,false,true,271834,Photographs,Photograph,[Cornfield in Back of the Barn],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1883,1883,1883,Albumen silver print,,"Gift of Joan and Martin E. Messinger, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.42,false,true,271835,Photographs,Photograph,"[Two Boys Playing at the Creek, July 4, 1883]",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1883,1883,1883,Albumen silver print,8.8 x 11.1 cm (3 7/16 x 4 3/8 in.),"Gift of Joseph R. Lasser and Ruth P. Lasser, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.43,false,true,271836,Photographs,Photograph,"[Three Children and a Dog Playing in the Creek, July 4, 1883]",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1883,1883,1883,Albumen silver print,8.9 x 11.2 cm (3 1/2 x 4 7/16 in.),"Gift of Robert D. English, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.45,false,true,271838,Photographs,Photograph,"[Three Children and a Dog Playing in the Creek, July 4, 1883]",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1883,1883,1883,Albumen silver print,,"Gift of Daniel P. and Nancy C. Paduano, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.51,false,true,271844,Photographs,Photograph,"[Frances Crowell with Unidentified Boy, Katie, James, and Frances Crowell]",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1890,1890,1890,Platinum print,7.8 x 9.2 cm (3 1/16 x 3 5/8 in.),"Gift of Joseph R. Lasser and Ruth P. Lasser, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.59,false,true,271852,Photographs,Photograph,"[Thomas Eakins's Horse Billy and Two Crowell Children at Avondale, Pennsylvania]",,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1892,1890,1894,Platinum print,8.1 x 9.8 cm (3 3/16 x 3 7/8 in.) irregular,"Gift of Joseph R. Lasser and Ruth P. Lasser, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.589,false,true,285754,Photographs,Photograph,[Self-Portrait],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1880,1878,1882,Platinum print,Image: 15.7 x 10 cm (6 3/16 x 3 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1109,false,true,285842,Photographs,Photograph,[African-American Man],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,ca. 1884,1882,1886,Gelatin silver print,Image: 3 13/16 × 2 11/16 in. (9.7 × 6.8 cm),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.671,false,true,261176,Photographs,Photograph,Herbert,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1974",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.351,false,true,267766,Photographs,Photograph,Portrait Study,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1917,1917,1917,Gelatin silver print,24.1 x 17.7 cm. (9 1/2 x 7 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.1,false,true,261015,Photographs,Photograph,Jeanne,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1917,1917,1917,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.2,false,true,261026,Photographs,Photograph,Jeanne,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1917,1917,1917,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.3,false,true,261027,Photographs,Photograph,Jeanne,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1917,1917,1917,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.4,false,true,261028,Photographs,Photograph,Jeanne and Regena,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1917,1917,1917,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.5,false,true,261029,Photographs,Photograph,Regena,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1917,1917,1917,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.6,false,true,261030,Photographs,Photograph,Regena,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.7,false,true,261031,Photographs,Photograph,Regena and Herbert,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.8,false,true,261032,Photographs,Photograph,Herbert,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.9,false,true,261033,Photographs,Photograph,Herbert,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.10,false,true,261016,Photographs,Photograph,Jeanne,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.11,false,true,261017,Photographs,Photograph,Jeanne,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.12,false,true,261018,Photographs,Photograph,Jeanne and Richard,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.13,false,true,261019,Photographs,Photograph,Jeanne and Richard,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.14,false,true,261020,Photographs,Photograph,Richard,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.15,false,true,261021,Photographs,Photograph,Etellea,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.16,false,true,261022,Photographs,Photograph,Jeanne,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1915,1915,1915,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.17,false,true,261023,Photographs,Photograph,Jeanne,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.18,false,true,261024,Photographs,Photograph,Morton Schamberg,,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,ca. 1912,1910,1914,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.664.19,false,true,261025,Photographs,Photograph,"Morton, Richard, Jeanne, Gilbert, Jesse, Etellea, Yvonne, Henry, Regena, and Herbert",,,,,,Artist,,Morton Schamberg,"American, Philadelphia, Pennsylvania 1881–1918 Philadelphia, Pennsylvania",,"Schamberg, Morton",American,1881,1918,1912,1912,1912,Gelatin silver print,,"Gift of Jean Loeb Whitehill, 1973",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/261025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.62.2,false,true,271903,Photographs,Glass positive,[Man on a Ladder],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Glass positive,,"Gift of Charles Bregler, 1947",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/271903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.62.3,false,true,271904,Photographs,Glass positive,Margaret Eakins,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Glass positive,,"Gift of Charles Bregler, 1947",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/271904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.62.4,false,true,271905,Photographs,Glass positive,[Female portrait],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Glass positive,,"Gift of Charles Bregler, 1947",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/271905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.62.5,false,true,271902,Photographs,Glass positive,Frank MacDowell,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Glass positive,,"Gift of Charles Bregler, 1947",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/271902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.75.9,false,true,271870,Photographs,Photograph,Edmund Quinn Fencing,,,,,,Artist,Circle of,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Gelatin silver print,Image: 9.1 x 11 cm (3 9/16 x 4 5/16 in.),"Gift of Charles Bregler, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.142.4,false,true,271898,Photographs,Photograph,[Woman Playing Cello],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Platinum print,,"Gift of Charles Bregler, 1941",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.10,false,true,271878,Photographs,Photograph,Mary Macdowell,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Platinum print,,"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.11,false,true,271879,Photographs,Photograph,Elizabeth MacDowell Kenton,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Albumen silver print,,"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.14,false,true,271882,Photographs,Photograph,[Woman in White Laced-bodice Dress in Studio of Thomas Eakins],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Platinum print,23.3 x 14.9 cm (9 3/16 x 5 7/8 in.),"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.87.21,false,true,271888,Photographs,Photograph,[Standing Male Nude with Pipes],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Platinum print,22.9 x 17.3 cm (9 x 6 13/16 in.),"David Hunter McAlpin Fund, 1943",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.75.11,false,true,271869,Photographs,Photograph,[Thomas Eakins in Swim Suit],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Albumen silver print,,"Gift of Charles Bregler, 1944",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.547.1,false,true,271855,Photographs,Photograph,Mr. MacDowell,,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Albumen silver print,,"Gift of Julius Ravzin, 1979",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.547.3,false,true,271857,Photographs,Photograph,[Mrs. Louis Kentin in Empire Dress],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Albumen silver print,,"Gift of Julius Ravzin, 1979",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.1027.11,false,true,271804,Photographs,Photograph,[Thomas Eakins's Dog Harry and Another Setter],,,,,,Artist,,Thomas Eakins,"American, Philadelphia, Pennsylvania 1844–1916 Philadelphia, Pennsylvania",,"Eakins, Thomas",American,1844,1916,1880s,1880,1889,Platinum print,8.5 x 10.4 cm (3 3/8 x 4 1/8 in.),"Gift of John T. Marvin, 1985",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/271804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.199,false,true,296297,Photographs,Cabinet card,[Man in Bottle],,,,,,Artist,,John C. Higgins,"American, active 1880s–90s",,"Higgins, John C.",American,1880,1899,ca. 1888,1883,1893,Albumen silver print from glass negative,Image: 13.5 x 10 cm (5 5/16 x 3 15/16 in.) Mount: 16.4 x 10.6 cm (6 7/16 x 4 3/16 in.) Frame: 35.6 x 27.9 cm (14 x 11 in.),"Purchase, Susan and Thomas Dunn Gift, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/296297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.460.1,false,true,290469,Photographs,Photograph,Niagara Falls,,,,,,Artist,,George Barker,"American, born Canada, 1844–1894",,"Barker, George",American,1844,1894,ca. 1888,1885,1890,Albumen silver print from glass negative,Image: 49.1 x 42.5 cm (19 5/16 x 16 3/4 in.) Sheet: 50.8 x 43.3 cm (20 x 17 1/16 in.) Mount: 54.7 x 48.8 cm (21 9/16 x 19 3/16 in.),"Gift of Paul F. Walter, 2009",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/290469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.3,false,true,302666,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,April 1861,1861,1861,Albumen silver print from glass negative,Image: 3 1/8 × 1 15/16 in. (7.9 × 5 cm) Mount: 4 3/4 in. × 3 3/8 in. (12 × 8.5 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.5,false,true,302668,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.6,false,true,302669,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.10,false,true,302673,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.12,false,true,302675,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.14,false,true,302677,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher,,Edward Anthony,"American, 1818–1888",,"Anthony, Edward",American,1818,1888,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.939,false,true,288280,Photographs,Stereographs,"Tower of London, London, England",,,,,,Artist|Publisher,,Benneville Lloyd Singley|Keystone View Company,"American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania",,"Singley, Benneville Lloyd|Keystone View Company",American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mount: 8.9 x 17.9 cm (3 1/2 x 7 1/16 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.958–.962,false,true,288288,Photographs,Stereographs,"[Group of 5 Stereograph Views of the Thames River at Night, London, England]",,,,,,Artist|Publisher,,Benneville Lloyd Singley|Keystone View Company,"American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania",,"Singley, Benneville Lloyd|Keystone View Company",American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,,"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.992–.995,false,true,288294,Photographs,Stereographs,[Group of 4 Stereograph Views of Babies],,,,,,Artist|Publisher,,Benneville Lloyd Singley|Keystone View Company,"American, Union Township, Pennsylvania 1864–1938 Meadville, Pennsylvania",,"Singley, Benneville Lloyd|Keystone View Company",American,1864,1938,1850s–1910s,1850,1919,Albumen silver prints,Mounts approximately: 8.9 x 17.8 cm (3 1/2 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (1b),false,true,288478,Photographs,Photograph,[Young Japanese Woman],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,24.8 x 19.8 cm (9 3/4 x 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (2b),false,true,283170,Photographs,Photograph,Actor in Samurai Armor,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,24.6 x 18.9 cm (9 11/16 x 7 7/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (4b),false,true,288483,Photographs,Photograph,[Woman in Traditional Japanese Garment Photographed from Behind],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.1 x 20 cm (9 7/8 x 7 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (5b),false,true,288484,Photographs,Photograph,[Young Japanese Woman],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25 x 19.9 cm (9 13/16 x 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (9b),false,true,288490,Photographs,Photograph,"Street Minstrel, Gose",,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20.2 cm (9 15/16 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (10b),false,true,288492,Photographs,Photograph,Farm laborer with rain coat (mino),,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 19.7 cm (9 15/16 x 7 3/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (11b),false,true,288494,Photographs,Photograph,Professional Singers,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.1 x 20.1 cm (9 7/8 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (12b),false,true,288496,Photographs,Photograph,Newsman,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.3 x 20.1 cm (9 15/16 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (13b),false,true,288498,Photographs,Photograph,Tea House waitress,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.1 x 20.2 cm (9 7/8 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (14b),false,true,288500,Photographs,Photograph,La Toilette,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20.3 cm (9 15/16 x 8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (15b),false,true,288502,Photographs,Photograph,[Japanese Women in Traditional Dress],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20 cm (9 15/16 x 7 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (16b),false,true,288505,Photographs,Photograph,[Japanese Women in Traditional Dress],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20 cm (9 15/16 x 7 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (17b),false,true,288507,Photographs,Photograph,[Japanese Woman in Traditional Dress],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 19.7 cm (9 15/16 x 7 3/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (18b),false,true,288509,Photographs,Photograph,Osaki Kioto [illegible] dancer,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 19.9 cm (9 15/16 x 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (19b),false,true,288511,Photographs,Photograph,[Two Japanese Women in Traditional Dress],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 19.9 cm (9 15/16 x 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (20b),false,true,288513,Photographs,Photograph,[Two Japanese Women in Traditional Dress with Fan and Screen],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 19.7 cm (9 15/16 x 7 3/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (21b),false,true,288514,Photographs,Photograph,[Japanese Man Preparing a Fish],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 19.9 cm (9 15/16 x 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (22b),false,true,288516,Photographs,Photograph,[A Japanese Woman and a Japanese Boy in Traditional Dress],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20.1 cm (9 15/16 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (23b),false,true,288518,Photographs,Photograph,[Japanese Woman with Parasol],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.1 x 20.3 cm (9 7/8 x 8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (24b),false,true,288520,Photographs,Photograph,Florist,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.1 x 20.3 cm (9 7/8 x 8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (25b),false,true,288522,Photographs,Photograph,[Two Japanese Women Posing with Fans],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20.1 cm (9 15/16 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (26b),false,true,288524,Photographs,Photograph,[Japanese Woman in Traditional Dress Posing with a Child on her Back],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25 x 20.2 cm (9 13/16 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (27b),false,true,288526,Photographs,Photograph,[Japanese Woman in Traditional Dress Posing with Cat and Instrument],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20 cm (9 15/16 x 7 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (28b),false,true,288527,Photographs,Photograph,[Japanese Woman in Traditional Dress Posing Outdoors],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 19.8 cm (9 15/16 x 7 13/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (29b),false,true,288529,Photographs,Photograph,Cobbler,,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.3 x 20.2 cm (9 15/16 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (30b),false,true,288530,Photographs,Photograph,[Two Japanese Men in Traditional Dress],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,20.1 x 25.2 cm (7 15/16 x 9 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (40b),false,true,288548,Photographs,Photograph,[Japanese Woman in Carriage],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,20.1 x 25.2 cm (7 15/16 x 9 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (41b),false,true,288549,Photographs,Photograph,[Landscape with Buddha Sculpture],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.1 x 20.1 cm (9 7/8 x 7 15/16 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.505.1 (43b),false,true,288553,Photographs,Photograph,[Buddha Sculpture],,,,,,Artist,,Suzuki Shin'ichi,"Japanese, 1835–1919",,"Suzuki, Shin'ichi",Japanese,1835,1919,1870s,1870,1879,Albumen silver print from glass negative,25.2 x 20 cm (9 15/16 x 7 7/8 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2004.283a, b",false,true,285443,Photographs,Photograph,[Geisha with Attendant],,,,,,Artist,,Yokoyama Matsusaburō,"Japanese, 1838–1884",,"Yokoyama, Matsusaburō",Japanese,1838,1884,1860s,1860,1869,Ambrotype,Image: 10 x 7.6 cm (3 15/16 x 3 in.) Case: 1.3 x 11.1 x 8.6 cm (1/2 x 4 3/8 x 3 3/8 in.),"Funds from various donors, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1064.1,false,true,264743,Photographs,Photograph,"Mutsuhito, The Meiji Emperor",,,,,,Artist,,Uchida Kuichi,"Japanese, 1844–1875",,"Uchida, Kuichi",Japanese,1844,1875,1872,1872,1872,Albumen silver print from glass negative,25.1 x 19.5 cm (9 7/8 x 7 11/16 in.),"The Elisha Whittelsey Collection, The Elisha Whittelsey Fund, 1986",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/264743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2004.282a, b",false,true,285442,Photographs,Photograph,Kenji Morita,,,,,,Artist,,Fujita,"Japanese, active 1880s",,Fujita,Japanese,1880,1880,1886,1886,1886,Ambrotype,Image: 9.6 x 6.8 cm (3 3/4 x 2 11/16 in.) Case: 1.3 x 11.1 x 8.3 cm (1/2 x 4 3/8 x 3 1/4 in.),"Funds from various donors, 2004",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.454.1.27,false,true,291461,Photographs,Album,"Barracks Post, Place de la Bastille; Canal Tunnel and July Column",,,,,,Artist|Author,,Alphonse J. Liébert|Alfred d'Aunay,"French, 1827–1913|French",,"Liébert, Alphonse J.|d'Aunay, Alfred",French|French,1827,1913,1871,1871,1871,Albumen silver print from glass negative,"Images approx.: 19 x 25 cm (7 1/2 x 9 13/16 in.), or the reverse Mounts: 32.8 x 41.3 cm (12 15/16 x 16 1/4 in.), or the reverse","Joyce F. Menschel Photography Library Fund, 2007",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/291461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.454.1.1–.33,false,true,288894,Photographs,Album,"Les Ruines de Paris et de ses Environs 1870-1871: Cent Photographies: Premier Volume. Par A. Liébert, text par Alfred d'Aunay.",,,,,,Artist|Author,,Alphonse J. Liébert|Alfred d'Aunay,"French, 1827–1913|French",,"Liébert, Alphonse J.|d'Aunay, Alfred",French|French,1827,1913,1870–71,1870,1871,Albumen silver prints from glass negatives,"Images approx.: 19 x 25 cm (7 1/2 x 9 13/16 in.), or the reverse Mounts: 32.8 x 41.3 cm (12 15/16 x 16 1/4 in.), or the reverse","Joyce F. Menschel Photography Library Fund, 2007",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/288894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.454.2.1–.32,false,true,288895,Photographs,Album,"Les Ruines de Paris et de ses Environs 1870-1871: Cent Photographies: Second Volume. Par A. Liébert, text par Alfred d'Aunay.",,,,,,Artist|Author,,Alphonse J. Liébert|Alfred d'Aunay,"French, 1827–1913|French",,"Liébert, Alphonse J.|d'Aunay, Alfred",French|French,1827,1913,1870–71,1870,1871,Albumen silver prints from glass negatives,"Images approx.: 19 x 25 cm (7 1/2 x 9 13/16 in.), or the reverse Mounts: 32.8 x 41.3 cm (12 15/16 x 16 1/4 in.), or the reverse","Joyce F. Menschel Photography Library Fund, 2007",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/288895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.270,false,true,266917,Photographs,Album,Album d'Études–Poses,,,,,,Artist|Editor,,Louis Igout|A. Calavas,"French, 1837–1881|French",,"Igout, Louis|Calavas, A.",French|French,1837,1881,ca. 1880,1878,1882,Albumen silver prints from glass negatives,,"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1993",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/266917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.1–.179,false,true,285688,Photographs,Portfolio,"Jerusalem, Etude et reproduction photographique des monuments de la ville sainte depuis l' époque judaique jusqu'à nos jours 1856",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,"1854, printed 1856",1854,1859,Salted paper prints from paper negatives,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Portfolios,,http://www.metmuseum.org/art/collection/search/285688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.175–.179,false,true,681092,Photographs,Portfolio,"Jerusalem, Etude et reproduction photographique des monuments de la ville sainte depuis l' époque judaique jusqu'à nos jours 1856",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Ink on paper,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Portfolios,,http://www.metmuseum.org/art/collection/search/681092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.677.4,false,true,270956,Photographs,Photomechanical print,Charles Baudelaire,,,,,,Printer|Artist,,Goupil et Cie|Étienne Carjat,"French, active 1850–84|French, Fareins 1828–1906 Paris",,"Goupil et Cie|Carjat, Étienne",French|French,1850 |1828,1884 |1906,ca. 1863,1861,1865,Woodburytype,,"David Hunter McAlpin Fund, 1964",,,,,,,,,,,,Photographs|Prints,,http://www.metmuseum.org/art/collection/search/270956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.454.1.4,false,true,291458,Photographs,Photograph,"Tuileries Palace, Burned. General View",,,,,,Artist|Author,,Alphonse J. Liébert|Alfred d'Aunay,"French, 1827–1913|French",,"Liébert, Alphonse J.|d'Aunay, Alfred",French|French,1827,1913,1871,1871,1871,Albumen silver print from glass negative,"Images approx.: 19 x 25 cm (7 1/2 x 9 13/16 in.), or the reverse Mounts: 32.8 x 41.3 cm (12 15/16 x 16 1/4 in.), or the reverse","Joyce F. Menschel Photography Library Fund, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291458,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.454.1.12,false,true,291459,Photographs,Photograph,"Finance Ministry, Burned. Exterior View",,,,,,Artist|Author,,Alphonse J. Liébert|Alfred d'Aunay,"French, 1827–1913|French",,"Liébert, Alphonse J.|d'Aunay, Alfred",French|French,1827,1913,1871,1871,1871,Albumen silver print from glass negative,"Images approx.: 19 x 25 cm (7 1/2 x 9 13/16 in.), or the reverse Mounts: 32.8 x 41.3 cm (12 15/16 x 16 1/4 in.), or the reverse","Joyce F. Menschel Photography Library Fund, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.454.1.26,false,true,291460,Photographs,Photograph,"Place de la Bastille, Burned",,,,,,Artist|Author,,Alphonse J. Liébert|Alfred d'Aunay,"French, 1827–1913|French",,"Liébert, Alphonse J.|d'Aunay, Alfred",French|French,1827,1913,1871,1871,1871,Albumen silver print from glass negative,"Images approx.: 19 x 25 cm (7 1/2 x 9 13/16 in.), or the reverse Mounts: 32.8 x 41.3 cm (12 15/16 x 16 1/4 in.), or the reverse","Joyce F. Menschel Photography Library Fund, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.454.2.3,false,true,291457,Photographs,Photograph,Neuilly Bombarded. General View of the rue de Chezy,,,,,,Author|Artist,,Alfred d'Aunay|Alphonse J. Liébert,"French|French, 1827–1913",,"d'Aunay, Alfred|Liébert, Alphonse J.",French|French,1827,1913,1871,1871,1871,Albumen silver print from glass negative,"Images approx.: 19 x 25 cm (7 1/2 x 9 13/16 in.), or the reverse Mounts: 32.8 x 41.3 cm (12 15/16 x 16 1/4 in.), or the reverse","Joyce F. Menschel Photography Library Fund, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291457,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.33,false,true,283106,Photographs,Photograph,[Henriette-Caroline-Victoire Robert],,,,,,Artist|Person in Photograph,Person in photograph,Louis-Rémy Robert|Henriette-Caroline-Victoire Robert,"French, 1810–1882|French, 1834–1934",,"Robert, Louis-Rémy|Robert, Henriette-Caroline-Victoire",French|French,1810 |1834,1882 |1934,ca. 1850,1848,1852,Salted paper print from paper negative,Mount: 12 13/16 in. × 10 3/8 in. (32.5 × 26.3 cm) Image: 8 11/16 × 5 13/16 in. (22 × 14.7 cm),"Gilman Collection, Purchase, Mrs. Walter Annenberg and The Annenberg Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.46,false,true,286945,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Tombeau d'Absalon",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.2 x 23.2 cm (12 11/16 x 9 1/8 in.) Mount: 59.8 x 44.8 cm (23 9/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.47,false,true,286946,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Détails du Tombeau d'Absalom",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33 x 23.1 cm (13 x 9 1/8 in.) Mount: 60 x 44.6 cm (23 5/8 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.48,false,true,286947,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Fronton du Tombeau de Josaphat",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 32.5 cm (9 1/4 x 12 13/16 in.) Mount: 44.8 x 59.5 cm (17 5/8 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.49,false,true,286948,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Grotte sépulcrale, 1",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 33.5 cm (9 1/8 x 13 3/16 in.) Mount: 44.8 x 59.8 cm (17 5/8 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.50,false,true,286949,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Grottes sépulcrales, 2",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.7 x 33.2 cm (8 15/16 x 13 1/16 in.) Mount: 44.7 x 60.4 cm (17 5/8 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.51,false,true,286950,Photographs,Photograph,"Jérusalem, Tombeau des rois de Juda, Cour extérieure",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.2 cm (9 1/8 x 12 11/16 in.) Mount: 44.6 x 59.9 cm (17 9/16 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.52,false,true,286951,Photographs,Photograph,"Jérusalem, Tombeau des rois de Juda, Intérieur de la cour",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 31.9 cm (9 3/16 x 12 9/16 in.) Mount: 44.6 x 60.1 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.53,false,true,286952,Photographs,Photograph,"Jérusalem, Tombeau des rois de Juda, Frise supérieure et centrale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.2 cm (9 1/8 x 12 11/16 in.) Mount: 44.5 x 59.7 cm (17 1/2 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.54,false,true,286953,Photographs,Photograph,"Jérusalem, Tombeau des rois de Juda, Encadrement de feuillages et de fruits",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.3 cm (9 3/16 x 12 11/16 in.) Mount: 45.1 x 59.9 cm (17 3/4 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.55,false,true,286954,Photographs,Photograph,"Jérusalem, Tombeau des rois de Juda, Fragments d'un sarcophage",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.6 x 33.6 cm (9 5/16 x 13 1/4 in.) Mount: 44.8 x 59.9 cm (17 5/8 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.56,false,true,286955,Photographs,Photograph,"Jérusalem, Tombeau des rois de Juda, Couvercle de sarcophage et fragment d'une porte en pierre",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.3 x 33.2 cm (9 3/16 x 13 1/16 in.) Mount: 44.8 x 59.7 cm (17 5/8 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.57,false,true,286956,Photographs,Photograph,"Jérusalem, Escalier antique taillé dans le roc, conduisant à l'ancienne Porte du Fumier",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.3 x 23.5 cm (13 1/8 x 9 1/4 in.) Mount: 59.9 x 44.7 cm (23 9/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.58,false,true,286957,Photographs,Photograph,"Jérusalem, Tombeau des Juges, Vue générale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 33.3 cm (9 1/8 x 13 1/8 in.) Mount: 45.2 x 60.2 cm (17 13/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.59,false,true,286958,Photographs,Photograph,"Jérusalem, Tombeau des Juges, Détails",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.5 cm (9 1/8 x 12 13/16 in.) Mount: 44.8 x 60.1 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.60,false,true,286959,Photographs,Photograph,"Jérusalem, Tombeau Juif, à trois milles Nord-Nord-Ouest de Jérusalem",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 33.3 cm (9 1/16 x 13 1/8 in.) Mount: 45 x 60.3 cm (17 11/16 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.61,false,true,286960,Photographs,Photograph,"Jérusalem, Tombeau Juif, Détails",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.7 x 33.6 cm (9 5/16 x 13 1/4 in.) Mount: 44.9 x 60.4 cm (17 11/16 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.62,false,true,286961,Photographs,Photograph,"Jérusalem, Tombeau de Salomon, Vue générale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.9 x 32.7 cm (9 x 12 7/8 in.) Mount: 44.7 x 60.1 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.63,false,true,286962,Photographs,Photograph,"Jérusalem, Tombeau de Salomon, Détails",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 33.1 cm (9 1/16 x 13 1/16 in.) Mount: 44.8 x 60.1 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.64,false,true,286963,Photographs,Photograph,"Jérusalem, Birket-Mamillah",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.7 x 32.8 cm (9 5/16 x 12 15/16 in.) Mount: 44.6 x 60.1 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.65,false,true,286964,Photographs,Photograph,"Jérusalem, Birket-es-Soutlan",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.8 cm (9 1/8 x 12 15/16 in.) Mount: 44.8 x 60.1 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.66,false,true,286965,Photographs,Photograph,"Jérusalem, Birket-Hammam-el-Batrak",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.2 cm (9 1/4 x 13 1/16 in.) Mount: 44.8 x 59.5 cm (17 5/8 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.67,false,true,286966,Photographs,Photograph,"Jérusalem, Carrière à la Porte de Damas",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.4 cm (9 1/4 x 13 1/8 in.) Mount: 44.7 x 59.4 cm (17 5/8 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.68,false,true,286967,Photographs,Photograph,"Jérusalem, Birket-Hammam-Setty-Mariam",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 33.1 cm (9 1/8 x 13 1/16 in.) Mount: 44.5 x 59.6 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.69,false,true,286968,Photographs,Photograph,"Jérusalem, Tour de David",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.9 cm (9 3/16 x 12 15/16 in.) Mount: 44.6 x 59.6 cm (17 9/16 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.70,false,true,286969,Photographs,Photograph,"Jérusalem, Restes de scupltures judaïques",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33 cm (9 1/4 x 13 in.) Mount: 44.5 x 60.6 cm (17 1/2 x 23 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.71,false,true,283150,Photographs,Photograph,"Jérusalem, Sarcophage judaïque",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 × 32.7 cm (9 1/8 × 12 7/8 in.) Mount: 44.5 x 59.7 cm (17 1/2 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.72,false,true,286970,Photographs,Photograph,"Jérusalem, Casque trouvé dans le Jourdain, 1",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.3 cm (9 1/4 x 13 1/8 in.) Mount: 45.1 x 60.2 cm (17 3/4 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.73,false,true,286971,Photographs,Photograph,"Jérusalem, Casque trouvé dans le Jourdain, 2",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 33 cm (9 1/8 x 13 in.) Mount: 44.7 x 59.9 cm (17 5/8 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.74,false,true,286972,Photographs,Photograph,"Jérusalem, Fragments judaïque et romain",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 30.9 cm (9 1/4 x 12 3/16 in.) Mount: 44.8 x 59.7 cm (17 5/8 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.75,false,true,286973,Photographs,Photograph,"Jérusalem, Forteresse de Soin",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.3 cm (9 1/4 x 13 1/8 in.) Mount: 44.6 x 60 cm (17 9/16 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.76,false,true,286974,Photographs,Photograph,"Jérusalem, Arc de l'Ecce-Homo",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.2 x 23.5 cm (13 1/16 x 9 1/4 in.) Mount: 59.9 x 45.2 cm (23 9/16 x 17 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.77,false,true,286975,Photographs,Photograph,"Jérusalem, Arc de l'Ecce-Homo, Détails",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.3 x 23.3 cm (13 1/8 x 9 3/16 in.) Mount: 59.8 x 44.8 cm (23 9/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.78,false,true,286976,Photographs,Photograph,"Jérusalem, Fontaine de Saint-Philippe",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.7 x 23.6 cm (13 1/4 x 9 5/16 in.) Mount: 60.3 x 44.5 cm (23 3/4 x 17 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.79,false,true,286977,Photographs,Photograph,"Jérusalem, Colonne de la Porte judiciaire",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.2 x 23.6 cm (13 1/16 x 9 5/16 in.) Mount: 59.9 x 44.5 cm (23 9/16 x 17 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.80,false,true,286978,Photographs,Photograph,"Jérusalem, Grotte de Jérémie",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 32.5 cm (9 x 12 13/16 in.) Mount: 44.7 x 59.3 cm (17 5/8 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.81,false,true,286979,Photographs,Photograph,"Jérusalem, Via Dolorosa, Reste antique",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.3 x 23.3 cm (13 1/8 x 9 3/16 in.) Mount: 60.2 x 44.8 cm (23 11/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.83,false,true,286980,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Entrée principale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.4 cm (9 3/16 x 12 3/4 in.) Mount: 45 x 60.1 cm (17 11/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.84,false,true,286981,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Bas-relief (porte d'entrée)",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 31.6 cm (9 3/16 x 12 7/16 in.) Mount: 45.1 x 59.9 cm (17 3/4 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.85,false,true,286982,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Bas-relief (porte murée)",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.6 x 32.8 cm (9 5/16 x 12 15/16 in.) Mount: 44.3 x 60.5 cm (17 7/16 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.87,false,true,286983,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Détails de la porte",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.3 x 23.3 cm (12 11/16 x 9 3/16 in.) Mount: 60.3 x 44.7 cm (23 3/4 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.88,false,true,286984,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Détails des chapiteaux de la porte principale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.9 x 23.2 cm (12 15/16 x 9 1/8 in.) Mount: 59.6 x 44.4 cm (23 7/16 x 17 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.89,false,true,286985,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Partie supérieure de la façade",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.9 cm (9 3/16 x 12 15/16 in.) Mount: 44.6 x 60 cm (17 9/16 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.91,false,true,286986,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Chapelle du Calvaire",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.5 x 23.4 cm (12 13/16 x 9 3/16 in.) Mount: 59.8 x 44.6 cm (23 9/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.92,false,true,286987,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Détails de la Chapelle du Calvaire",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.7 x 23.4 cm (12 7/8 x 9 3/16 in.) Mount: 59.9 x 44.6 cm (23 9/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.93,false,true,286988,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Intérieur de la Chapelle du Calvaire",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.9 x 23.4 cm (12 15/16 x 9 3/16 in.) Mount: 59.9 x 44.7 cm (23 9/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.94,false,true,286989,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Détails de la façade",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.7 × 33.1 cm (8 15/16 × 13 1/16 in.) Mount: 59.9 x 44.6 cm (23 9/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.95,false,true,286990,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Vue générale, 1",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 × 33.1 cm (9 1/8 × 13 1/16 in.) Mount: 60 x 44.6 cm (23 5/8 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.98,false,true,286991,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Clocher",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.9 x 23.5 cm (12 15/16 x 9 1/4 in.) Mount: 60 x 45.2 cm (23 5/8 x 17 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.99,false,true,286992,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Détails du Clocher",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.5 × 23.5 cm (12 13/16 × 9 1/4 in.) Mount: 60.2 x 44.6 cm (23 11/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.101,false,true,286993,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Porte Ouest",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.3 × 23.7 cm (13 1/8 × 9 5/16 in.) Mount: 59.6 x 44.7 cm (23 7/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.102,false,true,286994,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Face Ouest, Rue du Patriarche",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.4 × 23.8 cm (12 3/4 × 9 3/8 in.) Mount: 59.5 x 45 cm (23 7/16 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.103,false,true,286995,Photographs,Photograph,"Jérusalem, Saint-Sépulcre, Colonne du parvis",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.6 x 23 cm (12 13/16 x 9 1/16 in.) Mount: 59.6 x 45 cm (23 7/16 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.104,false,true,286996,Photographs,Photograph,"Jérusalem, Epée de Godefroy de Bouillon",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.6 x 23.1 cm (12 13/16 x 9 1/8 in.) Mount: 60 x 44.5 cm (23 5/8 x 17 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.106,false,true,286997,Photographs,Photograph,"Jérusalem, Sainte-Marie-la-Grande, Portail",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 21.7 × 33.4 cm (8 9/16 × 13 1/8 in.) Mount: 44.9 x 59.4 cm (17 11/16 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.107,false,true,286998,Photographs,Photograph,"Jérusalem, Sainte-Marie-la-Grande, Détails de la porte",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.3 x 32.7 cm (9 3/16 x 12 7/8 in.) Mount: 44.7 x 60.3 cm (17 5/8 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.108,false,true,286999,Photographs,Photograph,"Jérusalem, Sainte-Marie-la-Grande, Cloître",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 32.9 cm (9 1/16 x 12 15/16 in.) Mount: 44.3 x 60.4 cm (17 7/16 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.109,false,true,287000,Photographs,Photograph,"Jérusalem, Sainte-Marie-la-Grande, Vue générale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.5 cm (9 1/4 x 13 3/16 in.) Mount: 44.9 x 59.7 cm (17 11/16 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.110,false,true,287001,Photographs,Photograph,"Jérusalem, Sainte-Marie-la-Latine",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.7 cm (9 3/16 x 12 7/8 in.) Mount: 44.9 x 61 cm (17 11/16 x 24 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.111,false,true,287002,Photographs,Photograph,"Jérusalem, Enceinte de l'Hopital des Chevaliers-de-Saint-Jean, Côté Sud",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33.1 cm (9 3/16 x 13 1/16 in.) Mount: 45.1 x 59.8 cm (17 3/4 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.112,false,true,287003,Photographs,Photograph,"Jérusalem, Fontaine du Couvent grec",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.3 cm (9 3/16 x 12 11/16 in.) Mount: 44.8 x 60 cm (17 5/8 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.113,false,true,287004,Photographs,Photograph,"Jérusalem, Église Sainte-Anne, Vue générale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.2 x 23.7 cm (13 1/16 x 9 5/16 in.) Mount: 60.4 x 44.8 cm (23 3/4 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.114,false,true,287005,Photographs,Photograph,"Jérusalem, Église Sainte-Anne, Façade",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.5 x 23.4 cm (12 13/16 x 9 3/16 in.) Mount: 59.3 x 44.6 cm (23 3/8 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.115,false,true,287006,Photographs,Photograph,"Jérusalem, Église Sainte-Anne, Détails du portail",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.7 cm (9 1/8 x 12 7/8 in.) Mount: 45.1 x 59.8 cm (17 3/4 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.116,false,true,287007,Photographs,Photograph,"Jérusalem, Tombeau de la Vierge",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33 cm (9 1/4 x 13 in.) Mount: 44.6 x 60.1 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.117,false,true,287008,Photographs,Photograph,"Jérusalem, Chapelle de l'Ascension",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 33.3 cm (9 1/8 x 13 1/8 in.) Mount: 44.7 x 59.5 cm (17 5/8 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.118,false,true,287009,Photographs,Photograph,"Jérusalem, Église de Sainte-Marie-Madeleine",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33.2 cm (9 3/16 x 13 1/16 in.) Mount: 45 x 59.3 cm (17 11/16 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.119,false,true,287010,Photographs,Photograph,"Jérusalem, Palais de rois de Jérusalem, Vue générale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.3 x 33.3 cm (9 3/16 x 13 1/8 in.) Mount: 44.8 x 59.6 cm (17 5/8 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.120,false,true,287011,Photographs,Photograph,"Jérusalem, Palais de rois de Jérusalem, Entrée principale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.9 x 23.3 cm (12 15/16 x 9 3/16 in.) Mount: 59.7 x 44.7 cm (23 1/2 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.121,false,true,287012,Photographs,Photograph,"Jérusalem, Auberge d'Allemagne",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.2 x 23.4 cm (13 1/16 x 9 3/16 in.) Mount: 60.1 x 44.7 cm (23 11/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.122,false,true,287013,Photographs,Photograph,"Jérusalem, Couvent Arménien, Ornements, 1",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 31 x 22.9 cm (12 3/16 x 9 in.) Mount: 59.9 x 44.8 cm (23 9/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.123,false,true,287014,Photographs,Photograph,"Jérusalem, Couvent Arménien, Ornements, 2",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 33 cm (9 1/8 x 13 in.) Mount: 44.9 x 60 cm (17 11/16 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.124,false,true,287015,Photographs,Photograph,"Jérusalem, Chapelle anglaise",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 32.6 cm (9 1/4 x 12 13/16 in.) Mount: 44.8 x 60 cm (17 5/8 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.125,false,true,287016,Photographs,Photograph,"Jérusalem, Beit-Lehem, Vue générale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.6 x 27.6 cm (8 7/8 x 10 7/8 in.) Mount: 44.6 x 60.3 cm (17 9/16 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.126,false,true,287017,Photographs,Photograph,"Beit-Lehem, Mosaïque de l'Église I",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.127,false,true,287018,Photographs,Photograph,"Beit-Lehem, Mosaïque de l'Église II",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.128,false,true,287019,Photographs,Photograph,"Jérusalem, Mosquée d'Omar, côté ouest",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33 cm (9 3/16 x 13 in.) Mount: 44.5 x 59.3 cm (17 1/2 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.129,false,true,287020,Photographs,Photograph,"Jérusalem, Mosquée d'Omar, côté est, Intérieur de l'enceinte, 1",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.1 x 26.8 cm (8 11/16 x 10 9/16 in.) Mount: 44.8 x 60.3 cm (17 5/8 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.130,false,true,287021,Photographs,Photograph,"Jérusalem, Mosquée d'Omar, côté Est, Intérieur de l'enceinte, 2",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.6 x 33 cm (8 7/8 x 13 in.) Mount: 44.8 x 60.2 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.131,false,true,287022,Photographs,Photograph,"Jérusalem, Mosquée d'Omar, côté Nord, Intérieur de l'enceinte",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.8 cm (9 3/16 x 12 15/16 in.) Mount: 44.5 x 59.8 cm (17 1/2 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.132,false,true,287023,Photographs,Photograph,"Jérusalem, Mosquée d'Omar, côté Ouest, Intérieur de l'enceinte",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 32.5 cm (9 x 12 13/16 in.) Mount: 44.6 x 59.6 cm (17 9/16 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.133,false,true,287024,Photographs,Photograph,"Jérusalem, Minaret de la Rue du Patriarche",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33 x 23.6 cm (13 x 9 5/16 in.) Mount: 60.3 x 44.6 cm (23 3/4 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.134,false,true,287025,Photographs,Photograph,"Jérusalem, Minaret de l'ancienne mosquée d'Abd-es-Samed",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.8 x 23.3 cm (12 15/16 x 9 3/16 in.) Mount: 59.4 x 45.1 cm (23 3/8 x 17 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.135,false,true,287026,Photographs,Photograph,"Jérusalem, Porte de Jaffa, Vue extérieure",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 31.8 x 23 cm (12 1/2 x 9 1/16 in.) Mount: 59.3 x 44.9 cm (23 3/8 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.136,false,true,287027,Photographs,Photograph,"Jérusalem, Porte de Jaffa, Vue générale",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.137,false,true,287028,Photographs,Photograph,"Jérusalem, Porte de Jaffa, Intérieur",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 31.9 x 23.1 cm (12 9/16 x 9 1/8 in.) Mount: 59.6 x 44.9 cm (23 7/16 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.138,false,true,287029,Photographs,Photograph,"Jérusalem, Porte de Jaffa, Inscription",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.139,false,true,287030,Photographs,Photograph,"Jérusalem, Porte de Jaffa, Inscription de la fausse porte",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.2 cm (9 1/4 x 13 1/16 in.) Mount: 44.5 x 59.5 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.140,false,true,287031,Photographs,Photograph,"Jérusalem, Porte de David, Vue extérieure",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.8 x 23.5 cm (12 15/16 x 9 1/4 in.) Mount: 59.8 x 44.8 cm (23 9/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.141,false,true,287032,Photographs,Photograph,"Jérusalem, Porte de David, Intérieur",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.1 x 23.7 cm (13 1/16 x 9 5/16 in.) Mount: 60.2 x 44.4 cm (23 11/16 x 17 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.142,false,true,287033,Photographs,Photograph,"Jérusalem, Porte de Mograbins",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.143,false,true,287034,Photographs,Photograph,"Jérusalem, Porte Saint-Étienne, Vue extérieure",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.4 x 23.7 cm (12 3/4 x 9 5/16 in.) Mount: 59.7 x 44.7 cm (23 1/2 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.144,false,true,287035,Photographs,Photograph,"Jérusalem, Porte Saint-Étienne, Intérieur",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 32.7 cm (9 1/16 x 12 7/8 in.) Mount: 45 x 59.8 cm (17 11/16 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.145,false,true,287036,Photographs,Photograph,"Jérusalem, Porte d'Hérode",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 33 cm (9 1/8 x 13 in.) Mount: 44.8 x 60.3 cm (17 5/8 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.146,false,true,287037,Photographs,Photograph,"Jérusalem, Porte de Damas, Vue extérieure",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 32.4 cm (9 x 12 3/4 in.) Mount: 44.4 x 59.6 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.147,false,true,287038,Photographs,Photograph,"Jérusalem, Porte de Damas, Intérieur",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 32.6 cm (9 1/8 x 12 13/16 in.) Mount: 44.5 x 59.5 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.148,false,true,287039,Photographs,Photograph,"Jérusalem, Escalier arabe de Sainte-Marie-la-Grande",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.9 x 23.4 cm (12 15/16 x 9 3/16 in.) Mount: 59.6 x 44.7 cm (23 7/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.149,false,true,287040,Photographs,Photograph,"Jérusalem, Escalier arabe de Sainte-Marie-la-Grande, Détails de la partie supérieure",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.7 x 23.4 cm (12 7/8 x 9 3/16 in.) Mount: 59.5 x 44.6 cm (23 7/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.150,false,true,287041,Photographs,Photograph,"Jérusalem, Hospital de Sainte-Hélène, Face Sud",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.5 x 23.7 cm (12 13/16 x 9 5/16 in.) Mount: 59.6 x 44.6 cm (23 7/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.151,false,true,287042,Photographs,Photograph,"Jérusalem, Hospital de Sainte-Hélène, Intérieur",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 33 cm (9 1/16 x 13 in.) Mount: 44.8 x 60.4 cm (17 5/8 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.152,false,true,287043,Photographs,Photograph,"Jérusalem, Fontaine Arabe, 1",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.153,false,true,287044,Photographs,Photograph,"Jérusalem, Fontaine Arabe, 2",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.1 x 23.5 cm (13 1/16 x 9 1/4 in.) Mount: 59.8 x 44.8 cm (23 9/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.154,false,true,287045,Photographs,Photograph,"Jérusalem, Fontaine Arabe, 3",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.1 x 23.5 cm (13 1/16 x 9 1/4 in.) Mount: 60.3 x 44.6 cm (23 3/4 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.155,false,true,287046,Photographs,Photograph,"Jérusalem, Fontaine Arabe, 4",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.8 x 23.4 cm (12 15/16 x 9 3/16 in.) Mount: 59.6 x 45 cm (23 7/16 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.156,false,true,287047,Photographs,Photograph,"Jérusalem, Rue du quartier arabe, 1",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.7 x 23.6 cm (12 7/8 x 9 5/16 in.) Mount: 59.6 x 44.6 cm (23 7/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.157,false,true,287048,Photographs,Photograph,"Jérusalem, Rue du quartier arabe, 2",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.5 x 23.6 cm (13 3/16 x 9 5/16 in.) Mount: 60.3 x 45 cm (23 3/4 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.158,false,true,287049,Photographs,Photograph,"Jérusalem, Ornements arabes",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.1 cm (9 3/16 x 12 5/8 in.) Mount: 44.8 x 60 cm (17 5/8 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.159,false,true,287050,Photographs,Photograph,"Jérusalem, Fenêtre arabe",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.5 x 23.3 cm (13 3/16 x 9 3/16 in.) Mount: 60.1 x 44.8 cm (23 11/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.160,false,true,287051,Photographs,Photograph,"Jérusalem, Détails de la porte d'un Dôme sépulcral",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 32.8 cm (9 1/16 x 12 15/16 in.) Mount: 44.8 x 59.9 cm (17 5/8 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.161,false,true,287052,Photographs,Photograph,"Jérusalem, Tombeau arabe",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.9 cm (9 1/8 x 12 15/16 in.) Mount: 44.9 x 60.4 cm (17 11/16 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.162,false,true,287053,Photographs,Photograph,"Jérusalem, Porte de la citadelle",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.9 x 23.1 cm (12 15/16 x 9 1/8 in.) Mount: 59.6 x 44.7 cm (23 7/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.163,false,true,287054,Photographs,Photograph,"Jérusalem, Porte de la citadelle, Inscription",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.5 x 33 cm (8 7/8 x 13 in.) Mount: 45 x 60.4 cm (17 11/16 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.164,false,true,287055,Photographs,Photograph,"Jérusalem, Aqueduc de Ponce-Pilate, Inscription",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 32.8 cm (9 x 12 15/16 in.) Mount: 45 x 59.4 cm (17 11/16 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.165,false,true,287056,Photographs,Photograph,"Jérusalem, Maison du mauvais riche",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 33.5 x 23.6 cm (13 3/16 x 9 5/16 in.) Mount: 60.3 x 44.8 cm (23 3/4 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.166,false,true,287057,Photographs,Photograph,"Jérusalem, Mont Sion",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.9 x 33.3 cm (9 x 13 1/8 in.) Mount: 45 x 59.5 cm (17 11/16 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.167,false,true,287058,Photographs,Photograph,"Jérusalem, Forteresse de David (citadelle), Face Ouest",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 32.6 x 23.7 cm (12 13/16 x 9 5/16 in.) Mount: 44.4 x 59.5 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.168,false,true,287059,Photographs,Photograph,"Jérusalem, Pins du Couvent arménien",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 32.9 cm (9 1/16 x 12 15/16 in.) Mount: 44.5 x 59.4 cm (17 1/2 x 23 3/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.169,false,true,287060,Photographs,Photograph,"Jérusalem, Côté Est de Jérusalem",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.6 x 32.2 cm (8 7/8 x 12 11/16 in.) Mount: 44.9 x 60.4 cm (17 11/16 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.170,false,true,287061,Photographs,Photograph,"Jérusalem, Côté Sud de Jérusalem",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33 cm (9 3/16 x 13 in.) Mount: 44.8 x 59.7 cm (17 5/8 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.171,false,true,287062,Photographs,Photograph,"Jérusalem, Chemin de Naplouse",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 33 cm (9 1/8 x 13 in.) Mount: 44.8 x 59.5 cm (17 5/8 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.172,false,true,287063,Photographs,Photograph,"Jérusalem, Chemin de Beit-Lehem",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.3 x 32.7 cm (9 3/16 x 12 7/8 in.) Mount: 44.5 x 59.5 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.173,false,true,287064,Photographs,Photograph,"Jérusalem, Vue générale de la Vallée de Hinnom",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.5 cm (9 1/4 x 13 3/16 in.) Mount: 44.5 x 59.5 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.174,false,true,287065,Photographs,Photograph,"Jérusalem, Côté Nord de Jérusalem",,,,,,Artist|Printer,,"Auguste Salzmann|Imprimerie photographique de Blanquart-Évrard, à Lille","French, 1824–1872|French, active 1851–55",,"Salzmann, Auguste|Imprimerie photographique de Blanquart-Évrard, à Lille",French|French,1824 |1851,1872 |1855,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 32.6 cm (9 x 12 13/16 in.) Mount: 45.1 x 60.1 cm (17 3/4 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/287065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.1,false,true,286901,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Côté Ouest, Heit-el-Morharby",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 × 33.2 cm (9 1/8 × 13 1/16 in.) Mount: 44.1 x 60 cm (17 3/8 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.2,false,true,286902,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Arche du Pont Salomonien qui reliait Moria à Sion",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.5 cm (9 1/8 x 12 13/16 in.) Mount: 44.6 x 59.6 cm (17 9/16 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.3,false,true,286903,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Mosquée El-Aksa, angle Sud-Ouest",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 32.6 cm (9 1/8 x 12 13/16 in.) Mount: 44.6 x 60 cm (17 9/16 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.4,false,true,286904,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Face sud de l'angle Sud-Est",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.3 x 32.8 cm (9 3/16 x 12 15/16 in.) Mount: 44.7 x 60.5 cm (17 5/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.5,false,true,286905,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Vue générale de la face Sud 1",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 31.7 cm (9 1/16 x 12 1/2 in.) Mount: 44.7 x 60.2 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.6,false,true,286906,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Vue générale de la face Sud 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 32.7 cm (9 1/8 x 12 7/8 in.) Mount: 44.7 x 60 cm (17 5/8 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.7,false,true,286907,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Porte hérodienne",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 32.9 x 23.1 cm (12 15/16 x 9 1/8 in.) Mount: 59.5 x 44.5 cm (23 7/16 x 17 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.8,false,true,286908,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Triple porte romaine",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 31.9 cm (9 3/16 x 12 9/16 in.) Mount: 44.6 x 59.6 cm (17 9/16 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.9,false,true,286909,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Moulure judaïque formant pied-droit de l'une des portes romaines",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 32.7 x 22.7 cm (12 7/8 x 8 15/16 in.) Mount: 60 x 44.9 cm (23 5/8 x 17 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.10,false,true,286910,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Face Sud de l'angle Sud-Est",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 33.1 x 23.6 cm (13 1/16 x 9 5/16 in.) Mount: 59.8 x 44.6 cm (23 9/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.11,false,true,286911,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Face Est de l'angle Sud-Est",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 33 x 23.5 cm (13 x 9 1/4 in.) Mount: 60 x 44.7 cm (23 5/8 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286911,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.12,false,true,286912,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Vue générale de la face Est, Pl. 1",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 31.8 cm (9 1/8 x 12 1/2 in.) Mount: 44.5 x 60 cm (17 1/2 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.13,false,true,286913,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Vue générale de la face Est, Pl. 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.5 x 32.9 cm (8 7/8 x 12 15/16 in.) Mount: 44.8 x 60.5 cm (17 5/8 x 23 13/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.14,false,true,286914,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Vue générale de la face Est, Pl. 3",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 × 33.2 cm (9 in. × 13 1/16 in.) Mount: 44.8 x 58.8 cm (17 5/8 x 23 1/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.15,false,true,286915,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Poterne de Josaphat",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 32.3 x 23.2 cm (12 11/16 x 9 1/8 in.) Mount: 60 x 44.7 cm (23 5/8 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.16,false,true,286916,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Porte Dorée",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 32 x 22.4 cm (12 5/8 x 8 13/16 in.) Mount: 60 x 44.8 cm (23 5/8 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.17,false,true,286917,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Détails de la Porte Dorée",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 32.2 x 22.9 cm (12 11/16 x 9 in.) Mount: 60 x 44.7 cm (23 5/8 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.18,false,true,286918,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Face Est de l'angle Nord-Est",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 31.8 cm (9 x 12 1/2 in.) Mount: 44.5 x 60 cm (17 1/2 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.19,false,true,286919,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Face Nord de l'angle Nord-Est",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 32.1 x 22.7 cm (12 5/8 x 8 15/16 in.) Mount: 59.5 x 44.7 cm (23 7/16 x 17 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.20,false,true,286920,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Piscine probatique, 1",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.7 x 32.8 cm (8 15/16 x 12 15/16 in.) Mount: 44.4 x 59.9 cm (17 1/2 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.21,false,true,286921,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Piscine probatique, 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 32.4 cm (9 1/8 x 12 3/4 in.) Mount: 44.7 x 60 cm (17 5/8 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.22,false,true,286922,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Détails de l'appareil de la piscine probatique",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 32.5 cm (9 x 12 13/16 in.) Mount: 44.6 x 60.2 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.23,false,true,286923,Photographs,Photograph,"Jérusalem, Enceinte du Temple, Angle Nord-Ouest et minaret élevé en l'an 697 de l'Hégire",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 33.1 x 23.4 cm (13 1/16 x 9 3/16 in.) Mount: 60.3 x 44.6 cm (23 3/4 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.24,false,true,286924,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Tombeaux antiques",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33 cm (9 3/16 x 13 in.) Mount: 44.6 x 60.1 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.25,false,true,286925,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Inscription tumulaire grecque, 1",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 32.7 cm (9 1/4 x 12 7/8 in.) Mount: 44.7 x 60.2 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.26,false,true,286926,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Inscription tumulaire grecque, 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 32.5 cm (9 1/16 x 12 13/16 in.) Mount: 44.5 x 59.8 cm (17 1/2 x 23 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.27,false,true,286927,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Retraite des Apôtres",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 32.8 cm (9 1/4 x 12 15/16 in.) Mount: 44.8 x 59.7 cm (17 5/8 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.28,false,true,286928,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Détails de la frise de la retraite des Apôtres",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 21.9 x 32.4 cm (8 5/8 x 12 3/4 in.) Mount: 44.6 x 58.9 cm (17 9/16 x 23 3/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.29,false,true,286929,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Ensemble du flanc droit",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33.3 cm (9 3/16 x 13 1/8 in.) Mount: 44.5 x 59.7 cm (17 1/2 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.30,false,true,286930,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Détails du flanc droit, 1",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 32.3 cm (9 1/8 x 12 11/16 in.) Mount: 44.7 x 60.4 cm (17 5/8 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.31,false,true,286931,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Détails du flanc droit, 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33 cm (9 1/4 x 13 in.) Mount: 44.6 x 49.7 cm (17 9/16 x 19 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.32,false,true,286932,Photographs,Photograph,"Jérusalem, Champ du sang",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33 cm (9 3/16 x 13 in.) Mount: 44.6 x 59.7 cm (17 9/16 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.33,false,true,286933,Photographs,Photograph,"Jérusalem, Vallée de Hinnom, Tombeau antique à fronton triangulaire et à crossettes",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 32.6 x 23.4 cm (12 13/16 x 9 3/16 in.) Mount: 59.6 x 44.4 cm (23 7/16 x 17 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.34,false,true,286934,Photographs,Photograph,"Jérusalem, Piscine de Siloe, Vue générale",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 32.5 cm (9 3/16 x 12 13/16 in.) Mount: 44.6 x 60.1 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.35,false,true,286935,Photographs,Photograph,"Jérusalem, Piscine de Siloe, Détails",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 33.3 cm (9 1/4 x 13 1/8 in.) Mount: 44.7 x 60.2 cm (17 5/8 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.36,false,true,286936,Photographs,Photograph,"Jérusalem, Piscine de Siloe, Canal taillé dans le roc",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.7 x 32.3 cm (8 15/16 x 12 11/16 in.) Mount: 44.6 x 60 cm (17 9/16 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.37,false,true,286937,Photographs,Photograph,"Jérusalem, Village de Siloam, Vue générale",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.2 cm (9 1/8 x 12 11/16 in.) Mount: 44.5 x 60 cm (17 1/2 x 23 5/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.38,false,true,286938,Photographs,Photograph,"Jérusalem, Village de Siloam, Monolithe de forme égyptienne, 1",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 33.1 cm (9 3/16 x 13 1/16 in.) Mount: 44.6 x 59.5 cm (17 9/16 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.39,false,true,286939,Photographs,Photograph,"Jérusalem, Village de Siloam, Monolithe de forme égyptienne, 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.1 x 32.4 cm (9 1/8 x 12 3/4 in.) Mount: 44.7 x 59.7 cm (17 5/8 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.40,false,true,286940,Photographs,Photograph,"Jérusalem, Village de Siloam, Monolithe de forme égyptienne, 3",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23 x 32.5 cm (9 1/16 x 12 13/16 in.) Mount: 44.7 x 60.7 cm (17 5/8 x 23 7/8 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.41,false,true,286941,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Faces Ouest et Nord, 1",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.8 x 32.8 cm (9 x 12 15/16 in.) Mount: 44.5 x 60.4 cm (17 1/2 x 23 3/4 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.42,false,true,286942,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Face Ouest et Nord, 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 32.5 cm (9 1/4 x 12 13/16 in.) Mount: 45 x 59.5 cm (17 11/16 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.43,false,true,286943,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Vue générale",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 22.3 x 32.2 cm (8 3/4 x 12 11/16 in.) Mount: 44.4 x 59.6 cm (17 1/2 x 23 7/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.44,false,true,286944,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Tombeau de Zacharie",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.4 x 31.1 cm (9 3/16 x 12 1/4 in.) Mount: 44.8 x 64.3 cm (17 5/8 x 25 5/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.45,false,true,286894,Photographs,Photograph,"Jérusalem, Vallée de Josaphat, Tombeau de St. Jacques",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.3 x 31.7 cm (9 3/16 x 12 1/2 in.) Mount: 44.6 x 59.7 cm (17 9/16 x 23 1/2 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.82,false,true,286797,Photographs,Photograph,"Jérusalem, Saint Sépulcre, Façade",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 33.3 x 23.7 cm (13 1/8 x 9 5/16 in.) Mount: 60.2 x 44.6 cm (23 11/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.86,false,true,286798,Photographs,Photograph,"Jérusalem, Saint Sépulcre, détails des chapiteaux",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 32.5 cm (9 1/8 x 12 13/16 in.) Mount: 44.6 x 60.2 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.90,false,true,286799,Photographs,Photograph,"Jérusalem, Saint Sépulcre, Vue générale de la Chapelle du Calvaire",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 33.3 x 23.7 cm (13 1/8 x 9 5/16 in.) Mount: 60.2 x 44.6 cm (23 11/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.96,false,true,286800,Photographs,Photograph,"Jérusalem, Saint Sépulcre, vue générale, 2",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.2 x 33 cm (9 1/8 x 13 in.) Mount: 44.6 x 60.2 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.97,false,true,286801,Photographs,Photograph,"Jérusalem, Saint Sépulcre, coupole",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 33.2 x 23.5 cm (13 1/16 x 9 1/4 in.) Mount: 60.2 x 44.6 cm (23 11/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286801,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.100,false,true,286802,Photographs,Photograph,"Jérusalem, Saint Sépulcre, abside",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper print from paper negative,Image: 23.5 x 31 cm (9 1/4 x 12 3/16 in.) Mount: 44.6 x 60.2 cm (17 9/16 x 23 11/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.373.105,false,true,286793,Photographs,Photograph,"Jérusalem, croix en argent ciselé, donnée par Louis XIII à l'Eglise du Saint Sépulcre",,,,,,Printer|Artist,,"Imprimerie photographique de Blanquart-Évrard, à Lille|Auguste Salzmann","French, active 1851–55|French, 1824–1872",,"Imprimerie photographique de Blanquart-Évrard, à Lille|Salzmann, Auguste",French|French,1851 |1824,1855 |1872,1854,1854,1859,Salted paper prints from paper negative,Image: 34.8 x 23.5 cm (13 11/16 x 9 1/4 in.) Mount: 60.2 x 44.6 cm (23 11/16 x 17 9/16 in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.54,false,true,283137,Photographs,Photograph,[The Ascent of Mont Blanc],,,,,,Artist|Photography Studio,,Auguste-Rosalie Bisson|Bisson Frères,"French, 1826–1900|French, active 1852–1863",,"Bisson, Auguste-Rosalie|Bisson Frères",French|French,1826 |1852,1900 |1863,1861,1861,1861,Albumen silver print from glass negative,Image: 39.6 x 23.7 cm (15 9/16 x 9 5/16 in.) Mount: 63 x 45.7 cm (24 13/16 x 18 in.),"Gilman Collection, Purchase, Alfred Stieglitz Society Gifts, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1249,false,true,286265,Photographs,Photograph,François Flameng,,,,,,Artist|Person in Photograph,Person in photograph,Edmond Bénard|François Flameng,"French, 1838–1907|French, Paris 1856–1923 Paris",,"Bénard, Edmond|Flameng, François",French|French,1838 |1856,1907 |1923,1880s–90s,1880,1899,Albumen silver print from glass negative,Image: 20.1 × 26 cm (7 15/16 × 10 1/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.65,false,true,294768,Photographs,Photograph,Charles Delahaye,,,,,,Artist|Person in Photograph,,Charles Marville|Charles Hippolyte Delahaye,"French, Paris 1813–1879 Paris|French, 1835/36– 1878 Paris",,"Marville, Charles|Delahaye, Charles Hippolyte",French|French,1813 |1835,1879 |1878,1852–53,1852,1853,Salted paper print from paper negative,Image: 21.6 x 15.9 cm (8 1/2 x 6 1/4 in.) Mount: 45 x 31.5 cm (17 11/16 x 12 3/8 in.),"Purchase, W. Bruce and Delaney H. Lundberg and Christian Keesee Charitable Trust Gifts, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/294768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.57,true,true,282190,Photographs,Photograph,Pierrot Laughing,,,,,,Artist|Artist|Person in Photograph,,Adrien Tournachon|Nadar|Jean-Charles Deburau,"French, 1825–1903|French, Paris 1820–1910 Paris|French, 1829–1873",,"Tournachon, Adrien|Nadar|Deburau, Jean-Charles",French|French,1825 |1820 |1829,1903 |1910 |1873,1855,1855,1855,Gelatin-coated salted paper print (vernis-cuir),27.3 x 19.8 cm (10 3/4 x 7 13/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1998",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/282190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.43,false,true,283118,Photographs,Photograph,Pierrot Running,,,,,,Artist|Artist|Person in Photograph,Person in photograph,Adrien Tournachon|Nadar|Jean-Charles Deburau,"French, 1825–1903|French, Paris 1820–1910 Paris|French, 1829–1873",,"Tournachon, Adrien|Nadar|Deburau, Jean-Charles",French|French,1825 |1820 |1829,1903 |1910 |1873,1854–55,1854,1855,Albumen silver print from glass negative,Image: 26.5 x 20.8cm (10 7/16 x 8 3/16in.) Mount: 41.3 x 34.5 cm (16 1/4 x 13 9/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.1–.48,false,true,305829,Photographs,Portfolio,Mécanisme de la physionomie humaine ou Analyse électro-physiologique de l’expression des passions applicable à la pratique des arts plastiques,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver prints from glass negatives,,"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Portfolios,,http://www.metmuseum.org/art/collection/search/305829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1156,false,true,285664,Photographs,Stereograph; Autochrome,[Tulips],,,,,,Artist|Artist,,Auguste-Marie-Louis-Nicolas Lumière|Louis-Jean Lumière,"French, Besançon 1862–1954 Lyon|French, Besançon 1864–1948 Bandol",and,"Lumière, Auguste-Marie-Louis-Nicolas|Lumière, Louis-Jean",French|French,1862 |1864,1954 |1948,1896–1903,1896,1903,Trichromie,8.5 x 17.8cm,"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/285664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1157,false,true,285666,Photographs,Stereograph; Autochrome,[Still Life of Flowers in a Stein],,,,,,Artist|Artist,,Auguste-Marie-Louis-Nicolas Lumière|Louis-Jean Lumière,"French, Besançon 1862–1954 Lyon|French, Besançon 1864–1948 Bandol",and,"Lumière, Auguste-Marie-Louis-Nicolas|Lumière, Louis-Jean",French|French,1862 |1864,1954 |1948,1896–1903,1896,1903,Trichromie,2 13/16 × 6 9/16 in. (7.2 × 16.7 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/285666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1155,false,true,285665,Photographs,Autochrome,[Peacock],,,,,,Artist|Artist,,Louis-Jean Lumière|Auguste-Marie-Louis-Nicolas Lumière,"French, Besançon 1864–1948 Bandol|French, Besançon 1862–1954 Lyon",and,"Lumière, Louis-Jean|Lumière, Auguste-Marie-Louis-Nicolas",French|French,1864 |1862,1948 |1954,ca. 1907,1902,1912,Autochrome,13.0 x 17.7 cm (5 1/8 x 7in.),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Transparencies,,http://www.metmuseum.org/art/collection/search/285665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.631,false,true,286695,Photographs,Photograph,[Empress Eugénie and the Prince Imperial],,,,,,Artist|Artist,Possibly by|Attributed to,Louise Deglane|François-Benjamin-Maria Delessert,"French|French, 1817–1868",,"Deglane, Louise|Delessert, François-Benjamin-Marie",French|French,1817,1868,1862,1862,1862,Albumen silver print from glass negative,21.6 x 16 cm (8 1/2 x 6 5/16 in.),"Gilman Collection, Purchase, Gift of The Howard Gilman Foundation, by exchange, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.248,false,true,266904,Photographs,Photograph,Icono-photographique. Mécanisme de la Physionomie Humaine. Fig. 65,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,12.0 x 9.2 cm (4 3/4 x 3 5/8 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.229,false,true,307063,Photographs,Photograph,Faradisation du muscle frontal,,,,,,Artist|Artist,Possibly with,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image: 18.6 × 11.7 cm (7 5/16 × 4 5/8 in.) Mount: 27 × 18.6 cm (10 5/8 × 7 5/16 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/307063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.1,false,true,623026,Photographs,Photograph,Figure 3: The face of an old man... photographed in repose.,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.3 cm (11 1/8 × 8 in.) Sheet: 30 × 22.9 cm (11 13/16 × 9 in.) Mount: 40.1 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.2,false,true,623027,Photographs,Photograph,Figure 4: The face in repose of a young man,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.3 cm (11 1/4 × 8 in.) Sheet: 30 × 22.8 cm (11 13/16 × 9 in.) Mount: 40.1 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.3,false,true,623028,Photographs,Photograph,Figure 6: The grimice produced is similar to a tic of the face,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.4 cm (11 1/8 × 8 1/16 in.) Sheet: 29.8 × 22.4 cm (11 3/4 × 8 13/16 in.) Mount: 40.1 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.4,false,true,623029,Photographs,Photograph,Figure 8: Contraction of the right m. frontalis.,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.2 cm (11 3/16 × 7 15/16 in.) Sheet: 29.8 × 22.9 cm (11 3/4 × 9 in.) Mount: 40.1 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.5,false,true,623030,Photographs,Photograph,Figure 9: A study of m. frontalis in maximum contraction,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.3 cm (11 3/16 × 8 in.) Sheet: 29.6 × 22.1 cm (11 5/8 × 8 11/16 in.) Mount: 40.2 × 28.2 cm (15 13/16 × 11 1/8 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.6,false,true,623031,Photographs,Photograph,Figure 10: Showing the expressive lines of m. frontalis in a young girl,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.4 cm (11 3/16 × 8 1/16 in.) Sheet: 30 × 22.4 cm (11 13/16 × 8 13/16 in.) Mount: 40.2 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.7,false,true,623032,Photographs,Photograph,Figure 12: A study of the contraction of and the expression produced by the superior part of m. orbicularis oculi,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.4 cm (11 1/8 × 8 1/16 in.) Sheet: 29.5 × 22.1 cm (11 5/8 × 8 11/16 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.8,false,true,623033,Photographs,Photograph,"Figure 15: Mediation, mental concentration",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.3 cm (11 1/8 × 8 in.) Sheet: 29.9 × 22.1 cm (11 3/4 × 8 11/16 in.) Mount: 40.1 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.9,false,true,623034,Photographs,Photograph,Figure 16: Expression of severity,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 29.9 × 21.9 cm (11 3/4 × 8 5/8 in.) Mount: 40.2 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.10,false,true,623035,Photographs,Photograph,"Figure 17: On the right, electrization of m. procerus: severity, aggression. On the left: attention.",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.5 cm (11 1/8 × 8 1/16 in.) Sheet: 29.7 × 22.7 cm (11 11/16 × 8 15/16 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.11,false,true,623036,Photographs,Photograph,"Figure 18: Aggression, wickedness",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 29.9 × 22.6 cm (11 3/4 × 8 7/8 in.) Mount: 40 × 28.3 cm (15 3/4 × 11 1/8 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.12,false,true,623037,Photographs,Photograph,Figure 19: Suffering,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,"Image (Oval): 28.2 × 20.4 cm (11 1/8 × 8 1/16 in.) Sheet: 29.4 cm, 22.2 gr (11 9/16) Mount: 40.3 × 28 cm (15 7/8 × 11 in.)","Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.13,false,true,623038,Photographs,Photograph,"Figure 20: Profound suffering, with resignation",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 29.8 × 22.4 cm (11 3/4 × 8 13/16 in.) Mount: 40.3 × 28.5 cm (15 7/8 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.14,false,true,623039,Photographs,Photograph,Figure 21: Painful recollection and recollection or calling something to mind,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.3 cm (11 3/16 × 8 in.) Sheet: 29.7 × 22.7 cm (11 11/16 × 8 15/16 in.) Mount: 40.2 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.15,false,true,623040,Photographs,Photograph,Figure 22: No painful expression,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 29.7 × 22.6 cm (11 11/16 × 8 7/8 in.) Mount: 40.3 × 28.1 cm (15 7/8 × 11 1/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.16,false,true,623041,Photographs,Photograph,"Figure 24: Extreme pain to the point of exhaustion, the head of Christ and memory of love or ecstatic gaze.",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.3 cm (11 1/8 × 8 in.) Sheet: 29.9 × 22 cm (11 3/4 × 8 11/16 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.17,false,true,623042,Photographs,Photograph,Figure 25: Not an expression of pain,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.4 cm (11 3/16 × 8 1/16 in.) Sheet: 29.3 × 22 cm (11 9/16 × 8 11/16 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.18,false,true,623043,Photographs,Photograph,"Figure 26: Expression of painful attention and attention, attentive gaze.",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.3 cm (11 1/4 × 8 in.) Sheet: 29.7 × 22.5 cm (11 11/16 × 8 7/8 in.) Mount: 40.3 × 28 cm (15 7/8 × 11 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.19,false,true,623044,Photographs,Photograph,Figure 27: Expression proportionally more pained,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.3 cm (11 3/16 × 8 in.) Sheet: 29.9 × 22.2 cm (11 3/4 × 8 3/4 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.20,false,true,623045,Photographs,Photograph,Figure 34: Grimace,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.4 cm (11 3/16 × 8 1/16 in.) Sheet: 29.9 × 22.5 cm (11 3/4 × 8 7/8 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.21,false,true,623046,Photographs,Photograph,Figure 36: Scornful laughter and scornful disgust,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.2 × 20.5 cm (11 1/8 × 8 1/16 in.) Sheet: 29.6 × 22.3 cm (11 5/8 × 8 3/4 in.) Mount: 40.2 × 28.1 cm (15 13/16 × 11 1/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.22,false,true,623047,Photographs,Photograph,Figure 39: The attention attracted by an object that provokes lascivious ideas and desires.,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 29.8 × 22.8 cm (11 3/4 × 9 in.) Mount: 40.2 × 28.2 cm (15 13/16 × 11 1/8 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.23,false,true,623048,Photographs,Photograph,"Figure 42: Gaiety expressed by the ideas of lustfulness, cynicism, and lewdness.",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.6 × 20.3 cm (11 1/4 × 8 in.) Sheet: 29.9 × 22.4 cm (11 3/4 × 8 13/16 in.) Mount: 40.2 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.24,false,true,623049,Photographs,Photograph,Figure 45: Pain and despair.,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.6 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 29.8 × 22.9 cm (11 3/4 × 9 in.) Mount: 40.1 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.25,false,true,623050,Photographs,Photograph,Figure 47: A suggestion of this same weeping,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 27.9 × 20.3 cm (11 × 8 in.) Sheet: 29.6 × 21.8 cm (11 5/8 × 8 9/16 in.) Mount: 40.2 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.26,false,true,623051,Photographs,Photograph,"Figure 48: Mild weeping, pity and feeble false laughter",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 30.2 × 23.1 cm (11 7/8 × 9 1/8 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.27,false,true,623052,Photographs,Photograph,Figure 49: Painful weeping and forward looking.,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.6 × 20.5 cm (11 1/4 × 8 1/16 in.) Sheet: 29.5 × 22.8 cm (11 5/8 × 9 in.) Mount: 40.1 × 28.7 cm (15 13/16 × 11 5/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.28,false,true,623053,Photographs,Photograph,Figure 50: Affected weeping and face in repose,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 30.1 × 22.6 cm (11 7/8 × 8 7/8 in.) Mount: 40.3 × 28.4 cm (15 7/8 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.29,false,true,623054,Photographs,Photograph,Figure 52: Voluntary retraction of the lower lip,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.3 cm (11 3/16 × 8 in.) Sheet: 29.2 × 22.2 cm (11 1/2 × 8 3/4 in.) Mount: 40.1 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623054,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.30,false,true,623055,Photographs,Photograph,Figure 53: Whimpering and false laughter,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.6 × 20.5 cm (11 1/4 × 8 1/16 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.) Sheet: 29.7 × 22.1 cm (11 11/16 × 8 11/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.31,false,true,623056,Photographs,Photograph,Figure 54: Voluntary lowering of the lower jaw,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.6 × 20.6 cm (11 1/4 × 8 1/8 in.) Sheet: 30 × 23.1 cm (11 13/16 × 9 1/8 in.) Mount: 40.3 × 28.6 cm (15 7/8 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623056,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.32,false,true,623057,Photographs,Photograph,Figure 55: Astonishment badly rendered by the subject: a ridiculous and inane expression.,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.4 cm (11 1/8 × 8 1/16 in.) Sheet: 29.8 × 23.1 cm (11 3/4 × 9 1/8 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.33,false,true,623058,Photographs,Photograph,Figure 56: Surprise,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.3 cm (11 1/8 × 8 in.) Sheet: 30 × 22.6 cm (11 13/16 × 8 7/8 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.34,false,true,623059,Photographs,Photograph,"Figure 57: Astonishment, stupefaction, amazement",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,"Image (Oval): 28.3 × 20.3 cm (11 1/8 × 8 in.) Sheet: 29.9 × 22.7 cm (11 3/4 in., 22.7 kg) Mount: 40.2 × 28.4 cm (15 13/16 × 11 3/16 in.)","Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.35,false,true,623060,Photographs,Photograph,Figure 60: Fright,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.3 cm (11 1/8 × 8 in.) Sheet: 29.8 × 23 cm (11 3/4 × 9 1/16 in.) Mount: 40.2 × 28.5 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.36,false,true,623061,Photographs,Photograph,"Figure 62: Terror, semiprofile",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image: 28.4 × 20.3 cm (11 3/16 × 8 in.) Sheet: 29.7 × 22.3 cm (11 11/16 × 8 3/4 in.) Mount: 40.3 × 28.5 cm (15 7/8 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.37,false,true,623062,Photographs,Photograph,Figure 63: Expression of terror,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.2 × 20.3 cm (11 1/8 × 8 in.) Sheet: 29.5 × 22.2 cm (11 5/8 × 8 3/4 in.) Mount: 40.3 × 28.5 cm (15 7/8 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.38,false,true,623063,Photographs,Photograph,"Figure 66: Head of Arrotino (the spy, the knife grinder, and so on)",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 30 × 22.7 cm (11 13/16 × 8 15/16 in.) Mount: 40.1 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.39,false,true,623064,Photographs,Photograph,Figure 70: Head of the Laocoön of Rome,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.2 × 20.3 cm (11 1/8 × 8 in.) Sheet: 29.1 × 21.4 cm (11 7/16 × 8 7/16 in.) Mount: 40.3 × 28.6 cm (15 7/8 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.40,false,true,623065,Photographs,Photograph,Figure 71: Same head as in Plate 70,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.3 × 20.4 cm (11 1/8 × 8 1/16 in.) Sheet: 29.7 × 22.7 cm (11 11/16 × 8 15/16 in.) Mount: 40.2 × 28.2 cm (15 13/16 × 11 1/8 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.41,false,true,623066,Photographs,Photograph,Figure 73: Head of Niobe,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.6 × 20.4 cm (11 1/4 × 8 1/16 in.) Sheet: 29.2 × 21.5 cm (11 1/2 × 8 7/16 in.) Mount: 40.1 × 28.3 cm (15 13/16 × 11 1/8 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.42,false,true,623067,Photographs,Photograph,Figure 75: Nun saying her prayers,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.8 × 22 cm (11 5/16 × 8 11/16 in.) Sheet: 30.5 × 23.4 cm (12 × 9 3/16 in.) Mount: 40.2 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.43,false,true,623068,Photographs,Photograph,Figure 78: Scene of coquetry,,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.5 × 20.3 cm (11 1/4 × 8 in.) Sheet: 30.1 × 21.9 cm (11 7/8 × 8 5/8 in.) Mount: 40.2 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.44,false,true,623069,Photographs,Photograph,"Figure 81: Lady Macbeth, moderate expression of cruelty",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image: 28.4 × 22.7 cm (11 3/16 × 8 15/16 in.) Sheet: 29.7 × 23.6 cm (11 11/16 × 9 5/16 in.) Mount: 40.2 × 28.6 cm (15 13/16 × 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.45,false,true,623070,Photographs,Photograph,"Figure 82: Lady Macbeth, strong expression of cruelty",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image (Oval): 28.4 × 20.3 cm (11 3/16 × 8 in.) Sheet: 29.2 × 21.9 cm (11 1/2 × 8 5/8 in.) Mount: 40.3 × 28.1 cm (15 7/8 × 11 1/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.173.46,false,true,623071,Photographs,Photograph,"Figure 83: Lady Macbeth, ferocious cruelty",,,,,,Artist|Artist,,Guillaume-Benjamin-Amand Duchenne de Boulogne|Adrien Tournachon,"French, 1806–1875|French, 1825–1903",,"Duchenne de Boulogne, Guillaume-Benjamin-Amand|Tournachon, Adrien",French|French,1806 |1825,1875 |1903,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image: 28.3 × 21.4 cm (11 1/8 × 8 7/16 in.) Sheet: 30 × 22.9 cm (11 13/16 × 9 in.) Mount: 40.2 × 28.4 cm (15 13/16 × 11 3/16 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors; Harris Brisbane Dick and William E. Dodge Funds; and W. Bruce and Delaney H. Lundberg Gift, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/623071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.417,true,true,267087,Photographs,Photograph,[Landscape with Cottage],,,,,,Artist|Artist,,Marie-Charles-Isidore Choiselat|Stanislas Ratel,"French, 1815–1858|French, 1824–1904",,"Choiselat, Marie-Charles-Isidore & Stanislas Ratel|Ratel, Stanislas",French|French,1815 |1824,1858 |1904,1844,1844,1844,Daguerreotype,16.4 x 21.7 cm (6 7/16 x 8 9/16 in.),"Louis V. Bell Fund, 1994",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267087,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.29,false,true,283103,Photographs,Photograph,[The Pavillon de Flore and the Tuileries Gardens],,,,,,Artist|Artist,,Marie-Charles-Isidore Choiselat|Stanislas Ratel,"French, 1815–1858|French, 1824–1904",,"Choiselat, Marie-Charles-Isidore & Stanislas Ratel|Ratel, Stanislas",French|French,1815 |1824,1858 |1904,1849,1849,1849,Daguerreotype,15.2 x 18.7 cm (6 x 7 3/8 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.185,false,true,286110,Photographs,Photograph,Défilé sur le Pont-Royal,,,,,,Artist|Artist,,Marie-Charles-Isidore Choiselat|Stanislas Ratel,"French, 1815–1858|French, 1824–1904",,"Choiselat, Marie-Charles-Isidore & Stanislas Ratel|Ratel, Stanislas",French|French,1815 |1824,1858 |1904,"May 1, 1844",1844,1844,Daguerreotype,Image: 6 1/16 × 4 7/16 in. (15.4 × 11.3 cm) Frame: 8 3/4 × 7 1/16 in. (22.3 × 18 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.954,false,true,629811,Photographs,Photograph,"[Pompeii, Pompey's Lane (or Street of Pompeii), Tomb Monument of Mamia]",,,,,,Artist|Artist,,Firmin-Eugène Le Dien|Gustave Le Gray,"French, 1817–1865|French, 1820–1884",,"Le Dien, Firmin-Eugène|Le Gray, Gustave",French|French,1817 |1820,1865 |1884,ca. 1853,1848,1858,Salted paper print from a waxed paper negative,Mount: 13 13/16 in. × 19 3/16 in. (35.1 × 48.8 cm) Image: 8 15/16 × 12 7/16 in. (22.7 × 31.6 cm),"Purchase, Mr. and Mrs. John A. Moran Gift, in memory of Louise Chisholm Moran, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/629811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.955,false,true,629831,Photographs,Photograph,"Amalfi, Cathedral",,,,,,Artist|Artist,,Firmin-Eugène Le Dien|Gustave Le Gray,"French, 1817–1865|French, 1820–1884",,"Le Dien, Firmin-Eugène|Le Gray, Gustave",French|French,1817 |1820,1865 |1884,1853,1853,1853,Salted paper print from a waxed paper negative,Mount: 19 5/16 in. × 13 13/16 in. (49.1 × 35.1 cm) Image: 13 1/8 × 9 5/16 in. (33.3 × 23.7 cm),"Purchase, Alfred Stieglitz Society Gifts, 2013",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/629831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.108,false,true,268645,Photographs,Photograph,Maison Élevée Rue St. Georges par M. Renaud,,,,,,Artist|Artist,From a daguerreotype plate by,Armand-Hippolyte-Louis Fizeau|Noël-Marie-Paymal Lerebours,"French, 1819–1896|French, 1807–1873",,"Fizeau, Armand-Hippolyte-Louis|Lerebours, Noël-Marie-Paymal",French|French,1819 |1807,1896 |1873,ca. 1841,1839,1843,Photogravure,,"Gift of the Museum of Modern Art, 1939",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.152,false,true,269123,Photographs,Photograph,Excursions Daguerriennes. Vues et monuments les plus remarquables du globe,,,,,,Artist|Artist,,Armand-Hippolyte-Louis Fizeau|Noël-Marie-Paymal Lerebours,"French, 1819–1896|French, 1807–1873",,"Fizeau, Armand-Hippolyte-Louis|Lerebours, Noël-Marie-Paymal",French|French,1819 |1807,1896 |1873,1840s,1840,1849,"Etchings, aquatints, lithographs, and photogravures after daguerreotypes",,"David Hunter McAlpin Fund, 1947",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/269123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.140,false,true,302241,Photographs,Photograph,"Electro–Physiologie, Figure 64",,,,,,Artist|Artist,,Adrien Tournachon|Guillaume-Benjamin-Amand Duchenne de Boulogne,"French, 1825–1903|French, 1806–1875",,"Tournachon, Adrien|Duchenne de Boulogne, Guillaume-Benjamin-Amand",French|French,1825 |1806,1903 |1875,"1854–56, printed 1862",1854,1856,Albumen silver print from glass negative,Image: 29.8 x 22.3 cm (11 3/4 x 8 3/4 in.) Mount: 40.1 x 28.5 cm (15 13/16 x 11 1/4 in.),"Purchase, The Buddy Taub Foundation Gift, Dennis A. Roach and Jill Roach, Directors, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.558,false,true,286696,Photographs,Photograph,Marine Terrace,,,,,,Artist|Artist,,Charles Victor Hugo|Auguste Vacquerie,"French, 1826–1871|French, 1819–1855",,"Hugo, Charles Victor|Vacquerie, Auguste",French|French,1826 |1819,1871 |1855,"October 9, 1855",1855,1855,Salted paper print from glass negative,Image: 6.4 x 9.5 cm (2 1/2 x 3 3/4 in.) Mount: 5 13/16 × 8 11/16 in. (14.8 × 22.1 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.819,false,true,286434,Photographs,Photograph,Victor Hugo,,,,,,Artist|Artist,,Charles Victor Hugo|Auguste Vacquerie,"French, 1826–1871|French, 1819–1855",,"Hugo, Charles Victor|Vacquerie, Auguste",French|French,1826 |1819,1871 |1855,1852,1852,1852,Salted paper print from paper negative,Image: 3 15/16 in. × 3 in. (10 × 7.6 cm),"Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/286434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.1054,false,true,265094,Photographs,Photograph,Chronophotograph,,,,,,Artist|Artist,,Etienne-Jules Marey|Charles Fremont,"French, 1830–1904|French, 1855–1930",,"Marey, Etienne-Jules|Fremont, Charles",French|French,1830 |1855,1904 |1930,1894,1894,1894,Gelatin silver print from glass negative,16.3 x 20.2 cm (6 7/16 x 7 15/16 in.),"Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel and Rogers Fund, 1987",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/265094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.219,false,true,266895,Photographs,Photograph,Nebuleuse de la Lyre,,,,,,Artist|Artist,,Paul Henry|Prosper Henry,"French, 1848–1905|French, 1849–1903",,"Henry, Paul|Henry, Prosper",French|French,1848 |1849,1905 |1903,ca. 1885,1883,1887,Albumen silver print from glass negative,Image: 22.8 x 16.4 cm. (9 x 6 7/16 in.),"Gift of Arnold H. Crane, by exchange, and Purchase, The Horace W. Goldsmith Foundation Gift through Joyce and Robert Menschel, 1993",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/266895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.124,false,true,283255,Photographs,Photograph,"A Section of the Constellation Cygnus (August 13, 1885)",,,,,,Artist|Artist,,Paul Henry|Prosper Henry,"French, 1848–1905|French, 1849–1903",,"Henry, Paul|Henry, Prosper",French|French,1848 |1849,1905 |1903,1885,1885,1885,Albumen silver print from glass negative,25.8 x 21.2cm (10 3/16 x 8 3/8in.) Mount: 31.4 × 23.8 cm (12 3/8 × 9 3/8 in.),"Gilman Collection, Purchase, Robert Rosenkranz Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.59,false,true,299470,Photographs,Photograph,[Male Musculature Study],,,,,,Artist|Artist,,Albert Londe|Paul Marie Louis Pierre Richer,"French, 1858–1917|French, 1849–1933",,"Londe, Albert|Richer, Paul Marie Louis Pierre",French|French,1858 |1849,1917 |1933,ca. 1890,1885,1895,Albumen silver print,Image: 14.9 x 9.6 cm (5 7/8 x 3 3/4 in.) Mount: 14.9 x 9.9 cm (5 7/8 x 3 7/8 in.),"Gift of Charles Isaacs and Carol Nigro, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.34,false,true,283107,Photographs,Photograph,The Ramparts of Carcassonne,,,,,,Artist|Artist,,Gustave Le Gray|Auguste Mestral,"French, 1820–1884|French, Rans 1812–1884 Rans",,"Le Gray, Gustave|Mestral, Auguste",French|French,1820 |1812,1884 |1884,1851,1851,1851,Salted paper print from waxed paper negative,Image: 23.5 x 33.2 cm (9 1/4 x 13 1/16 in.) Mount: 31.2 x 45.2 cm (12 5/16 x 17 13/16 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.799 (1-30),false,true,285904,Photographs,Album,Roma,,,,,,Artist|Publisher,,Eugène Constant|Edouard Mauche et Cie,"French, active Italy, 1848–55",,"Constant, Eugène|Edouard Mauche et Cie",French|French,1848,1855,1848–52,1848,1852,Salted paper print from glass negative,Prints approx. 8 3/4 x 11 1/4,"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.1–.16,false,true,285641,Photographs,Album,The Evacuation of Fort Sumter,,,,,,Publisher|Publisher,,Osborn's Gallery|Edward Anthony,"American, active Charleston, South Carolina, 1850s–1860s|American, 1818–1888",,"Osborn's Gallery|Anthony, Edward",American|American,1850 |1818,1869 |1888,April 1861,1861,1861,Albumen silver prints from glass negatives,Album: 12.6 × 9.4 × 2.5 cm (4 15/16 × 3 11/16 × 1 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/285641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.89,false,true,282753,Photographs,Album; Photographs; Locket; Pendant,[Miniature Wedding Album of General Tom Thumb and Lavinia Warren],,,,,,Photography Studio|Person in Photograph|Person in Photograph,,Mathew B. Brady|General Tom Thumb|Lavinia Warren,"American, born Ireland, 1823?–1896 New York|American, 1838–1883|American, 1841–1919",,"Brady, Mathew B.|Thumb, Tom|Warren, Lavinia",American|American,1823 |1838 |1841,1896 |1883 |1919,ca. 1863,1862,1864,"Albumen silver prints, brass","Overall: 1 1/16 × 13/16 × 3/8 in. (2.7 × 2 × 1 cm) Images: 7/8 × 13/16 in. (2.3 × 2 cm), each","Joyce F. Menschel Photography Library Fund, 1999",,,,,,,,,,,,Albums|Jewelry,,http://www.metmuseum.org/art/collection/search/282753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.479,false,true,299298,Photographs,Postcard,"Animas Canyon, Colorado",,,,,,Publisher|Artist,,Detroit Publishing Company|William Henry Jackson,"American|American, 1843–1942",,"Detroit Publishing Company|Jackson, William Henry",American|American,1843,1942,1906,1906,1906,Chromolithograph,Image: 8.1 x 12.3 cm (3 3/16 x 4 13/16 in.) 8.9 x 14 cm (3 1/2 x 5 1/2 in.) Frame: 27.9 x 35.6 cm (11 x 14 in.),"Funds from various donors, 2011",,,,,,,,,,,,Prints,,http://www.metmuseum.org/art/collection/search/299298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.476,false,true,299309,Photographs,Photograph,"Slave Pen, Alexandria, Virginia",,,,,,Photography Studio|Publisher,,Brady & Co.|E. & H. T. Anthony,"American, active 1840s–1880s|American",,Brady & Co.|E. & H. T. Anthony,American|American,1840,1889,1862,1862,1862,Albumen silver print from glass negative,Image: 8 x 16 cm (3 1/8 x 6 5/16 in.) Mount: 8.4 x 17.5 cm (3 5/16 x 6 7/8 in.),"The Horace W. Goldsmith Foundation Fund, through Joyce and Robert Menschel, 2011",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1065.14,false,true,263363,Photographs,Photograph,Nude,,,,,,Printer|Artist,,Morris & Bendien|Charles W. Gilhousen,"American, New York|American, 1867–1929",,"Morris & Bendien|Gilhousen, Charles W.",American|American,1867,1929,1917,1917,1917,Gelatin silver print,Image: 9 3/16 × 6 11/16 in. (23.4 × 17 cm) Mount: 9 9/16 in. × 7 5/16 in. (24.3 × 18.5 cm),"Gift of Rita McNamara Pleet, 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.457.916,false,true,291117,Photographs,Stereograph,"[Stereographic View of Statue of Simon Bolivar by R. de la Cora, Central Park, New York]",,,,,,Publisher|Publisher,,Edward Anthony|Henry T. Anthony,"American, 1818–1888|American, 1814–1884",,"Anthony, Edward|Anthony, Henry T.",American|American,1818 |1814,1888 |1884,1884–98,1884,1898,Gelatin silver print from glass negative,"Image: 8.3 x 14.5 cm (3 1/4 x 5 11/16 in.), overall Mount: 8.8 x 17.7 cm (3 7/16 x 6 15/16 in.)","Herbert Mitchell Collection, 2007",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.100,false,true,283210,Photographs,Photograph,Sky Chief (Tirawahut Resaru),,,,,,Artist|Printer,Attributed to,Edric L. Eaton|William Henry Jackson,"American, 1836–1890|American, 1843–1942",,"Eaton, Edric L.|Jackson, William Henry",American|American,1836 |1843,1890 |1942,ca. 1867,1865,1869,Albumen silver print from glass negative,Image: 18.6 × 13.2 cm (7 5/16 × 5 3/16 in.) Mount: 35.4 × 27.8 cm (13 15/16 × 10 15/16 in.),"Gilman Collection, Purchase, Sam Salz Foundation Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.13,false,true,299482,Photographs,Medal; Button,[Presidential Campaign Medal with Portraits of Abraham Lincoln and Andrew Johnson],,,,,,Maker|Photography Studio|Artist,After,Unknown|Brady & Co.|Thomas Le Mere,"American, active 1840s–1880s|American, active 1860s",,"Unknown|Brady & Co.|Le Mere, Thomas",American|American,1840 |1860,1889 |1869,1864,1864,1864,Tintype,"Image: 1.6 cm (5/8 in.), diameter Overall: 2.5 cm (1 in.), diameter","Purchase, The Overbrook Foundation Gift, 2012",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/299482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1116,false,true,285876,Photographs,Photograph,Abraham Lincoln,,,,,,Artist|Photography Studio,,Anthony Berger|Brady & Co.,"American, active 1860s|American, active 1840s–1880s",,"Berger, Anthony|Brady & Co.",American|American,1860 |1840,1869 |1889,"February 9, 1864",1864,1864,Albumen silver print from glass negative,Image: 41 × 24.2 cm (16 1/8 × 9 1/2 in.) Mount: 50.8 × 34.4 cm (20 × 13 9/16 in.),"Gilman Collection, Purchase, The Horace W. Goldsmith Foundation Gift, through Joyce and Robert Menschel, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/285876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.1182.2017–.2034,false,true,288317,Photographs,Stereographs,[Group of 18 Stereograph Views of the 1884/1885 New Orleans Centennial International Exhibition],,,,,,Publisher|Artist|Artist,,Centennial Photographic Company|Unknown|Edward Livingston Wilson,"American, founded 1876|American|American, 1838–1903",,"Centennial Photographic Company|Unknown|Wilson, Edward Livingston",American|American,1876 |1838,1903,1850s–1910s,1850,1919,Albumen silver prints,Mounts: 10.8 x 17.8 cm (4 1/4 x 7 in.),"Gift of Weston J. Naef, in memory of Kathleen W. Naef and Weston J. Naef Sr., 1982",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/288317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.182,false,true,267971,Photographs,Photograph,Yorktown Landing,,,,,,Artist|Former Attribution,Formerly attributed to,James F. Gibson|Mathew B. Brady,"American, born 1828|American, born Ireland, 1823?–1896 New York",,"Gibson, James F.|Brady, Mathew B.",American|American,1828 |1823,1928 |1896,1862,1862,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.50,false,true,268242,Photographs,Photograph,Water Battery at Yorktown,,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|James F. Gibson,"American, born Ireland, 1823?–1896 New York|American, born 1828",,"Brady, Mathew B.|Gibson, James F.",American|American,1823 |1828,1896 |1928,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.252,false,true,268048,Photographs,Photograph,"Water Battery, Yorktown",,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|James F. Gibson,"American, born Ireland, 1823?–1896 New York|American, born 1828",,"Brady, Mathew B.|Gibson, James F.",American|American,1823 |1828,1896 |1928,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.418,false,true,268232,Photographs,Photograph,"Confederate Fortifications, Yorktown, Virginia",,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|James F. Gibson,"American, born Ireland, 1823?–1896 New York|American, born 1828",,"Brady, Mathew B.|Gibson, James F.",American|American,1823 |1828,1896 |1928,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.52,false,true,268244,Photographs,Photograph,Charleston,,,,,,Artist|Former Attribution,Mathew B. Brady,George N. Barnard|Mathew B. Brady,"American, 1819–1902|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Brady, Mathew B.",American|American,1819 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.40,false,true,268212,Photographs,Photograph,"Laborers at Quartermaster's Wharf, Alexandria, Virginia",,,,,,Former Attribution|Artist,Formerly attributed to|Attributed to,Mathew B. Brady|Andrew Joseph Russell,"American, born Ireland, 1823?–1896 New York|American, 1830–1902",,"Brady, Mathew B.|Russell, Andrew Joseph",American|American,1823 |1830,1896 |1902,1863–65,1863,1865,Albumen silver print from glass negative,Image: 13.2 × 20.2 cm (5 3/16 × 7 15/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.190,false,true,267980,Photographs,Photograph,Cannon,,,,,,Former Attribution|Artist,Formerly attributed to,Mathew B. Brady|Andrew Joseph Russell,"American, born Ireland, 1823?–1896 New York|American, 1830–1902",,"Brady, Mathew B.|Russell, Andrew Joseph",American|American,1823 |1830,1896 |1902,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.659.560,false,true,291992,Photographs,Carte-de-visite,[Jervis McEntee],,,,,,Person in Photograph|Artist,,Jervis McEntee|Austin Augustus Turner,"American, Rondout, New York 1828–1891 Rondout, New York|American, ca. 1813–1866",,"McEntee, Jervis|Turner, Austin Augustus",American|American,1828 |1810,1891 |1866,1860s,1860,1869,Albumen silver print from glass negative,Image: 9.4 x 5.1 cm (3 11/16 x 2 in.) Mount: 10.2 x 6.3 cm (4 x 2 1/2 in.),"The Albert Ten Eyck Gardner Collection, Gift of the Centennial Committee, 1970",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.281,false,true,268080,Photographs,Photograph,"Confederate Earthworks, Centreville, Virginia",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,George N. Barnard|James F. Gibson|Mathew B. Brady,"American, 1819–1902|American, born 1828|American, born Ireland, 1823?–1896 New York",,"Barnard, George N.|Gibson, James F.|Brady, Mathew B.",American|American,1819 |1828 |1823,1902 |1928 |1896,1862,1862,1862,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.68,false,true,268261,Photographs,Photograph,[A Union Station on the James River Established for Extracting Gunpowder from Confederate Torpedoes],,,,,,Former Attribution|Artist|Former Attribution,Attributed to|Formerly attributed to,Andrew Joseph Russell|Egbert Guy Fowx|Mathew B. Brady,"American, 1830–1902|American, born 1821|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Fowx, Egbert Guy|Brady, Mathew B.",American|American,1830 |1821 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,12.9 x 19.6 cm (5 1/16 x 7 11/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.69,false,true,268262,Photographs,Photograph,[A Union Station on the James River Established for Extracting Gunpowder from Confederate Torpedoes],,,,,,Former Attribution|Artist|Former Attribution,Attributed to|Formerly attributed to,Andrew Joseph Russell|Egbert Guy Fowx|Mathew B. Brady,"American, 1830–1902|American, born 1821|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Fowx, Egbert Guy|Brady, Mathew B.",American|American,1830 |1821 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,Image: 13.3 x 20.6 cm (5 1/4 x 8 1/8 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.172,false,true,267960,Photographs,Photograph,Pontoon Bridge,,,,,,Former Attribution|Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Egbert Guy Fowx|Mathew B. Brady,"American, 1830–1902|American, born 1821|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Fowx, Egbert Guy|Brady, Mathew B.",American|American,1830 |1821 |1823,1902 |1896,1861–65,1861,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.253,false,true,268049,Photographs,Photograph,Dutch Gap Canal,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Egbert Guy Fowx|Mathew B. Brady,"American, 1830–1902|American, born 1821|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Fowx, Egbert Guy|Brady, Mathew B.",American|American,1830 |1821 |1823,1902 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.254,false,true,268050,Photographs,Photograph,Dutch Gap Canal,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Andrew Joseph Russell|Egbert Guy Fowx|Mathew B. Brady,"American, 1830–1902|American, born 1821|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Fowx, Egbert Guy|Brady, Mathew B.",American|American,1830 |1821 |1823,1902 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.347,false,true,268153,Photographs,Photograph,"Butler's Lookout Tower, Opposite Dutch Gap",,,,,,Artist|Artist|Former Attribution,Mathew B. Brady,Andrew Joseph Russell|Egbert Guy Fowx|Mathew B. Brady,"American, 1830–1902|American, born 1821|American, born Ireland, 1823?–1896 New York",,"Russell, Andrew Joseph|Fowx, Egbert Guy|Brady, Mathew B.",American|American,1830 |1821 |1823,1902 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.32,false,true,268123,Photographs,Photograph,Dutch Gap Canal,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Egbert Guy Fowx|Andrew Joseph Russell|Mathew B. Brady,"American, born 1821|American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Fowx, Egbert Guy|Russell, Andrew Joseph|Brady, Mathew B.",American|American,1821 |1830 |1823,1902 |1896,1865,1865,1865,Albumen silver print from glass negative,16.4 x 22.5 cm (6 7/16 x 8 7/8 in. ),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.74,false,true,268268,Photographs,Photograph,"Dutch Gap Canal, James River",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Egbert Guy Fowx|Andrew Joseph Russell|Mathew B. Brady,"American, born 1821|American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Fowx, Egbert Guy|Russell, Andrew Joseph|Brady, Mathew B.",American|American,1821 |1830 |1823,1902 |1896,1864,1864,1864,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.234,false,true,268028,Photographs,Photograph,Dutch Gap Canal,,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Egbert Guy Fowx|Andrew Joseph Russell|Mathew B. Brady,"American, born 1821|American, 1830–1902|American, born Ireland, 1823?–1896 New York",,"Fowx, Egbert Guy|Russell, Andrew Joseph|Brady, Mathew B.",American|American,1821 |1830 |1823,1902 |1896,1865,1865,1865,Albumen silver print from glass negative,,"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.65.174,false,true,267962,Photographs,Photograph,"Procession of Troops and Civilians on Way to Dedication of Soldiers' National Cemetery, Gettysburg, Pennsylvania",,,,,,Artist|Artist|Former Attribution,Formerly attributed to,Isaac G. Tyson|Charles J. Tyson|Mathew B. Brady,"American, 1833–1913|American, 1838–1906|American, born Ireland, 1823?–1896 New York",and,"Tyson, Isaac G.|Tyson, Charles J.|Brady, Mathew B.",American|American,1833 |1838 |1823,1913 |1906 |1896,"November 19, 1863",1863,1863,Albumen silver print from glass negative,Image: 17.6 × 20.8 cm (6 15/16 × 8 3/16 in.),"Harris Brisbane Dick Fund, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.288,false,true,283251,Photographs,Photograph,[Portrait of F. Holland Day with Male Nude],,,,,,Person in Photograph|Artist,,F. Holland Day|Clarence H. White,"American, Norwood, Massachusetts 1864–1933 Norwood, Massachusetts|American, 1871–1925",,"Day, F. Holland|White, Clarence H.",American|American,1864 |1871,1933 |1925,1902,1902,1902,Platinum print,Image: 24.2 x 18.8 cm (9 1/2 x 7 3/8 in.),"Gilman Collection, Purchase, Harriette and Noel Levine Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.14.8,false,true,268361,Photographs,Photograph,[Students from the Emerson School for Girls],,,,,,Artist|Photography Studio,,Albert Sands Southworth|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, active 1843–1863",,"Southworth, Albert Sands|Southworth and Hawes",American|American,1811 |1843,1894 |1863,ca. 1850,1848,1852,Daguerreotype,21.6 x 16.5 cm (8 1/2 x 6 1/2 in.),"Gift of I. N. Phelps Stokes, Edward S. Hawes, Alice Mary Hawes, and Marion Augusta Hawes, 1937",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/268361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.79,false,true,283178,Photographs,Photograph,[Albert Sands Southworth],,,,,,Artist|Photography Studio,,Albert Sands Southworth|Southworth and Hawes,"American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, active 1843–1863",,"Southworth, Albert Sands|Southworth and Hawes",American|American,1811 |1843,1894 |1863,ca. 1845–50,1845,1850,Daguerreotype,"Overall: 28.3 × 23.4 cm (11 1/8 × 9 3/16 in.) Image: 11.8 × 8.5 cm (4 5/8 in., 8.5 cm); visible","Gilman Collection, Gift of The Howard Gilman Foundation, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.89,false,true,283191,Photographs,Photograph,Abraham Lincoln,,,,,,Artist|Person in Photograph,,William Marsh|Abraham Lincoln,"American, active Springfield, Illinois, 1850s–1860s|American, Hardin County, Kentucky 1809–1865 Washington, D.C.",,"Marsh, William|Lincoln, Abraham",American|American,1850 |1809,1869 |1865,"May 20, 1860",1860,1860,Salted paper print from glass negative,Image: 19.9 x 14.5 cm (7 13/16 x 5 11/16 in.),"Gilman Collection, Purchase, Joyce F. Menschel Gift, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/283191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.1,false,true,302664,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist,,Osborn's Gallery|J. M. Osborn,"American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Carolina, 1850s–1860s",,"Osborn's Gallery|Osborn, J. M.",American|American,1850 |1850,1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.2,false,true,302665,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist,,Osborn's Gallery|J. M. Osborn,"American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Carolina, 1850s–1860s",,"Osborn's Gallery|Osborn, J. M.",American|American,1850 |1850,1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/8 in. (5 × 7.9 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.15,false,true,302678,Photographs,Carte-de-visite,"Salient with North-west Casemates, Fort Sumter",,,,,,Publisher|Artist,,Osborn's Gallery|J. M. Osborn,"American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Carolina, 1850s–1860s",,"Osborn's Gallery|Osborn, J. M.",American|American,1850 |1850,1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 6 × 7.3 cm (2 3/8 × 2 7/8 in.) Mount: 8.5 × 12 cm (3 3/8 × 4 3/4 in.),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.100.1174.16,false,true,302679,Photographs,Carte-de-visite,"The Evacuation of Fort Sumter, April 1861",,,,,,Publisher|Artist,,Osborn's Gallery|J. M. Osborn,"American, active Charleston, South Carolina, 1850s–1860s|American, active Charleston, South Carolina, 1850s–1860s",,"Osborn's Gallery|Osborn, J. M.",American|American,1850 |1850,1869 |1869,April 1861,1861,1861,Albumen silver print from glass negative,Image: 1 15/16 × 3 1/4 in. (5 × 8.2 cm) Mount: 3 3/8 in. × 4 3/4 in. (8.5 × 12 cm),"Gilman Collection, Museum Purchase, 2005",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/302679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.12,false,true,291765,Photographs,Daguerreotype,[Young Woman Wearing Lace Collar and Brooch],,,,,,Photography Studio|Artist|Artist,Attributed to|Attributed to|Attributed to,Southworth and Hawes|Albert Sands Southworth|Josiah Johnson Hawes,"American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire",,"Southworth and Hawes|Southworth, Albert Sands|Hawes, Josiah Johnson",American|American,1843 |1811 |1808,1863 |1894 |1901,1850s,1850,1859,Daguerreotype,Image: 8.9 x 6.6 cm (3 1/2 x 2 5/8 in.) Plate: 10.6 x 8.1 cm (4 3/16 x 3 3/16 in.) Case: 1.9 x 11.7 x 9.4 cm (3/4 x 4 5/8 x 3 11/16 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.400.15,false,true,291768,Photographs,Daguerreotype,[Young Man in Three-piece Suit and Bow Tie],,,,,,Photography Studio|Artist|Artist,Attributed to|Attributed to,Southworth and Hawes|Albert Sands Southworth|Josiah Johnson Hawes,"American, active 1843–1863|American, West Fairlee, Vermont 1811–1894 Charlestown, Massachusetts|American, Wayland, Massachusetts 1808–1901 Crawford Notch, New Hampshire",,"Southworth and Hawes|Southworth, Albert Sands|Hawes, Josiah Johnson",American|American,1843 |1811 |1808,1863 |1894 |1901,1850s,1850,1859,Daguerreotype,Image: 9.9 x 7.5 cm (3 7/8 x 2 15/16 in.) Plate: 10.8 x 8.3 cm (4 1/4 x 3 1/4 in.) Case: 1.9 x 11.7 x 9.5 cm (3/4 x 4 5/8 x 3 3/4 in.),"Bequest of Herbert Mitchell, 2008",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/291768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.1189.1–.101,false,true,264895,Photographs,Album,Ambroise Bernard Album,,,,,,Artist|Artist,Attributed to,Carleton E. Watkins|Carleton E. Watkins,"American, 1829–1916|American, 1829–1916",,"Watkins, Carleton E.|Watkins, Carleton E.",American|American,1829 |1829,1916 |1916,1870s,1870,1879,Albumen silver prints,43.5 x 37.5 cm (17 1/8 x 14 3/4 in.),"Gift of Carole and Irwin Lainoff, Ruth P. Lasser and Joseph R. Lasser, Mr. and Mrs. John T. Marvin, Martin E. and Joan Messinger, Richard L. Yett and Sheri and Paul Siegel, 1986",,,,,,,,,,,,Albums,,http://www.metmuseum.org/art/collection/search/264895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.1056.3,false,true,262762,Photographs,Stereograph,"[Broadway, New York City, in the rain]",,,,,,Artist|Artist,,Henry T. Anthony|Edward Anthony,"American, 1814–1884|American, 1818–1888",,"Anthony, Henry T.|Anthony, Edward",American|American,1814 |1818,1884 |1888,ca. 1860s,1858,1862,Albumen silver print,,"Warner Communications Inc. Purchase Fund, 1980",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/262762,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.19,false,true,263167,Photographs,Photograph,The Moon,,,,,,Artist|Artist,,John Adams Whipple|James Wallace Black,"American, 1822–1891|American, 1825–1896",,"Whipple, John Adams|Black, James Wallace",American|American,1822 |1825,1891 |1896,1857–60,1857,1860,Salted paper print from glass negative,21 x 15.7 cm (8 1/4 x 6 3/16 in. ),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.1229.55,false,true,263204,Photographs,Photograph,The Moon,,,,,,Artist|Artist,,John Adams Whipple|James Wallace Black,"American, 1822–1891|American, 1825–1896",,"Whipple, John Adams|Black, James Wallace",American|American,1822 |1825,1891 |1896,1857–60,1857,1860,Salted paper print from glass negative,21 x 16.2 cm (8 1/4 x 6 3/8 in. ),"Robert O. Dougan Collection, Gift of Warner Communications Inc., 1981",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/263204,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.43.391,false,true,267805,Photographs,Photograph,Torso,,,,,,Artist|Artist,,Clarence H. White|Alfred Stieglitz,"American, 1871–1925|American, Hoboken, New Jersey 1864–1946 New York",,"White, Clarence H.|Stieglitz, Alfred",American|American,1871 |1864,1925 |1946,"1907, printed 1907–9",1907,1907,Platinum print,24.0 x 18.9 cm. (9 7/16 x 7 7/16 in.),"Alfred Stieglitz Collection, 1933",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/267805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.386,false,true,464366,Medieval Art,Statuette,Mourner,French,,,,,Artist|Artist,,Étienne Bobillet|Paul Mosselman,"Franco-Netherlandish, active Bourges, 1453|Franco-Netherlandish, active Bourges, 1453",,Bobillet Étienne|Mosselman Paul,Franco-Netherlandish|Franco-Netherlandish,1453 |1453,1453 |1453,ca. 1453,1453,1453,Alabaster,Overall: 15 3/16 x 5 5/16 x 3 7/8 in. (38.6 x 13.5 x 9.8 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Sculpture-Stone,,http://www.metmuseum.org/art/collection/search/464366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.18.1,false,true,466663,Medieval Art,sculpture,Griffin and Other Monster,Spanish,,,,,Artist,,Gil de Siloe,"Spanish, active 1475–1505",,"Siloe, Gil de",Spanish,1475,1505,15th century,1400,1499,Alabaster,Overall: 18 5/16 x 9 7/8 x 9 1/4 in. (46.5 x 25.1 x 23.5 cm),"Rogers Fund, 1927",,,,,,,,,,,,Sculpture,,http://www.metmuseum.org/art/collection/search/466663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.4.227,false,true,479678,Medieval Art,Facsimile,"Facsimile of the Apse Painting in Tomb 25, Bagawat Necropolis, Kharga Oasis",Egyptian,,,,,Artist,,Charles K. Wilkinson,,,"Wilkinson, Charles K.",,1897,1986,A.D. 2nd century or later,100,199,Tempera on paper,Overall: 26 3/4 x 18 7/8 in. (67.9 x 47.9 cm) Framed: 27 7/8 x 20 7/16 x 7/8 in. (70.8 x 51.9 x 2.2 cm),"Rogers Fund, 1930",,,,,,,,,,,,Reproductions-Paintings,,http://www.metmuseum.org/art/collection/search/479678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.4.229,false,true,479680,Medieval Art,Facsimile,"Facsimile of Painting in the Chapel of Peace, Bagawat Necropolis, Kharga Oasis",Egyptian,,,,,Artist,,Charles K. Wilkinson,,,"Wilkinson, Charles K.",,1897,1986,A.D. 2nd–6th century,100,599,Tempera on paper,Overall: 22 1/16 x 13 3/4 in. (56 x 34.9 cm) Framed: 23 1/4 x 15 1/8 x 7/8 in. (59.1 x 38.4 x 2.2 cm),"Rogers Fund, 1930",,,,,,,,,,,,Reproductions-Paintings,,http://www.metmuseum.org/art/collection/search/479680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.154.1,false,true,468257,Medieval Art,Relief,Holy Family,South German,,,,,Artist,,Niclaus Weckmann,1481–1528,,WECKMANN NICLAUS,,1481,1528,ca. 1500,1500,1500,Limewood with traces of paint and gilding,Overall: 31 7/8 x 19 11/16 x 7 3/4 in. (81 x 50 x 19.7 cm),"Gift of Alastair Bradley Martin, 1948",,,,,,,,,,,,Sculpture-Wood,,http://www.metmuseum.org/art/collection/search/468257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"06.163a, b",false,true,462887,Medieval Art,Statuette,Female Saint,South Netherlandish,,,,,Artist,,Jan van Steffesweert,"Maastricht, ca. 1460–1531",,"van Steffesweert, Jan",,1460,1531,ca. 1520,1520,1520,Oak with traces of polychromy,Overall: 32 1/4 x 11 1/16 x 9 7/8 in. (81.9 x 28.1 x 25.1 cm),"Rogers Fund, 1906",,,,,,,,,,,,Sculpture-Wood,,http://www.metmuseum.org/art/collection/search/462887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"51.28a, b",false,true,468372,Medieval Art,Sculpture,"The Three Magi, from an Adoration Group",South German,,,,,Artist,Workshop of,Hans Thoman,"German, active Memmingen, ca. 1514–25",,"Thoman, Hans",,1514,1525,ca. 1515–20,1515,1520,"Wood, gesso, paint, gilding",Overall (Balthasar and Melchior): 23 1/2 x 12 1/4 x 6 1/2 in. (59.7 x 31.1 x 16.5 cm) Overall (Gaspar): 22 1/2 x 9 1/4 x 5 1/4 in. (57.2 x 23.5 x 13.3 cm) Base: 4 x 20 x 10 in. (10.2 x 50.8 x 25.4 cm),"Purchase, Joseph Pulitzer Bequest, 1951",,,,,,,,,,,,Sculpture-Wood,,http://www.metmuseum.org/art/collection/search/468372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +31.134.3,false,true,467415,Medieval Art,Choir Book; Manuscript cutting,"Manuscript Illumination with the Visitation in an Initial D, from a Choir Book",French,,,,,Artist,,Spanish Forger,"French, active late 19th–early 20th century",,Spanish Forger,French,1870,1935,late 19th–early 20th century,1875,1925,"Tempera, ink, and gold on parchment",Overall: 6 7/8 x 4 1/2 in. (17.5 x 11.5 cm) Mat size: 19 3/16 x 14 3/16 in. (48.8 x 36.1 cm),"Bequest of Gwynne M. Andrews, 1930",,,,,,,,,,,,Manuscripts and Illuminations,,http://www.metmuseum.org/art/collection/search/467415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.32.213,false,true,463816,Medieval Art,Relief,Meeting of Saints Joachim and Anne at the Golden Gate,North German,,,,,Artist,,Benedikt Dreyer,"German, active Lübeck, ca. 1500–1525",,Dreyer Benedikt,German,1510,1530,ca. 1515–20,1515,1520,Oak with polychromy and gilding,Overall: 23 x 19 1/4 x 4 7/8 in. (58.4 x 48.9 x 12.4 cm),"Gift of J. Pierpont Morgan, 1916",,,,,,,,,,,,Sculpture-Wood,,http://www.metmuseum.org/art/collection/search/463816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"10.197a, b",false,true,463412,Medieval Art,Beaker and cover,Beaker and Cover,German,,,,,Artist,Probably,Friedrich Hillebrand,"German, 1580–1608",,"Hillebrand, Friedrich",German,1580,1608,19th century (16th century style),1800,1900,"Silver, partially gilt",Overall: 14 15/16 x 5 1/8 in. (37.9 x 13 cm),"Rogers Fund, 1910",,,,,,,,,,,,Metalwork-Silver,,http://www.metmuseum.org/art/collection/search/463412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.65.1,false,true,463042,Medieval Art,Bell,The Bell of Saint Patrick Shrine,Irish,,,,,Maker,,Elkington & Co.,"British, Birmingham, 1829–1963",,Elkington & Co.,British,1829,1963,early 20th century (original dated 1091–1105),1091,1105,"Bronze, gold, silver, gems",Overall: 10 1/2 x 6 3/16 x 4 1/2 in. (26.6 x 15.7 x 11.4 cm),"Rogers Fund, 1906",,,,,,,,,,,,Reproductions-Metalwork,,http://www.metmuseum.org/art/collection/search/463042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.233.4,false,true,463161,Medieval Art,Shrine,Shrine of Saint Patrick's Tooth,Irish,,,,,Maker,,Elkington & Co.,"British, Birmingham, 1829–1963",,Elkington & Co.,British,1829,1963,early 20th century (original dated 1376),1376,1376,"Bronze, gilt, gem stones",12 x 9 1/4 x 2 1/2 in. (30.5 x 23.5 x 6.4 cm),"Rogers Fund, 1908",,,,,,,,,,,,Reproductions-Metalwork,,http://www.metmuseum.org/art/collection/search/463161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"11.125.15a, b",false,true,463505,Medieval Art,Salt cellar,Salt Cellar,British,,,,,Maker,,Elkington & Co.,"British, Birmingham, 1829–1963",,Elkington & Co.,British,1829,1963,early 20th century (original dated late 15th century),1450,1500,"Silver gilt, glass",Overall: 15 1/16 x 5 1/4 in. (38.2 x 13.4 cm) Lid: 5 9/16 x 4 1/4 in. (14.2 x 10.8 cm) Cellar: 9 1/2 x 5 1/4 in. (24.2 x 13.4 cm),"Dodge Fund, 1911",,,,,,,,,,,,Reproductions-Metalwork,,http://www.metmuseum.org/art/collection/search/463505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.456,false,true,474389,Medieval Art,Statue,Virgin and Child,British,,,,,Artist,Attributed to,Alexander of Abingdon,"British, active 1291–1317",,Alexander,British,1250,1350,ca. 1275–1325,1275,1325,Caen Limestone,Overall: 59 1/4 x 19 3/8 x 11 3/4 in. (150.5 x 49.2 x 29.8 cm),"Purchase, Edward J. Gallagher Jr. Bequest, in memory of his father, Edward Joseph Gallagher, his mother, Ann Hay Gallagher, and his son, Edward Joseph Gallagher III; and Caroline Howard Hyman Gift, 2003",,,,,,,,,,,,Sculpture-Stone,,http://www.metmuseum.org/art/collection/search/474389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.489,false,true,464453,Medieval Art,Altarpiece,Altarpiece,North Italian,,,,,Artist,,Baldassare degli Embriachi,"Italian, active 1390–1409",,"Embriachi, Baldassare degli",Italian,1390,1409,ca. 1390–1400,1390,1400,"Bone framed with intarsia and horn, traces of paint and gilding",without wooden base: 50 1/2 x 60 1/2 in. (128.3 x 153.7 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Ivories-Bone,,http://www.metmuseum.org/art/collection/search/464453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.248.1,false,true,466677,Medieval Art,Book of Hours; Manuscript leaf,"Manuscript Leaf with Adoration of the Holy Name, from a Book of Hours",South Netherlandish,,,,,Artist,Influence of,Simon Bening,"Netherlandish, Ghent (?) 1483/84–1561 Bruges",,"Bening, Simon",Netherlandish,1483,1561,after 1530 (?),1530,1530,"Tempera, ink and shell gold on parchment",5 7/8 x 4 3/16 in. (15 x 10.6 cm) Mat: 12 × 10 in. (30.5 × 25.4 cm),"Gift of Alice M. Dike, 1927",,,,,,,,,,,,Manuscripts and Illuminations,,http://www.metmuseum.org/art/collection/search/466677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.120.2,false,true,471730,The Cloisters,Painting,Saint Michael,North Spanish,,,,,Artist,,Master of Belmonte,"Spanish, Aragon, active ca. 1460–90",,Master of Belmonte,Spanish,1455,1490,1450–1500,1450,1500,Tempera and oil on wood,Overall: 85 1/2 x 47 in. (217.2 x 119.4 cm),"The Cloisters Collection, 1955",,,,,,,,,,,,Paintings-Panels,,http://www.metmuseum.org/art/collection/search/471730,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.135,false,true,472854,The Cloisters,Panel,Stained-Glass Panel with a Coat of Arms and a Female Supporter,Swiss,,,,,Artist,Workshop of,Lukas Zeiner,"Swiss, active ca. 1480–1510",,Zeiner Lukas,Swiss,1480,1510,1500–1505,1500,1505,"Pot metal and colorless glass, vitreous paint, and silver stain, lead",14 7/8 x 19 7/8 in. (37.8 x 50.5 cm),"Purchase, Bequest of Jane Hayward, by exchange, 2000",,,,,,,,,,,,Glass-Stained,,http://www.metmuseum.org/art/collection/search/472854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.304.3,false,true,469916,The Cloisters,Roundel,Roundel with Saint Jerome in his Study,South Netherlandish,,,,,Artist,Based on a design by,Pseudo-Ortkens,"South Netherlandish, active Antwerp and Brussels, ca. 1500–30",,Pseudo-Ortkens,South Netherlandish,1500,1530,ca. 1520,1520,1520,"Colorless glass, vitreous paint and silver stain",Overall: 9 in. (22.8 cm),"The Cloisters Collection, 1988",,,,,,,,,,,,Glass-Stained,,http://www.metmuseum.org/art/collection/search/469916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.120.368,false,true,472330,The Cloisters,Statuette,Virgin,French,,,,,Artist,,Joan Avesta,"Spanish, active Catalonia and southwest France, 1355–1390",,"Avesta, Joan",Spanish,1355,1390,ca. 1370–90,1367,1393,"Alabaster, traces of gilt, paint",Overall: 25 9/16 x 9 7/8 x 8 5/8 in. (64.9 x 25.1 x 21.9 cm),"The Cloisters Collection, 1925",,,,,,,,,,,,Sculpture-Stone,,http://www.metmuseum.org/art/collection/search/472330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.22.1,false,true,478972,The Cloisters,Panel,Gathering Manna,German,,,,,Artist|Artist,Workshop of|Based on a design by,Friedrich Brunner|Jan Pollack,"German|Polish (?), active Bavaria, ca. 1479–died 1519",,"Brunner Friedrich|Pollack, Jan",German|Polish (?),1479,1519,1497–99,1497,1499,"Pot-metal glass, vitreous paint, and silver stain",Overall: 19 3/4 x 20 7/8 in. (50.2 x 53 cm),"Purchase, The Cloisters Collection and Gift of The Hearst Foundation, by exchange, 2010",,,,,,,,,,,,Glass-Stained,,http://www.metmuseum.org/art/collection/search/478972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.22.2,false,true,478997,The Cloisters,Panel,Storing up Manna,German,,,,,Artist|Artist,Workshop of|Based on a design by,Friedrich Brunner|Jan Pollack,"German|Polish (?), active Bavaria, ca. 1479–died 1519",,"Brunner Friedrich|Pollack, Jan",German|Polish (?),1479,1519,1497–99,1497,1499,"Pot-metal glass, vitreous paint, and silver stain",Overall: 19 3/4 x 20 7/8 in. (50.2 x 53 cm),"Purchase, The Cloisters Collection and Gift of The Hearst Foundation, by exchange, 2010",,,,,,,,,,,,Glass-Stained,,http://www.metmuseum.org/art/collection/search/478997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.1,false,true,468466,The Cloisters,Shrine,Reliquary Shrine of Saint Barbara,European,,,,,Artist,Workshop of,Louis Marcy (Luigi Parmeggiani),"Italian, 1860–1945",(?),"Marcy, Louis",,1860,1945,ca. 1880–1900 (14th–15th century style),1880,1900,"Silver, Silver-gilt",Overall: 14 13/16 x 11 7/16 x 5 13/16 in. (37.6 x 29 x 14.8 cm),"The Cloisters Collection, 1955",,,,,,,,,,,,Metalwork-Silver,,http://www.metmuseum.org/art/collection/search/468466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.278,true,true,478211,The Cloisters,Dish,Dish with Abraham and Melchizedek,South German,,,,,Artist,,Hans of Landshut,"German, Landshut, active late 15th century",,Hans of Landshut,German,1400,1550,1498,1498,1498,Free-blown glass with paint and metallic foils,Overall: 14 1/2 x 1 5/8 in. (36.9 x 4.2 cm),"The Cloisters Collection, 2008",,,,,,,,,,,,Glass-Miscellany,,http://www.metmuseum.org/art/collection/search/478211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.146,false,true,469897,The Cloisters,Roundel,Roundel with Christ Taking Leave of His Mother,South German,,,,,Artist,After,Hans Schäufelein,"German, Nuremberg ca. 1480–ca. 1540 Nördlingen",,"Schäufelein, Hans",German,1480,1540,1507–15,1507,1515,"Colorless glass, vitreous paint and silver stain",Overall: 6 1/2 in. (16.5 cm),"Gift of Louis R. Slattery, in honor of Ashton Hawkins, 1985",,,,,,,,,,,,Glass-Stained,,http://www.metmuseum.org/art/collection/search/469897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.185,false,true,469840,The Cloisters,Roundel,Roundel with Netting Quail,German,,,,,Artist,After a design by,Augustin Hirschvogel,"German, Nuremberg 1503–1553 Vienna",,"Hirschvolgel, Augustin",German,1503,1553,16th century,1500,1600,"Colorless glass, vitreous paint, silver stain and cold enamel",Overall: 9 1/2 in. (24.1 cm),"The Cloisters Collection, 1979",,,,,,,,,,,,Glass-Stained,,http://www.metmuseum.org/art/collection/search/469840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.266,false,true,471908,The Cloisters,Reliquary bust,Reliquary Bust of Saint Juliana,Italian,,,,,Artist,Circle of,Giovanni di Bartolo,"Italian, active 1364–1404",,"di Bartolo, Giovanni",Italian,1364,1404,ca. 1376,1371,1381,"Copper, gilding, gesso, and tempera paint",Overall: 11 1/16 x 9 x 8 3/8 in. (28.1 x 22.9 x 21.3 cm),"The Cloisters Collection, 1961",,,,,,,,,,,,Metalwork-Copper,,http://www.metmuseum.org/art/collection/search/471908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.24.40,false,true,471103,The Cloisters,Roundel,Roundel with the Hanging of Haman,North Netherlandish (?),,,,,Artist,Style of,Jan Swart van Groningen,"Netherlandish, Groningen ca. 1490/1500–1553 or later Antwerp",(?),"van Groningen, Jan Swart",Netherlandish,1490,1553,ca. 1530–40,1530,1540,"Colorless glass, silver stain, vitreous paint",Overall Diam.: 9 in. (22.9 cm),"The Cloisters Collection, 1932",,,,,,,,,,,,Glass-Stained,,http://www.metmuseum.org/art/collection/search/471103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +GB1321 .M68 1766,true,true,682011,The Libraries,,Dialogo sobre hua nova obra no Rio Tejo ...,,,,,,Author,,Bento de Moura Portugal,"Portuguese, 1702–1776",,"Moura Portugal, Bento de",Portuguese,1702,1776,1776 (?),1771,1781,,"235 pages, [7] leaves of plates (some folded); Overall: 8 1/16 × 6 1/16 × 1 1/2 in. (20.4 × 15.4 × 3.8 cm)","Gift of Jayne Wrightsman, 2008",,,,,Portugal,,,,,,,||,,http://www.metmuseum.org/art/collection/search/682011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PN6231.P6 B63 1669,true,true,681546,The Libraries,,De' ragguagli di Parnaso: Centuria prima-seconda,,,,,,Printer|Author|Binder|Author,,Johannes Blaeu|Girolamo Briani|Simier|Traiano Boccalini,"Dutch|Italian, 1581–1646|Italian, 1556–1613",,"Blaeu, Johannes|Briani, Girolamo|Simier|Boccalini, Traiano",Dutch|Italian|Italian,1581 |0 |1556,1646 |0 |1613,1669,1669,1669,,"2 volumes: [16], 471, [55] pages; 415, [41], 139, [25] pages; Height: 6 1/8 in. (15.5 cm)","Gift of Jayne Wrightsman, 2008",,Amsterdam,,,Netherlands,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PQ4272.F5 A34 1697,true,true,681534,The Libraries,,"Contes et nouvelles de Bocace florentin : traduction libre, accommodée au gout de ce temps, & enrichie de figures en taille-douce gravées par Mr. Romain de Hooge",,,,,,Binder|Printer|Author|Printmaker,,Kleihnans|George Gallet|Giovanni Boccaccio|Romeyn de Hooghe,"Italian, Paris 1313–1375 Certaldo, Tuscany|Dutch, 1645–1708",,"Kleihnans|Gallet, George|Boccaccio, Giovanni|Hooghe, Romeyn de",Netherlandish|Italian|Dutch,0 |1313 |1645,0 |1375 |1708,1697,1697,1697,,"2 volumes: illustrations, etchings; Height: 6 5/16 in. (16 cm)","Gift of Jayne Wrightsman, 2008",,Amsterdam,,,Netherlands,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +BX2010 .A2 1794,true,true,681389,The Libraries,,"Uffizio della Settimana Santa : colle rubriche volgari, argomenti de' Salmi, spiegazione delle cerimonie e misterj, con osservazioni, e riflessioni divote dell'abate Alessandro Mazzinelli",,,,,,Author|Artist|Author|Artist|Artist|Publisher,After|After|After|Presso,Alessandro Mazzinelli|Giovanni Battista Pacetti|Catholic Church|Giuseppe Passeri (Passari)|Annibale Carracci|Luigi Perego Salvioni,"Italian, active ca. 1700|Italian, 1693–1743|Italian, Rome 1654–1714 Rome|Italian, Bologna 1560–1609 Rome",,"Mazzinelli, Alessandro|Pacetti, Giovanni Battista|Catholic Church|Passeri, Giuseppe|Carracci, Annibale|Salvioni",Italian|Italian|Italian|Italian,1650 |1693 |0 |1654 |1560,1750 |1743 |0 |1714 |1609,1794,1794,1794,,3 volumes in 1 (560 pages) : illustrations ; Height: 8 11/16 in. (22 cm),"Gift of Jayne Wrightsman, 2008",,Rome,,,Italy,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +240.7 T344 F,true,true,591832,The Libraries,,"Academie de l'espee; ou se domonstrent par reigles mathématiques sur le fondement d'un cercle mystérieux, la théorie et pratique des vrais . . .","Anvers, 1628",,,,,Publisher|Artist|Author,Printer:|Engraver:,probably the Elseviers of Leyden|probably A. Boslwert|Girard Thibault,"Flemish, died ca. 1629",,Elseviers|Boslwert A.|Thibault Girard,Flemish,1629,1629,1628,1628,1628,Illustrated book,2 pts. in 1vol.; H: 22 in. (56 cm),Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +216.7 T71,true,true,591860,The Libraries,,The Treatyse of Fysshynge wyth an Angle from the book of Saint Albans,"New York, C. Scribner's Sons, 1903",,,,,Author|Author,Introduction by:,William Loring Andrews|Juliana Berners,"American, 1837–1920|British, b. 1388",,"Andrews, William Loring|Berners Juliana",American|British,1837 |1388,1920 |1388,1903,1903,1903,Illustrated book,H: 7 7/8 in. (20 cm),Presented by Mr. and Mrs. Edward Dean Adams,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +239 B63 Q,true,true,591831,The Libraries,,"Descriptio pbvlicae gratvlationis, spectacvlorvm et lvdorvm, in aventv sereniss: Principis Ernesti Archidvcis Avstriae Dvcis Vrgvndiae","Antwerp: Ex Officina Plantíníana, 1595",,,,,Author|Engraver,,Jean Boch|Peeter van der Borcht,"Belgian, 1545–1608|Netherlandish, Mechelen ca. 1545–1608 Antwerp",,"Boch Jean|Borcht, Peeter van der",Belgian|Netherlandish,1545 |1540,1608 |1608,1595,1595,1595,Illustrated book,174 pp.; 15 x 10 1/4 in. (38 x 26 cm),Presented by Mrs. S. P. Avery,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +920.84 M292,true,true,591865,The Libraries,,Morte d'Arthur,"London: J. M. Dent and Co., 1893",,,,,Author|Illustrator,,Sir Thomas Malory|Aubrey Vincent Beardsley,"British, 1415/18–1471|British, Brighton, Sussex 1872–1898 Menton",,"Malory Thomas Sir|Beardsley, Aubrey Vincent",British|British,1415 |1872,1471 |1898,1893,1893,1893,Illustrated book,12 pts.; H: 10 1/4 in. (26 cm),,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +903.6 R541 F,true,true,591848,The Libraries,,"The Holy Land, Syria, Idumea, Arabia, Egypt & Nubia","London : F. G. Moon, 1842–49",,,,,Author|Author|Author,Lithographed by|Historical descriptions by,Louis Haghe|Rev. George Croly|David Roberts,"Belgian, Tournai 1806–1885 Surrey|1780–1860|British, Stockbridge, Scotland 1796–1864 London",,"Haghe, Louis|Croly George Rev.|Roberts, David","Belgian|British, Scottish",1806 |1780 |1796,1885 |1860 |1864,1842–49,1842,1849,Illustrated books,3 vols.; H: 24 3/4 in. (63 cm),Presented by Charles Lanier,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +146.8 M821 Q,true,true,591863,The Libraries,,"Catalogue of the Collection of Jewels and Precious Works of Art, the Property of J. Pierpont Morgan","London: Chiswick Press, 1910 (deluxe ed.)",,,,,Author|Author,,J. Pierpont Morgan|George Charles Williamson,"British, 1858–1942",,"Morgan, J. Pierpont|Williamson, George Charles",American|British,1837 |1858,1913 |1942,1910,1910,1910,"Illustrated book, fine binding",H: 15 3/4 in. (39 cm),Presented by J. Pierpont Morgan,,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/591863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PQ2067.T28 D4 1770,true,true,681573,The Libraries,,"Les oeuvres morales de Mr. Diderot : contenant son traité De l'amitié, et celui Des passions",,,,,,Author|Author|Publisher,Erroneously attributed to|Aux depens de,Marie Geneviève Charlotte Darlus Thiroux d'Arconville|Denis Diderot|La Compagnie des libraires associés,"French, 1720–1805|French, 1713–1784",,"Thiroux d'Arconville, Marie Geneviève Charlotte Darlus|Diderot, Denis|Compagnie des libraires associés",French|French|French,1720 |1713,1805 |1784,1770,1770,1770,,2 volumes bound in 1; Height: 6 11/16 in. (17 cm),"Gift of Jayne Wrightsman, 2008",,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PN1489 .M37 1651,true,true,682013,The Libraries,,L'eschole de Salerne en vers burlesques & duo poemata macaronica: de bello huguenotico: et De gestis magnanimi & prudentissimi Baldi,,,,,,Author|Author|Binder|Author|Author,,Docteur (Louis) Martin|Remy Belleau|Robert Joly|Simon Moynet|Theophilo Folengo,"active 17th century|French, 1527?–1577|French, 1870?–1924|Italian, 1496–1544",,"Martin, Docteur (Louis)|Belleau, Remy|Joly, Robert |Moynet, Simon|Folengo, Theophilo",French|French|Italian,1600 |1527 |1870 |1496,1699 |1577 |1924 |1544,1651,1651,1651,,"139, [1] pages; Height: 5 1/8 in. (13 cm)","Gift of Jayne Wrightsman, 2008",,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/682013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +BS75 1785b Q,true,true,681149,The Libraries,,Bibliorum Sacrorum Vulgatae versionis,,,,,,Printer|Author,,François Ambroise Didot|Josiah W. (Josiah Willard) Gibbs,"French, 1730–1804|American, 1790–1861",,"Didot, François Ambroise|Gibbs, Josiah W. (Josiah Willard)",French|American,1730 |1790,1804 |1861,1785,1785,1785,,"2 v. (ix [i.e. vii], [1], 596; [4], 548 p.) ; Height: 12 3/16 in. (31 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +DA447.G7 H26 1876,true,true,681556,The Libraries,,Mémoires du Comte de Grammont : histoire amoureuse de la cour d'Angleterre sous Charles II / par Antoine Hamilton ; préface et notes par Benjamin Pifteau,,,,,,Author|Illustrator|Printer|Binder,,Count Anthony Hamilton|Jules Adolphe Chauvet|Leon Lamire|Marius Michel et fils,"Irish, ca. 1646–1720|French|French|French",,"Hamilton, Anthony, Count|Chauvet, Jules Adolphe|Lamire, Leon|Marius Michel et fils",Irish|French|French|French,1646,1720,1876,1876,1876,,20 cm,"Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PA4375.M8 D3 1772,true,true,681388,The Libraries,,"Traité de Plutarque, sur la manière de discerner un flatteur d'avec un ami et Le banquet des sept sages: dialogue du même auteur revu & corrigé sur des manuscrits de la Bibliothèque du roi; avec une version françoise & des notes",,,,,,Author|Publisher|Artist,De,Plutarch|L'Imprimerie Royale|François Jean Gabriel de La Porte du Theil,"Greek, ca. A.D. 45–ca. 125|French, 1742–1815",,"Plutarch|L'Imprimerie Royale|La Porte du Theil, François Jean Gabriel de",Greek|French|French,0045 |1742,0125 |1815,1772,1772,1772,,"xii, 335, [1] pages ; Height: 8 1/4 in. (21 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +SB472.32.E54 W48 1771,true,true,680835,The Libraries,,"L'art de former les jardins modernes, ou, L'art des jardins anglois : traduit de l'anglois : à quoi le traducteur a ajouté un discours préliminaire sur l'origine de l'art, des notes sur le texte, & une description détaillée des jardins de Stowe, accompagnée du plan",,,,,,Translator|Author|Publisher,Chez,François de Paule Latapie|Thomas Whately|Charles Antoine Jombert,"French, 1739–1823|British, died 1772|French, 1712–1784",,"Latapie, François de Paule|Whately, Thomas|Jombert, Charles Antoine",French|British|French,1739 |1672 |1712,1823 |1772 |1784,1771,1771,1771,,"lxiv, 406 p., 1 folded leaf of plates, plan, Height: 8 11/16 in. (22 cm)","Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/680835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +BV4823 .D48 1741,true,true,681547,The Libraries,,"De l'imitation de Jesus-Christ, traduction nouvelle ; ornée de figures en taille-douce",,,,,,Designer|Engraver|Engraver,Engravings drawn by,Antoine Humblot|J. B. Guélard|Claude Duflos,"French, died 1758|French, active ca. 1730|French, Coucy-le-Château 1665–1727 Paris",,"Humblot, Antoine|Guélard, J. B.|Duflos, Claude",French|French|French,1658 |1730 |1665,1758 |1730 |1727,1741,1741,1741,,"[8], xx, 608 p., [4] leaves of plates ; ill. (engravings) ; Height: 8 1/4 in. (21 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PQ1809 .A1 1795,true,true,681545,The Libraries,,Contes et nouvelles en vers par Jean de la Fontaine,,,,,,Author|Author|Draftsman|Printer,,Nicolas Boileau Despréaux|Jean de La Fontaine|Charles Dominique Joseph Eisen|Pierre Didot l'ainé,"French, 1636–1711|French, Château-Thierry 1621–1695 Paris|French, Valenciennes 1720–1778 Brussels|French, 1761–1853",,"Boileau Despréaux, Nicolas|La Fontaine, Jean de|Eisen, Charles Dominique Joseph|Didot, Pierre l'ainé",French|French|French|French,1636 |1621 |1720 |1761,1711 |1695 |1778 |1853,1795,1795,1795,,"2 volumes, portraits, Height: 5 1/2 in. (14 cm)","Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +NE2049.5.W38 W38 1710,true,true,682014,The Libraries,,Figures françoises et comiques nouvellement inventées par M. Watteau; Figures de modes dessinées et gravées à l'eau forte par Watteau; a terminées au buin par Thomassin le fils,,,,,,Artist|Engraver|Engraver|Engraver|Binder,,Antoine Watteau|Charles Nicolas Cochin I|Louis Desplaces|Henri Simon Thomassin|Hardy-Mennil,"French, Valenciennes 1684–1721 Nogent-sur-Marne|French, Paris 1688–1754 Paris|French, Paris 1682–1739 Paris|French, Paris 1687–1741",,"Watteau, Antoine|Cochin, Charles Nicolas, I|Desplaces, Louis|Thomassin, Henri Simon|Hardy-Mennil",French|French|French|French,1684 |1688 |1682 |1687,1721 |1754 |1739 |1687,1710?–?1720,1705,1725,,"[12], [8] leaves; Height: 10 1/4 in. (26 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/682014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PQ1795 .T5 1790,true,true,681128,The Libraries,,"Les aventures de Télémaque, fils d'Ulysse par M. de Fénélon ; avec figures en taille-douce, dessinées par MM. Cochin et Moreau le jeune",,,,,,Author|Engraver|Engraver|Binder,,François de Salignac de La Mothe-Fénelon|Charles Nicolas Cochin II|Jean Michel Moreau the Younger|Jean Claude Bozerian,"French, Château de Fénelon, Périgord 1651–1715 Cambrai|French, Paris 1715–1790 Paris|French, Paris 1741–1814 Paris|French, 1762–1840",,"Fénelon, François de Salignac de La Mothe-|Cochin, Charles Nicolas, II|Moreau, Jean Michel, the Younger|Bozerian, Jean Claude",French|French|French|French,1651 |1715 |1741 |1762,1715 |1790 |1814 |1840,1790,1790,1790,,2 volumes: illustrations (engravings); Height: 9 13/16 in. (25 cm),"Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PA6525.H4 N3 1762,true,true,682012,The Libraries,,Epistole eroiche di P. Ovidio Nasone,,,,,,Author|Illustrator|Publisher|Author|Translator,,Ovid|Carlo Gregori|Durand|Giovan Stefano Conti|Remigio Nannini,"Roman, Sulmo 43 B.C.–A.D. 17 Tomis, Moesia|Italian, Lucca 1702–1759 Florence|French, 18th century|Italian, 1720–1791|Italian, 1521?–?1581",,"Ovid|Gregori, Carlo|Durand|Conti, Giovan Stefano|Nannini, Remigio",Roman|Italian|French|Italian|Italian,-0043 |1702 |1700 |1720 |1521,0017 |1759 |1800 |1791 |1581,1762,1762,1762,,"xii, 323 pages: portraits; Height: 8 1/4 in. (21 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/682012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PQ1821 1773,true,true,681563,The Libraries,,"Oeuvres de Moliere : avec des remarques grammaticales, des avertissemens et des observations sur chaque piéce, par M. Bret",,,,,,Author|Printer|Author|Author|Illustrator|Illustrator|Publisher,De l'imprimerie de|Par,M. (Antoine) Bret|Michel Lambert|Jean-Baptiste Poquelin Molière|Voltaire|Pierre Mignard |Jean Michel Moreau the Younger|La Compagnie des libraires associés,"French, Dijon 1717–1792 Paris|French, 1722?–1787|French, 1622–1673|1694–1778|French, Troyes 1612–1695 Paris|French, Paris 1741–1814 Paris",,"Bret, M. (Antoine)|Lambert, Michel|Molière, Jean-Baptiste Poquelin|Voltaire|Mignard, Pierre|Moreau, Jean Michel, the Younger|Compagnie des libraires associés",French|French|French|French|French|French,1717 |1722 |1622 |1694 |1612 |1741,1792 |1787 |1673 |1778 |1695 |1814,1773,1773,1773,,"6 volumes: illustrations, portraits, engravings; Height: 8 1/4 in. (21 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AE25 .E533 1765 Q,true,true,716648,The Libraries,,"Planches pour l'Encyclopédie, ou pour le Dictionaire raisonné des sciences, des arts libéraux, et des arts méchaniques, avec leur explication",,,,,,Publisher|Author,,Vincenzo Giuntini|Denis Diderot,"French, 1713–1784",,"Giuntini, Vincenzo|Diderot, Denis",,1713,1784,1765–1776,1765,1776,,"11 volumes, illustrations, height: 16 9/16 in. (42 cm)",Bequest of Marianne Khuner,,Lucca,,,Italy,,,,,,,,,http://www.metmuseum.org/art/collection/search/716648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +NK7983.A1 W36 1752,true,true,705290,The Libraries,,博古圖錄,,,,,,Author,,Fu Wang,"Chinese, 1079–1126",,"Wang, Fu",,1079,1126,1752,1752,1752,,16 volumes : illustrations ; Height: 11 13/16 in. (30 cm),,,,,,China,,,,,,,,,http://www.metmuseum.org/art/collection/search/705290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +230.51 N55,true,true,715005,The Libraries,,三禮圖,,,,,,Publisher|Author,,Tong zhi tang|Chongyi Nie,"Chinese, active 10th century",,"Tong zhi tang|Nie, Chongyi",,0900,0999,1676,1676,1676,,4 volumes : illustrations ; height: 11 7/16 in. (29 cm),"Purchased with income from the Jacob S. Rogers Fund, 1940",,Beijing,,,China,,,,,,,,,http://www.metmuseum.org/art/collection/search/715005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +NK1483 .J7 1867 Q,true,true,739504,The Libraries,,Examples of Chinese ornament selected from objects in the South Kensington Museum and other collections,,,,,,Publisher|Author,,S. & T. Gilbert|Owen Jones,"London|British, London 1809–1874 London",,"Gilbert, S. & T.|Jones, Owen",,1800 |1809,1900 |1874,1867,1867,1867,,"3 pages, leaf, 5-15 pages : illustrations, color plate ; Height: 13 3/4 in. (35 cm)",,,London,,,England,,,,,,,,,http://www.metmuseum.org/art/collection/search/739504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +GT2050 .U813 1884,true,true,726677,The Libraries,,"The sunshade, the glove, the muff",,,,,,Illustrator|Publisher|Author,,Paul Avril|J. C. Nimmo and Bain|Octave Uzanne,"French, born 1843|London|French, Auxerre 1851–1931",,"Avril, Paul|Nimmo, J. C. and Bain|Uzanne, Octave",,1843 |1875 |1851,1943 |1899 |1931,1884,1884,1884,,"viii, 138 pages : illustrations (some color) ; Height: 11 in. (28 cm)",,,London,,,England,,,,,,,,,http://www.metmuseum.org/art/collection/search/726677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AE25 .E532 1758 Q,true,true,716639,The Libraries,,"Encyclopédie, ou Dictionnaire raisonné des sciences, des arts et des métiers, par une société de gens de lettres",,,,,,Author|Publisher|Author|Author,,Ottaviano Diodati|Vincenzo Giuntini|Jean Le Rond d'Alembert|Denis Diderot,"Italian, 1716–1786|French, 1717–1783|French, 1713–1784",,"Diodati, Ottaviano|Giuntini, Vincenzo|Alembert, Jean Le Rond d'|Diderot, Denis",,1716 |1717 |1713,1786 |1783 |1784,1758–1771,1758,1771,,"17 volumes, illustrations, height: 16 9/16 in. (42 cm)",Bequest of Marianne Khuner,,Lucca,,,Italy,,,,,,,,,http://www.metmuseum.org/art/collection/search/716639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +171.1 C17,true,true,699505,The Libraries,,"Dialogos de la pintvra : sv defensa, origen, essecia, definicion, modos y diferencias",,,,,,Author|Publisher|Engraver|Engraver,,Vicente Carducho|Francisco Martínez|Francisco López|Francisco Fernández,"Italian, 1570/78–1638|Spanish, active 1627–45|Spanish, ca. 1552–1629|Spanish, 1605–1646",,"Carducho, Vincente|Martínez, Francisco|López, Francisco|Fernández, Francisco",,1570 |1625 |1550 |1605,1638 |1645 |1630 |1646,1633,1633,1633,,"[18], 229, [24] pages, illustrations, height: 7 7/8 in. (20 cm)",,,Madrid,,,Spain,,,,,,,,,http://www.metmuseum.org/art/collection/search/699505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +U820.A9 S34 1603 F,true,true,697348,The Libraries,,Der aller durchleuchtigisten und grosmächtigen Kayser ... Königen ... Herren vom Adel ... Bildtnussen und kurtz Beschreibungen ihrer so wol in Fridts- als Kriegszzeiten verrichten fürnembsten Thaten und Handlungen : deren Waffen und Rüstungen ... auss allen Landen der Welt ... in dem Schloss Ombrass ... zu ewiger Gedächtnuss auffbehalten werden ...,,,,,,Author|Translator|Publisher|Engraver|Illustrator,,Jacob Schrenck von Nozing|Johann Engelbert Noyse von Campenhouten|Daniel Baur|Dominicus Custos|Giovanni Battista Fontana,"died 1612|active 1603|died 1639|German, Antwerp after 1550–1612 Augsburg|Italian, ca. 1524–1587",,"Schrenck von Nozing, Jacob|Noyse von Campenhouten, Johann Engelbert|Baur, Daniel|Custos, Dominicus|Fontana, Giovanni Battista",,1603 |1550 |1524,1612 |1603 |1639 |1612 |1587,1603,1603,1603,,"126 engraved plates, Height: 19 5/16 in. (49 cm)",,,Innsbruck,,,Austria,,,,,,,,,http://www.metmuseum.org/art/collection/search/697348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +QB7 .C15 1772,true,true,681349,The Libraries,,"Calendrier belgique, curieux et utile, contenant les evenemens historiques sur les jours de l'an, et les travaux à faire dans les jardins en chaque mois de l'année, la description des tableaux remarquables, que l'on trouve dans la ville de Gand, avec les noms des peintres pour l'annėe MDCCLXXII",,,,,,Publisher,,Pierre de Goesin,,,Pierre de Goesin,,0,0,1772,1772,1772,,"108 pages, Height: 4 5/16 in. (11 cm)","Gift of Jayne Wrightsman, 2009",,Ghent,,,Belgium,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +110.5B75 C44,true,true,591837,The Libraries,,Le pitture e sculture di Brescia che sono esposte al pubblico con un' appendice di alcune private gallerie,"Brescia: [s.n.], 1760",,,,,Author,,Luigi Chizzola,,,"Chizzola, Luigi",,1690,1790,1760,1760,1760,Illustrated book,H: 8 1/4 in. (21 cm),Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +201.9F88 B73,true,true,591862,The Libraries,,"Edwin Davis French, A Memorial: His Life, His Art","New York: De Vinne Press, 1908",,,,,Author,,Ira Hutchinson Brainerd,1862–1935,,Brainerd Ira Hutchinson,,1862,1935,1908,1908,1908,Illustrated book,H: 9 7/ 8 in. (25 cm),Presented by Mr. William L. Andrews,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +911.1. M362,true,true,591836,The Libraries,,Il Ritratto di Venezia,"Venice: Presso Gio. Giacomo Hertz, 1684",,,,,Author,,Domenico Martinelli,active 1663–1669,,Martinellie Domenico,,1663,1669,1684,1684,1684,Printed book,H: 6 3/4 in. (17 cm),Presented by Bobby and Allan Weissglass,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +161 L841,true,true,700238,The Libraries,,"The cabinet-makers' London book of prices, and designs of cabinet work, calculated for the convenience of cabinet makers in general, whereby the price of executing any piece of work may be easily found",,,,,,Printer|Publisher|Author,,W. Brown and A. O'Neil|London Society of Cabinet Makers|London Society of Cabinet Makers,London,,"Brown, W. and O'Neil A.|London Society of Cabinet Makers|London Society of Cabinet Makers",,1700,1850,1793,1793,1793,,"xvi, 266, 24 pages, 29 leaves of plates, height: 10 5/8 in. (27 cm)",,,London,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/700238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +170.1 R674,true,true,738466,The Libraries,,Modern chromatics : with applications to art and industry,,,,,,Author,,Ogden Nicholas Rood,1831-1902,,"Rood, Ogden Nicholas",,1831,1902,1879,1879,1879,,"3 pages, 1 leaf, [v]-viii, [9]-329 pages : color frontispiece, illustrations, diagrams ; Height: 7 7/8 in. (20 cm)",,,New York,New York,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/738466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +551 G41,true,true,700032,The Libraries,,Petri Gyllii De Bosporo thracio libri III ; Petri Gyllii De topographia Constantinopoleos,,,,,,Publisher|Author,,"Apvd Gvlielmvm, svb scvto veneto|Pierre Gilles",1490–1555,,"Apvd Gvlielmvm, svb scvto veneto|Gilles, Pierre",,1490,1555,1562,1562,1562,,2 parts in 1 volume ; Height: 9 13/16 in. (25 cm),,,Lvgdvni,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/700032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +146.9 T34,true,true,700050,The Libraries,,"Traité de l'horlogerie, méchanique et pratique, approuvé par l'Academie royale des sciences",,,,,,Author,,Antoine Thiout l'aîné,1692–1767,,"Thiout, Antoine",,1690,1770,1741,1741,1741,,"2 volumes ([26], 400 pages, 50, 41 folded leaves of plates) : illustrations ; Height: 10 1/4 in. (26 cm)",,,Paris,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/700050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +170.1 C424,true,true,738463,The Libraries,,"The principles of harmony and contrast of colours, and their applications to the arts",,,,,,Author|Translator,,Michel Eugène Chevreul|Charles Martel,1786-1889|-1865,,"Chevreul, Michel Eugène|Martel, Charles",,1786,1889 |1865,1872,1872,1872,,"xlvi, 465 pages, 3 plates ; Height: 7 1/2 in. (19 cm)",,,London,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/738463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +106.1 V28 F,true,true,704714,The Libraries,,Mr. Vanderbilt's house and collection,,,,,,Publisher|Author,,George Barrie|Edward Strahan (Earl Shinn),"American, 1838–1886",,"Barrie|Strahan, Edward",,1838,1886,1883–84,1883,1884,,"4 volumes, illustrations, height: 18 7/8 in. (48 cm)",,,Boston,Massachusetts,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/704714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +161 H41,true,true,700240,The Libraries,,"The cabinet-maker and upholsterer's guide, or, Repository of designs for every article of household furniture, in the newest and most approved taste : displaying a great variety of patterns for chairs, stools ... in the plainest and most enriched styles : with a scale to each, and an explanation in letter press : also the plan of a room, shewing the proper distribution of the furniture ... from drawings",,,,,,Publisher|Author,,I. & J. Taylor|A. Hepplewhite & Co.,"London|British, 18th century",,"Taylor, I. & J.|Hepplewhite, A. & Co.",,1750 |1750,1850 |1800,1788,1788,1788,,"30 pages, 125 leaves of plates, illustrations, height: 14 9/16 in. (37 cm)",,,London,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/700240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +F595 .R38 1895,true,true,738758,The Libraries,,Pony tracks,,,,,,Publisher|Artist,,Harper & Brothers|Frederic Remington,"American, New York|American, Canton, New York 1861–1909 Ridgefield, Connecticut",,"Harper & Brothers|Remington, Frederic",,1833 |1861,1962 |1909,1895,1895,1895,,"viii pages, 1 leaf, 269 pages including plates : illustrations, frontispiece ; Height: 9 1/16 in. (23 cm)",Gift of Friends of the Thomas J. Watson Library,,New York,New York,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/738758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +NK2229 .S54 1793,true,true,700232,The Libraries,,"The cabinet-maker and upholsterer's drawing-book, in three parts",,,,,,Publisher|Printer|Author,,Thomas Sheraton|Thomas Bensley|Thomas Sheraton,"British, Stockton-on-Tees 1751–1806 London|London|British, Stockton-on-Tees 1751–1806 London",,"Sheraton, Thomas|Bensley, Thomas|Sheraton, Thomas",,1751 |1759 |1751,1806 |1835 |1806,1793–94,1793,1794,,"2 volumes, illustrations, height: 10 5/8 in. (27 cm)",,,London,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/700232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +NK1510 .J7 1868 Q,true,true,700091,The Libraries,,The grammar of ornament,,,,,,Author|Artist|Artist|Publisher|Artist,,Owen Jones|J. O. Westwood|Matthew Digby Wyatt|Bernard Quaritch Ltd.|J. B. Waring,"British, London 1809–1874 London|British, Rowde, Wiltshire 1820–1877 Cowbridge, South Glamorgan",,"Jones, Owen|Westwood, J. O.|Wyatt, Matthew Digby Sir|Bernard Quaritch Ltd.|Waring, J. B.",,1809 |1805 |1820,1874 |1893 |1877,1868,1868,1868,,"157 pages : illustrations, plates ; Height: 13 3/4 in. (35 cm)",,,London,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/700091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PS2267 .A1 1891,true,true,738765,The Libraries,,The song of Hiawatha,,,,,,Publisher|Illustrator|Author,,Houghton Mifflin Company|Frederic Remington|Henry Wadsworth Longfellow,"American, Canton, New York 1861–1909 Ridgefield, Connecticut|American, Portland, Maine 1807–1882 Cambridge, Massachusetts",,"Houghton Mifflin Company|Remington, Frederic|Longfellow, Henry Wadsworth",,1861 |1807,1909 |1882,1891,1891,1891,,"xviii, 242 pages : frontispiece (portrait), illustrations, plates ; Height: 9 7/16 in. (24 cm)",Gift of Friends of the Thomas J. Watson Library,,New York,New York,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/738765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +DH188.H67 W47 1568,true,true,682010,The Libraries,,"La dedvction de l'innocence de messire Philippe Baron de Montmorency, Conte de Hornes, franc seigneur de Vveert, admiral & capitaine general de la Mer du pais bas ... contre la malicievse apprehension, indeüe detention, injuste procedure, fausse accusation, iniques sentences et tyrannicque exécution en sa personne à grand tort, par voye de faict perpetrees",,,,,,Author,,Jacques de Wesenbeke,1523?–?1577,,"Wesenbeke, Jacques de",,1523,1577,1568,1568,1568,,"[16], 573 [i.e. 572], [2] pages ; Overall: 6 1/8 × 4 1/8 × 1 3/8 in. (15.6 × 10.5 × 3.5 cm)","Gift of Jayne Wrightsman, 2008",,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/682010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +M2149.5 .C5 1768,true,true,681259,The Libraries,,"Loffice de Noël, 1768",,,,,,Author|Calligrapher,,Catholic Church|Baudouin,,,Catholic Church|Baudouin,,0 |0,0 |0,1768,1768,1768,,209 pages : music ; Height: 8 1/4 in. (21 cm),"Gift of Jayne Wrightsman, 2008",,Versailles,,,France,,,,,,,||,,http://www.metmuseum.org/art/collection/search/681259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +GV1801 .L6 1889,true,true,726717,The Libraries,,Les jeux du cirque et la vie foraine,,,,,,Author|Illustrator|Publisher,,"Hugues Le Roux|Jules Garnier|E. Plon, Nourrit et Cie.","French, 1847–1889",,"Le Roux Hugues|Garnier, Jules|Plon E., Nourrit et Cie.",,1847,1889,1889,1889,1889,,"v, 250 pages : illustrations(some color), color portraits ; Height: 11 13/16 in. (30 cm)",,,Paris,,,France,,,,,,,,,http://www.metmuseum.org/art/collection/search/726717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AE25 .E531 1762 Q,true,true,707455,The Libraries,,"Recueil de planches, sur les sciences, les arts libéraux, et les arts méchaniques : avec leur explication",,,,,,Author|Publisher|Author|Author,,Pierre Mouchon|Briasson|Jean Le Rond d'Alembert|Denis Diderot,"French, 1733–1797|French, 1717–1783|French, 1713–1784",,"Mouchon, Pierre|Briasson|Alembert, Jean Le Rond d'|Diderot, Denis",,1733 |1717 |1713,1797 |1783 |1784,1762–72,1762,1772,,11 volumes ; H: 15 3/4 in. (40 cm),"Jane E. Andrews Fund, in memory of her husband, William Loring Andrews, 1955",,Paris,,,France,,,,,,,,,http://www.metmuseum.org/art/collection/search/707455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +100.1 B591,true,true,726673,The Libraries,,"Grammaire des arts du dessin : architecture, sculpture, peinture",,,,,,Illustrator|Author,,Léon Gaucherel|Charles Blanc,"French, Paris 1816–1886|French, Castres 1813–1882 Paris",,"Gaucherel, Léon|Blanc, Charles",,1816 |1813,1886 |1882,1876,1876,1876,,"691 pages : illustrations, color plate, diagrams ; Height: 11 in. (28 cm)",,,Paris,,,France,,,,,,,,,http://www.metmuseum.org/art/collection/search/726673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +100.1 B59,true,true,726669,The Libraries,,"Grammaire des arts du dessin : architecture, sculpture, peinture",,,,,,Author|Illustrator,,Charles Blanc|David-Pierre Giottino Humbert de Superville,"French, Castres 1813–1882 Paris|Dutch, The Hague 1770–1849 Leiden",,"Blanc, Charles|Humbert, de Superville David-Pierre Giottino",,1813 |1770,1882 |1849,1870,1870,1870,,"743 pages : illustrations, color plate ; Height: 11 in. (28 cm)",,,Paris,,,France,,,,,,,,,http://www.metmuseum.org/art/collection/search/726669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +BX2016.A5 F7 1792,true,true,681246,The Libraries,,"L'office de l'Église en françois contenant les offices pour toute l'année, plusieurs prières tirées de l'écriture-sainte & des saints pères, les hymnes en vers françois, avec une instruction pour les fidèles",,,,,,Author|Publisher,Chez,Catholic Church|Langlois,,,Catholic Church|Langlois,,0,0,1792,1792,1792,,"[12], 612, [2] pages ; Height: 7 1/16 in. (18 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AY831 .Z7 1792,true,true,679732,The Libraries,,"Almanach royal, année bissextile M.DCC.LXCII : présenté a sa Majeste pour la premiere fois en 1699 par Laurent d'Houry",,,,,,Editor|Publisher,,Laurent Charles d' Houry|Imprimerie de Testu,"French, 1717?–1786",,"Houry, Laurent Charles d'|Imprimerie de Testu",,1717,1786,1791,1791,1791,,"679 pages, 1 folded map, Height: 7 7/8 in. (20 cm)","Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/679732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +273.4D26 AL2,true,true,726678,The Libraries,,"Honoré Daumier, l'homme et l'œuvre : ouvrage orné d'un portrait à l'ea-forte, de deux héliogravures et de 47 illustrations",,,,,,Subject of book|Author,,Honoré Daumier|Arsène Alexandre,"French, Marseilles 1808–1879 Valmondois",,"Daumier, Honoré|Alexandre, Arsène",,1808,1879,1888,1888,1888,,"4 pages of leaves, 383 pages : 12 plates, 1 portrait, illustrations ; Height: 10 5/8 in. (27 cm)",,,Paris,,,France,,,,,,,,,http://www.metmuseum.org/art/collection/search/726678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +912.63 D23,true,true,591835,The Libraries,,"Asia; oder, Genaue und grundliche Beschreibung des gantzen Syrien und Palestins, oder belobten Landes . . .","Amsterdam : Jacob von Meursen, 1681",,,,,Author,,Olfert Dapper,"Dutch, 1635–1689",,"Dapper, Olfert",Dutch,1639,1689,1681,1681,1681,Illustrated book,2 pts. in 1 vol.; H: 12 1/4 in. (31 cm),Presented by Mrs. John C. McVoy,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +DC130.L14 L14 1749,true,true,681559,The Libraries,,"Memoires et réflexions sur les principaux évenemens du regne de Louis XIV, & sur le caractere de ceux qui y ont eu la principale part. Par mr. l.m.d.L.F",,,,,,Author,,"Charles Auguste, marquis de La Fare","French, 1644–1712",,"Auguste, Charles, marquis de La Fare",French,1644,1712,1749,1749,1749,,15 cm,"Gift of Jayne Wrightsman, 2008",,Amsterdam,,,Netherlands,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +250 T34,true,true,591839,The Libraries,,"The Theory and Practice of Gardening : Wherein is Fully Handled all that Relates to Fine Gardens, Commonly called Pleasure-Gardens, as Parterres, Groves, Bowling-Greens &c. ....","London: printed by Geo. James, 1712",,,,,Author,,Antoine Joseph Dézallier d'Argenville,"French, Paris 1680–1765 Paris",,"Dézallier d'Argenville, Antoine Joseph",French,1680,1765,1712,1712,1712,Illustrated book,H: 10 1/4 in. (26 cm),"Presented in memory of Daniel W. Langton, Landscape Architect, by Mrs. Langton",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PQ1954.A54 L4 1741,true,true,681502,The Libraries,,"Lettres cabalistiques, ou, Correspondance philosophique, historique & critique, entre deux cabalistes, divers esprits elementaires, & le seigneur Astaroth",,,,,,Author|Publisher,Chez,"Jean Baptiste de Boyer, marquis d'Argens|Pierre Paupie","French, 1704–1771",,"Argens, Jean-Baptiste de Boyer, marquis d'|Pierre Paupie",French,1704 |0,1771 |0,1741,1741,1741,,"6 volumes: illustrations, portraits; Height: 6 5/16 in. (16 cm)","Gift of Jayne Wrightsman, 2008",,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AY831 .Z7 1783,true,true,680708,The Libraries,,Almanach royal : année bissextile M.DCC.LXXXIV. présenté a Sa Majesté pour la premiere fois en 1699 par Laurent d'Houry ...,,,,,,Publisher,,Laurent Charles d' Houry,"French, 1717?–1786",,"Houry, Laurent Charles d'",French,1717,1786,1783,1783,1783,,683 pages,"Gift of Jayne Wrightsman, 2009",,Paris,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/680708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +ND616 .G35 1812,true,true,682015,The Libraries,,"Galerie Giustiniani, ou, Catalogue figuré des tableaux de cette célèbre galerie, transportée d'Italie en France, accompagné d'observations critiques et historiques, et de soixante-douze planches gravée au trait, contenant environ cent cinquante sujets",,,,,,Editor|Printer|Binder,,Charles-Paul Landon|Chaignieau|Simier,French,,"Landon, Charles-Paul|Chaignieau|Simier",French,0019 |0,0019 |0,1812,1812,1812,,"160 pages, 73 leaves of plates; Height: 8 11/16 in. (22 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/682015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +BX9454 .S25 1688,true,true,681386,The Libraries,,"Réponse aux plaintes des protestans touchant la prétendüe persecution de France. : Où l'on expose le sentiment de Calvin, & de tous les plus célebres ministres, sur les peines dûës aux hérétiques. On découves aussi plusieurs particularitez dignes d'être sçûës, touchant la Réformation & les réformateurs",,,,,,Printer|Author,Chez,Arnold Seneuse|Denis de Sainte-Marthe,"French, 1650–1725",,Arnold Seneuse|Denis de Sainte-Marthe,French,1650,1725,1688,1688,1688,,"[60], 10 [i.e. 310], [38] pages","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PA6558 .A5 1726,true,true,679476,The Libraries,,"Histoire secrette de Neron, ov, Le festin de Trimalcion, traduit de Petrone, avec des notes historiques par M. Lavaur ...",,,,,,Author|Author,,M. (Guillaume de) Lavaur|Petronius Arbiter,"French, 1653–1730",,"Lavaur, M. (Guillaume de)|Petronius Arbiter",French,1653,1730,1726,1726,1726,,"2 volumes bound in 1 (2 pages, leaf, lxxij, 192 pages; 1 leaf, 193-447, 3 pages), Height: 6 5/16 in. (16 cm)","Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/679476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +TS1484 .B56 1777,true,true,681551,The Libraries,,Mémoire sur un rouet a filer des deux mains a la fois,,,,,,Author|Publisher,,de Bernieres|Clousier,"died 1783|French, active 18th century",,"Bernieres, de|Clousier",French,1683 |1700,1783 |1800,1777,1777,1777,,"vi, 7-22 pages: illustrated plates; Height: 9 13/16 in. (25 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AY831 .Z7 1788,true,true,680818,The Libraries,,"Almanach royal, année bissextile M.DCC.LXXXVIII, présenté a sa Majeste pour la premiere fois en 1699",,,,,,Publisher,,Laurent Charles d' Houry,"French, 1717?–1786",,"Houry, Laurent Charles d'",French,1717,1786,1787,1787,1787,,"716 pages, Height: 7 7/8 in. (20 cm)","Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/680818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AY831 .Z7 1784a,true,true,680752,The Libraries,,"Almanach royal, année bissextile M.DCC.LXXXIV.",,,,,,Publisher,,Laurent Charles d' Houry,"French, 1717?–1786",,"Houry, Laurent Charles d'",French,1717,1786,1783,1783,1783,,"127 pages, Height: 4 5/16 in. (11 cm)","Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/680752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +221.1 K63,true,true,591844,The Libraries,,"Phonurgia nova, sive conjugium mechanico-physicum artis & natvrae paranympha phonosophia concinnatum . . .","Campidonae: Rudolphum Dreherr, 1673",,,,,Author,,Athanasius Kircher,"German, 1602–1680",,Kirchner Athanasius,German,1602,1680,1673,1673,1673,Illustrated book,H: 13 3/8 in. (34 cm),"Purchased with income from the Jacob S. Rogers Fund and Bought with the income from the bequest of Nathaniel I. Bowditch, of Boston, (Class of 1812)",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +532 L551,true,true,591849,The Libraries,,Denkmaler aus Aegypten und Aethiopien: nach den Zeichnungen der von Deiner Majestat dem Konige von Preussen Friedrich Wilhelm IV Nach Diesen Landern Gesendeten und in den Jahren 1842–1845 Ausgefuhrten Wissenschaftlichen Expedition . . .,"Leipzig, 1897–1913",,,,,Author|Author|Author,,Richard Lepsius|Édouard F. Naville|Ludwig Borchardt,"German, 1810–1884",,"Lepsius, Richard|Naville, Édouard F.|Borchardt, Ludwig",German,1810 |1844 |1863,1884 |1926 |1938,1913,1913,1913,Illustrated books,5 vols.; H: 25 1/4 in. (64 cm),Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +254 B63,true,true,591830,The Libraries,,"Architectura Curiosa Nova: Das ist, neue ergotzliche Sinn und Kunstreiche auch nutzliche Bau- und Wasser-Kunst . . .","Nuremberg : [s.n.], [1664]",,,,,Author,,Georg Andreas Böckler,"German, Cronheim 1644–1698 Ansbach",,"Böckler, Georg Andreas",German,1644,1698,1664,1664,1664,"Printed book, engraved plates",4 pts. in 1 vol.; H: 13 3/8 in. (34 cm),Purchased with income from the Jacob S. Rogers Fund,,Nuremberg,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +125.97 D932,true,true,591824,The Libraries,,"""Alberti Dvreri pictoris et architecti praestantissimi De vrbibvs...""","Paris: Officina Christiani Wecheli, 1535",,,,,Author,,Albrecht Dürer,"German, Nuremberg 1471–1528 Nuremberg",,"Dürer, Albrecht",German,1471,1528,1535,1535,1535,Illustrated book,78 pp.; H: 13 3/4 in. (35 cm),Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +912.1 K63 Q,true,true,591834,The Libraries,,"Athanasii Kircheri e Soc. Jesu China monumentis: qua sacris qua profanis, nec non variis naturae & artis spectaculis, aliarumque rerum memorabilium argumentis illustrata",,,,,,Author,,Athanasius Kircher,"German, 1602–1680",,Kirchner Athanasius,German,1602,1680,1667,1667,1667,Printed book,H: 14 5/8 in. (37 cm),"Jane E. Andrews Fund, in memory of her husband, William Loring Andrews",,Amsterdam: Apud Joannem Janssonium a Waesberge,"& Elizeum Weyerstraet, 1667",,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +916.3 An4 F,true,true,591854,The Libraries,,The New Zealanders,"London: T. McLean, 1847",,,,,Author,,George French Angas,"British, 1822–1886",,Angas George French,British,1822,1886,1847,1847,1847,Illustrated book,H: 22 1/8 in. ( 56 cm),Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +161.1 C44 Q,true,true,591840,The Libraries,,"The Gentleman and Cabinet-maker's Director: Being a Large Collection of . . . Designs of Household Furniture in the Gothic, Chinese and Modern Taste . . .","London: Thomas Chippendale, 1754",,,,,Author,,Thomas Chippendale,"British, baptised Otley, West Yorkshire 1718–1779 London",,"Chippendale, Thomas",British,1718,1779,1754,1754,1754,"Printed book, engraved plates",17 3/4 x 12 1/4 in. (45 x 31 cm),Library Purchase,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +926.1 M642,true,true,591864,The Libraries,,Paradise Lost,"Hammersmith: Doves Press, 1902",,,,,Author,,John Milton,"British, London 1608–1674 London",,"Milton, John",British,1608,1674,1902,1902,1902,Printed book,386 + [2] pp.; H: 9 1/2 in. (24 cm),Presented by Alice M. Dike,,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/591864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +143.7W41 W413,true,true,591841,The Libraries,,An Address to the Workmen in the Pottery on the Subject of Entering into the Service of Foreign Manufacturers,"Newcastle: J. Smith, 1783 (1st ed.)",,,,,Artist,,Josiah Wedgwood,"British, Burslem, Stoke-on-Trent 1730–1795 Burslem, Stoke-on-Trent",,"Wedgwood, Josiah",British,1730,1795,1783,1783,1783,Printed book,24 pp.; H: 7 7/8 in. (20 cm),Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +170.9 F46,true,true,591855,The Libraries,,"On Painting in Oil and Water Colours, Landscape and Portraits . . .",,,,,,Author,,Theodore Henry Adolphus Fielding,"British, Yorkshire 1781–1851 Croyden",,"Fielding, Theodore Henry Adolphus",British,1781,1851,1839,1839,1839,Illustrated book,H: 11 in. (28 cm),Purchased with income from the Jacob S. Rogers Fund,,London: published for the author,"by Ackermann and Co., 1839",,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +920.3 W52 F,true,true,591842,The Libraries,,The letters of Sir Richard Westmacott,,,,,,Author,,Sir Richard Westmacott,"British, 1775–1856",,"Westmacott, Richard, Sir",British,1775,1856,19th century,1800,1899,Collection of letters,"18 vols. (ca. 1,400 items) plus index",Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/591842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +KKL273.1.Z94 C4 1815,true,true,681372,The Libraries,,Codice civile generale austriaco,,,,,,Binder,,Luigi Lodigiani,"Italian, 1778–1843",,"Lodigiani, Luigi",Italian,1778,1843,1815,1815,1815,,Length: 9 13/16 in. (25 cm),"Gift of Jayne Wrightsman, 2008",,Milan,,,Italy,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +226 B641,true,true,591845,The Libraries,,Descrizione degli' istromenti armonici d'ogni genere . . .,Presso Pietro Paolo Montignani-Mirabili,,,,,Author,,Filippo Buonanni,"Italian, 1638–1725",,"Buonanni, Filippo",Italian,1638,1725,1806,1806,1806,"Printed books, engraved plates",2 vols.,Presented by Mr. S.P. Avery,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +532 Eg961,true,true,591847,The Libraries,,"Narrative of the Operations and Recent Discoveries Within the Pyramids, Temples, Tombs and Excavations in Egypt and Nubia; and of a Journey to the Coast of the Red Sea, in Search of the Ancient Berenice; and Another to the Oasis of Jupiter Ammon","London: J. Murray, 1820",,,,,Author,,Giovanni Battista Belzoni,"Italian, 1778–1823",,"Belzoni, Giovanni Battista",Italian,1778,1823,1820,1820,1820,Printed book,xix + 533 pp.; H: 11 3/8 in. (29 cm),Presented by Mr. Theodore M. Davis,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +109A L83,true,true,591829,The Libraries,,"Trattato dell' arte della pittura, scultura et architetettura","Milan: [s.n.], 1585",,,,,Author,,Giovanni Paolo Lomazzo,"Italian, Milan 1538–1600 Milan",,"Lomazzo, Giovanni Paolo",Italian,1538,1600,1585,1585,1585,Printed book,H: 8 5/8 in. (22 cm),Purchased with income from the Library Fund,,Milan,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +120.32P17 P17,true,true,591828,The Libraries,,I quattro libri dell'architettura di Andrea Palladio . . .,"Venice: Domenico de'Franceschi, 1570",,,,,Author,,Andrea Palladio,"Italian, Padua 1508–1580 Vicenza",,"Palladio, Andrea",Italian,1508,1580,1570,1570,1570,Illustrated book,"4 pts. in 1 vol., 128 pp.; H: 4 3/4 in. (12 cm)",Library Purchase,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591828,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +131.1M58 C75,true,true,591825,The Libraries,,Vita di Michelagnolo Buonarroti raccolta per Ascanio Condivi da la Ripa Transone,"Rome: Antonio Blado, 1553 (1st ed.)",,,,,Author,,Ascanio Condivi,"Italian, Ripatransone 1525–1574 Ripatransone",,"Condivi, Ascanio",Italian,1525,1574,1553,1553,1553,Illustrated book,H: 7 7/8 in. (20 cm),Purchased with income from the Jacob S. Rogers Fund,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +120 Se6,true,true,591827,The Libraries,,"Bononiensis de architectura libri quinque quibus cuncta fere architectonicae facultatis mysteria docte, perspicue . . .",,,,,,Author,,Sebastiano Serlio,"Italian, Bologna 1475–1554 Fontainebleau",,"Serlio, Sebastiano",Italian,1475,1554,1568–69,1568,1569,Illustrated book,5 pts. in 1 vol.; H: 12 5/8 in. (32 cm),Purchased with income from the Jacob S. Rogers Fund,,Venice: Apud Francifcum de Francifcis Senenfem,"& Joanneum Chriegher, 1568–1569",,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +U101 .M7814 1760,true,true,681554,The Libraries,,"Memoires de Montecuculi, generalissime des troupes de l'empereur : divisés en trois livres : I. De l'art militaire en général, II. De la guerre contre le turc, III. Relation de la campagne de 1664",,,,,,Author,,Prince Raimondo Montecuccoli,"Italian, 1609–1680",,"Montecuccoli, Raimondo, Prince",Italian,1609,1680,1760,1760,1760,,"xl, 510, [4] pages: leaves of plates, illustrations, engravings; Height: 6 11/16 in. (17 cm)","Gift of Jayne Wrightsman, 2008",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +201.9Av3 Av32,true,true,591857,The Libraries,,"S. P. Avery, Engraver on Wood","New York: [s.n.], [18--]",,,,,Author,,Samuel Putnam Avery Sr.,"American, New York 1822–1904 New York",,"Avery, Samuel Putnam, Sr.",American,1822,1904,1800s,1800,1810,Scrapbook,Height: 11 13/16 in. (30 cm),Presented by Emma Avery Welcher and Amy Ogden Welcher,,,,,,,,,,,,||,,http://www.metmuseum.org/art/collection/search/591857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +543.1N48 M97 Q,true,true,591850,The Libraries,,"A Descriptive Atlas of the Cesnola Collection of Cypriote Antiquities in the Metropolitan Museum of Art, New York (1885–1903)","Boston: J. R. Osgood, 1885–1903",,,,,Author|Author,Introduction by,Ernst Curtius|Luigi Palma di Cesnola,,,"Curtius Ernst|Cesnola, Luigi Palma di",American,1832,1904,1885–1904,1885,1904,Illustrated book,3 vols.; H: 17 3/8 in. (44 cm),Presented by General L. P. di Cesnola,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +UD S83 1842 v.1,true,true,591852,The Libraries,,"Incidents of Travel in Central America, Chiapas, and Yucatan","London: J. Murray, 1842 (new ed.)",,,,,Author,,John Lloyd Stephens,"American, 1805–1852",,Stephens John Lloyd,American,1805,1852,1842,1842,1842,Printed book,2 vols.; H: 9 in. (23 cm),"Gift of the Dept. of Twentieth Century Art, 1998",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +UD S83 1843 v.2,true,true,591853,The Libraries,,Incidents of Travel in Yucatan,"New York: Harper Bros., 1843",,,,,Author,,John Lloyd Stephens,"American, 1805–1852",,Stephens John Lloyd,American,1805,1852,1843,1843,1843,Illustrated book,2 vols.; 9 in. (23 cm),"Gift of the Dept. of Twentieth Century Art, 1998",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +272.4 Au8,true,true,591859,The Libraries,,Autographs and Sketches from Artist Friends to Samuel P. Avery,,,,,,Author,,Samuel Putnam Avery Sr.,"American, New York 1822–1904 New York",,"Avery, Samuel Putnam, Sr.",American,1822,1904,1874–80,1874,1880,Letter-book (manuscript),1 vol.; H: 11 3/8 in. (29 cm),Presented by Amy Ogden Welcher and Emma Avery Welcher,,,,,,,,,,,,||,,http://www.metmuseum.org/art/collection/search/591859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +201.9 Av3,true,true,591858,The Libraries,,Diaries of Samuel P. Avery,,,,,,Author,,Samuel Putnam Avery Sr.,"American, New York 1822–1904 New York",,"Avery, Samuel Putnam, Sr.",American,1822,1904,1871–82,1871,1882,Bound manuscript,5 vols.; H: 7 1/8 in. (18 cm),"Presented by Emma Avery Welcher, Amy Ogden Welcher and Alice Lee (Mrs. C. Telford) Erickson. (Avery's granddaughters)",,,,,,,,,,,,|,,http://www.metmuseum.org/art/collection/search/591858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +192H221 B61,true,true,591861,The Libraries,,"Scrapbook containing newspaper and magazine clippings, calling cards, sketches, photographs of paintings, and two sales catalogs of Harnett's (William Michael Harnett, 1848–1892) work",,,,,,Author,,William Ignatius Blemly,American,,Blemly William Ignatius,American,0018,0018,,1848,1900,Scrapbook,108 pp.; H: 10 1/4 in. (26 cm),Presented by James Maroney in friendship for H. Barbara Weinberg,,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/591861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AE25 .E53 1751 Q,true,true,591843,The Libraries,,"Encyclopédie, ou Dictionnaire raisonné des sciences, des arts et des métiers",,,,,,Publisher|Author|Author,,Briasson|Denis Diderot|Jean Le Rond d'Alembert,"French, 1713–1784|French, 1717–1783",,"Briasson|Diderot, Denis|Alembert, Jean Le Rond d'",French|French,1713 |1717,1784 |1783,1751–65,1751,1765,,17 volumes ; H: 15 3/4 in. (40 cm),Bequest of Marianne Khuner,,Paris,,,France,,,,,,,,,http://www.metmuseum.org/art/collection/search/591843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +AY831 .Z7 1784,true,true,681357,The Libraries,,"Le calendrier de la cour : tiré des ephémérides, pour l'année bissextile mil sept-cent quatre-vingt-quatre : contenant le lieu du soleil, son lever, son coucher, sa déclinaison: le lever de la lune & son coucher, &c. : avec la naissance des rois, reines, princes & princesses de l'Europe imprimé pour la famille royale et maison de sa Majesté",,,,,,Printer|Author,Chez,La Veuve Hérissant|Jacques Collombat,"French, 1668–1744",,"Hérissant, La Veuve|Collombat, Jacques",French|French,1668,1744,1784,1784,1784,,Height: 4 5/16 in. (11 cm),"Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/681357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +PQ1189 .M66 1765,true,true,680832,The Libraries,,"Anthologie françoise, ou, Chansons choisies, depuis le 13a siècle jusqu'à présent",,,,,,Publisher|Author|Author,,Barbou|Jean Monnet|Anne Gabriel Meusnier de Querlon,"French, 1703–1785|French, 1702–1780",,"Barbou|Monnet, Jean|de Querlon, Anne Gabriel Meusnier",French|French,0 |1703 |1702,0 |1785 |1780,1765,1765,1765,,"3 volumes, frontispiece, portraits, plates, Height: 7 1/2 in. (19 cm)","Gift of Jayne Wrightsman, 2009",,Paris,,,France,,,,,,,|,,http://www.metmuseum.org/art/collection/search/680832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2000.194.1, .2",false,true,26555,Arms and Armor,Pair of flintlock pistols,Pair of Flintlock Pistols,"Scottish, Doune",,,,,Gunsmith,,Alexander Campbell,"Scottish, Doune, died 1790",,"Campbell, Alexander",Scottish,1690,1790,ca. 1750–70,1725,1795,"Steel, silver",L. 11 3/4 in. (29.8 cm),"Gift of Edward Coe Embury Jr., Philip Aymar Embury, and Dorothy Embury Staats, in memory of Aymar Embury II and his wife, Jane Embury Benepe, 2000",,Doune,Perthshire,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/26555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.208.5,false,true,24824,Arms and Armor,Wheellock pistol,Wheellock Pistol,"Swiss, Zürich",,,,,Gunsmith,,Felix Werder,"Swiss, Zurich, 1591–1673",,"Werder, Felix",Swiss,1591,1673,dated 1640,1640,1640,"Steel, bronze, gold, wood",Cal. .50 in. (12.7 mm); L. 24 3/8 in. (61.9 cm); L. of barrel 17 in. (43.2 cm); L. of lockplate 6 5/8 in. (16.8 cm); Wt. 2 lb. 4 oz. (1021 g),"Gift of Alan Rutherfurd Stuyvesant, 1952",,Zürich,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/24824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.42,false,true,22115,Arms and Armor,Wheellock pistol,Wheellock Pistol,"Swiss, Zürich",,,,,Gunsmith,,Felix Werder,"Swiss, Zurich, 1591–1673",,"Werder, Felix",Swiss,1591,1673,dated 1630,1630,1630,"Steel, bronze, wood (beech), silver",L. 23 1/4 in. (59.1 cm); L. of barrel 16 in. (40.6 cm); L. of plug 1 3/4 in. (4.4 cm); L. of lock 6 1/2 in. (16.5 cm); Cal. 458 in. (11.6 mm); Wt. 3 lb. 7 oz. (1559 g),"Rogers Fund, 1910",,Zürich,,,,,,,,,,Firearms-Pistols-Wheellock,,http://www.metmuseum.org/art/collection/search/22115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.790a, b",false,true,22285,Arms and Armor,Armored skirt (base),Armored Skirt (Base),"Austrian, Innsbruck",,,,,Armorer,Attributed to,Konrad Seusenhofer,"Austrian, Innsbruck, died 1517",,"Seusenhofer, Konrad",Austrian,1417,1517,ca. 1510–15,1485,1540,"Steel, gold",D. 1/16 in. (0.2 cm); Wt. 12 lb. 14 oz. (5840 g),"Gift of William H. Riggs, 1913",,Innsbruck,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.121,false,true,22824,Arms and Armor,Left pauldron (shoulder defense) of a boy's costume armor,Left Pauldron (Shoulder Defense) from a Boy's Costume Armor,"Austrian, Innsbruck",,,,,Armorer,,Hans Seusenhofer,"Austrian, Innsbruck, 1470–1555",,"Seusenhofer, Hans",Austrian,1470,1555,ca. 1532,1507,1557,"Steel, copper alloy, gold",H. 4 1/2 in. (11.4 cm); W. 4 1/2 in. (11.4 cm); D. 3 1/2 in. (8.9 cm),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Innsbruck,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/22824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.304,false,true,23287,Arms and Armor,Left tasset (thigh defense) from a boy's costume armor,Left Tasset (Thigh Defense) from a Boy's Costume Armor,"Austrian, Innsbruck",,,,,Armorer,,Hans Seusenhofer,"Austrian, Innsbruck, 1470–1555",,"Seusenhofer, Hans",Austrian,1470,1555,ca. 1532,1507,1557,"Steel, copper alloy, gold",H. 8 3/8 in. (21.3 cm); W. 6 in. (15.2 cm); D. 4 1/2 in. (11.4 cm),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Innsbruck,,,,,,,,,,Armor Parts-Thigh and Leg Defense,,http://www.metmuseum.org/art/collection/search/23287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.157.13a, b",false,true,27958,Arms and Armor,Rapier with scabbard,Rapier with Scabbard,"German; blade, Spanish, Toledo",,,,,Bladesmith,Blade signed by,Juan Martinez,"Spanish, Toledo, active ca. 1600",,"Martinez, Juan",Spanish,1575,1625,ca. 1580,1555,1605,"Steel, leather, gold, copper wire, wood, velvet",L. 48 3/4 in. (123.8 cm); W. 8 1/2 in. (21.6 cm); D. 5 in. (12.7 cm); Wt. 3 lb. 5 oz. (1507 g); Wt. of scabbard 6 oz. (170.1 g),"Bashford Dean Memorial Collection, Gift of Mr. and Mrs. Robert W. de Forest, 1929",,Toledo,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/27958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.157.13a, b, .14",false,true,690007,Arms and Armor,Rapier and parrying dagger,Rapier and Parrying Dagger,"German; rapier blade, Spanish, Toledo",,,,,Bladesmith,Rapier blade signed by,Juan Martinez,"Spanish, Toledo, active ca. 1600",,"Martinez, Juan",Spanish,1575,1625,ca. 1580,1555,1605,"Steel, gold, copper wire, wood, velvet",L. 48 3/4 in. (123.8 cm); W. 8 1/2 in. (21.6 cm); D. 5 in. (12.7 cm); Wt. 3 lb. 5 oz. (1507 g),"Bashford Dean Memorial Collection, Gift of Mr. and Mrs. Robert W. de Forest, 1929",,Toledo,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/690007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.356,false,true,26554,Arms and Armor,Snaphaunce hunting rifle,Snaphaunce Hunting Rifle,"Swedish, Stockholm",,,,,Stock maker,Signed by,Jonas Schertiger the Younger,"Swedish, active 1715–died 1748",,"Schertiger, Jonas the Younger",Swedish,1715,1748,dated 1722,1722,1722,"Steel, wood (walnut), brass, horn",L. 43 3/8 in. (117.8 cm),"Purchase, Gifts of Prince Albrecht Radziwill and Charles M. Schott Jr., by exchange, and Rogers Fund, 1997",,Stockholm,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/26554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.151,false,true,24849,Arms and Armor,Barbute,Barbute,"Italian, Brescia",,,,,Armorer,,"Jacopo da Cannobio, called Bichignola","Italian, Brescia, active ca. 1460",,"Cannobio, Jacobo de","Italian, Brescia",1435,1485,ca. 1460,1435,1485,Steel,H. 11 3/8 in. (28.9 cm); W. 7 7/8 in. (20 cm); D. 10 5/8 in. (27 cm); Wt. 5 lb. 4 oz. (2381 g),"Gift of Mrs. George A. Douglass, in memory of her husband, 1960",,,Brescia,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/24849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.152,false,true,21962,Arms and Armor,Hunting knife,Hunting Knife,"Austrian, Hall",,,,,Bladesmith,,Hans Sumersperger,"Austrian, Hall, active 1492–1498",,"Sumersperger, Hans",Austrian,1492,1498,ca. 1500,1475,1525,"Steel, copper alloy, wood, bone, mother-of-pearl",L. 18 5/8 in. (47.29 cm); W. 2 1/2 in. (6.35 cm),"Rogers Fund, 1904",,Hall,Tyrol,,,,,,,,,Knives,,http://www.metmuseum.org/art/collection/search/21962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.704,false,true,23342,Arms and Armor,Hunting sword,Hunting Sword,"Austrian, Hall",,,,,Sword maker,Attributed to,Hans Sumersperger,"Austrian, Hall, active 1492–1498",,"Sumersperger, Hans",Austrian,1492,1498,ca. 1500,1475,1525,"Steel, copper alloy, horn, bone",L. 49 1/2 in. (125.7 cm); L. of blade 40 in. (101.6 cm); W. of blade 1 7/16 in. (3.6 cm); Wt. 3 ls. 9 oz. (1615.9 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Hall,Tyrol,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/23342,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.150.5a,false,true,23089,Arms and Armor,Sallet,Sallet,"Austrian, Innsbruck",,,,,Armorer,,Jörg Wagner,"Austrian, Innsbruck, recorded 1485–92",,"Wagner, Jörg",Austrian,1485,1492,ca. 1485–95,1460,1520,Steel,D. of tail 7 1/2 in. (19.1 cm),"Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,Innsbruck,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/23089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.4,false,true,24945,Arms and Armor,Elements of a light-cavalry armor,Elements of a Light-Cavalry Armor,"Austrian, Innsbruck or Mühlau",,,,,Armorer,,Christian Schreiner the Younger,"Austrian, Mühlau, recorded 1499–1528",,"Schreiner the Younger, Christian",Austrian,1499,1528,ca. 1505–10,1480,1535,"Steel, leather",H. as mounted approximately 32 in. (81.28 cm); Wt. 21 lb. 7 oz. (9724 g),"Purchase, Mr. and Mrs. Arthur Ochs Sulzberger Gift, in honor of Helmut Nickel, 1991",,Innsbruck,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/24945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.229,false,true,21984,Arms and Armor,Sallet,Sallet,"Austrian, Innsbruck",,,,,Armorer,Attributed to,Adrian Treytz the Elder,"Austrian, Innsbruck, active ca. 1473–92",,"Treytz the Elder, Adrian",Austrian,1448,1517,ca. 1480,1455,1505,Steel,H. 9 3/4 in. (24.8 cm); W. 11 1/2 in. (29.2 cm); D. 14 5/8 in. (37.1 cm); Wt. 6 lb. 2 oz. (2778 g),"Rogers Fund, 1904",,Innsbruck,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/21984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.35,false,true,23234,Arms and Armor,Close helmet,Close Helmet,"Austrian, Innsbruck",,,,,Armorer,,Hans Maystetter,"Austrian, Innsbruck, documented 1508–33",,"Maystetter, Hans",Austrian,1508,1533,ca. 1505–10,1480,1535,Steel,H. 11 in. (27.9 cm); W. 9 in. (22.9 cm); D. 10 3/8 in. (26.4 cm); Wt. 5 lb. 8 oz. (2495 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Innsbruck,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/23234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.74.4,false,true,23040,Arms and Armor,Breastplate,Breastplate,"Austrian, Innsbruck",,,,,Armorer,,Hans Maystetter,"Austrian, Innsbruck, documented 1508–33",,"Maystetter, Hans",Austrian,1508,1533,ca. 1510,1485,1535,Steel,H. 17 1/2 in. (44.45 cm); Wt. 5 lb. 11 oz. (2580 g),"Gift of George D. Pratt, 1928",,Innsbruck,,,,,,,,,,Armor Parts-Breastplates,,http://www.metmuseum.org/art/collection/search/23040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.1408a, b",false,true,22382,Arms and Armor,Pair of flintlock pistols,Pair of Flintlock Pistols,"Southern Netherlandish, Aachen",,,,,Gunsmith,,Leonardus Graeff,"Aachen (now Germany), active ca. 1670–80",,"Graeff, Leonardus",Aachen,1645,1705,ca. 1675–85,1650,1710,"Steel, gold, silver, ivory",L. of each 19 3/8 in. (49.2 cm); L. of each barrel 12 3/16 in. (30.9 cm); Cal. of each barrel .52 in. (13.2 mm); Wt. of each 2 lb. 3 oz. (992 g),"Gift of William H. Riggs, 1913",,Aachen,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/22382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"50.203.1, .2",false,true,24815,Arms and Armor,Pair of wheellock rifles,Pair of Wheellock Rifles Made for Emperor Leopold I (1640–1705),"Bohemian, Prague",,,,,Gunsmith,,Caspar Neireiter,"Bohemian, Prague, recorded 1667–ca. 1730",,"Neireiter, Caspar",Bohemian,1667,1755,ca. 1670–80,1645,1705,"Steel, silver, wood (walnut)",L. of each 43 3/8 in. (110.2 cm); L. of each barrel 32 1/16 in. (81.4 cm); Cal. of each .544 in. (13.8 mm); Wt. of 50.203.1 9 lb. 4 oz. (4196 g); Wt. of 50.203.2 9 lb. 3 oz. (4167 g),"Purchase, Joseph Pulitzer Bequest, 1950",,Prague,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/24815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.137a–c,false,true,24661,Arms and Armor,"Helmet, breastplate, and backplate","Helmet, Breastplate, and Backplate","Flemish, possibly Antwerp",,,,,Armorer,Signed on the backplate by,D. G. V. Lochorst,"Flemish, possibly Antwerp, active ca. 1575",,Lochorst D. G. V.,Flemish,1550,1600,ca. 1575,1550,1600,"Steel, leather, textile (velvet, wool)",H. as mounted 30 1/4 in. (36.8 cm); Wt. 12 lb. 3 oz. (5529 g); helmet (a): H. 15 1/2 in. (39.37 cm); Wt. 3 lb. 12 oz. (1695 g); breastplate (b): H. 18 1/2 in. (47.0 cm); W. 12 in. (30.5 cm); Wt. 4 lb. 9 oz. (2073 g); backplate (c): H. 15 in. (38.1 cm); W. 12 in. (30.5 cm); Wt. 3 lb. 14 oz. (1765 g),"Gift of Christian A. Zabriskie, 1938",,Antwerp,Antwerpen,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/24661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +09.210.2,false,true,22110,Arms and Armor,Basket-hilted sword,Basket-hilted Sword,"hilt, British; blade, German",,,,,Bladesmith,Blade by,Johannes Wundes the Younger,"Germany, Solingen, active mid-17th century",,"Wundes the Younger, Johannes",Germany,1625,1675,blade dated 1662,1662,1662,"Steel, wood, silver",L. 39 7/8 in. (101.3 cm); L. of blade 34 1/4 in. (87 cm); W. 5 in. (12.7 cm); Wt. 2 lb. 6 oz. (1077 g),"Rogers Fund, 1909",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.163,false,true,21965,Arms and Armor,Wheellock rifle,Wheellock Rifle,"Silesian, Cieszyn",,,,,Gunsmith,,Johannes Hartel,"Polish, Cieszyn (Silesian), active ca. 1650",,"Hartel, Johannes","Polish, Cieszyn (Silesian)",1625,1675,ca. 1650–60,1625,1685,"Steel, silver, wood (red beech), staghorn, mother of pearl",L. 42 3/4 in. (108.6 cm); Cal. 17/32 in. (13.5 mm); L. of barrel 30 7/8 in. (78.4 cm); L. of trigger 8 1 4/ in. (21 cm); Wt. 7 lb. 11 oz. (3500 g),"Rogers Fund, 1904",,Cieszyn,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/21965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.397,false,true,24930,Arms and Armor,Flintlock gun,Gun with Flintlock a la moda,"Spanish, Madrid",,,,,Gunsmith,,Gabriel de Algora,"Spanish, Madrid, documented 1733–died 1761",,"Algora, Gabriel de",Spanish,1733,1761,dated 1744,1744,1744,"Steel, gold, wood",L. of barrel 38 3/4 in. (98.4 cm),"Purchase, Gifts of George D. Pratt, Charles M. Schott Jr., and Bashford Dean, and Bashford Dean Memorial Collection, Funds from various donors, by exchange, 1987",,Madrid,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"27.160.12, .13",false,true,22985,Arms and Armor,Pair of pistols,Pair of Pistols with Flintlocks a la moda,"Spanish, Madrid",,,,,Gunsmith,,Gabriel de Algora,"Spanish, Madrid, documented 1733–died 1761",,"Algora, Gabriel de",Spanish,1733,1761,ca. 1735–40,1710,1765,"Steel, wood (walnut), gold, silver, horn",L. of each 19 in. (48.3 cm),"Gift of Archer M. Huntington, 1927",,Madrid,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/22985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1986.265.1, .2",true,true,24927,Arms and Armor,Pair of flintlock pistols,Pair of Flintlock Pistols of Empress Catherine the Great (1729–1796),"Russian, Saint Petersburg",,,,,Gunsmith,,Johan Adolph Grecke,"Russian, Saint Petersburg, recorded 1755–90",,Grecke Johan Adolph,Russian,1755,1790,1786,1786,1786,"Steel, ivory, gold, brass",L. of each 14 1/2 in. (36.8 cm),"Gift of John M. Schiff, in memory of Edith Baker Schiff, 1986",,Saint Petersburg,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/24927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.52,false,true,22775,Arms and Armor,Armet,Armet,Flemish,,,,,Armorer,Possibly by,Guillem Margot,"Flemish, active in Brussels, recorded 1505–20",,"Margot, Guillem",Flemish,1505,1520,ca. 1505,1480,1530,"Steel, copper alloy",H. 12 in. (30.5 cm); W. 8 7/16 in. (21.4 cm); D. 13 in. (33 cm); Wt. 9 lb. 4 oz. (4192 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.158.1b–d, g–m",false,true,23226,Arms and Armor,Composed armor,Composed Armor,"Italian, probably Milan",,,,,Armorer,Left elbow cop (h) marked by,Guillem Margot,"Flemish, active in Brussels, recorded 1505–20",,"Margot, Guillem",Flemish,1505,1520,ca. 1500–1520,1475,1545,"Steel, leather",Wt. 16 lb. 15 oz. (7682.7 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,probably Milan,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.14,true,true,24953,Arms and Armor,Short sword (Yatagan),Short Sword (Yatagan) from the Court of Süleyman the Magnificent (reigned 1520–66),"Turkish, Istanbul",,,,,Sword maker,Workshop of,Ahmed Tekelü,"possibly Iranian, active Istanbul, ca. 1520–30",,"Tekelü, Ahmed",possibly Iranian,1515,1530,ca. 1525–30,1500,1555,"Steel, gold, ivory (walrus), silver, turquoise, pearls, rubies",L. 23 3/8 in. (59.3 cm); L. of blade 18 3/8 in. (46.7 cm); Wt. 1 lb. 8 oz. (691 g),"Purchase, Lila Acheson Wallace Gift, 1993",,Istanbul,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.50.32,false,true,24701,Arms and Armor,Sallet,Sallet,"Austrian, Innsbruck or Mühlau",,,,,Armorer,,Kaspar Riederer,"Austrian, Innsbruck and Mühlau, active 1455–99",,"Riederer, Kaspar",Austrian,1455,1499,ca. 1480,1455,1505,"Steel, textile",H. 9 1/2 in. (24.13 cm); W. 9 in. (22.86 cm); D. 14 7/8 in. (37.77 cm); Wt. 6 lb. 8 oz. (2948 g),"Gift of Stephen V. Grancsay, 1942",,Innsbruck or Mühlau,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/24701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1661b,false,true,22400,Arms and Armor,Rondel,Rondel for a Shaffron (Horse's Head Defense),"Austrian, Innsbruck or Mühlau",,,,,Armorer,,Kaspar Riederer,"Austrian, Innsbruck and Mühlau, active 1455–99",,"Riederer, Kaspar",Austrian,1455,1499,ca. 1485–95,1460,1520,Steel,Diam. 7 1/2 in. (19 cm); Wt. 8 oz. (226 g),"Gift of William H. Riggs, 1913",,Innsbruck or Mühlau,,,,,,,,,,Equestrian Equipment-Shaffrons,,http://www.metmuseum.org/art/collection/search/22400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"48.26a, b",false,true,24785,Arms and Armor,Hunting sword,Hunting Sword,"Austrian, Vienna",,,,,Decorator|Decorator,,Emanuel Pioté|Jacob H. Köchert,"Austrian, Vienna 1781–1865|Austrian, Vienna 1795–1868",,"Pioté, Emanuel|Köchert, Jacob H.",Austrian|Austrian,1781 |1795,1865 |1868,ca. 1825,1800,1850,"Steel, gold, enamel, agate, wood, leather",L. 26 15/16 in. (68.5 cm),"Rogers Fund, 1948",,Vienna,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.150.6,false,true,23090,Arms and Armor,Half armor,Half Armor,"German, Augsburg",,,,,Armorer|Armorer,Helmet and reinforcing pieces attributed to,Kolman Helmschmid|Kolman Helmschmid,"German, Augsburg 1471–1532|German, Augsburg 1471–1532",,"Helmschmid, Kolman|Helmschmid, Kolman",German|German,1471 |1471,1532 |1532,ca. 1510–20 and later,1485,1545,Steel,"as mounted, H. 37 in. (94 cm)","Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,Augsburg,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/23090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.150.1d, e",false,true,23085,Arms and Armor,Pair of tassets,Pair of Tassets of Emperor Charles V of Austria (1500–1558),"German, Augsburg",,,,,Armorer|Armorer,or,Kolman Helmschmid|Desiderius Helmschmid,"German, Augsburg 1471–1532|German, Augsburg, 1513–1579",,"Helmschmid, Kolman|Helmschmid, Desiderius",German|German,1471 |1513,1532 |1579,ca. 1530–40,1505,1565,"Steel, gold",L. of each 8 3/4 in. (22.2 cm); W. of each 9 3/8 in. (23.8 cm); Wt. of right tasset 1 lb. 1 oz. (482 g); Wt. of left tasset 1 lb. 2 oz. (510 g),"Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,Augsburg,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/23085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.143a,false,true,35795,Arms and Armor,Armet,Armet,"German, Augsburg",,,,,Armorer|Armorer,Attributed to|Attributed to,Kolman Helmschmid|Desiderius Helmschmid,"German, Augsburg 1471–1532|German, Augsburg, 1513–1579",,"Helmschmid, Kolman|Helmschmid, Desiderius",German|German,1471 |1513,1532 |1579,"ca. 1525; bolts, modern",1500,1850,"Steel, gold",Wt. 8 lb. 8 oz. (3849 g),"Gift of Marshall Field, 1938",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/35795,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.135.66,false,true,22885,Arms and Armor,Burgonet,Burgonet,"German, Augsburg",,,,,Armorer|Decorator,Attributed to|Embossed decoration attributed to,Desiderius Helmschmid|Jörg Sigman,"German, Augsburg, 1513–1579|German, Augsburg, 1527–1601",,"Helmschmid, Desiderius|Sigman, Jörg",German|German,1513 |1527,1579 |1601,ca. 1550–55,1525,1580,Steel,H. 11 1/4 in. (28.6 cm); W. 8 3/4 in. (22.3 cm); D. 15 in. (38.1 cm); Wt. 4 lb. 11 oz. (2126 g),"Gift of William H. Riggs, 1913",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.233.2–.26,false,true,25106,Arms and Armor,Twenty-five inked impressions of engraved firearms ornament,"Twenty-Five Inked Impressions (or ""Pulls"") of Engraved Firearms Ornament",American and German,,,,,Decorator,,Gustave Young,"American (born Prussia), 1827–1895 Springfield, Massachusetts",,"Young, Gustave","American, born Prussia",1827,1895,ca. 1845–65,1845,1865,Ink on paper,"various sizes, largest: 1 5/8 x 6 3/4 in. (4.1 x 17.1 cm); smallest: 5/8 x 2 in. (1.6 x 5.1 cm)","Gift of Herbert G. Houze, 2002",,,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/25106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.206a–l,false,true,23016,Arms and Armor,Foot-combat armor,Foot-Combat Armor of Prince-Elector Christian I of Saxony (reigned 1586–91),"German, Augsburg",,,,,Armorer|Decorator,Decoration attributed to,Anton Peffenhauser|Jörg Sorg the Younger,"German, Augsburg, 1525–1603|German, Augsburg, ca. 1522–1603",,"Peffenhauser, Anton|Sorg the Younger, Jörg",German|German,1525 |1500,1603 |1625,1591,1591,1591,"Steel, gold, leather, copper alloy",H. 38 11/16 in. (98.2 cm); Wt. 46 lb. 3 oz. (20.96 kg); helmet Wt. 11 lb. 9 oz. (5245 g),"Gift of Henry Walters, 1927",,Augsburg,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/23016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.155.2,false,true,23207,Arms and Armor,Field and tournament armor,"Field and Tournament Armor of Johann Wilhelm (1530–1573), Duke of Saxe-Weimar","German, Augsburg",,,,,Etcher|Armorer,Etched decoration attributed to|Attributed to,Jörg Sorg the Younger|Anton Peffenhauser,"German, Augsburg, ca. 1522–1603|German, Augsburg, 1525–1603",,"Sorg the Younger, Jörg|Peffenhauser, Anton",German|German,1500 |1525,1625 |1603,ca. 1565,1540,1590,"Steel, gold, brass, textile, leather",Wt. 61 lb. 1 oz. (27.7 kg),"Bashford Dean Memorial Collection, Gift of Helen Fahnestock Hubbard, in memory of her father, Harris C. Fahnestock, 1929",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.901a, b",false,true,22339,Arms and Armor,Pair of gauntlets,Pair of Gauntlets from a Garniture of Armor of Philip II of Spain (reigned 1554–58),"German, Augsburg",,,,,Etcher|Armorer,,Ulrich Holzmann|Desiderius Helmschmid,"German, Augsburg, recorded 1535–62|German, Augsburg, 1513–1579",,"Holzmann, Ulrich|Helmschmid, Desiderius",German|German,1535 |1513,1562 |1579,1546,1546,1546,"Steel, leather, gold",Gauntlet (a); H. 10 in. (25.5 cm); W. 4 3/4 in. (12 cm); D. 5 in. (12.7 cm); Wt. 1 lb. 1 oz. (475 g); gauntlet (b); H. 10 in. (25.5 cm); W. 4 3/4 in. (12 cm); D. 5 in. (12.7 cm); Wt. 16 oz. (439 g),"Gift of William H. Riggs, 1913",,Augsburg,,,,,,,,,,Armor Parts-Gauntlets,,http://www.metmuseum.org/art/collection/search/22339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.125,false,true,23045,Arms and Armor,Wheellock rifle,Wheellock Rifle Made for Emperor Leopold I,"German, Schwäbisch Gmünd",,,,,Stock maker|Barrelsmith,,Johann Michael Maucher|I. C. Schefl,"German, Schwäbisch Gmünd, 1645–1701|German, Graz, 17th century",,"Maucher, Johann Michael|Schefl, I. C.",German|German,1670 |1601,1701 |1700,ca. 1685,1660,1710,"Steel, pearwood, ivory, mother-of-pearl",L. 43 1/2 in. (110.5 cm); L. of barrel 30 1/4 in. (76.8 cm); Cal. .50 in. (12.7 mm); L. of lock 8 1/16 in. (20.5 cm); L. of stock 43 9/16 in. (110.6 cm); Wt. 8 lb. 10 oz. (3912 g),"Fletcher Fund, 1928",,Schwäbisch Gmünd,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/23045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1200,false,true,22369,Arms and Armor,Rapier,Rapier,"probably French, Paris",,,,,Hilt Maker|Bladesmith,Hilt signed,Bouqueton|Johannes Wundes,"French, active early 17th century|German, Solingen, ca. 1560–1610",,"Bouqueton|Wundes, Johannes",French|German,1600 |1535,1650 |1635,ca. 1610–20,1585,1645,"Steel, silver, wood",L. 48 1/8 in. (122.2 cm); L. of blade 41 1/2 in. (105.4 cm); W. 9 1/2 in. (24.1 cm); Wt. 3 lb. 6 oz. (1530.9 g),"Gift of William H. Riggs, 1913",,probably Paris,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22369,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.143b–d,false,true,24660,Arms and Armor,Cuirass and tassets (torso and hip defense),Cuirass and Tassets (Torso and Hip Defense),"German, Augsburg",,,,,Armorer|Decorator,Attributed to|Etching attributed to,Kolman Helmschmid|Daniel Hopfer,"German, Augsburg 1471–1532|German, Kaufbeuren 1471–1536 Augsburg",,"Helmschmid, Kolman|Hopfer, Daniel",German|German,1471 |1471,1532 |1536,ca. 1510–20,1485,1545,"Steel, leather",H. 41 1/2 in. (105.4 cm); Wt. 19 lb. 8 oz. (8845 g),"Gift of Marshall Field, 1938",,Augsburg,,,,,,,,,,Armor,,http://www.metmuseum.org/art/collection/search/24660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.668,false,true,23339,Arms and Armor,Wheellock rifle,Wheellock Rifle,"German, Munich and Augsburg",,,,,Stock maker|Steel-chiseler,,Elias Becker|Caspar Spät,"German, Augsburg, recorded 1633–74|German, Munich, ca. 1611–1691",,"Becker, Elias|Spät, Caspar",German|German,1633 |1586,1674 |1716,dated 1668,1668,1668,"Steel, gold, wood (ebony), staghorn, bone",L. 30 9/16 in. (77.6 cm); L. of barrel 30 9/16 in. (77.6 cm); Cal. .55 in. (13.9 mm); Wt. 7 lb. 14 oz. (3572 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Munich and Augsburg,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/23339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.180,false,true,21968,Arms and Armor,Wheellock rifle,Wheellock Rifle,"German, Munich and Augsburg",,,,,Steel-chiseler|Stock maker,,Caspar Spät|Elias Becker,"German, Munich, ca. 1611–1691|German, Augsburg, recorded 1633–74",,"Spät, Caspar|Becker, Elias",German|German,1586 |1633,1716 |1674,ca. 1640–50,1615,1675,"Steel, gold, fruitwood, staghorn, bone",L. 41 7/8 in. (106.4 cm); L. of barrel 30 9/16 in. (77.6 cm); Cal. .46 in. (11.7 mm); Wt. 7 lb. 6 oz. (3350 g),"Rogers Fund, 1904",,Munich and Augsburg,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/21968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.135,false,true,22626,Arms and Armor,Miquelet flintlock gun,Miquelet Flintlock Gun Made for Charles IV of Spain (reigned 1788–1808),"Spanish, Eibar",,,,,Barrelsmith|Lock maker,,Antonio Guisasola|Juan Navaro,"Spanish, Eibar, recorded 1796–1833|Spanish, Eibar, active ca. 1800",,"Guisasola, Antonio|Navaro, Juan",Spanish|Spanish,1796 |1775,1833 |1825,dated 1796,1796,1796,"Steel, gold, silver, wood (walnut)",L. 48 1/2 in. (123.2 cm); L. of barrel 34 in. (86.4 cm); Cal. .68 in. (17.3 mm); Wt. 5 lb. 6 oz. (2438 g),"Rogers Fund, 1916",,Eibar,,,,,,,,,,Firearms-Guns-Miquelet,,http://www.metmuseum.org/art/collection/search/22626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.118,false,true,27568,Arms and Armor,Fencing book,"Gründtliche Beschreibung der freyen ritterlichen unnd adelichen Kunst des Fechtens in allerley gebreuchlichen Wehren mit vil schönen und nützlichen Figuren gezieret und fürgestellet (A Thorough Description of the Free Knightly and Noble Art of Fencing, in All the Typical Guards, Adorned and Arranged with Many Beautiful and Useful Figures)","German, Strasbourg",,,,,Author|Artist,Woodcuts by,Joachim Meyer|Tobias Stimmer,"German, active 16th–17th century|German, active 16th–17th century",,"Meyer, Joachim|Stimmer, Tobias",German|German,1550 |1550,1650 |1650,dated 1570,1570,1570,"Ink, paper",,"Gift of Christian A. Zabriskie, 1957",,Strassburg,,,,,,,,,,Books & Manuscripts,,http://www.metmuseum.org/art/collection/search/27568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"19.131.1a–r, t–w, .2a–f, l",false,true,22741,Arms and Armor,Armor garniture,"Armor Garniture, Probably of King Henry VIII of England (reigned 1509–47)","British, Greenwich",,,,,Designer|Armorer,Design of the decoration attributed to|Made in the,Hans Holbein the Younger|Royal Workshops at Greenwich,"German, Augsburg 1497/98–1543 London|British, Greenwich, 1511–1640s",,"Holbein, Hans, the Younger|Royal Workshops at Greenwich",German|British,1497 |1511,1543 |1650,dated 1527,1527,1527,"Steel, gold, leather, copper alloys",H. 73 in. (185.4 cm); Wt. 62 lb. 12 oz. (28.45 kg),"Purchase, William H. Riggs Gift and Rogers Fund, 1919",,Greenwich,,,,,,,,,,Armor for Horse and Man,,http://www.metmuseum.org/art/collection/search/22741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.77,true,true,24860,Arms and Armor,Rapier,Rapier of Prince-Elector Christian II of Saxony (1583–1611),"hilt, German, Dresden; blade, Spanish, Toledo",,,,,Sword cutler|Bladesmith,Hilt by|Blade by,Israel Schuech|Juan Martinez,"German, Dresden, active ca. 1590–1610|Spanish, Toledo, active ca. 1600",,"Schuech, Israel|Martinez, Juan",German|Spanish,1565 |1575,1635 |1625,dated 1606,1606,1606,"Steel, bronze, gold, enamel, paste jewels, cameos, pearls, wood",L. 48 in. (121.9 cm); L. of blade 41 1/4 in. (104.8 cm); W. 6 3/4 in. (17.2 cm); Wt. 3 lb. 4 oz. (1474 g),"Fletcher Fund, 1970",,Dresden|Toledo,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1397,false,true,22379,Arms and Armor,Wheellock rifle,Wheellock Rifle,"German, Munich",,,,,Steel-chiseler|Stock maker,,Daniel Sadeler|Hieronymus Borstorffer,"German, Munich, recorded 1602–1632|German, Munich, recorded 1597–1637",,"Sadeler, Daniel|Borstorffer, Hieronymus",German|German,1602 |1597,1632 |1637,ca. 1610–30,1585,1655,"Steel, gold, wood (ebony, fruitwood), ivory, horn",L. 42 3/8 in. (107.6 cm); L. of barrel 30 7/8 in. (78.4 cm); Cal. .56 in. (14.2 mm); Wt. 8 lb. 13 oz. (4000 g),"Gift of William H. Riggs, 1913",,Munich,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/22379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1396,false,true,22378,Arms and Armor,Wheellock rifle,Wheellock Rifle,"German, Munich",,,,,Steel-chiseler|Stock maker,,Daniel Sadeler|Hieronymus Borstorffer,"German, Munich, recorded 1602–1632|German, Munich, recorded 1597–1637",,"Sadeler, Daniel|Borstorffer, Hieronymus",German|German,1602 |1597,1632 |1637,ca. 1610–30,1585,1655,"Steel, gold, wood (fruitwood), ivory, horn",L. 43 5/8 in. (110.8 cm); L. of barrel 31 15/16 in. (81.1 cm); Cal..56 in. (14.2 mm); Wt. 7 lb. 10 oz. (3450 g),"Gift of William H. Riggs, 1913",,Munich,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/22378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.109.5,false,true,32216,Arms and Armor,Waist lame,Waist Lame,"German, Landshut",,,,,Armorer|Armorer,Attributed to|Attributed to,Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1555–60,1530,1585,Steel,H. 2 1/2 in. (6.35 cm); W. 13 in. (33.0 cm); Wt. 5 oz. (141.7 g),"Rogers Fund, 1932",,Landshut,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/32216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.109.6,false,true,32217,Arms and Armor,Waist lame,Waist Lame,"German, Landshut",,,,,Armorer|Armorer,Attributed to|Attributed to,Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1555–60,1530,1585,Steel,H. 2 1/2 in. (6.4 cm); W. 13 in. (33 cm); Wt. 3 oz. (85 g),"Rogers Fund, 1932",,Landshut,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/32217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"32.109.1a, b",false,true,32212,Arms and Armor,Right shoulder and arm defense,Right Shoulder and Arm Defense,"German, Landshut",,,,,Armorer|Armorer,Attributed to|Attributed to,Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1555–60,1530,1585,"Steel, leather, copper alloy",Shoulder defense (a); H. 8 1/2 in. (21.6 cm); W. 9 in. (22.9 cm); D. 8 3/4 in. (22.2 cm); Wt. 34.1 oz. (966.7 g); shoulder defense (a) and arm defense (b); H. approx. 28 in. (71.1 cm); W. approx. 10 in. (25.4 cm); Wt. 5 lb. 9 oz. (2523.1 g),"Rogers Fund, 1932",,Landshut,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/32212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"32.109.2a, b",false,true,32213,Arms and Armor,Left shoulder and arm defense,Left Shoulder and Arm Defense,"German, Landshut",,,,,Armorer|Armorer,Attributed to|Attributed to,Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1555–60,1530,1585,Steel,L. approx. 28 in. (71.1 cm); W. approx. 10 in. (25.4 cm); Wt. 5 lb. 6 oz. (2438.1 g),"Rogers Fund, 1932",,Landshut,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/32213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.109.1–.6,false,true,690153,Arms and Armor,Elements of an armor garniture,Elements of an Armor Garniture,"German, Landshut",,,,,Armorer|Armorer,Attributed to|Attributed to,Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1555–60,1530,1585,Steel,"right shoulder and arm defense (32.109.1a, b): H. approx. 28 in. (71.1 cm); W. approx. 10 in. (25.4 cm); Wt. 5 lb. 9 oz. (2523.1 g); left shoulder and arm defense (32.109.2a, b): L. approx. 28 in. (71.1 cm); W. approx. 10 in. (25.4 cm); Wt. 5 lb. 6 oz. (2438.1 g); right thigh and knee defense (32.109.3a–c): L. 20 in. (50.8 cm); W. 8 1/2 in. (21.6 cm); Wt. 3 lb. 3 oz. (1445.8 g); falling buffe (32.109.4): H. 7 1/2 in. (19.1 cm); W. 6 1/2 in. (16.5 cm); Wt. 1 lb. 1 oz. (481.9 g); waist lame (32.109.5): H. 2 1/2 in. (6.35 cm); W. 13 in. (33.0 cm); Wt. 5 oz. (141.7 g); waist lame (32.109.6): H. 2 1/2 in. (6.4 cm); W. 13 in. (33 cm); Wt. 3 oz. (85 g)","Rogers Fund, 1932",,Landshut,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/690153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.278,false,true,21998,Arms and Armor,Composed armor,Composed Armor,"German, Landshut and Augsburg; gorget, Italian",,,,,Armorer|Armorer,"Helmet, arm defenses, gauntlets and leg defenses by|Helmet, arm defenses, gauntlets and leg defenses by",Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1550–80,1525,1605,"Steel, gold, leather, textile, copper alloy",Wt. 51 lb. 4 oz. (23.25 kg),"Rogers Fund, 1904",,Landshut|Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/21998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.109.4,false,true,32215,Arms and Armor,Falling buffe for a helmet,Falling Buffe for a Helmet,"German, Landshut",,,,,Armorer|Armorer,Attributed to|Attributed to,Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1555–60,1530,1585,Steel,H. 7 1/2 in. (19.1 cm); W. 6 1/2 in. (16.5 cm); Wt. 1 lb. 1 oz. (481.9 g),"Rogers Fund, 1932",,Landshut,,,,,,,,,,Helmets Parts,,http://www.metmuseum.org/art/collection/search/32215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.109.3a–c,false,true,32214,Arms and Armor,Right thigh and knee defense,Right Thigh and Knee Defense,"German, Landshut",,,,,Armorer|Armorer,Attributed to|Attributed to,Wolfgang Grosschedel|Franz Grosschedel,"German, Landshut, active ca. 1517–62|German, Landshut, recorded 1555–79",,"Grosschedel, Wolfgang|Grosschedel, Franz",German|German,1517 |1555,1562 |1579,ca. 1555–60,1530,1585,Steel,L. 20 in. (50.8 cm); W. 8 1/2 in. (21.6 cm); Wt. 3 lb. 3 oz. (1445.8 g),"Rogers Fund, 1932",,Landshut,,,,,,,,,,Armor Parts-Knee Defenses,,http://www.metmuseum.org/art/collection/search/32214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"04.3.278; 22.147.6, .7",false,true,35774,Arms and Armor,Composed armor,Composed Armor,"German, Landshut and Augsburg; gorget, Italian",,,,,Armorer|Armorer,"Helmet, arm defenses, gauntlets and leg defenses by|Helmet, arm defenses, gauntlets and leg defenses by",Franz Grosschedel|Wolfgang Grosschedel,"German, Landshut, recorded 1555–79|German, Landshut, active ca. 1517–62",,"Grosschedel, Franz|Grosschedel, Wolfgang",German|German,1555 |1517,1579 |1562,ca. 1550–80,1525,1605,"Steel, gold, leather, textile, copper alloy",Wt. 51 lb. 4 oz. (23.25 kg),"Rogers Fund, 1904; gauntlets: Fletcher Fund, 1922",,Landshut|Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/35774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1425,true,true,22387,Arms and Armor,Double-barreled wheellock pistol,Double-Barreled Wheellock Pistol Made for Emperor Charles V (reigned 1519–56),"German, Munich",,,,,Etcher|Gunsmith,,Ambrosius Gemlich|Peter Peck,"German, Munich and Landshut, active ca. 1520–50|German, Munich, 1503–1596",,"Gemlich, Ambrosius|Peck, Peter",German|German,1495 |1503,1575 |1596,ca. 1540–45,1515,1570,"Steel, gold, wood (cherry), staghorn",L. 19 3/8 in. (49.2 cm); L. of upper barrel 10 in. (25.4 cm); L. of lower barrel 7 5/8 in. (19.4 cm); Cal. of each barrel .46 in. (11.7 mm); Wt. 5 lb. 10 oz. (2550 g),"Gift of William H. Riggs, 1913",,Munich,,,,,,,,,,Firearms-Pistols-Wheellock,,http://www.metmuseum.org/art/collection/search/22387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.1398a, b",false,true,22380,Arms and Armor,Pair of wheellock pistols,Pair of Wheellock Pistols Made for the Bodyguard of the Prince-Elector of Saxony,"German, Dresden",,,,,Stock maker|Gunsmith,,Hans Fleischer|Simon Helbig,"German, Dresden, recorded 1590–ca. 1625|German, Dresden, recorded 1609–1642",,"Fleischer, Hans|Helbig, Simon",German|German,1590 |1609,1650 |1642,ca. 1610,1585,1635,"Steel, gold, brass, staghorn, wood (beech)",L. of each pistol 29 1/8 in. (73.9 cm); L. of each barrel 8 3/4 in. (47.6 cm); Cal. of each pistol .57 in. (14.5 mm); Wt. of each pistol 4 lb. 5 oz. (1956 g),"Gift of William H. Riggs, 1913",,Dresden,,,,,,,,,,Firearms-Pistols-Wheellock,,http://www.metmuseum.org/art/collection/search/22380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.154.5,false,true,29594,Arms and Armor,Miquelet pistol,Miquelet Pistol,"Spanish, Barcelona",,,,,Gunsmith|Gunsmith,,Llorens Torrens|Jacinto Jaumeandreu,"Spanish, Catalonia, active 1793–1824|Spanish, Catalonia, active about 1750–1807",,"Torrens, Llorens|Jaumeandreu, Jacinto",Spanish|Spanish,1793 |1725,1824 |1832,ca. 1790–1807,1765,1832,"Steel, gold, wood (walnut)","Caliber, .665 in, (16.89 mm) Weight, 1 lb. 12 oz. (800 g) Length, 14 11/16 in. (37.31 cm) Length of barrel, 8 13/16 in. (22.38 cm)","Roger's Fund, 1937",,Barcelona,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/29594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.621,false,true,26505,Arms and Armor,Helmet,Close Helmet for a Boy,"German, Augsburg",,,,,Armorer|Armorer|Decorator,Possibly attributed to|Possibly attributed to|Etching in the style of,Kolman Helmschmid|Desiderius Helmschmid|Daniel Hopfer,"German, Augsburg 1471–1532|German, Augsburg, 1513–1579|German, Kaufbeuren 1471–1536 Augsburg",,"Helmschmid, Kolman|Helmschmid, Desiderius|Hopfer, Daniel",German|German|German,1471 |1513 |1471,1532 |1579 |1536,ca. 1530–40,1505,1565,"Steel, leather, copper alloy",H. 11 in. (27.9 cm); W. 7 3/4 in. (19.7 cm); D. 11 in. (27.9 cm); Wt. 4 lb. 4 oz. (1921 g),"Gift of William H. Riggs, 1913",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/26505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.218,false,true,21980,Arms and Armor,Reinforce for a helmet (Gupfe),Reinforce for a Helmet (Gupfe),"German, Augsburg",,,,,Armorer|Etcher|Armorer,,Mattheus Frauenpreis the Elder|Jörg Sorg the Younger|Mattheus Frauenpreis the Younger,"German, Augsburg, recorded 1510–49|German, Augsburg, ca. 1522–1603|German, Augsburg, 1530–1604",,"Frauenpreis the Elder, Mattheus|Sorg the Younger, Jörg|Frauenpreis the Younger, Mattheus",German|German|German,1529 |1500 |1530,1549 |1625 |1604,1549–50,1549,1550,"Steel, gold",H. 5 5/8 in. (14.3 cm); W. 8 11/16 in. (22.1 cm); D. 9 1/8 in. (23.2 cm); Wt. 1 lb. 5 oz. (595 g),"Rogers Fund, 1904",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/21980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.157.1,false,true,24854,Arms and Armor,Percussion revolver,"Colt Model 1849 Pocket Percussion Revolver, Serial no. 81015","American, Hartford, Connecticut",,,,,Engraver|Manufacturer,,Gustave Young|Samuel Colt,"American (born Prussia), 1827–1895 Springfield, Massachusetts|American, Hartford, Connecticut 1814–1862",,"Young, Gustave|Colt, Samuel","American, born Prussia|American",1827 |1814,1895 |1862,ca. 1853,1828,1878,"Steel, brass, silver, ivory",L. of barrel 6 in. (15.24 cm); Cal. .32 in. (8 mm),"Gift of John E. Parsons, 1968",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.481a–l,false,true,35690,Arms and Armor,Percussion revolver with case and accessories,"Colt Model 1851 Navy Percussion Revolver, Serial Number 29705, with Case and Accessories","American, Hartford, Connecticut",,,,,Engraver|Manufacturer,Engraved by,Gustave Young|Samuel Colt,"American (born Prussia), 1827–1895 Springfield, Massachusetts|American, Hartford, Connecticut 1814–1862",,"Young, Gustave|Colt, Samuel","American, born Prussia|American",1827 |1814,1895 |1862,ca. 1853–54,1828,1879,"Steel, brass, silver, wood, copper, tin, lead, paper",L. of pistol 13 in. (33 cm); L. of barrel 7 1/2 in. (19 cm); Cal. .36 in. (9 mm); case: 14 1/4 x 6 3/8 x 2 1/4 in. (36.2 x 16.2 x 5.6 cm),"Gift of Jack Sayre, 2010",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/35690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.50.46,false,true,24708,Arms and Armor,Smallsword hilt,Smallsword Hilt,"British, possibly Birmingham",,,,,Hilt Maker|Hilt Maker,,Matthew Boulton|Josiah Wedgwood,"British, Birmingham 1728–1809 Birmingham|British, Burslem, Stoke-on-Trent 1730–1795 Burslem, Stoke-on-Trent",,"Boulton, Matthew|Wedgwood, Josiah",British|British,1728 |1730,1809 |1795,ca. 1790,1765,1815,"Steel, Wedgwood jasperware",L. 6 7/8 in. (17.5 cm); W. 4 1/2 in. (11.4 cm),"Gift of Stephen V. Grancsay, 1942",,possibly Birmingham,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.264a–r,false,true,24921,Arms and Armor,Revolver with case and accessories,"Cased Colt Model 1862 Police Revolver, Serial no. 9174, with Thuer Conversion for Self-contained Cartridges, and Accessories","American, Hartford, Connecticut and New York",,,,,Manufacturer|Steel-chiseler|Maker,Decoration attributed to|Case made by,"Samuel Colt|Louis D. Nimschke|Schuyler, Hartley and Graham","American, Hartford, Connecticut 1814–1862|American, New York, active ca. 1850–1900|American, New York, 19th century",,"Colt, Samuel|Nimschke, Louis D.|Schuyler, Hartley and Graham",American|American|American,1814 |1825 |1801,1862 |1925 |1900,1862,1862,1862,"Steel, gold, silver, brass, wood (rosewood), textile",Cal. .36 in. (9 mm); case 15 x 9 in. (38 x 23 cm),"Gift of Mr. and Mrs. Jerry D. Berger, 1985",,Hartford|New York,Connecticut|New York,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.50.7a–n,false,true,24684,Arms and Armor,Double-barreled flintlock shotgun with exchangeable percussion locks and barrels,Double-Barreled Flintlock Shotgun with Exchangeable Percussion Locks and Barrels,"French, Versailles and Paris",,,,,Barrelsmith|Gunsmith|Lock maker|Retailer,Exchangeable percussion barrels by|Exchangeable percussion locks by|Exchangeable percussion locks and barrels originally sold by,Léopold Bernard|Nicolas Noël Boutet|B. Montagnon|G. Zaoué,"French, Paris, active 1832–70|French, Versailles and Paris, 1761–1833|French, active ca. 1850|French, active 1857–61",,"Bernard, Léopold|Boutet, Nicholas-Noël|Montagnon, B.|Zaoué, G.",French|French|French|French,1832 |1761 |1825 |1857,1870 |1833 |1875 |1861,"ca. 1818–20; exchangeable percussion locks and barrels, dated 1860",1793,1860,"Steel, gold, wood (walnut), silver, horn",L. of gun 47 3/8 in. (120.3 cm); L. of double barrel 31 7/8 in. (81.0 cm); L. of percussion locks 5 in. (12.7 cm); L. of bullet pouch 7 1/4 in. (18.4 cm); L. of priming flask 6 5/8 in. (16.8 cm); L. of bullet mold 6 3/4 in. (17.1 cm); L. of ramrod extension 10 3/8 in. (26.4 cm); L. of hammer extractor 2 3/16 in. (5.6 cm); L. of brush for cleaning barrel 2 13/16 in. (7.1 cm); L. of bullet extractor 1 5/16 in. (3.3 cm); L. of screwdriver 6 in. (15.2 cm); L. of bullet extractor 3 1/8 in. (7.9 cm); L. of touch hole cleaner 3 3/16 in. (8.1 cm); Cal. .61 in. (15.5 mm); Wt. of gun 6 lbs. 15 oz. (3150 g); Wt. of double barrel 3 lbs. 8 oz. (1600 g); Wt. of percussion locks 5 oz. (150 g); Wt. of bullet pouch 2.5 oz. (71 g); Wt. of priming flask 4.6 oz. (131 g); Wt. of bullet mold 7 oz. (198 g),"Gift of Stephen V. Grancsay, 1942",,Versailles and Paris,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.274,false,true,24929,Arms and Armor,Flintlock gun,Flintlock Gun,"French, Saint-Etienne",,,,,Steel-chiseler|Barrelsmith|Stock maker,,Louis Jaley|Nicolas Carteron|Joseph Blachon,"French, Saint-Étienne 1696–1773|French, Saint-Étienne, recorded ca. 1733–63|French, Saint-Étienne, recorded ca. 1725–35",,"Jaley, Louis|Carteron, Nicolas|Blachon Joseph",French|French|French,1696 |1708 |1700,1773 |1788 |1760,dated 1735,1735,1735,"Steel, gold, wood, silver",L. 57 1/2 in. (146.1 cm); L. of barrel 41 7/8 in. (106.5 cm); caliber .62 in. (15.5 cm),"Harris Brisbane Dick and Rogers Funds, 1987",,Saint-Etienne,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.1661b;29.150.5,.70,.80;29.156.66k...",false,true,35789,Arms and Armor,Composite armor,Composite Armor,"Austrian, Innsbruck and Mühlau",,,,,Armorer|Armorer|Armorer,Rondel by|Sallet by|Breastplate and backplate by,Kaspar Riederer|Jörg Wagner|Hans Prunner,"Austrian, Innsbruck and Mühlau, active 1455–99|Austrian, Innsbruck, recorded 1485–92|Austrian, Innsbruck, recorded 1482–99",,"Riederer, Kaspar|Wagner, Jörg|Prunner, Hans",Austrian|Austrian|Austrian,1455 |1485 |1482,1499 |1492 |1499,comprehensively ca. 1485–95,1460,1520,Steel,,"Rondel (14.25.1661b): Gift of William H. Riggs, 1913; sallet, backplate, breastplate (29.150.5a, .70, .80): Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928; couter (29.156.66k): Bashford Dean Memorial Collection, Gift of Edward S. Harkness, 1929; gauntlet (29.158.255b): Bashford Dean Memorial Collection, Funds from various donors, 1929",,Innsbruck and Mühlau,,,,,,,,,,Armor for Horse and Man,,http://www.metmuseum.org/art/collection/search/35789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.162,false,true,35833,Arms and Armor,Cup-hilted rapier,Cup-Hilted Rapier,"Italian, Milan",,,,,Hilt Maker|Bladesmith|Bladesmith,Attributed to|Attributed to,Francesco Maria Rivolta|Francisco Ruiz the Elder|Francisco Ruiz the Younger,"Italian, Milan, active second half of 17th century|Spanish, Toledo, died after 1617|Spanish, Toledo, active first half of 17th century",", or his son.|, or his father.","Rivolta, Francesco Maria|Ruiz the Elder, Francisco|Ruiz the Younger, Francisco","Italian, Milan|Spanish, Toledo|Spanish, Toledo",1650 |1550 |1600,1700 |1650 |1650,ca. 1670,1645,1695,"Steel, iron wire, wood, textile (felt)",L. 44 5/16 in. (112.5 cm); W. 10 1/8 in. (25.7 cm); D. 5 in. (12.7 cm); Wt. 1 lb 12 oz. (795 g),"Purchase, Arthur Ochs Sulzberger Gift, 2012",,Milan,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/35833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.486,false,true,35796,Arms and Armor,Engraving,"Two Armorial Trophies marked by the incorporation of a Broken Amphora on the Left, Plate 2 from the Libro de' Trofei","Italian, Rome",,,,,Artist|Former Attribution|Artist|Publisher,,"Anonymous, Italian|Enea Vico|Polidoro da Caravaggio|Antonio Lafreri","Italian, Parma 1523–1567 Ferrara|Italian, Caravaggio ca. 1499–ca. 1543 Messina|French, Orgelet, Franche-Comte ca. 1512–1577 Rome",", 16th century","Anonymous, Italian|Vico, Enea|Caravaggio, Polidoro da|Lafreri, Antonio",Italian|Italian|French,1400 |1523 |1494 |1507,2050 |1567 |1548 |1577,ca. 1550–53,1545,1555,Engraving,9 3/4 x 6 15/16 in. (24.8 x 17.6 cm),"Bequest of Stephen V. Grancsay, 1980",,Rome|Rome,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/35796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.171.1,false,true,24844,Arms and Armor,Percussion revolver,"Colt Walker Percussion Revolver, serial no. 1017","American, Whitneyville, Connecticut",,,,,Designer|Engraver|Designer|Manufacturer,,Samuel Colt|Waterman Lilly Ormsby|Captain Samuel Hamilton Walker|Eli Whitney Jr.,"American, Hartford, Connecticut 1814–1862|American, Hampton, Connecticut 1809–1883 Brooklyn, New York|American, died 1847|American, 1820–1888",,"Colt, Samuel|Ormsby, Waterman Lilly|Walker, Samuel Hamilton Captain|Whitney, Eli Jr.",American|American|American|American,1814 |1809 |1747 |1820,1862 |1883 |1847 |1888,1847,1847,1847,"Steel, brass, wood (walnut)",L. 15 1/2 in. (39.37 cm); L. of barrel 9 in. (22.86 cm); Cal. .44 in. (11 mm),"Gift of John E. Parsons, 1958",,Whitneyville,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"26.259.5, .6",false,true,22962,Arms and Armor,Pair of flintlock pistols,"Pair of Flintlock Pistols Made for Ferdinand IV, King of Naples and Sicily (1751–1825)","Italo-Spanish, Naples",,,,,Manufacturer|Gunsmith|Barrelsmith,,Royal Arms Manufactory at Torre Annunziata|Michele Battista|Emanuel Esteva,"Italian, Naples, established 1757|Spanish, active in Naples, Italy, recorded about 1760–90|Spanish, active in Naples, Italy, recorded about 1768–73",,"Royal Arms Manufactory at Torre Annunziata|Battista, Michele|Esteva, Emanuel",Italian|Spanish|Spanish,1757 |1735 |1743,1900 |1815 |1798,ca. 1768,1743,1793,"Steel, gold, wood (walnut), silver",L. of each 17 3/8 in. (44.1 cm); L. of each barrel 11 1/16 in. (28.1 cm); Cal. .63 in. (16.0 mm); Wt. of each 2 lb. 4 oz. (1021 g),"Gift of Henry Walters, 1926",,Naples,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/22962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"28.196.3, .4",false,true,23051,Arms and Armor,Pair of pistols,Pair of Pistols with Flintlocks a Las Tres Modas,"Spanish, Eibar",,,,,Gunsmith,,Workshop of the Ybarzabel family,"Spanish, Eibar, recorded 1784–1891",,Workshop of the Ybarzabel family,Spanish,1784,1891,late 18th century,1750,1800,"Steel, gold, wood (walnut)",L. of each 11 in. (27.9 cm); L. of each barrel 6 5/8 in. (16.8 cm); Cal. of each .61 in. (15.5 mm); Wt. of each 1 lb. 5 oz. (600 g),"Rogers Fund, 1928",,Eibar,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/23051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.53.87,false,true,22736,Arms and Armor,Powder flask,Powder Flask,"Spanish, Madrid",,,,,Maker,,Joseph Cano,"Spanish, Madrid, documented 1733–died 1751",,"Cano, Joseph",Spanish,1733,1751,ca. 1740–50,1715,1775,"Tortoiseshell, steel, gold",L. 9 1/2 in. (24.1 cm),"Gift of Charles M. Schott Jr., 1917",,Madrid,,,,,,,,,,Firearms Accessories-Flasks & Primers,,http://www.metmuseum.org/art/collection/search/22736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.36,true,true,21940,Arms and Armor,Crossbow,Crossbow of Count Ulrich V of Württemberg (1413–1480),"German, probably Stuttgart",,,,,Maker,Attributed to,Heinrich Heid von Winterthur,"probably Swiss, active Stuttgart, recorded 1453–1460",,"von Winterthur, Heinrich Heid",probably Swiss,1453,1460,dated 1460,1460,1460,"Wood (European hornbeam), horn, animal sinew, staghorn, birch bark, iron alloy, copper alloy, pigment",L. 28 1/16 in. (71.2 cm); W. 25 5/8 in. (65 cm); Wt. 6 lb. 9 oz. (2972 g),"Rogers Fund, 1904",,probably Stuttgart,,,,,,,,,,Archery Equipment-Crossbows,,http://www.metmuseum.org/art/collection/search/21940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.150,false,true,35654,Arms and Armor,Portrait,"Cosimo II de' Medici (1590–1621), Grand Duke of Tuscany",Flemish,,,,,Artist,Workshop of,Justus Sustermans,"Flemish, Antwerp 1597–1681 Florence",,"Sustermans, Justus",Flemish,1597,1681,1597–1681,1597,1681,"Oil on canvas, transferred from wood",78 x 48 in. (198.1 x 121.9 cm),"Gift of Bashford Dean, 1922",,,,,,,,,,,,Miscellaneous-Paintings & Portraits,,http://www.metmuseum.org/art/collection/search/35654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.590,false,true,35962,Arms and Armor,Album of bit designs,Album of Bit Designs,Bohemian,,,,,Designer,,Rudolf Franz Ferdinand von Talmberg,"Bohemian, ca. 1645–1702",,"Talmberg, Rudolf Franz Ferdinand von",Bohemian,1620,1727,1674,1674,1674,Ink on paper,Covers: 14 1/2 x 9 3/4 in. (36.8 x 24.8 cm); sheets: 14 1/4 x 9 1/2 in. (36.2 x 24.1 cm); plates: approx. 12 1/4 x 4 in. (31.5 x 10 cm),"Purchase, The Elisha Whittelsey Collection, The Elisha Whittelsey Fund; Joseph M. Scheuner and Kenneth and Vivian Lam Gifts; and Bequest of Stephen V. Grancsay, Rogers Fund, Helmut Nickel Gift, and funds from various donors, by exchange, 2013",,,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/35962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.261.14,false,true,27937,Arms and Armor,Presentation coin,Presentation Coin (Doppelguldiner) Showing Maximilian I (1459–1519),"Flemish, Antwerp; dies cut in Hall, Austria",,,,,Designer,,Ulrich Ursentaler,"Austrian, Hall, recorded 1508–35, Master of the Mint (Münz–Weister) at Hall in 1535",,Ursentaler Ulrich,Austrian,1508,1535,"minted, 1517; dies cut and dated, 1509",1509,1517,Silver,Diam. 2 1/8 in. (5.4 cm); thickness 1/8 in. (0.3 cm); Wt. 1.9 oz. (53.9 g),"Gift of George D. Pratt, 1926",,Antwerp|Hall,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/27937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.59,false,true,21945,Arms and Armor,Mace,Mace Made for Henry II of France,French,,,,,Damascener,,Diego de Çaias,"Spanish, recorded 1535–49",,Çaias Diego de,Spanish,1535,1549,ca. 1540,1515,1565,"Steel, gold, silver",L. 24 in. (60.9 cm); W. 4 1/2 in. (11.4 cm); Wt. 3 lb. 8 oz. (1588 g),"Rogers Fund, 1904",,,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/21945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.514.2,false,true,26835,Arms and Armor,Percussion target pistol,Percussion Exhibition Pistol,French,,,,,Gunsmith|Decorator|Barrelsmith,Signed by|Decorated by|Barrel by,Gilles Michel Louis Moutier-Le Page|Antoine Vechte|Léopold Bernard,"French, 1810–1887|French, 1799–1868|French, Paris, active 1832–70",,"Moutier-Le Page|Vechte, Antoine|Bernard, Léopold",French|French|French,1842 |1799 |1832,1865 |1868 |1870,dated 1851,1851,1851,"Steel, gold",L. 15 7/8 in. (40.3 cm); L. of barrel 10 1/4 in. (26.0 cm); Cal. .46 in. (11.7 mm),"Purchase, Ronald S. Lauder Gift, 2013",,Paris|London,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/26835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.514.1, .2",false,true,35866,Arms and Armor,Two percussion target pistols,Two Percussion Exhibition Pistols,French,,,,,Gunsmith|Decorator|Barrelsmith,Signed by|Decorated by|Barrels by,Gilles Michel Louis Moutier-Le Page|Antoine Vechte|Léopold Bernard,"French, 1810–1887|French, 1799–1868|French, Paris, active 1832–70",,"Moutier-Le Page|Vechte, Antoine|Bernard, Léopold",French|French|French,1842 |1799 |1832,1865 |1868 |1870,dated 1849 and 1851,1849,1851,"Steel, gold",2013.514.1: L. 16 in. (40.7 cm); L. of barrel 10 1/4 in. (26.1 cm); Cal. .45 in. (11.4 mm); 2013.514.2: L. 15 7/8 in. (40.3 cm); L. of barrel 10 1/4 in. (26.0 cm); Cal. .46 in. (11.7 mm),"Purchase, Ronald S. Lauder Gift, 2013",,,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/35866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.218,false,true,27558,Arms and Armor,Double-barreled shotgun,Double-barreled Shotgun,French,,,,,Barrelsmith|Gunsmith|Gunsmith,,Antoine Giraud|Nicolas Bouillet|Jean François Brunon,"French, active ca. 1760–80|French, Paris, recorded 1776–1800|French, St. Etienne, born 1737–recorded until 1784",,"Giraud, Antoine|Bouillet, Nicolas|Brunon, Jean François",French|French|French,1735 |1776 |1737,1805 |1800 |1784,"dated 1784, converted to percussion locks ca. 1840–50",1784,1875,"Steel, wood",L. of barrel 34 in. (86.4 cm); Cal. .58 in. (14.7 mm); Wt. 6 lb. 1 oz. (2750 g),"Rogers Fund, 1955",,,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/27558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.305.2,false,true,24951,Arms and Armor,Print,"Print of Designs for a Pommel, Quillons, and Locket",British,,,,,Engraver|Designer,After,Wenceslaus Hollar|Hans Holbein the Younger,"Bohemian, Prague 1607–1677 London|German, Augsburg 1497/98–1543 London",,"Hollar, Wenceslaus|Holbein, Hans, the Younger",Bohemian|German,1607 |1497,1677 |1543,1645,1645,1645,Ink on paper,6 1/8 x 4 1/2 in. (15.5 x 11.4 cm),"Gift of Lois Earl Blumka, 1992",,,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/24951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.99,false,true,679156,Arms and Armor,Drawing,"Design for the Decoration of Two Firearms Accessories, a Ladle and a Screwdriver",Spanish,,,,,Designer,,Eusebio Zuloaga,"Spanish, Madrid and Eibar 1808–1898",,"Zuloaga, Eusebio",Spanish,1808,1898,ca. 1850–51,1825,1876,"Pen, ink, and colored wash on paper",8 7/8 x 9 3/8 in. (22.5 x 24 cm),"Purchase, Arthur Ochs Sulzberger Bequest, 2015",,,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/679156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.100,false,true,679157,Arms and Armor,Drawing,Design for Two Sides of a Dagger Sheath,Spanish,,,,,Designer,,Eusebio Zuloaga,"Spanish, Madrid and Eibar 1808–1898",,"Zuloaga, Eusebio",Spanish,1808,1898,ca. 1850–55,1825,1880,"Pen, ink, and wash on paper",13 5/8 x 10 1/8 in. (34.5 x 25.7 cm),"Purchase, Arthur Ochs Sulzberger Bequest, 2015",,,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/679157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.101,false,true,679158,Arms and Armor,Drawing,Designs for the Decoration of a Percussion Pistol,Spanish,,,,,Designer,,Eusebio Zuloaga,"Spanish, Madrid and Eibar 1808–1898",,"Zuloaga, Eusebio",Spanish,1808,1898,ca. 1847,1822,1872,"Pen, ink, colored wash, and silver on paper",13 1/2 x 17 5/8 in. (34.5 x 44.8 cm),"Purchase, Arthur Ochs Sulzberger Bequest, 2015",,,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/679158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.35,false,true,35697,Arms and Armor,Mounting for a short sword (Wakizashi goshirae),Mounting for a Short Sword (Wakizashi Goshirae),Japanese,,,,,Lacquer worker|Fittings maker,,Shibata Zeshin|Tsuchiya Masayoshi Yasuchika,"Japanese, 1807–1891|Japanese, died 1860",,"Shibata Zeshin|Yasuchika, Tsuchiya Masayoshi",Japanese|Japanese,1807 |1760,1891 |1860,"mounting 19th century; grip ornaments 16th century; hilt collar (fuchi), dated 1849",1500,1900,"Wood, lacquer, ray skin (samé), thread, copper-gold alloy (shakudō), brass, iron",L. 26 1/8 in. (66.4 cm),"Purchase, The Howard Mansfield Collection, Gift of Howard Mansfield, by exchange, 2011",,,,,,,,,,,,Sword Fittings,,http://www.metmuseum.org/art/collection/search/35697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.101,false,true,24626,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker|Decorator,Inscribed by|Inscribed by,Kanō Natsuo|Toshiyoshi,"Japanese, 1828–1898|Japanese, recorded 1865–84",,"Natsuo, Kanō|Toshiyoshi",Japanese|Japanese,1828 |1865,1898 |1884,mid-19th century,1825,1875,"Copper-silver alloy (shibuichi), copper-gold alloy (shakudō), gold, silver, copper",H. 2 3/4 in. (7 cm); W. 2 3/8 in. (6 cm); thickness 3/16 in. (0.5 cm); Wt. 4.3 oz. (121.9 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/24626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.418a–c,false,true,24631,Arms and Armor,Blade and mounting for a short sword (Wakizashi),Blade and Mounting for a Short Sword (Wakizashi),Japanese,,,,,Swordsmith|Fittings maker,Blade attributed to|Mounting by,Yasumitsu|Iwamoto Konkan,"Japanese, Muromachi period, 15th century|Japanese, Edo period, 1744–1801",,Yasumitsu|Iwamoto Konkan,Japanese|Japanese,1400 |1744,1500 |1801,"blade, 15th century; mounting, 18th century",1401,1900,"Steel, wood, lacquer, ray skin (samé); thread, copper-gold alloy (shakudō), copper-silver alloy (shibuichi)",L. 27 1/16 in. (68.8 cm); L. of blade 24 5/16 in. (61.8 cm); L. of cutting edge 19 7/8 in. (50.5 cm); D. of curvature 9/16 in. (1.45 cm); L. of scabbard 19 3/4 in. (50.2 cm); knife (c); L. of blade 7 11/16 in. (19.5 cm); Wt. 0.4 oz. (11.3 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.120.417a, b",false,true,24630,Arms and Armor,Blade and mounting for a sword (Katana),Blade and Mounting for a Sword (Katana),Japanese,,,,,Swordsmith|Fittings maker,Blade inscribed by|Mounting by,Sukemitsu of Bizen|Iwamoto Konkan,"Japanese, Bizen, Muromachi period, active ca. 1440|Japanese, Edo period, 1744–1801",,Sukemitsu of Bizen|Iwamoto Konkan,Japanese|Japanese,1400 |1744,1500 |1801,"blade, dated 1440; mounting, 18th century",1440,1900,"Steel, wood, lacquer, ray skin (samé), thread, copper-gold alloy (shakudō), copper-silver alloy (shibuichi)",L. 36 1/2 in. (92.7 cm); L. of blade 30 11/16 in. (77.9 cm); L of cutting edge 24 13/16 in. (63.1 cm); D. of curvature 11/16 in. (1.8 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,Okayama Prefecture,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.100.172,true,true,22521,Arms and Armor,Armor (Gusoku),Armor (Gusoku),Japanese,,,,,Armorer|Armorer,"Helmet bowl signed|Breastplate inscribed inside,",Saotome Iyetada|Myōchin Munesuke,"Japanese, Edo period, active early–mid-19th century|Japanese, Edo period, 1688–1735",,"Iyetada, Saotome|Munesuke, Myōchin",Japanese|Japanese,1800 |1688,1875 |1735,16th and 18th centuries,1501,1800,"Iron, lacquer, silk, gilt copper",H. 67 1/2 in. (171.5 cm),"Gift of Bashford Dean, 1914",,,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.100.172; 14.100.527–.528,false,true,35721,Arms and Armor,Armor (Gusoku),Armor (Gusoku),Japanese,,,,,Armorer|Armorer,"Helmet bowl signed|Breastplate inscribed inside,",Saotome Iyetada|Myōchin Munesuke,"Japanese, Edo period, active early–mid-19th century|Japanese, Edo period, 1688–1735",,"Iyetada, Saotome|Munesuke, Myōchin",Japanese|Japanese,1800 |1688,1875 |1735,16th and 18th centuries,1501,1800,"Iron, lacquer, silk, gilt copper",H. 67 1/2 in. (171.5 cm); L. of each foot defense 9 3/4 in. (24.8 cm); W. of each foot defense 4 1/4 in. (10.8 cm),"Gift of Bashford Dean, 1914",,,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/35721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.2.84,false,true,21913,Arms and Armor,Blade and mounting for a short sword (Wakizashi),Blade and Mounting for a Short Sword (Wakizashi),Japanese,,,,,Swordsmith|Decorator|Fittings maker,Blade inscribed by|Engraving on blade attributed to|Sword guard (Tsuba) inscribed by,Naotane Taikei|Honjo Yoshitane|Yukinaka,"Japanese, Yamagata 1778–1857 Edo|Japanese, Edo period, 19th century|Japanese, Hagi, active 19th century",,"Naotane Taikei|Yoshitane, Honjo|Yukinaka",Japanese|Japanese|Japanese,1778 |1801 |1801,1857 |1900 |1900,"blade, dated 1839; mounting, early–mid 19th century",1801,1900,"Steel, wood, lacquer, rayskin (samé), thread, iron, copper-silver alloy (shibuichi), gold, silver",L. 26 3/4 in. (67.9 cm); L. of blade 23 3/8 in. (59.4 cm); L. of cutting edge 17 27/32 in. (45.3 cm); D. of curvature 9/64 in. (0.36 cm),"Gift of Brayton Ives and W. T. Walters, 1891",,Hagi,Nagato Province (Yamaguchi Prefecture),,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/21913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"91.2.56, .84",false,true,638804,Arms and Armor,Blades and mountings for a pair of swords (Daishō),Blades and Mountings for a Pair of Swords (Daishō),Japanese,,,,,Swordsmith|Decorator|Fittings maker,Blades inscribed by|Engraving on blades attributed to|Sword guards (Tsuba) inscribed by,Naotane Taikei|Honjo Yoshitane|Yukinaka,"Japanese, Yamagata 1778–1857 Edo|Japanese, Edo period, 19th century|Japanese, Hagi, active 19th century",,"Naotane Taikei|Yoshitane, Honjo|Yukinaka",Japanese|Japanese|Japanese,1778 |1801 |1801,1857 |1900 |1900,"blades, dated 1839; mountings, 19th century",1801,1900,"Steel, wood, lacquer, rayskin, silk, iron, copper-gold alloy (shakudō), copper, gold, silver, copper-silver alloy (shibuichi)",L. of sword (katana) 39 1/4 in. (99.8 cm); L. of sword (katana) blade 36 31/32 in. (93.9 cm); L. of sword (katana) blade cutting edge 28 5/8 in. (72.7 cm); D. of sword (katana) blade curvature 1 7/32 in. (3.1 cm); L. of short sword (wakizashi) 26 3/4 in. (67.9 cm); L. of short sword (wakizashi) blade 17 27/32 in. (45.3 cm); D. of short sword (wakizashi) blade curvature 9/64 in. (0.36 cm),"Gift of Brayton Ives and W. T. Walters, 1891",,Hagi,Nagato Province (Yamaguchi Prefecture),,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/638804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.120.417a, b, .418a–c",true,true,27789,Arms and Armor,Blades and mountings for a pair of swords (Daishō),Blades and Mountings for a Pair of Swords (Daishō),Japanese,,,,,Swordsmith|Swordsmith|Fittings maker,Long sword (katana) blade inscribed by|Short sword (wakizashi) blade attributed to|Set of sword mountings by,Sukemitsu of Bizen|Yasumitsu|Iwamoto Konkan,"Japanese, Bizen, Muromachi period, active ca. 1440|Japanese, Muromachi period, 15th century|Japanese, Edo period, 1744–1801",,Sukemitsu of Bizen|Yasumitsu|Iwamoto Konkan,Japanese|Japanese|Japanese,1400 |1400 |1744,1500 |1500 |1801,"sword (katana) blade, dated 1440; short sword (wakizashi) blade, 15th century; mountings, late 18th century",1401,1900,"Steel, wood, lacquer, copper-silver alloy (shibuichi), gold, copper, rayskin, silk",L. of sword (katana) 34 1/8 in. (86.7 cm); L. of sword (katana) blade 25 1/4 in. (64.1 cm); L. of sword (katana) scabbard 28 1/4 in. (71.8 cm); L. of short sword (wakizashi) 26 in. (66.0 cm); L. of short sword (wakizashi) blade 20 3/4 in. (52.7 cm); L. of short sword (wakizashi) scabbard 19 3/4 in. (50.2 cm); L. of knife (kozuka) for short sword (wakizashi) mounting 8 3/4 in. (22.2 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/27789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.52,false,true,26558,Arms and Armor,Cover for a smallsword hilt,Cover for a Smallsword Hilt,"French, Paris",,,,,Sword maker,,Guillaume Pagés,"French, Paris, recorded 1709–56",,Pagés Guillaume,French (Paris),1709,1756,ca. 1725–50,1700,1775,"Leather, ink",L. 8 1/4 in. (20.9 cm); W. 6 7/8 in. (17.5 cm),"Purchase, Rogers Fund, by exchange, 1995",,Paris,,,,,,,,,,Miscellaneous,,http://www.metmuseum.org/art/collection/search/26558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.514.1,false,true,26893,Arms and Armor,Percussion target pistol,Percussion Exhibition Pistol,"French, Paris",,,,,Gunsmith|Decorator|Barrelsmith,Signed by|Decorated by|Barrel by,Gilles Michel Louis Moutier-Le Page|Antoine Vechte|Léopold Bernard,"French, 1810–1887|French, 1799–1868|French, Paris, active 1832–70",,"Moutier-Le Page|Vechte, Antoine|Bernard, Léopold",French|French|French,1842 |1799 |1832,1865 |1868 |1870,dated 1849,1849,1849,"Steel, gold",L. 16 in. (40.7 cm); L. of barrel 10 1/4 in. (26.1 cm); Cal. .45 in. (11.4 mm),"Purchase, Ronald S. Lauder Gift, 2013",,Paris,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/26893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.512.1, .2",false,true,26980,Arms and Armor,Pair of percussion target pistols,Pair of Percussion Target Pistols Made for Display at the 1844 Exposition des Produits de l'Industrie in Paris,"French, Paris",,,,,Gunsmith|Designer|Barrelsmith,Signed by|Designed by|Barrels by,Alfred Gauvain|Michel Liénard|Léopold Bernard,"French, Paris 1801–1889 Paris|French, La Bouille 1810–1870 Brussels|French, Paris, active 1832–70",,"Gauvain, Alfred|Liénard, Michel|Bernard, Léopold",French|French|French,1801 |1810 |1832,1889 |1870 |1870,dated 1844,1844,1844,"Steel, wood (ebony)",L. of each pistol 16 5/8 in. (42.3 cm); L. of each barrel 11 in. (27.8 cm); Cal. of each barrel .50 in. (13 mm),"Purchase, Arthur Ochs Sulzberger Bequest, 2013",,Paris,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/26980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.513.1, .2",false,true,26981,Arms and Armor,Pair of percussion target pistols,"Pair of Percussion Target Pistols Made for Display at the Crystal Palace Exhibition in London, 1851","French, Paris",,,,,Gunsmith|Designer|Barrelsmith,Signed by|Designed by|Barrels by,Alfred Gauvain|Michel Liénard|Léopold Bernard,"French, Paris 1801–1889 Paris|French, La Bouille 1810–1870 Brussels|French, Paris, active 1832–70",,"Gauvain, Alfred|Liénard, Michel|Bernard, Léopold",French|French|French,1801 |1810 |1832,1889 |1870 |1870,dated 1851,1851,1851,"Steel, wood (ebony), gold",L. of each pistol 17 1/4 in. (44 cm); L. of each barrel 11 3/8 in. (28.8 cm); Cal. of each barrel .46 in. (12 mm),"Purchase, Arthur Ochs Sulzberger Bequest, 2013",,Paris,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/26981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.310.1–.13,false,true,35701,Arms and Armor,Pattern book of firearms ornament,Plusievrs Pieces et Ornements Darquebuzerie (4th extended edition),"French, Paris",,,,,Engraver|Publisher|Engraver|Engraver,Initial eight-plate edition by|Editions with eight and thirdteen plates issued by|Designs by|Expanded edition produced by,Claude Simonin|Laurent le Languedoc|De Lacollombe|Gilles-Antoine Demarteau,"French, Nantes ca. 1635–1693 Nantes|French, Paris, active ca. 1705|French, Paris, active ca. 1702–ca. 1736|French, Paris, 1756–1802",", published in 1684 and reissued in1685|, included in the 1705 edition","Simonin, Claude|Languedoc, Laurent le|Lacollombe, De|Demarteau, Gilles-Antoine",French|French|French|French,1635 |1680 |1702 |1756,1693 |1730 |1736 |1802,ca. 1776,1751,1801,"Ink, paper",9 1/2 x 12 3/4 in. (24.1 x 32.4 cm) to 9 1/4 x 14 in. (23.5 x 35.6 cm),"Purchase, Kenneth and Vivian Lam Gift, 2011",,Paris,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/35701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.415,true,true,24957,Arms and Armor,Double-barrel breech-loading pinfire shotgun,Double-Barrel Breech-Loading Pinfire Shotgun,"French, Paris",,,,,Barrelsmith|Gunsmith|Engraver|Goldsmith|Goldsmith,Designed and steel chiseled by|Designed and steel chiseled by,Léopold Bernard|J. C. A. Brun|Jean-Claude Tissot|François-Auguste Fannière|François-Joseph-Louis Fannière,"French, Paris, active 1832–70|French, Paris, active 1849–72|French, Paris, 1811–1889|French, Paris, 1818–1900|French, Paris, 1822–1897",,"Bernard, Léopold|Brun, J. C. A.|Tissot, Jean-Claude|Fannière, François-Auguste|Fannière, François-Joseph-Louis",French|French|French|French|French,1832 |1849 |1811 |1818 |1822,1870 |1872 |1889 |1900 |1897,dated 1866,1866,1866,"Steel, wood (walnut), gold",L. 44 1/8 in. (112 cm),"Purchase, The Sulzberger Foundation Inc. Gift and Rogers Fund; Bashford Dean Memorial Collection, Funds from various donors, Gift of William H. Riggs, The Collection of Giovanni P. Morosini, presented by his daughter Giulia, and Gift of Charles M. Schott Jr., by exchange; and gifts and funds from various donors, 1993",,Paris,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/24957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.363.1–.3,false,true,24888,Arms and Armor,Smallsword,Smallsword,"British, London",,,,,Enameler|Goldsmith,"Signed AT, probably for|Attributed to",Simon Augustin Toussaint|James Morisset,"British, London, active 1768–85|English, London, active 1768–1800",,"Toussaint, Simon Augustin|Morisset James",British|British,1768 |1768,1785 |1800,ca. 1780–85,1755,1810,"Gold, enamel, steel",Sword (a); L. 39 in. (99.1 cm); box (c); L. 41 in. (104.1 cm); W. 6 3/16 in. (15.7 cm); D. 5 1/4 in. (13.3 cm),"Gift of Dr. and Mrs. John C. Weber, 1981",,London,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1992.330.1, .2",false,true,24952,Arms and Armor,Pair of flintlock pistols,Pair of Flintlock Pistols,"British, London",,,,,Gunsmith|Silversmith,Attributed to,Samuel Brunn|Michael Barnett,"English, London, recorded 1795–1820|English, London, 1758–ca. 1823",,"Brunn, Samuel|Barnett, Michael",British|British,1795 |1758,1820 |1850,hallmarked for 1800–1801,1800,1801,"Steel, wood (walnut), silver, gold",L. of each pistol 16 in. (40.6 cm); L. of each barrel 10 1/8 in. (25.7 cm); Cal. of each pistol .603 in. (15.3 mm),"Purchase, Harris Brisbane Dick Fund and Gift of George D. Pratt, by exchange, 1992",,London,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.305.1,false,true,24950,Arms and Armor,Print,Print of Designs for Hilt and Sheath Fittings,"British, London",,,,,Engraver|Designer,After,Wenceslaus Hollar|Hans Holbein the Younger,"Bohemian, Prague 1607–1677 London|German, Augsburg 1497/98–1543 London",,"Hollar, Wenceslaus|Holbein, Hans, the Younger",Bohemian|German,1607 |1497,1677 |1543,1644,1644,1644,Ink on paper,5 7/8 x 3 3/4 in. (14.9 x 9.5 cm),"Gift of Lois Earl Blumka, 1992",,London,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/24950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.361–.362,false,true,35702,Arms and Armor,Pair of miquelet flintlock pistols,Pair of Miquelet Flintlock Pistols,"Colonial Spanish, probably Mexico",,,,,Gunsmith,Signed by,Francisco Pintan,active mid-18th century,,"Pintan, Francisco",,1725,1775,dated 1757,1757,1757,"Steel, wood (family leguminosae), silver",L. of 2011.361: 10 7/8 in. (27.5 cm); L. of barrel of 2011.361: 6 in. (15.2 cm); L. of 2011.362: 10 5/8 in. (27.2 cm); L. of barrel of 2011.362: 6 in. (15.2 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2011",,,,,,,,,,,,Firearms-Guns-Miquelet,,http://www.metmuseum.org/art/collection/search/35702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.282,false,true,650965,Arms and Armor,Close helmet,Close Helmet,"Italian, Milan",,,,,Armorer,,Gian Giacomo Negroli,"Italian, Milan 1463–1543",,"Negroli, Gian Giacomo",,1463,1543,ca. 1510–20,1485,1545,"Steel, gold",H. 13 1/8 in. (33.3 cm); W. 8 1/2 in. (21.6 cm); D. 12 in. (30.5 cm); Wt. 3 lb. 6 oz. (1530 g),"Purchase, Arthur Ochs Sulzberger Gift, 2014",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/650965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.38,false,true,26533,Arms and Armor,Close-helmet,Close-Helmet,"German, Nuremberg",,,,,Armorer,Attributed to,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",,1510,1567,1550,1525,1575,"Steel, leather",H. 12 3/4 (32.4 cm); W. 8 1/8 in. (20.6 cm); D. 13 in. (33 cm); Wt. 6 lb. 9 oz. (2975 g),"Gift of Mrs. Theodore Offerman, in memory of her husband, 1939",,Nuremberg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/26533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.130.5a–m,false,true,23937,Arms and Armor,Armor,"Armor of Henry Herbert (1534–1601), Second Earl of Pembroke","British, Greenwich",,,,,Armorer,Made in the,Royal Workshops at Greenwich,"British, Greenwich, 1511–1640s",,Royal Workshops at Greenwich,,1511,1650,ca. 1585–86,1560,1611,"Steel, gold",Wt. 60 lb. 1 oz. (27.24 kg),"Rogers Fund, 1932",,Greenwich,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1191,false,true,662414,Arms and Armor,Rapier,Rapier,"hilt, Italian; blade, German, Solingen",,,,,Bladesmith,Blade by,Clemens Hartkopf,"German, Solingen, active ca. 1625",,"Hartkopf, Clemens",,1600,1650,ca. 1625,1600,1650,"Steel, silver, wood, copper alloy",L. 49 1/2 in. (125.7 cm); L. of blade 43 in. (109.2 cm); W. 8 1/2 in. (21.6 cm); D. 5 in. (12.7 cm); Wt. 2 lb. 10 oz. (1190.7 g),"Gift of William H. Riggs, 1913",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/662414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.446a–d,false,true,679294,Arms and Armor,Wheellock rifle with spanner and accessories,"Wheellock Rifle with Spanner, Shot Extracting Tool, and Shooting Patch","German, Augsburg",,,,,Gunsmith,,Martin Kammerer,"German, Augsburg, active 1654–67",,"Kammerer, Martin",,1654,1667,ca. 1665,1640,1690,"Steel, iron, gold, wood, antler, copper alloy, enamel, bone, textile",rifle (2015.446a): L. 43 1/2 in. (110.5 cm); L. of barrel 32 1/2 in. (82.6 cm); W. 8 1/4 in. (21 cm); D. 4 1/4 in. (10.8 cm); Wt. 9 lb. 7 oz. (4280.8 g); spanner (2015.446b): L. 7 3/4 in. (19.7 cm); W. 7/8 in. (2.2 cm); D. 1 9/16 in. (4 cm); Wt. 4 oz. (113.4 g); shot extracting tool (2015.446c): L. 1 13/16 in. (4.6 cm); Diam. 3/8 in. (1 cm); Wt. 0.5 oz. (14.2 g); shooting patch (2015.446d): Diam. approx. 1 3/8 in. (3.5 cm),"Purchase, Arthur Ochs Sulzberger Bequest, 2015",,Augsburg,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/679294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"32.130.4a, b",false,true,23938,Arms and Armor,Rapier,Rapier of Ambrogio Spinola (1569–1630),"Northern European, possibly France",,,,,Hilt Maker,Hilt inscribed,M. I. F.,"northern European, active ca. 1600",,M. I. F.,,1575,1625,ca. 1600,1575,1625,Steel,L. 46 1/8 in. (117.1 cm); L. of blade 39 3/4 in. (101 cm),"Rogers Fund, 1932",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/23938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1052,false,true,27437,Arms and Armor,Rapier,Rapier,"hilt, probably German; blade, Italian, Milan",,,,,Bladesmith,Blade by,Pietro Caino,"Italian, Milan, active second half 16th century",,"Caino, Pietro",,1550,1600,late 16th century,1550,1600,"Steel, wood, iron",L. 45 5/8 in. (115.9 cm); L. of blade 40 1/2 in. (102.9 cm); W. 7 in. (17.8 cm); D. 5 in. (12.7 cm); Wt. 2 lb. 9 oz. (1162.3 g),"Gift of William H. Riggs, 1913",,Milan,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/27437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.131.2g,false,true,734140,Arms and Armor,Saddle cloth,"Saddle Cloth Associated with Armor Garniture, Probably of King Henry VIII of England (reigned 1509–47)","British, Greenwich",,,,,Armorer|Designer,Made in the|Design of the decoration attributed to,Royal Workshops at Greenwich|Hans Holbein the Younger,"British, Greenwich, 1511–1640s|German, Augsburg 1497/98–1543 London",,"Royal Workshops at Greenwich|Holbein, Hans, the Younger",,1511 |1497,1650 |1543,dated 1527,1527,1527,Textile,,"Purchase, William H. Riggs Gift and Rogers Fund, 1919",,Greenwich,,,,,,,,,,Equestrian Equipment-Saddles,,http://www.metmuseum.org/art/collection/search/734140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"22.147.4a, b",false,true,22851,Arms and Armor,Right thigh and knee defense (cuisse and poleyn),Right Thigh and Knee Defense (Cuisse and Poleyn) for the Armor of Sir John Scudamore (1541 or 1542–1623),"British, Greenwich",,,,,Armorer,Made under the direction of,Jacob Halder,"British, master armorer at the royal workshops at Greenwich, documented in England 1558–1608",,"Halder, Jacob",,1558,1608,ca. 1587,1562,1612,"Steel, gold, leather",22.147.4a: H. 4 1/2 in. (11.43 cm); 22.147.4b: H. 7 3/4 in. (19.69 cm); W. 5 in. (12.7 cm),"Fletcher Fund, 1922",,Greenwich,,,,,,,,,,Armor Parts-Knee Defenses,,http://www.metmuseum.org/art/collection/search/22851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.121a–n,true,true,24671,Arms and Armor,Armor,"Armor of Henry II, King of France (reigned 1547–59)","French, possibly Paris",,,,,Designer|Designer|Designer,Part of the decoration design by|Part of the decoration design possibly by|Part of the decoration design possibly by,Jean Cousin the Elder|Étienne Delaune|Baptiste Pellerin,"French, Souci (?) ca. 1490–ca. 1560 Paris (?)|French, Orléans 1518/19–1583 Strasbourg|French, documented in Étampes 1542–75 Paris",,"Cousin, Jean, the Elder|Delaune, Étienne|Pellerin, Baptiste",,1485 |1518 |1542,1565 |1583 |1575,ca. 1555,1530,1580,"Steel, gold, silver, leather, textile",H. 74 in. (187.96 cm); Wt. 53 lb. 4 oz. (24.20 kg),"Harris Brisbane Dick Fund, 1939",,possibly Paris,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/24671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.295a, b",false,true,681022,Arms and Armor,Cuirass,Cuirass,"French, Klingenthal, Alsace",,,,,Designer|Decorator|Manufactory,Designed by|Etched and gilt by|Manufactured at,"François-Joseph Bisch|François-Xavier Bisch|Coulaux Frères, Manufacture Royale d'Armes de Klingenthal","French, Klingenthal, Alsace 1756–1831|French, active in Klingenthal and Boersch, Alsace 1793–1841|French, Klingenthal, Alsace 1801–1836",,"Bisch, François-Joseph|Bisch, François-Xavier|Coulaux Frères, Manufacture Royale d'Armes de Klingenthal",,1756 |1793 |1801,1831 |1841 |1836,ca. 1825,1820,1830,"Steel, gold, copper alloy, leather",H. 17 1/2 in. (44.5 cm); W. 14 1/8 in. (35.9 cm); D. 12 3/4 in. (32.4 cm); Wt. 12 lb. 2.2 oz. (5505.5 g); breastplate: H. 17 1/2 in. (44.5 cm); W. 14 1/8 in. (35.9 cm); D. 7 in. (17.8 cm); Wt. 8 lb. 0.5 oz. (3642.9 g); backplate: H. 16 15/16 in. (43 cm); W. 14 1/8 in. (35.9 cm); D. 5 3/4 in. (14.6 cm); Wt. 4 lb. 1.7 oz. (1862.6 g),"Purchase, Arthur Ochs Sulzberger Bequest, 2015",,Klingenthal,Alsace,,,,,,,,,Armor Parts-Cuirasses,,http://www.metmuseum.org/art/collection/search/681022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.649,false,true,717696,Arms and Armor,Album of photographs,"Album of Photographs of Views of the Interior of the Ottoman Military Museum in the Former Church of St. Irene, Constantinople",Turkish,,,,,Photographer,,Abdullah Frères,"Ottoman, 1858–1899",,Abdullah Frères,,1858,1899,1891,1891,1891,"Albumen prints, paper, leather, textile, gold",L. 17 3/4 in. (45.1 cm); H. 13 in. (33.0 cm); W. 2 3/8 in. (6.0 cm),"Gift of Howard Ricketts in Memory of Roy and Neil Cole, Collectors, 2016",,,,,,,,,,,,Photographs,,http://www.metmuseum.org/art/collection/search/717696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.122.48,false,true,32997,Arms and Armor,Medal,Medal Showing Prince Charles and Prince Henry,Scottish,,,,,Artist,,Ottone Hamerani,"Italian, Rome 1694–1768",,Hamerani Ottone,,1694,1768,1729,1729,1729,Bronze,Diam. 1 13/16 in. (4.6 cm); thickness 1/4 in. (0.6 cm); Wt. 1.8 oz. (51 g),"Gift of Bashford Dean, 1922",,,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/32997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.122.60,false,true,33009,Arms and Armor,Medal,"Medal Showing Charles III of Spain, 1778","Spanish, Mexico",,,,,Artist,,Jerónimo Antonio Gil,"Spanish, Zamora 1731–1798 Mexico",,"Gil, Jerónimo Antonio",,1732,1798,1778,1778,1778,Bronze,Diam. 2 5/16 in. (5.9 cm); thickness 1/4 in. (0.6 cm); Wt. 3.9 oz. (110.6 g),"Gift of Bashford Dean, 1922",,,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/33009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2016.205a, b",false,true,712927,Arms and Armor,Pair of stirrups (Abumi),Pair of Stirrups (Abumi),"Japanese, Kashu",,,,,Artist,Inscribed by,Sanemitsu,"Japanese, active in Ka shū, possibly early 18th century",,Sanemitsu,,1675,1775,probably 18th century,1675,1825,"Iron, wood, silver, lacquer, paper",left: L. 10.7 in. (27.2 cm); W. 5.03 in. (12.8 cm); H. 9.68 in. (24.6 cm); right: L. 10.86 in. (27.6 cm); W. 5.03 in. (12.8 cm); H. 9.76 in. (24.8 cm),"Purchase, Gift of Morihiro and Sumiko Ogawa, in memory of Charles Baber, 2016",,Kanazawa City,Ishikawa Prefecture,,,,,,,,,Equestrian Equipment-Stirrups,,http://www.metmuseum.org/art/collection/search/712927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1573,false,true,22397,Arms and Armor,Pellet and bolt crossbow combined with a wheel-lock gun,Pellet and Bolt Crossbow Combined with a Wheel-Lock Gun,Central European; possibly Southern German or Austrian,,,,,Designer,Decoration based on designs by,Jacob Floris,1524–1581,", published in Antwerp in 1564.","Floris, Jacob",,1524,1581,ca. 1570–1600,1545,1625,"Steel, wood (cherry), staghorn, hemp, felt","L. 28 1/2 in. (72.4 cm); W. 26 1/2 in. (67.2 cm); Wt. 11 lb. 7 oz. (5,197 g)","Gift of William H. Riggs, 1913",,,,,,,,,,,,Archery Equipment-Crossbows,,http://www.metmuseum.org/art/collection/search/22397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.597,false,true,693886,Arms and Armor,Medal,Medal of Kolman Helmschmid (1471–1532),"German, Augsburg",,,,,Designer,After a model by,Hans Kels the Younger,"German, 1508/10–1565",,"Kels, Hans, the Younger",,1508,1565,dated 1532,1532,1532,Lead,Diam. 1 15/16 in. (50 mm); Wt. 1.6 oz. (44.96 g),"Purchase, Kenneth and Vivian Lam Gift, 2015",,Augsburg,,,,,,,,,,Medals,,http://www.metmuseum.org/art/collection/search/693886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.29.1–.2,false,true,22609,Arms and Armor,Miniature Italian-style armor for man and horse,Miniature Italian-Style Armor for Man and Horse,French,,,,,Armorer,Possibly made by,Granger LeBlanc,"French, active ca. 1840–70",,"LeBlanc, Granger",,1815,1895,ca. 1860,1835,1885,Steel,H. as mounted 17 7/8 in. (45.4 cm),"Gift of William Oothout, 1916",,,,,,,,,,,,Armor-Miniatures,,http://www.metmuseum.org/art/collection/search/22609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.311,false,true,716772,Arms and Armor,Engraving,Plate Six from Nouveavx Desseins D’Arquebvseries,French,,,,,Engraver,Signed by,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,dated 1749,1749,1749,Engraving,sheet: 11 7/8 x 9 in. (30.16 x 22.86 cm); plate: 9 1/8 x 6 3/8 in. (23.17 x 16.19 cm),"Purchase, Michael H. Pourfar Gift, 2016",,,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/716772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.312,false,true,716774,Arms and Armor,Engraving,Plate Ten from Nouveavx Desseins D’Arquebvseries,French,,,,,Engraver,Signed by,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,dated 1744,1744,1744,Engraving,sheet: 11 7/8 x 9 1/8 in. (30.16 x 23.17 cm); plate: 8 1/2 x 6 3/8 in. (21.59 x 16.19 cm),"Purchase, Michael H. Pourfar Gift, 2016",,,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/716774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"26.145.291a, b",false,true,32526,Arms and Armor,Smallsword with scabbard,Smallsword with Scabbard,French,,,,,Sword cutler,,C. Liger,"French, Paris, recorded 1770–93",,"Liger, C.",,1770,1793,ca. 1780,1770,1793,"Steel, silver, gold, wood, textile, fishskin",L. with scabbard 38 5/8 in. (98.1 cm); L. without scabbard 38 1/8 in. (96.8 cm); L. of blade 31 1/2 in. (80 cm); W. 4 1/2 in. (11.4 cm); D. 3 1/4 in. (8.3 cm); Wt. 13 oz. (368.54 g); Wt. of scabbard 2.5 oz. (70.9 g),"Gift of Jean Jacques Reubell, in memory of his mother, Julia C. Coster, and of his wife, Adeline E. Post, both of New York City, 1926",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/32526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.122.19,false,true,32971,Arms and Armor,Medal,"Medal Showing Henry IV of France (b. 1553, r. 1589–1610) and Marie de Médicis (1573–1642)",French,,,,,Artist,,Guillaume Dupré,"French, 1579–1640",,Dupré Guillaume,,1579,1640,dated 1603,1603,1603,Bronze,Diam. 2 9/16 in. (6.5 cm); thickness 1/4 in. (0.6 cm); Wt. 2.7 oz. (76.5 g),"Gift of Bashford Dean, 1922",,,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/32971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.245,false,true,716365,Arms and Armor,Etching and engraving,Design for a Rapier Hilt and Scabbard Chape,French,,,,,Artist,,Pierre Woeiriot de Bouzey II,"French, Neufchâteau 1532–1599 Damblain",,"de Bouzey, Pierre Woeiriot II",,1532,1599,1555,1555,1555,"Etching, engraving",Diam. 7 1/2 in. (19.1 cm),"Purchase, Gift of Andrew Solomon, 2016",,,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/716365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.697,false,true,739608,Arms and Armor,Engraving,"Design for a Sword-belt, a Knife and a Stylus",French,,,,,Artist,,Pierre Woeiriot de Bouzey II,"French, Neufchâteau 1532–1599 Damblain",,"de Bouzey, Pierre Woeiriot II",,1532,1599,ca. 1555,1530,1580,Engraving,7 3/16 x 9 3/4 in. (18.3 x 24.7 cm),"Purchase, Kenneth and Vivian Lam Gift, 2016",,,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/739608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.627,false,true,665952,Arms and Armor,Drawing,Drawing with Seven Designs for Firearms Ornament,French,,,,,Artist,,Jean-Francois Lucas,"French, 1747–1825",,"Lucas, Jean-Francois",,1747,1825,1806,1806,1806,"Pen and black ink, with gray wash and traces of graphite, on paper",10 15/16 x 7 13/16 in. (27.8 x 19.8 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2014",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/665952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.122.43,false,true,32992,Arms and Armor,Medal,"Medal Commemorating the Marriage of James III and Princess Clementina, 1719",British,,,,,Artist,Signed on the reverse by,Ottone Hamerani,"Italian, Rome 1694–1768",,Hamerani Ottone,,1694,1768,1719,1719,1719,Bronze,Diam. 1 7/8 in. (4.8 cm); thickness 3/16 in. (0.5 cm); Wt. 1.6 oz. (45.4 g),"Gift of Bashford Dean, 1922",,,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/32992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.154.2a–l,false,true,22629,Arms and Armor,Infantry armor,Infantry Armor,Italian,,,,,Armorer,"Upper plate of the gorget, skirt lames, and cheek pieces made by",Daniel Tachaux,"French, 1857–1928, active in France and America","in the Metropolitan Museum of Art, Armor Workshop","Tachaux, Daniel",,1857,1928,"dated 1571; upper plate of the gorget, skirt lames, and cheek pieces, 1917",1571,1917,"Steel, leather, brass",Wt. 33 lb. 11 oz. (15.28 kg),"Rogers Fund, 1916",,,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/22629,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.314,false,true,35640,Arms and Armor,Letter,"Letter from Henry Knox (1750–1806), Secretary of War, to Colonel Marinus Willet (1740–1830)",American,,,,,Author,,General Henry Knox,"American, Boston, Massachusetts 1750–1806 Thomaston, Maine",,"Knox, Henry General",,1750,1806,"dated May 27, 1786",1786,1786,Ink on paper,12 1/2 x 7 7/8 in. (31.7 x 20 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2008",,,,,,,,,,,,Works on Paper,,http://www.metmuseum.org/art/collection/search/35640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.5,false,true,722411,Arms and Armor,Engraving,Plate Eight from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,dated 1743,1743,1743,Engraving,sheet: 11 7/8 x 9 1/16 in. (30.2 x 23 cm); plate: 7 7/16 x 5 1/4 in. (18.9 x 13.3 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.6,false,true,722414,Arms and Armor,Engraving,Plate Nine from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,dated 1743,1743,1743,Engraving,sheet: 11 3/4 x 9 1/8 in. (29.8 x 23.2 cm); plate: 7 1/4 x 5 1/4 in. (18.4 x 13.3 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.7,false,true,722417,Arms and Armor,Engraving,Plate Eleven from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,ca. 1749,1724,1774,Engraving,sheet: 11 1/2 x 9 1/4 in. (29.2 x 23.5 cm); plate: 8 7/16 x 6 7/16 in. (21.4 x 16.3 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.18,false,true,722508,Arms and Armor,Engraving,Plate Nine from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,dated 1743,1743,1743,Engraving,sheet: 8 1/2 x 6 5/8 in. (21.6 x 16.8 cm); plate: 7 1/4 x 5 1/4 in. (18.4 x 13.3 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.20,false,true,722509,Arms and Armor,Engraving,Plate Eleven from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,ca. 1749,1724,1774,Engraving,sheet: 9 x 7 in. (22.9 x 17.8 cm); plate: 8 7/16 x 6 7/16 in. (21.4 x 16.3 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.21,false,true,722510,Arms and Armor,Engraving,Plate Eight from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",,1722,1776,dated 1743,1743,1743,Engraving,sheet: 8 5/8 x 6 7/8 (21.9 x 17.5 cm); plate: 7 7/16 x 5 1/4 in. (18.9 x 13.3 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.8,false,true,722420,Arms and Armor,Etching and engraving,"Design of a Flintlock, Side Plate, Butt Plate, and Trigger Guard, unnumbered plate from Nouveaux Desseins d'Arquebuserie Inventez et Gravez par Le Sr. Gillot","French, Paris",,,,,Engraver,,Claude Gillot,"French, Langres 1673–1722 Paris",,"Gillot, Claude",,1673,1722,ca. 1715,1690,1740,"Etching, engraving",Sheet: 9 5/8 x 6 9/16 in. (24.4 x 16.6 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.2,false,true,722405,Arms and Armor,Engraving,Plate Two from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,De Lacollombe,"French, Paris, active ca. 1702–ca. 1736",,"Lacollombe, De",,1702,1736,dated 1730,1730,1730,Engraving,sheet: 11 7/8 x 9 in. (30.2 x 22.9 cm); plate: 9 3/8 x 6 5/8 (23.8 x 16.8 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.3,false,true,722406,Arms and Armor,Engraving,Plate Four from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,De Lacollombe,"French, Paris, active ca. 1702–ca. 1736",,"Lacollombe, De",,1702,1736,ca. 1730,1705,1755,Engraving,sheet: 11 3/4 x 9 1/8 in. (29.8 x 23.2 cm); plate: 9 1/4 x 6 5/8 in. (23.5 x 16.8 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.4,false,true,722409,Arms and Armor,Engraving,Plate Seven from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,De Lacollombe,"French, Paris, active ca. 1702–ca. 1736",,"Lacollombe, De",,1702,1736,ca. 1730,1705,1755,Engraving,sheet: 12 x 9 in. (30.5 x 22.9 cm); plate: 9 3/8 x 6 3/8 in. (23.8 x 16.2 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.19,false,true,716437,Arms and Armor,Engraving,Plate Three from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver,,De Lacollombe,"French, Paris, active ca. 1702–ca. 1736",,"Lacollombe, De",,1702,1736,ca. 1730,1705,1755,Engraving,sheet: 8 x 6 3/8 in. (20.3 x 16.2 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/716437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.1,false,true,722401,Arms and Armor,Engraving,Plate One (Title Page) from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver|Engraver,,Gilles Demarteau|De Lacollombe,"French, Liège 1722–1776 Paris|French, Paris, active ca. 1702–ca. 1736",,"Demarteau, Gilles|Lacollombe, De",,1722 |1702,1776 |1736,dated 1730,1730,1730,Engraving,sheet: 11 1/2 x 9 in. (29.2 x 22.9 cm); plate: 9 3/8 x 6 5/8 in. (23.8 x 16.8 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.905,.906;2016.311,.312,.403.1–.7,.409",false,true,726774,Arms and Armor,Engravings,Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver|Engraver,,Gilles Demarteau|De Lacollombe,"French, Liège 1722–1776 Paris|French, Paris, active ca. 1702–ca. 1736",,"Demarteau, Gilles|Lacollombe, De",,1722 |1702,1776 |1736,ca. 1705–49,1680,1774,Engravings,plate 1 (2016.403.1): sheet: 11 1/2 x 9 in. (29.2 x 22.9 cm); plate: 9 3/8 x 6 5/8 in. (23.8 x 16.8 cm); plate 2 (2016.403.2): sheet: 11 7/8 x 9 in. (30.2 x 22.9 cm); plate: 9 3/8 x 6 5/8 (23.8 x 16.8 cm); plate 3 (2013.906): 10 1/2 x 7 1/2 in. (26.7 x 19.1 cm); plate 4 (2016.403.3): sheet: 11 3/4 x 9 1/8 in. (29.8 x 23.2 cm); plate: 9 1/4 x 6 5/8 in. (23.5 x 16.8 cm); plate 5 (2016.409): sheet: 11 1/2 x 9 in. (29.2 x 22.9 cm); plate: 9 3/8 x 6 5/8 in. (23.8 x 16.8 cm); plate 6 (2016.311): sheet: 11 7/8 x 9 in. (30.16 x 22.86 cm); plate: 9 1/8 x 6 3/8 in. (23.17 x 16.19 cm); plate 7 (2016.403.4): sheet: 12 x 9 in. (30.5 x 22.9 cm); plate: 9 3/8 x 6 3/8 in. (23.8 x 16.2 cm); plate 8 (2016.403.5): sheet: 11 7/8 x 9 1/16 in. (30.2 x 23 cm); plate: 7 7/16 x 5 1/4 in. (18.9 x 13.3 cm); plate 9 (2016.403.5): sheet: 11 3/4 x 9 1/8 in. (29.8 x 23.2 cm); plate: 7 1/4 x 5 1/4 in. (18.4 x 13.3 cm); plate 10 (2016.312): sheet: 11 7/8 x 9 1/8 in. (30.16 x 23.17 cm); plate: 8 1/2 x 6 3/8 in. (21.59 x 16.19 cm); plate 11 (2016.403.7): sheet: 11 1/2 x 9 1/4 in. (29.2 x 23.5 cm); plate: 8 7/16 x 6 7/16 in. (21.4 x 16.3 cm); plate 12 (2013.905): 7 1/2 x 9 3/8 in. (19 x 23.8 cm),"2013.905, .906: Purchase, Bequest of Stephen V. Grancsay, Rogers Fund, Helmut Nickel Gift, and funds from various donors, by exchange, 2013; 2016.311, .312: Purchase, Michael H. Pourfar Gift, 2016; 2016.403.1–.7: Purchase, Arthur Ochs Sulzberger Gift, 2016; 2016.409: Purchase, Marica F. Vilcek Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/726774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.9,false,true,722489,Arms and Armor,Engraving,Plate One from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Publisher|Engraver,,Le Hollandois|Thuraine|C. Jacquinet,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Hollandois, Le|Thuraine|Jacquinet, C.",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 x 5 in. (17.8 x 12.7 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722489,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.10,false,true,722495,Arms and Armor,Engraving,Plate Two from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Publisher|Engraver,,Le Hollandois|Thuraine|C. Jacquinet,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Hollandois, Le|Thuraine|Jacquinet, C.",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 7/8 x 5 3/4 in. (20 x 14.6 cm); plate: 7 7/16 x 5 5/16 in. (18.9 x 13.5 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.11,false,true,722497,Arms and Armor,Engraving,Plate Three from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Engraver|Publisher|Publisher,,C. Jacquinet|Le Hollandois|Thuraine,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Jacquinet, C.|Hollandois, Le|Thuraine",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 x 5 1/8 in. (17.8 x 13 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.12,false,true,722499,Arms and Armor,Engraving,Plate Five from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Engraver|Publisher,,Le Hollandois|C. Jacquinet|Thuraine,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Hollandois, Le|Jacquinet, C.|Thuraine",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 1/4 x 5 1/4 in. (18.4 x 13.3 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.13,false,true,722500,Arms and Armor,Engraving,Plate Six from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Publisher|Engraver,,Thuraine|Le Hollandois|C. Jacquinet,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Thuraine|Hollandois, Le|Jacquinet, C.",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 1/16 x 5 3/8 in. (17.9 x 13.7 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.14,false,true,722502,Arms and Armor,Engraving,Plate Seven from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Publisher|Engraver,,Thuraine|Le Hollandois|C. Jacquinet,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Thuraine|Hollandois, Le|Jacquinet, C.",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 1/2 x 5 1/2 in. (19.1 x 14 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.15,false,true,722503,Arms and Armor,Engraving,Plate Eight from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Publisher|Engraver,,Le Hollandois|Thuraine|C. Jacquinet,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Hollandois, Le|Thuraine|Jacquinet, C.",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 1/16 x 5 1/8 in. (17.9 x 13 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.16,false,true,722505,Arms and Armor,Engraving,Plate Nine from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Publisher|Engraver,,Le Hollandois|Thuraine|C. Jacquinet,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Hollandois, Le|Thuraine|Jacquinet, C.",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 1/8 x 5 in. (18.1 x 5 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.403.17,false,true,722506,Arms and Armor,Engraving,Plate Ten from Plusieurs Models des plus nouuelles manieres qui sont en usage en l'Art de Arquebuzerie,"French, Paris",,,,,Publisher|Engraver|Publisher,,Le Hollandois|C. Jacquinet|Thuraine,"French, Paris, active mid-17th century|French, Paris, active mid-17th century|French, Paris, active mid-17th century",,"Hollandois, Le|Jacquinet, C.|Thuraine",,1625 |1625 |1625,1675 |1675 |1675,ca. 1660,1635,1685,Engraving,sheet: 7 x 5 1/16 in. (17.8 x 12.8 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/722506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.698,false,true,742566,Arms and Armor,"Design for a sword hilt, scabbard, and belt fittings","Design for a Sword Hilt, Scabbard, and Belt Fittings","French, Paris",,,,,Artist,Attributed to,Eugène Julienne,"French, Paris 1808–1875 Paris",,"Julienne, Eugène",,1808,1875,ca. 1840–50,1815,1875,Watercolor and ink on paper,12 3/8 x 18 3/4 in. (32 x 48 cm),"Purchase, Kenneth and Vivian Lam Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/742566,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.409,false,true,718296,Arms and Armor,Plate five from Nouveavx Desseins D'Arquebvseries,Plate Five from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Artist,,De Lacollombe,"French, Paris, active ca. 1702–ca. 1736",,"Lacollombe, De",,1702,1736,dated 1730,1730,1730,Engraving,sheet: 11 1/2 x 9 in. (29.2 x 22.9 cm); plate: 9 3/8 x 6 5/8 in. (23.8 x 16.8 cm),"Purchase, Marica F. Vilcek Gift, 2016",,Paris,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/718296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2004.118.1, .2",false,true,27017,Arms and Armor,Pair of tubelock hammers,Pair of Tubelock Hammers,"British, London",,,,,Gunsmith,,Joseph Manton,"British, Grantham, Lincolnshire 1766–1835 London",,Manton Joseph,,1766,1835,ca. 1819–20,1819,1820,Steel,H. of each 2 5/16 in. (6 cm); Wt. of each 1.1 oz (31 g),"Purchase, Fletcher Fund, by exchange",,London,,,,,,,,,,Firearms Parts,,http://www.metmuseum.org/art/collection/search/27017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.81.3a–f,false,true,32180,Arms and Armor,Repeating flintlock pistol,"Flintlock Repeating Pistol with Lorenzoni Action, bearing the Crests of Vice Admiral Horatio Nelson, with Case and Acccessories","British, London",,,,,Gunsmith,,Harvey Walklate Mortimer,"British, Newcastle-under-Lyme 1753–1819 Hampstead-heath (now London)",,"Mortimer, Harvey Walklate",,1753,1819,ca. 1798–1799,1793,1803,"pistol: steel, wood (walnut), silver; reserve barrel: steel; bullet mould: steel; punch; steel; wrench: steel; case: wood (mahogany), brass, textile, paper",Pistol (a); L. 14 1/2 in. (36.8 cm); L. of barrel 6 in. (15.2 cm); Cal. .55 in. (14 mm); Wt. 3 lb. 15 oz. (1786 g); reserve barrel (b); L. 6 1/8 in. (15.6 cm); Wt. 14.4 oz. (408.2 g); bullet mould (c); L. 5 3/8 in. (13.7 cm); Wt. 2.6 oz. (73.7 g); punch (d); L. 5 1/2 in. (14 cm); Wt. 4.4 oz. (124.7 g); wrench (e); L. 3 3/4 in. (9.5 cm); Wt. 1.6 oz. (45.4 g); case (f); H. 4 1/8 in. (10.5 cm); W. 15 9/16 in. (39.5 cm); D. 7 5/8 in. (19.4 cm); Wt. 6 lb. 7.2 oz. (2925.7 g),"Bequest of Charles N. Daly, 1934",,London,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/32180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.145.343,false,true,22943,Arms and Armor,Smallsword,Smallsword,"Dutch, Amsterdam",,,,,Sword maker,,Jan Nieuwland,"Dutch, Amsterdam, active 1747–1807",,"Nieuwland, Jan",Dutch,1747,1807,ca. 1750,1725,1775,"Silver, porcelain (Meissen), steel, gold, textile",L. 34 13/16 in. (88.5 cm); L. of blade 28 3/4 in. (73 cm); W. 3 7/8 in. (9.8 cm); D. 2 3/4 in. (7 cm); Wt. 14 oz. (396.9 g),"Gift of Jean Jacques Reubell, in memory of his mother, Julia C. Coster, and of his wife, Adeline E. Post, both of New York City, 1926",,Amsterdam,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"19.53.13, .14",false,true,22732,Arms and Armor,Pair of wheellock pistols,Pair of Wheellock Pistols,"French, Lisieux",,,,,Gunsmith,,Pierre Le Bourgeois,"French, Lisieux, died 1627",,"Le Bourgeois, Pierre",French,1527,1627,ca. 1610–20,1585,1645,"Steel, gold, wood, silver, mother-of-pearl","L. of each pistol 23 5/16 in. (59.2 cm); Cal. of each pistol, .49 in. (12.5 mm); L. of each barrel 15 5/8 in. (39.7 cm); Wt. of 19.53.13, 2 lb. 8 oz. (1134 g); Wt. of 19.56.14, 2 lb. 10 oz. (1191 g)","Gift of Charles M. Schott Jr., 1917",,Lisieux,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/22732,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.58a–e,false,true,23950,Arms and Armor,Double-barreled flintlock shotgun,Double-Barreled Flintlock Shotgun,"French, Versailles",,,,,Gunsmith,,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,dated 1801,1801,1801,"Steel, wood (walnut, ebony), gold, silver",L. 48 3/8 in. (122.9 cm); L. of barrel 33 in. (83.8 cm); Cal. .60 in. (15.2 mm); Wt. 7 lb. 6 oz. (3350 g),"Rogers Fund, 1936",,Versailles,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/23950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.179.1a–q,true,true,24861,Arms and Armor,"Cased set of a flintlock rifle, a pair of pistols, and accessories","Cased Set of a Flintlock Rifle, a Pair of Pistols, and Accessories","French, Versailles",,,,,Gunsmith,,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1800,1775,1825,"Steel, wood (walnut, mahogany), silver, gold, horn, velvet",L. of rifle 43 1/2 in. (110.5 cm); L. of barrel of rifle 27 5/8 in. (70.2 cm); Cal. of rifle .64 in. (16.3 mm); Wt. of rifle 6 lb. 7 oz. (2920 g); L. of each pistol 17 in. (43.17 cm); L. of barrel 11 5/8 in. (29.53 cm); Cal. of each pistol .52 in. (13.2 mm); Wt. of each pistol 2 lbs. 2 oz. (963.9 g); Dim. of case 46 7/16 x 15 15/16 x 2 3/16 in. (118 x 40.5 x 5.5 cm),"Fletcher Fund, 1970",,Versailles,,,,,,,,,,Firearms-Guns-Flintlock,,http://www.metmuseum.org/art/collection/search/24861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.581a, b",false,true,35956,Arms and Armor,Prototype for helmet model no. 2,Prototype for Helmet Model No. 2,"American, New York",,,,,Armorer,,Daniel Tachaux,"French, 1857–1928, active in France and America",,"Tachaux, Daniel",French,1857,1928,1917,1917,1917,"Steel, pressed paper or cardboard",H. 8 in. (20.3 cm); W. 10 in. (25.4 cm); D. 13 3/8 in. (34 cm); Wt. 2 lb. 4 oz. (1020 g),"Purchase, Gift of Bashford Dean, by exchange, 2013",,New York,New York,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/35956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.128.1o,false,true,35902,Arms and Armor,Right thigh and knee defense (cuisse and poleyn),Right Thigh and Knee Defense (Cuisse and Poleyn) for the Armor of Sir John Scudamore (1541 or 1542–1623),"American, New York",,,,,Armorer,,Daniel Tachaux,"French, 1857–1928, active in France and America",,"Tachaux, Daniel",French,1857,1928,dated 1913,1913,1913,"Steel, gold, leather",H. 16 1/2 in. (41.9 cm); W. 7 11/16 in. (19.5 cm); D. 5 in. (12.7 cm); Wt. 2 lb. 12 oz. (1247.4 g),"Frederick C. Hewitt Fund, 1911",,New York,New York,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/35902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.291a–q,false,true,35928,Arms and Armor,Jousting armor,Armor for the Joust of Peace,"German; restorations, French, Paris",,,,,Armorer,Helmet (04.3.291a) made by,Daniel Tachaux,"French, 1857–1928, active in France and America",,"Tachaux, Daniel",French,1857,1928,ca. 1500 and later,1475,1960,"Steel, copper alloy, leather, textile, horn","Helmet (a); Wt. 21.5 lb. (9752 g); breastplate (c); Wt. 23 lb. (10.4 kg); tassets (e, f); Wt. of each 6 lb. (2723 g); backplate (i); Wt. 5 lb. (2268 g); rondels (o, p); Wt. of each 5 lb. (2268 g); shield (q); Wt. 6 lb. (2723 g); Wt. overall 85.5 lb. (38.78 kg)","Armor for man and Shaffron: Rogers Fund, 1904; spurs: Bashford Dean Memorial Collection, Funds from various donors, 1929; bit, stirrups, and lance coronel: Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928; vamplate for the lance: Gift of William H. Riggs, 1913",,Paris,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/35928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"04.3.291a–q, .292; etc.",false,true,21926,Arms and Armor,Jousting armor,Jousting Armor,"German; restorations, French, Paris",,,,,Armorer,Helmet made by,Daniel Tachaux,"French, 1857–1928, active in France and America",,"Tachaux, Daniel",French,1857,1928,ca. 1500 and later,1475,1900,"Steel, copper alloy, leather, textile, horn","Helmet (a); Wt. 21.5 lb. (9752 g); breastplate (c); Wt. 23 lb. (10.4 kg); tassets (e, f); Wt. of each 6 lb. (2723 g); backplate (i); Wt. 5 lb. (2268 g); rondels (o, p); Wt. of each 5 lb. (2268 g); shield (q); Wt. 6 lb. (2723 g); Wt. overall 85.5 lb. (38.78 kg)","Rogers Fund, 1904",,Paris,,,,,,,,,,Armor for Horse and Man,,http://www.metmuseum.org/art/collection/search/21926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.354,false,true,35703,Arms and Armor,Flintlock sporting gun,Flintlock Sporting Gun of Empress Margarita Teresa of Spain (1651–1673),"Austrian, Vienna",,,,,Gunsmith,,Jacques Lamarre,"French, recorded Paris 1657–1700 Vienna, Austria",,"Lamarre, Jacques",French,1657,1700,ca. 1670–73,1645,1698,"Steel, wood (burl walnut), silver, copper alloy, gold",L. 52 1/8 in. (132.4 cm); L. of barrel 38 3/8 in. (97.5 cm),"Purchase, Arthur Ochs Sulzberger and Irene Roosevelt Aitken Gifts, 2011",,Vienna,,,,,,,,,,Firearms-Guns-Flintlock,,http://www.metmuseum.org/art/collection/search/35703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.223,true,true,24865,Arms and Armor,Flintlock gun,"Flintlock Gun of Louis XIII (1601–1643), King of France","French, Lisieux",,,,,Gunsmith|Gunsmith,,Pierre Le Bourgeois|Marin Le Bourgeois,"French, Lisieux, died 1627|French, Lisieux, ca. 1550–1634",,"Le Bourgeois, Pierre|Le Bourgeois, Marin",French,1527 |1550,1627 |1634,ca. 1620,1595,1645,"Steel, brass, silver, gold, wood (walnut), mother-of-pearl",L. 55 5/16 in. (140.5 cm); Cal. .59 in. (55 mm); L. of barrel 41 in. (104.1 cm); L. of lockplate 6 9/16 in. (16.7 cm); Wt. 5 lb. 11 oz. (2580 g),"Rogers Fund and Harris Brisbane Dick Fund, 1972",,Lisieux,,,,,,,,,,Firearms-Guns-Flintlock,,http://www.metmuseum.org/art/collection/search/24865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.1,false,true,652955,Arms and Armor,Drawings,Design for the Decoration of the Grip of a Pocket Pistol,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",3 3/8 x 7 7/16 in. (8.6 x 18.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.2,false,true,652958,Arms and Armor,Drawings,Design for the Decoration of the Grip of a Pocket Pistol,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 5/8 x 2 13/16 in. (9.2 x 7.1 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.3,false,true,652959,Arms and Armor,Drawings,Design for the Decoration of the Grip of a Pocket Pistol,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 x 3 1/2 in. (10.2 x 8.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.4,false,true,652960,Arms and Armor,Drawings,Design for the Decoration of the Grip of a Pocket Pistol,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",5 5/8 x 4 in. (14.3 x 10.2 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.5,false,true,652961,Arms and Armor,Drawings,Design for the Decoration of the Side Plate of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 3/4 x 6 1/4 in. (7 x 15.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.6,false,true,652962,Arms and Armor,Drawings,Design for the Decoration of the Side Plate of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",1 7/16 x 5 in. (3.7 x 12.7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.7,false,true,652963,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 3/16 x 7 3/4 in. (8.1 x 19.7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.8,false,true,652964,Arms and Armor,Drawings,Designs for the Decoration of the Frizzen and Jaws of the Cock of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 1/2 x 1 1/2 in. (8.9 x 3.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.9,false,true,652965,Arms and Armor,Drawings,Designs for the Decoration of the Jaw and Profile of the Cock of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/4 x 3 in. (5.7 x 7.6 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.10,false,true,652966,Arms and Armor,Drawings,Designs for the Decoration of the Jaw and Profile of the Cock of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",2 1/4 x 3 1/8 in. (5.7 x 7.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.11,false,true,652967,Arms and Armor,Drawings,Design for the Decoration of the Jaw of the Cock of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",1 x 1 in. (2.5 x 2.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.12,false,true,652968,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",6 3/4 x 3 1/2 in. (17.1 x 8.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.13,false,true,652969,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",6 1/2 x 3 in. (16.5 x 7.6 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.14,false,true,652970,Arms and Armor,Drawings,Design for the Decoration of a Gun Stock,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",3 1/4 x 7 1/8 in. (8.3 x 18.1 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.15,false,true,652971,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",2 7/8 x 2 1/2 in. (7.3 x 6.4 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.16,false,true,652972,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",3 3/8 x 4 5/8 in. (8.6 x 11.7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.17,false,true,652973,Arms and Armor,Drawings,Design for the Decoration of the Surround of the Ramrod Pipe of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",2 5/8 x 3 7/8 in. (6.7 x 9.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.18,false,true,652974,Arms and Armor,Drawings,Design for the Decoration of the Surround of the Ramrod Pipe of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",3 1/4 x 1 7/8 in. (8.3 x 4.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.19,false,true,652975,Arms and Armor,Drawings,Design for the Decoration of the Surround of the Barrel Tang of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper",5 1/8 x 3 1/8 in. (13 x 7.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.20,false,true,652978,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",6 1/8 x 8 5/8 in. (15.6 x 21.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.21,false,true,652980,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 3/4 x 1 3/4 in. (9.5 x 4.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.22,false,true,652981,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/8 x 3 3/4 in. (10.5 x 9.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.23,false,true,652982,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/8 x 2 1/2 in. (10.5 x 6.4 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.24,false,true,652983,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/2 x 1 3/4 in. (11.4 x 4.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.25,false,true,652984,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 3/4 x 2 5/8 in. (7 x 6.7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.26,false,true,652985,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",1 5/8 x 2 in. (4.1 x 5.1 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.27,false,true,652986,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/8 x 1 7/8 in. (5.4 x 4.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.28,false,true,652987,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",1 1/8 x 1 3/4 in. (2.9 x 4.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.29,false,true,652988,Arms and Armor,Drawings,Design for the Decoration of the Surround of the Barrel Tang of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/8 x 3 3/4 in. (10.5 x 9.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.30,false,true,652989,Arms and Armor,Drawings,Partial Design for the Decoration of the Surround of the Barrel Tang of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/4 x 4 1/8 in. (10.8 x 10.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.31,false,true,652990,Arms and Armor,Drawings,Two Designs for the Decoration of Barrel Tang Surrounds of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/8 x 7 7/8 in. (10.5 x 20 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.32,false,true,652991,Arms and Armor,Drawings,Thirteen Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",5 3/4 x 8 1/4 in. (14.6 x 21 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.33,false,true,652992,Arms and Armor,Drawings,Four Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",7 3/4 x 5 7/8 in. (19.7 x 14.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.34,false,true,652993,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",8 1/2 x 3 3/4 in. (21.6 x 9.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.35,false,true,652994,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",8 1/8 x 3 5/8 in. (20.6 x 9.2 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.36,false,true,652995,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 3/4 x 5 1/4 in. (7 x 13.3 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.37,false,true,652996,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 3/4 x 3 7/8 in. (7 x 9.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.38,false,true,652997,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 x 2 3/4 in. (7.6 x 7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.39,false,true,652998,Arms and Armor,Drawings,Partial Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 7/8 x 2 3/4 in. (9.8 x 7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.40,false,true,652999,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 x 4 7/8 in. (5.1 x 12.4 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/652999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.41,false,true,653000,Arms and Armor,Drawings,Design for the Decoration of the Barrel Tang Surround of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 7/8 x 1 7/8 in. (9.8 x 4.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.42,false,true,653001,Arms and Armor,Drawings,Design for the Decoration of the Barrel Tang Surround of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",5 1/4 x 2 7/8 in. (13.3 x 7.3 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.43,false,true,653002,Arms and Armor,Drawings,Design for the Decoration of the Barrel Tang Surround of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 7/8 x 3 1/4 in (7.3 x 8.3 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.44,false,true,653003,Arms and Armor,Drawings,Design for the Decoration of the Surround of the Barrel Flat and Rear Sight of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/8 x 3 1/2 in. (10.5 x 8.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.45,false,true,653004,Arms and Armor,Drawings,Design for the Decoration of the Surround of the Rear Sight of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 3/4 x 3 5/8 in. (9.5 x 9.2 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.46,false,true,653005,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/8 x 1 3/4 in. (5.4 x 4.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.47,false,true,653006,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 5/8 x 2 7/8 in. (6.7 x 7.3 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.48,false,true,653007,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 7/8 x 1 7/8 in. (7.3 x 4.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.49,false,true,653008,Arms and Armor,Drawings,Two Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",1 1/8 x 2 1/2 in. (2.9 x 6.4 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.50,false,true,653009,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/8 x 1 1/2 in. (5.4 x 3.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.51,false,true,653010,Arms and Armor,Drawings,Two Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/8 x 2 3/8 in. (5.4 x 6 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.52,false,true,653011,Arms and Armor,Drawings,Two Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/8 x 2 1/2 in. (5.4 x 6.4 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.53,false,true,653012,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 5/8 x 2 in. (9.2 x 5.1 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.54,false,true,653013,Arms and Armor,Drawings,Design for the Decoration of a Pistol Grip,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 x 8 1/2 in. (10.2 x 21.6 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.55,false,true,653014,Arms and Armor,Drawings,Pair of Designs for the Decoration of the Grips of Pocket Pistols,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 1/4 x 7 1/4 in. (8.3 x 18.4 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.56,false,true,653015,Arms and Armor,Drawings,Pair of Designs for the Decoration of the Grips of Pocket Pistols,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 1/4 x 7 1/8 in. (8.3 x 18.1 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.57,false,true,653016,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/4 x 2 in. (5.7 x 5.1 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.58,false,true,653017,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 7/8 x 6 in. (7.3 x 15.2 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.59,false,true,653018,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",1 7/8 x 6 1/8 in. (4.8 x 15.6 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.60,false,true,653019,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",5 1/4 x 2 1/4 in. (13.3 x 5.7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.61,false,true,653020,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",5 3/4 x 2 1/4 in. (14.6 x 5.7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.62,false,true,653021,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",5 5/8 x 2 1/4 in. (14.3 x 5.7 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.63,false,true,653022,Arms and Armor,Drawings,Design for the Decoration of the Barrel of a Firearm,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",11 1/4 x 3 1/4 in. (28.6 x 8.3 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.64,false,true,653023,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 1/8 x 1 7/8 in. (7.9 x 4.8 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.65,false,true,653024,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 5/8 x 1 3/4 in. (9.2 x 4.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.66,false,true,653025,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",3 3/4 x 7/8 in. (9.5 x 2.2 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.67,false,true,653026,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 1/4 x 1 1/8 in. (10.8 x 2.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.68,false,true,653027,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",4 x 1 3/4 in. (10.2 x 4.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.69,false,true,653028,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/8 x 5 1/4 in. (5.4 x 13.3 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.70,false,true,653029,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",2 1/2 x 6 1/2 in. (6.4 x 16.5 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.71,false,true,653030,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",1 3/4 x 5 7/8 in. (4.5 x 14.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.72,false,true,653031,Arms and Armor,Drawings,Design for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1797–1805,1772,1830,"Pencil, ink, gray wash on paper",1 3/4 x 5 7/8 in. (4.5 x 14.9 cm),"Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/653031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.101.1–.72,false,true,26900,Arms and Armor,Drawings,Designs for the Decoration of Firearms,"French, Versailles",,,,,Designer,Workshop of,Nicolas Noël Boutet,"French, Versailles and Paris, 1761–1833",,"Boutet, Nicholas-Noël",French,1761,1833,ca. 1795–1805,1770,1830,"Pencil, ink, gray wash on paper","various sizes, largest: 8 3/8 x 6 1/16 in. (21.2 x 15.3 cm); smallest: 1 x 5/8 in. (2.5 x 1.5 cm)","Purchase, Clay P. Bedford Gift, 2004",,Versailles,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/26900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.122.14,false,true,32967,Arms and Armor,Medal,"François II, King of France, King Consort of Scotland","French, probably Paris",,,,,Designer,After designs of 1559–60 by,Étienne Delaune,"French, Orléans 1518/19–1583 Strasbourg",,"Delaune, Étienne",French,1518,1583,struck ca. 1600–1635,1559,1660,Bronze,Diam. 2 1/16 in. (5.2 cm),"Gift of Bashford Dean, 1922",,,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/32967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.57,false,true,26920,Arms and Armor,Engraving,Engraving of Firearms Parts,"French, Strasbourg",,,,,Designer,,Perrier,"French, Strasbourg, active mid-18th century",,Perrier,French,1725,1775,ca. 1750,1725,1775,Ink on paper,18 3/4 x 25 1/4 in. (47.5 x 64 cm),"Purchase, Jonathan and Elizabeth Roberts Gift, 2004",,Strasbourg,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/26920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.674,false,true,667460,Arms and Armor,Pommel plate,Pommel Plate for a Saddle in the Style of the Late Middles Ages,French,,,,,Maker|Goldsmith,Workshop of,Louis Marcy (Luigi Parmeggiani)|Henri Husson,"Italian, 1860–1945|French, 1851–1914",,"Marcy, Louis|Husson, Henri",French,1860 |1852,1945 |1914,late 19th–early 20th century,1850,1950,"Copper alloy, gold, enamel",H. 13 3/4 in. (34.9 cm); W. 16 1/8 in. (41 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2014",,,,,,,,,,,,Equestrian Equipment-Saddles,,http://www.metmuseum.org/art/collection/search/667460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.149.1,false,true,32850,Arms and Armor,Kidney Dagger,Kidney Dagger in Gothic Style,French,,,,,Maker,,Henri Husson,"French, 1851–1914",,"Husson, Henri",French,1852,1914,ca. 1880–90,1855,1915,"Steel, wood (walnut), copper, black enamel",L. 17 in. (43.2 cm),"Gift of Jacques Reubell, 1923",,Mantes,Île-de-France,,,,,,,,,Forgeries,,http://www.metmuseum.org/art/collection/search/32850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.493.1a, b",false,true,35879,Arms and Armor,Medal with case,Lamarck Medal with Case,French,,,,,Maker,,François-Léon Sicard,"French, 1862–1934",,"Sicard, François-Léon",French,1862,1934,ca. 1910,1885,1935,"Bronze, leather, textile",Diam. 2 3/8 in. (6.0 cm); Diam. of case 2 3/4 in. (7.0 cm); D. of case 1/2 in. (1.3 cm),"Gift of Dean K. Boorman, 2013",,,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/35879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.324,false,true,35366,Arms and Armor,Engraving,Engraving of Firearms Ornament,French,,,,,Artist,,Claude Simonin,"French, Nantes ca. 1635–1693 Nantes",,"Simonin, Claude",French,1635,1693,1693 or 1695,1693,1695,Ink on paper,8 1/2 x 6 5/16 in. (21.8 x 16.2 cm),"Purchase, funds from various donors, by exchange, 2007",,,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/35366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.1433a, b",false,true,22389,Arms and Armor,Pair of wheellock pistols,Pair of Wheellock Pistols,French,,,,,Designer,Decoration on the stocks copied in part from engravings by,Étienne Delaune,"French, Orléans 1518/19–1583 Strasbourg",,"Delaune, Étienne",French,1518,1583,ca. 1570–80,1545,1605,"Steel, gold, silver, brass, wood (walnut), staghorn, pigment",L. of each pistol 20 1/8 in. (51.1 cm); Cal. of 14.25.1433a .444 in (11.2 mm); Cal. of 14.25.1433b .463 in. (11.3 mm); L. of each barrel 12 in. (30.5 cm); L. of plug 7 1/2 in. (19.1 cm); Diam. at muzzle 3/4 in. (19.1 mm); Diam. at breech 1 5/16 in. (3.3 cm); L. of lock 5 5/8 in. (14.3 cm); Wt. of 14.25.1433a 4 lb. 2 oz. (1871 g); Wt. of 14.25.1433b 4 lb. (1814 g),"Gift of William H. Riggs, 1913",,,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/22389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.1433a, b; .1523",false,true,35867,Arms and Armor,Pair of wheellock pistols with matching priming flask/spanner,Pair of Wheellock Pistols with Matching Priming Flask/Spanner,French,,,,,Designer,Decoration on the stocks copied in part from engravings by,Étienne Delaune,"French, Orléans 1518/19–1583 Strasbourg",,"Delaune, Étienne",French,1518,1583,ca. 1570–80,1545,1605,"Steel, gold, silver, brass, wood (walnut), staghorn, brass wire, pigment","Cal. of each pistol, .44 in (11.18 mm); Wt. of 14.25.1433a: 4 lb. 2 oz. (1871 g); Wt. of 14.25.1433b: 4 lb. (1814 g); L. of each pistol, 20 1/8 in. (51.1 cm); L. of priming flask/spanner, 5 7/8 in. (14.91 cm)","Gift of William H. Riggs, 1913",,,,,,,,,,,,Firearms-Pistols-Wheellock,,http://www.metmuseum.org/art/collection/search/35867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.4,false,true,24869,Arms and Armor,Flintlock gun,Flintlock Gun,German,,,,,Designer,Decoration follows closely designs of the engraver,De Lacollombe,"French, Paris, active ca. 1702–ca. 1736",,"Lacollombe, De",French,1702,1736,ca. 1730–40,1705,1765,"Steel, silver, wood (walnut)","L. 56 3/8 in. (143.2 cm); L. of barrel 41 1/16 in. (104.3 cm); L. of lockplate 5 7/8 in. (14.9 cm), Cal. .62 in. (15.8 mm); Wt. 6 lb. 10 oz. (3005 g)","Purchase, Bashford Dean Bequest, James Elwood Jones Jr. Gift, and Rogers and Fletcher Funds, 1974",,,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.511.1–.3a–k,false,true,26891,Arms and Armor,Cased pair of percussion target pistols with loading and cleaning accessories,"Cased Pair of Percussion Target Pistols with Loading and Cleaning Accessories, Made for Henri Charles Ferdinand Marie Dieudonné d'Artois, Duke of Bordeaux, Count of Chambord (1820–1883)","French, Paris",,,,,Gunsmith,,Jean André Prosper Henri Le Page,"French, 1792–1854",,"Le Page, Jean André Prosper Henri",French,1792,1854,dated 1829,1829,1829,"Steel, gold, wood (ebony, walnut, amboyna), silver, velvet, ivory",case L. 15 in. (38 cm); W. 9 7/8 in. (25 cm); H. 3 1/4 in. ( 8 cm); pistol L. (each) 11 1/4 in. (28.6 cm); barrel L. (each) 6 3/8 in. (17 cm); Cal. (each) .43 in. (11 mm),"Purchase, Arthur Ochs Sulzberger Bequest and Irene Roosevelt Aitken Gift, 2013",,,Paris,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/26891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.189a–g,false,true,24936,Arms and Armor,Breech-loading rimfire cartridge pistol with case and accessories,Breech-Loading Rimfire Cartridge Pistol with Case and Accessories,"French, Paris",,,,,Gunsmith,,Louis Nicolas Auguste Flobert,"French, Paris, 1819–1894",,"Flobert, Louis Nicolas Auguste",French,1819,1894,ca. 1855,1830,1880,"Steel, gold, ivory, leather, textile",Pistol (a); L. 13 3/8 in. (33.9 cm); L. of barrel 8 3/8 in. (21.2 cm); Cal. .22 in. (5.6 mm); forked cleaning rod (b); L. 9 9/16 in. (24.3 cm); Wt. 1 oz. (28.3 g); scourging rod (c); L. 9 11/16 in. (24.6 cm); Wt. 1.1 oz. (31.2 g); cartridge box (d); H. 1 1/4 in. (3.2 cm); Diam. 2 in. (5.1 cm); Wt. 1.7 oz. (48.2 g); screwdriver (e); L. 3 1/16 in. (7.8 cm); Wt. 0.5 oz. (14.2 g); case (f); H. 2 3/16 in. (5.6 cm); W. 14 1/2 in. (36.8 cm); D. 8 5/8 in. (21.9 cm); Wt. 1 lb. 14.6 oz. (867.5 g); key (g); L. 1 1/8 in. (2.9 cm),"Purchase, John Stoneacre Ellis Collection, Gift of Mrs. Ellis and Augustus Van Horne Ellis, by exchange, 1989",,Paris,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/24936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.81.4,false,true,23949,Arms and Armor,Percussion pistol with case,Percussion Pistol with Case,"French, Paris",,,,,Gunsmith,,Alfred Gauvain,"French, Paris 1801–1889 Paris",,"Gauvain, Alfred",French,1801,1889,possibly 1844,1819,1869,"Steel, wood (ebony), gold, wood, textile",L. of pistol 16 7/8 in. (42.9 cm); Cal. .44 in. (11.2 mm); Wt. 3 lb. 4 oz. (1474 g),"Bequest of Charles N. Daly, 1934",,Paris,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/23949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.318a–m,false,true,27165,Arms and Armor,Cased pair of percussion pistols with accessories,Cased Pair of Percussion Pistols with Accessories,"French, Paris",,,,,Gunsmith,,Louis-Julien Gastinne-Renette,"French, Paris 1812–1885 Paris",,Gastinne-Renette Louis-Julien,French,1812,1885,dated 1856,1856,1856,"Steel, brass, gold, wood (ebony), textile (baizé, wool), copper, leather","Pistols (a, b); L. of each 16 in. (40.7 cm); L. of each barrel 10 5/8 in. (27 cm); Cal. of each .46 in. (12 mm); Wt. of each 2 lb. 3 oz. (992.2 g); ramrod (c); L. 11 1/4 in. (28.6 cm); Wt. 1.5 oz. (42.5 g); cleaning rod (d); L. 12 3/8 in. (31.4 cm); Wt. 1.8 oz. (51 g); mallet (e); L. 7 7/8 in. (20 cm); W. 2 1/2 in. (6.4 cm); Wt. 5.3 oz. (150.3 g); screwdriver (f); L. 6 1/4 in. (15.9 cm); Wt. 2.6 oz. (73.7 g); bullet mould (g); L. 7 7/8 in. (20 cm); Wt. 8.8 oz. (249.5 g); powder flask (h); H. 4 in. (10.2 cm); Wt. 4.2 oz. (119.1 g); patch box (i); H. 1 in. (2.5 cm); Diam. 1 1/4 in. (3.2 cm); Wt. 0.4 oz. (11.3 g); nipple box (j); H. 1 11/16 in. (4.3 cm); Diam. 1 7/8 in. (4.8 cm); Wt. 1.5 oz. (42.5 g); spoon (k); L. 4 in. (10.2 cm); Wt. 0.4 oz. (11.3 g); case (l); H. 2 7/8 in. (7.3 cm); W. 18 7/8 in. (47.9 cm); D. 8 1/4 in. (21 cm); Wt. 7 lb. 4.7 oz. (3308.4 g); leather case (m); H. 3 3/8 in. (8.6 cm); W. 19 3/8 in. (49.2 cm); D. 8 3/4 in. (22.2 cm); Wt. 3 lb. 7.5 oz. (1573.4 g)","Purchase, John Stoneacre Ellis Collection, Gift of Mrs. Ellis and Augustus Van Horne Ellis, and Gift of Charles M. Schott, by exchange, 1989",,Paris,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/27165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.350.1–.19,false,true,34812,Arms and Armor,Engravings,Nouveaux Ornemans D'Arquebuseries,"French, Paris",,,,,Engraver,,Gilles Demarteau,"French, Liège 1722–1776 Paris",,"Demarteau, Gilles",French,1722,1776,ca. 1750–55,1725,1780,Ink on paper,10 1/4 x 6 3/4 in. (26 x 17.1 cm),"Purchase, Gift of Russell B. Aitken, by exchange, 2006",,Paris,,,,,,,,,,Works on Paper-Prints,,http://www.metmuseum.org/art/collection/search/34812,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2002.521.1a, b, .2a, b",false,true,26587,Arms and Armor,Pair of double-barreled flintlock pistols,Pair of Double-Barreled Flintlock Pistols,"French, Paris",,,,,Gunsmith,,François-Alexander Chasteau,"French, Paris, recorded 1741–84",,"Chasteau, François-Alexander",French,1741,1784,1752–53,1752,1753,"Steel, silver, gold, wood (walnut), whalebone",L. of each 14 3/8 in. (36.5 cm); Wt. of each 36 oz. (1030 g),"Gift of Walter A. Eberstadt, 2002",,Paris,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/26587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"17.87.3a, b",true,true,22631,Arms and Armor,Smallsword with scabbard,Congressional Presentation Sword with Scabbard of Colonel Marinus Willett (1740–1830),"French, Paris",,,,,Sword maker,,C. Liger,"French, Paris, recorded 1770–93",,"Liger, C.",French,1770,1793,hallmarked for 1785–86,1785,1786,"Steel, silver, gold, fish skin, textile, wood",sword L. 39 5/8 in. (100.6 cm); scabbard L. 33 1/4 in. (84.5 cm),"Bequest of George Willett Van Nest, 1916",,Paris,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.65,false,true,21949,Arms and Armor,Partisan,"Partisan Carried by the Bodyguard of Louis XIV (1638–1715, reigned from 1643)","French, Paris",,,,,Sword cutler,Inscription probably refers to,Bonaventure Ravoisie,"French, Paris, recorded 1678–1709",,"Ravoisie, Bonaventure",French,1678,1709,ca. 1678–1709,1678,1709,"Steel, gold, wood, textile",L. 94 1/8 in. (239 cm); L. of head 22 9/16 in. (57.3 cm); W. of head 6 1/2 in. (16.5 cm),"Rogers Fund, 1904",,,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/21949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.3,true,true,24937,Arms and Armor,Armor,"Armor of Infante Luis, Prince of Asturias (1707–1724)","French, Paris",,,,,Armorer,Signature probably refers to,Jean Drouart,"French, Paris, died before October 1715",,"Drouart, Jean",French,1615,1715,dated 1712,1687,1737,"Steel, gold, brass, silk, cotton, metallic yarn, paper",H. 28 in. (71.1 cm),"Purchase, Armand Hammer, Occidental Petroleum Corporation Gift, 1989",,Paris,,,,,,,,,,Armor for Child,,http://www.metmuseum.org/art/collection/search/24937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.114.2,false,true,24943,Arms and Armor,Flintlock pistol,Flintlock Pistol Made for Charles XI of Sweden (1655–1697),"French, Paris",,,,,Gunsmith,,Bertrand Piraube,"French, Paris, recorded ca. 1663–1725",,"Piraube, Bertrand",French,1638,1750,dated 1676,1676,1676,"Steel, gold, wood (walnut)",L. 21 in. (53.3 cm),"Purchase, Annie Laurie Aitken Charitable Trust Gift, 1990",,Paris,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.65.1–.2,true,true,24931,Arms and Armor,Costume armor,Costume Armor in the Classical Style,"French, Paris",,,,,Maker,Helmet includes original paper label of,Hallé,"French, Paris, active ca. 1780–1800",,Hallé,French,1755,1825,ca. 1788–90,1763,1815,"Linen, papier-mâché, bole, gold leaf, graphite (helmet); silk, cotton, metal coils and spangles, metallic yarn (tunic)",armor: H. 26 3/4 (68 cm); W. 22 7/16 (57 cm); D. 11 in. (28 cm); helmet: H. 15 3/4 (40 cm); W. 7 11/16 (19.5 cm); D. 13 3/4 in. (35 cm),"Funds from various donors, 1988",,Paris,,,,,,,,,,Costumes,,http://www.metmuseum.org/art/collection/search/24931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1988.65.1–.2; 1995.93a, b",false,true,626019,Arms and Armor,Costume armor and sword in the classical style,Costume Armor and Sword in the Classical Style,"French, Paris",,,,,Maker,Helmet includes original paper label of,Hallé,"French, Paris, active ca. 1780–1800",,Hallé,French,1755,1825,ca. 1788–90,1763,1815,"Linen, papier-mâché, bole, gold leaf, graphite (helmet); silk, cotton, metal coils and spangles, metallic yarn (tunic); steel, wood, gesso, silver, gold leaf (sword)","Helmet, 15 3/4 x 7 11/16 x 13 3/4 in. (40 x 19.5 x 35 cm); Armor, 26 3/4 x 22 7/16 x 11 in. (68 x 57 x 28 cm); Sword L., 31 7/8 in. (81 cm)","Armor: Funds from various donors, 1988; sword: Purchase, Gift of Estate of James Hazen Hyde, by exchange, and Rogers Fund, 1995",,,,,,,,,,,,Costumes,,http://www.metmuseum.org/art/collection/search/626019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.144.2a–n,false,true,27367,Arms and Armor,Cased pair of percussion pistols,Cased Pair of Percussion Pistols,"French, Paris",,,,,Artist,,Louis-Julien Gastinne-Renette,"French, Paris 1812–1885 Paris",,Gastinne-Renette Louis-Julien,French,1812,1885,mid-19th century,1851,1900,"Steel, wood (ebony), gold, velvet, brass, baize","L. of each pistol (a, b) 16 9/16 in. (42 cm); L. of each barrel 11 1/4 in. (28.6 cm); Cal. of each barrel .46 in. (11.7 mm); Wt. of each 2 lb. 6.7 oz. (1097.1 g); case (c); H. 3 in. (7.6 cm); W. 19 in. (48.3 cm); D. 10 1/2 in. (26.7 cm); Wt. 6 lb. 1.2 oz. (2755.6 g); screwdriver (d); L. 5 3/8 in. (13.7 cm); Wt. 1.7 oz. (48.2 g); nipple wrench (e); L. 5 1/2 in. (14 cm); Wt. 2.2 oz. (62.4 g); bullet mould (f); L. 5 7/8 in. (14.9 cm); Wt. 4.2 oz. (119.1 g); powder measure (g); L. 3 5/8 in. (9.2 cm); Wt. 0.4 oz. (11.3 g); patch box (h); H. 1 1/2 in. (3.8 cm); Diam. 1 1/2 in. (3.8 cm); Wt. 1.1 oz. (31.2 g); percussion cap box with contents (i); H. 1 3/8 in. (3.5 cm); Diam. 2 1/8 in. (5.4 cm); Wt. 2.6 oz. (73.7 g); mallet (j); L. 7 7/8 in. (20 cm); W. 2 1/2 in. (6.4 cm); Wt. 4.7 oz. (133.2 g); ramrod (k); L. 11 1/4 in. (28.6 cm); Wt. 1.5 oz. (42.5 g); cleaning rod (l); L. 11 7/8 in. (30.2 cm); Wt. 1.7 oz. (48.2 g); key (n); L. 1 1/2 in. (3.8 cm); Wt. 0.2 oz. (5.7 cm)","Gift of Mrs. George Henry Warren, in memory of her husband, 1972",,Paris,,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/27367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.138,false,true,35854,Arms and Armor,Drawing,Design for a Percussion Pistol,"French, Paris",,,,,Artist,,Martin Riester,"French, Colmar 1819–1883 Paris",,"Riester, Martin",French,1819,1883,dated 1850,1850,1850,"Pen, ink, and pencil on paper",11 x 7 1/8 in. (28 x 18 cm),"Purchase, James C. Meade Gift, 2012",,Paris,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/35854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.135.174,false,true,27290,Arms and Armor,Drawing,Architectural Drawing of the Exterior of the Comte de Nieuwerkerke's House,"French, Paris",,,,,Artist,Studio of,Hector-Martin Lefuel,"French, Versailles 1810–1880 Paris",,"Lefuel, Hector-Martin",French,1810,1880,ca. 1870,1845,1895,Pen on paper,16 1/8 x 9 3/8 in. (40.9 x 23.8 cm),"Gift of William H. Riggs, 1913",,Paris,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/27290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1878,false,true,436699,Arms and Armor,Painting,William H. Riggs (1837–1924) in Sixteenth-Century Half-Armor,"French, Paris",,,,,Artist,,Ferdinand Humbert,"French, 1842–1934",,"Humbert, Ferdinand",French,1842,1934,dated 1871,1871,1871,Oil on canvas,22 x 15 in. (55.9 x 38.1 cm),"Gift of William H. Riggs, 1913",,Paris,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.174a–gg,false,true,26743,Arms and Armor,"Album of thirty-five original drawings of swords, firearms, and related items",Designs for the Ornament of Swords and Firearms,"French, Paris",,,,,Designer,Workshop of,Louis-François Devisme,"French, Paris, active 1833–1886",,"Devisme, Louis-François",French,1833,1886,ca. 1850–60,1825,1885,"Leather, paper, pencil, ink, colored wash",19 x 12 1/2 in. (48.2 x 31.7 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2003",,Paris,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/26743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.454,false,true,22209,Arms and Armor,Partisan,"Partisan Carried by the Bodyguard of Louis XIV (1638–1715, reigned from 1643)","French, Paris",,,,,Designer,,Jean Berain,"French, Saint-Mihiel 1640–1711 Paris",,"Berain, Jean",French,1640,1711,ca. 1670–80,1645,1705,"Steel, gold, wood, textile",L. 86 11/16 in. (220.2 cm); L. of head 20 9/16 in. (52.2 cm) W. of head 6 1/16 in. (15.4 cm),"Gift of William H. Riggs, 1913",,Paris,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/22209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.498,false,true,24877,Arms and Armor,Drawing,Design for Shield,"French, Paris",,,,,Designer,,Étienne Delaune,"French, Orléans 1518/19–1583 Strasbourg",,"Delaune, Étienne",French,1518,1583,ca. 1550,1550,1550,"Paper, chalk",7 3/4 x 3 3/4 in. (19.7 x 9.6 cm),"Purchase, Rogers Fund, 1978",,Paris,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/24877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.170,false,true,23348,Arms and Armor,Powder horn,Powder Flask,"German, Nuremberg",,,,,Silversmith,,Jeremias Ritter,"German, recorded 1605–46",,"Ritter, Jeremias",German,1605,1646,ca. 1610,1585,1635,"Staghorn, silver, gold",L. 8 1/4 in. (21 cm); W. 4 in. (10.2 cm),"Rogers Fund, 1929",,Nuremberg,,,,,,,,,,Firearms Accessories-Powder Horns,,http://www.metmuseum.org/art/collection/search/23348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.153.1,false,true,23201,Arms and Armor,Jousting sallet (Rennhut),"Jousting Sallet Made for Louis II (1506–1526), King of Hungary and Bohemia","German, Augsburg",,,,,Armorer,Attributed to,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525,1500,1550,"Steel, copper alloy, gold",H. 10 in. (25.4 cm); W. 15 in. (38.1 cm); D. 10 in. (25.4 cm); Wt. 9 lb. (4082 g),"Bashford Dean Memorial Collection, Gift of Mr. and Mrs. Alexander McMillan Welch, 1929",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/23201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.286a,false,true,35825,Arms and Armor,Close helmet with mask visor in form of a human face,Close Helmet with Mask Visor in Form of a Human Face,"German, Augsburg",,,,,Armorer,Attributed to,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1515,1490,1540,"Steel, gold",H. 12 in. (30.5 cm); W. 9 3/4 in. (24.8 cm); D. 13 in. (33 cm); Wt. 4 lb. 12 oz. (2146 g),"Rogers Fund, 1904",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/35825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.159.18,false,true,26441,Arms and Armor,Closed burgonet,Closed Burgonet,"German, Augsburg",,,,,Armorer,Attributed to,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525–30,1500,1555,"Steel, leather",H. 14 5/8 in. (37.1 cm); W. 9 in. (22.9 cm); D. 12 7/8 in. (32.7 cm); Wt. 6 lb. 10 oz. (3004 g),"Gift of George D. Pratt, 1927",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/26441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.150.6a, b",false,true,35826,Arms and Armor,Armet for the tourney,Armet for the Tourney,"German, Augsburg",,,,,Armorer,Attributed to,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1515–20,1490,1545,"Steel, copper alloy",H. 12 1/2 in. (31.8 cm); W. 9 3/4 in. (24.8 cm); D. 11 7/16 in. (29.1 cm); Wt. 8 lb. 8 oz. (3866 g),"Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/35826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.179,false,true,22875,Arms and Armor,Backplate and hoguine (rump defense) from a costume armor,Backplate and Hoguine (Rump Defense) from a Costume Armor,"German, Augsburg",,,,,Armorer,,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525,1500,1550,"Steel, gold",H. 27 in. (68.6 cm); W. 18 in. (45.7 cm),"Gift of Bashford Dean, 1924",,Augsburg,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/22875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"26.188.1, .2",false,true,22949,Arms and Armor,Pair of Vambraces (arm defenses) from a costume armor,Pair of Vambraces (Arm Defenses) from a Costume Armor,"German, Augsburg",,,,,Armorer,,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525,1500,1550,"Steel, gold",H. 16 in. (40.6 cm); W. 8 in. (20.3 cm); D. 12 in. (30.5 cm),"Mrs. Stephen V. Harkness Fund, 1926",,Augsburg,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/22949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.158.363a, b",false,true,23307,Arms and Armor,Top lames of vambraces (arm defenses) from a costume armor,Top Lames of Vambraces (Arm Defenses) from a Costume Armor,"German, Augsburg",,,,,Armorer,,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525,1500,1550,"Steel, copper alloy, gold",H. 11 1/2 in. (29.2 cm); W. 10 in. (25.4 cm); D. 12 in. (30.5 cm),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Augsburg,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/23307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.286,false,true,22000,Arms and Armor,Armor,Armor,"German, Augsburg and Landshut",,,,,Armorer,Helmet attributed to,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1515 and later,1490,1900,"Steel, gold, leather",Helmet (04.3.286a); H. 12 in. (30.5 cm); W. 9 3/4 in. (24.8 cm); D. 13 in. (33 cm); Wt. 4 lb. 12 oz. (2146 g),"Rogers Fund, 1904",,Augsburg and Landshut,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"24.179; 26.188.1, .2; 29.158.363a, b",true,true,27790,Arms and Armor,Portions of a costume armor,Portions of a Costume Armor,"German, Augsburg",,,,,Armorer,,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525,1500,1550,"Steel, gold",H. 27 in. (68.6 cm); W. 18 in. (45.7 cm),"Backplate and rump: Gift of Bashford Dean, 1924; vambraces: Mrs. Stephen V. Harkness Fund, 1926; top lames of vambraces: Bashford Dean Memorial Collection, Funds from various donors, 1929",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/27790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.151.3a–s,false,true,23199,Arms and Armor,Armor,Three-Quarter Armor,"German, Augsburg",,,,,Armorer,Pauldrons and vambraces attributed to,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525 and later,1500,1925,"Steel, leather",Wt. 48 lb. 7 oz. (21.97 kg); helmet (a); H. 12 in. (30.5 cm); W. 11 in. (27.9 cm); D. 12 1/2 in. (31.8 cm); Wt. 6 lb. 1.2 oz. (2755.6 g); gauntlet (p); H. 10 1/2 in. (26.7 cm); W. 5 in. (12.7 cm); D. 4 3/8 in. (11.1 cm); Wt. 1 lb. 4.5 oz. (581.2 g); gauntlet (q); H. 10 1/2 in. (26.7 cm); W. 5 in. (12.7 cm); D. 4 3/8 in. (11.1 cm); Wt. 1 lb. 5.5 oz. (609.5 g); mail sleeve (r): L. 25 1/2 in. (65.0 cm); H. at shoulder 10 3/16 in. (26.0 cm); Diam. (outside) of chest links 5/16 in. (7.6 mm); Diam. (inside) of chest links 3/16 in. (4.8 mm); Diam. (outside) of sleeve links 5/16 in. (7.7 mm); Diam. (inside) of sleeve links 7/32 in. (5.8 mm); mail sleeve (s): L. 26 13/16 in. (68.0 cm); H. at shoulder 11 in. (28.0 cm); Diam. (outside) of chest links 11/32 in. (8.6 mm); Diam. (inside) of chest links 3/16 in. (4.8 mm); Diam. (outside) of sleeve links 9/32 in. (7.1 mm); Diam. (inside) of sleeve links 7/32 in. (5.6 mm).,"Bashford Dean Memorial Collection, Gift of Mrs. Bashford Dean, 1929",,Augsburg,,,,,,,,,,Armor for Man-3/4 Armor,,http://www.metmuseum.org/art/collection/search/23199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.828,false,true,22302,Arms and Armor,Left pauldron (shoulder defense),Left Shoulder Defense (Pauldron),"German, Augsburg",,,,,Armorer,,Kolman Helmschmid,"German, Augsburg 1471–1532",,"Helmschmid, Kolman",German,1471,1532,ca. 1525,1500,1550,"Steel, copper alloy, leather",H. 10 1/4 in. (26 cm); W. 9 1/4 in. (23.5 cm); D. 12 in. (30.5 cm); Wt. 2 lb. 10.4 oz. (1202 g),"Gift of William H. Riggs, 1913",,Augsburg,,,,,,,,,,Armor Parts-Arms & Shoulders,,http://www.metmuseum.org/art/collection/search/22302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.151.7–.8,false,true,22749,Arms and Armor,Two ear guards,Two Ear Guards from a Shaffron (Horse's Head Defense) of Emperor Charles V (1500–1558),"German, Augsburg",,,,,Armorer,Attributed to,Desiderius Helmschmid,"German, Augsburg, 1513–1579",,"Helmschmid, Desiderius",German,1513,1579,1544,1544,1544,"Steel, gold","20.151.7; H. 5 3/8 in. (13.7 cm); W. 2 3/4 in. (7 cm), Wt. 2 oz. (47 g); 20.151.8; H. 5 1/4 in. (13.3 cm); W. 2 7/8 in. (7.3 cm), Wt. 2 oz. (51 g)","Rogers Fund, 1920",,Augsburg,,,,,,,,,,Equestrian Equipment-Shaffrons,,http://www.metmuseum.org/art/collection/search/22749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.154.1a–u,false,true,23203,Arms and Armor,Armor for the tilt,Armor for the Tilt,"German, Augsburg",,,,,Armorer,Attributed to,Anton Peffenhauser,"German, Augsburg, 1525–1603",,"Peffenhauser, Anton",German,1525,1603,ca. 1580,1555,1605,"Steel, brass, leather",H. 68 3/4 in. (174.6 cm); W. at shoulders 18 in. (45.72 cm); Wt. 81 lb. (36.8 kg),"Bashford Dean Memorial Collection, Gift of Helen Fahnestock Hubbard, in memory of her father, Harris C. Fahnestock, 1929",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.163.1a–d,false,true,26534,Arms and Armor,Elements of an armor garniture,Elements from a Garniture Made for Christian I of Saxony (1560–1591),"German, Augsburg",,,,,Armorer,,Anton Peffenhauser,"German, Augsburg, 1525–1603",,"Peffenhauser, Anton",German,1525,1603,1582,1582,1582,"Steel, gold, leather, brass",breastplate (a): 19 1/2 x 15 3/8 in. (49.6 x 39 cm); backplate (b): 17 1/2 x 14 3/16 in. (44.5 x 36 cm); right cuisse (c): 16 x 7 3/8 in. (40.6 x 18.7 cm); Wt. 2 lb. 6 oz. (1088 g); left cuisse (d): 16 1/2 x 7 1/2 in. (41.9 x 19 cm); Wt. 2 lb. 7 oz. (1098 g),"Fletcher Fund, 1938",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/26534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.689a–p,false,true,22252,Arms and Armor,Jousting armor,Jousting Armor,"German, Augsburg",,,,,Armorer,Left Vambrace attributed to,Anton Peffenhauser,"German, Augsburg, 1525–1603",,"Peffenhauser, Anton",German,1525,1603,ca. 1580,1555,1605,"Steel, leather",Wt. 55 lb. (24.95 kg),"Gift of William H. Riggs, 1913",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.154.1a–u; 14.25.689h, .894; 29.158.204",false,true,35873,Arms and Armor,Armor for the tilt,Armor for the Tilt,"German, Augsburg",,,,,Armorer,Attributed to,Anton Peffenhauser,"German, Augsburg, 1525–1603",,"Peffenhauser, Anton",German,1525,1603,ca. 1580,1555,1605,"Steel, brass, leather",H. 68 3/4 in. (174.6 cm); W. at shoulders 18 in. (45.72 cm); Wt. 81 lb. (36.8 kg),"armor: Bashford Dean Memorial Collection, Gift of Helen Fahnestock Hubbard, in memory of her father, Harris C. Fahnestock, 1929; left vambrace and left gauntlet: Gift of William H. Riggs, 1913; mail brayette: Bashford Dean Memorial Collection, Funds from various donors, 1929",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/35873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.72,false,true,24903,Arms and Armor,Gauntlet for the left hand,Gauntlet for the Left Hand,"German, Augsburg",,,,,Goldsmith,Gilt copper ornament attributed to,Jörg Sigman,"German, Augsburg, 1527–1601",,"Sigman, Jörg",German,1527,1601,ca. 1557,1532,1582,"Steel, gold, copper alloy, leather",L. 12 in. (30.5 cm),"Purchase, Bequest of Stephen V. Grancsay, by exchange, 1984",,Augsburg,,,,,,,,,,Armor Parts-Gauntlets,,http://www.metmuseum.org/art/collection/search/24903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.165,false,true,35367,Arms and Armor,Cross hilt sword,Cross Hilt Sword,"hilt, British, London; blade, German, Solingen",,,,,Bladesmith,Blade signed by,Clemens Horn,"German, Solingen, 1580–1630",,"Horn, Clemens",German,1580,1630,1600–1625,1600,1625,"Iron, silver, wood, copper alloy, steel, gold",L. 39 1/4 in. (99.7 cm); L. of blade 30 1/4 in. (76.8 cm); W. 8 3/4 in. (22.2 cm); Wt. 2 lb. 6.5 oz (1093 g),"Purchase, Arthur Ochs Sulzberger Gift, 2010",,London|Solingen,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/35367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.164a–x,true,true,23944,Arms and Armor,Armor,Armor of Emperor Ferdinand I (1503–1564),"German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,dated 1549,1549,1549,"Steel, brass, leather",H. 67 in. (170.2 cm); Wt. 52 lb. 14 oz. (24 kg),"Purchase, Rogers Fund and George D. Pratt Gift, 1933",,Nuremberg,,,,,,,,,,Armor,,http://www.metmuseum.org/art/collection/search/23944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.628,false,true,26507,Arms and Armor,Burgonet,Burgonet,"German, Nuremberg",,,,,Armorer,Attributed to,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1540–50,1515,1575,"Steel, leather, textile",H. 12 1/2 in. (31.8 cm); W. 7 7/8 in. (20 cm); D. 12 1/4 in. (31.1 cm); Wt. 3 lb. 15 oz. (1786 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/26507,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.856,false,true,26581,Arms and Armor,Left pauldron (shoulder defense),"Left Pauldron (Shoulder Defense) Belonging to an Armor for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuania","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555; probably repainted later,1530,1900,"Steel, gold, paint",H. 8 1/2 in. (12.6 cm); W. 12 1/2 in. (31.7 cm); Wt. 1 lb. 10 oz. (730 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/26581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.151.2a–s,false,true,23198,Arms and Armor,Armor,Armor,"German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,"dated 1548, with later restorations",1548,1900,"Steel, leather, copper alloy, textile",Wt. approx. 56 lb. (25.4 kg),"Bashford Dean Memorial Collection, Gift of Mrs. Bashford Dean, 1929",,Nuremberg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.854,false,true,26579,Arms and Armor,Crinet plate,"Crinet Plate Belonging to an Armor for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuania","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555; probably repainted later,1530,1900,"Steel, brass, gold, paint",L. 13 7/8 in. (35.5 cm); H. 3 7/8 in. (9.8 cm); Wt. 9 oz. (261 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Armor for Horse,,http://www.metmuseum.org/art/collection/search/26579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.69a–q,false,true,23358,Arms and Armor,Horse armor,"Horse Armor Made for Johann Ernst, Duke of Saxony-Coburg (1521–1553)","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,dated 1548,1548,1548,"Steel, leather, copper alloy, textile",Wt. including saddle 92 lb. (41.73 kg) Bit: H. 6 in (15.2 cm); W. 11 in (27.9 cm),"Rogers Fund, 1932",,Nuremberg,,,,,,,,,,Armor for Horse,,http://www.metmuseum.org/art/collection/search/23358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.854; 21.42,false,true,684263,Arms and Armor,Crinet plate and shaffron,"Crinet Plate and Shaffron Belonging to an Armor for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuania","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555; probably repainted and eye guards restored later,1530,1900,"Steel, brass, gold, paint",crinet plate: L. 13 7/8 in. (35.5 cm); H. 3 7/8 in. (9.8 cm); Wt. 9 oz. (261 g); shaffron: H. 23 1/4 in. (59 cm); W. 13 1/4 in. (33.6 cm); Wt. 4 lb. 8 oz. (2034 g),"Crinet plate: Gift of William H. Riggs, 1913; shaffron: Rogers Fund, 1921",,Nuremberg,,,,,,,,,,Armor for Horse,,http://www.metmuseum.org/art/collection/search/684263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.854–.856, .865, .881a, b; 21.42",false,true,684262,Arms and Armor,Portions of an armor garniture for field and tournament,"Portions of an Armor Garniture for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuania","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555; probably repainted and shaffron eye guards restored later,1530,1900,"Steel, brass, gold, paint, leather, textile (velvet)",crinet plate (14.25.854): L. 13 7/8 in. (35.5 cm); H. 3 7/8 in. (9.8 cm); Wt. 9 oz. (261 g); vamplate (14.25.855): D. 8 1/2 in. (33.3 cm); H. 4 7/8 in. (12.4 cm); Diam. 13 1/8 in. (33.3 cm); Wt. 3 lb. 3 oz. (1454 g); left pauldron (14.25.856): H. 8 1/2 in. (12.6 cm); W. 12 1/2 in. (31.7 cm); Wt. 1 lb. 10 oz. (730 g); crinet plate (14.25.865): H. 2 3/8 in. (6 cm); W. 10 5/8 in. (27 cm); D. 6 5/16 in. (16 cm); Wt. 8 oz. (236 g); right tasset (14.25.881a): H. 6 in. (15.2 cm); W. 7 3/4 in. (19.7 cm); Wt. 1 lb. 5 oz. (587 g); left tasset (14.25.881b): H. 7 1/2 in. (19 cm); W. 8 1/2 in. (21.6 cm); Wt. 1 lb. 10 oz. (726 g); shaffron (21.42): H. 23 1/4 in. (59 cm); W. 13 1/4 in. (33.6 cm); Wt. 4 lb. 8 oz. (2034 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Armor for Horse,,http://www.metmuseum.org/art/collection/search/684262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.855,false,true,26580,Arms and Armor,Vamplate (handguard for the lance) for use in the field and tilt,"Vamplate Belonging to an Armor for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuania","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555; probably repainted later,1530,1900,"Steel, gold, paint",D. 8 1/2 in. (33.3 cm); H. 4 7/8 in. (12.4 cm); Diam. 13 1/8 in. (33.3 cm); Wt. 3 lb. 3 oz. (1454 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/26580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.865,false,true,22316,Arms and Armor,Crinet plate,"Crinet Plate Belonging to an Armor for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuaniac","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555,1530,1580,"Steel, gold, leather, textile (velvet)",H. 2 3/8 in. (6 cm); W. 10 5/8 in. (27 cm); D. 6 5/16 in. (16 cm); Wt. 8 oz. (236 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Equestrian Equipment,,http://www.metmuseum.org/art/collection/search/22316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.151.2a–s; 29.158.183–.184; 32.69a–q,false,true,35739,Arms and Armor,Armor for man and horse,Armor for Man and Horse,"German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,"dated 1548, with later restorations",1548,1900,"Steel, leather, copper alloy, textile",man's armor: Wt. approx. 56 lb. (25.4 kg); horse armor with saddle: Wt. 92 lb. (41.7 kg),"man's armor: Bashford Dean Memorial Collection, Gift of Mrs. Bashford Dean, 1929; mail sleeves: Bashford Dean Memorial Collection, Funds from various donors, 1929; horse armor: Rogers Fund, 1932",,Nuremberg,,,,,,,,,,Armor for Horse and Man,,http://www.metmuseum.org/art/collection/search/35739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.42,false,true,26583,Arms and Armor,Shaffron (Horse's head defense),"Shaffron (Horse's Head Defense) Belonging to an Armor for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuania","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555; probably repainted and eye guards restored later,1530,1900,"Steel, brass, gold, paint",H. 23 1/4 in. (59 cm); W. 13 1/4 in. (33.6 cm); D. 7 1/2 in. (19.1 cm); Wt. 4 lb. 8 oz. (2034 g),"Rogers Fund, 1921",,Nuremberg,,,,,,,,,,Equestrian Equipment-Shaffrons,,http://www.metmuseum.org/art/collection/search/26583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.881a, b",false,true,26582,Arms and Armor,Pair of tassets (thigh defenses) for the tilt,"Pair of Tassets (Thigh Defenses) Belonging to an Armor for Field and Tournament Made for Duke Nikolaus ""The Black"" Radziwill (1515–1565), Duke of Nesvizh and Olyka, Prince of the Empire, Grand Chancellor and Marshal of Lithuania","German, Nuremberg",,,,,Armorer,,Kunz Lochner,"German, Nuremberg, 1510–1567",,"Lochner, Kunz",German,1510,1567,ca. 1555; probably repainted later,1530,1900,"Steel, gold, paint",right tasset (a): H. 6 in. (15.2 cm); W. 7 3/4 in. (19.7 cm); Wt. 1 lb. 5 oz. (587 g); left tasset (b): H. 7 1/2 in. (19 cm); W. 8 1/2 in. (21.6 cm); Wt. 1 lb. 10 oz. (726 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Armor Parts-Thigh and Leg Defense,,http://www.metmuseum.org/art/collection/search/26582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"26.145.243a, b",true,true,22914,Arms and Armor,Hunting sword with scabbard,Hunting Sword with Scabbard,"German, possibly Munich",,,,,Sword maker,Grip attributed to,Joseph Deutschmann,"German, Imst 1717–1787 Passau",,"Deutschmann, Joseph",German,1717,1787,ca. 1740,1715,1765,"Steel, silver, ivory, wood, leather",L. 29 1/2 in. (74.9 cm); L. of blade 23 1/4 in. (59 cm); W. of blade 1 1/8 in. (2.8 cm); D. of blade 5/16 in. (0.8 cm);Wt. 1 lb. 2 oz. (510.29 g); Wt. of scabbard 1 lb 6 oz. (624 g),"Gift of Jean Jacques Reubell, in memory of his mother, Julia C. Coster, and of his wife, Adeline E. Post, both of New York City, 1926",,possibly Munich,,,,,,,,,,Swords-Hunting,,http://www.metmuseum.org/art/collection/search/22914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"69.295.1, .2",false,true,24859,Arms and Armor,Pair of flintlock pistols,Pair of Flintlock Pistols,"German, Regensburg",,,,,Gunsmith,,Johann Andreas Kuchenreuter,"German, Regensburg, 1716–1795",,"Kuchenreuter, Johann Andreas",German,1716,1795,ca. 1760–70,1735,1795,"Steel, wood, bronze, gold, silver, horn",L. 16 3/4 in. (42.6 cm); L. of barrel 11 1/4 in. (28.6 cm); L. of lockplate 4 1/2 in. (11.4 cm); Cal. .56 in. (14.2 mm); Wt. 1 lb. 13 oz. (822 g),"Purchase, Bashford Dean Bequest, 1969",,Regensburg,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/24859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.156.45,false,true,23213,Arms and Armor,Sallet,Sallet of Emperor Maximilian I (1459–1519),"German, Augsburg",,,,,Armorer,Attributed to,Lorenz Helmschmid,"German, Augsburg, ca. 1445–1516",,"Helmschmid, Lorenz",German,1425,1525,ca. 1490–95,1465,1520,"Steel, copper alloy, gold",H. 12 in. (30.5 cm); W. 9 in. (22.9 cm); D. 12 3/8 in. (31.4 cm); Wt. 4 lb. 15.7 oz. (2261 g),"Bashford Dean Memorial Collection, Gift of Edward S. Harkness, 1929",,Augsburg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/23213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.156.67h, i",false,true,23208,Arms and Armor,Pair of tournament pauldrons (shoulder defenses),Pair of Tournament Pauldrons (Shoulder Defenses),"German, Augsburg",,,,,Armorer,Marked by,Lorenz Helmschmid,"German, Augsburg, ca. 1445–1516",,"Helmschmid, Lorenz",German,1425,1525,ca. 1500,1475,1525,"Steel, copper alloy",Wt. of right pauldron 3 lb. 15 oz. (1786 g); Wt. of left pauldron 4 lb. (1814.4 g),"Bashford Dean Memorial Collection, Gift of Edward S. Harkness, 1929",,Augsburg,,,,,,,,,,Armor Parts-Arms & Shoulders,,http://www.metmuseum.org/art/collection/search/23208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"04.3.280, .282; 23.58; 26.234.3",false,true,35727,Arms and Armor,Portions of an armor garniture,Portions of an Armor Garniture,"German, Augsburg",,,,,Etcher,,Jörg Sorg the Younger,"German, Augsburg, ca. 1522–1603",,"Sorg the Younger, Jörg",German,1500,1625,"ca. 1550–55, some etched decoration, 19th century",1525,1900,"Steel, gold, leather, velvet",,"04.3.280, .282: Rogers Fund, 1904; 23.58: Anonymous Gift, in memory of Cornelius Stevenson, 1923; 26.234.3: Gift of George D. Pratt, 1926",,Augsburg,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/35727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.282,false,true,32198,Arms and Armor,Portions of an armor garniture,Portions of an Armor Garniture,"German, Augsburg",,,,,Etcher,,Jörg Sorg the Younger,"German, Augsburg, ca. 1522–1603",,"Sorg the Younger, Jörg",German,1500,1625,"ca. 1550–55, some etched decoration, 19th century",1525,1900,"Steel, gold, leather, velvet",,"Rogers Fund, 1904",,Augsburg,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/32198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.288,false,true,25419,Arms and Armor,Pommel plate,Pommel Plate,"German, Augsburg",,,,,Etcher,Attributed to,Jörg Sorg the Younger,"German, Augsburg, ca. 1522–1603",,"Sorg the Younger, Jörg",German,1500,1625,ca. 1550,1525,1575,"Steel, gold","9 x 6 in. (22.9 x 15.2 cm), wt. 7 oz. (185 g)","Rogers Fund, 1904",,Augsburg,Bavaria,,,,,,,,,Equestrian Equipment-Saddles,,http://www.metmuseum.org/art/collection/search/25419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.135.88,false,true,22890,Arms and Armor,Wheellock pistol,Wheellock Pistol Made for Maximilian I of Bavaria (1573–1651),"German, Munich",,,,,Steel-chiseler,,Emanuel Sadeler,"German, Munich, active 1594–1610",,"Sadeler, Emanuel",German,1594,1610,ca. 1600–1610,1575,1635,"Steel, gold",L. 14 3/16 in. (36.0 cm); L. of barrel 8 9/16 in. (21.7 cm); Cal. .45 in. (11.4 mm); Wt. 2 lb. 12 oz. (1247 g),"Gift of William H. Riggs, 1913",,Munich,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/22890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1419,false,true,22383,Arms and Armor,Wheellock pistol,Wheellock Pistol,"German, Nuremberg",,,,,Gunsmith,,Peter Danner,"German, Nuremberg, ca. 1580–1602",,"Danner, Peter",German,1555,1627,ca. 1580,1555,1605,"Steel, bronze, gold, wood (walnut), staghorn",L. 19 3/4 in. (50.2 cm); L. of barrel 12 in. (30.5 cm); Cal. .528 in. (13.4 mm); L. of lock 7 5/8 in. (19.4 cm); L. of plug 7 3/8 in. (18.7 cm); Wt. 3 lb. 14 oz. (1758 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Firearms-Pistols-Wheellock,,http://www.metmuseum.org/art/collection/search/22383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.11,false,true,26445,Arms and Armor,Sallet,Sallet,"German, Basel",,,,,Armorer,Attributed to,Hans Blarer the Younger,"German, Basel, documented 1453–83",,"Blarer, Hans the Younger",German,1453,1483,ca. 1470–80,1445,1505,Steel,H. 9 3/4 (24.8 cm); W. 8 in. (20.2 cm); D. 14 3/4 (37.4 cm); Wt. 7 lb. 2 oz. (3232 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Basel,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/26445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.183,false,true,21970,Arms and Armor,Wheellock spanner with priming flask and screwdriver,Wheellock Spanner with Priming Flask and Screwdriver,"German, Munich",,,,,Steel-chiseler,Workshop of,Daniel Sadeler,"German, Munich, recorded 1602–1632",,"Sadeler, Daniel",German,1602,1632,ca. 1610–30,1585,1655,"Steel, gold",L. 9 in. (22.9 cm); W. 2 3/4 in. (7 cm),"Rogers Fund, 1904",,Munich,,,,,,,,,,Firearms Accessories-Flasks & Primers,,http://www.metmuseum.org/art/collection/search/21970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.267a,false,true,35890,Arms and Armor,Close helmet,Close Helmet,"German, Landshut",,,,,Armorer,Attributed to,Wolfgang Grosschedel,"German, Landshut, active ca. 1517–62",,"Grosschedel, Wolfgang",German,1517,1562,ca. 1560,1535,1585,"Steel, leather, copper alloy",H. 14 3/4 in. (37.5 cm); W. 9 1/8 in. (23.2 cm); D. 13 1/4 in. (33.7 cm); Wt. 11 lb. 3.1 oz. (5077.4 g),"Rogers Fund, 1904",,Landshut,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/35890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.261,false,true,22874,Arms and Armor,Armor for man and horse,Armor for Man and Horse Armor Presumably Made for Baron Pankraz von Freyberg (1508–1565),"German, Landshut",,,,,Armorer,,Wolfgang Grosschedel,"German, Landshut, active ca. 1517–62",,"Grosschedel, Wolfgang",German,1517,1562,"man's armor, ca. 1535–40; horse armor, dated 1554; saddle steels, later restorations",1510,1900,"Steel; leather, copper alloy, textile",Wt. of man's armor approx. 55 lb. 11 oz. (25.25 kg); Wt. of horse armor with saddle 65 lb. 7 oz. (29.69 kg),"Fletcher Fund, 1923",,Landshut,,,,,,,,,,Armor for Horse and Man,,http://www.metmuseum.org/art/collection/search/22874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.633,false,true,22234,Arms and Armor,Morion,Morion for the Bodyguard of the Prince-Elector of Saxony,"German, Nuremberg",,,,,Armorer,Probably,Martin Schneider the Younger,"German, Nuremberg, active ca. 1610–20",,"Schneider the Younger, Martin",German,1610,1620,ca. 1570,1545,1595,"Steel, gold, brass, leather",H. 11 9/16 in. (29.4 cm); W. 9 1/4 in. (23.5 cm); D. 13 3/4 in. (34.9 cm); Wt. 3 lb. 5 oz. (1503 g),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.684a–i,false,true,22251,Arms and Armor,Half armor,Half Armor,"German, Nuremberg",,,,,Armorer,Attributed to,Martin Schneider the Younger,"German, Nuremberg, active ca. 1610–20",,"Schneider the Younger, Martin",German,1610,1620,1610–20,1610,1620,Steel,Wt. 41 lb. 15 oz. (19.01 kg),"Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/22251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.150.8a,false,true,23091,Arms and Armor,Sallet,Sallet,"German, Landshut",,,,,Armorer,,Matthes Deutsch,"German, Landshut, documented 1485–1505",,"Deutsch, Matthes",German,1485,1505,ca. 1490,1465,1515,"Steel, textile",H. 9 3/4 in. (24.8 cm); W. 9 3/4 in. (24.8 cm); D. 15 in. (38.1 cm); Wt. 9 lb. 4 oz. (4190 g),"Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,Landshut,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/23091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.160a–x,false,true,24814,Arms and Armor,Composed armor,Composed Armor,"European, Italian and German",,,,,Armorer,Right pauldron (shoulder defense) marked by,Matthes Deutsch,"German, Landshut, documented 1485–1505",,"Deutsch, Matthes",German,1485,1505,15th century and later,1400,1900,"Steel, iron, copper alloy (latten), leather, brass","Wt. approx. 41 lb. 12 oz. (18.94 kg); 50.160i (gauntlet); L. 11 3/4 in. (29.8 cm); W. 5 3/8 in. (13.7 cm); D. 5 in. (12.7 cm); Wt. 1 lb. (453.6 g); 50.160h (elbow reinforce); H. 8 3/8 in. (21.3 cm); W. 4 1/4 in. (10.8 cm); D. 9 1/2 in. (24.1 cm); Wt. 12.2 oz. (345.9 g); 50.160o (greave); H. 14 13/16 in. (37.6 cm); W. 4 5/8 in. (11.7 cm); D. 5 5/16 in. (13.5 cm); Wt. 1 lb. 9.2 oz. (714.4 g); 50.160n (thigh defense); H. 20 3/8 in. (51.8 cm); W. 6 11/16 in. (17 cm); D. 7 1/2 in. (19.1 cm); Wt. 4 lb. 5.5 oz. (1970.3 g); 50.160u (mail collar with integral bevor); H. 15 5/8 in. (37 cm); W. 31 5/16 in. (79.5 cm); Diam. (outside) of collar links 11/32 in. (8.3 mm); Diam. (inside) of collar links 7/32 in. (5.9 mm); Diam. (outside) of shawl links 5/16 in. (7.7 mm); Diam. (inside) of shawl links 1/4 in. (6.3 mm); 50.160v (mail sleeve); L. 29 1/8 in. (74 cm) W. 13 in. (33 cm); Diam. (outside) of links, 9/32 in. (7.2 mm); Diam. (inside) of links, 3/16 in. (5 mm); 50.160w (mail sleeve): L. 27 5/8 in. (70.0 cm); W. 12 3/16 in. (31.0 mm); Diam. (outside) of links 9/32 in. (6.8 mm); Diam. (inside) of links, 3/16 in. (4.6 mm); 50.160x (pair of mail paunces); H. 8 11/16 in. (22 cm); H. of skirt 14 13/16 in. (37.5 cm); W. of bravette 26 in. (66 cm); Diam. of waist 32 1/8 in. (81.5 cm); Diam. (outside) of links 13/32 in. (10.6 mm); Diam. (inside) of links, 7/32 in. (5.6 mm)","Gift of Mrs. Alexander McMillan Welch, in memory of her sister, Mary Alice Dyckman Dean (Mrs. Bashford Dean), 1950",,Landshut,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/24814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.756,false,true,22282,Arms and Armor,Vamplate (Hand guard for a lance),Vamplate (Hand Guard for a Lance),"German, Landshut",,,,,Armorer,,Matthes Deutsch,"German, Landshut, documented 1485–1505",,"Deutsch, Matthes",German,1485,1505,ca. 1490,1465,1515,Steel,H. 16 3/8 in. (41.5 cm); W. 11 3/8 in. (28.9 cm),"Gift of William H. Riggs, 1913",,Landshut,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/22282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.188.18,false,true,22899,Arms and Armor,Glaive,Glaive of Emperor Rudolf II (reigned 1576–1612),"German, Augsburg",,,,,Etcher,,Hans Stromair,"German, Augsburg, 1524 or 1525–ca. 1583",,"Stromair, Hans",German,1524,1600,dated 1577,1577,1577,"Steel, wood, textile, gold",L. 100 1/2 in. (255.27 cm); blade L. 23 in. (58.42 cm),"Gift of George D. Pratt, 1925",,Augsburg,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/22899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.158,false,true,21963,Arms and Armor,Hunting knife combined with wheellock pistol,Hunting Knife Combined with Wheellock Pistol,"German, Munich",,,,,Etcher,,Ambrosius Gemlich,"German, Munich and Landshut, active ca. 1520–50",,"Gemlich, Ambrosius",German,1495,1575,"blade ca. 1528–29, etched with a calendar for the years 1529–34; barrel dated 1540 or 1546",1528,1546,"Steel, gold, staghorn, bronze",L. 18 1/4 in. (46.4 cm); L. of barrel 12 3/8 in. (31.4 cm); L. of blade 13 1/4 in. (33.7 cm); Cal. .28 in. (7.1 mm),"Rogers Fund, 1904",,Munich,,,,,,,,,,Combination Weapons,,http://www.metmuseum.org/art/collection/search/21963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1655,false,true,27181,Arms and Armor,Crinet,Crinet,"German, Nuremberg",,,,,Armorer,,Valentin Siebenbürger,"German, Nuremberg, ca. 1510–1564, master in 1531",,"Siebenbürger, Valentin",German,1485,1564,ca. 1535,1510,1560,Steel,"L. 37 1/2 in. (95.2 cm); W. 11 3/4 in. (29.9 cm); Wt. 6 lb. 14 oz. (3,070 g)","Gift of William H. Riggs, 1913",,Nuremberg,,,,,,,,,,Armor for Horse,,http://www.metmuseum.org/art/collection/search/27181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.5.115c,false,true,26575,Arms and Armor,Breastplate,Breastplate,"German, Nuremberg",,,,,Armorer,,Valentin Siebenbürger,"German, Nuremberg, ca. 1510–1564, master in 1531",,"Siebenbürger, Valentin",German,1485,1564,ca. 1530–35,1505,1560,Steel,H. 15 in. (38 cm); W. 13 1/2 in. (34.5 cm),"John Stoneacre Ellis Collection, Gift of Mrs. Ellis and Augustus Van Horne Ellis, 1896",,Nuremberg,,,,,,,,,,Armor Parts-Breastplates,,http://www.metmuseum.org/art/collection/search/26575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"19.131.1a–r, t–w, .2a–c; 27.183.16",true,true,35775,Arms and Armor,Armor garniture,"Armor Garniture, Probably of King Henry VIII of England (reigned 1509–47)","British, Greenwich",,,,,Armorer|Designer,Made at the|Design of the decoration attributed to,Royal Workshops at Greenwich|Hans Holbein the Younger,"British, Greenwich, 1511–1640s|German, Augsburg 1497/98–1543 London",,"Royal Workshops at Greenwich|Holbein, Hans, the Younger",German,1511 |1497,1650 |1543,dated 1527,1527,1527,"Steel, gold, leather, copper alloys",H. 73 in. (185.4 cm); Wt. 62 lb. 12 oz. (28.45 kg),"Armor: Purchase, William H. Riggs Gift and Rogers Fund, 1919; mail brayette: Gift of Prince Albrecht Radziwill, 1927",,Greenwich,,,,,,,,,,Armor for Horse and Man,,http://www.metmuseum.org/art/collection/search/35775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1985.248a, b",false,true,24924,Arms and Armor,Crossbow (Halbe rüstung) with winder (cranequin),Crossbow (Halbe Rüstung) with Winder (Cranequin),"German, Dresden",,,,,Maker,Crossbow by,Johann Gottfried Hänisch the Elder,"German, Dresden 1696–1778",,"Hänisch the Elder, Johann Gottfried",German,1696,1778,dated 1742,1742,1742,"Steel, wood, staghorn, copper alloy, hemp, wool, gold, iron alloy","L. of crossbow, 28 13/16 in. (73.2 cm); W. of crossbow, 24 15/16 in. (63.3 cm); Wt. of crossbow, 10 lb. 7 oz. (4,817 g); L. of cranequin (without crank), 14 1/4 in. (36.2 cm); L. of crank, 10 3/4 in. (27.3 cm)","Purchase, Louis V. Bell Fund and Bequest of Stephen V. Grancsay, by exchange, 1985",,Dresden,Saxony,,,,,,,,,Archery Equipment-Crossbows,,http://www.metmuseum.org/art/collection/search/24924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"29.158.650a, b",false,true,23337,Arms and Armor,Crossbow (Halbe rüstung) with winder (Cranequin),Crossbow (Halbe Rüstung) with Winder (Cranequin),"crossbow, German, probably Dresden; winder, possibly German",,,,,Maker,Crossbow attributed to,Johann Gottfried Hänisch the Elder,"German, Dresden 1696–1778",,"Hänisch the Elder, Johann Gottfried",German,1696,1778,"crossbow, ca. 1720–30; winder, ca. 1575–1600",1550,1755,"Steel, wood (walnut), staghorn, copper alloy, hemp, leather, silk, gold, iron alloy, wool",crossbow: L. 26 15/16 in. (68.4 cm); W. 29 7/8 in. (75.8 cm); Wt. 13 lb. 15 oz. (6329 g); winder: L. 14 in. (35.5 cm); W. 4 1/8 in. (10.4 cm); Wt. 5 lb. 3 oz. (2341 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,probably Dresden,Saxony,,,,,,,,,Archery Equipment-Crossbows,,http://www.metmuseum.org/art/collection/search/23337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.315,false,true,35684,Arms and Armor,Light crossbow (Schnepper),Light Crossbow (Schnepper) from the Armory of Sedlitz Palace,"German, Dresden",,,,,Maker,,Johann Gottfried Hänisch the Elder,"German, Dresden, 1696–1778",,"Hänisch the Elder, Johann Gottfried",German,1696,1778,dated 1733,1733,1733,"Steel, wood (walnut), staghorn, hemp, wool, gold","L. 27 11/16 in. (70.3 cm); W. 22 1/2 in. (57.1 cm); Wt. 4 lb. 5 oz. (1,949 g)","Purchase, Arthur Ochs Sulzberger Gift, 2010",,Dresden,,,,,,,,,,Archery Equipment-Crossbows,,http://www.metmuseum.org/art/collection/search/35684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.429,false,true,35706,Arms and Armor,"Small crossbow (Schnepper), probably for a woman or child","Small Crossbow (Schnepper), Probably for a Woman or Child","German, Dresden",,,,,Maker,,Johann Gottfried Hänisch the Elder,"German, Dresden, 1696–1778",,"Hänisch the Elder, Johann Gottfried",German,1696,1778,dated 1738,1738,1738,"Steel, wood (fruitwood, probably plum or cherry), staghorn, hemp, wool, gold, horn",L. 22 7/32 in. (56.4 cm); W. 16 15/16 in. (43 cm); Wt. 1 lb. 14 1/2 oz. (866 g),"Purchase, Arthur Ochs Sulzberger Gift, 2011",,Dresden,Saxony,,,,,,,,,Archery Equipment-Crossbows,,http://www.metmuseum.org/art/collection/search/35706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.199,false,true,24858,Arms and Armor,Box for crossbow bolts (Bolzenkasten),"Box For Crossbow Bolts (Bolzenkasten), Probably Made for William IV, Duke of Bavaria (r. 1508–50)","German, Munich",,,,,Maker,,Hans Wagner the Elder,"German, Munich, recorded 1539–56",,"Wagner the Elder, Hans",German,1539,1556,dated 1539,1539,1539,"Wood (lid and front panel: fruitwood, possibly pear; bottom: walnut; inlay: possibly sycamore; later repairs: mahogany moldings and Indian rosewood veneers on sides), staghorn, iron, gold, paste, paper",H. 3 5/16 in. (8.4 cm); W. 16 in. (40.6 cm); D. 7 31/32 in. (20.2 cm),"Purchase, Bashford Dean Bequest, 1969",,Munich,,,,,,,,,,Archery Equipment,,http://www.metmuseum.org/art/collection/search/24858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.50.8,false,true,24689,Arms and Armor,Wheellock rifle,Wheellock Rifle,"German, Schwäbisch Gmünd",,,,,Maker,,Johann Michael Maucher,"German, Schwäbisch Gmünd, 1645–1701",,"Maucher, Johann Michael",German,1670,1701,ca. 1680–90,1655,1715,"Steel, wood (cherry), ivory, mother-of-pearl",L. 41 5/8 in. (105.7 cm); L. of barrel 30 13/16 in. (78.3 cm); L. of lock 7 1/8 in. (18.1 cm); Cal. .56 in. (14.2 mm); Wt. 7 lb. 11 oz. (3500 g),"Gift of Stephen V. Grancsay, 1942",,Schwäbisch Gmünd,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.156.47,false,true,23214,Arms and Armor,Close helmet for the tourney,Close Helmet for the Tourney,"Austrian, Innsbruck",,,,,Designer,Ornament copied from a design by,Daniel Hopfer,"German, Kaufbeuren 1471–1536 Augsburg",,"Hopfer, Daniel",German,1471,1536,dated 1552,1552,1552,"Steel, leather, brass",H. 10 1/2 in. (26.7 cm); W. 8 5/16 in. (21.1 cm); D. 12 1/4 in. (31.1 cm); Wt. 6 lb. 3 oz. (2803 g),"Bashford Dean Memorial Collection, Gift of Edward S. Harkness, 1929",,Innsbruck,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/23214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.370,false,true,26781,Arms and Armor,Glaive,"Glaive of Maximilian III Joseph, Prince-Elector of Bavaria (reigned 1745–77)",German,,,,,Decorator,Inscribed:,Jungwierth,"German, ca. 1770",,Jungwierth,German,1745,1795,dated 1771,1771,1771,"Steel, wood, gold, copper alloy, textile",L. 8 ft. 5 3/4 in. (258.4 cm); L. of head 27 1/2 in. (69.9 cm); W. 3 3/4 in. (9.5 cm); Wt. 7 lb. 0.9 oz. (3200.7 g),"Gift of William H. Riggs, 1913",,,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/26781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.8,false,true,24797,Arms and Armor,Hunting sword,Hunting Sword,German,,,,,Sword maker,,Johann Georg Klett,"German, 1720–1793",,Klett Johann Georg,German,1720,1793,ca. 1750,1725,1775,"Steel, silver, gold, bloodstone",L. 29 in. (73.7 cm),"Rogers Fund, 1949",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.702,false,true,22263,Arms and Armor,Armor,Armor,German,,,,,Armorer,Gorget plate of the helmet possibly marked by,Hans Michel,"German, Nuremberg 1539–1599",,"Michel, Hans",German,1539,1599,16th century and later,1500,1900,"Steel, brass, leather",Wt. 41 lb. 9 oz. (18.85 kg); Wt. of helmet 10 lb. 8 oz. (4.75 kg),"Gift of William H. Riggs, 1913",,,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.150.4b–t,false,true,23083,Arms and Armor,Armor,Armor,German,,,,,Etcher,Etched decoration on poleyns (knee defeneses) attributed to,Heilig Jörg,"German, active ca. 1490–1505",,"Jörg, Heilig",German,1465,1530,ca. 1500 and later,1475,1900,"Steel, leather",H. 71 in. (180.3 cm); Wt. 39 lb. 2 oz. (17.75 kg),"Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.434,false,true,647195,Arms and Armor,Hand-colored engraving,Ludi equestres. Das Thurnieren (The Tournament),German,,,,,Artist,,Martin Engelbrecht,"German, Augsburg 1684–1756 Augsburg",,"Engelbrecht, Martin",German,1684,1756,ca. 1730,1705,1755,"Paper, ink, polychromy",9 1/2 x 15 3/16 in. (241 x 385 mm),"Purchase, Kenneth and Vivian Lam Gift, 2014",,,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/647195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2012.204a, b",false,true,35859,Arms and Armor,Sword with scabbard,"Sword with Scabbard of Faustin I (1782–1867), Emperor of Haiti","British, Birmingham",,,,,Sword cutler,,Robert Mole,"British, Birmingham,1800–1856",,"Mole, Robert",British,1800,1856,1850,1825,1875,"Steel, silver, gold, wood, textile (velvet)",L. 39 1/2 in. (100.3 cm),"Bequest of William S. Delafield Sr., 2012",,Birmingham,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/35859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.15,false,true,35782,Arms and Armor,Helmet for a harquebusier,Helmet for a Harquebusier,"British, London or Greenwich",,,,,Armorer,Probably made in the,Royal Workshops at Greenwich,"British, Greenwich, 1511–1640s",,Royal Workshops at Greenwich,British,1511,1650,ca. 1630–40,1605,1665,"Steel, silver, gold, copper alloy, textile",H. 13 7/8 (35.3 cm); W. 9 3/8 (23.8 cm); D. 16 7/16 in. (41.7 cm); Wt. 4 lb. 7 oz. (2010 g),"Purchase, Arthur Ochs Sulzberger Gift, 2012",,probably Greenwich,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/35782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.180a–c,false,true,22768,Arms and Armor,Smallsword hilt with storage case,Smallsword Hilt with Storage Case,"British, Birmingham or London",,,,,Decorator,,"Rundell, Bridge and Rundell","British, London, active 1797–1843",,"Rundell, Bridge and Rundell",British,1797,1843,ca. 1785–1800,1760,1825,"Steel, wood, leather, silk",Hilt (a); L. including tang 8 1/4 in. (21 cm); L. excluding tang 6 15/16 in. (17.7 cm); W. 4 1/2 in. (11.4 cm); D. 3 1/2 in. (8.9 cm); Wt. 10 oz. (284.1 g); case (b); 5.2 oz. (147.4 g); trade card (c); 2 1/2 x 3 9/16 in. (6.4 x 9 cm),"Gift of Richard Hoe Lawrence, 1921",,London or Birmingham,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.145.311,false,true,22930,Arms and Armor,Smallsword,Smallsword,"British, probably London",,,,,Sword cutler,Attributed to,John Bland,"British, London, active ca. 1780–85",,"Bland, John",British,1755,1810,ca. 1780–85,1755,1810,"Gold, steel",L. 39 in. (99.1 cm); L. of blade 32 3/16 in. (81.7 cm); Wt. 1 lb. (454 g),"Gift of Jean Jacques Reubell, in memory of his mother, Julia C. Coster, and of his wife, Adeline E. Post, both of New York City, 1926",,London,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.113.1–.5; 29.158.885,true,true,27792,Arms and Armor,Harquebusier's armor with buff coat,"Harquebusier's Armor of Pedro II, King of Portugal (reigned 1683–1706) with Buff Coat","British, London; buff coat, European",,,,,Armorer|Armorer,Armor attributed to|Helmet cheek pieces and metal plates on shoulder straps made by,Richard Holden|Daniel Tachaux,"British, London, recorded 1658–1708|French, 1857–1928, active in France and America",,"Holden, Richard|Tachaux, Daniel",British,1658 |1857,1708 |1928,"ca. 1683 and later; buff coat, 17th–18th century",1601,1916,"Steel, gold, leather, textile",armor (15.113.1–.5): Wt. 43 lb. 5 oz. (19.6 kg); helmet: 14 x 11 x 15 1/4 in. (35.6 x 27.9 x 38.7 cm); Wt. 9 lb. 10 oz. (4354 g); breastplate: 18 1/2 x 16 5/16 x 7 13/16 in. (47 x 41.4 x 19.8 cm); Wt. 10 lb. 14 oz. (4944 g); backplate: 17 1/2 x 16 1/4 x 8 1/2 in. (44.5 x 41.3 x 21.6 cm); Wt. 11 lb. 5 oz. (5126 g); reinforcing breastplate: 17 3/8 x 16 3/16 x 6 13/16 in. (44.1 x 41.1 x 17.3 cm); Wt. 9 lb. 6 oz. (4264 g); bridle gauntlet: 5 1/2 x 19 7/16 x 6 3/4 in. (14 x 49.4 x 17.1 cm); Wt. 2 lb. 2 oz. (953 g); buff coat (29.158.885): L. 35 in. (88.9 cm),"Armor: Rogers Fund, 1915; buff coat: Bashford Dean Memorial Collection, Funds from various donors, 1929",,London,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/27792,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.98.1,false,true,24612,Arms and Armor,Terminal lame of a grand guard,"Terminal Lame of a Grand Guard from the Armor Garniture of George Clifford (1558–1605), Third Earl of Cumberland","British, Greenwich",,,,,Armorer,Made under the direction of,Jacob Halder,"British, master armorer at the royal workshops at Greenwich, documented in England 1558–1608",,"Halder, Jacob",British,1558,1608,1586,1586,1586,"Steel, gold",H. 10 1/4 in. (26.0 cm); W. 8 3/8 in. (21.3 cm),"Rogers Fund, 1936",,Greenwich,,,,,,,,,,Armor Parts,,http://www.metmuseum.org/art/collection/search/24612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.130.6a–y,true,true,23939,Arms and Armor,Armor garniture,"Armor Garniture of George Clifford (1558–1605), Third Earl of Cumberland","British, Greenwich",,,,,Armorer,Made under the direction of,Jacob Halder,"British, master armorer at the royal workshops at Greenwich, documented in England 1558–1608",,"Halder, Jacob",British,1558,1608,1586,1586,1586,"Steel, gold, leather, textile",H. 69 1/2 in. (176.5 cm); Wt. 60 lb. (27.2 kg),"Munsey Fund, 1932",,Greenwich,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.128.1a–p,false,true,22138,Arms and Armor,Field armor,Field Armor Probably of Sir John Scudamore (1541 or 1542–1623),"British, Greenwich",,,,,Armorer|Armorer,"Made under the direction of|Helmet, left pauldron, gauntlets, and right sabaton made by",Jacob Halder|Daniel Tachaux,"British, master armorer at the royal workshops at Greenwich, documented in England 1558–1608|French, 1857–1928, active in France and America","in The Metropolitan Museum of Art, Armor Workshop","Halder, Jacob|Tachaux, Daniel",British,1558 |1857,1608 |1928,"ca. 1587, restored and completed 1915",1562,1915,"Steel, gold, leather",Wt. 68 lb. 8 oz. (31.07 kg),"Frederick C. Hewitt Fund, 1911",,Greenwich|New York,New York,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.128.2a–n,false,true,22139,Arms and Armor,Armor,Armor of Sir James Scudamore (1558–1619),"British, Greenwich",,,,,Armorer|Armorer,"Breastplate, backplate, and gauntlets made by|Made under the direction of",Daniel Tachaux|Jacob Halder,"French, 1857–1928, active in France and America|British, master armorer at the royal workshops at Greenwich, documented in England 1558–1608","in the Metropolitan Museum of Art, Armor Workshop","Tachaux, Daniel|Halder, Jacob",British,1857 |1558,1928 |1608,ca. 1595–96,1570,1621,"Steel, gold, leather",H. 70 1/4 in. (178.5 cm); Wt. 50 lb. 7 oz. (22.88 kg),"Frederick C. Hewitt Fund, 1911",,Greenwich,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"11.128.1a–p; 22.147.4a, b, .11",false,true,35896,Arms and Armor,Field armor,Field Armor Probably of Sir John Scudamore (1541 or 1542–1623),"British, Greenwich",,,,,Armorer|Armorer,"Helmet, left pauldron, gauntlets, and right sabaton made by|Made under the direction of",Daniel Tachaux|Jacob Halder,"French, 1857–1928, active in France and America|British, master armorer at the royal workshops at Greenwich, documented in England 1558–1608","in The Metropolitan Museum of Art, Armor Workshop","Tachaux, Daniel|Halder, Jacob",British,1857 |1558,1928 |1608,"ca. 1587, restored and completed 1915",1562,1915,"Steel, gold, leather",Wt. 68 lb. 8 oz. (31.07 kg),"armor: Frederick C. Hewitt Fund, 1911; right thigh and knee defense, and left foot defense: Fletcher Fund, 1922",,Greenwich|New York,New York,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/35896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.90,false,true,35705,Arms and Armor,Mezzotint,"George Wallis, The Late Celebrated Antiquary and Gunsmith of Hull",British,,,,,Engraver,Engraved by,John Raphael Smith,"British, baptized Derby 1751–1812 Doncaster",after a painting by John Harrison,"Smith, John Raphael",British,1751,1812,"June 20, 1804",1804,1804,Mezzotint on white wove paper,Sheet: 17 3/4 x 14 7/8 in. (45 x 37.7 cm); plate: 15 x 11 in. (38 x 28 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2011",,,,,,,,,,,,Works on Paper-Engravings,,http://www.metmuseum.org/art/collection/search/35705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.427,false,true,35148,Arms and Armor,Sketchbook,Memorandum Book Showing Colored Sketches Mostly of European Helmets,British,,,,,Artist,,William Burges,1827–1881,,"Burges, William",British,1827,1881,ca. 1880,1880,1880,"Paper, leather, silver",H. 3 3/4 in. (9.5 cm); W. 2 3/8 in. (6 cm),"Purchase, Gift of Bashford Dean, by exchange, 2006",,,,,,,,,,,,Books & Manuscripts,,http://www.metmuseum.org/art/collection/search/35148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.465,false,true,28564,Arms and Armor,Medal,Medal Awarded to the Sword Makers Robert Mole and Sons,British,,,,,Artist,,William Wyon,"British, Birmingham 1795–1851 Brighton",,"Wyon, William",British,1795,1851,1851,1851,1851,Bronze,Diam. 1 3/4 in. (4.4 cm); thickness 5/16 in. (0.8 cm); Wt. 2.2 oz. (62.4 g),"Gift of Herbert G. Houze, 2005",,,,,,,,,,,,Miscellaneous-Coins and Medals,,http://www.metmuseum.org/art/collection/search/28564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.113.1–.5,false,true,22605,Arms and Armor,Harquebusier's armor,"Harquebusier's Armor of Pedro II, King of Portugal (reigned 1683–1706)","British, London",,,,,Armorer|Armorer,Attributed to|Helmet cheek pieces and metal plates on shoulder straps made by,Richard Holden|Daniel Tachaux,"British, London, recorded 1658–1708|French, 1857–1928, active in France and America",,"Holden, Richard|Tachaux, Daniel",British,1658 |1857,1708 |1928,ca. 1683 and later,1655,1916,"Steel, gold, leather, textile",Wt. 43 lb. 5 oz. (19.6 kg); helmet: 14 x 11 x 15 1/4 in. (35.6 x 27.9 x 38.7 cm); Wt. 9 lb. 10 oz. (4354 g); breastplate: 18 1/2 x 16 5/16 x 7 13/16 in. (47 x 41.4 x 19.8 cm); Wt. 10 lb. 14 oz. (4944 g); backplate: 17 1/2 x 16 1/4 x 8 1/2 in. (44.5 x 41.3 x 21.6 cm); Wt. 11 lb. 5 oz. (5126 g); reinforcing breastplate: 17 3/8 x 16 3/16 x 6 13/16 in. (44.1 x 41.1 x 17.3 cm); Wt. 9 lb. 6 oz. (4264 g); bridle gauntlet: 5 1/2 x 19 7/16 x 6 3/4 in. (14 x 49.4 x 17.1 cm); Wt. 2 lb. 2 oz. (953 g),"Rogers Fund, 1915",,London,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2006.471.1, .2",false,true,34183,Arms and Armor,Pair of pistols,Pair of Snaphaunce Pistols,"Italian, Bargi",,,,,Gunsmith,,"Matteo Cecchi, called Acquafresca","Italian, Bargi, 1651–1738",,"Cecchi, Matteo",Italian,1651,1738,ca. 1690,1690,1690,"Steel, silver, wood (ebony)",L. of each pistol 21 1/2 in. (54.7 cm); L. of each barrel 15 in. (38.2 cm); Cal. of each barrel 1/2 in. (13 mm),"Purchase, Arthur Ochs Sulzberger Gift, 2006",,Bargi,,,,,,,,,,Firearms-Pistols-Snaphaunce,,http://www.metmuseum.org/art/collection/search/34183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.202,false,true,21974,Arms and Armor,Helmet all'antica,Helmet all'Antica,"Italian, Milan",,,,,Armorer,Attributed to,Filippo Negroli,"Italian, Milan ca. 1510–1579",,"Negroli, Filippo",Italian,1485,1605,ca. 1532–35,1507,1560,Steel,H. 11 1/4 in. (85 cm); W. 8 1/4 in. (20.9 cm); D. 9 in. (22.7 cm); Wt. 2 lb. 2 oz. (964 g),"Rogers Fund, 1904",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/21974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.1720,true,true,22634,Arms and Armor,Burgonet,Burgonet,"Italian, Milan",,,,,Armorer,,Filippo Negroli,"Italian, Milan ca. 1510–1579",,"Negroli, Filippo",Italian,1485,1605,dated 1543,1543,1543,"Steel, gold, textile",H. 9 1/2 in. (24.1 cm); W. 7 5/16 in. (18.6 cm); D. 11 1/2 in. (29.2 cm); Wt. 4 lb. 2 oz. (1871 g),"Gift of J. Pierpont Morgan, 1917",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.53,false,true,22903,Arms and Armor,Close helmet,Close Helmet,"Italian, Milan",,,,,Armorer,Attributed to,Giovan Paolo Negroli,"Italian, Milan ca. 1513–1569",,"Negroli, Giovan Paolo",Italian,1488,1594,ca. 1540–45,1515,1570,"Steel, copper alloy, gold",H. 10 3/4 in. (27.3 cm); W. 11 1/2 in. (29.2 cm); D. 15 in. (38.1 cm); Wt. 6 lb. 8 oz. (3068 g),"Purchase, Rogers Fund and George D. Pratt Gift, 1926",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1855,false,true,22408,Arms and Armor,Breastplate,Breastplate,"Italian, Milan",,,,,Armorer,,Giovan Paolo Negroli,"Italian, Milan ca. 1513–1569",,"Negroli, Giovan Paolo",Italian,1488,1594,ca. 1540–45,1515,1570,"Steel, gold",W. 16 3/8 in. (41.6 cm); H. 23 1/4 in. (59.1 cm),"Gift of William H. Riggs, 1913",,Milan,,,,,,,,,,Armor Parts-Breastplates,,http://www.metmuseum.org/art/collection/search/22408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.603,false,true,22230,Arms and Armor,Close helmet with falling buffe,Close Helmet with Falling Buffe,"Italian, Milan",,,,,Armorer,Attributed to,Pompeo della Cesa,"Italian, Milan, ca. 1537–1610",,"Cesa, Pompeo della",Italian,1512,1635,ca. 1590–95,1565,1620,"Steel, gold",H. 11 in. (27.9 cm); W. 8 7/8 in. (22.5 cm); D. 12 1/2 in. (31.8 cm); Wt. 6 lb. 2 oz. (2778 g),"Gift of William H. Riggs, 1913",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.656,false,true,22239,Arms and Armor,Close helmet,Close Helmet,"Italian, Milan",,,,,Armorer,Attributed to,Pompeo della Cesa,"Italian, Milan, ca. 1537–1610",,"Cesa, Pompeo della",Italian,1512,1635,ca. 1585,1560,1610,"Steel, gold, brass",H. 11 5/8 in. (29.5 cm); W. 9 1/2 in. (24.1 cm); D. 11 in. (27.9 cm); Wt. 5 lb. 14 oz. (2665 g),"Gift of William H. Riggs, 1913",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.72a–d,false,true,25114,Arms and Armor,Portions of an armor,Portions of an Armor for Vincenzo Luigi di Capua (d. 1627),"Italian, Milan",,,,,Armorer,,Pompeo della Cesa,"Italian, Milan, ca. 1537–1610",,"Cesa, Pompeo della",Italian,1512,1635,ca. 1595,1570,1620,"Steel, gold, leather, copper alloy",H. as mounted 19 in. (48 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2001",,Milan,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/25114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.72; 2013.618,false,true,642980,Arms and Armor,Portions of an armor,Portions of an Armor for Vincenzo Luigi di Capua (d. 1627),"Italian, Milan",,,,,Armorer,,Pompeo della Cesa,"Italian, Milan, ca. 1537–1610",,"Cesa, Pompeo della",Italian,1512,1635,ca. 1595,1570,1620,"Steel, gold, leather, copper alloy",H. as mounted 19 in. (48 cm),"Gorget, pauldrons, and breastplate: Purchase, Arthur Ochs Sulzberger Gift, 2001; Backplate: Purchase, Gift in honor of Maximilian and Alexander Saga, 2013",,Milan,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/642980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.618,false,true,35960,Arms and Armor,Backplate,Backplate of an Armor for Vincenzo Luigi di Capua (d. 1627),"Italian, Milan",,,,,Armorer,,Pompeo della Cesa,"Italian, Milan, ca. 1537–1610",,"Cesa, Pompeo della",Italian,1512,1635,ca. 1595,1570,1620,"Steel, gold, leather, copper alloy",H. approx. 15 in. (38 cm); W. approx. 11 in. (28 cm),"Purchase, Gift in honor of Maximilian and Alexander Saga, 2013",,Milan,,,,,,,,,,Armor Parts-Backplates,,http://www.metmuseum.org/art/collection/search/35960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"28.196.15, .16",false,true,23049,Arms and Armor,Pair of pistols,Pair of Pistols with Flintlocks alla Fiorentina,"Italian, Pistoia",,,,,Gunsmith,Attributed to,Cristiano Leoni,"Italian, Pistoia, active ca. 1780",,"Leoni, Cristiano",Italian,1755,1805,ca. 1750–75,1725,1800,"Steel, wood",L. of each 18 3/8 in. (46.7 cm); L. of each barrel 13 3/8 in. (33.9 cm); Cal. of each .46 in. (11.7 mm); Wt. of each 1 lb. 8 oz. (680 g),"Rogers Fund, 1928",,Pistoia,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/23049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.162,false,true,21964,Arms and Armor,Wheellock gun,Wheellock gun,"Italian, Brescia",,,,,Barrelsmith,,Cominazzo workshop,"Italian, Brescia, mid-17th century",,Cominazzo workshop,Italian,1625,1675,mid-17th century,1625,1675,"Steel, wood (walnut)",L. 45 1/2 in. (115.6 cm); L. of barrel 32 3/4 in. (83.2 cm); Cal. .58 in. (14.7 mm); Wt. 9 lb. 6 oz. (4250 g),"Rogers Fund, 1904",,Brescia,,,,,,,,,,Firearms-Guns-Wheellock,,http://www.metmuseum.org/art/collection/search/21964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"04.3.23a, b",false,true,21935,Arms and Armor,Rapier with scabbard,Rapier with Scabbard,French or Italian,,,,,Goldsmith,Decoration attributed to,Gasparo Mola,"Italian, Coldre ca. 1580–1640 Rome",,"Mola, Gasparo",Italian,1575,1640,ca. 1620–30,1595,1655,"Steel, gold, silver, leather, wood",L. 49 1/4 in. (125.1 cm); W. 8 3/4 in. (22.2 cm); Wt. of scabbard 7.5 oz. (212.6 g),"Rogers Fund, 1904",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/21935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.128.1–.2,false,true,22740,Arms and Armor,Pair of gauntlets for a child,Pair of Gauntlets for a Child,"Italian, Milan",,,,,Armorer,,Lucio Piccinino,"Italian, Milan, active ca. 1575–90",,"Piccinino, Lucio",Italian,1550,1615,ca. 1585,1560,1610,"Steel, gold, silver",L. of each 7 1/8 in. (18.1 cm); W. of each 4 1/2 in. (12.4 cm); D. of each 4 3/8 in. (11.1 cm); Wt. of each 8 oz. (226.8 g),"Rogers Fund, 1919",,Milan,,,,,,,,,,Armor Parts-Gauntlets,,http://www.metmuseum.org/art/collection/search/22740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.270a–o,false,true,21996,Arms and Armor,Half armor,"Half Armor Probably Made for Don Gonzalo Fernández de Córdoba y Fernández de Córdoba, Duke of Sessa (1520/1524–1578)","Italian, Milan",,,,,Armorer,Attributed to,Lucio Piccinino,"Italian, Milan, active ca. 1575–90",,"Piccinino, Lucio",Italian,1550,1615,ca. 1560 and later,1535,1900,"Steel, gold",Wt. approx. 30 lb. 7 oz. (13.8 kg); Wt. of helmet approx. 7 lb. 11 oz. (,"Rogers Fund, 1904",,Milan,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/21996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.714a–h, j",false,true,22184,Arms and Armor,Armor,Armor of the Dukes of Alba,"Italian, Milan",,,,,Armorer,,Lucio Piccinino,"Italian, Milan, active ca. 1575–90",,"Piccinino, Lucio",Italian,1550,1615,ca. 1575–85,1550,1610,"Steel, gold, silver","Wt. 25 lbs. 12 oz. (11.68 kg); Helmet (a) H. 12 in. (30.5 cm); W. 8 1/2 in. (21.6 cm); D. 11 1/2 in. (29.2 cm); Wt. 5 lbs. 1 oz. (2,296.31 g); Colletin (b) H. 7 in. (17.8 cm); W. 11 7/16 in. (29.1 cm); Wt. 2 lbs. 2 oz. (963.88 g); Breastplate (c) H. 18 1/8 in. (46.0 cm); W. 15 in. (38.1 cm); D. 7 7/8 in. (19.9 cm); Wt. 5 lbs. 4 oz (2,381.36 g); Backplate (d) H. 14 5/8 in. (37.2 cm); W. 14 5/8 in. (37.2 cm); Wt. 3 lbs. 3 oz. (1,445.83 g); Pauldron right (e) H. 10 1/2 in. (26.7 cm); W. 11 5/8 in. (29.5 cm); Wt. 2 lbs. 9 oz. (1,162.33 g); Pauldron left (f) H. 10 3/8 in. (26.4 cm); W. 11 in. (27.9 cm); Wt. 2 lbs. 12 oz. (1,247.38 g); Arm Defense right (g) L. 18 1/2 in. (46.9 cm); Wt. 2 lbs. 7 oz. (1,105.63 g); Arm Defense left (h) L. 18 1/4 in. (46.4 cm); Wt. 2 lbs. 6 oz. (1,077.28 g)","Gift of William H. Riggs, 1913",,Milan,,,,,,,,,,Armor for Man-1/2 Armor,,http://www.metmuseum.org/art/collection/search/22184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.89.2,false,true,22135,Arms and Armor,Cup-hilted rapier,Cup-Hilted Rapier,"Italian, Milan",,,,,Sword maker,Cup signed,Carlo Piccinino,"Italian, Milan, active ca. 1650–75",,"Piccinino, Carlo",Italian,1625,1700,ca. 1650–75,1625,1700,"Steel, iron wire",L. 44 in. (111.8 cm); L. of blade 37 1/4 in. (94.6 cm); W. 12 1/16 in. (30.6 cm); Wt. 2 lb. 5 oz. (1048.9 g),"Gift of J. Pierpont Morgan, 1911",,Milan,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.150.9aa,false,true,35853,Arms and Armor,Shaffron (Horse's head defense),Shaffron (Horse's Head Defense),"Italian, possibly Brescia",,,,,Armorer,Stamped with marks attributed to,Ambrogio de Osma,"Italian, Brescia, documented 1446–75",,"Osma, Ambrogio de",Italian,1446,1475,ca. 1460–70,1435,1495,Steel,H. 21 1/2 in. (54.6 cm); W. 9 1/4 in. (23.5 cm); Wt. 1 lb. 15 oz. (878.8 g),"Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,possibly Brescia,,,,,,,,,,Equestrian Equipment-Shaffrons,,http://www.metmuseum.org/art/collection/search/35853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"28.196.17, .18",false,true,23048,Arms and Armor,Pair of flintlock pistols,Pair of Flintlock Pistols,"Italian, Brescia",,,,,Gunsmith,,Girolamo Francino,"Italian, Brescia, recorded 1666–1709",,"Francino, Girolamo",Italian,1666,1709,ca. 1650–60,1625,1685,"Steel, wood (walnut)",L. of each 22 1/2 in. (57.2 cm); L. of each barrel 15 13/16 in. (40.2 cm); Cal. of each .52 in. (13.2 mm); Wt. of each 1 lb. 15 oz. (879 g),"Rogers Fund, 1928",,Brescia,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/23048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.1e–f,false,true,26584,Arms and Armor,Arm defenses,Arm Defenses,"Italian, Milan",,,,,Armorer,Both arms marked by,Domenico Negroli,"Italian, Milan, active 1492–ca. 1516",,"Negroli, Domenico",Italian,1492,1531,ca. 1510,1485,1535,Steel,Wt. of right arm (e) 2 lb. 10 oz. (1190.7 g); Wt. of left arm (f) 2 lb. 6 oz. (1077.3 g),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,Milan,,,,,,,,,,Armor Parts-Arms & Shoulders,,http://www.metmuseum.org/art/collection/search/26584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.598,false,true,22227,Arms and Armor,Close-helmet,Close-Helmet for the Tournament on Foot,"Italian, Milan",,,,,Armorer,,the Master of the Castle Mark,"Italian, Milan, active ca. 1590–1620",,"Master of the Castle Mark, the",Italian,1565,1645,ca. 1600–1610,1575,1635,"Steel, gold",H. 13 in. (33 cm); W. 9 1/2 in. (24.1 cm); D. 11 9/16 in. (29.4 cm); Wt. 11 lb. 13 oz. (5360 g),"Gift of William H. Riggs, 1913",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.605a,false,true,22232,Arms and Armor,Close helmet for foot combat,Close Helmet for Foot Combat,"Italian, Milan",,,,,Armorer,Attributed to,the Master of the Castle Mark,"Italian, Milan, active ca. 1590–1620",,"Master of the Castle Mark, the",Italian,1565,1645,ca. 1600–1610,1575,1635,"Steel, gold, leather",H. 12 1/2 in. (31.8 cm); W. 9 3/8 in. (23.8 cm); D. 12 1/4 in. (31.1 cm); Wt. 14 lb. 7 oz. (6450 g),"Gift of William H. Riggs, 1913",,Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.154,false,true,639897,Arms and Armor,Backplate,Backplate,"Italian, Milan",,,,,Armorer,Probably by,Francesco Negroli,"Italian, Milan, died before December 1519",,"Negroli, Francesco",Italian,1450,1519,ca. 1505–10,1480,1535,Steel,19 3/8 x 15 1/4 x 6 3/4 in. (49.3 x 39 x 17.3 cm); Wt. 5 lb. 6 oz. (2461 g),"Purchase, Arthur Ochs Sulzberger Gift, 2014",,Milan,,,,,,,,,,Armor Parts-Backplates,,http://www.metmuseum.org/art/collection/search/639897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1662,false,true,27460,Arms and Armor,Crinet,Crinet,Franco-Italian,,,,,Armorer,Attributed to,Romain des Ursins,"Italian, Milan, recorded in Lyon 1493–95",,"des Ursins, Romain",Italian,1493,1495,"ca. 1480–95; bottom lame and mail fringe, restored ca. 1863–1900",1455,1900,Steel,"L. 33 7/8 in. (85.9 cm); W. 6 7/8 in. (17.5 cm); Wt. 9 lb. 10 oz. (4,360 g)","Gift of William H. Riggs, 1913",,,,,,,,,,,,Armor for Horse,,http://www.metmuseum.org/art/collection/search/27460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.3.253,false,true,25403,Arms and Armor,Shaffron (horse's head defense),"Shaffron (Horse's Head Defense) of Henry II of France, When Dauphin",Franco-Italian,,,,,Armorer,Attributed to,Romain des Ursins,"Italian, Milan, recorded in Lyon 1493–95",,"des Ursins, Romain",Italian,1493,1495,"ca. 1490–1500, redecorated 1539",1465,1539,"Steel, gold, brass",H. 27 1/2 in. (69.8 cm); W. 15 in. (38.1 cm); Wt. 5 lb. 3 oz. (2350 g),"Rogers Fund, 1904",,,,,,,,,,,,Equestrian Equipment-Shaffrons,,http://www.metmuseum.org/art/collection/search/25403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1661a,false,true,26921,Arms and Armor,Shaffron (Horse's head defense),Shaffron (Horse's Head Defense),Franco-Italian,,,,,Armorer,Attributed to,Romain des Ursins,"Italian, Milan, recorded in Lyon 1493–95",,"des Ursins, Romain",Italian,1493,1495,"ca. 1480–95; ear guards, eye guards, and plume holder, 19th century restorations",1455,1900,"Steel, textile",H. 24 7/8 (63.2 cm); W. 13 in. (33 cm); D. 9 1/2 in. (24.1 cm); Wt. 4 lb. 4 oz. (1918 g),"Gift of William H. Riggs, 1913",,,,,,,,,,,,Equestrian Equipment-Shaffrons,,http://www.metmuseum.org/art/collection/search/26921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"14.25.1661a, .1662",false,true,35725,Arms and Armor,Shaffron and crinet (Horse's head and neck defenses),Shaffron and Crinet (Horse's Head and Neck Defenses),Franco-Italian,,,,,Armorer,Attributed to,Romain des Ursins,"Italian, Milan, recorded in Lyon 1493–95",,"des Ursins, Romain",Italian,1493,1495,"ca. 1480–95, with 19th century restorations",1455,1900,"Steel, textile",14.25.1661a (shaffron); H. 24 7/8 in. (63.2 cm); W 13 in. (33 cm); D. 9 1/2 in. (24.1 cm); Wt. 4 lb. 4 oz. (1918 g); 14.25.1662 (crinet); L. 33 7/8 in. (85.9 cm.); W. 6 7/8 in. (17.5 cm); Wt. 9 lb. 10 oz. (4360 g),"Gift of William H. Riggs, 1913",,,,,,,,,,,,Equestrian Equipment-Shaffrons,,http://www.metmuseum.org/art/collection/search/35725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.50.2,true,true,24686,Arms and Armor,Armet,Armet,"Italian, probably Milan",,,,,Armorer,"Stamped with the armorer's name,",LIONARDO,"Italian, probably active in Milan, ca. 1440",,LIONARDO,Italian,1415,1465,ca. 1440,1415,1465,"Steel, copper alloy",H. 10 3/8 in. (26.3 cm); W. 8 1/4 in. (21 cm); D. 11 in. (27.9 cm); Wt. 9 lb. 4 oz. (4196 g),"Gift of Stephen V. Grancsay, 1942",,probably Milan,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/24686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.23,false,true,24837,Arms and Armor,Trigger guard of a gun,Trigger Guard of a Gun made for a Prince of the House of the Medici,"Italian, Reggio Emilia",,,,,Steel-chiseler,,Pietro Ancini,"Italian, Reggio Emilia, recorded 1616–1702",,"Ancini, Pietro",Italian,1616,1702,dated 1643,1643,1643,Steel,L. 8 1/4 in. (21 cm),"Purchase, Bashford Dean Bequest, 1956",,Reggio Emilia,,,,,,,,,,Firearms Parts,,http://www.metmuseum.org/art/collection/search/24837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.50.14,false,true,24692,Arms and Armor,Barbute,Barbute,"Italian, Milan",,,,,Armorer,,Bernardino da Carnago,"Italian, active in Milan and Naples, ca. 1475",,Carnago Bernardino da,Italian,1450,1500,ca. 1475,1450,1500,Steel,H. 12 1/8 in. (30.8 cm); W. 8 1/8 in. (20.6 cm); D. 10 3/4 in. (27.3 cm); Wt. 6 lb. 8 oz. (2948 g),"Gift of Stephen V. Grancsay, 1942",,,Milan,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/24692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.579,false,true,22223,Arms and Armor,Barbute,Barbute,"Italian, Brescia",,,,,Armorer,,Pietro da Castello,"Italian, Brescia, documented 1469–86, died before 1498",,"Castello, Pietro da",Italian,1469,1498,ca. 1470–80,1445,1505,Steel,H. 10 1/4 in. (26 cm); W. 7 1/2 in. (19.1 cm); D. 10 in. (25.4 cm); Wt. 4 lb. 14 oz. (2211 g),"Gift of William H. Riggs, 1913",,Brescia,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.50.16,true,true,24693,Arms and Armor,Shield,Shield,"Italian, probably Bologna",,,,,Maker,Attributed to,Girolamo da Treviso,"Italian, Treviso ca. 1498–1544 Boulogne-sur-Mer",,"Treviso, Girolamo da",Italian,1498,1544,ca. 1535,1510,1560,"Wood, linen, gesso, gold leaf, polychromy",Diam. 24 5/8 in. (62.53 cm),"Gift of Stephen V. Grancsay, 1942",,probably Bologna,,,,,,,,,,Shell,,http://www.metmuseum.org/art/collection/search/24693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.251,false,true,27136,Arms and Armor,Drawing,Design for the Hilt of a Small-Sword,"Italian, Rome",,,,,Artist,Workshop of,Luigi Valadier,"Italian, Rome 1726–1785 Rome",,"Valadier, Luigi",Italian,1726,1785,ca. 1780–90,1755,1815,"Pen, ink, wash, graphite, vellum",9 5/8 x 6 5/8 in. (24.4 x 16.8 cm),"Rogers Fund, 1991",,Rome,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/27136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1870,false,true,436343,Arms and Armor,Painting,"Alfonso II d'Este (1533–1597), Duke of Ferrara","Italian, Ferrara",,,,,Artist,,Italian (Ferrarese) Painter,late 16th century,,Italian (Ferrarese) Painter,Italian,1400,1499,late 16th century,1570,1599,Oil on canvas,47 x 35 3/4 in. (119.4 x 90.8 cm),"Gift of William H. Riggs, 1913",,Ferrara,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.25.1874,false,true,436729,Arms and Armor,Painting,"Alfonso I d'Este (1476–1534), Duke of Ferrara","Italian, Ferrara",,,,,Artist,,Italian (Ferrarese) Painter,second quarter 16th century,,Italian (Ferrarese) Painter,Italian,1400,1499,second quarter 16th century,1525,1550,Oil on canvas,52 7/8 x 38 1/4 in. (134.3 x 97.2 cm),"Gift of William H. Riggs, 1913",,Ferrara,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.234,false,true,24956,Arms and Armor,Design for a saddle plate,Design for the Pommel Plate of a Saddle from a Garniture of Alessandro Farnese (1545–1592),"Italian, Parma",,,,,Designer,,Andrea Casalini,"Italian, Parma, died 1597",,"Casalini, Andrea",Italian,1500,1597,ca. 1575–80,1575,1580,"Pen and brown ink, with color washes, on paper",19 1/2 x 16 3/8 in. (49.5 x 39.2 cm),"Purchase, Fletcher Fund and Gift of William H. Riggs, by exchange, 1993",,Parma,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/24956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.89.5,false,true,22136,Arms and Armor,Sallet,Sallet,Italian,,,,,Armorer,Attributed to,Pietro Giacomo da Castello,"Italian, documented 1485–1525",,"Castello, Pietro Giacomo da",Italian,1485,1525,ca. 1510–20,1485,1545,Steel,"H. without crest, 11 1/2 in. (29.2 cm); W. 7 5/8 in. (19.4 cm); D. 10 1/2 in. (26.7 cm); Wt. 4 lb. 3 oz. (1899 g)","Gift of J. Pierpont Morgan, 1911",,,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/22136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.156.77,false,true,25367,Arms and Armor,Medal,"Medal of Vicenzo II Gonzaga, 7th Duke of Mantua",Italian,,,,,Artist,,Gasparo Morone,"Italian, born Milan (?), died Rome, 1669",,"Morone, Gasparo",Italian,1569,1669,ca. 1625,1625,1625,"Bronze, gold",Diam. 1 11/16 in. (4.3 cm); thickness 1/8 in. (0.3 cm); Wt. 0.8 oz. (22.7 g),"Bashford Dean Memorial Collection, Gift of Edward S. Harkness, 1929",,,,,,,,,,,,Miscellaneous,,http://www.metmuseum.org/art/collection/search/25367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"22.19a, b",true,true,22769,Arms and Armor,Presentation sword with scabbard,Presentation Sword and Scabbard of Brigadier General Daniel Davis (1777–1814) of the New York Militia,"American, New York",,,,,Silversmith,,John Targee,"American, ca. 1774–1850",,"Targee, John",American,1769,1850,ca. 1815–17,1790,1842,"Steel, gold, silver",L. 37 1/4 in. (94.6 cm),"Gift of Francis P. Garvan, 1922",,New York,New York,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.8a–c,true,true,35650,Arms and Armor,Sword with scabbard,Congressional Presentation Sword and Scabbard of Major General John E. Wool (1784–1869),"American, probably Baltimore",,,,,Sword cutler,,Samuel Jackson,"American, Baltimore, active 1833–70",,"Jackson, Samuel",American,1833,1870,1854–55,1854,1855,"Steel, gold, brass, diamonds, rubies",L. with scabbard 39 3/16 in. (99.6 cm); L. without scabbard 38 13/16 in. (98.5 cm); L. of blade 31 7/16 in. (79.9 cm); W. 5 5/8 in. (14.3 cm),"Purchase, Arthur Ochs Sulzberger and Mr. and Mrs. Robert G. Goelet Gifts, 2009",,Baltimore,Maryland,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/35650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.131.7,false,true,24649,Arms and Armor,Powder horn,Powder Horn,Colonial American,,,,,Engraver,,Jacob Gay,"American, New York, recorded 1758–87",,"Gay, Jacob",American,1758,1887,dated 1759,1759,1759,"Horn (cow), wood",L. 13 in. (33 cm); Diam. 3 1/2 in. (8.9 cm); Wt. 14.8 oz. (419.6 g),"The Collection of J. H. Grenville Gilbert, of Ware, Massachusetts, Gift of Mrs. Gilbert, 1937",,,New York,,,,,,,,,Firearms Accessories-Powder Horns,,http://www.metmuseum.org/art/collection/search/24649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.127,false,true,679351,Arms and Armor,Sword,Cavalry Officer's Saber,"American, Baltimore",,,,,Silversmith,Hilt by,John Lynch,"American, Baltimore 1761–1848 Baltimore",,"Lynch, John",American,1761,1848,ca. 1810,1785,1835,"Steel, silver, wood, gold",H. 39 7/8 in. (101.3 cm); H. of blade 34 1/4 in. (87 cm); W. 3 3/4 in. (9.5 cm); Wt. 1 lb. 9.6 oz. (725.7 g),"Purchase, Arthur Ochs Sulzberger Bequest, 2015",,Baltimore,Maryland,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/679351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.336,true,true,24960,Arms and Armor,Percussion revolver,"Colt Third Model Dragoon Percussion Revolver, Serial Number 12406","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,ca. 1853,1828,1878,"Steel, brass, gold, wood (walnut)",L. 14 in. (35.6 cm); L. of barrel 7 1/2 in. (19.1 cm); Cal. .44 in. (11.2 mm); case; H. 3 in. (7.6 cm); W. 16 3/16 in. (41.1 cm); D. 8 1/8 in. (20.6 cm); Wt. 3 lb. 9.8 oz. (224 g),"Gift of George and Butonne Repaire, 1995",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.206.1,false,true,24835,Arms and Armor,Percussion revolver,"Colt Paterson Percussion Revolver, No. 5, Holster Model, serial no. 528","American, Paterson, New Jersey",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,ca. 1838–40,1813,1865,"Steel, silver, ivory",L. 16 3/4 in. (42.55 cm); L. of barrel 12 in. (30.48 cm); Cal. .40 in. (10.2 mm),"Gift of John E. Parsons, 1955",,Paterson,New Jersey,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.206.2,false,true,24836,Arms and Armor,Percussion revolver,"Colt Model 1861 Navy Percussion Revolver, serial no. 12240","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,1863,1863,1863,"Steel, brass, wood (walnut)",L. 12 3/4 in. (32.39 cm); L. of barrel 7 1/2 in. (19.05 cm); Cal. .36 in. (9.1 mm),"Gift of John E. Parsons, 1955",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.167.2,false,true,24839,Arms and Armor,Percussion revolver,"Colt Dragoon Percussion Revolver, Third Model, serial no. 12403","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,1852,1852,1852,"Steel, brass, silver, wood (walnut)",L. 14 in. (35.56 cm); L. of barrel 7 1/2 in. (19.05 cm); Cal. .44 in. (11 mm),"Gift of John E. Parsons, 1956",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.166.1,false,true,24841,Arms and Armor,Percussion revolver,"Colt Paterson Percussion Revolver, No. 5, Holster Model, serial no. 940","American, Paterson, New Jersey",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,ca. 1840,1815,1865,"Steel, silver, mother-of-pearl",L. 14 in. (35.56 cm); L. of barrel 9 in. (22.86 cm); Cal. .40 in. (10 mm),"Gift of John E. Parsons, 1957",,Paterson,New Jersey,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.171.3,false,true,24840,Arms and Armor,Revolver,"Colt Model 1851 Navy Revolver with Thuer Conversion for Self-Contained Cartridges, Serial no. 27060","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,"1853; converted for cartidges, ca. 1868–71",1853,1896,"Steel, brass, silver, ivory",L. 13 in. (33.02 cm); L. of barrel 7 1/2 in. (19.05 cm); Cal. .36 in. (9 mm),"Gift of John E. Parsons, 1958",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.143.4,false,true,24848,Arms and Armor,Revolver,"""Peacemaker"" Colt Single-Action Army Revolver, serial no. 4519","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,1874,1874,1874,"Steel, iron, wood",L.13 in. (33.02 cm); L. of barrel 7 1/2 in. (19.05 cm); Cal. .45 in. (11 mm),"Gift of John E. Parsons, 1959",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.157.2,false,true,24855,Arms and Armor,Percussion revolver,"Colt Model 1851 Navy Percussion Revolver, serial no. 2","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,1850,1850,1850,"Steel, brass, silver, wood (walnut)",L. of barrel 7 1/2 in. (19.05 cm); Cal. .38 in. (10 mm),"Gift of John E. Parsons, 1968",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.442a–o,false,true,24892,Arms and Armor,Percussion revolver with case and accessories,"Cased Colt Model 1860 Army Percussion Revolver, Serial no. 7569, with Accessories","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,1861,1861,1861,"Steel, brass, silver, gold, wood (mahogany), textile",L. 14 1/2 in. (36.83 cm); L. of barrel 8 in. (20.32 cm); Cal. .44 in. (11 mm),"Gift of Mr. and Mrs. Jerry D. Berger, 1983",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.167.1a–g,false,true,24838,Arms and Armor,Percussion revolver with case and accessories,"Colt Paterson Pocket Percussion Revolver, Fourth Ehlers Model, serial no. 152, with Case and Accessories","American, Paterson, New Jersey",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,ca. 1840–43,1815,1868,"Steel, wood (walnut)",Revolver (a); L. 6 1/2 in. (16.5 cm); L. of barrel 3 in. (7.6 cm); Cal. .28 in. (7.1 mm); bullet mould (b); L. 3 3/16 in. (8.1 cm); W. (when closed) 1 3/4 in. (4.5 cm); Wt. 2.2 oz. (62.4 g); primer (c); L. 3 11/16 in. (9.4 cm); Diam. 1 1/8 in. (2.8 cm); Wt. 3.3 oz. (93.6 g); cleaning rod (d); L. 4 5/8 in. (11.7 cm); Wt. 1.2 oz. (34 g); spanner-screwdriver (e); L. 2 5/8 in. (6.7 cm); W. 2 in. (5.1 cm); Wt. 0.9 oz. (25.5 g); case (f); H. 2 in. (5.1 cm); W. 10 in. (25.4 cm); D. 5 7/8 in. (14.9 cm); Wt. 1 lb. 0.8 oz. (476.3 g); key (g); L. 1 1/8 in. (2.8 cm),"Gift of John E. Parsons, 1956",,Paterson,New Jersey,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.166.2a–e,false,true,24842,Arms and Armor,Percussion revolver with case and accessories,"Colt Model 1855 Pocket Percussion Revolver, Serial no. 4460, with Case and Accessories","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,1855,1855,1855,"Steel, wood (oak)",L. 7 7/8 in. (19.99 cm); L. of barrel 3 1/2 in. (8.89 cm); Cal. .28 in. (7 mm); case 9 3/8 x 5 3/4 x 1 7/8 in. (23.8 x 14.61 x 4.75 cm); L. of spanner-screwdriver 3 1/4 in. (8.26 cm); greatest W. of spanner-screwdriver 1 3/8 in. (3.48 cm); L. of primer 4 5/8 in. (11.73 cm); greatest W. of primer 2 in. (5.08 cm); L. of bullet mould 4 3/8 in. (11.1 cm),"Gift of John E. Parsons, 1957",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.143.1a–h,false,true,24846,Arms and Armor,Percussion revolver with case and accessories,"Colt Paterson Percussion Revolver, No. 3, Belt Model, Serial no. 156, with Case and Accessories","American, Paterson, New Jersey",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,ca. 1838,1813,1863,"Steel, silver, brass, wood (walnut), copper, velvet","Revolver (a); L. 9 5/8 in. (24.4 cm); L. of barrel 5 1/2 in. (14 cm); Cal. .34 in. (8 mm); extra cylinder (b); L. 1 1/4 in. (3.2 cm); Diam. 1 1/4 in. (3.2 cm); Wt. 4.2 oz. (119.1 g); combination patch, powder and ball cylinder (c); L. 5 3/8 in. (13.6 cm); Diam. 1 5/16 in. (3.3 cm); Wt. 12.2 oz. (345.9 g); bullet mould (d); L. 3 11/16 in. (9.4 cm); W. 1 1/4 in. (3.2 cm); Wt. 2.2 oz. (62.4 g); combination screwdriver and pricker (e); L. 4 11/16 in. (11.9 cm); Wt. 1.7 oz. (48.2 g); percussion cap box (f); L. 3 5/16 in. (8.4 cm); Diam. 1 13/16 in. (4.6 cm); Wt. 3.1 oz. (87.9 g); cleaning rod (g); L. 6 7/8 in. (17.5 cm); Wt. 1.1 oz. (31.2 g); case (h); H. 2 1/8 in. (5.4 cm); W. 10 3/8 in. (26.3 cm); D. 7 in. (17.8 cm); Wt. 2 lb. 3.3 oz. (1000.7 g)","Gift of John E. Parsons, 1959",,Paterson,New Jersey,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.125,false,true,24794,Arms and Armor,Percussion revolver,"Colt Dragoon Percussion Revolver, Third Model, serial no. 13096","American, Hartford, Connecticut",,,,,Manufacturer,,Samuel Colt,"American, Hartford, Connecticut 1814–1862",,"Colt, Samuel",American,1814,1862,1853,1853,1853,"Steel, brass, silver, wood (walnut)",L. 14 3/4 in. (37.47 cm); L. of barrel 7 1/2 in. (19.05 cm); Cal. .45 in. (11.4 mm),"Purchase, Friends of Albert Foster Jr. Gifts, in his memory, 1948",,Hartford,Connecticut,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/24794,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2012.105a, b",false,true,35845,Arms and Armor,Sword with scabbard,Sword and Scabbard of Captain Richard French,"American, Chicopee, Massachusetts",,,,,Manufacturer,,Ames Manufacturing Company,"American, Chicopee, Massachusetts, 1829–1935",,Ames Manufacturing Company,American,1829,1935,1850,1825,1875,"Gold, brass, steel",L. 40 in. (101.6 cm); L. of blade 32 1/4 in. (81.9 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2012",,Chicopee,Massachusetts,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/35845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.5.36,false,true,21921,Arms and Armor,Flintlock duelling pistol,Flintlock Duelling Pistol,"American, Middletown, Connecticut",,,,,Gunsmith,,Simeon North,"American, Middletown, Connecticut, 1765–1852",,North Simeon,American,1765,1852,ca. 1815–20,1790,1845,"Steel, gold, silver, wood (walnut, hickory), horn",L. 15 3/4 in. (40.01 cm); L. of barrel 10 in. (25.4 cm); Cal. .56 in. (14.2 mm); Wt. 2 lb. 2 oz. (964 g),"John Stoneacre Ellis Collection, Gift of Mrs. Ellis and Augustus Van Horne Ellis, 1896",,Middletown,Connecticut,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/21921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"96.5.36, .149",false,true,35815,Arms and Armor,Pair of flintlock duelling pistols,Pair of Flintlock Duelling Pistols,"American, Middletown, Connecticut",,,,,Gunsmith,,Simeon North,"American, Middletown, Connecticut, 1765–1852",,North Simeon,American,1765,1852,ca. 1815–20,1790,1845,"Steel, gold, silver, wood (walnut, hickory), horn","L. of each 15 3/4 in. (40 cm); L. of barrel of each 10 in. (25.4 cm); Cal. of each .56 in. (14.2 mm); Wt. of 96.5.36, 2 lb. 2 oz. (964 g); Wt. of 96.5.149, 2 lb. (907 g)","John Stoneacre Ellis Collection, Gift of Mrs. Ellis and Augustus Van Horne Ellis, 1896",,Middletown,Connecticut,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/35815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.5.149,false,true,25035,Arms and Armor,Flintlock duelling pistol,Flintlock Duelling Pistol,"American, Middletown, Connecticut",,,,,Gunsmith,,Simeon North,"American, Middletown, Connecticut, 1765–1852",,North Simeon,American,1765,1852,ca. 1815–20,1790,1845,"Steel, gold, silver, wood (walnut, hickory), horn",L. 15 3/4 in. (40 cm); L. of barrel 10 in. (25.4 cm); Cal. .56 in. (14.2 mm); Wt. 2 lb. (907 g),"John Stoneacre Ellis Collection, Gift of Mrs. Ellis and Augustus Van Horne Ellis, 1896",,Middletown,Connecticut,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/25035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.126,false,true,679349,Arms and Armor,Sword,Cavalry Officer's Saber,"American, Philadelphia",,,,,Silversmith,Hilt by,Parry & Musgrave,"American, Philadelphia 1792–1796 Philadelphia",,Parry & Musgrave,American,1792,1796,ca. 1793–95,1768,1820,"Steel, silver, wood, textile, copper",H. 41 in. (104.1 cm); H. of blade 34 1/2 in. (87.6 cm); W. 4 5/8 in. (11.7 cm); D. 4 in. (10.2 cm); Wt. 1 lb. 10.3 oz. (745.6 g),"Purchase, Arthur Ochs Sulzberger Bequest, 2015",,Philadelphia,Pennsylvania,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/679349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.116a–k,false,true,24666,Arms and Armor,Flintlock rifle with case and bullet mould,Flintlock Rifle with Case and Bullet Mould Made for Colonel Jacob Bates (1746–1836),"American, Shrewsbury, Massachusetts",,,,,Gunsmith,,Silas Allen Jr.,"American, Shrewsbury, Massachusetts, 1785–1868",,Allen Silas,American,1785,1868,ca. 1820,1795,1845,"Steel, silver, brass, wood, silver wire",Rifle (a); L. 55 3/4 in. (141.61 cm); L. of barrel 40 1/2 in. (102.87 cm); Cal. .51 in. (12.7 mm); Wt. 11 lb. 2 oz. (5046 g); bullet mould (c); L. 6 1/4 in. (15.9 cm); Wt. 5.6 oz. (158.8 g),"Gift of Christian A. Zabriskie, 1938",,Shrewsbury,Massachusetts,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.582,false,true,35957,Arms and Armor,Experimental helmet prototype,American Helmet Model No. 5,"American, Philadelphia, Pennsylvania",,,,,Manufacturer,,Hale & Kilburn Company,"American, Philadelphia, Pennsylvania 1873–1933 Indianapolis, Indiana",,Hale & Kilburn Company,American,1873,1933,1918,1918,1918,"Steel, paint, leather, textile, string",H. 9 1/4 in. (23.5 cm); W. 10 3/4 in. (27.3 cm); D. 12 5/8 in. (32.1 cm); Wt. 2 lb. 8 oz. (1130 g),"Purchase, Gift of Bashford Dean, by exchange, 2013",,Philadelphia,Pennsylvania,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/35957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.22,true,true,24681,Arms and Armor,Flintlock rifle,Flintlock Rifle,"American, Pennsylvania",,,,,Gunsmith,,Jacob Kuntz,"American, Allentown, Pennsylvania 1780–1876 Philadelphia, Pennsylvania",,"Kuntz, Jacob",American,1780,1876,ca. 1810–15,1785,1840,"Steel, brass, wood (maple), silver, bone, horn",L. 59 1/4 in. (150.5 cm); L. of barrel 43 in. (109.22 cm); Cal. .46 in. (11.7 mm); Wt. 9 lb. 13 oz. (4451 g),"Gift of Wilfrid Wood, 1956",,,Pennsylvania,,,,,,,,,Firearms-Guns-Flintlock,,http://www.metmuseum.org/art/collection/search/24681,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2013.622a, b",false,true,625200,Arms and Armor,Presentation sword and scabbard,Congressional Presentation Sword and Scabbard of Peleg K. Dunham (1794–1822),American,,,,,Bladesmith|Decorator,Possibly decorated by,"William Rose Sr., 1783–1856|John Meer",active ca. 1795–1834,,"Rose, William Sr.|Meer, John",American,1783 |1795,1856 |1834,1817,1817,1817,"Steel, copper alloy (brass), gold, leather",L. with scabbard 39 5/16 in. (99.8 cm); L. without scabbard 38 1/2 in. (97.8 cm); L. of blade 32 5/16 in. (82 cm); W. 4 5/8 in. (11.7 cm); D. 1 7/8 in. (4.7 cm); Wt. 1 lb. 11 oz. (888 g); Wt. of scabbard 1 lb. 1 oz. (481.9 g),"Purchase, Arthur Ochs Sulberger Gift, 2013",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/625200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.52,false,true,32177,Arms and Armor,Letter,Letter from George Washington to New York Governor George Clinton,American,,,,,Author|Maker,In the hand of,George Washington|Tench Tilghman,"American, 1732–1799|1774–1786",,"Washington, George|Tilghman, Tench",American,1732 |1774,1799 |1786,1780,1780,1780,"Ink, paper",13 1/2 x 8 3/8 in. (34.2 x 21.2 cm),"Rogers Fund, 1935",,New Windsor,New York,,,,,,,,,Works on Paper-Miscellaneous,,http://www.metmuseum.org/art/collection/search/32177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.178,false,true,23349,Arms and Armor,Armor (Gusoku),Armor (Gusoku),Japanese and European,,,,,Armorer,Helmet signed by,Saotome Iyetada,"Japanese, Edo period, active early–mid-19th century",,"Iyetada, Saotome",Japanese,1800,1875,early–mid-19th century,1800,1875,"Iron, silk, copper, gold",,"Gift of Edith McCagg, in memory of her husband, Louis B. McCagg, 1929",,,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/23349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.270,false,true,26636,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),"Japanese, Tokyo",,,,,Fittings maker,Inscribed by,Joka,"Japanese, Tokyo, active late 18th–early 19th century",", after Yasuchika",Joka,Japanese,1750,1850,early 19th century,1801,1850,"Iron, lacquer, gold",H. 3 1/4 in. (8.3 cm); W. 3 in. (7.6 cm),"Gift of Ryoichi Iida, 1999",,Tokyo,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/26636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.723,false,true,28455,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Ishiguro Masatsune,"Japanese, 1760–1828",,Ishiguro Masatsune,Japanese,1760,1828,late 18th–early 19th century,1750,1850,"Copper-gold alloy (shakudō), copper-silver alloy (shibuichi), gold, copper",H. 2 15/16 in. (7.5 cm); W. 2 3/4 in. (7 cm); thickness 5/16 in. (0.8 cm); Wt. 5.7 oz. (161.6 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/28455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.724,false,true,28456,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Ishiguro Masatsune,"Japanese, 1760–1828",,Ishiguro Masatsune,Japanese,1760,1828,late 18th–early 19th century,1750,1850,"Copper-gold alloy (shakudō), copper-silver alloy (shibuichi), gold, copper",H. 3 1/8 in. (7.9 cm); W. 2 15/16 in. (7.5 cm); thickness 5/16 in. (0.8 cm); Wt. 7 oz. (198.4 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/28456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"43.120.723, .724",false,true,657341,Arms and Armor,Pair of sword guards (daishō tsuba),Pair of Sword Guards (Daishō Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Ishiguro Masatsune,"Japanese, 1760–1828",,Ishiguro Masatsune,Japanese,1760,1828,late 18th–early 19th century,1750,1850,"Copper-gold alloy (shakudō), copper-silver alloy (shibuichi), gold, copper",43.120.723: H. 2 15/16 in. (7.5 cm); W. 2 3/4 in. (7 cm); thickness 5/16 in. (0.8 cm); Wt. 5.7 oz. (161.6 g); 43.120.724: H. 3 1/8 in. (7.9 cm); W. 2 15/16 in. (7.5 cm); thickness 5/16 in. (0.8 cm); Wt. 7 oz. (198.4 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/657341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.467,false,true,25362,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Fittings maker,Inscribed by,Iwama Masayoshi (Shoro),"Japanese, 1764–1837",,Iwama Masayoshi (Shoro),Japanese,1764,1837,late 18th–early 19th century,1750,1850,"Copper-silver alloy (shibuichi), copper-gold alloy (shakudō), gold, silver, copper",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.4 oz. (39.7 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25362,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.335,false,true,29721,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Fittings maker,Inscribed by,Kanō Natsuo,"Japanese, 1828–1898",,"Natsuo, Kanō",Japanese,1828,1898,mid-19th century,1825,1875,"Copper-silver alloy (shibuichi), silver, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.2 oz. (34 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/29721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.339,false,true,29725,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Fittings maker,Inscribed by,Kanō Natsuo,"Japanese, 1828–1898",,"Natsuo, Kanō",Japanese,1828,1898,mid-19th century,1825,1875,"Copper-silver alloy (shibuichi), silver",L. 3 1/4 in. (8.3 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1 oz. (28.3 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/29725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.109.5,false,true,22102,Arms and Armor,Blade and mounting for a slung sword (Tachi),Blade and Mounting for a Slung Sword (Tachi),Japanese,,,,,Sword maker,,Naganori,"Japanese, active 13th century",,Naganori,Japanese,1201,1300,"blade, late 13th–early 14th century; mounting, 18th–early 19th century",1201,1900,"Steel, wood, lacquer, rayskin (samé), thread, gold, silver",L. 32 7/8 in. (83. 5 cm); L. of blade 25 15/16 in. (65.8 cm); L. of cutting edge 19 5/8 in. (49.8 cm); D. of curvature 5/16 in. (0.75 cm),"Gift of the family of Dr. Francis E. Doughty, 1907",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/22102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.321,false,true,23393,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 12 5/8 in. (32.1 cm); L. of head 5 3/4 in. (14.6 cm); W. 2 5/8 in. (6.7 cm); Wt. 6.7 oz. (189.9 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.327,false,true,23399,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,"Iron, gold",L. 11 1/4 in. (28.6 cm); L. of head 4 3/4 in. (12.1 cm); W. 5 in. (12.7 cm); Wt. 6.4 oz. (181.4 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.330,false,true,23402,Arms and Armor,Ceremonial arrowhead (Yanoné),Ceremonial Arrowhead (Yanoné),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 12 1/8 in. (30.8 cm); L. of head 5 7/16 in. (13.8 cm); W. 2 1/2 in. (6.4 cm); Wt. 6.5 oz. (184.3 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.334,false,true,23406,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 11 3/8 in. (28.9 cm); L. of head 4 1/2 in. (11.4 cm); W. 2 5/16 in. (5.9 cm); Wt. 5.2 oz. (147.4 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.337,false,true,23409,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 11 in. (27.9 cm); L. of head 4 1/8 in. (10.5 cm); W. 2 1/8 in. (5.4 cm); Wt. 3.9 oz. (110.6 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.339,false,true,23411,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 11 3/4 in. (29.9 cm); L. of head 3 7/8 in. (9.8 cm); W. 2 1/8 in. (5.4 cm); Wt. 4.3 oz. (121.9 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.340,false,true,23412,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated August 1645,1645,1645,Steel,L. 11 3/4 in. (29.9 cm); L. of head 5 1/8 in. (13 cm); W. 2 3/8 in. (6 cm); Wt. 5.3 oz. (150.3 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.398,false,true,23469,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated August 1645,1645,1645,Steel,L. 12 3/4 in. (32.4 cm); L. of head 6 in. (15.7 cm); W. 2 3/4 in. (7 cm); Wt. 7.1 oz. (201.3 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.399,false,true,23470,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 11 1/4 in. (28.6 cm); L. of head 4 3/8 in. (11.1 cm); W. 3 3/8 in. (8.6 cm); Wt. 5.7 oz. (161.6 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.403,false,true,23474,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 10 5/8 in. (27 cm); L. of head 4 1/8 in. (10.5 cm); W. 1 3/4 in. (4.5 cm); Wt. 3.5 oz. (99.2 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.406,false,true,23477,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 10 1/2 in. (26.7 cm); L. of head 3 7/8 in. (9.8 cm); W. 2 in. (5.1 cm); Wt. 3.5 oz. (99.2 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.409,false,true,23480,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Motoshige,"Japanese, Edo period, died 1675",,"Motoshige, Umetada",Japanese,1575,1675,dated 1645,1645,1645,Steel,L. 10 5/8 in. (27 cm); L. of head 4 1/16 in. (10.3 cm); W. 1 7/8 in. (4.8 cm); Wt. 3.8 oz. (107.7 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.112.13,false,true,22161,Arms and Armor,Mask,Mask,Japanese,,,,,Armorer,Inscribed by,Myōchin Muneakira,"Japanese, Edo period, 1673–1745",,"Muneakira, Myōchin",Japanese,1673,1745,dated 1715,1715,1715,"Iron, lacquer",H. 7 3/4 in. (19.7 cm); W. 6 3/4 in. (17.2 cm),"Rogers Fund, 1913",,,,,,,,,,,,Armor Parts-Masks,,http://www.metmuseum.org/art/collection/search/22161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.100.45,false,true,22469,Arms and Armor,Mask,Mask,Japanese,,,,,Armorer,Inscribed by,Myōchin Muneakira,"Japanese, Edo period, 1673–1745",,"Muneakira, Myōchin",Japanese,1673,1745,dated 1713,1713,1713,"Iron, lacquer",H. 8 11/16 in. (22.2 cm); W. 6 3/4 in. (17.2 cm),"Gift of Bashford Dean, 1914",,,,,,,,,,,,Armor Parts-Masks,,http://www.metmuseum.org/art/collection/search/22469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.112.4,false,true,22154,Arms and Armor,Armor (Gusoku),Armor (Gusoku),Japanese,,,,,Armorer,Signed by,Myōchin Munesuke,"Japanese, Edo period, 1688–1735",,"Munesuke, Myōchin",Japanese,1688,1735,early 18th century,1700,1750,"Iron, lacquer, silk, copper, gold",,"Rogers Fund, 1913",,,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.60.27,false,true,34931,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Hisanori,"Japanese, active ca. 17th century",,Hisanori,Japanese,1575,1725,possibly 17th century,1575,1725,"Brass, copper-gold alloy (shakudō)",H. 3 1/4 in. (8.3 cm); W. 3 1/8 in. (7.9 cm); thickness 3/16 in. (0.5 cm); Wt. 4.4 oz. (124.7 g),"Gift of Mrs. Mary E. Larkin Joline, 1914",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/34931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.574,false,true,24978,Arms and Armor,Blade for a sword (Katana),Blade for a Sword (Katana),Japanese,,,,,Swordsmith,Blade inscribed by,Masazane,"Japanese, Ise, documented 1515–26",,Masazane,Japanese,1515,1526,dated 1526,1526,1526,Steel,L. 36 1/8 in. (91.8 cm); L. of blade edge 29 9/16 in. (75.1 cm); D. of curvature 3/32 in. (2.4 cm),"Purchase, Arthur Ochs Sulzberger Gift, 2001",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/24978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2007.478.2a, b",false,true,27600,Arms and Armor,Blade for a sword (Katana),Blade for a Sword (Katana),Japanese,,,,,Sword maker,Signed,Etchu no kami Fujiwara Takahira,"Japanese, active early 17th century",,"Takahira, Etchu no kami Fujiwara",Japanese,1600,1650,dated June 1622,1622,1622,Steel,L. 36 1/2 in. (92.8 cm); L. of cutting edge 28 1/16 in. (71.5 cm); D. of curvature 1 in. (1.5 cm),"Gift of Etsuko O. Morris and John H. Morris Jr., in memory of Dr. Frederick M. Pederson, 2007",,,,,,,,,,,,Sword Blades,,http://www.metmuseum.org/art/collection/search/27600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1991.373a, b",true,true,24948,Arms and Armor,Blade for a dagger (Tantō),Blade for a Dagger (Tantō),Japanese,,,,,Swordsmith,Blade inscribed by,Rai Kunitoshi,"Japanese, active ca. 1290–ca. 1320",,Kunitoshi Rai,Japanese,1265,1345,ca. 1315–16,1290,1341,Steel,L. 13 5/8 in. (34.6 cm); L. of cutting edge 9 3/8 in. (23.8 cm); W. of blade at hilt 15/16 in. (2.4 cm); Wt. 7 oz. (185 g),"Gift of Mr. and Mrs. Robert Andrews Izard, 1991",,,,,,,,,,,,Daggers,,http://www.metmuseum.org/art/collection/search/24948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.127.2,false,true,26571,Arms and Armor,Blade and mounting for a dagger (Tantō),Blade and Mounting for a Dagger (Tantō),Japanese,,,,,Swordsmith,Blade inscribed by,Uda Kunimitsu,"Japanese, active early–mid-14th century",,"Kunimitsu, Uda",Japanese,1300,1375,"blade, dated November, 1333; mounting, 19th century",1333,1900,"Steel, wood, lacquer, ray skin (samé), leather, copper, gold, silver, gold, shark skin, copper-gold alloy (shakudō)",L. 17 5/8 in. (44.7 cm); L. of cutting edge 12 in. (30.4 cm),"Gift of Peter H. B. Frelinghuysen, 1998",,,,,,,,,,,,Daggers,,http://www.metmuseum.org/art/collection/search/26571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +09.49.2,false,true,22108,Arms and Armor,Hand cannon,Hand Cannon,Japanese,,,,,Gunsmith,Inscribed by,Kazuki Nobumichi,"Japanese, active late 18th–early 19th century",,"Nobumichi, Kazuki",Japanese,1750,1850,late 18th–early 19th century,1750,1850,"Iron, gold, silver, wood, brass",L. 37 in. (94 cm),"Gift of Marshall C. Lefferts, 1909",,,,,,,,,,,,Firearms-Hand Cannon,,http://www.metmuseum.org/art/collection/search/22108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.24.52a–d,false,true,24727,Arms and Armor,Set of sword fittings (Mitokoromono),Set of Sword Fittings (Mitokoromono),Japanese,,,,,Fittings maker,,Gotō Sōjō,"Japanese, ca. 1461–1538, second-generation Gotō master",,Gotō Sōjō,Japanese,1461,1538,late 15th–early 16th century,1450,1550,"Copper-gold alloy (shakudō), gold","L. of hair dressing tool (kogai) (a) 8 5/16 in. (21.1 cm); L. of each grip ornament (menuki) (b, c) 1 11/32 in. (3.4 cm); L. of knife handle (kozuka) (d) 3 13/16 in. (9.7 cm)","Rogers Fund, 1945",,,,,,,,,,,,"Sword Furniture-Fittings, Sets of",,http://www.metmuseum.org/art/collection/search/24727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"36.120.407a, b",false,true,29684,Arms and Armor,Helmet (Hoshi kabuto) in the 16th-century style,Helmet (Hoshi- Kabuto) in the 16th-Century Style,Japanese,,,,,Armorer,Inscribed by,Saotame Iesuek,"Japanese, Miyazaki Prefecture, active late 17th–early 18th century",,"Iesuek, Saotame",Japanese,1650,1750,probably late 17th–early 18th century,1625,1775,"Iron, lacquer, silk",H. 11 3/4 in. (29.9 cm); W. 13 in. (33 cm),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/29684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.208.65,false,true,35060,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Nobuiye Myōchin,"Japanese, ca. 1504–1554",,"Myōchin, Nobuiye",Japanese,1479,1579,16th century,1501,1600,Iron,H. 3 1/8 in. (7.9 cm); W. 3 in. (7.6 cm); thickness 3/16 in. (0.5 cm); Wt. 4.4 oz. (124.7 g),"Gift of a Trustee of the Museum, 1917",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/35060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +04.4.9a–l,false,true,22022,Arms and Armor,Armor (Gusoku),Armor (Gusoku),Japanese,,,,,Armorer,Inscribed by,Yukinoshita Sadaiyé,"Japanese, active 17th century",,"Sadaiyé, Yukinoshita",Japanese,1601,1700,17th century,1601,1700,"Iron, lacquer, silk, gilt copper",as mounted: H. 53 in. (134.6 cm); W. 32 in. (81.3 cm); D. 20 in. (50.8 cm),"Rogers Fund, 1904",,,,,,,,,,,,Armor for Man,,http://www.metmuseum.org/art/collection/search/22022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.318,false,true,23390,Arms and Armor,Ceremonial arrowhead (Yanonē),Ceremonial Arrowhead (Yanonē),Japanese,,,,,Steel-chiseler,,Umetada Yoshinobu,"Japanese, Edo period, 17th century",,"Yoshinobu, Umetada",Japanese,1601,1700,17th century,1601,1700,Steel,L. 18 5/8 in. (47.3 cm); L. of head 6 3/4 in. (17.1 cm); W. 3 1/2 in. (8.9 cm); Wt. 6.9 oz. (195.6 g),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Archery Equipment-Arrowheads,,http://www.metmuseum.org/art/collection/search/23390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.63.4,false,true,24790,Arms and Armor,Helmet (Hoshi-kabuto),Helmet (Hoshi-Kabuto),Japanese,,,,,Armorer,Inscribed by,Iyehisa of Nara,"Japanese, Nara, active 17th century",,"Japanese, Nara Iyehisa of Nara",Japanese,1601,1700,17th century,1601,1700,"Iron, lacquer, gilt copper, copper-gold alloy (shakūdo), silver, silk, gilt leather",H. 11 in. (27.9 cm); L. 15 1/2 in. (39.4 cm),"Rogers Fund, 1948",,,,,,,,,,,,Helmets,,http://www.metmuseum.org/art/collection/search/24790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.91,false,true,30090,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Ichinomiya Nagatsune,"Japanese, 1721–1786",,Ichinomiya Nagatsune,Japanese,1721,1786,18th century,1701,1800,"Iron, silver",H. 3 1/8 in. (7.9 cm); W. 3 in. (7.6 cm); thickness 3/8 in. (1 cm); Wt. 7.6 oz. (215.5 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/30090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.71.8,false,true,33366,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Ōmori Teruhide,"Japanese, Edo period, 1730–1798",,"Teruhide, Ōmori",Japanese,1730,1798,18th century,1701,1800,"Copper-gold alloy (shakudō), gold, copper-silver alloy (shibuichi)",H. 2 7/8 in. (7.3 cm); W. 2 3/4 in. (7 cm); thickness 5/16 in. (0.8 cm); Wt. 5.7 oz. (161.6 g),"Charles Stewart Smith Memorial Fund, 1919",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/33366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.728,false,true,24712,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Gotō Ichijō,"Japanese, 1791–1876",,Gotō Ichijō,Japanese,1791,1876,19th century,1801,1900,"Copper-gold alloy (shakudō), silver, gold, copper",H. 2 11/16 in. (6.8 cm); W. 2 1/2 in. (6.4 cm); thickness 3/16 in. (0.5 cm); Wt. 4 oz. (113.4 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/24712,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.528.3,false,true,35913,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Gotō Mitsuakira,"Japanese, 1816–1856",,"Mitsuakira, Gotō",Japanese,1816,1856,19th century,1801,1900,"Copper-gold alloy (shakudō), gold, silver, copper",H. 2 3/4 in. (7 cm); W. 2 9/16 in. (6.5 cm); thickness 5/16 in. (0.8 cm); Wt. 4.4 oz. (124.7 g),"Gift of the Baber Family, in loving memory of Charles Chenault Baber, 2012",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/35913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.208.42,false,true,35038,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Fittings maker,Inscribed by,Ginshōtei Tōmei,"Japanese, 1817–1870",,"Tōmei, Ginshōtei",Japanese,1817,1870,19th century,1801,1900,"Iron, gold, copper-silver alloy (shibuichi)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.4 oz. (39.7 g),"Gift of a Trustee of the Museum, 1917",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/35038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.208.32a–e,false,true,35031,Arms and Armor,Set of sword fittings (Mitokoromono),Set of Sword Fittings (Mitokoromono),Japanese,,,,,Fittings maker,Inscribed by,Ginshōtei Tōmei,"Japanese, 1817–1870",,"Tōmei, Ginshōtei",Japanese,1817,1870,19th century,1801,1900,"Iron, gold, copper-silver alloy (shibuichi)",sword guard (tsuba) 17.208.32a: 2 5/8 x 2 3/8 in. (6.7 x 6.0 cm); cord knob (kurigata) 17.208.32b: 1 1/4 x 3/4 in. (3.2 x 1.9 cm); socket brace for knife (uragawara) 17.208.32c 1 1/4 x 3/4 in. (3.2 x 1.9 cm); pommel cap (kashira) 17.208.32d: 1 3/8 x 3/4 in. (3.5 x 1.9 cm); hilt collar (fuchi) 17.208.32e: 1 1/2 x 3/4 in. (3.8 x 1.9 cm),"Gift of a Trustee of the Museum, 1917",,,,,,,,,,,,"Sword Furniture-Fittings, Sets of",,http://www.metmuseum.org/art/collection/search/35031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.99,false,true,24625,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,,Kanō Natsuo,"Japanese, 1828–1898",,"Natsuo, Kanō",Japanese,1828,1898,19th century,1801,1900,"Copper-gold alloy (shakudō), gold, copper",H. 2 1/4 in. (5.7 cm); W. 1 15/16 in. (4.9 cm); thickness 3/16 in. (0.5 cm); Wt. 2.4 oz. (68 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/24625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.79,true,true,24623,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Fittings maker,Inscribed by,Ishiguro Masayoshi,"Japanese, 1772–after 1851",,Ishiguro Masayoshi,Japanese,1772,1851,19th century,1801,1900,"Copper-gold alloy (shakudō), gold, copper-silver alloy (shibuichi), copper",H. 2 7/8 in. (7.3 cm); W. 2 5/8 in. (6.7 cm); thickness 5/16 in. (0.8 cm); Wt. 5.4 oz. (153.1 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/24623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.63.5a–l,false,true,24786,Arms and Armor,Fittings for a pair of swords (Daishō soroi-mono),Fittings for a Pair of Swords (Daishō Soroi-Mono),Japanese,,,,,Fittings maker,Inscribed by,Masayoshi,"Japanese, born 1784",,Masayoshi,Japanese,1784,1884,early 19th century,1800,1850,"Copper-gold alloy (shakudō), gold, silver, copper-silver alloy (shibuichi), copper","Sword guards (Tsuba) (a, b); H. of each 2 7/8 in. (7.3 cm); W. of each 2 11/16 in. (6.8 cm); thickness of each 3/8 in. (0.5 cm); Wt. of each 5.2 oz. (147.4 g); knife handles (Kozuka) (c, d); 3 7/8 x 9/16 in. (9.8 x 1.4 cm); sword-hilt collar (fuchi) (e); 1 1/2 x 13/16 in. (3.8 x 2.1 cm); pommel (kashira) (f); 1 3/8 x 11/16 in. (3.5 x 1.7 cm); sword-hilt collar (fuchi) (g); 1 1/2 x 7/8 in. (3.8 x 2.2 cm); pommel (kashira) (h); 1 3/8 x 3/4 in. (3.5 x 1.9 cm); each of a pair of menuki (i, j); 1 1/16 x 5/8 in. (2.7 x 1.6 cm); each of a pair of menuki (k, l); 1 x 9/16 in. (2.5 x 1.4 cm)","Rogers Fund, 1948",,,,,,,,,,,,"Sword Furniture-Fittings, Sets of",,http://www.metmuseum.org/art/collection/search/24786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2006.262.1, .2",false,true,33557,Arms and Armor,Pair of sword guards (Tsuba),Pair of Sword Guards (Tsuba),Japanese,,,,,Fittings maker,Inscribed on the inner face of each tsuba by,Sunagawa Mao-Yoshi,"Japanese, active early–mid-19th century",,Sunagawa Mao-Yoshi,Japanese,1800,1875,early 19th century,1800,1850,"Copper-gold alloy (shakudō), gold, copper",2006.262.1: H. 2 15/16 in. (7.4 cm); W. 2 3/4 in. (7.0 cm); 2006.262.2: H. 2 13/16 in. (7.1 cm); W. 2 9/16 in. (6.5 cm),"Purchase, Gift of Mrs. George A. Crocker (Elizabeth Masten), by exchange, 2006",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/33557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.537,false,true,25355,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Toshihiro,"Japanese, died 1861",,Hamano Toshihiro,Japanese,,1861,late 18th–mid-19th century,1775,1870,"Copper-silver alloy (shibuichi), gold, copper, silver, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.692,false,true,24711,Arms and Armor,Sword guard (Tsuba),Sword Guard (Tsuba),Japanese,,,,,Maker,Attributed to the,Hirata School,"Japanese, Edo period",,Hirata School,Japanese,1615,1868,ca. 1615–1868,1590,1900,"Copper-gold alloy (shakudō), gold, enameled cloisonné (shippō), copper",H. 2 7/8 in. (7.3 cm); W. 2 15/16 in. (7.5 cm); thickness 1/4 in. (0.6 cm); Wt. 5.4 oz. (153.1 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Tsuba,,http://www.metmuseum.org/art/collection/search/24711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.460,false,true,27692,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Goto Mitsunori,"Japanese, 1646–1712",,Goto Mitsunori,Japanese,1646,1712,early 18th century,1701,1712,"Copper-gold alloy (shakudō), gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Purchase, Arthur Ochs Sulzberger Gift, 2004",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/27692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.293,false,true,25262,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Masayuki,"Japanese, 1696–1769",,Hamano Masayuki,Japanese,1696,1769,early–mid-18th century,1701,1775,"Copper, gold, copper alloy (sentoku), silver",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 5/16 in. (0.8 cm); Wt. 1.3 oz. (36.9 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.459a–c,false,true,27691,Arms and Armor,Pair of sword-grip ornaments (Menuki),Pair of Sword-Grip Ornaments (Menuki),Japanese,,,,,Maker,,Tsu Jumpo (Jimpo),"Japanese, 1721–1762",,Tsu Jumpo,Japanese,1721,1762,ca. 1750,1750,1750,Gold,L. of each 2 in. (5 cm); W. of each 1/2 in. (1.3 cm); Wt. of each 0.3 oz. (8.5 g),"Purchase, Arthur Ochs Sulzberger Gift, 2004",,,,,,,,,,,,Sword Furniture-Menuki,,http://www.metmuseum.org/art/collection/search/27691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.913,false,true,26912,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Iwama Masayoshi (Shoro),"Japanese, 1764–1837",,Iwama Masayoshi (Shoro),Japanese,1764,1837,dated 1828,1828,1828,"Copper-silver alloy (shibuichi), gold, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/26912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.300,false,true,25266,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Iwama Masayoshi (Shoro),"Japanese, 1764–1837",,Iwama Masayoshi (Shoro),Japanese,1764,1837,late 18th–mid-19th century,1775,1837,"Copper, silver, gold, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.456,false,true,25330,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Iwama Masayoshi (Shoro),"Japanese, 1764–1837",,Iwama Masayoshi (Shoro),Japanese,1764,1837,late 18th–early 19th century,1801,1900,"Copper-gold alloy (shakudō), silver, gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.3 oz. (36.9 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.455,false,true,25329,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Naochika,"Japanese, 1784–1808",,Hamano Naochika,Japanese,1784,1808,late 18th–early 19th century,1801,1900,"Copper-gold alloy (shakudō), gold, silver, copper",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.3 oz. (36.9 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.459,false,true,25331,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Furukawa Jochin,"Japanese, died ca.1750",,Furukawa Jochin,Japanese,1750,1750,early 18th century,1701,1760,Copper-silver alloy (shibuichi),L. 3 3/4 in. (9.5 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.421,false,true,25311,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Katsutora,"Japanese, died ca.1825",,Katsutora,Japanese,,1825,late 18th century–early 19th century,1765,1835,"Copper-silver alloy (shibuichi), silver",L. 3 3/4 in. (9.5 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.430,false,true,25317,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Haruyuki,"Japanese, died ca.1830",,Hamano Haruyuki,Japanese,1830,1830,late 18th–early 19th century,1740,1850,"Copper-silver alloy (shibuichi), gold, silver, copper, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.477,false,true,25339,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Haruchika,"Japanese, died ca.1850",,Hamano Haruchika,Japanese,1850,1850,late 18th–early 19th century,1750,1860,"Copper-silver alloy (shibuichi), gold, silver",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25339,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.478,false,true,25340,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Naotsune,"Japanese, died ca.1850",,Hamano Naotsune,Japanese,,1850,late 18th–early 19th century,1750,1860,"Copper-silver alloy (shibuichi), gold, silver",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.37.143,false,true,25647,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Akichika Oishi,"Japanese, active ca.1850",,Oishi Akichika,Japanese,1850,1850,late 18th–early 19th century,1800,1825,"Copper-silver alloy (shibuichi), copper-gold alloy (shakudō), gold, copper",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Rogers Fund, 1912",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.43.1,false,true,23356,Arms and Armor,Saddle (Kura),Saddle (Kura),Japanese,,,,,Maker,Signed by,Chikara,"Japanese, active ca. 1610",,Chikara,Japanese,1585,1635,dated 1610,1610,1610,"Wood, mother-of-pearl, linen, lacquer, gold",H. 12 1/4 in. (31.1 cm); W. 15 1/2 in. (39.4 cm),"Gift of Fredrick C. MacDonell, 1932",,,,,,,,,,,,Equestrian Equipment-Saddles,,http://www.metmuseum.org/art/collection/search/23356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.317,false,true,25270,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Ichijosai Hironaga (Hirotoshi),"Japanese, died ca. 1800–25",,Ichijosai Hironaga (Hirotoshi),Japanese,1800,1825,mid-18th–early 19th century,1725,1825,"Copper-silver alloy (shibuichi), gold, copper",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 5/16 in. (0.8 cm); Wt. 1.3 oz. (36.9 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.512,false,true,25349,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Ichijosai Hironaga (Hirotoshi),"Japanese, died ca. 1800–25",,Ichijosai Hironaga (Hirotoshi),Japanese,1800,1825,late 17th–early 18th century,1801,1900,"Copper-silver alloy (shibuichi), gold, silver, copper, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.115.2,true,true,22739,Arms and Armor,Mask,Mask,Japanese,,,,,Maker,Inscribed by,Myōchin Muneakira,"Japanese, Edo period, 1673–1745",,"Muneakira, Myōchin",Japanese,1673,1745,dated 1745,1745,1745,"Iron, lacquer, textile (silk)",L. 9 1/2 in. (24.1 cm); W. 7 in. (17.8 cm),"Rogers Fund, 1919",,,,,,,,,,,,Armor Parts-Masks,,http://www.metmuseum.org/art/collection/search/22739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.112.20,false,true,22176,Arms and Armor,Okimono in the form of a raven,Okimono in the Form of a Raven,Japanese,,,,,Maker,,Myōchin Munesuke,"Japanese, Edo period, 1688–1735",,"Munesuke, Myōchin",Japanese,1688,1735,early 18th century,1700,1750,Steel,L. 18 in. (45.7 cm),"Rogers Fund, 1913",,,,,,,,,,,,Miscellaneous,,http://www.metmuseum.org/art/collection/search/22176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.178,false,true,25218,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Gotō Eijō,"Japanese, 1577–1617, sixth-generation Gotō master",,Gotō Eijō,Japanese,1577,1617,17th century,1601,1700,"Gold, copper-gold alloy (shakudō), silver",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.4 oz. (39.7 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.111,false,true,25189,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Gotō Renjō (Mitsutomo),"Japanese, 1628–1708, tenth-generation Gotō master",,"Renjō (Mitsutomo), Gotō",Japanese,1628,1708,17th century,1601,1700,"Copper-gold alloy (shakudō), gold, silver",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1 oz. (28.3 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.487,false,true,25345,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Gotō Renjō (Mitsutomo),"Japanese, 1628–1708, tenth-generation Gotō master",,"Renjō (Mitsutomo), Gotō",Japanese,1628,1708,17th century,1601,1700,"Copper-gold alloy (shakudō), gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.4 oz. (39.7 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.341,false,true,25280,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Genshosai Masaharu,"Japanese, died 1724",,Genshosai Masaharu,Japanese,,1724,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, copper",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.372,false,true,26151,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Toshihiro,"Japanese, died 1861",,Hamano Toshihiro,Japanese,,1861,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, silver, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.2 oz. (34 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/26151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.426,false,true,25314,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Toshihiro,"Japanese, died 1861",,Hamano Toshihiro,Japanese,,1861,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, silver, copper",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.569,false,true,25733,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Joken Mori,"Japanese, died 1866",,Mori Joken,Japanese,,1866,19th century,1801,1900,"Copper-silver alloy (shibuichi), iron",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1 oz. (28.3 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.439,false,true,25319,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Gotō Ichijō,"Japanese, 1791–1876",,Gotō Ichijō,Japanese,1791,1876,19th century,1801,1900,"Copper-gold alloy (shakudō), gold, silver, copper",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.318,false,true,25271,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Tanaka Kiyotoshi (Kiyonaga),"Japanese, 1804–1876",,Tanaka Kiyotoshi (Kiyonaga) Ryuso,Japanese,1804,1876,19th century,1801,1876,"Copper-silver alloy (shibuichi), gold, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.3 oz. (36.9 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25271,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.182,false,true,25220,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Iwama Nobuyoshi,"Japanese, 1807–1878",,Iwama Nobuyoshi,Japanese,1807,1878,19th century,1801,1900,"Copper-silver alloy (shibuichi), silver, gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.366,false,true,25286,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Iwama Nobuyoshi,"Japanese, 1807–1878",,Iwama Nobuyoshi,Japanese,1807,1878,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, silver",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.56,false,true,25180,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Funada Yoshinaga,"Japanese, 1812–1863",,Funada Yoshinaga,Japanese,1812,1863,19th century,1801,1900,"Copper-silver alloy (shibuichi), copper, gold, silver",L. 3 3/4 in. (9.5 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.120.237,false,true,26117,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Haruchika,"Japanese, died ca.1850",,Hamano Haruchika,Japanese,1850,1850,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, copper, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"The Howard Mansfield Collection, Gift of Howard Mansfield, 1936",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/26117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.240,false,true,25244,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Haruchika,"Japanese, died ca.1850",,Hamano Haruchika,Japanese,1850,1850,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1 oz. (28.3 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.385,false,true,25297,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Haruchika,"Japanese, died ca.1850",,Hamano Haruchika,Japanese,1850,1850,19th century,1801,1900,"Copper-gold alloy (shakudō), iron, gold, silver, copper, brass",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.184,false,true,25221,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Marukawa Hiroyoshi,"Japanese, died 1841 or 1842",,Marukawa Hiroyoshi,Japanese,1841,1841,19th century,1801,1900,"Copper-silver alloy (shibuichi), silver, gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.393,false,true,25301,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Ichijosai Hironaga (Hirotoshi),"Japanese, died ca. 1800–25",,Ichijosai Hironaga (Hirotoshi),Japanese,1800,1825,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, silver, copper, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.214,false,true,25235,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Ichiyosai Hironao,"Japanese, died ca. 1825–50",,Ichiyosai Hironao,Japanese,1825,1850,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, silver",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.337,false,true,25279,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Ichiyosai Hironao,"Japanese, died ca. 1825–50",,Ichiyosai Hironao,Japanese,1825,1850,19th century,1801,1900,"Copper-silver alloy (shibuichi), gold, copper, silver, copper-gold alloy (shakudō)",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.451,false,true,25325,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Gotō Mitsuyoshi (Shinjō),"Japanese, 1780–1843, fifteenth-generation Gotō master",,Gotō Mitsuyoshi (Shinjō),Japanese,1780,1843,19th century,1801,1900,"Copper-gold alloy (shakudō), gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.4 oz. (39.7 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.529,false,true,25353,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Naoyuki,"Japanese, 1754–1795",,Hamano Naoyuki,Japanese,1754,1795,late 18th century,1775,1799,"Copper-silver alloy (shibuichi), gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.253,false,true,25251,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Hamano Haruchika,"Japanese, died ca.1850",,Hamano Haruchika,Japanese,1850,1850,early 19th century,1801,1850,"Copper-silver alloy (shibuichi), gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25251,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.391,false,true,25300,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Marukawa Hiroyoshi,"Japanese, died 1841 or 1842",,Marukawa Hiroyoshi,Japanese,1841,1841,early 19th century,1801,1850,"Copper-silver alloy (shibuichi), gold, silver, copper-gold alloy (shakudō)",L. 3 3/4 in. (9.5 cm); W. 9/16 in. (1.4 cm); thickness 1/4 in. (0.6 cm); Wt. 1.1 oz. (31.2 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.120.356,false,true,25282,Arms and Armor,Knife handle (Kozuka),Knife Handle (Kozuka),Japanese,,,,,Maker,,Ichijosai Hironaga (Hirotoshi),"Japanese, died ca. 1800–25",,Ichijosai Hironaga (Hirotoshi),Japanese,1800,1825,early 19th century,1801,1850,"Copper-silver alloy (shibuichi), copper, gold",L. 3 13/16 in. (9.7 cm); W. 9/16 in. (1.4 cm); thickness 3/16 in. (0.5 cm); Wt. 1.2 oz. (34 g),"Gift of Herman A. E. and Paul C. Jaehne, 1943",,,,,,,,,,,,Sword Furniture-Kozuka,,http://www.metmuseum.org/art/collection/search/25282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.105,false,true,22137,Arms and Armor,Okimono in the form of an eagle,Eagle with Outstreched Wings,Japanese,,,,,Artist,,Suzuki Chōkichi,"Japanese, 1848–1919",,"Japanese Chōkichi, Suzuki",Japanese,1848,1919,late 19th century,1850,1900,"Iron, pigment, shakudo, shibuichi, wood","H. (without base), 17 in. (43.2 cm); W. of wingspan, 55 in. (139.7 cm)","Gift of James R. Steers, 1911",,,,,,,,,,,,Miscellaneous-Ironwork,,http://www.metmuseum.org/art/collection/search/22137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1997.194.1, .2",false,true,24964,Arms and Armor,Stirrups (Abumi),Stirrups (Abumi),Japanese,,,,,Artist,,Ujiyoshi,"Japanese, active 18th century",,Ujiyoshi,Japanese,0018,0018,18th century,1701,1800,"Iron, silver",H. of each 10 in. (25.4 cm); L. of each 11 1/4 in. (28.6 cm); W. of each 5 1/4 in. (13.3 cm); Wt. of each 5 lb. 6 oz. (2438 g),"Purchase, Gift of Estate of James Hazen Hyde, by exchange, 1997",,,,,,,,,,,,Equestrian Equipment-Stirrups,,http://www.metmuseum.org/art/collection/search/24964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"04.3.195, .196",false,true,21924,Arms and Armor,Pair of flintlock pistols,"Pair of Flintlock Pistols Made for Christian Ernst, Margrave of Brandenburg-Ansbach-Bayreuth-Kulmbach","Franco-German, Erlangen-Neustadt",,,,,Gunsmith|Barrelsmith,,Charles Froment|M. Bréat,"French, active Germany, 1657–1722|French, active Germany, ca. 1680–90",,"Froment, Charles|Bréat, M.",French|French,1657 |1680,1722 |1680,ca. 1686–90,1661,1715,"Steel, wood, silver",L. of each 19 3/4 in. (50.2 cm); L. of each barrel 12 5/8 in. (32.1 cm); L. of each plug 2 3/8 in. (6 cm); Diam. at muzzle of each 5/8 in. (1.6 cm); Diam. at breech of each 1 in. (2.5 cm); L. of each lock 5 in. (12.7 cm); Cal. of each .545 in. (13.8 mm); Wt. of each 2 lb. 2 oz. (950 g),"Rogers Fund, 1904",,Erlangen,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/21924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.595,false,true,667967,Arms and Armor,Smallsword,Smallsword Presented by the City of Paris to Commandant Ildefonse Favé (1812–1894),French,,,,,Hilt Maker|Manufacturer,,Paul Bled|Lepage-Moutier,"French, Falaise 1807–1881|French 1842–1868",,"Bled, Paul|Lepage-Moutier",French|French,1807 |1842,1881 |1868,dated 1856,1831,1881,"Steel, gold",L. 36 in. (91.5 cm); L. of blade 30 1/8 in. (76.6 cm); W. 3 1/4 in. (8.3 cm); Wt. 1 lb. 0.6 oz. (470.6 g),"Gift of Peter Finer, in honor of Stuart Pyhrr, 2014",,,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/667967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.5,false,true,24946,Arms and Armor,Double-barreled percussion shotgun,Double-Barreled Percussion Shotgun,"French, Paris",,,,,Barrelsmith|Gunsmith,,Léopold Bernard|Louis Perrin,"French, Paris, active 1832–70|French, Paris, active 1823–65",,"Bernard, Léopold|Perrin Louis",French|French,1832 |1823,1870 |1865,dated 1854,1854,1854,"Steel, wood (walnut), silver",L. 46 3/5 in. (118.4 cm); L. of barrel 29 1/8 in. (73.9 cm),"Purchase, Rogers Fund, The Sulzberger Foundation Inc. Gift, Gifts of William H. Riggs, Bill and Joyce Anderson, Charles M. Schott Jr., Mr. and Mrs. Robert W. de Forest, William B. Osgood Field, Christian A. Zabriskie, Dr. Albert T. Weston, Henry Victor Burgy, and Bequest of Alan Rutherfurd Stuyvesant, by exchange, and The Collection of Giovanni P. Morosini, presented by his daughter Giulia, John Stoneacre Ellis Collection, Gift of Mrs. Ellis and Augustus Van Horne Ellis, and Bashford Dean Memorial Collection, funds from various donors, by exchange, 1991",,Paris,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.114.1,false,true,24942,Arms and Armor,Flintlock gun,Flintlock Gun,"French, Paris",,,,,Barrelsmith|Gunsmith,,Le Faure|Mollier,"French, Paris, active ca. 1750–90|French, Paris, active ca. 1750",,Le Faure|Mollier,French|French,1725 |1725,1815 |1775,ca. 1750,1725,1775,"Steel, gold, wood (walnut), silver",L. 58 in. (147.3 cm),"Purchase, Annie Laurie Aitken Charitable Trust Gift, 1990",,Paris,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.203,false,true,23015,Arms and Armor,Double-barreled flintlock shotgun,Double-Barreled Flintlock Shotgun,"French, Paris",,,,,Gunsmith|Barrelsmith,,François Pirmet|Jean Le Clerc,"French, Paris, recorded 1779–1818|French, Paris, recorded 1807–10",,"Pirmet, François|Le Clerc, Jean",French|French,1779 |1807,1818 |1810,dated 1809,1809,1809,"Steel, wood (walnut), silver, gold",L. 48 in. (121.9 cm); L. of barrel 32 1/2 in. (82.6 cm); L. of lock 5 1/4 in. (13.3 cm); Cal. .59 in. (15.1 mm); Wt. 6 lb. 10 oz. (3000 g),"Rogers Fund, 1927",,Paris,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/23015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.136,false,true,24891,Arms and Armor,Hunting sword,Hunting Sword of Prince Camillo Borghese (1775–1832),"French, Paris",,,,,Goldsmith|Maker,,Antoine-Modeste Fournera|François Pirmet,"French, Paris, documented 1806–17|French, Paris, recorded 1779–1818",,"Fournera, Antoine-Modeste|Pirmet, François",French|French,1806 |1779,1817 |1818,1809–13,1809,1813,"Silver-gilt, steel, leather, mother-of-pearl",L. with scabbard 27 in. (68.6 cm); Wt. with scabbard 1 lb. 10 oz. (737 g); L. of sword 25 7/16 in. (64.6 cm); W. of hilt 4 5/8 in. (11.7 cm); L. of blade 19 3/4 in. (50.2 cm); W. of blade 1 3/16 in. (3 cm); L. of scabbard 21 3/8 in. (54.3 cm),"Purchase, The Sulzberger Foundation, Inc. and David G. Alexander Gifts, and Bequest of Stephen V. Grancsay, by exchange, 1982",,Paris,,,,,,,,,,Swords-Hunting,,http://www.metmuseum.org/art/collection/search/24891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.229,false,true,27164,Arms and Armor,Sword hilt,Sword Hilt,"French, Paris",,,,,Designer|Goldsmith,Designed and modeled by|Executed by,Albert-Ernest Carrier-Belleuse|Lucien Falize,"French, Anizy-le-Château 1824–1887 Sèvres|French, Paris, 1842–1897",,"Carrier-Belleuse, Albert Ernest|Falize, Lucien",French|French,1824 |1842,1887 |1897,1881–82,1881,1882,"Bronze, gold",H. 6 1/2 in. (16.5 cm); W. 5 3/16 in. (13.2 cm); D. 4 1/2 in. (11.4 cm); Wt. 1 lb. 12 oz. (799.5 g),"Purchase, Gift of William H. Riggs, by exchange, 1989",,Paris,,,,,,,,,,Swords,,http://www.metmuseum.org/art/collection/search/27164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.905,false,true,625352,Arms and Armor,Ornament print from a firearms pattern book,Plate twelve from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver|Publisher,Published by,De Lacollombe|Gilles Demarteau,"French, Paris, active ca. 1702–ca. 1736|French, Liège 1722–1776 Paris",,"Lacollombe, De|Demarteau, Gilles",French|French,1702 |1722,1736 |1776,dated 1736,1736,1736,Engraving,7 1/2 x 9 3/8 in. (19 x 23.8 cm),"Purchase, Mr. and Mrs. Robert G. Goelet Gift, 2013",,Paris,,,,,,,,,,Works on Paper,,http://www.metmuseum.org/art/collection/search/625352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.906,false,true,625353,Arms and Armor,Ornament print from a firearms pattern book,Plate three from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver|Publisher,,De Lacollombe|Gilles Demarteau,"French, Paris, active ca. 1702–ca. 1736|French, Liège 1722–1776 Paris",,"Lacollombe, De|Demarteau, Gilles",French|French,1702 |1722,1736 |1776,ca. 1705–30,1680,1755,Engraving,10 1/2 x 7 1/2 in. (26.7 x 19.1 cm),"Purchase, Bequest of Stephen V. Grancsay, Rogers Fund, Helmut Nickel Gift, and funds from various donors, by exchange, 2013",,Paris,,,,,,,,,,Works on Paper,,http://www.metmuseum.org/art/collection/search/625353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.907,false,true,625354,Arms and Armor,Ornament print from a firearms pattern book,Plate seven from Nouveavx Desseins D'Arquebvseries,"French, Paris",,,,,Engraver|Publisher,,De Lacollombe|Gilles Demarteau,"French, Paris, active ca. 1702–ca. 1736|French, Liège 1722–1776 Paris",,"Lacollombe, De|Demarteau, Gilles",French|French,1702 |1722,1736 |1776,ca. 1705–30,1680,1755,Engraving,11 x 8 1/4 in. (27.9 x 20.9 cm),"Purchase, Bequest of Stephen V. Grancsay, Rogers Fund, Helmut Nickel Gift, and funds from various donors, by exchange, 2013",,Paris,,,,,,,,,,Works on Paper,,http://www.metmuseum.org/art/collection/search/625354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"04.3.64, .65; 14.25.454",false,true,626023,Arms and Armor,Three partisans,"Three Partisans Carried by the Bodyguard of Louis XIV (1638–1715, reigned from 1643)","French, Paris",,,,,Designer|Sword cutler,14.25.454 designed by|Inscription on 04.3.65 probably refers to,Jean Berain|Bonaventure Ravoisie,"French, Saint-Mihiel 1640–1711 Paris|French, Paris, recorded 1678–1709",,"Berain, Jean|Ravoisie, Bonaventure",French|French,1640 |1678,1711 |1709,ca. 1670–80,1645,1705,"Steel, blued and damascened with gold; wood; textile","04.3.64: Head, 20 3/4 x 5 7/8 in. (52.7 x 15 cm); Overall, 94 3/8 in. (239.7 cm); 04.3.65: Head, 22 9/16 x 6 1/2 in. (57.3 x 16.5 cm); Overall, 94 1/8 in. (239 cm); Head, 20 9/16 x 6 1/16 in. (52.2 x 15.4 cm); Overall, 86 11/16 in. (220.2 cm)","04.3.64, .65: Rogers Fund, 1904; 14.25.454: Gift of William H. Riggs, 1913",,,,,,,,,,,,Shafted Weapons,,http://www.metmuseum.org/art/collection/search/626023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"28.196.1a, b, .2a–l",false,true,23052,Arms and Armor,Cased pair of flintlock pistols with accessories,Cased Pair of Flintlock Pistols with Accessories,"French, Paris",,,,,Lock maker|Mount maker,Silver mounts hallmarked by,François Pirmet|Nicolas Noël Boutet,"French, Paris, recorded 1779–1818|French, Versailles and Paris, 1761–1833",,"Pirmet, François|Boutet, Nicholas-Noël",French|French,1779 |1761,1818 |1833,ca. 1810,1785,1835,"Steel, gold, wood (walnut, mahogany), silver, tortoiseshell, velvet","L. of each pistol 15 1/4 in. (38.7 cm); L. of each barrel 9 3/8 in. (23.8 cm); Cal. of barrel 28.196.1a, .48 in. (12 mm); Cal. of barrel 28.196.1b, .49 in. (12 mm); Wt. of each pistol 2 lb. 5 oz. (1050 g)","Rogers Fund, 1928",,Paris,,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/23052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"04.3.192, .193",false,true,21927,Arms and Armor,Pair of wheellock pistols,Pair of Wheellock Pistols,"French, Paris",,,,,Gunsmith|Engraver,Gilt foliate decoration attributed to,François Du Clos|Thomas Picquot,"French, Paris, recorded 1636–active ca. 1650|French, Paris, recorded 1636–38",,"Du Clos, François|Picquot, Thomas",French|French,1636 |1636,1675 |1638,ca. 1640,1615,1665,"Steel, gold, brass, wood, silver, mother-of-pearl",04.3.192; L. 23 1/8 in. (58.7 cm); L. of barrel 15 1/2 in. (39.4 cm); Cal. .52 in. (13.2 mm); Wt. 2 lb. 3 oz. (992 g) 04.3.193; L. 23 1/4 in. (59.1 cm); L. of barrel 15 1/2 in. (39.4 cm); Cal. .53 in. (13.5 mm); Wt. 2 lb. 3 oz. (992 g),"Rogers Fund, 1904",,Paris,,,,,,,,,,Firearms-Pistols-Wheellock,,http://www.metmuseum.org/art/collection/search/21927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.196.5–.6a–c,false,true,23050,Arms and Armor,Cased pair of double-barreled turn-off flintlock pistols,Cased Pair of Double-Barreled Turn-Off Flintlock Pistols,"French, Paris",,,,,Gunsmith|Engraver,Possibly,Jean Lepage|Fleury Montagny,"French, Paris, 1746–1834|French, born St. Étienne, February 4, 1760–died 1836, Marseilles",,"Lepage, Jean|Montagny, Fleury",French|French,1746 |1760,1834 |1836,ca. 1800,1775,1825,"Steel, wood (boxwood), brass, velvet",L. of each pistol 8 in. (20.3 cm); L. of each barrel 3 7/8 in. (9.8 cm); Cal. of each .46 in. (11.7 mm); Wt. of each pistol 1 lb. 5 oz. (600 g); bullet mould (28.196.6a); L. 5 3/4 in. (14.6 cm); Wt. 4.3 oz. (121.9 g); wrench (28.196.6b); L. 3 7/8 in. (9.8 cm); Wt. 2.6 oz. (73.7 g); case (28.196.6c); H. 3 7/16 in. (8.7 cm); W. 11 1/2 in. (29.2 cm); D. 6 7/8 in. (17.5 cm); Wt. 2 lb. 15 oz. (1332.4 g),"Rogers Fund, 1928",,Paris,,,,,,,,,,Firearms-Pistols-Flintlock,,http://www.metmuseum.org/art/collection/search/23050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.173,false,true,27552,Arms and Armor,Drawing,Design for the Right Pauldron of a Parade Armor,"French, Paris",,,,,Artist|Artist,or attributed to|Attributed to,Jean Cousin the Elder|Étienne Delaune,"French, Souci (?) ca. 1490–ca. 1560 Paris (?)|French, Orléans 1518/19–1583 Strasbourg",,"Cousin, Jean, the Elder|Delaune, Étienne",French|French,1485 |1518,1565 |1583,ca. 1555,1530,1580,Pen and ink with watercolor wash on paper,10 x 6 9/16 in. (25.4 x 16.6 cm),"Rogers Fund, 1954",,Paris,,,,,,,,,,Works on Paper-Drawings,,http://www.metmuseum.org/art/collection/search/27552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.104,false,true,23943,Arms and Armor,Detached flintlock,Detached Flintlock alla Fiorentina,"Italian, Bargi",,,,,Gunsmith|Gunsmith,or,"Matteo Cecchi, called Acquafresca|Sebastiano Cecchi, called Acquafresca","Italian, Bargi, 1651–1738|Italian, Bargi, 1619–1692",,"Cecchi, Matteo|Cecchi, Sebastiano",Italian|Italian,1651 |1619,1738 |1692,dated 1679,1679,1679,Steel,H. 2 3/4 in. (7.0 cm); W. 5 3/4 in. (14.6 cm),"Rogers Fund, 1933",,Bargi,,,,,,,,,,Firearms-Pistols-Snaphaunce,,http://www.metmuseum.org/art/collection/search/23943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.150.9,false,true,26585,Arms and Armor,Composed armor for man and horse,Composed Armor for Man and Horse,"European, Italy, Spain, Flanders and England",,,,,Armorer|Armorer,Left reinforcing elbow stamped with marks belonging to the|Shaffron stamped with marks attributed to,Missaglia workshop|Ambrogio de Osma,"Italian, Milan, recorded 1430–1529|Italian, Brescia, documented 1446–75",,"Missaglia workshop|Osma, Ambrogio de",Italian|Italian,1430 |1446,1529 |1475,ca. 1450–1525 and later,1425,1550,"Steel, brass, leather, textile (velvet, wool), iron",Wt. of armor for man 59 lb. 4 oz. (26.88 kg); Wt. of helmet 3 lb. 12 oz. (1697 g); 29.150.9v (mail shirt): H. 31 1/2 in. (80.0 cm); W. 45 11/16 in. (116.0 cm); W. of chest 24 in. (61.0 cm); Diam. (outside) of solid links 13/32 in. (10.2 mm); Diam. (inside) of solid links 9/32 in. (7.3 mm); Diam. (outside) of riveted links 3/8 in. (9.5 mm); Diam. (inside) of riveted links 9/32 in. (7.0 mm); Diam. (outside) of latten solid links 3/8 in. (9.1 mm); Diam. (inside) of latten solid links 1/4 in. (6.4 mm); Diam. (outside) of latten riveted links 3/8 in. (9.3 mm); Diam. (inside) of lattened riveted links 5/16 in. (7.8 mm); Diam. (outside) of collar links 11/32 in. (8.5 mm); Diam. (inside) of collar links 1/4 in. (6.2 mm).,"Bashford Dean Memorial Collection, Bequest of Bashford Dean, 1928",,,,,,,,,,,,Armor for Horse and Man,,http://www.metmuseum.org/art/collection/search/26585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.163.5,false,true,24811,Arms and Armor,Carbine,Carbine with Flintlock alla Fiorentina,"Italian, Brescia",,,,,Steel-chiseler|Barrelsmith,,Carlo Bottarelli|Giovanni Lazzarino Cominazzo,"Italian, Brescia, active ca. 1660–90|Italian, Brescia, active mid-17th century",,"Bottarelli, Carlo|Cominazzo, Giovanni Lazzarino",Italian|Italian,1635 |1625,1715 |1675,ca. 1660–70,1635,1695,"Steel, wood (walnut)",L. 37 3/4 in. (95.9 cm),"Gift of Alan Rutherfurd Stuyvesant, 1949",,Brescia,,,,,,,,,,Firearms,,http://www.metmuseum.org/art/collection/search/24811,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"47.110.1, .2",false,true,24784,Arms and Armor,Pair of wheellock pistols,Pair of Wheellock Pistols,"Italian, Brescia",,,,,Lock maker|Barrelsmith,Pistols made and decorated by,Giovan Antonio Gavacciolo|Lazarino Cominazzo,"Italian, Brescia, active mid-17th century|Italian, Brescia, active mid-17th century",,"Gavacciolo, Giovan Antonio|Cominazzo, Lazarino",Italian|Italian,1625 |1625,1675 |1675,mid-17th century,1625,1675,"Steel, wood (walnut)",L. of 47.110.1: 22 11/16 in. (57.7 cm); Cal. of 47.110.1: .495 in. (12.6 mm); Wt. of 47.110.1: 2 lb. 3 oz. (997 g); L. of 47.110.2: 22 13/16 in. (57.9 cm); Cal. of 47.110.2: .485 in. (12.3 mm); Wt. of 47.110.2: 2 lb. 3 oz. (1003 g),"Rogers Fund, 1947",,Brescia,,,,,,,,,,Firearms-Pistols-Wheellock,,http://www.metmuseum.org/art/collection/search/24784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.137a–j,false,true,24941,Arms and Armor,Percussion revolver with case,"Cased Six-Barreled Revolving Percussion Pistol (""Pepperbox"")","American, Norwich, Connecticut",,,,,Manufacturer|Manufacturer,,Ethan Allen|Charles T. Thurber,"American, 1808–1871|American, active 1837–55",,"Allen|Thurber, Charles T.",American|American,1808 |1837,1871 |1855,1842–47,1842,1847,"Steel, gold, silver, ivory, wood (rosewood), velvet",L. of pistol 7 in. (17.78 cm); case 12 15/16 x 8 3/16 x 2 5/8 in. (32.84 x 20.78 x 6.65 cm),"Gift of Eric Vaule, 1990",,Norwich,Connecticut,,,,,,,,,Firearms-Pistols-Percussion,,http://www.metmuseum.org/art/collection/search/24941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.375,false,true,26562,Arms and Armor,Percussion target rifle,Percussion Target Rifle,"American, New Bedford, Massachusetts",,,,,Gunsmith|Gunsmith,,Julius Grudchos|Selmar Eggers,"American, active ca. 1856–1860|American, active ca. 1856–1860",,Grudchos Julius|Eggers Selmar,American|American,1856 |1856,1860 |1860,ca. 1855–60,1830,1885,"Wood (walnut), steel, silver, gold, baleen, ivory",L. 49 1/4 in. (125.1 cm),"Purchase, Bashford Dean Memorial Collection, Funds from various donors, by exchange, 1992",,New Bedford,Massachusetts,,,,,,,,,Firearms-Guns-Percussion,,http://www.metmuseum.org/art/collection/search/26562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.143.3,false,true,24847,Arms and Armor,Percussion revolver,"Colt Navy Percussion Revolver, Confederate Model, serial no. 2651","American, Griswoldville, Georgia",,,,,Designer|Manufacturer,,Samuel Colt|The Griswold and Grier Company,"American, Hartford, Connecticut 1814–1862|American, 19th century",,"Colt, Samuel|The Griswold and Grier Company",American|American,1814 |1801,1862 |1900,1862–64,1862,1864,"Steel, brass, wood",L. 13 1/8 in. (33.3 cm); L. of barrel 7 1/2 in. (19.1 cm); Cal. .36 in. (9 mm),"Gift of John E. Parsons, 1959",,Griswoldville,Georgia,,,,,,,,,Firearms-Pistols-Revolvers,,http://www.metmuseum.org/art/collection/search/24847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.699,false,true,675957,Arms and Armor,Revolver,"Colt Model 1862 Police Revolver, Serial No. 38549","American, Hartford, Connecticut and New York",,,,,Manufacturer|Designer,Grip designed by,Colt's Patent Fire Arms Manufacturing Company|John Quincy Adams Ward,"American, Hartford, Connecticut, 1855–present|American, Urbana, Ohio 1830–1910 New York",,"Colt's Patent Fire Arms Manufacturing Company|Ward, John Quincy Adams",American|American,1855 |1830,2014 |1910,ca. 1868,1840,1890,"Steel, gold, copper alloy (brass)",L. of pistol 11 in. (27.9 cm); Cal. .36 in. (9.1 mm),"Gift of W. C. Foxley, 2014",,Hartford|New York,Connecticut|New York,,,,,,,,,Firearms-Pistols,,http://www.metmuseum.org/art/collection/search/675957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2028,false,true,156809,Costume Institute,Fan,Fan,Dutch,,,,,Artist,,Carl Gustav Klingstedt,"Swedish, 1657–1734",,Klingstedt Carl Gustav,Swedish,1657,1734,1715–25,1715,1725,"ivory, mother-of-pearl, parchment, gouache, paint",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Millicent V. Hearst, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2004.28a, b",false,true,98556,Costume Institute,Necklace,Necklace,Swiss,,,,,Designer,,Jean Dunand,"French (born Switzerland), Lancy 1877–1942 Paris",,"Dunand, Jean","French, born Switzerland",1877,1942,ca. 1927,1922,1932,"(a,b) metal, lacquer",,"Purchase, Friends of The Costume Institute Gifts, 2004",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/98556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.61.7a, b",false,true,100854,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Francevramant,"France, founded 1935",,Francevramant,France,1935,1935,1934–36,1934,1936,silk,,"Gift of Mrs. Leonard Feist, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/100854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3041a–c,false,true,157935,Costume Institute,Wedding Corset,Wedding corset,French,,,,,Department Store|Manufacturer,,Stern Brothers|Corset Parisien,"American, founded New York, 1867|French",,Stern Brothers|Corset Parisien,American|French,1867,2001,1881–82,1881,1882,"silk, baleen, cotton, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. M. Disbrow, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.95,false,true,81638,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1919,1914,1924,"silk, fur, metallic thread",,"Gift of Anita Zahn, 1974",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.69,false,true,81635,Costume Institute,Dress,Dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1920,1915,1925,"silk, metal",,"Purchase, Barbara and Gregory Reynolds Gift, 1992",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.274,false,true,81637,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,early 1920s,1920,1925,"silk, metallic thread",,"Gift of Anita Zahn, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.465,false,true,135706,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,early 1920s,1920,1925,"silk, metal",,"Gift of Richard and Judith Webb, 2006",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/135706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.273,false,true,145416,Costume Institute,Robe,Robe,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,1920s,1920,1929,"silk, metallic",,"Gift of Jacqueline Loewe Fowler, 2008",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/145416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.134.15,false,true,81636,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,1920–23,1920,1923,"silk, metallic thread",,"Gift of Alice Roosevelt Longworth, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.45.91.2,false,true,81521,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1925,1920,1930,"silk, metal thread",,"Gift of Mrs. Aline Bernstein, 1945",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.45.91.3,false,true,109551,Costume Institute,Blouse,Blouse,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1925,1925,1930,silk,,"Gift of Mrs. Aline Bernstein, 1945",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.45.91.5,false,true,96129,Costume Institute,Dress,Dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,1925,1925,1925,[no medium available],,"Gift of Mrs. Aline Bernstein, 1945",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.46.16.12,false,true,96130,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,1924–25,1924,1925,silk,,"Gift of Mrs. Sophie Gimbel, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1980.443.2a, b",false,true,81639,Costume Institute,Tea gown,Tea gown,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,1917,1917,1917,"silk, metallic thread, cotton",,"Gift of Mrs. Benjamin H. Namm, 1980",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.490a, b",false,true,159027,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1920,1918,1922,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of George Mangini, 1971",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.66.14.2,true,true,81136,Costume Institute,Waistcoat,Waistcoat,British,,,,,Designer|Manufacturer,Textile by|Textile by,Anna Maria Garthwaite|Peter Lekeux,"British, 1690–1763|British, 1716–1768",,"Garthwaite, Anna Maria|Lekeux Peter",British|British,1690 |1716,1763 |1768,1747,1747,1747,"silk, wool, metallic",,"Purchase, Irene Lewisohn Bequest, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7540a–c,false,true,174593,Costume Institute,Walking suit,Walking suit,British,,,,,Design House|Designer,,House of Lucile|Lucile,"British, founded 1895|British, 1863–1935",,Lucile House of|Lucile,British|British,1895 |1863,1895 |1935,1910–12,1910,1912,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.116.1,false,true,81634,Costume Institute,Evening coat,Evening coat,Italian,,,,,Designer,Attributed to,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,early 1920s,1920,1925,silk,,"Gift of Beatrice S. Bartlett, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.506.1,false,true,80217,Costume Institute,Dress,Dress,Italian,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1926,1921,1931,"silk, metal",,"Gift of Richard and Judith Webb, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.506.2,false,true,80218,Costume Institute,Dress,Dress,Italian,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1926,1921,1931,"silk, metal",,"Gift of Richard and Judith Webb, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.506.3,false,true,80219,Costume Institute,Coat,Coat,Italian,,,,,Designer,,Vitaldi Babani,"French, born Middle East, active 1895–1940",,"Babani, Vitaldi","French, born Middle East",1895,1940,ca. 1926,1921,1931,"silk, metal",,"Gift of Richard and Judith Webb, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.184.4a–c,false,true,84070,Costume Institute,Costume,Costume,American,,,,,Designer,,Léon Bakst,"Russian, Grodno 1866–1924 Paris",,"Bakst, Léon",Russian,1866,1924,1922–23,1922,1923,"silk, cotton, metallic thread, glass, plastic",,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.190.149,false,true,141787,Costume Institute,Pendant,"""Mobius""",American,,,,,Designer,,Georg Jensen,"Danish, Rådvad 1866–1935 Hellerup",,"Georg, Jensen",Danish,1866,1935,1969,1969,1969,metal,,"Gift of Muriel Kallis Newman, 2008",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/141787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2010.487.6a, b",false,true,159725,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Catherine Donovan,"American (born Ireland), 1826 (?)–1906",,"Donovan, Catherine","American, born Ireland",1826,1906,1890s,1890,1899,"silk, cotton",,"Gift of Christopher Scholz and Ines Elskop, 2010",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.990a, b",false,true,159582,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Catherine Donovan,"American (born Ireland), 1826 (?)–1906",,"Donovan, Catherine","American, born Ireland",1826,1906,1900–1903,1900,1903,"silk, linen, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of George R. Cook III, 1980",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.375a–c,false,true,158731,Costume Institute,Ensemble,Ensemble,American,,,,,Designer,,Catherine Donovan,"American (born Ireland), 1826 (?)–1906",,"Donovan, Catherine","American, born Ireland",1826,1906,1900–1903,1900,1903,"silk, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Daniel M. McKeon and Robert Hoguet, Jr., 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158731,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6290a, b",false,true,173337,Costume Institute,Afternoon dress,Afternoon dress,American,,,,,Designer,,Catherine Donovan,"American (born Ireland), 1826 (?)–1906",,"Donovan, Catherine","American, born Ireland",1826,1906,ca. 1883,1881,1885,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the Princess Viggo in accordance with the wishes of the Misses Hewitt, 1931",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7535a–d,false,true,174588,Costume Institute,Dress,Dress,American,,,,,Designer,,Catherine Donovan,"American (born Ireland), 1826 (?)–1906",,"Donovan, Catherine","American, born Ireland",1826,1906,ca. 1885,1883,1887,Cotton,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3120a–c,false,true,158022,Costume Institute,Waist Cincher,Waist cincher,probably French,,,,,Design House,Attributed to,Redfern,1847–1940,,Redfern,,1847,1940,ca. 1900,1898,1902,"silk, bone, metal, elastic, cotton",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1240,false,true,155934,Costume Institute,Evening overdress,Evening overdress,probably French,,,,,Design House,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,,1895,1937,ca. 1920,1918,1922,"silk, linen, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1955",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.40.130.2a–c,false,true,106752,Costume Institute,Ensemble,Ensemble,American or European,,,,,Design House,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,,1895,1937,1875–1925,1875,1925,silk,,"Gift of Miss Mercedes de Acosta, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1771,false,true,156523,Costume Institute,Bonnet,Bonnet,probably French,,,,,Design House,,Redfern,1847–1940,,Redfern,,1847,1940,ca. 1888,1886,1890,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. James Dowd Lester, 1942",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3278a–c,false,true,158197,Costume Institute,Waist Cincher,Waist cincher,French,,,,,Design House,,Redfern,1847–1940,,Redfern,,1847,1940,1900–1910,1900,1910,"silk, bone, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. George A. Bonaventure in memory of Mrs. James Steel, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3768a–c,false,true,158740,Costume Institute,Ensemble,Ensemble,French,,,,,Design House,,Redfern,1847–1940,,Redfern,,1847,1940,ca. 1930,1928,1932,"silk, wool, beads, metal, fur",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert S. Kilborne, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.732a, b",false,true,693846,Costume Institute,Dress,Dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",,1889 |1867,9999 |1946,winter 1926–27,1926,1927,"silk, metal",,"Gift of Dean L. Merceron, 2015",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/693846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.3.32a–e,false,true,107066,Costume Institute,Ensemble,Ensemble,French,,,,,Designer,,Redfern,1847–1940,,Redfern,,1847,1940,1887–89,1887,1889,"wool, silk, cotton, metallic thread",,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.258,false,true,157422,Costume Institute,Dinner dress,Dinner dress,British,,,,,Design House,,Redfern,1847–1940,,Redfern,,1847,1940,1909–11,1909,1911,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert S. Kilborne, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3189,false,true,158098,Costume Institute,Evening cape,Evening cape,British,,,,,Design House,,Redfern,1847–1940,,Redfern,,1847,1940,1901,1901,1901,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert S. Kilborne, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.107a, b",false,true,155754,Costume Institute,Walking suit,Walking suit,British,,,,,Design House,,Redfern,1847–1940,,Redfern,,1847,1940,ca. 1910,1908,1912,wool,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. J. W. Post, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6575a, b",false,true,173574,Costume Institute,Dinner dress,Dinner dress,American,,,,,Retailer,,"Wechsler, Abraham & Company",,,"Wechsler, Abraham & Company",,1865,1994,1876–78,1876,1878,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. James McF. Baker, 1948",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1981.515.1a, b",false,true,92336,Costume Institute,Dress,Dress,American,,,,,Designer,,Redfern,1847–1940,,Redfern,,1847,1940,ca. 1892,1887,1897,"silk, cotton",,"Gift of Mrs. Peter H. B. Frelinghuysen, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/92336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3009a–d,false,true,157899,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Mme. Olympe,"American, born France, 1830",,Olympe Mme.,,1830,1930,ca. 1865,1863,1867,"silk, mother-of-pearl",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. H. E. Rifflard, 1932",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.69.9.1,false,true,106486,Costume Institute,Jacket,Jacket,probably French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,1902–3,1902,1903,"fur, silk",,"Gift of Mr. Hayward R. Alker, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.38.98.2,false,true,106446,Costume Institute,Evening coat,Evening coat,American or European,,,,,Designer,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1908–10,1908,1910,"silk, fur",,"Gift of Mrs. Sidney W. Ffoulkes, 1938",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.45.36.2,false,true,106461,Costume Institute,Dress,Dress,French,,,,,Design House,,Beer,French,,Beer,French,,1929,late 19th–early 20th century,1875,1925,[no medium available],,"Gift of Mrs. George Kent, 1945",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.64.46.3a, b",false,true,99746,Costume Institute,Dress,Dress,French,,,,,Design House,,Beer,French,,Beer,French,,1929,ca. 1925,1920,1930,"silk, metallic thread, beading",,"Gift of Mrs. William Dubilier, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/99746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4084a, b",false,true,170488,Costume Institute,Wedding Shoes,Wedding shoes,French,,,,,Maker|Retailer,,Esté|R.W.H. Rogers,"French, 1821–1839",,Esté|Rogers R.W.H.,French,1821,1839,1840–49,1840,1849,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Chauncey E. Low, 1924",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170488,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3024,false,true,157916,Costume Institute,Evening coat,Evening coat,French,,,,,Design House,,Rouff,"French, 1844–1914",,Rouff,French,1844,1914,1895–1905,1895,1905,"silk, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. William E. S. Griswold, 1941",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.332a, b",false,true,158254,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Rouff,"French, 1844–1914",,Rouff,French,1844,1914,ca. 1897,1895,1899,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Jason Westerfield, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.376a, b",false,true,158742,Costume Institute,Dinner dress,Dinner dress,French,,,,,Design House,,Rouff,"French, 1844–1914",,Rouff,French,1844,1914,1900–1903,1900,1903,"silk, jet beads, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Daniel M. McKeon and Robert Hoguet, Jr., 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.2339a, b",false,true,157153,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Rouff,"French, 1844–1914",,Rouff,French,1844,1914,ca. 1895,1893,1897,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. William E. S. Griswold, 1941",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6476a, b",false,true,173496,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Rouff,"French, 1844–1914",,Rouff,French,1844,1914,1883–96,1883,1896,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. William E. S. Griswold, 1941",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6477a, b",false,true,173497,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Rouff,"French, 1844–1914",,Rouff,French,1844,1914,ca. 1895,1893,1897,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. William E. S. Griswold, 1941",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173497,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.8354a, b",false,true,175365,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Rouff,"French, 1844–1914",,Rouff,French,1844,1914,1896–97,1896,1897,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1990",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7006a, b",false,true,173996,Costume Institute,Dress,Dress,French,,,,,Design House,,Chéruit,"French, 1906–1935",,Chéruit,French,1906,1935,1912,1912,1912,Cotton,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the estate of Valerie Dreyfus, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.47.6,false,true,99657,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Myrbor,"French, 1922–1936",,Myrbor,French,1922,1936,1923,1923,1923,silk,,"Gift of Miss Ida Brenner, 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/99657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3247,false,true,158163,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Myrbor,"French, 1922–1936",,Myrbor,French,1922,1936,ca. 1926,1924,1928,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. V. D. Crisp, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3248,false,true,158164,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Myrbor,"French, 1922–1936",,Myrbor,French,1922,1936,1924,1924,1924,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. V. D. Crisp, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158164,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3246a, b",false,true,158162,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Design House,,Myrbor,"French, 1922–1936",,Myrbor,French,1922,1936,1929,1929,1929,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. V. D. Crisp, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.56.21,false,true,99596,Costume Institute,Evening coat,Evening coat,French,,,,,Design House,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,1928,1928,1928,"silk, fur, glass, embroidery",,"Gift of Mrs. B. A. Goodman, in memory of Mrs. Gussie A. Matz, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/99596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.59.55,false,true,99597,Costume Institute,Evening wrap,Evening wrap,French,,,,,Design House,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1923,1918,1928,"silk, fur",,"Gift of Mrs. Gustavus Ober Jr., 1959",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/99597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.74.28,false,true,128422,Costume Institute,Cape,Cape,French,,,,,Design House,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1988,1983,1993,"leather, fur",,"Gift of Muriel Kallis Newman, 2006",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/128422,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.8377,false,true,175388,Costume Institute,Stole,Stole,French,,,,,Design House,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1935,1933,1937,"Fur, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Lady Emilia Dreher Armstrong, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2003.256.3a, b",false,true,93634,Costume Institute,Accessory Set,Accessory set,French,,,,,Design House,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1960,1955,1965,"a) fur, rhinestone, metal, plastic; b) fur, silk",,"Gift of Laura Johnson, 2003",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/93634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1973.112a, b",false,true,84498,Costume Institute,Evening shoes,Evening shoes,French,,,,,Manufacturer,,"F. Pinet, Paris","French, founded 1855",,"F. Pinet, Paris",French,1855,1855,1926,1926,1926,silk,,"Gift of Mrs. James A. Cole, 1973",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"49.3.5a, b",false,true,105066,Costume Institute,Dress,Dress,French,,,,,Design House,,House of Rouff,"French, founded 1929",,Rouff House of,French,1929,1929,1905–7,1905,1907,silk,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.84,false,true,85998,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,fall/winter 1921–22,1921,1922,"silk, cotton, metal, glass",,"Gift of Sandra Grey-Fretty, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.50,false,true,94273,Costume Institute,Slip,Slip,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1927,1927,1927,"silk, cotton",,"Isabel Shults Fund, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.60,false,true,94277,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1927,1922,1932,"silk, metal",,"Isabel Shults Fund, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.95,false,true,80789,Costume Institute,Slip,Slip,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1927,1922,1932,"silk, cotton",,"Gift of Martin M. Kamer, Switzerland, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.64,false,true,631535,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,winter 1916–17,1916,1917,"silk, glass, metal",,"Millia Davenport and Zipporah Fleisher Fund, 2013",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/631535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.95.3,false,true,86000,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925–26,1925,1926,"silk, metal thread",,"Gift of Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.95.4,false,true,85995,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925,1925,1925,silk,,"Gift of Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.3.22,false,true,105415,Costume Institute,Negligée,Negligée,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1898–1902,1898,1902,silk,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +40.182.9,false,true,86024,Costume Institute,Ball gown,Ball gown,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1897–1905,1897,1905,"silk, metal thread",,"Gift of Grace Rainey Rogers, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.40.4,false,true,82547,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926–27,1926,1927,"silk, metal thread",,"Gift of Mrs. John Magnin, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.37.2,false,true,106439,Costume Institute,Evening coat,Evening coat,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1900,1895,1905,"wool, silk, fur",,"Gift of Mrs. G. Macculloch Miller, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.134.11,false,true,101637,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1900,1900,1900,"silk, metal thread",,"Gift of Susan Dwight Bliss, 1935",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/101637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.303.1,false,true,84506,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,fall/winter 1920–21,1920,1921,"silk, metallic thread",,"Gift of David Toser, 1977",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.303.2,false,true,85997,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1922–23,1922,1923,"silk, glass, metallic threads and cellophane",,"Gift of David Toser, 1977",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.288.8,false,true,86008,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1925,1920,1930,"silk, metallic thread, glass",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.288.9,false,true,86009,Costume Institute,Dance dress,Dance dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926–27,1926,1927,"silk, cotton, metallic thread, glass",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.380.2,true,true,81139,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1910–14,1910,1914,"cotton, silk, metal",,"The Jacqueline Loewe Fowler Costume Collection, Gift of Jacqueline Loewe Fowler, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.46.4.8,false,true,82093,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1922,1922,1922,"silk, beads",,"Gift of Mrs. Harrison Williams, Lady Mendl, and Mrs. Ector Munn, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.113,false,true,86002,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925–26,1925,1926,"silk, silver thread",,"Gift of Mrs. Nathaniel Bowdich Potter, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.63.2.2,false,true,86019,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1930,1930,1930,silk,,"Gift of Mrs. Russell W. Davenport, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.288.10,false,true,83188,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1924,1919,1929,"cotton, metallic thread, glass",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.288.12,false,true,86011,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1924–25,1924,1925,"cotton, plastic, metallic thread",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"44.95.1a, b",false,true,86005,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925,1925,1925,silk,,"Gift of Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"44.95.2a, b",false,true,86006,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925,1925,1925,silk,,"Gift of Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.40.27.2,false,true,83430,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1909–10,1909,1910,silk,,"Gift of Miss Agnes Miles Carpenter, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.43.13.5,false,true,85993,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,early 1920s,1920,1925,silk,,"Gift of Mrs. John Jay Whitehead, 1943",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.97.1,false,true,86026,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1918–19,1918,1919,silk,,"Purchase, Irene Lewisohn Bequest, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.97.4,false,true,86028,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1913,1913,1913,"silk, sequins",,"Purchase, Irene Lewisohn Bequest, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.97.6,false,true,106735,Costume Institute,Evening coat,Evening coat,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1916–17,1916,1917,"silk, metallic, fur",,"Purchase, Irene Lewisohn Bequest, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.52.19.2,false,true,86003,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1920s,1920,1929,"silk, metallic",,"Gift of Madame Veronique Wolf and Madame Frederic Bon, 1952",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.52.19.3,false,true,86029,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1919,1919,1919,"silk, metallic",,"Gift of Madame Veronique Wolf and Madame Frederic Bon, 1952",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.65.16.2,false,true,96131,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1908,1908,1908,[no medium available],,"Gift of Mrs. John C. Tomlinson, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.66.43.1,false,true,86031,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1910,1905,1915,silk,,"Gift of Mrs. William M. Haupt, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.66.58.3,false,true,99777,Costume Institute,Tea gown,Tea gown,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,late 1920s,1925,1929,silk,,"Gift of Mrs. Leon L. Roos, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/99777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"35.134.6a, b",false,true,86023,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1899,1899,1899,silk,,"Gift of Susan Dwight Bliss, 1935",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.10,false,true,84585,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1923–24,1923,1924,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.12,false,true,82550,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925–26,1925,1926,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.15,false,true,85994,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1924–25,1924,1925,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.65,false,true,86018,Costume Institute,Scarf,Scarf,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925–26,1925,1926,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.57.22.11,false,true,86030,Costume Institute,Evening wrap,Evening wrap,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1914,1914,1914,silk,,"Gift of Estate of Valerie Dreyfus, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.58.34.11,false,true,82610,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1926,1921,1931,silk,,"Gift of Mrs. John Chambers Hughes, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1973.104.3a, b",false,true,110042,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1922,1917,1927,[no medium available],,"Gift of Mrs. Leon L. Roos, 1973",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/110042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.184.5a, b",false,true,83224,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1915,1915,1915,"cotton, silk",,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.288.7a, b",true,true,81113,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1924,1919,1929,"wool, silk, metallic thread",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1983.290.1a, b",false,true,86021,Costume Institute,Ensemble,Ensemble,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1929,1924,1934,"silk, fur",,"Gift of Isabel Shults, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.288.11a, b",false,true,86010,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925–26,1925,1926,"cotton, silk, plastic, glass",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.40.37.2a, b",false,true,86014,Costume Institute,Coat,Coat,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1920s,1920,1929,silk,,"Gift of Mrs. William Bamberger, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.40.90.6a, b",false,true,92226,Costume Institute,Suit,Suit,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1915–17,1915,1917,wool,,"Gift of Mme. Louis Cerlian, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/92226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.41.107a–c,false,true,84937,Costume Institute,Cocktail Suit,Cocktail suit,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1935,1935,1935,silk,,"Gift of Mrs. William Bamberger, 1941",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.46.46.8a, b",false,true,86001,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1920s,1920,1929,[no medium available],,"Gift of Mrs. Harrison Williams, Lady Mendl, and Mrs. Ector Munn, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.51.97.2a, b",false,true,86027,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1915–16,1915,1916,"silk, metallic",,"Purchase, Irene Lewisohn Bequest, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.52.19.1a, b",false,true,86007,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1920s,1920,1929,"silk, metallic",,"Gift of Madame Veronique Wolf and Madame Frederic Bon, 1952",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.60.42.6a, b",false,true,86025,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,fall/winter 1910–11,1910,1911,"silk, metal, glass",,"Gift of Mrs. Howard Crosby Brokaw, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.288.13a–c,false,true,86012,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1923,1918,1928,"silk, cotton, plastic",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.44.64.11a, b",false,true,86004,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925–26,1925,1926,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.44.64.14a, b",false,true,85999,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926,1926,1926,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.51.97.25a, b",false,true,106867,Costume Institute,Suit,Suit,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1917,1917,1917,"wool, fur",,"Purchase, Irene Lewisohn Bequest, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.67.64.1a–c,false,true,96117,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1920–22,1920,1922,[no medium available],,"Gift of Ms. Ruth T. Constantino, 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.67.64.2a–c,false,true,85996,Costume Institute,Dress,Dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1920–22,1920,1922,[no medium available],,"Gift of Ruth T. Costantino, 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.13a–c,false,true,84586,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1925–26,1925,1926,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.19a–d,false,true,82551,Costume Institute,Pajamas,Pajamas,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926–27,1926,1927,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.70.18a–d,false,true,82632,Costume Institute,Evening dress,Evening dress,French,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1922,1922,1922,"silk, whalebone, steel",,"Gift of Mrs. Robert Lovett, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.51.97.10a, b",false,true,83256,Costume Institute,Dress,Dress,French,,,,,Design House,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1918–19,1918,1919,cotton,,"Purchase, Irene Lewisohn Bequest, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7477,false,true,174537,Costume Institute,Evening bodice,Evening bodice,French,,,,,Designer|Design House,Possibly|Possibly,Charles Frederick Worth|Worth and Bobergh,"French (born England), Bourne 1825–1895 Paris",,"Worth, Charles Frederick|Worth and Bobergh",French,1825,1895,ca. 1865,1863,1867,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Roland A. Goodman, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174537,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1372a–d,false,true,156080,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer|Design House,,Charles Frederick Worth|Worth and Bobergh,"French (born England), Bourne 1825–1895 Paris",,"Worth, Charles Frederick|Worth and Bobergh",French,1825,1895,1862–65,1862,1865,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1987",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4249a, b",false,true,168479,Costume Institute,Evening slippers,Evening slippers,French,,,,,Maker,,Esté,"French, 1821–1839",,Esté,French,1821,1839,1860–69,1860,1869,"Silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Miriam Storrs Coe, 1934",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/168479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.10a–t,false,true,83332,Costume Institute,Book,"Le bonheur du jour; ou, Les graces a la mode",French,,,,,Artist,,George Barbier,"French, Nantes 1882–1932 Paris",,"Barbier, George",French,1882,1932,1924,1924,1924,paper,,"Purchase, The Paul D. Schurgot Foundation Inc. Gift, 2002",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1254,false,true,155948,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Georges Doeuillet,"French, 1865–1929",,"Doeuillet, Georges",French,1865,1929,1926–28,1926,1928,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mark Mooring, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1338,false,true,156042,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Georges Doeuillet,"French, 1865–1929",,"Doeuillet, Georges",French,1865,1929,1910–13,1910,1913,"silk, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.4949,false,true,169172,Costume Institute,Evening stole,Evening stole,French,,,,,Designer,,Mme. Jeanne Paquin,"French, 1869–1936",,Paquin Jeanne,French,1869,1936,1920–30,1920,1930,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the estate of Valerie Dreyfus, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.328.3,false,true,94862,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1911–15,1911,1915,"silk, glass",,"Isabel Shults Fund, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.328.4,false,true,94863,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1910–14,1910,1914,"silk, glass",,"Isabel Shults Fund, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.328.5,false,true,94864,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1910–14,1910,1914,"silk, glass",,"Isabel Shults Fund, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.328.8,false,true,94865,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1913–14,1913,1914,"silk, metal",,"Isabel Shults Fund, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.328.9,false,true,94866,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1910–14,1910,1914,"silk, glass",,"Isabel Shults Fund, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.49.2.4,false,true,94861,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1912,1912,1912,silk,,"Gift of Howard Sturges, in memory of his mother, Mrs. Howard O. Sturges, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.49.2.5,false,true,84591,Costume Institute,Dress,Dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1907,1907,1907,silk,,"Gift of Howard Sturges, in memory of his mother, Mrs. Howard O. Sturges, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.328.10,false,true,94860,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1910–14,1910,1914,"silk, metal",,"Isabel Shults Fund, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.37.51.1,false,true,94859,Costume Institute,Opera cape,Opera cape,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,ca. 1905,1900,1910,silk,,"Gift of V. Everett Macy Estate, 1937",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1995.5.1a, b",false,true,80189,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1905,1905,1905,"silk, metal",,"Purchase, Irene Lewisohn Trust Gift, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"35.134.10a, b",false,true,94869,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1906–7,1906,1907,silk,,"Gift of Susan Dwight Bliss, 1935",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.56.51a, b",false,true,104767,Costume Institute,Riding Habit,Riding habit,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1910,1910,1910,"wool, silk",,"Gift of Mrs. Albert Ten Eyck Gardner, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/104767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.50.40.4a, b",false,true,94867,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1901–5,1901,1905,"silk, metallic, glass",,"Gift of Estate of Annie-May Hegeman, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.64.7.5a–c,false,true,83435,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1913–14,1913,1914,"(a) silk, metallic thread, glass beading; (b, c) silk, leather, metallic thread",,"Gift of Mrs. David J. Colton, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.374a, b",false,true,158720,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1894–96,1894,1896,"cotton, silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Daniel M. McKeon and Robert Hoguet, Jr., 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.62.36.4a–c,false,true,107166,Costume Institute,Dress,Dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,ca. 1892,1887,1897,silk,,"Gift of Mrs. Ogden W. Ross, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3098a, b",false,true,157996,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,ca. 1900,1898,1902,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Marion Litchfield, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.430a–c,false,true,158961,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,ca. 1903,1901,1905,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.337.1,false,true,99580,Costume Institute,Evening cape,Evening cape,French,,,,,Designer,,Chéruit,"French, 1906–1935",,Chéruit,French,1906,1935,ca. 1920,1915,1925,silk,,"Gift of the Estate of Mrs. Julia M. Weldon from Mary McDougall, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/99580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.97.5,false,true,106775,Costume Institute,Evening wrap,Evening wrap,French,,,,,Designer,,Chéruit,"French, 1906–1935",,Chéruit,French,1906,1935,1918–19,1918,1919,silk,,"Purchase, Irene Lewisohn Bequest, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.58.42.3,false,true,106482,Costume Institute,Evening wrap,Evening wrap,French,,,,,Designer,,Chéruit,"French, 1906–1935",,Chéruit,French,1906,1935,1902–4,1902,1904,"silk, metal",,"Gift of Mrs. Lawrence Tibbett, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.46.4.21a, b",false,true,82106,Costume Institute,Tea gown,Tea gown,French,,,,,Designer,,Chéruit,"French, 1906–1935",,Chéruit,French,1906,1935,1922,1922,1922,silk,,"Gift of Mrs. Harrison Williams, Lady Mendl, and Mrs. Ector Munn, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.264,false,true,106763,Costume Institute,Duster,Duster,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,1914–20,1914,1920,silk,,"Gift of Charles D. Wood in honor of Kathryn Wood, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.359,false,true,80823,Costume Institute,Coat,Coat,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1931,1926,1936,"fur, silk",,"Gift of Douglas Dillon, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.501,false,true,81868,Costume Institute,Coat,Coat,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1950,1945,1955,fur,,"Gift of Gilbert S. Kahn in memory of Janet Annenberg Hooker, 2000",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.60.11,false,true,122982,Costume Institute,Cape,Cape,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,1940s,1940,1949,fur,,"Gift of Roger Goiran, 1977",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/122982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.297,false,true,157854,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1926,1924,1928,"fur, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward H. Pflueger in memory of Florence Hazard Murphy, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.455,false,true,158988,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1930,1928,1932,fur,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.58.67.14,false,true,106738,Costume Institute,Evening wrap,Evening wrap,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1917,1912,1922,silk,,"Gift of Mrs. Robert S. Kilborne, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.5742,false,true,169949,Costume Institute,Muff,Muff,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,first quarter 20th century,1900,1925,"Fur, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted and Constable MacCracken, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1976.269.13a, b",false,true,106227,Costume Institute,Suit,Suit,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,1967–70,1967,1970,"fur, leather",,"Gift of Mrs. Morton Jay Seifter, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1069a, b",false,true,155742,Costume Institute,Coat,Coat,French,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,ca. 1983,1981,1985,"fur, leather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1994",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1972.30.3a, b",false,true,113590,Costume Institute,Shoes,Shoes,French,,,,,Designer,,"F. Pinet, Paris","French, founded 1855",,"F. Pinet, Paris",French,1855,1855,1920,1920,1920,[no medium available],,"Gift of Madame Lilliana Teruzzi, given in memory of her mother, Mrs. Isak Walker Weiman, 1972",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1973.260.5a, b",false,true,83481,Costume Institute,Evening shoes,Evening shoes,French,,,,,Designer,,"F. Pinet, Paris","French, founded 1855",,"F. Pinet, Paris",French,1855,1855,1930s,1930,1939,"silk, leather",,"Purchase, Irene Lewisohn Bequest, 1973",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.47.58.1a, b",false,true,113471,Costume Institute,Shoes,Shoes,French,,,,,Designer,,"F. Pinet, Paris","French, founded 1855",,"F. Pinet, Paris",French,1855,1855,1920s,1920,1929,leather,,"Gift of Mr. James Stewart Cushman, 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.52.22.1a, b",false,true,113181,Costume Institute,Slippers,Slippers,French,,,,,Designer,,"F. Pinet, Paris","French, founded 1855",,"F. Pinet, Paris",French,1855,1855,1910,1910,1910,silk,,"Gift of Miss Susan W. Street, 1952",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.45.36.17a, b",false,true,113469,Costume Institute,Shoes,Shoes,French,,,,,Designer,,"F. Pinet, Paris","French, founded 1855",,"F. Pinet, Paris",French,1855,1855,ca. 1920s,1915,1935,leather,,"Gift of Mrs. George Kent, 1945",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"25.200a, b",false,true,85359,Costume Institute,Visiting dress,Visiting dress,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1872,1867,1877,silk,,"GIft of Mrs. George D. Cross, 1925",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.60.6.7,false,true,107875,Costume Institute,Cloak,Cloak,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1879–80,1879,1880,silk,,"Gift of Chauncey Stillman, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.60.6.8,false,true,107154,Costume Institute,Cape,Cape,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1891–93,1891,1893,silk,,"Gift of Chauncey Stillman, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.60.6.9,false,true,107876,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1877–80,1877,1880,"silk, jet",,"Gift of Chauncey Stillman, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.67,false,true,159226,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1888–90,1888,1890,"silk, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Lillian E. Glenn Peirce and Mabel Glenn Cooper, 1929",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.76,false,true,159326,Costume Institute,Afternoon jacket,Afternoon jacket,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1885–90,1885,1890,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the Princess Viggo in accordance with the wishes of the Misses Hewitt, 1931",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.47.64.2,false,true,107148,Costume Institute,Cape,Cape,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1890s,1890,1899,"silk, jet",,"Gift of Misses Irene and Emily Braman, 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.139,false,true,156099,Costume Institute,Evening jacket,Evening jacket,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1893,1891,1895,"silk, jet, feathers, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Marion Litchfield, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.140,false,true,156111,Costume Institute,Evening cape,Evening cape,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1885–89,1885,1889,"silk, feathers",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Marion Litchfield, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.141,false,true,156122,Costume Institute,Evening cape,Evening cape,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1891,1889,1893,"wool, silk, fur, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Marion Litchfield, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.337,false,true,158299,Costume Institute,Mantle,Mantle,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1891,1889,1893,"wool, silk, metal, feathers",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Cornelia Gracie Henshaw, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.484,false,true,159020,Costume Institute,Evening cloak,Evening cloak,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1885–89,1885,1889,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, In memory of Mrs. John Roebling, 1970",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.485,false,true,159021,Costume Institute,Cape,Cape,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1895,1893,1897,"wool, silk, jet",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the Van Tassell family, 1970",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.39.112.3,false,true,106645,Costume Institute,Coat,Coat,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1860–96,1860,1896,silk,,"Gift of Mrs. Roswell Skeel, Jr., 1939",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.60.42.13,false,true,84561,Costume Institute,Opera cloak,Opera cloak,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1882,1877,1887,"silk, fur, feathers, metal",,"Gift of Mrs. Howard Crosby Brokaw, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6294,false,true,175581,Costume Institute,Evening dolman,Evening dolman,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1885,1883,1887,"Silk, jet beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the Princess Viggo in accordance with the wishes of the Misses Hewitt, 1931",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7431,false,true,174512,Costume Institute,Evening cape,Evening cape,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1890,1890,1890,"Silk, metallic, beads, stones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.295.7a, b",false,true,107064,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1891–93,1891,1893,"silk, cotton",,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.43.66a–c,false,true,107765,Costume Institute,Ensemble,Ensemble,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1866–68,1866,1868,silk,,"Gift of Mrs. Price Collier, 1943",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.60.6.4a, b",false,true,107195,Costume Institute,Dress,Dress,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1891–93,1891,1893,silk,,"Gift of Chauncey Stillman, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.295.3a–c,false,true,107058,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1891–93,1891,1893,"silk, metallic thread, glass",,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.69.33.1a, b",false,true,82430,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1860,1855,1865,silk,,"Gift of Mary Pierrepont Beckwith, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.628a, b",false,true,159180,Costume Institute,Dress,Dress,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1885,1883,1887,"silk, cotton",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Lillian E. Glenn Peirce and Mabel Glenn Cooper, 1929",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.69.33.12a–c,false,true,82642,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1864,1863,1865,silk,,"Gift of Mary Pierrepont Beckwith, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.33,false,true,86022,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1927,1922,1932,silk,,"Purchase, German Fur Industry; Brenner Couture Inc. Leisure Dynamics Foundation, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.167,false,true,106443,Costume Institute,Opera coat,Opera coat,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1907,1902,1912,"silk, metal, feathers",,"Gift of Karen Roston, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.184.6,false,true,106848,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1915,1915,1915,"cotton, silk",,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.150.1,false,true,109777,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1911,1906,1916,"silk, cotton, metallic thread, metal beads",,"Gift of Mrs. W. Allston Flagg, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.133.3,false,true,108933,Costume Institute,Tea gown,Tea gown,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1906–8,1906,1908,"silk, cotton",,"Hoechst Fiber Industries Fund, 1982",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/108933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.169.1,false,true,86032,Costume Institute,Dress,Dress,French,,,,,Designer,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1918,1913,1923,silk,,"Gift of Richard Martin, 2000",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.66.58.4,false,true,81734,Costume Institute,Dress,Dress,French,,,,,Designer,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926,1926,1926,silk,,"Gift of Mrs. Leon L. Roos, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81734,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1982.369.2a, b",false,true,109793,Costume Institute,Promenade suit,Promenade suit,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1911,1906,1916,"silk, cotton",,"Purchase, German Fur Industry Gift, 1982",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109793,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.65.39.1a, b",false,true,84006,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1925,1925,1925,"silk, metallic thread",,"Gift of Mrs. Robin Craven, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.16a–d,false,true,86015,Costume Institute,Loungewear,Loungewear,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926–27,1926,1927,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.17a–c,false,true,86016,Costume Institute,Loungewear,Loungewear,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926–27,1926,1927,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.18a–c,false,true,86017,Costume Institute,Loungewear,Loungewear,French,,,,,Designer,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,1926–27,1926,1927,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.58.30,false,true,119398,Costume Institute,Scarf,Scarf,French,,,,,Designer,Textile by,René-Jules Lalique,"French, Aÿ 1860–1945 Paris",,"Lalique, René-Jules",French,1860,1945,early 20th century,1900,1950,silk,,"Purchase, Irene Lewisohn Bequest, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/119398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.113.7,false,true,121909,Costume Institute,Belt Buckle,Belt buckle,French,,,,,Designer,attributed to,René-Jules Lalique,"French, Aÿ 1860–1945 Paris",,"Lalique, René-Jules",French,1860,1945,ca. 1932,1927,1937,"glass, metal",,"Gift of Judith Leiber, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.3.19,false,true,105413,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1905–8,1905,1908,silk,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.3.20,false,true,95195,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1907–8,1907,1908,silk,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/95195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.242.1,false,true,101303,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1910,1905,1915,"silk, metallic tread, fur",,"Gift of Olivia Constable, 1974",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/101303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.390.8,false,true,81749,Costume Institute,Visiting dress,Visiting dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1903,1898,1908,"wool, cotton, silk, metallic thread",,"Gift of Irma A. Bloomingdale, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.177.2,false,true,79775,Costume Institute,Opera cape,Opera cape,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1890,1885,1895,"silk, fur",,"Purchase, Irene Lewisohn Trust Gift, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/79775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.329.29,false,true,110065,Costume Institute,Scarf,Scarf,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1930s–40s,1930,1949,silk,,"Gift of Col. Edgar W. Garbisch, 1977",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/110065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"49.3.26a, b",false,true,93763,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1897,1897,1897,silk,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/93763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"49.3.27a, b",false,true,84645,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1898–1900,1898,1900,silk,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"49.3.30a, b",false,true,84646,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1898–1900,1898,1900,silk,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"49.3.33a, b",false,true,105047,Costume Institute,Suit,Suit,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1903–6,1903,1906,cotton,,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.13.1,false,true,83429,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1905–7,1905,1907,silk,,"Gift of Miss Marie Louise Constable, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.15a–c,true,true,81138,Costume Institute,Ensemble,Ensemble,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1920–23,1920,1923,"wool, silk, glass",,"Gift of Mrs. W.G. Constable, 1975",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1984.337a, b",false,true,107637,Costume Institute,Dress,Dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1880,1875,1885,"silk, cotton",,"Purchase, Irene Lewisohn and Alice L. Crowley Bequests, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.128,false,true,155977,Costume Institute,Evening cape,Evening cape,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1900–1905,1900,1905,"wool, silk, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Orme and R. Thornton Wilson in memory of Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.431,false,true,158962,Costume Institute,Dress,Dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1910,1908,1912,"silk, cotton",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.471,false,true,159006,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,fall/winter 1902,1902,1902,"wool, fur, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted and Constable MacCracken, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.25.1a–c,false,true,102260,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1904,1899,1909,silk,,"Gift of Marie L. Constable, 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/102260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1154,false,true,155837,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1910,1908,1912,"silk, fur, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Orme and R. Thornton Wilson in memory of Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3196,false,true,158106,Costume Institute,Tea gown,Tea gown,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1907,1905,1909,"silk, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Rodman A. Heeren, 1959",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3258,false,true,158174,Costume Institute,Afternoon jacket,Afternoon jacket,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1905,1903,1907,"silk, cotton, metal, wood",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Roland A. Goodman, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.174.1a–d,false,true,109758,Costume Institute,Wedding Ensemble,Wedding ensemble,French,,,,,Designer,(a),Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1907,1902,1912,"cotton, silk, wax",,"Gift of the Alice Langhorne Washburn Estate, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.303.3a–c,false,true,110064,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1925,1920,1930,"silk, glass, plastic",,"Gift of David Toser, 1977",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/110064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.579a, b",false,true,159124,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1900–1903,1900,1903,"silk, linen, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1990",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.892a, b",false,true,159473,Costume Institute,Visiting dress,Visiting dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1900–1905,1900,1905,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.44.64.41a, b",false,true,106672,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1893–96,1893,1896,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.13.2a–c,false,true,106438,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1906–7,1906,1907,silk,,"Gift of Miss Marie Louise Constable, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1153a, b",false,true,155836,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1903,1901,1905,"cotton, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Orme and R. Thornton Wilson in memory of Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1557a, b",false,true,156285,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1903,1901,1905,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156285,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.2533a, b",false,true,157371,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1898–1900,1898,1900,"silk, metal, plastic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Daniel M. McKeon and Robert Hoguet, Jr., 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157371,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3274a, b",false,true,158193,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1898–1902,1898,1902,"silk, metal, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Daniel M. McKeon and Robert Hoguet, Jr., 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3309a, b",false,true,158232,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1902,1900,1904,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3346a, b",false,true,158273,Costume Institute,Afternoon suit,Afternoon suit,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,fall/winter 1904,1904,1904,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted and Constable MacCracken, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7070a, b",false,true,174206,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1905–10,1905,1910,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Alfred Roberts, 1959",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174206,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7409a, b",false,true,174491,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1900–1905,1900,1905,"Silk, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Daniel M. McKeon and Robert Hoguet, Jr., 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7430a, b",false,true,174511,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1905,1903,1907,"Silk, sequins, beads, metallic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174511,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.891a–d,false,true,159472,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,ca. 1902,1900,1904,"silk, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert G. Olmsted, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.56a–c,false,true,85067,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",", Paris","Doucet, Jacques",French,1853,1929,1897–1900,1897,1900,"silk, beads",,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3275a–c,false,true,158194,Costume Institute,Ball gown,Ball gown,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1898–1900,1898,1900,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Daniel M. McKeon and Robert Hoguet, Jr., 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158194,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.148,false,true,83467,Costume Institute,Hat,Hat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1928,1928,1928,[no medium available],,"Gift of Mrs. Francis Henry Taylor, 1942",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83467,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.86,false,true,81562,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1913–19,1913,1919,"silk, metallic thread",,"Purchase, the Kyoto Institute: Koichi Tsukamoto, President Fund, 1980",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81562,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.277,false,true,105657,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1922,1917,1927,"silk, metallic thread, plastic, glass",,"Purchase, Friends of The Costume Institute Gifts, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.428,false,true,84616,Costume Institute,Dress,Dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1922,1922,1922,"silk, metallic thread",,"Gift of Leone B. Moats, in memory of Mrs. Wallace Payne Moats, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.249,false,true,97118,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1925,1925,1925,silk,,"Gift of Mrs. Ivor Bevan, 1982",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/97118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.188,false,true,121167,Costume Institute,Dress,"""Rosière""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1911,1911,1911,linen,,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.191,false,true,121170,Costume Institute,Headdress,Flonflon,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1920,1915,1925,"silk, metallic thread, feathers",,"Purchase, Judith and Gerson Leiber Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.195,false,true,121174,Costume Institute,Tunic,Tunic,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1920,1915,1925,cotton,,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.196,false,true,121175,Costume Institute,Tunic,Tunic,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1920,1915,1925,cotton,,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.200,false,true,121192,Costume Institute,Coat,"""Manteau D'Auto""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912,1912,1912,"linen, silk, cellulose",,"Isabel Shults Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.201,false,true,121193,Costume Institute,Coat,Coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1918,1918,1918,"wool, rayon",,"Isabel Shults Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.203,false,true,121195,Costume Institute,Jacket,Jacket,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912,1912,1912,"wool, cotton, rayon",,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.205,false,true,121197,Costume Institute,Coat,"""Pré Catelan""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1918,1918,1918,"silk, metallic thread",,"Millia Davenport and Zipporah Fleisher Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121197,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.206,false,true,121198,Costume Institute,Headdress,Headdress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1920,1915,1925,"cotton, metal, ceramic",,"Gerson and Judith Leiber Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.207,false,true,121199,Costume Institute,Coat,"""Paris""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1919,1919,1919,"silk, wool, metallic thread",,"Purchase, Friends of The Costume Institute Gifts, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.208,false,true,121200,Costume Institute,Headdress,Headdress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1910,1905,1915,metal,,"Gerson and Judith Leiber Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.209,false,true,121201,Costume Institute,Jacket,"""Steppe""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912,1912,1912,"silk, wool, fur, cotton",,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.210,false,true,121202,Costume Institute,Dress,Mademoiselle,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1923,1923,1923,wool,,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.211,false,true,121203,Costume Institute,Hat,"""Bahia""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1914,1914,1914,"silk, metallic thread, jet, glass, silver, cotton",,"Gerson and Judith Leiber Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.386,false,true,123612,Costume Institute,Dress,Dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912,1912,1912,silk,,"Purchase, Friends of The Costume Institute Gifts, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/123612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.513,false,true,129937,Costume Institute,Dress,Dress,French,,,,,Designer,Attributed to,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1920s,1920,1929,silk,,"Purchase, Friends of The Costume Institute Gifts, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/129937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.146,false,true,136302,Costume Institute,Dress,"""Irudree""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1922,1922,1922,"metallic, silk",,"Purchase, Friends of The Costume Institute Gifts, 2007",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/136302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.288,false,true,141903,Costume Institute,Opera coat,Opera coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1911,1911,1911,silk,,"Alfred Z. Solomon-Janet A. Sloane Endowment Fund, 2008",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/141903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.47,false,true,81560,Costume Institute,Dress,Dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1925,1920,1930,"silk, leather",,"Gift of Mrs. Kenneth Maconochie, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.279.1,false,true,81684,Costume Institute,Shawl,Shawl,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1920s,1920,1929,"silk, metal",,"Gift of Henry F. Callahan, 1973",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.367.2,false,true,97117,Costume Institute,Dance dress,Dance dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1926,1921,1931,"silk, cotton, plastic, glass",,"Gift of Mary Van Rensselaer Thayer, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/97117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.281.1,false,true,113915,Costume Institute,Picture hat,Picture hat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1921,1916,1926,"horse hair, cotton",,"Purchase, Gifts from Various Donors Fund, 1982",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.350.2,false,true,81563,Costume Institute,Opera coat,Opera coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912,1912,1912,"silk, metal",,"Purchase, Irene Lewisohn Bequest, 1982",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.115.2,false,true,105663,Costume Institute,Wedding Dress,Wedding dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1925,1925,1925,"silk, metal",,"Given by the children of Mrs. Kenneth F. Simpson in memory of her parents, Mr. & Mrs. Nathan T. Porter, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1983.8a, b",true,true,81781,Costume Institute,Fancy dress costume,Fancy dress costume,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1911,1911,1911,"metal, silk, cotton",,"Purchase, Irene Lewisohn Trust Gift, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.516.7,false,true,109411,Costume Institute,Teddy,Teddy,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1920s,1920,1929,silk,,"Gift of Miriam W. Coletti, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.226.2,false,true,97119,Costume Institute,Coat,Coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1925,1920,1930,"silk, fur",,"Gift of Mrs. John Campbell White, 1988",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/97119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.165.2,false,true,105666,Costume Institute,Dress,Dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1928,1928,1928,"silk, glass",,"Gift of Mary C. Hartshorne, 1989",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.50.117,false,true,82558,Costume Institute,Dress,Dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1925,1925,1925,"wool, silk",,"Gift of Mrs. Alfred Rheinstein, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.64.7.2,false,true,84568,Costume Institute,Coat,Coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1923,1923,1923,wool,,"Gift of Mrs. David J. Colton, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.516.16,false,true,116106,Costume Institute,Scarf,Scarf,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1920s,1920,1929,silk,,"Gift of Miriam W. Coletti, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/116106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2007.58a, b",false,true,131994,Costume Institute,Boots,"""Favereau""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1920,1920,1920,"leather, wood",,"Alfred Z. Solomon-Janet A. Sloane Endowment Fund, 2007",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/131994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.39,false,true,158888,Costume Institute,Evening cape,Evening cape,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1920,1918,1922,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Brooklyn Museum Collection",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.43.85.1,false,true,97120,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1923,1923,1923,silk,,"Gift of Mrs. Muriel Draper, 1943",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/97120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.48.3,false,true,105655,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1924,1924,1924,"silk, metallic",,"Gift of Mrs. Robert L. Dodge, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.52.1,false,true,105667,Costume Institute,Fancy dress costume,Fancy dress costume,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1900–1944,1900,1944,"silk, metallic, simulated pearls",,"Gift of Mrs. Mary S. Thomas, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.58.34.8,false,true,105665,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1927,1927,1927,"silk, metal, plastic",,"Gift of Mrs. John Chambers Hughes, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.61.40.4,true,true,81123,Costume Institute,Coat,Coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1919,1919,1919,"silk, wool, fur, leather",,"Gift of Mrs. David J. Colton, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.190a, b",false,true,121169,Costume Institute,Dress,Butard,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912,1912,1912,"a, b) linen",,"Millia Davenport and Zipporah Fleisher Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2005.367a, b",false,true,123613,Costume Institute,Ensemble,Ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912,1912,1912,"a) silk, fur; b) silk",,"Purchase, Friends of The Costume Institute Gifts, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/123613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.316,false,true,158066,Costume Institute,Evening cape,Evening cape,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1920,1918,1922,"wool, metal, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Ogden Goelet, Peter Goelet, and Madison Clews, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.549,false,true,159091,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,spring/summer 1917,1917,1917,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.560,false,true,159105,Costume Institute,Jacket,Jacket,French,,,,,Designer,Attributed to,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1924,1924,1924,"wool, cotton",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mary Sefton Thomas, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.193a–g,false,true,121172,Costume Institute,Ensemble,"""Théâtre des Champs-Élysées""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1913,1913,1913,"a,c-e) silk, rhinestones; b) silk; f, g) silk, leather",,"Purchase, The Paul D. Schurgot Foundation Inc. Gift, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.194a–c,false,true,121173,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1920,1915,1925,"a, b) silk; c) silk, enamel",,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.204a–c,false,true,121196,Costume Institute,Ensemble,"""Feuille d'automne""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1916,1916,1916,"a, b) silk; c) fur, silk",,"Catharine Breyer Van Bomel Foundation Fund, 2005",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/121196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1289,false,true,155987,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1910,1910,1910,"silk, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Ogden Goelet, Peter Goelet and Madison Clews in memory of Mrs. Henry Clews, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1339,false,true,156043,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,Attributed to,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1925,1923,1927,synthetic,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. John Chapman, 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1368,false,true,156074,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1912,1910,1914,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Purchased with funds given by Mrs. Carl L. Selden, 1985",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2111,false,true,156902,Costume Institute,Cloche,Cloche,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1923,1921,1925,"wool, leather, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2503,false,true,157337,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1912–14,1912,1914,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Andrew J. Love, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3360,false,true,158288,Costume Institute,Evening dress,"""Robe Sabat""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1921,1921,1921,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Augustus Graham School of Design Fund, 1973",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.8212,false,true,175230,Costume Institute,Evening coat,Evening coat,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1924,1924,1924,"Silk, synthetic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mary Sefton Thomas, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.44.82a, b",false,true,105658,Costume Institute,Dress,Dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1922–24,1922,1924,silk,,"Gift of Mrs. Dudley Wadsworth, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.367.3a, b",false,true,105660,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1924,1924,1924,"cotton, fur, metallic thread, silk",,"Gift of Mary Van Rensselaer Thayer, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.367.4a, b",false,true,105659,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1923,1923,1923,"silk, metallic thread",,"Gift of Mary Van Rensselaer Thayer, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1988.226.1a, b",false,true,105661,Costume Institute,Ensemble,Ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1925,1920,1930,"silk, felt, metallic thread",,"Gift of Mrs. John Campbell White, 1988",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1995.588.3a, b",false,true,80338,Costume Institute,Ensemble,Ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1922,1922,1922,silk,,"Gift of Miriam K. W. Coletti, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.569.6a–c,false,true,105664,Costume Institute,Ensemble,Ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1925–26,1925,1926,"wool, metal, leather",,"Gift of Mrs. C. O. Kalman, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.115.3a–k,false,true,105662,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1925,1920,1930,"silk, metal",,"Gift of the children of Mrs. Kenneth F. Simpson, in her memory, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.43.85.2a, b",false,true,82549,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1922–23,1922,1923,silk,,"Gift of Mrs. Muriel Draper, 1943",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.50.84.2a, b",false,true,97388,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,fall/winter 1928–29,1928,1929,silk,,"Gift of Miss Frances McFadden, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/97388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.51.48.1a, b",false,true,105669,Costume Institute,Fancy dress costume,Fancy dress costume,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,20th century,1900,1944,"silk, metallic, synthetic gems",,"Gift of Mrs. Robert L. Dodge, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.51.48.4a, b",false,true,85419,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1925,1920,1930,"metallic, simulated pearls",,"Gift of Mrs. Robert L. Dodge, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/85419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.51.48.5a, b",false,true,105656,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1920s,1920,1929,"sequins, silk, metallic, beads",,"Gift of Mrs. Robert L. Dodge, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.58.34.7a–c,false,true,97121,Costume Institute,Afternoon ensemble,Afternoon ensemble,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1927,1927,1927,silk,,"Gift of Mrs. John Chambers Hughes, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/97121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1304a, b",false,true,156005,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1930,1928,1932,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Anthony Wilson, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7190a, b",false,true,174318,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1928,1926,1930,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Peter W. Lyon, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7309a, b",false,true,175768,Costume Institute,Evening dress,Evening dress,French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1932,1930,1934,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Anthony Wilson, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.51.70.19a–c,false,true,81676,Costume Institute,Evening dress,"""Arrow of Gold""",French,,,,,Designer,,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,1925,1925,1925,"silk, metallic thread",,"Gift of Mrs. Robert A. Lovett, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1094a–g,false,true,155770,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer,,Charles Frederick Worth,"French (born England), Bourne 1825–1895 Paris",,"Worth, Charles Frederick",French,1825,1895,1887,1887,1887,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Edith Gardiner, 1926",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.487.1,false,true,159717,Costume Institute,Dolman,Dolman,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,1880s,1880,1889,"silk, linen",,"Gift of Christopher Scholz and Ines Elskop, 2010",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.37.59.1a, b",false,true,107660,Costume Institute,Dress,Dress,French,,,,,Designer,,Jacques Doucet,"French, Paris 1853–1929 Paris",,"Doucet, Jacques",French,1853,1929,1880s,1880,1889,silk,,"Gift of Mrs. M. Villone, 1937",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7758a, b",false,true,174797,Costume Institute,Promenade dress,Promenade dress,French,,,,,Designer,,Emile Pingat,"French, active 1860–96",,Pingat Emile,French,1860,1896,ca. 1888,1886,1890,"Silk, metallic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift in memory of Mrs. John Roebling, 1970",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174797,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.69,false,true,84504,Costume Institute,Evening dress,Evening dress,British,,,,,Design House,,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1930,1925,1935,"silk, metallic thread",,"Gift of Reneé C. Rinaldi and Michelle R. Rinaldi, 1977",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.48.38.2,false,true,106772,Costume Institute,Cloak,Cloak,American,,,,,Designer,,Revillon Frères,"French, founded 1723",,Revillon Frères,French,1723,1850,1919,1919,1919,silk,,"Gift of Mrs. Carlo Vicario, 1948",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1979.346.121a, b",false,true,86013,Costume Institute,Dress,Dress,American,,,,,Designer,Attributed to,Callot Soeurs,"French, active 1895–1937",,Callot Soeurs,French,1895,1937,ca. 1924,1919,1929,silk,,"Gift of The New York Historical Society, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.62.36.1a–d,false,true,94892,Costume Institute,Evening ensemble,Evening ensemble,European,,,,,Designer,,Jeanne Hallée,"French, 1880–1914",,Hallée Jeanne,French,1880,1914,1897–98,1897,1898,"(a–d) silk; (c, d) leather",,"Gift of Mrs. Ogden W. Ross, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.303.1,false,true,127572,Costume Institute,Parasol,Parasol,Japanese,,,,,Designer,Attributed to,Paul Poiret,"French, Paris 1879–1944 Paris",,"Poiret, Paul",French,1879,1944,ca. 1910,1905,1915,[no medium available],,"Gift of Elizabeth A. Tilson, 1975",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/127572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.62.55.1,false,true,86190,Costume Institute,Dress,Dress,Indian,,,,,Department Store,,Liberty & Co.,"British, founded London, 1875",,Liberty & Co.,British,1875,2050,late 18th–early 19th century,1775,1825,[no medium available],,"Gift of Mr. and Mrs. Clarence Stein, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.99.4a–c,false,true,106302,Costume Institute,Evening dress,Evening dress,probably British,,,,,Designer,Attributed to,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,ca. 1917,1912,1922,silk,,"Gift of Miss Isabel Shults and Mrs. Bertha Shults Dougherty, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.8103,false,true,175128,Costume Institute,Dress,Dress,French,,,,,Maker,,House of Lucile,"British, founded 1895",,Lucile House of,British,1895,1895,ca. 1925,1923,1927,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2001.576a, b",false,true,83042,Costume Institute,Shoes,Shoes,British,,,,,Manufacturer,,E. Pattison,"British, 1800–1850",,E. Pattison,British,1800,1850,1806–15,1806,1815,"(a, b) leather, silk",,"Purchase, Gerson and Judith Leiber Foundation Gift, 2001",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.68.53.9,false,true,81527,Costume Institute,Dress,Dress,British,,,,,Design House,,Liberty & Co.,"British, founded London, 1875",,Liberty & Co.,British,1875,2050,1891,1891,1891,silk,,"Gift of Mrs. James G. Flockhart, 1968",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.155,false,true,81513,Costume Institute,Evening dress,Evening dress,British,,,,,Design House,Attributed to,Liberty & Co.,"British, founded London, 1875",,Liberty & Co.,British,1875,2050,1880s,1880,1889,silk,,"Purchase, Gifts from Various Donors, 1985",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.294,false,true,80875,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1910s,1910,1919,metallic thread,,"Purchase, New School for Social Research Fund, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.56.48,false,true,106869,Costume Institute,Afternoon dress,Afternoon dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1913,1913,1913,"silk, metal",,"Gift of Mrs. E. Theophilus MacDermott, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.68.75,false,true,94636,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1915–16,1915,1916,[no medium available],,"Gift of Miss Barbara Jane Pentlarge, 1968",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.217.1,false,true,94669,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,ca. 1916,1911,1921,"silk, cotton",,"Gift of the Staten Island Institute of Arts & Sciences, pursuant to the instructions of Mr. and Mrs. Elisha Dyer, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.217.3,false,true,94637,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,ca. 1918,1913,1923,"silk, cotton, metal",,"Gift of the Staten Island Institute of Arts & Sciences, pursuant to the instructions of Mr. and Mrs. Elisha Dyer, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.217.6,false,true,94670,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1922,1922,1922,"silk, cotton",,"Gift of the Staten Island Institute of Arts & Sciences, pursuant to the instructions of Mr. and Mrs. Elisha Dyer, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.369.2,false,true,94672,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,ca. 1913,1908,1918,"silk, cotton, glass, plastic",,"Gift of Mrs. Kingsley Mabon, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.569.2,false,true,94663,Costume Institute,Afternoon dress,Afternoon dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1917,1917,1917,"silk, cotton",,"Gift of Mrs. C. O. Kalman, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.569.4,false,true,94664,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1919,1919,1919,"silk, cotton",,"Gift of Mrs. C. O. Kalman, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.126.32,false,true,107456,Costume Institute,Hat,Hat,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1914–20,1914,1920,"silk, glass, metal, cotton",,"Gift of Mrs. Alan L. Corey Jr., Mrs. William T. Newbold, and Mrs. A.G. Paine, II, 1980",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.47.57.1,false,true,94665,Costume Institute,Dance dress,Dance dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1939,1939,1939,"silk, fur, metallic thread",,"Gift of Irene Castle (Mrs. George Enzinger), 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.47.57.2,false,true,102428,Costume Institute,Muff,Muff,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1914,1914,1914,fur,,"Gift of Irene Castle (Mrs. George Enzinger), 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/102428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.47.57.4,false,true,108239,Costume Institute,Hat,Hat,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1914–19,1914,1919,"silk, cotton",,"Gift of Irene Castle (Mrs. George Enzinger), 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/108239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.47.57.5,false,true,94635,Costume Institute,Dance dress,Dance dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,ca. 1921,1916,1926,"silk, artificial pearls, horsehair",,"Gift of Irene Castle (Mrs. George Enzinger), 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.47.57.6,false,true,106877,Costume Institute,Dance dress,Dance dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1914–29,1914,1929,"silk, metal thread, glass, horsehair, cotton",,"Gift of Irene Castle (Mrs. George Enzinger), 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/106877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.112a, b",false,true,95130,Costume Institute,Wedding Dress,Wedding dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1915,1915,1915,"silk, cotton, plastic, metal",,"Gift of Charles V. Hickox, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/95130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.33,false,true,94667,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,ca. 1915,1910,1920,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.36,false,true,94668,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1914–16,1914,1916,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.38,false,true,108238,Costume Institute,Afternoon dress,Afternoon dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,ca. 1914,1909,1919,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/108238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.39,false,true,107905,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1916–18,1916,1918,[no medium available],,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.43,false,true,94662,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1914,1914,1914,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.39.41a, b",false,true,94666,Costume Institute,Dress,Dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1900–1935,1900,1935,silk,,"Gift of Mrs. Edna Woolman Newton, 1939",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.288.1a, b",false,true,82580,Costume Institute,Dress,Dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1916–17,1916,1917,"silk, cotton",,"Gift of Julia B. Henry, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1979.569.3a, b",false,true,94638,Costume Institute,Dinner dress,Dinner dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1918,1918,1918,"silk, cotton, metal",,"Gift of Mrs. C. O. Kalman, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.47.57.3a, b",false,true,107906,Costume Institute,Dance dress,Dance dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1914,1914,1914,"silk, cotton",,"Gift of Irene Castle (Mrs. George Enzinger), 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3307a, b",false,true,158230,Costume Institute,Suit,Suit,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1910–12,1910,1912,"wool, silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.44.64.37a–c,false,true,94661,Costume Institute,Evening dress,Evening dress,British,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1916–18,1916,1918,silk,,"Gift of Miss Isabel Shults, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.582.98,false,true,113127,Costume Institute,Boater,Boater,British,,,,,Designer,,James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,1950s,1950,1959,straw,,"Gift of Marvin B. Patterson (Mrs. Jefferson Patterson), 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.291.11,false,true,112803,Costume Institute,Fedora,Fedora,British,,,,,Designer,,James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,1935–49,1935,1949,wool (probably),,"Gift of Jane de Rochemont, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.291.14,false,true,112805,Costume Institute,Derby,Derby,British,,,,,Designer,,James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,1930–49,1930,1949,wool (probably),,"Gift of Jane de Rochemont, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.271.41,false,true,129921,Costume Institute,Hat,Hat,British,,,,,Designer,,James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,1970s,1970,1979,"wool, silk",,"Bequest of Yolande Fielding–Scheftel, 2006",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/129921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2214,false,true,157015,Costume Institute,Riding Hat,Riding hat,British,,,,,Designer,,James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,ca. 1930,1928,1932,"fur, wool, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. William Randolph Hearst, Jr., 1985",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.472.2a–j,false,true,84675,Costume Institute,Riding Ensemble,Riding ensemble,British,,,,,Designer,(d) Hat by,James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,ca. 1936,1931,1941,a) cotton b) silk c) Linen d) silk e–h) leather i) silk j) leather,,"Gift of Mrs. C. Suydam Cutting, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.594.1a–j,false,true,98589,Costume Institute,Ensemble,Ensemble,British,,,,,Designer,"(g, h) Hat by",James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,ca. 1950,1945,1955,"a) wool, silk; b-d) wool; e) cotton; f-h) wool; i,j) leather",,"Gift of Mrs. Doreen Simmons, 2003",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/98589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1981.301a–i,l–q",false,true,108600,Costume Institute,Hunting ensemble,Hunting ensemble,British,,,,,Designer,(l),James Lock & Co. Ltd,"British, founded 1676",,Lock & Co. Ltd James,British,1676,1676,ca. 1930,1925,1935,"wool, cotton, leather",,"Gift of Thomas A. Bradley, Jr., 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/108600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3384,false,true,158314,Costume Institute,Tea gown,Tea gown,British,,,,,Designer,,Liberty & Co.,"British, founded London, 1875",,Liberty & Co.,British,1875,2050,ca. 1885,1883,1887,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2104,false,true,156894,Costume Institute,Hat,Hat,British,,,,,Designer,,William Charles Brown,"British, active late 19th century",,"Brown, William Charles",British,1800,1899,ca. 1870,1868,1872,"straw, silk, feathers",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.46.46.6a, b",false,true,86742,Costume Institute,Dress,Dress,American,,,,,Designer,,Lucile,"British, 1863–1935",,Lucile,British,1863,1935,1910–12,1910,1912,silk,,"Gift of Mrs. Harrison Williams, Lady Mendl, and Mrs. Ector Munn, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/86742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4707a, b",false,true,168912,Costume Institute,Evening shoes,Evening shoes,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"Silk, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/168912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5679a, b",false,true,169894,Costume Institute,Shoes,Shoes,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"Leather, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5681a, b",false,true,169895,Costume Institute,Shoes,Shoes,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"Leather, silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5683a, b",false,true,169896,Costume Institute,Shoes,Shoes,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,Leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5686a, b",false,true,169898,Costume Institute,Shoes,Shoes,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,Leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.4708a–f,false,true,168913,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"Wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/168913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.5680a–f,false,true,170470,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"Wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.5682a–f,false,true,170471,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"Wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.5684a–d,false,true,169897,Costume Institute,Shoes,Shoes,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,Leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.5685a–f,false,true,170472,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"Wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.5687a–f,false,true,170473,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"Wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6176a–f,false,true,170492,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"Wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170492,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6177a–f,false,true,170493,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Maker,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"Wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170493,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.25,false,true,172080,Costume Institute,Trunk,Trunk,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"wood, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/172080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.26,false,true,175904,Costume Institute,Trunk,Trunk,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"wood, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.378a–j,false,true,113560,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1910–20,1910,1920,leather,,"Gift of Mrs. John E. Roosevelt, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.49.2.9a, b",false,true,113555,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1913–14,1913,1914,"silk, leather",,"Gift of Howard Sturges, in memory of his mother, Mrs. Howard O. Sturges, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1978.583.31a, b",false,true,84615,Costume Institute,Evening shoes,Evening shoes,French,,,,,Designer,Attributed to,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1920–25,1920,1925,"silk, leather",,"Gift of Mrs. John Scholz (Helen Marshall), 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.49.2.7a–f,false,true,113564,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,early 20th century,1900,1950,wood,,"Gift of Howard Sturges, in memory of his mother, Mrs. Howard O. Sturges, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113564,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.583.27a–h,false,true,113561,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,late 1920s,1925,1929,leather,,"Gift of Mrs. John Scholz (Helen Marshall), 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113561,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.583.28a–h,false,true,113557,Costume Institute,Evening shoes,Evening shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1920–25,1920,1925,"silk, metallic thread, leather, wood",,"Gift of Mrs. John Scholz (Helen Marshall), 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.583.29a–h,false,true,113558,Costume Institute,Evening shoes,Evening shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1920–25,1920,1925,"silk, metallic thread, leather",,"Gift of Mrs. John Scholz (Helen Marshall), 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.583.30a–h,false,true,113559,Costume Institute,Evening shoes,Evening shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1920–25,1920,1925,"silk, metallic thread, leather, wood",,"Gift of Mrs. John Scholz (Helen Marshall), 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.1a–h,false,true,112919,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.2a–h,false,true,112920,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.3a–g,false,true,112921,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.4a–h,false,true,112922,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.5a–f,false,true,112923,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.6a–h,false,true,112924,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.7a–h,false,true,112925,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.8a–h,false,true,112926,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.9a–h,false,true,112927,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1910–19,1910,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.69.12.1a–h,false,true,113563,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1913–17,1913,1917,"(a, b) silk, metallic thread; (c–h) wood",,"Gift of Elizabeth Hudson, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113563,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.69.12.2a–h,false,true,105033,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1913–18,1913,1918,"(a, b) silk; (c–h) wood",,"Gift of Elizabeth Hudson, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1178a, b",false,true,155863,Costume Institute,Evening shoes,Evening shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"silk, metal, jet",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1459a, b",false,true,156176,Costume Institute,Mules,Mules,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1592a, b",false,true,156323,Costume Institute,Evening pumps,Evening pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"metal, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1593a, b",false,true,156324,Costume Institute,Evening pumps,Evening pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"silk, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1853a, b",false,true,156614,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.10a–h,false,true,112928,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.11a–h,false,true,112929,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.12a–h,false,true,112930,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.13a–h,false,true,112931,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.14a–f,false,true,112932,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.15a–h,false,true,112933,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.16a–e,false,true,112934,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.17a–h,false,true,112935,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.18a–h,false,true,112936,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.19a–h,false,true,112937,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.20a–g,false,true,104759,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/104759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.21a–h,false,true,112938,Costume Institute,Pumps,Pumps,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk, metallic",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.22a–h,false,true,112939,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.23a–h,false,true,112940,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112940,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.76.24a–g,false,true,112941,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"leather, silk",,"Gift of Capezio Inc., 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/112941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1179a–f,false,true,155864,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1457a–d,false,true,156174,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1458a–f,false,true,156175,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1854a–f,false,true,156615,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1914–19,1914,1919,"wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mercedes de Acosta, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2144a–d,false,true,156937,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2145a–f,false,true,156938,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,wood,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3408a–f,false,true,158343,Costume Institute,Shoe Trees,Shoe trees,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3866a–h,false,true,158850,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1925–30,1925,1930,"leather, wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Edward G. Sparrow, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.40.130.12a–h,false,true,104725,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1911–15,1911,1915,"silk, wood",,"Gift of Miss Mercedes de Acosta, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/104725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.40.130.13a–h,false,true,110469,Costume Institute,Shoes,Shoes,French,,,,,Designer,,Pierre Yantorny,"Italian, 1874–1936",,"Yantorny, Pierre",Italian,1874,1936,1910–15,1910,1915,"leather, wood",,"Gift of Miss Mercedes de Acosta, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/110469,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.46,false,true,81652,Costume Institute,Evening coat,Evening coat,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1925–26,1925,1926,"silk, fur",,"Gift of Helen M. Woodruff, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.96,false,true,81625,Costume Institute,Evening cape,Evening cape,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,early 1920s,1920,1925,"silk, glass, metallic thread",,"Gift of Leafie Freda, 1980",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.170,false,true,81651,Costume Institute,Evening cape,Evening cape,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1923–24,1923,1924,silk,,"Gift of Mrs. A. Winslow Meade, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.160,false,true,81593,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,second quarter 20th century,1925,1950,silk,,"Gift of Mr. and Mrs. James Pinckney Kinard, in memory of her mother, the late Agnes J. Dodds, (Mrs. Robert James Dodds), 1982",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81593,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.191,false,true,141972,Costume Institute,Dress,Dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,early 20th century,1900,1950,silk,,"Gift of Anne Parmelee Reed Dean, In Memory of Elizabeth Burd Thompson Reed, 2008",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/141972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.32.1,false,true,81653,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1924,1924,1924,silk,,"Gift of Mrs. Douglas Delanoy, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.32.2,false,true,81654,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1924,1924,1924,silk,,"Gift of Mrs. Douglas Delanoy, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.28.5,false,true,80161,Costume Institute,Dress,Dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1920s,1920,1929,silk,,"Gift of Estate of Lillian Gish, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.63.14,false,true,81508,Costume Institute,Tea gown,Tea gown,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,ca. 1930,1925,1935,silk,,"Gift of Louise Rorimer Dushkin, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.165.1,false,true,81650,Costume Institute,Evening coat,Evening coat,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,ca. 1926,1921,1931,silk,,"Gift of Mary C. Hartshorne, 1989",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.1,false,true,81641,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,first half 20th century,1900,1950,"silk, glass",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.2,false,true,81642,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1920s,1920,1929,"silk, glass",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.3,false,true,81643,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1920s,1920,1929,"silk, glass",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.4,false,true,81522,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,early 20th century,1900,1950,"silk, glass",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meriere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81522,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.7,false,true,81644,Costume Institute,Evening coat,Evening coat,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1920s,1920,1929,silk,,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.8,false,true,81645,Costume Institute,Scarf,Scarf,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,first quarter 20th century,1900,1925,silk,,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.9,false,true,81646,Costume Institute,Scarf,Scarf,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,first quarter 20th century,1900,1925,"silk, glass",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.569.15,false,true,81597,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,Attributed to,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1925–30,1925,1930,"silk, glass",,"Gift of Mrs. C. O. Kalman, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.10,false,true,81647,Costume Institute,Purse,Purse,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,first quarter 20th century,1900,1925,silk,,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.11,false,true,81648,Costume Institute,Belt,Belt,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,first quarter 20th century,1900,1925,"metal, glass",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.12,false,true,81649,Costume Institute,Belt,Belt,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,first quarter 20th century,1900,1925,silk,,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.342,false,true,158356,Costume Institute,Evening cape,Evening cape,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1925,1925,1925,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Cecil Lubell, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1995.28.3a, b",false,true,80159,Costume Institute,Dress,Dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1920s,1920,1929,silk,,"Gift of Estate of Lillian Gish, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6903,false,true,173923,Costume Institute,Evening overdress,Evening overdress,Italian,,,,,Designer,Attributed to,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,ca. 1920,1918,1922,"Silk, metallic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Thomas Brown Rudd, 1955",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1991.187.6a, b",false,true,81523,Costume Institute,Evening ensemble,Evening ensemble,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,early 20th century,1900,1925,"silk, glass",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.187.5a–c,false,true,81590,Costume Institute,Evening ensemble,Evening ensemble,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1920s,1920,1929,"silk, glass, fur",,"Gift of Mrs. Paxton T. Dunn in memory of her mother, Hildreth Meiere, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.23a–c,false,true,157221,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,1926,1926,1926,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mary Cheney Platt, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.312a, b",false,true,158032,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,ca. 1920,1918,1922,"silk, metal, glass beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Natalie Rector, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.479a, b",false,true,159014,Costume Institute,Evening dress,Evening dress,Italian,,,,,Designer,,Maria Gallenga,"Italian, Rome 1880–1944 Umbria",,Gallenga Maria,Italian,1880,1944,ca. 1920,1918,1922,"silk, metal, glass",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Philip J. Roosevelt, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.43.113.1,false,true,109103,Costume Institute,Nightgown,Nightgown,American or European,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,1894,1894,1894,[no medium available],,"Gift of Mrs. William Rosenfeld, 1943",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.43.113.8,false,true,109109,Costume Institute,Drawers,Drawers,American or European,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,1894,1894,1894,[no medium available],,"Gift of Mrs. William Rosenfeld, 1943",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.43.113.11,false,true,109104,Costume Institute,Corset Cover,Corset cover,American or European,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,1894,1894,1894,[no medium available],,"Gift of Mrs. William Rosenfeld, 1943",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.39.24a–c,false,true,109013,Costume Institute,Lingerie,Lingerie,American or European,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,1881,1881,1881,cotton,,"Gift of Miss Gertrude M. Oppenheimer, 1939",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.3.45,false,true,105424,Costume Institute,Slippers,Slippers,French,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,1850,1850,1850,"silk, leather, pearls",,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.3.46,false,true,105428,Costume Institute,Slippers,Slippers,French,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,1850,1850,1850,"silk, leather, pearls",,"Gift of Orme Wilson and R. Thornton Wilson, in memory of their mother, Mrs. Caroline Schermerhorn Astor Wilson, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.42.131.3a, b",false,true,103558,Costume Institute,Dress,Dress,French,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,1883,1883,1883,[no medium available],,"Gift of Mrs. R. E. Seeligman, 1942",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/103558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.246.9a–d,false,true,105006,Costume Institute,Ensemble,Ensemble,American,,,,,Manufacturer,"(c, d)",Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1895–98,1895,1898,"wool, leather, canvas",,"Gift of Anne L. Maxwell, in memory of her mother, Julia H. Lawrence, 1989",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/105006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.48.39.7a, b",false,true,113195,Costume Institute,Pumps,Pumps,American,,,,,Manufacturer,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1890s,1890,1899,"silk, leather, glass",,"Gift of Mrs. E. R. Gerkin, 1948",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.2039a, b",false,true,156821,Costume Institute,Stockings,Stockings,American,,,,,Manufacturer,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1900–1915,1900,1915,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Albert Ogden in memory of Sheldon Stewart, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4762a, b",false,true,168967,Costume Institute,Carriage boots,Carriage boots,American,,,,,Department Store,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1895–1915,1895,1915,"Silk, fur",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Charles D. Cords, 1954",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/168967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4945a, b",false,true,169168,Costume Institute,Evening shoes,Evening shoes,American,,,,,Department Store,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1915–20,1915,1920,"Leather, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Adeline Delbon, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5256a, b",false,true,169470,Costume Institute,Slippers,Slippers,American,,,,,Department Store,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,ca. 1891,1889,1893,Leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Ferris J. Meigs, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5597a, b",false,true,169810,Costume Institute,Evening shoes,Evening shoes,American,,,,,Department Store,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,ca. 1925,1923,1927,Leather; metallic,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5696a, b",false,true,169907,Costume Institute,Slippers,Slippers,American,,,,,Department Store,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1893–99,1893,1899,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. William T. Rose, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5954a, b",false,true,170345,Costume Institute,Oxfords,Oxfords,American,,,,,Department Store,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,ca. 1898,1896,1900,"Cotton, leather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Vera Maxwell, 1985",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6071a, b",false,true,170286,Costume Institute,Evening pumps,Evening pumps,American,,,,,Department Store,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1907,1907,1907,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Ann Ellis, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/170286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2250,false,true,157055,Costume Institute,Cage crinoline,Cage crinoline,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,1862–63,1862,1863,"linen, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Brooklyn Museum Collection",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157055,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6642,false,true,173656,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1880,1878,1882,"Cotton, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6643,false,true,173657,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1885,1883,1887,"Cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E.A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3110a, b",false,true,158012,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Worcester Corset Company,"American, 1864–1950",,Worcester Corset Company,American,1864,1950,ca. 1880,1878,1882,"cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6633a, b",false,true,175650,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1862,1860,1864,"Cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6635a, b",false,true,175652,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1869,1867,1871,"Cotton, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6636a, b",false,true,175653,Costume Institute,Corset,Corset,American,,,,,Manufacturer,Probably,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1872,1870,1874,"Cotton, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6637a, b",false,true,173651,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1870,1868,1872,"Cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6638a, b",false,true,173652,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1872,1870,1874,"Cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6650a, b",false,true,175660,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1890,1888,1892,"Cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E.A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3101a–c,false,true,158002,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Worcester Skirt Company,"American, 1864–1950",,Worcester Skirt Company,American,1864,1950,1861–63,1861,1863,"cotton, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3105a–c,false,true,158006,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,1876,1876,1876,"cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3106a–c,false,true,158007,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,1876,1876,1876,"silk, cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3112a–d,false,true,158014,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1885,1883,1887,"cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E.A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3113a–c,false,true,158015,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,ca. 1890,1888,1892,"cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E.A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3496a–e,false,true,158439,Costume Institute,Corset,"""Queen Bess""",American,,,,,Manufacturer,,Royal Worcester Corset Company,"American, 1864–1950",,Royal Worcester Corset Company,American,1864,1950,1876,1876,1876,"silk, bone, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6584a, b",false,true,173583,Costume Institute,Spats,Spats,American,,,,,Department Store,,B. Altman & Co.,"American, 1865–1990",,B. Altman & Co.,American,1865,1990,ca. 1890,1888,1892,Wool,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Bequest of Matilda Alice Shaw, 1948",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.223,false,true,157033,Costume Institute,Knickerbockers,Knickerbockers,American,,,,,Department Store,,"Browning, King & Company","American, 1868–1934",,"Browning, King & Company",American,1868,1934,ca. 1925,1923,1927,wool,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Wesley Wallace Tillotson, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.257,false,true,157411,Costume Institute,Smoking jacket,Smoking jacket,American,,,,,Department Store,,"Browning, King & Company","American, 1868–1934",,"Browning, King & Company",American,1868,1934,1905–15,1905,1915,wool,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Simon Spiegal, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2762a–d,false,true,157625,Costume Institute,Corset,Corset,American,,,,,Manufacturer,,"O'Conner, Moffatt & Company","American, 1868–1945",,"O'Conner, Moffatt & Company",American,1868,1945,1918–19,1918,1919,"cotton, bone, metal, elastic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.258,false,true,84226,Costume Institute,Fan,Fan,American,,,,,Manufacturer,,H.C.F. Koch,"American, 1890–1930",,Koch H.C.F.,American,1890,1930,ca. 1880,1875,1885,"pasteboard, wood",,"Gift of The Brooklyn Historical Society, 1991",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7532,false,true,174585,Costume Institute,Cravat,Cravat,American,,,,,Retailer,,Saks & Company,"American, 1902–1924",,Saks & Company,American,1902,1924,1920–30,1920,1930,Cotton,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Albert Moss, 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5784a, b",false,true,169988,Costume Institute,Evening shoes,Evening shoes,American,,,,,Retailer,,Saks & Company,"American, 1902–1924",,Saks & Company,American,1902,1924,ca. 1933,1931,1935,"Silk, leather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Helen Gray, 1970",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.53.2,false,true,84013,Costume Institute,Opera cloak,Opera cloak,American,,,,,Department Store,,Lord & Taylor,"American, founded 1826",,Lord & Taylor,American,1826,2050,1850s,1850,1859,silk,,"Gift of Mrs. Josephine Mingle Tennant, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.34.2a–d,false,true,81661,Costume Institute,Dinner dress,Dinner dress,American,,,,,Department Store,,Lord & Taylor,"American, founded 1826",,Lord & Taylor,American,1826,2050,1877–83,1877,1883,"silk, glass",,"Gift of Elizabeth Kellogg Ammidon, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6669,false,true,173676,Costume Institute,Ribbon,Ribbon,American,,,,,Manufacturer,,W. B. Conkey Co.,"American, Chicago, Illinois",,"Conkey Co., W. B.",American,1850,1950,1893,1893,1893,"Silk, metallic, pigment",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E.A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2351,false,true,157167,Costume Institute,Parasol,Parasol,American,,,,,Department Store,,Stern Brothers,"American, founded New York, 1867",,Stern Brothers,American,1867,2001,ca. 1870,1868,1872,"silk, wood, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Lillian E. Glenn Peirce in memory of Mrs. Luther G. Tillotson, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2528,false,true,157365,Costume Institute,Parasol,Parasol,American,,,,,Department Store,,Stern Brothers,"American, founded New York, 1867",,Stern Brothers,American,1867,2001,1876,1876,1876,"silk, wood, glass, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Genevieve Doherty in memory of Mrs. John Henry, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4709a, b",false,true,168914,Costume Institute,Wedding Slippers,Wedding slippers,American,,,,,Department Store,,Stern Brothers,"American, founded New York, 1867",,Stern Brothers,American,1867,2001,1880,1880,1880,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Polly Dix, 1954",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/168914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6567,false,true,180660,Costume Institute,Dinner shoes,Dinner shoes,American,,,,,Manufacturer,,A.E. Little & Co.,"American, Lynn, Massachusetts 1898–1934",,"Little & Co., A.E.",American,1898,1934,ca. 1916,1911,1921,"Leather, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the estate of Carrie Chapman Catt, 1947",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/180660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1423a, b",false,true,156137,Costume Institute,Evening shoes,Evening shoes,American,,,,,Manufacturer,,A.E. Little & Co.,"American, Lynn, Massachusetts 1898–1934",,"Little & Co., A.E.",American,1898,1934,ca. 1916,1914,1918,leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Theodora Wilbour, 1932",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4226a, b",false,true,168459,Costume Institute,Dinner shoes,Dinner shoes,American,,,,,Manufacturer,,A.E. Little & Co.,"American, Lynn, Massachusetts 1898–1934",,"Little & Co., A.E.",American,1898,1934,ca. 1910,1908,1912,"Leather, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Theodora Wilbour, 1932",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/168459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.4509a, b",false,true,168717,Costume Institute,Evening pumps,Evening pumps,American,,,,,Manufacturer,,A.E. Little & Co.,"American, Lynn, Massachusetts 1898–1934",,"Little & Co., A.E.",American,1898,1934,1918,1918,1918,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Eleanor Curnow, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/168717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5012a, b",false,true,169234,Costume Institute,Dinner shoes,Dinner shoes,American,,,,,Manufacturer,,A.E. Little & Co.,"American, Lynn, Massachusetts 1898–1934",,"Little & Co., A.E.",American,1898,1934,1915,1915,1915,"Leather, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Hollis K. Thayer, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.5340a, b",false,true,169558,Costume Institute,Evening shoes,Evening shoes,American,,,,,Manufacturer,,A.E. Little & Co.,"American, Lynn, Massachusetts 1898–1934",,"Little & Co., A.E.",American,1898,1934,1915–16,1915,1916,"Leather, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. William B. Parker, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1455a–d,false,true,156172,Costume Institute,Evening oxfords,Evening oxfords,American,,,,,Department Store,,Alfred J. Cammeyer,"American, founded New York, active 1875–1930s",,"Cammeyer, Alfred J.",American,1875,1940,ca. 1891,1889,1893,leather,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Marion Fisher, 1952",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6950a–c,false,true,173956,Costume Institute,Morning suit,Morning suit,American,,,,,Maker,,"Browning, King & Company","American, 1868–1934",,"Browning, King & Company",American,1868,1934,1913,1913,1913,"Wool, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Wesley Wallace Tillotson, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.53.39a, b",false,true,113633,Costume Institute,Shoes,Shoes,American,,,,,Designer,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1925–30,1925,1930,leather,,"Gift of Mrs. Peter A. Cohn, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.49.57.2a, b",false,true,113160,Costume Institute,Evening slippers,Evening slippers,American,,,,,Designer,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1900–1910,1900,1910,leather,,"Gift of Mrs. Robert S. Dixon, 1949",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113160,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.51.72.2a, b",false,true,113180,Costume Institute,Wedding Shoes,Wedding shoes,American,,,,,Designer,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1896,1896,1896,"leather, silk",,"Gift of Miss Mildred Mendelson, 1951",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.56.33.33a, b",false,true,113615,Costume Institute,Shoes,Shoes,American,,,,,Designer,,Alfred J. Cammeyer,1849–1913,,"Cammeyer, Alfred J.",American,1875,1940,1928,1928,1928,"leather, straw",,"Gift of Mrs. Sidney Bernard, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/113615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.424.1,false,true,96026,Costume Institute,Cocktail Dress,Cocktail dress,American,,,,,Designer,,Ceil Chapman,"American, born 1912",,Chapman Ceil,American,1912,1912,1950–53,1950,1953,rayon,,"Gift of Ann M. Kivlan, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.424.4,false,true,89341,Costume Institute,Cocktail Dress,Cocktail dress,American,,,,,Designer,,Ceil Chapman,"American, born 1912",,Chapman Ceil,American,1912,1912,1955–59,1955,1959,silk,,"Gift of Ann M. Kivlan, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/89341,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.196.3,false,true,84293,Costume Institute,Dress,Dress,American,,,,,Designer,,Ceil Chapman,"American, born 1912",,Chapman Ceil,American,1912,1912,late 1940s,1945,1955,synthetic,,"Gift of Mary (Howard) de Liagre, 2002",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1993.185a, b",false,true,80752,Costume Institute,Wedding Dress,Wedding dress,American,,,,,Designer,,Ceil Chapman,"American, born 1912",,Chapman Ceil,American,1912,1912,1948,1948,1948,cotton,,"Gift of Sophie Mitropoulos, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7196,false,true,174324,Costume Institute,Dinner dress,Dinner dress,American,,,,,Designer,,Ceil Chapman,"American, born 1912",,Chapman Ceil,American,1912,1912,1945,1945,1945,Synthetic,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of George F. Hoag, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7204,false,true,174332,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Ceil Chapman,"American, born 1912",,Chapman Ceil,American,1912,1912,1953,1953,1953,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Hanna T. Rose, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174332,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2000.366.1a, b",false,true,81763,Costume Institute,Cocktail Dress,Cocktail dress,American,,,,,Designer,,Ceil Chapman,"American, born 1912",,Chapman Ceil,American,1912,1912,1950s,1950,1959,synthetic,,"Anonymous Gift, 2000",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.238a–z,false,true,79894,Costume Institute,Vanity Case,Vanity case,American,,,,,Designer,,Richard Hudnut,"American, 1855–1928",,"Hudnut, RIchard",American,1855,1928,1920s,1920,1929,"leather, brass",,"Gift of Lois Small Zabriskie, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/79894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.5369,false,true,169584,Costume Institute,Apron,Apron,American,,,,,Designer,,L. S. Plaut & Company,"American, 1870–1923",,Plaut & Company L. S.,American,1870,1923,1930–39,1930,1939,Cotton,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Albert Ogden in memory of Sheldon Stewart, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1315,false,true,156017,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Peggy Hoyt,"American, 1893–1937",,Hoyt Peggy,American,1893,1937,spring/summer 1928,1928,1928,"silk, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6926a, b",false,true,173946,Costume Institute,Riding Habit,Riding Habit,American,,,,,Designer,,Saks & Company,"American, 1902–1924",,Saks & Company,American,1902,1924,1922,1922,1922,"Wool, leather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. A. E. Laurancelle, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.68.53.5a–h,true,true,81137,Costume Institute,Wedding Ensemble,Wedding ensemble,American,,,,,Designer,,Herman Rossberg,"American, active 1880s",,Rossberg Herman,American,1880,1880,1887,1887,1887,wool,,"Gift of Mrs. James G. Flockhart, 1968",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.104,false,true,155711,Costume Institute,Evening coat,Evening coat,American,,,,,Designer,,C. G. Gunther's Sons,"American, founded 1820",,Gunther,American,1820,1820,ca. 1930,1928,1932,"silk, fur, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. J. W. Post, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3862,false,true,158846,Costume Institute,Cape (Tippet),Tippet,American,,,,,Designer,,C. G. Gunther's Sons,"American, founded 1820",,Gunther,American,1820,1820,ca. 1865,1863,1867,"fur, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Emma Crampton Trainer, 1968",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6528,false,true,173541,Costume Institute,Evening coat,Evening coat,American,,,,,Designer,,C. G. Gunther's Sons,"American, founded 1820",,Gunther,American,1820,1820,1914,1914,1914,"Silk, metallic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. J. W. Post, 1944",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.478a, b",false,true,159013,Costume Institute,Accessory Set,Accessory set,American,,,,,Designer,,C. G. Gunther's Sons,"American, founded 1820",,Gunther,American,1820,1820,1890–99,1890,1899,"fur, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Eleanor F. Peck, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.57.38.1,false,true,107859,Costume Institute,Cape,Cape,American,,,,,Designer,,Lord & Taylor,"American, founded 1826",,Lord & Taylor,American,1826,2050,ca. 1885,1880,1890,"jet, silk, cotton",,"Gift of Lord and Taylor, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.152.1,false,true,91143,Costume Institute,Hunting coat,Hunting coat,American,,,,,Designer,,John Patterson & Co.,"American, founded 1852",,Patterson John,American,1852,1852,late 19th century,1850,1899,"wool, silk, metal",,"Gift of Jessie Leonard Hill, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/91143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.69.24.1,false,true,81727,Costume Institute,Coat,Coat,American,,,,,Designer,,John Patterson & Co.,"American, founded 1852",,Patterson John,American,1852,1852,1890s,1890,1899,wool,,"Gift of Mrs. Edward E. Murray, 1969",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.4969,false,true,169192,Costume Institute,Cap,Cap,American,,,,,Designer,,John Patterson & Co.,"American, founded 1852",,Patterson John,American,1852,1852,1929,1929,1929,"Wool, leather, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/169192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7030,false,true,174020,Costume Institute,Coat,Coat,American,,,,,Designer,,John Patterson & Co.,"American, founded 1852",,Patterson John,American,1852,1852,1929,1929,1929,Wool,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.820a, b",false,true,159394,Costume Institute,Uniform Coat,Uniform coat,American,,,,,Designer,,John Patterson & Co.,"American, founded 1852",,Patterson John,American,1852,1852,1915,1915,1915,"wool, fur",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.819a–f,false,true,159391,Costume Institute,Uniform,Uniform,American,,,,,Designer,,John Patterson & Co.,"American, founded 1852",,Patterson John,American,1852,1852,1928,1928,1928,"wool, leather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7029a–c,false,true,174019,Costume Institute,Uniform,Uniform,American,,,,,Designer,,John Patterson & Co.,"American, founded 1852",,Patterson John,American,1852,1852,1928,1928,1928,"Wool, leather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1981.21.2a, b",false,true,92337,Costume Institute,Dinner dress,Dinner dress,American,,,,,Designer,,Alice M. Dunstan,"American, active 1892–1926",,Dunstan Alice M.,American,1892,1926,ca. 1895,1890,1900,silk,,"Purchase, Irene Lewisohn Bequest, 1981",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/92337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3289,false,true,158208,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Herbert Luey,"American, Northfield, Massachusetts 1860–1916 Brooklyn",,Luey Herbert,American,1860,1916,1912–14,1912,1914,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Amelia Beard Hollenback, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158208,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6561,false,true,173568,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Herbert Luey,"American, Northfield, Massachusetts 1860–1916 Brooklyn",,Luey Herbert,American,1860,1916,1908–11,1908,1911,"Silk, beads, metallic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Helen Rice, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7483,false,true,175822,Costume Institute,Dinner dress,Dinner dress,American,,,,,Designer,,Herbert Luey,"American, Northfield, Massachusetts 1860–1916 Brooklyn",,Luey Herbert,American,1860,1916,1908–10,1908,1910,"Silk, cotton",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Amelia Beard Hollenback, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3030a, b",false,true,157922,Costume Institute,Evening dress,Evening dress,American,,,,,Designer,,Herbert Luey,"American, Northfield, Massachusetts 1860–1916 Brooklyn",,Luey Herbert,American,1860,1916,ca. 1890,1888,1892,"silk, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. James Dowd Lester, 1942",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.6764a, b",false,true,173779,Costume Institute,Bridesmaid dress,Bridesmaid dress,American,,,,,Designer,,Herbert Luey,"American, Northfield, Massachusetts 1860–1916 Brooklyn",,Luey Herbert,American,1860,1916,1880,1880,1880,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Polly Dix, 1954",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.8,false,true,96595,Costume Institute,Evening coat,Evening coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1926,1926,1926,[no medium available],,"Gift of Mrs. Juliet Mason, 1972",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.7,false,true,104694,Costume Institute,Picture hat,Picture hat,French,,,,,Designer|Design House,Attributed to|Attributed to,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1922,1917,1927,"silk, glass",,"Purchase, Gifts in memory of Elizabeth Lawrence, 1983",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/104694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.91,false,true,96596,Costume Institute,Evening coat,Evening coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1931–32,1931,1932,[no medium available],,"Gift of Pamela Rankin Smith, 1974",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.77,false,true,104257,Costume Institute,Bonnet,Bonnet,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1910,1905,1915,"cotton, metallic thread, silk",,"Purchase, Irene Lewisohn Bequest, 1989",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/104257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.393,false,true,84806,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1933–37,1933,1937,synthetic,,"Gift of Mrs. Hilda Sutton, 1975",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.215,false,true,82597,Costume Institute,Evening coat,Evening coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1927,1927,1927,"cotton, wool",,"Isabel Shults Fund, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.162,false,true,84146,Costume Institute,Dress,Dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1916,1916,1916,"silk, glass",,"Gift of Charles Kleibacker, 2002",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84146,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.98.4,false,true,84818,Costume Institute,Evening cape,Evening cape,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,late 1930s,1935,1939,"cotton, plastic",,"Gift of Sven E. Hsia, 1980",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.30.13,false,true,96597,Costume Institute,Evening wrap,Evening wrap,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1929,1929,1929,fur,,"Gift of Madame Lilliana Teruzzi, 1972",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.199.1,false,true,84813,Costume Institute,Evening dress,"""Fusée""",French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1938,1938,1938,silk,,"Gift of Mrs. Lawrence W. Snell, 1973",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.122.1,false,true,81478,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1924–25,1924,1925,"silk, cotton",,"Gift of Mrs. William B. Given Jr., 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81478,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.122.2,false,true,81479,Costume Institute,Coat,Coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1924–25,1924,1925,"silk, wool, fur, metal",,"Gift of Mrs. William B. Given Jr., 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.344.8,false,true,84802,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1934,1934,1934,silk,,"Gift of Mrs. Anthony Wilson, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84802,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.344.9,false,true,84803,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1934,1934,1934,silk,,"Gift of Mrs. Anthony Wilson, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.574.1,false,true,96601,Costume Institute,Coat,Coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1927–28,1927,1928,"wool, silk",,"Gift of Mrs. Herbert Parsons, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.28.20,false,true,80176,Costume Institute,Coat,Coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1932–33,1932,1933,"silk, fur (possibly ermine)",,"Gift of Estate of Lillian Gish, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.588.2,false,true,80337,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1926,1921,1931,silk,,"Gift of Miriam K. W. Coletti, 1995",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80337,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.62.8.5,false,true,82646,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1922,1922,1922,"silk, metal, glass",,"Gift of Mrs. Stephen C. Clark, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.198.10,false,true,103869,Costume Institute,Coat,Coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1930–31,1930,1931,silk,,"Gift of Mrs. William Rhinelander Stewart, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/103869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.198.11,false,true,103870,Costume Institute,Coat,Coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1930–31,1930,1931,silk,,"Gift of Mrs. William Rhinelander Stewart, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/103870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.198.12,false,true,103279,Costume Institute,Dress,Dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1930–31,1930,1931,"cotton, silk",,"Gift of Mrs. William Rhinelander Stewart, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/103279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.165.19,false,true,84810,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1937–39,1937,1939,"silk, cotton",,"Gift of Mrs. Stephen M. Kellen, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.295.22,false,true,96599,Costume Institute,Evening coat,Evening coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1917,1912,1922,silk,,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.344.18,false,true,108910,Costume Institute,Teddy,Teddy,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1920s,1920,1929,silk,,"Gift of Mrs. Anthony Wilson, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/108910,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.344.19,false,true,84807,Costume Institute,Teddy,Teddy,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,mid-1930s,1933,1937,silk,,"Gift of Mrs. Anthony Wilson, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.587.17,false,true,96602,Costume Institute,Ski jacket,Ski jacket,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1924–25,1924,1925,wool,,"Gift of Miriam Whitney Coletti, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/96602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.40.22.1,false,true,102920,Costume Institute,Dress,Dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1910,1910,1910,silk,,"Gift of Mrs. Whitewright Watson, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/102920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.40.22.2,false,true,102921,Costume Institute,Dress,Dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1910,1910,1910,silk,,"Gift of Mrs. Whitewright Watson, 1940",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/102921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.56.49.1,false,true,81970,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1924–25,1924,1925,silk,,"Gift of Mrs. W.R. Grace, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.62.58.1,false,true,81462,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1924,1924,1924,"silk, metallic thread, glass",,"Gift of Mrs. Albert Spalding, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.63.38.3,false,true,109363,Costume Institute,Blouse,Blouse,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1946,1946,1946,silk,,"Gift of Mrs. Moore Montgomery, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.66.58.1,false,true,84065,Costume Institute,Evening jacket,Evening jacket,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1936–37,1936,1937,"silk, metallic thread, fur",,"Gift of Mrs. Leon L. Roos, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.357,false,true,158521,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1938,1938,1938,"silk, mother-of-pearl, beads",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Ian B. MacDonald, 1965",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1288,false,true,155986,Costume Institute,Headdress,Headdress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1925,1923,1927,"cotton, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Rodman A. Heeren, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1365,false,true,156071,Costume Institute,Evening dress,"""Phèdre""",French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1933,1933,1933,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mark Walsh, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1376,false,true,156084,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1934–35,1934,1935,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of George Drew, 1988",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2221,false,true,157023,Costume Institute,Evening shawl,Evening shawl,French,,,,,Designer|Design House,Attributed to|Attributed to,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1922,1920,1924,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2249,false,true,157053,Costume Institute,Evening belt,Evening belt,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1925,1923,1927,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Brooklyn Museum Collection",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2401,false,true,157223,Costume Institute,Hat,Hat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1932,1932,1932,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frank L. Babbott, 1954",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2506,false,true,157340,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1926,1926,1926,"silk, rhinestones, pearls",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Ogden Goelet, Peter Goelet, and Madison Clews, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2520,false,true,157356,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1935,1933,1937,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Ogden Goelet, Peter Goelet, and Madison Clews, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2543,false,true,157382,Costume Institute,Evening bag,Evening bag,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1925–35,1925,1935,"silk, metal Silk, metallic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of the executors of the estate of Clara M. Blum in memory of Mr. and Mrs. Albert Blum, 1966",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2563,false,true,157403,Costume Institute,Evening cloche,Evening cloche,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1925,1923,1927,"cotton, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2565,false,true,157405,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,Attributed to|Attributed to,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1922,1920,1924,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2572,false,true,157414,Costume Institute,Evening cloche,Evening cloche,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1925,1923,1927,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2631,false,true,157479,Costume Institute,Blouse,Blouse,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1928,1926,1930,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Martin Kamer, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2635,false,true,157483,Costume Institute,Evening dress,"""Jolibois""",French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1922–23,1922,1923,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Louise Gross, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2640,false,true,157490,Costume Institute,Evening coat,Evening coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1926,1926,1926,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Gardner and Diana Thoenen in Memory of Meredith Smith Thoenen, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2782,false,true,157646,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1920,1920,1920,"silk, rhinestones, linen",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Countess Edna E. de Frise, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157646,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2802,false,true,157669,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1922,1922,1922,"metal, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Norman W. Wassman, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2860,false,true,157733,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1925,1925,1925,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Helen Appleton Read, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157733,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3225,false,true,158139,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1923,1921,1925,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mr. and Mrs. Maxime L. Hermanos, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3400,false,true,158335,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1938–39,1938,1939,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Brooklyn Museum Collection",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6756,false,true,173768,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,Attributed to|Attributed to,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1920,1918,1922,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Countess Edna E. de Frise, 1953",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6767,false,true,173782,Costume Institute,Evening coat,Evening coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,winter 1932–33,1932,1933,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frank L. Babbott, 1954",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173782,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6768,false,true,173783,Costume Institute,Hat,Hat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1932,1932,1932,Wool,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frank L. Babbott, 1954",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.6850,false,true,173863,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,winter 1928,1928,1928,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Arturo and Paul Peralta-Ramos, 1955",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/173863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7025,false,true,174015,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,Attributed to|Attributed to,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1930,1930,1930,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. George B. Wells, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7328,false,true,175780,Costume Institute,Overcoat,Overcoat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1934,1934,1934,Wool,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of A. C. Moss, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7533,false,true,174586,Costume Institute,Hat,Hat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1918,1916,1920,"Cotton, wire",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7534,false,true,174587,Costume Institute,Hat,Hat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1935,1933,1937,"Straw, silk",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.8114,false,true,175139,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,Possibly|Possibly,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1927,1925,1929,"Cotton, beads, metallic",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Rena Gill, 1985",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.60.19a, b",false,true,84022,Costume Institute,Evening ensemble,"""Ko.I.Noor""",French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1927,1927,1927,"silk, metal b) silk, metal",,"Gift of Mme. Yves Lanvin, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.61.22a, b",false,true,103601,Costume Institute,Suit,Suit,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1927,1927,1927,"wool, silk",,"Gift of Mrs. Dale Scott, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/103601,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1993.423.1a, b",false,true,80887,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1934,1929,1939,"silk, metal",,"Gift of Miriam W. Coletti, 1993",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/80887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.528.7a–c,false,true,84892,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,Attributed to|Attributed to,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1935,1930,1940,silk,,"Gift of Mrs. Charles C. Paterson, 1982",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84892,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.377.1a–c,false,true,81484,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1930,1925,1935,silk,,"Gift of Mrs. Jill L. Leinbach & Mr. James L. Long in memory of their mother, Mrs. Jane P. Long, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/81484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.96a, b",false,true,159558,Costume Institute,Evening coat,Evening coat,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1932–33,1932,1933,"wool, fur",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frank L. Babbott, Jr., 1941",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/159558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.46.4.18a, b",true,true,82103,Costume Institute,Evening dress,"""Cyclone""",French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1939,1939,1939,"silk, spangles",,"Gift of Mrs. Harrison Williams, Lady Mendl, and Mrs. Ector Munn, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.54.49.3a, b",false,true,103584,Costume Institute,Ensemble,Ensemble,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1921,1916,1926,wool,,"Gift of Mrs. Roland L. Redmond, 1954",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/103584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.165.18a–c,false,true,84809,Costume Institute,Ensemble,Ensemble,French,,,,,Designer|Design House,Attributed to,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1937–38,1937,1938,a) silk b) cotton c) glass,,"Gift of Mrs. Stephen M. Kellen, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.184.12a–c,false,true,94981,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1930,1930,1930,"silk, metallic thread",,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.436a, b",false,true,158967,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1937,1937,1937,"silk, leather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.46.4.17a–c,false,true,82102,Costume Institute,Dinner dress,Dinner dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1939,1939,1939,"silk, spangles",,"Gift of Mrs. Harrison Williams, Lady Mendl, and Mrs. Ector Munn, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.58.34.19a, b",false,true,84557,Costume Institute,Dress,Dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1937,1937,1937,"cotton, silk",,"Gift of Mrs. John Chambers Hughes, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.58.34.24a, b",false,true,84239,Costume Institute,Suit,Suit,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1940,1940,1940,silk,,"Gift of Mrs. John Chambers Hughes, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/84239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.582.199a–e,false,true,97353,Costume Institute,Wedding Ensemble,Wedding ensemble,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,1940,1940,1940,"linen, silk, cotton",,"Gift of Marvin B. Patterson (Mrs. Jefferson Patterson), 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/97353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1259a, b",false,true,155953,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1935,1935,1935,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Ogden Goelet, Peter Goelet and Madison Clews in memory of Mrs. Henry Clews, 1960",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/155953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1318a, b",false,true,156020,Costume Institute,Evening dress,"""Roseraie""",French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1923,1923,1923,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Anonymous gift, 1964",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.1335a, b",false,true,156039,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1938,1938,1938,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.2228a, b",false,true,157030,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1923,1923,1923,"silk, metal, feather",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1988",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.2887a, b",false,true,157761,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1925–26,1925,1926,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3182a, b",false,true,158091,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1930–31,1930,1931,silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. George B. Wells, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.3310a, b",false,true,158233,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1937,1937,1937,synthetic,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2009.300.7310a, b",false,true,175769,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,winter 1930–31,1930,1931,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Anthony Wilson, 1963",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/175769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.1364a–c,false,true,156070,Costume Institute,Evening ensemble,Evening ensemble,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,spring/summer 1923,1923,1923,"silk, metal, rhinestones",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/156070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2442a–c,false,true,157269,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1935,1933,1937,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Robert E. Blum, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2564a–d,false,true,157404,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1923–24,1923,1924,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. Frederick H. Prince, Jr., 1967",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2628a–c,false,true,157476,Costume Institute,Ensemble,Ensemble,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,ca. 1926,1924,1928,"silk, wool",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Designated Purchase Fund, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.2655a–c,false,true,157506,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,fall/winter 1926–27,1926,1927,"silk, metal",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Brooklyn Museum Collection",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/157506,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.7026a–c,false,true,174016,Costume Institute,Evening dress,Evening dress,French,,,,,Designer|Design House,,Jeanne Lanvin|House of Lanvin,"French, 1867–1946|French, founded 1889",,"Lanvin, Jeanne|Lanvin, House of",French|French,1867 |1889,1946 |9999,winter 1930–31,1930,1931,Silk,,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of Mrs. George B. Wells, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/174016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.90,false,true,110001,Costume Institute,Necktie,Necktie,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1974,1974,1974,silk,,"Gift of John Michael Powers, Jr., 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/110001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.54,false,true,109995,Costume Institute,Evening dress,Evening dress,French,,,,,Design House|Designer,Attributed to|Attributed to,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,ca. 1933,1928,1938,"silk, cotton",,"Gift of Gabriella de Balogh, 1984",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.30.2,false,true,94721,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,fall/winter 1926–27,1926,1927,"silk, plastic, glass",,"Gift of Mrs. Gilbert W. Chapman, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.29.3,false,true,94699,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1928–29,1928,1929,silk,,"Gift of Mr. and Mrs. Charles Abrams, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.303.2,false,true,109980,Costume Institute,Hat,Hat,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1914–20,1914,1920,[no medium available],,"Gift of Elizabeth A. Tilson, 1975",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.303.3,false,true,83472,Costume Institute,Hat,Hat,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1918–25,1918,1925,"horsehair, silk, feathers",,"Gift of Elizabeth A. Tilson, 1975",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/83472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.184.7,false,true,94692,Costume Institute,Robe de Style,Robe de Style,French,,,,,Designer|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1920–25,1920,1925,"silk, metallic thread, glass, plastic",,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.184.8,false,true,94715,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,fall/winter 1926–27,1926,1927,silk,,"Purchase, Marcia Sand Bequest, in memory of her daughter, Tiger (Joan) Morse, 1978",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.87.43,false,true,107363,Costume Institute,Hat,Hat,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1967–69,1967,1969,wool,,"Gift of Janet A. Sloane, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/107363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.487.2,false,true,94722,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,fall/winter 1924–25,1924,1925,"silk, metallic thread",,"Gift of Mrs. Loretta Hines Howard, 1980",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.365.1,false,true,109985,Costume Institute,Wedding Dress,Wedding dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,fall/winter 1926–27,1926,1927,silk,,"Gift of Varney Thompson Elliott and Rosemary Thompson Franciscus, in memory of their mother, Margaret Whitney Thompson, 1985",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.365.5,false,true,94720,Costume Institute,Dress,Dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1927,1927,1927,silk,,"Gift of Varney Thompson Elliott and Rosemary Thompson Franciscus, in memory of their mother, Margaret Whitney Thompson, 1985",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.377.2,false,true,94727,Costume Institute,Evening dress,Evening dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,ca. 1930,1925,1935,silk,,"Gift of Mrs. Jill L. Leinbach & Mr. James L. Long in memory of their mother, Mrs. Jane P. Long, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.516.4,false,true,94703,Costume Institute,Dress,Dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1925–29,1925,1929,"silk, feathers",,"Gift of Miriam W. Coletti, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.516.9,false,true,91818,Costume Institute,Ski trousers,Ski trousers,French,,,,,Design House|Designer,Attributed to,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,ca. 1925,1920,1930,"wool, silk",,"Gift of Miriam W. Coletti, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/91818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.62.8.2,false,true,94718,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1922,1922,1922,silk,,"Gift of Mrs. Stephen C. Clark, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.516.10,false,true,109982,Costume Institute,Belt,Belt,French,,,,,Design House|Designer,Attributed to|Attributed to,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1920,1920,1920,"leather, steel",,"Gift of Miriam W. Coletti, 1986",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109982,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2015.95a, b",false,true,680139,Costume Institute,Dress,Dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1928,1928,1928,"silk, metal, glass",,"Purchase, Friends of The Costume Institute Gifts, 2015",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/680139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.46.93.1,false,true,94725,Costume Institute,Evening dress,Evening dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,fall/winter 1930–31,1930,1931,silk,,"Gift of Mrs. Charles Leibman, 1946",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.50.84.1,false,true,94726,Costume Institute,Evening dress,Evening dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,spring/summer 1930,1930,1930,silk,,"Gift of Miss Frances McFadden, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.56.49.3,false,true,94719,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,Attributed to|Attributed to,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1925,1925,1925,"silk, beading",,"Gift of Mrs. W. R. Grace, 1956",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.57.46.2,false,true,94702,Costume Institute,Dress,Dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,ca. 1927,1922,1932,"silk, wool, metal",,"Gift of Mrs. Seaman Schepps, 1957",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1976.28.2a, b",false,true,94705,Costume Institute,Evening dress,Evening dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,spring/summer 1927,1927,1927,"silk, metallic thread",,"Gift of Mrs. Herbert Bayer, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1976.30.1a, b",false,true,94723,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,fall/winter 1926–27,1926,1927,"silk, plastic, glass",,"Gift of Mrs. Gilbert W. Chapman, 1976",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1973.199.2a, b",false,true,94694,Costume Institute,Evening dress,Evening dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1949,1949,1949,silk,,"Gift of Mrs. Lawrence W. Snell, 1973",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.62.8.1a, b",false,true,94717,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1922,1922,1922,"silk, glass, metal",,"Gift of Mrs. Stephen C. Clark, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.62.8.3a, b",false,true,94716,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1922,1922,1922,"cotton, silk, glass, metal",,"Gift of Mrs. Stephen C. Clark, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.62.8.4a, b",false,true,109983,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1922,1922,1922,"cotton, metal, silk",,"Gift of Mrs. Stephen C. Clark, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.201.9a–c,false,true,94724,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1931,1931,1931,"silk, plastic",,"Gift of Esmé O'Brien Hammond, 1977",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"1979.344.10a, b",false,true,94706,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1922,1922,1922,"silk, glass, metallic thread",,"Gift of Mrs. Anthony Wilson, 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.59.13a–c,false,true,92035,Costume Institute,Suit,Suit,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1931,1931,1931,wool,,"Gift of Stanley F. Waldman, President, The Manhattan Galleries, Inc., 1979",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/92035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"C.I.62.58.2a, b",false,true,94704,Costume Institute,Evening dress,Evening dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,spring/summer 1923,1923,1923,"silk, metallic thread, glass beads",,"Gift of Mrs. Albert Spaulding, 1962",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.58.34.2a–c,false,true,94707,Costume Institute,Afternoon dress,Afternoon dress,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,1920,1920,1920,silk,,"Gift of Mrs. John Chambers Hughes, 1958",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/94707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +C.I.61.40.1a–c,false,true,109986,Costume Institute,Robe de Style,Robe de Style,French,,,,,Design House|Designer,,House of Lanvin|Jeanne Lanvin,"French, founded 1889|French, 1867–1946",,"Lanvin, House of|Lanvin, Jeanne",French|French,1889 |1867,9999 |1946,ca. 1927,1922,1932,"silk, metal",,"Gift of Mrs. David J. Colton, 1961",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/109986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.341.1,false,true,82615,Costume Institute,Engraving,The Vanity of Women: Masks and Bustles,Dutch,,,,,Artist,Attributed to,Maerten de Vos,"Netherlandish, Antwerp 1532–1603 Antwerp",,"Vos, Maerten de",Netherlandish,1532,1603,ca. 1600,1595,1605,engraved paper,,"Purchase, Irene Lewisohn Trust Gift, 2001",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.341.2,false,true,82616,Costume Institute,Engraving,The Pride of Women: Ruffs,Dutch,,,,,Artist,,Maerten de Vos,"Netherlandish, Antwerp 1532–1603 Antwerp",,"Vos, Maerten de",Netherlandish,1532,1603,ca. 1600,1595,1605,engraved paper,,"Purchase, Irene Lewisohn Trust Gift, 2001",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/82616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.300.3102a–c,false,true,158003,Costume Institute,Corset,Corset,American,,,,,Department Store|Manufacturer,,Day & Horton|Worcester Skirt Company,"American|American, 1864–1950",,Day & Horton|Worcester Skirt Company,American|American,1864,1950,1866–67,1866,1867,"cotton, metal, bone",,"Brooklyn Museum Costume Collection at The Metropolitan Museum of Art, Gift of the Brooklyn Museum, 2009; Gift of E. A. Meister, 1950",,,,,,,,,,,,,,http://www.metmuseum.org/art/collection/search/158003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.134.8,false,true,435608,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Barrois,ca. 1790,,Barrois,French(?),1790,1790,ca. 1790,1785,1795,Ivory,Diameter 2 3/4 in. (67 mm),"Bequest of Ella Church Strobell, 1917",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435608,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.72,false,true,437485,European Paintings,"Painting, miniature",Portrait of a Girl,,,,,,Artist,,Sampson Towgood Roch,"Irish, 1759–1847",,"Roch, Sampson Towgood",Irish,1759,1847,ca. 1790,1785,1795,Ivory,"Oval, 2 1/4 x 1 7/8 in. (58 x 48 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437485,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.43.291,false,true,437141,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Monogrammist IS,"Flemish, dated 1621",,Monogrammist IS,Flemish,1621,1621,1621,1621,1621,Oil on copper,"Oval, 4 1/8 x 3 5/8 in. (105 x 91 mm)","Bequest of Mary Anna Palmer Draper, 1914",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.10,false,true,437783,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Mikail Ivanovich Terebenev,"Russian, 1795–1866",,"Terebenev, Mikail Ivanovich",Russian,1795,1866,ca. 1830,1825,1835,Ivory,"Oval, 2 7/8 x 2 1/4 in. (72 x 57 mm)","Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437783,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.53,false,true,436610,European Paintings,"Painting, miniature",The Painter Louis Joseph Maurice (1730–1820),,,,,,Artist,,Peter Adolf Hall,"Swedish, 1739–1793",,"Hall, Peter Adolf",Swedish,1739,1793,1772,1772,1772,Ivory,"Oval, 3 3/8 x 2 5/8 in. (84 x 67 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.45,false,true,436612,European Paintings,"Painting, miniature",Portrait of a Young Woman,,,,,,Artist,,Peter Adolf Hall,"Swedish, 1739–1793",,"Hall, Peter Adolf",Swedish,1739,1793,ca. 1790,1785,1795,Ivory,"Oval, 2 3/4 x 2 1/8 in. (70 x 55 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +40.186.1,false,true,435685,European Paintings,"Painting, miniature",A Man with the Initials AC,,,,,,Artist,,John Bogle,"British, 1746?–1803",,"Bogle, John","British, Scottish",1746,1803,1774,1774,1774,Ivory,"Oval, 1 3/8 x 1 1/8 in. (36 x 30 mm)","Gift of Mrs. S. M. Breckenridge Long, 1940",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +40.186.2,false,true,435686,European Paintings,"Painting, miniature",A Woman with the Initials MCC,,,,,,Artist,,John Bogle,"British, 1746?–1803",,"Bogle, John","British, Scottish",1746,1803,1773,1773,1773,Ivory,"Oval, 1 5/8 x 1 3/8 in. (41 x 34 mm)","Gift of Mrs. S. M. Breckenridge Long, 1940",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.61,false,true,437484,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Charles Robertson,"Irish, ca. 1760–1821",,"Robertson, Charles",Irish,1760,1821,1810,1810,1810,Ivory,"Oval, 2 1/2 x 2 in. (61 x 50 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.136.13,false,true,436865,European Paintings,"Painting, miniature",Vertumnus and Pomona,,,,,,Artist,,Thomas Lefebure,"Flemish, ca. 1636–1720",,"Lefebure, Thomas",Flemish,1636,1720,1676,1676,1676,Paper,"Image exclusive of gold rim and dark brown border, 6 3/4 x 5 in. (172 x 126 mm)","Bequest of Margaret Crane Hurlbut, 1933",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.50.333,false,true,437142,European Paintings,"Painting, miniature",Traveling Players,,,,,,Artist,,Monogrammist JG,"Northern European, ca. 1630",,Monogrammist JG,Northern European,1630,1630,ca. 1630,1625,1635,Vellum laid on wood,2 3/4 x 3 1/2 in. (70 x 90 mm),"Bequest of Kate Read Blacque, in memory of her husband, Valentine Alexander Blacque, 1937",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.740,false,true,437139,European Paintings,"Painting, miniature",An Artist Painting a Heraldic Shield in a Cabinet of Curiosities,,,,,,Artist,,Monogrammist FA,"Northern European, dated 1664",,Monogrammist FA,Northern European,1664,1664,1664,1664,1664,Vellum laid on card,4 1/4 x 6 in. (106 x 152 mm),"Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.81,false,true,435821,European Paintings,"Painting, miniature","Portrait of a Woman, Said to Be Emma (1765–1815), Lady Hamilton",,,,,,Artist,,Adam Buck,"Irish, Cork 1759–1833 London",,"Buck, Adam",Irish,1759,1833,1804,1804,1804,Ivory,Diameter 1 1/4 in. (32 mm),"The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.27,false,true,436844,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Jean-Baptiste Ponce Lambert,"Swiss (?), active ca. 1801–12",,"Lambert, Jean-Baptiste Ponce",Swiss,1801,1812,1801,1801,1801,Ivory,Diameter 2 3/8 in. (59 mm),"Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.27,false,true,436674,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,Attributed to,Nathaniel Hone,"Irish, Dublin 1718–1784 London",,"Hone, Nathaniel",Irish,1718,1784,ca. 1760,1755,1765,Enamel,"Oval, 1 1/8 x 1 in. (30 x 25 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436674,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.1,false,true,435636,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Rudolphe Bel,"Swiss, active by 1822–died 1849",,"Bel, Rudolphe",Swiss,1822,1849,1822,1822,1822,Paper stretched over metal,"Oval, 5 x 3 1/2 in. (126 x 88 mm)","Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.25,false,true,436258,European Paintings,Painting,Self-Portrait,,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1620–21,1620,1621,Oil on canvas,47 1/8 x 34 5/8 in. (119.7 x 87.9 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Painted Canvases,,http://www.metmuseum.org/art/collection/search/436258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.37,false,true,436259,European Paintings,"Painting, drawing",Study Head of a Young Woman,,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1618–20,1618,1620,"Oil on paper, laid down on wood",22 1/4 x 16 3/8 in. (56.5 x 41.6 cm),"Gift of Mrs. Ralph J. Hines, 1957",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/436259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.110.4,false,true,436059,European Paintings,"Painting, miniature",Princess María Francisca de Asis de Borbón and Her Son Infante Carlos Luis María Fernando de Borbón,,,,,,Artist,,Luis de la Cruz y Rios,"Spanish, active by 1815–died 1850",,"Cruz y Rios, Luis de la",Spanish,1815,1850,1818,1818,1818,Ivory,Obverse and reverse each 2 1/4 x 1 3/4 in. (57 x 47 mm),"Gift of Mrs. John LaPorte Given, 1945",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436059,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.6,false,true,437614,European Paintings,"Painting, miniature",Portrait of a Young Woman,,,,,,Artist,Attributed to,Piat Joseph Sauvage,"Flemish, Tournai 1744–1818 Tournai",,"Sauvage, Piat Joseph",Flemish,1744,1818,ca. 1790–95,1790,1795,Ivory,Diameter 2 1/8 in. (54 mm),"Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437614,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.390,false,true,437482,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Andrew Robertson,"British, Aberdeen, Scotland 1777–1845 London",,"Robertson, Andrew","British, Scottish",1777,1845,1828,1828,1828,Ivory,"Oval, 3 3/4 x 2 7/8 in. (80 x 63 mm)","Morris K. Jesup Fund, 1986",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.21,false,true,437483,European Paintings,"Painting, miniature",Sir Joshua Reynolds (1723–1792),,,,,,Artist,,Archibald Robertson,"American, Moneymusk, Scotland 1765–1835 New York",,"Robertson, Archibald","American, Scottish",1765,1835,1786–91,1786,1791,Ivory,"Oval, 3 x 2 3/8 in. (75 x 60 mm)","Bequest of Geraldine Winslow Goddard, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.110.3,false,true,436289,European Paintings,"Painting, miniature",Self-Portrait,,,,,,Artist,Attributed to,John Faed,"British, Burley Mill, Scotland 1820–1902 Burley Mill, Scotland",,"Faed, John","British, Scottish",1820,1902,ca. 1850,1845,1855,Ivory,"Oval, 2 5/8 x 2 1/8 in. (68 x 55 mm)","Gift of Mrs. John LaPorte Given, 1945",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436289,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.8,false,true,436577,European Paintings,Painting,Saint Andrew,,,,,,Artist,Workshop of,El Greco,"Spanish, ca. 1610",,Greco El,Greek,1540,1614,ca. 1610,1605,1615,Oil on canvas,43 1/4 x 25 1/4 in. (109.9 x 64.1 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436577,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.115.2,false,true,436816,European Paintings,Painting,The Lacemaker,,,,,,Artist,,Bernhard Keil,"Danish, 1624–1687",,"Keil, Bernhard",Danish,1624,1687,ca. 1665,1660,1670,Oil on canvas,28 1/4 x 38 1/4 in. (71.8 x 97.2 cm),"Bequest of Edward Fowles, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.294,false,true,436698,European Paintings,Painting,Still Life: A Basket of Grapes and Other Fruit,,,,,,Artist,,Jacob van Hulsdonck,"Flemish, 1582–1647",,"Hulsdonck, Jacob van",Flemish,1582,1647,probably ca. 1635–45,1635,1645,Oil on wood,19 5/8 x 25 1/2 in. (49.8 x 64.8 cm),"The Alfred N. Punnett Endowment Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.128.14,false,true,436049,European Paintings,Painting,Philip IV (1605–1665) in Parade Armor,,,,,,Artist,,Gaspar de Crayer,"Flemish, 1584–1669",,"Crayer, Gaspar de",Flemish,1584,1669,ca. 1628,1623,1633,Oil on canvas,72 x 46 1/2 in. (182.9 x 118.1 cm),"Bequest of Helen Hay Whitney, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.45,false,true,436427,European Paintings,Painting,A Partridge and Small Game Birds,,,,,,Artist,,Jan Fyt,"Flemish, 1611–1661",,"Fyt, Jan",Flemish,1611,1661,1650s,1650,1659,Oil on canvas,18 1/4 x 14 1/4 in. (46.4 x 36.2 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.652,false,true,437867,European Paintings,Painting,A Bouquet of Flowers in a Crystal Vase,,,,,,Artist,,Nicolaes van Veerendael,"Flemish, 1640–1691",,"Veerendael, Nicolaes van",Flemish,1640,1691,1662,1662,1662,Oil on canvas,19 1/2 x 15 7/8 in. (49.5 x 40.3 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.4,false,true,436693,European Paintings,Painting,A Musical Gathering at the Court of the Elector Karl Albrecht of Bavaria,,,,,,Artist,,Peter Jacob Horemans,"Flemish, 1700–1776",,"Horemans, Peter Jacob",Flemish,1700,1776,1730,1730,1730,Oil on canvas,34 1/2 x 42 in. (87.6 x 106.7 cm),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.737,false,true,436290,European Paintings,Painting,Flowers by a Stone Vase,,,,,,Artist,,Peter Faes,"Flemish, 1750–1814",,"Faes, Peter",Flemish,1750,1814,1786,1786,1786,Oil on wood,20 x 14 7/8 in. (50.8 x 37.8 cm),"Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436290,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.738,false,true,436291,European Paintings,Painting,Flowers in a Stone Vase,,,,,,Artist,,Peter Faes,"Flemish, 1750–1814",,"Faes, Peter",Flemish,1750,1814,1786,1786,1786,Oil on wood,19 1/2 x 15 1/8 in. (49.5 x 38.4 cm),"Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.50,false,true,437177,European Paintings,Painting,"Count Giacomo Durazzo (1717–1794) in the Guise of a Huntsman with His Wife (Ernestine Aloisia Ungnad von Weissenwolff, 1732–1794)",,,,,,Artist,,Martin van Meytens the Younger,"Swedish, 1695–1770",,"Meytens, Martin van, the Younger",Swedish,1695,1770,probably early 1760s,1760,1763,Oil on canvas,90 1/8 x 75 in. (228.9 x 190.5 cm),"Gift of Mr. and Mrs. Nate B. Spingold, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.101,false,true,436776,European Paintings,Painting,The Dead Christ in the Tomb with Two Angels,,,,,,Artist,,Abraham Janssen van Nuyssen,"Flemish, ca. 1575–1632",,"Janssen van Nuyssen, Abraham",Flemish,1575,1632,ca. 1610,1605,1615,Oil on canvas,45 3/8 x 58 in. (115.3 x 147.3 cm),"Gift of James Belden, in memory of Evelyn Berry Belden, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.17,false,true,437729,European Paintings,Painting,Saint Michael the Archangel,,,,,,Artist,,Ignacio de Ries,"Spanish, 1616–after 1665",,"Ries, Ignacio de",Spanish,1616,1665,1640s,1640,1649,Oil on canvas,64 3/4 x 43 1/4 in. (164.5 x 109.9 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.189.2,false,true,436808,European Paintings,Painting,"Edward Smith Stanley (1752–1834), Twelfth Earl of Derby, with His First Wife (Lady Elizabeth Hamilton, 1753–1797) and Their Son (Edward Smith Stanley, 1775–1851)",,,,,,Artist,,Angelica Kauffmann,"Swiss, Chur 1741–1807 Rome",,"Kauffmann, Angelica",Swiss,1741,1807,ca. 1776,1771,1781,Oil on canvas,50 x 40 in. (127 x 101.6 cm),"Gift of Bernard M. Baruch, in memory of his wife, Annie Griffen Baruch, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.187,false,true,436809,European Paintings,Painting,The Sorrow of Telemachus,,,,,,Artist,,Angelica Kauffmann,"Swiss, Chur 1741–1807 Rome",,"Kauffmann, Angelica",Swiss,1741,1807,1783,1783,1783,Oil on canvas,32 3/4 x 45 in. (83.2 x 114.3 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436809,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.188,false,true,436810,European Paintings,Painting,Telemachus and the Nymphs of Calypso,,,,,,Artist,,Angelica Kauffmann,"Swiss, Chur 1741–1807 Rome",,"Kauffmann, Angelica",Swiss,1741,1807,1782,1782,1782,Oil on canvas,32 1/2 x 44 1/4 in. (82.6 x 112.4 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436810,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.45.10,false,true,437238,European Paintings,Painting,Still Life of Fruit and Game,,,,,,Artist,,Pieter van Overschee,"Flemish, active ca. 1645–61",,"Overschee, Pieter van",Flemish,1645,1661,1645,1645,1645,Oil on wood,32 7/8 x 46 3/4 in. (83.5 x 118.7 cm),"Bequest of Grace Wilkes, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.22,false,true,436318,European Paintings,Painting,Madame Gaye,,,,,,Artist,,Mariano Fortuny Marsal,"Spanish, Reus 1838–1874 Rome",,"Fortuny Marsal, Mariano",Spanish,1838,1874,1865,1865,1865,Oil on canvas,54 x 39 1/2 in. (137.2 x 100.3 cm),"Gift of Alfred Corning Clark, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436318,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.1134,false,true,634108,European Paintings,Painting,The Dream of the Shepherd (Der Traum des Hirten),,,,,,Artist,,Ferdinand Hodler,"Swiss, Bern 1853–1918 Geneva",,"Hodler, Ferdinand",Swiss,1853,1918,1896,1896,1896,Oil on canvas,98 1/2 × 51 3/8 in. (250.2 × 130.5 cm),"Purchase, European Paintings Funds, Lila Acheson Wallace Gift, Catharine Lorillard Wolfe Collection, Wolfe Fund, Charles and Jessie Price Gift, funds from various donors, and Bequests of Collis P. Huntington and Isaac D. Fletcher, by exchange, 2013",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/634108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.6,false,true,437198,European Paintings,Painting,The Nightingale Sings,,,,,,Artist,,Mikhail Vasilievich Nesterov,"Russian, Ufa 1862–1942 Moscow",,"Nesterov, Mikhail Vasilievich",Russian,1862,1942,1923,1923,1923,Oil on canvas,31 7/8 x 27 3/8 in. (81 x 69.5 cm),"Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1198,false,true,436872,European Paintings,Painting,Study for a Portrait of a Woman,,,,,,Artist,,Sir Peter Lely (Pieter van der Faes),"British, Soest 1618–1680 London",,"Lely, Peter, Sir (Pieter van der Faes)","Dutch, British",1618,1680,1670s,1670,1679,Oil on canvas,26 1/2 x 21 1/8 in. (67.3 x 53.7 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.613,false,true,438407,European Paintings,Painting,Man Holding a Jug,,,,,,Artist,,Michiel Sweerts,"Flemish, Brussels 1618–1664 Goa",,"Sweerts, Michiel",Flemish,1618,1664,ca. 1660,1655,1665,Oil on canvas,19 3/8 x 15 3/8 in. (49.2 x 39.1 cm),"Gift of Herman and Lila Shickman, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.459.1,true,true,437769,European Paintings,Painting,Clothing the Naked,,,,,,Artist,,Michiel Sweerts,"Flemish, Brussels 1618–1664 Goa",,"Sweerts, Michiel",Flemish,1618,1664,ca. 1661,1656,1666,Oil on canvas,32 1/4 x 45 in. (81.9 x 114.3 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.411,false,true,436423,European Paintings,Painting,The Night-Hag Visiting Lapland Witches,,,,,,Artist,,Henry Fuseli,"Swiss, Zürich 1741–1825 London",,"Fuseli, Henry",Swiss,1741,1825,1796,1796,1796,Oil on canvas,40 x 49 3/4 in. (101.6 x 126.4 cm),"Purchase, Bequest of Lillian S. Timken, by exchange, and Victor Wilbour Memorial, The Alfred N. Punnett Endowment, Marquand and Charles B. Curtis Funds, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436423,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +93.29,false,true,437084,European Paintings,Painting,Landscape with a Battle between Two Rams,,,,,,Artist,,Jan Miel,"Flemish, Beveren 1599–1664 Turin",,"Miel, Jan",Flemish,1599,1664,ca. 1640,1635,1645,Oil on canvas,68 1/4 x 97 5/8 in. (173.4 x 248 cm),"Gift of Princess Brancaccio, 1893",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.48,false,true,437662,European Paintings,Painting,"William Archer Shee (1810–1899), the Artist's Son",,,,,,Artist,,Sir Martin Archer Shee,"Irish, Dublin 1769–1850 Brighton",,"Shee, Martin Archer, Sir",Irish,1769,1850,ca. 1820,1815,1825,Oil on canvas,30 x 24 3/4 in. (76.2 x 62.9 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.4,false,true,437631,European Paintings,Painting,Night Scene on the Volga,,,,,,Artist,,Alexei Kondratievich Savrasov,"Russian, Moscow 1830–1897 Moscow",,"Savrasov, Alexei Kondratievich",Russian,1830,1897,1871,1871,1871,Oil on wood,12 7/8 x 21 1/2 in. (32.7 x 54.6 cm),"Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.57,false,true,437460,European Paintings,Painting,A Canal in Venice,,,,,,Artist,,Martín Rico y Ortega,"Spanish, Madrid 1833–1908 Venice",,"Rico y Ortega, Martín",Spanish,1833,1908,ca. 1875,1875,1875,Oil on canvas,19 3/4 x 26 3/4 in. (50.2 x 67.9 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437460,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.39,false,true,437053,European Paintings,Painting,The Afternoon Meal (La Merienda),,,,,,Artist,,Luis Meléndez,"Spanish, Naples 1716–1780 Madrid",,"Meléndez, Luis",Spanish,1716,1780,ca. 1772,1772,1772,Oil on canvas,41 1/2 x 60 1/2 in. (105.4 x 153.7 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437053,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +83.11,false,true,436879,European Paintings,Painting,"Auction Sale in Clinton Hall, New York, 1876",,,,,,Artist,,Ignacio de León y Escosura,"Spanish, Oviedo 1834–1901 Toledo",,"León y Escosura, Ignacio de",Spanish,1834,1901,1876,1876,1876,Oil on canvas,22 3/8 x 31 5/8 in. (56.8 x 80.3 cm),"Gift of the artist, 1883",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.147.1,false,true,437004,European Paintings,"Painting, part of an altarpiece",Saint Adalbert and Saint Procopius,,,,,,Artist,,Master of Eggenburg,"Austrian, Tirol, active 1490–1500",,Master of Eggenburg,Austrian,1490,1500,ca. 1490–1500,1490,1500,"Oil on spruce, gold ground","Painted surface, including black border, 27 1/8 x 17 in. (68.9 x 43.2 cm)","Gift of William Rosenwald, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.147.2,false,true,437005,European Paintings,"Painting, part of an altarpiece",The Burial of Saint Wenceslas,,,,,,Artist,,Master of Eggenburg,"Austrian, Tirol, active 1490–1500",,Master of Eggenburg,Austrian,1490,1500,ca. 1490–1500,1490,1500,Oil and gold on spruce,"Painted surface, including black border, 27 1/8 x 17 in. (68.9 x 43.2 cm)","Gift of William Rosenwald, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.150.1,false,true,437754,European Paintings,Painting,After the Ball,,,,,,Artist,,Alfred Stevens,"Belgian, Brussels 1823–1906 Paris",,"Stevens, Alfred",Belgian,1823,1906,1874,1874,1874,Oil on canvas,37 3/4 x 27 1/8 in. (95.9 x 68.9 cm),"Gift of Estate of Marie L. Russell, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.56,false,true,437756,European Paintings,Painting,The Japanese Robe,,,,,,Artist,,Alfred Stevens,"Belgian, Brussels 1823–1906 Paris",,"Stevens, Alfred",Belgian,1823,1906,ca. 1872,1868,1877,Oil on canvas,36 1/2 x 25 1/8 in. (92.7 x 63.8 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.33,false,true,437169,European Paintings,Painting,Maude Adams (1872–1953) as Joan of Arc,,,,,,Artist,,Alphonse Mucha,"Czech, Ivančice 1860–1939 Prague",,"Mucha, Alphonse",Czech,1860,1939,1909,1909,1909,Oil on canvas,82 1/4 x 30 in. (208.9 x 76.2 cm),"Gift of A. J. Kobler, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437169,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.41,false,true,436257,European Paintings,Painting,Saint Rosalie Interceding for the Plague-stricken of Palermo,,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,1624,1624,1624,Oil on canvas,39 1/4 x 29 in. (99.7 x 73.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.221,false,true,436260,European Paintings,Painting,Study Head of an Old Man with a White Beard,,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1617–20,1617,1620,Oil on wood,26 x 20 1/4 in. (66 x 51.4 cm),"Egleston Fund, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.26,false,true,436256,European Paintings,Painting,"Robert Rich (1587–1658), Second Earl of Warwick",,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1632–35,1632,1635,Oil on canvas,"81 7/8 x 50 3/8 in. (208 x 128 cm), with added strip of 2 1/8 in. (5.4 cm) at top","The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.33.1,false,true,436261,European Paintings,Painting,Virgin and Child,,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1620,1615,1625,Oil on wood,25 1/4 x 19 1/2 in. (64.1 x 49.5 cm),"Fletcher Fund, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.5,false,true,436262,European Paintings,Painting,Virgin and Child with Saint Catherine of Alexandria,,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1630,1625,1635,Oil on canvas,43 x 35 3/4 in. (109.2 x 90.8 cm); with added strips 44 1/8 x 37 in. (112.1 x 94 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.145.1,false,true,436264,European Paintings,"Painting, monochrome",A Man Riding a Horse,,,,,,Artist,Attributed to,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1630,1625,1635,Oil on wood,10 1/8 x 8 7/8 in. (25.7 x 22.5 cm),"Gift of Mr. and Mrs. Siegfried Bieber, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.145.2,false,true,436263,European Paintings,"Painting, monochrome",A Man Mounting a Horse,,,,,,Artist,Attributed to,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1630,1625,1635,Oil on wood,10 x 8 3/4 in. (25.4 x 22.2 cm),"Gift of Mr. and Mrs. Siegfried Bieber, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.11,false,true,436254,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1618,1613,1623,Oil on wood,41 3/4 x 28 5/8 in. (106 x 72.7 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.16,false,true,436252,European Paintings,Painting,"James Stuart (1612–1655), Duke of Richmond and Lennox",,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1633–35,1633,1635,Oil on canvas,85 x 50 1/4 in. (215.9 x 127.6 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436252,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.615,false,true,436255,European Paintings,Painting,"Portrait of a Woman, Called the Marchesa Durazzo",,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,probably ca. 1622–25,1622,1625,Oil on canvas,44 5/8 x 37 3/4 in. (113.3 x 95.9 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436255,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.619,true,true,436253,European Paintings,Painting,Lucas van Uffel (died 1637),,,,,,Artist,,Anthony van Dyck,"Flemish, Antwerp 1599–1641 London",,"Dyck, Anthony van",Flemish,1599,1641,ca. 1622,1621,1627,Oil on canvas,49 x 39 5/8 in. (124.5 x 100.6 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.42.20,false,true,438644,European Paintings,Painting,"View on the Quirinal Hill, Rome",,,,,,Artist,,Simon Denis,"Flemish, Antwerp 1755–1813 Naples",,"Denis, Simon",Flemish,1755,1813,1800,1800,1800,"Oil on paper, laid down on canvas",11 5/8 x 16 1/8 in. (29.5 x 41 cm),"The Whitney Collection, Gift of Wheelock Whitney III, and Purchase, Gift of Mr. and Mrs. Charles S. McVeigh, by exchange, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.400.40,false,true,439365,European Paintings,Painting,Aniene River at Tivoli,,,,,,Artist,,Simon Denis,"Flemish, Antwerp 1755–1813 Naples",,"Denis, Simon",Flemish,1755,1813,ca. 1786–89,1786,1806,Oil on paper,12 3/4 x 11 1/2 in. (32.4 x 29.2 cm),"Thaw Collection, Jointly Owned by The Metropolitan Museum of Art and The Morgan Library & Museum, Gift of Eugene V. Thaw, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +02.24,false,true,437528,European Paintings,Painting,The Holy Family with Saints Francis and Anne and the Infant Saint John the Baptist,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,early or mid-1630s,1630,1636,Oil on canvas,69 1/2 x 82 1/2 in. (176.5 x 209.6 cm),"Gift of James Henry Smith, 1902",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.73,false,true,437536,European Paintings,Painting,Wolf and Fox Hunt,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",and Workshop,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1616,1611,1621,Oil on canvas,96 5/8 x 148 1/8 in. (245.4 x 376.2 cm),"John Stewart Kennedy Fund, 1910",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.22,false,true,437523,European Paintings,Painting,Atalanta and Meleager,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1616,1611,1621,Oil on wood,52 1/2 x 42 in. (133.4 x 106.7 cm),"Fletcher Fund, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437523,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.162,false,true,437535,European Paintings,Painting,Venus and Adonis,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,probably mid-1630s,1634,1636,Oil on canvas,"With added strips, 77 3/4 x 95 5/8 in. (197.5 x 242.9 cm)","Gift of Harry Payne Bingham, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.187,false,true,437534,European Paintings,"Painting, sketch",The Triumph of Henry IV,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1630,1625,1635,Oil on wood,19 1/2 x 32 7/8 in. (49.5 x 83.5 cm),"Rogers Fund, 1942",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.218,false,true,437531,European Paintings,Painting,"Portrait of a Woman, Probably Susanna Lunden (Susanna Fourment, 1599–1628)",,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1625–27,1625,1627,Oil on wood,"30 1/4 x 23 5/8 in. (76.8 x 60 cm), including added strip of 3 3/4 in. (9.5 cm) at bottom","Gift of Mr. and Mrs. Charles Wrightsman, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.238,true,true,437532,European Paintings,Painting,"Rubens, His Wife Helena Fourment (1614–1673), and Their Son Frans (1633–1678)",,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1635,1630,1640,Oil on wood,80 1/4 x 62 1/4 in. (203.8 x 158.1 cm),"Gift of Mr. and Mrs. Charles Wrightsman, in honor of Sir John Pope-Hennessy, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.196,false,true,437526,European Paintings,Painting,A Forest at Dawn with a Deer Hunt,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1635,1630,1640,Oil on wood,24 1/4 x 35 1/2 in. (61.5 x 90.2 cm),"Purchase, The Annenberg Foundation, Mrs. Charles Wrightsman, Michel David-Weill, The Dillon Fund, Henry J. and Drue Heinz Foundation, Lola Kramarsky, Annette de la Renta, Mr. and Mrs. Arthur Ochs Sulzberger, The Vincent Astor Foundation, and Peter J. Sharp Gifts; special funds, gifts, and other gifts and bequests, by exchange, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.135.1,false,true,437529,European Paintings,Painting,"The Holy Family with Saint Elizabeth, Saint John, and a Dove",,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1608–9,1608,1609,Oil on wood,26 x 20 1/4 in. (66 x 51.4 cm),"Bequest of Ada Small Moore, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.160.12,false,true,437527,European Paintings,Painting,The Glorification of the Eucharist,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1630–32,1630,1632,Oil on wood,28 x 19 in. (71.1 x 48.3 cm),"Bequest of Ogden Mills, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.99,false,true,437533,European Paintings,Painting,Study of Two Heads,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1609,1604,1614,Oil on wood,27 1/2 x 20 1/2 in. (69.9 x 52.1 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.24,false,true,437530,European Paintings,Painting,"Portrait of a Man, Possibly an Architect or Geographer",,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,1597,1597,1597,Oil on copper,8 1/2 x 5 3/4 in. (21.6 x 14.6 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.433.336,false,true,437524,European Paintings,"Painting, sketch",The Coronation of the Virgin,,,,,,Artist,,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,ca. 1632–33,1632,1633,Oil on wood,19 5/8 x 16 in. (49.8 x 40.6 cm),"Bequest of Scofield Thayer, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.433.20,false,true,483334,European Paintings,Painting,"Landscape, Kragerø",,,,,,Artist,,Edvard Munch,"Norwegian, Løten 1863–1944 Ekely",,"Munch, Edvard",Norwegian,1863,1944,1912,1912,1912,Oil on canvas,28 1/2 x 39 1/2 in. (72.4 x 100.3 cm),"Bequest of Scofield Thayer, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/483334,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +34.73,true,true,437455,European Paintings,Painting,The Holy Family with Saints Anne and Catherine of Alexandria,,,,,,Artist,,Jusepe de Ribera (called Lo Spagnoletto),"Spanish, Játiva 1591–1652 Naples",,"Ribera, Jusepe de (called Lo Spagnoletto)",Spanish,1591,1652,1648,1648,1648,Oil on canvas,82 1/2 x 60 3/4 in. (209.6 x 154.3 cm),"Samuel D. Lee Fund, 1934",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.416,false,true,441971,European Paintings,Painting,The Tears of Saint Peter,,,,,,Artist,,Jusepe de Ribera (called Lo Spagnoletto),"Spanish, Játiva 1591–1652 Naples",,"Ribera, Jusepe de (called Lo Spagnoletto)",Spanish,1591,1652,ca. 1612–13,1612,1613,Oil on canvas,63 3/4 x 45 in. (161.9 x 114.3 cm),"Purchase, Gift of Mrs. William M. Haupt, from the collection of Mrs. James B. Haggin, by exchange, and 2011 Benefit Fund, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.86,true,true,437869,European Paintings,Painting,Juan de Pareja (1606–1670),,,,,,Artist,,Velázquez (Diego Rodríguez de Silva y Velázquez),"Spanish, Seville 1599–1660 Madrid",,Velázquez (Diego Rodríguez de Silva y Velázquez),Spanish,1599,1660,1650,1650,1650,Oil on canvas,32 x 27 1/2 in. (81.3 x 69.9 cm),"Purchase, Fletcher and Rogers Funds, and Bequest of Miss Adelaide Milton de Groot (1876–1967), by exchange, supplemented by gifts from friends of the Museum, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.42,false,true,437874,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Velázquez (Diego Rodríguez de Silva y Velázquez),"Spanish, Seville 1599–1660 Madrid",,Velázquez (Diego Rodríguez de Silva y Velázquez),Spanish,1599,1660,ca. 1630–35,1630,1635,Oil on canvas,27 x 21 3/4 in. (68.6 x 55.2 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.43,false,true,437870,European Paintings,Painting,"María Teresa (1638–1683), Infanta of Spain",,,,,,Artist,,Velázquez (Diego Rodríguez de Silva y Velázquez),"Spanish, Seville 1599–1660 Madrid",,Velázquez (Diego Rodríguez de Silva y Velázquez),Spanish,1599,1660,1651–54,1651,1654,Oil on canvas,Overall 13 1/2 x 15 3/4 in. (34.3 x 40 cm); original painted surface 12 7/8 x 15 1/8 in. (32.7 x 38.4 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.29,false,true,437875,European Paintings,Painting,Portrait of a Man,,,,,,Artist,Workshop of,Velázquez,"Spanish, Seville 1599–1660 Madrid",,Velázquez,Spanish,1599,1660,ca. 1650,1645,1655,Oil on canvas,27 1/4 x 22 1/4 in. (69.2 x 56.5 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.631,false,true,437871,European Paintings,Painting,The Supper at Emmaus,,,,,,Artist,,Velázquez (Diego Rodríguez de Silva y Velázquez),"Spanish, Seville 1599–1660 Madrid",,Velázquez (Diego Rodríguez de Silva y Velázquez),Spanish,1599,1660,1622–23,1622,1623,Oil on canvas,48 1/2 x 52 1/4 in. (123.2 x 132.7 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.639,false,true,437873,European Paintings,Painting,"Philip IV (1605–1665), King of Spain",,,,,,Artist,,Velázquez (Diego Rodríguez de Silva y Velázquez),"Spanish, Seville 1599–1660 Madrid",,Velázquez (Diego Rodríguez de Silva y Velázquez),Spanish,1599,1660,probably 1624,1624,1624,Oil on canvas,78 3/4 x 40 1/2 in. (200 x 102.9 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.39,false,true,437902,European Paintings,Painting,Examining Antique Arms,,,,,,Artist,,José Villegas y Cordero,"Spanish, Seville 1848–1921 Madrid",,"Villegas y Cordero, José",Spanish,1848,1921,1870,1870,1870,Oil on wood,15 5/8 x 12 1/2 in. (39.7 x 31.8 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437902,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.11,false,true,436799,European Paintings,Painting,The Holy Family with Saint Anne and the Young Baptist and His Parents,,,,,,Artist,,Jacob Jordaens,"Flemish, Antwerp 1593–1678 Antwerp",,"Jordaens, Jacob",Flemish,1593,1678,early 1620s and 1650s,1620,1659,Oil on wood,66 7/8 x 59 in. (169.9 x 149.9 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436799,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.76,false,true,436798,European Paintings,Painting,The Holy Family with Shepherds,,,,,,Artist,,Jacob Jordaens,"Flemish, Antwerp 1593–1678 Antwerp",,"Jordaens, Jacob",Flemish,1593,1678,1616,1616,1616,"Oil on canvas, transferred from wood",42 x 30 in. (106.7 x 76.2 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.444,false,true,435813,European Paintings,Painting,Aeneas and the Sibyl in the Underworld,,,,,,Artist,,Jan Brueghel the Younger,"Flemish, Antwerp 1601–1678 Antwerp",,"Brueghel, Jan, the Younger",Flemish,1601,1678,1630s,1630,1639,Oil on copper,10 1/2 x 14 1/8 in. (26.7 x 35.9 cm),"Gift of Mrs. Erna S. Blade, in memory of her uncle, Sigmund Herrmann, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.58,false,true,435814,European Paintings,Painting,A Basket of Flowers,,,,,,Artist,,Jan Brueghel the Younger,"Flemish, Antwerp 1601–1678 Antwerp",,"Brueghel, Jan, the Younger",Flemish,1601,1678,probably 1620s,1620,1629,Oil on wood,18 1/2 x 26 7/8 in. (47 x 68.3 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.272,false,true,437624,European Paintings,"Painting, grisaille overdoor",The Triumph of Bacchus,,,,,,Artist,,Piat Joseph Sauvage,"Flemish, Tournai 1744–1818 Tournai",,"Sauvage, Piat Joseph",Flemish,1744,1818,early 1780s,1780,1783,Oil on canvas,19 1/4 x 46 1/8 in. (48.9 x 117.2 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.228,false,true,652416,European Paintings,Painting,The Crucifixion,,,,,,Artist,,Pedro Orrente,"Spanish, Murcia 1580–1645 Valencia",,"Orrente, Pedro",Spanish,1580,1645,ca. 1625–30,1625,1630,Oil on canvas,48 3/4 × 40 1/2 in. (123.8 × 102.9 cm),"Purchase, Charles and Jessie Price and Fern and George Wachter Gifts, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/652416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.13,true,true,437175,European Paintings,Painting,Virgin and Child,,,,,,Artist,,Bartolomé Estebán Murillo,"Spanish, Seville 1617–1682 Seville",,"Murillo, Bartolomé Estebán",Spanish,1617,1682,ca. 1670–72,1670,1672,Oil on canvas,65 1/4 x 43 in. (165.7 x 109.2 cm),"Rogers Fund, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.219,false,true,437173,European Paintings,Painting,Don Andrés de Andrade y la Cal,,,,,,Artist,,Bartolomé Estebán Murillo,"Spanish, Seville 1617–1682 Seville",,"Murillo, Bartolomé Estebán",Spanish,1617,1682,ca. 1665–72,1665,1672,Oil on canvas,79 x 47 in. (200.7 x 119.4 cm),"Bequest of Collis P. Huntington, by exchange, 1927",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.190,false,true,437174,European Paintings,Painting,A Knight of Alcántara or Calatrava,,,,,,Artist,,Bartolomé Estebán Murillo,"Spanish, Seville 1617–1682 Seville",,"Murillo, Bartolomé Estebán",Spanish,1617,1682,ca. 1650–55,1650,1655,Oil on canvas,"Overall, with added strips, 77 x 43 3/4 in. (195.6 x 111.1 cm); original canvas 77 x 38 1/2 in. (195.6 x 97.8 cm)","Gift of Rudolf J. Heinemann, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.17,false,true,437172,European Paintings,Painting,The Crucifixion,,,,,,Artist,,Bartolomé Estebán Murillo,"Spanish, Seville 1617–1682 Seville",,"Murillo, Bartolomé Estebán",Spanish,1617,1682,ca. 1675,1670,1680,Oil on canvas,20 x 13 in. (50.8 x 33 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.168,false,true,437862,European Paintings,Painting,Pietà,,,,,,Artist,,Juan de Valdés Leal,"Spanish, Seville 1622–1690 Seville",,"Valdés Leal, Juan de",Spanish,1622,1690,ca. 1657–60,1657,1660,Oil on canvas,63 1/4 x 56 1/2 in. (160.7 x 143.5 cm),"Victor Wilbour Memorial Fund, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.285,false,true,440727,European Paintings,Painting,German Landscape with View towards a Broad Valley,,,,,,Artist,,Fritz Petzholdt,"Danish, Copenhagen 1805–1838 Patras",,"Petzholdt, Fritz",Danish,1805,1838,ca. 1829–30,1824,1834,"Oil on paper, laid down on canvas",Original paper support: 5 3/16 x 9 11/16 in. (13.2 x 24.6 cm) Paper support mounted on stretched canvas: 5 13/16 x 10 1/4 in. (14.8 x 26 cm),"Gift of Wheelock Whitney III, in honor of Eugene V. Thaw, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +72.2,false,true,437778,European Paintings,Painting,Judith with the Head of Holofernes,,,,,,Artist,,David Teniers the Younger,"Flemish, Antwerp 1610–1690 Brussels",,"Teniers, David, the Younger",Flemish,1610,1690,1650s,1650,1659,Oil on copper,14 1/2 x 10 3/8 in. (36.8 x 26.4 cm),"Gift of Gouverneur Kemble, 1872",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.99,false,true,437779,European Paintings,Painting,Peasants Dancing and Feasting,,,,,,Artist,,David Teniers the Younger,"Flemish, Antwerp 1610–1690 Brussels",,"Teniers, David, the Younger",Flemish,1610,1690,ca. 1660,1655,1665,Oil on canvas,25 1/8 x 29 1/2 in. (63.8 x 74.9 cm); with added strip 26 7/8 x 29 1/2 in. (68.3 x 74.9 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.65.5,false,true,437777,European Paintings,Painting,Guardroom with the Deliverance of Saint Peter,,,,,,Artist,,David Teniers the Younger,"Flemish, Antwerp 1610–1690 Brussels",,"Teniers, David, the Younger",Flemish,1610,1690,ca. 1645–47,1645,1647,Oil on wood,21 3/4 x 29 7/8 in. (55.2 x 75.9 cm),"Gift of Edith Neuman de Végvár, in honor of her husband, Charles Neuman de Végvár, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.447,false,true,441967,European Paintings,Painting,View over Hallingdal,,,,,,Artist,,Johan Christian Dahl,"Norwegian, Bergen 1788–1857 Dresden",,"Dahl, Johan Christian",Norwegian,1788,1857,1844,1844,1844,Oil on canvas,9 1/2 x 14 3/8 in. (24.1 x 36.5 cm),"Gift of Asbjorn R. Lunde, in memory of his brother, Karl Lunde, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.2,false,true,438954,European Paintings,Painting,Mother and Child by the Sea,,,,,,Artist,,Johan Christian Dahl,"Norwegian, Bergen 1788–1857 Dresden",,"Dahl, Johan Christian",Norwegian,1788,1857,1830,1830,1830,Oil on canvas,6 1/4 x 8 1/8 in. (15.9 x 20.6 cm),"Gift of Eugene V. Thaw, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.1,false,true,437441,European Paintings,Painting,Portrait of a Boy,,,,,,Artist,,Ilia Efimovich Repin,"Russian, Chuguev 1844–1930 Kuokkala",,"Repin, Ilia Efimovich",Russian,1844,1930,1884,1884,1884,Oil on canvas,22 1/2 x 17 3/8 in. (57.2 x 44.1 cm),"Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.2,false,true,437442,European Paintings,Painting,Vsevolod Mikhailovich Garshin (1855–1888),,,,,,Artist,,Ilia Efimovich Repin,"Russian, Chuguev 1844–1930 Kuokkala",,"Repin, Ilia Efimovich",Russian,1844,1930,1884,1884,1884,Oil on canvas,35 x 27 1/4 in. (88.9 x 69.2 cm),"Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.280.4,false,true,437440,European Paintings,Painting,Shepherd with a Flock of Sheep,,,,,,Artist,,Ilia Efimovich Repin,"Russian, Chuguev 1844–1930 Kuokkala",,"Repin, Ilia Efimovich",Russian,1844,1930,1870,1870,1870,Oil on canvas board,4 7/8 x 8 7/8 in. (12.4 x 22.5 cm),"Bequest of Mary Jane Dastich, in memory of her husband, General Frank Dastich, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.7,false,true,438952,European Paintings,Painting,View from the Citadel Ramparts in Copenhagen by Moonlight,,,,,,Artist,,Martinus Rørbye,"Danish, Drammen 1803–1848 Copenhagen",,"Rørbye, Martinus",Danish,1803,1848,1839,1839,1839,Oil on canvas,11 3/8 x 9 5/8 in. (28.9 x 24.4 cm),"Gift of Eugene V. Thaw, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.6,false,true,438951,European Paintings,Painting,An Evening beside Lake Arresø,,,,,,Artist,,Johan Thomas Lundbye,"Danish, Kalundborg 1818–1848 Bedsted",,"Lundbye, Johan Thomas",Danish,1818,1848,ca. 1837,1832,1842,Oil on canvas,9 x 11 3/4 in. (22.9 x 29.8 cm),"Gift of Eugene V. Thaw, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.4,false,true,438949,European Paintings,Painting,Columns of the Temple of Neptune at Paestum,,,,,,Artist,,Constantin Hansen,"Danish, Rome 1804–1880 Frederiksberg",,"Hansen, Constantin",Danish,1804,1880,1838,1838,1838,Oil on canvas,12 5/8 x 10 in. (32.1 x 25.4 cm),"Gift of Eugene V. Thaw, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.101,false,true,437046,European Paintings,Painting,"María Teresa (1638–1683), Infanta of Spain",,,,,,Artist,,Juan Bautista Martínez del Mazo,"Spanish, Cuenca ca. 1612–1667 Madrid",,"Mazo, Juan Bautista Martínez del",Spanish,1612,1667,ca. 1645,1640,1650,Oil on canvas,58 1/4 x 40 1/2 in. (148 x 102.9 cm),"Rogers Fund, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.99,false,true,437923,European Paintings,Painting,"A Day in October, near Waxholm, Sweden",,,,,,Artist,,Alfred Wahlberg,"Swedish, Stockholm 1834–1906 Tranås",,"Wahlberg, Alfred",Swedish,1834,1906,1873,1873,1873,Oil on canvas,41 x 64 1/2 in. (104.1 x 163.8 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.148,true,true,436819,European Paintings,Painting,Mäda Primavesi (1903–2000),,,,,,Artist,,Gustav Klimt,"Austrian, Baumgarten 1862–1918 Vienna",,"Klimt, Gustav",Austrian,1862,1918,1912–13,1912,1913,Oil on canvas,59 x 43 1/2 in. (149.9 x 110.5 cm),"Gift of André and Clara Mertens, in memory of her mother, Jenny Pulitzer Steiner, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.412,false,true,436820,European Paintings,Painting,Serena Pulitzer Lederer (1867–1943),,,,,,Artist,,Gustav Klimt,"Austrian, Baumgarten 1862–1918 Vienna",,"Klimt, Gustav",Austrian,1862,1918,1899,1899,1899,Oil on canvas,75 1/8 x 33 5/8 in. (190.8 x 85.4 cm),"Purchase, Wolfe Fund, and Rogers and Munsey Funds, Gift of Henry Walters, and Bequests of Catharine Lorillard Wolfe and Collis P. Huntington, by exchange, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.25,false,true,441768,European Paintings,Painting,"A Section of the Via Sacra, Rome (The Church of Saints Cosmas and Damian)",,,,,,Artist,,Christoffer Wilhelm Eckersberg,"Danish, Blåkrog 1783–1853 Copenhagen",,"Eckersberg, Christoffer Wilhelm",Danish,1783,1853,ca. 1814–15,1814,1815,Oil on canvas,12 3/8 x 17 1/8 in. (31.4 x 43.5 cm),"Nineteenth-Century, Modern, and Contemporary Funds, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.4.7,false,true,435599,European Paintings,Painting,Boatmen of Barcelona,,,,,,Artist,,Dionisio Baixeras y Verdaguer,"Spanish, Barcelona 1862–1943 Barcelona",,"Baixeras y Verdaguer, Dionisio",Spanish,1862,1943,1886,1886,1886,Oil on canvas,59 x 83 in. (149.9 x 210.8 cm),"Gift of George I. Seney, 1886",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.81,false,true,437707,European Paintings,Painting,"Mrs. Winthrop W. Aldrich (Harriet Alexander, 1888–1972)",,,,,,Artist,,Joaquín Sorolla y Bastida,"Spanish, Valencia 1863–1923 Cercedilla",,"Sorolla y Bastida, Joaquín",Spanish,1863,1923,1909,1909,1909,Oil on canvas,40 x 30 3/8 in. (101.6 x 77.2 cm),"Gift of Harriet Alexander Aldrich, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +09.71.2,false,true,437704,European Paintings,Painting,"The Bath, Jávea",,,,,,Artist,,Joaquín Sorolla y Bastida,"Spanish, Valencia 1863–1923 Cercedilla",,"Sorolla y Bastida, Joaquín",Spanish,1863,1923,1905,1905,1905,Oil on canvas,35 1/2 x 50 1/2 in. (90.2 x 128.3 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1909",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +09.71.3,false,true,437706,European Paintings,Painting,"Señora de Sorolla (Clotilde García del Castillo, 1865–1929) in Black",,,,,,Artist,,Joaquín Sorolla y Bastida,"Spanish, Valencia 1863–1923 Cercedilla",,"Sorolla y Bastida, Joaquín",Spanish,1863,1923,1906,1906,1906,Oil on canvas,73 1/2 x 46 3/4 in. (186.7 x 118.7 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1909",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.119.1,false,true,437705,European Paintings,Painting,"Castle of San Servando, Toledo",,,,,,Artist,,Joaquín Sorolla y Bastida,"Spanish, Valencia 1863–1923 Cercedilla",,"Sorolla y Bastida, Joaquín",Spanish,1863,1923,1906,1906,1906,Oil on canvas,26 1/4 x 36 1/2 in. (66.7 x 92.7 cm),"Gift of Archer M. Huntington, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.233,false,true,436825,European Paintings,Painting,"Valdemar Hjartvar Købke (1813–1893), the Artist's Brother",,,,,,Artist,,Christen Købke,"Danish, Copenhagen 1810–1848 Copenhagen",,"Købke, Christen",Danish,1810,1848,ca. 1838,1833,1843,Oil on canvas,21 1/8 x 18 1/4 in. (53.7 x 46.4 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436825,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.203,false,true,441933,European Paintings,Painting,"Moonlight, Strandgade 30",,,,,,Artist,,Vilhelm Hammershøi,"Danish, Copenhagen 1864–1916 Copenhagen",,"Hammershøi, Vilhelm",Danish,1864,1916,1900–1906,1900,1906,Oil on canvas,16 1/8 x 20 1/8 in. (41 x 51.1 cm),"Purchase, European Paintings Funds, and Annette de la Renta Gift, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.1,false,true,435806,European Paintings,Painting,A Peasant Woman Picking Fleas off a Dog,,,,,,Artist,,Adriaen Brouwer,"Flemish, Oudenaarde 1605/6–1638 Antwerp",,"Brouwer, Adriaen",Flemish,1605,1638,ca. 1626–27,1626,1627,Oil on wood,Oval 7 1/8 x 5 3/8 in. (18.1 x 13.7 cm); set in rectangular panel 8 x 6 1/4 in. (20.3 x 15.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.21,false,true,435807,European Paintings,Painting,The Smokers,,,,,,Artist,,Adriaen Brouwer,"Flemish, Oudenaarde 1605/6–1638 Antwerp",,"Brouwer, Adriaen",Flemish,1605,1638,ca. 1636,1631,1638,Oil on wood,18 1/4 x 14 1/2 in. (46.4 x 36.8 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435807,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.289,false,true,436541,European Paintings,Painting,Sebastián Martínez y Pérez (1747–1800),,,,,,Artist,,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,1792,1792,1792,Oil on canvas,36 5/8 x 26 5/8 in. (93 x 67.6 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.259,false,true,436546,European Paintings,Painting,"José Costa y Bonells (died l870), Called Pepito",,,,,,Artist,,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,ca. 1810,1805,1815,Oil on canvas,41 3/8 x 33 1/4 in. (105.1 x 84.5 cm),"Gift of Countess Bismarck, 1961",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.41,true,true,436545,European Paintings,Painting,Manuel Osorio Manrique de Zuñiga (1784–1792),,,,,,Artist,,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,1787–88,1787,1788,Oil on canvas,50 x 40 in. (127 x 101.6 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.145.1,false,true,436542,European Paintings,Painting,"Ignacio Garcini y Queralt (1752–1825), Brigadier of Engineers",,,,,,Artist,,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,1804,1804,1804,Oil on canvas,41 x 32 3/4 in. (104.1 x 83.2 cm),"Bequest of Harry Payne Bingham, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.145.2,false,true,436543,European Paintings,Painting,Josefa de Castilla Portugal y van Asbrock de Garcini (1775–about 1850),,,,,,Artist,,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,1804,1804,1804,Oil on canvas,41 x 32 3/8 in. (104.1 x 82.2 cm),"Bequest of Harry Payne Bingham, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.10,false,true,436548,European Paintings,Painting,Majas on a Balcony,,,,,,Artist,Attributed to,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,ca. 1800–1810,1800,1810,Oil on canvas,76 3/4 x 49 1/2in. (194.9 x 125.7cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.242,false,true,436544,European Paintings,Painting,"Tiburcio Pérez y Cuervo (1785/86–1841), the Architect",,,,,,Artist,,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,1820,1820,1820,Oil on canvas,40 1/4 x 32 in. (102.2 x 81.3 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.3,false,true,437298,European Paintings,Painting,The Presentation in the Temple,,,,,,Artist,,Alvaro Pirez,"Portuguese, Évora, active 1411–34 Italy",,"Pirez, Alvaro",Portuguese,1411,1434,probably ca. 1430,1425,1435,Tempera and gold on wood,13 3/8 x 15 7/8 in. (34 x 40.3 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.94.2,false,true,437750,European Paintings,Painting,A Renaissance Portico with Elegant Figures,,,,,,Artist,,Hendrick van Steenwijck II,"Flemish, Antwerp (?) ca. 1580–1649 Leiden",,"Steenwijck, Hendrick van, II",Flemish,1475,1649,ca. 1615,1610,1620,Oil on copper,Diameter 4 3/8 in. (11.1 cm),"Gift of Mrs. James Eads Switzer, in memory of her aunt, Yrene Ceballos de Sanz, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.452,false,true,436817,European Paintings,Painting,A Mountainous Landscape with a Waterfall,,,,,,Artist,,Kerstiaen de Keuninck,"Flemish, Kortrijk ca. 1560–1632/33 Antwerp",,"Keuninck, Kerstiaen de",Flemish,1560,1633,ca. 1600,1595,1605,Oil on wood,27 1/4 x 48 in. (69.2 x 121.9 cm),"Purchase, Anonymous Gift, L. H. P. Klotz and George T. Delacorte Jr. Gifts; Rogers, Marquand, Charles B. Curtis, and The Alfred N. Punnett Endowment Funds; and Gift of Eugen Boross and Bequest of Collis P. Huntington, by exchange, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.100,false,true,436833,European Paintings,Painting,Red Sunset on the Dnieper,,,,,,Artist,,Arkhip Ivanovich Kuindzhi,"Russian, Mariupol 1842–1910 St. Petersburg",,"Kuindzhi, Arkhip Ivanovich",Russian,1842,1910,1905–8,1905,1908,Oil on canvas,53 x 74 in. (134.6 x 188 cm),"Rogers Fund, 1974",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.5,false,true,437321,European Paintings,Painting,Christ and the Woman Taken in Adultery,,,,,,Artist,,Vasilii Dmitriviech Polenov,"Russian, St. Petersburg 1844–1927 Polenovo",,"Polenov, Vasilii Dmitrievich",Russian,1844,1927,1884,1884,1884,Oil on canvas,9 1/2 x 17 in. (24.1 x 43.2 cm),"Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.149,false,true,641257,European Paintings,Painting,Head of Christ,,,,,,Artist,,Fernando Yáñez de la Almedina,"Spanish, Almedina, ca. 1475?–1536 Valencia",,"Yáñez de la Almedina, Fernando",Spanish,1470,1536,ca. 1506,1501,1511,Oil on poplar,16 1/2 × 12 in. (41.9 × 30.5 cm),"Purchase, The Morris and Alma Schapiro Fund Gift, and Bequest of George D. Pratt, by exchange, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/641257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.104,false,true,437969,European Paintings,"Painting, part of an altarpiece",The Battle between Christians and Moors at El Sotillo,,,,,,Artist,,Francisco de Zurbarán,"Spanish, Fuente de Cantos 1598–1664 Madrid",,"Zurbarán, Francisco de",Spanish,1598,1664,ca. 1637–39,1637,1639,Oil on canvas,"Arched top, 131 7/8 x 75 1/4 in. (335 x 191.1 cm)","Kretschmar Fund, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.137,true,true,437971,European Paintings,Painting,The Young Virgin,,,,,,Artist,,Francisco de Zurbarán,"Spanish, Fuente de Cantos 1598–1664 Madrid",,"Zurbarán, Francisco de",Spanish,1598,1664,ca. 1632–33,1632,1633,Oil on canvas,46 x 37 in. (116.8 x 94 cm),"Fletcher Fund, 1927",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.21,false,true,437970,European Paintings,Painting,Saint Benedict,,,,,,Artist,,Francisco de Zurbarán,"Spanish, Fuente de Cantos 1598–1664 Madrid",,"Zurbarán, Francisco de",Spanish,1598,1664,ca. 1640–45,1640,1645,Oil on canvas,74 x 40 3/4 in. (188 x 103.5 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.90,true,true,435683,European Paintings,Painting,Island of the Dead,,,,,,Artist,,Arnold Böcklin,"Swiss, Basel 1827–1901 San Domenico, Italy",,"Böcklin, Arnold",Swiss,1827,1901,1880,1880,1880,Oil on wood,29 x 48 in. (73.7 x 121.9 cm),"Reisinger Fund, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.100,false,true,435684,European Paintings,Painting,Roman Landscape,,,,,,Artist,Attributed to,Arnold Böcklin,"Swiss, Basel 1827–1901 San Domenico, Italy",,"Böcklin, Arnold",Swiss,1827,1901,ca. 1850–52,1850,1852,"Oil on canvas, several pieces joined",12 1/2 x 18 1/8 in. (31.8 x 46 cm),"Gift of Fearon Galleries Inc., 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +34.83.2,false,true,436861,European Paintings,Painting,A Masked Ball in Bohemia,,,,,,Artist,Attributed to,Andreas Altomonte,"Austrian, Warsaw or Vienna 1699–1780 Vienna",,"Altomonte, Andreas",Austrian,1699,1780,ca. 1748,1743,1753,Oil on canvas,19 x 38 in. (48.3 x 96.5 cm),"Bequest of Mariana Griswold Van Rensselaer, 1934",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.56,false,true,437179,European Paintings,Painting,"Near Penshurst, Kent",,,,,,Artist,,Patrick Nasmyth,"British, Edinburgh, Scotland 1787–1831 London",,"Nasmyth, Patrick","British, Scottish",1787,1831,1828,1828,1828,Oil on wood,27 1/2 x 36 1/4 in. (69.9 x 92.1 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +05.42,false,true,436570,European Paintings,Painting,The Adoration of the Shepherds,,,,,,Artist,,El Greco (Domenikos Theotokopoulos),"Greek, Iráklion (Candia) 1540/41–1614 Toledo",,"Greco, El (Domenikos Theotokopoulos)",Greek,1540,1614,ca. 1605–10,1605,1610,Oil on canvas,56 7/8 x 39 7/8 in. (144.5 x 101.3 cm); with added strips 64 1/2 x 42 in. (163.8 x 106.7 cm),"Rogers Fund, 1905",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436570,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.48,false,true,436576,European Paintings,Painting,The Vision of Saint John,,,,,,Artist,,El Greco (Domenikos Theotokopoulos),"Greek, Iráklion (Candia) 1540/41–1614 Toledo",,"Greco, El (Domenikos Theotokopoulos)",Greek,1540,1614,ca. 1609–14,1609,1614,Oil on canvas,87 1/2 x 76in. (222.3 x 193cm); with added strips 88 1/2 x 78 1/2 in. (224.8 x 199.4 cm) [top truncated],"Rogers Fund, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436576,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.416,false,true,436572,European Paintings,Painting,Christ Healing the Blind,,,,,,Artist,,El Greco (Domenikos Theotokopoulos),"Greek, Iráklion (Candia) 1540/41–1614 Toledo",,"Greco, El (Domenikos Theotokopoulos)",Greek,1540,1614,ca. 1570,1565,1575,Oil on canvas,47 x 57 1/2 in. (119.4 x 146.1 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.197.1,false,true,436574,European Paintings,Painting,Portrait of an Old Man,,,,,,Artist,,El Greco (Domenikos Theotokopoulos),"Greek, Iráklion (Candia) 1540/41–1614 Toledo",,"Greco, El (Domenikos Theotokopoulos)",Greek,1540,1614,ca. 1595–1600,1595,1600,Oil on canvas,20 3/4 x 18 3/8 in. (52.7 x 46.7 cm),"Purchase, Joseph Pulitzer Bequest, 1924",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436574,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.5,true,true,436573,European Paintings,Painting,Cardinal Fernando Niño de Guevara (1541–1609),,,,,,Artist,,El Greco (Domenikos Theotokopoulos),"Greek, Iráklion (Candia) 1540/41–1614 Toledo",,"Greco, El (Domenikos Theotokopoulos)",Greek,1540,1614,ca. 1600,1595,1605,Oil on canvas,67 1/4 x 42 1/2in. (170.8 x 108cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.6,true,true,436575,European Paintings,Painting,View of Toledo,,,,,,Artist,,El Greco (Domenikos Theotokopoulos),"Greek, Iráklion (Candia) 1540/41–1614 Toledo",,"Greco, El (Domenikos Theotokopoulos)",Greek,1540,1614,ca. 1598–99,1598,1599,Oil on canvas,47 3/4 x 42 3/4 in. (121.3 x 108.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436575,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.190.17,false,true,436571,European Paintings,Painting,The Adoration of the Shepherds,,,,,,Artist,,El Greco (Domenikos Theotokopoulos) and Workshop,"Greek, Iráklion (Candia) 1540/41–1614 Toledo",,,Greek,1540,1614,ca. 1612–14,1612,1614,Oil on canvas,43 1/2 x 25 5/8 in. (110.5 x 65.1 cm),"Bequest of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436571,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.70,false,true,437217,European Paintings,Painting,Moses and Aaron before Pharaoh: An Allegory of the Dinteville Family,,,,,,Artist,,Master of the Dinteville Allegory,"Netherlandish or French, active mid-16th century",,Master of the Dinteville Allegory,Netherlandish/French,1525,1575,1537,1537,1537,Oil on wood,69 1/2 x 75 7/8 in. (176.5 x 192.7 cm),"Wentworth Fund, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.52,false,true,437939,European Paintings,Painting,The Highland Family,,,,,,Artist,,Sir David Wilkie,"British, Cults, Scotland 1785–1841 off Gibraltar",,"Wilkie, David, Sir","British, Scottish",1785,1841,1824,1824,1824,Oil on wood,24 x 36 in. (61 x 91.4 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437939,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.26.1,false,true,436821,European Paintings,Painting,Girl Building a House of Cards,,,,,,Artist,Attributed to,Thomas Frye,"Irish, Dublin, born ca. 1711–12, died 1762 London",,"Frye, Thomas",Irish,1711,1762,mid-18th century,1731,1762,Oil on canvas,30 1/8 x 25 1/4 in. (76.5 x 64.1 cm),"Marquand Collection, Gift of Henry G. Marquand, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.63,false,true,435586,European Paintings,Painting,Ivan Rodin,,,,,,Artist,,Abram Efimovich Arkhipov,"Russian, Egorovo Riazan province 1862–1930 Moscow",,"Arkhipov, Abram Efimovich",Russian,1862,1930,1928,1928,1928,Oil on canvas,44 x 34 1/4 in. (111.8 x 87 cm),"Gift of George D. Pratt, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.28,false,true,439117,European Paintings,"Painting, grisaille",The Glorification of the Royal Hungarian Saints,,,,,,Artist,,Franz Anton Maulbertsch,"Austrian, Langenargen am Bodensee 1724–1796 Vienna",,"Maulbertsch, Franz Anton",Austrian,1724,1796,ca. 1772–73,1772,1773,Oil on canvas,27 1/2 x 19 7/8 in. (70 x 50.5 cm),"Purchase, Friends of European Paintings Gifts, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.420,false,true,439844,European Paintings,Painting,Heroic Landscape with Rainbow,,,,,,Artist,,Joseph Anton Koch,"Austrian, Obergibeln bei Elbigenalp 1768–1839 Rome",,"Koch, Joseph Anton",Austrian,1768,1839,1824,1824,1824,Oil on canvas,42 3/4 x 37 3/4 in. (108.6 x 95.9 cm),"Purchase, Anne Cox Chambers Gift, Gift of Alfred and Katrin Romney, by exchange, and Nineteenth-Century, Modern, and Contemporary Art Funds, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +76.10,false,true,435572,European Paintings,"Painting, part of an altarpiece",Saint Giles with Christ Triumphant over Satan and the Mission of the Apostles,,,,,,Artist,,Miguel Alcañiz (or Miquel Alcanyís),"Spanish, Valencian, active by 1408–died after 1447",,"Alcañiz, Miguel (or Miquel Alcanyís)",Spanish,1408,1447,ca. 1408,1403,1413,"Tempera on wood, gold ground","Overall 59 5/8 x 39 1/2 in. (151.4 x 100.3 cm); upper left panel, painted surface 24 1/8 x 16 7/8 in. (61.3 x 42.9 cm); lower left panel, painted surface 24 5/8 x 16 7/8 in. (62.5 x 42.9 cm); right panel, painted surface 46 1/8 x 16 7/8 in. (117.2 x 42.9 cm)","Gift of J. Bruyn Andrews, 1876",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435572,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +05.32.3,false,true,437941,European Paintings,Painting,Lake Nemi and Genzano from the Terrace of the Capuchin Monastery,,,,,,Artist,,Richard Wilson,"British, Penegoes, Wales 1712/13–1782 Denbighshire, Wales",,"Wilson, Richard","British, Welsh",1712,1782,ca. 1756–57,1756,1757,Oil on canvas,16 7/8 x 21 1/8 in. (42.9 x 53.7 cm),"Gift of George A. Hearn, 1905",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.142,false,true,437355,European Paintings,Painting,George Harley Drummond (1783–1855),,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,ca. 1808–9,1808,1809,Oil on canvas,94 1/4 x 58 in. (239.4 x 147.3 cm),"Gift of Mrs. Guy Fairfax Cary, in memory of her mother, Mrs. Burke Roche, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437355,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.59.2,false,true,437364,European Paintings,Painting,William Scott-Elliot of Arkleton (1811–1901),,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,ca. 1815–16,1815,1816,Oil on canvas,47 3/8 x 36 5/8 in. (120.3 x 93 cm),"Fletcher Fund, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437364,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.30.5,false,true,437363,European Paintings,Painting,William Forsyth (1749–1814),,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,ca. 1800,1795,1805,Oil on canvas,30 x 24 7/8 in. (76.2 x 63.2 cm),"Gift of Arthur H. Hearn, 1896",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437363,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.305,false,true,437361,European Paintings,Painting,"Mrs. Richard Alexander Oswald (Louisa Johnston, ?born about 1760, died 1797)",,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,ca. 1794,1789,1798,Oil on canvas,48 1/2 x 40 7/8 in. (123.2 x 103.8 cm),"Gift of Mrs. Paul Moore, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437361,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.31,false,true,437354,European Paintings,Painting,The Drummond Children,,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,ca. 1808–9,1808,1809,Oil on canvas,94 1/4 x 60 1/4 in. (239.4 x 153 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437354,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.32,false,true,437365,European Paintings,Painting,"William Robertson (1753–1835), Lord Robertson",,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,1805,1805,1805,Oil on canvas,49 1/2 x 39 1/4 in. (125.7 x 99.7 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437365,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.119,false,true,436611,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,Style of,Peter Adolf Hall,ca. 1780,,"Hall, Peter Adolf",Swedish,1739,1793,,1775,1785,Ivory,Diameter 2 in. (52 mm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.137,false,true,436613,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,Style of,Peter Adolf Hall,ca. 1780,,"Hall, Peter Adolf",Swedish,1739,1793,,1775,1785,Ivory,Diameter 2 3/8 in. (62 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.508,false,true,437459,European Paintings,"Painting, miniature","John Churchill (1650–1722), First Duke of Marlborough",,,,,,Artist,,Christian Richter,"Swedish, 1678–1732",,"Richter, Christian",Swedish,1678,1732,,1698,1732,Vellum,"Oval, 3 1/4 x 2 5/8 in. (82 x 67 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437459,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.103,false,true,435588,European Paintings,"Painting, miniature","A Young Knight of the Garter, Possibly George Augustus (1683–1760), Later George II of Great Britain and Ireland",,,,,,Artist,Style of,Benjamin Arlaud,"Continental, ca. 1706",,"Arlaud, Benjamin",Swiss,1701,1717,,1701,1711,Vellum,"Oval, 2 x 1 3/4 in. (52 x 45 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.104,false,true,435589,European Paintings,"Painting, miniature","A Woman, Possibly Sophia Dorothea (1687–1757), Later Queen of Prussia",,,,,,Artist,Style of,Benjamin Arlaud,"Continental, ca. 1706",,"Arlaud, Benjamin",Swiss,1701,1717,,1701,1711,Vellum,"Oval, 2 x 1 3/4 in. (52 x 45 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.17,false,true,435758,European Paintings,"Painting, miniature",A Woman Playing a Harp,,,,,,Artist,,Joseph Marie Bouton,"French (?), 1768–1823",,"Bouton, Joseph Marie",French(?),1768,1823,,1788,1823,Ivory,Diameter 3 1/2 in. (90 mm),"Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435758,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.74,false,true,437626,European Paintings,"Painting, miniature",Putti Harvesting Wheat,,,,,,Artist,Style of,Piat Joseph Sauvage,"French, late 18th century",,"Sauvage, Piat Joseph",Flemish,1744,1818,,1770,1799,Oil on wood,Painted surface diameter 3 in. (76 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.107.1,false,true,435587,European Paintings,"Painting, miniature","Caroline of Ansbach (1683–1737), Consort of George II of Great Britain and Ireland",,,,,,Artist,Attributed to,Benjamin Arlaud,"Swiss, active ca. 1701–17",,"Arlaud, Benjamin",Swiss,1701,1717,,1701,1717,Ivory,"Oval, 2 5/8 x 2 1/4 in. (68 x 57 mm)","Gift of Estate of Isaac A. Josephi, 1955",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.106,false,true,436234,European Paintings,"Painting, miniature",Portrait of an Officer,,,,,,Artist,,Andrew Dunn,"Irish, active ca. 1800–1820",,"Dunn, Andrew",Irish,1800,1820,,1800,1820,Ivory,"Oval, 2 5/8 x 2 1/8 in. (67 x 55 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +18.73,false,true,436242,European Paintings,"Painting, miniature",Margaret Rieche Richard,,,,,,Artist,,Dupuy,"German (?), active ca. 1801–17",,Dupuy,German (?),1801,1817,,1801,1817,Enamel,Diameter 2 1/8 in. (54 mm),"Bequest of Georgiana Emily Reynolds, 1918",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436242,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.71,false,true,437278,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,Attributed to,Jean Petitot,"Swiss, Geneva 1607–1691 Geneva",,"Petitot, Jean",Swiss,1607,1691,,1627,1691,Vellum,"Oval, 1 3/8 x 1 1/8 in. (36 x 30 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.80,false,true,436995,European Paintings,Painting,Beggars at a Doorway,,,,,,Artist,,"Master of the Béguins, French or Flemish, active 1650–60 (possibly Abraham Willemsens, Flemish, active by 1627, died 1672)",,,"Master of the Béguins (possibly Abraham Willemsens, Flemish, act",French/Flemish,1627,1672,,1650,1660,Oil on canvas,20 1/4 x 23 3/8 in. (51.4 x 59.4 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.14,false,true,437554,European Paintings,"Painting, icon",The Annunciation,,,,,,Artist,,"Russian Painter, second half 16th century",,,"Russian Painter, second half 16th century",Russian,1550,1599,,1550,1599,Tempera on wood,13 x 10 3/8 in. (33 x 26.4 cm),"Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.9,false,true,436874,European Paintings,Painting,"Barbara Villiers (1640–1709), Duchess of Cleveland",,,,,,Artist,Workshop of,Sir Peter Lely,"British, after 1670",,"Lely, Peter, Sir","Dutch, British",1618,1680,,1670,1727,Oil on canvas,89 x 54 in. (226.1 x 137.2 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.11,false,true,436552,European Paintings,Painting,"María Luisa of Parma (1751–1819), Queen of Spain",,,,,,Artist,Copy after,Goya,"Spanish, after 1800",,Goya,Spanish,1746,1828,,1800,1907,Oil on canvas,43 1/2 x 33 1/2 in. (110.5 x 85.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436552,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.1,false,true,436048,European Paintings,Painting,The Meeting of Alexander the Great and Diogenes,,,,,,Artist,,Gaspar de Crayer,"Flemish, 1584–1669",,"Crayer, Gaspar de",Flemish,1584,1669,,1605,1669,Oil on canvas,"88 3/4 x 127 5/8 in. (225.4 x 324.2 cm), including added strips of 13 1/2 in. (34.3 cm) at left and 15 1/2 in. (39.4 cm) at right","Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.43,false,true,436424,European Paintings,"Painting, possibly an overdoor",A Basket and Birds,,,,,,Artist,,Jan Fyt,"Flemish, 1611–1661",,"Fyt, Jan",Flemish,1611,1661,,1631,1661,Oil on canvas,23 3/4 x 30 1/4 in. (60.3 x 76.8 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.44,false,true,436425,European Paintings,"Painting, possibly an overdoor",A Hare and Birds,,,,,,Artist,,Jan Fyt,"Flemish, 1611–1661",,"Fyt, Jan",Flemish,1611,1661,,1631,1661,Oil on canvas,23 7/8 x 31 in. (60.6 x 78.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.141,false,true,436426,European Paintings,Painting,"A Hare, Partridges, and Fruit",,,,,,Artist,,Jan Fyt,"Flemish, 1611–1661",,"Fyt, Jan",Flemish,1611,1661,,1611,1661,Oil on canvas,37 1/2 x 43 1/2 in. (95.3 x 110.5 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.12,false,true,437592,European Paintings,Painting,The Yard of the Inn at Emmaus,,,,,,Artist,,David Ryckaert III,"Flemish, 1612–1661",,"Ryckaert, David, III",Flemish,1612,1661,,1632,1661,Oil on canvas,35 5/8 x 45 3/8 in. (90.5 x 115.3 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.13,false,true,437591,European Paintings,Painting,Rustic Interior,,,,,,Artist,,David Ryckaert III,"Flemish, 1612–1661",,"Ryckaert, David, III",Flemish,1612,1661,,1632,1661,Oil on canvas,36 3/8 x 45 5/8 in. (92.4 x 115.9 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.118,false,true,436752,European Paintings,Painting,Pomegranates and Other Fruit in a Landscape,,,,,,Artist,,Abraham Brueghel,"Flemish, 1631–1697",,"Brueghel, Abraham",Flemish,1631,1697,,1650,1674,Oil on canvas,24 3/8 x 29 1/8 in. (61.9 x 74 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.93,false,true,436118,European Paintings,Painting,The Forge,,,,,,Artist,,Léonard Defrance,"Flemish, 1735–1805",,"Defrance, Léonard",Flemish,1735,1805,,1755,1805,Oil on wood,12 5/8 x 16 1/2 in. (32.1 x 41.9 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.105,false,true,436119,European Paintings,Painting,The Rope Dance,,,,,,Artist,,Léonard Defrance,"Flemish, 1735–1805",,"Defrance, Léonard",Flemish,1735,1805,,1755,1805,Oil on wood,19 7/8 x 28 5/8 in. (50.5 x 72.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.3,false,true,437697,European Paintings,Painting,Soldiers Bivouacking,,,,,,Artist,,Pieter Snayers,"Flemish, 1592–?1667",,"Snayers, Pieter",Flemish,1592,1667,,1612,1667,Oil on wood,28 5/8 x 41 1/8 in. (72.7 x 104.5 cm),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.23.1,false,true,436265,European Paintings,"Painting, monochrome",Sir Peter Paul Rubens (1577–1640),,,,,,Artist,Copy after,Anthony van Dyck,"Flemish, 17th century",,"Dyck, Anthony van",Flemish,1599,1641,,1600,1699,Oil on wood,10 x 7 5/8 in. (25.4 x 19.4 cm),"Bequest of Bertha H. Buswell, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.2,false,true,435808,European Paintings,Painting,The Brawl,,,,,,Artist,Copy after,Adriaen Brouwer,"Flemish, 17th century",,"Brouwer, Adriaen",Flemish,1605,1638,,1600,1699,Oil on wood,9 5/8 x 7 1/2 in. (24.4 x 19.1 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435808,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.128.13,false,true,437766,European Paintings,Painting,Ferdinando II de' Medici (1610–1670) as a Boy,,,,,,Artist,Copy after,Justus Sustermans,"Flemish, 17th century",,"Sustermans, Justus",Flemish,1597,1681,,1600,1699,Oil on canvas,51 7/8 x 40 1/2 in. (131.8 x 102.9 cm),"Bequest of Helen Hay Whitney, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437766,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.12,false,true,436553,European Paintings,Painting,A City on a Rock,,,,,,Artist,Style of,Goya,"Spanish, 19th century",,Goya,Spanish,1746,1828,,1800,1899,Oil on canvas,33 x 41 in. (83.8 x 104.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436553,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +00.16,false,true,437539,European Paintings,Painting,Cambyses Appointing Otanes Judge,,,,,,Artist,Copy after,Peter Paul Rubens,probably 18th century,,"Rubens, Peter Paul",Flemish,1577,1640,,1700,1799,Oil on wood,18 x 17 1/2 in. (45.7 x 44.5 cm),"Gift of William E. Dodge, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.12,false,true,436992,European Paintings,Painting,The Sense of Sight,,,,,,Artist,,Juan Dò,"Spanish, 1604?–?1656",,"Dò, Juan",Spanish,1604,1656,,1625,1649,Oil on canvas,"Overall, with added strips, 29 7/8 x 24 7/8 in. (75.9 x 63.2 cm); without additions 27 3/4 x 21 3/4 in. (70.5 x 55.2 cm)","The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436992,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.19,false,true,436549,European Paintings,Painting,Don Bernardo de Iriarte (1735–1814),,,,,,Artist,Copy after,Goya,"Spanish, 1797 or later",,Goya,Spanish,1746,1828,,1797,1797,Oil on canvas,42 1/2 x 33 1/2 in. (108 x 85.1 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.18,false,true,437600,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Alonzo Sánchez Coello,"Spanish, 1531/32–1588",,"Sánchez Coello, Alonzo",Spanish,1531,1588,,1551,1588,Oil on canvas,38 3/4 x 28 3/8 in. (98.4 x 72.1 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.79.17,false,true,437100,European Paintings,"Painting, icon",The Dormition of the Virgin,,,,,,Artist,,Ioannes Mokos,"Greek, active 1680–1724",,"Mokos, Ioannes",Greek,1680,1724,,1680,1724,"Tempera and oil on wood, gold ground",13 1/2 x 11 1/4 in. (34.3 x 28.6 cm),"Gift of Mrs. Henry Morgenthau, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437100,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.498,false,true,436925,European Paintings,Painting,"Laure de Sade, Comtesse Adhéaume de Chevigné",,,,,,Artist,,Federico de Madrazo y de Ochoa,"Spanish, Paris 1875–1934",,"Madrazo y de Ochoa, Federico de",Spanish,1875,1934,,1895,1934,Oil on canvas,45 1/2 x 29 in. (115.6 x 73.7 cm),"Anonymous Gift, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.158.746,false,true,437855,European Paintings,"Painting, icon",Christ Bearing the Cross,,,,,,Artist,,Nicolaos Tzafouris,"Greek, ca. 1455–1500/1501",,"Tzafouris, Nicolaos",Greek,1455,1501,,1489,1500,"Oil and tempera on wood, gold ground",27 1/4 x 21 1/2 in. (69.2 x 54.6 cm),"Bashford Dean Memorial Collection, Funds from various donors, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.365.1,false,true,436990,European Paintings,Painting,The Martyrdom of Saint Lawrence; (reverse) Giving Drink to the Thirsty,,,,,,Artist,,Master of the Acts of Mercy,"Austrian, Salzburg, ca. 1465",,Master of the Acts of Mercy,Austrian,1460,1470,,1460,1470,"Oil on fir, (obverse) gold ground",Painted surface 29 x 18 3/8 in. (73.7 x 46.7 cm),"Gift of The Jack and Belle Linsky Foundation, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.32,false,true,437817,European Paintings,Painting,Group Portrait: A Wedding Celebration,,,,,,Artist,,Gillis van Tilborgh,"Flemish, ca. 1625–ca. 1678",,"Tilborgh, Gillis van",Flemish,1625,1678,,1645,1678,Oil on canvas,45 1/2 x 63 1/4 in. (115.6 x 160.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +88.3.82,false,true,437232,European Paintings,Painting,Saint Anne Enthroned with the Virgin and Child,,,,,,Artist,,Osma Master,"Spanish, Castilian, ca. 1500",,Osma Master,Spanish,1500,1500,,1495,1505,Tempera and gold on wood,59 x 32 in. (149.9 x 81.3 cm),"Gift of Coudert Brothers, 1888",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437232,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.145.1,false,true,435822,European Paintings,Painting,The Annunciation,,,,,,Artist,,Budapest Master,"Spanish, Castilian, ca. 1500",,Budapest Master,Spanish,1500,1500,,1495,1505,"Oil and gold on canvas, transferred from wood",32 x 20 1/4 in. (81.3 x 51.4 cm),"Bequest of Muriel Stokes, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.243,false,true,436550,European Paintings,Painting,Infanta María Luisa (1782–1824) and Her Son Carlos Luis (1799–1883),,,,,,Artist,Copy after,Goya,"Spanish, 1800 or shortly after",,Goya,Spanish,1746,1828,,1800,1805,Oil on canvas,39 1/8 x 27 in. (99.4 x 68.6 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436550,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.3,false,true,436871,European Paintings,Painting,"Mary Capel (1630–1715), Later Duchess of Beaufort, and Her Sister Elizabeth (1633–1678), Countess of Carnarvon",,,,,,Artist,,Sir Peter Lely (Pieter van der Faes),"British, Soest 1618–1680 London",,"Lely, Peter, Sir (Pieter van der Faes)","Dutch, British",1618,1680,,1652,1662,Oil on canvas,51 1/4 x 67 in. (130.2 x 170.2 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.6,false,true,436873,European Paintings,Painting,Sir Henry Capel (1638–1696),,,,,,Artist,,Sir Peter Lely (Pieter van der Faes),"British, Soest 1618–1680 London",,"Lely, Peter, Sir (Pieter van der Faes)","Dutch, British",1618,1680,,1654,1664,Oil on canvas,49 3/4 x 40 1/2 in. (126.4 x 102.9 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.79.14,false,true,437856,European Paintings,"Painting, icon",Head of Christ,,,,,,Artist,,Emmanuel Tzanès,"Greek, active by 1636–died 1690",,"Tzanès, Emmanuel",Greek,1636,1690,,1636,1690,"Tempera on wood, gold ground",8 3/8 x 7 1/8 in. (21.3 x 18.1 cm),"Gift of Mrs. Henry Morgenthau, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.79.15,false,true,437858,European Paintings,Painting,Head of the Virgin,,,,,,Artist,,Emmanuel Tzanès,"Greek, active by 1636–died 1690",,"Tzanès, Emmanuel",Greek,1636,1690,,1636,1690,"Tempera on wood, gold ground",8 3/8 x 7 1/8 in. (21.3 x 18.1 cm),"Gift of Mrs. Henry Morgenthau, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.79.18,false,true,437857,European Paintings,Painting,Head of Saint John the Baptist,,,,,,Artist,,Emmanuel Tzanès,"Greek, active by 1636–died 1690",,"Tzanès, Emmanuel",Greek,1636,1690,,1636,1690,"Tempera on wood, gold ground",8 3/8 x 7 1/8 in. (21.3 x 18.1 cm),"Gift of Mrs. Henry Morgenthau, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.71,false,true,437461,European Paintings,Painting,On the Seine,,,,,,Artist,,Martín Rico y Ortega,"Spanish, Madrid 1833–1908 Venice",,"Rico y Ortega, Martín",Spanish,1833,1908,,1853,1908,Oil on canvas,15 1/4 x 25 1/2 in. (38.7 x 64.8 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437461,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.666,false,true,437462,European Paintings,Painting,A Spanish Garden,,,,,,Artist,,Martín Rico y Ortega,"Spanish, Madrid 1833–1908 Venice",,"Rico y Ortega, Martín",Spanish,1833,1908,,1853,1908,Oil on canvas,24 x 15 1/4 in. (61 x 38.7 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437462,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.58,false,true,437045,European Paintings,Painting,The Last Token: A Christian Martyr,,,,,,Artist,,Gabriel Max,"Austrian, Prague 1840–1915 Munich",,"Max, Gabriel",Austrian,1840,1915,,1860,1915,Oil on canvas,67 1/2 x 47 in. (171.5 x 119.4 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.103.3,false,true,437279,European Paintings,Painting,Market Scene,,,,,,Artist,,August Xaver Karl von Pettenkofen,"Austrian, Vienna 1821–1889 Vienna",,"Pettenkofen, August Xaver Karl von",Austrian,1821,1889,,1841,1889,Oil on wood,4 x 8 1/2 in. (10.2 x 21.6 cm),"The John Hobart Warren Bequest, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.96,false,true,437074,European Paintings,Painting,A Cavalry Engagement,,,,,,Artist,,Adam Frans van der Meulen,"Flemish, Brussels 1632–1690 Paris",,"Meulen, Adam Frans van der",Flemish,1632,1690,,1652,1690,Oil on wood,8 5/8 x 12 1/2 in. (21.9 x 31.8 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437074,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.26.4,false,true,437543,European Paintings,Painting,Susanna and the Elders,,,,,,Artist,Workshop of,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,,1597,1640,Oil on wood,18 1/4 x 25 3/8 in. (46.4 x 64.5 cm),"Marquand Collection, Gift of Henry G. Marquand, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.19,false,true,437542,European Paintings,Painting,Saint Teresa of Ávila Interceding for Souls in Purgatory,,,,,,Artist,Workshop of,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,,1597,1640,Oil on wood,25 1/4 x 19 1/4 in. (64.1 x 48.9 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.37,false,true,437540,European Paintings,Painting,Frans Francken I (1542–1616),,,,,,Artist,Workshop of,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,,1597,1640,Oil on wood,25 1/4 x 19 1/8 in. (64.1 x 48.6 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.42,false,true,437544,European Paintings,Painting,Virgin and Child,,,,,,Artist,Workshop of,Peter Paul Rubens,"Flemish, Siegen 1577–1640 Antwerp",,"Rubens, Peter Paul",Flemish,1577,1640,,1597,1640,Oil on wood,39 3/4 x 30 3/8 in. (101 x 77.2 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.162.2,false,true,436971,European Paintings,Painting,The Flood Gate,,,,,,Artist,,Émile van Marcke,"French, Sèvres 1827–1890 Hyères",,"Marcke, Émile van",Belgian,1827,1890,,1847,1890,Oil on canvas,24 1/2 x 32 1/2 in. (62.2 x 82.6 cm),"Bequest of Susan P. Colgate, in memory of her husband, Romulus R. Colgate, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.18,false,true,437872,European Paintings,Painting,"Mariana of Austria (1634–1696), Queen of Spain",,,,,,Artist,Workshop of,Velázquez,"Spanish, Seville 1599–1660 Madrid",,Velázquez,Spanish,1599,1660,,1619,1660,Oil on canvas,32 1/4 x 39 1/2 in. (81.9 x 100.3 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.104,false,true,437861,European Paintings,Painting,Portrait of a Boy with a Falcon,,,,,,Artist,,Wallerant Vaillant,"Flemish, Lille 1623–1677 Amsterdam",,"Vaillant, Wallerant",Flemish,1623,1677,,1643,1677,Oil on canvas,29 3/4 x 25 in. (75.6 x 63.5 cm),"Purchase, George T. Delacorte Jr. Gift, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437861,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.306ab,false,true,437620,European Paintings,"Painting, grisaille",Nymph and Putti in a Vintage Scene; Nymph with a Wreath and Putti with Garlands of Flowers,,,,,,Artist,,Piat Joseph Sauvage,"Flemish, Tournai 1744–1818 Tournai",,"Sauvage, Piat Joseph",Flemish,1744,1818,,1763,1818,Oil on slate,(a) 9 1/2 x 23 1/4 in. (24.1 x 59.1 cm); (b) 9 5/8 x 23 3/4 in. (24.4 x 60.3 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.133,false,true,436937,European Paintings,Painting,The Dream after the Ball,,,,,,Artist,,Hans Makart,"Austrian, Salzburg 1840–1884 Vienna",,"Makart, Hans",Austrian,1840,1884,,1860,1884,Oil on canvas,62 3/8 x 37 1/4 in. (158.4 x 94.6 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.47.3,false,true,437781,European Paintings,Painting,Landscape with Thatched Cottages,,,,,,Artist,Workshop of,David Teniers the Younger,"Flemish, Antwerp 1610–1690 Brussels",,"Teniers, David, the Younger",Flemish,1610,1690,,1630,1690,Oil on wood,5 3/4 x 7 3/4 in. (14.6 x 19.7 cm),"Bequest of John Henry Abegg, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.22,false,true,437780,European Paintings,Painting,Shepherds and Sheep,,,,,,Artist,,David Teniers the Younger,"Flemish, Antwerp 1610–1690 Brussels",,"Teniers, David, the Younger",Flemish,1610,1690,,1630,1690,Oil on wood,6 5/8 x 9 in. (16.8 x 22.9 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.25,false,true,437776,European Paintings,Painting,The Good Samaritan,,,,,,Artist,,David Teniers the Younger,"Flemish, Antwerp 1610–1690 Brussels",,"Teniers, David, the Younger",Flemish,1610,1690,,1630,1690,Oil on wood,6 3/4 x 9 in. (17.1 x 22.9 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.46,false,true,437918,European Paintings,Painting,Portrait of a Young Woman,,,,,,Artist,,Cornelis de Vos,"Flemish, Hulst 1584/85–1651 Antwerp",,"Vos, Cornelis de",Flemish,1584,1651,,1603,1651,Oil on canvas,"46 1/2 x 37 1/4 in. (118.1 x 94.6 cm), including added strip of 2 3/4 in. (7 cm) at top","Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.37,false,true,437917,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Cornelis de Vos,"Flemish, Hulst 1584/85–1651 Antwerp",,"Vos, Cornelis de",Flemish,1584,1651,,1603,1651,Oil on wood,49 3/8 x 38 in. (125.4 x 96.5 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437917,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.190.28a–d,false,true,437152,European Paintings,"Painting, retable",Virgin and Child Enthroned with Scenes from the Life of the Virgin,,,,,,Artist,,Morata Master,"Spanish, Aragonese, late 15th century",,Morata Master,Spanish,1470,1499,,1470,1499,Tempera and gold on wood,"Central panel, below, 52 x 34 5/8 in. (132.1 x 87.9 cm); central panel, above, 43 3/4 x 34 1/2 in. (111.1 x 87.6 cm); each side panel 84 1/2 x 22 3/4 in. (214.6 x 57.8 cm)","Bequest of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.224.1,false,true,436077,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Michael Dahl,"Swedish, Stockholm 1659–1743 London",,"Dahl, Michael",Swedish,1659,1743,,1696,1743,Oil on canvas,77 1/4 x 51 3/4 in. (196.2 x 131.4 cm),"Gift of Margaret Bruguière, in memory of Louis Bruguière, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436077,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.662,false,true,436829,European Paintings,Painting,Hugo van der Goes Painting the Portrait of Mary of Burgundy,,,,,,Artist,,Guillaume Koller,"Belgian, Vienna 1829–1884 near Nancy",,"Koller, Guillaume",Belgian,1829,1884,,1849,1884,Oil on wood,23 3/8 x 34 in. (59.4 x 86.4 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.61,false,true,437637,European Paintings,Painting,Lost: Souvenir of Auvergne,,,,,,Artist,,August Friedrich Albrecht Schenck,"Danish, Glückstadt 1828–1901 Ecouen",,"Schenck, August Friedrich Albrecht",Danish,1828,1901,,1848,1901,Oil on canvas,58 x 97 3/4 in. (147.3 x 248.3 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.3,false,true,435805,European Paintings,Painting,A Peasant with a Bird,,,,,,Artist,,Adriaen Brouwer,"Flemish, Oudenaarde 1605/6–1638 Antwerp",,"Brouwer, Adriaen",Flemish,1605,1638,,1626,1638,Oil on wood,Oval 7 1/8 x 5 1/2 in. (18.1 x 14 cm); set in rectangular panel 8 x 6 1/4 in. (20.3 x 15.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.70,false,true,436551,European Paintings,Painting,"Ferdinand VII (1784–1833), When Prince of Asturias",,,,,,Artist,,Goya,"Spanish, Fuendetodos 1746–1828 Bordeaux",and Workshop,Goya,Spanish,1746,1828,,1800,1805,Oil on canvas,32 3/4 x 26 1/4 in. (83.2 x 66.7 cm),"Gift of René Fribourg, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.181,false,true,436554,European Paintings,Painting,Bullfight in a Divided Ring,,,,,,Artist,Attributed to,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,,1800,1829,Oil on canvas,38 3/4 x 49 3/4 in. (98.4 x 126.4 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436554,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.180,false,true,436547,European Paintings,Painting,Narcisa Barañana de Goicoechea,,,,,,Artist,Attributed to,Goya (Francisco de Goya y Lucientes),"Spanish, Fuendetodos 1746–1828 Bordeaux",,Goya (Francisco de Goya y Lucientes),Spanish,1746,1828,,1766,1828,Oil on canvas,44 1/4 x 30 3/4 in. (112.4 x 78.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.220.2,false,true,437972,European Paintings,Painting,The Crucifixion,,,,,,Artist,Workshop of,Francisco de Zurbarán,"Spanish, Fuente de Cantos 1598–1664 Madrid",,"Zurbarán, Francisco de",Spanish,1598,1664,,1618,1664,Oil on canvas,"Arched top, 112 x 75 7/8 in. (284.5 x 192.7 cm)","Gift of George R. Hann, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.14,false,true,436198,European Paintings,Painting,Saint Cecilia,,,,,,Artist,,Abraham van Diepenbeeck,"Flemish, 's Hertogenbosch 1596–1675 Antwerp",,"Diepenbeeck, Abraham van",Flemish,1596,1675,,1616,1675,Oil on canvas,47 7/8 x 40 3/4 in. (121.6 x 103.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436198,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.145.2,false,true,435847,European Paintings,Painting,"Saint Vincent, Patron Saint of Lisbon",,,,,,Artist,,Frei Carlos,"Portuguese, active second quarter 16th century",,"Carlos, Frei",Portuguese,1525,1549,,1525,1549,Oil on wood,64 x 20 7/8 in. (162.6 x 53 cm),"Bequest of Muriel Stokes, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.280.6,false,true,437270,European Paintings,Painting,Head of a Man,,,,,,Artist,,Vasilii Grigorievich Perov,"Russian, Tobolsk 1834–1882 Kuz'minki, Moscow",,"Perov, Vasilii Grigorievich",Russian,1834,1882,,1854,1882,Oil on canvas board,20 7/8 x 13 7/8 in. (53 x 35.2 cm),"Bequest of Mary Jane Dastich, in memory of her husband, General Frank Dastich, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.180,false,true,437360,European Paintings,Painting,"Lady Maitland (Catherine Connor, died 1865)",,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,,1776,1823,Oil on canvas,49 3/4 x 39 3/4 in. (126.4 x 101 cm),"Gift of Jessie Woolworth Donahue, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437360,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.13.5,false,true,437358,European Paintings,Painting,Janet Law,,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,,1776,1823,Oil on canvas,35 1/4 x 27 1/4 in. (89.5 x 69.2 cm),"Bequest of Helen Swift Neilson, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437358,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.94.1,false,true,437356,European Paintings,Painting,Alexander Maconochie (1777–1861) of Meadowbank,,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,,1776,1823,Oil on canvas,30 1/4 x 25 in. (76.8 x 63.5 cm),"Gift of William P. Clyde, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.13,false,true,437359,European Paintings,Painting,John Gray (1731–1811) of Newholm,,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,,1776,1823,Oil on canvas,49 3/8 x 40 in. (125.4 x 101.6 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437359,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.181.13,false,true,437357,European Paintings,Painting,James Johnston of Straiton (died 1841),,,,,,Artist,,Sir Henry Raeburn,"British, Stockbridge, Scotland 1756–1823 Edinburgh, Scotland",,"Raeburn, Henry, Sir","British, Scottish",1756,1923,,1776,1823,Oil on canvas,35 1/4 x 27 1/4 in. (89.5 x 69.2 cm),"Bequest of Adele L. Lehman, in memory of Arthur Lehman, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437357,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.339.2,false,true,437755,European Paintings,Painting,In the Studio,,,,,,Artist,,Alfred Stevens,"Belgian, Brussels 1823–1906 Paris",,"Stevens, Alfred",Belgian,1823,1906,1888,1888,1888,Oil on canvas,42 x 53 1/2 in. (106.7 x 135.9 cm),"Gift of Mrs. Charles Wrightsman, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.241,false,true,435590,European Paintings,Painting,Family Group in a Landscape,,,,,,Artist|Artist,and,Jacques d'Arthois|Flemish Painter,"Flemish, 1613–ca. 1686|ca. 1645",,"Arthois, Jacques d'|Flemish Painter",Flemish|Flemish,1613,1686,ca. 1645,1640,1650,Oil on canvas,49 1/8 x 60 1/8 in. (124.8 x 152.7 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.50,false,true,437189,European Paintings,Painting,Interior of a Gothic Church at Night,,,,,,Artist|Artist,and,Pieter Neeffs the Younger|Frans Francken III,"Flemish, 1620–after 1675|Flemish, 1607–1667",,"Neeffs, Pieter, the Younger|Francken, Frans, III",Flemish|Flemish,1620 |1607,1675 |1667,ca. 1660,1655,1665,Oil on wood,10 x 7 3/4 in. (25.4 x 19.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.58.20,false,true,437187,European Paintings,Painting,Interior of a Gothic Church by Day,,,,,,Artist|Artist,and,Frans Francken III|Pieter Neeffs the Elder,"Flemish, 1607–1667|Flemish, active 1605–1656/61",,"Francken, Frans, III|Neeffs, Pieter, the Elder",Flemish|Flemish,1607 |1605,1667 |1661,probably ca. 1635–40,1635,1640,Oil on copper,5 1/8 x 6 1/2 in. (13 x 16.5 cm),"Bequest of Edward C. Post, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.58.21,false,true,437188,European Paintings,Painting,Interior of a Gothic Church at Night,,,,,,Artist|Artist,and,Pieter Neeffs the Elder|Frans Francken III,"Flemish, active 1605–1656/61|Flemish, 1607–1667",,"Neeffs, Pieter, the Elder|Francken, Frans, III",Flemish|Flemish,1605 |1607,1661 |1667,probably ca. 1635–40,1635,1640,Oil on copper,5 1/8 x 6 1/2 in. (13 x 16.5 cm),"Bequest of Edward C. Post, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.52,false,true,436987,European Paintings,Painting,The Rest on the Flight into Egypt,,,,,,Artist|Artist,and|Follower of,Master of the Liège Disciples at Emmaus|Quentin Metsys,"Netherlandish, active mid-16th century|Netherlandish, mid-16th century",,"Master of the Liège Disciples at Emmaus|Metsys, Quentin",Netherlandish|Netherlandish,1530 |1466,1569 |1530,ca. 1540,1535,1545,Oil on wood,37 1/2 x 30 1/4 in. (95.3 x 76.8 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.125,false,true,437868,European Paintings,Painting,"Don Gaspar de Guzmán (1587–1645), Count-Duke of Olivares",,,,,,Artist|Artist,Attributed to|and/or,Velázquez (Diego Rodríguez de Silva y Velázquez)|Juan Bautista Martínez del Mazo,"Spanish, Seville 1599–1660 Madrid|Spanish, Cuenca ca. 1612–1667 Madrid",,"Velázquez (Diego Rodríguez de Silva y Velázquez)|Mazo, Juan Bautista Martínez del",Spanish|Spanish,1599 |1612,1660 |1667,ca. 1635,1630,1640,Oil on canvas,50 1/4 x 41 in. (127.6 x 104.1 cm),"Fletcher Fund, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +"2011.485a, b",false,true,441672,European Paintings,"Painting, from the wing of an altarpiece",The Dormition of the Virgin; (reverse) Christ Carrying the Cross,,,,,,Artist|Artist,and Attributed to the,Hans Schäufelein|Master of Engerda,"German, Nuremberg ca. 1480–ca. 1540 Nördlingen|German, active ca. 1510–20",,"Schäufelein, Hans|Master of Engerda",German|German,1480 |1510,1540 |1520,ca. 1510,1505,1515,Oil and gold on fir,55 x 53 1/8 in. (139.7 x 134.9 cm),"Purchase, Lila Acheson Wallace, Karen and Mo Zukerman, Kowitz Family Foundation, Anonymous, and Hester Diamond Gifts, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.141,false,true,437525,European Paintings,Painting,The Feast of Acheloüs,,,,,,Artist|Artist,and,Peter Paul Rubens|Jan Brueghel the Elder,"Flemish, Siegen 1577–1640 Antwerp|Netherlandish, Brussels 1568–1625 Antwerp",,"Rubens, Peter Paul|Brueghel, Jan, the Elder",Flemish|Netherlandish,1577 |1568,1640 |1625,ca. 1615,1610,1620,Oil on wood,42 1/2 x 64 1/2 in. (108 x 163.8 cm),"Gift of Alvin and Irwin Untermyer, in memory of their parents, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.179,false,true,437843,European Paintings,Painting,Portrait of a Man,,,,,,Artist|Artist,Attributed to|Fraudulent Imitation of,Gaspare Traversi|Goya,"Italian, Neapolitan, ca. 1722–1770",,"Traversi, Gaspare|Goya",Italian|Spanish,1722 |1746,1770 |1828,,1742,1770,Oil on canvas,22 x 17 1/2 in. (55.9 x 44.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1039,false,true,437919,European Paintings,Painting,Two Tritons at the Feast of Acheloüs,,,,,,Artist|Artist,Attributed to|and Attributed to,Cornelis de Vos|Frans Snyders,"Flemish, Hulst 1584/85–1651 Antwerp|Flemish, Antwerp 1579–1657 Antwerp",,"Vos, Cornelis de|Snyders, Frans",Flemish|Flemish,1584 |1579,1651 |1657,,1603,1651,Oil on canvas,62 3/4 x 45 7/8 in. (159.4 x 116.5 cm),"Marquand Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.143,false,true,437324,European Paintings,Painting,Portrait of a Young Woman,,,,,,Artist|Artist,?,Pieter Jansz. Pourbus|Pieter Jansz. Pourbus,"Netherlandish, Gouda? 1524–1584 Bruges|Netherlandish, Gouda? 1524–1584 Bruges",,"Pourbus, Pieter Jansz.|Pourbus, Pieter Jansz.",Netherlandish|Netherlandish,1524 |1524,1584 |1584,,1544,1584,Oil on wood,15 1/2 x 12 1/2 in. (39.4 x 31.8 cm),"Charles B. Curtis Fund, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.392,false,true,726543,European Paintings,Painting,Monsignor Giuseppe Spina (1756–1828),,,,,,Artist,,Angelica Kauffmann,"Swiss, Chur 1741–1807 Rome",,"Kauffmann, Angelica",,1741,1807,1798,1798,1798,Oil on canvas,37 5/8 × 31 1/2 in. (95.5 × 80 cm),"Gift of Carlo Orsi, 2016",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/726543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.438,false,true,678013,European Paintings,Painting,Tiger in Repose,,,,,,Artist,,Antoine-Louis Barye,"French, Paris 1796–1875 Paris",,"Barye, Antoine-Louis",,1796,1875,ca. 1850–65,1845,1870,Oil on canvas,10 3/4 × 14 in. (27.3 × 35.6 cm),"Gift of Eugene V. Thaw, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/678013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.306,false,true,712013,European Paintings,Painting,"Grape Vines and Fruit, with Three Wagtails",,,,,,Artist,,Bartolomeo Cavarozzi,"Italian, Viterbo 1587–1625 Rome",,"Cavarozzi, Bartolomeo",,1587,1625,ca. 1615–18,1615,1618,Oil on canvas,40 × 61 3/4 in. (101.6 × 156.8 cm),"Gift of Claire and Giovanni Sarti, in honor of Keith Christiansen, 2016",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/712013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.645,false,true,702752,European Paintings,Painting,Christ and the Woman of Samaria,,,,,,Artist,,Benedetto Luti,"Italian, Florence 1666–1724 Rome",,"Luti, Benedetto",,1666,1724,1715–20,1715,1720,Oil on copper,15 × 12 1/8 in. (38.2 × 30.9 cm),"Purchase, Rogers Fund, by exchange, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/702752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.485,false,true,712946,European Paintings,Painting,The Calling of Saint Matthew,,,,,,Artist,,Giovanni Battista Caracciolo,"Italian, Naples 1578–1635 Naples",,"Caracciolo, Giovanni Battista",,1578,1635,ca. 1625–30,1625,1630,Oil on canvas,51 3/8 × 61 1/2 in. (130.5 × 156.2 cm),"Purchase, The Morris and Alma Schapiro Fund Gift, 2016",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/712946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.653,false,true,695496,European Paintings,Painting,Bust of a Man (Saint Matthias?),,,,,,Artist,,Giovanni Battista Piazzetta,"Italian, Venice 1682–1754 Venice",,"Piazzetta, Giovanni Battista",,1682,1754,ca. 1715–20,1715,1720,Oil on canvas,17 1/2 × 14 3/4 in. (44.5 × 37.5 cm),"Gift of Dianne Modestini and Eugene V. Thaw, 2016",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/695496,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.314a,false,true,437625,European Paintings,"Painting, grisaille overdoor",The Triumph of Bacchus,,,,,,Artist,,Piat Joseph Sauvage,"Flemish, Tournai 1744–1818 Tournai",,"Sauvage, Piat Joseph",,1744,1818,1780s,1780,1789,Oil on canvas,14 x 32 7/8 in. (35.6 x 83.5 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.314b,false,true,437618,European Paintings,"Painting, grisaille",Infant Bacchanal,,,,,,Artist,,Piat Joseph Sauvage,"Flemish, Tournai 1744–1818 Tournai",,"Sauvage, Piat Joseph",,1744,1818,1780s,1780,1789,Oil on canvas,10 1/4 x 29 5/8 in. (26 x 75.2 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.761,false,true,440900,European Paintings,Painting,Saint Dominic in Penitence,,,,,,Artist,,Filippo Tarchiani,"Italian, Castello 1576–1645 Florence",,"Tarchiani, Filippo",,1576,1645,ca. 1607,1602,1612,Oil on canvas,52 x 43 in. (132.1 x 109.2 cm),"Gift of Brian J. Brille, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.507,false,true,712539,European Paintings,Painting,Saint Philip Neri (1515–1595),,,,,,Artist,,Carlo Dolci,"Italian, Florence 1616–1687 Florence",,"Dolci, Carlo",,1616,1687,1645 or 1646,1645,1646,Oil on canvas,17 1/4 × 14 1/4 in. (43.8 × 36.2 cm),"Purchase, George Delacorte Fund Gift, in memory of George T. Delacorte Jr., Ronald S. Lauder, Mr. and Mrs. Richard L. Chilton Jr., and Mr. and Mrs. Frederick W. Beinecke Gifts, 2016",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/712539,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.263,false,true,679686,European Paintings,Painting,Hortensia,,,,,,Artist,,Fernand Khnopff,"Belgian, Grembergen 1858–1921 Brussels",,"Khnopff, Fernand",,1858,1921,1884,1884,1884,Oil on canvas,18 13/16 × 23 1/2 in. (47.8 × 59.7 cm),"Purchase, Bequest of Julia W. Emmons, by exchange, and Catharine Lorillard Wolfe Collection, Wolfe Fund, and Gift of Charles Hack and the Hearn Family Trust, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/679686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.55,false,true,435760,European Paintings,Painting,The Man of Sorrows,,,,,,Artist,Workshop of,Aelbert Bouts,"Netherlandish, Leuven ca. 1451/54–1549",,"Bouts, Aelbert",,1451,1549,ca. 1525,1520,1530,Oil on oak,"Arched top, 17 1/2 x 11 1/4 in. (44.5 x 28.6 cm)","The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2015.398,false,true,687513,European Paintings,Painting,The Lamentation,,,,,,Artist,,Luis de Morales,"Spanish, Plasencia (?) 1510/11–1586 Alcántara",,"Morales, Luis de",,1510,1586,ca. 1560,1555,1565,Oil on walnut,35 × 24 5/8 in. (89 × 62.5 cm),"Purchase, Alejandro Santo Domingo and Annette de la Renta Gifts; Bequests of George D. Pratt and of Annette B. McFadden, and Gifts of Estate of George Quackenbush, in his memory, of Dr. and Mrs. Max A. Goldzieher, of Francis Neilson, of Dr. Foo Chu and Dr. Marguerite Hainje-Chu, of Mr. and Mrs. Harold H. Burns, and of Mr. and Mrs. Joshua Logan, and other gifts and bequests, by exchange; Victor Wilbour Fund; and Hester Diamond Gift, 2015",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/687513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.39,false,true,439977,European Paintings,Painting,Christ Carrying the Cross,,,,,,Artist,,Jan Gossart (called Mabuse),"Netherlandish, Maubeuge ca. 1478–1532 Antwerp (?)",,"Gossart, Jan (called Mabuse)",,1478,1532,ca. 1520–25,1520,1525,Oil on oak,9 7/8 × 7 1/2 in. (25.1 × 19 cm),"Gift of Honorable J. William Middendorf II, and Purchase, Walter and Leonore Annenberg and The Annenberg Foundation Gift, Director's Fund, Gift of George A. Hearn, by exchange, and Marquand and The Alfred N. Punnett Endowment Funds, 2016",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2016.63,false,true,670765,European Paintings,Painting,The Death of Cleopatra,,,,,,Artist,,Guido Cagnacci,"Italian, Santarcangelo di Romagna 1601–1663 Vienna",,"Cagnacci, Guido",,1601,1663,ca. 1645–55,1645,1655,Oil on canvas,37 3/8 × 29 1/2 in. (95 × 75 cm),"Purchase, Diane Burke Gift, Gift of J. Pierpont Morgan, by exchange, Friends of European Paintings Gifts, Gwynne Andrews Fund, Lila Acheson Wallace, Charles and Jessie Price, and Álvaro Saieh Bendeck Gifts, Gift and Bequest of George Blumenthal and Fletcher Fund, by exchange, and Michel David-Weill Gift, 2016",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/670765,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.54,false,true,436983,European Paintings,"Painting, miniature","Pierre Louis Dubus (1721–1799), Called Préville, of the Comédie-Française",,,,,,Artist,Attributed to,Jean-Baptiste Massé,"French, Paris 1687–1767 Paris",,"Massé, Jean-Baptiste",,1687,1767,,1707,1767,Ivory laid on card,Diameter 1 5/8 in. (42 mm),"Rogers Fund, 1957",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.156–57,false,true,435763,European Paintings,Painting,The Mourning Virgin; The Man of Sorrows,,,,,,Artist,Posthumous Workshop Copy after,Dieric Bouts,"Netherlandish, Leuven, ca. 1525",,"Bouts, Dieric",,1457,1475,,1520,1530,Oil on oak,Each 16 x 12 1/2 in. (40.6 x 31.8 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.138,false,true,436689,European Paintings,Painting,"Mrs. Whaley (died 1798, Isle of Man)",,,,,,Artist,Attributed to,George Chinnery,"British, London 1774–1852 Macau",,"Chinnery, George",,1774,1852,,1794,1798,Oil on canvas,93 1/2 x 58 in. (237.5 x 147.3 cm),"Gift of Henry S. Morgan, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.265,false,true,437613,European Paintings,"Painting, grisaille",Venus and Cupid,,,,,,Artist,Attributed to,Piat Joseph Sauvage,"Flemish, Tournai 1744–1818 Tournai",,"Sauvage, Piat Joseph",,1744,1818,,1764,1818,Oil on canvas,49 7/8 x 29 1/4 in. (126.7 x 74.3 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437613,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.134.3a–c,false,true,437491,European Paintings,"Painting, parts of a polyptych",Saint Michael; The Mass of Saint Gregory; Saint Jerome,,,,,,Artist,,Master of the Saint Catherine Legend,"Netherlandish, active ca. 1470–1500",,Master of the Saint Catherine Legend,,1470,1500,,1450,1499,Oil on wood,Central panel 6 1/8 x 3 3/4 in. (15.6 x 9.5 cm); left panel 6 1/4 x 3 7/8 in. (15.9 x 9.8 cm); right panel 6 1/4 x 3 3/4 in. (15.9 x 9.5 cm),"Bequest of William H. Herriman, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437491,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.26,false,true,437035,European Paintings,Painting,The Lamentation,,,,,,Artist,Follower of the,Master of the Virgin among Virgins,"Netherlandish, active late 15th century",,Master of the Virgin among Virgins,,1460,1495,,1470,1499,Oil on wood,34 7/8 x 20 1/4 in. (88.6 x 51.4 cm),"Rogers Fund, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.244,false,true,435725,European Paintings,Painting,Christ's Descent into Hell,,,,,,Artist,Follower of,Hieronymus Bosch,"Netherlandish, second quarter 16th century",,"Bosch, Hieronymus",,1450,1516,,1550,1560,Oil on wood,21 x 46 in. (53.3 x 116.8 cm),"Harris Brisbane Dick Fund, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.13,false,true,436347,European Paintings,Painting,Trompe l'oeil with Palettes and Miniature,,,,,,Artist,Attributed to,Jean François de Le Motte,"French, born before 1635–died in or after 1685",,"Le Motte, Jean François de",,1635,1685,,1670,1699,Oil on canvas,46 7/8 x 36 1/8 in. (119.1 x 91.8 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.133,false,true,436702,European Paintings,Painting,The Temptation of Saint Anthony,,,,,,Artist,Attributed to,Pieter Huys,"Netherlandish, Antwerp, active by 1545–died 1584 Antwerp",,"Huys, Pieter",,1545,1584,,1545,1584,Oil on wood,43 x 59 in. (109.2 x 149.9 cm),"Anonymous Gift, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.16,false,true,435624,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,David Baudringhien,"Dutch, ca. 1581–1650",,"Baudringhien, David",Dutch,1581,1650,1627,1627,1627,Oil on copper,"Oval, 3 3/4 x 3 in. (95 x 76 mm)","The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.392,false,true,438740,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Nicolaes Maes,"Dutch, Dordrecht 1634–1693 Amsterdam",,"Maes, Nicolaes",Dutch,1634,1693,1657,1657,1657,Oil on copper,4 5/8 x 3 3/8 in. (117 x 86 mm),"Gift of Lila and Herman Shickman, 2004",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/438740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.7,false,true,438379,European Paintings,Painting,Young Woman with a Red Necklace,,,,,,Artist,Style of,Rembrandt,"Dutch, ca. 1645",,Rembrandt,Dutch,1606,1669,ca. 1645,1640,1650,Oil on wood,"Overall, with added strips, 8 1/2 x 7 1/4 in. (21.6 x 18.4 cm)","From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.8,false,true,436627,European Paintings,Painting,Frans Hals (1582/83–1666),,,,,,Artist,Copy after,Frans Hals,"Dutch, 17th century",,"Hals, Frans",Dutch,1582,1666,probably 1650s,1650,1659,Oil on wood,12 7/8 x 11 in. (32.7 x 27.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.610,false,true,437414,European Paintings,Painting,Pilate Washing His Hands,,,,,,Artist,Style of,Rembrandt,"Dutch, 17th century",,Rembrandt,Dutch,1606,1669,probably 1660s,1600,1699,Oil on canvas,51 1/4 x 65 3/4 in. (130.2 x 167 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437414,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.73,false,true,435769,European Paintings,Painting,The Judgment of Solomon,,,,,,Artist,,Leonaert Bramer,"Dutch, Delft 1596–1674 Delft",,"Bramer, Leonaert",Dutch,1596,1674,1640s,1640,1649,Oil on wood,31 1/8 x 40 1/2 in. (79.1 x 102.9 cm),"Gift of National Surety Company, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.21,true,true,437881,European Paintings,Painting,Young Woman with a Water Pitcher,,,,,,Artist,,Johannes Vermeer,"Dutch, Delft 1632–1675 Delft",,"Vermeer, Johannes",Dutch,1632,1675,ca. 1662,1657,1667,Oil on canvas,18 x 16 in. (45.7 x 40.6 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.611,false,true,437878,European Paintings,Painting,A Maid Asleep,,,,,,Artist,,Johannes Vermeer,"Dutch, Delft 1632–1675 Delft",,"Vermeer, Johannes",Dutch,1632,1675,ca. 1656–57,1656,1657,Oil on canvas,34 1/2 x 30 1/8 in. (87.6 x 76.5 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.24,false,true,437880,European Paintings,Painting,Woman with a Lute,,,,,,Artist,,Johannes Vermeer,"Dutch, Delft 1632–1675 Delft",,"Vermeer, Johannes",Dutch,1632,1675,ca. 1662–63,1662,1663,Oil on canvas,20 1/4 x 18 in. (51.4 x 45.7 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.18,false,true,437877,European Paintings,Painting,Allegory of the Catholic Faith,,,,,,Artist,,Johannes Vermeer,"Dutch, Delft 1632–1675 Delft",,"Vermeer, Johannes",Dutch,1632,1675,ca. 1670–72,1670,1672,Oil on canvas,45 x 35 in. (114.3 x 88.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.396.1,true,true,437879,European Paintings,Painting,Study of a Young Woman,,,,,,Artist,,Johannes Vermeer,"Dutch, Delft 1632–1675 Delft",,"Vermeer, Johannes",Dutch,1632,1675,ca. 1665–67,1665,1667,Oil on canvas,17 1/2 x 15 3/4 in. (44.5 x 40 cm),"Gift of Mr. and Mrs. Charles Wrightsman, in memory of Theodore Rousseau Jr., 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.29,false,true,435598,European Paintings,Painting,"Portrait of a Man, Possibly a Botanist",,,,,,Artist,Attributed to,David Bailly,"Dutch, Leiden 1584–1657 Leiden",,"Bailly, David",Dutch,1584,1657,1641,1641,1641,Oil on wood,33 x 24 1/2 in. (83.8 x 62.2 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435598,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +40.64,false,true,436209,European Paintings,Painting,An Evening School,,,,,,Artist,,Gerrit Dou,"Dutch, Leiden 1613–1675 Leiden",,"Dou, Gerrit",Dutch,1613,1675,ca. 1655–57,1655,1657,Oil on wood,"Arched top, 10 x 9 in. (25.4 x 22.9 cm)","Bequest of Lillian M. Ellis, 1940",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436209,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.607,false,true,436210,European Paintings,Painting,Self-Portrait,,,,,,Artist,,Gerrit Dou,"Dutch, Leiden 1613–1675 Leiden",,"Dou, Gerrit",Dutch,1613,1675,ca. 1665,1660,1670,Oil on wood,19 1/4 x 15 3/8 in. (48.9 x 39.1 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436210,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.89,true,true,437749,European Paintings,Painting,Merry Company on a Terrace,,,,,,Artist,,Jan Steen,"Dutch, Leiden 1626–1679 Leiden",,"Steen, Jan",Dutch,1626,1679,ca. 1670,1665,1675,Oil on canvas,55 1/2 x 51 3/4 in. (141 x 131.4 cm),"Fletcher Fund, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.13.2,false,true,437748,European Paintings,Painting,The Lovesick Maiden,,,,,,Artist,,Jan Steen,"Dutch, Leiden 1626–1679 Leiden",,"Steen, Jan",Dutch,1626,1679,ca. 1660,1655,1665,Oil on canvas,34 x 39 in. (86.4 x 99.1 cm),"Bequest of Helen Swift Neilson, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.31,false,true,437747,European Paintings,Painting,The Dissolute Household,,,,,,Artist,,Jan Steen,"Dutch, Leiden 1626–1679 Leiden",,"Steen, Jan",Dutch,1626,1679,ca. 1663–64,1663,1664,Oil on canvas,42 1/2 x 35 1/2 in. (108 x 90.2 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.6,false,true,437876,European Paintings,Painting,Entrance to a Dutch Port,,,,,,Artist,,Willem van de Velde II,"Dutch, Leiden 1633–1707 London",,"Velde, Willem van de, II",Dutch,1633,1707,ca. 1665,1660,1670,Oil on canvas,25 7/8 x 30 5/8 in. (65.7 x 77.8 cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.3,false,true,437088,European Paintings,Painting,The Serenade,,,,,,Artist,,Frans van Mieris the Elder,"Dutch, Leiden 1635–1681 Leiden",,"Mieris, Frans van, the Elder",Dutch,1635,1681,ca. 1678–80,1678,1680,Oil on wood,"Arched top, 5 3/4 x 4 3/8 in. (14.6 x 11.1 cm)","Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.70,false,true,437687,European Paintings,Painting,Johan Hulshout (1623–1687),,,,,,Artist,,Pieter van Slingelandt,"Dutch, Leiden 1640–1691 Leiden",,"Slingelandt, Pieter van",Dutch,1640,1691,ca. 1670 or slightly later,1665,1675,Oil on wood,14 1/2 x 11 3/4 in. (36.8 x 29.8 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.55.1,false,true,437688,European Paintings,Painting,Portrait of a Man,,,,,,Artist,Attributed to,Pieter van Slingelandt,"Dutch, Leiden 1640–1691 Leiden",,"Slingelandt, Pieter van",Dutch,1640,1691,ca. 1680,1675,1685,Oil on copper,"Oval, 3 3/8 x 2 1/2 in. (8.6 x 6.4 cm)","Bequest of Rupert L. Joseph, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.23.2,false,true,437915,European Paintings,Painting,"Interior of the Oude Kerk, Delft",,,,,,Artist,,Hendrick van Vliet,"Dutch, Delft 1611/12–1675 Delft",,"Vliet, Hendrick van",Dutch,1611,1675,1660,1660,1660,Oil on canvas,32 1/2 x 26 in. (82.6 x 66 cm),"Gift of Clarence Dillon, 1976",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.30.1,false,true,436788,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Cornelis Jonson van Ceulen the Elder,"Dutch, London 1593–1661 Utrecht",,"Jonson van Ceulen, Cornelis, the Elder",Dutch,1593,1661,1648,1648,1648,Oil on canvas,Overall 40 3/4 x 31 1/2 in. (103.5 x 80 cm); painted surface 40 3/4 x 31 1/8 in. (103.5 x 79.1 cm),"Gift of Mrs. J. E. Spingarn, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436788,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.30.2,false,true,436789,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Cornelis Jonson van Ceulen the Elder,"Dutch, London 1593–1661 Utrecht",,"Jonson van Ceulen, Cornelis, the Elder",Dutch,1593,1661,1648,1648,1648,Oil on canvas,Overall 40 3/4 x 31 1/2 in. (103.5 x 80 cm); painted surface 40 3/4 x 30 7/8 in. (103.5 x 78.4 cm),"Gift of Mrs. J. E. Spingarn, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436789,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.7,false,true,437102,European Paintings,Painting,Landscape with a Cottage,,,,,,Artist,,Pieter de Molijn,"Dutch, London 1595–1661 Haarlem",,"Molijn, Pieter de",Dutch,1595,1661,1629,1629,1629,Oil on wood,14 3/4 x 21 3/4 in. (37.5 x 55.2 cm),"Gift of Henry G. Marquand, 1895",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437102,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.116.3,false,true,436790,European Paintings,Painting,Portrait of a Man with a Watch,,,,,,Artist,,Cornelis Jonson van Ceulen the Younger,"Dutch, London 1634–1715 Utrecht",,"Jonson van Ceulen, Cornelis, the Younger",Dutch,1634,1715,1657,1657,1657,Oil on canvas,33 x 27 3/4 in. (83.8 x 70.5 cm),"Given in memory of Felix M. Warburg by his wife and children, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.109,false,true,437633,European Paintings,Painting,Cephalus and Procris,,,,,,Artist,,Godfried Schalcken,"Dutch, Made 1643–1706 The Hague",,"Schalcken, Godfried",Dutch,1643,1706,probably 1680s,1680,1689,Oil on canvas,25 1/2 x 31 3/8 in. (64.8 x 79.7 cm),"Rogers Fund, 1974",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.810,false,true,437040,European Paintings,Painting,Changing Pasture,,,,,,Artist,,Anton Mauve,"Dutch, Zaandam 1838–1888 Arnhem",,"Mauve, Anton",Dutch,1838,1888,ca. 1880s,1858,1888,Oil on canvas,24 x 39 5/8 in. (61 x 100.6 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.624,false,true,437406,European Paintings,Painting,"Portrait of a Man (""The Auctioneer"")",,,,,,Artist,Follower of,Rembrandt,"Dutch, third quarter 17th century",,Rembrandt,Dutch,1606,1669,probably ca. 1658–62,1658,1662,Oil on canvas,42 3/4 x 34 in. (108.6 x 86.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437406,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.27,false,true,436309,European Paintings,Painting,Bearded Man with a Velvet Cap,,,,,,Artist,,Govert Flinck,"Dutch, Cleve 1615–1660 Amsterdam",,"Flinck, Govert",Dutch,1615,1660,1645,1645,1645,Oil on wood,23 3/4 x 20 5/8 in. (60.3 x 52.4 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.5,false,true,437920,European Paintings,Painting,A Vase with Flowers,,,,,,Artist,,Jacob Vosmaer,"Dutch, Delft ca. 1584–1641 Delft",,"Vosmaer, Jacob",Dutch,1584,1641,probably 1613,1613,1613,Oil on wood,33 1/2 x 24 5/8 in. (85.1 x 62.5 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.108,false,true,436615,European Paintings,Painting,A Banquet,,,,,,Artist,,Dirck Hals,"Dutch, Haarlem 1591–1656 Haarlem",,"Hals, Dirck",Dutch,1591,1656,1628,1628,1628,Oil on wood,16 x 26 in. (40.6 x 66 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.318,false,true,437323,European Paintings,Painting,A Brazilian Landscape,,,,,,Artist,,Frans Post,"Dutch, Haarlem 1612–1680 Haarlem",,"Post, Frans",Dutch,1612,1680,1650,1650,1650,Oil on wood,24 x 36 in. (61 x 91.4 cm),"Purchase, Rogers Fund, special funds, James S. Deely Gift, and Gift of Edna H. Sachs and other gifts and bequests, by exchange, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.48,false,true,437953,European Paintings,Painting,A Man and a Woman on Horseback,,,,,,Artist,,Philips Wouwerman,"Dutch, Haarlem 1619–1668 Haarlem",,"Wouwerman, Philips",Dutch,1619,1668,ca. 1653–54,1653,1654,Oil on wood,12 1/8 x 16 1/4 in. (30.8 x 41.3 cm),"Purchase, Pfeiffer Fund, Joseph Pulitzer Bequest, and Gift of Dr. Ernest G. Stillman, by exchange, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.288,false,true,437956,European Paintings,Painting,Kitchen Scene,,,,,,Artist,,Peter Wtewael,"Dutch, Utrecht 1596–1660 Utrecht",,"Wtewael, Peter",Dutch,1596,1660,1620s,1620,1629,Oil on canvas,44 3/4 x 63 in. (113.7 x 160 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.38,false,true,435714,European Paintings,Painting,Curiosity,,,,,,Artist,,Gerard ter Borch the Younger,"Dutch, Zwolle 1617–1681 Deventer",,"Borch, Gerard ter, the Younger",Dutch,1617,1681,ca. 1660–62,1660,1662,Oil on canvas,30 x 24 1/2 in. (76.2 x 62.2 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.15,false,true,435715,European Paintings,Painting,Portrait of a Seated Man,,,,,,Artist,,Gerard ter Borch the Younger,"Dutch, Zwolle 1617–1681 Deventer",,"Borch, Gerard ter, the Younger",Dutch,1617,1681,late 1650s or early 1660s,1657,1663,Oil on wood,14 1/8 x 12 in. (35.9 x 30.5 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.617,false,true,435717,European Paintings,Painting,A Woman Playing the Theorbo-Lute and a Cavalier,,,,,,Artist,,Gerard ter Borch the Younger,"Dutch, Zwolle 1617–1681 Deventer",,"Borch, Gerard ter, the Younger",Dutch,1617,1681,ca. 1658,1653,1663,Oil on wood,14 1/2 x 12 3/4 in. (36.8 x 32.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.10,false,true,435718,European Paintings,Painting,A Young Woman at Her Toilet with a Maid,,,,,,Artist,,Gerard ter Borch the Younger,"Dutch, Zwolle 1617–1681 Deventer",,"Borch, Gerard ter, the Younger",Dutch,1617,1681,ca. 1650–51,1650,1651,Oil on wood,18 3/4 x 13 5/8 in. (47.6 x 34.6 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.30,false,true,435716,European Paintings,Painting,The Van Moerkerken Family,,,,,,Artist,,Gerard ter Borch the Younger,"Dutch, Zwolle 1617–1681 Deventer",,"Borch, Gerard ter, the Younger",Dutch,1617,1681,ca. 1653–54,1653,1654,Oil on wood,16 1/4 x 14 in. (41.3 x 35.6 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435716,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.116.2,false,true,436211,European Paintings,Painting,Portrait of a Man (Self-Portrait?),,,,,,Artist,,Willem Drost,"Dutch, Amsterdam 1633–1659 Venice",,"Drost, Willem",Dutch,1633,1659,1653 or 1655,1653,1655,Oil on canvas,34 1/8 x 28 1/2 in. (86.7 x 72.4 cm),"Given in memory of Felix M. Warburg by his wife and children, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436211,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.268,false,true,437415,European Paintings,Painting,The Sibyl,,,,,,Artist,,Willem Drost,"Dutch, Amsterdam 1633–1659 Venice",,"Drost, Willem",Dutch,1633,1659,ca. 1654,1649,1659,Oil on canvas,38 1/2 x 30 3/4 in. (97.8 x 78.1 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437415,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.4,false,true,438376,European Paintings,Painting,"Still Life with Oysters, a Silver Tazza, and Glassware",,,,,,Artist,,Willem Claesz Heda,"Dutch, Haarlem? 1594–1680 Haarlem",,"Heda, Willem Claesz",Dutch,1594,1680,1635,1635,1635,Oil on wood,19 5/8 x 31 3/4 in. (49.8 x 80.6 cm),"From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.62,false,true,436558,European Paintings,Painting,View of Haarlem and the Haarlemmer Meer,,,,,,Artist,,Jan van Goyen,"Dutch, Leiden 1596–1656 The Hague",,"Goyen, Jan van",Dutch,1596,1656,1646,1646,1646,Oil on wood,13 5/8 x 19 7/8 in. (34.6 x 50.5 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436558,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.25,false,true,436555,European Paintings,Painting,Sandy Road with a Farmhouse,,,,,,Artist,,Jan van Goyen,"Dutch, Leiden 1596–1656 The Hague",,"Goyen, Jan van",Dutch,1596,1656,1627,1627,1627,Oil on wood,12 1/8 x 16 1/4 in. (30.8 x 41.3 cm),"Bequest of Myra Mortimer Pinter, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436555,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.65.1,false,true,436559,European Paintings,Painting,Castle by a River,,,,,,Artist,,Jan van Goyen,"Dutch, Leiden 1596–1656 The Hague",,"Goyen, Jan van",Dutch,1596,1656,1647,1647,1647,Oil on wood,26 x 38 1/4 in. (66 x 97.2 cm),"Gift of Edith Neuman de Végvár, in honor of her husband, Charles Neuman de Végvár, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.6,false,true,436556,European Paintings,Painting,Country House near the Water,,,,,,Artist,,Jan van Goyen,"Dutch, Leiden 1596–1656 The Hague",,"Goyen, Jan van",Dutch,1596,1656,1646,1646,1646,Oil on wood,14 3/8 x 13 in. (36.5 x 33 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436556,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.146.3,false,true,436557,European Paintings,Painting,The Pelkus Gate near Utrecht,,,,,,Artist,,Jan van Goyen,"Dutch, Leiden 1596–1656 The Hague",,"Goyen, Jan van",Dutch,1596,1656,1646,1646,1646,Oil on wood,"14 1/2 x 22 1/2 in. (36.8 x 57.2 cm) Frame, 23 1/4 x 31 x 2 3/4 in. (59.1 x 78.7 x 7 cm)","Gift of Francis Neilson, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436557,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.2,false,true,438374,European Paintings,Painting,A Beach with Fishing Boats,,,,,,Artist,,Jan van Goyen,"Dutch, Leiden 1596–1656 The Hague",,"Goyen, Jan van",Dutch,1596,1656,probably 1653,1653,1653,Oil on wood,11 x 17 in. (27.9 x 43.2 cm),"From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.3,false,true,438375,European Paintings,Painting,A View of The Hague from the Northwest,,,,,,Artist,,Jan van Goyen,"Dutch, Leiden 1596–1656 The Hague",,"Goyen, Jan van",Dutch,1596,1656,1647,1647,1647,Oil on wood,26 x 37 7/8 in. (66 x 96.2 cm),"From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438375,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.125,false,true,437391,European Paintings,Painting,Portrait of a Young Woman with a Fan,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1633,1633,1633,Oil on canvas,49 1/2 x 39 3/4 in. (125.7 x 101 cm),"Gift of Helen Swift Neilson, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437391,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.198,true,true,437394,European Paintings,Painting,Aristotle with a Bust of Homer,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1653,1653,1653,Oil on canvas,56 1/2 x 53 3/4 in. (143.5 x 136.5 cm),"Purchase, special contributions and funds given or bequeathed by friends of the Museum, 1961",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437394,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.126,false,true,437387,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1632,1632,1632,Oil on wood,"Oval, 29 3/4 x 20 1/2 in. (75.6 x 52.1 cm)","Gift of Mrs. Lincoln Ellsworth, in memory of Lincoln Ellsworth, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437387,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.35,false,true,437395,European Paintings,Painting,"The Standard Bearer (Floris Soop, 1604–1657)",,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1654,1654,1654,Oil on canvas,55 1/4 x 45 1/4in. (140.3 x 114.9cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437395,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.26.7,false,true,437400,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,ca. 1655–60,1655,1660,Oil on canvas,32 7/8 x 25 3/8 in. (83.5 x 64.5 cm),"Marquand Collection, Gift of Henry G. Marquand, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437400,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.2,false,true,437385,European Paintings,Painting,"Man in Oriental Costume (""The Noble Slav"")",,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1632,1632,1632,Oil on canvas,60 1/8 x 43 3/4in. (152.7 x 111.1cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437385,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.101.9,false,true,437396,European Paintings,Painting,Hendrickje Stoffels (1626–1663),,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,mid-1650s,1654,1656,Oil on canvas,30 7/8 x 27 1/8 in. (78.4 x 68.9 cm),"Gift of Archer M. Huntington, in memory of his father, Collis Potter Huntington, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437396,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.1,false,true,437392,European Paintings,Painting,"Herman Doomer (born about 1595, died 1650)",,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1640,1640,1640,Oil on wood,29 5/8 x 21 3/4 in. (75.2 x 55.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437392,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.3,false,true,437386,European Paintings,Painting,"Portrait of a Man, probably a Member of the Van Beresteyn Family",,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1632,1632,1632,Oil on canvas,44 x 35 in. (111.8 x 88.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437386,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.4,false,true,437388,European Paintings,Painting,"Portrait of a Woman, probably a Member of the Van Beresteyn Family",,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1632,1632,1632,Oil on canvas,44 x 35 in. (111.8 x 88.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437388,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.618,true,true,437397,European Paintings,Painting,Self-Portrait,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1660,1660,1660,Oil on canvas,31 5/8 x 26 1/2 in. (80.3 x 67.3 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437397,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.620,false,true,437401,European Paintings,Painting,Portrait of a Man Holding Gloves,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1648,1648,1648,Oil on wood,31 3/4 x 26 1/2 in. (80.6 x 67.3 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437401,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.621,false,true,437399,European Paintings,Painting,Man with a Magnifying Glass,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,early 1660s,1660,1663,Oil on canvas,36 x 29 1/4 in. (91.4 x 74.3 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437399,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.622,false,true,437402,European Paintings,Painting,Woman with a Pink,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,early 1660s,1660,1663,Oil on canvas,36 1/4 x 29 3/8 in. (92.1 x 74.6 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437402,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.625,false,true,437390,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1633,1633,1633,Oil on wood,"Oval, 26 3/4 x 19 3/4 in. (67.9 x 50.2 cm)","Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.651,false,true,437393,European Paintings,Painting,The Toilet of Bathsheba,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1643,1643,1643,Oil on wood,22 1/2 x 30 in. (57.2 x 76.2 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.101.10,false,true,437398,European Paintings,Painting,Flora,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,ca. 1654,1649,1659,Oil on canvas,39 3/8 x 36 1/8 in. (100 x 91.8 cm),"Gift of Archer M. Huntington, in memory of his father, Collis Potter Huntington, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437398,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.23,false,true,437389,European Paintings,Painting,Bellona,,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,1633,1633,1633,Oil on canvas,50 x 38 3/8 in. (127 x 97.5 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.26.11,false,true,437070,European Paintings,Painting,A Musical Party,,,,,,Artist,,Gabriël Metsu,"Dutch, Leiden 1629–1667 Amsterdam",,"Metsu, Gabriël",Dutch,1629,1667,1659,1659,1659,Oil on canvas,24 1/2 x 21 3/8 in. (62.2 x 54.3 cm),"Marquand Collection, Gift of Henry G. Marquand, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437070,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.20,false,true,437071,European Paintings,Painting,The Visit to the Nursery,,,,,,Artist,,Gabriël Metsu,"Dutch, Leiden 1629–1667 Amsterdam",,"Metsu, Gabriël",Dutch,1629,1667,1661,1661,1661,Oil on canvas,30 1/2 x 32 in. (77.5 x 81.3 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437071,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.32,false,true,437073,European Paintings,Painting,A Woman Seated at a Window,,,,,,Artist,,Gabriël Metsu,"Dutch, Leiden 1629–1667 Amsterdam",,"Metsu, Gabriël",Dutch,1629,1667,early 1660s,1660,1663,Oil on wood,10 7/8 x 8 7/8 in. (27.6 x 22.5 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437073,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.160,false,true,437178,European Paintings,Painting,The Newborn Baby,,,,,,Artist,,Matthijs Naiveu,"Dutch, Leiden 1647–1726 Amsterdam",,"Naiveu, Matthijs",Dutch,1647,1726,1675,1675,1675,Oil on canvas,25 1/4 x 31 1/2 in. (64.1 x 80 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.118,false,true,436843,European Paintings,Painting,Apollo and Aurora,,,,,,Artist,,Gerard de Lairesse,"Dutch, Liège 1641–1711 Amsterdam",,"Lairesse, Gerard de",Dutch,1641,1711,1671,1671,1671,Oil on canvas,80 1/2 x 76 1/8 in. (204.5 x 193.4 cm),"Gift of Manuel E. and Ellen G. Rionda, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.30,false,true,436826,European Paintings,Painting,"Winter Landscape, Holland",,,,,,Artist,,Barend Cornelis Koekkoek,"Dutch, Middelburg 1803–1862 Cleve",,"Koekkoek, Barend Cornelis",Dutch,1803,1862,1833,1833,1833,Oil on wood,14 x 17 in. (35.6 x 43.2 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.45,false,true,436827,European Paintings,Painting,Sunset on the Rhine,,,,,,Artist,,Barend Cornelis Koekkoek,"Dutch, Middelburg 1803–1862 Cleve",,"Koekkoek, Barend Cornelis",Dutch,1803,1862,1853,1853,1853,Oil on canvas,32 1/4 x 42 3/8 in. (81.9 x 107.6 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436827,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.1.49,false,true,436975,European Paintings,Painting,Reverie,,,,,,Artist,,Matthys Maris,"Dutch, The Hague 1839–1917 London",,"Maris, Matthys",Dutch,1839,1917,1875,1875,1875,Oil on canvas,12 x 9 1/4 in. (30.5 x 23.5 cm),"Bequest of Elizabeth U. Coles, in memory of her son, William F. Coles, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.250.1,false,true,436671,European Paintings,Painting,Peacocks,,,,,,Artist,,Melchior d' Hondecoeter,"Dutch, Utrecht 1636–1695 Amsterdam",,"Hondecoeter, Melchior d'",Dutch,1636,1695,1683,1683,1683,Oil on canvas,74 7/8 x 53 in. (190.2 x 134.6 cm),"Gift of Samuel H. Kress, 1927",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.33,false,true,436621,European Paintings,Painting,"Portrait of a Man, Possibly Nicolaes Pietersz Duyst van Voorhout (born about 1600, died 1650)",,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,ca. 1636–38,1636,1638,Oil on canvas,31 3/4 x 26 in. (80.6 x 66 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.34,false,true,436617,European Paintings,Painting,Portrait of a Bearded Man with a Ruff,,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,1625,1625,1625,Oil on canvas,30 x 25 in. (76.2 x 63.5 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.26.9,false,true,436623,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,early 1650s,1650,1653,Oil on canvas,43 1/2 x 34 in. (110.5 x 86.4 cm),"Marquand Collection, Gift of Henry G. Marquand, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.8,false,true,436619,European Paintings,Painting,Petrus Scriverius (1576–1660),,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,1626,1626,1626,Oil on wood,8 3/4 x 6 1/2 in. (22.2 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436619,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.9,false,true,436618,European Paintings,Painting,"Anna van der Aar (born 1576/77, died after 1626)",,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,1626,1626,1626,Oil on wood,8 3/4 x 6 1/2 in. (22.2 x 16.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436618,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.34,false,true,436625,European Paintings,Painting,The Smoker,,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,ca. 1623–25,1623,1625,Oil on wood,"Octagonal, 18 3/8 x 19 1/2 in. (46.7 x 49.5 cm)","Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.26.10,false,true,436624,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,"ca. 1650, reworked probably 18th century",1645,1655,Oil on canvas,39 3/8 x 32 1/4 in. (100 x 81.9 cm),"Marquand Collection, Gift of Henry G. Marquand, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436624,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.602,true,true,436616,European Paintings,Painting,"Young Man and Woman in an Inn (""Yonker Ramp and His Sweetheart"")",,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,1623,1623,1623,Oil on canvas,41 1/2 x 31 1/4 in. (105.4 x 79.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.604,false,true,436626,European Paintings,Painting,Boy with a Lute,,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,ca. 1625,1620,1630,Oil on canvas,28 3/8 x 23 1/4 in. (72.1 x 59.1 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.605,true,true,436622,European Paintings,Painting,Merrymakers at Shrovetide,,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,ca. 1616–17,1616,1617,Oil on canvas,51 3/4 x 39 1/4 in. (131.4 x 99.7 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.101.11,false,true,436620,European Paintings,Painting,Paulus Verschuur (1606–1667),,,,,,Artist,,Frans Hals,"Dutch, Antwerp 1582/83–1666 Haarlem",,"Hals, Frans",Dutch,1582,1666,1643,1643,1643,Oil on canvas,46 3/4 x 37 in. (118.7 x 94 cm),"Gift of Archer M. Huntington, in memory of his father, Collis Potter Huntington, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436620,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.305,false,true,435770,European Paintings,Painting,The Preaching of John the Baptist,,,,,,Artist,,Bartholomeus Breenbergh,"Dutch, Deventer 1598–1657 Amsterdam",,"Breenbergh, Bartholomeus",Dutch,1598,1657,1634,1634,1634,Oil on wood,21 1/2 x 29 5/8 in. (54.6 x 75.2 cm),"Purchase, The Annenberg Foundation Gift, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435770,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.228,true,true,435817,European Paintings,Painting,The Crucifixion with the Virgin and Saint John,,,,,,Artist,,Hendrick ter Brugghen,"Dutch, The Hague? 1588–1629 Utrecht",,"Brugghen, Hendrick ter",Dutch,1588,1629,ca. 1624–25,1624,1625,Oil on canvas,61 x 40 1/4 in. (154.9 x 102.2 cm),"Funds from various donors, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.78,false,true,436636,European Paintings,Painting,Still Life with a Glass and Oysters,,,,,,Artist,,Jan Davidsz de Heem,"Dutch, Utrecht 1606–1683/84 Antwerp",,"Heem, Jan Davidsz de",Dutch,1606,1684,ca. 1640,1635,1645,Oil on wood,9 7/8 x 7 1/2 in. (25.1 x 19.1 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436636,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.195,false,true,436637,European Paintings,Painting,Still Life: A Banqueting Scene,,,,,,Artist,,Jan Davidsz de Heem,"Dutch, Utrecht 1606–1683/84 Antwerp",,"Heem, Jan Davidsz de",Dutch,1606,1684,probably ca. 1640–41,1640,1641,Oil on canvas,53 1/4 x 73 in. (135.3 x 185.4 cm),"Charles B. Curtis Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.144,false,true,436830,European Paintings,Painting,A Panoramic Landscape with a Country Estate,,,,,,Artist,,Philips Koninck,"Dutch, Amsterdam 1619–1688 Amsterdam",,"Koninck, Philips",Dutch,1619,1688,ca. 1649,1644,1654,Oil on canvas,56 3/8 x 68 1/4 in. (143.2 x 173.4 cm),"John Stewart Kennedy Fund, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.4,false,true,436831,European Paintings,Painting,An Extensive Wooded Landscape,,,,,,Artist,,Philips Koninck,"Dutch, Amsterdam 1619–1688 Amsterdam",,"Koninck, Philips",Dutch,1619,1688,1670s,1670,1679,Oil on canvas,32 3/4 x 44 5/8 in. (83.2 x 113.3 cm),"Purchase, Mr. and Mrs. David T. Schiff and George T. Delacorte Jr. Gifts, special funds, and Bequest of Mary Cushing Fosburgh and other gifts and bequests, by exchange, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.43.2,false,true,436832,European Paintings,Painting,Wide River Landscape,,,,,,Artist,,Philips Koninck,"Dutch, Amsterdam 1619–1688 Amsterdam",,"Koninck, Philips",Dutch,1619,1688,ca. 1648–49,1648,1649,Oil on canvas,16 1/4 x 22 7/8 in. (41.3 x 58.1 cm),"Gift of Edith Neuman de Végvár, in honor of her husband, Charles Neuman de Végvár, 1963",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.260.8,false,true,436267,European Paintings,Painting,A Musical Party,,,,,,Artist,,Gerbrand van den Eeckhout,"Dutch, Amsterdam 1621–1674 Amsterdam",,"Eeckhout, Gerbrand van den",Dutch,1621,1674,early 1650s,1650,1653,Oil on canvas,20 x 24 1/2 in. (50.8 x 62.2 cm),"Bequest of Annie C. Kane, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436267,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.16,false,true,436266,European Paintings,Painting,Isaac Blessing Jacob,,,,,,Artist,,Gerbrand van den Eeckhout,"Dutch, Amsterdam 1621–1674 Amsterdam",,"Eeckhout, Gerbrand van den",Dutch,1621,1674,1642,1642,1642,Oil on canvas,39 5/8 x 50 1/2 in. (100.6 x 128.3 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436266,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.31,false,true,435842,European Paintings,Painting,A State Yacht and Other Craft in Calm Water,,,,,,Artist,,Jan van de Cappelle,"Dutch, Amsterdam 1626–1679 Amsterdam",,"Cappelle, Jan van de",Dutch,1626,1679,ca. 1660,1655,1665,Oil on wood,27 1/2 x 36 3/8 in. (69.9 x 92.4 cm),"Francis L. Leland Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435842,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.614,false,true,436652,European Paintings,Painting,Entrance to a Village,,,,,,Artist,,Meyndert Hobbema,"Dutch, Amsterdam 1638–1709 Amsterdam",,"Hobbema, Meyndert",Dutch,1638,1709,ca. 1665,1660,1670,Oil on wood,29 1/2 x 43 3/8 in. (74.9 x 110.2 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436652,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.22,false,true,436653,European Paintings,Painting,Woodland Road,,,,,,Artist,,Meyndert Hobbema,"Dutch, Amsterdam 1638–1709 Amsterdam",,"Hobbema, Meyndert",Dutch,1638,1709,ca. 1670,1665,1675,Oil on canvas,37 1/4 x 51 in. (94.6 x 129.5 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.298,false,true,437946,European Paintings,"Painting, sketch for a ceiling decoration",Allegory of the Arts,,,,,,Artist,,Jacob de Wit,"Dutch, Amsterdam 1695–1754 Amsterdam",,"Wit, Jacob de",Dutch,1695,1754,1742,1742,1742,Oil on canvas,18 7/8 x 23 1/4 in. (47.9 x 59.1 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437946,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.301,false,true,437947,European Paintings,"Painting, sketch for a ceiling decoration",Flora and Zephyr,,,,,,Artist,,Jacob de Wit,"Dutch, Amsterdam 1695–1754 Amsterdam",,"Wit, Jacob de",Dutch,1695,1754,1743,1743,1743,Oil on canvas,20 7/8 x 24 7/8 in. (53 x 63.2 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.239,false,true,436270,European Paintings,Painting,Conversation Piece (The Sense of Smell),,,,,,Artist,,Jan Ekels the Younger,"Dutch, Amsterdam 1759–1793 Amsterdam",,"Ekels, Jan, the Younger",Dutch,1759,1793,probably 1791,1791,1791,Oil on canvas,25 7/8 x 23 1/2 in. (65.7 x 59.7 cm),"Gift of Mr. and Mrs. Bertram L. Podell, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436270,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.107,false,true,435904,European Paintings,Painting,Still Life with a Skull and a Writing Quill,,,,,,Artist,,Pieter Claesz,"Dutch, Berchem? 1596/97–1660 Haarlem",,"Claesz, Pieter",Dutch,1596,1660,1628,1628,1628,Oil on wood,9 1/2 x 14 1/8 in. (24.1 x 35.9 cm),"Rogers Fund, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.68,false,true,435690,European Paintings,Painting,Petronella Elias (1648–1667) with a Basket of Fruit,,,,,,Artist,,Ferdinand Bol,"Dutch, Dordrecht 1616–1680 Amsterdam",,"Bol, Ferdinand",Dutch,1616,1680,1657,1657,1657,Oil on canvas,31 5/8 x 26 in. (80.3 x 66 cm),"Purchase, George T. Delacorte Jr. Gift, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.269,false,true,435689,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Ferdinand Bol,"Dutch, Dordrecht 1616–1680 Amsterdam",,"Bol, Ferdinand",Dutch,1616,1680,1642,1642,1642,Oil on canvas,34 3/8 x 28 in. (87.3 x 71.1 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +34.83.1,false,true,436065,European Paintings,Painting,Children and a Cow,,,,,,Artist,Attributed to,Aelbert Cuyp,"Dutch, Dordrecht 1620–1691 Dordrecht",,"Cuyp, Aelbert",Dutch,1620,1691,1635–39,1635,1639,Oil on wood,17 1/4 x 21 1/2 in. (43.8 x 54.6 cm),"Bequest of Mariana Griswold Van Rensselaer, in memory of her father, George Griswold, 1934",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.616,false,true,436064,European Paintings,Painting,Young Herdsmen with Cows,,,,,,Artist,,Aelbert Cuyp,"Dutch, Dordrecht 1620–1691 Dordrecht",,"Cuyp, Aelbert",Dutch,1620,1691,ca. 1655–60,1655,1660,Oil on canvas,44 1/8 x 52 1/8 in. (112.1 x 132.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436064,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.15,false,true,436062,European Paintings,Painting,Piping Shepherds,,,,,,Artist,,Aelbert Cuyp,"Dutch, Dordrecht 1620–1691 Dordrecht",,"Cuyp, Aelbert",Dutch,1620,1691,ca. 1643–44,1643,1644,Oil on canvas,35 3/4 x 47 in. (90.8 x 119.4 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436062,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.20,false,true,436063,European Paintings,Painting,"Equestrian Portrait of Cornelis (1639–1680) and Michiel Pompe van Meerdervoort (1638–1653) with Their Tutor and Coachman (""Starting for the Hunt"")",,,,,,Artist,,Aelbert Cuyp,"Dutch, Dordrecht 1620–1691 Dordrecht",,"Cuyp, Aelbert",Dutch,1620,1691,ca. 1652–53,1652,1653,Oil on canvas,43 1/4 x 61 1/2in. (109.9 x 156.2cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436063,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.155.2,false,true,436061,European Paintings,Painting,Landscape with the Flight into Egypt,,,,,,Artist,,Aelbert Cuyp,"Dutch, Dordrecht 1620–1691 Dordrecht",,"Cuyp, Aelbert",Dutch,1620,1691,ca. 1650,1645,1655,Oil on wood,18 x 22 7/8 in. (45.7 x 58.1 cm),"Bequest of Josephine Bieber, in memory of her husband, Siegfried Bieber, 1970",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436061,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.133,false,true,436680,European Paintings,Painting,The Annunciation of the Death of the Virgin,,,,,,Artist,,Samuel van Hoogstraten,"Dutch, Dordrecht 1627–1678 Dordrecht",,"Hoogstraten, Samuel van",Dutch,1627,1678,ca. 1670,1665,1675,Oil on canvas,26 x 20 3/4 in. (66 x 52.7 cm),"Purchase, Rogers Fund and Joseph Pulitzer Bequest, 1992",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.33,false,true,435670,European Paintings,Painting,A Young Woman and a Cavalier,,,,,,Artist,,Cornelis Bisschop,"Dutch, Dordrecht 1630–1674 Dordrecht",,"Bisschop, Cornelis",Dutch,1630,1674,early 1660s,1660,1663,Oil on canvas,38 1/2 x 34 3/4 in. (97.8 x 88.3 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1325,false,true,436933,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Nicolaes Maes,"Dutch, Dordrecht 1634–1693 Amsterdam",,"Maes, Nicolaes",Dutch,1634,1693,ca. 1665–70,1665,1670,Oil on canvas,44 x 35 1/4 in. (111.8 x 89.5 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.73,false,true,436929,European Paintings,Painting,Abraham Dismissing Hagar and Ishmael,,,,,,Artist,,Nicolaes Maes,"Dutch, Dordrecht 1634–1693 Amsterdam",,"Maes, Nicolaes",Dutch,1634,1693,1653,1653,1653,Oil on canvas,34 1/2 x 27 1/2 in. (87.6 x 69.9 cm),"Gift of Mrs. Edward Brayton, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436929,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.149.3,false,true,436930,European Paintings,Painting,"Ingena Rotterdam (died 1704), Betrothed of Admiral Jacob Binkes",,,,,,Artist,,Nicolaes Maes,"Dutch, Dordrecht 1634–1693 Amsterdam",,"Maes, Nicolaes",Dutch,1634,1693,1676,1676,1676,Oil on canvas,17 1/4 x 13 in. (43.8 x 33 cm),"Gift of J. Pierpont Morgan, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436930,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.5,false,true,436932,European Paintings,Painting,The Lacemaker,,,,,,Artist,,Nicolaes Maes,"Dutch, Dordrecht 1634–1693 Amsterdam",,"Maes, Nicolaes",Dutch,1634,1693,ca. 1656,1651,1661,Oil on canvas,17 3/4 x 20 3/4 in. (45.1 x 52.7 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436932,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.612,false,true,436934,European Paintings,Painting,Young Woman Peeling Apples,,,,,,Artist,,Nicolaes Maes,"Dutch, Dordrecht 1634–1693 Amsterdam",,"Maes, Nicolaes",Dutch,1634,1693,ca. 1655,1650,1660,Oil on wood,21 1/2 x 18 in. (54.6 x 45.7 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436934,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.190,false,true,437089,European Paintings,Painting,Portrait of a Man,,,,,,Artist,Circle of,Arnold Boonen,"Dutch, Dordrecht 1669–1729 Amsterdam",,"Boonen, Arnold",Dutch,1669,1729,ca. 1720,1715,1725,Oil on canvas,22 1/4 x 18 3/4 in. (56.5 x 47.6 cm),"Gift of Marcel Aubry, 1968",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +91.26.8,false,true,437760,European Paintings,Painting,Landscape with Cattle,,,,,,Artist,,Jacob van Strij,"Dutch, Dordrecht 1756–1815 Dordrecht",,"Strij, Jacob van",Dutch,1756,1815,probably ca. 1800,1795,1805,Oil on wood,31 1/2 x 42 1/4 in. (80 x 107.3 cm),"Marquand Collection, Gift of Henry G. Marquand, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437760,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.23,false,true,436890,European Paintings,Painting,Battle Scene,,,,,,Artist,,Johannes Lingelbach,"Dutch, Frankfurt 1622–1674 Amsterdam",,"Lingelbach, Johannes",Dutch,1622,1674,1671,1671,1671,Oil on canvas,44 3/8 x 63 1/4 in. (112.7 x 160.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436890,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.123,false,true,436889,European Paintings,Painting,Peasants Dancing,,,,,,Artist,,Johannes Lingelbach,"Dutch, Frankfurt 1622–1674 Amsterdam",,"Lingelbach, Johannes",Dutch,1622,1674,1651,1651,1651,Oil on canvas,26 1/2 x 29 1/2 in. (67.3 x 74.9 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436889,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.65.2,false,true,436647,European Paintings,Painting,The Huis ten Bosch at The Hague and Its Formal Garden (View from the South),,,,,,Artist,,Jan van der Heyden,"Dutch, Gorinchem 1637–1712 Amsterdam",,"Heyden, Jan van der",Dutch,1637,1712,ca. 1668–70,1668,1670,Oil on wood,15 3/8 x 21 3/4 in. (39.1 x 55.2 cm),"Gift of Edith Neuman de Végvár, in honor of her husband, Charles Neuman de Végvár, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.65.3,false,true,436648,European Paintings,Painting,The Huis ten Bosch at The Hague and Its Formal Garden (View from the East),,,,,,Artist,,Jan van der Heyden,"Dutch, Gorinchem 1637–1712 Amsterdam",,"Heyden, Jan van der",Dutch,1637,1712,ca. 1668–70,1668,1670,Oil on wood,15 3/8 x 21 5/8 in. (39.1 x 54.9 cm),"Gift of Edith Neuman de Végvár, in honor of her husband, Charles Neuman de Végvár, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.69,false,true,436806,European Paintings,Painting,Interior of a Kitchen,,,,,,Artist,,Willem Kalf,"Dutch, Rotterdam 1619–1693 Amsterdam",,"Kalf, Willem",Dutch,1619,1693,ca. 1642–44,1642,1644,Oil on wood,10 1/2 x 12 1/2 in. (26.7 x 31.8 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.111,false,true,436805,European Paintings,Painting,"Still Life with Fruit, Glassware, and a Wanli Bowl",,,,,,Artist,,Willem Kalf,"Dutch, Rotterdam 1619–1693 Amsterdam",,"Kalf, Willem",Dutch,1619,1693,1659,1659,1659,Oil on canvas,23 x 20 in. (58.4 x 50.8 cm),"Maria DeWitt Jesup Fund, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436805,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.144,false,true,436677,European Paintings,Painting,Paying the Hostess,,,,,,Artist,,Pieter de Hooch,"Dutch, Rotterdam 1629–1684 Amsterdam",,"Hooch, Pieter de",Dutch,1629,1684,ca. 1670,1665,1675,Oil on canvas,37 1/4 x 43 3/4 in. (94.6 x 111.1 cm),"Gift of Stuart Borchard and Evelyn B. Metzger, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.7,false,true,436678,European Paintings,Painting,The Visit,,,,,,Artist,,Pieter de Hooch,"Dutch, Rotterdam 1629–1684 Amsterdam",,"Hooch, Pieter de",Dutch,1629,1684,ca. 1657,1652,1662,Oil on wood,26 3/4 x 23 in. (67.9 x 58.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436678,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.613,false,true,436675,European Paintings,Painting,Interior with a Young Couple,,,,,,Artist,,Pieter de Hooch,"Dutch, Rotterdam 1629–1684 Amsterdam",,"Hooch, Pieter de",Dutch,1629,1684,probably ca. 1662–65,1662,1665,Oil on canvas,21 5/8 x 24 3/4 in. (54.9 x 62.9 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436675,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.15,false,true,436676,European Paintings,Painting,"Woman with a Water Pitcher, and a Man by a Bed (""The Maidservant"")",,,,,,Artist,,Pieter de Hooch,"Dutch, Rotterdam 1629–1684 Amsterdam",,"Hooch, Pieter de",Dutch,1629,1684,ca. 1667–70,1667,1670,Oil on canvas,24 1/4 x 20 1/2 in. (61.5 x 52.1 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436676,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.25,false,true,436679,European Paintings,Painting,A Woman and Two Men in an Arbor,,,,,,Artist,,Pieter de Hooch,"Dutch, Rotterdam 1629–1684 Amsterdam",,"Hooch, Pieter de",Dutch,1629,1684,ca. 1657–58,1657,1658,Oil on wood,Overall 17 3/8 x 14 3/4 in. (44.1 x 37.5 cm); painted surface 17 x 14 3/8 in. (43.2 x 36.5 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436679,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.203.5,false,true,437227,European Paintings,Painting,The Love Letter,,,,,,Artist,,Jacob Ochtervelt,"Dutch, Rotterdam 1634–1682 Amsterdam",,"Ochtervelt, Jacob",Dutch,1634,1682,early 1670s,1670,1673,Oil on canvas,36 x 25 in. (91.4 x 63.5 cm),"Gift of Mr. and Mrs. Walter Mendelsohn, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.37,false,true,436060,European Paintings,Painting,Bacchus and Nymphs in a Landscape,,,,,,Artist,,Abraham van Cuylenborch,"Dutch, Utrecht ca. 1620–1658 Utrecht",,"Cuylenborch, Abraham van",Dutch,1620,1658,probably 1640s,1640,1649,Oil on wood,22 7/8 x 28 3/8 in. (58.1 x 72.1 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436060,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.125,false,true,435655,European Paintings,Painting,Rest,,,,,,Artist,,Nicolaes Berchem,"Dutch, Haarlem 1621/22–1683 Amsterdam",,"Berchem, Nicolaes",Dutch,1622,1683,1644,1644,1644,Oil on wood,17 x 13 1/2 in. (43.2 x 34.3 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.4,false,true,437546,European Paintings,Painting,The Forest Stream,,,,,,Artist,,Jacob van Ruisdael,"Dutch, Haarlem 1628/29–1682 Amsterdam",,"Ruisdael, Jacob van",Dutch,1628,1682,ca. 1660,1655,1665,Oil on canvas,39 1/4 x 50 7/8 in. (99.7 x 129.2 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.623,true,true,437549,European Paintings,Painting,Wheat Fields,,,,,,Artist,,Jacob van Ruisdael,"Dutch, Haarlem 1628/29–1682 Amsterdam",,"Ruisdael, Jacob van",Dutch,1628,1682,ca. 1670,1665,1675,Oil on canvas,39 3/8 x 51 1/4 in. (100 x 130.2 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437549,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.18,false,true,437548,European Paintings,Painting,Mountain Torrent,,,,,,Artist,,Jacob van Ruisdael,"Dutch, Haarlem 1628/29–1682 Amsterdam",,"Ruisdael, Jacob van",Dutch,1628,1682,1670s,1670,1679,Oil on canvas,21 1/4 x 16 1/2 in. (54 x 41.9 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437548,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.14,false,true,437547,European Paintings,Painting,Grainfields,,,,,,Artist,,Jacob van Ruisdael,"Dutch, Haarlem 1628/29–1682 Amsterdam",,"Ruisdael, Jacob van",Dutch,1628,1682,mid- or late 1660s,1664,1669,Oil on canvas,18 1/2 x 22 1/2 in. (47 x 57.2 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437547,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.181.10,false,true,437545,European Paintings,Painting,Landscape with a Village in the Distance,,,,,,Artist,,Jacob van Ruisdael,"Dutch, Haarlem 1628/29–1682 Amsterdam",,"Ruisdael, Jacob van",Dutch,1628,1682,1646,1646,1646,Oil on wood,30 x 43 in. (76.2 x 109.2 cm),"Bequest of Adele L. Lehman, in memory of Arthur Lehman, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437545,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.368,false,true,436672,European Paintings,Painting,Christ among the Doctors,,,,,,Artist,,Abraham Hondius,"Dutch, Rotterdam ca. 1631–1691 London",,"Hondius, Abraham",Dutch,1631,1691,1668,1668,1668,Oil on wood,15 x 19 1/2 in. (38.1 x 49.5 cm),"Gift of Dr. and Mrs. Carl F. Culicchia, 1974",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.403,false,true,438490,European Paintings,Painting,"Interior of the Oude Kerk, Delft",,,,,,Artist,,Emanuel de Witte,"Dutch, Alkmaar ca. 1616–1692 Amsterdam",,"Witte, Emanuel de",Dutch,1616,1692,probably 1650,1650,1650,Oil on wood,19 x 13 5/8 in. (48.3 x 34.6 cm),"Purchase, Lila Acheson Wallace, Virgilia and Walter C. Klein, The Walter C. Klein Foundation, Edwin Weisl Jr., and Frank E. Richardson Gifts, and Bequest of Theodore Rousseau and Gift of Lincoln Kirstein, by exchange, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438490,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.60,false,true,437190,European Paintings,Painting,The Farrier,,,,,,Artist,,Aert van der Neer,"Dutch, Gorinchem 1603/4–1677 Amsterdam",,"Neer, Aert van der",Dutch,1603,1677,early or mid-1650s,1650,1656,Oil on wood,19 x 24 1/8 in. (48.3 x 61.3 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.11,false,true,437191,European Paintings,Painting,Landscape at Sunset,,,,,,Artist,,Aert van der Neer,"Dutch, Gorinchem 1603/4–1677 Amsterdam",,"Neer, Aert van der",Dutch,1603,1677,1650s,1650,1659,Oil on canvas,20 x 28 1/8 in. (50.8 x 71.4 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.11,false,true,437192,European Paintings,Painting,Sports on a Frozen River,,,,,,Artist,,Aert van der Neer,"Dutch, Gorinchem 1603/4–1677 Amsterdam",,"Neer, Aert van der",Dutch,1603,1677,probably ca. 1660,1655,1665,Oil on wood,9 1/8 x 13 3/4 in. (23.2 x 34.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.6,false,true,437218,European Paintings,Painting,The Card Party,,,,,,Artist,,Caspar Netscher,"Dutch, Heidelberg 1639?–1684 The Hague",,"Netscher, Caspar",Dutch,1639,1684,ca. 1665,1660,1670,Oil on canvas,19 3/4 x 17 3/4 in. (50.2 x 45.1 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.155,false,true,436976,European Paintings,Painting,"Still Life with Poppy, Insects, and Reptiles",,,,,,Artist,,Otto Marseus van Schrieck,"Dutch, Nijmegen 1619/20–1678 Amsterdam",,"Marseus van Schrieck, Otto",Dutch,1619,1678,ca. 1670,1665,1675,Oil on canvas,26 7/8 x 20 3/4 in. (68.3 x 52.7 cm),"Rogers Fund, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.27,false,true,436630,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Adriaen Hanneman,"Dutch, The Hague 1603/4–1671 The Hague",,"Hanneman, Adriaen",Dutch,1603,1671,ca. 1653,1648,1658,Oil on canvas,31 1/2 x 25 in. (80 x 63.5 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.22,false,true,437282,European Paintings,Painting,Man with a Celestial Globe,,,,,,Artist,,Nicolaes Eliasz Pickenoy,"Dutch, Amsterdam 1588–1650/56 Amsterdam",,"Pickenoy, Nicolaes Eliasz",Dutch,1588,1656,1624,1624,1624,Oil on wood,41 1/4 x 30 in. (104.8 x 76.2 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437282,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.12,false,true,435723,European Paintings,Painting,Barnyard Scene,,,,,,Artist,,Anthonie van Borssom,"Dutch, Amsterdam 1630/31–1677 Amsterdam",,"Borssom, Anthonie van",Dutch,1631,1677,ca. 1650–55,1650,1655,Oil on canvas,20 x 27 in. (50.8 x 68.6 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1289,false,true,437090,European Paintings,Painting,"Charles I (1600–1649), King of England",,,,,,Artist,,Daniël Mijtens,"Dutch, Delft ca. 1590–1647/48 The Hague",,"Mijtens, Daniël",Dutch,1585,1648,1629,1629,1629,Oil on canvas,78 7/8 x 55 3/8 in. (200.3 x 140.7 cm),"Gift of George A. Hearn, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.7,false,true,437703,European Paintings,Painting,A Kitchen,,,,,,Artist,,Hendrick Sorgh,"Dutch, Rotterdam 1609/11–1670 Rotterdam",,"Sorgh, Hendrick",Dutch,1609,1670,ca. 1643,1638,1648,Oil on wood,20 1/2 x 17 3/8 in. (52.1 x 44.1 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.254,false,true,435663,European Paintings,Painting,Still Life with Lobster and Fruit,,,,,,Artist,,Abraham van Beyeren,"Dutch, The Hague 1620/21–1690 Overschie",,"Beyeren, Abraham van",Dutch,1620,1690,probably early 1650s,1650,1653,Oil on wood,38 x 31 in. (96.5 x 78.7 cm),"Gift of Edith Neuman de Végvár, in honor of her husband, Charles Neuman de Végvár, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.5,false,true,436784,European Paintings,Painting,Scene in a Courtyard,,,,,,Artist,,Ludolf de Jongh,"Dutch, Overschie 1616–1679 Hillegersberg",,"Jongh, Ludolf de",Dutch,1616,1679,early 1660s,1660,1663,Oil on canvas,26 1/2 x 32 3/8 in. (67.3 x 82.2 cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.30,true,true,437980,European Paintings,Painting,Cypresses,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1889,1889,1889,Oil on canvas,36 3/4 x 29 1/8 in. (93.4 x 74 cm),"Rogers Fund, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.41,false,true,436524,European Paintings,Painting,Sunflowers,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1887,1887,1887,Oil on canvas,17 x 24 in. (43.2 x 61 cm),"Rogers Fund, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436524,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.187,false,true,436528,European Paintings,Painting,Irises,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1890,1890,1890,Oil on canvas,29 x 36 1/4 in. (73.7 x 92.1 cm),"Gift of Adele R. Levy, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436528,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.393,false,true,436531,European Paintings,Painting,Peasant Woman Cooking by a Fireplace,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1885,1885,1885,Oil on canvas,17 3/8 x 15 in. (44.1 x 38.1 cm),"Gift of Mr. and Mrs. Mortimer Hays, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436531,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.132,true,true,436535,European Paintings,Painting,Wheat Field with Cypresses,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1889,1889,1889,Oil on canvas,28 7/8 × 36 3/4 in. (73.2 × 93.4 cm),"Purchase, The Annenberg Foundation Gift, 1993",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436535,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.535,false,true,436536,European Paintings,Painting,Women Picking Olives,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1889,1889,1889,Oil on canvas,28 5/8 x 36 in. (72.7 x 91.4 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1995, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436536,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.435,false,true,437984,European Paintings,Painting,"La Berceuse (Woman Rocking a Cradle; Augustine-Alix Pellicot Roulin, 1851–1930)",,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1889,1889,1889,Oil on canvas,36 1/2 x 29 in. (92.7 x 73.7 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1996, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.112.3,false,true,436529,European Paintings,Painting,"L'Arlésienne: Madame Joseph-Michel Ginoux (Marie Julien, 1848–1911)",,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1888–89,1888,1889,Oil on canvas,36 x 29 in. (91.4 x 73.7 cm),"Bequest of Sam A. Lewisohn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436529,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.165.2,false,true,436526,European Paintings,Painting,"First Steps, after Millet",,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1890,1890,1890,Oil on canvas,28 1/2 x 35 7/8 in. (72.4 x 91.1 cm),"Gift of George N. and Helen M. Richard, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436526,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.400.4,false,true,436525,European Paintings,Painting,Bouquet of Flowers in a Vase,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1890,1890,1890,Oil on canvas,25 5/8 x 21 1/4 in. (65.1 x 54 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1993, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436525,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.400.5,false,true,436534,European Paintings,Painting,Roses,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1890,1890,1890,Oil on canvas,36 5/8 x 29 1/8 in. (93 x 74 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1993, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436534,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.325.1,false,true,437998,European Paintings,Painting,Olive Trees,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1889,1853,1890,Oil on canvas,28 5/8 x 36 1/4 in. (72.7 x 92.1 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1998, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.70a,false,true,436532,European Paintings,Painting,Self-Portrait with a Straw Hat (obverse: The Potato Peeler),,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1887,1887,1887,Oil on canvas,16 x 12 1/2 in. (40.6 x 31.8 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436532,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.70b,false,true,438722,European Paintings,Painting,The Potato Peeler (reverse: Self-Portrait with a Straw Hat),,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1885,1885,1885,Oil on canvas,16 x 12 1/2 in. (40.6 x 31.8 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438722,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.6,false,true,436634,European Paintings,Painting,A Vase of Flowers,,,,,,Artist,,Margareta Haverman,"Dutch, active by 1716–died 1722 or later",,"Haverman, Margareta",Dutch,1716,1722,1716,1716,1716,Oil on wood,31 1/4 x 23 3/4 in. (79.4 x 60.3 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.170,false,true,437897,European Paintings,Painting,Abraham's Parting from the Family of Lot,,,,,,Artist,,Jan Victors,"Dutch, Amsterdam 1619–1676/77 East Indies",,"Victors, Jan",Dutch,1619,1677,ca. 1655–65,1655,1665,Oil on canvas,58 x 65 1/8 in. (147.3 x 165.4 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437897,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.55,false,true,437937,European Paintings,Painting,"Gamepiece with a Dead Heron (""Falconer's Bag"")",,,,,,Artist,,Jan Weenix,"Dutch, Amsterdam ca. 1641?–1719 Amsterdam",,"Weenix, Jan",Dutch,1641,1719,1695,1695,1695,Oil on canvas,52 3/4 x 43 3/4 in. (134 x 111.1 cm),"Rogers Fund, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437937,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.23,false,true,436288,European Paintings,Painting,Abraham Dismissing Hagar and Ishmael,,,,,,Artist,,Barent Fabritius,"Dutch, Middenbeemster 1624–1673 Amsterdam",,"Fabritius, Barent",Dutch,1624,1673,1658,1658,1658,Oil on wood,19 1/2 x 14 in. (49.5 x 35.6 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436288,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.609,false,true,437412,European Paintings,Painting,Old Woman Cutting Her Nails,,,,,,Artist,Style of,Rembrandt,"Dutch, second or third quarter 17th century",,Rembrandt,Dutch,1606,1669,ca. 1655–60,1655,1660,Oil on canvas,49 5/8 x 40 1/8 in. (126.1 x 101.9 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437412,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.261,false,true,435713,European Paintings,Painting,"The Disillusioned Medea (""The Enchantress"")",,,,,,Artist,,Paulus Bor,"Dutch, Amersfoort ca. 1601–1669 Amersfoort",,"Bor, Paulus",Dutch,1601,1669,ca. 1640,1635,1645,Oil on canvas,61 1/4 x 44 1/4 in. (155.6 x 112.4 cm),"Gift of Ben Heller, 1972",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.155.1,false,true,437352,European Paintings,Painting,A Party of Merrymakers,,,,,,Artist,,Pieter Jansz. Quast,"Dutch, Amsterdam (?) 1605/6–1647 Amsterdam",,"Quast, Pieter Jansz.",Dutch,1605,1647,ca. 1635–38,1635,1638,Oil on wood,14 3/4 x 19 1/2 in. (37.5 x 49.5 cm),"Bequest of Josephine Bieber, in memory of her husband, Siegfried Bieber, 1970",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437352,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.65.4,false,true,436818,European Paintings,Painting,A Musician and His Daughter,,,,,,Artist,,Thomas de Keyser,"Dutch, Amsterdam (?) 1596/97–1667 Amsterdam",,"Keyser, Thomas de",Dutch,1596,1667,1629,1629,1629,Oil on wood,29 1/2 x 20 3/4 in. (74.9 x 52.7 cm),"Gift of Edith Neuman de Végvár, in honor of her husband, Charles Neuman de Végvár, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.5,false,true,438377,European Paintings,Painting,Portrait of a Man with a Shell,,,,,,Artist,,Thomas de Keyser,"Dutch, Amsterdam (?) 1596/97–1667 Amsterdam",,"Keyser, Thomas de",Dutch,1596,1667,ca. 1625–26,1625,1626,Oil on wood,9 3/8 x 6 3/4 in. (23.8 x 17.1 cm),"From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.6,false,true,438378,European Paintings,Painting,Portrait of a Woman with a Balance,,,,,,Artist,,Thomas de Keyser,"Dutch, Amsterdam (?) 1596/97–1667 Amsterdam",,"Keyser, Thomas de",Dutch,1596,1667,ca. 1625–26,1625,1626,Oil on wood,9 1/8 x 6 7/8 in. (23.2 x 17.5 cm),"From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.202,false,true,437374,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Jan van Ravesteyn,"Dutch, Culemborg (?) ca. 1572–1657 The Hague",,"Ravesteyn, Jan van",Dutch,1572,1657,1635,1635,1635,Oil on wood,26 7/8 x 22 7/8 in. (68.3 x 58.1 cm),"Gift of Henry Goldman, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437374,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.39,false,true,436785,European Paintings,Painting,Honfleur,,,,,,Artist,,Johan Barthold Jongkind,"Dutch, Latrop 1819–1891 La-Côte-Saint-André",,"Jongkind, Johan Barthold",Dutch,1819,1891,1865,1865,1865,Oil on canvas,20 1/2 x 32 1/8 in. (52.1 x 81.6 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1916",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.652,false,true,438381,European Paintings,Painting,View from the Quai d'Orsay,,,,,,Artist,,Johan Barthold Jongkind,"Dutch, Latrop 1819–1891 La-Côte-Saint-André",,"Jongkind, Johan Barthold",Dutch,1819,1891,1854,1854,1854,"Oil on canvas, mounted on wood",17 1/4 x 26 in. (43.8 x 66 cm),"Bequest of Meta Cecile Schwarz, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.203.3,false,true,436787,European Paintings,Painting,The Pont Neuf,,,,,,Artist,,Johan Barthold Jongkind,"Dutch, Latrop 1819–1891 La-Côte-Saint-André",,"Jongkind, Johan Barthold",Dutch,1819,1891,1849–50,1849,1850,Oil on canvas,21 1/2 x 32 1/8 in. (54.6 x 81.6 cm),"Gift of Mr. and Mrs. Walter Mendelsohn, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1200,false,true,437914,European Paintings,Painting,Calm Sea,,,,,,Artist,,Simon de Vlieger,"Dutch, Rotterdam (?) ca. 1600/1601–1653 Weesp",,"Vlieger, Simon de",Dutch,1600,1653,after 1640,1640,1653,Oil on wood,14 3/4 x 17 1/2 in. (37.5 x 44.5 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.7,false,true,437945,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Willem Wissing,"Dutch, Amsterdam or The Hague 1656–1687 Stamford",,"Wissing, Willem",Dutch,1656,1687,ca. 1687,1682,1687,Oil on canvas,49 3/4 x 40 1/4 in. (126.4 x 102.2 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.110,false,true,435771,European Paintings,Painting,The Spinner,,,,,,Artist,,Quirijn van Brekelenkam,"Dutch, Zwammerdam (?), after 1622–ca. 1669 Leiden",,"Brekelenkam, Quirijn van",Dutch,1622,1669,1653,1653,1653,Oil on wood,19 x 25 1/4 in. (48.3 x 64.1 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435771,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.19,false,true,435772,European Paintings,Painting,Sentimental Conversation,,,,,,Artist,,Quirijn van Brekelenkam,"Dutch, Zwammerdam (?), after 1622–ca. 1669 Leiden",,"Brekelenkam, Quirijn van",Dutch,1622,1669,early 1660s,1660,1663,Oil on wood,16 1/4 x 13 7/8 in. (41.3 x 35.2 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.19,false,true,435918,European Paintings,Painting,Vanitas Still Life,,,,,,Artist,,Edwaert Collier,"Dutch, Breda ca. 1640?–after 1707 London or Leiden",,"Collier, Edwaert",Dutch,1640,1707,1662,1662,1662,Oil on wood,37 x 44 1/8 in. (94 x 112.1 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435918,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.162.1,false,true,437893,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Johannes Verspronck,"Dutch, Haarlem, born ca. 1601–3, died 1662 Haarlem",,"Verspronck, Johannes",Dutch,1601,1662,1645,1645,1645,Oil on canvas,31 1/4 x 25 1/4 in. (79.4 x 64.1 cm),"Bequest of Susan P. Colgate, in memory of her husband, Romulus R. Colgate, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.63,false,true,437921,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Abraham de Vries,"Dutch, The Hague (?) ca. 1590–1649/50 The Hague (?)",,"Vries, Abraham de",Dutch,1585,1650,1643,1643,1643,Oil on wood,25 1/4 x 21 in. (64.1 x 53.3 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +73.2,false,true,436642,European Paintings,Painting,The Musician,,,,,,Artist,,Bartholomeus van der Helst,"Dutch, Haarlem, born ca. 1612–15, died 1670 Amsterdam",,"Helst, Bartholomeus van der",Dutch,1612,1670,1662,1662,1662,Oil on canvas,54 1/2 x 43 3/4 in. (138.4 x 111.1 cm),"Purchase, 1873",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.73,false,true,436641,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Bartholomeus van der Helst,"Dutch, Haarlem, born ca. 1612–15, died 1670 Amsterdam",,"Helst, Bartholomeus van der",Dutch,1612,1670,1647,1647,1647,Oil on wood,"Oval, 26 1/4 x 21 5/8 in. (66.7 x 54.9 cm)","Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.75,false,true,437588,European Paintings,Painting,Drawing the Eel,,,,,,Artist,,Salomon van Ruysdael,"Dutch, Naarden, born ca. 1600–1603, died 1670 Haarlem",,"Ruysdael, Salomon van",Dutch,1600,1670,early 1650s,1650,1653,Oil on wood,29 1/2 x 41 3/4 in. (74.9 x 106 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.98,false,true,437587,European Paintings,Painting,Marine,,,,,,Artist,,Salomon van Ruysdael,"Dutch, Naarden, born ca. 1600–1603, died 1670 Haarlem",,"Ruysdael, Salomon van",Dutch,1600,1670,1650,1650,1650,Oil on wood,13 5/8 x 17 1/8 in. (34.6 x 43.5 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1201,false,true,437586,European Paintings,Painting,A Country Road,,,,,,Artist,,Salomon van Ruysdael,"Dutch, Naarden, born ca. 1600–1603, died 1670 Haarlem",,"Ruysdael, Salomon van",Dutch,1600,1670,1648,1648,1648,Oil on canvas,38 7/8 x 52 7/8 in. (98.7 x 134.3 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.4,false,true,437589,European Paintings,Painting,Ferry near Gorinchem,,,,,,Artist,,Salomon van Ruysdael,"Dutch, Naarden, born ca. 1600–1603, died 1670 Haarlem",,"Ruysdael, Salomon van",Dutch,1600,1670,1646,1646,1646,Oil on canvas,41 7/8 x 52 1/2 in. (106.4 x 133.4 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.55.4,false,true,437585,European Paintings,Painting,Market by the Seashore,,,,,,Artist,,Salomon van Ruysdael,"Dutch, Naarden, born ca. 1600–1603, died 1670 Haarlem",,"Ruysdael, Salomon van",Dutch,1600,1670,1637,1637,1637,Oil on wood,16 x 23 3/8 in. (40.6 x 59.4 cm),"Bequest of Rupert L. Joseph, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.8,false,true,438380,European Paintings,Painting,Fishing Boats on a River,,,,,,Artist,,Salomon van Ruysdael,"Dutch, Naarden, born ca. 1600–1603, died 1670 Haarlem",,"Ruysdael, Salomon van",Dutch,1600,1670,early 1660s,1660,1663,Oil on wood,"Overall, with added strips, 14 3/4 x 21 7/8 in. (37.5 x 55.6 cm); painted surface 14 1/4 x 21 1/4 in. (36.2 x 54 cm)","From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.102,false,true,436221,European Paintings,Painting,A Couple in an Interior with a Gypsy Fortune-Teller,,,,,,Artist,,Jacob Duck,"Dutch, Utrecht, born ca. 1598–1600, died 1667 Utrecht",,"Duck, Jacob",Dutch,1598,1667,ca. 1632–33,1632,1633,Oil on wood,"Oval, 9 7/8 x 13 in. (25.1 x 33 cm)","Gift of Dr. and Mrs. Richard W. Levy, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436221,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.331.1,false,true,438373,European Paintings,Painting,A Winter Landscape with Ice Skaters and an Imaginary Castle,,,,,,Artist,,Christoffel van den Berghe,"Dutch, Antwerp ca. 1590–1628 or later, active Middelburg",,"Berghe, Christoffel van den",Dutch,1590,1628,ca. 1615–20,1615,1620,Oil on wood,"Overall, with added strips, 11 x 18 3/8 in. (27.9 x 46.7 cm); painted surface 10 3/4 x 18 in. (27.3 x 45.7 cm)","From the Collection of Rita and Frits Markus, Bequest of Rita Markus, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438373,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.25,false,true,437757,European Paintings,Painting,Old Woman Praying,,,,,,Artist,,Matthias Stom,"Dutch, Amersfoort?, born ca. 1599–1600, died after 1652 ?Italy",,"Stom, Matthias",Dutch,1599,1652,late 1630s or early 1640s,1637,1643,Oil on canvas,30 5/8 x 25 1/8 in. (77.8 x 63.8 cm),"Gift of Ian Woodner, 1981",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.15,false,true,435625,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,David Baudringhien,"Dutch, ca. 1581–1650",,"Baudringhien, David",Dutch,1581,1650,,1601,1650,Oil on copper,"Oval, 3 3/4 x 3 in. (95 x 76 mm)","The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435625,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.222,false,true,437404,European Paintings,Painting,Head of Christ,,,,,,Artist,Style of,Rembrandt,"Dutch, 1650s",,Rembrandt,Dutch,1606,1669,,1626,1669,Oil on canvas,16 3/4 x 13 1/2 in. (42.5 x 34.3 cm); with added strips 18 5/8 x 14 5/8 in. (47.3 x 37.1 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437404,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.40,false,true,437882,European Paintings,Painting,A Young Woman Reading,,,,,,Artist,Imitator of,Johannes Vermeer,ca. 1925–27,,"Vermeer, Johannes",Dutch,1632,1675,,1900,1924,Oil on canvas,7 3/4 x 5 3/4 in. (19.7 x 14.6 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.14,false,true,437403,European Paintings,Painting,Christ and the Woman of Samaria,,,,,,Artist,Style of,Rembrandt,"Dutch, ca. 1655",,Rembrandt,Dutch,1606,1669,,1650,1660,Oil on wood,25 x 19 1/4 in. (63.5 x 48.9 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437403,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.257,false,true,437949,European Paintings,"Painting, grisaille",Children Playing with a Goat,,,,,,Artist,Style of,Jacob de Wit,"Dutch, 18th century",,"Wit, Jacob de",Dutch,1695,1754,,1715,1754,Oil on canvas,26 3/4 x 41 in. (67.9 x 104.1 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.16,false,true,435843,European Paintings,Painting,Winter Scene,,,,,,Artist,Style of,Jan van de Cappelle,18th or 19th century,,"Cappelle, Jan van de",Dutch,1626,1679,,1644,1679,Oil on oak,13 3/8 x 19 1/2 in. (34 x 49.5 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435843,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.18,false,true,437419,European Paintings,Painting,Rembrandt (1606–1669) as a Young Man,,,,,,Artist,Style of,Rembrandt,"Dutch, ca. 1630–35",,Rembrandt,Dutch,1606,1669,,1600,1699,Oil on wood,8 5/8 x 6 1/2 in. (21.9 x 16.5 cm),"Bequest of Evander B. Schley, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.3,false,true,437418,European Paintings,Painting,Man with a Beard,,,,,,Artist,Style of,Rembrandt,17th century or later,,Rembrandt,Dutch,1606,1669,,1600,1889,Oil on canvas,28 7/8 x 25 1/4 in. (73.3 x 64.1 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.608,false,true,437420,European Paintings,Painting,Rembrandt's Son Titus (1641–1668),,,,,,Artist,Style of,Rembrandt,17th century or later,,Rembrandt,Dutch,1606,1669,,1650,1883,Oil on canvas,31 1/8 x 23 1/4 in. (79.1 x 59.1 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.181.11,false,true,436560,European Paintings,Painting,River View with a Village Church,,,,,,Artist,Style of,Jan van Goyen,"Dutch, mid-17th century",,"Goyen, Jan van",Dutch,1596,1656,,1630,1669,Oil on canvas,25 1/2 x 38 1/2 in. (64.8 x 97.8 cm),"Bequest of Adele L. Lehman, in memory of Arthur Lehman, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436560,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.134.5,false,true,437072,European Paintings,Painting,Tavern Scene,,,,,,Artist,Copy after,Gabriël Metsu,"Dutch, late 17th century",,"Metsu, Gabriël",Dutch,1629,1667,,1649,1667,Oil on wood,14 3/8 x 12 5/8 in. (36.5 x 32.1 cm),"Bequest of William H. Herriman, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437072,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.16,false,true,437421,European Paintings,Painting,Study Head of an Old Man,,,,,,Artist,Style of,Rembrandt,"Dutch, mid- to late 1630s",,Rembrandt,Dutch,1606,1669,,1600,1699,Oil on wood,8 1/4 x 6 7/8 in. (21 x 17.5 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.102,false,true,437410,European Paintings,Painting,Portrait of a Man with a Breastplate and Plumed Hat,,,,,,Artist,Style of,Rembrandt,"Dutch, mid- to late 1640s",,Rembrandt,Dutch,1606,1669,,1625,1674,Oil on canvas,47 3/4 x 38 3/4 in. (121.3 x 98.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437410,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.103,false,true,437409,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,Style of,Rembrandt,"Dutch, mid- to late 1640s",,Rembrandt,Dutch,1606,1669,,1625,1674,Oil on canvas,47 5/8 x 38 5/8 in. (121 x 98.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437409,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.36,false,true,437413,European Paintings,Painting,Man in a Red Cloak,,,,,,Artist,Style of,Rembrandt,"Dutch, 1650s or early 1660s",,Rembrandt,Dutch,1606,1669,,1625,1674,Oil on wood,15 1/8 x 12 1/4 in. (38.4 x 31.1 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437413,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.198,false,true,437233,European Paintings,Painting,Man with a Tankard,,,,,,Artist,Style of,Adriaen van Ostade,"Dutch, second half 17th century",,"Ostade, Adriaen van",Dutch,1610,1685,,1650,1699,Oil on wood,10 1/8 x 8 1/2 in. (25.7 x 21.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.88,false,true,437043,European Paintings,Painting,A Shepherdess and Her Flock,,,,,,Artist,,Anton Mauve,"Dutch, Zaandam 1838–1888 Arnhem",,"Mauve, Anton",Dutch,1838,1888,,1858,1888,Oil on canvas,17 7/8 x 25 1/4 in. (45.4 x 64.1 cm),"Gift of Cole J. Younger, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.86.8,false,true,437041,European Paintings,Painting,Gathering Wood,,,,,,Artist,,Anton Mauve,"Dutch, Zaandam 1838–1888 Arnhem",,"Mauve, Anton",Dutch,1838,1888,,1858,1888,Oil on canvas,16 1/2 x 13 in. (41.9 x 33 cm),"Bequest of Richard De Wolfe Brixey, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.812,false,true,437044,European Paintings,Painting,Twilight,,,,,,Artist,,Anton Mauve,"Dutch, Zaandam 1838–1888 Arnhem",,"Mauve, Anton",Dutch,1838,1888,,1858,1888,Oil on canvas,25 7/8 x 17 7/8 in. (65.7 x 45.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.816,false,true,437042,European Paintings,Painting,The Return to the Fold,,,,,,Artist,,Anton Mauve,"Dutch, Zaandam 1838–1888 Arnhem",,"Mauve, Anton",Dutch,1838,1888,,1858,1888,Oil on canvas,19 3/4 x 33 7/8 in. (50.2 x 86 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.37,false,true,437407,European Paintings,Painting,Christ with a Staff,,,,,,Artist,Follower of,Rembrandt,"Dutch, third quarter 17th century",,Rembrandt,Dutch,1606,1669,,1650,1674,Oil on canvas,37 1/2 x 32 1/2 in. (95.3 x 82.6 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437407,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.15,false,true,437405,European Paintings,Painting,"A Young Woman as a Shepherdess (""Saskia as Flora"")",,,,,,Artist,,Govert Flinck,"Dutch, Cleve 1615–1660 Amsterdam",,"Flinck, Govert",Dutch,1615,1660,,1635,1660,"Oil on canvas, transferred from wood","Oval, 26 1/4 x 19 7/8 in. (66.7 x 50.5 cm)","Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.373,false,true,437408,European Paintings,Painting,Man in a Beret,,,,,,Artist,Style of,Rembrandt,"Dutch, fourth quarter 17th century",,Rembrandt,Dutch,1606,1669,,1675,1699,Oil on canvas,29 7/8 x 24 3/4 in. (75.9 x 62.9 cm),"Gift of Charles S. Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437408,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.76,false,true,436628,European Paintings,Painting,Malle Babbe,,,,,,Artist,Style of,Frans Hals,"Dutch, second quarter 17th century",,"Hals, Frans",Dutch,1582,1666,,1625,1649,Oil on canvas,29 1/2 x 24 in. (74.9 x 61 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.2,false,true,435597,European Paintings,Painting,Portrait of an Old Woman,,,,,,Artist,Style of,Jacob Backer,"Dutch, second quarter 17th century",,"Backer, Jacob",Dutch,1608,1651,,1625,1649,Oil on wood,28 x 24 in. (71.1 x 61 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435597,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.116,false,true,437922,European Paintings,Painting,The Pigeon House,,,,,,Artist,,Roelof van Vries,"Dutch, Haarlem 1630/31–after 1681",,"Vries, Roelof van",Dutch,1630,1681,,1650,1681,Oil on canvas,14 1/2 x 12 in. (36.8 x 30.5 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.33,false,true,437417,European Paintings,Painting,"Lieven Willemsz van Coppenol (born about 1599, died 1671 or later)",,,,,,Artist,,Rembrandt (Rembrandt van Rijn),"Dutch, Leiden 1606–1669 Amsterdam",,Rembrandt (Rembrandt van Rijn),Dutch,1606,1669,,1626,1669,Oil on wood,14 3/8 x 11 3/8 in. (36.5 x 28.9 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.136.3,false,true,436974,European Paintings,Painting,Canal Side,,,,,,Artist,,Jacob Maris,"Dutch, The Hague 1837–1899 Karlsbad",,"Maris, Jacob",Dutch,1837,1899,,1857,1899,Oil on canvas,5 3/8 x 7 in. (13.7 x 17.8 cm),"Bequest of Margaret Crane Hurlbut, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.92,false,true,435633,European Paintings,Painting,"Skating at Sloten, near Amsterdam",,,,,,Artist,,Johannes Abrahamsz Beerstraten,"Dutch, Amsterdam 1622–1666 Amsterdam",,"Beerstraten, Johannes Abrahamsz",Dutch,1622,1666,,1642,1666,Oil on canvas,36 1/4 x 51 5/8 in. (92.1 x 131.1 cm),"Rogers Fund, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.296,false,true,437948,European Paintings,"Painting, sketch for a ceiling decoration",Allegory of Government: Wisdom Defeating Discord,,,,,,Artist,,Jacob de Wit,"Dutch, Amsterdam 1695–1754 Amsterdam",,"Wit, Jacob de",Dutch,1695,1754,,1715,1754,Oil on canvas,20 1/8 x 15 3/8 in. (51.1 x 39.1 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.149.2,false,true,436931,European Paintings,Painting,"Admiral Jacob Binkes (born about 1640, died 1677)",,,,,,Artist,,Nicolaes Maes,"Dutch, Dordrecht 1634–1693 Amsterdam",,"Maes, Nicolaes",Dutch,1634,1693,,1654,1693,Oil on canvas,17 1/4 x 12 7/8 in. (43.8 x 32.7 cm),"Gift of J. Pierpont Morgan, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436931,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.603,false,true,435596,European Paintings,Painting,Old Woman in an Armchair,,,,,,Artist,Attributed to,Jacob Backer,"Dutch, Harlingen 1608–1651 Amsterdam",,"Backer, Jacob",Dutch,1608,1651,,1629,1639,Oil on canvas,50 3/8 x 39 1/8 in. (128 x 99.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.629,false,true,436212,European Paintings,Painting,Young Woman with a Pearl Necklace,,,,,,Artist,Copy after,Willem Drost,"Dutch, late 17th or early 18th century",,"Drost, Willem",Dutch,1633,1659,,1670,1729,Oil on canvas,33 1/8 x 24 1/2 in. (84.1 x 62.2 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436212,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.260,false,true,437171,European Paintings,Painting,The Old Castle,,,,,,Artist,,Emanuel Murant,"Dutch, Amsterdam 1622–1700 Leeuwarden",,"Murant, Emanuel",Dutch,1622,1700,,1642,1700,Oil on wood,15 5/8 x 21 7/8 in. (39.7 x 55.6 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.8.13,false,true,436724,European Paintings,Painting,Expectation,,,,,,Artist,,Jozef Israëls,"Dutch, Groningen 1824–1911 Scheveningen",,"Israëls, Jozef",Dutch,1824,1911,,1844,1911,Oil on canvas,71 1/2 x 54 in. (181.6 x 137.2 cm),"Gift of George I. Seney, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.227,false,true,436725,European Paintings,Painting,Grandmother's Treasure,,,,,,Artist,,Jozef Israëls,"Dutch, Groningen 1824–1911 Scheveningen",,"Israëls, Jozef",Dutch,1824,1911,,1844,1911,Oil on canvas,27 x 35 5/8 in. (68.6 x 90.5 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.9,false,true,437193,European Paintings,Painting,The Reader,,,,,,Artist,,Eglon van der Neer,"Dutch, Amsterdam 1635/36–1703 Düsseldorf",,"Neer, Eglon van der",Dutch,1635,1703,,1654,1703,Oil on canvas,15 x 11 in. (38.1 x 27.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.84,false,true,437416,European Paintings,Painting,Man in Armor (Mars?),,,,,,Artist,Style of,Rembrandt,"Dutch, second or third quarter 17th century",,Rembrandt,Dutch,1606,1669,,1625,1674,Oil on canvas,40 1/8 x 35 5/8 in. (101.9 x 90.5 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437416,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.601,false,true,437411,European Paintings,Painting,Man with a Steel Gorget,,,,,,Artist,Style of,Rembrandt,"Dutch, second or third quarter 17th century",,Rembrandt,Dutch,1606,1669,,1625,1674,Oil on canvas,37 1/8 x 30 5/8 in. (94.3 x 77.8 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437411,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.255,false,true,437351,European Paintings,Painting,Paul and Barnabas at Lystra,,,,,,Artist,,Jacob Pynas,"Dutch, Amsterdam 1592/93–after 1650 Amsterdam (?)",,"Pynas, Jacob",Dutch,1592,1650,,1605,1650,Oil on wood,19 x 28 7/8 in. (48.3 x 73.3 cm),"Gift of Emile E. Wolf, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437351,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.135,false,true,437590,European Paintings,Painting,View of the Town of Alkmaar,,,,,,Artist,,Salomon van Ruysdael,"Dutch, Naarden, born ca. 1600–1603, died 1670 Haarlem",,"Ruysdael, Salomon van",Dutch,1600,1670,,1620,1670,Oil on wood,20 1/4 x 33 in. (51.4 x 83.8 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.13,false,true,436527,European Paintings,Painting,The Flowering Orchard,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1888,1888,1888,Oil on canvas,28 1/2 x 21 in. (72.4 x 53.3 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436527,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.24,false,true,436530,European Paintings,Painting,Oleanders,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1888,1888,1888,Oil on canvas,23 3/4 x 29 in. (60.3 x 73.7 cm),"Gift of Mr. and Mrs. John L. Loeb, 1962",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436530,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.374,false,true,436533,European Paintings,Painting,Shoes,,,,,,Artist,,Vincent van Gogh,"Dutch, Zundert 1853–1890 Auvers-sur-Oise",,"Gogh, Vincent van",Dutch,1853,1890,1888,1888,1888,Oil on canvas,18 x 21 3/4 in. (45.7 x 55.2 cm),"Purchase, The Annenberg Foundation Gift, 1992",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436533,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.20,false,true,437253,European Paintings,"Painting, miniature",Abbé Charles Bossut (1730–1814),,,,,,Artist,,Pierre Pasquier,"French, 1731–1806",,"Pasquier, Pierre",French,1731,1806,1772,1772,1772,Enamel,"Oval, 2 3/8 x 2 in. (62 x 50 mm)","Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437253,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.32,false,true,437254,European Paintings,"Painting, miniature",Gérard de Vesme,,,,,,Artist,,Pierre Pasquier,"French, 1731–1806",,"Pasquier, Pierre",French,1731,1806,177(3?),1771,1779,Enamel,"Oval, 2 1/8 x 1 3/4 in. (54 x 44 mm)","Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437254,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.14,false,true,436328,European Paintings,"Painting, miniature",Portrait of a Boy,,,,,,Artist,,Marie Anne Gérard Fragonard (Madame Fragonard),"French, 1745–1823",,"Fragonard, Marie Anne Gérard (Madame Fragonard)",French,1745,1823,ca. 1775,1770,1780,Ivory,"Oval, 2 7/8 x 2 3/8 in. (73 x 59 mm)","Rogers Fund, 1960",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.77,false,true,436605,European Paintings,"Painting, miniature","Alexandre Théodore Victor (1760–1829), Comte de Lameth",,,,,,Artist,,Jean Urbain Guérin,"French, 1761–1836",,"Guérin, Jean Urbain",French,1761,1836,ca. 1789–90,1789,1790,Ivory laid on card,Diameter 3 in. (75 mm),"Rogers Fund, 1961",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.87,false,true,436606,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Jean Urbain Guérin,"French, 1761–1836",,"Guérin, Jean Urbain",French,1761,1836,ca. 1815,1810,1820,Ivory,"Oval, 2 3/8 x 1 3/4 in. (59 x 46 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.9,false,true,436855,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Étienne Charles Le Guay,"French, 1762–1846",,"Le Guay, Étienne Charles",French,1762,1846,ca. 1800,1795,1805,Ivory,"Oval, 3 1/2 x 3 in. (90 x 75 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.16,false,true,436848,European Paintings,"Painting, miniature",Portrait of a Young Woman,,,,,,Artist,Attributed to,Jean Antoine Laurent,"French, 1763–1832",,"Laurent, Jean Antoine",French,1763,1832,ca. 1795,1790,1800,Ivory,"Octagonal, 2 3/8 x 2 7/8 in. (63 x 73 mm)","Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.11,false,true,436862,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Jeanne Philiberte Ledoux,"French, 1767–1840",,"Ledoux, Jeanne Philiberte",French,1767,1840,ca. 1790,1785,1795,Ivory,Diameter 2 1/4 in. (57 mm),"Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.108,false,true,436924,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Ferdinand Machéra,"French, 1776–1843",,"Machéra, Ferdinand",French,1776,1843,1827,1827,1827,Ivory,"Oval, 2 1/4 x 1 5/8 in. (57 x 43 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436924,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.742,false,true,436768,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Nicolas Henri Jacob,"French, 1782–1871",,"Jacob, Nicolas Henri",French,1782,1871,1817,1817,1817,Card,"Oval, 8 1/4 x 6 3/4 in. (208 x 171 mm)","Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436768,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.90,false,true,435841,European Paintings,"Painting, miniature",Baron Joseph Dominique Louis (1755–1837),,,,,,Artist,,Pierre Laurent Canon,"French, 1787–1852",,"Canon, Pierre Laurent",French,1787,1852,1844,1844,1844,Ivory,"Oval, 4 1/8 x 3 1/8 in. (105 x 80 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435841,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.90,false,true,437486,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,François-Théodore Rochard,"French, 1798–1858",,"Rochard, François-Théodore",French,1798,1858,1829,1829,1829,Ivory,3 7/8 x 3 1/4 in. (98 x 83 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437486,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.53.1,false,true,437075,European Paintings,"Painting, miniature","Portrait of a Woman, Said to Be Marie de Mautesson",,,,,,Artist,,François Meuret,"French, 1800–1887",,"Meuret, François",French,1800,1887,ca. 1830,1825,1835,Ivory,2 1/4 x 1 7/8 in. (58 x 47 mm),"Gift of Helen O. Brice, 1942",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.533,false,true,435767,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,Attributed to,Joseph Boze,"French, 1745–1825/26",,"Boze, Joseph",French,1745,1826,probably ca. 1790,1785,1795,Ivory,"Oval, 1 7/8 x 1 1/2 in. (47 x 38 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435767,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.43.298,false,true,437907,European Paintings,"Painting, miniature",Madame Ingouf,,,,,,Artist,,Vincent,"French, active ca. 1790",,Vincent,French,1790,1790,ca. 1790,1785,1795,Ivory,Diameter 4 1/8 in. (105 mm),"Bequest of Mary Anna Palmer Draper, 1914",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.222.9,false,true,436109,European Paintings,"Painting, miniature","Benjamin Franklin (1706–1790), after a Painting by Greuze of 1777",,,,,,Artist,,Charles Paul Jérôme de Bréa,"French, ca. 1739–1820",,"Bréa, Charles Paul Jérôme de",French,1739,1820,1777,1777,1777,Ivory,"Oval, 3 3/8 x 2 3/4 in. (85 x 68 mm)","Gift of J. William Middendorf II, 1968",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.67,false,true,436110,European Paintings,"Painting, miniature","Marie-Thérèse-Charlotte (1778–1851), Daughter of Louis XVI",,,,,,Artist,,Jacques Joseph de Gault,"French, 1738–after 1812",,"Gault, Jacques Joseph de",French,1738,1812,1795,1795,1795,Ivory,Diameter 2 3/8 in. (60 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.94,false,true,436919,European Paintings,"Painting, miniature","Portrait of a Man, Said to Be James Madison (1751–1836)",,,,,,Artist,,Annibal Christian Loutherbourg,"French, 1765–after 1795",,"Loutherbourg, Annibal Christian",French,1765,1795,1795,1795,1795,Ivory,"Oval, 2 1/2 x 2 1/4 in. (65 x 56 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.136.8,false,true,436181,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Jacques Delaplace,"French, 1767–after 1831",,"Delaplace, Jacques",French,1767,1831,ca. 1805–10,1805,1810,Ivory extended by card,2 1/2 x 2 1/4 in. (64 x 55 mm),"Bequest of Margaret Crane Hurlbut, 1933",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.110.1,false,true,435662,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Vincent Bertrand,"French, 1770–after 1817",,"Bertrand, Vincent",French,1770,1817,ca. 1810,1805,1815,Ivory,4 1/4 x 4 in. (110 x 100 mm),"Gift of Mrs. John LaPorte Given, 1945",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.150.15,false,true,435661,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Vincent Bertrand,"French, 1770–after 1817",,"Bertrand, Vincent",French,1770,1817,ca. 1810,1805,1815,Ivory,"Oval, 2 5/8 x 2 in. (58 x 48 mm)","Gift of Mrs. Heyward Cutting, 1942",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.51,false,true,435719,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Joseph Bordes,"French, 1773–after 1835",,"Bordes, Joseph",French,1773,1835,1808,1808,1808,Ivory,"Octagonal, 4 1/8 x 3 1/8 in. (106 x 80 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435719,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.83,false,true,435721,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Joseph Bordes,"French, 1773–after 1835",,"Bordes, Joseph",French,1773,1835,ca. 1810,1805,1815,Ivory set into card,"Oval, 7 1/8 x 5 7/8 in. (181 x 149 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435721,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.86,false,true,435720,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Joseph Bordes,"French, 1773–after 1835",,"Bordes, Joseph",French,1773,1835,1812,1812,1812,Ivory,"Oval, 4 x 3 1/8 in. (104 x 82 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.525,false,true,437627,European Paintings,"Painting, miniature",A River Landscape,,,,,,Artist,,Edmé Charles de Lioux de Savignac,"French, active ca. 1766–72",,"Savignac, Edmé Charles de Lioux de",French,1766,1772,ca. 1766–72,1766,1772,Paper,Diameter 3 in. (77 mm),"Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.527,false,true,437628,European Paintings,"Painting, miniature",A Picnic,,,,,,Artist,,Edmé Charles de Lioux de Savignac,"French, active ca. 1766–72",,"Savignac, Edmé Charles de Lioux de",French,1766,1772,ca. 1766–72,1766,1772,Paper,Diameter 3 in. (77 mm),"Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437628,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.21,false,true,437905,European Paintings,"Painting, miniature",A Man with the Initials JD,,,,,,Artist,,Villers,"French, active ca. 1781–93",,Villers,French,1781,1793,1790,1790,1790,Ivory,Diameter 2 1/2 in. (62 mm),"Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.55,false,true,437904,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Villers,"French, active ca. 1781–93",,Villers,French,1781,1793,ca. 1790,1785,1793,Ivory,"Oval, 2 3/8 x 1 7/8 in. (60 x 48 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437904,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.35,false,true,436860,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,J. Lecourt,"French, active ca. 1804–30",,"Lecourt, J.",French,1804,1830,ca. 1810,1805,1815,Ivory,Diameter 2 in. (50 mm),"Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.7,false,true,437292,European Paintings,"Painting, drawing",A Shipwreck in a Storm,,,,,,Artist,,Jean Pillement,"French, Lyons 1728–1808 Lyons",,"Pillement, Jean",French,1728,1808,1782,1782,1782,Pastel on gessoed canvas,24 3/4 x 36 in. (62.9 x 91.4 cm),"Gift of Martin Birnbaum, 1956",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.60,false,true,436717,European Paintings,"Painting, miniature","Mrs. Rufus Prime (Augusta Temple Palmer, 1807–1840)",,,,,,Artist,,Jean-Baptiste Isabey,"French, Nancy 1767–1855 Paris",,"Isabey, Jean-Baptiste",French,1767,1855,1828,1828,1828,Card,"Oval, 5 3/8 x 4 in. (138 x 102 mm)","Gift of Cornelia Prime, 1908",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436717,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.53.5,false,true,436715,European Paintings,"Painting, miniature",Napoléon I (1769–1821),,,,,,Artist,,Jean-Baptiste Isabey,"French, Nancy 1767–1855 Paris",,"Isabey, Jean-Baptiste",French,1767,1855,1812,1812,1812,Ivory,"Oval, 2 1/4 x 1 3/8 in. (56 x 36 mm)","Gift of Helen O. Brice, 1942",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436715,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.33.3,false,true,436714,European Paintings,"Painting, miniature",Napoléon I (1769–1821),,,,,,Artist,,Jean-Baptiste Isabey,"French, Nancy 1767–1855 Paris",,"Isabey, Jean-Baptiste",French,1767,1855,1810,1810,1810,Ivory,"Oval, 2 x 1 1/8 in. (50 x 30 mm)","Gift of Junius S. and Henry S. Morgan, 1947",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436714,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.7,false,true,436710,European Paintings,"Painting, miniature",The Reader,,,,,,Artist,,Jean-Baptiste Isabey,"French, Nancy 1767–1855 Paris",,"Isabey, Jean-Baptiste",French,1767,1855,1790,1790,1790,Ivory,Diameter 3 1/8 in. (79 mm),"Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436710,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.20,false,true,436713,European Paintings,"Painting, miniature","Madame Jean-Baptiste Isabey (Jeanne Laurice de Salienne, died 1829)",,,,,,Artist,,Jean-Baptiste Isabey,"French, Nancy 1767–1855 Paris",,"Isabey, Jean-Baptiste",French,1767,1855,ca. 1796–1800,1796,1800,Ivory,Diameter 3 3/8 in. (86 mm),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436713,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.111.2,false,true,436718,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Jean-Baptiste Isabey,"French, Nancy 1767–1855 Paris",,"Isabey, Jean-Baptiste",French,1767,1855,ca. 1815,1810,1820,Ivory,"Oval, 3 x 2 1/4 in. (76 x 56 mm)","Bequest of Helen Winslow Durkee Mileham, 1954",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436718,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.88,false,true,436711,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Jean-Baptiste Isabey,"French, Nancy 1767–1855 Paris",,"Isabey, Jean-Baptiste",French,1767,1855,ca. 1795,1790,1800,Ivory,Diameter 2 7/8 in. (73 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.84,false,true,441183,European Paintings,"Painting, drawing","Double Portrait Presumed to Represent François de Jullienne (1722–1754) and His Wife (Marie Élisabeth de Séré de Rieux, 1724–1795)",,,,,,Artist,,Charles Antoine Coypel,"French, Paris 1694–1752 Paris",,"Coypel, Charles Antoine",French,1694,1752,1743,1743,1743,"Pastel, black chalk, watercolor, and traces of black chalk underdrawing on four joined sheets of handmade blue laid paper, mounted on canvas and adhered to a keyed stretcher",39 3/8 x 31 1/2 in. (100 x 80 cm),"Purchase, Mrs. Charles Wrightsman Gift, in honor of Annette de la Renta, 2011",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/441183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.383,false,true,437596,European Paintings,"Painting, drawing",The Two Sisters,,,,,,Artist,,"Jean Claude Richard, Abbé de Saint-Non","French, Paris 1727–1791 Paris",,"Saint-Non, Jean Claude Richard, Abbé de",French,1727,1791,1770,1770,1770,"Pastel on paper, laid down on canvas",31 5/8 x 25 in. (80.3 x 63.5 cm),"Gift of Daniel Wildenstein, 1977",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437596,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.10,false,true,435834,European Paintings,"Painting, miniature","Louis XVI (1754–1793), King of France",,,,,,Artist,Attributed to,Antoine François Callet,"French, Paris 1741–1823 Paris",,"Callet, Antoine François",French,1741,1823,1787,1787,1787,Ivory,Diameter 2 5/8 in. (67 mm),"The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.441,false,true,439405,European Paintings,"Painting, drawing",Madame Élisabeth de France (1764–1794),,,,,,Artist,,Adélaïde Labille-Guiard,"French, Paris 1749–1803 Paris",,"Labille-Guiard, Adélaïde",French,1749,1803,ca. 1787,1782,1792,"Pastel on blue paper, seven sheets joined, laid down on canvas","Oval, 31 x 25 3/4 in. (78.7 x 65.4 cm.)","Gift of Mrs. Frederick M. Stafford, 2007",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/439405,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.4,false,true,435736,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Étienne Bouchardy,"French, Paris 1797–1849 Paris",,"Bouchardy, Étienne",French,1797,1849,1832,1832,1832,Ivory laid on card,"Oval, 3 1/4 x 2 5/8 in. (82 x 67 mm)","Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435736,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.136.7,false,true,435735,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Étienne Bouchardy,"French, Paris 1797–1849 Paris",,"Bouchardy, Étienne",French,1797,1849,1838,1838,1838,Ivory,4 7/8 x 4 1/8 in. (125 x 104 mm),"Bequest of Margaret Crane Hurlbut, 1933",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435735,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.391.1,false,true,437993,European Paintings,"Painting, drawing",At the Milliner's,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1881,1881,1881,"Pastel on five pieces of wove paper, backed with paper, and laid down on canvas",27 1/4 x 27 1/4 in. (69.2 x 69.2 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1997, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.13,false,true,437668,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Louis Marie Sicardi,"French, Avignon 1743–1825 Paris",,"Sicardi, Louis Marie",French,1743,1825,ca. 1780,1775,1785,Ivory,"Oval, 1 3/8 x 1 1/4 in. (36 x 32 mm)","Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.26,false,true,438615,European Paintings,"Painting, drawing",Olivier Journu (1724–1764),,,,,,Artist,,Jean-Baptiste Perronneau,"French, Paris 1715–1783 Amsterdam",,"Perronneau, Jean-Baptiste",French,1715,1783,1756,1756,1756,"Pastel on blue-gray laid paper, laid down on canvas",22 7/8 x 18 1/2 in. (58.1 x 47 cm),"Wrightsman Fund, 2003",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/438615,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.17,false,true,436233,European Paintings,"Painting, miniature","Portrait of a Woman, Said to Be Madame Récamier (1777–1849)",,,,,,Artist,,Nicolas François Dun,"French, Lunéville 1764–1832 Naples",,"Dun, Nicolas François",French,1764,1832,ca. 1812–14,1812,1814,Ivory,"Oval, 2 3/8 x 1 7/8 in. (60 x 48 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436233,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.287,false,true,436349,European Paintings,"Painting, drawing",Panthers of Bacchus Eating Grapes,,,,,,Artist,,Alexandre François Desportes,"French, Champigneulle 1661–1743 Paris",,"Desportes, Alexandre François",French,1661,1743,ca. 1719–20,1719,1720,"Oil on paper, laid down on card (paste-paper)",13 5/8 x 6 3/4 in. (34.6 x 17.1 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/436349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.69,false,true,437167,European Paintings,"Painting, miniature","Louis XVI (1754–1793), King of France",,,,,,Artist,,Jean Laurent Mosnier,"French, Paris 1743/44–1808 St. Petersburg",,"Mosnier, Jean Laurent",French,1743,1808,1790,1790,1790,Ivory,Diameter 2 3/4 in. (69 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.89.1,false,true,435915,European Paintings,"Painting, miniature","Charles de Cossé (1506–1563), Count of Brissac",,,,,,Artist,,Jean Clouet,"French, active by 1516–died 1540/41 Paris",,"Clouet, Jean",French,1516,1541,ca. 1535,1530,1540,Vellum,Diameter 1 1/2 in. (37 mm),"Fletcher Fund, 1935",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435915,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.72.1,false,true,437594,European Paintings,"Painting, miniature",Portrait of a Churchman,,,,,,Artist,,E. Jean Saillant,"French, active by 1620–died in or after 1638",,"Saillant, E. Jean",French,1620,1638,1628,1628,1628,Vellum stretched over copper,"Octagonal, 6 1/8 x 4 3/4 in. (157 x 122 mm)","Rogers Fund, 1959",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437594,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.72.2,false,true,437595,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,E. Jean Saillant,"French, active by 1620–died in or after 1638",,"Saillant, E. Jean",French,1620,1638,ca. 1628,1623,1633,Vellum stretched over wood,"Octagonal, 6 3/4 x 5 5/8 in. (170 x 142 mm)","Rogers Fund, 1959",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437595,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.73,false,true,437353,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Edme Quenedey,"French, Riceys-le-Haut (Aube) 1756–1830 Paris",,"Quenedey, Edme",French,1756,1830,ca. 1780,1775,1785,Ivory,Diameter 2 in. (49 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437353,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.439,false,true,438585,European Paintings,"Painting, drawing",Jean Charles Garnier d'Isle (1697–1755),,,,,,Artist,,Maurice Quentin de La Tour,"French, Saint-Quentin 1704–1788 Saint-Quentin",,"La Tour, Maurice Quentin de",French,1704,1788,ca. 1750,1745,1755,"Pastel and gouache on blue paper, laid down on canvas",25 3/8 x 21 1/4 in. (64.5 x 54 cm),"Purchase, Walter and Leonore Annenberg and The Annenberg Foundation Gift, 2002",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/438585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.46.1,false,true,437345,European Paintings,Drawing,Cider,,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,ca. 1864,1859,1869,"Oil on paper, laid down on canvas",51 x 99 1/4 in. (129.5 x 252.1 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1926",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437345,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.46.2,false,true,437348,European Paintings,Drawing,The River,,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,ca. 1864,1859,1869,"Oil on paper, laid down on canvas",51 x 99 1/4 in. (129.5 x 252.1 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1926",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437348,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.27.2,false,true,435974,European Paintings,Drawing,Lake Albano and Castel Gandolfo,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1826–27,1826,1827,"Oil on paper, laid down on wood",9 x 15 1/2 in. (22.9 x 39.4 cm),"Purchase, Dikran G. Kelekian Gift, 1922",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/435974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.71.2,false,true,435973,European Paintings,Drawing,Italian Landscape,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1825–28,1825,1828,"Oil on paper, laid down on canvas",5 x 10 5/8 in. (12.7 x 27 cm),"Gift of Mr. and Mrs. William B. Jaffe, 1950",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/435973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.404,false,true,435970,European Paintings,Drawing,Fontainebleau: Oak Trees at Bas-Bréau,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1832 or 1833,1832,1833,"Oil on paper, laid down on wood",15 5/8 x 19 1/2 in. (39.7 x 49.5 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1979",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/435970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.296,false,true,435660,European Paintings,Drawing,Ravine at Sorrento,,,,,,Artist,,Édouard Bertin,"French, Paris 1797–1871 Paris",,"Bertin, Édouard",French,1797,1871,1821 or later,1821,1871,Oil on paper mounted on board,16 1/8 x 11 5/8 in. (41 x 29.5 cm),"Purchase, Karen B. Cohen Gift, 1986",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/435660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.55,false,true,436953,European Paintings,Drawing,George Moore (1852–1933),,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1879,1879,1879,Pastel on canvas,21 3/4 x 13 7/8 in. (55.2 x 35.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.56,false,true,436958,European Paintings,Drawing,Mademoiselle Isabelle Lemonnier (1857–1926),,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1879–82,1879,1882,Pastel on canvas,22 x 18 1/4 in. (55.9 x 46.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436958,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.561,false,true,436959,European Paintings,Drawing,"Mademoiselle Lucie Delabigne (1859–1910), Called Valtesse de la Bigne",,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1879,1879,1879,Pastel on canvas,21 3/4 x 14 in. (55.2 x 35.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.231,false,true,436170,European Paintings,Drawing,Woman Combing Her Hair,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1888–90,1888,1890,"Pastel on light green wove paper, now discolored to warm gray, affixed to original pulpboard mount",24 1/8 x 18 1/8 in. (61.3 x 46 cm),"Gift of Mr. and Mrs. Nate B. Spingold, 1956",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436170,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.27.3,false,true,436151,European Paintings,Drawing,The Milliner,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1882,1877,1887,"Pastel and charcoal on warm gray wove paper, now discolored to buff (watermark MICHALLET), laid down on dark brown wove paper",18 3/4 x 24 1/2 in. (47.6 x 62.2 cm),"Purchase, Rogers Fund and Dikran G. Kelekian Gift, 1922",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436151,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.185,false,true,436132,European Paintings,Drawing,The Dance Lesson,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1879,1874,1884,"Pastel and black chalk on three pieces of wove paper, joined together",25 3/8 x 22 1/8 in. (64.5 x 56.2 cm),"H. O. Havemeyer Collection, Gift of Adaline Havemeyer Perkins, in memory of her father, Horace Havemeyer, 1971",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436132,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.6,false,true,436158,European Paintings,Drawing,Self-Portrait,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1855–56,1855,1856,"Oil on paper, laid down on canvas",16 x 13 1/2 in. (40.6 x 34.3 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.7,false,true,436159,European Paintings,Drawing,The Singer in Green,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1884,1879,1889,Pastel on light blue laid paper,23 3/4 x 18 1/4 in. (60.3 x 46.4 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.35,false,true,436173,European Paintings,Drawing,Woman Having Her Hair Combed,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1886–88,1886,1888,"Pastel on light green wove paper, now discolored to warm gray, affixed to original pulpboard mount",29 1/8 x 23 7/8 in. (74 x 60.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436173,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.36,false,true,436172,European Paintings,Drawing,Woman Drying Her Foot,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1885–86,1885,1886,"Pastel on buff wove paper, affixed to original pulpboard mount",19 3/4 x 21 1/4 in. (50.2 x 54 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436172,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.37,false,true,436128,European Paintings,Drawing,Woman with a Towel,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1894 or 1898,1894,1894,Pastel on cream-colored wove paper with red and blue fibers throughout,37 3/4 x 30in. (95.9 x 76.2cm) Frame: 44 1/4 x 36 1/4 x 1 7/8 in. (112.4 x 92.1 x 4.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.38,false,true,436126,European Paintings,Drawing,At the Milliner's,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1882,1882,1882,"Pastel on pale gray wove paper (industrial wrapping paper), laid down on silk bolting",30 x 34 in. (76.2 x 86.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.39,false,true,436156,European Paintings,Drawing,The Rehearsal Onstage,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1874,1871,1877,"Pastel over brush-and-ink drawing on thin cream-colored wove paper, laid down on bristol board and mounted on canvas",21 x 28 1/2 in. (53.3 x 72.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.40,false,true,436124,European Paintings,Drawing,"The Artist's Cousin, Probably Mrs. William Bell (Mathilde Musson, 1841–1878)",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1873,1873,1873,"Pastel on green wove paper, now darkened to brown",18 5/8 x 15 1/8 in. (47.3 x 38.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.41,false,true,436127,European Paintings,Drawing,Woman Bathing in a Shallow Tub,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1885,1885,1885,"Charcoal and pastel on light green wove paper, now discolored to warm gray, laid down on silk bolting",32 x 22 1/8in. (81.3 x 56.2cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.26,false,true,436155,European Paintings,Drawing,The Rehearsal of the Ballet Onstage,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1874,1871,1877,"Oil colors freely mixed with turpentine, with traces of watercolor and pastel over pen-and-ink drawing on cream-colored wove paper, laid down on bristol board and mounted on canvas",21 3/8 x 28 3/4 in. (54.3 x 73 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436155,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.277.1,false,true,436154,European Paintings,Drawing,Portraits at the Stock Exchange,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1878–79,1878,1879,"Pastel on paper, pieced, and laid down on canvas",28 3/8 x 22 7/8 in. (72.1 x 58.1 cm),"Gift of Janice H. Levin, 1991",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436154,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.288.3,false,true,437994,European Paintings,Drawing,Race Horses,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1885–88,1885,1888,Pastel on wood,11 7/8 x 16 in. (30.2 x 40.6 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1999, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.332.1,false,true,438518,European Paintings,Drawing,Dancer,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1880–85,1880,1885,"Pastel on paper, laid down on board",19 1/4 x 12 3/4 in. (48.9 x 32.4 cm),"Gift of The Philip and Janice Levin Foundation, 2007",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/438518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.185,false,true,436125,European Paintings,Drawing,Woman on a Sofa,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1875,1875,1875,"Oil colors freely mixed with turpentine, with touches of pastel, over graphite underdrawing, on pink paper",19 1/8 x 16 3/4 in. (48.6 x 42.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.187,false,true,436165,European Paintings,Drawing,Two Dancers,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1873,1873,1873,"Dark brown wash and white gouache on bright pink commercially coated wove paper, now faded to pale pink",24 1/8 x 15 1/2 in. (61.3 x 39.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436165,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.188,false,true,436135,European Paintings,Drawing,Dancer with a Fan,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1880,1875,1885,Pastel on gray-green laid paper,24 x 16 1/2 in. (61 x 41.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.189,false,true,436166,European Paintings,Drawing,Two Dancers,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1879,1874,1879,Charcoal and white chalk on green commercially coated wove paper,25 1/8 x 19 1/4 in. (63.8 x 48.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436166,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.190,false,true,436131,European Paintings,Drawing,Bather Stepping into a Tub,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1890,1885,1895,"Pastel and charcoal on blue laid paper, mounted at perimeter on backing board",22 x 18 3/4 in. (55.9 x 47.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.553,false,true,436171,European Paintings,Drawing,Woman Drying Her Arm,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,late 1880s–early 1890s,1885,1895,"Pastel and charcoal on light pink wove paper, discolored at the edges",12 x 17 1/2in. (30.5 x 44.5cm) Frame: 17 1/8 x 22 7/8 x 1 7/8 in. (43.5 x 58.1 x 4.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436171,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.554,false,true,436143,European Paintings,Drawing,Fan Mount: The Ballet,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1879,1879,1879,"Watercolor, India ink, silver, and gold on silk",6 1/8 x 21 1/4 in. (15.6 x 54 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.555,false,true,436142,European Paintings,Drawing,Fan Mount: Ballet Girls,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1879,1879,1879,"Watercolor, silver, and gold on silk",7 1/2 x 22 3/4 in. (19.1 x 57.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436142,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.556,false,true,436157,European Paintings,Drawing,Russian Dancer,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1899,1899,1899,Pastel over charcoal on tracing paper,24 3/8 x 18 in. (61.9 x 45.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436157,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.557,false,true,436136,European Paintings,Drawing,Dancer with a Fan,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1890–95,1890,1895,Pastel and charcoal on buff-colored wove tracing paper,21 7/8 x 19 1/4 in. (55.6 x 48.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.558,false,true,436163,European Paintings,Drawing,Three Dancers Preparing for Class,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,after 1878,1878,1885,Pastel on buff-colored wove paper,21 1/2 x 20 1/2 in. (54.6 x 52.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436163,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.356.30,false,true,436134,European Paintings,Drawing,Dancer Onstage,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1877,1872,1882,"Gouache over graphite underdrawing on thin wove commercially coated yellow paper, laid down on board",Paper 6 3/4 x 8 3/8 in. (17.1 x 21.3 cm); board 7 x 9 in. (17.8 x 22.9 cm),"The Lesley and Emma Sheafer Collection, Bequest of Emma A. Sheafer, 1973",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436134,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.50,false,true,437379,European Paintings,Drawing,Bouquet of Flowers,,,,,,Artist,,Odilon Redon,"French, Bordeaux 1840–1916 Paris",,"Redon, Odilon",French,1840,1916,probably ca. 1905,1900,1910,Pastel on paper,31 5/8 x 25 1/4 in. (80.3 x 64.1 cm),"Gift of Mrs. George B. Post, 1956",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437379,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.54,false,true,437377,European Paintings,Drawing,"Madame Arthur Fontaine (Marie Escudier, born 1865)",,,,,,Artist,,Odilon Redon,"French, Bordeaux 1840–1916 Paris",,"Redon, Odilon",French,1840,1916,1901,1901,1901,Pastel on paper,28 1/2 x 22 1/2 in. (72.4 x 57.2 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1960",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.133,false,true,437521,European Paintings,Drawing,A Village in a Valley,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,late 1820s,1825,1835,"Oil on paper, mounted on canvas",9 1/8 x 16 in. (23.2 x 40.6 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437521,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.117,false,true,435612,European Paintings,Drawing,"The Artist's Wife (Périe, 1849–1887) Reading",,,,,,Artist,,Albert Bartholomé,"French, Thiverval 1848–1928 Paris",,"Bartholomé, Albert",French,1848,1928,1883,1883,1883,"Pastel and charcoal on wove paper, laid down on blue wove paper, laid down on stretched canvas",19 7/8 x 24 1/8 in. (50.5 x 61.3 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1990",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/435612,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.356.34,false,true,437435,European Paintings,Drawing,The Milliner,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1877,1877,1877,Pastel on paper,21 x 16 1/4 in. (53.3 x 41.3 cm),"The Lesley and Emma Sheafer Collection, Bequest of Emma A. Sheafer, 1973",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.153,false,true,437833,European Paintings,Drawing,"Madame Thadée Natanson (Misia Godebska, 1872–1950) at the Theater",,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,1895,1895,1895,Oil on cardboard,24 1/2 x 29 1/2 in. (62.2 x 74.9 cm),"Gift of Mr. and Mrs. Richard Rodgers, 1964",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.14,false,true,438017,European Paintings,Drawing,Henri-Gabriel Ibels (1867–1936),,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,1892–93,1892,1893,Oil on cardboard,20 1/2 x 15 1/2 in. (52.1 x 39.4 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/438017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.105,false,true,437315,European Paintings,Drawing,Fan Mount: The Cabbage Gatherers,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,ca. 1878–79,1850,1903,Gouache on silk,6 1/2 x 20 1/2 in. (16.5 x 52.1 cm),"Purchase, Leonora Brenauer Bequest, in memory of her father, Joseph B. Brenauer, 1994",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.195,false,true,436336,European Paintings,"Painting, polyptych",The Pérussis Altarpiece,,,,,,Artist,Circle of,Nicolas Froment,,,"Froment, Nicolas",French,1460,1484,1480,1480,1480,Oil and gold on wood,"Three panels, each 54 1/2 x 23 in. (138.4 x 58.4 cm)","Purchase, Mary Wetmore Shively Bequest, in memory of her husband, Henry L. Shively, M.D., 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1268,false,true,435672,European Paintings,Painting,Angelica and Medoro,,,,,,Artist,,Jacques Blanchard,"French, 1600–1638",,"Blanchard, Jacques",French,1600,1638,possibly early 1630s,1630,1633,Oil on canvas,With added strip at top 47 7/8 x 69 1/4 in. (121.6 x 175.9 cm),"Gift of George A. Hearn, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.85.1,false,true,436709,European Paintings,Painting,Ingres (1780–1867) as a Young Man,,,,,,Artist,?,Madame Gustave Héquet,"French, 1845–1865",,"Héquet, Gustave, Madame",French,1845,1865,1850–60,1850,1860,Oil on canvas,34 x 27 1/ 2 in. (86.4 x 69.9 cm),"Bequest of Grace Rainey Rogers, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.101,false,true,489542,European Paintings,Painting,The Large Bouquet,,,,,,Artist,,Séraphine Louis,"French, 1864–1942",,"Louis, Séraphine",French,1864,1942,ca. 1907,1902,1912,Oil on canvas,57 1/2 x 44 3/4 in. (146.1 x 113.7 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,"© 2016 Artists Rights Society (ARS), New York",http://www.metmuseum.org/art/collection/search/489542,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.59,false,true,437082,European Paintings,Painting,The Baker's Cart,,,,,,Artist,,Jean Michelin,"French, ca. 1616–1670",,"Michelin, Jean",French,1616,1670,1656,1656,1656,Oil on canvas,38 3/4 x 49 3/8 in. (98.4 x 125.4 cm),"Fletcher Fund, 1927",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437082,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.119,false,true,437143,European Paintings,Painting,Portrait of a Man in White,,,,,,Artist,,Monogrammist LAM,"French, active 1568–74",,Monogrammist LAM,French,1568,1574,1574,1574,1574,Oil on wood,16 1/8 x 9 1/2 in. (41 x 24.1 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437143,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.160,false,true,436190,European Paintings,Painting,Louis-Félix Amiel (1802–1864),,,,,,Artist,,Eugène Devéria,"French, Paris 1805–1865 Pau",,"Devéria, Eugène",French,1805,1865,1837,1837,1837,Oil on canvas,24 x 19 3/4 in. (61 x 50.2 cm),"Rogers Fund, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436190,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.243,false,true,438737,European Paintings,Painting,Faustine Léo (1832–1865),,,,,,Artist,,Henri Lehmann,"French, Kiel 1814–1882 Paris",,"Lehmann, Henri",French,1814,1882,1842,1842,1842,Oil on canvas,39 3/8 x 32 in. (100 x 81.3 cm),"Purchase, Wolfe Fund and Mr. and Mrs. Frank E. Richardson Gift, 2004",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.317,false,true,437865,European Paintings,Painting,The Hunt Breakfast,,,,,,Artist,,Carle (Charles André) Vanloo,"French, Nice 1705–1765 Paris",,"Vanloo, Carle (Charles André)",French,1705,1765,ca. 1737,1732,1742,Oil on canvas,23 1/4 x 19 1/2 in. (59.1 x 49.5 cm),"Wrightsman Fund, 1995",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437865,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.285,false,true,437845,European Paintings,"Painting, study for a tapestry cartoon",The Triumph of Mordecai,,,,,,Artist,,Jean François de Troy,"French, Paris 1679–1752 Rome",,"Troy, Jean François de",French,1679,1752,ca. 1736,1731,1741,Oil on canvas,33 7/8 x 59 1/8 in. (86 x 150.2 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.31,false,true,438546,European Paintings,Painting,Madame Charles Maurice de Talleyrand Périgord (1761–1835),,,,,,Artist,,baron François Gérard,"French, Rome 1770–1837 Paris",,"Gérard, François, baron",French,1770,1837,ca. 1804,1799,1809,Oil on canvas,88 7/8 x 64 7/8 in. (225.7 x 164.8 cm),"Wrightsman Fund, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438546,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.348,false,true,441969,European Paintings,Painting,"Charles Maurice de Talleyrand Périgord (1754–1838), Prince de Bénévent",,,,,,Artist,,baron François Gérard,"French, Rome 1770–1837 Paris",,"Gérard, François, baron",French,1770,1837,1808,1808,1808,Oil on canvas,83 7/8 x 57 7/8 in. (213 x 147 cm),"Purchase, Mrs. Charles Wrightsman Gift, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.190,false,true,437340,European Paintings,Painting,"Charles Maurice de Talleyrand Périgord (1754–1838), Prince de Talleyrand",,,,,,Artist,,Pierre Paul Prud'hon,"French, Cluny 1758–1823 Paris",,"Prud'hon, Pierre Paul",French,1758,1823,1817,1817,1817,Oil on canvas,85 x 55 7/8 in. (215.9 x 141.9 cm),"Purchase, Mrs. Charles Wrightsman Gift, in memory of Jacqueline Bouvier Kennedy Onassis, 1994",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437340,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.15,false,true,437512,European Paintings,Painting,The Banks of the Bièvre near Bicêtre,,,,,,Artist,,Henri Rousseau (le Douanier),"French, Laval 1844–1910 Paris",,"Rousseau, Henri (le Douanier)",French,1844,1910,ca. 1908–09,1903,1913,Oil on canvas,21 1/2 x 18 in. (54.6 x 45.7 cm),"Gift of Marshall Field, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.112.5,true,true,438822,European Paintings,Painting,The Repast of the Lion,,,,,,Artist,,Henri Rousseau (le Douanier),"French, Laval 1844–1910 Paris",,"Rousseau, Henri (le Douanier)",French,1844,1910,ca. 1907,1902,1912,Oil on canvas,44 3/4 x 63 in. (113.7 x 160 cm),"Bequest of Sam A. Lewisohn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.493,false,true,435673,European Paintings,Painting,The Outer Harbor of Brest,,,,,,Artist,,Henri Joseph van Blarenberghe,"French, Lille 1750–1826 Lille",,"Blarenberghe, Henri Joseph van",French,1750,1826,1773,1773,1773,Oil on canvas,29 1/4 x 42 1/8 in. (74.3 x 107 cm),"Gift of Mrs. Vincent Astor, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.4,false,true,435849,European Paintings,Painting,"Mrs. William Astor (Caroline Webster Schermerhorn, 1831–1908)",,,,,,Artist,,Carolus-Duran (Charles-Auguste-Émile Durant),"French, Lille 1837–1917 Paris",,"Carolus-Duran, Charles-Auguste-Émile Durant",French,1837,1917,1890,1890,1890,Oil on canvas,83 1/2 x 42 1/4 in. (212.1 x 107.3 cm),"Gift of R. Thornton Wilson and Orme Wilson, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.608,false,true,669033,European Paintings,Painting,Henri Fantin-Latour (1836–1904),,,,,,Artist,,Carolus-Duran (Charles-Auguste-Émile Durant),"French, Lille 1837–1917 Paris",,"Carolus-Duran, Charles-Auguste-Émile Durant",French,1837,1917,1861,1861,1861,Oil on canvas,18 × 14 7/8 in. (45.7 × 37.8 cm),"Purchase, Marisa I. Alonso Bequest and Elizabeth and Thomas Easton Gift, in memory of their mother, Joan K. Easton, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/669033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.260.9,false,true,437265,European Paintings,Painting,"Maria Luisa of Parma (1751–1819), Later Queen of Spain",,,,,,Artist,,Laurent Pécheux,"French, Lyons 1729–1821 Turin",,"Pécheux, Laurent",French,1729,1821,1765,1765,1765,Oil on canvas,90 7/8 x 64 3/4 in. (230.8 x 164.5 cm),"Bequest of Annie C. Kane, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437265,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.42.42,false,true,438665,European Paintings,Painting,Pope Gregory XVI Visiting the Church of San Benedetto at Subiaco,,,,,,Artist,,Jean-François Montessuy,"French, Lyons 1804–1876 Lyons",,"Montessuy, Jean-François",French,1804,1876,1843,1843,1843,Oil on canvas,49 1/4 x 55 3/8 in. (125.1 x 140.7 cm),"The Whitney Collection, Gift of Wheelock Whitney III, and Purchase, Gift of Mr. and Mrs. Charles S. McVeigh, by exchange, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.20.1,false,true,437052,European Paintings,Painting,"1807, Friedland",,,,,,Artist,,Ernest Meissonier,"French, Lyons 1815–1891 Paris",,"Meissonier, Ernest",French,1815,1891,ca. 1861–75,1860,1875,Oil on canvas,53 1/2 x 95 1/2 in. (135.9 x 242.6 cm),"Gift of Henry Hilton, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437052,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.136.7,false,true,437050,European Paintings,Painting,Soldier Playing the Theorbo,,,,,,Artist,,Ernest Meissonier,"French, Lyons 1815–1891 Paris",,"Meissonier, Ernest",French,1815,1891,1865,1865,1865,Oil on wood,11 1/2 x 8 5/8 in. (29.2 x 21.9 cm),"Bequest of Martha T. Fiske Collord, in memory of her first husband, Josiah M. Fiske, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437050,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.37,false,true,437051,European Paintings,Painting,A General and His Aide-de-camp,,,,,,Artist,,Ernest Meissonier,"French, Lyons 1815–1891 Paris",,"Meissonier, Ernest",French,1815,1891,1869,1869,1869,Oil on wood,7 3/4 x 10 7/8 in. (19.7 x 27.6 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437051,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.39,false,true,437049,European Paintings,Painting,The Card Players,,,,,,Artist,,Ernest Meissonier,"French, Lyons 1815–1891 Paris",,"Meissonier, Ernest",French,1815,1891,1863,1863,1863,Oil on wood,13 7/8 x 10 1/2 in. (35.2 x 26.7 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437049,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.20,false,true,437350,European Paintings,Painting,Tamaris,,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,ca. 1886–87,1886,1887,Oil on canvas,10 x 15 1/2 in. (25.4 x 39.4 cm),"H. O. Havemeyer Collection, Gift of Mrs. J. Watson Webb, 1930",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437350,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.177,false,true,437344,European Paintings,Painting,The Shepherd's Song,,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,1891,1891,1891,Oil on canvas,41 1/8 x 43 1/4 in. (104.5 x 109.9 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437344,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.15.1,false,true,437347,European Paintings,Painting,Ludus pro patria (Patriotic Games),,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,ca. 1883–89,1878,1894,Oil on canvas,13 1/8 x 52 7/8 in. (33.3 x 134.3 cm),"Gift of Mrs. Harry Payne Bingham, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437347,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.15.2,false,true,437346,European Paintings,Painting,Inter artes et naturam (Between Art and Nature),,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,ca. 1890–95,1885,1900,Oil on canvas,15 7/8 x 44 3/4 in. (40.3 x 113.7 cm),"Gift of Mrs. Harry Payne Bingham, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.253,false,true,437349,European Paintings,Painting,Sleep,,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,ca. 1867–70,1867,1870,Oil on canvas,26 1/8 x 41 3/4 in. (66.4 x 106 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437349,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.117,false,true,437343,European Paintings,Painting,The Allegory of the Sorbonne,,,,,,Artist,,Pierre Puvis de Chavannes,"French, Lyons 1824–1898 Paris",,"Puvis de Chavannes, Pierre",French,1824,1898,1889,1889,1889,Oil on canvas,32 5/8 x 180 1/4 in. (82.9 x 457.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437343,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.189,false,true,436836,European Paintings,Painting,Allegory of Music,,,,,,Artist,,Laurent de La Hyre,"French, Paris 1606–1656 Paris",,"La Hyre, Laurent de",French,1606,1656,1649,1649,1649,Oil on canvas,41 5/8 x 56 3/4 in. (105.7 x 144.1 cm),"Charles B. Curtis Fund, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.342,false,true,436858,European Paintings,Painting,The Rape of Tamar,,,,,,Artist,,Eustache Le Sueur,"French, Paris 1616–1655 Paris",,"Le Sueur, Eustache",French,1616,1655,probably ca. 1640,1635,1645,Oil on canvas,74 1/2 x 63 1/2 in. (189.2 x 161.3 cm),"Purchase, Mr. and Mrs. Charles Wrightsman Gift, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436858,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.183,false,true,442761,European Paintings,Painting,The Sacrifice of Polyxena,,,,,,Artist,,Charles Le Brun,"French, Paris 1619–1690 Paris",,"Le Brun, Charles",French,1619,1690,1647,1647,1647,Oil on canvas,67 5/16 × 51 9/16 in. (171 × 131 cm),"Purchase, 2012 Benefit Fund, and Bequest of Grace Wilkes and Fletcher Fund, by exchange, 2013",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/442761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.250,false,true,626692,European Paintings,Painting,Everhard Jabach (1618–1695) and His Family,,,,,,Artist,,Charles Le Brun,"French, Paris 1619–1690 Paris",,"Le Brun, Charles",French,1619,1690,ca. 1660,1655,1665,Oil on canvas,110 1/4 × 129 1/8 in. (280 × 328 cm),"Purchase, Mrs. Charles Wrightsman Gift, in honor of Keith Christiansen, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/626692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +03.37.2,false,true,436846,European Paintings,Painting,"Portrait of a Woman, Possibly Madame Claude Lambert de Thorigny (Marie Marguerite Bontemps, 1668–1701), and an Enslaved Servant",,,,,,Artist,,Nicolas de Largillierre,"French, Paris 1656–1746 Paris",,"Largillierre, Nicolas de",French,1656,1746,1696,1696,1696,Oil on canvas,55 x 42 in. (139.7 x 106.7 cm),"Rogers Fund, 1903",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.311.4,false,true,436847,European Paintings,Painting,André François Alloys de Theys d'Herculais (1692–1779),,,,,,Artist,,Nicolas de Largillierre,"French, Paris 1656–1746 Paris",,"Largillierre, Nicolas de",French,1656,1746,1727,1727,1727,Oil on canvas,54 1/4 x 41 1/2 in. (137.8 x 105.4 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.172,false,true,437181,European Paintings,Painting,Madame Marsollier and Her Daughter,,,,,,Artist,,Jean Marc Nattier,"French, Paris 1685–1766 Paris",,"Nattier, Jean Marc",French,1685,1766,1749,1749,1749,Oil on canvas,57 1/2 x 45 in. (146.1 x 114.3 cm),"Bequest of Florence S. Schuette, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437181,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +03.37.3,false,true,437183,European Paintings,Painting,Madame de Maison-Rouge as Diana,,,,,,Artist,,Jean Marc Nattier,"French, Paris 1685–1766 Paris",,"Nattier, Jean Marc",French,1685,1766,1756,1756,1756,Oil on canvas,53 3/4 x 41 3/8 in. (136.5 x 105.1 cm),"Rogers Fund, 1903",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.100.2,false,true,437184,European Paintings,Painting,The Spring (La Source),,,,,,Artist,,Jean Marc Nattier,"French, Paris 1685–1766 Paris",,"Nattier, Jean Marc",French,1685,1766,1738,1738,1738,Oil on canvas,31 3/4 x 25 5/8 in. (80.6 x 65.1 cm),"Gift of Jessie Woolworth Donahue, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.102.1,false,true,437185,European Paintings,Painting,"Marie Françoise de La Cropte de St. Abre, Marquise d'Argence",,,,,,Artist,,Jean Marc Nattier,"French, Paris 1685–1766 Paris",,"Nattier, Jean Marc",French,1685,1766,1744,1744,1744,Oil on canvas,32 1/2 x 25 1/2 in. (82.6 x 64.8 cm),"Gift of Jessie Woolworth Donahue, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437185,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.42,false,true,437182,European Paintings,Painting,"Portrait of a Woman, Called the Marquise Perrin de Cypierre",,,,,,Artist,,Jean Marc Nattier,"French, Paris 1685–1766 Paris",,"Nattier, Jean Marc",French,1685,1766,1753,1753,1753,Oil on canvas,"31 1/2 x 25 1/4 in. (80 x 64.1 cm), with later additions of 1 1/4 in. (3.2 cm) at bottom, 1 in. (2.5 cm) at left, and 1/2 in. (1.3 cm) at right","The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.85,false,true,438726,European Paintings,Painting,The Servant Justified,,,,,,Artist,,Nicolas Lancret,"French, Paris 1690–1743 Paris",,"Lancret, Nicolas",French,1690,1743,ca. 1740,1735,1745,Oil on copper,11 x 14 in. (27.9 x 35.6 cm),"Purchase, Walter and Leonore Annenberg and The Annenberg Foundation Gift, 2004",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.86,false,true,438727,European Paintings,Painting,Brother Philippe's Geese,,,,,,Artist,,Nicolas Lancret,"French, Paris 1690–1743 Paris",,"Lancret, Nicolas",French,1690,1743,ca. 1736,1731,1741,Oil on copper,10 3/4 x 13 7/8 in. (27.3 x 35.2 cm),"Purchase, Walter and Leonore Annenberg and The Annenberg Foundation Gift, 2004",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438727,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.9,false,true,435887,European Paintings,Painting,The Silver Tureen,,,,,,Artist,,Jean Siméon Chardin,"French, Paris 1699–1779 Paris",,"Chardin, Jean Siméon",French,1699,1779,ca. 1728–30,1728,1730,Oil on canvas,30 x 42 1/2 in. (76.2 x 108 cm),"Fletcher Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.24,true,true,435888,European Paintings,Painting,Soap Bubbles,,,,,,Artist,,Jean Siméon Chardin,"French, Paris 1699–1779 Paris",,"Chardin, Jean Siméon",French,1699,1779,ca. 1733–34,1733,1734,Oil on canvas,24 x 24 7/8 in. (61 x 63.2 cm),"Wentworth Fund, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.141,false,true,435743,European Paintings,Painting,The Dispatch of the Messenger,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1765,1765,1765,Oil on canvas,"Oval, 12 5/8 x 10 1/2 in. (32.1 x 26.7 cm)","Gift of Mrs. Joseph Heine, in memory of her husband, I. D. Levy, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435743,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.167,false,true,435744,European Paintings,Painting,Virgin and Child with the Young Saint John the Baptist and Angels,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1765,1765,1765,Oil on canvas,"Oval, 16 1/8 x 13 5/8 in. (41 x 34.6 cm)","Gift of Adelaide Milton de Groot, in memory of the de Groot and Hawley families, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435744,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.46,false,true,435738,European Paintings,Painting,The Interrupted Sleep,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1750,1750,1750,Oil on canvas,Overall 32 1/4 x 29 5/8 in. (81.9 x 75.2 cm); painted surface (irregular oval) 31 x 27 3/4 in. (78.7 x 70.5 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.9,true,true,435739,European Paintings,"Painting, overdoor",The Toilette of Venus,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1751,1751,1751,Oil on canvas,42 5/8 x 33 1/2 in. (108.3 x 85.1 cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435739,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.225.1,false,true,435745,European Paintings,Painting,Shepherd's Idyll,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1768,1768,1768,Oil on canvas,94 1/2 x 93 1/2 in. (240 x 237.5 cm),"Gift of Julia A. Berwind, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.225.2,false,true,435746,European Paintings,Painting,Washerwomen,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1768,1768,1768,Oil on canvas,95 x 93 in. (241.3 x 236.2 cm),"Gift of Julia A. Berwind, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435746,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.155.1,false,true,435740,European Paintings,Painting,Allegory of Autumn,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",and Workshop,"Boucher, François",French,1703,1770,1753,1753,1753,Oil on canvas,"Irregular, 44 3/4 x 63 3/4 in. (113.7 x 161.9 cm)","Purchase, Mr. and Mrs. Charles Wrightsman Gift, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435740,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.155.2,false,true,435741,European Paintings,Painting,Allegory of Lyric Poetry,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",and Workshop,"Boucher, François",French,1703,1770,1753,1753,1753,Oil on canvas,"Irregular, 45 1/4 x 62 3/4 in. (114.9 x 159.4 cm)","Purchase, Mr. and Mrs. Charles Wrightsman Gift, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435741,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.44,false,true,435737,European Paintings,Painting,Imaginary Landscape with the Palatine Hill from Campo Vaccino,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1734,1734,1734,Oil on canvas,25 x 31 7/8 in. (63.5 x 81 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435737,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.45,false,true,435747,European Paintings,Painting,"Jupiter, in the Guise of Diana, and Callisto",,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1763,1763,1763,Oil on canvas,"Oval, 25 1/2 x 21 5/8 in. (64.8 x 54.9 cm)","The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435747,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.46,false,true,435742,European Paintings,Painting,Angelica and Medoro,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,1763,1763,1763,Oil on canvas,"Oval, 26 1/4 x 22 1/8 in. (66.7 x 56.2 cm)","The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435742,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.129,false,true,437286,European Paintings,Painting,The Death of Harmonia,,,,,,Artist,,Jean-Baptiste Marie Pierre,"French, Paris 1714–1789 Paris",,"Pierre, Jean-Baptiste Marie",French,1714,1789,ca. 1740–41,1740,1741,Oil on canvas,77 1/2 x 58 1/4 in. (196.9 x 148 cm),"Gift of Mr. and Mrs. Harry N. Abrams, by exchange, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.47,false,true,436214,European Paintings,Painting,"Marie Rinteau, called Mademoiselle de Verrières",,,,,,Artist,,François Hubert Drouais,"French, Paris 1727–1775 Paris",,"Drouais, François Hubert",French,1727,1775,1761,1761,1761,Oil on canvas,45 1/2 x 34 5/8 in. (115.6 x 87.9 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436214,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.159.1,false,true,436215,European Paintings,Painting,Madame Sophie de France (1734–1782),,,,,,Artist,,François Hubert Drouais,"French, Paris 1727–1775 Paris",,"Drouais, François Hubert",French,1727,1775,1762,1762,1762,Oil on canvas,25 5/8 x 20 7/8 in. (65.1 x 53 cm),"Gift of Barbara Lowe Fallass, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436215,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.242.2,false,true,436216,European Paintings,Painting,Portrait of a Young Woman as a Vestal Virgin,,,,,,Artist,,François Hubert Drouais,"French, Paris 1727–1775 Paris",,"Drouais, François Hubert",French,1727,1775,1767,1767,1767,Oil on canvas,31 1/2 x 24 7/8 in. (80 x 63.2 cm),"Gift of Mrs. William M. Haupt, from the collection of Mrs. James B. Haggin, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436216,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.210,false,true,436213,European Paintings,Painting,"Portrait of a Woman, Said to be Madame Charles Simon Favart (Marie Justine Benoîte Duronceray, 1727–1772)",,,,,,Artist,,François Hubert Drouais,"French, Paris 1727–1775 Paris",,"Drouais, François Hubert",French,1727,1775,1757,1757,1757,Oil on canvas,31 1/2 x 25 1/2 in. (80 x 64.8 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436213,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.40.1,false,true,437479,European Paintings,Painting,The Return of the Cattle,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,ca. 1773–75,1773,1775,Oil on canvas,80 3/4 x 48 in. (205.1 x 121.9 cm),"Bequest of Lucy Work Hewitt, 1934",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437479,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.40.2,false,true,437468,European Paintings,Painting,The Portico of a Country Mansion,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,1773,1773,1773,Oil on canvas,80 3/4 x 48 1/4 in. (205.1 x 122.6 cm),"Bequest of Lucy Work Hewitt, 1934",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437468,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.25,false,true,437470,European Paintings,Painting,The Mouth of a Cave,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,1784,1784,1784,Oil on canvas,68 3/4 x 31 1/4 in. (174.6 x 79.4 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437470,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.504,false,true,437864,European Paintings,Painting,Vase of Flowers and Conch Shell,,,,,,Artist,,Anne Vallayer-Coster,"French, Paris 1744–1818 Paris",,"Vallayer-Coster, Anne",French,1744,1818,1780,1780,1780,Oil on canvas,"Oval, 19 3/4 x 15 in. (50.2 x 38.1 cm)","Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.225.5,false,true,436840,European Paintings,Painting,"Self-Portrait with Two Pupils, Marie Gabrielle Capet (1761–1818) and Marie Marguerite Carreaux de Rosemond (died 1788)",,,,,,Artist,,Adélaïde Labille-Guiard,"French, Paris 1749–1803 Paris",,"Labille-Guiard, Adélaïde",French,1749,1803,1785,1785,1785,Oil on canvas,83 x 59 1/2 in. (210.8 x 151.1 cm),"Gift of Julia A. Berwind, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436840,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.103,false,true,436875,European Paintings,Painting,The Interior of an Atelier of a Woman Painter,,,,,,Artist,,Marie Victoire Lemoine,"French, Paris 1754–1820 Paris",,"Lemoine, Marie Victoire",French,1754,1820,1789,1789,1789,Oil on canvas,45 7/8 x 35 in. (116.5 x 88.9 cm),"Gift of Mrs. Thorneycroft Ryle, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.49,false,true,437775,European Paintings,Painting,The Billiard Room,,,,,,Artist,,Nicolas Antoine Taunay,"French, Paris 1755–1830 Paris",,"Taunay, Nicolas Antoine",French,1755,1830,ca. 1808,1803,1813,Oil on wood,6 3/8 x 8 5/8 in. (16.2 x 21.9 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.182,false,true,437900,European Paintings,Painting,"Comtesse de la Châtre (Marie Charlotte Louise Perrette Aglaé Bontemps, 1762–1848)",,,,,,Artist,,Élisabeth Louise Vigée Le Brun,"French, Paris 1755–1842 Paris",,"Vigée Le Brun, Élisabeth Louise",French,1755,1842,1789,1789,1789,Oil on canvas,45 x 34 1/2 in. (114.3 x 87.6 cm),"Gift of Jessie Woolworth Donahue, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437900,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.53,false,true,437899,European Paintings,Painting,Alexandre Charles Emmanuel de Crussol-Florensac (1743–1815),,,,,,Artist,,Élisabeth Louise Vigée Le Brun,"French, Paris 1755–1842 Paris",,"Vigée Le Brun, Élisabeth Louise",French,1755,1842,1787,1787,1787,Oil on wood,35 3/8 x 25 1/2 in. (89.9 x 64.8 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437899,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.135.2,false,true,437898,European Paintings,Painting,"Madame Grand (Noël Catherine Vorlée, 1761–1835)",,,,,,Artist,,Élisabeth Louise Vigée Le Brun,"French, Paris 1755–1842 Paris",,"Vigée Le Brun, Élisabeth Louise",French,1755,1842,1783,1783,1783,Oil on canvas,"Oval, 36 1/4 x 28 1/2 in. (92.1 x 72.4 cm)","Bequest of Edward S. Harkness, 1940",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437898,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.8,false,true,437080,European Paintings,Painting,The Mill of Montmartre,,,,,,Artist,,Georges Michel,"French, Paris 1763–1843 Paris",,"Michel, Georges",French,1763,1843,probably ca. 1820,1783,1843,Oil on canvas,29 x 40 in. (73.7 x 101.6 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.61.4,false,true,435650,European Paintings,Painting,"Madame Philippe Panon Desbassayns de Richemont (Jeanne Eglé Mourgue, 1778–1855) and Her Son, Eugène (1800–1859)",,,,,,Artist,,Marie Guillelmine Benoist,"French, Paris 1768–1826 Paris",,"Benoist, Marie Guillelmine",French,1768,1826,1802,1802,1802,Oil on canvas,46 x 35 1/4 in. (116.8 x 89.5 cm),"Gift of Julia A. Berwind, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.254,false,true,437887,European Paintings,Painting,Bertel Thorvaldsen (1768–1844) with the Bust of Horace Vernet,,,,,,Artist,,Horace Vernet,"French, Paris 1789–1863 Paris",,"Vernet, Horace",French,1789,1863,1833 or later,1833,1833,Oil on canvas,38 x 29 1/2 in. (96.5 x 74.9 cm),"Gift of Dr. Rudolf J. Heinemann, 1962",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.84,false,true,438033,European Paintings,Painting,Jean-Louis-André-Théodore Gericault (1791–1824),,,,,,Artist,,Horace Vernet,"French, Paris 1789–1863 Paris",,"Vernet, Horace",French,1789,1863,probably 1822 or 1823,1822,1823,Oil on canvas,18 5/8 x 15 1/8 in. (47.3 x 38.4 cm),"Purchase, Gift of Joanne Toor Cummings, by exchange, 1998",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.47,false,true,437888,European Paintings,Painting,The Start of the Race of the Riderless Horses,,,,,,Artist,,Horace Vernet,"French, Paris 1789–1863 Paris",,"Vernet, Horace",French,1789,1863,1820,1820,1820,Oil on canvas,18 1/8 x 21 1/4 in. (46 x 54 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.376,false,true,437078,European Paintings,Painting,Waterfall at Mont-Dore,,,,,,Artist,,Achille-Etna Michallon,"French, Paris 1796–1822 Paris",,"Michallon, Achille-Etna",French,1796,1822,1818,1818,1818,Oil on canvas,16 1/4 x 22 1/8 in. (41.3 x 56.2 cm),"Purchase, Wolfe Fund and Nancy Richardson Gift, 1994",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437078,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.90,false,true,435991,European Paintings,Painting,A Woman Reading,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1869 and 1870,1869,1870,Oil on canvas,21 3/8 x 14 3/4 in. (54.3 x 37.5 cm),"Gift of Louise Senff Cameron, in memory of her uncle, Charles H. Senff, 1928",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.13,false,true,435977,European Paintings,Painting,Mother and Child,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,probably 1860s,1860,1869,Oil on wood,12 3/4 x 8 7/8 in. (32.4 x 22.5 cm),"H. O. Havemeyer Collection, Gift of Mrs. P. H. B. Frelinghuysen, 1930",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.64,false,true,435962,European Paintings,Painting,Hagar in the Wilderness,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1835,1835,1835,Oil on canvas,71 x 106 1/2 in. (180.3 x 270.5 cm),"Rogers Fund, 1938",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.3,false,true,435972,European Paintings,Painting,Honfleur: Calvary,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1830,1825,1835,Oil on wood,11 3/4 x 16 1/8 in. (29.8 x 41 cm),"Purchase, Mr. and Mrs. Richard J. Bernhard Gift, by exchange, 1974",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435972,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.45.4,false,true,435983,European Paintings,Painting,River with a Distant Tower,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1865,1865,1865,Oil on canvas,21 1/2 x 30 7/8 in. (54.6 x 78.4 cm),"Bequest of Robert Graham Dun, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.75,false,true,435985,European Paintings,Painting,"Study for ""The Destruction of Sodom""",,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1843,1843,1843,Oil on canvas,14 1/8 x 19 5/8 in. (35.9 x 49.8 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.70.4,false,true,435989,European Paintings,Painting,The Banks of the Seine at Conflans,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1865–70,1865,1870,Oil on canvas,18 1/4 x 21 7/8 in. (46.4 x 55.6 cm),"Bequest of Eloise Lawrence Breese Norrie, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.811,false,true,435969,European Paintings,Painting,The Ferryman,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1865,1860,1870,Oil on canvas,26 1/8 x 19 3/8 in. (66.4 x 49.2 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435969,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.813,false,true,435979,European Paintings,Painting,A Pond in Picardy,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1867,1862,1872,Oil on canvas,17 x 25 in. (43.2 x 63.5 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435979,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.817,false,true,435975,European Paintings,Painting,A Lane through the Trees,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1870–73,1870,1873,Oil on canvas,24 x 18 in. (61 x 45.7 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.17,false,true,435987,European Paintings,Painting,A Village Street: Dardagny,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,"1852, 1857, or 1863",1852,1863,Oil on canvas,13 1/2 x 9 1/2 in. (34.3 x 24.1 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.18,false,true,435967,European Paintings,Painting,"The Burning of Sodom (formerly ""The Destruction of Sodom"")",,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1843 and 1857,1843,1857,Oil on canvas,36 3/8 x 71 3/8 in. (92.4 x 181.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435967,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.19,false,true,435963,European Paintings,Painting,Bacchante by the Sea,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1865,1865,1865,Oil on wood,15 1/4 x 23 3/8 in. (38.7 x 59.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.33,false,true,435976,European Paintings,Painting,The Letter,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1865,1860,1870,Oil on wood,21 1/2 x 14 1/4 in. (54.6 x 36.2 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435976,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.272,false,true,435968,European Paintings,Painting,The Environs of Paris,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1860s,1860,1869,Oil on wood,13 1/2 x 20 1/4 in. (34.3 x 51.4 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435968,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.141,false,true,435988,European Paintings,Painting,Ville-d'Avray,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1870,1870,1870,Oil on canvas,21 5/8 x 31 1/2 in. (54.9 x 80 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.212,false,true,435964,European Paintings,Painting,The Gypsies,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1872,1872,1872,Oil on canvas,21 3/4 x 31 1/2 in. (55.2 x 80 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.225,false,true,435990,European Paintings,Painting,A Woman Gathering Faggots at Ville-d'Avray,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1871–74,1871,1874,Oil on canvas,28 3/8 x 22 1/2 in. (72.1 x 57.2 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.203.4,false,true,435986,European Paintings,Painting,View of Lormes,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,early 1840s,1840,1844,Oil on canvas,6 1/2 x 21 5/8 in. (16.5 x 54.9 cm),"Gift of Mr. and Mrs. Walter Mendelsohn, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435986,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.288.2,false,true,437991,European Paintings,Painting,The Curious Little Girl,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1860–64,1816,1875,"Oil on cardboard, laid down on wood",16 1/4 x 11 1/4 in. (41.3 x 28.6 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1999, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437991,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.42.13,false,true,438635,European Paintings,Painting,Waterfall at Terni,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1826,1826,1826,"Oil on paper, laid down on wood",10 1/2 x 12 1/8 in. (26.7 x 30.8 cm),"The Whitney Collection, Gift of Wheelock Whitney III, and Purchase, Gift of Mr. and Mrs. Charles S. McVeigh, by exchange, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.193,false,true,435978,European Paintings,Painting,The Muse: History,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1865,1860,1870,Oil on canvas,18 1/8 x 13 7/8 in. (46 x 35.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435978,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.562,false,true,435971,European Paintings,Painting,Girl Weaving a Garland,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1860–65,1860,1865,Oil on canvas,16 1/2 x 11 3/4 in. (41.9 x 29.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435971,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.563,false,true,435981,European Paintings,Painting,Reverie,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1860–65,1855,1870,Oil on wood,19 5/8 x 14 3/8 in. (49.8 x 36.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435981,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.564,false,true,435980,European Paintings,Painting,Portrait of a Child,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1835,1830,1840,Oil on wood,12 5/8 x 9 1/4 in. (32.1 x 23.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435980,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.565,true,true,435984,European Paintings,Painting,Sibylle,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1870,1865,1875,Oil on canvas,32 1/4 x 25 1/2 in. (81.9 x 64.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435984,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.598,false,true,435965,European Paintings,Painting,Bacchante in a Landscape,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1865–70,1865,1870,Oil on canvas,12 1/8 x 24 1/4 in. (30.8 x 61.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.136,false,true,435966,European Paintings,Painting,Boatman among the Reeds,,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,ca. 1865,1860,1870,Oil on canvas,23 1/2 x 32 in. (59.7 x 81.3 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435966,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.13,false,true,435961,European Paintings,Painting,Toussaint Lemaistre (1807/8–1888),,,,,,Artist,,Camille Corot,"French, Paris 1796–1875 Paris",,"Corot, Camille",French,1796,1875,1833,1833,1833,Oil on canvas,15 1/8 x 11 5/8 in. (38.4 x 29.5 cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.5,false,true,438950,European Paintings,Painting,A Storm off the Normandy Coast,,,,,,Artist,,Eugène Isabey,"French, Paris 1803–1886 Lagny",,"Isabey, Eugène",French,1803,1886,possibly ca. 1850,1840,1860,"Oil on paper, laid down on canvas",13 x 20 in. (33 x 50.8 cm),"Gift of The Eugene Victor Thaw Art Foundation, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +97.40,false,true,436772,European Paintings,Painting,The Sheepfold,,,,,,Artist,,Charles Jacque,"French, Paris 1813–1894 Paris",,"Jacque, Charles",French,1813,1894,1857,1857,1857,Oil on wood,18 1/8 x 36 1/8 in. (46 x 91.8 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1897",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436772,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.106,false,true,436418,European Paintings,Painting,Jerusalem from the Mount of Olives,,,,,,Artist,,Charles-Théodore Frère,"French, Paris 1814–1888 Paris",,"Frère, Charles-Théodore",French,1814,1888,by 1880,1870,1880,Oil on canvas,29 1/2 x 43 1/2 in. (74.9 x 110.5 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436418,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +03.29,false,true,436084,European Paintings,Painting,Boats on the Seacoast at Étaples,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,1871,1871,1871,Oil on wood,13 1/2 x 22 7/8 in. (34.3 x 58.1 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1903",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436084,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.45.3,false,true,436089,European Paintings,Painting,The Hamlet of Optevoz,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,ca. 1852,1849,1857,Oil on canvas,22 3/4 x 36 1/2 in. (57.8 x 92.7 cm),"Bequest of Robert Graham Dun, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436089,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.136.4,false,true,436090,European Paintings,Painting,Landscape with a Sunlit Stream,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,ca. 1877,1877,1877,Oil on canvas,25 1/8 x 18 7/8 in. (63.8 x 47.9 cm),"Bequest of Martha T. Fiske Collord, in memory of her first husband, Josiah M. Fiske, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436090,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.3,false,true,436085,European Paintings,Painting,Apple Blossoms,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,1873,1873,1873,Oil on canvas,23 1/8 x 33 3/8 in. (58.7 x 84.8 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436085,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.149.7,false,true,436081,European Paintings,Painting,Landscape on a River,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,1863,1863,1863,Oil on wood,8 1/4 x 15 in. (21 x 38.1 cm),"Gift of Mary V. T. Eberstadt, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.815,false,true,436080,European Paintings,Painting,The Banks of the Oise,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,1863,1863,1863,Oil on wood,14 3/4 x 26 3/8 in. (37.5 x 67 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436080,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.818,false,true,436083,European Paintings,Painting,A River Landscape with Storks,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,1864,1864,1864,Oil on wood,9 1/2 x 17 5/8 in. (24.1 x 44.8 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436083,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.275,false,true,436088,European Paintings,Painting,Portejoie on the Seine,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,1850–1878,1850,1878,Oil on wood,9 5/8 x 17 3/8 in. (24.4 x 44.1 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436088,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.120,false,true,436086,European Paintings,Painting,The Seine: Morning,,,,,,Artist,,Charles-François Daubigny,"French, Paris 1817–1878 Paris",,"Daubigny, Charles-François",French,1817,1878,1874,1874,1874,Oil on wood,15 1/4 x 27 1/4 in. (38.7 x 69.2 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436086,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.138.2,false,true,436970,European Paintings,Painting,Penelope,,,,,,Artist,,Charles-François Marchal,"French, Paris 1825–1877 Paris",,"Marchal, Charles-François",French,1825,1877,ca. 1868,1845,1877,Oil on canvas,43 1/2 x 19 1/2 in. (110.5 x 49.5 cm),"Gift of Mrs. Adolf Obrig, in memory of her husband, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436970,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.134.1,false,true,437153,European Paintings,Painting,Oedipus and the Sphinx,,,,,,Artist,,Gustave Moreau,"French, Paris 1826–1898 Paris",,"Moreau, Gustave",French,1826,1898,1864,1864,1864,Oil on canvas,81 1/4 x 41 1/4 in. (206.4 x 104.8 cm),"Bequest of William H. Herriman, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.119,false,true,436184,European Paintings,Painting,Objects of Art from the Louvre,,,,,,Artist,,Blaise-Alexandre Desgoffe,"French, Paris 1830–1901 Paris",,"Desgoffe, Blaise-Alexandre",French,1830,1901,1874,1874,1874,Oil on canvas,28 3/4 x 36 1/4 in. (73 x 92.1 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436184,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.36,false,true,436952,European Paintings,Painting,The Funeral,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,ca. 1867,1867,1867,Oil on canvas,28 5/8 x 35 5/8 in. (72.7 x 90.5 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1909",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436952,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.10,false,true,436951,European Paintings,Painting,Fishing,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,ca. 1862–63,1862,1863,Oil on canvas,30 1/4 x 48 1/2 in. (76.8 x 123.2 cm),"Purchase, Mr. and Mrs. Richard J. Bernhard Gift, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.193,false,true,436954,European Paintings,Painting,George Moore (1852–1933) at the Café,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1878 or 1879,1878,1879,Oil on canvas,25 3/4 x 32 in. (65.4 x 81.3 cm),"Gift of Mrs. Ralph J. Hines, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.129,false,true,436955,European Paintings,Painting,Head of Jean-Baptiste Faure (1830–1914),,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1882–83,1882,1883,Oil on canvas,18 1/8 x 14 7/8 in. (46 x 37.8 cm),"Gift of Mrs. Ralph J. Hines, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436955,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.58.2,false,true,436944,European Paintings,Painting,The Spanish Singer,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1860,1860,1860,Oil on canvas,58 x 45 in. (147.3 x 114.3 cm),"Gift of William Church Osborn, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.71.1,false,true,436956,European Paintings,Painting,Jean-Baptiste Faure (1830–1914),,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1882–83,1882,1883,Oil on canvas,23 1/4 x 19 1/2 in. (59.1 x 49.5 cm),"Gift of Mr. and Mrs. William B. Jaffe, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436956,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.21.2,false,true,436948,European Paintings,Painting,Boy with a Sword,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1861,1861,1861,Oil on canvas,51 5/8 x 36 3/4 in. (131.1 x 93.4 cm),"Gift of Erwin Davis, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.21.3,false,true,436964,European Paintings,Painting,Young Lady in 1866,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1866,1866,1866,Oil on canvas,72 7/8 x 50 5/8 in. (185.1 x 128.6 cm),"Gift of Erwin Davis, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436964,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.442,false,true,438144,European Paintings,Painting,"The ""Kearsarge"" at Boulogne",,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1864,1864,1864,Oil on canvas,32 1/8 x 39 3/8 in. (81.6 x 100 cm),"Gift of Peter H. B. Frelinghuysen, and Purchase, Mr. and Mrs. Richard J. Bernhard Gift, by exchange, Gifts of Mr. and Mrs. Richard Rodgers and Joanne Toor Cummings, by exchange, and Drue Heinz Trust, The Dillon Fund, The Vincent Astor Foundation, Mr. and Mrs. Henry R. Kravis, The Charles Engelhard Foundation, and Florence and Herbert Irving Gifts, 1999",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.230.1,false,true,436963,European Paintings,Painting,Strawberries,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,ca. 1882,1882,1882,Oil on canvas,8 3/8 x 10 1/2 in. (21.3 x 26.7 cm),"Gift of Mr. and Mrs. Nate B. Spingold, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.51,false,true,436950,European Paintings,Painting,The Dead Christ with Angels,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1864,1864,1864,Oil on canvas,70 5/8 x 59 in. (179.4 x 149.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.52,false,true,436960,European Paintings,Painting,A Matador,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1866–67,1866,1867,Oil on canvas,67 3/8 x 44 1/2 in. (171.1 x 113 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.53,true,true,436945,European Paintings,Painting,Mademoiselle V. . . in the Costume of an Espada,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1862,1862,1862,Oil on canvas,65 x 50 1/4 in. (165.1 x 127.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436945,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.54,false,true,438819,European Paintings,Painting,Young Man in the Costume of a Majo,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1863,1863,1863,Oil on canvas,74 x 49 1/8 in. (188 x 124.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.81,false,true,436957,European Paintings,Painting,"Madame Édouard Manet (Suzanne Leenhoff, 1830–1906)",,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,ca. 1873,1870,1876,Oil on canvas,39 1/2 x 30 7/8 in. (100.3 x 78.4 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436957,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.391.4,false,true,438002,European Paintings,Painting,"Madame Manet (Suzanne Leenhoff, 1830–1906) at Bellevue",,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1880,1880,1880,Oil on canvas,31 3/4 x 23 3/4 in. (80.6 x 60.3 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1997, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.114,false,true,436949,European Paintings,Painting,"Copy after Delacroix's ""Bark of Dante""",,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,ca. 1859,1854,1864,Oil on canvas,13 x 16 1/8 in. (33 x 41 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436949,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.115,true,true,436947,European Paintings,Painting,Boating,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1874,1874,1874,Oil on canvas,38 1/4 x 51 1/4 in. (97.2 x 130.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.14,false,true,436965,European Paintings,Painting,The Monet Family in Their Garden at Argenteuil,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1874,1874,1874,Oil on canvas,24 x 39 1/4 in. (61 x 99.7 cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436965,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.16,false,true,436961,European Paintings,Painting,Peonies,,,,,,Artist,,Édouard Manet,"French, Paris 1832–1883 Paris",,"Manet, Édouard",French,1832,1883,1864–65,1864,1865,Oil on canvas,23 3/8 x 13 7/8 in. (59.4 x 35.2 cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.161,false,true,436144,European Paintings,Painting,James-Jacques-Joseph Tissot (1836–1902),,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1867–68,1867,1868,Oil on canvas,59 5/8 x 44 in. (151.4 x 111.8 cm),"Rogers Fund, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436144,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.171,false,true,436152,European Paintings,Painting,Portrait of a Woman in Gray,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1865,1860,1870,Oil on canvas,36 x 28 1/2 in. (91.4 x 72.4 cm),"Gift of Mr. and Mrs. Edwin C. Vogel, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436152,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.65.2,false,true,436120,European Paintings,Painting,The Old Italian Woman,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1857,1857,1857,Oil on canvas,29 1/2 x 24 in. (74.9 x 61 cm),"Bequest of Charles Goldman, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.380,false,true,436168,European Paintings,Painting,Two Men,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1865–69,1865,1869,Oil on wood,10 5/8 x 8 1/8 in. (27 x 20.6 cm),"Gift of Yvonne Lamon, 1992",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436168,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.529,false,true,438857,European Paintings,Painting,Male Nude,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1856,1856,1856,Oil on canvas,13 3/4 x 24 1/4 in. (34.9 x 61.6 cm),"Gift of Philip and Catherine Korsant, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.277,false,true,438156,European Paintings,Painting,Young Woman with Ibis,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1860–62,1860,1862,Oil on canvas,39 3/8 x 29 1/2 in. (100 x 74.9 cm),"Gift of Stephen Mazoh and Purchase, Bequest of Gioconda King, by exchange, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438156,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.47.1,true,true,438817,European Paintings,Painting,The Dance Class,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1874,1874,1874,Oil on canvas,32 7/8 x 30 3/8 in. (83.5 x 77.2 cm),"Bequest of Mrs. Harry Payne Bingham, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438817,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.34,false,true,436139,European Paintings,Painting,Dancers Practicing at the Barre,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1877,1877,1877,Mixed media on canvas,29 3/4 x 32 in. (75.6 x 81.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436139,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.42,false,true,436140,European Paintings,Painting,"Dancers, Pink and Green",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1890,1885,1895,Oil on canvas,32 3/8 x 29 3/4 in. (82.2 x 75.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.43,false,true,436162,European Paintings,Painting,Sulking,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1870,1870,1870,Oil on canvas,12 3/4 x 18 1/4 in. (32.4 x 46.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436162,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.44,false,true,436122,European Paintings,Painting,The Collector of Prints,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1866,1866,1866,Oil on canvas,20 7/8 x 15 3/4 in. (53 x 40 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.45,false,true,436149,European Paintings,Painting,"Madame Théodore Gobillard (Yves Morisot, 1838–1893)",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1869,1869,1869,Oil on canvas,21 3/4 x 25 5/8 in. (55.2 x 65.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.46,false,true,436174,European Paintings,Painting,A Woman Ironing,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1873,1873,1873,Oil on canvas,21 3/8 x 15 1/2 in. (54.3 x 39.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436174,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.127,false,true,436138,European Paintings,Painting,Dancers in the Rehearsal Room with a Double Bass,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1882–85,1882,1885,Oil on canvas,15 3/8 x 35 1/4 in. (39.1 x 89.5 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.128,false,true,436121,European Paintings,Painting,A Woman Seated beside a Vase of Flowers (Madame Paul Valpinçon?),,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1865,1865,1865,Oil on canvas,29 x 36 1/2 in. (73.7 x 92.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.181,false,true,436145,European Paintings,Painting,Joseph-Henri Altès (1826–1895),,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1868,1868,1868,Oil on canvas,9 7/8 x 7 7/8 in. (25.1 x 20 cm); with added strips 10 5/8 x 8 1/2 in. (27 x 21.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436145,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.182,false,true,436150,European Paintings,Painting,Mademoiselle Marie Dihau (1843–1935),,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1867–68,1867,1868,Oil on canvas,8 3/4 x 10 3/4 in. (22.2 x 27.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436150,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.183,false,true,436153,European Paintings,Painting,Portrait of a Young Woman,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1885,1880,1890,Oil on canvas,10 3/4 x 8 3/4 in. (27.3 x 22.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436153,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.184,false,true,436141,European Paintings,Painting,The Dancing Class,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,ca. 1870,1870,1870,Oil on wood,7 3/4 x 10 5/8 in. (19.7 x 27 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436141,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.552,false,true,436123,European Paintings,Painting,"The Ballet from ""Robert le Diable""",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,1871,1871,1871,Oil on canvas,26 x 21 3/8 in. (66 x 54.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.101,false,true,437895,European Paintings,Painting,The Reprimand,,,,,,Artist,,Jean-Georges Vibert,"French, Paris 1840–1902 Paris",,"Vibert, Jean-Georges",French,1840,1902,1874,1874,1874,Oil on canvas,20 3/8 x 33 in. (51.8 x 83.8 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.140,false,true,437896,European Paintings,Painting,The Missionary's Adventures,,,,,,Artist,,Jean-Georges Vibert,"French, Paris 1840–1902 Paris",,"Vibert, Jean-Georges",French,1840,1902,ca. 1883,1878,1888,Oil on wood,39 x 53 in. (99.1 x 134.6 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437896,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.74,false,true,436519,European Paintings,Painting,Before the Mirror,,,,,,Artist,,Pierre-Paul-Léon Glaize,"French, Paris 1842–1932 Paris",,"Glaize, Pierre-Paul-Léon",French,1842,1932,1873,1873,1873,Oil on canvas,39 3/4 x 29 7/8 in. (101 x 75.9 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.90,false,true,436870,European Paintings,Painting,Choosing the Dinner,,,,,,Artist,,Alexandre-Louis Leloir,"French, Paris 1843–1884 Paris",,"Leloir, Alexandre-Louis",French,1843,1884,1872,1872,1872,Oil on canvas,12 1/4 x 18 3/8 in. (31.1 x 46.7 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.185,false,true,435649,European Paintings,Painting,Judith,,,,,,Artist,,Benjamin-Constant (Jean-Joseph-Benjamin Constant),"French, Paris 1845–1902 Paris",,Benjamin-Constant (Jean-Joseph-Benjamin Constant),French,1845,1902,possibly ca. 1886,1883,1889,Oil on canvas,47 1/2 x 31 1/2 in. (120.7 x 80 cm),"Gift of J. E. Gombos, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.115,false,true,436189,European Paintings,Painting,Gendarmes d'Ordonnance,,,,,,Artist,,Édouard Detaille,"French, Paris 1848–1912 Paris",,"Detaille, Édouard",French,1848,1912,1894,1894,1894,Oil on canvas,22 x 16 5/8 in. (55.9 x 42.2 cm),"Gift of Estate of George Albert Draper, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436189,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.20.2,false,true,436188,European Paintings,Painting,The Defense of Champigny,,,,,,Artist,,Édouard Detaille,"French, Paris 1848–1912 Paris",,"Detaille, Édouard",French,1848,1912,1879,1879,1879,Oil on canvas,48 x 84 3/4 in. (121.9 x 215.3 cm),"Gift of Henry Hilton, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436188,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.136.5,false,true,436187,European Paintings,Painting,A Dragoon on Horseback,,,,,,Artist,,Édouard Detaille,"French, Paris 1848–1912 Paris",,"Detaille, Édouard",French,1848,1912,1876,1876,1876,Oil on wood,9 1/2 x 5 3/8 in. (24.1 x 13.7 cm),"Bequest of Martha T. Fiske Collord, in memory of her first husband, Josiah M. Fiske, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436187,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.8.12,false,true,436880,European Paintings,Painting,The Organ Rehearsal,,,,,,Artist,,Henry Lerolle,"French, Paris 1848–1929 Paris",,"Lerolle, Henry",French,1848,1929,1885,1885,1885,Oil on canvas,93 1/4 x 142 3/4 in. (236.9 x 362.6 cm),"Gift of George I. Seney, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.228,false,true,437366,European Paintings,Painting,"The Fletcher Mansion, New York City",,,,,,Artist,,Jean-François Raffaëlli,"French, Paris 1850–1924 Paris",,"Raffaëlli, Jean-François",French,1850,1924,1899,1899,1899,Oil on canvas,23 3/4 x 32 in. (60.3 x 81.3 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437366,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.80,false,true,436923,European Paintings,Painting,"Morning, Interior",,,,,,Artist,,Maximilien Luce,"French, Paris 1858–1941 Paris",,"Luce, Maximilien",French,1858,1941,1890,1890,1890,Oil on canvas,25 1/2 x 31 7/8 in. (64.8 x 81 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.16.5,false,true,437659,European Paintings,Painting,View of the Seine,,,,,,Artist,,Georges Seurat,"French, Paris 1859–1891 Paris",,"Seurat, Georges",French,1859,1891,1882–83,1882,1883,Oil on wood,6 1/4 x 9 3/4 in. (15.9 x 24.8 cm),"Bequest of Mabel Choate, in memory of her father, Joseph Hodges Choate, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.342,false,true,437657,European Paintings,Painting,Landscape at Saint-Ouen,,,,,,Artist,,Georges Seurat,"French, Paris 1859–1891 Paris",,"Seurat, Georges",French,1859,1891,1878 or 1879,1878,1879,"Oil on wood, mounted on wood",Overall 6 7/8 x 10 3/8 in. (17.5 x 26.4 cm); painted surface 6 5/8 x 10 in. (16.8 x 25.4 cm),"Gift of Bernice Richard, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.237,false,true,437655,European Paintings,Painting,The Forest at Pontaubert,,,,,,Artist,,Georges Seurat,"French, Paris 1859–1891 Paris",,"Seurat, Georges",French,1859,1891,1881,1881,1881,Oil on canvas,31 1/8 x 24 5/8 in. (79.1 x 62.5 cm),"Purchase, Gift of Raymonde Paul, in memory of her brother, C. Michael Paul, by exchange, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437655,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.112.6,false,true,437658,European Paintings,Painting,"Study for ""A Sunday on La Grande Jatte""",,,,,,Artist,,Georges Seurat,"French, Paris 1859–1891 Paris",,"Seurat, Georges",French,1859,1891,1884,1884,1884,Oil on canvas,27 3/4 x 41 in. (70.5 x 104.1 cm),"Bequest of Sam A. Lewisohn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.62.3,false,true,438015,European Paintings,Painting,"Gray Weather, Grande Jatte",,,,,,Artist,,Georges Seurat,"French, Paris 1859–1891 Paris",,"Seurat, Georges",French,1859,1891,ca. 1886–88,1886,1888,Oil on canvas,27 3/4 x 34 in. (70.5 x 86.4 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2002, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.17,true,true,437654,European Paintings,Painting,Circus Sideshow (Parade de cirque),,,,,,Artist,,Georges Seurat,"French, Paris 1859–1891 Paris",,"Seurat, Georges",French,1859,1891,1887–88,1887,1888,Oil on canvas,39 1/4 x 59 in. (99.7 x 149.9 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.102,false,true,437656,European Paintings,Painting,The Gardener,,,,,,Artist,,Georges Seurat,"French, Paris 1859–1891 Paris",,"Seurat, Georges",French,1859,1891,1882–83,1882,1883,Oil on wood,6 1/4 x 9 3/4 in. (15.9 x 24.8 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.220.1,false,true,437672,European Paintings,Painting,"Notre-Dame-de-la-Garde (La Bonne-Mère), Marseilles",,,,,,Artist,,Paul Signac,"French, Paris 1863–1935 Paris",,"Signac, Paul",French,1863,1935,1905–6,1905,1906,Oil on canvas,35 x 45 3/4 in. (88.9 x 116.2 cm),"Gift of Robert Lehman, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437672,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.19,false,true,437671,European Paintings,Painting,"The Jetty at Cassis, Opus 198",,,,,,Artist,,Paul Signac,"French, Paris 1863–1935 Paris",,"Signac, Paul",French,1863,1935,1889,1889,1889,Oil on canvas,18 1/4 x 25 5/8 in. (46.4 x 65.1 cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.17,false,true,436454,European Paintings,Painting,Alfred Dedreux (1810–1860) as a Child,,,,,,Artist,,Théodore Gericault,"French, Rouen 1791–1824 Paris",,"Gericault, Théodore",French,1791,1824,ca. 1819–20,1819,1820,Oil on canvas,18 x 15 in. (45.7 x 38.1 cm),"The Alfred N. Punnett Endowment Fund, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.5,false,true,441226,European Paintings,Painting,Lions in a Mountainous Landscape,,,,,,Artist,,Théodore Gericault,"French, Rouen 1791–1824 Paris",,"Gericault, Théodore",French,1791,1824,ca. 1818–20,1813,1825,Oil on wood,19 x 23 1/2 in. (48.3 x 59.7 cm),"Purchase, Nineteenth-Century, Modern, and Contemporary Funds and Lila Acheson Wallace Gift, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.183,true,true,436455,European Paintings,Painting,Evening: Landscape with an Aqueduct,,,,,,Artist,,Théodore Gericault,"French, Rouen 1791–1824 Paris",,"Gericault, Théodore",French,1791,1824,1818,1818,1818,Oil on canvas,98 1/2 x 86 1/2 in. (250.2 x 219.7 cm),"Purchase, Gift of James A. Moffett 2nd, in memory of George M. Moffett, by exchange, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436455,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.356.27,false,true,437844,European Paintings,Painting,Comedy,,,,,,Artist,,Pierre Charles Trémolières,"French, Cholet 1703–1739 Paris",,"Trémolières, Pierre Charles",French,1703,1739,ca. 1736,1731,1741,"Oil on canvas, enlarged",18 3/4 x 23 1/2 in. (47.6 x 59.7 cm),"The Lesley and Emma Sheafer Collection, Bequest of Emma A. Sheafer, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.30,false,true,436321,European Paintings,Painting,Roman Interior,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,ca. 1760,1755,1765,Oil on canvas,19 1/4 x 23 3/8 in. (48.9 x 59.4 cm),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436321,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.118,false,true,436323,European Paintings,Painting,A Woman with a Dog,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,ca. 1769,1764,1774,Oil on canvas,32 x 25 3/4 in. (81.3 x 65.4 cm),"Fletcher Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436323,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.161,false,true,436327,European Paintings,Painting,Allegory of Vigilance,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,ca. 1772,1767,1777,Oil on canvas,"Oval, 27 1/8 x 21 5/8 in. (68.9 x 54.9 cm)","Gift of René Fribourg, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.226,false,true,436320,European Paintings,Painting,Portrait of a Young Woman,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,1770s,1770,1779,Oil on canvas,"Oval, 31 3/4 x 25 in. (80.6 x 63.5 cm)","Bequest of Margaret V. Haggin, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.49,true,true,436322,European Paintings,Painting,The Love Letter,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,early 1770s,1770,1773,Oil on canvas,32 3/4 x 26 3/8 in. (83.2 x 67 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436322,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.50,false,true,436319,European Paintings,Painting,The Cascade,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,ca. 1775,1770,1780,Oil on wood,11 1/2 x 9 1/2 in. (29.2 x 24.1 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.51,false,true,436324,European Paintings,Painting,A Shaded Avenue,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,ca. 1775,1770,1780,Oil on wood,11 1/2 x 9 1/2 in. (29.2 x 24.1 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436324,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.61.5,false,true,436326,European Paintings,Painting,The Two Sisters,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,ca. 1769–70,1769,1770,Oil on canvas,28 1/4 x 22 in. (71.8 x 55.9 cm),"Gift of Julia A. Berwind, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.100.1,false,true,436325,European Paintings,Painting,The Stolen Kiss,,,,,,Artist,,Jean Honoré Fragonard,"French, Grasse 1732–1806 Paris",,"Fragonard, Jean Honoré",French,1732,1806,ca. 1760,1755,1765,Oil on canvas,19 x 25 in. (48.3 x 63.5 cm),"Gift of Jessie Woolworth Donahue, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436325,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.441,false,true,438603,European Paintings,Painting,"François Gérard (1770–1837), later Baron Gérard",,,,,,Artist,,baron Antoine Jean Gros,"French, Paris 1771–1835 Meudon",,"Gros, Antoine Jean, baron",French,1771,1835,ca. 1790,1785,1795,Oil on canvas,22 1/8 x 18 5/8 in. (56.2 x 47.3 cm),"Gift of Mrs. Charles Wrightsman, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.200,false,true,436483,European Paintings,Painting,Pygmalion and Galatea,,,,,,Artist,,Jean-Léon Gérôme,"French, Vesoul 1824–1904 Paris",,"Gérôme, Jean-Léon",French,1824,1904,ca. 1890,1885,1895,Oil on canvas,35 x 27 in. (88.9 x 68.6 cm),"Gift of Louis C. Raegner, 1927",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436483,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +05.13.4,false,true,436481,European Paintings,Painting,"Cafe House, Cairo (Casting Bullets)",,,,,,Artist,,Jean-Léon Gérôme,"French, Vesoul 1824–1904 Paris",,"Gérôme, Jean-Léon",French,1824,1904,1884 or earlier,1870,1884,Oil on canvas,21 1/2 x 24 3/4 in. (54.6 x 62.9 cm),"Bequest of Henry H. Cook, 1905",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.162.4,false,true,436484,European Paintings,Painting,Tiger and Cubs,,,,,,Artist,,Jean-Léon Gérôme,"French, Vesoul 1824–1904 Paris",,"Gérôme, Jean-Léon",French,1824,1904,ca. 1884,1879,1889,Oil on canvas,29 x 36 in. (73.7 x 91.4 cm),"Bequest of Susan P. Colgate, in memory of her husband, Romulus R. Colgate, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436484,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.130,false,true,436482,European Paintings,Painting,Prayer in the Mosque,,,,,,Artist,,Jean-Léon Gérôme,"French, Vesoul 1824–1904 Paris",,"Gérôme, Jean-Léon",French,1824,1904,1871,1871,1871,Oil on canvas,35 x 29 1/2 in. (88.9 x 74.9 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436482,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.547.1,true,true,440723,European Paintings,Painting,Bashi-Bazouk,,,,,,Artist,,Jean-Léon Gérôme,"French, Vesoul 1824–1904 Paris",,"Gérôme, Jean-Léon",French,1824,1904,1868–69,1868,1869,Oil on canvas,31 3/4 x 26 in. (80.6 x 66 cm),"Gift of Mrs. Charles Wrightsman, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440723,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.121,false,true,436026,European Paintings,Painting,Spring Flowers,,,,,,Artist,Copy after,Gustave Courbet,"French, second half 19th century",,"Courbet, Gustave",French,1819,1877,ca. 1855–60,1855,1860,Oil on canvas,23 3/4 x 32 1/4 in. (60.3 x 81.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436026,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.405,false,true,437894,European Paintings,Painting,Eugène Joseph Stanislas Foullon d'Écotier (1753–1821),,,,,,Artist,,Antoine Vestier,"French, Avallon 1740–1824 Paris",,"Vestier, Antoine",French,1740,1824,1785,1785,1785,Oil on canvas,"Oval, 31 5/8 x 25 1/8 in. (80.3 x 63.8 cm)","Gift of Mr. and Mrs. Charles Wrightsman, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437894,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.69,false,true,438559,European Paintings,Painting,The Sack of Jerusalem by the Romans,,,,,,Artist,,François Joseph Heim,"French, Belfort 1787–1865 Paris",,"Heim, François Joseph",French,1787,1865,1824,1824,1824,Oil on canvas,14 x 15 in. (35.6 x 38.1 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438559,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.8,false,true,438009,European Paintings,Painting,"The Pink Dress (Albertie-Marguerite Carré, later Madame Ferdinand-Henri Himmes, 1854–1935)",,,,,,Artist,,Berthe Morisot,"French, Bourges 1841–1895 Paris",,"Morisot, Berthe",French,1841,1895,ca. 1870,1865,1875,Oil on canvas,21 1/2 x 26 1/2 in. (54.6 x 67.3 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.89,false,true,437159,European Paintings,Painting,Young Woman Knitting,,,,,,Artist,,Berthe Morisot,"French, Bourges 1841–1895 Paris",,"Morisot, Berthe",French,1841,1895,ca. 1883,1878,1888,Oil on canvas,19 3/4 x 23 5/8 in. (50.2 x 60 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437159,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.97,false,true,435856,European Paintings,Painting,Self-Portrait,,,,,,Artist,,Eugène Carrière,"French, Gournay 1849–1906 Paris",,"Carrière, Eugène",French,1849,1906,ca. 1893,1888,1898,Oil on canvas,16 1/4 x 12 7/8 in. (41.3 x 32.7 cm),"Purchase, Albert Otten Foundation Gift, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435856,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.428,false,true,442849,European Paintings,Painting,Women Sewing at a Table,,,,,,Artist,,Eugène Carrière,"French, Gournay 1849–1906 Paris",,"Carrière, Eugène",French,1849,1906,ca. 1894–96,1894,1896,Oil on canvas,10 1/4 × 15 in. (26 × 38.1 cm),"Gift of Ariane and Alain Kirili, 2013",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/442849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.150.2,false,true,435627,European Paintings,Painting,In the Sun,,,,,,Artist,,Charles-Édouard de Beaumont,"French, Lannion 1821–1888 Paris",,"Beaumont, Charles-Édouard de",French,1821,1888,1875,1875,1875,Oil on canvas,23 1/2 x 37 3/4 in. (59.7 x 95.9 cm),"Gift of Estate of Marie L. Russell, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435627,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.660,false,true,496203,European Paintings,Painting,Woman Standing Beside Railing with Poodle,,,,,,Artist,,Paul Ranson,"French, Limoges 1864–1909 Paris",,"Ranson, Paul",French,1864,1909,ca. 1895,1895,1895,Oil on panel,33 1/2 x 11 5/8 in. (85.1 x 29.5 cm),"Gift of Mrs. Patricia Altschul, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/496203,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.32,false,true,437104,European Paintings,Painting,Dr. Leclenché,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1864,1864,1864,Oil on canvas,18 x 12 3/4 in. (45.7 x 32.4 cm),"Gift of Mr. and Mrs. Edwin C. Vogel, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.183,false,true,437130,European Paintings,Painting,Apples and Grapes,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1879–80,1879,1880,Oil on canvas,26 5/8 x 35 1/4 in. (67.6 x 89.5 cm),"Gift of Henry R. Luce, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437130,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.142,false,true,437108,European Paintings,Painting,The Parc Monceau,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1878,1878,1878,Oil on canvas,28 5/8 x 21 3/8 in. (72.7 x 54.3 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.206,false,true,437107,European Paintings,Painting,Landscape: The Parc Monceau,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1876,1876,1876,Oil on canvas,23 1/2 x 32 1/2 in. (59.7 x 82.6 cm),"Bequest of Loula D. Lasker, New York City, 1961",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.154,false,true,437126,European Paintings,Painting,Île aux Orties near Vernon,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1897,1897,1897,Oil on canvas,28 7/8 x 36 1/2 in. (73.3 x 92.7 cm),"Gift of Mr. and Mrs. Charles S. McVeigh, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437126,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.210,false,true,437131,European Paintings,Painting,"The Bodmer Oak, Fontainebleau Forest",,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1865,1865,1865,Oil on canvas,37 7/8 x 50 7/8 in. (96.2 x 129.2 cm),"Gift of Sam Salz and Bequest of Julia W. Emmons, by exchange, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437131,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.241,true,true,437133,European Paintings,Painting,Garden at Sainte-Adresse,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1867,1867,1867,Oil on canvas,38 5/8 x 51 1/8 in. (98.1 x 129.9 cm),"Purchase, special contributions and funds given or bequeathed by friends of the Museum, 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437133,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.30.3,false,true,437111,European Paintings,Painting,Vétheuil in Summer,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1880,1880,1880,Oil on canvas,23 5/8 x 39 1/4 in. (60 x 99.7 cm),"Bequest of William Church Osborn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437111,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.30.4,false,true,437136,European Paintings,Painting,Regatta at Sainte-Adresse,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1867,1867,1867,Oil on canvas,29 5/8 x 40 in. (75.2 x 101.6 cm),"Bequest of William Church Osborn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.30.5,false,true,438823,European Paintings,Painting,The Manneporte (Étretat),,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1883,1883,1883,Oil on canvas,25 3/4 x 32 in. (65.4 x 81.3 cm),"Bequest of William Church Osborn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.532,false,true,437137,European Paintings,Painting,Water Lilies,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1916–19,1916,1919,Oil on canvas,51 1/4 x 79 in. (130.2 x 200.7 cm),"Gift of Louise Reinhardt Smith, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.341,false,true,437113,European Paintings,Painting,Cabin of the Customs Watch,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1882,1882,1882,Oil on canvas,24 x 32 1/4 in. (61 x 81.9 cm),"Bequest of Julia B. Engel, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437113,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.195,false,true,438435,European Paintings,Painting,Jean Monet (1867–1913) on His Hobby Horse,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1872,1872,1872,Oil on canvas,23 7/8 x 29 1/4 in. (60.6 x 74.3 cm),"Gift of Sara Lee Corporation, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.186.1,false,true,437106,European Paintings,Painting,Spring (Fruit Trees in Bloom),,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1873,1873,1873,Oil on canvas,24 1/2 x 39 5/8 in. (62.2 x 100.6 cm),"Bequest of Mary Livingston Willard, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +31.67.11,false,true,437119,European Paintings,Painting,The Manneporte near Étretat,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1886,1886,1886,Oil on canvas,32 x 25 3/4 in. (81.3 x 65.4 cm),"Bequest of Lillie P. Bliss, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437119,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.135.1,false,true,437110,European Paintings,Painting,View of Vétheuil,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1880,1880,1880,Oil on canvas,31 1/2 x 23 3/4 in. (80 x 60.3 cm),"Bequest of Julia W. Emmons, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437110,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.135.4,false,true,437125,European Paintings,Painting,Morning on the Seine near Giverny,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1897,1897,1897,Oil on canvas,32 1/8 x 36 5/8 in. (81.6 x 93 cm),"Bequest of Julia W. Emmons, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437125,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.135.5,false,true,437138,European Paintings,Painting,Île aux Fleurs near Vétheuil,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1880,1880,1880,Oil on canvas,26 x 32 in. (66 x 81.3 cm),"Bequest of Julia W. Emmons, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437138,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.135.6,false,true,437128,European Paintings,Painting,The Houses of Parliament (Effect of Fog),,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1903–4,1903,1904,Oil on canvas,32 x 36 3/8 in. (81.3 x 92.4 cm),"Bequest of Julia W. Emmons, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437128,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.188.1,false,true,437129,European Paintings,Painting,The Doge's Palace Seen from San Giorgio Maggiore,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1908,1908,1908,Oil on canvas,25 3/4 x 36 1/2 in. (65.4 x 92.7 cm),"Gift of Mr. and Mrs. Charles S. McVeigh, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437129,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.93.1,false,true,438005,European Paintings,Painting,Camille Monet (1847–1879) in the Garden at Argenteuil,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1876,1876,1876,Oil on canvas,32 1/8 x 23 5/8 in. (81.6 x 60 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2000, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.62.1,false,true,438003,European Paintings,Painting,Camille Monet (1847–1879) on a Garden Bench,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1873,1873,1873,Oil on canvas,23 7/8 x 31 5/8 in. (60.6 x 80.3 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2002, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.7,false,true,438006,European Paintings,Painting,"The Stroller (Suzanne Hoschedé, later Mrs. Theodore Earl Butler, 1868–1899)",,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1887,1887,1887,Oil on canvas,39 5/8 x 27 3/4 in. (100.6 x 70.5 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.250,false,true,437124,European Paintings,Painting,Rouen Cathedral: The Portal (Sunlight),,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1894,1894,1894,Oil on canvas,39 1/4 x 25 7/8 in. (99.7 x 65.7 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437124,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.251,false,true,437118,European Paintings,Painting,The Valley of the Nervia,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1884,1884,1884,Oil on canvas,26 x 32 in. (66 x 81.3 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.95.271,false,true,437109,European Paintings,Painting,The Seine at Vétheuil,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1880,1880,1880,Oil on canvas,23 3/4 x 39 1/2 in. (60.3 x 100.3 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, 1915",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437109,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.87,false,true,437117,European Paintings,Painting,Palm Trees at Bordighera,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1884,1884,1884,Oil on canvas,25 1/2 x 32in. (64.8 x 81.3cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437117,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.88,false,true,437120,European Paintings,Painting,Rapids on the Petite Creuse at Fresselines,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1889,1889,1889,Oil on canvas,25 3/4 x 36 1/8 in. (65.4 x 91.8 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.325.2,false,true,438008,European Paintings,Painting,Water Lilies,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1919,1919,1919,Oil on canvas,39 3/4 x 78 3/4 in. (101 x 200 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1998, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.202.5,false,true,438004,European Paintings,Painting,Poppy Fields near Argenteuil,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1875,1875,1875,Oil on canvas,21 1/4 x 29 in. (54 x 73.7 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2001, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.202.6,false,true,438007,European Paintings,Painting,The Path through the Irises,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1914–17,1914,1917,Oil on canvas,78 7/8 x 70 7/8 in. (200.3 x 180 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2001, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.106,false,true,437115,European Paintings,Painting,Chrysanthemums,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1882,1882,1882,Oil on canvas,39 1/2 x 32 1/4 in. (100.3 x 81.9 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.107,false,true,437112,European Paintings,Painting,Bouquet of Sunflowers,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1881,1881,1881,Oil on canvas,39 3/4 x 32 in. (101 x 81.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437112,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.108,false,true,437123,European Paintings,Painting,Ice Floes,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1893,1893,1893,Oil on canvas,26 x 39 1/2 in. (66 x 100.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437123,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.109,false,true,437122,European Paintings,Painting,Haystacks (Effect of Snow and Sun),,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1891,1891,1891,Oil on canvas,25 3/4 x 36 1/4 in. (65.4 x 92.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.110,false,true,437121,European Paintings,Painting,The Four Trees,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1891,1891,1891,Oil on canvas,32 1/4 x 32 1/8 in. (81.9 x 81.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437121,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.111,false,true,437105,European Paintings,Painting,The Green Wave,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,ca. 1866–67,1861,1871,Oil on canvas,19 1/8 x 25 1/2 in. (48.6 x 64.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.112,false,true,437135,European Paintings,Painting,La Grenouillère,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1869,1869,1869,Oil on canvas,29 3/8 x 39 1/4 in. (74.6 x 99.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437135,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.113,false,true,437127,European Paintings,Painting,Bridge over a Pond of Water Lilies,,,,,,Artist,,Claude Monet,"French, Paris 1840–1926 Giverny",,"Monet, Claude",French,1840,1926,1899,1899,1899,Oil on canvas,36 1/2 x 29 in. (92.7 x 73.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437127,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1233.2,false,true,436075,European Paintings,Painting,Madonna of the Rose,,,,,,Artist,,Pascal-Adolphe-Jean Dagnan-Bouveret,"French, Paris 1852–1929 Quincey",,"Dagnan-Bouveret, Pascal-Adolphe-Jean",French,1852,1929,1885,1885,1885,Oil on canvas,33 3/4 x 27 in. (85.7 x 68.6 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436075,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +31.132.34,false,true,436076,European Paintings,Painting,The Pardon in Brittany,,,,,,Artist,,Pascal-Adolphe-Jean Dagnan-Bouveret,"French, Paris 1852–1929 Quincey",,"Dagnan-Bouveret, Pascal-Adolphe-Jean",French,1852,1929,1886,1886,1886,Oil on canvas,45 1/8 x 33 3/8 in. (114.6 x 84.8 cm),"Gift of George F. Baker, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.220,false,true,437846,European Paintings,Painting,Going to Market,,,,,,Artist,,Constant Troyon,"French, Sèvres 1810–1865 Paris",,"Troyon, Constant",French,1810,1865,1860,1860,1860,Oil on canvas,16 1/8 x 12 7/8 in. (41 x 32.7 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437846,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.111,false,true,436866,European Paintings,Painting,Graziella,,,,,,Artist,,Jules-Joseph Lefebvre,"French, Tournan 1836–1912 Paris",,"Lefebvre, Jules-Joseph",French,1836,1912,1878,1878,1878,Oil on canvas,78 3/4 x 44 1/4 in. (200 x 112.4 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.91,false,true,436583,European Paintings,Painting,Study Head of a Woman,,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,ca. 1780,1775,1785,Oil on wood,18 1/2 x 16 in. (47 x 40.6 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.55.3,false,true,436587,European Paintings,Painting,Jean Jacques Caffiéri (1725–1792),,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,ca. 1763,1758,1768,Oil on canvas,"Oval, 25 1/4 x 20 3/4 in. (64.1 x 52.7 cm)","Bequest of Ethel Tod Humphrys, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436587,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.28.1,false,true,436581,European Paintings,Painting,"Charles Claude de Flahaut (1730–1809), Comte d'Angiviller",,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,1763,1763,1763,Oil on canvas,25 1/4 x 21 1/4 in. (64.1 x 54 cm),"Gift of Edith C. Blum (et al.) Executors, in memory of Mr. and Mrs. Albert Blum, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1970.295,false,true,436580,European Paintings,Painting,Aegina Visited by Jupiter,,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,ca. 1767–69,1767,1769,Oil on canvas,57 7/8 x 77 1/8 in. (147 x 195.9 cm),"Gift of Harry N. Abrams and Purchase, Joseph Pulitzer Bequest, Pfeiffer, Fletcher, and Rogers Funds, 1970",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.8,true,true,436579,European Paintings,Painting,Broken Eggs,,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,1756,1756,1756,Oil on canvas,28 3/4 x 37 in. (73 x 94 cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.205.2,false,true,436584,European Paintings,Painting,"Madame Jean-Baptiste Nicolet (Anne Antoinette Desmoulins, 1743–1817)",,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,late 1780s,1787,1789,Oil on wood,25 1/4 x 21 in. (64.1 x 53.3 cm),"Gift of Colonel and Mrs. Jacques Balsan, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.242.3,false,true,436586,European Paintings,Painting,Princess Varvara Nikolaevna Gagarina (1762–1802),,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,ca. 1780–82,1780,1782,Oil on canvas,"Oval, 31 1/2 x 25 in. (80 x 63.5 cm)","Gift of Mrs. William M. Haupt, from the collection of Mrs. James B. Haggin, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436586,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.72,false,true,436582,European Paintings,Painting,Head of a Young Woman,,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,possibly 1780s,1780,1789,Oil on canvas,16 1/8 x 12 3/4 in. (41 x 32.4 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.137,false,true,436588,European Paintings,Painting,Head of a Young Boy,,,,,,Artist,,Jean-Baptiste Greuze,"French, Tournus 1725–1805 Paris",,"Greuze, Jean-Baptiste",French,1725,1805,1763,1763,1763,Oil on canvas,18 7/8 x 15 3/8 in. (47.9 x 39.1 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436588,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.144,false,true,437885,European Paintings,Painting,The Triumph of Aemilius Paulus,,,,,,Artist,,Carle (Antoine Charles Horace) Vernet,"French, Bordeaux 1758–1836 Paris",,"Vernet, Carle (Antoine Charles Horace)",French,1758,1836,1789,1789,1789,Oil on canvas,51 1/8 x 172 1/2 in. (129.9 x 438.2 cm),"Gift of Darius O. Mills, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +27.29,false,true,437380,European Paintings,Painting,The Chariot of Apollo,,,,,,Artist,,Odilon Redon,"French, Bordeaux 1840–1916 Paris",,"Redon, Odilon",French,1840,1916,1905–16,1905,1916,Oil on canvas,26 x 32 in. (66 x 81.3 cm),"Anonymous Gift, 1927",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437380,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.266,false,true,437378,European Paintings,Painting,Bouquet in a Chinese Vase,,,,,,Artist,,Odilon Redon,"French, Bordeaux 1840–1916 Paris",,"Redon, Odilon",French,1840,1916,ca. 1912–14,1890,1916,Oil on canvas,25 1/2 x 19 5/8 in. (64.8 x 49.8 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437378,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.16.3,false,true,437382,European Paintings,Painting,Vase of Flowers (Pink Background),,,,,,Artist,,Odilon Redon,"French, Bordeaux 1840–1916 Paris",,"Redon, Odilon",French,1840,1916,ca. 1906,1901,1911,Oil on canvas,28 5/8 x 21 1/4 in. (72.7 x 54 cm),"Bequest of Mabel Choate, in memory of her father, Joseph Hodges Choate, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437382,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.19.1,false,true,437383,European Paintings,Painting,Pandora,,,,,,Artist,,Odilon Redon,"French, Bordeaux 1840–1916 Paris",,"Redon, Odilon",French,1840,1916,ca. 1914,1909,1919,Oil on canvas,56 1/2 x 24 1/2 in. (143.5 x 62.2 cm),"Bequest of Alexander M. Bing, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437383,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.140.5,false,true,437381,European Paintings,Painting,Etruscan Vase with Flowers,,,,,,Artist,,Odilon Redon,"French, Bordeaux 1840–1916 Paris",,"Redon, Odilon",French,1840,1916,1900–1910,1900,1910,Tempera on canvas,32 x 23 1/4 in. (81.3 x 59.1 cm),"Maria DeWitt Jesup Fund, 1951; acquired from The Museum of Modern Art, Lillie P. Bliss Collection",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437381,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.34,false,true,435886,European Paintings,Painting,Jean-Baptiste Colbert (1619–1683),,,,,,Artist,,Philippe de Champaigne,"French, Brussels 1602–1674 Paris",,"Champaigne, Philippe de",French,1602,1674,1655,1655,1655,Oil on canvas,36 1/4 x 28 1/2 in. (92.1 x 72.4 cm),"Gift of The Wildenstein Foundation Inc., 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.31,false,true,438724,European Paintings,Painting,The Annunciation,,,,,,Artist,,Philippe de Champaigne,"French, Brussels 1602–1674 Paris",,"Champaigne, Philippe de",French,1602,1674,ca. 1644,1639,1649,Oil on oak,"Overall, 28 x 28 3/4 in. (71.1 x 73 cm); painted surface, 27 1/4 x 27 3/4 in. (69.2 x 70.5 cm)","Wrightsman Fund, 2004",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438724,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.41,false,true,436295,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1885,1885,1885,Oil on canvas,39 1/2 x 32 in. (100.3 x 81.3 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1910",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.3,false,true,436293,European Paintings,Painting,Still Life with Flowers and Fruit,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1866,1866,1866,Oil on canvas,28 3/4 x 23 5/8 in. (73 x 60 cm),"Purchase, Mr. and Mrs. Richard J. Bernhard Gift, by exchange, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436293,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.194,false,true,436294,European Paintings,Painting,Still Life with Pansies,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1874,1874,1874,Oil on canvas,18 1/2 x 22 1/4 in. (47 x 56.5 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436294,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.91,false,true,436297,European Paintings,Painting,Self-Portrait,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,ca. 1858,1856,1904,"Oil on canvas, laid down on canvas",Overall 10 3/8 x 8 3/8 in. (26.4 x 21.3 cm); original canvas 10 x 7 7/8 in. (25.4 x 20 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1995",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436297,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.119,false,true,436292,European Paintings,Painting,Still Life with Roses and Fruit,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1863,1863,1863,Oil on canvas,13 5/8 x 16 3/8 in. (34.6 x 41.6 cm),"Bequest of Alice A. Hay, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436292,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.517,false,true,437985,European Paintings,Painting,Pansies,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1903,1903,1903,Oil on canvas,9 x 11 1/8 in. (22.9 x 28.3 cm),"Gift of Paul O. Fabri, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437985,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.347,false,true,438031,European Paintings,Painting,Summer Flowers,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1880,1880,1880,Oil on canvas,20 x 24 3/8 in. (50.8 x 61.9 cm),"Gift of Susan S. Dillon, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438031,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.636,false,true,629928,European Paintings,Painting,Potted Pansies,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1883,1883,1883,Oil on canvas,11 × 13 1/2 in. (27.9 × 34.3 cm),"Gift of Susan S. Dillon, 2013",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/629928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.280.9,false,true,436296,European Paintings,Painting,The Palace of Aurora,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1902,1856,1904,Oil on canvas,18 1/8 x 15 in. (46 x 38.1 cm),"Bequest of Anne D. Thomson, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436296,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.6,false,true,437995,European Paintings,Painting,Roses in a Bowl,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1883,1883,1883,Oil on canvas,11 3/4 x 16 3/8 in. (29.8 x 41.6 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437995,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.202.3,false,true,437997,European Paintings,Painting,Asters and Fruit on a Table,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1868,1868,1868,Oil on canvas,22 3/8 x 21 5/8 in. (56.8 x 54.9 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2001, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.114,false,true,437647,European Paintings,Painting,Spring in Brittany,,,,,,Artist,,Paul Sébillot,"French, Matignon 1843–1918 Paris",,"Sébillot, Paul",French,1843,1918,1874,1874,1874,Oil on wood,14 x 10 3/4 in. (35.6 x 27.3 cm),"Gift of Paul-Yves Sébillot, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437647,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.57,false,true,437235,European Paintings,Painting,Ducks Resting in Sunshine,,,,,,Artist,,Jean-Baptiste Oudry,"French, Paris 1686–1755 Beauvais",,"Oudry, Jean-Baptiste",French,1686,1755,1753,1753,1753,Oil on canvas,25 1/2 x 31 3/4 in. (64.8 x 80.6 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.89,false,true,437234,European Paintings,Painting,Dog Guarding Dead Game,,,,,,Artist,,Jean-Baptiste Oudry,"French, Paris 1686–1755 Beauvais",,"Oudry, Jean-Baptiste",French,1686,1755,1753,1753,1753,Oil on canvas,25 1/2 x 31 3/4 in. (64.8 x 80.6 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437234,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +31.45,true,true,436105,European Paintings,Painting,The Death of Socrates,,,,,,Artist,,Jacques Louis David,"French, Paris 1748–1825 Brussels",,"David, Jacques Louis",French,1748,1825,1787,1787,1787,Oil on canvas,51 x 77 1/4 in. (129.5 x 196.2 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436105,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.10,true,true,436106,European Paintings,Painting,"Antoine-Laurent Lavoisier (1743–1794) and His Wife (Marie-Anne-Pierrette Paulze, 1758–1836)",,,,,,Artist,,Jacques Louis David,"French, Paris 1748–1825 Brussels",,"David, Jacques Louis",French,1748,1825,1788,1788,1788,Oil on canvas,102 1/4 x 76 5/8 in. (259.7 x 194.6 cm),"Purchase, Mr. and Mrs. Charles Wrightsman Gift, in honor of Everett Fahy, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436106,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.14.5,false,true,436107,European Paintings,Painting,General Étienne-Maurice Gérard (1773–1852),,,,,,Artist,,Jacques Louis David,"French, Paris 1748–1825 Brussels",,"David, Jacques Louis",French,1748,1825,1816,1816,1816,Oil on canvas,77 5/8 x 53 5/8 in. (197.2 x 136.2 cm),"Purchase, Rogers and Fletcher Funds, and Mary Wetmore Shively Bequest, in memory of her husband, Henry L. Shively, M.D., 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436107,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.4,false,true,438816,European Paintings,Painting,The Forest in Winter at Sunset,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,ca. 1846–67,1846,1867,Oil on canvas,64 x 102 3/8 in. (162.6 x 260 cm),"Gift of P. A. B. Widener, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438816,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +03.28,false,true,437516,European Paintings,Painting,An Old Chapel in a Valley,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,ca. 1835,1832,1867,Oil on wood,10 1/2 x 13 7/8 in. (26.7 x 35.2 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1903",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437516,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.27,false,true,437514,European Paintings,Painting,"The Edge of the Woods at Monts-Girard, Fontainebleau Forest",,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,1852–54,1852,1854,Oil on wood,31 1/2 x 48 in. (80 x 121.9 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1896",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.45.5,false,true,437515,European Paintings,Painting,A Meadow Bordered by Trees,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,ca. 1845,1840,1850,Oil on wood,16 3/8 x 24 3/8 in. (41.6 x 61.9 cm),"Bequest of Robert Graham Dun, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.4,false,true,437520,European Paintings,Painting,Sunset near Arbonne,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,ca. 1860–65,1860,1865,Oil on wood,25 1/4 x 39 in. (64.1 x 99.1 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.814,false,true,437517,European Paintings,Painting,A Path among the Rocks,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,probably 1861,1832,1867,Oil on wood,15 x 23 5/8 in. (38.1 x 60 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437517,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.52,false,true,437518,European Paintings,Painting,A River in a Meadow,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,probably late 1830s–early 1840s,1835,1845,Oil on wood,16 3/4 x 26 1/8 in. (42.5 x 66.4 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437518,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.320,false,true,437513,European Paintings,Painting,Still Life with Ham,,,,,,Artist,,Philippe Rousseau,"French, Paris 1816–1887 Acquigny",,"Rousseau, Philippe",French,1816,1887,1870s,1870,1879,Oil on canvas,28 3/4 x 36 1/4 in. (73 x 92.1 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +81.1.656,false,true,435603,European Paintings,Painting,A Footman Sleeping,,,,,,Artist,,Charles Bargue,"French, Paris 1825/26–1883 Paris",,"Bargue, Charles",French,1825,1883,1871,1871,1871,Oil on wood,13 3/4 x 10 1/4 in. (34.9 x 26 cm),"Bequest of Stephen Whitney Phoenix, 1881",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.102,false,true,435604,European Paintings,Painting,A Bashi-Bazouk,,,,,,Artist,,Charles Bargue,"French, Paris 1825/26–1883 Paris",,"Bargue, Charles",French,1825,1883,1875,1875,1875,Oil on canvas,18 1/4 x 13 1/8 in. (46.4 x 33.3 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.95,false,true,437384,European Paintings,Painting,Salomé,,,,,,Artist,,Henri Regnault,"French, Paris 1843–1871 Buzenval",,"Regnault, Henri",French,1843,1871,1870,1870,1870,Oil on canvas,63 x 40 1/2 in. (160 x 102.9 cm),"Gift of George F. Baker, 1916",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437384,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.42.54,false,true,438677,European Paintings,Painting,"The Banks of the Rance, Brittany",,,,,,Artist,,Pierre Henri de Valenciennes,"French, Toulouse 1750–1819 Paris",,"Valenciennes, Pierre Henri de",French,1750,1819,possibly 1785,1785,1819,"Oil on paper, laid down on canvas",8 3/8 x 19 3/8 in. (21.3 x 49.2 cm),"The Whitney Collection, Gift of Wheelock Whitney III, and Purchase, Gift of Mr. and Mrs. Charles S. McVeigh, by exchange, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438677,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.819,false,true,436193,European Paintings,Painting,The Edge of the Woods,,,,,,Artist,,Narcisse-Virgile Diaz de la Peña,"French, Bordeaux 1808–1876 Menton",,"Diaz de la Peña, Narcisse-Virgile",French,1808,1876,1872,1872,1872,Oil on wood,14 7/8 x 18 1/2 in. (37.8 x 47 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436193,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.30,false,true,436191,European Paintings,Painting,Diana,,,,,,Artist,,Narcisse-Virgile Diaz de la Peña,"French, Bordeaux 1808–1876 Menton",,"Diaz de la Peña, Narcisse-Virgile",French,1808,1876,1849,1849,1849,Oil on canvas,46 1/2 x 27 3/4 in. (118.1 x 70.5 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436191,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.92,false,true,436196,European Paintings,Painting,The Forest of Fontainebleau,,,,,,Artist,,Narcisse-Virgile Diaz de la Peña,"French, Bordeaux 1808–1876 Menton",,"Diaz de la Peña, Narcisse-Virgile",French,1808,1876,1874,1874,1874,Oil on wood,18 5/8 x 23 5/8 in. (47.3 x 60 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436196,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.214,false,true,436192,European Paintings,Painting,Autumn: The Woodland Pond,,,,,,Artist,,Narcisse-Virgile Diaz de la Peña,"French, Bordeaux 1808–1876 Menton",,"Diaz de la Peña, Narcisse-Virgile",French,1808,1876,1867,1867,1867,Oil on canvas,19 3/4 x 26 in. (50.2 x 66 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436192,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.230,false,true,436195,European Paintings,Painting,A Vista through Trees: Fontainebleau,,,,,,Artist,,Narcisse-Virgile Diaz de la Peña,"French, Bordeaux 1808–1876 Menton",,"Diaz de la Peña, Narcisse-Virgile",French,1808,1876,1873,1873,1873,Oil on wood,12 3/4 x 17 1/4 in. (32.4 x 43.8 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.75,false,true,437099,European Paintings,Painting,Woman with a Rake,,,,,,Artist,,Jean-François Millet,"French, Gruchy 1814–1875 Barbizon",,"Millet, Jean-François",French,1814,1875,probably 1856–57,1856,1857,Oil on canvas,15 5/8 x 13 1/2 in. (39.7 x 34.3 cm),"Gift of Stephen C. Clark, 1938",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.151,false,true,437095,European Paintings,Painting,Calling the Cows Home,,,,,,Artist,,Jean-François Millet,"French, Gruchy 1814–1875 Barbizon",,"Millet, Jean-François",French,1814,1875,ca. 1872,1872,1872,Oil on wood,37 1/4 x 25 1/2 in. (94.6 x 64.8 cm),"Gift of Mrs. Arthur Whitney, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.24,false,true,437096,European Paintings,Painting,Garden Scene,,,,,,Artist,,Jean-François Millet,"French, Gruchy 1814–1875 Barbizon",,"Millet, Jean-François",French,1814,1875,1854,1854,1854,Oil on canvas,6 3/4 x 8 3/8 in. (17.1 x 21.3 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437096,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.446,false,true,437098,European Paintings,Painting,Shepherdess Seated on a Rock,,,,,,Artist,,Jean-François Millet,"French, Gruchy 1814–1875 Barbizon",,"Millet, Jean-François",French,1814,1875,1856,1856,1856,Oil on wood,14 1/8 x 11 1/8 in. (35.9 x 28.3 cm),"Gift of Douglas Dillon, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.613,false,true,438616,European Paintings,Painting,Retreat from the Storm,,,,,,Artist,,Jean-François Millet,"French, Gruchy 1814–1875 Barbizon",,"Millet, Jean-François",French,1814,1875,ca. 1846,1846,1846,Oil on canvas,18 1/4 x 15 in. (46.4 x 38.1 cm),"Gift of Sarina Tang and Peter M. Wood, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.12,false,true,437097,European Paintings,Painting,Haystacks: Autumn,,,,,,Artist,,Jean-François Millet,"French, Gruchy 1814–1875 Barbizon",,"Millet, Jean-François",French,1814,1875,ca. 1874,1869,1875,Oil on canvas,33 1/2 x 43 3/8 in. (85.1 x 110.2 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437097,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.209,false,true,437094,European Paintings,Painting,Autumn Landscape with a Flock of Turkeys,,,,,,Artist,,Jean-François Millet,"French, Gruchy 1814–1875 Barbizon",,"Millet, Jean-François",French,1814,1875,1872–73,1872,1873,Oil on canvas,31 7/8 x 39 in. (81 x 99.1 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437094,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.371,false,true,438032,European Paintings,Painting,"Study for ""Portrait of an Indian""",,,,,,Artist,,Anne Louis Girodet-Trioson,"French, Montargis 1767–1824 Paris",,"Girodet-Trioson, Anne Louis",French,1767,1824,ca. 1807,1802,1812,Oil on canvas,16 x 12 7/8 in. (40.6 x 32.7 cm),"Purchase, Gift of Joanne Toor Cummings, by exchange, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.101,false,true,438389,European Paintings,Painting,"Madame Jacques-Louis-Étienne Reizet (Colette-Désirée-Thérèse Godefroy, 1782–1850)",,,,,,Artist,,Anne Louis Girodet-Trioson,"French, Montargis 1767–1824 Paris",,"Girodet-Trioson, Anne Louis",French,1767,1824,1823,1823,1823,Oil on canvas,23 3/4 x 19 1/2 in. (60.3 x 49.5 cm),"Purchase, Gifts of Joanne Toor Cummings, Mr. and Mrs. Richard Rodgers, Raymonde Paul, and Estate of Dorothy Lichtensteiger, by exchange, 1999",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438389,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.65,false,true,436708,European Paintings,Painting,Odalisque in Grisaille,,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",and Workshop,"Ingres, Jean Auguste Dominique",French,1780,1867,ca. 1824–34,1824,1834,Oil on canvas,32 3/4 x 43 in. (83.2 x 109.2 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1938",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.77.1,false,true,436706,European Paintings,Painting,Jacques-Louis Leblanc (1774–1846),,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",,"Ingres, Jean Auguste Dominique",French,1780,1867,1823,1823,1823,Oil on canvas,47 5/8 x 37 5/8 in. (121 x 95.6 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436706,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.77.2,true,true,436703,European Paintings,Painting,"Madame Jacques-Louis Leblanc (Françoise Poncelle, 1788–1839)",,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",,"Ingres, Jean Auguste Dominique",French,1780,1867,1823,1823,1823,Oil on canvas,47 x 36 1/2 in. (119.4 x 92.7 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1918",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.85.2,false,true,436704,European Paintings,Painting,Edmond Cavé (1794–1852),,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",,"Ingres, Jean Auguste Dominique",French,1780,1867,1844,1844,1844,Oil on canvas,16 x 12 7/8 in. (40.6 x 32.7 cm),"Bequest of Grace Rainey Rogers, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.85.3,false,true,436707,European Paintings,Painting,"Madame Edmond Cavé (Marie-Élisabeth Blavot, born 1810)",,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",,"Ingres, Jean Auguste Dominique",French,1780,1867,ca. 1831–34,1826,1839,Oil on canvas,16 x 12 7/8 in. (40.6 x 32.7 cm),"Bequest of Grace Rainey Rogers, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.186,false,true,438434,European Paintings,Painting,The Virgin Adoring the Host,,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",,"Ingres, Jean Auguste Dominique",French,1780,1867,1852,1852,1852,Oil on canvas,15 7/8 x 12 7/8 in. (40.3 x 32.7 cm),"Gift of Lila and Herman Shickman, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.23,true,true,438818,European Paintings,Painting,Joseph-Antoine Moltedo (born 1775),,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",,"Ingres, Jean Auguste Dominique",French,1780,1867,ca. 1810,1805,1815,Oil on canvas,29 5/8 x 22 7/8 in. (75.2 x 58.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.6,false,true,436183,European Paintings,Painting,Departure of the Amazons,,,,,,Artist,,Claude Déruet,"French, Nancy ca. 1588–1660 Nancy",,"Déruet, Claude",French,1583,1660,1620s,1620,1629,Oil on canvas,20 x 26 in. (50.8 x 66 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.7,false,true,436182,European Paintings,Painting,Triumph of the Amazons,,,,,,Artist,,Claude Déruet,"French, Nancy ca. 1588–1660 Nancy",,"Déruet, Claude",French,1583,1660,1620s,1620,1629,Oil on canvas,20 1/4 x 26 in. (51.4 x 66 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436182,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.204,false,true,437903,European Paintings,Painting,Marie Joséphine Charlotte du Val d'Ognes (died 1868),,,,,,Artist,,Marie Denise Villers,"French, Paris 1774–1821 Paris (?)",,"Villers, Marie Denise",French,1774,1821,1801,1801,1801,Oil on canvas,63 1/2 x 50 5/8in. (161.3 x 128.6cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.6,false,true,437464,European Paintings,Painting,Louis XV (1710–1774) at the Age of Five in the Costume of the Sacre,,,,,,Artist,,Hyacinthe Rigaud,"French, Perpignan 1659–1743 Paris",and Workshop,"Rigaud, Hyacinthe",French,1659,1743,ca. 1716–24,1716,1724,Oil on canvas,77 x 55 1/2 in. (195.6 x 141 cm),"Purchase, Mary Wetmore Shively Bequest, in memory of her husband, Henry L. Shively, M.D., 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.119,false,true,437463,European Paintings,Painting,Portrait of a Military Officer,,,,,,Artist,,Hyacinthe Rigaud,"French, Perpignan 1659–1743 Paris",,"Rigaud, Hyacinthe",French,1659,1743,ca. 1710,1705,1715,Oil on canvas,54 x 41 3/8 in. (137.2 x 105.1 cm),"The Alfred N. Punnett Endowment Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437463,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.733,false,true,437465,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Hyacinthe Rigaud,"French, Perpignan 1659–1743 Paris",,"Rigaud, Hyacinthe",French,1659,1743,1693,1693,1693,Oil on canvas,"Oval, 32 1/2 x 25 3/4 in. (82.6 x 65.4 cm)","Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437465,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.54,false,true,436644,European Paintings,Painting,A Bather (Echo),,,,,,Artist,,Jean-Jacques Henner,"French, Bernwiller 1829–1905 Paris",,"Henner, Jean-Jacques",French,1829,1905,1881,1881,1881,Oil on canvas,38 1/8 x 27 3/4 in. (96.8 x 70.5 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.25,true,true,435702,European Paintings,Painting,The Horse Fair,,,,,,Artist,,Rosa Bonheur,"French, Bordeaux 1822–1899 Thomery",,"Bonheur, Rosa",French,1822,1899,1852–55,1852,1855,Oil on canvas,96 1/4 x 199 1/2 in. (244.5 x 506.7 cm),"Gift of Cornelius Vanderbilt, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435702,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.77,false,true,435704,European Paintings,Painting,A Limier Briquet Hound,,,,,,Artist,,Rosa Bonheur,"French, Bordeaux 1822–1899 Thomery",,"Bonheur, Rosa",French,1822,1899,ca. 1856,1856,1856,Oil on canvas,14 1/2 x 18 in. (36.8 x 45.7 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435704,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.109,false,true,435703,European Paintings,Painting,Weaning the Calves,,,,,,Artist,,Rosa Bonheur,"French, Bordeaux 1822–1899 Thomery",,"Bonheur, Rosa",French,1822,1899,1879,1879,1879,Oil on canvas,25 5/8 x 32 in. (65.1 x 81.3 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435703,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.575,false,true,438158,European Paintings,Painting,Springtime,,,,,,Artist,,Pierre-Auguste Cot,"French, Bédarieux 1837–1883 Paris",,"Cot, Pierre-Auguste",French,1837,1883,1873,1873,1873,Oil on canvas,84 x 50 in. (213.4 x 127 cm),"Gift of Steven and Alexandra Cohen, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438158,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.134,false,true,435997,European Paintings,Painting,The Storm,,,,,,Artist,,Pierre-Auguste Cot,"French, Bédarieux 1837–1883 Paris",,"Cot, Pierre-Auguste",French,1837,1883,1880,1880,1880,Oil on canvas,92 1/4 x 61 3/4 in. (234.3 x 156.8 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.12,false,true,435907,European Paintings,Painting,Sunrise,,,,,,Artist,,Claude Lorrain (Claude Gellée),"French, Chamagne 1604/5?–1682 Rome",,Claude Lorrain (Claude Gellée),French,1604,1682,possibly 1646–47,1646,1647,Oil on canvas,40 1/2 x 52 3/4 in. (102.9 x 134 cm),"Fletcher Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435907,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.117,false,true,435905,European Paintings,Painting,The Ford,,,,,,Artist,,Claude Lorrain (Claude Gellée),"French, Chamagne 1604/5?–1682 Rome",,Claude Lorrain (Claude Gellée),French,1604,1682,possibly 1636,1636,1636,Oil on canvas,29 1/4 x 39 3/4 in. (74.3 x 101 cm),"Fletcher Fund, 1928",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435905,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.119,true,true,435908,European Paintings,Painting,The Trojan Women Setting Fire to Their Fleet,,,,,,Artist,,Claude Lorrain (Claude Gellée),"French, Chamagne 1604/5?–1682 Rome",,Claude Lorrain (Claude Gellée),French,1604,1682,ca. 1643,1643,1643,Oil on canvas,41 3/8 x 59 7/8 in. (105.1 x 152.1 cm),"Fletcher Fund, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.205,false,true,435909,European Paintings,Painting,View of La Crescenza,,,,,,Artist,,Claude Lorrain (Claude Gellée),"French, Chamagne 1604/5?–1682 Rome",,Claude Lorrain (Claude Gellée),French,1604,1682,1648–50,1648,1650,Oil on canvas,15 1/4 x 22 7/8 in. (38.7 x 58.1 cm),"Purchase, The Annenberg Fund Inc. Gift, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435909,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.181.12,false,true,435906,European Paintings,Painting,Pastoral Landscape: The Roman Campagna,,,,,,Artist,,Claude Lorrain (Claude Gellée),"French, Chamagne 1604/5?–1682 Rome",,Claude Lorrain (Claude Gellée),French,1604,1682,ca. 1639,1634,1644,Oil on canvas,40 x 53 1/2 in. (101.6 x 135.9 cm),"Bequest of Adele L. Lehman, in memory of Arthur Lehman, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435906,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.21.1,true,true,435621,European Paintings,Painting,Joan of Arc,,,,,,Artist,,Jules Bastien-Lepage,"French, Damvillers 1848–1884 Paris",,"Bastien-Lepage, Jules",French,1848,1884,1879,1879,1879,Oil on canvas,100 x 110 in. (254 x 279.4 cm),"Gift of Erwin Davis, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435621,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.156,false,true,438099,European Paintings,Painting,"The Public Viewing David’s ""Coronation"" at the Louvre",,,,,,Artist,,Louis Léopold Boilly,"French, La Bassée 1761–1845 Paris",,"Boilly, Louis Léopold",French,1761,1845,1810,1810,1810,Oil on canvas,24 1/4 x 32 1/2 in. (61.6 x 82.6 cm),"Gift of Mrs. Charles Wrightsman, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438099,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.56,false,true,437328,European Paintings,Painting,Midas Washing at the Source of the Pactolus,,,,,,Artist,,Nicolas Poussin,"French, Les Andelys 1594–1665 Rome",,"Poussin, Nicolas",French,1594,1665,ca. 1627,1622,1632,Oil on canvas,38 3/8 x 28 5/8 in. (97.5 x 72.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437328,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.160,true,true,437329,European Paintings,Painting,The Abduction of the Sabine Women,,,,,,Artist,,Nicolas Poussin,"French, Les Andelys 1594–1665 Rome",,"Poussin, Nicolas",French,1594,1665,probably 1633–34,1633,1634,Oil on canvas,60 7/8 x 82 5/8 in. (154.6 x 209.9 cm),"Harris Brisbane Dick Fund, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.45.1,true,true,437326,European Paintings,Painting,Blind Orion Searching for the Rising Sun,,,,,,Artist,,Nicolas Poussin,"French, Les Andelys 1594–1665 Rome",,"Poussin, Nicolas",French,1594,1665,1658,1658,1658,Oil on canvas,46 7/8 x 72 in. (119.1 x 182.9 cm),"Fletcher Fund, 1924",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437326,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.45.2,false,true,437330,European Paintings,Painting,Saints Peter and John Healing the Lame Man,,,,,,Artist,,Nicolas Poussin,"French, Les Andelys 1594–1665 Rome",,"Poussin, Nicolas",French,1594,1665,1655,1655,1655,Oil on canvas,49 1/2 x 65 in. (125.7 x 165.1 cm),"Marquand Fund, 1924",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.1.2,false,true,437327,European Paintings,Painting,The Companions of Rinaldo,,,,,,Artist,,Nicolas Poussin,"French, Les Andelys 1594–1665 Rome",,"Poussin, Nicolas",French,1594,1665,ca. 1633,1628,1638,Oil on canvas,46 1/2 x 40 1/4 in. (118.1 x 102.2 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437327,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.117.6,false,true,438025,European Paintings,Painting,The Rest on the Flight into Egypt,,,,,,Artist,,Nicolas Poussin,"French, Les Andelys 1594–1665 Rome",,"Poussin, Nicolas",French,1594,1665,ca. 1627,1622,1632,Oil on canvas,30 x 25 in. (76.2 x 63.5 cm),"Bequest of Lore Heinemann, in memory of her husband, Dr. Rudolf J. Heinemann, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.20,false,true,437220,European Paintings,Painting,A Cavalryman,,,,,,Artist,,Alphonse-Marie-Adolphe de Neuville,"French, Saint-Omer 1835–1885 Paris",,"Neuville, Alphonse-Marie-Adolphe de",French,1835,1885,1884,1884,1884,Oil on canvas,18 1/8 x 15 in. (46 x 38.1 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437220,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.26,false,true,437219,European Paintings,Painting,The Spy,,,,,,Artist,,Alphonse-Marie-Adolphe de Neuville,"French, Saint-Omer 1835–1885 Paris",,"Neuville, Alphonse-Marie-Adolphe de",French,1835,1885,1880,1880,1880,Oil on canvas,51 1/4 x 84 in. (130.2 x 213.4 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.113,false,true,436207,European Paintings,Painting,Don Quixote and Sancho Panza Entertained by Basil and Quiteria,,,,,,Artist,,Gustave Doré,"French, Strasbourg 1832–1883 Paris",,"Doré, Gustave",French,1832,1883,1863?,1863,1863,Oil on canvas,36 1/4 x 28 3/4 in. (92.1 x 73 cm),"Gift of Mrs. William A. McFadden and Mrs. Giles Whiting, 1928",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436207,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.21,false,true,435774,European Paintings,Painting,A Peasant Girl Knitting,,,,,,Artist,,Jules Breton,"French, Courrières 1827–1906 Paris",,"Breton, Jules",French,1827,1906,ca. 1870,1865,1875,Oil on canvas,22 5/8 x 18 1/2 in. (57.5 x 47 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.66,false,true,435773,European Paintings,Painting,The Weeders,,,,,,Artist,,Jules Breton,"French, Courrières 1827–1906 Paris",,"Breton, Jules",French,1827,1906,1868,1868,1868,Oil on canvas,28 1/8 x 50 1/4 in. (71.4 x 127.6 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435773,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.2,false,true,435755,European Paintings,Painting,The Baptism of Christ,,,,,,Artist,,Sébastien Bourdon,"French, Montpellier 1616–1671 Paris",,"Bourdon, Sébastien",French,1616,1671,ca. 1650,1645,1655,Oil on canvas,Overall 59 3/4 x 46 1/2 in. (151.8 x 118.1 cm); painted surface (oval) 59 1/8 x 45 1/2 in. (150.2 x 115.6 cm),"Purchase, George T. Delacorte Jr. Gift, 1974",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.90,false,true,435756,European Paintings,Painting,A Classical Landscape,,,,,,Artist,,Sébastien Bourdon,"French, Montpellier 1616–1671 Paris",,"Bourdon, Sébastien",French,1616,1671,probably 1660s,1660,1669,Oil on canvas,27 1/2 x 36 1/4 in. (69.9 x 92.1 cm),"Gift of Atwood A. Allaire, Pamela Askew, and Phoebe A. DesMarais, in memory of their mother, Constance Askew, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435756,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +94.24.1,false,true,435831,European Paintings,Painting,The Birth of Venus,,,,,,Artist,,Alexandre Cabanel,"French, Montpellier 1823–1889 Paris",,"Cabanel, Alexandre",French,1823,1889,1875,1875,1875,Oil on canvas,41 3/4 x 71 7/8 in. (106 x 182.6 cm),"Gift of John Wolfe, 1893",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.103.1,false,true,435832,European Paintings,Painting,Florentine Poet,,,,,,Artist,,Alexandre Cabanel,"French, Montpellier 1823–1889 Paris",,"Cabanel, Alexandre",French,1823,1889,1861,1861,1861,Oil on wood,12 x 19 7/8 in. (30.5 x 50.5 cm),"The John Hobart Warren Bequest, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435832,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.258.1,false,true,435829,European Paintings,Painting,Echo,,,,,,Artist,,Alexandre Cabanel,"French, Montpellier 1823–1889 Paris",,"Cabanel, Alexandre",French,1823,1889,1874,1874,1874,Oil on canvas,38 1/2 x 26 1/4 in. (97.8 x 66.7 cm),"Gift of Mary Phelps Smith, in memory of her husband, Howard Caswell Smith, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435829,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.82,false,true,435830,European Paintings,Painting,Catharine Lorillard Wolfe (1828–1887),,,,,,Artist,,Alexandre Cabanel,"French, Montpellier 1823–1889 Paris",,"Cabanel, Alexandre",French,1823,1889,1876,1876,1876,Oil on canvas,67 1/2 x 42 3/4 in. (171.5 x 108.6 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435830,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.102,false,true,435638,European Paintings,"Painting, triptych",The Le Cellier Altarpiece,,,,,,Artist,,Jean Bellegambe,"French, Douai ca. 1470–1535/36 Douai",,"Bellegambe, Jean",French,1470,1536,1509,1509,1509,Oil on wood,Shaped top: central panel 40 x 24 in. (101.6 x 61 cm); left wing 37 3/4 x 10 in. (95.9 x 25.4 cm); right wing 37 1/2 x 9 1/2 in. (95.3 x 24.1 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435638,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.125,false,true,435637,European Paintings,"Painting, wing of a triptych","Charles Coguin, Abbot of Anchin",,,,,,Artist,,Jean Bellegambe,"French, Douai ca. 1470–1535/36 Douai",,"Bellegambe, Jean",French,1470,1536,ca. 1509–13,1509,1513,Oil on wood,"Arched top, 26 3/4 x 11 3/8 in. (67.9 x 28.9 cm)","The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435637,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.140,false,true,435751,European Paintings,Painting,Village by a River,,,,,,Artist,,Eugène Boudin,"French, Honfleur 1824–1898 Deauville",,"Boudin, Eugène",French,1824,1898,probably 1867,1867,1867,Oil on wood,14 x 23 in. (35.6 x 58.4 cm),"Gift of Arthur J. Neumark, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.88.4,false,true,435749,European Paintings,Painting,On the Beach at Trouville,,,,,,Artist,,Eugène Boudin,"French, Honfleur 1824–1898 Deauville",,"Boudin, Eugène",French,1824,1898,1863,1863,1863,Oil on wood,10 x 18 in. (25.4 x 45.7 cm),"Bequest of Amelia B. Lazarus, 1907",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435749,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.2,false,true,435750,European Paintings,Painting,Beaulieu: The Bay of Fourmis,,,,,,Artist,,Eugène Boudin,"French, Honfleur 1824–1898 Deauville",,"Boudin, Eugène",French,1824,1898,1892,1892,1892,Oil on canvas,21 5/8 x 35 1/2 in. (54.9 x 90.2 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435750,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.1,false,true,437987,European Paintings,Painting,"On the Beach, Dieppe",,,,,,Artist,,Eugène Boudin,"French, Honfleur 1824–1898 Deauville",,"Boudin, Eugène",French,1824,1898,1864,1864,1864,Oil on wood,12 1/2 x 11 1/2 in. (31.8 x 29.2 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437987,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.2,false,true,438551,European Paintings,Painting,"On the Beach, Sunset",,,,,,Artist,,Eugène Boudin,"French, Honfleur 1824–1898 Deauville",,"Boudin, Eugène",French,1824,1898,1865,1865,1865,Oil on wood,15 x 23 in. (38.1 x 58.4 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438551,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.288.1,false,true,437988,European Paintings,Painting,Princess Pauline Metternich (1836–1921) on the Beach,,,,,,Artist,,Eugène Boudin,"French, Honfleur 1824–1898 Deauville",,"Boudin, Eugène",French,1824,1898,ca. 1865–67,1844,1898,"Oil on cardboard, laid down on wood",11 5/8 x 9 1/4 in. (29.5 x 23.5 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1999, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437988,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.213,false,true,436241,European Paintings,Painting,Cows Crossing a Ford,,,,,,Artist,,Jules Dupré,"French, Nantes 1811–1889 L'Isle-Adam",,"Dupré, Jules",French,1811,1889,1836,1836,1836,Oil on canvas,14 1/4 x 24 5/8 in. (36.2 x 62.5 cm),"Gift of Mrs. Leon L. Watters, in memory of Leon Laizer Watters, 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436241,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.68,false,true,438543,European Paintings,Painting,Still Life with Shells and a Chip-Wood Box,,,,,,Artist,,Sebastian Stoskopff,"French, Strasbourg 1597–1657 Idstein",,"Stoskopff, Sebastian",French,1597,1657,late 1620s,1626,1629,Oil on canvas,18 1/2 x 23 3/8 in. (47 x 59.4 cm),"Wrightsman Fund, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438543,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.27,false,true,437256,European Paintings,Painting,Concert Champêtre,,,,,,Artist,,Jean-Baptiste Joseph Pater,"French, Valenciennes 1695–1736 Paris",,"Pater, Jean-Baptiste Joseph",French,1695,1736,ca. 1734,1729,1739,Oil on canvas,20 1/2 x 26 3/4 in. (52.1 x 67.9 cm),"Purchase, Joseph Pulitzer Bequest, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437256,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.52,false,true,437257,European Paintings,Painting,The Fair at Bezons,,,,,,Artist,,Jean-Baptiste Joseph Pater,"French, Valenciennes 1695–1736 Paris",,"Pater, Jean-Baptiste Joseph",French,1695,1736,ca. 1733,1728,1738,Oil on canvas,42 x 56 in. (106.7 x 142.2 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437257,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.55.1,false,true,437260,European Paintings,Painting,Troops on the March,,,,,,Artist,,Jean-Baptiste Joseph Pater,"French, Valenciennes 1695–1736 Paris",,"Pater, Jean-Baptiste Joseph",French,1695,1736,ca. 1725,1720,1730,Oil on canvas,21 1/4 x 25 3/4 in. (54 x 65.4 cm),"Bequest of Ethel Tod Humphrys, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.55.2,false,true,437259,European Paintings,Painting,Troops at Rest,,,,,,Artist,,Jean-Baptiste Joseph Pater,"French, Valenciennes 1695–1736 Paris",,"Pater, Jean-Baptiste Joseph",French,1695,1736,ca. 1725,1720,1730,Oil on canvas,21 1/4 x 25 3/4 in. (54 x 65.4 cm),"Bequest of Ethel Tod Humphrys, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437259,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.120,false,true,436520,European Paintings,Painting,"Marie Adélaïde de Savoie (1685–1712), Duchesse de Bourgogne",,,,,,Artist,,Pierre Gobert,"French, Fontainebleau 1662–1744 Paris",,"Gobert, Pierre",French,1662,1744,1710,1710,1710,Oil on canvas,"Oval, 28 3/4 x 23 1/4 in. (73 x 59.1 cm)","Gift of the Marquis de La Bégassière, 1963",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436520,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.55.1,false,true,436222,European Paintings,Painting,Self-Portrait with a Harp,,,,,,Artist,,Rose Adélaïde Ducreux,"French, Paris 1761–1802 Santo Domingo",,"Ducreux, Rose Adélaïde",French,1761,1802,1791,1791,1791,Oil on canvas,76 x 50 3/4 in. (193 x 128.9 cm),"Bequest of Susan Dwight Bliss, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436222,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.36,false,true,436115,European Paintings,Painting,The Good Samaritan,,,,,,Artist,,Alexandre-Gabriel Decamps,"French, Paris 1803–1860 Fontainebleau",,"Decamps, Alexandre-Gabriel",French,1803,1860,by 1853,1845,1860,Oil on canvas,36 5/8 x 29 1/8 in. (93 x 74 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.196,false,true,436114,European Paintings,Painting,The Experts,,,,,,Artist,,Alexandre-Gabriel Decamps,"French, Paris 1803–1860 Fontainebleau",,"Decamps, Alexandre-Gabriel",French,1803,1860,1837,1837,1837,Oil on canvas,18 1/4 x 25 1/4 in. (46.4 x 64.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436114,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.736,false,true,671456,European Paintings,Painting,Chrysanthemums in the Garden at Petit-Gennevilliers,,,,,,Artist,,Gustave Caillebotte,"French, Paris 1848–1894 Gennevilliers",,"Caillebotte, Gustave",French,1848,1894,1893,1893,1893,Oil on canvas,38 5/8 × 23 1/2 in. (98 × 59.8 cm),"Gift of the Honorable John C. Whitehead, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/671456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.35,false,true,435653,European Paintings,Painting,"Sunday at the Church of Saint-Philippe-du-Roule, Paris",,,,,,Artist,,Jean Béraud,"French, St. Petersburg 1849–1936 Paris",,"Béraud, Jean",French,1849,1936,1877,1877,1877,Oil on canvas,23 3/8 x 31 7/8 in. (59.4 x 81 cm),"Gift of Mr. and Mrs. William B. Jaffe, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.48.1,false,true,435654,European Paintings,Painting,A Windy Day on the Pont des Arts,,,,,,Artist,,Jean Béraud,"French, St. Petersburg 1849–1936 Paris",,"Béraud, Jean",French,1849,1936,ca. 1880–81,1880,1881,Oil on canvas,15 5/8 x 22 1/4 in. (39.7 x 56.5 cm),"Bequest of Eda K. Loeb, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435654,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.161,false,true,436235,European Paintings,Painting,Madame de Saint-Maurice,,,,,,Artist,,Joseph Siffred Duplessis,"French, Carpentras 1725–1802 Versailles",,"Duplessis, Joseph Siffred",French,1725,1802,1776,1776,1776,Oil on canvas,39 1/2 x 31 7/8 in. (100.3 x 81 cm),"Bequest of James A. Aborn, 1968",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436235,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.132,false,true,436236,European Paintings,Painting,Benjamin Franklin (1706–1790),,,,,,Artist,,Joseph Siffred Duplessis,"French, Carpentras 1725–1802 Versailles",,"Duplessis, Joseph Siffred",French,1725,1802,1778,1778,1778,Oil on canvas,"Oval, 28 1/2 x 23 in. (72.4 x 58.4 cm)","The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436236,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.151,false,true,438510,European Paintings,Painting,The Outskirts of a Village,,,,,,Artist,,Edmond-François Aman-Jean,"French, Chevry-Cossigny 1858–1936 Paris",,"Aman-Jean, Edmond-François",French,1858,1936,ca. 1880,1875,1885,Oil on panel,4 7/8 x 8 9/16 in. (12.4 x 21.7 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +09.198,false,true,436092,European Paintings,Painting,Don Quixote and the Dead Mule,,,,,,Artist,,Honoré Daumier,"French, Marseilles 1808–1879 Valmondois",,"Daumier, Honoré",French,1808,1879,after 1864,1828,1879,Oil on wood,9 3/4 x 18 1/8 in. (24.8 x 46 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1909",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.122,false,true,436091,European Paintings,Painting,The Laundress,,,,,,Artist,,Honoré Daumier,"French, Marseilles 1808–1879 Valmondois",,"Daumier, Honoré",French,1808,1879,186[3?],1863,1863,Oil on wood,19 1/4 x 13 in. (48.9 x 33 cm),"Bequest of Lillie P. Bliss, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436091,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.143.1,false,true,436093,European Paintings,Painting,The Drinkers,,,,,,Artist,,Honoré Daumier,"French, Marseilles 1808–1879 Valmondois",,"Daumier, Honoré",French,1808,1879,by 1861,1841,1861,Oil on wood,14 3/8 x 11 in. (36.5 x 27.9 cm),"Bequest of Margaret Seligman Lewisohn, in memory of her husband, Sam A. Lewisohn, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.129,true,true,436095,European Paintings,Painting,The Third-Class Carriage,,,,,,Artist,,Honoré Daumier,"French, Marseilles 1808–1879 Valmondois",,"Daumier, Honoré",French,1808,1879,ca. 1862–64,1862,1864,Oil on canvas,25 3/4 x 35 1/2 in. (65.4 x 90.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436095,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.183,false,true,437148,European Paintings,Painting,Flowers in a Blue Vase,,,,,,Artist,,Adolphe Monticelli,"French, Marseilles 1824–1886 Marseilles",,"Monticelli, Adolphe",French,1824,1886,1879–1883,1879,1883,Oil on wood,"Overall, with added strip at right, 26 1/2 x 19 1/4 in. (67.3 x 48.9 cm)","Gift of Mr. and Mrs. Werner E. Josten, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437148,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.107,false,true,437069,European Paintings,Painting,"Falling Leaves, Allegory of Autumn",,,,,,Artist,,Hugues Merle,"French, Saint-Marcellin 1823–1881 Paris",,"Merle, Hugues",French,1823,1881,1872,1872,1872,Oil on canvas,68 7/8 x 43 1/4 in. (174.9 x 109.9 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437069,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.21,false,true,437439,European Paintings,Painting,A Young Girl with Daisies,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1889,1889,1889,Oil on canvas,25 5/8 x 21 1/4 in. (65.1 x 54 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.122,true,true,438815,European Paintings,Painting,"Madame Georges Charpentier (Marguérite-Louise Lemonnier, 1848–1904) and Her Children, Georgette-Berthe (1872–1945) and Paul-Émile-Charles (1875–1895)",,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1878,1878,1878,Oil on canvas,60 1/2 x 74 7/8 in. (153.7 x 190.2 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1907",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.200,false,true,437424,European Paintings,Painting,"Madame Édouard Bernier (Marie-Octavie-Stéphanie Laurens, 1838–1920)",,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1871,1871,1871,Oil on canvas,30 3/4 x 24 1/2 in. (78.1 x 62.2 cm),"Gift of Margaret Seligman Lewisohn, in memory of her husband, Sam A. Lewisohn, and of her sister-in-law, Adele Lewisohn Lehman, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437424,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.218,false,true,437428,European Paintings,Painting,Still Life with Peaches and Grapes,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1881,1881,1881,Oil on canvas,21 1/4 x 25 5/8 in. (54 x 65.1 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437428,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.190,false,true,437433,European Paintings,Painting,"The Farm at Les Collettes, Cagnes",,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1908–14,1908,1914,Oil on canvas,21 1/2 x 25 3/4in. (54.6 x 65.4cm),"Bequest of Charlotte Gina Abrams, in memory of her husband, Lucien Abrams, 1961",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.150,false,true,437438,European Paintings,Painting,Young Girl in a Pink-and-Black Hat,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,ca. 1891,1886,1896,Oil on canvas,16 x 12 3/4 in. (40.6 x 32.4 cm),"Gift of Kathryn B. Miller, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.454,false,true,441104,European Paintings,Painting,Still Life with Flowers and Prickly Pears,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,ca. 1885,1880,1890,Oil on canvas,28 7/8 x 23 3/8 in. (73.3 x 59.4 cm),"Bequest of Catherine Vance Gaisman, 2010",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441104,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.112.4,false,true,437434,European Paintings,Painting,In the Meadow,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1888–92,1888,1892,Oil on canvas,32 x 25 3/4 in. (81.3 x 65.4 cm),"Bequest of Sam A. Lewisohn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.135.7,false,true,437426,European Paintings,Painting,View of the Seacoast near Wargemont in Normandy,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1880,1880,1880,Oil on canvas,19 7/8 x 24 1/2 in. (50.5 x 62.2 cm),"Bequest of Julia W. Emmons, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437426,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.135.8,false,true,437427,European Paintings,Painting,The Bay of Naples,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1881,1881,1881,Oil on canvas,23 1/2 x 32 in. (59.7 x 81.3 cm),"Bequest of Julia W. Emmons, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437427,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.135.9,false,true,437431,European Paintings,Painting,"Hills around the Bay of Moulin Huet, Guernsey",,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1883,1883,1883,Oil on canvas,18 1/8 x 25 3/4 in. (46 x 65.4 cm),"Bequest of Julia W. Emmons, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.62.2,false,true,438010,European Paintings,Painting,Nini in the Garden (Nini Lopez),,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1876,1876,1876,Oil on canvas,24 3/8 x 20 in. (61.9 x 50.8 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2002, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.9,false,true,438011,European Paintings,Painting,"Eugène Murer (Hyacinthe-Eugène Meunier, 1841–1906)",,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1877,1877,1877,Oil on canvas,18 1/2 x 15 1/2 in. (47 x 39.4 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.12,false,true,437429,European Paintings,Painting,Still Life with Peaches,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1881,1881,1881,Oil on canvas,21 x 25 1/2 in. (53.3 x 64.8 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437429,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.13,false,true,437432,European Paintings,Painting,"Tilla Durieux (Ottilie Godeffroy, 1880–1971)",,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1914,1914,1914,Oil on canvas,36 1/4 x 29 in. (92.1 x 73.7 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.14,false,true,437437,European Paintings,Painting,A Waitress at Duval's Restaurant,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,ca. 1875,1870,1880,Oil on canvas,39 1/2 x 28 1/8 in. (100.3 x 71.4 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.15,false,true,437425,European Paintings,Painting,Marguerite-Thérèse (Margot) Berard (1874–1956),,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1879,1879,1879,Oil on canvas,16 1/8 x 12 3/4 in. (41 x 32.4 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437425,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.10,false,true,438012,European Paintings,Painting,Bouquet of Chrysanthemums,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1881,1881,1881,Oil on canvas,26 x 21 7/8 in. (66 x 55.6 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.12,false,true,438013,European Paintings,Painting,Reclining Nude,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1883,1883,1883,Oil on canvas,25 5/8 x 32 in. (65.1 x 81.3 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.125,false,true,437430,European Paintings,Painting,By the Seashore,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1883,1883,1883,Oil on canvas,36 1/4 x 28 1/2 in. (92.1 x 72.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437430,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.356.32,false,true,437436,European Paintings,Painting,A Road in Louveciennes,,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,ca. 1870,1865,1875,Oil on canvas,15 x 18 1/4 in. (38.1 x 46.4 cm),"The Lesley and Emma Sheafer Collection, Bequest of Emma A. Sheafer, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.279,false,true,437180,European Paintings,Painting,The Rebuke of Adam and Eve,,,,,,Artist,,Charles Joseph Natoire,"French, Nîmes 1700–1777 Castel Gandolfo",,"Natoire, Charles Joseph",French,1700,1777,1740,1740,1740,Oil on copper,26 3/4 x 19 3/4 in. (67.9 x 50.2 cm),"Purchase, Mr. and Mrs. Frank E. Richardson III, George T. Delacorte Jr., and Mr. and Mrs. Henry J. Heinz II Gifts; Victor Wilbour Memorial, Marquand, and The Alfred N. Punnett Endowment Funds; and The Edward Joseph Gallagher III Memorial Collection, Edward J. Gallagher Jr. Bequest, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.22,false,true,436030,European Paintings,Painting,Soap Bubbles,,,,,,Artist,,Thomas Couture,"French, Senlis 1815–1879 Villiers-le-Bel",,"Couture, Thomas",French,1815,1879,ca. 1859,1859,1859,Oil on canvas,51 1/2 x 38 5/8 in. (130.8 x 98.1 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.122,false,true,435753,European Paintings,Painting,The Proposal,,,,,,Artist,,William Bouguereau,"French, La Rochelle 1825–1905 La Rochelle",,"Bouguereau, William",French,1825,1905,1872,1872,1872,Oil on canvas,64 3/8 x 44 in. (163.5 x 111.8 cm),"Gift of Mrs. Elliot L. Kamen, in memory of her father, Bernard R. Armour, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435753,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.402,false,true,435752,European Paintings,Painting,Young Mother Gazing at Her Child,,,,,,Artist,,William Bouguereau,"French, La Rochelle 1825–1905 La Rochelle",,"Bouguereau, William",French,1825,1905,1871,1871,1871,Oil on canvas,56 x 40 1/2 in. (142.2 x 102.9 cm),"Bequest of Zene Montgomery Pyle, 1993",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.32,false,true,435754,European Paintings,Painting,Breton Brother and Sister,,,,,,Artist,,William Bouguereau,"French, La Rochelle 1825–1905 La Rochelle",,"Bouguereau, William",French,1825,1905,1871,1871,1871,Oil on canvas,50 7/8 x 35 1/8 in. (129.2 x 89.2 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435754,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.170,false,true,438098,European Paintings,Painting,Tea,,,,,,Artist,,James Tissot,"French, Nantes 1836–1902 Chenecey-Buillon",,"Tissot, James",French,1836,1902,1872,1872,1872,Oil on wood,26 x 18 7/8 in. (66 x 47.9 cm),"Gift of Mrs. Charles Wrightsman, 1998",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438098,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.278,false,true,438887,European Paintings,Painting,In Full Sunlight (En plein soleil),,,,,,Artist,,James Tissot,"French, Nantes 1836–1902 Chenecey-Buillon",,"Tissot, James",French,1836,1902,ca. 1881,1876,1886,Oil on wood,9 3/4 x 13 7/8 in. (24.8 x 35.2 cm),"Gift of Mrs. Charles Wrightsman, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.359,false,true,440729,European Paintings,Painting,Spring Morning,,,,,,Artist,,James Tissot,"French, Nantes 1836–1902 Chenecey-Buillon",,"Tissot, James",French,1836,1902,ca. 1875,1870,1880,Oil on canvas,22 x 16 3/4 in. (55.9 x 42.5 cm),"Gift of Mrs. Charles Wrightsman, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440729,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +19.84,false,true,436015,European Paintings,Painting,Louis Gueymard (1822–1880) as Robert le Diable,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1857,1857,1857,Oil on canvas,58 1/2 x 42 in. (148.6 x 106.7 cm),"Gift of Elizabeth Milbank Anderson, 1919",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436015,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.77,false,true,436014,European Paintings,Painting,Hunting Dogs with Dead Hare,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1857,1857,1857,Oil on canvas,36 1/2 x 58 1/2 in. (92.7 x 148.6 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1933",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +40.175,false,true,438820,European Paintings,Painting,Young Ladies of the Village,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1851–52,1851,1852,Oil on canvas,76 3/4 x 102 3/4 in. (194.9 x 261 cm),"Gift of Harry Payne Bingham, 1940",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.27.1,false,true,436021,European Paintings,Painting,The Sea,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1865 or later,1865,1877,Oil on canvas,20 x 24 in. (50.8 x 61 cm),"Purchase, Dikran G. Kelekian Gift, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436021,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +99.11.3,false,true,436012,European Paintings,Painting,The Fishing Boat,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1865,1839,1877,Oil on canvas,25 1/2 x 32 in. (64.8 x 81.3 cm),"Gift of Mary Goldenberg, 1899",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436012,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1995.537,false,true,436025,European Paintings,Painting,View of Ornans,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,probably mid-1850s,1839,1877,Oil on canvas,28 3/4 x 36 1/4 in. (73 x 92.1 cm),"Bequest of Alice Tully, 1993",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436025,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.16.13,false,true,436013,European Paintings,Painting,The Hidden Brook,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,ca. 1873–77,1839,1877,Oil on canvas,23 3/8 x 29 3/4 in. (59.4 x 75.6 cm),"From the Collection of James Stillman, Gift of Dr. Ernest G. Stillman, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436013,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.16.14,false,true,436020,European Paintings,Painting,River and Rocks,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1873–77,1873,1877,Oil on canvas,19 5/8 x 23 7/8 in. (49.8 x 60.6 cm),"From the Collection of James Stillman, Gift of Dr. Ernest G. Stillman, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436020,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.16.15,false,true,436009,European Paintings,Painting,"A Brook in a Clearing (possibly ""Brook, Valley of Fontcouverte; Study"")",,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,probably 1862,1862,1862,Oil on canvas,20 3/4 x 25 1/2 in. (52.7 x 64.8 cm),"From the Collection of James Stillman, Gift of Dr. Ernest G. Stillman, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436009,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.57,true,true,436002,European Paintings,Painting,Woman with a Parrot,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1866,1866,1866,Oil on canvas,51 x 77 in. (129.5 x 195.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436002,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.58,false,true,436022,European Paintings,Painting,The Source,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1862,1862,1862,Oil on canvas,47 1/4 x 29 1/4 in. (120 x 74.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436022,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.59,false,true,436024,European Paintings,Painting,Woman in a Riding Habit (L'Amazone),,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1856,1856,1856,Oil on canvas,45 1/2 x 35 1/8 in. (115.6 x 89.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.60,false,true,436018,European Paintings,Painting,Nude with Flowering Branch,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1863,1863,1863,Oil on canvas,29 1/2 x 24 in. (74.9 x 61 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.61,false,true,436007,European Paintings,Painting,After the Hunt,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,ca. 1859,1859,1859,Oil on canvas,93 x 73 1/4 in. (236.2 x 186.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436007,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.62,false,true,436004,European Paintings,Painting,The Woman in the Waves,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1868,1868,1868,Oil on canvas,25 3/4 x 21 1/4 in. (65.4 x 54 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436004,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.63,false,true,436001,European Paintings,Painting,"Jo, La Belle Irlandaise",,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1865–66,1865,1866,Oil on canvas,22 x 26 in. (55.9 x 66 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.34,false,true,436011,European Paintings,Painting,The Deer,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,ca. 1865,1860,1870,Oil on canvas,29 3/8 x 36 3/8 in. (74.6 x 92.4 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436011,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.160.35,false,true,436006,European Paintings,Painting,Marine: The Waterspout,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1870,1870,1870,Oil on canvas,27 1/8 x 39 1/4 in. (68.9 x 99.7 cm),"H. O. Havemeyer Collection, Gift of Horace Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436006,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.118,false,true,436000,European Paintings,Painting,Madame de Brayer,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1858,1858,1858,Oil on canvas,36 x 28 5/8 in. (91.4 x 72.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.120,false,true,436017,European Paintings,Painting,Charles Suisse,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1861,1861,1861,Oil on canvas,23 1/4 x 19 3/8 in. (59.1 x 49.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436017,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.122,false,true,436023,European Paintings,Painting,The Source of the Loue,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1864,1864,1864,Oil on canvas,39 1/4 x 56 in. (99.7 x 142.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436023,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.124,false,true,436003,European Paintings,Painting,The Young Bather,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1866,1866,1866,Oil on canvas,51 1/4 x 38 1/4 in. (130.2 x 97.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436003,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.130,false,true,436016,European Paintings,Painting,"Madame Auguste Cuoq (Mathilde Desportes, 1827–1910)",,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,ca. 1852–57,1852,1857,Oil on canvas,69 1/2 x 42 1/2 in. (176.5 x 108 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.132,false,true,436008,European Paintings,Painting,Alphonse Promayet (1822–1872),,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1851,1851,1851,Oil on canvas,42 1/8 x 27 5/8 in. (107 x 70.2 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436008,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.201,false,true,436019,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,probably ca. 1862,1857,1867,Oil on canvas,16 1/4 x 13 1/8 in. (41.3 x 33.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436019,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.566,false,true,436005,European Paintings,Painting,The Calm Sea,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,1869,1869,1869,Oil on canvas,23 1/2 x 28 3/4 in. (59.7 x 73 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436005,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.68,false,true,435914,European Paintings,Painting,Guillaume Budé (1467–1540),,,,,,Artist,,Jean Clouet,"French, active by 1516–died 1540/41 Paris",,"Clouet, Jean",French,1516,1541,ca. 1536,1531,1541,Oil on wood,15 5/8 x 13 1/2 in. (39.7 x 34.3 cm),"Maria DeWitt Jesup Fund, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435914,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.459,true,true,439933,European Paintings,Painting,Lute Player,,,,,,Artist,,Valentin de Boulogne,"French, Coulommiers-en-Brie 1591–1632 Rome",,Valentin de Boulogne,French,1591,1632,ca. 1625–26,1625,1626,Oil on canvas,50 1/2 x 39 in. (128.3 x 99.1 cm),"Purchase, Walter and Leonore Annenberg Acquisitions Endowment Fund; Director's Fund; Acquisitions Fund; James and Diane Burke and Mr. and Mrs. Mark Fisch Gifts; Louis V. Bell, Harris Brisbane Dick, Fletcher, and Rogers Funds and Joseph Pulitzer Bequest, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439933,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.33.2,true,true,437837,European Paintings,Painting,The Sofa,,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,ca. 1894–96,1894,1896,Oil on cardboard,24 3/4 x 31 7/8 in. (62.9 x 81 cm),"Rogers Fund, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.13,false,true,438016,European Paintings,Painting,The Streetwalker,,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,ca. 1890–91,1890,1891,Oil on cardboard,25 1/2 x 21 in. (64.8 x 53.3 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438016,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2003.20.15,false,true,438018,European Paintings,Painting,Woman before a Mirror,,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,1897,1897,1897,Oil on cardboard,24 1/2 x 18 1/2 in. (62.2 x 47 cm),"The Walter H. and Leonore Annenberg Collection, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438018,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.108,false,true,437835,European Paintings,Painting,"The Englishman (William Tom Warrener, 1861–1934) at the Moulin Rouge",,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,1892,1892,1892,Oil on cardboard,33 3/4 x 26 in. (85.7 x 66 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.356.35,false,true,437839,European Paintings,Painting,Émilie,,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,late 1890s,1897,1899,Oil on wood,16 1/4 x 12 3/4 in. (41.3 x 32.4 cm),"The Lesley and Emma Sheafer Collection, Bequest of Emma A. Sheafer, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.356.36,false,true,437834,European Paintings,Painting,Mademoiselle Nys,,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,1899,1899,1899,Oil on unprimed wood,10 5/8 x 8 5/8 in. (27 x 21.9 cm),"The Lesley and Emma Sheafer Collection, Bequest of Emma A. Sheafer, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.15,false,true,437838,European Paintings,Painting,Woman in the Garden of Monsieur Forest,,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,1889–91,1889,1891,Oil on canvas,21 7/8 x 18 1/4 in. (55.6 x 46.4 cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.135.14,false,true,437836,European Paintings,Painting,Albert (René) Grenier (1858–1925),,,,,,Artist,,Henri de Toulouse-Lautrec,"French, Albi 1864–1901 Saint-André-du-Bois",,"Toulouse-Lautrec, Henri de",French,1864,1901,1887,1887,1887,Oil on wood,13 3/8 x 10 in. (34 x 25.4 cm),"Bequest of Mary Cushing Fosburgh, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437836,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +80.8,false,true,435709,European Paintings,Painting,John Taylor Johnston (1820–1893),,,,,,Artist,,Léon Bonnat,"French, Bayonne 1833–1922 Monchy-Saint-Eloi",,"Bonnat, Léon",French,1833,1922,1880,1880,1880,Oil on canvas,52 1/2 x 44 in. (133.4 x 111.8 cm),"Gift of the Trustees, 1880",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435709,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.97,false,true,435711,European Paintings,Painting,An Egyptian Peasant Woman and Her Child,,,,,,Artist,,Léon Bonnat,"French, Bayonne 1833–1922 Monchy-Saint-Eloi",,"Bonnat, Léon",French,1833,1922,1869–70,1869,1870,Oil on canvas,73 1/2 x 41 1/2 in. (186.7 x 105.4 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435711,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.137,false,true,435708,European Paintings,Painting,Roman Girl at a Fountain,,,,,,Artist,,Léon Bonnat,"French, Bayonne 1833–1922 Monchy-Saint-Eloi",,"Bonnat, Léon",French,1833,1922,1875,1875,1875,Oil on canvas,67 x 39 1/2 in. (170.2 x 100.3 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435708,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.103.2,false,true,436419,European Paintings,Painting,The Arab Falconer,,,,,,Artist,,Eugène Fromentin,"French, La Rochelle 1820–1876 Saint-Maurice",,"Fromentin, Eugène",French,1820,1876,1864,1864,1864,Oil on canvas,42 3/4 x 28 1/2 in. (108.6 x 72.4 cm),"The John Hobart Warren Bequest, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436419,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.64,false,true,436420,European Paintings,Painting,Arabs Crossing a Ford,,,,,,Artist,,Eugène Fromentin,"French, La Rochelle 1820–1876 Saint-Maurice",,"Fromentin, Eugène",French,1820,1876,1873,1873,1873,Oil on wood,20 x 24 1/2 in. (50.8 x 62.2 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436420,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.542,false,true,442356,European Paintings,Painting,"Still Life with Silver and Gold Plate, Shells, and a Sword",,,,,,Artist,,Meiffren Conte,"French, Marseilles ca. 1630–1705 Marseilles",,"Conte, Meiffren",French,1625,1705,fourth quarter 17th century,1675,1699,Oil on canvas,40 × 50 3/4 in. (101.6 × 128.9 cm),"Gift of Mrs. Russell B. Aitken, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/442356,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.166,false,true,439120,European Paintings,Painting,The Mass of Saint Basil,,,,,,Artist,,Pierre Hubert Subleyras,"French, Saint-Gilles-du-Gard 1699–1749 Rome",,"Subleyras, Pierre Hubert",French,1699,1749,1746,1746,1746,"Oil on canvas, transferred from canvas",54 x 31 1/8 in. (137 x 79 cm),"Wrightsman Fund, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439120,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.145,false,true,440464,European Paintings,Painting,"Pope Benedict XIV (Prospero Lambertini, 1675–1758)",,,,,,Artist,,Pierre Hubert Subleyras,"French, Saint-Gilles-du-Gard 1699–1749 Rome",,"Subleyras, Pierre Hubert",French,1699,1749,1746,1746,1746,Oil on canvas,25 1/4 x 19 1/4 in. (64.1 x 48.9 cm),"Purchase, Friends of European Paintings Gifts, Bequest of Joan Whitney Payson, by exchange, Gwynne Andrews Fund, Charles and Jessie Price Gift, and Valerie Delacorte Fund Gift, in memory of George T. Delacorte, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440464,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +86.6,false,true,436632,European Paintings,Painting,Moonrise,,,,,,Artist,,Henri-Joseph Harpignies,"French, Valenciennes 1819–1916 Saint-Privé",,"Harpignies, Henri-Joseph",French,1819,1916,1885,1885,1885,Oil on canvas,34 1/2 x 64 1/4 in. (87.6 x 163.2 cm),"Gift of Arnold and Tripp, 1886",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.30,true,true,436838,European Paintings,Painting,The Fortune-Teller,,,,,,Artist,,Georges de La Tour,"French, Vic-sur-Seille 1593–1653 Lunéville",,"La Tour, Georges de",French,1593,1653,probably 1630s,1630,1639,Oil on canvas,40 1/8 x 48 5/8 in. (101.9 x 123.5 cm),"Rogers Fund, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436838,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.517,false,true,436839,European Paintings,Painting,The Penitent Magdalen,,,,,,Artist,,Georges de La Tour,"French, Vic-sur-Seille 1593–1653 Lunéville",,"La Tour, Georges de",French,1593,1653,ca. 1640,1635,1645,Oil on canvas,52 1/2 x 40 1/4 in. (133.4 x 102.2 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436839,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.597,false,true,441755,European Paintings,Painting,Landscape Study with Clouds,,,,,,Artist,,Émile Loubon,"French, Aix-en-Provence 1809–1863 Marseilles",,"Loubon, Émile",French,1809,1863,ca. 1829–31,1829,1831,Oil on cardboard,5 7/8 x 9 1/16 in. (15 x 23 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441755,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +03.30,true,true,438814,European Paintings,Painting,The Abduction of Rebecca,,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,1846,1846,1846,Oil on canvas,39 1/2 x 32 1/4 in. (100.3 x 81.9 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1903",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438814,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.27.4,false,true,436177,European Paintings,Painting,George Sand's Garden at Nohant,,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,1840s,1842,1863,Oil on canvas,17 7/8 x 21 3/4 in. (45.4 x 55.2 cm),"Purchase, Dikran G. Kelekian Gift, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436177,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.328,false,true,436180,European Paintings,Painting,The Natchez,,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,1823–24 and 1835,1823,1835,Oil on canvas,35 1/2 x 46 in. (90.2 x 116.8 cm),"Purchase, Gifts of George N. and Helen M. Richard and Mr. and Mrs. Charles S. McVeigh and Bequest of Emma A. Sheafer, by exchange, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436180,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.430,false,true,436179,European Paintings,Painting,"Madame Henri François Riesener (Félicité Longrois, 1786–1847)",,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,1835,1835,1835,Oil on canvas,29 1/4 x 23 3/4 in. (74.3 x 60.3 cm),"Gift of Mrs. Charles Wrightsman, 1994",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436179,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.101,false,true,439631,European Paintings,Painting,Ovid among the Scythians,,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,1862,1862,1862,"Oil on paper, laid down on wood",12 5/8 x 19 3/4 in. (32.1 x 50.2 cm),"Wrightsman Fund, in honor of Philippe de Montebello, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.60,false,true,436175,European Paintings,Painting,Basket of Flowers,,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,1848–49,1848,1849,Oil on canvas,42 1/4 x 56 in. (107.3 x 142.2 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436175,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.61,false,true,436178,European Paintings,Painting,Hamlet and His Mother,,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,1849,1849,1849,Oil on canvas,10 3/4 x 7 1/8 in. (27.3 x 18.1 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436178,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.131,false,true,436176,European Paintings,Painting,Christ Asleep during the Tempest,,,,,,Artist,,Eugène Delacroix,"French, Charenton-Saint-Maurice 1798–1863 Paris",,"Delacroix, Eugène",French,1798,1863,ca. 1853,1853,1853,Oil on canvas,20 x 24 in. (50.8 x 61 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1988.221,false,true,435626,European Paintings,Painting,Porte de la Reine at Aigues-Mortes,,,,,,Artist,,Jean-Frédéric Bazille,"French, Montpellier 1841–1870 Beaune-la-Rolande",,"Bazille, Jean-Frédéric",French,1841,1870,1867,1867,1867,Oil on canvas,31 3/4 x 39 1/4 in. (80.6 x 99.7 cm),"Purchase, Gift of Raymonde Paul, in memory of her brother, C. Michael Paul, by exchange, 1988",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435626,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +34.138,true,true,437926,European Paintings,Painting,Mezzetin,,,,,,Artist,,Antoine Watteau,"French, Valenciennes 1684–1721 Nogent-sur-Marne",,"Watteau, Antoine",French,1684,1721,ca. 1718–20,1718,1720,Oil on canvas,21 3/4 x 17 in. (55.2 x 43.2 cm),"Munsey Fund, 1934",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437926,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.54,false,true,437925,European Paintings,Painting,The French Comedians,,,,,,Artist,,Antoine Watteau,"French, Valenciennes 1684–1721 Nogent-sur-Marne",,"Watteau, Antoine",French,1684,1721,ca. 1720,1715,1725,Oil on canvas,22 1/2 x 28 3/4 in. (57.2 x 73 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437925,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.736,false,true,437456,European Paintings,Painting,Breton Fishermen and Their Families,,,,,,Artist,,Théodule-Augustin Ribot,"French, Saint-Nicolas-d'Attez 1823–1891 Colombes",,"Ribot, Théodule-Augustin",French,1823,1891,possibly ca. 1880–85,1860,1891,Oil on canvas,21 3/4 x 18 1/4 in. (55.2 x 46.4 cm),"Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437456,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +80.5.2,false,true,436569,European Paintings,Painting,The Choir of the Capuchin Church in Rome,,,,,,Artist,,François Marius Granet,"French, Aix-en-Provence 1775–1849 Aix-en-Provence",,"Granet, François Marius",French,1775,1849,1814–15,1814,1815,Oil on canvas,77 1/2 x 58 1/4 in. (196.9 x 148 cm),"Gift of P. L. Everard, 1880",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436569,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.181,false,true,437973,European Paintings,Painting,"Ponte San Rocco and Waterfalls, Tivoli",,,,,,Artist,,François Marius Granet,"French, Aix-en-Provence 1775–1849 Aix-en-Provence",,"Granet, François Marius",French,1775,1849,ca. 1810–20,1810,1820,Oil on canvas,14 7/8 x 11 1/8 in. (37.8 x 28.3 cm),"Purchase, Leonora Brenauer Bequest, in memory of her father, Joseph B. Brenauer; Wolfe Fund, and Wolfe Fund, by exchange, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437973,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.3,false,true,438948,European Paintings,Painting,"View in the Stables of the Villa of Maecenas, Tivoli",,,,,,Artist,,François Marius Granet,"French, Aix-en-Provence 1775–1849 Aix-en-Provence",,"Granet, François Marius",French,1775,1849,ca. 1805–10,1800,1815,"Oil on paper, laid down on canvas",10 1/2 x 8 3/4 in. (26.7 x 22.2 cm),"Gift of Eugene V. Thaw, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438948,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.66,false,true,435885,European Paintings,Painting,View of the Domaine Saint-Joseph,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,late 1880s,1886,1889,Oil on canvas,25 5/8 x 32 in. (65.1 x 81.3 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435885,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.45,false,true,435876,European Paintings,Painting,"Madame Cézanne (Hortense Fiquet, 1850–1922) in a Red Dress",,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1888–90,1888,1890,Oil on canvas,45 7/8 x 35 1/4 in. ( 116.5 x 89.5 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1962",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.181,false,true,435871,European Paintings,Painting,Gardanne,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1885–86,1885,1886,Oil on canvas,31 1/2 x 25 1/4 in. (80 x 64.1 cm),"Gift of Dr. and Mrs. Franz H. Hirschland, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435871,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.103,false,true,435866,European Paintings,Painting,Apples,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1878–79,1878,1879,Oil on canvas,9 x 13 in. (22.9 x 33 cm),"The Mr. and Mrs. Henry Ittleson Jr. Purchase Fund, 1961",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435866,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1994.420,false,true,435878,European Paintings,Painting,Mont Sainte-Victoire,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1902–6,1902,1906,Oil on canvas,22 1/2 x 38 1/4 in. (57.2 x 97.2 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1994, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.473,false,true,438136,European Paintings,Painting,The Fishermen (Fantastic Scene),,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1875,1870,1880,Oil on canvas,21 3/4 x 32 1/4 in. (55.2 x 81.9 cm),"Gift of Heather Daniels and Katharine Whild, and Purchase, The Annenberg Foundation Gift, Gift of Joanne Toor Cummings, by exchange, Wolfe Fund, and Ellen Lichtenstein and Joanne Toor Cummings Bequests, Mr. and Mrs. Richard J. Bernhard Gift, Gift of Mr. and Mrs. Richard Rodgers, and Wolfe Fund, by exchange, and funds from various donors, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438136,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.112.1,true,true,435882,European Paintings,Painting,Still Life with Apples and a Pot of Primroses,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1890,1885,1895,Oil on canvas,28 3/4 x 36 3/8 in. (73 x 92.4 cm),"Bequest of Sam A. Lewisohn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435882,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.140.1,false,true,435870,European Paintings,Painting,"Antoine Dominique Sauveur Aubert (born 1817), the Artist's Uncle",,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1866,1866,1866,Oil on canvas,31 3/8 x 25 1/4 in. (79.7 x 64.1 cm),"Wolfe Fund, 1951; acquired from The Museum of Modern Art, Lillie P. Bliss Collection",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435870,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.1,true,true,435868,European Paintings,Painting,The Card Players,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1890–92,1890,1892,Oil on canvas,25 3/4 x 32 1/4 in. (65.4 x 81.9 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.2,false,true,435875,European Paintings,Painting,"Madame Cézanne (Hortense Fiquet, 1850–1922) in the Conservatory",,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1891,1891,1891,Oil on canvas,36 1/4 x 28 3/4 in. (92.1 x 73 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435875,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.3,false,true,435883,European Paintings,Painting,Still Life with Apples and Pears,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1891–92,1891,1892,Oil on canvas,17 5/8 x 23 1/8 in. (44.8 x 58.7 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435883,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.4,false,true,435881,European Paintings,Painting,Still Life with a Ginger Jar and Eggplants,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1893–94,1893,1894,Oil on canvas,28 1/2 x 36 in. (72.4 x 91.4 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435881,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.101.5,false,true,435879,European Paintings,Painting,The Pool at the Jas de Bouffan,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,late 1880s,1886,1889,Oil on canvas,25 1/2 x 31 7/8 in. (64.8 x 81 cm),"Bequest of Stephen C. Clark, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435879,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.60.1,false,true,437989,European Paintings,Painting,Dish of Apples,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1876–77,1876,1877,Oil on canvas,18 1/8 x 21 3/4 in. (46 x 55.2 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1997, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437989,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.60.2,false,true,437990,European Paintings,Painting,Seated Peasant,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1892–96,1892,1896,Oil on canvas,21 1/2 x 17 3/4 in. (54.6 x 45.1 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1997, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437990,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.64,false,true,435877,European Paintings,Painting,Mont Sainte-Victoire and the Viaduct of the Arc River Valley,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1882–85,1882,1885,Oil on canvas,25 3/4 x 32 1/8 in. (65.4 x 81.6 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.65,false,true,435873,European Paintings,Painting,Gustave Boyer (b. 1840) in a Straw Hat,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1870–71,1870,1871,"Oil on paper, laid down on canvas",21 5/8 x 15 1/4 in. (54.9 x 38.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435873,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.66,true,true,435884,European Paintings,Painting,"Still Life with Jar, Cup, and Apples",,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1877,1872,1882,Oil on canvas,23 7/8 x 29 in. (60.6 x 73.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435884,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.67,false,true,435872,European Paintings,Painting,The Gulf of Marseilles Seen from L'Estaque,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,ca. 1885,1880,1890,Oil on canvas,28 3/4 x 39 1/2 in. (73 x 100.3 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435872,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.400.1,false,true,435869,European Paintings,Painting,"Antoine Dominique Sauveur Aubert (born 1817), the Artist's Uncle, as a Monk",,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1866,1866,1866,Oil on canvas,25 5/8 x 21 1/2 in. (65.1 x 54.6 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1993, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.400.2,false,true,435874,European Paintings,Painting,The House with the Cracked Walls,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1892–94,1892,1894,Oil on canvas,31 1/2 x 25 1/4 in. (80 x 64.1 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1993, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435874,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.194,false,true,435880,European Paintings,Painting,Rocks in the Forest,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1890s,1890,1899,Oil on canvas,28 7/8 x 36 3/8 in. (73.3 x 92.4 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435880,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.12,false,true,435867,European Paintings,Painting,Bathers,,,,,,Artist,,Paul Cézanne,"French, Aix-en-Provence 1839–1906 Aix-en-Provence",,"Cézanne, Paul",French,1839,1906,1874–75,1874,1875,Oil on canvas,15 x 18 1/8 in. (38.1 x 46 cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435867,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.5,false,true,437311,European Paintings,Painting,"Rue de l'Épicerie, Rouen (Effect of Sunlight)",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1898,1898,1898,Oil on canvas,32 x 25 5/8 in. (81.3 x 65.1 cm),"Purchase, Mr. and Mrs. Richard J. Bernhard Gift, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437311,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.36,false,true,437314,European Paintings,Painting,The Garden of the Tuileries on a Winter Afternoon,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1899,1899,1899,Oil on canvas,29 x 36 1/4 in. (73.7 x 92.1 cm),"Gift of Katrin S. Vietor, in loving memory of Ernest G. Vietor, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.182,false,true,437300,European Paintings,Painting,"A Cowherd at Valhermeil, Auvers-sur-Oise",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1874,1874,1874,Oil on canvas,21 5/8 x 36 1/4 in. (54.9 x 92.1 cm),"Gift of Edna H. Sachs, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437300,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.133,false,true,437309,European Paintings,Painting,Steamboats in the Port of Rouen,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1896,1896,1896,Oil on canvas,18 x 21 1/2 in. (45.7 x 54.6 cm),"Gift of Arthur J. Neumark, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437309,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.174,false,true,437310,European Paintings,Painting,The Boulevard Montmartre on a Winter Morning,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1897,1897,1897,Oil on canvas,25 1/2 x 32 in. (64.8 x 81.3 cm),"Gift of Katrin S. Vietor, in loving memory of Ernest G. Vietor, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437310,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.156,false,true,437301,European Paintings,Painting,The Public Garden at Pontoise,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1874,1874,1874,Oil on canvas,23 5/8 x 28 3/4 in. (60 x 73 cm),"Gift of Mr. and Mrs. Arthur Murray, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437301,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.30.2,false,true,437299,European Paintings,Painting,"Jalais Hill, Pontoise",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1867,1867,1867,Oil on canvas,34 1/4 x 45 1/4 in. (87 x 114.9 cm),"Bequest of William Church Osborn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437299,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.414,false,true,437312,European Paintings,Painting,The Garden of the Tuileries on a Winter Afternoon,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1899,1899,1899,Oil on canvas,28 7/8 x 36 3/8 in. (73.3 x 92.4 cm),"Gift from the Collection of Marshall Field III, 1979",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.166,false,true,437317,European Paintings,Painting,Still Life with Apples and Pitcher,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1872,1872,1872,Oil on canvas,18 1/4 x 22 1/4 in. (46.4 x 56.5 cm),"Purchase, Mr. and Mrs. Richard J. Bernhard Gift, by exchange, 1983",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2004.359,false,true,438738,European Paintings,Painting,"Haystacks, Morning, Éragny",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1899,1899,1899,Oil on canvas,25 x 31 1/2 in. (63.5 x 80 cm),"Bequest of Douglas Dillon, 2003",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438738,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.184.1,false,true,437303,European Paintings,Painting,"Washerwoman, Study",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1880,1880,1880,Oil on canvas,28 3/4 x 23 1/4 in. (73 x 59.1 cm),"Gift of Mr. and Mrs. Nate B. Spingold, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437303,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.154.1,false,true,437305,European Paintings,Painting,A Washerwoman at Éragny,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1893,1893,1893,Oil on canvas,18 x 15 in. (45.7 x 38.1 cm),"Gift of Mr. and Mrs. Richard Rodgers, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.21.1,false,true,437308,European Paintings,Painting,"Morning, An Overcast Day, Rouen",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1896,1896,1896,Oil on canvas,21 3/8 x 25 5/8 in. (54.3 x 65.1 cm),"Bequest of Grégoire Tarnopol, 1979, and Gift of Alexander Tarnopol, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437308,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.93,false,true,437307,European Paintings,Painting,"Poplars, Éragny",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1895,1895,1895,Oil on canvas,36 1/2 x 25 1/2 in. (92.7 x 64.8 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437307,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.311.5,false,true,437304,European Paintings,Painting,Two Young Peasant Women,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1891–92,1891,1892,Oil on canvas,35 1/4 x 45 7/8 in. (89.5 x 116.5 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437304,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.277.2,false,true,437316,European Paintings,Painting,"Côte des Grouettes, near Pontoise",,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,probably 1878,1878,1878,Oil on canvas,29 1/8 x 23 5/8 in. (74 x 60 cm),"Gift of Janice H. Levin, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.126,false,true,437306,European Paintings,Painting,Bather in the Woods,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1895,1895,1895,Oil on canvas,23 3/4 x 28 3/4 in. (60.3 x 73 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437306,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.135.16,false,true,437302,European Paintings,Painting,Barges at Pontoise,,,,,,Artist,,Camille Pissarro,"French, Charlotte Amalie, Saint Thomas 1830–1903 Paris",,"Pissarro, Camille",French,1830,1903,1876,1876,1876,Oil on canvas,18 1/8 x 21 5/8 in. (46 x 54.9 cm),"Bequest of Mary Cushing Fosburgh, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437302,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.182,false,true,436451,European Paintings,Painting,Tahitian Landscape,,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,1892,1892,1892,Oil on canvas,25 3/8 x 18 5/8 in. (64.5 x 47.3 cm),"Anonymous Gift, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.58.1,false,true,436446,European Paintings,Painting,Two Tahitian Women,,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,1899,1899,1899,Oil on canvas,37 x 28 1/2 in. (94 x 72.4 cm),"Gift of William Church Osborn, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.112.2,true,true,438821,European Paintings,Painting,Ia Orana Maria (Hail Mary),,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,1891,1891,1891,Oil on canvas,44 3/4 x 34 1/2 in. (113.7 x 87.6 cm),"Bequest of Sam A. Lewisohn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.143.2,false,true,436448,European Paintings,Painting,A Farm in Brittany,,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,ca. 1894,1894,1894,Oil on canvas,28 1/2 x 35 5/8 in. (72.4 x 90.5 cm),"Bequest of Margaret Seligman Lewisohn, in memory of her husband, Sam A. Lewisohn, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.60.3,false,true,438000,European Paintings,Painting,Three Tahitian Women,,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,1896,1896,1896,Oil on wood,9 5/8 x 17 in. (24.4 x 43.2 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1997, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438000,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1993.400.3,false,true,436449,European Paintings,Painting,The Siesta,,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,ca. 1892–94,1892,1894,Oil on canvas,35 x 45 3/4 in. (88.9 x 116.2 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1993, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.391.2,false,true,437999,European Paintings,Painting,Still Life with Teapot and Fruit,,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,1896,1896,1896,Oil on canvas,18 3/4 x 26 in. (47.6 x 66 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1997, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.391.3,false,true,438001,European Paintings,Painting,Two Women,,,,,,Artist,,Paul Gauguin,"French, Paris 1848–1903 Atuona, Hiva Oa, Marquesas Islands",,"Gauguin, Paul",French,1848,1903,1901 or 1902,1901,1902,Oil on canvas,29 x 36 1/4 in. (73.7 x 92.1 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1997, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438001,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.285,false,true,437974,European Paintings,Painting,Scene in the Jewish Quarter of Constantine,,,,,,Artist,,Théodore Chassériau,"French, Le Limon, Saint-Domingue, West Indies 1819–1856 Paris",,"Chassériau, Théodore",French,1819,1856,1851,1851,1851,Oil on canvas,22 3/8 x 18 1/2 in. (56.8 x 47 cm),"Purchase, The Annenberg Foundation Gift, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437974,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.720,false,true,438541,European Paintings,Painting,"Scene from the Gallic Wars: The Gaul Littavicus, Betraying the Roman Cause, Flees to Gergovie to Support Vercingétorix",,,,,,Artist,,Théodore Chassériau,"French, Le Limon, Saint-Domingue, West Indies 1819–1856 Paris",,"Chassériau, Théodore",French,1819,1856,ca. 1838–40,1838,1840,Oil on canvas,13 1/2 x 17 3/4 in. (34.3 x 45.1 cm),"Gift of Lisa and William O'Reilly, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438541,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.291,false,true,438590,European Paintings,Painting,"Comtesse de La Tour-Maubourg (Marie-Louise-Charlotte-Gabrielle Thomas de Pange, 1816–1850)",,,,,,Artist,,Théodore Chassériau,"French, Le Limon, Saint-Domingue, West Indies 1819–1856 Paris",,"Chassériau, Théodore",French,1819,1856,1841,1841,1841,Oil on canvas,52 x 37 1/4 in. (132.1 x 94.6 cm),"Wrightsman Fund, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +97.18,false,true,436329,European Paintings,Painting,Gathering Olives at Tivoli,,,,,,Artist,,François-Louis Français,"French, Plombières-les-Bains 1814–1897 Plombières-les-Bains",,"Français, François-Louis",French,1814,1897,1868,1868,1868,Oil on canvas,83 3/4 x 51 5/8 in. (212.7 x 131.1 cm),"Gift of I. Montaignac, 1897",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436329,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.60,false,true,436720,European Paintings,"Painting, miniature",Napoléon I (1769–1821),,,,,,Artist,Style of,Jean-Baptiste Isabey,19th century,,"Isabey, Jean-Baptiste",French,1767,1855,,1800,1899,Ivory,"Oval, 2 3/4 x 2 in. (70 x 50 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436720,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.526,false,true,436066,European Paintings,"Painting, miniature",Portrait of an Officer,,,,,,Artist,,D. B.,"French, 1812",,"B., D.",French,1812,1812,,1812,1812,Ivory,"Oval, 2 3/4 x 2 1/4 in. (72 x 58 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436066,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.50,false,true,435592,European Paintings,"Painting, miniature",Portrait of a Woman with Tapestry Work,,,,,,Artist,Style of,Jean-Baptiste Jacques Augustin,ca. 1800–1810,,"Augustin, Jean-Baptiste Jacques",French,1759,1832,,1800,1810,Ivory extended by card,5 5/8 x 4 5/8 in. (140 x 117 mm),"The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435592,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.512,false,true,435913,European Paintings,"Painting, miniature","Henry III (1551–1589), King of France",,,,,,Artist,Style of,François Clouet,"French, 1578 or later",,"Clouet, François",French,1536,1572,,1578,1599,Vellum laid on wood,2 1/4 x 1 3/4 in. (58 x 44 mm),"Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.63,false,true,435891,European Paintings,"Painting, miniature","Leda and the Swan, after Boucher",,,,,,Artist,Style of,Jacques Charlier,probably 19th century,,"Charlier, Jacques",French,1720,1790,,1800,1899,Ivory,2 x 2 7/8 in. (50 x 72 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.80,false,true,437599,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,Attributed to,Jean-Baptiste Sambat,"French, ca. 1760–1827",,"Sambat, Jean-Baptiste",French,1760,1827,,1780,1827,Ivory,Diameter 2 5/8 in. (70 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437599,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.26,false,true,435903,European Paintings,"Painting, miniature",Portrait of an Officer,,,,,,Artist,,Charles Pierre Cior,"French, 1769–after 1838",,"Cior, Charles Pierre",French,1769,1838,,1789,1838,Ivory,Diameter 3 in. (77 mm),"Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435903,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.66,false,true,436219,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Frédéric Dubois,"French, active ca. 1780–1819",,"Dubois, Frédéric",French,1780,1819,,1793,1794,Ivory,Diameter 2 5/8 in. (66 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436219,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.291,false,true,435748,European Paintings,"Painting, drawing",Study for a Monument to a Princely Figure,,,,,,Artist,,François Boucher,"French, Paris 1703–1770 Paris",,"Boucher, François",French,1703,1770,,1723,1770,"Oil on paper, laid down on canvas",15 x 12 5/8 in. (38.1 x 32.1 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/435748,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.2,false,true,435893,European Paintings,Drawing,Fantasia,,,,,,Artist,,Jules Chéret,"French, Paris 1836–1932 Nice",,"Chéret, Jules",French,1836,1932,,1856,1933,Pastel on canvas,25 5/8 x 18 1/4 in. (65.1 x 46.4 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/435893,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.165.1,false,true,436137,European Paintings,Drawing,The Dancers,,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,,1854,1917,Pastel and charcoal on paper,28 x 23 1/4 in. (71.1 x 59.1 cm),"Gift of George N. and Helen M. Richard, 1964",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436137,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.356.31,false,true,436167,European Paintings,Drawing,"Two Dancers, Half-length",,,,,,Artist,,Edgar Degas,"French, Paris 1834–1917 Paris",,"Degas, Edgar",French,1834,1917,,1854,1917,Pastel on paper,18 3/8 x 21 5/8 in. (46.7 x 54.9 cm),"The Lesley and Emma Sheafer Collection, Bequest of Emma A. Sheafer, 1973",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436167,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.21,false,true,437093,European Paintings,Painting,Mercury and Battus,,,,,,Artist,,Francisque Millet,"French, 1642–1679",,"Millet, Francisque",French,1642,1679,,1662,1679,Oil on canvas,47 x 70 in. (119.4 x 177.8 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437093,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.11,false,true,436346,European Paintings,Painting,"An Allegory, Probably of the Peace of Utrecht of 1713",,,,,,Artist,,Antoine Rivalz,"French, 1667–1735",,"Rivalz, Antoine",French,1667,1735,,1670,1699,Oil on canvas,18 5/8 x 22 in. (47.3 x 55.9 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.205.1,false,true,437831,European Paintings,"Painting, sketch",Jean Marc Nattier (1685–1766),,,,,,Artist,,Louis Tocqué,"French, 1696–1772",,"Tocqué, Louis",French,1696,1772,,1716,1772,Oil on canvas,30 1/2 x 23 1/4 in. (77.5 x 59.1 cm),"Gift of Colonel and Mrs. Jacques Balsan, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437831,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.20,false,true,437928,European Paintings,Painting,The Country Dance,,,,,,Artist,Copy after,Antoine Watteau,"French, late 18th century",,"Watteau, Antoine",French,1684,1721,,1704,1721,Oil on wood,Diameter 8 1/2 in. (21.6 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437928,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.21,false,true,437927,European Paintings,Painting,The Cascade,,,,,,Artist,Copy after,Antoine Watteau,"French, late 18th century",,"Watteau, Antoine",French,1684,1721,,1704,1721,Oil on wood,Diameter 8 1/2 in. (21.6 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437927,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.739,false,true,437886,European Paintings,Painting,Harbor Scene with a Grotto and Fishermen Hauling in Nets,,,,,,Artist,Style of,Joseph Vernet,"French, late 18th century",,"Vernet, Joseph",French,1714,1789,,1770,1799,Oil on canvas,22 3/4 x 42 1/8 in. (57.8 x 107 cm),"Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.69,false,true,436450,European Paintings,Painting,Still Life,,,,,,Artist,Style of,Paul Gauguin,"French, late 19th century",,"Gauguin, Paul",French,1848,1903,,1870,1899,Oil on canvas,15 1/8 x 18 1/4 in. (38.4 x 46.4 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.227.1,false,true,436229,European Paintings,Painting,Imaginary Landscape,,,,,,Artist,,Gaspard Dughet,"French, Rome 1615–1675 Rome",,"Dughet, Gaspard",French,1615,1675,,1635,1675,Oil on canvas,37 7/8 x 60 1/2 in. (96.2 x 153.7 cm),"Rogers Fund, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436229,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.258,false,true,436837,European Paintings,"Painting, overdoor",Allegory of Winter,,,,,,Artist,,Jacques de La Joue the Younger,"French, Paris 1686–1761 Paris",,"La Joue, Jacques de, the Younger",French,1686,1761,,1706,1761,Oil on canvas,"Irregular, 39 1/4 x 41 5/8 in. (99.7 x 105.7 cm)","Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436837,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.8,false,true,436230,European Paintings,Painting,Card Players in a Drawing Room,,,,,,Artist,,Pierre Louis Dumesnil the Younger,"French, Paris 1698–1781 Paris",,"Dumesnil, Pierre Louis, the Younger",French,1698,1781,,1718,1781,Oil on canvas,31 1/8 x 38 3/4 in. (79.1 x 98.4 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.48,false,true,436217,European Paintings,Painting,Boy with a Black Spaniel,,,,,,Artist,,François Hubert Drouais,"French, Paris 1727–1775 Paris",,"Drouais, François Hubert",French,1727,1775,,1747,1775,Oil on canvas,"Oval, 25 3/8 x 21 in. (64.5 x 53.3 cm)","The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436217,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.242.1,false,true,436218,European Paintings,Painting,Boy with a House of Cards,,,,,,Artist,,François Hubert Drouais,"French, Paris 1727–1775 Paris",,"Drouais, François Hubert",French,1727,1775,,1747,1775,Oil on canvas,"Oval, 28 x 23 in. (71.1 x 58.4 cm)","Gift of Mrs. William M. Haupt, from the collection of Mrs. James B. Haggin, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436218,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.26,false,true,437477,European Paintings,Painting,The Fountain,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,68 1/4 x 31 3/8 in. (173.4 x 79.7 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437477,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.27,false,true,437480,European Paintings,Painting,The Swing,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,68 1/4 x 34 5/8 in. (173.4 x 87.9 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437480,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.28,false,true,437476,European Paintings,Painting,The Dance,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,68 1/4 x 33 5/8 in. (173.4 x 85.4 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437476,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.29,false,true,437473,European Paintings,Painting,The Bathing Pool,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,68 3/4 x 48 3/4 in. (174.6 x 123.8 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437473,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.30,false,true,437481,European Paintings,Painting,Wandering Minstrels,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,68 3/4 x 48 1/4 in. (174.6 x 122.6 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437481,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.31,false,true,437472,European Paintings,"Painting, overdoor",Arches in Ruins,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,23 1/8 x 61 1/4 in. (58.7 x 155.6 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437472,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.32,false,true,437475,European Paintings,"Painting, overdoor",A Colonnade in Ruins,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,23 x 61 1/8 in. (58.4 x 155.3 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437475,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.264a,false,true,437474,European Paintings,"Painting, overdoor",Bridge over a Cascade,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,32 x 54 1/8 in. (81.3 x 137.5 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437474,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.264b,false,true,437471,European Paintings,"Painting, overdoor",Aqueduct in Ruins,,,,,,Artist,,Hubert Robert,"French, Paris 1733–1808 Paris",,"Robert, Hubert",French,1733,1808,,1753,1808,Oil on canvas,32 1/8 x 54 1/8 in. (81.6 x 137.5 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437471,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +80.2,false,true,437079,European Paintings,Painting,Landscape with a Plowed Field and a Village,,,,,,Artist,,Georges Michel,"French, Paris 1763–1843 Paris",,"Michel, Georges",French,1763,1843,,1783,1843,Oil on canvas,20 1/8 x 27 5/8 in. (51.1 x 70.2 cm),"Gift of Paul Durand-Ruel, 1880",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.400.44,false,true,440336,European Paintings,Painting,View in the Roman Campagna,,,,,,Artist,,Alexandre Desgoffe,"French, Paris 1805–1882 Paris",,Desgoffe Alexandre,French,1805,1882,,1834,1837,"Oil on paper, laid down on canvas",5 5/8 x 14 3/8 in. (14.3 x 36.5 cm),"Thaw Collection, Jointly Owned by The Metropolitan Museum of Art and The Morgan Library & Museum, Gift of Eugene V. Thaw, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440336,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.10,false,true,436774,European Paintings,Painting,Springtime,,,,,,Artist,,Charles Jacque,"French, Paris 1813–1894 Paris",,"Jacque, Charles",French,1813,1894,,1833,1894,Oil on wood,16 x 11 1/2 in. (40.6 x 29.2 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436774,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.155,false,true,436849,European Paintings,Painting,The Village of La Celle-sous-Moret,,,,,,Artist,,Eugène Lavieille,"French, Paris 1820–1889 Paris",,"Lavieille, Eugène",French,1820,1889,,1840,1889,Oil on wood,13 5/8 x 23 in. (34.6 x 58.4 cm),"Gift of Arthur Wiesenberger, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.123,false,true,437367,European Paintings,Painting,"Place Saint-Germain-des-Prés, Paris",,,,,,Artist,,Jean-François Raffaëlli,"French, Paris 1850–1924 Paris",,"Raffaëlli, Jean-François",French,1850,1924,,1870,1924,Oil on canvas,27 1/2 x 31 1/2 in. (69.9 x 80 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437367,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.217,false,true,436312,European Paintings,Painting,Recess of the Court,,,,,,Artist,,Jean-Louis Forain,"French, Reims 1852–1931 Paris",,"Forain, Jean-Louis",French,1852,1931,,1872,1931,Oil on canvas,23 7/8 x 28 7/8 in. (60.6 x 73.3 cm),"Gift of Mr. and Mrs. Arthur Wiesenberger, 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436312,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.461,false,true,435639,European Paintings,"Painting, overdoor",Vase of Flowers in a Niche,,,,,,Artist,Attributed to,Michel Bruno Bellengé,"French, Rouen 1726–1793 Rouen",,"Bellengé, Michel Bruno",French,1726,1793,,1746,1793,Oil on canvas,48 3/8 x 55 in. (122.9 x 139.7 cm),"Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.119,false,true,436028,European Paintings,Painting,"Portrait of a Woman, Called Héloïse Abélard",,,,,,Artist,Style of,Gustave Courbet,"French, second half 19th century",,"Courbet, Gustave",French,1819,1877,,1850,1899,Oil on canvas,25 3/8 x 21 1/8 in. (64.5 x 53.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436028,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.123,false,true,436027,European Paintings,Painting,Apples,,,,,,Artist,Style of,Gustave Courbet,"French, second half 19th century",,"Courbet, Gustave",French,1819,1877,,1850,1899,Oil on canvas,13 x 17 3/8 in. (33 x 44.1 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436027,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.138,false,true,435857,European Paintings,Painting,Landscape with a Cave,,,,,,Artist,,Théodore Caruelle d'Aligny,"French, Chaumes 1798–1871 Lyons",,"Aligny, Théodore Caruelle d'",French,1798,1871,,1818,1871,Oil on canvas,24 1/2 x 18 in. (62.2 x 45.7 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435857,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +63.138.5,false,true,435855,European Paintings,Painting,The First Communion,,,,,,Artist,,Eugène Carrière,"French, Gournay 1849–1906 Paris",,"Carrière, Eugène",French,1849,1906,,1869,1906,Oil on canvas,25 3/4 x 21 in. (65.4 x 53.3 cm),"Gift of Chester Dale, 1963",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435855,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.19,false,true,437847,European Paintings,Painting,Road in the Woods,,,,,,Artist,,Constant Troyon,"French, Sèvres 1810–1865 Paris",,"Troyon, Constant",French,1810,1865,,1840,1860,Oil on canvas,22 7/8 x 19 in. (58.1 x 48.3 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437847,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.233.39,false,true,437319,European Paintings,Painting,Mother and Child,,,,,,Artist,,Antoine-Émile Plassan,"French, Bordeaux 1817–1903 Paris",,"Plassan, Antoine-Émile",French,1817,1903,,1837,1903,Oil on wood,10 5/8 x 8 5/8 in. (27 x 21.9 cm),"Bequest of Margarette A. Jones, 1905",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437319,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.86.7,false,true,437519,European Paintings,Painting,A River Landscape,,,,,,Artist,,Théodore Rousseau,"French, Paris 1812–1867 Barbizon",,"Rousseau, Théodore",French,1812,1867,,1832,1867,Oil on wood,16 3/8 x 24 7/8 in. (41.6 x 63.2 cm),"Bequest of Richard De Wolfe Brixey, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437519,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.26,false,true,435862,European Paintings,Painting,The Route Nationale at Samer,,,,,,Artist,,Jean-Charles Cazin,"French, Samer 1841–1901 Lavandou",,"Cazin, Jean-Charles",French,1841,1901,,1861,1901,Oil on canvas,41 1/2 x 48 1/4 in. (105.4 x 122.6 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435862,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.5,false,true,436108,European Paintings,Painting,Head of a Child,,,,,,Artist,Style of,Jacques Louis David,"French, first quarter 19th century",,"David, Jacques Louis",French,1748,1825,,1768,1825,Oil on canvas,15 3/4 x 12 5/8 in. (40 x 32.1 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436108,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.124,false,true,435912,European Paintings,Painting,"Charles IX (1550–1574), King of France",,,,,,Artist,Style of,François Clouet,"French, painted shortly after 1561",,"Clouet, François",French,1536,1572,,1561,1566,Oil on wood,12 3/8 x 9 in. (31.4 x 22.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.20,false,true,437331,European Paintings,Painting,Orpheus and Eurydice,,,,,,Artist,Style of,Nicolas Poussin,"French, third quarter 17th century",,"Poussin, Nicolas",French,1594,1665,,1650,1674,Oil on canvas,47 1/2 x 70 3/4 in. (120.7 x 179.7 cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437331,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.118,false,true,436705,European Paintings,Painting,Head of Saint John the Evangelist,,,,,,Artist,,Jean Auguste Dominique Ingres,"French, Montauban 1780–1867 Paris",,"Ingres, Jean Auguste Dominique",French,1780,1867,,1818,1856,"Oil on canvas, laid down on wood",15 1/2 x 10 5/8 in. (39.4 x 27 cm),"Catharine Lorillard Wolfe Collection, Purchase, Bequest of Catharine Lorillard Wolfe, by exchange, and Wolfe Fund, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436705,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.20.2,false,true,436645,European Paintings,Painting,Young Woman Praying,,,,,,Artist,,Jean-Jacques Henner,"French, Bernwiller 1829–1905 Paris",,"Henner, Jean-Jacques",French,1829,1905,,1849,1905,Oil on canvas,24 7/8 x 17 7/8 in. (63.2 x 45.4 cm),"Bequest of Emma T. Gary, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436645,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.2,false,true,435688,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Louis Léopold Boilly,"French, La Bassée 1761–1845 Paris",,"Boilly, Louis Léopold",French,1761,1845,,1781,1845,Oil on canvas,8 3/4 x 6 7/8 in. (22.2 x 17.5 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.3,false,true,435687,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Louis Léopold Boilly,"French, La Bassée 1761–1845 Paris",,"Boilly, Louis Léopold",French,1761,1845,,1781,1845,Oil on canvas,8 3/4 x 6 7/8 in. (22.2 x 17.5 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +90.25,false,true,435701,European Paintings,Painting,Environs of Fontainebleau: Woodland and Cattle,,,,,,Artist,,Auguste-François Bonheur,"French, Bordeaux 1824–1884 Bellevue",,"Bonheur, Auguste-François",French,1824,1884,,1844,1884,Oil on canvas,104 1/2 x 157 1/4 in. (265.4 x 399.4 cm),"Gift of James Clinch Smith and his sisters, in memory of their mother, 1890",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435701,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.39,false,true,435757,European Paintings,Painting,Portrait of a Young Boy,,,,,,Artist,Attributed to,Sébastien Bourdon,"French, Montpellier 1616–1671 Paris",,"Bourdon, Sébastien",French,1616,1671,,1636,1671,Oil on canvas,23 1/4 x 19 3/4 in. (59.1 x 50.2 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435757,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.257,false,true,438030,European Paintings,Painting,Valley of the River Loire,,,,,,Artist,,Jules Dupré,"French, Nantes 1811–1889 L'Isle-Adam",,"Dupré, Jules",French,1811,1889,,1831,1889,Oil on wood,10 3/4 x 19 1/4 in. (27.3 x 48.9 cm),"Bequest of Mr. and Mrs. Richard S. Richards, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438030,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.43,false,true,437258,European Paintings,Painting,The Golden Age,,,,,,Artist,,Jean-Baptiste Joseph Pater,"French, Valenciennes 1695–1736 Paris",,"Pater, Jean-Baptiste Joseph",French,1695,1736,,1715,1736,Oil on wood,6 3/8 x 9 in. (16.2 x 22.9 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437258,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.315,false,true,436186,European Paintings,Painting,Still Life with Silver,,,,,,Artist,,Alexandre François Desportes,"French, Champigneulle 1661–1743 Paris",,"Desportes, Alexandre François",French,1661,1743,,1681,1743,Oil on canvas,103 x 73 3/4 in. (261.6 x 187.3 cm),"Purchase, Mary Wetmore Shively Bequest, in memory of her husband, Henry L. Shively, M.D., 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436186,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.100.9,false,true,437039,European Paintings,Painting,Classical Landscape with Figures,,,,,,Artist,,Henri Mauperché,"French, Paris (?) ca. 1602–1686 Paris",,"Mauperché, Henri",French,1602,1686,,1622,1686,Oil on canvas,27 7/8 x 44 1/4 in. (70.8 x 112.4 cm),"Bequest of Harry G. Sperling, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.93,false,true,436116,European Paintings,Painting,The Night Patrol at Smyrna,,,,,,Artist,,Alexandre-Gabriel Decamps,"French, Paris 1803–1860 Fontainebleau",,"Decamps, Alexandre-Gabriel",French,1803,1860,,1823,1860,Oil on canvas,29 1/4 x 36 3/8 in. (74.3 x 92.4 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436116,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.179.12,false,true,436780,European Paintings,Painting,Captain Swaton,,,,,,Artist,Attributed to,Paulin Jénot,"French, active by 1886, died after 1930",,"Jénot, Paulin",French,1886,1930,,1886,1930,Oil on canvas,16 1/8 x 13 in. (41 x 33 cm),"Gift of Raymonde Paul, in memory of her brother, C. Michael Paul, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.21,false,true,436238,European Paintings,Painting,Benjamin Franklin (1706–1790),,,,,,Artist,Workshop of,Joseph Siffred Duplessis,"French, Carpentras 1725–1802 Versailles",,"Duplessis, Joseph Siffred",French,1725,1802,,1745,1802,Oil on canvas,"Oval, 27 5/8 x 22 1/4 in. (70.2 x 56.5 cm)","Gift of George A. Lucas, 1895",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436238,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.267,false,true,437147,European Paintings,Painting,The Court of the Princess,,,,,,Artist,,Adolphe Monticelli,"French, Marseilles 1824–1886 Marseilles",,"Monticelli, Adolphe",French,1824,1886,,1844,1886,Oil on wood,15 x 23 3/8 in. (38.1 x 59.4 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1907",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437147,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.198,false,true,437149,European Paintings,Painting,Four Figures,,,,,,Artist,,Adolphe Monticelli,"French, Marseilles 1824–1886 Marseilles",,"Monticelli, Adolphe",French,1824,1886,,1844,1886,Oil on wood,9 3/4 x 7 3/4 in. (24.8 x 19.7 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437149,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.24,false,true,437269,European Paintings,Painting,"January: Cernay, near Rambouillet",,,,,,Artist,,Léon-Germain Pelouse,"French, Pierrelaye 1838–1891 Pierrelaye",,"Pelouse, Léon-Germain",French,1838,1891,,1858,1891,Oil on canvas,35 3/8 x 46 1/4 in. (89.9 x 117.5 cm),"Gift of Mabel Schaus, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.438a,false,true,436268,European Paintings,Painting,Putti with a Medallion,,,,,,Artist,,Charles Dominique Joseph Eisen,"French, Valenciennes 1720–1778 Brussels",,"Eisen, Charles Dominique Joseph",French,1720,1778,,1740,1778,Oil on wood,"Oval, 25 3/8 x 21 1/4 in. (64.5 x 54 cm)","Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.225.438b,false,true,436269,European Paintings,Painting,Putti with a Medallion,,,,,,Artist,,Charles Dominique Joseph Eisen,"French, Valenciennes 1720–1778 Brussels",,"Eisen, Charles Dominique Joseph",French,1720,1778,,1740,1778,Oil on wood,"Oval, 25 1/2 x 21 1/4 in. (64.8 x 54 cm)","Gift of J. Pierpont Morgan, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436269,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.212,false,true,436010,European Paintings,Painting,A Brook in the Forest,,,,,,Artist,,Gustave Courbet,"French, Ornans 1819–1877 La Tour-de-Peilz",,"Courbet, Gustave",French,1819,1877,,1839,1877,Oil on canvas,19 7/8 x 24 1/8 in. (50.5 x 61.3 cm),"Gift of Ralph Weiler, 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436010,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.128.12,false,true,435916,European Paintings,Painting,"Henry II (1519–1559), King of France",,,,,,Artist,Workshop of,François Clouet,"French, Tours (?), active by 1536–died 1572 Paris",,"Clouet, François",French,1536,1572,,1536,1572,"Oil on canvas, transferred from wood",61 1/2 x 53 in. (156.2 x 134.6 cm),"Bequest of Helen Hay Whitney, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435916,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.202.4,false,true,437996,European Paintings,Painting,Roses and Lilies,,,,,,Artist,,Henri Fantin-Latour,"French, Grenoble 1836–1904 Buré",,"Fantin-Latour, Henri",French,1836,1904,1888,1888,1888,Oil on canvas,23 1/2 x 18 in. (59.7 x 45.7 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 2001, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.325.3,false,true,438014,European Paintings,Painting,"The Daughters of Catulle Mendès, Huguette (1871–1964), Claudine (1876–1937), and Helyonne (1879–1955)",,,,,,Artist,,Auguste Renoir,"French, Limoges 1841–1919 Cagnes-sur-Mer",,"Renoir, Auguste",French,1841,1919,1888,1888,1888,Oil on canvas,63 3/4 x 51 1/8 in. (161.9 x 129.9 cm),"The Walter H. and Leonore Annenberg Collection, Gift of Walter H. and Leonore Annenberg, 1998, Bequest of Walter H. Annenberg, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438014,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.68,false,true,436067,European Paintings,"Painting, miniature","Joseph II (1741–1790), Emperor of Austria",,,,,,Artist,,Adam Ludwig d'Argent,"German, 1748–1829",,"Argent, Adam Ludwig d'",German,1748,1829,ca. 1780,1775,1785,Enamel,"Oval, 1 1/4 x 1 in. (32 x 26 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.511,false,true,437653,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Carl August Senff,"German, 1770–1838",,"Senff, Carl August",German,1770,1838,1808,1808,1808,Ivory,"Oval, 2 3/8 x 2 in. (60 x 50 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437653,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.67,false,true,436887,European Paintings,"Painting, miniature",Prince Klemens Wenzel Lothar von Metternich (1773–1859),,,,,,Artist,,Friedrich Johann Gottlieb Lieder,"German, 1780–1859",,"Lieder, Friedrich Johann Gottlieb",German,1780,1859,1822,1822,1822,Card laid on recent support,"Oval, 8 3/4 x 6 3/4 in. (224 x 172 mm)","Fletcher Fund, 1941",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436887,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.85,false,true,437632,European Paintings,"Painting, miniature",Joseph and Karl August von Klein,,,,,,Artist,,Heinrich Franz Schalck,"German, 1791–1832",,"Schalk, Heinrich Franz",German,1791,1832,ca. 1810–15,1810,1815,Ivory,4 5/8 x 5 1/8 in. (117 x 131 mm),"The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.52,false,true,436633,European Paintings,"Painting, miniature","Ali Pasha (born about 1741, died 1822)",,,,,,Artist,,Jacob Ritter von Hartmann,"German, 1795–1873",,"Hartmann, Jacob Ritter von",German,1795,1873,1822,1822,1822,Ivory,"Oval, 4 1/8 x 3 1/4 in. (105 x 84 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436633,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.191.3,false,true,436639,European Paintings,"Painting, miniature",Henriette Sontag (1806–1854),,,,,,Artist,,Franz Napoleon Heigel,"German, 1813–1888",,"Heigel, Franz Napoleon",German,1813,1888,ca. 1835,1830,1840,Ivory,"Oval, 2 7/8 x 2 3/8 in. (73 x 60 mm)","Gift of Mrs. Thomas Hunt, 1941",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.57,false,true,437761,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Peter Edward Stroely,"German, 1768–after 1826",,"Stroely, Peter Edward",German,1768,1826,ca. 1800,1795,1805,Ivory,"Octagonal, 3 3/8 x 2 5/8 in. (87 x 67 mm)","Rogers Fund, 1950",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437761,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.519,false,true,437786,European Paintings,"Painting, miniature",Diana,,,,,,Artist,,Carl Friedrich Thienpondt,"German, Berlin 1730–1796 Warsaw",,"Thienpondt, Carl Friedrich",German,1730,1796,ca. 1760,1755,1765,Enamel,2 7/8 x 2 1/4 in. (74 x 59 mm),"Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437786,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.520,false,true,437785,European Paintings,"Painting, miniature",A Sea Nymph,,,,,,Artist,,Carl Friedrich Thienpondt,"German, Berlin 1730–1796 Warsaw",,"Thienpondt, Carl Friedrich",German,1730,1796,ca. 1760,1755,1765,Enamel,2 7/8 x 2 1/4 in. (74 x 59 mm),"Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437785,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.78,false,true,437076,European Paintings,"Painting, miniature",Hannah Mahady,,,,,,Artist,,Jeremiah Meyer,"German, Tübingen 1735–1789 Kew",,"Meyer, Jeremiah",German,1735,1789,ca. 1760,1755,1765,Ivory,"Oval, 7/8 x 5/8 in. (22 x 16 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437076,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.68,false,true,436421,European Paintings,"Painting, miniature","Maria Louisa (1745–1792), Empress of Austria",,,,,,Artist,,Heinrich Friedrich Füger,"German, Heilbronn 1751–1818 Vienna",,"Füger, Heinrich Friedrich",German,1751,1818,ca. 1790,1785,1795,Ivory,"Octagonal, 1 1/4 x 7/8 in. (31 x 22 mm)","Fletcher Fund, 1941",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436421,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.61,false,true,437960,European Paintings,"Painting, miniature",Richard Abell,,,,,,Artist,,Christian Friedrich Zincke,"German, Dresden 1683/85–1767 London",,"Zincke, Christian Friedrich",German,1683,1767,1724,1724,1724,Enamel,"Oval, 1 3/4 x 1 3/8 in. (45 x 35 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437960,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.99,false,true,437961,European Paintings,"Painting, miniature",Mrs. Vanderbank,,,,,,Artist,,Christian Friedrich Zincke,"German, Dresden 1683/85–1767 London",,"Zincke, Christian Friedrich",German,1683,1767,ca. 1730,1725,1735,Enamel,"Oval, 1 3/8 x 1 1/2 in. (35 x 38 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437961,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.205,false,true,436663,European Paintings,"Painting, miniature","Thomas Wriothesley (1505–1550), First Earl of Southampton",,,,,,Artist,,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,ca. 1535,1530,1540,Vellum laid on card,"Irregular, cut down, 1 1/8 x 1 in. (28 x 25 mm)","Rogers Fund, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.69.1,false,true,436661,European Paintings,"Painting, miniature",William Roper (1493/94–1578),,,,,,Artist,,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,1535–36,1535,1536,Vellum laid on card,Diameter 1 3/4 in. (45 mm),"Rogers Fund, 1950",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436661,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.69.2,false,true,436662,European Paintings,"Painting, miniature","Margaret More (1505–1544), Wife of William Roper",,,,,,Artist,,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,1535–36,1535,1536,Vellum laid on playing card,Diameter 1 3/4 in. (45 mm),"Rogers Fund, 1950",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436662,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2005.231,false,true,438780,European Paintings,"Painting, drawing",Pleasure,,,,,,Artist,,Anton Raphael Mengs,"German, Ústi nad Labem (Aussig) 1728–1779 Rome",,"Mengs, Anton Raphael",German,1728,1779,ca. 1754,1749,1759,"Pastel on paper, laid down on canvas","Oval, 24 3/8 x 19 1/4 in. (61.9 x 48.9 cm)","Victor Wilbour Memorial, The Alfred N. Punnett Endowment, and Marquand Funds, 2005",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/438780,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.31,false,true,436670,European Paintings,Painting,"Edward VI (1537–1553), When Duke of Cornwall",,,,,,Artist,Workshop of,Hans Holbein the Younger,,,"Holbein, Hans, the Younger",German,1497,1543,ca. 1545; reworked 1547 or later,1540,1547,Oil and gold on oak,Diameter 12 3/4 in. (32.4 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.646,false,true,436669,European Paintings,Painting,"Lady Rich (Elizabeth Jenks, died 1558)",,,,,,Artist,Workshop of,Hans Holbein the Younger,,,"Holbein, Hans, the Younger",German,1497,1543,ca. 1540,1535,1545,Oil and gold on oak,17 1/2 x 13 3/8 in. (44.5 x 34 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436669,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.61,false,true,436044,European Paintings,Painting,Portrait of a Man,,,,,,Artist,Circle of,Lucas Cranach the Elder,,,"Cranach, Lucas, the Elder",German,1472,1553,1537,1537,1537,Oil on alder,22 x 16 3/4 in. (55.9 x 42.5 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436044,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.37,false,true,436287,European Paintings,Painting,Heinrich(?) vom Rhein zum Mohren (1477–1536),,,,,,Artist,Copy after,Conrad Faber von Creuznach,,,"Faber von Creuznach, Conrad",German,1524,1553,late 1520s,1527,1529,Oil and gold on oak,Overall 21 3/4 x 15 5/8 in. (55.2 x 39.7 cm); painted surface 21 1/2 x 15 in. (54.6 x 38.1 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436287,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.267.1,false,true,435818,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Barthel Bruyn the Elder,"German, 1493–1555",,"Bruyn, Barthel, the Elder",German,1493,1555,1533,1533,1533,Oil on oak,"Overall, with arched top, 12 x 8 7/8 in. (30.5 x 22.5 cm); painted surface 11 3/4 x 8 1/8 in. (29.8 x 20.6 cm)","Gift of James A. Moffett 2nd, 1962",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.267.2,false,true,435819,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Barthel Bruyn the Elder,"German, 1493–1555",,"Bruyn, Barthel, the Elder",German,1493,1555,1533,1533,1533,Oil on oak,"Overall, with arched top, 12 x 8 7/8 in. (30.5 x 22.5 cm); painted surface 11 3/4 x 8 1/8 in. (29.8 x 20.6 cm)","Gift of James A. Moffett 2nd, 1962",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.554.2,false,true,436590,European Paintings,Painting,"The Empress Elizabeth of Russia (1709–1762) on Horseback, Attended by a Page",,,,,,Artist,Attributed to,Georg Christoph Grooth,"German, 1716–1749",,"Grooth, Georg Christoph",German,1716,1749,after 1743–49,1743,1749,Oil on canvas,31 3/8 x 24 1/2 in. (79.7 x 62.2 cm),"Gift of Mr. and Mrs. Nathaniel Spear Jr., 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436590,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.116,false,true,438849,European Paintings,Painting,Landscape,,,,,,Artist,Circle of,Carl Rottmann,"German, 1797–1850",,"Rottmann, Carl",German,1797,1850,ca. 1835–45,1830,1850,"Oil on paper, laid down on board",8 7/8 x 10 5/8 in. (22.5 x 27 cm),"Purchase, Gift of Joanne Toor Cummings, by exchange, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438849,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.50,false,true,435820,European Paintings,Painting,Portrait of a Woman of the Slosgin Family of Cologne,,,,,,Artist,,Barthel Bruyn the Younger,"German, ca. 1530–before 1610",,"Bruyn, Barthel, the Younger",German,1530,1610,1557,1557,1557,Oil on oak,"Shaped top, 17 3/4 x 14 1/8 in. (45.1 x 35.9 cm)","The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435820,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.564,false,true,438617,European Paintings,Painting,"The Children of Martin Anton Heckscher: Johann Gustav Wilhelm Moritz (1797–1865), Carl Martin Adolph (1796–1850), and Leopold (born 1792)",,,,,,Artist,,Johann Heinrich Wilhelm Tischbein,"German, Haina 1751–1829 Eutin",,"Tischbein, Johann Heinrich Wilhelm",German,1751,1829,1805,1805,1805,Oil on canvas,58 x 45 in. (147.3 x 114.3 cm),"Gift of the family of August Heckscher II, in his memory, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438617,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.148.2,false,true,436886,European Paintings,Painting,The Ropewalk in Edam,,,,,,Artist,,Max Liebermann,"German, Berlin 1847–1935 Berlin",,"Liebermann, Max",German,1847,1935,1904,1904,1904,Oil on canvas,39 3/4 x 28 in. (101 x 71.1 cm),"Reisinger Fund, 1916",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436886,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.70,false,true,439065,European Paintings,Painting,The Family of Mr. Westfal in the Conservatory,,,,,,Artist,,Eduard Gaertner,"German, Berlin 1801–1877 Zechlin",,"Gaertner, Eduard",German,1801,1877,1836,1836,1836,Oil on canvas,9 3/8 x 7 7/8 in. (23.8 x 20 cm),"Purchase, funds from various donors, by exchange, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439065,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.258,false,true,438848,European Paintings,Painting,Parochialstrasse in Berlin,,,,,,Artist,,Eduard Gaertner,"German, Berlin 1801–1877 Zechlin",,"Gaertner, Eduard",German,1801,1877,1831,1831,1831,Oil on canvas,16 x 11 in. (40.6 x 27.9 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, and funds from various donors, by exchange, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.64,false,true,440726,European Paintings,Painting,The Artist's Sitting Room in Ritterstrasse,,,,,,Artist,,Adolph Menzel,"German, Breslau 1815–1905 Berlin",,"Menzel, Adolph",German,1815,1905,1851,1851,1851,Oil on cardboard,12 5/8 x 10 5/8 in. (32.1 x 27 cm),"Purchase, Nineteenth-Century, Modern and Contemporary Funds, Leonora Brenauer Bequest, in memory of her father, Joseph B. Brenauer, Catharine Lorillard Wolfe Collection, Wolfe Fund, and Paul L. and Marlene A. Herring and John D. Herring Gift, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.19,false,true,436036,European Paintings,Painting,"Johann (1498–1537), Duke of Saxony",,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,ca. 1534–37,1534,1537,Oil on beech,25 5/8 x 17 3/8 in. (65.1 x 44.1 cm),"Rogers Fund, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436036,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.15,false,true,436038,European Paintings,Painting,Judith with the Head of Holofernes,,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,ca. 1530,1525,1535,Oil on linden,35 1/4 x 24 3/8 in. (89.5 x 61.9 cm),"Rogers Fund, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436038,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.22,false,true,436039,European Paintings,Painting,The Martyrdom of Saint Barbara,,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,ca. 1510,1505,1515,Oil on linden,Overall 60 3/8 x 54 1/4 in. (153.4 x 137.8 cm); painted surface 59 3/8 x 53 1/8 in. (150.8 x 134.9 cm),"Rogers Fund, 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436039,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +28.221,true,true,436037,European Paintings,Painting,The Judgment of Paris,,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,ca. 1528,1523,1533,Oil on beech,40 1/8 x 28in. (101.9 x 71.1cm),"Rogers Fund, 1928",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436037,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.128,false,true,436046,European Paintings,Painting,"Johann I (1468–1532), the Constant, Elector of Saxony",,,,,,Artist,Workshop of,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,1532–33,1532,1533,"Oil on canvas, transferred from wood, with letterpress-printed paper labels",8 1/4 x 5 7/8 in. (21 x 14.9 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436046,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.469,false,true,439081,European Paintings,Painting,Saint Maurice,,,,,,Artist,,Lucas Cranach the Elder and Workshop,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder, and Workshop",German,1472,1553,ca. 1520–25,1520,1525,Oil on linden,54 x 15 1/2 in. (137.2 x 39.4 cm),"Bequest of Eva F. Kollsman, 2005",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439081,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.179.1,false,true,436043,European Paintings,Painting,"Friedrich III (1463–1525), the Wise, Elector of Saxony",,,,,,Artist,,Lucas Cranach the Elder and Workshop,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder, and Workshop",German,1472,1553,1533,1533,1533,"Oil on beech, with letterpress-printed paper labels",8 x 5 5/8 in. (20.3 x 14.3 cm),"Gift of Robert Lehman, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436043,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.179.2,false,true,436045,European Paintings,Painting,"Johann I (1468–1532), the Constant, Elector of Saxony",,,,,,Artist,,Lucas Cranach the Elder and Workshop,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder, and Workshop",German,1472,1553,1532–33,1532,1533,"Oil on beech, with letterpress-printed paper labels",8 x 5 5/8 in. (20.3 x 14.3 cm),"Gift of Robert Lehman, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436045,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.220.2,false,true,436047,European Paintings,Painting,Martin Luther (1483–1546),,,,,,Artist,Workshop of,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,probably 1532,1532,1532,Oil on wood,13 1/8 x 9 1/8 in. (33.3 x 23.2 cm),"Gift of Robert Lehman, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1981.57.1,false,true,436033,European Paintings,Painting,Lukas Spielhausen,,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,1532,1532,1532,Oil and gold on beech,20 x 14 3/8 in. (50.8 x 36.5 cm),"Bequest of Gula V. Hirschland, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436033,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +29.100.24,false,true,436040,European Paintings,Painting,Portrait of a Man with a Rosary,,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,ca. 1508,1503,1513,Oil on oak,18 3/4 x 13 7/8 in. (47.6 x 35.2cm),"H. O. Havemeyer Collection, Bequest of Mrs. H. O. Havemeyer, 1929",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436040,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.48,false,true,436042,European Paintings,Painting,Venus and Cupid,,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,ca. 1525–27,1525,1527,Oil on wood,Diameter 4 3/4 in. (12.1 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436042,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.11,false,true,436041,European Paintings,Painting,Samson and Delilah,,,,,,Artist,,Lucas Cranach the Elder,"German, Kronach 1472–1553 Weimar",,"Cranach, Lucas, the Elder",German,1472,1553,ca. 1528–30,1528,1530,Oil on beech,22 1/2 x 14 7/8 in. (57.2 x 37.8cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436041,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.8,false,true,436823,European Paintings,Painting,"Charles Beauclerk (1670–1726), Duke of St. Albans",,,,,,Artist,,Sir Godfrey Kneller,"German, Lübeck 1646–1723 London",,"Kneller, Godfrey, Sir",German,1646,1723,ca. 1690–95,1690,1695,Oil on canvas,49 7/8 x 40 1/2 in. (126.7 x 102.9 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436823,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.30.6,false,true,436824,European Paintings,Painting,"Lady Mary Berkeley, Wife of Thomas Chambers",,,,,,Artist,,Sir Godfrey Kneller,"German, Lübeck 1646–1723 London",,"Kneller, Godfrey, Sir",German,1646,1723,ca. 1700,1695,1705,Oil on canvas,29 x 25 in. (73.7 x 63.5 cm),"Gift of George A. Hearn, 1896",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436824,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +85.9,false,true,436199,European Paintings,Painting,Christ Healing the Sick,,,,,,Artist,,Christian Wilhelm Ernst Dietrich,"German, Weimar 1712–1774 Dresden",,"Dietrich, Christian Wilhelm Ernst",German,1712,1774,1742,1742,1742,Oil on canvas,35 1/8 x 41 3/8 in. (89.2 x 105.1 cm),"Gift of William H. Webb, 1885",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436199,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.162,false,true,436200,European Paintings,Painting,The Adoration of the Shepherds,,,,,,Artist,,Christian Wilhelm Ernst Dietrich,"German, Weimar 1712–1774 Dresden",,"Dietrich, Christian Wilhelm Ernst",German,1712,1774,1760s,1760,1769,Oil on canvas,21 5/8 x 28 3/4 in. (54.9 x 73 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436200,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.192,false,true,439122,European Paintings,Painting,Gothic Windows in the Ruins of the Monastery at Oybin,,,,,,Artist,,Carl Gustav Carus,"German, Leipzig 1789–1869 Dresden",,"Carus, Carl Gustav",German,1789,1869,ca. 1828,1823,1833,Oil on canvas,17 x 13 1/4 in. (43.2 x 33.7 cm) Frame: 21 3/4 x 17 7/8 x 2 1/4 in. (55.2 x 45.4 x 5.7 cm),"Purchase, 2005 Benefit Fund, and Anna-Maria and Stephen Kellen Foundation and Eugene V. Thaw Gifts, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439122,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.1,false,true,438947,European Paintings,Painting,An Overgrown Mineshaft,,,,,,Artist,,Carl Gustav Carus,"German, Leipzig 1789–1869 Dresden",,"Carus, Carl Gustav",German,1789,1869,ca. 1824,1819,1829,"Oil on paper, laid down on cardboard",11 1/4 x 8 1/4 in. (28.6 x 21 cm),"Gift of Eugene V. Thaw, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438947,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.16,false,true,436609,European Paintings,Painting,In the Studio,,,,,,Artist,,Hugo von Habermann,"German, Dillingen 1849–1929 Munich",,"Habermann, Hugo von",German,1849,1929,1885,1885,1885,Oil on canvas,39 5/8 x 37 3/4 in. (100.6 x 95.9 cm),"Reisinger Fund, 1916",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.190,false,true,437648,European Paintings,Painting,Sir James Dashwood (1715–1779),,,,,,Artist,,Enoch Seeman the Younger,"German, Danzig ca. 1690–1744 London",,"Seeman, Enoch, the Younger",German,1685,1744,1737,1737,1737,Oil on canvas,96 x 60 1/4 in. (243.8 x 153 cm),"Victor Wilbour Memorial Fund, 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437648,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.6,false,true,439346,European Paintings,Painting,At the Edge of the Forest,,,,,,Artist,,August Heinrich,"German, Dresden 1794–1822 Innsbruck",,"Heinrich, August",German,1794,1822,ca. 1820,1815,1825,Oil on canvas,10 3/4 x 12 3/4 in. (27.3 x 32.4 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund; and Wolfe Fund and Gift of Frederick Loeser, by exchange, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439346,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.279,false,true,440888,European Paintings,Painting,Window,,,,,,Artist,,Anton Dieffenbach,"German, Wiesbaden 1831–1904 Hohwald",,"Dieffenbach, Anton",German,1831,1904,1856,1856,1856,"Oil on paper, laid down on canvas",14 3/8 x 9 7/8 in. (36.5 x 25.1 cm),"Purchase, Gifts of Mr. and Mrs. Charles Zadok and William Schaus and Bequest of Mary Jane Dastich, by exchange, 2010",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440888,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.203,false,true,437860,European Paintings,Painting,Going Home,,,,,,Artist,,Fritz von Uhde,"German, Wolkenburg 1848–1911 Munich",,"von Uhde, Fritz",German,1848,1911,ca. 1889,1884,1894,Oil on wood,30 7/8 x 39 1/4 in. (78.4 x 99.7 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437860,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1038,false,true,436657,European Paintings,Painting,"Benedikt von Hertenstein (born about 1495, died 1522)",,,,,,Artist,,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,1517,1517,1517,"Oil and gold on paper, laid down on wood",Overall 20 1/2 x 15 in. (52.4 x 38.1 cm); painted surface 20 3/8 x 14 5/8 in. (51.4 x 37.1 cm),"Rogers Fund, aided by subscribers, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436657,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.28,false,true,436668,European Paintings,Painting,Portrait of a Man (Sir Ralph Sadler?),,,,,,Artist,Workshop of,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,1535,1535,1535,Oil and gold on oak,Diameter 12 in. (30.5 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436668,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.29,false,true,436659,European Paintings,Painting,Derick Berck of Cologne,,,,,,Artist,,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,1536,1536,1536,"Oil on canvas, transferred from wood",21 x 16 3/4 in. (53.3 x 42.5 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.30,false,true,436667,European Paintings,Painting,Portrait of a Young Woman,,,,,,Artist,Workshop of,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,ca. 1540–45,1540,1545,Oil and gold on oak,11 1/8 x 9 1/8 in. (28.3 x 23.2 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436667,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.135.4,true,true,436658,European Paintings,Painting,Hermann von Wedigh III (died 1560),,,,,,Artist,,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,1532,1532,1532,Oil and gold on oak,"16 5/8 x 12 3/4 in. (42.2 x 32.4 cm), with added strip of 1/2 in. (1.3 cm) at bottom","Bequest of Edward S. Harkness, 1940",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436658,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.637,false,true,436665,European Paintings,Painting,"Lady Lee (Margaret Wyatt, born about 1509)",,,,,,Artist,Workshop of,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,early 1540s,1540,1543,Oil and gold on oak,17 3/8 × 13 3/8 in. (44.1 × 34 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.24,false,true,436660,European Paintings,Painting,Portrait of a Man in a Red Cap,,,,,,Artist,,Hans Holbein the Younger,"German, Augsburg 1497/98–1543 London",,"Holbein, Hans, the Younger",German,1497,1543,1532–35,1532,1535,"Oil and gold on parchment, laid down on linden","Overall, with engaged frame, diameter 5 in. (12.7 cm); painted surface diameter 3 3/4 in. (9.5 cm)","Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2000.51,true,true,438417,European Paintings,Painting,Two Men Contemplating the Moon,,,,,,Artist,,Caspar David Friedrich,"German, Greifswald 1774–1840 Dresden",,"Friedrich, Caspar David",German,1774,1840,ca. 1825–30,1825,1830,Oil on canvas,13 3/4 x 17 1/4 in. (34.9 x 43.8 cm),"Wrightsman Fund, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438417,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.23,false,true,437975,European Paintings,Painting,Sunset after a Storm on the Coast of Sicily,,,,,,Artist,,Andreas Achenbach,"German, Kassel 1815–1910 Düsseldorf",,"Achenbach, Andreas",German,1815,1910,1853,1853,1853,Oil on canvas,32 3/4 x 42 1/4 in. (83.2 x 107.3 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437975,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.34,false,true,437759,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Bernhard Strigel,"German, Memmingen 1460–1528 Memmingen",,"Strigel, Bernhard",German,1460,1528,ca. 1510–15,1510,1515,Oil on linden,15 1/8 x 10 1/2 in. (38.4 x 26.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437759,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.5,false,true,436245,European Paintings,Painting,Virgin and Child,,,,,,Artist,,Albrecht Dürer,"German, Nuremberg 1471–1528 Nuremberg",,"Dürer, Albrecht",German,1471,1528,1516,1516,1516,Oil on spruce,11 x 7 3/8 in. (27.9 x 18.7 cm); set in panel 11 x 8 1/4 in. (27.9 x 22.2 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.633,true,true,436244,European Paintings,Painting,Virgin and Child with Saint Anne,,,,,,Artist,,Albrecht Dürer,"German, Nuremberg 1471–1528 Nuremberg",,"Dürer, Albrecht",German,1471,1528,probably 1519,1519,1519,Oil on linden,23 5/8 x 19 5/8 in. (60 x 49.8 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.64,false,true,436243,European Paintings,Painting,Salvator Mundi,,,,,,Artist,,Albrecht Dürer,"German, Nuremberg 1471–1528 Nuremberg",,"Dürer, Albrecht",German,1471,1528,ca. 1505,1500,1510,Oil on linden,22 7/8 x 18 1/2in. (58.1 x 47cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.194,false,true,435635,European Paintings,Painting,Chancellor Leonhard von Eck (1480–1550),,,,,,Artist,,Barthel Beham,"German, Nuremberg ca. 1502–1540 Italy",,"Beham, Barthel",German,1502,1540,1527,1527,1527,Oil on spruce,22 1/8 x 14 7/8 in. (56.2 x 37.8 cm),"John Stewart Kennedy Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.15,false,true,437848,European Paintings,Painting,Landscape,,,,,,Artist,,Wilhelm Trübner,"German, Heidelberg 1851–1917 Karlsruhe",,"Trübner, Wilhelm",German,1851,1917,1910,1910,1910,Oil on canvas,29 7/8 x 24 1/4 in. (75.9 x 61.5 cm),"Reisinger Fund, 1916",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437848,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.115,false,true,435585,European Paintings,Painting,Portrait of a Man and His Wife (Lorenz Kraffter and Honesta Merz?),,,,,,Artist,,Ulrich Apt the Elder,"German, Augsburg ca. 1460–1532 Augsburg",,"Apt, Ulrich, the Elder",German,1460,1532,1512,1512,1512,Oil on linden,13 x 24 7/8 in. (33 x 63.2 cm),"Rogers Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435585,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.20,false,true,435776,European Paintings,Painting,Joseph Interpreting the Dreams of Pharaoh,,,,,,Artist,Attributed to,Jörg Breu the Younger,"German, Augsburg ca. 1510–1547 Augsburg",,"Breu, Jörg, the Younger",German,1510,1547,ca. 1534–47,1534,1547,Distemper on linen,67 5/8 x 57 1/4 in. (171.8 x 145.4 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435776,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.164.8,false,true,438953,European Paintings,Painting,The Cemetery at Pronoia near Nauplia,,,,,,Artist,,Carl Rottmann,"German, Handschuhsheim 1797–1850 Munich",,"Rottmann, Carl",German,1797,1850,ca. 1841–47,1836,1847,Oil on canvas,10 x 12 in. (25.4 x 30.5 cm),"Gift of Eugene V. Thaw, 2007",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438953,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.4,false,true,436877,European Paintings,Painting,Prince Regent Luitpold of Bavaria (1821–1912),,,,,,Artist,,Franz von Lenbach,"German, Schrobenhausen 1836–1904 Munich",,"Lenbach, Franz von",German,1836,1904,1902,1902,1902,Oil on board,30 x 24 1/4 in. (76.2 x 61.5 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436877,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.46,false,true,436876,European Paintings,Painting,"Marion Lenbach (1892–1947), the Artist's Daughter",,,,,,Artist,,Franz von Lenbach,"German, Schrobenhausen 1836–1904 Munich",,"Lenbach, Franz von",German,1836,1904,1900,1900,1900,Oil on canvas,58 7/8 x 41 1/2 in. (149.5 x 105.4 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436876,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.161,false,true,436997,European Paintings,"Painting, part of the wing of an altarpiece",The Crucifixion,,,,,,Artist,,Master of the Berswordt Altar,"German, Westphalian, active ca. 1400–35",,Master of the Berswordt Altar,German,1400,1435,ca. 1400,1395,1405,"Oil, egg(?), and gold on plywood, transferred from wood",23 1/2 x 17 in. (59.7 x 43.2 cm),"Rogers Fund, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436997,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.216.2,false,true,438466,European Paintings,Painting,The Flagellation,,,,,,Artist,,Master of the Berswordt Altar,"German, Westphalian, active ca. 1400–35",,Master of the Berswordt Altar,German,1400,1435,ca. 1400,1395,1405,"Oil, egg(?), and gold on plywood, transferred from wood",22 3/4 x 16 7/8 in. (57.8 x 42.9 cm),"Bequest of Hertha Katz, 2000",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438466,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.35,false,true,436034,European Paintings,Painting,Christ and the Adulteress,,,,,,Artist,,Lucas Cranach the Younger and Workshop,"German, Wittenberg 1515–1586 Wittenberg",,"Cranach, Lucas, the Younger, and Workshop",German,1515,1586,ca. 1545–50,1545,1550,Oil on beech,6 1/4 x 8 1/2 in. (15.9 x 21.6 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436034,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.36,false,true,436035,European Paintings,Painting,Christ Blessing the Children,,,,,,Artist,,Lucas Cranach the Younger and Workshop,"German, Wittenberg 1515–1586 Wittenberg",,"Cranach, Lucas, the Younger, and Workshop",German,1515,1586,ca. 1545–50,1545,1550,Oil on beech,6 1/2 x 8 3/4 in. (16.5 x 22.2 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436035,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.566,false,true,439118,European Paintings,Painting,The Reverend Philip Cocks (1735–1797),,,,,,Artist,,Johan Joseph Zoffany,"German, near Frankfurt 1733–1810 London",,"Zoffany, Johan Joseph",German,1733,1810,late 1760s,1767,1769,Oil on canvas,35 1/2 x 27 1/4 in. (90.2 x 69.2 cm),"Gift of Mrs. Henry A. Grunwald, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439118,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.84,false,true,436835,European Paintings,Painting,The Ascension of Christ,,,,,,Artist,,Hans Süss von Kulmbach,"German, Kulmbach ca. 1480–1522 Nuremberg",,"Kulmbach, Hans Süss von",German,1480,1522,1513,1513,1513,Oil on fir,Overall 24 1/4 x 15 in. (61.5 x 38.1 cm); painted surface 24 1/4 x 14 1/8 in. (61.5 x 35.9 cm),"Rogers Fund, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436835,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.21,false,true,436834,European Paintings,Painting,Portrait of a Young Man; (reverse) Girl Making a Garland,,,,,,Artist,,Hans Süss von Kulmbach,"German, Kulmbach ca. 1480–1522 Nuremberg",,"Kulmbach, Hans Süss von",German,1480,1522,ca. 1508,1503,1513,Oil on poplar,7 x 5 1/2 in. (17.8 x 14 cm),"Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436834,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.7,false,true,439333,European Paintings,Painting,Wanderer in the Storm,,,,,,Artist,,Julius von Leypold,"German, Dresden 1806–1874 Niederlößnitz",,"Leypold, Julius von",German,1806,1874,1835,1835,1835,Oil on canvas,16 3/4 x 22 1/4 in. (42.5 x 56.5 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/439333,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +01.21,false,true,437944,European Paintings,Painting,Florinda,,,,,,Artist,,Franz Xaver Winterhalter,"German, Menzenschwand 1805–1873 Frankfurt",,"Winterhalter, Franz Xaver",German,1805,1873,1853,1853,1853,Oil on canvas,70 1/4 x 96 3/4 in. (178.4 x 245.7 cm),"Bequest of William H. Webb, 1899",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437944,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1978.403,false,true,437942,European Paintings,Painting,"The Empress Eugénie (Eugénie de Montijo, 1826–1920, Condesa de Teba)",,,,,,Artist,,Franz Xaver Winterhalter,"German, Menzenschwand 1805–1873 Frankfurt",,"Winterhalter, Franz Xaver",German,1805,1873,1854,1854,1854,Oil on canvas,36 1/2 x 29 in. (92.7 x 73.7 cm),"Purchase, Mr. and Mrs. Claus von Bülow Gift, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.119,false,true,437943,European Paintings,Painting,"Countess Alexander Nikolaevitch Lamsdorff (Maria Ivanovna Beck, 1835–1866)",,,,,,Artist,,Franz Xaver Winterhalter,"German, Menzenschwand 1805–1873 Frankfurt",,"Winterhalter, Franz Xaver",German,1805,1873,1859,1859,1859,Oil on canvas,57 1/4 x 45 1/4 in. (145.4 x 114.9 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.28,false,true,437237,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Jürgen Ovens,"German, Tönning 1623–1678 Friedrichstadt",,"Ovens, Jürgen",German,1623,1678,1650,1650,1650,Oil on canvas,49 3/8 x 37 3/4 in. (125.4 x 95.9 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437237,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.2,false,true,437295,European Paintings,Painting,Thusnelda at the Triumphal Entry of Germanicus into Rome,,,,,,Artist,,Karl Theodor von Piloty,"German, Munich 1826–1886 Ambach bei Munich",,"Piloty, Karl Theodor von",German,1826,1886,ca. 1875,1870,1880,Oil on canvas,53 x 77 1/4 in. (134.6 x 196.2 cm),"Gift of Horace Russell, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437295,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.121,false,true,438390,European Paintings,Painting,Nymph and Shepherd,,,,,,Artist,,Johann Liss,"German, Oldenburg ca. 1595/1600–1631 Verona",,"Liss, Johann",German,1595,1631,ca. 1625,1620,1630,Oil on canvas,41 1/8 x 37 3/8 in. (104.5 x 94.9 cm),"Purchase, Lila Acheson Wallace Gift, Victor Wilbour Memorial Fund, The Alfred N. Punnett Endowment Fund, and Marquand and Curtis Funds, and Bequests of Theodore M. Davis and Helen R. Bleibtreu, by exchange, 1999",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438390,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +21.152.1,false,true,436305,European Paintings,Painting,Still Life,,,,,,Artist,,Georg Flegel,"German, Olomouc (Olmütz) 1566–1638 Frankfurt",,"Flegel, Georg",German,1566,1638,probably ca. 1625–30,1625,1630,Oil on wood,10 5/8 x 13 3/8 in. (27 x 34 cm),"Gift of Dr. W. Bopp, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436305,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.141,false,true,437067,European Paintings,Painting,Johann Joachim Winckelmann (1717–1768),,,,,,Artist,,Anton Raphael Mengs,"German, Ústi nad Labem (Aussig) 1728–1779 Rome",,"Mengs, Anton Raphael",German,1728,1779,ca. 1777,1772,1782,Oil on canvas,25 x 19 3/8 in. (63.5 x 49.2 cm),"Harris Brisbane Dick Fund, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437067,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.445,false,true,441115,European Paintings,Painting,Self-Portrait,,,,,,Artist,,Anton Raphael Mengs,"German, Ústi nad Labem (Aussig) 1728–1779 Rome",,"Mengs, Anton Raphael",German,1728,1779,1776,1776,1776,Oil on canvas,35 1/2 x 25 7/8 in. (90 x 65.5 cm),"Harris Brisbane Dick Fund, 2010",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441115,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +09.48,false,true,437787,European Paintings,Painting,At Lake Garda,,,,,,Artist,,Hans Thoma,"German, Bernau im Schwarzwald 1839–1924 Karlsruhe",,"Thoma, Hans",German,1839,1924,1907,1907,1907,Oil on millboard,33 x 26 3/4 in. (83.8 x 67.9 cm),"Gift of Hugo Reisinger, 1909",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437787,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +14.40.630,false,true,436942,European Paintings,Painting,Ulrich Fugger the Younger (1490–1525),,,,,,Artist,,Hans Maler,"German, Ulm, born ca. 1480, died ca. 1526–29 Schwaz (?)",,"Maler, Hans",German,1475,1529,1525,1525,1525,Oil on linden,15 7/8 x 12 3/4 in. (40.3 x 32.4 cm),"Bequest of Benjamin Altman, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.33,false,true,436941,European Paintings,Painting,Sebastian Andorfer (1469–1537),,,,,,Artist,,Hans Maler,"German, Ulm, born ca. 1480, died ca. 1526–29 Schwaz (?)",,"Maler, Hans",German,1475,1529,1517,1517,1517,Oil on Swiss stone pine,17 x 14 1/8 in. (43.2 x 35.9 cm),"The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1997.157,false,true,437983,European Paintings,Painting,A Roman Landscape with Figures,,,,,,Artist,,Goffredo Wals,"German, Cologne, born ca. 1590–95, died 1638–40 Calabria",,"Wals, Goffredo",German,1590,1640,probably 1630s,1630,1639,Oil on copper,Diameter 16 in. (40.6 cm),"Wrightsman Fund, 1997",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437983,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1983.451,true,true,435600,European Paintings,Painting,Saint John on Patmos,,,,,,Artist,,Hans Baldung (called Hans Baldung Grien),"German, Schwäbisch Gmünd (?) 1484/85–1545 Strasbourg (Strassburg)",,"Baldung, Hans (called Hans Baldung Grien)",German,1484,1545,ca. 1511,1506,1516,"Oil, gold, and white metal on spruce",Overall 35 1/4 x 30 1/4 in. (89.5 x 76.8 cm); painted surface 34 3/8 x 29 3/4 in. (87.3 x 75.6 cm),"Purchase, Rogers and Fletcher Funds; The Vincent Astor Foundation, The Dillon Fund, The Charles Engelhard Foundation, Lawrence A. Fleischman, Mrs. Henry J. Heinz II, The Willard T. C. Johnson Foundation Inc., Reliance Group Holdings Inc., Baron H. H. Thyssen-Bornemisza, and Mr. and Mrs. Charles Wrightsman Gifts; Joseph Pulitzer Bequest; special funds; and other gifts and bequests, by exchange, 1983",,,,,,,,,,,,Paintings,"The following credit line may be used for photographs, reproductions, etc.: Contributions from various donors supplemented by Museum purchase funds, 1983",http://www.metmuseum.org/art/collection/search/435600,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.53.3,false,true,436640,European Paintings,"Painting, miniature",Lola Montez (1818–1861),,,,,,Artist,Attributed to,Josef Heigel,"German, 1780–1837",,"Heigel, Josef",German,1780,1837,,1800,1837,Ivory,"Oval, 2 1/2 x 2 1/8 in. (65 x 54 mm)","Gift of Helen O. Brice, 1942",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436640,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.109,false,true,436666,European Paintings,"Painting, miniature","Portrait of a Man, Said to Be Arnold Franz",,,,,,Artist,Imitator of,Hans Holbein the Younger,17th or early 18th century,,"Holbein, Hans, the Younger",German,1497,1543,,1600,1729,Vellum laid on card,Diameter 2 1/8 in. (53 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436666,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.154,false,true,437752,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,Attributed to,Theodor Friedrich Stein,"German, active ca. 1750–88",,"Stein, Theodor Friedrich",German,1750,1788,,1750,1788,Ivory,1 5/8 x 2 1/4 in. (41 x 62 mm),"Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437752,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.66,false,true,437224,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Johann Esaias Nilson,"German, Augsburg 1721–1788 Augsburg",,"Nilson, Johann Esaias",German,1721,1788,,1741,1788,Ivory,"Oval, 1 7/8 x 1 1/2 in. (49 x 37 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.187.495,false,true,437962,European Paintings,"Painting, miniature",Portrait of a Young Man,,,,,,Artist,,Christian Friedrich Zincke,"German, Dresden 1683/85–1767 London",,"Zincke, Christian Friedrich",German,1683,1767,,1703,1767,Enamel,"Oval, 1 3/4 x 1 3/8 in. (45 x 36 mm)","Bequest of Catherine D. Wentworth, 1948",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437962,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.114,false,true,437963,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,Attributed to,Christian Friedrich Zincke,"German, Dresden 1683/85–1767 London",,"Zincke, Christian Friedrich",German,1683,1767,,1703,1767,Enamel,"Oval, 1 7/8 x 1 1/2 in. (46 x 38 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437963,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.115,false,true,437959,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Christian Friedrich Zincke,"German, Dresden 1683/85–1767 London",,"Zincke, Christian Friedrich",German,1683,1767,,1703,1767,Enamel,"Oval, 1 7/8 x 1 1/2 in. (47 x 38 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437959,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.160,false,true,437068,European Paintings,"Painting, miniature",The Vision of Saint Anthony of Padua,,,,,,Artist,,Anton Raphael Mengs,"German, Ústi nad Labem (Aussig) 1728–1779 Rome",,"Mengs, Anton Raphael",German,1728,1779,,1758,1758,Ivory,5 1/8 x 3 3/4 in. (130 x 96 mm),"Gift of Harry G. Friedman, 1951",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437068,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2006.151.1,false,true,438844,European Paintings,Painting,"Portrait of a Woman, Said to Be Caritas Pirckheimer (1467–1532)",,,,,,Artist,Imitator of,Albrecht Dürer,20th century,,"Dürer, Albrecht",German,1471,1528,,1900,1999,Oil on linen,18 1/2 x 14 1/2 in. (47 x 36.8 cm),"Gift of Julie and Lawrence Salander, 2006",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438844,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.96,false,true,435764,European Paintings,Painting,Virgin and Child,,,,,,Artist,Workshop or Circle of,Hans Traut,"German, ca. 1500",,"Traut, Hans",German,1477,1516,,1495,1505,"Oil, gold, and silver on linden",15 5/8 x 12 1/8 in. (39.7 x 30.8 cm),"Purchase, Joseph Pulitzer Bequest, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.245.1,false,true,437494,European Paintings,Painting,"Friedrich I (1460–1536), Margrave of Brandenburg-Ansbach",,,,,,Artist,Attributed to,Franz Wolfgang Rohrich,"German, 1787–1834",,"Rohrich, Franz Wolfgang",German,1787,1834,,1807,1834,Oil on canvas,30 1/4 x 22 3/8 in. (76.8 x 56.8 cm),"Gift of Laura Wolcott Lowndes, in memory of her father, Lucius Tuckerman, 1907",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437494,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +07.245.2,false,true,437495,European Paintings,Painting,Sophia (1464–1512) of Poland,,,,,,Artist,,Franz Wolfgang Rohrich,"German, 1787–1834",,"Rohrich, Franz Wolfgang",German,1787,1834,,1807,1834,Oil on canvas,30 1/4 x 22 1/4 in. (76.8 x 56.5 cm),"Gift of Laura Wolcott Lowndes, in memory of her father, Lucius Tuckerman, 1907",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437495,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.174,false,true,436591,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Johann Nikolaus Grooth,"German, 1723?–1797",,"Grooth, Johann Nikolaus",German,1723,1797,,1743,1797,Oil on canvas,32 x 25 5/8 in. (81.3 x 65.1 cm),"Gift of Édouard Jonas, 1922",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436591,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.4,false,true,436664,European Paintings,Painting,"Lady Guildford (Mary Wotton, born 1500)",,,,,,Artist,Copy after,Hans Holbein the Younger,"British, 16th century",,"Holbein, Hans, the Younger",German,1497,1543,,1527,1527,Oil and gold on oak,32 1/8 x 26 1/8 in. (81.6 x 66.4 cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.22,false,true,436781,European Paintings,Painting,The Adoration of the Christ Child,,,,,,Artist,Follower of,Jan Joest of Kalkar,"Netherlandish, active ca. 1515",,"Joest of Kalkar, Jan",German,1510,1520,,1510,1520,Oil on wood,Overall 41 x 28 1/4 in. (104.1 x 71.8 cm); painted surface 41 x 27 5/8 in. (104.1 x 70.2 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436781,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.110,false,true,436815,European Paintings,Painting,Crusaders before Jerusalem,,,,,,Artist,,Wilhelm von Kaulbach,"German, Arolsen 1804–1874 Munich",,"Kaulbach, Wilhelm von",German,1804,1874,,1825,1874,Oil on canvas,61 5/8 x 74 1/2 in. (156.5 x 189.2 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436815,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.142,false,true,436201,European Paintings,Painting,"Surprised, or Infidelity Found Out",,,,,,Artist,,Christian Wilhelm Ernst Dietrich,"German, Weimar 1712–1774 Dresden",,"Dietrich, Christian Wilhelm Ernst",German,1712,1774,,1732,1774,Oil on canvas,28 3/4 x 28 5/8 in. (73 x 72.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436201,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +94.24.2,false,true,437642,European Paintings,Painting,Battle Scene: Arabs Making a Detour,,,,,,Artist,,Adolf Schreyer,"German, Frankfurt 1828–1899 Kronberg",,"Schreyer, Adolf",German,1828,1899,,1848,1899,Oil on canvas,59 3/8 x 99 1/2 in. (150.8 x 252.7 cm),"Gift of John Wolfe, 1893",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437642,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.127,false,true,437641,European Paintings,Painting,Arabs on the March,,,,,,Artist,,Adolf Schreyer,"German, Frankfurt 1828–1899 Kronberg",,"Schreyer, Adolf",German,1828,1899,,1848,1899,Oil on canvas,22 5/8 x 37 3/4 in. (57.5 x 95.9 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437641,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.38,false,true,435804,European Paintings,Painting,Katharina Merian,,,,,,Artist,Attributed to,Hans Brosamer,"German, active by 1536, probably died 1552",,"Brosamer, Hans",German,1536,1552,,1536,1552,"Oil, gold, and white metal on linden",Overall 18 1/4 x 13 1/8 in. (46.4 x 33.3 cm); painted surface 17 5/8 x 13 1/8 in. (44.8 x 33.3 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.34ab,false,true,437639,European Paintings,Painting,Christ before Pilate; The Resurrection,,,,,,Artist,,Ludwig Schongauer,"German, Colmar ca. 1440/55–1493/94 Colmar",,"Schongauer, Ludwig",German,1435,1494,,1479,1494,Oil on fir,"(a) overall 15 1/8 x 8 1/4 in. (38.4 x 21 cm), painted surface 14 3/8 x 7 3/4 in. (36.5 x 19.7 cm); (b) overall 15 1/8 x 8 1/4 in. (38.4 x 21 cm), painted surface 14 1/2 x 7 3/4 in. (36.8 x 19.7 cm)","The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437639,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +12.75,false,true,436286,European Paintings,Painting,Portrait of a Man with a Moor's Head on His Signet Ring,,,,,,Artist,,Conrad Faber von Creuznach,"German, Kreuznach, active by 1524–died 1552/53 Frankfurt",,"Faber von Creuznach, Conrad",German,1524,1553,,1524,1553,"Oil, gold, and white metal on linden",20 7/8 x 14 1/8 in. (53 x 35.9 cm),"John Stewart Kennedy Fund, 1912",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436286,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.33,false,true,435999,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Samuel Cotes,"British, 1734–1818",,"Cotes, Samuel",British,1734,1818,1767,1767,1767,Ivory,"Oval, 1 1/2 x 1 1/8 in. (38 x 30 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435999,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.136.14,false,true,436058,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Richard Crosse,"British, 1742–1810",,"Crosse, Richard",British,1742,1810,possibly ca. 1780,1775,1785,Ivory,"Oval, 2 1/4 x 1 7/8 in. (58 x 48 mm)","Bequest of Margaret Crane Hurlbut, 1933",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436058,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.59,false,true,437320,European Paintings,"Painting, miniature",Elizabeth Bushby,,,,,,Artist,,Andrew Plimer,"British, 1763–1837",,"Plimer, Andrew",British,1763,1837,1804,1804,1804,Ivory,"Oval, 3 x 2 1/4 in. (75 x 57 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437320,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.95,false,true,437950,European Paintings,"Painting, miniature",An Interesting Story (Miss Ray),,,,,,Artist,,William Wood,"British, 1769–1810",,"Wood, William",British,1769,1810,1806,1806,1806,Ivory,4 3/4 x 3 7/8 in. (119 x 97 mm),"The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437950,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.122,false,true,437225,European Paintings,"Painting, miniature",Portrait of a Young Woman,,,,,,Artist,,James Nixon,"British, ca. 1741–1812",,"Nixon, James",British,1736,1812,ca. 1780–85,1780,1785,Ivory,"Oval, 3 3/4 x 3 in. (97 x 75 mm)","Fletcher Fund, 1939",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.42,false,true,437226,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,James Nixon,"British, ca. 1741–1812",,"Nixon, James",British,1736,1812,ca. 1790,1785,1795,Ivory,"Oval, 2 1/4 x 1 7/8 in. (56 x 47 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.60,false,true,437048,European Paintings,"Painting, miniature","Portrait of a Woman, Said to Be Lady Sophia Boyle",,,,,,Artist,,Anne Foldsone Mee,"British, ca. 1770–1851",,"Mee, Anne Foldsone",British,1765,1851,ca. 1790,1785,1795,Ivory,"Oval, 2 5/8 x 2 1/8 in. (67 x 53 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437048,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.190.1150,false,true,437047,European Paintings,"Painting, miniature, snuffbox","Portrait of a Woman, Possibly Barbara (1768–1829), Marchioness of Donegall",,,,,,Artist,,Anne Foldsone Mee,"British, ca. 1770–1851",,"Mee, Anne Foldsone",British,1765,1851,ca. 1790,1785,1795,Ivory,"Oval, 2 7/8 x 2 1/8 in. (73 x 53 mm)","Gift of J. Pierpont Morgan, 1917",,,,,,,,,,,,Miniatures|Metalwork-Gold and Platinum,,http://www.metmuseum.org/art/collection/search/437047,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.187.44,false,true,435634,European Paintings,"Painting, miniature",Miss Chambers,,,,,,Artist,,Isabella Beetham,"British, 1750–after 1809",,"Beetham, Isabella",British,1750,1809,after 1782,1782,1809,Ivory,"Oval, 2 1/4 x 1 7/8 in. (59 x 48 mm)","The Glenn Tilley Morse Collection, Bequest of Glenn Tilley Morse, 1950",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435634,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.95,false,true,436635,European Paintings,"Painting, miniature",Agnes Sewell,,,,,,Artist,,Thomas Hazlehurst,"British, ca. 1740–ca. 1821",,"Hazlehurst, Thomas",British,1740,1821,possibly ca. 1800,1795,1805,Ivory,"Oval, 3 x 2 3/8 in. (75 x 60 mm)","Gift of Elise Shackelford Black, 1945",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436635,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.23,false,true,437140,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,Attributed to,Monogrammist FS (Franciszek Smiadecki?),"British, active ca. 1650–65",,Monogrammist FS (Franciszek Smiadecki?),British,1650,1665,ca. 1650,1645,1655,Oil on card with gessoed back,"Oval, 2 5/8 x 2 1/4 in. (67 x 56 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437140,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.510,false,true,437176,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,David Myers,"British, active ca. 1659–76",,"Myers, David",British,1659,1676,1664,1664,1664,Vellum laid on prepared gessoed card,"Oval, 2 1/2 x 2 in. (62 x 51 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437176,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.43.287,false,true,437644,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,Attributed to,Noah Seaman,"British, active ca. 1724–41",,"Seaman, Noah",British,1724,1741,ca. 1730,1725,1735,Enamel,"Oval, 1 7/8 x 1 1/2 in. (47 x 37 mm)","Bequest of Mary Anna Palmer Draper, 1914",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437644,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.35,false,true,436779,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,James Jennings,"British, active ca. 1763–93",,"Jennings, James",British,1763,1793,probably early 1770s,1770,1773,Ivory,"Oval, 1 5/8 x 1 1/4 in. (40 x 34 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436779,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.509,false,true,436500,European Paintings,"Painting, miniature","Portrait of a Man, Said to Be John Cecil (1628–1678), Fourth Earl of Exeter",,,,,,Artist,Attributed to,Richard Gibson,"British, 1605/15?–1690 London",,"Gibson, Richard",British,1605,1690,ca. 1670,1665,1675,Vellum laid on card,"Oval, 2 7/8 x 2 3/8 in. (72 x 59 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.36.1,false,true,436316,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Thomas Forster,"British, active ca. 1690–1713",,"Forster, Thomas",British,1690,1713,1701,1701,1701,Plumbago on vellum,"Oval, 4 1/4 x 3 1/4 in. (107 x 82 mm)","Rogers Fund, 1944",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436316,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.36.2,false,true,436314,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Thomas Forster,"British, active ca. 1690–1713",,"Forster, Thomas",British,1690,1713,1700,1700,1700,Plumbago on vellum,"Oval, 4 3/8 x 3 5/8 in. (112 x 92 mm)","Rogers Fund, 1944",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436314,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.36.4,false,true,436317,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Thomas Forster,"British, active ca. 1690–1713",,"Forster, Thomas",British,1690,1713,1705,1705,1705,Plumbago on vellum,"Oval, 5 1/8 x 4 in. (130 x 102 mm)","Rogers Fund, 1944",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436317,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.36.5,false,true,436315,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Thomas Forster,"British, active ca. 1690–1713",,"Forster, Thomas",British,1690,1713,1700,1700,1700,Plumbago on vellum,"Oval, 4 3/8 x 3 1/2 in. (112 x 90 mm)","Rogers Fund, 1944",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436315,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.111.1,false,true,435609,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,John Barry,"British, active ca. 1784–1827",,"Barry, John",British,1784,1827,ca. 1790,1785,1795,Ivory,"Oval, 2 1/2 x 2 1/8 in. (64 x 54 mm)","Gift of Mrs. Sherwood Eddy, 1930",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435609,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.111.2,false,true,435610,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,John Barry,"British, active ca. 1784–1827",,"Barry, John",British,1784,1827,ca. 1790,1785,1795,Ivory,"Oval, 2 1/2 x 2 1/8 in. (64 x 54 mm)","Gift of Mrs. Sherwood Eddy, 1930",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435610,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.40,false,true,435611,European Paintings,"Painting, miniature","Portrait of a Man, Said to Be John Durham",,,,,,Artist,,John Barry,"British, active ca. 1784–1827",,"Barry, John",British,1784,1827,ca. 1790,1785,1795,Ivory,"Oval, 2 5/8 x 2 1/4 in. (67 x 56 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435611,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.75.8,false,true,437264,European Paintings,"Painting, miniature",Sir Joshua Reynolds (1723–1792),,,,,,Artist,,Thomas Peat,"British, active ca. 1791–1831",,"Peat, Thomas",British,1791,1831,1792,1792,1792,Enamel,"Oval, 4 x 3 1/8 in. (101 x 80 mm)","The Collection of Giovanni P. Morosini, presented by his daughter Giulia, 1932",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437264,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.36.3,false,true,436901,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,David Loggan,"British, Gdansk 1634–1692 London",,"Loggan, David",British,1634,1692,1680,1680,1680,Plumbago on vellum,"Oval, 5 1/4 x 4 1/4 in. (132 x 107 mm)","Rogers Fund, 1944",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436901,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.36.6,false,true,437938,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Robert White,"British, London 1645–1703 London",,"White, Robert",British,1645,1703,1690,1690,1690,Plumbago on vellum,"Oval, 4 1/4 x 3 3/8 in. (106 x 85 mm)","Rogers Fund, 1944",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437938,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.71,false,true,437663,European Paintings,"Painting, miniature",The Hours,,,,,,Artist,,Samuel Shelley,"British, London 1756–1808 London",,"Shelley, Samuel",British,1756,1808,1801,1801,1801,Ivory,5 1/2 x 4 1/4 in. (140 x 109 mm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437663,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.39,false,true,437664,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Samuel Shelley,"British, London 1756–1808 London",,"Shelley, Samuel",British,1756,1808,probably ca. 1800,1795,1805,Ivory,"Oval, 3 x 2 3/8 in. (75 x 60 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437664,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.131,false,true,437582,European Paintings,"Painting, drawing",Robert Shurlock (1772–1847),,,,,,Artist,,John Russell,"British, Guildford 1745–1806 Hull",,"Russell, John",British,1745,1806,1801,1801,1801,"Pastel on paper, laid down on canvas",23 3/4 x 17 3/8 in. (60.3 x 44.1 cm),"Gift of Alan R. Shurlock, 1967",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437582,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.132,false,true,437580,European Paintings,"Painting, drawing","Mrs. Robert Shurlock (Henrietta Ann Jane Russell, 1775–1849) and Her Daughter Ann",,,,,,Artist,,John Russell,"British, Guildford 1745–1806 Hull",,"Russell, John",British,1745,1806,1801,1801,1801,"Pastel on paper, laid down on canvas",23 7/8 x 17 3/4 in. (60.6 x 45.1 cm),"Gift of Geoffrey Shurlock, 1967",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437580,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.182.1,false,true,437579,European Paintings,"Painting, drawing",William Man Godschall (1720–1802),,,,,,Artist,,John Russell,"British, Guildford 1745–1806 Hull",,"Russell, John",British,1745,1806,1791,1791,1791,"Pastel on paper, laid down on canvas",23 3/4 x 17 3/4 in. (60.3 x 45.1 cm),"Gift of Mr. and Mrs. Arthur Wiesenberger, 1961",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437579,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.182.2,false,true,437583,European Paintings,"Painting, drawing","Mrs. William Man Godschall (Sarah Godschall, 1730–1795)",,,,,,Artist,,John Russell,"British, Guildford 1745–1806 Hull",,"Russell, John",British,1745,1806,1791,1791,1791,"Pastel on paper, laid down on canvas",23 3/4 x 17 3/4 in. (60.3 x 45.1 cm),"Gift of Mr. and Mrs. Arthur Wiesenberger, 1961",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437583,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.217.2,false,true,437581,European Paintings,"Painting, drawing",Mrs. Robert Shurlock Sr. (Ann Manwaring),,,,,,Artist,,John Russell,"British, Guildford 1745–1806 Hull",,"Russell, John",British,1745,1806,1801,1801,1801,"Pastel on paper, laid down on canvas",24 x 17 7/8 in. (61 x 45.4 cm),"Gift of Olive Shurlock Sjölander, 1975",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/437581,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.218,false,true,436277,European Paintings,"Painting, miniature","Portrait of a Man, Said to Be Mr. De Wolf",,,,,,Artist,,George Engleheart,"British, Kew 1750–1829 Blackheath",,"Engleheart, George",British,1750,1829,ca. 1805,1800,1810,Ivory,"Oval, 3 1/4 x 2 1/2 in. (81 x 62 mm)","Gift of Alfred Ram, 1911",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.55,false,true,436273,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,George Engleheart,"British, Kew 1750–1829 Blackheath",,"Engleheart, George",British,1750,1829,ca. 1780,1775,1785,Ivory,"Oval, 1 1/4 x 1 1/8 in. (32 x 29 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.14,false,true,436275,European Paintings,"Painting, miniature",Colonel Woodford,,,,,,Artist,,George Engleheart,"British, Kew 1750–1829 Blackheath",,"Engleheart, George",British,1750,1829,probably 1788,1788,1788,Ivory,"Oval, 2 1/8 x 1 3/4 in. (55 x 44 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436275,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +38.146.16,false,true,436274,European Paintings,"Painting, miniature","Mrs. Peter De Lancey (Elizabeth Colden, 1720–1784)",,,,,,Artist,,George Engleheart,"British, Kew 1750–1829 Blackheath",,"Engleheart, George",British,1750,1829,1783,1778,1788,Ivory,"Oval, 1 3/8 x 1 1/8 in. (34 x 28 mm)","Fletcher Fund, 1938",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436274,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.122.1,false,true,437690,European Paintings,"Painting, miniature",Mrs. Charlotte Lennox,,,,,,Artist,,John Smart,"British, Norfolk 1741–1811 London",,"Smart, John",British,1741,1811,1777,1777,1777,Pencil and watercolor on card,"Oval, 2 1/2 x 2 in. (62 x 51 mm)","Rogers Fund, 1949",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.23.75,false,true,437694,European Paintings,"Painting, miniature",Miss Ramus,,,,,,Artist,,John Smart,"British, Norfolk 1741–1811 London",,"Smart, John",British,1741,1811,ca. 1770,1765,1775,Pencil and watercolor on paper,2 1/4 x 2 in. (57 x 51 mm),"Bequest of Alexandrine Sinsheimer, 1958",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.23.76,false,true,437691,European Paintings,"Painting, miniature",Mrs. Comyns,,,,,,Artist,,John Smart,"British, Norfolk 1741–1811 London",,"Smart, John",British,1741,1811,ca. 1760,1755,1765,Pencil and some watercolor on paper,1 3/4 x 1 5/8 in. (46 x 42 mm),"Bequest of Alexandrine Sinsheimer, 1958",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.23.77,false,true,437693,European Paintings,"Painting, miniature","Sir George Armytage (1761–1836), Fourth Baronet",,,,,,Artist,,John Smart,"British, Norfolk 1741–1811 London",,"Smart, John",British,1741,1811,possibly ca. 1763,1758,1768,Pencil and watercolor on paper,2 1/8 x 2 in. (54 x 49 mm),"Bequest of Alexandrine Sinsheimer, 1958",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.23.79,false,true,437692,European Paintings,"Painting, miniature",Mrs. Caroline Deas,,,,,,Artist,,John Smart,"British, Norfolk 1741–1811 London",,"Smart, John",British,1741,1811,ca. 1760,1755,1765,Pencil and watercolor on paper,2 1/4 x 2 1/4 in. (56 x 55 mm),"Bequest of Alexandrine Sinsheimer, 1958",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.493,false,true,437689,European Paintings,"Painting, miniature",Sir William Hood,,,,,,Artist,,John Smart,"British, Norfolk 1741–1811 London",,"Smart, John",British,1741,1811,ca. 1766,1761,1771,Ivory,"Oval, 1 1/2 x 1 1/4 in. (37 x 32 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437689,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.47,false,true,435700,European Paintings,"Painting, miniature","Algernon Percy (1602–1668), Tenth Earl of Northumberland, after Van Dyck",,,,,,Artist,,Henry Bone,"British, Truro 1755–1834 Somerstown",,"Bone, Henry",British,1755,1834,1827,1827,1827,Enamel,7 x 5 3/8 in. (178 x 136 mm),"The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435700,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.82,false,true,435696,European Paintings,"Painting, miniature","Thomas Howard (1585–1646), Second Earl of Arundel, after Rubens",,,,,,Artist,,Henry Bone,"British, Truro 1755–1834 Somerstown",,"Bone, Henry",British,1755,1834,1808,1808,1808,Enamel,7 1/4 x 5 3/4 in. (185 x 147 mm),"The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.503,false,true,435698,European Paintings,"Painting, miniature","Matthew Baillie (1761–1823), F.R.S., after Hoppner",,,,,,Artist,,Henry Bone,"British, Truro 1755–1834 Somerstown",,"Bone, Henry",British,1755,1834,1817,1817,1817,Enamel,5 1/8 x 4 in. (130 x 103 mm),"Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435698,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.521,false,true,435695,European Paintings,"Painting, miniature","Henry Hope (1735/36–1811), after Jones",,,,,,Artist,,Henry Bone,"British, Truro 1755–1834 Somerstown",,"Bone, Henry",British,1755,1834,1802,1802,1802,Enamel,"Oval, 2 1/8 x 1 3/4 in. (54 x 43 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.523,false,true,435699,European Paintings,"Painting, miniature","Charles X (1757–1836), King of France, after Gérard",,,,,,Artist,,Henry Bone,"British, Truro 1755–1834 Somerstown",,"Bone, Henry",British,1755,1834,1829,1829,1829,Enamel,14 1/4 x 10 1/4 in. (364 x 260 mm),"Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435699,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.168.61,false,true,435697,European Paintings,"Painting, miniature","George IV (1762–1830) as Prince Regent, after Lawrence",,,,,,Artist,,Henry Bone,"British, Truro 1755–1834 Somerstown",,"Bone, Henry",British,1755,1834,1816,1816,1816,Enamel,"Oval, 2 1/2 x 2 in. (64 x 49 mm)","Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435697,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.89.2,false,true,436650,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Nicholas Hilliard,"British, Exeter ca. 1547–1619 London",,"Hilliard, Nicholas",British,1542,1619,1597,1597,1597,Vellum,"Oval, 1 7/8 x 1 1/2 in. (47 x 39 mm)","Fletcher Fund, 1935",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436650,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.89.4,false,true,436649,European Paintings,"Painting, miniature","Portrait of a Young Man, Probably Robert Devereux (1566–1601), Second Earl of Essex",,,,,,Artist,,Nicholas Hilliard,"British, Exeter ca. 1547–1619 London",,"Hilliard, Nicholas",British,1542,1619,1588,1588,1588,Vellum laid on card,"Oval, 1 5/8 x 1 3/8 in. (40 x 33 mm)","Fletcher Fund, 1935",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436649,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.311,false,true,436651,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Nicholas Hilliard,"British, Exeter ca. 1547–1619 London",,"Hilliard, Nicholas",British,1542,1619,ca. 1590,1585,1595,Vellum laid on card,"Oval, 1 x 7/8 in. (27 x 22 mm)","The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436651,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.33,false,true,435941,European Paintings,"Painting, miniature","Henry Carey (1596–1661), Second Earl of Monmouth",,,,,,Artist,,Samuel Cooper,"British, London (?) 1608?–1672 London",,"Cooper, Samuel",British,1608,1672,1649,1649,1649,Vellum on prepared card,"Oval, 2 1/2 x 2 in. (64 x 52 mm)","Rogers Fund, 1949",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435941,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.516,false,true,435942,European Paintings,"Painting, miniature","Portrait of a Woman, Said to Be Lucy Percy (1600?–1660), Countess of Carlisle",,,,,,Artist,,Samuel Cooper,"British, London (?) 1608?–1672 London",,"Cooper, Samuel",British,1608,1672,1653,1653,1653,Vellum laid on prepared card,"Oval, 2 1/2 x 2 in. (65 x 50 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435942,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +35.89.3,false,true,436696,European Paintings,"Painting, miniature","Dr. Brian Walton (born about 1600, died 1661)",,,,,,Artist,,John Hoskins,"British, active by ca. 1615–died 1665",,"Hoskins, John",British,1615,1665,1657,1657,1657,Vellum laid on card,"Oval, 2 3/4 x 2 1/4 in. (72 x 58 mm)","Fletcher Fund, 1935",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.505,false,true,436694,European Paintings,"Painting, miniature",Endymion Porter (1587–1649),,,,,,Artist,,John Hoskins,"British, active by ca. 1615–died 1665",,"Hoskins, John",British,1615,1665,ca. 1630,1625,1635,Vellum,"Oval, 3 1/8 x 2 5/8 in. (80 x 66 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.22,false,true,436695,European Paintings,"Painting, miniature","Portrait of a Man, Said to Be Philip Wharton (1613–1696), Fourth Baron Wharton",,,,,,Artist,,John Hoskins,"British, active by ca. 1615–died 1665",,"Hoskins, John",British,1615,1665,1648,1648,1648,Vellum on prepared card,"Oval, 2 3/4 x 2 1/4 in. (69 x 56 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.28,false,true,436673,European Paintings,"Painting, miniature","Portrait of a Woman, Said to Be Lady Agnes Anne Wrothesley",,,,,,Artist,,Horace Hone,"British, London ca. 1754/56–1825 London",,"Hone, Horace",British,1749,1825,1791,1791,1791,Ivory,"Oval, 2 x 1 5/8 in. (52 x 41 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436673,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.49,false,true,435994,European Paintings,"Painting, miniature",Self-Portrait,,,,,,Artist,,Richard Cosway,"British, Oakford, Devon 1742–1821 London",,"Cosway, Richard",British,1742,1821,ca. 1770–75,1770,1775,Ivory,"Oval, 2 x 1 5/8 in. (50 x 42 mm)","Gift of Charlotte Guilford Muhlhofer, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435994,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.513,false,true,436202,European Paintings,"Painting, miniature",Sir Henry Blount (1602–1682),,,,,,Artist,,Nicholas Dixon,"British, active by ca. 1660–died after 1708",,"Dixon, Nicholas",British,1660,1708,1660s,1660,1669,Vellum laid on card,"Oval, 2 5/8 x 2 1/8 in. (67 x 55 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436202,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1975.217.1,false,true,437584,European Paintings,Drawing,Robert Shurlock (1772–1847),,,,,,Artist,Attributed to,William Russell,"British, London 1784–1870 Highgate",,"Russell, William",British,1784,1870,ca. 1805,1800,1810,Pastel on paper,23 7/8 x 17 7/8 in. (60.6 x 45.4 cm),"Gift of Olive Shurlock Sjölander, 1975",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/437584,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.233,false,true,436859,European Paintings,Drawing,Catania and Mount Etna,,,,,,Artist,,Edward Lear,"British, London 1812–1888 San Remo",,"Lear, Edward",British,1812,1888,1847,1847,1847,Oil on board,12 1/4 x 19 in. (31.1 x 48.3 cm),"Rogers Fund, 1961",,,,,,,,,,,,Drawings,,http://www.metmuseum.org/art/collection/search/436859,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.75,false,true,436845,European Paintings,Painting,"Copy after Rubens's ""Wolf and Fox Hunt""",,,,,,Artist,,Sir Edwin Henry Landseer,,,"Landseer, Edwin Henry, Sir",British,1802,1873,ca. 1824–26,1824,1826,Oil on wood,16 x 23 7/8 in. (40.6 x 60.6 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436845,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.10.42,false,true,436226,European Paintings,Painting,"Homeward Bound: ""The Great Eastern""",,,,,,Artist,,Robert Charles Dudley,"British, 1826–1909",,"Dudley, Robert Charles",British,1826,1909,ca. 1866,1861,1871,Oil on canvas,44 3/4 x 67 1/4 in. (113.7 x 170.8 cm),"Gift of Cyrus W. Field, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436226,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.10.43,false,true,436224,European Paintings,Painting,Awaiting the Reply,,,,,,Artist,,Robert Charles Dudley,"British, 1826–1909",,"Dudley, Robert Charles",British,1826,1909,ca. 1866,1861,1871,Oil on canvas,23 1/4 x 33 1/2 in. (59.1 x 85.1 cm),"Gift of Cyrus W. Field, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436224,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.10.44,false,true,436223,European Paintings,Painting,Landing the Shore End of the Atlantic Cable,,,,,,Artist,,Robert Charles Dudley,"British, 1826–1909",,"Dudley, Robert Charles",British,1826,1909,1866,1866,1866,Oil on canvas,22 1/2 x 33 in. (57.2 x 83.8 cm),"Gift of Cyrus W. Field, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436223,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.10.45,false,true,436225,European Paintings,Painting,Grappling for the Lost Cable,,,,,,Artist,,Robert Charles Dudley,"British, 1826–1909",,"Dudley, Robert Charles",British,1826,1909,ca. 1866,1861,1871,Oil on canvas,22 3/4 x 33 1/8 in. (57.8 x 84.1 cm),"Gift of Cyrus W. Field, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436225,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.10.46,false,true,436227,European Paintings,Painting,Landing at Newfoundland,,,,,,Artist,,Robert Charles Dudley,"British, 1826–1909",,"Dudley, Robert Charles",British,1826,1909,ca. 1866,1861,1871,Oil on canvas,22 3/4 x 33 1/4 in. (57.8 x 84.5 cm),"Gift of Cyrus W. Field, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.10.47,false,true,436228,European Paintings,Painting,Making the Splice between the Shore End and the Ocean Cable,,,,,,Artist,,Robert Charles Dudley,"British, 1826–1909",,"Dudley, Robert Charles",British,1826,1909,ca. 1866,1861,1871,Oil on canvas,22 3/4 x 33 1/4 in. (57.8 x 84.5 cm),"Gift of Cyrus W. Field, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436228,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.34,false,true,436442,European Paintings,Painting,The Painter's Daughter Mary (1750–1826),,,,,,Artist,Copy after,Thomas Gainsborough,"British, mid-19th century",,"Gainsborough, Thomas",British,1727,1788,mid-19th century,1830,1869,Oil on canvas,17 1/4 x 13 7/8 in. (43.8 x 35.2 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436442,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.27,false,true,437262,European Paintings,Painting,"Henry Frederick (1594–1612), Prince of Wales, with Sir John Harington (1592–1614), in the Hunting Field",,,,,,Artist,,Robert Peake the Elder,"British, ca. 1551–1619 London",,"Peake, Robert, the Elder",British,1546,1619,1603,1603,1603,Oil on canvas,79 1/2 x 58 in. (201.9 x 147.3 cm),"Purchase, Joseph Pulitzer Bequest, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437262,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.194.1,false,true,437263,European Paintings,Painting,"Princess Elizabeth (1596–1662), Later Queen of Bohemia",,,,,,Artist,,Robert Peake the Elder,"British, ca. 1551–1619 London",,"Peake, Robert, the Elder",British,1546,1619,ca. 1606,1601,1611,Oil on canvas,60 3/4 x 31 1/4 in. (154.3 x 79.4 cm),"Gift of Kate T. Davison, in memory of her husband, Henry Pomeroy Davison, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437263,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2013.155,false,true,441769,European Paintings,Painting,"Virgil's Tomb by Moonlight, with Silius Italicus Declaiming",,,,,,Artist,,Joseph Wright (Wright of Derby),"British, Derby 1734–1797 Derby",,"Wright, Joseph (Wright of Derby)",British,1734,1797,1779,1779,1779,Oil on canvas,40 x 50 in. (101.6 x 127 cm),"Purchase, Lila Acheson Wallace Gift, Gifts of Mrs. William M. Haupt, Josephine Bay Paul, and Estate of George Quackenbush, in his memory, by exchange, The Morris and Alma Schapiro Fund Gift, and funds from various donors, 2013",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441769,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.264.6,false,true,437954,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Joseph Wright (Wright of Derby),"British, Derby 1734–1797 Derby",,"Wright, Joseph (Wright of Derby)",British,1734,1797,ca. 1770,1765,1775,Oil on canvas,49 7/8 x 40 in. (126.7 x 101.6 cm),"Gift of Heathcote Art Foundation, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437954,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.132.4,false,true,435895,European Paintings,Painting,Self-Portrait,,,,,,Artist,,George Chinnery,"British, London 1774–1852 Macau",,"Chinnery, George",British,1774,1852,1825–28,1825,1828,Oil on canvas,8 5/8 x 7 1/4 in. (21.9 x 18.4 cm),"Rogers Fund, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435895,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +36.111,false,true,436656,European Paintings,Painting,The Wedding of Stephen Beckingham and Mary Cox,,,,,,Artist,,William Hogarth,"British, London 1697–1764 London",,"Hogarth, William",British,1697,1764,1729,1729,1729,Oil on canvas,50 1/2 x 40 1/2 in. (128.3 x 102.9 cm),"Marquand Fund, 1936",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436656,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.159,false,true,437280,European Paintings,Painting,The Strong Family,,,,,,Artist,,Charles Philips,"British, London 1703–1747 London",,"Philips, Charles",British,1703,1747,1732,1732,1732,Oil on canvas,29 5/8 x 37 in. (75.2 x 94 cm),"Gift of Robert Lehman, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437280,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.357,false,true,440568,European Paintings,Painting,The Saithwaite Family,,,,,,Artist,,Francis Wheatley,"British, London 1747–1801 London",,"Wheatley, Francis",British,1747,1801,ca. 1785,1780,1790,Oil on canvas,38 3/4 x 50 in. (98.4 x 127 cm),"Gift of Mrs. Charles Wrightsman, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440568,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +51.30.1,false,true,435671,European Paintings,Painting,The Angel Appearing to Zacharias,,,,,,Artist,,William Blake,"British, London 1757–1827 London",,"Blake, William",British,1757,1827,1799–1800,1799,1800,"Pen and black ink, tempera, and glue size on canvas",10 1/2 x 15 in. (26.7 x 38.1 cm),"Bequest of William Church Osborn, 1951",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435671,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +01.20,false,true,436686,European Paintings,Painting,"Mrs. Richard Bache (Sarah Franklin, 1743–1808)",,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,1793,1793,1793,Oil on canvas,30 1/8 x 24 7/8 in. (76.5 x 63.2 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1901",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1242,false,true,436690,European Paintings,Painting,"Portrait of a Woman; (reverse, now covered by relining canvas) Study of a Child's Head",,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,1790s,1790,1799,Oil on canvas,30 x 24 7/8 in. (76.2 x 63.2 cm),"Gift of William T. and Eleanor Blodgett, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436690,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.13.3,false,true,436684,European Paintings,Painting,Major Thomas Pechell (1753–1826),,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,1799,1799,1799,Oil on canvas,30 x 24 7/8 in. (76.2 x 63.2 cm),"Bequest of Helen Swift Neilson, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +46.13.4,false,true,436688,European Paintings,Painting,"Mrs. Thomas Pechell (Charlotte Clavering, died 1841)",,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,1799,1799,1799,Oil on canvas,30 x 25 in. (76.2 x 63.5 cm),"Bequest of Helen Swift Neilson, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436688,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.59.3,false,true,436692,European Paintings,Painting,The Sackville Children,,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,1796,1796,1796,Oil on canvas,60 x 49 in. (152.4 x 124.5 cm),"Bequest of Thomas W. Lamont, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.41,false,true,436685,European Paintings,Painting,"Mrs. John Garden (Ann Garden, 1769–1842) and Her Children, John (1796–1854) and Ann Margaret (born 1793)",,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,1796 or 1797,1796,1797,Oil on canvas,50 1/8 x 39 7/8 in. (127.3 x 101.3 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.189.3,false,true,436683,European Paintings,Painting,Lady Hester King (died 1873),,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,probably 1805,1804,1810,Oil on canvas,30 x 25 in. (76.2 x 63.5 cm),"Gift of Bernard M. Baruch, in memory of his wife, Annie Griffen Baruch, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.20,false,true,437161,European Paintings,Painting,The Bell Inn,,,,,,Artist,,George Morland,"British, London 1763–1804 London",,"Morland, George",British,1763,1804,late 1780s,1787,1789,Oil on canvas,20 1/2 x 26 1/4 in. (52.1 x 66.7 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437161,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.29,true,true,437854,European Paintings,Painting,Whalers,,,,,,Artist,,Joseph Mallord William Turner,"British, London 1775–1851 London",,"Turner, Joseph Mallord William",British,1775,1851,ca. 1845,1840,1850,Oil on canvas,36 1/8 x 48 1/4 in. (91.8 x 122.6 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1896",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +99.31,true,true,437853,European Paintings,Painting,"Venice, from the Porch of Madonna della Salute",,,,,,Artist,,Joseph Mallord William Turner,"British, London 1775–1851 London",,"Turner, Joseph Mallord William",British,1775,1851,ca. 1835,1830,1835,Oil on canvas,36 x 48 1/8 in. (91.4 x 122.2 cm),"Bequest of Cornelius Vanderbilt, 1899",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.9,false,true,437852,European Paintings,Painting,"Saltash with the Water Ferry, Cornwall",,,,,,Artist,,Joseph Mallord William Turner,"British, London 1775–1851 London",,"Turner, Joseph Mallord William",British,1775,1851,1811,1811,1811,Oil on canvas,35 3/8 x 47 1/2 in. (89.9 x 120.7 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +05.39.1,false,true,437936,European Paintings,Painting,Ariadne,,,,,,Artist,,George Frederic Watts,"British, London 1817–1904 London",,"Watts, George Frederic",British,1817,1904,1894,1894,1894,Oil on canvas,24 x 20 in. (61 x 50.8 cm),"Rogers Fund, 1905",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437936,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +97.41.3,false,true,437935,European Paintings,Painting,"An Old Bridge at Hendon, Middlesex",,,,,,Artist,,Frederick Waters Watts,"British, Bath 1800–1870 Hampstead",,"Watts, Frederick Waters",British,1800,1870,ca. 1828,1820,1833,Oil on canvas,21 3/4 x 32 3/4 in. (55.2 x 83.2 cm),"Gift of George A. Hearn, 1897",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +55.89,false,true,436853,European Paintings,Painting,Lady Maria Conyngham (died 1843),,,,,,Artist,,Sir Thomas Lawrence,"British, Bristol 1769–1830 London",,"Lawrence, Thomas, Sir",British,1769,1830,ca. 1824–25,1824,1825,Oil on canvas,36 1/4 x 28 1/4 in. (92.1 x 71.8 cm),"Gift of Jessie Woolworth Donahue, 1955",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.1,false,true,436850,European Paintings,Painting,"The Calmady Children (Emily, 1818–?1906, and Laura Anne, 1820–1894)",,,,,,Artist,,Sir Thomas Lawrence,"British, Bristol 1769–1830 London",,"Lawrence, Thomas, Sir",British,1769,1830,1823,1789,1830,Oil on canvas,30 7/8 x 30 1/8 in. (78.4 x 76.5 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436850,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.135.5,true,true,436851,European Paintings,Painting,"Elizabeth Farren (born about 1759, died 1829), Later Countess of Derby",,,,,,Artist,,Sir Thomas Lawrence,"British, Bristol 1769–1830 London",,"Lawrence, Thomas, Sir",British,1769,1830,1790,1790,1790,Oil on canvas,94 x 57 1/2 in. (238.8 x 146.1 cm),"Bequest of Edward S. Harkness, 1940",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436851,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1279,false,true,436441,European Paintings,Painting,Wooded Upland Landscape,,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,probably 1783,1783,1783,Oil on canvas,47 3/8 x 58 1/8 in. (120.3 x 147.6 cm),"Gift of George A. Hearn, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436441,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.59.1,false,true,436437,European Paintings,Painting,"Mrs. William Tennant (Mary Wylde, died 1798)",,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,1780s,1780,1789,Oil on canvas,49 1/2 x 40 in. (125.7 x 101.6 cm),"Fletcher Fund, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436437,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.8,false,true,436433,European Paintings,Painting,A Boy with a Cat—Morning,,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,1787,1787,1787,Oil on canvas,59 1/4 x 47 1/2 in. (150.5 x 120.7 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436433,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.1,true,true,436435,European Paintings,Painting,Mrs. Grace Dalrymple Elliott (1754?–1823),,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,1778,1778,1778,Oil on canvas,92 1/4 x 60 1/2in. (234.3 x 153.7cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436435,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.16,false,true,436431,European Paintings,Painting,Charles Rousseau Burney (1747–1819),,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,ca. 1780,1775,1785,Oil on canvas,30 1/4 x 25 1/8 in. (76.8 x 63.8 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436431,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.17,false,true,436440,European Paintings,Painting,Cottage Children (The Wood Gatherers),,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,1787,1787,1787,Oil on canvas,58 1/8 x 47 3/8 in. (147.6 x 120.3 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436440,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +17.120.224,false,true,436439,European Paintings,Painting,"Portrait of a Young Woman, Called Miss Sparrow",,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,1770s,1770,1779,Oil on canvas,30 1/8 x 24 7/8 in. (76.5 x 63.2 cm),"Mr. and Mrs. Isaac D. Fletcher Collection, Bequest of Isaac D. Fletcher, 1917",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436439,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +89.15.14,false,true,436057,European Paintings,Painting,"Hautbois Common, Norfolk",,,,,,Artist,,John Crome,"British, Norwich 1768–1821 Norwich",,"Crome, John",British,1768,1821,probably ca. 1810,1805,1815,Oil on canvas,22 x 35 in. (55.9 x 88.9 cm),"Marquand Collection, Gift of Henry G. Marquand, 1889",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436057,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.16,false,true,437449,European Paintings,Painting,The Honorable Henry Fane (1739–1802) with Inigo Jones and Charles Blair,,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1761–66,1761,1766,Oil on canvas,100 1/4 x 142 in. (254.6 x 360.7 cm),"Gift of Junius S. Morgan, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +48.181,false,true,437445,European Paintings,Painting,"George Capel, Viscount Malden (1757–1839), and Lady Elizabeth Capel (1755–1834)",,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1768,1768,1768,Oil on canvas,71 1/2 x 57 1/4 in. (181.6 x 145.4 cm),"Gift of Henry S. Morgan, 1948",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437445,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +54.192,false,true,437451,European Paintings,Painting,John Barker (1707–1787),,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1786,1786,1786,Oil on canvas,68 1/4 x 47 1/2 in. (173.4 x 120.7 cm),"Gift of Ruth Armour, 1954",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437451,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1241,false,true,437454,European Paintings,Painting,"Mrs. George Baldwin (Jane Maltass, 1763–1839)",,,,,,Artist,Workshop of,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1782 or later,1782,1792,Oil on canvas,36 1/8 x 29 1/8 in. (91.8 x 74 cm),"Gift of William T. Blodgett and his sister Eleanor Blodgett, in memory of their father, William T. Blodgett, one of the founders of the Museum, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437454,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.38,false,true,437448,European Paintings,Painting,"Georgiana Augusta Frederica Elliott (1782–1813), Later Lady Charles Bentinck",,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",and Workshop,"Reynolds, Joshua, Sir",British,1723,1792,1784,1784,1784,Oil on canvas,35 x 30 in. (88.9 x 76.2 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437448,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.155.3,true,true,437447,European Paintings,Painting,Captain George K. H. Coussmaker (1759–1801),,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1782,1782,1782,Oil on canvas,93 3/4 x 57 1/4 in. (238.1 x 145.4 cm),"Bequest of William K. Vanderbilt, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437447,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.238.2,false,true,437444,European Paintings,Painting,"Anne Dashwood (1743–1830), Later Countess of Galloway",,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1764,1764,1764,Oil on canvas,"52 1/2 x 46 3/4 in. (133.4 x 118.7 cm), with strip of 7 1/8 in. (18.1 cm) folded over the top of the stretcher","Gift of Lillian S. Timken, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437444,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.47.2,false,true,437450,European Paintings,Painting,"Mrs. Lewis Thomas Watson (Mary Elizabeth Milles, 1767–1818)",,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1789,1789,1789,Oil on canvas,50 x 40 in. (127 x 101.6 cm),"Bequest of Mrs. Harry Payne Bingham, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437450,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.110.10,false,true,437452,European Paintings,Painting,"Lady Smith (Charlotte Delaval) and Her Children (George Henry, Louisa, and Charlotte)",,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1787,1787,1787,Oil on canvas,55 3/8 x 44 1/8 in. (140.7 x 112.1 cm),"Bequest of Collis P. Huntington, 1900",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437452,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.264.5,false,true,437443,European Paintings,Painting,"Thomas (1740–1825) and Martha Neate (1741–after 1795) with His Tutor, Thomas Needham",,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,1748,1748,1748,Oil on canvas,66 1/8 x 71 in. (168 x 180.3 cm),"Gift of Heathcote Art Foundation, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437443,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1980.468,false,true,437764,European Paintings,Painting,The Third Duke of Dorset's Hunter with a Groom and a Dog,,,,,,Artist,,George Stubbs,"British, Liverpool 1724–1806 London",,"Stubbs, George",British,1724,1806,1768,1768,1768,Oil on canvas,40 x 49 3/4 in. (101.6 x 126.4 cm),"Bequest of Mrs. Paul Moore, 1980",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437764,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +09.1.1,false,true,437751,European Paintings,Painting,"Richmond Castle, Yorkshire",,,,,,Artist,,Philip Wilson Steer,"British, Birkenhead 1860–1942 London",,"Steer, Philip Wilson",British,1860,1942,1903,1903,1903,Oil on canvas,29 1/8 x 34 1/2 in. (74 x 87.6 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437751,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.26,true,true,435826,European Paintings,Painting,The Love Song,,,,,,Artist,,Sir Edward Burne-Jones,"British, Birmingham 1833–1898 Fulham",,"Burne-Jones, Edward, Sir",British,1833,1898,1868–77,1868,1877,Oil on canvas,45 x 61 3/8 in. (114.3 x 155.9 cm),"The Alfred N. Punnett Endowment Fund, 1947",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435826,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +96.28,false,true,436869,European Paintings,Painting,Lachrymae,,,,,,Artist,,"Frederic, Lord Leighton","British, Scarborough 1830–1896 London",,"Leighton, Frederic, Lord",British,1830,1896,ca. 1894–95,1894,1895,Oil on canvas,62 x 24 3/4 in. (157.5 x 62.9 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1896",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436869,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1328,false,true,437092,European Paintings,Painting,Portia,,,,,,Artist,,Sir John Everett Millais,"British, Southampton 1829–1896 London",,"Millais, John Everett, Sir",British,1829,1896,1886,1886,1886,Oil on canvas,49 1/4 x 33 in. (125.1 x 83.8 cm),"Catharine Lorillard Wolfe Collection, Wolfe Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437092,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.287,false,true,437680,European Paintings,Painting,The Bridge at Villeneuve-la-Garenne,,,,,,Artist,,Alfred Sisley,"British, Paris 1839–1899 Moret-sur-Loing",,"Sisley, Alfred",British,1839,1899,1872,1872,1872,Oil on canvas,19 1/2 x 25 3/4 in. (49.5 x 65.4 cm),"Gift of Mr. and Mrs. Henry Ittleson Jr., 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437680,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.366,false,true,437686,European Paintings,Painting,Rue Eugène Moussoir at Moret: Winter,,,,,,Artist,,Alfred Sisley,"British, Paris 1839–1899 Moret-sur-Loing",,"Sisley, Alfred",British,1839,1899,1891,1891,1891,Oil on canvas,18 3/8 x 22 1/4 in. (46.7 x 56.5 cm),"Bequest of Ralph Friedman, 1992",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437686,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.154.2,false,true,437685,European Paintings,Painting,The Road from Versailles to Louveciennes,,,,,,Artist,,Alfred Sisley,"British, Paris 1839–1899 Moret-sur-Loing",,"Sisley, Alfred",British,1839,1899,probably 1879,1859,1899,Oil on canvas,18 x 22 in. (45.7 x 55.9 cm),"Gift of Mr. and Mrs. Richard Rodgers, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437685,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1991.277.3,false,true,437683,European Paintings,Painting,Sahurs Meadows in Morning Sun,,,,,,Artist,,Alfred Sisley,"British, Paris 1839–1899 Moret-sur-Loing",,"Sisley, Alfred",British,1839,1899,1894,1894,1894,Oil on canvas,28 3/4 x 36 1/4 in. (73 x 92.1 cm),"Gift of Janice H. Levin, 1991",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437683,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.103,false,true,437682,European Paintings,Painting,View of Marly-le-Roi from Coeur-Volant,,,,,,Artist,,Alfred Sisley,"British, Paris 1839–1899 Moret-sur-Loing",,"Sisley, Alfred",British,1839,1899,1876,1876,1876,Oil on canvas,25 3/4 x 36 3/8 in. (65.4 x 92.4 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437682,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1976.201.18,false,true,437684,European Paintings,Painting,The Road from Moret to Saint-Mammès,,,,,,Artist,,Alfred Sisley,"British, Paris 1839–1899 Moret-sur-Loing",,"Sisley, Alfred",British,1839,1899,1883–85,1883,1885,Oil on canvas,19 7/8 x 24 1/4 in. (50.5 x 61.5 cm),"Bequest of Joan Whitney Payson, 1975",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437684,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +26.128,false,true,435923,European Paintings,Painting,Stoke-by-Nayland,,,,,,Artist,,John Constable,"British, East Bergholt 1776–1837 Hampstead",,"Constable, John",British,1776,1837,ca. 1810–11,1805,1815,Oil on canvas,11 1/8 x 14 1/4 in. (28.3 x 36.2 cm),"Charles B. Curtis Fund, 1926",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435923,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1272,false,true,435921,European Paintings,Painting,"Mrs. James Pulham Sr. (Frances Amys, ca. 1766–1856)",,,,,,Artist,,John Constable,"British, East Bergholt 1776–1837 Hampstead",,"Constable, John",British,1776,1837,1818,1818,1818,Oil on canvas,29 3/4 x 24 3/4 in. (75.6 x 62.9 cm),"Gift of George A. Hearn, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435921,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.8,true,true,435922,European Paintings,Painting,Salisbury Cathedral from the Bishop's Grounds,,,,,,Artist,,John Constable,"British, East Bergholt 1776–1837 Hampstead",,"Constable, John",British,1776,1837,ca. 1825,1820,1830,Oil on canvas,34 5/8 x 44 in. (87.9 x 111.8 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435922,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.40,false,true,436079,European Paintings,Painting,Uvedale Tomkyns Price (1685–1764) and Members of His Family,,,,,,Artist,,Bartholomew Dandridge,"British, London 1691–in or after 1754 London",,"Dandridge, Bartholomew",British,1691,1754,possibly early 1730s,1730,1733,Oil on canvas,40 1/4 x 62 1/2 in. (102.2 x 158.8 cm),"Rogers Fund, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436079,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2008.547.3,false,true,440725,European Paintings,Painting,"The Two Central Figures in ""Derby Day""",,,,,,Artist,,William Powell Frith,"British, Aldfield, Yorkshire 1819–1909 London",,"Frith, William Powell",British,1819,1909,1860,1860,1860,Oil on canvas,18 x 12 1/2 in. (45.7 x 31.8 cm),"Gift of Mrs. Charles Wrightsman, 2008",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/440725,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.451.2,false,true,490260,European Paintings,Painting,"Maple Street, London",,,,,,Artist,,Walter Richard Sickert,"British, Munich 1860–1942 Bathampton, Somerset",,"Sickert, Walter Richard",British,1860,1942,ca. 1915–23,1910,1928,Oil on canvas,30 1/4 × 20 1/8 in. (76.8 × 51.1 cm),"Gift of Emma Swan Hall, 1998",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/490260,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.451.3,false,true,490261,European Paintings,Painting,The Antique Shop,,,,,,Artist,,Walter Richard Sickert,"British, Munich 1860–1942 Bathampton, Somerset",,"Sickert, Walter Richard",British,1860,1942,ca. 1906,1901,1911,Oil on cardboard,9 1/2 x 7 1/2 in. (24.1 x 19.1 cm),"Gift of Emma Swan Hall, 1998",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/490261,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1979.135.17,false,true,437670,European Paintings,Painting,The Cigarette (Jeanne Daurmont),,,,,,Artist,,Walter Richard Sickert,"British, Munich 1860–1942 Bathampton, Somerset",,"Sickert, Walter Richard",British,1860,1942,1906,1906,1906,Oil on canvas,20 x 16 in. (50.8 x 40.6 cm),"Bequest of Mary Cushing Fosburgh, 1978",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437670,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.433.24,false,true,483377,European Paintings,Painting,Reclining Nude (Thin Adeline),,,,,,Artist,,Walter Richard Sickert,"British, Munich 1860–1942 Bathampton, Somerset",,"Sickert, Walter Richard",British,1860,1942,1906,1906,1906,Oil on canvas,18 1/8 × 15 1/8 in. (46 × 38.4 cm),"Bequest of Scofield Thayer, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/483377,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2001.45,false,true,438449,European Paintings,Painting,View near Rouen,,,,,,Artist,,Richard Parkes Bonington,"British, Arnold, Nottinghamshire 1802–1828 London",,"Bonington, Richard Parkes",British,1802,1828,ca. 1825,1820,1830,Oil on millboard,11 x 13 in. (27.9 x 33 cm),"Purchase, Gift of Joanne Toor Cummings, by exchange, 2001",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438449,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.146.1,false,true,435707,European Paintings,Painting,Roadside Halt,,,,,,Artist,,Richard Parkes Bonington,"British, Arnold, Nottinghamshire 1802–1828 London",,"Bonington, Richard Parkes",British,1802,1828,1826,1826,1826,Oil on canvas,18 1/4 x 14 7/8 in. (46.4 x 37.8 cm),"Gift of Francis Neilson, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435707,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +05.32.1,false,true,435632,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Sir William Beechey,"British, Burford, Oxfordshire 1753–1839 Hampstead",,"Beechey, William, Sir",British,1753,1839,ca. 1805,1800,1810,Oil on canvas,50 x 40 1/4 in. (127 x 102.2 cm),"Gift of George A. Hearn, 1905",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435632,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.59.4,false,true,437501,European Paintings,Painting,"Mrs. Bryan Cooke (Frances Puleston, 1765–1818)",,,,,,Artist,,George Romney,"British, Beckside, Lancashire 1734–1802 Kendal, Cumbria",,"Romney, George",British,1734,1802,ca. 1787–91,1787,1791,Oil on canvas,50 x 39 1/2 in. (127 x 100.3 cm),"Fletcher Fund, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437501,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.57,false,true,437500,European Paintings,Painting,"Lady Elizabeth Stanley (1753–1797), Countess of Derby",,,,,,Artist,,George Romney,"British, Beckside, Lancashire 1734–1802 Kendal, Cumbria",,"Romney, George",British,1734,1802,1776–78,1776,1778,Oil on canvas,50 x 40 in. (127 x 101.6 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437500,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +15.30.37,false,true,437504,European Paintings,Painting,Self-Portrait,,,,,,Artist,,George Romney,"British, Beckside, Lancashire 1734–1802 Kendal, Cumbria",,"Romney, George",British,1734,1802,1795,1795,1795,Oil on canvas,30 x 25 in. (76.2 x 63.5 cm),"Bequest of Maria DeWitt Jesup, from the collection of her husband, Morris K. Jesup, 1914",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437504,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.102.2,false,true,437498,European Paintings,Painting,"Portrait of a Woman, Said to Be Emily Bertie Pott (died 1782)",,,,,,Artist,,George Romney,"British, Beckside, Lancashire 1734–1802 Kendal, Cumbria",,"Romney, George",British,1734,1802,1781,1781,1781,Oil on canvas,29 3/4 x 24 7/8 in. (75.6 x 63.2 cm),"Gift of Jessie Woolworth Donahue, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.159,false,true,436863,European Paintings,Painting,General Garibaldi's Residence at Caprera,,,,,,Artist,,Frederick Richard Lee,"British, Barnstaple 1798–1879 Hermon Station, Malmsbury, Cape Colony, South Africa",,"Lee, Frederick Richard",British,1798,1879,1865,1865,1865,Oil on canvas,34 1/4 x 54 3/8 in. (87 x 138.1 cm),"Gift of Dr. Melvin Goldberg, 1974",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436863,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.60,false,true,436589,European Paintings,"Painting, miniature",Sir Joshua Reynolds (1723–1792),,,,,,Artist,Style of,William Grimaldi,1773 or later,,"Grimaldi, William",British,1751,1830,,1773,1773,Ivory,"Oval, 1 7/8 x 1 1/2 in. (47 x 37 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436589,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.191.2,false,true,437696,European Paintings,"Painting, miniature",Portrait of an Officer,,,,,,Artist,Imitator of,John Smart,1784 or later?,,"Smart, John",British,1741,1811,,1784,1789,Ivory,"Oval, 1 3/4 x 1 1/4 in. (45 x 32 mm)","Gift of Mrs. Thomas Hunt, 1941",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437696,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.32,false,true,437665,European Paintings,"Painting, miniature",Portrait of a Boy,,,,,,Artist,Style of,Samuel Shelley,late 18th century,,"Shelley, Samuel",British,1756,1808,,1770,1799,Ivory,"Oval, 1 1/4 x 1 in. (33 x 27 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437665,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.442,false,true,436878,European Paintings,"Painting, miniature","Copy after ""Rubens, His Wife Helena Fourment (1614–1673), and Their Son Frans (1633–1678)""",,,,,,Artist,,Bernard Lens,"British, 1682–1740",,"Lens, Bernard",British,1682,1740,,1721,1721,Vellum,15 1/2 x 11 7/8 in. (394 x 302 mm),"Purchase, Mr. and Mrs. Charles Wrightsman Gift, 1984",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436878,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.524,false,true,436777,European Paintings,"Painting, miniature",Admiral Adam Duncan (1731–1804),,,,,,Artist,Attributed to,Philip Jean,"British, 1755–1802",,"Jean, Philip",British,1755,1802,,1775,1802,Ivory,"Oval, 2 x 1 1/2 in. (50 x 40 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436777,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +25.106.29,false,true,436778,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,Attributed to,Philip Jean,"British, 1755–1802",,"Jean, Philip",British,1755,1802,,1775,1802,Ivory,"Oval, 1 3/4 x 1 1/2 in. (46 x 37 mm)","Gift of Mrs. Louis V. Bell, in memory of her husband, 1925",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436778,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.36,false,true,435919,European Paintings,"Painting, miniature","George Howard (1773–1848), Lord Morpeth",,,,,,Artist,,Richard Collins,"British, 1755–1831",,"Collins, Richard",British,1755,1831,,1775,1831,Ivory,"Oval, 2 x 1 5/8 in. (52 x 41 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435919,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.97,false,true,437951,European Paintings,"Painting, miniature","Portrait of a Man, Said to Be Mr. Fitzgerald",,,,,,Artist,,William Wood,"British, 1769–1810",,"Wood, William",British,1769,1810,,1789,1810,Ivory,"Oval, 3 1/4 x 2 5/8 in. (81 x 66 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437951,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.14.69,false,true,435920,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Samuel Collins,"British, 1735?–1768",,"Collins, Samuel",British,1735,1768,,1755,1768,Ivory,"Oval, 1 5/8 x 1 1/4 in. (40 x 32 mm)","The Moses Lazarus Collection, Gift of Josephine and Sarah Lazarus, in memory of their father, 1888–95",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +62.122.21,false,true,435943,European Paintings,"Painting, miniature","Charles II (1630–1685), King of England",,,,,,Artist,Style of,Samuel Cooper,"British, probably after 1672",,"Cooper, Samuel",British,1608,1672,,1672,1700,Vellum laid on prepared card with gessoed back,"Oval, 1 1/4 x 1 1/8 in. (33 x 27 mm)","Bequest of Millie Bruhl Fredrick, 1962",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +33.136.15,false,true,436298,European Paintings,"Painting, miniature",Portrait of a Woman,,,,,,Artist,,Joshua Wilson Faulkner,"British, active ca. 1809–20",,"Faulkner, Joshua Wilson",British,1809,1820,,1809,1820,Ivory,4 x 3 1/8 in. (100 x 80 mm),"Bequest of Margaret Crane Hurlbut, 1933",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436298,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.23.78,false,true,437695,European Paintings,"Painting, miniature","Portrait of a Woman, Said to Be Lady Dering",,,,,,Artist,,John Smart,"British, Norfolk 1741–1811 London",,"Smart, John",British,1741,1811,,1761,1811,Pencil and watercolor on paper,2 1/4 x 2 1/8 in. (56 x 54 mm),"Bequest of Alexandrine Sinsheimer, 1958",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437695,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +68.213,false,true,437277,European Paintings,"Painting, miniature",A Man with the Initials FM,,,,,,Artist,Attributed to,William Pether,"British, Carlisle ca. 1738–1821 Bristol",,"Pether, William",British,1733,1821,,1751,1821,Ivory,"Oval, 3 x 2 1/4 in. (75 x 56 mm)","Gift of Lilliana Teruzzi, 1968",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/437277,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +24.80.499,false,true,435996,European Paintings,"Painting, miniature",Ensign Lionel Robert Tollemache (1774–1793),,,,,,Artist,,Richard Cosway,"British, Oakford, Devon 1742–1821 London",,"Cosway, Richard",British,1742,1821,,1762,1821,Ivory,"Oval, 3 1/8 x 2 1/2 in. (81 x 65 mm)","Bequest of Mary Clark Thompson, 1923",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435996,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +05.31,false,true,436279,European Paintings,"Painting, sketch",The Three Graces,,,,,,Artist,,William Etty,"British, York 1787–1849 York",,"Etty, William",British,1787,1849,,1807,1849,Oil on millboard,22 1/2 x 18 3/4 in. (57.2 x 47.6 cm),"Rogers Fund, 1905",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436279,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.131,false,true,436278,European Paintings,Painting,Allegory,,,,,,Artist,,William Etty,"British, York 1787–1849 York",,"Etty, William",British,1787,1849,,1807,1849,"Oil on canvas, laid down on wood","Oval, 28 x 34 1/2 in. (71.1 x 87.6 cm)","Gift of Martin Birnbaum, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436278,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.1,false,true,437503,European Paintings,Painting,"Mrs. George Horsley (Charlotte Mary Talbot, died 1828)",,,,,,Artist,Attributed to,John Westbrooke Chandler,"British, 1763?–?1807 Edinburgh",,"Chandler, John Westbrooke",British,1763,1807,,1783,1807,Oil on canvas,30 x 24 7/8 in. (76.2 x 63.2 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437503,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.94.2,false,true,437103,European Paintings,Painting,Harbor Scene: An English Ship with Sails Loosened Firing a Gun,,,,,,Artist,,Peter Monamy,"British, London 1681–1749 London",,"Monamy, Peter",British,1681,1749,,1704,1749,Oil on canvas,48 x 59 in. (121.9 x 149.9 cm),"Gift of William P. Clyde, 1960",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437103,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.113,false,true,436691,European Paintings,Painting,"Richard Humphreys, the Boxer",,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,,1778,1810,Oil on canvas,55 3/4 x 44 1/4 in. (141.6 x 112.4 cm),"The Alfred N. Punnett Endowment Fund, 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.203,false,true,436687,European Paintings,Painting,"Mrs. Richard Brinsley Sheridan (Hester Jane Ogle, 1775/76–1817) and Her Son (Charles Brinsley Sheridan, 1796–1843)",,,,,,Artist,,John Hoppner,"British, London 1758–1810 London",,"Hoppner, John",British,1758,1810,,1778,1810,Oil on canvas,93 3/4 x 59 in. (238.1 x 149.9 cm),"Gift of Mrs. Carll Tucker, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436687,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +95.27.2,false,true,436631,European Paintings,Painting,Self-Portrait,,,,,,Artist,,George Henry Harlow,"British, London 1787–1819 London",,"Harlow, George Henry",British,1787,1819,,1807,1819,Oil on canvas,30 x 25 in. (76.2 x 63.5 cm),"Gift of George A. Hearn, 1895",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.264.4,false,true,437239,European Paintings,Painting,"The Grandchildren of Sir William Heathcote, 3rd Baronet",,,,,,Artist,,William Owen,"British, Ludlow 1769–1825 London",,"Owen, William",British,1769,1825,,1789,1825,Oil on canvas,55 1/4 x 67 1/2 in. (140.3 x 171.5 cm),"Gift of Heathcote Art Foundation, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.181.9,false,true,436852,European Paintings,Painting,John Julius Angerstein (1736–1823),,,,,,Artist,,Sir Thomas Lawrence,"British, Bristol 1769–1830 London",and Workshop,"Lawrence, Thomas, Sir",British,1769,1830,,1789,1830,Oil on canvas,36 x 28 in. (91.4 x 71.1 cm),"Bequest of Adele L. Lehman, in memory of Arthur Lehman, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436852,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.55,false,true,436432,European Paintings,Painting,Queen Charlotte,,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,,1747,1788,Oil on canvas,23 3/4 x 17 1/2 in. (60.3 x 44.5 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436432,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +60.71.7,false,true,436438,European Paintings,Painting,"John Hobart (1723–1793), 2nd Earl of Buckinghamshire",,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,,1747,1788,Oil on canvas,29 1/2 x 24 3/4 in. (74.9 x 62.9 cm),"Bequest of Lillian S. Timken, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436438,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +66.88.1,false,true,436436,European Paintings,Painting,"Mrs. Ralph Izard (Alice De Lancey, 1746/47–1832)",,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,,1747,1788,Oil on canvas,"Oval, 30 1/4 x 25 1/8 in. (76.8 x 63.8 cm)","Bequest of Jeanne King deRham, in memory of her father, David H. King Jr., 1966",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436436,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1990.200,false,true,436434,European Paintings,Painting,Lieutenant Colonel Paul Pechell (1724–1800),,,,,,Artist,,Thomas Gainsborough,"British, Sudbury 1727–1788 London",,"Gainsborough, Thomas",British,1727,1788,,1747,1788,Oil on canvas,30 1/8 x 25 1/8 in. (76.5 x 63.8 cm),"Gift of Mr. and Mrs. Harry Payne Bingham Jr., 1990",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436434,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +39.65.5,false,true,435998,European Paintings,Painting,"Admiral Harry Paulet (1719/20–1794), Sixth Duke of Bolton",,,,,,Artist,,Francis Cotes,"British, London 1726–1770 Richmond",,"Cotes, Francis",British,1726,1770,,1760,1770,Oil on canvas,50 x 40 in. (127 x 101.6 cm),"Bequest of Jacob Ruppert, 1939",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435998,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +44.56,false,true,437643,European Paintings,Painting,The Building of Westminster Bridge,,,,,,Artist,,Samuel Scott,"British, London ca. 1702–1772 Bath",,"Scott, Samuel",British,1697,1772,,1722,1772,Oil on canvas,24 x 44 3/8 in. (61 x 112.7 cm),"Purchase, Charles B. Curtis Fund and Joseph Pulitzer Bequest, 1944",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437643,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.59.3,false,true,437446,European Paintings,Painting,"Mrs. Horton, Later Viscountess Maynard (died 1814/15)",,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,,1767,1769,Oil on canvas,36 1/4 x 28 in. (92.1 x 71.1 cm),"Fletcher Fund, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437446,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +42.152.1,false,true,437453,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Sir Joshua Reynolds,"British, Plympton 1723–1792 London",,"Reynolds, Joshua, Sir",British,1723,1792,,1743,1792,Oil on canvas,29 5/8 x 24 1/2 in. (75.2 x 62.2 cm),"Bequest of George D. Pratt, 1935",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437453,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.145.18,false,true,436240,European Paintings,Painting,Mrs. John Puget (Catherine Hawkins),,,,,,Artist,Attributed to,Richard Gainsborough Dupont,"British, Sudbury 1789–1874 Sudbury",,"Dupont, Richard Gainsborough",British,1789,1874,,1774,1797,Oil on copper,6 x 4 3/4 in. (15.2 x 12.1 cm),"Bequest of Mary Stillman Harkness, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436240,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.79,false,true,436868,European Paintings,Painting,Lucia,,,,,,Artist,,"Frederic, Lord Leighton","British, Scarborough 1830–1896 London",,"Leighton, Frederic, Lord",British,1830,1896,,1850,1896,Oil on canvas,14 7/8 x 10 in. (37.8 x 25.4 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436868,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +69.104,false,true,435993,European Paintings,Painting,"Marianne Dorothy Harland (1759–1785), Later Mrs. William Dalrymple",,,,,,Artist,,Richard Cosway,"British, Oakford, Devon 1742–1821 London",,"Cosway, Richard",British,1742,1821,,1762,1821,Oil on canvas,28 x 36 1/8 in. (71.1 x 91.8 cm),"Gift of Mrs. William M. Haupt, from the collection of Mrs. James B. Haggin, 1969",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435993,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.7.56,false,true,436239,European Paintings,Painting,"Anne Elizabeth Cholmley (1769–1788), Later Lady Mulgrave",,,,,,Artist,,Gainsborough Dupont,"British, Sudbury, Suffolk 1754–1797 London",,"Dupont, Gainsborough",British,1754,1797,,1774,1797,Oil on wood,Overall 7 1/8 x 5 3/4 in. (18.1 x 14.6 cm); painted surface 6 x 4 3/4 in. (15.2 x 12.1 cm),"The Jules Bache Collection, 1949",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436239,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +56.54.1,false,true,437660,European Paintings,Painting,Portrait of a Horseman,,,,,,Artist,,James Seymour,"British, London ca. 1702–1752 Southwark (London)",,"Seymour, James",British,1697,1752,,1748,1748,Oil on canvas,37 x 51 5/8 in. (94 x 131.1 cm),"Gift of the children of the late Otto H. and Addie W. Kahn (Lady Maud E. Marriott, Mrs. Margaret D. Ryan, Roger W. Kahn, and Gilbert W. Kahn), 1956",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437660,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.264.2,false,true,435630,European Paintings,Painting,Edward Miles (1752–1828),,,,,,Artist,,Sir William Beechey,"British, Burford, Oxfordshire 1753–1839 Hampstead",,"Beechey, William, Sir",British,1753,1839,,1785,1785,Oil on canvas,11 7/8 x 9 7/8 in. (30.2 x 25.1 cm),"Gift of Heathcote Art Foundation, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435630,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1986.264.3,false,true,435631,European Paintings,Painting,"George IV (1762–1830), When Prince of Wales",,,,,,Artist,,Sir William Beechey,"British, Burford, Oxfordshire 1753–1839 Hampstead",and Workshop,"Beechey, William, Sir",British,1753,1839,,1773,1839,Oil on canvas,56 1/4 x 44 1/2 in. (142.9 x 113 cm),"Gift of Heathcote Art Foundation, 1986",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435631,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.169,false,true,437499,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,George Romney,"British, Beckside, Lancashire 1734–1802 Kendal, Cumbria",,"Romney, George",British,1734,1802,,1754,1802,Oil on canvas,30 x 24 3/4 in. (76.2 x 62.9 cm),"Gift of Mr. and Mrs. Edwin C. Vogel, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437499,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +53.220,false,true,437505,European Paintings,Painting,Admiral Sir Chaloner Ogle (1726–1816),,,,,,Artist,,George Romney,"British, Beckside, Lancashire 1734–1802 Kendal, Cumbria",,"Romney, George",British,1734,1802,,1754,1802,Oil on canvas,30 x 24 5/8 in. (76.2 x 62.5 cm),"Gift of Lennen and Newell Inc., 1953",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437505,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +45.59.5,false,true,437502,European Paintings,Painting,"Mrs. Charles Frederick (Martha Rigden, died 1794)",,,,,,Artist,,George Romney,"British, Beckside, Lancashire 1734–1802 Kendal, Cumbria",,"Romney, George",British,1734,1802,,1754,1802,Oil on canvas,29 3/4 x 24 3/4 in. (75.6 x 62.9 cm),"Fletcher Fund, 1945",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1974.289.2,false,true,435775,European Paintings,Painting,Kynance,,,,,,Artist,,John Brett,"British, Bletchingly 1831–1902 London",,"Brett, John",British,1831,1902,1888,1888,1888,Oil on canvas,7 x 14 1/8 in. (17.8 x 35.9 cm),"Bequest of Theodore Rousseau Jr., 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435775,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1972.145.7,false,true,435726,European Paintings,"Painting, miniature",Count Alexander Ivanovich Sollogoub (1788–1844),,,,,,Artist,,Domenico Bossi,"Italian, Venetian, 1765–1853",,"Bossi, Domenico",Italian,1765,1853,1810,1810,1810,Ivory,"Oval, 2 3/8 x 1 7/8 in. (60 x 47 mm)","Gift of Humanities Fund Inc., 1972",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435726,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1992.104,false,true,436977,European Paintings,"Painting, miniature",Napoléon I (1769–1821) on Horseback,,,,,,Artist,,Luigi Marta,"Italian, Neapolitan, 1790–1858",,"Marta, Luigi",Italian,1790,1858,1830,1830,1830,Ivory,5 3/4 x 7 1/2 in. (146 x 191 mm),"Gift of Gloria Zicht, 1992",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/436977,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.360,false,true,439272,European Paintings,"Painting, drawing",Study of a Boy in a Blue Jacket,,,,,,Artist,,Benedetto Luti,"Italian, Florence 1666–1724 Rome",,"Luti, Benedetto",Italian,1666,1724,1717,1717,1717,"Pastel and chalk on blue laid paper, laid down on paste paper",16 x 13 in. (40.6 x 33 cm),"Gwynne Andrews Fund, 2007",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/439272,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2007.361,false,true,439273,European Paintings,"Painting, drawing",Study of a Girl in Red,,,,,,Artist,,Benedetto Luti,"Italian, Florence 1666–1724 Rome",,"Luti, Benedetto",Italian,1666,1724,1717,1717,1717,"Pastel and chalk on blue laid paper, laid down on paste paper",16 1/2 x 13 3/8 in. (41.9 x 34 cm),"Gwynne Andrews Fund, 2007",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/439273,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +49.122.2,false,true,435854,European Paintings,"Painting, miniature",Portrait of a Man,,,,,,Artist,,Rosalba Carriera,"Italian, Venice 1673–1757 Venice",,"Carriera, Rosalba",Italian,1673,1757,ca. 1710,1705,1715,Ivory,"Oval, 3 x 2 1/4 in. (76 x 59 mm)","Rogers Fund, 1949",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/435854,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.22,false,true,438544,European Paintings,"Painting, drawing","Gustavus Hamilton (1710–1746), Second Viscount Boyne, in Masquerade Costume",,,,,,Artist,,Rosalba Carriera,"Italian, Venice 1673–1757 Venice",,"Carriera, Rosalba",Italian,1673,1757,1730–31,1730,1731,"Pastel on paper, laid down on canvas",22 1/4 x 16 7/8 in. (56.5 x 42.9 cm),"Purchase, George Delacorte Fund Gift, in memory of George T. Delacorte Jr., and Gwynne Andrews, Victor Wilbour Memorial, and Marquand Funds, 2002",,,,,,,,,,,,Pastels & Oil Sketches on Paper,,http://www.metmuseum.org/art/collection/search/438544,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1999.287,false,true,438393,European Paintings,"Painting, miniature",Pope Pius VII (1742–1823),,,,,,Artist,Attributed to,Bianca Boni,"Italian, Florentine, active early 19th century",,"Boni, Bianca",Italian,1800,1829,ca. 1820,1815,1825,Watercolor on ivory,Diameter 2 3/4 in. (70 mm),"Bequest of Francesca Rospigliosi, 1998",,,,,,,,,,,,Miniatures,,http://www.metmuseum.org/art/collection/search/438393,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +11.126.2,false,true,436032,European Paintings,"Painting, cassone panel",The Legend of Cloelia,,,,,,Artist,,Guidoccio di Giovanni Cozzarelli,"Italian, Sienese, 1450–1516",,"Cozzarelli, Guidoccio di Giovanni",Italian,1450,1516,ca. 1480,1475,1485,Tempera and gold on wood,Overall 17 3/4 x 45 1/2 in. (45.1 x 115.6 cm); painted surface 15 1/8 x 43 1/4 in. (38.4 x 109.9 cm),"Frederick C. Hewitt Fund, 1911",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436032,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2010.289,false,true,441024,European Paintings,Painting,Head of an Old Woman,,,,,,Artist,,Orazio Borgianni,"Italian, Rome 1578–1616 Rome",,"Borgianni, Orazio",Italian,1578,1616,after 1610,1611,1616,Oil on canvas,20 7/8 x 15 3/8 in. (53 x 39 cm),"Purchase, Gwynne Andrews Fund and Marco Voena and Luigi Koelliker Gift, 2010",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441024,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.543,false,true,442183,European Paintings,Painting,A Cardinal's Procession,,,,,,Artist,,Ottavio Leoni (Il Padovano),"Italian, Rome 1578–1630 Rome",,"Leoni, Ottavio (Il Padovano)",Italian,1578,1630,1621,1621,1621,Oil on copper,15 1/2 x 14 3/4 in. (39.4 x 37.5 cm),"Gift of Damon Mezzacappa, 2012",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/442183,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.277,false,true,643540,European Paintings,Painting,Portrait of a Woman,,,,,,Artist,,Giovanni Battista Gaulli (Il Baciccio),"Italian, Genoa 1639–1709 Rome",,"Gaulli, Giovanni Battista",Italian,1639,1709,ca. 1670s,1670,1679,Oil on canvas,28 5/8 × 23 1/4 in. (72.7 × 59.1 cm),"Gift of Álvaro Saieh Bendeck, Jean-Luc Baroni, and Fabrizio Moretti, in honor of Keith Christiansen, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/643540,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.75,false,true,437784,European Paintings,Painting,Alexander the Great Rescued from the River Cydnus,,,,,,Artist,,Pietro Testa,"Italian, Lucca 1612–1650 Rome",,"Testa, Pietro",Italian,1612,1650,ca. 1650,1645,1655,Oil on canvas,38 x 54 in. (96.5 x 137.2 cm),"Gift of Eula M. Ganz, in memory of Paul H. Ganz, 1987",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437784,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +03.37.1,false,true,435623,European Paintings,Painting,Portrait of a Young Man,,,,,,Artist,,Pompeo Batoni,"Italian, Lucca 1708–1787 Rome",,"Batoni, Pompeo",Italian,1708,1787,ca. 1760–65,1760,1765,Oil on canvas,97 1/8 x 69 1/4 in. (246.7 x 175.9 cm),"Rogers Fund, 1903",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435623,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.438,false,true,435622,European Paintings,Painting,Diana and Cupid,,,,,,Artist,,Pompeo Batoni,"Italian, Lucca 1708–1787 Rome",,"Batoni, Pompeo",Italian,1708,1787,1761,1761,1761,Oil on canvas,49 x 68 in. (124.5 x 172.7 cm),"Purchase, The Charles Engelhard Foundation, Robert Lehman Foundation Inc., Mrs. Haebler Frantz, April R. Axton, L. H. P. Klotz, and David Mortimer Gifts; and Gifts of Mr. and Mrs. Charles Wrightsman, George Blumenthal, and J. Pierpont Morgan, Bequests of Millie Bruhl Fredrick and Mary Clark Thompson, and Rogers Fund, by exchange, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435622,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.93,false,true,437607,European Paintings,Painting,Paradise,,,,,,Artist,,Carlo Saraceni,"Italian, Venetian, 1579?–1620",,"Saraceni, Carlo",Italian,1579,1620,ca. 1598,1593,1603,Oil on copper,Overall 21 3/8 x 18 7/8 in. (54.3 x 47.9 cm); painted surface 20 7/8 x 18 3/8 in. (53 x 46.7 cm),"Theodore M. Davis Collection, Bequest of Theodore M. Davis, by exchange, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437607,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.212,false,true,436908,European Paintings,Painting,Saint Catherine of Alexandria,,,,,,Artist,,Pietro Lorenzetti,"Italian, active Siena 1320–44",,"Lorenzetti, Pietro",Italian,1320,1344,shortly after 1342,1342,1344,"Tempera on wood, gold ground",Overall 26 x 16 1/4 in. (66 x 41.3 cm); painted surface 24 1/2 x 16 1/4 in. (62.2 x 41.3 cm),"Rogers Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436908,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2002.436,true,true,438605,European Paintings,Painting,The Crucifixion,,,,,,Artist,,Pietro Lorenzetti,"Italian, active Siena 1320–44",,"Lorenzetti, Pietro",Italian,1320,1344,1340s,1340,1344,Tempera and gold leaf on wood,Overall 16 1/2 x 12 1/2 in. (41.9 x 31.8 cm); painted surface 14 1/8 x 10 1/8 in. (35.9 x 25.7 cm),"Purchase, Lila Acheson Wallace Gift and Gwynne Andrews Fund, 2002",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +92.1.62,false,true,436943,European Paintings,Painting,A Circus Boy,,,,,,Artist,,Antonio Mancini,"Italian, Albano 1852–1930 Rome",,"Mancini, Antonio",Italian,1852,1930,1872,1872,1872,Oil on canvas,59 5/8 x 28 1/2 in. (151.4 x 72.4 cm),"Bequest of Elizabeth U. Coles, in memory of her son, William F. Coles, 1892",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436943,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.191,false,true,436935,European Paintings,Painting,The Tame Magpie,,,,,,Artist,,Alessandro Magnasco,"Italian, Genoa 1667–1749 Genoa",,"Magnasco, Alessandro",Italian,1667,1749,ca. 1707–8,1707,1708,Oil on canvas,25 x 29 1/2 in. (63.5 x 74.9 cm),"Purchase, Katherine D. W. Glover Gift, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436935,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.100.13,false,true,435659,European Paintings,Painting,Madonna and Child with Angels,,,,,,Artist,,Bernardino da Genoa,"Italian, Genoese, active in 1515",,Bernardino da Genoa,Italian,1515,1515,1515,1515,1515,Oil on wood,29 3/8 x 22 5/8 in. (74.6 x 57.5 cm),"Gift of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435659,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.165,false,true,435833,European Paintings,Painting,Herodias,,,,,,Artist,,Francesco Cairo,"Italian, Milan 1607–1665 Milan",,"Cairo, Francesco",Italian,1607,1665,before 1635,1627,1635,Oil on canvas,29 5/8 x 24 5/8 in. (75.2 x 62.5 cm),"Gift of Paul Ganz, in memory of Rudolf Wittkower, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435833,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2012.100.2,false,true,441230,European Paintings,Painting,A Female Martyr Saint,,,,,,Artist,,Carlo Francesco Nuvolone,"Italian, Milan 1609–1662 Milan",,"Nuvolone, Carlo Francesco",Italian,1609,1662,ca. 1650,1645,1655,Oil on wood,20 x 16 3/8 in. (50.8 x 41.6 cm),"Bequest of Anna Mont, in memory of Frederick Mont, 2010",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441230,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +30.15,false,true,435864,European Paintings,Painting,A Woman with a Dog,,,,,,Artist,,Giacomo Ceruti,"Italian, Milan 1698–1767 Milan",,"Ceruti, Giacomo",Italian,1698,1767,1740s,1740,1749,Oil on canvas,38 x 28 1/2 in. (96.5 x 72.4 cm),"Maria DeWitt Jesup Fund, 1930",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435864,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.40,false,true,437745,European Paintings,Painting,Judith with the Head of Holofernes,,,,,,Artist,,Massimo Stanzione,"Italian, Neapolitan, 1585–1656",,"Stanzione, Massimo",Italian,1585,1656,ca. 1640,1635,1645,Oil on canvas,78 1/2 x 57 1/2 in. (199.4 x 146.1 cm),"Gift of Edward W. Carter, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437745,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.17,false,true,437376,European Paintings,Painting,A Cat Stealing Fish,,,,,,Artist,,Giuseppe Recco,"Italian, Neapolitan, 1634–1695",,"Recco, Giuseppe",Italian,1634,1695,late 1660s,1667,1669,Oil on canvas,38 x 50 1/2 in. (96.5 x 128.3 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437376,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.1046,false,true,436512,European Paintings,"Painting, predella panel",Paradise,,,,,,Artist,,Giovanni di Paolo (Giovanni di Paolo di Grazia),"Italian, Siena 1398–1482 Siena",,Giovanni di Paolo (Giovanni di Paolo di Grazia),Italian,1398,1482,1445,1445,1445,"Tempera and gold on canvas, transferred from wood",Overall 18 1/2 x 16 in. (47 x 40.6 cm); painted surface 17 1/2 x 15 1/8 in. (44.5 x 38.4 cm),"Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436512,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.100.4,false,true,436513,European Paintings,"Painting, predella panel",The Presentation of Christ in the Temple,,,,,,Artist,,Giovanni di Paolo (Giovanni di Paolo di Grazia),"Italian, Siena 1398–1482 Siena",,Giovanni di Paolo (Giovanni di Paolo di Grazia),Italian,1398,1482,ca. 1435,1430,1440,Tempera and gold on wood,Overall 15 1/2 x 18 1/8 in. (39.4 x 46 cm); painted surface 15 1/4 x 17 1/4 in. (38.7 x 43.8 cm),"Gift of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436513,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +88.3.111,false,true,436515,European Paintings,Painting,Saints Matthew and Francis,,,,,,Artist,,Giovanni di Paolo (Giovanni di Paolo di Grazia),"Italian, Siena 1398–1482 Siena",,Giovanni di Paolo (Giovanni di Paolo di Grazia),Italian,1398,1482,ca. 1435,1430,1440,"Tempera on wood, gold ground","Overall, with added strips, 54 5/8 x 34 3/4 in. (138.7 x 88.3 cm); painted surface 52 7/8 x 33 1/2 in. (134.3 x 85.1 cm)","Gift of Coudert Brothers, 1888",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436515,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.4,false,true,436509,European Paintings,Painting,The Adoration of the Magi,,,,,,Artist,,Giovanni di Paolo (Giovanni di Paolo di Grazia),"Italian, Siena 1398–1482 Siena",,Giovanni di Paolo (Giovanni di Paolo di Grazia),Italian,1398,1482,ca. 1460,1455,1465,Tempera and gold on wood,10 5/8 x 9 1/8 in. (27 x 23.2 cm),"The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436509,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.76,false,true,436508,European Paintings,"Painting, polyptych",Madonna and Child with Saints,,,,,,Artist,,Giovanni di Paolo (Giovanni di Paolo di Grazia),"Italian, Siena 1398–1482 Siena",,Giovanni di Paolo (Giovanni di Paolo di Grazia),Italian,1398,1482,1454,1454,1454,"Tempera on wood, gold ground","Central panel 82 3/4 x 25 7/8 in. (210.2 x 65.7 cm); left panels 70 7/8 x 16 7/8 in. (180 x 42.9 cm), 70 7/8 x 16 3/4 in. (180 x 42.5 cm); right panels 70 7/8 x 16 7/8 in. (180 x 42.9 cm), 70 7/8 x 16 3/4 in. (180 x 42.5 cm)","The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436508,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.190.16,false,true,436510,European Paintings,Painting,Madonna and Child with Two Angels and a Donor,,,,,,Artist,,Giovanni di Paolo (Giovanni di Paolo di Grazia),"Italian, Siena 1398–1482 Siena",,Giovanni di Paolo (Giovanni di Paolo di Grazia),Italian,1398,1482,ca. 1445,1440,1450,"Tempera on wood, gold ground (partly checkered with modern red glazes)",Shaped top: overall 57 1/8 x 32 in. (145.1 x 81.3 cm); painted surface 54 1/4 x 32 in. (137.8 x 81.3 cm),"Bequest of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436510,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +32.100.83a–d,false,true,436514,European Paintings,"Painting, pilasters of an altarpiece","Saints Catherine of Alexandria, Barbara, Agatha, and Margaret",,,,,,Artist,,Giovanni di Paolo (Giovanni di Paolo di Grazia),"Italian, Siena 1398–1482 Siena",,Giovanni di Paolo (Giovanni di Paolo di Grazia),Italian,1398,1482,ca. 1470,1465,1475,"Tempera on wood, gold ground","(a) overall 18 3/4 x 6 in. (47.6 x 15.2 cm), painted surface 18 1/4 x 5 1/2 in. (46.4 x 14 cm); (b) overall 18 3/4 x 6 in. (47.6 x 15.2 cm), painted surface 18 3/8 x 5 5/8 in. (46.7 x 14.3 cm); (c) overall 18 3/4 x 6 in. (47.6 x 15.2 cm), painted surface 18 3/8 x 5 3/8 in. (46.7 x 13.7 cm); (d) overall 18 3/4 x 6 in. (47.6 x 15.2 cm), painted surface 18 1/4 x 5 5/8 in. (46.4 x 14.3 cm)","The Friedsam Collection, Bequest of Michael Friedsam, 1931",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436514,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.189.1,false,true,437606,European Paintings,"Painting, predella panel",The Massacre of the Innocents,,,,,,Artist,,Sano di Pietro (Ansano di Pietro di Mencio),"Italian, Siena 1405–1481 Siena",,Sano di Pietro (Ansano di Pietro di Mencio),Italian,1405,1481,ca. 1470,1465,1475,Tempera on wood,11 7/8 x 17 3/8 in. (30.2 x 44.1 cm),"Gift of Irma N. Straus, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437606,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.189.2,false,true,437602,European Paintings,"Painting, predella panel",The Adoration of the Magi,,,,,,Artist,,Sano di Pietro (Ansano di Pietro di Mencio),"Italian, Siena 1405–1481 Siena",,Sano di Pietro (Ansano di Pietro di Mencio),Italian,1405,1481,ca. 1470,1465,1475,Tempera and gold on wood,11 7/8 x 18 3/4 in. (30.2 x 47.6 cm),"Gift of Irma N. Straus, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437602,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +64.189.4,false,true,437605,European Paintings,"Painting, portable triptych",Madonna and Child; Saint John the Baptist; Saint Jerome,,,,,,Artist,,Sano di Pietro (Ansano di Pietro di Mencio),"Italian, Siena 1405–1481 Siena",,Sano di Pietro (Ansano di Pietro di Mencio),Italian,1405,1481,ca. 1450–55,1450,1455,"Tempera on wood, gold ground","Central panel, overall, with engaged frame, 17 3/8 x 12 5/8 in. (44.1 x 32.1 cm), painted surface 14 3/4 x 10 1/8 in. (37.5 x 25.7 cm); each wing, overall, with engaged frame, 17 3/8 x 6 1/4 in. (44.1x 15.9 cm), painted surface 15 1/2 x 4 5/8 in. (39.4 x 11.7 cm)","Gift of Irma N. Straus, 1964",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437605,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.181.7,false,true,437603,European Paintings,"Painting, predella panel",The Burial of Saint Martha,,,,,,Artist,,Sano di Pietro (Ansano di Pietro di Mencio),"Italian, Siena 1405–1481 Siena",,Sano di Pietro (Ansano di Pietro di Mencio),Italian,1405,1481,ca. 1460–70,1455,1465,Tempera and gold on wood,5 1/2 x 11 1/2 in. (14 x 29.2 cm),"Bequest of Adele L. Lehman, in memory of Arthur Lehman, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1987.290.2ab,false,true,437604,European Paintings,"Painting, portable altarpiece","Madonna and Child with the Dead Christ, Saints Agnes and Catherine of Alexandria, and Two Angels",,,,,,Artist,,Sano di Pietro (Ansano di Pietro di Mencio),"Italian, Siena 1405–1481 Siena",,Sano di Pietro (Ansano di Pietro di Mencio),Italian,1405,1481,ca. 1470–80,1465,1475,"Tempera on wood, gold ground","Main panel, overall, with engaged (modern) frame, 12 3/4 x 11 3/4 in. (32.4 x 29.8 cm), painted surface 10 7/8 x 9 7/8 in. (27.6 x 25.1 cm); predella, overall, with engaged (modern) frame, 3 5/8 x 12 5/8 in. (9.2 x 32.1 cm), painted surface 2 5/8 x 11 7/8 in. (6.7 x 30.2 cm)","Anonymous Bequest, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +20.182,false,true,436330,European Paintings,"Painting, fragment of a cassone panel",Goddess of Chaste Love,,,,,,Artist,,Francesco di Giorgio Martini,"Italian, Siena 1439–1501 Siena",,Francesco di Giorgio Martini,Italian,1439,1501,1468–75,1468,1475,Tempera and gold on wood,15 1/2 x 17 1/4 in. (39.4 x 43.8 cm),"Marquand Fund, 1920",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436330,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1996.341,false,true,438029,European Paintings,"Painting, lunette",The Man of Sorrows with Two Angels,,,,,,Artist,Workshop of,Francesco di Giorgio Martini,"Italian, Siena 1439–1501 Siena",,Francesco di Giorgio Martini,Italian,1439,1501,ca. 1470,1465,1475,Tempera on wood,Frame 53 3/4 x 32 5/8 in. (136.5 x 82.9 cm); painted surface 6 7/8 x 18 1/4 in. (17.5 x 46.4 cm),"Anonymous Gift, in memory of Kurt Cassirer, 1996",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438029,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +61.43,false,true,437195,European Paintings,Painting,Madonna and Child with Saints Jerome and Mary Magdalen,,,,,,Artist,,Neroccio de' Landi,"Italian, Siena 1447–1500 Siena",,Neroccio de' Landi,Italian,1447,1500,ca. 1490,1485,1495,Tempera on wood,24 x 17 1/4 in. (61 x 43.8 cm),"Gift of Samuel H. Kress Foundation, by exchange, 1961",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437195,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.60.61,false,true,437291,European Paintings,Painting,Processional Crucifix,,,,,,Artist,,Pietro di Francesco Orioli,"Italian, Siena 1458–1496 Siena",,Pietro di Francesco Orioli,Italian,1458,1496,ca. 1480s,1478,1496,"Tempera on wood, gold ground",Overall 21 1/4 x 18 1/2 in. (54 x 47 cm); painted surface 18 5/8 x 14 in. (47.3 x 35.6 cm),"The Bequest of Michael Dreicer, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437291,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +16.30ab,true,true,437372,European Paintings,"Painting, altarpiece",Madonna and Child Enthroned with Saints,,,,,,Artist,,Raphael (Raffaello Sanzio or Santi),"Italian, Urbino 1483–1520 Rome",,Raphael (Raffaello Sanzio or Santi),Italian,1483,1520,ca. 1504,1499,1509,Oil and gold on wood,"Main panel, overall 67 7/8 x 67 7/8 in. (172.4 x 172.4 cm), painted surface 66 3/4 x 66 1/2 in. (169.5 x 168.9 cm); lunette, overall 29 1/2 x 70 7/8 in. (74.9 x 180 cm), painted surface 25 1/2 x 67 1/2 in. (64.8 x 171.5 cm)","Gift of J. Pierpont Morgan, 1916",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437372,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +06.180,false,true,436498,European Paintings,Painting,The Man of Sorrows,,,,,,Artist,,Michele Giambono (Michele Giovanni Bono),"Italian, active Venice 1420–62",,"Giambono, Michele (Michele Giovanni Bono)",Italian,1420,1462,ca. 1430,1425,1435,Tempera and gold on wood,"Overall, with engaged frame, 21 5/8 x 15 1/4 in. (54.9 x 38.7 cm); painted surface 18 1/2 x 12 1/4 in. (47 x 31.1 cm)","Rogers Fund, 1906",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436498,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +50.229.1,false,true,437912,European Paintings,Painting,The Death of the Virgin,,,,,,Artist,,Bartolomeo Vivarini,"Italian, active Venice 1450–91",,"Vivarini, Bartolomeo",Italian,1450,1491,1485,1485,1485,Tempera on wood,"Arched top, 74 3/4 x 59 in. (189.9 x 149.9 cm)","Gift of Robert Lehman, 1950",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437912,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +65.181.1,false,true,437913,European Paintings,Painting fragment,A Saint (Mark?) Reading,,,,,,Artist,,Bartolomeo Vivarini,"Italian, active Venice 1450–91",,"Vivarini, Bartolomeo",Italian,1450,1491,ca. 1470,1465,1475,"Tempera on wood, gold ground",18 5/8 x 14 3/4 in. (47.3 x 37.5 cm),"Bequest of Adele L. Lehman, in memory of Arthur Lehman, 1965",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437913,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1971.155,true,true,435853,European Paintings,Painting,The Coronation of the Virgin,,,,,,Artist,,Annibale Carracci,"Italian, Bologna 1560–1609 Rome",,"Carracci, Annibale",Italian,1560,1609,after 1595,1595,1609,Oil on canvas,46 3/8 x 55 5/8 in. (117.8 x 141.3 cm),"Purchase, Bequest of Miss Adelaide Milton de Groot (1876–1967), by exchange, and Dr. and Mrs. Manuel Porter and sons Gift, in honor of Mrs. Sarah Porter, 1971",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435853,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1998.188,false,true,438338,European Paintings,Painting,The Burial of Christ,,,,,,Artist,,Annibale Carracci,"Italian, Bologna 1560–1609 Rome",,"Carracci, Annibale",Italian,1560,1609,1595,1595,1595,Oil on copper,17 1/4 x 13 3/4 in. (43.8 x 34.9 cm),"Purchase, Edwin L. Weisl Jr. Gift, 1998",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2009.252,false,true,438813,European Paintings,Painting,Saint John the Baptist Bearing Witness,,,,,,Artist,,Annibale Carracci,"Italian, Bologna 1560–1609 Rome",,"Carracci, Annibale",Italian,1560,1609,ca. 1600,1595,1605,Oil on copper,21 3/8 x 17 1/8 in. (54.3 x 43.5 cm),"Gift of Fabrizio Moretti and Adam Williams, in honor of Everett Fahy, 2009",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/438813,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +57.23,false,true,437763,European Paintings,Painting,Tobias Curing His Father's Blindness,,,,,,Artist,,Bernardo Strozzi,"Italian, Genoa 1581–1644 Venice",,"Strozzi, Bernardo",Italian,1581,1644,1630–35,1630,1635,Oil on canvas,57 1/2 x 88 in. (146.1 x 223.5 cm),"Purchase, Mary Wetmore Shively Bequest, in memory of her husband, Henry L. Shively, M.D., 1957",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437763,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2014.270,false,true,647338,European Paintings,Painting,Saint Francis in Ecstasy,,,,,,Artist,,Giovanni Benedetto Castiglione (Il Grechetto),"Italian, Genoa 1609–1664 Mantua",,"Castiglione, Giovanni Benedetto (Il Grechetto)",Italian,1609,1664,ca. 1650,1645,1655,Oil on canvas,77 × 53 1/4 in. (195.6 × 135.3 cm),"Purchase, Lila Acheson Wallace Gift; Gwynne Andrews Fund; and Gift in memory of Felix M. Warburg from his wife and children, Bequest and Gift of George Blumenthal, Bequests of Theodore M. Davis, Adele L. Lehman, in memory of Arthur Lehman, Helen Hay Whitney, Jean Fowles, in memory of her first husband, R. Langton Douglas, and Gifts of Coudert Brothers and Harry Payne Bingham Jr., by exchange, 2014",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/647338,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1982.60.1,false,true,437243,European Paintings,Painting,Saint Romuald,,,,,,Artist,,Guido Palmeruccio (Guiduccio Palmerucci),"Italian, Gubbio, active 1315–49",,"Palmeruccio, Guido (Guiduccio Palmerucci)",Italian,1315,1349,possibly 1320s,1315,1349,"Tempera on wood, gold ground","Overall, with engaged frame, 18 1/8 x 10 3/4 in. (46 x 27.3 cm)","The Jack and Belle Linsky Collection, 1982",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437243,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +22.60.59,false,true,435616,European Paintings,Painting,Saint Dominic Resuscitating Napoleone Orsini,,,,,,Artist,,Bartolomeo degli Erri,"Italian, Modena, active 1460–79",,Bartolomeo degli Erri,Italian,1460,1479,1467–74,1467,1474,"Tempera on canvas, transferred from wood",14 x 17 1/2 in. (35.6 x 44.5 cm),"The Bequest of Michael Dreicer, 1921",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435616,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.311.3,false,true,436604,European Paintings,Painting,The Vocation of Saint Aloysius (Luigi) Gonzaga,,,,,,Artist,,Guercino (Giovanni Francesco Barbieri),"Italian, Cento 1591–1666 Bologna",,Guercino (Giovanni Francesco Barbieri),Italian,1591,1666,ca. 1650,1645,1655,Oil on canvas,140 x 106 in. (355.6 x 269.2 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436604,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.459.2,true,true,436603,European Paintings,Painting,Samson Captured by the Philistines,,,,,,Artist,,Guercino (Giovanni Francesco Barbieri),"Italian, Cento 1591–1666 Bologna",,Guercino (Giovanni Francesco Barbieri),Italian,1591,1666,1619,1619,1619,Oil on canvas,75 1/4 x 93 1/4 in. (191.1 x 236.9 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436603,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +47.71,false,true,435693,European Paintings,Painting,"Consuelo Vanderbilt (1876–1964), Duchess of Marlborough, and Her Son, Lord Ivor Spencer-Churchill (1898–1956)",,,,,,Artist,,Giovanni Boldini,"Italian, Ferrara 1842–1931 Paris",,"Boldini, Giovanni",Italian,1842,1931,1906,1906,1906,Oil on canvas,87 1/4 x 67 in. (221.6 x 170.2 cm),"Gift of Consuelo Vanderbilt Balsan, 1946",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435693,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +59.78,false,true,435692,European Paintings,Painting,"Mrs. Charles Warren-Cram (Ella Brooks Carter, 1846–1896)",,,,,,Artist,,Giovanni Boldini,"Italian, Ferrara 1842–1931 Paris",,"Boldini, Giovanni",Italian,1842,1931,1885,1885,1885,Oil on canvas,19 3/8 x 14 in. (49.2 x 35.6 cm),"Gift of Mrs. Edward C. Moën, 1959",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435692,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +87.15.81,false,true,435691,European Paintings,Painting,Gossip,,,,,,Artist,,Giovanni Boldini,"Italian, Ferrara 1842–1931 Paris",,"Boldini, Giovanni",Italian,1842,1931,1873,1873,1873,Oil on wood,7 x 9 1/2 in. (17.8 x 24.1 cm),"Catharine Lorillard Wolfe Collection, Bequest of Catharine Lorillard Wolfe, 1887",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435691,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +08.136.12,false,true,435694,European Paintings,Painting,The Dispatch-Bearer,,,,,,Artist,,Giovanni Boldini,"Italian, Ferrara 1842–1931 Paris",,"Boldini, Giovanni",Italian,1842,1931,?1879,1865,1931,Oil on wood,16 3/4 x 13 1/2 in. (42.5 x 34.3 cm),"Bequest of Martha T. Fiske Collord, in memory of her first husband, Josiah M. Fiske, 1908",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435694,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +2011.26,false,true,441227,European Paintings,Painting,The Holy Family with the Infant Saint John the Baptist,,,,,,Artist,,Perino del Vaga (Pietro Buonaccorsi),"Italian, Florence 1501–1547 Rome",,Perino del Vaga (Pietro Buonaccorsi),Italian,1501,1547,ca. 1524–26,1524,1526,Oil on wood,34 3/4 x 25 5/8 in. (88.3 x 65.1 cm),"Purchase, Acquisitions Fund, Mr. and Mrs. Mark Fisch, Denise and Andrew Saul, and Friends of European Paintings Gifts, Gwynne Andrews Fund, Mr. and Mrs. J. Tomilson Hill, Jon and Barbara Landau, Charles and Jessie Price, Hester Diamond, and Fern and George Wachter Gifts, 2011",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/441227,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1989.225,false,true,436891,European Paintings,Painting,Tobit Burying the Dead,,,,,,Artist,,Andrea di Lione,"Italian, Naples 1610–1685 Naples",,"Lione, Andrea di",Italian,1610,1685,1640s,1640,1649,Oil on canvas,50 1/4 x 68 1/2 in. (127.6 x 174 cm),"Gwynne Andrews Fund, 1989",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436891,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1973.311.2,false,true,436502,European Paintings,Painting,The Annunciation,,,,,,Artist,,Luca Giordano,"Italian, Naples 1634–1705 Naples",,"Giordano, Luca",Italian,1634,1705,1672,1672,1672,Oil on canvas,93 1/8 x 66 7/8 in. (236.5 x 169.9 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1973",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436502,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.31,false,true,437246,European Paintings,Painting,"Interior of Saint Peter's, Rome",,,,,,Artist,,Giovanni Paolo Panini,"Italian, Piacenza 1691–1765 Rome",,"Panini, Giovanni Paolo",Italian,1691,1765,after 1754,1754,1765,Oil on canvas,29 1/8 x 39 1/4 in. (74 x 99.7 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437246,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.63.1,false,true,437244,European Paintings,Painting,Ancient Rome,,,,,,Artist,,Giovanni Paolo Panini,"Italian, Piacenza 1691–1765 Rome",,"Panini, Giovanni Paolo",Italian,1691,1765,1757,1757,1757,Oil on canvas,67 3/4 x 90 1/2 in. (172.1 x 229.9 cm),"Gwynne Andrews Fund, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437244,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +52.63.2,false,true,437245,European Paintings,Painting,Modern Rome,,,,,,Artist,,Giovanni Paolo Panini,"Italian, Piacenza 1691–1765 Rome",,"Panini, Giovanni Paolo",Italian,1691,1765,1757,1757,1757,Oil on canvas,67 3/4 x 91 3/4 in. (172.1 x 233 cm),"Gwynne Andrews Fund, 1952",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437245,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.100.34,false,true,436920,European Paintings,Painting,Madonna and Child,,,,,,Artist,,Luca di Tommè di Nuto,"Italian, Sienese, active 1356–89",,Luca di Tommè di Nuto,Italian,1356,1389,ca. 1360–65,1360,1365,"Tempera on wood, transferred from wood, gold ground","Shaped top, 52 7/8 x 23 1/8 in. (134.3 x 58.7 cm)","Gift of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/436920,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.100.35–37,false,true,437335,European Paintings,"Painting, triptych",Madonna and Child with Saints,,,,,,Artist,,Priamo della Quercia (Priamo del Pietro),"Italian, Sienese, active 1442–67",,Priamo della Quercia (Priamo del Pietro),Italian,1442,1467,ca. 1442,1442,1467,"Tempera on wood, gold ground",Central panel 43 1/4 x 22 1/2 in. (109.9 x 57.2 cm); left wing 45 1/2 x 22 in. (115.6 x 55.9 cm); right wing 45 1/4 x 22 1/4 in. (114.9 x 56.5 cm),"Gift of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437335,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +13.75,false,true,437821,European Paintings,Painting,The Miracle of the Loaves and Fishes,,,,,,Artist,,Jacopo Tintoretto (Jacopo Robusti),"Italian, Venice 1519–1594 Venice",,"Tintoretto, Jacopo (Jacopo Robusti)",Italian,1519,1594,ca. 1545–50,1545,1550,Oil on canvas,61 x 160 1/2 in. (154.9 x 407.7 cm),"Francis L. Leland Fund, 1913",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437821,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +58.49,false,true,437818,European Paintings,Painting,Portrait of a Young Man,,,,,,Artist,,Jacopo Tintoretto (Jacopo Robusti),"Italian, Venice 1519–1594 Venice",,"Tintoretto, Jacopo (Jacopo Robusti)",Italian,1519,1594,1551,1551,1551,Oil on canvas,54 1/2 x 42 in. (138.4 x 106.7 cm),"Gift of Lionel F. Straus Jr., in memory of his parents, Mr. and Mrs. Lionel F. Straus, 1958",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437818,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +10.206,false,true,437819,European Paintings,Painting,Doge Alvise Mocenigo (1507–1577) Presented to the Redeemer,,,,,,Artist,,Jacopo Tintoretto (Jacopo Robusti),"Italian, Venice 1519–1594 Venice",,"Tintoretto, Jacopo (Jacopo Robusti)",Italian,1519,1594,probably 1577,1577,1577,Oil on canvas,38 1/4 x 78 in. (97.2 x 198.1 cm),"John Stewart Kennedy Fund, 1910",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437819,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +41.100.12,false,true,437822,European Paintings,Painting,Portrait of a Man,,,,,,Artist,,Jacopo Tintoretto (Jacopo Robusti),"Italian, Venice 1519–1594 Venice",,"Tintoretto, Jacopo (Jacopo Robusti)",Italian,1519,1594,ca. 1540,1535,1545,Oil on canvas,44 3/8 x 35 in. (112.7 x 88.9 cm),"Gift of George Blumenthal, 1941",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437822,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.458,false,true,437268,European Paintings,Painting,Bacchus and Ariadne,,,,,,Artist,,Giovanni Antonio Pellegrini,"Italian, Venice 1675–1741 Venice",,"Pellegrini, Giovanni Antonio",Italian,1675,1741,1720s,1720,1729,Oil on canvas,46 x 50 1/2 in. (116.8 x 128.3 cm),"Gift of Mr. and Mrs. Eugene Victor Thaw, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437268,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1985.5,false,true,435573,European Paintings,Painting,Flora and Zephyr,,,,,,Artist,,Jacopo Amigoni,"Italian, Venice 1682–1752 Madrid",,"Amigoni, Jacopo",Italian,1682,1752,1730s,1730,1739,Oil on canvas,84 x 58 in. (213.4 x 147.3 cm),"Purchase, Rudolph and Lentilhon G. von Fluegge Foundation Inc. Gift, 1985",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/435573,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +67.187.90,false,true,437281,European Paintings,Painting,Saint Christopher Carrying the Infant Christ,,,,,,Artist,,Giovanni Battista Piazzetta,"Italian, Venice 1682–1754 Venice",,"Piazzetta, Giovanni Battista",Italian,1682,1754,1730s,1730,1739,Oil on canvas,28 1/4 x 22 1/8 in. (71.8 x 56.2 cm),"Bequest of Miss Adelaide Milton de Groot (1876–1967), 1967",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437281,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +23.128,false,true,437798,European Paintings,"Painting, ceiling decoration",The Glorification of the Barbaro Family,,,,,,Artist,,Giovanni Battista Tiepolo,"Italian, Venice 1696–1770 Madrid",,"Tiepolo, Giovanni Battista",Italian,1696,1770,ca. 1750,1745,1755,Oil on canvas,"Irregular oval, 96 x 183 3/4 in. (243.8 x 466.7 cm)","Anonymous Gift, in memory of Oliver H. Payne, 1923",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437798,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +71.121,false,true,437800,European Paintings,"Painting, sketch",The Investiture of Bishop Harold as Duke of Franconia,,,,,,Artist,,Giovanni Battista Tiepolo,"Italian, Venice 1696–1770 Madrid",,"Tiepolo, Giovanni Battista",Italian,1696,1770,ca. 1751–52,1751,1752,Oil on canvas,28 1/4 x 20 1/4 in. (71.8 x 51.4 cm),"Purchase, 1871",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437800,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1984.49,false,true,437796,European Paintings,"Painting, grisaille",A Female Allegorical Figure,,,,,,Artist,,Giovanni Battista Tiepolo,"Italian, Venice 1696–1770 Madrid",,"Tiepolo, Giovanni Battista",Italian,1696,1770,ca. 1740–50,1740,1750,"Oil on canvas, gold ground","Oval, 32 x 24 7/8 in. (81.3 x 63.2 cm)","Gift of Mr. and Mrs. Charles Wrightsman, 1984",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437796,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +1977.1.3,true,true,437790,European Paintings,"Painting, sketch",Allegory of the Planets and Continents,,,,,,Artist,,Giovanni Battista Tiepolo,"Italian, Venice 1696–1770 Madrid",,"Tiepolo, Giovanni Battista",Italian,1696,1770,1752,1752,1752,Oil on canvas,73 x 54 7/8 in. (185.4 x 139.4 cm),"Gift of Mr. and Mrs. Charles Wrightsman, 1977",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437790,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +37.165.2,false,true,437803,European Paintings,"Painting, sketch",Saint Thecla Praying for the Plague-Stricken,,,,,,Artist,,Giovanni Battista Tiepolo,"Italian, Venice 1696–1770 Madrid",,"Tiepolo, Giovanni Battista",Italian,1696,1770,1758–59,1758,1759,Oil on canvas,32 x 17 5/8 in. (81.3 x 44.8 cm),"Rogers Fund, 1937",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437803,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.85.12,false,true,437804,European Paintings,Painting,Virtue and Abundance,,,,,,Artist,,Giovanni Battista Tiepolo,"Italian, Venice 1696–1770 Madrid",and Workshop,"Tiepolo, Giovanni Battista",Italian,1696,1770,1760,1760,1760,"Fresco, transferred to canvas",Diameter 114 in. (289.6 cm),"Bequest of Grace Rainey Rogers, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437804,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" +43.85.21,false,true,437806,European Paintings,"Painting, grisaille",Prudence,,,,,,Artist,Workshop of,Giovanni Battista Tiepolo,"Italian, Venice 1696–1770 Madrid",,"Tiepolo, Giovanni Battista",Italian,1696,1770,1760,1760,1760,"Fresco, transferred to canvas","Oval, 49 1/8 x 36 1/4 in. (124.8 x 92.1 cm)","Bequest of Grace Rainey Rogers, 1943",,,,,,,,,,,,Paintings,,http://www.metmuseum.org/art/collection/search/437806,2017-02-06 08:00:16.000000 UTC,"Metropolitan Museum of Art, New York, NY" diff --git a/notebooks/remote_functions/remote_function_vertex_claude_model.ipynb b/notebooks/remote_functions/remote_function_vertex_claude_model.ipynb new file mode 100644 index 00000000000..650bb92e508 --- /dev/null +++ b/notebooks/remote_functions/remote_function_vertex_claude_model.ipynb @@ -0,0 +1,484 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Use BigQuery DataFrames to run Anthropic LLM at scale\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Overview\n", + "\n", + "Anthropic Claude models are available as APIs on Vertex AI ([docs](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude)).\n", + "\n", + "To run the Claude models at large scale data we can utilze the BigQuery\n", + "DataFrames remote functions ([docs](https://cloud.google.com/bigquery/docs/use-bigquery-dataframes#remote-functions)).\n", + "BigQuery DataFrames provides a simple pythonic interface `remote_function` to\n", + "deploy the user code as a BigQuery remote function and then invoke it at scale\n", + "by utilizing the parallel distributed computing architecture of BigQuery and\n", + "Google Cloud Function.\n", + "\n", + "In this notebook we showcase one such example. For the demonstration purpose we\n", + "use a small amount of data, but the example generalizes for large data. Check out\n", + "various IO APIs provided by BigQuery DataFrames [here](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.pandas#bigframes_pandas_read_gbq)\n", + "to see how you could create a DataFrame from your Big Data sitting in a BigQuery\n", + "table or GCS bucket." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Set Up" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set up a claude model in Vertex\n", + "\n", + "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install Anthropic with Vertex if needed\n", + "\n", + "Uncomment the following cell and run the cell to install anthropic python\n", + "package with vertex extension if you don't already have it." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install anthropic[vertex] --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define project and location for GCP integration" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "PROJECT = \"bigframes-dev\" # replace with your project\n", + "LOCATION = \"us-east5\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Initialize BigQuery DataFrames dataframe\n", + "\n", + "BigQuery DataFrames is a set of open source Python libraries that let you take\n", + "advantage of BigQuery data processing by using familiar Python APIs.\n", + "See for more details https://cloud.google.com/bigquery/docs/bigquery-dataframes-introduction." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Import BigQuery DataFrames pandas module and initialize it with your project\n", + "# and location\n", + "\n", + "import bigframes.pandas as bpd\n", + "bpd.options.bigquery.project = PROJECT\n", + "bpd.options.bigquery.location = LOCATION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's use a DataFrame with small amount of inline data for demo purpose.\n", + "You could create a DataFrame from your own data. See APIs like `read_gbq`,\n", + "`read_csv`, `read_json` etc. at https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.pandas." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job c4c27713-51c8-4293-8454-5c904df79318 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 0b1b71d8-8546-45f2-b403-707161fe4002 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
questions
0What is the capital of France?
1Explain the concept of photosynthesis in simpl...
2Write a haiku about artificial intelligence.
\n", + "

3 rows × 1 columns

\n", + "
[3 rows x 1 columns in total]" + ], + "text/plain": [ + " questions\n", + "0 What is the capital of France?\n", + "1 Explain the concept of photosynthesis in simpl...\n", + "2 Write a haiku about artificial intelligence.\n", + "\n", + "[3 rows x 1 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({\"questions\": [\n", + " \"What is the capital of France?\",\n", + " \"Explain the concept of photosynthesis in simple terms.\",\n", + " \"Write a haiku about artificial intelligence.\"\n", + " ]})\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Use BigQuery DataFrames `remote_function`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create a remote function from a custom python function that takes a prompt\n", + "and returns the output of the claude LLM running in Vertex. We will be using\n", + "`max_batching_rows=1` to control parallelization. This ensures that a single\n", + "prompt is processed per batch in the underlying cloud function so that the batch\n", + "processing does not time out. An ideal value for `max_batching_rows` depends on\n", + "the complexity of the prompts in the real use case and should be discovered\n", + "through offline experimentation. Check out the API for other ways to control\n", + "parallelization https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.pandas#bigframes_pandas_remote_function." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 488a116f-44b2-4ff7-9f95-bd36473dab0f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "@bpd.remote_function(packages=[\"anthropic[vertex]\"], max_batching_rows=1, \n", + " bigquery_connection=\"bigframes-dev.us-east5.bigframes-rf-conn\") # replace with your connection\n", + "def anthropic_transformer(message: str) -> str:\n", + " from anthropic import AnthropicVertex\n", + " client = AnthropicVertex(region=LOCATION, project_id=PROJECT)\n", + "\n", + " message = client.messages.create(\n", + " max_tokens=1024,\n", + " messages=[\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": message,\n", + " }\n", + " ],\n", + " model=\"claude-3-5-sonnet@20240620\",\n", + " )\n", + " content_text = message.content[0].text if message.content else \"\"\n", + " return content_text" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'bigframes-dev._b52b272a35b88e236e1f96fbe3f560c83a8fee85.bigframes_session265649_de1176dd4c57f40ba959503af3981682'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Print the BigQuery remote function created\n", + "anthropic_transformer.bigframes_remote_function" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'projects/bigframes-dev/locations/us-east5/functions/bigframes-session265649-de1176dd4c57f40ba959503af3981682'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Print the cloud function created\n", + "anthropic_transformer.bigframes_cloud_function" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 58b230a8-6536-4bac-ab02-dcf574692dd6 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 46d6a1e9-426a-4615-8eb5-98d34d08ec07 is DONE. 1.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job cf8fcbaa-b233-47cd-b4e3-60876b24879f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2620a544-d1df-4b30-bec4-4221e79ddf0f is DONE. 1.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
questionsanswers
0What is the capital of France?The capital of France is Paris.
1Explain the concept of photosynthesis in simpl...Photosynthesis is the process plants use to ma...
2Write a haiku about artificial intelligence.Here's a haiku about artificial intelligence:\n", + "...
\n", + "

3 rows × 2 columns

\n", + "
[3 rows x 2 columns in total]" + ], + "text/plain": [ + " questions \\\n", + "0 What is the capital of France? \n", + "1 Explain the concept of photosynthesis in simpl... \n", + "2 Write a haiku about artificial intelligence. \n", + "\n", + " answers \n", + "0 The capital of France is Paris. \n", + "1 Photosynthesis is the process plants use to ma... \n", + "2 Here's a haiku about artificial intelligence:\n", + "... \n", + "\n", + "[3 rows x 2 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Apply the remote function on the user data\n", + "df[\"answers\"] = df[\"questions\"].apply(anthropic_transformer)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 5f9a80126318726d23fac9ce685643647c1c5d5c Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 28 Aug 2024 16:08:17 -0700 Subject: [PATCH 787/921] refactor: Extract data loading logic into class (#913) --- bigframes/core/blocks.py | 3 +- bigframes/dataframe.py | 79 +-- .../functions/_remote_function_client.py | 9 +- bigframes/session/__init__.py | 624 +---------------- bigframes/session/_io/bigquery/__init__.py | 16 +- bigframes/session/executor.py | 60 +- bigframes/session/loader.py | 657 ++++++++++++++++++ bigframes/session/temp_storage.py | 10 +- tests/system/small/test_encryption.py | 2 +- tests/unit/session/test_session.py | 2 +- tests/unit/test_compute_options.py | 28 - 11 files changed, 780 insertions(+), 710 deletions(-) create mode 100644 bigframes/session/loader.py delete mode 100644 tests/unit/test_compute_options.py diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 9361543d5f2..d89d35516e5 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2542,7 +2542,8 @@ def _get_rows_as_json_values(self) -> Block: SELECT {select_columns_csv} FROM T1 """ # The only ways this code is used is through df.apply(axis=1) cope path - destination, query_job = self.session._query_to_destination( + # TODO: Stop using internal API + destination, query_job = self.session._loader._query_to_destination( json_sql, index_cols=[ordering_column_name], api_name="apply" ) if not destination: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 17dde7021b8..6b782b4692c 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2956,17 +2956,20 @@ def to_csv( if "*" not in path_or_buf: raise NotImplementedError(ERROR_IO_REQUIRES_WILDCARD) - result_table = self._run_io_query( - index=index, ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID - ) - export_data_statement = bigframes.session._io.bigquery.create_export_csv_statement( - f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", - uri=path_or_buf, - field_delimiter=sep, - header=header, + export_array, id_overrides = self._prepare_export( + index=index and self._has_index, + ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID, ) - _, query_job = self._block.expr.session._start_query( - export_data_statement, api_name="dataframe-to_csv" + options = { + "field_delimiter": sep, + "header": header, + } + query_job = self._session._executor.export_gcs( + export_array, + id_overrides, + path_or_buf, + format="csv", + export_options=options, ) self._set_internal_query_job(query_job) return None @@ -3006,17 +3009,12 @@ def to_json( "'lines' keyword is only valid when 'orient' is 'records'." ) - result_table = self._run_io_query( - index=index, ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID - ) - export_data_statement = bigframes.session._io.bigquery.create_export_data_statement( - f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", - uri=path_or_buf, - format="JSON", - export_options={}, + export_array, id_overrides = self._prepare_export( + index=index and self._has_index, + ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID, ) - _, query_job = self._block.expr.session._start_query( - export_data_statement, api_name="dataframe-to_json" + query_job = self._session._executor.export_gcs( + export_array, id_overrides, path_or_buf, format="json", export_options={} ) self._set_internal_query_job(query_job) return None @@ -3145,18 +3143,17 @@ def to_parquet( if compression: export_options["compression"] = compression.upper() - result_table = self._run_io_query( - index=index, ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID + export_array, id_overrides = self._prepare_export( + index=index and self._has_index, + ordering_id=bigframes.session._io.bigquery.IO_ORDERING_ID, ) - export_data_statement = bigframes.session._io.bigquery.create_export_data_statement( - f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", - uri=path, - format="PARQUET", + query_job = self._session._executor.export_gcs( + export_array, + id_overrides, + path, + format="parquet", export_options=export_options, ) - _, query_job = self._block.expr.session._start_query( - export_data_statement, api_name="dataframe-to_parquet" - ) self._set_internal_query_job(query_job) return None @@ -3386,30 +3383,6 @@ def _prepare_export( array_value = array_value.promote_offsets(ordering_id) return array_value, id_overrides - def _run_io_query( - self, - index: bool, - ordering_id: Optional[str] = None, - ) -> bigquery.TableReference: - """Executes a query job presenting this dataframe and returns the destination - table.""" - session = self._block.expr.session - export_array, id_overrides = self._prepare_export( - index=index and self._has_index, ordering_id=ordering_id - ) - - _, query_job = session._execute( - export_array, - ordered=False, - col_id_overrides=id_overrides, - ) - self._set_internal_query_job(query_job) - - # The query job should have finished, so there should be always be a result table. - result_table = query_job.destination - assert result_table is not None - return result_table - def map(self, func, na_action: Optional[str] = None) -> DataFrame: if not callable(func): raise TypeError("the first argument must be callable") diff --git a/bigframes/functions/_remote_function_client.py b/bigframes/functions/_remote_function_client.py index 6ef482ecda3..3698bda28b6 100644 --- a/bigframes/functions/_remote_function_client.py +++ b/bigframes/functions/_remote_function_client.py @@ -37,6 +37,8 @@ import google.api_core.retry from google.cloud import bigquery, functions_v2 +import bigframes.session._io.bigquery + from . import _utils logger = logging.getLogger(__name__) @@ -142,7 +144,12 @@ def create_bq_remote_function( self._bq_client.create_dataset(dataset, exists_ok=True) # TODO(swast): plumb through the original, user-facing api_name. - _, query_job = self._session._start_query(create_function_ddl) + _, query_job = bigframes.session._io.bigquery.start_query_with_client( + self._session.bqclient, + create_function_ddl, + job_config=bigquery.QueryJobConfig(), + ) + logger.info(f"Created remote function {query_job.ddl_target_routine}") def get_cloud_function_fully_qualified_parent(self): diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 0868ef202ae..c91266b8757 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,9 +16,6 @@ from __future__ import annotations -import copy -import datetime -import itertools import logging import os import secrets @@ -27,10 +24,8 @@ Any, Callable, Dict, - Hashable, IO, Iterable, - List, Literal, Mapping, MutableSequence, @@ -47,21 +42,10 @@ import bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet import bigframes_vendored.pandas.io.parsers.readers as third_party_pandas_readers import bigframes_vendored.pandas.io.pickle as third_party_pandas_pickle -import google.api_core.client_info -import google.api_core.client_options -import google.api_core.exceptions -import google.api_core.gapic_v1.client_info -import google.auth.credentials import google.cloud.bigquery as bigquery -import google.cloud.bigquery.table -import google.cloud.bigquery_connection_v1 -import google.cloud.bigquery_storage_v1 -import google.cloud.functions_v2 -import google.cloud.resourcemanager_v3 import google.cloud.storage as storage # type: ignore import ibis import ibis.backends.bigquery as ibis_bigquery -import jellyfish import numpy as np import pandas from pandas._typing import ( @@ -80,8 +64,6 @@ import bigframes.core.compile import bigframes.core.guid import bigframes.core.pruning -import bigframes.core.schema as schemata -import bigframes.core.utils as utils # Even though the ibis.backends.bigquery import is unused, it's needed # to register new and replacement ops with the Ibis BigQuery backend. @@ -92,9 +74,9 @@ import bigframes.functions._remote_function_session as bigframes_rf_session import bigframes.functions.remote_function as bigframes_rf import bigframes.session._io.bigquery as bf_io_bigquery -import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table import bigframes.session.clients import bigframes.session.executor +import bigframes.session.loader import bigframes.session.metrics import bigframes.session.planner import bigframes.session.temp_storage @@ -109,8 +91,6 @@ _BIGFRAMES_DEFAULT_CONNECTION_ID = "bigframes-default-connection" -_MAX_CLUSTER_COLUMNS = 4 - # TODO(swast): Need to connect to regional endpoints when performing remote # functions operations (BQ Connection IAM, Cloud Run / Cloud Functions). # Also see if resource manager client library supports regional endpoints. @@ -145,20 +125,6 @@ ) -def _to_index_cols( - index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), -) -> List[str]: - """Convert index_col into a list of column names.""" - if isinstance(index_col, bigframes.enums.DefaultIndexKind): - index_cols: List[str] = [] - elif isinstance(index_col, str): - index_cols = [index_col] - else: - index_cols = list(index_col) - - return index_cols - - class Session( third_party_pandas_gbq.GBQIOMixin, third_party_pandas_parquet.ParquetIOMixin, @@ -225,14 +191,6 @@ def __init__( bq_kms_key_name=self._bq_kms_key_name, ) - self._anonymous_dataset = ( - bigframes.session._io.bigquery.create_bq_dataset_reference( - self.bqclient, - location=self._location, - api_name="session-__init__", - ) - ) - # TODO(shobs): Remove this logic after https://github.com/ibis-project/ibis/issues/8494 # has been fixed. The ibis client changes the default query job config # so we are going to remember the current config and restore it after @@ -259,9 +217,6 @@ def __init__( # Now that we're starting the session, don't allow the options to be # changed. context._session_started = True - self._df_snapshot: Dict[ - bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table] - ] = {} # unique session identifier, short enough to be human readable # only needs to be unique among sessions created by the same user @@ -287,20 +242,19 @@ def __init__( bigframes.exceptions.OrderingModePartialPreviewWarning, ) - # Sequential index needs total ordering to generate, so use null index with unstrict ordering. - self._default_index_type: bigframes.enums.DefaultIndexKind = ( + self._allow_ambiguity = not self._strictly_ordered + self._default_index_type = ( bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 if self._strictly_ordered else bigframes.enums.DefaultIndexKind.NULL ) - self._allow_ambiguity = not self._strictly_ordered self._metrics = bigframes.session.metrics.ExecutionMetrics() self._remote_function_session = bigframes_rf_session.RemoteFunctionSession() self._temp_storage_manager = ( bigframes.session.temp_storage.TemporaryGbqStorageManager( self._clients_provider.bqclient, - dataset=self._anonymous_dataset, + location=self._location, session_id=self._session_id, kms_key=self._bq_kms_key_name, ) @@ -312,6 +266,15 @@ def __init__( metrics=self._metrics, ) + self._loader = bigframes.session.loader.GbqDataLoader( + session=self, + bqclient=self._clients_provider.bqclient, + storage_manager=self._temp_storage_manager, + default_index_type=self._default_index_type, + scan_index_uniqueness=self._strictly_ordered, + metrics=self._metrics, + ) + @property def bqclient(self): return self._clients_provider.bqclient @@ -377,9 +340,13 @@ def slot_millis_sum(self): def _allows_ambiguity(self) -> bool: return self._allow_ambiguity + @property + def _anonymous_dataset(self): + return self._temp_storage_manager.dataset + def __hash__(self): # Stable hash needed to use in expression tree - return hash(str(self._anonymous_dataset)) + return hash(str(self._session_id)) def close(self): """Delete resources that were created with this session's session_id. @@ -412,7 +379,7 @@ def read_gbq( columns = col_order if bf_io_bigquery.is_query(query_or_table): - return self._read_gbq_query( + return self._loader.read_gbq_query( query_or_table, index_col=index_col, columns=columns, @@ -430,7 +397,7 @@ def read_gbq( "'configuration' or use a query." ) - return self._read_gbq_table( + return self._loader.read_gbq_table( query_or_table, index_col=index_col, columns=columns, @@ -448,71 +415,6 @@ def _register_object( ): self._objects.append(weakref.ref(object)) - def _query_to_destination( - self, - query: str, - index_cols: List[str], - api_name: str, - configuration: dict = {"query": {"useQueryCache": True}}, - do_clustering=True, - ) -> Tuple[Optional[bigquery.TableReference], bigquery.QueryJob]: - # If a dry_run indicates this is not a query type job, then don't - # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. - dry_run_config = bigquery.QueryJobConfig() - dry_run_config.dry_run = True - _, dry_run_job = self._start_query( - query, job_config=dry_run_config, api_name=api_name - ) - if dry_run_job.statement_type != "SELECT": - _, query_job = self._start_query(query, api_name=api_name) - return query_job.destination, query_job - - # Create a table to workaround BigQuery 10 GB query results limit. See: - # internal issue 303057336. - # Since we have a `statement_type == 'SELECT'`, schema should be populated. - schema = dry_run_job.schema - assert schema is not None - if do_clustering: - cluster_cols = bf_io_bigquery.select_cluster_cols( - schema, cluster_candidates=index_cols - ) - else: - cluster_cols = [] - temp_table = self._temp_storage_manager.create_temp_table(schema, cluster_cols) - - timeout_ms = configuration.get("jobTimeoutMs") or configuration["query"].get( - "timeoutMs" - ) - - # Convert timeout_ms to seconds, ensuring a minimum of 0.1 seconds to avoid - # the program getting stuck on too-short timeouts. - timeout = max(int(timeout_ms) * 1e-3, 0.1) if timeout_ms else None - - job_config = typing.cast( - bigquery.QueryJobConfig, - bigquery.QueryJobConfig.from_api_repr(configuration), - ) - job_config.destination = temp_table - - try: - # Write to temp table to workaround BigQuery 10 GB query results - # limit. See: internal issue 303057336. - job_config.labels["error_caught"] = "true" - _, query_job = self._start_query( - query, - job_config=job_config, - timeout=timeout, - api_name=api_name, - ) - return query_job.destination, query_job - except google.api_core.exceptions.BadRequest: - # Some SELECT statements still aren't compatible with cluster - # tables as the destination. For example, if the query has a - # top-level ORDER BY, this conflicts with our ability to cluster - # the table by the index column(s). - _, query_job = self._start_query(query, timeout=timeout, api_name=api_name) - return query_job.destination, query_job - def read_gbq_query( self, query: str, @@ -582,7 +484,7 @@ def read_gbq_query( elif col_order: columns = col_order - return self._read_gbq_query( + return self._loader.read_gbq_query( query=query, index_col=index_col, columns=columns, @@ -593,95 +495,6 @@ def read_gbq_query( filters=filters, ) - def _read_gbq_query( - self, - query: str, - *, - index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), - columns: Iterable[str] = (), - configuration: Optional[Dict] = None, - max_results: Optional[int] = None, - api_name: str = "read_gbq_query", - use_cache: Optional[bool] = None, - filters: third_party_pandas_gbq.FiltersType = (), - ) -> dataframe.DataFrame: - import bigframes.dataframe as dataframe - - configuration = _transform_read_gbq_configuration(configuration) - - if "query" not in configuration: - configuration["query"] = {} - - if "query" in configuration["query"]: - raise ValueError( - "The query statement must not be included in the ", - "'configuration' because it is already provided as", - " a separate parameter.", - ) - - if "useQueryCache" in configuration["query"]: - if use_cache is not None: - raise ValueError( - "'useQueryCache' in 'configuration' conflicts with" - " 'use_cache' parameter. Please specify only one." - ) - else: - configuration["query"]["useQueryCache"] = ( - True if use_cache is None else use_cache - ) - - index_cols = _to_index_cols(index_col) - - filters_copy1, filters_copy2 = itertools.tee(filters) - has_filters = len(list(filters_copy1)) != 0 - filters = typing.cast(third_party_pandas_gbq.FiltersType, filters_copy2) - if has_filters or max_results is not None: - # TODO(b/338111344): If we are running a query anyway, we might as - # well generate ROW_NUMBER() at the same time. - all_columns = itertools.chain(index_cols, columns) if columns else () - query = bf_io_bigquery.to_query( - query, - all_columns, - bf_io_bigquery.compile_filters(filters) if has_filters else None, - max_results=max_results, - # We're executing the query, so we don't need time travel for - # determinism. - time_travel_timestamp=None, - ) - - destination, query_job = self._query_to_destination( - query, - index_cols, - api_name=api_name, - configuration=configuration, - ) - - # If there was no destination table, that means the query must have - # been DDL or DML. Return some job metadata, instead. - if not destination: - return dataframe.DataFrame( - data=pandas.DataFrame( - { - "statement_type": [ - query_job.statement_type if query_job else "unknown" - ], - "job_id": [query_job.job_id if query_job else "unknown"], - "location": [query_job.location if query_job else "unknown"], - } - ), - session=self, - ) - - return self._read_gbq_table( - f"{destination.project}.{destination.dataset_id}.{destination.table_id}", - index_col=index_col, - columns=columns, - use_cache=configuration["query"]["useQueryCache"], - api_name=api_name, - # max_results and filters are omitted because they are already - # handled by to_query(), above. - ) - def read_gbq_table( self, query: str, @@ -715,7 +528,7 @@ def read_gbq_table( elif col_order: columns = col_order - return self._read_gbq_table( + return self._loader.read_gbq_table( query=query, index_col=index_col, columns=columns, @@ -750,7 +563,7 @@ def read_gbq_table_streaming( import bigframes.streaming.dataframe as streaming_dataframe - df = self._read_gbq_table( + df = self._loader.read_gbq_table( table, api_name="read_gbq_table_steaming", enable_snapshot=False, @@ -759,249 +572,6 @@ def read_gbq_table_streaming( return streaming_dataframe.StreamingDataFrame._from_table_df(df) - def _read_gbq_table( - self, - query: str, - *, - index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), - columns: Iterable[str] = (), - max_results: Optional[int] = None, - api_name: str, - use_cache: bool = True, - filters: third_party_pandas_gbq.FiltersType = (), - enable_snapshot: bool = True, - ) -> dataframe.DataFrame: - import bigframes.dataframe as dataframe - - # --------------------------------- - # Validate and transform parameters - # --------------------------------- - - if max_results and max_results <= 0: - raise ValueError( - f"`max_results` should be a positive number, got {max_results}." - ) - - table_ref = bigquery.table.TableReference.from_string( - query, default_project=self.bqclient.project - ) - - columns = list(columns) - filters = typing.cast(list, list(filters)) - - # --------------------------------- - # Fetch table metadata and validate - # --------------------------------- - - time_travel_timestamp, table = bf_read_gbq_table.get_table_metadata( - self.bqclient, - table_ref=table_ref, - api_name=api_name, - cache=self._df_snapshot, - use_cache=use_cache, - ) - table_column_names = {field.name for field in table.schema} - - if table.location.casefold() != self._location.casefold(): - raise ValueError( - f"Current session is in {self._location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" - ) - - for key in columns: - if key not in table_column_names: - possibility = min( - table_column_names, - key=lambda item: jellyfish.levenshtein_distance(key, item), - ) - raise ValueError( - f"Column '{key}' of `columns` not found in this table. Did you mean '{possibility}'?" - ) - - # Converting index_col into a list of column names requires - # the table metadata because we might use the primary keys - # when constructing the index. - index_cols = bf_read_gbq_table.get_index_cols( - table=table, - index_col=index_col, - ) - - for key in index_cols: - if key not in table_column_names: - possibility = min( - table_column_names, - key=lambda item: jellyfish.levenshtein_distance(key, item), - ) - raise ValueError( - f"Column '{key}' of `index_col` not found in this table. Did you mean '{possibility}'?" - ) - - # ----------------------------- - # Optionally, execute the query - # ----------------------------- - - # max_results introduces non-determinism and limits the cost on - # clustered tables, so fallback to a query. We do this here so that - # the index is consistent with tables that have primary keys, even - # when max_results is set. - # TODO(b/338419730): We don't need to fallback to a query for wildcard - # tables if we allow some non-determinism when time travel isn't supported. - if max_results is not None or bf_io_bigquery.is_table_with_wildcard_suffix( - query - ): - # TODO(b/338111344): If we are running a query anyway, we might as - # well generate ROW_NUMBER() at the same time. - all_columns = itertools.chain(index_cols, columns) if columns else () - query = bf_io_bigquery.to_query( - query, - columns=all_columns, - sql_predicate=bf_io_bigquery.compile_filters(filters) - if filters - else None, - max_results=max_results, - # We're executing the query, so we don't need time travel for - # determinism. - time_travel_timestamp=None, - ) - - return self._read_gbq_query( - query, - index_col=index_cols, - columns=columns, - api_name="read_gbq_table", - use_cache=use_cache, - ) - - # ----------------------------------------- - # Validate table access and features - # ----------------------------------------- - - # Use a time travel to make sure the DataFrame is deterministic, even - # if the underlying table changes. - - # If a dry run query fails with time travel but - # succeeds without it, omit the time travel clause and raise a warning - # about potential non-determinism if the underlying tables are modified. - filter_str = bf_io_bigquery.compile_filters(filters) if filters else None - all_columns = ( - () - if len(columns) == 0 - else (*columns, *[col for col in index_cols if col not in columns]) - ) - - enable_snapshot = enable_snapshot and bf_read_gbq_table.validate_table( - self.bqclient, table_ref, all_columns, time_travel_timestamp, filter_str - ) - - # ---------------------------- - # Create ordering and validate - # ---------------------------- - - # TODO(b/337925142): Generate a new subquery with just the index_cols - # in the Ibis table expression so we don't have a "SELECT *" subquery - # in the query that checks for index uniqueness. - # TODO(b/338065601): Provide a way to assume uniqueness and avoid this - # check. - is_index_unique = bf_read_gbq_table.are_index_cols_unique( - bqclient=self.bqclient, - table=table, - index_cols=index_cols, - api_name=api_name, - # If non in strict ordering mode, don't go through overhead of scanning index column(s) to determine if unique - metadata_only=not self._strictly_ordered, - ) - schema = schemata.ArraySchema.from_bq_table(table) - if columns: - schema = schema.select(index_cols + columns) - array_value = core.ArrayValue.from_table( - table, - schema=schema, - predicate=filter_str, - at_time=time_travel_timestamp if enable_snapshot else None, - primary_key=index_cols if is_index_unique else (), - session=self, - ) - - # ---------------------------------------------------- - # Create Default Sequential Index if still have no index - # ---------------------------------------------------- - - # If no index columns provided or found, fall back to session default - if (index_col != bigframes.enums.DefaultIndexKind.NULL) and len( - index_cols - ) == 0: - index_col = self._default_index_type - - index_names: Sequence[Hashable] = index_cols - if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: - sequential_index_col = bigframes.core.guid.generate_guid("index_") - array_value = array_value.promote_offsets(sequential_index_col) - index_cols = [sequential_index_col] - index_names = [None] - - value_columns = [col for col in array_value.column_ids if col not in index_cols] - block = blocks.Block( - array_value, - index_columns=index_cols, - column_labels=value_columns, - index_labels=index_names, - ) - if max_results: - block = block.slice(stop=max_results) - df = dataframe.DataFrame(block) - - # If user provided index columns, should sort over it - if len(index_cols) > 0: - df.sort_index() - return df - - def _read_bigquery_load_job( - self, - filepath_or_buffer: str | IO["bytes"], - table: Union[bigquery.Table, bigquery.TableReference], - *, - job_config: bigquery.LoadJobConfig, - index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), - columns: Iterable[str] = (), - ) -> dataframe.DataFrame: - index_cols = _to_index_cols(index_col) - - if not job_config.clustering_fields and index_cols: - job_config.clustering_fields = index_cols[:_MAX_CLUSTER_COLUMNS] - - if isinstance(filepath_or_buffer, str): - if filepath_or_buffer.startswith("gs://"): - load_job = self.bqclient.load_table_from_uri( - filepath_or_buffer, table, job_config=job_config - ) - else: - with open(filepath_or_buffer, "rb") as source_file: - load_job = self.bqclient.load_table_from_file( - source_file, table, job_config=job_config - ) - else: - load_job = self.bqclient.load_table_from_file( - filepath_or_buffer, table, job_config=job_config - ) - - self._start_generic_job(load_job) - table_id = f"{table.project}.{table.dataset_id}.{table.table_id}" - - # Update the table expiration so we aren't limited to the default 24 - # hours of the anonymous dataset. - table_expiration = bigquery.Table(table_id) - table_expiration.expires = ( - datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION - ) - self.bqclient.update_table(table_expiration, ["expires"]) - - # The BigQuery REST API for tables.get doesn't take a session ID, so we - # can't get the schema for a temp table that way. - return self.read_gbq_table( - table_id, - index_col=index_col, - columns=columns, - ) - def read_gbq_model(self, model_name: str): """Loads a BigQuery ML model from BigQuery. @@ -1117,7 +687,7 @@ def _read_pandas( if inline_df is not None: return inline_df try: - return self._read_pandas_load_job(pandas_dataframe, api_name) + return self._loader.read_pandas_load_job(pandas_dataframe, api_name) except pa.ArrowInvalid as e: raise pa.ArrowInvalid( f"Could not convert with a BigQuery type: `{e}`. " @@ -1149,78 +719,6 @@ def _read_pandas_inline( return inline_df return None - def _read_pandas_load_job( - self, pandas_dataframe: pandas.DataFrame, api_name: str - ) -> dataframe.DataFrame: - import bigframes.dataframe as dataframe - - col_index = pandas_dataframe.columns.copy() - col_labels, idx_labels = ( - col_index.to_list(), - pandas_dataframe.index.names, - ) - new_col_ids, new_idx_ids = utils.get_standardized_ids( - col_labels, - idx_labels, - # Loading parquet files into BigQuery with special column names - # is only supported under an allowlist. - strict=True, - ) - - # Add order column to pandas DataFrame to preserve order in BigQuery - ordering_col = "rowid" - columns = frozenset(col_labels + idx_labels) - suffix = 2 - while ordering_col in columns: - ordering_col = f"rowid_{suffix}" - suffix += 1 - - pandas_dataframe_copy = pandas_dataframe.copy() - pandas_dataframe_copy.index.names = new_idx_ids - pandas_dataframe_copy.columns = pandas.Index(new_col_ids) - pandas_dataframe_copy[ordering_col] = np.arange(pandas_dataframe_copy.shape[0]) - - job_config = bigquery.LoadJobConfig() - # Specify the datetime dtypes, which is auto-detected as timestamp types. - schema: list[bigquery.SchemaField] = [] - for column, dtype in zip(new_col_ids, pandas_dataframe.dtypes): - if dtype == "timestamp[us][pyarrow]": - schema.append( - bigquery.SchemaField(column, bigquery.enums.SqlTypeNames.DATETIME) - ) - job_config.schema = schema - - # Clustering probably not needed anyways as pandas tables are small - cluster_cols = [ordering_col] - job_config.clustering_fields = cluster_cols - - job_config.labels = {"bigframes-api": api_name} - - load_table_destination = self._temp_storage_manager._random_table() - load_job = self.bqclient.load_table_from_dataframe( - pandas_dataframe_copy, - load_table_destination, - job_config=job_config, - ) - self._start_generic_job(load_job) - - destination_table = self.bqclient.get_table(load_table_destination) - array_value = core.ArrayValue.from_table( - table=destination_table, - # TODO: Generate this directly from original pandas df. - schema=schemata.ArraySchema.from_bq_table(destination_table), - session=self, - offsets_col=ordering_col, - ).drop_columns([ordering_col]) - - block = blocks.Block( - array_value, - index_columns=new_idx_ids, - column_labels=col_index, - index_labels=idx_labels, - ) - return dataframe.DataFrame(block) - def read_csv( self, filepath_or_buffer: str | IO["bytes"], @@ -1334,7 +832,7 @@ def read_csv( elif header > 0: job_config.skip_leading_rows = header - return self._read_bigquery_load_job( + return self._loader._read_bigquery_load_job( filepath_or_buffer, table, job_config=job_config, @@ -1403,7 +901,9 @@ def read_parquet( job_config.write_disposition = bigquery.WriteDisposition.WRITE_EMPTY job_config.labels = {"bigframes-api": "read_parquet"} - return self._read_bigquery_load_job(path, table, job_config=job_config) + return self._loader._read_bigquery_load_job( + path, table, job_config=job_config + ) else: read_parquet_kwargs: Dict[str, Any] = {} if pandas.__version__.startswith("1."): @@ -1463,7 +963,7 @@ def read_json( job_config.encoding = encoding job_config.labels = {"bigframes-api": "read_json"} - return self._read_bigquery_load_job( + return self._loader._read_bigquery_load_job( path_or_buf, table, job_config=job_config, @@ -1521,20 +1021,6 @@ def _check_file_size(self, filepath: str): "for large files to avoid loading the file into local memory." ) - def _sql_to_temp_table( - self, - sql: str, - cluster_cols: Iterable[str], - api_name: str, - ) -> bigquery.TableReference: - destination, _ = self._query_to_destination( - sql, - index_cols=list(cluster_cols), - api_name=api_name, - ) - # There should always be a destination table for this query type. - return typing.cast(bigquery.TableReference, destination) - def remote_function( self, input_types: Union[None, type, Sequence[type]] = None, @@ -1811,34 +1297,6 @@ def _prepare_copy_job_config(self) -> bigquery.CopyJobConfig: return job_config - def _start_query( - self, - sql: str, - job_config: Optional[bigquery.job.QueryJobConfig] = None, - max_results: Optional[int] = None, - timeout: Optional[float] = None, - api_name: Optional[str] = None, - ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - """ - Starts BigQuery query job and waits for results. - - Do not execute dataframe through this API, instead use the executor. - """ - job_config = bigquery.QueryJobConfig() if job_config is None else job_config - if bigframes.options.compute.maximum_bytes_billed is not None: - # Maybe this should be pushed down into start_query_with_client - job_config.maximum_bytes_billed = ( - bigframes.options.compute.maximum_bytes_billed - ) - return bf_io_bigquery.start_query_with_client( - self.bqclient, - sql, - job_config, - max_results, - timeout, - api_name=api_name, - ) - def _start_query_ml_ddl( self, sql: str, @@ -1883,7 +1341,7 @@ def _export( cluster_cols: Sequence[str], ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: # Note: cluster_cols use pre-override column ids - return self._executor.export( + return self._executor.export_gbq( array_value, destination=destination, col_id_overrides=col_id_overrides, @@ -1937,25 +1395,3 @@ def _start_generic_job(self, job: formatting_helpers.GenericJob): def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: return Session(context) - - -def _transform_read_gbq_configuration(configuration: Optional[dict]) -> dict: - """ - For backwards-compatibility, convert any previously client-side only - parameters such as timeoutMs to the property name expected by the REST API. - - Makes a copy of configuration if changes are needed. - """ - - if configuration is None: - return {} - - timeout_ms = configuration.get("query", {}).get("timeoutMs") - if timeout_ms is not None: - # Transform timeoutMs to an actual server-side configuration. - # https://github.com/googleapis/python-bigquery-pandas/issues/479 - configuration = copy.deepcopy(configuration) - del configuration["query"]["timeoutMs"] - configuration["jobTimeoutMs"] = timeout_ms - - return configuration diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index a77729cef97..011c1f1bee6 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -82,26 +82,12 @@ def create_job_configs_labels( return dict(zip(labels[:MAX_LABELS_COUNT], values[:MAX_LABELS_COUNT])) -def create_export_csv_statement( - table_id: str, uri: str, field_delimiter: str, header: bool -) -> str: - return create_export_data_statement( - table_id, - uri, - "CSV", - { - "field_delimiter": field_delimiter, - "header": header, - }, - ) - - def create_export_data_statement( table_id: str, uri: str, format: str, export_options: Dict[str, Union[bool, str]] ) -> str: all_options: Dict[str, Union[bool, str]] = { "uri": uri, - "format": format, + "format": format.upper(), # TODO(swast): Does pandas have an option not to overwrite files? "overwrite": True, } diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 539658a18c4..24bcd02798c 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -15,7 +15,7 @@ from __future__ import annotations import math -from typing import cast, Iterable, Literal, Mapping, Optional, Sequence, Tuple +from typing import cast, Iterable, Literal, Mapping, Optional, Sequence, Tuple, Union import warnings import weakref @@ -118,9 +118,9 @@ def execute( job_config=job_config, ) - def export( + def export_gbq( self, - array_value, + array_value: bigframes.core.ArrayValue, col_id_overrides: Mapping[str, str], destination: bigquery.TableReference, if_exists: Literal["fail", "replace", "append"] = "fail", @@ -147,6 +147,35 @@ def export( job_config=job_config, ) + def export_gcs( + self, + array_value: bigframes.core.ArrayValue, + col_id_overrides: Mapping[str, str], + uri: str, + format: Literal["json", "csv", "parquet"], + export_options: Mapping[str, Union[bool, str]], + ): + """ + Export the ArrayValue to gcs. + """ + _, query_job = self.execute( + array_value, + ordered=False, + col_id_overrides=col_id_overrides, + ) + result_table = query_job.destination + export_data_statement = bq_io.create_export_data_statement( + f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", + uri=uri, + format=format, + export_options=dict(export_options), + ) + job_config = bigquery.QueryJobConfig() + bq_io.add_labels(job_config, api_name=f"dataframe-to_{format.lower()}") + export_job = self.bqclient.query(export_data_statement, job_config=job_config) + self._wait_on_job(export_job) + return query_job + def dry_run(self, array_value: bigframes.core.ArrayValue, ordered: bool = True): """ Dry run executing the ArrayValue. @@ -198,17 +227,7 @@ def _run_execute_query( job_config.labels["bigframes-mode"] = "unordered" try: query_job = self.bqclient.query(sql, job_config=job_config) - opts = bigframes.options.display - if opts.progress_bar is not None and not query_job.configuration.dry_run: - results_iterator = formatting_helpers.wait_for_query_job( - query_job, progress_bar=opts.progress_bar - ) - else: - results_iterator = query_job.result() - - if self.metrics is not None: - self.metrics.count_job_stats(query_job) - return results_iterator, query_job + return self._wait_on_job(query_job), query_job except google.api_core.exceptions.BadRequest as e: # Unfortunately, this error type does not have a separate error code or exception type @@ -218,6 +237,19 @@ def _run_execute_query( else: raise + def _wait_on_job(self, query_job: bigquery.QueryJob) -> bigquery.table.RowIterator: + opts = bigframes.options.display + if opts.progress_bar is not None and not query_job.configuration.dry_run: + results_iterator = formatting_helpers.wait_for_query_job( + query_job, progress_bar=opts.progress_bar + ) + else: + results_iterator = query_job.result() + + if self.metrics is not None: + self.metrics.count_job_stats(query_job) + return results_iterator + def _with_cached_executions(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: return tree_properties.replace_nodes(node, (dict(self._cached_executions))) diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py new file mode 100644 index 00000000000..edfd57b9657 --- /dev/null +++ b/bigframes/session/loader.py @@ -0,0 +1,657 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import copy +import dataclasses +import datetime +import itertools +import typing +from typing import Dict, Hashable, IO, Iterable, List, Optional, Sequence, Tuple, Union + +import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq +import google.api_core.exceptions +import google.auth.credentials +import google.cloud.bigquery as bigquery +import google.cloud.bigquery.table +import google.cloud.bigquery_connection_v1 +import google.cloud.bigquery_storage_v1 +import google.cloud.functions_v2 +import google.cloud.resourcemanager_v3 +import jellyfish +import numpy as np +import pandas + +import bigframes.clients +import bigframes.constants as constants +import bigframes.core as core +import bigframes.core.blocks as blocks +import bigframes.core.compile +import bigframes.core.guid +import bigframes.core.pruning +import bigframes.core.schema as schemata +import bigframes.core.utils as utils + +# Even though the ibis.backends.bigquery import is unused, it's needed +# to register new and replacement ops with the Ibis BigQuery backend. +import bigframes.dataframe +import bigframes.dtypes +import bigframes.exceptions +import bigframes.formatting_helpers as formatting_helpers +import bigframes.session._io.bigquery as bf_io_bigquery +import bigframes.session._io.bigquery.read_gbq_table as bf_read_gbq_table +import bigframes.session.clients +import bigframes.session.executor +import bigframes.session.metrics +import bigframes.session.planner +import bigframes.session.temp_storage +import bigframes.version + +# Avoid circular imports. +if typing.TYPE_CHECKING: + import bigframes.core.indexes + import bigframes.dataframe as dataframe + import bigframes.series + import bigframes.session + +_MAX_CLUSTER_COLUMNS = 4 + + +def _to_index_cols( + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), +) -> List[str]: + """Convert index_col into a list of column names.""" + if isinstance(index_col, bigframes.enums.DefaultIndexKind): + index_cols: List[str] = [] + elif isinstance(index_col, str): + index_cols = [index_col] + else: + index_cols = list(index_col) + + return index_cols + + +@dataclasses.dataclass +class GbqDataLoader: + """ + Responsible for loading data into BigFrames using temporary bigquery tables. + + This loader is constrained to loading local data and queries against data sources in the same region as the storage manager. + + + Args: + session (bigframes.session.Session): + The session the data will be loaded into. Objects will not be compatible with other sessions. + bqclient (bigquery.Client): + An object providing client library objects. + storage_manager (bigframes.session.temp_storage.TemporaryGbqStorageManager): + Manages temporary storage used by the loader. + default_index_type (bigframes.enums.DefaultIndexKind): + Determines the index type created for data loaded from gcs or gbq. + scan_index_uniqueness (bool): + Whether the loader will scan index columns to determine whether the values are unique. + This behavior is useful in total ordering mode to use index column as order key. + metrics (bigframes.session.metrics.ExecutionMetrics or None): + Used to record query execution statistics. + """ + + def __init__( + self, + session: bigframes.session.Session, + bqclient: bigquery.Client, + storage_manager: bigframes.session.temp_storage.TemporaryGbqStorageManager, + default_index_type: bigframes.enums.DefaultIndexKind, + scan_index_uniqueness: bool, + metrics: Optional[bigframes.session.metrics.ExecutionMetrics] = None, + ): + self._bqclient = bqclient + self._storage_manager = storage_manager + self._default_index_type = default_index_type + self._scan_index_uniqueness = scan_index_uniqueness + self._df_snapshot: Dict[ + bigquery.TableReference, Tuple[datetime.datetime, bigquery.Table] + ] = {} + self._metrics = metrics + # Unfortunate circular reference, but need to pass reference when constructing objects + self._session = session + + def read_pandas_load_job( + self, pandas_dataframe: pandas.DataFrame, api_name: str + ) -> dataframe.DataFrame: + import bigframes.dataframe as dataframe + + col_index = pandas_dataframe.columns.copy() + col_labels, idx_labels = ( + col_index.to_list(), + pandas_dataframe.index.names, + ) + new_col_ids, new_idx_ids = utils.get_standardized_ids( + col_labels, + idx_labels, + # Loading parquet files into BigQuery with special column names + # is only supported under an allowlist. + strict=True, + ) + + # Add order column to pandas DataFrame to preserve order in BigQuery + ordering_col = "rowid" + columns = frozenset(col_labels + idx_labels) + suffix = 2 + while ordering_col in columns: + ordering_col = f"rowid_{suffix}" + suffix += 1 + + pandas_dataframe_copy = pandas_dataframe.copy() + pandas_dataframe_copy.index.names = new_idx_ids + pandas_dataframe_copy.columns = pandas.Index(new_col_ids) + pandas_dataframe_copy[ordering_col] = np.arange(pandas_dataframe_copy.shape[0]) + + job_config = bigquery.LoadJobConfig() + # Specify the datetime dtypes, which is auto-detected as timestamp types. + schema: list[bigquery.SchemaField] = [] + for column, dtype in zip(new_col_ids, pandas_dataframe.dtypes): + if dtype == "timestamp[us][pyarrow]": + schema.append( + bigquery.SchemaField(column, bigquery.enums.SqlTypeNames.DATETIME) + ) + job_config.schema = schema + + # Clustering probably not needed anyways as pandas tables are small + cluster_cols = [ordering_col] + job_config.clustering_fields = cluster_cols + + job_config.labels = {"bigframes-api": api_name} + + load_table_destination = self._storage_manager._random_table() + load_job = self._bqclient.load_table_from_dataframe( + pandas_dataframe_copy, + load_table_destination, + job_config=job_config, + ) + self._start_generic_job(load_job) + + destination_table = self._bqclient.get_table(load_table_destination) + array_value = core.ArrayValue.from_table( + table=destination_table, + # TODO: Generate this directly from original pandas df. + schema=schemata.ArraySchema.from_bq_table(destination_table), + session=self._session, + offsets_col=ordering_col, + ).drop_columns([ordering_col]) + + block = blocks.Block( + array_value, + index_columns=new_idx_ids, + column_labels=col_index, + index_labels=idx_labels, + ) + return dataframe.DataFrame(block) + + def _start_generic_job(self, job: formatting_helpers.GenericJob): + if bigframes.options.display.progress_bar is not None: + formatting_helpers.wait_for_job( + job, bigframes.options.display.progress_bar + ) # Wait for the job to complete + else: + job.result() + + def read_gbq_table( + self, + query: str, + *, + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), + columns: Iterable[str] = (), + max_results: Optional[int] = None, + api_name: str, + use_cache: bool = True, + filters: third_party_pandas_gbq.FiltersType = (), + enable_snapshot: bool = True, + ) -> dataframe.DataFrame: + import bigframes.dataframe as dataframe + + # --------------------------------- + # Validate and transform parameters + # --------------------------------- + + if max_results and max_results <= 0: + raise ValueError( + f"`max_results` should be a positive number, got {max_results}." + ) + + table_ref = google.cloud.bigquery.table.TableReference.from_string( + query, default_project=self._bqclient.project + ) + + columns = list(columns) + filters = typing.cast(list, list(filters)) + + # --------------------------------- + # Fetch table metadata and validate + # --------------------------------- + + time_travel_timestamp, table = bf_read_gbq_table.get_table_metadata( + self._bqclient, + table_ref=table_ref, + api_name=api_name, + cache=self._df_snapshot, + use_cache=use_cache, + ) + table_column_names = {field.name for field in table.schema} + + if table.location.casefold() != self._storage_manager.location.casefold(): + raise ValueError( + f"Current session is in {self._storage_manager.location} but dataset '{table.project}.{table.dataset_id}' is located in {table.location}" + ) + + for key in columns: + if key not in table_column_names: + possibility = min( + table_column_names, + key=lambda item: jellyfish.levenshtein_distance(key, item), + ) + raise ValueError( + f"Column '{key}' of `columns` not found in this table. Did you mean '{possibility}'?" + ) + + # Converting index_col into a list of column names requires + # the table metadata because we might use the primary keys + # when constructing the index. + index_cols = bf_read_gbq_table.get_index_cols( + table=table, + index_col=index_col, + ) + + for key in index_cols: + if key not in table_column_names: + possibility = min( + table_column_names, + key=lambda item: jellyfish.levenshtein_distance(key, item), + ) + raise ValueError( + f"Column '{key}' of `index_col` not found in this table. Did you mean '{possibility}'?" + ) + + # ----------------------------- + # Optionally, execute the query + # ----------------------------- + + # max_results introduces non-determinism and limits the cost on + # clustered tables, so fallback to a query. We do this here so that + # the index is consistent with tables that have primary keys, even + # when max_results is set. + # TODO(b/338419730): We don't need to fallback to a query for wildcard + # tables if we allow some non-determinism when time travel isn't supported. + if max_results is not None or bf_io_bigquery.is_table_with_wildcard_suffix( + query + ): + # TODO(b/338111344): If we are running a query anyway, we might as + # well generate ROW_NUMBER() at the same time. + all_columns = itertools.chain(index_cols, columns) if columns else () + query = bf_io_bigquery.to_query( + query, + columns=all_columns, + sql_predicate=bf_io_bigquery.compile_filters(filters) + if filters + else None, + max_results=max_results, + # We're executing the query, so we don't need time travel for + # determinism. + time_travel_timestamp=None, + ) + + return self.read_gbq_query( + query, + index_col=index_cols, + columns=columns, + api_name="read_gbq_table", + use_cache=use_cache, + ) + + # ----------------------------------------- + # Validate table access and features + # ----------------------------------------- + + # Use a time travel to make sure the DataFrame is deterministic, even + # if the underlying table changes. + + # If a dry run query fails with time travel but + # succeeds without it, omit the time travel clause and raise a warning + # about potential non-determinism if the underlying tables are modified. + filter_str = bf_io_bigquery.compile_filters(filters) if filters else None + all_columns = ( + () + if len(columns) == 0 + else (*columns, *[col for col in index_cols if col not in columns]) + ) + + enable_snapshot = enable_snapshot and bf_read_gbq_table.validate_table( + self._bqclient, table_ref, all_columns, time_travel_timestamp, filter_str + ) + + # ---------------------------- + # Create ordering and validate + # ---------------------------- + + # TODO(b/337925142): Generate a new subquery with just the index_cols + # in the Ibis table expression so we don't have a "SELECT *" subquery + # in the query that checks for index uniqueness. + # TODO(b/338065601): Provide a way to assume uniqueness and avoid this + # check. + is_index_unique = bf_read_gbq_table.are_index_cols_unique( + bqclient=self._bqclient, + table=table, + index_cols=index_cols, + api_name=api_name, + # If non in strict ordering mode, don't go through overhead of scanning index column(s) to determine if unique + metadata_only=not self._scan_index_uniqueness, + ) + schema = schemata.ArraySchema.from_bq_table(table) + if columns: + schema = schema.select(index_cols + columns) + array_value = core.ArrayValue.from_table( + table, + schema=schema, + predicate=filter_str, + at_time=time_travel_timestamp if enable_snapshot else None, + primary_key=index_cols if is_index_unique else (), + session=self._session, + ) + + # ---------------------------------------------------- + # Create Default Sequential Index if still have no index + # ---------------------------------------------------- + + # If no index columns provided or found, fall back to session default + if (index_col != bigframes.enums.DefaultIndexKind.NULL) and len( + index_cols + ) == 0: + index_col = self._default_index_type + + index_names: Sequence[Hashable] = index_cols + if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: + sequential_index_col = bigframes.core.guid.generate_guid("index_") + array_value = array_value.promote_offsets(sequential_index_col) + index_cols = [sequential_index_col] + index_names = [None] + + value_columns = [col for col in array_value.column_ids if col not in index_cols] + block = blocks.Block( + array_value, + index_columns=index_cols, + column_labels=value_columns, + index_labels=index_names, + ) + if max_results: + block = block.slice(stop=max_results) + df = dataframe.DataFrame(block) + + # If user provided index columns, should sort over it + if len(index_cols) > 0: + df.sort_index() + return df + + def _read_bigquery_load_job( + self, + filepath_or_buffer: str | IO["bytes"], + table: Union[bigquery.Table, bigquery.TableReference], + *, + job_config: bigquery.LoadJobConfig, + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), + columns: Iterable[str] = (), + ) -> dataframe.DataFrame: + index_cols = _to_index_cols(index_col) + + if not job_config.clustering_fields and index_cols: + job_config.clustering_fields = index_cols[:_MAX_CLUSTER_COLUMNS] + + if isinstance(filepath_or_buffer, str): + if filepath_or_buffer.startswith("gs://"): + load_job = self._bqclient.load_table_from_uri( + filepath_or_buffer, table, job_config=job_config + ) + else: + with open(filepath_or_buffer, "rb") as source_file: + load_job = self._bqclient.load_table_from_file( + source_file, table, job_config=job_config + ) + else: + load_job = self._bqclient.load_table_from_file( + filepath_or_buffer, table, job_config=job_config + ) + + self._start_generic_job(load_job) + table_id = f"{table.project}.{table.dataset_id}.{table.table_id}" + + # Update the table expiration so we aren't limited to the default 24 + # hours of the anonymous dataset. + table_expiration = bigquery.Table(table_id) + table_expiration.expires = ( + datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION + ) + self._bqclient.update_table(table_expiration, ["expires"]) + + # The BigQuery REST API for tables.get doesn't take a session ID, so we + # can't get the schema for a temp table that way. + + return self.read_gbq_table( + query=table_id, + index_col=index_col, + columns=columns, + api_name="read_gbq_table", + ) + + def read_gbq_query( + self, + query: str, + *, + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), + columns: Iterable[str] = (), + configuration: Optional[Dict] = None, + max_results: Optional[int] = None, + api_name: str = "read_gbq_query", + use_cache: Optional[bool] = None, + filters: third_party_pandas_gbq.FiltersType = (), + ) -> dataframe.DataFrame: + import bigframes.dataframe as dataframe + + configuration = _transform_read_gbq_configuration(configuration) + + if "query" not in configuration: + configuration["query"] = {} + + if "query" in configuration["query"]: + raise ValueError( + "The query statement must not be included in the ", + "'configuration' because it is already provided as", + " a separate parameter.", + ) + + if "useQueryCache" in configuration["query"]: + if use_cache is not None: + raise ValueError( + "'useQueryCache' in 'configuration' conflicts with" + " 'use_cache' parameter. Please specify only one." + ) + else: + configuration["query"]["useQueryCache"] = ( + True if use_cache is None else use_cache + ) + + index_cols = _to_index_cols(index_col) + + filters_copy1, filters_copy2 = itertools.tee(filters) + has_filters = len(list(filters_copy1)) != 0 + filters = typing.cast(third_party_pandas_gbq.FiltersType, filters_copy2) + if has_filters or max_results is not None: + # TODO(b/338111344): If we are running a query anyway, we might as + # well generate ROW_NUMBER() at the same time. + all_columns = itertools.chain(index_cols, columns) if columns else () + query = bf_io_bigquery.to_query( + query, + all_columns, + bf_io_bigquery.compile_filters(filters) if has_filters else None, + max_results=max_results, + # We're executing the query, so we don't need time travel for + # determinism. + time_travel_timestamp=None, + ) + + destination, query_job = self._query_to_destination( + query, + index_cols, + api_name=api_name, + configuration=configuration, + ) + + # If there was no destination table, that means the query must have + # been DDL or DML. Return some job metadata, instead. + if not destination: + return dataframe.DataFrame( + data=pandas.DataFrame( + { + "statement_type": [ + query_job.statement_type if query_job else "unknown" + ], + "job_id": [query_job.job_id if query_job else "unknown"], + "location": [query_job.location if query_job else "unknown"], + } + ), + session=self._session, + ) + + return self.read_gbq_table( + f"{destination.project}.{destination.dataset_id}.{destination.table_id}", + index_col=index_col, + columns=columns, + use_cache=configuration["query"]["useQueryCache"], + api_name=api_name, + # max_results and filters are omitted because they are already + # handled by to_query(), above. + ) + + def _query_to_destination( + self, + query: str, + index_cols: List[str], + api_name: str, + configuration: dict = {"query": {"useQueryCache": True}}, + do_clustering=True, + ) -> Tuple[Optional[bigquery.TableReference], bigquery.QueryJob]: + # If a dry_run indicates this is not a query type job, then don't + # bother trying to do a CREATE TEMP TABLE ... AS SELECT ... statement. + dry_run_config = bigquery.QueryJobConfig() + dry_run_config.dry_run = True + _, dry_run_job = self._start_query( + query, job_config=dry_run_config, api_name=api_name + ) + if dry_run_job.statement_type != "SELECT": + _, query_job = self._start_query(query, api_name=api_name) + return query_job.destination, query_job + + # Create a table to workaround BigQuery 10 GB query results limit. See: + # internal issue 303057336. + # Since we have a `statement_type == 'SELECT'`, schema should be populated. + schema = dry_run_job.schema + assert schema is not None + if do_clustering: + cluster_cols = bf_io_bigquery.select_cluster_cols( + schema, cluster_candidates=index_cols + ) + else: + cluster_cols = [] + temp_table = self._storage_manager.create_temp_table(schema, cluster_cols) + + timeout_ms = configuration.get("jobTimeoutMs") or configuration["query"].get( + "timeoutMs" + ) + + # Convert timeout_ms to seconds, ensuring a minimum of 0.1 seconds to avoid + # the program getting stuck on too-short timeouts. + timeout = max(int(timeout_ms) * 1e-3, 0.1) if timeout_ms else None + + job_config = typing.cast( + bigquery.QueryJobConfig, + bigquery.QueryJobConfig.from_api_repr(configuration), + ) + job_config.destination = temp_table + + try: + # Write to temp table to workaround BigQuery 10 GB query results + # limit. See: internal issue 303057336. + job_config.labels["error_caught"] = "true" + _, query_job = self._start_query( + query, + job_config=job_config, + timeout=timeout, + api_name=api_name, + ) + return query_job.destination, query_job + except google.api_core.exceptions.BadRequest: + # Some SELECT statements still aren't compatible with cluster + # tables as the destination. For example, if the query has a + # top-level ORDER BY, this conflicts with our ability to cluster + # the table by the index column(s). + _, query_job = self._start_query(query, timeout=timeout, api_name=api_name) + return query_job.destination, query_job + + def _start_query( + self, + sql: str, + job_config: Optional[google.cloud.bigquery.QueryJobConfig] = None, + max_results: Optional[int] = None, + timeout: Optional[float] = None, + api_name: Optional[str] = None, + ) -> Tuple[google.cloud.bigquery.table.RowIterator, bigquery.QueryJob]: + """ + Starts BigQuery query job and waits for results. + + Do not execute dataframe through this API, instead use the executor. + """ + job_config = bigquery.QueryJobConfig() if job_config is None else job_config + if bigframes.options.compute.maximum_bytes_billed is not None: + # Maybe this should be pushed down into start_query_with_client + job_config.maximum_bytes_billed = ( + bigframes.options.compute.maximum_bytes_billed + ) + return bf_io_bigquery.start_query_with_client( + self._bqclient, + sql, + job_config, + max_results, + timeout, + api_name=api_name, + ) + + +def _transform_read_gbq_configuration(configuration: Optional[dict]) -> dict: + """ + For backwards-compatibility, convert any previously client-side only + parameters such as timeoutMs to the property name expected by the REST API. + + Makes a copy of configuration if changes are needed. + """ + + if configuration is None: + return {} + + timeout_ms = configuration.get("query", {}).get("timeoutMs") + if timeout_ms is not None: + # Transform timeoutMs to an actual server-side configuration. + # https://github.com/googleapis/python-bigquery-pandas/issues/479 + configuration = copy.deepcopy(configuration) + del configuration["query"]["timeoutMs"] + configuration["jobTimeoutMs"] = timeout_ms + + return configuration diff --git a/bigframes/session/temp_storage.py b/bigframes/session/temp_storage.py index fb8c4bac7a8..de764e4535c 100644 --- a/bigframes/session/temp_storage.py +++ b/bigframes/session/temp_storage.py @@ -32,13 +32,19 @@ class TemporaryGbqStorageManager: def __init__( self, bqclient: bigquery.Client, - dataset: bigquery.DatasetReference, + location: str, session_id: str, *, kms_key: Optional[str] = None ): self.bqclient = bqclient - self.dataset = dataset + self.location = location + self.dataset = bf_io_bigquery.create_bq_dataset_reference( + self.bqclient, + location=self.location, + api_name="session-__init__", + ) + self.session_id = session_id self._table_ids: List[str] = [] self._kms_key = kms_key diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 960752a9350..7d684e64b49 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -69,7 +69,7 @@ def test_session_query_job(bq_cmek, session_with_bq_cmek): if not bq_cmek: # pragma: NO COVER pytest.skip("no cmek set for testing") # pragma: NO COVER - _, query_job = session_with_bq_cmek._start_query( + _, query_job = session_with_bq_cmek._loader._start_query( "SELECT 123", job_config=bigquery.QueryJobConfig(use_query_cache=False) ) query_job.result() diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 2f7eaa567a5..b76c74654c1 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -181,7 +181,7 @@ def test_read_gbq_cached_table(): table._properties["location"] = session._location table._properties["numRows"] = "1000000000" table._properties["location"] = session._location - session._df_snapshot[table_ref] = ( + session._loader._df_snapshot[table_ref] = ( datetime.datetime(1999, 1, 2, 3, 4, 5, 678901, tzinfo=datetime.timezone.utc), table, ) diff --git a/tests/unit/test_compute_options.py b/tests/unit/test_compute_options.py deleted file mode 100644 index 2de715a40ec..00000000000 --- a/tests/unit/test_compute_options.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from unittest import mock - -import bigframes as bf - -from . import resources - - -def test_maximum_bytes_option(): - session = resources.create_bigquery_session() - session.bqclient.query = mock.MagicMock() - with bf.option_context("compute.maximum_bytes_billed", 10000): - session._start_query("query") - call = session.bqclient.query.call_args - assert call.kwargs["job_config"].maximum_bytes_billed == 10000 - session.bqclient.query.assert_called_once() From e0afb7ae7e04433d9dc8f4efabc626268a519f25 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:31:00 -0700 Subject: [PATCH 788/921] chre: add tpch q19-22 and benchmark readme (#929) --- tests/benchmark/README.md | 13 ++++ tests/benchmark/tpch/q19.py | 25 +++++++ tests/benchmark/tpch/q20.py | 25 +++++++ tests/benchmark/tpch/q21.py | 25 +++++++ tests/benchmark/tpch/q22.py | 25 +++++++ .../bigframes_vendored/tpch/queries/q19.py | 57 ++++++++++++++++ .../bigframes_vendored/tpch/queries/q20.py | 67 +++++++++++++++++++ .../bigframes_vendored/tpch/queries/q21.py | 62 +++++++++++++++++ .../bigframes_vendored/tpch/queries/q22.py | 52 ++++++++++++++ 9 files changed, 351 insertions(+) create mode 100644 tests/benchmark/README.md create mode 100644 tests/benchmark/tpch/q19.py create mode 100644 tests/benchmark/tpch/q20.py create mode 100644 tests/benchmark/tpch/q21.py create mode 100644 tests/benchmark/tpch/q22.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q19.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q20.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q21.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q22.py diff --git a/tests/benchmark/README.md b/tests/benchmark/README.md new file mode 100644 index 00000000000..9c5f4a99d6e --- /dev/null +++ b/tests/benchmark/README.md @@ -0,0 +1,13 @@ +# Benchmark Tests + +## Overview + +This directory contains scripts for performance benchmarking of various components of BigFrames. + +## Execution Details + +Scripts in this directory can be executed as part of the benchmarking session or independently from the command line. This allows for quick, standalone runs for immediate debugging and validation without the overhead of initiating full benchmark sessions. + +## Why Separate Processes? + +Each benchmark is executed in a separate process to mitigate the effects of any residual caching or settings that may persist in BigFrames, ensuring that each test is conducted in a clean state. diff --git a/tests/benchmark/tpch/q19.py b/tests/benchmark/tpch/q19.py new file mode 100644 index 00000000000..7b13b0d2507 --- /dev/null +++ b/tests/benchmark/tpch/q19.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q19 as vendored_tpch_q19 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q19.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q20.py b/tests/benchmark/tpch/q20.py new file mode 100644 index 00000000000..e02e9306f88 --- /dev/null +++ b/tests/benchmark/tpch/q20.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q20 as vendored_tpch_q20 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q20.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q21.py b/tests/benchmark/tpch/q21.py new file mode 100644 index 00000000000..d123286c3e4 --- /dev/null +++ b/tests/benchmark/tpch/q21.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q21 as vendored_tpch_q21 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q21.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q22.py b/tests/benchmark/tpch/q22.py new file mode 100644 index 00000000000..841740da0e2 --- /dev/null +++ b/tests/benchmark/tpch/q22.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q22 as vendored_tpch_q22 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q22.q, current_path, suffix, dataset_id, session + ) diff --git a/third_party/bigframes_vendored/tpch/queries/q19.py b/third_party/bigframes_vendored/tpch/queries/q19.py new file mode 100644 index 00000000000..526d0aa1a62 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q19.py @@ -0,0 +1,57 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q19.py + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + merged = bpd.merge(part, lineitem, left_on="P_PARTKEY", right_on="L_PARTKEY") + + filtered = merged[ + (merged["L_SHIPMODE"].isin(["AIR", "AIR REG"])) + & (merged["L_SHIPINSTRUCT"] == "DELIVER IN PERSON") + & ( + ( + (merged["P_BRAND"] == "Brand#12") + & ( + merged["P_CONTAINER"].isin( + ["SM CASE", "SM BOX", "SM PACK", "SM PKG"] + ) + ) + & (merged["L_QUANTITY"].between(1, 11, inclusive="both")) + & (merged["P_SIZE"].between(1, 5, inclusive="both")) + ) + | ( + (merged["P_BRAND"] == "Brand#23") + & ( + merged["P_CONTAINER"].isin( + ["MED BAG", "MED BOX", "MED PKG", "MED PACK"] + ) + ) + & (merged["L_QUANTITY"].between(10, 20, inclusive="both")) + & (merged["P_SIZE"].between(1, 10, inclusive="both")) + ) + | ( + (merged["P_BRAND"] == "Brand#34") + & ( + merged["P_CONTAINER"].isin( + ["LG CASE", "LG BOX", "LG PACK", "LG PKG"] + ) + ) + & (merged["L_QUANTITY"].between(20, 30, inclusive="both")) + & (merged["P_SIZE"].between(1, 15, inclusive="both")) + ) + ) + ] + + revenue = (filtered["L_EXTENDEDPRICE"] * (1 - filtered["L_DISCOUNT"])).sum() + _ = round(revenue, 2) diff --git a/third_party/bigframes_vendored/tpch/queries/q20.py b/third_party/bigframes_vendored/tpch/queries/q20.py new file mode 100644 index 00000000000..671d7e06fb0 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q20.py @@ -0,0 +1,67 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q20.py + +from datetime import date + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + partsupp = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = date(1994, 1, 1) + var2 = date(1995, 1, 1) + var3 = "CANADA" + var4 = "forest" + + q1 = lineitem[(lineitem["L_SHIPDATE"] >= var1) & (lineitem["L_SHIPDATE"] < var2)] + q1 = q1.groupby(["L_PARTKEY", "L_SUPPKEY"], as_index=False).agg( + SUM_QUANTITY=bpd.NamedAgg(column="L_QUANTITY", aggfunc="sum") + ) + q1["SUM_QUANTITY"] = q1["SUM_QUANTITY"] * 0.5 + q2 = nation[nation["N_NAME"] == var3] + + q3 = supplier.merge(q2, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + + filtered_parts = part[part["P_NAME"].str.startswith(var4)] + + if not session._strictly_ordered: + filtered_parts = filtered_parts[["P_PARTKEY"]].sort_values(by=["P_PARTKEY"]) + filtered_parts = filtered_parts[["P_PARTKEY"]].drop_duplicates() + joined_parts = filtered_parts.merge( + partsupp, left_on="P_PARTKEY", right_on="PS_PARTKEY" + ) + + final_join = joined_parts.merge( + q1, left_on=["PS_SUPPKEY", "P_PARTKEY"], right_on=["L_SUPPKEY", "L_PARTKEY"] + ) + final_filtered = final_join[final_join["PS_AVAILQTY"] > final_join["SUM_QUANTITY"]] + + final_filtered = final_filtered[["PS_SUPPKEY"]] + if not session._strictly_ordered: + final_filtered = final_filtered.sort_values(by="PS_SUPPKEY") + final_filtered = final_filtered.drop_duplicates() + + final_result = final_filtered.merge(q3, left_on="PS_SUPPKEY", right_on="S_SUPPKEY") + final_result = final_result[["S_NAME", "S_ADDRESS"]].sort_values(by="S_NAME") + + final_result.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q21.py b/third_party/bigframes_vendored/tpch/queries/q21.py new file mode 100644 index 00000000000..3a4ea495c92 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q21.py @@ -0,0 +1,62 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/duckdb/q21.py + +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + nation = nation[nation["N_NAME"] == "SAUDI ARABIA"] + orders = orders[orders["O_ORDERSTATUS"] == "F"] + + l1 = lineitem[lineitem["L_RECEIPTDATE"] > lineitem["L_COMMITDATE"]][ + ["L_ORDERKEY", "L_SUPPKEY"] + ] + + l2 = lineitem.groupby("L_ORDERKEY", as_index=False).agg( + NUNIQUE_COL=bpd.NamedAgg(column="L_SUPPKEY", aggfunc="nunique") + ) + l2 = l2[l2["NUNIQUE_COL"] > 1][["L_ORDERKEY"]] + + l3 = l1.groupby("L_ORDERKEY", as_index=False).agg( + NUNIQUE_COL=bpd.NamedAgg(column="L_SUPPKEY", aggfunc="nunique") + ) + l3 = l3[l3["NUNIQUE_COL"] == 1][["L_ORDERKEY"]] + + l1 = l1.merge(l2, on="L_ORDERKEY", how="inner").merge( + l3, on="L_ORDERKEY", how="inner" + ) + + merged = supplier.merge(nation, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + merged = merged.merge(l1, left_on="S_SUPPKEY", right_on="L_SUPPKEY") + merged = merged.merge(orders, left_on="L_ORDERKEY", right_on="O_ORDERKEY") + + result = merged.groupby("S_NAME", as_index=False).agg( + NUMWAIT=bpd.NamedAgg(column="L_SUPPKEY", aggfunc="size") + ) + + result = ( + typing.cast(bpd.DataFrame, result) + .sort_values(["NUMWAIT", "S_NAME"], ascending=[False, True]) + .head(100) + ) + + result.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q22.py b/third_party/bigframes_vendored/tpch/queries/q22.py new file mode 100644 index 00000000000..97180cd11a8 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q22.py @@ -0,0 +1,52 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q22.py + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + country_codes = ["13", "31", "23", "29", "30", "18", "17"] + + customer["CNTRYCODE"] = customer["C_PHONE"].str.slice(0, 2) + + avg_acctbal = customer[ + (customer["CNTRYCODE"].isin(country_codes)) & (customer["C_ACCTBAL"] > 0) + ]["C_ACCTBAL"].mean() + + if not session._strictly_ordered: + orders = orders.sort_values(by="O_CUSTKEY") + orders_unique = orders.drop_duplicates(subset=["O_CUSTKEY"]) + + matched_customers = customer.merge( + orders_unique, left_on="C_CUSTKEY", right_on="O_CUSTKEY" + ) + matched_customers["IS_IN_ORDERS"] = True + + customer = customer.merge( + matched_customers[["C_CUSTKEY", "IS_IN_ORDERS"]], on="C_CUSTKEY", how="left" + ) + customer["IS_IN_ORDERS"] = customer["IS_IN_ORDERS"].fillna(False) + + filtered_customers = customer[ + (customer["CNTRYCODE"].isin(country_codes)) + & (customer["C_ACCTBAL"] > avg_acctbal) + & (~customer["IS_IN_ORDERS"]) + ] + + result = filtered_customers.groupby("CNTRYCODE", as_index=False).agg( + NUMCUST=bpd.NamedAgg(column="C_CUSTKEY", aggfunc="count"), + TOTACCTBAL=bpd.NamedAgg(column="C_ACCTBAL", aggfunc="sum"), + ) + + result = result.sort_values(by="CNTRYCODE") + + result.to_gbq() From 2f6cd9f56a780a75c9d472187334593ce5683a66 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:31:13 -0700 Subject: [PATCH 789/921] chore: add tpch q4-8 (#926) * chore: add tpch q4-8 * remove 10t test --- tests/benchmark/tpch/config.jsonl | 2 - tests/benchmark/tpch/q4.py | 25 ++++++ tests/benchmark/tpch/q5.py | 25 ++++++ tests/benchmark/tpch/q6.py | 25 ++++++ tests/benchmark/tpch/q7.py | 25 ++++++ tests/benchmark/tpch/q8.py | 25 ++++++ .../bigframes_vendored/tpch/queries/q4.py | 38 +++++++++ .../bigframes_vendored/tpch/queries/q5.py | 55 +++++++++++++ .../bigframes_vendored/tpch/queries/q6.py | 30 +++++++ .../bigframes_vendored/tpch/queries/q7.py | 79 +++++++++++++++++++ .../bigframes_vendored/tpch/queries/q8.py | 78 ++++++++++++++++++ 11 files changed, 405 insertions(+), 2 deletions(-) create mode 100644 tests/benchmark/tpch/q4.py create mode 100644 tests/benchmark/tpch/q5.py create mode 100644 tests/benchmark/tpch/q6.py create mode 100644 tests/benchmark/tpch/q7.py create mode 100644 tests/benchmark/tpch/q8.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q4.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q5.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q6.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q7.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q8.py diff --git a/tests/benchmark/tpch/config.jsonl b/tests/benchmark/tpch/config.jsonl index 151100594ad..1c8b4dd19ae 100644 --- a/tests/benchmark/tpch/config.jsonl +++ b/tests/benchmark/tpch/config.jsonl @@ -6,5 +6,3 @@ {"benchmark_suffix": "100g_unordered", "dataset_id": "tpch_0100g", "ordered": false} {"benchmark_suffix": "1t_ordered", "dataset_id": "tpch_0001t", "ordered": true} {"benchmark_suffix": "1t_unordered", "dataset_id": "tpch_0001t", "ordered": false} -{"benchmark_suffix": "10t_ordered", "dataset_id": "tpch_0010t", "ordered": true} -{"benchmark_suffix": "10t_unordered", "dataset_id": "tpch_0010t", "ordered": false} diff --git a/tests/benchmark/tpch/q4.py b/tests/benchmark/tpch/q4.py new file mode 100644 index 00000000000..aa67cc77a07 --- /dev/null +++ b/tests/benchmark/tpch/q4.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q4 as vendored_tpch_q4 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q4.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q5.py b/tests/benchmark/tpch/q5.py new file mode 100644 index 00000000000..e4b3cb0f51c --- /dev/null +++ b/tests/benchmark/tpch/q5.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q1 as vendored_tpch_q5 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q5.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q6.py b/tests/benchmark/tpch/q6.py new file mode 100644 index 00000000000..a1933330454 --- /dev/null +++ b/tests/benchmark/tpch/q6.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q6 as vendored_tpch_q6 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q6.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q7.py b/tests/benchmark/tpch/q7.py new file mode 100644 index 00000000000..8a17eb91eab --- /dev/null +++ b/tests/benchmark/tpch/q7.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q7 as vendored_tpch_q7 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q7.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q8.py b/tests/benchmark/tpch/q8.py new file mode 100644 index 00000000000..b5e7d7aa376 --- /dev/null +++ b/tests/benchmark/tpch/q8.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q8 as vendored_tpch_q8 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q8.q, current_path, suffix, dataset_id, session + ) diff --git a/third_party/bigframes_vendored/tpch/queries/q4.py b/third_party/bigframes_vendored/tpch/queries/q4.py new file mode 100644 index 00000000000..9cc6f73c989 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q4.py @@ -0,0 +1,38 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q4.py + + +from datetime import date +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = date(1993, 7, 1) + var2 = date(1993, 10, 1) + + jn = lineitem.merge(orders, left_on="L_ORDERKEY", right_on="O_ORDERKEY") + + jn = jn[(jn["O_ORDERDATE"] >= var1) & (jn["O_ORDERDATE"] < var2)] + jn = jn[jn["L_COMMITDATE"] < jn["L_RECEIPTDATE"]] + + if not session._strictly_ordered: + jn = jn.sort_values(by=["O_ORDERPRIORITY", "L_ORDERKEY"]) + + jn = jn.drop_duplicates(subset=["O_ORDERPRIORITY", "L_ORDERKEY"]) + + gb = jn.groupby("O_ORDERPRIORITY", as_index=False) + agg = gb.agg(ORDER_COUNT=bpd.NamedAgg(column="L_ORDERKEY", aggfunc="count")) + + result_df = typing.cast(bpd.DataFrame, agg).sort_values(["O_ORDERPRIORITY"]) + result_df.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q5.py b/third_party/bigframes_vendored/tpch/queries/q5.py new file mode 100644 index 00000000000..20bd8d9c441 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q5.py @@ -0,0 +1,55 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q5.py + +from datetime import date + +import bigframes + + +def q(dataset_id: str, session: bigframes.Session): + region = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.REGION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERES", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = "ASIA" + var2 = date(1994, 1, 1) + var3 = date(1995, 1, 1) + + jn1 = region.merge(nation, left_on="R_REGIONKEY", right_on="N_REGIONKEY") + jn2 = jn1.merge(customer, left_on="N_NATIONKEY", right_on="C_NATIONKEY") + jn3 = jn2.merge(orders, left_on="C_CUSTKEY", right_on="O_CUSTKEY") + jn4 = jn3.merge(lineitem, left_on="O_ORDERKEY", right_on="L_ORDERKEY") + jn5 = jn4.merge( + supplier, + left_on=["L_SUPPKEY", "N_NATIONKEY"], + right_on=["S_SUPPKEY", "S_NATIONKEY"], + ) + + jn5 = jn5[jn5["R_NAME"] == var1] + jn5 = jn5[(jn5["O_ORDERDATE"] >= var2) & (jn5["O_ORDERDATE"] < var3)] + jn5["REVENUE"] = jn5["L_EXTENDEDPRICE"] * (1.0 - jn5["L_DISCOUNT"]) + + gb = jn5.groupby("N_NAME", as_index=False)["REVENUE"].sum() + result_df = gb.sort_values("REVENUE", ascending=False) + + result_df.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q6.py b/third_party/bigframes_vendored/tpch/queries/q6.py new file mode 100644 index 00000000000..13341c4f4ed --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q6.py @@ -0,0 +1,30 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q6.py + +from datetime import date + +import bigframes + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = date(1994, 1, 1) + var2 = date(1995, 1, 1) + var3 = 0.05 + var4 = 0.07 + var5 = 24 + + filt = lineitem[(lineitem["L_SHIPDATE"] >= var1) & (lineitem["L_SHIPDATE"] < var2)] + filt = filt[(filt["L_DISCOUNT"] >= var3) & (filt["L_DISCOUNT"] <= var4)] + filt = filt[filt["L_QUANTITY"] < var5] + result_df = ( + (filt["L_EXTENDEDPRICE"] * filt["L_DISCOUNT"]) + .agg(["sum"]) + .rename("REVENUE") + .to_frame() + ) + + result_df.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q7.py b/third_party/bigframes_vendored/tpch/queries/q7.py new file mode 100644 index 00000000000..4ea5e6b2387 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q7.py @@ -0,0 +1,79 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q7.py + +from datetime import date +import typing + +import bigframes +import bigframes.dataframe +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = "FRANCE" + var2 = "GERMANY" + var3 = date(1995, 1, 1) + var4 = date(1996, 12, 31) + + n1 = nation[(nation["N_NAME"] == var1)] + n2 = nation[(nation["N_NAME"] == var2)] + + jn1 = customer.merge(n1, left_on="C_NATIONKEY", right_on="N_NATIONKEY") + jn2 = jn1.merge(orders, left_on="C_CUSTKEY", right_on="O_CUSTKEY") + jn2 = jn2.rename(columns={"N_NAME": "CUST_NATION"}) + jn3 = jn2.merge(lineitem, left_on="O_ORDERKEY", right_on="L_ORDERKEY") + jn4 = jn3.merge(supplier, left_on="L_SUPPKEY", right_on="S_SUPPKEY") + jn5 = jn4.merge(n2, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + df1 = jn5.rename(columns={"N_NAME": "SUPP_NATION"}) + + jn1 = customer.merge(n2, left_on="C_NATIONKEY", right_on="N_NATIONKEY") + jn2 = jn1.merge(orders, left_on="C_CUSTKEY", right_on="O_CUSTKEY") + jn2 = jn2.rename(columns={"N_NAME": "CUST_NATION"}) + jn3 = jn2.merge(lineitem, left_on="O_ORDERKEY", right_on="L_ORDERKEY") + jn4 = jn3.merge(supplier, left_on="L_SUPPKEY", right_on="S_SUPPKEY") + jn5 = jn4.merge(n1, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + df2 = jn5.rename(columns={"N_NAME": "SUPP_NATION"}) + + total = bpd.concat([df1, df2]) + + # TODO(huanc): TEMPORARY CODE to force a fresh start. Currently, + # combining everything into a single query seems to trigger a bug + # causing incorrect results. This workaround involves writing to and + # then reading from BigQuery. Remove this once b/355714291 is + # resolved. + dest = total.to_gbq() + total = bpd.read_gbq(dest) + + total = total[(total["L_SHIPDATE"] >= var3) & (total["L_SHIPDATE"] <= var4)] + total["VOLUME"] = total["L_EXTENDEDPRICE"] * (1.0 - total["L_DISCOUNT"]) + total["L_YEAR"] = total["L_SHIPDATE"].dt.year + + gb = typing.cast(bpd.DataFrame, total).groupby( + ["SUPP_NATION", "CUST_NATION", "L_YEAR"], as_index=False + ) + agg = gb.agg(REVENUE=bpd.NamedAgg(column="VOLUME", aggfunc="sum")) + + result_df = typing.cast(bpd.DataFrame, agg).sort_values( + ["SUPP_NATION", "CUST_NATION", "L_YEAR"] + ) + result_df.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q8.py b/third_party/bigframes_vendored/tpch/queries/q8.py new file mode 100644 index 00000000000..4520fce14af --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q8.py @@ -0,0 +1,78 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/pandas/q8.py + +from datetime import date + +import bigframes + + +def q(dataset_id: str, session: bigframes.Session): + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + nation = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.NATION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + region = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.REGION", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = "BRAZIL" + var2 = "AMERICA" + var3 = "ECONOMY ANODIZED STEEL" + var4 = date(1995, 1, 1) + var5 = date(1996, 12, 31) + + n1 = nation[["N_NATIONKEY", "N_REGIONKEY"]] + n2 = nation[["N_NATIONKEY", "N_NAME"]] + + jn1 = part.merge(lineitem, left_on="P_PARTKEY", right_on="L_PARTKEY") + jn2 = jn1.merge(supplier, left_on="L_SUPPKEY", right_on="S_SUPPKEY") + jn3 = jn2.merge(orders, left_on="L_ORDERKEY", right_on="O_ORDERKEY") + jn4 = jn3.merge(customer, left_on="O_CUSTKEY", right_on="C_CUSTKEY") + jn5 = jn4.merge(n1, left_on="C_NATIONKEY", right_on="N_NATIONKEY") + jn6 = jn5.merge(region, left_on="N_REGIONKEY", right_on="R_REGIONKEY") + + jn6 = jn6[(jn6["R_NAME"] == var2)] + + jn7 = jn6.merge(n2, left_on="S_NATIONKEY", right_on="N_NATIONKEY") + + jn7 = jn7[(jn7["O_ORDERDATE"] >= var4) & (jn7["O_ORDERDATE"] <= var5)] + jn7 = jn7[jn7["P_TYPE"] == var3] + + jn7["O_YEAR"] = jn7["O_ORDERDATE"].dt.year + jn7["VOLUME"] = jn7["L_EXTENDEDPRICE"] * (1.0 - jn7["L_DISCOUNT"]) + jn7 = jn7.rename(columns={"N_NAME": "NATION"}) + + denominator = jn7.groupby("O_YEAR")["VOLUME"].sum().rename("DENOMINATOR") + numerator = ( + jn7[jn7["NATION"] == var1] + .groupby(jn7["O_YEAR"])["VOLUME"] + .sum() + .rename("NUMERATOR") + ) + jn8 = denominator.to_frame().join(numerator.to_frame(), how="left") + + # ValueError: Caching with offsets only supported in strictly ordered mode. + jn8["MKT_SHARE"] = (jn8["NUMERATOR"] / jn8["DENOMINATOR"]).round(2) + + result_df = jn8["MKT_SHARE"].sort_index().rename("MKT_SHARE").reset_index() + result_df.to_gbq() From 04139644af0c30c10519397fb3be7bebf6ec813c Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:31:25 -0700 Subject: [PATCH 790/921] chore: add tpch q14-18 (#928) --- tests/benchmark/tpch/q14.py | 25 +++++++++ tests/benchmark/tpch/q15.py | 25 +++++++++ tests/benchmark/tpch/q16.py | 25 +++++++++ tests/benchmark/tpch/q17.py | 25 +++++++++ tests/benchmark/tpch/q18.py | 25 +++++++++ .../bigframes_vendored/tpch/queries/q14.py | 34 +++++++++++++ .../bigframes_vendored/tpch/queries/q15.py | 48 +++++++++++++++++ .../bigframes_vendored/tpch/queries/q16.py | 44 ++++++++++++++++ .../bigframes_vendored/tpch/queries/q17.py | 40 +++++++++++++++ .../bigframes_vendored/tpch/queries/q18.py | 51 +++++++++++++++++++ 10 files changed, 342 insertions(+) create mode 100644 tests/benchmark/tpch/q14.py create mode 100644 tests/benchmark/tpch/q15.py create mode 100644 tests/benchmark/tpch/q16.py create mode 100644 tests/benchmark/tpch/q17.py create mode 100644 tests/benchmark/tpch/q18.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q14.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q15.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q16.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q17.py create mode 100644 third_party/bigframes_vendored/tpch/queries/q18.py diff --git a/tests/benchmark/tpch/q14.py b/tests/benchmark/tpch/q14.py new file mode 100644 index 00000000000..8aa7ed4d2ea --- /dev/null +++ b/tests/benchmark/tpch/q14.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q14 as vendored_tpch_q14 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q14.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q15.py b/tests/benchmark/tpch/q15.py new file mode 100644 index 00000000000..511cfbc683b --- /dev/null +++ b/tests/benchmark/tpch/q15.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q15 as vendored_tpch_q15 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q15.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q16.py b/tests/benchmark/tpch/q16.py new file mode 100644 index 00000000000..1d1f4b5f307 --- /dev/null +++ b/tests/benchmark/tpch/q16.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q16 as vendored_tpch_q16 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q16.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q17.py b/tests/benchmark/tpch/q17.py new file mode 100644 index 00000000000..e285cc9fca2 --- /dev/null +++ b/tests/benchmark/tpch/q17.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q17 as vendored_tpch_q17 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q17.q, current_path, suffix, dataset_id, session + ) diff --git a/tests/benchmark/tpch/q18.py b/tests/benchmark/tpch/q18.py new file mode 100644 index 00000000000..de9e4f2f94f --- /dev/null +++ b/tests/benchmark/tpch/q18.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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 pathlib + +import benchmark.utils as utils +import bigframes_vendored.tpch.queries.q18 as vendored_tpch_q18 + +if __name__ == "__main__": + dataset_id, session, suffix = utils.get_tpch_configuration() + current_path = pathlib.Path(__file__).absolute() + + utils.get_execution_time( + vendored_tpch_q18.q, current_path, suffix, dataset_id, session + ) diff --git a/third_party/bigframes_vendored/tpch/queries/q14.py b/third_party/bigframes_vendored/tpch/queries/q14.py new file mode 100644 index 00000000000..8c25a5897dc --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q14.py @@ -0,0 +1,34 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q14.py + +from datetime import date + +import bigframes + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = date(1995, 9, 1) + var2 = date(1995, 10, 1) + + merged = lineitem.merge(part, left_on="L_PARTKEY", right_on="P_PARTKEY") + + filtered = merged[(merged["L_SHIPDATE"] >= var1) & (merged["L_SHIPDATE"] < var2)] + + filtered["CONDI_REVENUE"] = ( + filtered["L_EXTENDEDPRICE"] * (1 - filtered["L_DISCOUNT"]) + ) * filtered["P_TYPE"].str.contains("PROMO").astype("Int64") + + total_revenue = (filtered["L_EXTENDEDPRICE"] * (1 - filtered["L_DISCOUNT"])).sum() + promo_revenue = filtered["CONDI_REVENUE"].sum() + + promo_revenue_percent = 100.00 * promo_revenue / total_revenue + + _ = round(promo_revenue_percent, 2) diff --git a/third_party/bigframes_vendored/tpch/queries/q15.py b/third_party/bigframes_vendored/tpch/queries/q15.py new file mode 100644 index 00000000000..fae3010e366 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q15.py @@ -0,0 +1,48 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q15.py + +from datetime import date + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = date(1996, 1, 1) + var2 = date(1996, 4, 1) + + filtered_lineitem = lineitem[ + (lineitem["L_SHIPDATE"] >= var1) & (lineitem["L_SHIPDATE"] < var2) + ] + filtered_lineitem["REVENUE"] = filtered_lineitem["L_EXTENDEDPRICE"] * ( + 1 - filtered_lineitem["L_DISCOUNT"] + ) + + grouped_revenue = ( + filtered_lineitem.groupby("L_SUPPKEY", as_index=False) + .agg(TOTAL_REVENUE=bpd.NamedAgg(column="REVENUE", aggfunc="sum")) + .rename(columns={"L_SUPPKEY": "SUPPLIER_NO"}) + ) + + joined_data = bpd.merge( + supplier, grouped_revenue, left_on="S_SUPPKEY", right_on="SUPPLIER_NO" + ) + + max_revenue = joined_data["TOTAL_REVENUE"].max() + max_revenue_suppliers = joined_data[joined_data["TOTAL_REVENUE"] == max_revenue] + + max_revenue_suppliers["TOTAL_REVENUE"] = max_revenue_suppliers[ + "TOTAL_REVENUE" + ].round(2) + q_final = max_revenue_suppliers[ + ["S_SUPPKEY", "S_NAME", "S_ADDRESS", "S_PHONE", "TOTAL_REVENUE"] + ].sort_values("S_SUPPKEY") + q_final.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q16.py b/third_party/bigframes_vendored/tpch/queries/q16.py new file mode 100644 index 00000000000..b5eb62547f0 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q16.py @@ -0,0 +1,44 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q16.py + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + partsupp = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + supplier = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = "Brand#45" + + supplier = supplier[ + supplier["S_COMMENT"].str.contains("Customer.*Complaints", regex=True) + ]["S_SUPPKEY"] + + q_filtered = part.merge(partsupp, left_on="P_PARTKEY", right_on="PS_PARTKEY") + q_filtered = q_filtered[q_filtered["P_BRAND"] != var1] + q_filtered = q_filtered[~q_filtered["P_TYPE"].str.contains("MEDIUM POLISHED")] + q_filtered = q_filtered[q_filtered["P_SIZE"].isin([49, 14, 23, 45, 19, 3, 36, 9])] + + final_df = q_filtered[~q_filtered["PS_SUPPKEY"].isin(supplier)] + + grouped = final_df.groupby(["P_BRAND", "P_TYPE", "P_SIZE"], as_index=False) + result = grouped.agg( + SUPPLIER_CNT=bpd.NamedAgg(column="PS_SUPPKEY", aggfunc="nunique") + ) + + q_final = result.sort_values( + by=["SUPPLIER_CNT", "P_BRAND", "P_TYPE", "P_SIZE"], + ascending=[False, True, True, True], + ) + + q_final.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q17.py b/third_party/bigframes_vendored/tpch/queries/q17.py new file mode 100644 index 00000000000..a95d128b5c4 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q17.py @@ -0,0 +1,40 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q17.py + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + part = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.PART", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + VAR1 = "Brand#23" + VAR2 = "MED BOX" + + filtered_part = part[(part["P_BRAND"] == VAR1) & (part["P_CONTAINER"] == VAR2)] + q1 = bpd.merge( + filtered_part, lineitem, how="left", left_on="P_PARTKEY", right_on="L_PARTKEY" + ) + + grouped = ( + q1.groupby("P_PARTKEY", as_index=False) + .agg(AVG_QUANTITY=bpd.NamedAgg(column="L_QUANTITY", aggfunc="mean")) + .rename(columns={"P_PARTKEY": "KEY"}) + ) + grouped["AVG_QUANTITY"] = grouped["AVG_QUANTITY"] * 0.2 + + q_final = bpd.merge(grouped, q1, left_on="KEY", right_on="P_PARTKEY") + + q_final = q_final[q_final["L_QUANTITY"] < q_final["AVG_QUANTITY"]] + + q_final = bpd.DataFrame( + {"AVG_YEARLY": [(q_final["L_EXTENDEDPRICE"].sum() / 7.0).round(2)]} + ) + + q_final.to_gbq() diff --git a/third_party/bigframes_vendored/tpch/queries/q18.py b/third_party/bigframes_vendored/tpch/queries/q18.py new file mode 100644 index 00000000000..dac9f5c4384 --- /dev/null +++ b/third_party/bigframes_vendored/tpch/queries/q18.py @@ -0,0 +1,51 @@ +# Contains code from https://github.com/pola-rs/tpch/blob/main/queries/polars/q18.py + +import typing + +import bigframes +import bigframes.pandas as bpd + + +def q(dataset_id: str, session: bigframes.Session): + customer = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + lineitem = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.LINEITEM", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + orders = session.read_gbq( + f"bigframes-dev-perf.{dataset_id}.ORDERS", + index_col=bigframes.enums.DefaultIndexKind.NULL, + ) + + var1 = 300 + + q1 = lineitem.groupby("L_ORDERKEY", as_index=False).agg( + SUM_QUANTITY=bpd.NamedAgg(column="L_QUANTITY", aggfunc="sum") + ) + q1 = q1[q1["SUM_QUANTITY"] > var1] + + filtered_orders = orders.merge( + q1, left_on="O_ORDERKEY", right_on="L_ORDERKEY", how="inner" + ) + + result = filtered_orders.merge( + lineitem, left_on="O_ORDERKEY", right_on="L_ORDERKEY" + ) + result = result.merge(customer, left_on="O_CUSTKEY", right_on="C_CUSTKEY") + + final_result = result.groupby( + ["C_NAME", "C_CUSTKEY", "O_ORDERKEY", "O_ORDERDATE", "O_TOTALPRICE"], + as_index=False, + ).agg(COL6=bpd.NamedAgg(column="L_QUANTITY", aggfunc="sum")) + + final_result = final_result.rename(columns={"O_ORDERDATE": "O_ORDERDAT"}) + + final_result = typing.cast(bpd.DataFrame, final_result).sort_values( + ["O_TOTALPRICE", "O_ORDERDAT"], ascending=[False, True] + ) + + q_final = final_result.head(100) + q_final.to_gbq() From 7ed39629c638874d8e9cc3c7a9b3ec92ad480eca Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:22:04 -0700 Subject: [PATCH 791/921] deps: Update minimum support to Pandas 1.5.3 and Pyarrow 10.0.1 (#903) * deps: Update mininum support to Pandas 1.5.3 and Pyarrow 10.0.1 * fixing tests --- setup.py | 5 ++--- testing/constraints-3.10.txt | 6 +++--- testing/constraints-3.9.txt | 4 ++-- tests/system/small/operations/test_strings.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 0e0be5fd775..2ae61a44f33 100644 --- a/setup.py +++ b/setup.py @@ -50,9 +50,8 @@ "ibis-framework[bigquery] >=8.0.0,<9.0.0dev", "jellyfish >=0.8.9", "numpy >=1.24.0", - # TODO: Relax upper bound once we have fixed `system_prerelease` tests. - "pandas >=1.5.0", - "pyarrow >=8.0.0", + "pandas >=1.5.3", + "pyarrow >=10.0.1", "pydata-google-auth >=1.8.2", "requests >=2.27.1", "scikit-learn >=1.2.2", diff --git a/testing/constraints-3.10.txt b/testing/constraints-3.10.txt index 5782b03a2f3..b11ab5a88db 100644 --- a/testing/constraints-3.10.txt +++ b/testing/constraints-3.10.txt @@ -3,10 +3,10 @@ google-auth==2.27.0 ipykernel==5.5.6 ipython==7.34.0 notebook==6.5.5 -pandas==2.0.3 -pandas-stubs==2.0.3.230814 +pandas==2.1.4 +pandas-stubs==2.1.4.231227 portpicker==1.5.2 -requests==2.31.0 +requests==2.32.3 tornado==6.3.3 absl-py==1.4.0 debugpy==1.6.6 diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index 0d3f16e95f9..a446a4bfa77 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -15,8 +15,8 @@ google-cloud-storage==2.0.0 ibis-framework==8.0.0 jellyfish==0.8.9 numpy==1.24.0 -pandas==1.5.0 -pyarrow==8.0.0 +pandas==1.5.3 +pyarrow==10.0.1 pydata-google-auth==1.8.2 requests==2.27.1 scikit-learn==1.2.2 diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 3191adf9206..15e8512317e 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -634,7 +634,7 @@ def test_getitem_w_array(index): def test_getitem_w_struct_array(): - if packaging.version.Version(pd.__version__) <= packaging.version.Version("1.5.0"): + if packaging.version.Version(pd.__version__) <= packaging.version.Version("1.5.3"): pytest.skip("https://github.com/googleapis/python-bigquery/issues/1992") pa_struct = pa.struct( From 7472a11e858e77efe18841d1f74b8769464459f0 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:14:45 -0700 Subject: [PATCH 792/921] test: add CSV sample data file for testing (#938) * test: add CSV sample data file for testing --- tests/data/people.csv | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/data/people.csv diff --git a/tests/data/people.csv b/tests/data/people.csv new file mode 100644 index 00000000000..f5f9998b82c --- /dev/null +++ b/tests/data/people.csv @@ -0,0 +1,4 @@ +Name,Age,City +Alice,25,New York +Bob,30,London +Charlie,22,Paris From 46f2dd79f59131bbb98fe4ae3780b98cb4d50646 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 30 Aug 2024 15:28:25 -0700 Subject: [PATCH 793/921] perf: Improve repr performance (#918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: Improve repr performance * extract gbq metadata from nodes to common struct * clarify fast head * fix physical_schema to be bq client types * add classmethod annotation to GbqTable struct factory method * add classmethod annotation to GbqTable struct factory method --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/core/__init__.py | 28 +------ bigframes/core/blocks.py | 26 +++---- bigframes/core/compile/compiler.py | 12 ++- bigframes/core/nodes.py | 120 +++++++++++++++++++---------- bigframes/core/tree_properties.py | 34 +++++++- bigframes/session/executor.py | 116 +++++++++++++++++++++++----- tests/unit/core/test_blocks.py | 7 +- 7 files changed, 238 insertions(+), 105 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 2e9b5fa994a..f3c75f7143e 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -70,23 +70,7 @@ def from_pyarrow(cls, arrow_table: pa.Table, session: Session): iobytes.getvalue(), data_schema=schema, session=session, - ) - return cls(node) - - @classmethod - def from_cached( - cls, - original: ArrayValue, - table: google.cloud.bigquery.Table, - ordering: orderings.TotalOrdering, - ): - node = nodes.CachedTableNode( - original_node=original.node, - project_id=table.reference.project, - dataset_id=table.reference.dataset_id, - table_id=table.reference.table_id, - physical_schema=tuple(table.schema), - ordering=ordering, + n_rows=arrow_table.num_rows, ) return cls(node) @@ -110,10 +94,7 @@ def from_table( bigframes.exceptions.PreviewWarning, ) node = nodes.ReadTableNode( - project_id=table.reference.project, - dataset_id=table.reference.dataset_id, - table_id=table.reference.table_id, - physical_schema=tuple(table.schema), + table=nodes.GbqTable.from_table(table), total_order_cols=(offsets_col,) if offsets_col else tuple(primary_key), order_col_is_sequential=(offsets_col is not None), columns=schema, @@ -154,10 +135,7 @@ def as_cached( """ node = nodes.CachedTableNode( original_node=self.node, - project_id=cache_table.reference.project, - dataset_id=cache_table.reference.dataset_id, - table_id=cache_table.reference.table_id, - physical_schema=tuple(cache_table.schema), + table=nodes.GbqTable.from_table(cache_table), ordering=ordering, ) return ArrayValue(node) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index d89d35516e5..25dec808ddf 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -200,6 +200,7 @@ def index(self) -> BlockIndexProperties: @functools.cached_property def shape(self) -> typing.Tuple[int, int]: """Returns dimensions as (length, width) tuple.""" + row_count_expr = self.expr.row_count() # Support in-memory engines for hermetic unit tests. @@ -210,8 +211,7 @@ def shape(self) -> typing.Tuple[int, int]: except Exception: pass - iter, _ = self.session._execute(row_count_expr, ordered=False) - row_count = next(iter)[0] + row_count = self.session._executor.get_row_count(self.expr) return (row_count, len(self.value_columns)) @property @@ -560,7 +560,7 @@ def to_pandas( def try_peek( self, n: int = 20, force: bool = False ) -> typing.Optional[pd.DataFrame]: - if force or tree_properties.peekable(self.expr.node): + if force or tree_properties.can_fast_peek(self.expr.node): iterator, _ = self.session._peek(self.expr, n) df = self._to_dataframe(iterator) self._copy_index_to_pandas(df) @@ -1587,19 +1587,13 @@ def retrieve_repr_request_results( Returns a tuple of the dataframe and the overall number of rows of the query. """ - # TODO(swast): Select a subset of columns if max_columns is less than the - # number of columns in the schema. - count = self.shape[0] - if count > max_results: - head_block = self.slice(0, max_results) - else: - head_block = self - computed_df, query_job = head_block.to_pandas() - formatted_df = computed_df.set_axis(self.column_labels, axis=1) - # we reset the axis and substitute the bf index name(s) for the default - if len(self.index.names) > 0: - formatted_df.index.names = self.index.names # type: ignore - return formatted_df, count, query_job + + results, query_job = self.session._executor.head(self.expr, max_results) + count = self.session._executor.get_row_count(self.expr) + + computed_df = self._to_dataframe(results) + self._copy_index_to_pandas(computed_df) + return computed_df, count, query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: result_id = guid.generate_guid() diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 8fb1f7ab3a3..3fedf5c0c80 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -103,14 +103,16 @@ def compile_readlocal(self, node: nodes.ReadLocalNode, ordered: bool = True): @_compile_node.register def compile_cached_table(self, node: nodes.CachedTableNode, ordered: bool = True): - full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" + full_table_name = ( + f"{node.table.project_id}.{node.table.dataset_id}.{node.table.table_id}" + ) used_columns = ( *node.schema.names, *node.hidden_columns, ) # Physical schema might include unused columns, unsupported datatypes like JSON physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( - list(i for i in node.physical_schema if i.name in used_columns) + list(i for i in node.table.physical_schema if i.name in used_columns) ) ibis_table = ibis.table(physical_schema, full_table_name) if ordered: @@ -156,14 +158,16 @@ def compile_readtable(self, node: nodes.ReadTableNode, ordered: bool = True): def read_table_as_unordered_ibis( self, node: nodes.ReadTableNode ) -> ibis.expr.types.Table: - full_table_name = f"{node.project_id}.{node.dataset_id}.{node.table_id}" + full_table_name = ( + f"{node.table.project_id}.{node.table.dataset_id}.{node.table.table_id}" + ) used_columns = ( *node.schema.names, *[i for i in node.total_order_cols if i not in node.schema.names], ) # Physical schema might include unused columns, unsupported datatypes like JSON physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( - list(i for i in node.physical_schema if i.name in used_columns) + list(i for i in node.table.physical_schema if i.name in used_columns) ) if node.at_time is not None or node.sql_predicate is not None: import bigframes.session._io.bigquery diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 30edc7740a6..73780719a92 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -312,18 +312,36 @@ def transform_children( # Input Nodex @dataclass(frozen=True) -class ReadLocalNode(BigFrameNode): +class LeafNode(BigFrameNode): + @property + def roots(self) -> typing.Set[BigFrameNode]: + return {self} + + @property + def supports_fast_head(self) -> bool: + return False + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return self + + @property + def row_count(self) -> typing.Optional[int]: + """How many rows are in the data source. None means unknown.""" + return None + + +@dataclass(frozen=True) +class ReadLocalNode(LeafNode): feather_bytes: bytes data_schema: schemata.ArraySchema + n_rows: int session: typing.Optional[bigframes.session.Session] = None def __hash__(self): return self._node_hash - @property - def roots(self) -> typing.Set[BigFrameNode]: - return {self} - @functools.cached_property def schema(self) -> schemata.ArraySchema: return self.data_schema @@ -333,6 +351,10 @@ def variables_introduced(self) -> int: """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" return len(self.schema.items) + 1 + @property + def supports_fast_head(self) -> bool: + return True + @property def order_ambiguous(self) -> bool: return False @@ -341,20 +363,38 @@ def order_ambiguous(self) -> bool: def explicitly_ordered(self) -> bool: return True - def transform_children( - self, t: Callable[[BigFrameNode], BigFrameNode] - ) -> BigFrameNode: - return self + @property + def row_count(self) -> typing.Optional[int]: + return self.n_rows -## Put ordering in here or just add order_by node above? @dataclass(frozen=True) -class ReadTableNode(BigFrameNode): +class GbqTable: project_id: str = field() dataset_id: str = field() table_id: str = field() - physical_schema: Tuple[bq.SchemaField, ...] = field() + n_rows: int = field() + cluster_cols: typing.Optional[Tuple[str, ...]] + + @staticmethod + def from_table(table: bq.Table) -> GbqTable: + return GbqTable( + project_id=table.project, + dataset_id=table.dataset_id, + table_id=table.table_id, + physical_schema=tuple(table.schema), + n_rows=table.num_rows, + cluster_cols=None + if table.clustering_fields is None + else tuple(table.clustering_fields), + ) + + +## Put ordering in here or just add order_by node above? +@dataclass(frozen=True) +class ReadTableNode(LeafNode): + table: GbqTable # Subset of physical schema columns, with chosen BQ types columns: schemata.ArraySchema = field() @@ -370,10 +410,10 @@ class ReadTableNode(BigFrameNode): def __post_init__(self): # enforce invariants - physical_names = set(map(lambda i: i.name, self.physical_schema)) + physical_names = set(map(lambda i: i.name, self.table.physical_schema)) if not set(self.columns.names).issubset(physical_names): raise ValueError( - f"Requested schema {self.columns} cannot be derived from table schemal {self.physical_schema}" + f"Requested schema {self.columns} cannot be derived from table schemal {self.table.physical_schema}" ) if self.order_col_is_sequential and len(self.total_order_cols) != 1: raise ValueError("Sequential primary key must have only one component") @@ -385,10 +425,6 @@ def session(self): def __hash__(self): return self._node_hash - @property - def roots(self) -> typing.Set[BigFrameNode]: - return {self} - @property def schema(self) -> schemata.ArraySchema: return self.columns @@ -398,6 +434,13 @@ def relation_ops_created(self) -> int: # Assume worst case, where readgbq actually has baked in analytic operation to generate index return 3 + @property + def supports_fast_head(self) -> bool: + # Fast head is only supported when row offsets are available. + # In the future, ORDER BY+LIMIT optimizations may allow fast head when + # clustered and/or partitioned on ordering key + return self.order_col_is_sequential + @property def order_ambiguous(self) -> bool: return len(self.total_order_cols) == 0 @@ -410,37 +453,34 @@ def explicitly_ordered(self) -> bool: def variables_introduced(self) -> int: return len(self.schema.items) + 1 - def transform_children( - self, t: Callable[[BigFrameNode], BigFrameNode] - ) -> BigFrameNode: - return self + @property + def row_count(self) -> typing.Optional[int]: + if self.sql_predicate is None: + return self.table.n_rows + return None # This node shouldn't be used in the "original" expression tree, only used as replacement for original during planning @dataclass(frozen=True) -class CachedTableNode(BigFrameNode): +class CachedTableNode(LeafNode): # The original BFET subtree that was cached # note: this isn't a "child" node. original_node: BigFrameNode = field() # reference to cached materialization of original_node - project_id: str = field() - dataset_id: str = field() - table_id: str = field() - physical_schema: Tuple[bq.SchemaField, ...] = field() - + table: GbqTable ordering: typing.Optional[orderings.RowOrdering] = field() def __post_init__(self): # enforce invariants - physical_names = set(map(lambda i: i.name, self.physical_schema)) + physical_names = set(map(lambda i: i.name, self.table.physical_schema)) logical_names = self.original_node.schema.names if not set(logical_names).issubset(physical_names): raise ValueError( - f"Requested schema {logical_names} cannot be derived from table schema {self.physical_schema}" + f"Requested schema {logical_names} cannot be derived from table schema {self.table.physical_schema}" ) if not set(self.hidden_columns).issubset(physical_names): raise ValueError( - f"Requested hidden columns {self.hidden_columns} cannot be derived from table schema {self.physical_schema}" + f"Requested hidden columns {self.hidden_columns} cannot be derived from table schema {self.table.physical_schema}" ) @property @@ -450,10 +490,6 @@ def session(self): def __hash__(self): return self._node_hash - @property - def roots(self) -> typing.Set[BigFrameNode]: - return {self} - @property def schema(self) -> schemata.ArraySchema: return self.original_node.schema @@ -473,6 +509,13 @@ def hidden_columns(self) -> typing.Tuple[str, ...]: if col not in self.schema.names ) + @property + def supports_fast_head(self) -> bool: + # Fast head is only supported when row offsets are available. + # In the future, ORDER BY+LIMIT optimizations may allow fast head when + # clustered and/or partitioned on ordering key + return (self.ordering is None) or self.ordering.is_sequential + @property def order_ambiguous(self) -> bool: return not isinstance(self.ordering, orderings.TotalOrdering) @@ -483,10 +526,9 @@ def explicitly_ordered(self) -> bool: self.ordering.all_ordering_columns ) > 0 - def transform_children( - self, t: Callable[[BigFrameNode], BigFrameNode] - ) -> BigFrameNode: - return self + @property + def row_count(self) -> typing.Optional[int]: + return self.table.n_rows # Unary nodes diff --git a/bigframes/core/tree_properties.py b/bigframes/core/tree_properties.py index 846cf50d779..4978e75e388 100644 --- a/bigframes/core/tree_properties.py +++ b/bigframes/core/tree_properties.py @@ -32,14 +32,44 @@ def local_only(node: nodes.BigFrameNode) -> bool: return all(isinstance(node, nodes.ReadLocalNode) for node in node.roots) -def peekable(node: nodes.BigFrameNode) -> bool: +def can_fast_peek(node: nodes.BigFrameNode) -> bool: if local_only(node): return True - children_peekable = all(peekable(child) for child in node.child_nodes) + children_peekable = all(can_fast_peek(child) for child in node.child_nodes) self_peekable = not node.non_local return children_peekable and self_peekable +def can_fast_head(node: nodes.BigFrameNode) -> bool: + """Can get head fast if can push head operator down to leafs and operators preserve rows.""" + if isinstance(node, nodes.LeafNode): + return node.supports_fast_head + if isinstance(node, nodes.UnaryNode): + return node.row_preserving and can_fast_head(node.child) + return False + + +def row_count(node: nodes.BigFrameNode) -> Optional[int]: + """Determine row count from local metadata, return None if unknown.""" + if isinstance(node, nodes.LeafNode): + return node.row_count + if isinstance(node, nodes.AggregateNode): + if len(node.by_column_ids) == 0: + return 1 + return None + if isinstance(node, nodes.ConcatNode): + sub_counts = list(map(row_count, node.child_nodes)) + total = 0 + for count in sub_counts: + if count is None: + return None + total += count + return total + if isinstance(node, nodes.UnaryNode) and node.row_preserving: + return row_count(node.child) + return None + + # Replace modified_cost(node) = cost(apply_cache(node)) def select_cache_target( root: nodes.BigFrameNode, diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 24bcd02798c..b696b77eb1f 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -25,11 +25,13 @@ import bigframes.core import bigframes.core.compile +import bigframes.core.expression as ex import bigframes.core.guid import bigframes.core.nodes as nodes import bigframes.core.ordering as order import bigframes.core.tree_properties as tree_properties import bigframes.formatting_helpers as formatting_helpers +import bigframes.operations as ops import bigframes.session._io.bigquery as bq_io import bigframes.session.metrics import bigframes.session.planner @@ -84,7 +86,7 @@ def to_sql( if offset_column: array_value = array_value.promote_offsets(offset_column) node = ( - self._with_cached_executions(array_value.node) + self._get_optimized_plan(array_value.node) if enable_cache else array_value.node ) @@ -190,20 +192,74 @@ def dry_run(self, array_value: bigframes.core.ArrayValue, ordered: bool = True): return results_iterator, query_job def peek( - self, array_value: bigframes.core.ArrayValue, n_rows: int + self, + array_value: bigframes.core.ArrayValue, + n_rows: int, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - """A 'peek' efficiently accesses a small number of rows in the dataframe.""" - if not tree_properties.peekable(self._with_cached_executions(array_value.node)): + """ + A 'peek' efficiently accesses a small number of rows in the dataframe. + """ + plan = self._get_optimized_plan(array_value.node) + if not tree_properties.can_fast_peek(plan): warnings.warn("Peeking this value cannot be done efficiently.") - sql = self.compiler.compile_peek( - self._with_cached_executions(array_value.node), n_rows - ) + + sql = self.compiler.compile_peek(plan, n_rows) # TODO(swast): plumb through the api_name of the user-facing api that # caused this query. - return self._run_execute_query( - sql=sql, - ) + return self._run_execute_query(sql=sql) + + def head( + self, array_value: bigframes.core.ArrayValue, n_rows: int + ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + """ + Preview the first n rows of the dataframe. This is less efficient than the unordered peek preview op. + """ + maybe_row_count = self._local_get_row_count(array_value) + if (maybe_row_count is not None) and (maybe_row_count <= n_rows): + return self.execute(array_value, ordered=True) + + if not self.strictly_ordered and not array_value.node.explicitly_ordered: + # No user-provided ordering, so just get any N rows, its faster! + return self.peek(array_value, n_rows) + + plan = self._get_optimized_plan(array_value.node) + if not tree_properties.can_fast_head(plan): + # If can't get head fast, we are going to need to execute the whole query + # Will want to do this in a way such that the result is reusable, but the first + # N values can be easily extracted. + # This currently requires clustering on offsets. + self._cache_with_offsets(array_value) + # Get a new optimized plan after caching + plan = self._get_optimized_plan(array_value.node) + assert tree_properties.can_fast_head(plan) + + head_plan = generate_head_plan(plan, n_rows) + sql = self.compiler.compile_ordered(head_plan) + + # TODO(swast): plumb through the api_name of the user-facing api that + # caused this query. + return self._run_execute_query(sql=sql) + + def get_row_count(self, array_value: bigframes.core.ArrayValue) -> int: + count = self._local_get_row_count(array_value) + if count is not None: + return count + else: + row_count_plan = self._get_optimized_plan( + generate_row_count_plan(array_value.node) + ) + sql = self.compiler.compile_unordered(row_count_plan) + iter, _ = self._run_execute_query(sql) + return next(iter)[0] + + def _local_get_row_count( + self, array_value: bigframes.core.ArrayValue + ) -> Optional[int]: + # optimized plan has cache materializations which will have row count metadata + # that is more likely to be usable than original leaf nodes. + plan = self._get_optimized_plan(array_value.node) + return tree_properties.row_count(plan) # Helpers def _run_execute_query( @@ -250,8 +306,17 @@ def _wait_on_job(self, query_job: bigquery.QueryJob) -> bigquery.table.RowIterat self.metrics.count_job_stats(query_job) return results_iterator - def _with_cached_executions(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: - return tree_properties.replace_nodes(node, (dict(self._cached_executions))) + def _get_optimized_plan(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: + """ + Takes the original expression tree and applies optimizations to accelerate execution. + + At present, the only optimization is to replace subtress with cached previous materializations. + """ + # Apply any rewrites *after* applying cache, as cache is sensitive to exact tree structure + optimized_plan = tree_properties.replace_nodes( + node, (dict(self._cached_executions)) + ) + return optimized_plan def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): """ @@ -261,7 +326,7 @@ def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): # Once rewriting is available, will want to rewrite before # evaluating execution cost. return tree_properties.is_trivially_executable( - self._with_cached_executions(array_value.node) + self._get_optimized_plan(array_value.node) ) def _cache_with_cluster_cols( @@ -270,7 +335,7 @@ def _cache_with_cluster_cols( """Executes the query and uses the resulting table to rewrite future executions.""" sql, schema, ordering_info = self.compiler.compile_raw( - self._with_cached_executions(array_value.node) + self._get_optimized_plan(array_value.node) ) tmp_table = self._sql_as_cached_temp_table( sql, @@ -292,9 +357,7 @@ def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): ) offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") node_w_offsets = array_value.promote_offsets(offset_column).node - sql = self.compiler.compile_unordered( - self._with_cached_executions(node_w_offsets) - ) + sql = self.compiler.compile_unordered(self._get_optimized_plan(node_w_offsets)) tmp_table = self._sql_as_cached_temp_table( sql, @@ -329,7 +392,7 @@ def _simplify_with_caching(self, array_value: bigframes.core.ArrayValue): """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" # Apply existing caching first for _ in range(MAX_SUBTREE_FACTORINGS): - node_with_cache = self._with_cached_executions(array_value.node) + node_with_cache = self._get_optimized_plan(array_value.node) if node_with_cache.planning_complexity < QUERY_COMPLEXITY_LIMIT: return @@ -375,4 +438,21 @@ def _sql_as_cached_temp_table( job_config=job_config, api_name="cached", ) + query_job.destination + query_job.result() return query_job.destination + + +def generate_head_plan(node: nodes.BigFrameNode, n: int): + offsets_id = bigframes.core.guid.generate_guid("offsets_") + plan_w_offsets = nodes.PromoteOffsetsNode(node, offsets_id) + predicate = ops.lt_op.as_expr(ex.free_var(offsets_id), ex.const(n)) + plan_w_head = nodes.FilterNode(plan_w_offsets, predicate) + # Finally, drop the offsets column + return nodes.ProjectionNode( + plan_w_head, tuple((ex.free_var(i), i) for i in node.schema.names) + ) + + +def generate_row_count_plan(node: nodes.BigFrameNode): + return nodes.RowCountNode(node) diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py index 8cde187cb35..8ed3acba0fd 100644 --- a/tests/unit/core/test_blocks.py +++ b/tests/unit/core/test_blocks.py @@ -20,6 +20,7 @@ import bigframes import bigframes.core.blocks as blocks +import bigframes.session.executor @pytest.mark.parametrize( @@ -78,9 +79,13 @@ def test_block_from_local(data): expected = pandas.DataFrame(data) mock_session = mock.create_autospec(spec=bigframes.Session) + mock_executor = mock.create_autospec( + spec=bigframes.session.executor.BigQueryCachingExecutor + ) # hard-coded the returned dimension of the session for that each of the test case contains 3 rows. - mock_session._execute.return_value = (iter([[3]]), None) + mock_session._executor = mock_executor + mock_executor.get_row_count.return_value = 3 block = blocks.Block.from_local(pandas.DataFrame(data), mock_session) From 7dd304cc7168fac222fa1330f868677818d10903 Mon Sep 17 00:00:00 2001 From: mattyopl <90574735+mattyopl@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:42:36 -0400 Subject: [PATCH 794/921] feat: recover struct column from exploded Series (#904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: recover struct columns from exploded Series Fixes #357588049 internal 🦕 --------- Co-authored-by: Matthew Laurence Chen --- bigframes/bigquery/__init__.py | 34 +++++++++++ bigframes/core/compile/scalar_op_compiler.py | 11 ++++ bigframes/operations/__init__.py | 23 ++++++++ tests/system/small/bigquery/test_struct.py | 61 ++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 tests/system/small/bigquery/test_struct.py diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index bec7b5ff0ea..1e8e8d578d7 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -271,6 +271,40 @@ def json_extract_array( return series._apply_unary_op(ops.JSONExtractArray(json_path=json_path)) +def struct(value: dataframe.DataFrame) -> series.Series: + """Takes a DataFrame and converts it into a Series of structs with each + struct entry corresponding to a DataFrame row and each struct field + corresponding to a DataFrame column + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import bigframes.series as series + >>> bpd.options.display.progress_bar = None + + >>> srs = series.Series([{"version": 1, "project": "pandas"}, {"version": 2, "project": "numpy"},]) + >>> df = srs.struct.explode() + >>> bbq.struct(df) + 0 {'project': 'pandas', 'version': 1} + 1 {'project': 'numpy', 'version': 2} + dtype: struct[pyarrow] + + Args: + value (bigframes.dataframe.DataFrame): + The DataFrame to be converted to a Series of structs + + Returns: + bigframes.series.Series: A new Series with struct entries representing rows of the original DataFrame + """ + block = value._block + block, result_id = block.apply_nary_op( + block.value_columns, ops.StructOp(column_names=tuple(block.column_labels)) + ) + block = block.select_column(result_id) + return bigframes.series.Series(block) + + # Search functions defined from # https://cloud.google.com/bigquery/docs/reference/standard-sql/search_functions diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 86501214ad7..9e18b391d6f 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -1539,6 +1539,17 @@ def nary_remote_function_op_impl( return result +@scalar_op_compiler.register_nary_op(ops.StructOp, pass_op=True) +def struct_op_impl( + *values: ibis_types.Value, op: ops.StructOp +) -> ibis_types.StructValue: + data = {} + for i, value in enumerate(values): + data[op.column_names[i]] = value + + return ibis.struct(data) + + # Helpers def is_null(value) -> bool: # float NaN/inf should be treated as distinct from 'true' null values diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index cd9e70819e6..51a962b13bc 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -867,6 +867,29 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT case_when_op = CaseWhenOp() +@dataclasses.dataclass(frozen=True) +class StructOp(NaryOp): + name: typing.ClassVar[str] = "struct" + column_names: tuple[str] + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + num_input_types = len(input_types) + # value1, value2, ... + assert num_input_types == len(self.column_names) + fields = [] + + for i in range(num_input_types): + fields.append( + ( + self.column_names[i], + dtypes.bigframes_dtype_to_arrow_dtype(input_types[i]), + ) + ) + return pd.ArrowDtype( + pa.struct(fields) + ) # [(name1, value1), (name2, value2), ...] + + # Just parameterless unary ops for now # TODO: Parameter mappings NUMPY_TO_OP: typing.Final = { diff --git a/tests/system/small/bigquery/test_struct.py b/tests/system/small/bigquery/test_struct.py new file mode 100644 index 00000000000..58c822f642f --- /dev/null +++ b/tests/system/small/bigquery/test_struct.py @@ -0,0 +1,61 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + +import bigframes.bigquery as bbq +import bigframes.series as series + + +@pytest.mark.parametrize( + "columns_arg", + [ + [ + {"version": 1, "project": "pandas"}, + {"version": 2, "project": "pandas"}, + {"version": 1, "project": "numpy"}, + ], + [ + {"version": 1, "project": "pandas"}, + {"version": None, "project": "pandas"}, + {"version": 1, "project": "numpy"}, + ], + [ + {"array": [6, 4, 6], "project": "pandas"}, + {"array": [6, 4, 7, 6], "project": "pandas"}, + {"array": [7, 2, 3], "project": "numpy"}, + ], + [ + {"array": [6, 4, 6], "project": "pandas"}, + {"array": [6, 4, 7, 6], "project": "pandas"}, + {"array": [7, 2, 3], "project": "numpy"}, + ], + [ + {"struct": [{"x": 2, "y": 4}], "project": "pandas"}, + {"struct": [{"x": 9, "y": 3}], "project": "pandas"}, + {"struct": [{"x": 1, "y": 2}], "project": "numpy"}, + ], + ], +) +def test_struct_from_dataframe(columns_arg): + srs = series.Series( + columns_arg, + ) + pd.testing.assert_series_equal( + srs.to_pandas(), + bbq.struct(srs.struct.explode()).to_pandas(), + check_index_type=False, + check_dtype=False, + ) From 1291110c066ec75855efa718eabe84b7c336851b Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:55:31 -0700 Subject: [PATCH 795/921] chore: fix the test cover workflow (#948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: fix the test cover workflow * try absolute paths * tag to old version * include-hidden-files * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * exclude unitest.yml in owlbot.py --------- Co-authored-by: Owl Bot --- .github/workflows/unittest.yml | 1 + owlbot.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 132369f3edf..9c84741c6d3 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -30,6 +30,7 @@ jobs: with: name: coverage-artifact-${{ matrix.python }} path: .coverage-${{ matrix.python }} + include-hidden-files: true cover: runs-on: ubuntu-latest diff --git a/owlbot.py b/owlbot.py index b29384d4625..ca3c8cbe14c 100644 --- a/owlbot.py +++ b/owlbot.py @@ -49,6 +49,7 @@ "README.rst", "CONTRIBUTING.rst", ".github/release-trigger.yml", + ".github/workflows/unittest.yml", # BigQuery DataFrames manages its own Kokoro cluster for presubmit & continuous tests. ".kokoro/build.sh", ".kokoro/continuous/common.cfg", From 2b0f0faf840a1ec43d007827bbbf908df62ce9d3 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 4 Sep 2024 08:13:08 -0700 Subject: [PATCH 796/921] fix: Fix issue with iterating on >10gb dataframes (#949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Sweña (Swast) --- bigframes/core/blocks.py | 4 +++- bigframes/session/__init__.py | 1 + bigframes/session/executor.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 25dec808ddf..a309671842b 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -577,7 +577,9 @@ def to_pandas_batches( see https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob#google_cloud_bigquery_job_QueryJob_result""" dtypes = dict(zip(self.index_columns, self.index.dtypes)) dtypes.update(zip(self.value_columns, self.dtypes)) - _, query_job = self.session._execute(self.expr, ordered=True) + _, query_job = self.session._executor.execute( + self.expr, ordered=True, use_explicit_destination=True + ) results_iterator = query_job.result( page_size=page_size, max_results=max_results ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index c91266b8757..fba1d41e301 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1324,6 +1324,7 @@ def _execute( *, ordered: bool = True, col_id_overrides: Mapping[str, str] = {}, + use_explicit_destination: bool = False, ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: return self._executor.execute( array_value, diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index b696b77eb1f..72d54932943 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -102,6 +102,7 @@ def execute( *, ordered: bool = True, col_id_overrides: Mapping[str, str] = {}, + use_explicit_destination: bool = False, ): """ Execute the ArrayValue, storing the result to a temporary session-owned table. @@ -113,6 +114,13 @@ def execute( array_value, ordered=ordered, col_id_overrides=col_id_overrides ) job_config = bigquery.QueryJobConfig() + # Use explicit destination to avoid 10GB limit of temporary table + if use_explicit_destination: + schema = array_value.schema.to_bigquery() + destination_table = self.storage_manager.create_temp_table( + schema, cluster_cols=[] + ) + job_config.destination = destination_table # TODO(swast): plumb through the api_name of the user-facing api that # caused this query. return self._run_execute_query( From d632cd03e3e3ea6dfa7c56dd459c422e95be906e Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:23:22 -0700 Subject: [PATCH 797/921] docs: update struct examples. (#953) * docs:update struct examples. * format fix --- .../pandas/core/arrays/arrow/accessors.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py index 4cba928bb6d..ab199d53bd2 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py @@ -22,12 +22,12 @@ def field(self, name_or_index: str | int): >>> bpd.options.display.progress_bar = None >>> s = bpd.Series( ... [ - ... {"version": 1, "project": "pandas"}, - ... {"version": 2, "project": "pandas"}, - ... {"version": 1, "project": "numpy"}, + ... {"project": "pandas", "version": 1}, + ... {"project": "pandas", "version": 2}, + ... {"project": "numpy", "version": 1}, ... ], ... dtype=bpd.ArrowDtype(pa.struct( - ... [("version", pa.int64()), ("project", pa.string())] + ... [("project", pa.string()), ("version", pa.int64())] ... )) ... ) @@ -41,7 +41,7 @@ def field(self, name_or_index: str | int): Extract by field index. - >>> s.struct.field(0) + >>> s.struct.field(1) 0 1 1 2 2 1 @@ -68,22 +68,22 @@ def explode(self): >>> bpd.options.display.progress_bar = None >>> s = bpd.Series( ... [ - ... {"version": 1, "project": "pandas"}, - ... {"version": 2, "project": "pandas"}, - ... {"version": 1, "project": "numpy"}, + ... {"project": "pandas", "version": 1}, + ... {"project": "pandas", "version": 2}, + ... {"project": "numpy", "version": 1}, ... ], ... dtype=bpd.ArrowDtype(pa.struct( - ... [("version", pa.int64()), ("project", pa.string())] + ... [("project", pa.string()), ("version", pa.int64())] ... )) ... ) Extract all child fields. >>> s.struct.explode() - version project - 0 1 pandas - 1 2 pandas - 2 1 numpy + project version + 0 pandas 1 + 1 pandas 2 + 2 numpy 1 [3 rows x 2 columns] @@ -113,8 +113,8 @@ def dtypes(self): ... )) ... ) >>> s.struct.dtypes() - version Int64 project string[pyarrow] + version Int64 dtype: object Returns: @@ -140,21 +140,21 @@ def explode(self, column, *, separator: str = "."): >>> countries = bpd.Series(["cn", "es", "us"]) >>> files = bpd.Series( ... [ - ... {"version": 1, "project": "pandas"}, - ... {"version": 2, "project": "pandas"}, - ... {"version": 1, "project": "numpy"}, + ... {"project": "pandas", "version": 1}, + ... {"project": "pandas", "version": 2}, + ... {"project": "numpy", "version": 1}, ... ], ... dtype=bpd.ArrowDtype(pa.struct( - ... [("version", pa.int64()), ("project", pa.string())] + ... [("project", pa.string()), ("version", pa.int64())] ... )) ... ) >>> downloads = bpd.Series([100, 200, 300]) >>> df = bpd.DataFrame({"country": countries, "file": files, "download_count": downloads}) >>> df.struct.explode("file") - country file.version file.project download_count - 0 cn 1 pandas 100 - 1 es 2 pandas 200 - 2 us 1 numpy 300 + country file.project file.version download_count + 0 cn pandas 1 100 + 1 es pandas 2 200 + 2 us numpy 1 300 [3 rows x 4 columns] From 6fdb6b130400c431b3328342b2203ed08122d5b5 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:53:15 -0500 Subject: [PATCH 798/921] chore(main): release 1.16.0 (#911) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e77062dfa13..f3dae5af719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.16.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.15.0...v1.16.0) (2024-09-04) + + +### Features + +* Add `DataFrame.struct.explode` to add struct subfields to a DataFrame ([#916](https://github.com/googleapis/python-bigquery-dataframes/issues/916)) ([ad2f75e](https://github.com/googleapis/python-bigquery-dataframes/commit/ad2f75ecbc3660459814716eec7d1f88d1188942)) +* Implement `bigframes.bigquery.json_extract_array` ([#910](https://github.com/googleapis/python-bigquery-dataframes/issues/910)) ([575a29e](https://github.com/googleapis/python-bigquery-dataframes/commit/575a29e77d50d60d7e9a84ebb87abcdb993adef1)) +* Recover struct column from exploded Series ([#904](https://github.com/googleapis/python-bigquery-dataframes/issues/904)) ([7dd304c](https://github.com/googleapis/python-bigquery-dataframes/commit/7dd304cc7168fac222fa1330f868677818d10903)) + + +### Bug Fixes + +* Fix issue with iterating on >10gb dataframes ([#949](https://github.com/googleapis/python-bigquery-dataframes/issues/949)) ([2b0f0fa](https://github.com/googleapis/python-bigquery-dataframes/commit/2b0f0faf840a1ec43d007827bbbf908df62ce9d3)) +* Improve `Series.replace` for dict input ([#907](https://github.com/googleapis/python-bigquery-dataframes/issues/907)) ([4208044](https://github.com/googleapis/python-bigquery-dataframes/commit/4208044222c6a8494004ec6f511a3b85f4eb4180)) +* NullIndex in ML model.predict error ([#917](https://github.com/googleapis/python-bigquery-dataframes/issues/917)) ([612271d](https://github.com/googleapis/python-bigquery-dataframes/commit/612271d35675353effa465a797d6e3a1285d4d37)) +* Struct field non-nullable type issue. ([#914](https://github.com/googleapis/python-bigquery-dataframes/issues/914)) ([149d5ff](https://github.com/googleapis/python-bigquery-dataframes/commit/149d5ff822da3d7fda18dbed4814e0406708cf07)) +* Unordered mode errors in ml train_test_split ([#925](https://github.com/googleapis/python-bigquery-dataframes/issues/925)) ([85d7c21](https://github.com/googleapis/python-bigquery-dataframes/commit/85d7c21b4bd5dc669098342fc60d66d89ef06b2b)) + + +### Performance Improvements + +* Improve repr performance ([#918](https://github.com/googleapis/python-bigquery-dataframes/issues/918)) ([46f2dd7](https://github.com/googleapis/python-bigquery-dataframes/commit/46f2dd79f59131bbb98fe4ae3780b98cb4d50646)) + + +### Dependencies + +* Re-introduce support for numpy 1.24.x ([#931](https://github.com/googleapis/python-bigquery-dataframes/issues/931)) ([3d71913](https://github.com/googleapis/python-bigquery-dataframes/commit/3d71913b3cf357fc9e94304ca0c94070e0a16f92)) +* Update minimum support to Pandas 1.5.3 and Pyarrow 10.0.1 ([#903](https://github.com/googleapis/python-bigquery-dataframes/issues/903)) ([7ed3962](https://github.com/googleapis/python-bigquery-dataframes/commit/7ed39629c638874d8e9cc3c7a9b3ec92ad480eca)) + + +### Documentation + +* Add Claude3 ML and RemoteFunc notebooks ([#930](https://github.com/googleapis/python-bigquery-dataframes/issues/930)) ([cfd16c1](https://github.com/googleapis/python-bigquery-dataframes/commit/cfd16c1278023bd2c3dce9c0cb378615aa00e58d)) +* Create sample notebook to manipulate struct and array data ([#883](https://github.com/googleapis/python-bigquery-dataframes/issues/883)) ([3031903](https://github.com/googleapis/python-bigquery-dataframes/commit/303190331d3194562c5ed44fefc2c9fd1d73bedd)) +* Update struct examples. ([#953](https://github.com/googleapis/python-bigquery-dataframes/issues/953)) ([d632cd0](https://github.com/googleapis/python-bigquery-dataframes/commit/d632cd03e3e3ea6dfa7c56dd459c422e95be906e)) +* Use unstack() from BigQuery DataFrames instead of pandas in the PyPI sample notebook ([#890](https://github.com/googleapis/python-bigquery-dataframes/issues/890)) ([d1883cc](https://github.com/googleapis/python-bigquery-dataframes/commit/d1883cc04ce5b2944d87a00c79b99a406001ba8f)) + ## [1.15.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.14.0...v1.15.0) (2024-08-20) diff --git a/bigframes/version.py b/bigframes/version.py index f0f332d182f..d5b4691b988 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.15.0" +__version__ = "1.16.0" From 27764a64f90092374458fafbe393bc6c30c85681 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:30:52 -0700 Subject: [PATCH 799/921] fix: astype Decimal to Int64 conversion. (#957) * fix: astype Decimal to Int64 conversion. * update format --- bigframes/core/compile/ibis_types.py | 2 ++ tests/system/small/test_series.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/bigframes/core/compile/ibis_types.py b/bigframes/core/compile/ibis_types.py index 0b3038c9c7b..f4ec295d5f7 100644 --- a/bigframes/core/compile/ibis_types.py +++ b/bigframes/core/compile/ibis_types.py @@ -144,10 +144,12 @@ def cast_ibis_value( ), ibis_dtypes.Decimal(precision=38, scale=9): ( ibis_dtypes.float64, + ibis_dtypes.int64, ibis_dtypes.Decimal(precision=76, scale=38), ), ibis_dtypes.Decimal(precision=76, scale=38): ( ibis_dtypes.float64, + ibis_dtypes.int64, ibis_dtypes.Decimal(precision=38, scale=9), ), ibis_dtypes.time: ( diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 7458187a825..9a6783ee5c5 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -3080,6 +3080,16 @@ def test_astype(scalars_df_index, scalars_pandas_df_index, column, to_type): pd.testing.assert_series_equal(bf_result, pd_result) +@skip_legacy_pandas +def test_astype_numeric_to_int(scalars_df_index, scalars_pandas_df_index): + column = "numeric_col" + to_type = "Int64" + bf_result = scalars_df_index[column].astype(to_type).to_pandas() + # Round to the nearest whole number to avoid TypeError + pd_result = scalars_pandas_df_index[column].round(0).astype(to_type) + pd.testing.assert_series_equal(bf_result, pd_result) + + @pytest.mark.parametrize( ("column", "to_type"), [ From cccc6ca8c1271097bbe15e3d9ccdcfd7c633227a Mon Sep 17 00:00:00 2001 From: mattyopl <90574735+mattyopl@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:18:04 -0400 Subject: [PATCH 800/921] feat: allow setting table labels in `to_gbq` (#941) * chore: allow setting table labels in `to_gbq` --------- Co-authored-by: Matthew Laurence Chen Co-authored-by: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> --- bigframes/dataframe.py | 14 +++++++++++--- tests/system/small/test_dataframe.py | 11 +++++++++++ .../bigframes_vendored/pandas/core/frame.py | 4 ++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 6b782b4692c..2ae6aefe1bb 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3027,6 +3027,7 @@ def to_gbq( index: bool = True, ordering_id: Optional[str] = None, clustering_columns: Union[pandas.Index, Iterable[typing.Hashable]] = (), + labels: dict[str, str] = {}, ) -> str: temp_table_ref = None @@ -3081,9 +3082,11 @@ def to_gbq( export_array, id_overrides = self._prepare_export( index=index and self._has_index, ordering_id=ordering_id ) - destination = bigquery.table.TableReference.from_string( - destination_table, - default_project=default_project, + destination: bigquery.table.TableReference = ( + bigquery.table.TableReference.from_string( + destination_table, + default_project=default_project, + ) ) _, query_job = self._session._export( export_array, @@ -3106,6 +3109,11 @@ def to_gbq( + constants.DEFAULT_EXPIRATION, ) + if len(labels) != 0: + table = bigquery.Table(result_table) + table.labels = labels + self._session.bqclient.update_table(table, ["labels"]) + return destination_table def to_numpy( diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index ddcf044911a..f51b5976507 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4657,6 +4657,17 @@ def test_to_gbq_and_create_dataset(session, scalars_df_index, dataset_id_not_cre assert not loaded_scalars_df_index.empty +def test_to_gbq_table_labels(scalars_df_index): + destination_table = "bigframes-dev.bigframes_tests_sys.table_labels" + result_table = scalars_df_index.to_gbq( + destination_table, labels={"test": "labels"}, if_exists="replace" + ) + client = scalars_df_index._session.bqclient + table = client.get_table(result_table) + assert table.labels + assert table.labels["test"] == "labels" + + @pytest.mark.parametrize( ("col_names", "ignore_index"), [ diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index 10565a2552f..fe1c8a12ffb 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -390,6 +390,7 @@ def to_gbq( index: bool = True, ordering_id: Optional[str] = None, clustering_columns: Union[pd.Index, Iterable[Hashable]] = (), + labels: dict[str, str] = {}, ) -> str: """Write a DataFrame to a BigQuery table. @@ -467,6 +468,9 @@ def to_gbq( clustering order within the Index/DataFrame columns follows the order specified in `clustering_columns`. + labels (dict[str, str], default None): + Specifies table labels within BigQuery + Returns: str: The fully-qualified ID for the written table, in the form From 3b35860776033fc8e71e471422c6d2b9366a7c9f Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:39:52 -0700 Subject: [PATCH 801/921] feat: enable read_csv() to process other files (#940) * add tests * feat: enable read_csv() to process other files * update to main * add docs --- bigframes/session/__init__.py | 6 ++++-- bigframes/session/loader.py | 8 +++++++- tests/system/small/test_session.py | 19 +++++++++++++++++++ .../pandas/io/parsers/readers.py | 6 +++--- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index fba1d41e301..7aa4ed4b5a9 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1008,10 +1008,12 @@ def _check_file_size(self, filepath: str): blob = bucket.blob(blob_name) blob.reload() file_size = blob.size - else: # local file path + elif os.path.exists(filepath): # local file path file_size = os.path.getsize(filepath) + else: + file_size = None - if file_size > max_size: + if file_size is not None and file_size > max_size: # Convert to GB file_size = round(file_size / (1024**3), 1) max_size = int(max_size / 1024**3) diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index edfd57b9657..924fddce12c 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -18,6 +18,7 @@ import dataclasses import datetime import itertools +import os import typing from typing import Dict, Hashable, IO, Iterable, List, Optional, Sequence, Tuple, Union @@ -421,11 +422,16 @@ def _read_bigquery_load_job( load_job = self._bqclient.load_table_from_uri( filepath_or_buffer, table, job_config=job_config ) - else: + elif os.path.exists(filepath_or_buffer): # local file path with open(filepath_or_buffer, "rb") as source_file: load_job = self._bqclient.load_table_from_file( source_file, table, job_config=job_config ) + else: + raise NotImplementedError( + f"BigQuery engine only supports a local file path or GCS path. " + f"{constants.FEEDBACK_LINK}" + ) else: load_job = self._bqclient.load_table_from_file( filepath_or_buffer, table, job_config=job_config diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 5b5db74ea6a..ed3e38e6f84 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -1036,6 +1036,25 @@ def test_read_csv_local_w_usecols(session, scalars_pandas_df_index, engine): assert len(df.columns) == 1 +@pytest.mark.parametrize( + "engine", + [ + pytest.param( + "bigquery", + id="bq_engine", + marks=pytest.mark.xfail( + raises=NotImplementedError, + ), + ), + pytest.param(None, id="default_engine"), + ], +) +def test_read_csv_others(session, engine): + uri = "https://raw.githubusercontent.com/googleapis/python-bigquery-dataframes/main/tests/data/people.csv" + df = session.read_csv(uri, engine=engine) + assert len(df.columns) == 3 + + @pytest.mark.parametrize( "engine", [ diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py index 248cf8e0fe9..35b2a1982a0 100644 --- a/third_party/bigframes_vendored/pandas/io/parsers/readers.py +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -51,8 +51,7 @@ def read_csv( encoding: Optional[str] = None, **kwargs, ): - """Loads DataFrame from comma-separated values (csv) file locally or from - Cloud Storage. + """Loads data from a comma-separated values (csv) file into a DataFrame. The CSV file data will be persisted as a temporary BigQuery table, which can be automatically recycled after the Session is closed. @@ -60,7 +59,8 @@ def read_csv( .. note:: using `engine="bigquery"` will not guarantee the same ordering as the file. Instead, set a serialized index column as the index and sort by - that in the resulting DataFrame. + that in the resulting DataFrame. Only files stored on your local machine + or in Google Cloud Storage are supported. .. note:: For non-bigquery engine, data is inlined in the query SQL if it is From 8e8279d4da90feb5766f266b49cb417f8cbec6c9 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Thu, 5 Sep 2024 11:44:43 -0700 Subject: [PATCH 802/921] feat: define list accessor for bigframes Series (#946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: define list accessor for bigframes Series * Add doc for list accessor * Fix bug in docstring and inheritance * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Skip test if Pandas version is too old * Fix docstring format, and provide notebook examples. * Use func link under see also --------- Co-authored-by: Owl Bot --- bigframes/operations/_op_converters.py | 37 ++++++++ bigframes/operations/lists.py | 46 ++++++++++ bigframes/operations/strings.py | 24 +---- bigframes/series.py | 9 +- docs/reference/bigframes.pandas/series.rst | 8 ++ .../dataframes/struct_and_array_dtypes.ipynb | 88 ++++++++++++++----- tests/system/small/operations/test_lists.py | 83 +++++++++++++++++ .../pandas/core/arrays/arrow/accessors.py | 65 ++++++++++++++ 8 files changed, 318 insertions(+), 42 deletions(-) create mode 100644 bigframes/operations/_op_converters.py create mode 100644 bigframes/operations/lists.py create mode 100644 tests/system/small/operations/test_lists.py diff --git a/bigframes/operations/_op_converters.py b/bigframes/operations/_op_converters.py new file mode 100644 index 00000000000..3ebf22bcb6a --- /dev/null +++ b/bigframes/operations/_op_converters.py @@ -0,0 +1,37 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.operations as ops + + +def convert_index(key: int) -> ops.ArrayIndexOp: + if key < 0: + raise NotImplementedError("Negative indexing is not supported.") + return ops.ArrayIndexOp(index=key) + + +def convert_slice(key: slice) -> ops.ArraySliceOp: + if key.step is not None and key.step != 1: + raise NotImplementedError(f"Only a step of 1 is allowed, got {key.step}") + + if (key.start is not None and key.start < 0) or ( + key.stop is not None and key.stop < 0 + ): + raise NotImplementedError("Slicing with negative numbers is not allowed.") + + return ops.ArraySliceOp( + start=key.start if key.start is not None else 0, + stop=key.stop, + step=key.step, + ) diff --git a/bigframes/operations/lists.py b/bigframes/operations/lists.py new file mode 100644 index 00000000000..16c22dfb2a9 --- /dev/null +++ b/bigframes/operations/lists.py @@ -0,0 +1,46 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import inspect +from typing import Union + +import bigframes_vendored.pandas.core.arrays.arrow.accessors as vendoracessors + +from bigframes.core import log_adapter +import bigframes.operations as ops +from bigframes.operations._op_converters import convert_index, convert_slice +import bigframes.operations.base +import bigframes.series as series + + +@log_adapter.class_logger +class ListAccessor( + bigframes.operations.base.SeriesMethods, vendoracessors.ListAccessor +): + __doc__ = vendoracessors.ListAccessor.__doc__ + + def len(self): + return self._apply_unary_op(ops.len_op) + + def __getitem__(self, key: Union[int, slice]) -> series.Series: + if isinstance(key, int): + return self._apply_unary_op(convert_index(key)) + elif isinstance(key, slice): + return self._apply_unary_op(convert_slice(key)) + else: + raise ValueError(f"key must be an int or slice, got {type(key).__name__}") + + __getitem__.__doc__ = inspect.getdoc(vendoracessors.ListAccessor.__getitem__) diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index d3e9c7edc67..4af142e0d51 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -23,6 +23,7 @@ from bigframes.core import log_adapter import bigframes.dataframe as df import bigframes.operations as ops +from bigframes.operations._op_converters import convert_index, convert_slice import bigframes.operations.base import bigframes.series as series @@ -40,28 +41,9 @@ class StringMethods(bigframes.operations.base.SeriesMethods, vendorstr.StringMet def __getitem__(self, key: Union[int, slice]) -> series.Series: if isinstance(key, int): - if key < 0: - raise NotImplementedError("Negative indexing is not supported.") - return self._apply_unary_op(ops.ArrayIndexOp(index=key)) + return self._apply_unary_op(convert_index(key)) elif isinstance(key, slice): - if key.step is not None and key.step != 1: - raise NotImplementedError( - f"Only a step of 1 is allowed, got {key.step}" - ) - if (key.start is not None and key.start < 0) or ( - key.stop is not None and key.stop < 0 - ): - raise NotImplementedError( - "Slicing with negative numbers is not allowed." - ) - - return self._apply_unary_op( - ops.ArraySliceOp( - start=key.start if key.start is not None else 0, - stop=key.stop, - step=key.step, - ) - ) + return self._apply_unary_op(convert_slice(key)) else: raise ValueError(f"key must be an int or slice, got {type(key).__name__}") diff --git a/bigframes/series.py b/bigframes/series.py index a166680f855..5192a9cf496 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -53,6 +53,7 @@ import bigframes.operations.aggregations as agg_ops import bigframes.operations.base import bigframes.operations.datetimes as dt +import bigframes.operations.lists as lists import bigframes.operations.plotting as plotting import bigframes.operations.strings as strings import bigframes.operations.structs as structs @@ -66,6 +67,8 @@ " Try converting it to a remote function." ) +_list = list # Type alias to escape Series.list property + @log_adapter.class_logger class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Series): @@ -161,6 +164,10 @@ def query_job(self) -> Optional[bigquery.QueryJob]: def struct(self) -> structs.StructAccessor: return structs.StructAccessor(self._block) + @property + def list(self) -> lists.ListAccessor: + return lists.ListAccessor(self._block) + @property @validations.requires_ordering() def T(self) -> Series: @@ -1708,7 +1715,7 @@ def to_latex( buf, columns=columns, header=header, index=index, **kwargs ) - def tolist(self) -> list: + def tolist(self) -> _list: return self.to_pandas().to_list() to_list = tolist diff --git a/docs/reference/bigframes.pandas/series.rst b/docs/reference/bigframes.pandas/series.rst index f14eb8e8625..30cf851de7c 100644 --- a/docs/reference/bigframes.pandas/series.rst +++ b/docs/reference/bigframes.pandas/series.rst @@ -35,6 +35,14 @@ String handling :inherited-members: :undoc-members: +List handling +^^^^^^^^^^^^^ + +.. automodule:: bigframes.operations.lists + :members: + :inherited-members: + :undoc-members: + Struct handling ^^^^^^^^^^^^^^^ diff --git a/notebooks/dataframes/struct_and_array_dtypes.ipynb b/notebooks/dataframes/struct_and_array_dtypes.ipynb index 3bcdaf40f7b..def65ee6ca5 100644 --- a/notebooks/dataframes/struct_and_array_dtypes.ipynb +++ b/notebooks/dataframes/struct_and_array_dtypes.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Copyright 2023 Google LLC\n", + "# Copyright 2024 Google LLC\n", "#\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", @@ -212,6 +212,54 @@ "cell_type": "code", "execution_count": 7, "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 3\n", + "1 2\n", + "2 4\n", + "Name: Scores, dtype: Int64" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Find the length of each array with list accessor\n", + "df['Scores'].list.len()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 88\n", + "1 81\n", + "2 89\n", + "Name: Scores, dtype: Int64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Find the second element in each array with list accessor\n", + "df['Scores'].list[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, "outputs": [ { "data": { @@ -228,7 +276,7 @@ "Name: Scores, dtype: Int64" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -243,7 +291,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -261,7 +309,7 @@ "Name: Scores, dtype: Float64" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -274,7 +322,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -286,7 +334,7 @@ "Name: Scores, dtype: list[pyarrow]" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -299,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -361,7 +409,7 @@ "[3 rows x 3 columns]" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -394,14 +442,14 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes/venv/lib/python3.12/site-packages/google/cloud/bigquery/_pandas_helpers.py:570: UserWarning: Pyarrow could not determine the type of columns: bigframes_unnamed_index.\n", + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/venv/lib/python3.11/site-packages/google/cloud/bigquery/_pandas_helpers.py:570: UserWarning: Pyarrow could not determine the type of columns: bigframes_unnamed_index.\n", " warnings.warn(\n" ] }, @@ -460,7 +508,7 @@ "[3 rows x 2 columns]" ] }, - "execution_count": 11, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -483,7 +531,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -494,7 +542,7 @@ "dtype: object" ] }, - "execution_count": 12, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -514,7 +562,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -525,7 +573,7 @@ "dtype: object" ] }, - "execution_count": 13, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -537,7 +585,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -549,7 +597,7 @@ "Name: City, dtype: string" ] }, - "execution_count": 14, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -562,7 +610,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -620,7 +668,7 @@ "[3 rows x 2 columns]" ] }, - "execution_count": 15, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -648,7 +696,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/tests/system/small/operations/test_lists.py b/tests/system/small/operations/test_lists.py new file mode 100644 index 00000000000..7ecf79dc6af --- /dev/null +++ b/tests/system/small/operations/test_lists.py @@ -0,0 +1,83 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import packaging.version +import pandas as pd +import pyarrow as pa +import pytest + +import bigframes.pandas as bpd + +from ...utils import assert_series_equal + + +@pytest.mark.parametrize( + ("key"), + [ + pytest.param(0, id="int"), + pytest.param(slice(None, None, None), id="default_start_slice"), + pytest.param(slice(0, None, 1), id="default_stop_slice"), + pytest.param(slice(0, 2, None), id="default_step_slice"), + ], +) +def test_getitem(key): + if packaging.version.Version(pd.__version__) < packaging.version.Version("2.2.0"): + pytest.skip( + "https://pandas.pydata.org/docs/whatsnew/v2.2.0.html#series-list-accessor-for-pyarrow-list-data" + ) + data = [[1], [2, 3], [4, 5, 6]] + s = bpd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) + pd_s = pd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) + + bf_result = s.list[key].to_pandas() + pd_result = pd_s.list[key] + + assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) + + +@pytest.mark.parametrize( + ("key", "expectation"), + [ + # Negative index + (-1, pytest.raises(NotImplementedError)), + # Slice with negative start + (slice(-1, None, None), pytest.raises(NotImplementedError)), + # Slice with negatiev end + (slice(0, -1, None), pytest.raises(NotImplementedError)), + # Slice with step not equal to 1 + (slice(0, 2, 2), pytest.raises(NotImplementedError)), + ], +) +def test_getitem_notsupported(key, expectation): + data = [[1], [2, 3], [4, 5, 6]] + s = bpd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) + + with expectation as e: + assert s.list[key] == e + + +def test_len(): + if packaging.version.Version(pd.__version__) < packaging.version.Version("2.2.0"): + pytest.skip( + "https://pandas.pydata.org/docs/whatsnew/v2.2.0.html#series-list-accessor-for-pyarrow-list-data" + ) + data = [[], [1], [1, 2], [1, 2, 3]] + s = bpd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) + pd_s = pd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) + + bf_result = s.list.len().to_pandas() + pd_result = pd_s.list.len() + + assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) diff --git a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py index ab199d53bd2..771146250ad 100644 --- a/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py +++ b/third_party/bigframes_vendored/pandas/core/arrays/arrow/accessors.py @@ -6,6 +6,71 @@ from bigframes import constants +class ListAccessor: + """Accessor object for list data properties of the Series values.""" + + def len(self): + """Compute the length of each list in the Series. + + **See Also:** + + - :func:`StringMethods.len` : Compute the length of each element in the Series/Index. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pyarrow as pa + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... [ + ... [1, 2, 3], + ... [3], + ... ], + ... dtype=bpd.ArrowDtype(pa.list_(pa.int64())), + ... ) + >>> s.list.len() + 0 3 + 1 1 + dtype: Int64 + + Returns: + bigframes.series.Series: A Series or Index of integer values indicating + the length of each element in the Series or Index. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def __getitem__(self, key: int | slice): + """Index or slice lists in the Series. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pyarrow as pa + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series( + ... [ + ... [1, 2, 3], + ... [3], + ... ], + ... dtype=bpd.ArrowDtype(pa.list_(pa.int64())), + ... ) + >>> s.list[0] + 0 1 + 1 3 + dtype: Int64 + + Args: + key (int | slice): Index or slice of indices to access from each list. + For integer indices, only non-negative values are accepted. For + slices, you must use a non-negative start, a non-negative end, and + a step of 1. + + Returns: + bigframes.series.Series: The list at requested index. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + class StructAccessor: """ Accessor object for structured data properties of the Series values. From c1cde19769c169b962b58b25f0be61c8c41edb95 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:04:33 -0700 Subject: [PATCH 803/921] feat: add Gemini 1.5 stable models support (#945) * feat: add Gemini 1.5 stable models support * add to loader --- bigframes/ml/llm.py | 12 ++++++++++-- bigframes/ml/loader.py | 2 ++ tests/system/small/ml/test_llm.py | 26 ++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 35bcf0a33c5..a3cd065a55a 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -55,10 +55,14 @@ _GEMINI_PRO_ENDPOINT = "gemini-pro" _GEMINI_1P5_PRO_PREVIEW_ENDPOINT = "gemini-1.5-pro-preview-0514" _GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT = "gemini-1.5-flash-preview-0514" +_GEMINI_1P5_PRO_001_ENDPOINT = "gemini-1.5-pro-001" +_GEMINI_1P5_FLASH_001_ENDPOINT = "gemini-1.5-flash-001" _GEMINI_ENDPOINTS = ( _GEMINI_PRO_ENDPOINT, _GEMINI_1P5_PRO_PREVIEW_ENDPOINT, _GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT, + _GEMINI_1P5_PRO_001_ENDPOINT, + _GEMINI_1P5_FLASH_001_ENDPOINT, ) _CLAUDE_3_SONNET_ENDPOINT = "claude-3-sonnet" @@ -728,7 +732,7 @@ class GeminiTextGenerator(base.BaseEstimator): 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" and "gemini-1.5-flash-preview-0514". 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" and "gemini-1.5-flash-001". Default to "gemini-pro". .. note:: "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 @@ -750,7 +754,11 @@ def __init__( self, *, model_name: Literal[ - "gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514" + "gemini-pro", + "gemini-1.5-pro-preview-0514", + "gemini-1.5-flash-preview-0514", + "gemini-1.5-pro-001", + "gemini-1.5-flash-001", ] = "gemini-pro", session: Optional[bigframes.Session] = None, connection_name: Optional[str] = None, diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 7d75f4c65a8..4e7e808260f 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -63,6 +63,8 @@ llm._GEMINI_PRO_ENDPOINT: llm.GeminiTextGenerator, llm._GEMINI_1P5_PRO_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, llm._GEMINI_1P5_PRO_FLASH_PREVIEW_ENDPOINT: llm.GeminiTextGenerator, + llm._GEMINI_1P5_PRO_001_ENDPOINT: llm.GeminiTextGenerator, + llm._GEMINI_1P5_FLASH_001_ENDPOINT: llm.GeminiTextGenerator, llm._CLAUDE_3_HAIKU_ENDPOINT: llm.Claude3TextGenerator, llm._CLAUDE_3_SONNET_ENDPOINT: llm.Claude3TextGenerator, llm._CLAUDE_3_5_SONNET_ENDPOINT: llm.Claude3TextGenerator, diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 43e756019d2..e3d2b510816 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -324,7 +324,7 @@ def test_create_load_text_embedding_generator_model( ("text-embedding-004", "text-multilingual-embedding-002"), ) @pytest.mark.flaky(retries=2) -def test_gemini_text_embedding_generator_predict_default_params_success( +def test_text_embedding_generator_predict_default_params_success( llm_text_df, model_name, session, bq_connection ): text_embedding_model = llm.TextEmbeddingGenerator( @@ -340,7 +340,13 @@ def test_gemini_text_embedding_generator_predict_default_params_success( @pytest.mark.parametrize( "model_name", - ("gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514"), + ( + "gemini-pro", + "gemini-1.5-pro-preview-0514", + "gemini-1.5-flash-preview-0514", + "gemini-1.5-pro-001", + "gemini-1.5-flash-001", + ), ) def test_create_load_gemini_text_generator_model( dataset_id, model_name, session, bq_connection @@ -362,7 +368,13 @@ def test_create_load_gemini_text_generator_model( @pytest.mark.parametrize( "model_name", - ("gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514"), + ( + "gemini-pro", + "gemini-1.5-pro-preview-0514", + "gemini-1.5-flash-preview-0514", + "gemini-1.5-pro-001", + "gemini-1.5-flash-001", + ), ) @pytest.mark.flaky(retries=2) def test_gemini_text_generator_predict_default_params_success( @@ -379,7 +391,13 @@ def test_gemini_text_generator_predict_default_params_success( @pytest.mark.parametrize( "model_name", - ("gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514"), + ( + "gemini-pro", + "gemini-1.5-pro-preview-0514", + "gemini-1.5-flash-preview-0514", + "gemini-1.5-pro-001", + "gemini-1.5-flash-001", + ), ) @pytest.mark.flaky(retries=2) def test_gemini_text_generator_predict_with_params_success( From 71a8ab91928e6180d479d89eb91f1ea45d00152a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 5 Sep 2024 15:24:34 -0700 Subject: [PATCH 804/921] refactor: Simplify projection nodes (#961) --- bigframes/core/__init__.py | 77 +++++++++--------------------- bigframes/core/blocks.py | 25 ++++++---- bigframes/core/compile/compiled.py | 15 +++++- bigframes/core/compile/compiler.py | 5 ++ bigframes/core/expression.py | 29 ++++++++--- bigframes/core/nodes.py | 31 +++++++++++- bigframes/core/ordering.py | 2 +- bigframes/core/rewrite.py | 39 ++++++++++++--- bigframes/session/executor.py | 4 +- bigframes/session/planner.py | 12 ++++- tests/unit/test_planner.py | 16 +++---- 11 files changed, 164 insertions(+), 91 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index f3c75f7143e..f65509e5b78 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -192,20 +192,15 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: ) def project_to_id(self, expression: ex.Expression, output_id: str): - if output_id in self.column_ids: # Mutate case - exprs = [ - ((expression if (col_id == output_id) else ex.free_var(col_id)), col_id) - for col_id in self.column_ids - ] - else: # append case - self_projection = ( - (ex.free_var(col_id), col_id) for col_id in self.column_ids - ) - exprs = [*self_projection, (expression, output_id)] return ArrayValue( nodes.ProjectionNode( child=self.node, - assignments=tuple(exprs), + assignments=( + ( + expression, + output_id, + ), + ), ) ) @@ -213,28 +208,22 @@ def assign(self, source_id: str, destination_id: str) -> ArrayValue: if destination_id in self.column_ids: # Mutate case exprs = [ ( - ( - ex.free_var(source_id) - if (col_id == destination_id) - else ex.free_var(col_id) - ), + (source_id if (col_id == destination_id) else col_id), col_id, ) for col_id in self.column_ids ] else: # append case - self_projection = ( - (ex.free_var(col_id), col_id) for col_id in self.column_ids - ) - exprs = [*self_projection, (ex.free_var(source_id), destination_id)] + self_projection = ((col_id, col_id) for col_id in self.column_ids) + exprs = [*self_projection, (source_id, destination_id)] return ArrayValue( - nodes.ProjectionNode( + nodes.SelectionNode( child=self.node, - assignments=tuple(exprs), + input_output_pairs=tuple(exprs), ) ) - def assign_constant( + def create_constant( self, destination_id: str, value: typing.Any, @@ -244,49 +233,31 @@ def assign_constant( # Need to assign a data type when value is NaN. dtype = dtype or bigframes.dtypes.DEFAULT_DTYPE - if destination_id in self.column_ids: # Mutate case - exprs = [ - ( - ( - ex.const(value, dtype) - if (col_id == destination_id) - else ex.free_var(col_id) - ), - col_id, - ) - for col_id in self.column_ids - ] - else: # append case - self_projection = ( - (ex.free_var(col_id), col_id) for col_id in self.column_ids - ) - exprs = [*self_projection, (ex.const(value, dtype), destination_id)] return ArrayValue( nodes.ProjectionNode( child=self.node, - assignments=tuple(exprs), + assignments=((ex.const(value, dtype), destination_id),), ) ) def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: - selections = ((ex.free_var(col_id), col_id) for col_id in column_ids) + # This basically just drops and reorders columns - logically a no-op except as a final step + selections = ((col_id, col_id) for col_id in column_ids) return ArrayValue( - nodes.ProjectionNode( + nodes.SelectionNode( child=self.node, - assignments=tuple(selections), + input_output_pairs=tuple(selections), ) ) def drop_columns(self, columns: Iterable[str]) -> ArrayValue: new_projection = ( - (ex.free_var(col_id), col_id) - for col_id in self.column_ids - if col_id not in columns + (col_id, col_id) for col_id in self.column_ids if col_id not in columns ) return ArrayValue( - nodes.ProjectionNode( + nodes.SelectionNode( child=self.node, - assignments=tuple(new_projection), + input_output_pairs=tuple(new_projection), ) ) @@ -422,15 +393,13 @@ def unpivot( col_expr = ops.case_when_op.as_expr(*cases) unpivot_exprs.append((col_expr, col_id)) - label_exprs = ((ex.free_var(id), id) for id in index_col_ids) - # passthrough columns are unchanged, just repeated N times each - passthrough_exprs = ((ex.free_var(id), id) for id in passthrough_columns) + unpivot_col_ids = [id for id, _ in unpivot_columns] return ArrayValue( nodes.ProjectionNode( child=joined_array.node, - assignments=(*label_exprs, *unpivot_exprs, *passthrough_exprs), + assignments=(*unpivot_exprs,), ) - ) + ).select_columns([*index_col_ids, *unpivot_col_ids, *passthrough_columns]) def _cross_join_w_labels( self, labels_array: ArrayValue, join_side: typing.Literal["left", "right"] diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index a309671842b..d7df7801bc3 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -939,7 +939,7 @@ def multi_apply_unary_op( for col_id in columns: label = self.col_id_to_label[col_id] block, result_id = block.project_expr( - expr.bind_all_variables({input_varname: ex.free_var(col_id)}), + expr.bind_variables({input_varname: ex.free_var(col_id)}), label=label, ) block = block.copy_values(result_id, col_id) @@ -1006,7 +1006,7 @@ def create_constant( dtype: typing.Optional[bigframes.dtypes.Dtype] = None, ) -> typing.Tuple[Block, str]: result_id = guid.generate_guid() - expr = self.expr.assign_constant(result_id, scalar_constant, dtype=dtype) + expr = self.expr.create_constant(result_id, scalar_constant, dtype=dtype) # Create index copy with label inserted # See: https://pandas.pydata.org/docs/reference/api/pandas.Index.insert.html labels = self.column_labels.insert(len(self.column_labels), label) @@ -1067,7 +1067,7 @@ def aggregate_all_and_stack( index_id = guid.generate_guid() result_expr = self.expr.aggregate( aggregations, dropna=dropna - ).assign_constant(index_id, None, None) + ).create_constant(index_id, None, None) # Transpose as last operation so that final block has valid transpose cache return Block( result_expr, @@ -1222,7 +1222,7 @@ def aggregate( names: typing.List[Label] = [] if len(by_column_ids) == 0: label_id = guid.generate_guid() - result_expr = result_expr.assign_constant(label_id, 0, pd.Int64Dtype()) + result_expr = result_expr.create_constant(label_id, 0, pd.Int64Dtype()) index_columns = (label_id,) names = [None] else: @@ -1614,17 +1614,22 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: axis_number = utils.get_axis_number("rows" if (axis is None) else axis) if axis_number == 0: expr = self._expr + new_index_cols = [] for index_col in self._index_columns: + new_col = guid.generate_guid() expr = expr.project_to_id( expression=ops.add_op.as_expr( ex.const(prefix), ops.AsTypeOp(to_type="string").as_expr(index_col), ), - output_id=index_col, + output_id=new_col, ) + new_index_cols.append(new_col) + expr = expr.select_columns((*new_index_cols, *self.value_columns)) + return Block( expr, - index_columns=self.index_columns, + index_columns=new_index_cols, column_labels=self.column_labels, index_labels=self.index.names, ) @@ -1635,17 +1640,21 @@ def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: axis_number = utils.get_axis_number("rows" if (axis is None) else axis) if axis_number == 0: expr = self._expr + new_index_cols = [] for index_col in self._index_columns: + new_col = guid.generate_guid() expr = expr.project_to_id( expression=ops.add_op.as_expr( ops.AsTypeOp(to_type="string").as_expr(index_col), ex.const(suffix), ), - output_id=index_col, + output_id=new_col, ) + new_index_cols.append(new_col) + expr = expr.select_columns((*new_index_cols, *self.value_columns)) return Block( expr, - index_columns=self.index_columns, + index_columns=new_index_cols, column_labels=self.column_labels, index_labels=self.index.names, ) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 512238440cf..9a9f598e892 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -134,10 +134,23 @@ def projection( ) -> T: """Apply an expression to the ArrayValue and assign the output to a column.""" bindings = {col: self._get_ibis_column(col) for col in self.column_ids} - values = [ + new_values = [ op_compiler.compile_expression(expression, bindings).name(id) for expression, id in expression_id_pairs ] + result = self._select(tuple([*self._columns, *new_values])) # type: ignore + return result + + def selection( + self: T, + input_output_pairs: typing.Tuple[typing.Tuple[str, str], ...], + ) -> T: + """Apply an expression to the ArrayValue and assign the output to a column.""" + bindings = {col: self._get_ibis_column(col) for col in self.column_ids} + values = [ + op_compiler.compile_expression(ex.free_var(input), bindings).name(id) + for input, id in input_output_pairs + ] result = self._select(tuple(values)) # type: ignore return result diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 3fedf5c0c80..80d5f5a8932 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -264,6 +264,11 @@ def compile_reversed(self, node: nodes.ReversedNode, ordered: bool = True): else: return self.compile_unordered_ir(node.child) + @_compile_node.register + def compile_selection(self, node: nodes.SelectionNode, ordered: bool = True): + result = self.compile_node(node.child, ordered) + return result.selection(node.input_output_pairs) + @_compile_node.register def compile_projection(self, node: nodes.ProjectionNode, ordered: bool = True): result = self.compile_node(node.child, ordered) diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index c216c297172..bbd23b689cf 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -110,8 +110,13 @@ def output_type( ... @abc.abstractmethod - def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: - """Replace all variables with expression given in `bindings`.""" + def bind_variables( + self, bindings: Mapping[str, Expression], check_bind_all: bool = True + ) -> Expression: + """Replace variables with expression given in `bindings`. + + If check_bind_all is True, validate that all free variables are bound to a new value. + """ ... @property @@ -141,7 +146,9 @@ def output_type( ) -> dtypes.ExpressionType: return self.dtype - def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: + def bind_variables( + self, bindings: Mapping[str, Expression], check_bind_all: bool = True + ) -> Expression: return self @property @@ -178,11 +185,14 @@ def output_type( else: raise ValueError(f"Type of variable {self.id} has not been fixed.") - def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: + def bind_variables( + self, bindings: Mapping[str, Expression], check_bind_all: bool = True + ) -> Expression: if self.id in bindings.keys(): return bindings[self.id] - else: + elif check_bind_all: raise ValueError(f"Variable {self.id} remains unbound") + return self @property def is_bijective(self) -> bool: @@ -225,10 +235,15 @@ def output_type( ) return self.op.output_type(*operand_types) - def bind_all_variables(self, bindings: Mapping[str, Expression]) -> Expression: + def bind_variables( + self, bindings: Mapping[str, Expression], check_bind_all: bool = True + ) -> Expression: return OpExpression( self.op, - tuple(input.bind_all_variables(bindings) for input in self.inputs), + tuple( + input.bind_variables(bindings, check_bind_all=check_bind_all) + for input in self.inputs + ), ) @property diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 73780719a92..27e76c79104 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -622,8 +622,32 @@ def relation_ops_created(self) -> int: return 0 +@dataclass(frozen=True) +class SelectionNode(UnaryNode): + input_output_pairs: typing.Tuple[typing.Tuple[str, str], ...] + + def __hash__(self): + return self._node_hash + + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + input_types = self.child.schema._mapping + items = tuple( + schemata.SchemaItem(output, input_types[input]) + for input, output in self.input_output_pairs + ) + return schemata.ArraySchema(items) + + @property + def variables_introduced(self) -> int: + # This operation only renames variables, doesn't actually create new ones + return 0 + + @dataclass(frozen=True) class ProjectionNode(UnaryNode): + """Assigns new variables (without modifying existing ones)""" + assignments: typing.Tuple[typing.Tuple[ex.Expression, str], ...] def __post_init__(self): @@ -631,6 +655,8 @@ def __post_init__(self): for expression, id in self.assignments: # throws TypeError if invalid _ = expression.output_type(input_types) + # Cannot assign to existing variables - append only! + assert all(name not in self.child.schema.names for _, name in self.assignments) def __hash__(self): return self._node_hash @@ -644,7 +670,10 @@ def schema(self) -> schemata.ArraySchema: ) for ex, id in self.assignments ) - return schemata.ArraySchema(items) + schema = self.child.schema + for item in items: + schema = schema.append(item) + return schema @property def variables_introduced(self) -> int: diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index bff7e2ce44b..a57d7a18d63 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -63,7 +63,7 @@ def bind_variables( self, mapping: Mapping[str, expression.Expression] ) -> OrderingExpression: return OrderingExpression( - self.scalar_expression.bind_all_variables(mapping), + self.scalar_expression.bind_variables(mapping), self.direction, self.na_last, ) diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index 60ed4069a9c..0e73166ea59 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -27,6 +27,7 @@ Selection = Tuple[Tuple[scalar_exprs.Expression, str], ...] REWRITABLE_NODE_TYPES = ( + nodes.SelectionNode, nodes.ProjectionNode, nodes.FilterNode, nodes.ReversedNode, @@ -54,7 +55,12 @@ def from_node_span( for id in get_node_column_ids(node) ) return cls(node, selection, None, ()) - if isinstance(node, nodes.ProjectionNode): + + if isinstance(node, nodes.SelectionNode): + return cls.from_node_span(node.child, target).select( + node.input_output_pairs + ) + elif isinstance(node, nodes.ProjectionNode): return cls.from_node_span(node.child, target).project(node.assignments) elif isinstance(node, nodes.FilterNode): return cls.from_node_span(node.child, target).filter(node.predicate) @@ -69,22 +75,39 @@ def from_node_span( def column_lookup(self) -> Mapping[str, scalar_exprs.Expression]: return {col_id: expr for expr, col_id in self.columns} + def select(self, input_output_pairs: Tuple[Tuple[str, str], ...]) -> SquashedSelect: + new_columns = tuple( + ( + scalar_exprs.free_var(input).bind_variables(self.column_lookup), + output, + ) + for input, output in input_output_pairs + ) + return SquashedSelect( + self.root, new_columns, self.predicate, self.ordering, self.reverse_root + ) + def project( self, projection: Tuple[Tuple[scalar_exprs.Expression, str], ...] ) -> SquashedSelect: + existing_columns = self.columns new_columns = tuple( - (expr.bind_all_variables(self.column_lookup), id) for expr, id in projection + (expr.bind_variables(self.column_lookup), id) for expr, id in projection ) return SquashedSelect( - self.root, new_columns, self.predicate, self.ordering, self.reverse_root + self.root, + (*existing_columns, *new_columns), + self.predicate, + self.ordering, + self.reverse_root, ) def filter(self, predicate: scalar_exprs.Expression) -> SquashedSelect: if self.predicate is None: - new_predicate = predicate.bind_all_variables(self.column_lookup) + new_predicate = predicate.bind_variables(self.column_lookup) else: new_predicate = ops.and_op.as_expr( - self.predicate, predicate.bind_all_variables(self.column_lookup) + self.predicate, predicate.bind_variables(self.column_lookup) ) return SquashedSelect( self.root, self.columns, new_predicate, self.ordering, self.reverse_root @@ -204,7 +227,11 @@ def expand(self) -> nodes.BigFrameNode: root = nodes.FilterNode(child=root, predicate=self.predicate) if self.ordering: root = nodes.OrderByNode(child=root, by=self.ordering) - return nodes.ProjectionNode(child=root, assignments=self.columns) + selection = tuple((id, id) for _, id in self.columns) + return nodes.SelectionNode( + child=nodes.ProjectionNode(child=root, assignments=self.columns), + input_output_pairs=selection, + ) def join_as_projection( diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 72d54932943..424e6d7dad6 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -457,9 +457,7 @@ def generate_head_plan(node: nodes.BigFrameNode, n: int): predicate = ops.lt_op.as_expr(ex.free_var(offsets_id), ex.const(n)) plan_w_head = nodes.FilterNode(plan_w_offsets, predicate) # Finally, drop the offsets column - return nodes.ProjectionNode( - plan_w_head, tuple((ex.free_var(i), i) for i in node.schema.names) - ) + return nodes.SelectionNode(plan_w_head, tuple((i, i) for i in node.schema.names)) def generate_row_count_plan(node: nodes.BigFrameNode): diff --git a/bigframes/session/planner.py b/bigframes/session/planner.py index 2a74521b43c..bc640ec9fac 100644 --- a/bigframes/session/planner.py +++ b/bigframes/session/planner.py @@ -33,7 +33,7 @@ def session_aware_cache_plan( """ node_counts = traversals.count_nodes(session_forest) # These node types are cheap to re-compute, so it makes more sense to cache their children. - de_cachable_types = (nodes.FilterNode, nodes.ProjectionNode) + de_cachable_types = (nodes.FilterNode, nodes.ProjectionNode, nodes.SelectionNode) caching_target = cur_node = root caching_target_refs = node_counts.get(caching_target, 0) @@ -49,7 +49,15 @@ def session_aware_cache_plan( # Projection defines the variables that are used in the filter expressions, need to substitute variables with their scalar expressions # that instead reference variables in the child node. bindings = {name: expr for expr, name in cur_node.assignments} - filters = [i.bind_all_variables(bindings) for i in filters] + filters = [ + i.bind_variables(bindings, check_bind_all=False) for i in filters + ] + elif isinstance(cur_node, nodes.SelectionNode): + bindings = { + output: ex.free_var(input) + for input, output in cur_node.input_output_pairs + } + filters = [i.bind_variables(bindings) for i in filters] else: raise ValueError(f"Unexpected de-cached node: {cur_node}") diff --git a/tests/unit/test_planner.py b/tests/unit/test_planner.py index 2e276d0f1ae..84dd05ddaa1 100644 --- a/tests/unit/test_planner.py +++ b/tests/unit/test_planner.py @@ -46,8 +46,8 @@ def test_session_aware_caching_project_filter(): """ Test that if a node is filtered by a column, the node is cached pre-filter and clustered by the filter column. """ - session_objects = [LEAF, LEAF.assign_constant("col_c", 4, pd.Int64Dtype())] - target = LEAF.assign_constant("col_c", 4, pd.Int64Dtype()).filter( + session_objects = [LEAF, LEAF.create_constant("col_c", 4, pd.Int64Dtype())] + target = LEAF.create_constant("col_c", 4, pd.Int64Dtype()).filter( ops.gt_op.as_expr("col_a", ex.const(3)) ) result, cluster_cols = planner.session_aware_cache_plan( @@ -61,14 +61,14 @@ def test_session_aware_caching_project_multi_filter(): """ Test that if a node is filtered by multiple columns, all of them are in the cluster cols """ - session_objects = [LEAF, LEAF.assign_constant("col_c", 4, pd.Int64Dtype())] + session_objects = [LEAF, LEAF.create_constant("col_c", 4, pd.Int64Dtype())] predicate_1a = ops.gt_op.as_expr("col_a", ex.const(3)) predicate_1b = ops.lt_op.as_expr("col_a", ex.const(55)) predicate_1 = ops.and_op.as_expr(predicate_1a, predicate_1b) predicate_3 = ops.eq_op.as_expr("col_b", ex.const(1)) target = ( LEAF.filter(predicate_1) - .assign_constant("col_c", 4, pd.Int64Dtype()) + .create_constant("col_c", 4, pd.Int64Dtype()) .filter(predicate_3) ) result, cluster_cols = planner.session_aware_cache_plan( @@ -84,8 +84,8 @@ def test_session_aware_caching_unusable_filter(): Most filters with multiple column references cannot be used for scan pruning, as they cannot be converted to fixed value ranges. """ - session_objects = [LEAF, LEAF.assign_constant("col_c", 4, pd.Int64Dtype())] - target = LEAF.assign_constant("col_c", 4, pd.Int64Dtype()).filter( + session_objects = [LEAF, LEAF.create_constant("col_c", 4, pd.Int64Dtype())] + target = LEAF.create_constant("col_c", 4, pd.Int64Dtype()).filter( ops.gt_op.as_expr("col_a", "col_b") ) result, cluster_cols = planner.session_aware_cache_plan( @@ -101,12 +101,12 @@ def test_session_aware_caching_fork_after_window_op(): Windowing is expensive, so caching should always compute the window function, in order to avoid later recomputation. """ - other = LEAF.promote_offsets("offsets_col").assign_constant( + other = LEAF.promote_offsets("offsets_col").create_constant( "col_d", 5, pd.Int64Dtype() ) target = ( LEAF.promote_offsets("offsets_col") - .assign_constant("col_c", 4, pd.Int64Dtype()) + .create_constant("col_c", 4, pd.Int64Dtype()) .filter( ops.eq_op.as_expr("col_a", ops.add_op.as_expr(ex.const(4), ex.const(3))) ) From a2640a2d731c8d0aba1307311092f5e85b8ba077 Mon Sep 17 00:00:00 2001 From: Arwa Sharif <146148342+arwas11@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:41:49 -0500 Subject: [PATCH 805/921] docs: add docstring returns section to Options (#937) --- bigframes/_config/__init__.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index c9b2a3f95a9..ac58c19fa5a 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -73,7 +73,12 @@ def _init_bigquery_thread_local(self): @property def bigquery(self) -> bigquery_options.BigQueryOptions: - """Options to use with the BigQuery engine.""" + """Options to use with the BigQuery engine. + + Returns: + bigframes._config.bigquery_options.BigQueryOptions: + Options for BigQuery engine. + """ if self._local.bigquery_options is not None: # The only way we can get here is if someone called # _init_bigquery_thread_local. @@ -83,7 +88,12 @@ def bigquery(self) -> bigquery_options.BigQueryOptions: @property def display(self) -> display_options.DisplayOptions: - """Options controlling object representation.""" + """Options controlling object representation. + + Returns: + bigframes._config.display_options.DisplayOptions: + Options for controlling object representation. + """ return self._local.display_options @property @@ -95,12 +105,21 @@ def sampling(self) -> sampling_options.SamplingOptions: (e.g., to_pandas, to_numpy, values) or implicitly (e.g., matplotlib plotting). This option can be overriden by parameters in specific functions. + + Returns: + bigframes._config.sampling_options.SamplingOptions: + Options for controlling downsampling. """ return self._local.sampling_options @property def compute(self) -> compute_options.ComputeOptions: - """Thread-local options controlling object computation.""" + """Thread-local options controlling object computation. + + Returns: + bigframes._config.compute_options.ComputeOptions: + Thread-local options for controlling object computation + """ return self._local.compute_options @property @@ -109,6 +128,11 @@ def is_bigquery_thread_local(self) -> bool: A thread-local session can be started by using `with bigframes.option_context("bigquery.some_option", "some-value"):`. + + Returns: + bool: + A boolean value, where a value is True if a thread-local session + is in use; otherwise False. """ return self._local.bigquery_options is not None From ac9f300842eff896366984371e986522d954e16a Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:42:09 -0700 Subject: [PATCH 806/921] chore: drop unused columns at is_monotonic methods (#912) * chore: drop unused columns at is_monotonic methods * fixing mypy --- bigframes/core/blocks.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index d7df7801bc3..4db171ec70a 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2429,9 +2429,11 @@ def _is_monotonic( block, last_notna_id = self.apply_unary_op(column_ids[0], ops.notnull_op) for column_id in column_ids[1:]: block, notna_id = block.apply_unary_op(column_id, ops.notnull_op) + old_last_notna_id = last_notna_id block, last_notna_id = block.apply_binary_op( - last_notna_id, notna_id, ops.and_op + old_last_notna_id, notna_id, ops.and_op ) + block.drop_columns([notna_id, old_last_notna_id]) # loop over all columns to check monotonicity last_result_id = None @@ -2443,21 +2445,27 @@ def _is_monotonic( column_id, lag_result_id, ops.gt_op if increasing else ops.lt_op ) block, equal_id = block.apply_binary_op(column_id, lag_result_id, ops.eq_op) + block = block.drop_columns([lag_result_id]) if last_result_id is None: block, last_result_id = block.apply_binary_op( equal_id, strict_monotonic_id, ops.or_op ) - continue - block, equal_monotonic_id = block.apply_binary_op( - equal_id, last_result_id, ops.and_op - ) - block, last_result_id = block.apply_binary_op( - equal_monotonic_id, strict_monotonic_id, ops.or_op - ) + block = block.drop_columns([equal_id, strict_monotonic_id]) + else: + block, equal_monotonic_id = block.apply_binary_op( + equal_id, last_result_id, ops.and_op + ) + block = block.drop_columns([equal_id, last_result_id]) + block, last_result_id = block.apply_binary_op( + equal_monotonic_id, strict_monotonic_id, ops.or_op + ) + block = block.drop_columns([equal_monotonic_id, strict_monotonic_id]) block, monotonic_result_id = block.apply_binary_op( last_result_id, last_notna_id, ops.and_op # type: ignore ) + if last_result_id is not None: + block = block.drop_columns([last_result_id, last_notna_id]) result = block.get_stat(monotonic_result_id, agg_ops.all_op) self._stats_cache[column_name].update({op_name: result}) return result From 1f419eb87916c83b390e66d580c5119e70c023e7 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 5 Sep 2024 22:18:38 -0700 Subject: [PATCH 807/921] test: retry streaming tests to accommodate flakiness (#956) * test: retry streaming tests to accommodate flakiness * reduce delay, increase retries --- tests/system/large/test_streaming.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/system/large/test_streaming.py b/tests/system/large/test_streaming.py index 391aec85333..e4992f85738 100644 --- a/tests/system/large/test_streaming.py +++ b/tests/system/large/test_streaming.py @@ -14,10 +14,13 @@ import time +import pytest + import bigframes import bigframes.streaming +@pytest.mark.flaky(retries=3, delay=10) def test_streaming_df_to_bigtable(session_load: bigframes.Session): # launch a continuous query job_id_prefix = "test_streaming_" @@ -51,6 +54,7 @@ def test_streaming_df_to_bigtable(session_load: bigframes.Session): query_job.cancel() +@pytest.mark.flaky(retries=3, delay=10) def test_streaming_df_to_pubsub(session_load: bigframes.Session): # launch a continuous query job_id_prefix = "test_streaming_pubsub_" From c750be6093941677572a10c36a92984e954de32c Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 5 Sep 2024 23:22:53 -0700 Subject: [PATCH 808/921] fix: make `read_gbq_function` work for multi-param functions (#947) * fix: make `read_gbq_function` work for multi-param functions * fix hyperlink * specify hyperlink differently * make hyperlink markdown format --- bigframes/functions/remote_function.py | 8 +++++++ bigframes/session/__init__.py | 27 ++++++++++++++++++---- tests/system/small/test_remote_function.py | 9 +++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 7e9df74e763..ddb36a9bef5 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -14,6 +14,7 @@ from __future__ import annotations +import inspect import logging from typing import cast, Optional, TYPE_CHECKING import warnings @@ -149,6 +150,13 @@ def func(*ignored_args, **ignored_kwargs): expr = node(*ignored_args, **ignored_kwargs) # type: ignore return ibis_client.execute(expr) + func.__signature__ = inspect.signature(func).replace( # type: ignore + parameters=[ + inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) + for name in ibis_signature.parameter_names + ] + ) + # TODO: Move ibis logic to compiler step func.__name__ = routine_ref.routine_id diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 7aa4ed4b5a9..e52e2ef17f7 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1241,12 +1241,22 @@ def read_gbq_function( **Examples:** - Use the ``cw_lower_case_ascii_only`` function from Community UDFs. - (https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/cw_lower_case_ascii_only.sqlx) - >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None + Use the [cw_lower_case_ascii_only](https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/README.md#cw_lower_case_ascii_onlystr-string) + function from Community UDFs. + + >>> func = bpd.read_gbq_function("bqutil.fn.cw_lower_case_ascii_only") + + You can run it on scalar input. Usually you would do so to verify that + it works as expected before applying to all values in a Series. + + >>> func('AURÉLIE') + 'aurÉlie' + + You can apply it to a BigQuery DataFrame Series. + >>> df = bpd.DataFrame({'id': [1, 2, 3], 'name': ['AURÉLIE', 'CÉLESTINE', 'DAPHNÉ']}) >>> df id name @@ -1256,7 +1266,6 @@ def read_gbq_function( [3 rows x 2 columns] - >>> func = bpd.read_gbq_function("bqutil.fn.cw_lower_case_ascii_only") >>> df1 = df.assign(new_name=df['name'].apply(func)) >>> df1 id name new_name @@ -1266,9 +1275,17 @@ def read_gbq_function( [3 rows x 3 columns] + You can even use a function with multiple inputs. For example, let's use + [cw_instr4](https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/README.md#cw_instr4source-string-search-string-position-int64-ocurrence-int64) + from Community UDFs. + + >>> func = bpd.read_gbq_function("bqutil.fn.cw_instr4") + >>> func('TestStr123456Str', 'Str', 1, 2) + 14 + Args: function_name (str): - the function's name in BigQuery in the format + The function's name in BigQuery in the format `project_id.dataset_id.function_name`, or `dataset_id.function_name` to load from the default project, or `function_name` to load from the default project and the dataset diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index db573efa40e..b000354ed4f 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -671,12 +671,19 @@ def square1(x): @pytest.mark.flaky(retries=2, delay=120) -def test_read_gbq_function_runs_existing_udf(session, bigquery_client, dataset_id): +def test_read_gbq_function_runs_existing_udf(session): func = session.read_gbq_function("bqutil.fn.cw_lower_case_ascii_only") got = func("AURÉLIE") assert got == "aurÉlie" +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_runs_existing_udf_4_params(session): + func = session.read_gbq_function("bqutil.fn.cw_instr4") + got = func("TestStr123456Str", "Str", 1, 2) + assert got == 14 + + @pytest.mark.flaky(retries=2, delay=120) def test_read_gbq_function_reads_udfs(session, bigquery_client, dataset_id): dataset_ref = bigquery.DatasetReference.from_string(dataset_id) From 86e54b13d2b91517b1df2d9c1f852a8e1925309a Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 6 Sep 2024 09:59:54 -0700 Subject: [PATCH 809/921] fix: support `read_gbq_function` for axis=1 application (#950) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: support `read_gbq_function` for axis=1 application * remove stray newline * Update bigframes/session/__init__.py * remove first person reference in the doc * use correct product name --------- Co-authored-by: Tim Sweña (Swast) --- .../functions/_remote_function_session.py | 2 +- bigframes/functions/remote_function.py | 2 + bigframes/pandas/__init__.py | 3 +- bigframes/session/__init__.py | 38 ++++++++++++++++--- tests/system/large/test_remote_function.py | 14 +++++++ 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py index 0ab19ca353c..c69e430836c 100644 --- a/bigframes/functions/_remote_function_session.py +++ b/bigframes/functions/_remote_function_session.py @@ -176,7 +176,7 @@ def remote_function( getting and setting IAM roles on cloud resources. If this param is not provided then resource manager client from the session would be used. - dataset (str, Optional.): + dataset (str, Optional): Dataset in which to create a BigQuery remote function. It should be in `.` or `` format. If this parameter is not provided then session dataset id is used. diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index ddb36a9bef5..39e3bfd8f09 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -108,6 +108,7 @@ def read_gbq_function( function_name: str, *, session: Session, + is_row_processor: bool = False, ): """ Read an existing BigQuery function and prepare it for use in future queries. @@ -194,5 +195,6 @@ def func(*ignored_args, **ignored_kwargs): func.output_dtype = bigframes.core.compile.ibis_types.ibis_dtype_to_bigframes_dtype( # type: ignore ibis_signature.output_type ) + func.is_row_processor = is_row_processor # type: ignore func.ibis_node = node # type: ignore return func diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 08d808572d2..9f33a8a1ea6 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -692,10 +692,11 @@ def remote_function( remote_function.__doc__ = inspect.getdoc(bigframes.session.Session.remote_function) -def read_gbq_function(function_name: str): +def read_gbq_function(function_name: str, is_row_processor: bool = False): return global_session.with_default_session( bigframes.session.Session.read_gbq_function, function_name=function_name, + is_row_processor=is_row_processor, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index e52e2ef17f7..045483bd53b 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1225,6 +1225,7 @@ def remote_function( def read_gbq_function( self, function_name: str, + is_row_processor: bool = False, ): """Loads a BigQuery function from BigQuery. @@ -1255,7 +1256,7 @@ def read_gbq_function( >>> func('AURÉLIE') 'aurÉlie' - You can apply it to a BigQuery DataFrame Series. + You can apply it to a BigQuery DataFrames Series. >>> df = bpd.DataFrame({'id': [1, 2, 3], 'name': ['AURÉLIE', 'CÉLESTINE', 'DAPHNÉ']}) >>> df @@ -1275,13 +1276,33 @@ def read_gbq_function( [3 rows x 3 columns] - You can even use a function with multiple inputs. For example, let's use - [cw_instr4](https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/README.md#cw_instr4source-string-search-string-position-int64-ocurrence-int64) + You can even use a function with multiple inputs. For example, + [cw_regexp_replace_5](https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/README.md#cw_regexp_replace_5haystack-string-regexp-string-replacement-string-offset-int64-occurrence-int64) from Community UDFs. - >>> func = bpd.read_gbq_function("bqutil.fn.cw_instr4") - >>> func('TestStr123456Str', 'Str', 1, 2) - 14 + >>> func = bpd.read_gbq_function("bqutil.fn.cw_regexp_replace_5") + >>> func('TestStr123456', 'Str', 'Cad$', 1, 1) + 'TestCad$123456' + + >>> df = bpd.DataFrame({ + ... "haystack" : ["TestStr123456", "TestStr123456Str", "TestStr123456Str"], + ... "regexp" : ["Str", "Str", "Str"], + ... "replacement" : ["Cad$", "Cad$", "Cad$"], + ... "offset" : [1, 1, 1], + ... "occurrence" : [1, 2, 1] + ... }) + >>> df + haystack regexp replacement offset occurrence + 0 TestStr123456 Str Cad$ 1 1 + 1 TestStr123456Str Str Cad$ 1 2 + 2 TestStr123456Str Str Cad$ 1 1 + + [3 rows x 5 columns] + >>> df.apply(func, axis=1) + 0 TestCad$123456 + 1 TestStr123456Cad$ + 2 TestCad$123456Str + dtype: string Args: function_name (str): @@ -1290,6 +1311,10 @@ def read_gbq_function( `dataset_id.function_name` to load from the default project, or `function_name` to load from the default project and the dataset associated with the current session. + is_row_processor (bool, default False): + Whether the function is a row processor. This is set to True + for a function which receives an entire row of a DataFrame as + a pandas Series. Returns: callable: A function object pointing to the BigQuery function read @@ -1303,6 +1328,7 @@ def read_gbq_function( return bigframes_rf.read_gbq_function( function_name=function_name, session=self, + is_row_processor=is_row_processor, ) def _prepare_copy_job_config(self) -> bigquery.CopyJobConfig: diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index d6eefc1e31a..77ea4627ec5 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1603,6 +1603,13 @@ def serialize_row(row): # bf_result.dtype is 'string[pyarrow]' while pd_result.dtype is 'object' # , ignore this mismatch by using check_dtype=False. pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + # Let's make sure the read_gbq_function path works for this function + serialize_row_reuse = session.read_gbq_function( + serialize_row_remote.bigframes_remote_function, is_row_processor=True + ) + bf_result = scalars_df[columns].apply(serialize_row_reuse, axis=1).to_pandas() + pandas.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( @@ -2085,6 +2092,13 @@ def foo(x, y, z): pandas.testing.assert_series_equal( expected_result, bf_result, check_dtype=False, check_index_type=False ) + + # Let's make sure the read_gbq_function path works for this function + foo_reuse = session.read_gbq_function(foo.bigframes_remote_function) + bf_result = bf_df.apply(foo_reuse, axis=1).to_pandas() + pandas.testing.assert_series_equal( + expected_result, bf_result, check_dtype=False, check_index_type=False + ) finally: # clean up the gcp assets created for the remote function cleanup_remote_function_assets( From cd62e604967adac0c2f8600408bd9ce7886f2f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 9 Sep 2024 10:45:50 -0500 Subject: [PATCH 810/921] docs: update title of pypi notebook example to reflect use of the PyPI public dataset (#952) In response to feedback on internal change 662899733. --- notebooks/dataframes/pypi.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/dataframes/pypi.ipynb b/notebooks/dataframes/pypi.ipynb index 3777e98d428..7b16412ff58 100644 --- a/notebooks/dataframes/pypi.ipynb +++ b/notebooks/dataframes/pypi.ipynb @@ -25,7 +25,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Analyzing Python dependencies with BigQuery DataFrames\n", + "# Analyzing package downloads from PyPI with BigQuery DataFrames\n", "\n", "In this notebook, you'll use the [PyPI public dataset](https://console.cloud.google.com/marketplace/product/gcp-public-data-pypi/pypi) and the [deps.dev public dataset](https://deps.dev/) to visualize Python package downloads for a package and its dependencies.\n", "\n", From aeccc4842e2dae0731d09bbf5f1295bf95ebb44c Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 9 Sep 2024 19:03:40 -0700 Subject: [PATCH 811/921] test: adjust expectations in ml tests after bqml model update (#972) --- tests/system/small/ml/test_ensemble.py | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/system/small/ml/test_ensemble.py b/tests/system/small/ml/test_ensemble.py index 55d9fef661a..42aa3809567 100644 --- a/tests/system/small/ml/test_ensemble.py +++ b/tests/system/small/ml/test_ensemble.py @@ -39,12 +39,12 @@ def test_xgbregressor_model_score( result = penguins_xgbregressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "mean_absolute_error": [108.77582], - "mean_squared_error": [20943.272738], - "mean_squared_log_error": [0.00135], - "median_absolute_error": [86.313477], - "r2_score": [0.967571], - "explained_variance": [0.967609], + "mean_absolute_error": [115.57598], + "mean_squared_error": [23455.52121], + "mean_squared_log_error": [0.00147], + "median_absolute_error": [88.01318], + "r2_score": [0.96368], + "explained_variance": [0.96384], }, dtype="Float64", ) @@ -76,12 +76,12 @@ def test_xgbregressor_model_score_series( result = penguins_xgbregressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "mean_absolute_error": [108.77582], - "mean_squared_error": [20943.272738], - "mean_squared_log_error": [0.00135], - "median_absolute_error": [86.313477], - "r2_score": [0.967571], - "explained_variance": [0.967609], + "mean_absolute_error": [115.57598], + "mean_squared_error": [23455.52121], + "mean_squared_log_error": [0.00147], + "median_absolute_error": [88.01318], + "r2_score": [0.96368], + "explained_variance": [0.96384], }, dtype="Float64", ) @@ -136,12 +136,12 @@ def test_to_gbq_saved_xgbregressor_model_scores( result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "mean_absolute_error": [109.016973], - "mean_squared_error": [20867.299758], - "mean_squared_log_error": [0.00135], - "median_absolute_error": [86.490234], - "r2_score": [0.967458], - "explained_variance": [0.967504], + "mean_absolute_error": [115.57598], + "mean_squared_error": [23455.52121], + "mean_squared_log_error": [0.00147], + "median_absolute_error": [88.01318], + "r2_score": [0.96368], + "explained_variance": [0.96384], }, dtype="Float64", ) @@ -260,11 +260,11 @@ def test_to_gbq_saved_xgbclassifier_model_scores( result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "precision": [1.0], - "recall": [1.0], - "accuracy": [1.0], - "f1_score": [1.0], - "log_loss": [0.331442], + "precision": [0.662674], + "recall": [0.664646], + "accuracy": [0.994012], + "f1_score": [0.663657], + "log_loss": [0.374438], "roc_auc": [1.0], }, dtype="Float64", From 9ce10b4248f106ac9e09fc0fe686cece86827337 Mon Sep 17 00:00:00 2001 From: rey-esp Date: Tue, 10 Sep 2024 16:14:51 +0000 Subject: [PATCH 812/921] feat: add `__version__` alias to bigframes.pandas (#967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add new alias '__version__' * remove accidental changes * correct assignment --------- Co-authored-by: Tim Sweña (Swast) --- bigframes/functions/_remote_function_session.py | 2 +- bigframes/pandas/__init__.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py index c69e430836c..893b903aeb2 100644 --- a/bigframes/functions/_remote_function_session.py +++ b/bigframes/functions/_remote_function_session.py @@ -387,7 +387,7 @@ def wrapper(func): # https://docs.python.org/3/library/inspect.html#inspect.signature signature_kwargs: Mapping[str, Any] = {"eval_str": True} else: - signature_kwargs = {} + signature_kwargs = {} # type: ignore signature = inspect.signature( func, diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 9f33a8a1ea6..3809384c954 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -69,6 +69,7 @@ import bigframes.session import bigframes.session._io.bigquery import bigframes.session.clients +import bigframes.version try: import resource @@ -838,6 +839,7 @@ def clean_up_by_session_id( Index = bigframes.core.indexes.Index MultiIndex = bigframes.core.indexes.MultiIndex Series = bigframes.series.Series +__version__ = bigframes.version.__version__ # Other public pandas attributes NamedAgg = namedtuple("NamedAgg", ["column", "aggfunc"]) @@ -911,6 +913,7 @@ def reset_session(): "Index", "MultiIndex", "Series", + "__version__", # Other public pandas attributes "NamedAgg", "options", From e0eab7c6f5bbe8fcde7faa7800a579d35d873b77 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:16:26 -0700 Subject: [PATCH 813/921] chore: vendor ibis paritial codes for future v9 upgrade (#944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: vendor ibis paritial codes for future v9 upgrade * doctest skips ibis folder * Import from bigframes_vendored if possible --------- Co-authored-by: Tim Sweña (Swast) --- .pre-commit-config.yaml | 3 +- noxfile.py | 7 +- .../ibis/backends/bigquery/backend.py | 1259 +++++++++++++ .../ibis/backends/sql/__init__.py | 0 .../ibis/backends/sql/compilers/__init__.py | 7 + .../ibis/backends/sql/compilers/base.py | 1660 +++++++++++++++++ .../sql/compilers/bigquery/__init__.py | 1114 +++++++++++ .../ibis/backends/sql/rewrites.py | 367 ++++ .../bigframes_vendored/ibis/expr/rewrites.py | 380 ++++ 9 files changed, 4795 insertions(+), 2 deletions(-) create mode 100644 third_party/bigframes_vendored/ibis/backends/bigquery/backend.py create mode 100644 third_party/bigframes_vendored/ibis/backends/sql/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/backends/sql/compilers/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py create mode 100644 third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py create mode 100644 third_party/bigframes_vendored/ibis/backends/sql/rewrites.py create mode 100644 third_party/bigframes_vendored/ibis/expr/rewrites.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fd6488c9c7..2d11c951a1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,4 +39,5 @@ repos: hooks: - id: mypy additional_dependencies: [types-requests, types-tabulate, pandas-stubs] - args: ["--check-untyped-defs", "--explicit-package-bases", '--exclude="^third_party"', "--ignore-missing-imports"] + exclude: "^third_party" + args: ["--check-untyped-defs", "--explicit-package-bases", "--ignore-missing-imports"] diff --git a/noxfile.py b/noxfile.py index efe5a53082a..a7f05002106 100644 --- a/noxfile.py +++ b/noxfile.py @@ -384,7 +384,12 @@ def doctest(session: nox.sessions.Session): run_system( session=session, prefix_name="doctest", - extra_pytest_options=("--doctest-modules", "third_party"), + extra_pytest_options=( + "--doctest-modules", + "third_party", + "--ignore", + "third_party/bigframes_vendored/ibis", + ), test_folder="bigframes", check_cov=True, ) diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/backend.py b/third_party/bigframes_vendored/ibis/backends/bigquery/backend.py new file mode 100644 index 00000000000..f917ef950da --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/backend.py @@ -0,0 +1,1259 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/backends/bigquery/__init__.py + +"""BigQuery public API.""" + +from __future__ import annotations + +import concurrent.futures +import contextlib +import glob +import os +import re +from typing import Any, Optional, TYPE_CHECKING + +from bigframes_vendored.ibis.backends.bigquery.datatypes import BigQueryType +import google.api_core.exceptions +import google.auth.credentials +import google.cloud.bigquery as bq +import google.cloud.bigquery_storage_v1 as bqstorage +import ibis +from ibis import util +from ibis.backends import CanCreateDatabase, CanCreateSchema +from ibis.backends.bigquery.client import ( + bigquery_param, + parse_project_and_dataset, + rename_partitioned_column, + schema_from_bigquery_table, +) +from ibis.backends.bigquery.datatypes import BigQuerySchema +from ibis.backends.sql import SQLBackend +import ibis.backends.sql.compilers as sc +import ibis.common.exceptions as com +import ibis.expr.operations as ops +import ibis.expr.schema as sch +import ibis.expr.types as ir +import pydata_google_auth +from pydata_google_auth import cache +import sqlglot as sg +import sqlglot.expressions as sge + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + from pathlib import Path + from urllib.parse import ParseResult + + import pandas as pd + import polars as pl + import pyarrow as pa + + +SCOPES = ["https://www.googleapis.com/auth/bigquery"] +EXTERNAL_DATA_SCOPES = [ + "https://www.googleapis.com/auth/bigquery", + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/drive", +] +CLIENT_ID = "546535678771-gvffde27nd83kfl6qbrnletqvkdmsese.apps.googleusercontent.com" +CLIENT_SECRET = "iU5ohAF2qcqrujegE3hQ1cPt" # noqa: S105 + + +def _create_user_agent(application_name: str) -> str: + user_agent = [] + + if application_name: + user_agent.append(application_name) + + user_agent_default_template = f"ibis/{ibis.__version__}" + user_agent.append(user_agent_default_template) + + return " ".join(user_agent) + + +def _create_client_info(application_name): + from google.api_core.client_info import ClientInfo + + return ClientInfo(user_agent=_create_user_agent(application_name)) + + +def _create_client_info_gapic(application_name): + from google.api_core.gapic_v1.client_info import ClientInfo + + return ClientInfo(user_agent=_create_user_agent(application_name)) + + +_MEMTABLE_PATTERN = re.compile( + r"^_?ibis_(?:[A-Za-z_][A-Za-z_0-9]*)_memtable_[a-z0-9]{26}$" +) + + +def _qualify_memtable( + node: sge.Expression, *, dataset: str | None, project: str | None +) -> sge.Expression: + """Add a BigQuery dataset and project to memtable references.""" + if isinstance(node, sge.Table) and _MEMTABLE_PATTERN.match(node.name) is not None: + node.args["db"] = dataset + node.args["catalog"] = project + # make sure to quote table location + node = _force_quote_table(node) + return node + + +def _remove_null_ordering_from_unsupported_window( + node: sge.Expression, +) -> sge.Expression: + """Remove null ordering in window frame clauses not supported by BigQuery. + + BigQuery has only partial support for NULL FIRST/LAST in RANGE windows so + we remove it from any window frame clause that doesn't support it. + + Here's the support matrix: + + ✅ sum(x) over (order by y desc nulls last) + 🚫 sum(x) over (order by y asc nulls last) + ✅ sum(x) over (order by y asc nulls first) + 🚫 sum(x) over (order by y desc nulls first) + """ + if isinstance(node, sge.Window): + order = node.args.get("order") + if order is not None: + for key in order.args["expressions"]: + kargs = key.args + if kargs.get("desc") is True and kargs.get("nulls_first", False): + kargs["nulls_first"] = False + elif kargs.get("desc") is False and not kargs.setdefault( + "nulls_first", True + ): + kargs["nulls_first"] = True + return node + + +def _force_quote_table(table: sge.Table) -> sge.Table: + """Force quote all the parts of a bigquery path. + + https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers + + my-table is OK, but not mydataset.my-table + + mytable-287 is OK, but not mytable-287a + + Just quote everything. + """ + for key in ("this", "db", "catalog"): + if (val := table.args[key]) is not None: + if isinstance(val, sg.exp.Identifier) and not val.quoted: + val.args["quoted"] = True + else: + table.args[key] = sg.to_identifier(val, quoted=True) + return table + + +class Backend(SQLBackend, CanCreateDatabase, CanCreateSchema): + name = "bigquery" + compiler = sc.bigquery.compiler + supports_in_memory_tables = True + supports_python_udfs = False + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.__session_dataset: bq.DatasetReference | None = None + + @property + def _session_dataset(self): + if self.__session_dataset is None: + self.__session_dataset = self._make_session() + return self.__session_dataset + + def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: + raw_name = op.name + + session_dataset = self._session_dataset + project = session_dataset.project + dataset = session_dataset.dataset_id + + table_ref = bq.TableReference(session_dataset, raw_name) + try: + self.client.get_table(table_ref) + except google.api_core.exceptions.NotFound: + table_id = sg.table( + raw_name, db=dataset, catalog=project, quoted=False + ).sql(dialect=self.name) + bq_schema = BigQuerySchema.from_ibis(op.schema) + load_job = self.client.load_table_from_dataframe( + op.data.to_frame(), + table_id, + job_config=bq.LoadJobConfig( + # fail if the table already exists and contains data + write_disposition=bq.WriteDisposition.WRITE_EMPTY, + schema=bq_schema, + ), + ) + load_job.result() + + def _read_file( + self, + path: str | Path, + *, + table_name: str | None = None, + job_config: bq.LoadJobConfig, + ) -> ir.Table: + self._make_session() + + if table_name is None: + table_name = util.gen_name(f"bq_read_{job_config.source_format}") + + table_ref = self._session_dataset.table(table_name) + + database = self._session_dataset.dataset_id + catalog = self._session_dataset.project + + # drop the table if it exists + # + # we could do this with write_disposition = WRITE_TRUNCATE but then the + # concurrent append jobs aren't possible + # + # dropping the table first means all write_dispositions can be + # WRITE_APPEND + self.drop_table(table_name, database=(catalog, database), force=True) + + if os.path.isdir(path): + raise NotImplementedError("Reading from a directory is not supported.") + elif str(path).startswith("gs://"): + load_job = self.client.load_table_from_uri( + path, table_ref, job_config=job_config + ) + load_job.result() + else: + + def load(file: str) -> None: + with open(file, mode="rb") as f: + load_job = self.client.load_table_from_file( + f, table_ref, job_config=job_config + ) + load_job.result() + + job_config.write_disposition = bq.WriteDisposition.WRITE_APPEND + + with concurrent.futures.ThreadPoolExecutor() as executor: + for fut in concurrent.futures.as_completed( + executor.submit(load, file) for file in glob.glob(str(path)) + ): + fut.result() + + return self.table(table_name, database=(catalog, database)) + + def read_parquet( + self, path: str | Path, table_name: str | None = None, **kwargs: Any + ): + """Read Parquet data into a BigQuery table. + + Parameters + ---------- + path + Path to a Parquet file on GCS or the local filesystem. Globs are supported. + table_name + Optional table name + kwargs + Additional keyword arguments passed to `google.cloud.bigquery.LoadJobConfig`. + + Returns + ------- + Table + An Ibis table expression + + """ + return self._read_file( + path, + table_name=table_name, + job_config=bq.LoadJobConfig( + source_format=bq.SourceFormat.PARQUET, **kwargs + ), + ) + + def read_csv( + self, path: str | Path, table_name: str | None = None, **kwargs: Any + ) -> ir.Table: + """Read CSV data into a BigQuery table. + + Parameters + ---------- + path + Path to a CSV file on GCS or the local filesystem. Globs are supported. + table_name + Optional table name + kwargs + Additional keyword arguments passed to + `google.cloud.bigquery.LoadJobConfig`. + + Returns + ------- + Table + An Ibis table expression + + """ + job_config = bq.LoadJobConfig( + source_format=bq.SourceFormat.CSV, + autodetect=True, + skip_leading_rows=1, + **kwargs, + ) + return self._read_file(path, table_name=table_name, job_config=job_config) + + def read_json( + self, path: str | Path, table_name: str | None = None, **kwargs: Any + ) -> ir.Table: + """Read newline-delimited JSON data into a BigQuery table. + + Parameters + ---------- + path + Path to a newline-delimited JSON file on GCS or the local + filesystem. Globs are supported. + table_name + Optional table name + kwargs + Additional keyword arguments passed to + `google.cloud.bigquery.LoadJobConfig`. + + Returns + ------- + Table + An Ibis table expression + + """ + job_config = bq.LoadJobConfig( + source_format=bq.SourceFormat.NEWLINE_DELIMITED_JSON, + autodetect=True, + **kwargs, + ) + return self._read_file(path, table_name=table_name, job_config=job_config) + + def _from_url(self, url: ParseResult, **kwargs): + return self.connect( + project_id=url.netloc or kwargs.get("project_id", [""])[0], + dataset_id=url.path[1:] or kwargs.get("dataset_id", [""])[0], + **kwargs, + ) + + def do_connect( + self, + project_id: str | None = None, + dataset_id: str = "", + credentials: google.auth.credentials.Credentials | None = None, + application_name: str | None = None, + auth_local_webserver: bool = True, + auth_external_data: bool = False, + auth_cache: str = "default", + partition_column: str | None = "PARTITIONTIME", + client: bq.Client | None = None, + storage_client: bqstorage.BigQueryReadClient | None = None, + location: str | None = None, + ) -> Backend: + """Create a `Backend` for use with Ibis. + + Parameters + ---------- + project_id + A BigQuery project id. + dataset_id + A dataset id that lives inside of the project indicated by + `project_id`. + credentials + Optional credentials. + application_name + A string identifying your application to Google API endpoints. + auth_local_webserver + Use a local webserver for the user authentication. Binds a + webserver to an open port on localhost between 8080 and 8089, + inclusive, to receive authentication token. If not set, defaults to + False, which requests a token via the console. + auth_external_data + Authenticate using additional scopes required to `query external + data sources + `_, + such as Google Sheets, files in Google Cloud Storage, or files in + Google Drive. If not set, defaults to False, which requests the + default BigQuery scopes. + auth_cache + Selects the behavior of the credentials cache. + + `'default'`` + Reads credentials from disk if available, otherwise + authenticates and caches credentials to disk. + + `'reauth'`` + Authenticates and caches credentials to disk. + + `'none'`` + Authenticates and does **not** cache credentials. + + Defaults to `'default'`. + partition_column + Identifier to use instead of default `_PARTITIONTIME` partition + column. Defaults to `'PARTITIONTIME'`. + client + A `Client` from the `google.cloud.bigquery` package. If not + set, one is created using the `project_id` and `credentials`. + storage_client + A `BigQueryReadClient` from the + `google.cloud.bigquery_storage_v1` package. If not set, one is + created using the `project_id` and `credentials`. + location + Default location for BigQuery objects. + + Returns + ------- + Backend + An instance of the BigQuery backend. + + """ + default_project_id = client.project if client is not None else project_id + + # Only need `credentials` to create a `client` and + # `storage_client`, so only one or the other needs to be set. + if (client is None or storage_client is None) and credentials is None: + scopes = SCOPES + if auth_external_data: + scopes = EXTERNAL_DATA_SCOPES + + if auth_cache == "default": + credentials_cache = cache.ReadWriteCredentialsCache( + filename="ibis.json" + ) + elif auth_cache == "reauth": + credentials_cache = cache.WriteOnlyCredentialsCache( + filename="ibis.json" + ) + elif auth_cache == "none": + credentials_cache = cache.NOOP + else: + raise ValueError( + f"Got unexpected value for auth_cache = '{auth_cache}'. " + "Expected one of 'default', 'reauth', or 'none'." + ) + + credentials, default_project_id = pydata_google_auth.default( + scopes, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + credentials_cache=credentials_cache, + use_local_webserver=auth_local_webserver, + ) + + project_id = project_id or default_project_id + + ( + self.data_project, + self.billing_project, + self.dataset, + ) = parse_project_and_dataset(project_id, dataset_id) + + if client is not None: + self.client = client + else: + self.client = bq.Client( + project=self.billing_project, + credentials=credentials, + client_info=_create_client_info(application_name), + location=location, + ) + + if self.client.default_query_job_config is None: + self.client.default_query_job_config = bq.QueryJobConfig() + + self.client.default_query_job_config.use_legacy_sql = False + self.client.default_query_job_config.allow_large_results = True + + if storage_client is not None: + self.storage_client = storage_client + else: + self.storage_client = bqstorage.BigQueryReadClient( + credentials=credentials, + client_info=_create_client_info_gapic(application_name), + ) + + self.partition_column = partition_column + + @util.experimental + @classmethod + def from_connection( + cls, + client: bq.Client, + partition_column: str | None = "PARTITIONTIME", + storage_client: bqstorage.BigQueryReadClient | None = None, + dataset_id: str = "", + ) -> Backend: + """Create a BigQuery `Backend` from an existing `Client`. + + Parameters + ---------- + client + A `Client` from the `google.cloud.bigquery` package. + partition_column + Identifier to use instead of default `_PARTITIONTIME` partition + column. Defaults to `'PARTITIONTIME'`. + storage_client + A `BigQueryReadClient` from the `google.cloud.bigquery_storage_v1` + package. + dataset_id + A dataset id that lives inside of the project attached to `client`. + """ + return ibis.bigquery.connect( + client=client, + partition_column=partition_column, + storage_client=storage_client, + dataset_id=dataset_id, + ) + + def disconnect(self) -> None: + self.client.close() + + def _parse_project_and_dataset(self, dataset) -> tuple[str, str]: + if isinstance(dataset, sge.Table): + dataset = dataset.sql(self.dialect) + if not dataset and not self.dataset: + raise ValueError("Unable to determine BigQuery dataset.") + project, _, dataset = parse_project_and_dataset( + self.billing_project, + dataset or f"{self.data_project}.{self.dataset}", + ) + return project, dataset + + @property + def project_id(self): + return self.data_project + + @property + def dataset_id(self): + return self.dataset + + def create_database( + self, + name: str, + catalog: str | None = None, + force: bool = False, + collate: str | None = None, + **options: Any, + ) -> None: + properties = [ + sge.Property(this=sg.to_identifier(name), value=sge.convert(value)) + for name, value in (options or {}).items() + ] + + if collate is not None: + properties.append( + sge.CollateProperty(this=sge.convert(collate), default=True) + ) + + stmt = sge.Create( + kind="SCHEMA", + this=sg.table(name, db=catalog), + exists=force, + properties=sge.Properties(expressions=properties), + ) + + self.raw_sql(stmt.sql(self.name)) + + def drop_database( + self, + name: str, + catalog: str | None = None, + force: bool = False, + cascade: bool = False, + ) -> None: + """Drop a BigQuery dataset.""" + stmt = sge.Drop( + kind="SCHEMA", + this=sg.table(name, db=catalog), + exists=force, + cascade=cascade, + ) + + self.raw_sql(stmt.sql(self.name)) + + def table( + self, name: str, database: str | None = None, schema: str | None = None + ) -> ir.Table: + table_loc = self._warn_and_create_table_loc(database, schema) + table = sg.parse_one(f"`{name}`", into=sge.Table, read=self.name) + + # Bigquery, unlike other backends, had existing support for specifying + # table hierarchy in the table name, e.g. con.table("dataset.table_name") + # so here we have an extra layer of disambiguation to handle. + + # Default `catalog` to None unless we've parsed it out of the database/schema kwargs + # Raise if there are path specifications in both the name and as a kwarg + catalog = table_loc.args["catalog"] # args access will return None, not '' + if table.catalog: + if table_loc.catalog: + raise com.IbisInputError( + "Cannot specify catalog both in the table name and as an argument" + ) + else: + catalog = table.catalog + + # Default `db` to None unless we've parsed it out of the database/schema kwargs + db = table_loc.args["db"] # args access will return None, not '' + if table.db: + if table_loc.db: + raise com.IbisInputError( + "Cannot specify database both in the table name and as an argument" + ) + else: + db = table.db + + database = ( + sg.table(None, db=db, catalog=catalog, quoted=False).sql(dialect=self.name) + or None + ) + + project, dataset = self._parse_project_and_dataset(database) + + bq_table = self.client.get_table( + bq.TableReference( + bq.DatasetReference(project=project, dataset_id=dataset), + table.name, + ) + ) + + node = ops.DatabaseTable( + table.name, + # https://cloud.google.com/bigquery/docs/querying-wildcard-tables#filtering_selected_tables_using_table_suffix + schema=schema_from_bigquery_table(bq_table, wildcard=table.name[-1] == "*"), + source=self, + namespace=ops.Namespace(database=dataset, catalog=project), + ) + table_expr = node.to_expr() + return rename_partitioned_column(table_expr, bq_table, self.partition_column) + + def _make_session(self) -> tuple[str, str]: + if (client := getattr(self, "client", None)) is not None: + job_config = bq.QueryJobConfig(use_query_cache=False) + query = client.query( + "SELECT 1", job_config=job_config, project=self.billing_project + ) + query.result() + + return bq.DatasetReference( + project=query.destination.project, + dataset_id=query.destination.dataset_id, + ) + return None + + def _get_schema_using_query(self, query: str) -> sch.Schema: + job = self.client.query( + query, + job_config=bq.QueryJobConfig(dry_run=True, use_query_cache=False), + project=self.billing_project, + ) + return BigQuerySchema.to_ibis(job.schema) + + def raw_sql(self, query: str, params=None, page_size: int | None = None): + query_parameters = [ + bigquery_param( + param.type(), + value, + ( + param.get_name() + if not isinstance(op := param.op(), ops.Alias) + else op.arg.name + ), + ) + for param, value in (params or {}).items() + ] + with contextlib.suppress(AttributeError): + query = query.sql(self.dialect) + + job_config = bq.job.QueryJobConfig(query_parameters=query_parameters or []) + return self.client.query_and_wait( + query, + job_config=job_config, + project=self.billing_project, + page_size=page_size, + ) + + @property + def current_catalog(self) -> str: + return self.data_project + + @property + def current_database(self) -> str | None: + return self.dataset + + def compile( + self, + expr: ir.Expr, + limit: str | None = None, + params=None, + pretty: bool = True, + **kwargs: Any, + ): + """Compile an Ibis expression to a SQL string.""" + session_dataset = self._session_dataset + query = self.compiler.to_sqlglot( + expr, + limit=limit, + params=params, + session_dataset_id=getattr(session_dataset, "dataset_id", None), + session_project=getattr(session_dataset, "project", None), + **kwargs, + ) + queries = query if isinstance(query, list) else [query] + sql = ";\n".join(query.sql(self.dialect, pretty=pretty) for query in queries) + self._log(sql) + return sql + + def execute(self, expr, params=None, limit="default", **kwargs): + """Compile and execute the given Ibis expression. + + Compile and execute Ibis expression using this backend client + interface, returning results in-memory in the appropriate object type + + Parameters + ---------- + expr + Ibis expression to execute + limit + Retrieve at most this number of values/rows. Overrides any limit + already set on the expression. + params + Query parameters + kwargs + Extra arguments specific to the backend + + Returns + ------- + pd.DataFrame | pd.Series | scalar + Output from execution + + """ + from ibis.backends.bigquery.converter import BigQueryPandasData + + self._run_pre_execute_hooks(expr) + + schema = expr.as_table().schema() - ibis.schema({"_TABLE_SUFFIX": "string"}) + + sql = self.compile(expr, limit=limit, params=params, **kwargs) + self._log(sql) + query = self.raw_sql(sql, params=params, **kwargs) + + arrow_t = query.to_arrow( + progress_bar_type=None, bqstorage_client=self.storage_client + ) + + result = BigQueryPandasData.convert_table( + arrow_t.to_pandas(timestamp_as_object=True), schema + ) + + return expr.__pandas_result__(result, schema=schema) + + def insert( + self, + table_name: str, + obj: pd.DataFrame | ir.Table | list | dict, + schema: str | None = None, + database: str | None = None, + overwrite: bool = False, + ): + """Insert data into a table. + + Parameters + ---------- + table_name + The name of the table to which data needs will be inserted + obj + The source data or expression to insert + schema + The name of the schema that the table is located in + database + Name of the attached database that the table is located in. + overwrite + If `True` then replace existing contents of table + + """ + table_loc = self._warn_and_create_table_loc(database, schema) + catalog, db = self._to_catalog_db_tuple(table_loc) + if catalog is None: + catalog = self.current_catalog + if db is None: + db = self.current_database + + return super().insert( + table_name, + obj, + database=(catalog, db), + overwrite=overwrite, + ) + + def to_pyarrow( + self, + expr: ir.Expr, + *, + params: Mapping[ir.Scalar, Any] | None = None, + limit: int | str | None = None, + **kwargs: Any, + ) -> pa.Table: + self._import_pyarrow() + self._register_in_memory_tables(expr) + sql = self.compile(expr, limit=limit, params=params, **kwargs) + self._log(sql) + query = self.raw_sql(sql, params=params, **kwargs) + table = query.to_arrow( + progress_bar_type=None, bqstorage_client=self.storage_client + ) + table = table.rename_columns(list(expr.as_table().schema().names)) + return expr.__pyarrow_result__(table) + + def to_pyarrow_batches( + self, + expr: ir.Expr, + *, + params: Mapping[ir.Scalar, Any] | None = None, + limit: int | str | None = None, + chunk_size: int = 1_000_000, + **kwargs: Any, + ): + pa = self._import_pyarrow() + + schema = expr.as_table().schema() + + self._register_in_memory_tables(expr) + sql = self.compile(expr, limit=limit, params=params, **kwargs) + self._log(sql) + query = self.raw_sql(sql, params=params, page_size=chunk_size, **kwargs) + batch_iter = query.to_arrow_iterable(bqstorage_client=self.storage_client) + return pa.ipc.RecordBatchReader.from_batches(schema.to_pyarrow(), batch_iter) + + def _gen_udf_name(self, name: str, schema: Optional[str]) -> str: + func = ".".join(filter(None, (schema, name))) + if "." in func: + return ".".join(f"`{part}`" for part in func.split(".")) + return func + + def get_schema( + self, + name, + *, + catalog: str | None = None, + database: str | None = None, + ): + table_ref = bq.TableReference( + bq.DatasetReference( + project=catalog or self.data_project, + dataset_id=database or self.current_database, + ), + name, + ) + return schema_from_bigquery_table( + self.client.get_table(table_ref), + # https://cloud.google.com/bigquery/docs/querying-wildcard-tables#filtering_selected_tables_using_table_suffix + wildcard=name[-1] == "*", + ) + + def list_databases( + self, like: str | None = None, catalog: str | None = None + ) -> list[str]: + results = [ + dataset.dataset_id + for dataset in self.client.list_datasets( + project=catalog if catalog is not None else self.data_project + ) + ] + return self._filter_with_like(results, like) + + def list_tables( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the tables in the database. + + Parameters + ---------- + like + A pattern to use for listing tables. + database + The database location to perform the list against. + + By default uses the current `dataset` (`self.current_database`) and + `project` (`self.current_catalog`). + + To specify a table in a separate BigQuery dataset, you can pass in the + dataset and project as a string `"dataset.project"`, or as a tuple of + strings `("dataset", "project")`. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] The schema (dataset) inside `database` to perform the list against. + """ + table_loc = self._warn_and_create_table_loc(database, schema) + + project, dataset = self._parse_project_and_dataset(table_loc) + dataset_ref = bq.DatasetReference(project, dataset) + result = [table.table_id for table in self.client.list_tables(dataset_ref)] + return self._filter_with_like(result, like) + + def set_database(self, name): + self.data_project, self.dataset = self._parse_project_and_dataset(name) + + @property + def version(self): + return bq.__version__ + + def create_table( + self, + name: str, + obj: ir.Table + | pd.DataFrame + | pa.Table + | pl.DataFrame + | pl.LazyFrame + | None = None, + *, + schema: sch.SchemaLike | None = None, + database: str | None = None, + temp: bool = False, + overwrite: bool = False, + default_collate: str | None = None, + partition_by: str | None = None, + cluster_by: Iterable[str] | None = None, + options: Mapping[str, Any] | None = None, + ) -> ir.Table: + """Create a table in BigQuery. + + Parameters + ---------- + name + Name of the table to create + obj + The data with which to populate the table; optional, but one of `obj` + or `schema` must be specified + schema + The schema of the table to create; optional, but one of `obj` or + `schema` must be specified + database + The BigQuery *dataset* in which to create the table; optional + temp + Whether the table is temporary + overwrite + If `True`, replace the table if it already exists, otherwise fail if + the table exists + default_collate + Default collation for string columns. See BigQuery's documentation + for more details: https://cloud.google.com/bigquery/docs/reference/standard-sql/collation-concepts + partition_by + Partition the table by the given expression. See BigQuery's documentation + for more details: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#partition_expression + cluster_by + List of columns to cluster the table by. See BigQuery's documentation + for more details: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#clustering_column_list + options + BigQuery-specific table options; see the BigQuery documentation for + details: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#table_option_list + + Returns + ------- + Table + The table that was just created + + """ + if obj is None and schema is None: + raise com.IbisError("One of the `schema` or `obj` parameter is required") + if schema is not None: + schema = ibis.schema(schema) + + if isinstance(obj, ir.Table) and schema is not None: + if not schema.equals(obj.schema()): + raise com.IbisTypeError( + "Provided schema and Ibis table schema are incompatible. Please " + "align the two schemas, or provide only one of the two arguments." + ) + + project_id, dataset = self._parse_project_and_dataset(database) + + properties = [] + + if default_collate is not None: + properties.append( + sge.CollateProperty(this=sge.convert(default_collate), default=True) + ) + + if partition_by is not None: + properties.append( + sge.PartitionedByProperty( + this=sge.Tuple( + expressions=list(map(sg.to_identifier, partition_by)) + ) + ) + ) + + if cluster_by is not None: + properties.append( + sge.Cluster(expressions=list(map(sg.to_identifier, cluster_by))) + ) + + properties.extend( + sge.Property(this=sg.to_identifier(name), value=sge.convert(value)) + for name, value in (options or {}).items() + ) + + if obj is not None and not isinstance(obj, ir.Table): + obj = ibis.memtable(obj, schema=schema) + + if obj is not None: + self._register_in_memory_tables(obj) + + if temp: + dataset = self._session_dataset.dataset_id + if database is not None: + raise com.IbisInputError("Cannot specify database for temporary table") + database = self._session_dataset.project + else: + dataset = database or self.current_database + + try: + table = sg.parse_one(name, into=sge.Table, read="bigquery") + except sg.ParseError: + table = sg.table( + name, + db=dataset, + catalog=project_id, + quoted=self.compiler.quoted, + ) + else: + if table.args["db"] is None: + table.args["db"] = dataset + + if table.args["catalog"] is None: + table.args["catalog"] = project_id + + table = _force_quote_table(table) + + column_defs = [ + sge.ColumnDef( + this=sg.to_identifier(name, quoted=self.compiler.quoted), + kind=BigQueryType.from_ibis(typ), + constraints=( + None + if typ.nullable or typ.is_array() + else [sge.ColumnConstraint(kind=sge.NotNullColumnConstraint())] + ), + ) + for name, typ in (schema or {}).items() + ] + + stmt = sge.Create( + kind="TABLE", + this=sge.Schema(this=table, expressions=column_defs or None), + replace=overwrite, + properties=sge.Properties(expressions=properties), + expression=None if obj is None else self.compile(obj), + ) + + sql = stmt.sql(self.name) + + self.raw_sql(sql) + return self.table(table.name, database=(table.catalog, table.db)) + + def drop_table( + self, + name: str, + *, + schema: str | None = None, + database: tuple[str | str] | str | None = None, + force: bool = False, + ) -> None: + table_loc = self._warn_and_create_table_loc(database, schema) + catalog, db = self._to_catalog_db_tuple(table_loc) + stmt = sge.Drop( + kind="TABLE", + this=sg.table( + name, + db=db or self.current_database, + catalog=catalog or self.billing_project, + ), + exists=force, + ) + self.raw_sql(stmt.sql(self.name)) + + def create_view( + self, + name: str, + obj: ir.Table, + *, + schema: str | None = None, + database: str | None = None, + overwrite: bool = False, + ) -> ir.Table: + table_loc = self._warn_and_create_table_loc(database, schema) + catalog, db = self._to_catalog_db_tuple(table_loc) + + stmt = sge.Create( + kind="VIEW", + this=sg.table( + name, + db=db or self.current_database, + catalog=catalog or self.billing_project, + ), + expression=self.compile(obj), + replace=overwrite, + ) + self._register_in_memory_tables(obj) + self.raw_sql(stmt.sql(self.name)) + return self.table(name, database=(catalog, database)) + + def drop_view( + self, + name: str, + *, + schema: str | None = None, + database: str | None = None, + force: bool = False, + ) -> None: + table_loc = self._warn_and_create_table_loc(database, schema) + catalog, db = self._to_catalog_db_tuple(table_loc) + + stmt = sge.Drop( + kind="VIEW", + this=sg.table( + name, + db=db or self.current_database, + catalog=catalog or self.billing_project, + ), + exists=force, + ) + self.raw_sql(stmt.sql(self.name)) + + def _drop_cached_table(self, name): + self.drop_table( + name, + database=(self._session_dataset.project, self._session_dataset.dataset_id), + force=True, + ) + + def _register_udfs(self, expr: ir.Expr) -> None: + """No op because UDFs made with CREATE TEMPORARY FUNCTION must be followed by a query.""" + + @contextlib.contextmanager + def _safe_raw_sql(self, *args, **kwargs): + yield self.raw_sql(*args, **kwargs) + + # TODO: remove when the schema kwarg is removed + def _warn_and_create_table_loc(self, database=None, schema=None): + if schema is not None: + self._warn_schema() + if database is not None and schema is not None: + if isinstance(database, str): + table_loc = f"{database}.{schema}" + elif isinstance(database, tuple): + table_loc = database + schema + elif schema is not None: + table_loc = schema + elif database is not None: + table_loc = database + else: + table_loc = None + + table_loc = self._to_sqlglot_table(table_loc) + + if table_loc is not None: + if (sg_cat := table_loc.args["catalog"]) is not None: + sg_cat.args["quoted"] = False + if (sg_db := table_loc.args["db"]) is not None: + sg_db.args["quoted"] = False + + return table_loc + + +def compile(expr, params=None, **kwargs): + """Compile an expression for BigQuery.""" + backend = Backend() + return backend.compile(expr, params=params, **kwargs) + + +def connect( + project_id: str | None = None, + dataset_id: str = "", + credentials: google.auth.credentials.Credentials | None = None, + application_name: str | None = None, + auth_local_webserver: bool = False, + auth_external_data: bool = False, + auth_cache: str = "default", + partition_column: str | None = "PARTITIONTIME", +) -> Backend: + """Create a :class:`Backend` for use with Ibis. + + Parameters + ---------- + project_id + A BigQuery project id. + dataset_id + A dataset id that lives inside of the project indicated by + `project_id`. + credentials + Optional credentials. + application_name + A string identifying your application to Google API endpoints. + auth_local_webserver + Use a local webserver for the user authentication. Binds a + webserver to an open port on localhost between 8080 and 8089, + inclusive, to receive authentication token. If not set, defaults + to False, which requests a token via the console. + auth_external_data + Authenticate using additional scopes required to `query external + data sources + `_, + such as Google Sheets, files in Google Cloud Storage, or files in + Google Drive. If not set, defaults to False, which requests the + default BigQuery scopes. + auth_cache + Selects the behavior of the credentials cache. + + `'default'`` + Reads credentials from disk if available, otherwise + authenticates and caches credentials to disk. + + `'reauth'`` + Authenticates and caches credentials to disk. + + `'none'`` + Authenticates and does **not** cache credentials. + + Defaults to `'default'`. + partition_column + Identifier to use instead of default `_PARTITIONTIME` partition + column. Defaults to `'PARTITIONTIME'`. + + Returns + ------- + Backend + An instance of the BigQuery backend + + """ + backend = Backend() + return backend.connect( + project_id=project_id, + dataset_id=dataset_id, + credentials=credentials, + application_name=application_name, + auth_local_webserver=auth_local_webserver, + auth_external_data=auth_external_data, + auth_cache=auth_cache, + partition_column=partition_column, + ) + + +__all__ = [ + "Backend", + "compile", + "connect", +] diff --git a/third_party/bigframes_vendored/ibis/backends/sql/__init__.py b/third_party/bigframes_vendored/ibis/backends/sql/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/third_party/bigframes_vendored/ibis/backends/sql/compilers/__init__.py b/third_party/bigframes_vendored/ibis/backends/sql/compilers/__init__.py new file mode 100644 index 00000000000..b8a477dd4d0 --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/sql/compilers/__init__.py @@ -0,0 +1,7 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/backends/sql/compilers/__init__.py + +import bigframes_vendored.ibis.backends.sql.compilers.bigquery as bigquery + +__all__ = [ + "bigquery", +] diff --git a/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py b/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py new file mode 100644 index 00000000000..c74de82099e --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py @@ -0,0 +1,1660 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/backends/sql/compilers/base.py + +from __future__ import annotations + +import abc +import calendar +from functools import partial, reduce +import itertools +import math +import operator +import string +from typing import Any, ClassVar, TYPE_CHECKING + +from bigframes_vendored.ibis.backends.sql.rewrites import ( + add_one_to_nth_value_input, + add_order_by_to_empty_ranking_window_functions, + empty_in_values_right_side, + FirstValue, + LastValue, + lower_bucket, + lower_capitalize, + lower_sample, + one_to_zero_index, + sqlize, +) +from bigframes_vendored.ibis.expr.rewrites import lower_stringslice +import ibis.common.exceptions as com +import ibis.common.patterns as pats +from ibis.config import options +import ibis.expr.datatypes as dt +import ibis.expr.operations as ops +from ibis.expr.operations.udf import InputType +from public import public +import sqlglot as sg +import sqlglot.expressions as sge + +try: + from sqlglot.expressions import Alter +except ImportError: + from sqlglot.expressions import AlterTable +else: + + def AlterTable(*args, kind="TABLE", **kwargs): + return Alter(*args, kind=kind, **kwargs) + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Mapping + + from bigframes_vendored.ibis.backends.bigquery.datatypes import SqlglotType + import ibis.expr.schema as sch + import ibis.expr.types as ir + + +def get_leaf_classes(op): + for child_class in op.__subclasses__(): + if not child_class.__subclasses__(): + yield child_class + else: + yield from get_leaf_classes(child_class) + + +ALL_OPERATIONS = frozenset(get_leaf_classes(ops.Node)) + + +class AggGen: + """A descriptor for compiling aggregate functions. + + Common cases can be handled by setting configuration flags, + special cases should override the `aggregate` method directly. + + Parameters + ---------- + supports_filter + Whether the backend supports a FILTER clause in the aggregate. + Defaults to False. + supports_order_by + Whether the backend supports an ORDER BY clause in (relevant) + aggregates. Defaults to False. + """ + + class _Accessor: + """An internal type to handle getattr/getitem access.""" + + __slots__ = ("handler", "compiler") + + def __init__(self, handler: Callable, compiler: SQLGlotCompiler): + self.handler = handler + self.compiler = compiler + + def __getattr__(self, name: str) -> Callable: + return partial(self.handler, self.compiler, name) + + __getitem__ = __getattr__ + + __slots__ = ("supports_filter", "supports_order_by") + + def __init__( + self, *, supports_filter: bool = False, supports_order_by: bool = False + ): + self.supports_filter = supports_filter + self.supports_order_by = supports_order_by + + def __get__(self, instance, owner=None): + if instance is None: + return self + + return AggGen._Accessor(self.aggregate, instance) + + def aggregate( + self, + compiler: SQLGlotCompiler, + name: str, + *args: Any, + where: Any = None, + order_by: tuple = (), + ): + """Compile the specified aggregate. + + Parameters + ---------- + compiler + The backend's compiler. + name + The aggregate name (e.g. `"sum"`). + args + Any arguments to pass to the aggregate. + where + An optional column filter to apply before performing the aggregate. + order_by + Optional ordering keys to use to order the rows before performing + the aggregate. + """ + func = compiler.f[name] + + if order_by and not self.supports_order_by: + raise com.UnsupportedOperationError( + "ordering of order-sensitive aggregations via `order_by` is " + f"not supported for the {compiler.dialect} backend" + ) + + if where is not None and not self.supports_filter: + args = tuple(compiler.if_(where, arg, NULL) for arg in args) + + if order_by and self.supports_order_by: + *rest, last = args + out = func(*rest, sge.Order(this=last, expressions=order_by)) + else: + out = func(*args) + + if where is not None and self.supports_filter: + out = sge.Filter(this=out, expression=sge.Where(this=where)) + + return out + + +class VarGen: + __slots__ = () + + def __getattr__(self, name: str) -> sge.Var: + return sge.Var(this=name) + + def __getitem__(self, key: str) -> sge.Var: + return sge.Var(this=key) + + +class AnonymousFuncGen: + __slots__ = () + + def __getattr__(self, name: str) -> Callable[..., sge.Anonymous]: + return lambda *args: sge.Anonymous( + this=name, expressions=list(map(sge.convert, args)) + ) + + def __getitem__(self, key: str) -> Callable[..., sge.Anonymous]: + return getattr(self, key) + + +class FuncGen: + __slots__ = ("namespace", "anon", "copy") + + def __init__(self, namespace: str | None = None, copy: bool = False) -> None: + self.namespace = namespace + self.anon = AnonymousFuncGen() + self.copy = copy + + def __getattr__(self, name: str) -> Callable[..., sge.Func]: + name = ".".join(filter(None, (self.namespace, name))) + return lambda *args, **kwargs: sg.func( + name, *map(sge.convert, args), **kwargs, copy=self.copy + ) + + def __getitem__(self, key: str) -> Callable[..., sge.Func]: + return getattr(self, key) + + def array(self, *args: Any) -> sge.Array: + if not args: + return sge.Array(expressions=[]) + + first, *rest = args + + if isinstance(first, sge.Select): + assert ( + not rest + ), "only one argument allowed when `first` is a select statement" + + return sge.Array(expressions=list(map(sge.convert, (first, *rest)))) + + def tuple(self, *args: Any) -> sge.Anonymous: + return self.anon.tuple(*args) + + def exists(self, query: sge.Expression) -> sge.Exists: + return sge.Exists(this=query) + + def concat(self, *args: Any) -> sge.Concat: + return sge.Concat(expressions=list(map(sge.convert, args))) + + def map(self, keys: Iterable, values: Iterable) -> sge.Map: + return sge.Map(keys=keys, values=values) + + +class ColGen: + __slots__ = ("table",) + + def __init__(self, table: str | None = None) -> None: + self.table = table + + def __getattr__(self, name: str) -> sge.Column: + return sg.column(name, table=self.table, copy=False) + + def __getitem__(self, key: str) -> sge.Column: + return sg.column(key, table=self.table, copy=False) + + +C = ColGen() +F = FuncGen() +NULL = sge.Null() +FALSE = sge.false() +TRUE = sge.true() +STAR = sge.Star() + + +def parenthesize_inputs(f): + """Decorate a translation rule to parenthesize inputs.""" + + def wrapper(self, op, *, left, right): + return f( + self, + op, + left=self._add_parens(op.left, left), + right=self._add_parens(op.right, right), + ) + + return wrapper + + +@public +class SQLGlotCompiler(abc.ABC): + __slots__ = "f", "v" + + agg = AggGen() + """A generator for handling aggregate functions""" + + rewrites: tuple[type[pats.Replace], ...] = ( + empty_in_values_right_side, + add_order_by_to_empty_ranking_window_functions, + one_to_zero_index, + add_one_to_nth_value_input, + ) + """A sequence of rewrites to apply to the expression tree before SQL-specific transforms.""" + + post_rewrites: tuple[type[pats.Replace], ...] = () + """A sequence of rewrites to apply to the expression tree after SQL-specific transforms.""" + + no_limit_value: sge.Null | None = None + """The value to use to indicate no limit.""" + + quoted: bool = True + """Whether to always quote identifiers.""" + + copy_func_args: bool = False + """Whether to copy function arguments when generating SQL.""" + + supports_qualify: bool = False + """Whether the backend supports the QUALIFY clause.""" + + NAN: ClassVar[sge.Expression] = sge.Cast( + this=sge.convert("NaN"), to=sge.DataType(this=sge.DataType.Type.DOUBLE) + ) + """Backend's NaN literal.""" + + POS_INF: ClassVar[sge.Expression] = sge.Cast( + this=sge.convert("Inf"), to=sge.DataType(this=sge.DataType.Type.DOUBLE) + ) + """Backend's positive infinity literal.""" + + NEG_INF: ClassVar[sge.Expression] = sge.Cast( + this=sge.convert("-Inf"), to=sge.DataType(this=sge.DataType.Type.DOUBLE) + ) + """Backend's negative infinity literal.""" + + EXTRA_SUPPORTED_OPS: tuple[type[ops.Node], ...] = ( + ops.Project, + ops.Filter, + ops.Sort, + ops.WindowFunction, + ) + """A tuple of ops classes that are supported, but don't have explicit + `visit_*` methods (usually due to being handled by rewrite rules). Used by + `has_operation`""" + + UNSUPPORTED_OPS: tuple[type[ops.Node], ...] = () + """Tuple of operations the backend doesn't support.""" + + LOWERED_OPS: dict[type[ops.Node], pats.Replace | None] = { + ops.Bucket: lower_bucket, + ops.Capitalize: lower_capitalize, + ops.Sample: lower_sample, + ops.StringSlice: lower_stringslice, + } + """A mapping from an operation class to either a rewrite rule for rewriting that + operation to one composed of lower-level operations ("lowering"), or `None` to + remove an existing rewrite rule for that operation added in a base class""" + + SIMPLE_OPS = { + ops.Abs: "abs", + ops.Acos: "acos", + ops.All: "bool_and", + ops.Any: "bool_or", + ops.ApproxCountDistinct: "approx_distinct", + ops.ArgMax: "max_by", + ops.ArgMin: "min_by", + ops.ArrayContains: "array_contains", + ops.ArrayFlatten: "flatten", + ops.ArrayLength: "array_size", + ops.ArraySort: "array_sort", + ops.ArrayStringJoin: "array_to_string", + ops.Asin: "asin", + ops.Atan2: "atan2", + ops.Atan: "atan", + ops.Cos: "cos", + ops.Cot: "cot", + ops.Count: "count", + ops.CumeDist: "cume_dist", + ops.Date: "date", + ops.DateFromYMD: "datefromparts", + ops.Degrees: "degrees", + ops.DenseRank: "dense_rank", + ops.Exp: "exp", + FirstValue: "first_value", + ops.GroupConcat: "group_concat", + ops.IfElse: "if", + ops.IsInf: "isinf", + ops.IsNan: "isnan", + ops.JSONGetItem: "json_extract", + ops.LPad: "lpad", + LastValue: "last_value", + ops.Levenshtein: "levenshtein", + ops.Ln: "ln", + ops.Log10: "log", + ops.Log2: "log2", + ops.Lowercase: "lower", + ops.Map: "map", + ops.Median: "median", + ops.MinRank: "rank", + ops.NTile: "ntile", + ops.NthValue: "nth_value", + ops.NullIf: "nullif", + ops.PercentRank: "percent_rank", + ops.Pi: "pi", + ops.Power: "pow", + ops.RPad: "rpad", + ops.Radians: "radians", + ops.RegexSearch: "regexp_like", + ops.RegexSplit: "regexp_split", + ops.Repeat: "repeat", + ops.Reverse: "reverse", + ops.RowNumber: "row_number", + ops.Sign: "sign", + ops.Sin: "sin", + ops.Sqrt: "sqrt", + ops.StartsWith: "starts_with", + ops.StrRight: "right", + ops.StringAscii: "ascii", + ops.StringContains: "contains", + ops.StringLength: "length", + ops.StringReplace: "replace", + ops.StringSplit: "split", + ops.StringToDate: "str_to_date", + ops.StringToTimestamp: "str_to_time", + ops.Tan: "tan", + ops.Translate: "translate", + ops.Unnest: "explode", + ops.Uppercase: "upper", + } + + BINARY_INFIX_OPS = ( + # Binary operations + ops.Add, + ops.Subtract, + ops.Multiply, + ops.Divide, + ops.Modulus, + ops.Power, + # Comparisons + ops.GreaterEqual, + ops.Greater, + ops.LessEqual, + ops.Less, + ops.Equals, + ops.NotEquals, + # Boolean comparisons + ops.And, + ops.Or, + ops.Xor, + # Bitwise business + ops.BitwiseLeftShift, + ops.BitwiseRightShift, + ops.BitwiseAnd, + ops.BitwiseOr, + ops.BitwiseXor, + # Time arithmetic + ops.DateAdd, + ops.DateSub, + ops.DateDiff, + ops.TimestampAdd, + ops.TimestampSub, + ops.TimestampDiff, + # Interval Marginalia + ops.IntervalAdd, + ops.IntervalMultiply, + ops.IntervalSubtract, + ) + + NEEDS_PARENS = BINARY_INFIX_OPS + (ops.IsNull,) + + # Constructed dynamically in `__init_subclass__` from their respective + # UPPERCASE values to handle inheritance, do not modify directly here. + extra_supported_ops: ClassVar[frozenset[type[ops.Node]]] = frozenset() + lowered_ops: ClassVar[dict[type[ops.Node], pats.Replace]] = {} + + def __init__(self) -> None: + self.f = FuncGen(copy=self.__class__.copy_func_args) + self.v = VarGen() + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + def methodname(op: type) -> str: + assert isinstance(type(op), type), type(op) + return f"visit_{op.__name__}" + + def make_impl(op, target_name): + assert isinstance(type(op), type), type(op) + + if issubclass(op, ops.Reduction): + + def impl( + self, _, *, _name: str = target_name, where, order_by=(), **kw + ): + return self.agg[_name](*kw.values(), where=where, order_by=order_by) + + else: + + def impl(self, _, *, _name: str = target_name, **kw): + return self.f[_name](*kw.values()) + + return impl + + for op, target_name in cls.SIMPLE_OPS.items(): + setattr(cls, methodname(op), make_impl(op, target_name)) + + # unconditionally raise an exception for unsupported operations + # + # these *must* be defined after SIMPLE_OPS to handle compilers that + # subclass other compilers + for op in cls.UNSUPPORTED_OPS: + # change to visit_Unsupported in a follow up + # TODO: handle geoespatial ops as a separate case? + setattr(cls, methodname(op), cls.visit_Undefined) + + # raise on any remaining unsupported operations + for op in ALL_OPERATIONS: + name = methodname(op) + if not hasattr(cls, name): + setattr(cls, name, cls.visit_Undefined) + + # Amend `lowered_ops` and `extra_supported_ops` using their + # respective UPPERCASE classvar values. + extra_supported_ops = set(cls.extra_supported_ops) + lowered_ops = dict(cls.lowered_ops) + extra_supported_ops.update(cls.EXTRA_SUPPORTED_OPS) + for op_cls, rewrite in cls.LOWERED_OPS.items(): + if rewrite is not None: + lowered_ops[op_cls] = rewrite + extra_supported_ops.add(op_cls) + else: + lowered_ops.pop(op_cls, None) + extra_supported_ops.discard(op_cls) + cls.lowered_ops = lowered_ops + cls.extra_supported_ops = frozenset(extra_supported_ops) + + @property + @abc.abstractmethod + def dialect(self) -> str: + """Backend dialect.""" + + @property + @abc.abstractmethod + def type_mapper(self) -> type[SqlglotType]: + """The type mapper for the backend.""" + + def _compile_builtin_udf(self, udf_node: ops.ScalarUDF) -> None: # noqa: B027 + """No-op.""" + + def _compile_python_udf(self, udf_node: ops.ScalarUDF) -> None: + raise NotImplementedError( + f"Python UDFs are not supported in the {self.dialect} backend" + ) + + def _compile_pyarrow_udf(self, udf_node: ops.ScalarUDF) -> None: + raise NotImplementedError( + f"PyArrow UDFs are not supported in the {self.dialect} backend" + ) + + def _compile_pandas_udf(self, udf_node: ops.ScalarUDF) -> str: + raise NotImplementedError( + f"pandas UDFs are not supported in the {self.dialect} backend" + ) + + # Concrete API + + def if_(self, condition, true, false: sge.Expression | None = None) -> sge.If: + return sge.If( + this=sge.convert(condition), + true=sge.convert(true), + false=None if false is None else sge.convert(false), + ) + + def cast(self, arg, to: dt.DataType) -> sge.Cast: + return sge.Cast( + this=sge.convert(arg), to=self.type_mapper.from_ibis(to), copy=False + ) + + def _prepare_params(self, params): + result = {} + for param, value in params.items(): + node = param.op() + if isinstance(node, ops.Alias): + node = node.arg + result[node] = value + return result + + def to_sqlglot( + self, + expr: ir.Expr, + *, + limit: str | None = None, + params: Mapping[ir.Expr, Any] | None = None, + ): + import ibis + + table_expr = expr.as_table() + + if limit == "default": + limit = ibis.options.sql.default_limit + if limit is not None: + table_expr = table_expr.limit(limit) + + if params is None: + params = {} + + sql = self.translate(table_expr.op(), params=params) + assert not isinstance(sql, sge.Subquery) + + if isinstance(sql, sge.Table): + sql = sg.select(STAR, copy=False).from_(sql, copy=False) + + assert not isinstance(sql, sge.Subquery) + return sql + + def translate(self, op, *, params: Mapping[ir.Value, Any]) -> sge.Expression: + """Translate an ibis operation to a sqlglot expression. + + Parameters + ---------- + op + An ibis operation + params + A mapping of expressions to concrete values + compiler + An instance of SQLGlotCompiler + translate_rel + Relation node translator + translate_val + Value node translator + + Returns + ------- + sqlglot.expressions.Expression + A sqlglot expression + + """ + # substitute parameters immediately to avoid having to define a + # ScalarParameter translation rule + params = self._prepare_params(params) + if self.lowered_ops: + op = op.replace(reduce(operator.or_, self.lowered_ops.values())) + op, ctes = sqlize( + op, + params=params, + rewrites=self.rewrites, + post_rewrites=self.post_rewrites, + fuse_selects=options.sql.fuse_selects, + ) + + aliases = {} + counter = itertools.count() + + def fn(node, _, **kwargs): + result = self.visit_node(node, **kwargs) + + # if it's not a relation then we don't need to do anything special + if node is op or not isinstance(node, ops.Relation): + return result + + # alias ops.Views to their explicitly assigned name otherwise generate + alias = node.name if isinstance(node, ops.View) else f"t{next(counter)}" + aliases[node] = alias + + alias = sg.to_identifier(alias, quoted=self.quoted) + if isinstance(result, sge.Subquery): + return result.as_(alias, quoted=self.quoted) + else: + try: + return result.subquery(alias, copy=False) + except AttributeError: + return result.as_(alias, quoted=self.quoted) + + # apply translate rules in topological order + results = op.map(fn) + + # get the root node as a sqlglot select statement + out = results[op] + if isinstance(out, sge.Table): + out = sg.select(STAR, copy=False).from_(out, copy=False) + elif isinstance(out, sge.Subquery): + out = out.this + + # add cte definitions to the select statement + for cte in ctes: + alias = sg.to_identifier(aliases[cte], quoted=self.quoted) + out = out.with_( + alias, as_=results[cte].this, dialect=self.dialect, copy=False + ) + + return out + + def visit_node(self, op: ops.Node, **kwargs): + if isinstance(op, ops.ScalarUDF): + return self.visit_ScalarUDF(op, **kwargs) + elif isinstance(op, ops.AggUDF): + return self.visit_AggUDF(op, **kwargs) + else: + method = getattr(self, f"visit_{type(op).__name__}", None) + if method is not None: + return method(op, **kwargs) + else: + raise com.OperationNotDefinedError( + f"No translation rule for {type(op).__name__}" + ) + + def visit_Field(self, op, *, rel, name): + return sg.column( + self._gen_valid_name(name), table=rel.alias_or_name, quoted=self.quoted + ) + + def visit_Cast(self, op, *, arg, to): + from_ = op.arg.dtype + + if from_.is_integer() and to.is_interval(): + return self._make_interval(arg, to.unit) + + return self.cast(arg, to) + + def visit_ScalarSubquery(self, op, *, rel): + return rel.this.subquery(copy=False) + + def visit_Alias(self, op, *, arg, name): + return arg + + def visit_Literal(self, op, *, value, dtype): + """Compile a literal value. + + This is the default implementation for compiling literal values. + + Most backends should not need to override this method unless they want + to handle NULL literals as well as every other type of non-null literal + including integers, floating point numbers, decimals, strings, etc. + + The logic here is: + + 1. If the value is None and the type is nullable, return NULL + 1. If the value is None and the type is not nullable, raise an error + 1. Call `visit_NonNullLiteral` method. + 1. If the previous returns `None`, call `visit_DefaultLiteral` method + else return the result of the previous step. + """ + if value is None: + if dtype.nullable: + return NULL if dtype.is_null() else self.cast(NULL, dtype) + raise com.UnsupportedOperationError( + f"Unsupported NULL for non-nullable type: {dtype!r}" + ) + else: + result = self.visit_NonNullLiteral(op, value=value, dtype=dtype) + if result is None: + return self.visit_DefaultLiteral(op, value=value, dtype=dtype) + return result + + def visit_NonNullLiteral(self, op, *, value, dtype): + """Compile a non-null literal differently than the default implementation. + + Most backends should implement this, but only when they need to handle + some non-null literal differently than the default implementation + (`visit_DefaultLiteral`). + + Return `None` from an override of this method to fall back to + `visit_DefaultLiteral`. + """ + return self.visit_DefaultLiteral(op, value=value, dtype=dtype) + + def visit_DefaultLiteral(self, op, *, value, dtype): + """Compile a literal with a non-null value. + + This is the default implementation for compiling non-null literals. + + Most backends should not need to override this method unless they want + to handle compiling every kind of non-null literal value. + """ + if dtype.is_integer(): + return sge.convert(value) + elif dtype.is_floating(): + if math.isnan(value): + return self.NAN + elif math.isinf(value): + return self.POS_INF if value > 0 else self.NEG_INF + return sge.convert(value) + elif dtype.is_decimal(): + return self.cast(str(value), dtype) + elif dtype.is_interval(): + return sge.Interval( + this=sge.convert(str(value)), + unit=sge.Var(this=dtype.resolution.upper()), + ) + elif dtype.is_boolean(): + return sge.Boolean(this=bool(value)) + elif dtype.is_string(): + return sge.convert(value) + elif dtype.is_inet() or dtype.is_macaddr(): + return sge.convert(str(value)) + elif dtype.is_timestamp() or dtype.is_time(): + return self.cast(value.isoformat(), dtype) + elif dtype.is_date(): + return self.f.datefromparts(value.year, value.month, value.day) + elif dtype.is_array(): + value_type = dtype.value_type + return self.f.array( + *( + self.visit_Literal( + ops.Literal(v, value_type), value=v, dtype=value_type + ) + for v in value + ) + ) + elif dtype.is_map(): + key_type = dtype.key_type + keys = self.f.array( + *( + self.visit_Literal( + ops.Literal(k, key_type), value=k, dtype=key_type + ) + for k in value.keys() + ) + ) + + value_type = dtype.value_type + values = self.f.array( + *( + self.visit_Literal( + ops.Literal(v, value_type), value=v, dtype=value_type + ) + for v in value.values() + ) + ) + + return self.f.map(keys, values) + elif dtype.is_struct(): + items = [ + self.visit_Literal( + ops.Literal(v, field_dtype), value=v, dtype=field_dtype + ).as_(k, quoted=self.quoted) + for field_dtype, (k, v) in zip(dtype.types, value.items()) + ] + return sge.Struct.from_arg_list(items) + elif dtype.is_uuid(): + return self.cast(str(value), dtype) + elif dtype.is_geospatial(): + args = [value.wkt] + if (srid := dtype.srid) is not None: + args.append(srid) + return self.f.st_geomfromtext(*args) + + raise NotImplementedError(f"Unsupported type: {dtype!r}") + + def visit_BitwiseNot(self, op, *, arg): + return sge.BitwiseNot(this=arg) + + ### Mathematical Calisthenics + + def visit_E(self, op): + return self.f.exp(1) + + def visit_Log(self, op, *, arg, base): + if base is None: + return self.f.ln(arg) + elif str(base) in ("2", "10"): + return self.f[f"log{base}"](arg) + else: + return self.f.ln(arg) / self.f.ln(base) + + def visit_Clip(self, op, *, arg, lower, upper): + if upper is not None: + arg = self.if_(arg.is_(NULL), arg, self.f.least(upper, arg)) + + if lower is not None: + arg = self.if_(arg.is_(NULL), arg, self.f.greatest(lower, arg)) + + return arg + + def visit_FloorDivide(self, op, *, left, right): + return self.cast(self.f.floor(left / right), op.dtype) + + def visit_Ceil(self, op, *, arg): + return self.cast(self.f.ceil(arg), op.dtype) + + def visit_Floor(self, op, *, arg): + return self.cast(self.f.floor(arg), op.dtype) + + def visit_Round(self, op, *, arg, digits): + if digits is not None: + return sge.Round(this=arg, decimals=digits) + return sge.Round(this=arg) + + ### Random Noise + + def visit_RandomScalar(self, op, **kwargs): + return self.f.rand() + + def visit_RandomUUID(self, op, **kwargs): + return self.f.uuid() + + ### Dtype Dysmorphia + + def visit_TryCast(self, op, *, arg, to): + return sge.TryCast(this=arg, to=self.type_mapper.from_ibis(to)) + + ### Comparator Conundrums + + def visit_Between(self, op, *, arg, lower_bound, upper_bound): + return sge.Between(this=arg, low=lower_bound, high=upper_bound) + + def visit_Negate(self, op, *, arg): + return -sge.paren(arg, copy=False) + + def visit_Not(self, op, *, arg): + if isinstance(arg, sge.Filter): + return sge.Filter( + this=sg.not_(arg.this, copy=False), expression=arg.expression + ) + return sg.not_(sge.paren(arg, copy=False)) + + ### Timey McTimeFace + + def visit_Time(self, op, *, arg): + return self.cast(arg, to=dt.time) + + def visit_TimestampNow(self, op): + return sge.CurrentTimestamp() + + def visit_DateNow(self, op): + return sge.CurrentDate() + + def visit_Strftime(self, op, *, arg, format_str): + return sge.TimeToStr(this=arg, format=format_str) + + def visit_ExtractEpochSeconds(self, op, *, arg): + return self.f.epoch(self.cast(arg, dt.timestamp)) + + def visit_ExtractYear(self, op, *, arg): + return self.f.extract(self.v.year, arg) + + def visit_ExtractMonth(self, op, *, arg): + return self.f.extract(self.v.month, arg) + + def visit_ExtractDay(self, op, *, arg): + return self.f.extract(self.v.day, arg) + + def visit_ExtractDayOfYear(self, op, *, arg): + return self.f.extract(self.v.dayofyear, arg) + + def visit_ExtractQuarter(self, op, *, arg): + return self.f.extract(self.v.quarter, arg) + + def visit_ExtractWeekOfYear(self, op, *, arg): + return self.f.extract(self.v.week, arg) + + def visit_ExtractHour(self, op, *, arg): + return self.f.extract(self.v.hour, arg) + + def visit_ExtractMinute(self, op, *, arg): + return self.f.extract(self.v.minute, arg) + + def visit_ExtractSecond(self, op, *, arg): + return self.f.extract(self.v.second, arg) + + def visit_TimestampTruncate(self, op, *, arg, unit): + unit_mapping = { + "Y": "year", + "Q": "quarter", + "M": "month", + "W": "week", + "D": "day", + "h": "hour", + "m": "minute", + "s": "second", + "ms": "ms", + "us": "us", + } + + if (raw_unit := unit_mapping.get(unit.short)) is None: + raise com.UnsupportedOperationError( + f"Unsupported truncate unit {unit.short!r}" + ) + + return self.f.date_trunc(raw_unit, arg) + + def visit_DateTruncate(self, op, *, arg, unit): + return self.visit_TimestampTruncate(op, arg=arg, unit=unit) + + def visit_TimeTruncate(self, op, *, arg, unit): + return self.visit_TimestampTruncate(op, arg=arg, unit=unit) + + def visit_DayOfWeekIndex(self, op, *, arg): + return (self.f.dayofweek(arg) + 6) % 7 + + def visit_DayOfWeekName(self, op, *, arg): + # day of week number is 0-indexed + # Sunday == 0 + # Saturday == 6 + return sge.Case( + this=(self.f.dayofweek(arg) + 6) % 7, + ifs=list(itertools.starmap(self.if_, enumerate(calendar.day_name))), + ) + + def _make_interval(self, arg, unit): + return sge.Interval(this=arg, unit=self.v[unit.singular]) + + def visit_IntervalFromInteger(self, op, *, arg, unit): + return self._make_interval(arg, unit) + + ### String Instruments + def visit_Strip(self, op, *, arg): + return self.f.trim(arg, string.whitespace) + + def visit_RStrip(self, op, *, arg): + return self.f.rtrim(arg, string.whitespace) + + def visit_LStrip(self, op, *, arg): + return self.f.ltrim(arg, string.whitespace) + + def visit_Substring(self, op, *, arg, start, length): + if isinstance(op.length, ops.Literal) and (value := op.length.value) < 0: + raise com.IbisInputError( + f"Length parameter must be a non-negative value; got {value}" + ) + start += 1 + start = self.if_(start >= 1, start, start + self.f.length(arg)) + if length is None: + return self.f.substring(arg, start) + return self.f.substring(arg, start, length) + + def visit_StringFind(self, op, *, arg, substr, start, end): + if end is not None: + raise com.UnsupportedOperationError( + "String find doesn't support `end` argument" + ) + + if start is not None: + arg = self.f.substr(arg, start + 1) + pos = self.f.strpos(arg, substr) + return self.if_(pos > 0, pos + start, 0) + + return self.f.strpos(arg, substr) + + def visit_RegexReplace(self, op, *, arg, pattern, replacement): + return self.f.regexp_replace(arg, pattern, replacement, "g") + + def visit_StringConcat(self, op, *, arg): + return self.f.concat(*arg) + + def visit_StringJoin(self, op, *, sep, arg): + return self.f.concat_ws(sep, *arg) + + def visit_StringSQLLike(self, op, *, arg, pattern, escape): + return arg.like(pattern) + + def visit_StringSQLILike(self, op, *, arg, pattern, escape): + return arg.ilike(pattern) + + ### NULL PLAYER CHARACTER + def visit_IsNull(self, op, *, arg): + return arg.is_(NULL) + + def visit_NotNull(self, op, *, arg): + return arg.is_(sg.not_(NULL, copy=False)) + + def visit_InValues(self, op, *, value, options): + return value.isin(*options) + + ### Counting + + def visit_CountDistinct(self, op, *, arg, where): + return self.agg.count(sge.Distinct(expressions=[arg]), where=where) + + def visit_CountDistinctStar(self, op, *, arg, where): + return self.agg.count(sge.Distinct(expressions=[STAR]), where=where) + + def visit_CountStar(self, op, *, arg, where): + return self.agg.count(STAR, where=where) + + def visit_Sum(self, op, *, arg, where): + if op.arg.dtype.is_boolean(): + arg = self.cast(arg, dt.int32) + return self.agg.sum(arg, where=where) + + def visit_Mean(self, op, *, arg, where): + if op.arg.dtype.is_boolean(): + arg = self.cast(arg, dt.int32) + return self.agg.avg(arg, where=where) + + def visit_Min(self, op, *, arg, where): + if op.arg.dtype.is_boolean(): + return self.agg.bool_and(arg, where=where) + return self.agg.min(arg, where=where) + + def visit_Max(self, op, *, arg, where): + if op.arg.dtype.is_boolean(): + return self.agg.bool_or(arg, where=where) + return self.agg.max(arg, where=where) + + ### Stats + + def visit_VarianceStandardDevCovariance(self, op, *, how, where, **kw): + hows = {"sample": "samp", "pop": "pop"} + funcs = { + ops.Variance: "var", + ops.StandardDev: "stddev", + ops.Covariance: "covar", + } + + args = [] + + for oparg, arg in zip(op.args, kw.values()): + if (arg_dtype := oparg.dtype).is_boolean(): + arg = self.cast(arg, dt.Int32(nullable=arg_dtype.nullable)) + args.append(arg) + + funcname = f"{funcs[type(op)]}_{hows[how]}" + return self.agg[funcname](*args, where=where) + + visit_Variance = ( + visit_StandardDev + ) = visit_Covariance = visit_VarianceStandardDevCovariance + + def visit_SimpleCase(self, op, *, base=None, cases, results, default): + return sge.Case( + this=base, ifs=list(map(self.if_, cases, results)), default=default + ) + + visit_SearchedCase = visit_SimpleCase + + def visit_ExistsSubquery(self, op, *, rel): + select = rel.this.select(1, append=False) + return self.f.exists(select) + + def visit_InSubquery(self, op, *, rel, needle): + query = rel.this + if not isinstance(query, sge.Select): + query = sg.select(STAR).from_(query) + return needle.isin(query=query) + + def visit_Array(self, op, *, exprs): + return self.f.array(*exprs) + + def visit_StructColumn(self, op, *, names, values): + return sge.Struct.from_arg_list( + [value.as_(name, quoted=self.quoted) for name, value in zip(names, values)] + ) + + def visit_StructField(self, op, *, arg, field): + return sge.Dot(this=arg, expression=sg.to_identifier(field, quoted=self.quoted)) + + def visit_IdenticalTo(self, op, *, left, right): + return sge.NullSafeEQ(this=left, expression=right) + + def visit_Greatest(self, op, *, arg): + return self.f.greatest(*arg) + + def visit_Least(self, op, *, arg): + return self.f.least(*arg) + + def visit_Coalesce(self, op, *, arg): + return self.f.coalesce(*arg) + + ### Ordering and window functions + + def visit_SortKey(self, op, *, expr, ascending: bool, nulls_first: bool): + return sge.Ordered(this=expr, desc=not ascending, nulls_first=nulls_first) + + def visit_ApproxMedian(self, op, *, arg, where): + return self.agg.approx_quantile(arg, 0.5, where=where) + + def visit_WindowBoundary(self, op, *, value, preceding): + # TODO: bit of a hack to return a dict, but there's no sqlglot expression + # that corresponds to _only_ this information + return {"value": value, "side": "preceding" if preceding else "following"} + + def visit_WindowFunction(self, op, *, how, func, start, end, group_by, order_by): + if start is None: + start = {} + if end is None: + end = {} + + start_value = start.get("value", "UNBOUNDED") + start_side = start.get("side", "PRECEDING") + end_value = end.get("value", "UNBOUNDED") + end_side = end.get("side", "FOLLOWING") + + if getattr(start_value, "this", None) == "0": + start_value = "CURRENT ROW" + start_side = None + + if getattr(end_value, "this", None) == "0": + end_value = "CURRENT ROW" + end_side = None + + spec = sge.WindowSpec( + kind=how.upper(), + start=start_value, + start_side=start_side, + end=end_value, + end_side=end_side, + over="OVER", + ) + order = sge.Order(expressions=order_by) if order_by else None + + spec = self._minimize_spec(op.start, op.end, spec) + + return sge.Window(this=func, partition_by=group_by, order=order, spec=spec) + + @staticmethod + def _minimize_spec(start, end, spec): + return spec + + def visit_LagLead(self, op, *, arg, offset, default): + args = [arg] + + if default is not None: + if offset is None: + offset = 1 + + args.append(offset) + args.append(default) + elif offset is not None: + args.append(offset) + + return self.f[type(op).__name__.lower()](*args) + + visit_Lag = visit_Lead = visit_LagLead + + def visit_Argument(self, op, *, name: str, shape, dtype): + return sg.to_identifier(op.param) + + def visit_RowID(self, op, *, table): + return sg.column( + op.name, table=table.alias_or_name, quoted=self.quoted, copy=False + ) + + # TODO(kszucs): this should be renamed to something UDF related + def __sql_name__(self, op: ops.ScalarUDF | ops.AggUDF) -> str: + # for builtin functions use the exact function name, otherwise use the + # generated name to handle the case of redefinition + funcname = ( + op.__func_name__ + if op.__input_type__ == InputType.BUILTIN + else type(op).__name__ + ) + + # not actually a table, but easier to quote individual namespace + # components this way + namespace = op.__udf_namespace__ + return sg.table(funcname, db=namespace.database, catalog=namespace.catalog).sql( + self.dialect + ) + + def visit_ScalarUDF(self, op, **kw): + return self.f[self.__sql_name__(op)](*kw.values()) + + def visit_AggUDF(self, op, *, where, **kw): + return self.agg[self.__sql_name__(op)](*kw.values(), where=where) + + def visit_TimestampDelta(self, op, *, part, left, right): + # dialect is necessary due to sqlglot's default behavior + # of `part` coming last + return sge.DateDiff( + this=left, expression=right, unit=part, dialect=self.dialect + ) + + visit_TimeDelta = visit_DateDelta = visit_TimestampDelta + + def visit_TimestampBucket(self, op, *, arg, interval, offset): + origin = self.f.cast("epoch", self.type_mapper.from_ibis(dt.timestamp)) + if offset is not None: + origin += offset + return self.f.time_bucket(interval, arg, origin) + + def visit_ArrayConcat(self, op, *, arg): + return sge.ArrayConcat(this=arg[0], expressions=list(arg[1:])) + + ## relations + + @staticmethod + def _gen_valid_name(name: str) -> str: + """Generate a valid name for a value expression. + + Override this method if the dialect has restrictions on valid + identifiers even when quoted. + + See the BigQuery backend's implementation for an example. + """ + return name + + def _cleanup_names(self, exprs: Mapping[str, sge.Expression]): + """Compose `_gen_valid_name` and `_dedup_name` to clean up names in projections.""" + + for name, value in exprs.items(): + name = self._gen_valid_name(name) + if isinstance(value, sge.Column) and name == value.name: + # don't alias columns that are already named the same as their alias + yield value + else: + yield value.as_(name, quoted=self.quoted, copy=False) + + def visit_Select( + self, op, *, parent, selections, predicates, qualified, sort_keys, distinct + ): + # if we've constructed a useless projection return the parent relation + if not (selections or predicates or qualified or sort_keys or distinct): + return parent + + result = parent + + if selections: + # if there are `qualify` predicates then sqlglot adds a hidden + # column to implement the functionality if the dialect doesn't + # support it + # + # using STAR in that case would lead to an extra column, so in that + # case we have to spell out the columns + if op.is_star_selection() and (not qualified or self.supports_qualify): + fields = [STAR] + else: + fields = self._cleanup_names(selections) + result = sg.select(*fields, copy=False).from_(result, copy=False) + + if predicates: + result = result.where(*predicates, copy=False) + + if qualified: + result = result.qualify(*qualified, copy=False) + + if sort_keys: + result = result.order_by(*sort_keys, copy=False) + + if distinct: + result = result.distinct() + + return result + + def visit_DummyTable(self, op, *, values): + return sg.select(*self._cleanup_names(values), copy=False) + + def visit_UnboundTable( + self, op, *, name: str, schema: sch.Schema, namespace: ops.Namespace + ) -> sg.Table: + return sg.table( + name, db=namespace.database, catalog=namespace.catalog, quoted=self.quoted + ) + + def visit_InMemoryTable( + self, op, *, name: str, schema: sch.Schema, data + ) -> sg.Table: + return sg.table(name, quoted=self.quoted) + + def visit_DatabaseTable( + self, + op, + *, + name: str, + schema: sch.Schema, + source: Any, + namespace: ops.Namespace, + ) -> sg.Table: + return sg.table( + name, db=namespace.database, catalog=namespace.catalog, quoted=self.quoted + ) + + def visit_SelfReference(self, op, *, parent, identifier): + return parent + + visit_JoinReference = visit_SelfReference + + def visit_JoinChain(self, op, *, first, rest, values): + result = sg.select(*self._cleanup_names(values), copy=False).from_( + first, copy=False + ) + + for link in rest: + if isinstance(link, sge.Alias): + link = link.this + result = result.join(link, copy=False) + return result + + def visit_JoinLink(self, op, *, how, table, predicates): + sides = { + "inner": None, + "left": "left", + "right": "right", + "semi": "left", + "anti": "left", + "cross": None, + "outer": "full", + "asof": "asof", + "any_left": "left", + "any_inner": None, + "positional": None, + } + kinds = { + "any_left": "any", + "any_inner": "any", + "asof": "left", + "inner": "inner", + "left": "outer", + "right": "outer", + "semi": "semi", + "anti": "anti", + "cross": "cross", + "outer": "outer", + "positional": "positional", + } + assert predicates or how in { + "cross", + "positional", + }, "expected non-empty predicates when not a cross join" + on = sg.and_(*predicates) if predicates else None + return sge.Join(this=table, side=sides[how], kind=kinds[how], on=on) + + @staticmethod + def _generate_groups(groups): + return map(sge.convert, range(1, len(groups) + 1)) + + def visit_Aggregate(self, op, *, parent, groups, metrics): + sel = sg.select( + *self._cleanup_names(groups), *self._cleanup_names(metrics), copy=False + ).from_(parent, copy=False) + + if groups: + sel = sel.group_by(*self._generate_groups(groups.values()), copy=False) + + return sel + + @classmethod + def _add_parens(cls, op, sg_expr): + if isinstance(op, cls.NEEDS_PARENS): + return sge.paren(sg_expr, copy=False) + return sg_expr + + def visit_Union(self, op, *, left, right, distinct): + if isinstance(left, (sge.Table, sge.Subquery)): + left = sg.select(STAR, copy=False).from_(left, copy=False) + + if isinstance(right, (sge.Table, sge.Subquery)): + right = sg.select(STAR, copy=False).from_(right, copy=False) + + return sg.union( + left.args.get("this", left), + right.args.get("this", right), + distinct=distinct, + copy=False, + ) + + def visit_Intersection(self, op, *, left, right, distinct): + if isinstance(left, (sge.Table, sge.Subquery)): + left = sg.select(STAR, copy=False).from_(left, copy=False) + + if isinstance(right, (sge.Table, sge.Subquery)): + right = sg.select(STAR, copy=False).from_(right, copy=False) + + return sg.intersect( + left.args.get("this", left), + right.args.get("this", right), + distinct=distinct, + copy=False, + ) + + def visit_Difference(self, op, *, left, right, distinct): + if isinstance(left, (sge.Table, sge.Subquery)): + left = sg.select(STAR, copy=False).from_(left, copy=False) + + if isinstance(right, (sge.Table, sge.Subquery)): + right = sg.select(STAR, copy=False).from_(right, copy=False) + + return sg.except_( + left.args.get("this", left), + right.args.get("this", right), + distinct=distinct, + copy=False, + ) + + def visit_Limit(self, op, *, parent, n, offset): + # push limit/offset into subqueries + if isinstance(parent, sge.Subquery) and parent.this.args.get("limit") is None: + result = parent.this.copy() + alias = parent.alias + else: + result = sg.select(STAR, copy=False).from_(parent, copy=False) + alias = None + + if isinstance(n, int): + result = result.limit(n, copy=False) + elif n is not None: + result = result.limit( + sg.select(n, copy=False).from_(parent, copy=False).subquery(copy=False), + copy=False, + ) + else: + assert n is None, n + if self.no_limit_value is not None: + result = result.limit(self.no_limit_value, copy=False) + + assert offset is not None, "offset is None" + + if not isinstance(offset, int): + skip = offset + skip = ( + sg.select(skip, copy=False) + .from_(parent, copy=False) + .subquery(copy=False) + ) + elif not offset: + if alias is not None: + return result.subquery(alias, copy=False) + return result + else: + skip = offset + + result = result.offset(skip, copy=False) + if alias is not None: + return result.subquery(alias, copy=False) + return result + + def visit_CTE(self, op, *, parent): + return sg.table(parent.alias_or_name, quoted=self.quoted) + + def visit_View(self, op, *, child, name: str): + if isinstance(child, sge.Table): + child = sg.select(STAR, copy=False).from_(child, copy=False) + else: + child = child.copy() + + if isinstance(child, sge.Subquery): + return child.as_(name, quoted=self.quoted) + else: + try: + return child.subquery(name, copy=False) + except AttributeError: + return child.as_(name, quoted=self.quoted) + + def visit_SQLStringView(self, op, *, query: str, child, schema): + return sg.parse_one(query, read=self.dialect) + + def visit_SQLQueryResult(self, op, *, query, schema, source): + return sg.parse_one(query, dialect=self.dialect).subquery(copy=False) + + def visit_RegexExtract(self, op, *, arg, pattern, index): + return self.f.regexp_extract(arg, pattern, index, dialect=self.dialect) + + @parenthesize_inputs + def visit_Add(self, op, *, left, right): + return sge.Add(this=left, expression=right) + + visit_DateAdd = visit_TimestampAdd = visit_IntervalAdd = visit_Add + + @parenthesize_inputs + def visit_Subtract(self, op, *, left, right): + return sge.Sub(this=left, expression=right) + + visit_DateSub = ( + visit_DateDiff + ) = ( + visit_TimestampSub + ) = visit_TimestampDiff = visit_IntervalSubtract = visit_Subtract + + @parenthesize_inputs + def visit_Multiply(self, op, *, left, right): + return sge.Mul(this=left, expression=right) + + visit_IntervalMultiply = visit_Multiply + + @parenthesize_inputs + def visit_Divide(self, op, *, left, right): + return sge.Div(this=left, expression=right) + + @parenthesize_inputs + def visit_Modulus(self, op, *, left, right): + return sge.Mod(this=left, expression=right) + + @parenthesize_inputs + def visit_Power(self, op, *, left, right): + return sge.Pow(this=left, expression=right) + + @parenthesize_inputs + def visit_GreaterEqual(self, op, *, left, right): + return sge.GTE(this=left, expression=right) + + @parenthesize_inputs + def visit_Greater(self, op, *, left, right): + return sge.GT(this=left, expression=right) + + @parenthesize_inputs + def visit_LessEqual(self, op, *, left, right): + return sge.LTE(this=left, expression=right) + + @parenthesize_inputs + def visit_Less(self, op, *, left, right): + return sge.LT(this=left, expression=right) + + @parenthesize_inputs + def visit_Equals(self, op, *, left, right): + return sge.EQ(this=left, expression=right) + + @parenthesize_inputs + def visit_NotEquals(self, op, *, left, right): + return sge.NEQ(this=left, expression=right) + + @parenthesize_inputs + def visit_And(self, op, *, left, right): + return sge.And(this=left, expression=right) + + @parenthesize_inputs + def visit_Or(self, op, *, left, right): + return sge.Or(this=left, expression=right) + + @parenthesize_inputs + def visit_Xor(self, op, *, left, right): + return sge.Xor(this=left, expression=right) + + @parenthesize_inputs + def visit_BitwiseLeftShift(self, op, *, left, right): + return sge.BitwiseLeftShift(this=left, expression=right) + + @parenthesize_inputs + def visit_BitwiseRightShift(self, op, *, left, right): + return sge.BitwiseRightShift(this=left, expression=right) + + @parenthesize_inputs + def visit_BitwiseAnd(self, op, *, left, right): + return sge.BitwiseAnd(this=left, expression=right) + + @parenthesize_inputs + def visit_BitwiseOr(self, op, *, left, right): + return sge.BitwiseOr(this=left, expression=right) + + @parenthesize_inputs + def visit_BitwiseXor(self, op, *, left, right): + return sge.BitwiseXor(this=left, expression=right) + + def visit_Undefined(self, op, **_): + raise com.OperationNotDefinedError( + f"Compilation rule for {type(op).__name__!r} operation is not defined" + ) + + def visit_Unsupported(self, op, **_): + raise com.UnsupportedOperationError( + f"{type(op).__name__!r} operation is not supported in the {self.dialect} backend" + ) + + def visit_DropColumns(self, op, *, parent, columns_to_drop): + # the generated query will be huge for wide tables + # + # TODO: figure out a way to produce an IR that only contains exactly + # what is used + parent_alias = parent.alias_or_name + quoted = self.quoted + columns_to_keep = ( + sg.column(column, table=parent_alias, quoted=quoted) + for column in op.schema.names + ) + return sg.select(*columns_to_keep).from_(parent) + + def add_query_to_expr(self, *, name: str, table: ir.Table, query: str) -> str: + dialect = self.dialect + + compiled_ibis_expr = self.to_sqlglot(table) + + # pull existing CTEs from the compiled Ibis expression and combine them + # with the new query + parsed = reduce( + lambda parsed, cte: parsed.with_(cte.args["alias"], as_=cte.args["this"]), + compiled_ibis_expr.ctes, + sg.parse_one(query, read=dialect), + ) + + # remove all ctes from the compiled expression, since they're now in + # our larger expression + compiled_ibis_expr.args.pop("with", None) + + # add the new str query as a CTE + parsed = parsed.with_( + sg.to_identifier(name, quoted=self.quoted), as_=compiled_ibis_expr + ) + + # generate the SQL string + return parsed.sql(dialect) + + def _make_sample_backwards_compatible(self, *, sample, parent): + # sample was changed to be owned by the table being sampled in 25.17.0 + # + # this is a small workaround for backwards compatibility + if "this" in sample.__class__.arg_types: + sample.args["this"] = parent + else: + parent.args["sample"] = sample + return sg.select(STAR).from_(parent) + + +# `__init_subclass__` is uncalled for subclasses - we manually call it here to +# autogenerate the base class implementations as well. +SQLGlotCompiler.__init_subclass__() diff --git a/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py b/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py new file mode 100644 index 00000000000..fc8d93a433c --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py @@ -0,0 +1,1114 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/backends/sql/compilers/bigquery/__init__.py + +"""Module to convert from Ibis expression to SQL string.""" + +from __future__ import annotations + +import decimal +import math +import re +from typing import Any, TYPE_CHECKING + +from bigframes_vendored.ibis.backends.bigquery.datatypes import ( + BigQueryType, + BigQueryUDFType, +) +from bigframes_vendored.ibis.backends.sql.compilers.base import ( + AggGen, + NULL, + SQLGlotCompiler, + STAR, +) +from bigframes_vendored.ibis.backends.sql.rewrites import ( + exclude_unsupported_window_frame_from_ops, + exclude_unsupported_window_frame_from_rank, + exclude_unsupported_window_frame_from_row_number, + split_select_distinct_with_order_by, +) +from ibis import util +from ibis.backends.sql.compilers.bigquery.udf.core import PythonToJavaScriptTranslator +import ibis.common.exceptions as com +from ibis.common.temporal import DateUnit, IntervalUnit, TimestampUnit, TimeUnit +import ibis.expr.datatypes as dt +import ibis.expr.operations as ops +import sqlglot as sg +from sqlglot.dialects import BigQuery +import sqlglot.expressions as sge + +if TYPE_CHECKING: + from collections.abc import Mapping + + import ibis.expr.types as ir + +_NAME_REGEX = re.compile(r'[^!"$()*,./;?@[\\\]^`{}~\n]+') + + +_MEMTABLE_PATTERN = re.compile( + r"^_?ibis_(?:[A-Za-z_][A-Za-z_0-9]*)_memtable_[a-z0-9]{26}$" +) + + +def _qualify_memtable( + node: sge.Expression, *, dataset: str | None, project: str | None +) -> sge.Expression: + """Add a BigQuery dataset and project to memtable references.""" + if isinstance(node, sge.Table) and _MEMTABLE_PATTERN.match(node.name) is not None: + node.args["db"] = dataset + node.args["catalog"] = project + # make sure to quote table location + node = _force_quote_table(node) + return node + + +def _remove_null_ordering_from_unsupported_window( + node: sge.Expression, +) -> sge.Expression: + """Remove null ordering in window frame clauses not supported by BigQuery. + + BigQuery has only partial support for NULL FIRST/LAST in RANGE windows so + we remove it from any window frame clause that doesn't support it. + + Here's the support matrix: + + ✅ sum(x) over (order by y desc nulls last) + 🚫 sum(x) over (order by y asc nulls last) + ✅ sum(x) over (order by y asc nulls first) + 🚫 sum(x) over (order by y desc nulls first) + """ + if isinstance(node, sge.Window): + order = node.args.get("order") + if order is not None: + for key in order.args["expressions"]: + kargs = key.args + if kargs.get("desc") is True and kargs.get("nulls_first", False): + kargs["nulls_first"] = False + elif kargs.get("desc") is False and not kargs.setdefault( + "nulls_first", True + ): + kargs["nulls_first"] = True + return node + + +def _force_quote_table(table: sge.Table) -> sge.Table: + """Force quote all the parts of a bigquery path. + + The BigQuery identifier quoting semantics are bonkers + https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers + + my-table is OK, but not mydataset.my-table + + mytable-287 is OK, but not mytable-287a + + Just quote everything. + """ + for key in ("this", "db", "catalog"): + if (val := table.args[key]) is not None: + if isinstance(val, sg.exp.Identifier) and not val.quoted: + val.args["quoted"] = True + else: + table.args[key] = sg.to_identifier(val, quoted=True) + return table + + +class BigQueryCompiler(SQLGlotCompiler): + dialect = BigQuery + type_mapper = BigQueryType + udf_type_mapper = BigQueryUDFType + + agg = AggGen(supports_order_by=True) + + rewrites = ( + exclude_unsupported_window_frame_from_ops, + exclude_unsupported_window_frame_from_row_number, + exclude_unsupported_window_frame_from_rank, + *SQLGlotCompiler.rewrites, + ) + post_rewrites = (split_select_distinct_with_order_by,) + + supports_qualify = True + + UNSUPPORTED_OPS = ( + ops.DateDiff, + ops.ExtractAuthority, + ops.ExtractUserInfo, + ops.FindInSet, + ops.Median, + ops.RegexSplit, + ops.RowID, + ops.TimestampDiff, + ) + + NAN = sge.Cast( + this=sge.convert("NaN"), to=sge.DataType(this=sge.DataType.Type.DOUBLE) + ) + POS_INF = sge.Cast( + this=sge.convert("Infinity"), to=sge.DataType(this=sge.DataType.Type.DOUBLE) + ) + NEG_INF = sge.Cast( + this=sge.convert("-Infinity"), to=sge.DataType(this=sge.DataType.Type.DOUBLE) + ) + + SIMPLE_OPS = { + ops.Arbitrary: "any_value", + ops.StringAscii: "ascii", + ops.BitAnd: "bit_and", + ops.BitOr: "bit_or", + ops.BitXor: "bit_xor", + ops.DateFromYMD: "date", + ops.Divide: "ieee_divide", + ops.EndsWith: "ends_with", + ops.GeoArea: "st_area", + ops.GeoAsBinary: "st_asbinary", + ops.GeoAsText: "st_astext", + ops.GeoAzimuth: "st_azimuth", + ops.GeoBuffer: "st_buffer", + ops.GeoCentroid: "st_centroid", + ops.GeoContains: "st_contains", + ops.GeoCoveredBy: "st_coveredby", + ops.GeoCovers: "st_covers", + ops.GeoDWithin: "st_dwithin", + ops.GeoDifference: "st_difference", + ops.GeoDisjoint: "st_disjoint", + ops.GeoDistance: "st_distance", + ops.GeoEndPoint: "st_endpoint", + ops.GeoEquals: "st_equals", + ops.GeoGeometryType: "st_geometrytype", + ops.GeoIntersection: "st_intersection", + ops.GeoIntersects: "st_intersects", + ops.GeoLength: "st_length", + ops.GeoMaxDistance: "st_maxdistance", + ops.GeoNPoints: "st_numpoints", + ops.GeoPerimeter: "st_perimeter", + ops.GeoPoint: "st_geogpoint", + ops.GeoPointN: "st_pointn", + ops.GeoStartPoint: "st_startpoint", + ops.GeoTouches: "st_touches", + ops.GeoUnaryUnion: "st_union_agg", + ops.GeoUnion: "st_union", + ops.GeoWithin: "st_within", + ops.GeoX: "st_x", + ops.GeoY: "st_y", + ops.Hash: "farm_fingerprint", + ops.IsInf: "is_inf", + ops.IsNan: "is_nan", + ops.Log10: "log10", + ops.LPad: "lpad", + ops.RPad: "rpad", + ops.Levenshtein: "edit_distance", + ops.Modulus: "mod", + ops.RegexReplace: "regexp_replace", + ops.RegexSearch: "regexp_contains", + ops.Time: "time", + ops.TimeFromHMS: "time_from_parts", + ops.TimestampNow: "current_timestamp", + ops.ExtractHost: "net.host", + } + + def to_sqlglot( + self, + expr: ir.Expr, + *, + limit: str | None = None, + params: Mapping[ir.Expr, Any] | None = None, + session_dataset_id: str | None = None, + session_project: str | None = None, + ) -> Any: + """Compile an Ibis expression. + + Parameters + ---------- + expr + Ibis expression + limit + For expressions yielding result sets; retrieve at most this number + of values/rows. Overrides any limit already set on the expression. + params + Named unbound parameters + session_dataset_id + Optional dataset ID to qualify memtable references. + session_project + Optional project ID to qualify memtable references. + + Returns + ------- + Any + The output of compilation. The type of this value depends on the + backend. + + """ + sql = super().to_sqlglot(expr, limit=limit, params=params) + + table_expr = expr.as_table() + geocols = table_expr.schema().geospatial + + result = sql.transform( + _qualify_memtable, + dataset=session_dataset_id, + project=session_project, + ).transform(_remove_null_ordering_from_unsupported_window) + + if geocols: + # if there are any geospatial columns, we have to convert them to WKB, + # so interactive mode knows how to display them + # + # by default bigquery returns data to python as WKT, and there's really + # no point in supporting both if we don't need to. + quoted = self.quoted + result = sg.select( + sge.Star( + replace=[ + self.f.st_asbinary(sg.column(col, quoted=quoted)).as_( + col, quoted=quoted + ) + for col in geocols + ] + ) + ).from_(result.subquery()) + + sources = [] + + for udf_node in table_expr.op().find(ops.ScalarUDF): + compile_func = getattr( + self, f"_compile_{udf_node.__input_type__.name.lower()}_udf" + ) + if sql := compile_func(udf_node): + sources.append(sql) + + if not sources: + return result + + sources.append(result) + return sources + + def _compile_python_udf(self, udf_node: ops.ScalarUDF) -> sge.Create: + name = type(udf_node).__name__ + type_mapper = self.udf_type_mapper + + body = PythonToJavaScriptTranslator(udf_node.__func__).compile() + config = udf_node.__config__ + libraries = config.get("libraries", []) + + signature = [ + sge.ColumnDef( + this=sg.to_identifier(name, quoted=self.quoted), + kind=type_mapper.from_ibis(param.annotation.pattern.dtype), + ) + for name, param in udf_node.__signature__.parameters.items() + ] + + lines = ['"""'] + + if config.get("strict", True): + lines.append('"use strict";') + + lines += [ + body, + "", + f"return {udf_node.__func_name__}({', '.join(udf_node.argnames)});", + '"""', + ] + + func = sge.Create( + kind="FUNCTION", + this=sge.UserDefinedFunction( + this=sg.to_identifier(name), expressions=signature, wrapped=True + ), + # not exactly what I had in mind, but it works + # + # quoting is too simplistic to handle multiline strings + expression=sge.Var(this="\n".join(lines)), + exists=False, + properties=sge.Properties( + expressions=[ + sge.TemporaryProperty(), + sge.ReturnsProperty(this=type_mapper.from_ibis(udf_node.dtype)), + sge.StabilityProperty( + this="IMMUTABLE" if config.get("determinism") else "VOLATILE" + ), + sge.LanguageProperty(this=sg.to_identifier("js")), + ] + + [ + sge.Property( + this=sg.to_identifier("library"), value=self.f.array(*libraries) + ) + ] + * bool(libraries) + ), + ) + + return func + + @staticmethod + def _minimize_spec(start, end, spec): + if ( + start is None + and isinstance(getattr(end, "value", None), ops.Literal) + and end.value.value == 0 + and end.following + ): + return None + return spec + + def visit_BoundingBox(self, op, *, arg): + name = type(op).__name__[len("Geo") :].lower() + return sge.Dot( + this=self.f.st_boundingbox(arg), expression=sg.to_identifier(name) + ) + + visit_GeoXMax = visit_GeoXMin = visit_GeoYMax = visit_GeoYMin = visit_BoundingBox + + def visit_GeoSimplify(self, op, *, arg, tolerance, preserve_collapsed): + if ( + not isinstance(op.preserve_collapsed, ops.Literal) + or op.preserve_collapsed.value + ): + raise com.UnsupportedOperationError( + "BigQuery simplify does not support preserving collapsed geometries, " + "pass preserve_collapsed=False" + ) + return self.f.st_simplify(arg, tolerance) + + def visit_ApproxMedian(self, op, *, arg, where): + return self.agg.approx_quantiles(arg, 2, where=where)[self.f.offset(1)] + + def visit_Pi(self, op): + return self.f.acos(-1) + + def visit_E(self, op): + return self.f.exp(1) + + def visit_TimeDelta(self, op, *, left, right, part): + return self.f.time_diff(left, right, part, dialect=self.dialect) + + def visit_DateDelta(self, op, *, left, right, part): + return self.f.date_diff(left, right, part, dialect=self.dialect) + + def visit_TimestampDelta(self, op, *, left, right, part): + left_tz = op.left.dtype.timezone + right_tz = op.right.dtype.timezone + + if left_tz is None and right_tz is None: + return self.f.datetime_diff(left, right, part) + elif left_tz is not None and right_tz is not None: + return self.f.timestamp_diff(left, right, part) + + raise com.UnsupportedOperationError( + "timestamp difference with mixed timezone/timezoneless values is not implemented" + ) + + def visit_GroupConcat(self, op, *, arg, sep, where, order_by): + if where is not None: + arg = self.if_(where, arg, NULL) + + if order_by: + sep = sge.Order(this=sep, expressions=order_by) + + return sge.GroupConcat(this=arg, separator=sep) + + def visit_ApproxQuantile(self, op, *, arg, quantile, where): + if not isinstance(op.quantile, ops.Literal): + raise com.UnsupportedOperationError( + "quantile must be a literal in BigQuery" + ) + + # BigQuery syntax is `APPROX_QUANTILES(col, resolution)` to return + # `resolution + 1` quantiles array. To handle this, we compute the + # resolution ourselves then restructure the output array as needed. + # To avoid excessive resolution we arbitrarily cap it at 100,000 - + # since these are approximate quantiles anyway this seems fine. + quantiles = util.promote_list(op.quantile.value) + fracs = [decimal.Decimal(str(q)).as_integer_ratio() for q in quantiles] + resolution = min(math.lcm(*(den for _, den in fracs)), 100_000) + indices = [(num * resolution) // den for num, den in fracs] + + if where is not None: + arg = self.if_(where, arg, NULL) + + if not op.arg.dtype.is_floating(): + arg = self.cast(arg, dt.float64) + + array = self.f.approx_quantiles( + arg, sge.IgnoreNulls(this=sge.convert(resolution)) + ) + if isinstance(op, ops.ApproxQuantile): + return array[indices[0]] + + if indices == list(range(resolution + 1)): + return array + else: + return sge.Array(expressions=[array[i] for i in indices]) + + visit_ApproxMultiQuantile = visit_ApproxQuantile + + def visit_FloorDivide(self, op, *, left, right): + return self.cast(self.f.floor(self.f.ieee_divide(left, right)), op.dtype) + + def visit_Log2(self, op, *, arg): + return self.f.log(arg, 2, dialect=self.dialect) + + def visit_Log(self, op, *, arg, base): + if base is None: + return self.f.ln(arg) + return self.f.log(arg, base, dialect=self.dialect) + + def visit_ArrayRepeat(self, op, *, arg, times): + start = step = 1 + array_length = self.f.array_length(arg) + stop = self.f.greatest(times, 0) * array_length + i = sg.to_identifier("i") + idx = self.f.coalesce( + self.f.nullif(self.f.mod(i, array_length), 0), array_length + ) + series = self.f.generate_array(start, stop, step) + return self.f.array( + sg.select(arg[self.f.safe_ordinal(idx)]).from_(self._unnest(series, as_=i)) + ) + + def visit_NthValue(self, op, *, arg, nth): + if not isinstance(op.nth, ops.Literal): + raise com.UnsupportedOperationError( + f"BigQuery `nth` must be a literal; got {type(op.nth)}" + ) + return self.f.nth_value(arg, nth) + + def visit_StrRight(self, op, *, arg, nchars): + return self.f.substr(arg, -self.f.least(self.f.length(arg), nchars)) + + def visit_StringJoin(self, op, *, arg, sep): + return self.f.array_to_string(self.f.array(*arg), sep) + + def visit_DayOfWeekIndex(self, op, *, arg): + return self.f.mod(self.f.extract(self.v.dayofweek, arg) + 5, 7) + + def visit_DayOfWeekName(self, op, *, arg): + return self.f.initcap(sge.Cast(this=arg, to="STRING FORMAT 'DAY'")) + + def visit_StringToTimestamp(self, op, *, arg, format_str): + if (timezone := op.dtype.timezone) is not None: + return self.f.parse_timestamp(format_str, arg, timezone) + return self.f.parse_datetime(format_str, arg) + + def visit_ArrayCollect(self, op, *, arg, where, order_by, include_null): + if where is not None and include_null: + raise com.UnsupportedOperationError( + "Combining `include_null=True` and `where` is not supported " + "by bigquery" + ) + out = self.agg.array_agg(arg, where=where, order_by=order_by) + if not include_null: + out = sge.IgnoreNulls(this=out) + return out + + def _neg_idx_to_pos(self, arg, idx): + return self.if_(idx < 0, self.f.array_length(arg) + idx, idx) + + def visit_ArraySlice(self, op, *, arg, start, stop): + index = sg.to_identifier("bq_arr_slice") + cond = [index >= self._neg_idx_to_pos(arg, start)] + + if stop is not None: + cond.append(index < self._neg_idx_to_pos(arg, stop)) + + el = sg.to_identifier("el") + return self.f.array( + sg.select(el).from_(self._unnest(arg, as_=el, offset=index)).where(*cond) + ) + + def visit_ArrayIndex(self, op, *, arg, index): + return arg[self.f.safe_offset(index)] + + def visit_ArrayContains(self, op, *, arg, other): + name = sg.to_identifier(util.gen_name("bq_arr_contains")) + return sge.Exists( + this=sg.select(sge.convert(1)) + .from_(self._unnest(arg, as_=name)) + .where(name.eq(other)) + ) + + def visit_StringContains(self, op, *, haystack, needle): + return self.f.strpos(haystack, needle) > 0 + + def visti_StringFind(self, op, *, arg, substr, start, end): + if start is not None: + raise NotImplementedError( + "`start` not implemented for BigQuery string find" + ) + if end is not None: + raise NotImplementedError("`end` not implemented for BigQuery string find") + return self.f.strpos(arg, substr) + + def visit_TimestampFromYMDHMS( + self, op, *, year, month, day, hours, minutes, seconds + ): + return self.f.anon.DATETIME(year, month, day, hours, minutes, seconds) + + def visit_NonNullLiteral(self, op, *, value, dtype): + if dtype.is_inet() or dtype.is_macaddr(): + return sge.convert(str(value)) + elif dtype.is_timestamp(): + funcname = "DATETIME" if dtype.timezone is None else "TIMESTAMP" + return self.f.anon[funcname](value.isoformat()) + elif dtype.is_date(): + return self.f.date_from_parts(value.year, value.month, value.day) + elif dtype.is_time(): + time = self.f.time_from_parts(value.hour, value.minute, value.second) + if micros := value.microsecond: + # bigquery doesn't support `time(12, 34, 56.789101)`, AKA a + # float seconds specifier, so add any non-zero micros to the + # time value + return sge.TimeAdd( + this=time, expression=sge.convert(micros), unit=self.v.MICROSECOND + ) + return time + elif dtype.is_binary(): + return sge.Cast( + this=sge.convert(value.hex()), + to=sge.DataType(this=sge.DataType.Type.BINARY), + format=sge.convert("HEX"), + ) + elif dtype.is_interval(): + if dtype.unit == IntervalUnit.NANOSECOND: + raise com.UnsupportedOperationError( + "BigQuery does not support nanosecond intervals" + ) + elif dtype.is_uuid(): + return sge.convert(str(value)) + return None + + def visit_IntervalFromInteger(self, op, *, arg, unit): + if unit == IntervalUnit.NANOSECOND: + raise com.UnsupportedOperationError( + "BigQuery does not support nanosecond intervals" + ) + return sge.Interval(this=arg, unit=self.v[unit.singular]) + + def visit_Strftime(self, op, *, arg, format_str): + arg_dtype = op.arg.dtype + if arg_dtype.is_timestamp(): + if (timezone := arg_dtype.timezone) is None: + return self.f.format_datetime(format_str, arg) + else: + return self.f.format_timestamp(format_str, arg, timezone) + elif arg_dtype.is_date(): + return self.f.format_date(format_str, arg) + else: + assert arg_dtype.is_time(), arg_dtype + return self.f.format_time(format_str, arg) + + def visit_IntervalMultiply(self, op, *, left, right): + unit = self.v[op.left.dtype.resolution.upper()] + return sge.Interval(this=self.f.extract(unit, left) * right, unit=unit) + + def visit_TimestampFromUNIX(self, op, *, arg, unit): + unit = op.unit + if unit == TimestampUnit.SECOND: + return self.f.timestamp_seconds(arg) + elif unit == TimestampUnit.MILLISECOND: + return self.f.timestamp_millis(arg) + elif unit == TimestampUnit.MICROSECOND: + return self.f.timestamp_micros(arg) + elif unit == TimestampUnit.NANOSECOND: + return self.f.timestamp_micros( + self.cast(self.f.round(arg / 1_000), dt.int64) + ) + else: + raise com.UnsupportedOperationError(f"Unit not supported: {unit}") + + def visit_Cast(self, op, *, arg, to): + from_ = op.arg.dtype + if from_.is_timestamp() and to.is_integer(): + return self.f.unix_micros(arg) + elif from_.is_integer() and to.is_timestamp(): + return self.f.timestamp_seconds(arg) + elif from_.is_interval() and to.is_integer(): + if from_.unit in { + IntervalUnit.WEEK, + IntervalUnit.QUARTER, + IntervalUnit.NANOSECOND, + }: + raise com.UnsupportedOperationError( + f"BigQuery does not allow extracting date part `{from_.unit}` from intervals" + ) + return self.f.extract(self.v[to.resolution.upper()], arg) + elif from_.is_floating() and to.is_integer(): + return self.cast(self.f.trunc(arg), dt.int64) + return super().visit_Cast(op, arg=arg, to=to) + + def visit_JSONGetItem(self, op, *, arg, index): + return arg[index] + + def visit_UnwrapJSONString(self, op, *, arg): + return self.f.anon["safe.string"](arg) + + def visit_UnwrapJSONInt64(self, op, *, arg): + return self.f.anon["safe.int64"](arg) + + def visit_UnwrapJSONFloat64(self, op, *, arg): + return self.f.anon["safe.float64"](arg) + + def visit_UnwrapJSONBoolean(self, op, *, arg): + return self.f.anon["safe.bool"](arg) + + def visit_ExtractEpochSeconds(self, op, *, arg): + return self.f.unix_seconds(arg) + + def visit_ExtractWeekOfYear(self, op, *, arg): + return self.f.extract(self.v.isoweek, arg) + + def visit_ExtractIsoYear(self, op, *, arg): + return self.f.extract(self.v.isoyear, arg) + + def visit_ExtractMillisecond(self, op, *, arg): + return self.f.extract(self.v.millisecond, arg) + + def visit_ExtractMicrosecond(self, op, *, arg): + return self.f.extract(self.v.microsecond, arg) + + def visit_TimestampTruncate(self, op, *, arg, unit): + if unit == IntervalUnit.NANOSECOND: + raise com.UnsupportedOperationError( + f"BigQuery does not support truncating {op.arg.dtype} values to unit {unit!r}" + ) + elif unit == IntervalUnit.WEEK: + unit = "WEEK(MONDAY)" + else: + unit = unit.name + return self.f.timestamp_trunc(arg, self.v[unit], dialect=self.dialect) + + def visit_DateTruncate(self, op, *, arg, unit): + if unit == DateUnit.WEEK: + unit = "WEEK(MONDAY)" + else: + unit = unit.name + return self.f.date_trunc(arg, self.v[unit], dialect=self.dialect) + + def visit_TimeTruncate(self, op, *, arg, unit): + if unit == TimeUnit.NANOSECOND: + raise com.UnsupportedOperationError( + f"BigQuery does not support truncating {op.arg.dtype} values to unit {unit!r}" + ) + else: + unit = unit.name + return self.f.time_trunc(arg, self.v[unit], dialect=self.dialect) + + def _nullifzero(self, step, zero, step_dtype): + if step_dtype.is_interval(): + return self.if_(step.eq(zero), NULL, step) + return self.f.nullif(step, zero) + + def _zero(self, dtype): + if dtype.is_interval(): + return self.f.make_interval() + return sge.convert(0) + + def _sign(self, value, dtype): + if dtype.is_interval(): + zero = self._zero(dtype) + return sge.Case( + ifs=[ + self.if_(value < zero, -1), + self.if_(value.eq(zero), 0), + self.if_(value > zero, 1), + ], + default=NULL, + ) + return self.f.sign(value) + + def _make_range(self, func, start, stop, step, step_dtype): + step_sign = self._sign(step, step_dtype) + delta_sign = self._sign(stop - start, step_dtype) + zero = self._zero(step_dtype) + nullifzero = self._nullifzero(step, zero, step_dtype) + condition = sg.and_(sg.not_(nullifzero.is_(NULL)), step_sign.eq(delta_sign)) + gen_array = func(start, stop, step) + name = sg.to_identifier(util.gen_name("bq_arr_range")) + inner = ( + sg.select(name) + .from_(self._unnest(gen_array, as_=name)) + .where(name.neq(stop)) + ) + return self.if_(condition, self.f.array(inner), self.f.array()) + + def visit_IntegerRange(self, op, *, start, stop, step): + return self._make_range(self.f.generate_array, start, stop, step, op.step.dtype) + + def visit_TimestampRange(self, op, *, start, stop, step): + if op.start.dtype.timezone is None or op.stop.dtype.timezone is None: + raise com.IbisTypeError( + "Timestamps without timezone values are not supported when generating timestamp ranges" + ) + return self._make_range( + self.f.generate_timestamp_array, start, stop, step, op.step.dtype + ) + + def visit_First(self, op, *, arg, where, order_by, include_null): + if where is not None: + arg = self.if_(where, arg, NULL) + if include_null: + raise com.UnsupportedOperationError( + "Combining `include_null=True` and `where` is not supported " + "by bigquery" + ) + + if order_by: + arg = sge.Order(this=arg, expressions=order_by) + + if not include_null: + arg = sge.IgnoreNulls(this=arg) + + array = self.f.array_agg(sge.Limit(this=arg, expression=sge.convert(1))) + return array[self.f.safe_offset(0)] + + def visit_Last(self, op, *, arg, where, order_by, include_null): + if where is not None: + arg = self.if_(where, arg, NULL) + if include_null: + raise com.UnsupportedOperationError( + "Combining `include_null=True` and `where` is not supported " + "by bigquery" + ) + + if order_by: + arg = sge.Order(this=arg, expressions=order_by) + + if not include_null: + arg = sge.IgnoreNulls(this=arg) + + array = self.f.array_reverse(self.f.array_agg(arg)) + return array[self.f.safe_offset(0)] + + def visit_ArrayFilter(self, op, *, arg, body, param): + return self.f.array( + sg.select(param).from_(self._unnest(arg, as_=param)).where(body) + ) + + def visit_ArrayMap(self, op, *, arg, body, param): + return self.f.array(sg.select(body).from_(self._unnest(arg, as_=param))) + + def visit_ArrayZip(self, op, *, arg): + lengths = [self.f.array_length(arr) - 1 for arr in arg] + idx = sg.to_identifier(util.gen_name("bq_arr_idx")) + indices = self._unnest( + self.f.generate_array(0, self.f.greatest(*lengths)), as_=idx + ) + struct_fields = [ + arr[self.f.safe_offset(idx)].as_(name) + for name, arr in zip(op.dtype.value_type.names, arg) + ] + return self.f.array( + sge.Select(kind="STRUCT", expressions=struct_fields).from_(indices) + ) + + def visit_ArrayPosition(self, op, *, arg, other): + name = sg.to_identifier(util.gen_name("bq_arr")) + idx = sg.to_identifier(util.gen_name("bq_arr_idx")) + unnest = self._unnest(arg, as_=name, offset=idx) + return self.f.coalesce( + sg.select(idx + 1).from_(unnest).where(name.eq(other)).limit(1).subquery(), + 0, + ) + + def _unnest(self, expression, *, as_, offset=None): + alias = sge.TableAlias(columns=[sg.to_identifier(as_)]) + return sge.Unnest(expressions=[expression], alias=alias, offset=offset) + + def visit_ArrayRemove(self, op, *, arg, other): + name = sg.to_identifier(util.gen_name("bq_arr")) + unnest = self._unnest(arg, as_=name) + both_null = sg.and_(name.is_(NULL), other.is_(NULL)) + cond = sg.or_(name.neq(other), both_null) + return self.f.array(sg.select(name).from_(unnest).where(cond)) + + def visit_ArrayDistinct(self, op, *, arg): + name = util.gen_name("bq_arr") + return self.f.array( + sg.select(name).distinct().from_(self._unnest(arg, as_=name)) + ) + + def visit_ArraySort(self, op, *, arg): + name = util.gen_name("bq_arr") + return self.f.array( + sg.select(name).from_(self._unnest(arg, as_=name)).order_by(name) + ) + + def visit_ArrayUnion(self, op, *, left, right): + lname = util.gen_name("bq_arr_left") + rname = util.gen_name("bq_arr_right") + lhs = sg.select(lname).from_(self._unnest(left, as_=lname)) + rhs = sg.select(rname).from_(self._unnest(right, as_=rname)) + return self.f.array(sg.union(lhs, rhs, distinct=True)) + + def visit_ArrayIntersect(self, op, *, left, right): + lname = util.gen_name("bq_arr_left") + rname = util.gen_name("bq_arr_right") + lhs = sg.select(lname).from_(self._unnest(left, as_=lname)) + rhs = sg.select(rname).from_(self._unnest(right, as_=rname)) + return self.f.array(sg.intersect(lhs, rhs, distinct=True)) + + def visit_RegexExtract(self, op, *, arg, pattern, index): + matches = self.f.regexp_contains(arg, pattern) + nonzero_index_replace = self.f.regexp_replace( + arg, + self.f.concat(".*?", pattern, ".*"), + self.f.concat("\\", self.cast(index, dt.string)), + ) + zero_index_replace = self.f.regexp_replace( + arg, self.f.concat(".*?", self.f.concat("(", pattern, ")"), ".*"), "\\1" + ) + extract = self.if_(index.eq(0), zero_index_replace, nonzero_index_replace) + return self.if_(matches, extract, NULL) + + def visit_TimestampAddSub(self, op, *, left, right): + if not isinstance(right, sge.Interval): + raise com.OperationNotDefinedError( + "BigQuery does not support non-literals on the right side of timestamp add/subtract" + ) + if (unit := op.right.dtype.unit) == IntervalUnit.NANOSECOND: + raise com.UnsupportedOperationError( + f"BigQuery does not allow binary operation {type(op).__name__} with " + f"INTERVAL offset {unit}" + ) + + opname = type(op).__name__[len("Timestamp") :] + funcname = f"TIMESTAMP_{opname.upper()}" + return self.f.anon[funcname](left, right) + + visit_TimestampAdd = visit_TimestampSub = visit_TimestampAddSub + + def visit_DateAddSub(self, op, *, left, right): + if not isinstance(right, sge.Interval): + raise com.OperationNotDefinedError( + "BigQuery does not support non-literals on the right side of date add/subtract" + ) + if not (unit := op.right.dtype.unit).is_date(): + raise com.UnsupportedOperationError( + f"BigQuery does not allow binary operation {type(op).__name__} with " + f"INTERVAL offset {unit}" + ) + opname = type(op).__name__[len("Date") :] + funcname = f"DATE_{opname.upper()}" + return self.f.anon[funcname](left, right) + + visit_DateAdd = visit_DateSub = visit_DateAddSub + + def visit_Covariance(self, op, *, left, right, how, where): + if where is not None: + left = self.if_(where, left, NULL) + right = self.if_(where, right, NULL) + + if op.left.dtype.is_boolean(): + left = self.cast(left, dt.int64) + + if op.right.dtype.is_boolean(): + right = self.cast(right, dt.int64) + + how = op.how[:4].upper() + assert how in ("POP", "SAMP"), 'how not in ("POP", "SAMP")' + return self.agg[f"COVAR_{how}"](left, right, where=where) + + def visit_Correlation(self, op, *, left, right, how, where): + if how == "sample": + raise ValueError(f"Correlation with how={how!r} is not supported.") + + if where is not None: + left = self.if_(where, left, NULL) + right = self.if_(where, right, NULL) + + if op.left.dtype.is_boolean(): + left = self.cast(left, dt.int64) + + if op.right.dtype.is_boolean(): + right = self.cast(right, dt.int64) + + return self.agg.corr(left, right, where=where) + + def visit_TypeOf(self, op, *, arg): + return self._pudf("typeof", arg) + + def visit_Xor(self, op, *, left, right): + return sg.or_(sg.and_(left, sg.not_(right)), sg.and_(sg.not_(left), right)) + + def visit_HashBytes(self, op, *, arg, how): + if how not in ("md5", "sha1", "sha256", "sha512"): + raise NotImplementedError(how) + return self.f[how](arg) + + @staticmethod + def _gen_valid_name(name: str) -> str: + candidate = "_".join(map(str.strip, _NAME_REGEX.findall(name))) or "tmp" + # column names cannot be longer than 300 characters + # + # https://cloud.google.com/bigquery/docs/schemas#column_names + # + # it's easy to rename columns, so raise an exception telling the user + # to do so + # + # we could potentially relax this and support arbitrary-length columns + # by compressing the information using hashing, but there's no reason + # to solve that problem until someone encounters this error and cannot + # rename their columns + limit = 300 + if len(candidate) > limit: + raise com.IbisError( + f"BigQuery does not allow column names longer than {limit:d} characters. " + "Please rename your columns to have fewer characters." + ) + return candidate + + def visit_CountStar(self, op, *, arg, where): + if where is not None: + return self.f.countif(where) + return self.f.count(STAR) + + def visit_CountDistinctStar(self, op, *, where, arg): + # Bigquery does not support count(distinct a,b,c) or count(distinct (a, b, c)) + # as expressions must be "groupable": + # https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#group_by_grouping_item + # + # Instead, convert the entire expression to a string + # SELECT COUNT(DISTINCT concat(to_json_string(a), to_json_string(b))) + # This works with an array of datatypes which generates a unique string + # https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions#json_encodings + row = sge.Concat( + expressions=[ + self.f.to_json_string(sg.column(x, quoted=self.quoted)) + for x in op.arg.schema.keys() + ] + ) + if where is not None: + row = self.if_(where, row, NULL) + return self.f.count(sge.Distinct(expressions=[row])) + + def visit_Degrees(self, op, *, arg): + return self._pudf("degrees", arg) + + def visit_Radians(self, op, *, arg): + return self._pudf("radians", arg) + + def visit_CountDistinct(self, op, *, arg, where): + if where is not None: + arg = self.if_(where, arg, NULL) + return self.f.count(sge.Distinct(expressions=[arg])) + + def visit_RandomUUID(self, op, **kwargs): + return self.f.generate_uuid() + + def visit_ExtractFile(self, op, *, arg): + return self._pudf("cw_url_extract_file", arg) + + def visit_ExtractFragment(self, op, *, arg): + return self._pudf("cw_url_extract_fragment", arg) + + def visit_ExtractPath(self, op, *, arg): + return self._pudf("cw_url_extract_path", arg) + + def visit_ExtractProtocol(self, op, *, arg): + return self._pudf("cw_url_extract_protocol", arg) + + def visit_ExtractQuery(self, op, *, arg, key): + if key is not None: + return self._pudf("cw_url_extract_parameter", arg, key) + else: + return self._pudf("cw_url_extract_query", arg) + + def _pudf(self, name, *args): + name = sg.table(name, db="persistent_udfs", catalog="bigquery-public-data").sql( + self.dialect + ) + return self.f[name](*args) + + def visit_DropColumns(self, op, *, parent, columns_to_drop): + quoted = self.quoted + excludes = [sg.column(column, quoted=quoted) for column in columns_to_drop] + star = sge.Star(**{"except": excludes}) + table = sg.to_identifier(parent.alias_or_name, quoted=quoted) + column = sge.Column(this=star, table=table) + return sg.select(column).from_(parent) + + def visit_TableUnnest( + self, op, *, parent, column, offset: str | None, keep_empty: bool + ): + quoted = self.quoted + + column_alias = sg.to_identifier( + util.gen_name("table_unnest_column"), quoted=quoted + ) + + selcols = [] + + table = sg.to_identifier(parent.alias_or_name, quoted=quoted) + + opname = op.column.name + overlaps_with_parent = opname in op.parent.schema + computed_column = column_alias.as_(opname, quoted=quoted) + + # replace the existing column if the unnested column hasn't been + # renamed + # + # e.g., table.unnest("x") + if overlaps_with_parent: + selcols.append( + sge.Column(this=sge.Star(replace=[computed_column]), table=table) + ) + else: + selcols.append(sge.Column(this=STAR, table=table)) + selcols.append(computed_column) + + if offset is not None: + offset = sg.to_identifier(offset, quoted=quoted) + selcols.append(offset) + + unnest = sge.Unnest( + expressions=[column], + alias=sge.TableAlias(columns=[column_alias]), + offset=offset, + ) + return ( + sg.select(*selcols) + .from_(parent) + .join(unnest, join_type="CROSS" if not keep_empty else "LEFT") + ) + + def visit_TimestampBucket(self, op, *, arg, interval, offset): + arg_dtype = op.arg.dtype + if arg_dtype.timezone is not None: + funcname = "timestamp" + else: + funcname = "datetime" + + func = self.f[f"{funcname}_bucket"] + + origin = sge.convert("1970-01-01") + if offset is not None: + origin = self.f.anon[f"{funcname}_add"](origin, offset) + + return func(arg, interval, origin) + + def _array_reduction(self, *, arg, reduction): + name = sg.to_identifier(util.gen_name(f"bq_arr_{reduction}")) + return ( + sg.select(self.f[reduction](name)) + .from_(self._unnest(arg, as_=name)) + .subquery() + ) + + def visit_ArrayMin(self, op, *, arg): + return self._array_reduction(arg=arg, reduction="min") + + def visit_ArrayMax(self, op, *, arg): + return self._array_reduction(arg=arg, reduction="max") + + def visit_ArraySum(self, op, *, arg): + return self._array_reduction(arg=arg, reduction="sum") + + def visit_ArrayMean(self, op, *, arg): + return self._array_reduction(arg=arg, reduction="avg") + + def visit_ArrayAny(self, op, *, arg): + return self._array_reduction(arg=arg, reduction="logical_or") + + def visit_ArrayAll(self, op, *, arg): + return self._array_reduction(arg=arg, reduction="logical_and") + + +compiler = BigQueryCompiler() diff --git a/third_party/bigframes_vendored/ibis/backends/sql/rewrites.py b/third_party/bigframes_vendored/ibis/backends/sql/rewrites.py new file mode 100644 index 00000000000..1f67902395a --- /dev/null +++ b/third_party/bigframes_vendored/ibis/backends/sql/rewrites.py @@ -0,0 +1,367 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/backends/sql/rewrites.py + +"""Some common rewrite functions to be shared between backends.""" + +from __future__ import annotations + +from collections import defaultdict + +from ibis.common.collections import FrozenDict # noqa: TCH001 +from ibis.common.deferred import _, deferred, Item, var +from ibis.common.exceptions import ExpressionError, IbisInputError +from ibis.common.graph import Node as Traversable +from ibis.common.graph import traverse +from ibis.common.grounds import Concrete +from ibis.common.patterns import Check, pattern, replace +from ibis.common.typing import VarTuple # noqa: TCH001 +import ibis.expr.operations as ops +from ibis.util import Namespace, promote_list +import toolz + +p = Namespace(pattern, module=ops) +d = Namespace(deferred, module=ops) + + +x = var("x") +y = var("y") +name = var("name") + + +class DerefMap(Concrete, Traversable): + """Trace and replace fields from earlier relations in the hierarchy. + In order to provide a nice user experience, we need to allow expressions + from earlier relations in the hierarchy. Consider the following example: + t = ibis.table([('a', 'int64'), ('b', 'string')], name='t') + t1 = t.select([t.a, t.b]) + t2 = t1.filter(t.a > 0) # note that not t1.a is referenced here + t3 = t2.select(t.a) # note that not t2.a is referenced here + However the relational operations in the IR are strictly enforcing that + the expressions are referencing the immediate parent only. So we need to + track fields upwards the hierarchy to replace `t.a` with `t1.a` and `t2.a` + in the example above. This is called dereferencing. + Whether we can treat or not a field of a relation semantically equivalent + with a field of an earlier relation in the hierarchy depends on the + `.values` mapping of the relation. Leaf relations, like `t` in the example + above, have an empty `.values` mapping, so we cannot dereference fields + from them. On the other hand a projection, like `t1` in the example above, + has a `.values` mapping like `{'a': t.a, 'b': t.b}`, so we can deduce that + `t1.a` is semantically equivalent with `t.a` and so on. + """ + + """The relations we want the values to point to.""" + rels: VarTuple[ops.Relation] + + """Substitution mapping from values of earlier relations to the fields of `rels`.""" + subs: FrozenDict[ops.Value, ops.Field] + + """Ambiguous field references.""" + ambigs: FrozenDict[ops.Value, VarTuple[ops.Value]] + + @classmethod + def from_targets(cls, rels, extra=None): + """Create a dereference map from a list of target relations. + Usually a single relation is passed except for joins where multiple + relations are involved. + Parameters + ---------- + rels : list of ops.Relation + The target relations to dereference to. + extra : dict, optional + Extra substitutions to be added to the dereference map. + Returns + ------- + DerefMap + """ + rels = promote_list(rels) + mapping = defaultdict(dict) + for rel in rels: + for field in rel.fields.values(): + for value, distance in cls.backtrack(field): + mapping[value][field] = distance + + subs, ambigs = {}, {} + for from_, to in mapping.items(): + mindist = min(to.values()) + minkeys = [k for k, v in to.items() if v == mindist] + # if all the closest fields are from the same relation, then we + # can safely substitute them and we pick the first one arbitrarily + if all(minkeys[0].relations == k.relations for k in minkeys): + subs[from_] = minkeys[0] + else: + ambigs[from_] = minkeys + + if extra is not None: + subs.update(extra) + + return cls(rels, subs, ambigs) + + @classmethod + def backtrack(cls, value): + """Backtrack the field in the relation hierarchy. + The field is traced back until no modification is made, so only follow + ops.Field nodes not arbitrary values. + Parameters + ---------- + value : ops.Value + The value to backtrack. + Yields + ------ + tuple[ops.Field, int] + The value node and the distance from the original value. + """ + distance = 0 + # track down the field in the hierarchy until no modification + # is made so only follow ops.Field nodes not arbitrary values; + while isinstance(value, ops.Field): + yield value, distance + value = value.rel.values.get(value.name) + distance += 1 + if ( + value is not None + and value.relations + and not value.find(ops.Impure, filter=ops.Value) + ): + yield value, distance + + def dereference(self, value): + """Dereference a value to the target relations. + Also check for ambiguous field references. If a field reference is found + which is marked as ambiguous, then raise an error. + Parameters + ---------- + value : ops.Value + The value to dereference. + Returns + ------- + ops.Value + The dereferenced value. + """ + ambigs = value.find(lambda x: x in self.ambigs, filter=ops.Value) + if ambigs: + raise IbisInputError( + f"Ambiguous field reference {ambigs!r} in expression {value!r}" + ) + return value.replace(self.subs, filter=ops.Value) + + +def flatten_predicates(node): + """Yield the expressions corresponding to the `And` nodes of a predicate. + Examples + -------- + >>> import ibis + >>> t = ibis.table([("a", "int64"), ("b", "string")], name="t") + >>> filt = (t.a == 1) & (t.b == "foo") + >>> predicates = flatten_predicates(filt.op()) + >>> len(predicates) + 2 + >>> predicates[0].to_expr().name("left") + r0 := UnboundTable: t + a int64 + b string + left: r0.a == 1 + >>> predicates[1].to_expr().name("right") + r0 := UnboundTable: t + a int64 + b string + right: r0.b == 'foo' + """ + + def predicate(node): + if isinstance(node, ops.And): + # proceed and don't yield the node + return True, None + else: + # halt and yield the node + return False, node + + return list(traverse(predicate, node)) + + +@replace(p.Field(p.JoinChain)) +def peel_join_field(_): + return _.rel.values[_.name] + + +@replace(p.ScalarParameter) +def replace_parameter(_, params, **kwargs): + """Replace scalar parameters with their values.""" + return ops.Literal(value=params[_], dtype=_.dtype) + + +@replace(p.StringSlice) +def lower_stringslice(_, **kwargs): + """Rewrite StringSlice in terms of Substring.""" + if _.end is None: + return ops.Substring(_.arg, start=_.start) + if _.start is None: + return ops.Substring(_.arg, start=0, length=_.end) + if ( + isinstance(_.start, ops.Literal) + and isinstance(_.start.value, int) + and isinstance(_.end, ops.Literal) + and isinstance(_.end.value, int) + ): + # optimization for constant values + length = _.end.value - _.start.value + else: + length = ops.Subtract(_.end, _.start) + return ops.Substring(_.arg, start=_.start, length=length) + + +@replace(p.Analytic) +def wrap_analytic(_, **__): + # Wrap analytic functions in a window function + return ops.WindowFunction(_) + + +@replace(p.Reduction) +def project_wrap_reduction(_, rel): + # Query all the tables that the reduction depends on + if _.relations == {rel}: + # The reduction is fully originating from the `rel`, so turn + # it into a window function of `rel` + return ops.WindowFunction(_) + else: + # 1. The reduction doesn't depend on any table, constructed from + # scalar values, so turn it into a scalar subquery. + # 2. The reduction is originating from `rel` and other tables, + # so this is a correlated scalar subquery. + # 3. The reduction is originating entirely from other tables, + # so this is an uncorrelated scalar subquery. + return ops.ScalarSubquery(_.to_expr().as_table()) + + +def rewrite_project_input(value, relation): + # we need to detect reductions which are either turned into window functions + # or scalar subqueries depending on whether they are originating from the + # relation + return value.replace( + wrap_analytic | project_wrap_reduction, + filter=p.Value & ~p.WindowFunction, + context={"rel": relation}, + ) + + +ReductionLike = p.Reduction | p.Field(p.Aggregate(groups={})) + + +@replace(ReductionLike) +def filter_wrap_reduction(_): + # Wrap reductions or fields referencing an aggregation without a group by - + # which are scalar fields - in a scalar subquery. In the latter case we + # use the reduction value from the aggregation. + if isinstance(_, ops.Field): + value = _.rel.values[_.name] + else: + value = _ + return ops.ScalarSubquery(value.to_expr().as_table()) + + +def rewrite_filter_input(value): + return value.replace( + wrap_analytic | filter_wrap_reduction, filter=p.Value & ~p.WindowFunction + ) + + +@replace(p.Analytic | p.Reduction) +def window_wrap_reduction(_, window): + # Wrap analytic and reduction functions in a window function. Used in the + # value.over() API. + return ops.WindowFunction( + _, + how=window.how, + start=window.start, + end=window.end, + group_by=window.groupings, + order_by=window.orderings, + ) + + +@replace(p.WindowFunction) +def window_merge_frames(_, window): + # Merge window frames, used in the value.over() and groupby.select() APIs. + if _.how != window.how: + raise ExpressionError( + f"Unable to merge {_.how} window with {window.how} window" + ) + elif _.start and window.start and _.start != window.start: + raise ExpressionError( + "Unable to merge windows with conflicting `start` boundary" + ) + elif _.end and window.end and _.end != window.end: + raise ExpressionError("Unable to merge windows with conflicting `end` boundary") + + start = _.start or window.start + end = _.end or window.end + group_by = tuple(toolz.unique(_.group_by + window.groupings)) + + order_keys = {} + for sort_key in window.orderings + _.order_by: + order_keys[sort_key.expr] = sort_key.ascending, sort_key.nulls_first + + order_by = ( + ops.SortKey(expr, ascending=ascending, nulls_first=nulls_first) + for expr, (ascending, nulls_first) in order_keys.items() + ) + return _.copy(start=start, end=end, group_by=group_by, order_by=order_by) + + +def rewrite_window_input(value, window): + context = {"window": window} + # if self is a reduction or analytic function, wrap it in a window function + node = value.replace( + window_wrap_reduction, + filter=p.Value & ~p.WindowFunction, + context=context, + ) + # if self is already a window function, merge the existing window frame + # with the requested window frame + return node.replace(window_merge_frames, filter=p.Value, context=context) + + +# TODO(kszucs): schema comparison should be updated to not distinguish between +# different column order +@replace(p.Project(y @ p.Relation) & Check(_.schema == y.schema)) +def complete_reprojection(_, y): + # TODO(kszucs): this could be moved to the pattern itself but not sure how + # to express it, especially in a shorter way then the following check + for name in _.schema: + if _.values[name] != ops.Field(y, name): + return _ + return y + + +@replace(p.Project(y @ p.Project)) +def subsequent_projects(_, y): + rule = p.Field(y, name) >> Item(y.values, name) + values = {k: v.replace(rule, filter=ops.Value) for k, v in _.values.items()} + return ops.Project(y.parent, values) + + +@replace(p.Filter(y @ p.Filter)) +def subsequent_filters(_, y): + rule = p.Field(y, name) >> d.Field(y.parent, name) + preds = tuple(v.replace(rule, filter=ops.Value) for v in _.predicates) + return ops.Filter(y.parent, y.predicates + preds) + + +@replace(p.Filter(y @ p.Project)) +def reorder_filter_project(_, y): + rule = p.Field(y, name) >> Item(y.values, name) + preds = tuple(v.replace(rule, filter=ops.Value) for v in _.predicates) + + inner = ops.Filter(y.parent, preds) + rule = p.Field(y.parent, name) >> d.Field(inner, name) + projs = {k: v.replace(rule, filter=ops.Value) for k, v in y.values.items()} + + return ops.Project(inner, projs) + + +def simplify(node): + # TODO(kszucs): add a utility to the graph module to do rewrites in multiple + # passes after each other + node = node.replace(reorder_filter_project) + node = node.replace(reorder_filter_project) + node = node.replace(subsequent_projects | subsequent_filters) + node = node.replace(complete_reprojection) + return node diff --git a/third_party/bigframes_vendored/ibis/expr/rewrites.py b/third_party/bigframes_vendored/ibis/expr/rewrites.py new file mode 100644 index 00000000000..0583d2b87e3 --- /dev/null +++ b/third_party/bigframes_vendored/ibis/expr/rewrites.py @@ -0,0 +1,380 @@ +# Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/expr/rewrites.py + +"""Some common rewrite functions to be shared between backends.""" + +from __future__ import annotations + +from collections import defaultdict + +from ibis.common.collections import FrozenDict # noqa: TCH001 +from ibis.common.deferred import _, deferred, Item, var +from ibis.common.exceptions import ExpressionError, IbisInputError +from ibis.common.graph import Node as Traversable +from ibis.common.graph import traverse +from ibis.common.grounds import Concrete +from ibis.common.patterns import Check, pattern, replace +from ibis.common.typing import VarTuple # noqa: TCH001 +import ibis.expr.operations as ops +from ibis.util import Namespace, promote_list +import toolz + +p = Namespace(pattern, module=ops) +d = Namespace(deferred, module=ops) + + +x = var("x") +y = var("y") +name = var("name") + + +class DerefMap(Concrete, Traversable): + """Trace and replace fields from earlier relations in the hierarchy. + + In order to provide a nice user experience, we need to allow expressions + from earlier relations in the hierarchy. Consider the following example: + + t = ibis.table([('a', 'int64'), ('b', 'string')], name='t') + t1 = t.select([t.a, t.b]) + t2 = t1.filter(t.a > 0) # note that not t1.a is referenced here + t3 = t2.select(t.a) # note that not t2.a is referenced here + + However the relational operations in the IR are strictly enforcing that + the expressions are referencing the immediate parent only. So we need to + track fields upwards the hierarchy to replace `t.a` with `t1.a` and `t2.a` + in the example above. This is called dereferencing. + + Whether we can treat or not a field of a relation semantically equivalent + with a field of an earlier relation in the hierarchy depends on the + `.values` mapping of the relation. Leaf relations, like `t` in the example + above, have an empty `.values` mapping, so we cannot dereference fields + from them. On the other hand a projection, like `t1` in the example above, + has a `.values` mapping like `{'a': t.a, 'b': t.b}`, so we can deduce that + `t1.a` is semantically equivalent with `t.a` and so on. + """ + + """The relations we want the values to point to.""" + rels: VarTuple[ops.Relation] + + """Substitution mapping from values of earlier relations to the fields of `rels`.""" + subs: FrozenDict[ops.Value, ops.Field] + + """Ambiguous field references.""" + ambigs: FrozenDict[ops.Value, VarTuple[ops.Value]] + + @classmethod + def from_targets(cls, rels, extra=None): + """Create a dereference map from a list of target relations. + + Usually a single relation is passed except for joins where multiple + relations are involved. + + Parameters + ---------- + rels : list of ops.Relation + The target relations to dereference to. + extra : dict, optional + Extra substitutions to be added to the dereference map. + + Returns + ------- + DerefMap + """ + rels = promote_list(rels) + mapping = defaultdict(dict) + for rel in rels: + for field in rel.fields.values(): + for value, distance in cls.backtrack(field): + mapping[value][field] = distance + + subs, ambigs = {}, {} + for from_, to in mapping.items(): + mindist = min(to.values()) + minkeys = [k for k, v in to.items() if v == mindist] + # if all the closest fields are from the same relation, then we + # can safely substitute them and we pick the first one arbitrarily + if all(minkeys[0].relations == k.relations for k in minkeys): + subs[from_] = minkeys[0] + else: + ambigs[from_] = minkeys + + if extra is not None: + subs.update(extra) + + return cls(rels, subs, ambigs) + + @classmethod + def backtrack(cls, value): + """Backtrack the field in the relation hierarchy. + + The field is traced back until no modification is made, so only follow + ops.Field nodes not arbitrary values. + + Parameters + ---------- + value : ops.Value + The value to backtrack. + + Yields + ------ + tuple[ops.Field, int] + The value node and the distance from the original value. + """ + distance = 0 + # track down the field in the hierarchy until no modification + # is made so only follow ops.Field nodes not arbitrary values; + while isinstance(value, ops.Field): + yield value, distance + value = value.rel.values.get(value.name) + distance += 1 + if ( + value is not None + and value.relations + and not value.find(ops.Impure, filter=ops.Value) + ): + yield value, distance + + def dereference(self, value): + """Dereference a value to the target relations. + + Also check for ambiguous field references. If a field reference is found + which is marked as ambiguous, then raise an error. + + Parameters + ---------- + value : ops.Value + The value to dereference. + + Returns + ------- + ops.Value + The dereferenced value. + """ + ambigs = value.find(lambda x: x in self.ambigs, filter=ops.Value) + if ambigs: + raise IbisInputError( + f"Ambiguous field reference {ambigs!r} in expression {value!r}" + ) + return value.replace(self.subs, filter=ops.Value) + + +def flatten_predicates(node): + """Yield the expressions corresponding to the `And` nodes of a predicate. + + Examples + -------- + >>> import ibis + >>> t = ibis.table([("a", "int64"), ("b", "string")], name="t") + >>> filt = (t.a == 1) & (t.b == "foo") + >>> predicates = flatten_predicates(filt.op()) + >>> len(predicates) + 2 + >>> predicates[0].to_expr().name("left") + r0 := UnboundTable: t + a int64 + b string + left: r0.a == 1 + >>> predicates[1].to_expr().name("right") + r0 := UnboundTable: t + a int64 + b string + right: r0.b == 'foo' + + """ + + def predicate(node): + if isinstance(node, ops.And): + # proceed and don't yield the node + return True, None + else: + # halt and yield the node + return False, node + + return list(traverse(predicate, node)) + + +@replace(p.Field(p.JoinChain)) +def peel_join_field(_): + return _.rel.values[_.name] + + +@replace(p.ScalarParameter) +def replace_parameter(_, params, **kwargs): + """Replace scalar parameters with their values.""" + return ops.Literal(value=params[_], dtype=_.dtype) + + +@replace(p.StringSlice) +def lower_stringslice(_, **kwargs): + """Rewrite StringSlice in terms of Substring.""" + if _.end is None: + return ops.Substring(_.arg, start=_.start) + if _.start is None: + return ops.Substring(_.arg, start=0, length=_.end) + if ( + isinstance(_.start, ops.Literal) + and isinstance(_.start.value, int) + and isinstance(_.end, ops.Literal) + and isinstance(_.end.value, int) + ): + # optimization for constant values + length = _.end.value - _.start.value + else: + length = ops.Subtract(_.end, _.start) + return ops.Substring(_.arg, start=_.start, length=length) + + +@replace(p.Analytic) +def project_wrap_analytic(_, rel): + # Wrap analytic functions in a window function + return ops.WindowFunction(_) + + +@replace(p.Reduction) +def project_wrap_reduction(_, rel): + # Query all the tables that the reduction depends on + if _.relations == {rel}: + # The reduction is fully originating from the `rel`, so turn + # it into a window function of `rel` + return ops.WindowFunction(_) + else: + # 1. The reduction doesn't depend on any table, constructed from + # scalar values, so turn it into a scalar subquery. + # 2. The reduction is originating from `rel` and other tables, + # so this is a correlated scalar subquery. + # 3. The reduction is originating entirely from other tables, + # so this is an uncorrelated scalar subquery. + return ops.ScalarSubquery(_.to_expr().as_table()) + + +def rewrite_project_input(value, relation): + # we need to detect reductions which are either turned into window functions + # or scalar subqueries depending on whether they are originating from the + # relation + return value.replace( + project_wrap_analytic | project_wrap_reduction, + filter=p.Value & ~p.WindowFunction, + context={"rel": relation}, + ) + + +ReductionLike = p.Reduction | p.Field(p.Aggregate(groups={})) + + +@replace(ReductionLike) +def filter_wrap_reduction(_): + # Wrap reductions or fields referencing an aggregation without a group by - + # which are scalar fields - in a scalar subquery. In the latter case we + # use the reduction value from the aggregation. + if isinstance(_, ops.Field): + value = _.rel.values[_.name] + else: + value = _ + return ops.ScalarSubquery(value.to_expr().as_table()) + + +def rewrite_filter_input(value): + return value.replace(filter_wrap_reduction, filter=p.Value & ~p.WindowFunction) + + +@replace(p.Analytic | p.Reduction) +def window_wrap_reduction(_, window): + # Wrap analytic and reduction functions in a window function. Used in the + # value.over() API. + return ops.WindowFunction( + _, + how=window.how, + start=window.start, + end=window.end, + group_by=window.groupings, + order_by=window.orderings, + ) + + +@replace(p.WindowFunction) +def window_merge_frames(_, window): + # Merge window frames, used in the value.over() and groupby.select() APIs. + if _.how != window.how: + raise ExpressionError( + f"Unable to merge {_.how} window with {window.how} window" + ) + elif _.start and window.start and _.start != window.start: + raise ExpressionError( + "Unable to merge windows with conflicting `start` boundary" + ) + elif _.end and window.end and _.end != window.end: + raise ExpressionError("Unable to merge windows with conflicting `end` boundary") + + start = _.start or window.start + end = _.end or window.end + group_by = tuple(toolz.unique(_.group_by + window.groupings)) + + order_keys = {} + for sort_key in window.orderings + _.order_by: + order_keys[sort_key.expr] = sort_key.ascending, sort_key.nulls_first + + order_by = ( + ops.SortKey(expr, ascending=ascending, nulls_first=nulls_first) + for expr, (ascending, nulls_first) in order_keys.items() + ) + return _.copy(start=start, end=end, group_by=group_by, order_by=order_by) + + +def rewrite_window_input(value, window): + context = {"window": window} + # if self is a reduction or analytic function, wrap it in a window function + node = value.replace( + window_wrap_reduction, + filter=p.Value & ~p.WindowFunction, + context=context, + ) + # if self is already a window function, merge the existing window frame + # with the requested window frame + return node.replace(window_merge_frames, filter=p.Value, context=context) + + +# TODO(kszucs): schema comparison should be updated to not distinguish between +# different column order +@replace(p.Project(y @ p.Relation) & Check(_.schema == y.schema)) +def complete_reprojection(_, y): + # TODO(kszucs): this could be moved to the pattern itself but not sure how + # to express it, especially in a shorter way then the following check + for name in _.schema: + if _.values[name] != ops.Field(y, name): + return _ + return y + + +@replace(p.Project(y @ p.Project)) +def subsequent_projects(_, y): + rule = p.Field(y, name) >> Item(y.values, name) + values = {k: v.replace(rule, filter=ops.Value) for k, v in _.values.items()} + return ops.Project(y.parent, values) + + +@replace(p.Filter(y @ p.Filter)) +def subsequent_filters(_, y): + rule = p.Field(y, name) >> d.Field(y.parent, name) + preds = tuple(v.replace(rule, filter=ops.Value) for v in _.predicates) + return ops.Filter(y.parent, y.predicates + preds) + + +@replace(p.Filter(y @ p.Project)) +def reorder_filter_project(_, y): + rule = p.Field(y, name) >> Item(y.values, name) + preds = tuple(v.replace(rule, filter=ops.Value) for v in _.predicates) + + inner = ops.Filter(y.parent, preds) + rule = p.Field(y.parent, name) >> d.Field(inner, name) + projs = {k: v.replace(rule, filter=ops.Value) for k, v in y.values.items()} + + return ops.Project(inner, projs) + + +def simplify(node): + # TODO(kszucs): add a utility to the graph module to do rewrites in multiple + # passes after each other + node = node.replace(reorder_filter_project) + node = node.replace(reorder_filter_project) + node = node.replace(subsequent_projects | subsequent_filters) + node = node.replace(complete_reprojection) + return node From 60061052741c6cffd713214d3422c04497f62a66 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 10 Sep 2024 09:50:23 -0700 Subject: [PATCH 814/921] chore: make doctest tests less parallel (#960) Parallel test runs contribute to them running into rate limiting quota issue with cloud functions. Currently doctest nox session is running in about 9 minutes, while presubmit is running in 45 minutes. This gives us some leeway in making doctest less parallel to gain less likelihood of running into rate quota issue without comprimising the overall PR merge readiness turnaround. --- noxfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index a7f05002106..6abd943ed7b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -304,6 +304,7 @@ def run_system( print_duration=False, extra_pytest_options=(), timeout_seconds=900, + num_workers=20, ): """Run the system test suite.""" constraints_path = str( @@ -323,7 +324,7 @@ def run_system( pytest_cmd = [ "py.test", "--quiet", - "-n=20", + f"-n={num_workers}", # Any individual test taking longer than 15 mins will be terminated. f"--timeout={timeout_seconds}", # Log 20 slowest tests @@ -392,6 +393,7 @@ def doctest(session: nox.sessions.Session): ), test_folder="bigframes", check_cov=True, + num_workers=5, ) From 7b59b6dc6f0cedfee713b5b273d46fa84b70bfa4 Mon Sep 17 00:00:00 2001 From: rey-esp Date: Tue, 10 Sep 2024 17:03:23 +0000 Subject: [PATCH 815/921] feat: include the bigframes package version alongside the feedback link in error messages (#936) * chore: update ABSTRACT_METHOD_ERROR_MESSAGE to include bigframes version number * fix bigframes import * add BF_VERSION to FEEDBACK_LINK, add test to test_formatting_helpers.py to ensure the version is included in the error message, add test_constants.py to ensure BF_VERSION is not an empty string, add BF_VERSION to bigframes/constants.py * format --- bigframes/constants.py | 1 + tests/unit/test_constants.py | 20 ++++++++++++++++++++ tests/unit/test_formatting_helpers.py | 11 +++++++++++ third_party/bigframes_vendored/constants.py | 4 ++++ 4 files changed, 36 insertions(+) create mode 100644 tests/unit/test_constants.py diff --git a/bigframes/constants.py b/bigframes/constants.py index 3c18fd20bdf..d6fe6997135 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -21,6 +21,7 @@ import bigframes_vendored.constants +BF_VERSION = bigframes_vendored.constants.BF_VERSION FEEDBACK_LINK = bigframes_vendored.constants.FEEDBACK_LINK ABSTRACT_METHOD_ERROR_MESSAGE = ( bigframes_vendored.constants.ABSTRACT_METHOD_ERROR_MESSAGE diff --git a/tests/unit/test_constants.py b/tests/unit/test_constants.py new file mode 100644 index 00000000000..aabc09c388e --- /dev/null +++ b/tests/unit/test_constants.py @@ -0,0 +1,20 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bigframes.constants as constants + + +def test_feedback_link_includes_version(): + assert len(constants.BF_VERSION) > 0 + assert constants.BF_VERSION in constants.FEEDBACK_LINK diff --git a/tests/unit/test_formatting_helpers.py b/tests/unit/test_formatting_helpers.py index 9db9b372e21..3c966752c9b 100644 --- a/tests/unit/test_formatting_helpers.py +++ b/tests/unit/test_formatting_helpers.py @@ -44,3 +44,14 @@ def test_wait_for_job_error_includes_feedback_link(): cap_exc.match("Test message 123.") cap_exc.match(constants.FEEDBACK_LINK) + + +def test_wait_for_job_error_includes_version(): + mock_job = mock.create_autospec(bigquery.LoadJob) + mock_job.result.side_effect = api_core_exceptions.BadRequest("Test message 123.") + + with pytest.raises(api_core_exceptions.BadRequest) as cap_exc: + formatting_helpers.wait_for_job(mock_job) + + cap_exc.match("Test message 123.") + cap_exc.match(constants.BF_VERSION) diff --git a/third_party/bigframes_vendored/constants.py b/third_party/bigframes_vendored/constants.py index 0d4a7d1df69..91084b38f9c 100644 --- a/third_party/bigframes_vendored/constants.py +++ b/third_party/bigframes_vendored/constants.py @@ -16,10 +16,14 @@ This module should not depend on any others in the package. """ +import bigframes.version + +BF_VERSION = bigframes.version.__version__ FEEDBACK_LINK = ( "Share your usecase with the BigQuery DataFrames team at the " "https://bit.ly/bigframes-feedback survey." + f"You are currently running BigFrames version {BF_VERSION}" ) ABSTRACT_METHOD_ERROR_MESSAGE = ( From 569a7ad5a2fe72a8d3deb1304eb7180b176d8830 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 10 Sep 2024 19:09:29 -0700 Subject: [PATCH 816/921] Revert "test: adjust expectations in ml tests after bqml model update (#972)" (#975) This reverts commit aeccc4842e2dae0731d09bbf5f1295bf95ebb44c. --- tests/system/small/ml/test_ensemble.py | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/system/small/ml/test_ensemble.py b/tests/system/small/ml/test_ensemble.py index 42aa3809567..55d9fef661a 100644 --- a/tests/system/small/ml/test_ensemble.py +++ b/tests/system/small/ml/test_ensemble.py @@ -39,12 +39,12 @@ def test_xgbregressor_model_score( result = penguins_xgbregressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "mean_absolute_error": [115.57598], - "mean_squared_error": [23455.52121], - "mean_squared_log_error": [0.00147], - "median_absolute_error": [88.01318], - "r2_score": [0.96368], - "explained_variance": [0.96384], + "mean_absolute_error": [108.77582], + "mean_squared_error": [20943.272738], + "mean_squared_log_error": [0.00135], + "median_absolute_error": [86.313477], + "r2_score": [0.967571], + "explained_variance": [0.967609], }, dtype="Float64", ) @@ -76,12 +76,12 @@ def test_xgbregressor_model_score_series( result = penguins_xgbregressor_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "mean_absolute_error": [115.57598], - "mean_squared_error": [23455.52121], - "mean_squared_log_error": [0.00147], - "median_absolute_error": [88.01318], - "r2_score": [0.96368], - "explained_variance": [0.96384], + "mean_absolute_error": [108.77582], + "mean_squared_error": [20943.272738], + "mean_squared_log_error": [0.00135], + "median_absolute_error": [86.313477], + "r2_score": [0.967571], + "explained_variance": [0.967609], }, dtype="Float64", ) @@ -136,12 +136,12 @@ def test_to_gbq_saved_xgbregressor_model_scores( result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "mean_absolute_error": [115.57598], - "mean_squared_error": [23455.52121], - "mean_squared_log_error": [0.00147], - "median_absolute_error": [88.01318], - "r2_score": [0.96368], - "explained_variance": [0.96384], + "mean_absolute_error": [109.016973], + "mean_squared_error": [20867.299758], + "mean_squared_log_error": [0.00135], + "median_absolute_error": [86.490234], + "r2_score": [0.967458], + "explained_variance": [0.967504], }, dtype="Float64", ) @@ -260,11 +260,11 @@ def test_to_gbq_saved_xgbclassifier_model_scores( result = saved_model.score(X_test, y_test).to_pandas() expected = pandas.DataFrame( { - "precision": [0.662674], - "recall": [0.664646], - "accuracy": [0.994012], - "f1_score": [0.663657], - "log_loss": [0.374438], + "precision": [1.0], + "recall": [1.0], + "accuracy": [1.0], + "f1_score": [1.0], + "log_loss": [0.331442], "roc_auc": [1.0], }, dtype="Float64", From 8fbfb9a52eb17ed44bf0adbce52278ef4e2c048e Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Wed, 11 Sep 2024 10:50:31 -0700 Subject: [PATCH 817/921] Replace raw pd types with predefined constants (#974) --- bigframes/dtypes.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 45c1e7e4e2f..bfed783e1ea 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -189,18 +189,18 @@ class SimpleDtypeInfo: "binary[pyarrow]", ] -BOOL_BIGFRAMES_TYPES = [pd.BooleanDtype()] +BOOL_BIGFRAMES_TYPES = [BOOL_DTYPE] # Corresponds to the pandas concept of numeric type (such as when 'numeric_only' is specified in an operation) # Pandas is inconsistent, so two definitions are provided, each used in different contexts NUMERIC_BIGFRAMES_TYPES_RESTRICTIVE = [ - pd.Float64Dtype(), - pd.Int64Dtype(), + FLOAT_DTYPE, + INT_DTYPE, ] NUMERIC_BIGFRAMES_TYPES_PERMISSIVE = NUMERIC_BIGFRAMES_TYPES_RESTRICTIVE + [ - pd.BooleanDtype(), - pd.ArrowDtype(pa.decimal128(38, 9)), - pd.ArrowDtype(pa.decimal256(76, 38)), + BOOL_DTYPE, + NUMERIC_DTYPE, + BIGNUMERIC_DTYPE, ] @@ -308,10 +308,10 @@ def is_bool_coercable(type_: ExpressionType) -> bool: # special case - string[pyarrow] doesn't include the storage in its name, and both # "string" and "string[pyarrow]" are accepted -BIGFRAMES_STRING_TO_BIGFRAMES["string[pyarrow]"] = pd.StringDtype(storage="pyarrow") +BIGFRAMES_STRING_TO_BIGFRAMES["string[pyarrow]"] = STRING_DTYPE # special case - both "Int64" and "int64[pyarrow]" are accepted -BIGFRAMES_STRING_TO_BIGFRAMES["int64[pyarrow]"] = pd.Int64Dtype() +BIGFRAMES_STRING_TO_BIGFRAMES["int64[pyarrow]"] = INT_DTYPE # For the purposes of dataframe.memory_usage DTYPE_BYTE_SIZES = { @@ -552,14 +552,14 @@ def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: elif pd.api.types.is_numeric_dtype(dtype): # Implicit conversion currently only supported for numeric types if pd.api.types.is_bool(scalar): - return lcd_type(pd.BooleanDtype(), dtype) + return lcd_type(BOOL_DTYPE, dtype) if pd.api.types.is_float(scalar): - return lcd_type(pd.Float64Dtype(), dtype) + return lcd_type(FLOAT_DTYPE, dtype) if pd.api.types.is_integer(scalar): - return lcd_type(pd.Int64Dtype(), dtype) + return lcd_type(INT_DTYPE, dtype) if isinstance(scalar, decimal.Decimal): # TODO: Check context to see if can use NUMERIC instead of BIGNUMERIC - return lcd_type(pd.ArrowDtype(pa.decimal256(76, 38)), dtype) + return lcd_type(BIGNUMERIC_DTYPE, dtype) return None @@ -573,11 +573,11 @@ def lcd_type(*dtypes: Dtype) -> Dtype: return unique_dtypes.pop() # Implicit conversion currently only supported for numeric types hierarchy: list[Dtype] = [ - pd.BooleanDtype(), - pd.Int64Dtype(), - pd.ArrowDtype(pa.decimal128(38, 9)), - pd.ArrowDtype(pa.decimal256(76, 38)), - pd.Float64Dtype(), + BOOL_DTYPE, + INT_DTYPE, + NUMERIC_DTYPE, + BIGNUMERIC_DTYPE, + FLOAT_DTYPE, ] if any([dtype not in hierarchy for dtype in dtypes]): return None From da3524bc799489d8a5ad53e5b5e8e1a3656c0692 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:52:38 -0700 Subject: [PATCH 818/921] chore: update notebook session to run faster. (#970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update notebook session to run faster. * update lint * separate to two runs. * update code * update code * update format * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix nbmake internal error. --------- Co-authored-by: Owl Bot --- noxfile.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 6abd943ed7b..5dbcdea5832 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,10 +16,13 @@ from __future__ import absolute_import +import multiprocessing import os import pathlib import re import shutil +import time +import traceback from typing import Dict, List import warnings @@ -754,6 +757,12 @@ def notebook(session: nox.Session): for nb in notebooks + list(notebooks_reg): assert os.path.exists(nb), nb + # Determine whether to enable multi-process mode based on the environment + # variable. If BENCHMARK_AND_PUBLISH is "true", it indicates we're running + # a benchmark, so we disable multi-process mode. If BENCHMARK_AND_PUBLISH + # is "false", we enable multi-process mode for faster execution. + multi_process_mode = os.getenv("BENCHMARK_AND_PUBLISH", "false") == "false" + try: # Populate notebook parameters and make a backup so that the notebooks # are runnable. @@ -762,23 +771,65 @@ def notebook(session: nox.Session): CURRENT_DIRECTORY / "scripts" / "notebooks_fill_params.py", *notebooks, ) + + # Shared flag using multiprocessing.Manager() to indicate if + # any process encounters an error. This flag may be updated + # across different processes. + error_flag = multiprocessing.Manager().Value("i", False) + processes = [] for notebook in notebooks: - session.run( + args = ( "python", "scripts/run_and_publish_benchmark.py", "--notebook", f"--benchmark-path={notebook}", ) - + if multi_process_mode: + process = multiprocessing.Process( + target=_run_process, + args=(session, args, error_flag), + ) + process.start() + processes.append(process) + # Adding a small delay between starting each + # process to avoid potential race conditions。 + time.sleep(1) + else: + session.run(*args) + + for process in processes: + process.join() + + processes = [] for notebook, regions in notebooks_reg.items(): for region in regions: - session.run( + args = ( "python", "scripts/run_and_publish_benchmark.py", "--notebook", f"--benchmark-path={notebook}", f"--region={region}", ) + if multi_process_mode: + process = multiprocessing.Process( + target=_run_process, + args=(session, args, error_flag), + ) + process.start() + processes.append(process) + # Adding a small delay between starting each + # process to avoid potential race conditions。 + time.sleep(1) + else: + session.run(*args) + + for process in processes: + process.join() + + # Check the shared error flag and raise an exception if any process + # reported an error + if error_flag.value: + raise Exception("Errors occurred in one or more subprocesses.") finally: # Prevent our notebook changes from getting checked in to git # accidentally. @@ -795,6 +846,15 @@ def notebook(session: nox.Session): ) +def _run_process(session: nox.Session, args, error_flag): + try: + session.run(*args) + except Exception: + traceback_str = traceback.format_exc() + print(traceback_str) + error_flag.value = True + + @nox.session(python=DEFAULT_PYTHON_VERSION) def benchmark(session: nox.Session): session.install("-e", ".[all]") From 36385bf62065f7cb9b3c5b770ca57d7d1c88ef27 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:01:52 -0700 Subject: [PATCH 819/921] chore: tpch q7 workaround removed. (#969) * chore: tpch q7 workaround removed. * format fix --- third_party/bigframes_vendored/tpch/queries/q7.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/third_party/bigframes_vendored/tpch/queries/q7.py b/third_party/bigframes_vendored/tpch/queries/q7.py index 4ea5e6b2387..d922efd1e21 100644 --- a/third_party/bigframes_vendored/tpch/queries/q7.py +++ b/third_party/bigframes_vendored/tpch/queries/q7.py @@ -56,14 +56,6 @@ def q(dataset_id: str, session: bigframes.Session): total = bpd.concat([df1, df2]) - # TODO(huanc): TEMPORARY CODE to force a fresh start. Currently, - # combining everything into a single query seems to trigger a bug - # causing incorrect results. This workaround involves writing to and - # then reading from BigQuery. Remove this once b/355714291 is - # resolved. - dest = total.to_gbq() - total = bpd.read_gbq(dest) - total = total[(total["L_SHIPDATE"] >= var3) & (total["L_SHIPDATE"] <= var4)] total["VOLUME"] = total["L_EXTENDEDPRICE"] * (1.0 - total["L_DISCOUNT"]) total["L_YEAR"] = total["L_SHIPDATE"].dt.year From d42d674052c77b6e15c0f8591f53271d4bed922f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:34:06 -0700 Subject: [PATCH 820/921] chore(main): release 1.17.0 (#958) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 25 +++++++++++++++++++++++++ bigframes/version.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3dae5af719..a989d8af662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.17.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.16.0...v1.17.0) (2024-09-11) + + +### Features + +* Add `__version__` alias to bigframes.pandas ([#967](https://github.com/googleapis/python-bigquery-dataframes/issues/967)) ([9ce10b4](https://github.com/googleapis/python-bigquery-dataframes/commit/9ce10b4248f106ac9e09fc0fe686cece86827337)) +* Add Gemini 1.5 stable models support ([#945](https://github.com/googleapis/python-bigquery-dataframes/issues/945)) ([c1cde19](https://github.com/googleapis/python-bigquery-dataframes/commit/c1cde19769c169b962b58b25f0be61c8c41edb95)) +* Allow setting table labels in `to_gbq` ([#941](https://github.com/googleapis/python-bigquery-dataframes/issues/941)) ([cccc6ca](https://github.com/googleapis/python-bigquery-dataframes/commit/cccc6ca8c1271097bbe15e3d9ccdcfd7c633227a)) +* Define list accessor for bigframes Series ([#946](https://github.com/googleapis/python-bigquery-dataframes/issues/946)) ([8e8279d](https://github.com/googleapis/python-bigquery-dataframes/commit/8e8279d4da90feb5766f266b49cb417f8cbec6c9)) +* Enable read_csv() to process other files ([#940](https://github.com/googleapis/python-bigquery-dataframes/issues/940)) ([3b35860](https://github.com/googleapis/python-bigquery-dataframes/commit/3b35860776033fc8e71e471422c6d2b9366a7c9f)) +* Include the bigframes package version alongside the feedback link in error messages ([#936](https://github.com/googleapis/python-bigquery-dataframes/issues/936)) ([7b59b6d](https://github.com/googleapis/python-bigquery-dataframes/commit/7b59b6dc6f0cedfee713b5b273d46fa84b70bfa4)) + + +### Bug Fixes + +* Astype Decimal to Int64 conversion. ([#957](https://github.com/googleapis/python-bigquery-dataframes/issues/957)) ([27764a6](https://github.com/googleapis/python-bigquery-dataframes/commit/27764a64f90092374458fafbe393bc6c30c85681)) +* Make `read_gbq_function` work for multi-param functions ([#947](https://github.com/googleapis/python-bigquery-dataframes/issues/947)) ([c750be6](https://github.com/googleapis/python-bigquery-dataframes/commit/c750be6093941677572a10c36a92984e954de32c)) +* Support `read_gbq_function` for axis=1 application ([#950](https://github.com/googleapis/python-bigquery-dataframes/issues/950)) ([86e54b1](https://github.com/googleapis/python-bigquery-dataframes/commit/86e54b13d2b91517b1df2d9c1f852a8e1925309a)) + + +### Documentation + +* Add docstring returns section to Options ([#937](https://github.com/googleapis/python-bigquery-dataframes/issues/937)) ([a2640a2](https://github.com/googleapis/python-bigquery-dataframes/commit/a2640a2d731c8d0aba1307311092f5e85b8ba077)) +* Update title of pypi notebook example to reflect use of the PyPI public dataset ([#952](https://github.com/googleapis/python-bigquery-dataframes/issues/952)) ([cd62e60](https://github.com/googleapis/python-bigquery-dataframes/commit/cd62e604967adac0c2f8600408bd9ce7886f2f98)) + ## [1.16.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.15.0...v1.16.0) (2024-09-04) diff --git a/bigframes/version.py b/bigframes/version.py index d5b4691b988..2c0c6e4d3ac 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.16.0" +__version__ = "1.17.0" From 3a4a9de1f797557dfbbb7317eb3754e16131ddbf Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 11 Sep 2024 13:35:17 -0700 Subject: [PATCH 821/921] refactor: Simplify join node definition (#966) --- bigframes/core/__init__.py | 149 ++---------- bigframes/core/blocks.py | 311 ++++++++++++------------ bigframes/core/compile/compiled.py | 2 +- bigframes/core/compile/compiler.py | 6 +- bigframes/core/compile/single_column.py | 35 ++- bigframes/core/guid.py | 2 +- bigframes/core/identifiers.py | 26 ++ bigframes/core/join_def.py | 39 +-- bigframes/core/nodes.py | 30 +-- bigframes/core/schema.py | 3 + 10 files changed, 257 insertions(+), 346 deletions(-) create mode 100644 bigframes/core/identifiers.py diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index f65509e5b78..b4074dd94f9 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -17,9 +17,8 @@ import datetime import functools import io -import itertools import typing -from typing import Iterable, Optional, Sequence +from typing import Iterable, Optional, Sequence, Tuple import warnings import google.cloud.bigquery @@ -191,19 +190,14 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: nodes.ConcatNode(children=tuple([self.node, *[val.node for val in other]])) ) - def project_to_id(self, expression: ex.Expression, output_id: str): + def compute_values(self, assignments: Sequence[Tuple[ex.Expression, str]]): return ArrayValue( - nodes.ProjectionNode( - child=self.node, - assignments=( - ( - expression, - output_id, - ), - ), - ) + nodes.ProjectionNode(child=self.node, assignments=tuple(assignments)) ) + def project_to_id(self, expression: ex.Expression, output_id: str): + return self.compute_values(((expression, output_id),)) + def assign(self, source_id: str, destination_id: str) -> ArrayValue: if destination_id in self.column_ids: # Mutate case exprs = [ @@ -341,124 +335,33 @@ def _reproject_to_table(self) -> ArrayValue: ) ) - def unpivot( - self, - row_labels: typing.Sequence[typing.Hashable], - unpivot_columns: typing.Sequence[ - typing.Tuple[str, typing.Tuple[typing.Optional[str], ...]] - ], - *, - passthrough_columns: typing.Sequence[str] = (), - index_col_ids: typing.Sequence[str] = ["index"], - join_side: typing.Literal["left", "right"] = "left", - ) -> ArrayValue: - """ - Unpivot ArrayValue columns. - - Args: - row_labels: Identifies the source of the row. Must be equal to length to source column list in unpivot_columns argument. - unpivot_columns: Mapping of column id to list of input column ids. Lists of input columns may use None. - passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. - index_col_id (str): The column id to be used for the row labels. - - Returns: - ArrayValue: The unpivoted ArrayValue - """ - # There will be N labels, used to disambiguate which of N source columns produced each output row - explode_offsets_id = bigframes.core.guid.generate_guid("unpivot_offsets_") - labels_array = self._create_unpivot_labels_array( - row_labels, index_col_ids, explode_offsets_id - ) - - # Unpivot creates N output rows for each input row, labels disambiguate these N rows - joined_array = self._cross_join_w_labels(labels_array, join_side) - - # Build the output rows as a case statment that selects between the N input columns - unpivot_exprs = [] - # Supports producing multiple stacked ouput columns for stacking only part of hierarchical index - for col_id, input_ids in unpivot_columns: - # row explode offset used to choose the input column - # we use offset instead of label as labels are not necessarily unique - cases = itertools.chain( - *( - ( - ops.eq_op.as_expr(explode_offsets_id, ex.const(i)), - ex.free_var(id_or_null) - if (id_or_null is not None) - else ex.const(None), - ) - for i, id_or_null in enumerate(input_ids) - ) - ) - col_expr = ops.case_when_op.as_expr(*cases) - unpivot_exprs.append((col_expr, col_id)) - - unpivot_col_ids = [id for id, _ in unpivot_columns] - return ArrayValue( - nodes.ProjectionNode( - child=joined_array.node, - assignments=(*unpivot_exprs,), - ) - ).select_columns([*index_col_ids, *unpivot_col_ids, *passthrough_columns]) - - def _cross_join_w_labels( - self, labels_array: ArrayValue, join_side: typing.Literal["left", "right"] - ) -> ArrayValue: - """ - Convert each row in self to N rows, one for each label in labels array. - """ - table_join_side = ( - join_def.JoinSide.LEFT if join_side == "left" else join_def.JoinSide.RIGHT - ) - labels_join_side = table_join_side.inverse() - labels_mappings = tuple( - join_def.JoinColumnMapping(labels_join_side, id, id) - for id in labels_array.schema.names - ) - table_mappings = tuple( - join_def.JoinColumnMapping(table_join_side, id, id) - for id in self.schema.names - ) - join = join_def.JoinDefinition( - conditions=(), mappings=(*labels_mappings, *table_mappings), type="cross" - ) - if join_side == "left": - joined_array = self.relational_join(labels_array, join_def=join) - else: - joined_array = labels_array.relational_join(self, join_def=join) - return joined_array - - def _create_unpivot_labels_array( - self, - former_column_labels: typing.Sequence[typing.Hashable], - col_ids: typing.Sequence[str], - offsets_id: str, - ) -> ArrayValue: - """Create an ArrayValue from a list of label tuples.""" - rows = [] - for row_offset in range(len(former_column_labels)): - row_label = former_column_labels[row_offset] - row_label = (row_label,) if not isinstance(row_label, tuple) else row_label - row = { - col_ids[i]: (row_label[i] if pandas.notnull(row_label[i]) else None) - for i in range(len(col_ids)) - } - row[offsets_id] = row_offset - rows.append(row) - - return ArrayValue.from_pyarrow(pa.Table.from_pylist(rows), session=self.session) - def relational_join( self, other: ArrayValue, - join_def: join_def.JoinDefinition, - ) -> ArrayValue: + conditions: typing.Tuple[typing.Tuple[str, str], ...] = (), + type: typing.Literal["inner", "outer", "left", "right", "cross"] = "inner", + ) -> typing.Tuple[ArrayValue, typing.Tuple[dict[str, str], dict[str, str]]]: join_node = nodes.JoinNode( left_child=self.node, right_child=other.node, - join=join_def, + conditions=conditions, + type=type, ) - return ArrayValue(join_node) + # Maps input ids to output ids for caller convenience + l_size = len(self.node.schema) + l_mapping = { + lcol: ocol + for lcol, ocol in zip( + self.node.schema.names, join_node.schema.names[:l_size] + ) + } + r_mapping = { + rcol: ocol + for rcol, ocol in zip( + other.node.schema.names, join_node.schema.names[l_size:] + ) + } + return ArrayValue(join_node), (l_mapping, r_mapping) def try_align_as_projection( self, diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 4db171ec70a..42b1a0aeb07 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -44,6 +44,7 @@ import bigframes.core.expression as ex import bigframes.core.expression as scalars import bigframes.core.guid as guid +import bigframes.core.identifiers import bigframes.core.join_def as join_defs import bigframes.core.ordering as ordering import bigframes.core.schema as bf_schema @@ -1050,7 +1051,6 @@ def aggregate_all_and_stack( operation: typing.Union[agg_ops.UnaryAggregateOp, agg_ops.NullaryAggregateOp], *, axis: int | str = 0, - value_col_id: str = "values", dropna: bool = True, ) -> Block: axis_n = utils.get_axis_number(axis) @@ -1080,15 +1080,18 @@ def aggregate_all_and_stack( # TODO: Allow to promote identity/total_order columns instead for better perf offset_col = guid.generate_guid() expr_with_offsets = self.expr.promote_offsets(offset_col) - stacked_expr = expr_with_offsets.unpivot( - row_labels=self.column_labels.to_list(), - index_col_ids=[guid.generate_guid()], - unpivot_columns=[(value_col_id, tuple(self.value_columns))], + stacked_expr, (_, value_col_ids, passthrough_cols,) = unpivot( + expr_with_offsets, + row_labels=self.column_labels, + unpivot_columns=[tuple(self.value_columns)], passthrough_columns=[*self.index_columns, offset_col], ) + # these corresponed to passthrough_columns provided to unpivot + index_cols = passthrough_cols[:-1] + og_offset_col = passthrough_cols[-1] index_aggregations = [ (ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.free_var(col_id)), col_id) - for col_id in [*self.index_columns] + for col_id in index_cols ] # TODO: may need add NullaryAggregation in main_aggregation # when agg add support for axis=1, needed for agg("size", axis=1) @@ -1096,17 +1099,18 @@ def aggregate_all_and_stack( operation, agg_ops.UnaryAggregateOp ), f"Expected a unary operation, but got {operation}. Please report this error and how you got here to the BigQuery DataFrames team (bit.ly/bigframes-feedback)." main_aggregation = ( - ex.UnaryAggregation(operation, ex.free_var(value_col_id)), - value_col_id, + ex.UnaryAggregation(operation, ex.free_var(value_col_ids[0])), + value_col_ids[0], ) + # Drop row identity after aggregating over it result_expr = stacked_expr.aggregate( [*index_aggregations, main_aggregation], - by_column_ids=[offset_col], + by_column_ids=[og_offset_col], dropna=dropna, - ) + ).drop_columns([og_offset_col]) return Block( - result_expr.drop_columns([offset_col]), - self.index_columns, + result_expr, + index_columns=index_cols, column_labels=[None], index_labels=self.index.names, ) @@ -1318,8 +1322,7 @@ def summarize( ], ): """Get a list of stats as a deferred block object.""" - label_col_id = guid.generate_guid() - labels = [stat.name for stat in stats] + labels = pd.Index([stat.name for stat in stats]) aggregations = [ ( ex.UnaryAggregation(stat, ex.free_var(col_id)) @@ -1331,18 +1334,17 @@ def summarize( for col_id in column_ids ] columns = [ - (col_id, tuple(f"{col_id}-{stat.name}" for stat in stats)) - for col_id in column_ids + (tuple(f"{col_id}-{stat.name}" for stat in stats)) for col_id in column_ids ] - expr = self.expr.aggregate(aggregations).unpivot( + expr, (index_cols, _, _) = unpivot( + self.expr.aggregate(aggregations), labels, unpivot_columns=tuple(columns), - index_col_ids=tuple([label_col_id]), ) return Block( expr, column_labels=self._get_labels_for_columns(column_ids), - index_columns=[label_col_id], + index_columns=index_cols, ) def calculate_pairwise_metric(self, op=agg_ops.CorrOp()): @@ -1368,23 +1370,17 @@ def calculate_pairwise_metric(self, op=agg_ops.CorrOp()): ] expr = self.expr.aggregate(aggregations) - index_col_ids = [ - guid.generate_guid() for i in range(self.column_labels.nlevels) - ] input_count = len(self.value_columns) unpivot_columns = tuple( - ( - guid.generate_guid(), - tuple(expr.column_ids[input_count * i : input_count * (i + 1)]), - ) + tuple(expr.column_ids[input_count * i : input_count * (i + 1)]) for i in range(input_count) ) labels = self._get_labels_for_columns(self.value_columns) # TODO(b/340896143): fix type error - expr = expr.unpivot( - row_labels=labels, # type: ignore - index_col_ids=index_col_ids, + expr, (index_col_ids, _, _) = unpivot( + expr, + row_labels=labels, unpivot_columns=unpivot_columns, ) @@ -1604,7 +1600,7 @@ def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: Block( expr, index_columns=self.index_columns, - column_labels=self.column_labels.insert(0, label), + column_labels=self.column_labels.insert(len(self.column_labels), label), index_labels=self._index_labels, ), result_id, @@ -1722,8 +1718,6 @@ def stack(self, how="left", levels: int = 1): col_labels, row_labels = utils.split_index(self.column_labels, levels=levels) row_labels = row_labels.drop_duplicates() - row_label_tuples = utils.index_as_tuples(row_labels) - if col_labels is None: result_index: pd.Index = pd.Index([None]) result_col_labels: Sequence[Tuple] = list([()]) @@ -1737,26 +1731,24 @@ def stack(self, how="left", levels: int = 1): result_col_labels = utils.index_as_tuples(result_index) # Get matching columns - unpivot_columns: List[Tuple[str, List[str]]] = [] + unpivot_columns: List[Tuple[Optional[str], ...]] = [] for val in result_col_labels: - col_id = guid.generate_guid("unpivot_") - input_columns, dtype = self._create_stack_column(val, row_label_tuples) - unpivot_columns.append((col_id, input_columns)) + input_columns, _ = self._create_stack_column(val, row_labels) + unpivot_columns.append(input_columns) - added_index_columns = [guid.generate_guid() for _ in range(row_labels.nlevels)] - unpivot_expr = self._expr.unpivot( - row_labels=row_label_tuples, + unpivot_expr, (added_index_columns, _, passthrough_cols) = unpivot( + self._expr, + row_labels=row_labels, passthrough_columns=self.index_columns, unpivot_columns=unpivot_columns, - index_col_ids=added_index_columns, join_side=how, ) new_index_level_names = self.column_labels.names[-levels:] if how == "left": - index_columns = [*self.index_columns, *added_index_columns] + index_columns = [*passthrough_cols, *added_index_columns] index_labels = [*self._index_labels, *new_index_level_names] else: - index_columns = [*added_index_columns, *self.index_columns] + index_columns = [*added_index_columns, *passthrough_cols] index_labels = [*new_index_level_names, *self._index_labels] return Block( @@ -1780,18 +1772,16 @@ def melt( Arguments correspond to pandas.melt arguments. """ # TODO: Implement col_level and ignore_index - unpivot_col_id = guid.generate_guid() - var_col_ids = tuple([guid.generate_guid() for _ in var_names]) - # single unpivot col - unpivot_col = (unpivot_col_id, tuple(value_vars)) - value_labels = [self.col_id_to_label[col_id] for col_id in value_vars] + value_labels: pd.Index = pd.Index( + [self.col_id_to_label[col_id] for col_id in value_vars] + ) id_labels = [self.col_id_to_label[col_id] for col_id in id_vars] - unpivot_expr = self._expr.unpivot( + unpivot_expr, (var_col_ids, unpivot_out, passthrough_cols) = unpivot( + self._expr, row_labels=value_labels, passthrough_columns=id_vars, - unpivot_columns=(unpivot_col,), - index_col_ids=var_col_ids, + unpivot_columns=(tuple(value_vars),), # single unpivot col join_side="right", ) @@ -1804,7 +1794,7 @@ def melt( # Need to reorder to get id_vars before var_col and unpivot_col unpivot_expr = unpivot_expr.select_columns( - [*index_cols, *id_vars, *var_col_ids, unpivot_col_id] + [*index_cols, *passthrough_cols, *var_col_ids, *unpivot_out] ) return Block( @@ -1859,6 +1849,7 @@ def transpose( value_vars=block.value_columns, create_offsets_index=False, ) + row_offset = stacked_block.value_columns[0] col_labels = stacked_block.value_columns[-2 - original_col_index.nlevels : -2] col_offset = stacked_block.value_columns[-2] # disambiguator we created earlier cell_values = stacked_block.value_columns[-1] @@ -1867,7 +1858,7 @@ def transpose( [*col_labels, col_offset] ) # col index is now row index result = stacked_block.pivot( - columns=[offsets], + columns=[row_offset], values=[cell_values], columns_unique_values=tuple(range(original_row_count)), ) @@ -1879,12 +1870,10 @@ def transpose( .with_transpose_cache(self) ) - def _create_stack_column( - self, col_label: typing.Tuple, stack_labels: typing.Sequence[typing.Tuple] - ): + def _create_stack_column(self, col_label: typing.Tuple, stack_labels: pd.Index): dtype = None input_columns: list[Optional[str]] = [] - for uvalue in stack_labels: + for uvalue in utils.index_as_tuples(stack_labels): label_to_match = (*col_label, *uvalue) label_to_match = ( label_to_match[0] if len(label_to_match) == 1 else label_to_match @@ -2013,38 +2002,16 @@ def merge( sort: bool, suffixes: tuple[str, str] = ("_x", "_y"), ) -> Block: - left_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.LEFT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in self.expr.column_ids - ] - right_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.RIGHT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in other.expr.column_ids - ] - - join_def = join_defs.JoinDefinition( - conditions=tuple( - join_defs.JoinCondition(left, right) - for left, right in zip(left_join_ids, right_join_ids) - ), - mappings=(*left_mappings, *right_mappings), - type=how, + conditions = tuple( + (lid, rid) for lid, rid in zip(left_join_ids, right_join_ids) + ) + joined_expr, (get_column_left, get_column_right) = self.expr.relational_join( + other.expr, type=how, conditions=conditions ) - joined_expr = self.expr.relational_join(other.expr, join_def=join_def) result_columns = [] matching_join_labels = [] coalesced_ids = [] - get_column_left = join_def.get_left_mapping() - get_column_right = join_def.get_right_mapping() for left_id, right_id in zip(left_join_ids, right_join_ids): coalesced_id = guid.generate_guid() joined_expr = joined_expr.project_to_id( @@ -2748,34 +2715,10 @@ def join_with_single_row( left_expr = left.expr # ignore index columns by dropping them right_expr = single_row_block.expr.select_columns(single_row_block.value_columns) - left_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.LEFT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in left_expr.column_ids - ] - right_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.RIGHT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in right_expr.column_ids # skip index column - ] - - join_def = join_defs.JoinDefinition( - conditions=(), - mappings=(*left_mappings, *right_mappings), - type="cross", - ) - combined_expr = left_expr.relational_join( + combined_expr, (get_column_left, get_column_right) = left_expr.relational_join( right_expr, - join_def=join_def, + type="cross", ) - get_column_left = join_def.get_left_mapping() - get_column_right = join_def.get_right_mapping() # Drop original indices from each side. and used the coalesced combination generated by the join. index_cols_post_join = [get_column_left[id] for id in left.index_columns] @@ -2800,38 +2743,15 @@ def join_mono_indexed( ) -> Tuple[Block, Tuple[Mapping[str, str], Mapping[str, str]],]: left_expr = left.expr right_expr = right.expr - left_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.LEFT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in left_expr.column_ids - ] - right_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.RIGHT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in right_expr.column_ids - ] - join_def = join_defs.JoinDefinition( + combined_expr, (get_column_left, get_column_right) = left_expr.relational_join( + right_expr, + type=how, conditions=( join_defs.JoinCondition(left.index_columns[0], right.index_columns[0]), ), - mappings=(*left_mappings, *right_mappings), - type=how, ) - combined_expr = left_expr.relational_join( - right_expr, - join_def=join_def, - ) - - get_column_left = join_def.get_left_mapping() - get_column_right = join_def.get_right_mapping() left_index = get_column_left[left.index_columns[0]] right_index = get_column_right[right.index_columns[0]] # Drop original indices from each side. and used the coalesced combination generated by the join. @@ -2886,39 +2806,15 @@ def join_multi_indexed( left_expr = left.expr right_expr = right.expr - left_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.LEFT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in left_expr.column_ids - ] - right_mappings = [ - join_defs.JoinColumnMapping( - source_table=join_defs.JoinSide.RIGHT, - source_id=id, - destination_id=guid.generate_guid(), - ) - for id in right_expr.column_ids - ] - - join_def = join_defs.JoinDefinition( + combined_expr, (get_column_left, get_column_right) = left_expr.relational_join( + right_expr, + type=how, conditions=tuple( join_defs.JoinCondition(left, right) for left, right in zip(left_join_ids, right_join_ids) ), - mappings=(*left_mappings, *right_mappings), - type=how, ) - combined_expr = left_expr.relational_join( - right_expr, - join_def=join_def, - ) - - get_column_left = join_def.get_left_mapping() - get_column_right = join_def.get_right_mapping() left_ids_post_join = [get_column_left[id] for id in left_join_ids] right_ids_post_join = [get_column_right[id] for id in right_join_ids] # Drop original indices from each side. and used the coalesced combination generated by the join. @@ -3114,3 +3010,94 @@ def _get_block_schema( for label, dtype in zip(block.column_labels, block.dtypes): result[label] = typing.cast(bigframes.dtypes.Dtype, dtype) return result + + +## Unpivot helpers +def unpivot( + array_value: core.ArrayValue, + row_labels: pd.Index, + unpivot_columns: Sequence[Tuple[Optional[str], ...]], + *, + passthrough_columns: typing.Sequence[str] = (), + join_side: Literal["left", "right"] = "left", +) -> Tuple[core.ArrayValue, Tuple[Tuple[str, ...], Tuple[str, ...], Tuple[str, ...]]]: + """ + Unpivot ArrayValue columns. + + Args: + row_labels: Identifies the source of the row. Must be equal to length to source column list in unpivot_columns argument. + unpivot_columns: Sequence of column ids tuples. Each tuple of columns will be combined into a single output column + passthrough_columns: Columns that will not be unpivoted. Column id will be preserved. + index_col_id (str): The column id to be used for the row labels. + + Returns: + ArrayValue, (index_cols, unpivot_cols, passthrough_cols): The unpivoted ArrayValue and resulting column ids. + """ + # There will be N labels, used to disambiguate which of N source columns produced each output row + labels_array = _pd_index_to_array_value( + session=array_value.session, index=row_labels + ) + + # Unpivot creates N output rows for each input row, labels disambiguate these N rows + # Join_side is necessary to produce desired row ordering + if join_side == "left": + joined_array, (column_mapping, labels_mapping) = array_value.relational_join( + labels_array, type="cross" + ) + else: + joined_array, (labels_mapping, column_mapping) = labels_array.relational_join( + array_value, type="cross" + ) + new_passthrough_cols = [column_mapping[col] for col in passthrough_columns] + # Last column is offsets + index_col_ids = [labels_mapping[col] for col in labels_array.column_ids[:-1]] + explode_offsets_id = labels_mapping[labels_array.column_ids[-1]] + + # Build the output rows as a case statment that selects between the N input columns + unpivot_exprs: List[Tuple[ex.Expression, str]] = [] + # Supports producing multiple stacked ouput columns for stacking only part of hierarchical index + for input_ids in unpivot_columns: + # row explode offset used to choose the input column + # we use offset instead of label as labels are not necessarily unique + cases = itertools.chain( + *( + ( + ops.eq_op.as_expr(explode_offsets_id, ex.const(i)), + ex.free_var(column_mapping[id_or_null]) + if (id_or_null is not None) + else ex.const(None), + ) + for i, id_or_null in enumerate(input_ids) + ) + ) + col_expr = ops.case_when_op.as_expr(*cases) + unpivot_exprs.append((col_expr, guid.generate_guid())) + + unpivot_col_ids = [id for _, id in unpivot_exprs] + + return joined_array.compute_values(unpivot_exprs).select_columns( + [*index_col_ids, *unpivot_col_ids, *new_passthrough_cols] + ), (tuple(index_col_ids), tuple(unpivot_col_ids), tuple(new_passthrough_cols)) + + +def _pd_index_to_array_value( + session: core.Session, + index: pd.Index, +) -> core.ArrayValue: + """ + Create an ArrayValue from a list of label tuples. + The last column will be row offsets. + """ + rows = [] + labels_as_tuples = utils.index_as_tuples(index) + for row_offset in range(len(index)): + id_gen = bigframes.core.identifiers.standard_identifiers() + row_label = labels_as_tuples[row_offset] + row_label = (row_label,) if not isinstance(row_label, tuple) else row_label + row = {} + for label_part, id in zip(row_label, id_gen): + row[id] = label_part if pd.notnull(label_part) else None + row[next(id_gen)] = row_offset + rows.append(row) + + return core.ArrayValue.from_pyarrow(pa.Table.from_pylist(rows), session=session) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 9a9f598e892..38b8fb50e3f 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -791,10 +791,10 @@ def promote_offsets(self, col_id: str) -> OrderedIR: if ordering.is_sequential and (ordering.total_order_col is not None): expr_builder = self.builder() expr_builder.columns = [ + *self.columns, self._compile_expression( ordering.total_order_col.scalar_expression ).name(col_id), - *self.columns, ] return expr_builder.build() # Cannot nest analytic expressions, so reproject to cte first if needed. diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 80d5f5a8932..950c2c2cc7a 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -81,7 +81,8 @@ def compile_join(self, node: nodes.JoinNode, ordered: bool = True): return bigframes.core.compile.single_column.join_by_column_ordered( left=left_ordered, right=right_ordered, - join=node.join, + type=node.type, + conditions=node.conditions, ) else: left_unordered = self.compile_unordered_ir(node.left_child) @@ -89,7 +90,8 @@ def compile_join(self, node: nodes.JoinNode, ordered: bool = True): return bigframes.core.compile.single_column.join_by_column_unordered( left=left_unordered, right=right_unordered, - join=node.join, + type=node.type, + conditions=node.conditions, ) @_compile_node.register diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index 9b621c9c79d..26af969b744 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -16,20 +16,23 @@ from __future__ import annotations +from typing import Literal, Tuple + import ibis import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types import bigframes.core.compile.compiled as compiled import bigframes.core.guid as guids -import bigframes.core.join_def as join_defs +import bigframes.core.identifiers as ids import bigframes.core.ordering as orderings def join_by_column_ordered( left: compiled.OrderedIR, right: compiled.OrderedIR, - join: join_defs.JoinDefinition, + conditions: Tuple[Tuple[str, str], ...], + type: Literal["inner", "outer", "left", "right", "cross"], ) -> compiled.OrderedIR: """Join two expressions by column equality. @@ -48,6 +51,11 @@ def join_by_column_ordered( finally, all the right columns. """ + # Do not reset the generator + id_generator = ids.standard_identifiers() + l_value_mapping = dict(zip(left.column_ids, id_generator)) + r_value_mapping = dict(zip(right.column_ids, id_generator)) + l_hidden_mapping = { id: guids.generate_guid("hidden_") for id in left._hidden_column_ids } @@ -55,8 +63,8 @@ def join_by_column_ordered( id: guids.generate_guid("hidden_") for id in right._hidden_column_ids } - l_mapping = {**join.get_left_mapping(), **l_hidden_mapping} - r_mapping = {**join.get_right_mapping(), **r_hidden_mapping} + l_mapping = {**l_value_mapping, **l_hidden_mapping} + r_mapping = {**r_value_mapping, **r_hidden_mapping} left_table = left._to_ibis_expr( ordering_mode="unordered", @@ -71,14 +79,14 @@ def join_by_column_ordered( join_conditions = [ value_to_join_key(left_table[l_mapping[left_index]]) == value_to_join_key(right_table[r_mapping[right_index]]) - for left_index, right_index in join.conditions + for left_index, right_index in conditions ] combined_table = ibis.join( left_table, right_table, predicates=join_conditions, - how=join.type, # type: ignore + how=type, # type: ignore ) # Preserve ordering accross joins. @@ -87,7 +95,7 @@ def join_by_column_ordered( right._ordering, l_mapping, r_mapping, - left_order_dominates=(join.type != "right"), + left_order_dominates=(type != "right"), ) # We could filter out the original join columns, but predicates/ordering @@ -116,7 +124,8 @@ def join_by_column_ordered( def join_by_column_unordered( left: compiled.UnorderedIR, right: compiled.UnorderedIR, - join: join_defs.JoinDefinition, + conditions: Tuple[Tuple[str, str], ...], + type: Literal["inner", "outer", "left", "right", "cross"], ) -> compiled.UnorderedIR: """Join two expressions by column equality. @@ -134,9 +143,9 @@ def join_by_column_unordered( first the coalesced join keys, then, all the left columns, and finally, all the right columns. """ - # Value column mapping must use JOIN_NAME_REMAPPER to stay in sync with consumers of join result - l_mapping = join.get_left_mapping() - r_mapping = join.get_right_mapping() + id_generator = ids.standard_identifiers() + l_mapping = dict(zip(left.column_ids, id_generator)) + r_mapping = dict(zip(right.column_ids, id_generator)) left_table = left._to_ibis_expr( col_id_overrides=l_mapping, ) @@ -146,14 +155,14 @@ def join_by_column_unordered( join_conditions = [ value_to_join_key(left_table[l_mapping[left_index]]) == value_to_join_key(right_table[r_mapping[right_index]]) - for left_index, right_index in join.conditions + for left_index, right_index in conditions ] combined_table = ibis.join( left_table, right_table, predicates=join_conditions, - how=join.type, # type: ignore + how=type, # type: ignore ) # We could filter out the original join columns, but predicates/ordering # might still reference them in implicit joins. diff --git a/bigframes/core/guid.py b/bigframes/core/guid.py index 4eb6c7a9d62..8930d0760af 100644 --- a/bigframes/core/guid.py +++ b/bigframes/core/guid.py @@ -18,4 +18,4 @@ def generate_guid(prefix="col_"): global _GUID_COUNTER _GUID_COUNTER += 1 - return prefix + str(_GUID_COUNTER) + return f"bfuid_{prefix}{_GUID_COUNTER}" diff --git a/bigframes/core/identifiers.py b/bigframes/core/identifiers.py new file mode 100644 index 00000000000..9239c41248f --- /dev/null +++ b/bigframes/core/identifiers.py @@ -0,0 +1,26 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Later, plan on migrating ids to use integers to reduce memory usage allow use of bitmaps to represent column sets + +from typing import Generator + +ID_TYPE = str + + +def standard_identifiers() -> Generator[ID_TYPE, None, None]: + i = 0 + while True: + yield f"col_{i}" + i = i + 1 diff --git a/bigframes/core/join_def.py b/bigframes/core/join_def.py index 4079abc8fa8..5b7b7e45dda 100644 --- a/bigframes/core/join_def.py +++ b/bigframes/core/join_def.py @@ -15,7 +15,9 @@ import dataclasses import enum -from typing import Literal, Mapping, NamedTuple, Tuple +from typing import Literal, NamedTuple + +import bigframes.core.identifiers as ids class JoinSide(enum.Enum): @@ -32,42 +34,21 @@ def inverse(self) -> JoinSide: class JoinCondition(NamedTuple): - left_id: str - right_id: str + left_id: ids.ID_TYPE + right_id: ids.ID_TYPE @dataclasses.dataclass(frozen=True) class JoinColumnMapping: source_table: JoinSide - source_id: str - destination_id: str + source_id: ids.ID_TYPE + destination_id: ids.ID_TYPE @dataclasses.dataclass(frozen=True) class CoalescedColumnMapping: """Special column mapping used only by implicit joiner only""" - left_source_id: str - right_source_id: str - destination_id: str - - -@dataclasses.dataclass(frozen=True) -class JoinDefinition: - conditions: Tuple[JoinCondition, ...] - mappings: Tuple[JoinColumnMapping, ...] - type: JoinType - - def get_left_mapping(self) -> Mapping[str, str]: - return { - i.source_id: i.destination_id - for i in self.mappings - if i.source_table == JoinSide.LEFT - } - - def get_right_mapping(self) -> Mapping[str, str]: - return { - i.source_id: i.destination_id - for i in self.mappings - if i.source_table == JoinSide.RIGHT - } + left_source_id: ids.ID_TYPE + right_source_id: ids.ID_TYPE + destination_id: ids.ID_TYPE diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 27e76c79104..e90ecd06b6d 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -26,7 +26,7 @@ import bigframes.core.expression as ex import bigframes.core.guid -from bigframes.core.join_def import JoinColumnMapping, JoinDefinition, JoinSide +import bigframes.core.identifiers as bfet_ids from bigframes.core.ordering import OrderingExpression import bigframes.core.schema as schemata import bigframes.core.window_spec as window @@ -206,7 +206,8 @@ def order_ambiguous(self) -> bool: class JoinNode(BigFrameNode): left_child: BigFrameNode right_child: BigFrameNode - join: JoinDefinition + conditions: typing.Tuple[typing.Tuple[str, str], ...] + type: typing.Literal["inner", "outer", "left", "right", "cross"] @property def row_preserving(self) -> bool: @@ -233,19 +234,14 @@ def __hash__(self): @functools.cached_property def schema(self) -> schemata.ArraySchema: - def join_mapping_to_schema_item(mapping: JoinColumnMapping): - result_id = mapping.destination_id - result_dtype = ( - self.left_child.schema.get_type(mapping.source_id) - if mapping.source_table == JoinSide.LEFT - else self.right_child.schema.get_type(mapping.source_id) - ) - return schemata.SchemaItem(result_id, result_dtype) - - items = tuple( - join_mapping_to_schema_item(mapping) for mapping in self.join.mappings + items = [] + schema_items = itertools.chain( + self.left_child.schema.items, self.right_child.schema.items ) - return schemata.ArraySchema(items) + identifiers = bfet_ids.standard_identifiers() + for id, item in zip(identifiers, schema_items): + items.append(schemata.SchemaItem(id, item.dtype)) + return schemata.ArraySchema(tuple(items)) @functools.cached_property def variables_introduced(self) -> int: @@ -545,7 +541,7 @@ def non_local(self) -> bool: @property def schema(self) -> schemata.ArraySchema: - return self.child.schema.prepend( + return self.child.schema.append( schemata.SchemaItem(self.col_id, bigframes.dtypes.INT_DTYPE) ) @@ -626,6 +622,10 @@ def relation_ops_created(self) -> int: class SelectionNode(UnaryNode): input_output_pairs: typing.Tuple[typing.Tuple[str, str], ...] + def __post_init__(self): + for input, _ in self.input_output_pairs: + assert input in self.child.schema.names + def __hash__(self): return self._node_hash diff --git a/bigframes/core/schema.py b/bigframes/core/schema.py index ee27c6ff301..03e4de8993c 100644 --- a/bigframes/core/schema.py +++ b/bigframes/core/schema.py @@ -92,3 +92,6 @@ def update_dtype( def get_type(self, id: ColumnIdentifierType): return self._mapping[id] + + def __len__(self) -> int: + return len(self.items) From 40113d8079446452b87b9c7e794a86f2635d1ff2 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 11 Sep 2024 14:54:44 -0700 Subject: [PATCH 822/921] refactor: Switch explode node to use column offsets (#978) --- bigframes/core/__init__.py | 8 +++++--- bigframes/core/compile/compiled.py | 11 ++++++++--- bigframes/core/nodes.py | 9 ++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index b4074dd94f9..a0563e6edd9 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -382,9 +382,8 @@ def explode(self, column_ids: typing.Sequence[str]) -> ArrayValue: for column_id in column_ids: assert bigframes.dtypes.is_array_like(self.get_column_type(column_id)) - return ArrayValue( - nodes.ExplodeNode(child=self.node, column_ids=tuple(column_ids)) - ) + offsets = tuple(self.get_offset_for_name(id) for id in column_ids) + return ArrayValue(nodes.ExplodeNode(child=self.node, column_ids=offsets)) def _uniform_sampling(self, fraction: float) -> ArrayValue: """Sampling the table on given fraction. @@ -393,3 +392,6 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: The row numbers of result is non-deterministic, avoid to use. """ return ArrayValue(nodes.RandomSampleNode(self.node, fraction)) + + def get_offset_for_name(self, name: str): + return self.schema.names.index(name) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 38b8fb50e3f..2a13ce0da0e 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -401,8 +401,9 @@ def _uniform_sampling(self, fraction: float) -> UnorderedIR: columns=columns, ) - def explode(self, column_ids: typing.Sequence[str]) -> UnorderedIR: + def explode(self, offsets: typing.Sequence[int]) -> UnorderedIR: table = self._to_ibis_expr() + column_ids = tuple(table.columns[offset] for offset in offsets) # The offset array ensures null represents empty arrays after unnesting. offset_array_id = bigframes.core.guid.generate_guid("offset_array_") @@ -712,8 +713,9 @@ def _uniform_sampling(self, fraction: float) -> OrderedIR: ordering=self._ordering, ) - def explode(self, column_ids: typing.Sequence[str]) -> OrderedIR: + def explode(self, offsets: typing.Sequence[int]) -> OrderedIR: table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) + column_ids = tuple(table.columns[offset] for offset in offsets) offset_array_id = bigframes.core.guid.generate_guid("offset_array_") offset_array = ( @@ -721,7 +723,10 @@ def explode(self, column_ids: typing.Sequence[str]) -> OrderedIR: ibis.greatest( 0, ibis.least( - *[table[column_id].length() - 1 for column_id in column_ids] + *[ + table[table.columns[offset]].length() - 1 + for offset in offsets + ] ), ) ) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index e90ecd06b6d..e11fcafff73 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -42,6 +42,9 @@ OVERHEAD_VARIABLES = 5 +COL_OFFSET = int + + @dataclass(frozen=True) class BigFrameNode: """ @@ -826,7 +829,7 @@ def variables_introduced(self) -> int: @dataclass(frozen=True) class ExplodeNode(UnaryNode): - column_ids: typing.Tuple[str, ...] + column_ids: typing.Tuple[COL_OFFSET, ...] @property def row_preserving(self) -> bool: @@ -844,9 +847,9 @@ def schema(self) -> schemata.ArraySchema: self.child.schema.get_type(name).pyarrow_dtype.value_type ), ) - if name in self.column_ids + if offset in self.column_ids else schemata.SchemaItem(name, self.child.schema.get_type(name)) - for name in self.child.schema.names + for offset, name in enumerate(self.child.schema.names) ) return schemata.ArraySchema(items) From c94ead996e3bfa98edd51ff678a3d43a10ee980f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 12 Sep 2024 09:42:47 -0700 Subject: [PATCH 823/921] chore: apply `remote_function` on the original series without reprojecting (#874) * chore: apply `remote_function` on the original series This change tests application of remote function without reprojecting the original series. * add failing mask doctest as a system test for easier debugging * more comprehensive repr tests * more tests, move to small tests * rename "name" param * manipulate copy of the original udf * move the funciton copy after i/o types resolution * rename all params to avoid collisions, widely use bigframes_ prefix for consistency --- .../functions/_remote_function_session.py | 18 +++ bigframes/functions/remote_function.py | 11 +- bigframes/series.py | 8 +- tests/system/small/test_remote_function.py | 110 ++++++++++++++++++ 4 files changed, 138 insertions(+), 9 deletions(-) diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py index 893b903aeb2..c947fcdc63f 100644 --- a/bigframes/functions/_remote_function_session.py +++ b/bigframes/functions/_remote_function_session.py @@ -22,6 +22,7 @@ from typing import Any, cast, Dict, Mapping, Optional, Sequence, TYPE_CHECKING, Union import warnings +import cloudpickle import google.api_core.exceptions from google.cloud import ( bigquery, @@ -458,6 +459,11 @@ def wrapper(func): session=session, # type: ignore ) + # To respect the user code/environment let's use a copy of the + # original udf, especially since we would be setting some properties + # on it + func = cloudpickle.loads(cloudpickle.dumps(func)) + # In the unlikely case where the user is trying to re-deploy the same # function, cleanup the attributes we add below, first. This prevents # the pickle from having dependencies that might not otherwise be @@ -499,6 +505,18 @@ def try_delattr(attr): cloud_function_memory_mib=cloud_function_memory_mib, ) + # TODO(shobs): Find a better way to support udfs with param named "name". + # This causes an issue in the ibis compilation. + func.__signature__ = inspect.signature(func).replace( # type: ignore + parameters=[ + inspect.Parameter( + f"bigframes_{param.name}", + param.kind, + ) + for param in inspect.signature(func).parameters.values() + ] + ) + # TODO: Move ibis logic to compiler step node = ibis.udf.scalar.builtin( func, diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 39e3bfd8f09..b4c74e90d6a 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -144,16 +144,21 @@ def read_gbq_function( # The name "args" conflicts with the Ibis operator, so we use # non-standard names for the arguments here. - def func(*ignored_args, **ignored_kwargs): + def func(*bigframes_args, **bigframes_kwargs): f"""Remote function {str(routine_ref)}.""" nonlocal node # type: ignore - expr = node(*ignored_args, **ignored_kwargs) # type: ignore + expr = node(*bigframes_args, **bigframes_kwargs) # type: ignore return ibis_client.execute(expr) func.__signature__ = inspect.signature(func).replace( # type: ignore parameters=[ - inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) + # TODO(shobs): Find a better way to support functions with param + # named "name". This causes an issue in the ibis compilation. + inspect.Parameter( + f"bigframes_{name}", + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) for name in ibis_signature.parameter_names ] ) diff --git a/bigframes/series.py b/bigframes/series.py index 5192a9cf496..d9e3bb19dd3 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1481,12 +1481,8 @@ def apply( ex.message += f"\n{_remote_function_recommendation_message}" raise - # We are working with remote function at this point. - # Reproject as workaround to applying filter too late. This forces the - # filter to be applied before passing data to remote function, - # protecting from bad inputs causing errors. - reprojected_series = Series(self._block._force_reproject()) - result_series = reprojected_series._apply_unary_op( + # We are working with remote function at this point + result_series = self._apply_unary_op( ops.RemoteFunctionOp(func=func, apply_on_null=True) ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index b000354ed4f..5ffda56f926 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect import re import google.api_core.exceptions @@ -972,3 +973,112 @@ def echo_len(row): bigframes.exceptions.PreviewWarning, match="axis=1 scenario is in preview." ): scalars_df[[column]].apply(echo_len_remote, axis=1) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_application_repr(session, dataset_id_permanent): + # This function deliberately has a param with name "name", this is to test + # a specific ibis' internal handling of object names + def should_mask(name: str) -> bool: + hash = 0 + for char_ in name: + hash += ord(char_) + return hash % 2 == 0 + + assert "name" in inspect.signature(should_mask).parameters + + should_mask = session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(should_mask) + )(should_mask) + + s = bigframes.series.Series(["Alice", "Bob", "Caroline"]) + + repr(s.apply(should_mask)) + repr(s.where(s.apply(should_mask))) + repr(s.where(~s.apply(should_mask))) + repr(s.mask(should_mask)) + repr(s.mask(should_mask, "REDACTED")) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_read_gbq_function_application_repr(session, dataset_id, scalars_df_index): + gbq_function = f"{dataset_id}.should_mask" + + # This function deliberately has a param with name "name", this is to test + # a specific ibis' internal handling of object names + session.bqclient.query_and_wait( + f"CREATE OR REPLACE FUNCTION `{gbq_function}`(name STRING) RETURNS BOOL AS (MOD(LENGTH(name), 2) = 1)" + ) + routine = session.bqclient.get_routine(gbq_function) + assert "name" in [arg.name for arg in routine.arguments] + + # read the function and apply to dataframe + should_mask = session.read_gbq_function(gbq_function) + + s = scalars_df_index["string_col"] + + repr(s.apply(should_mask)) + repr(s.where(s.apply(should_mask))) + repr(s.where(~s.apply(should_mask))) + repr(s.mask(should_mask)) + repr(s.mask(should_mask, "REDACTED")) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_apply_after_filter(session, dataset_id_permanent, scalars_dfs): + + # This function is deliberately written to not work with NA input + def plus_one(x: int) -> int: + return x + 1 + + scalars_df, scalars_pandas_df = scalars_dfs + int_col_name_with_nulls = "int64_col" + + # make sure there are NA values in the test column + assert any([pd.isna(val) for val in scalars_df[int_col_name_with_nulls]]) + + # create a remote function + plus_one_remote = session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(plus_one) + )(plus_one) + + # with nulls in the series the remote function application would fail + with pytest.raises( + google.api_core.exceptions.BadRequest, match="unsupported operand" + ): + scalars_df[int_col_name_with_nulls].apply(plus_one_remote).to_pandas() + + # after filtering out nulls the remote function application should works + # similar to pandas + pd_result = scalars_pandas_df[scalars_pandas_df[int_col_name_with_nulls].notnull()][ + int_col_name_with_nulls + ].apply(plus_one) + bf_result = ( + scalars_df[scalars_df[int_col_name_with_nulls].notnull()][ + int_col_name_with_nulls + ] + .apply(plus_one_remote) + .to_pandas() + ) + + # ignore pandas "int64" vs bigframes "Int64" dtype difference + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_apply_assign_partial_ordering_mode(dataset_id_permanent): + session = bigframes.Session(bigframes.BigQueryOptions(ordering_mode="partial")) + + df = session.read_gbq("bigquery-public-data.baseball.schedules")[ + ["duration_minutes"] + ] + + def plus_one(x: int) -> int: + return x + 1 + + plus_one = session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(plus_one) + )(plus_one) + + df1 = df.assign(duration_cat=df["duration_minutes"].apply(plus_one)) + repr(df1) From 42b0724fc5859614a0771bcee6cb972056543436 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 12 Sep 2024 18:41:48 -0700 Subject: [PATCH 824/921] test: disable claude3 predict tests to protect bqml capacity (#983) --- tests/system/small/ml/test_llm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index e3d2b510816..cd5d65c458a 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -439,6 +439,7 @@ def test_claude3_text_generator_create_load( assert reloaded_model.model_name == model_name +@pytest.mark.skip("b/366290533 too many requests are exhausting bqml capacity") @pytest.mark.parametrize( "model_name", ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), @@ -458,6 +459,7 @@ def test_claude3_text_generator_predict_default_params_success( ) +@pytest.mark.skip("b/366290533 too many requests are exhausting bqml capacity") @pytest.mark.parametrize( "model_name", ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), From 30e11d901e72faf02c6d594eaa03c4eb24e69b2a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 13 Sep 2024 10:58:55 -0700 Subject: [PATCH 825/921] refactor: Limit access to ArrayValue node field. (#977) --- bigframes/core/__init__.py | 15 +++++++++++++++ bigframes/core/blocks.py | 14 +++++--------- bigframes/core/validations.py | 2 +- bigframes/session/executor.py | 4 ++-- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index a0563e6edd9..aa3aa63cc2f 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -36,6 +36,7 @@ import bigframes.core.ordering as orderings import bigframes.core.rewrite import bigframes.core.schema as schemata +import bigframes.core.tree_properties import bigframes.core.utils from bigframes.core.window_spec import WindowSpec import bigframes.dtypes @@ -124,6 +125,20 @@ def schema(self) -> schemata.ArraySchema: def _compiled_schema(self) -> schemata.ArraySchema: return bigframes.core.compile.test_only_ibis_inferred_schema(self.node) + @property + def explicitly_ordered(self) -> bool: + # see BigFrameNode.explicitly_ordered + return self.node.explicitly_ordered + + @property + def order_ambiguous(self) -> bool: + # see BigFrameNode.order_ambiguous + return self.node.order_ambiguous + + @property + def supports_fast_peek(self) -> bool: + return bigframes.core.tree_properties.can_fast_peek(self.node) + def as_cached( self: ArrayValue, cache_table: google.cloud.bigquery.Table, diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 42b1a0aeb07..feeed788850 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -49,7 +49,6 @@ import bigframes.core.ordering as ordering import bigframes.core.schema as bf_schema import bigframes.core.sql as sql -import bigframes.core.tree_properties as tree_properties import bigframes.core.utils as utils import bigframes.core.window_spec as window_specs import bigframes.dtypes @@ -205,7 +204,7 @@ def shape(self) -> typing.Tuple[int, int]: row_count_expr = self.expr.row_count() # Support in-memory engines for hermetic unit tests. - if self.expr.node.session is None: + if self.expr.session is None: try: row_count = row_count_expr._try_evaluate_local().squeeze() return (row_count, len(self.value_columns)) @@ -283,7 +282,7 @@ def index_name_to_col_id(self) -> typing.Mapping[Label, typing.Sequence[str]]: @property def explicitly_ordered(self) -> bool: - return self.expr.node.explicitly_ordered + return self.expr.explicitly_ordered def cols_matching_label(self, partial_label: Label) -> typing.Sequence[str]: """ @@ -466,7 +465,7 @@ def _validate_result_schema( ): actual_schema = tuple(bq_result_schema) ibis_schema = self.expr._compiled_schema - internal_schema = self.expr.node.schema + internal_schema = self.expr.schema if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: return if internal_schema.to_bigquery() != actual_schema: @@ -561,7 +560,7 @@ def to_pandas( def try_peek( self, n: int = 20, force: bool = False ) -> typing.Optional[pd.DataFrame]: - if force or tree_properties.can_fast_peek(self.expr.node): + if force or self.expr.supports_fast_peek: iterator, _ = self.session._peek(self.expr, n) df = self._to_dataframe(iterator) self._copy_index_to_pandas(df) @@ -2365,10 +2364,7 @@ def cached(self, *, force: bool = False, session_aware: bool = False) -> None: if (not force) and self.session._executor._is_trivially_executable(self.expr): return elif session_aware: - bfet_roots = [obj._block._expr.node for obj in self.session.objects] - self.session._executor._cache_with_session_awareness( - self.expr, session_forest=bfet_roots - ) + self.session._executor._cache_with_session_awareness(self.expr) else: self.session._executor._cache_with_cluster_cols( self.expr, cluster_cols=self.index_columns diff --git a/bigframes/core/validations.py b/bigframes/core/validations.py index daa12528243..ca65445dabb 100644 --- a/bigframes/core/validations.py +++ b/bigframes/core/validations.py @@ -66,7 +66,7 @@ def enforce_ordered( object: HasSession, opname: str, suggestion: Optional[str] = None ) -> None: session = object._session - if session._strictly_ordered or not object._block.expr.node.order_ambiguous: + if session._strictly_ordered or not object._block.expr.order_ambiguous: # No ambiguity for how to calculate ordering, so no error or warning return None if not session._allows_ambiguity: diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 424e6d7dad6..0512fa78e34 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -15,7 +15,7 @@ from __future__ import annotations import math -from typing import cast, Iterable, Literal, Mapping, Optional, Sequence, Tuple, Union +from typing import cast, Literal, Mapping, Optional, Sequence, Tuple, Union import warnings import weakref @@ -381,8 +381,8 @@ def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): def _cache_with_session_awareness( self, array_value: bigframes.core.ArrayValue, - session_forest: Iterable[nodes.BigFrameNode], ) -> None: + session_forest = [obj._block._expr.node for obj in array_value.session.objects] # These node types are cheap to re-compute target, cluster_cols = bigframes.session.planner.session_aware_cache_plan( array_value.node, list(session_forest) From 0f047b4fae2a10b2a465c506bea561f8bb8d4262 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 13 Sep 2024 13:03:34 -0700 Subject: [PATCH 826/921] fix: DataFrameGroupby.agg now works with unnamed tuples (#985) --- bigframes/core/groupby/__init__.py | 10 ++++----- tests/system/small/test_groupby.py | 35 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 2b80d0389e0..a0105f4ef00 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -414,12 +414,10 @@ def _agg_named(self, **kwargs) -> df.DataFrame: raise NotImplementedError( f"Only string aggregate names supported. {constants.FEEDBACK_LINK}" ) - if not hasattr(v, "column") or not hasattr(v, "aggfunc"): - import bigframes.pandas as bpd - - raise TypeError(f"kwargs values must be {bpd.NamedAgg.__qualname__}") - col_id = self._resolve_label(v.column) - aggregations.append((col_id, agg_ops.lookup_agg_func(v.aggfunc))) + if not isinstance(v, tuple) or (len(v) != 2): + raise TypeError("kwargs values must be 2-tuples of column, aggfunc") + col_id = self._resolve_label(v[0]) + aggregations.append((col_id, agg_ops.lookup_agg_func(v[1]))) column_labels.append(k) agg_block, _ = self._block.aggregate( by_column_ids=self._by_col_ids, diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 8e3baff4c27..8574860daad 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -247,6 +247,41 @@ def test_dataframe_groupby_agg_named(scalars_df_index, scalars_pandas_df_index): pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) +def test_dataframe_groupby_agg_kw_tuples(scalars_df_index, scalars_pandas_df_index): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + bf_result = ( + scalars_df_index[col_names] + .groupby("string_col") + .agg( + agg1=("int64_too", "sum"), + agg2=("float64_col", "max"), + ) + ) + pd_result = ( + scalars_pandas_df_index[col_names] + .groupby("string_col") + .agg(agg1=("int64_too", "sum"), agg2=("float64_col", "max")) + ) + bf_result_computed = bf_result.to_pandas() + + pd.testing.assert_frame_equal(pd_result, bf_result_computed, check_dtype=False) + + +@pytest.mark.parametrize( + ("kwargs"), + [ + ({"hello": "world"}), + ({"too_many_fields": ("one", "two", "three")}), + ], +) +def test_dataframe_groupby_agg_kw_error(scalars_df_index, kwargs): + col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] + with pytest.raises( + TypeError, match=r"kwargs values must be 2-tuples of column, aggfunc" + ): + (scalars_df_index[col_names].groupby("string_col").agg(**kwargs)) + + @pytest.mark.parametrize( ("as_index"), [ From 06c3120d5111cf9ecde4decc6af436cc3487ae23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 13 Sep 2024 15:05:23 -0500 Subject: [PATCH 827/921] refactor: remove circular dependencies preventing local doctest runs (#987) With this change I can once again run ``` pytest --doctest-modules third_party/bigframes_vendored/pandas/core/frame.py ``` Note: having multiple `version.py` files should be fine. release-please will update all such files it finds. --- bigframes/bigquery/__init__.py | 3 ++- bigframes/constants.py | 8 -------- bigframes/core/block_transforms.py | 5 +++-- bigframes/core/blocks.py | 2 +- bigframes/core/compile/aggregate_compiler.py | 2 +- bigframes/core/compile/ibis_types.py | 2 +- bigframes/core/compile/scalar_op_compiler.py | 2 +- bigframes/core/groupby/__init__.py | 2 +- bigframes/core/indexers.py | 2 +- bigframes/core/indexes/base.py | 2 +- bigframes/core/reshape/__init__.py | 2 +- bigframes/core/tools/datetimes.py | 2 +- bigframes/core/validations.py | 7 ++++--- bigframes/dataframe.py | 4 ++-- bigframes/dtypes.py | 3 +-- bigframes/formatting_helpers.py | 3 +-- bigframes/functions/_remote_function_client.py | 2 +- bigframes/functions/_remote_function_session.py | 3 ++- bigframes/functions/remote_function.py | 2 +- bigframes/ml/compose.py | 2 +- bigframes/ml/linear_model.py | 2 +- bigframes/ml/llm.py | 3 ++- bigframes/ml/loader.py | 2 +- bigframes/ml/metrics/_metrics.py | 2 +- bigframes/ml/pipeline.py | 2 +- bigframes/ml/sql.py | 3 +-- bigframes/ml/utils.py | 2 +- bigframes/operations/_matplotlib/core.py | 2 +- bigframes/operations/_matplotlib/hist.py | 2 +- bigframes/operations/base.py | 2 +- bigframes/operations/plotting.py | 2 +- bigframes/operations/strings.py | 2 +- bigframes/pandas/__init__.py | 2 +- bigframes/series.py | 2 +- bigframes/session/__init__.py | 2 +- bigframes/session/_io/bigquery/read_gbq_table.py | 4 ++-- bigframes/session/_io/pandas.py | 4 ++-- bigframes/session/loader.py | 6 ++++-- tests/unit/test_constants.py | 9 ++++++--- tests/unit/test_formatting_helpers.py | 5 +++-- third_party/bigframes_vendored/constants.py | 6 ++---- third_party/bigframes_vendored/version.py | 15 +++++++++++++++ 42 files changed, 77 insertions(+), 64 deletions(-) create mode 100644 third_party/bigframes_vendored/version.py diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 1e8e8d578d7..303120b88a7 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -23,7 +23,8 @@ import typing from typing import Literal, Optional, Union -import bigframes.constants as constants +import bigframes_vendored.constants as constants + import bigframes.core.groupby as groupby import bigframes.core.sql import bigframes.ml.utils as utils diff --git a/bigframes/constants.py b/bigframes/constants.py index d6fe6997135..4d5b6b8eb3d 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -19,14 +19,6 @@ import datetime -import bigframes_vendored.constants - -BF_VERSION = bigframes_vendored.constants.BF_VERSION -FEEDBACK_LINK = bigframes_vendored.constants.FEEDBACK_LINK -ABSTRACT_METHOD_ERROR_MESSAGE = ( - bigframes_vendored.constants.ABSTRACT_METHOD_ERROR_MESSAGE -) - DEFAULT_EXPIRATION = datetime.timedelta(days=7) # https://cloud.google.com/bigquery/docs/locations diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index eaee2e2cc06..eaac0dc7855 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -17,9 +17,10 @@ import typing from typing import Sequence +import bigframes_vendored.constants as constants import pandas as pd -import bigframes.constants as constants +import bigframes.constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.expression as ex @@ -117,7 +118,7 @@ def quantile( ) quantile_cols = [] labels = [] - if len(columns) * len(qs) > constants.MAX_COLUMNS: + if len(columns) * len(qs) > bigframes.constants.MAX_COLUMNS: raise NotImplementedError("Too many aggregates requested.") for col in columns: for q in qs: diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index feeed788850..3e97b1cb5e5 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -32,13 +32,13 @@ from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple, Union import warnings +import bigframes_vendored.constants as constants import google.cloud.bigquery as bigquery import pandas as pd import pyarrow as pa import bigframes._config.sampling_options as sampling_options import bigframes.constants -import bigframes.constants as constants import bigframes.core as core import bigframes.core.compile.googlesql as googlesql import bigframes.core.expression as ex diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 58973b10eb6..488acd63db9 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -15,13 +15,13 @@ import typing from typing import cast, Optional +import bigframes_vendored.constants as constants import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types import pandas as pd -import bigframes.constants as constants import bigframes.core.compile.ibis_types as compile_ibis_types import bigframes.core.compile.scalar_op_compiler as scalar_compilers import bigframes.core.expression as ex diff --git a/bigframes/core/compile/ibis_types.py b/bigframes/core/compile/ibis_types.py index f4ec295d5f7..77bfb844251 100644 --- a/bigframes/core/compile/ibis_types.py +++ b/bigframes/core/compile/ibis_types.py @@ -17,6 +17,7 @@ from typing import Any, cast, Dict, Iterable, Optional, Tuple, Union import warnings +import bigframes_vendored.constants as constants import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import geopandas as gpd # type: ignore @@ -29,7 +30,6 @@ import pandas as pd import pyarrow as pa -import bigframes.constants as constants import bigframes.dtypes # Type hints for Ibis data types supported by BigQuery DataFrame diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 9e18b391d6f..c0f12865d66 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -17,6 +17,7 @@ import functools import typing +import bigframes_vendored.constants as constants import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis import ibis.common.exceptions @@ -26,7 +27,6 @@ import numpy as np import pandas as pd -import bigframes.constants as constants import bigframes.core.compile.ibis_types import bigframes.core.expression as ex import bigframes.dtypes diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index a0105f4ef00..eda517964db 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -17,10 +17,10 @@ import typing from typing import Sequence, Union +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby import pandas as pd -import bigframes.constants as constants from bigframes.core import log_adapter import bigframes.core as core import bigframes.core.block_transforms as block_ops diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index dae5eada700..06d9c4bbabe 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -17,10 +17,10 @@ import typing from typing import Tuple, Union +import bigframes_vendored.constants as constants import ibis import pandas as pd -import bigframes.constants as constants import bigframes.core.blocks import bigframes.core.expression as ex import bigframes.core.guid as guid diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 0376e37f960..017702b85aa 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -19,12 +19,12 @@ import typing from typing import Hashable, Optional, Sequence, Union +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.indexes.base as vendored_pandas_index import google.cloud.bigquery as bigquery import numpy as np import pandas -import bigframes.constants as constants import bigframes.core.block_transforms as block_ops import bigframes.core.blocks as blocks import bigframes.core.expression as ex diff --git a/bigframes/core/reshape/__init__.py b/bigframes/core/reshape/__init__.py index a23461bdb94..49ecedcc874 100644 --- a/bigframes/core/reshape/__init__.py +++ b/bigframes/core/reshape/__init__.py @@ -16,9 +16,9 @@ import typing from typing import Iterable, Literal, Optional, Union +import bigframes_vendored.constants as constants import pandas as pd -import bigframes.constants as constants import bigframes.core.expression as ex import bigframes.core.ordering as order import bigframes.core.utils as utils diff --git a/bigframes/core/tools/datetimes.py b/bigframes/core/tools/datetimes.py index 5d8d8c96854..2abb86a2f37 100644 --- a/bigframes/core/tools/datetimes.py +++ b/bigframes/core/tools/datetimes.py @@ -16,10 +16,10 @@ from datetime import datetime from typing import Optional, Union +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes import pandas as pd -import bigframes.constants as constants import bigframes.dataframe import bigframes.dtypes import bigframes.operations as ops diff --git a/bigframes/core/validations.py b/bigframes/core/validations.py index ca65445dabb..701752c9fc1 100644 --- a/bigframes/core/validations.py +++ b/bigframes/core/validations.py @@ -19,7 +19,8 @@ import functools from typing import Optional, Protocol, TYPE_CHECKING, Union -import bigframes.constants +import bigframes_vendored.constants as constants + import bigframes.exceptions if TYPE_CHECKING: @@ -72,9 +73,9 @@ def enforce_ordered( if not session._allows_ambiguity: suggestion_substr = suggestion + " " if suggestion else "" raise bigframes.exceptions.OrderRequiredError( - f"Op {opname} not supported when strict ordering is disabled. {suggestion_substr}{bigframes.constants.FEEDBACK_LINK}" + f"Op {opname} not supported when strict ordering is disabled. {suggestion_substr}{constants.FEEDBACK_LINK}" ) if not object._block.explicitly_ordered: raise bigframes.exceptions.OrderRequiredError( - f"Op {opname} requires an ordering. Use .sort_values or .sort_index to provide an ordering. {bigframes.constants.FEEDBACK_LINK}" + f"Op {opname} requires an ordering. Use .sort_values or .sort_index to provide an ordering. {constants.FEEDBACK_LINK}" ) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 2ae6aefe1bb..d7bdd5529bd 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -36,6 +36,7 @@ ) import warnings +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.frame as vendored_pandas_frame import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing import google.api_core.exceptions @@ -49,7 +50,6 @@ import bigframes import bigframes._config.display_options as display_options import bigframes.constants -import bigframes.constants as constants import bigframes.core from bigframes.core import log_adapter import bigframes.core.block_transforms as block_ops @@ -3106,7 +3106,7 @@ def to_gbq( self._session.bqclient, temp_table_ref, datetime.datetime.now(datetime.timezone.utc) - + constants.DEFAULT_EXPIRATION, + + bigframes.constants.DEFAULT_EXPIRATION, ) if len(labels) != 0: diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index bfed783e1ea..3cd2507231a 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -20,14 +20,13 @@ import typing from typing import Dict, Literal, Union +import bigframes_vendored.constants as constants import geopandas as gpd # type: ignore import google.cloud.bigquery import numpy as np import pandas as pd import pyarrow as pa -import bigframes.constants as constants - # Type hints for Pandas dtypes supported by BigQuery DataFrame Dtype = Union[ pd.BooleanDtype, diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index 752aeb7a10b..de0ae8cc684 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -19,6 +19,7 @@ import random from typing import Any, Optional, Union +import bigframes_vendored.constants as constants import google.api_core.exceptions as api_core_exceptions import google.cloud.bigquery as bigquery import humanize @@ -26,8 +27,6 @@ import IPython.display as display import ipywidgets as widgets -import bigframes.constants as constants - GenericJob = Union[ bigquery.LoadJob, bigquery.ExtractJob, bigquery.QueryJob, bigquery.CopyJob ] diff --git a/bigframes/functions/_remote_function_client.py b/bigframes/functions/_remote_function_client.py index 3698bda28b6..75385f11a58 100644 --- a/bigframes/functions/_remote_function_client.py +++ b/bigframes/functions/_remote_function_client.py @@ -25,9 +25,9 @@ import tempfile from typing import cast, Tuple, TYPE_CHECKING +from bigframes_vendored import constants import requests -from bigframes import constants import bigframes.functions.remote_function_template if TYPE_CHECKING: diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py index c947fcdc63f..05109801787 100644 --- a/bigframes/functions/_remote_function_session.py +++ b/bigframes/functions/_remote_function_session.py @@ -22,6 +22,7 @@ from typing import Any, cast, Dict, Mapping, Optional, Sequence, TYPE_CHECKING, Union import warnings +import bigframes_vendored.constants as constants import cloudpickle import google.api_core.exceptions from google.cloud import ( @@ -31,7 +32,7 @@ resourcemanager_v3, ) -from bigframes import clients, constants +from bigframes import clients if TYPE_CHECKING: from bigframes.session import Session diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index b4c74e90d6a..4dc6c1ad6b1 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -24,12 +24,12 @@ if TYPE_CHECKING: from bigframes.session import Session +import bigframes_vendored.constants as constants import google.api_core.exceptions import google.api_core.retry from google.cloud import bigquery import google.iam.v1 -import bigframes.constants as constants import bigframes.core.compile.ibis_types import bigframes.dtypes import bigframes.functions.remote_function_template diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 4ea63d2e817..3cfa1851f58 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -23,10 +23,10 @@ import typing from typing import cast, Iterable, List, Optional, Set, Tuple, Union +from bigframes_vendored import constants import bigframes_vendored.sklearn.compose._column_transformer from google.cloud import bigquery -from bigframes import constants from bigframes.core import log_adapter from bigframes.ml import base, core, globals, impute, preprocessing, utils import bigframes.pandas as bpd diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 0816ef9b24e..8fe1d6ec272 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -19,12 +19,12 @@ from typing import Dict, List, Literal, Optional, Union +import bigframes_vendored.constants as constants import bigframes_vendored.sklearn.linear_model._base import bigframes_vendored.sklearn.linear_model._logistic from google.cloud import bigquery import bigframes -import bigframes.constants as constants from bigframes.core import log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index a3cd065a55a..53a9d40c6e5 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -19,10 +19,11 @@ from typing import cast, Literal, Optional, Union import warnings +import bigframes_vendored.constants as constants from google.cloud import bigquery import bigframes -from bigframes import clients, constants +from bigframes import clients from bigframes.core import blocks, log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd diff --git a/bigframes/ml/loader.py b/bigframes/ml/loader.py index 4e7e808260f..de9681660e0 100644 --- a/bigframes/ml/loader.py +++ b/bigframes/ml/loader.py @@ -17,10 +17,10 @@ from types import MappingProxyType from typing import Union +import bigframes_vendored.constants as constants from google.cloud import bigquery import bigframes -import bigframes.constants as constants from bigframes.ml import ( cluster, compose, diff --git a/bigframes/ml/metrics/_metrics.py b/bigframes/ml/metrics/_metrics.py index a40c175000a..3c2d6514ae9 100644 --- a/bigframes/ml/metrics/_metrics.py +++ b/bigframes/ml/metrics/_metrics.py @@ -19,6 +19,7 @@ import typing from typing import Tuple, Union +import bigframes_vendored.constants as constants import bigframes_vendored.sklearn.metrics._classification as vendored_metrics_classification import bigframes_vendored.sklearn.metrics._ranking as vendored_metrics_ranking import bigframes_vendored.sklearn.metrics._regression as vendored_metrics_regression @@ -26,7 +27,6 @@ import pandas as pd import sklearn.metrics as sklearn_metrics # type: ignore -import bigframes.constants as constants from bigframes.ml import utils import bigframes.pandas as bpd diff --git a/bigframes/ml/pipeline.py b/bigframes/ml/pipeline.py index 4cd60c5836a..dc3bd1f3f40 100644 --- a/bigframes/ml/pipeline.py +++ b/bigframes/ml/pipeline.py @@ -20,11 +20,11 @@ from typing import List, Optional, Tuple, Union +import bigframes_vendored.constants as constants import bigframes_vendored.sklearn.pipeline from google.cloud import bigquery import bigframes -import bigframes.constants as constants from bigframes.core import log_adapter from bigframes.ml import ( base, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index d14627f590f..7120a5a5fdb 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -18,10 +18,9 @@ from typing import Iterable, Literal, Mapping, Optional, Union +import bigframes_vendored.constants as constants import google.cloud.bigquery -import bigframes.constants as constants - # TODO: Add proper escaping logic from core/compile module class BaseSqlGenerator: diff --git a/bigframes/ml/utils.py b/bigframes/ml/utils.py index 75dfb916f6e..d754b1d0021 100644 --- a/bigframes/ml/utils.py +++ b/bigframes/ml/utils.py @@ -15,9 +15,9 @@ import typing from typing import Any, Iterable, Literal, Mapping, Optional, Union +import bigframes_vendored.constants as constants from google.cloud import bigquery -import bigframes.constants as constants from bigframes.core import blocks import bigframes.pandas as bpd diff --git a/bigframes/operations/_matplotlib/core.py b/bigframes/operations/_matplotlib/core.py index ff8dd86cffe..9e59e09877d 100644 --- a/bigframes/operations/_matplotlib/core.py +++ b/bigframes/operations/_matplotlib/core.py @@ -15,9 +15,9 @@ import abc import typing +import bigframes_vendored.constants as constants import pandas as pd -import bigframes.constants as constants import bigframes.dtypes as dtypes DEFAULT_SAMPLING_N = 1000 diff --git a/bigframes/operations/_matplotlib/hist.py b/bigframes/operations/_matplotlib/hist.py index 720b94d7da3..213e2abd775 100644 --- a/bigframes/operations/_matplotlib/hist.py +++ b/bigframes/operations/_matplotlib/hist.py @@ -15,10 +15,10 @@ import itertools from typing import Literal +import bigframes_vendored.constants as constants import numpy as np import pandas as pd -import bigframes.constants as constants import bigframes.operations._matplotlib.core as bfplt diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 2f870454157..68f46badedd 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -17,10 +17,10 @@ import typing from typing import List, Sequence +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing import pandas as pd -import bigframes.constants as constants import bigframes.core.blocks as blocks import bigframes.core.convert import bigframes.core.expression as ex diff --git a/bigframes/operations/plotting.py b/bigframes/operations/plotting.py index ff748069937..a45b825354f 100644 --- a/bigframes/operations/plotting.py +++ b/bigframes/operations/plotting.py @@ -14,9 +14,9 @@ import typing +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.plotting._core as vendordt -import bigframes.constants as constants import bigframes.operations._matplotlib as bfplt diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 4af142e0d51..2e401159853 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -17,9 +17,9 @@ import re from typing import cast, Literal, Optional, Union +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.strings.accessor as vendorstr -import bigframes.constants as constants from bigframes.core import log_adapter import bigframes.dataframe as df import bigframes.operations as ops diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 3809384c954..94ea6becaba 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -36,6 +36,7 @@ Union, ) +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat import bigframes_vendored.pandas.core.reshape.encoding as vendored_pandas_encoding import bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge @@ -53,7 +54,6 @@ ) import bigframes._config as config -import bigframes.constants as constants import bigframes.core.blocks import bigframes.core.expression as ex import bigframes.core.global_session as global_session diff --git a/bigframes/series.py b/bigframes/series.py index d9e3bb19dd3..d4cb1d3700f 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -24,6 +24,7 @@ import typing from typing import Any, cast, Literal, Mapping, Optional, Sequence, Tuple, Union +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.series as vendored_pandas_series import google.cloud.bigquery as bigquery import numpy @@ -31,7 +32,6 @@ import pandas.core.dtypes.common import typing_extensions -import bigframes.constants as constants import bigframes.core from bigframes.core import log_adapter import bigframes.core.block_transforms as block_ops diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 045483bd53b..7d0cfaee5cf 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -37,6 +37,7 @@ import warnings import weakref +import bigframes_vendored.constants as constants import bigframes_vendored.ibis.backends.bigquery # noqa import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import bigframes_vendored.pandas.io.parquet as third_party_pandas_parquet @@ -58,7 +59,6 @@ import bigframes._config.bigquery_options as bigquery_options import bigframes.clients -import bigframes.constants as constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.compile diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 03b26f94606..7585dd3f452 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -23,12 +23,12 @@ from typing import Dict, Iterable, List, Optional, Sequence, Tuple import warnings +import bigframes_vendored.constants as constants import google.api_core.exceptions import google.cloud.bigquery as bigquery import bigframes import bigframes.clients -import bigframes.constants import bigframes.core.compile import bigframes.core.compile.default_ordering import bigframes.core.sql @@ -241,7 +241,7 @@ def get_index_cols( # test, as it's not possible to subclass enums in Python. See: # https://stackoverflow.com/a/33680021/101923 raise NotImplementedError( - f"Got unexpected index_col {repr(index_col)}. {bigframes.constants.FEEDBACK_LINK}" + f"Got unexpected index_col {repr(index_col)}. {constants.FEEDBACK_LINK}" ) elif isinstance(index_col, str): index_cols: List[str] = [index_col] diff --git a/bigframes/session/_io/pandas.py b/bigframes/session/_io/pandas.py index 789426a6e31..83e30fd9003 100644 --- a/bigframes/session/_io/pandas.py +++ b/bigframes/session/_io/pandas.py @@ -14,6 +14,7 @@ from typing import Dict, Union +import bigframes_vendored.constants as constants import geopandas # type: ignore import pandas import pandas.arrays @@ -21,7 +22,6 @@ import pyarrow.compute # type: ignore import pyarrow.types # type: ignore -import bigframes.constants import bigframes.features @@ -54,7 +54,7 @@ def arrow_to_pandas( if len(dtypes) != arrow_table.num_columns: raise ValueError( f"Number of types {len(dtypes)} doesn't match number of columns " - f"{arrow_table.num_columns}. {bigframes.constants.FEEDBACK_LINK}" + f"{arrow_table.num_columns}. {constants.FEEDBACK_LINK}" ) serieses = {} diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index 924fddce12c..ce9874e35f8 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -22,6 +22,7 @@ import typing from typing import Dict, Hashable, IO, Iterable, List, Optional, Sequence, Tuple, Union +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions import google.auth.credentials @@ -36,7 +37,7 @@ import pandas import bigframes.clients -import bigframes.constants as constants +import bigframes.constants import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.compile @@ -444,7 +445,8 @@ def _read_bigquery_load_job( # hours of the anonymous dataset. table_expiration = bigquery.Table(table_id) table_expiration.expires = ( - datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION + datetime.datetime.now(datetime.timezone.utc) + + bigframes.constants.DEFAULT_EXPIRATION ) self._bqclient.update_table(table_expiration, ["expires"]) diff --git a/tests/unit/test_constants.py b/tests/unit/test_constants.py index aabc09c388e..4e11419077f 100644 --- a/tests/unit/test_constants.py +++ b/tests/unit/test_constants.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import bigframes.constants as constants +import bigframes_vendored.constants + +import bigframes.version def test_feedback_link_includes_version(): - assert len(constants.BF_VERSION) > 0 - assert constants.BF_VERSION in constants.FEEDBACK_LINK + version = bigframes.version.__version__ + assert len(version) > 0 + assert version in bigframes_vendored.constants.FEEDBACK_LINK diff --git a/tests/unit/test_formatting_helpers.py b/tests/unit/test_formatting_helpers.py index 3c966752c9b..d4fe0394843 100644 --- a/tests/unit/test_formatting_helpers.py +++ b/tests/unit/test_formatting_helpers.py @@ -14,12 +14,13 @@ import unittest.mock as mock +import bigframes_vendored.constants as constants import google.api_core.exceptions as api_core_exceptions import google.cloud.bigquery as bigquery import pytest -import bigframes.constants as constants import bigframes.formatting_helpers as formatting_helpers +import bigframes.version def test_wait_for_query_job_error_includes_feedback_link(): @@ -54,4 +55,4 @@ def test_wait_for_job_error_includes_version(): formatting_helpers.wait_for_job(mock_job) cap_exc.match("Test message 123.") - cap_exc.match(constants.BF_VERSION) + cap_exc.match(bigframes.version.__version__) diff --git a/third_party/bigframes_vendored/constants.py b/third_party/bigframes_vendored/constants.py index 91084b38f9c..1effdffcbe2 100644 --- a/third_party/bigframes_vendored/constants.py +++ b/third_party/bigframes_vendored/constants.py @@ -16,14 +16,12 @@ This module should not depend on any others in the package. """ -import bigframes.version - -BF_VERSION = bigframes.version.__version__ +import bigframes_vendored.version FEEDBACK_LINK = ( "Share your usecase with the BigQuery DataFrames team at the " "https://bit.ly/bigframes-feedback survey." - f"You are currently running BigFrames version {BF_VERSION}" + f"You are currently running BigFrames version {bigframes_vendored.version.__version__}" ) ABSTRACT_METHOD_ERROR_MESSAGE = ( diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py new file mode 100644 index 00000000000..2c0c6e4d3ac --- /dev/null +++ b/third_party/bigframes_vendored/version.py @@ -0,0 +1,15 @@ +# Copyright 2022 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. + +__version__ = "1.17.0" From 89ea44fb66314b134fc0a10d816c1659978d4182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 13 Sep 2024 17:22:12 -0500 Subject: [PATCH 828/921] deps: update to ibis-framework 9.x and newer sqlglot (#827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deps: update to ibis-framework 9.x and newer sqlglot * update sqlglot and ibis * bump minimum pandas * bump pyarrow * fix bfill and ffill * nearly implement describe * remove remaining reference to vendored_ibis_ops.ApproximateMultiQuantile * support ToJsonString * partial support for quantile * fix inmemorytable * fixed Series.explode * nearly fix to_datetime * remove tests I added * patch for python 3.9 support * fix unit tests * fix explode with time type * fix array_agg * fix array_agg for asc order * actually fix array_agg * fix remote function * fix in-memory nullable integer compilation * fix test_df_construct_pandas_default on Python 3.9 * fix ShiftOp windows * fix inf to SQL by treating values as literal in in memory table * fix unit tests for ibis-framework 9.2.0 * fix Python 3.10 unit tests by syncing deps * fixing remote function after merge * fix visit_NonNullLiteral for int types * visit_WindowFunction to fix s.median() method * fix lint * fix s.diff with window * fix mypy * patch visit_And to fix is_monotonic methods * fix mypy and fillna warning * undo window changes for test_series_autocorr * undo fill_null because it was missed at 9.0 version * vendor more of ibis for python 3.9 compatibility * add default arg for nulls_first for python 3.9 support * restore integer conversion * fix window tests: diff, duplicated, shift * fixing ibis parenthesize_inputs bugs and related tests * fixing lint * disable test_query_complexity_error * fix doctest np.int64(0) upgrades * fix doctest np.int64(0) upgrades more * fix groupby diff * addressing system-3.12/doctest issues related to numpy 2.1.1 * fix test_df_apply_axis_1_complex * address compiler errors after merge * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix unit-test compile errors * remove unused ibis codes * fix fillna deprecated warning * add _remove_null_ordering_from_unsupported_window back to fix test_precision_score etc ml tests * fix is_monotonic_decreasing test * fix explode after merge * fix numpy on remote function test * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * ml numpy sql generations --------- Co-authored-by: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Co-authored-by: Chelsea Lin Co-authored-by: Owl Bot --- bigframes/core/block_transforms.py | 7 +- bigframes/core/blocks.py | 15 +- bigframes/core/compile/aggregate_compiler.py | 38 +- bigframes/core/compile/compiled.py | 55 +- bigframes/core/compile/default_ordering.py | 7 +- bigframes/core/compile/scalar_op_compiler.py | 17 +- bigframes/core/compile/single_column.py | 6 +- bigframes/core/groupby/__init__.py | 13 +- bigframes/core/window_spec.py | 1 - bigframes/dataframe.py | 34 +- .../functions/_remote_function_session.py | 3 +- bigframes/functions/remote_function.py | 3 +- bigframes/ml/preprocessing.py | 1 + bigframes/ml/sql.py | 7 +- bigframes/operations/__init__.py | 4 +- bigframes/series.py | 48 +- noxfile.py | 3 +- setup.py | 4 +- testing/constraints-3.11.txt | 3 + testing/constraints-3.12.txt | 3 + testing/constraints-3.9.txt | 4 +- tests/system/conftest.py | 4 +- tests/system/large/test_remote_function.py | 6 +- tests/system/small/test_dataframe.py | 3 + tests/system/small/test_ibis.py | 44 - tests/system/small/test_numpy.py | 7 + tests/system/small/test_series.py | 9 +- .../ibis/backends/bigquery/__init__.py | 3 - .../ibis/backends/bigquery/backend.py | 85 +- .../ibis/backends/bigquery/compiler.py | 59 -- .../ibis/backends/bigquery/registry.py | 72 -- .../ibis/backends/sql/compilers/base.py | 22 +- .../sql/compilers/bigquery/__init__.py | 221 ++--- .../ibis/backends/sql/rewrites.py | 779 +++++++++++------- .../ibis/expr/operations/__init__.py | 1 - .../ibis/expr/operations/arrays.py | 18 - .../ibis/expr/operations/json.py | 2 + .../ibis/expr/operations/reductions.py | 13 +- .../bigframes_vendored/pandas/core/frame.py | 14 +- .../bigframes_vendored/pandas/core/series.py | 60 +- .../sklearn/metrics/_classification.py | 4 +- .../sklearn/metrics/_ranking.py | 8 +- .../sklearn/metrics/_regression.py | 4 +- 43 files changed, 813 insertions(+), 901 deletions(-) delete mode 100644 tests/system/small/test_ibis.py delete mode 100644 third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py delete mode 100644 third_party/bigframes_vendored/ibis/backends/bigquery/registry.py delete mode 100644 third_party/bigframes_vendored/ibis/expr/operations/arrays.py diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index eaac0dc7855..1990647e0a2 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -387,10 +387,9 @@ def value_counts( def pct_change(block: blocks.Block, periods: int = 1) -> blocks.Block: column_labels = block.column_labels - window_spec = windows.rows( - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, - ) + + # Window framing clause is not allowed for analytic function lag. + window_spec = windows.unbound() original_columns = block.value_columns block, shift_columns = block.multi_apply_window_op( diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 3e97b1cb5e5..39cfed588f7 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -50,7 +50,7 @@ import bigframes.core.schema as bf_schema import bigframes.core.sql as sql import bigframes.core.utils as utils -import bigframes.core.window_spec as window_specs +import bigframes.core.window_spec as windows import bigframes.dtypes import bigframes.exceptions import bigframes.features @@ -900,7 +900,7 @@ def multi_apply_window_op( self, columns: typing.Sequence[str], op: agg_ops.WindowOp, - window_spec: window_specs.WindowSpec, + window_spec: windows.WindowSpec, *, skip_null_groups: bool = False, never_skip_nulls: bool = False, @@ -959,7 +959,7 @@ def apply_window_op( self, column: str, op: agg_ops.WindowOp, - window_spec: window_specs.WindowSpec, + window_spec: windows.WindowSpec, *, result_label: Label = None, skip_null_groups: bool = False, @@ -1475,7 +1475,7 @@ def grouped_head( value_columns: typing.Sequence[str], n: int, ): - window_spec = window_specs.cumulative_rows(grouping_keys=tuple(by_column_ids)) + window_spec = windows.cumulative_rows(grouping_keys=tuple(by_column_ids)) block, result_id = self.apply_window_op( value_columns[0], @@ -2383,10 +2383,7 @@ def _is_monotonic( return self._stats_cache[column_name][op_name] period = 1 - window = window_specs.rows( - preceding=period, - following=None, - ) + window_spec = windows.rows() # any NaN value means not monotonic block, last_notna_id = self.apply_unary_op(column_ids[0], ops.notnull_op) @@ -2402,7 +2399,7 @@ def _is_monotonic( last_result_id = None for column_id in column_ids[::-1]: block, lag_result_id = block.apply_window_op( - column_id, agg_ops.ShiftOp(period), window + column_id, agg_ops.ShiftOp(period), window_spec ) block, strict_monotonic_id = block.apply_binary_op( column_id, lag_result_id, ops.gt_op if increasing else ops.lt_op diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 488acd63db9..91a3045efb6 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -13,7 +13,7 @@ # limitations under the License. import functools import typing -from typing import cast, Optional +from typing import cast, List, Optional import bigframes_vendored.constants as constants import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops @@ -31,6 +31,17 @@ scalar_compiler = scalar_compilers.scalar_op_compiler +# TODO(swast): We can remove this if ibis adds general approx_quantile +# See: https://github.com/ibis-project/ibis/issues/9541 +@ibis.udf.agg.builtin +def approx_quantiles(expression: float, number) -> List[float]: + """APPROX_QUANTILES + + https://cloud.google.com/bigquery/docs/reference/standard-sql/approximate_aggregate_functions#approx_quantiles + """ + return [] # pragma: NO COVER + + def compile_aggregate( aggregate: ex.Aggregation, bindings: typing.Dict[str, ibis_types.Value], @@ -176,15 +187,12 @@ def _( column: ibis_types.NumericColumn, window=None, ) -> ibis_types.NumericValue: - # PERCENTILE_CONT has very few allowed windows. For example, "window - # framing clause is not allowed for analytic function percentile_cont". + # APPROX_QUANTILES has very few allowed windows. if window is not None: raise NotImplementedError( f"Approx Quartiles with windowing is not supported. {constants.FEEDBACK_LINK}" ) - value = vendored_ibis_ops.ApproximateMultiQuantile( - column, num_bins=4 # type: ignore - ).to_expr()[op.quartile] + value = approx_quantiles(column, 4)[op.quartile] # type: ignore return cast(ibis_types.NumericValue, value) @@ -513,11 +521,15 @@ def _( column: ibis_types.Column, window=None, ) -> ibis_types.BooleanValue: - # BQ will return null for empty column, result would be true in pandas. - result = _is_true(column).all() + # BQ will return null for empty column, result would be false in pandas. + result = _apply_window_if_present(_is_true(column).all(), window) + literal = ibis_types.literal(True) + return cast( ibis_types.BooleanScalar, - _apply_window_if_present(result, window).fillna(ibis_types.literal(True)), + result.fill_null(literal) + if hasattr(result, "fill_null") + else result.fillna(literal), ) @@ -528,10 +540,14 @@ def _( window=None, ) -> ibis_types.BooleanValue: # BQ will return null for empty column, result would be false in pandas. - result = _is_true(column).any() + result = _apply_window_if_present(_is_true(column).any(), window) + literal = ibis_types.literal(False) + return cast( ibis_types.BooleanScalar, - _apply_window_if_present(result, window).fillna(ibis_types.literal(False)), + result.fill_null(literal) + if hasattr(result, "fill_null") + else result.fillna(literal), ) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 2a13ce0da0e..cd00c98381f 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -19,10 +19,9 @@ import typing from typing import Collection, Literal, Optional, Sequence -import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops +import bigframes_vendored.ibis.backends.bigquery.backend as ibis_bigquery import google.cloud.bigquery import ibis -import ibis.backends.bigquery as ibis_bigquery import ibis.backends.bigquery.datatypes import ibis.common.deferred # type: ignore import ibis.expr.datatypes as ibis_dtypes @@ -407,18 +406,13 @@ def explode(self, offsets: typing.Sequence[int]) -> UnorderedIR: # The offset array ensures null represents empty arrays after unnesting. offset_array_id = bigframes.core.guid.generate_guid("offset_array_") - offset_array = ( - vendored_ibis_ops.GenerateArray( - ibis.greatest( - 0, - ibis.least( - *[table[column_id].length() - 1 for column_id in column_ids] - ), - ) - ) - .to_expr() - .name(offset_array_id), - ) + offset_array = ibis.range( + 0, + ibis.greatest( + 1, # We always want at least 1 element to fill in NULLs for empty arrays. + ibis.least(*[table[column_id].length() for column_id in column_ids]), + ), + ).name(offset_array_id) table_w_offset_array = table.select( offset_array, *self._column_names, @@ -718,21 +712,13 @@ def explode(self, offsets: typing.Sequence[int]) -> OrderedIR: column_ids = tuple(table.columns[offset] for offset in offsets) offset_array_id = bigframes.core.guid.generate_guid("offset_array_") - offset_array = ( - vendored_ibis_ops.GenerateArray( - ibis.greatest( - 0, - ibis.least( - *[ - table[table.columns[offset]].length() - 1 - for offset in offsets - ] - ), - ) - ) - .to_expr() - .name(offset_array_id), - ) + offset_array = ibis.range( + 0, + ibis.greatest( + 1, # We always want at least 1 element to fill in NULLs for empty arrays. + ibis.least(*[table[column_id].length() for column_id in column_ids]), + ), + ).name(offset_array_id) table_w_offset_array = table.select( offset_array, *self._column_names, @@ -870,7 +856,7 @@ def project_window_op( clauses = [] if op.skips_nulls and not never_skip_nulls: - clauses.append((column.isnull(), ibis.NA)) + clauses.append((column.isnull(), ibis.null())) if window_spec.min_periods: if op.skips_nulls: # Most operations do not count NULL values towards min_periods @@ -891,7 +877,7 @@ def project_window_op( clauses.append( ( observation_count < ibis_types.literal(window_spec.min_periods), - ibis.NA, + ibis.null(), ) ) if clauses: @@ -1322,9 +1308,10 @@ def _ibis_window_from_spec( bounds.preceding, bounds.following, how="range" ) if isinstance(bounds, RowsWindowBounds): - window = window.preceding_following( - bounds.preceding, bounds.following, how="rows" - ) + 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}") return window diff --git a/bigframes/core/compile/default_ordering.py b/bigframes/core/compile/default_ordering.py index 7d7a41f742a..a6b625caca6 100644 --- a/bigframes/core/compile/default_ordering.py +++ b/bigframes/core/compile/default_ordering.py @@ -49,7 +49,12 @@ def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringVa # Needed for JSON, STRUCT and ARRAY datatypes result = vendored_ibis_ops.ToJsonString(column).to_expr() # type: ignore # Escape backslashes and use backslash as delineator - escaped = cast(ibis_types.StringColumn, result.fillna("")).replace("\\", "\\\\") # type: ignore + escaped = cast( + ibis_types.StringColumn, + result.fill_null("") if hasattr(result, "fill_null") else result.fillna(""), + ).replace( + "\\", "\\\\" + ) # type: ignore return cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index c0f12865d66..799a408d5b5 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -842,7 +842,7 @@ def isin_op_impl(x: ibis_types.Value, op: ops.IsInOp): @scalar_op_compiler.register_unary_op(ops.ToDatetimeOp, pass_op=True) def to_datetime_op_impl(x: ibis_types.Value, op: ops.ToDatetimeOp): if x.type() == ibis_dtypes.str: - return vendored_ibis_ops.SafeCastToDatetime(x).to_expr() + return x.try_cast(ibis_dtypes.Timestamp(None)) else: # Numerical inputs. if op.format: @@ -995,8 +995,14 @@ def eq_nulls_match_op( y: ibis_types.Value, ): """Variant of eq_op where nulls match each other. Only use where dtypes are known to be same.""" - left = x.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) - right = y.cast(ibis_dtypes.str).fillna(ibis_types.literal("$NULL_SENTINEL$")) + literal = ibis_types.literal("$NULL_SENTINEL$") + if hasattr(x, "fill_null"): + left = x.cast(ibis_dtypes.str).fill_null(literal) + right = y.cast(ibis_dtypes.str).fill_null(literal) + else: + left = x.cast(ibis_dtypes.str).fillna(literal) + right = y.cast(ibis_dtypes.str).fillna(literal) + return left == right @@ -1379,7 +1385,10 @@ def fillna_op( x: ibis_types.Value, y: ibis_types.Value, ): - return x.fillna(typing.cast(ibis_types.Scalar, y)) + if hasattr(x, "fill_null"): + return x.fill_null(typing.cast(ibis_types.Scalar, y)) + else: + return x.fillna(typing.cast(ibis_types.Scalar, y)) @scalar_op_compiler.register_binary_op(ops.round_op) diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index 26af969b744..325df8e180a 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -179,4 +179,8 @@ def value_to_join_key(value: ibis_types.Value): """Converts nullable values to non-null string SQL will not match null keys together - but pandas does.""" if not value.type().is_string(): value = value.cast(ibis_dtypes.str) - return value.fillna(ibis_types.literal("$NULL_SENTINEL$")) + return ( + value.fill_null(ibis_types.literal("$NULL_SENTINEL$")) + if hasattr(value, "fill_null") + else value.fillna(ibis_types.literal("$NULL_SENTINEL$")) + ) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index eda517964db..5cb0e657290 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -255,19 +255,17 @@ def cumprod(self, *args, **kwargs) -> df.DataFrame: @validations.requires_ordering() def shift(self, periods=1) -> series.Series: - window = window_specs.rows( + # Window framing clause is not allowed for analytic function lag. + window = window_specs.unbound( grouping_keys=tuple(self._by_col_ids), - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, ) return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) @validations.requires_ordering() def diff(self, periods=1) -> series.Series: + # Window framing clause is not allowed for analytic function lag. window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, ) return self._apply_window_op(agg_ops.DiffOp(periods), window=window) @@ -685,10 +683,9 @@ def cumcount(self, *args, **kwargs) -> series.Series: @validations.requires_ordering() def shift(self, periods=1) -> series.Series: """Shift index by desired number of periods.""" + # Window framing clause is not allowed for analytic function lag. window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, ) return self._apply_window_op(agg_ops.ShiftOp(periods), window=window) @@ -696,8 +693,6 @@ def shift(self, periods=1) -> series.Series: def diff(self, periods=1) -> series.Series: window = window_specs.rows( grouping_keys=tuple(self._by_col_ids), - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, ) return self._apply_window_op(agg_ops.DiffOp(periods), window=window) diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py index f011e2848de..3d80afea5ac 100644 --- a/bigframes/core/window_spec.py +++ b/bigframes/core/window_spec.py @@ -70,7 +70,6 @@ def rows( Returns: WindowSpec """ - assert (preceding is not None) or (following is not None) bounds = RowsWindowBounds(preceding=preceding, following=following) return WindowSpec( grouping_keys=grouping_keys, diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d7bdd5529bd..d7738a569d4 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -65,7 +65,7 @@ import bigframes.core.utils as utils import bigframes.core.validations as validations import bigframes.core.window -import bigframes.core.window_spec as window_spec +import bigframes.core.window_spec as windows import bigframes.dtypes import bigframes.exceptions import bigframes.formatting_helpers as formatter @@ -1958,12 +1958,12 @@ def replace( @validations.requires_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> DataFrame: - window = window_spec.rows(preceding=limit, following=0) + window = windows.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) @validations.requires_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> DataFrame: - window = window_spec.rows(preceding=0, following=limit) + window = windows.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) def isin(self, values) -> DataFrame: @@ -2670,7 +2670,7 @@ def _perform_join_by_index( @validations.requires_ordering() 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 = window_spec.rows( + window_def = windows.rows( preceding=window - 1, following=0, min_periods=min_periods or window ) return bigframes.core.window.Window( @@ -2679,7 +2679,7 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window @validations.requires_ordering() def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: - window = window_spec.cumulative_rows(min_periods=min_periods) + window = windows.cumulative_rows(min_periods=min_periods) return bigframes.core.window.Window( self._block, window, self._block.value_columns ) @@ -2790,7 +2790,7 @@ def cumsum(self): raise ValueError("All values must be numeric to apply cumsum.") return self._apply_window_op( agg_ops.sum_op, - window_spec.cumulative_rows(), + windows.cumulative_rows(), ) @validations.requires_ordering() @@ -2803,38 +2803,32 @@ def cumprod(self) -> DataFrame: raise ValueError("All values must be numeric to apply cumsum.") return self._apply_window_op( agg_ops.product_op, - window_spec.cumulative_rows(), + windows.cumulative_rows(), ) @validations.requires_ordering() def cummin(self) -> DataFrame: return self._apply_window_op( agg_ops.min_op, - window_spec.cumulative_rows(), + windows.cumulative_rows(), ) @validations.requires_ordering() def cummax(self) -> DataFrame: return self._apply_window_op( agg_ops.max_op, - window_spec.cumulative_rows(), + windows.cumulative_rows(), ) @validations.requires_ordering() def shift(self, periods: int = 1) -> DataFrame: - window = window_spec.rows( - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, - ) - return self._apply_window_op(agg_ops.ShiftOp(periods), window) + window_spec = windows.rows() + return self._apply_window_op(agg_ops.ShiftOp(periods), window_spec) @validations.requires_ordering() def diff(self, periods: int = 1) -> DataFrame: - window = window_spec.rows( - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, - ) - return self._apply_window_op(agg_ops.DiffOp(periods), window) + window_spec = windows.rows() + return self._apply_window_op(agg_ops.DiffOp(periods), window_spec) @validations.requires_ordering() def pct_change(self, periods: int = 1) -> DataFrame: @@ -2845,7 +2839,7 @@ def pct_change(self, periods: int = 1) -> DataFrame: def _apply_window_op( self, op: agg_ops.WindowOp, - window_spec: window_spec.WindowSpec, + window_spec: windows.WindowSpec, ): block, result_ids = self._block.multi_apply_window_op( self._block.value_columns, diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py index 05109801787..6bc7a4b079e 100644 --- a/bigframes/functions/_remote_function_session.py +++ b/bigframes/functions/_remote_function_session.py @@ -522,7 +522,8 @@ def try_delattr(attr): node = ibis.udf.scalar.builtin( func, name=rf_name, - schema=f"{dataset_ref.project}.{dataset_ref.dataset_id}", + catalog=dataset_ref.project, + database=dataset_ref.dataset_id, signature=(ibis_signature.input_types, ibis_signature.output_type), ) func.bigframes_cloud_function = ( diff --git a/bigframes/functions/remote_function.py b/bigframes/functions/remote_function.py index 4dc6c1ad6b1..1f4fdaad7bb 100644 --- a/bigframes/functions/remote_function.py +++ b/bigframes/functions/remote_function.py @@ -170,7 +170,8 @@ def func(*bigframes_args, **bigframes_kwargs): node = ibis.udf.scalar.builtin( func, name=routine_ref.routine_id, - schema=f"{routine_ref.project}.{routine_ref.dataset_id}", + catalog=routine_ref.project, + database=routine_ref.dataset_id, signature=(ibis_signature.input_types, ibis_signature.output_type), ) func.bigframes_remote_function = str(routine_ref) # type: ignore diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 13d2041ef3e..2c327f63f8e 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -307,6 +307,7 @@ def _compile_to_sql( for column in columns: min_value = X[column].min() max_value = X[column].max() + bin_size = (max_value - min_value) / self.n_bins array_split_points[column] = [ min_value + i * bin_size for i in range(self.n_bins - 1) diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 7120a5a5fdb..1cb327f19c8 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -123,7 +123,12 @@ def ml_bucketize( name: str, ) -> str: """Encode ML.BUCKETIZE for BQML""" - return f"""ML.BUCKETIZE({numeric_expr_sql}, {array_split_points}, FALSE) AS {name}""" + # Use Python value rather than Numpy value to serialization. + points = [ + point.item() if hasattr(point, "item") else point + for point in array_split_points + ] + return f"""ML.BUCKETIZE({numeric_expr_sql}, {points}, FALSE) AS {name}""" def ml_quantile_bucketize( self, diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 51a962b13bc..55b8fa1802b 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -892,7 +892,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT # Just parameterless unary ops for now # TODO: Parameter mappings -NUMPY_TO_OP: typing.Final = { +NUMPY_TO_OP: dict[np.ufunc, UnaryOp] = { np.sin: sin_op, np.cos: cos_op, np.tan: tan_op, @@ -917,7 +917,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT } -NUMPY_TO_BINOP: typing.Final = { +NUMPY_TO_BINOP: dict[np.ufunc, BinaryOp] = { np.add: add_op, np.subtract: sub_op, np.multiply: mul_op, diff --git a/bigframes/series.py b/bigframes/series.py index d4cb1d3700f..3a75ab9ccc8 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -45,7 +45,7 @@ import bigframes.core.utils as utils import bigframes.core.validations as validations import bigframes.core.window -import bigframes.core.window_spec +import bigframes.core.window_spec as windows import bigframes.dataframe import bigframes.dtypes import bigframes.formatting_helpers as formatter @@ -465,13 +465,11 @@ def case_when(self, caselist) -> Series: @validations.requires_ordering() def cumsum(self) -> Series: - return self._apply_window_op( - agg_ops.sum_op, bigframes.core.window_spec.cumulative_rows() - ) + return self._apply_window_op(agg_ops.sum_op, windows.cumulative_rows()) @validations.requires_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> Series: - window = bigframes.core.window_spec.rows(preceding=limit, following=0) + window = windows.rows(preceding=limit, following=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) pad = ffill @@ -479,42 +477,30 @@ def ffill(self, *, limit: typing.Optional[int] = None) -> Series: @validations.requires_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> Series: - window = bigframes.core.window_spec.rows(preceding=0, following=limit) + window = windows.rows(preceding=0, following=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) @validations.requires_ordering() def cummax(self) -> Series: - return self._apply_window_op( - agg_ops.max_op, bigframes.core.window_spec.cumulative_rows() - ) + return self._apply_window_op(agg_ops.max_op, windows.cumulative_rows()) @validations.requires_ordering() def cummin(self) -> Series: - return self._apply_window_op( - agg_ops.min_op, bigframes.core.window_spec.cumulative_rows() - ) + return self._apply_window_op(agg_ops.min_op, windows.cumulative_rows()) @validations.requires_ordering() def cumprod(self) -> Series: - return self._apply_window_op( - agg_ops.product_op, bigframes.core.window_spec.cumulative_rows() - ) + return self._apply_window_op(agg_ops.product_op, windows.cumulative_rows()) @validations.requires_ordering() def shift(self, periods: int = 1) -> Series: - window = bigframes.core.window_spec.rows( - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, - ) - return self._apply_window_op(agg_ops.ShiftOp(periods), window) + window_spec = windows.rows() + return self._apply_window_op(agg_ops.ShiftOp(periods), window_spec) @validations.requires_ordering() def diff(self, periods: int = 1) -> Series: - window = bigframes.core.window_spec.rows( - preceding=periods if periods > 0 else None, - following=-periods if periods < 0 else None, - ) - return self._apply_window_op(agg_ops.DiffOp(periods), window) + window_spec = windows.rows() + return self._apply_window_op(agg_ops.DiffOp(periods), window_spec) @validations.requires_ordering() def pct_change(self, periods: int = 1) -> Series: @@ -1053,7 +1039,7 @@ def mode(self) -> Series: block, max_value_count_col_id = block.apply_window_op( value_count_col_id, agg_ops.max_op, - window_spec=bigframes.core.window_spec.unbound(), + window_spec=windows.unbound(), ) block, is_mode_col_id = block.apply_binary_op( value_count_col_id, @@ -1286,9 +1272,7 @@ def _apply_aggregation( ) -> Any: return self._block.get_stat(self._value_column, op) - def _apply_window_op( - self, op: agg_ops.WindowOp, window_spec: bigframes.core.window_spec.WindowSpec - ): + def _apply_window_op(self, op: agg_ops.WindowOp, window_spec: windows.WindowSpec): block = self._block block, result_id = block.apply_window_op( self._value_column, op, window_spec=window_spec, result_label=self.name @@ -1345,7 +1329,7 @@ def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: @validations.requires_ordering() 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 = bigframes.core.window_spec.rows( + window_spec = windows.rows( preceding=window - 1, following=0, min_periods=min_periods or window ) return bigframes.core.window.Window( @@ -1354,9 +1338,7 @@ def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window @validations.requires_ordering() def expanding(self, min_periods: int = 1) -> bigframes.core.window.Window: - window_spec = bigframes.core.window_spec.cumulative_rows( - min_periods=min_periods - ) + window_spec = windows.cumulative_rows(min_periods=min_periods) return bigframes.core.window.Window( self._block, window_spec, self._block.value_columns, is_series=True ) diff --git a/noxfile.py b/noxfile.py index 5dbcdea5832..f4142af21bc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -62,8 +62,9 @@ UNIT_TEST_EXTRAS: List[str] = [] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} +# There are 4 different ibis-framework 9.x versions we want to test against. # 3.10 is needed for Windows tests. -SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.12"] +SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12"] SYSTEM_TEST_STANDARD_DEPENDENCIES = [ "jinja2", "mock", diff --git a/setup.py b/setup.py index 2ae61a44f33..908a3c244f2 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ "google-cloud-iam >=2.12.1", "google-cloud-resource-manager >=1.10.3", "google-cloud-storage >=2.0.0", - "ibis-framework[bigquery] >=8.0.0,<9.0.0dev", + "ibis-framework[bigquery] >=9.0.0,<=9.3.0", "jellyfish >=0.8.9", "numpy >=1.24.0", "pandas >=1.5.3", @@ -59,7 +59,7 @@ # Keep sqlglot versions in sync with ibis-framework. This avoids problems # where the incorrect version of sqlglot is installed, such as # https://github.com/googleapis/python-bigquery-dataframes/issues/315 - "sqlglot >=20.8.0,<=20.11", + "sqlglot >=23.6.3,<25.2", "tabulate >= 0.9", "ipywidgets >=7.7.1", "humanize >= 4.6.0", diff --git a/testing/constraints-3.11.txt b/testing/constraints-3.11.txt index e69de29bb2d..60ac0af60f5 100644 --- a/testing/constraints-3.11.txt +++ b/testing/constraints-3.11.txt @@ -0,0 +1,3 @@ +# Some internal modules have moved, +# so make sure we test on all ibis-framework 9.x versions. +ibis-framework==9.1.0 diff --git a/testing/constraints-3.12.txt b/testing/constraints-3.12.txt index e69de29bb2d..dbbb5a2d888 100644 --- a/testing/constraints-3.12.txt +++ b/testing/constraints-3.12.txt @@ -0,0 +1,3 @@ +# Some internal modules have moved, +# so make sure we test on all ibis-framework 9.x versions. +ibis-framework==9.2.0 diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index a446a4bfa77..4a9d1ae281e 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -12,7 +12,7 @@ google-cloud-bigquery-connection==1.12.0 google-cloud-iam==2.12.1 google-cloud-resource-manager==1.10.3 google-cloud-storage==2.0.0 -ibis-framework==8.0.0 +ibis-framework==9.0.0 jellyfish==0.8.9 numpy==1.24.0 pandas==1.5.3 @@ -21,7 +21,7 @@ pydata-google-auth==1.8.2 requests==2.27.1 scikit-learn==1.2.2 sqlalchemy==1.4 -sqlglot==20.8.0 +sqlglot==23.6.3 tabulate==0.9 ipywidgets==7.7.1 humanize==4.6.0 diff --git a/tests/system/conftest.py b/tests/system/conftest.py index b382a5593ce..49cd887cfdf 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -29,7 +29,7 @@ import google.cloud.functions_v2 as functions_v2 import google.cloud.resourcemanager_v3 as resourcemanager_v3 import google.cloud.storage as storage # type: ignore -import ibis.backends.base +import ibis.backends import numpy as np import pandas as pd import pytest @@ -105,7 +105,7 @@ def bigquery_client_tokyo(session_tokyo: bigframes.Session) -> bigquery.Client: @pytest.fixture(scope="session") -def ibis_client(session: bigframes.Session) -> ibis.backends.base.BaseBackend: +def ibis_client(session: bigframes.Session) -> ibis.backends.BaseBackend: return session.ibis_client diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 77ea4627ec5..e224f65a016 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1726,8 +1726,10 @@ def test_df_apply_axis_1_complex(session, pd_df): def serialize_row(row): custom = { - "name": row.name, - "index": [idx for idx in row.index], + "name": row.name.item() if hasattr(row.name, "item") else row.name, + "index": [ + idx.item() if hasattr(idx, "item") else idx for idx in row.index + ], "values": [ val.item() if hasattr(val, "item") else val for val in row.values ], diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index f51b5976507..9e046dc62ee 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4568,6 +4568,9 @@ def test_recursion_limit(scalars_df_index): scalars_df_index.to_pandas() +@pytest.mark.skipif( + reason="b/366477265: Skip until query complexity error can be reliably triggered." +) def test_query_complexity_error(scalars_df_index): # This test requires automatic caching/query decomposition to be turned off bf_df = scalars_df_index diff --git a/tests/system/small/test_ibis.py b/tests/system/small/test_ibis.py deleted file mode 100644 index e2648d1eba4..00000000000 --- a/tests/system/small/test_ibis.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for monkeypatched ibis code.""" - -import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops -import ibis.expr.types as ibis_types - -import bigframes - - -def test_approximate_quantiles(session: bigframes.Session, scalars_table_id: str): - num_bins = 3 - ibis_client = session.ibis_client - project, dataset, table_id = scalars_table_id.split(".") - ibis_table: ibis_types.Table = ibis_client.table( # type: ignore - table_id, - schema=dataset, - database=project, - ) - ibis_column: ibis_types.NumericColumn = ibis_table["int64_col"] - quantiles: ibis_types.ArrayScalar = vendored_ibis_ops.ApproximateMultiQuantile( - ibis_column, # type: ignore - num_bins=num_bins, # type: ignore - ).to_expr() - value = quantiles[1] - num_edges = quantiles.length() - - sql = ibis_client.compile(value) - num_edges_result = num_edges.to_pandas() - - assert "APPROX_QUANTILES" in sql - assert num_edges_result == num_bins + 1 diff --git a/tests/system/small/test_numpy.py b/tests/system/small/test_numpy.py index 8f62d9628c0..37a707b9d08 100644 --- a/tests/system/small/test_numpy.py +++ b/tests/system/small/test_numpy.py @@ -70,6 +70,13 @@ def test_df_ufuncs(scalars_dfs, opname): ).to_pandas() pd_result = getattr(np, opname)(scalars_pandas_df[["float64_col", "int64_col"]]) + # In NumPy versions 2 and later, `np.floor` and `np.ceil` now produce integer + # outputs for the "int64_col" column. + if opname in ["floor", "ceil"] and isinstance( + pd_result["int64_col"].dtypes, pd.Int64Dtype + ): + pd_result["int64_col"] = pd_result["int64_col"].astype(pd.Float64Dtype()) + pd.testing.assert_frame_equal(bf_result, pd_result) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 9a6783ee5c5..b8f7926aecd 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2346,8 +2346,13 @@ def test_value_counts(scalars_dfs, kwargs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_too" - bf_result = scalars_df[col_name].value_counts(**kwargs).to_pandas() - pd_result = scalars_pandas_df[col_name].value_counts(**kwargs) + # Pandas `value_counts` can produce non-deterministic results with tied counts. + # Remove duplicates to enforce a consistent output. + s = scalars_df[col_name].drop(0) + pd_s = scalars_pandas_df[col_name].drop(0) + + bf_result = s.value_counts(**kwargs).to_pandas() + pd_result = pd_s.value_counts(**kwargs) pd.testing.assert_series_equal( bf_result, diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py b/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py index 1d2d05a741a..e69de29bb2d 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/__init__.py @@ -1,3 +0,0 @@ -# Import all sub-modules to monkeypatch everything. -import bigframes_vendored.ibis.backends.bigquery.compiler # noqa -import bigframes_vendored.ibis.backends.bigquery.registry # noqa diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/backend.py b/third_party/bigframes_vendored/ibis/backends/bigquery/backend.py index f917ef950da..d4d51565723 100644 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/backend.py +++ b/third_party/bigframes_vendored/ibis/backends/bigquery/backend.py @@ -8,10 +8,10 @@ import contextlib import glob import os -import re from typing import Any, Optional, TYPE_CHECKING from bigframes_vendored.ibis.backends.bigquery.datatypes import BigQueryType +import bigframes_vendored.ibis.backends.sql.compilers as sc import google.api_core.exceptions import google.auth.credentials import google.cloud.bigquery as bq @@ -27,7 +27,6 @@ ) from ibis.backends.bigquery.datatypes import BigQuerySchema from ibis.backends.sql import SQLBackend -import ibis.backends.sql.compilers as sc import ibis.common.exceptions as com import ibis.expr.operations as ops import ibis.expr.schema as sch @@ -81,52 +80,6 @@ def _create_client_info_gapic(application_name): return ClientInfo(user_agent=_create_user_agent(application_name)) -_MEMTABLE_PATTERN = re.compile( - r"^_?ibis_(?:[A-Za-z_][A-Za-z_0-9]*)_memtable_[a-z0-9]{26}$" -) - - -def _qualify_memtable( - node: sge.Expression, *, dataset: str | None, project: str | None -) -> sge.Expression: - """Add a BigQuery dataset and project to memtable references.""" - if isinstance(node, sge.Table) and _MEMTABLE_PATTERN.match(node.name) is not None: - node.args["db"] = dataset - node.args["catalog"] = project - # make sure to quote table location - node = _force_quote_table(node) - return node - - -def _remove_null_ordering_from_unsupported_window( - node: sge.Expression, -) -> sge.Expression: - """Remove null ordering in window frame clauses not supported by BigQuery. - - BigQuery has only partial support for NULL FIRST/LAST in RANGE windows so - we remove it from any window frame clause that doesn't support it. - - Here's the support matrix: - - ✅ sum(x) over (order by y desc nulls last) - 🚫 sum(x) over (order by y asc nulls last) - ✅ sum(x) over (order by y asc nulls first) - 🚫 sum(x) over (order by y desc nulls first) - """ - if isinstance(node, sge.Window): - order = node.args.get("order") - if order is not None: - for key in order.args["expressions"]: - kargs = key.args - if kargs.get("desc") is True and kargs.get("nulls_first", False): - kargs["nulls_first"] = False - elif kargs.get("desc") is False and not kargs.setdefault( - "nulls_first", True - ): - kargs["nulls_first"] = True - return node - - def _force_quote_table(table: sge.Table) -> sge.Table: """Force quote all the parts of a bigquery path. @@ -156,6 +109,10 @@ class Backend(SQLBackend, CanCreateDatabase, CanCreateSchema): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.__session_dataset: bq.DatasetReference | None = None + self._query_cache.lookup = lambda name: self.table( + name, + database=(self._session_dataset.project, self._session_dataset.dataset_id), + ).op() @property def _session_dataset(self): @@ -163,32 +120,6 @@ def _session_dataset(self): self.__session_dataset = self._make_session() return self.__session_dataset - def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: - raw_name = op.name - - session_dataset = self._session_dataset - project = session_dataset.project - dataset = session_dataset.dataset_id - - table_ref = bq.TableReference(session_dataset, raw_name) - try: - self.client.get_table(table_ref) - except google.api_core.exceptions.NotFound: - table_id = sg.table( - raw_name, db=dataset, catalog=project, quoted=False - ).sql(dialect=self.name) - bq_schema = BigQuerySchema.from_ibis(op.schema) - load_job = self.client.load_table_from_dataframe( - op.data.to_frame(), - table_id, - job_config=bq.LoadJobConfig( - # fail if the table already exists and contains data - write_disposition=bq.WriteDisposition.WRITE_EMPTY, - schema=bq_schema, - ), - ) - load_job.result() - def _read_file( self, path: str | Path, @@ -793,7 +724,6 @@ def to_pyarrow( **kwargs: Any, ) -> pa.Table: self._import_pyarrow() - self._register_in_memory_tables(expr) sql = self.compile(expr, limit=limit, params=params, **kwargs) self._log(sql) query = self.raw_sql(sql, params=params, **kwargs) @@ -816,7 +746,6 @@ def to_pyarrow_batches( schema = expr.as_table().schema() - self._register_in_memory_tables(expr) sql = self.compile(expr, limit=limit, params=params, **kwargs) self._log(sql) query = self.raw_sql(sql, params=params, page_size=chunk_size, **kwargs) @@ -1009,9 +938,6 @@ def create_table( if obj is not None and not isinstance(obj, ir.Table): obj = ibis.memtable(obj, schema=schema) - if obj is not None: - self._register_in_memory_tables(obj) - if temp: dataset = self._session_dataset.dataset_id if database is not None: @@ -1107,7 +1033,6 @@ def create_view( expression=self.compile(obj), replace=overwrite, ) - self._register_in_memory_tables(obj) self.raw_sql(stmt.sql(self.name)) return self.table(name, database=(catalog, database)) diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py b/third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py deleted file mode 100644 index 414f0a7c81e..00000000000 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/compiler.py +++ /dev/null @@ -1,59 +0,0 @@ -# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/compiler.py -"""Module to convert from Ibis expression to SQL string.""" - -from __future__ import annotations - -import re - -from ibis.backends.base.sql import compiler as sql_compiler -import ibis.backends.bigquery.compiler -from ibis.backends.bigquery.datatypes import BigQueryType -import ibis.expr.datatypes as dt -import ibis.expr.operations as ops - -_NAME_REGEX = re.compile(r'[^!"$()*,./;?@[\\\]^`{}~\n]+') -_EXACT_NAME_REGEX = re.compile(f"^{_NAME_REGEX.pattern}$") - - -class BigQueryTableSetFormatter(sql_compiler.TableSetFormatter): - def _quote_identifier(self, name): - """Restore 6.x version of identifier quoting. - - 7.x uses sqlglot which as of December 2023 doesn't know about the - extended unicode names for BigQuery yet. - """ - if _EXACT_NAME_REGEX.match(name) is not None: - return name - return f"`{name}`" - - def _format_in_memory_table(self, op): - """Restore 6.x version of InMemoryTable. - - BigQuery DataFrames explicitly uses InMemoryTable only when we know - the data is small enough to embed in SQL. - """ - schema = op.schema - names = schema.names - types = schema.types - - raw_rows = [] - for row in op.data.to_frame().itertuples(index=False): - raw_row = ", ".join( - f"{self._translate(lit)} AS {name}" - for lit, name in zip( - map(ops.Literal, row, types), map(self._quote_identifier, names) - ) - ) - raw_rows.append(f"STRUCT({raw_row})") - array_type = BigQueryType.from_ibis(dt.Array(op.schema.as_struct())) - - return f"UNNEST({array_type}[{', '.join(raw_rows)}])" - - -# Override implementation. -ibis.backends.bigquery.compiler.BigQueryTableSetFormatter._quote_identifier = ( - BigQueryTableSetFormatter._quote_identifier -) -ibis.backends.bigquery.compiler.BigQueryTableSetFormatter._format_in_memory_table = ( - BigQueryTableSetFormatter._format_in_memory_table -) diff --git a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py b/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py deleted file mode 100644 index ecef2115e5b..00000000000 --- a/third_party/bigframes_vendored/ibis/backends/bigquery/registry.py +++ /dev/null @@ -1,72 +0,0 @@ -# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/backends/bigquery/registry.py -"""Module to convert from Ibis expression to SQL string.""" - -import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops -from ibis.backends.bigquery.registry import OPERATION_REGISTRY -import ibis.expr.operations.reductions as ibis_reductions - - -def _approx_quantiles(translator, op: vendored_ibis_ops.ApproximateMultiQuantile): - arg = translator.translate(op.arg) - num_bins = translator.translate(op.num_bins) - return f"APPROX_QUANTILES({arg}, {num_bins})" - - -def _first_non_null_value(translator, op: vendored_ibis_ops.FirstNonNullValue): - arg = translator.translate(op.arg) - return f"FIRST_VALUE({arg} IGNORE NULLS)" - - -def _last_non_null_value(translator, op: vendored_ibis_ops.LastNonNullValue): - arg = translator.translate(op.arg) - return f"LAST_VALUE({arg} IGNORE NULLS)" - - -def _to_json_string(translator, op: vendored_ibis_ops.ToJsonString): - arg = translator.translate(op.arg) - return f"TO_JSON_STRING({arg})" - - -def _generate_array(translator, op: vendored_ibis_ops.GenerateArray): - arg = translator.translate(op.arg) - return f"GENERATE_ARRAY(0, {arg})" - - -def _safe_cast_to_datetime(translator, op: vendored_ibis_ops.SafeCastToDatetime): - arg = translator.translate(op.arg) - return f"SAFE_CAST({arg} AS DATETIME)" - - -def _quantile(translator, op: ibis_reductions.Quantile): - arg = translator.translate(op.arg) - quantile = translator.translate(op.quantile) - return f"PERCENTILE_CONT({arg}, {quantile})" - - -def _array_aggregate(translator, op: vendored_ibis_ops.ArrayAggregate): - """This method provides the same functionality as the collect() method in Ibis, with - the added capability of ordering the results using order_by. - https://github.com/ibis-project/ibis/issues/9170 - """ - arg = translator.translate(op.arg) - - order_by_sql = "" - if len(op.order_by) > 0: - order_by = ", ".join([translator.translate(column) for column in op.order_by]) - order_by_sql = f"ORDER BY {order_by}" - - return f"ARRAY_AGG({arg} IGNORE NULLS {order_by_sql})" - - -patched_ops = { - vendored_ibis_ops.ApproximateMultiQuantile: _approx_quantiles, # type:ignore - vendored_ibis_ops.FirstNonNullValue: _first_non_null_value, # type:ignore - vendored_ibis_ops.LastNonNullValue: _last_non_null_value, # type:ignore - vendored_ibis_ops.ToJsonString: _to_json_string, # type:ignore - vendored_ibis_ops.GenerateArray: _generate_array, # type:ignore - vendored_ibis_ops.SafeCastToDatetime: _safe_cast_to_datetime, # type:ignore - ibis_reductions.Quantile: _quantile, # type:ignore - vendored_ibis_ops.ArrayAggregate: _array_aggregate, # type:ignore -} - -OPERATION_REGISTRY.update(patched_ops) diff --git a/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py b/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py index c74de82099e..cbd8e4e2d9d 100644 --- a/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py +++ b/third_party/bigframes_vendored/ibis/backends/sql/compilers/base.py @@ -610,7 +610,6 @@ def translate(self, op, *, params: Mapping[ir.Value, Any]) -> sge.Expression: op, params=params, rewrites=self.rewrites, - post_rewrites=self.post_rewrites, fuse_selects=options.sql.fuse_selects, ) @@ -1125,7 +1124,7 @@ def visit_Coalesce(self, op, *, arg): ### Ordering and window functions - def visit_SortKey(self, op, *, expr, ascending: bool, nulls_first: bool): + def visit_SortKey(self, op, *, expr, ascending: bool, nulls_first: bool = False): return sge.Ordered(this=expr, desc=not ascending, nulls_first=nulls_first) def visit_ApproxMedian(self, op, *, arg, where): @@ -1262,11 +1261,9 @@ def _cleanup_names(self, exprs: Mapping[str, sge.Expression]): else: yield value.as_(name, quoted=self.quoted, copy=False) - def visit_Select( - self, op, *, parent, selections, predicates, qualified, sort_keys, distinct - ): + def visit_Select(self, op, *, parent, selections, predicates, qualified, sort_keys): # if we've constructed a useless projection return the parent relation - if not (selections or predicates or qualified or sort_keys or distinct): + if not (selections or predicates or qualified or sort_keys): return parent result = parent @@ -1293,9 +1290,6 @@ def visit_Select( if sort_keys: result = result.order_by(*sort_keys, copy=False) - if distinct: - result = result.distinct() - return result def visit_DummyTable(self, op, *, values): @@ -1392,7 +1386,10 @@ def visit_Aggregate(self, op, *, parent, groups, metrics): @classmethod def _add_parens(cls, op, sg_expr): - if isinstance(op, cls.NEEDS_PARENS): + # Patch for https://github.com/ibis-project/ibis/issues/9975 + if isinstance(op, cls.NEEDS_PARENS) or ( + isinstance(op, ops.Alias) and isinstance(op.arg, cls.NEEDS_PARENS) + ): return sge.paren(sg_expr, copy=False) return sg_expr @@ -1480,6 +1477,11 @@ def visit_Limit(self, op, *, parent, n, offset): return result.subquery(alias, copy=False) return result + def visit_Distinct(self, op, *, parent): + return ( + sg.select(STAR, copy=False).distinct(copy=False).from_(parent, copy=False) + ) + def visit_CTE(self, op, *, parent): return sg.table(parent.alias_or_name, quoted=self.quoted) diff --git a/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py b/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py index fc8d93a433c..3015991a26f 100644 --- a/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py +++ b/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py @@ -1,5 +1,4 @@ # Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/backends/sql/compilers/bigquery/__init__.py - """Module to convert from Ibis expression to SQL string.""" from __future__ import annotations @@ -9,28 +8,25 @@ import re from typing import Any, TYPE_CHECKING -from bigframes_vendored.ibis.backends.bigquery.datatypes import ( - BigQueryType, - BigQueryUDFType, -) +import bigframes_vendored.ibis.backends.bigquery.datatypes as bq_datatypes from bigframes_vendored.ibis.backends.sql.compilers.base import ( AggGen, NULL, SQLGlotCompiler, STAR, ) -from bigframes_vendored.ibis.backends.sql.rewrites import ( +from ibis import util +from ibis.backends.sql.datatypes import BigQueryType, BigQueryUDFType +from ibis.backends.sql.rewrites import ( exclude_unsupported_window_frame_from_ops, exclude_unsupported_window_frame_from_rank, exclude_unsupported_window_frame_from_row_number, - split_select_distinct_with_order_by, ) -from ibis import util -from ibis.backends.sql.compilers.bigquery.udf.core import PythonToJavaScriptTranslator import ibis.common.exceptions as com from ibis.common.temporal import DateUnit, IntervalUnit, TimestampUnit, TimeUnit import ibis.expr.datatypes as dt import ibis.expr.operations as ops +import numpy as np import sqlglot as sg from sqlglot.dialects import BigQuery import sqlglot.expressions as sge @@ -40,6 +36,7 @@ import ibis.expr.types as ir + _NAME_REGEX = re.compile(r'[^!"$()*,./;?@[\\\]^`{}~\n]+') @@ -55,8 +52,6 @@ def _qualify_memtable( if isinstance(node, sge.Table) and _MEMTABLE_PATTERN.match(node.name) is not None: node.args["db"] = dataset node.args["catalog"] = project - # make sure to quote table location - node = _force_quote_table(node) return node @@ -64,12 +59,9 @@ def _remove_null_ordering_from_unsupported_window( node: sge.Expression, ) -> sge.Expression: """Remove null ordering in window frame clauses not supported by BigQuery. - BigQuery has only partial support for NULL FIRST/LAST in RANGE windows so we remove it from any window frame clause that doesn't support it. - Here's the support matrix: - ✅ sum(x) over (order by y desc nulls last) 🚫 sum(x) over (order by y asc nulls last) ✅ sum(x) over (order by y asc nulls first) @@ -89,27 +81,6 @@ def _remove_null_ordering_from_unsupported_window( return node -def _force_quote_table(table: sge.Table) -> sge.Table: - """Force quote all the parts of a bigquery path. - - The BigQuery identifier quoting semantics are bonkers - https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers - - my-table is OK, but not mydataset.my-table - - mytable-287 is OK, but not mytable-287a - - Just quote everything. - """ - for key in ("this", "db", "catalog"): - if (val := table.args[key]) is not None: - if isinstance(val, sg.exp.Identifier) and not val.quoted: - val.args["quoted"] = True - else: - table.args[key] = sg.to_identifier(val, quoted=True) - return table - - class BigQueryCompiler(SQLGlotCompiler): dialect = BigQuery type_mapper = BigQueryType @@ -123,7 +94,6 @@ class BigQueryCompiler(SQLGlotCompiler): exclude_unsupported_window_frame_from_rank, *SQLGlotCompiler.rewrites, ) - post_rewrites = (split_select_distinct_with_order_by,) supports_qualify = True @@ -214,7 +184,6 @@ def to_sqlglot( session_project: str | None = None, ) -> Any: """Compile an Ibis expression. - Parameters ---------- expr @@ -228,18 +197,16 @@ def to_sqlglot( Optional dataset ID to qualify memtable references. session_project Optional project ID to qualify memtable references. - Returns ------- Any The output of compilation. The type of this value depends on the backend. - """ sql = super().to_sqlglot(expr, limit=limit, params=params) table_expr = expr.as_table() - geocols = table_expr.schema().geospatial + geocols = getattr(table_expr.schema(), "geospatial", None) result = sql.transform( _qualify_memtable, @@ -280,64 +247,6 @@ def to_sqlglot( sources.append(result) return sources - def _compile_python_udf(self, udf_node: ops.ScalarUDF) -> sge.Create: - name = type(udf_node).__name__ - type_mapper = self.udf_type_mapper - - body = PythonToJavaScriptTranslator(udf_node.__func__).compile() - config = udf_node.__config__ - libraries = config.get("libraries", []) - - signature = [ - sge.ColumnDef( - this=sg.to_identifier(name, quoted=self.quoted), - kind=type_mapper.from_ibis(param.annotation.pattern.dtype), - ) - for name, param in udf_node.__signature__.parameters.items() - ] - - lines = ['"""'] - - if config.get("strict", True): - lines.append('"use strict";') - - lines += [ - body, - "", - f"return {udf_node.__func_name__}({', '.join(udf_node.argnames)});", - '"""', - ] - - func = sge.Create( - kind="FUNCTION", - this=sge.UserDefinedFunction( - this=sg.to_identifier(name), expressions=signature, wrapped=True - ), - # not exactly what I had in mind, but it works - # - # quoting is too simplistic to handle multiline strings - expression=sge.Var(this="\n".join(lines)), - exists=False, - properties=sge.Properties( - expressions=[ - sge.TemporaryProperty(), - sge.ReturnsProperty(this=type_mapper.from_ibis(udf_node.dtype)), - sge.StabilityProperty( - this="IMMUTABLE" if config.get("determinism") else "VOLATILE" - ), - sge.LanguageProperty(this=sg.to_identifier("js")), - ] - + [ - sge.Property( - this=sg.to_identifier("library"), value=self.f.array(*libraries) - ) - ] - * bool(libraries) - ), - ) - - return func - @staticmethod def _minimize_spec(start, end, spec): if ( @@ -528,7 +437,7 @@ def visit_ArrayContains(self, op, *, arg, other): def visit_StringContains(self, op, *, haystack, needle): return self.f.strpos(haystack, needle) > 0 - def visti_StringFind(self, op, *, arg, substr, start, end): + def visit_StringFind(self, op, *, arg, substr, start, end): if start is not None: raise NotImplementedError( "`start` not implemented for BigQuery string find" @@ -573,6 +482,9 @@ def visit_NonNullLiteral(self, op, *, value, dtype): ) elif dtype.is_uuid(): return sge.convert(str(value)) + + elif dtype.is_int64(): + return sge.convert(np.int64(value)) return None def visit_IntervalFromInteger(self, op, *, arg, unit): @@ -1110,5 +1022,116 @@ def visit_ArrayAny(self, op, *, arg): def visit_ArrayAll(self, op, *, arg): return self._array_reduction(arg=arg, reduction="logical_and") + # Customized ops for bigframes + + def visit_InMemoryTable(self, op, *, name, schema, data): + # Avoid creating temp tables for small data, which is how memtable is + # used in BigQuery DataFrames. Inspired by: + # https://github.com/ibis-project/ibis/blob/efa6fb72bf4c790450d00a926d7bd809dade5902/ibis/backends/druid/compiler.py#L95 + tuples = data.to_frame().itertuples(index=False) + quoted = self.quoted + columns = [sg.column(col, quoted=quoted) for col in schema.names] + array_expr = sge.DataType( + this=sge.DataType.Type.STRUCT, + expressions=[ + sge.ColumnDef( + this=sge.to_identifier(field, quoted=self.quoted), + kind=bq_datatypes.BigQueryType.from_ibis(type_), + ) + for field, type_ in zip(schema.names, schema.types) + ], + nested=True, + ) + array_values = [ + sge.Tuple( + expressions=tuple( + self.visit_Literal(None, value=value, dtype=type_) + for value, type_ in zip(row, schema.types) + ) + ) + for row in tuples + ] + expr = sge.Unnest( + expressions=[ + sge.DataType( + this=sge.DataType.Type.ARRAY, + expressions=[array_expr], + nested=True, + values=array_values, + ), + ], + alias=sge.TableAlias( + this=sg.to_identifier(name, quoted=quoted), + columns=columns, + ), + ) + # return expr + return sg.select(sge.Star()).from_(expr) + + def visit_ArrayAggregate(self, op, *, arg, order_by, where): + if len(order_by) > 0: + expr = sge.Order( + this=arg, + expressions=[ + # Avoid adding NULLS FIRST / NULLS LAST in SQL, which is + # unsupported in ARRAY_AGG by reconstructing the node as + # plain SQL text. + f"({order_column.args['this'].sql(dialect='bigquery')}) {'DESC' if order_column.args.get('desc') else 'ASC'}" + for order_column in order_by + ], + ) + else: + expr = arg + return sge.IgnoreNulls(this=self.agg.array_agg(expr, where=where)) + + def visit_FirstNonNullValue(self, op, *, arg): + return sge.IgnoreNulls(this=sge.FirstValue(this=arg)) + + def visit_LastNonNullValue(self, op, *, arg): + return sge.IgnoreNulls(this=sge.LastValue(this=arg)) + + def visit_ToJsonString(self, op, *, arg): + return self.f.to_json_string(arg) + + def visit_Quantile(self, op, *, arg, quantile, where): + return sge.PercentileCont(this=arg, expression=quantile) + + def visit_WindowFunction(self, op, *, how, func, start, end, group_by, order_by): + # Patch for https://github.com/ibis-project/ibis/issues/9872 + if start is None and end is None: + spec = None + else: + if start is None: + start = {} + if end is None: + end = {} + + start_value = start.get("value", "UNBOUNDED") + start_side = start.get("side", "PRECEDING") + end_value = end.get("value", "UNBOUNDED") + end_side = end.get("side", "FOLLOWING") + + if getattr(start_value, "this", None) == "0": + start_value = "CURRENT ROW" + start_side = None + + if getattr(end_value, "this", None) == "0": + end_value = "CURRENT ROW" + end_side = None + + spec = sge.WindowSpec( + kind=how.upper(), + start=start_value, + start_side=start_side, + end=end_value, + end_side=end_side, + over="OVER", + ) + spec = self._minimize_spec(op.start, op.end, spec) + + order = sge.Order(expressions=order_by) if order_by else None + + return sge.Window(this=func, partition_by=group_by, order=order, spec=spec) + compiler = BigQueryCompiler() diff --git a/third_party/bigframes_vendored/ibis/backends/sql/rewrites.py b/third_party/bigframes_vendored/ibis/backends/sql/rewrites.py index 1f67902395a..28954003f32 100644 --- a/third_party/bigframes_vendored/ibis/backends/sql/rewrites.py +++ b/third_party/bigframes_vendored/ibis/backends/sql/rewrites.py @@ -1,367 +1,516 @@ # Contains code from https://github.com/ibis-project/ibis/blob/main/ibis/backends/sql/rewrites.py -"""Some common rewrite functions to be shared between backends.""" +"""Lower the ibis expression graph to a SQL-like relational algebra.""" from __future__ import annotations -from collections import defaultdict +from collections.abc import Mapping +from functools import reduce +import operator +from typing import Any, TYPE_CHECKING +from ibis.common.annotations import attribute from ibis.common.collections import FrozenDict # noqa: TCH001 -from ibis.common.deferred import _, deferred, Item, var -from ibis.common.exceptions import ExpressionError, IbisInputError -from ibis.common.graph import Node as Traversable -from ibis.common.graph import traverse -from ibis.common.grounds import Concrete -from ibis.common.patterns import Check, pattern, replace +from ibis.common.deferred import var +import ibis.common.exceptions as com +from ibis.common.graph import Graph +from ibis.common.patterns import InstanceOf, Object, Pattern, replace from ibis.common.typing import VarTuple # noqa: TCH001 +import ibis.expr.datatypes as dt import ibis.expr.operations as ops -from ibis.util import Namespace, promote_list +from ibis.expr.rewrites import d, p, replace_parameter +from ibis.expr.schema import Schema +from public import public import toolz -p = Namespace(pattern, module=ops) -d = Namespace(deferred, module=ops) - +if TYPE_CHECKING: + from collections.abc import Sequence x = var("x") y = var("y") -name = var("name") - - -class DerefMap(Concrete, Traversable): - """Trace and replace fields from earlier relations in the hierarchy. - In order to provide a nice user experience, we need to allow expressions - from earlier relations in the hierarchy. Consider the following example: - t = ibis.table([('a', 'int64'), ('b', 'string')], name='t') - t1 = t.select([t.a, t.b]) - t2 = t1.filter(t.a > 0) # note that not t1.a is referenced here - t3 = t2.select(t.a) # note that not t2.a is referenced here - However the relational operations in the IR are strictly enforcing that - the expressions are referencing the immediate parent only. So we need to - track fields upwards the hierarchy to replace `t.a` with `t1.a` and `t2.a` - in the example above. This is called dereferencing. - Whether we can treat or not a field of a relation semantically equivalent - with a field of an earlier relation in the hierarchy depends on the - `.values` mapping of the relation. Leaf relations, like `t` in the example - above, have an empty `.values` mapping, so we cannot dereference fields - from them. On the other hand a projection, like `t1` in the example above, - has a `.values` mapping like `{'a': t.a, 'b': t.b}`, so we can deduce that - `t1.a` is semantically equivalent with `t.a` and so on. - """ - """The relations we want the values to point to.""" - rels: VarTuple[ops.Relation] - - """Substitution mapping from values of earlier relations to the fields of `rels`.""" - subs: FrozenDict[ops.Value, ops.Field] - - """Ambiguous field references.""" - ambigs: FrozenDict[ops.Value, VarTuple[ops.Value]] - - @classmethod - def from_targets(cls, rels, extra=None): - """Create a dereference map from a list of target relations. - Usually a single relation is passed except for joins where multiple - relations are involved. - Parameters - ---------- - rels : list of ops.Relation - The target relations to dereference to. - extra : dict, optional - Extra substitutions to be added to the dereference map. - Returns - ------- - DerefMap - """ - rels = promote_list(rels) - mapping = defaultdict(dict) - for rel in rels: - for field in rel.fields.values(): - for value, distance in cls.backtrack(field): - mapping[value][field] = distance - - subs, ambigs = {}, {} - for from_, to in mapping.items(): - mindist = min(to.values()) - minkeys = [k for k, v in to.items() if v == mindist] - # if all the closest fields are from the same relation, then we - # can safely substitute them and we pick the first one arbitrarily - if all(minkeys[0].relations == k.relations for k in minkeys): - subs[from_] = minkeys[0] - else: - ambigs[from_] = minkeys - - if extra is not None: - subs.update(extra) - - return cls(rels, subs, ambigs) - - @classmethod - def backtrack(cls, value): - """Backtrack the field in the relation hierarchy. - The field is traced back until no modification is made, so only follow - ops.Field nodes not arbitrary values. - Parameters - ---------- - value : ops.Value - The value to backtrack. - Yields - ------ - tuple[ops.Field, int] - The value node and the distance from the original value. - """ - distance = 0 - # track down the field in the hierarchy until no modification - # is made so only follow ops.Field nodes not arbitrary values; - while isinstance(value, ops.Field): - yield value, distance - value = value.rel.values.get(value.name) - distance += 1 - if ( - value is not None - and value.relations - and not value.find(ops.Impure, filter=ops.Value) - ): - yield value, distance - - def dereference(self, value): - """Dereference a value to the target relations. - Also check for ambiguous field references. If a field reference is found - which is marked as ambiguous, then raise an error. - Parameters - ---------- - value : ops.Value - The value to dereference. - Returns - ------- - ops.Value - The dereferenced value. - """ - ambigs = value.find(lambda x: x in self.ambigs, filter=ops.Value) - if ambigs: - raise IbisInputError( - f"Ambiguous field reference {ambigs!r} in expression {value!r}" - ) - return value.replace(self.subs, filter=ops.Value) - - -def flatten_predicates(node): - """Yield the expressions corresponding to the `And` nodes of a predicate. - Examples - -------- - >>> import ibis - >>> t = ibis.table([("a", "int64"), ("b", "string")], name="t") - >>> filt = (t.a == 1) & (t.b == "foo") - >>> predicates = flatten_predicates(filt.op()) - >>> len(predicates) - 2 - >>> predicates[0].to_expr().name("left") - r0 := UnboundTable: t - a int64 - b string - left: r0.a == 1 - >>> predicates[1].to_expr().name("right") - r0 := UnboundTable: t - a int64 - b string - right: r0.b == 'foo' + +@public +class CTE(ops.Relation): + """Common table expression.""" + + parent: ops.Relation + + @attribute + def schema(self): + return self.parent.schema + + @attribute + def values(self): + return self.parent.values + + +@public +class Select(ops.Relation): + """Relation modelled after SQL's SELECT statement.""" + + parent: ops.Relation + selections: FrozenDict[str, ops.Value] = {} + predicates: VarTuple[ops.Value[dt.Boolean]] = () + qualified: VarTuple[ops.Value[dt.Boolean]] = () + sort_keys: VarTuple[ops.SortKey] = () + + def is_star_selection(self): + return tuple(self.values.items()) == tuple(self.parent.fields.items()) + + @attribute + def values(self): + return self.selections + + @attribute + def schema(self): + return Schema({k: v.dtype for k, v in self.selections.items()}) + + +@public +class FirstValue(ops.Analytic): + """Retrieve the first element.""" + + arg: ops.Column[dt.Any] + + @attribute + def dtype(self): + return self.arg.dtype + + +@public +class LastValue(ops.Analytic): + """Retrieve the last element.""" + + arg: ops.Column[dt.Any] + + @attribute + def dtype(self): + return self.arg.dtype + + +# TODO(kszucs): there is a better strategy to rewrite the relational operations +# to Select nodes by wrapping the leaf nodes in a Select node and then merging +# Project, Filter, Sort, etc. incrementally into the Select node. This way we +# can have tighter control over simplification logic. + + +@replace(p.Project) +def project_to_select(_, **kwargs): + """Convert a Project node to a Select node.""" + return Select(_.parent, selections=_.values) + + +def partition_predicates(predicates): + qualified = [] + unqualified = [] + + for predicate in predicates: + if predicate.find(ops.WindowFunction, filter=ops.Value): + qualified.append(predicate) + else: + unqualified.append(predicate) + + return unqualified, qualified + + +@replace(p.Filter) +def filter_to_select(_, **kwargs): + """Convert a Filter node to a Select node.""" + predicates, qualified = partition_predicates(_.predicates) + return Select( + _.parent, selections=_.values, predicates=predicates, qualified=qualified + ) + + +@replace(p.Sort) +def sort_to_select(_, **kwargs): + """Convert a Sort node to a Select node.""" + return Select(_.parent, selections=_.values, sort_keys=_.keys) + + +if hasattr(p, "DropColumns"): + + @replace(p.DropColumns) + def drop_columns_to_select(_, **kwargs): + """Convert a DropColumns node to a Select node.""" + # if we're dropping fewer than 50% of the parent table's columns then the + # compiled query will likely be smaller than if we list everything *NOT* + # being dropped + if len(_.columns_to_drop) < len(_.schema) // 2: + return _ + return Select(_.parent, selections=_.values) + + +if hasattr(p, "FillNull"): + + @replace(p.FillNull) + def fill_null_to_select(_, **kwargs): + """Rewrite FillNull to a Select node.""" + if isinstance(_.replacements, Mapping): + mapping = _.replacements + else: + mapping = { + name: _.replacements + for name, type in _.parent.schema.items() + if type.nullable + } + + if not mapping: + return _.parent + + selections = {} + for name in _.parent.schema.names: + col = ops.Field(_.parent, name) + if (value := mapping.get(name)) is not None: + col = ops.Alias(ops.Coalesce((col, value)), name) + selections[name] = col + + return Select(_.parent, selections=selections) + + +if hasattr(p, "DropNull"): + + @replace(p.DropNull) + def drop_null_to_select(_, **kwargs): + """Rewrite DropNull to a Select node.""" + if _.subset is None: + columns = [ops.Field(_.parent, name) for name in _.parent.schema.names] + else: + columns = _.subset + + if columns: + preds = [ + reduce( + ops.And if _.how == "any" else ops.Or, + [ops.NotNull(c) for c in columns], + ) + ] + elif _.how == "all": + preds = [ops.Literal(False, dtype=dt.bool)] + else: + return _.parent + + return Select(_.parent, selections=_.values, predicates=tuple(preds)) + + +@replace(p.WindowFunction(p.First | p.Last)) +def first_to_firstvalue(_, **kwargs): + """Convert a First or Last node to a FirstValue or LastValue node.""" + if _.func.where is not None: + raise com.UnsupportedOperationError( + f"`{type(_.func).__name__.lower()}` with `where` is unsupported " + "in a window function" + ) + klass = FirstValue if isinstance(_.func, ops.First) else LastValue + return _.copy(func=klass(_.func.arg)) + + +def complexity(node): + """Assign a complexity score to a node. + + Subsequent projections can be merged into a single projection by replacing + the fields referenced in the outer projection with the computed expressions + from the inner projection. This inlining can result in very complex value + expressions depending on the projections. In order to prevent excessive + inlining, we assign a complexity score to each node. + + The complexity score assigns 1 to each value expression and adds up in the + tree hierarchy unless there is a Field node where we don't add up the + complexity of the referenced relation. This way we treat fields kind of like + reusable variables considering them less complex than they were inlined. """ - def predicate(node): - if isinstance(node, ops.And): - # proceed and don't yield the node - return True, None + def accum(node, *args): + if isinstance(node, ops.Field): + return 1 else: - # halt and yield the node - return False, node - - return list(traverse(predicate, node)) - - -@replace(p.Field(p.JoinChain)) -def peel_join_field(_): - return _.rel.values[_.name] - - -@replace(p.ScalarParameter) -def replace_parameter(_, params, **kwargs): - """Replace scalar parameters with their values.""" - return ops.Literal(value=params[_], dtype=_.dtype) - - -@replace(p.StringSlice) -def lower_stringslice(_, **kwargs): - """Rewrite StringSlice in terms of Substring.""" - if _.end is None: - return ops.Substring(_.arg, start=_.start) - if _.start is None: - return ops.Substring(_.arg, start=0, length=_.end) - if ( - isinstance(_.start, ops.Literal) - and isinstance(_.start.value, int) - and isinstance(_.end, ops.Literal) - and isinstance(_.end.value, int) - ): - # optimization for constant values - length = _.end.value - _.start.value - else: - length = ops.Subtract(_.end, _.start) - return ops.Substring(_.arg, start=_.start, length=length) + return 1 + sum(args) + return node.map_nodes(accum)[node] -@replace(p.Analytic) -def wrap_analytic(_, **__): - # Wrap analytic functions in a window function - return ops.WindowFunction(_) +@replace(Object(Select, Object(Select))) +def merge_select_select(_, **kwargs): + """Merge subsequent Select relations into one. -@replace(p.Reduction) -def project_wrap_reduction(_, rel): - # Query all the tables that the reduction depends on - if _.relations == {rel}: - # The reduction is fully originating from the `rel`, so turn - # it into a window function of `rel` - return ops.WindowFunction(_) - else: - # 1. The reduction doesn't depend on any table, constructed from - # scalar values, so turn it into a scalar subquery. - # 2. The reduction is originating from `rel` and other tables, - # so this is a correlated scalar subquery. - # 3. The reduction is originating entirely from other tables, - # so this is an uncorrelated scalar subquery. - return ops.ScalarSubquery(_.to_expr().as_table()) - - -def rewrite_project_input(value, relation): - # we need to detect reductions which are either turned into window functions - # or scalar subqueries depending on whether they are originating from the - # relation - return value.replace( - wrap_analytic | project_wrap_reduction, - filter=p.Value & ~p.WindowFunction, - context={"rel": relation}, + This rewrites eliminates `_.parent` by merging the outer and the inner + `predicates`, `sort_keys` and keeping the outer `selections`. All selections + from the inner Select are inlined into the outer Select. + """ + # don't merge if either the outer or the inner select has window functions + blocking = ( + ops.WindowFunction, + ops.ExistsSubquery, + ops.InSubquery, + ops.Unnest, + ops.Impure, ) + if _.find_below(blocking, filter=ops.Value): + return _ + if _.parent.find_below(blocking, filter=ops.Value): + return _ + subs = {ops.Field(_.parent, k): v for k, v in _.parent.values.items()} + selections = {k: v.replace(subs, filter=ops.Value) for k, v in _.selections.items()} -ReductionLike = p.Reduction | p.Field(p.Aggregate(groups={})) + predicates = tuple(p.replace(subs, filter=ops.Value) for p in _.predicates) + unique_predicates = toolz.unique(_.parent.predicates + predicates) + qualified = tuple(p.replace(subs, filter=ops.Value) for p in _.qualified) + unique_qualified = toolz.unique(_.parent.qualified + qualified) -@replace(ReductionLike) -def filter_wrap_reduction(_): - # Wrap reductions or fields referencing an aggregation without a group by - - # which are scalar fields - in a scalar subquery. In the latter case we - # use the reduction value from the aggregation. - if isinstance(_, ops.Field): - value = _.rel.values[_.name] - else: - value = _ - return ops.ScalarSubquery(value.to_expr().as_table()) + sort_keys = tuple(s.replace(subs, filter=ops.Value) for s in _.sort_keys) + sort_key_exprs = {s.expr for s in sort_keys} + parent_sort_keys = tuple( + k for k in _.parent.sort_keys if k.expr not in sort_key_exprs + ) + unique_sort_keys = sort_keys + parent_sort_keys + + result = Select( + _.parent.parent, + selections=selections, + predicates=unique_predicates, + qualified=unique_qualified, + sort_keys=unique_sort_keys, + ) + return result if complexity(result) <= complexity(_) else _ -def rewrite_filter_input(value): - return value.replace( - wrap_analytic | filter_wrap_reduction, filter=p.Value & ~p.WindowFunction +def extract_ctes(node: ops.Relation) -> set[ops.Relation]: + cte_types = (Select, ops.Aggregate, ops.JoinChain, ops.Set, ops.Limit, ops.Sample) + dont_count = (ops.Field, ops.CountStar, ops.CountDistinctStar) + + g = Graph.from_bfs(node, filter=~InstanceOf(dont_count)) + result = set() + for op, dependents in g.invert().items(): + if isinstance(op, ops.View) or ( + len(dependents) > 1 and isinstance(op, cte_types) + ): + result.add(op) + + return result + + +def sqlize( + node: ops.Node, + params: Mapping[ops.ScalarParameter, Any], + rewrites: Sequence[Pattern] = (), + fuse_selects: bool = True, +) -> tuple[ops.Node, list[ops.Node]]: + """Lower the ibis expression graph to a SQL-like relational algebra. + + Parameters + ---------- + node + The root node of the expression graph. + params + A mapping of scalar parameters to their values. + rewrites + Supplementary rewrites to apply to the expression graph. + fuse_selects + Whether to merge subsequent Select nodes into one where possible. + + Returns + ------- + Tuple of the rewritten expression graph and a list of CTEs. + + """ + assert isinstance(node, ops.Relation) + + # apply the backend specific rewrites + if rewrites: + node = node.replace(reduce(operator.or_, rewrites)) + + # lower the expression graph to a SQL-like relational algebra + context = {"params": params} + replacements = ( + replace_parameter | project_to_select | filter_to_select | sort_to_select ) + if hasattr(p, "FillNull"): + replacements = replacements | fill_null_to_select + + if hasattr(p, "DropNull"): + replacements = replacements | drop_null_to_select -@replace(p.Analytic | p.Reduction) -def window_wrap_reduction(_, window): - # Wrap analytic and reduction functions in a window function. Used in the - # value.over() API. - return ops.WindowFunction( - _, - how=window.how, - start=window.start, - end=window.end, - group_by=window.groupings, - order_by=window.orderings, + if hasattr(p, "DropColumns"): + replacements = replacements | drop_columns_to_select + + replacements = replacements | first_to_firstvalue + sqlized = node.replace( + replacements, + context=context, ) + # squash subsequent Select nodes into one + if fuse_selects: + simplified = sqlized.replace(merge_select_select) + else: + simplified = sqlized -@replace(p.WindowFunction) -def window_merge_frames(_, window): - # Merge window frames, used in the value.over() and groupby.select() APIs. - if _.how != window.how: - raise ExpressionError( - f"Unable to merge {_.how} window with {window.how} window" - ) - elif _.start and window.start and _.start != window.start: - raise ExpressionError( - "Unable to merge windows with conflicting `start` boundary" - ) - elif _.end and window.end and _.end != window.end: - raise ExpressionError("Unable to merge windows with conflicting `end` boundary") + # extract common table expressions while wrapping them in a CTE node + ctes = extract_ctes(simplified) + + def wrap(node, _, **kwargs): + new = node.__recreate__(kwargs) + return CTE(new) if node in ctes else new + + result = simplified.replace(wrap) + ctes = reversed([cte.parent for cte in result.find(CTE)]) + + return result, ctes + + +# supplemental rewrites selectively used on a per-backend basis + + +@replace(p.WindowFunction(func=p.NTile(y), order_by=())) +def add_order_by_to_empty_ranking_window_functions(_, **kwargs): + """Add an ORDER BY clause to rank window functions that don't have one.""" + return _.copy(order_by=(y,)) + + +"""Replace checks against an empty right side with `False`.""" +empty_in_values_right_side = p.InValues(options=()) >> d.Literal(False, dtype=dt.bool) - start = _.start or window.start - end = _.end or window.end - group_by = tuple(toolz.unique(_.group_by + window.groupings)) - order_keys = {} - for sort_key in window.orderings + _.order_by: - order_keys[sort_key.expr] = sort_key.ascending, sort_key.nulls_first +@replace( + p.WindowFunction(p.RankBase | p.NTile) + | p.StringFind + | p.FindInSet + | p.ArrayPosition +) +def one_to_zero_index(_, **kwargs): + """Subtract one from one-index functions.""" + return ops.Subtract(_, 1) - order_by = ( - ops.SortKey(expr, ascending=ascending, nulls_first=nulls_first) - for expr, (ascending, nulls_first) in order_keys.items() + +@replace(ops.NthValue) +def add_one_to_nth_value_input(_, **kwargs): + if isinstance(_.nth, ops.Literal): + nth = ops.Literal(_.nth.value + 1, dtype=_.nth.dtype) + else: + nth = ops.Add(_.nth, 1) + return _.copy(nth=nth) + + +@replace(p.WindowFunction(order_by=())) +def rewrite_empty_order_by_window(_, **kwargs): + return _.copy(order_by=(ops.NULL,)) + + +@replace(p.WindowFunction(p.RowNumber | p.NTile)) +def exclude_unsupported_window_frame_from_row_number(_, **kwargs): + return ops.Subtract(_.copy(start=None, end=0), 1) + + +@replace(p.WindowFunction(p.MinRank | p.DenseRank, start=None)) +def exclude_unsupported_window_frame_from_rank(_, **kwargs): + return ops.Subtract( + _.copy(start=None, end=0, order_by=_.order_by or (ops.NULL,)), 1 ) - return _.copy(start=start, end=end, group_by=group_by, order_by=order_by) -def rewrite_window_input(value, window): - context = {"window": window} - # if self is a reduction or analytic function, wrap it in a window function - node = value.replace( - window_wrap_reduction, - filter=p.Value & ~p.WindowFunction, - context=context, +@replace( + p.WindowFunction( + p.Lag | p.Lead | p.PercentRank | p.CumeDist | p.Any | p.All, start=None ) - # if self is already a window function, merge the existing window frame - # with the requested window frame - return node.replace(window_merge_frames, filter=p.Value, context=context) - - -# TODO(kszucs): schema comparison should be updated to not distinguish between -# different column order -@replace(p.Project(y @ p.Relation) & Check(_.schema == y.schema)) -def complete_reprojection(_, y): - # TODO(kszucs): this could be moved to the pattern itself but not sure how - # to express it, especially in a shorter way then the following check - for name in _.schema: - if _.values[name] != ops.Field(y, name): - return _ - return y +) +def exclude_unsupported_window_frame_from_ops(_, **kwargs): + return _.copy(start=None, end=0, order_by=_.order_by or (ops.NULL,)) + + +# Rewrite rules for lowering a high-level operation into one composed of more +# primitive operations. + + +@replace(p.Log2) +def lower_log2(_, **kwargs): + """Rewrite `log2` as `log`.""" + return ops.Log(_.arg, base=2) -@replace(p.Project(y @ p.Project)) -def subsequent_projects(_, y): - rule = p.Field(y, name) >> Item(y.values, name) - values = {k: v.replace(rule, filter=ops.Value) for k, v in _.values.items()} - return ops.Project(y.parent, values) +@replace(p.Log10) +def lower_log10(_, **kwargs): + """Rewrite `log10` as `log`.""" + return ops.Log(_.arg, base=10) -@replace(p.Filter(y @ p.Filter)) -def subsequent_filters(_, y): - rule = p.Field(y, name) >> d.Field(y.parent, name) - preds = tuple(v.replace(rule, filter=ops.Value) for v in _.predicates) - return ops.Filter(y.parent, y.predicates + preds) +@replace(p.Bucket) +def lower_bucket(_, **kwargs): + """Rewrite `Bucket` as `SearchedCase`.""" + cases = [] + results = [] + if _.closed == "left": + l_cmp = ops.LessEqual + r_cmp = ops.Less + else: + l_cmp = ops.Less + r_cmp = ops.LessEqual + + user_num_buckets = len(_.buckets) - 1 + + bucket_id = 0 + if _.include_under: + if user_num_buckets > 0: + cmp = ops.Less if _.close_extreme else r_cmp + else: + cmp = ops.LessEqual if _.closed == "right" else ops.Less + cases.append(cmp(_.arg, _.buckets[0])) + results.append(bucket_id) + bucket_id += 1 + + for j, (lower, upper) in enumerate(zip(_.buckets, _.buckets[1:])): + if _.close_extreme and ( + (_.closed == "right" and j == 0) + or (_.closed == "left" and j == (user_num_buckets - 1)) + ): + cases.append( + ops.And(ops.LessEqual(lower, _.arg), ops.LessEqual(_.arg, upper)) + ) + results.append(bucket_id) + else: + cases.append(ops.And(l_cmp(lower, _.arg), r_cmp(_.arg, upper))) + results.append(bucket_id) + bucket_id += 1 -@replace(p.Filter(y @ p.Project)) -def reorder_filter_project(_, y): - rule = p.Field(y, name) >> Item(y.values, name) - preds = tuple(v.replace(rule, filter=ops.Value) for v in _.predicates) + if _.include_over: + if user_num_buckets > 0: + cmp = ops.Less if _.close_extreme else l_cmp + else: + cmp = ops.Less if _.closed == "right" else ops.LessEqual + + cases.append(cmp(_.buckets[-1], _.arg)) + results.append(bucket_id) + bucket_id += 1 + + return ops.SearchedCase( + cases=tuple(cases), results=tuple(results), default=ops.NULL + ) - inner = ops.Filter(y.parent, preds) - rule = p.Field(y.parent, name) >> d.Field(inner, name) - projs = {k: v.replace(rule, filter=ops.Value) for k, v in y.values.items()} - return ops.Project(inner, projs) +@replace(p.Capitalize) +def lower_capitalize(_, **kwargs): + """Rewrite Capitalize in terms of substring, concat, upper, and lower.""" + first = ops.Uppercase(ops.Substring(_.arg, start=0, length=1)) + # use length instead of length - 1 to avoid backends complaining about + # asking for negative length + # + # there are at most length - 1 characters, so asking for length is fine + rest = ops.Lowercase(ops.Substring(_.arg, start=1, length=ops.StringLength(_.arg))) + return ops.StringConcat((first, rest)) -def simplify(node): - # TODO(kszucs): add a utility to the graph module to do rewrites in multiple - # passes after each other - node = node.replace(reorder_filter_project) - node = node.replace(reorder_filter_project) - node = node.replace(subsequent_projects | subsequent_filters) - node = node.replace(complete_reprojection) - return node +@replace(p.Sample) +def lower_sample(_, **kwargs): + """Rewrite Sample as `t.filter(random() <= fraction)`. + + Errors as unsupported if a `seed` is specified. + """ + if _.seed is not None: + raise com.UnsupportedOperationError( + "`Table.sample` with a random seed is unsupported" + ) + return ops.Filter(_.parent, (ops.LessEqual(ops.RandomScalar(), _.fraction),)) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py index 3ae5fc10e42..2c2efe528db 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/__init__.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/__init__.py @@ -2,6 +2,5 @@ from __future__ import annotations from bigframes_vendored.ibis.expr.operations.analytic import * # noqa: F401 F403 -from bigframes_vendored.ibis.expr.operations.arrays import * # noqa: F401 F403 from bigframes_vendored.ibis.expr.operations.json import * # noqa: F401 F403 from bigframes_vendored.ibis.expr.operations.reductions import * # noqa: F401 F403 diff --git a/third_party/bigframes_vendored/ibis/expr/operations/arrays.py b/third_party/bigframes_vendored/ibis/expr/operations/arrays.py deleted file mode 100644 index a0ad915a9be..00000000000 --- a/third_party/bigframes_vendored/ibis/expr/operations/arrays.py +++ /dev/null @@ -1,18 +0,0 @@ -# Contains code from https://github.com/ibis-project/ibis/blob/master/ibis/expr/operations/arrays.py -from __future__ import annotations - -import ibis.expr.datatypes as dt -from ibis.expr.operations.core import Unary - - -class GenerateArray(Unary): - """ - Generates an array of values, similar to ibis.range(), but with simpler and - more efficient SQL generation. - """ - - dtype = dt.Array(dt.int64) - - -class SafeCastToDatetime(Unary): - dtype = dt.Timestamp(timezone=None) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/json.py b/third_party/bigframes_vendored/ibis/expr/operations/json.py index 1eb05541379..ea1f766a71b 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/json.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/json.py @@ -5,5 +5,7 @@ import ibis.expr.operations.core as ibis_ops_core +# TODO(swast): Remove once supported upstream. +# See: https://github.com/ibis-project/ibis/issues/9542 class ToJsonString(ibis_ops_core.Unary): dtype = dt.string diff --git a/third_party/bigframes_vendored/ibis/expr/operations/reductions.py b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py index bd971e408a0..a428c73449c 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/reductions.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/reductions.py @@ -9,17 +9,6 @@ from ibis.expr.operations.reductions import Filterable, Reduction -class ApproximateMultiQuantile(Filterable, Reduction): - """Calculate (approximately) evenly-spaced quantiles. - - See: https://cloud.google.com/bigquery/docs/reference/standard-sql/approximate_aggregate_functions#approx_quantiles - """ - - arg: ibis_ops_core.Value - num_bins: ibis_ops_core.Value[dt.Int64] - dtype = dt.Array(dt.float64) - - class ArrayAggregate(Filterable, Reduction): """ Collects the elements of this expression into an ordered array. Similar to @@ -34,4 +23,4 @@ def dtype(self): return dt.Array(self.arg.dtype) -__all__ = ["ApproximateMultiQuantile", "ArrayAggregate"] +__all__ = ["ArrayAggregate"] diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index fe1c8a12ffb..e7f555c729c 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -544,7 +544,7 @@ def to_dict( >>> df = bpd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) >>> df.to_dict() - {'col1': {0: 1, 1: 2}, 'col2': {0: 3, 1: 4}} + {'col1': {np.int64(0): 1, np.int64(1): 2}, 'col2': {np.int64(0): 3, np.int64(1): 4}} You can specify the return orientation. @@ -1773,7 +1773,7 @@ def iterrows(self): ... }) >>> index, row = next(df.iterrows()) >>> index - 0 + np.int64(0) >>> row A 1 B 4 @@ -1794,7 +1794,7 @@ def itertuples(self, index: bool = True, name: str | None = "Pandas"): ... 'B': [4, 5, 6], ... }) >>> next(df.itertuples(name="Pair")) - Pair(Index=0, A=1, B=4) + Pair(Index=np.int64(0), A=np.int64(1), B=np.int64(4)) Args: index (bool, default True): @@ -6486,12 +6486,12 @@ def iat(self): Get value at specified row/column pair >>> df.iat[1, 2] - 1 + np.int64(1) Get value within a series >>> df.loc[0].iat[1] - 2 + np.int64(2) Returns: bigframes.core.indexers.IatDataFrameIndexer: Indexers object. @@ -6519,12 +6519,12 @@ def at(self): Get value at specified row/column pair >>> df.at[4, 'B'] - 2 + np.int64(2) Get value within a series >>> df.loc[5].at['B'] - 4 + np.int64(4) Returns: bigframes.core.indexers.AtDataFrameIndexer: Indexers object. diff --git a/third_party/bigframes_vendored/pandas/core/series.py b/third_party/bigframes_vendored/pandas/core/series.py index a30ed9cd922..a6363e3285d 100644 --- a/third_party/bigframes_vendored/pandas/core/series.py +++ b/third_party/bigframes_vendored/pandas/core/series.py @@ -205,7 +205,7 @@ def hasnans(self) -> bool: 3 dtype: Float64 >>> s.hasnans - True + np.True_ Returns: bool @@ -592,7 +592,7 @@ def agg(self, func): dtype: Int64 >>> s.agg('min') - 1 + np.int64(1) >>> s.agg(['min', 'max']) min 1 @@ -626,7 +626,7 @@ def count(self): 2 dtype: Float64 >>> s.count() - 2 + np.int64(2) Returns: int or Series (if level specified): Number of non-null values in the @@ -834,12 +834,12 @@ def corr(self, other, method="pearson", min_periods=None) -> float: >>> s1 = bpd.Series([.2, .0, .6, .2]) >>> s2 = bpd.Series([.3, .6, .0, .1]) >>> s1.corr(s2) - -0.8510644963469901 + np.float64(-0.8510644963469901) >>> s1 = bpd.Series([1, 2, 3], index=[0, 1, 2]) >>> s2 = bpd.Series([1, 2, 3], index=[2, 1, 0]) >>> s1.corr(s2) - -1.0 + np.float64(-1.0) Args: other (Series): @@ -870,15 +870,15 @@ def autocorr(self, lag: int = 1) -> float: >>> s = bpd.Series([0.25, 0.5, 0.2, -0.05]) >>> s.autocorr() # doctest: +ELLIPSIS - 0.10355... + np.float64(0.10355263309024067) >>> s.autocorr(lag=2) - -1.0 + np.float64(-1.0) If the Pearson correlation is not well defined, then 'NaN' is returned. >>> s = bpd.Series([1, 0, 0, 0]) >>> s.autocorr() - nan + np.float64(nan) Args: lag (int, default 1): @@ -951,12 +951,12 @@ def dot(self, other) -> Series | np.ndarray: >>> s = bpd.Series([0, 1, 2, 3]) >>> other = bpd.Series([-1, 2, -3, 4]) >>> s.dot(other) - 8 + np.int64(8) You can also use the operator ``@`` for the dot product: >>> s @ other - 8 + np.int64(8) Args: other (Series): @@ -3080,7 +3080,7 @@ def max( 1 3 dtype: Int64 >>> s.max() - 3 + np.int64(3) Calculating the max of a Series containing ``NA`` values: @@ -3091,7 +3091,7 @@ def max( 2 dtype: Int64 >>> s.max() - 3 + np.int64(3) Returns: scalar: Scalar. @@ -3120,7 +3120,7 @@ def min( 1 3 dtype: Int64 >>> s.min() - 1 + np.int64(1) Calculating the min of a Series containing ``NA`` values: @@ -3131,7 +3131,7 @@ def min( 2 dtype: Int64 >>> s.min() - 1 + np.int64(1) Returns: scalar: Scalar. @@ -3207,7 +3207,7 @@ def sum(self): 1 3 dtype: Int64 >>> s.sum() - 4 + np.int64(4) Calculating the sum of a Series containing ``NA`` values: @@ -3218,7 +3218,7 @@ def sum(self): 2 dtype: Int64 >>> s.sum() - 4 + np.int64(4) Returns: scalar: Scalar. @@ -3241,7 +3241,7 @@ def mean(self): 1 3 dtype: Int64 >>> s.mean() - 2.0 + np.float64(2.0) Calculating the mean of a Series containing ``NA`` values: @@ -3252,7 +3252,7 @@ def mean(self): 2 dtype: Int64 >>> s.mean() - 2.0 + np.float64(2.0) Returns: scalar: Scalar. @@ -3285,7 +3285,7 @@ def quantile( >>> bpd.options.display.progress_bar = None >>> s = bpd.Series([1, 2, 3, 4]) >>> s.quantile(.5) - 2.5 + np.float64(2.5) >>> s.quantile([.25, .5, .75]) 0.25 1.75 0.5 2.5 @@ -3576,10 +3576,10 @@ def argmax(self): dtype: Float64 >>> s.argmax() - 2 + np.int64(2) >>> s.argmin() - 0 + np.int64(0) The maximum cereal calories is the third element and the minimum cereal calories is the first element, since series is zero-indexed. @@ -3612,10 +3612,10 @@ def argmin(self): dtype: Float64 >>> s.argmax() - 2 + np.int64(2) >>> s.argmin() - 0 + np.int64(0) The maximum cereal calories is the third element and the minimum cereal calories is the first element, since series is zero-indexed. @@ -3887,11 +3887,11 @@ def is_monotonic_increasing(self) -> bool: >>> s = bpd.Series([1, 2, 2]) >>> s.is_monotonic_increasing - True + np.True_ >>> s = bpd.Series([3, 2, 1]) >>> s.is_monotonic_increasing - False + np.False_ Returns: bool: Boolean. @@ -3910,11 +3910,11 @@ def is_monotonic_decreasing(self) -> bool: >>> s = bpd.Series([3, 2, 2, 1]) >>> s.is_monotonic_decreasing - True + np.True_ >>> s = bpd.Series([1, 2, 3]) >>> s.is_monotonic_decreasing - False + np.False_ Returns: bool: Boolean. @@ -4041,7 +4041,7 @@ def iat(self): Get value at specified row number >>> s.iat[1] - 2 + np.int64(2) Returns: bigframes.core.indexers.IatSeriesIndexer: Indexers object. @@ -4066,7 +4066,7 @@ def at(self): Get value at specified row label >>> s.at['B'] - 2 + np.int64(2) Returns: @@ -4314,7 +4314,7 @@ def __getitem__(self, indexer): >>> s = bpd.Series([15, 30, 45]) >>> s[1] - 30 + np.int64(30) >>> s[0:2] 0 15 1 30 diff --git a/third_party/bigframes_vendored/sklearn/metrics/_classification.py b/third_party/bigframes_vendored/sklearn/metrics/_classification.py index 8e8b2c1952e..c1a909e8490 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_classification.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_classification.py @@ -36,13 +36,13 @@ def accuracy_score(y_true, y_pred, normalize=True) -> float: >>> y_pred = bpd.DataFrame([0, 1, 2, 3]) >>> accuracy_score = bigframes.ml.metrics.accuracy_score(y_true, y_pred) >>> accuracy_score - 0.5 + np.float64(0.5) If False, return the number of correctly classified samples: >>> accuracy_score = bigframes.ml.metrics.accuracy_score(y_true, y_pred, normalize=False) >>> accuracy_score - 2 + np.int64(2) Args: y_true (Series or DataFrame of shape (n_samples,)): diff --git a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py index dee8b350c0e..7b97526de23 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_ranking.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_ranking.py @@ -37,7 +37,7 @@ def auc(x, y) -> float: >>> y = bpd.DataFrame([2, 3, 4, 5]) >>> auc = bigframes.ml.metrics.auc(x, y) >>> auc - 3.5 + np.float64(3.5) The input can be Series: @@ -47,7 +47,7 @@ def auc(x, y) -> float: ... ) >>> auc = bigframes.ml.metrics.auc(df["x"], df["y"]) >>> auc - 3.5 + np.float64(3.5) Args: @@ -77,7 +77,7 @@ def roc_auc_score(y_true, y_score) -> float: >>> y_score = bpd.DataFrame([0.1, 0.4, 0.35, 0.8, 0.65, 0.9, 0.5, 0.3, 0.6, 0.45]) >>> roc_auc_score = bigframes.ml.metrics.roc_auc_score(y_true, y_score) >>> roc_auc_score - 0.625 + np.float64(0.625) The input can be Series: @@ -87,7 +87,7 @@ def roc_auc_score(y_true, y_score) -> float: ... ) >>> roc_auc_score = bigframes.ml.metrics.roc_auc_score(df["y_true"], df["y_score"]) >>> roc_auc_score - 0.625 + np.float64(0.625) Args: y_true (Series or DataFrame of shape (n_samples,)): diff --git a/third_party/bigframes_vendored/sklearn/metrics/_regression.py b/third_party/bigframes_vendored/sklearn/metrics/_regression.py index c3e579bd299..56f78c6d0b5 100644 --- a/third_party/bigframes_vendored/sklearn/metrics/_regression.py +++ b/third_party/bigframes_vendored/sklearn/metrics/_regression.py @@ -52,7 +52,7 @@ def r2_score(y_true, y_pred, force_finite=True) -> float: >>> y_pred = bpd.DataFrame([2.5, 0.0, 2, 8]) >>> r2_score = bigframes.ml.metrics.r2_score(y_true, y_pred) >>> r2_score - 0.9486081370449679 + np.float64(0.9486081370449679) Args: y_true (Series or DataFrame of shape (n_samples,)): @@ -79,7 +79,7 @@ def mean_squared_error(y_true, y_pred) -> float: >>> y_pred = bpd.DataFrame([2.5, 0.0, 2, 8]) >>> mse = bigframes.ml.metrics.mean_squared_error(y_true, y_pred) >>> mse - 0.375 + np.float64(0.375) Args: y_true (Series or DataFrame of shape (n_samples,)): From 596b03bb3ea27cead9b90200b9ef3cdcd99ca184 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Fri, 13 Sep 2024 15:54:22 -0700 Subject: [PATCH 829/921] fix: Fix a bug that raises exception when re-indexing columns with their original order (#988) --- bigframes/dataframe.py | 5 +++++ tests/system/small/test_dataframe.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d7738a569d4..29f22c28b90 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1913,6 +1913,11 @@ def _reindex_rows( def _reindex_columns(self, columns): block = self._block new_column_index, indexer = self.columns.reindex(columns) + + if indexer is None: + # The new index is the same as the old one. Do nothing. + return self + result_cols = [] for label, index in zip(columns, indexer): if index >= 0: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 9e046dc62ee..b9291085fab 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -3664,6 +3664,21 @@ def test_df_reindex_columns(scalars_df_index, scalars_pandas_df_index): ) +def test_df_reindex_columns_with_same_order(scalars_df_index, scalars_pandas_df_index): + # First, make sure the two dataframes have the same columns in order. + columns = ["int64_col", "int64_too"] + bf = scalars_df_index[columns] + pd_df = scalars_pandas_df_index[columns] + + bf_result = bf.reindex(columns=columns).to_pandas() + pd_result = pd_df.reindex(columns=columns) + + pd.testing.assert_frame_equal( + bf_result, + pd_result, + ) + + def test_df_equals_identical(scalars_df_index, scalars_pandas_df_index): unsupported = [ "geography_col", From deac6d2d6e459b26c05f6e5ff328ea03a3cff45f Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Mon, 16 Sep 2024 11:09:11 -0700 Subject: [PATCH 830/921] feat: add "include" param to describe for string types (#973) --- bigframes/dataframe.py | 88 ++++++++++++++++----- bigframes/operations/aggregations.py | 11 +++ tests/system/small/test_dataframe.py | 81 +++++++++++++++++++ tests/unit/operations/__init__.py | 13 +++ tests/unit/operations/test_aggregations.py | 92 ++++++++++++++++++++++ 5 files changed, 267 insertions(+), 18 deletions(-) create mode 100644 tests/unit/operations/__init__.py create mode 100644 tests/unit/operations/test_aggregations.py diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 29f22c28b90..c4597ab8434 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -18,6 +18,7 @@ import datetime import inspect +import itertools import re import sys import textwrap @@ -70,6 +71,7 @@ import bigframes.exceptions import bigframes.formatting_helpers as formatter import bigframes.operations as ops +import bigframes.operations.aggregations import bigframes.operations.aggregations as agg_ops import bigframes.operations.plotting as plotting import bigframes.operations.structs @@ -2207,14 +2209,17 @@ def agg( self, func: str | typing.Sequence[str] ) -> DataFrame | bigframes.series.Series: if utils.is_list_like(func): - if any( - dtype not in bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE - for dtype in self.dtypes - ): - raise NotImplementedError( - f"Multiple aggregations only supported on numeric columns. {constants.FEEDBACK_LINK}" - ) aggregations = [agg_ops.lookup_agg_func(f) for f in func] + + for dtype, agg in itertools.product(self.dtypes, aggregations): + if not bigframes.operations.aggregations.is_agg_op_supported( + dtype, agg + ): + raise NotImplementedError( + f"Type {dtype} does not support aggregation {agg}. " + f"Share your usecase with the BigQuery DataFrames team at the {constants.FEEDBACK_LINK}" + ) + return DataFrame( self._block.summarize( self._block.value_columns, @@ -2280,16 +2285,55 @@ def melt( self._block.melt(id_col_ids, val_col_ids, var_name, value_name) ) - def describe(self) -> DataFrame: - df_numeric = self._drop_non_numeric(permissive=False) - if len(df_numeric.columns) == 0: - raise NotImplementedError( - f"df.describe() currently only supports numeric values. {constants.FEEDBACK_LINK}" + _NUMERICAL_DISCRIBE_AGGS = ( + "count", + "mean", + "std", + "min", + "25%", + "50%", + "75%", + "max", + ) + _NON_NUMERICAL_DESCRIBE_AGGS = ("count", "nunique") + + def describe(self, include: None | Literal["all"] = None) -> DataFrame: + if include is None: + numeric_df = self._drop_non_numeric(permissive=False) + if len(numeric_df.columns) == 0: + # Describe eligible non-numerical columns + result = self._drop_non_string().agg(self._NON_NUMERICAL_DESCRIBE_AGGS) + else: + # Otherwise, only describe numerical columns + result = numeric_df.agg(self._NUMERICAL_DISCRIBE_AGGS) + return typing.cast(DataFrame, result) + + elif include == "all": + numeric_result = typing.cast( + DataFrame, + self._drop_non_numeric(permissive=False).agg( + self._NUMERICAL_DISCRIBE_AGGS + ), + ) + string_result = typing.cast( + DataFrame, + self._drop_non_string().agg(self._NON_NUMERICAL_DESCRIBE_AGGS), ) - result = df_numeric.agg( - ["count", "mean", "std", "min", "25%", "50%", "75%", "max"] - ) - return typing.cast(DataFrame, result) + + if len(numeric_result.columns) == 0: + return string_result + elif len(string_result.columns) == 0: + return numeric_result + else: + import bigframes.core.reshape as rs + + # Use reindex after join to preserve the original column order. + return rs.concat( + [numeric_result, string_result], axis=1 + )._reindex_columns(self.columns) + + else: + raise ValueError(f"Unsupported include type: {include}") def skew(self, *, numeric_only: bool = False): if not numeric_only: @@ -2487,7 +2531,7 @@ def unstack(self, level: LevelsType = -1): return DataFrame(pivot_block) def _drop_non_numeric(self, permissive=True) -> DataFrame: - types_to_keep = ( + numerical_types = ( set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) if permissive else set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_RESTRICTIVE) @@ -2495,10 +2539,18 @@ def _drop_non_numeric(self, permissive=True) -> DataFrame: non_numeric_cols = [ col_id for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) - if dtype not in types_to_keep + if dtype not in numerical_types ] return DataFrame(self._block.drop_columns(non_numeric_cols)) + def _drop_non_string(self) -> DataFrame: + string_cols = [ + col_id + for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) + if dtype == bigframes.dtypes.STRING_DTYPE + ] + return DataFrame(self._block.select_columns(string_cols)) + def _drop_non_bool(self) -> DataFrame: non_bool_cols = [ col_id diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index 675ead11889..f20429e4498 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -562,3 +562,14 @@ def lookup_agg_func(key: str) -> typing.Union[UnaryAggregateOp, NullaryAggregate return _AGGREGATIONS_LOOKUP[key] else: raise ValueError(f"Unrecognize aggregate function: {key}") + + +def is_agg_op_supported(dtype: dtypes.Dtype, op: AggregateOp) -> bool: + if dtype in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: + return True + + if dtype == dtypes.STRING_DTYPE: + return isinstance(op, (CountOp, NuniqueOp)) + + # For all other types, support no aggregation + return False diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index b9291085fab..fe63a1ed287 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2612,6 +2612,87 @@ def test_df_describe(scalars_dfs): ).all() +@skip_legacy_pandas +@pytest.mark.parametrize("include", [None, "all"]) +def test_df_describe_non_numerical(scalars_dfs, include): + scalars_df, scalars_pandas_df = scalars_dfs + + non_numerical_columns = ["string_col"] + + modified_bf = scalars_df[non_numerical_columns] + bf_result = modified_bf.describe(include=include).to_pandas() + + modified_pd_df = scalars_pandas_df[non_numerical_columns] + pd_result = modified_pd_df.describe(include=include) + + # Reindex results with the specified keys and their order, because + # the relative order is not important. + bf_result = bf_result.reindex(["count", "nunique"]) + pd_result = pd_result.reindex( + ["count", "unique"] + # BF counter part of "unique" is called "nunique" + ).rename(index={"unique": "nunique"}) + + pd.testing.assert_frame_equal( + pd_result[non_numerical_columns].astype("Int64"), + bf_result[non_numerical_columns], + check_index_type=False, + ) + + +@skip_legacy_pandas +def test_df_describe_mixed_types_include_all(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + numerical_columns = [ + "int64_col", + "float64_col", + ] + non_numerical_columns = ["string_col"] + supported_columns = numerical_columns + non_numerical_columns + + modified_bf = scalars_df[supported_columns] + bf_result = modified_bf.describe(include="all").to_pandas() + + modified_pd_df = scalars_pandas_df[supported_columns] + pd_result = modified_pd_df.describe(include="all") + + # Drop quartiles, as they are approximate + bf_min = bf_result.loc["min", :] + bf_p25 = bf_result.loc["25%", :] + bf_p50 = bf_result.loc["50%", :] + bf_p75 = bf_result.loc["75%", :] + bf_max = bf_result.loc["max", :] + + # Reindex results with the specified keys and their order, because + # the relative order is not important. + bf_result = bf_result.reindex(["count", "nunique", "mean", "std", "min", "max"]) + pd_result = pd_result.reindex( + ["count", "unique", "mean", "std", "min", "max"] + # BF counter part of "unique" is called "nunique" + ).rename(index={"unique": "nunique"}) + + pd.testing.assert_frame_equal( + pd_result[numerical_columns].astype("Float64"), + bf_result[numerical_columns], + check_index_type=False, + ) + + pd.testing.assert_frame_equal( + pd_result[non_numerical_columns].astype("Int64"), + bf_result[non_numerical_columns], + check_index_type=False, + ) + + # Double-check that quantiles are at least plausible. + assert ( + (bf_min <= bf_p25) + & (bf_p25 <= bf_p50) + & (bf_p50 <= bf_p50) + & (bf_p75 <= bf_max) + ).all() + + def test_df_transpose(): # Include some floats to ensure type coercion values = [[0, 3.5, True], [1, 4.5, False], [2, 6.5, None]] diff --git a/tests/unit/operations/__init__.py b/tests/unit/operations/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/tests/unit/operations/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unit/operations/test_aggregations.py b/tests/unit/operations/test_aggregations.py new file mode 100644 index 00000000000..4cb6934c9d3 --- /dev/null +++ b/tests/unit/operations/test_aggregations.py @@ -0,0 +1,92 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.dtypes as dtypes +from bigframes.operations.aggregations import ( + all_op, + any_op, + count_op, + dense_rank_op, + first_op, + is_agg_op_supported, + max_op, + mean_op, + median_op, + min_op, + nunique_op, + product_op, + rank_op, + size_op, + std_op, + sum_op, + var_op, +) + +_ALL_OPS = set( + [ + size_op, + sum_op, + mean_op, + median_op, + product_op, + max_op, + min_op, + std_op, + var_op, + count_op, + nunique_op, + rank_op, + dense_rank_op, + all_op, + any_op, + first_op, + ] +) +_STRING_SUPPORTED_OPS = set([count_op, nunique_op]) + + +@pytest.mark.parametrize("dtype", dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) +@pytest.mark.parametrize("op", _ALL_OPS) +def test_is_agg_op_supported_numerical_support_all(dtype, op): + assert is_agg_op_supported(dtype, op) is True + + +@pytest.mark.parametrize("dtype", [dtypes.STRING_DTYPE]) +@pytest.mark.parametrize("op", _STRING_SUPPORTED_OPS) +def test_is_agg_op_supported_string_support_ops(dtype, op): + assert is_agg_op_supported(dtype, op) is True + + +@pytest.mark.parametrize("dtype", [dtypes.STRING_DTYPE]) +@pytest.mark.parametrize("op", _ALL_OPS - _STRING_SUPPORTED_OPS) +def test_is_agg_op_supported_string_not_support_ops(dtype, op): + assert is_agg_op_supported(dtype, op) is False + + +@pytest.mark.parametrize( + "dtype", + [ + dtypes.BYTES_DTYPE, + dtypes.DATE_DTYPE, + dtypes.TIME_DTYPE, + dtypes.DATETIME_DTYPE, + dtypes.TIMESTAMP_DTYPE, + dtypes.GEO_DTYPE, + ], +) +@pytest.mark.parametrize("op", _ALL_OPS) +def test_is_agg_op_supported_non_numerical_no_support(dtype, op): + assert is_agg_op_supported(dtype, op) is False From f7c03dcaf7ee4d62497f6653851e390795fc60a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 16 Sep 2024 14:22:26 -0500 Subject: [PATCH 831/921] feat: add `subset` parameter to `DataFrame.dropna` to select which columns to consider (#981) * feat: add `subset` parameter to `DataFrame.dropna` to select which columns to consider * fix dropna with subset=None * refactor: remove circular dependencies preventing local doctest runs With this change I can once again run ``` pytest --doctest-modules third_party/bigframes_vendored/pandas/core/frame.py ``` Note: having multiple `version.py` files should be fine. release-please will update all such files it finds. * fix doctest * Revert "Merge branch 'tswast-circular-import' into b366248570-dropna-subset" This reverts commit 57e8335af83558323d4df213854efc1edcdb35f0, reversing changes made to 197074abc53c5a85c66ec1940e54ea3f2c9c3677. * Reapply "Merge branch 'tswast-circular-import' into b366248570-dropna-subset" This reverts commit 0f18294513a2a6a806f5fff4ff38646d4029a884. * loop over tuple result --------- Co-authored-by: Huan Chen <142538604+Genesis929@users.noreply.github.com> --- bigframes/core/block_transforms.py | 12 ++++++++-- bigframes/dataframe.py | 22 +++++++++++++++++-- tests/system/small/test_dataframe.py | 21 +++++++++++------- tests/unit/test_dataframe.py | 9 ++++++++ .../bigframes_vendored/pandas/core/frame.py | 17 ++++++++++++++ 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 1990647e0a2..398c6ab26ae 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -15,7 +15,7 @@ import functools import typing -from typing import Sequence +from typing import Optional, Sequence import bigframes_vendored.constants as constants import pandas as pd @@ -488,11 +488,19 @@ def dropna( block: blocks.Block, column_ids: typing.Sequence[str], how: typing.Literal["all", "any"] = "any", + subset: Optional[typing.Sequence[str]] = None, ): """ Drop na entries from block """ - predicates = [ops.notnull_op.as_expr(column_id) for column_id in column_ids] + if subset is None: + subset = column_ids + + predicates = [ + ops.notnull_op.as_expr(column_id) + for column_id in column_ids + if column_id in subset + ] if len(predicates) == 0: return block if how == "any": diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index c4597ab8434..c2fb9336f3e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2027,8 +2027,9 @@ def dropna( self, *, axis: int | str = 0, - inplace: bool = False, how: str = "any", + subset: typing.Union[None, blocks.Label, Sequence[blocks.Label]] = None, + inplace: bool = False, ignore_index=False, ) -> DataFrame: if inplace: @@ -2040,8 +2041,25 @@ def dropna( axis_n = utils.get_axis_number(axis) + if subset is not None and axis_n != 0: + raise NotImplementedError( + f"subset only supported when axis=0. {constants.FEEDBACK_LINK}" + ) + if axis_n == 0: - result = block_ops.dropna(self._block, self._block.value_columns, how=how) # type: ignore + # subset needs to be converted into column IDs, not column labels. + if subset is None: + subset_ids = None + elif not utils.is_list_like(subset): + subset_ids = [id_ for id_ in self._block.label_to_col_id[subset]] + else: + subset_ids = [ + id_ + for label in subset + for id_ in self._block.label_to_col_id[label] + ] + + result = block_ops.dropna(self._block, self._block.value_columns, how=how, subset=subset_ids) # type: ignore if ignore_index: result = result.reset_index() return DataFrame(result) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index fe63a1ed287..b4c81bfbef5 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -936,19 +936,24 @@ def test_assign_callable_lambda(scalars_dfs): @skip_legacy_pandas @pytest.mark.parametrize( - ("axis", "how", "ignore_index"), + ("axis", "how", "ignore_index", "subset"), [ - (0, "any", False), - (0, "any", True), - (1, "any", False), - (1, "all", False), + (0, "any", False, None), + (0, "any", True, None), + (0, "all", False, ["bool_col", "time_col"]), + (0, "any", False, ["bool_col", "time_col"]), + (0, "all", False, "time_col"), + (1, "any", False, None), + (1, "all", False, None), ], ) -def test_df_dropna(scalars_dfs, axis, how, ignore_index): +def test_df_dropna(scalars_dfs, axis, how, ignore_index, subset): scalars_df, scalars_pandas_df = scalars_dfs - df = scalars_df.dropna(axis=axis, how=how, ignore_index=ignore_index) + df = scalars_df.dropna(axis=axis, how=how, ignore_index=ignore_index, subset=subset) bf_result = df.to_pandas() - pd_result = scalars_pandas_df.dropna(axis=axis, how=how, ignore_index=ignore_index) + pd_result = scalars_pandas_df.dropna( + axis=axis, how=how, ignore_index=ignore_index, subset=subset + ) # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) diff --git a/tests/unit/test_dataframe.py b/tests/unit/test_dataframe.py index 6370d1b9876..560c0cf0f40 100644 --- a/tests/unit/test_dataframe.py +++ b/tests/unit/test_dataframe.py @@ -20,6 +20,15 @@ from . import resources +def test_dataframe_dropna_axis_1_subset_not_implememented( + monkeypatch: pytest.MonkeyPatch, +): + dataframe = resources.create_dataframe(monkeypatch) + + with pytest.raises(NotImplementedError, match="subset"): + dataframe.dropna(axis=1, subset=["col1", "col2"]) + + def test_dataframe_repr_with_uninitialized_object(): """Ensures DataFrame.__init__ can be paused in a visual debugger without crashing. diff --git a/third_party/bigframes_vendored/pandas/core/frame.py b/third_party/bigframes_vendored/pandas/core/frame.py index e7f555c729c..970883257c9 100644 --- a/third_party/bigframes_vendored/pandas/core/frame.py +++ b/third_party/bigframes_vendored/pandas/core/frame.py @@ -1614,6 +1614,8 @@ def dropna( *, axis: int | str = 0, how: str = "any", + subset=None, + inplace: bool = False, ignore_index=False, ) -> DataFrame: """Remove missing values. @@ -1662,6 +1664,15 @@ def dropna( [3 rows x 3 columns] + Define in which columns to look for missing values. + + >>> df.dropna(subset=['name', 'toy']) + name toy born + 1 Batman Batmobile 1940-04-25 + 2 Catwoman Bullwhip + + [2 rows x 3 columns] + Args: axis ({0 or 'index', 1 or 'columns'}, default 'columns'): Determine if rows or columns which contain missing values are @@ -1675,6 +1686,12 @@ def dropna( * 'any' : If any NA values are present, drop that row or column. * 'all' : If all values are NA, drop that row or column. + subset (column label or sequence of labels, optional): + Labels along other axis to consider, e.g. if you are dropping + rows these would be a list of columns to include. + Only supports axis=0. + inplace (bool, default ``False``): + Not supported. ignore_index (bool, default ``False``): If ``True``, the resulting axis will be labeled 0, 1, …, n - 1. From b00db7eb61752f4f93b0244c8eb72af7d24c6524 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 16 Sep 2024 12:52:59 -0700 Subject: [PATCH 832/921] refactor: Make column id namespaces explicit (#982) --- bigframes/core/__init__.py | 89 +++++++++++++++++++++--------- bigframes/core/blocks.py | 72 +++++++++--------------- bigframes/core/compile/compiled.py | 4 +- bigframes/core/compile/compiler.py | 12 ++-- bigframes/core/nodes.py | 54 +++++++++++++++--- bigframes/dataframe.py | 3 +- bigframes/session/executor.py | 10 ++-- bigframes/session/loader.py | 3 +- tests/unit/test_planner.py | 27 +++++---- 9 files changed, 165 insertions(+), 109 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index aa3aa63cc2f..8ad00a46c61 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -18,7 +18,7 @@ import functools import io import typing -from typing import Iterable, Optional, Sequence, Tuple +from typing import Iterable, List, Optional, Sequence, Tuple import warnings import google.cloud.bigquery @@ -106,6 +106,7 @@ def from_table( @property def column_ids(self) -> typing.Sequence[str]: + """Returns column ids as strings.""" return self.schema.names @property @@ -182,10 +183,11 @@ def order_by(self, by: Sequence[OrderingExpression]) -> ArrayValue: def reversed(self) -> ArrayValue: return ArrayValue(nodes.ReversedNode(child=self.node)) - def promote_offsets(self, col_id: str) -> ArrayValue: + def promote_offsets(self) -> Tuple[ArrayValue, str]: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. """ + col_id = self._gen_namespaced_uid() if self.node.order_ambiguous and not (self.session._strictly_ordered): if not self.session._allows_ambiguity: raise ValueError( @@ -197,7 +199,10 @@ def promote_offsets(self, col_id: str) -> ArrayValue: bigframes.exceptions.AmbiguousWindowWarning, ) - return ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)) + return ( + ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)), + col_id, + ) def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: """Append together multiple ArrayValue objects.""" @@ -205,13 +210,19 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: nodes.ConcatNode(children=tuple([self.node, *[val.node for val in other]])) ) - def compute_values(self, assignments: Sequence[Tuple[ex.Expression, str]]): - return ArrayValue( - nodes.ProjectionNode(child=self.node, assignments=tuple(assignments)) + def compute_values(self, assignments: Sequence[ex.Expression]): + col_ids = self._gen_namespaced_uids(len(assignments)) + ex_id_pairs = tuple((ex, id) for ex, id in zip(assignments, col_ids)) + return ( + ArrayValue(nodes.ProjectionNode(child=self.node, assignments=ex_id_pairs)), + col_ids, ) - def project_to_id(self, expression: ex.Expression, output_id: str): - return self.compute_values(((expression, output_id),)) + def project_to_id(self, expression: ex.Expression): + array_val, ids = self.compute_values( + [expression], + ) + return array_val, ids[0] def assign(self, source_id: str, destination_id: str) -> ArrayValue: if destination_id in self.column_ids: # Mutate case @@ -234,19 +245,22 @@ def assign(self, source_id: str, destination_id: str) -> ArrayValue: def create_constant( self, - destination_id: str, value: typing.Any, dtype: typing.Optional[bigframes.dtypes.Dtype], - ) -> ArrayValue: + ) -> Tuple[ArrayValue, str]: + destination_id = self._gen_namespaced_uid() if pandas.isna(value): # Need to assign a data type when value is NaN. dtype = dtype or bigframes.dtypes.DEFAULT_DTYPE - return ArrayValue( - nodes.ProjectionNode( - child=self.node, - assignments=((ex.const(value, dtype), destination_id),), - ) + return ( + ArrayValue( + nodes.ProjectionNode( + child=self.node, + assignments=((ex.const(value, dtype), destination_id),), + ) + ), + destination_id, ) def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: @@ -297,11 +311,10 @@ def project_window_op( column_name: str, op: agg_ops.UnaryWindowOp, window_spec: WindowSpec, - output_name=None, *, never_skip_nulls=False, skip_reproject_unsafe: bool = False, - ) -> ArrayValue: + ) -> Tuple[ArrayValue, str]: """ Creates a new expression based on this expression with unary operation applied to one column. column_name: the id of the input column present in the expression @@ -324,16 +337,20 @@ def project_window_op( bigframes.exceptions.AmbiguousWindowWarning, ) - return ArrayValue( - nodes.WindowOpNode( - child=self.node, - column_name=column_name, - op=op, - window_spec=window_spec, - output_name=output_name, - never_skip_nulls=never_skip_nulls, - skip_reproject_unsafe=skip_reproject_unsafe, - ) + output_name = self._gen_namespaced_uid() + return ( + ArrayValue( + nodes.WindowOpNode( + child=self.node, + column_name=column_name, + op=op, + window_spec=window_spec, + output_name=output_name, + never_skip_nulls=never_skip_nulls, + skip_reproject_unsafe=skip_reproject_unsafe, + ) + ), + output_name, ) def _reproject_to_table(self) -> ArrayValue: @@ -410,3 +427,21 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: def get_offset_for_name(self, name: str): return self.schema.names.index(name) + + # Deterministically generate namespaced ids for new variables + # These new ids are only unique within the current namespace. + # Many operations, such as joins, create new namespaces. See: BigFrameNode.defines_namespace + # When migrating to integer ids, these will generate the next available integer, in order to densely pack ids + # this will help represent variables sets as compact bitsets + def _gen_namespaced_uid(self) -> str: + return self._gen_namespaced_uids(1)[0] + + def _gen_namespaced_uids(self, n: int) -> List[str]: + i = len(self.node.defined_variables) + genned_ids: List[str] = [] + while len(genned_ids) < n: + attempted_id = f"col_{i}" + if attempted_id not in self.node.defined_variables: + genned_ids.append(attempted_id) + i = i + 1 + return genned_ids diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 39cfed588f7..7f6f5f1cc9e 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -336,8 +336,7 @@ def reset_index(self, drop: bool = True) -> Block: self.session._default_index_type == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 ): - new_index_col_id = guid.generate_guid() - expr = expr.promote_offsets(new_index_col_id) + expr, new_index_col_id = expr.promote_offsets() new_index_cols = [new_index_col_id] elif self.session._default_index_type == bigframes.enums.DefaultIndexKind.NULL: new_index_cols = [] @@ -846,9 +845,7 @@ def project_expr( """ Apply a scalar expression to the block. Creates a new column to store the result. """ - # TODO(tbergeron): handle labels safely so callers don't need to - result_id = guid.generate_guid() - array_val = self._expr.project_to_id(expr, result_id) + array_val, result_id = self._expr.project_to_id(expr) block = Block( array_val, index_columns=self.index_columns, @@ -971,12 +968,10 @@ def apply_window_op( for key in window_spec.grouping_keys: block, not_null_id = block.apply_unary_op(key, ops.notnull_op) block = block.filter_by_id(not_null_id).drop_columns([not_null_id]) - result_id = guid.generate_guid() - expr = block._expr.project_window_op( + expr, result_id = block._expr.project_window_op( column, op, window_spec, - result_id, skip_reproject_unsafe=skip_reproject_unsafe, never_skip_nulls=never_skip_nulls, ) @@ -1005,8 +1000,7 @@ def create_constant( label: Label = None, dtype: typing.Optional[bigframes.dtypes.Dtype] = None, ) -> typing.Tuple[Block, str]: - result_id = guid.generate_guid() - expr = self.expr.create_constant(result_id, scalar_constant, dtype=dtype) + expr, result_id = self.expr.create_constant(scalar_constant, dtype=dtype) # Create index copy with label inserted # See: https://pandas.pydata.org/docs/reference/api/pandas.Index.insert.html labels = self.column_labels.insert(len(self.column_labels), label) @@ -1063,10 +1057,9 @@ def aggregate_all_and_stack( ) for col_id in self.value_columns ] - index_id = guid.generate_guid() - result_expr = self.expr.aggregate( + result_expr, index_id = self.expr.aggregate( aggregations, dropna=dropna - ).create_constant(index_id, None, None) + ).create_constant(None, None) # Transpose as last operation so that final block has valid transpose cache return Block( result_expr, @@ -1077,8 +1070,7 @@ def aggregate_all_and_stack( else: # axis_n == 1 # using offsets as identity to group on. # TODO: Allow to promote identity/total_order columns instead for better perf - offset_col = guid.generate_guid() - expr_with_offsets = self.expr.promote_offsets(offset_col) + expr_with_offsets, offset_col = self.expr.promote_offsets() stacked_expr, (_, value_col_ids, passthrough_cols,) = unpivot( expr_with_offsets, row_labels=self.column_labels, @@ -1224,8 +1216,7 @@ def aggregate( names: typing.List[Label] = [] if len(by_column_ids) == 0: - label_id = guid.generate_guid() - result_expr = result_expr.create_constant(label_id, 0, pd.Int64Dtype()) + result_expr, label_id = result_expr.create_constant(0, pd.Int64Dtype()) index_columns = (label_id,) names = [None] else: @@ -1275,8 +1266,7 @@ def get_stat( for stat in stats_to_fetch ] expr = self.expr.aggregate(aggregations) - offset_index_id = guid.generate_guid() - expr = expr.promote_offsets(offset_index_id) + expr, offset_index_id = expr.promote_offsets() block = Block( expr, index_columns=[offset_index_id], @@ -1303,8 +1293,7 @@ def get_binary_stat( ) ] expr = self.expr.aggregate(aggregations) - offset_index_id = guid.generate_guid() - expr = expr.promote_offsets(offset_index_id) + expr, offset_index_id = expr.promote_offsets() block = Block( expr, index_columns=[offset_index_id], @@ -1406,9 +1395,10 @@ def explode( expr = self.expr.explode(column_ids) if ignore_index: - new_index_ids = guid.generate_guid() + expr = expr.drop_columns(self.index_columns) + expr, new_index_ids = expr.promote_offsets() return Block( - expr.drop_columns(self.index_columns).promote_offsets(new_index_ids), + expr, column_labels=self.column_labels, # Initiates default index creation using the block constructor. index_columns=[new_index_ids], @@ -1593,8 +1583,7 @@ def retrieve_repr_request_results( return computed_df, count, query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: - result_id = guid.generate_guid() - expr = self._expr.promote_offsets(result_id) + expr, result_id = self._expr.promote_offsets() return ( Block( expr, @@ -1611,13 +1600,11 @@ def add_prefix(self, prefix: str, axis: str | int | None = None) -> Block: expr = self._expr new_index_cols = [] for index_col in self._index_columns: - new_col = guid.generate_guid() - expr = expr.project_to_id( + expr, new_col = expr.project_to_id( expression=ops.add_op.as_expr( ex.const(prefix), ops.AsTypeOp(to_type="string").as_expr(index_col), ), - output_id=new_col, ) new_index_cols.append(new_col) expr = expr.select_columns((*new_index_cols, *self.value_columns)) @@ -1637,13 +1624,11 @@ def add_suffix(self, suffix: str, axis: str | int | None = None) -> Block: expr = self._expr new_index_cols = [] for index_col in self._index_columns: - new_col = guid.generate_guid() - expr = expr.project_to_id( + expr, new_col = expr.project_to_id( expression=ops.add_op.as_expr( ops.AsTypeOp(to_type="string").as_expr(index_col), ex.const(suffix), ), - output_id=new_col, ) new_index_cols.append(new_col) expr = expr.select_columns((*new_index_cols, *self.value_columns)) @@ -1785,8 +1770,7 @@ def melt( ) if create_offsets_index: - index_id = guid.generate_guid() - unpivot_expr = unpivot_expr.promote_offsets(index_id) + unpivot_expr, index_id = unpivot_expr.promote_offsets() index_cols = [index_id] else: index_cols = [] @@ -2012,12 +1996,10 @@ def merge( coalesced_ids = [] for left_id, right_id in zip(left_join_ids, right_join_ids): - coalesced_id = guid.generate_guid() - joined_expr = joined_expr.project_to_id( + joined_expr, coalesced_id = joined_expr.project_to_id( ops.coalesce_op.as_expr( get_column_left[left_id], get_column_right[right_id] ), - coalesced_id, ) coalesced_ids.append(coalesced_id) @@ -2076,8 +2058,7 @@ def merge( expr = joined_expr index_columns = [] else: - offset_index_id = guid.generate_guid() - expr = joined_expr.promote_offsets(offset_index_id) + expr, offset_index_id = joined_expr.promote_offsets() index_columns = [offset_index_id] return Block(expr, index_columns=index_columns, column_labels=labels) @@ -2442,8 +2423,7 @@ def _get_rows_as_json_values(self) -> Block: # expression. # TODO(shobs): Replace direct SQL manipulation by structured expression # manipulation - ordering_column_name = guid.generate_guid() - expr = self.expr.promote_offsets(ordering_column_name) + expr, ordering_column_name = self.expr.promote_offsets() expr_sql = self.session._to_sql(expr) # Names of the columns to serialize for the row. @@ -2869,8 +2849,8 @@ def coalesce_columns( expr = expr.drop_columns([left_id]) elif how == "outer": coalesced_id = guid.generate_guid() - expr = expr.project_to_id( - ops.coalesce_op.as_expr(left_id, right_id), coalesced_id + expr, coalesced_id = expr.project_to_id( + ops.coalesce_op.as_expr(left_id, right_id) ) expr = expr.drop_columns([left_id, right_id]) result_ids.append(coalesced_id) @@ -3047,7 +3027,7 @@ def unpivot( explode_offsets_id = labels_mapping[labels_array.column_ids[-1]] # Build the output rows as a case statment that selects between the N input columns - unpivot_exprs: List[Tuple[ex.Expression, str]] = [] + unpivot_exprs: List[ex.Expression] = [] # Supports producing multiple stacked ouput columns for stacking only part of hierarchical index for input_ids in unpivot_columns: # row explode offset used to choose the input column @@ -3064,11 +3044,11 @@ def unpivot( ) ) col_expr = ops.case_when_op.as_expr(*cases) - unpivot_exprs.append((col_expr, guid.generate_guid())) + unpivot_exprs.append(col_expr) - unpivot_col_ids = [id for _, id in unpivot_exprs] + joined_array, unpivot_col_ids = joined_array.compute_values(unpivot_exprs) - return joined_array.compute_values(unpivot_exprs).select_columns( + return joined_array.select_columns( [*index_col_ids, *unpivot_col_ids, *new_passthrough_cols] ), (tuple(index_col_ids), tuple(unpivot_col_ids), tuple(new_passthrough_cols)) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index cd00c98381f..693d93de8ca 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -814,7 +814,7 @@ def project_window_op( column_name: str, op: agg_ops.UnaryWindowOp, window_spec: WindowSpec, - output_name=None, + output_name: str, *, never_skip_nulls=False, ) -> OrderedIR: @@ -823,7 +823,7 @@ def project_window_op( column_name: the id of the input column present in the expression op: the windowable operator to apply to the input column window_spec: a specification of the window over which to apply the operator - output_name: the id to assign to the output of the operator, by default will replace input col if distinct output id not provided + output_name: the id to assign to the output of the operator never_skip_nulls: will disable null skipping for operators that would otherwise do so """ # Cannot nest analytic expressions, so reproject to cte first if needed. diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 950c2c2cc7a..23501f93c8b 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -110,7 +110,7 @@ def compile_cached_table(self, node: nodes.CachedTableNode, ordered: bool = True ) used_columns = ( *node.schema.names, - *node.hidden_columns, + *node._hidden_columns, ) # Physical schema might include unused columns, unsupported datatypes like JSON physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( @@ -127,18 +127,20 @@ def compile_cached_table(self, node: nodes.CachedTableNode, ordered: bool = True raise ValueError( "Cannot use partially ordered cached value. Result requires total ordering information." ) - return compiled.OrderedIR( + ir = compiled.OrderedIR( ibis_table, columns=tuple( bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( ibis_table[col] ) - for col in node.schema.names + for col in [*node.schema.names, *node._hidden_columns] ), ordering=node.ordering, - hidden_ordering_columns=[ibis_table[c] for c in node.hidden_columns], ) - + ir = ir._select( + tuple(ir._get_ibis_column(name) for name in node.schema.names) + ) + return ir else: return compiled.UnorderedIR( ibis_table, diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index e11fcafff73..2dc9623d89d 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -178,6 +178,25 @@ def transform_children( """Apply a function to each child node.""" ... + @property + def defines_namespace(self) -> bool: + """ + If true, this node establishes a new column id namespace. + + If false, this node consumes and produces ids in the namespace + """ + return False + + @functools.cached_property + def defined_variables(self) -> set[str]: + """Full set of variables defined in the namespace, even if not selected.""" + self_defined_variables = set(self.schema.names) + if self.defines_namespace: + return self_defined_variables + return self_defined_variables.union( + *(child.defined_variables for child in self.child_nodes) + ) + @dataclass(frozen=True) class UnaryNode(BigFrameNode): @@ -262,6 +281,10 @@ def transform_children( self, left_child=t(self.left_child), right_child=t(self.right_child) ) + @property + def defines_namespace(self) -> bool: + return True + @dataclass(frozen=True) class ConcatNode(BigFrameNode): @@ -477,9 +500,9 @@ def __post_init__(self): raise ValueError( f"Requested schema {logical_names} cannot be derived from table schema {self.table.physical_schema}" ) - if not set(self.hidden_columns).issubset(physical_names): + if not set(self._hidden_columns).issubset(physical_names): raise ValueError( - f"Requested hidden columns {self.hidden_columns} cannot be derived from table schema {self.table.physical_schema}" + f"Requested hidden columns {self._hidden_columns} cannot be derived from table schema {self.table.physical_schema}" ) @property @@ -498,7 +521,7 @@ def variables_introduced(self) -> int: return len(self.schema.items) + OVERHEAD_VARIABLES @property - def hidden_columns(self) -> typing.Tuple[str, ...]: + def _hidden_columns(self) -> typing.Tuple[str, ...]: """Physical columns used to define ordering but not directly exposed as value columns.""" if self.ordering is None: return () @@ -646,6 +669,13 @@ def variables_introduced(self) -> int: # This operation only renames variables, doesn't actually create new ones return 0 + # TODO: Reuse parent namespace + # Currently, Selection node allows renaming an reusing existing names, so it must establish a + # new namespace. + @property + def defines_namespace(self) -> bool: + return True + @dataclass(frozen=True) class ProjectionNode(UnaryNode): @@ -707,6 +737,10 @@ def schema(self) -> schemata.ArraySchema: def variables_introduced(self) -> int: return 1 + @property + def defines_namespace(self) -> bool: + return True + @dataclass(frozen=True) class AggregateNode(UnaryNode): @@ -752,13 +786,17 @@ def order_ambiguous(self) -> bool: def explicitly_ordered(self) -> bool: return True + @property + def defines_namespace(self) -> bool: + return True + @dataclass(frozen=True) class WindowOpNode(UnaryNode): column_name: str op: agg_ops.UnaryWindowOp window_spec: window.WindowSpec - output_name: typing.Optional[str] = None + output_name: str never_skip_nulls: bool = False skip_reproject_unsafe: bool = False @@ -773,10 +811,6 @@ def non_local(self) -> bool: def schema(self) -> schemata.ArraySchema: input_type = self.child.schema.get_type(self.column_name) new_item_dtype = self.op.output_type(input_type) - if self.output_name is None: - return self.child.schema.update_dtype(self.column_name, new_item_dtype) - if self.output_name in self.child.schema.names: - return self.child.schema.update_dtype(self.output_name, new_item_dtype) return self.child.schema.append( schemata.SchemaItem(self.output_name, new_item_dtype) ) @@ -860,3 +894,7 @@ def relation_ops_created(self) -> int: @functools.cached_property def variables_introduced(self) -> int: return len(self.column_ids) + 1 + + @property + def defines_namespace(self) -> bool: + return True diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index c2fb9336f3e..862c8dc2c80 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3457,7 +3457,8 @@ def _prepare_export( } if ordering_id is not None: - array_value = array_value.promote_offsets(ordering_id) + array_value, internal_ordering_id = array_value.promote_offsets() + id_overrides[internal_ordering_id] = ordering_id return array_value, id_overrides def map(self, func, na_action: Optional[str] = None) -> DataFrame: diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 0512fa78e34..d2a2e0f1b22 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -84,7 +84,9 @@ def to_sql( Convert an ArrayValue to a sql query that will yield its value. """ if offset_column: - array_value = array_value.promote_offsets(offset_column) + array_value, internal_offset_col = array_value.promote_offsets() + col_id_overrides = dict(col_id_overrides) + col_id_overrides[internal_offset_col] = offset_column node = ( self._get_optimized_plan(array_value.node) if enable_cache @@ -364,12 +366,12 @@ def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): "Caching with offsets only supported in strictly ordered mode." ) offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") - node_w_offsets = array_value.promote_offsets(offset_column).node - sql = self.compiler.compile_unordered(self._get_optimized_plan(node_w_offsets)) + w_offsets, offset_column = array_value.promote_offsets() + sql = self.compiler.compile_unordered(self._get_optimized_plan(w_offsets.node)) tmp_table = self._sql_as_cached_temp_table( sql, - node_w_offsets.schema.to_bigquery(), + w_offsets.schema.to_bigquery(), cluster_cols=[offset_column], ) cached_replacement = array_value.as_cached( diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index ce9874e35f8..22de367804f 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -383,8 +383,7 @@ def read_gbq_table( index_names: Sequence[Hashable] = index_cols if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: - sequential_index_col = bigframes.core.guid.generate_guid("index_") - array_value = array_value.promote_offsets(sequential_index_col) + array_value, sequential_index_col = array_value.promote_offsets() index_cols = [sequential_index_col] index_names = [None] diff --git a/tests/unit/test_planner.py b/tests/unit/test_planner.py index 84dd05ddaa1..27ec7d5921f 100644 --- a/tests/unit/test_planner.py +++ b/tests/unit/test_planner.py @@ -46,10 +46,9 @@ def test_session_aware_caching_project_filter(): """ Test that if a node is filtered by a column, the node is cached pre-filter and clustered by the filter column. """ - session_objects = [LEAF, LEAF.create_constant("col_c", 4, pd.Int64Dtype())] - target = LEAF.create_constant("col_c", 4, pd.Int64Dtype()).filter( - ops.gt_op.as_expr("col_a", ex.const(3)) - ) + session_objects = [LEAF, LEAF.create_constant(4, pd.Int64Dtype())[0]] + target, _ = LEAF.create_constant(4, pd.Int64Dtype()) + target = target.filter(ops.gt_op.as_expr("col_a", ex.const(3))) result, cluster_cols = planner.session_aware_cache_plan( target.node, [obj.node for obj in session_objects] ) @@ -61,14 +60,16 @@ def test_session_aware_caching_project_multi_filter(): """ Test that if a node is filtered by multiple columns, all of them are in the cluster cols """ - session_objects = [LEAF, LEAF.create_constant("col_c", 4, pd.Int64Dtype())] + obj1 = LEAF + obj2, _ = LEAF.create_constant(4, pd.Int64Dtype()) + session_objects = [obj1, obj2] predicate_1a = ops.gt_op.as_expr("col_a", ex.const(3)) predicate_1b = ops.lt_op.as_expr("col_a", ex.const(55)) predicate_1 = ops.and_op.as_expr(predicate_1a, predicate_1b) predicate_3 = ops.eq_op.as_expr("col_b", ex.const(1)) target = ( LEAF.filter(predicate_1) - .create_constant("col_c", 4, pd.Int64Dtype()) + .create_constant(4, pd.Int64Dtype())[0] .filter(predicate_3) ) result, cluster_cols = planner.session_aware_cache_plan( @@ -84,8 +85,8 @@ def test_session_aware_caching_unusable_filter(): Most filters with multiple column references cannot be used for scan pruning, as they cannot be converted to fixed value ranges. """ - session_objects = [LEAF, LEAF.create_constant("col_c", 4, pd.Int64Dtype())] - target = LEAF.create_constant("col_c", 4, pd.Int64Dtype()).filter( + session_objects = [LEAF, LEAF.create_constant(4, pd.Int64Dtype())[0]] + target = LEAF.create_constant(4, pd.Int64Dtype())[0].filter( ops.gt_op.as_expr("col_a", "col_b") ) result, cluster_cols = planner.session_aware_cache_plan( @@ -101,12 +102,10 @@ def test_session_aware_caching_fork_after_window_op(): Windowing is expensive, so caching should always compute the window function, in order to avoid later recomputation. """ - other = LEAF.promote_offsets("offsets_col").create_constant( - "col_d", 5, pd.Int64Dtype() - ) + other = LEAF.promote_offsets()[0].create_constant(5, pd.Int64Dtype())[0] target = ( - LEAF.promote_offsets("offsets_col") - .create_constant("col_c", 4, pd.Int64Dtype()) + LEAF.promote_offsets()[0] + .create_constant(4, pd.Int64Dtype())[0] .filter( ops.eq_op.as_expr("col_a", ops.add_op.as_expr(ex.const(4), ex.const(3))) ) @@ -117,5 +116,5 @@ def test_session_aware_caching_fork_after_window_op(): other.node, ], ) - assert result == LEAF.promote_offsets("offsets_col").node + assert result == LEAF.promote_offsets()[0].node assert cluster_cols == ["col_a"] From c164e85cd3a526908592eb681b43ec04bd4fb47b Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:58:55 +0000 Subject: [PATCH 833/921] build(python): release script update (#993) Source-Link: https://github.com/googleapis/synthtool/commit/71a72973dddbc66ea64073b53eda49f0d22e0942 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:e8dcfd7cbfd8beac3a3ff8d3f3185287ea0625d859168cc80faccfc9a7a00455 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/release.sh | 2 +- .kokoro/release/common.cfg | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 6d064ddb9b0..597e0c3261c 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:94bb690db96e6242b2567a4860a94d48fa48696d092e51b0884a1a2c0a79a407 -# created: 2024-07-31T14:52:44.926548819Z + digest: sha256:e8dcfd7cbfd8beac3a3ff8d3f3185287ea0625d859168cc80faccfc9a7a00455 +# created: 2024-09-16T21:04:09.091105552Z diff --git a/.kokoro/release.sh b/.kokoro/release.sh index 21a9b558c5e..b1dd5f09ec8 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -23,7 +23,7 @@ python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source / export PYTHONUNBUFFERED=1 # Move into the package, build the distribution and upload. -TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-1") +TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-2") cd github/python-bigquery-dataframes python3 setup.py sdist bdist_wheel twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index a0c39946cf1..824d62f2575 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -28,7 +28,7 @@ before_action { fetch_keystore { keystore_resource { keystore_config_id: 73713 - keyname: "google-cloud-pypi-token-keystore-1" + keyname: "google-cloud-pypi-token-keystore-2" } } } From 06c1b3396d77d1de4f927328bae70cd7b3eb0b0b Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:39:28 -0700 Subject: [PATCH 834/921] deps: limit ibis-framework version to 9.2.0 (#989) * chore: limit ibis version to 9.2.0 * remove ibis constrains on 3.12 tests --- setup.py | 2 +- testing/constraints-3.12.txt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 908a3c244f2..833d4fe5655 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ "google-cloud-iam >=2.12.1", "google-cloud-resource-manager >=1.10.3", "google-cloud-storage >=2.0.0", - "ibis-framework[bigquery] >=9.0.0,<=9.3.0", + "ibis-framework[bigquery] >=9.0.0,<=9.2.0", "jellyfish >=0.8.9", "numpy >=1.24.0", "pandas >=1.5.3", diff --git a/testing/constraints-3.12.txt b/testing/constraints-3.12.txt index dbbb5a2d888..e69de29bb2d 100644 --- a/testing/constraints-3.12.txt +++ b/testing/constraints-3.12.txt @@ -1,3 +0,0 @@ -# Some internal modules have moved, -# so make sure we test on all ibis-framework 9.x versions. -ibis-framework==9.2.0 From 22b483a9d9bc50b594ef270a691e2ea3a31aa295 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Tue, 17 Sep 2024 12:29:17 -0700 Subject: [PATCH 835/921] chore: Use fixture for list/str accessor tests (#992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Use fixture for list/str accessor tests * fix format * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * add more type coverage in tests * fix format * remove unnecessary dep * remove import --------- Co-authored-by: Owl Bot --- tests/data/repeated.jsonl | 3 + tests/data/repeated_schema.json | 42 ++++++++++++ tests/system/conftest.py | 27 ++++++++ tests/system/small/operations/test_lists.py | 67 +++++++++++++------ tests/system/small/operations/test_strings.py | 25 ++++--- 5 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 tests/data/repeated.jsonl create mode 100644 tests/data/repeated_schema.json diff --git a/tests/data/repeated.jsonl b/tests/data/repeated.jsonl new file mode 100644 index 00000000000..b3c47772f6d --- /dev/null +++ b/tests/data/repeated.jsonl @@ -0,0 +1,3 @@ +{"rowindex": 0, "int_list_col": [1], "bool_list_col": [true], "float_list_col": [1.2, 2.3], "date_list_col": ["2021-07-21"], "date_time_list_col": ["2021-07-21 11:39:45"], "numeric_list_col": [1.2, 2.3, 3.4], "string_list_col": ["abc", "de", "f"]} +{"rowindex": 1, "int_list_col": [1,2], "bool_list_col": [true, false], "float_list_col": [1.1], "date_list_col": ["2021-07-21", "1987-03-28"], "date_time_list_col": ["1999-03-14 17:22:00"], "numeric_list_col": [5.5, 2.3], "string_list_col": ["a", "bc", "de"]} +{"rowindex": 2, "int_list_col": [1,2,3], "bool_list_col": [true], "float_list_col": [0.5, -1.9, 2.3], "date_list_col": ["2017-08-01", "2004-11-22"], "date_time_list_col": ["1979-06-03 03:20:45"], "numeric_list_col": [1.7], "string_list_col": ["", "a"]} diff --git a/tests/data/repeated_schema.json b/tests/data/repeated_schema.json new file mode 100644 index 00000000000..300f32c994c --- /dev/null +++ b/tests/data/repeated_schema.json @@ -0,0 +1,42 @@ +[ + { + "name": "rowindex", + "type": "INTEGER", + "mode": "REQUIRED" + }, + { + "name": "int_list_col", + "type": "INTEGER", + "mode": "REPEATED" + }, + { + "name": "bool_list_col", + "type": "BOOLEAN", + "mode": "REPEATED" + }, + { + "name": "float_list_col", + "type": "FLOAT", + "mode": "REPEATED" + }, + { + "name": "date_list_col", + "type": "DATE", + "mode": "REPEATED" + }, + { + "name": "date_time_list_col", + "type": "DATETIME", + "mode": "REPEATED" + }, + { + "name": "numeric_list_col", + "type": "NUMERIC", + "mode": "REPEATED" + }, + { + "name": "string_list_col", + "type": "STRING", + "mode": "REPEATED" + } +] diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 49cd887cfdf..9cfb9082af5 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -39,6 +39,7 @@ import bigframes import bigframes.dataframe import bigframes.pandas as bpd +import bigframes.series import tests.system.utils # Use this to control the number of cloud functions being deleted in a single @@ -294,6 +295,7 @@ def load_test_data_tables( ("scalars", "scalars_schema.json", "scalars.jsonl"), ("scalars_too", "scalars_schema.json", "scalars.jsonl"), ("nested", "nested_schema.json", "nested.jsonl"), + ("repeated", "repeated_schema.json", "repeated.jsonl"), ("penguins", "penguins_schema.json", "penguins.jsonl"), ("time_series", "time_series_schema.json", "time_series.jsonl"), ("hockey_players", "hockey_players.json", "hockey_players.jsonl"), @@ -370,6 +372,11 @@ def nested_table_id(test_data_tables) -> str: return test_data_tables["nested"] +@pytest.fixture(scope="session") +def repeated_table_id(test_data_tables) -> str: + return test_data_tables["repeated"] + + @pytest.fixture(scope="session") def penguins_table_id(test_data_tables) -> str: return test_data_tables["penguins"] @@ -410,6 +417,26 @@ def nested_pandas_df() -> pd.DataFrame: return df +@pytest.fixture(scope="session") +def repeated_df( + repeated_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """Returns a DataFrame containing columns of list type.""" + return session.read_gbq(repeated_table_id, index_col="rowindex") + + +@pytest.fixture(scope="session") +def repeated_pandas_df() -> pd.DataFrame: + """Returns a DataFrame containing columns of list type.""" + + df = pd.read_json( + DATA_DIR / "repeated.jsonl", + lines=True, + ) + df = df.set_index("rowindex") + return df + + @pytest.fixture(scope="session") def scalars_df_default_index( scalars_df_index: bigframes.dataframe.DataFrame, diff --git a/tests/system/small/operations/test_lists.py b/tests/system/small/operations/test_lists.py index 7ecf79dc6af..7b39bdebd5e 100644 --- a/tests/system/small/operations/test_lists.py +++ b/tests/system/small/operations/test_lists.py @@ -18,8 +18,6 @@ import pyarrow as pa import pytest -import bigframes.pandas as bpd - from ...utils import assert_series_equal @@ -32,19 +30,34 @@ pytest.param(slice(0, 2, None), id="default_step_slice"), ], ) -def test_getitem(key): +@pytest.mark.parametrize( + ("column_name", "dtype"), + [ + pytest.param("int_list_col", pd.ArrowDtype(pa.list_(pa.int64()))), + pytest.param("bool_list_col", pd.ArrowDtype(pa.list_(pa.bool_()))), + pytest.param("float_list_col", pd.ArrowDtype(pa.list_(pa.float64()))), + pytest.param("date_list_col", pd.ArrowDtype(pa.list_(pa.date32()))), + pytest.param("date_time_list_col", pd.ArrowDtype(pa.list_(pa.timestamp("us")))), + pytest.param("numeric_list_col", pd.ArrowDtype(pa.list_(pa.decimal128(38, 9)))), + pytest.param("string_list_col", pd.ArrowDtype(pa.list_(pa.string()))), + ], +) +def test_getitem(key, column_name, dtype, repeated_df, repeated_pandas_df): if packaging.version.Version(pd.__version__) < packaging.version.Version("2.2.0"): pytest.skip( "https://pandas.pydata.org/docs/whatsnew/v2.2.0.html#series-list-accessor-for-pyarrow-list-data" ) - data = [[1], [2, 3], [4, 5, 6]] - s = bpd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) - pd_s = pd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) - bf_result = s.list[key].to_pandas() - pd_result = pd_s.list[key] + bf_result = repeated_df[column_name].list[key].to_pandas() + pd_result = repeated_pandas_df[column_name].astype(dtype).list[key] - assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) + assert_series_equal( + pd_result, + bf_result, + check_dtype=False, + check_index_type=False, + check_names=False, + ) @pytest.mark.parametrize( @@ -60,24 +73,36 @@ def test_getitem(key): (slice(0, 2, 2), pytest.raises(NotImplementedError)), ], ) -def test_getitem_notsupported(key, expectation): - data = [[1], [2, 3], [4, 5, 6]] - s = bpd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) - +def test_getitem_notsupported(key, expectation, repeated_df): with expectation as e: - assert s.list[key] == e + assert repeated_df["int_list_col"].list[key] == e -def test_len(): +@pytest.mark.parametrize( + ("column_name", "dtype"), + [ + pytest.param("int_list_col", pd.ArrowDtype(pa.list_(pa.int64()))), + pytest.param("bool_list_col", pd.ArrowDtype(pa.list_(pa.bool_()))), + pytest.param("float_list_col", pd.ArrowDtype(pa.list_(pa.float64()))), + pytest.param("date_list_col", pd.ArrowDtype(pa.list_(pa.date32()))), + pytest.param("date_time_list_col", pd.ArrowDtype(pa.list_(pa.timestamp("us")))), + pytest.param("numeric_list_col", pd.ArrowDtype(pa.list_(pa.decimal128(38, 9)))), + pytest.param("string_list_col", pd.ArrowDtype(pa.list_(pa.string()))), + ], +) +def test_len(column_name, dtype, repeated_df, repeated_pandas_df): if packaging.version.Version(pd.__version__) < packaging.version.Version("2.2.0"): pytest.skip( "https://pandas.pydata.org/docs/whatsnew/v2.2.0.html#series-list-accessor-for-pyarrow-list-data" ) - data = [[], [1], [1, 2], [1, 2, 3]] - s = bpd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) - pd_s = pd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) - bf_result = s.list.len().to_pandas() - pd_result = pd_s.list.len() + bf_result = repeated_df[column_name].list.len().to_pandas() + pd_result = repeated_pandas_df[column_name].astype(dtype).list.len() - assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) + assert_series_equal( + pd_result, + bf_result, + check_dtype=False, + check_index_type=False, + check_names=False, + ) diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 15e8512317e..98fecaa93b3 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -615,21 +615,28 @@ def test_getitem_w_string(scalars_dfs, index): @pytest.mark.parametrize( ("index"), [ - pytest.param(2, id="int"), + pytest.param(0, id="int"), pytest.param(slice(None, None, None), id="default_start_slice"), pytest.param(slice(0, None, 1), id="default_stop_slice"), pytest.param(slice(0, 2, None), id="default_step_slice"), pytest.param(slice(0, 0, None), id="single_one_slice"), ], ) -def test_getitem_w_array(index): - data = [[1], [2, 3], [], [4, 5, 6]] - s = bpd.Series(data) - pd_s = pd.Series(data) - - bf_result = s.str[index].to_pandas() - pd_result = pd_s.str[index] - # Skip dtype checks here because pandas returns `int64` while BF returns `Int64`. +@pytest.mark.parametrize( + "column_name", + [ + pytest.param("int_list_col"), + pytest.param("bool_list_col"), + pytest.param("float_list_col"), + pytest.param("string_list_col"), + # date, date_time and numeric are excluded because their default types are different + # in Pandas and BigFrames + ], +) +def test_getitem_w_array(index, column_name, repeated_df, repeated_pandas_df): + bf_result = repeated_df[column_name].str[index].to_pandas() + pd_result = repeated_pandas_df[column_name].str[index] + assert_series_equal(pd_result, bf_result, check_dtype=False, check_index_type=False) From ad8263dd0772d87106b401703cd32897ab01f187 Mon Sep 17 00:00:00 2001 From: Luis Velasco <57447519+velascoluis@users.noreply.github.com> Date: Tue, 17 Sep 2024 22:15:34 +0200 Subject: [PATCH 836/921] chore: fix tpch q5 (#984) --- tests/benchmark/tpch/q5.py | 2 +- third_party/bigframes_vendored/tpch/queries/q5.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/tpch/q5.py b/tests/benchmark/tpch/q5.py index e4b3cb0f51c..bc39b76e82d 100644 --- a/tests/benchmark/tpch/q5.py +++ b/tests/benchmark/tpch/q5.py @@ -14,7 +14,7 @@ import pathlib import benchmark.utils as utils -import bigframes_vendored.tpch.queries.q1 as vendored_tpch_q5 +import bigframes_vendored.tpch.queries.q5 as vendored_tpch_q5 if __name__ == "__main__": dataset_id, session, suffix = utils.get_tpch_configuration() diff --git a/third_party/bigframes_vendored/tpch/queries/q5.py b/third_party/bigframes_vendored/tpch/queries/q5.py index 20bd8d9c441..b3a9b5e614b 100644 --- a/third_party/bigframes_vendored/tpch/queries/q5.py +++ b/third_party/bigframes_vendored/tpch/queries/q5.py @@ -23,7 +23,7 @@ def q(dataset_id: str, session: bigframes.Session): index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERES", + f"bigframes-dev-perf.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( From 7dbda8461da84bdfa2c15049235b7f925129e5e6 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:08:04 -0700 Subject: [PATCH 837/921] chore: update local benchmark, 10t config and kokoro project. (#995) * chore: update local benchmark, 10t config and kokoro project. * update details * fixes * update config * update config reading * update config * remove notebook for now. --- .kokoro/load/benchmark.cfg | 2 +- noxfile.py | 56 ++++++- scripts/run_and_publish_benchmark.py | 138 ++++++++++++------ tests/benchmark/tpch/config.jsonl | 18 ++- tests/benchmark/tpch/q1.py | 4 +- tests/benchmark/tpch/q10.py | 4 +- tests/benchmark/tpch/q11.py | 4 +- tests/benchmark/tpch/q12.py | 4 +- tests/benchmark/tpch/q13.py | 4 +- tests/benchmark/tpch/q14.py | 4 +- tests/benchmark/tpch/q15.py | 4 +- tests/benchmark/tpch/q16.py | 4 +- tests/benchmark/tpch/q17.py | 4 +- tests/benchmark/tpch/q18.py | 4 +- tests/benchmark/tpch/q19.py | 4 +- tests/benchmark/tpch/q2.py | 4 +- tests/benchmark/tpch/q20.py | 4 +- tests/benchmark/tpch/q21.py | 4 +- tests/benchmark/tpch/q22.py | 4 +- tests/benchmark/tpch/q3.py | 4 +- tests/benchmark/tpch/q4.py | 4 +- tests/benchmark/tpch/q5.py | 4 +- tests/benchmark/tpch/q6.py | 4 +- tests/benchmark/tpch/q7.py | 4 +- tests/benchmark/tpch/q8.py | 4 +- tests/benchmark/tpch/q9.py | 4 +- tests/benchmark/utils.py | 10 +- .../bigframes_vendored/tpch/queries/q1.py | 4 +- .../bigframes_vendored/tpch/queries/q10.py | 10 +- .../bigframes_vendored/tpch/queries/q11.py | 8 +- .../bigframes_vendored/tpch/queries/q12.py | 6 +- .../bigframes_vendored/tpch/queries/q13.py | 6 +- .../bigframes_vendored/tpch/queries/q14.py | 6 +- .../bigframes_vendored/tpch/queries/q15.py | 6 +- .../bigframes_vendored/tpch/queries/q16.py | 8 +- .../bigframes_vendored/tpch/queries/q17.py | 6 +- .../bigframes_vendored/tpch/queries/q18.py | 8 +- .../bigframes_vendored/tpch/queries/q19.py | 6 +- .../bigframes_vendored/tpch/queries/q2.py | 12 +- .../bigframes_vendored/tpch/queries/q20.py | 12 +- .../bigframes_vendored/tpch/queries/q21.py | 10 +- .../bigframes_vendored/tpch/queries/q22.py | 6 +- .../bigframes_vendored/tpch/queries/q3.py | 8 +- .../bigframes_vendored/tpch/queries/q4.py | 6 +- .../bigframes_vendored/tpch/queries/q5.py | 14 +- .../bigframes_vendored/tpch/queries/q6.py | 4 +- .../bigframes_vendored/tpch/queries/q7.py | 12 +- .../bigframes_vendored/tpch/queries/q8.py | 16 +- .../bigframes_vendored/tpch/queries/q9.py | 14 +- 49 files changed, 301 insertions(+), 199 deletions(-) diff --git a/.kokoro/load/benchmark.cfg b/.kokoro/load/benchmark.cfg index bc2d7a26559..45e2527a0c1 100644 --- a/.kokoro/load/benchmark.cfg +++ b/.kokoro/load/benchmark.cfg @@ -13,7 +13,7 @@ env_vars: { env_vars: { key: "GOOGLE_CLOUD_PROJECT" - value: "bigframes-load-testing" + value: "bigframes-benchmarking" } env_vars: { diff --git a/noxfile.py b/noxfile.py index f4142af21bc..bf4c6f9641c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,6 +16,7 @@ from __future__ import absolute_import +import argparse import multiprocessing import os import pathlib @@ -804,7 +805,7 @@ def notebook(session: nox.Session): processes = [] for notebook, regions in notebooks_reg.items(): for region in regions: - args = ( + region_args = ( "python", "scripts/run_and_publish_benchmark.py", "--notebook", @@ -814,7 +815,7 @@ def notebook(session: nox.Session): if multi_process_mode: process = multiprocessing.Process( target=_run_process, - args=(session, args, error_flag), + args=(session, region_args, error_flag), ) process.start() processes.append(process) @@ -822,7 +823,7 @@ def notebook(session: nox.Session): # process to avoid potential race conditions。 time.sleep(1) else: - session.run(*args) + session.run(*region_args) for process in processes: process.join() @@ -861,7 +862,51 @@ def benchmark(session: nox.Session): session.install("-e", ".[all]") base_path = os.path.join("tests", "benchmark") - benchmark_script_list = list(pathlib.Path(base_path).rglob("*.py")) + parser = argparse.ArgumentParser() + parser.add_argument( + "-i", + "--iterations", + type=int, + default=1, + help="Number of iterations to run each benchmark.", + ) + parser.add_argument( + "-o", + "--output-csv", + nargs="?", + const=True, + default=False, + help=( + "Determines whether to output results to a CSV file. If no location is provided, " + "a temporary location is automatically generated." + ), + ) + parser.add_argument( + "-b", + "--benchmark-filter", + nargs="+", + help=( + "List of file or directory names to include in the benchmarks. If not provided, " + "all benchmarks are run." + ), + ) + + args = parser.parse_args(session.posargs) + + benchmark_script_list: List[pathlib.Path] = [] + if args.benchmark_filter: + for filter_item in args.benchmark_filter: + full_path = os.path.join(base_path, filter_item) + if os.path.isdir(full_path): + benchmark_script_list.extend(pathlib.Path(full_path).rglob("*.py")) + elif os.path.isfile(full_path) and full_path.endswith(".py"): + benchmark_script_list.append(pathlib.Path(full_path)) + else: + raise ValueError( + f"Item {filter_item} does not match any valid file or directory" + ) + else: + benchmark_script_list = list(pathlib.Path(base_path).rglob("*.py")) try: for benchmark in benchmark_script_list: @@ -871,12 +916,15 @@ def benchmark(session: nox.Session): "python", "scripts/run_and_publish_benchmark.py", f"--benchmark-path={benchmark}", + f"--iterations={args.iterations}", ) finally: session.run( "python", "scripts/run_and_publish_benchmark.py", f"--publish-benchmarks={base_path}", + f"--iterations={args.iterations}", + f"--output-csv={args.output_csv}", ) diff --git a/scripts/run_and_publish_benchmark.py b/scripts/run_and_publish_benchmark.py index 4b3d4f4d962..a42301cb13e 100644 --- a/scripts/run_and_publish_benchmark.py +++ b/scripts/run_and_publish_benchmark.py @@ -19,6 +19,7 @@ import pathlib import subprocess import sys +import tempfile from typing import Dict, List, Union import numpy as np @@ -50,7 +51,7 @@ def run_benchmark_subprocess(args, log_env_name_var, filename=None, region=None) subprocess.run(args, env=env, check=True) -def collect_benchmark_result(benchmark_path: str) -> pd.DataFrame: +def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFrame: """Generate a DataFrame report on HTTP queries, bytes processed, slot time and execution time from log files.""" path = pathlib.Path(benchmark_path) try: @@ -100,28 +101,23 @@ def collect_benchmark_result(benchmark_path: str) -> pd.DataFrame: with open(bytes_file, "r") as file: lines = file.read().splitlines() - query_count = len(lines) - total_bytes = sum(int(line) for line in lines) + query_count = len(lines) / iterations + total_bytes = sum(int(line) for line in lines) / iterations with open(millis_file, "r") as file: lines = file.read().splitlines() - total_slot_millis = sum(int(line) for line in lines) + total_slot_millis = sum(int(line) for line in lines) / iterations if has_local_seconds: - # 'local_seconds' captures the total execution time for a benchmark as it - # starts timing immediately before the benchmark code begins and stops - # immediately after it ends. Unlike other metrics that might accumulate - # values proportional to the number of queries executed, 'local_seconds' is - # a singular measure of the time taken for the complete execution of the - # benchmark, from start to finish. with open(local_seconds_file, "r") as file: - local_seconds = float(file.readline().strip()) + lines = file.read().splitlines() + local_seconds = sum(float(line) for line in lines) / iterations else: local_seconds = None with open(bq_seconds_file, "r") as file: lines = file.read().splitlines() - bq_seconds = sum(float(line) for line in lines) + bq_seconds = sum(float(line) for line in lines) / iterations results_dict[str(filename)] = [ query_count, @@ -154,7 +150,12 @@ def collect_benchmark_result(benchmark_path: str) -> pd.DataFrame: columns=columns, ) - print("---BIGQUERY USAGE REPORT---") + report_title = ( + "---BIGQUERY USAGE REPORT---" + if iterations == 1 + else f"---BIGQUERY USAGE REPORT (Averages over {iterations} Iterations)---" + ) + print(report_title) for index, row in benchmark_metrics.iterrows(): formatted_local_exec_time = ( f"{round(row['Local_Execution_Time_Sec'], 1)} seconds" @@ -259,7 +260,26 @@ def find_config(start_path): return None -def run_benchmark_from_config(benchmark: str): +def publish_to_bigquery(dataframe, notebook, project_name="bigframes-metrics"): + bigquery_table = ( + f"{project_name}.benchmark_report.notebook_benchmark" + if notebook + else f"{project_name}.benchmark_report.benchmark" + ) + + repo_status = get_repository_status() + for idx, col in enumerate(repo_status.keys()): + dataframe.insert(idx, col, repo_status[col]) + + pandas_gbq.to_gbq( + dataframe=dataframe, + destination_table=bigquery_table, + if_exists="append", + ) + print(f"Results have been successfully uploaded to {bigquery_table}.") + + +def run_benchmark_from_config(benchmark: str, iterations: int): print(benchmark) config_path = find_config(benchmark) @@ -267,24 +287,26 @@ def run_benchmark_from_config(benchmark: str): benchmark_configs = [] with open(config_path, "r") as f: for line in f: - config = json.loads(line) - python_args = [f"--{key}={value}" for key, value in config.items()] - suffix = ( - config["benchmark_suffix"] - if "benchmark_suffix" in config - else "_".join(f"{key}_{value}" for key, value in config.items()) - ) - benchmark_configs.append((suffix, python_args)) + if line.strip(): + config = json.loads(line) + python_args = [f"--{key}={value}" for key, value in config.items()] + suffix = ( + config["benchmark_suffix"] + if "benchmark_suffix" in config + else "_".join(f"{key}_{value}" for key, value in config.items()) + ) + benchmark_configs.append((suffix, python_args)) else: benchmark_configs = [(None, [])] - for benchmark_config in benchmark_configs: - args = ["python", str(benchmark)] - args.extend(benchmark_config[1]) - log_env_name_var = str(benchmark) - if benchmark_config[0] is not None: - log_env_name_var += f"_{benchmark_config[0]}" - run_benchmark_subprocess(args=args, log_env_name_var=log_env_name_var) + for _ in range(iterations): + for benchmark_config in benchmark_configs: + args = ["python", str(benchmark)] + args.extend(benchmark_config[1]) + log_env_name_var = str(benchmark) + if benchmark_config[0] is not None: + log_env_name_var += f"_{benchmark_config[0]}" + run_benchmark_subprocess(args=args, log_env_name_var=log_env_name_var) def run_notebook_benchmark(benchmark_file: str, region: str): @@ -341,6 +363,19 @@ def parse_arguments(): help="Set the benchmarks to be published to BigQuery.", ) + parser.add_argument( + "--iterations", + type=int, + default=1, + help="Number of iterations to run each benchmark.", + ) + parser.add_argument( + "--output-csv", + type=str, + default=None, + help="Determines whether to output results to a CSV file. If no location is provided, a temporary location is automatically generated.", + ) + return parser.parse_args() @@ -348,28 +383,39 @@ def main(): args = parse_arguments() if args.publish_benchmarks: - bigquery_table = ( - "bigframes-metrics.benchmark_report.notebook_benchmark" - if args.notebook - else "bigframes-metrics.benchmark_report.benchmark" + benchmark_metrics = collect_benchmark_result( + args.publish_benchmarks, args.iterations ) - benchmark_metrics = collect_benchmark_result(args.publish_benchmarks) - - if os.getenv("BENCHMARK_AND_PUBLISH", "false") == "true": - repo_status = get_repository_status() - for idx, col in enumerate(repo_status.keys()): - benchmark_metrics.insert(idx, col, repo_status[col]) - - pandas_gbq.to_gbq( - dataframe=benchmark_metrics, - destination_table=bigquery_table, - if_exists="append", + # Output results to CSV without specifying a location + if args.output_csv == "True": + current_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + temp_file = tempfile.NamedTemporaryFile( + prefix=f"benchmark_{current_time}_", delete=False, suffix=".csv" ) - print("Results have been successfully uploaded to BigQuery.") + benchmark_metrics.to_csv(temp_file.name, index=False) + print( + f"Benchmark result is saved to a temporary location: {temp_file.name}" + ) + temp_file.close() + # Output results to CSV with specified a custom location + elif args.output_csv != "False": + benchmark_metrics.to_csv(args.output_csv, index=False) + print(f"Benchmark result is saved to: {args.output_csv}") + + # Publish the benchmark metrics to BigQuery under the 'bigframes-metrics' project. + # The 'BENCHMARK_AND_PUBLISH' environment variable should be set to 'true' only + # in specific Kokoro sessions. + if os.getenv("BENCHMARK_AND_PUBLISH", "false") == "true": + publish_to_bigquery(benchmark_metrics, args.notebook) + # If the 'GCLOUD_BENCH_PUBLISH_PROJECT' environment variable is set, publish the + # benchmark metrics to a specified BigQuery table in the provided project. This is + # intended for local testing where the default behavior is not to publish results. + elif project := os.getenv("GCLOUD_BENCH_PUBLISH_PROJECT", ""): + publish_to_bigquery(benchmark_metrics, args.notebook, project) elif args.notebook: run_notebook_benchmark(args.benchmark_path, args.region) else: - run_benchmark_from_config(args.benchmark_path) + run_benchmark_from_config(args.benchmark_path, args.iterations) if __name__ == "__main__": diff --git a/tests/benchmark/tpch/config.jsonl b/tests/benchmark/tpch/config.jsonl index 1c8b4dd19ae..e6f7a444f65 100644 --- a/tests/benchmark/tpch/config.jsonl +++ b/tests/benchmark/tpch/config.jsonl @@ -1,8 +1,10 @@ -{"benchmark_suffix": "1g_ordered", "dataset_id": "tpch_0001g", "ordered": true} -{"benchmark_suffix": "1g_unordered", "dataset_id": "tpch_0001g", "ordered": false} -{"benchmark_suffix": "10g_ordered", "dataset_id": "tpch_0010g", "ordered": true} -{"benchmark_suffix": "10g_unordered", "dataset_id": "tpch_0010g", "ordered": false} -{"benchmark_suffix": "100g_ordered", "dataset_id": "tpch_0100g", "ordered": true} -{"benchmark_suffix": "100g_unordered", "dataset_id": "tpch_0100g", "ordered": false} -{"benchmark_suffix": "1t_ordered", "dataset_id": "tpch_0001t", "ordered": true} -{"benchmark_suffix": "1t_unordered", "dataset_id": "tpch_0001t", "ordered": false} +{"benchmark_suffix": "1g_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0001g", "ordered": true} +{"benchmark_suffix": "1g_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0001g", "ordered": false} +{"benchmark_suffix": "10g_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0010g", "ordered": true} +{"benchmark_suffix": "10g_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0010g", "ordered": false} +{"benchmark_suffix": "100g_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0100g", "ordered": true} +{"benchmark_suffix": "100g_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0100g", "ordered": false} +{"benchmark_suffix": "1t_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0001t", "ordered": true} +{"benchmark_suffix": "1t_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0001t", "ordered": false} +{"benchmark_suffix": "10t_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0010t", "ordered": true} +{"benchmark_suffix": "10t_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "tpch_0010t", "ordered": false} diff --git a/tests/benchmark/tpch/q1.py b/tests/benchmark/tpch/q1.py index 398e9bf9b29..3f1c63967ec 100644 --- a/tests/benchmark/tpch/q1.py +++ b/tests/benchmark/tpch/q1.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q1 as vendored_tpch_q1 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q1.q, current_path, suffix, dataset_id, session + vendored_tpch_q1.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q10.py b/tests/benchmark/tpch/q10.py index e500a657b60..bea18975ca9 100644 --- a/tests/benchmark/tpch/q10.py +++ b/tests/benchmark/tpch/q10.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q10 as vendored_tpch_q10 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q10.q, current_path, suffix, dataset_id, session + vendored_tpch_q10.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q11.py b/tests/benchmark/tpch/q11.py index 46bd2f022c0..538d8d3e5f1 100644 --- a/tests/benchmark/tpch/q11.py +++ b/tests/benchmark/tpch/q11.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q11 as vendored_tpch_q11 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q11.q, current_path, suffix, dataset_id, session + vendored_tpch_q11.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q12.py b/tests/benchmark/tpch/q12.py index d2fab97699d..6503b543f49 100644 --- a/tests/benchmark/tpch/q12.py +++ b/tests/benchmark/tpch/q12.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q12 as vendored_tpch_q12 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q12.q, current_path, suffix, dataset_id, session + vendored_tpch_q12.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q13.py b/tests/benchmark/tpch/q13.py index 24eec7090c3..60c2101f6fe 100644 --- a/tests/benchmark/tpch/q13.py +++ b/tests/benchmark/tpch/q13.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q13 as vendored_tpch_q13 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q13.q, current_path, suffix, dataset_id, session + vendored_tpch_q13.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q14.py b/tests/benchmark/tpch/q14.py index 8aa7ed4d2ea..1698a016287 100644 --- a/tests/benchmark/tpch/q14.py +++ b/tests/benchmark/tpch/q14.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q14 as vendored_tpch_q14 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q14.q, current_path, suffix, dataset_id, session + vendored_tpch_q14.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q15.py b/tests/benchmark/tpch/q15.py index 511cfbc683b..49e2ce4e929 100644 --- a/tests/benchmark/tpch/q15.py +++ b/tests/benchmark/tpch/q15.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q15 as vendored_tpch_q15 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q15.q, current_path, suffix, dataset_id, session + vendored_tpch_q15.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q16.py b/tests/benchmark/tpch/q16.py index 1d1f4b5f307..ef6edf6b128 100644 --- a/tests/benchmark/tpch/q16.py +++ b/tests/benchmark/tpch/q16.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q16 as vendored_tpch_q16 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q16.q, current_path, suffix, dataset_id, session + vendored_tpch_q16.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q17.py b/tests/benchmark/tpch/q17.py index e285cc9fca2..2f680d206e1 100644 --- a/tests/benchmark/tpch/q17.py +++ b/tests/benchmark/tpch/q17.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q17 as vendored_tpch_q17 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q17.q, current_path, suffix, dataset_id, session + vendored_tpch_q17.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q18.py b/tests/benchmark/tpch/q18.py index de9e4f2f94f..7336246f1bd 100644 --- a/tests/benchmark/tpch/q18.py +++ b/tests/benchmark/tpch/q18.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q18 as vendored_tpch_q18 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q18.q, current_path, suffix, dataset_id, session + vendored_tpch_q18.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q19.py b/tests/benchmark/tpch/q19.py index 7b13b0d2507..3bf34794bfc 100644 --- a/tests/benchmark/tpch/q19.py +++ b/tests/benchmark/tpch/q19.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q19 as vendored_tpch_q19 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q19.q, current_path, suffix, dataset_id, session + vendored_tpch_q19.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q2.py b/tests/benchmark/tpch/q2.py index 57151e36bdd..c738aae124e 100644 --- a/tests/benchmark/tpch/q2.py +++ b/tests/benchmark/tpch/q2.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q2 as vendored_tpch_q2 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q2.q, current_path, suffix, dataset_id, session + vendored_tpch_q2.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q20.py b/tests/benchmark/tpch/q20.py index e02e9306f88..1b254c6a785 100644 --- a/tests/benchmark/tpch/q20.py +++ b/tests/benchmark/tpch/q20.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q20 as vendored_tpch_q20 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q20.q, current_path, suffix, dataset_id, session + vendored_tpch_q20.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q21.py b/tests/benchmark/tpch/q21.py index d123286c3e4..18e8df87fe7 100644 --- a/tests/benchmark/tpch/q21.py +++ b/tests/benchmark/tpch/q21.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q21 as vendored_tpch_q21 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q21.q, current_path, suffix, dataset_id, session + vendored_tpch_q21.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q22.py b/tests/benchmark/tpch/q22.py index 841740da0e2..6c10021c2b6 100644 --- a/tests/benchmark/tpch/q22.py +++ b/tests/benchmark/tpch/q22.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q22 as vendored_tpch_q22 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q22.q, current_path, suffix, dataset_id, session + vendored_tpch_q22.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q3.py b/tests/benchmark/tpch/q3.py index 28cdfed49c0..5bcaaa0d5da 100644 --- a/tests/benchmark/tpch/q3.py +++ b/tests/benchmark/tpch/q3.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q3 as vendored_tpch_q3 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q3.q, current_path, suffix, dataset_id, session + vendored_tpch_q3.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q4.py b/tests/benchmark/tpch/q4.py index aa67cc77a07..462c6336d10 100644 --- a/tests/benchmark/tpch/q4.py +++ b/tests/benchmark/tpch/q4.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q4 as vendored_tpch_q4 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q4.q, current_path, suffix, dataset_id, session + vendored_tpch_q4.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q5.py b/tests/benchmark/tpch/q5.py index bc39b76e82d..108cde58ccb 100644 --- a/tests/benchmark/tpch/q5.py +++ b/tests/benchmark/tpch/q5.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q5 as vendored_tpch_q5 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q5.q, current_path, suffix, dataset_id, session + vendored_tpch_q5.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q6.py b/tests/benchmark/tpch/q6.py index a1933330454..ccefc1b0bff 100644 --- a/tests/benchmark/tpch/q6.py +++ b/tests/benchmark/tpch/q6.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q6 as vendored_tpch_q6 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q6.q, current_path, suffix, dataset_id, session + vendored_tpch_q6.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q7.py b/tests/benchmark/tpch/q7.py index 8a17eb91eab..0cad599a607 100644 --- a/tests/benchmark/tpch/q7.py +++ b/tests/benchmark/tpch/q7.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q7 as vendored_tpch_q7 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q7.q, current_path, suffix, dataset_id, session + vendored_tpch_q7.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q8.py b/tests/benchmark/tpch/q8.py index b5e7d7aa376..6c6ac23b9bb 100644 --- a/tests/benchmark/tpch/q8.py +++ b/tests/benchmark/tpch/q8.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q8 as vendored_tpch_q8 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q8.q, current_path, suffix, dataset_id, session + vendored_tpch_q8.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/tpch/q9.py b/tests/benchmark/tpch/q9.py index c098e9a8dac..05c82fb66ec 100644 --- a/tests/benchmark/tpch/q9.py +++ b/tests/benchmark/tpch/q9.py @@ -17,9 +17,9 @@ import bigframes_vendored.tpch.queries.q9 as vendored_tpch_q9 if __name__ == "__main__": - dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_tpch_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_tpch_q9.q, current_path, suffix, dataset_id, session + vendored_tpch_q9.q, current_path, suffix, project_id, dataset_id, session ) diff --git a/tests/benchmark/utils.py b/tests/benchmark/utils.py index c286d4e2292..32be33fc74b 100644 --- a/tests/benchmark/utils.py +++ b/tests/benchmark/utils.py @@ -43,6 +43,12 @@ def get_dbbenchmark_configuration(): def get_tpch_configuration(): parser = argparse.ArgumentParser(description="Process TPC-H Query using BigFrames.") + parser.add_argument( + "--project_id", + type=str, + required=True, + help="The BigQuery dataset ID to query.", + ) parser.add_argument( "--dataset_id", type=str, @@ -62,7 +68,7 @@ def get_tpch_configuration(): args = parser.parse_args() session = _initialize_session(_str_to_bool(args.ordered)) - return args.dataset_id, session, args.benchmark_suffix + return args.project_id, args.dataset_id, session, args.benchmark_suffix def get_execution_time(func, current_path, suffix, *args, **kwargs): @@ -73,7 +79,7 @@ def get_execution_time(func, current_path, suffix, *args, **kwargs): clock_time_file_path = f"{current_path}_{suffix}.local_exec_time_seconds" - with open(clock_time_file_path, "w") as log_file: + with open(clock_time_file_path, "a") as log_file: log_file.write(f"{runtime}\n") diff --git a/third_party/bigframes_vendored/tpch/queries/q1.py b/third_party/bigframes_vendored/tpch/queries/q1.py index 3f155ec1c9d..e1fdf85f58e 100644 --- a/third_party/bigframes_vendored/tpch/queries/q1.py +++ b/third_party/bigframes_vendored/tpch/queries/q1.py @@ -7,9 +7,9 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q10.py b/third_party/bigframes_vendored/tpch/queries/q10.py index 95b7b87b0e7..75a8f2de7fb 100644 --- a/third_party/bigframes_vendored/tpch/queries/q10.py +++ b/third_party/bigframes_vendored/tpch/queries/q10.py @@ -7,21 +7,21 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q11.py b/third_party/bigframes_vendored/tpch/queries/q11.py index 269728f6f1c..484a7c00018 100644 --- a/third_party/bigframes_vendored/tpch/queries/q11.py +++ b/third_party/bigframes_vendored/tpch/queries/q11.py @@ -4,17 +4,17 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) partsupp = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + f"{project_id}.{dataset_id}.PARTSUPP", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q12.py b/third_party/bigframes_vendored/tpch/queries/q12.py index 1c392759323..e2b7aaf9f21 100644 --- a/third_party/bigframes_vendored/tpch/queries/q12.py +++ b/third_party/bigframes_vendored/tpch/queries/q12.py @@ -7,13 +7,13 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q13.py b/third_party/bigframes_vendored/tpch/queries/q13.py index dd69cf509f6..ea2f0da2848 100644 --- a/third_party/bigframes_vendored/tpch/queries/q13.py +++ b/third_party/bigframes_vendored/tpch/queries/q13.py @@ -6,13 +6,13 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q14.py b/third_party/bigframes_vendored/tpch/queries/q14.py index 8c25a5897dc..27f3d9e224b 100644 --- a/third_party/bigframes_vendored/tpch/queries/q14.py +++ b/third_party/bigframes_vendored/tpch/queries/q14.py @@ -5,13 +5,13 @@ import bigframes -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q15.py b/third_party/bigframes_vendored/tpch/queries/q15.py index fae3010e366..042adbda8ba 100644 --- a/third_party/bigframes_vendored/tpch/queries/q15.py +++ b/third_party/bigframes_vendored/tpch/queries/q15.py @@ -6,13 +6,13 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q16.py b/third_party/bigframes_vendored/tpch/queries/q16.py index b5eb62547f0..1bd2795c42a 100644 --- a/third_party/bigframes_vendored/tpch/queries/q16.py +++ b/third_party/bigframes_vendored/tpch/queries/q16.py @@ -4,17 +4,17 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) partsupp = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + f"{project_id}.{dataset_id}.PARTSUPP", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q17.py b/third_party/bigframes_vendored/tpch/queries/q17.py index a95d128b5c4..0bd1c443153 100644 --- a/third_party/bigframes_vendored/tpch/queries/q17.py +++ b/third_party/bigframes_vendored/tpch/queries/q17.py @@ -4,13 +4,13 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q18.py b/third_party/bigframes_vendored/tpch/queries/q18.py index dac9f5c4384..f645a08681e 100644 --- a/third_party/bigframes_vendored/tpch/queries/q18.py +++ b/third_party/bigframes_vendored/tpch/queries/q18.py @@ -6,17 +6,17 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q19.py b/third_party/bigframes_vendored/tpch/queries/q19.py index 526d0aa1a62..1371af53fc0 100644 --- a/third_party/bigframes_vendored/tpch/queries/q19.py +++ b/third_party/bigframes_vendored/tpch/queries/q19.py @@ -4,13 +4,13 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q2.py b/third_party/bigframes_vendored/tpch/queries/q2.py index 0586a5d1a53..f388252993d 100644 --- a/third_party/bigframes_vendored/tpch/queries/q2.py +++ b/third_party/bigframes_vendored/tpch/queries/q2.py @@ -3,25 +3,25 @@ import bigframes -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): region = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.REGION", + f"{project_id}.{dataset_id}.REGION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) partsupp = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + f"{project_id}.{dataset_id}.PARTSUPP", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q20.py b/third_party/bigframes_vendored/tpch/queries/q20.py index 671d7e06fb0..26651a31c4b 100644 --- a/third_party/bigframes_vendored/tpch/queries/q20.py +++ b/third_party/bigframes_vendored/tpch/queries/q20.py @@ -6,25 +6,25 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) partsupp = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + f"{project_id}.{dataset_id}.PARTSUPP", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q21.py b/third_party/bigframes_vendored/tpch/queries/q21.py index 3a4ea495c92..b844cd6f6b1 100644 --- a/third_party/bigframes_vendored/tpch/queries/q21.py +++ b/third_party/bigframes_vendored/tpch/queries/q21.py @@ -6,21 +6,21 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q22.py b/third_party/bigframes_vendored/tpch/queries/q22.py index 97180cd11a8..137a7d5c36e 100644 --- a/third_party/bigframes_vendored/tpch/queries/q22.py +++ b/third_party/bigframes_vendored/tpch/queries/q22.py @@ -4,13 +4,13 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q3.py b/third_party/bigframes_vendored/tpch/queries/q3.py index 94b85f20543..fb09abe1594 100644 --- a/third_party/bigframes_vendored/tpch/queries/q3.py +++ b/third_party/bigframes_vendored/tpch/queries/q3.py @@ -5,17 +5,17 @@ import bigframes -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q4.py b/third_party/bigframes_vendored/tpch/queries/q4.py index 9cc6f73c989..b89f70845f1 100644 --- a/third_party/bigframes_vendored/tpch/queries/q4.py +++ b/third_party/bigframes_vendored/tpch/queries/q4.py @@ -8,13 +8,13 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q5.py b/third_party/bigframes_vendored/tpch/queries/q5.py index b3a9b5e614b..9839c025a5f 100644 --- a/third_party/bigframes_vendored/tpch/queries/q5.py +++ b/third_party/bigframes_vendored/tpch/queries/q5.py @@ -5,29 +5,29 @@ import bigframes -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): region = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.REGION", + f"{project_id}.{dataset_id}.REGION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q6.py b/third_party/bigframes_vendored/tpch/queries/q6.py index 13341c4f4ed..b883837fe2f 100644 --- a/third_party/bigframes_vendored/tpch/queries/q6.py +++ b/third_party/bigframes_vendored/tpch/queries/q6.py @@ -5,9 +5,9 @@ import bigframes -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q7.py b/third_party/bigframes_vendored/tpch/queries/q7.py index d922efd1e21..a4dfe3f12e7 100644 --- a/third_party/bigframes_vendored/tpch/queries/q7.py +++ b/third_party/bigframes_vendored/tpch/queries/q7.py @@ -8,25 +8,25 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q8.py b/third_party/bigframes_vendored/tpch/queries/q8.py index 4520fce14af..1676ec63495 100644 --- a/third_party/bigframes_vendored/tpch/queries/q8.py +++ b/third_party/bigframes_vendored/tpch/queries/q8.py @@ -5,33 +5,33 @@ import bigframes -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): customer = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.CUSTOMER", + f"{project_id}.{dataset_id}.CUSTOMER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) region = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.REGION", + f"{project_id}.{dataset_id}.REGION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) diff --git a/third_party/bigframes_vendored/tpch/queries/q9.py b/third_party/bigframes_vendored/tpch/queries/q9.py index d04117f0f13..c2b52789bdd 100644 --- a/third_party/bigframes_vendored/tpch/queries/q9.py +++ b/third_party/bigframes_vendored/tpch/queries/q9.py @@ -6,29 +6,29 @@ import bigframes.pandas as bpd -def q(dataset_id: str, session: bigframes.Session): +def q(project_id: str, dataset_id: str, session: bigframes.Session): lineitem = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.LINEITEM", + f"{project_id}.{dataset_id}.LINEITEM", index_col=bigframes.enums.DefaultIndexKind.NULL, ) nation = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.NATION", + f"{project_id}.{dataset_id}.NATION", index_col=bigframes.enums.DefaultIndexKind.NULL, ) orders = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.ORDERS", + f"{project_id}.{dataset_id}.ORDERS", index_col=bigframes.enums.DefaultIndexKind.NULL, ) part = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PART", + f"{project_id}.{dataset_id}.PART", index_col=bigframes.enums.DefaultIndexKind.NULL, ) partsupp = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.PARTSUPP", + f"{project_id}.{dataset_id}.PARTSUPP", index_col=bigframes.enums.DefaultIndexKind.NULL, ) supplier = session.read_gbq( - f"bigframes-dev-perf.{dataset_id}.SUPPLIER", + f"{project_id}.{dataset_id}.SUPPLIER", index_col=bigframes.enums.DefaultIndexKind.NULL, ) From d2fb49f84d3e27d79d3248227d057fa1f57bb897 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:21:14 -0700 Subject: [PATCH 838/921] chore(main): release 1.18.0 (#986) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 21 +++++++++++++++++++++ bigframes/version.py | 2 +- third_party/bigframes_vendored/version.py | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a989d8af662..c210f5c991f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.18.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.17.0...v1.18.0) (2024-09-18) + + +### Features + +* Add "include" param to describe for string types ([#973](https://github.com/googleapis/python-bigquery-dataframes/issues/973)) ([deac6d2](https://github.com/googleapis/python-bigquery-dataframes/commit/deac6d2d6e459b26c05f6e5ff328ea03a3cff45f)) +* Add `subset` parameter to `DataFrame.dropna` to select which columns to consider ([#981](https://github.com/googleapis/python-bigquery-dataframes/issues/981)) ([f7c03dc](https://github.com/googleapis/python-bigquery-dataframes/commit/f7c03dcaf7ee4d62497f6653851e390795fc60a2)) + + +### Bug Fixes + +* DataFrameGroupby.agg now works with unnamed tuples ([#985](https://github.com/googleapis/python-bigquery-dataframes/issues/985)) ([0f047b4](https://github.com/googleapis/python-bigquery-dataframes/commit/0f047b4fae2a10b2a465c506bea561f8bb8d4262)) +* Fix a bug that raises exception when re-indexing columns with their original order ([#988](https://github.com/googleapis/python-bigquery-dataframes/issues/988)) ([596b03b](https://github.com/googleapis/python-bigquery-dataframes/commit/596b03bb3ea27cead9b90200b9ef3cdcd99ca184)) +* Make the `Series.apply` outcome `assign`able to the original dataframe in partial ordering mode ([#874](https://github.com/googleapis/python-bigquery-dataframes/issues/874)) ([c94ead9](https://github.com/googleapis/python-bigquery-dataframes/commit/c94ead996e3bfa98edd51ff678a3d43a10ee980f)) + + +### Dependencies + +* Limit ibis-framework version to 9.2.0 ([#989](https://github.com/googleapis/python-bigquery-dataframes/issues/989)) ([06c1b33](https://github.com/googleapis/python-bigquery-dataframes/commit/06c1b3396d77d1de4f927328bae70cd7b3eb0b0b)) +* Update to ibis-framework 9.x and newer sqlglot ([#827](https://github.com/googleapis/python-bigquery-dataframes/issues/827)) ([89ea44f](https://github.com/googleapis/python-bigquery-dataframes/commit/89ea44fb66314b134fc0a10d816c1659978d4182)) + ## [1.17.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.16.0...v1.17.0) (2024-09-11) diff --git a/bigframes/version.py b/bigframes/version.py index 2c0c6e4d3ac..745632616c9 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0" +__version__ = "1.18.0" diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index 2c0c6e4d3ac..745632616c9 100644 --- a/third_party/bigframes_vendored/version.py +++ b/third_party/bigframes_vendored/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.17.0" +__version__ = "1.18.0" From ccd3c03cf99951465162d4d4316e6c3868bdaa16 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:07:35 -0700 Subject: [PATCH 839/921] test: move Claude3 tests to load test (#997) * test: move Claude3 tests to load test * add conftest --- tests/system/conftest.py | 34 +++++++++++----- tests/system/load/conftest.py | 39 ++++++++++++++++++ tests/system/load/test_llm.py | 66 ++++++++++++++++++++++++++++++ tests/system/small/ml/conftest.py | 24 ----------- tests/system/small/ml/test_llm.py | 67 ------------------------------- 5 files changed, 129 insertions(+), 101 deletions(-) create mode 100644 tests/system/load/conftest.py diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 9cfb9082af5..5ee2dc63972 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -146,16 +146,6 @@ def session() -> Generator[bigframes.Session, None, None]: session.close() # close generated session at cleanup time -@pytest.fixture(scope="session") -def session_us_east5() -> Generator[bigframes.Session, None, None]: - context = bigframes.BigQueryOptions( - location="us-east5", - ) - session = bigframes.Session(context=context) - yield session - session.close() # close generated session at cleanup time - - @pytest.fixture(scope="session") def session_load() -> Generator[bigframes.Session, None, None]: context = bigframes.BigQueryOptions(location="US", project="bigframes-load-testing") @@ -188,6 +178,11 @@ def session_tokyo(tokyo_location: str) -> Generator[bigframes.Session, None, Non session.close() # close generated session at cleanup type +@pytest.fixture(scope="session") +def bq_connection(bigquery_client: bigquery.Client) -> str: + return f"{bigquery_client.project}.{bigquery_client.location}.bigframes-rf-conn" + + @pytest.fixture(scope="session", autouse=True) def cleanup_datasets(bigquery_client: bigquery.Client) -> None: """Cleanup any datasets that were created but not cleaned up.""" @@ -728,6 +723,25 @@ def new_penguins_df(session, new_penguins_pandas_df): return session.read_pandas(new_penguins_pandas_df) +@pytest.fixture(scope="session") +def llm_text_pandas_df(): + """Additional data matching the penguins dataset, with a new index""" + return pd.DataFrame( + { + "prompt": [ + "What is BigQuery?", + "What is BQML?", + "What is BigQuery DataFrame?", + ], + } + ) + + +@pytest.fixture(scope="session") +def llm_text_df(session, llm_text_pandas_df): + return session.read_pandas(llm_text_pandas_df) + + @pytest.fixture(scope="session") def penguins_linear_model_name( session: bigframes.Session, dataset_id_permanent, penguins_table_id diff --git a/tests/system/load/conftest.py b/tests/system/load/conftest.py new file mode 100644 index 00000000000..f15f50c7e73 --- /dev/null +++ b/tests/system/load/conftest.py @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Generator + +import pytest + +import bigframes + + +# Override the session to target at bigframes-load-testing at all load tests. That allows to run load tests locally with authentic env. +@pytest.fixture(scope="session") +def session() -> Generator[bigframes.Session, None, None]: + context = bigframes.BigQueryOptions(location="US", project="bigframes-load-testing") + session = bigframes.Session(context=context) + yield session + session.close() # close generated session at cleanup time + + +@pytest.fixture(scope="session") +def session_us_east5() -> Generator[bigframes.Session, None, None]: + context = bigframes.BigQueryOptions( + location="us-east5", project="bigframes-load-testing" + ) + session = bigframes.Session(context=context) + yield session + session.close() # close generated session at cleanup time diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index 6d22963a97a..1d133001151 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -90,3 +90,69 @@ def test_llm_gemini_configure_fit(llm_fine_tune_df_default_index, llm_remote_tex index=3, ) # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept + + +# (b/366290533): Claude models are of extremely low capacity. The tests should reside in small tests. Moving these here just to protect BQML's shared capacity(as load test only runs once per day.) and make sure we still have minimum coverage. +@pytest.mark.parametrize( + "model_name", + ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), +) +@pytest.mark.flaky(retries=3, delay=120) +def test_claude3_text_generator_create_load( + dataset_id, model_name, session, session_us_east5, bq_connection +): + if model_name in ("claude-3-5-sonnet", "claude-3-opus"): + session = session_us_east5 + claude3_text_generator_model = llm.Claude3TextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + assert claude3_text_generator_model is not None + assert claude3_text_generator_model._bqml_model is not None + + # save, load to ensure configuration was kept + reloaded_model = claude3_text_generator_model.to_gbq( + f"{dataset_id}.temp_text_model", replace=True + ) + assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name + assert reloaded_model.connection_name == bq_connection + assert reloaded_model.model_name == model_name + + +@pytest.mark.parametrize( + "model_name", + ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), +) +@pytest.mark.flaky(retries=3, delay=120) +def test_claude3_text_generator_predict_default_params_success( + llm_text_df, model_name, session, session_us_east5, bq_connection +): + if model_name in ("claude-3-5-sonnet", "claude-3-opus"): + session = session_us_east5 + claude3_text_generator_model = llm.Claude3TextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + df = claude3_text_generator_model.predict(llm_text_df).to_pandas() + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) + + +@pytest.mark.parametrize( + "model_name", + ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), +) +@pytest.mark.flaky(retries=3, delay=120) +def test_claude3_text_generator_predict_with_params_success( + llm_text_df, model_name, session, session_us_east5, bq_connection +): + if model_name in ("claude-3-5-sonnet", "claude-3-opus"): + session = session_us_east5 + claude3_text_generator_model = llm.Claude3TextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + df = claude3_text_generator_model.predict( + llm_text_df, max_output_tokens=100, top_k=20, top_p=0.5 + ).to_pandas() + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False + ) diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index ee966466870..c1643776a5a 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -34,11 +34,6 @@ ) -@pytest.fixture(scope="session") -def bq_connection(bigquery_client) -> str: - return f"{bigquery_client.project}.us.bigframes-rf-conn" - - @pytest.fixture(scope="session") def penguins_bqml_linear_model(session, penguins_linear_model_name) -> core.BqmlModel: model = session.bqclient.get_model(penguins_linear_model_name) @@ -157,20 +152,6 @@ def penguins_pca_model( ) -@pytest.fixture(scope="session") -def llm_text_pandas_df(): - """Additional data matching the penguins dataset, with a new index""" - return pd.DataFrame( - { - "prompt": [ - "What is BigQuery?", - "What is BQML?", - "What is BigQuery DataFrame?", - ], - } - ) - - @pytest.fixture(scope="session") def onnx_iris_pandas_df(): """Data matching the iris dataset.""" @@ -212,11 +193,6 @@ def xgboost_iris_df(session, xgboost_iris_pandas_df): return session.read_pandas(xgboost_iris_pandas_df) -@pytest.fixture(scope="session") -def llm_text_df(session, llm_text_pandas_df): - return session.read_pandas(llm_text_pandas_df) - - @pytest.fixture(scope="session") def bqml_palm2_text_generator_model(session, bq_connection) -> core.BqmlModel: options = { diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index cd5d65c458a..914548be58e 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -414,73 +414,6 @@ def test_gemini_text_generator_predict_with_params_success( ) -@pytest.mark.parametrize( - "model_name", - ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), -) -@pytest.mark.flaky(retries=3, delay=120) -def test_claude3_text_generator_create_load( - dataset_id, model_name, session, session_us_east5, bq_connection -): - if model_name in ("claude-3-5-sonnet", "claude-3-opus"): - session = session_us_east5 - claude3_text_generator_model = llm.Claude3TextGenerator( - model_name=model_name, connection_name=bq_connection, session=session - ) - assert claude3_text_generator_model is not None - assert claude3_text_generator_model._bqml_model is not None - - # save, load to ensure configuration was kept - reloaded_model = claude3_text_generator_model.to_gbq( - f"{dataset_id}.temp_text_model", replace=True - ) - assert f"{dataset_id}.temp_text_model" == reloaded_model._bqml_model.model_name - assert reloaded_model.connection_name == bq_connection - assert reloaded_model.model_name == model_name - - -@pytest.mark.skip("b/366290533 too many requests are exhausting bqml capacity") -@pytest.mark.parametrize( - "model_name", - ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), -) -@pytest.mark.flaky(retries=3, delay=120) -def test_claude3_text_generator_predict_default_params_success( - llm_text_df, model_name, session, session_us_east5, bq_connection -): - if model_name in ("claude-3-5-sonnet", "claude-3-opus"): - session = session_us_east5 - claude3_text_generator_model = llm.Claude3TextGenerator( - model_name=model_name, connection_name=bq_connection, session=session - ) - df = claude3_text_generator_model.predict(llm_text_df).to_pandas() - utils.check_pandas_df_schema_and_index( - df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False - ) - - -@pytest.mark.skip("b/366290533 too many requests are exhausting bqml capacity") -@pytest.mark.parametrize( - "model_name", - ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), -) -@pytest.mark.flaky(retries=3, delay=120) -def test_claude3_text_generator_predict_with_params_success( - llm_text_df, model_name, session, session_us_east5, bq_connection -): - if model_name in ("claude-3-5-sonnet", "claude-3-opus"): - session = session_us_east5 - claude3_text_generator_model = llm.Claude3TextGenerator( - model_name=model_name, connection_name=bq_connection, session=session - ) - df = claude3_text_generator_model.predict( - llm_text_df, max_output_tokens=100, top_k=20, top_p=0.5 - ).to_pandas() - utils.check_pandas_df_schema_and_index( - df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False - ) - - @pytest.mark.flaky(retries=2) def test_llm_palm_score(llm_fine_tune_df_default_index): model = llm.PaLM2TextGenerator(model_name="text-bison") From 4221632e69f1e4866d5db3cc183021ae8f80b245 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:34:54 -0700 Subject: [PATCH 840/921] chore: include unittest workflow in owlbot (#990) --- owlbot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/owlbot.py b/owlbot.py index ca3c8cbe14c..b29384d4625 100644 --- a/owlbot.py +++ b/owlbot.py @@ -49,7 +49,6 @@ "README.rst", "CONTRIBUTING.rst", ".github/release-trigger.yml", - ".github/workflows/unittest.yml", # BigQuery DataFrames manages its own Kokoro cluster for presubmit & continuous tests. ".kokoro/build.sh", ".kokoro/continuous/common.cfg", From cc48f58cbd94f8110ee863eb57d3fe8dc5a17778 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Thu, 19 Sep 2024 16:42:15 -0700 Subject: [PATCH 841/921] feat: Support bool and bytes types in `describe(include='all')` (#994) * feat: Support bool and bytes types in describe(include='all') * update aggregation unit tests * fix typo and remove unnecessary helper * remove unnecessary dep * fix wording --- bigframes/dataframe.py | 48 ++++++++++++---------- bigframes/operations/aggregations.py | 2 +- tests/system/small/test_dataframe.py | 26 ++++++------ tests/unit/operations/test_aggregations.py | 39 +++++++----------- 4 files changed, 55 insertions(+), 60 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 862c8dc2c80..817a02f492d 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2303,7 +2303,7 @@ def melt( self._block.melt(id_col_ids, val_col_ids, var_name, value_name) ) - _NUMERICAL_DISCRIBE_AGGS = ( + _NUMERIC_DESCRIBE_AGGS = ( "count", "mean", "std", @@ -2313,41 +2313,53 @@ def melt( "75%", "max", ) - _NON_NUMERICAL_DESCRIBE_AGGS = ("count", "nunique") + _NON_NUMERIC_DESCRIBE_AGGS = ("count", "nunique") def describe(self, include: None | Literal["all"] = None) -> DataFrame: + + allowed_non_numeric_types = { + bigframes.dtypes.STRING_DTYPE, + bigframes.dtypes.BOOL_DTYPE, + bigframes.dtypes.BYTES_DTYPE, + } + if include is None: numeric_df = self._drop_non_numeric(permissive=False) if len(numeric_df.columns) == 0: - # Describe eligible non-numerical columns - result = self._drop_non_string().agg(self._NON_NUMERICAL_DESCRIBE_AGGS) + # Describe eligible non-numeric columns + result = self.select_dtypes(include=allowed_non_numeric_types).agg( + self._NON_NUMERIC_DESCRIBE_AGGS + ) else: - # Otherwise, only describe numerical columns - result = numeric_df.agg(self._NUMERICAL_DISCRIBE_AGGS) + # Otherwise, only describe numeric columns + result = numeric_df.agg(self._NUMERIC_DESCRIBE_AGGS) return typing.cast(DataFrame, result) elif include == "all": numeric_result = typing.cast( DataFrame, self._drop_non_numeric(permissive=False).agg( - self._NUMERICAL_DISCRIBE_AGGS + self._NUMERIC_DESCRIBE_AGGS ), ) - string_result = typing.cast( + + non_numeric_result = typing.cast( DataFrame, - self._drop_non_string().agg(self._NON_NUMERICAL_DESCRIBE_AGGS), + self.select_dtypes(include=allowed_non_numeric_types).agg( + self._NON_NUMERIC_DESCRIBE_AGGS + ), ) if len(numeric_result.columns) == 0: - return string_result - elif len(string_result.columns) == 0: + return non_numeric_result + elif len(non_numeric_result.columns) == 0: return numeric_result else: import bigframes.core.reshape as rs # Use reindex after join to preserve the original column order. return rs.concat( - [numeric_result, string_result], axis=1 + [non_numeric_result, numeric_result], axis=1 )._reindex_columns(self.columns) else: @@ -2549,7 +2561,7 @@ def unstack(self, level: LevelsType = -1): return DataFrame(pivot_block) def _drop_non_numeric(self, permissive=True) -> DataFrame: - numerical_types = ( + numeric_types = ( set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) if permissive else set(bigframes.dtypes.NUMERIC_BIGFRAMES_TYPES_RESTRICTIVE) @@ -2557,18 +2569,10 @@ def _drop_non_numeric(self, permissive=True) -> DataFrame: non_numeric_cols = [ col_id for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) - if dtype not in numerical_types + if dtype not in numeric_types ] return DataFrame(self._block.drop_columns(non_numeric_cols)) - def _drop_non_string(self) -> DataFrame: - string_cols = [ - col_id - for col_id, dtype in zip(self._block.value_columns, self._block.dtypes) - if dtype == bigframes.dtypes.STRING_DTYPE - ] - return DataFrame(self._block.select_columns(string_cols)) - def _drop_non_bool(self) -> DataFrame: non_bool_cols = [ col_id diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index f20429e4498..d071889ac46 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -568,7 +568,7 @@ def is_agg_op_supported(dtype: dtypes.Dtype, op: AggregateOp) -> bool: if dtype in dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE: return True - if dtype == dtypes.STRING_DTYPE: + if dtype in (dtypes.STRING_DTYPE, dtypes.BOOL_DTYPE, dtypes.BYTES_DTYPE): return isinstance(op, (CountOp, NuniqueOp)) # For all other types, support no aggregation diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index b4c81bfbef5..0a637e983f2 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -2619,15 +2619,15 @@ def test_df_describe(scalars_dfs): @skip_legacy_pandas @pytest.mark.parametrize("include", [None, "all"]) -def test_df_describe_non_numerical(scalars_dfs, include): +def test_df_describe_non_numeric(scalars_dfs, include): scalars_df, scalars_pandas_df = scalars_dfs - non_numerical_columns = ["string_col"] + non_numeric_columns = ["string_col", "bytes_col", "bool_col"] - modified_bf = scalars_df[non_numerical_columns] + modified_bf = scalars_df[non_numeric_columns] bf_result = modified_bf.describe(include=include).to_pandas() - modified_pd_df = scalars_pandas_df[non_numerical_columns] + modified_pd_df = scalars_pandas_df[non_numeric_columns] pd_result = modified_pd_df.describe(include=include) # Reindex results with the specified keys and their order, because @@ -2639,8 +2639,8 @@ def test_df_describe_non_numerical(scalars_dfs, include): ).rename(index={"unique": "nunique"}) pd.testing.assert_frame_equal( - pd_result[non_numerical_columns].astype("Int64"), - bf_result[non_numerical_columns], + pd_result[non_numeric_columns].astype("Int64"), + bf_result[non_numeric_columns], check_index_type=False, ) @@ -2649,12 +2649,12 @@ def test_df_describe_non_numerical(scalars_dfs, include): def test_df_describe_mixed_types_include_all(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs - numerical_columns = [ + numeric_columns = [ "int64_col", "float64_col", ] - non_numerical_columns = ["string_col"] - supported_columns = numerical_columns + non_numerical_columns + non_numeric_columns = ["string_col"] + supported_columns = numeric_columns + non_numeric_columns modified_bf = scalars_df[supported_columns] bf_result = modified_bf.describe(include="all").to_pandas() @@ -2678,14 +2678,14 @@ def test_df_describe_mixed_types_include_all(scalars_dfs): ).rename(index={"unique": "nunique"}) pd.testing.assert_frame_equal( - pd_result[numerical_columns].astype("Float64"), - bf_result[numerical_columns], + pd_result[numeric_columns].astype("Float64"), + bf_result[numeric_columns], check_index_type=False, ) pd.testing.assert_frame_equal( - pd_result[non_numerical_columns].astype("Int64"), - bf_result[non_numerical_columns], + pd_result[non_numeric_columns].astype("Int64"), + bf_result[non_numeric_columns], check_index_type=False, ) diff --git a/tests/unit/operations/test_aggregations.py b/tests/unit/operations/test_aggregations.py index 4cb6934c9d3..68ad48ac297 100644 --- a/tests/unit/operations/test_aggregations.py +++ b/tests/unit/operations/test_aggregations.py @@ -55,38 +55,29 @@ first_op, ] ) -_STRING_SUPPORTED_OPS = set([count_op, nunique_op]) @pytest.mark.parametrize("dtype", dtypes.NUMERIC_BIGFRAMES_TYPES_PERMISSIVE) @pytest.mark.parametrize("op", _ALL_OPS) -def test_is_agg_op_supported_numerical_support_all(dtype, op): +def test_is_agg_op_supported_numeric_support_all(dtype, op): assert is_agg_op_supported(dtype, op) is True -@pytest.mark.parametrize("dtype", [dtypes.STRING_DTYPE]) -@pytest.mark.parametrize("op", _STRING_SUPPORTED_OPS) -def test_is_agg_op_supported_string_support_ops(dtype, op): - assert is_agg_op_supported(dtype, op) is True - - -@pytest.mark.parametrize("dtype", [dtypes.STRING_DTYPE]) -@pytest.mark.parametrize("op", _ALL_OPS - _STRING_SUPPORTED_OPS) -def test_is_agg_op_supported_string_not_support_ops(dtype, op): - assert is_agg_op_supported(dtype, op) is False - - @pytest.mark.parametrize( - "dtype", + ("dtype", "supported_ops"), [ - dtypes.BYTES_DTYPE, - dtypes.DATE_DTYPE, - dtypes.TIME_DTYPE, - dtypes.DATETIME_DTYPE, - dtypes.TIMESTAMP_DTYPE, - dtypes.GEO_DTYPE, + (dtypes.STRING_DTYPE, {count_op, nunique_op}), + (dtypes.BYTES_DTYPE, {count_op, nunique_op}), + (dtypes.DATE_DTYPE, set()), + (dtypes.TIME_DTYPE, set()), + (dtypes.DATETIME_DTYPE, set()), + (dtypes.TIMESTAMP_DTYPE, set()), + (dtypes.GEO_DTYPE, set()), ], ) -@pytest.mark.parametrize("op", _ALL_OPS) -def test_is_agg_op_supported_non_numerical_no_support(dtype, op): - assert is_agg_op_supported(dtype, op) is False +def test_is_agg_op_supported_non_numeric(dtype, supported_ops): + for op in supported_ops: + assert is_agg_op_supported(dtype, op) is True + + for op in _ALL_OPS - supported_ops: + assert is_agg_op_supported(dtype, op) is False From 8520873a6e4ba8e572b196e610454793e0fce9af Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:02:58 -0700 Subject: [PATCH 842/921] test: disable deprecated embedding model tests (#1006) * test: disable deprecated embedding models * delete tests --- tests/system/small/ml/test_llm.py | 119 ------------------------------ 1 file changed, 119 deletions(-) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 914548be58e..3093a36534b 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -178,125 +178,6 @@ def test_text_generator_predict_with_params_success( ) -def test_create_embedding_generator_model( - palm2_embedding_generator_model, dataset_id, bq_connection -): - # Model creation doesn't return error - assert palm2_embedding_generator_model is not None - assert palm2_embedding_generator_model._bqml_model is not None - - # save, load to ensure configuration was kept - reloaded_model = palm2_embedding_generator_model.to_gbq( - f"{dataset_id}.temp_embedding_model", replace=True - ) - assert f"{dataset_id}.temp_embedding_model" == reloaded_model._bqml_model.model_name - assert reloaded_model.model_name == "textembedding-gecko" - assert reloaded_model.connection_name == bq_connection - - -def test_create_embedding_generator_model_002( - palm2_embedding_generator_model_002, dataset_id, bq_connection -): - # Model creation doesn't return error - assert palm2_embedding_generator_model_002 is not None - assert palm2_embedding_generator_model_002._bqml_model is not None - - # save, load to ensure configuration was kept - reloaded_model = palm2_embedding_generator_model_002.to_gbq( - f"{dataset_id}.temp_embedding_model", replace=True - ) - assert f"{dataset_id}.temp_embedding_model" == reloaded_model._bqml_model.model_name - assert reloaded_model.model_name == "textembedding-gecko" - assert reloaded_model.version == "002" - assert reloaded_model.connection_name == bq_connection - - -def test_create_embedding_generator_multilingual_model( - palm2_embedding_generator_multilingual_model, - dataset_id, - bq_connection, -): - # Model creation doesn't return error - assert palm2_embedding_generator_multilingual_model is not None - assert palm2_embedding_generator_multilingual_model._bqml_model is not None - - # save, load to ensure configuration was kept - reloaded_model = palm2_embedding_generator_multilingual_model.to_gbq( - f"{dataset_id}.temp_embedding_model", replace=True - ) - assert f"{dataset_id}.temp_embedding_model" == reloaded_model._bqml_model.model_name - assert reloaded_model.model_name == "textembedding-gecko-multilingual" - assert reloaded_model.connection_name == bq_connection - - -def test_create_text_embedding_generator_model_defaults(bq_connection): - import bigframes.pandas as bpd - - # Note: This starts a thread-local session. - with bpd.option_context( - "bigquery.bq_connection", - bq_connection, - "bigquery.location", - "US", - ): - model = llm.PaLM2TextEmbeddingGenerator() - assert model is not None - assert model._bqml_model is not None - - -def test_create_text_embedding_generator_multilingual_model_defaults(bq_connection): - import bigframes.pandas as bpd - - # Note: This starts a thread-local session. - with bpd.option_context( - "bigquery.bq_connection", - bq_connection, - "bigquery.location", - "US", - ): - model = llm.PaLM2TextEmbeddingGenerator( - model_name="textembedding-gecko-multilingual" - ) - assert model is not None - assert model._bqml_model is not None - - -@pytest.mark.flaky(retries=2) -def test_embedding_generator_predict_success( - palm2_embedding_generator_model, llm_text_df -): - df = palm2_embedding_generator_model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "text_embedding" in df.columns - series = df["text_embedding"] - value = series[0] - assert len(value) == 768 - - -@pytest.mark.flaky(retries=2) -def test_embedding_generator_multilingual_predict_success( - palm2_embedding_generator_multilingual_model, llm_text_df -): - df = palm2_embedding_generator_multilingual_model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "text_embedding" in df.columns - series = df["text_embedding"] - value = series[0] - assert len(value) == 768 - - -@pytest.mark.flaky(retries=2) -def test_embedding_generator_predict_series_success( - palm2_embedding_generator_model, llm_text_df -): - df = palm2_embedding_generator_model.predict(llm_text_df["prompt"]).to_pandas() - assert df.shape == (3, 4) - assert "text_embedding" in df.columns - series = df["text_embedding"] - value = series[0] - assert len(value) == 768 - - @pytest.mark.parametrize( "model_name", ("text-embedding-004", "text-multilingual-embedding-002"), From 038139dfa4fa89167c52c1cb559c2eb5fe2f0411 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 20 Sep 2024 16:52:49 -0700 Subject: [PATCH 843/921] fix: Fix miscasting issues with case_when (#1003) --- bigframes/core/expression.py | 9 +++- bigframes/operations/base.py | 74 ++++++++++++++++++++++++------- bigframes/series.py | 33 ++++---------- tests/system/small/test_series.py | 13 +++--- 4 files changed, 81 insertions(+), 48 deletions(-) diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index bbd23b689cf..4779e92cdec 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -25,7 +25,9 @@ import bigframes.operations.aggregations as agg_ops -def const(value: typing.Hashable, dtype: dtypes.ExpressionType = None) -> Expression: +def const( + value: typing.Hashable, dtype: dtypes.ExpressionType = None +) -> ScalarConstantExpression: return ScalarConstantExpression(value, dtype or dtypes.infer_literal_type(value)) @@ -141,6 +143,9 @@ class ScalarConstantExpression(Expression): def is_const(self) -> bool: return True + def rename(self, name_mapping: Mapping[str, str]) -> ScalarConstantExpression: + return self + def output_type( self, input_types: dict[str, bigframes.dtypes.Dtype] ) -> dtypes.ExpressionType: @@ -167,7 +172,7 @@ class UnboundVariableExpression(Expression): def unbound_variables(self) -> typing.Tuple[str, ...]: return (self.id,) - def rename(self, name_mapping: Mapping[str, str]) -> Expression: + def rename(self, name_mapping: Mapping[str, str]) -> UnboundVariableExpression: if self.id in name_mapping: return UnboundVariableExpression(name_mapping[self.id]) else: diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index 68f46badedd..f9a6a87b7a2 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -15,7 +15,7 @@ from __future__ import annotations import typing -from typing import List, Sequence +from typing import List, Sequence, Union import bigframes_vendored.constants as constants import bigframes_vendored.pandas.pandas._typing as vendored_pandas_typing @@ -180,9 +180,10 @@ def _apply_binary_op( (self_col, other_col, block) = self._align(other_series, how=alignment) name = self._name + # Drop name if both objects have name attr, but they don't match if ( hasattr(other, "name") - and other.name != self._name + and other_series.name != self._name and alignment == "outer" ): name = None @@ -208,22 +209,41 @@ def _apply_nary_op( ignore_self=False, ): """Applies an n-ary operator to the series and others.""" - values, block = self._align_n(others, ignore_self=ignore_self) - block, result_id = block.apply_nary_op( - values, - op, - self._name, + values, block = self._align_n( + others, ignore_self=ignore_self, cast_scalars=False ) + block, result_id = block.project_expr(op.as_expr(*values)) return series.Series(block.select_column(result_id)) def _apply_binary_aggregation( self, other: series.Series, stat: agg_ops.BinaryAggregateOp ) -> float: (left, right, block) = self._align(other, how="outer") + assert isinstance(left, ex.UnboundVariableExpression) + assert isinstance(right, ex.UnboundVariableExpression) + return block.get_binary_stat(left.id, right.id, stat) + + AlignedExprT = Union[ex.ScalarConstantExpression, ex.UnboundVariableExpression] - return block.get_binary_stat(left, right, stat) + @typing.overload + def _align( + self, other: series.Series, how="outer" + ) -> tuple[ + ex.UnboundVariableExpression, + ex.UnboundVariableExpression, + blocks.Block, + ]: + ... - def _align(self, other: series.Series, how="outer") -> tuple[str, str, blocks.Block]: # type: ignore + @typing.overload + def _align( + self, other: typing.Union[series.Series, scalars.Scalar], how="outer" + ) -> tuple[ex.UnboundVariableExpression, AlignedExprT, blocks.Block,]: + ... + + def _align( + self, other: typing.Union[series.Series, scalars.Scalar], how="outer" + ) -> tuple[ex.UnboundVariableExpression, AlignedExprT, blocks.Block,]: """Aligns the series value with another scalar or series object. Returns new left column id, right column id and joined tabled expression.""" values, block = self._align_n( [ @@ -231,18 +251,36 @@ def _align(self, other: series.Series, how="outer") -> tuple[str, str, blocks.Bl ], how, ) - return (values[0], values[1], block) + return (typing.cast(ex.UnboundVariableExpression, values[0]), values[1], block) + + def _align3(self, other1: series.Series | scalars.Scalar, other2: series.Series | scalars.Scalar, how="left") -> tuple[ex.UnboundVariableExpression, AlignedExprT, AlignedExprT, blocks.Block]: # type: ignore + """Aligns the series value with 2 other scalars or series objects. Returns new values and joined tabled expression.""" + values, index = self._align_n([other1, other2], how) + return ( + typing.cast(ex.UnboundVariableExpression, values[0]), + values[1], + values[2], + index, + ) def _align_n( self, others: typing.Sequence[typing.Union[series.Series, scalars.Scalar]], how="outer", ignore_self=False, - ) -> tuple[typing.Sequence[str], blocks.Block]: + cast_scalars: bool = True, + ) -> tuple[ + typing.Sequence[ + Union[ex.ScalarConstantExpression, ex.UnboundVariableExpression] + ], + blocks.Block, + ]: if ignore_self: - value_ids: List[str] = [] + value_ids: List[ + Union[ex.ScalarConstantExpression, ex.UnboundVariableExpression] + ] = [] else: - value_ids = [self._value_column] + value_ids = [ex.free_var(self._value_column)] block = self._block for other in others: @@ -252,14 +290,16 @@ def _align_n( get_column_right, ) = block.join(other._block, how=how) value_ids = [ - *[get_column_left[value] for value in value_ids], - get_column_right[other._value_column], + *[value.rename(get_column_left) for value in value_ids], + ex.free_var(get_column_right[other._value_column]), ] else: # Will throw if can't interpret as scalar. dtype = typing.cast(bigframes.dtypes.Dtype, self._dtype) - block, constant_col_id = block.create_constant(other, dtype=dtype) - value_ids = [*value_ids, constant_col_id] + value_ids = [ + *value_ids, + ex.const(other, dtype=dtype if cast_scalars else None), + ] return (value_ids, block) def _throw_if_null_index(self, opname: str): diff --git a/bigframes/series.py b/bigframes/series.py index 3a75ab9ccc8..82fb6c50894 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -445,23 +445,13 @@ def between(self, left, right, inclusive="both"): ) def case_when(self, caselist) -> Series: + cases = list(itertools.chain(*caselist, (True, self))) return self._apply_nary_op( ops.case_when_op, - tuple( - itertools.chain( - itertools.chain(*caselist), - # Fallback to current value if no other matches. - ( - # We make a Series with a constant value to avoid casts to - # types other than boolean. - Series(True, index=self.index, dtype=pandas.BooleanDtype()), - self, - ), - ), - ), + cases, # Self is already included in "others". ignore_self=True, - ) + ).rename(self.name) @validations.requires_ordering() def cumsum(self) -> Series: @@ -1116,8 +1106,8 @@ def ne(self, other: object) -> Series: def where(self, cond, other=None): value_id, cond_id, other_id, block = self._align3(cond, other) - block, result_id = block.apply_ternary_op( - value_id, cond_id, other_id, ops.where_op + block, result_id = block.project_expr( + ops.where_op.as_expr(value_id, cond_id, other_id) ) return Series(block.select_column(result_id).with_column_labels([self.name])) @@ -1129,8 +1119,8 @@ def clip(self, lower, upper): if upper is None: return self._apply_binary_op(lower, ops.maximum_op, alignment="left") value_id, lower_id, upper_id, block = self._align3(lower, upper) - block, result_id = block.apply_ternary_op( - value_id, lower_id, upper_id, ops.clip_op + block, result_id = block.project_expr( + ops.clip_op.as_expr(value_id, lower_id, upper_id), ) return Series(block.select_column(result_id).with_column_labels([self.name])) @@ -1242,8 +1232,8 @@ def __getitem__(self, indexer): return self.iloc[indexer] if isinstance(indexer, Series): (left, right, block) = self._align(indexer, "left") - block = block.filter_by_id(right) - block = block.select_column(left) + block = block.filter(right) + block = block.select_column(left.id) return Series(block) return self.loc[indexer] @@ -1262,11 +1252,6 @@ def __getattr__(self, key: str): else: raise AttributeError(key) - def _align3(self, other1: Series | scalars.Scalar, other2: Series | scalars.Scalar, how="left") -> tuple[str, str, str, blocks.Block]: # type: ignore - """Aligns the series value with 2 other scalars or series objects. Returns new values and joined tabled expression.""" - values, index = self._align_n([other1, other2], how) - return (values[0], values[1], values[2], index) - def _apply_aggregation( self, op: agg_ops.UnaryAggregateOp | agg_ops.NullaryAggregateOp ) -> Any: diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index b8f7926aecd..793a4062c50 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -2709,27 +2709,30 @@ def test_between(scalars_df_index, scalars_pandas_df_index, left, right, inclusi ) -def test_case_when(scalars_df_index, scalars_pandas_df_index): +def test_series_case_when(scalars_dfs_maybe_ordered): pytest.importorskip( "pandas", minversion="2.2.0", reason="case_when added in pandas 2.2.0", ) + scalars_df, scalars_pandas_df = scalars_dfs_maybe_ordered - bf_series = scalars_df_index["int64_col"] - pd_series = scalars_pandas_df_index["int64_col"] + bf_series = scalars_df["int64_col"] + pd_series = scalars_pandas_df["int64_col"] # TODO(tswast): pandas case_when appears to assume True when a value is # null. I suspect this should be considered a bug in pandas. bf_result = bf_series.case_when( [ - ((bf_series > 100).fillna(True), 1000), + ((bf_series > 100).fillna(True), bf_series - 1), + ((bf_series > 0).fillna(True), pd.NA), ((bf_series < -100).fillna(True), -1000), ] ).to_pandas() pd_result = pd_series.case_when( [ - (pd_series > 100, 1000), + (pd_series > 100, pd_series - 1), + (pd_series > 0, pd.NA), (pd_series < -100, -1000), ] ) From bb04742500d445ae196698df87291dc7b1c94102 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:02:59 -0700 Subject: [PATCH 844/921] chore: exclude claude3 notebook in nox session (#1008) --- noxfile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/noxfile.py b/noxfile.py index bf4c6f9641c..f459efef108 100644 --- a/noxfile.py +++ b/noxfile.py @@ -719,6 +719,8 @@ def notebook(session: nox.Session): # bq_dataframes_llm_code_generation creates a bucket in the sample. "notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb", # Needs BUCKET_URI. "notebooks/generative_ai/sentiment_analysis.ipynb", # Too slow + # TODO(b/366290533): to protect BQML quota + "notebooks/generative_ai/bq_dataframes_llm_claude3_museum_art.ipynb", "notebooks/vertex_sdk/sdk2_bigframes_pytorch.ipynb", # Needs BUCKET_URI. "notebooks/vertex_sdk/sdk2_bigframes_sklearn.ipynb", # Needs BUCKET_URI. "notebooks/vertex_sdk/sdk2_bigframes_tensorflow.ipynb", # Needs BUCKET_URI. From f89785fcfc51c541253ca8c1e8baf80fbfaea3b6 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Sun, 22 Sep 2024 18:46:02 -0700 Subject: [PATCH 845/921] deps: update ibis version in prerelease tests (#1012) * deps: update ibis version in prerelease tests * exclude remote function tests from prerelease --- noxfile.py | 24 +++++++++++++++++++--- tests/system/large/test_remote_function.py | 3 +++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index f459efef108..c704da00a52 100644 --- a/noxfile.py +++ b/noxfile.py @@ -543,7 +543,7 @@ def docfx(session): ) -def prerelease(session: nox.sessions.Session, tests_path): +def prerelease(session: nox.sessions.Session, tests_path, extra_pytest_options=()): constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) @@ -588,7 +588,7 @@ def prerelease(session: nox.sessions.Session, tests_path): session.install( "--upgrade", "--pre", - "ibis-framework>=8.0.0,<9.0.0dev", + "ibis-framework>=9.0.0,<=9.2.0", ) already_installed.add("ibis-framework") @@ -662,6 +662,7 @@ def prerelease(session: nox.sessions.Session, tests_path): "--cov-report=term-missing", "--cov-fail-under=0", tests_path, + *extra_pytest_options, *session.posargs, ) @@ -675,7 +676,24 @@ def unit_prerelease(session: nox.sessions.Session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS[-1]) def system_prerelease(session: nox.sessions.Session): """Run the system test suite with prerelease dependencies.""" - prerelease(session, os.path.join("tests", "system", "small")) + small_tests_dir = os.path.join("tests", "system", "small") + + # Let's exclude remote function tests from the prerelease tests, since the + # some of the package dependencies propagate to the cloud run functions' + # requirements.txt, and the prerelease package versions may not be available + # in the standard pip install. + # This would mean that we will only rely on the standard remote function + # tests. + small_remote_function_tests = os.path.join( + small_tests_dir, "test_remote_function.py" + ) + assert os.path.exists(small_remote_function_tests) + + prerelease( + session, + os.path.join("tests", "system", "small"), + (f"--ignore={small_remote_function_tests}",), + ) @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index e224f65a016..d1e82dd4159 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1690,6 +1690,9 @@ def analyze(row): ), ), id="multiindex", + marks=pytest.mark.skip( + reason="TODO(b/368639580) revert this skip after fix" + ), ), pytest.param( pandas.DataFrame( From 6b34244e9ebe747161da21af2f31ee537b653093 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 23 Sep 2024 03:40:13 -0700 Subject: [PATCH 846/921] test: ensure all `remote_function` APIs work in partial ordering mode (#1000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: ensure all `remote_function` APIs work in partial ordering mode * remove force_reproject from more APIs * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * temporarily skip multiindex test for axis=1 --------- Co-authored-by: Owl Bot --- bigframes/dataframe.py | 14 +- bigframes/series.py | 6 +- tests/system/small/test_remote_function.py | 246 ++++++++++++++++++--- 3 files changed, 224 insertions(+), 42 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 817a02f492d..49a668f008e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3473,11 +3473,7 @@ def map(self, func, na_action: Optional[str] = None) -> DataFrame: raise ValueError(f"na_action={na_action} not supported") # TODO(shobs): Support **kwargs - # Reproject as workaround to applying filter too late. This forces the - # filter to be applied before passing data to remote function, - # protecting from bad inputs causing errors. - reprojected_df = DataFrame(self._block._force_reproject()) - return reprojected_df._apply_unary_op( + return self._apply_unary_op( ops.RemoteFunctionOp(func=func, apply_on_null=(na_action is None)) ) @@ -3572,13 +3568,7 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs): ) series_list = [self[col] for col in self.columns] - # Reproject as workaround to applying filter too late. This forces the - # filter to be applied before passing data to remote function, - # protecting from bad inputs causing errors. - reprojected_series = bigframes.series.Series( - series_list[0]._block._force_reproject() - ) - result_series = reprojected_series._apply_nary_op( + result_series = series_list[0]._apply_nary_op( ops.NaryRemoteFunctionOp(func=func), series_list[1:] ) result_series.name = None diff --git a/bigframes/series.py b/bigframes/series.py index 82fb6c50894..193eea7ee39 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1480,11 +1480,7 @@ def combine( ex.message += f"\n{_remote_function_recommendation_message}" raise - # Reproject as workaround to applying filter too late. This forces the - # filter to be applied before passing data to remote function, - # protecting from bad inputs causing errors. - reprojected_series = Series(self._block._force_reproject()) - result_series = reprojected_series._apply_binary_op( + result_series = self._apply_binary_op( other, ops.BinaryRemoteFunctionOp(func=func) ) diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index 5ffda56f926..f68589f4316 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -498,6 +498,37 @@ def add_one(x): assert_pandas_df_equal(bf_result, pd_result) +@pytest.mark.flaky(retries=2, delay=120) +def test_dataframe_applymap_explicit_filter( + session_with_bq_connection, scalars_dfs, dataset_id_permanent +): + def add_one(x): + return x + 1 + + remote_add_one = session_with_bq_connection.remote_function( + [int], int, dataset_id_permanent, name=get_rf_name(add_one) + )(add_one) + + scalars_df, scalars_pandas_df = scalars_dfs + int64_cols = ["int64_col", "int64_too"] + + bf_int64_df = scalars_df[int64_cols] + bf_int64_df_filtered = bf_int64_df[bf_int64_df["int64_col"].notnull()] + bf_result = bf_int64_df_filtered.applymap(remote_add_one).to_pandas() + + pd_int64_df = scalars_pandas_df[int64_cols] + pd_int64_df_filtered = pd_int64_df[pd_int64_df["int64_col"].notnull()] + pd_result = pd_int64_df_filtered.applymap(add_one) + # TODO(shobs): Figure why pandas .applymap() changes the dtype, i.e. + # pd_int64_df_filtered.dtype is Int64Dtype() + # pd_int64_df_filtered.applymap(lambda x: x).dtype is int64. + # For this test let's force the pandas dtype to be same as input. + for col in pd_result: + pd_result[col] = pd_result[col].astype(pd_int64_df_filtered[col].dtype) + + assert_pandas_df_equal(bf_result, pd_result) + + @pytest.mark.flaky(retries=2, delay=120) def test_dataframe_applymap_na_ignore( session_with_bq_connection, scalars_dfs, dataset_id_permanent @@ -1024,12 +1055,21 @@ def test_read_gbq_function_application_repr(session, dataset_id, scalars_df_inde repr(s.mask(should_mask, "REDACTED")) +@pytest.mark.parametrize( + ("method",), + [ + pytest.param("apply"), + pytest.param("map"), + pytest.param("mask"), + ], +) @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_apply_after_filter(session, dataset_id_permanent, scalars_dfs): - +def test_remote_function_unary_applied_after_filter( + session, dataset_id_permanent, scalars_dfs, method +): # This function is deliberately written to not work with NA input - def plus_one(x: int) -> int: - return x + 1 + def is_odd(x: int) -> bool: + return x % 2 == 1 scalars_df, scalars_pandas_df = scalars_dfs int_col_name_with_nulls = "int64_col" @@ -1038,47 +1078,203 @@ def plus_one(x: int) -> int: assert any([pd.isna(val) for val in scalars_df[int_col_name_with_nulls]]) # create a remote function - plus_one_remote = session.remote_function( - dataset=dataset_id_permanent, name=get_rf_name(plus_one) - )(plus_one) + is_odd_remote = session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(is_odd) + )(is_odd) # with nulls in the series the remote function application would fail with pytest.raises( google.api_core.exceptions.BadRequest, match="unsupported operand" ): - scalars_df[int_col_name_with_nulls].apply(plus_one_remote).to_pandas() + bf_method = getattr(scalars_df[int_col_name_with_nulls], method) + bf_method(is_odd_remote).to_pandas() - # after filtering out nulls the remote function application should works + # after filtering out nulls the remote function application should work # similar to pandas - pd_result = scalars_pandas_df[scalars_pandas_df[int_col_name_with_nulls].notnull()][ - int_col_name_with_nulls - ].apply(plus_one) - bf_result = ( + pd_method = getattr( + scalars_pandas_df[scalars_pandas_df[int_col_name_with_nulls].notnull()][ + int_col_name_with_nulls + ], + method, + ) + pd_result = pd_method(is_odd) + bf_method = getattr( scalars_df[scalars_df[int_col_name_with_nulls].notnull()][ int_col_name_with_nulls - ] - .apply(plus_one_remote) + ], + method, + ) + bf_result = bf_method(is_odd_remote).to_pandas() + + # ignore any dtype difference + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_binary_applied_after_filter( + session, dataset_id_permanent, scalars_dfs +): + # This function is deliberately written to not work with NA input + def add(x: int, y: int) -> int: + return x + y + + scalars_df, scalars_pandas_df = scalars_dfs + int_col_name_with_nulls = "int64_col" + int_col_name_no_nulls = "int64_too" + bf_df = scalars_df[[int_col_name_with_nulls, int_col_name_no_nulls]] + pd_df = scalars_pandas_df[[int_col_name_with_nulls, int_col_name_no_nulls]] + + # make sure there are NA values in the test column + assert any([pd.isna(val) for val in bf_df[int_col_name_with_nulls]]) + + # create a remote function + add_remote = session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(add) + )(add) + + # with nulls in the series the remote function application would fail + with pytest.raises( + google.api_core.exceptions.BadRequest, match="unsupported operand" + ): + bf_df[int_col_name_with_nulls].combine( + bf_df[int_col_name_no_nulls], add_remote + ).to_pandas() + + # after filtering out nulls the remote function application should work + # similar to pandas + pd_filter = pd_df[int_col_name_with_nulls].notnull() + pd_result = pd_df[pd_filter][int_col_name_with_nulls].combine( + pd_df[pd_filter][int_col_name_no_nulls], add + ) + bf_filter = bf_df[int_col_name_with_nulls].notnull() + bf_result = ( + bf_df[bf_filter][int_col_name_with_nulls] + .combine(bf_df[bf_filter][int_col_name_no_nulls], add_remote) .to_pandas() ) - # ignore pandas "int64" vs bigframes "Int64" dtype difference + # ignore any dtype difference pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.flaky(retries=2, delay=120) -def test_remote_function_apply_assign_partial_ordering_mode(dataset_id_permanent): - session = bigframes.Session(bigframes.BigQueryOptions(ordering_mode="partial")) +def test_remote_function_nary_applied_after_filter( + session, dataset_id_permanent, scalars_dfs +): + # This function is deliberately written to not work with NA input + def add(x: int, y: int, z: float) -> float: + return x + y + z - df = session.read_gbq("bigquery-public-data.baseball.schedules")[ + scalars_df, scalars_pandas_df = scalars_dfs + int_col_name_with_nulls = "int64_col" + int_col_name_no_nulls = "int64_too" + float_col_name_with_nulls = "float64_col" + bf_df = scalars_df[ + [int_col_name_with_nulls, int_col_name_no_nulls, float_col_name_with_nulls] + ] + pd_df = scalars_pandas_df[ + [int_col_name_with_nulls, int_col_name_no_nulls, float_col_name_with_nulls] + ] + + # make sure there are NA values in the test columns + assert any([pd.isna(val) for val in bf_df[int_col_name_with_nulls]]) + assert any([pd.isna(val) for val in bf_df[float_col_name_with_nulls]]) + + # create a remote function + add_remote = session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(add) + )(add) + + # pandas does not support nary functions, so let's create a proxy function + # for testing purpose that takes a series and in turn calls the naray function + def add_pandas(s: pd.Series) -> float: + return add( + s[int_col_name_with_nulls], + s[int_col_name_no_nulls], + s[float_col_name_with_nulls], + ) + + # with nulls in the series the remote function application would fail + with pytest.raises( + google.api_core.exceptions.BadRequest, match="unsupported operand" + ): + bf_df.apply(add_remote, axis=1).to_pandas() + + # after filtering out nulls the remote function application should work + # similar to pandas + pd_filter = ( + pd_df[int_col_name_with_nulls].notnull() + & pd_df[float_col_name_with_nulls].notnull() + ) + pd_result = pd_df[pd_filter].apply(add_pandas, axis=1) + bf_filter = ( + bf_df[int_col_name_with_nulls].notnull() + & bf_df[float_col_name_with_nulls].notnull() + ) + bf_result = bf_df[bf_filter].apply(add_remote, axis=1).to_pandas() + + # ignore any dtype difference + pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + + +@pytest.mark.parametrize( + ("method",), + [ + pytest.param("apply"), + pytest.param("map"), + pytest.param("mask"), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_unary_partial_ordering_mode_assign( + unordered_session, dataset_id_permanent, method +): + df = unordered_session.read_gbq("bigquery-public-data.baseball.schedules")[ ["duration_minutes"] ] - def plus_one(x: int) -> int: - return x + 1 + def is_long_duration(minutes: int) -> bool: + return minutes >= 120 + + is_long_duration = unordered_session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(is_long_duration) + )(is_long_duration) + + method = getattr(df["duration_minutes"], method) + + df1 = df.assign(duration_meta=method(is_long_duration)) + repr(df1) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_binary_partial_ordering_mode_assign( + unordered_session, dataset_id_permanent, scalars_df_index +): + def combiner(x: int, y: int) -> int: + if x is None: + return y + return x + + combiner = unordered_session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(combiner) + )(combiner) + + df = scalars_df_index[["int64_col", "int64_too", "float64_col", "string_col"]] + df1 = df.assign(int64_combined=df["int64_col"].combine(df["int64_too"], combiner)) + repr(df1) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_nary_partial_ordering_mode_assign( + unordered_session, dataset_id_permanent, scalars_df_index +): + def processor(x: int, y: int, z: float, w: str) -> str: + return f"I got x={x}, y={y}, z={z} and w={w}" - plus_one = session.remote_function( - dataset=dataset_id_permanent, name=get_rf_name(plus_one) - )(plus_one) + processor = unordered_session.remote_function( + dataset=dataset_id_permanent, name=get_rf_name(processor) + )(processor) - df1 = df.assign(duration_cat=df["duration_minutes"].apply(plus_one)) + df = scalars_df_index[["int64_col", "int64_too", "float64_col", "string_col"]] + df1 = df.assign(combined=df.apply(processor, axis=1)) repr(df1) From 952cab92e548b70d077b20bf10f5307751d2ae76 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:44:39 -0700 Subject: [PATCH 847/921] feat: add ml.model_selection.KFold class (#1001) --- bigframes/ml/model_selection.py | 74 +++++--- bigframes/ml/utils.py | 39 +++- tests/system/small/ml/test_model_selection.py | 173 ++++++++++++++++++ .../sklearn/model_selection/_split.py | 109 +++++++++++ 4 files changed, 367 insertions(+), 28 deletions(-) create mode 100644 third_party/bigframes_vendored/sklearn/model_selection/_split.py diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index a6553d13dc8..e4c41b2a399 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -17,8 +17,12 @@ https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection.""" -from typing import cast, List, Union +import inspect +from typing import cast, Generator, List, Union +import bigframes_vendored.sklearn.model_selection._split as vendored_model_selection_split + +from bigframes.core import log_adapter from bigframes.ml import utils import bigframes.pandas as bpd @@ -30,30 +34,6 @@ def train_test_split( random_state: Union[int, None] = None, stratify: Union[bpd.Series, None] = None, ) -> List[Union[bpd.DataFrame, bpd.Series]]: - """Splits dataframes or series into random train and test subsets. - - Args: - *arrays (bigframes.dataframe.DataFrame or bigframes.series.Series): - A sequence of BigQuery DataFrames or Series that can be joined on - their indexes. - test_size (default None): - The proportion of the dataset to include in the test split. If - None, this will default to the complement of train_size. If both - are none, it will be set to 0.25. - train_size (default None): - The proportion of the dataset to include in the train split. If - None, this will default to the complement of test_size. - random_state (default None): - A seed to use for randomly choosing the rows of the split. If not - set, a random split will be generated each time. - stratify: (bigframes.series.Series or None, default None): - If not None, data is split in a stratified fashion, using this as the class labels. Each split has the same distribution of the class labels with the original dataset. - Default to None. - Note: By setting the stratify parameter, the memory consumption and generated SQL will be linear to the unique values in the Series. May return errors if the unique values size is too large. - - Returns: - List[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]]: A list of BigQuery DataFrames or Series. - """ # TODO(garrettwu): scikit-learn throws an error when the dataframes don't have the same # number of rows. We probably want to do something similar. Now the implementation is based @@ -123,3 +103,47 @@ def _stratify_split(df: bpd.DataFrame, stratify: bpd.Series) -> List[bpd.DataFra results.append(joined_df_test[columns]) return results + + +train_test_split.__doc__ = inspect.getdoc( + vendored_model_selection_split.train_test_split +) + + +@log_adapter.class_logger +class KFold(vendored_model_selection_split.KFold): + def __init__(self, n_splits: int = 5, *, random_state: Union[int, None] = None): + if n_splits < 2: + raise ValueError(f"n_splits must be at least 2. Got {n_splits}") + self._n_splits = n_splits + self._random_state = random_state + + def get_n_splits(self) -> int: + return self._n_splits + + def split( + self, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series, None] = None, + ) -> Generator[tuple[Union[bpd.DataFrame, bpd.Series, None]], None, None]: + X_df = next(utils.convert_to_dataframe(X)) + y_df_or = next(utils.convert_to_dataframe(y)) if y is not None else None + joined_df = X_df.join(y_df_or, how="outer") if y_df_or is not None else X_df + + fracs = (1 / self._n_splits,) * self._n_splits + + dfs = joined_df._split(fracs=fracs, random_state=self._random_state) + + for i in range(len(dfs)): + train_df = bpd.concat(dfs[:i] + dfs[i + 1 :]) + test_df = dfs[i] + + X_train = train_df[X_df.columns] + y_train = train_df[y_df_or.columns] if y_df_or is not None else None + + X_test = test_df[X_df.columns] + y_test = test_df[y_df_or.columns] if y_df_or is not None else None + + yield utils.convert_to_types( + [X_train, X_test, y_train, y_test], [X, X, y, y] + ) diff --git a/bigframes/ml/utils.py b/bigframes/ml/utils.py index d754b1d0021..96f0bc31e93 100644 --- a/bigframes/ml/utils.py +++ b/bigframes/ml/utils.py @@ -13,7 +13,7 @@ # limitations under the License. import typing -from typing import Any, Iterable, Literal, Mapping, Optional, Union +from typing import Any, Generator, Iterable, Literal, Mapping, Optional, Union import bigframes_vendored.constants as constants from google.cloud import bigquery @@ -25,7 +25,7 @@ ArrayType = Union[bpd.DataFrame, bpd.Series] -def convert_to_dataframe(*input: ArrayType) -> Iterable[bpd.DataFrame]: +def convert_to_dataframe(*input: ArrayType) -> Generator[bpd.DataFrame, None, None]: return (_convert_to_dataframe(frame) for frame in input) @@ -39,7 +39,7 @@ def _convert_to_dataframe(frame: ArrayType) -> bpd.DataFrame: ) -def convert_to_series(*input: ArrayType) -> Iterable[bpd.Series]: +def convert_to_series(*input: ArrayType) -> Generator[bpd.Series, None, None]: return (_convert_to_series(frame) for frame in input) @@ -60,6 +60,39 @@ def _convert_to_series(frame: ArrayType) -> bpd.Series: ) +def convert_to_types( + inputs: Iterable[Union[ArrayType, None]], + type_instances: Iterable[Union[ArrayType, None]], +) -> tuple[Union[ArrayType, None]]: + """Convert the DF, Series and None types of the input to corresponding type_instances types.""" + results = [] + for input, type_instance in zip(inputs, type_instances): + results.append(_convert_to_type(input, type_instance)) + return tuple(results) + + +def _convert_to_type( + input: Union[ArrayType, None], type_instance: Union[ArrayType, None] +): + if type_instance is None: + if input is not None: + raise ValueError( + f"Trying to convert not None type to None. {constants.FEEDBACK_LINK}" + ) + return None + if input is None: + raise ValueError( + f"Trying to convert None type to not None. {constants.FEEDBACK_LINK}" + ) + if isinstance(type_instance, bpd.DataFrame): + return _convert_to_dataframe(input) + if isinstance(type_instance, bpd.Series): + return _convert_to_series(input) + raise ValueError( + f"Unsupport converting to {type(type_instance)}. {constants.FEEDBACK_LINK}" + ) + + def parse_model_endpoint(model_endpoint: str) -> tuple[str, Optional[str]]: """Parse model endpoint string to model_name and version.""" model_name = model_endpoint diff --git a/tests/system/small/ml/test_model_selection.py b/tests/system/small/ml/test_model_selection.py index 47529565b7f..e6b5f8cdc28 100644 --- a/tests/system/small/ml/test_model_selection.py +++ b/tests/system/small/ml/test_model_selection.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import math + import pandas as pd import pytest @@ -302,3 +304,174 @@ def test_train_test_split_stratify(df_fixture, request): test_counts, check_index_type=False, ) + + +@pytest.mark.parametrize( + "n_splits", + (3, 5, 10), +) +def test_KFold_get_n_splits(n_splits): + kf = model_selection.KFold(n_splits) + assert kf.get_n_splits() == n_splits + + +@pytest.mark.parametrize( + "df_fixture", + ("penguins_df_default_index", "penguins_df_null_index"), +) +@pytest.mark.parametrize( + "n_splits", + (3, 5), +) +def test_KFold_split(df_fixture, n_splits, request): + df = request.getfixturevalue(df_fixture) + + kf = model_selection.KFold(n_splits=n_splits) + + X = df[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = df["body_mass_g"] + + len_test_upper, len_test_lower = math.ceil(len(df) / n_splits), math.floor( + len(df) / n_splits + ) + len_train_upper, len_train_lower = ( + len(df) - len_test_lower, + len(df) - len_test_upper, + ) + + for X_train, X_test, y_train, y_test in kf.split(X, y): # type: ignore + assert isinstance(X_train, bpd.DataFrame) + assert isinstance(X_test, bpd.DataFrame) + assert isinstance(y_train, bpd.Series) + assert isinstance(y_test, bpd.Series) + + # Depend on the iteration, train/test can +-1 in size. + assert ( + X_train.shape == (len_train_upper, 3) + and y_train.shape == (len_train_upper,) + and X_test.shape == (len_test_lower, 3) + and y_test.shape == (len_test_lower,) + ) or ( + X_train.shape == (len_train_lower, 3) + and y_train.shape == (len_train_lower,) + and X_test.shape == (len_test_upper, 3) + and y_test.shape == (len_test_upper,) + ) + + +@pytest.mark.parametrize( + "df_fixture", + ("penguins_df_default_index", "penguins_df_null_index"), +) +@pytest.mark.parametrize( + "n_splits", + (3, 5), +) +def test_KFold_split_X_only(df_fixture, n_splits, request): + df = request.getfixturevalue(df_fixture) + + kf = model_selection.KFold(n_splits=n_splits) + + X = df[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + + len_test_upper, len_test_lower = math.ceil(len(df) / n_splits), math.floor( + len(df) / n_splits + ) + len_train_upper, len_train_lower = ( + len(df) - len_test_lower, + len(df) - len_test_upper, + ) + + for X_train, X_test, y_train, y_test in kf.split(X, y=None): # type: ignore + assert isinstance(X_train, bpd.DataFrame) + assert isinstance(X_test, bpd.DataFrame) + assert y_train is None + assert y_test is None + + # Depend on the iteration, train/test can +-1 in size. + assert ( + X_train.shape == (len_train_upper, 3) + and X_test.shape == (len_test_lower, 3) + ) or ( + X_train.shape == (len_train_lower, 3) + and X_test.shape == (len_test_upper, 3) + ) + + +def test_KFold_seeded_correct_rows(session, penguins_pandas_df_default_index): + kf = model_selection.KFold(random_state=42) + # Note that we're using `penguins_pandas_df_default_index` as this test depends + # on a stable row order being present end to end + # filter down to the chunkiest penguins, to keep our test code a reasonable size + all_data = penguins_pandas_df_default_index[ + penguins_pandas_df_default_index.body_mass_g > 5500 + ] + + # Note that bigframes loses the index if it doesn't have a name + all_data.index.name = "rowindex" + + df = session.read_pandas(all_data) + + X = df[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = df["body_mass_g"] + X_train, X_test, y_train, y_test = next(kf.split(X, y)) # type: ignore + + X_train_sorted = X_train.to_pandas().sort_index() + X_test_sorted = X_test.to_pandas().sort_index() + y_train_sorted = y_train.to_pandas().sort_index() + y_test_sorted = y_test.to_pandas().sort_index() + + train_index: pd.Index = pd.Index( + [ + 144, + 146, + 148, + 161, + 168, + 183, + 217, + 221, + 225, + 226, + 237, + 244, + 257, + 262, + 264, + 266, + 267, + 269, + 278, + 289, + 290, + 291, + ], + dtype="Int64", + name="rowindex", + ) + test_index: pd.Index = pd.Index( + [186, 240, 245, 260, 263, 268], dtype="Int64", name="rowindex" + ) + + pd.testing.assert_index_equal(X_train_sorted.index, train_index) + pd.testing.assert_index_equal(X_test_sorted.index, test_index) + pd.testing.assert_index_equal(y_train_sorted.index, train_index) + pd.testing.assert_index_equal(y_test_sorted.index, test_index) diff --git a/third_party/bigframes_vendored/sklearn/model_selection/_split.py b/third_party/bigframes_vendored/sklearn/model_selection/_split.py new file mode 100644 index 00000000000..280962473ee --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/model_selection/_split.py @@ -0,0 +1,109 @@ +""" +The :mod:`sklearn.model_selection._split` module includes classes and +functions to split the data based on a preset strategy. +""" + +# Author: Alexandre Gramfort +# Gael Varoquaux +# Olivier Grisel +# Raghav RV +# Leandro Hermida +# Rodion Martynov +# License: BSD 3 clause + + +from abc import ABCMeta + +from bigframes import constants + + +class _BaseKFold(metaclass=ABCMeta): + """Base class for K-Fold cross-validators.""" + + def split(self, X, y=None): + """Generate indices to split data into training and test set. + + Args: + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + BigFrames DataFrame or Series of shape (n_samples, n_features) + Training data, where `n_samples` is the number of samples + and `n_features` is the number of features. + + y (bigframes.dataframe.DataFrame, bigframes.series.Series or None): + BigFrames DataFrame, Series of shape (n_samples,) or None. + The target variable for supervised learning problems. Default to None. + + Yields: + X_train (bigframes.dataframe.DataFrame or bigframes.series.Series): + The training data for that split. + + X_test (bigframes.dataframe.DataFrame or bigframes.series.Series): + The testing data for that split. + + y_train (bigframes.dataframe.DataFrame, bigframes.series.Series or None): + The training label for that split. + + y_test (bigframes.dataframe.DataFrame, bigframes.series.Series or None): + The testing label for that split. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def get_n_splits(self): + """Returns the number of splitting iterations in the cross-validator. + + Returns: + int: the number of splitting iterations in the cross-validator. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +class KFold(_BaseKFold): + """K-Fold cross-validator. + + Split data in train/test sets. Split dataset into k consecutive folds. + + Each fold is then used once as a validation while the k - 1 remaining + folds form the training set. + + Args: + n_splits (int): + Number of folds. Must be at least 2. Default to 5. + + random_state (Optional[int]): + A seed to use for randomly choosing the rows of the split. If not + set, a random split will be generated each time. Default to None. + """ + + +def train_test_split( + *arrays, + test_size=None, + train_size=None, + random_state=None, + stratify=None, +): + """Splits dataframes or series into random train and test subsets. + + Args: + *arrays (bigframes.dataframe.DataFrame or bigframes.series.Series): + A sequence of BigQuery DataFrames or Series that can be joined on + their indexes. + test_size (default None): + The proportion of the dataset to include in the test split. If + None, this will default to the complement of train_size. If both + are none, it will be set to 0.25. + train_size (default None): + The proportion of the dataset to include in the train split. If + None, this will default to the complement of test_size. + random_state (default None): + A seed to use for randomly choosing the rows of the split. If not + set, a random split will be generated each time. + stratify: (bigframes.series.Series or None, default None): + If not None, data is split in a stratified fashion, using this as the class labels. Each split has the same distribution of the class labels with the original dataset. + Default to None. + Note: By setting the stratify parameter, the memory consumption and generated SQL will be linear to the unique values in the Series. May return errors if the unique values size is too large. + + Returns: + List[Union[bigframes.dataframe.DataFrame, bigframes.series.Series]]: A list of BigQuery DataFrames or Series. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 7bcec880b5443c8a95b9ce9db666d6146426389a Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:10:42 -0700 Subject: [PATCH 848/921] chore: update benchmark readme for usage reference. (#1009) * chore: update benchmark readme for usage reference. * add s * update format --- tests/benchmark/README.md | 62 ++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/tests/benchmark/README.md b/tests/benchmark/README.md index 9c5f4a99d6e..a30c36065bc 100644 --- a/tests/benchmark/README.md +++ b/tests/benchmark/README.md @@ -1,13 +1,61 @@ -# Benchmark Tests - +# BigFrames Benchmarking ## Overview - This directory contains scripts for performance benchmarking of various components of BigFrames. -## Execution Details +## Why Separate Processes? +Each benchmark is executed in a separate process to mitigate the effects of any residual caching or settings that may persist in BigFrames, ensuring that each test is conducted in a clean state. -Scripts in this directory can be executed as part of the benchmarking session or independently from the command line. This allows for quick, standalone runs for immediate debugging and validation without the overhead of initiating full benchmark sessions. +## Available Benchmarks +This section lists the benchmarks currently available, with descriptions and links to their sources: +- **DB Benchmark**: This benchmark is adapted from DuckDB Labs and is designed to assess database performance. More information can be found on the [official DB Benchmark GitHub page](https://github.com/duckdblabs/db-benchmark). +- **TPC-H Benchmark**: Based on the TPC-H standards, this benchmark evaluates transaction processing capabilities. It is adapted from code found in the Polars repository, specifically tailored to test and compare these capabilities. Details are available on the [Polars Benchmark GitHub repository](https://github.com/pola-rs/polars-benchmark). +- **Notebooks**: These Jupyter notebooks showcase BigFrames' key features and patterns, and also enable performance benchmarking. Explore them at the [BigFrames Notebooks repository](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks). -## Why Separate Processes? +## Usage Examples +Our benchmarking process runs internally on a daily basis to continuously monitor the performance of BigFrames. However, there are occasions when you might need to conduct benchmarking locally to test specific changes or new features. -Each benchmark is executed in a separate process to mitigate the effects of any residual caching or settings that may persist in BigFrames, ensuring that each test is conducted in a clean state. +Here's how you can run benchmarks locally: + +- **Running Notebook Benchmarks**: To execute all notebook benchmarks, use the following command: + ```bash + nox -r -s notebook + ``` + + This command runs all the Jupyter notebooks in the repository as benchmarks. +- **Running Pure Benchmarks**: For executing more traditional benchmarks that do not involve notebooks, use: + ```bash + nox -r -s benchmark + ``` + This will run all the non-notebook benchmarks specified in the repository. + +- **Saving Results**: By default, when run locally, each benchmark concludes by printing a summary of the results, which are not saved automatically. To save the results to a CSV file, you can use the --output-csv or -o option followed by a specific path. If no path is specified, the results will be saved to a temporary location, and the path to this location will be printed at the end of the benchmark. + ```bash + nox -r -s benchmark -- --output-csv path/to/your/results.csv + nox -r -s benchmark -- --output-csv + ``` + +- **Running Multiple Iterations**: To run a benchmark multiple times and obtain an average result, use the -i or --iterations option followed by the number of iterations: + ```bash + nox -r -s benchmark -- --iterations 5 + ``` + +- **Filtering Benchmarks**: If you want to run only specific benchmarks, such as TPC-H, or specific queries within a benchmark, like tpch/q1, you can use the --benchmark-filter or -b option followed by the folder, file name, or both: + ```bash + # Runs all benchmarks in the 'tpch' directory + nox -r -s benchmark -- --benchmark-filter tpch + + # Runs all benchmarks in 'db_benchmark' and specific queries q1 and q2 from TPC-H + nox -r -s benchmark -- --benchmark-filter db_benchmark tpch/q1.py tpch/q2.py + ``` +- **Uploading Results to BigQuery**: To upload benchmark results to BigQuery, set the environment variable GCLOUD_BENCH_PUBLISH_PROJECT to the Google Cloud project where you want to store the results. This enables automatic uploading of the benchmark data to your specified project in BigQuery: + ```bash + export GCLOUD_BENCH_PUBLISH_PROJECT='your-google-cloud-project-id' + + # Run all non-notebook benchmarks and uploads the results to + # your-google-cloud-project-id.benchmark_report.benchmark + nox -r -s benchmark + + # Run all notebook benchmarks and uploads the results to + # your-google-cloud-project-id.benchmark_report.notebook_benchmark + nox -r -s notebook + ``` From 1b5b0eea92631b7dd1b688cf1da617fc7ce862dc Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Mon, 23 Sep 2024 14:19:00 -0700 Subject: [PATCH 849/921] perf: Join op discards child ordering in unordered mode (#923) --- bigframes/core/compile/compiled.py | 4 ++++ bigframes/core/compile/compiler.py | 29 +++++++++++++++++++++-------- bigframes/core/nodes.py | 4 +++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 693d93de8ca..69730912966 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -441,6 +441,10 @@ def explode(self, offsets: typing.Sequence[int]) -> UnorderedIR: columns=columns, ) + def as_ordered_ir(self) -> OrderedIR: + """Convert to OrderedIr, but without any definite ordering.""" + return OrderedIR(self._table, self._columns, predicates=self._predicates) + ## Helpers def _set_or_replace_by_id( self, id: str, new_value: ibis_types.Value diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 23501f93c8b..74fcaf5f2a5 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -76,14 +76,27 @@ def _compile_node( @_compile_node.register def compile_join(self, node: nodes.JoinNode, ordered: bool = True): if ordered: - left_ordered = self.compile_ordered_ir(node.left_child) - right_ordered = self.compile_ordered_ir(node.right_child) - return bigframes.core.compile.single_column.join_by_column_ordered( - left=left_ordered, - right=right_ordered, - type=node.type, - conditions=node.conditions, - ) + # In general, joins are an ordering destroying operation. + # With ordering_mode = "partial", make this explicit. In + # this case, we don't need to provide a deterministic ordering. + if self.strict: + left_ordered = self.compile_ordered_ir(node.left_child) + right_ordered = self.compile_ordered_ir(node.right_child) + return bigframes.core.compile.single_column.join_by_column_ordered( + left=left_ordered, + right=right_ordered, + type=node.type, + conditions=node.conditions, + ) + else: + left_unordered = self.compile_unordered_ir(node.left_child) + right_unordered = self.compile_unordered_ir(node.right_child) + return bigframes.core.compile.single_column.join_by_column_unordered( + left=left_unordered, + right=right_unordered, + type=node.type, + conditions=node.conditions, + ).as_ordered_ir() else: left_unordered = self.compile_unordered_ir(node.left_child) right_unordered = self.compile_unordered_ir(node.right_child) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 2dc9623d89d..93b59f75eef 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -249,6 +249,7 @@ def order_ambiguous(self) -> bool: @property def explicitly_ordered(self) -> bool: + # Do not consider user pre-join ordering intent - they need to re-order post-join in unordered mode. return False def __hash__(self): @@ -307,7 +308,8 @@ def order_ambiguous(self) -> bool: @property def explicitly_ordered(self) -> bool: - return all(child.explicitly_ordered for child in self.children) + # Consider concat as an ordered operations (even though input frames may not be ordered) + return True def __hash__(self): return self._node_hash From 35b458e94e5b5c96b97e55c80945cf3f3c0d163f Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Mon, 23 Sep 2024 16:27:23 -0700 Subject: [PATCH 850/921] chore: remove pandas version limit from test `test_getitem_w_struct_array` (#1014) * chore: remove version limit from test * remove redundant import --- tests/system/small/operations/test_strings.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/system/small/operations/test_strings.py b/tests/system/small/operations/test_strings.py index 98fecaa93b3..bb328360ee4 100644 --- a/tests/system/small/operations/test_strings.py +++ b/tests/system/small/operations/test_strings.py @@ -14,7 +14,6 @@ import re -import packaging.version import pandas as pd import pyarrow as pa import pytest @@ -641,9 +640,6 @@ def test_getitem_w_array(index, column_name, repeated_df, repeated_pandas_df): def test_getitem_w_struct_array(): - if packaging.version.Version(pd.__version__) <= packaging.version.Version("1.5.3"): - pytest.skip("https://github.com/googleapis/python-bigquery/issues/1992") - pa_struct = pa.struct( [ ("name", pa.string()), From 8e9919b53899b6951a10d02643d1d0e53e15665f Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 23 Sep 2024 17:22:23 -0700 Subject: [PATCH 851/921] feat: support ingress settings in `remote_function` (#1011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support ingress settings in `remote_function` * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * propagate new param to bigframes.pandas module * fix mypy error --------- Co-authored-by: Owl Bot --- .../functions/_remote_function_client.py | 23 +++++++ .../functions/_remote_function_session.py | 21 +++++- bigframes/pandas/__init__.py | 4 ++ bigframes/session/__init__.py | 9 +++ tests/system/large/test_remote_function.py | 66 +++++++++++++++++++ 5 files changed, 122 insertions(+), 1 deletion(-) diff --git a/bigframes/functions/_remote_function_client.py b/bigframes/functions/_remote_function_client.py index 75385f11a58..5acd31b4250 100644 --- a/bigframes/functions/_remote_function_client.py +++ b/bigframes/functions/_remote_function_client.py @@ -23,6 +23,7 @@ import string import sys import tempfile +import types from typing import cast, Tuple, TYPE_CHECKING from bigframes_vendored import constants @@ -43,6 +44,15 @@ logger = logging.getLogger(__name__) +# https://cloud.google.com/sdk/gcloud/reference/functions/deploy#--ingress-settings +_INGRESS_SETTINGS_MAP = types.MappingProxyType( + { + "all": functions_v2.ServiceConfig.IngressSettings.ALLOW_ALL, + "internal-only": functions_v2.ServiceConfig.IngressSettings.ALLOW_INTERNAL_ONLY, + "internal-and-gclb": functions_v2.ServiceConfig.IngressSettings.ALLOW_INTERNAL_AND_GCLB, + } +) + class RemoteFunctionClient: # Wait time (in seconds) for an IAM binding to take effect after creation @@ -228,6 +238,7 @@ def create_cloud_function( is_row_processor=False, vpc_connector=None, memory_mib=1024, + ingress_settings="all", ): """Create a cloud function from the given user defined function. @@ -324,6 +335,16 @@ def create_cloud_function( function.service_config.service_account_email = ( self._cloud_function_service_account ) + if ingress_settings not in _INGRESS_SETTINGS_MAP: + raise ValueError( + "'{}' not one of the supported ingress settings values: {}".format( + ingress_settings, list(_INGRESS_SETTINGS_MAP) + ) + ) + function.service_config.ingress_settings = cast( + functions_v2.ServiceConfig.IngressSettings, + _INGRESS_SETTINGS_MAP[ingress_settings], + ) function.kms_key_name = self._cloud_function_kms_key_name create_function_request.function = function @@ -372,6 +393,7 @@ def provision_bq_remote_function( is_row_processor, cloud_function_vpc_connector, cloud_function_memory_mib, + cloud_function_ingress_settings, ): """Provision a BigQuery remote function.""" # Augment user package requirements with any internal package @@ -418,6 +440,7 @@ def provision_bq_remote_function( is_row_processor=is_row_processor, vpc_connector=cloud_function_vpc_connector, memory_mib=cloud_function_memory_mib, + ingress_settings=cloud_function_ingress_settings, ) else: logger.info(f"Cloud function {cloud_function_name} already exists.") diff --git a/bigframes/functions/_remote_function_session.py b/bigframes/functions/_remote_function_session.py index 6bc7a4b079e..a924dbd9c5c 100644 --- a/bigframes/functions/_remote_function_session.py +++ b/bigframes/functions/_remote_function_session.py @@ -19,7 +19,17 @@ import inspect import sys import threading -from typing import Any, cast, Dict, Mapping, Optional, Sequence, TYPE_CHECKING, Union +from typing import ( + Any, + cast, + Dict, + Literal, + Mapping, + Optional, + Sequence, + TYPE_CHECKING, + Union, +) import warnings import bigframes_vendored.constants as constants @@ -110,6 +120,9 @@ def remote_function( cloud_function_max_instances: Optional[int] = None, cloud_function_vpc_connector: Optional[str] = None, cloud_function_memory_mib: Optional[int] = 1024, + cloud_function_ingress_settings: Literal[ + "all", "internal-only", "internal-and-gclb" + ] = "all", ): """Decorator to turn a user defined function into a BigQuery remote function. @@ -280,6 +293,11 @@ def remote_function( default memory of cloud functions be allocated, pass `None`. See for more details https://cloud.google.com/functions/docs/configuring/memory. + cloud_function_ingress_settings (str, Optional): + Ingress settings controls dictating what traffic can reach the + function. By default `all` will be used. It must be one of: + `all`, `internal-only`, `internal-and-gclb`. See for more details + https://cloud.google.com/functions/docs/networking/network-settings#ingress_settings. """ # Some defaults may be used from the session if not provided otherwise import bigframes.exceptions as bf_exceptions @@ -504,6 +522,7 @@ def try_delattr(attr): is_row_processor=is_row_processor, cloud_function_vpc_connector=cloud_function_vpc_connector, cloud_function_memory_mib=cloud_function_memory_mib, + cloud_function_ingress_settings=cloud_function_ingress_settings, ) # TODO(shobs): Find a better way to support udfs with param named "name". diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 94ea6becaba..1bdf49eaf5f 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -669,6 +669,9 @@ def remote_function( cloud_function_max_instances: Optional[int] = None, cloud_function_vpc_connector: Optional[str] = None, cloud_function_memory_mib: Optional[int] = 1024, + cloud_function_ingress_settings: Literal[ + "all", "internal-only", "internal-and-gclb" + ] = "all", ): return global_session.with_default_session( bigframes.session.Session.remote_function, @@ -687,6 +690,7 @@ def remote_function( cloud_function_max_instances=cloud_function_max_instances, cloud_function_vpc_connector=cloud_function_vpc_connector, cloud_function_memory_mib=cloud_function_memory_mib, + cloud_function_ingress_settings=cloud_function_ingress_settings, ) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 7d0cfaee5cf..3a9cba442c0 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1040,6 +1040,9 @@ def remote_function( cloud_function_max_instances: Optional[int] = None, cloud_function_vpc_connector: Optional[str] = None, cloud_function_memory_mib: Optional[int] = 1024, + cloud_function_ingress_settings: Literal[ + "all", "internal-only", "internal-and-gclb" + ] = "all", ): """Decorator to turn a user defined function into a BigQuery remote function. Check out the code samples at: https://cloud.google.com/bigquery/docs/remote-functions#bigquery-dataframes. @@ -1194,6 +1197,11 @@ def remote_function( default memory of cloud functions be allocated, pass `None`. See for more details https://cloud.google.com/functions/docs/configuring/memory. + cloud_function_ingress_settings (str, Optional): + Ingress settings controls dictating what traffic can reach the + function. By default `all` will be used. It must be one of: + `all`, `internal-only`, `internal-and-gclb`. See for more details + https://cloud.google.com/functions/docs/networking/network-settings#ingress_settings. Returns: callable: A remote function object pointing to the cloud assets created in the background to support the remote execution. The cloud assets can be @@ -1220,6 +1228,7 @@ def remote_function( cloud_function_max_instances=cloud_function_max_instances, cloud_function_vpc_connector=cloud_function_vpc_connector, cloud_function_memory_mib=cloud_function_memory_mib, + cloud_function_ingress_settings=cloud_function_ingress_settings, ) def read_gbq_function( diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index d1e82dd4159..18d2609347e 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -2173,3 +2173,69 @@ def foo(x): cleanup_remote_function_assets( session.bqclient, session.cloudfunctionsclient, foo ) + + +@pytest.mark.parametrize( + ("ingress_settings_args", "effective_ingress_settings"), + [ + pytest.param( + {}, functions_v2.ServiceConfig.IngressSettings.ALLOW_ALL, id="no-set" + ), + pytest.param( + {"cloud_function_ingress_settings": "all"}, + functions_v2.ServiceConfig.IngressSettings.ALLOW_ALL, + id="set-all", + ), + pytest.param( + {"cloud_function_ingress_settings": "internal-only"}, + functions_v2.ServiceConfig.IngressSettings.ALLOW_INTERNAL_ONLY, + id="set-internal-only", + ), + pytest.param( + {"cloud_function_ingress_settings": "internal-and-gclb"}, + functions_v2.ServiceConfig.IngressSettings.ALLOW_INTERNAL_AND_GCLB, + id="set-internal-and-gclb", + ), + ], +) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_ingress_settings( + session, scalars_dfs, ingress_settings_args, effective_ingress_settings +): + try: + + def square(x: int) -> int: + return x * x + + square_remote = session.remote_function(reuse=False, **ingress_settings_args)( + square + ) + + # Assert that the GCF is created with the intended maximum timeout + gcf = session.cloudfunctionsclient.get_function( + name=square_remote.bigframes_cloud_function + ) + assert gcf.service_config.ingress_settings == effective_ingress_settings + + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df["int64_too"].apply(square_remote).to_pandas() + pd_result = scalars_pandas_df["int64_too"].apply(square) + + pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + finally: + # clean up the gcp assets created for the remote function + cleanup_remote_function_assets( + session.bqclient, session.cloudfunctionsclient, square_remote + ) + + +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_ingress_settings_unsupported(session): + with pytest.raises( + ValueError, match="'unknown' not one of the supported ingress settings values" + ): + + @session.remote_function(reuse=False, cloud_function_ingress_settings="unknown") + def square(x: int) -> int: + return x * x From a95493df27208ad25b925d6a1c134a7d5c668182 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:27:18 +0000 Subject: [PATCH 852/921] chore(main): release 1.19.0 (#1002) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 24 +++++++++++++++++++++++ bigframes/version.py | 2 +- third_party/bigframes_vendored/version.py | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c210f5c991f..a99e0ecd914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.19.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.18.0...v1.19.0) (2024-09-24) + + +### Features + +* Add ml.model_selection.KFold class ([#1001](https://github.com/googleapis/python-bigquery-dataframes/issues/1001)) ([952cab9](https://github.com/googleapis/python-bigquery-dataframes/commit/952cab92e548b70d077b20bf10f5307751d2ae76)) +* Support bool and bytes types in `describe(include='all')` ([#994](https://github.com/googleapis/python-bigquery-dataframes/issues/994)) ([cc48f58](https://github.com/googleapis/python-bigquery-dataframes/commit/cc48f58cbd94f8110ee863eb57d3fe8dc5a17778)) +* Support ingress settings in `remote_function` ([#1011](https://github.com/googleapis/python-bigquery-dataframes/issues/1011)) ([8e9919b](https://github.com/googleapis/python-bigquery-dataframes/commit/8e9919b53899b6951a10d02643d1d0e53e15665f)) + + +### Bug Fixes + +* Fix miscasting issues with case_when ([#1003](https://github.com/googleapis/python-bigquery-dataframes/issues/1003)) ([038139d](https://github.com/googleapis/python-bigquery-dataframes/commit/038139dfa4fa89167c52c1cb559c2eb5fe2f0411)) + + +### Performance Improvements + +* Join op discards child ordering in unordered mode ([#923](https://github.com/googleapis/python-bigquery-dataframes/issues/923)) ([1b5b0ee](https://github.com/googleapis/python-bigquery-dataframes/commit/1b5b0eea92631b7dd1b688cf1da617fc7ce862dc)) + + +### Dependencies + +* Update ibis version in prerelease tests ([#1012](https://github.com/googleapis/python-bigquery-dataframes/issues/1012)) ([f89785f](https://github.com/googleapis/python-bigquery-dataframes/commit/f89785fcfc51c541253ca8c1e8baf80fbfaea3b6)) + ## [1.18.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.17.0...v1.18.0) (2024-09-18) diff --git a/bigframes/version.py b/bigframes/version.py index 745632616c9..5dda345fcb8 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0" +__version__ = "1.19.0" diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index 745632616c9..5dda345fcb8 100644 --- a/third_party/bigframes_vendored/version.py +++ b/third_party/bigframes_vendored/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.18.0" +__version__ = "1.19.0" From 208a98475389f59d4e32e0cfbcc46824cac278a6 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 24 Sep 2024 11:32:17 -0700 Subject: [PATCH 853/921] fix: Fix __repr__ caching with partial ordering (#1016) --- bigframes/core/tree_properties.py | 4 ++-- bigframes/dataframe.py | 1 - bigframes/session/executor.py | 5 ----- tests/system/conftest.py | 4 ++-- tests/system/small/test_dataframe.py | 24 ++++++++++++++++++++++++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bigframes/core/tree_properties.py b/bigframes/core/tree_properties.py index 4978e75e388..1b0fe0d0722 100644 --- a/bigframes/core/tree_properties.py +++ b/bigframes/core/tree_properties.py @@ -44,8 +44,8 @@ def can_fast_head(node: nodes.BigFrameNode) -> bool: """Can get head fast if can push head operator down to leafs and operators preserve rows.""" if isinstance(node, nodes.LeafNode): return node.supports_fast_head - if isinstance(node, nodes.UnaryNode): - return node.row_preserving and can_fast_head(node.child) + if isinstance(node, (nodes.ProjectionNode, nodes.SelectionNode)): + return can_fast_head(node.child) return False diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 49a668f008e..a59f6bf941d 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -643,7 +643,6 @@ def __repr__(self) -> str: if opts.repr_mode == "deferred": return formatter.repr_query_job(self._compute_dry_run()) - self._cached() # TODO(swast): pass max_columns and get the true column count back. Maybe # get 1 more column than we have requested so that pandas can add the # ... for us? diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index d2a2e0f1b22..f89b5aefec8 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -360,11 +360,6 @@ def _cache_with_cluster_cols( def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): """Executes the query and uses the resulting table to rewrite future executions.""" - - if not self.strictly_ordered: - raise ValueError( - "Caching with offsets only supported in strictly ordered mode." - ) offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") w_offsets, offset_column = array_value.promote_offsets() sql = self.compiler.compile_unordered(self._get_optimized_plan(w_offsets.node)) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 5ee2dc63972..d9246eecfb0 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -154,9 +154,9 @@ def session_load() -> Generator[bigframes.Session, None, None]: session.close() # close generated session at cleanup time -@pytest.fixture(scope="session", params=["ordered", "unordered"]) +@pytest.fixture(scope="session", params=["strict", "partial"]) def maybe_ordered_session(request) -> Generator[bigframes.Session, None, None]: - context = bigframes.BigQueryOptions(location="US", ordering_mode="partial") + context = bigframes.BigQueryOptions(location="US", ordering_mode=request.param) session = bigframes.Session(context=context) yield session session.close() # close generated session at cleanup type diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 0a637e983f2..340df93791d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -567,6 +567,30 @@ def test_repr_w_all_rows(scalars_dfs): assert actual == expected +def test_join_repr(scalars_dfs_maybe_ordered): + scalars_df, scalars_pandas_df = scalars_dfs_maybe_ordered + + scalars_df = ( + scalars_df[["int64_col"]] + .join(scalars_df.set_index("int64_col")[["int64_too"]]) + .sort_index() + ) + scalars_pandas_df = ( + scalars_pandas_df[["int64_col"]] + .join(scalars_pandas_df.set_index("int64_col")[["int64_too"]]) + .sort_index() + ) + # Pandas join result index name seems to depend on the index values in a way that bigframes can't match exactly + scalars_pandas_df.index.name = None + + actual = repr(scalars_df) + + with display_options.pandas_repr(bigframes.options.display): + expected = repr(scalars_pandas_df) + + assert actual == expected + + def test_repr_html_w_all_rows(scalars_dfs): scalars_df, _ = scalars_dfs # get a pandas df of the expected format From 4cb62fd74fc1ac3bb21da23b8639464a9ae3525d Mon Sep 17 00:00:00 2001 From: rey-esp Date: Tue, 24 Sep 2024 19:41:31 +0000 Subject: [PATCH 854/921] docs: move and edit existing linear-regression tutorial snippet (#991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add new alias '__version__' * remove accidental changes * correct assignment * docs: edit existing code snippet for linear regression and add to /snippets * return type * remove filtering * input data updated * remove score and predict Co-authored-by: Tim Sweña (Swast) * delete blank line Co-authored-by: Tim Sweña (Swast) * remove test data Co-authored-by: Tim Sweña (Swast) * remove test checks Co-authored-by: Tim Sweña (Swast) * remove test check Co-authored-by: Tim Sweña (Swast) * add variable input to allow random string to be passed and saved as the model Co-authored-by: Tim Sweña (Swast) * add line that writes the dataframe to big query * update comment with the correct tutorial model reference * update comment with the correct tutorial model reference * remove extra line --------- Co-authored-by: Tim Sweña (Swast) --- .../linear_regression_tutorial_test.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 samples/snippets/linear_regression_tutorial_test.py diff --git a/samples/snippets/linear_regression_tutorial_test.py b/samples/snippets/linear_regression_tutorial_test.py new file mode 100644 index 00000000000..0c861d1120f --- /dev/null +++ b/samples/snippets/linear_regression_tutorial_test.py @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_linear_regression(random_model_id: str) -> None: + your_model_id = random_model_id + # [START bigquery_dataframes_bqml_linear_regression] + from bigframes.ml.linear_model import LinearRegression + import bigframes.pandas as bpd + + # Load data from BigQuery + bq_df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + + # Drop rows with nulls to get training data + training_data = bq_df.dropna(subset=["body_mass_g"]) + + # Specify your feature (or input) columns and the label (or output) column: + feature_columns = training_data.drop(columns=["body_mass_g"]) + label_columns = training_data[["body_mass_g"]] + + # Create the linear model + model = LinearRegression() + model.fit(feature_columns, label_columns) + model.to_gbq( + your_model_id, # For example: "bqml_tutorial.penguins_model" + replace=True, + ) + # [END bigquery_dataframes_bqml_linear_regression] + assert feature_columns is not None + assert label_columns is not None + assert model is not None From 5c1a4c7af56546d7eb464381d82b2f7ed666dac6 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Tue, 24 Sep 2024 12:44:33 -0700 Subject: [PATCH 855/921] refactor: re-arrange `describe()` logic into two helper methods (#1005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: re-arrange describe logic into two helper methods * 🦉 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 --- bigframes/dataframe.py | 74 ++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a59f6bf941d..47c93bfa302 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2302,52 +2302,19 @@ def melt( self._block.melt(id_col_ids, val_col_ids, var_name, value_name) ) - _NUMERIC_DESCRIBE_AGGS = ( - "count", - "mean", - "std", - "min", - "25%", - "50%", - "75%", - "max", - ) - _NON_NUMERIC_DESCRIBE_AGGS = ("count", "nunique") - def describe(self, include: None | Literal["all"] = None) -> DataFrame: - - allowed_non_numeric_types = { - bigframes.dtypes.STRING_DTYPE, - bigframes.dtypes.BOOL_DTYPE, - bigframes.dtypes.BYTES_DTYPE, - } - if include is None: numeric_df = self._drop_non_numeric(permissive=False) if len(numeric_df.columns) == 0: # Describe eligible non-numeric columns - result = self.select_dtypes(include=allowed_non_numeric_types).agg( - self._NON_NUMERIC_DESCRIBE_AGGS - ) - else: - # Otherwise, only describe numeric columns - result = numeric_df.agg(self._NUMERIC_DESCRIBE_AGGS) - return typing.cast(DataFrame, result) + return self._describe_non_numeric() - elif include == "all": - numeric_result = typing.cast( - DataFrame, - self._drop_non_numeric(permissive=False).agg( - self._NUMERIC_DESCRIBE_AGGS - ), - ) + # Otherwise, only describe numeric columns + return self._describe_numeric() - non_numeric_result = typing.cast( - DataFrame, - self.select_dtypes(include=allowed_non_numeric_types).agg( - self._NON_NUMERIC_DESCRIBE_AGGS - ), - ) + elif include == "all": + numeric_result = self._describe_numeric() + non_numeric_result = self._describe_non_numeric() if len(numeric_result.columns) == 0: return non_numeric_result @@ -2364,6 +2331,35 @@ def describe(self, include: None | Literal["all"] = None) -> DataFrame: else: raise ValueError(f"Unsupported include type: {include}") + def _describe_numeric(self) -> DataFrame: + return typing.cast( + DataFrame, + self._drop_non_numeric(permissive=False).agg( + [ + "count", + "mean", + "std", + "min", + "25%", + "50%", + "75%", + "max", + ] + ), + ) + + def _describe_non_numeric(self) -> DataFrame: + return typing.cast( + DataFrame, + self.select_dtypes( + include={ + bigframes.dtypes.STRING_DTYPE, + bigframes.dtypes.BOOL_DTYPE, + bigframes.dtypes.BYTES_DTYPE, + } + ).agg(["count", "nunique"]), + ) + def skew(self, *, numeric_only: bool = False): if not numeric_only: frame = self._raise_on_non_numeric("skew") From 3c543990297ec3be0e30425ee841546217e26d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 24 Sep 2024 15:53:41 -0500 Subject: [PATCH 856/921] docs: limit pypi notebook to 7 days and add more info about differences with partial ordering mode (#1013) * docs: limit pypi notebook to 7 days and add more info about differences with partial ordering mode * avoid expensive `max()` call --- notebooks/dataframes/pypi.ipynb | 554 ++++++++++++++++++++++++++++++-- 1 file changed, 522 insertions(+), 32 deletions(-) diff --git a/notebooks/dataframes/pypi.ipynb b/notebooks/dataframes/pypi.ipynb index 7b16412ff58..a62bd457689 100644 --- a/notebooks/dataframes/pypi.ipynb +++ b/notebooks/dataframes/pypi.ipynb @@ -40,6 +40,16 @@ "execution_count": 2, "metadata": {}, "outputs": [], + "source": [ + "# Choose a package which you want to visualize.\n", + "package_name = \"pandas\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], "source": [ "import bigframes.pandas as bpd\n", "\n", @@ -58,7 +68,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -83,7 +93,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -111,24 +121,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Limit to the most recent 30 days of data\n", + "## Limit to the most recent 7 days of data\n", "\n", - "The PyPI and deps.dev tables are partitioned by date. Query only the most recent 30 days of data to reduce the number of bytes scanned.\n", + "The PyPI and deps.dev tables are partitioned by date. Query only the most recent 7 days of data to reduce the number of bytes scanned.\n", "\n", "Just as with the default ordering mode, filters can be describe in a pandas-compatible way by passing a Boolean Series to the DataFrame's `__getitem__` accessor." ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "import datetime\n", "\n", - "last_30_days = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=30)\n", - "pypi = pypi[pypi[\"timestamp\"] > last_30_days]\n", - "deps = deps[(deps[\"SnapshotAt\"] > last_30_days) & (deps[\"System\"] == \"PYPI\")]" + "now = datetime.datetime.now(datetime.timezone.utc)\n", + "last_7_days = now - datetime.timedelta(days=7)\n", + "last_30_days = now - datetime.timedelta(days=30)\n", + "pypi = pypi[pypi[\"timestamp\"] > last_7_days]\n", + "deps = deps[deps[\"SnapshotAt\"] > last_30_days] # deps are refreshed less frequently\n", + "deps = deps[deps[\"System\"] == \"PYPI\"]" ] }, { @@ -143,15 +156,295 @@ "```\n", "import datetime\n", "\n", - "last_30_days = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=30)\n", + "last_7_days = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7)\n", "\n", "# Without ordering_mode = \"partial\", one must limit the data at \"read\" time to reduce bytes scanned.\n", "pypi = bpd.read_gbq_table(\n", " \"bigquery-public-data.pypi.file_downloads\",\n", " columns=[\"timestamp\", \"project\"],\n", - " filters=[(\"timestamp\", \">\", last_30_days)],\n", + " filters=[(\"timestamp\", \">\", last_7_days)],\n", ")\n", - "```" + "```\n", + "\n", + "`head()` is not available when no ordering has been established. It fails with `OrderRequiredError`. Use `peek()` instead to download a sample of the data. This will be much more efficient, as the query doesn't need to order all rows to determine which are first." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 423d9d93-1495-4c76-b8c2-e830a6e19ff4 is DONE. 110.3 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", + "
timestampcountry_codeurlprojectfiledetailstls_protocoltls_cipher
02024-09-18 18:15:04+00:00US/packages/ff/c8/4cd4b2834012ffc71ae3fd69187f08...aiobreaker{'filename': 'aiobreaker-1.2.0-py3-none-any.wh...{'installer': {'name': 'pip', 'version': '21.1...TLSv1.3TLS_AES_128_GCM_SHA256
12024-09-18 18:29:50+00:00US/packages/21/8e/4562029e179226051cd4aa3135444d...aiobotocore{'filename': 'aiobotocore-1.3.0.tar.gz', 'proj...{'installer': {'name': 'pip', 'version': '24.1...TLSv1.2ECDHE-RSA-AES128-GCM-SHA256
22024-09-18 18:22:14+00:00US/packages/11/16/4226e59bb72e096d9809ccedf349a1...aiobotocore{'filename': 'aiobotocore-2.0.1.tar.gz', 'proj...{'installer': {'name': 'pip', 'version': '24.2...TLSv1.2ECDHE-RSA-AES128-GCM-SHA256
32024-09-18 18:22:08+00:00US/packages/11/16/4226e59bb72e096d9809ccedf349a1...aiobotocore{'filename': 'aiobotocore-2.0.1.tar.gz', 'proj...{'installer': {'name': 'pip', 'version': '24.2...TLSv1.2ECDHE-RSA-AES128-GCM-SHA256
42024-09-18 18:29:22+00:00US/packages/54/b7/453119271cc4c36b07fdeab9b0ff25...aiobotocore{'filename': 'aiobotocore-2.3.3.tar.gz', 'proj...{'installer': {'name': 'pip', 'version': '24.1...TLSv1.2ECDHE-RSA-AES128-GCM-SHA256
\n", + "
" + ], + "text/plain": [ + " timestamp country_code \\\n", + "0 2024-09-18 18:15:04+00:00 US \n", + "1 2024-09-18 18:29:50+00:00 US \n", + "2 2024-09-18 18:22:14+00:00 US \n", + "3 2024-09-18 18:22:08+00:00 US \n", + "4 2024-09-18 18:29:22+00:00 US \n", + "\n", + " url project \\\n", + "0 /packages/ff/c8/4cd4b2834012ffc71ae3fd69187f08... aiobreaker \n", + "1 /packages/21/8e/4562029e179226051cd4aa3135444d... aiobotocore \n", + "2 /packages/11/16/4226e59bb72e096d9809ccedf349a1... aiobotocore \n", + "3 /packages/11/16/4226e59bb72e096d9809ccedf349a1... aiobotocore \n", + "4 /packages/54/b7/453119271cc4c36b07fdeab9b0ff25... aiobotocore \n", + "\n", + " file \\\n", + "0 {'filename': 'aiobreaker-1.2.0-py3-none-any.wh... \n", + "1 {'filename': 'aiobotocore-1.3.0.tar.gz', 'proj... \n", + "2 {'filename': 'aiobotocore-2.0.1.tar.gz', 'proj... \n", + "3 {'filename': 'aiobotocore-2.0.1.tar.gz', 'proj... \n", + "4 {'filename': 'aiobotocore-2.3.3.tar.gz', 'proj... \n", + "\n", + " details tls_protocol \\\n", + "0 {'installer': {'name': 'pip', 'version': '21.1... TLSv1.3 \n", + "1 {'installer': {'name': 'pip', 'version': '24.1... TLSv1.2 \n", + "2 {'installer': {'name': 'pip', 'version': '24.2... TLSv1.2 \n", + "3 {'installer': {'name': 'pip', 'version': '24.2... TLSv1.2 \n", + "4 {'installer': {'name': 'pip', 'version': '24.1... TLSv1.2 \n", + "\n", + " tls_cipher \n", + "0 TLS_AES_128_GCM_SHA256 \n", + "1 ECDHE-RSA-AES128-GCM-SHA256 \n", + "2 ECDHE-RSA-AES128-GCM-SHA256 \n", + "3 ECDHE-RSA-AES128-GCM-SHA256 \n", + "4 ECDHE-RSA-AES128-GCM-SHA256 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Warning: Ensure bpd.options.bigquery.ordering_mode = \"partial\" or else\n", + "# this query() will cause a full table scan because of the sequential index.\n", + "assert bpd.options.bigquery.ordering_mode == \"partial\"\n", + "pypi.peek()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 3a421217-59e2-4722-8382-0930f0a3b9ee is DONE. 1.5 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", + "
SnapshotAtSystemNameVersionDependencyMinimumDepth
02024-08-29 04:39:16.121656+00:00PYPIzxkane-cdk-construct-simple-nat0.2.89{'System': 'PYPI', 'Name': 'attrs', 'Version':...2
12024-08-29 04:39:16.121656+00:00PYPIzxkane-cdk-construct-simple-nat0.2.82{'System': 'PYPI', 'Name': 'attrs', 'Version':...2
22024-08-29 04:39:16.121656+00:00PYPIzxkane-cdk-construct-simple-nat0.2.88{'System': 'PYPI', 'Name': 'attrs', 'Version':...2
32024-08-29 04:39:16.121656+00:00PYPIzxkane-cdk-construct-simple-nat0.2.91{'System': 'PYPI', 'Name': 'attrs', 'Version':...2
42024-08-29 04:39:16.121656+00:00PYPIzxkane-cdk-construct-simple-nat0.2.77{'System': 'PYPI', 'Name': 'attrs', 'Version':...2
\n", + "
" + ], + "text/plain": [ + " SnapshotAt System Name \\\n", + "0 2024-08-29 04:39:16.121656+00:00 PYPI zxkane-cdk-construct-simple-nat \n", + "1 2024-08-29 04:39:16.121656+00:00 PYPI zxkane-cdk-construct-simple-nat \n", + "2 2024-08-29 04:39:16.121656+00:00 PYPI zxkane-cdk-construct-simple-nat \n", + "3 2024-08-29 04:39:16.121656+00:00 PYPI zxkane-cdk-construct-simple-nat \n", + "4 2024-08-29 04:39:16.121656+00:00 PYPI zxkane-cdk-construct-simple-nat \n", + "\n", + " Version Dependency MinimumDepth \n", + "0 0.2.89 {'System': 'PYPI', 'Name': 'attrs', 'Version':... 2 \n", + "1 0.2.82 {'System': 'PYPI', 'Name': 'attrs', 'Version':... 2 \n", + "2 0.2.88 {'System': 'PYPI', 'Name': 'attrs', 'Version':... 2 \n", + "3 0.2.91 {'System': 'PYPI', 'Name': 'attrs', 'Version':... 2 \n", + "4 0.2.77 {'System': 'PYPI', 'Name': 'attrs', 'Version':... 2 " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "deps.peek()" ] }, { @@ -167,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -185,11 +478,111 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job 5b69917f-9ed7-483a-9241-0083acea9990 is DONE. 1.1 GB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ac94c55d-ce8e-4694-ad97-55c933cf3053 is DONE. 123 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameDependencyNamesize
0pandaspytz168
1pandasnumpy168
2pandaspython-dateutil168
3pandassix168
4pandastzdata56
\n", + "
" + ], + "text/plain": [ + " Name DependencyName size\n", + "0 pandas pytz 168\n", + "1 pandas numpy 168\n", + "2 pandas python-dateutil 168\n", + "3 pandas six 168\n", + "4 pandas tzdata 56" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "pandas_deps = deps[deps[\"Name\"] == \"pandas\"].groupby([\"Name\", \"DependencyName\"], as_index=False).size()" + "package_deps = deps[deps[\"Name\"] == package_name].groupby([\"Name\", \"DependencyName\"], as_index=False).size()\n", + "package_deps.peek()" ] }, { @@ -205,14 +598,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "pandas_and_deps = bpd.concat(\n", " [\n", - " pandas_deps.drop(columns=[\"Name\", \"size\"]).rename(columns={\"DependencyName\": \"Name\"}),\n", - " bpd.DataFrame({\"Name\": [\"pandas\"]}),\n", + " package_deps.drop(columns=[\"Name\", \"size\"]).rename(columns={\"DependencyName\": \"Name\"}),\n", + " bpd.DataFrame({\"Name\": [package_name]}),\n", " ],\n", "\n", " # To join DataFrames that have a NULL index, set ignore_index = True.\n", @@ -229,7 +622,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -240,18 +633,24 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Create a time series to visualize by grouping by the date, extracted from the `timestamp` column." + "Create a time series to visualize by grouping by the date, extracted from the `timestamp` column.\n", + "\n", + "**Note:** If you don't `peek()` at your data and only do grouped aggregations, BigQuery DataFrames can eliminate unnecessary ordering from the compilation even without `ordering_mode = \"partial\"`.\n", + "\n", + "When BigQuery DataFrames aggregates over columns, those columns provide a\n", + "unique key post-aggregation that is used for ordering. Any ordering applied before is overridden. By aggregating over\n", + "a time series, the line plots will render in the expected order." ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 88c93524-d74f-4bbd-b6b1-0fb752ebedde is DONE. 28.6 GB processed. Open Job" + "Query job 57037a4c-5b8b-4f30-a5c6-bfeb9731a38f is DONE. 270.4 GB processed. Open Job" ], "text/plain": [ "" @@ -259,14 +658,86 @@ }, "metadata": {}, "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a8ea0b8e-2260-4175-b80d-668a2411c6ad is DONE. 2.0 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "date project \n", + "2024-09-17 numpy 2572982\n", + " pandas 2195692\n", + " python-dateutil 3608119\n", + " pytz 1863133\n", + " six 3064640\n", + " tzdata 991989\n", + "2024-09-18 numpy 13282573\n", + " pandas 10856758\n", + " python-dateutil 17877058\n", + " pytz 9450103\n", + " six 15225000\n", + " tzdata 5230039\n", + "2024-09-19 numpy 13637868\n", + " pandas 11077817\n", + " python-dateutil 18449777\n", + " pytz 9690329\n", + " six 15706263\n", + " tzdata 5473910\n", + "2024-09-20 numpy 12609524\n", + " pandas 10758593\n", + " python-dateutil 17257536\n", + " pytz 9082050\n", + " six 14489456\n", + " tzdata 5206738\n", + "2024-09-21 numpy 8316481\n", + " pandas 7483241\n", + " python-dateutil 11604691\n", + " pytz 5494178\n", + " six 8814983\n", + " tzdata 3141578\n", + "2024-09-22 numpy 7768078\n", + " pandas 6566272\n", + " python-dateutil 10835755\n", + " pytz 5130018\n", + " six 8297507\n", + " tzdata 2811247\n", + "2024-09-23 numpy 12389164\n", + " pandas 10758931\n", + " python-dateutil 17153013\n", + " pytz 9045824\n", + " six 14512209\n", + " tzdata 5214048\n", + "2024-09-24 numpy 10385658\n", + " pandas 8830996\n", + " python-dateutil 14066307\n", + " pytz 7425446\n", + " six 11917222\n", + " tzdata 4550626\n", + "dtype: Int64" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# When BigQuery DataFrames aggregates over columns, those columns provide a\n", - "# unique key post-aggregation that is used for ordering. By aggregating over\n", - "# a time series, the line plots will render in the expexted order.\n", "pandas_pypi = pandas_pypi.assign(date=pandas_pypi[\"timestamp\"].dt.date)\n", - "downloads_per_day = pandas_pypi.groupby([\"date\", \"project\"]).size().unstack()" + "downloads_per_day = pandas_pypi.groupby([\"date\", \"project\"]).size()\n", + "\n", + "# Cache after the aggregation so that the aggregation only runs once.\n", + "downloads_per_day.cache()\n", + "downloads_per_day.to_pandas()" ] }, { @@ -278,13 +749,25 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 09bcc1a0-def3-474f-acd5-1d55b9653e07 is DONE. 28.6 GB processed. Open Job" + "Query job 319558aa-e092-4fd0-a8aa-447fca216a57 is DONE. 1.6 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job e64eb861-bd12-4748-9c29-d97990aa1241 is DONE. 1.2 kB processed. Open Job" ], "text/plain": [ "" @@ -299,13 +782,13 @@ "" ] }, - "execution_count": 19, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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LmdvvfWWgyq0vVOnTrGAgAD21FNPse+//54tXLiQxcbGsszMzKtuq9PpGGOMLVq0iPn5+bH8/HxHl2u1a+U1n8q8kl6vZy+88AILCwtjZ86ccXC11usqb0xMzDXzOsNrVmVlJevTpw9btmwZKywsZN9//z176KGHmFQqZV999VWn25pPUz/xxBPM19fXJV93rpd306ZNV93e/DobHBzMCgoKeKi4a9QAubndu3ez4cOHs7q6OsuxoqIiNmvWLKZUKi3r4BgMBnbixAn23HPPscDAQJdcH6e1tZXNnj2bzZo1i61atYolJiayJ5988ppNwfbt29mcOXNYnz59BJHX7M0333TJF13GTPNeJk+ezBYvXtzp+LBhw9iiRYu6/Jq33nqLcRznkj/jW8371Vdfsfvvv58FBwcLIu/Jkyed4jUrOzubDR48mJWUlFiOtbW1sWeffZbJZDL2448/MsYuzbtbv369yz4nGbu1vN9//z2bNWsWCwkJcbq8dArMzTU2NiInJwc6nc5yLCoqCm+++SZ0Oh2mT5+OPXv2IDw8HH379sWkSZOwePFiRERE8Fd0N8nlcqSmpsLDwwMPP/wwfH198emnnwIAlixZgqioqE63HzZsGIqLi/HSSy9d9TlXcKt5zefdn3nmGT7KtYny8nJ4e3tj5syZAACtVguZTIYJEyagrq7uqtsbjUaMGzcOhYWF1zw96MxuNW9KSgoyMjKQnp6O6OhoR5drtVvN26dPH0ycOJH316ympibk5+dbrugyGo1QKpV4/fXX0d7ejtmzZ+PYsWOW5+DMmTMxZcoU9OvXj7earXEreZOSknD69Gn885//RP/+/Xmu/Ap8d2DEviorK1lycjJ7/vnnWXNzc6fPZWRksKSkJPbll1/yVJ3ttbe3d1pbY926dZaREfPwuEajYdXV1Ywx178S6mbyarVaVltby1eJNmU0GtnmzZstH5t/fmvWrGEzZszodNvW1laH1mYPt5LX/Pvd1ZWAruJW8ra0tDi0tuvR6/Vs7NixbObMmZbRdnPtFy5cYGPHjmWrVq1iRqPR5V9zGLu5vC+//LLlueismfmfhk3sKjg4GKmpqdi1axe2bNkCtVpt+dzIkSNhMBicau0MaykUCnAcB4PBAAD461//irlz5+LgwYP497//jYKCAixbtgz33HMPtFqt80zG66abyfvcc8/h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x0ptg/K6d15AeJkpr/PndcWarSG0vNdDp8CciHl9DDPzkGRrays0Gg1ycnIwe/Zs9O3bF76+vvDz88O2bduQkZGBIUOG8Fh591Be984LCC8z5XX+vK5YszWElvdW0AiQE7h48SKAS505ABgMBkgkEpSWlmLgwIE4evQoJkyYgPz8fEydOhVhYWEIDAxEZmamyz1JKa975wWEl5nyOn9eV6zZGkLL2y2OnXNNrlRYWMi8vLzYwoULLcfM62OUlZUxf39/9thjjzGj0Wg5bp6R74q7QlNe987LmPAyU17nz+uKNVtDaHm7i0aAeHbq1CkolUqcOHECf/nLXwCYNgXUarXYvn07HnnkEXz44YfgOA5isbjT13Icx0fJVqG87p0XEF5myuv8eV2xZmsILW93UQPEM7lcjp49e2LatGnIyMjAokWLAAAymQz33nsv3n777Ws+QV3xiUp53TsvILzMlNf587pizdYQWt7ukvBdgNANGTIEw4cPx4IFCyCTyfDZZ59h6dKlaGpqQnJyMubPnw+pVMp3mTZDed07LyC8zJTX+fO6Ys3WEFrebuP7HJzQqVQqNnToUJadnc1UKhX76KOPmJ+fH+M4juXl5THGOu+Z4+oor3vnZUx4mSmv8+d1xZqtIbS83UWnwHik0+kgl8sRHByM1tZWeHh4YPfu3dDpdOjfvz8+/vhjALhqqNJVUV73zgsILzPldf68rlizNYSW1xp0CsxBKioqkJWVBa1Wi4iICAwbNswyBDl8+HAUFRXho48+wr59+/DDDz/gxIkTWLt2LSQSCd566y2eq791lNe98wLCy0x5nT+vK9ZsDaHltTm+h6CEIC8vj/Xr148lJyczf39/lpSUxL799lvL519++WXGcRyLjIxkx48fZ4wx1tDQwP73f/+XFRcX81V2t1Fe987LmPAyU17nz+uKNVtDaHntgRogOysqKmK9e/dmy5YtY42NjezYsWNszpw5bP78+Z124F28eDHLzMxkjLn2egyU173zMia8zJTX+fO6Ys3WEFpee6EGyI40Gg1bunQpmzFjBtNoNJbjn3zyCfPz82MXL17ksTrbo7wm7pqXMeFlprwmzpzXFWu2htDy2hPNAbIjo9GI3r17IzY2FjKZzLLT7m233QZPT0/odLouv+byfVtcCeV177yA8DJTXufP64o1W0Noee2JGiA7UigUmDZtGiIjIzsd79mzJ6RSaacnanZ2NhITE136SUp5Tdw1LyC8zJTXxJnzumLN1hBaXnui74qNVVZWIjMzEzt37oTRaLQ8SQ0Gg2WFzaamJjQ0NFi+ZsWKFZgwYQLq6urAGOOl7u6ivO6dFxBeZsrr/HldsWZrCC2vwzj+rJv7ys3NZX379mUDBw5kPj4+LCYmhm3atInV1dUxxi5NQissLGQBAQGsvr6erV69mimVSnbs2DE+S+8WyuveeRkTXmbK6/x5XbFmawgtryNRA2QjNTU1LCYmhi1fvpwVFxez8vJyNnPmTBYbG8tWrlzJampqLLetrq5miYmJbObMmUwmk7nkk5TyundexoSXmfI6f15XrNkaQsvraNQA2Uh+fj6LiIi46kn397//nQ0ZMoS9/vrrTKVSMcYYO3XqFOM4jimVSpadnc1DtdajvCbumpcx4WWmvCbOnNcVa7aG0PI6Gs0BshGdTge9Xo+2tjYAQHt7OwBg7dq1GD9+PN5//30UFRUBAHr16oXFixcjKysLCQkJfJVsFcrr3nkB4WWmvM6f1xVrtobQ8joaxxjNjrKV5ORkeHp64vfffwcAaDQayOVyAMCIESPQv39/fPXVVwAAtVoNhULBW622QHndOy8gvMyU1/nzumLN1hBaXkeiEaBuUqlUaGlpQXNzs+XYhx9+iPz8fMyePRsAIJfLodfrAQBjx46FSqWy3NbVnqSU173zAsLLTHmdP68r1mwNoeXlGzVA3XDq1Cncf//9SE1NRWxsLDZu3AgAiI2Nxbp16/Drr79i+vTp0Ol0lvUXampq0KNHD+j1epe7JJHyundeQHiZKa/z53XFmq0htLxOga/JR64qPz+f+fn5saeffppt3LiRLV26lEmlUpaVlcUYY0ylUrHt27ez3r17s5iYGDZt2jQ2Y8YM1qNHD3bixAmeq791lNe98zImvMyU1/nzumLN1hBaXmdBc4BuQX19PWbNmoWYmBisW7fOcnz8+PEYMmQI3n33XcuxlpYWvPLKK6ivr4dCocATTzyBuLg4PsruNspr4q55AeFlprwmzpzXFWu2htDyOhPaCuMW6HQ6NDY24sEHHwRwaX+VyMhI1NfXAwCYaWkBeHl54V//+len27kayuveeQHhZaa8zp/XFWu2htDyOhP67t2CoKAgfPnllxgzZgwA0zLkABAWFmZ5InIcB5FI1GkSm3mpcldDed07LyC8zJTX+fO6Ys3WEFpeZ0IN0C0aMGAAAFP3LZVKAZi685qaGstt1qxZg48//tgyU9+Vn6iU173zAsLLTHmdP68r1mwNoeV1FnQKrJtEIhEYY5YnoblTX7FiBV555RVkZ2dDInGfby/lde+8gPAyU17nz+uKNVtDaHn5RiNAVjDPH5dIJAgPD8ebb76J119/HceOHUN8fDzP1dke5XXvvIDwMlNe58/rijVbQ2h5+UStpBXM3blUKsV//vMfeHt748CBAxg2bBjPldkH5XXvvIDwMlNe58/rijVbQ2h5+UQjQDaQlpYGADh06BCSkpJ4rsb+KK/7E1pmyuv8XLFmawgtLx9oHSAbUalU6NGjB99lOAzldX9Cy0x5nZ8r1mwNoeV1NGqACCGEECI4dAqMEEIIIYJDDRAhhBBCBIcaIEIIIYQIDjVAhBBCCBEcaoAIIYQQIjjUABFCCCFEcKgBIoS4lXHjxmHJkiV8l0EIcXLUABFCBCs9PR0cx6GxsZHvUgghDkYNECGEEEIEhxogQojLUqlUePTRR+Hp6YmQkBC89dZbnT7/xRdfICkpCV5eXggODsbs2bNRU1MDACgtLcX48eMBAL169QLHcZg7dy4AwGg0Ys2aNYiMjIRSqUR8fDw2b97s0GyEEPuiBogQ4rKee+457N27F9u2bcMvv/yC9PR0ZGVlWT6v0+mwevVq5ObmYuvWrSgtLbU0OeHh4fjuu+8AAIWFhaisrMS6desAAGvWrMHnn3+ODz74APn5+Xj66afx8MMPY+/evQ7PSAixD9oLjBDiklpbW+Hn54cvv/wS06dPBwDU19ejd+/eePzxx/HOO+9c9TXHjh3DiBEj0NLSAk9PT6Snp2P8+PFoaGhAz549AQAajQa+vr747bffMGrUKMvXLliwAG1tbdi0aZMj4hFC7EzCdwGEENIdxcXF0Gq1SElJsRzz9fVFdHS05ePjx4/j5ZdfRm5uLhoaGmA0GgEAZWVliIuL6/J+i4qK0NbWhkmTJnU6rtVqkZiYaIckhBA+UANECHFLKpUKaWlpSEtLw8aNGxEQEICysjKkpaVBq9Ve8+taW1sBADt27EBYWFinz8nlcrvWTAhxHGqACCEuKSoqClKpFEeOHEGfPn0AAA0NDThz5gxSU1NRUFCAuro6rF27FuHh4QBMp8AuJ5PJAAAGg8FyLC4uDnK5HGVlZUhNTXVQGkKIo1EDRAhxSZ6ennjsscfw3HPPwc/PD4GBgXjhhRcgEpmu7ejTpw9kMhn+53/+B4sWLcLJkyexevXqTvfRt29fcByHH3/8EVOnToVSqYSXlxeeffZZPP300zAajRg9ejSamppw8OBBeHt7Y86cOXzEJYTYGF0FRghxWW+88QbGjBmDu+++GxMnTsTo0aMxfPhwAEBAQAA+++wzfPvtt4iLi8PatWvx5ptvdvr6sLAwrFq1Cv/4xz8QFBSEJ598EgCwevVqvPTSS1izZg1iY2MxZcoU7NixA5GRkQ7PSAixD7oKjBBCCCGCQyNAhBBCCBEcaoAIIYQQIjjUABFCCCFEcKgBIoQQQojgUANECCGEEMGhBogQQgghgkMNECGEEEIEhxogQgghhAgONUCEEEIIERxqgAghhBAiONQAEUIIIURw/h/vaOJwmJvaIQAAAABJRU5ErkJggg==", + "image/png": "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JL1eL73xxhuSn59frXgPSJIk/fTTT1L37t0L/c/1ev0dn1cUnKziZGVl8fTTT5ORkUHfvn1Zt24dzs7OvPfee3To0KHY1/3444+0b9+eVq1aVaK15U9Jxm+1WlEqlURFRbF27VoOHTpE/fr1GT9+PM2aNbPzCMpOad8DCQkJLFy4kO+++47ff/+d9u3bV77R5YhOp2PcuHEEBgaycOFC2/ohQ4bg7OzMunXrbO+B6OhowsPD2bFjB8HBwdx333014j1QkmtwMwsWLODZZ5/lyJEjt/yuqA6Udvz//fcfPXr0wMvLi61bt9KxY8fKNrncKe01+OOPP/j111/ZuXMn69evrzXvAbPZTEZGBp6enkD5FJysOQksNRQnJyeGDBnCoEGDePrpp3n66afJzMxk1qxZHD16tND+uSUBHn300WovkqBk41cqlVgsFkJCQpg+fTqrVq3igw8+qBE/kFD694CXlxcTJkzgv//+q/YiCSAlJQVfX1/uvfdeIC9Pa/jw4QVa10iSRHBwMMOHD+eLL77g5ZdfrjHvgZJcg5vveUePHs2lS5eq/Q8klH78devWpWvXruzZs6dGiCQo/TXo2LEjHTp0YMeOHbXqPaBWq20iCcqplt4d+6IEdmPNmjVS//79pfvuu88WWsrOzpZiYmLsa1glUdz4889+qenUtvdA/ryj3PDCsmXLpL59+xZYl1uNvCZS269BScefnJwsSZL8eahplPQapKSkSJJU82Y9l3T8Op2uXM8rPErViFxv0ahRo3jyySfJzMzknXfe4dChQ7z44ouEhYVhMBhqbM2U242/U6dONXr8UPveA7njGDBggO157h1iRkYGycnJtnWzZ89mypQpBbxMNYE7uQb5Z4hWd0o7/ieeeAKTyYSDg4PdbC5vSnsNpk6dislkqjH140o7/smTJ2M2m8vte1BdLkcRVCi5bwCVSmX7AnjooYdQKBR8++23DB06FIvFwt9//11jpn/mp7aPH2rvNcj9Mswdv0KhwGw2o1ar8fDwwM3NDYVCwdtvv81HH33EgQMHUKtr1tfanVyDmiQSavv4QVwDe38PCI9SFcdisaBQKEhLSwOwdQYH2atgsViwWCzs2bOHLl262NPUCqG2jx/ENbh5/LlfgFqtFm9vb958800++eQT9u/fXyNyMYqitl+D2j5+ENfAruMv10CeoFzJrah7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", "text/plain": [ "
" ] @@ -315,8 +798,15 @@ } ], "source": [ - "downloads_per_day.plot.line(rot=45, ylabel=\"daily downloads\", ylim=(0, 2e7))" + "downloads_per_day.unstack().plot.line(rot=45, ylabel=\"daily downloads\", ylim=(0, 2e7))" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -335,7 +825,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.12.6" } }, "nbformat": 4, From 1930b4efe60295751ceef89c2a824923a35b19af Mon Sep 17 00:00:00 2001 From: feri Date: Wed, 25 Sep 2024 17:37:35 +0200 Subject: [PATCH 857/921] feat: add bigframes.ml.compose.SQLScalarColumnTransformer to create custom SQL-based transformations (#955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for custom transformers (not ML.) in ColumnTransformer. * allow numbers in Custom-Transformer-IDs. * comment was moved to the end of the sql. * Do not offer the feedback link for missing custom transformers. * cleanup typing hints. * Add unit tests for CustomTransformer. * added unit tests for _extract_output_names() and _compile_to_sql(). * run black and flake8 linter. * fixed wrong @classmethod annotation. * on the way to SQLScalarColumnTransformer * remove pytest.main call. * remove CustomTransformer class and implementations. * fix typing. * fix typing. * fixed mock typing. * replace _NameClass. * black formating. * add traget_column as input_column with a "?" prefix when parsing SQLScalarColumnTransformer from sql. * reformatted with black version 22.3.0. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * remove eclipse project files * SQLScalarColumnTransformer needs not to be inherited from base.BaseTransformer. * remove filter for "ML." sqls in _extract_output_names() of BaseTransformer * introduced type hint SingleColTransformer for transformers contained in ColumnTransformer * make sql and target_column private in SQLScalarColumnTransformer * Add documentation for SQLScalarColumnTransformer. * add first system test for SQLScalarColumnTransformer. * SQLScalarColumnTransformer system tests for fit-transform and save-load * make SQLScalarColumnTransformer comparable (equals) for comparing sets in tests * implement hash and eq (copied from BaseTransformer) * undo accidentally checked in files * remove eclipse settings accidentally checked in. * fix docs. * Update bigframes/ml/compose.py * Update bigframes/ml/compose.py * add support for flexible column names. * remove main. * add system test for output column with flexible column name * system tests: add new flexible output column to check-df-schema. * Apply suggestions from code review --------- Co-authored-by: Ferenc Hechler Co-authored-by: Tim Sweña (Swast) Co-authored-by: Owl Bot --- bigframes/ml/base.py | 4 - bigframes/ml/compose.py | 134 ++++++++- tests/system/large/ml/test_compose.py | 103 +++++++ tests/unit/ml/test_compose.py | 406 ++++++++++++++++++++++++++ 4 files changed, 633 insertions(+), 14 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 6ae06c9d9f4..81181b58cfe 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -198,10 +198,6 @@ def _extract_output_names(self): # pass the columns that are not transformed if "transformSql" not in transform_col_dict: continue - transform_sql: str = transform_col_dict["transformSql"] - if not transform_sql.startswith("ML."): - continue - output_names.append(transform_col_dict["name"]) self._output_names = output_names diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 3cfa1851f58..08c9761cc37 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -46,6 +46,101 @@ ) +class SQLScalarColumnTransformer: + r""" + Wrapper for plain SQL code contained in a ColumnTransformer. + + Create a single column transformer in plain sql. + This transformer can only be used inside ColumnTransformer. + + When creating an instance '{0}' can be used as placeholder + for the column to transform: + + SQLScalarColumnTransformer("{0}+1") + + The default target column gets the prefix 'transformed\_' + but can also be changed when creating an instance: + + SQLScalarColumnTransformer("{0}+1", "inc_{0}") + + **Examples:** + + >>> from bigframes.ml.compose import ColumnTransformer, SQLScalarColumnTransformer + >>> import bigframes.pandas as bpd + + >>> df = bpd.DataFrame({'name': ["James", None, "Mary"], 'city': ["New York", "Boston", None]}) + >>> col_trans = ColumnTransformer([ + ... ("strlen", + ... SQLScalarColumnTransformer("CASE WHEN {0} IS NULL THEN 15 ELSE LENGTH({0}) END"), + ... ['name', 'city']), + ... ]) + >>> col_trans = col_trans.fit(df) + >>> df_transformed = col_trans.transform(df) + >>> df_transformed + transformed_name transformed_city + 0 5 8 + 1 15 6 + 2 4 15 + + [3 rows x 2 columns] + + SQLScalarColumnTransformer can be combined with other transformers, like StandardScaler: + + >>> col_trans = ColumnTransformer([ + ... ("identity", SQLScalarColumnTransformer("{0}", target_column="{0}"), ["col1", "col5"]), + ... ("increment", SQLScalarColumnTransformer("{0}+1", target_column="inc_{0}"), "col2"), + ... ("stdscale", preprocessing.StandardScaler(), "col3"), + ... # ... + ... ]) + + """ + + def __init__(self, sql: str, target_column: str = "transformed_{0}"): + super().__init__() + self._sql = sql + self._target_column = target_column.replace("`", "") + + PLAIN_COLNAME_RX = re.compile("^[a-z][a-z0-9_]*$", re.IGNORECASE) + + def escape(self, colname: str): + colname = colname.replace("`", "") + if self.PLAIN_COLNAME_RX.match(colname): + return colname + return f"`{colname}`" + + def _compile_to_sql( + self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None + ) -> List[str]: + if columns is None: + columns = X.columns + result = [] + for column in columns: + current_sql = self._sql.format(self.escape(column)) + current_target_column = self.escape(self._target_column.format(column)) + result.append(f"{current_sql} AS {current_target_column}") + return result + + def __repr__(self): + return f"SQLScalarColumnTransformer(sql='{self._sql}', target_column='{self._target_column}')" + + def __eq__(self, other) -> bool: + return type(self) is type(other) and self._keys() == other._keys() + + def __hash__(self) -> int: + return hash(self._keys()) + + def _keys(self): + return (self._sql, self._target_column) + + +# Type hints for transformers contained in ColumnTransformer +SingleColTransformer = Union[ + preprocessing.PreprocessingType, + impute.SimpleImputer, + SQLScalarColumnTransformer, +] + + @log_adapter.class_logger class ColumnTransformer( base.Transformer, @@ -60,7 +155,7 @@ def __init__( transformers: Iterable[ Tuple[ str, - Union[preprocessing.PreprocessingType, impute.SimpleImputer], + SingleColTransformer, Union[str, Iterable[str]], ] ], @@ -78,14 +173,12 @@ def _keys(self): @property def transformers_( self, - ) -> List[ - Tuple[str, Union[preprocessing.PreprocessingType, impute.SimpleImputer], str] - ]: + ) -> List[Tuple[str, SingleColTransformer, str,]]: """The collection of transformers as tuples of (name, transformer, column).""" result: List[ Tuple[ str, - Union[preprocessing.PreprocessingType, impute.SimpleImputer], + SingleColTransformer, str, ] ] = [] @@ -103,6 +196,8 @@ def transformers_( return result + AS_FLEXNAME_SUFFIX_RX = re.compile("^(.*)\\bAS\\s*`[^`]+`\\s*$", re.IGNORECASE) + @classmethod def _extract_from_bq_model( cls, @@ -114,7 +209,7 @@ def _extract_from_bq_model( transformers_set: Set[ Tuple[ str, - Union[preprocessing.PreprocessingType, impute.SimpleImputer], + SingleColTransformer, Union[str, List[str]], ] ] = set() @@ -130,8 +225,11 @@ def camel_to_snake(name): if "transformSql" not in transform_col_dict: continue transform_sql: str = transform_col_dict["transformSql"] - if not transform_sql.startswith("ML."): - continue + + # workaround for bug in bq_model returning " AS `...`" suffix for flexible names + flex_name_match = cls.AS_FLEXNAME_SUFFIX_RX.match(transform_sql) + if flex_name_match: + transform_sql = flex_name_match.group(1) output_names.append(transform_col_dict["name"]) found_transformer = False @@ -148,8 +246,22 @@ def camel_to_snake(name): found_transformer = True break if not found_transformer: - raise NotImplementedError( - f"Unsupported transformer type. {constants.FEEDBACK_LINK}" + if transform_sql.startswith("ML."): + raise NotImplementedError( + f"Unsupported transformer type. {constants.FEEDBACK_LINK}" + ) + + target_column = transform_col_dict["name"] + sql_transformer = SQLScalarColumnTransformer( + transform_sql, target_column=target_column + ) + input_column_name = f"?{target_column}" + transformers_set.add( + ( + camel_to_snake(sql_transformer.__class__.__name__), + sql_transformer, + input_column_name, + ) ) transformer = cls(transformers=list(transformers_set)) @@ -167,6 +279,8 @@ def _merge( assert len(transformers) > 0 _, transformer_0, column_0 = transformers[0] + if isinstance(transformer_0, SQLScalarColumnTransformer): + return self # SQLScalarColumnTransformer only work inside ColumnTransformer feature_columns_sorted = sorted( [ cast(str, feature_column.name) diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index 59c5a1538f3..ba963837e53 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -36,6 +36,32 @@ def test_columntransformer_standalone_fit_and_transform( preprocessing.MinMaxScaler(), ["culmen_length_mm"], ), + ( + "increment", + compose.SQLScalarColumnTransformer("{0}+1"), + ["culmen_length_mm", "flipper_length_mm"], + ), + ( + "length", + compose.SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -1 ELSE LENGTH({0}) END", + target_column="len_{0}", + ), + "species", + ), + ( + "ohe", + compose.SQLScalarColumnTransformer( + "CASE WHEN {0}='Adelie Penguin (Pygoscelis adeliae)' THEN 1 ELSE 0 END", + target_column="ohe_adelie", + ), + "species", + ), + ( + "identity", + compose.SQLScalarColumnTransformer("{0}", target_column="{0}"), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) @@ -51,6 +77,12 @@ def test_columntransformer_standalone_fit_and_transform( "standard_scaled_culmen_length_mm", "min_max_scaled_culmen_length_mm", "standard_scaled_flipper_length_mm", + "transformed_culmen_length_mm", + "transformed_flipper_length_mm", + "len_species", + "ohe_adelie", + "culmen_length_mm", + "flipper_length_mm", ], index=[1633, 1672, 1690], col_exact=False, @@ -70,6 +102,19 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "length", + compose.SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -1 ELSE LENGTH({0}) END", + target_column="len_{0}", + ), + "species", + ), + ( + "identity", + compose.SQLScalarColumnTransformer("{0}", target_column="{0}"), + ["culmen_length_mm", "flipper_length_mm"], + ), ] ) @@ -83,6 +128,9 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): "onehotencoded_species", "standard_scaled_culmen_length_mm", "standard_scaled_flipper_length_mm", + "len_species", + "culmen_length_mm", + "flipper_length_mm", ], index=[1633, 1672, 1690], col_exact=False, @@ -102,6 +150,27 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): preprocessing.StandardScaler(), ["culmen_length_mm", "flipper_length_mm"], ), + ( + "length", + compose.SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -1 ELSE LENGTH({0}) END", + target_column="len_{0}", + ), + "species", + ), + ( + "identity", + compose.SQLScalarColumnTransformer("{0}", target_column="{0}"), + ["culmen_length_mm", "flipper_length_mm"], + ), + ( + "flexname", + compose.SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -1 ELSE LENGTH({0}) END", + target_column="Flex {0} Name", + ), + "species", + ), ] ) transformer.fit( @@ -122,6 +191,36 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): ), ("standard_scaler", preprocessing.StandardScaler(), "culmen_length_mm"), ("standard_scaler", preprocessing.StandardScaler(), "flipper_length_mm"), + ( + "sql_scalar_column_transformer", + compose.SQLScalarColumnTransformer( + "CASE WHEN species IS NULL THEN -1 ELSE LENGTH(species) END", + target_column="len_species", + ), + "?len_species", + ), + ( + "sql_scalar_column_transformer", + compose.SQLScalarColumnTransformer( + "flipper_length_mm", target_column="flipper_length_mm" + ), + "?flipper_length_mm", + ), + ( + "sql_scalar_column_transformer", + compose.SQLScalarColumnTransformer( + "culmen_length_mm", target_column="culmen_length_mm" + ), + "?culmen_length_mm", + ), + ( + "sql_scalar_column_transformer", + compose.SQLScalarColumnTransformer( + "CASE WHEN species IS NULL THEN -1 ELSE LENGTH(species) END ", + target_column="Flex species Name", + ), + "?Flex species Name", + ), ] assert set(reloaded_transformer.transformers) == set(expected) assert reloaded_transformer._bqml_model is not None @@ -136,6 +235,10 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): "onehotencoded_species", "standard_scaled_culmen_length_mm", "standard_scaled_flipper_length_mm", + "len_species", + "culmen_length_mm", + "flipper_length_mm", + "Flex species Name", ], index=[1633, 1672, 1690], col_exact=False, diff --git a/tests/unit/ml/test_compose.py b/tests/unit/ml/test_compose.py index 60dcc75b63c..7643f76e56b 100644 --- a/tests/unit/ml/test_compose.py +++ b/tests/unit/ml/test_compose.py @@ -11,11 +11,17 @@ # 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 unittest import mock +from google.cloud import bigquery +import pytest import sklearn.compose as sklearn_compose # type: ignore import sklearn.preprocessing as sklearn_preprocessing # type: ignore from bigframes.ml import compose, preprocessing +from bigframes.ml.compose import ColumnTransformer, SQLScalarColumnTransformer +from bigframes.ml.core import BqmlModel +import bigframes.pandas as bpd def test_columntransformer_init_expectedtransforms(): @@ -173,3 +179,403 @@ def test_columntransformer_repr_matches_sklearn(): ) assert bf_column_transformer.__repr__() == sk_column_transformer.__repr__() + + +@pytest.fixture(scope="session") +def mock_X(): + mock_df = mock.create_autospec(spec=bpd.DataFrame) + return mock_df + + +def test_columntransformer_init_with_sqltransformers(): + ident_transformer = SQLScalarColumnTransformer("{0}", target_column="ident_{0}") + len1_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -2 ELSE LENGTH({0}) END", target_column="len1_{0}" + ) + len2_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN 99 ELSE LENGTH({0}) END", target_column="len2_{0}" + ) + label_transformer = preprocessing.LabelEncoder() + column_transformer = compose.ColumnTransformer( + [ + ( + "ident_trafo", + ident_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), + ("len1_trafo", len1_transformer, ["species"]), + ("len2_trafo", len2_transformer, ["species"]), + ("label", label_transformer, "species"), + ] + ) + + assert column_transformer.transformers_ == [ + ("ident_trafo", ident_transformer, "culmen_length_mm"), + ("ident_trafo", ident_transformer, "flipper_length_mm"), + ("len1_trafo", len1_transformer, "species"), + ("len2_trafo", len2_transformer, "species"), + ("label", label_transformer, "species"), + ] + + +def test_columntransformer_repr_sqltransformers(): + ident_transformer = SQLScalarColumnTransformer("{0}", target_column="ident_{0}") + len1_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -2 ELSE LENGTH({0}) END", target_column="len1_{0}" + ) + len2_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN 99 ELSE LENGTH({0}) END", target_column="len2_{0}" + ) + label_transformer = preprocessing.LabelEncoder() + column_transformer = compose.ColumnTransformer( + [ + ( + "ident_trafo", + ident_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), + ("len1_trafo", len1_transformer, ["species"]), + ("len2_trafo", len2_transformer, ["species"]), + ("label", label_transformer, "species"), + ] + ) + + expected = """ColumnTransformer(transformers=[('ident_trafo', + SQLScalarColumnTransformer(sql='{0}', target_column='ident_{0}'), + ['culmen_length_mm', 'flipper_length_mm']), + ('len1_trafo', + SQLScalarColumnTransformer(sql='CASE WHEN {0} IS NULL THEN -2 ELSE LENGTH({0}) END', target_column='len1_{0}'), + ['species']), + ('len2_trafo', + SQLScalarColumnTransformer(sql='CASE WHEN {0} IS NULL THEN 99 ELSE LENGTH({0}) END', target_column='len2_{0}'), + ['species']), + ('label', LabelEncoder(), 'species')])""" + actual = column_transformer.__repr__() + assert expected == actual + + +def test_customtransformer_compile_sql(mock_X): + ident_trafo = SQLScalarColumnTransformer("{0}", target_column="ident_{0}") + sqls = ident_trafo._compile_to_sql(X=mock_X, columns=["col1", "col2"]) + assert sqls == [ + "col1 AS ident_col1", + "col2 AS ident_col2", + ] + + len1_trafo = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -5 ELSE LENGTH({0}) END", target_column="len1_{0}" + ) + sqls = len1_trafo._compile_to_sql(X=mock_X, columns=["col1", "col2"]) + assert sqls == [ + "CASE WHEN col1 IS NULL THEN -5 ELSE LENGTH(col1) END AS len1_col1", + "CASE WHEN col2 IS NULL THEN -5 ELSE LENGTH(col2) END AS len1_col2", + ] + + len2_trafo = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN 99 ELSE LENGTH({0}) END", target_column="len2_{0}" + ) + sqls = len2_trafo._compile_to_sql(X=mock_X, columns=["col1", "col2"]) + assert sqls == [ + "CASE WHEN col1 IS NULL THEN 99 ELSE LENGTH(col1) END AS len2_col1", + "CASE WHEN col2 IS NULL THEN 99 ELSE LENGTH(col2) END AS len2_col2", + ] + + +def create_bq_model_mock(mocker, transform_columns, feature_columns=None): + properties = {"transformColumns": transform_columns} + mock_bq_model = bigquery.Model("model_project.model_dataset.model_id") + type(mock_bq_model)._properties = mock.PropertyMock(return_value=properties) + if feature_columns: + result = [ + bigquery.standard_sql.StandardSqlField(col, None) for col in feature_columns + ] + mocker.patch( + "google.cloud.bigquery.model.Model.feature_columns", + new_callable=mock.PropertyMock(return_value=result), + ) + + return mock_bq_model + + +@pytest.fixture +def bq_model_good(mocker): + return create_bq_model_mock( + mocker, + [ + { + "name": "ident_culmen_length_mm", + "type": {"typeKind": "INT64"}, + "transformSql": "culmen_length_mm /*CT.IDENT()*/", + }, + { + "name": "ident_flipper_length_mm", + "type": {"typeKind": "INT64"}, + "transformSql": "flipper_length_mm /*CT.IDENT()*/", + }, + { + "name": "len1_species", + "type": {"typeKind": "INT64"}, + "transformSql": "CASE WHEN species IS NULL THEN -5 ELSE LENGTH(species) END /*CT.LEN1()*/", + }, + { + "name": "len2_species", + "type": {"typeKind": "INT64"}, + "transformSql": "CASE WHEN species IS NULL THEN 99 ELSE LENGTH(species) END /*CT.LEN2([99])*/", + }, + { + "name": "labelencoded_county", + "type": {"typeKind": "INT64"}, + "transformSql": "ML.LABEL_ENCODER(county, 1000000, 0) OVER()", + }, + { + "name": "labelencoded_species", + "type": {"typeKind": "INT64"}, + "transformSql": "ML.LABEL_ENCODER(species, 1000000, 0) OVER()", + }, + ], + ) + + +@pytest.fixture +def bq_model_merge(mocker): + return create_bq_model_mock( + mocker, + [ + { + "name": "labelencoded_county", + "type": {"typeKind": "INT64"}, + "transformSql": "ML.LABEL_ENCODER(county, 1000000, 0) OVER()", + }, + { + "name": "labelencoded_species", + "type": {"typeKind": "INT64"}, + "transformSql": "ML.LABEL_ENCODER(species, 1000000, 0) OVER()", + }, + ], + ["county", "species"], + ) + + +@pytest.fixture +def bq_model_no_merge(mocker): + return create_bq_model_mock( + mocker, + [ + { + "name": "ident_culmen_length_mm", + "type": {"typeKind": "INT64"}, + "transformSql": "culmen_length_mm /*CT.IDENT()*/", + } + ], + ["culmen_length_mm"], + ) + + +@pytest.fixture +def bq_model_unknown_ML(mocker): + return create_bq_model_mock( + mocker, + [ + { + "name": "unknownml_culmen_length_mm", + "type": {"typeKind": "INT64"}, + "transformSql": "ML.UNKNOWN(culmen_length_mm)", + }, + { + "name": "labelencoded_county", + "type": {"typeKind": "INT64"}, + "transformSql": "ML.LABEL_ENCODER(county, 1000000, 0) OVER()", + }, + ], + ) + + +@pytest.fixture +def bq_model_flexnames(mocker): + return create_bq_model_mock( + mocker, + [ + { + "name": "Flex Name culmen_length_mm", + "type": {"typeKind": "INT64"}, + "transformSql": "culmen_length_mm", + }, + { + "name": "transformed_Culmen Length MM", + "type": {"typeKind": "INT64"}, + "transformSql": "`Culmen Length MM`*/", + }, + # test workaround for bug in get_model + { + "name": "Flex Name flipper_length_mm", + "type": {"typeKind": "INT64"}, + "transformSql": "flipper_length_mm AS `Flex Name flipper_length_mm`", + }, + { + "name": "transformed_Flipper Length MM", + "type": {"typeKind": "INT64"}, + "transformSql": "`Flipper Length MM` AS `transformed_Flipper Length MM`*/", + }, + ], + ) + + +def test_columntransformer_extract_from_bq_model_good(bq_model_good): + col_trans = ColumnTransformer._extract_from_bq_model(bq_model_good) + assert len(col_trans.transformers) == 6 + # normalize the representation for string comparing + col_trans.transformers.sort(key=lambda trafo: str(trafo)) + actual = col_trans.__repr__() + expected = """ColumnTransformer(transformers=[('label_encoder', + LabelEncoder(max_categories=1000001, + min_frequency=0), + 'county'), + ('label_encoder', + LabelEncoder(max_categories=1000001, + min_frequency=0), + 'species'), + ('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='CASE WHEN species IS NULL THEN -5 ELSE LENGTH(species) END /*CT.LEN1()*/', target_column='len1_species'), + '?len1_species'), + ('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='CASE WHEN species IS NULL THEN 99 ELSE LENGTH(species) END /*CT.LEN2([99])*/', target_column='len2_species'), + '?len2_species'), + ('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='culmen_length_mm /*CT.IDENT()*/', target_column='ident_culmen_length_mm'), + '?ident_culmen_length_mm'), + ('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='flipper_length_mm /*CT.IDENT()*/', target_column='ident_flipper_length_mm'), + '?ident_flipper_length_mm')])""" + assert expected == actual + + +def test_columntransformer_extract_from_bq_model_merge(bq_model_merge): + col_trans = ColumnTransformer._extract_from_bq_model(bq_model_merge) + assert isinstance(col_trans, ColumnTransformer) + merged_col_trans = col_trans._merge(bq_model_merge) + assert isinstance(merged_col_trans, preprocessing.LabelEncoder) + assert ( + merged_col_trans.__repr__() + == """LabelEncoder(max_categories=1000001, min_frequency=0)""" + ) + assert merged_col_trans._output_names == [ + "labelencoded_county", + "labelencoded_species", + ] + + +def test_columntransformer_extract_from_bq_model_no_merge(bq_model_no_merge): + col_trans = ColumnTransformer._extract_from_bq_model(bq_model_no_merge) + merged_col_trans = col_trans._merge(bq_model_no_merge) + assert isinstance(merged_col_trans, ColumnTransformer) + expected = """ColumnTransformer(transformers=[('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='culmen_length_mm /*CT.IDENT()*/', target_column='ident_culmen_length_mm'), + '?ident_culmen_length_mm')])""" + actual = merged_col_trans.__repr__() + assert expected == actual + + +def test_columntransformer_extract_from_bq_model_unknown_ML(bq_model_unknown_ML): + try: + _ = ColumnTransformer._extract_from_bq_model(bq_model_unknown_ML) + assert False + except NotImplementedError as e: + assert "Unsupported transformer type" in e.args[0] + + +def test_columntransformer_extract_output_names(bq_model_good): + class BQMLModel(BqmlModel): + def __init__(self, bq_model): + self._model = bq_model + + col_trans = ColumnTransformer._extract_from_bq_model(bq_model_good) + col_trans._bqml_model = BQMLModel(bq_model_good) + col_trans._extract_output_names() + assert col_trans._output_names == [ + "ident_culmen_length_mm", + "ident_flipper_length_mm", + "len1_species", + "len2_species", + "labelencoded_county", + "labelencoded_species", + ] + + +def test_columntransformer_compile_to_sql(mock_X): + ident_transformer = SQLScalarColumnTransformer("{0}", target_column="ident_{0}") + len1_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -2 ELSE LENGTH({0}) END", target_column="len1_{0}" + ) + len2_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN 99 ELSE LENGTH({0}) END", target_column="len2_{0}" + ) + label_transformer = preprocessing.LabelEncoder() + column_transformer = compose.ColumnTransformer( + [ + ( + "ident_trafo", + ident_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), + ("len1_trafo", len1_transformer, ["species"]), + ("len2_trafo", len2_transformer, ["species"]), + ("label", label_transformer, "species"), + ] + ) + sqls = column_transformer._compile_to_sql(mock_X) + assert sqls == [ + "culmen_length_mm AS ident_culmen_length_mm", + "flipper_length_mm AS ident_flipper_length_mm", + "CASE WHEN species IS NULL THEN -2 ELSE LENGTH(species) END AS len1_species", + "CASE WHEN species IS NULL THEN 99 ELSE LENGTH(species) END AS len2_species", + "ML.LABEL_ENCODER(species, 1000000, 0) OVER() AS labelencoded_species", + ] + + +def test_columntransformer_flexible_column_names(mock_X): + ident_transformer = SQLScalarColumnTransformer("{0}", target_column="ident {0}") + len1_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN -2 ELSE LENGTH({0}) END", target_column="len1_{0}" + ) + len2_transformer = SQLScalarColumnTransformer( + "CASE WHEN {0} IS NULL THEN 99 ELSE LENGTH({0}) END", target_column="len2_{0}" + ) + column_transformer = compose.ColumnTransformer( + [ + ( + "ident_trafo", + ident_transformer, + ["culmen_length_mm", "flipper_length_mm"], + ), + ("len1_trafo", len1_transformer, ["species shortname"]), + ("len2_trafo", len2_transformer, ["`species longname`"]), + ] + ) + sqls = column_transformer._compile_to_sql(mock_X) + assert sqls == [ + "culmen_length_mm AS `ident culmen_length_mm`", + "flipper_length_mm AS `ident flipper_length_mm`", + "CASE WHEN `species shortname` IS NULL THEN -2 ELSE LENGTH(`species shortname`) END AS `len1_species shortname`", + "CASE WHEN `species longname` IS NULL THEN 99 ELSE LENGTH(`species longname`) END AS `len2_species longname`", + ] + + +def test_columntransformer_extract_from_bq_model_flexnames(bq_model_flexnames): + col_trans = ColumnTransformer._extract_from_bq_model(bq_model_flexnames) + assert len(col_trans.transformers) == 4 + # normalize the representation for string comparing + col_trans.transformers.sort(key=lambda trafo: str(trafo)) + actual = col_trans.__repr__() + expected = """ColumnTransformer(transformers=[('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='`Culmen Length MM`*/', target_column='transformed_Culmen Length MM'), + '?transformed_Culmen Length MM'), + ('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='`Flipper Length MM` AS `transformed_Flipper Length MM`*/', target_column='transformed_Flipper Length MM'), + '?transformed_Flipper Length MM'), + ('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='culmen_length_mm', target_column='Flex Name culmen_length_mm'), + '?Flex Name culmen_length_mm'), + ('sql_scalar_column_transformer', + SQLScalarColumnTransformer(sql='flipper_length_mm ', target_column='Flex Name flipper_length_mm'), + '?Flex Name flipper_length_mm')])""" + assert expected == actual From 2fe5e48c56bbc359d3769824c83745d65a001dd7 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:52:24 -0700 Subject: [PATCH 858/921] feat: allow multiple columns input for llm models (#998) * feat: allow multiple columns input for llm models * fix * add tests * resolve comments --- bigframes/ml/llm.py | 70 +++++++++++------------------ tests/system/load/test_llm.py | 24 ++++++++++ tests/system/small/ml/test_llm.py | 74 ++++++++++++++++++++++++++++--- tests/system/utils.py | 6 +++ 4 files changed, 124 insertions(+), 50 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 53a9d40c6e5..3d11cd123e7 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -244,7 +244,7 @@ def predict( Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame or Series, which contains only one column of prompts. + Input DataFrame or Series, can contain one or more columns. If multiple columns are in the DataFrame, it must contain a "prompt" column for prediction. Prompts can include preamble, questions, suggestions, instructions, or examples. temperature (float, default 0.0): @@ -307,14 +307,10 @@ def predict( (X,) = utils.convert_to_dataframe(X) - if len(X.columns) != 1: - raise ValueError( - f"Only support one column as input. {constants.FEEDBACK_LINK}" - ) - - # BQML identified the column by name - col_label = cast(blocks.Label, X.columns[0]) - X = X.rename(columns={col_label: "prompt"}) + if len(X.columns) == 1: + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "prompt"}) options = { "temperature": temperature, @@ -522,7 +518,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame, which needs to contain a column with name "content". Only the column will be used as input. Content can include preamble, questions, suggestions, instructions, or examples. + Input DataFrame or Series, can contain one or more columns. If multiple columns are in the DataFrame, it must contain a "content" column for prediction. Returns: bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. @@ -531,14 +527,10 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models (X,) = utils.convert_to_dataframe(X) - if len(X.columns) != 1: - raise ValueError( - f"Only support one column as input. {constants.FEEDBACK_LINK}" - ) - - # BQML identified the column by name - col_label = cast(blocks.Label, X.columns[0]) - X = X.rename(columns={col_label: "content"}) + if len(X.columns) == 1: + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "content"}) options = { "flatten_json_output": True, @@ -679,7 +671,7 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame, which needs to contain a column with name "content". Only the column will be used as input. Content can include preamble, questions, suggestions, instructions, or examples. + Input DataFrame or Series, can contain one or more columns. If multiple columns are in the DataFrame, it must contain a "content" column for prediction. Returns: bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. @@ -688,14 +680,10 @@ def predict(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: # Params reference: https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models (X,) = utils.convert_to_dataframe(X) - if len(X.columns) != 1: - raise ValueError( - f"Only support one column as input. {constants.FEEDBACK_LINK}" - ) - - # BQML identified the column by name - col_label = cast(blocks.Label, X.columns[0]) - X = X.rename(columns={col_label: "content"}) + if len(X.columns) == 1: + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "content"}) options = { "flatten_json_output": True, @@ -893,7 +881,7 @@ def predict( Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame or Series, which contains only one column of prompts. + Input DataFrame or Series, can contain one or more columns. If multiple columns are in the DataFrame, it must contain a "prompt" column for prediction. Prompts can include preamble, questions, suggestions, instructions, or examples. temperature (float, default 0.9): @@ -938,14 +926,10 @@ def predict( (X,) = utils.convert_to_dataframe(X) - if len(X.columns) != 1: - raise ValueError( - f"Only support one column as input. {constants.FEEDBACK_LINK}" - ) - - # BQML identified the column by name - col_label = cast(blocks.Label, X.columns[0]) - X = X.rename(columns={col_label: "prompt"}) + if len(X.columns) == 1: + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "prompt"}) options = { "temperature": temperature, @@ -1181,7 +1165,7 @@ def predict( Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series): - Input DataFrame or Series, which contains only one column of prompts. + Input DataFrame or Series, can contain one or more columns. If multiple columns are in the DataFrame, it must contain a "prompt" column for prediction. Prompts can include preamble, questions, suggestions, instructions, or examples. max_output_tokens (int, default 128): @@ -1222,14 +1206,10 @@ def predict( (X,) = utils.convert_to_dataframe(X) - if len(X.columns) != 1: - raise ValueError( - f"Only support one column as input. {constants.FEEDBACK_LINK}" - ) - - # BQML identified the column by name - col_label = cast(blocks.Label, X.columns[0]) - X = X.rename(columns={col_label: "prompt"}) + if len(X.columns) == 1: + # BQML identified the column by name + col_label = cast(blocks.Label, X.columns[0]) + X = X.rename(columns={col_label: "prompt"}) options = { "max_output_tokens": max_output_tokens, diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index 1d133001151..51b45485ade 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -156,3 +156,27 @@ def test_claude3_text_generator_predict_with_params_success( utils.check_pandas_df_schema_and_index( df, columns=utils.ML_GENERATE_TEXT_OUTPUT, index=3, col_exact=False ) + + +@pytest.mark.parametrize( + "model_name", + ("claude-3-sonnet", "claude-3-haiku", "claude-3-5-sonnet", "claude-3-opus"), +) +@pytest.mark.flaky(retries=3, delay=120) +def test_claude3_text_generator_predict_multi_col_success( + llm_text_df, model_name, session, session_us_east5, bq_connection +): + if model_name in ("claude-3-5-sonnet", "claude-3-opus"): + session = session_us_east5 + + llm_text_df["additional_col"] = 1 + claude3_text_generator_model = llm.Claude3TextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + df = claude3_text_generator_model.predict(llm_text_df).to_pandas() + utils.check_pandas_df_schema_and_index( + df, + columns=utils.ML_GENERATE_TEXT_OUTPUT + ["additional_col"], + index=3, + col_exact=False, + ) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 3093a36534b..a4a09731a18 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -15,6 +15,7 @@ import pytest from bigframes.ml import llm +import bigframes.pandas as bpd from tests.system import utils @@ -166,6 +167,20 @@ def test_text_generator_predict_arbitrary_col_label_success( ) +@pytest.mark.flaky(retries=2) +def test_text_generator_predict_multiple_cols_success( + palm2_text_generator_model, llm_text_df: bpd.DataFrame +): + df = llm_text_df.assign(additional_col=1) + pd_df = palm2_text_generator_model.predict(df).to_pandas() + utils.check_pandas_df_schema_and_index( + pd_df, + columns=utils.ML_GENERATE_TEXT_OUTPUT + ["additional_col"], + index=3, + col_exact=False, + ) + + @pytest.mark.flaky(retries=2) def test_text_generator_predict_with_params_success( palm2_text_generator_model, llm_text_df @@ -212,11 +227,33 @@ def test_text_embedding_generator_predict_default_params_success( model_name=model_name, connection_name=bq_connection, session=session ) df = text_embedding_model.predict(llm_text_df).to_pandas() - assert df.shape == (3, 4) - assert "ml_generate_embedding_result" in df.columns - series = df["ml_generate_embedding_result"] - value = series[0] - assert len(value) == 768 + utils.check_pandas_df_schema_and_index( + df, columns=utils.ML_GENERATE_EMBEDDING_OUTPUT, index=3, col_exact=False + ) + assert len(df["ml_generate_embedding_result"][0]) == 768 + + +@pytest.mark.parametrize( + "model_name", + ("text-embedding-004", "text-multilingual-embedding-002"), +) +@pytest.mark.flaky(retries=2) +def test_text_embedding_generator_multi_cols_predict_success( + llm_text_df: bpd.DataFrame, model_name, session, bq_connection +): + df = llm_text_df.assign(additional_col=1) + df = df.rename(columns={"prompt": "content"}) + text_embedding_model = llm.TextEmbeddingGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + pd_df = text_embedding_model.predict(df).to_pandas() + utils.check_pandas_df_schema_and_index( + pd_df, + columns=utils.ML_GENERATE_EMBEDDING_OUTPUT + ["additional_col"], + index=3, + col_exact=False, + ) + assert len(pd_df["ml_generate_embedding_result"][0]) == 768 @pytest.mark.parametrize( @@ -295,6 +332,33 @@ def test_gemini_text_generator_predict_with_params_success( ) +@pytest.mark.parametrize( + "model_name", + ( + "gemini-pro", + "gemini-1.5-pro-preview-0514", + "gemini-1.5-flash-preview-0514", + "gemini-1.5-pro-001", + "gemini-1.5-flash-001", + ), +) +@pytest.mark.flaky(retries=2) +def test_gemini_text_generator_multi_cols_predict_success( + llm_text_df: bpd.DataFrame, model_name, session, bq_connection +): + df = llm_text_df.assign(additional_col=1) + gemini_text_generator_model = llm.GeminiTextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + pd_df = gemini_text_generator_model.predict(df).to_pandas() + utils.check_pandas_df_schema_and_index( + pd_df, + columns=utils.ML_GENERATE_TEXT_OUTPUT + ["additional_col"], + index=3, + col_exact=False, + ) + + @pytest.mark.flaky(retries=2) def test_llm_palm_score(llm_fine_tune_df_default_index): model = llm.PaLM2TextGenerator(model_name="text-bison") diff --git a/tests/system/utils.py b/tests/system/utils.py index 26e3e97e245..83d0e683bc3 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -50,6 +50,12 @@ "ml_generate_text_status", "prompt", ] +ML_GENERATE_EMBEDDING_OUTPUT = [ + "ml_generate_embedding_result", + "ml_generate_embedding_statistics", + "ml_generate_embedding_status", + "content", +] def skip_legacy_pandas(test): From 3263bd70cff01bc18f1ae4ac3d5aa7f9d70fd4b7 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:52:17 -0700 Subject: [PATCH 859/921] feat: add bigframes.bigquery.approx_top_count (#1010) * feat: add bigframes.bigquery.approx_top_count * fix docs --- bigframes/bigquery/__init__.py | 40 ++++++++++ bigframes/core/compile/aggregate_compiler.py | 32 ++++++++ bigframes/operations/aggregations.py | 17 +++++ .../system/small/bigquery/test_approx_agg.py | 76 +++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 tests/system/small/bigquery/test_approx_agg.py diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 303120b88a7..28a818e7092 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -272,6 +272,46 @@ def json_extract_array( return series._apply_unary_op(ops.JSONExtractArray(json_path=json_path)) +# Approximate aggrgate functions defined from +# https://cloud.google.com/bigquery/docs/reference/standard-sql/approximate_aggregate_functions + + +def approx_top_count( + series: series.Series, + number: int, +) -> series.Series: + """Returns the approximate top elements of `expression` as an array of STRUCTs. + The number parameter specifies the number of elements returned. + + Each `STRUCT` contains two fields. The first field (named `value`) contains an input + value. The second field (named `count`) contains an `INT64` specifying the number + of times the value was returned. + + Returns `NULL` if there are zero input rows. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series(["apple", "apple", "pear", "pear", "pear", "banana"]) + >>> bbq.approx_top_count(s, number=2) + [{'value': 'pear', 'count': 3}, {'value': 'apple', 'count': 2}] + + Args: + series (bigframes.series.Series): + The Series with any data type that the `GROUP BY` clause supports. + number (int): + An integer specifying the number of times the value was returned. + + Returns: + bigframes.series.Series: A new Series with the result data. + """ + if number < 1: + raise ValueError("The number of approx_top_count must be at least 1") + return series._apply_aggregation(agg_ops.ApproxTopCountOp(number=number)) + + def struct(value: dataframe.DataFrame) -> series.Series: """Takes a DataFrame and converts it into a Series of structs with each struct entry corresponding to a DataFrame row and each struct field diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/aggregate_compiler.py index 91a3045efb6..b65953934dd 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/aggregate_compiler.py @@ -11,6 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +from __future__ import annotations + import functools import typing from typing import cast, List, Optional @@ -19,6 +22,7 @@ import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops import ibis import ibis.expr.datatypes as ibis_dtypes +import ibis.expr.operations as ibis_ops import ibis.expr.types as ibis_types import pandas as pd @@ -196,6 +200,34 @@ def _( return cast(ibis_types.NumericValue, value) +@compile_unary_agg.register +def _( + op: agg_ops.ApproxTopCountOp, + column: ibis_types.Column, + window=None, +) -> ibis_types.ArrayColumn: + # APPROX_TOP_COUNT has very few allowed windows. + if window is not None: + raise NotImplementedError( + f"Approx top count with windowing is not supported. {constants.FEEDBACK_LINK}" + ) + + # Define a user-defined function (UDF) that approximates the top counts of an expression. + # The type of value is dynamically matching the input column. + def approx_top_count(expression, number: ibis_dtypes.int64): # type: ignore + ... + + return_type = ibis_dtypes.Array( + ibis_dtypes.Struct.from_tuples( + [("value", column.type()), ("count", ibis_dtypes.int64)] + ) + ) + approx_top_count.__annotations__["return"] = return_type + udf_op = ibis_ops.udf.agg.builtin(approx_top_count) + + return udf_op(expression=column, number=op.number) + + @compile_unary_agg.register @numeric_op def _( diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index d071889ac46..faba7465d99 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -184,6 +184,23 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT return input_types[0] +@dataclasses.dataclass(frozen=True) +class ApproxTopCountOp(UnaryAggregateOp): + name: typing.ClassVar[str] = "approx_top_count" + number: int + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + if not dtypes.is_orderable(input_types[0]): + raise TypeError(f"Type {input_types[0]} is not orderable") + + input_type = input_types[0] + fields = [ + pa.field("value", dtypes.bigframes_dtype_to_arrow_dtype(input_type)), + pa.field("count", pa.int64()), + ] + return pd.ArrowDtype(pa.list_(pa.struct(fields))) + + @dataclasses.dataclass(frozen=True) class MeanOp(UnaryAggregateOp): name: ClassVar[str] = "mean" diff --git a/tests/system/small/bigquery/test_approx_agg.py b/tests/system/small/bigquery/test_approx_agg.py new file mode 100644 index 00000000000..c88f5850f80 --- /dev/null +++ b/tests/system/small/bigquery/test_approx_agg.py @@ -0,0 +1,76 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.bigquery as bbq +import bigframes.pandas as bpd + + +@pytest.mark.parametrize( + ("data", "expected"), + [ + pytest.param( + [1, 2, 3, 3, 2], [{"value": 3, "count": 2}, {"value": 2, "count": 2}] + ), + pytest.param( + ["apple", "apple", "pear", "pear", "pear", "banana"], + [{"value": "pear", "count": 3}, {"value": "apple", "count": 2}], + ), + pytest.param( + [True, False, True, False, True], + [{"value": True, "count": 3}, {"value": False, "count": 2}], + ), + pytest.param( + [], + [], + ), + pytest.param( + [[1, 2], [1], [1, 2]], + [], + marks=pytest.mark.xfail(raises=TypeError), + ), + ], + ids=["int64", "string", "bool", "null", "array"], +) +def test_approx_top_count_w_dtypes(data, expected): + s = bpd.Series(data) + result = bbq.approx_top_count(s, number=2) + assert result == expected + + +@pytest.mark.parametrize( + ("number", "expected"), + [ + pytest.param( + 0, + [], + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param(1, [{"value": 3, "count": 2}]), + pytest.param( + 4, + [ + {"value": 3, "count": 2}, + {"value": 2, "count": 2}, + {"value": 1, "count": 1}, + ], + ), + ], + ids=["zero", "one", "full"], +) +def test_approx_top_count_w_numbers(number, expected): + s = bpd.Series([1, 2, 3, 3, 2]) + result = bbq.approx_top_count(s, number=number) + assert result == expected From 7aaef6fdd0466ac7bcf066d0dd74d0dd9b1c3b4d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 07:33:17 +0000 Subject: [PATCH 860/921] chore(main): release 1.20.0 (#1017) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ bigframes/version.py | 2 +- third_party/bigframes_vendored/version.py | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a99e0ecd914..a0539af01e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.20.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.19.0...v1.20.0) (2024-09-25) + + +### Features + +* Add bigframes.bigquery.approx_top_count ([#1010](https://github.com/googleapis/python-bigquery-dataframes/issues/1010)) ([3263bd7](https://github.com/googleapis/python-bigquery-dataframes/commit/3263bd70cff01bc18f1ae4ac3d5aa7f9d70fd4b7)) +* Add bigframes.ml.compose.SQLScalarColumnTransformer to create custom SQL-based transformations ([#955](https://github.com/googleapis/python-bigquery-dataframes/issues/955)) ([1930b4e](https://github.com/googleapis/python-bigquery-dataframes/commit/1930b4efe60295751ceef89c2a824923a35b19af)) +* Allow multiple columns input for llm models ([#998](https://github.com/googleapis/python-bigquery-dataframes/issues/998)) ([2fe5e48](https://github.com/googleapis/python-bigquery-dataframes/commit/2fe5e48c56bbc359d3769824c83745d65a001dd7)) + + +### Bug Fixes + +* Fix __repr__ caching with partial ordering ([#1016](https://github.com/googleapis/python-bigquery-dataframes/issues/1016)) ([208a984](https://github.com/googleapis/python-bigquery-dataframes/commit/208a98475389f59d4e32e0cfbcc46824cac278a6)) + + +### Documentation + +* Limit pypi notebook to 7 days and add more info about differences with partial ordering mode ([#1013](https://github.com/googleapis/python-bigquery-dataframes/issues/1013)) ([3c54399](https://github.com/googleapis/python-bigquery-dataframes/commit/3c543990297ec3be0e30425ee841546217e26d2a)) +* Move and edit existing linear-regression tutorial snippet ([#991](https://github.com/googleapis/python-bigquery-dataframes/issues/991)) ([4cb62fd](https://github.com/googleapis/python-bigquery-dataframes/commit/4cb62fd74fc1ac3bb21da23b8639464a9ae3525d)) + ## [1.19.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.18.0...v1.19.0) (2024-09-24) diff --git a/bigframes/version.py b/bigframes/version.py index 5dda345fcb8..60f49421756 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0" +__version__ = "1.20.0" diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index 5dda345fcb8..60f49421756 100644 --- a/third_party/bigframes_vendored/version.py +++ b/third_party/bigframes_vendored/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.19.0" +__version__ = "1.20.0" From 1604139dd8b113c6327021b319946fdf829ce3ef Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 26 Sep 2024 20:08:46 +0000 Subject: [PATCH 861/921] chore: run doctest and notebook tests in bigframes-testing project (#976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: target bigframes-testing project for doctest and notebook tests * skip the axis=1 multi index test temporarily * regionalized notebook to honor GOOGLE_CLOUD_PROJECT * temporarily disable southamerica-west1 for regionalization testing This is to confirm that only southamerica-west1 has issue running notebook test * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * restore southameria-west1 for regionalization testing --------- Co-authored-by: Owl Bot --- .kokoro/continuous/doctest.cfg | 7 +------ .kokoro/continuous/notebook.cfg | 7 +------ .kokoro/presubmit/doctest.cfg | 7 +------ .kokoro/presubmit/notebook.cfg | 7 +------ notebooks/location/regionalized.ipynb | 26 +++++++++++++++----------- scripts/setup-project-for-testing.sh | 3 +++ 6 files changed, 22 insertions(+), 35 deletions(-) diff --git a/.kokoro/continuous/doctest.cfg b/.kokoro/continuous/doctest.cfg index dfdc78782fe..dca21d43fd7 100644 --- a/.kokoro/continuous/doctest.cfg +++ b/.kokoro/continuous/doctest.cfg @@ -8,10 +8,5 @@ env_vars: { env_vars: { key: "GOOGLE_CLOUD_PROJECT" - value: "bigframes-load-testing" -} - -env_vars: { - key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" - value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" + value: "bigframes-testing" } diff --git a/.kokoro/continuous/notebook.cfg b/.kokoro/continuous/notebook.cfg index ca3d98b58be..c14297019a1 100644 --- a/.kokoro/continuous/notebook.cfg +++ b/.kokoro/continuous/notebook.cfg @@ -13,10 +13,5 @@ env_vars: { env_vars: { key: "GOOGLE_CLOUD_PROJECT" - value: "bigframes-load-testing" -} - -env_vars: { - key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" - value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" + value: "bigframes-testing" } diff --git a/.kokoro/presubmit/doctest.cfg b/.kokoro/presubmit/doctest.cfg index dfdc78782fe..dca21d43fd7 100644 --- a/.kokoro/presubmit/doctest.cfg +++ b/.kokoro/presubmit/doctest.cfg @@ -8,10 +8,5 @@ env_vars: { env_vars: { key: "GOOGLE_CLOUD_PROJECT" - value: "bigframes-load-testing" -} - -env_vars: { - key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" - value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" + value: "bigframes-testing" } diff --git a/.kokoro/presubmit/notebook.cfg b/.kokoro/presubmit/notebook.cfg index 94e2a3c686e..cc73c3bea40 100644 --- a/.kokoro/presubmit/notebook.cfg +++ b/.kokoro/presubmit/notebook.cfg @@ -8,10 +8,5 @@ env_vars: { env_vars: { key: "GOOGLE_CLOUD_PROJECT" - value: "bigframes-load-testing" -} - -env_vars: { - key: "BIGFRAMES_TEST_MODEL_VERTEX_ENDPOINT" - value: "https://us-central1-aiplatform.googleapis.com/v1/projects/272725758477/locations/us-central1/endpoints/590545496255234048" + value: "bigframes-testing" } diff --git a/notebooks/location/regionalized.ipynb b/notebooks/location/regionalized.ipynb index c05d27c24e8..c383a226097 100644 --- a/notebooks/location/regionalized.ipynb +++ b/notebooks/location/regionalized.ipynb @@ -47,32 +47,36 @@ ], "source": [ "# Take multi-region US as the default BQ location, where most of the BQ data lies including the BQ public datasets\n", - "BQ_LOCATION = \"us\"\n", - "PROJECT = \"bigframes-dev\"\n", + "import os\n", + "\n", + "PROJECT_ID = os.environ.get(\"GOOGLE_CLOUD_PROJECT\")\n", + "BQ_LOCATION = os.environ.get(\"BIGQUERY_LOCATION\")\n", + "\n", + "if not PROJECT_ID:\n", + " raise ValueError(\"Project must be set via environment variable GOOGLE_CLOUD_PROJECT\")\n", + "if not BQ_LOCATION:\n", + " raise ValueError(\"BQ location must be set via environment variable BIGQUERY_LOCATION\")\n", + "\n", "DATASET = \"bigframes_testing\"\n", "PENGUINS_TABLE = \"bigquery-public-data.ml_datasets.penguins\"\n", "\n", "\n", "# Check for a location set in the environment and do location-specific setup if needed\n", "\n", - "import os\n", "import google.api_core.exceptions\n", "from google.cloud import bigquery\n", "import bigframes\n", - " \n", - "env_bq_location = os.getenv(\"BIGQUERY_LOCATION\")\n", - "if env_bq_location and env_bq_location != BQ_LOCATION:\n", - " BQ_LOCATION = env_bq_location.lower()\n", "\n", "client = bigquery.Client()\n", "\n", + "BQ_LOCATION = BQ_LOCATION.lower()\n", "if BQ_LOCATION != \"us\":\n", " bq_location_normalized = BQ_LOCATION.replace('-', '_')\n", "\n", " # Nominate a local penguins table\n", " penguins_table_ref = bigquery.TableReference.from_string(PENGUINS_TABLE)\n", " penguins_local_dataset_name = f\"{DATASET}_{bq_location_normalized}\"\n", - " penguins_local_dataset_ref = bigquery.DatasetReference(project=PROJECT, dataset_id=penguins_local_dataset_name)\n", + " penguins_local_dataset_ref = bigquery.DatasetReference(project=PROJECT_ID, dataset_id=penguins_local_dataset_name)\n", " penguins_local_dataset = bigquery.Dataset(penguins_local_dataset_ref)\n", " penguins_local_dataset.location = BQ_LOCATION\n", " penguins_local_table_ref= bigquery.TableReference(penguins_local_dataset, penguins_table_ref.table_id)\n", @@ -94,13 +98,13 @@ " DATASET = f\"{DATASET}_{bq_location_normalized}\"\n", "\n", "# Create the dataset to store the model if it doesn't exist \n", - "model_local_dataset = bigquery.Dataset(bigquery.DatasetReference(project=PROJECT, dataset_id=DATASET))\n", + "model_local_dataset = bigquery.Dataset(bigquery.DatasetReference(project=PROJECT_ID, dataset_id=DATASET))\n", "model_local_dataset.location = BQ_LOCATION\n", "model_dataset = client.create_dataset(model_local_dataset, exists_ok=True)\n", "\n", "# Finally log the variables driving the core notebook execution\n", "log = ('\\n'.join(f\"{name}: {str(value)}\" for name, value in {\n", - " \"BigQuery project\" : PROJECT,\n", + " \"BigQuery project\" : PROJECT_ID,\n", " \"BigQuery location\" : BQ_LOCATION,\n", " \"Penguins Table\" : PENGUINS_TABLE,\n", " \"ML Model Dataset\" : model_dataset.reference\n", @@ -134,7 +138,7 @@ "\n", "# Note: The project option is not required in all environments.\n", "# On BigQuery Studio, the project ID is automatically detected.\n", - "bigframes.pandas.options.bigquery.project = PROJECT\n", + "bigframes.pandas.options.bigquery.project = PROJECT_ID\n", "\n", "# Note: The location option is not required.\n", "# It defaults to the location of the first table or query\n", diff --git a/scripts/setup-project-for-testing.sh b/scripts/setup-project-for-testing.sh index a160784c120..df9cea46a4b 100755 --- a/scripts/setup-project-for-testing.sh +++ b/scripts/setup-project-for-testing.sh @@ -57,12 +57,14 @@ function log_and_execute() { ################################################################################ function enable_apis() { for service in aiplatform.googleapis.com \ + artifactregistry.googleapis.com \ bigquery.googleapis.com \ bigqueryconnection.googleapis.com \ bigquerystorage.googleapis.com \ cloudbuild.googleapis.com \ cloudfunctions.googleapis.com \ cloudresourcemanager.googleapis.com \ + compute.googleapis.com \ run.googleapis.com \ ; do log_and_execute gcloud --project=$PROJECT_ID services enable $service @@ -148,6 +150,7 @@ function ensure_bq_connections_with_iam() { southamerica-west1 \ us \ us-central1 \ + us-east5 \ ; do ensure_bq_connection_with_iam "$location" "$BIGFRAMES_RF_CONNECTION_NAME" done From 1a380631f793f82637cd384601956ee4457dc58a Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:23:48 -0700 Subject: [PATCH 862/921] feat: add ml.model_selection.cross_validate support (#1020) * feat: add ml.model_selection.cross_validate support * use time.perf_count --- bigframes/ml/model_selection.py | 38 ++++++++++- tests/system/large/ml/test_model_selection.py | 64 +++++++++++++++++++ .../sklearn/model_selection/_validation.py | 46 +++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 tests/system/large/ml/test_model_selection.py create mode 100644 third_party/bigframes_vendored/sklearn/model_selection/_validation.py diff --git a/bigframes/ml/model_selection.py b/bigframes/ml/model_selection.py index e4c41b2a399..f9d7e6cf734 100644 --- a/bigframes/ml/model_selection.py +++ b/bigframes/ml/model_selection.py @@ -18,9 +18,11 @@ import inspect -from typing import cast, Generator, List, Union +import time +from typing import cast, Generator, List, Optional, Union import bigframes_vendored.sklearn.model_selection._split as vendored_model_selection_split +import bigframes_vendored.sklearn.model_selection._validation as vendored_model_selection_validation from bigframes.core import log_adapter from bigframes.ml import utils @@ -147,3 +149,37 @@ def split( yield utils.convert_to_types( [X_train, X_test, y_train, y_test], [X, X, y, y] ) + + +def cross_validate( + estimator, + X: Union[bpd.DataFrame, bpd.Series], + y: Union[bpd.DataFrame, bpd.Series, None] = None, + *, + cv: Optional[Union[int, KFold]] = None, +) -> dict[str, list]: + if cv is None: + cv = KFold(n_splits=5) + elif isinstance(cv, int): + cv = KFold(n_splits=cv) + + result: dict[str, list] = {"test_score": [], "fit_time": [], "score_time": []} + for X_train, X_test, y_train, y_test in cv.split(X, y): # type: ignore + fit_start_time = time.perf_counter() + estimator.fit(X_train, y_train) + fit_time = time.perf_counter() - fit_start_time + + score_start_time = time.perf_counter() + score = estimator.score(X_test, y_test) + score_time = time.perf_counter() - score_start_time + + result["test_score"].append(score) + result["fit_time"].append(fit_time) + result["score_time"].append(score_time) + + return result + + +cross_validate.__doc__ = inspect.getdoc( + vendored_model_selection_validation.cross_validate +) diff --git a/tests/system/large/ml/test_model_selection.py b/tests/system/large/ml/test_model_selection.py new file mode 100644 index 00000000000..c1856a1537b --- /dev/null +++ b/tests/system/large/ml/test_model_selection.py @@ -0,0 +1,64 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from bigframes.ml import linear_model, model_selection +from tests.system import utils + + +@pytest.mark.parametrize( + ("cv", "n_fold"), + ( + pytest.param( + None, + 5, + ), + pytest.param( + 4, + 4, + ), + pytest.param( + model_selection.KFold(3), + 3, + ), + ), +) +def test_cross_validate(penguins_df_default_index, cv, n_fold): + model = linear_model.LinearRegression() + df = penguins_df_default_index.dropna() + X = df[ + [ + "species", + "island", + "culmen_length_mm", + ] + ] + y = df["body_mass_g"] + + cv_results = model_selection.cross_validate(model, X, y, cv=cv) + + assert "test_score" in cv_results + assert "fit_time" in cv_results + assert "score_time" in cv_results + + assert len(cv_results["test_score"]) == n_fold + assert len(cv_results["fit_time"]) == n_fold + assert len(cv_results["score_time"]) == n_fold + + utils.check_pandas_df_schema_and_index( + cv_results["test_score"][0].to_pandas(), + columns=utils.ML_REGRESSION_METRICS, + index=1, + ) diff --git a/third_party/bigframes_vendored/sklearn/model_selection/_validation.py b/third_party/bigframes_vendored/sklearn/model_selection/_validation.py new file mode 100644 index 00000000000..43e155da7d3 --- /dev/null +++ b/third_party/bigframes_vendored/sklearn/model_selection/_validation.py @@ -0,0 +1,46 @@ +""" +The :mod:`sklearn.model_selection._validation` module includes classes and +functions to validate the model. +""" + +# Author: Alexandre Gramfort +# Gael Varoquaux +# Olivier Grisel +# Raghav RV +# Michal Karbownik +# License: BSD 3 clause + + +def cross_validate(estimator, X, y=None, *, cv=None): + """Evaluate metric(s) by cross-validation and also record fit/score times. + + Args: + estimator: + bigframes.ml model that implements fit(). + The object to use to fit the data. + + X (bigframes.dataframe.DataFrame or bigframes.series.Series): + The data to fit. + + y (bigframes.dataframe.DataFrame, bigframes.series.Series or None): + The target variable to try to predict in the case of supe()rvised learning. Default to None. + + cv (int, bigframes.ml.model_selection.KFold or None): + Determines the cross-validation splitting strategy. + Possible inputs for cv are: + + - None, to use the default 5-fold cross validation, + - int, to specify the number of folds in a `KFold`, + - bigframes.ml.model_selection.KFold instance. + + Returns: + Dict[str, List]: A dict of arrays containing the score/time arrays for each scorer is returned. The keys for this ``dict`` are: + + ``test_score`` + The score array for test scores on each cv split. + ``fit_time`` + The time for fitting the estimator on the train + set for each cv split. + ``score_time`` + The time for scoring the estimator on the test set for each + cv split.""" From c89e92e400bf966d5b878d25e6a2228c293d7b9b Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 26 Sep 2024 14:26:18 -0700 Subject: [PATCH 863/921] refactor: Simplify query executor interface (#1015) --- bigframes/core/blocks.py | 152 ++++++++----------- bigframes/core/compile/ibis_types.py | 6 +- bigframes/core/schema.py | 22 +++ bigframes/dataframe.py | 12 +- bigframes/dtypes.py | 25 +++- bigframes/formatting_helpers.py | 15 +- bigframes/ml/core.py | 12 +- bigframes/operations/__init__.py | 6 +- bigframes/operations/aggregations.py | 4 +- bigframes/series.py | 8 +- bigframes/session/__init__.py | 60 +------- bigframes/session/_io/bigquery/__init__.py | 2 +- bigframes/session/_io/pandas.py | 13 +- bigframes/session/executor.py | 162 ++++++++++++++++++--- tests/system/small/test_dataframe.py | 22 ++- tests/system/small/test_remote_function.py | 1 - tests/system/small/test_session.py | 12 +- tests/unit/session/test_io_pandas.py | 19 ++- 18 files changed, 337 insertions(+), 216 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 7f6f5f1cc9e..4bc87233b0d 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -25,11 +25,20 @@ import dataclasses import functools import itertools -import os import random import textwrap import typing -from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple, Union +from typing import ( + Iterable, + List, + Literal, + Mapping, + Optional, + Sequence, + Tuple, + TYPE_CHECKING, + Union, +) import warnings import bigframes_vendored.constants as constants @@ -56,7 +65,10 @@ import bigframes.features import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops -import bigframes.session._io.pandas +import bigframes.session._io.pandas as io_pandas + +if TYPE_CHECKING: + import bigframes.session.executor # Type constraint for wherever column labels are used Label = typing.Hashable @@ -450,46 +462,14 @@ def reorder_levels(self, ids: typing.Sequence[str]): level_names = [self.col_id_to_index_name[index_id] for index_id in ids] return Block(self.expr, ids, self.column_labels, level_names) - def _to_dataframe(self, result) -> pd.DataFrame: - """Convert BigQuery data to pandas DataFrame with specific dtypes.""" - result_dataframe = self.session._rows_to_dataframe(result) - # Runs strict validations to ensure internal type predictions and ibis are completely in sync - # Do not execute these validations outside of testing suite. - if "PYTEST_CURRENT_TEST" in os.environ: - self._validate_result_schema(result.schema) - return result_dataframe - - def _validate_result_schema( - self, bq_result_schema: list[bigquery.schema.SchemaField] - ): - actual_schema = tuple(bq_result_schema) - ibis_schema = self.expr._compiled_schema - internal_schema = self.expr.schema - if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: - return - if internal_schema.to_bigquery() != actual_schema: - raise ValueError( - f"This error should only occur while testing. BigFrames internal schema: {internal_schema.to_bigquery()} does not match actual schema: {actual_schema}" - ) - if ibis_schema.to_bigquery() != actual_schema: - raise ValueError( - f"This error should only occur while testing. Ibis schema: {ibis_schema.to_bigquery()} does not match actual schema: {actual_schema}" - ) - def to_arrow( self, *, ordered: bool = True, ) -> Tuple[pa.Table, bigquery.QueryJob]: """Run query and download results as a pyarrow Table.""" - # pa.Table.from_pandas puts index columns last, so update the expression to match. - expr = self.expr.select_columns( - list(self.value_columns) + list(self.index_columns) - ) - - _, query_job = self.session._execute(expr, ordered=ordered) - results_iterator = query_job.result() - pa_table = results_iterator.to_arrow() + execute_result = self.session._executor.execute(self.expr, ordered=ordered) + pa_table = execute_result.to_arrow_table() pa_index_labels = [] for index_level, index_label in enumerate(self._index_labels): @@ -498,8 +478,10 @@ def to_arrow( else: pa_index_labels.append(f"__index_level_{index_level}__") + # pa.Table.from_pandas puts index columns last, so update to match. + pa_table = pa_table.select([*self.value_columns, *self.index_columns]) pa_table = pa_table.rename_columns(list(self.column_labels) + pa_index_labels) - return pa_table, query_job + return pa_table, execute_result.query_job def to_pandas( self, @@ -508,7 +490,7 @@ def to_pandas( random_state: Optional[int] = None, *, ordered: bool = True, - ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: + ) -> Tuple[pd.DataFrame, Optional[bigquery.QueryJob]]: """Run query and download results as a pandas DataFrame. Args: @@ -560,8 +542,8 @@ def try_peek( self, n: int = 20, force: bool = False ) -> typing.Optional[pd.DataFrame]: if force or self.expr.supports_fast_peek: - iterator, _ = self.session._peek(self.expr, n) - df = self._to_dataframe(iterator) + result = self.session._executor.peek(self.expr, n) + df = io_pandas.arrow_to_pandas(result.to_arrow_table(), self.expr.schema) self._copy_index_to_pandas(df) return df else: @@ -574,18 +556,15 @@ def to_pandas_batches( page_size and max_results determine the size and number of batches, see https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJob#google_cloud_bigquery_job_QueryJob_result""" - dtypes = dict(zip(self.index_columns, self.index.dtypes)) - dtypes.update(zip(self.value_columns, self.dtypes)) - _, query_job = self.session._executor.execute( - self.expr, ordered=True, use_explicit_destination=True - ) - results_iterator = query_job.result( - page_size=page_size, max_results=max_results - ) - for arrow_table in results_iterator.to_arrow_iterable( - bqstorage_client=self.session.bqstoragereadclient - ): - df = bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) + execute_result = self.session._executor.execute( + self.expr, + ordered=True, + use_explicit_destination=True, + page_size=page_size, + max_results=max_results, + ) + for record_batch in execute_result.arrow_batches(): + df = io_pandas.arrow_to_pandas(record_batch, self.expr.schema) self._copy_index_to_pandas(df) yield df @@ -605,22 +584,19 @@ def _copy_index_to_pandas(self, df: pd.DataFrame): def _materialize_local( self, materialize_options: MaterializationOptions = MaterializationOptions() - ) -> Tuple[pd.DataFrame, bigquery.QueryJob]: + ) -> Tuple[pd.DataFrame, Optional[bigquery.QueryJob]]: """Run query and download results as a pandas DataFrame. Return the total number of results as well.""" # TODO(swast): Allow for dry run and timeout. - _, query_job = self.session._execute( - self.expr, ordered=materialize_options.ordered - ) - results_iterator = query_job.result() - - table_size = ( - self.session._get_table_size(query_job.destination) / _BYTES_TO_MEGABYTES + execute_result = self.session._executor.execute( + self.expr, ordered=materialize_options.ordered, get_size_bytes=True ) + assert execute_result.total_bytes is not None + table_mb = execute_result.total_bytes / _BYTES_TO_MEGABYTES sample_config = materialize_options.downsampling max_download_size = sample_config.max_download_size fraction = ( - max_download_size / table_size - if (max_download_size is not None) and (table_size != 0) + max_download_size / table_mb + if (max_download_size is not None) and (table_mb != 0) else 2 ) @@ -629,7 +605,7 @@ def _materialize_local( if fraction < 1: if not sample_config.enable_downsampling: raise RuntimeError( - f"The data size ({table_size:.2f} MB) exceeds the maximum download limit of " + f"The data size ({table_mb:.2f} MB) exceeds the maximum download limit of " f"{max_download_size} MB. You can:\n\t* Enable downsampling in global options:\n" "\t\t`bigframes.options.sampling.enable_downsampling = True`\n" "\t* Update the global `max_download_size` option. Please make sure " @@ -640,12 +616,12 @@ def _materialize_local( ) warnings.warn( - f"The data size ({table_size:.2f} MB) exceeds the maximum download limit of" + f"The data size ({table_mb:.2f} MB) exceeds the maximum download limit of" f"({max_download_size} MB). It will be downsampled to {max_download_size} MB for download." "\nPlease refer to the documentation for configuring the downloading limit.", UserWarning, ) - total_rows = results_iterator.total_rows + total_rows = execute_result.total_rows # Remove downsampling config from subsequent invocations, as otherwise could result in many # iterations if downsampling undershoots return self._downsample( @@ -657,11 +633,12 @@ def _materialize_local( MaterializationOptions(ordered=materialize_options.ordered) ) else: - total_rows = results_iterator.total_rows - df = self._to_dataframe(results_iterator) + total_rows = execute_result.total_rows + arrow = self.session._executor.execute(self.expr).to_arrow_table() + df = io_pandas.arrow_to_pandas(arrow, schema=self.expr.schema) self._copy_index_to_pandas(df) - return df, query_job + return df, execute_result.query_job def _downsample( self, total_rows: int, sampling_method: str, fraction: float, random_state @@ -680,7 +657,7 @@ def _downsample( ) return block elif sampling_method == _UNIFORM: - block = self._split( + block = self.split( fracs=(fraction,), random_state=random_state, sort=False, @@ -693,7 +670,7 @@ def _downsample( f"please choose from {','.join(_SAMPLING_METHODS)}." ) - def _split( + def split( self, ns: Iterable[int] = (), fracs: Iterable[float] = (), @@ -785,7 +762,7 @@ def _compute_dry_run( self, value_keys: Optional[Iterable[str]] = None ) -> bigquery.QueryJob: expr = self._apply_value_keys_to_expr(value_keys=value_keys) - _, query_job = self.session._dry_run(expr) + query_job = self.session._executor.dry_run(expr) return query_job def _apply_value_keys_to_expr(self, value_keys: Optional[Iterable[str]] = None): @@ -1567,7 +1544,7 @@ def _forward_slice(self, start: int = 0, stop=None, step: int = 1): @functools.cache def retrieve_repr_request_results( self, max_results: int - ) -> Tuple[pd.DataFrame, int, bigquery.QueryJob]: + ) -> Tuple[pd.DataFrame, int, Optional[bigquery.QueryJob]]: """ Retrieves a pandas dataframe containing only max_results many rows for use with printing methods. @@ -1575,12 +1552,13 @@ def retrieve_repr_request_results( Returns a tuple of the dataframe and the overall number of rows of the query. """ - results, query_job = self.session._executor.head(self.expr, max_results) + head_result = self.session._executor.head(self.expr, max_results) count = self.session._executor.get_row_count(self.expr) - computed_df = self._to_dataframe(results) - self._copy_index_to_pandas(computed_df) - return computed_df, count, query_job + arrow = self.session._executor.execute(self.expr).to_arrow_table() + df = io_pandas.arrow_to_pandas(arrow, schema=self.expr.schema) + self._copy_index_to_pandas(df) + return df, count, head_result.query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: expr, result_id = self._expr.promote_offsets() @@ -2330,7 +2308,10 @@ def to_sql_query( # the BigQuery unicode column name feature? substitutions[old_id] = new_id - sql = self.session._to_sql( + # Note: this uses the sql from the executor, so is coupled tightly to execution + # implementaton. It will reference cached tables instead of original data sources. + # Maybe should just compile raw BFET? Depends on user intent. + sql = self.session._executor.to_sql( array_value, col_id_overrides=substitutions, enable_cache=enable_cache ) return ( @@ -2424,7 +2405,7 @@ def _get_rows_as_json_values(self) -> Block: # TODO(shobs): Replace direct SQL manipulation by structured expression # manipulation expr, ordering_column_name = self.expr.promote_offsets() - expr_sql = self.session._to_sql(expr) + expr_sql = self.session._executor.to_sql(expr) # Names of the columns to serialize for the row. # We will use the repr-eval pattern to serialize a value here and @@ -2578,17 +2559,8 @@ def to_pandas(self, *, ordered: Optional[bool] = None) -> pd.Index: raise bigframes.exceptions.NullIndexError( "Cannot materialize index, as this object does not have an index. Set index column(s) using set_index." ) - # Project down to only the index column. So the query can be cached to visualize other data. - index_columns = list(self._block.index_columns) - expr = self._expr.select_columns(index_columns) - results, _ = self.session._execute( - expr, ordered=ordered if ordered is not None else True - ) - df = expr.session._rows_to_dataframe(results) - df = df.set_index(index_columns) - index = df.index - index.names = list(self._block._index_labels) # type:ignore - return index + ordered = ordered if ordered is not None else True + return self._block.select_columns([]).to_pandas(ordered=ordered)[0].index def resolve_level(self, level: LevelsType) -> typing.Sequence[str]: if utils.is_list_like(level): diff --git a/bigframes/core/compile/ibis_types.py b/bigframes/core/compile/ibis_types.py index 77bfb844251..a4c37b7c5d3 100644 --- a/bigframes/core/compile/ibis_types.py +++ b/bigframes/core/compile/ibis_types.py @@ -330,7 +330,11 @@ def _ibis_dtype_to_arrow_dtype(ibis_dtype: ibis_dtypes.DataType) -> pa.DataType: if isinstance(ibis_dtype, ibis_dtypes.Struct): return pa.struct( [ - (name, _ibis_dtype_to_arrow_dtype(dtype)) + pa.field( + name, + _ibis_dtype_to_arrow_dtype(dtype), + nullable=not pa.types.is_list(_ibis_dtype_to_arrow_dtype(dtype)), + ) for name, dtype in ibis_dtype.fields.items() ] ) diff --git a/bigframes/core/schema.py b/bigframes/core/schema.py index 03e4de8993c..2b49f81d851 100644 --- a/bigframes/core/schema.py +++ b/bigframes/core/schema.py @@ -19,6 +19,7 @@ import typing import google.cloud.bigquery +import pyarrow import bigframes.core.guid import bigframes.dtypes @@ -64,6 +65,19 @@ def to_bigquery(self) -> typing.Tuple[google.cloud.bigquery.SchemaField, ...]: for item in self.items ) + def to_pyarrow(self) -> pyarrow.Schema: + fields = [] + for item in self.items: + pa_type = bigframes.dtypes.bigframes_dtype_to_arrow_dtype(item.dtype) + fields.append( + pyarrow.field( + item.column, + pa_type, + nullable=not pyarrow.types.is_list(pa_type), + ) + ) + return pyarrow.schema(fields) + def drop(self, columns: typing.Iterable[str]) -> ArraySchema: return ArraySchema( tuple(item for item in self.items if item.column not in columns) @@ -74,6 +88,14 @@ def select(self, columns: typing.Iterable[str]) -> ArraySchema: tuple(SchemaItem(name, self.get_type(name)) for name in columns) ) + def rename(self, mapping: typing.Mapping[str, str]) -> ArraySchema: + return ArraySchema( + tuple( + SchemaItem(mapping.get(item.column, item.column), item.dtype) + for item in self.items + ) + ) + def append(self, item: SchemaItem): return ArraySchema(tuple([*self.items, item])) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 47c93bfa302..1778c228cfe 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -499,7 +499,7 @@ def select_dtypes(self, include=None, exclude=None) -> DataFrame: ) return DataFrame(self._block.select_columns(selected_columns)) - def _set_internal_query_job(self, query_job: bigquery.QueryJob): + def _set_internal_query_job(self, query_job: Optional[bigquery.QueryJob]): self._query_job = query_job def __getitem__( @@ -2937,9 +2937,9 @@ def sample( ns = (n,) if n is not None else () fracs = (frac,) if frac is not None else () return DataFrame( - self._block._split( - ns=ns, fracs=fracs, random_state=random_state, sort=sort - )[0] + self._block.split(ns=ns, fracs=fracs, random_state=random_state, sort=sort)[ + 0 + ] ) def explode( @@ -2976,7 +2976,7 @@ def _split( At most one of ns and fracs can be passed in. If neither, default to ns = (1,). Return a list of sampled DataFrames. """ - blocks = self._block._split(ns=ns, fracs=fracs, random_state=random_state) + blocks = self._block.split(ns=ns, fracs=fracs, random_state=random_state) return [DataFrame(block) for block in blocks] @classmethod @@ -3156,7 +3156,7 @@ def to_gbq( default_project=default_project, ) ) - _, query_job = self._session._export( + query_job = self._session._executor.export_gbq( export_array, destination=destination, col_id_overrides=id_overrides, diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index 3cd2507231a..bc5b89b779f 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -356,6 +356,8 @@ def arrow_dtype_to_bigframes_dtype(arrow_dtype: pa.DataType) -> Dtype: for mapping in SIMPLE_TYPES if mapping.arrow_dtype is not None } +# unidirectional mapping +_BIGFRAMES_TO_ARROW[GEO_DTYPE] = pa.string() def bigframes_dtype_to_arrow_dtype( @@ -382,10 +384,14 @@ def infer_literal_type(literal) -> typing.Optional[Dtype]: as_arrow = bigframes_dtype_to_arrow_dtype(common_type) return pd.ArrowDtype(as_arrow) if pd.api.types.is_dict_like(literal): - fields = [ - (key, bigframes_dtype_to_arrow_dtype(infer_literal_type(literal[key]))) - for key in literal.keys() - ] + fields = [] + for key in literal.keys(): + field_type = bigframes_dtype_to_arrow_dtype( + infer_literal_type(literal[key]) + ) + fields.append( + pa.field(key, field_type, nullable=(not pa.types.is_list(field_type))) + ) return pd.ArrowDtype(pa.struct(fields)) if pd.isna(literal): return None # Null value without a definite type @@ -437,10 +443,13 @@ def convert_schema_field( is_repeated = field.mode == "REPEATED" if field.field_type == "RECORD": mapped_fields = map(convert_schema_field, field.fields) - pa_struct = pa.struct( - (name, bigframes_dtype_to_arrow_dtype(dtype)) - for name, dtype in mapped_fields - ) + fields = [] + for name, dtype in mapped_fields: + arrow_type = bigframes_dtype_to_arrow_dtype(dtype) + fields.append( + pa.field(name, arrow_type, nullable=not pa.types.is_list(arrow_type)) + ) + pa_struct = pa.struct(fields) pa_type = pa.list_(pa_struct) if is_repeated else pa_struct return field.name, pd.ArrowDtype(pa_type) elif ( diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index de0ae8cc684..63249b1a8a5 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -112,6 +112,7 @@ def repr_query_job(query_job: Optional[bigquery.QueryJob]): def wait_for_query_job( query_job: bigquery.QueryJob, max_results: Optional[int] = None, + page_size: Optional[int] = None, progress_bar: Optional[str] = None, ) -> bigquery.table.RowIterator: """Return query results. Displays a progress bar while the query is running @@ -120,6 +121,8 @@ def wait_for_query_job( The job representing the execution of the query on the server. max_results (int, Optional): The maximum number of rows the row iterator should return. + page_size (int, Optional): + The number of results to return on each results page. progress_bar (str, Optional): Which progress bar to show. Returns: @@ -133,7 +136,9 @@ def wait_for_query_job( display_id = str(random.random()) loading_bar = display.HTML(get_query_job_loading_html(query_job)) display.display(loading_bar, display_id=display_id) - query_result = query_job.result(max_results=max_results) + query_result = query_job.result( + max_results=max_results, page_size=page_size + ) query_job.reload() display.update_display( display.HTML(get_query_job_loading_html(query_job)), @@ -142,13 +147,17 @@ def wait_for_query_job( elif progress_bar == "terminal": initial_loading_bar = get_query_job_loading_string(query_job) print(initial_loading_bar) - query_result = query_job.result(max_results=max_results) + query_result = query_job.result( + max_results=max_results, page_size=page_size + ) query_job.reload() if initial_loading_bar != get_query_job_loading_string(query_job): print(get_query_job_loading_string(query_job)) else: # No progress bar. - query_result = query_job.result(max_results=max_results) + query_result = query_job.result( + max_results=max_results, page_size=page_size + ) query_job.reload() return query_result except api_core_exceptions.RetryError as exc: diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index b41426f7d70..02ccc9d6a54 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -24,6 +24,7 @@ import bigframes import bigframes.constants as constants +import bigframes.formatting_helpers as formatting_helpers from bigframes.ml import sql as ml_sql import bigframes.pandas as bpd @@ -233,7 +234,7 @@ def copy(self, new_model_name: str, replace: bool = False) -> BqmlModel: copy_job = self._session.bqclient.copy_table( self.model_name, new_model_name, job_config=job_config ) - self._session._start_generic_job(copy_job) + _start_generic_job(copy_job) new_model = self._session.bqclient.get_model(new_model_name) return BqmlModel(self._session, new_model) @@ -479,3 +480,12 @@ def create_xgboost_imported_model( ) return self._create_model_with_sql(session=session, sql=sql) + + +def _start_generic_job(job: formatting_helpers.GenericJob): + if bigframes.options.display.progress_bar is not None: + formatting_helpers.wait_for_job( + job, bigframes.options.display.progress_bar + ) # Wait for the job to complete + else: + job.result() diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 55b8fa1802b..c48c8c4136f 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -879,10 +879,12 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT fields = [] for i in range(num_input_types): + arrow_type = dtypes.bigframes_dtype_to_arrow_dtype(input_types[i]) fields.append( - ( + pa.field( self.column_names[i], - dtypes.bigframes_dtype_to_arrow_dtype(input_types[i]), + arrow_type, + nullable=(not pa.types.is_list(arrow_type)), ) ) return pd.ArrowDtype( diff --git a/bigframes/operations/aggregations.py b/bigframes/operations/aggregations.py index faba7465d99..3e4e9d1df15 100644 --- a/bigframes/operations/aggregations.py +++ b/bigframes/operations/aggregations.py @@ -323,8 +323,8 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT ) pa_type = pa.struct( [ - ("left_exclusive", interval_dtype), - ("right_inclusive", interval_dtype), + pa.field("left_exclusive", interval_dtype, nullable=True), + pa.field("right_inclusive", interval_dtype, nullable=True), ] ) return pd.ArrowDtype(pa_type) diff --git a/bigframes/series.py b/bigframes/series.py index 193eea7ee39..7f2e5c5ccc0 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -185,7 +185,7 @@ def _session(self) -> bigframes.Session: def transpose(self) -> Series: return self - def _set_internal_query_job(self, query_job: bigquery.QueryJob): + def _set_internal_query_job(self, query_job: Optional[bigquery.QueryJob]): self._query_job = query_job def __len__(self): @@ -1790,9 +1790,9 @@ def sample( ns = (n,) if n is not None else () fracs = (frac,) if frac is not None else () return Series( - self._block._split( - ns=ns, fracs=fracs, random_state=random_state, sort=sort - )[0] + self._block.split(ns=ns, fracs=fracs, random_state=random_state, sort=sort)[ + 0 + ] ) def explode(self, *, ignore_index: Optional[bool] = False) -> Series: diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 3a9cba442c0..0d7a90c250c 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -70,7 +70,6 @@ import bigframes.dataframe import bigframes.dtypes import bigframes.exceptions -import bigframes.formatting_helpers as formatting_helpers import bigframes.functions._remote_function_session as bigframes_rf_session import bigframes.functions.remote_function as bigframes_rf import bigframes.session._io.bigquery as bf_io_bigquery @@ -261,11 +260,11 @@ def __init__( ) self._executor = bigframes.session.executor.BigQueryCachingExecutor( bqclient=self._clients_provider.bqclient, + bqstoragereadclient=self._clients_provider.bqstoragereadclient, storage_manager=self._temp_storage_manager, strictly_ordered=self._strictly_ordered, metrics=self._metrics, ) - self._loader = bigframes.session.loader.GbqDataLoader( session=self, bqclient=self._clients_provider.bqclient, @@ -1372,20 +1371,6 @@ def _start_query_ml_ddl( return bf_io_bigquery.start_query_with_client(self.bqclient, sql, job_config) - def _execute( - self, - array_value: core.ArrayValue, - *, - ordered: bool = True, - col_id_overrides: Mapping[str, str] = {}, - use_explicit_destination: bool = False, - ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - return self._executor.execute( - array_value, - ordered=ordered, - col_id_overrides=col_id_overrides, - ) - def _export( self, array_value: core.ArrayValue, @@ -1404,49 +1389,6 @@ def _export( cluster_cols=cluster_cols, ) - def _dry_run( - self, array_value: core.ArrayValue, ordered: bool = True - ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - return self._executor.dry_run(array_value, ordered=ordered) - - def _peek( - self, array_value: core.ArrayValue, n_rows: int - ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: - """A 'peek' efficiently accesses a small number of rows in the dataframe.""" - return self._executor.peek(array_value, n_rows) - - def _to_sql( - self, - array_value: core.ArrayValue, - offset_column: typing.Optional[str] = None, - col_id_overrides: typing.Mapping[str, str] = {}, - ordered: bool = False, - enable_cache: bool = True, - ) -> str: - return self._executor.to_sql( - array_value, offset_column, col_id_overrides, ordered, enable_cache - ) - - def _get_table_size(self, destination_table): - table = self.bqclient.get_table(destination_table) - return table.num_bytes - - def _rows_to_dataframe( - self, row_iterator: bigquery.table.RowIterator - ) -> pandas.DataFrame: - # Can ignore inferred datatype until dtype emulation breaks 1:1 mapping between BQ types and bigframes types - dtypes_from_bq = bigframes.dtypes.bf_type_from_type_kind(row_iterator.schema) - arrow_table = row_iterator.to_arrow() - return bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes_from_bq) - - def _start_generic_job(self, job: formatting_helpers.GenericJob): - if bigframes.options.display.progress_bar is not None: - formatting_helpers.wait_for_job( - job, bigframes.options.display.progress_bar - ) # Wait for the job to complete - else: - job.result() - def connect(context: Optional[bigquery_options.BigQueryOptions] = None) -> Session: return Session(context) diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 011c1f1bee6..b7706d34ca3 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -237,7 +237,7 @@ def start_query_with_client( opts = bigframes.options.display if opts.progress_bar is not None and not query_job.configuration.dry_run: results_iterator = formatting_helpers.wait_for_query_job( - query_job, max_results, opts.progress_bar + query_job, max_results=max_results, progress_bar=opts.progress_bar ) else: results_iterator = query_job.result(max_results=max_results) diff --git a/bigframes/session/_io/pandas.py b/bigframes/session/_io/pandas.py index 83e30fd9003..6ceaab69156 100644 --- a/bigframes/session/_io/pandas.py +++ b/bigframes/session/_io/pandas.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations -from typing import Dict, Union +from typing import Union import bigframes_vendored.constants as constants import geopandas # type: ignore @@ -22,6 +23,7 @@ import pyarrow.compute # type: ignore import pyarrow.types # type: ignore +import bigframes.core.schema import bigframes.features @@ -49,17 +51,18 @@ def _arrow_to_pandas_arrowdtype( def arrow_to_pandas( - arrow_table: Union[pyarrow.Table, pyarrow.RecordBatch], dtypes: Dict + arrow_table: Union[pyarrow.Table, pyarrow.RecordBatch], + schema: bigframes.core.schema.ArraySchema, ): - if len(dtypes) != arrow_table.num_columns: + if len(schema) != arrow_table.num_columns: raise ValueError( - f"Number of types {len(dtypes)} doesn't match number of columns " + f"Number of types {len(schema)} doesn't match number of columns " f"{arrow_table.num_columns}. {constants.FEEDBACK_LINK}" ) serieses = {} for field, column in zip(arrow_table.schema, arrow_table): - dtype = dtypes[field.name] + dtype = schema.get_type(field.name) if dtype == geopandas.array.GeometryDtype(): series = geopandas.GeoSeries.from_wkt( diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index f89b5aefec8..13d74a73948 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -14,14 +14,29 @@ from __future__ import annotations +import dataclasses import math -from typing import cast, Literal, Mapping, Optional, Sequence, Tuple, Union +import os +from typing import ( + Callable, + cast, + Iterator, + Literal, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) import warnings import weakref import google.api_core.exceptions import google.cloud.bigquery as bigquery import google.cloud.bigquery.job as bq_job +import google.cloud.bigquery.table as bq_table +import google.cloud.bigquery_storage_v1 +import pyarrow import bigframes.core import bigframes.core.compile @@ -29,7 +44,9 @@ import bigframes.core.guid import bigframes.core.nodes as nodes import bigframes.core.ordering as order +import bigframes.core.schema import bigframes.core.tree_properties as tree_properties +import bigframes.features import bigframes.formatting_helpers as formatting_helpers import bigframes.operations as ops import bigframes.session._io.bigquery as bq_io @@ -41,10 +58,27 @@ QUERY_COMPLEXITY_LIMIT = 1e7 # Number of times to factor out subqueries before giving up. MAX_SUBTREE_FACTORINGS = 5 - _MAX_CLUSTER_COLUMNS = 4 +@dataclasses.dataclass(frozen=True) +class ExecuteResult: + arrow_batches: Callable[[], Iterator[pyarrow.RecordBatch]] + schema: bigframes.core.schema.ArraySchema + query_job: Optional[bigquery.QueryJob] = None + total_bytes: Optional[int] = None + total_rows: Optional[int] = None + + def to_arrow_table(self) -> pyarrow.Table: + # Need to provide schema if no result rows, as arrow can't infer + # If ther are rows, it is safest to infer schema from batches. + # Any discrepencies between predicted schema and actual schema will produce errors. + return pyarrow.Table.from_batches( + self.arrow_batches(), + self.schema.to_pyarrow() if not self.total_rows else None, + ) + + class BigQueryCachingExecutor: """Computes BigFrames values using BigQuery Engine. @@ -58,6 +92,7 @@ def __init__( self, bqclient: bigquery.Client, storage_manager: bigframes.session.temp_storage.TemporaryGbqStorageManager, + bqstoragereadclient: google.cloud.bigquery_storage_v1.BigQueryReadClient, strictly_ordered: bool = True, metrics: Optional[bigframes.session.metrics.ExecutionMetrics] = None, ): @@ -71,6 +106,7 @@ def __init__( nodes.BigFrameNode, nodes.BigFrameNode ] = weakref.WeakKeyDictionary() self.metrics = metrics + self.bqstoragereadclient = bqstoragereadclient def to_sql( self, @@ -105,6 +141,9 @@ def execute( ordered: bool = True, col_id_overrides: Mapping[str, str] = {}, use_explicit_destination: bool = False, + get_size_bytes: bool = False, + page_size: Optional[int] = None, + max_results: Optional[int] = None, ): """ Execute the ArrayValue, storing the result to a temporary session-owned table. @@ -115,19 +154,43 @@ def execute( sql = self.to_sql( array_value, ordered=ordered, col_id_overrides=col_id_overrides ) + adjusted_schema = array_value.schema.rename(col_id_overrides) job_config = bigquery.QueryJobConfig() # Use explicit destination to avoid 10GB limit of temporary table if use_explicit_destination: - schema = array_value.schema.to_bigquery() destination_table = self.storage_manager.create_temp_table( - schema, cluster_cols=[] + adjusted_schema.to_bigquery(), cluster_cols=[] ) job_config.destination = destination_table # TODO(swast): plumb through the api_name of the user-facing api that # caused this query. - return self._run_execute_query( + iterator, query_job = self._run_execute_query( sql=sql, job_config=job_config, + page_size=page_size, + max_results=max_results, + ) + + # Though we provide the read client, iterator may or may not use it based on what is efficient for the result + def iterator_supplier(): + return iterator.to_arrow_iterable(bqstorage_client=self.bqstoragereadclient) + + if get_size_bytes is True: + size_bytes = self.bqclient.get_table(query_job.destination).num_bytes + else: + size_bytes = None + + # Runs strict validations to ensure internal type predictions and ibis are completely in sync + # Do not execute these validations outside of testing suite. + if "PYTEST_CURRENT_TEST" in os.environ and len(col_id_overrides) == 0: + validate_result_schema(array_value, iterator.schema) + + return ExecuteResult( + arrow_batches=iterator_supplier, + schema=adjusted_schema, + query_job=query_job, + total_bytes=size_bytes, + total_rows=iterator.total_rows, ) def export_gbq( @@ -154,10 +217,11 @@ def export_gbq( ) # TODO(swast): plumb through the api_name of the user-facing api that # caused this query. - return self._run_execute_query( + _, query_job = self._run_execute_query( sql=sql, job_config=job_config, ) + return query_job def export_gcs( self, @@ -170,11 +234,11 @@ def export_gcs( """ Export the ArrayValue to gcs. """ - _, query_job = self.execute( + query_job = self.execute( array_value, ordered=False, col_id_overrides=col_id_overrides, - ) + ).query_job result_table = query_job.destination export_data_statement = bq_io.create_export_data_statement( f"{result_table.project}.{result_table.dataset_id}.{result_table.table_id}", @@ -188,7 +252,9 @@ def export_gcs( self._wait_on_job(export_job) return query_job - def dry_run(self, array_value: bigframes.core.ArrayValue, ordered: bool = True): + def dry_run( + self, array_value: bigframes.core.ArrayValue, ordered: bool = True + ) -> bigquery.QueryJob: """ Dry run executing the ArrayValue. @@ -198,14 +264,14 @@ def dry_run(self, array_value: bigframes.core.ArrayValue, ordered: bool = True): job_config = bigquery.QueryJobConfig(dry_run=True) bq_io.add_labels(job_config) query_job = self.bqclient.query(sql, job_config=job_config) - results_iterator = query_job.result() - return results_iterator, query_job + _ = query_job.result() + return query_job def peek( self, array_value: bigframes.core.ArrayValue, n_rows: int, - ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + ) -> ExecuteResult: """ A 'peek' efficiently accesses a small number of rows in the dataframe. """ @@ -217,11 +283,22 @@ def peek( # TODO(swast): plumb through the api_name of the user-facing api that # caused this query. - return self._run_execute_query(sql=sql) + iterator, query_job = self._run_execute_query(sql=sql) + return ExecuteResult( + # Probably don't need read client for small peek results, but let client decide + arrow_batches=lambda: iterator.to_arrow_iterable( + bqstorage_client=self.bqstoragereadclient + ), + schema=array_value.schema, + query_job=query_job, + total_rows=iterator.total_rows, + ) + # This is used exclusively to optimize __repr__ + # TODO: We need to model this def head( self, array_value: bigframes.core.ArrayValue, n_rows: int - ) -> tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + ) -> ExecuteResult: """ Preview the first n rows of the dataframe. This is less efficient than the unordered peek preview op. """ @@ -249,8 +326,18 @@ def head( # TODO(swast): plumb through the api_name of the user-facing api that # caused this query. - return self._run_execute_query(sql=sql) + iterator, query_job = self._run_execute_query(sql=sql) + return ExecuteResult( + # Probably don't need read client for small head results, but let client decide + arrow_batches=lambda: iterator.to_arrow_iterable( + bqstorage_client=self.bqstoragereadclient + ), + schema=array_value.schema, + query_job=query_job, + total_rows=iterator.total_rows, + ) + # TODO: Remove. We shouldn't need this method, row count node can automatically be detected def get_row_count(self, array_value: bigframes.core.ArrayValue) -> int: count = self._local_get_row_count(array_value) if count is not None: @@ -277,7 +364,9 @@ def _run_execute_query( sql: str, job_config: Optional[bq_job.QueryJobConfig] = None, api_name: Optional[str] = None, - ) -> Tuple[bigquery.table.RowIterator, bigquery.QueryJob]: + page_size: Optional[int] = None, + max_results: Optional[int] = None, + ) -> Tuple[bq_table.RowIterator, bigquery.QueryJob]: """ Starts BigQuery query job and waits for results. """ @@ -293,7 +382,12 @@ def _run_execute_query( job_config.labels["bigframes-mode"] = "unordered" try: query_job = self.bqclient.query(sql, job_config=job_config) - return self._wait_on_job(query_job), query_job + return ( + self._wait_on_job( + query_job, max_results=max_results, page_size=page_size + ), + query_job, + ) except google.api_core.exceptions.BadRequest as e: # Unfortunately, this error type does not have a separate error code or exception type @@ -303,14 +397,24 @@ def _run_execute_query( else: raise - def _wait_on_job(self, query_job: bigquery.QueryJob) -> bigquery.table.RowIterator: + def _wait_on_job( + self, + query_job: bigquery.QueryJob, + page_size: Optional[int] = None, + max_results: Optional[int] = None, + ) -> bq_table.RowIterator: opts = bigframes.options.display if opts.progress_bar is not None and not query_job.configuration.dry_run: results_iterator = formatting_helpers.wait_for_query_job( - query_job, progress_bar=opts.progress_bar + query_job, + progress_bar=opts.progress_bar, + max_results=max_results, + page_size=page_size, ) else: - results_iterator = query_job.result() + results_iterator = query_job.result( + max_results=max_results, page_size=page_size + ) if self.metrics is not None: self.metrics.count_job_stats(query_job) @@ -459,3 +563,21 @@ def generate_head_plan(node: nodes.BigFrameNode, n: int): def generate_row_count_plan(node: nodes.BigFrameNode): return nodes.RowCountNode(node) + + +def validate_result_schema( + array_value: bigframes.core.ArrayValue, bq_schema: list[bigquery.schema.SchemaField] +): + actual_schema = tuple(bq_schema) + ibis_schema = array_value._compiled_schema + internal_schema = array_value.schema + if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: + return + if internal_schema.to_bigquery() != actual_schema: + raise ValueError( + f"This error should only occur while testing. BigFrames internal schema: {internal_schema.to_bigquery()} does not match actual schema: {actual_schema}" + ) + if ibis_schema.to_bigquery() != actual_schema: + raise ValueError( + f"This error should only occur while testing. Ibis schema: {ibis_schema.to_bigquery()} does not match actual schema: {actual_schema}" + ) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 340df93791d..d44afb4fe00 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1477,7 +1477,7 @@ def test_get_dtypes_array_struct_table(nested_df): pa.list_( pa.struct( [ - ( + pa.field( "data", pa.list_( pa.struct( @@ -1487,6 +1487,7 @@ def test_get_dtypes_array_struct_table(nested_df): ], ), ), + nullable=False, ), ("timestamp", pa.timestamp("us", "UTC")), ("category", pa.string()), @@ -2195,17 +2196,15 @@ def test_series_binop_axis_index( [ ((1000, 2000, 3000)), (pd.Index([1000, 2000, 3000])), - (bf_indexes.Index([1000, 2000, 3000])), (pd.Series((1000, 2000), index=["int64_too", "float64_col"])), ], ids=[ "tuple", "pd_index", - "bf_index", "pd_series", ], ) -def test_listlike_binop_axis_1(scalars_dfs, input): +def test_listlike_binop_axis_1_in_memory_data(scalars_dfs, input): scalars_df, scalars_pandas_df = scalars_dfs df_columns = ["int64_col", "float64_col", "int64_too"] @@ -2218,6 +2217,21 @@ def test_listlike_binop_axis_1(scalars_dfs, input): assert_pandas_df_equal(bf_result, pd_result, check_dtype=False) +def test_listlike_binop_axis_1_bf_index(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + df_columns = ["int64_col", "float64_col", "int64_too"] + + bf_result = ( + scalars_df[df_columns] + .add(bf_indexes.Index([1000, 2000, 3000]), axis=1) + .to_pandas() + ) + pd_result = scalars_pandas_df[df_columns].add(pd.Index([1000, 2000, 3000]), axis=1) + + assert_pandas_df_equal(bf_result, pd_result, check_dtype=False) + + def test_binop_with_self_aggregate(session, scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/tests/system/small/test_remote_function.py b/tests/system/small/test_remote_function.py index f68589f4316..c1ca0d04c09 100644 --- a/tests/system/small/test_remote_function.py +++ b/tests/system/small/test_remote_function.py @@ -1225,7 +1225,6 @@ def add_pandas(s: pd.Series) -> float: pytest.param("mask"), ], ) -@pytest.mark.flaky(retries=2, delay=120) def test_remote_function_unary_partial_ordering_mode_assign( unordered_session, dataset_id_permanent, method ): diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index ed3e38e6f84..17e8b99704b 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -47,10 +47,10 @@ def test_read_gbq_tokyo( result = df.sort_index().to_pandas() expected = scalars_pandas_df_index - _, query_job = session_tokyo._execute(df._block.expr) - assert query_job.location == tokyo_location + result = session_tokyo._executor.execute(df._block.expr) + assert result.query_job.location == tokyo_location - pd.testing.assert_frame_equal(result, expected) + assert len(expected) == result.total_rows @pytest.mark.parametrize( @@ -671,10 +671,10 @@ def test_read_pandas_tokyo( result = df.to_pandas() expected = scalars_pandas_df_index - _, query_job = session_tokyo._execute(df._block.expr) - assert query_job.location == tokyo_location + result = session_tokyo._executor.execute(df._block.expr) + assert result.query_job.location == tokyo_location - pd.testing.assert_frame_equal(result, expected) + assert len(expected) == result.total_rows @utils.skip_legacy_pandas diff --git a/tests/unit/session/test_io_pandas.py b/tests/unit/session/test_io_pandas.py index 81d02466ef0..2fa07aed35f 100644 --- a/tests/unit/session/test_io_pandas.py +++ b/tests/unit/session/test_io_pandas.py @@ -25,6 +25,7 @@ import pyarrow # type: ignore import pytest +import bigframes.core.schema import bigframes.features import bigframes.pandas import bigframes.session._io.pandas @@ -445,7 +446,13 @@ def test_arrow_to_pandas( dtypes: Dict, expected: pandas.DataFrame, ): - actual = bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) + schema = bigframes.core.schema.ArraySchema( + tuple( + bigframes.core.schema.SchemaItem(name, dtype) + for name, dtype in dtypes.items() + ) + ) + actual = bigframes.session._io.pandas.arrow_to_pandas(arrow_table, schema) pandas.testing.assert_series_equal(actual.dtypes, expected.dtypes) # assert_frame_equal is converting to numpy internally, which causes some @@ -478,8 +485,14 @@ def test_arrow_to_pandas( def test_arrow_to_pandas_wrong_size_dtypes( arrow_table: Union[pyarrow.Table, pyarrow.RecordBatch], dtypes: Dict ): - with pytest.raises(ValueError, match=f"Number of types {len(dtypes)}"): - bigframes.session._io.pandas.arrow_to_pandas(arrow_table, dtypes) + schema = bigframes.core.schema.ArraySchema( + tuple( + bigframes.core.schema.SchemaItem(name, dtype) + for name, dtype in dtypes.items() + ) + ) + with pytest.raises(ValueError, match=f"Number of types {len(schema)}"): + bigframes.session._io.pandas.arrow_to_pandas(arrow_table, schema) def test_read_pandas_with_bigframes_dataframe(): From ff11ac843ffeecda23d5273b25bd8026968fc5f5 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 26 Sep 2024 22:24:47 -0700 Subject: [PATCH 864/921] refactor: Create class for column ids and column refs (#1022) --- bigframes/core/__init__.py | 65 +++--- bigframes/core/block_transforms.py | 14 +- bigframes/core/blocks.py | 55 ++--- bigframes/core/compile/compiled.py | 134 ++++++++----- bigframes/core/compile/compiler.py | 41 ++-- bigframes/core/compile/concat.py | 3 +- bigframes/core/compile/default_ordering.py | 5 +- bigframes/core/compile/scalar_op_compiler.py | 6 +- bigframes/core/compile/single_column.py | 4 +- bigframes/core/expression.py | 170 ++++++++++++---- bigframes/core/identifiers.py | 31 ++- bigframes/core/indexes/base.py | 20 +- bigframes/core/join_def.py | 16 +- bigframes/core/nodes.py | 199 ++++++++++--------- bigframes/core/ordering.py | 99 +++++---- bigframes/core/pruning.py | 35 ++-- bigframes/core/rewrite.py | 49 +++-- bigframes/core/sql.py | 6 +- bigframes/core/window/__init__.py | 2 +- bigframes/core/window_spec.py | 24 ++- bigframes/dataframe.py | 4 +- bigframes/operations/__init__.py | 4 +- bigframes/operations/base.py | 46 ++--- bigframes/series.py | 2 +- bigframes/session/executor.py | 18 +- bigframes/session/planner.py | 23 ++- tests/unit/core/test_expression.py | 9 +- tests/unit/test_planner.py | 7 +- 28 files changed, 654 insertions(+), 437 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 8ad00a46c61..a8b79223f4b 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -29,6 +29,7 @@ import bigframes.core.compile import bigframes.core.expression as ex import bigframes.core.guid +import bigframes.core.identifiers as ids import bigframes.core.join_def as join_def import bigframes.core.local_data as local_data import bigframes.core.nodes as nodes @@ -169,7 +170,7 @@ def row_count(self) -> ArrayValue: # Operations def filter_by_id(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: """Filter the table on a given expression, the predicate must be a boolean series aligned with the table expression.""" - predicate: ex.Expression = ex.free_var(predicate_id) + predicate: ex.Expression = ex.deref(predicate_id) if keep_null: predicate = ops.fillna_op.as_expr(predicate, ex.const(True)) return self.filter(predicate) @@ -200,7 +201,9 @@ def promote_offsets(self) -> Tuple[ArrayValue, str]: ) return ( - ArrayValue(nodes.PromoteOffsetsNode(child=self.node, col_id=col_id)), + ArrayValue( + nodes.PromoteOffsetsNode(child=self.node, col_id=ids.ColumnId(col_id)) + ), col_id, ) @@ -212,7 +215,9 @@ def concat(self, other: typing.Sequence[ArrayValue]) -> ArrayValue: def compute_values(self, assignments: Sequence[ex.Expression]): col_ids = self._gen_namespaced_uids(len(assignments)) - ex_id_pairs = tuple((ex, id) for ex, id in zip(assignments, col_ids)) + ex_id_pairs = tuple( + (ex, ids.ColumnId(id)) for ex, id in zip(assignments, col_ids) + ) return ( ArrayValue(nodes.ProjectionNode(child=self.node, assignments=ex_id_pairs)), col_ids, @@ -228,14 +233,19 @@ def assign(self, source_id: str, destination_id: str) -> ArrayValue: if destination_id in self.column_ids: # Mutate case exprs = [ ( - (source_id if (col_id == destination_id) else col_id), - col_id, + ex.deref(source_id if (col_id == destination_id) else col_id), + ids.ColumnId(col_id), ) for col_id in self.column_ids ] else: # append case - self_projection = ((col_id, col_id) for col_id in self.column_ids) - exprs = [*self_projection, (source_id, destination_id)] + self_projection = ( + (ex.deref(col_id), ids.ColumnId(col_id)) for col_id in self.column_ids + ) + exprs = [ + *self_projection, + (ex.deref(source_id), ids.ColumnId(destination_id)), + ] return ArrayValue( nodes.SelectionNode( child=self.node, @@ -248,24 +258,15 @@ def create_constant( value: typing.Any, dtype: typing.Optional[bigframes.dtypes.Dtype], ) -> Tuple[ArrayValue, str]: - destination_id = self._gen_namespaced_uid() if pandas.isna(value): # Need to assign a data type when value is NaN. dtype = dtype or bigframes.dtypes.DEFAULT_DTYPE - return ( - ArrayValue( - nodes.ProjectionNode( - child=self.node, - assignments=((ex.const(value, dtype), destination_id),), - ) - ), - destination_id, - ) + return self.project_to_id(ex.const(value, dtype)) def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: # This basically just drops and reorders columns - logically a no-op except as a final step - selections = ((col_id, col_id) for col_id in column_ids) + selections = ((ex.deref(col_id), ids.ColumnId(col_id)) for col_id in column_ids) return ArrayValue( nodes.SelectionNode( child=self.node, @@ -274,14 +275,8 @@ def select_columns(self, column_ids: typing.Sequence[str]) -> ArrayValue: ) def drop_columns(self, columns: Iterable[str]) -> ArrayValue: - new_projection = ( - (col_id, col_id) for col_id in self.column_ids if col_id not in columns - ) - return ArrayValue( - nodes.SelectionNode( - child=self.node, - input_output_pairs=tuple(new_projection), - ) + return self.select_columns( + [col_id for col_id in self.column_ids if col_id not in columns] ) def aggregate( @@ -297,11 +292,12 @@ def aggregate( by_column_id: column id of the aggregation key, this is preserved through the transform dropna: whether null keys should be dropped """ + agg_defs = tuple((agg, ids.ColumnId(name)) for agg, name in aggregations) return ArrayValue( nodes.AggregateNode( child=self.node, - aggregations=tuple(aggregations), - by_column_ids=tuple(by_column_ids), + aggregations=agg_defs, + by_column_ids=tuple(map(ex.deref, by_column_ids)), dropna=dropna, ) ) @@ -342,10 +338,10 @@ def project_window_op( ArrayValue( nodes.WindowOpNode( child=self.node, - column_name=column_name, + column_name=ex.deref(column_name), op=op, window_spec=window_spec, - output_name=output_name, + output_name=ids.ColumnId(output_name), never_skip_nulls=never_skip_nulls, skip_reproject_unsafe=skip_reproject_unsafe, ) @@ -376,7 +372,9 @@ def relational_join( join_node = nodes.JoinNode( left_child=self.node, right_child=other.node, - conditions=conditions, + conditions=tuple( + (ex.deref(l_col), ex.deref(r_col)) for l_col, r_col in conditions + ), type=type, ) # Maps input ids to output ids for caller convenience @@ -414,7 +412,7 @@ def explode(self, column_ids: typing.Sequence[str]) -> ArrayValue: for column_id in column_ids: assert bigframes.dtypes.is_array_like(self.get_column_type(column_id)) - offsets = tuple(self.get_offset_for_name(id) for id in column_ids) + offsets = tuple(ex.deref(id) for id in column_ids) return ArrayValue(nodes.ExplodeNode(child=self.node, column_ids=offsets)) def _uniform_sampling(self, fraction: float) -> ArrayValue: @@ -425,9 +423,6 @@ def _uniform_sampling(self, fraction: float) -> ArrayValue: """ return ArrayValue(nodes.RandomSampleNode(self.node, fraction)) - def get_offset_for_name(self, name: str): - return self.schema.names.index(name) - # Deterministically generate namespaced ids for new variables # These new ids are only unique within the current namespace. # Many operations, such as joins, create new namespaces. See: BigFrameNode.defines_namespace diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 398c6ab26ae..2c4991b6290 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -210,7 +210,7 @@ def _interpolate_column( ) -> typing.Tuple[blocks.Block, str]: if interpolate_method not in ["linear", "nearest", "ffill"]: raise ValueError("interpolate method not supported") - window_ordering = (ordering.OrderingExpression(ex.free_var(x_values)),) + 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) @@ -373,7 +373,7 @@ def value_counts( block = block.order_by( [ ordering.OrderingExpression( - ex.free_var(count_id), + ex.deref(count_id), direction=ordering.OrderingDirection.ASC if ascending else ordering.OrderingDirection.DESC, @@ -430,7 +430,7 @@ def rank( nullity_col_ids.append(nullity_col_id) window_ordering = ( ordering.OrderingExpression( - ex.free_var(col), + ex.deref(col), ordering.OrderingDirection.ASC if ascending else ordering.OrderingDirection.DESC, @@ -522,7 +522,7 @@ def nsmallest( block = block.reversed() order_refs = [ ordering.OrderingExpression( - ex.free_var(col_id), direction=ordering.OrderingDirection.ASC + ex.deref(col_id), direction=ordering.OrderingDirection.ASC ) for col_id in column_ids ] @@ -552,7 +552,7 @@ def nlargest( block = block.reversed() order_refs = [ ordering.OrderingExpression( - ex.free_var(col_id), direction=ordering.OrderingDirection.DESC + ex.deref(col_id), direction=ordering.OrderingDirection.DESC ) for col_id in column_ids ] @@ -849,9 +849,9 @@ def _idx_extrema( ) # Have to find the min for each order_refs = [ - ordering.OrderingExpression(ex.free_var(value_col), direction), + ordering.OrderingExpression(ex.deref(value_col), direction), *[ - ordering.OrderingExpression(ex.free_var(idx_col)) + ordering.OrderingExpression(ex.deref(idx_col)) for idx_col in original_block.index_columns ], ] diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 4bc87233b0d..76cf065f894 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -716,7 +716,7 @@ def split( ) block, hash_string_sum_col = block.apply_unary_op(string_sum_col, ops.hash_op) block = block.order_by( - [ordering.OrderingExpression(ex.free_var(hash_string_sum_col))] + [ordering.OrderingExpression(ex.deref(hash_string_sum_col))] ) intervals = [] @@ -735,7 +735,7 @@ def split( sliced_blocks = [ sliced_block.order_by( [ - ordering.OrderingExpression(ex.free_var(idx_col)) + ordering.OrderingExpression(ex.deref(idx_col)) for idx_col in sliced_block.index_columns ] ) @@ -744,7 +744,7 @@ def split( elif sort is False: sliced_blocks = [ sliced_block.order_by( - [ordering.OrderingExpression(ex.free_var(ordering_col))] + [ordering.OrderingExpression(ex.deref(ordering_col))] ) for sliced_block in sliced_blocks ] @@ -902,9 +902,9 @@ def multi_apply_unary_op( ) -> Block: if isinstance(op, ops.UnaryOp): input_varname = guid.generate_guid() - expr = op.as_expr(input_varname) + expr = op.as_expr(ex.free_var(input_varname)) else: - input_varnames = op.unbound_variables + input_varnames = op.free_variables assert len(input_varnames) == 1 expr = op input_varname = input_varnames[0] @@ -913,7 +913,7 @@ def multi_apply_unary_op( for col_id in columns: label = self.col_id_to_label[col_id] block, result_id = block.project_expr( - expr.bind_variables({input_varname: ex.free_var(col_id)}), + expr.bind_variables({input_varname: ex.deref(col_id)}), label=label, ) block = block.copy_values(result_id, col_id) @@ -943,7 +943,7 @@ def apply_window_op( block = self if skip_null_groups: for key in window_spec.grouping_keys: - block, not_null_id = block.apply_unary_op(key, ops.notnull_op) + block, not_null_id = block.apply_unary_op(key.id.name, ops.notnull_op) block = block.filter_by_id(not_null_id).drop_columns([not_null_id]) expr, result_id = block._expr.project_window_op( column, @@ -1027,7 +1027,7 @@ def aggregate_all_and_stack( if axis_n == 0: aggregations = [ ( - ex.UnaryAggregation(operation, ex.free_var(col_id)) + ex.UnaryAggregation(operation, ex.deref(col_id)) if isinstance(operation, agg_ops.UnaryAggregateOp) else ex.NullaryAggregation(operation), col_id, @@ -1058,7 +1058,10 @@ def aggregate_all_and_stack( index_cols = passthrough_cols[:-1] og_offset_col = passthrough_cols[-1] index_aggregations = [ - (ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.free_var(col_id)), col_id) + ( + ex.UnaryAggregation(agg_ops.AnyValueOp(), ex.deref(col_id)), + col_id, + ) for col_id in index_cols ] # TODO: may need add NullaryAggregation in main_aggregation @@ -1067,7 +1070,7 @@ def aggregate_all_and_stack( operation, agg_ops.UnaryAggregateOp ), f"Expected a unary operation, but got {operation}. Please report this error and how you got here to the BigQuery DataFrames team (bit.ly/bigframes-feedback)." main_aggregation = ( - ex.UnaryAggregation(operation, ex.free_var(value_col_ids[0])), + ex.UnaryAggregation(operation, ex.deref(value_col_ids[0])), value_col_ids[0], ) # Drop row identity after aggregating over it @@ -1177,7 +1180,7 @@ def aggregate( """ agg_specs = [ ( - ex.UnaryAggregation(operation, ex.free_var(input_id)) + ex.UnaryAggregation(operation, ex.deref(input_id)) if isinstance(operation, agg_ops.UnaryAggregateOp) else ex.NullaryAggregation(operation), guid.generate_guid(), @@ -1235,7 +1238,7 @@ def get_stat( aggregations = [ ( - ex.UnaryAggregation(stat, ex.free_var(column_id)) + ex.UnaryAggregation(stat, ex.deref(column_id)) if isinstance(stat, agg_ops.UnaryAggregateOp) else ex.NullaryAggregation(stat), stat.name, @@ -1264,7 +1267,7 @@ def get_binary_stat( aggregations = [ ( ex.BinaryAggregation( - stat, ex.free_var(column_id_left), ex.free_var(column_id_right) + stat, ex.deref(column_id_left), ex.deref(column_id_right) ), f"{stat.name}_{column_id_left}{column_id_right}", ) @@ -1290,7 +1293,7 @@ def summarize( labels = pd.Index([stat.name for stat in stats]) aggregations = [ ( - ex.UnaryAggregation(stat, ex.free_var(col_id)) + ex.UnaryAggregation(stat, ex.deref(col_id)) if isinstance(stat, agg_ops.UnaryAggregateOp) else ex.NullaryAggregation(stat), f"{col_id}-{stat.name}", @@ -1327,7 +1330,7 @@ def calculate_pairwise_metric(self, op=agg_ops.CorrOp()): aggregations = [ ( - ex.BinaryAggregation(op, ex.free_var(left_col), ex.free_var(right_col)), + ex.BinaryAggregation(op, ex.deref(left_col), ex.deref(right_col)), f"{left_col}-{right_col}", ) for left_col in self.value_columns @@ -2009,7 +2012,7 @@ def merge( # sort uses coalesced join keys always joined_expr = joined_expr.order_by( [ - ordering.OrderingExpression(ex.free_var(col_id)) + ordering.OrderingExpression(ex.deref(col_id)) for col_id in coalesced_ids ], ) @@ -2062,12 +2065,12 @@ def _align_both_axes( ) left_input_lookup = ( - lambda index: ex.free_var(get_column_left[self.value_columns[index]]) + lambda index: ex.deref(get_column_left[self.value_columns[index]]) if index != -1 else ex.const(None) ) righ_input_lookup = ( - lambda index: ex.free_var(get_column_right[other.value_columns[index]]) + lambda index: ex.deref(get_column_right[other.value_columns[index]]) if index != -1 else ex.const(None) ) @@ -2085,8 +2088,8 @@ def _align_axis_0( series_column_id = other.value_columns[0] inputs = tuple( ( - ex.free_var(get_column_left[col]), - ex.free_var(get_column_right[series_column_id]), + ex.deref(get_column_left[col]), + ex.deref(get_column_right[series_column_id]), ) for col in self.value_columns ) @@ -2121,12 +2124,12 @@ def _align_series_block_axis_1( ) left_input_lookup = ( - lambda index: ex.free_var(get_column_left[self.value_columns[index]]) + lambda index: ex.deref(get_column_left[self.value_columns[index]]) if index != -1 else ex.const(None) ) righ_input_lookup = ( - lambda index: ex.free_var( + lambda index: ex.deref( get_column_right[other.transpose().value_columns[index]] ) if index != -1 @@ -2156,7 +2159,7 @@ def _align_pd_series_axis_1( ) left_input_lookup = ( - lambda index: ex.free_var(self.value_columns[index]) + lambda index: ex.deref(self.value_columns[index]) if index != -1 else ex.const(None) ) @@ -2706,7 +2709,7 @@ def join_mono_indexed( if sort: combined_expr = combined_expr.order_by( [ - ordering.OrderingExpression(ex.free_var(col_id)) + ordering.OrderingExpression(ex.deref(col_id)) for col_id in coalesced_join_cols ] ) @@ -2769,7 +2772,7 @@ def join_multi_indexed( if sort: combined_expr = combined_expr.order_by( [ - ordering.OrderingExpression(ex.free_var(col_id)) + ordering.OrderingExpression(ex.deref(col_id)) for col_id in coalesced_join_cols ] ) @@ -3008,7 +3011,7 @@ def unpivot( *( ( ops.eq_op.as_expr(explode_offsets_id, ex.const(i)), - ex.free_var(column_mapping[id_or_null]) + ex.deref(column_mapping[id_or_null]) if (id_or_null is not None) else ex.const(None), ) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 69730912966..7be7013cfc0 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -36,6 +36,7 @@ import bigframes.core.compile.scalar_op_compiler as op_compilers import bigframes.core.expression as ex import bigframes.core.guid +import bigframes.core.identifiers as ids from bigframes.core.ordering import ( ascending_over, encode_order_string, @@ -142,12 +143,12 @@ def projection( def selection( self: T, - input_output_pairs: typing.Tuple[typing.Tuple[str, str], ...], + input_output_pairs: typing.Tuple[typing.Tuple[ex.DerefOp, str], ...], ) -> T: """Apply an expression to the ArrayValue and assign the output to a column.""" bindings = {col: self._get_ibis_column(col) for col in self.column_ids} values = [ - op_compiler.compile_expression(ex.free_var(input), bindings).name(id) + op_compiler.compile_expression(input, bindings).name(id) for input, id in input_output_pairs ] result = self._select(tuple(values)) # type: ignore @@ -184,7 +185,7 @@ def _aggregate_base( table: ibis_types.Table, order_by: typing.Sequence[ibis_types.Value] = [], aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]] = [], - by_column_ids: typing.Sequence[str] = (), + by_column_ids: typing.Sequence[ex.DerefOp] = (), dropna: bool = True, ) -> OrderedIR: assert not self.is_ordered_ir or len(order_by) > 0 @@ -197,17 +198,21 @@ def _aggregate_base( for aggregate, col_out in aggregations } if by_column_ids: - result = table.group_by(by_column_ids).aggregate(**stats) + result = table.group_by((ref.id.sql for ref in by_column_ids)).aggregate( + **stats + ) # Must have deterministic ordering, so order by the unique "by" column ordering = TotalOrdering( - tuple([ascending_over(column_id) for column_id in by_column_ids]), - total_ordering_columns=frozenset(by_column_ids), + tuple([OrderingExpression(column_id) for column_id in by_column_ids]), + total_ordering_columns=frozenset( + [ex.DerefOp(ref.id.local_normalized) for ref in by_column_ids] + ), ) columns = tuple(result[key] for key in result.columns) expr = OrderedIR(result, columns=columns, ordering=ordering) if dropna: - for column_id in by_column_ids: - expr = expr._filter(expr._get_ibis_column(column_id).notnull()) + for ref in by_column_ids: + expr = expr._filter(expr._compile_expression(ref).notnull()) return expr else: aggregates = {**stats, ORDER_ID_COLUMN: ibis_types.literal(0)} @@ -286,7 +291,7 @@ def row_count(self) -> OrderedIR: (ibis_table["count"],), ordering=TotalOrdering( ordering_value_columns=(ascending_over("count"),), - total_ordering_columns=frozenset(["count"]), + total_ordering_columns=frozenset([ex.deref("count")]), ), ) @@ -351,10 +356,13 @@ def _to_ibis_expr( return table def filter(self, predicate: ex.Expression) -> UnorderedIR: - if any(map(is_window, map(self._get_ibis_column, predicate.unbound_variables))): - # ibis doesn't support qualify syntax, so create CTE if filtering over window expression - # https://github.com/ibis-project/ibis/issues/9775 - return self._reproject_to_table().filter(predicate) + for ref in predicate.column_references: + ibis_value = self._get_ibis_column(ref.sql) + if is_window(ibis_value): + # ibis doesn't support qualify syntax, so create CTE if filtering over window expression + # https://github.com/ibis-project/ibis/issues/9775 + return self._reproject_to_table().filter(predicate) + bindings = {col: self._get_ibis_column(col) for col in self.column_ids} condition = op_compiler.compile_expression(predicate, bindings) return self._filter(condition) @@ -368,7 +376,7 @@ def _filter(self, predicate_value: ibis_types.BooleanValue) -> UnorderedIR: def aggregate( self, aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]], - by_column_ids: typing.Sequence[str] = (), + by_column_ids: typing.Sequence[ex.DerefOp] = (), dropna: bool = True, ) -> OrderedIR: """ @@ -400,9 +408,9 @@ def _uniform_sampling(self, fraction: float) -> UnorderedIR: columns=columns, ) - def explode(self, offsets: typing.Sequence[int]) -> UnorderedIR: + def explode(self, columns: typing.Sequence[ex.DerefOp]) -> UnorderedIR: table = self._to_ibis_expr() - column_ids = tuple(table.columns[offset] for offset in offsets) + column_ids = tuple(ref.id.sql for ref in columns) # The offset array ensures null represents empty arrays after unnesting. offset_array_id = bigframes.core.guid.generate_guid("offset_array_") @@ -555,7 +563,9 @@ def __init__( all_columns = value_col_ids | hidden_col_ids ordering_valid = all( - set(col.scalar_expression.unbound_variables).issubset(all_columns) + set(ref.sql for ref in col.scalar_expression.column_references).issubset( + all_columns + ) for col in ordering.all_ordering_columns ) if value_col_ids & hidden_col_ids: @@ -656,7 +666,7 @@ def reversed(self) -> OrderedIR: def aggregate( self, aggregations: typing.Sequence[typing.Tuple[ex.Aggregation, str]], - by_column_ids: typing.Sequence[str] = (), + by_column_ids: typing.Sequence[ex.DerefOp] = (), dropna: bool = True, ) -> OrderedIR: """ @@ -711,9 +721,9 @@ def _uniform_sampling(self, fraction: float) -> OrderedIR: ordering=self._ordering, ) - def explode(self, offsets: typing.Sequence[int]) -> OrderedIR: + def explode(self, columns: typing.Sequence[ex.DerefOp]) -> OrderedIR: table = self._to_ibis_expr(ordering_mode="unordered", expose_hidden_cols=True) - column_ids = tuple(table.columns[offset] for offset in offsets) + column_ids = tuple(ref.id.sql for ref in columns) offset_array_id = bigframes.core.guid.generate_guid("offset_array_") offset_array = ibis.range( @@ -761,7 +771,7 @@ def explode(self, offsets: typing.Sequence[int]) -> OrderedIR: table_w_unnest[unnest_offset_id], ] l_mappings = {id: id for id in self._ordering.referenced_columns} - r_mappings = {unnest_offset_id: unnest_offset_id} + r_mappings = {ids.ColumnId(unnest_offset_id): ids.ColumnId(unnest_offset_id)} ordering = join_orderings( self._ordering, TotalOrdering.from_offset_col(unnest_offset_id), @@ -796,7 +806,10 @@ def promote_offsets(self, col_id: str) -> OrderedIR: # Also ibis cannot window literals, so need to reproject those (even though this is legal in googlesql) # Seee: https://github.com/ibis-project/ibis/issues/9773 can_directly_window = not any( - map(lambda x: is_literal(x) or is_window(x), self._ibis_order) + map( + lambda x: is_literal(x) or is_window(x), + itertools.chain(self._ibis_order, self._predicates), + ) ) if not can_directly_window: return self._reproject_to_table().promote_offsets(col_id) @@ -810,12 +823,14 @@ def promote_offsets(self, col_id: str) -> OrderedIR: *self.columns, offsets.name(col_id), ] - return expr_builder.build() + # Reproject, so that offsets are just a scalar value that can be used elsewhere + expr_builder.ordering = TotalOrdering.from_offset_col(col_id) + return expr_builder.build()._reproject_to_table() ## Methods that only work with ordering def project_window_op( self, - column_name: str, + column_name: ex.DerefOp, op: agg_ops.UnaryWindowOp, window_spec: WindowSpec, output_name: str, @@ -834,7 +849,10 @@ def project_window_op( # Also ibis cannot window literals, so need to reproject those (even though this is legal in googlesql) # See: https://github.com/ibis-project/ibis/issues/9773 used_exprs = map( - self._get_any_column, [column_name, *window_spec.all_referenced_columns] + self._compile_expression, + itertools.chain( + (column_name,), map(ex.DerefOp, window_spec.all_referenced_columns) + ), ) can_directly_window = not any( map(lambda x: is_literal(x) or is_window(x), used_exprs) @@ -848,14 +866,16 @@ def project_window_op( never_skip_nulls=never_skip_nulls, ) - column = typing.cast(ibis_types.Column, self._get_ibis_column(column_name)) + column = typing.cast(ibis_types.Column, self._compile_expression(column_name)) window = self._ibis_window_from_spec( window_spec, require_total_order=op.uses_total_row_ordering ) bindings = {col: self._get_ibis_column(col) for col in self.column_ids} window_op = agg_compiler.compile_analytic( - ex.UnaryAggregation(op, ex.free_var(column_name)), window, bindings=bindings + ex.UnaryAggregation(op, column_name), + window, + bindings=bindings, ) clauses = [] @@ -865,7 +885,7 @@ def project_window_op( if op.skips_nulls: # Most operations do not count NULL values towards min_periods observation_count = agg_compiler.compile_analytic( - ex.UnaryAggregation(agg_ops.count_op, ex.free_var(column_name)), + ex.UnaryAggregation(agg_ops.count_op, column_name), window, bindings=bindings, ) @@ -874,7 +894,7 @@ def project_window_op( # notnull is just used to convert null values to non-null (FALSE) values to be counted denulled_value = typing.cast(ibis_types.BooleanColumn, column.notnull()) observation_count = agg_compiler.compile_analytic( - ex.UnaryAggregation(agg_ops.count_op, ex.free_var("_denulled")), + ex.UnaryAggregation(agg_ops.count_op, ex.deref("_denulled")), window, bindings={**bindings, "_denulled": denulled_value}, ) @@ -891,7 +911,7 @@ def project_window_op( case_statement = case_statement.else_(window_op).end() # type: ignore window_op = case_statement - result = self._set_or_replace_by_id(output_name or column_name, window_op) + result = self._set_or_replace_by_id(output_name, window_op) return result def _reproject_to_table(self) -> OrderedIR: @@ -901,8 +921,9 @@ def _reproject_to_table(self) -> OrderedIR: ) columns = [table[column_name] for column_name in self._column_names] ordering_col_ids = list( - itertools.chain.from_iterable( - ref.scalar_expression.unbound_variables + id.sql + for id in itertools.chain.from_iterable( + ref.scalar_expression.column_references for ref in self._ordering.all_ordering_columns ) ) @@ -1069,10 +1090,13 @@ def _to_ibis_expr( return table def filter(self, predicate: ex.Expression) -> OrderedIR: - if any(map(is_window, map(self._get_ibis_column, predicate.unbound_variables))): - # ibis doesn't support qualify syntax, so create CTE if filtering over window expression - # https://github.com/ibis-project/ibis/issues/9775 - return self._reproject_to_table().filter(predicate) + for ref in predicate.column_references: + ibis_value = self._get_ibis_column(ref.sql) + if is_window(ibis_value): + # ibis doesn't support qualify syntax, so create CTE if filtering over window expression + # https://github.com/ibis-project/ibis/issues/9775 + return self._reproject_to_table().filter(predicate) + bindings = {col: self._get_ibis_column(col) for col in self.column_ids} condition = op_compiler.compile_expression(predicate, bindings) return self._filter(condition) @@ -1088,8 +1112,9 @@ def _set_or_replace_by_id(self, id: str, new_value: ibis_types.Value) -> Ordered """Safely assign by id while maintaining ordering integrity.""" # TODO: Split into explicit set and replace methods ordering_col_ids = set( - itertools.chain.from_iterable( - col_ref.scalar_expression.unbound_variables + id.sql + for id in itertools.chain.from_iterable( + col_ref.scalar_expression.column_references for col_ref in self._ordering.ordering_value_columns ) ) @@ -1110,15 +1135,16 @@ def _select(self, values: typing.Tuple[ibis_types.Value]) -> OrderedIR: """Safely assign by id while maintaining ordering integrity.""" # TODO: Split into explicit set and replace methods ordering_col_ids = set( - itertools.chain.from_iterable( + id.sql + for id in itertools.chain.from_iterable( [ - col_ref.scalar_expression.unbound_variables + col_ref.scalar_expression.column_references for col_ref in self._ordering.ordering_value_columns ] ) ) ir = self - mappings = {value.name: value for value in values} + mappings = {typing.cast(str, value.get_name()): value for value in values} for ordering_id in ordering_col_ids: # Drop case if (ordering_id not in mappings) and (ordering_id in ir.column_ids): @@ -1156,17 +1182,25 @@ def _get_hidden_ordering_column(self, key: str) -> ibis_types.Column: ) return typing.cast(ibis_types.Column, self._hidden_ordering_column_names[key]) - def _hide_column(self, column_id) -> OrderedIR: + def _hide_column(self, column_id: str) -> OrderedIR: """Pushes columns to hidden columns list. Used to hide ordering columns that have been dropped or destructively mutated.""" expr_builder = self.builder() # Need to rename column as caller might be creating a new row with the same name but different values. # Can avoid this if don't allow callers to determine ids and instead generate unique ones in this class. - new_name = bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") + new_name = ids.ColumnId( + bigframes.core.guid.generate_guid(prefix="bigframes_hidden_") + ) expr_builder.hidden_ordering_columns = [ *self._hidden_ordering_columns, - self._get_ibis_column(column_id).name(new_name), + self._get_ibis_column(column_id).name(new_name.sql), ] - expr_builder.ordering = self._ordering.with_column_remap({column_id: new_name}) + matching_ref = next( + ref for ref in self._ordering.referenced_columns if ref.sql == column_id + ) + # allow_partial_bindings since only remapping hidden column, not all columns + expr_builder.ordering = self._ordering.remap_column_refs( + {matching_ref: new_name}, allow_partial_bindings=True + ) return expr_builder.build() def _bake_ordering(self) -> OrderedIR: @@ -1181,15 +1215,15 @@ def _bake_ordering(self) -> OrderedIR: ) new_baked_cols.append(baked_column) new_expr = OrderingExpression( - ex.free_var(baked_column.get_name()), expr.direction, expr.na_last + ex.deref(baked_column.get_name()), expr.direction, expr.na_last ) new_exprs.append(new_expr) - elif isinstance(expr.scalar_expression, ex.UnboundVariableExpression): + elif isinstance(expr.scalar_expression, ex.DerefOp): order_col = expr.scalar_expression.id new_exprs.append(expr) - if order_col not in self.column_ids: + if order_col.sql not in self.column_ids: new_baked_cols.append( - self._ibis_bindings[expr.scalar_expression.id] + self._ibis_bindings[expr.scalar_expression.id.sql] ) if isinstance(self._ordering, TotalOrdering): @@ -1275,7 +1309,7 @@ def _ibis_window_from_spec( group_by: typing.List[ibis_types.Value] = ( [ typing.cast( - ibis_types.Column, _as_identity(self._get_ibis_column(column)) + ibis_types.Column, _as_identity(self._compile_expression(column)) ) for column in window_spec.grouping_keys ] diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 74fcaf5f2a5..cbf35196516 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -16,6 +16,7 @@ import dataclasses import functools import io +import itertools import typing import ibis @@ -30,6 +31,7 @@ import bigframes.core.compile.ibis_types import bigframes.core.compile.schema_translator import bigframes.core.compile.single_column +import bigframes.core.expression as ex import bigframes.core.nodes as nodes import bigframes.core.ordering as bf_ordering @@ -75,6 +77,9 @@ def _compile_node( @_compile_node.register def compile_join(self, node: nodes.JoinNode, ordered: bool = True): + condition_pairs = tuple( + (left.id.sql, right.id.sql) for left, right in node.conditions + ) if ordered: # In general, joins are an ordering destroying operation. # With ordering_mode = "partial", make this explicit. In @@ -86,7 +91,7 @@ def compile_join(self, node: nodes.JoinNode, ordered: bool = True): left=left_ordered, right=right_ordered, type=node.type, - conditions=node.conditions, + conditions=condition_pairs, ) else: left_unordered = self.compile_unordered_ir(node.left_child) @@ -95,7 +100,7 @@ def compile_join(self, node: nodes.JoinNode, ordered: bool = True): left=left_unordered, right=right_unordered, type=node.type, - conditions=node.conditions, + conditions=condition_pairs, ).as_ordered_ir() else: left_unordered = self.compile_unordered_ir(node.left_child) @@ -104,7 +109,7 @@ def compile_join(self, node: nodes.JoinNode, ordered: bool = True): left=left_unordered, right=right_unordered, type=node.type, - conditions=node.conditions, + conditions=condition_pairs, ) @_compile_node.register @@ -121,13 +126,8 @@ def compile_cached_table(self, node: nodes.CachedTableNode, ordered: bool = True full_table_name = ( f"{node.table.project_id}.{node.table.dataset_id}.{node.table.table_id}" ) - used_columns = ( - *node.schema.names, - *node._hidden_columns, - ) - # Physical schema might include unused columns, unsupported datatypes like JSON physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( - list(i for i in node.table.physical_schema if i.name in used_columns) + node.table.physical_schema ) ibis_table = ibis.table(physical_schema, full_table_name) if ordered: @@ -144,9 +144,11 @@ def compile_cached_table(self, node: nodes.CachedTableNode, ordered: bool = True ibis_table, columns=tuple( bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - ibis_table[col] + ibis_table[col.sql] + ) + for col in itertools.chain( + map(lambda x: x.id, node.fields), node._hidden_columns ) - for col in [*node.schema.names, *node._hidden_columns] ), ordering=node.ordering, ) @@ -228,7 +230,7 @@ def compile_read_table_ordered(self, node: nodes.ReadTableNode): ordering: bf_ordering.RowOrdering = bf_ordering.TotalOrdering( ordering_value_columns, integer_encoding=integer_encoding, - total_ordering_columns=frozenset(node.total_order_cols), + total_ordering_columns=frozenset(map(ex.deref, node.total_order_cols)), ) hidden_columns = () elif self.strict: @@ -260,7 +262,7 @@ def compile_read_table_ordered(self, node: nodes.ReadTableNode): def compile_promote_offsets( self, node: nodes.PromoteOffsetsNode, ordered: bool = True ): - result = self.compile_ordered_ir(node.child).promote_offsets(node.col_id) + result = self.compile_ordered_ir(node.child).promote_offsets(node.col_id.sql) return result if ordered else result.to_unordered() @_compile_node.register @@ -284,12 +286,14 @@ def compile_reversed(self, node: nodes.ReversedNode, ordered: bool = True): @_compile_node.register def compile_selection(self, node: nodes.SelectionNode, ordered: bool = True): result = self.compile_node(node.child, ordered) - return result.selection(node.input_output_pairs) + selection = tuple((ref, id.sql) for ref, id in node.input_output_pairs) + return result.selection(selection) @_compile_node.register def compile_projection(self, node: nodes.ProjectionNode, ordered: bool = True): result = self.compile_node(node.child, ordered) - return result.projection(node.assignments) + projections = ((expr, id.sql) for expr, id in node.assignments) + return result.projection(tuple(projections)) @_compile_node.register def compile_concat(self, node: nodes.ConcatNode, ordered: bool = True): @@ -312,13 +316,14 @@ def compile_aggregate(self, node: nodes.AggregateNode, ordered: bool = True): has_ordered_aggregation_ops = any( aggregate.op.can_order_by for aggregate, _ in node.aggregations ) + aggs = tuple((agg, id.sql) for agg, id in node.aggregations) if ordered and has_ordered_aggregation_ops: return self.compile_ordered_ir(node.child).aggregate( - node.aggregations, node.by_column_ids, node.dropna + aggs, node.by_column_ids, node.dropna ) else: result = self.compile_unordered_ir(node.child).aggregate( - node.aggregations, node.by_column_ids, node.dropna + aggs, node.by_column_ids, node.dropna ) return result if ordered else result.to_unordered() @@ -328,7 +333,7 @@ def compile_window(self, node: nodes.WindowOpNode, ordered: bool = True): node.column_name, node.op, node.window_spec, - node.output_name, + node.output_name.sql, never_skip_nulls=node.never_skip_nulls, ) return result if ordered else result.to_unordered() diff --git a/bigframes/core/compile/concat.py b/bigframes/core/compile/concat.py index 35cf9ec5b46..81d6805d22b 100644 --- a/bigframes/core/compile/concat.py +++ b/bigframes/core/compile/concat.py @@ -19,6 +19,7 @@ import ibis import bigframes.core.compile.compiled as compiled +import bigframes.core.expression as ex from bigframes.core.ordering import ( ascending_over, reencode_order_string, @@ -85,7 +86,7 @@ def concat_ordered( combined_table = ibis.union(*tables) ordering = TotalOrdering( ordering_value_columns=tuple([ascending_over(ORDER_ID_COLUMN)]), - total_ordering_columns=frozenset([ORDER_ID_COLUMN]), + total_ordering_columns=frozenset([ex.deref(ORDER_ID_COLUMN)]), string_encoding=StringEncoding(True, prefix_size + max_encoding_size), ) return compiled.OrderedIR( diff --git a/bigframes/core/compile/default_ordering.py b/bigframes/core/compile/default_ordering.py index a6b625caca6..910f822d638 100644 --- a/bigframes/core/compile/default_ordering.py +++ b/bigframes/core/compile/default_ordering.py @@ -27,6 +27,7 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types +import bigframes.core.expression as ex import bigframes.core.guid as guid import bigframes.core.ordering as order @@ -91,6 +92,8 @@ def gen_default_ordering(table: ibis.table, use_double_hash: bool = True): ordering_value_columns=tuple( order.ascending_over(col.get_name()) for col in order_values ), - total_ordering_columns=frozenset(col.get_name() for col in order_values), + total_ordering_columns=frozenset( + ex.deref(col.get_name()) for col in order_values + ), ) return table_with_ordering, ordering diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 799a408d5b5..78181744660 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -86,13 +86,13 @@ def _( @compile_expression.register def _( self, - expression: ex.UnboundVariableExpression, + expression: ex.DerefOp, bindings: typing.Dict[str, ibis_types.Value], ) -> ibis_types.Value: - if expression.id not in bindings: + if expression.id.sql not in bindings: raise ValueError(f"Could not resolve unbound variable {expression.id}") else: - return bindings[expression.id] + return bindings[expression.id.sql] @compile_expression.register def _( diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index 325df8e180a..48fa52974ed 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -93,8 +93,8 @@ def join_by_column_ordered( ordering = orderings.join_orderings( left._ordering, right._ordering, - l_mapping, - r_mapping, + {ids.ColumnId(lin): ids.ColumnId(lout) for lin, lout in l_mapping.items()}, + {ids.ColumnId(rin): ids.ColumnId(rout) for rin, rout in r_mapping.items()}, left_order_dominates=(type != "right"), ) diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index 4779e92cdec..cf7001a455d 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -20,6 +20,7 @@ import typing from typing import Mapping, Union +import bigframes.core.identifiers as ids import bigframes.dtypes as dtypes import bigframes.operations import bigframes.operations.aggregations as agg_ops @@ -31,6 +32,10 @@ def const( return ScalarConstantExpression(value, dtype or dtypes.infer_literal_type(value)) +def deref(name: str) -> DerefOp: + return DerefOp(ids.ColumnId(name)) + + def free_var(id: str) -> UnboundVariableExpression: return UnboundVariableExpression(id) @@ -43,7 +48,7 @@ class Aggregation(abc.ABC): @abc.abstractmethod def output_type( - self, input_types: dict[str, dtypes.ExpressionType] + self, input_types: dict[ids.ColumnId, dtypes.ExpressionType] ) -> dtypes.ExpressionType: ... @@ -53,7 +58,7 @@ class NullaryAggregation(Aggregation): op: agg_ops.NullaryWindowOp = dataclasses.field() def output_type( - self, input_types: dict[str, bigframes.dtypes.Dtype] + self, input_types: dict[ids.ColumnId, bigframes.dtypes.Dtype] ) -> dtypes.ExpressionType: return self.op.output_type() @@ -61,12 +66,10 @@ def output_type( @dataclasses.dataclass(frozen=True) class UnaryAggregation(Aggregation): op: agg_ops.UnaryWindowOp = dataclasses.field() - arg: Union[ - UnboundVariableExpression, ScalarConstantExpression - ] = dataclasses.field() + arg: Union[DerefOp, ScalarConstantExpression] = dataclasses.field() def output_type( - self, input_types: dict[str, bigframes.dtypes.Dtype] + self, input_types: dict[ids.ColumnId, bigframes.dtypes.Dtype] ) -> dtypes.ExpressionType: return self.op.output_type(self.arg.output_type(input_types)) @@ -74,15 +77,11 @@ def output_type( @dataclasses.dataclass(frozen=True) class BinaryAggregation(Aggregation): op: agg_ops.BinaryAggregateOp = dataclasses.field() - left: Union[ - UnboundVariableExpression, ScalarConstantExpression - ] = dataclasses.field() - right: Union[ - UnboundVariableExpression, ScalarConstantExpression - ] = dataclasses.field() + left: Union[DerefOp, ScalarConstantExpression] = dataclasses.field() + right: Union[DerefOp, ScalarConstantExpression] = dataclasses.field() def output_type( - self, input_types: dict[str, bigframes.dtypes.Dtype] + self, input_types: dict[ids.ColumnId, bigframes.dtypes.Dtype] ) -> dtypes.ExpressionType: return self.op.output_type( self.left.output_type(input_types), self.right.output_type(input_types) @@ -94,11 +93,19 @@ class Expression(abc.ABC): """An expression represents a computation taking N scalar inputs and producing a single output scalar.""" @property - def unbound_variables(self) -> typing.Tuple[str, ...]: + def free_variables(self) -> typing.Tuple[str, ...]: return () - def rename(self, name_mapping: Mapping[str, str]) -> Expression: - return self + @property + def column_references(self) -> typing.Tuple[ids.ColumnId, ...]: + return () + + def remap_column_refs( + self, name_mapping: Mapping[ids.ColumnId, ids.ColumnId] + ) -> Expression: + return self.bind_refs( + {old_id: DerefOp(new_id) for old_id, new_id in name_mapping.items()} + ) @property @abc.abstractmethod @@ -107,17 +114,29 @@ def is_const(self) -> bool: @abc.abstractmethod def output_type( - self, input_types: dict[str, dtypes.ExpressionType] + self, input_types: dict[ids.ColumnId, dtypes.ExpressionType] ) -> dtypes.ExpressionType: ... + @abc.abstractmethod + def bind_refs( + self, + bindings: Mapping[ids.ColumnId, Expression], + allow_partial_bindings: bool = False, + ) -> Expression: + """Replace variables with expression given in `bindings`. + + If allow_partial_bindings is False, validate that all free variables are bound to a new value. + """ + ... + @abc.abstractmethod def bind_variables( - self, bindings: Mapping[str, Expression], check_bind_all: bool = True + self, bindings: Mapping[str, Expression], allow_partial_bindings: bool = False ) -> Expression: """Replace variables with expression given in `bindings`. - If check_bind_all is True, validate that all free variables are bound to a new value. + If allow_partial_bindings is False, validate that all free variables are bound to a new value. """ ... @@ -143,19 +162,23 @@ class ScalarConstantExpression(Expression): def is_const(self) -> bool: return True - def rename(self, name_mapping: Mapping[str, str]) -> ScalarConstantExpression: - return self - def output_type( - self, input_types: dict[str, bigframes.dtypes.Dtype] + self, input_types: dict[ids.ColumnId, bigframes.dtypes.Dtype] ) -> dtypes.ExpressionType: return self.dtype def bind_variables( - self, bindings: Mapping[str, Expression], check_bind_all: bool = True + self, bindings: Mapping[str, Expression], allow_partial_bindings: bool = False ) -> Expression: return self + def bind_refs( + self, + bindings: Mapping[ids.ColumnId, Expression], + allow_partial_bindings: bool = False, + ) -> ScalarConstantExpression: + return self + @property def is_bijective(self) -> bool: # () <-> value @@ -169,21 +192,59 @@ class UnboundVariableExpression(Expression): id: str @property - def unbound_variables(self) -> typing.Tuple[str, ...]: + def free_variables(self) -> typing.Tuple[str, ...]: return (self.id,) - def rename(self, name_mapping: Mapping[str, str]) -> UnboundVariableExpression: - if self.id in name_mapping: - return UnboundVariableExpression(name_mapping[self.id]) - else: - return self + @property + def is_const(self) -> bool: + return False + + def output_type( + self, input_types: dict[ids.ColumnId, bigframes.dtypes.Dtype] + ) -> dtypes.ExpressionType: + raise ValueError(f"Type of variable {self.id} has not been fixed.") + + def bind_refs( + self, + bindings: Mapping[ids.ColumnId, Expression], + allow_partial_bindings: bool = False, + ) -> UnboundVariableExpression: + return self + + def bind_variables( + self, bindings: Mapping[str, Expression], allow_partial_bindings: bool = False + ) -> Expression: + if self.id in bindings.keys(): + return bindings[self.id] + elif not allow_partial_bindings: + raise ValueError(f"Variable {self.id} remains unbound") + return self + + @property + def is_bijective(self) -> bool: + return True + + @property + def is_identity(self) -> bool: + return True + + +@dataclasses.dataclass(frozen=True) +class DerefOp(Expression): + """A variable expression representing an unbound variable.""" + + id: ids.ColumnId + + @property + def column_references(self) -> typing.Tuple[ids.ColumnId, ...]: + return (self.id,) @property def is_const(self) -> bool: return False def output_type( - self, input_types: dict[str, bigframes.dtypes.Dtype] + self, input_types: dict[ids.ColumnId, bigframes.dtypes.Dtype] ) -> dtypes.ExpressionType: if self.id in input_types: return input_types[self.id] @@ -191,11 +252,18 @@ def output_type( raise ValueError(f"Type of variable {self.id} has not been fixed.") def bind_variables( - self, bindings: Mapping[str, Expression], check_bind_all: bool = True + self, bindings: Mapping[str, Expression], allow_partial_bindings: bool = False + ) -> Expression: + return self + + def bind_refs( + self, + bindings: Mapping[ids.ColumnId, Expression], + allow_partial_bindings: bool = False, ) -> Expression: if self.id in bindings.keys(): return bindings[self.id] - elif check_bind_all: + elif not allow_partial_bindings: raise ValueError(f"Variable {self.id} remains unbound") return self @@ -216,16 +284,19 @@ class OpExpression(Expression): inputs: typing.Tuple[Expression, ...] @property - def unbound_variables(self) -> typing.Tuple[str, ...]: + def column_references( + self, + ) -> typing.Tuple[bigframes.core.identifiers.ColumnId, ...]: return tuple( itertools.chain.from_iterable( - map(lambda x: x.unbound_variables, self.inputs) + map(lambda x: x.column_references, self.inputs) ) ) - def rename(self, name_mapping: Mapping[str, str]) -> Expression: - return OpExpression( - self.op, tuple(input.rename(name_mapping) for input in self.inputs) + @property + def free_variables(self) -> typing.Tuple[str, ...]: + return tuple( + itertools.chain.from_iterable(map(lambda x: x.free_variables, self.inputs)) ) @property @@ -233,7 +304,7 @@ def is_const(self) -> bool: return all(child.is_const for child in self.inputs) def output_type( - self, input_types: dict[str, dtypes.ExpressionType] + self, input_types: dict[ids.ColumnId, dtypes.ExpressionType] ) -> dtypes.ExpressionType: operand_types = tuple( map(lambda x: x.output_type(input_types=input_types), self.inputs) @@ -241,12 +312,27 @@ def output_type( return self.op.output_type(*operand_types) def bind_variables( - self, bindings: Mapping[str, Expression], check_bind_all: bool = True - ) -> Expression: + self, bindings: Mapping[str, Expression], allow_partial_bindings: bool = False + ) -> OpExpression: + return OpExpression( + self.op, + tuple( + input.bind_variables( + bindings, allow_partial_bindings=allow_partial_bindings + ) + for input in self.inputs + ), + ) + + def bind_refs( + self, + bindings: Mapping[ids.ColumnId, Expression], + allow_partial_bindings: bool = False, + ) -> OpExpression: return OpExpression( self.op, tuple( - input.bind_variables(bindings, check_bind_all=check_bind_all) + input.bind_refs(bindings, allow_partial_bindings=allow_partial_bindings) for input in self.inputs ), ) diff --git a/bigframes/core/identifiers.py b/bigframes/core/identifiers.py index 9239c41248f..0d2aaeb07cd 100644 --- a/bigframes/core/identifiers.py +++ b/bigframes/core/identifiers.py @@ -11,16 +11,37 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations -# Later, plan on migrating ids to use integers to reduce memory usage allow use of bitmaps to represent column sets - +import dataclasses +import functools from typing import Generator -ID_TYPE = str - -def standard_identifiers() -> Generator[ID_TYPE, None, None]: +def standard_identifiers() -> Generator[str, None, None]: i = 0 while True: yield f"col_{i}" i = i + 1 + + +# Used for expression trees +@functools.total_ordering +@dataclasses.dataclass(frozen=True) +class ColumnId: + """Local id without plan-wide id.""" + + name: str + + @property + def sql(self) -> str: + """Returns the unescaped SQL name.""" + return self.name + + @property + def local_normalized(self) -> ColumnId: + """For use in compiler only. Normalizes to ColumnId referring to sql name.""" + return self # == ColumnId(name=self.sql) + + def __lt__(self, other: ColumnId) -> bool: + return self.name < other.name diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 017702b85aa..0ba79bebee1 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -327,7 +327,9 @@ def astype( ) -> Index: if self.nlevels > 1: raise TypeError("Multiindex does not support 'astype'") - return self._apply_unary_expr(ops.AsTypeOp(to_type=dtype).as_expr("arg")) + return self._apply_unary_expr( + ops.AsTypeOp(to_type=dtype).as_expr(ex.free_var("arg")) + ) def all(self) -> bool: if self.nlevels > 1: @@ -396,7 +398,9 @@ def value_counts( def fillna(self, value=None) -> Index: if self.nlevels > 1: raise TypeError("Multiindex does not support 'fillna'") - return self._apply_unary_expr(ops.fillna_op.as_expr("arg", ex.const(value))) + return self._apply_unary_expr( + ops.fillna_op.as_expr(ex.free_var("arg"), ex.const(value)) + ) def rename(self, name: Union[str, Sequence[str]]) -> Index: names = [name] if isinstance(name, str) else list(name) @@ -446,7 +450,9 @@ def isin(self, values) -> Index: ) return self._apply_unary_expr( - ops.IsInOp(values=tuple(values), match_nulls=True).as_expr("arg") + ops.IsInOp(values=tuple(values), match_nulls=True).as_expr( + ex.free_var("arg") + ) ).fillna(value=False) def _apply_unary_expr( @@ -454,14 +460,16 @@ def _apply_unary_expr( op: ex.Expression, ) -> Index: """Applies a unary operator to the index.""" - if len(op.unbound_variables) != 1: + if len(op.free_variables) != 1: raise ValueError("Expression must have exactly 1 unbound variable.") - unbound_variable = op.unbound_variables[0] + unbound_variable = op.free_variables[0] block = self._block result_ids = [] for col in self._block.index_columns: - block, result_id = block.project_expr(op.rename({unbound_variable: col})) + block, result_id = block.project_expr( + op.bind_variables({unbound_variable: ex.deref(col)}) + ) result_ids.append(result_id) block = block.set_index(result_ids, index_labels=self._block.index.names) diff --git a/bigframes/core/join_def.py b/bigframes/core/join_def.py index 5b7b7e45dda..cd9c2acd174 100644 --- a/bigframes/core/join_def.py +++ b/bigframes/core/join_def.py @@ -17,8 +17,6 @@ import enum from typing import Literal, NamedTuple -import bigframes.core.identifiers as ids - class JoinSide(enum.Enum): LEFT = 0 @@ -34,21 +32,21 @@ def inverse(self) -> JoinSide: class JoinCondition(NamedTuple): - left_id: ids.ID_TYPE - right_id: ids.ID_TYPE + left_id: str + right_id: str @dataclasses.dataclass(frozen=True) class JoinColumnMapping: source_table: JoinSide - source_id: ids.ID_TYPE - destination_id: ids.ID_TYPE + source_id: str + destination_id: str @dataclasses.dataclass(frozen=True) class CoalescedColumnMapping: """Special column mapping used only by implicit joiner only""" - left_source_id: ids.ID_TYPE - right_source_id: ids.ID_TYPE - destination_id: ids.ID_TYPE + left_source_id: str + right_source_id: str + destination_id: str diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 93b59f75eef..2649b41227c 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -26,6 +26,7 @@ import bigframes.core.expression as ex import bigframes.core.guid +import bigframes.core.identifiers import bigframes.core.identifiers as bfet_ids from bigframes.core.ordering import OrderingExpression import bigframes.core.schema as schemata @@ -42,7 +43,10 @@ OVERHEAD_VARIABLES = 5 -COL_OFFSET = int +@dataclass(frozen=True) +class Field: + id: bfet_ids.ColumnId + dtype: bigframes.dtypes.Dtype @dataclass(frozen=True) @@ -103,9 +107,10 @@ def roots(self) -> typing.Set[BigFrameNode]: ) return set(roots) + # TODO: For deep trees, this can create a lot of overhead, maybe use zero-copy persistent datastructure? @property @abc.abstractmethod - def schema(self) -> schemata.ArraySchema: + def fields(self) -> Tuple[Field, ...]: ... @property @@ -162,6 +167,13 @@ def total_relational_ops(self) -> int: def total_joins(self) -> int: return int(self.joins) + sum(map(lambda x: x.total_joins, self.child_nodes)) + @functools.cached_property + def schema(self) -> schemata.ArraySchema: + # TODO: Make schema just a view on fields + return schemata.ArraySchema( + tuple(schemata.SchemaItem(i.id.name, i.dtype) for i in self.fields) + ) + @property def planning_complexity(self) -> int: """ @@ -197,6 +209,13 @@ def defined_variables(self) -> set[str]: *(child.defined_variables for child in self.child_nodes) ) + def get_type(self, id: bfet_ids.ColumnId) -> bigframes.dtypes.Dtype: + return self._dtype_lookup[id] + + @functools.cached_property + def _dtype_lookup(self): + return {field.id: field.dtype for field in self.fields} + @dataclass(frozen=True) class UnaryNode(BigFrameNode): @@ -207,8 +226,8 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: return (self.child,) @functools.cached_property - def schema(self) -> schemata.ArraySchema: - return self.child.schema + def fields(self) -> Tuple[Field, ...]: + return self.child.fields @property def explicitly_ordered(self) -> bool: @@ -228,7 +247,7 @@ def order_ambiguous(self) -> bool: class JoinNode(BigFrameNode): left_child: BigFrameNode right_child: BigFrameNode - conditions: typing.Tuple[typing.Tuple[str, str], ...] + conditions: typing.Tuple[typing.Tuple[ex.DerefOp, ex.DerefOp], ...] type: typing.Literal["inner", "outer", "left", "right", "cross"] @property @@ -256,15 +275,13 @@ def __hash__(self): return self._node_hash @functools.cached_property - def schema(self) -> schemata.ArraySchema: + def fields(self) -> Tuple[Field, ...]: items = [] - schema_items = itertools.chain( - self.left_child.schema.items, self.right_child.schema.items - ) - identifiers = bfet_ids.standard_identifiers() + schema_items = itertools.chain(self.left_child.fields, self.right_child.fields) + identifiers = map(bfet_ids.ColumnId, bfet_ids.standard_identifiers()) for id, item in zip(identifiers, schema_items): - items.append(schemata.SchemaItem(id, item.dtype)) - return schemata.ArraySchema(tuple(items)) + items.append(Field(id, item.dtype)) + return tuple(items) @functools.cached_property def variables_introduced(self) -> int: @@ -289,6 +306,7 @@ def defines_namespace(self) -> bool: @dataclass(frozen=True) class ConcatNode(BigFrameNode): + # TODO: Explcitly map column ids from each child children: Tuple[BigFrameNode, ...] def __post_init__(self): @@ -315,13 +333,12 @@ def __hash__(self): return self._node_hash @functools.cached_property - def schema(self) -> schemata.ArraySchema: + def fields(self) -> Tuple[Field, ...]: # TODO: Output names should probably be aligned beforehand or be part of concat definition - items = tuple( - schemata.SchemaItem(f"column_{i}", dtype) - for i, dtype in enumerate(self.children[0].schema.dtypes) + return tuple( + Field(bfet_ids.ColumnId(f"column_{i}"), field.dtype) + for i, field in enumerate(self.children[0].fields) ) - return schemata.ArraySchema(items) @functools.cached_property def variables_introduced(self) -> int: @@ -335,6 +352,8 @@ def transform_children( # Input Nodex +# TODO: Most leaf nodes produce fixed column names based on the datasource +# They should support renaming @dataclass(frozen=True) class LeafNode(BigFrameNode): @property @@ -367,8 +386,10 @@ def __hash__(self): return self._node_hash @functools.cached_property - def schema(self) -> schemata.ArraySchema: - return self.data_schema + def fields(self) -> Tuple[Field, ...]: + return tuple( + Field(bfet_ids.ColumnId(i.column), i.dtype) for i in self.data_schema.items + ) @functools.cached_property def variables_introduced(self) -> int: @@ -449,9 +470,11 @@ def session(self): def __hash__(self): return self._node_hash - @property - def schema(self) -> schemata.ArraySchema: - return self.columns + @functools.cached_property + def fields(self) -> Tuple[Field, ...]: + return tuple( + Field(bfet_ids.ColumnId(i.column), i.dtype) for i in self.columns.items + ) @property def relation_ops_created(self) -> int: @@ -502,10 +525,6 @@ def __post_init__(self): raise ValueError( f"Requested schema {logical_names} cannot be derived from table schema {self.table.physical_schema}" ) - if not set(self._hidden_columns).issubset(physical_names): - raise ValueError( - f"Requested hidden columns {self._hidden_columns} cannot be derived from table schema {self.table.physical_schema}" - ) @property def session(self): @@ -515,22 +534,23 @@ def __hash__(self): return self._node_hash @property - def schema(self) -> schemata.ArraySchema: - return self.original_node.schema + def fields(self) -> Tuple[Field, ...]: + return self.original_node.fields @functools.cached_property def variables_introduced(self) -> int: - return len(self.schema.items) + OVERHEAD_VARIABLES + return len(self.fields) + OVERHEAD_VARIABLES @property - def _hidden_columns(self) -> typing.Tuple[str, ...]: + def _hidden_columns(self) -> typing.Tuple[bfet_ids.ColumnId, ...]: """Physical columns used to define ordering but not directly exposed as value columns.""" + field_names = set(field.id for field in self.fields) if self.ordering is None: return () return tuple( col for col in sorted(self.ordering.referenced_columns) - if col not in self.schema.names + if col not in field_names ) @property @@ -558,7 +578,7 @@ def row_count(self) -> typing.Optional[int]: # Unary nodes @dataclass(frozen=True) class PromoteOffsetsNode(UnaryNode): - col_id: str + col_id: bigframes.core.identifiers.ColumnId def __hash__(self): return self._node_hash @@ -568,10 +588,8 @@ def non_local(self) -> bool: return True @property - def schema(self) -> schemata.ArraySchema: - return self.child.schema.append( - schemata.SchemaItem(self.col_id, bigframes.dtypes.INT_DTYPE) - ) + def fields(self) -> Tuple[Field, ...]: + return (*self.child.fields, Field(self.col_id, bigframes.dtypes.INT_DTYPE)) @property def relation_ops_created(self) -> int: @@ -602,15 +620,6 @@ def variables_introduced(self) -> int: class OrderByNode(UnaryNode): by: Tuple[OrderingExpression, ...] - def __post_init__(self): - available_variables = self.child.schema.names - for order_expr in self.by: - for variable in order_expr.scalar_expression.unbound_variables: - if variable not in available_variables: - raise ValueError( - f"Cannot over unknown id:{variable}, columns are {available_variables}" - ) - def __hash__(self): return self._node_hash @@ -648,23 +657,19 @@ def relation_ops_created(self) -> int: @dataclass(frozen=True) class SelectionNode(UnaryNode): - input_output_pairs: typing.Tuple[typing.Tuple[str, str], ...] - - def __post_init__(self): - for input, _ in self.input_output_pairs: - assert input in self.child.schema.names + input_output_pairs: typing.Tuple[ + typing.Tuple[ex.DerefOp, bigframes.core.identifiers.ColumnId], ... + ] def __hash__(self): return self._node_hash @functools.cached_property - def schema(self) -> schemata.ArraySchema: - input_types = self.child.schema._mapping - items = tuple( - schemata.SchemaItem(output, input_types[input]) + def fields(self) -> Tuple[Field, ...]: + return tuple( + Field(output, self.child.get_type(input.id)) for input, output in self.input_output_pairs ) - return schemata.ArraySchema(items) @property def variables_introduced(self) -> int: @@ -683,10 +688,12 @@ def defines_namespace(self) -> bool: class ProjectionNode(UnaryNode): """Assigns new variables (without modifying existing ones)""" - assignments: typing.Tuple[typing.Tuple[ex.Expression, str], ...] + assignments: typing.Tuple[ + typing.Tuple[ex.Expression, bigframes.core.identifiers.ColumnId], ... + ] def __post_init__(self): - input_types = self.child.schema._mapping + input_types = self.child._dtype_lookup for expression, id in self.assignments: # throws TypeError if invalid _ = expression.output_type(input_types) @@ -697,18 +704,13 @@ def __hash__(self): return self._node_hash @functools.cached_property - def schema(self) -> schemata.ArraySchema: - input_types = self.child.schema._mapping - items = tuple( - schemata.SchemaItem( - id, bigframes.dtypes.dtype_for_etype(ex.output_type(input_types)) - ) + def fields(self) -> Tuple[Field, ...]: + input_types = self.child._dtype_lookup + new_fields = ( + Field(id, bigframes.dtypes.dtype_for_etype(ex.output_type(input_types))) for ex, id in self.assignments ) - schema = self.child.schema - for item in items: - schema = schema.append(item) - return schema + return (*self.child.fields, *new_fields) @property def variables_introduced(self) -> int: @@ -730,10 +732,8 @@ def non_local(self) -> bool: return True @functools.cached_property - def schema(self) -> schemata.ArraySchema: - return schemata.ArraySchema( - (schemata.SchemaItem("count", bigframes.dtypes.INT_DTYPE),) - ) + def fields(self) -> Tuple[Field, ...]: + return (Field(bfet_ids.ColumnId("count"), bigframes.dtypes.INT_DTYPE),) @property def variables_introduced(self) -> int: @@ -746,8 +746,10 @@ def defines_namespace(self) -> bool: @dataclass(frozen=True) class AggregateNode(UnaryNode): - aggregations: typing.Tuple[typing.Tuple[ex.Aggregation, str], ...] - by_column_ids: typing.Tuple[str, ...] = tuple([]) + aggregations: typing.Tuple[ + typing.Tuple[ex.Aggregation, bigframes.core.identifiers.ColumnId], ... + ] + by_column_ids: typing.Tuple[ex.DerefOp, ...] = tuple([]) dropna: bool = True @property @@ -762,19 +764,20 @@ def non_local(self) -> bool: return True @functools.cached_property - def schema(self) -> schemata.ArraySchema: - by_items = tuple( - schemata.SchemaItem(id, self.child.schema.get_type(id)) - for id in self.by_column_ids + def fields(self) -> Tuple[Field, ...]: + by_items = ( + Field(ref.id, self.child.get_type(ref.id)) for ref in self.by_column_ids ) - input_types = self.child.schema._mapping - agg_items = tuple( - schemata.SchemaItem( - id, bigframes.dtypes.dtype_for_etype(agg.output_type(input_types)) + agg_items = ( + Field( + id, + bigframes.dtypes.dtype_for_etype( + agg.output_type(self.child._dtype_lookup) + ), ) for agg, id in self.aggregations ) - return schemata.ArraySchema(tuple([*by_items, *agg_items])) + return (*by_items, *agg_items) @property def variables_introduced(self) -> int: @@ -795,10 +798,10 @@ def defines_namespace(self) -> bool: @dataclass(frozen=True) class WindowOpNode(UnaryNode): - column_name: str + column_name: ex.DerefOp op: agg_ops.UnaryWindowOp window_spec: window.WindowSpec - output_name: str + output_name: bigframes.core.identifiers.ColumnId never_skip_nulls: bool = False skip_reproject_unsafe: bool = False @@ -810,12 +813,10 @@ def non_local(self) -> bool: return True @functools.cached_property - def schema(self) -> schemata.ArraySchema: - input_type = self.child.schema.get_type(self.column_name) + def fields(self) -> Tuple[Field, ...]: + input_type = self.child.get_type(self.column_name.id) new_item_dtype = self.op.output_type(input_type) - return self.child.schema.append( - schemata.SchemaItem(self.output_name, new_item_dtype) - ) + return (*self.child.fields, Field(self.output_name, new_item_dtype)) @property def variables_introduced(self) -> int: @@ -863,9 +864,10 @@ def variables_introduced(self) -> int: return 1 +# TODO: Explode should create a new column instead of overriding the existing one @dataclass(frozen=True) class ExplodeNode(UnaryNode): - column_ids: typing.Tuple[COL_OFFSET, ...] + column_ids: typing.Tuple[ex.DerefOp, ...] @property def row_preserving(self) -> bool: @@ -875,19 +877,18 @@ def __hash__(self): return self._node_hash @functools.cached_property - def schema(self) -> schemata.ArraySchema: - items = tuple( - schemata.SchemaItem( - name, + def fields(self) -> Tuple[Field, ...]: + return tuple( + Field( + field.id, bigframes.dtypes.arrow_dtype_to_bigframes_dtype( - self.child.schema.get_type(name).pyarrow_dtype.value_type + self.child.get_type(field.id).pyarrow_dtype.value_type # type: ignore ), ) - if offset in self.column_ids - else schemata.SchemaItem(name, self.child.schema.get_type(name)) - for offset, name in enumerate(self.child.schema.names) + if field.id in set(map(lambda x: x.id, self.column_ids)) + else field + for field in self.child.fields ) - return schemata.ArraySchema(items) @property def relation_ops_created(self) -> int: diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index a57d7a18d63..daf08d5e879 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -24,6 +24,7 @@ import ibis.expr.types as ibis_types import bigframes.core.expression as expression +import bigframes.core.identifiers as ids # TODO(tbergeron): Encode more efficiently ORDERING_ID_STRING_BASE: int = 10 @@ -54,16 +55,25 @@ class OrderingExpression: direction: OrderingDirection = OrderingDirection.ASC na_last: bool = True - def remap_names(self, mapping: Mapping[str, str]) -> OrderingExpression: - return OrderingExpression( - self.scalar_expression.rename(mapping), self.direction, self.na_last + def remap_column_refs( + self, + mapping: Mapping[ids.ColumnId, ids.ColumnId], + allow_partial_bindings: bool = False, + ) -> OrderingExpression: + return self.bind_refs( + {old_id: expression.DerefOp(new_id) for old_id, new_id in mapping.items()}, + allow_partial_bindings=allow_partial_bindings, ) - def bind_variables( - self, mapping: Mapping[str, expression.Expression] + def bind_refs( + self, + mapping: Mapping[ids.ColumnId, expression.Expression], + allow_partial_bindings: bool = False, ) -> OrderingExpression: return OrderingExpression( - self.scalar_expression.bind_variables(mapping), + self.scalar_expression.bind_refs( + mapping, allow_partial_bindings=allow_partial_bindings + ), self.direction, self.na_last, ) @@ -106,11 +116,11 @@ def all_ordering_columns(self) -> Sequence[OrderingExpression]: return list(self.ordering_value_columns) @property - def referenced_columns(self) -> Set[str]: + def referenced_columns(self) -> Set[ids.ColumnId]: return set( col for part in self.ordering_value_columns - for col in part.scalar_expression.unbound_variables + for col in part.scalar_expression.column_references ) @property @@ -133,9 +143,16 @@ def with_reverse(self) -> RowOrdering: tuple([col.with_reverse() for col in self.ordering_value_columns]), ) - def with_column_remap(self, mapping: typing.Mapping[str, str]) -> RowOrdering: + def remap_column_refs( + self, + mapping: typing.Mapping[ids.ColumnId, ids.ColumnId], + allow_partial_bindings: bool = False, + ) -> RowOrdering: new_value_columns = [ - col.remap_names(mapping) for col in self.all_ordering_columns + col.remap_column_refs( + mapping, allow_partial_bindings=allow_partial_bindings + ) + for col in self.all_ordering_columns ] return RowOrdering( tuple(new_value_columns), @@ -182,13 +199,13 @@ def _truncate_ordering( self, order_refs: tuple[OrderingExpression, ...] ) -> tuple[OrderingExpression, ...]: # Truncate once we refer to a full key in bijective operations - columns_seen: Set[str] = set() + columns_seen: Set[ids.ColumnId] = set() truncated_refs = [] for order_part in order_refs: expr = order_part.scalar_expression - if not set(expr.unbound_variables).issubset(columns_seen): + if not set(expr.column_references).issubset(columns_seen): if expr.is_bijective: - columns_seen.update(expr.unbound_variables) + columns_seen.update(expr.column_references) truncated_refs.append(order_part) return tuple(truncated_refs) @@ -200,14 +217,16 @@ class TotalOrdering(RowOrdering): # A table has a total ordering defined by the identities of a set of 1 or more columns. # These columns must always be part of the ordering, in order to guarantee that the ordering is total. # Therefore, any modifications(or drops) done to these columns must result in hidden copies being made. - total_ordering_columns: frozenset[str] = field(default_factory=frozenset) + total_ordering_columns: frozenset[expression.DerefOp] = field( + default_factory=frozenset + ) @classmethod def from_offset_col(cls, col: str) -> TotalOrdering: return TotalOrdering( (ascending_over(col),), integer_encoding=IntegerEncoding(True, is_sequential=True), - total_ordering_columns=frozenset({col}), + total_ordering_columns=frozenset({expression.deref(col)}), ) def with_non_sequential(self): @@ -253,14 +272,14 @@ def _truncate_ordering( self, order_refs: tuple[OrderingExpression, ...] ) -> tuple[OrderingExpression, ...]: # Truncate once we refer to a full key in bijective operations - must_see = set(self.total_ordering_columns) - columns_seen: Set[str] = set() + must_see = set(ref.id for ref in self.total_ordering_columns) + columns_seen: Set[ids.ColumnId] = set() truncated_refs = [] for order_part in order_refs: expr = order_part.scalar_expression - if not set(expr.unbound_variables).issubset(columns_seen): + if not set(expr.column_references).issubset(columns_seen): if expr.is_bijective: - columns_seen.update(expr.unbound_variables) + columns_seen.update(expr.column_references) truncated_refs.append(order_part) if columns_seen.issuperset(must_see): return tuple(truncated_refs) @@ -275,12 +294,20 @@ def with_reverse(self): total_ordering_columns=self.total_ordering_columns, ) - def with_column_remap(self, mapping: typing.Mapping[str, str]): + def remap_column_refs( + self, + mapping: typing.Mapping[ids.ColumnId, ids.ColumnId], + allow_partial_bindings: bool = False, + ): new_value_columns = [ - col.remap_names(mapping) for col in self.all_ordering_columns + col.remap_column_refs( + mapping, allow_partial_bindings=allow_partial_bindings + ) + for col in self.all_ordering_columns ] new_total_order = frozenset( - mapping.get(col_id, col_id) for col_id in self.total_ordering_columns + expression.DerefOp(mapping.get(col_id.id, col_id.id)) + for col_id in self.total_ordering_columns ) return TotalOrdering( tuple(new_value_columns), @@ -326,12 +353,12 @@ def reencode_order_string( # Convenience functions def ascending_over(id: str, nulls_last: bool = True) -> OrderingExpression: - return OrderingExpression(expression.free_var(id), na_last=nulls_last) + return OrderingExpression(expression.deref(id), na_last=nulls_last) def descending_over(id: str, nulls_last: bool = True) -> OrderingExpression: return OrderingExpression( - expression.free_var(id), direction=OrderingDirection.DESC, na_last=nulls_last + expression.deref(id), direction=OrderingDirection.DESC, na_last=nulls_last ) @@ -339,8 +366,8 @@ def descending_over(id: str, nulls_last: bool = True) -> OrderingExpression: def join_orderings( left: TotalOrdering, right: TotalOrdering, - left_id_mapping: Mapping[str, str], - right_id_mapping: Mapping[str, str], + left_id_mapping: Mapping[ids.ColumnId, ids.ColumnId], + right_id_mapping: Mapping[ids.ColumnId, ids.ColumnId], left_order_dominates: bool = True, ) -> TotalOrdering: ... @@ -350,8 +377,8 @@ def join_orderings( def join_orderings( left: RowOrdering, right: RowOrdering, - left_id_mapping: Mapping[str, str], - right_id_mapping: Mapping[str, str], + left_id_mapping: Mapping[ids.ColumnId, ids.ColumnId], + right_id_mapping: Mapping[ids.ColumnId, ids.ColumnId], left_order_dominates: bool = True, ) -> RowOrdering: ... @@ -360,15 +387,15 @@ def join_orderings( def join_orderings( left: RowOrdering, right: RowOrdering, - left_id_mapping: Mapping[str, str], - right_id_mapping: Mapping[str, str], + left_id_mapping: Mapping[ids.ColumnId, ids.ColumnId], + right_id_mapping: Mapping[ids.ColumnId, ids.ColumnId], left_order_dominates: bool = True, ) -> RowOrdering: left_ordering_refs = [ - ref.remap_names(left_id_mapping) for ref in left.all_ordering_columns + ref.remap_column_refs(left_id_mapping) for ref in left.all_ordering_columns ] right_ordering_refs = [ - ref.remap_names(right_id_mapping) for ref in right.all_ordering_columns + ref.remap_column_refs(right_id_mapping) for ref in right.all_ordering_columns ] if left_order_dominates: joined_refs = [*left_ordering_refs, *right_ordering_refs] @@ -377,14 +404,16 @@ def join_orderings( if isinstance(left, TotalOrdering) and isinstance(right, TotalOrdering): left_total_order_cols = frozenset( - [left_id_mapping[id] for id in left.total_ordering_columns] + [left_id_mapping[ref.id] for ref in left.total_ordering_columns] ) right_total_order_cols = frozenset( - [right_id_mapping[id] for id in right.total_ordering_columns] + [right_id_mapping[ref.id] for ref in right.total_ordering_columns] ) return TotalOrdering( ordering_value_columns=tuple(joined_refs), - total_ordering_columns=left_total_order_cols | right_total_order_cols, + total_ordering_columns=frozenset( + map(expression.DerefOp, left_total_order_cols | right_total_order_cols) + ), ) else: return RowOrdering(tuple(joined_refs)) diff --git a/bigframes/core/pruning.py b/bigframes/core/pruning.py index 55165a616ca..2542c8b6f00 100644 --- a/bigframes/core/pruning.py +++ b/bigframes/core/pruning.py @@ -12,11 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Set, TYPE_CHECKING + import bigframes.core.expression as ex -import bigframes.core.schema as schemata +import bigframes.core.identifiers as ids +import bigframes.core.nodes import bigframes.dtypes import bigframes.operations as ops +if TYPE_CHECKING: + import bigframes.core.nodes + + LOW_CARDINALITY_TYPES = [bigframes.dtypes.BOOL_DTYPE] COMPARISON_OP_TYPES = tuple( @@ -34,11 +41,11 @@ def cluster_cols_for_predicate( - predicate: ex.Expression, schema: schemata.ArraySchema -) -> list[str]: + predicate: ex.Expression, clusterable_cols: Set[ids.ColumnId] +) -> list[ids.ColumnId]: """Try to determine cluster col candidates that work with given predicates.""" # TODO: Prioritize based on predicted selectivity (eg. equality conditions are probably very selective) - if isinstance(predicate, ex.UnboundVariableExpression): + if isinstance(predicate, ex.DerefOp): cols = [predicate.id] elif isinstance(predicate, ex.OpExpression): op = predicate.op @@ -47,31 +54,33 @@ def cluster_cols_for_predicate( if isinstance(op, COMPARISON_OP_TYPES): cols = cluster_cols_for_comparison(predicate.inputs[0], predicate.inputs[1]) elif isinstance(op, (type(ops.invert_op))): - cols = cluster_cols_for_predicate(predicate.inputs[0], schema) + cols = cluster_cols_for_predicate(predicate.inputs[0], clusterable_cols) elif isinstance(op, (type(ops.and_op), type(ops.or_op))): - left_cols = cluster_cols_for_predicate(predicate.inputs[0], schema) - right_cols = cluster_cols_for_predicate(predicate.inputs[1], schema) + left_cols = cluster_cols_for_predicate( + predicate.inputs[0], clusterable_cols + ) + right_cols = cluster_cols_for_predicate( + predicate.inputs[1], clusterable_cols + ) cols = [*left_cols, *[col for col in right_cols if col not in left_cols]] else: cols = [] else: # Constant cols = [] - return [ - col for col in cols if bigframes.dtypes.is_clusterable(schema.get_type(col)) - ] + return [col for col in cols if col in clusterable_cols] def cluster_cols_for_comparison( left_ex: ex.Expression, right_ex: ex.Expression -) -> list[str]: +) -> list[ids.ColumnId]: # TODO: Try to normalize expressions such that one side is a single variable. # eg. Convert -cola>=3 to cola<-3 and colb+3 < 4 to colb < 1 if left_ex.is_const: # There are some invertible ops that would also be ok - if isinstance(right_ex, ex.UnboundVariableExpression): + if isinstance(right_ex, ex.DerefOp): return [right_ex.id] elif right_ex.is_const: - if isinstance(left_ex, ex.UnboundVariableExpression): + if isinstance(left_ex, ex.DerefOp): return [left_ex.id] return [] diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index 0e73166ea59..095f537c219 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -19,12 +19,13 @@ from typing import Mapping, Optional, Sequence, Tuple import bigframes.core.expression as scalar_exprs +import bigframes.core.identifiers as ids import bigframes.core.join_def as join_defs import bigframes.core.nodes as nodes import bigframes.core.ordering as order import bigframes.operations as ops -Selection = Tuple[Tuple[scalar_exprs.Expression, str], ...] +Selection = Tuple[Tuple[scalar_exprs.Expression, ids.ColumnId], ...] REWRITABLE_NODE_TYPES = ( nodes.SelectionNode, @@ -40,7 +41,7 @@ class SquashedSelect: """Squash nodes together until target node, separating out the projection, filter and reordering expressions.""" root: nodes.BigFrameNode - columns: Tuple[Tuple[scalar_exprs.Expression, str], ...] + columns: Tuple[Tuple[scalar_exprs.Expression, ids.ColumnId], ...] predicate: Optional[scalar_exprs.Expression] ordering: Tuple[order.OrderingExpression, ...] reverse_root: bool = False @@ -51,8 +52,7 @@ def from_node_span( ) -> SquashedSelect: if node == target: selection = tuple( - (scalar_exprs.UnboundVariableExpression(id), id) - for id in get_node_column_ids(node) + (scalar_exprs.DerefOp(id), id) for id in get_node_column_ids(node) ) return cls(node, selection, None, ()) @@ -72,13 +72,15 @@ def from_node_span( raise ValueError(f"Cannot rewrite node {node}") @property - def column_lookup(self) -> Mapping[str, scalar_exprs.Expression]: + def column_lookup(self) -> Mapping[ids.ColumnId, scalar_exprs.Expression]: return {col_id: expr for expr, col_id in self.columns} - def select(self, input_output_pairs: Tuple[Tuple[str, str], ...]) -> SquashedSelect: + def select( + self, input_output_pairs: Tuple[Tuple[scalar_exprs.DerefOp, ids.ColumnId], ...] + ) -> SquashedSelect: new_columns = tuple( ( - scalar_exprs.free_var(input).bind_variables(self.column_lookup), + input.bind_refs(self.column_lookup), output, ) for input, output in input_output_pairs @@ -88,11 +90,11 @@ def select(self, input_output_pairs: Tuple[Tuple[str, str], ...]) -> SquashedSel ) def project( - self, projection: Tuple[Tuple[scalar_exprs.Expression, str], ...] + self, projection: Tuple[Tuple[scalar_exprs.Expression, ids.ColumnId], ...] ) -> SquashedSelect: existing_columns = self.columns new_columns = tuple( - (expr.bind_variables(self.column_lookup), id) for expr, id in projection + (expr.bind_refs(self.column_lookup), id) for expr, id in projection ) return SquashedSelect( self.root, @@ -104,10 +106,10 @@ def project( def filter(self, predicate: scalar_exprs.Expression) -> SquashedSelect: if self.predicate is None: - new_predicate = predicate.bind_variables(self.column_lookup) + new_predicate = predicate.bind_refs(self.column_lookup) else: new_predicate = ops.and_op.as_expr( - self.predicate, predicate.bind_variables(self.column_lookup) + self.predicate, predicate.bind_refs(self.column_lookup) ) return SquashedSelect( self.root, self.columns, new_predicate, self.ordering, self.reverse_root @@ -121,7 +123,7 @@ def reverse(self) -> SquashedSelect: def order_with(self, by: Tuple[order.OrderingExpression, ...]): adjusted_orderings = [ - order_part.bind_variables(self.column_lookup) for order_part in by + order_part.bind_refs(self.column_lookup) for order_part in by ] new_ordering = (*adjusted_orderings, *self.ordering) return SquashedSelect( @@ -134,8 +136,8 @@ def can_merge( join_keys: Tuple[join_defs.CoalescedColumnMapping, ...], ) -> bool: """Determines whether the two selections can be merged into a single selection.""" - r_exprs_by_id = {id: expr for expr, id in right.columns} - l_exprs_by_id = {id: expr for expr, id in self.columns} + r_exprs_by_id = {id.name: expr for expr, id in right.columns} + l_exprs_by_id = {id.name: expr for expr, id in self.columns} l_join_exprs = [ l_exprs_by_id[join_key.left_source_id] for join_key in join_keys ] @@ -227,7 +229,7 @@ def expand(self) -> nodes.BigFrameNode: root = nodes.FilterNode(child=root, predicate=self.predicate) if self.ordering: root = nodes.OrderByNode(child=root, by=self.ordering) - selection = tuple((id, id) for _, id in self.columns) + selection = tuple((scalar_exprs.DerefOp(id), id) for _, id in self.columns) return nodes.SelectionNode( child=nodes.ProjectionNode(child=root, assignments=self.columns), input_output_pairs=selection, @@ -266,14 +268,15 @@ def merge_expressions( rmask: Optional[scalar_exprs.Expression], ) -> Selection: new_selection: Selection = tuple() - l_exprs_by_id = {id: expr for expr, id in lselection} - r_exprs_by_id = {id: expr for expr, id in rselection} + # Assumption is simple ids + l_exprs_by_id = {id.name: expr for expr, id in lselection} + r_exprs_by_id = {id.name: expr for expr, id in rselection} for key in join_keys: # Join keys expressions are equivalent on both sides, so can choose either left or right key assert l_exprs_by_id[key.left_source_id] == r_exprs_by_id[key.right_source_id] expr = l_exprs_by_id[key.left_source_id] id = key.destination_id - new_selection = (*new_selection, (expr, id)) + new_selection = (*new_selection, (expr, ids.ColumnId(id))) for mapping in mappings: if mapping.source_table == join_defs.JoinSide.LEFT: expr = l_exprs_by_id[mapping.source_id] @@ -283,7 +286,7 @@ def merge_expressions( expr = r_exprs_by_id[mapping.source_id] if rmask is not None: expr = apply_mask(expr, rmask) - new_selection = (*new_selection, (expr, mapping.destination_id)) + new_selection = (*new_selection, (expr, ids.ColumnId(mapping.destination_id))) return new_selection @@ -354,12 +357,8 @@ def decompose_conjunction( return (expr,) -def get_node_column_ids(node: nodes.BigFrameNode) -> Tuple[str, ...]: - # TODO: Convert to use node.schema once that has been merged - # Note: this actually compiles the node to get the schema - import bigframes.core - - return tuple(bigframes.core.ArrayValue(node).column_ids) +def get_node_column_ids(node: nodes.BigFrameNode) -> Tuple[ids.ColumnId, ...]: + return tuple(field.id for field in node.fields) def common_selection_root( diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index 528c9bcc748..e44091e7b17 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -110,10 +110,8 @@ def ordering_clause( if ordering_expr.is_const: # Probably shouldn't have constants in ordering definition, but best to ignore if somehow they end up here. continue - assert isinstance( - ordering_expr, bigframes.core.expression.UnboundVariableExpression - ) - part = f"`{ordering_expr.id}` {asc_desc} {null_clause}" + assert isinstance(ordering_expr, bigframes.core.expression.DerefOp) + part = f"`{ordering_expr.id.sql}` {asc_desc} {null_clause}" parts.append(part) return f"ORDER BY {' ,'.join(parts)}" diff --git a/bigframes/core/window/__init__.py b/bigframes/core/window/__init__.py index fb682c950e1..2b45560b15b 100644 --- a/bigframes/core/window/__init__.py +++ b/bigframes/core/window/__init__.py @@ -81,7 +81,7 @@ def _apply_aggregate( original_index_ids = block.index_columns block = block.reset_index(drop=False) index_ids = ( - *[col for col in self._window_spec.grouping_keys], + *[col.id.name for col in self._window_spec.grouping_keys], *original_index_ids, ) block = block.set_index(col_ids=index_ids) diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py index 3d80afea5ac..2b9ff65084c 100644 --- a/bigframes/core/window_spec.py +++ b/bigframes/core/window_spec.py @@ -17,6 +17,8 @@ import itertools from typing import Optional, Set, Tuple, Union +import bigframes.core.expression as ex +import bigframes.core.identifiers as ids import bigframes.core.ordering as orderings @@ -41,7 +43,9 @@ def unbound( WindowSpec """ return WindowSpec( - grouping_keys=grouping_keys, min_periods=min_periods, ordering=ordering + grouping_keys=tuple(map(ex.deref, grouping_keys)), + min_periods=min_periods, + ordering=ordering, ) @@ -72,7 +76,7 @@ def rows( """ bounds = RowsWindowBounds(preceding=preceding, following=following) return WindowSpec( - grouping_keys=grouping_keys, + grouping_keys=tuple(map(ex.deref, grouping_keys)), bounds=bounds, min_periods=min_periods, ordering=ordering, @@ -95,7 +99,9 @@ def cumulative_rows( """ bounds = RowsWindowBounds(following=0) return WindowSpec( - grouping_keys=grouping_keys, bounds=bounds, min_periods=min_periods + grouping_keys=tuple(map(ex.deref, grouping_keys)), + bounds=bounds, + min_periods=min_periods, ) @@ -115,7 +121,9 @@ def inverse_cumulative_rows( """ bounds = RowsWindowBounds(preceding=0) return WindowSpec( - grouping_keys=grouping_keys, bounds=bounds, min_periods=min_periods + grouping_keys=tuple(map(ex.deref, grouping_keys)), + bounds=bounds, + min_periods=min_periods, ) @@ -148,7 +156,7 @@ class WindowSpec: ordering: List of columns ids and ordering direction to override base ordering """ - grouping_keys: Tuple[str, ...] = tuple() + grouping_keys: Tuple[ex.DerefOp, ...] = tuple() ordering: Tuple[orderings.OrderingExpression, ...] = tuple() bounds: Union[RowsWindowBounds, RangeWindowBounds, None] = None min_periods: int = 0 @@ -164,11 +172,11 @@ def row_bounded(self): return isinstance(self.bounds, RowsWindowBounds) @property - def all_referenced_columns(self) -> Set[str]: + def all_referenced_columns(self) -> Set[ids.ColumnId]: """ Return list of all variables reference ind the window. """ ordering_vars = itertools.chain.from_iterable( - item.scalar_expression.unbound_variables for item in self.ordering + item.scalar_expression.column_references for item in self.ordering ) - return set(itertools.chain(self.grouping_keys, ordering_vars)) + return set(itertools.chain((i.id for i in self.grouping_keys), ordering_vars)) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1778c228cfe..ba63bbb0160 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -749,11 +749,11 @@ def _apply_scalar_binop( if reverse: expr = op.as_expr( left_input=ex.const(other), - right_input=bigframes.core.guid.generate_guid(), + right_input=ex.free_var("var1"), ) else: expr = op.as_expr( - left_input=bigframes.core.guid.generate_guid(), + left_input=ex.free_var("var1"), right_input=ex.const(other), ) return DataFrame( diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index c48c8c4136f..ad2598b2af4 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -148,11 +148,11 @@ def as_expr( def _convert_expr_input( input: typing.Union[str, bigframes.core.expression.Expression] ) -> bigframes.core.expression.Expression: - """Allows creating free variables with just a string""" + """Allows creating column references with just a string""" import bigframes.core.expression if isinstance(input, str): - return bigframes.core.expression.UnboundVariableExpression(input) + return bigframes.core.expression.deref(input) else: return input diff --git a/bigframes/operations/base.py b/bigframes/operations/base.py index f9a6a87b7a2..8304f0070c6 100644 --- a/bigframes/operations/base.py +++ b/bigframes/operations/base.py @@ -24,6 +24,7 @@ import bigframes.core.blocks as blocks import bigframes.core.convert import bigframes.core.expression as ex +import bigframes.core.identifiers as ids import bigframes.core.indexes as indexes import bigframes.core.scalar as scalars import bigframes.dtypes @@ -219,31 +220,27 @@ def _apply_binary_aggregation( self, other: series.Series, stat: agg_ops.BinaryAggregateOp ) -> float: (left, right, block) = self._align(other, how="outer") - assert isinstance(left, ex.UnboundVariableExpression) - assert isinstance(right, ex.UnboundVariableExpression) - return block.get_binary_stat(left.id, right.id, stat) + assert isinstance(left, ex.DerefOp) + assert isinstance(right, ex.DerefOp) + return block.get_binary_stat(left.id.name, right.id.name, stat) - AlignedExprT = Union[ex.ScalarConstantExpression, ex.UnboundVariableExpression] + AlignedExprT = Union[ex.ScalarConstantExpression, ex.DerefOp] @typing.overload def _align( self, other: series.Series, how="outer" - ) -> tuple[ - ex.UnboundVariableExpression, - ex.UnboundVariableExpression, - blocks.Block, - ]: + ) -> tuple[ex.DerefOp, ex.DerefOp, blocks.Block,]: ... @typing.overload def _align( self, other: typing.Union[series.Series, scalars.Scalar], how="outer" - ) -> tuple[ex.UnboundVariableExpression, AlignedExprT, blocks.Block,]: + ) -> tuple[ex.DerefOp, AlignedExprT, blocks.Block,]: ... def _align( self, other: typing.Union[series.Series, scalars.Scalar], how="outer" - ) -> tuple[ex.UnboundVariableExpression, AlignedExprT, blocks.Block,]: + ) -> tuple[ex.DerefOp, AlignedExprT, blocks.Block,]: """Aligns the series value with another scalar or series object. Returns new left column id, right column id and joined tabled expression.""" values, block = self._align_n( [ @@ -251,13 +248,13 @@ def _align( ], how, ) - return (typing.cast(ex.UnboundVariableExpression, values[0]), values[1], block) + return (typing.cast(ex.DerefOp, values[0]), values[1], block) - def _align3(self, other1: series.Series | scalars.Scalar, other2: series.Series | scalars.Scalar, how="left") -> tuple[ex.UnboundVariableExpression, AlignedExprT, AlignedExprT, blocks.Block]: # type: ignore + def _align3(self, other1: series.Series | scalars.Scalar, other2: series.Series | scalars.Scalar, how="left") -> tuple[ex.DerefOp, AlignedExprT, AlignedExprT, blocks.Block]: # type: ignore """Aligns the series value with 2 other scalars or series objects. Returns new values and joined tabled expression.""" values, index = self._align_n([other1, other2], how) return ( - typing.cast(ex.UnboundVariableExpression, values[0]), + typing.cast(ex.DerefOp, values[0]), values[1], values[2], index, @@ -270,17 +267,13 @@ def _align_n( ignore_self=False, cast_scalars: bool = True, ) -> tuple[ - typing.Sequence[ - Union[ex.ScalarConstantExpression, ex.UnboundVariableExpression] - ], + typing.Sequence[Union[ex.ScalarConstantExpression, ex.DerefOp]], blocks.Block, ]: if ignore_self: - value_ids: List[ - Union[ex.ScalarConstantExpression, ex.UnboundVariableExpression] - ] = [] + value_ids: List[Union[ex.ScalarConstantExpression, ex.DerefOp]] = [] else: - value_ids = [ex.free_var(self._value_column)] + value_ids = [ex.deref(self._value_column)] block = self._block for other in others: @@ -289,9 +282,16 @@ def _align_n( get_column_left, get_column_right, ) = block.join(other._block, how=how) + rebindings = { + ids.ColumnId(old): ids.ColumnId(new) + for old, new in get_column_left.items() + } + remapped_value_ids = ( + value.remap_column_refs(rebindings) for value in value_ids + ) value_ids = [ - *[value.rename(get_column_left) for value in value_ids], - ex.free_var(get_column_right[other._value_column]), + *remapped_value_ids, # type: ignore + ex.deref(get_column_right[other._value_column]), ] else: # Will throw if can't interpret as scalar. diff --git a/bigframes/series.py b/bigframes/series.py index 7f2e5c5ccc0..bd9cb7ef0c4 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1233,7 +1233,7 @@ def __getitem__(self, indexer): if isinstance(indexer, Series): (left, right, block) = self._align(indexer, "left") block = block.filter(right) - block = block.select_column(left.id) + block = block.select_column(left.id.name) return Series(block) return self.loc[indexer] diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 13d74a73948..78e4e74adc9 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -42,6 +42,7 @@ import bigframes.core.compile import bigframes.core.expression as ex import bigframes.core.guid +import bigframes.core.identifiers import bigframes.core.nodes as nodes import bigframes.core.ordering as order import bigframes.core.schema @@ -488,9 +489,10 @@ def _cache_with_session_awareness( target, cluster_cols = bigframes.session.planner.session_aware_cache_plan( array_value.node, list(session_forest) ) + cluster_cols_sql_names = [id.sql for id in cluster_cols] if len(cluster_cols) > 0: self._cache_with_cluster_cols( - bigframes.core.ArrayValue(target), cluster_cols + bigframes.core.ArrayValue(target), cluster_cols_sql_names ) elif self.strictly_ordered: self._cache_with_offsets(bigframes.core.ArrayValue(target)) @@ -554,11 +556,19 @@ def _sql_as_cached_temp_table( def generate_head_plan(node: nodes.BigFrameNode, n: int): offsets_id = bigframes.core.guid.generate_guid("offsets_") - plan_w_offsets = nodes.PromoteOffsetsNode(node, offsets_id) - predicate = ops.lt_op.as_expr(ex.free_var(offsets_id), ex.const(n)) + plan_w_offsets = nodes.PromoteOffsetsNode( + node, bigframes.core.identifiers.ColumnId(offsets_id) + ) + predicate = ops.lt_op.as_expr(ex.deref(offsets_id), ex.const(n)) plan_w_head = nodes.FilterNode(plan_w_offsets, predicate) # Finally, drop the offsets column - return nodes.SelectionNode(plan_w_head, tuple((i, i) for i in node.schema.names)) + return nodes.SelectionNode( + plan_w_head, + tuple( + (ex.deref(i), bigframes.core.identifiers.ColumnId(i)) + for i in node.schema.names + ), + ) def generate_row_count_plan(node: nodes.BigFrameNode): diff --git a/bigframes/session/planner.py b/bigframes/session/planner.py index bc640ec9fac..2a562abadf1 100644 --- a/bigframes/session/planner.py +++ b/bigframes/session/planner.py @@ -18,14 +18,16 @@ from typing import Sequence, Tuple import bigframes.core.expression as ex +import bigframes.core.identifiers as ids import bigframes.core.nodes as nodes import bigframes.core.pruning as predicate_pruning import bigframes.core.tree_properties as traversals +import bigframes.dtypes def session_aware_cache_plan( root: nodes.BigFrameNode, session_forest: Sequence[nodes.BigFrameNode] -) -> Tuple[nodes.BigFrameNode, list[str]]: +) -> Tuple[nodes.BigFrameNode, list[ids.ColumnId]]: """ Determines the best node to cache given a target and a list of object roots for objects in a session. @@ -40,7 +42,7 @@ def session_aware_cache_plan( filters: list[ ex.Expression ] = [] # accumulate filters into this as traverse downwards - clusterable_cols: set[str] = set() + clusterable_cols: set[ids.ColumnId] = set() while isinstance(cur_node, de_cachable_types): if isinstance(cur_node, nodes.FilterNode): # Filter node doesn't define any variables, so no need to chain expressions @@ -50,14 +52,11 @@ def session_aware_cache_plan( # that instead reference variables in the child node. bindings = {name: expr for expr, name in cur_node.assignments} filters = [ - i.bind_variables(bindings, check_bind_all=False) for i in filters + i.bind_refs(bindings, allow_partial_bindings=True) for i in filters ] elif isinstance(cur_node, nodes.SelectionNode): - bindings = { - output: ex.free_var(input) - for input, output in cur_node.input_output_pairs - } - filters = [i.bind_variables(bindings) for i in filters] + bindings = {output: input for input, output in cur_node.input_output_pairs} + filters = [i.bind_refs(bindings) for i in filters] else: raise ValueError(f"Unexpected de-cached node: {cur_node}") @@ -65,13 +64,17 @@ def session_aware_cache_plan( cur_node_refs = node_counts.get(cur_node, 0) if cur_node_refs > caching_target_refs: caching_target, caching_target_refs = cur_node, cur_node_refs - schema = cur_node.schema + cluster_compatible_cols = { + field.id + for field in cur_node.fields + if bigframes.dtypes.is_clusterable(field.dtype) + } # Cluster cols only consider the target object and not other sesssion objects clusterable_cols = set( itertools.chain.from_iterable( map( lambda f: predicate_pruning.cluster_cols_for_predicate( - f, schema + f, cluster_compatible_cols ), filters, ) diff --git a/tests/unit/core/test_expression.py b/tests/unit/core/test_expression.py index f46c47a5827..72e200f0076 100644 --- a/tests/unit/core/test_expression.py +++ b/tests/unit/core/test_expression.py @@ -13,13 +13,16 @@ # limitations under the License. import bigframes.core.expression as ex +import bigframes.core.identifiers as ids import bigframes.dtypes as dtypes import bigframes.operations as ops def test_expression_dtype_simple(): expression = ops.add_op.as_expr("a", "b") - result = expression.output_type({"a": dtypes.INT_DTYPE, "b": dtypes.INT_DTYPE}) + result = expression.output_type( + {ids.ColumnId("a"): dtypes.INT_DTYPE, ids.ColumnId("b"): dtypes.INT_DTYPE} + ) assert result == dtypes.INT_DTYPE @@ -28,7 +31,9 @@ def test_expression_dtype_nested(): "a", ops.abs_op.as_expr(ops.sub_op.as_expr("b", ex.const(3.14))) ) - result = expression.output_type({"a": dtypes.INT_DTYPE, "b": dtypes.INT_DTYPE}) + result = expression.output_type( + {ids.ColumnId("a"): dtypes.INT_DTYPE, ids.ColumnId("b"): dtypes.INT_DTYPE} + ) assert result == dtypes.FLOAT_DTYPE diff --git a/tests/unit/test_planner.py b/tests/unit/test_planner.py index 27ec7d5921f..491f3de6fa3 100644 --- a/tests/unit/test_planner.py +++ b/tests/unit/test_planner.py @@ -20,6 +20,7 @@ import bigframes.core as core import bigframes.core.expression as ex +import bigframes.core.identifiers as ids import bigframes.core.schema import bigframes.operations as ops import bigframes.session.planner as planner @@ -53,7 +54,7 @@ def test_session_aware_caching_project_filter(): target.node, [obj.node for obj in session_objects] ) assert result == LEAF.node - assert cluster_cols == ["col_a"] + assert cluster_cols == [ids.ColumnId("col_a")] def test_session_aware_caching_project_multi_filter(): @@ -76,7 +77,7 @@ def test_session_aware_caching_project_multi_filter(): target.node, [obj.node for obj in session_objects] ) assert result == LEAF.node - assert cluster_cols == ["col_a", "col_b"] + assert cluster_cols == [ids.ColumnId("col_a"), ids.ColumnId("col_b")] def test_session_aware_caching_unusable_filter(): @@ -117,4 +118,4 @@ def test_session_aware_caching_fork_after_window_op(): ], ) assert result == LEAF.promote_offsets()[0].node - assert cluster_cols == ["col_a"] + assert cluster_cols == [ids.ColumnId("col_a")] From 2d18815d776682c3f842b21cb8bd871e788c9545 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Fri, 27 Sep 2024 08:51:50 -0700 Subject: [PATCH 865/921] refactor: move reader functions from `__init__.py` to a separate file under the pandas package (#1023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: move reader functions from __init__.py to a separate file under pandas package * fix type in __all__ * move read functions under bigframes.pandas.io.api to reflect the structure of pandas * move read functions under bigframes.pandas.io.api to reflect the structure of pandas * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * update function import * fix missing comma * try to fix dup doc by directly import types from pandas * fix doc generation --------- Co-authored-by: Owl Bot --- bigframes/pandas/__init__.py | 335 +---------------- bigframes/pandas/io/__init__.py | 13 + bigframes/pandas/io/api.py | 347 ++++++++++++++++++ bigframes/streaming/__init__.py | 4 +- .../bigframes.pandas/general_functions.rst | 1 + 5 files changed, 382 insertions(+), 318 deletions(-) create mode 100644 bigframes/pandas/io/__init__.py create mode 100644 bigframes/pandas/io/api.py diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 1bdf49eaf5f..98da6d826c9 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -21,20 +21,7 @@ import inspect import sys import typing -from typing import ( - Any, - Callable, - Dict, - IO, - Iterable, - List, - Literal, - MutableSequence, - Optional, - Sequence, - Tuple, - Union, -) +from typing import Any, Iterable, List, Literal, Optional, Sequence, Tuple, Union import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.reshape.concat as vendored_pandas_concat @@ -42,16 +29,7 @@ import bigframes_vendored.pandas.core.reshape.merge as vendored_pandas_merge import bigframes_vendored.pandas.core.reshape.tile as vendored_pandas_tile import bigframes_vendored.pandas.core.tools.datetimes as vendored_pandas_datetimes -import bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq -from google.cloud import bigquery -import numpy import pandas -from pandas._typing import ( - CompressionOptions, - FilePath, - ReadPickleBuffer, - StorageOptions, -) import bigframes._config as config import bigframes.core.blocks @@ -65,6 +43,18 @@ import bigframes.enums import bigframes.functions._utils as functions_utils import bigframes.operations as ops +from bigframes.pandas.io.api import ( + read_csv, + read_gbq, + read_gbq_function, + read_gbq_model, + read_gbq_query, + read_gbq_table, + read_json, + read_pandas, + read_parquet, + read_pickle, +) import bigframes.series import bigframes.session import bigframes.session._io.bigquery @@ -373,286 +363,6 @@ def merge( merge.__doc__ = vendored_pandas_merge.merge.__doc__ -def _set_default_session_location_if_possible(query): - # Set the location as per the query if this is the first query the user is - # running and: - # (1) Default session has not started yet, and - # (2) Location is not set yet, and - # (3) Use of regional endpoints is not set. - # If query is a table name, then it would be the location of the table. - # If query is a SQL with a table, then it would be table's location. - # If query is a SQL with no table, then it would be the BQ default location. - if ( - options.bigquery._session_started - or options.bigquery.location - or options.bigquery.use_regional_endpoints - ): - return - - clients_provider = bigframes.session.clients.ClientsProvider( - project=options.bigquery.project, - location=options.bigquery.location, - use_regional_endpoints=options.bigquery.use_regional_endpoints, - credentials=options.bigquery.credentials, - application_name=options.bigquery.application_name, - bq_kms_key_name=options.bigquery.kms_key_name, - ) - - bqclient = clients_provider.bqclient - - if bigframes.session._io.bigquery.is_query(query): - # Intentionally run outside of the session so that we can detect the - # location before creating the session. Since it's a dry_run, labels - # aren't necessary. - job = bqclient.query(query, bigquery.QueryJobConfig(dry_run=True)) - options.bigquery.location = job.location - else: - table = bqclient.get_table(query) - options.bigquery.location = table.location - - -# Note: the following methods are duplicated from Session. This duplication -# enables the following: -# -# 1. Static type checking knows the argument and return types, which is -# difficult to do with decorators. Aside: When we require Python 3.10, we -# can use Concatenate for generic typing in decorators. See: -# https://stackoverflow.com/a/68290080/101923 -# 2. docstrings get processed by static processing tools, such as VS Code's -# autocomplete. -# 3. Positional arguments function as expected. If we were to pull in the -# methods directly from Session, a Session object would need to be the first -# argument, even if we allow a default value. -# 4. Allows to set BigQuery options for the BigFrames session based on the -# method and its arguments. - - -def read_csv( - filepath_or_buffer: str | IO["bytes"], - *, - sep: Optional[str] = ",", - header: Optional[int] = 0, - names: Optional[ - Union[MutableSequence[Any], numpy.ndarray[Any, Any], Tuple[Any, ...], range] - ] = None, - index_col: Optional[ - Union[ - int, - str, - Sequence[Union[str, int]], - bigframes.enums.DefaultIndexKind, - Literal[False], - ] - ] = None, - usecols: Optional[ - Union[ - MutableSequence[str], - Tuple[str, ...], - Sequence[int], - pandas.Series, - pandas.Index, - numpy.ndarray[Any, Any], - Callable[[Any], bool], - ] - ] = None, - dtype: Optional[Dict] = None, - engine: Optional[ - Literal["c", "python", "pyarrow", "python-fwf", "bigquery"] - ] = None, - encoding: Optional[str] = None, - **kwargs, -) -> bigframes.dataframe.DataFrame: - return global_session.with_default_session( - bigframes.session.Session.read_csv, - filepath_or_buffer=filepath_or_buffer, - sep=sep, - header=header, - names=names, - index_col=index_col, - usecols=usecols, - dtype=dtype, - engine=engine, - encoding=encoding, - **kwargs, - ) - - -read_csv.__doc__ = inspect.getdoc(bigframes.session.Session.read_csv) - - -def read_json( - path_or_buf: str | IO["bytes"], - *, - orient: Literal[ - "split", "records", "index", "columns", "values", "table" - ] = "columns", - dtype: Optional[Dict] = None, - encoding: Optional[str] = None, - lines: bool = False, - engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", - **kwargs, -) -> bigframes.dataframe.DataFrame: - return global_session.with_default_session( - bigframes.session.Session.read_json, - path_or_buf=path_or_buf, - orient=orient, - dtype=dtype, - encoding=encoding, - lines=lines, - engine=engine, - **kwargs, - ) - - -read_json.__doc__ = inspect.getdoc(bigframes.session.Session.read_json) - - -def read_gbq( - query_or_table: str, - *, - index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), - columns: Iterable[str] = (), - configuration: Optional[Dict] = None, - max_results: Optional[int] = None, - filters: vendored_pandas_gbq.FiltersType = (), - use_cache: Optional[bool] = None, - col_order: Iterable[str] = (), -) -> bigframes.dataframe.DataFrame: - _set_default_session_location_if_possible(query_or_table) - return global_session.with_default_session( - bigframes.session.Session.read_gbq, - query_or_table, - index_col=index_col, - columns=columns, - configuration=configuration, - max_results=max_results, - filters=filters, - use_cache=use_cache, - col_order=col_order, - ) - - -read_gbq.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq) - - -def read_gbq_model(model_name: str): - return global_session.with_default_session( - bigframes.session.Session.read_gbq_model, - model_name, - ) - - -read_gbq_model.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_model) - - -def read_gbq_query( - query: str, - *, - index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), - columns: Iterable[str] = (), - configuration: Optional[Dict] = None, - max_results: Optional[int] = None, - use_cache: Optional[bool] = None, - col_order: Iterable[str] = (), - filters: vendored_pandas_gbq.FiltersType = (), -) -> bigframes.dataframe.DataFrame: - _set_default_session_location_if_possible(query) - return global_session.with_default_session( - bigframes.session.Session.read_gbq_query, - query, - index_col=index_col, - columns=columns, - configuration=configuration, - max_results=max_results, - use_cache=use_cache, - col_order=col_order, - filters=filters, - ) - - -read_gbq_query.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_query) - - -def read_gbq_table( - query: str, - *, - index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), - columns: Iterable[str] = (), - max_results: Optional[int] = None, - filters: vendored_pandas_gbq.FiltersType = (), - use_cache: bool = True, - col_order: Iterable[str] = (), -) -> bigframes.dataframe.DataFrame: - _set_default_session_location_if_possible(query) - return global_session.with_default_session( - bigframes.session.Session.read_gbq_table, - query, - index_col=index_col, - columns=columns, - max_results=max_results, - filters=filters, - use_cache=use_cache, - col_order=col_order, - ) - - -read_gbq_table.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_table) - - -@typing.overload -def read_pandas(pandas_dataframe: pandas.DataFrame) -> bigframes.dataframe.DataFrame: - ... - - -@typing.overload -def read_pandas(pandas_dataframe: pandas.Series) -> bigframes.series.Series: - ... - - -@typing.overload -def read_pandas(pandas_dataframe: pandas.Index) -> bigframes.core.indexes.Index: - ... - - -def read_pandas(pandas_dataframe: Union[pandas.DataFrame, pandas.Series, pandas.Index]): - return global_session.with_default_session( - bigframes.session.Session.read_pandas, - pandas_dataframe, - ) - - -read_pandas.__doc__ = inspect.getdoc(bigframes.session.Session.read_pandas) - - -def read_pickle( - filepath_or_buffer: FilePath | ReadPickleBuffer, - compression: CompressionOptions = "infer", - storage_options: StorageOptions = None, -): - return global_session.with_default_session( - bigframes.session.Session.read_pickle, - filepath_or_buffer=filepath_or_buffer, - compression=compression, - storage_options=storage_options, - ) - - -read_pickle.__doc__ = inspect.getdoc(bigframes.session.Session.read_pickle) - - -def read_parquet( - path: str | IO["bytes"], *, engine: str = "auto" -) -> bigframes.dataframe.DataFrame: - return global_session.with_default_session( - bigframes.session.Session.read_parquet, - path, - engine=engine, - ) - - -read_parquet.__doc__ = inspect.getdoc(bigframes.session.Session.read_parquet) - - def remote_function( input_types: Union[None, type, Sequence[type]] = None, output_type: Optional[type] = None, @@ -697,17 +407,6 @@ def remote_function( remote_function.__doc__ = inspect.getdoc(bigframes.session.Session.remote_function) -def read_gbq_function(function_name: str, is_row_processor: bool = False): - return global_session.with_default_session( - bigframes.session.Session.read_gbq_function, - function_name=function_name, - is_row_processor=is_row_processor, - ) - - -read_gbq_function.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_function) - - @typing.overload def to_datetime( arg: Union[ @@ -893,7 +592,7 @@ def reset_session(): pass # Use __all__ to let type checkers know what is part of the public API. -__all___ = [ +__all__ = [ # Functions "concat", "merge", @@ -901,7 +600,11 @@ def reset_session(): "read_gbq", "read_gbq_function", "read_gbq_model", + "read_gbq_query", + "read_gbq_table", + "read_json", "read_pandas", + "read_parquet", "read_pickle", "remote_function", "to_datetime", @@ -911,7 +614,7 @@ def reset_session(): "Float64Dtype", "Int64Dtype", "StringDtype", - "ArrowDtype" + "ArrowDtype", # Class aliases "DataFrame", "Index", diff --git a/bigframes/pandas/io/__init__.py b/bigframes/pandas/io/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/bigframes/pandas/io/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/bigframes/pandas/io/api.py b/bigframes/pandas/io/api.py new file mode 100644 index 00000000000..4e08b3ef5e6 --- /dev/null +++ b/bigframes/pandas/io/api.py @@ -0,0 +1,347 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import inspect +import typing +from typing import ( + Any, + Callable, + Dict, + IO, + Iterable, + Literal, + MutableSequence, + Optional, + Sequence, + Tuple, + Union, +) + +import bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq +from google.cloud import bigquery +import numpy +import pandas +from pandas._typing import ( + CompressionOptions, + FilePath, + ReadPickleBuffer, + StorageOptions, +) + +import bigframes._config as config +import bigframes.core.blocks +import bigframes.core.global_session as global_session +import bigframes.core.indexes +import bigframes.core.joins +import bigframes.core.reshape +import bigframes.core.tools +import bigframes.dataframe +import bigframes.enums +import bigframes.series +import bigframes.session +import bigframes.session._io.bigquery +import bigframes.session.clients +import bigframes.version + +# Note: the following methods are duplicated from Session. This duplication +# enables the following: +# +# 1. Static type checking knows the argument and return types, which is +# difficult to do with decorators. Aside: When we require Python 3.10, we +# can use Concatenate for generic typing in decorators. See: +# https://stackoverflow.com/a/68290080/101923 +# 2. docstrings get processed by static processing tools, such as VS Code's +# autocomplete. +# 3. Positional arguments function as expected. If we were to pull in the +# methods directly from Session, a Session object would need to be the first +# argument, even if we allow a default value. +# 4. Allows to set BigQuery options for the BigFrames session based on the +# method and its arguments. + + +def read_csv( + filepath_or_buffer: str | IO["bytes"], + *, + sep: Optional[str] = ",", + header: Optional[int] = 0, + names: Optional[ + Union[MutableSequence[Any], numpy.ndarray[Any, Any], Tuple[Any, ...], range] + ] = None, + index_col: Optional[ + Union[ + int, + str, + Sequence[Union[str, int]], + bigframes.enums.DefaultIndexKind, + Literal[False], + ] + ] = None, + usecols: Optional[ + Union[ + MutableSequence[str], + Tuple[str, ...], + Sequence[int], + pandas.Series, + pandas.Index, + numpy.ndarray[Any, Any], + Callable[[Any], bool], + ] + ] = None, + dtype: Optional[Dict] = None, + engine: Optional[ + Literal["c", "python", "pyarrow", "python-fwf", "bigquery"] + ] = None, + encoding: Optional[str] = None, + **kwargs, +) -> bigframes.dataframe.DataFrame: + return global_session.with_default_session( + bigframes.session.Session.read_csv, + filepath_or_buffer=filepath_or_buffer, + sep=sep, + header=header, + names=names, + index_col=index_col, + usecols=usecols, + dtype=dtype, + engine=engine, + encoding=encoding, + **kwargs, + ) + + +read_csv.__doc__ = inspect.getdoc(bigframes.session.Session.read_csv) + + +def read_json( + path_or_buf: str | IO["bytes"], + *, + orient: Literal[ + "split", "records", "index", "columns", "values", "table" + ] = "columns", + dtype: Optional[Dict] = None, + encoding: Optional[str] = None, + lines: bool = False, + engine: Literal["ujson", "pyarrow", "bigquery"] = "ujson", + **kwargs, +) -> bigframes.dataframe.DataFrame: + return global_session.with_default_session( + bigframes.session.Session.read_json, + path_or_buf=path_or_buf, + orient=orient, + dtype=dtype, + encoding=encoding, + lines=lines, + engine=engine, + **kwargs, + ) + + +read_json.__doc__ = inspect.getdoc(bigframes.session.Session.read_json) + + +def read_gbq( + query_or_table: str, + *, + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), + columns: Iterable[str] = (), + configuration: Optional[Dict] = None, + max_results: Optional[int] = None, + filters: vendored_pandas_gbq.FiltersType = (), + use_cache: Optional[bool] = None, + col_order: Iterable[str] = (), +) -> bigframes.dataframe.DataFrame: + _set_default_session_location_if_possible(query_or_table) + return global_session.with_default_session( + bigframes.session.Session.read_gbq, + query_or_table, + index_col=index_col, + columns=columns, + configuration=configuration, + max_results=max_results, + filters=filters, + use_cache=use_cache, + col_order=col_order, + ) + + +read_gbq.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq) + + +def read_gbq_model(model_name: str): + return global_session.with_default_session( + bigframes.session.Session.read_gbq_model, + model_name, + ) + + +read_gbq_model.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_model) + + +def read_gbq_query( + query: str, + *, + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), + columns: Iterable[str] = (), + configuration: Optional[Dict] = None, + max_results: Optional[int] = None, + use_cache: Optional[bool] = None, + col_order: Iterable[str] = (), + filters: vendored_pandas_gbq.FiltersType = (), +) -> bigframes.dataframe.DataFrame: + _set_default_session_location_if_possible(query) + return global_session.with_default_session( + bigframes.session.Session.read_gbq_query, + query, + index_col=index_col, + columns=columns, + configuration=configuration, + max_results=max_results, + use_cache=use_cache, + col_order=col_order, + filters=filters, + ) + + +read_gbq_query.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_query) + + +def read_gbq_table( + query: str, + *, + index_col: Iterable[str] | str | bigframes.enums.DefaultIndexKind = (), + columns: Iterable[str] = (), + max_results: Optional[int] = None, + filters: vendored_pandas_gbq.FiltersType = (), + use_cache: bool = True, + col_order: Iterable[str] = (), +) -> bigframes.dataframe.DataFrame: + _set_default_session_location_if_possible(query) + return global_session.with_default_session( + bigframes.session.Session.read_gbq_table, + query, + index_col=index_col, + columns=columns, + max_results=max_results, + filters=filters, + use_cache=use_cache, + col_order=col_order, + ) + + +read_gbq_table.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_table) + + +@typing.overload +def read_pandas(pandas_dataframe: pandas.DataFrame) -> bigframes.dataframe.DataFrame: + ... + + +@typing.overload +def read_pandas(pandas_dataframe: pandas.Series) -> bigframes.series.Series: + ... + + +@typing.overload +def read_pandas(pandas_dataframe: pandas.Index) -> bigframes.core.indexes.Index: + ... + + +def read_pandas(pandas_dataframe: Union[pandas.DataFrame, pandas.Series, pandas.Index]): + return global_session.with_default_session( + bigframes.session.Session.read_pandas, + pandas_dataframe, + ) + + +read_pandas.__doc__ = inspect.getdoc(bigframes.session.Session.read_pandas) + + +def read_pickle( + filepath_or_buffer: FilePath | ReadPickleBuffer, + compression: CompressionOptions = "infer", + storage_options: StorageOptions = None, +): + return global_session.with_default_session( + bigframes.session.Session.read_pickle, + filepath_or_buffer=filepath_or_buffer, + compression=compression, + storage_options=storage_options, + ) + + +read_pickle.__doc__ = inspect.getdoc(bigframes.session.Session.read_pickle) + + +def read_parquet( + path: str | IO["bytes"], *, engine: str = "auto" +) -> bigframes.dataframe.DataFrame: + return global_session.with_default_session( + bigframes.session.Session.read_parquet, + path, + engine=engine, + ) + + +read_parquet.__doc__ = inspect.getdoc(bigframes.session.Session.read_parquet) + + +def read_gbq_function(function_name: str, is_row_processor: bool = False): + return global_session.with_default_session( + bigframes.session.Session.read_gbq_function, + function_name=function_name, + is_row_processor=is_row_processor, + ) + + +read_gbq_function.__doc__ = inspect.getdoc(bigframes.session.Session.read_gbq_function) + + +def _set_default_session_location_if_possible(query): + # Set the location as per the query if this is the first query the user is + # running and: + # (1) Default session has not started yet, and + # (2) Location is not set yet, and + # (3) Use of regional endpoints is not set. + # If query is a table name, then it would be the location of the table. + # If query is a SQL with a table, then it would be table's location. + # If query is a SQL with no table, then it would be the BQ default location. + if ( + config.options.bigquery._session_started + or config.options.bigquery.location + or config.options.bigquery.use_regional_endpoints + ): + return + + clients_provider = bigframes.session.clients.ClientsProvider( + project=config.options.bigquery.project, + location=config.options.bigquery.location, + use_regional_endpoints=config.options.bigquery.use_regional_endpoints, + credentials=config.options.bigquery.credentials, + application_name=config.options.bigquery.application_name, + bq_kms_key_name=config.options.bigquery.kms_key_name, + ) + + bqclient = clients_provider.bqclient + + if bigframes.session._io.bigquery.is_query(query): + # Intentionally run outside of the session so that we can detect the + # location before creating the session. Since it's a dry_run, labels + # aren't necessary. + job = bqclient.query(query, bigquery.QueryJobConfig(dry_run=True)) + config.options.bigquery.location = job.location + else: + table = bqclient.get_table(query) + config.options.bigquery.location = table.location diff --git a/bigframes/streaming/__init__.py b/bigframes/streaming/__init__.py index 66f345f0ab9..d439d622a2f 100644 --- a/bigframes/streaming/__init__.py +++ b/bigframes/streaming/__init__.py @@ -15,13 +15,13 @@ import inspect import bigframes.core.global_session as global_session -import bigframes.pandas as bpd +from bigframes.pandas.io.api import _set_default_session_location_if_possible import bigframes.session import bigframes.streaming.dataframe as streaming_dataframe def read_gbq_table(table: str) -> streaming_dataframe.StreamingDataFrame: - bpd._set_default_session_location_if_possible(table) + _set_default_session_location_if_possible(table) return global_session.with_default_session( bigframes.session.Session.read_gbq_table_streaming, table ) diff --git a/docs/reference/bigframes.pandas/general_functions.rst b/docs/reference/bigframes.pandas/general_functions.rst index 4fff9aabf83..fff1a9ef59f 100644 --- a/docs/reference/bigframes.pandas/general_functions.rst +++ b/docs/reference/bigframes.pandas/general_functions.rst @@ -6,3 +6,4 @@ General functions .. automodule:: bigframes.pandas :members: :undoc-members: + :noindex: From ef76f137fbbf9e8f8c5a63023554d22059ab4fbd Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Fri, 27 Sep 2024 10:43:53 -0700 Subject: [PATCH 866/921] feat: allow access of struct fields with dot operators on `Series` (#1019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: allow access of struct fields with dot operators for Series * fix infinite recursion of __getattr__() * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix typing and version * 🦉 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 --- bigframes/series.py | 26 ++++++++++++++-- tests/data/nested_structs.jsonl | 2 ++ tests/data/nested_structs_schema.json | 39 ++++++++++++++++++++++++ tests/system/conftest.py | 44 +++++++++++++++++++++++++++ tests/system/small/test_series.py | 39 ++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 tests/data/nested_structs.jsonl create mode 100644 tests/data/nested_structs_schema.json diff --git a/bigframes/series.py b/bigframes/series.py index bd9cb7ef0c4..d46f4ee9bd8 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -22,7 +22,7 @@ import numbers import textwrap import typing -from typing import Any, cast, Literal, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, cast, List, Literal, Mapping, Optional, Sequence, Tuple, Union import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.series as vendored_pandas_series @@ -30,6 +30,7 @@ import numpy import pandas import pandas.core.dtypes.common +import pyarrow as pa import typing_extensions import bigframes.core @@ -181,6 +182,14 @@ def _info_axis(self) -> indexes.Index: def _session(self) -> bigframes.Session: return self._get_block().expr.session + @property + def _struct_fields(self) -> List[str]: + if not bigframes.dtypes.is_struct_like(self._dtype): + return [] + + struct_type = typing.cast(pa.StructType, self._dtype.pyarrow_dtype) + return [struct_type.field(i).name for i in range(struct_type.num_fields)] + @validations.requires_ordering() def transpose(self) -> Series: return self @@ -1096,6 +1105,9 @@ def __pos__(self) -> Series: def __neg__(self) -> Series: return self._apply_unary_op(ops.neg_op) + def __dir__(self) -> List[str]: + return dir(type(self)) + self._struct_fields + def eq(self, other: object) -> Series: # TODO: enforce stricter alignment return self._apply_binary_op(other, ops.eq_op) @@ -1240,7 +1252,15 @@ def __getitem__(self, indexer): __getitem__.__doc__ = inspect.getdoc(vendored_pandas_series.Series.__getitem__) def __getattr__(self, key: str): - if hasattr(pandas.Series, key): + # Protect against recursion errors with uninitialized Series objects. + # We use "_block" attribute to check whether the instance is initialized. + # See: + # https://github.com/googleapis/python-bigquery-dataframes/issues/728 + # and + # https://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html + if key == "_block": + raise AttributeError(key) + elif hasattr(pandas.Series, key): raise AttributeError( textwrap.dedent( f""" @@ -1249,6 +1269,8 @@ def __getattr__(self, key: str): """ ) ) + elif key in self._struct_fields: + return self.struct.field(key) else: raise AttributeError(key) diff --git a/tests/data/nested_structs.jsonl b/tests/data/nested_structs.jsonl new file mode 100644 index 00000000000..f57214b0b3c --- /dev/null +++ b/tests/data/nested_structs.jsonl @@ -0,0 +1,2 @@ +{"id": 1, "person": {"name": "Alice", "age":30, "address": {"city": "New York", "country": "USA"}}} +{"id": 2, "person": {"name": "Bob", "age":25, "address": {"city": "London", "country": "UK"}}} \ No newline at end of file diff --git a/tests/data/nested_structs_schema.json b/tests/data/nested_structs_schema.json new file mode 100644 index 00000000000..6692615ceff --- /dev/null +++ b/tests/data/nested_structs_schema.json @@ -0,0 +1,39 @@ +[ + { + "name": "id", + "type": "INTEGER", + "mode": "REQUIRED" + }, + { + "name": "person", + "type": "RECORD", + "fields": [ + { + "name": "name", + "type": "STRING", + "mode": "NULLABLE" + }, + { + "name": "age", + "type": "INTEGER", + "mode": "NULLABLE" + }, + { + "name": "address", + "type": "RECORD", + "fields": [ + { + "name": "city", + "type": "STRING", + "mode": "NULLABLE" + }, + { + "name": "country", + "type": "STRING", + "mode": "NULLABLE" + } + ] + } + ] + } +] diff --git a/tests/system/conftest.py b/tests/system/conftest.py index d9246eecfb0..217cf71e0cd 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -32,6 +32,7 @@ import ibis.backends import numpy as np import pandas as pd +import pyarrow as pa import pytest import pytz import test_utils.prefixer @@ -290,6 +291,7 @@ def load_test_data_tables( ("scalars", "scalars_schema.json", "scalars.jsonl"), ("scalars_too", "scalars_schema.json", "scalars.jsonl"), ("nested", "nested_schema.json", "nested.jsonl"), + ("nested_structs", "nested_structs_schema.json", "nested_structs.jsonl"), ("repeated", "repeated_schema.json", "repeated.jsonl"), ("penguins", "penguins_schema.json", "penguins.jsonl"), ("time_series", "time_series_schema.json", "time_series.jsonl"), @@ -367,6 +369,11 @@ def nested_table_id(test_data_tables) -> str: return test_data_tables["nested"] +@pytest.fixture(scope="session") +def nested_structs_table_id(test_data_tables) -> str: + return test_data_tables["nested_structs"] + + @pytest.fixture(scope="session") def repeated_table_id(test_data_tables) -> str: return test_data_tables["repeated"] @@ -412,6 +419,43 @@ def nested_pandas_df() -> pd.DataFrame: return df +@pytest.fixture(scope="session") +def nested_structs_df( + nested_structs_table_id: str, session: bigframes.Session +) -> bigframes.dataframe.DataFrame: + """DataFrame pointing at test data.""" + return session.read_gbq(nested_structs_table_id, index_col="id") + + +@pytest.fixture(scope="session") +def nested_structs_pandas_df() -> pd.DataFrame: + """pd.DataFrame pointing at test data.""" + + df = pd.read_json( + DATA_DIR / "nested_structs.jsonl", + lines=True, + ) + df = df.set_index("id") + return df + + +@pytest.fixture(scope="session") +def nested_structs_pandas_type() -> pd.ArrowDtype: + address_struct_schema = pa.struct( + [pa.field("city", pa.string()), pa.field("country", pa.string())] + ) + + person_struct_schema = pa.struct( + [ + pa.field("name", pa.string()), + pa.field("age", pa.int64()), + pa.field("address", address_struct_schema), + ] + ) + + return pd.ArrowDtype(person_struct_schema) + + @pytest.fixture(scope="session") def repeated_df( repeated_table_id: str, session: bigframes.Session diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 793a4062c50..aa70b7c6550 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -19,6 +19,7 @@ import geopandas as gpd # type: ignore import numpy +from packaging.version import Version import pandas as pd import pyarrow as pa # type: ignore import pytest @@ -3912,3 +3913,41 @@ def test_series_explode_null(data): s.to_pandas().explode(), check_dtype=False, ) + + +def test_series_struct_get_field_by_attribute( + nested_structs_df, nested_structs_pandas_df, nested_structs_pandas_type +): + if Version(pd.__version__) < Version("2.2.0"): + pytest.skip("struct accessor is not supported before pandas 2.2") + + bf_series = nested_structs_df["person"] + df_series = nested_structs_pandas_df["person"].astype(nested_structs_pandas_type) + + pd.testing.assert_series_equal( + bf_series.address.city.to_pandas(), + df_series.struct.field("address").struct.field("city"), + check_dtype=False, + check_index=False, + ) + pd.testing.assert_series_equal( + bf_series.address.country.to_pandas(), + df_series.struct.field("address").struct.field("country"), + check_dtype=False, + check_index=False, + ) + + +def test_series_struct_fields_in_dir(nested_structs_df): + series = nested_structs_df["person"] + + assert "age" in dir(series) + assert "address" in dir(series) + assert "city" in dir(series.address) + assert "country" in dir(series.address) + + +def test_series_struct_class_attributes_shadow_struct_fields(nested_structs_df): + series = nested_structs_df["person"] + + assert series.name == "person" From 4af5bbb9e42fdb0add17308475c7881d7035fbfd Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:15:53 -0700 Subject: [PATCH 867/921] feat: add DeprecationWarning for PaLM2TextEmbeddingGenerator (#1018) * feat: add DeprecationWarning for PaLM2TextEmbeddingGenerator * fix wording * fix wording * use typing_extensions instead --- bigframes/exceptions.py | 4 ++++ bigframes/ml/llm.py | 9 +++++++-- tests/system/small/ml/test_llm.py | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/bigframes/exceptions.py b/bigframes/exceptions.py index 00abb887b02..462bdf2bddc 100644 --- a/bigframes/exceptions.py +++ b/bigframes/exceptions.py @@ -69,3 +69,7 @@ class AmbiguousWindowWarning(Warning): class UnknownDataTypeWarning(Warning): """Data type is unknown.""" + + +class ApiDeprecationWarning(FutureWarning): + """The API has been deprecated.""" diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 3d11cd123e7..36b5955e833 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -21,9 +21,10 @@ import bigframes_vendored.constants as constants from google.cloud import bigquery +import typing_extensions import bigframes -from bigframes import clients +from bigframes import clients, exceptions from bigframes.core import blocks, log_adapter from bigframes.ml import base, core, globals, utils import bigframes.pandas as bpd @@ -403,12 +404,16 @@ def to_gbq(self, model_name: str, replace: bool = False) -> PaLM2TextGenerator: return new_model.session.read_gbq_model(model_name) +@typing_extensions.deprecated( + "PaLM2TextEmbeddingGenerator has been deprecated. Use TextEmbeddingGenerator(https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.TextEmbeddingGenerator) instead. ", + category=exceptions.ApiDeprecationWarning, +) @log_adapter.class_logger class PaLM2TextEmbeddingGenerator(base.BaseEstimator): """PaLM2 text embedding generator LLM model. .. note:: - Models in this class are outdated and going to be deprecated. To use the most updated text embedding models, go to the TextEmbeddingGenerator class. + PaLM2TextEmbeddingGenerator has been deprecated. Use TextEmbeddingGenerator(https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.TextEmbeddingGenerator) instead. Args: diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index a4a09731a18..73a77e6bddf 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -14,6 +14,7 @@ import pytest +from bigframes import exceptions from bigframes.ml import llm import bigframes.pandas as bpd from tests.system import utils @@ -447,3 +448,11 @@ def test_llm_gemini_pro_score_params(llm_fine_tune_df_default_index): ], index=6, ) + + +def test_palm2_text_embedding_deprecated(): + with pytest.warns(exceptions.ApiDeprecationWarning): + try: + llm.PaLM2TextEmbeddingGenerator() + except (Exception): + pass From 0359bc85839c37b5cd10c0c418b275ac0dc29c4a Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 1 Oct 2024 01:03:23 +0000 Subject: [PATCH 868/921] fix: remove pre-caching of remote function results (#1028) This will save redundant remote function execution as reported in b/370088754. The trade-off is that any remote function integration issues will be caughts only at a later point through a usage triggered sql execution. Why the caching is not working as indended in the reported use case in the bug? as per TrevorBergeron@ "the cached execution is useless when it is implicitly joined back to the base dataframe" --- bigframes/dataframe.py | 6 +----- bigframes/series.py | 10 ++-------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index ba63bbb0160..a87687af2ee 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3567,11 +3567,7 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs): ops.NaryRemoteFunctionOp(func=func), series_list[1:] ) result_series.name = None - - # Return Series with materialized result so that any error in the remote - # function is caught early - materialized_series = result_series.cache() - return materialized_series + return result_series # Per-column apply results = {name: func(col, *args, **kwargs) for name, col in self.items()} diff --git a/bigframes/series.py b/bigframes/series.py index d46f4ee9bd8..40b468ca7ee 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1475,10 +1475,7 @@ def apply( ops.RemoteFunctionOp(func=func, apply_on_null=True) ) - # return Series with materialized result so that any error in the remote - # function is caught early - materialized_series = result_series._cached(session_aware=False) - return materialized_series + return result_series def combine( self, @@ -1506,10 +1503,7 @@ def combine( other, ops.BinaryRemoteFunctionOp(func=func) ) - # return Series with materialized result so that any error in the remote - # function is caught early - materialized_series = result_series._cached() - return materialized_series + return result_series @validations.requires_index def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: From a5ad033480a8ab07de8139235af4f7e0ce5a8b40 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:38:02 -0700 Subject: [PATCH 869/921] chore: internal version of resample. (#1004) * chore: internal version of resample. * skip legacy pandas * replace generate array with range. * update casts * update to avoid ibis 9 aggregation bug. * re-order the code to avoid ibis 9 'must be qualified with a dataset' bug. * update comments * add unordered mode test * test case with duplicate * updates * doc updates * node updates * fix doc * fix doc --- bigframes/core/__init__.py | 10 + bigframes/core/blocks.py | 175 +++++++++++++++++ bigframes/core/compile/compiler.py | 34 ++++ bigframes/core/compile/scalar_op_compiler.py | 188 +++++++++++++++++++ bigframes/core/nodes.py | 40 ++++ bigframes/dataframe.py | 82 ++++++++ bigframes/operations/__init__.py | 30 +++ bigframes/series.py | 67 +++++++ tests/system/small/test_dataframe.py | 117 ++++++++++++ tests/system/small/test_series.py | 20 ++ tests/system/small/test_unordered.py | 48 +++++ 11 files changed, 811 insertions(+) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index a8b79223f4b..64021d50bcc 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -75,6 +75,16 @@ def from_pyarrow(cls, arrow_table: pa.Table, session: Session): ) return cls(node) + @classmethod + def from_range(cls, start, end, step): + return cls( + nodes.FromRangeNode( + start=start.node, + end=end.node, + step=step, + ) + ) + @classmethod def from_table( cls, diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 76cf065f894..36e8f8e73ca 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -23,6 +23,7 @@ import ast import dataclasses +import datetime import functools import itertools import random @@ -43,6 +44,7 @@ import bigframes_vendored.constants as constants import google.cloud.bigquery as bigquery +import numpy import pandas as pd import pyarrow as pa @@ -1834,6 +1836,179 @@ def transpose( .with_transpose_cache(self) ) + def _generate_sequence( + self, + start, + stop, + step: int = 1, + ): + range_expr = self.expr.from_range( + start, + stop, + step, + ) + + return Block( + range_expr, + column_labels=["min"], + index_columns=[], + ) + + def _generate_resample_label( + self, + rule: str, + closed: Optional[Literal["right", "left"]] = None, + label: Optional[Literal["right", "left"]] = None, + on: Optional[Label] = None, + level: typing.Union[LevelType, typing.Sequence[LevelType]] = None, + origin: Union[ + Union[pd.Timestamp, datetime.datetime, numpy.datetime64, int, float, str], + Literal["epoch", "start", "start_day", "end", "end_day"], + ] = "start_day", + ) -> Block: + # Validate and resolve the index or column to use for grouping + if on is None: + if len(self.index_columns) == 0: + raise ValueError( + f"No index for resampling. Expected {bigframes.dtypes.DATETIME_DTYPE} or " + f"{bigframes.dtypes.TIMESTAMP_DTYPE} index or 'on' parameter specifying a column." + ) + if len(self.index_columns) > 1 and (level is None): + raise ValueError( + "Multiple indices are not supported for this operation" + " when 'level' is not set." + ) + level = level or 0 + col_id = self.index.resolve_level(level)[0] + # Reset index to make the resampling level a column, then drop all other index columns. + # This simplifies processing by focusing solely on the column required for resampling. + block = self.reset_index(drop=False) + block = block.drop_columns( + [col for col in self.index.column_ids if col != col_id] + ) + elif level is not None: + raise ValueError("The Grouper cannot specify both a key and a level!") + else: + matches = self.label_to_col_id.get(on, []) + if len(matches) > 1: + raise ValueError( + f"Multiple columns matching id {on} were found. {constants.FEEDBACK_LINK}" + ) + if len(matches) == 0: + raise KeyError(f"The grouper name {on} is not found") + + col_id = matches[0] + block = self + if level is None: + dtype = self._column_type(col_id) + elif isinstance(level, int): + dtype = self.index.dtypes[level] + else: + dtype = self.index.dtypes[self.index.names.index(level)] + + if dtype not in ( + bigframes.dtypes.DATETIME_DTYPE, + bigframes.dtypes.TIMESTAMP_DTYPE, + ): + raise TypeError( + f"Invalid column type: {dtype}. Expected types are " + f"{bigframes.dtypes.DATETIME_DTYPE}, or " + f"{bigframes.dtypes.TIMESTAMP_DTYPE}." + ) + + freq = pd.tseries.frequencies.to_offset(rule) + assert freq is not None + + if origin not in ("epoch", "start", "start_day"): + raise ValueError( + "'origin' should be equal to 'epoch', 'start' or 'start_day'" + f". Got '{origin}' instead." + ) + + agg_specs = [ + ( + ex.UnaryAggregation(agg_ops.min_op, ex.deref(col_id)), + guid.generate_guid(), + ), + ] + origin_block = Block( + block.expr.aggregate(agg_specs, dropna=True), + column_labels=["origin"], + index_columns=[], + ) + + col_level = block.value_columns.index(col_id) + + block = block.merge( + origin_block, how="cross", left_join_ids=[], right_join_ids=[], sort=True + ) + + # After merging, the original column ids are altered. 'col_level' is the index of + # the datetime column used for resampling. 'block.value_columns[-1]' is the + # 'origin' column, which is the minimum datetime value. + block, label_col_id = block.apply_binary_op( + block.value_columns[col_level], + block.value_columns[-1], + op=ops.DatetimeToIntegerLabelOp(freq=freq, closed=closed, origin=origin), + ) + block = block.drop_columns([block.value_columns[-2]]) + + # Generate integer label sequence. + min_agg_specs = [ + ( + ex.UnaryAggregation(agg_ops.min_op, ex.deref(label_col_id)), + guid.generate_guid(), + ), + ] + max_agg_specs = [ + ( + ex.UnaryAggregation(agg_ops.max_op, ex.deref(label_col_id)), + guid.generate_guid(), + ), + ] + label_start = block.expr.aggregate(min_agg_specs, dropna=True) + label_stop = block.expr.aggregate(max_agg_specs, dropna=True) + + label_block = block._generate_sequence( + start=label_start, + stop=label_stop, + ) + + label_block = label_block.merge( + origin_block, how="cross", left_join_ids=[], right_join_ids=[], sort=True + ) + + block = label_block.merge( + block, + how="left", + left_join_ids=[label_block.value_columns[0]], + right_join_ids=[label_col_id], + sort=True, + ) + + block, resample_label_id = block.apply_binary_op( + block.value_columns[0], + block.value_columns[1], + op=ops.IntegerLabelToDatetimeOp(freq=freq, label=label, origin=origin), + ) + + # After multiple merges, the columns: + # - block.value_columns[0] is the integer label sequence, + # - block.value_columns[1] is the origin column (minimum datetime value), + # - col_level+2 represents the datetime column used for resampling, + # - block.value_columns[-2] is the integer label column derived from the datetime column. + # These columns are no longer needed. + block = block.drop_columns( + [ + block.value_columns[0], + block.value_columns[1], + block.value_columns[col_level + 2], + block.value_columns[-2], + ] + ) + + return block.set_index([resample_label_id]) + def _create_stack_column(self, col_label: typing.Tuple, stack_labels: pd.Index): dtype = None input_columns: list[Optional[str]] = [] diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index cbf35196516..de44739aba4 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -112,6 +112,40 @@ def compile_join(self, node: nodes.JoinNode, ordered: bool = True): conditions=condition_pairs, ) + @_compile_node.register + def compile_fromrange(self, node: nodes.FromRangeNode, ordered: bool = True): + # Both start and end are single elements and do not inherently have an order + start = self.compile_unordered_ir(node.start) + end = self.compile_unordered_ir(node.end) + start_table = start._to_ibis_expr() + end_table = end._to_ibis_expr() + + start_column = start_table.schema().names[0] + end_column = end_table.schema().names[0] + + # Perform a cross join to avoid errors + joined_table = start_table.cross_join(end_table) + + labels_array_table = ibis.range( + joined_table[start_column], joined_table[end_column] + node.step, node.step + ).name("labels") + labels = ( + typing.cast(ibis.expr.types.ArrayValue, labels_array_table) + .unnest() + .as_table() + ) + if ordered: + return compiled.OrderedIR( + labels, + columns=[labels[labels.columns[0]]], + ordering=bf_ordering.TotalOrdering().from_offset_col(labels.columns[0]), + ) + else: + return compiled.UnorderedIR( + labels, + columns=[labels[labels.columns[0]]], + ) + @_compile_node.register def compile_readlocal(self, node: nodes.ReadLocalNode, ordered: bool = True): array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 78181744660..729b341e85e 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -729,6 +729,194 @@ def floor_dt_op_impl(x: ibis_types.Value, op: ops.FloorDtOp): return result.cast(result_type) +@scalar_op_compiler.register_binary_op(ops.DatetimeToIntegerLabelOp, pass_op=True) +def datetime_to_integer_label_op_impl( + x: ibis_types.Value, y: ibis_types.Value, op: ops.DatetimeToIntegerLabelOp +): + # Determine if the frequency is fixed by checking if 'op.freq.nanos' is defined. + try: + return datetime_to_integer_label_fixed_frequency(x, y, op) + except ValueError: + return datetime_to_integer_label_non_fixed_frequency(x, y, op) + + +def datetime_to_integer_label_fixed_frequency( + x: ibis_types.Value, y: ibis_types.Value, op: ops.DatetimeToIntegerLabelOp +): + """ + This function handles fixed frequency conversions where the unit can range + from microseconds (us) to days. + """ + us = op.freq.nanos / 1000 + x_int = x.cast(ibis_dtypes.Timestamp(timezone="UTC")).cast(ibis_dtypes.int64) + first = calculate_resample_first(y, op.origin) + x_int_label = (x_int - first) // us + return x_int_label + + +def datetime_to_integer_label_non_fixed_frequency( + x: ibis_types.Value, y: ibis_types.Value, op: ops.DatetimeToIntegerLabelOp +): + """ + This function handles non-fixed frequency conversions for units ranging + from weeks to years. + """ + rule_code = op.freq.rule_code + n = op.freq.n + if rule_code == "W-SUN": # Weekly + us = n * 7 * 24 * 60 * 60 * 1000000 + x = x.truncate("week") + ibis.interval(days=6) + y = y.truncate("week") + ibis.interval(days=6) + x_int = x.cast(ibis_dtypes.Timestamp(timezone="UTC")).cast(ibis_dtypes.int64) + first = y.cast(ibis_dtypes.Timestamp(timezone="UTC")).cast(ibis_dtypes.int64) + x_int_label = ( + ibis.case() + .when(x_int == first, 0) + .else_((x_int - first - 1) // us + 1) + .end() + ) + elif rule_code == "ME": # Monthly + x_int = x.year() * 12 + x.month() - 1 + first = y.year() * 12 + y.month() - 1 + x_int_label = ( + ibis.case() + .when(x_int == first, 0) + .else_((x_int - first - 1) // n + 1) + .end() + ) + elif rule_code == "QE-DEC": # Quarterly + x_int = x.year() * 4 + x.quarter() - 1 + first = y.year() * 4 + y.quarter() - 1 + x_int_label = ( + ibis.case() + .when(x_int == first, 0) + .else_((x_int - first - 1) // n + 1) + .end() + ) + elif rule_code == "YE-DEC": # Yearly + x_int = x.year() + first = y.year() + x_int_label = ( + ibis.case() + .when(x_int == first, 0) + .else_((x_int - first - 1) // n + 1) + .end() + ) + else: + raise ValueError(rule_code) + return x_int_label + + +@scalar_op_compiler.register_binary_op(ops.IntegerLabelToDatetimeOp, pass_op=True) +def integer_label_to_datetime_op_impl( + x: ibis_types.Value, y: ibis_types.Value, op: ops.IntegerLabelToDatetimeOp +): + # Determine if the frequency is fixed by checking if 'op.freq.nanos' is defined. + try: + return integer_label_to_datetime_op_fixed_frequency(x, y, op) + except ValueError: + return integer_label_to_datetime_op_non_fixed_frequency(x, y, op) + + +def integer_label_to_datetime_op_fixed_frequency( + x: ibis_types.Value, y: ibis_types.Value, op: ops.IntegerLabelToDatetimeOp +): + """ + This function handles fixed frequency conversions where the unit can range + from microseconds (us) to days. + """ + us = op.freq.nanos / 1000 + + first = calculate_resample_first(y, op.origin) + + x_label = ( + (x * us + first) + .cast(ibis_dtypes.int64) + .to_timestamp(unit="us") + .cast(ibis_dtypes.Timestamp(timezone="UTC")) + .cast(y.type()) + ) + return x_label + + +def integer_label_to_datetime_op_non_fixed_frequency( + x: ibis_types.Value, y: ibis_types.Value, op: ops.IntegerLabelToDatetimeOp +): + """ + This function handles non-fixed frequency conversions for units ranging + from weeks to years. + """ + rule_code = op.freq.rule_code + n = op.freq.n + if rule_code == "W-SUN": # Weekly + us = n * 7 * 24 * 60 * 60 * 1000000 + first = ( + y.cast(ibis_dtypes.Timestamp(timezone="UTC")).truncate("week") + + ibis.interval(days=6) + ).cast(ibis_dtypes.int64) + x_label = ( + (x * us + first) + .cast(ibis_dtypes.int64) + .to_timestamp(unit="us") + .cast(ibis_dtypes.Timestamp(timezone="UTC")) + .cast(y.type()) + ) + elif rule_code == "ME": # Monthly + one = ibis.literal(1) + twelve = ibis.literal(12) + first = y.year() * twelve + y.month() - one + + x = x * n + first + year = x // twelve + month = (x % twelve) + one + + next_year = (month == twelve).ifelse(year + one, year) + next_month = (month == twelve).ifelse(one, month + one) + next_month_date = ibis.timestamp(next_year, next_month, one, 0, 0, 0) + + x_label = next_month_date - ibis.interval(days=1) + elif rule_code == "QE-DEC": # Quarterly + one = ibis.literal(1) + three = ibis.literal(3) + four = ibis.literal(4) + twelve = ibis.literal(12) + first = y.year() * four + y.quarter() - one + + x = x * n + first + year = x // four + month = ((x % four) + one) * three + + next_year = (month == twelve).ifelse(year + one, year) + next_month = (month == twelve).ifelse(one, month + one) + next_month_date = ibis.timestamp(next_year, next_month, one, 0, 0, 0) + + x_label = next_month_date - ibis.interval(days=1) + elif rule_code == "YE-DEC": # Yearly + one = ibis.literal(1) + first = y.year() + x = x * n + first + next_year = x + one + next_month_date = ibis.timestamp(next_year, 1, 1, 0, 0, 0) + x_label = next_month_date - ibis.interval(days=1) + + return x_label.cast(ibis_dtypes.Timestamp(timezone="UTC")).cast(y.type()) + + +def calculate_resample_first(y: ibis_types.Value, origin): + if origin == "epoch": + return ibis.literal(0) + elif origin == "start_day": + return ( + y.cast(ibis_dtypes.date) + .cast(ibis_dtypes.Timestamp(timezone="UTC")) + .cast(ibis_dtypes.int64) + ) + elif origin == "start": + return y.cast(ibis_dtypes.Timestamp(timezone="UTC")).cast(ibis_dtypes.int64) + else: + raise ValueError(f"Origin {origin} not supported") + + @scalar_op_compiler.register_unary_op(ops.time_op) def time_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.TimestampValue, x).time() diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 2649b41227c..de1dc5d012c 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -351,6 +351,46 @@ def transform_children( return replace(self, children=tuple(t(child) for child in self.children)) +@dataclass(frozen=True) +class FromRangeNode(BigFrameNode): + start: BigFrameNode + end: BigFrameNode + step: int + + def __hash__(self): + return self._node_hash + + @property + def roots(self) -> typing.Set[BigFrameNode]: + return {self} + + @property + def child_nodes(self) -> typing.Sequence[BigFrameNode]: + return (self.start, self.end) + + @property + def order_ambiguous(self) -> bool: + return False + + @property + def explicitly_ordered(self) -> bool: + return True + + @functools.cached_property + def fields(self) -> Tuple[Field, ...]: + return (Field(bfet_ids.ColumnId("labels"), self.start.fields[0].dtype),) + + @functools.cached_property + def variables_introduced(self) -> int: + """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" + return len(self.schema.items) + OVERHEAD_VARIABLES + + def transform_children( + self, t: Callable[[BigFrameNode], BigFrameNode] + ) -> BigFrameNode: + return replace(self, start=t(self.start), end=t(self.end)) + + # Input Nodex # TODO: Most leaf nodes produce fixed column names based on the datasource # They should support renaming diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index a87687af2ee..d9f7cb9f420 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2979,6 +2979,88 @@ def _split( blocks = self._block.split(ns=ns, fracs=fracs, random_state=random_state) return [DataFrame(block) for block in blocks] + @validations.requires_ordering() + def _resample( + self, + rule: str, + *, + on: blocks.Label = None, + level: Optional[LevelsType] = None, + origin: Union[ + Union[ + pandas.Timestamp, datetime.datetime, numpy.datetime64, int, float, str + ], + Literal["epoch", "start", "start_day", "end", "end_day"], + ] = "start_day", + ) -> bigframes.core.groupby.DataFrameGroupBy: + """Internal function to support resample. Resample time-series data. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pandas as pd + >>> bpd.options.display.progress_bar = None + + >>> data = { + ... "timestamp_col": pd.date_range( + ... start="2021-01-01 13:00:00", periods=30, freq="1s" + ... ), + ... "int64_col": range(30), + ... "int64_too": range(10, 40), + ... } + + Resample on a DataFrame with index: + + >>> df = bpd.DataFrame(data).set_index("timestamp_col") + >>> df._resample(rule="7s").min() + int64_col int64_too + 2021-01-01 12:59:55 0 10 + 2021-01-01 13:00:02 2 12 + 2021-01-01 13:00:09 9 19 + 2021-01-01 13:00:16 16 26 + 2021-01-01 13:00:23 23 33 + + [5 rows x 2 columns] + + Resample with column and origin set to 'start': + + >>> df = bpd.DataFrame(data) + >>> df._resample(rule="7s", on = "timestamp_col", origin="start").min() + int64_col int64_too + 2021-01-01 13:00:00 0 10 + 2021-01-01 13:00:07 7 17 + 2021-01-01 13:00:14 14 24 + 2021-01-01 13:00:21 21 31 + 2021-01-01 13:00:28 28 38 + + [5 rows x 2 columns] + + Args: + rule (str): + The offset string representing target conversion. + on (str, default None): + For a DataFrame, column to use instead of index for resampling. Column + must be datetime-like. + level (str or int, default None): + For a MultiIndex, level (name or number) to use for resampling. + level must be datetime-like. + origin(str, default 'start_day'): + The timestamp on which to adjust the grouping. Must be one of the following: + 'epoch': origin is 1970-01-01 + 'start': origin is the first value of the timeseries + 'start_day': origin is the first day at midnight of the timeseries + Returns: + DataFrameGroupBy: DataFrameGroupBy object. + """ + block = self._block._generate_resample_label( + rule=rule, + on=on, + level=level, + origin=origin, + ) + df = DataFrame(block) + return df.groupby(level=0) + @classmethod def from_dict( cls, diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index ad2598b2af4..63127a70de7 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -15,12 +15,14 @@ from __future__ import annotations import dataclasses +import datetime import functools import typing from typing import Union import numpy as np import pandas as pd +from pandas.tseries.offsets import DateOffset import pyarrow as pa import bigframes.dtypes @@ -589,6 +591,34 @@ def output_type(self, *input_types): return input_types[0] +@dataclasses.dataclass(frozen=True) +class DatetimeToIntegerLabelOp(BinaryOp): + name: typing.ClassVar[str] = "datetime_to_integer_label" + freq: DateOffset + closed: typing.Optional[typing.Literal["right", "left"]] + origin: Union[ + Union[pd.Timestamp, datetime.datetime, np.datetime64, int, float, str], + typing.Literal["epoch", "start", "start_day", "end", "end_day"], + ] + + def output_type(self, *input_types): + return dtypes.INT_DTYPE + + +@dataclasses.dataclass(frozen=True) +class IntegerLabelToDatetimeOp(BinaryOp): + name: typing.ClassVar[str] = "integer_label_to_datetime" + freq: DateOffset + label: typing.Optional[typing.Literal["right", "left"]] + origin: Union[ + Union[pd.Timestamp, datetime.datetime, np.datetime64, int, float, str], + typing.Literal["epoch", "start", "start_day", "end", "end_day"], + ] + + def output_type(self, *input_types): + return input_types[1] + + ## Array Ops @dataclasses.dataclass(frozen=True) class ArrayToStringOp(UnaryOp): diff --git a/bigframes/series.py b/bigframes/series.py index 40b468ca7ee..16e2eef6f15 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -16,6 +16,7 @@ from __future__ import annotations +import datetime import functools import inspect import itertools @@ -1818,6 +1819,72 @@ def explode(self, *, ignore_index: Optional[bool] = False) -> Series: ) ) + @validations.requires_ordering() + def _resample( + self, + rule: str, + *, + closed: Optional[Literal["right", "left"]] = None, + label: Optional[Literal["right", "left"]] = None, + level: Optional[LevelsType] = None, + origin: Union[ + Union[ + pandas.Timestamp, datetime.datetime, numpy.datetime64, int, float, str + ], + Literal["epoch", "start", "start_day", "end", "end_day"], + ] = "start_day", + ) -> bigframes.core.groupby.SeriesGroupBy: + """Internal function to support resample. Resample time-series data. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pandas as pd + >>> bpd.options.display.progress_bar = None + + >>> data = { + ... "timestamp_col": pd.date_range( + ... start="2021-01-01 13:00:00", periods=30, freq="1s" + ... ), + ... "int64_col": range(30), + ... } + >>> s = bpd.DataFrame(data).set_index("timestamp_col") + >>> s._resample(rule="7s", origin="epoch").min() + int64_col + 2021-01-01 12:59:56 0 + 2021-01-01 13:00:03 3 + 2021-01-01 13:00:10 10 + 2021-01-01 13:00:17 17 + 2021-01-01 13:00:24 24 + + [5 rows x 1 columns] + + + Args: + rule (str): + The offset string representing target conversion. + level (str or int, default None): + For a MultiIndex, level (name or number) to use for resampling. + level must be datetime-like. + origin(str, default 'start_day'): + The timestamp on which to adjust the grouping. Must be one of the following: + 'epoch': origin is 1970-01-01 + 'start': origin is the first value of the timeseries + 'start_day': origin is the first day at midnight of the timeseries + Returns: + SeriesGroupBy: SeriesGroupBy object. + """ + block = self._block._generate_resample_label( + rule=rule, + closed=closed, + label=label, + on=None, + level=level, + origin=origin, + ) + series = Series(block) + return series.groupby(level=0) + def __array_ufunc__( self, ufunc: numpy.ufunc, method: str, *inputs, **kwargs ) -> Series: diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index d44afb4fe00..f8604f7207d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4877,3 +4877,120 @@ def test_dataframe_explode_reserve_order(ignore_index, ordered): def test_dataframe_explode_xfail(col_names): df = bpd.DataFrame({"A": [[0, 1, 2], [], [3, 4]]}) df.explode(col_names) + + +@skip_legacy_pandas +@pytest.mark.parametrize( + ("on", "rule", "origin"), + [ + pytest.param("datetime_col", "100D", "start"), + pytest.param("datetime_col", "30W", "start"), + pytest.param("datetime_col", "5M", "epoch"), + pytest.param("datetime_col", "3Q", "start_day"), + pytest.param("datetime_col", "3YE", "start"), + pytest.param( + "int64_col", "100D", "start", marks=pytest.mark.xfail(raises=TypeError) + ), + pytest.param( + "datetime_col", "100D", "end", marks=pytest.mark.xfail(raises=ValueError) + ), + ], +) +def test__resample_with_column( + scalars_df_index, scalars_pandas_df_index, on, rule, origin +): + bf_result = ( + scalars_df_index._resample(rule=rule, on=on, origin=origin)[ + ["int64_col", "int64_too"] + ] + .max() + .to_pandas() + ) + pd_result = scalars_pandas_df_index.resample(rule=rule, on=on, origin=origin)[ + ["int64_col", "int64_too"] + ].max() + pd.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + +@skip_legacy_pandas +@pytest.mark.parametrize( + ("append", "level", "col", "rule"), + [ + pytest.param(False, None, "timestamp_col", "100d"), + pytest.param(True, 1, "timestamp_col", "1200h"), + pytest.param(False, None, "datetime_col", "100d"), + ], +) +def test__resample_with_index( + scalars_df_index, scalars_pandas_df_index, append, level, col, rule +): + scalars_df_index = scalars_df_index.set_index(col, append=append) + scalars_pandas_df_index = scalars_pandas_df_index.set_index(col, append=append) + bf_result = ( + scalars_df_index[["int64_col", "int64_too"]] + ._resample(rule=rule, level=level) + .min() + .to_pandas() + ) + pd_result = ( + scalars_pandas_df_index[["int64_col", "int64_too"]] + .resample(rule=rule, level=level) + .min() + ) + assert_pandas_df_equal(bf_result, pd_result) + + +@skip_legacy_pandas +@pytest.mark.parametrize( + ("rule", "origin", "data"), + [ + ( + "5h", + "epoch", + { + "timestamp_col": pd.date_range( + start="2021-01-01 13:00:00", periods=30, freq="1h" + ), + "int64_col": range(30), + "int64_too": range(10, 40), + }, + ), + ( + "75min", + "start_day", + { + "timestamp_col": pd.date_range( + start="2021-01-01 13:00:00", periods=30, freq="10min" + ), + "int64_col": range(30), + "int64_too": range(10, 40), + }, + ), + ( + "7s", + "epoch", + { + "timestamp_col": pd.date_range( + start="2021-01-01 13:00:00", periods=30, freq="1s" + ), + "int64_col": range(30), + "int64_too": range(10, 40), + }, + ), + ], +) +def test__resample_start_time(rule, origin, data): + col = "timestamp_col" + scalars_df_index = bpd.DataFrame(data).set_index(col) + scalars_pandas_df_index = pd.DataFrame(data).set_index(col) + scalars_pandas_df_index.index.name = None + + bf_result = scalars_df_index._resample(rule=rule, origin=origin).min().to_pandas() + + pd_result = scalars_pandas_df_index.resample(rule=rule, origin=origin).min() + + pd.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index aa70b7c6550..624e287f8de 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -3915,6 +3915,26 @@ def test_series_explode_null(data): ) +@skip_legacy_pandas +@pytest.mark.parametrize( + ("append", "level", "col", "rule"), + [ + pytest.param(False, None, "timestamp_col", "75D"), + pytest.param(True, 1, "timestamp_col", "25W"), + pytest.param(False, None, "datetime_col", "3ME"), + pytest.param(True, "timestamp_col", "timestamp_col", "1YE"), + ], +) +def test__resample(scalars_df_index, scalars_pandas_df_index, append, level, col, rule): + scalars_df_index = scalars_df_index.set_index(col, append=append)["int64_col"] + scalars_pandas_df_index = scalars_pandas_df_index.set_index(col, append=append)[ + "int64_col" + ] + bf_result = scalars_df_index._resample(rule=rule, level=level).min().to_pandas() + pd_result = scalars_pandas_df_index.resample(rule=rule, level=level).min() + pd.testing.assert_series_equal(bf_result, pd_result) + + def test_series_struct_get_field_by_attribute( nested_structs_df, nested_structs_pandas_df, nested_structs_pandas_type ): diff --git a/tests/system/small/test_unordered.py b/tests/system/small/test_unordered.py index 5e124d73cdd..fe3411e2660 100644 --- a/tests/system/small/test_unordered.py +++ b/tests/system/small/test_unordered.py @@ -198,3 +198,51 @@ def test_unordered_mode_no_ambiguity_warning(unordered_session): with warnings.catch_warnings(): warnings.simplefilter("error") df.groupby("a").head(3) + + +@skip_legacy_pandas +@pytest.mark.parametrize( + ("rule", "origin", "data"), + [ + ( + "5h", + "epoch", + { + "timestamp_col": pd.date_range( + start="2021-01-01 13:00:00", periods=30, freq="1h" + ), + "int64_col": range(30), + "int64_too": range(10, 40), + }, + ), + ( + "5h", + "epoch", + { + "timestamp_col": pd.DatetimeIndex( + pd.date_range( + start="2021-01-01 13:00:00", periods=15, freq="1h" + ).tolist() + + pd.date_range( + start="2021-01-01 13:00:00", periods=15, freq="1h" + ).tolist() + ), + "int64_col": range(30), + "int64_too": range(10, 40), + }, + ), + ], +) +def test__resample_with_index(unordered_session, rule, origin, data): + col = "timestamp_col" + scalars_df_index = bpd.DataFrame(data, session=unordered_session).set_index(col) + scalars_pandas_df_index = pd.DataFrame(data).set_index(col) + scalars_pandas_df_index.index.name = None + + bf_result = scalars_df_index._resample(rule=rule, origin=origin).min().to_pandas() + + pd_result = scalars_pandas_df_index.resample(rule=rule, origin=origin).min() + + pd.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) From 4992cc27e46bc2b0a908c7d521785989735186f4 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 1 Oct 2024 10:12:06 -0700 Subject: [PATCH 870/921] fix: Ensure no double execution for to_pandas (#1032) --- bigframes/core/blocks.py | 2 +- tests/system/small/test_dataframe.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 36e8f8e73ca..9e245399cd0 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -636,7 +636,7 @@ def _materialize_local( ) else: total_rows = execute_result.total_rows - arrow = self.session._executor.execute(self.expr).to_arrow_table() + arrow = execute_result.to_arrow_table() df = io_pandas.arrow_to_pandas(arrow, schema=self.expr.schema) self._copy_index_to_pandas(df) diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index f8604f7207d..8c2912edd4a 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4821,20 +4821,33 @@ def test_to_gbq_table_labels(scalars_df_index): pytest.param(["A", "C"], True, id="two_arrays_true"), ], ) -def test_dataframe_explode(col_names, ignore_index): +def test_dataframe_explode(col_names, ignore_index, session): data = { "A": [[0, 1, 2], [], [3, 4]], "B": 3, "C": [["a", "b", "c"], np.nan, ["d", "e"]], } - df = bpd.DataFrame(data) + + metrics = session._metrics + df = bpd.DataFrame(data, session=session) pd_df = df.to_pandas() + pd_result = pd_df.explode(col_names, ignore_index=ignore_index) + bf_result = df.explode(col_names, ignore_index=ignore_index) + + # Check that to_pandas() results in at most a single query execution + execs_pre = metrics.execution_count + bf_materialized = bf_result.to_pandas() + execs_post = metrics.execution_count + pd.testing.assert_frame_equal( - df.explode(col_names, ignore_index=ignore_index).to_pandas(), - pd_df.explode(col_names, ignore_index=ignore_index), + bf_materialized, + pd_result, check_index_type=False, check_dtype=False, ) + # we test this property on this method in particular as compilation + # is non-deterministic and won't use the query cache as implemented + assert execs_post - execs_pre <= 1 @pytest.mark.parametrize( From 1183b0fb2be7af7386e4bd0d0d1312433db60454 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:49:51 -0700 Subject: [PATCH 871/921] feat: add deprecation warning to PaLM2TextGenerator model (#1035) --- bigframes/ml/llm.py | 7 +++++++ tests/system/small/ml/test_llm.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 36b5955e833..c12da01b54c 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -84,10 +84,17 @@ _ML_GENERATE_EMBEDDING_STATUS = "ml_generate_embedding_status" +@typing_extensions.deprecated( + "PaLM2TextGenerator is going to be deprecated. Use GeminiTextGenerator(https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.GeminiTextGenerator) instead. ", + category=exceptions.ApiDeprecationWarning, +) @log_adapter.class_logger class PaLM2TextGenerator(base.BaseEstimator): """PaLM2 text generator LLM model. + .. note:: + PaLM2TextGenerator is going to be deprecated. Use GeminiTextGenerator(https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.GeminiTextGenerator) instead. + Args: model_name (str, Default to "text-bison"): The model for natural language tasks. “text-bison” returns model fine-tuned to follow natural language instructions diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 73a77e6bddf..78fed6b82f5 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -450,6 +450,11 @@ def test_llm_gemini_pro_score_params(llm_fine_tune_df_default_index): ) +def test_palm2_text_generator_deprecated(): + with pytest.warns(exceptions.ApiDeprecationWarning): + llm.PaLM2TextGenerator() + + def test_palm2_text_embedding_deprecated(): with pytest.warns(exceptions.ApiDeprecationWarning): try: From 057f3f0d694ddffe8745443a85b4fb43081893bb Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:57:49 -0700 Subject: [PATCH 872/921] docs: add ml cross-validation notebook (#1037) --- .../bq_dataframes_ml_cross_validation.ipynb | 1105 +++++++++++++++++ .../bq_dataframes_ml_linear_regression.ipynb | 0 .../easy_linear_regression.ipynb | 0 .../sklearn_linear_regression.ipynb | 0 noxfile.py | 2 +- 5 files changed, 1106 insertions(+), 1 deletion(-) create mode 100644 notebooks/ml/bq_dataframes_ml_cross_validation.ipynb rename notebooks/{regression => ml}/bq_dataframes_ml_linear_regression.ipynb (100%) rename notebooks/{regression => ml}/easy_linear_regression.ipynb (100%) rename notebooks/{regression => ml}/sklearn_linear_regression.ipynb (100%) diff --git a/notebooks/ml/bq_dataframes_ml_cross_validation.ipynb b/notebooks/ml/bq_dataframes_ml_cross_validation.ipynb new file mode 100644 index 00000000000..824d911aff6 --- /dev/null +++ b/notebooks/ml/bq_dataframes_ml_cross_validation.ipynb @@ -0,0 +1,1105 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BigFrames ML Cross-Vaidation\n", + "\n", + "This demo shows how to do cross validation in bigframes.ml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Prepare Data" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/garrettwu/src/bigframes/venv/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3577: UserWarning: Reading cached table from 2024-10-01 22:44:50.650768+00:00 to avoid incompatibilies with previous reads of this table. To read the latest version, set `use_cache=False` or close the current session with Session.close() or bigframes.pandas.close_session().\n", + " exec(code_obj, self.user_global_ns, self.user_ns)\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 4c2f2252-687a-47c3-87ad-22db8ad96e2b is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a05c7268-8db2-468b-9fb4-0fb5c9534f51 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Gentoo penguin (Pygoscelis papua)Biscoe50.515.9225.05400.0MALE
1Gentoo penguin (Pygoscelis papua)Biscoe45.114.5215.05000.0FEMALE
2Adelie Penguin (Pygoscelis adeliae)Torgersen41.418.5202.03875.0MALE
3Adelie Penguin (Pygoscelis adeliae)Torgersen38.617.0188.02900.0FEMALE
4Gentoo penguin (Pygoscelis papua)Biscoe46.514.8217.05200.0FEMALE
........................
339Adelie Penguin (Pygoscelis adeliae)Dream38.117.6187.03425.0FEMALE
340Adelie Penguin (Pygoscelis adeliae)Biscoe36.417.1184.02850.0FEMALE
341Chinstrap penguin (Pygoscelis antarctica)Dream40.916.6187.03200.0FEMALE
342Adelie Penguin (Pygoscelis adeliae)Biscoe41.321.1195.04400.0MALE
343Chinstrap penguin (Pygoscelis antarctica)Dream45.216.6191.03250.0FEMALE
\n", + "

334 rows × 7 columns

\n", + "
[334 rows x 7 columns in total]" + ], + "text/plain": [ + " species island culmen_length_mm \\\n", + "0 Gentoo penguin (Pygoscelis papua) Biscoe 50.5 \n", + "1 Gentoo penguin (Pygoscelis papua) Biscoe 45.1 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Torgersen 41.4 \n", + "3 Adelie Penguin (Pygoscelis adeliae) Torgersen 38.6 \n", + "4 Gentoo penguin (Pygoscelis papua) Biscoe 46.5 \n", + ".. ... ... ... \n", + "339 Adelie Penguin (Pygoscelis adeliae) Dream 38.1 \n", + "340 Adelie Penguin (Pygoscelis adeliae) Biscoe 36.4 \n", + "341 Chinstrap penguin (Pygoscelis antarctica) Dream 40.9 \n", + "342 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.3 \n", + "343 Chinstrap penguin (Pygoscelis antarctica) Dream 45.2 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 15.9 225.0 5400.0 MALE \n", + "1 14.5 215.0 5000.0 FEMALE \n", + "2 18.5 202.0 3875.0 MALE \n", + "3 17.0 188.0 2900.0 FEMALE \n", + "4 14.8 217.0 5200.0 FEMALE \n", + ".. ... ... ... ... \n", + "339 17.6 187.0 3425.0 FEMALE \n", + "340 17.1 184.0 2850.0 FEMALE \n", + "341 16.6 187.0 3200.0 FEMALE \n", + "342 21.1 195.0 4400.0 MALE \n", + "343 16.6 191.0 3250.0 FEMALE \n", + "...\n", + "\n", + "[334 rows x 7 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# read and filter out unavailable data\n", + "df = bpd.read_gbq(\"bigframes-dev.bqml_tutorial.penguins\")\n", + "df = df.dropna()\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Select X and y from the dataset\n", + "X = df[\n", + " [\n", + " \"species\",\n", + " \"island\",\n", + " \"culmen_length_mm\",\n", + " ]\n", + " ]\n", + "y = df[\"body_mass_g\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.1 Define KFold class and Train/Test for Each Fold (Mauanl Approach)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from bigframes.ml import model_selection, linear_model" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Create KFold instance, n_splits defines how many folds the data will split. For example, n_split=5 will split the entire dataset into 5 pieces. \n", + "# In each fold, 4 pieces will be used for training, and the other piece will be used for evaluation. \n", + "kf = model_selection.KFold(n_splits=5)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 582e7c02-bcc6-412a-a513-46ee5dba7ad8 is DONE. 2.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 917ff09b-072b-4c55-b26f-1780e2e97519 is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 2f4e102d-48bc-401f-a781-39830e2c6c9b is DONE. 16.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job aabe8a28-8dce-4e00-8a8c-18e9e090e6e7 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ec9d8798-e28e-44bc-aa8e-44ab28f0214f is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8aa0fa94-e43e-41c6-9de3-f0a67392c47f is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 318.358226 151689.571141 0.009814 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 255.095561 0.780659 0.783304 \n", + "\n", + "[1 rows x 6 columns]\n" + ] + }, + { + "data": { + "text/html": [ + "Query job bf6ef937-9583-4aa8-8313-563638465d5f is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 4c8b564c-5bbd-4447-babf-e307524962e5 is DONE. 16.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job cd5e337f-6d44-473d-a90b-be8a79bba6bf is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ad80012d-7c6c-4dbf-9271-2ff7f899f174 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8fc20587-d8ba-4c0f-bed9-3e1cf3c6ae52 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 306.435423 151573.84019 0.008539 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 244.2899 0.737623 0.742859 \n", + "\n", + "[1 rows x 6 columns]\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 90286d2b-e805-4b19-8876-c9973579e9ff is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ceb6c8f2-16cc-4758-bde8-3e4975ba1452 is DONE. 16.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f49434fa-a7e0-406a-bbe2-5651595e3418 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5dd7a277-10fe-4117-a354-ef8668a8b913 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 4b58b016-9a50-4a66-b86c-8431faad43bf is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 253.349578 112039.741164 0.007153 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 185.916761 0.823381 0.823456 \n", + "\n", + "[1 rows x 6 columns]\n" + ] + }, + { + "data": { + "text/html": [ + "Query job ca700ecf-0c08-4286-b979-2bc7a0bee89c is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f0731e71-7754-47a2-a553-93a61e712533 is DONE. 16.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ae66d34d-5f0a-4297-9d41-57067ae54a9b is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7655a649-ceca-4792-b764-fb371f5872ec is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8b0634c8-73a9-422c-9644-842142dbb059 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 320.381386 155234.800349 0.008638 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 306.281263 0.793405 0.794504 \n", + "\n", + "[1 rows x 6 columns]\n" + ] + }, + { + "data": { + "text/html": [ + "Query job bb26cde9-1991-4e0a-8492-b19d15b1b7aa is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7ddd0883-492d-46bc-a588-f3cbab2474bb is DONE. 16.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5de571e4-d2f9-43c7-b014-3d65a3731b64 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d20ac7d8-cd21-4a1f-a200-2dfa6373bcdb is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 235e8a80-33ea-4a95-a7d0-34e40a8ca396 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 303.855563 141869.030392 0.008989 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 245.102301 0.731737 0.732793 \n", + "\n", + "[1 rows x 6 columns]\n" + ] + } + ], + "source": [ + "for X_train, X_test, y_train, y_test in kf.split(X, y):\n", + " model = linear_model.LinearRegression()\n", + " model.fit(X_train, y_train)\n", + " score = model.score(X_test, y_test)\n", + "\n", + " print(score)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.2 Use cross_validate Function to Do Cross Validation (Automatic Approach)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 9274ae2e-e9a7-4701-ac64-56632323d02a is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 22f9477b-de02-4c07-b480-c3270a69d7e0 is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ebb192b7-4a9e-4238-b4e6-b630e2f94988 is DONE. 16.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 44441e8c-8753-41b0-b1b7-9a6c4eab8c74 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 239fed9a-b488-47da-a0df-a3b7c6ec40f4 is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f4248b2d-3430-426c-872d-8590f2878366 is DONE. 16.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d9f6b034-c300-4dd7-91dd-48fa912f2456 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job e2f39f5b-2f4c-402a-a8d5-a7cff918508d is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 54cf3710-b5f4-4aec-b11f-0281126a151a is DONE. 16.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 833d13cd-ec59-499b-98f6-95ec18766698 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 0120e332-0691-44a4-9198-f5c131b8f59c is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f4ba7a4c-5fd9-4f97-ab34-a8f139e7472a is DONE. 16.4 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 857aadfc-2ade-429c-bef8-428e44d48c55 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 906d6d34-a506-4957-b07f-7e5ed2e0634b is DONE. 25.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 498563db-3e68-4df7-a2d5-83da6adb49ed is DONE. 16.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 01af95ca-6288-4253-b379-7327e1c9de88 is DONE. 26.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 5ce36d32-6db1-42e5-a8cf-84bb8244a57e is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job e05ec77d-6025-4edd-b5e3-9c4e7a124e71 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 418a4a5d-2bb3-41e5-9e7c-9852389a491b is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job b33e30da-cfed-4d6f-b227-f433d97879cb is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7ad7f0c8-ecae-4ef2-bc91-0ebeb5f88e7b is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a6e8bd12-1122-4c26-b0e1-58342238016c is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job c553439c-9586-479c-92c5-01a0d333125b is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job c598d64c-26b9-49fc-afad-a6544b38cfa2 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ebcb73e8-1294-4f10-b826-c495046fd714 is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d73f57ba-a25d-4b90-b474-13d81a3e22ab is DONE. 48 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "{'test_score': [ mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + " 0 237.154735 97636.17064 0.005571 \n", + " \n", + " median_absolute_error r2_score explained_variance \n", + " 0 187.883888 0.842018 0.846816 \n", + " \n", + " [1 rows x 6 columns],\n", + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + " 0 304.281635 141966.045867 0.008064 \n", + " \n", + " median_absolute_error r2_score explained_variance \n", + " 0 236.096453 0.762979 0.764008 \n", + " \n", + " [1 rows x 6 columns],\n", + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + " 0 316.380322 157332.146085 0.009699 \n", + " \n", + " median_absolute_error r2_score explained_variance \n", + " 0 222.824496 0.764607 0.765369 \n", + " \n", + " [1 rows x 6 columns],\n", + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + " 0 309.609657 152421.826588 0.009772 \n", + " \n", + " median_absolute_error r2_score explained_variance \n", + " 0 254.163976 0.772954 0.773119 \n", + " \n", + " [1 rows x 6 columns],\n", + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + " 0 339.339345 169760.629993 0.010597 \n", + " \n", + " median_absolute_error r2_score explained_variance \n", + " 0 312.335706 0.741167 0.74118 \n", + " \n", + " [1 rows x 6 columns]],\n", + " 'fit_time': [18.200648623984307,\n", + " 17.565149880945683,\n", + " 18.202434757025912,\n", + " 18.04062689607963,\n", + " 19.370970834977925],\n", + " 'score_time': [4.76077218609862,\n", + " 4.577479084953666,\n", + " 4.581933492794633,\n", + " 4.741644307971001,\n", + " 5.1031754210125655]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# By using model_selection.cross_validate, the above 2.1 process is automated. The returned scores contains the evaluation results for each fold.\n", + "model = linear_model.LinearRegression()\n", + "scores = model_selection.cross_validate(model, X, y, cv=5)\n", + "scores" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/regression/bq_dataframes_ml_linear_regression.ipynb b/notebooks/ml/bq_dataframes_ml_linear_regression.ipynb similarity index 100% rename from notebooks/regression/bq_dataframes_ml_linear_regression.ipynb rename to notebooks/ml/bq_dataframes_ml_linear_regression.ipynb diff --git a/notebooks/regression/easy_linear_regression.ipynb b/notebooks/ml/easy_linear_regression.ipynb similarity index 100% rename from notebooks/regression/easy_linear_regression.ipynb rename to notebooks/ml/easy_linear_regression.ipynb diff --git a/notebooks/regression/sklearn_linear_regression.ipynb b/notebooks/ml/sklearn_linear_regression.ipynb similarity index 100% rename from notebooks/regression/sklearn_linear_regression.ipynb rename to notebooks/ml/sklearn_linear_regression.ipynb diff --git a/noxfile.py b/noxfile.py index c704da00a52..714c8333bd4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -731,7 +731,7 @@ def notebook(session: nox.Session): # appropriate values and omitting cleanup logic that may break # our test infrastructure. "notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb", # Needs DATASET. - "notebooks/regression/bq_dataframes_ml_linear_regression.ipynb", # Needs DATASET_ID. + "notebooks/ml/bq_dataframes_ml_linear_regression.ipynb", # Needs DATASET_ID. "notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb", # Needs CONNECTION. # TODO(b/332737009): investigate why we get 404 errors, even though # bq_dataframes_llm_code_generation creates a bucket in the sample. From 4105dbaf4fad3a00c305a8ae3cb8d9de4d1e8916 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 1 Oct 2024 18:29:02 -0700 Subject: [PATCH 873/921] refactor: Make expression nodes prunable (#1030) --- bigframes/core/__init__.py | 91 +++++-- bigframes/core/compile/compiled.py | 14 +- bigframes/core/compile/compiler.py | 151 +++++------- bigframes/core/compile/default_ordering.py | 23 +- bigframes/core/compile/single_column.py | 26 +- bigframes/core/expression.py | 12 + bigframes/core/nodes.py | 270 +++++++++++++-------- bigframes/core/ordering.py | 23 +- bigframes/session/executor.py | 5 + 9 files changed, 358 insertions(+), 257 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 64021d50bcc..acab99f2498 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -67,11 +67,20 @@ def from_pyarrow(cls, arrow_table: pa.Table, session: Session): iobytes = io.BytesIO() pa_feather.write_feather(adapted_table, iobytes) + # Scan all columns by default, we define this list as it can be pruned while preserving source_def + scan_list = nodes.ScanList( + tuple( + nodes.ScanItem(ids.ColumnId(item.column), item.dtype, item.column) + for item in schema.items + ) + ) + node = nodes.ReadLocalNode( iobytes.getvalue(), data_schema=schema, session=session, n_rows=arrow_table.num_rows, + scan_list=scan_list, ) return cls(node) @@ -104,14 +113,30 @@ def from_table( "Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.", bigframes.exceptions.PreviewWarning, ) + # define data source only for needed columns, this makes row-hashing cheaper + table_def = nodes.GbqTable.from_table(table, columns=schema.names) + + # create ordering from info + ordering = None + if offsets_col: + ordering = orderings.TotalOrdering.from_offset_col(offsets_col) + elif primary_key: + ordering = orderings.TotalOrdering.from_primary_key(primary_key) + + # Scan all columns by default, we define this list as it can be pruned while preserving source_def + scan_list = nodes.ScanList( + tuple( + nodes.ScanItem(ids.ColumnId(item.column), item.dtype, item.column) + for item in schema.items + ) + ) + source_def = nodes.BigqueryDataSource( + table=table_def, at_time=at_time, sql_predicate=predicate, ordering=ordering + ) node = nodes.ReadTableNode( - table=nodes.GbqTable.from_table(table), - total_order_cols=(offsets_col,) if offsets_col else tuple(primary_key), - order_col_is_sequential=(offsets_col is not None), - columns=schema, - at_time=at_time, + source=source_def, + scan_list=scan_list, table_session=session, - sql_predicate=predicate, ) return cls(node) @@ -157,12 +182,22 @@ def as_cached( ordering: Optional[orderings.RowOrdering], ) -> ArrayValue: """ - Replace the node with an equivalent one that references a tabel where the value has been materialized to. + Replace the node with an equivalent one that references a table where the value has been materialized to. """ + table = nodes.GbqTable.from_table(cache_table) + source = nodes.BigqueryDataSource(table, ordering=ordering) + # Assumption: GBQ cached table uses field name as bq column name + scan_list = nodes.ScanList( + tuple( + nodes.ScanItem(field.id, field.dtype, field.id.name) + for field in self.node.fields + ) + ) node = nodes.CachedTableNode( original_node=self.node, - table=nodes.GbqTable.from_table(cache_table), - ordering=ordering, + source=source, + table_session=self.session, + scan_list=scan_list, ) return ArrayValue(node) @@ -379,28 +414,34 @@ def relational_join( conditions: typing.Tuple[typing.Tuple[str, str], ...] = (), type: typing.Literal["inner", "outer", "left", "right", "cross"] = "inner", ) -> typing.Tuple[ArrayValue, typing.Tuple[dict[str, str], dict[str, str]]]: + l_mapping = { # Identity mapping, only rename right side + lcol.name: lcol.name for lcol in self.node.ids + } + r_mapping = { # Rename conflicting names + rcol.name: rcol.name + if (rcol.name not in l_mapping) + else bigframes.core.guid.generate_guid() + for rcol in other.node.ids + } + other_node = other.node + if set(other_node.ids) & set(self.node.ids): + other_node = nodes.SelectionNode( + other_node, + tuple( + (ex.deref(old_id), ids.ColumnId(new_id)) + for old_id, new_id in r_mapping.items() + ), + ) + join_node = nodes.JoinNode( left_child=self.node, - right_child=other.node, + right_child=other_node, conditions=tuple( - (ex.deref(l_col), ex.deref(r_col)) for l_col, r_col in conditions + (ex.deref(l_mapping[l_col]), ex.deref(r_mapping[r_col])) + for l_col, r_col in conditions ), type=type, ) - # Maps input ids to output ids for caller convenience - l_size = len(self.node.schema) - l_mapping = { - lcol: ocol - for lcol, ocol in zip( - self.node.schema.names, join_node.schema.names[:l_size] - ) - } - r_mapping = { - rcol: ocol - for rcol, ocol in zip( - other.node.schema.names, join_node.schema.names[l_size:] - ) - } return ArrayValue(join_node), (l_mapping, r_mapping) def try_align_as_projection( diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 7be7013cfc0..f4afdaa97c0 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -45,7 +45,6 @@ RowOrdering, TotalOrdering, ) -import bigframes.core.schema as schemata import bigframes.core.sql from bigframes.core.window_spec import RangeWindowBounds, RowsWindowBounds, WindowSpec import bigframes.dtypes @@ -585,9 +584,7 @@ def has_total_order(self) -> bool: @classmethod def from_pandas( - cls, - pd_df: pandas.DataFrame, - schema: schemata.ArraySchema, + cls, pd_df: pandas.DataFrame, scan_cols: bigframes.core.nodes.ScanList ) -> OrderedIR: """ Builds an in-memory only (SQL only) expr from a pandas dataframe. @@ -603,10 +600,10 @@ def from_pandas( # derive the ibis schema from the original pandas schema ibis_schema = [ ( - name, + local_label, bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype(dtype), ) - for name, dtype in zip(schema.names, schema.dtypes) + for id, dtype, local_label in scan_cols.items ] ibis_schema.append((ORDER_ID_COLUMN, ibis_dtypes.int64)) @@ -614,7 +611,10 @@ def from_pandas( return cls( keys_memtable, - columns=[keys_memtable[column].name(column) for column in pd_df.columns], + columns=[ + keys_memtable[local_label].name(col_id.sql) + for col_id, _, local_label in scan_cols.items + ], ordering=TotalOrdering.from_offset_col(ORDER_ID_COLUMN), hidden_ordering_columns=(keys_memtable[ORDER_ID_COLUMN],), ) diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index de44739aba4..0917097c708 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -16,7 +16,6 @@ import dataclasses import functools import io -import itertools import typing import ibis @@ -29,9 +28,12 @@ import bigframes.core.compile.concat as concat_impl import bigframes.core.compile.default_ordering as default_ordering import bigframes.core.compile.ibis_types +import bigframes.core.compile.scalar_op_compiler +import bigframes.core.compile.scalar_op_compiler as compile_scalar import bigframes.core.compile.schema_translator import bigframes.core.compile.single_column -import bigframes.core.expression as ex +import bigframes.core.guid as guids +import bigframes.core.identifiers as ids import bigframes.core.nodes as nodes import bigframes.core.ordering as bf_ordering @@ -45,6 +47,7 @@ class Compiler: # In strict mode, ordering will always be deterministic # In unstrict mode, ordering from ReadTable or after joins may be ambiguous to improve query performance. strict: bool = True + scalar_op_compiler = compile_scalar.ScalarOpCompiler() def compile_ordered_ir(self, node: nodes.BigFrameNode) -> compiled.OrderedIR: ir = typing.cast(compiled.OrderedIR, self.compile_node(node, True)) @@ -148,88 +151,40 @@ def compile_fromrange(self, node: nodes.FromRangeNode, ordered: bool = True): @_compile_node.register def compile_readlocal(self, node: nodes.ReadLocalNode, ordered: bool = True): - array_as_pd = pd.read_feather(io.BytesIO(node.feather_bytes)) - ordered_ir = compiled.OrderedIR.from_pandas(array_as_pd, node.schema) + array_as_pd = pd.read_feather( + io.BytesIO(node.feather_bytes), + columns=[item.source_id for item in node.scan_list.items], + ) + ordered_ir = compiled.OrderedIR.from_pandas(array_as_pd, node.scan_list) if ordered: return ordered_ir else: return ordered_ir.to_unordered() - @_compile_node.register - def compile_cached_table(self, node: nodes.CachedTableNode, ordered: bool = True): - full_table_name = ( - f"{node.table.project_id}.{node.table.dataset_id}.{node.table.table_id}" - ) - physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( - node.table.physical_schema - ) - ibis_table = ibis.table(physical_schema, full_table_name) - if ordered: - if node.ordering is None: - # If this happens, session malfunctioned while applying cached results. - raise ValueError( - "Cannot use unordered cached value. Result requires ordering information." - ) - if self.strict and not isinstance(node.ordering, bf_ordering.TotalOrdering): - raise ValueError( - "Cannot use partially ordered cached value. Result requires total ordering information." - ) - ir = compiled.OrderedIR( - ibis_table, - columns=tuple( - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - ibis_table[col.sql] - ) - for col in itertools.chain( - map(lambda x: x.id, node.fields), node._hidden_columns - ) - ), - ordering=node.ordering, - ) - ir = ir._select( - tuple(ir._get_ibis_column(name) for name in node.schema.names) - ) - return ir - else: - return compiled.UnorderedIR( - ibis_table, - columns=tuple( - bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - ibis_table[col] - ) - for col in node.schema.names - ), - ) - @_compile_node.register def compile_readtable(self, node: nodes.ReadTableNode, ordered: bool = True): if ordered: - return self.compile_read_table_ordered(node) + return self.compile_read_table_ordered(node.source, node.scan_list) else: - return self.compile_read_table_unordered(node) + return self.compile_read_table_unordered(node.source, node.scan_list) def read_table_as_unordered_ibis( - self, node: nodes.ReadTableNode + self, source: nodes.BigqueryDataSource ) -> ibis.expr.types.Table: - full_table_name = ( - f"{node.table.project_id}.{node.table.dataset_id}.{node.table.table_id}" - ) - used_columns = ( - *node.schema.names, - *[i for i in node.total_order_cols if i not in node.schema.names], - ) + full_table_name = f"{source.table.project_id}.{source.table.dataset_id}.{source.table.table_id}" + used_columns = tuple(col.name for col in source.table.physical_schema) # Physical schema might include unused columns, unsupported datatypes like JSON physical_schema = ibis.backends.bigquery.BigQuerySchema.to_ibis( - list(i for i in node.table.physical_schema if i.name in used_columns) + list(i for i in source.table.physical_schema if i.name in used_columns) ) - if node.at_time is not None or node.sql_predicate is not None: + if source.at_time is not None or source.sql_predicate is not None: import bigframes.session._io.bigquery sql = bigframes.session._io.bigquery.to_query( full_table_name, columns=used_columns, - sql_predicate=node.sql_predicate, - time_travel_timestamp=node.at_time, + sql_predicate=source.sql_predicate, + time_travel_timestamp=source.at_time, ) return ibis.backends.bigquery.Backend().sql( schema=physical_schema, query=sql @@ -237,56 +192,64 @@ def read_table_as_unordered_ibis( else: return ibis.table(physical_schema, full_table_name) - def compile_read_table_unordered(self, node: nodes.ReadTableNode): - ibis_table = self.read_table_as_unordered_ibis(node) + def compile_read_table_unordered( + self, source: nodes.BigqueryDataSource, scan: nodes.ScanList + ): + ibis_table = self.read_table_as_unordered_ibis(source) return compiled.UnorderedIR( ibis_table, tuple( bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - ibis_table[col] + ibis_table[scan_item.source_id].name(scan_item.id.sql) ) - for col in node.schema.names + for scan_item in scan.items ), ) - def compile_read_table_ordered(self, node: nodes.ReadTableNode): - ibis_table = self.read_table_as_unordered_ibis(node) - if node.total_order_cols: - ordering_value_columns = tuple( - bf_ordering.ascending_over(col) for col in node.total_order_cols + def compile_read_table_ordered( + self, source: nodes.BigqueryDataSource, scan_list: nodes.ScanList + ): + ibis_table = self.read_table_as_unordered_ibis(source) + if source.ordering is not None: + visible_column_mapping = { + ids.ColumnId(scan_item.source_id): scan_item.id + for scan_item in scan_list.items + } + full_mapping = { + ids.ColumnId(col.name): ids.ColumnId(guids.generate_guid()) + for col in source.ordering.referenced_columns + } + full_mapping.update(visible_column_mapping) + + ordering = source.ordering.remap_column_refs(full_mapping) + hidden_columns = tuple( + ibis_table[source_id.sql].name(out_id.sql) + for source_id, out_id in full_mapping.items() + if source_id not in visible_column_mapping ) - if node.order_col_is_sequential: - integer_encoding = bf_ordering.IntegerEncoding( - is_encoded=True, is_sequential=True + elif self.strict: # In strict mode, we fallback to ordering by row hash + order_values = [ + col.name(guids.generate_guid()) + for col in default_ordering.gen_default_ordering( + ibis_table, use_double_hash=True ) - else: - integer_encoding = bf_ordering.IntegerEncoding() - ordering: bf_ordering.RowOrdering = bf_ordering.TotalOrdering( - ordering_value_columns, - integer_encoding=integer_encoding, - total_ordering_columns=frozenset(map(ex.deref, node.total_order_cols)), - ) - hidden_columns = () - elif self.strict: - ibis_table, ordering = default_ordering.gen_default_ordering( - ibis_table, use_double_hash=True - ) - hidden_columns = tuple( - ibis_table[col] - for col in ibis_table.columns - if col not in node.schema.names + ] + ordering = bf_ordering.TotalOrdering.from_primary_key( + [value.get_name() for value in order_values] ) + hidden_columns = tuple(order_values) else: # In unstrict mode, don't generate total ordering from hashing as this is # expensive (prevent removing any columns from table scan) ordering, hidden_columns = bf_ordering.RowOrdering(), () + return compiled.OrderedIR( ibis_table, columns=tuple( bigframes.core.compile.ibis_types.ibis_value_to_canonical_type( - ibis_table[col] + ibis_table[scan_item.source_id].name(scan_item.id.sql) ) - for col in node.schema.names + for scan_item in scan_list.items ), ordering=ordering, hidden_ordering_columns=hidden_columns, diff --git a/bigframes/core/compile/default_ordering.py b/bigframes/core/compile/default_ordering.py index 910f822d638..bafeebddc9c 100644 --- a/bigframes/core/compile/default_ordering.py +++ b/bigframes/core/compile/default_ordering.py @@ -18,7 +18,6 @@ from __future__ import annotations -import itertools from typing import cast import bigframes_vendored.ibis.expr.operations as vendored_ibis_ops @@ -27,9 +26,7 @@ import ibis.expr.datatypes as ibis_dtypes import ibis.expr.types as ibis_types -import bigframes.core.expression as ex import bigframes.core.guid as guid -import bigframes.core.ordering as order def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringValue: @@ -59,7 +56,9 @@ def _convert_to_nonnull_string(column: ibis_types.Column) -> ibis_types.StringVa return cast(ibis_types.StringColumn, ibis.literal("\\")).concat(escaped) -def gen_default_ordering(table: ibis.table, use_double_hash: bool = True): +def gen_default_ordering( + table: ibis.table, use_double_hash: bool = True +) -> list[ibis.Value]: ordering_hash_part = guid.generate_guid("bigframes_ordering_") ordering_hash_part2 = guid.generate_guid("bigframes_ordering_") ordering_rand_part = guid.generate_guid("bigframes_ordering_") @@ -82,18 +81,4 @@ def gen_default_ordering(table: ibis.table, use_double_hash: bool = True): if use_double_hash else [full_row_hash, random_value] ) - - original_column_ids = table.columns - table_with_ordering = table.select( - itertools.chain(original_column_ids, order_values) - ) - - ordering = order.TotalOrdering( - ordering_value_columns=tuple( - order.ascending_over(col.get_name()) for col in order_values - ), - total_ordering_columns=frozenset( - ex.deref(col.get_name()) for col in order_values - ), - ) - return table_with_ordering, ordering + return order_values diff --git a/bigframes/core/compile/single_column.py b/bigframes/core/compile/single_column.py index 48fa52974ed..6f2f3f5b6e3 100644 --- a/bigframes/core/compile/single_column.py +++ b/bigframes/core/compile/single_column.py @@ -52,9 +52,8 @@ def join_by_column_ordered( """ # Do not reset the generator - id_generator = ids.standard_identifiers() - l_value_mapping = dict(zip(left.column_ids, id_generator)) - r_value_mapping = dict(zip(right.column_ids, id_generator)) + l_value_mapping = dict(zip(left.column_ids, left.column_ids)) + r_value_mapping = dict(zip(right.column_ids, right.column_ids)) l_hidden_mapping = { id: guids.generate_guid("hidden_") for id in left._hidden_column_ids @@ -143,18 +142,13 @@ def join_by_column_unordered( first the coalesced join keys, then, all the left columns, and finally, all the right columns. """ - id_generator = ids.standard_identifiers() - l_mapping = dict(zip(left.column_ids, id_generator)) - r_mapping = dict(zip(right.column_ids, id_generator)) - left_table = left._to_ibis_expr( - col_id_overrides=l_mapping, - ) - right_table = right._to_ibis_expr( - col_id_overrides=r_mapping, - ) + # Shouldn't need to select the column ids explicitly, but it seems that ibis has some + # bug resolving column ids otherwise, potentially because of the "JoinChain" op + left_table = left._to_ibis_expr().select(left.column_ids) + right_table = right._to_ibis_expr().select(right.column_ids) join_conditions = [ - value_to_join_key(left_table[l_mapping[left_index]]) - == value_to_join_key(right_table[r_mapping[right_index]]) + value_to_join_key(left_table[left_index]) + == value_to_join_key(right_table[right_index]) for left_index, right_index in conditions ] @@ -166,8 +160,8 @@ def join_by_column_unordered( ) # We could filter out the original join columns, but predicates/ordering # might still reference them in implicit joins. - columns = [combined_table[l_mapping[col.get_name()]] for col in left.columns] + [ - combined_table[r_mapping[col.get_name()]] for col in right.columns + columns = [combined_table[col.get_name()] for col in left.columns] + [ + combined_table[col.get_name()] for col in right.columns ] return compiled.UnorderedIR( combined_table, diff --git a/bigframes/core/expression.py b/bigframes/core/expression.py index cf7001a455d..9dee599a7cc 100644 --- a/bigframes/core/expression.py +++ b/bigframes/core/expression.py @@ -52,6 +52,10 @@ def output_type( ) -> dtypes.ExpressionType: ... + @property + def column_references(self) -> typing.Tuple[ids.ColumnId, ...]: + return () + @dataclasses.dataclass(frozen=True) class NullaryAggregation(Aggregation): @@ -73,6 +77,10 @@ def output_type( ) -> dtypes.ExpressionType: return self.op.output_type(self.arg.output_type(input_types)) + @property + def column_references(self) -> typing.Tuple[ids.ColumnId, ...]: + return self.arg.column_references + @dataclasses.dataclass(frozen=True) class BinaryAggregation(Aggregation): @@ -87,6 +95,10 @@ def output_type( self.left.output_type(input_types), self.right.output_type(input_types) ) + @property + def column_references(self) -> typing.Tuple[ids.ColumnId, ...]: + return (*self.left.column_references, *self.right.column_references) + @dataclasses.dataclass(frozen=True) class Expression(abc.ABC): diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index de1dc5d012c..e65040686e5 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -20,7 +20,7 @@ import functools import itertools import typing -from typing import Callable, Tuple +from typing import Callable, Iterable, Sequence, Tuple import google.cloud.bigquery as bq @@ -42,6 +42,8 @@ # A fixed number of variable to assume for overhead on some operations OVERHEAD_VARIABLES = 5 +COLUMN_SET = frozenset[bfet_ids.ColumnId] + @dataclass(frozen=True) class Field: @@ -113,6 +115,10 @@ def roots(self) -> typing.Set[BigFrameNode]: def fields(self) -> Tuple[Field, ...]: ... + @property + def ids(self) -> Iterable[bfet_ids.ColumnId]: + return (field.id for field in self.fields) + @property @abc.abstractmethod def variables_introduced(self) -> int: @@ -216,6 +222,9 @@ def get_type(self, id: bfet_ids.ColumnId) -> bigframes.dtypes.Dtype: def _dtype_lookup(self): return {field.id: field.dtype for field in self.fields} + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + return self.transform_children(lambda x: x.prune(used_cols)) + @dataclass(frozen=True) class UnaryNode(BigFrameNode): @@ -250,6 +259,11 @@ class JoinNode(BigFrameNode): conditions: typing.Tuple[typing.Tuple[ex.DerefOp, ex.DerefOp], ...] type: typing.Literal["inner", "outer", "left", "right", "cross"] + def __post_init__(self): + assert not ( + set(self.left_child.ids) & set(self.right_child.ids) + ), "Join ids collide" + @property def row_preserving(self) -> bool: return False @@ -276,12 +290,7 @@ def __hash__(self): @functools.cached_property def fields(self) -> Tuple[Field, ...]: - items = [] - schema_items = itertools.chain(self.left_child.fields, self.right_child.fields) - identifiers = map(bfet_ids.ColumnId, bfet_ids.standard_identifiers()) - for id, item in zip(identifiers, schema_items): - items.append(Field(id, item.dtype)) - return tuple(items) + return tuple(itertools.chain(self.left_child.fields, self.right_child.fields)) @functools.cached_property def variables_introduced(self) -> int: @@ -303,6 +312,13 @@ def transform_children( def defines_namespace(self) -> bool: return True + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + # If this is a cross join, make sure to select at least one column from each side + new_used = used_cols.union( + map(lambda x: x.id, itertools.chain.from_iterable(self.conditions)) + ) + return self.transform_children(lambda x: x.prune(new_used)) + @dataclass(frozen=True) class ConcatNode(BigFrameNode): @@ -350,9 +366,14 @@ def transform_children( ) -> BigFrameNode: return replace(self, children=tuple(t(child) for child in self.children)) + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + # TODO: Make concat prunable, probably by redefining + return self + @dataclass(frozen=True) class FromRangeNode(BigFrameNode): + # TODO: Enforce single-row, single column constraint start: BigFrameNode end: BigFrameNode step: int @@ -390,6 +411,10 @@ def transform_children( ) -> BigFrameNode: return replace(self, start=t(self.start), end=t(self.end)) + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + # TODO: Make FromRangeNode prunable (or convert to other node types) + return self + # Input Nodex # TODO: Most leaf nodes produce fixed column names based on the datasource @@ -415,11 +440,24 @@ def row_count(self) -> typing.Optional[int]: return None +class ScanItem(typing.NamedTuple): + id: bfet_ids.ColumnId + dtype: bigframes.dtypes.Dtype # Might be multiple logical types for a given physical source type + source_id: str # Flexible enough for both local data and bq data + + +@dataclass(frozen=True) +class ScanList: + items: typing.Tuple[ScanItem, ...] + + @dataclass(frozen=True) class ReadLocalNode(LeafNode): feather_bytes: bytes data_schema: schemata.ArraySchema n_rows: int + # Mapping of local ids to bfet id. + scan_list: ScanList session: typing.Optional[bigframes.session.Session] = None def __hash__(self): @@ -427,14 +465,12 @@ def __hash__(self): @functools.cached_property def fields(self) -> Tuple[Field, ...]: - return tuple( - Field(bfet_ids.ColumnId(i.column), i.dtype) for i in self.data_schema.items - ) + return tuple(Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) @functools.cached_property def variables_introduced(self) -> int: """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" - return len(self.schema.items) + 1 + return len(self.scan_list.items) + 1 @property def supports_fast_head(self) -> bool: @@ -452,6 +488,18 @@ def explicitly_ordered(self) -> bool: def row_count(self) -> typing.Optional[int]: return self.n_rows + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + new_scan_list = ScanList( + tuple(item for item in self.scan_list.items if item.id in used_cols) + ) + return ReadLocalNode( + self.feather_bytes, + self.data_schema, + self.n_rows, + new_scan_list, + self.session, + ) + @dataclass(frozen=True) class GbqTable: @@ -463,12 +511,17 @@ class GbqTable: cluster_cols: typing.Optional[Tuple[str, ...]] @staticmethod - def from_table(table: bq.Table) -> GbqTable: + def from_table(table: bq.Table, columns: Sequence[str] = ()) -> GbqTable: + # Subsetting fields with columns can reduce cost of row-hash default ordering + if columns: + schema = tuple(item for item in table.schema if item.name in columns) + else: + schema = tuple(table.schema) return GbqTable( project_id=table.project, dataset_id=table.dataset_id, table_id=table.table_id, - physical_schema=tuple(table.schema), + physical_schema=schema, n_rows=table.num_rows, cluster_cols=None if table.clustering_fields is None @@ -476,32 +529,40 @@ def from_table(table: bq.Table) -> GbqTable: ) -## Put ordering in here or just add order_by node above? @dataclass(frozen=True) -class ReadTableNode(LeafNode): - table: GbqTable - # Subset of physical schema columns, with chosen BQ types - columns: schemata.ArraySchema = field() +class BigqueryDataSource: + """ + Google BigQuery Data source. - table_session: bigframes.session.Session = field() - # Empty tuple if no primary key (primary key can be any set of columns that together form a unique key) - # Empty if no known unique key - total_order_cols: Tuple[str, ...] = field() - # indicates a primary key that is exactly offsets 0, 1, 2, ..., N-2, N-1 - order_col_is_sequential: bool = False + This should not be modified once defined, as all attributes contribute to the default ordering. + """ + + table: GbqTable at_time: typing.Optional[datetime.datetime] = None # Added for backwards compatibility, not validated sql_predicate: typing.Optional[str] = None + ordering: typing.Optional[orderings.RowOrdering] = None + + +## Put ordering in here or just add order_by node above? +@dataclass(frozen=True) +class ReadTableNode(LeafNode): + source: BigqueryDataSource + # Subset of physical schema column + # Mapping of table schema ids to bfet id. + scan_list: ScanList + + table_session: bigframes.session.Session = field() def __post_init__(self): # enforce invariants - physical_names = set(map(lambda i: i.name, self.table.physical_schema)) - if not set(self.columns.names).issubset(physical_names): + physical_names = set(map(lambda i: i.name, self.source.table.physical_schema)) + if not set(scan.source_id for scan in self.scan_list.items).issubset( + physical_names + ): raise ValueError( - f"Requested schema {self.columns} cannot be derived from table schemal {self.table.physical_schema}" + f"Requested schema {self.scan_list} cannot be derived from table schemal {self.source.table.physical_schema}" ) - if self.order_col_is_sequential and len(self.total_order_cols) != 1: - raise ValueError("Sequential primary key must have only one component") @property def session(self): @@ -512,9 +573,7 @@ def __hash__(self): @functools.cached_property def fields(self) -> Tuple[Field, ...]: - return tuple( - Field(bfet_ids.ColumnId(i.column), i.dtype) for i in self.columns.items - ) + return tuple(Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) @property def relation_ops_created(self) -> int: @@ -526,93 +585,51 @@ def supports_fast_head(self) -> bool: # Fast head is only supported when row offsets are available. # In the future, ORDER BY+LIMIT optimizations may allow fast head when # clustered and/or partitioned on ordering key - return self.order_col_is_sequential + return (self.source.ordering is not None) and self.source.ordering.is_sequential @property def order_ambiguous(self) -> bool: - return len(self.total_order_cols) == 0 + return ( + self.source.ordering is None + ) or not self.source.ordering.is_total_ordering @property def explicitly_ordered(self) -> bool: - return len(self.total_order_cols) > 0 + return self.source.ordering is not None @functools.cached_property def variables_introduced(self) -> int: - return len(self.schema.items) + 1 + return len(self.scan_list.items) + 1 @property def row_count(self) -> typing.Optional[int]: - if self.sql_predicate is None: - return self.table.n_rows + if self.source.sql_predicate is None: + return self.source.table.n_rows return None + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + new_scan_list = ScanList( + tuple(item for item in self.scan_list.items if item.id in used_cols) + ) + return ReadTableNode(self.source, new_scan_list, self.table_session) + -# This node shouldn't be used in the "original" expression tree, only used as replacement for original during planning @dataclass(frozen=True) -class CachedTableNode(LeafNode): +class CachedTableNode(ReadTableNode): # The original BFET subtree that was cached # note: this isn't a "child" node. original_node: BigFrameNode = field() - # reference to cached materialization of original_node - table: GbqTable - ordering: typing.Optional[orderings.RowOrdering] = field() - - def __post_init__(self): - # enforce invariants - physical_names = set(map(lambda i: i.name, self.table.physical_schema)) - logical_names = self.original_node.schema.names - if not set(logical_names).issubset(physical_names): - raise ValueError( - f"Requested schema {logical_names} cannot be derived from table schema {self.table.physical_schema}" - ) - - @property - def session(self): - return self.original_node.session def __hash__(self): return self._node_hash - @property - def fields(self) -> Tuple[Field, ...]: - return self.original_node.fields - - @functools.cached_property - def variables_introduced(self) -> int: - return len(self.fields) + OVERHEAD_VARIABLES - - @property - def _hidden_columns(self) -> typing.Tuple[bfet_ids.ColumnId, ...]: - """Physical columns used to define ordering but not directly exposed as value columns.""" - field_names = set(field.id for field in self.fields) - if self.ordering is None: - return () - return tuple( - col - for col in sorted(self.ordering.referenced_columns) - if col not in field_names + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + new_scan_list = ScanList( + tuple(item for item in self.scan_list.items if item.id in used_cols) + ) + return CachedTableNode( + self.source, new_scan_list, self.table_session, self.original_node ) - - @property - def supports_fast_head(self) -> bool: - # Fast head is only supported when row offsets are available. - # In the future, ORDER BY+LIMIT optimizations may allow fast head when - # clustered and/or partitioned on ordering key - return (self.ordering is None) or self.ordering.is_sequential - - @property - def order_ambiguous(self) -> bool: - return not isinstance(self.ordering, orderings.TotalOrdering) - - @property - def explicitly_ordered(self) -> bool: - return (self.ordering is not None) and len( - self.ordering.all_ordering_columns - ) > 0 - - @property - def row_count(self) -> typing.Optional[int]: - return self.table.n_rows # Unary nodes @@ -639,6 +656,13 @@ def relation_ops_created(self) -> int: def variables_introduced(self) -> int: return 1 + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + if self.col_id not in used_cols: + return self.child.prune(used_cols) + else: + new_used = used_cols.difference([self.col_id]) + return self.transform_children(lambda x: x.prune(new_used)) + @dataclass(frozen=True) class FilterNode(UnaryNode): @@ -655,6 +679,11 @@ def __hash__(self): def variables_introduced(self) -> int: return 1 + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + consumed_ids = used_cols.union(self.predicate.column_references) + pruned_child = self.child.prune(consumed_ids) + return FilterNode(pruned_child, self.predicate) + @dataclass(frozen=True) class OrderByNode(UnaryNode): @@ -676,6 +705,14 @@ def relation_ops_created(self) -> int: def explicitly_ordered(self) -> bool: return True + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + ordering_cols = itertools.chain.from_iterable( + map(lambda x: x.referenced_columns, self.by) + ) + consumed_ids = used_cols.union(ordering_cols) + pruned_child = self.child.prune(consumed_ids) + return OrderByNode(pruned_child, self.by) + @dataclass(frozen=True) class ReversedNode(UnaryNode): @@ -723,6 +760,15 @@ def variables_introduced(self) -> int: def defines_namespace(self) -> bool: return True + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + pruned_selections = tuple( + select for select in self.input_output_pairs if select[1] in used_cols + ) + consumed_ids = frozenset(i[0].id for i in pruned_selections) + + pruned_child = self.child.prune(consumed_ids) + return SelectionNode(pruned_child, pruned_selections) + @dataclass(frozen=True) class ProjectionNode(UnaryNode): @@ -758,6 +804,16 @@ def variables_introduced(self) -> int: new_vars = sum(1 for i in self.assignments if not i[0].is_identity) return new_vars + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + pruned_assignments = tuple(i for i in self.assignments if i[1] in used_cols) + if len(pruned_assignments) == 0: + return self.child.prune(used_cols) + consumed_ids = itertools.chain.from_iterable( + i[0].column_references for i in pruned_assignments + ) + pruned_child = self.child.prune(used_cols.union(consumed_ids)) + return ProjectionNode(pruned_child, pruned_assignments) + # TODO: Merge RowCount into Aggregate Node? # Row count can be compute from table metadata sometimes, so it is a bit special. @@ -835,6 +891,16 @@ def explicitly_ordered(self) -> bool: def defines_namespace(self) -> bool: return True + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + by_ids = (ref.id for ref in self.by_column_ids) + pruned_aggs = tuple(agg for agg in self.aggregations if agg[1] in used_cols) + agg_inputs = itertools.chain.from_iterable( + agg.column_references for agg, _ in pruned_aggs + ) + consumed_ids = frozenset(itertools.chain(by_ids, agg_inputs)) + pruned_child = self.child.prune(consumed_ids) + return AggregateNode(pruned_child, pruned_aggs, self.by_column_ids, self.dropna) + @dataclass(frozen=True) class WindowOpNode(UnaryNode): @@ -867,6 +933,14 @@ def relation_ops_created(self) -> int: # Assume that if not reprojecting, that there is a sequence of window operations sharing the same window return 0 if self.skip_reproject_unsafe else 4 + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + if self.output_name not in used_cols: + return self.child + consumed_ids = used_cols.difference([self.output_name]).union( + [self.column_name.id] + ) + return self.transform_children(lambda x: x.prune(consumed_ids)) + # TODO: Remove this op @dataclass(frozen=True) @@ -941,3 +1015,9 @@ def variables_introduced(self) -> int: @property def defines_namespace(self) -> bool: return True + + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: + # Cannot prune explode op + return self.transform_children( + lambda x: x.prune(used_cols.union(ref.id for ref in self.column_ids)) + ) diff --git a/bigframes/core/ordering.py b/bigframes/core/ordering.py index daf08d5e879..8bba7d72b65 100644 --- a/bigframes/core/ordering.py +++ b/bigframes/core/ordering.py @@ -55,6 +55,10 @@ class OrderingExpression: direction: OrderingDirection = OrderingDirection.ASC na_last: bool = True + @property + def referenced_columns(self) -> Set[ids.ColumnId]: + return set(self.scalar_expression.column_references) + def remap_column_refs( self, mapping: Mapping[ids.ColumnId, ids.ColumnId], @@ -120,7 +124,7 @@ def referenced_columns(self) -> Set[ids.ColumnId]: return set( col for part in self.ordering_value_columns - for col in part.scalar_expression.column_references + for col in part.referenced_columns ) @property @@ -132,6 +136,10 @@ def is_string_encoded(self) -> bool: def is_sequential(self) -> bool: return self.integer_encoding.is_encoded and self.integer_encoding.is_sequential + @property + def is_total_ordering(self) -> bool: + return False + @property def total_order_col(self) -> Optional[OrderingExpression]: """Returns column id of columns that defines total ordering, if such as column exists""" @@ -229,6 +237,19 @@ def from_offset_col(cls, col: str) -> TotalOrdering: total_ordering_columns=frozenset({expression.deref(col)}), ) + @classmethod + def from_primary_key(cls, primary_key: Sequence[str]) -> TotalOrdering: + return TotalOrdering( + tuple(ascending_over(col) for col in primary_key), + total_ordering_columns=frozenset( + {expression.deref(col) for col in primary_key} + ), + ) + + @property + def is_total_ordering(self) -> bool: + return True + def with_non_sequential(self): """Create a copy that is marked as non-sequential. diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 78e4e74adc9..8508c714fda 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -60,6 +60,8 @@ # Number of times to factor out subqueries before giving up. MAX_SUBTREE_FACTORINGS = 5 _MAX_CLUSTER_COLUMNS = 4 +# TODO: b/338258028 Enable pruning to reduce text size. +ENABLE_PRUNING = False @dataclasses.dataclass(frozen=True) @@ -431,6 +433,9 @@ def _get_optimized_plan(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: optimized_plan = tree_properties.replace_nodes( node, (dict(self._cached_executions)) ) + if ENABLE_PRUNING: + used_fields = frozenset(field.id for field in optimized_plan.fields) + optimized_plan = optimized_plan.prune(used_fields) return optimized_plan def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): From 56fec286452560acf2554a6e30d0c1d62962ad08 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:24:57 -0700 Subject: [PATCH 874/921] chore(main): release 1.21.0 (#1025) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 21 +++++++++++++++++++++ bigframes/version.py | 2 +- third_party/bigframes_vendored/version.py | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0539af01e4..c398f17d43f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.21.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.20.0...v1.21.0) (2024-10-02) + + +### Features + +* Add deprecation warning to PaLM2TextGenerator model ([#1035](https://github.com/googleapis/python-bigquery-dataframes/issues/1035)) ([1183b0f](https://github.com/googleapis/python-bigquery-dataframes/commit/1183b0fb2be7af7386e4bd0d0d1312433db60454)) +* Add DeprecationWarning for PaLM2TextEmbeddingGenerator ([#1018](https://github.com/googleapis/python-bigquery-dataframes/issues/1018)) ([4af5bbb](https://github.com/googleapis/python-bigquery-dataframes/commit/4af5bbb9e42fdb0add17308475c7881d7035fbfd)) +* Add ml.model_selection.cross_validate support ([#1020](https://github.com/googleapis/python-bigquery-dataframes/issues/1020)) ([1a38063](https://github.com/googleapis/python-bigquery-dataframes/commit/1a380631f793f82637cd384601956ee4457dc58a)) +* Allow access of struct fields with dot operators on `Series` ([#1019](https://github.com/googleapis/python-bigquery-dataframes/issues/1019)) ([ef76f13](https://github.com/googleapis/python-bigquery-dataframes/commit/ef76f137fbbf9e8f8c5a63023554d22059ab4fbd)) + + +### Bug Fixes + +* Ensure no double execution for to_pandas ([#1032](https://github.com/googleapis/python-bigquery-dataframes/issues/1032)) ([4992cc2](https://github.com/googleapis/python-bigquery-dataframes/commit/4992cc27e46bc2b0a908c7d521785989735186f4)) +* Remove pre-caching of remote function results ([#1028](https://github.com/googleapis/python-bigquery-dataframes/issues/1028)) ([0359bc8](https://github.com/googleapis/python-bigquery-dataframes/commit/0359bc85839c37b5cd10c0c418b275ac0dc29c4a)) + + +### Documentation + +* Add ml cross-validation notebook ([#1037](https://github.com/googleapis/python-bigquery-dataframes/issues/1037)) ([057f3f0](https://github.com/googleapis/python-bigquery-dataframes/commit/057f3f0d694ddffe8745443a85b4fb43081893bb)) + ## [1.20.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.19.0...v1.20.0) (2024-09-25) diff --git a/bigframes/version.py b/bigframes/version.py index 60f49421756..c07f26bc6f6 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0" +__version__ = "1.21.0" diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index 60f49421756..c07f26bc6f6 100644 --- a/third_party/bigframes_vendored/version.py +++ b/third_party/bigframes_vendored/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.20.0" +__version__ = "1.21.0" From af6aa9c7aab9f4c7251c79df6c08ac3ade3dd7e1 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Wed, 2 Oct 2024 11:45:05 -0700 Subject: [PATCH 875/921] chore: Add an ExperimentOptions class to control experiments (#1039) --- bigframes/_config/__init__.py | 14 ++++++++ bigframes/_config/experiment_options.py | 36 +++++++++++++++++++ tests/unit/_config/test_experiment_options.py | 32 +++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 bigframes/_config/experiment_options.py create mode 100644 tests/unit/_config/test_experiment_options.py diff --git a/bigframes/_config/__init__.py b/bigframes/_config/__init__.py index ac58c19fa5a..75f91b28d3e 100644 --- a/bigframes/_config/__init__.py +++ b/bigframes/_config/__init__.py @@ -29,6 +29,7 @@ import bigframes._config.bigquery_options as bigquery_options import bigframes._config.compute_options as compute_options import bigframes._config.display_options as display_options +import bigframes._config.experiment_options as experiment_options import bigframes._config.sampling_options as sampling_options @@ -46,6 +47,9 @@ class ThreadLocalConfig(threading.local): compute_options: compute_options.ComputeOptions = field( default_factory=compute_options.ComputeOptions ) + experiment_options: experiment_options.ExperimentOptions = field( + default_factory=experiment_options.ExperimentOptions + ) class Options: @@ -122,6 +126,16 @@ def compute(self) -> compute_options.ComputeOptions: """ return self._local.compute_options + @property + def experiments(self) -> experiment_options.ExperimentOptions: + """Options controlling experiments + + Returns: + bigframes._config.experiment_options.ExperimentOptions: + Thread-local options for controlling experiments + """ + return self._local.experiment_options + @property def is_bigquery_thread_local(self) -> bool: """Indicator that we're using a thread-local session. diff --git a/bigframes/_config/experiment_options.py b/bigframes/_config/experiment_options.py new file mode 100644 index 00000000000..c39502eadee --- /dev/null +++ b/bigframes/_config/experiment_options.py @@ -0,0 +1,36 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import warnings + + +class ExperimentOptions: + """ + Encapsulates the configration for experiments + """ + + def __init__(self): + self._semantic_operators = False + + @property + def semantic_operators(self) -> bool: + return self._semantic_operators + + @semantic_operators.setter + def semantic_operators(self, value: bool): + if value is True: + warnings.warn( + "Semantic operators are still under experiments, and are subject to change in the future." + ) + self._semantic_operators = value diff --git a/tests/unit/_config/test_experiment_options.py b/tests/unit/_config/test_experiment_options.py new file mode 100644 index 00000000000..49c3d9e53ca --- /dev/null +++ b/tests/unit/_config/test_experiment_options.py @@ -0,0 +1,32 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes._config.experiment_options as experiment_options + + +def test_semantic_operators_default_false(): + options = experiment_options.ExperimentOptions() + + assert options.semantic_operators is False + + +def test_semantic_operators_set_true_shows_warning(): + options = experiment_options.ExperimentOptions() + + with pytest.warns(UserWarning): + options.semantic_operators = True + + assert options.semantic_operators is True From 991a4625d38833515552ef4056a37c690b2aec16 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:40:37 -0700 Subject: [PATCH 876/921] chore: update db benchmark config/add notebook load testing session/update readme (#1041) * chore: update db benchmark config * update project --- .kokoro/continuous/notebook.cfg | 5 -- .kokoro/load/notebook.cfg | 17 ++++++ tests/benchmark/README.md | 22 ++++++++ .../db_benchmark/groupby/config.jsonl | 4 +- tests/benchmark/db_benchmark/groupby/q1.py | 16 +++++- tests/benchmark/db_benchmark/groupby/q10.py | 10 +++- tests/benchmark/db_benchmark/groupby/q2.py | 16 +++++- tests/benchmark/db_benchmark/groupby/q3.py | 16 +++++- tests/benchmark/db_benchmark/groupby/q4.py | 16 +++++- tests/benchmark/db_benchmark/groupby/q5.py | 16 +++++- tests/benchmark/db_benchmark/groupby/q6.py | 16 +++++- tests/benchmark/db_benchmark/groupby/q7.py | 16 +++++- tests/benchmark/db_benchmark/groupby/q8.py | 16 +++++- .../benchmark/db_benchmark/join/config.jsonl | 4 +- tests/benchmark/db_benchmark/join/q1.py | 16 +++++- tests/benchmark/db_benchmark/join/q2.py | 16 +++++- tests/benchmark/db_benchmark/join/q3.py | 16 +++++- tests/benchmark/db_benchmark/join/q4.py | 16 +++++- tests/benchmark/db_benchmark/join/q5.py | 16 +++++- .../benchmark/db_benchmark/sort/config.jsonl | 4 +- tests/benchmark/db_benchmark/sort/q1.py | 16 +++++- tests/benchmark/tpch/q1.py | 2 +- tests/benchmark/tpch/q10.py | 2 +- tests/benchmark/tpch/q11.py | 2 +- tests/benchmark/tpch/q12.py | 2 +- tests/benchmark/tpch/q13.py | 2 +- tests/benchmark/tpch/q14.py | 2 +- tests/benchmark/tpch/q15.py | 2 +- tests/benchmark/tpch/q16.py | 2 +- tests/benchmark/tpch/q17.py | 2 +- tests/benchmark/tpch/q18.py | 2 +- tests/benchmark/tpch/q19.py | 2 +- tests/benchmark/tpch/q2.py | 2 +- tests/benchmark/tpch/q20.py | 2 +- tests/benchmark/tpch/q21.py | 2 +- tests/benchmark/tpch/q22.py | 2 +- tests/benchmark/tpch/q3.py | 2 +- tests/benchmark/tpch/q4.py | 2 +- tests/benchmark/tpch/q5.py | 2 +- tests/benchmark/tpch/q6.py | 2 +- tests/benchmark/tpch/q7.py | 2 +- tests/benchmark/tpch/q8.py | 2 +- tests/benchmark/tpch/q9.py | 2 +- tests/benchmark/utils.py | 55 ++++++++++--------- .../db_benchmark/groupby_queries.py | 36 ++++++------ .../db_benchmark/join_queries.py | 30 +++++----- .../db_benchmark/sort_queries.py | 6 +- 47 files changed, 337 insertions(+), 124 deletions(-) create mode 100644 .kokoro/load/notebook.cfg diff --git a/.kokoro/continuous/notebook.cfg b/.kokoro/continuous/notebook.cfg index c14297019a1..cc73c3bea40 100644 --- a/.kokoro/continuous/notebook.cfg +++ b/.kokoro/continuous/notebook.cfg @@ -6,11 +6,6 @@ env_vars: { value: "notebook" } -env_vars: { - key: "BENCHMARK_AND_PUBLISH" - value: "true" -} - env_vars: { key: "GOOGLE_CLOUD_PROJECT" value: "bigframes-testing" diff --git a/.kokoro/load/notebook.cfg b/.kokoro/load/notebook.cfg new file mode 100644 index 00000000000..c14297019a1 --- /dev/null +++ b/.kokoro/load/notebook.cfg @@ -0,0 +1,17 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "notebook" +} + +env_vars: { + key: "BENCHMARK_AND_PUBLISH" + value: "true" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "bigframes-testing" +} diff --git a/tests/benchmark/README.md b/tests/benchmark/README.md index a30c36065bc..e5b75855143 100644 --- a/tests/benchmark/README.md +++ b/tests/benchmark/README.md @@ -11,6 +11,28 @@ This section lists the benchmarks currently available, with descriptions and lin - **TPC-H Benchmark**: Based on the TPC-H standards, this benchmark evaluates transaction processing capabilities. It is adapted from code found in the Polars repository, specifically tailored to test and compare these capabilities. Details are available on the [Polars Benchmark GitHub repository](https://github.com/pola-rs/polars-benchmark). - **Notebooks**: These Jupyter notebooks showcase BigFrames' key features and patterns, and also enable performance benchmarking. Explore them at the [BigFrames Notebooks repository](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks). +## Benchmark Configuration Using `config.jsonl` Files + +For each benchmark, a corresponding `config.jsonl` file exists in the same folder or its parent folder. These configuration files allow users to control various benchmark parameters without modifying the code directly. By updating the relevant `config.jsonl` file in the specific benchmark's folder, you can easily configure settings such as: +- **benchmark_suffix**: A suffix appended to the benchmark name for identification purposes. +- **ordered**: Controls the mode for BigFrames, specifying whether to use ordered (`true`) or unordered mode (`false`). +- **project_id**: The Google Cloud project ID where the benchmark dataset or table is located. +- **dataset_id**: The dataset ID for querying during the benchmark. +- **table_id**: This is **required** for benchmarks like `dbbenchmark` that target a specific table, but is **not configurable** for benchmarks like `TPC-H`, which use multiple tables with fixed names. + +### Example `config.jsonl` Files + +#### `dbbenchmark` Example +```jsonl +{"benchmark_suffix": "50g_ordered", "project_id": "your-google-cloud-project", "dataset_id": "dbbenchmark", "table_id": "G1_1e9_1e2_5_0", "ordered": true} +{"benchmark_suffix": "50g_unordered", "project_id": "your-google-cloud-project", "dataset_id": "dbbenchmark", "table_id": "G1_1e9_1e2_5_0", "ordered": false} +``` + +#### `TPC-H` Example +```jsonl +{"benchmark_suffix": "10t_unordered", "project_id": "your-google-cloud-project", "dataset_id": "tpch_0010t", "ordered": false} +``` + ## Usage Examples Our benchmarking process runs internally on a daily basis to continuously monitor the performance of BigFrames. However, there are occasions when you might need to conduct benchmarking locally to test specific changes or new features. diff --git a/tests/benchmark/db_benchmark/groupby/config.jsonl b/tests/benchmark/db_benchmark/groupby/config.jsonl index dd881e76ac9..b6f23ebbf78 100644 --- a/tests/benchmark/db_benchmark/groupby/config.jsonl +++ b/tests/benchmark/db_benchmark/groupby/config.jsonl @@ -1,2 +1,2 @@ -{"benchmark_suffix": "50g_ordered", "table_id": "G1_1e9_1e2_5_0", "ordered": true} -{"benchmark_suffix": "50g_unordered", "table_id": "G1_1e9_1e2_5_0", "ordered": false} +{"benchmark_suffix": "50g_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "dbbenchmark", "table_id": "G1_1e9_1e2_5_0", "ordered": true} +{"benchmark_suffix": "50g_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "dbbenchmark", "table_id": "G1_1e9_1e2_5_0", "ordered": false} diff --git a/tests/benchmark/db_benchmark/groupby/q1.py b/tests/benchmark/db_benchmark/groupby/q1.py index 02a709def9d..dc868179085 100644 --- a/tests/benchmark/db_benchmark/groupby/q1.py +++ b/tests/benchmark/db_benchmark/groupby/q1.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q1, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q1, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q10.py b/tests/benchmark/db_benchmark/groupby/q10.py index 0cd195b04a2..99d28e2f9a6 100644 --- a/tests/benchmark/db_benchmark/groupby/q10.py +++ b/tests/benchmark/db_benchmark/groupby/q10.py @@ -18,13 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( vendored_dbbenchmark_groupby_queries.q10, current_path, suffix, + project_id, + dataset_id, table_id, session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q2.py b/tests/benchmark/db_benchmark/groupby/q2.py index 398c63e09f9..b06a4189fe5 100644 --- a/tests/benchmark/db_benchmark/groupby/q2.py +++ b/tests/benchmark/db_benchmark/groupby/q2.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q2, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q2, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q3.py b/tests/benchmark/db_benchmark/groupby/q3.py index 9863b969d8e..d66dd7b39d3 100644 --- a/tests/benchmark/db_benchmark/groupby/q3.py +++ b/tests/benchmark/db_benchmark/groupby/q3.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q3, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q3, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q4.py b/tests/benchmark/db_benchmark/groupby/q4.py index ce29e3ceafb..6c72069a538 100644 --- a/tests/benchmark/db_benchmark/groupby/q4.py +++ b/tests/benchmark/db_benchmark/groupby/q4.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q4, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q4, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q5.py b/tests/benchmark/db_benchmark/groupby/q5.py index 27a4a52a8fe..3e6db9783e3 100644 --- a/tests/benchmark/db_benchmark/groupby/q5.py +++ b/tests/benchmark/db_benchmark/groupby/q5.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q5, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q5, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q6.py b/tests/benchmark/db_benchmark/groupby/q6.py index f1befc68406..f763280b5be 100644 --- a/tests/benchmark/db_benchmark/groupby/q6.py +++ b/tests/benchmark/db_benchmark/groupby/q6.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q6, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q6, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q7.py b/tests/benchmark/db_benchmark/groupby/q7.py index c0791612e8e..4e7f2d58b61 100644 --- a/tests/benchmark/db_benchmark/groupby/q7.py +++ b/tests/benchmark/db_benchmark/groupby/q7.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q7, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q7, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/groupby/q8.py b/tests/benchmark/db_benchmark/groupby/q8.py index 46dd8c45eb2..75d5dcaa0cf 100644 --- a/tests/benchmark/db_benchmark/groupby/q8.py +++ b/tests/benchmark/db_benchmark/groupby/q8.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.groupby_queries as vendored_dbbenchmark_groupby_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_groupby_queries.q8, current_path, suffix, table_id, session + vendored_dbbenchmark_groupby_queries.q8, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/join/config.jsonl b/tests/benchmark/db_benchmark/join/config.jsonl index 72884d6c5ab..e709281137d 100644 --- a/tests/benchmark/db_benchmark/join/config.jsonl +++ b/tests/benchmark/db_benchmark/join/config.jsonl @@ -1,2 +1,2 @@ -{"benchmark_suffix": "50g_ordered", "table_id": "J1_1e9_NA_0_0", "ordered": true} -{"benchmark_suffix": "50g_unordered", "table_id": "J1_1e9_NA_0_0", "ordered": false} +{"benchmark_suffix": "50g_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "dbbenchmark", "table_id": "J1_1e9_NA_0_0", "ordered": true} +{"benchmark_suffix": "50g_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "dbbenchmark", "table_id": "J1_1e9_NA_0_0", "ordered": false} diff --git a/tests/benchmark/db_benchmark/join/q1.py b/tests/benchmark/db_benchmark/join/q1.py index ce053597893..4ca0ee33891 100644 --- a/tests/benchmark/db_benchmark/join/q1.py +++ b/tests/benchmark/db_benchmark/join/q1.py @@ -18,10 +18,22 @@ import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_join_queries.q1, current_path, suffix, table_id, session + vendored_dbbenchmark_join_queries.q1, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/join/q2.py b/tests/benchmark/db_benchmark/join/q2.py index 6c9141b316f..19efd6fbf24 100644 --- a/tests/benchmark/db_benchmark/join/q2.py +++ b/tests/benchmark/db_benchmark/join/q2.py @@ -18,10 +18,22 @@ import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_join_queries.q2, current_path, suffix, table_id, session + vendored_dbbenchmark_join_queries.q2, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/join/q3.py b/tests/benchmark/db_benchmark/join/q3.py index 284ab6a2b3e..d0a931bfb2f 100644 --- a/tests/benchmark/db_benchmark/join/q3.py +++ b/tests/benchmark/db_benchmark/join/q3.py @@ -18,10 +18,22 @@ import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_join_queries.q3, current_path, suffix, table_id, session + vendored_dbbenchmark_join_queries.q3, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/join/q4.py b/tests/benchmark/db_benchmark/join/q4.py index 1504e0a6638..ebd7c461d06 100644 --- a/tests/benchmark/db_benchmark/join/q4.py +++ b/tests/benchmark/db_benchmark/join/q4.py @@ -18,10 +18,22 @@ import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_join_queries.q4, current_path, suffix, table_id, session + vendored_dbbenchmark_join_queries.q4, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/join/q5.py b/tests/benchmark/db_benchmark/join/q5.py index 575b3711e6e..7114acd4086 100644 --- a/tests/benchmark/db_benchmark/join/q5.py +++ b/tests/benchmark/db_benchmark/join/q5.py @@ -18,10 +18,22 @@ import bigframes_vendored.db_benchmark.join_queries as vendored_dbbenchmark_join_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_join_queries.q5, current_path, suffix, table_id, session + vendored_dbbenchmark_join_queries.q5, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/db_benchmark/sort/config.jsonl b/tests/benchmark/db_benchmark/sort/config.jsonl index 72884d6c5ab..e709281137d 100644 --- a/tests/benchmark/db_benchmark/sort/config.jsonl +++ b/tests/benchmark/db_benchmark/sort/config.jsonl @@ -1,2 +1,2 @@ -{"benchmark_suffix": "50g_ordered", "table_id": "J1_1e9_NA_0_0", "ordered": true} -{"benchmark_suffix": "50g_unordered", "table_id": "J1_1e9_NA_0_0", "ordered": false} +{"benchmark_suffix": "50g_ordered", "project_id": "bigframes-dev-perf", "dataset_id": "dbbenchmark", "table_id": "J1_1e9_NA_0_0", "ordered": true} +{"benchmark_suffix": "50g_unordered", "project_id": "bigframes-dev-perf", "dataset_id": "dbbenchmark", "table_id": "J1_1e9_NA_0_0", "ordered": false} diff --git a/tests/benchmark/db_benchmark/sort/q1.py b/tests/benchmark/db_benchmark/sort/q1.py index f17a8431923..4888dda0452 100644 --- a/tests/benchmark/db_benchmark/sort/q1.py +++ b/tests/benchmark/db_benchmark/sort/q1.py @@ -18,9 +18,21 @@ import bigframes_vendored.db_benchmark.sort_queries as vendored_dbbenchmark_sort_queries if __name__ == "__main__": - table_id, session, suffix = utils.get_dbbenchmark_configuration() + ( + project_id, + dataset_id, + table_id, + session, + suffix, + ) = utils.get_dbbenchmark_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( - vendored_dbbenchmark_sort_queries.q1, current_path, suffix, table_id, session + vendored_dbbenchmark_sort_queries.q1, + current_path, + suffix, + project_id, + dataset_id, + table_id, + session, ) diff --git a/tests/benchmark/tpch/q1.py b/tests/benchmark/tpch/q1.py index 3f1c63967ec..a672103931c 100644 --- a/tests/benchmark/tpch/q1.py +++ b/tests/benchmark/tpch/q1.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q1 as vendored_tpch_q1 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q10.py b/tests/benchmark/tpch/q10.py index bea18975ca9..d468a901567 100644 --- a/tests/benchmark/tpch/q10.py +++ b/tests/benchmark/tpch/q10.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q10 as vendored_tpch_q10 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q11.py b/tests/benchmark/tpch/q11.py index 538d8d3e5f1..dbf3fd94deb 100644 --- a/tests/benchmark/tpch/q11.py +++ b/tests/benchmark/tpch/q11.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q11 as vendored_tpch_q11 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q12.py b/tests/benchmark/tpch/q12.py index 6503b543f49..57774457ae8 100644 --- a/tests/benchmark/tpch/q12.py +++ b/tests/benchmark/tpch/q12.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q12 as vendored_tpch_q12 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q13.py b/tests/benchmark/tpch/q13.py index 60c2101f6fe..a7f2780e4b9 100644 --- a/tests/benchmark/tpch/q13.py +++ b/tests/benchmark/tpch/q13.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q13 as vendored_tpch_q13 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q14.py b/tests/benchmark/tpch/q14.py index 1698a016287..e9599f3bd8d 100644 --- a/tests/benchmark/tpch/q14.py +++ b/tests/benchmark/tpch/q14.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q14 as vendored_tpch_q14 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q15.py b/tests/benchmark/tpch/q15.py index 49e2ce4e929..ff200384a8e 100644 --- a/tests/benchmark/tpch/q15.py +++ b/tests/benchmark/tpch/q15.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q15 as vendored_tpch_q15 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q16.py b/tests/benchmark/tpch/q16.py index ef6edf6b128..69fc1b95232 100644 --- a/tests/benchmark/tpch/q16.py +++ b/tests/benchmark/tpch/q16.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q16 as vendored_tpch_q16 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q17.py b/tests/benchmark/tpch/q17.py index 2f680d206e1..14707f4a93d 100644 --- a/tests/benchmark/tpch/q17.py +++ b/tests/benchmark/tpch/q17.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q17 as vendored_tpch_q17 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q18.py b/tests/benchmark/tpch/q18.py index 7336246f1bd..54cf0d04328 100644 --- a/tests/benchmark/tpch/q18.py +++ b/tests/benchmark/tpch/q18.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q18 as vendored_tpch_q18 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q19.py b/tests/benchmark/tpch/q19.py index 3bf34794bfc..1ec44391ff7 100644 --- a/tests/benchmark/tpch/q19.py +++ b/tests/benchmark/tpch/q19.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q19 as vendored_tpch_q19 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q2.py b/tests/benchmark/tpch/q2.py index c738aae124e..da8064b4001 100644 --- a/tests/benchmark/tpch/q2.py +++ b/tests/benchmark/tpch/q2.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q2 as vendored_tpch_q2 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q20.py b/tests/benchmark/tpch/q20.py index 1b254c6a785..33e4f72ef69 100644 --- a/tests/benchmark/tpch/q20.py +++ b/tests/benchmark/tpch/q20.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q20 as vendored_tpch_q20 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q21.py b/tests/benchmark/tpch/q21.py index 18e8df87fe7..f73f87725fb 100644 --- a/tests/benchmark/tpch/q21.py +++ b/tests/benchmark/tpch/q21.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q21 as vendored_tpch_q21 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q22.py b/tests/benchmark/tpch/q22.py index 6c10021c2b6..0a6f6d923c5 100644 --- a/tests/benchmark/tpch/q22.py +++ b/tests/benchmark/tpch/q22.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q22 as vendored_tpch_q22 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q3.py b/tests/benchmark/tpch/q3.py index 5bcaaa0d5da..92322eea21b 100644 --- a/tests/benchmark/tpch/q3.py +++ b/tests/benchmark/tpch/q3.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q3 as vendored_tpch_q3 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q4.py b/tests/benchmark/tpch/q4.py index 462c6336d10..2d6931d6b15 100644 --- a/tests/benchmark/tpch/q4.py +++ b/tests/benchmark/tpch/q4.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q4 as vendored_tpch_q4 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q5.py b/tests/benchmark/tpch/q5.py index 108cde58ccb..e8fd83e193c 100644 --- a/tests/benchmark/tpch/q5.py +++ b/tests/benchmark/tpch/q5.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q5 as vendored_tpch_q5 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q6.py b/tests/benchmark/tpch/q6.py index ccefc1b0bff..152d6c663e4 100644 --- a/tests/benchmark/tpch/q6.py +++ b/tests/benchmark/tpch/q6.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q6 as vendored_tpch_q6 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q7.py b/tests/benchmark/tpch/q7.py index 0cad599a607..1c3e455e1cf 100644 --- a/tests/benchmark/tpch/q7.py +++ b/tests/benchmark/tpch/q7.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q7 as vendored_tpch_q7 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q8.py b/tests/benchmark/tpch/q8.py index 6c6ac23b9bb..8d231948341 100644 --- a/tests/benchmark/tpch/q8.py +++ b/tests/benchmark/tpch/q8.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q8 as vendored_tpch_q8 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/tpch/q9.py b/tests/benchmark/tpch/q9.py index 05c82fb66ec..329e315c2cb 100644 --- a/tests/benchmark/tpch/q9.py +++ b/tests/benchmark/tpch/q9.py @@ -17,7 +17,7 @@ import bigframes_vendored.tpch.queries.q9 as vendored_tpch_q9 if __name__ == "__main__": - project_id, dataset_id, session, suffix = utils.get_tpch_configuration() + project_id, dataset_id, session, suffix = utils.get_configuration() current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( diff --git a/tests/benchmark/utils.py b/tests/benchmark/utils.py index 32be33fc74b..887d54dba2f 100644 --- a/tests/benchmark/utils.py +++ b/tests/benchmark/utils.py @@ -18,43 +18,29 @@ import bigframes -def get_dbbenchmark_configuration(): +def get_configuration(include_table_id=False): parser = argparse.ArgumentParser() - parser.add_argument( - "--table_id", - type=str, - required=True, - help="The BigQuery table ID to query.", - ) - parser.add_argument( - "--ordered", - type=str, - help="Set to True (default) to have an ordered session, or False for an unordered session.", - ) - parser.add_argument( - "--benchmark_suffix", - type=str, - help="Suffix to append to benchmark names for identification purposes.", - ) - args = parser.parse_args() - session = _initialize_session(_str_to_bool(args.ordered)) - return args.table_id, session, args.benchmark_suffix - - -def get_tpch_configuration(): - parser = argparse.ArgumentParser(description="Process TPC-H Query using BigFrames.") parser.add_argument( "--project_id", type=str, required=True, - help="The BigQuery dataset ID to query.", + help="The BigQuery project ID.", ) parser.add_argument( "--dataset_id", type=str, required=True, - help="The BigQuery dataset ID to query.", + help="The BigQuery dataset ID.", ) + + if include_table_id: + parser.add_argument( + "--table_id", + type=str, + required=True, + help="The BigQuery table ID to query.", + ) + parser.add_argument( "--ordered", type=str, @@ -68,7 +54,22 @@ def get_tpch_configuration(): args = parser.parse_args() session = _initialize_session(_str_to_bool(args.ordered)) - return args.project_id, args.dataset_id, session, args.benchmark_suffix + + if include_table_id: + return ( + args.project_id, + args.dataset_id, + args.table_id, + session, + args.benchmark_suffix, + ) + else: + return ( + args.project_id, + args.dataset_id, + session, + args.benchmark_suffix, + ) def get_execution_time(func, current_path, suffix, *args, **kwargs): diff --git a/third_party/bigframes_vendored/db_benchmark/groupby_queries.py b/third_party/bigframes_vendored/db_benchmark/groupby_queries.py index 672a9b5d5f6..7758496db59 100644 --- a/third_party/bigframes_vendored/db_benchmark/groupby_queries.py +++ b/third_party/bigframes_vendored/db_benchmark/groupby_queries.py @@ -4,10 +4,10 @@ import bigframes.session -def q1(table_id: str, session: bigframes.Session): +def q1(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 1: sum v1 by id1") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.groupby("id1", as_index=False, dropna=False).agg({"v1": "sum"}) print(ans.shape) @@ -15,10 +15,10 @@ def q1(table_id: str, session: bigframes.Session): print(chk) -def q2(table_id: str, session: bigframes.Session): +def q2(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 2: sum v1 by id1:id2") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.groupby(["id1", "id2"], as_index=False, dropna=False).agg({"v1": "sum"}) print(ans.shape) @@ -26,10 +26,10 @@ def q2(table_id: str, session: bigframes.Session): print(chk) -def q3(table_id: str, session: bigframes.Session): +def q3(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 3: sum v1 mean v3 by id3") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.groupby("id3", as_index=False, dropna=False).agg( {"v1": "sum", "v3": "mean"} @@ -39,10 +39,10 @@ def q3(table_id: str, session: bigframes.Session): print(chk) -def q4(table_id: str, session: bigframes.Session): +def q4(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 4: mean v1:v3 by id4") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.groupby("id4", as_index=False, dropna=False).agg( {"v1": "mean", "v2": "mean", "v3": "mean"} @@ -52,10 +52,10 @@ def q4(table_id: str, session: bigframes.Session): print(chk) -def q5(table_id: str, session: bigframes.Session): +def q5(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 5: sum v1:v3 by id6") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.groupby("id6", as_index=False, dropna=False).agg( {"v1": "sum", "v2": "sum", "v3": "sum"} @@ -65,10 +65,10 @@ def q5(table_id: str, session: bigframes.Session): print(chk) -def q6(table_id: str, session: bigframes.Session): +def q6(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 6: median v3 sd v3 by id4 id5") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.groupby(["id4", "id5"], as_index=False, dropna=False).agg( {"v3": ["median", "std"]} @@ -78,10 +78,10 @@ def q6(table_id: str, session: bigframes.Session): print(chk) -def q7(table_id: str, session: bigframes.Session): +def q7(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 7: max v1 - min v2 by id3") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = ( x.groupby("id3", as_index=False, dropna=False) @@ -93,10 +93,10 @@ def q7(table_id: str, session: bigframes.Session): print(chk) -def q8(table_id: str, session: bigframes.Session): +def q8(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 8: largest two v3 by id6") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = ( x[~x["v3"].isna()][["id6", "v3"]] @@ -110,10 +110,10 @@ def q8(table_id: str, session: bigframes.Session): print(chk) -def q10(table_id: str, session: bigframes.Session): +def q10(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Groupby benchmark 10: sum v3 count by id1:id6") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.groupby( ["id1", "id2", "id3", "id4", "id5", "id6"], as_index=False, dropna=False diff --git a/third_party/bigframes_vendored/db_benchmark/join_queries.py b/third_party/bigframes_vendored/db_benchmark/join_queries.py index 0c01e427a6f..f0073436c02 100644 --- a/third_party/bigframes_vendored/db_benchmark/join_queries.py +++ b/third_party/bigframes_vendored/db_benchmark/join_queries.py @@ -4,12 +4,12 @@ import bigframes -def q1(table_id: str, session: bigframes.Session): +def q1(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Join benchmark 1: small inner on int") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") small = session.read_gbq( - f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'small')}" + f"{project_id}.{dataset_id}.{_get_join_table_id(table_id, 'small')}" ) ans = x.merge(small, on="id1") @@ -19,12 +19,12 @@ def q1(table_id: str, session: bigframes.Session): print(chk) -def q2(table_id: str, session: bigframes.Session): +def q2(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Join benchmark 2: medium inner on int") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") medium = session.read_gbq( - f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'medium')}" + f"{project_id}.{dataset_id}.{_get_join_table_id(table_id, 'medium')}" ) ans = x.merge(medium, on="id2") @@ -34,12 +34,12 @@ def q2(table_id: str, session: bigframes.Session): print(chk) -def q3(table_id: str, session: bigframes.Session): +def q3(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Join benchmark 3: medium outer on int") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") medium = session.read_gbq( - f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'medium')}" + f"{project_id}.{dataset_id}.{_get_join_table_id(table_id, 'medium')}" ) ans = x.merge(medium, how="left", on="id2") @@ -49,12 +49,12 @@ def q3(table_id: str, session: bigframes.Session): print(chk) -def q4(table_id: str, session: bigframes.Session): +def q4(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Join benchmark 4: medium inner on factor") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") medium = session.read_gbq( - f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'medium')}" + f"{project_id}.{dataset_id}.{_get_join_table_id(table_id, 'medium')}" ) ans = x.merge(medium, on="id5") @@ -64,12 +64,12 @@ def q4(table_id: str, session: bigframes.Session): print(chk) -def q5(table_id: str, session: bigframes.Session): +def q5(project_id: str, dataset_id: str, table_id: str, session: bigframes.Session): print("Join benchmark 5: big inner on int") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") big = session.read_gbq( - f"bigframes-dev-perf.dbbenchmark.{_get_join_table_id(table_id, 'big')}" + f"{project_id}.{dataset_id}.{_get_join_table_id(table_id, 'big')}" ) ans = x.merge(big, on="id3") diff --git a/third_party/bigframes_vendored/db_benchmark/sort_queries.py b/third_party/bigframes_vendored/db_benchmark/sort_queries.py index 600df103cf8..bbaf46cf279 100644 --- a/third_party/bigframes_vendored/db_benchmark/sort_queries.py +++ b/third_party/bigframes_vendored/db_benchmark/sort_queries.py @@ -4,10 +4,12 @@ import bigframes.session -def q1(table_id: str, session: bigframes.Session) -> None: +def q1( + project_id: str, dataset_id: str, table_id: str, session: bigframes.Session +) -> None: print("Sort benchmark 1: sort by int id2") - x = session.read_gbq(f"bigframes-dev-perf.dbbenchmark.{table_id}") + x = session.read_gbq(f"{project_id}.{dataset_id}.{table_id}") ans = x.sort_values("id2") print(ans.shape) From 1bfa598ad533f9f10c61e0dfa806b0bb4619cfa5 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Wed, 2 Oct 2024 17:38:55 -0700 Subject: [PATCH 877/921] chore: Add `semantics.filter`, guarded by an experiment flag. (#1040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add , guarded by an experiment flag. * fix test index * remove redundant line * Move semantic operators into a separate Semantics class * move test file location * check column references and update tests * check column references and update tests * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Clean up further * fix model name --------- Co-authored-by: Owl Bot --- bigframes/dataframe.py | 5 + bigframes/operations/semantics.py | 90 +++++++ .../experimental/semantic_operators.ipynb | 238 ++++++++++++++++++ tests/system/small/operations/conftest.py | 26 ++ .../system/small/operations/test_semantics.py | 64 +++++ 5 files changed, 423 insertions(+) create mode 100644 bigframes/operations/semantics.py create mode 100644 notebooks/experimental/semantic_operators.ipynb create mode 100644 tests/system/small/operations/conftest.py create mode 100644 tests/system/small/operations/test_semantics.py diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index d9f7cb9f420..7fa584bcc02 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -74,6 +74,7 @@ import bigframes.operations.aggregations import bigframes.operations.aggregations as agg_ops import bigframes.operations.plotting as plotting +import bigframes.operations.semantics import bigframes.operations.structs import bigframes.series import bigframes.series as bf_series @@ -3875,3 +3876,7 @@ def _throw_if_null_index(self, opname: str): raise bigframes.exceptions.NullIndexError( f"DataFrame cannot perform {opname} as it has no index. Set an index using set_index." ) + + @property + def semantics(self): + return bigframes.operations.semantics.Semantics(self) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py new file mode 100644 index 00000000000..b20dc594968 --- /dev/null +++ b/bigframes/operations/semantics.py @@ -0,0 +1,90 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import re +import typing + +import bigframes + + +class Semantics: + def __init__(self, df) -> None: + if not bigframes.options.experiments.semantic_operators: + raise NotImplementedError() + + self._df = df + + def filter(self, instruction: str, model): + """ + Filters the DataFrame with the semantics of the user instruction. + + Args: + instruction: + 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: + A LLM model provided by Bigframes ML package. + + Returns: + DataFrame filtered by the instruction. + + Raises: + NotImplementedError: when the semantic operator experiment is off. + ValueError: when the instruction refers to a non-existing column, or when no + columns are referred to. + """ + + # Validate column references + columns = re.findall(r"(?Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import bigframes.ml.llm as llm\n", + "model = llm.GeminiTextGenerator(model_name=llm._GEMINI_1P5_FLASH_001_ENDPOINT)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Filtering" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job f9439f7e-13cd-4990-847b-d318f223af02 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 51d2b023-6834-47f6-b17c-6b50d759ad88 is DONE. 4 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7979b08b-687e-41fc-8251-dfa0c0b41bed is DONE. 90 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 6e39dcb2-fe2f-40ad-899b-f1a29bf47bd2 is DONE. 33 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 60853851-bd33-4745-959e-bfddd970e4c4 is DONE. 33 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
countrycity
1GermanyBerlin
\n", + "

1 rows × 2 columns

\n", + "
[1 rows x 2 columns in total]" + ], + "text/plain": [ + " country city\n", + "1 Germany Berlin\n", + "\n", + "[1 rows x 2 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({'country': ['USA', 'Germany'], 'city': ['Seattle', 'Berlin']})\n", + "df.semantics.filter(\"{city} is the capital of {country}\", model)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/system/small/operations/conftest.py b/tests/system/small/operations/conftest.py new file mode 100644 index 00000000000..a85df4c5df3 --- /dev/null +++ b/tests/system/small/operations/conftest.py @@ -0,0 +1,26 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.ml.llm as llm + + +@pytest.fixture(scope="session") +def gemini_flash_model(session, bq_connection) -> llm.GeminiTextGenerator: + return llm.GeminiTextGenerator( + session=session, + connection_name=bq_connection, + model_name="gemini-1.5-flash-001", + ) diff --git a/tests/system/small/operations/test_semantics.py b/tests/system/small/operations/test_semantics.py new file mode 100644 index 00000000000..dac0232f840 --- /dev/null +++ b/tests/system/small/operations/test_semantics.py @@ -0,0 +1,64 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pandas.testing +import pytest + +import bigframes +import bigframes.dataframe as dataframe + + +def test_semantics_experiment_off_raise_error(): + bigframes.options.experiments.semantic_operators = False + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + + with pytest.raises(NotImplementedError): + df.semantics + + +def test_filter(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]}, + session=session, + ) + + actual_df = df.semantics.filter( + "{city} is the capital of {country}", 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", + [ + "No column reference", + "{city} is in the {non_existing_column}", + ], +) +def test_filter_invalid_instruction_raise_error(instruction, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + + with pytest.raises(ValueError): + df.semantics.filter(instruction, gemini_flash_model) From ffb406a4da23d7ebe91da1c2e61551a644f4c43d Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:45:15 -0700 Subject: [PATCH 878/921] chore: benchmark refactors and updates (#1043) * chore: delete failed benchmarks and collect local execution time for notebook * simplify reading * update error handling. * update geometric mean --- noxfile.py | 31 +----- scripts/run_and_publish_benchmark.py | 141 ++++++++++++++++++--------- 2 files changed, 98 insertions(+), 74 deletions(-) diff --git a/noxfile.py b/noxfile.py index 714c8333bd4..92f8acad7f1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -23,7 +23,6 @@ import re import shutil import time -import traceback from typing import Dict, List import warnings @@ -794,10 +793,6 @@ def notebook(session: nox.Session): *notebooks, ) - # Shared flag using multiprocessing.Manager() to indicate if - # any process encounters an error. This flag may be updated - # across different processes. - error_flag = multiprocessing.Manager().Value("i", False) processes = [] for notebook in notebooks: args = ( @@ -808,8 +803,8 @@ def notebook(session: nox.Session): ) if multi_process_mode: process = multiprocessing.Process( - target=_run_process, - args=(session, args, error_flag), + target=session.run, + args=args, ) process.start() processes.append(process) @@ -819,10 +814,6 @@ def notebook(session: nox.Session): else: session.run(*args) - for process in processes: - process.join() - - processes = [] for notebook, regions in notebooks_reg.items(): for region in regions: region_args = ( @@ -834,8 +825,8 @@ def notebook(session: nox.Session): ) if multi_process_mode: process = multiprocessing.Process( - target=_run_process, - args=(session, region_args, error_flag), + target=session.run, + args=region_args, ) process.start() processes.append(process) @@ -847,11 +838,6 @@ def notebook(session: nox.Session): for process in processes: process.join() - - # Check the shared error flag and raise an exception if any process - # reported an error - if error_flag.value: - raise Exception("Errors occurred in one or more subprocesses.") finally: # Prevent our notebook changes from getting checked in to git # accidentally. @@ -868,15 +854,6 @@ def notebook(session: nox.Session): ) -def _run_process(session: nox.Session, args, error_flag): - try: - session.run(*args) - except Exception: - traceback_str = traceback.format_exc() - print(traceback_str) - error_flag.value = True - - @nox.session(python=DEFAULT_PYTHON_VERSION) def benchmark(session: nox.Session): session.install("-e", ".[all]") diff --git a/scripts/run_and_publish_benchmark.py b/scripts/run_and_publish_benchmark.py index a42301cb13e..8b554937701 100644 --- a/scripts/run_and_publish_benchmark.py +++ b/scripts/run_and_publish_benchmark.py @@ -17,10 +17,11 @@ import json import os import pathlib +import re import subprocess import sys import tempfile -from typing import Dict, List, Union +from typing import Dict, List, Tuple, Union import numpy as np import pandas as pd @@ -30,7 +31,7 @@ CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() -def run_benchmark_subprocess(args, log_env_name_var, filename=None, region=None): +def run_benchmark_subprocess(args, log_env_name_var, file_path=None, region=None): """ Runs a benchmark subprocess with configured environment variables. Adjusts PYTHONPATH, sets region-specific BigQuery location, and logs environment variables. @@ -48,10 +49,37 @@ def run_benchmark_subprocess(args, log_env_name_var, filename=None, region=None) if region: env["BIGQUERY_LOCATION"] = region env[LOGGING_NAME_ENV_VAR] = log_env_name_var - subprocess.run(args, env=env, check=True) - - -def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFrame: + try: + if file_path: # Notebooks + duration_pattern = re.compile(r"(\d+\.\d+)s call") + process = subprocess.Popen(args, env=env, stdout=subprocess.PIPE, text=True) + assert process.stdout is not None + for line in process.stdout: + print(line, end="") + match = duration_pattern.search(line) + if match: + duration = match.group(1) + with open(f"{file_path}.local_exec_time_seconds", "w") as f: + f.write(f"{duration}\n") + process.wait() + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, args) + else: # Benchmarks + file_path = log_env_name_var + subprocess.run(args, env=env, check=True) + except Exception: + directory = pathlib.Path(file_path).parent + for file in directory.glob(f"{pathlib.Path(file_path).name}.*"): + if file.suffix != ".backup": + print(f"Benchmark failed, deleting: {file}") + file.unlink() + error_file = directory / f"{pathlib.Path(file_path).name}.error" + error_file.touch() + + +def collect_benchmark_result( + benchmark_path: str, iterations: int +) -> Tuple[pd.DataFrame, Union[str, None]]: """Generate a DataFrame report on HTTP queries, bytes processed, slot time and execution time from log files.""" path = pathlib.Path(benchmark_path) try: @@ -59,25 +87,18 @@ def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFra bytes_files = sorted(path.rglob("*.bytesprocessed")) millis_files = sorted(path.rglob("*.slotmillis")) bq_seconds_files = sorted(path.rglob("*.bq_exec_time_seconds")) - local_seconds_files = sorted(path.rglob("*.local_exec_time_seconds")) - has_local_seconds = len(local_seconds_files) > 0 - - if has_local_seconds: - if not ( - len(bytes_files) - == len(millis_files) - == len(local_seconds_files) - == len(bq_seconds_files) - ): - raise ValueError( - "Mismatch in the number of report files for bytes, millis, and seconds." - ) - else: - if not (len(bytes_files) == len(millis_files) == len(bq_seconds_files)): - raise ValueError( - "Mismatch in the number of report files for bytes, millis, and seconds." - ) + error_files = sorted(path.rglob("*.error")) + + if not ( + len(bytes_files) + == len(millis_files) + == len(local_seconds_files) + == len(bq_seconds_files) + ): + raise ValueError( + "Mismatch in the number of report files for bytes, millis, and seconds." + ) for idx in range(len(bytes_files)): bytes_file = bytes_files[idx] @@ -92,12 +113,11 @@ def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFra "File name mismatch among bytes, millis, and seconds reports." ) - if has_local_seconds: - local_seconds_file = local_seconds_files[idx] - if filename != local_seconds_file.relative_to(path).with_suffix(""): - raise ValueError( - "File name mismatch among bytes, millis, and seconds reports." - ) + local_seconds_file = local_seconds_files[idx] + if filename != local_seconds_file.relative_to(path).with_suffix(""): + raise ValueError( + "File name mismatch among bytes, millis, and seconds reports." + ) with open(bytes_file, "r") as file: lines = file.read().splitlines() @@ -108,12 +128,9 @@ def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFra lines = file.read().splitlines() total_slot_millis = sum(int(line) for line in lines) / iterations - if has_local_seconds: - with open(local_seconds_file, "r") as file: - lines = file.read().splitlines() - local_seconds = sum(float(line) for line in lines) / iterations - else: - local_seconds = None + with open(local_seconds_file, "r") as file: + lines = file.read().splitlines() + local_seconds = sum(float(line) for line in lines) / iterations with open(bq_seconds_file, "r") as file: lines = file.read().splitlines() @@ -132,6 +149,7 @@ def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFra path.rglob("*.slotmillis"), path.rglob("*.local_exec_time_seconds"), path.rglob("*.bq_exec_time_seconds"), + path.rglob("*.error"), ): for log_file in files_to_remove: log_file.unlink() @@ -170,13 +188,19 @@ def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFra f" bigquery execution time: {round(row['BigQuery_Execution_Time_Sec'], 1)} seconds" ) - geometric_mean_queries = geometric_mean(benchmark_metrics["Query_Count"]) - geometric_mean_bytes = geometric_mean(benchmark_metrics["Bytes_Processed"]) - geometric_mean_slot_millis = geometric_mean(benchmark_metrics["Slot_Millis"]) - geometric_mean_local_seconds = geometric_mean( + geometric_mean_queries = geometric_mean_excluding_zeros( + benchmark_metrics["Query_Count"] + ) + geometric_mean_bytes = geometric_mean_excluding_zeros( + benchmark_metrics["Bytes_Processed"] + ) + geometric_mean_slot_millis = geometric_mean_excluding_zeros( + benchmark_metrics["Slot_Millis"] + ) + geometric_mean_local_seconds = geometric_mean_excluding_zeros( benchmark_metrics["Local_Execution_Time_Sec"] ) - geometric_mean_bq_seconds = geometric_mean( + geometric_mean_bq_seconds = geometric_mean_excluding_zeros( benchmark_metrics["BigQuery_Execution_Time_Sec"] ) @@ -188,15 +212,33 @@ def collect_benchmark_result(benchmark_path: str, iterations: int) -> pd.DataFra f"Geometric mean of BigQuery execution time: {geometric_mean_bq_seconds} seconds---" ) - return benchmark_metrics.reset_index().rename(columns={"index": "Benchmark_Name"}) + error_message = ( + "\n" + + "\n".join( + [ + f"Failed: {error_file.relative_to(path).with_suffix('')}" + for error_file in error_files + ] + ) + if error_files + else None + ) + return ( + benchmark_metrics.reset_index().rename(columns={"index": "Benchmark_Name"}), + error_message, + ) -def geometric_mean(data): +def geometric_mean_excluding_zeros(data): """ - Calculate the geometric mean of a dataset, rounding the result to one decimal place. - Returns NaN if the dataset is empty or contains only NaN values. + Calculate the geometric mean of a dataset, excluding any zero values. + Returns NaN if the dataset is empty, contains only NaN values, or if + all non-NaN values are zeros. + + The result is rounded to one decimal place. """ data = data.dropna() + data = data[data != 0] if len(data) == 0: return np.nan log_data = np.log(data) @@ -321,13 +363,15 @@ def run_notebook_benchmark(benchmark_file: str, region: str): "py.test", "--nbmake", "--nbmake-timeout=900", # 15 minutes + "--durations=0", + "--color=yes", ] benchmark_args = (*pytest_command, benchmark_file) run_benchmark_subprocess( args=benchmark_args, log_env_name_var=log_env_name_var, - filename=export_file, + file_path=export_file, region=region, ) @@ -383,7 +427,7 @@ def main(): args = parse_arguments() if args.publish_benchmarks: - benchmark_metrics = collect_benchmark_result( + benchmark_metrics, error_message = collect_benchmark_result( args.publish_benchmarks, args.iterations ) # Output results to CSV without specifying a location @@ -412,6 +456,9 @@ def main(): # intended for local testing where the default behavior is not to publish results. elif project := os.getenv("GCLOUD_BENCH_PUBLISH_PROJECT", ""): publish_to_bigquery(benchmark_metrics, args.notebook, project) + + if error_message: + raise Exception(error_message) elif args.notebook: run_notebook_benchmark(args.benchmark_path, args.region) else: From d204603fdc024823421397dbe514f1f7ced1bc2c Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 3 Oct 2024 13:41:29 -0700 Subject: [PATCH 879/921] perf: repr generates fewer queries (#1046) --- bigframes/core/blocks.py | 3 ++- bigframes/dataframe.py | 1 - tests/system/small/test_dataframe.py | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 9e245399cd0..2b3734edd5d 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1557,10 +1557,11 @@ def retrieve_repr_request_results( Returns a tuple of the dataframe and the overall number of rows of the query. """ + # head caches full underlying expression, so row_count will be free after head_result = self.session._executor.head(self.expr, max_results) count = self.session._executor.get_row_count(self.expr) - arrow = self.session._executor.execute(self.expr).to_arrow_table() + arrow = head_result.to_arrow_table() df = io_pandas.arrow_to_pandas(arrow, schema=self.expr.schema) self._copy_index_to_pandas(df) return df, count, head_result.query_job diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 7fa584bcc02..efd0e65adb4 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -690,7 +690,6 @@ def _repr_html_(self) -> str: if opts.repr_mode == "deferred": return formatter.repr_query_job(self._compute_dry_run()) - self._cached() # TODO(swast): pass max_columns and get the true column count back. Maybe # get 1 more column than we have requested so that pandas can add the # ... for us? diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 8c2912edd4a..cfd6efe9bd3 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -591,15 +591,19 @@ def test_join_repr(scalars_dfs_maybe_ordered): assert actual == expected -def test_repr_html_w_all_rows(scalars_dfs): +def test_repr_html_w_all_rows(scalars_dfs, session): + metrics = session._metrics scalars_df, _ = scalars_dfs # get a pandas df of the expected format df, _ = scalars_df._block.to_pandas() pandas_df = df.set_axis(scalars_df._block.column_labels, axis=1) pandas_df.index.name = scalars_df.index.name + executions_pre = metrics.execution_count # When there are 10 or fewer rows, the outputs should be identical except for the extra note. actual = scalars_df.head(10)._repr_html_() + executions_post = metrics.execution_count + with display_options.pandas_repr(bigframes.options.display): pandas_repr = pandas_df.head(10)._repr_html_() @@ -608,6 +612,7 @@ def test_repr_html_w_all_rows(scalars_dfs): + f"[{len(pandas_df.index)} rows x {len(pandas_df.columns)} columns in total]" ) assert actual == expected + assert (executions_post - executions_pre) <= 2 def test_df_column_name_with_space(scalars_dfs): From b6cd55afc49b522904a13a7fd34d40201d176588 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 3 Oct 2024 20:51:45 +0000 Subject: [PATCH 880/921] fix: make invalid location warning case-insensitive (#1044) * fix: make invalid location warning case-insensitive * fix failing unit test * add system tests for non canonical location setting --- bigframes/_config/bigquery_options.py | 53 ++++++++++++--------- bigframes/constants.py | 9 ++-- tests/system/large/test_location.py | 39 ++++++++++++--- tests/unit/_config/test_bigquery_options.py | 4 ++ 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 502f103bb50..e81557e3438 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -36,26 +36,36 @@ UNKNOWN_LOCATION_MESSAGE = "The location '{location}' is set to an unknown value. Did you mean '{possibility}'?" -def _validate_location(value: Optional[str]): - - if value is None: - return - - if value not in bigframes.constants.ALL_BIGQUERY_LOCATIONS: - location = str(value) - possibility = min( - bigframes.constants.ALL_BIGQUERY_LOCATIONS, - key=lambda item: jellyfish.levenshtein_distance(location, item), - ) - warnings.warn( - UNKNOWN_LOCATION_MESSAGE.format(location=location, possibility=possibility), - # There are many layers before we get to (possibly) the user's code: - # -> bpd.options.bigquery.location = "us-central-1" - # -> location.setter - # -> _validate_location - stacklevel=3, - category=bigframes.exceptions.UnknownLocationWarning, - ) +def _get_validated_location(value: Optional[str]) -> Optional[str]: + + if value is None or value in bigframes.constants.ALL_BIGQUERY_LOCATIONS: + return value + + location = str(value) + + location_lowercase = location.lower() + if location_lowercase in bigframes.constants.BIGQUERY_REGIONS: + return location_lowercase + + location_uppercase = location.upper() + if location_uppercase in bigframes.constants.BIGQUERY_MULTIREGIONS: + return location_uppercase + + possibility = min( + bigframes.constants.ALL_BIGQUERY_LOCATIONS, + key=lambda item: jellyfish.levenshtein_distance(location, item), + ) + warnings.warn( + UNKNOWN_LOCATION_MESSAGE.format(location=location, possibility=possibility), + # There are many layers before we get to (possibly) the user's code: + # -> bpd.options.bigquery.location = "us-central-1" + # -> location.setter + # -> _get_validated_location + stacklevel=3, + category=bigframes.exceptions.UnknownLocationWarning, + ) + + return value def _validate_ordering_mode(value: str) -> bigframes.enums.OrderingMode: @@ -135,8 +145,7 @@ def location(self) -> Optional[str]: def location(self, value: Optional[str]): if self._session_started and self._location != value: raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="location")) - _validate_location(value) - self._location = value + self._location = _get_validated_location(value) @property def project(self) -> Optional[str]: diff --git a/bigframes/constants.py b/bigframes/constants.py index 4d5b6b8eb3d..e0c83050797 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -22,9 +22,8 @@ DEFAULT_EXPIRATION = datetime.timedelta(days=7) # https://cloud.google.com/bigquery/docs/locations -ALL_BIGQUERY_LOCATIONS = frozenset( +BIGQUERY_REGIONS = frozenset( { - # regions "us-east5", "us-south1", "us-central1", @@ -68,11 +67,15 @@ "me-central1", "me-west1", "africa-south1", - # multi-regions + } +) +BIGQUERY_MULTIREGIONS = frozenset( + { "US", "EU", } ) +ALL_BIGQUERY_LOCATIONS = frozenset(BIGQUERY_REGIONS.union(BIGQUERY_MULTIREGIONS)) # https://cloud.google.com/storage/docs/regional-endpoints REP_ENABLED_BIGQUERY_LOCATIONS = frozenset( diff --git a/tests/system/large/test_location.py b/tests/system/large/test_location.py index 204c6b7463b..2ef002d7e05 100644 --- a/tests/system/large/test_location.py +++ b/tests/system/large/test_location.py @@ -22,7 +22,9 @@ import bigframes.session.clients -def _assert_bq_execution_location(session: bigframes.Session): +def _assert_bq_execution_location( + session: bigframes.Session, expected_location: typing.Optional[str] = None +): df = session.read_gbq( """ SELECT "aaa" as name, 111 as number @@ -33,10 +35,10 @@ def _assert_bq_execution_location(session: bigframes.Session): """ ) - assert ( - typing.cast(bigquery.QueryJob, df.query_job).location - == session.bqclient.location - ) + if expected_location is None: + expected_location = session._location + + assert typing.cast(bigquery.QueryJob, df.query_job).location == expected_location result = ( df[["name", "number"]] @@ -47,8 +49,7 @@ def _assert_bq_execution_location(session: bigframes.Session): ) assert ( - typing.cast(bigquery.QueryJob, result.query_job).location - == session.bqclient.location + typing.cast(bigquery.QueryJob, result.query_job).location == expected_location ) @@ -87,6 +88,30 @@ def test_bq_location(bigquery_location): _assert_bq_execution_location(session) +@pytest.mark.parametrize( + ("set_location", "resolved_location"), + # Sort the set to avoid nondeterminism. + [ + (loc.capitalize(), loc) + for loc in sorted(bigframes.constants.ALL_BIGQUERY_LOCATIONS) + ], +) +def test_bq_location_non_canonical(set_location, resolved_location): + session = bigframes.Session( + context=bigframes.BigQueryOptions(location=set_location) + ) + + assert session.bqclient.location == set_location + + # by default global endpoint is used + assert ( + session.bqclient._connection.API_BASE_URL == "https://bigquery.googleapis.com" + ) + + # assert that bigframes session honors the location + _assert_bq_execution_location(session, resolved_location) + + @pytest.mark.parametrize( "bigquery_location", # Sort the set to avoid nondeterminism. diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index b827b0723d9..f40c140a9e9 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -90,6 +90,10 @@ def test_setter_if_session_started_but_setting_the_same_value(attribute): [ (None,), ("us-central1",), + ("us-Central1",), + ("US-CENTRAL1",), + ("US",), + ("us",), ], ) def test_location_set_to_valid_no_warning(valid_location): From 307385f5295ae6918e7d42dcca2c0e0c32e82446 Mon Sep 17 00:00:00 2001 From: Arwa Sharif <146148342+arwas11@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:11:10 -0500 Subject: [PATCH 881/921] docs: add docstring return type section to BigQueryOptions class (#964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add docstring return type section to BigQueryOptions class * docs: add docstring return type section to BigQueryOptions class * docs: add docstrings to SamplingOptions class * feat: add Gemini 1.5 stable models support (#945) * feat: add Gemini 1.5 stable models support * add to loader * refactor: Simplify projection nodes (#961) * docs: add docstring returns section to Options (#937) * chore: drop unused columns at is_monotonic methods (#912) * chore: drop unused columns at is_monotonic methods * fixing mypy * test: retry streaming tests to accommodate flakiness (#956) * test: retry streaming tests to accommodate flakiness * reduce delay, increase retries * fix: make `read_gbq_function` work for multi-param functions (#947) * fix: make `read_gbq_function` work for multi-param functions * fix hyperlink * specify hyperlink differently * make hyperlink markdown format * fix: support `read_gbq_function` for axis=1 application (#950) * fix: support `read_gbq_function` for axis=1 application * remove stray newline * Update bigframes/session/__init__.py * remove first person reference in the doc * use correct product name --------- Co-authored-by: Tim Sweña (Swast) * docs: update title of pypi notebook example to reflect use of the PyPI public dataset (#952) In response to feedback on internal change 662899733. * docs: add docstrings to SamplingOptions class --------- Co-authored-by: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Co-authored-by: TrevorBergeron Co-authored-by: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Co-authored-by: Shobhit Singh Co-authored-by: Tim Sweña (Swast) --- bigframes/_config/bigquery_options.py | 48 +++++++++++++++++++++++++-- bigframes/_config/sampling_options.py | 36 ++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index e81557e3438..42007a388ea 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -111,6 +111,10 @@ def application_name(self) -> Optional[str]: The application name to amend to the user agent sent to Google APIs. The recommended format is ``"application-name/major.minor.patch_version"`` or ``"(gpn:PartnerName;)"`` for official Google partners. + + Returns: + None or str: + Application name as a string if exists; otherwise None. """ return self._application_name @@ -124,7 +128,12 @@ def application_name(self, value: Optional[str]): @property def credentials(self) -> Optional[google.auth.credentials.Credentials]: - """The OAuth2 credentials to use for this client.""" + """The OAuth2 credentials to use for this client. + + Returns: + None or google.auth.credentials.Credentials: + google.auth.credentials.Credentials if exists; otherwise None. + """ return self._credentials @credentials.setter @@ -138,6 +147,10 @@ def location(self) -> Optional[str]: """Default location for job, datasets, and tables. For more information, see https://cloud.google.com/bigquery/docs/locations BigQuery locations. + + Returns: + None or str: + Default location as a string; otherwise None. """ return self._location @@ -149,7 +162,12 @@ def location(self, value: Optional[str]): @property def project(self) -> Optional[str]: - """Google Cloud project ID to use for billing and as the default project.""" + """Google Cloud project ID to use for billing and as the default project. + + Returns: + None or str: + Google Cloud project ID as a string; otherwise None. + """ return self._project @project.setter @@ -172,6 +190,10 @@ def bq_connection(self) -> Optional[str]: If this option isn't provided, or project or location aren't provided, session will use its default project/location/connection_id as default connection. + + Returns: + None or str: + Name of the BigQuery connection as a string; otherwise None. """ return self._bq_connection @@ -190,6 +212,12 @@ def skip_bq_connection_check(self) -> bool: connection (default or user-provided) does not exist, or it does not have necessary permissions set up to support BigQuery DataFrames operations, then a runtime error will be reported. + + Returns: + bool: + A boolean value, where True indicates a BigQuery connection is + not created or the connection does not have necessary + permissions set up; otherwise False. """ return self._skip_bq_connection_check @@ -212,6 +240,11 @@ def use_regional_endpoints(self) -> bool: Requires that ``location`` is set. For example, to connect to asia-northeast1-bigquery.googleapis.com, specify ``location='asia-northeast1'`` and ``use_regional_endpoints=True``. + + Returns: + bool: + A boolean value, where True indicates that a location is set; + otherwise False. """ return self._use_regional_endpoints @@ -244,6 +277,10 @@ def kms_key_name(self) -> Optional[str]: Cloud KMS CryptoKey Encrypter/Decrypter IAM role in the key's project. For more information, see https://cloud.google.com/bigquery/docs/customer-managed-encryption#assign_role Assign the Encrypter/Decrypter. + + Returns: + None or str: + Name of the customer managed encryption key as a string; otherwise None. """ return self._kms_key_name @@ -256,7 +293,12 @@ def kms_key_name(self, value: str): @property def ordering_mode(self) -> Literal["strict", "partial"]: - """Controls whether total row order is always maintained for DataFrame/Series.""" + """Controls whether total row order is always maintained for DataFrame/Series. + + Returns: + Literal: + A literal string value of either strict or partial ordering mode. + """ return self._ordering_mode.value @ordering_mode.setter diff --git a/bigframes/_config/sampling_options.py b/bigframes/_config/sampling_options.py index f4fa0928e15..ddb2a497133 100644 --- a/bigframes/_config/sampling_options.py +++ b/bigframes/_config/sampling_options.py @@ -33,14 +33,44 @@ class SamplingOptions: random_state: Optional[int] = None def with_max_download_size(self, max_rows: Optional[int]) -> SamplingOptions: + """Configures the maximum download size for data sampling in MB + + Args: + max_rows (None or int): + An int value for the maximum row size. + + Returns: + bigframes._config.sampling_options.SamplingOptions: + The configuration for data sampling. + """ return SamplingOptions( max_rows, self.enable_downsampling, self.sampling_method, self.random_state ) def with_method(self, method: Literal["head", "uniform"]) -> SamplingOptions: + """Configures the downsampling algorithms to be chosen from + + Args: + method (None or Literal): + A literal string value of either head or uniform data sampling method. + + Returns: + bigframes._config.sampling_options.SamplingOptions: + The configuration for data sampling. + """ return SamplingOptions(self.max_download_size, True, method, self.random_state) def with_random_state(self, state: Optional[int]) -> SamplingOptions: + """Configures the seed for the uniform downsampling algorithm + + Args: + state (None or int): + An int value for the data sampling random state + + Returns: + bigframes._config.sampling_options.SamplingOptions: + The configuration for data sampling. + """ return SamplingOptions( self.max_download_size, self.enable_downsampling, @@ -49,6 +79,12 @@ def with_random_state(self, state: Optional[int]) -> SamplingOptions: ) def with_disabled(self) -> SamplingOptions: + """Configures whether to disable downsampling + + Returns: + bigframes._config.sampling_options.SamplingOptions: + The configuration for data sampling. + """ return SamplingOptions( self.max_download_size, False, self.sampling_method, self.random_state ) From c86e00202b97a5e127c7df45cfdcdabd86b7b82e Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:17:25 -0700 Subject: [PATCH 882/921] chore: benchmark fix (#1049) --- tests/benchmark/db_benchmark/sort/q1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/benchmark/db_benchmark/sort/q1.py b/tests/benchmark/db_benchmark/sort/q1.py index 4888dda0452..5f6c404443c 100644 --- a/tests/benchmark/db_benchmark/sort/q1.py +++ b/tests/benchmark/db_benchmark/sort/q1.py @@ -24,7 +24,7 @@ table_id, session, suffix, - ) = utils.get_dbbenchmark_configuration() + ) = utils.get_configuration(include_table_id=True) current_path = pathlib.Path(__file__).absolute() utils.get_execution_time( From 3988edabdd25c17d49acd30aa3473f934d31f51a Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Thu, 3 Oct 2024 18:00:27 -0700 Subject: [PATCH 883/921] chore: Implement semantic map (#1045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: implement semantic map * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * validate model type * update API and tests * migrate tests to large system and update doc examples * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * add more test and polish parameters --------- Co-authored-by: Owl Bot --- bigframes/operations/semantics.py | 134 +++++++++++--- .../experimental/semantic_operators.ipynb | 168 ++++++++++++++++-- tests/system/large/operations/__init__.py | 13 ++ .../{small => large}/operations/conftest.py | 0 .../system/large/operations/test_semantics.py | 162 +++++++++++++++++ .../system/small/operations/test_semantics.py | 64 ------- 6 files changed, 435 insertions(+), 106 deletions(-) create mode 100644 tests/system/large/operations/__init__.py rename tests/system/{small => large}/operations/conftest.py (100%) create mode 100644 tests/system/large/operations/test_semantics.py delete mode 100644 tests/system/small/operations/test_semantics.py diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index b20dc594968..976ce17c9c6 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -30,6 +30,24 @@ def filter(self, instruction: str, model): """ Filters the DataFrame with the semantics of the user instruction. + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> import bigframes + >>> bigframes.options.experiments.semantic_operators = True + + >>> 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.semantics.filter("{city} is the capital of {country}", model) + country city + 1 Germany Berlin + + [1 rows x 2 columns] + Args: instruction: An instruction on how to filter the data. This value must contain @@ -39,7 +57,7 @@ def filter(self, instruction: str, model): "The {food} is healthy." model: - A LLM model provided by Bigframes ML package. + A GeminiTextGenerator provided by Bigframes ML package. Returns: DataFrame filtered by the instruction. @@ -49,9 +67,89 @@ def filter(self, instruction: str, model): ValueError: when the instruction refers to a non-existing column, or when no columns are referred to. """ + _validate_model(model) + + output_instruction = "Based on the provided context, reply to the following claim by only True or False:" + + from bigframes.dataframe import DataFrame + + results = typing.cast( + DataFrame, model.predict(self._make_prompt(instruction, output_instruction)) + ) + + return self._df[ + results["ml_generate_text_llm_result"].str.lower().str.contains("true") + ] + + def map(self, instruction: str, output_column: str, model): + """ + Maps the DataFrame with the semantics of the user instruction. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> import bigframes + >>> bigframes.options.experiments.semantic_operators = True + + >>> 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.semantics.map("What is the food made from {ingredient_1} and {ingredient_2}? One word only.", result_column_name="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: + 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}." + result_column_name: + The column name of the mapping result. + + model: + A GeminiTextGenerator provided by Bigframes ML package. + + Returns: + DataFrame with attached mapping results. + + Raises: + NotImplementedError: when the semantic operator experiment is off. + ValueError: when the instruction refers to a non-existing column, or when no + columns are referred to. + """ + _validate_model(model) + + output_instruction = ( + "Based on the provided contenxt, answer the following instruction:" + ) + + from bigframes.series import Series + + results = typing.cast( + Series, + model.predict(self._make_prompt(instruction, output_instruction))[ + "ml_generate_text_llm_result" + ], + ) + + from bigframes.core.reshape import concat + + return concat([self._df, results.rename(output_column)], axis=1) + + def _make_prompt(self, user_instruction: str, output_instruction: str): # Validate column references - columns = re.findall(r"(?Open Job" + "Query job 56de4aea-6e28-42fc-9760-b65c7a9c0ae7 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -89,13 +89,13 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job f9439f7e-13cd-4990-847b-d318f223af02 is DONE. 0 Bytes processed. Open Job" + "Query job bf5dd330-8e3e-45d2-b443-a61e595debba is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -115,19 +115,7 @@ { "data": { "text/html": [ - "Query job 51d2b023-6834-47f6-b17c-6b50d759ad88 is DONE. 4 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 7979b08b-687e-41fc-8251-dfa0c0b41bed is DONE. 90 Bytes processed. Open Job" + "Query job 8ede807b-ae35-4d44-aaac-0788aab8398c is DONE. 4 Bytes processed. Open Job" ], "text/plain": [ "" @@ -139,7 +127,7 @@ { "data": { "text/html": [ - "Query job 6e39dcb2-fe2f-40ad-899b-f1a29bf47bd2 is DONE. 33 Bytes processed. Open Job" + "Query job 1c7d1215-0661-4d4a-95eb-79dfbea65413 is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -151,7 +139,7 @@ { "data": { "text/html": [ - "Query job 60853851-bd33-4745-959e-bfddd970e4c4 is DONE. 33 Bytes processed. Open Job" + "Query job e562f224-9cd6-4b55-8bf0-145a3bd64540 is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -203,7 +191,7 @@ "[1 rows x 2 columns]" ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -212,6 +200,148 @@ "df = bpd.DataFrame({'country': ['USA', 'Germany'], 'city': ['Seattle', 'Berlin']})\n", "df.semantics.filter(\"{city} is the capital of {country}\", model)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Mapping" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "df = bpd.DataFrame(\n", + " data={\"ingredient_1\": [\"Burger Bun\", \"Soy Bean\"], \"ingredient_2\": [\"Beef Patty\", \"Bittern\"]}\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job f62b4175-cb34-4e04-9a3f-4bfe1965f72f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job b86fbb98-a566-4887-a938-f80fe3888b27 is DONE. 4 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d4f09988-48d9-48df-a138-a7256b9a5766 is DONE. 34 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 293d186f-359c-40d1-87f2-e8d525fd72ba is DONE. 93 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ingredient_1ingredient_2food
0Burger BunBeef PattyBurger
1Soy BeanBitternTofu
\n", + "

2 rows × 3 columns

\n", + "
[2 rows x 3 columns in total]" + ], + "text/plain": [ + " ingredient_1 ingredient_2 food\n", + "0 Burger Bun Beef Patty Burger \n", + "\n", + "1 Soy Bean Bittern Tofu \n", + "\n", + "\n", + "[2 rows x 3 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.semantics.map(\"What is the food made from {ingredient_1} and {ingredient_2}? One word only.\", output_column=\"food\", model=model)" + ] } ], "metadata": { diff --git a/tests/system/large/operations/__init__.py b/tests/system/large/operations/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/tests/system/large/operations/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/system/small/operations/conftest.py b/tests/system/large/operations/conftest.py similarity index 100% rename from tests/system/small/operations/conftest.py rename to tests/system/large/operations/conftest.py diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py new file mode 100644 index 00000000000..e9668aacccb --- /dev/null +++ b/tests/system/large/operations/test_semantics.py @@ -0,0 +1,162 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pandas.testing +import pytest + +import bigframes +import bigframes.dataframe as dataframe + + +def test_semantics_experiment_off_raise_error(): + bigframes.options.experiments.semantic_operators = False + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + + with pytest.raises(NotImplementedError): + df.semantics + + +def test_filter(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]}, + session=session, + ) + + actual_df = df.semantics.filter( + "{city} is the capital of {country}", 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 + ) + + +def test_filter_single_column_reference(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]}, + session=session, + ) + + actual_df = df.semantics.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", + [ + "No column reference", + "{city} is in the {non_existing_column}", + ], +) +def test_filter_invalid_instruction_raise_error(instruction, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + + with pytest.raises(ValueError): + df.semantics.filter(instruction, gemini_flash_model) + + +def test_filter_invalid_model_raise_error(): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + + with pytest.raises(ValueError): + df.semantics.filter("{city} is the capital of {country}", None) + + +def test_map(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={ + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + }, + session=session, + ) + + actual_df = df.semantics.map( + "What is the 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"], + "food": ["burger", "tofu"], + } + ) + pandas.testing.assert_frame_equal( + actual_df, + expected_df, + check_dtype=False, + check_index_type=False, + check_column_type=False, + ) + + +@pytest.mark.parametrize( + "instruction", + [ + "No column reference", + "What is the food made from {ingredient_1} and {non_existing_column}?}", + ], +) +def test_map_invalid_instruction_raise_error(instruction, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={ + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + } + ) + + with pytest.raises(ValueError): + df.semantics.map(instruction, "food", gemini_flash_model) + + +def test_map_invalid_model_raise_error(): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={ + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + }, + ) + + with pytest.raises(ValueError): + df.semantics.map( + "What is the food made from {ingredient_1} and {ingredient_2}? One word only.", + "food", + None, + ) diff --git a/tests/system/small/operations/test_semantics.py b/tests/system/small/operations/test_semantics.py deleted file mode 100644 index dac0232f840..00000000000 --- a/tests/system/small/operations/test_semantics.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pandas as pd -import pandas.testing -import pytest - -import bigframes -import bigframes.dataframe as dataframe - - -def test_semantics_experiment_off_raise_error(): - bigframes.options.experiments.semantic_operators = False - df = dataframe.DataFrame( - {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} - ) - - with pytest.raises(NotImplementedError): - df.semantics - - -def test_filter(session, gemini_flash_model): - bigframes.options.experiments.semantic_operators = True - df = dataframe.DataFrame( - data={"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]}, - session=session, - ) - - actual_df = df.semantics.filter( - "{city} is the capital of {country}", 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", - [ - "No column reference", - "{city} is in the {non_existing_column}", - ], -) -def test_filter_invalid_instruction_raise_error(instruction, gemini_flash_model): - bigframes.options.experiments.semantic_operators = True - df = dataframe.DataFrame( - {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} - ) - - with pytest.raises(ValueError): - df.semantics.filter(instruction, gemini_flash_model) From 5ac217d650bc4f5576ba2b6595a3c0b1d88813ad Mon Sep 17 00:00:00 2001 From: Arwa Sharif <146148342+arwas11@users.noreply.github.com> Date: Sun, 6 Oct 2024 18:02:15 -0500 Subject: [PATCH 884/921] fix: fix generic error message when entering an incorrect column name (#1031) * fix: fix generic error message when entering incorrect column name * fix varibale initialization * Add a test case * Update code and fix failling tests * Add test case for multiple columns --------- Co-authored-by: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> --- bigframes/core/groupby/__init__.py | 6 ++++++ tests/system/small/test_groupby.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 5cb0e657290..2d351cf82df 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -88,6 +88,12 @@ def __getitem__( keys = list(key) else: keys = [key] + + bad_keys = [key for key in keys if key not in self._block.column_labels] + + if len(bad_keys) > 0: + raise KeyError(f"Columns not found: {str(bad_keys)[1:-1]}") + columns = [ col_id for col_id, label in self._col_id_labels.items() if label in keys ] diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index 8574860daad..2d5ae21bb4d 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -421,6 +421,34 @@ def test_dataframe_groupby_getitem( pd.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) +def test_dataframe_groupby_getitem_error( + scalars_df_index, + scalars_pandas_df_index, +): + col_names = ["float64_col", "int64_col", "bool_col", "string_col"] + with pytest.raises(KeyError, match="\"Columns not found: 'not_in_group'\""): + ( + scalars_df_index[col_names] + .groupby("string_col")["not_in_group"] + .min() + .to_pandas() + ) + + +def test_dataframe_groupby_getitem_multiple_columns_error( + scalars_df_index, + scalars_pandas_df_index, +): + col_names = ["float64_col", "int64_col", "bool_col", "string_col"] + with pytest.raises(KeyError, match="\"Columns not found: 'col1', 'col2'\""): + ( + scalars_df_index[col_names] + .groupby("string_col")["col1", "col2"] + .min() + .to_pandas() + ) + + def test_dataframe_groupby_getitem_list( scalars_df_index, scalars_pandas_df_index, From feacaf4b2166b0c1dd0dff760ef617c1c1ab8afc Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Mon, 7 Oct 2024 10:54:35 -0700 Subject: [PATCH 885/921] chore: implement semantic join (#1051) * chore: implement semantic join * remove redundant lines * fix column reference validation * add row size check * Fix doctest in semantics.map --- bigframes/operations/semantics.py | 130 ++++++- .../experimental/semantic_operators.ipynb | 325 +++++++++++++++++- .../system/large/operations/test_semantics.py | 142 ++++++++ 3 files changed, 582 insertions(+), 15 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index 976ce17c9c6..1a4ff90b4ed 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -15,6 +15,7 @@ import re import typing +from typing import List import bigframes @@ -97,7 +98,7 @@ def map(self, instruction: str, output_column: str, model): >>> 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.semantics.map("What is the food made from {ingredient_1} and {ingredient_2}? One word only.", result_column_name="food", model=model) + >>> df.semantics.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 @@ -148,11 +149,7 @@ def map(self, instruction: str, output_column: str, model): return concat([self._df, results.rename(output_column)], axis=1) def _make_prompt(self, user_instruction: str, output_instruction: str): - # Validate column references - columns = re.findall(r"(?>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> import bigframes + >>> bigframes.options.experiments.semantic_operators = True + + >>> 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.semantics.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: + The other dataframe. + + instruction: + 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" suffix for differentiation. This is especially important when you do + self joins. For example: "The {employee_name_left} reports to {employee_name_right}" + You must not add "_left" or "_right" suffix to non-overlapping columns. + + model: + A GeminiTextGenerator provided by Bigframes ML package. + + max_rows: + 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. + + Returns: + The joined dataframe. + + Raises: + ValueError if the amount of data that will be sent for LLM processing is larger than max_rows. + """ + _validate_model(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}." + ) + + columns = _parse_columns(instruction) + + 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.endswith("_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: + raise ValueError(f"Unnecessary suffix for {col}") + else: + raise ValueError(f"Column {col} not found") + + elif col.endswith("_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: + raise ValueError(f"Unnecessary suffix for {col}") + else: + raise ValueError(f"Column {col} not found") + + else: + raise ValueError(f"Column {col} not found") + + if not left_columns or not right_columns: + raise ValueError() + + joined_df = self._df.merge(other, how="cross", suffixes=("_left", "_right")) + + return joined_df.semantics.filter(instruction, model).reset_index(drop=True) + def _validate_model(model): from bigframes.ml.llm import GeminiTextGenerator if not isinstance(model, GeminiTextGenerator): raise ValueError("Model is not GeminiText Generator") + + +def _parse_columns(instruction: str) -> List[str]: + columns = re.findall(r"(?Open Job" + "Query job b113bf05-6b85-41bc-8754-aebab5d6c65e is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -95,7 +95,7 @@ { "data": { "text/html": [ - "Query job bf5dd330-8e3e-45d2-b443-a61e595debba is DONE. 0 Bytes processed. Open Job" + "Query job 105c8fa5-6c33-48c7-9526-25fc5ac0013d is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -115,7 +115,7 @@ { "data": { "text/html": [ - "Query job 8ede807b-ae35-4d44-aaac-0788aab8398c is DONE. 4 Bytes processed. Open Job" + "Query job cf8e6bdd-1845-412b-ac13-16f03e968262 is DONE. 4 Bytes processed. Open Job" ], "text/plain": [ "" @@ -127,7 +127,7 @@ { "data": { "text/html": [ - "Query job 1c7d1215-0661-4d4a-95eb-79dfbea65413 is DONE. 33 Bytes processed. Open Job" + "Query job 28b6a9e8-5742-421b-b79c-f2fa647ebd10 is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -139,7 +139,7 @@ { "data": { "text/html": [ - "Query job e562f224-9cd6-4b55-8bf0-145a3bd64540 is DONE. 33 Bytes processed. Open Job" + "Query job 3d3e530f-4dde-447e-939d-eaf3b072f75b is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -227,7 +227,7 @@ { "data": { "text/html": [ - "Query job f62b4175-cb34-4e04-9a3f-4bfe1965f72f is DONE. 0 Bytes processed. Open Job" + "Query job 25b719ed-c1ad-45d9-9926-0a3408aff33d is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -247,7 +247,7 @@ { "data": { "text/html": [ - "Query job b86fbb98-a566-4887-a938-f80fe3888b27 is DONE. 4 Bytes processed. Open Job" + "Query job 010de4ac-7e7d-4aa2-9e73-34e2db77f2c8 is DONE. 4 Bytes processed. Open Job" ], "text/plain": [ "" @@ -259,7 +259,7 @@ { "data": { "text/html": [ - "Query job d4f09988-48d9-48df-a138-a7256b9a5766 is DONE. 34 Bytes processed. Open Job" + "Query job e5688060-a489-44bb-91e1-0272ca2d836b is DONE. 34 Bytes processed. Open Job" ], "text/plain": [ "" @@ -271,7 +271,7 @@ { "data": { "text/html": [ - "Query job 293d186f-359c-40d1-87f2-e8d525fd72ba is DONE. 93 Bytes processed. Open Job" + "Query job 7eeea068-83dd-4451-a18d-ae06525097d0 is DONE. 93 Bytes processed. Open Job" ], "text/plain": [ "" @@ -342,6 +342,313 @@ "source": [ "df.semantics.map(\"What is the food made from {ingredient_1} and {ingredient_2}? One word only.\", output_column=\"food\", model=model)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Joining" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "cities = bpd.DataFrame({'city': ['Seattle', 'Ottawa', 'Berlin', 'Shanghai', 'New Delhi']})\n", + "continents = bpd.DataFrame({'continent': ['North America', 'Africa', 'Asia']})" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 5467ef2f-832a-44c0-a4f8-f3b92f495348 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job f3123dc6-ec48-444f-a940-7b768cf6341a is DONE. 30 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 71c00675-0639-4d75-b5d2-5fb150f1aa54 is DONE. 251 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f64ede6e-f4ed-4764-a82a-8fefce20dfca is DONE. 144 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
citycontinent
0SeattleNorth America
1OttawaNorth America
2ShanghaiAsia
3New DelhiAsia
\n", + "

4 rows × 2 columns

\n", + "
[4 rows x 2 columns in total]" + ], + "text/plain": [ + " city continent\n", + "0 Seattle North America\n", + "1 Ottawa North America\n", + "2 Shanghai Asia\n", + "3 New Delhi Asia\n", + "\n", + "[4 rows x 2 columns]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cities.semantics.join(continents, \"{city} is in {continent}\", model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Self Joins" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "animals = bpd.DataFrame({'animal': ['cow', 'cat', 'spider', 'elephant']})" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job ca4abfb9-6fc9-4d70-b948-d426ec85e83f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 25e0cdb2-9cfd-4d5e-91ea-34116af5dd91 is DONE. 32 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 9b93cd37-51f2-4908-91e3-369380d43028 is DONE. 266 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 552be84f-e6e7-46b3-ab74-3e8261ccb396 is DONE. 180 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
animal_leftanimal_right
0cowcat
1cowspider
2catspider
3elephantcow
4elephantcat
5elephantspider
\n", + "

6 rows × 2 columns

\n", + "
[6 rows x 2 columns in total]" + ], + "text/plain": [ + " animal_left animal_right\n", + "0 cow cat\n", + "1 cow spider\n", + "2 cat spider\n", + "3 elephant cow\n", + "4 elephant cat\n", + "5 elephant spider\n", + "\n", + "[6 rows x 2 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "animals.semantics.join(animals, \"{animal_left} generally weighs heavier than {animal_right}\", model)" + ] } ], "metadata": { diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py index e9668aacccb..a2c73b5d269 100644 --- a/tests/system/large/operations/test_semantics.py +++ b/tests/system/large/operations/test_semantics.py @@ -160,3 +160,145 @@ def test_map_invalid_model_raise_error(): "food", None, ) + + +def test_join(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + cities = dataframe.DataFrame( + data={ + "city": ["Seattle", "Berlin"], + }, + session=session, + ) + countries = dataframe.DataFrame( + data={"country": ["USA", "UK", "Germany"]}, + session=session, + ) + + actual_df = cities.semantics.join( + countries, + "{city} belongs to {country}", + 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, + ) + + +def test_self_join(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + animals = dataframe.DataFrame( + data={ + "animal": ["spider", "capybara"], + }, + session=session, + ) + + actual_df = animals.semantics.join( + animals, + "{animal_left} is heavier than {animal_right}", + 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, + ) + + +def test_join_data_too_large_raise_error(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + cities = dataframe.DataFrame( + data={ + "city": ["Seattle", "Berlin"], + }, + session=session, + ) + countries = dataframe.DataFrame( + data={"country": ["USA", "UK", "Germany"]}, + session=session, + ) + + with pytest.raises(ValueError): + cities.semantics.join( + countries, "{city} belongs to {country}", gemini_flash_model, max_rows=1 + ) + + +@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( + "{city_left} is in {country}", + r"Unnecessary suffix for .+", + id="suffix_on_left_unique_column", + ), + pytest.param( + "{city} is in {region_right}", + r"Unnecessary suffix for .+", + id="suffix_on_right_unique_column", + ), + pytest.param( + "{city_right} is in {country}", r"Column .+ not found", id="wrong_suffix" + ), + pytest.param( + "{city} is in {continent_right}", + r"Column .+ not found", + id="suffix_on_non_existing_column", + ), + ], +) +def test_join_invalid_instruction_raise_error( + instruction, error_pattern, gemini_flash_model +): + bigframes.options.experiments.semantic_operators = True + df1 = dataframe.DataFrame( + {"city": ["Seattle", "Berlin"], "country": ["USA", "Germany"]} + ) + df2 = dataframe.DataFrame( + { + "country": ["USA", "UK", "Germany"], + "region": ["North America", "Europe", "Europe"], + } + ) + + with pytest.raises(ValueError, match=error_pattern): + df1.semantics.join(df2, instruction, gemini_flash_model) + + +def test_join_invalid_model_raise_error(): + bigframes.options.experiments.semantic_operators = True + cities = dataframe.DataFrame({"city": ["Seattle", "Berlin"]}) + countries = dataframe.DataFrame({"country": ["USA", "UK", "Germany"]}) + + with pytest.raises(ValueError): + cities.semantics.join(countries, "{city} is in {country}", None) From a70d683c909d5d0f9c31c190a6b7d4e0569f4a85 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:57:44 -0700 Subject: [PATCH 886/921] test: restore original udf in the remote function test, use supported type in the test data (#996) * test: fix test_df_apply_axis_1_complex by converting numpy value * undo all changes * improve numpy value handling in gcf code * enable the multiindex axis=1 test back * nit reword comment * Revert "improve numpy value handling in gcf code" This reverts commit f549683a2fe98b825f56fca81e9dba4523567bc0. --------- Co-authored-by: Shobhit Singh --- tests/system/large/test_remote_function.py | 42 ++++++++++++---------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/tests/system/large/test_remote_function.py b/tests/system/large/test_remote_function.py index 18d2609347e..2365002857e 100644 --- a/tests/system/large/test_remote_function.py +++ b/tests/system/large/test_remote_function.py @@ -1670,7 +1670,11 @@ def analyze(row): (3, 4): ["pq", "rs", "tu"], (5.0, "six", 7): [8, 9, 10], 'raise Exception("hacked!")': [11, 12, 13], - } + }, + # Default pandas index has non-numpy type, whereas bigframes is + # always numpy-based type, so let's use the index compatible + # with bigframes. See more details in b/369689696. + index=pandas.Index([0, 1, 2], dtype=pandas.Int64Dtype()), ), id="all-kinds-of-column-names", ), @@ -1681,17 +1685,22 @@ def analyze(row): "y": [1.5, 3.75, 5], "z": ["pq", "rs", "tu"], }, - index=pandas.MultiIndex.from_tuples( - [ - ("a", 100), - ("a", 200), - ("b", 300), - ] + index=pandas.MultiIndex.from_frame( + pandas.DataFrame( + { + "idx0": pandas.Series( + ["a", "a", "b"], dtype=pandas.StringDtype() + ), + "idx1": pandas.Series( + [100, 200, 300], dtype=pandas.Int64Dtype() + ), + } + ) ), ), id="multiindex", marks=pytest.mark.skip( - reason="TODO(b/368639580) revert this skip after fix" + reason="TODO: revert this skip after this pandas bug is fixed: https://github.com/pandas-dev/pandas/issues/59908" ), ), pytest.param( @@ -1701,6 +1710,10 @@ def analyze(row): [20, 3.75, "rs"], [30, 8.0, "tu"], ], + # Default pandas index has non-numpy type, whereas bigframes is + # always numpy-based type, so let's use the index compatible + # with bigframes. See more details in b/369689696. + index=pandas.Index([0, 1, 2], dtype=pandas.Int64Dtype()), columns=pandas.MultiIndex.from_arrays( [ ["first", "last_two", "last_two"], @@ -1729,10 +1742,8 @@ def test_df_apply_axis_1_complex(session, pd_df): def serialize_row(row): custom = { - "name": row.name.item() if hasattr(row.name, "item") else row.name, - "index": [ - idx.item() if hasattr(idx, "item") else idx for idx in row.index - ], + "name": row.name, + "index": [idx for idx in row.index], "values": [ val.item() if hasattr(val, "item") else val for val in row.values ], @@ -1756,12 +1767,7 @@ def serialize_row(row): bf_result = bf_df.apply(serialize_row_remote, axis=1).to_pandas() pd_result = pd_df.apply(serialize_row, axis=1) - # bf_result.dtype is 'string[pyarrow]' while pd_result.dtype is 'object' - # , ignore this mismatch by using check_dtype=False. - # - # bf_result.index[0].dtype is 'string[pyarrow]' while - # pd_result.index[0].dtype is 'object', ignore this mismatch by using - # check_index_type=False. + # ignore known dtype difference between pandas and bigframes pandas.testing.assert_series_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) From 1162b3b1cf1297dd3f9a216864ac33139ab2e302 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Mon, 7 Oct 2024 15:10:06 -0700 Subject: [PATCH 887/921] chore: implement semantic search (#1058) --- bigframes/operations/semantics.py | 96 ++++- .../experimental/semantic_operators.ipynb | 342 ++++++++++++++++-- tests/system/large/operations/conftest.py | 7 + .../system/large/operations/test_semantics.py | 62 ++++ 4 files changed, 481 insertions(+), 26 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index 1a4ff90b4ed..cb24633ed5f 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -15,7 +15,7 @@ import re import typing -from typing import List +from typing import List, Optional import bigframes @@ -279,6 +279,100 @@ def join(self, other, instruction: str, model, max_rows: int = 1000): return joined_df.semantics.filter(instruction, model).reset_index(drop=True) + def search( + self, + search_column: str, + query: str, + top_k: int, + model, + score_column: Optional[str] = None, + ): + """ + Performs semantic search on the DataFrame. + + ** Examples: ** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> import bigframes + >>> bigframes.options.experiments.semantic_operators = True + + >>> import bigframes.ml.llm as llm + >>> model = llm.TextEmbeddingGenerator(model_name="text-embedding-004") + + >>> df = bpd.DataFrame({"creatures": ["salmon", "sea urchin", "frog", "chimpanzee"]}) + >>> df.semantics.search("creatures", "monkey", top_k=1, model=model, score_column='distance') + creatures distance + 3 chimpanzee 0.781101 + + [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") + + import bigframes.ml.llm as llm + + if not isinstance(model, llm.TextEmbeddingGenerator): + raise TypeError(f"Expect a text embedding model, but got: {type(model)}") + + 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 _validate_model(model): from bigframes.ml.llm import GeminiTextGenerator diff --git a/notebooks/experimental/semantic_operators.ipynb b/notebooks/experimental/semantic_operators.ipynb index 80ed1a35a51..96813159b21 100644 --- a/notebooks/experimental/semantic_operators.ipynb +++ b/notebooks/experimental/semantic_operators.ipynb @@ -65,7 +65,7 @@ { "data": { "text/html": [ - "Query job b113bf05-6b85-41bc-8754-aebab5d6c65e is DONE. 0 Bytes processed. Open Job" + "Query job 3ab1bb82-0634-4a31-8ce1-0bac7c06887d is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -77,7 +77,7 @@ ], "source": [ "import bigframes.ml.llm as llm\n", - "model = llm.GeminiTextGenerator(model_name=llm._GEMINI_1P5_FLASH_001_ENDPOINT)" + "gemini_model = llm.GeminiTextGenerator(model_name=llm._GEMINI_1P5_FLASH_001_ENDPOINT)" ] }, { @@ -95,7 +95,7 @@ { "data": { "text/html": [ - "Query job 105c8fa5-6c33-48c7-9526-25fc5ac0013d is DONE. 0 Bytes processed. Open Job" + "Query job a3652c48-9c00-438b-8443-ef11b8a4c81d is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -115,7 +115,7 @@ { "data": { "text/html": [ - "Query job cf8e6bdd-1845-412b-ac13-16f03e968262 is DONE. 4 Bytes processed. Open Job" + "Query job dd133a7f-22a5-45a1-bedb-65c2cebd74ec is DONE. 4 Bytes processed. Open Job" ], "text/plain": [ "" @@ -127,7 +127,7 @@ { "data": { "text/html": [ - "Query job 28b6a9e8-5742-421b-b79c-f2fa647ebd10 is DONE. 33 Bytes processed. Open Job" + "Query job d162b1a6-76e3-4c1c-9d06-7cc8792297e1 is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -139,7 +139,7 @@ { "data": { "text/html": [ - "Query job 3d3e530f-4dde-447e-939d-eaf3b072f75b is DONE. 33 Bytes processed. Open Job" + "Query job 3b8212fb-07ba-4e6f-afa9-db98becd59fe is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -198,7 +198,7 @@ ], "source": [ "df = bpd.DataFrame({'country': ['USA', 'Germany'], 'city': ['Seattle', 'Berlin']})\n", - "df.semantics.filter(\"{city} is the capital of {country}\", model)" + "df.semantics.filter(\"{city} is the capital of {country}\", gemini_model)" ] }, { @@ -227,7 +227,7 @@ { "data": { "text/html": [ - "Query job 25b719ed-c1ad-45d9-9926-0a3408aff33d is DONE. 0 Bytes processed. Open Job" + "Query job ee842f5d-3572-4283-8161-f7b7cee51b06 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -247,7 +247,7 @@ { "data": { "text/html": [ - "Query job 010de4ac-7e7d-4aa2-9e73-34e2db77f2c8 is DONE. 4 Bytes processed. Open Job" + "Query job c2f1829d-b0b5-4316-942d-ab6881ad09bb is DONE. 4 Bytes processed. Open Job" ], "text/plain": [ "" @@ -259,7 +259,7 @@ { "data": { "text/html": [ - "Query job e5688060-a489-44bb-91e1-0272ca2d836b is DONE. 34 Bytes processed. Open Job" + "Query job 066c9101-c069-450f-8189-810b474fa5f2 is DONE. 34 Bytes processed. Open Job" ], "text/plain": [ "" @@ -271,7 +271,7 @@ { "data": { "text/html": [ - "Query job 7eeea068-83dd-4451-a18d-ae06525097d0 is DONE. 93 Bytes processed. Open Job" + "Query job e0bb869a-e3ef-49c2-8a9e-7dd343b06cbb is DONE. 93 Bytes processed. Open Job" ], "text/plain": [ "" @@ -340,7 +340,7 @@ } ], "source": [ - "df.semantics.map(\"What is the food made from {ingredient_1} and {ingredient_2}? One word only.\", output_column=\"food\", model=model)" + "df.semantics.map(\"What is the food made from {ingredient_1} and {ingredient_2}? One word only.\", output_column=\"food\", model=gemini_model)" ] }, { @@ -368,7 +368,7 @@ { "data": { "text/html": [ - "Query job 5467ef2f-832a-44c0-a4f8-f3b92f495348 is DONE. 0 Bytes processed. Open Job" + "Query job 2857268d-a5f6-47bc-9819-eef64c89dd83 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -388,7 +388,7 @@ { "data": { "text/html": [ - "Query job f3123dc6-ec48-444f-a940-7b768cf6341a is DONE. 30 Bytes processed. Open Job" + "Query job ebea0da7-ed07-480b-84dd-2abbd1fb5258 is DONE. 30 Bytes processed. Open Job" ], "text/plain": [ "" @@ -400,7 +400,7 @@ { "data": { "text/html": [ - "Query job 71c00675-0639-4d75-b5d2-5fb150f1aa54 is DONE. 251 Bytes processed. Open Job" + "Query job 8971d7c6-11de-47ea-b6c9-6b59cd6b58d2 is DONE. 251 Bytes processed. Open Job" ], "text/plain": [ "" @@ -412,7 +412,7 @@ { "data": { "text/html": [ - "Query job f64ede6e-f4ed-4764-a82a-8fefce20dfca is DONE. 144 Bytes processed. Open Job" + "Query job 7f90c077-48a9-433f-9c6b-ef7bc775e8ec is DONE. 144 Bytes processed. Open Job" ], "text/plain": [ "" @@ -488,7 +488,7 @@ } ], "source": [ - "cities.semantics.join(continents, \"{city} is in {continent}\", model)" + "cities.semantics.join(continents, \"{city} is in {continent}\", gemini_model)" ] }, { @@ -500,7 +500,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -509,13 +509,13 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job ca4abfb9-6fc9-4d70-b948-d426ec85e83f is DONE. 0 Bytes processed. Open Job" + "Query job f8aaae82-3622-4ec1-8e42-65a1712f26e2 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -535,7 +535,7 @@ { "data": { "text/html": [ - "Query job 25e0cdb2-9cfd-4d5e-91ea-34116af5dd91 is DONE. 32 Bytes processed. Open Job" + "Query job 511dbbca-8514-4ba6-bcca-93fbe961a2c9 is DONE. 32 Bytes processed. Open Job" ], "text/plain": [ "" @@ -547,7 +547,7 @@ { "data": { "text/html": [ - "Query job 9b93cd37-51f2-4908-91e3-369380d43028 is DONE. 266 Bytes processed. Open Job" + "Query job 2cca3eee-db60-4faa-9f26-6656002f0200 is DONE. 266 Bytes processed. Open Job" ], "text/plain": [ "" @@ -559,7 +559,7 @@ { "data": { "text/html": [ - "Query job 552be84f-e6e7-46b3-ab74-3e8261ccb396 is DONE. 180 Bytes processed. Open Job" + "Query job 91385afd-1107-4162-973b-3a8da98541ee is DONE. 180 Bytes processed. Open Job" ], "text/plain": [ "" @@ -641,13 +641,305 @@ "[6 rows x 2 columns]" ] }, - "execution_count": 14, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "animals.semantics.join(animals, \"{animal_left} generally weighs heavier than {animal_right}\", gemini_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Search" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 48aafee2-4948-4677-ab02-a94a71b9f6e2 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "text_embedding_model = llm.TextEmbeddingGenerator(model_name=\"text-embedding-004\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job b03ec7b9-dfd6-4c0c-9eb6-484a999c913e is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
creatures
0salmon
1sea urchin
2baboons
3frog
4chimpanzee
\n", + "

5 rows × 1 columns

\n", + "
[5 rows x 1 columns in total]" + ], + "text/plain": [ + " creatures\n", + "0 salmon\n", + "1 sea urchin\n", + "2 baboons\n", + "3 frog\n", + "4 chimpanzee\n", + "\n", + "[5 rows x 1 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({\"creatures\": [\"salmon\", \"sea urchin\", \"baboons\", \"frog\", \"chimpanzee\"]})\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 93fbc66e-4f98-4cdb-bb3a-5af738d6fd67 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 34a9725a-058a-4930-bc78-8372355d0c85 is DONE. 10 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ed533856-32fe-4012-8d72-ffc73c41418c is DONE. 30.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 91f67984-652b-44d4-a69a-91c3e1a3d2b1 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job f866ce11-f832-4c25-b3a7-1dce683b7553 is DONE. 2 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 41e22a53-6ef8-44c5-9cdd-01d99fc1c2fc is RUNNING. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 57f809b4-7e2a-46ba-992d-d33b64b92664 is DONE. 37.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d512a274-0c54-4c53-a92e-20b111477697 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
creaturessimilarity score
2baboons0.773411
4chimpanzee0.781101
\n", + "

2 rows × 2 columns

\n", + "
[2 rows x 2 columns in total]" + ], + "text/plain": [ + " creatures similarity score\n", + "2 baboons 0.773411\n", + "4 chimpanzee 0.781101\n", + "\n", + "[2 rows x 2 columns]" + ] + }, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "animals.semantics.join(animals, \"{animal_left} generally weighs heavier than {animal_right}\", model)" + "df.semantics.search(\"creatures\", \"monkey\", top_k = 2, model = text_embedding_model, score_column='similarity score')" ] } ], diff --git a/tests/system/large/operations/conftest.py b/tests/system/large/operations/conftest.py index a85df4c5df3..7ab3811f104 100644 --- a/tests/system/large/operations/conftest.py +++ b/tests/system/large/operations/conftest.py @@ -24,3 +24,10 @@ def gemini_flash_model(session, bq_connection) -> llm.GeminiTextGenerator: connection_name=bq_connection, model_name="gemini-1.5-flash-001", ) + + +@pytest.fixture(scope="session") +def text_embedding_generator(session, bq_connection) -> llm.TextEmbeddingGenerator: + return llm.TextEmbeddingGenerator( + session=session, connection_name=bq_connection, model_name="text-embedding-004" + ) diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py index a2c73b5d269..f0e13640e0c 100644 --- a/tests/system/large/operations/test_semantics.py +++ b/tests/system/large/operations/test_semantics.py @@ -302,3 +302,65 @@ def test_join_invalid_model_raise_error(): with pytest.raises(ValueError): cities.semantics.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): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + actual_result = df.semantics.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 + + +def test_search_invalid_column_raises_error(session, text_embedding_generator): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + with pytest.raises(ValueError): + df.semantics.search( + "whatever", "monkey", top_k=2, model=text_embedding_generator + ) + + +def test_search_invalid_model_raises_error(session): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + with pytest.raises(TypeError): + df.semantics.search("creatures", "monkey", top_k=2, model=None) From 02c2da733b834b99d8044f3c5cac3ac9a85802a6 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 7 Oct 2024 16:53:27 -0700 Subject: [PATCH 888/921] fix: show warning for unknown location set through .ctor (#1052) * fix: show warning for unknown location set through .ctor * update expected location in bigframes session --- bigframes/_config/bigquery_options.py | 2 +- tests/system/large/test_location.py | 2 +- tests/unit/_config/test_bigquery_options.py | 50 ++++++++++++++------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index 42007a388ea..afb0f00a27f 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -94,7 +94,7 @@ def __init__( ): self._credentials = credentials self._project = project - self._location = location + self._location = _get_validated_location(location) self._bq_connection = bq_connection self._use_regional_endpoints = use_regional_endpoints self._application_name = application_name diff --git a/tests/system/large/test_location.py b/tests/system/large/test_location.py index 2ef002d7e05..3521e4cd20e 100644 --- a/tests/system/large/test_location.py +++ b/tests/system/large/test_location.py @@ -101,7 +101,7 @@ def test_bq_location_non_canonical(set_location, resolved_location): context=bigframes.BigQueryOptions(location=set_location) ) - assert session.bqclient.location == set_location + assert session.bqclient.location == resolved_location # by default global endpoint is used assert ( diff --git a/tests/unit/_config/test_bigquery_options.py b/tests/unit/_config/test_bigquery_options.py index f40c140a9e9..d04b5bd575f 100644 --- a/tests/unit/_config/test_bigquery_options.py +++ b/tests/unit/_config/test_bigquery_options.py @@ -97,17 +97,26 @@ def test_setter_if_session_started_but_setting_the_same_value(attribute): ], ) def test_location_set_to_valid_no_warning(valid_location): - options = bigquery_options.BigQueryOptions() - # Ensure that no warnings are emitted. - # https://docs.pytest.org/en/7.0.x/how-to/capture-warnings.html#additional-use-cases-of-warnings-in-tests - with warnings.catch_warnings(): - # Turn matching UnknownLocationWarning into exceptions. - # https://docs.python.org/3/library/warnings.html#warning-filter - warnings.simplefilter( - "error", category=bigframes.exceptions.UnknownLocationWarning - ) + # test setting location through constructor + def set_location_in_ctor(): + bigquery_options.BigQueryOptions(location=valid_location) + + # test setting location property + def set_location_property(): + options = bigquery_options.BigQueryOptions() options.location = valid_location + for op in [set_location_in_ctor, set_location_property]: + # Ensure that no warnings are emitted. + # https://docs.pytest.org/en/7.0.x/how-to/capture-warnings.html#additional-use-cases-of-warnings-in-tests + with warnings.catch_warnings(): + # Turn matching UnknownLocationWarning into exceptions. + # https://docs.python.org/3/library/warnings.html#warning-filter + warnings.simplefilter( + "error", category=bigframes.exceptions.UnknownLocationWarning + ) + op() + @pytest.mark.parametrize( [ @@ -126,11 +135,20 @@ def test_location_set_to_valid_no_warning(valid_location): ], ) def test_location_set_to_invalid_warning(invalid_location, possibility): - options = bigquery_options.BigQueryOptions() - with pytest.warns( - bigframes.exceptions.UnknownLocationWarning, - match=re.escape( - f"The location '{invalid_location}' is set to an unknown value. Did you mean '{possibility}'?" - ), - ): + # test setting location through constructor + def set_location_in_ctor(): + bigquery_options.BigQueryOptions(location=invalid_location) + + # test setting location property + def set_location_property(): + options = bigquery_options.BigQueryOptions() options.location = invalid_location + + for op in [set_location_in_ctor, set_location_property]: + with pytest.warns( + bigframes.exceptions.UnknownLocationWarning, + match=re.escape( + f"The location '{invalid_location}' is set to an unknown value. Did you mean '{possibility}'?" + ), + ): + op() From 650d80d1ad90927068cdb71efbfc548b416641a6 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:34:28 -0700 Subject: [PATCH 889/921] feat: update LLM generators to warn user about model name instead of raising error. (#1048) * feat: update LLM generators to warn user about model name instead of raise error. * update message and format * update message and format --- bigframes/ml/llm.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index c12da01b54c..3920da6c714 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -83,6 +83,13 @@ _ML_EMBED_TEXT_STATUS = "ml_embed_text_status" _ML_GENERATE_EMBEDDING_STATUS = "ml_generate_embedding_status" +_MODEL_NOT_SUPPORTED_WARNING = ( + "Model name '{model_name}' is not supported. " + "We are currently aware of the following models: {known_models}. " + "However, model names can change, and the supported models may be outdated. " + "You should use this model name only if you are sure that it is supported in BigQuery." +) + @typing_extensions.deprecated( "PaLM2TextGenerator is going to be deprecated. Use GeminiTextGenerator(https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.GeminiTextGenerator) instead. ", @@ -154,8 +161,11 @@ def _create_bqml_model(self): ) if self.model_name not in _TEXT_GENERATOR_ENDPOINTS: - raise ValueError( - f"Model name {self.model_name} is not supported. We only support {', '.join(_TEXT_GENERATOR_ENDPOINTS)}." + warnings.warn( + _MODEL_NOT_SUPPORTED_WARNING.format( + model_name=self.model_name, + known_models=", ".join(_TEXT_GENERATOR_ENDPOINTS), + ) ) options = { @@ -484,8 +494,11 @@ def _create_bqml_model(self): ) if self.model_name not in _PALM2_EMBEDDING_GENERATOR_ENDPOINTS: - raise ValueError( - f"Model name {self.model_name} is not supported. We only support {', '.join(_PALM2_EMBEDDING_GENERATOR_ENDPOINTS)}." + warnings.warn( + _MODEL_NOT_SUPPORTED_WARNING.format( + model_name=self.model_name, + known_models=", ".join(_PALM2_EMBEDDING_GENERATOR_ENDPOINTS), + ) ) endpoint = ( @@ -644,8 +657,11 @@ def _create_bqml_model(self): ) if self.model_name not in _TEXT_EMBEDDING_ENDPOINTS: - raise ValueError( - f"Model name {self.model_name} is not supported. We only support {', '.join(_TEXT_EMBEDDING_ENDPOINTS)}." + warnings.warn( + _MODEL_NOT_SUPPORTED_WARNING.format( + model_name=self.model_name, + known_models=", ".join(_TEXT_EMBEDDING_ENDPOINTS), + ) ) options = { @@ -801,8 +817,11 @@ def _create_bqml_model(self): ) if self.model_name not in _GEMINI_ENDPOINTS: - raise ValueError( - f"Model name {self.model_name} is not supported. We only support {', '.join(_GEMINI_ENDPOINTS)}." + warnings.warn( + _MODEL_NOT_SUPPORTED_WARNING.format( + model_name=self.model_name, + known_models=", ".join(_GEMINI_ENDPOINTS), + ) ) options = {"endpoint": self.model_name} @@ -1118,8 +1137,11 @@ def _create_bqml_model(self): ) if self.model_name not in _CLAUDE_3_ENDPOINTS: - raise ValueError( - f"Model name {self.model_name} is not supported. We only support {', '.join(_CLAUDE_3_ENDPOINTS)}." + warnings.warn( + _MODEL_NOT_SUPPORTED_WARNING.format( + model_name=self.model_name, + known_models=", ".join(_CLAUDE_3_ENDPOINTS), + ) ) options = { From b53607015abb79be0aa5666681f1c53b5b1bc2b5 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:14:01 -0700 Subject: [PATCH 890/921] fix: correct zero row count in DataFrame from table view (#1062) * fix: correct zero row count display in DataFrame from table view * update logic and test --- bigframes/core/nodes.py | 4 +++- tests/system/small/test_dataframe.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index e65040686e5..d750ee63fbe 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -508,6 +508,7 @@ class GbqTable: table_id: str = field() physical_schema: Tuple[bq.SchemaField, ...] = field() n_rows: int = field() + is_physical_table: bool = field() cluster_cols: typing.Optional[Tuple[str, ...]] @staticmethod @@ -523,6 +524,7 @@ def from_table(table: bq.Table, columns: Sequence[str] = ()) -> GbqTable: table_id=table.table_id, physical_schema=schema, n_rows=table.num_rows, + is_physical_table=(table.table_type == "TABLE"), cluster_cols=None if table.clustering_fields is None else tuple(table.clustering_fields), @@ -603,7 +605,7 @@ def variables_introduced(self) -> int: @property def row_count(self) -> typing.Optional[int]: - if self.source.sql_predicate is None: + if self.source.sql_predicate is None and self.source.table.is_physical_table: return self.source.table.n_rows return None diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index cfd6efe9bd3..6ee9fb82472 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1521,6 +1521,26 @@ def test_shape(scalars_dfs): assert bf_result == pd_result +@pytest.mark.parametrize( + "reference_table, test_table", + [ + ( + "bigframes-dev.bigframes_tests_sys.base_table", + "bigframes-dev.bigframes_tests_sys.base_table_view", + ), + ( + "bigframes-dev.bigframes_tests_sys.csv_native_table", + "bigframes-dev.bigframes_tests_sys.csv_external_table", + ), + ], +) +def test_view_and_external_table_shape(session, reference_table, test_table): + reference_df = session.read_gbq(reference_table) + test_df = session.read_gbq(test_table) + + assert test_df.shape == reference_df.shape + + def test_len(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = len(scalars_df) From 575a10a7ba0fbac76867f02da1dd65355f00d7aa Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Mon, 7 Oct 2024 21:38:01 -0700 Subject: [PATCH 891/921] fix: remove palm2 test case from llm load test (#1063) --- tests/system/load/test_llm.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/system/load/test_llm.py b/tests/system/load/test_llm.py index 51b45485ade..4b0f50973b3 100644 --- a/tests/system/load/test_llm.py +++ b/tests/system/load/test_llm.py @@ -38,30 +38,6 @@ def llm_remote_text_df(session, llm_remote_text_pandas_df): return session.read_pandas(llm_remote_text_pandas_df) -@pytest.mark.flaky(retries=2) -def test_llm_palm_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): - model = llm.PaLM2TextGenerator(model_name="text-bison", max_iterations=1) - - X_train = llm_fine_tune_df_default_index[["prompt"]] - y_train = llm_fine_tune_df_default_index[["label"]] - model.fit(X_train, y_train) - - assert model is not None - - df = model.predict(llm_remote_text_df["prompt"]).to_pandas() - utils.check_pandas_df_schema_and_index( - df, - columns=[ - "ml_generate_text_llm_result", - "ml_generate_text_rai_result", - "ml_generate_text_status", - "prompt", - ], - index=3, - ) - # TODO(ashleyxu b/335492787): After bqml rolled out version control: save, load, check parameters to ensure configuration was kept - - @pytest.mark.flaky(retries=2) def test_llm_gemini_configure_fit(llm_fine_tune_df_default_index, llm_remote_text_df): model = llm.GeminiTextGenerator(model_name="gemini-pro", max_iterations=1) From d1b87e205d1d7c290ca2d499da1c03fe321c89f7 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:39:30 -0700 Subject: [PATCH 892/921] chore: Implement Semantics agg (#1059) * chore: Implement Semantics agg * fix tests * address comments --- bigframes/operations/semantics.py | 268 +++++++++++++++--- .../experimental/semantic_operators.ipynb | 221 ++++++++++++++- .../system/large/operations/test_semantics.py | 100 +++++++ 3 files changed, 540 insertions(+), 49 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index cb24633ed5f..0df7041914c 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -18,6 +18,8 @@ from typing import List, Optional import bigframes +import bigframes.core.guid +import bigframes.dtypes as dtypes class Semantics: @@ -27,6 +29,171 @@ def __init__(self, df) -> None: self._df = df + def agg( + self, + instruction: str, + model, + cluster_column: typing.Optional[str] = None, + max_agg_rows: int = 10, + ): + """ + Performs an aggregation over all rows of the table. + + This method recursively aggregates the input data to produce partial answers + in parallel, until a single answer remains. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> bpd.options.experiments.semantic_operators = True + + >>> import bigframes.ml.llm as llm + >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") + + >>> df = bpd.DataFrame( + ... { + ... "Movies": [ + ... "Titanic", + ... "The Wolf of Wall Street", + ... "Inception", + ... ], + ... "Year": [1997, 2013, 2010], + ... }) + >>> df.semantics.agg( + ... "Find the first name shared by all actors in {Movies}. One word answer.", + ... model=model, + ... ) + 0 Leonardo + + Name: Movies, dtype: string + + 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 "movies", use "{movies}" in the + instruction, like: "Find actor names shared by all {movies}." + + model (bigframes.ml.llm.GeminiTextGenerator): + A GeminiTextGenerator provided by the Bigframes ML package. + + cluster_column (Optional[str], default None): + If set, aggregates each cluster before performing aggregations across + clusters. Clustering based on semantic similarity can improve accuracy + of the sementic aggregations. + + max_agg_rows (int, default 10): + The maxinum number of rows to be aggregated at a time. + + Returns: + bigframes.dataframe.DataFrame: A new DataFrame with the aggregated answers. + + Raises: + NotImplementedError: when the semantic operator experiment is off. + ValueError: when the instruction refers to a non-existing column, or when + more than one columns are referred to. + """ + 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( + "Semantic aggregations are limited to a single column." + ) + column = columns[0] + + if max_agg_rows <= 1: + raise ValueError( + f"Invalid value for `max_agg_rows`: {max_agg_rows}." + "It must be greater than 1." + ) + + import bigframes.bigquery as bbq + import bigframes.dataframe + import bigframes.series + + df: bigframes.dataframe.DataFrame = self._df.copy() + user_instruction = self._format_instruction(instruction, columns) + + num_cluster = 1 + if cluster_column is not None: + if cluster_column not in df.columns: + raise ValueError(f"Cluster column `{cluster_column}` not found.") + + if df[cluster_column].dtype != dtypes.INT_DTYPE: + raise TypeError( + "Cluster column must be an integer type, not " + f"{type(df[cluster_column])}" + ) + + num_cluster = len(df[cluster_column].unique()) + df = df.sort_values(cluster_column) + else: + cluster_column = bigframes.core.guid.generate_guid("pid") + df[cluster_column] = 0 + + aggregation_group_id = bigframes.core.guid.generate_guid("agg") + group_row_index = bigframes.core.guid.generate_guid("gid") + llm_prompt = bigframes.core.guid.generate_guid("prompt") + df = ( + df.reset_index(drop=True) + .reset_index() + .rename(columns={"index": aggregation_group_id}) + ) + + output_instruction = ( + "Answer user instructions using the provided context from various sources. " + "Combine all relevant information into a single, concise, well-structured response. " + f"Instruction: {user_instruction}.\n\n" + ) + + while len(df) > 1: + df[group_row_index] = (df[aggregation_group_id] % max_agg_rows + 1).astype( + dtypes.STRING_DTYPE + ) + df[aggregation_group_id] = (df[aggregation_group_id] / max_agg_rows).astype( + dtypes.INT_DTYPE + ) + df[llm_prompt] = "\t\nSource #" + df[group_row_index] + ": " + df[column] + + if len(df) > num_cluster: + # Aggregate within each partition + agg_df = bbq.array_agg( + df.groupby(by=[cluster_column, aggregation_group_id]) + ) + else: + # Aggregate cross partitions + agg_df = bbq.array_agg(df.groupby(by=[aggregation_group_id])) + agg_df[cluster_column] = agg_df[cluster_column].list[0] + + # Skip if the aggregated group only has a single item + single_row_df: bigframes.series.Series = bbq.array_to_string( + agg_df[agg_df[group_row_index].list.len() <= 1][column], + delimiter="", + ) + prompt_s: bigframes.series.Series = bbq.array_to_string( + agg_df[agg_df[group_row_index].list.len() > 1][llm_prompt], + delimiter="", + ) + prompt_s = output_instruction + prompt_s # type:ignore + + # Run model + predict_df = typing.cast( + bigframes.dataframe.DataFrame, model.predict(prompt_s) + ) + agg_df[column] = predict_df["ml_generate_text_llm_result"].combine_first( + single_row_df + ) + + agg_df = agg_df.reset_index() + df = agg_df[[aggregation_group_id, cluster_column, column]] + + return df[column] + def filter(self, instruction: str, model): """ Filters the DataFrame with the semantics of the user instruction. @@ -35,9 +202,7 @@ def filter(self, instruction: str, model): >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None - - >>> import bigframes - >>> bigframes.options.experiments.semantic_operators = True + >>> bpd.options.experiments.semantic_operators = True >>> import bigframes.ml.llm as llm >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") @@ -68,14 +233,22 @@ def filter(self, instruction: str, model): ValueError: when the instruction refers to a non-existing column, or when no columns are referred to. """ - _validate_model(model) + 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.") + user_instruction = self._format_instruction(instruction, columns) output_instruction = "Based on the provided context, reply to the following claim by only True or False:" from bigframes.dataframe import DataFrame results = typing.cast( - DataFrame, model.predict(self._make_prompt(instruction, output_instruction)) + DataFrame, + model.predict( + self._make_prompt(columns, user_instruction, output_instruction) + ), ) return self._df[ @@ -90,9 +263,7 @@ def map(self, instruction: str, output_column: str, model): >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None - - >>> import bigframes - >>> bigframes.options.experiments.semantic_operators = True + >>> bpd.options.experiments.semantic_operators = True >>> import bigframes.ml.llm as llm >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") @@ -129,8 +300,13 @@ def map(self, instruction: str, output_column: str, model): ValueError: when the instruction refers to a non-existing column, or when no columns are referred to. """ - _validate_model(model) + 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.") + user_instruction = self._format_instruction(instruction, columns) output_instruction = ( "Based on the provided contenxt, answer the following instruction:" ) @@ -139,34 +315,15 @@ def map(self, instruction: str, output_column: str, model): results = typing.cast( Series, - model.predict(self._make_prompt(instruction, output_instruction))[ - "ml_generate_text_llm_result" - ], + model.predict( + self._make_prompt(columns, user_instruction, output_instruction) + )["ml_generate_text_llm_result"], ) from bigframes.core.reshape import concat return concat([self._df, results.rename(output_column)], axis=1) - def _make_prompt(self, user_instruction: str, output_instruction: str): - columns = _parse_columns(user_instruction) - - for column in columns: - if column not in self._df.columns: - raise ValueError(f"Column {column} not found.") - - # Replace column references with names. - user_instruction = user_instruction.format(**{col: col for col in columns}) - - prompt_df = self._df[columns].copy() - 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"] - def join(self, other, instruction: str, model, max_rows: int = 1000): """ Joines two dataframes by applying the instruction over each pair of rows from @@ -176,9 +333,7 @@ def join(self, other, instruction: str, model, max_rows: int = 1000): >>> import bigframes.pandas as bpd >>> bpd.options.display.progress_bar = None - - >>> import bigframes - >>> bigframes.options.experiments.semantic_operators = True + >>> bpd.options.experiments.semantic_operators = True >>> import bigframes.ml.llm as llm >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") @@ -221,7 +376,8 @@ def join(self, other, instruction: str, model, max_rows: int = 1000): Raises: ValueError if the amount of data that will be sent for LLM processing is larger than max_rows. """ - _validate_model(model) + self._validate_model(model) + columns = self._parse_columns(instruction) joined_table_rows = len(self._df) * len(other) @@ -230,8 +386,6 @@ def join(self, other, instruction: str, model, max_rows: int = 1000): f"Number of rows that need processing is {joined_table_rows}, which exceeds row limit {max_rows}." ) - columns = _parse_columns(instruction) - left_columns = [] right_columns = [] @@ -373,18 +527,40 @@ def search( return typing.cast(bigframes.dataframe.DataFrame, search_result) + def _make_prompt( + self, columns: List[str], user_instruction: str, output_instruction: str + ): + prompt_df = self._df[columns].copy() + 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"] -def _validate_model(model): - from bigframes.ml.llm import GeminiTextGenerator + def _parse_columns(self, 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"(? List[str]: - 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}) - if not columns: - raise ValueError("No column references") + @staticmethod + def _validate_model(model): + from bigframes.ml.llm import GeminiTextGenerator - return columns + if not isinstance(model, GeminiTextGenerator): + raise ValueError("Model is not GeminiText Generator") diff --git a/notebooks/experimental/semantic_operators.ipynb b/notebooks/experimental/semantic_operators.ipynb index 96813159b21..4273ceb36ba 100644 --- a/notebooks/experimental/semantic_operators.ipynb +++ b/notebooks/experimental/semantic_operators.ipynb @@ -33,7 +33,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", + "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", " warnings.warn(\n" ] } @@ -58,14 +58,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", + "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", " return global_session.get_global_session()\n" ] }, { "data": { "text/html": [ - "Query job 3ab1bb82-0634-4a31-8ce1-0bac7c06887d is DONE. 0 Bytes processed. Open Job" + "Query job aef2dd7b-bdad-4dda-91be-867e8dac2613 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -941,6 +941,221 @@ "source": [ "df.semantics.search(\"creatures\", \"monkey\", top_k = 2, model = text_embedding_model, score_column='similarity score')" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Aggregation" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 9922a236-2597-48d3-9188-d859d31042e4 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MoviesYear
0Titanic1997
1The Wolf of Wall Street2013
2Killers of the Flower Moon2023
3The Revenant2015
4Inception2010
5Shuttle Island2010
6The Great Gatsby2013
\n", + "

7 rows × 2 columns

\n", + "
[7 rows x 2 columns in total]" + ], + "text/plain": [ + " Movies Year\n", + "0 Titanic 1997\n", + "1 The Wolf of Wall Street 2013\n", + "2 Killers of the Flower Moon 2023\n", + "3 The Revenant 2015\n", + "4 Inception 2010\n", + "5 Shuttle Island 2010\n", + "6 The Great Gatsby 2013\n", + "\n", + "[7 rows x 2 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({\n", + " \"Movies\": [\n", + " \"Titanic\",\n", + " \"The Wolf of Wall Street\",\n", + " \"Killers of the Flower Moon\",\n", + " \"The Revenant\",\n", + " \"Inception\",\n", + " \"Shuttle Island\",\n", + " \"The Great Gatsby\",\n", + " ],\n", + " \"Year\": [1997, 2013, 2023, 2015, 2010, 2010, 2013],\n", + "})\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 2b50b8d0-8cf6-4930-8219-2a25ca2b0285 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job e57370cb-1cb7-4ceb-b084-ac45dd0149dd is DONE. 2 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7b6ab8a2-37f8-4206-a5c4-da84d45d94b2 is DONE. 16 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job e1b92cd9-cdda-4d1e-8fb4-5b83183eb26b is DONE. 28 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 643a742a-5cbf-4c3d-bd94-dd2211cb685c is DONE. 28 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 Leonardo \n", + "\n", + "Name: Movies, dtype: string" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agg_df = df.semantics.agg(\"Find the shared first name of actors in {Movies}. One word answer.\", model=gemini_model)\n", + "agg_df" + ] } ], "metadata": { diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py index f0e13640e0c..f66f2bc029c 100644 --- a/tests/system/large/operations/test_semantics.py +++ b/tests/system/large/operations/test_semantics.py @@ -18,6 +18,7 @@ import bigframes import bigframes.dataframe as dataframe +import bigframes.dtypes as dtypes def test_semantics_experiment_off_raise_error(): @@ -30,6 +31,105 @@ def test_semantics_experiment_off_raise_error(): df.semantics +@pytest.mark.parametrize( + ("max_agg_rows", "cluster_column"), + [ + pytest.param(1, None, id="one", marks=pytest.mark.xfail(raises=ValueError)), + pytest.param(2, None, id="two"), + pytest.param(3, None, id="three"), + pytest.param(4, None, id="four"), + pytest.param(5, "Year", id="two_w_cluster_column"), + pytest.param(6, "Year", id="three_w_cluster_column"), + pytest.param(7, "Year", id="four_w_cluster_column"), + ], +) +def test_agg_w_max_agg_rows(session, gemini_flash_model, max_agg_rows, cluster_column): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={ + "Movies": [ + "Titanic", + "The Wolf of Wall Street", + "Killers of the Flower Moon", + "The Revenant", + "Inception", + "Shuttle Island", + "The Great Gatsby", + ], + "Year": [1997, 2013, 2023, 2015, 2010, 2010, 2013], + }, + session=session, + ) + instruction = "Find the shared first name of actors in {Movies}. One word answer." + actual_s = df.semantics.agg( + instruction, + model=gemini_flash_model, + max_agg_rows=max_agg_rows, + cluster_column=cluster_column, + ).to_pandas() + + expected_s = pd.Series(["Leonardo \n"], dtype=dtypes.STRING_DTYPE) + expected_s.name = "Movies" + pandas.testing.assert_series_equal(actual_s, expected_s, 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( + "{city} is in the {country}", + id="two_columns", + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + ], +) +def test_agg_invalid_instruction_raise_error(instruction, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + df.semantics.agg(instruction, gemini_flash_model) + + +@pytest.mark.parametrize( + "cluster_column", + [ + pytest.param( + "non_existing_column", + id="non_existing_column", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "Movies", id="non_int_column", marks=pytest.mark.xfail(raises=TypeError) + ), + ], +) +def test_agg_invalid_cluster_column_raise_error(gemini_flash_model, cluster_column): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={ + "Movies": [ + "Titanic", + "The Wolf of Wall Street", + "Killers of the Flower Moon", + "The Revenant", + ], + }, + ) + instruction = "Find the shared first name of actors in {Movies}. One word answer." + df.semantics.agg(instruction, gemini_flash_model, cluster_column=cluster_column) + + def test_filter(session, gemini_flash_model): bigframes.options.experiments.semantic_operators = True df = dataframe.DataFrame( From 4379438fc4f44ea847fd2c00a82af544265a30d2 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 8 Oct 2024 12:05:21 -0700 Subject: [PATCH 893/921] perf: Speedup internal tree comparisons (#1060) --- bigframes/core/nodes.py | 117 +++++++++++++++------------------------- 1 file changed, 42 insertions(+), 75 deletions(-) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index d750ee63fbe..c152aabf297 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -51,8 +51,8 @@ class Field: dtype: bigframes.dtypes.Dtype -@dataclass(frozen=True) -class BigFrameNode: +@dataclass(eq=False, frozen=True) +class BigFrameNode(abc.ABC): """ Immutable node for representing 2D typed array as a tree of operators. @@ -95,12 +95,30 @@ def session(self): return sessions[0] return None + def _as_tuple(self) -> Tuple: + """Get all fields as tuple.""" + return tuple(getattr(self, field.name) for field in fields(self)) + + def __hash__(self) -> int: + # Custom hash that uses cache to avoid costly recomputation + return self._cached_hash + + def __eq__(self, other) -> bool: + # Custom eq that tries to short-circuit full structural comparison + if not isinstance(other, self.__class__): + return False + if self is other: + return True + if hash(self) != hash(other): + return False + return self._as_tuple() == other._as_tuple() + # BigFrameNode trees can be very deep so its important avoid recalculating the hash from scratch # Each subclass of BigFrameNode should use this property to implement __hash__ # The default dataclass-generated __hash__ method is not cached @functools.cached_property - def _node_hash(self): - return hash(tuple(hash(getattr(self, field.name)) for field in fields(self))) + def _cached_hash(self): + return hash(self._as_tuple()) @property def roots(self) -> typing.Set[BigFrameNode]: @@ -226,7 +244,7 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return self.transform_children(lambda x: x.prune(used_cols)) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class UnaryNode(BigFrameNode): child: BigFrameNode @@ -252,7 +270,7 @@ def order_ambiguous(self) -> bool: return self.child.order_ambiguous -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class JoinNode(BigFrameNode): left_child: BigFrameNode right_child: BigFrameNode @@ -285,9 +303,6 @@ def explicitly_ordered(self) -> bool: # Do not consider user pre-join ordering intent - they need to re-order post-join in unordered mode. return False - def __hash__(self): - return self._node_hash - @functools.cached_property def fields(self) -> Tuple[Field, ...]: return tuple(itertools.chain(self.left_child.fields, self.right_child.fields)) @@ -320,7 +335,7 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return self.transform_children(lambda x: x.prune(new_used)) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class ConcatNode(BigFrameNode): # TODO: Explcitly map column ids from each child children: Tuple[BigFrameNode, ...] @@ -345,9 +360,6 @@ def explicitly_ordered(self) -> bool: # Consider concat as an ordered operations (even though input frames may not be ordered) return True - def __hash__(self): - return self._node_hash - @functools.cached_property def fields(self) -> Tuple[Field, ...]: # TODO: Output names should probably be aligned beforehand or be part of concat definition @@ -371,16 +383,13 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return self -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class FromRangeNode(BigFrameNode): # TODO: Enforce single-row, single column constraint start: BigFrameNode end: BigFrameNode step: int - def __hash__(self): - return self._node_hash - @property def roots(self) -> typing.Set[BigFrameNode]: return {self} @@ -419,7 +428,7 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: # Input Nodex # TODO: Most leaf nodes produce fixed column names based on the datasource # They should support renaming -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class LeafNode(BigFrameNode): @property def roots(self) -> typing.Set[BigFrameNode]: @@ -451,7 +460,7 @@ class ScanList: items: typing.Tuple[ScanItem, ...] -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class ReadLocalNode(LeafNode): feather_bytes: bytes data_schema: schemata.ArraySchema @@ -460,9 +469,6 @@ class ReadLocalNode(LeafNode): scan_list: ScanList session: typing.Optional[bigframes.session.Session] = None - def __hash__(self): - return self._node_hash - @functools.cached_property def fields(self) -> Tuple[Field, ...]: return tuple(Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) @@ -547,7 +553,7 @@ class BigqueryDataSource: ## Put ordering in here or just add order_by node above? -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class ReadTableNode(LeafNode): source: BigqueryDataSource # Subset of physical schema column @@ -570,9 +576,6 @@ def __post_init__(self): def session(self): return self.table_session - def __hash__(self): - return self._node_hash - @functools.cached_property def fields(self) -> Tuple[Field, ...]: return tuple(Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) @@ -616,15 +619,12 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return ReadTableNode(self.source, new_scan_list, self.table_session) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class CachedTableNode(ReadTableNode): # The original BFET subtree that was cached # note: this isn't a "child" node. original_node: BigFrameNode = field() - def __hash__(self): - return self._node_hash - def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: new_scan_list = ScanList( tuple(item for item in self.scan_list.items if item.id in used_cols) @@ -635,13 +635,10 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: # Unary nodes -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class PromoteOffsetsNode(UnaryNode): col_id: bigframes.core.identifiers.ColumnId - def __hash__(self): - return self._node_hash - @property def non_local(self) -> bool: return True @@ -666,7 +663,7 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return self.transform_children(lambda x: x.prune(new_used)) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class FilterNode(UnaryNode): predicate: ex.Expression @@ -674,9 +671,6 @@ class FilterNode(UnaryNode): def row_preserving(self) -> bool: return False - def __hash__(self): - return self._node_hash - @property def variables_introduced(self) -> int: return 1 @@ -687,13 +681,10 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return FilterNode(pruned_child, self.predicate) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class OrderByNode(UnaryNode): by: Tuple[OrderingExpression, ...] - def __hash__(self): - return self._node_hash - @property def variables_introduced(self) -> int: return 0 @@ -716,14 +707,11 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return OrderByNode(pruned_child, self.by) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class ReversedNode(UnaryNode): # useless field to make sure has distinct hash reversed: bool = True - def __hash__(self): - return self._node_hash - @property def variables_introduced(self) -> int: return 0 @@ -734,15 +722,12 @@ def relation_ops_created(self) -> int: return 0 -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class SelectionNode(UnaryNode): input_output_pairs: typing.Tuple[ typing.Tuple[ex.DerefOp, bigframes.core.identifiers.ColumnId], ... ] - def __hash__(self): - return self._node_hash - @functools.cached_property def fields(self) -> Tuple[Field, ...]: return tuple( @@ -772,7 +757,7 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return SelectionNode(pruned_child, pruned_selections) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class ProjectionNode(UnaryNode): """Assigns new variables (without modifying existing ones)""" @@ -788,9 +773,6 @@ def __post_init__(self): # Cannot assign to existing variables - append only! assert all(name not in self.child.schema.names for _, name in self.assignments) - def __hash__(self): - return self._node_hash - @functools.cached_property def fields(self) -> Tuple[Field, ...]: input_types = self.child._dtype_lookup @@ -819,7 +801,7 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: # TODO: Merge RowCount into Aggregate Node? # Row count can be compute from table metadata sometimes, so it is a bit special. -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class RowCountNode(UnaryNode): @property def row_preserving(self) -> bool: @@ -842,7 +824,7 @@ def defines_namespace(self) -> bool: return True -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class AggregateNode(UnaryNode): aggregations: typing.Tuple[ typing.Tuple[ex.Aggregation, bigframes.core.identifiers.ColumnId], ... @@ -854,9 +836,6 @@ class AggregateNode(UnaryNode): def row_preserving(self) -> bool: return False - def __hash__(self): - return self._node_hash - @property def non_local(self) -> bool: return True @@ -904,7 +883,7 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return AggregateNode(pruned_child, pruned_aggs, self.by_column_ids, self.dropna) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class WindowOpNode(UnaryNode): column_name: ex.DerefOp op: agg_ops.UnaryWindowOp @@ -913,9 +892,6 @@ class WindowOpNode(UnaryNode): never_skip_nulls: bool = False skip_reproject_unsafe: bool = False - def __hash__(self): - return self._node_hash - @property def non_local(self) -> bool: return True @@ -945,11 +921,8 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: # TODO: Remove this op -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class ReprojectOpNode(UnaryNode): - def __hash__(self): - return self._node_hash - @property def variables_introduced(self) -> int: return 0 @@ -960,7 +933,7 @@ def relation_ops_created(self) -> int: return 0 -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class RandomSampleNode(UnaryNode): fraction: float @@ -972,16 +945,13 @@ def deterministic(self) -> bool: def row_preserving(self) -> bool: return False - def __hash__(self): - return self._node_hash - @property def variables_introduced(self) -> int: return 1 # TODO: Explode should create a new column instead of overriding the existing one -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class ExplodeNode(UnaryNode): column_ids: typing.Tuple[ex.DerefOp, ...] @@ -989,9 +959,6 @@ class ExplodeNode(UnaryNode): def row_preserving(self) -> bool: return False - def __hash__(self): - return self._node_hash - @functools.cached_property def fields(self) -> Tuple[Field, ...]: return tuple( From 1c3879df2d6925e17e2cdca827db8ec919471f72 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 8 Oct 2024 13:18:00 -0700 Subject: [PATCH 894/921] perf: Reduce schema tracking overhead (#1056) --- bigframes/core/nodes.py | 85 +++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index c152aabf297..2bb1d685389 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -127,10 +127,10 @@ def roots(self) -> typing.Set[BigFrameNode]: ) return set(roots) - # TODO: For deep trees, this can create a lot of overhead, maybe use zero-copy persistent datastructure? + # TODO: Store some local data lazily for select, aggregate nodes. @property @abc.abstractmethod - def fields(self) -> Tuple[Field, ...]: + def fields(self) -> Iterable[Field]: ... @property @@ -252,8 +252,8 @@ class UnaryNode(BigFrameNode): def child_nodes(self) -> typing.Sequence[BigFrameNode]: return (self.child,) - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: + @property + def fields(self) -> Iterable[Field]: return self.child.fields @property @@ -303,9 +303,9 @@ def explicitly_ordered(self) -> bool: # Do not consider user pre-join ordering intent - they need to re-order post-join in unordered mode. return False - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: - return tuple(itertools.chain(self.left_child.fields, self.right_child.fields)) + @property + def fields(self) -> Iterable[Field]: + return itertools.chain(self.left_child.fields, self.right_child.fields) @functools.cached_property def variables_introduced(self) -> int: @@ -360,10 +360,10 @@ def explicitly_ordered(self) -> bool: # Consider concat as an ordered operations (even though input frames may not be ordered) return True - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: + @property + def fields(self) -> Iterable[Field]: # TODO: Output names should probably be aligned beforehand or be part of concat definition - return tuple( + return ( Field(bfet_ids.ColumnId(f"column_{i}"), field.dtype) for i, field in enumerate(self.children[0].fields) ) @@ -407,8 +407,10 @@ def explicitly_ordered(self) -> bool: return True @functools.cached_property - def fields(self) -> Tuple[Field, ...]: - return (Field(bfet_ids.ColumnId("labels"), self.start.fields[0].dtype),) + def fields(self) -> Iterable[Field]: + return ( + Field(bfet_ids.ColumnId("labels"), next(iter(self.start.fields)).dtype), + ) @functools.cached_property def variables_introduced(self) -> int: @@ -469,11 +471,11 @@ class ReadLocalNode(LeafNode): scan_list: ScanList session: typing.Optional[bigframes.session.Session] = None - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: - return tuple(Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) + @property + def fields(self) -> Iterable[Field]: + return (Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) - @functools.cached_property + @property def variables_introduced(self) -> int: """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" return len(self.scan_list.items) + 1 @@ -576,9 +578,9 @@ def __post_init__(self): def session(self): return self.table_session - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: - return tuple(Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) + @property + def fields(self) -> Iterable[Field]: + return (Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) @property def relation_ops_created(self) -> int: @@ -644,8 +646,10 @@ def non_local(self) -> bool: return True @property - def fields(self) -> Tuple[Field, ...]: - return (*self.child.fields, Field(self.col_id, bigframes.dtypes.INT_DTYPE)) + def fields(self) -> Iterable[Field]: + return itertools.chain( + self.child.fields, [Field(self.col_id, bigframes.dtypes.INT_DTYPE)] + ) @property def relation_ops_created(self) -> int: @@ -729,7 +733,7 @@ class SelectionNode(UnaryNode): ] @functools.cached_property - def fields(self) -> Tuple[Field, ...]: + def fields(self) -> Iterable[Field]: return tuple( Field(output, self.child.get_type(input.id)) for input, output in self.input_output_pairs @@ -774,13 +778,16 @@ def __post_init__(self): assert all(name not in self.child.schema.names for _, name in self.assignments) @functools.cached_property - def fields(self) -> Tuple[Field, ...]: + def added_fields(self) -> Tuple[Field, ...]: input_types = self.child._dtype_lookup - new_fields = ( + return tuple( Field(id, bigframes.dtypes.dtype_for_etype(ex.output_type(input_types))) for ex, id in self.assignments ) - return (*self.child.fields, *new_fields) + + @property + def fields(self) -> Iterable[Field]: + return itertools.chain(self.child.fields, self.added_fields) @property def variables_introduced(self) -> int: @@ -811,8 +818,8 @@ def row_preserving(self) -> bool: def non_local(self) -> bool: return True - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: + @property + def fields(self) -> Iterable[Field]: return (Field(bfet_ids.ColumnId("count"), bigframes.dtypes.INT_DTYPE),) @property @@ -841,7 +848,7 @@ def non_local(self) -> bool: return True @functools.cached_property - def fields(self) -> Tuple[Field, ...]: + def fields(self) -> Iterable[Field]: by_items = ( Field(ref.id, self.child.get_type(ref.id)) for ref in self.by_column_ids ) @@ -854,7 +861,7 @@ def fields(self) -> Tuple[Field, ...]: ) for agg, id in self.aggregations ) - return (*by_items, *agg_items) + return tuple(itertools.chain(by_items, agg_items)) @property def variables_introduced(self) -> int: @@ -896,11 +903,9 @@ class WindowOpNode(UnaryNode): def non_local(self) -> bool: return True - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: - input_type = self.child.get_type(self.column_name.id) - new_item_dtype = self.op.output_type(input_type) - return (*self.child.fields, Field(self.output_name, new_item_dtype)) + @property + def fields(self) -> Iterable[Field]: + return itertools.chain(self.child.fields, [self.added_field]) @property def variables_introduced(self) -> int: @@ -911,6 +916,12 @@ def relation_ops_created(self) -> int: # Assume that if not reprojecting, that there is a sequence of window operations sharing the same window return 0 if self.skip_reproject_unsafe else 4 + @functools.cached_property + def added_field(self) -> Field: + input_type = self.child.get_type(self.column_name.id) + new_item_dtype = self.op.output_type(input_type) + return Field(self.output_name, new_item_dtype) + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: if self.output_name not in used_cols: return self.child @@ -959,9 +970,9 @@ class ExplodeNode(UnaryNode): def row_preserving(self) -> bool: return False - @functools.cached_property - def fields(self) -> Tuple[Field, ...]: - return tuple( + @property + def fields(self) -> Iterable[Field]: + return ( Field( field.id, bigframes.dtypes.arrow_dtype_to_bigframes_dtype( From f86483d485e295da36f8b679082ddfb184f9e0d7 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 8 Oct 2024 14:43:28 -0700 Subject: [PATCH 895/921] refactor: Remove reproject node (#1065) --- bigframes/core/__init__.py | 14 -------------- bigframes/core/block_transforms.py | 3 +-- bigframes/core/blocks.py | 9 --------- bigframes/core/compile/compiler.py | 4 ---- bigframes/core/nodes.py | 13 ------------- 5 files changed, 1 insertion(+), 42 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index acab99f2498..0c5d72f0782 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -394,20 +394,6 @@ def project_window_op( output_name, ) - def _reproject_to_table(self) -> ArrayValue: - """ - Internal operators that projects the internal representation into a - new ibis table expression where each value column is a direct - reference to a column in that table expression. Needed after - some operations such as window operations that cannot be used - recursively in projections. - """ - return ArrayValue( - nodes.ReprojectOpNode( - child=self.node, - ) - ) - def relational_join( self, other: ArrayValue, diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 2c4991b6290..785691edd6d 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -196,8 +196,7 @@ def interpolate(block: blocks.Block, method: str = "linear") -> blocks.Block: else: output_column_ids.append(column) - # Force reproject since used `skip_project_unsafe` perviously - block = block.select_columns(output_column_ids)._force_reproject() + block = block.select_columns(output_column_ids) return block.with_column_labels(original_labels) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 2b3734edd5d..a45f0ab505e 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -2433,15 +2433,6 @@ def join( # Always sort mult-index join return join_multi_indexed(self, other, how=how, sort=sort) - def _force_reproject(self) -> Block: - """Forces a reprojection of the underlying tables expression. Used to force predicate/order application before subsequent operations.""" - return Block( - self._expr._reproject_to_table(), - index_columns=self.index_columns, - column_labels=self.column_labels, - index_labels=self.index.names, - ) - def is_monotonic_increasing( self, column_id: typing.Union[str, Sequence[str]] ) -> bool: diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 0917097c708..fd1514d7b7b 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -335,10 +335,6 @@ def compile_window(self, node: nodes.WindowOpNode, ordered: bool = True): ) return result if ordered else result.to_unordered() - @_compile_node.register - def compile_reproject(self, node: nodes.ReprojectOpNode, ordered: bool = True): - return self.compile_node(node.child, ordered)._reproject_to_table() - @_compile_node.register def compile_explode(self, node: nodes.ExplodeNode, ordered: bool = True): return self.compile_node(node.child, ordered).explode(node.column_ids) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 2bb1d685389..3ed4558acba 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -931,19 +931,6 @@ def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: return self.transform_children(lambda x: x.prune(consumed_ids)) -# TODO: Remove this op -@dataclass(frozen=True, eq=False) -class ReprojectOpNode(UnaryNode): - @property - def variables_introduced(self) -> int: - return 0 - - @property - def relation_ops_created(self) -> int: - # This op is not a real transformation, just a hint to the sql generator - return 0 - - @dataclass(frozen=True, eq=False) class RandomSampleNode(UnaryNode): fraction: float From 45b672a9a6359ec8c4755d94e63e5ae77a39754b Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 8 Oct 2024 15:36:29 -0700 Subject: [PATCH 896/921] feat: support regional endpoints for more bigquery locations (#1061) * feat: Support bigquery regional endpoints for more locations * typo plural * correct the arg documentation --- bigframes/_config/bigquery_options.py | 27 +++++++++++++++++++-------- bigframes/constants.py | 5 +++-- bigframes/session/clients.py | 16 +++++----------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/bigframes/_config/bigquery_options.py b/bigframes/_config/bigquery_options.py index afb0f00a27f..2fdd7d6feba 100644 --- a/bigframes/_config/bigquery_options.py +++ b/bigframes/_config/bigquery_options.py @@ -233,18 +233,29 @@ def skip_bq_connection_check(self, value: bool): def use_regional_endpoints(self) -> bool: """Flag to connect to regional API endpoints. - .. deprecated:: 0.13.0 - Use of regional endpoints is a feature in Preview and - available only in selected regions and projects. + .. note:: + Use of regional endpoints is a feature in Preview and available only + in regions "europe-west3", "europe-west9", "europe-west8", + "me-central2", "us-east4" and "us-west1". - Requires that ``location`` is set. For example, to connect to - asia-northeast1-bigquery.googleapis.com, specify - ``location='asia-northeast1'`` and ``use_regional_endpoints=True``. + .. deprecated:: 0.13.0 + Use of locational endpoints is available only in selected projects. + + Requires that ``location`` is set. For supported regions, for example + ``europe-west3``, you need to specify ``location='europe-west3'`` and + ``use_regional_endpoints=True``, and then BigQuery DataFrames would + connect to the BigQuery endpoint ``bigquery.europe-west3.rep.googleapis.com``. + For not supported regions, for example ``asia-northeast1``, when you + specify ``location='asia-northeast1'`` and ``use_regional_endpoints=True``, + a different endpoint (called locational endpoint, now deprecated, used + to provide weaker promise on the request remaining within the location + during transit) ``europe-west3-bigquery.googleapis.com`` would be used. Returns: bool: - A boolean value, where True indicates that a location is set; - otherwise False. + A boolean value, where True indicates that regional endpoints + would be used for BigQuery and BigQuery storage APIs; otherwise + global endpoints would be used. """ return self._use_regional_endpoints diff --git a/bigframes/constants.py b/bigframes/constants.py index e0c83050797..13636a44849 100644 --- a/bigframes/constants.py +++ b/bigframes/constants.py @@ -80,9 +80,10 @@ # https://cloud.google.com/storage/docs/regional-endpoints REP_ENABLED_BIGQUERY_LOCATIONS = frozenset( { - "me-central2", - "europe-west9", "europe-west3", + "europe-west9", + "europe-west8", + "me-central2", "us-east4", "us-west1", } diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index 7b53d40f74a..04cd1a2ff0f 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -31,21 +31,13 @@ import ibis import pydata_google_auth +import bigframes.constants import bigframes.version _ENV_DEFAULT_PROJECT = "GOOGLE_CLOUD_PROJECT" _APPLICATION_NAME = f"bigframes/{bigframes.version.__version__} ibis/{ibis.__version__}" _SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] -# Regions for which Regional Endpoints (REPs) are supported -_REP_SUPPORTED_REGIONS = { - "me-central2", - "europe-west9", - "europe-west3", - "us-east4", - "us-west1", -} - # BigQuery is a REST API, which requires the protocol as part of the URL. _BIGQUERY_LOCATIONAL_ENDPOINT = "https://{location}-bigquery.googleapis.com" @@ -129,7 +121,8 @@ def _create_bigquery_client(self): api_endpoint=( _BIGQUERY_REGIONAL_ENDPOINT if self._location is not None - and self._location.lower() in _REP_SUPPORTED_REGIONS + and self._location.lower() + in bigframes.constants.REP_ENABLED_BIGQUERY_LOCATIONS else _BIGQUERY_LOCATIONAL_ENDPOINT ).format(location=self._location), ) @@ -201,7 +194,8 @@ def bqstoragereadclient(self): api_endpoint=( _BIGQUERYSTORAGE_REGIONAL_ENDPOINT if self._location is not None - and self._location.lower() in _REP_SUPPORTED_REGIONS + and self._location.lower() + in bigframes.constants.REP_ENABLED_BIGQUERY_LOCATIONS else _BIGQUERYSTORAGE_LOCATIONAL_ENDPOINT ).format(location=self._location), ) From 99ca0df90acbbd81197c9b6718b7de7e4dfb86cc Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 8 Oct 2024 16:18:35 -0700 Subject: [PATCH 897/921] fix: make `explode` respect the index labels (#1064) --- bigframes/core/blocks.py | 2 +- tests/system/small/test_multiindex.py | 20 +++++++++++++++++++- tests/system/small/test_series.py | 22 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index a45f0ab505e..6e5cffcc3a1 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1390,7 +1390,7 @@ def explode( expr, column_labels=self.column_labels, index_columns=self.index_columns, - index_labels=self.column_labels.names, + index_labels=self._index_labels, ) def _standard_stats(self, column_id) -> typing.Sequence[agg_ops.UnaryAggregateOp]: diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index ab2a9c19b8d..cab74f617d0 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -1178,7 +1178,7 @@ def test_column_multi_index_dot_not_supported(): bf1 @ bf2 -def test_explode_w_multi_index(): +def test_explode_w_column_multi_index(): data = [[[1, 1], np.nan, [3, 3]], [[2], [5], []]] multi_level_columns = pandas.MultiIndex.from_arrays( [["col0", "col0", "col1"], ["col00", "col01", "col11"]] @@ -1197,6 +1197,24 @@ def test_explode_w_multi_index(): ) +def test_explode_w_multi_index(): + data = [[[1, 1], np.nan, [3, 3]], [[2], [5], []]] + columns = ["col00", "col01", "col11"] + multi_index = pandas.MultiIndex.from_frame( + pandas.DataFrame({"idx0": [5, 1], "idx1": ["z", "x"]}) + ) + + df = bpd.DataFrame(data, index=multi_index, columns=columns) + pd_df = df.to_pandas() + + pandas.testing.assert_frame_equal( + df.explode("col00").to_pandas(), + pd_df.explode("col00"), + check_dtype=False, + check_index_type=False, + ) + + def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index): columns = ["int64_too", "int64_col", "rowindex_2"] level1 = pandas.Index(["b", "c", "d"]) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 624e287f8de..f1c60664a15 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -3852,6 +3852,28 @@ def test_series_explode(data): pytest.param([5, 1, 3, 2], False, id="ignore_unordered_index"), pytest.param(["z", "x", "a", "b"], True, id="str_index"), pytest.param(["z", "x", "a", "b"], False, id="ignore_str_index"), + pytest.param( + pd.Index(["z", "x", "a", "b"], name="idx"), True, id="str_named_index" + ), + pytest.param( + pd.Index(["z", "x", "a", "b"], name="idx"), + False, + id="ignore_str_named_index", + ), + pytest.param( + pd.MultiIndex.from_frame( + pd.DataFrame({"idx0": [5, 1, 3, 2], "idx1": ["z", "x", "a", "b"]}) + ), + True, + id="multi_index", + ), + pytest.param( + pd.MultiIndex.from_frame( + pd.DataFrame({"idx0": [5, 1, 3, 2], "idx1": ["z", "x", "a", "b"]}) + ), + False, + id="ignore_multi_index", + ), ], ) def test_series_explode_w_index(index, ignore_index): From f6282b0f359cb2170ed26d6af44343d73dcb3bd0 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:33:06 -0700 Subject: [PATCH 898/921] chore: implement semantics cluster_by (#1067) * chore: implement semantics cluster_by * address comments and fix tests --- bigframes/operations/semantics.py | 82 +++++++ .../experimental/semantic_operators.ipynb | 231 +++++++++++++++--- .../system/large/operations/test_semantics.py | 62 +++++ 3 files changed, 342 insertions(+), 33 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index 0df7041914c..a4d73965615 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -194,6 +194,88 @@ def agg( return df[column] + def cluster_by( + self, + column: str, + output_column: str, + model, + n_clusters: int = 5, + ): + """ + Clusters data based on the semantic similarity of text within a specified column. + + This method leverages a language model to generate text embeddings for each value in + the given column. These embeddings capture the semantic meaning of the text. + The data is then grouped into `n` clusters using the k-means clustering algorithm, + which groups data points based on the similarity of their embeddings. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> bpd.options.experiments.semantic_operators = True + + >>> import bigframes.ml.llm as llm + >>> model = llm.TextEmbeddingGenerator() + + >>> df = bpd.DataFrame({ + ... "Product": ["Smartphone", "Laptop", "T-shirt", "Jeans"], + ... }) + >>> df.semantics.cluster_by("Product", "Cluster ID", model, n_clusters=2) + Product Cluster ID + 0 Smartphone 2 + 1 Laptop 2 + 2 T-shirt 1 + 3 Jeans 1 + + [4 rows x 2 columns] + + Args: + column (str): + An column name to perform the similarity clustering. + + output_column (str): + An output column to store the clustering ID. + + model (bigframes.ml.llm.TextEmbeddingGenerator): + A TextEmbeddingGenerator provided by Bigframes ML package. + + n_clusters (int, default 5): + Default 5. Number of clusters to be detected. + + Returns: + bigframes.dataframe.DataFrame: A new DataFrame with the clustering output column. + + Raises: + NotImplementedError: when the semantic operator experiment is off. + ValueError: when the column refers to a non-existing column. + """ + + import bigframes.dataframe + import bigframes.ml.cluster as cluster + 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 column not in self._df.columns: + raise ValueError(f"Column {column} not found.") + + if n_clusters <= 1: + raise ValueError( + f"Invalid value for `n_clusters`: {n_clusters}." + "It must be greater than 1." + ) + + df: bigframes.dataframe.DataFrame = self._df.copy() + embeddings_df = model.predict(df[column]) + + cluster_model = cluster.KMeans(n_clusters=n_clusters) + cluster_model.fit(embeddings_df[["ml_generate_embedding_result"]]) + clustered_result = cluster_model.predict(embeddings_df) + df[output_column] = clustered_result["CENTROID_ID"] + return df + def filter(self, instruction: str, model): """ Filters the DataFrame with the semantics of the user instruction. diff --git a/notebooks/experimental/semantic_operators.ipynb b/notebooks/experimental/semantic_operators.ipynb index 4273ceb36ba..b2aa22d9219 100644 --- a/notebooks/experimental/semantic_operators.ipynb +++ b/notebooks/experimental/semantic_operators.ipynb @@ -33,7 +33,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", + "/usr/local/google/home/chelsealin/src/bigframes/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", " warnings.warn(\n" ] } @@ -51,21 +51,25 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", - " return global_session.get_global_session()\n" - ] + "data": { + "text/html": [ + "Query job 13e4b10e-70cf-4b93-8c59-5f6f5fb10aeb is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" }, { "data": { "text/html": [ - "Query job aef2dd7b-bdad-4dda-91be-867e8dac2613 is DONE. 0 Bytes processed. Open Job" + "Query job 559dd42c-573d-4b00-8fe9-b7061afdd672 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -77,7 +81,8 @@ ], "source": [ "import bigframes.ml.llm as llm\n", - "gemini_model = llm.GeminiTextGenerator(model_name=llm._GEMINI_1P5_FLASH_001_ENDPOINT)" + "gemini_model = llm.GeminiTextGenerator(model_name=llm._GEMINI_1P5_FLASH_001_ENDPOINT)\n", + "text_embedding_model = llm.TextEmbeddingGenerator(model_name=\"text-embedding-004\")" ] }, { @@ -657,28 +662,6 @@ "## Semantic Search" ] }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 48aafee2-4948-4677-ab02-a94a71b9f6e2 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "text_embedding_model = llm.TextEmbeddingGenerator(model_name=\"text-embedding-004\")" - ] - }, { "cell_type": "code", "execution_count": 12, @@ -1156,6 +1139,188 @@ "agg_df = df.semantics.agg(\"Find the shared first name of actors in {Movies}. One word answer.\", model=gemini_model)\n", "agg_df" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Cluster" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 92ce82b9-c521-42af-a2b7-6114b27a9ce4 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/chelsealin/src/bigframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 8c4c7391-2889-4cf1-bbfa-5cbf6b144db5 is DONE. 10 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 19ae7cc6-3d61-4c69-9148-1956fafb577a is DONE. 30.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7c2b62df-3bed-4469-9ffc-131843efe25e is DONE. 30.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 74155e34-d8ca-4fba-8b93-33b1b325a5f1 is DONE. 138.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d9151043-a9c3-4388-8268-ef41162012b7 is DONE. 80 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d2c4ad9a-c637-490e-a2cf-37d7f5a34024 is DONE. 170 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ProductCluster ID
0Smartphone3
1Laptop3
2Coffee Maker1
3T-shirt2
4Jeans2
\n", + "

5 rows × 2 columns

\n", + "
[5 rows x 2 columns in total]" + ], + "text/plain": [ + " Product Cluster ID\n", + "0 Smartphone 3\n", + "1 Laptop 3\n", + "2 Coffee Maker 1\n", + "3 T-shirt 2\n", + "4 Jeans 2\n", + "\n", + "[5 rows x 2 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({'Product': ['Smartphone', 'Laptop', 'Coffee Maker', 'T-shirt', 'Jeans']})\n", + "\n", + "df.semantics.cluster_by(column='Product', output_column='Cluster ID', model=text_embedding_model, n=3)" + ] } ], "metadata": { @@ -1174,7 +1339,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.1" } }, "nbformat": 4, diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py index f66f2bc029c..626d6734cf0 100644 --- a/tests/system/large/operations/test_semantics.py +++ b/tests/system/large/operations/test_semantics.py @@ -130,6 +130,68 @@ def test_agg_invalid_cluster_column_raise_error(gemini_flash_model, cluster_colu df.semantics.agg(instruction, gemini_flash_model, cluster_column=cluster_column) +@pytest.mark.parametrize( + ("n_clusters"), + [ + pytest.param(1, id="one", marks=pytest.mark.xfail(raises=ValueError)), + pytest.param(2, id="two"), + pytest.param(4, id="four"), + ], +) +def test_cluster_by(session, text_embedding_generator, n_clusters): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + ({"Product": ["Smartphone", "Laptop", "Coffee Maker", "T-shirt", "Jeans"]}), + session=session, + ) + output_column = "cluster id" + result = df.semantics.cluster_by( + "Product", + output_column, + text_embedding_generator, + n_clusters=n_clusters, + ) + + assert output_column in result + assert len(result[output_column].unique()) == n_clusters + + +def test_cluster_by_invalid_column(session, text_embedding_generator): + bigframes.options.experiments.semantic_operators = True + + df = dataframe.DataFrame( + ({"Product": ["Smartphone", "Laptop", "Coffee Maker", "T-shirt", "Jeans"]}), + session=session, + ) + + output_column = "cluster id" + with pytest.raises(ValueError): + df.semantics.cluster_by( + "unknown_column", + output_column, + text_embedding_generator, + n_clusters=3, + ) + + +def test_cluster_by_invalid_model(session, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + + df = dataframe.DataFrame( + ({"Product": ["Smartphone", "Laptop", "Coffee Maker", "T-shirt", "Jeans"]}), + session=session, + ) + + output_column = "cluster id" + with pytest.raises(TypeError): + df.semantics.cluster_by( + "Product", + output_column, + gemini_flash_model, + n_clusters=3, + ) + + def test_filter(session, gemini_flash_model): bigframes.options.experiments.semantic_operators = True df = dataframe.DataFrame( From f88043b354928883448497a0877bbbd181264ea6 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Tue, 8 Oct 2024 18:46:24 -0700 Subject: [PATCH 899/921] chore: implement semantic similarity search (#1066) * chore: implement semantic similarity search * check amount of data before join * use temp column names for embedding. polish doc --- bigframes/operations/semantics.py | 114 ++++ .../experimental/semantic_operators.ipynb | 514 ++++++++++++++++-- .../system/large/operations/test_semantics.py | 107 ++++ 3 files changed, 679 insertions(+), 56 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index a4d73965615..9ff7ea38b26 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -609,6 +609,120 @@ def search( return typing.cast(bigframes.dataframe.DataFrame, search_result) + 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 + + >>> import bigframes + >>> bigframes.options.experiments.semantic_operators = True + + >>> import bigframes.ml.llm as llm + >>> model = llm.TextEmbeddingGenerator(model_name="text-embedding-004") + + >>> df1 = bpd.DataFrame({'animal': ['monkey', 'spider']}) + >>> df2 = bpd.DataFrame({'animal': ['scorpion', 'baboon']}) + + >>> df1.semantics.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}." + ) + + base_table_embedding_column = bigframes.core.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 + def _make_prompt( self, columns: List[str], user_instruction: str, output_instruction: str ): diff --git a/notebooks/experimental/semantic_operators.ipynb b/notebooks/experimental/semantic_operators.ipynb index b2aa22d9219..bfaad69ce25 100644 --- a/notebooks/experimental/semantic_operators.ipynb +++ b/notebooks/experimental/semantic_operators.ipynb @@ -33,7 +33,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", " warnings.warn(\n" ] } @@ -51,13 +51,21 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", + " return global_session.get_global_session()\n" + ] + }, { "data": { "text/html": [ - "Query job 13e4b10e-70cf-4b93-8c59-5f6f5fb10aeb is DONE. 0 Bytes processed. Open Job" + "Query job 1494d834-8b38-4928-9911-ba3bb9b1228b is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -69,7 +77,7 @@ { "data": { "text/html": [ - "Query job 559dd42c-573d-4b00-8fe9-b7061afdd672 is DONE. 0 Bytes processed. Open Job" + "Query job 6caa309b-492d-4ad3-94e3-cb2b9522ef1e is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -100,7 +108,7 @@ { "data": { "text/html": [ - "Query job a3652c48-9c00-438b-8443-ef11b8a4c81d is DONE. 0 Bytes processed. Open Job" + "Query job d56e32bd-f06a-4086-aac2-560ed03dceca is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -120,7 +128,7 @@ { "data": { "text/html": [ - "Query job dd133a7f-22a5-45a1-bedb-65c2cebd74ec is DONE. 4 Bytes processed. Open Job" + "Query job 0b96351f-5a48-4059-b830-1aebd330599f is DONE. 4 Bytes processed. Open Job" ], "text/plain": [ "" @@ -132,7 +140,7 @@ { "data": { "text/html": [ - "Query job d162b1a6-76e3-4c1c-9d06-7cc8792297e1 is DONE. 33 Bytes processed. Open Job" + "Query job 34b2ce70-b9be-49bb-a06d-f228b0e5937c is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -144,7 +152,7 @@ { "data": { "text/html": [ - "Query job 3b8212fb-07ba-4e6f-afa9-db98becd59fe is DONE. 33 Bytes processed. Open Job" + "Query job a4f799eb-24d6-4fcf-8661-371226788b53 is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -232,7 +240,7 @@ { "data": { "text/html": [ - "Query job ee842f5d-3572-4283-8161-f7b7cee51b06 is DONE. 0 Bytes processed. Open Job" + "Query job 04a27084-a71e-4c2d-9a73-46b768615c94 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -252,7 +260,7 @@ { "data": { "text/html": [ - "Query job c2f1829d-b0b5-4316-942d-ab6881ad09bb is DONE. 4 Bytes processed. Open Job" + "Query job 1a4c0d7f-0bb4-4f16-b2c0-ebb930fa6cd1 is DONE. 4 Bytes processed. Open Job" ], "text/plain": [ "" @@ -264,7 +272,7 @@ { "data": { "text/html": [ - "Query job 066c9101-c069-450f-8189-810b474fa5f2 is DONE. 34 Bytes processed. Open Job" + "Query job 87bf5653-d3d8-4c0a-8017-af43907465de is DONE. 34 Bytes processed. Open Job" ], "text/plain": [ "" @@ -276,7 +284,7 @@ { "data": { "text/html": [ - "Query job e0bb869a-e3ef-49c2-8a9e-7dd343b06cbb is DONE. 93 Bytes processed. Open Job" + "Query job 12822e33-0ca3-4968-a685-7fcb2bdb0790 is DONE. 93 Bytes processed. Open Job" ], "text/plain": [ "" @@ -373,7 +381,7 @@ { "data": { "text/html": [ - "Query job 2857268d-a5f6-47bc-9819-eef64c89dd83 is DONE. 0 Bytes processed. Open Job" + "Query job fcda7d35-d969-47a8-b611-0c516e2e39e8 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -393,7 +401,7 @@ { "data": { "text/html": [ - "Query job ebea0da7-ed07-480b-84dd-2abbd1fb5258 is DONE. 30 Bytes processed. Open Job" + "Query job c532592c-c4ce-4f08-9397-21b1b8b1f347 is DONE. 30 Bytes processed. Open Job" ], "text/plain": [ "" @@ -405,7 +413,7 @@ { "data": { "text/html": [ - "Query job 8971d7c6-11de-47ea-b6c9-6b59cd6b58d2 is DONE. 251 Bytes processed. Open Job" + "Query job a11bd20f-7a75-462c-b6a5-64d954645e1b is DONE. 251 Bytes processed. Open Job" ], "text/plain": [ "" @@ -417,7 +425,7 @@ { "data": { "text/html": [ - "Query job 7f90c077-48a9-433f-9c6b-ef7bc775e8ec is DONE. 144 Bytes processed. Open Job" + "Query job 4703c2a9-ab08-46f1-a612-3354c5df391f is DONE. 144 Bytes processed. Open Job" ], "text/plain": [ "" @@ -520,7 +528,7 @@ { "data": { "text/html": [ - "Query job f8aaae82-3622-4ec1-8e42-65a1712f26e2 is DONE. 0 Bytes processed. Open Job" + "Query job 8c1f1313-3eee-47dc-ad2d-27a49dc831dc is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -540,7 +548,7 @@ { "data": { "text/html": [ - "Query job 511dbbca-8514-4ba6-bcca-93fbe961a2c9 is DONE. 32 Bytes processed. Open Job" + "Query job 08dda435-13bd-49d0-a941-1cf91a9a1c96 is DONE. 32 Bytes processed. Open Job" ], "text/plain": [ "" @@ -552,7 +560,7 @@ { "data": { "text/html": [ - "Query job 2cca3eee-db60-4faa-9f26-6656002f0200 is DONE. 266 Bytes processed. Open Job" + "Query job abf33f67-0056-499b-b7fe-583391c6bc02 is DONE. 266 Bytes processed. Open Job" ], "text/plain": [ "" @@ -564,7 +572,7 @@ { "data": { "text/html": [ - "Query job 91385afd-1107-4162-973b-3a8da98541ee is DONE. 180 Bytes processed. Open Job" + "Query job 74249b99-8975-4fc4-b599-1b682edf8aeb is DONE. 180 Bytes processed. Open Job" ], "text/plain": [ "" @@ -664,13 +672,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job b03ec7b9-dfd6-4c0c-9eb6-484a999c913e is DONE. 0 Bytes processed. Open Job" + "Query job 8be41631-537e-4b73-b3c8-1cad09dffb95 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -740,7 +748,7 @@ "[5 rows x 1 columns]" ] }, - "execution_count": 12, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -752,13 +760,13 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 93fbc66e-4f98-4cdb-bb3a-5af738d6fd67 is DONE. 0 Bytes processed. Open Job" + "Query job 56d5f17f-f64a-46ca-8d30-74f8e2ad5dec is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -778,7 +786,7 @@ { "data": { "text/html": [ - "Query job 34a9725a-058a-4930-bc78-8372355d0c85 is DONE. 10 Bytes processed. Open Job" + "Query job fe75b64a-41a3-4675-ae1e-d2db6b2270d3 is DONE. 10 Bytes processed. Open Job" ], "text/plain": [ "" @@ -790,7 +798,7 @@ { "data": { "text/html": [ - "Query job ed533856-32fe-4012-8d72-ffc73c41418c is DONE. 30.9 kB processed. Open Job" + "Query job 9f06c24e-d931-4e59-a444-1a6013c43290 is DONE. 30.9 kB processed. Open Job" ], "text/plain": [ "" @@ -802,7 +810,7 @@ { "data": { "text/html": [ - "Query job 91f67984-652b-44d4-a69a-91c3e1a3d2b1 is DONE. 0 Bytes processed. Open Job" + "Query job 627b8206-b3f9-4c25-a5d9-dde7c0042a4d is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -822,7 +830,7 @@ { "data": { "text/html": [ - "Query job f866ce11-f832-4c25-b3a7-1dce683b7553 is DONE. 2 Bytes processed. Open Job" + "Query job d01597bb-30ef-495f-be5d-c9fb16d4c112 is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -834,7 +842,7 @@ { "data": { "text/html": [ - "Query job 41e22a53-6ef8-44c5-9cdd-01d99fc1c2fc is RUNNING. Open Job" + "Query job bbc67bc3-830d-4ede-829d-16d4829dec33 is RUNNING. Open Job" ], "text/plain": [ "" @@ -846,7 +854,7 @@ { "data": { "text/html": [ - "Query job 57f809b4-7e2a-46ba-992d-d33b64b92664 is DONE. 37.2 kB processed. Open Job" + "Query job 0c844655-b7d9-494b-8073-925b4e0743ce is DONE. 37.2 kB processed. Open Job" ], "text/plain": [ "" @@ -858,7 +866,7 @@ { "data": { "text/html": [ - "Query job d512a274-0c54-4c53-a92e-20b111477697 is DONE. 0 Bytes processed. Open Job" + "Query job 1993f0be-bfc2-4dad-ba85-92f5bba44945 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -916,7 +924,7 @@ "[2 rows x 2 columns]" ] }, - "execution_count": 13, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -925,6 +933,400 @@ "df.semantics.search(\"creatures\", \"monkey\", top_k = 2, model = text_embedding_model, score_column='similarity score')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Similarity Join" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "df1 = bpd.DataFrame({'animal': ['monkey', 'spider', 'salmon', 'giraffe', 'sparrow']})\n", + "df2 = bpd.DataFrame({'animal': ['scorpion', 'baboon', 'owl', 'elephant', 'tuna']})" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 222a9dcb-2389-4ad3-a1e6-c2b197f3a409 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 24afcd9d-6be5-44d9-aa89-6fbe71f5e9a7 is DONE. 10 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 6bc36226-7bbb-4954-b042-044e9fd98a47 is DONE. 30.8 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f247f63d-1d8a-4f81-a833-628143fda463 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 9bec5633-8ba1-4453-b9c7-6cb555d3c60e is DONE. 10 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job d7df7004-b499-436b-898c-15abee330d9e is RUNNING. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 9012c011-b4e7-4fba-85a6-e439fe3c32d3 is DONE. 61.5 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job bb9987eb-aa37-42ca-bcf1-1ea575a147a8 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
animalanimal_1distance
0monkeybaboon0.747665
1spiderscorpion0.890909
2salmontuna0.925461
3giraffeelephant0.887858
4sparrowowl0.932959
\n", + "

5 rows × 3 columns

\n", + "
[5 rows x 3 columns in total]" + ], + "text/plain": [ + " animal animal_1 distance\n", + "0 monkey baboon 0.747665\n", + "1 spider scorpion 0.890909\n", + "2 salmon tuna 0.925461\n", + "3 giraffe elephant 0.887858\n", + "4 sparrow owl 0.932959\n", + "\n", + "[5 rows x 3 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1.semantics.sim_join(df2, left_on='animal', right_on='animal', top_k=1, model= text_embedding_model, score_column='distance')" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 46e1cbb4-2b4a-4578-b3fd-7caba80d5dcc is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 356840f4-840c-41fc-9c9e-8bbaf9ffa02c is DONE. 4 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 428d070e-fd5c-4b2f-b651-b3de9836c02a is DONE. 12.3 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job bf566989-7bd4-4560-952e-34d007ee1e7e is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 92818df7-d4e9-4cea-884e-304126e78b71 is DONE. 4 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job e8619330-7b91-4ae2-99b3-f4386de4c512 is RUNNING. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job be89eca7-462a-4b1c-95ed-0b0c031aaaac is DONE. 24.6 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 40dcd8ed-1262-459a-b6b3-7471722da078 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
animalanimal_1
0monkeybaboon
1spiderscorpion
\n", + "

2 rows × 2 columns

\n", + "
[2 rows x 2 columns in total]" + ], + "text/plain": [ + " animal animal_1\n", + "0 monkey baboon\n", + "1 spider scorpion\n", + "\n", + "[2 rows x 2 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = bpd.DataFrame({'animal': ['monkey', 'spider']})\n", + "df2 = bpd.DataFrame({'animal': ['scorpion', 'baboon']})\n", + "\n", + "df1.semantics.sim_join(df2, left_on='animal', right_on='animal', top_k=1, model= text_embedding_model)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -934,13 +1336,13 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 9922a236-2597-48d3-9188-d859d31042e4 is DONE. 0 Bytes processed. Open Job" + "Query job ea1e5180-a13a-4ec7-a6b4-8eca042ac9a6 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1028,7 +1430,7 @@ "[7 rows x 2 columns]" ] }, - "execution_count": 4, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -1051,13 +1453,13 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 2b50b8d0-8cf6-4930-8219-2a25ca2b0285 is DONE. 0 Bytes processed. Open Job" + "Query job 274df4fb-06ee-49d8-8e7f-2c7eaee3440f is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1070,14 +1472,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ - "Query job e57370cb-1cb7-4ceb-b084-ac45dd0149dd is DONE. 2 Bytes processed. Open Job" + "Query job 878b41c8-6428-4f05-aa0b-dcba14761ac0 is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1089,7 +1491,7 @@ { "data": { "text/html": [ - "Query job 7b6ab8a2-37f8-4206-a5c4-da84d45d94b2 is DONE. 16 Bytes processed. Open Job" + "Query job 5a909cb7-fcbf-43d5-aac2-79b7ba466dd3 is DONE. 16 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1101,7 +1503,7 @@ { "data": { "text/html": [ - "Query job e1b92cd9-cdda-4d1e-8fb4-5b83183eb26b is DONE. 28 Bytes processed. Open Job" + "Query job 10f97d30-101c-447c-876c-d329d3a6d89b is DONE. 28 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1113,7 +1515,7 @@ { "data": { "text/html": [ - "Query job 643a742a-5cbf-4c3d-bd94-dd2211cb685c is DONE. 28 Bytes processed. Open Job" + "Query job b1b94183-6ad4-4014-94da-7d585d45bc6d is DONE. 28 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1130,7 +1532,7 @@ "Name: Movies, dtype: string" ] }, - "execution_count": 5, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -1149,13 +1551,13 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 92ce82b9-c521-42af-a2b7-6114b27a9ce4 is DONE. 0 Bytes processed. Open Job" + "Query job e52f886a-1f87-45fc-990d-e66c23417a66 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1168,14 +1570,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ - "Query job 8c4c7391-2889-4cf1-bbfa-5cbf6b144db5 is DONE. 10 Bytes processed. Open Job" + "Query job 82ac6302-78a1-41f7-8665-769887a47d42 is DONE. 10 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1187,7 +1589,7 @@ { "data": { "text/html": [ - "Query job 19ae7cc6-3d61-4c69-9148-1956fafb577a is DONE. 30.8 kB processed. Open Job" + "Query job cd42b04e-e9ea-4b56-a891-78608dbef215 is DONE. 30.8 kB processed. Open Job" ], "text/plain": [ "" @@ -1199,7 +1601,7 @@ { "data": { "text/html": [ - "Query job 7c2b62df-3bed-4469-9ffc-131843efe25e is DONE. 30.7 kB processed. Open Job" + "Query job dced08f2-12ee-4b52-b5b2-b7dd177dae12 is DONE. 30.7 kB processed. Open Job" ], "text/plain": [ "" @@ -1211,7 +1613,7 @@ { "data": { "text/html": [ - "Query job 74155e34-d8ca-4fba-8b93-33b1b325a5f1 is DONE. 138.9 kB processed. Open Job" + "Query job 5cbdac9b-f5dd-488c-8262-7a96f8501faa is DONE. 138.9 kB processed. Open Job" ], "text/plain": [ "" @@ -1223,7 +1625,7 @@ { "data": { "text/html": [ - "Query job d9151043-a9c3-4388-8268-ef41162012b7 is DONE. 80 Bytes processed. Open Job" + "Query job e30ff06e-b561-4ea2-b150-8cd91d4f827c is DONE. 80 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1235,7 +1637,7 @@ { "data": { "text/html": [ - "Query job d2c4ad9a-c637-490e-a2cf-37d7f5a34024 is DONE. 170 Bytes processed. Open Job" + "Query job 278d8a51-711a-42fe-86aa-408b2b44d4c7 is DONE. 170 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1311,7 +1713,7 @@ "[5 rows x 2 columns]" ] }, - "execution_count": 5, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -1319,7 +1721,7 @@ "source": [ "df = bpd.DataFrame({'Product': ['Smartphone', 'Laptop', 'Coffee Maker', 'T-shirt', 'Jeans']})\n", "\n", - "df.semantics.cluster_by(column='Product', output_column='Cluster ID', model=text_embedding_model, n=3)" + "df.semantics.cluster_by(column='Product', output_column='Cluster ID', model=text_embedding_model, n_clusters=3)" ] } ], @@ -1339,7 +1741,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py index 626d6734cf0..2d7f4756afa 100644 --- a/tests/system/large/operations/test_semantics.py +++ b/tests/system/large/operations/test_semantics.py @@ -526,3 +526,110 @@ def test_search_invalid_model_raises_error(session): with pytest.raises(TypeError): df.semantics.search("creatures", "monkey", top_k=2, model=None) + + +@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): + bigframes.options.experiments.semantic_operators = True + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + actual_result = df1.semantics.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( + ("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 +): + bigframes.options.experiments.semantic_operators = True + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with pytest.raises(ValueError): + df1.semantics.sim_join( + df2, left_on=left_on, right_on=right_on, model=text_embedding_generator + ) + + +def test_sim_join_invalid_model_raises_error(session): + bigframes.options.experiments.semantic_operators = True + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with pytest.raises(TypeError): + df1.semantics.sim_join( + df2, left_on="creatures", right_on="creatures", model=None + ) + + +def test_sim_join_data_too_large_raises_error(session, text_embedding_generator): + bigframes.options.experiments.semantic_operators = True + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with pytest.raises(ValueError): + df1.semantics.sim_join( + df2, + left_on="creatures", + right_on="creatures", + model=text_embedding_generator, + max_rows=1, + ) From d1b6800a9e1523260752454c5fae3436e8a47c44 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 9 Oct 2024 10:05:57 -0700 Subject: [PATCH 900/921] refactor: Introduce slice op to model array slicing (#1055) --- bigframes/core/__init__.py | 21 ++++- bigframes/core/blocks.py | 83 ++-------------- bigframes/core/nodes.py | 33 ++++++- bigframes/core/rewrite.py | 173 +++++++++++++++++++++++++++++++++- bigframes/dataframe.py | 4 +- bigframes/series.py | 6 +- bigframes/session/executor.py | 43 +++++---- 7 files changed, 259 insertions(+), 104 deletions(-) diff --git a/bigframes/core/__init__.py b/bigframes/core/__init__.py index 0c5d72f0782..485a9d79a77 100644 --- a/bigframes/core/__init__.py +++ b/bigframes/core/__init__.py @@ -158,10 +158,6 @@ def session(self) -> Session: def schema(self) -> schemata.ArraySchema: return self.node.schema - @functools.cached_property - def _compiled_schema(self) -> schemata.ArraySchema: - return bigframes.core.compile.test_only_ibis_inferred_schema(self.node) - @property def explicitly_ordered(self) -> bool: # see BigFrameNode.explicitly_ordered @@ -229,6 +225,23 @@ def order_by(self, by: Sequence[OrderingExpression]) -> ArrayValue: def reversed(self) -> ArrayValue: return ArrayValue(nodes.ReversedNode(child=self.node)) + def slice( + self, start: Optional[int], stop: Optional[int], step: Optional[int] + ) -> ArrayValue: + if self.node.order_ambiguous and not (self.session._strictly_ordered): + warnings.warn( + "Window ordering may be ambiguous, this can cause unstable results.", + bigframes.exceptions.AmbiguousWindowWarning, + ) + return ArrayValue( + nodes.SliceNode( + self.node, + start=start, + stop=stop, + step=step if (step is not None) else 1, + ) + ) + def promote_offsets(self) -> Tuple[ArrayValue, str]: """ Convenience function to promote copy of column offsets to a value column. Can be used to reset index. diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 6e5cffcc3a1..b0a8903e190 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -1465,84 +1465,17 @@ def slice( self, start: typing.Optional[int] = None, stop: typing.Optional[int] = None, - step: typing.Optional[int] = None, - ) -> bigframes.core.blocks.Block: - if step is None: - step = 1 + step: int = 1, + ) -> Block: if step == 0: - raise ValueError("slice step cannot be zero") - if step < 0: - reverse_start = (-start - 1) if start else 0 - reverse_stop = (-stop - 1) if stop else None - reverse_step = -step - return self.reversed()._forward_slice( - reverse_start, reverse_stop, reverse_step - ) - return self._forward_slice(start or 0, stop, step) - - def _forward_slice(self, start: int = 0, stop=None, step: int = 1): - """Performs slice but only for positive step size.""" - if step <= 0: - raise ValueError("forward_slice only supports positive step size") - - use_postive_offsets = ( - (start > 0) - or ((stop is not None) and (stop >= 0)) - or ((step > 1) and (start >= 0)) - ) - use_negative_offsets = ( - (start < 0) or (stop and (stop < 0)) or ((step > 1) and (start < 0)) + raise ValueError("Slice step size must be non-zero") + return Block( + self.expr.slice(start, stop, step), + index_columns=self.index_columns, + column_labels=self.column_labels, + index_labels=self._index_labels, ) - block = self - - # only generate offsets that are used - positive_offsets = None - negative_offsets = None - - if use_postive_offsets: - block, positive_offsets = self.promote_offsets() - if use_negative_offsets: - block, negative_offsets = block.reversed().promote_offsets() - block = block.reversed() - - conditions = [] - if start != 0: - if start > 0: - assert positive_offsets - conditions.append(ops.ge_op.as_expr(positive_offsets, ex.const(start))) - else: - assert negative_offsets - conditions.append( - ops.le_op.as_expr(negative_offsets, ex.const(-start - 1)) - ) - if stop is not None: - if stop >= 0: - assert positive_offsets - conditions.append(ops.lt_op.as_expr(positive_offsets, ex.const(stop))) - else: - assert negative_offsets - conditions.append( - ops.gt_op.as_expr(negative_offsets, ex.const(-stop - 1)) - ) - if step > 1: - if start >= 0: - assert positive_offsets - start_diff = ops.sub_op.as_expr(positive_offsets, ex.const(start)) - else: - assert negative_offsets - start_diff = ops.sub_op.as_expr(negative_offsets, ex.const(-start + 1)) - step_cond = ops.eq_op.as_expr( - ops.mod_op.as_expr(start_diff, ex.const(step)), ex.const(0) - ) - conditions.append(step_cond) - - for cond in conditions: - block, cond_id = block.project_expr(cond) - block = block.filter_by_id(cond_id) - - return block.select_columns(self.value_columns) - # Using cache to optimize for Jupyter Notebook's behavior where both '__repr__' # and '__repr_html__' are called in a single display action, reducing redundant # queries. diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 3ed4558acba..3494bee9eb9 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -20,7 +20,7 @@ import functools import itertools import typing -from typing import Callable, Iterable, Sequence, Tuple +from typing import Callable, Iterable, Optional, Sequence, Tuple import google.cloud.bigquery as bq @@ -270,6 +270,37 @@ def order_ambiguous(self) -> bool: return self.child.order_ambiguous +@dataclass(frozen=True, eq=False) +class SliceNode(UnaryNode): + """Logical slice node conditionally becomes limit or filter over row numbers.""" + + start: Optional[int] + stop: Optional[int] + step: int = 1 + + @property + def row_preserving(self) -> bool: + """Whether this node preserves input rows.""" + return False + + @property + def non_local(self) -> bool: + """ + Whether this node combines information across multiple rows instead of processing rows independently. + Used as an approximation for whether the expression may require shuffling to execute (and therefore be expensive). + """ + return True + + # these are overestimates, more accurate numbers available by converting to concrete limit or analytic+filter ops + @property + def variables_introduced(self) -> int: + return 2 + + @property + def relation_ops_created(self) -> int: + return 2 + + @dataclass(frozen=True, eq=False) class JoinNode(BigFrameNode): left_child: BigFrameNode diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index 095f537c219..d4e530fff35 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -16,13 +16,15 @@ import dataclasses import functools import itertools -from typing import Mapping, Optional, Sequence, Tuple +from typing import cast, Mapping, Optional, Sequence, Tuple import bigframes.core.expression as scalar_exprs +import bigframes.core.guid as guids import bigframes.core.identifiers as ids import bigframes.core.join_def as join_defs import bigframes.core.nodes as nodes import bigframes.core.ordering as order +import bigframes.core.tree_properties as traversals import bigframes.operations as ops Selection = Tuple[Tuple[scalar_exprs.Expression, ids.ColumnId], ...] @@ -381,3 +383,172 @@ def common_selection_root( if r_node in l_nodes: return r_node return None + + +def replace_slice_ops(root: nodes.BigFrameNode) -> nodes.BigFrameNode: + # TODO: we want to pull up some slices into limit op if near root. + if isinstance(root, nodes.SliceNode): + root = root.transform_children(replace_slice_ops) + return convert_slice_to_filter(cast(nodes.SliceNode, root)) + else: + return root.transform_children(replace_slice_ops) + + +def get_simplified_slice(node: nodes.SliceNode): + """Attempts to simplify the slice.""" + row_count = traversals.row_count(node) + start, stop, step = node.start, node.stop, node.step + + if start is None: + start = 0 if step > 0 else -1 + if row_count and step > 0: + if start and start < 0: + start = row_count + start + if stop and stop < 0: + stop = row_count + stop + return start, stop, step + + +def convert_slice_to_filter(node: nodes.SliceNode): + start, stop, step = get_simplified_slice(node) + + # no-op (eg. df[::1]) + if ( + ((start == 0) or (start is None)) + and ((stop is None) or (stop == -1)) + and (step == 1) + ): + return node.child + # No filtering, just reverse (eg. df[::-1]) + if ((start is None) or (start == -1)) and (not stop) and (step == -1): + return nodes.ReversedNode(node.child) + # if start/stop/step are all non-negative, and do a simple predicate on forward offsets + if ((start is None) or (start >= 0)) and ((stop is None) or (stop >= 0)): + node_w_offset = add_offsets(node.child) + predicate = convert_simple_slice( + scalar_exprs.DerefOp(node_w_offset.col_id), start or 0, stop, step + ) + filtered = nodes.FilterNode(node_w_offset, predicate) + return drop_cols(filtered, (node_w_offset.col_id,)) + + # fallback cases, generate both forward and backward offsets + if step < 0: + forward_offsets = add_offsets(node.child) + reversed_offsets = add_offsets(nodes.ReversedNode(forward_offsets)) + dual_indexed = reversed_offsets + else: + reversed_offsets = add_offsets(nodes.ReversedNode(node.child)) + forward_offsets = add_offsets(nodes.ReversedNode(reversed_offsets)) + dual_indexed = forward_offsets + predicate = convert_complex_slice( + scalar_exprs.DerefOp(forward_offsets.col_id), + scalar_exprs.DerefOp(reversed_offsets.col_id), + start, + stop, + step, + ) + filtered = nodes.FilterNode(dual_indexed, predicate) + return drop_cols(filtered, (forward_offsets.col_id, reversed_offsets.col_id)) + + +def add_offsets(node: nodes.BigFrameNode) -> nodes.PromoteOffsetsNode: + # Allow providing custom id generator? + offsets_id = ids.ColumnId(guids.generate_guid()) + return nodes.PromoteOffsetsNode(node, offsets_id) + + +def drop_cols( + node: nodes.BigFrameNode, drop_cols: Tuple[ids.ColumnId, ...] +) -> nodes.SelectionNode: + # adding a whole node that redefines the schema is a lot of overhead, should do something more efficient + selections = tuple( + (scalar_exprs.DerefOp(id), id) for id in node.ids if id not in drop_cols + ) + return nodes.SelectionNode(node, selections) + + +def convert_simple_slice( + offsets: scalar_exprs.Expression, + start: int = 0, + stop: Optional[int] = None, + step: int = 1, +) -> scalar_exprs.Expression: + """Performs slice but only for positive step size.""" + assert start >= 0 + assert (stop is None) or (stop >= 0) + + conditions = [] + if start > 0: + conditions.append(ops.ge_op.as_expr(offsets, scalar_exprs.const(start))) + if (stop is not None) and (stop >= 0): + conditions.append(ops.lt_op.as_expr(offsets, scalar_exprs.const(stop))) + if step > 1: + start_diff = ops.sub_op.as_expr(offsets, scalar_exprs.const(start)) + step_cond = ops.eq_op.as_expr( + ops.mod_op.as_expr(start_diff, scalar_exprs.const(step)), + scalar_exprs.const(0), + ) + conditions.append(step_cond) + + return merge_predicates(conditions) or scalar_exprs.const(True) + + +def convert_complex_slice( + forward_offsets: scalar_exprs.Expression, + reverse_offsets: scalar_exprs.Expression, + start: int, + stop: Optional[int], + step: int = 1, +) -> scalar_exprs.Expression: + conditions = [] + assert step != 0 + if start or ((start is not None) and step < 0): + if start > 0 and step > 0: + start_cond = ops.ge_op.as_expr(forward_offsets, scalar_exprs.const(start)) + elif start > 0 and step < 0: + start_cond = ops.le_op.as_expr(forward_offsets, scalar_exprs.const(start)) + elif start < 0 and step > 0: + start_cond = ops.le_op.as_expr( + reverse_offsets, scalar_exprs.const(-start - 1) + ) + else: + assert start < 0 and step < 0 + start_cond = ops.ge_op.as_expr( + reverse_offsets, scalar_exprs.const(-start - 1) + ) + conditions.append(start_cond) + if stop is not None: + if stop >= 0 and step > 0: + stop_cond = ops.lt_op.as_expr(forward_offsets, scalar_exprs.const(stop)) + elif stop >= 0 and step < 0: + stop_cond = ops.gt_op.as_expr(forward_offsets, scalar_exprs.const(stop)) + elif stop < 0 and step > 0: + stop_cond = ops.gt_op.as_expr( + reverse_offsets, scalar_exprs.const(-stop - 1) + ) + else: + assert (stop < 0) and (step < 0) + stop_cond = ops.lt_op.as_expr( + reverse_offsets, scalar_exprs.const(-stop - 1) + ) + conditions.append(stop_cond) + if step != 1: + if step > 1 and start >= 0: + start_diff = ops.sub_op.as_expr(forward_offsets, scalar_exprs.const(start)) + elif step > 1 and start < 0: + start_diff = ops.sub_op.as_expr( + reverse_offsets, scalar_exprs.const(-start + 1) + ) + elif step < 0 and start >= 0: + start_diff = ops.add_op.as_expr(forward_offsets, scalar_exprs.const(start)) + else: + assert step < 0 and start < 0 + start_diff = ops.add_op.as_expr( + reverse_offsets, scalar_exprs.const(-start + 1) + ) + step_cond = ops.eq_op.as_expr( + ops.mod_op.as_expr(start_diff, scalar_exprs.const(step)), + scalar_exprs.const(0), + ) + conditions.append(step_cond) + return merge_predicates(conditions) or scalar_exprs.const(True) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index efd0e65adb4..0cfa5a21541 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3722,7 +3722,9 @@ def _slice( stop: typing.Optional[int] = None, step: typing.Optional[int] = None, ) -> DataFrame: - block = self._block.slice(start=start, stop=stop, step=step) + block = self._block.slice( + start=start, stop=stop, step=step if (step is not None) else 1 + ) return DataFrame(block) def __array_ufunc__( diff --git a/bigframes/series.py b/bigframes/series.py index 16e2eef6f15..1a913f18d7f 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1923,9 +1923,9 @@ def _slice( step: typing.Optional[int] = None, ) -> bigframes.series.Series: return bigframes.series.Series( - self._block.slice(start=start, stop=stop, step=step).select_column( - self._value_column - ), + self._block.slice( + start=start, stop=stop, step=step if (step is not None) else 1 + ).select_column(self._value_column), ) def cache(self): diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 8508c714fda..ab2ebed0d40 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -45,6 +45,7 @@ import bigframes.core.identifiers import bigframes.core.nodes as nodes import bigframes.core.ordering as order +import bigframes.core.rewrite as rewrites import bigframes.core.schema import bigframes.core.tree_properties as tree_properties import bigframes.features @@ -186,7 +187,7 @@ def iterator_supplier(): # Runs strict validations to ensure internal type predictions and ibis are completely in sync # Do not execute these validations outside of testing suite. if "PYTEST_CURRENT_TEST" in os.environ and len(col_id_overrides) == 0: - validate_result_schema(array_value, iterator.schema) + self._validate_result_schema(array_value, iterator.schema) return ExecuteResult( arrow_batches=iterator_supplier, @@ -436,6 +437,7 @@ def _get_optimized_plan(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: if ENABLE_PRUNING: used_fields = frozenset(field.id for field in optimized_plan.fields) optimized_plan = optimized_plan.prune(used_fields) + optimized_plan = rewrites.replace_slice_ops(optimized_plan) return optimized_plan def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): @@ -558,6 +560,27 @@ def _sql_as_cached_temp_table( query_job.result() return query_job.destination + def _validate_result_schema( + self, + array_value: bigframes.core.ArrayValue, + bq_schema: list[bigquery.schema.SchemaField], + ): + actual_schema = tuple(bq_schema) + ibis_schema = bigframes.core.compile.test_only_ibis_inferred_schema( + self._get_optimized_plan(array_value.node) + ) + internal_schema = array_value.schema + if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: + return + if internal_schema.to_bigquery() != actual_schema: + raise ValueError( + f"This error should only occur while testing. BigFrames internal schema: {internal_schema.to_bigquery()} does not match actual schema: {actual_schema}" + ) + if ibis_schema.to_bigquery() != actual_schema: + raise ValueError( + f"This error should only occur while testing. Ibis schema: {ibis_schema.to_bigquery()} does not match actual schema: {actual_schema}" + ) + def generate_head_plan(node: nodes.BigFrameNode, n: int): offsets_id = bigframes.core.guid.generate_guid("offsets_") @@ -578,21 +601,3 @@ def generate_head_plan(node: nodes.BigFrameNode, n: int): def generate_row_count_plan(node: nodes.BigFrameNode): return nodes.RowCountNode(node) - - -def validate_result_schema( - array_value: bigframes.core.ArrayValue, bq_schema: list[bigquery.schema.SchemaField] -): - actual_schema = tuple(bq_schema) - ibis_schema = array_value._compiled_schema - internal_schema = array_value.schema - if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: - return - if internal_schema.to_bigquery() != actual_schema: - raise ValueError( - f"This error should only occur while testing. BigFrames internal schema: {internal_schema.to_bigquery()} does not match actual schema: {actual_schema}" - ) - if ibis_schema.to_bigquery() != actual_schema: - raise ValueError( - f"This error should only occur while testing. Ibis schema: {ibis_schema.to_bigquery()} does not match actual schema: {actual_schema}" - ) From 067fa089185736b7b41f8f734053f143f46ea383 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Wed, 9 Oct 2024 10:08:24 -0700 Subject: [PATCH 901/921] chore: fix typo in the cross validation notebook (#1069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- notebooks/ml/bq_dataframes_ml_cross_validation.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/ml/bq_dataframes_ml_cross_validation.ipynb b/notebooks/ml/bq_dataframes_ml_cross_validation.ipynb index 824d911aff6..4bfdcc24aad 100644 --- a/notebooks/ml/bq_dataframes_ml_cross_validation.ipynb +++ b/notebooks/ml/bq_dataframes_ml_cross_validation.ipynb @@ -272,7 +272,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2.1 Define KFold class and Train/Test for Each Fold (Mauanl Approach)" + "## 2.1 Define KFold class and Train/Test for Each Fold (Manual Approach)" ] }, { From 601e984aeb3ebf1dcf9cb3f1c34b7f0e4ec7cd16 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:05:18 -0700 Subject: [PATCH 902/921] fix: access MATERIALIZED_VIEW with read_gbq (#1070) * fix: corrected inability to access MATERIALIZED_VIEW with read_gbq * update test --- bigframes/core/nodes.py | 6 +++--- bigframes/session/_io/bigquery/read_gbq_table.py | 12 ++++++++++++ bigframes/session/loader.py | 7 ++++++- tests/system/small/test_dataframe.py | 4 ++++ tests/system/small/test_session.py | 11 +++++++++-- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 3494bee9eb9..1d019365098 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -547,7 +547,7 @@ class GbqTable: table_id: str = field() physical_schema: Tuple[bq.SchemaField, ...] = field() n_rows: int = field() - is_physical_table: bool = field() + is_physically_stored: bool = field() cluster_cols: typing.Optional[Tuple[str, ...]] @staticmethod @@ -563,7 +563,7 @@ def from_table(table: bq.Table, columns: Sequence[str] = ()) -> GbqTable: table_id=table.table_id, physical_schema=schema, n_rows=table.num_rows, - is_physical_table=(table.table_type == "TABLE"), + is_physically_stored=(table.table_type in ["TABLE", "MATERIALIZED_VIEW"]), cluster_cols=None if table.clustering_fields is None else tuple(table.clustering_fields), @@ -641,7 +641,7 @@ def variables_introduced(self) -> int: @property def row_count(self) -> typing.Optional[int]: - if self.source.sql_predicate is None and self.source.table.is_physical_table: + if self.source.sql_predicate is None and self.source.table.is_physically_stored: return self.source.table.n_rows return None diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 7585dd3f452..01ff1a3f150 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -102,6 +102,7 @@ def validate_table( table_ref: bigquery.table.TableReference, columns: Optional[Sequence[str]], snapshot_time: datetime.datetime, + table_type: str, filter_str: Optional[str] = None, ) -> bool: """Validates that the table can be read, returns True iff snapshot is supported.""" @@ -124,6 +125,17 @@ def validate_table( if table_ref.dataset_id.startswith("_"): return False + # Materialized views,does not support snapshot + if table_type == "MATERIALIZED_VIEW": + warnings.warn( + "Materialized views do not support FOR SYSTEM_TIME AS OF queries. " + "Attempting query without time travel. Be aware that as materialized views " + "are updated periodically, modifications to the underlying data in the view may " + "result in errors or unexpected behavior.", + category=bigframes.exceptions.TimeTravelDisabledWarning, + ) + return False + # Second, try with snapshot to verify table supports this feature snapshot_sql = bigframes.session._io.bigquery.to_query( query_or_table=f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}", diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index 22de367804f..923605627df 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -339,7 +339,12 @@ def read_gbq_table( ) enable_snapshot = enable_snapshot and bf_read_gbq_table.validate_table( - self._bqclient, table_ref, all_columns, time_travel_timestamp, filter_str + self._bqclient, + table_ref, + all_columns, + time_travel_timestamp, + table.table_type, + filter_str, ) # ---------------------------- diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 6ee9fb82472..1fb12d3f822 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -1524,6 +1524,10 @@ def test_shape(scalars_dfs): @pytest.mark.parametrize( "reference_table, test_table", [ + ( + "bigframes-dev.bigframes_tests_sys.base_table", + "bigframes-dev.bigframes_tests_sys.base_table_mat_view", + ), ( "bigframes-dev.bigframes_tests_sys.base_table", "bigframes-dev.bigframes_tests_sys.base_table_view", diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 17e8b99704b..4b48915d2df 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -390,9 +390,16 @@ def test_read_gbq_twice_with_same_timestamp(session, penguins_table_id): assert df3 is not None -def test_read_gbq_on_linked_dataset_warns(session): +@pytest.mark.parametrize( + "source_table", + [ + "bigframes-dev.thelook_ecommerce.orders", + "bigframes-dev.bigframes_tests_sys.base_table_mat_view", + ], +) +def test_read_gbq_on_linked_dataset_warns(session, source_table): with warnings.catch_warnings(record=True) as warned: - session.read_gbq("bigframes-dev.thelook_ecommerce.orders") + session.read_gbq(source_table) assert len(warned) == 1 assert warned[0].category == bigframes.exceptions.TimeTravelDisabledWarning From 8821dd45e6d5bc7bff350740d3225e07ac1bdaa9 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 17:52:28 -0500 Subject: [PATCH 903/921] chore(main): release 1.22.0 (#1047) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 31 +++++++++++++++++++++++ bigframes/version.py | 2 +- third_party/bigframes_vendored/version.py | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c398f17d43f..55e295f06a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ [1]: https://pypi.org/project/bigframes/#history +## [1.22.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.21.0...v1.22.0) (2024-10-09) + + +### Features + +* Support regional endpoints for more bigquery locations ([#1061](https://github.com/googleapis/python-bigquery-dataframes/issues/1061)) ([45b672a](https://github.com/googleapis/python-bigquery-dataframes/commit/45b672a9a6359ec8c4755d94e63e5ae77a39754b)) +* Update LLM generators to warn user about model name instead of raising error. ([#1048](https://github.com/googleapis/python-bigquery-dataframes/issues/1048)) ([650d80d](https://github.com/googleapis/python-bigquery-dataframes/commit/650d80d1ad90927068cdb71efbfc548b416641a6)) + + +### Bug Fixes + +* Access MATERIALIZED_VIEW with read_gbq ([#1070](https://github.com/googleapis/python-bigquery-dataframes/issues/1070)) ([601e984](https://github.com/googleapis/python-bigquery-dataframes/commit/601e984aeb3ebf1dcf9cb3f1c34b7f0e4ec7cd16)) +* Correct zero row count in DataFrame from table view ([#1062](https://github.com/googleapis/python-bigquery-dataframes/issues/1062)) ([b536070](https://github.com/googleapis/python-bigquery-dataframes/commit/b53607015abb79be0aa5666681f1c53b5b1bc2b5)) +* Fix generic error message when entering an incorrect column name ([#1031](https://github.com/googleapis/python-bigquery-dataframes/issues/1031)) ([5ac217d](https://github.com/googleapis/python-bigquery-dataframes/commit/5ac217d650bc4f5576ba2b6595a3c0b1d88813ad)) +* Make `explode` respect the index labels ([#1064](https://github.com/googleapis/python-bigquery-dataframes/issues/1064)) ([99ca0df](https://github.com/googleapis/python-bigquery-dataframes/commit/99ca0df90acbbd81197c9b6718b7de7e4dfb86cc)) +* Make invalid location warning case-insensitive ([#1044](https://github.com/googleapis/python-bigquery-dataframes/issues/1044)) ([b6cd55a](https://github.com/googleapis/python-bigquery-dataframes/commit/b6cd55afc49b522904a13a7fd34d40201d176588)) +* Remove palm2 test case from llm load test ([#1063](https://github.com/googleapis/python-bigquery-dataframes/issues/1063)) ([575a10a](https://github.com/googleapis/python-bigquery-dataframes/commit/575a10a7ba0fbac76867f02da1dd65355f00d7aa)) +* Show warning for unknown location set through .ctor ([#1052](https://github.com/googleapis/python-bigquery-dataframes/issues/1052)) ([02c2da7](https://github.com/googleapis/python-bigquery-dataframes/commit/02c2da733b834b99d8044f3c5cac3ac9a85802a6)) + + +### Performance Improvements + +* Reduce schema tracking overhead ([#1056](https://github.com/googleapis/python-bigquery-dataframes/issues/1056)) ([1c3879d](https://github.com/googleapis/python-bigquery-dataframes/commit/1c3879df2d6925e17e2cdca827db8ec919471f72)) +* Repr generates fewer queries ([#1046](https://github.com/googleapis/python-bigquery-dataframes/issues/1046)) ([d204603](https://github.com/googleapis/python-bigquery-dataframes/commit/d204603fdc024823421397dbe514f1f7ced1bc2c)) +* Speedup internal tree comparisons ([#1060](https://github.com/googleapis/python-bigquery-dataframes/issues/1060)) ([4379438](https://github.com/googleapis/python-bigquery-dataframes/commit/4379438fc4f44ea847fd2c00a82af544265a30d2)) + + +### Documentation + +* Add docstring return type section to BigQueryOptions class ([#964](https://github.com/googleapis/python-bigquery-dataframes/issues/964)) ([307385f](https://github.com/googleapis/python-bigquery-dataframes/commit/307385f5295ae6918e7d42dcca2c0e0c32e82446)) + ## [1.21.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v1.20.0...v1.21.0) (2024-10-02) diff --git a/bigframes/version.py b/bigframes/version.py index c07f26bc6f6..75f66191ca1 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0" +__version__ = "1.22.0" diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index c07f26bc6f6..75f66191ca1 100644 --- a/third_party/bigframes_vendored/version.py +++ b/third_party/bigframes_vendored/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.21.0" +__version__ = "1.22.0" From c5eccad4703cc8ccea4bedf9f92104640578d948 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:10:43 -0700 Subject: [PATCH 904/921] chore: implement semantics topk (#1072) * chore: implement semantics topk * rename top_k * fix mypy * fix lint --- bigframes/operations/semantics.py | 185 ++++++++++++- .../experimental/semantic_operators.ipynb | 262 +++++++++++++++++- .../system/large/operations/test_semantics.py | 55 +++- 3 files changed, 487 insertions(+), 15 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index 9ff7ea38b26..f3e237aa1ac 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -17,17 +17,21 @@ import typing from typing import List, Optional -import bigframes -import bigframes.core.guid +import numpy as np + +import bigframes.core.guid as guid import bigframes.dtypes as dtypes class Semantics: def __init__(self, df) -> None: + import bigframes + import bigframes.dataframe + if not bigframes.options.experiments.semantic_operators: raise NotImplementedError() - self._df = df + self._df: bigframes.dataframe.DataFrame = df def agg( self, @@ -130,15 +134,15 @@ def agg( f"{type(df[cluster_column])}" ) - num_cluster = len(df[cluster_column].unique()) + num_cluster = df[cluster_column].unique().shape[0] df = df.sort_values(cluster_column) else: - cluster_column = bigframes.core.guid.generate_guid("pid") + cluster_column = guid.generate_guid("pid") df[cluster_column] = 0 - aggregation_group_id = bigframes.core.guid.generate_guid("agg") - group_row_index = bigframes.core.guid.generate_guid("gid") - llm_prompt = bigframes.core.guid.generate_guid("prompt") + aggregation_group_id = guid.generate_guid("agg") + group_row_index = guid.generate_guid("gid") + llm_prompt = guid.generate_guid("prompt") df = ( df.reset_index(drop=True) .reset_index() @@ -609,6 +613,169 @@ def search( return typing.cast(bigframes.dataframe.DataFrame, search_result) + def top_k(self, instruction: str, model, k=10): + """ + 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.semantic_operators = True + + >>> 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.semantics.top_k("{Animals} are more popular as pets", model=model, k=2) + Animals + 0 Dog + 2 Cat + + [2 rows x 1 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. + + Returns: + bigframes.dataframe.DataFrame: A new DataFrame with the top k rows. + + Raises: + NotImplementedError: when the semantic operator experiment is off. + ValueError: when the instruction refers to a non-existing column, or when no + columns are referred to. + """ + 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( + "Semantic aggregations are limited to a single column." + ) + column = columns[0] + if self._df[column].dtype != dtypes.STRING_DTYPE: + raise TypeError( + "Referred column must be a string type, not " + f"{type(self._df[column])}" + ) + # `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) + + import bigframes.dataframe + import bigframes.series + + df: bigframes.dataframe.DataFrame = self._df[columns].copy() + 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) + + num_selected = 0 + while num_selected < k: + df, num_new_selected = self._topk_partition( + df, + column, + status_column, + user_instruction, + model, + k - num_selected, + ) + 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 + + @staticmethod + def _topk_partition( + df, column: str, status_column: str, user_instruction: str, model, k + ): + 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] - 1) + 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" + + "\nDocument 1: " + + pivot_df.iloc[0][column] + + "\nDocument 2: " + + prompt_s # type:ignore + ) + + import bigframes.dataframe + + predict_df = typing.cast(bigframes.dataframe.DataFrame, model.predict(prompt_s)) + + 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, @@ -688,7 +855,7 @@ def sim_join( f"Number of rows that need processing is {joined_table_rows}, which exceeds row limit {max_rows}." ) - base_table_embedding_column = bigframes.core.guid.generate_guid() + base_table_embedding_column = guid.generate_guid() base_table = self._attach_embedding( other, right_on, base_table_embedding_column, model ).to_gbq() diff --git a/notebooks/experimental/semantic_operators.ipynb b/notebooks/experimental/semantic_operators.ipynb index bfaad69ce25..1a7922c5305 100644 --- a/notebooks/experimental/semantic_operators.ipynb +++ b/notebooks/experimental/semantic_operators.ipynb @@ -33,7 +33,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", + "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", " warnings.warn(\n" ] } @@ -58,14 +58,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", + "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", " return global_session.get_global_session()\n" ] }, { "data": { "text/html": [ - "Query job 1494d834-8b38-4928-9911-ba3bb9b1228b is DONE. 0 Bytes processed. Open Job" + "Query job 97a4a649-de80-43e4-b075-e73e2a8d4471 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -77,7 +77,7 @@ { "data": { "text/html": [ - "Query job 6caa309b-492d-4ad3-94e3-cb2b9522ef1e is DONE. 0 Bytes processed. Open Job" + "Query job 1e63cad5-8306-4149-bc38-c95ff2d1dfd7 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1723,6 +1723,258 @@ "\n", "df.semantics.cluster_by(column='Product', output_column='Cluster ID', model=text_embedding_model, n_clusters=3)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic TopK" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job c25ae9ce-f2b8-4fd1-aeaf-095e8684fd36 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 091741c7-8f60-4413-8293-1d32a6293290 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a41b7804-7d94-4b30-869d-30f7ebddd182 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job b802f0ba-a6ba-4353-bc26-f37470fe49ec is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job dc715051-af99-4fa2-8b8d-1b43cb87177a is DONE. 6 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 9c6a3e0f-a8f8-4abb-8b0e-dcdca9b04efa is DONE. 66 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 101e6747-2786-445b-9ffd-036711de3ccb is DONE. 66 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job ea270c8b-d636-46b7-be91-58b6e204d64c is DONE. 66 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job cce38afd-93f9-4c24-8cad-0f517b8f9c6c is DONE. 66 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 00b7d18c-dcad-46a0-a700-1a92bcc89a2e is DONE. 66 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 2a85aaa7-ec57-4f31-9345-4173572ee20c is DONE. 4 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job f72b5269-1e98-48f2-b669-cd31cc1041e4 is DONE. 110 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 7071ba9f-bcfb-4df6-a7d1-dc81bb4319b7 is DONE. 110 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 659f26a2-eeed-4aec-a933-b4d32d2657bb is DONE. 42 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Animals
0Dog
1Cat
\n", + "

2 rows × 1 columns

\n", + "
[2 rows x 1 columns in total]" + ], + "text/plain": [ + " Animals\n", + "0 Dog\n", + "1 Cat\n", + "\n", + "[2 rows x 1 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({\"Animals\": [\"Dog\", \"Cat\", \"Bird\", \"Horse\"]})\n", + "df.semantics.top_k(\"{Animals} are more popular as pets\", model=gemini_model, k=2)" + ] } ], "metadata": { @@ -1741,7 +1993,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.1" } }, "nbformat": 4, diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py index 2d7f4756afa..b33b52ea1c5 100644 --- a/tests/system/large/operations/test_semantics.py +++ b/tests/system/large/operations/test_semantics.py @@ -43,7 +43,7 @@ def test_semantics_experiment_off_raise_error(): pytest.param(7, "Year", id="four_w_cluster_column"), ], ) -def test_agg_w_max_agg_rows(session, gemini_flash_model, max_agg_rows, cluster_column): +def test_agg(session, gemini_flash_model, max_agg_rows, cluster_column): bigframes.options.experiments.semantic_operators = True df = dataframe.DataFrame( data={ @@ -633,3 +633,56 @@ def test_sim_join_data_too_large_raises_error(session, text_embedding_generator) 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( + "{ID}", + id="invalid_dtypes", + marks=pytest.mark.xfail(raises=TypeError), + ), + pytest.param( + "{index}", + id="preserved", + marks=pytest.mark.xfail(raises=ValueError), + ), + ], +) +def test_top_k_invalid_instruction_raise_error(instruction, gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + { + "Animals": ["Dog", "Cat", "Bird", "Horse"], + "ID": [1, 2, 3, 4], + "index": ["a", "b", "c", "d"], + } + ) + df.semantics.top_k(instruction, model=gemini_flash_model, k=2) + + +def test_top_k_invalid_k_raise_error(gemini_flash_model): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame({"Animals": ["Dog", "Cat", "Bird", "Horse"]}) + with pytest.raises(ValueError): + df.semantics.top_k( + "{Animals} are more popular as pets", + gemini_flash_model, + k=0, + ) From 1d5373dd531c95b4a6a4132ef9b0ead0ecab14b4 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 10 Oct 2024 12:09:26 -0700 Subject: [PATCH 905/921] feat: cleanup temp resources with graceful session shutdown (#1068) * chore: improve error handling, redefine recency to 6 hours * make recency cutoff a script param * add automatic cleanup in session and test build script * move cleanup to a nox session * run cleanup nox session only with doctest as doctest finishes faster --- .kokoro/continuous/doctest.cfg | 2 +- .kokoro/presubmit/doctest.cfg | 2 +- bigframes/session/__init__.py | 4 ++++ noxfile.py | 32 +++++++++++++++++++++++++-- scripts/manage_cloud_functions.py | 36 ++++++++++++++++++++++++------- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/.kokoro/continuous/doctest.cfg b/.kokoro/continuous/doctest.cfg index dca21d43fd7..6016700408c 100644 --- a/.kokoro/continuous/doctest.cfg +++ b/.kokoro/continuous/doctest.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "doctest" + value: "doctest cleanup" } env_vars: { diff --git a/.kokoro/presubmit/doctest.cfg b/.kokoro/presubmit/doctest.cfg index dca21d43fd7..6016700408c 100644 --- a/.kokoro/presubmit/doctest.cfg +++ b/.kokoro/presubmit/doctest.cfg @@ -3,7 +3,7 @@ # Only run this nox session. env_vars: { key: "NOX_SESSION" - value: "doctest" + value: "doctest cleanup" } env_vars: { diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 0d7a90c250c..072bcc57815 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -274,6 +274,10 @@ def __init__( metrics=self._metrics, ) + def __del__(self): + """Automatic cleanup of internal resources""" + self.close() + @property def bqclient(self): return self._clients_provider.bqclient diff --git a/noxfile.py b/noxfile.py index 92f8acad7f1..ef4bf1a37a5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -697,8 +697,8 @@ def system_prerelease(session: nox.sessions.Session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def notebook(session: nox.Session): - GOOGLE_CLOUD_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT") - if not GOOGLE_CLOUD_PROJECT: + google_cloud_project = os.getenv("GOOGLE_CLOUD_PROJECT") + if not google_cloud_project: session.error( "Set GOOGLE_CLOUD_PROJECT environment variable to run notebook session." ) @@ -937,3 +937,31 @@ def release_dry_run(session): ): env["PROJECT_ROOT"] = "." session.run(".kokoro/release-nightly.sh", "--dry-run", env=env) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def cleanup(session): + """Clean up stale and/or temporary resources in the test project.""" + google_cloud_project = os.getenv("GOOGLE_CLOUD_PROJECT") + if not google_cloud_project: + session.error( + "Set GOOGLE_CLOUD_PROJECT environment variable to run notebook session." + ) + + # Cleanup a few stale (more than 12 hours old) temporary cloud run + # functions created by bigframems. This will help keeping the test GCP + # project within the "Number of functions" quota + # https://cloud.google.com/functions/quotas#resource_limits + recency_cutoff_hours = 12 + cleanup_count_per_location = 10 + + session.install("-e", ".") + + session.run( + "python", + "scripts/manage_cloud_functions.py", + f"--project-id={google_cloud_project}", + f"--recency-cutoff={recency_cutoff_hours}", + "cleanup", + f"--number={cleanup_count_per_location}", + ) diff --git a/scripts/manage_cloud_functions.py b/scripts/manage_cloud_functions.py index 6b690890892..33af8463c91 100644 --- a/scripts/manage_cloud_functions.py +++ b/scripts/manage_cloud_functions.py @@ -13,7 +13,7 @@ # limitations under the License. import argparse -from datetime import datetime +import datetime as dt import sys import time @@ -94,8 +94,10 @@ def summarize_gcfs(args): # Count how many GCFs are newer than a day recent = 0 for f in functions: - age = datetime.now() - datetime.fromtimestamp(f.update_time.timestamp()) - if age.days <= 0: + age = dt.datetime.now() - dt.datetime.fromtimestamp( + f.update_time.timestamp() + ) + if age.total_seconds() < args.recency_cutoff: recent += 1 region_counts[region] = (functions_count, recent) @@ -106,7 +108,7 @@ def summarize_gcfs(args): region = item[0] count, recent = item[1] print( - "{}: Total={}, Recent={}, OlderThanADay={}".format( + "{}: Total={}, Recent={}, Older={}".format( region, count, recent, count - recent ) ) @@ -120,8 +122,10 @@ def cleanup_gcfs(args): functions = get_bigframes_functions(args.project_id, region) count = 0 for f in functions: - age = datetime.now() - datetime.fromtimestamp(f.update_time.timestamp()) - if age.days > 0: + age = dt.datetime.now() - dt.datetime.fromtimestamp( + f.update_time.timestamp() + ) + if age.total_seconds() >= args.recency_cutoff: try: count += 1 GCF_CLIENT.delete_function(name=f.name) @@ -134,12 +138,15 @@ def cleanup_gcfs(args): # that for this clean-up, i.e. 6 mutations per minute. So wait for # 60/6 = 10 seconds time.sleep(10) + except google.api_core.exceptions.NotFound: + # Most likely the function was deleted otherwise + pass except google.api_core.exceptions.ResourceExhausted: # Stop deleting in this region for now print( - f"Cannot delete any more functions in region {region} due to quota exhaustion. Please try again later." + f"Failed to delete function in region {region} due to quota exhaustion. Pausing for 2 minutes." ) - break + time.sleep(120) def list_str(values): @@ -168,6 +175,19 @@ def list_str(values): help="Cloud functions region(s). If multiple regions, Specify comma separated (e.g. region1,region2)", ) + def hours_to_timedelta(hrs): + return dt.timedelta(hours=int(hrs)).total_seconds() + + parser.add_argument( + "-c", + "--recency-cutoff", + type=hours_to_timedelta, + required=False, + default=hours_to_timedelta("24"), + action="store", + help="Number of hours, cloud functions older than which should be considered stale (worthy of cleanup).", + ) + subparsers = parser.add_subparsers(title="subcommands", required=True) parser_summary = subparsers.add_parser( "summary", From 8d742693de2d106cb26d8dbfabdaee551417c13a Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Thu, 10 Oct 2024 14:17:00 -0700 Subject: [PATCH 906/921] chore: polish semantic operator demo notebook (#1076) * chore: polish semantic operator demo notebook * add top k section --- .../experimental/semantic_operators.ipynb | 2961 ++++++++++++++--- 1 file changed, 2435 insertions(+), 526 deletions(-) diff --git a/notebooks/experimental/semantic_operators.ipynb b/notebooks/experimental/semantic_operators.ipynb index 1a7922c5305..c0c65a5c5fd 100644 --- a/notebooks/experimental/semantic_operators.ipynb +++ b/notebooks/experimental/semantic_operators.ipynb @@ -4,7 +4,34 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Preparation" + "# BigFrames Semantic Operator Demo" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook gives you a hands-on preview of semantic operator APIs powered by LLM. The demonstration is devided into two sections: \n", + "\n", + "The first section introduces the API syntax with some simple examples. We aim to get you familiar with how BigFrames semantic operators work. \n", + "\n", + "The second section talks about applying semantic operators on real-world large datasets. The examples are designed to benchmark the performance of the operators, and to (maybe) spark some ideas for your next application scenarios.\n", + "\n", + "Without further ado, let's get started." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Preparation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, let's import BigFrames packages." ] }, { @@ -21,19 +48,37 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Enable the semantic operator experiment" + "Make sure the BigFrames version is at least `1.22.0`" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, + "outputs": [], + "source": [ + "from packaging.version import Version\n", + "\n", + "assert Version(bigframes.__version__) >= Version(\"1.22.0\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Turn on the semantic operator experiment. You will see a warning sign saying that these operators are still under experiments. This is a necessary step. Otherwise you will see `NotImplementedError` when calling these operators." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/_config/experiment_options.py:33: UserWarning: Semantic operators are still under experiments, and are subject to change in the future.\n", " warnings.warn(\n" ] } @@ -46,26 +91,42 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Prepare the LLM model. Here we are going to use Gemini 1.5 Flash." + "Optional: turn off the display of progress bar so that only the operation results will be printed out" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# bpd.options.display.progress_bar = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's also create some LLM instances for these operators. They will be passed in as paramters in each method call." + ] + }, + { + "cell_type": "code", + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:559: DefaultLocationWarning: No explicit location is set, so using location US for the session.\n", " return global_session.get_global_session()\n" ] }, { "data": { "text/html": [ - "Query job 97a4a649-de80-43e4-b075-e73e2a8d4471 is DONE. 0 Bytes processed. Open Job" + "Query job cdc57da6-3849-4e40-b2a4-0d678872c8a6 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -77,7 +138,7 @@ { "data": { "text/html": [ - "Query job 1e63cad5-8306-4149-bc38-c95ff2d1dfd7 is DONE. 0 Bytes processed. Open Job" + "Query job df036c3e-4557-44a3-a3fd-c592a7f16ee3 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -93,6 +154,20 @@ "text_embedding_model = llm.TextEmbeddingGenerator(model_name=\"text-embedding-004\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# API Syntax" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section we will go through the semantic operator APIs with small examples." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -100,15 +175,116 @@ "## Semantic Filtering" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Semantic filtering allows you to filter your dataframe based on the instruction (i.e. prompt) you provided. Let's first create a small dataframe:" + ] + }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 263b35a1-4f93-4d20-bc49-7c3736c6629d is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
countrycity
0USASeattle
1GermanyBerlin
2JapanKyoto
\n", + "

3 rows × 2 columns

\n", + "
[3 rows x 2 columns in total]" + ], + "text/plain": [ + " country city\n", + "0 USA Seattle\n", + "1 Germany Berlin\n", + "2 Japan Kyoto\n", + "\n", + "[3 rows x 2 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.DataFrame({'country': ['USA', 'Germany', 'Japan'], 'city': ['Seattle', 'Berlin', 'Kyoto']})\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's filter this dataframe by keeping only the rows where the value in `city` column is the capital of the value in `country` column. The column references could be \"escaped\" by using a pair of braces in your instruction. In this example, our instruction should be like this:\n", + "```\n", + "The {city} is the capital of the {country}.\n", + "```\n", + "\n", + "Note that this is not a Python f-string, so you shouldn't prefix your instruction with an `f`. Let's give it a try:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job d56e32bd-f06a-4086-aac2-560ed03dceca is DONE. 0 Bytes processed. Open Job" + "Query job 98f877ed-6a40-4b6c-84cf-43f3ea4d29c9 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -128,7 +304,7 @@ { "data": { "text/html": [ - "Query job 0b96351f-5a48-4059-b830-1aebd330599f is DONE. 4 Bytes processed. Open Job" + "Query job b0a9ac43-9be2-4b37-9bea-267406d1bd65 is DONE. 6 Bytes processed. Open Job" ], "text/plain": [ "" @@ -140,7 +316,7 @@ { "data": { "text/html": [ - "Query job 34b2ce70-b9be-49bb-a06d-f228b0e5937c is DONE. 33 Bytes processed. Open Job" + "Query job af163e99-1771-4d1f-841a-9aeb45936fd6 is DONE. 50 Bytes processed. Open Job" ], "text/plain": [ "" @@ -152,7 +328,7 @@ { "data": { "text/html": [ - "Query job a4f799eb-24d6-4fcf-8661-371226788b53 is DONE. 33 Bytes processed. Open Job" + "Query job bef74461-ba0e-4a4d-8158-7cdb1ddf993c is DONE. 33 Bytes processed. Open Job" ], "text/plain": [ "" @@ -204,14 +380,20 @@ "[1 rows x 2 columns]" ] }, - "execution_count": 4, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df = bpd.DataFrame({'country': ['USA', 'Germany'], 'city': ['Seattle', 'Berlin']})\n", - "df.semantics.filter(\"{city} is the capital of {country}\", gemini_model)" + "df.semantics.filter(\"The {city} is the capital of the {country}\", model=gemini_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The filter operator extracts the information from the referenced column to enrich your instruction with context. The instruction is then sent for the designated model for evaluation. For filtering operations, the LLM is asked to return only `True` and `False` for each row, and the operator removes the rows accordingly." ] }, { @@ -222,69 +404,21 @@ ] }, { - "cell_type": "code", - "execution_count": 5, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "df = bpd.DataFrame(\n", - " data={\"ingredient_1\": [\"Burger Bun\", \"Soy Bean\"], \"ingredient_2\": [\"Beef Patty\", \"Bittern\"]}\n", - " )" + "Semantic mapping allows to you to combine values from multiple columns into a single output based your instruction. To demonstrate this, let's create an example dataframe:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 04a27084-a71e-4c2d-9a73-46b768615c94 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/html": [ - "Query job 1a4c0d7f-0bb4-4f16-b2c0-ebb930fa6cd1 is DONE. 4 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 87bf5653-d3d8-4c0a-8017-af43907465de is DONE. 34 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 12822e33-0ca3-4968-a685-7fcb2bdb0790 is DONE. 93 Bytes processed. Open Job" + "Query job 18101b3e-f11a-44fd-971a-0c9c35df03ad is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -316,72 +450,67 @@ " \n", " ingredient_1\n", " ingredient_2\n", - " food\n", " \n", " \n", " \n", " \n", " 0\n", - " Burger Bun\n", + " Bun\n", " Beef Patty\n", - " Burger\n", " \n", " \n", " 1\n", " Soy Bean\n", " Bittern\n", - " Tofu\n", + " \n", + " \n", + " 2\n", + " Sausage\n", + " Long Bread\n", " \n", " \n", "\n", - "

2 rows × 3 columns

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

3 rows × 2 columns

\n", + "[3 rows x 2 columns in total]" ], "text/plain": [ - " ingredient_1 ingredient_2 food\n", - "0 Burger Bun Beef Patty Burger \n", - "\n", - "1 Soy Bean Bittern Tofu \n", - "\n", + " ingredient_1 ingredient_2\n", + "0 Bun Beef Patty\n", + "1 Soy Bean Bittern\n", + "2 Sausage Long Bread\n", "\n", - "[2 rows x 3 columns]" + "[3 rows x 2 columns]" ] }, - "execution_count": 6, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df.semantics.map(\"What is the food made from {ingredient_1} and {ingredient_2}? One word only.\", output_column=\"food\", model=gemini_model)" + "df = bpd.DataFrame({\n", + " \"ingredient_1\": [\"Bun\", \"Soy Bean\", \"Sausage\"], \n", + " \"ingredient_2\": [\"Beef Patty\", \"Bittern\", \"Long Bread\"]\n", + " })\n", + "df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Semantic Joining" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "cities = bpd.DataFrame({'city': ['Seattle', 'Ottawa', 'Berlin', 'Shanghai', 'New Delhi']})\n", - "continents = bpd.DataFrame({'continent': ['North America', 'Africa', 'Asia']})" + "Now, let's ask LLM what kind of food can be made from the two ingredients in each row. The column reference syntax in your instruction stays the same. In addition, you need to specify the column name by setting the `output_column` parameter to hold the mapping results." ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job fcda7d35-d969-47a8-b611-0c516e2e39e8 is DONE. 0 Bytes processed. Open Job" + "Query job fee13cd3-657c-4d89-8c0a-0ba006ad1751 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -401,7 +530,7 @@ { "data": { "text/html": [ - "Query job c532592c-c4ce-4f08-9397-21b1b8b1f347 is DONE. 30 Bytes processed. Open Job" + "Query job eb38238a-868e-4769-81fd-6614e9ef46d1 is DONE. 6 Bytes processed. Open Job" ], "text/plain": [ "" @@ -413,7 +542,7 @@ { "data": { "text/html": [ - "Query job a11bd20f-7a75-462c-b6a5-64d954645e1b is DONE. 251 Bytes processed. Open Job" + "Query job 7adaa9c1-13c5-4600-8be9-f204ed9764f1 is DONE. 52 Bytes processed. Open Job" ], "text/plain": [ "" @@ -425,7 +554,7 @@ { "data": { "text/html": [ - "Query job 4703c2a9-ab08-46f1-a612-3354c5df391f is DONE. 144 Bytes processed. Open Job" + "Query job c2342d4d-cfc6-4f68-9c83-b56ae01c7d4d is DONE. 133 Bytes processed. Open Job" ], "text/plain": [ "" @@ -455,53 +584,230 @@ " \n", " \n", " \n", - " city\n", - " continent\n", + " ingredient_1\n", + " ingredient_2\n", + " food\n", " \n", " \n", " \n", " \n", " 0\n", - " Seattle\n", - " North America\n", + " Bun\n", + " Beef Patty\n", + " Burger\n", " \n", " \n", " 1\n", - " Ottawa\n", - " North America\n", + " Soy Bean\n", + " Bittern\n", + " Tofu\n", " \n", " \n", " 2\n", - " Shanghai\n", - " Asia\n", - " \n", - " \n", - " 3\n", - " New Delhi\n", - " Asia\n", + " Sausage\n", + " Long Bread\n", + " Hotdog\n", " \n", " \n", "\n", - "

4 rows × 2 columns

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

3 rows × 3 columns

\n", + "[3 rows x 3 columns in total]" ], "text/plain": [ - " city continent\n", - "0 Seattle North America\n", - "1 Ottawa North America\n", + " ingredient_1 ingredient_2 food\n", + "0 Bun Beef Patty Burger \n", + "\n", + "1 Soy Bean Bittern Tofu \n", + "\n", + "2 Sausage Long Bread Hotdog \n", + "\n", + "\n", + "[3 rows x 3 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.semantics.map(\"What is the food made from {ingredient_1} and {ingredient_2}? One word only.\", output_column=\"food\", model=gemini_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The mechanism behind semantic mapping is very similar with semantic filtering. The one major difference: instead of asking LLM to reply true or false to each row, the operator lets LLM reply free-form strings and attach them as a new column to the dataframe." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Joining" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Semantic joining can join two dataframes based on the instruction you provided. First, let's prepare two dataframes." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "cities = bpd.DataFrame({'city': ['Seattle', 'Ottawa', 'Berlin', 'Shanghai', 'New Delhi']})\n", + "continents = bpd.DataFrame({'continent': ['North America', 'Africa', 'Asia']})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to join the `cities` with `continents` to form a new dataframe such that, in each row the city from the `cities` data frame is in the continent from the `continents` dataframe. We could re-use the aforementioned column reference syntax:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 39494a96-c1a2-4708-94e8-c87d4818dbc6 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job f55e46ba-4453-4fb3-9a9a-da6b769ed3a6 is DONE. 30 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 090121f4-0654-43e2-a62b-0d2e77659950 is DONE. 251 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 75e94a17-45bc-417f-8647-f3e74a52ada5 is DONE. 144 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
citycontinent
0SeattleNorth America
1OttawaNorth America
2ShanghaiAsia
3New DelhiAsia
\n", + "

4 rows × 2 columns

\n", + "
[4 rows x 2 columns in total]" + ], + "text/plain": [ + " city continent\n", + "0 Seattle North America\n", + "1 Ottawa North America\n", "2 Shanghai Asia\n", "3 New Delhi Asia\n", "\n", "[4 rows x 2 columns]" ] }, - "execution_count": 8, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "cities.semantics.join(continents, \"{city} is in {continent}\", gemini_model)" + "cities.semantics.join(continents, \"{city} is in {continent}\", model=gemini_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "!! **Important:** Semantic join can trigger probihitively expensitve operations! This operation first cross joins two dataframes, then invokes semantic filter on each row. That means if you have two dataframes of sizes `M` and `N`, the total amount of queries sent to the LLM is on the scale of `M * N`. Therefore, we have added a parameter `max_rows`, a threshold that guards against unexpected expensive calls. With this parameter, the operator first calculates the size of your cross-joined data, and compares it with the threshold. If the size exceeds your threshold, the fuction will abort early with a `ValueError`. You can manually set the value of `max_rows` to raise or lower the threshold." ] }, { @@ -511,24 +817,40 @@ "### Self Joins" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use a self-join example to demonstrate a special case: what happens when the joining columns exist in both data frames? It turns out that you need to provide extra information in your column references: by attaching \"_left\" and \"_right\" suffixes to your column names. \n", + "\n", + "Let's create an example data frame:" + ] + }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "animals = bpd.DataFrame({'animal': ['cow', 'cat', 'spider', 'elephant']})" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to compare the weights of these animals, and output all the pairs where the animal on the left is heavier than the animal on the right. In this case, we use `animal_left` and `animal_right` to differentiate the data sources:" + ] + }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 8c1f1313-3eee-47dc-ad2d-27a49dc831dc is DONE. 0 Bytes processed. Open Job" + "Query job 9dcd66e6-2354-4ee1-8d77-b94a66facc31 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -548,7 +870,7 @@ { "data": { "text/html": [ - "Query job 08dda435-13bd-49d0-a941-1cf91a9a1c96 is DONE. 32 Bytes processed. Open Job" + "Query job 01a2cff2-b4ba-4aa2-8126-d4754bccda12 is DONE. 32 Bytes processed. Open Job" ], "text/plain": [ "" @@ -560,7 +882,7 @@ { "data": { "text/html": [ - "Query job abf33f67-0056-499b-b7fe-583391c6bc02 is DONE. 266 Bytes processed. Open Job" + "Query job 35b2999b-207b-40b0-9cc1-425a9a68707a is DONE. 266 Bytes processed. Open Job" ], "text/plain": [ "" @@ -572,7 +894,7 @@ { "data": { "text/html": [ - "Query job 74249b99-8975-4fc4-b599-1b682edf8aeb is DONE. 180 Bytes processed. Open Job" + "Query job cb839bc6-0f13-4b66-a462-a440a9a162c8 is DONE. 180 Bytes processed. Open Job" ], "text/plain": [ "" @@ -654,31 +976,47 @@ "[6 rows x 2 columns]" ] }, - "execution_count": 10, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "animals.semantics.join(animals, \"{animal_left} generally weighs heavier than {animal_right}\", gemini_model)" + "animals.semantics.join(animals, \"{animal_left} generally weighs heavier than {animal_right}\", model=gemini_model)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Semantic Search" + "Note that you should not attach \"_left\" or \"_right\" suffixes to non-overlapping columns, otherwise you will get an error.\n", + "\n", + "There is an additional concern on this naming scheme: it risks collisions with existing column names that end with \"_left\" or \"_right\", so we need more efforts to find a better solution." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Aggregation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Semantic aggregation merges all the values in a column into one. At this moment you can only aggregate a single column in each oeprator call. Let's create an example:" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 8be41631-537e-4b73-b3c8-1cad09dffb95 is DONE. 0 Bytes processed. Open Job" + "Query job 6606270b-b734-494c-a602-544db495b4c1 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -708,65 +1046,101 @@ " \n", " \n", " \n", - " creatures\n", + " Movies\n", + " Year\n", " \n", " \n", " \n", " \n", " 0\n", - " salmon\n", + " Titanic\n", + " 1997\n", " \n", " \n", " 1\n", - " sea urchin\n", + " The Wolf of Wall Street\n", + " 2013\n", " \n", " \n", " 2\n", - " baboons\n", + " Killers of the Flower Moon\n", + " 2023\n", " \n", " \n", " 3\n", - " frog\n", + " The Revenant\n", + " 2015\n", " \n", " \n", " 4\n", - " chimpanzee\n", + " Inception\n", + " 2010\n", + " \n", + " \n", + " 5\n", + " Shuttle Island\n", + " 2010\n", + " \n", + " \n", + " 6\n", + " The Great Gatsby\n", + " 2013\n", " \n", " \n", "\n", - "

5 rows × 1 columns

\n", - "[5 rows x 1 columns in total]" + "

7 rows × 2 columns

\n", + "[7 rows x 2 columns in total]" ], "text/plain": [ - " creatures\n", - "0 salmon\n", - "1 sea urchin\n", - "2 baboons\n", - "3 frog\n", - "4 chimpanzee\n", + " Movies Year\n", + "0 Titanic 1997\n", + "1 The Wolf of Wall Street 2013\n", + "2 Killers of the Flower Moon 2023\n", + "3 The Revenant 2015\n", + "4 Inception 2010\n", + "5 Shuttle Island 2010\n", + "6 The Great Gatsby 2013\n", "\n", - "[5 rows x 1 columns]" + "[7 rows x 2 columns]" ] }, - "execution_count": 11, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df = bpd.DataFrame({\"creatures\": [\"salmon\", \"sea urchin\", \"baboons\", \"frog\", \"chimpanzee\"]})\n", + "df = bpd.DataFrame({\n", + " \"Movies\": [\n", + " \"Titanic\",\n", + " \"The Wolf of Wall Street\",\n", + " \"Killers of the Flower Moon\",\n", + " \"The Revenant\",\n", + " \"Inception\",\n", + " \"Shuttle Island\",\n", + " \"The Great Gatsby\",\n", + " ],\n", + " \"Year\": [1997, 2013, 2023, 2015, 2010, 2010, 2013],\n", + "})\n", "df" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's ask LLM to find the actor/actress that starred in all movies:" + ] + }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 56d5f17f-f64a-46ca-8d30-74f8e2ad5dec is DONE. 0 Bytes processed. Open Job" + "Query job 6918cec6-9ac3-49d5-82b3-61eeedbd54dc is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -786,7 +1160,127 @@ { "data": { "text/html": [ - "Query job fe75b64a-41a3-4675-ae1e-d2db6b2270d3 is DONE. 10 Bytes processed. Open Job" + "Query job d95e8f6d-bbe2-4149-86a5-f5e69ad07c9e is DONE. 2 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a6d7545d-91bc-4336-beed-a67295bdfa13 is DONE. 16 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job aafc86f8-2bc3-44da-821e-891ca4c75d46 is DONE. 37 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job 8ca9c614-268c-4592-aaf0-a5df0143e1e7 is DONE. 37 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 Leonardo DiCaprio \n", + "\n", + "Name: Movies, dtype: string" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agg_df = df.semantics.agg(\"Find the actors/actresses who starred in all {Movies}. Reply with their names only.\", model=gemini_model)\n", + "agg_df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead of going through each row one by one, this operator batches multiple rows in a single request towards LLM. It then aggregates all the batched results with the same technique, until there is only one value left. You could set the batch size with `max_agg_rows` parameter, which defaults to 10." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Top K" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Semantic Top K selects the top K values based on your instruction. Here is an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "df = bpd.DataFrame({\"Animals\": [\"Corgi\", \"Orange Cat\", \"Parrot\", \"Tarantula\"]})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to find the top two most popular pets:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 6cb9ade5-c3fd-468c-b590-c59d8c51c68c is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job a56383c1-3bd0-4af1-b0d6-1de829f4e371 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -798,7 +1292,7 @@ { "data": { "text/html": [ - "Query job 9f06c24e-d931-4e59-a444-1a6013c43290 is DONE. 30.9 kB processed. Open Job" + "Query job 2f58a6fe-4a80-481b-b590-9d657f965d91 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -810,7 +1304,7 @@ { "data": { "text/html": [ - "Query job 627b8206-b3f9-4c25-a5d9-dde7c0042a4d is DONE. 0 Bytes processed. Open Job" + "Query job 281c1181-fcdf-407b-9a5d-03c975f05687 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -830,7 +1324,7 @@ { "data": { "text/html": [ - "Query job d01597bb-30ef-495f-be5d-c9fb16d4c112 is DONE. 2 Bytes processed. Open Job" + "Query job 593b3b9c-e2b3-4eb9-a233-56454e2a8419 is DONE. 6 Bytes processed. Open Job" ], "text/plain": [ "" @@ -842,7 +1336,7 @@ { "data": { "text/html": [ - "Query job bbc67bc3-830d-4ede-829d-16d4829dec33 is RUNNING. Open Job" + "Query job 5fe0c094-740d-47ec-a4c5-9b4cdd4c66af is DONE. 66 Bytes processed. Open Job" ], "text/plain": [ "" @@ -854,7 +1348,7 @@ { "data": { "text/html": [ - "Query job 0c844655-b7d9-494b-8073-925b4e0743ce is DONE. 37.2 kB processed. Open Job" + "Query job bab92744-1ec8-42fc-aa8f-ced99ae2fc66 is DONE. 66 Bytes processed. Open Job" ], "text/plain": [ "" @@ -866,7 +1360,7 @@ { "data": { "text/html": [ - "Query job 1993f0be-bfc2-4dad-ba85-92f5bba44945 is DONE. 0 Bytes processed. Open Job" + "Query job 5a27f6f8-7e7b-45bf-8d17-580ef5c73432 is DONE. 52 Bytes processed. Open Job" ], "text/plain": [ "" @@ -896,69 +1390,165 @@ " \n", " \n", " \n", - " creatures\n", - " similarity score\n", + " Animals\n", " \n", " \n", " \n", " \n", - " 2\n", - " baboons\n", - " 0.773411\n", + " 1\n", + " Orange Cat\n", " \n", " \n", - " 4\n", - " chimpanzee\n", - " 0.781101\n", + " 2\n", + " Parrot\n", " \n", " \n", "\n", - "

2 rows × 2 columns

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

2 rows × 1 columns

\n", + "[2 rows x 1 columns in total]" ], "text/plain": [ - " creatures similarity score\n", - "2 baboons 0.773411\n", - "4 chimpanzee 0.781101\n", + " Animals\n", + "1 Orange Cat\n", + "2 Parrot\n", "\n", - "[2 rows x 2 columns]" + "[2 rows x 1 columns]" ] }, - "execution_count": 12, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df.semantics.search(\"creatures\", \"monkey\", top_k = 2, model = text_embedding_model, score_column='similarity score')" + "df.semantics.top_k(\"{Animals} are more popular as pets\", model=gemini_model, k=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Semantic Similarity Join" + "Under the hood, the semantic top K operator performs pair-wise comparisons with LLM. It also adopts the quick select algorithm, which means the top K results are returns in the order of their indices instead of their ranks." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Search" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Semantic search searches the most similar values to your qury within a single column. Here is an example:" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 18, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Query job f9e814b1-a3e4-47f4-b966-0177f879c2a9 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
creatures
0salmon
1sea urchin
2baboons
3frog
4chimpanzee
\n", + "

5 rows × 1 columns

\n", + "
[5 rows x 1 columns in total]" + ], + "text/plain": [ + " creatures\n", + "0 salmon\n", + "1 sea urchin\n", + "2 baboons\n", + "3 frog\n", + "4 chimpanzee\n", + "\n", + "[5 rows x 1 columns]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "df1 = bpd.DataFrame({'animal': ['monkey', 'spider', 'salmon', 'giraffe', 'sparrow']})\n", - "df2 = bpd.DataFrame({'animal': ['scorpion', 'baboon', 'owl', 'elephant', 'tuna']})" + "df = bpd.DataFrame({\"creatures\": [\"salmon\", \"sea urchin\", \"baboons\", \"frog\", \"chimpanzee\"]})\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to get the top 2 creatures that are most similar to \"monkey\":" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 222a9dcb-2389-4ad3-a1e6-c2b197f3a409 is DONE. 0 Bytes processed. Open Job" + "Query job 8ae9b68c-43e7-449d-8709-b2b6108981b9 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -978,7 +1568,7 @@ { "data": { "text/html": [ - "Query job 24afcd9d-6be5-44d9-aa89-6fbe71f5e9a7 is DONE. 10 Bytes processed. Open Job" + "Query job ff50ee3b-e005-4608-9e3f-37e48f924152 is DONE. 10 Bytes processed. Open Job" ], "text/plain": [ "" @@ -990,7 +1580,7 @@ { "data": { "text/html": [ - "Query job 6bc36226-7bbb-4954-b042-044e9fd98a47 is DONE. 30.8 kB processed. Open Job" + "Query job bbc89422-ee33-4afb-8c66-b08c0b6754b9 is DONE. 30.9 kB processed. Open Job" ], "text/plain": [ "" @@ -1002,7 +1592,7 @@ { "data": { "text/html": [ - "Query job f247f63d-1d8a-4f81-a833-628143fda463 is DONE. 0 Bytes processed. Open Job" + "Query job dfea7010-9fb5-4459-b789-b26dc98cb94d is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1022,7 +1612,7 @@ { "data": { "text/html": [ - "Query job 9bec5633-8ba1-4453-b9c7-6cb555d3c60e is DONE. 10 Bytes processed. Open Job" + "Query job 66c3b84e-7eb6-408a-a244-20713ad18f1b is DONE. 2 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1034,7 +1624,7 @@ { "data": { "text/html": [ - "Query job d7df7004-b499-436b-898c-15abee330d9e is RUNNING. Open Job" + "Query job 62f2a822-4b7f-4072-8957-e7539c7a6646 is RUNNING. Open Job" ], "text/plain": [ "" @@ -1046,7 +1636,7 @@ { "data": { "text/html": [ - "Query job 9012c011-b4e7-4fba-85a6-e439fe3c32d3 is DONE. 61.5 kB processed. Open Job" + "Query job 286ee4e0-ee7f-4ba7-b057-de17818d0ea1 is DONE. 37.2 kB processed. Open Job" ], "text/plain": [ "" @@ -1058,7 +1648,7 @@ { "data": { "text/html": [ - "Query job bb9987eb-aa37-42ca-bcf1-1ea575a147a8 is DONE. 0 Bytes processed. Open Job" + "Query job 9d56d05c-ad52-47ea-b911-0abf67b6489f is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1088,76 +1678,92 @@ " \n", " \n", " \n", - " animal\n", - " animal_1\n", - " distance\n", + " creatures\n", + " similarity score\n", " \n", " \n", " \n", " \n", - " 0\n", - " monkey\n", - " baboon\n", - " 0.747665\n", - " \n", - " \n", - " 1\n", - " spider\n", - " scorpion\n", - " 0.890909\n", - " \n", - " \n", " 2\n", - " salmon\n", - " tuna\n", - " 0.925461\n", - " \n", - " \n", - " 3\n", - " giraffe\n", - " elephant\n", - " 0.887858\n", + " baboons\n", + " 0.773411\n", " \n", " \n", " 4\n", - " sparrow\n", - " owl\n", - " 0.932959\n", + " chimpanzee\n", + " 0.781101\n", " \n", " \n", "\n", - "

5 rows × 3 columns

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

2 rows × 2 columns

\n", + "[2 rows x 2 columns in total]" ], "text/plain": [ - " animal animal_1 distance\n", - "0 monkey baboon 0.747665\n", - "1 spider scorpion 0.890909\n", - "2 salmon tuna 0.925461\n", - "3 giraffe elephant 0.887858\n", - "4 sparrow owl 0.932959\n", + " creatures similarity score\n", + "2 baboons 0.773411\n", + "4 chimpanzee 0.781101\n", "\n", - "[5 rows x 3 columns]" + "[2 rows x 2 columns]" ] }, - "execution_count": 14, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df1.semantics.sim_join(df2, left_on='animal', right_on='animal', top_k=1, model= text_embedding_model, score_column='distance')" + "df.semantics.search(\"creatures\", query=\"monkey\", top_k = 2, model = text_embedding_model, score_column='similarity score')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice that we are using a text embedding model this time. This model generates embedding vectors for both your query as well as the values in the search space. The operator then uses BigQuery's built-in VECTOR_SEARCH function to find the nearest neighbors of your query.\n", + "\n", + "In addition, `score_column` is an optional parameter for storing the distances between the results and your query. If not set, the score column won't be attached to the result." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Similarity Join" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When you have multiple queries to search in the same value space, you could use similarity join to simplify your call. For example:" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "df1 = bpd.DataFrame({'animal': ['monkey', 'spider', 'salmon', 'giraffe', 'sparrow']})\n", + "df2 = bpd.DataFrame({'animal': ['scorpion', 'baboon', 'owl', 'elephant', 'tuna']})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we want to pick the most related animal from `df2` for each value in `df1`, and this is how it's done:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 46e1cbb4-2b4a-4578-b3fd-7caba80d5dcc is DONE. 0 Bytes processed. Open Job" + "Query job 292332da-b5ec-45c2-aa34-322afbce8102 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1177,7 +1783,7 @@ { "data": { "text/html": [ - "Query job 356840f4-840c-41fc-9c9e-8bbaf9ffa02c is DONE. 4 Bytes processed. Open Job" + "Query job 95e9743a-2965-4d66-888c-49d5359253d0 is DONE. 10 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1189,7 +1795,7 @@ { "data": { "text/html": [ - "Query job 428d070e-fd5c-4b2f-b651-b3de9836c02a is DONE. 12.3 kB processed. Open Job" + "Query job 9bdaa6c6-0bdc-41c2-bf4d-aecd677a4991 is DONE. 30.8 kB processed. Open Job" ], "text/plain": [ "" @@ -1201,7 +1807,7 @@ { "data": { "text/html": [ - "Query job bf566989-7bd4-4560-952e-34d007ee1e7e is DONE. 0 Bytes processed. Open Job" + "Query job b148669b-1071-4128-8f7d-4c4c37b23d43 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1221,7 +1827,7 @@ { "data": { "text/html": [ - "Query job 92818df7-d4e9-4cea-884e-304126e78b71 is DONE. 4 Bytes processed. Open Job" + "Query job 0c09280c-69ad-4692-9559-c48a656d9cf2 is DONE. 10 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1233,7 +1839,7 @@ { "data": { "text/html": [ - "Query job e8619330-7b91-4ae2-99b3-f4386de4c512 is RUNNING. Open Job" + "Query job 9e31d888-d333-4bbd-9410-0444115e022b is RUNNING. Open Job" ], "text/plain": [ "" @@ -1245,7 +1851,7 @@ { "data": { "text/html": [ - "Query job be89eca7-462a-4b1c-95ed-0b0c031aaaac is DONE. 24.6 kB processed. Open Job" + "Query job bb8988ec-a2b7-4f5b-95c0-f8b89f53f4cc is DONE. 61.5 kB processed. Open Job" ], "text/plain": [ "" @@ -1257,7 +1863,7 @@ { "data": { "text/html": [ - "Query job 40dcd8ed-1262-459a-b6b3-7471722da078 is DONE. 0 Bytes processed. Open Job" + "Query job 6657a931-d0c5-4c8f-a12e-71719394740b is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1289,6 +1895,7 @@ " \n", " animal\n", " animal_1\n", + " distance\n", " \n", " \n", " \n", @@ -1296,170 +1903,103 @@ " 0\n", " monkey\n", " baboon\n", + " 0.747665\n", " \n", " \n", " 1\n", " spider\n", " scorpion\n", + " 0.890909\n", + " \n", + " \n", + " 2\n", + " salmon\n", + " tuna\n", + " 0.925461\n", + " \n", + " \n", + " 3\n", + " giraffe\n", + " elephant\n", + " 0.887858\n", + " \n", + " \n", + " 4\n", + " sparrow\n", + " owl\n", + " 0.932959\n", " \n", " \n", "\n", - "

2 rows × 2 columns

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

5 rows × 3 columns

\n", + "[5 rows x 3 columns in total]" ], "text/plain": [ - " animal animal_1\n", - "0 monkey baboon\n", - "1 spider scorpion\n", + " animal animal_1 distance\n", + "0 monkey baboon 0.747665\n", + "1 spider scorpion 0.890909\n", + "2 salmon tuna 0.925461\n", + "3 giraffe elephant 0.887858\n", + "4 sparrow owl 0.932959\n", "\n", - "[2 rows x 2 columns]" + "[5 rows x 3 columns]" ] }, - "execution_count": 15, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df1 = bpd.DataFrame({'animal': ['monkey', 'spider']})\n", - "df2 = bpd.DataFrame({'animal': ['scorpion', 'baboon']})\n", - "\n", - "df1.semantics.sim_join(df2, left_on='animal', right_on='animal', top_k=1, model= text_embedding_model)" + "df1.semantics.sim_join(df2, left_on='animal', right_on='animal', top_k=1, model= text_embedding_model, score_column='distance')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Semantic Aggregation" + "!! **Important** Like semantic join, this operator can also be very expensive. To guard against unexpected processing of large dataset, use the `max_rows` parameter to provide a threshold. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic Cluster" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Semantic Cluster group similar values together. For example:" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 22, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job ea1e5180-a13a-4ec7-a6b4-8eca042ac9a6 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
MoviesYear
0Titanic1997
1The Wolf of Wall Street2013
2Killers of the Flower Moon2023
3The Revenant2015
4Inception2010
5Shuttle Island2010
6The Great Gatsby2013
\n", - "

7 rows × 2 columns

\n", - "
[7 rows x 2 columns in total]" - ], - "text/plain": [ - " Movies Year\n", - "0 Titanic 1997\n", - "1 The Wolf of Wall Street 2013\n", - "2 Killers of the Flower Moon 2023\n", - "3 The Revenant 2015\n", - "4 Inception 2010\n", - "5 Shuttle Island 2010\n", - "6 The Great Gatsby 2013\n", - "\n", - "[7 rows x 2 columns]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "df = bpd.DataFrame({\n", - " \"Movies\": [\n", - " \"Titanic\",\n", - " \"The Wolf of Wall Street\",\n", - " \"Killers of the Flower Moon\",\n", - " \"The Revenant\",\n", - " \"Inception\",\n", - " \"Shuttle Island\",\n", - " \"The Great Gatsby\",\n", - " ],\n", - " \"Year\": [1997, 2013, 2023, 2015, 2010, 2010, 2013],\n", - "})\n", - "df" + "df = bpd.DataFrame({'Product': ['Smartphone', 'Laptop', 'Coffee Maker', 'T-shirt', 'Jeans']})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to cluster these products into 3 groups, and this is how:" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job 274df4fb-06ee-49d8-8e7f-2c7eaee3440f is DONE. 0 Bytes processed. Open Job" + "Query job 1b1d59bd-3bfe-4887-a595-f281d745cf2a is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1479,7 +2019,7 @@ { "data": { "text/html": [ - "Query job 878b41c8-6428-4f05-aa0b-dcba14761ac0 is DONE. 2 Bytes processed. Open Job" + "Query job 1f01891d-4177-450d-a647-85eb7461a1ec is DONE. 10 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1491,7 +2031,7 @@ { "data": { "text/html": [ - "Query job 5a909cb7-fcbf-43d5-aac2-79b7ba466dd3 is DONE. 16 Bytes processed. Open Job" + "Query job eee27d1d-e3c9-4efd-a0ae-bfb982e51cb1 is DONE. 30.8 kB processed. Open Job" ], "text/plain": [ "" @@ -1503,7 +2043,7 @@ { "data": { "text/html": [ - "Query job 10f97d30-101c-447c-876c-d329d3a6d89b is DONE. 28 Bytes processed. Open Job" + "Query job b98ed0ab-903f-4e4a-b450-d800cef3cabf is DONE. 30.7 kB processed. Open Job" ], "text/plain": [ "" @@ -1515,7 +2055,7 @@ { "data": { "text/html": [ - "Query job b1b94183-6ad4-4014-94da-7d585d45bc6d is DONE. 28 Bytes processed. Open Job" + "Query job ec7f3a21-cf30-4f5b-88e8-c7721da60066 is DONE. 138.9 kB processed. Open Job" ], "text/plain": [ "" @@ -1526,107 +2066,9 @@ }, { "data": { - "text/plain": [ - "0 Leonardo \n", - "\n", - "Name: Movies, dtype: string" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "agg_df = df.semantics.agg(\"Find the shared first name of actors in {Movies}. One word answer.\", model=gemini_model)\n", - "agg_df" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Semantic Cluster" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Query job e52f886a-1f87-45fc-990d-e66c23417a66 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/html": [ - "Query job 82ac6302-78a1-41f7-8665-769887a47d42 is DONE. 10 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job cd42b04e-e9ea-4b56-a891-78608dbef215 is DONE. 30.8 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job dced08f2-12ee-4b52-b5b2-b7dd177dae12 is DONE. 30.7 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 5cbdac9b-f5dd-488c-8262-7a96f8501faa is DONE. 138.9 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job e30ff06e-b561-4ea2-b150-8cd91d4f827c is DONE. 80 Bytes processed. Open Job" - ], + "text/html": [ + "Query job 0766e72b-cb0f-48e2-8475-91d55e95ff42 is DONE. 80 Bytes processed. Open Job" + ], "text/plain": [ "" ] @@ -1637,7 +2079,7 @@ { "data": { "text/html": [ - "Query job 278d8a51-711a-42fe-86aa-408b2b44d4c7 is DONE. 170 Bytes processed. Open Job" + "Query job 9c6275c9-f368-47bf-a9ce-7e4b64f97b2c is DONE. 170 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1713,14 +2155,12 @@ "[5 rows x 2 columns]" ] }, - "execution_count": 19, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df = bpd.DataFrame({'Product': ['Smartphone', 'Laptop', 'Coffee Maker', 'T-shirt', 'Jeans']})\n", - "\n", "df.semantics.cluster_by(column='Product', output_column='Cluster ID', model=text_embedding_model, n_clusters=3)" ] }, @@ -1728,18 +2168,32 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Semantic TopK" + "This operator uses the the embedding model to generate vectors for each value, and then uses KMeans algorithm to group them." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Performance Analyses" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section we will use BigQuery's public data of hacker news to perform some heavy work. First, let's load 3K rows from the table:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Query job c25ae9ce-f2b8-4fd1-aeaf-095e8684fd36 is DONE. 0 Bytes processed. Open Job" + "Query job 6622d251-728a-428f-8eb9-55c450460fe4 is DONE. 16.7 GB processed. Open Job" ], "text/plain": [ "" @@ -1751,7 +2205,7 @@ { "data": { "text/html": [ - "Query job 091741c7-8f60-4413-8293-1d32a6293290 is DONE. 0 Bytes processed. Open Job" + "Query job 409b18ea-bf23-44ac-84eb-deb33feeaa89 is DONE. 1.2 MB processed. Open Job" ], "text/plain": [ "" @@ -1763,19 +2217,375 @@ { "data": { "text/html": [ - "Query job a41b7804-7d94-4b30-869d-30f7ebddd182 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
titletextbyscoretimestamptype
0<NA>Well, most people aren&#x27;t alcoholics, so I...slipframe<NA>2021-06-26 02:37:56+00:00comment
1<NA>No, you don&#x27;t really <i>need</i> a smartp...vetinari<NA>2023-04-19 15:56:34+00:00comment
2<NA>It&#x27;s for the late Paul Allen RIP. Should&...lsr_ssri<NA>2018-10-16 01:07:55+00:00comment
3<NA>Yup they are dangerous. Be careful Donald Trump.Sven7<NA>2015-08-10 16:05:54+00:00comment
4<NA>Sure, it&#x27;s totally reasonable. Just point...nicoburns<NA>2020-10-05 11:20:51+00:00comment
5<NA>I wonder how long before special forces start ...autisticcurio<NA>2020-09-01 15:38:50+00:00comment
6The Impending NY Tech Apocalypse: Here's What ...<NA>gaoprea32011-09-27 22:43:27+00:00story
7<NA>Where would you relocate to? I'm assuming that...pavel_lishin<NA>2011-09-16 19:02:01+00:00comment
8Eureca beta is live. A place for your business...<NA>ricardos12012-10-15 13:09:32+00:00story
9<NA>It doesn’t work on Safari, and WebKit based br...archiewood<NA>2023-04-21 16:45:13+00:00comment
10<NA>I guess I don’t see the relevance. Vegans eat ...stevula<NA>2023-01-19 20:05:54+00:00comment
11<NA>I remember watching the American news media go...fareesh<NA>2019-06-17 19:49:17+00:00comment
12<NA>This article is incorrectly using the current ...stale2002<NA>2018-03-18 18:57:21+00:00comment
13<NA>In the firm I made my internship, we have to u...iserlohnmage<NA>2019-10-22 10:41:01+00:00comment
14<NA>The main reason it requires unsafe is for memo...comex<NA>2017-05-05 20:45:37+00:00comment
15Discord vs. IRC Rough Notes<NA>todsacerdoti482024-07-12 18:39:52+00:00story
16<NA>you have to auth again when you use apple pay.empath75<NA>2017-09-12 18:58:20+00:00comment
17<NA>It goes consumer grade, automotive, military, ...moftz<NA>2021-04-13 01:24:03+00:00comment
18<NA>I don&#x27;t have a link handy but the differe...KennyBlanken<NA>2022-05-13 16:08:38+00:00comment
19<NA>&gt; I don&#x27;t think the use case you menti...colanderman<NA>2017-09-28 05:16:06+00:00comment
20<NA>I think you need to watch it again, because yo...vladimirralev<NA>2018-12-07 11:25:52+00:00comment
21Oh dear: new Yahoo anti-spoofing measures brea...<NA>joshreads12014-04-08 13:29:50+00:00story
22How Much Warmer Was Your City in 2016?<NA>smb0612017-02-16 23:26:34+00:00story
23<NA>Except that they clearly never tried to incent...aenis<NA>2022-01-31 17:08:57+00:00comment
24Working Best at Coffee Shops<NA>GiraffeNecktie2492011-04-19 14:25:17+00:00story
\n", + "

25 rows × 6 columns

\n", + "
[3000 rows x 6 columns in total]" + ], + "text/plain": [ + " title \\\n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "5 \n", + "6 The Impending NY Tech Apocalypse: Here's What ... \n", + "7 \n", + "8 Eureca beta is live. A place for your business... \n", + "9 \n", + "10 \n", + "11 \n", + "12 \n", + "13 \n", + "14 \n", + "15 Discord vs. IRC Rough Notes \n", + "16 \n", + "17 \n", + "18 \n", + "19 \n", + "20 \n", + "21 Oh dear: new Yahoo anti-spoofing measures brea... \n", + "22 How Much Warmer Was Your City in 2016? \n", + "23 \n", + "24 Working Best at Coffee Shops \n", + "\n", + " text by score \\\n", + "0 Well, most people aren't alcoholics, so I... slipframe \n", + "1 No, you don't really need a smartp... vetinari \n", + "2 It's for the late Paul Allen RIP. Should&... lsr_ssri \n", + "3 Yup they are dangerous. Be careful Donald Trump. Sven7 \n", + "4 Sure, it's totally reasonable. Just point... nicoburns \n", + "5 I wonder how long before special forces start ... autisticcurio \n", + "6 gaoprea 3 \n", + "7 Where would you relocate to? I'm assuming that... pavel_lishin \n", + "8 ricardos 1 \n", + "9 It doesn’t work on Safari, and WebKit based br... archiewood \n", + "10 I guess I don’t see the relevance. Vegans eat ... stevula \n", + "11 I remember watching the American news media go... fareesh \n", + "12 This article is incorrectly using the current ... stale2002 \n", + "13 In the firm I made my internship, we have to u... iserlohnmage \n", + "14 The main reason it requires unsafe is for memo... comex \n", + "15 todsacerdoti 48 \n", + "16 you have to auth again when you use apple pay. empath75 \n", + "17 It goes consumer grade, automotive, military, ... moftz \n", + "18 I don't have a link handy but the differe... KennyBlanken \n", + "19 > I don't think the use case you menti... colanderman \n", + "20 I think you need to watch it again, because yo... vladimirralev \n", + "21 joshreads 1 \n", + "22 smb06 1 \n", + "23 Except that they clearly never tried to incent... aenis \n", + "24 GiraffeNecktie 249 \n", + "\n", + " timestamp type \n", + "0 2021-06-26 02:37:56+00:00 comment \n", + "1 2023-04-19 15:56:34+00:00 comment \n", + "2 2018-10-16 01:07:55+00:00 comment \n", + "3 2015-08-10 16:05:54+00:00 comment \n", + "4 2020-10-05 11:20:51+00:00 comment \n", + "5 2020-09-01 15:38:50+00:00 comment \n", + "6 2011-09-27 22:43:27+00:00 story \n", + "7 2011-09-16 19:02:01+00:00 comment \n", + "8 2012-10-15 13:09:32+00:00 story \n", + "9 2023-04-21 16:45:13+00:00 comment \n", + "10 2023-01-19 20:05:54+00:00 comment \n", + "11 2019-06-17 19:49:17+00:00 comment \n", + "12 2018-03-18 18:57:21+00:00 comment \n", + "13 2019-10-22 10:41:01+00:00 comment \n", + "14 2017-05-05 20:45:37+00:00 comment \n", + "15 2024-07-12 18:39:52+00:00 story \n", + "16 2017-09-12 18:58:20+00:00 comment \n", + "17 2021-04-13 01:24:03+00:00 comment \n", + "18 2022-05-13 16:08:38+00:00 comment \n", + "19 2017-09-28 05:16:06+00:00 comment \n", + "20 2018-12-07 11:25:52+00:00 comment \n", + "21 2014-04-08 13:29:50+00:00 story \n", + "22 2017-02-16 23:26:34+00:00 story \n", + "23 2022-01-31 17:08:57+00:00 comment \n", + "24 2011-04-19 14:25:17+00:00 story \n", + "...\n", + "\n", + "[3000 rows x 6 columns]" ] }, + "execution_count": 24, "metadata": {}, - "output_type": "display_data" - }, + "output_type": "execute_result" + } + ], + "source": [ + "hacker_news = bpd.read_gbq(\"bigquery-public-data.hacker_news.full\")[['title', 'text', 'by', 'score', 'timestamp', 'type']].head(3000)\n", + "hacker_news" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, let's keep only the rows that have text content:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ { "data": { "text/html": [ - "Query job b802f0ba-a6ba-4353-bc26-f37470fe49ec is DONE. 0 Bytes processed. Open Job" + "Query job d98d2f69-ece3-4e58-b608-8114e59d275b is DONE. 1.0 MB processed. Open Job" ], "text/plain": [ "" @@ -1784,30 +2594,38 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", - " warnings.warn(\n" - ] - }, { "data": { - "text/html": [ - "Query job dc715051-af99-4fa2-8b8d-1b43cb87177a is DONE. 6 Bytes processed. Open Job" - ], "text/plain": [ - "" + "2558" ] }, + "execution_count": 25, "metadata": {}, - "output_type": "display_data" - }, + "output_type": "execute_result" + } + ], + "source": [ + "hacker_news_with_texts = hacker_news[hacker_news['text'].isnull() == False]\n", + "len(hacker_news_with_texts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's calculate the average text length in all the rows:" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ { "data": { "text/html": [ - "Query job 9c6a3e0f-a8f8-4abb-8b0e-dcdca9b04efa is DONE. 66 Bytes processed. Open Job" + "Query job 0211925e-0599-454d-a1d5-e145e6db79c4 is DONE. 1.0 MB processed. Open Job" ], "text/plain": [ "" @@ -1818,32 +2636,87 @@ }, { "data": { - "text/html": [ - "Query job 101e6747-2786-445b-9ffd-036711de3ccb is DONE. 66 Bytes processed. Open Job" - ], "text/plain": [ - "" + "390.7251759186865" ] }, + "execution_count": 26, "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job ea270c8b-d636-46b7-be91-58b6e204d64c is DONE. 66 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, + "output_type": "execute_result" + } + ], + "source": [ + "hacker_news_with_texts['text'].str.len().mean()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now it's LLM's turn. Let's keep the rows in which the text is talking about iPhone. This will take several minutes to finish." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job d06977e3-3c83-4360-a920-4815dd1fbb0d is DONE. 1.0 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job af69426d-ebea-4338-a742-cef524ecca72 is DONE. 5.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/ml/llm.py:976: RuntimeWarning: Some predictions failed. Check column ml_generate_text_status for detailed status. You may want to filter the failed rows and retry.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job ccd12132-da6c-45ed-9bd3-a199d4c47118 is DONE. 1.2 MB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ - "Query job cce38afd-93f9-4c24-8cad-0f517b8f9c6c is DONE. 66 Bytes processed. Open Job" + "Query job 824d2d55-cc04-4794-9f18-9eb67c639b19 is DONE. 1.8 kB processed. Open Job" ], "text/plain": [ "" @@ -1855,7 +2728,151 @@ { "data": { "text/html": [ - "Query job 00b7d18c-dcad-46a0-a700-1a92bcc89a2e is DONE. 66 Bytes processed. Open Job" + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
titletextbyscoretimestamptype
16<NA>you have to auth again when you use apple pay.empath75<NA>2017-09-12 18:58:20+00:00comment
413<NA>Well last time I got angry down votes for sayi...drieddust<NA>2021-01-11 19:27:27+00:00comment
797<NA>New iPhone should be announced on September. L...meerita<NA>2019-07-30 20:54:42+00:00comment
1484<NA>Why would this take a week? i(phone)OS was ori...TheOtherHobbes<NA>2021-06-08 09:25:24+00:00comment
1529<NA>&gt;or because Apple drama brings many clicks?...weberer<NA>2022-09-05 13:16:02+00:00comment
1561<NA>Location: Sydney, AU<p>Remote: Yes<p>Willing t...drEv0<NA>2016-05-03 23:55:26+00:00comment
\n", + "

6 rows × 6 columns

\n", + "
[6 rows x 6 columns in total]" + ], + "text/plain": [ + " title text by \\\n", + "16 you have to auth again when you use apple pay. empath75 \n", + "413 Well last time I got angry down votes for sayi... drieddust \n", + "797 New iPhone should be announced on September. L... meerita \n", + "1484 Why would this take a week? i(phone)OS was ori... TheOtherHobbes \n", + "1529 >or because Apple drama brings many clicks?... weberer \n", + "1561 Location: Sydney, AU

Remote: Yes

Willing t... drEv0 \n", + "\n", + " score timestamp type \n", + "16 2017-09-12 18:58:20+00:00 comment \n", + "413 2021-01-11 19:27:27+00:00 comment \n", + "797 2019-07-30 20:54:42+00:00 comment \n", + "1484 2021-06-08 09:25:24+00:00 comment \n", + "1529 2022-09-05 13:16:02+00:00 comment \n", + "1561 2016-05-03 23:55:26+00:00 comment \n", + "\n", + "[6 rows x 6 columns]" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "iphone_comments=hacker_news_with_texts.semantics.filter(\"The {text} is mainly focused on iPhone\", gemini_model)\n", + "iphone_comments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The performance of the semantic operators depends on the length of your input as well as your quota. Here are my benchmarks for running the previous operation over data of different sizes.\n", + "\n", + "* 800 Rows -> 1m 21.3s\n", + "* 2550 Rows -> 5m 9s\n", + "* 8500 Rows -> 16m 34.4s\n", + "\n", + "These numbers can give you a general idea of how fast the operators run." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's use LLM to summarize the sentiments towards iPhone:" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job 6d7d7de8-0ec8-4966-a8d1-1dc09e61af6a is DONE. 1.6 kB processed. Open Job" ], "text/plain": [ "" @@ -1868,14 +2885,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/chelsealin/src/bigframes3/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ - "Query job 2a85aaa7-ec57-4f31-9345-4173572ee20c is DONE. 4 Bytes processed. Open Job" + "Query job 5aa9d5a4-aa2f-4320-91ab-3cf7479ab883 is DONE. 12 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1887,7 +2904,7 @@ { "data": { "text/html": [ - "Query job f72b5269-1e98-48f2-b669-cd31cc1041e4 is DONE. 110 Bytes processed. Open Job" + "Query job d64f52ab-6575-43a5-870d-418decc99e49 is DONE. 2.0 kB processed. Open Job" ], "text/plain": [ "" @@ -1899,7 +2916,7 @@ { "data": { "text/html": [ - "Query job 7071ba9f-bcfb-4df6-a7d1-dc81bb4319b7 is DONE. 110 Bytes processed. Open Job" + "Query job 9e11e90f-1ae1-4fee-b5bf-932436e4ea92 is DONE. 2.0 kB processed. Open Job" ], "text/plain": [ "" @@ -1911,7 +2928,166 @@ { "data": { "text/html": [ - "Query job 659f26a2-eeed-4aec-a933-b4d32d2657bb is DONE. 42 Bytes processed. Open Job" + "

\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
titletextbyscoretimestamptypesentiment
16<NA>you have to auth again when you use apple pay.empath75<NA>2017-09-12 18:58:20+00:00commentFrustrated, Negative, Annoyed
413<NA>Well last time I got angry down votes for sayi...drieddust<NA>2021-01-11 19:27:27+00:00commentFrustrated, feeling cheated.
797<NA>New iPhone should be announced on September. L...meerita<NA>2019-07-30 20:54:42+00:00commentExcited anticipation.
1484<NA>Why would this take a week? i(phone)OS was ori...TheOtherHobbes<NA>2021-06-08 09:25:24+00:00commentFrustrated, critical, obvious.
1529<NA>&gt;or because Apple drama brings many clicks?...weberer<NA>2022-09-05 13:16:02+00:00commentNegative, clickbait, controversy.
1561<NA>Location: Sydney, AU<p>Remote: Yes<p>Willing t...drEv0<NA>2016-05-03 23:55:26+00:00commentSeeking employment in Australia.
\n", + "

6 rows × 7 columns

\n", + "
[6 rows x 7 columns in total]" + ], + "text/plain": [ + " title text by \\\n", + "16 you have to auth again when you use apple pay. empath75 \n", + "413 Well last time I got angry down votes for sayi... drieddust \n", + "797 New iPhone should be announced on September. L... meerita \n", + "1484 Why would this take a week? i(phone)OS was ori... TheOtherHobbes \n", + "1529 >or because Apple drama brings many clicks?... weberer \n", + "1561 Location: Sydney, AU

Remote: Yes

Willing t... drEv0 \n", + "\n", + " score timestamp type \\\n", + "16 2017-09-12 18:58:20+00:00 comment \n", + "413 2021-01-11 19:27:27+00:00 comment \n", + "797 2019-07-30 20:54:42+00:00 comment \n", + "1484 2021-06-08 09:25:24+00:00 comment \n", + "1529 2022-09-05 13:16:02+00:00 comment \n", + "1561 2016-05-03 23:55:26+00:00 comment \n", + "\n", + " sentiment \n", + "16 Frustrated, Negative, Annoyed \n", + " \n", + "413 Frustrated, feeling cheated. \n", + " \n", + "797 Excited anticipation. \n", + " \n", + "1484 Frustrated, critical, obvious. \n", + " \n", + "1529 Negative, clickbait, controversy. \n", + " \n", + "1561 Seeking employment in Australia. \n", + " \n", + "\n", + "[6 rows x 7 columns]" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "iphone_comments.semantics.map(\"Summarize the sentiment of the {text}. Your answer should have at most 3 words\", output_column=\"sentiment\", model=gemini_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is another example: we count the number of rows whose authors have animals in their names." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/venv/lib/python3.11/site-packages/IPython/core/interactiveshell.py:3577: UserWarning: Reading cached table from 2024-10-10 20:04:54.370456+00:00 to avoid incompatibilies with previous reads of this table. To read the latest version, set `use_cache=False` or close the current session with Session.close() or bigframes.pandas.close_session().\n", + " exec(code_obj, self.user_global_ns, self.user_ns)\n" + ] + }, + { + "data": { + "text/html": [ + "Query job ec9a3769-cc0a-4f45-9565-c1af98ae98e5 is DONE. 0 Bytes processed. Open Job" ], "text/plain": [ "" @@ -1941,39 +3117,772 @@ " \n", " \n", " \n", - " Animals\n", + " title\n", + " text\n", + " by\n", + " score\n", + " timestamp\n", + " type\n", " \n", " \n", " \n", " \n", " 0\n", - " Dog\n", + " <NA>\n", + " Well, most people aren&#x27;t alcoholics, so I...\n", + " slipframe\n", + " <NA>\n", + " 2021-06-26 02:37:56+00:00\n", + " comment\n", " \n", " \n", " 1\n", - " Cat\n", + " <NA>\n", + " No, you don&#x27;t really <i>need</i> a smartp...\n", + " vetinari\n", + " <NA>\n", + " 2023-04-19 15:56:34+00:00\n", + " comment\n", + " \n", + " \n", + " 2\n", + " <NA>\n", + " It&#x27;s for the late Paul Allen RIP. Should&...\n", + " lsr_ssri\n", + " <NA>\n", + " 2018-10-16 01:07:55+00:00\n", + " comment\n", + " \n", + " \n", + " 3\n", + " <NA>\n", + " Yup they are dangerous. Be careful Donald Trump.\n", + " Sven7\n", + " <NA>\n", + " 2015-08-10 16:05:54+00:00\n", + " comment\n", + " \n", + " \n", + " 4\n", + " <NA>\n", + " Sure, it&#x27;s totally reasonable. Just point...\n", + " nicoburns\n", + " <NA>\n", + " 2020-10-05 11:20:51+00:00\n", + " comment\n", + " \n", + " \n", + " 5\n", + " <NA>\n", + " I wonder how long before special forces start ...\n", + " autisticcurio\n", + " <NA>\n", + " 2020-09-01 15:38:50+00:00\n", + " comment\n", + " \n", + " \n", + " 6\n", + " The Impending NY Tech Apocalypse: Here's What ...\n", + " <NA>\n", + " gaoprea\n", + " 3\n", + " 2011-09-27 22:43:27+00:00\n", + " story\n", + " \n", + " \n", + " 7\n", + " <NA>\n", + " Where would you relocate to? I'm assuming that...\n", + " pavel_lishin\n", + " <NA>\n", + " 2011-09-16 19:02:01+00:00\n", + " comment\n", + " \n", + " \n", + " 8\n", + " Eureca beta is live. A place for your business...\n", + " <NA>\n", + " ricardos\n", + " 1\n", + " 2012-10-15 13:09:32+00:00\n", + " story\n", + " \n", + " \n", + " 9\n", + " <NA>\n", + " It doesn’t work on Safari, and WebKit based br...\n", + " archiewood\n", + " <NA>\n", + " 2023-04-21 16:45:13+00:00\n", + " comment\n", + " \n", + " \n", + " 10\n", + " <NA>\n", + " I guess I don’t see the relevance. Vegans eat ...\n", + " stevula\n", + " <NA>\n", + " 2023-01-19 20:05:54+00:00\n", + " comment\n", + " \n", + " \n", + " 11\n", + " <NA>\n", + " I remember watching the American news media go...\n", + " fareesh\n", + " <NA>\n", + " 2019-06-17 19:49:17+00:00\n", + " comment\n", + " \n", + " \n", + " 12\n", + " <NA>\n", + " This article is incorrectly using the current ...\n", + " stale2002\n", + " <NA>\n", + " 2018-03-18 18:57:21+00:00\n", + " comment\n", + " \n", + " \n", + " 13\n", + " <NA>\n", + " In the firm I made my internship, we have to u...\n", + " iserlohnmage\n", + " <NA>\n", + " 2019-10-22 10:41:01+00:00\n", + " comment\n", + " \n", + " \n", + " 14\n", + " <NA>\n", + " The main reason it requires unsafe is for memo...\n", + " comex\n", + " <NA>\n", + " 2017-05-05 20:45:37+00:00\n", + " comment\n", + " \n", + " \n", + " 15\n", + " Discord vs. IRC Rough Notes\n", + " <NA>\n", + " todsacerdoti\n", + " 48\n", + " 2024-07-12 18:39:52+00:00\n", + " story\n", + " \n", + " \n", + " 16\n", + " <NA>\n", + " you have to auth again when you use apple pay.\n", + " empath75\n", + " <NA>\n", + " 2017-09-12 18:58:20+00:00\n", + " comment\n", + " \n", + " \n", + " 17\n", + " <NA>\n", + " It goes consumer grade, automotive, military, ...\n", + " moftz\n", + " <NA>\n", + " 2021-04-13 01:24:03+00:00\n", + " comment\n", + " \n", + " \n", + " 18\n", + " <NA>\n", + " I don&#x27;t have a link handy but the differe...\n", + " KennyBlanken\n", + " <NA>\n", + " 2022-05-13 16:08:38+00:00\n", + " comment\n", + " \n", + " \n", + " 19\n", + " <NA>\n", + " &gt; I don&#x27;t think the use case you menti...\n", + " colanderman\n", + " <NA>\n", + " 2017-09-28 05:16:06+00:00\n", + " comment\n", + " \n", + " \n", + " 20\n", + " <NA>\n", + " I think you need to watch it again, because yo...\n", + " vladimirralev\n", + " <NA>\n", + " 2018-12-07 11:25:52+00:00\n", + " comment\n", + " \n", + " \n", + " 21\n", + " Oh dear: new Yahoo anti-spoofing measures brea...\n", + " <NA>\n", + " joshreads\n", + " 1\n", + " 2014-04-08 13:29:50+00:00\n", + " story\n", + " \n", + " \n", + " 22\n", + " How Much Warmer Was Your City in 2016?\n", + " <NA>\n", + " smb06\n", + " 1\n", + " 2017-02-16 23:26:34+00:00\n", + " story\n", + " \n", + " \n", + " 23\n", + " <NA>\n", + " Except that they clearly never tried to incent...\n", + " aenis\n", + " <NA>\n", + " 2022-01-31 17:08:57+00:00\n", + " comment\n", + " \n", + " \n", + " 24\n", + " Working Best at Coffee Shops\n", + " <NA>\n", + " GiraffeNecktie\n", + " 249\n", + " 2011-04-19 14:25:17+00:00\n", + " story\n", " \n", " \n", "\n", - "

2 rows × 1 columns

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

25 rows × 6 columns

\n", + "[3000 rows x 6 columns in total]" + ], + "text/plain": [ + " title \\\n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "5 \n", + "6 The Impending NY Tech Apocalypse: Here's What ... \n", + "7 \n", + "8 Eureca beta is live. A place for your business... \n", + "9 \n", + "10 \n", + "11 \n", + "12 \n", + "13 \n", + "14 \n", + "15 Discord vs. IRC Rough Notes \n", + "16 \n", + "17 \n", + "18 \n", + "19 \n", + "20 \n", + "21 Oh dear: new Yahoo anti-spoofing measures brea... \n", + "22 How Much Warmer Was Your City in 2016? \n", + "23 \n", + "24 Working Best at Coffee Shops \n", + "\n", + " text by score \\\n", + "0 Well, most people aren't alcoholics, so I... slipframe \n", + "1 No, you don't really need a smartp... vetinari \n", + "2 It's for the late Paul Allen RIP. Should&... lsr_ssri \n", + "3 Yup they are dangerous. Be careful Donald Trump. Sven7 \n", + "4 Sure, it's totally reasonable. Just point... nicoburns \n", + "5 I wonder how long before special forces start ... autisticcurio \n", + "6 gaoprea 3 \n", + "7 Where would you relocate to? I'm assuming that... pavel_lishin \n", + "8 ricardos 1 \n", + "9 It doesn’t work on Safari, and WebKit based br... archiewood \n", + "10 I guess I don’t see the relevance. Vegans eat ... stevula \n", + "11 I remember watching the American news media go... fareesh \n", + "12 This article is incorrectly using the current ... stale2002 \n", + "13 In the firm I made my internship, we have to u... iserlohnmage \n", + "14 The main reason it requires unsafe is for memo... comex \n", + "15 todsacerdoti 48 \n", + "16 you have to auth again when you use apple pay. empath75 \n", + "17 It goes consumer grade, automotive, military, ... moftz \n", + "18 I don't have a link handy but the differe... KennyBlanken \n", + "19 > I don't think the use case you menti... colanderman \n", + "20 I think you need to watch it again, because yo... vladimirralev \n", + "21 joshreads 1 \n", + "22 smb06 1 \n", + "23 Except that they clearly never tried to incent... aenis \n", + "24 GiraffeNecktie 249 \n", + "\n", + " timestamp type \n", + "0 2021-06-26 02:37:56+00:00 comment \n", + "1 2023-04-19 15:56:34+00:00 comment \n", + "2 2018-10-16 01:07:55+00:00 comment \n", + "3 2015-08-10 16:05:54+00:00 comment \n", + "4 2020-10-05 11:20:51+00:00 comment \n", + "5 2020-09-01 15:38:50+00:00 comment \n", + "6 2011-09-27 22:43:27+00:00 story \n", + "7 2011-09-16 19:02:01+00:00 comment \n", + "8 2012-10-15 13:09:32+00:00 story \n", + "9 2023-04-21 16:45:13+00:00 comment \n", + "10 2023-01-19 20:05:54+00:00 comment \n", + "11 2019-06-17 19:49:17+00:00 comment \n", + "12 2018-03-18 18:57:21+00:00 comment \n", + "13 2019-10-22 10:41:01+00:00 comment \n", + "14 2017-05-05 20:45:37+00:00 comment \n", + "15 2024-07-12 18:39:52+00:00 story \n", + "16 2017-09-12 18:58:20+00:00 comment \n", + "17 2021-04-13 01:24:03+00:00 comment \n", + "18 2022-05-13 16:08:38+00:00 comment \n", + "19 2017-09-28 05:16:06+00:00 comment \n", + "20 2018-12-07 11:25:52+00:00 comment \n", + "21 2014-04-08 13:29:50+00:00 story \n", + "22 2017-02-16 23:26:34+00:00 story \n", + "23 2022-01-31 17:08:57+00:00 comment \n", + "24 2011-04-19 14:25:17+00:00 story \n", + "...\n", + "\n", + "[3000 rows x 6 columns]" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hacker_news = bpd.read_gbq(\"bigquery-public-data.hacker_news.full\")[['title', 'text', 'by', 'score', 'timestamp', 'type']].head(3000)\n", + "hacker_news" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Query job bff6ac16-2641-495a-a624-11883435e06c is DONE. 54.2 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/core/__init__.py:112: PreviewWarning: Interpreting JSON column(s) as StringDtype. This behavior may change in future versions.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job aa390314-5b6e-4c7e-9502-8497dc27429a is DONE. 5.9 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/sycai/src/python-bigquery-dataframes/bigframes/ml/llm.py:976: RuntimeWarning: Some predictions failed. Check column ml_generate_text_status for detailed status. You may want to filter the failed rows and retry.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "Query job 3591e287-5c45-4ec2-a11b-afe95fd956e3 is DONE. 1.2 MB processed. Open Job" ], "text/plain": [ - " Animals\n", - "0 Dog\n", - "1 Cat\n", + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Query job b0801c71-d44a-498a-ab3a-6302de52729f is DONE. 45.7 kB processed. Open Job" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
titletextbyscoretimestamptype
24Working Best at Coffee Shops<NA>GiraffeNecktie2492011-04-19 14:25:17+00:00story
96<NA>i resisted switching to chrome for months beca...catshirt<NA>2011-04-06 08:02:24+00:00comment
106<NA>I was about to say the same thing myself. For ...geophile<NA>2011-12-08 21:13:08+00:00comment
184<NA>I think it&#x27;s more than hazing. It may be ...bayesianhorse<NA>2015-06-18 16:42:53+00:00comment
223<NA>I don&#x27;t understand why a beginner would s...wolco<NA>2019-02-03 14:35:43+00:00comment
284<NA>I leaerned more with one minute of this than a...agumonkey<NA>2016-07-16 06:19:39+00:00comment
297<NA>I've suggested a <i>rationale</i> for the tabo...mechanical_fish<NA>2008-12-17 04:42:02+00:00comment
306<NA>Do you have any reference for this?<p>I&#x27;m...banashark<NA>2023-11-13 19:57:00+00:00comment
316<NA>Default search scope is an option in the Finde...kitsunesoba<NA>2017-08-13 17:15:19+00:00comment
386<NA>Orthogonality and biology aren&#x27;t friends.agumonkey<NA>2016-04-24 16:33:41+00:00comment
391<NA>I chose some random physics book that was good...prawn<NA>2011-03-27 22:29:51+00:00comment
417<NA>Seeing this get huge on Twitter. It&#x27;s the...shenanigoat<NA>2016-01-09 03:04:22+00:00comment
421<NA>Looking through the comments there are a numbe...moomin<NA>2024-10-01 14:37:04+00:00comment
422<NA>Legacy media is a tough business. GBTC is payi...arcticbull<NA>2021-04-16 16:30:33+00:00comment
429<NA>Same thing if you sell unsafe food, yet we hav...jabradoodle<NA>2023-08-03 20:47:52+00:00comment
431<NA>There was briefly a thing called HSCSD (&quot;...LeoPanthera<NA>2019-02-11 19:49:29+00:00comment
439<NA>&gt; This article is a bit comical to read and...lapcat<NA>2023-01-02 16:00:49+00:00comment
446<NA>Large positions are most likely sold off in sm...meowkit<NA>2021-01-27 23:22:48+00:00comment
500<NA>A US-based VPN (or really any VPN) is only goi...RandomBacon<NA>2019-04-05 00:58:58+00:00comment
533<NA><a href=\"https:&#x2F;&#x2F;codeberg.org&#x2F;A...ElectronBadger<NA>2023-12-13 08:13:15+00:00comment
589<NA>&gt; To me, a point of view is to do with your...dragonwriter<NA>2019-02-13 23:05:50+00:00comment
601<NA>So by using ADMIN_SL0T instead was it just set...minitoar<NA>2021-03-05 16:07:56+00:00comment
616<NA>I completely agree that this sets a bad preced...save_ferris<NA>2019-05-08 14:55:22+00:00comment
634<NA>How are guitar playing skills useful if you do...Yajirobe<NA>2019-06-14 10:18:19+00:00comment
647<NA>Outstanding!cafard<NA>2022-06-09 09:51:54+00:00comment
\n", + "

25 rows × 6 columns

\n", + "
[121 rows x 6 columns in total]" + ], + "text/plain": [ + " title \\\n", + "24 Working Best at Coffee Shops \n", + "96 \n", + "106 \n", + "184 \n", + "223 \n", + "284 \n", + "297 \n", + "306 \n", + "316 \n", + "386 \n", + "391 \n", + "417 \n", + "421 \n", + "422 \n", + "429 \n", + "431 \n", + "439 \n", + "446 \n", + "500 \n", + "533 \n", + "589 \n", + "601 \n", + "616 \n", + "634 \n", + "647 \n", + "\n", + " text by \\\n", + "24 GiraffeNecktie \n", + "96 i resisted switching to chrome for months beca... catshirt \n", + "106 I was about to say the same thing myself. For ... geophile \n", + "184 I think it's more than hazing. It may be ... bayesianhorse \n", + "223 I don't understand why a beginner would s... wolco \n", + "284 I leaerned more with one minute of this than a... agumonkey \n", + "297 I've suggested a rationale for the tabo... mechanical_fish \n", + "306 Do you have any reference for this?

I'm... banashark \n", + "316 Default search scope is an option in the Finde... kitsunesoba \n", + "386 Orthogonality and biology aren't friends. agumonkey \n", + "391 I chose some random physics book that was good... prawn \n", + "417 Seeing this get huge on Twitter. It's the... shenanigoat \n", + "421 Looking through the comments there are a numbe... moomin \n", + "422 Legacy media is a tough business. GBTC is payi... arcticbull \n", + "429 Same thing if you sell unsafe food, yet we hav... jabradoodle \n", + "431 There was briefly a thing called HSCSD ("... LeoPanthera \n", + "439 > This article is a bit comical to read and... lapcat \n", + "446 Large positions are most likely sold off in sm... meowkit \n", + "500 A US-based VPN (or really any VPN) is only goi... RandomBacon \n", + "533 2011-04-06 08:02:24+00:00 comment \n", + "106 2011-12-08 21:13:08+00:00 comment \n", + "184 2015-06-18 16:42:53+00:00 comment \n", + "223 2019-02-03 14:35:43+00:00 comment \n", + "284 2016-07-16 06:19:39+00:00 comment \n", + "297 2008-12-17 04:42:02+00:00 comment \n", + "306 2023-11-13 19:57:00+00:00 comment \n", + "316 2017-08-13 17:15:19+00:00 comment \n", + "386 2016-04-24 16:33:41+00:00 comment \n", + "391 2011-03-27 22:29:51+00:00 comment \n", + "417 2016-01-09 03:04:22+00:00 comment \n", + "421 2024-10-01 14:37:04+00:00 comment \n", + "422 2021-04-16 16:30:33+00:00 comment \n", + "429 2023-08-03 20:47:52+00:00 comment \n", + "431 2019-02-11 19:49:29+00:00 comment \n", + "439 2023-01-02 16:00:49+00:00 comment \n", + "446 2021-01-27 23:22:48+00:00 comment \n", + "500 2019-04-05 00:58:58+00:00 comment \n", + "533 2023-12-13 08:13:15+00:00 comment \n", + "589 2019-02-13 23:05:50+00:00 comment \n", + "601 2021-03-05 16:07:56+00:00 comment \n", + "616 2019-05-08 14:55:22+00:00 comment \n", + "634 2019-06-14 10:18:19+00:00 comment \n", + "647 2022-06-09 09:51:54+00:00 comment \n", + "...\n", + "\n", + "[121 rows x 6 columns]" ] }, - "execution_count": 4, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df = bpd.DataFrame({\"Animals\": [\"Dog\", \"Cat\", \"Bird\", \"Horse\"]})\n", - "df.semantics.top_k(\"{Animals} are more popular as pets\", model=gemini_model, k=2)" + "hacker_news.semantics.filter(\"{by} contains animal name\", model=gemini_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here are my performance numbers:\n", + "* 3000 rows -> 6m 9.2s\n", + "* 10000 rows -> 26m 42.4s" ] } ], From 103e99823d442a36b2aaa5113950b988f6d3ba1e Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 10 Oct 2024 18:24:32 -0700 Subject: [PATCH 907/921] fix: Escape ids more consistently in ml module (#1074) --- bigframes/core/utils.py | 4 +- bigframes/ml/compose.py | 17 +++-- bigframes/ml/core.py | 4 +- bigframes/ml/impute.py | 10 ++- bigframes/ml/preprocessing.py | 26 +++++--- bigframes/ml/sql.py | 87 +++++++++++++----------- tests/system/large/ml/test_compose.py | 20 +++--- tests/unit/core/test_bf_utils.py | 6 +- tests/unit/ml/test_compose.py | 30 ++++----- tests/unit/ml/test_golden_sql.py | 16 ++--- tests/unit/ml/test_sql.py | 95 ++++++++++++++------------- 11 files changed, 173 insertions(+), 142 deletions(-) diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 43c05c6c839..e684ac55a4b 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -116,9 +116,9 @@ def label_to_identifier(label: typing.Hashable, strict: bool = False) -> str: """ # Column values will be loaded as null if the column name has spaces. # https://github.com/googleapis/python-bigquery/issues/1566 - identifier = str(label).replace(" ", "_") - + identifier = str(label) if strict: + identifier = str(label).replace(" ", "_") identifier = re.sub(r"[^a-zA-Z0-9_]", "", identifier) if not identifier: identifier = "id" diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 08c9761cc37..14cf12014f7 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -28,6 +28,7 @@ from google.cloud import bigquery from bigframes.core import log_adapter +import bigframes.core.compile.googlesql as sql_utils from bigframes.ml import base, core, globals, impute, preprocessing, utils import bigframes.pandas as bpd @@ -98,16 +99,11 @@ class SQLScalarColumnTransformer: def __init__(self, sql: str, target_column: str = "transformed_{0}"): super().__init__() self._sql = sql + # TODO: More robust unescaping self._target_column = target_column.replace("`", "") PLAIN_COLNAME_RX = re.compile("^[a-z][a-z0-9_]*$", re.IGNORECASE) - def escape(self, colname: str): - colname = colname.replace("`", "") - if self.PLAIN_COLNAME_RX.match(colname): - return colname - return f"`{colname}`" - def _compile_to_sql( self, X: bpd.DataFrame, columns: Optional[Iterable[str]] = None ) -> List[str]: @@ -115,8 +111,10 @@ def _compile_to_sql( columns = X.columns result = [] for column in columns: - current_sql = self._sql.format(self.escape(column)) - current_target_column = self.escape(self._target_column.format(column)) + current_sql = self._sql.format(sql_utils.identifier(column)) + current_target_column = sql_utils.identifier( + self._target_column.format(column) + ) result.append(f"{current_sql} AS {current_target_column}") return result @@ -239,6 +237,7 @@ def camel_to_snake(name): transformers_set.add( ( camel_to_snake(transformer_cls.__name__), + # TODO: This is very fragile, use real SQL parser *transformer_cls._parse_from_sql(transform_sql), # type: ignore ) ) @@ -253,7 +252,7 @@ def camel_to_snake(name): target_column = transform_col_dict["name"] sql_transformer = SQLScalarColumnTransformer( - transform_sql, target_column=target_column + transform_sql.strip(), target_column=target_column ) input_column_name = f"?{target_column}" transformers_set.add( diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 02ccc9d6a54..4bc61c50157 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -47,8 +47,10 @@ class BqmlModel(BaseBqml): def __init__(self, session: bigframes.Session, model: bigquery.Model): self._session = session self._model = model + model_ref = self._model.reference + assert model_ref is not None self._model_manipulation_sql_generator = ml_sql.ModelManipulationSqlGenerator( - self.model_name + model_ref ) def _apply_ml_tvf( diff --git a/bigframes/ml/impute.py b/bigframes/ml/impute.py index 4955eb5de52..dddade8cc5a 100644 --- a/bigframes/ml/impute.py +++ b/bigframes/ml/impute.py @@ -80,7 +80,7 @@ def _parse_from_sql(cls, sql: str) -> tuple[SimpleImputer, str]: tuple(SimpleImputer, column_label)""" s = sql[sql.find("(") + 1 : sql.find(")")] col_label, strategy = s.split(", ") - return cls(strategy[1:-1]), col_label # type: ignore[arg-type] + return cls(strategy[1:-1]), _unescape_id(col_label) # type: ignore[arg-type] def fit( self, @@ -110,3 +110,11 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: bpd.DataFrame, df[self._output_names], ) + + +def _unescape_id(id: str) -> str: + """Very simple conversion to removed ` characters from ids. + + A proper sql parser should be used instead. + """ + return id.removeprefix("`").removesuffix("`") diff --git a/bigframes/ml/preprocessing.py b/bigframes/ml/preprocessing.py index 2c327f63f8e..eb53904a785 100644 --- a/bigframes/ml/preprocessing.py +++ b/bigframes/ml/preprocessing.py @@ -76,7 +76,7 @@ def _parse_from_sql(cls, sql: str) -> tuple[StandardScaler, str]: Returns: tuple(StandardScaler, column_label)""" col_label = sql[sql.find("(") + 1 : sql.find(")")] - return cls(), col_label + return cls(), _unescape_id(col_label) def fit( self, @@ -152,8 +152,9 @@ def _parse_from_sql(cls, sql: str) -> tuple[MaxAbsScaler, str]: Returns: tuple(MaxAbsScaler, column_label)""" + # TODO: Use real sql parser col_label = sql[sql.find("(") + 1 : sql.find(")")] - return cls(), col_label + return cls(), _unescape_id(col_label) def fit( self, @@ -229,8 +230,9 @@ def _parse_from_sql(cls, sql: str) -> tuple[MinMaxScaler, str]: Returns: tuple(MinMaxScaler, column_label)""" + # TODO: Use real sql parser col_label = sql[sql.find("(") + 1 : sql.find(")")] - return cls(), col_label + return cls(), _unescape_id(col_label) def fit( self, @@ -349,11 +351,11 @@ def _parse_from_sql(cls, sql: str) -> tuple[KBinsDiscretizer, str]: if sql.startswith("ML.QUANTILE_BUCKETIZE"): num_bins = s.split(",")[1] - return cls(int(num_bins), "quantile"), col_label + return cls(int(num_bins), "quantile"), _unescape_id(col_label) else: array_split_points = s[s.find("[") + 1 : s.find("]")] n_bins = array_split_points.count(",") + 2 - return cls(n_bins, "uniform"), col_label + return cls(n_bins, "uniform"), _unescape_id(col_label) def fit( self, @@ -469,7 +471,7 @@ def _parse_from_sql(cls, sql: str) -> tuple[OneHotEncoder, str]: max_categories = int(top_k) + 1 min_frequency = int(frequency_threshold) - return cls(drop, min_frequency, max_categories), col_label + return cls(drop, min_frequency, max_categories), _unescape_id(col_label) def fit( self, @@ -578,7 +580,7 @@ def _parse_from_sql(cls, sql: str) -> tuple[LabelEncoder, str]: max_categories = int(top_k) + 1 min_frequency = int(frequency_threshold) - return cls(min_frequency, max_categories), col_label + return cls(min_frequency, max_categories), _unescape_id(col_label) def fit( self, @@ -661,7 +663,7 @@ def _parse_from_sql(cls, sql: str) -> tuple[PolynomialFeatures, tuple[str, ...]] col_labels = sql[sql.find("STRUCT(") + 7 : sql.find(")")].split(",") col_labels = [label.strip() for label in col_labels] degree = int(sql[sql.rfind(",") + 1 : sql.rfind(")")]) - return cls(degree), tuple(col_labels) + return cls(degree), tuple(map(_unescape_id, col_labels)) def fit( self, @@ -694,6 +696,14 @@ def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: ) +def _unescape_id(id: str) -> str: + """Very simple conversion to removed ` characters from ids. + + A proper sql parser should be used instead. + """ + return id.removeprefix("`").removesuffix("`") + + PreprocessingType = Union[ OneHotEncoder, StandardScaler, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index 1cb327f19c8..b7d550ac635 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -21,6 +21,9 @@ import bigframes_vendored.constants as constants import google.cloud.bigquery +import bigframes.core.compile.googlesql as sql_utils +import bigframes.core.sql as sql_vals + # TODO: Add proper escaping logic from core/compile module class BaseSqlGenerator: @@ -29,10 +32,8 @@ class BaseSqlGenerator: # General methods def encode_value(self, v: Union[str, int, float, Iterable[str]]) -> str: """Encode a parameter value for SQL""" - if isinstance(v, str): - return f'"{v}"' - elif isinstance(v, int) or isinstance(v, float): - return f"{v}" + if isinstance(v, (str, int, float)): + return sql_vals.simple_literal(v) elif isinstance(v, Iterable): inner = ", ".join([self.encode_value(x) for x in v]) return f"[{inner}]" @@ -50,7 +51,10 @@ def build_parameters(self, **kwargs: Union[str, int, float, Iterable[str]]) -> s def build_structs(self, **kwargs: Union[int, float]) -> str: """Encode a dict of values into a formatted STRUCT items for SQL""" indent_str = " " - param_strs = [f"{v} AS {k}" for k, v in kwargs.items()] + param_strs = [ + f"{sql_vals.simple_literal(v)} AS {sql_utils.identifier(k)}" + for k, v in kwargs.items() + ] return "\n" + indent_str + f",\n{indent_str}".join(param_strs) def build_expressions(self, *expr_sqls: str) -> str: @@ -61,7 +65,7 @@ def build_expressions(self, *expr_sqls: str) -> str: def build_schema(self, **kwargs: str) -> str: """Encode a dict of values into a formatted schema type items for SQL""" indent_str = " " - param_strs = [f"{k} {v}" for k, v in kwargs.items()] + param_strs = [f"{sql_utils.identifier(k)} {v}" for k, v in kwargs.items()] return "\n" + indent_str + f",\n{indent_str}".join(param_strs) def options(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: @@ -74,7 +78,7 @@ def struct_options(self, **kwargs: Union[int, float]) -> str: def struct_columns(self, columns: Iterable[str]) -> str: """Encode a BQ Table columns to a STRUCT.""" - columns_str = ", ".join(columns) + columns_str = ", ".join(map(sql_utils.identifier, columns)) return f"STRUCT({columns_str})" def input(self, **kwargs: str) -> str: @@ -97,30 +101,30 @@ def transform(self, *expr_sqls: str) -> str: def ml_standard_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.STANDARD_SCALER for BQML""" - return f"""ML.STANDARD_SCALER({numeric_expr_sql}) OVER() AS {name}""" + return f"""ML.STANDARD_SCALER({sql_utils.identifier(numeric_expr_sql)}) OVER() AS {sql_utils.identifier(name)}""" def ml_max_abs_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.MAX_ABS_SCALER for BQML""" - return f"""ML.MAX_ABS_SCALER({numeric_expr_sql}) OVER() AS {name}""" + return f"""ML.MAX_ABS_SCALER({sql_utils.identifier(numeric_expr_sql)}) OVER() AS {sql_utils.identifier(name)}""" def ml_min_max_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.MIN_MAX_SCALER for BQML""" - return f"""ML.MIN_MAX_SCALER({numeric_expr_sql}) OVER() AS {name}""" + return f"""ML.MIN_MAX_SCALER({sql_utils.identifier(numeric_expr_sql)}) OVER() AS {sql_utils.identifier(name)}""" def ml_imputer( self, - expr_sql: str, + col_name: str, strategy: str, name: str, ) -> str: """Encode ML.IMPUTER for BQML""" - return f"""ML.IMPUTER({expr_sql}, '{strategy}') OVER() AS {name}""" + return f"""ML.IMPUTER({sql_utils.identifier(col_name)}, '{strategy}') OVER() AS {sql_utils.identifier(name)}""" def ml_bucketize( self, - numeric_expr_sql: str, + input_id: str, array_split_points: Iterable[Union[int, float]], - name: str, + output_id: str, ) -> str: """Encode ML.BUCKETIZE for BQML""" # Use Python value rather than Numpy value to serialization. @@ -128,7 +132,7 @@ def ml_bucketize( point.item() if hasattr(point, "item") else point for point in array_split_points ] - return f"""ML.BUCKETIZE({numeric_expr_sql}, {points}, FALSE) AS {name}""" + return f"""ML.BUCKETIZE({sql_utils.identifier(input_id)}, {points}, FALSE) AS {sql_utils.identifier(output_id)}""" def ml_quantile_bucketize( self, @@ -137,7 +141,7 @@ def ml_quantile_bucketize( name: str, ) -> str: """Encode ML.QUANTILE_BUCKETIZE for BQML""" - return f"""ML.QUANTILE_BUCKETIZE({numeric_expr_sql}, {num_bucket}) OVER() AS {name}""" + return f"""ML.QUANTILE_BUCKETIZE({sql_utils.identifier(numeric_expr_sql)}, {num_bucket}) OVER() AS {sql_utils.identifier(name)}""" def ml_one_hot_encoder( self, @@ -149,7 +153,7 @@ def ml_one_hot_encoder( ) -> str: """Encode ML.ONE_HOT_ENCODER for BQML. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder for params.""" - return f"""ML.ONE_HOT_ENCODER({numeric_expr_sql}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {name}""" + return f"""ML.ONE_HOT_ENCODER({sql_utils.identifier(numeric_expr_sql)}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {sql_utils.identifier(name)}""" def ml_label_encoder( self, @@ -160,14 +164,14 @@ def ml_label_encoder( ) -> str: """Encode ML.LABEL_ENCODER for BQML. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-label-encoder for params.""" - return f"""ML.LABEL_ENCODER({numeric_expr_sql}, {top_k}, {frequency_threshold}) OVER() AS {name}""" + return f"""ML.LABEL_ENCODER({sql_utils.identifier(numeric_expr_sql)}, {top_k}, {frequency_threshold}) OVER() AS {sql_utils.identifier(name)}""" def ml_polynomial_expand( self, columns: Iterable[str], degree: int, name: str ) -> str: """Encode ML.POLYNOMIAL_EXPAND. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-polynomial-expand""" - return f"""ML.POLYNOMIAL_EXPAND({self.struct_columns(columns)}, {degree}) AS {name}""" + return f"""ML.POLYNOMIAL_EXPAND({self.struct_columns(columns)}, {degree}) AS {sql_utils.identifier(name)}""" def ml_distance( self, @@ -179,7 +183,7 @@ def ml_distance( ) -> str: """Encode ML.DISTANCE for BQML. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-distance""" - return f"""SELECT *, ML.DISTANCE({col_x}, {col_y}, '{type}') AS {name} FROM ({source_sql})""" + return f"""SELECT *, ML.DISTANCE({sql_utils.identifier(col_x)}, {sql_utils.identifier(col_y)}, '{type}') AS {sql_utils.identifier(name)} FROM ({source_sql})""" class ModelCreationSqlGenerator(BaseSqlGenerator): @@ -189,7 +193,7 @@ def _model_id_sql( self, model_ref: google.cloud.bigquery.ModelReference, ): - return f"`{model_ref.project}`.`{model_ref.dataset_id}`.`{model_ref.model_id}`" + return f"{sql_utils.identifier(model_ref.project)}.{sql_utils.identifier(model_ref.dataset_id)}.{sql_utils.identifier(model_ref.model_id)}" # Model create and alter def create_model( @@ -276,8 +280,11 @@ def create_xgboost_imported_model( class ModelManipulationSqlGenerator(BaseSqlGenerator): """Sql generator for manipulating a model entity. Model name is the full model path of project_id.dataset_id.model_id.""" - def __init__(self, model_name: str): - self._model_name = model_name + def __init__(self, model_ref: google.cloud.bigquery.ModelReference): + self._model_ref = model_ref + + def _model_ref_sql(self) -> str: + return f"{sql_utils.identifier(self._model_ref.project)}.{sql_utils.identifier(self._model_ref.dataset_id)}.{sql_utils.identifier(self._model_ref.model_id)}" # Alter model def alter_model( @@ -287,20 +294,20 @@ def alter_model( """Encode the ALTER MODEL statement for BQML""" options_sql = self.options(**options) - parts = [f"ALTER MODEL `{self._model_name}`"] + parts = [f"ALTER MODEL {self._model_ref_sql()}"] parts.append(f"SET {options_sql}") return "\n".join(parts) # ML prediction TVFs def ml_predict(self, source_sql: str) -> str: """Encode ML.PREDICT for BQML""" - return f"""SELECT * FROM ML.PREDICT(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.PREDICT(MODEL {self._model_ref_sql()}, ({source_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) - return f"""SELECT * FROM ML.FORECAST(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.FORECAST(MODEL {self._model_ref_sql()}, {struct_options_sql})""" def ml_generate_text( @@ -308,7 +315,7 @@ def ml_generate_text( ) -> str: """Encode ML.GENERATE_TEXT for BQML""" struct_options_sql = self.struct_options(**struct_options) - return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.GENERATE_TEXT(MODEL {self._model_ref_sql()}, ({source_sql}), {struct_options_sql})""" def ml_generate_embedding( @@ -316,7 +323,7 @@ def ml_generate_embedding( ) -> str: """Encode ML.GENERATE_EMBEDDING for BQML""" struct_options_sql = self.struct_options(**struct_options) - return f"""SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.GENERATE_EMBEDDING(MODEL {self._model_ref_sql()}, ({source_sql}), {struct_options_sql})""" def ml_detect_anomalies( @@ -324,51 +331,51 @@ def ml_detect_anomalies( ) -> str: """Encode ML.DETECT_ANOMALIES for BQML""" struct_options_sql = self.struct_options(**struct_options) - return f"""SELECT * FROM ML.DETECT_ANOMALIES(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.DETECT_ANOMALIES(MODEL {self._model_ref_sql()}, {struct_options_sql}, ({source_sql}))""" # ML evaluation TVFs def ml_evaluate(self, source_sql: Optional[str] = None) -> str: """Encode ML.EVALUATE for BQML""" if source_sql is None: - return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`)""" + return f"""SELECT * FROM ML.EVALUATE(MODEL {self._model_ref_sql()})""" else: - return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.EVALUATE(MODEL {self._model_ref_sql()}, ({source_sql}))""" def ml_arima_coefficients(self) -> str: """Encode ML.ARIMA_COEFFICIENTS for BQML""" - return f"""SELECT * FROM ML.ARIMA_COEFFICIENTS(MODEL `{self._model_name}`)""" + return f"""SELECT * FROM ML.ARIMA_COEFFICIENTS(MODEL {self._model_ref_sql()})""" # ML evaluation TVFs def ml_llm_evaluate(self, source_sql: str, task_type: Optional[str] = None) -> str: """Encode ML.EVALUATE for BQML""" # Note: don't need index as evaluate returns a new table - return f"""SELECT * FROM ML.EVALUATE(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.EVALUATE(MODEL {self._model_ref_sql()}, ({source_sql}), STRUCT("{task_type}" AS task_type))""" # ML evaluation TVFs def ml_arima_evaluate(self, show_all_candidate_models: bool = False) -> str: """Encode ML.ARMIA_EVALUATE for BQML""" - return f"""SELECT * FROM ML.ARIMA_EVALUATE(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.ARIMA_EVALUATE(MODEL {self._model_ref_sql()}, STRUCT({show_all_candidate_models} AS show_all_candidate_models))""" def ml_centroids(self) -> str: """Encode ML.CENTROIDS for BQML""" - return f"""SELECT * FROM ML.CENTROIDS(MODEL `{self._model_name}`)""" + return f"""SELECT * FROM ML.CENTROIDS(MODEL {self._model_ref_sql()})""" def ml_principal_components(self) -> str: """Encode ML.PRINCIPAL_COMPONENTS for BQML""" - return f"""SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `{self._model_name}`)""" + return ( + f"""SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL {self._model_ref_sql()})""" + ) def ml_principal_component_info(self) -> str: """Encode ML.PRINCIPAL_COMPONENT_INFO for BQML""" - return ( - f"""SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `{self._model_name}`)""" - ) + return f"""SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL {self._model_ref_sql()})""" # ML transform TVF, that require a transform_only type model def ml_transform(self, source_sql: str) -> str: """Encode ML.TRANSFORM for BQML""" - return f"""SELECT * FROM ML.TRANSFORM(MODEL `{self._model_name}`, + return f"""SELECT * FROM ML.TRANSFORM(MODEL {self._model_ref_sql()}, ({source_sql}))""" diff --git a/tests/system/large/ml/test_compose.py b/tests/system/large/ml/test_compose.py index ba963837e53..cbc702018a4 100644 --- a/tests/system/large/ml/test_compose.py +++ b/tests/system/large/ml/test_compose.py @@ -90,12 +90,14 @@ def test_columntransformer_standalone_fit_and_transform( def test_columntransformer_standalone_fit_transform(new_penguins_df): + # rename column to ensure robustness to column names that must be escaped + new_penguins_df = new_penguins_df.rename(columns={"species": "123 'species'"}) transformer = compose.ColumnTransformer( [ ( "onehot", preprocessing.OneHotEncoder(), - "species", + "123 'species'", ), ( "standard_scale", @@ -108,7 +110,7 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): "CASE WHEN {0} IS NULL THEN -1 ELSE LENGTH({0}) END", target_column="len_{0}", ), - "species", + "123 'species'", ), ( "identity", @@ -119,16 +121,16 @@ def test_columntransformer_standalone_fit_transform(new_penguins_df): ) result = transformer.fit_transform( - new_penguins_df[["species", "culmen_length_mm", "flipper_length_mm"]] + new_penguins_df[["123 'species'", "culmen_length_mm", "flipper_length_mm"]] ).to_pandas() utils.check_pandas_df_schema_and_index( result, columns=[ - "onehotencoded_species", + "onehotencoded_123 'species'", "standard_scaled_culmen_length_mm", "standard_scaled_flipper_length_mm", - "len_species", + "len_123 'species'", "culmen_length_mm", "flipper_length_mm", ], @@ -194,7 +196,7 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): ( "sql_scalar_column_transformer", compose.SQLScalarColumnTransformer( - "CASE WHEN species IS NULL THEN -1 ELSE LENGTH(species) END", + "CASE WHEN `species` IS NULL THEN -1 ELSE LENGTH(`species`) END", target_column="len_species", ), "?len_species", @@ -202,21 +204,21 @@ def test_columntransformer_save_load(new_penguins_df, dataset_id): ( "sql_scalar_column_transformer", compose.SQLScalarColumnTransformer( - "flipper_length_mm", target_column="flipper_length_mm" + "`flipper_length_mm`", target_column="flipper_length_mm" ), "?flipper_length_mm", ), ( "sql_scalar_column_transformer", compose.SQLScalarColumnTransformer( - "culmen_length_mm", target_column="culmen_length_mm" + "`culmen_length_mm`", target_column="culmen_length_mm" ), "?culmen_length_mm", ), ( "sql_scalar_column_transformer", compose.SQLScalarColumnTransformer( - "CASE WHEN species IS NULL THEN -1 ELSE LENGTH(species) END ", + "CASE WHEN `species` IS NULL THEN -1 ELSE LENGTH(`species`) END", target_column="Flex species Name", ), "?Flex species Name", diff --git a/tests/unit/core/test_bf_utils.py b/tests/unit/core/test_bf_utils.py index 10ce1fd09e3..248b6796e2c 100644 --- a/tests/unit/core/test_bf_utils.py +++ b/tests/unit/core/test_bf_utils.py @@ -26,7 +26,7 @@ def test_get_standardized_ids_columns(): utils.UNNAMED_COLUMN_ID, "duplicate", "duplicate_1", - "with_space", + "with space", ] assert idx_ids == [] @@ -35,7 +35,7 @@ def test_get_standardized_ids_indexes(): col_labels = ["duplicate"] idx_labels = ["string", 0, None, "duplicate", "duplicate", "with space"] - col_ids, idx_ids = utils.get_standardized_ids(col_labels, idx_labels) + col_ids, idx_ids = utils.get_standardized_ids(col_labels, idx_labels, strict=True) assert col_ids == ["duplicate_2"] assert idx_ids == [ @@ -53,4 +53,4 @@ def test_get_standardized_ids_tuple(): col_ids, _ = utils.get_standardized_ids(col_labels) - assert col_ids == ["('foo',_1)", "('foo',_2)", "('bar',_1)"] + assert col_ids == ["('foo', 1)", "('foo', 2)", "('bar', 1)"] diff --git a/tests/unit/ml/test_compose.py b/tests/unit/ml/test_compose.py index 7643f76e56b..395296f3e4d 100644 --- a/tests/unit/ml/test_compose.py +++ b/tests/unit/ml/test_compose.py @@ -258,8 +258,8 @@ def test_customtransformer_compile_sql(mock_X): ident_trafo = SQLScalarColumnTransformer("{0}", target_column="ident_{0}") sqls = ident_trafo._compile_to_sql(X=mock_X, columns=["col1", "col2"]) assert sqls == [ - "col1 AS ident_col1", - "col2 AS ident_col2", + "`col1` AS `ident_col1`", + "`col2` AS `ident_col2`", ] len1_trafo = SQLScalarColumnTransformer( @@ -267,8 +267,8 @@ def test_customtransformer_compile_sql(mock_X): ) sqls = len1_trafo._compile_to_sql(X=mock_X, columns=["col1", "col2"]) assert sqls == [ - "CASE WHEN col1 IS NULL THEN -5 ELSE LENGTH(col1) END AS len1_col1", - "CASE WHEN col2 IS NULL THEN -5 ELSE LENGTH(col2) END AS len1_col2", + "CASE WHEN `col1` IS NULL THEN -5 ELSE LENGTH(`col1`) END AS `len1_col1`", + "CASE WHEN `col2` IS NULL THEN -5 ELSE LENGTH(`col2`) END AS `len1_col2`", ] len2_trafo = SQLScalarColumnTransformer( @@ -276,8 +276,8 @@ def test_customtransformer_compile_sql(mock_X): ) sqls = len2_trafo._compile_to_sql(X=mock_X, columns=["col1", "col2"]) assert sqls == [ - "CASE WHEN col1 IS NULL THEN 99 ELSE LENGTH(col1) END AS len2_col1", - "CASE WHEN col2 IS NULL THEN 99 ELSE LENGTH(col2) END AS len2_col2", + "CASE WHEN `col1` IS NULL THEN 99 ELSE LENGTH(`col1`) END AS `len2_col1`", + "CASE WHEN `col2` IS NULL THEN 99 ELSE LENGTH(`col2`) END AS `len2_col2`", ] @@ -524,11 +524,11 @@ def test_columntransformer_compile_to_sql(mock_X): ) sqls = column_transformer._compile_to_sql(mock_X) assert sqls == [ - "culmen_length_mm AS ident_culmen_length_mm", - "flipper_length_mm AS ident_flipper_length_mm", - "CASE WHEN species IS NULL THEN -2 ELSE LENGTH(species) END AS len1_species", - "CASE WHEN species IS NULL THEN 99 ELSE LENGTH(species) END AS len2_species", - "ML.LABEL_ENCODER(species, 1000000, 0) OVER() AS labelencoded_species", + "`culmen_length_mm` AS `ident_culmen_length_mm`", + "`flipper_length_mm` AS `ident_flipper_length_mm`", + "CASE WHEN `species` IS NULL THEN -2 ELSE LENGTH(`species`) END AS `len1_species`", + "CASE WHEN `species` IS NULL THEN 99 ELSE LENGTH(`species`) END AS `len2_species`", + "ML.LABEL_ENCODER(`species`, 1000000, 0) OVER() AS `labelencoded_species`", ] @@ -548,13 +548,13 @@ def test_columntransformer_flexible_column_names(mock_X): ["culmen_length_mm", "flipper_length_mm"], ), ("len1_trafo", len1_transformer, ["species shortname"]), - ("len2_trafo", len2_transformer, ["`species longname`"]), + ("len2_trafo", len2_transformer, ["species longname"]), ] ) sqls = column_transformer._compile_to_sql(mock_X) assert sqls == [ - "culmen_length_mm AS `ident culmen_length_mm`", - "flipper_length_mm AS `ident flipper_length_mm`", + "`culmen_length_mm` AS `ident culmen_length_mm`", + "`flipper_length_mm` AS `ident flipper_length_mm`", "CASE WHEN `species shortname` IS NULL THEN -2 ELSE LENGTH(`species shortname`) END AS `len1_species shortname`", "CASE WHEN `species longname` IS NULL THEN 99 ELSE LENGTH(`species longname`) END AS `len2_species longname`", ] @@ -576,6 +576,6 @@ def test_columntransformer_extract_from_bq_model_flexnames(bq_model_flexnames): SQLScalarColumnTransformer(sql='culmen_length_mm', target_column='Flex Name culmen_length_mm'), '?Flex Name culmen_length_mm'), ('sql_scalar_column_transformer', - SQLScalarColumnTransformer(sql='flipper_length_mm ', target_column='Flex Name flipper_length_mm'), + SQLScalarColumnTransformer(sql='flipper_length_mm', target_column='Flex Name flipper_length_mm'), '?Flex Name flipper_length_mm')])""" assert expected == actual diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index aa7e919b24c..65f079852e7 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -106,7 +106,7 @@ def test_linear_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LINEAR_REG',\n data_split_method='NO_SPLIT',\n optimize_strategy='auto_strategy',\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_sql" ) @@ -116,7 +116,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LINEAR_REG",\n data_split_method="NO_SPLIT",\n optimize_strategy="auto_strategy",\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LINEAR_REG',\n data_split_method='NO_SPLIT',\n optimize_strategy='auto_strategy',\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_sql" ) @@ -126,7 +126,7 @@ def test_linear_regression_predict(mock_session, bqml_model, mock_X): model.predict(mock_X) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_id`,\n (input_X_sql))", + "SELECT * FROM ML.PREDICT(MODEL `model_project`.`model_dataset`.`model_id`,\n (input_X_sql))", index_col=["index_column_id"], ) @@ -137,7 +137,7 @@ def test_linear_regression_score(mock_session, bqml_model, mock_X, mock_y): model.score(mock_X, mock_y) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_id`,\n (input_X_y_sql))" + "SELECT * FROM ML.EVALUATE(MODEL `model_project`.`model_dataset`.`model_id`,\n (input_X_y_sql))" ) @@ -149,7 +149,7 @@ def test_logistic_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=True,\n auto_class_weights=False,\n optimize_strategy="auto_strategy",\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy="line_search",\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LOGISTIC_REG',\n data_split_method='NO_SPLIT',\n fit_intercept=True,\n auto_class_weights=False,\n optimize_strategy='auto_strategy',\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_sql" ) @@ -171,7 +171,7 @@ def test_logistic_regression_params_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - 'CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type="LOGISTIC_REG",\n data_split_method="NO_SPLIT",\n fit_intercept=False,\n auto_class_weights=True,\n optimize_strategy="batch_gradient_descent",\n l2_reg=0.2,\n max_iterations=30,\n learn_rate_strategy="constant",\n min_rel_progress=0.02,\n calculate_p_values=False,\n enable_global_explain=False,\n l1_reg=0.2,\n learn_rate=0.2,\n INPUT_LABEL_COLS=["input_column_label"])\nAS input_X_y_sql' + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LOGISTIC_REG',\n data_split_method='NO_SPLIT',\n fit_intercept=False,\n auto_class_weights=True,\n optimize_strategy='batch_gradient_descent',\n l2_reg=0.2,\n max_iterations=30,\n learn_rate_strategy='constant',\n min_rel_progress=0.02,\n calculate_p_values=False,\n enable_global_explain=False,\n l1_reg=0.2,\n learn_rate=0.2,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_sql" ) @@ -181,7 +181,7 @@ def test_logistic_regression_predict(mock_session, bqml_model, mock_X): model.predict(mock_X) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.PREDICT(MODEL `model_project.model_dataset.model_id`,\n (input_X_sql))", + "SELECT * FROM ML.PREDICT(MODEL `model_project`.`model_dataset`.`model_id`,\n (input_X_sql))", index_col=["index_column_id"], ) @@ -192,5 +192,5 @@ def test_logistic_regression_score(mock_session, bqml_model, mock_X, mock_y): model.score(mock_X, mock_y) mock_session.read_gbq.assert_called_once_with( - "SELECT * FROM ML.EVALUATE(MODEL `model_project.model_dataset.model_id`,\n (input_X_y_sql))" + "SELECT * FROM ML.EVALUATE(MODEL `model_project`.`model_dataset`.`model_id`,\n (input_X_y_sql))" ) diff --git a/tests/unit/ml/test_sql.py b/tests/unit/ml/test_sql.py index cdf2d0b2e4a..ee0821dfe94 100644 --- a/tests/unit/ml/test_sql.py +++ b/tests/unit/ml/test_sql.py @@ -34,7 +34,9 @@ def model_creation_sql_generator() -> ml_sql.ModelCreationSqlGenerator: @pytest.fixture(scope="session") def model_manipulation_sql_generator() -> ml_sql.ModelManipulationSqlGenerator: return ml_sql.ModelManipulationSqlGenerator( - model_name="my_project_id.my_dataset_id.my_model_id" + model_ref=bigquery.ModelReference.from_string( + "my_project_id.my_dataset_id.my_model_id" + ) ) @@ -53,7 +55,7 @@ def test_ml_arima_coefficients( sql = model_manipulation_sql_generator.ml_arima_coefficients() assert ( sql - == """SELECT * FROM ML.ARIMA_COEFFICIENTS(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + == """SELECT * FROM ML.ARIMA_COEFFICIENTS(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`)""" ) @@ -64,8 +66,8 @@ def test_options_correct(base_sql_generator: ml_sql.BaseSqlGenerator): assert ( sql == """OPTIONS( - model_type="lin_reg", - input_label_cols=["col_a"], + model_type='lin_reg', + input_label_cols=['col_a'], l1_reg=0.6)""" ) @@ -89,42 +91,42 @@ def test_standard_scaler_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_standard_scaler("col_a", "scaled_col_a") - assert sql == "ML.STANDARD_SCALER(col_a) OVER() AS scaled_col_a" + assert sql == "ML.STANDARD_SCALER(`col_a`) OVER() AS `scaled_col_a`" def test_max_abs_scaler_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_max_abs_scaler("col_a", "scaled_col_a") - assert sql == "ML.MAX_ABS_SCALER(col_a) OVER() AS scaled_col_a" + assert sql == "ML.MAX_ABS_SCALER(`col_a`) OVER() AS `scaled_col_a`" def test_min_max_scaler_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_min_max_scaler("col_a", "scaled_col_a") - assert sql == "ML.MIN_MAX_SCALER(col_a) OVER() AS scaled_col_a" + assert sql == "ML.MIN_MAX_SCALER(`col_a`) OVER() AS `scaled_col_a`" def test_imputer_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_imputer("col_a", "mean", "scaled_col_a") - assert sql == "ML.IMPUTER(col_a, 'mean') OVER() AS scaled_col_a" + assert sql == "ML.IMPUTER(`col_a`, 'mean') OVER() AS `scaled_col_a`" def test_k_bins_discretizer_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_bucketize("col_a", [1, 2, 3, 4], "scaled_col_a") - assert sql == "ML.BUCKETIZE(col_a, [1, 2, 3, 4], FALSE) AS scaled_col_a" + assert sql == "ML.BUCKETIZE(`col_a`, [1, 2, 3, 4], FALSE) AS `scaled_col_a`" def test_k_bins_discretizer_quantile_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_quantile_bucketize("col_a", 5, "scaled_col_a") - assert sql == "ML.QUANTILE_BUCKETIZE(col_a, 5) OVER() AS scaled_col_a" + assert sql == "ML.QUANTILE_BUCKETIZE(`col_a`, 5) OVER() AS `scaled_col_a`" def test_one_hot_encoder_correct( @@ -134,7 +136,8 @@ def test_one_hot_encoder_correct( "col_a", "none", 1000000, 0, "encoded_col_a" ) assert ( - sql == "ML.ONE_HOT_ENCODER(col_a, 'none', 1000000, 0) OVER() AS encoded_col_a" + sql + == "ML.ONE_HOT_ENCODER(`col_a`, 'none', 1000000, 0) OVER() AS `encoded_col_a`" ) @@ -142,14 +145,14 @@ def test_label_encoder_correct( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_label_encoder("col_a", 1000000, 0, "encoded_col_a") - assert sql == "ML.LABEL_ENCODER(col_a, 1000000, 0) OVER() AS encoded_col_a" + assert sql == "ML.LABEL_ENCODER(`col_a`, 1000000, 0) OVER() AS `encoded_col_a`" def test_polynomial_expand( base_sql_generator: ml_sql.BaseSqlGenerator, ): sql = base_sql_generator.ml_polynomial_expand(["col_a", "col_b"], 2, "poly_exp") - assert sql == "ML.POLYNOMIAL_EXPAND(STRUCT(col_a, col_b), 2) AS poly_exp" + assert sql == "ML.POLYNOMIAL_EXPAND(STRUCT(`col_a`, `col_b`), 2) AS `poly_exp`" def test_create_model_correct( @@ -167,7 +170,7 @@ def test_create_model_correct( sql == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_model_correct_sql` OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2) AS input_X_y_sql""" ) @@ -195,7 +198,7 @@ def test_create_model_transform_correct( ML.STANDARD_SCALER(col_a) OVER(col_a) AS scaled_col_a, ML.ONE_HOT_ENCODER(col_b) OVER(col_b) AS encoded_col_b) OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2) AS input_X_y_sql""" ) @@ -218,7 +221,7 @@ def test_create_llm_remote_model_correct( == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_remote_model` REMOTE WITH CONNECTION `my_project.us.my_connection` OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2) AS input_X_y_sql""" ) @@ -239,7 +242,7 @@ def test_create_remote_model_correct( == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_remote_model` REMOTE WITH CONNECTION `my_project.us.my_connection` OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2)""" ) @@ -260,12 +263,12 @@ def test_create_remote_model_with_params_correct( sql == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_remote_model` INPUT( - column1 int64) + `column1` int64) OUTPUT( - result array) + `result` array) REMOTE WITH CONNECTION `my_project.us.my_connection` OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2)""" ) @@ -283,7 +286,7 @@ def test_create_imported_model_correct( sql == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_imported_model` OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2)""" ) @@ -303,11 +306,11 @@ def test_create_xgboost_imported_model_produces_correct_sql( sql == """CREATE OR REPLACE MODEL `test-proj`.`_anonXYZ`.`create_xgboost_imported_model` INPUT( - column1 int64) + `column1` int64) OUTPUT( - result array) + `result` array) OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2)""" ) @@ -320,9 +323,9 @@ def test_alter_model_correct_sql( ) assert ( sql - == """ALTER MODEL `my_project_id.my_dataset_id.my_model_id` + == """ALTER MODEL `my_project_id`.`my_dataset_id`.`my_model_id` SET OPTIONS( - option_key1="option_value1", + option_key1='option_value1', option_key2=2)""" ) @@ -334,7 +337,7 @@ def test_ml_predict_correct( sql = model_manipulation_sql_generator.ml_predict(source_sql=mock_df.sql) assert ( sql - == """SELECT * FROM ML.PREDICT(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.PREDICT(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, (input_X_y_sql))""" ) @@ -348,7 +351,7 @@ def test_ml_llm_evaluate_correct( ) assert ( sql - == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, (input_X_y_sql), STRUCT("CLASSIFICATION" AS task_type))""" ) @@ -360,7 +363,7 @@ def test_ml_evaluate_correct( sql = model_manipulation_sql_generator.ml_evaluate(source_sql=mock_df.sql) assert ( sql - == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, (input_X_y_sql))""" ) @@ -373,7 +376,7 @@ def test_ml_arima_evaluate_correct( ) assert ( sql - == """SELECT * FROM ML.ARIMA_EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.ARIMA_EVALUATE(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, STRUCT(True AS show_all_candidate_models))""" ) @@ -384,7 +387,7 @@ def test_ml_evaluate_no_source_correct( sql = model_manipulation_sql_generator.ml_evaluate() assert ( sql - == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + == """SELECT * FROM ML.EVALUATE(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`)""" ) @@ -394,7 +397,7 @@ def test_ml_centroids_correct( sql = model_manipulation_sql_generator.ml_centroids() assert ( sql - == """SELECT * FROM ML.CENTROIDS(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + == """SELECT * FROM ML.CENTROIDS(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`)""" ) @@ -406,10 +409,10 @@ def test_ml_forecast_correct_sql( ) assert ( sql - == """SELECT * FROM ML.FORECAST(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.FORECAST(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, STRUCT( - 1 AS option_key1, - 2.2 AS option_key2))""" + 1 AS `option_key1`, + 2.2 AS `option_key2`))""" ) @@ -423,10 +426,10 @@ def test_ml_generate_text_correct( ) assert ( sql - == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.GENERATE_TEXT(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, (input_X_y_sql), STRUCT( - 1 AS option_key1, - 2.2 AS option_key2))""" + 1 AS `option_key1`, + 2.2 AS `option_key2`))""" ) @@ -440,10 +443,10 @@ def test_ml_generate_embedding_correct( ) assert ( sql - == """SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, (input_X_y_sql), STRUCT( - 1 AS option_key1, - 2.2 AS option_key2))""" + 1 AS `option_key1`, + 2.2 AS `option_key2`))""" ) @@ -457,10 +460,10 @@ def test_ml_detect_anomalies_correct_sql( ) assert ( sql - == """SELECT * FROM ML.DETECT_ANOMALIES(MODEL `my_project_id.my_dataset_id.my_model_id`, + == """SELECT * FROM ML.DETECT_ANOMALIES(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`, STRUCT( - 1 AS option_key1, - 2.2 AS option_key2), (input_X_y_sql))""" + 1 AS `option_key1`, + 2.2 AS `option_key2`), (input_X_y_sql))""" ) @@ -470,7 +473,7 @@ def test_ml_principal_components_correct( sql = model_manipulation_sql_generator.ml_principal_components() assert ( sql - == """SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + == """SELECT * FROM ML.PRINCIPAL_COMPONENTS(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`)""" ) @@ -480,5 +483,5 @@ def test_ml_principal_component_info_correct( sql = model_manipulation_sql_generator.ml_principal_component_info() assert ( sql - == """SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `my_project_id.my_dataset_id.my_model_id`)""" + == """SELECT * FROM ML.PRINCIPAL_COMPONENT_INFO(MODEL `my_project_id`.`my_dataset_id`.`my_model_id`)""" ) From 2d16f6d1e9519e228533a67084000568a61c086e Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 10 Oct 2024 23:49:19 -0700 Subject: [PATCH 908/921] fix: Remove index requirement from some dataframe APIs (#1073) --- bigframes/bigquery/__init__.py | 2 +- bigframes/core/eval.py | 15 +++++--- bigframes/dataframe.py | 3 +- tests/system/small/test_null_index.py | 53 +++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 28a818e7092..847ed5eccb7 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -493,8 +493,8 @@ def vector_search( ) if index_col_ids is not None: df = query._session.read_gbq(sql, index_col=index_col_ids) + df.index.names = index_labels else: df = query._session.read_gbq(sql) - df.index.names = index_labels return df diff --git a/bigframes/core/eval.py b/bigframes/core/eval.py index 692ca1c7bb0..82add992589 100644 --- a/bigframes/core/eval.py +++ b/bigframes/core/eval.py @@ -38,12 +38,15 @@ def eval(df: dataframe.DataFrame, expr: str, target: Optional[dataframe.DataFram Returns: Result of evaluation. """ - index_resolver = { - vendored_pandas_eval_parsing.clean_column_name(str(name)): EvalSeries( - df.index.get_level_values(level).to_series() - ) - for level, name in enumerate(df.index.names) - } + if df._has_index: + index_resolver = { + vendored_pandas_eval_parsing.clean_column_name(str(name)): EvalSeries( + df.index.get_level_values(level).to_series() + ) + for level, name in enumerate(df.index.names) + } + else: + index_resolver = {} column_resolver = { vendored_pandas_eval_parsing.clean_column_name(str(name)): EvalSeries(series) for name, series in df.items() diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0cfa5a21541..0c50c2c749a 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -768,7 +768,7 @@ def _apply_series_binop_axis_0( reverse: bool = False, ) -> DataFrame: bf_series = bigframes.core.convert.to_bf_series( - other, self.index, self._session + other, self.index if self._has_index else None, self._session ) aligned_block, columns, expr_pairs = self._block._align_axis_0( bf_series._block, how=how @@ -3179,6 +3179,7 @@ def to_gbq( clustering_columns: Union[pandas.Index, Iterable[typing.Hashable]] = (), labels: dict[str, str] = {}, ) -> str: + index = index and self._has_index temp_table_ref = None if destination_table is None: diff --git a/tests/system/small/test_null_index.py b/tests/system/small/test_null_index.py index a1e360f73d5..c5be49a56ba 100644 --- a/tests/system/small/test_null_index.py +++ b/tests/system/small/test_null_index.py @@ -21,6 +21,23 @@ from tests.system.utils import skip_legacy_pandas +def test_null_index_to_gbq(session, scalars_df_null_index, dataset_id_not_created): + dataset_id = dataset_id_not_created + destination_table = f"{dataset_id}.scalars_df_unindexed" + + result_table = scalars_df_null_index.to_gbq( + destination_table, clustering_columns=["int64_col"] + ) + assert ( + result_table == destination_table + if destination_table + else result_table is not None + ) + + loaded_scalars_df_index = session.read_gbq(result_table) + assert not loaded_scalars_df_index.empty + + def test_null_index_materialize(scalars_df_null_index, scalars_pandas_df_default_index): bf_result = scalars_df_null_index.to_pandas() pd.testing.assert_frame_equal( @@ -83,6 +100,23 @@ def test_null_index_aggregate(scalars_df_null_index, scalars_pandas_df_default_i ) +def test_null_index_binop_series_axis_0( + scalars_df_null_index, scalars_pandas_df_default_index +): + bf_result = ( + scalars_df_null_index[["int64_col", "int64_too"]] + .add(scalars_df_null_index["int64_col"], axis=0) + .to_pandas() + ) + pd_result = scalars_pandas_df_default_index[["int64_col", "int64_too"]].add( + scalars_pandas_df_default_index.int64_col, axis=0 + ) + + pd.testing.assert_frame_equal( + bf_result, pd_result, check_dtype=False, check_index_type=False + ) + + def test_null_index_groupby_aggregate( scalars_df_null_index, scalars_pandas_df_default_index ): @@ -139,6 +173,25 @@ def test_null_index_merge_left_null_index_object( assert got.shape == expected.shape +@skip_legacy_pandas +@pytest.mark.parametrize( + ("expr",), + [ + ("new_col = int64_col + int64_too",), + ("new_col = (rowindex > 3) | bool_col",), + ("int64_too = bool_col\nnew_col2 = rowindex",), + ], +) +def test_null_index_df_eval( + scalars_df_null_index, scalars_pandas_df_default_index, expr +): + + bf_result = scalars_df_null_index.eval(expr).to_pandas() + pd_result = scalars_pandas_df_default_index.eval(expr) + + pd.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + + def test_null_index_merge_right_null_index_object( scalars_df_null_index, scalars_df_default_index, scalars_pandas_df_default_index ): From 9d6d9ddcf69914468de609c63eca3bbf7ca28e09 Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:22:28 -0700 Subject: [PATCH 909/921] chore: improve error messages for semantic operators (#1078) * chore: improve error messages for semantic operators * fix tests --- bigframes/operations/semantics.py | 35 ++++++- .../system/large/operations/test_semantics.py | 97 ++++++++++++++++--- 2 files changed, 114 insertions(+), 18 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index f3e237aa1ac..1f601b17256 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -104,6 +104,12 @@ def agg( for column in columns: if column not in self._df.columns: raise ValueError(f"Column {column} not found.") + if self._df[column].dtype != dtypes.STRING_DTYPE: + raise TypeError( + "Semantics aggregated column must be a string type, not " + f"{type(self._df[column])}" + ) + if len(columns) > 1: raise NotImplementedError( "Semantic aggregations are limited to a single column." @@ -324,6 +330,11 @@ def filter(self, instruction: str, model): for column in columns: if column not in self._df.columns: raise ValueError(f"Column {column} not found.") + if self._df[column].dtype != dtypes.STRING_DTYPE: + raise TypeError( + "Semantics aggregated column must be a string type, not " + f"{type(self._df[column])}" + ) user_instruction = self._format_instruction(instruction, columns) output_instruction = "Based on the provided context, reply to the following claim by only True or False:" @@ -372,7 +383,7 @@ def map(self, instruction: str, output_column: str, model): in the instructions like: "Get the ingredients of {food}." - result_column_name: + output_column: The column name of the mapping result. model: @@ -391,6 +402,11 @@ def map(self, instruction: str, output_column: str, model): for column in columns: if column not in self._df.columns: raise ValueError(f"Column {column} not found.") + if self._df[column].dtype != dtypes.STRING_DTYPE: + raise TypeError( + "Semantics aggregated column must be a string type, not " + f"{type(self._df[column])}" + ) user_instruction = self._format_instruction(instruction, columns) output_instruction = ( @@ -512,8 +528,11 @@ def join(self, other, instruction: str, model, max_rows: int = 1000): else: raise ValueError(f"Column {col} not found") - if not left_columns or not right_columns: - raise ValueError() + if not left_columns: + raise ValueError("No left column references.") + + if not right_columns: + raise ValueError("No right column references.") joined_df = self._df.merge(other, how="cross", suffixes=("_left", "_right")) @@ -570,13 +589,16 @@ def search( """ if search_column not in self._df.columns: - raise ValueError(f"Column {search_column} not found") + raise ValueError(f"Column `{search_column}` 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)}") + 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() @@ -855,6 +877,9 @@ def sim_join( 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.") + base_table_embedding_column = guid.generate_guid() base_table = self._attach_embedding( other, right_on, base_table_embedding_column, model @@ -926,4 +951,4 @@ def _validate_model(model): from bigframes.ml.llm import GeminiTextGenerator if not isinstance(model, GeminiTextGenerator): - raise ValueError("Model is not GeminiText Generator") + raise TypeError("Model is not GeminiText Generator") diff --git a/tests/system/large/operations/test_semantics.py b/tests/system/large/operations/test_semantics.py index b33b52ea1c5..72f2897211c 100644 --- a/tests/system/large/operations/test_semantics.py +++ b/tests/system/large/operations/test_semantics.py @@ -82,21 +82,33 @@ def test_agg(session, gemini_flash_model, max_agg_rows, cluster_column): marks=pytest.mark.xfail(raises=ValueError), ), pytest.param( - "{city} is in the {non_existing_column}", + "{Movies} is good", id="non_existing_column", marks=pytest.mark.xfail(raises=ValueError), ), pytest.param( - "{city} is in the {country}", + "{Movies} is better than {Movies}", id="two_columns", marks=pytest.mark.xfail(raises=NotImplementedError), ), + pytest.param( + "{Year}", + id="invalid_type", + marks=pytest.mark.xfail(raises=TypeError), + ), ], ) def test_agg_invalid_instruction_raise_error(instruction, gemini_flash_model): bigframes.options.experiments.semantic_operators = True df = dataframe.DataFrame( - {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + data={ + "Movies": [ + "Titanic", + "The Wolf of Wall Street", + "Killers of the Flower Moon", + ], + "Year": [1997, 2013, 2023], + }, ) df.semantics.agg(instruction, gemini_flash_model) @@ -229,15 +241,26 @@ def test_filter_single_column_reference(session, gemini_flash_model): @pytest.mark.parametrize( "instruction", [ - "No column reference", - "{city} is in the {non_existing_column}", + 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): bigframes.options.experiments.semantic_operators = True - df = dataframe.DataFrame( - {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} - ) + df = dataframe.DataFrame({"id": [1, 2], "city": ["Seattle", "Berlin"]}) with pytest.raises(ValueError): df.semantics.filter(instruction, gemini_flash_model) @@ -249,7 +272,7 @@ def test_filter_invalid_model_raise_error(): {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} ) - with pytest.raises(ValueError): + with pytest.raises(TypeError): df.semantics.filter("{city} is the capital of {country}", None) @@ -290,14 +313,28 @@ def test_map(session, gemini_flash_model): @pytest.mark.parametrize( "instruction", [ - "No column reference", - "What is the food made from {ingredient_1} and {non_existing_column}?}", + 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): bigframes.options.experiments.semantic_operators = True df = dataframe.DataFrame( data={ + "id": [1, 2], "ingredient_1": ["Burger Bun", "Soy Bean"], "ingredient_2": ["Beef Patty", "Bittern"], } @@ -316,7 +353,7 @@ def test_map_invalid_model_raise_error(): }, ) - with pytest.raises(ValueError): + with pytest.raises(TypeError): df.semantics.map( "What is the food made from {ingredient_1} and {ingredient_2}? One word only.", "food", @@ -462,7 +499,7 @@ def test_join_invalid_model_raise_error(): cities = dataframe.DataFrame({"city": ["Seattle", "Berlin"]}) countries = dataframe.DataFrame({"country": ["USA", "UK", "Germany"]}) - with pytest.raises(ValueError): + with pytest.raises(TypeError): cities.semantics.join(countries, "{city} is in {country}", None) @@ -528,6 +565,19 @@ def test_search_invalid_model_raises_error(session): df.semantics.search("creatures", "monkey", top_k=2, model=None) +def test_search_invalid_top_k_raises_error(session, text_embedding_generator): + bigframes.options.experiments.semantic_operators = True + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + with pytest.raises(ValueError): + df.semantics.search( + "creatures", "monkey", top_k=0, model=text_embedding_generator + ) + + @pytest.mark.parametrize( "score_column", [ @@ -614,6 +664,27 @@ def test_sim_join_invalid_model_raises_error(session): ) +def test_sim_join_invalid_top_k_raises_error(session, text_embedding_generator): + bigframes.options.experiments.semantic_operators = True + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with pytest.raises(ValueError): + df1.semantics.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): bigframes.options.experiments.semantic_operators = True df1 = dataframe.DataFrame( From 2dc22ec67fbbb283c5147828beabb7c7f89067c6 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 15 Oct 2024 07:29:34 -0700 Subject: [PATCH 910/921] chore: fix mypy errors, exclude experimental notebook from testing (#1083) * chore: fix mypy errors * use quoted annotation to avoid error in non type-checking environment * exclude experimental semantic operators notebook from testing --- bigframes/dataframe.py | 4 +++- bigframes/session/loader.py | 4 +++- bigframes/session/metrics.py | 6 ++++-- noxfile.py | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 0c50c2c749a..6ee51380bdb 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -81,6 +81,8 @@ import bigframes.session._io.bigquery if typing.TYPE_CHECKING: + from _typeshed import SupportsRichComparison + import bigframes.session SingleItemValue = Union[bigframes.series.Series, int, float, Callable] @@ -2464,7 +2466,7 @@ def pivot_table( values = [values] # Unlike pivot, pivot_table has values always ordered. - values.sort() + values.sort(key=lambda val: typing.cast("SupportsRichComparison", val)) keys = index + columns agged = self.groupby(keys, dropna=True)[values].agg(aggfunc) diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index 923605627df..2a03bcf145b 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -300,7 +300,9 @@ def read_gbq_table( ): # TODO(b/338111344): If we are running a query anyway, we might as # well generate ROW_NUMBER() at the same time. - all_columns = itertools.chain(index_cols, columns) if columns else () + all_columns: Iterable[str] = ( + itertools.chain(index_cols, columns) if columns else () + ) query = bf_io_bigquery.to_query( query, columns=all_columns, diff --git a/bigframes/session/metrics.py b/bigframes/session/metrics.py index 579cac1ac3e..9be7119368e 100644 --- a/bigframes/session/metrics.py +++ b/bigframes/session/metrics.py @@ -44,7 +44,7 @@ def count_job_stats(self, query_job: bq_job.QueryJob): def get_performance_stats( query_job: bigquery.QueryJob, -) -> Optional[Tuple[int, int, float]]: +) -> Optional[Tuple[int, int, Optional[float]]]: """Parse the query job for performance stats. Return None if the stats do not reflect real work done in bigquery. @@ -73,7 +73,9 @@ def get_performance_stats( return bytes_processed, slot_millis, exec_seconds -def write_stats_to_disk(bytes_processed: int, slot_millis: int, exec_seconds: float): +def write_stats_to_disk( + bytes_processed: int, slot_millis: int, exec_seconds: Optional[float] +): """For pytest runs only, log information about the query job to a file in order to create a performance report. """ diff --git a/noxfile.py b/noxfile.py index ef4bf1a37a5..03c39f33c45 100644 --- a/noxfile.py +++ b/noxfile.py @@ -744,6 +744,7 @@ def notebook(session: nox.Session): # The experimental notebooks imagine features that don't yet # exist or only exist as temporary prototypes. "notebooks/experimental/longer_ml_demo.ipynb", + "notebooks/experimental/semantic_operators.ipynb", # The notebooks that are added for more use cases, such as backing a # blog post, which may take longer to execute and need not be # continuously tested. From d73fe9d5fd2907aeaaa892a329221c10bb390da0 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 15 Oct 2024 09:00:06 -0700 Subject: [PATCH 911/921] perf: Speed up tree transforms during sql compile (#1071) --- bigframes/core/compile/compiler.py | 19 ++++++++++++++--- bigframes/core/nodes.py | 24 +++++++++++++++++---- bigframes/core/tree_properties.py | 3 +++ bigframes/session/executor.py | 34 ++++++++++++------------------ 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index fd1514d7b7b..19c18798c0a 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -36,6 +36,7 @@ import bigframes.core.identifiers as ids import bigframes.core.nodes as nodes import bigframes.core.ordering as bf_ordering +import bigframes.core.rewrite as rewrites if typing.TYPE_CHECKING: import bigframes.core @@ -48,20 +49,32 @@ class Compiler: # In unstrict mode, ordering from ReadTable or after joins may be ambiguous to improve query performance. strict: bool = True scalar_op_compiler = compile_scalar.ScalarOpCompiler() + enable_pruning: bool = False + + def _preprocess(self, node: nodes.BigFrameNode): + if self.enable_pruning: + used_fields = frozenset(field.id for field in node.fields) + node = node.prune(used_fields) + node = functools.cache(rewrites.replace_slice_ops)(node) + return node def compile_ordered_ir(self, node: nodes.BigFrameNode) -> compiled.OrderedIR: - ir = typing.cast(compiled.OrderedIR, self.compile_node(node, True)) + ir = typing.cast( + compiled.OrderedIR, self.compile_node(self._preprocess(node), True) + ) if self.strict: assert ir.has_total_order return ir def compile_unordered_ir(self, node: nodes.BigFrameNode) -> compiled.UnorderedIR: - return typing.cast(compiled.UnorderedIR, self.compile_node(node, False)) + return typing.cast( + compiled.UnorderedIR, self.compile_node(self._preprocess(node), False) + ) def compile_peak_sql( self, node: nodes.BigFrameNode, n_rows: int ) -> typing.Optional[str]: - return self.compile_unordered_ir(node).peek_sql(n_rows) + return self.compile_unordered_ir(self._preprocess(node)).peek_sql(n_rows) # TODO: Remove cache when schema no longer requires compilation to derive schema (and therefor only compiles for execution) @functools.lru_cache(maxsize=5000) diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 1d019365098..4aab9dc6317 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -263,7 +263,11 @@ def explicitly_ordered(self) -> bool: def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: - return replace(self, child=t(self.child)) + transformed = replace(self, child=t(self.child)) + if self == transformed: + # reusing existing object speeds up eq, and saves a small amount of memory + return self + return transformed @property def order_ambiguous(self) -> bool: @@ -350,9 +354,13 @@ def joins(self) -> bool: def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: - return replace( + transformed = replace( self, left_child=t(self.left_child), right_child=t(self.right_child) ) + if self == transformed: + # reusing existing object speeds up eq, and saves a small amount of memory + return self + return transformed @property def defines_namespace(self) -> bool: @@ -407,7 +415,11 @@ def variables_introduced(self) -> int: def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: - return replace(self, children=tuple(t(child) for child in self.children)) + transformed = replace(self, children=tuple(t(child) for child in self.children)) + if self == transformed: + # reusing existing object speeds up eq, and saves a small amount of memory + return self + return transformed def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: # TODO: Make concat prunable, probably by redefining @@ -451,7 +463,11 @@ def variables_introduced(self) -> int: def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: - return replace(self, start=t(self.start), end=t(self.end)) + transformed = replace(self, start=t(self.start), end=t(self.end)) + if self == transformed: + # reusing existing object speeds up eq, and saves a small amount of memory + return self + return transformed def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: # TODO: Make FromRangeNode prunable (or convert to other node types) diff --git a/bigframes/core/tree_properties.py b/bigframes/core/tree_properties.py index 1b0fe0d0722..3e61b830a98 100644 --- a/bigframes/core/tree_properties.py +++ b/bigframes/core/tree_properties.py @@ -113,6 +113,9 @@ def _node_counts_inner( node_counts = _node_counts_inner(root) + if len(node_counts) == 0: + raise ValueError("node counts should be non-zero") + return max( node_counts.keys(), key=lambda node: heuristic( diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index ab2ebed0d40..1b58d1a9933 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -45,7 +45,6 @@ import bigframes.core.identifiers import bigframes.core.nodes as nodes import bigframes.core.ordering as order -import bigframes.core.rewrite as rewrites import bigframes.core.schema import bigframes.core.tree_properties as tree_properties import bigframes.features @@ -128,7 +127,7 @@ def to_sql( col_id_overrides = dict(col_id_overrides) col_id_overrides[internal_offset_col] = offset_column node = ( - self._get_optimized_plan(array_value.node) + self._sub_cache_subtrees(array_value.node) if enable_cache else array_value.node ) @@ -279,7 +278,7 @@ def peek( """ A 'peek' efficiently accesses a small number of rows in the dataframe. """ - plan = self._get_optimized_plan(array_value.node) + plan = self._sub_cache_subtrees(array_value.node) if not tree_properties.can_fast_peek(plan): warnings.warn("Peeking this value cannot be done efficiently.") @@ -314,7 +313,7 @@ def head( # No user-provided ordering, so just get any N rows, its faster! return self.peek(array_value, n_rows) - plan = self._get_optimized_plan(array_value.node) + plan = self._sub_cache_subtrees(array_value.node) if not tree_properties.can_fast_head(plan): # If can't get head fast, we are going to need to execute the whole query # Will want to do this in a way such that the result is reusable, but the first @@ -322,7 +321,7 @@ def head( # This currently requires clustering on offsets. self._cache_with_offsets(array_value) # Get a new optimized plan after caching - plan = self._get_optimized_plan(array_value.node) + plan = self._sub_cache_subtrees(array_value.node) assert tree_properties.can_fast_head(plan) head_plan = generate_head_plan(plan, n_rows) @@ -347,7 +346,7 @@ def get_row_count(self, array_value: bigframes.core.ArrayValue) -> int: if count is not None: return count else: - row_count_plan = self._get_optimized_plan( + row_count_plan = self._sub_cache_subtrees( generate_row_count_plan(array_value.node) ) sql = self.compiler.compile_unordered(row_count_plan) @@ -359,7 +358,7 @@ def _local_get_row_count( ) -> Optional[int]: # optimized plan has cache materializations which will have row count metadata # that is more likely to be usable than original leaf nodes. - plan = self._get_optimized_plan(array_value.node) + plan = self._sub_cache_subtrees(array_value.node) return tree_properties.row_count(plan) # Helpers @@ -424,21 +423,14 @@ def _wait_on_job( self.metrics.count_job_stats(query_job) return results_iterator - def _get_optimized_plan(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: + def _sub_cache_subtrees(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: """ Takes the original expression tree and applies optimizations to accelerate execution. At present, the only optimization is to replace subtress with cached previous materializations. """ # Apply any rewrites *after* applying cache, as cache is sensitive to exact tree structure - optimized_plan = tree_properties.replace_nodes( - node, (dict(self._cached_executions)) - ) - if ENABLE_PRUNING: - used_fields = frozenset(field.id for field in optimized_plan.fields) - optimized_plan = optimized_plan.prune(used_fields) - optimized_plan = rewrites.replace_slice_ops(optimized_plan) - return optimized_plan + return tree_properties.replace_nodes(node, (dict(self._cached_executions))) def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): """ @@ -448,7 +440,7 @@ def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): # Once rewriting is available, will want to rewrite before # evaluating execution cost. return tree_properties.is_trivially_executable( - self._get_optimized_plan(array_value.node) + self._sub_cache_subtrees(array_value.node) ) def _cache_with_cluster_cols( @@ -457,7 +449,7 @@ def _cache_with_cluster_cols( """Executes the query and uses the resulting table to rewrite future executions.""" sql, schema, ordering_info = self.compiler.compile_raw( - self._get_optimized_plan(array_value.node) + self._sub_cache_subtrees(array_value.node) ) tmp_table = self._sql_as_cached_temp_table( sql, @@ -474,7 +466,7 @@ def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): """Executes the query and uses the resulting table to rewrite future executions.""" offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") w_offsets, offset_column = array_value.promote_offsets() - sql = self.compiler.compile_unordered(self._get_optimized_plan(w_offsets.node)) + sql = self.compiler.compile_unordered(self._sub_cache_subtrees(w_offsets.node)) tmp_table = self._sql_as_cached_temp_table( sql, @@ -510,7 +502,7 @@ def _simplify_with_caching(self, array_value: bigframes.core.ArrayValue): """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" # Apply existing caching first for _ in range(MAX_SUBTREE_FACTORINGS): - node_with_cache = self._get_optimized_plan(array_value.node) + node_with_cache = self._sub_cache_subtrees(array_value.node) if node_with_cache.planning_complexity < QUERY_COMPLEXITY_LIMIT: return @@ -567,7 +559,7 @@ def _validate_result_schema( ): actual_schema = tuple(bq_schema) ibis_schema = bigframes.core.compile.test_only_ibis_inferred_schema( - self._get_optimized_plan(array_value.node) + self._sub_cache_subtrees(array_value.node) ) internal_schema = array_value.schema if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: From dced46070ee4212b5585a1eb53ae341dc0bf63ba Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:02:54 -0700 Subject: [PATCH 912/921] fix: update session metrics in `read_gbq_query` (#1084) * fix: update read_gbq_query query metric and fix ML transform metric missing issue. * update test --- bigframes/session/loader.py | 3 +++ tests/system/large/ml/test_core.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index 2a03bcf145b..21d454d72f6 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -529,6 +529,9 @@ def read_gbq_query( configuration=configuration, ) + if self._metrics is not None: + self._metrics.count_job_stats(query_job) + # If there was no destination table, that means the query must have # been DDL or DML. Return some job metadata, instead. if not destination: diff --git a/tests/system/large/ml/test_core.py b/tests/system/large/ml/test_core.py index be5eea925fd..c1e1cc19d9f 100644 --- a/tests/system/large/ml/test_core.py +++ b/tests/system/large/ml/test_core.py @@ -146,10 +146,15 @@ def test_bqml_standalone_transform(penguins_df_default_index, new_penguins_df): "ML.ONE_HOT_ENCODER(species, 'none', 1000000, 0) OVER() AS onehotencoded_species", ], ) + start_execution_count = model.session._metrics.execution_count + + transformed = model.transform(new_penguins_df) + + end_execution_count = model.session._metrics.execution_count + assert end_execution_count - start_execution_count == 1 - transformed = model.transform(new_penguins_df).to_pandas() utils.check_pandas_df_schema_and_index( - transformed, + transformed.to_pandas(), columns=["scaled_culmen_length_mm", "onehotencoded_species"], index=[1633, 1672, 1690], col_exact=False, From fd06d31f5e7edab3ce3af9a4e3e4415a4463ee5d Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Tue, 15 Oct 2024 12:07:08 -0700 Subject: [PATCH 913/921] chore: skip cluster_by doctest (#1086) --- bigframes/operations/semantics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index 1f601b17256..f3e759fdd92 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -231,7 +231,7 @@ def cluster_by( >>> df = bpd.DataFrame({ ... "Product": ["Smartphone", "Laptop", "T-shirt", "Jeans"], ... }) - >>> df.semantics.cluster_by("Product", "Cluster ID", model, n_clusters=2) + >>> df.semantics.cluster_by("Product", "Cluster ID", model, n_clusters=2) # doctest: +SKIP Product Cluster ID 0 Smartphone 2 1 Laptop 2 From 06cec00c51ba4b8df591e0988379db75b20c450b Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:13:01 -0700 Subject: [PATCH 914/921] fix: model.fit metric not collected issue. (#1085) * fix: model.fit metric not collected issue. * update * fix unit test * update code and test * update code * update comment * update test --- bigframes/session/__init__.py | 4 +++- tests/system/large/ml/test_linear_model.py | 13 +++++++++++++ tests/system/small/ml/test_core.py | 6 ++++++ tests/unit/ml/test_golden_sql.py | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 072bcc57815..6cfaf48493c 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1373,7 +1373,9 @@ def _start_query_ml_ddl( # https://cloud.google.com/bigquery/docs/customer-managed-encryption#encrypt-model job_config.destination_encryption_configuration = None - return bf_io_bigquery.start_query_with_client(self.bqclient, sql, job_config) + return bf_io_bigquery.start_query_with_client( + self.bqclient, sql, job_config, metrics=self._metrics + ) def _export( self, diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index f593ac29834..5c3f7631d9e 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -128,10 +128,20 @@ def test_unordered_mode_linear_regression_configure_fit_score_predict( ] ] y_train = df[["body_mass_g"]] + + start_execution_count = df._block._expr.session._metrics.execution_count model.fit(X_train, y_train) + end_execution_count = df._block._expr.session._metrics.execution_count + # The fit function initiates two queries: the first generates and caches + # the training data, while the second creates and fits the model. + assert end_execution_count - start_execution_count == 2 # Check score to ensure the model was fitted + start_execution_count = end_execution_count result = model.score(X_train, y_train).to_pandas() + end_execution_count = df._block._expr.session._metrics.execution_count + assert end_execution_count - start_execution_count == 1 + utils.check_pandas_df_schema_and_index( result, columns=utils.ML_REGRESSION_METRICS, index=1 ) @@ -154,7 +164,10 @@ def test_unordered_mode_linear_regression_configure_fit_score_predict( assert reloaded_model.max_iterations == 20 assert reloaded_model.tol == 0.01 + start_execution_count = df._block._expr.session._metrics.execution_count pred = reloaded_model.predict(df) + end_execution_count = df._block._expr.session._metrics.execution_count + assert end_execution_count - start_execution_count == 1 utils.check_pandas_df_schema_and_index( pred, columns=("predicted_body_mass_g",), diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 6b852e87af9..65540e7e818 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -383,8 +383,14 @@ def test_model_forecast(time_series_bqml_arima_plus_model: core.BqmlModel): def test_model_register(ephemera_penguins_bqml_linear_model: core.BqmlModel): model = ephemera_penguins_bqml_linear_model + + start_execution_count = model.session._metrics.execution_count + model.register() + end_execution_count = model.session._metrics.execution_count + assert end_execution_count - start_execution_count == 1 + assert model.model.model_id is not None model_name = "bigframes_" + model.model.model_id # Only registered model contains the field, and the field includes project/dataset. Here only check model_id. diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 65f079852e7..ce050115466 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -36,6 +36,7 @@ def mock_session(): TEMP_MODEL_ID.project, TEMP_MODEL_ID.dataset_id ) mock_session._bq_kms_key_name = None + mock_session._metrics = None query_job = mock.create_autospec(bigquery.QueryJob) type(query_job).destination = mock.PropertyMock( From a120bae2a8039d6115369b1f4a9047d4f0586120 Mon Sep 17 00:00:00 2001 From: rey-esp Date: Wed, 16 Oct 2024 15:35:48 +0000 Subject: [PATCH 915/921] docs: add ml tutorial for Evaluate the model (#1038) * add ml tutorial for Evaluate the model * remove alternative * remove alternative snippet * update description * avoid contractions * fix comment description * add expected results --- .../linear_regression_tutorial_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/samples/snippets/linear_regression_tutorial_test.py b/samples/snippets/linear_regression_tutorial_test.py index 0c861d1120f..9a4908dbf51 100644 --- a/samples/snippets/linear_regression_tutorial_test.py +++ b/samples/snippets/linear_regression_tutorial_test.py @@ -37,6 +37,24 @@ def test_linear_regression(random_model_id: str) -> None: replace=True, ) # [END bigquery_dataframes_bqml_linear_regression] + # [START bigquery_dataframes_bqml_linear_evaluate] + import bigframes.pandas as bpd + + # Select the model you will be evaluating. `read_gbq_model` loads model data from + # BigQuery, but you could also use the `model` object from the previous steps. + model = bpd.read_gbq_model( + your_model_id, # For example: "bqml_tutorial.penguins_model" + ) + + # Score the model with input data defined in an earlier step to compare + # model predictions on feature_columns to true labels in label_columns. + score = model.score(feature_columns, label_columns) + # Expected output results: + # index mean_absolute_error mean_squared_error mean_squared_log_error median_absolute_error r2_score explained_variance + # 0 227.012237 81838.159892 0.00507 173.080816 0.872377 0.872377 + # 1 rows x columns + # [END bigquery_dataframes_bqml_linear_evaluate] assert feature_columns is not None assert label_columns is not None assert model is not None + assert score is not None From bd23ec30ba39f0f8f2af94b1288265d6dd565330 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:11:00 -0700 Subject: [PATCH 916/921] chore: score query count assertion fix. (#1093) --- tests/system/large/ml/test_linear_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/system/large/ml/test_linear_model.py b/tests/system/large/ml/test_linear_model.py index 5c3f7631d9e..273da97bc56 100644 --- a/tests/system/large/ml/test_linear_model.py +++ b/tests/system/large/ml/test_linear_model.py @@ -140,7 +140,8 @@ def test_unordered_mode_linear_regression_configure_fit_score_predict( start_execution_count = end_execution_count result = model.score(X_train, y_train).to_pandas() end_execution_count = df._block._expr.session._metrics.execution_count - assert end_execution_count - start_execution_count == 1 + # The score function and to_pandas each initiate one query. + assert end_execution_count - start_execution_count == 2 utils.check_pandas_df_schema_and_index( result, columns=utils.ML_REGRESSION_METRICS, index=1 From b4739ec33b3f3edcafa63bce605dd108f23a155c Mon Sep 17 00:00:00 2001 From: Chelsea Lin <124939984+chelsea-lin@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:40:20 -0700 Subject: [PATCH 917/921] chore: count */core/*.py files for code coverage tests (#1080) * chore: count */core/*.py files for code coverage tests * counts test coverage for product and test codes seperately * fail-under=87 --- .coveragerc | 1 - noxfile.py | 10 +++++++++- owlbot.py | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9d801989cba..94ecfe88ffc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -34,6 +34,5 @@ exclude_lines = omit = */gapic/*.py */proto/*.py - */core/*.py */site-packages/*.py google/cloud/__init__.py diff --git a/noxfile.py b/noxfile.py index 03c39f33c45..3ab271179e0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -432,7 +432,15 @@ def cover(session): (including system test runs), and then erases coverage data. """ session.install("coverage", "pytest-cov") - session.run("coverage", "report", "--show-missing", "--fail-under=90") + + # Create a coverage report that includes only the product code. + session.run( + "coverage", + "report", + "--include=bigframes/*", + "--show-missing", + "--fail-under=86", + ) # Make sure there is no dead code in our test directories. session.run( diff --git a/owlbot.py b/owlbot.py index b29384d4625..9f92f068626 100644 --- a/owlbot.py +++ b/owlbot.py @@ -130,6 +130,13 @@ "BigQuery DataFrames provides DataFrame APIs on the BigQuery engine", ) +# Don't omit `*/core/*.py` when counting test coverages +assert 1 == s.replace( + [".coveragerc"], + re.escape(" */core/*.py\n"), + "", +) + # ---------------------------------------------------------------------------- # Samples templates # ---------------------------------------------------------------------------- From 51cdd33e9f8377b3b992e0392eeb212aed499e3b Mon Sep 17 00:00:00 2001 From: Arwa Sharif <146148342+arwas11@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:42:09 -0500 Subject: [PATCH 918/921] feat: support uploading local geo data (#1036) * feat: support uploading local geo data * Fix mypy error * Add explanatory comment --- bigframes/session/__init__.py | 6 ++---- tests/system/small/test_series.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 6cfaf48493c..27707c21fd9 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -707,10 +707,8 @@ def _read_pandas_inline( try: local_block = blocks.Block.from_local(pandas_dataframe, self) inline_df = dataframe.DataFrame(local_block) - except pa.ArrowInvalid as e: - raise pa.ArrowInvalid( - f"Could not convert with a BigQuery type: `{e}`. " - ) from e + except pa.ArrowInvalid: # Thrown by arrow for unsupported types, such as geo. + return None except ValueError: # Thrown by ibis for some unhandled types return None except pa.ArrowTypeError: # Thrown by arrow for types without mapping (geo). diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index f1c60664a15..6cdb74fcf79 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -23,6 +23,7 @@ import pandas as pd import pyarrow as pa # type: ignore import pytest +import shapely # type: ignore import bigframes.pandas import bigframes.series as series @@ -213,6 +214,19 @@ def test_series_construct_from_list_escaped_strings(): pd.testing.assert_series_equal(bf_result.to_pandas(), pd_result) +def test_series_construct_geodata(): + pd_series = pd.Series( + [shapely.Point(1, 1), shapely.Point(2, 2), shapely.Point(3, 3)], + dtype=gpd.array.GeometryDtype(), + ) + + series = bigframes.pandas.Series(pd_series) + + pd.testing.assert_series_equal( + pd_series, series.to_pandas(), check_index_type=False + ) + + @pytest.mark.parametrize( ["col_name", "expected_dtype"], [ From 7003d1ae6fddd535f6c206081e85f82bb6006f17 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 18 Oct 2024 11:26:53 -0700 Subject: [PATCH 919/921] perf: Utilize ORDER BY LIMIT over ROW_NUMBER where possible (#1077) --- bigframes/core/compile/api.py | 8 +- bigframes/core/compile/compiled.py | 9 +- bigframes/core/nodes.py | 137 ++++++++++++++++++++++++++--- bigframes/core/rewrite.py | 88 +++++++++++------- bigframes/core/slices.py | 106 ++++++++++++++++++++++ bigframes/core/tree_properties.py | 41 +++++---- bigframes/session/executor.py | 52 ++++------- tests/unit/core/test_rewrite.py | 57 ++++++++++++ tests/unit/core/test_slices.py | 61 +++++++++++++ 9 files changed, 459 insertions(+), 100 deletions(-) create mode 100644 bigframes/core/slices.py create mode 100644 tests/unit/core/test_rewrite.py create mode 100644 tests/unit/core/test_slices.py diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index 4e833411aef..86c8fca25a9 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -18,6 +18,7 @@ import google.cloud.bigquery as bigquery import bigframes.core.compile.compiler as compiler +import bigframes.core.rewrite as rewrites if TYPE_CHECKING: import bigframes.core.nodes @@ -42,6 +43,7 @@ def compile_unordered( col_id_overrides: Mapping[str, str] = {}, ) -> str: """Compile node into sql where rows are unsorted, and no ordering information is preserved.""" + # TODO: Enable limit pullup, but only if not being used to write to clustered table. return self._compiler.compile_unordered_ir(node).to_sql( col_id_overrides=col_id_overrides ) @@ -53,8 +55,10 @@ def compile_ordered( col_id_overrides: Mapping[str, str] = {}, ) -> str: """Compile node into sql where rows are sorted with ORDER BY.""" - return self._compiler.compile_ordered_ir(node).to_sql( - col_id_overrides=col_id_overrides, ordered=True + # If we are ordering the query anyways, compiling the slice as a limit is probably a good idea. + new_node, limit = rewrites.pullup_limit_from_slice(node) + return self._compiler.compile_ordered_ir(new_node).to_sql( + col_id_overrides=col_id_overrides, ordered=True, limit=limit ) def compile_raw( diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index f4afdaa97c0..d02a2c444c7 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -943,8 +943,9 @@ def to_sql( self, col_id_overrides: typing.Mapping[str, str] = {}, ordered: bool = False, + limit: Optional[int] = None, ) -> str: - if ordered: + if ordered or limit: # Need to bake ordering expressions into the selected column in order for our ordering clause builder to work. baked_ir = self._bake_ordering() sql = ibis_bigquery.Backend().compile( @@ -969,7 +970,11 @@ def to_sql( order_by_clause = bigframes.core.sql.ordering_clause( baked_ir._ordering.all_ordering_columns ) - sql += f"{order_by_clause}\n" + sql += f"\n{order_by_clause}" + if limit is not None: + if not isinstance(limit, int): + raise TypeError(f"Limit param: {limit} must be an int.") + sql += f"\nLIMIT {limit}" else: sql = ibis_bigquery.Backend().compile( self._to_ibis_expr( diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 4aab9dc6317..2e23f529e2c 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -20,7 +20,7 @@ import functools import itertools import typing -from typing import Callable, Iterable, Optional, Sequence, Tuple +from typing import Callable, cast, Iterable, Optional, Sequence, Tuple import google.cloud.bigquery as bq @@ -30,6 +30,7 @@ import bigframes.core.identifiers as bfet_ids from bigframes.core.ordering import OrderingExpression import bigframes.core.schema as schemata +import bigframes.core.slices as slices import bigframes.core.window_spec as window import bigframes.dtypes import bigframes.operations.aggregations as agg_ops @@ -82,6 +83,11 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: """Direct children of this node""" return tuple([]) + @property + @abc.abstractmethod + def row_count(self) -> typing.Optional[int]: + return None + @functools.cached_property def session(self): sessions = [] @@ -304,6 +310,26 @@ def variables_introduced(self) -> int: def relation_ops_created(self) -> int: return 2 + @property + def is_limit(self) -> bool: + """Returns whether this is equivalent to a ORDER BY ... LIMIT N.""" + # TODO: Handle tail case. + return ( + (not self.start) + and (self.step == 1) + and (self.stop is not None) + and (self.stop > 0) + ) + + @property + def row_count(self) -> typing.Optional[int]: + child_length = self.child.row_count + if child_length is None: + return None + return slices.slice_output_rows( + (self.start, self.stop, self.step), child_length + ) + @dataclass(frozen=True, eq=False) class JoinNode(BigFrameNode): @@ -351,6 +377,15 @@ def variables_introduced(self) -> int: def joins(self) -> bool: return True + @property + def row_count(self) -> Optional[int]: + if self.type == "cross": + if self.left_child.row_count is None or self.right_child.row_count is None: + return None + return self.left_child.row_count * self.right_child.row_count + + return None + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -412,6 +447,16 @@ def variables_introduced(self) -> int: """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" return len(self.schema.items) + OVERHEAD_VARIABLES + @property + def row_count(self) -> Optional[int]: + sub_counts = [node.row_count for node in self.child_nodes] + total = 0 + for count in sub_counts: + if count is None: + return None + total += count + return total + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -460,6 +505,10 @@ def variables_introduced(self) -> int: """Defines the number of variables generated by the current node. Used to estimate query planning complexity.""" return len(self.schema.items) + OVERHEAD_VARIABLES + @property + def row_count(self) -> Optional[int]: + return None + def transform_children( self, t: Callable[[BigFrameNode], BigFrameNode] ) -> BigFrameNode: @@ -484,7 +533,11 @@ def roots(self) -> typing.Set[BigFrameNode]: return {self} @property - def supports_fast_head(self) -> bool: + def fast_offsets(self) -> bool: + return False + + @property + def fast_ordered_limit(self) -> bool: return False def transform_children( @@ -492,11 +545,6 @@ def transform_children( ) -> BigFrameNode: return self - @property - def row_count(self) -> typing.Optional[int]: - """How many rows are in the data source. None means unknown.""" - return None - class ScanItem(typing.NamedTuple): id: bfet_ids.ColumnId @@ -528,7 +576,11 @@ def variables_introduced(self) -> int: return len(self.scan_list.items) + 1 @property - def supports_fast_head(self) -> bool: + def fast_offsets(self) -> bool: + return True + + @property + def fast_ordered_limit(self) -> bool: return True @property @@ -635,12 +687,27 @@ def relation_ops_created(self) -> int: return 3 @property - def supports_fast_head(self) -> bool: - # Fast head is only supported when row offsets are available. - # In the future, ORDER BY+LIMIT optimizations may allow fast head when - # clustered and/or partitioned on ordering key + def fast_offsets(self) -> bool: + # Fast head is only supported when row offsets are available or data is clustered over ordering key. return (self.source.ordering is not None) and self.source.ordering.is_sequential + @property + def fast_ordered_limit(self) -> bool: + if self.source.ordering is None: + return False + order_cols = self.source.ordering.all_ordering_columns + # monotonicity would probably be fine + if not all(col.scalar_expression.is_identity for col in order_cols): + return False + order_col_ids = tuple( + cast(ex.DerefOp, col.scalar_expression).id.name for col in order_cols + ) + cluster_col_ids = self.source.table.cluster_cols + if cluster_col_ids is None: + return False + + return order_col_ids == cluster_col_ids[: len(order_col_ids)] + @property def order_ambiguous(self) -> bool: return ( @@ -706,6 +773,10 @@ def relation_ops_created(self) -> int: def variables_introduced(self) -> int: return 1 + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: if self.col_id not in used_cols: return self.child.prune(used_cols) @@ -726,6 +797,10 @@ def row_preserving(self) -> bool: def variables_introduced(self) -> int: return 1 + @property + def row_count(self) -> Optional[int]: + return None + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: consumed_ids = used_cols.union(self.predicate.column_references) pruned_child = self.child.prune(consumed_ids) @@ -749,6 +824,10 @@ def relation_ops_created(self) -> int: def explicitly_ordered(self) -> bool: return True + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: ordering_cols = itertools.chain.from_iterable( map(lambda x: x.referenced_columns, self.by) @@ -772,6 +851,10 @@ def relation_ops_created(self) -> int: # Doesnt directly create any relational operations return 0 + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + @dataclass(frozen=True, eq=False) class SelectionNode(UnaryNode): @@ -798,6 +881,10 @@ def variables_introduced(self) -> int: def defines_namespace(self) -> bool: return True + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: pruned_selections = tuple( select for select in self.input_output_pairs if select[1] in used_cols @@ -842,6 +929,10 @@ def variables_introduced(self) -> int: new_vars = sum(1 for i in self.assignments if not i[0].is_identity) return new_vars + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: pruned_assignments = tuple(i for i in self.assignments if i[1] in used_cols) if len(pruned_assignments) == 0: @@ -877,6 +968,10 @@ def variables_introduced(self) -> int: def defines_namespace(self) -> bool: return True + @property + def row_count(self) -> Optional[int]: + return 1 + @dataclass(frozen=True, eq=False) class AggregateNode(UnaryNode): @@ -926,6 +1021,12 @@ def explicitly_ordered(self) -> bool: def defines_namespace(self) -> bool: return True + @property + def row_count(self) -> Optional[int]: + if not self.by_column_ids: + return 1 + return None + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: by_ids = (ref.id for ref in self.by_column_ids) pruned_aggs = tuple(agg for agg in self.aggregations if agg[1] in used_cols) @@ -963,6 +1064,10 @@ def relation_ops_created(self) -> int: # Assume that if not reprojecting, that there is a sequence of window operations sharing the same window return 0 if self.skip_reproject_unsafe else 4 + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + @functools.cached_property def added_field(self) -> Field: input_type = self.child.get_type(self.column_name.id) @@ -994,6 +1099,10 @@ def row_preserving(self) -> bool: def variables_introduced(self) -> int: return 1 + @property + def row_count(self) -> Optional[int]: + return None + # TODO: Explode should create a new column instead of overriding the existing one @dataclass(frozen=True, eq=False) @@ -1030,6 +1139,10 @@ def variables_introduced(self) -> int: def defines_namespace(self) -> bool: return True + @property + def row_count(self) -> Optional[int]: + return None + def prune(self, used_cols: COLUMN_SET) -> BigFrameNode: # Cannot prune explode op return self.transform_children( diff --git a/bigframes/core/rewrite.py b/bigframes/core/rewrite.py index d4e530fff35..9c0eb814505 100644 --- a/bigframes/core/rewrite.py +++ b/bigframes/core/rewrite.py @@ -24,7 +24,7 @@ import bigframes.core.join_def as join_defs import bigframes.core.nodes as nodes import bigframes.core.ordering as order -import bigframes.core.tree_properties as traversals +import bigframes.core.slices as slices import bigframes.operations as ops Selection = Tuple[Tuple[scalar_exprs.Expression, ids.ColumnId], ...] @@ -385,46 +385,71 @@ def common_selection_root( return None +def pullup_limit_from_slice( + root: nodes.BigFrameNode, +) -> Tuple[nodes.BigFrameNode, Optional[int]]: + """ + This is a BQ-sql specific optimization that can be helpful as ORDER BY LIMIT is more efficient than WHERE + ROW_NUMBER(). + + Only use this if writing to an unclustered table. Clustering is not compatible with ORDER BY. + """ + if isinstance(root, nodes.SliceNode): + # head case + # More cases could be handled, but this is by far the most important, as it is used by df.head(), df[:N] + if root.is_limit: + assert not root.start + assert root.step == 1 + assert root.stop is not None + limit = root.stop + new_root, prior_limit = pullup_limit_from_slice(root.child) + if (prior_limit is not None) and (prior_limit < limit): + limit = prior_limit + return new_root, limit + elif ( + isinstance(root, (nodes.SelectionNode, nodes.ProjectionNode)) + and root.row_preserving + ): + new_child, prior_limit = pullup_limit_from_slice(root.child) + if prior_limit is not None: + return root.transform_children(lambda _: new_child), prior_limit + # Most ops don't support pulling up slice, like filter, agg, join, etc. + return root, None + + def replace_slice_ops(root: nodes.BigFrameNode) -> nodes.BigFrameNode: # TODO: we want to pull up some slices into limit op if near root. if isinstance(root, nodes.SliceNode): root = root.transform_children(replace_slice_ops) - return convert_slice_to_filter(cast(nodes.SliceNode, root)) + return rewrite_slice(cast(nodes.SliceNode, root)) else: return root.transform_children(replace_slice_ops) -def get_simplified_slice(node: nodes.SliceNode): - """Attempts to simplify the slice.""" - row_count = traversals.row_count(node) - start, stop, step = node.start, node.stop, node.step +def rewrite_slice(node: nodes.SliceNode): + slice_def = (node.start, node.stop, node.step) + + # no-op (eg. df[::1]) + if slices.is_noop(slice_def, node.child.row_count): + return node.child - if start is None: - start = 0 if step > 0 else -1 - if row_count and step > 0: - if start and start < 0: - start = row_count + start - if stop and stop < 0: - stop = row_count + stop - return start, stop, step + # No filtering, just reverse (eg. df[::-1]) + if slices.is_reverse(slice_def, node.child.row_count): + return nodes.ReversedNode(node.child) + if node.child.row_count: + slice_def = slices.to_forward_offsets(slice_def, node.child.row_count) + return slice_as_filter(node.child, *slice_def) -def convert_slice_to_filter(node: nodes.SliceNode): - start, stop, step = get_simplified_slice(node) - # no-op (eg. df[::1]) +def slice_as_filter( + node: nodes.BigFrameNode, start: Optional[int], stop: Optional[int], step: int +) -> nodes.BigFrameNode: if ( - ((start == 0) or (start is None)) - and ((stop is None) or (stop == -1)) - and (step == 1) + ((start is None) or (start >= 0)) + and ((stop is None) or (stop >= 0)) + and (step > 0) ): - return node.child - # No filtering, just reverse (eg. df[::-1]) - if ((start is None) or (start == -1)) and (not stop) and (step == -1): - return nodes.ReversedNode(node.child) - # if start/stop/step are all non-negative, and do a simple predicate on forward offsets - if ((start is None) or (start >= 0)) and ((stop is None) or (stop >= 0)): - node_w_offset = add_offsets(node.child) + node_w_offset = add_offsets(node) predicate = convert_simple_slice( scalar_exprs.DerefOp(node_w_offset.col_id), start or 0, stop, step ) @@ -433,17 +458,18 @@ def convert_slice_to_filter(node: nodes.SliceNode): # fallback cases, generate both forward and backward offsets if step < 0: - forward_offsets = add_offsets(node.child) + forward_offsets = add_offsets(node) reversed_offsets = add_offsets(nodes.ReversedNode(forward_offsets)) dual_indexed = reversed_offsets else: - reversed_offsets = add_offsets(nodes.ReversedNode(node.child)) + reversed_offsets = add_offsets(nodes.ReversedNode(node)) forward_offsets = add_offsets(nodes.ReversedNode(reversed_offsets)) dual_indexed = forward_offsets + default_start = 0 if step >= 0 else -1 predicate = convert_complex_slice( scalar_exprs.DerefOp(forward_offsets.col_id), scalar_exprs.DerefOp(reversed_offsets.col_id), - start, + start if (start is not None) else default_start, stop, step, ) @@ -505,7 +531,7 @@ def convert_complex_slice( if start or ((start is not None) and step < 0): if start > 0 and step > 0: start_cond = ops.ge_op.as_expr(forward_offsets, scalar_exprs.const(start)) - elif start > 0 and step < 0: + elif start >= 0 and step < 0: start_cond = ops.le_op.as_expr(forward_offsets, scalar_exprs.const(start)) elif start < 0 and step > 0: start_cond = ops.le_op.as_expr( diff --git a/bigframes/core/slices.py b/bigframes/core/slices.py new file mode 100644 index 00000000000..97f90d33492 --- /dev/null +++ b/bigframes/core/slices.py @@ -0,0 +1,106 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Optional + + +def to_forward_offsets( + slice: tuple[Optional[int], Optional[int], Optional[int]], input_rows: int +) -> tuple[int, Optional[int], int]: + """Redefine the slice to use forward offsets for start and stop indices.""" + step = slice[2] or 1 + stop = slice[1] + start = slice[0] + + # normalize start to positive number + if start is None: + start = 0 if (step > 0) else (input_rows - 1) + elif start < 0: + start = max(0, input_rows + start) + else: + start = min(start, input_rows) + + if stop is None: + stop = None + elif stop < 0: + stop = max(0, input_rows + stop) + else: + stop = min(stop, input_rows) + + return (start, stop, step) + + +def remove_unused_parts( + slice: tuple[Optional[int], Optional[int], Optional[int]], input_rows: int +) -> tuple[Optional[int], Optional[int], Optional[int]]: + """Makes a slice component null if it doesn't impact slice semantics.""" + start, stop, step = slice + is_forward = (step is None) or (step > 0) + if start is not None: + if is_forward and ((start == 0) or (start <= -input_rows)): + start = None + elif (not is_forward) and ((start == -1) or (start >= (input_rows - 1))): + start = None + if stop is not None: + if is_forward and (stop >= input_rows): + stop = None + elif (not is_forward) and (stop <= (-input_rows - 1)): + stop = None + if step == 1: + step = None + return start, stop, step + + +def slice_output_rows( + slice: tuple[Optional[int], Optional[int], Optional[int]], input_size: int +) -> int: + """Given input_size, returns the number of rows returned after the slice operation.""" + slice = to_forward_offsets(slice, input_size) + start, stop, step = slice + + if step > 0: + if stop is None: + stop = input_size + length = max(0, (stop - start + step - 1) // step) + else: + if stop is None: + stop = -1 + length = max(0, (start - stop - step - 1) // -step) + return length + + +def is_noop( + slice_def: tuple[Optional[int], Optional[int], Optional[int]], + input_size: Optional[int], +) -> bool: + """Returns true iff the slice op is a no-op returning the input array.""" + if input_size: + start, stop, step = remove_unused_parts(slice_def, input_size) + else: + start, stop, step = slice_def + return (not start) and (stop is None) and ((step is None) or (step == 1)) + + +def is_reverse( + slice_def: tuple[Optional[int], Optional[int], Optional[int]], + input_size: Optional[int], +) -> bool: + """Returns true iff the slice op is a pure reverse op, equivalent to df[::-1]""" + if input_size: + start, stop, step = remove_unused_parts(slice_def, input_size) + else: + start, stop, step = slice_def + return (start is None) and (stop is None) and (step == -1) diff --git a/bigframes/core/tree_properties.py b/bigframes/core/tree_properties.py index 3e61b830a98..0a4339ee061 100644 --- a/bigframes/core/tree_properties.py +++ b/bigframes/core/tree_properties.py @@ -42,32 +42,35 @@ def can_fast_peek(node: nodes.BigFrameNode) -> bool: def can_fast_head(node: nodes.BigFrameNode) -> bool: """Can get head fast if can push head operator down to leafs and operators preserve rows.""" + # To do fast head operation: + # (1) the underlying data must be arranged/indexed according to the logical ordering + # (2) transformations must support pushing down LIMIT or a filter on row numbers + return has_fast_offset_address(node) or has_fast_offset_address(node) + + +def has_fast_orderby_limit(node: nodes.BigFrameNode) -> bool: + """True iff ORDER BY LIMIT can be performed without a large full table scan.""" + # TODO: In theory compatible with some Slice nodes, potentially by adding OFFSET + if isinstance(node, nodes.LeafNode): + return node.fast_ordered_limit + if isinstance(node, (nodes.ProjectionNode, nodes.SelectionNode)): + return has_fast_orderby_limit(node.child) + return False + + +def has_fast_offset_address(node: nodes.BigFrameNode) -> bool: + """True iff specific offsets can be scanned without a large full table scan.""" + # TODO: In theory can push offset lookups through slice operators by translating indices if isinstance(node, nodes.LeafNode): - return node.supports_fast_head + return node.fast_offsets if isinstance(node, (nodes.ProjectionNode, nodes.SelectionNode)): - return can_fast_head(node.child) + return has_fast_offset_address(node.child) return False def row_count(node: nodes.BigFrameNode) -> Optional[int]: """Determine row count from local metadata, return None if unknown.""" - if isinstance(node, nodes.LeafNode): - return node.row_count - if isinstance(node, nodes.AggregateNode): - if len(node.by_column_ids) == 0: - return 1 - return None - if isinstance(node, nodes.ConcatNode): - sub_counts = list(map(row_count, node.child_nodes)) - total = 0 - for count in sub_counts: - if count is None: - return None - total += count - return total - if isinstance(node, nodes.UnaryNode) and node.row_preserving: - return row_count(node.child) - return None + return node.row_count # Replace modified_cost(node) = cost(apply_cache(node)) diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 1b58d1a9933..170f0ac086e 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -40,7 +40,6 @@ import bigframes.core import bigframes.core.compile -import bigframes.core.expression as ex import bigframes.core.guid import bigframes.core.identifiers import bigframes.core.nodes as nodes @@ -49,7 +48,6 @@ import bigframes.core.tree_properties as tree_properties import bigframes.features import bigframes.formatting_helpers as formatting_helpers -import bigframes.operations as ops import bigframes.session._io.bigquery as bq_io import bigframes.session.metrics import bigframes.session.planner @@ -127,7 +125,7 @@ def to_sql( col_id_overrides = dict(col_id_overrides) col_id_overrides[internal_offset_col] = offset_column node = ( - self._sub_cache_subtrees(array_value.node) + self.replace_cached_subtrees(array_value.node) if enable_cache else array_value.node ) @@ -207,6 +205,9 @@ def export_gbq( """ Export the ArrayValue to an existing BigQuery table. """ + if bigframes.options.compute.enable_multi_query_execution: + self._simplify_with_caching(array_value) + dispositions = { "fail": bigquery.WriteDisposition.WRITE_EMPTY, "replace": bigquery.WriteDisposition.WRITE_TRUNCATE, @@ -278,7 +279,7 @@ def peek( """ A 'peek' efficiently accesses a small number of rows in the dataframe. """ - plan = self._sub_cache_subtrees(array_value.node) + plan = self.replace_cached_subtrees(array_value.node) if not tree_properties.can_fast_peek(plan): warnings.warn("Peeking this value cannot be done efficiently.") @@ -313,7 +314,7 @@ def head( # No user-provided ordering, so just get any N rows, its faster! return self.peek(array_value, n_rows) - plan = self._sub_cache_subtrees(array_value.node) + plan = self.replace_cached_subtrees(array_value.node) if not tree_properties.can_fast_head(plan): # If can't get head fast, we are going to need to execute the whole query # Will want to do this in a way such that the result is reusable, but the first @@ -321,7 +322,7 @@ def head( # This currently requires clustering on offsets. self._cache_with_offsets(array_value) # Get a new optimized plan after caching - plan = self._sub_cache_subtrees(array_value.node) + plan = self.replace_cached_subtrees(array_value.node) assert tree_properties.can_fast_head(plan) head_plan = generate_head_plan(plan, n_rows) @@ -346,7 +347,7 @@ def get_row_count(self, array_value: bigframes.core.ArrayValue) -> int: if count is not None: return count else: - row_count_plan = self._sub_cache_subtrees( + row_count_plan = self.replace_cached_subtrees( generate_row_count_plan(array_value.node) ) sql = self.compiler.compile_unordered(row_count_plan) @@ -358,7 +359,7 @@ def _local_get_row_count( ) -> Optional[int]: # optimized plan has cache materializations which will have row count metadata # that is more likely to be usable than original leaf nodes. - plan = self._sub_cache_subtrees(array_value.node) + plan = self.replace_cached_subtrees(array_value.node) return tree_properties.row_count(plan) # Helpers @@ -423,13 +424,7 @@ def _wait_on_job( self.metrics.count_job_stats(query_job) return results_iterator - def _sub_cache_subtrees(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: - """ - Takes the original expression tree and applies optimizations to accelerate execution. - - At present, the only optimization is to replace subtress with cached previous materializations. - """ - # Apply any rewrites *after* applying cache, as cache is sensitive to exact tree structure + def replace_cached_subtrees(self, node: nodes.BigFrameNode) -> nodes.BigFrameNode: return tree_properties.replace_nodes(node, (dict(self._cached_executions))) def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): @@ -440,7 +435,7 @@ def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): # Once rewriting is available, will want to rewrite before # evaluating execution cost. return tree_properties.is_trivially_executable( - self._sub_cache_subtrees(array_value.node) + self.replace_cached_subtrees(array_value.node) ) def _cache_with_cluster_cols( @@ -449,7 +444,7 @@ def _cache_with_cluster_cols( """Executes the query and uses the resulting table to rewrite future executions.""" sql, schema, ordering_info = self.compiler.compile_raw( - self._sub_cache_subtrees(array_value.node) + self.replace_cached_subtrees(array_value.node) ) tmp_table = self._sql_as_cached_temp_table( sql, @@ -466,7 +461,9 @@ def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): """Executes the query and uses the resulting table to rewrite future executions.""" offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") w_offsets, offset_column = array_value.promote_offsets() - sql = self.compiler.compile_unordered(self._sub_cache_subtrees(w_offsets.node)) + sql = self.compiler.compile_unordered( + self.replace_cached_subtrees(w_offsets.node) + ) tmp_table = self._sql_as_cached_temp_table( sql, @@ -502,7 +499,7 @@ def _simplify_with_caching(self, array_value: bigframes.core.ArrayValue): """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" # Apply existing caching first for _ in range(MAX_SUBTREE_FACTORINGS): - node_with_cache = self._sub_cache_subtrees(array_value.node) + node_with_cache = self.replace_cached_subtrees(array_value.node) if node_with_cache.planning_complexity < QUERY_COMPLEXITY_LIMIT: return @@ -559,7 +556,7 @@ def _validate_result_schema( ): actual_schema = tuple(bq_schema) ibis_schema = bigframes.core.compile.test_only_ibis_inferred_schema( - self._sub_cache_subtrees(array_value.node) + self.replace_cached_subtrees(array_value.node) ) internal_schema = array_value.schema if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: @@ -575,20 +572,7 @@ def _validate_result_schema( def generate_head_plan(node: nodes.BigFrameNode, n: int): - offsets_id = bigframes.core.guid.generate_guid("offsets_") - plan_w_offsets = nodes.PromoteOffsetsNode( - node, bigframes.core.identifiers.ColumnId(offsets_id) - ) - predicate = ops.lt_op.as_expr(ex.deref(offsets_id), ex.const(n)) - plan_w_head = nodes.FilterNode(plan_w_offsets, predicate) - # Finally, drop the offsets column - return nodes.SelectionNode( - plan_w_head, - tuple( - (ex.deref(i), bigframes.core.identifiers.ColumnId(i)) - for i in node.schema.names - ), - ) + return nodes.SliceNode(node, start=None, stop=n) def generate_row_count_plan(node: nodes.BigFrameNode): diff --git a/tests/unit/core/test_rewrite.py b/tests/unit/core/test_rewrite.py new file mode 100644 index 00000000000..0965238fcd6 --- /dev/null +++ b/tests/unit/core/test_rewrite.py @@ -0,0 +1,57 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest.mock as mock + +import google.cloud.bigquery + +import bigframes.core as core +import bigframes.core.nodes as nodes +import bigframes.core.rewrite as rewrites +import bigframes.core.schema + +TABLE_REF = google.cloud.bigquery.TableReference.from_string("project.dataset.table") +SCHEMA = ( + google.cloud.bigquery.SchemaField("col_a", "INTEGER"), + google.cloud.bigquery.SchemaField("col_b", "INTEGER"), +) +TABLE = google.cloud.bigquery.Table( + table_ref=TABLE_REF, + schema=SCHEMA, +) +FAKE_SESSION = mock.create_autospec(bigframes.Session, instance=True) +type(FAKE_SESSION)._strictly_ordered = mock.PropertyMock(return_value=True) +LEAF = core.ArrayValue.from_table( + session=FAKE_SESSION, + table=TABLE, + schema=bigframes.core.schema.ArraySchema.from_bq_table(TABLE), +).node + + +def test_rewrite_noop_slice(): + slice = nodes.SliceNode(LEAF, None, None) + result = rewrites.rewrite_slice(slice) + assert result == LEAF + + +def test_rewrite_reverse_slice(): + slice = nodes.SliceNode(LEAF, None, None, -1) + result = rewrites.rewrite_slice(slice) + assert result == nodes.ReversedNode(LEAF) + + +def test_rewrite_filter_slice(): + slice = nodes.SliceNode(LEAF, None, 2) + result = rewrites.rewrite_slice(slice) + assert list(result.fields) == list(LEAF.fields) + assert isinstance(result.child, nodes.FilterNode) diff --git a/tests/unit/core/test_slices.py b/tests/unit/core/test_slices.py new file mode 100644 index 00000000000..745db45eab6 --- /dev/null +++ b/tests/unit/core/test_slices.py @@ -0,0 +1,61 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.core.slices as slices + + +@pytest.mark.parametrize( + ["slice", "input_rows", "expected"], + [ + ((1, 2, 3), 3, 1), + ((-3, 400, None), 401, 2), + ((5, 505, None), 300, 295), + ((1, 10, 4), 10, 3), + ((1, 9, 4), 10, 2), + ((-1, -10, -4), 10, 3), + ((-1, -10, 4), 10, 0), + ((99, 100, 1), 9, 0), + ], +) +def test_slice_row_count(slice, input_rows, expected): + assert expected == slices.slice_output_rows(slice, input_rows) + + +@pytest.mark.parametrize( + ["slice", "input_rows", "expected"], + [ + ((1, 2, 3), 3, (1, 2, 3)), + ((-3, 400, None), 401, (-3, 400, None)), + ((5, 505, None), 300, (5, None, None)), + ((99, 100, 1), 9, (99, None, None)), + ], +) +def test_remove_unused_parts(slice, input_rows, expected): + assert expected == slices.remove_unused_parts(slice, input_rows) + + +@pytest.mark.parametrize( + ["slice", "input_rows", "expected"], + [ + ((1, 2, 3), 3, (1, 2, 3)), + ((-3, 400, None), 401, (398, 400, 1)), + ((5, 505, None), 300, (5, 300, 1)), + ((None, None, None), 300, (0, None, 1)), + ((None, None, -1), 300, (299, None, -1)), + ], +) +def test_to_forward_offsets(slice, input_rows, expected): + assert expected == slices.to_forward_offsets(slice, input_rows) From 863d6942eaf0cc435c3b76dc5d579c68fd478aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Fri, 18 Oct 2024 14:28:46 -0500 Subject: [PATCH 920/921] feat: add `bigframes.bigquery.create_vector_index` to assist in creating vector index on `ARRAY` columns (#1024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add `bigframes.bigquery.create_vector_index` to assist in creating vector index on `ARRAY` columns * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix lint errors * format * fix mypy * fix test for older google-cloud-bigquery * fix type error * fix typing * use googlesql.identifier to escape column and table ids * wait for job to finish --------- Co-authored-by: Owl Bot --- bigframes/bigquery/__init__.py | 508 +----------------- bigframes/bigquery/_operations/__init__.py | 13 + bigframes/bigquery/_operations/approx_agg.py | 59 ++ bigframes/bigquery/_operations/array.py | 151 ++++++ bigframes/bigquery/_operations/json.py | 136 +++++ bigframes/bigquery/_operations/search.py | 245 +++++++++ bigframes/bigquery/_operations/struct.py | 63 +++ bigframes/core/sql.py | 45 +- tests/system/conftest.py | 5 + .../small/bigquery/test_vector_search.py | 96 ++++ 10 files changed, 836 insertions(+), 485 deletions(-) create mode 100644 bigframes/bigquery/_operations/__init__.py create mode 100644 bigframes/bigquery/_operations/approx_agg.py create mode 100644 bigframes/bigquery/_operations/array.py create mode 100644 bigframes/bigquery/_operations/json.py create mode 100644 bigframes/bigquery/_operations/search.py create mode 100644 bigframes/bigquery/_operations/struct.py diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 847ed5eccb7..0b2d2d5aebc 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -12,489 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. - """This module integrates BigQuery built-in functions for use with DataFrame objects, such as array functions: https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions. """ - -from __future__ import annotations - -import typing -from typing import Literal, Optional, Union - -import bigframes_vendored.constants as constants - -import bigframes.core.groupby as groupby -import bigframes.core.sql -import bigframes.ml.utils as utils -import bigframes.operations as ops -import bigframes.operations.aggregations as agg_ops -import bigframes.series - -if typing.TYPE_CHECKING: - import bigframes.dataframe as dataframe - import bigframes.series as series - - -# Array functions defined from -# https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions - - -def array_length(series: series.Series) -> series.Series: - """Compute the length of each array element in the Series. - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> bpd.options.display.progress_bar = None - - >>> s = bpd.Series([[1, 2, 8, 3], [], [3, 4]]) - >>> bbq.array_length(s) - 0 4 - 1 0 - 2 2 - dtype: Int64 - - You can also apply this function directly to Series. - - >>> s.apply(bbq.array_length, by_row=False) - 0 4 - 1 0 - 2 2 - dtype: Int64 - - Args: - series (bigframes.series.Series): A Series with array columns. - - Returns: - bigframes.series.Series: A Series of integer values indicating - the length of each element in the Series. - - """ - return series._apply_unary_op(ops.len_op) - - -def array_agg( - obj: groupby.SeriesGroupBy | groupby.DataFrameGroupBy, -) -> series.Series | dataframe.DataFrame: - """Group data and create arrays from selected columns, omitting NULLs to avoid - BigQuery errors (NULLs not allowed in arrays). - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> import numpy as np - >>> bpd.options.display.progress_bar = None - - For a SeriesGroupBy object: - - >>> lst = ['a', 'a', 'b', 'b', 'a'] - >>> s = bpd.Series([1, 2, 3, 4, np.nan], index=lst) - >>> bbq.array_agg(s.groupby(level=0)) - a [1. 2.] - b [3. 4.] - dtype: list[pyarrow] - - For a DataFrameGroupBy object: - - >>> l = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]] - >>> df = bpd.DataFrame(l, columns=["a", "b", "c"]) - >>> bbq.array_agg(df.groupby(by=["b"])) - a c - b - 1.0 [2] [3] - 2.0 [1 1] [3 2] - - [2 rows x 2 columns] - - Args: - obj (groupby.SeriesGroupBy | groupby.DataFrameGroupBy): - A GroupBy object to be applied the function. - - Returns: - bigframes.series.Series | bigframes.dataframe.DataFrame: A Series or - DataFrame containing aggregated array columns, and indexed by the - original group columns. - """ - if isinstance(obj, groupby.SeriesGroupBy): - return obj._aggregate(agg_ops.ArrayAggOp()) - elif isinstance(obj, groupby.DataFrameGroupBy): - return obj._aggregate_all(agg_ops.ArrayAggOp(), numeric_only=False) - else: - raise ValueError( - f"Unsupported type {type(obj)} to apply `array_agg` function. {constants.FEEDBACK_LINK}" - ) - - -def array_to_string(series: series.Series, delimiter: str) -> series.Series: - """Converts array elements within a Series into delimited strings. - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> import numpy as np - >>> bpd.options.display.progress_bar = None - - >>> s = bpd.Series([["H", "i", "!"], ["Hello", "World"], np.nan, [], ["Hi"]]) - >>> bbq.array_to_string(s, delimiter=", ") - 0 H, i, ! - 1 Hello, World - 2 - 3 - 4 Hi - dtype: string - - Args: - series (bigframes.series.Series): A Series containing arrays. - delimiter (str): The string used to separate array elements. - - Returns: - bigframes.series.Series: A Series containing delimited strings. - - """ - return series._apply_unary_op(ops.ArrayToStringOp(delimiter=delimiter)) - - -# JSON functions defined from -# https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions - - -def json_set( - series: series.Series, - json_path_value_pairs: typing.Sequence[typing.Tuple[str, typing.Any]], -) -> series.Series: - """Produces a new JSON value within a Series by inserting or replacing values at - specified paths. - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> import numpy as np - >>> bpd.options.display.progress_bar = None - - >>> s = bpd.read_gbq("SELECT JSON '{\\\"a\\\": 1}' AS data")["data"] - >>> bbq.json_set(s, json_path_value_pairs=[("$.a", 100), ("$.b", "hi")]) - 0 {"a":100,"b":"hi"} - Name: data, dtype: string - - Args: - series (bigframes.series.Series): - The Series containing JSON data (as native JSON objects or JSON-formatted strings). - json_path_value_pairs (Sequence[Tuple[str, typing.Any]]): - Pairs of JSON path and the new value to insert/replace. - - Returns: - bigframes.series.Series: A new Series with the transformed JSON data. - - """ - # SQLGlot parser does not support the "create_if_missing => true" syntax, so - # create_if_missing is not currently implemented. - - for json_path_value_pair in json_path_value_pairs: - if len(json_path_value_pair) != 2: - raise ValueError( - "Incorrect format: Expected (, ), but found: " - + f"{json_path_value_pair}" - ) - - json_path, json_value = json_path_value_pair - series = series._apply_binary_op( - json_value, ops.JSONSet(json_path=json_path), alignment="left" - ) - return series - - -def json_extract( - series: series.Series, - json_path: str, -) -> series.Series: - """Extracts a JSON value and converts it to a SQL JSON-formatted `STRING` or `JSON` - value. This function uses single quotes and brackets to escape invalid JSONPath - characters in JSON keys. - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> bpd.options.display.progress_bar = None - - >>> s = bpd.Series(['{"class": {"students": [{"id": 5}, {"id": 12}]}}']) - >>> bbq.json_extract(s, json_path="$.class") - 0 {"students":[{"id":5},{"id":12}]} - dtype: string - - Args: - series (bigframes.series.Series): - The Series containing JSON data (as native JSON objects or JSON-formatted strings). - json_path (str): - The JSON path identifying the data that you want to obtain from the input. - - Returns: - bigframes.series.Series: A new Series with the JSON or JSON-formatted STRING. - """ - return series._apply_unary_op(ops.JSONExtract(json_path=json_path)) - - -def json_extract_array( - series: series.Series, - json_path: str = "$", -) -> series.Series: - """Extracts a JSON array and converts it to a SQL array of JSON-formatted `STRING` or `JSON` - values. This function uses single quotes and brackets to escape invalid JSONPath - characters in JSON keys. - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> bpd.options.display.progress_bar = None - - >>> s = bpd.Series(['[1, 2, 3]', '[4, 5]']) - >>> bbq.json_extract_array(s) - 0 ['1' '2' '3'] - 1 ['4' '5'] - dtype: list[pyarrow] - - Args: - series (bigframes.series.Series): - The Series containing JSON data (as native JSON objects or JSON-formatted strings). - json_path (str): - The JSON path identifying the data that you want to obtain from the input. - - Returns: - bigframes.series.Series: A new Series with the JSON or JSON-formatted STRING. - """ - return series._apply_unary_op(ops.JSONExtractArray(json_path=json_path)) - - -# Approximate aggrgate functions defined from -# https://cloud.google.com/bigquery/docs/reference/standard-sql/approximate_aggregate_functions - - -def approx_top_count( - series: series.Series, - number: int, -) -> series.Series: - """Returns the approximate top elements of `expression` as an array of STRUCTs. - The number parameter specifies the number of elements returned. - - Each `STRUCT` contains two fields. The first field (named `value`) contains an input - value. The second field (named `count`) contains an `INT64` specifying the number - of times the value was returned. - - Returns `NULL` if there are zero input rows. - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> bpd.options.display.progress_bar = None - >>> s = bpd.Series(["apple", "apple", "pear", "pear", "pear", "banana"]) - >>> bbq.approx_top_count(s, number=2) - [{'value': 'pear', 'count': 3}, {'value': 'apple', 'count': 2}] - - Args: - series (bigframes.series.Series): - The Series with any data type that the `GROUP BY` clause supports. - number (int): - An integer specifying the number of times the value was returned. - - Returns: - bigframes.series.Series: A new Series with the result data. - """ - if number < 1: - raise ValueError("The number of approx_top_count must be at least 1") - return series._apply_aggregation(agg_ops.ApproxTopCountOp(number=number)) - - -def struct(value: dataframe.DataFrame) -> series.Series: - """Takes a DataFrame and converts it into a Series of structs with each - struct entry corresponding to a DataFrame row and each struct field - corresponding to a DataFrame column - - **Examples:** - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> import bigframes.series as series - >>> bpd.options.display.progress_bar = None - - >>> srs = series.Series([{"version": 1, "project": "pandas"}, {"version": 2, "project": "numpy"},]) - >>> df = srs.struct.explode() - >>> bbq.struct(df) - 0 {'project': 'pandas', 'version': 1} - 1 {'project': 'numpy', 'version': 2} - dtype: struct[pyarrow] - - Args: - value (bigframes.dataframe.DataFrame): - The DataFrame to be converted to a Series of structs - - Returns: - bigframes.series.Series: A new Series with struct entries representing rows of the original DataFrame - """ - block = value._block - block, result_id = block.apply_nary_op( - block.value_columns, ops.StructOp(column_names=tuple(block.column_labels)) - ) - block = block.select_column(result_id) - return bigframes.series.Series(block) - - -# Search functions defined from -# https://cloud.google.com/bigquery/docs/reference/standard-sql/search_functions - - -def vector_search( - base_table: str, - column_to_search: str, - query: Union[dataframe.DataFrame, series.Series], - *, - query_column_to_search: Optional[str] = None, - top_k: Optional[int] = 10, - distance_type: Literal["euclidean", "cosine"] = "euclidean", - fraction_lists_to_search: Optional[float] = None, - use_brute_force: bool = False, -) -> dataframe.DataFrame: - """ - Conduct vector search which searches embeddings to find semantically similar entities. - - **Examples:** - - - >>> import bigframes.pandas as bpd - >>> import bigframes.bigquery as bbq - >>> bpd.options.display.progress_bar = None - - DataFrame embeddings for which to find nearest neighbors. The ``ARRAY`` column - is used as the search query: - - >>> search_query = bpd.DataFrame({"query_id": ["dog", "cat"], - ... "embedding": [[1.0, 2.0], [3.0, 5.2]]}) - >>> bbq.vector_search( - ... base_table="bigframes-dev.bigframes_tests_sys.base_table", - ... column_to_search="my_embedding", - ... query=search_query, - ... top_k=2) - query_id embedding id my_embedding distance - 1 cat [3. 5.2] 5 [5. 5.4] 2.009975 - 0 dog [1. 2.] 1 [1. 2.] 0.0 - 0 dog [1. 2.] 4 [1. 3.2] 1.2 - 1 cat [3. 5.2] 2 [2. 4.] 1.56205 - - [4 rows x 5 columns] - - Series embeddings for which to find nearest neighbors: - - >>> search_query = bpd.Series([[1.0, 2.0], [3.0, 5.2]], - ... index=["dog", "cat"], - ... name="embedding") - >>> bbq.vector_search( - ... base_table="bigframes-dev.bigframes_tests_sys.base_table", - ... column_to_search="my_embedding", - ... query=search_query, - ... top_k=2) - embedding id my_embedding distance - dog [1. 2.] 1 [1. 2.] 0.0 - cat [3. 5.2] 5 [5. 5.4] 2.009975 - dog [1. 2.] 4 [1. 3.2] 1.2 - cat [3. 5.2] 2 [2. 4.] 1.56205 - - [4 rows x 4 columns] - - You can specify the name of the column in the query DataFrame embeddings and distance type. - If you specify query_column_to_search_value, it will use the provided column which contains - the embeddings for which to find nearest neighbors. Otherwiese, it uses the column_to_search value. - - >>> search_query = bpd.DataFrame({"query_id": ["dog", "cat"], - ... "embedding": [[1.0, 2.0], [3.0, 5.2]], - ... "another_embedding": [[0.7, 2.2], [3.3, 5.2]]}) - >>> bbq.vector_search( - ... base_table="bigframes-dev.bigframes_tests_sys.base_table", - ... column_to_search="my_embedding", - ... query=search_query, - ... distance_type="cosine", - ... query_column_to_search="another_embedding", - ... top_k=2) - query_id embedding another_embedding id my_embedding distance - 1 cat [3. 5.2] [3.3 5.2] 2 [2. 4.] 0.005181 - 0 dog [1. 2.] [0.7 2.2] 4 [1. 3.2] 0.000013 - 1 cat [3. 5.2] [3.3 5.2] 1 [1. 2.] 0.005181 - 0 dog [1. 2.] [0.7 2.2] 3 [1.5 7. ] 0.004697 - - [4 rows x 6 columns] - - Args: - base_table (str): - The table to search for nearest neighbor embeddings. - column_to_search (str): - The name of the base table column to search for nearest neighbor embeddings. - The column must have a type of ``ARRAY``. All elements in the array must be non-NULL. - query (bigframes.dataframe.DataFrame | bigframes.dataframe.Series): - A Series or DataFrame that provides the embeddings for which to find nearest neighbors. - query_column_to_search (str): - Specifies the name of the column in the query that contains the embeddings for which to - find nearest neighbors. The column must have a type of ``ARRAY``. All elements in - the array must be non-NULL and all values in the column must have the same array dimensions - as the values in the ``column_to_search`` column. Can only be set when query is a DataFrame. - top_k (int, default 10): - Sepecifies the number of nearest neighbors to return. Default to 10. - distance_type (str, defalt "euclidean"): - Specifies the type of metric to use to compute the distance between two vectors. - Possible values are "euclidean" and "cosine". Default to "euclidean". - fraction_lists_to_search (float, range in [0.0, 1.0]): - Specifies the percentage of lists to search. Specifying a higher percentage leads to - higher recall and slower performance, and the converse is true when specifying a lower - percentage. It is only used when a vector index is also used. You can only specify - ``fraction_lists_to_search`` when ``use_brute_force`` is set to False. - use_brute_force (bool, default False): - Determines whether to use brute force search by skipping the vector index if one is available. - Default to False. - - Returns: - bigframes.dataframe.DataFrame: A DataFrame containing vector search result. - """ - if not fraction_lists_to_search and use_brute_force is True: - raise ValueError( - "You can't specify fraction_lists_to_search when use_brute_force is set to True." - ) - if ( - isinstance(query, bigframes.series.Series) - and query_column_to_search is not None - ): - raise ValueError( - "You can't specify query_column_to_search when query is a Series." - ) - # TODO(ashleyxu): Support options in vector search. b/344019989 - if fraction_lists_to_search is not None or use_brute_force is True: - raise NotImplementedError( - f"fraction_lists_to_search and use_brute_force is not supported. {constants.FEEDBACK_LINK}" - ) - options = { - "base_table": base_table, - "column_to_search": column_to_search, - "query_column_to_search": query_column_to_search, - "distance_type": distance_type, - "top_k": top_k, - "fraction_lists_to_search": fraction_lists_to_search, - "use_brute_force": use_brute_force, - } - - (query,) = utils.convert_to_dataframe(query) - sql_string, index_col_ids, index_labels = query._to_sql_query(include_index=True) - - sql = bigframes.core.sql.create_vector_search_sql( - sql_string=sql_string, options=options # type: ignore - ) - if index_col_ids is not None: - df = query._session.read_gbq(sql, index_col=index_col_ids) - df.index.names = index_labels - else: - df = query._session.read_gbq(sql) - - return df +from bigframes.bigquery._operations.approx_agg import approx_top_count +from bigframes.bigquery._operations.array import ( + array_agg, + array_length, + array_to_string, +) +from bigframes.bigquery._operations.json import ( + json_extract, + json_extract_array, + json_set, +) +from bigframes.bigquery._operations.search import create_vector_index, vector_search +from bigframes.bigquery._operations.struct import struct + +__all__ = [ + "array_length", + "array_agg", + "array_to_string", + "json_set", + "json_extract", + "json_extract_array", + "approx_top_count", + "struct", + "create_vector_index", + "vector_search", +] diff --git a/bigframes/bigquery/_operations/__init__.py b/bigframes/bigquery/_operations/__init__.py new file mode 100644 index 00000000000..6d5e14bcf4a --- /dev/null +++ b/bigframes/bigquery/_operations/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/bigframes/bigquery/_operations/approx_agg.py b/bigframes/bigquery/_operations/approx_agg.py new file mode 100644 index 00000000000..696f8f5a66f --- /dev/null +++ b/bigframes/bigquery/_operations/approx_agg.py @@ -0,0 +1,59 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import bigframes.operations.aggregations as agg_ops +import bigframes.series as series + +""" +Approximate functions defined from +https://cloud.google.com/bigquery/docs/reference/standard-sql/approximate_aggregate_functions +""" + + +def approx_top_count( + series: series.Series, + number: int, +) -> series.Series: + """Returns the approximate top elements of `expression` as an array of STRUCTs. + The number parameter specifies the number of elements returned. + + Each `STRUCT` contains two fields. The first field (named `value`) contains an input + value. The second field (named `count`) contains an `INT64` specifying the number + of times the value was returned. + + Returns `NULL` if there are zero input rows. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + >>> s = bpd.Series(["apple", "apple", "pear", "pear", "pear", "banana"]) + >>> bbq.approx_top_count(s, number=2) + [{'value': 'pear', 'count': 3}, {'value': 'apple', 'count': 2}] + + Args: + series (bigframes.series.Series): + The Series with any data type that the `GROUP BY` clause supports. + number (int): + An integer specifying the number of times the value was returned. + + Returns: + bigframes.series.Series: A new Series with the result data. + """ + if number < 1: + raise ValueError("The number of approx_top_count must be at least 1") + return series._apply_aggregation(agg_ops.ApproxTopCountOp(number=number)) diff --git a/bigframes/bigquery/_operations/array.py b/bigframes/bigquery/_operations/array.py new file mode 100644 index 00000000000..4af14161274 --- /dev/null +++ b/bigframes/bigquery/_operations/array.py @@ -0,0 +1,151 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Array functions defined from +https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions +""" + + +from __future__ import annotations + +import typing + +import bigframes_vendored.constants as constants + +import bigframes.core.groupby as groupby +import bigframes.operations as ops +import bigframes.operations.aggregations as agg_ops +import bigframes.series as series + +if typing.TYPE_CHECKING: + import bigframes.dataframe as dataframe + + +def array_length(series: series.Series) -> series.Series: + """Compute the length of each array element in the Series. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([[1, 2, 8, 3], [], [3, 4]]) + >>> bbq.array_length(s) + 0 4 + 1 0 + 2 2 + dtype: Int64 + + You can also apply this function directly to Series. + + >>> s.apply(bbq.array_length, by_row=False) + 0 4 + 1 0 + 2 2 + dtype: Int64 + + Args: + series (bigframes.series.Series): A Series with array columns. + + Returns: + bigframes.series.Series: A Series of integer values indicating + the length of each element in the Series. + + """ + return series._apply_unary_op(ops.len_op) + + +def array_agg( + obj: groupby.SeriesGroupBy | groupby.DataFrameGroupBy, +) -> series.Series | dataframe.DataFrame: + """Group data and create arrays from selected columns, omitting NULLs to avoid + BigQuery errors (NULLs not allowed in arrays). + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + For a SeriesGroupBy object: + + >>> lst = ['a', 'a', 'b', 'b', 'a'] + >>> s = bpd.Series([1, 2, 3, 4, np.nan], index=lst) + >>> bbq.array_agg(s.groupby(level=0)) + a [1. 2.] + b [3. 4.] + dtype: list[pyarrow] + + For a DataFrameGroupBy object: + + >>> l = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]] + >>> df = bpd.DataFrame(l, columns=["a", "b", "c"]) + >>> bbq.array_agg(df.groupby(by=["b"])) + a c + b + 1.0 [2] [3] + 2.0 [1 1] [3 2] + + [2 rows x 2 columns] + + Args: + obj (groupby.SeriesGroupBy | groupby.DataFrameGroupBy): + A GroupBy object to be applied the function. + + Returns: + bigframes.series.Series | bigframes.dataframe.DataFrame: A Series or + DataFrame containing aggregated array columns, and indexed by the + original group columns. + """ + if isinstance(obj, groupby.SeriesGroupBy): + return obj._aggregate(agg_ops.ArrayAggOp()) + elif isinstance(obj, groupby.DataFrameGroupBy): + return obj._aggregate_all(agg_ops.ArrayAggOp(), numeric_only=False) + else: + raise ValueError( + f"Unsupported type {type(obj)} to apply `array_agg` function. {constants.FEEDBACK_LINK}" + ) + + +def array_to_string(series: series.Series, delimiter: str) -> series.Series: + """Converts array elements within a Series into delimited strings. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series([["H", "i", "!"], ["Hello", "World"], np.nan, [], ["Hi"]]) + >>> bbq.array_to_string(s, delimiter=", ") + 0 H, i, ! + 1 Hello, World + 2 + 3 + 4 Hi + dtype: string + + Args: + series (bigframes.series.Series): A Series containing arrays. + delimiter (str): The string used to separate array elements. + + Returns: + bigframes.series.Series: A Series containing delimited strings. + + """ + return series._apply_unary_op(ops.ArrayToStringOp(delimiter=delimiter)) diff --git a/bigframes/bigquery/_operations/json.py b/bigframes/bigquery/_operations/json.py new file mode 100644 index 00000000000..d3c3c97a9c7 --- /dev/null +++ b/bigframes/bigquery/_operations/json.py @@ -0,0 +1,136 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +JSON functions defined from +https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions +""" + + +from __future__ import annotations + +from typing import Any, Sequence, Tuple + +import bigframes.operations as ops +import bigframes.series as series + + +def json_set( + series: series.Series, + json_path_value_pairs: Sequence[Tuple[str, Any]], +) -> series.Series: + """Produces a new JSON value within a Series by inserting or replacing values at + specified paths. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import numpy as np + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.read_gbq("SELECT JSON '{\\\"a\\\": 1}' AS data")["data"] + >>> bbq.json_set(s, json_path_value_pairs=[("$.a", 100), ("$.b", "hi")]) + 0 {"a":100,"b":"hi"} + Name: data, dtype: string + + Args: + series (bigframes.series.Series): + The Series containing JSON data (as native JSON objects or JSON-formatted strings). + json_path_value_pairs (Sequence[Tuple[str, Any]]): + Pairs of JSON path and the new value to insert/replace. + + Returns: + bigframes.series.Series: A new Series with the transformed JSON data. + + """ + # SQLGlot parser does not support the "create_if_missing => true" syntax, so + # create_if_missing is not currently implemented. + + for json_path_value_pair in json_path_value_pairs: + if len(json_path_value_pair) != 2: + raise ValueError( + "Incorrect format: Expected (, ), but found: " + + f"{json_path_value_pair}" + ) + + json_path, json_value = json_path_value_pair + series = series._apply_binary_op( + json_value, ops.JSONSet(json_path=json_path), alignment="left" + ) + return series + + +def json_extract( + series: series.Series, + json_path: str, +) -> series.Series: + """Extracts a JSON value and converts it to a SQL JSON-formatted `STRING` or `JSON` + value. This function uses single quotes and brackets to escape invalid JSONPath + characters in JSON keys. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['{"class": {"students": [{"id": 5}, {"id": 12}]}}']) + >>> bbq.json_extract(s, json_path="$.class") + 0 {"students":[{"id":5},{"id":12}]} + dtype: string + + Args: + series (bigframes.series.Series): + The Series containing JSON data (as native JSON objects or JSON-formatted strings). + json_path (str): + The JSON path identifying the data that you want to obtain from the input. + + Returns: + bigframes.series.Series: A new Series with the JSON or JSON-formatted STRING. + """ + return series._apply_unary_op(ops.JSONExtract(json_path=json_path)) + + +def json_extract_array( + series: series.Series, + json_path: str = "$", +) -> series.Series: + """Extracts a JSON array and converts it to a SQL array of JSON-formatted `STRING` or `JSON` + values. This function uses single quotes and brackets to escape invalid JSONPath + characters in JSON keys. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + >>> s = bpd.Series(['[1, 2, 3]', '[4, 5]']) + >>> bbq.json_extract_array(s) + 0 ['1' '2' '3'] + 1 ['4' '5'] + dtype: list[pyarrow] + + Args: + series (bigframes.series.Series): + The Series containing JSON data (as native JSON objects or JSON-formatted strings). + json_path (str): + The JSON path identifying the data that you want to obtain from the input. + + Returns: + bigframes.series.Series: A new Series with the JSON or JSON-formatted STRING. + """ + return series._apply_unary_op(ops.JSONExtractArray(json_path=json_path)) diff --git a/bigframes/bigquery/_operations/search.py b/bigframes/bigquery/_operations/search.py new file mode 100644 index 00000000000..496e259944d --- /dev/null +++ b/bigframes/bigquery/_operations/search.py @@ -0,0 +1,245 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import typing +from typing import Collection, Literal, Mapping, Optional, Union + +import bigframes_vendored.constants as constants +import google.cloud.bigquery as bigquery + +import bigframes.core.sql +import bigframes.ml.utils as utils + +if typing.TYPE_CHECKING: + import bigframes.dataframe as dataframe + import bigframes.series as series + import bigframes.session + +""" +Search functions defined from +https://cloud.google.com/bigquery/docs/reference/standard-sql/search_functions +""" + + +def create_vector_index( + table_id: str, + column_name: str, + *, + replace: bool = False, + index_name: Optional[str] = None, + distance_type="cosine", + stored_column_names: Collection[str] = (), + index_type: str = "ivf", + ivf_options: Optional[Mapping] = None, + tree_ah_options: Optional[Mapping] = None, + session: Optional[bigframes.session.Session] = None, +) -> None: + """ + Creates a new vector index on a column of a table. + + This method calls the `CREATE VECTOR INDEX DDL statement + `_. + + """ + import bigframes.pandas + + if index_name is None: + table_ref = bigquery.TableReference.from_string(table_id) + index_name = table_ref.table_id + + options = { + "index_type": index_type.upper(), + "distance_type": distance_type.upper(), + } + + if ivf_options is not None: + options["ivf_options"] = json.dumps(ivf_options) + + if tree_ah_options is not None: + options["tree_ah_options"] = json.dumps(tree_ah_options) + + sql = bigframes.core.sql.create_vector_index_ddl( + replace=replace, + index_name=index_name, + table_name=table_id, + column_name=column_name, + stored_column_names=stored_column_names, + options=options, + ) + + # Use global read_gbq to execute this for better location autodetection. + if session is None: + read_gbq_query = bigframes.pandas.read_gbq_query + else: + read_gbq_query = session.read_gbq_query + + read_gbq_query(sql) + + +def vector_search( + base_table: str, + column_to_search: str, + query: Union[dataframe.DataFrame, series.Series], + *, + query_column_to_search: Optional[str] = None, + top_k: Optional[int] = 10, + distance_type: Literal["euclidean", "cosine"] = "euclidean", + fraction_lists_to_search: Optional[float] = None, + use_brute_force: bool = False, +) -> dataframe.DataFrame: + """ + Conduct vector search which searches embeddings to find semantically similar entities. + + This method calls the `VECTOR_SEARCH() SQL function + `_. + + **Examples:** + + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> bpd.options.display.progress_bar = None + + DataFrame embeddings for which to find nearest neighbors. The ``ARRAY`` column + is used as the search query: + + >>> search_query = bpd.DataFrame({"query_id": ["dog", "cat"], + ... "embedding": [[1.0, 2.0], [3.0, 5.2]]}) + >>> bbq.vector_search( + ... base_table="bigframes-dev.bigframes_tests_sys.base_table", + ... column_to_search="my_embedding", + ... query=search_query, + ... top_k=2) + query_id embedding id my_embedding distance + 1 cat [3. 5.2] 5 [5. 5.4] 2.009975 + 0 dog [1. 2.] 1 [1. 2.] 0.0 + 0 dog [1. 2.] 4 [1. 3.2] 1.2 + 1 cat [3. 5.2] 2 [2. 4.] 1.56205 + + [4 rows x 5 columns] + + Series embeddings for which to find nearest neighbors: + + >>> search_query = bpd.Series([[1.0, 2.0], [3.0, 5.2]], + ... index=["dog", "cat"], + ... name="embedding") + >>> bbq.vector_search( + ... base_table="bigframes-dev.bigframes_tests_sys.base_table", + ... column_to_search="my_embedding", + ... query=search_query, + ... top_k=2) + embedding id my_embedding distance + dog [1. 2.] 1 [1. 2.] 0.0 + cat [3. 5.2] 5 [5. 5.4] 2.009975 + dog [1. 2.] 4 [1. 3.2] 1.2 + cat [3. 5.2] 2 [2. 4.] 1.56205 + + [4 rows x 4 columns] + + You can specify the name of the column in the query DataFrame embeddings and distance type. + If you specify query_column_to_search_value, it will use the provided column which contains + the embeddings for which to find nearest neighbors. Otherwiese, it uses the column_to_search value. + + >>> search_query = bpd.DataFrame({"query_id": ["dog", "cat"], + ... "embedding": [[1.0, 2.0], [3.0, 5.2]], + ... "another_embedding": [[0.7, 2.2], [3.3, 5.2]]}) + >>> bbq.vector_search( + ... base_table="bigframes-dev.bigframes_tests_sys.base_table", + ... column_to_search="my_embedding", + ... query=search_query, + ... distance_type="cosine", + ... query_column_to_search="another_embedding", + ... top_k=2) + query_id embedding another_embedding id my_embedding distance + 1 cat [3. 5.2] [3.3 5.2] 2 [2. 4.] 0.005181 + 0 dog [1. 2.] [0.7 2.2] 4 [1. 3.2] 0.000013 + 1 cat [3. 5.2] [3.3 5.2] 1 [1. 2.] 0.005181 + 0 dog [1. 2.] [0.7 2.2] 3 [1.5 7. ] 0.004697 + + [4 rows x 6 columns] + + Args: + base_table (str): + The table to search for nearest neighbor embeddings. + column_to_search (str): + The name of the base table column to search for nearest neighbor embeddings. + The column must have a type of ``ARRAY``. All elements in the array must be non-NULL. + query (bigframes.dataframe.DataFrame | bigframes.dataframe.Series): + A Series or DataFrame that provides the embeddings for which to find nearest neighbors. + query_column_to_search (str): + Specifies the name of the column in the query that contains the embeddings for which to + find nearest neighbors. The column must have a type of ``ARRAY``. All elements in + the array must be non-NULL and all values in the column must have the same array dimensions + as the values in the ``column_to_search`` column. Can only be set when query is a DataFrame. + top_k (int, default 10): + Sepecifies the number of nearest neighbors to return. Default to 10. + distance_type (str, defalt "euclidean"): + Specifies the type of metric to use to compute the distance between two vectors. + Possible values are "euclidean" and "cosine". Default to "euclidean". + fraction_lists_to_search (float, range in [0.0, 1.0]): + Specifies the percentage of lists to search. Specifying a higher percentage leads to + higher recall and slower performance, and the converse is true when specifying a lower + percentage. It is only used when a vector index is also used. You can only specify + ``fraction_lists_to_search`` when ``use_brute_force`` is set to False. + use_brute_force (bool, default False): + Determines whether to use brute force search by skipping the vector index if one is available. + Default to False. + + Returns: + bigframes.dataframe.DataFrame: A DataFrame containing vector search result. + """ + import bigframes.series + + if not fraction_lists_to_search and use_brute_force is True: + raise ValueError( + "You can't specify fraction_lists_to_search when use_brute_force is set to True." + ) + if ( + isinstance(query, bigframes.series.Series) + and query_column_to_search is not None + ): + raise ValueError( + "You can't specify query_column_to_search when query is a Series." + ) + # TODO(ashleyxu): Support options in vector search. b/344019989 + if fraction_lists_to_search is not None or use_brute_force is True: + raise NotImplementedError( + f"fraction_lists_to_search and use_brute_force is not supported. {constants.FEEDBACK_LINK}" + ) + options = { + "base_table": base_table, + "column_to_search": column_to_search, + "query_column_to_search": query_column_to_search, + "distance_type": distance_type, + "top_k": top_k, + "fraction_lists_to_search": fraction_lists_to_search, + "use_brute_force": use_brute_force, + } + + (query,) = utils.convert_to_dataframe(query) + sql_string, index_col_ids, index_labels = query._to_sql_query(include_index=True) + + sql = bigframes.core.sql.create_vector_search_sql( + sql_string=sql_string, options=options # type: ignore + ) + if index_col_ids is not None: + df = query._session.read_gbq(sql, index_col=index_col_ids) + df.index.names = index_labels + else: + df = query._session.read_gbq(sql) + + return df diff --git a/bigframes/bigquery/_operations/struct.py b/bigframes/bigquery/_operations/struct.py new file mode 100644 index 00000000000..7cb826351c1 --- /dev/null +++ b/bigframes/bigquery/_operations/struct.py @@ -0,0 +1,63 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""This module integrates BigQuery built-in functions for use with DataFrame objects, +such as array functions: +https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions. """ + + +from __future__ import annotations + +import typing + +import bigframes.operations as ops +import bigframes.series as series + +if typing.TYPE_CHECKING: + import bigframes.dataframe as dataframe + + +def struct(value: dataframe.DataFrame) -> series.Series: + """Takes a DataFrame and converts it into a Series of structs with each + struct entry corresponding to a DataFrame row and each struct field + corresponding to a DataFrame column + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> import bigframes.series as series + >>> bpd.options.display.progress_bar = None + + >>> srs = series.Series([{"version": 1, "project": "pandas"}, {"version": 2, "project": "numpy"},]) + >>> df = srs.struct.explode() + >>> bbq.struct(df) + 0 {'project': 'pandas', 'version': 1} + 1 {'project': 'numpy', 'version': 2} + dtype: struct[pyarrow] + + Args: + value (bigframes.dataframe.DataFrame): + The DataFrame to be converted to a Series of structs + + Returns: + bigframes.series.Series: A new Series with struct entries representing rows of the original DataFrame + """ + block = value._block + block, result_id = block.apply_nary_op( + block.value_columns, ops.StructOp(column_names=tuple(block.column_labels)) + ) + block = block.select_column(result_id) + return series.Series(block) diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index e44091e7b17..d5dfc64ddd5 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -19,7 +19,7 @@ import datetime import math -from typing import Iterable, Mapping, TYPE_CHECKING, Union +from typing import cast, Collection, Iterable, Mapping, TYPE_CHECKING, Union import bigframes.core.compile.googlesql as googlesql @@ -116,6 +116,45 @@ def ordering_clause( return f"ORDER BY {' ,'.join(parts)}" +def create_vector_index_ddl( + *, + replace: bool, + index_name: str, + table_name: str, + column_name: str, + stored_column_names: Collection[str], + options: Mapping[str, Union[str | int | bool | float]] = {}, +) -> str: + """Encode the VECTOR INDEX statement for BigQuery Vector Search.""" + + if replace: + create = "CREATE OR REPLACE VECTOR INDEX " + else: + create = "CREATE VECTOR INDEX IF NOT EXISTS " + + if len(stored_column_names) > 0: + escaped_stored = [ + f"{googlesql.identifier(name)}" for name in stored_column_names + ] + storing = f"STORING({', '.join(escaped_stored)}) " + else: + storing = "" + + rendered_options = ", ".join( + [ + f"{option_name} = {simple_literal(option_value)}" + for option_name, option_value in options.items() + ] + ) + + return f""" + {create} {googlesql.identifier(index_name)} + ON {googlesql.identifier(table_name)}({googlesql.identifier(column_name)}) + {storing} + OPTIONS({rendered_options}); + """ + + def create_vector_search_sql( sql_string: str, options: Mapping[str, Union[str | int | bool | float]] = {}, @@ -135,7 +174,7 @@ def create_vector_search_sql( base.*, distance, FROM VECTOR_SEARCH( - TABLE `{base_table}`, + TABLE {googlesql.identifier(cast(str, base_table))}, {simple_literal(column_to_search)}, ({sql_string}), {simple_literal(query_column_to_search)}, @@ -150,7 +189,7 @@ def create_vector_search_sql( base.*, distance, FROM VECTOR_SEARCH( - TABLE `{base_table}`, + TABLE {googlesql.identifier(cast(str, base_table))}, {simple_literal(column_to_search)}, ({sql_string}), distance_type => {simple_literal(distance_type)}, diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 217cf71e0cd..ba8f350c73d 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -241,6 +241,11 @@ def dataset_id_permanent_tokyo( return dataset_id +@pytest.fixture(scope="session") +def table_id_not_created(dataset_id: str): + return f"{dataset_id}.{prefixer.create_prefix()}" + + @pytest.fixture(scope="session") def scalars_schema(bigquery_client: bigquery.Client): # TODO(swast): Add missing scalar data types such as BIGNUMERIC. diff --git a/tests/system/small/bigquery/test_vector_search.py b/tests/system/small/bigquery/test_vector_search.py index 4280c0a888f..b6a6d59c4cd 100644 --- a/tests/system/small/bigquery/test_vector_search.py +++ b/tests/system/small/bigquery/test_vector_search.py @@ -12,12 +12,108 @@ # See the License for the specific language governing permissions and # limitations under the License. +import random +from typing import Any, cast, Dict, Iterable + +import google.cloud.bigquery import numpy as np import pandas as pd +import pyarrow +import pytest import bigframes.bigquery as bbq import bigframes.pandas as bpd +# Need at least 5,000 rows to create a vector index. +VECTOR_DF = pd.DataFrame( + { + "rowid": np.arange(9_999), + # 3D values, clustered around the three unit vector axes. + "my_embedding": pd.Series( + [ + [ + 1 + (random.random() - 0.5) if (row % 3) == 0 else 0, + 1 + (random.random() - 0.5) if (row % 3) == 1 else 0, + 1 + (random.random() - 0.5) if (row % 3) == 2 else 0, + ] + for row in range(9_999) + ], + dtype=pd.ArrowDtype(pyarrow.list_(pyarrow.float64())), + ), + # Three groups of animal, vegetable, and mineral, corresponding to + # the embeddings above. + "mystery_word": [ + "aarvark", + "broccoli", + "calcium", + "dog", + "eggplant", + "ferrite", + "gopher", + "huckleberry", + "ice", + ] + * 1_111, + }, +) + + +@pytest.fixture +def vector_table_id( + bigquery_client: google.cloud.bigquery.Client, + # Use non-US location to ensure location autodetection works. + table_id_not_created: str, +): + table = google.cloud.bigquery.Table( + table_id_not_created, + [ + {"name": "rowid", "type": "INT64"}, + {"name": "my_embedding", "type": "FLOAT64", "mode": "REPEATED"}, + {"name": "mystery_word", "type": "STRING"}, + ], + ) + bigquery_client.create_table(table) + bigquery_client.load_table_from_json( + cast(Iterable[Dict[str, Any]], VECTOR_DF.to_dict(orient="records")), + table_id_not_created, + ).result() + yield table_id_not_created + bigquery_client.delete_table(table_id_not_created, not_found_ok=True) + + +def test_create_vector_index_ivf( + session, vector_table_id: str, bigquery_client: google.cloud.bigquery.Client +): + bbq.create_vector_index( + vector_table_id, + "my_embedding", + distance_type="cosine", + stored_column_names=["mystery_word"], + index_type="ivf", + ivf_options={"num_lists": 3}, + session=session, + ) + + # Check that the index was created successfully. + project_id, dataset_id, table_name = vector_table_id.split(".") + indexes = bigquery_client.query_and_wait( + f""" + SELECT index_catalog, index_schema, table_name, index_name, index_column_name + FROM `{project_id}`.`{dataset_id}`.INFORMATION_SCHEMA.VECTOR_INDEX_COLUMNS + WHERE table_name = '{table_name}'; + """ + ).to_dataframe() + + # There should only be one vector index. + assert len(indexes.index) == 1 + assert indexes["index_catalog"].iloc[0] == project_id + assert indexes["index_schema"].iloc[0] == dataset_id + assert indexes["table_name"].iloc[0] == table_name + assert indexes["index_column_name"].iloc[0] == "my_embedding" + + # If no name is specified, use the table name as the index name + assert indexes["index_name"].iloc[0] == table_name + def test_vector_search_basic_params_with_df(): search_query = bpd.DataFrame( From 62a88e87f55f9cc109aa38f4b7ac10dd45ca41fd Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Fri, 18 Oct 2024 13:22:24 -0700 Subject: [PATCH 921/921] docs: show best practice of closing the session to cleanup resources in sample notebooks (#1095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BEGIN_COMMIT_OVERRIDE docs: show best practice of closing the session to cleanup resources in sample notebooks (#1095) test: include stale cloud functions cleanup in the default nox run (#1095) END_COMMIT_OVERRIDE Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- bigframes/core/global_session.py | 2 +- .../bq_dataframes_llm_code_generation.ipynb | 9 +++++++ .../getting_started_bq_dataframes.ipynb | 10 +++++++ notebooks/location/regionalized.ipynb | 27 +++++++++++++++---- .../remote_function_usecases.ipynb | 18 ++++++++++++- .../remote_function_vertex_claude_model.ipynb | 11 +++++++- noxfile.py | 26 +++++++++--------- scripts/manage_cloud_functions.py | 17 ++++++++++-- 8 files changed, 97 insertions(+), 23 deletions(-) diff --git a/bigframes/core/global_session.py b/bigframes/core/global_session.py index 3187c5c11bf..e70cdad59eb 100644 --- a/bigframes/core/global_session.py +++ b/bigframes/core/global_session.py @@ -30,7 +30,7 @@ _global_session_state.thread_local_session = None -def _try_close_session(session): +def _try_close_session(session: bigframes.session.Session): """Try to close the session and warn if couldn't.""" try: session.close() diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index c0c3c58a3c5..f56472f1b5f 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -1217,6 +1217,15 @@ "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bf.close_session()" + ] + }, { "cell_type": "code", "execution_count": 24, diff --git a/notebooks/getting_started/getting_started_bq_dataframes.ipynb b/notebooks/getting_started/getting_started_bq_dataframes.ipynb index b59ccbb8ac8..e79db455fc7 100644 --- a/notebooks/getting_started/getting_started_bq_dataframes.ipynb +++ b/notebooks/getting_started/getting_started_bq_dataframes.ipynb @@ -1704,6 +1704,16 @@ "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Delete the temporary cloud artifacts created during the bigframes session \n", + "bpd.close_session()" + ] + }, { "cell_type": "code", "execution_count": 25, diff --git a/notebooks/location/regionalized.ipynb b/notebooks/location/regionalized.ipynb index c383a226097..5a8239a42a0 100644 --- a/notebooks/location/regionalized.ipynb +++ b/notebooks/location/regionalized.ipynb @@ -7,8 +7,9 @@ "source": [ "# README\n", "\n", - "This Notebook runs differently depending on the following environent variable:\n", - "1. BIGQUERY_LOCATION - can take values as per https://cloud.google.com/bigquery/docs/locations, e.g. `us`, `asia-east1`" + "This Notebook runs requiring the following environent variable:\n", + "1. GOOGLE_CLOUD_PROJECT - The google cloud project id.\n", + "1. BIGQUERY_LOCATION - can take values as per https://cloud.google.com/bigquery/docs/locations, e.g. `us`, `asia-east1`." ] }, { @@ -1420,8 +1421,8 @@ } ], "source": [ - "import bigframes.pandas as pd\n", - "help(pd.remote_function)" + "import bigframes.pandas as bpd\n", + "help(bpd.remote_function)" ] }, { @@ -1460,7 +1461,7 @@ } ], "source": [ - "@pd.remote_function([float], str, bigquery_connection='bigframes-rf-conn')\n", + "@bpd.remote_function([float], str, bigquery_connection='bigframes-rf-conn')\n", "def get_bucket(num):\n", " if not num: return \"NA\"\n", " boundary = 4000\n", @@ -2784,6 +2785,22 @@ "source": [ "model.to_gbq(f\"{DATASET}.penguins_model\", replace=True)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Clean Up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bpd.close_session()" + ] } ], "metadata": { diff --git a/notebooks/remote_functions/remote_function_usecases.ipynb b/notebooks/remote_functions/remote_function_usecases.ipynb index 9317e4b8fe1..b897def4e83 100644 --- a/notebooks/remote_functions/remote_function_usecases.ipynb +++ b/notebooks/remote_functions/remote_function_usecases.ipynb @@ -25,7 +25,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Setup" + "# Set Up" ] }, { @@ -1379,6 +1379,22 @@ "df1 = df.assign(duration_cat=df[\"duration_minutes\"].apply(duration_category))\n", "df1.peek()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Clean Up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bpd.close_session()" + ] } ], "metadata": { diff --git a/notebooks/remote_functions/remote_function_vertex_claude_model.ipynb b/notebooks/remote_functions/remote_function_vertex_claude_model.ipynb index 650bb92e508..a5769a22856 100644 --- a/notebooks/remote_functions/remote_function_vertex_claude_model.ipynb +++ b/notebooks/remote_functions/remote_function_vertex_claude_model.ipynb @@ -452,12 +452,21 @@ "df" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Clean Up" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "bpd.close_session()" + ] } ], "metadata": { diff --git a/noxfile.py b/noxfile.py index 3ab271179e0..f537005e57e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -105,6 +105,7 @@ "system-3.9", "system-3.12", "cover", + "cleanup", ] # Error if a python version is missing @@ -952,25 +953,24 @@ def release_dry_run(session): def cleanup(session): """Clean up stale and/or temporary resources in the test project.""" google_cloud_project = os.getenv("GOOGLE_CLOUD_PROJECT") - if not google_cloud_project: - session.error( - "Set GOOGLE_CLOUD_PROJECT environment variable to run notebook session." - ) + cleanup_options = [] + if google_cloud_project: + cleanup_options.append(f"--project-id={google_cloud_project}") # Cleanup a few stale (more than 12 hours old) temporary cloud run # functions created by bigframems. This will help keeping the test GCP # project within the "Number of functions" quota # https://cloud.google.com/functions/quotas#resource_limits recency_cutoff_hours = 12 - cleanup_count_per_location = 10 + cleanup_count_per_location = 20 + cleanup_options.extend( + [ + f"--recency-cutoff={recency_cutoff_hours}", + "cleanup", + f"--number={cleanup_count_per_location}", + ] + ) session.install("-e", ".") - session.run( - "python", - "scripts/manage_cloud_functions.py", - f"--project-id={google_cloud_project}", - f"--recency-cutoff={recency_cutoff_hours}", - "cleanup", - f"--number={cleanup_count_per_location}", - ) + session.run("python", "scripts/manage_cloud_functions.py", *cleanup_options) diff --git a/scripts/manage_cloud_functions.py b/scripts/manage_cloud_functions.py index 33af8463c91..145e178f4da 100644 --- a/scripts/manage_cloud_functions.py +++ b/scripts/manage_cloud_functions.py @@ -153,6 +153,12 @@ def list_str(values): return [val for val in values.split(",") if val] +def get_project_from_environment(): + from google.cloud import bigquery + + return bigquery.Client().project + + if __name__ == "__main__": parser = argparse.ArgumentParser( description="Manage cloud functions created to serve bigframes remote functions." @@ -161,9 +167,10 @@ def list_str(values): "-p", "--project-id", type=str, - required=True, + required=False, action="store", - help="GCP project-id.", + help="GCP project-id. If not provided, the project-id resolved by the" + " BigQuery client from the user environment would be used.", ) parser.add_argument( "-r", @@ -212,4 +219,10 @@ def hours_to_timedelta(hrs): parser_cleanup.set_defaults(func=cleanup_gcfs) args = parser.parse_args(sys.argv[1:]) + if args.project_id is None: + args.project_id = get_project_from_environment() + if args.project_id is None: + raise ValueError( + "Could not resolve a project. Plese set it via --project-id option." + ) args.func(args)